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 { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||||
import { EventEmitter } from 'events';
|
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';
|
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>(
|
function apiRequest<T>(
|
||||||
method: string,
|
method: string,
|
||||||
path: string,
|
path: string,
|
||||||
|
|
@ -218,12 +247,7 @@ export const taskCreateTool = {
|
||||||
_ctx: ExtensionContext,
|
_ctx: ExtensionContext,
|
||||||
): Promise<AgentToolResult<unknown>> {
|
): Promise<AgentToolResult<unknown>> {
|
||||||
try {
|
try {
|
||||||
const body: Record<string, unknown> = {
|
const body = buildTaskCreateBody(params);
|
||||||
title: params.title,
|
|
||||||
description: params.description,
|
|
||||||
priority: params.priority ?? 'medium',
|
|
||||||
};
|
|
||||||
if (params.assigned_to) body.assigned_to = params.assigned_to;
|
|
||||||
const res = await apiRequest<{ task?: { id: string }; error?: string }>(
|
const res = await apiRequest<{ task?: { id: string }; error?: string }>(
|
||||||
'POST',
|
'POST',
|
||||||
'/api/controlplane/tasks',
|
'/api/controlplane/tasks',
|
||||||
|
|
|
||||||
|
|
@ -597,6 +597,10 @@ export async function runJailAgent(
|
||||||
NODE_ENV: process.env.NODE_ENV ?? 'production',
|
NODE_ENV: process.env.NODE_ENV ?? 'production',
|
||||||
...readSecrets(),
|
...readSecrets(),
|
||||||
TENANT_ID,
|
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 =
|
const controlplaneApiKey =
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,11 @@ import { defineConfig } from 'vitest/config';
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
test: {
|
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