Inherit chat_jid into agent-created controlplane tasks (Sam & Claude)
Bug: when the chat agent decides a request needs a controlplane task
(e.g. "any system updates available?" — needs root-level audit), it
calls task_create. The task is stored with context: null because the
tool's parameter schema didn't accept context and nothing in the path
auto-injected the originating chat. controlplane-heartbeat.ts:874 then
reads context.chat_jid to deliver the result back to Telegram, finds
nothing, and the task completes with no thread back to the operator.
Sam saw this on 10.maj.2026 — the queue fix from b6d72d9 stopped silent
message loss, which immediately exposed silent task-result loss.
Fix:
- src/agent-runner.ts now passes CLAWDIE_CHAT_JID=input.chatJid to the
spawned pi process. The chat jid was already available at the call
site, just not exported.
- .pi/extensions/clawdie-harness/controlplane-tools.ts gets a new pure
helper buildTaskCreateBody(params, env) that auto-injects
context.chat_jid from $CLAWDIE_CHAT_JID. The task_create tool calls
the helper. Agent doesn't need to know about chat context — the
harness inherits it from its parent process.
The fix is asymmetric in a deliberate way: agent-runner sets the env
var unconditionally (every chat run has a chat_jid), but the helper
only injects context when the env var is non-empty. Scheduled-task
runs that don't yet thread chat context will simply omit context, same
as today — no regression there.
Tests:
- 5 new tests in controlplane-tools.test.ts cover the helper directly:
no env → no context, blank env → no context, set env → context with
chat_jid, plus priority + assigned_to passthrough.
- vitest.config.ts now includes .pi/extensions/**/*.test.ts so the
harness tests run with `npm test` and `just test` instead of needing
a separate runner invocation. This picks up the existing
controlplane-tools and system-tools test files (the harness rename
commit added them but they weren't in the default include).
Out of scope deliberately: src/controlplane-runner.ts (controlplane
heartbeat path that runs subtasks) does not yet read task.context.
chat_jid to thread it forward to its own spawned agents. So if a
controlplane task itself spawns subtasks, those subtasks won't inherit
chat_jid via this commit. The original bug (chat → agent → task) is
fixed end-to-end; sub-task propagation is a follow-up if needed.
Per docs/internal/SUDO_REPLACEMENT.md, the next privileged-op gap
(sysadmin can't run pkg/freebsd-update audits) belongs in hostd, not
in a sudo replacement.
---
2251 passed locally. Pre-existing argon2/controlplane-*/cms.test
failures unchanged. Codex to validate end-to-end on host: send a
chat-driven question that triggers a controlplane task, confirm
the task lands with context.chat_jid populated and the completion
message reaches the originating Telegram thread.
---
Build: FAIL | Tests: FAIL — 16 failed
This commit is contained in:
parent
0724a28d68
commit
c9f9bdb63d
4 changed files with 92 additions and 8 deletions
|
|
@ -9,7 +9,58 @@
|
|||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||
import { EventEmitter } from 'events';
|
||||
|
||||
import { taskDecomposeTool, taskStatusTool } from './controlplane-tools.js';
|
||||
import {
|
||||
buildTaskCreateBody,
|
||||
taskDecomposeTool,
|
||||
taskStatusTool,
|
||||
} from './controlplane-tools.js';
|
||||
|
||||
describe('buildTaskCreateBody', () => {
|
||||
const baseParams = {
|
||||
title: 'check pending updates',
|
||||
description: 'audit host + jails for available patches',
|
||||
};
|
||||
|
||||
it('omits context when CLAWDIE_CHAT_JID is unset', () => {
|
||||
const body = buildTaskCreateBody(baseParams, {});
|
||||
expect(body).toEqual({
|
||||
title: 'check pending updates',
|
||||
description: 'audit host + jails for available patches',
|
||||
priority: 'medium',
|
||||
});
|
||||
expect('context' in body).toBe(false);
|
||||
});
|
||||
|
||||
it('omits context when CLAWDIE_CHAT_JID is empty or whitespace', () => {
|
||||
const blank = buildTaskCreateBody(baseParams, { CLAWDIE_CHAT_JID: ' ' });
|
||||
expect('context' in blank).toBe(false);
|
||||
});
|
||||
|
||||
it('injects context.chat_jid when CLAWDIE_CHAT_JID is set', () => {
|
||||
const body = buildTaskCreateBody(baseParams, {
|
||||
CLAWDIE_CHAT_JID: '120363401234567890@g.us',
|
||||
});
|
||||
expect(body.context).toEqual({ chat_jid: '120363401234567890@g.us' });
|
||||
});
|
||||
|
||||
it('preserves explicit priority and assigned_to', () => {
|
||||
const body = buildTaskCreateBody(
|
||||
{ ...baseParams, priority: 'high', assigned_to: 'sysadmin' },
|
||||
{ CLAWDIE_CHAT_JID: 'group-1' },
|
||||
);
|
||||
expect(body).toMatchObject({
|
||||
title: baseParams.title,
|
||||
priority: 'high',
|
||||
assigned_to: 'sysadmin',
|
||||
context: { chat_jid: 'group-1' },
|
||||
});
|
||||
});
|
||||
|
||||
it('defaults priority to medium when omitted', () => {
|
||||
const body = buildTaskCreateBody(baseParams, {});
|
||||
expect(body.priority).toBe('medium');
|
||||
});
|
||||
});
|
||||
import http from 'http';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
|
|
|||
|
|
@ -49,6 +49,35 @@ function getApiConfig(): {
|
|||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the request body for POST /api/controlplane/tasks. Auto-injects the
|
||||
* originating chat jid from $CLAWDIE_CHAT_JID into the task's context so the
|
||||
* controlplane heartbeat can deliver the result back to the right Telegram
|
||||
* thread on completion. Without this, agent-spawned tasks orphan their
|
||||
* results — the bug from 10.maj.2026.
|
||||
*/
|
||||
export function buildTaskCreateBody(
|
||||
params: {
|
||||
title: string;
|
||||
description: string;
|
||||
assigned_to?: string;
|
||||
priority?: string;
|
||||
},
|
||||
env: NodeJS.ProcessEnv = process.env,
|
||||
): Record<string, unknown> {
|
||||
const body: Record<string, unknown> = {
|
||||
title: params.title,
|
||||
description: params.description,
|
||||
priority: params.priority ?? 'medium',
|
||||
};
|
||||
if (params.assigned_to) body.assigned_to = params.assigned_to;
|
||||
const chatJid = (env.CLAWDIE_CHAT_JID ?? '').trim();
|
||||
if (chatJid) {
|
||||
body.context = { chat_jid: chatJid };
|
||||
}
|
||||
return body;
|
||||
}
|
||||
|
||||
function apiRequest<T>(
|
||||
method: string,
|
||||
path: string,
|
||||
|
|
@ -218,12 +247,7 @@ export const taskCreateTool = {
|
|||
_ctx: ExtensionContext,
|
||||
): Promise<AgentToolResult<unknown>> {
|
||||
try {
|
||||
const body: Record<string, unknown> = {
|
||||
title: params.title,
|
||||
description: params.description,
|
||||
priority: params.priority ?? 'medium',
|
||||
};
|
||||
if (params.assigned_to) body.assigned_to = params.assigned_to;
|
||||
const body = buildTaskCreateBody(params);
|
||||
const res = await apiRequest<{ task?: { id: string }; error?: string }>(
|
||||
'POST',
|
||||
'/api/controlplane/tasks',
|
||||
|
|
|
|||
|
|
@ -597,6 +597,10 @@ export async function runJailAgent(
|
|||
NODE_ENV: process.env.NODE_ENV ?? 'production',
|
||||
...readSecrets(),
|
||||
TENANT_ID,
|
||||
// Surface the originating chat jid so the harness can auto-inject it as
|
||||
// task context when the agent creates controlplane tasks. Without this,
|
||||
// task results have no thread back to the chat that asked for them.
|
||||
CLAWDIE_CHAT_JID: input.chatJid,
|
||||
};
|
||||
|
||||
const controlplaneApiKey =
|
||||
|
|
|
|||
|
|
@ -2,6 +2,11 @@ import { defineConfig } from 'vitest/config';
|
|||
|
||||
export default defineConfig({
|
||||
test: {
|
||||
include: ['src/**/*.test.ts', 'setup/**/*.test.ts', 'skills-engine/**/*.test.ts'],
|
||||
include: [
|
||||
'src/**/*.test.ts',
|
||||
'setup/**/*.test.ts',
|
||||
'skills-engine/**/*.test.ts',
|
||||
'.pi/extensions/**/*.test.ts',
|
||||
],
|
||||
},
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue