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,
* state update (activateWorker), and audit logging.
*/
import { execFile } from "node:child_process";
import { randomUUID } from "node:crypto";
import { execFile, spawn } from "node:child_process";
import fs from "node:fs/promises";
import path from "node:path";
import { promisify } from "node:util";
@@ -40,6 +39,8 @@ export type DispatchOpts = {
transitionLabel: (issueId: number, from: string, to: string) => Promise<void>;
/** Plugin config for model resolution */
pluginConfig?: Record<string, unknown>;
/** Orchestrator's session key (used as spawnedBy for subagent tracking) */
sessionKey?: string;
};
export type DispatchResult = {
@@ -201,7 +202,7 @@ export async function dispatchTask(
try {
if (sessionAction === "spawn") {
sessionKey = `agent:${agentId ?? "unknown"}:subagent:${randomUUID()}`;
sessionKey = `agent:${agentId ?? "unknown"}:subagent:${project.name}-${role}-${modelAlias}`;
await execFileAsync(
"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",
[
"gateway",
"call",
"agent",
"--params",
JSON.stringify({
idempotencyKey: randomUUID(),
sessionId: sessionKey!,
message: taskMessage,
}),
gatewayParams,
"--expect-final",
"--json",
],
{ timeout: 30_000 },
{ detached: true, stdio: "ignore" },
);
child.unref();
dispatched = true;

View File

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

View File

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

View File

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