feat: add sessionKey to dispatch options and related tools for subagent tracking

This commit is contained in:
Lauren ten Hoor
2026-02-10 16:58:37 +08:00
parent 04f4f6ebc6
commit 9e3279872a
4 changed files with 32 additions and 10 deletions

View File

@@ -4,8 +4,7 @@
* Handles: session lookup, spawn/reuse via Gateway RPC, task dispatch via CLI, * Handles: session lookup, spawn/reuse via Gateway RPC, task dispatch via CLI,
* state update (activateWorker), and audit logging. * state update (activateWorker), and audit logging.
*/ */
import { execFile } from "node:child_process"; import { execFile, spawn } from "node:child_process";
import { randomUUID } from "node:crypto";
import fs from "node:fs/promises"; import fs from "node:fs/promises";
import path from "node:path"; import path from "node:path";
import { promisify } from "node:util"; import { promisify } from "node:util";
@@ -40,6 +39,8 @@ export type DispatchOpts = {
transitionLabel: (issueId: number, from: string, to: string) => Promise<void>; transitionLabel: (issueId: number, from: string, to: string) => Promise<void>;
/** Plugin config for model resolution */ /** Plugin config for model resolution */
pluginConfig?: Record<string, unknown>; pluginConfig?: Record<string, unknown>;
/** Orchestrator's session key (used as spawnedBy for subagent tracking) */
sessionKey?: string;
}; };
export type DispatchResult = { export type DispatchResult = {
@@ -201,7 +202,7 @@ export async function dispatchTask(
try { try {
if (sessionAction === "spawn") { if (sessionAction === "spawn") {
sessionKey = `agent:${agentId ?? "unknown"}:subagent:${randomUUID()}`; sessionKey = `agent:${agentId ?? "unknown"}:subagent:${project.name}-${role}-${modelAlias}`;
await execFileAsync( await execFileAsync(
"openclaw", "openclaw",
[ [
@@ -215,21 +216,38 @@ export async function dispatchTask(
); );
} }
await execFileAsync( // Dispatch via `gateway call agent --expect-final` as a detached background process.
// Without --expect-final the gateway accepts but never processes the request.
// Running with --expect-final in a detached process ensures the agent runs
// while task_pickup returns immediately.
// Using the gateway RPC (not `openclaw agent` CLI) lets us set lane, spawnedBy,
// and deliver — matching the official sessions_spawn internals.
const orchestratorSessionKey = opts.sessionKey;
const gatewayParams = JSON.stringify({
idempotencyKey: `devclaw-${project.name}-${issueId}-${role}-${Date.now()}`,
agentId: agentId ?? "devclaw",
sessionKey: sessionKey!,
message: taskMessage,
deliver: false,
lane: "subagent",
...(orchestratorSessionKey
? { spawnedBy: orchestratorSessionKey }
: {}),
});
const child = spawn(
"openclaw", "openclaw",
[ [
"gateway", "gateway",
"call", "call",
"agent", "agent",
"--params", "--params",
JSON.stringify({ gatewayParams,
idempotencyKey: randomUUID(), "--expect-final",
sessionId: sessionKey!, "--json",
message: taskMessage,
}),
], ],
{ timeout: 30_000 }, { detached: true, stdio: "ignore" },
); );
child.unref();
dispatched = true; dispatched = true;

View File

@@ -510,6 +510,7 @@ export function createHeartbeatTickTool(api: OpenClawPluginApi) {
transitionLabel: (id, from, to) => transitionLabel: (id, from, to) =>
provider.transitionLabel(id, from as StateLabel, to as StateLabel), provider.transitionLabel(id, from as StateLabel, to as StateLabel),
pluginConfig, pluginConfig,
sessionKey: ctx.sessionKey,
}); });
result.pickups.push({ result.pickups.push({

View File

@@ -165,6 +165,7 @@ export function createTaskCompleteTool(api: OpenClawPluginApi) {
to as StateLabel, to as StateLabel,
), ),
pluginConfig, pluginConfig,
sessionKey: ctx.sessionKey,
}); });
output.autoChain = { output.autoChain = {
dispatched: true, dispatched: true,
@@ -239,6 +240,7 @@ export function createTaskCompleteTool(api: OpenClawPluginApi) {
to as StateLabel, to as StateLabel,
), ),
pluginConfig, pluginConfig,
sessionKey: ctx.sessionKey,
}); });
output.autoChain = { output.autoChain = {
dispatched: true, dispatched: true,

View File

@@ -311,6 +311,7 @@ export function createTaskPickupTool(api: OpenClawPluginApi) {
transitionLabel: (id, from, to) => transitionLabel: (id, from, to) =>
provider.transitionLabel(id, from as StateLabel, to as StateLabel), provider.transitionLabel(id, from as StateLabel, to as StateLabel),
pluginConfig, pluginConfig,
sessionKey: ctx.sessionKey,
}); });
// 8b. Explicitly update worker state in projects.json // 8b. Explicitly update worker state in projects.json