From c9f9bdb63df92b69c7e2fadddb00d973c07fcf1d Mon Sep 17 00:00:00 2001 From: Operator & Claude Code Date: Sun, 10 May 2026 08:57:27 +0200 Subject: [PATCH] Inherit chat_jid into agent-created controlplane tasks (Sam & Claude) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .../controlplane-tools.test.ts | 53 ++++++++++++++++++- .../clawdie-harness/controlplane-tools.ts | 36 ++++++++++--- src/agent-runner.ts | 4 ++ vitest.config.ts | 7 ++- 4 files changed, 92 insertions(+), 8 deletions(-) diff --git a/.pi/extensions/clawdie-harness/controlplane-tools.test.ts b/.pi/extensions/clawdie-harness/controlplane-tools.test.ts index d284bf2..b115d93 100644 --- a/.pi/extensions/clawdie-harness/controlplane-tools.test.ts +++ b/.pi/extensions/clawdie-harness/controlplane-tools.test.ts @@ -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'; // --------------------------------------------------------------------------- diff --git a/.pi/extensions/clawdie-harness/controlplane-tools.ts b/.pi/extensions/clawdie-harness/controlplane-tools.ts index 0d8a918..8c2b26b 100644 --- a/.pi/extensions/clawdie-harness/controlplane-tools.ts +++ b/.pi/extensions/clawdie-harness/controlplane-tools.ts @@ -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 { + const body: Record = { + 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( method: string, path: string, @@ -218,12 +247,7 @@ export const taskCreateTool = { _ctx: ExtensionContext, ): Promise> { try { - const body: Record = { - 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', diff --git a/src/agent-runner.ts b/src/agent-runner.ts index defcdc1..bd83d72 100644 --- a/src/agent-runner.ts +++ b/src/agent-runner.ts @@ -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 = diff --git a/vitest.config.ts b/vitest.config.ts index 354e6a5..cc478ae 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -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', + ], }, });