@clawos-dev/clawd 0.2.141 → 0.2.142-beta.297.e4b56a8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.cjs +198 -41
- package/dist/dispatch/mcp-server.cjs +19 -15
- package/package.json +1 -1
package/dist/cli.cjs
CHANGED
|
@@ -5952,10 +5952,12 @@ var init_dispatch = __esm({
|
|
|
5952
5952
|
]);
|
|
5953
5953
|
DispatchRunArgsSchema = external_exports.object({
|
|
5954
5954
|
targetPersona: external_exports.string().min(1),
|
|
5955
|
-
prompt: external_exports.string()
|
|
5955
|
+
prompt: external_exports.string(),
|
|
5956
|
+
// mode='continue' 时 daemon 反查 (sourceSessionId, targetPersona) 最近 B session 复用;
|
|
5957
|
+
// 'new' 或 undefined(默认)= 当前行为,新建 B。
|
|
5958
|
+
mode: external_exports.enum(["new", "continue"]).optional()
|
|
5956
5959
|
});
|
|
5957
5960
|
DispatchCompleteArgsSchema = external_exports.object({
|
|
5958
|
-
dispatchId: external_exports.string().min(1),
|
|
5959
5961
|
outcome: DispatchOutcomeSchema
|
|
5960
5962
|
});
|
|
5961
5963
|
}
|
|
@@ -31802,8 +31804,6 @@ function buildSpawnContext(state, deps) {
|
|
|
31802
31804
|
env.CLAWD_SESSION_ID = file.sessionId;
|
|
31803
31805
|
const daemonUrl = deps.getDaemonUrl?.() ?? null;
|
|
31804
31806
|
if (daemonUrl) env.CLAWD_DAEMON_URL = daemonUrl;
|
|
31805
|
-
const dispatchId = deps.lookupDispatchByBSessionId?.(file.sessionId);
|
|
31806
|
-
if (dispatchId) env.CLAWD_DISPATCH_ID = dispatchId;
|
|
31807
31807
|
const dispatchMcpConfigPath2 = deps.getDispatchMcpConfigPath?.() ?? null;
|
|
31808
31808
|
const ctx = {
|
|
31809
31809
|
cwd: file.cwd,
|
|
@@ -32566,10 +32566,9 @@ var SessionRunner = class {
|
|
|
32566
32566
|
// 单栏 refactor (spec 2026-06-02 §5.1): cc 子进程 env 注入 CLAWD_DAEMON_URL,让 assistant
|
|
32567
32567
|
// curl daemon HTTP RPC adapter (/api/rpc/<method>) 触发管理操作。null = HTTP adapter 未启。
|
|
32568
32568
|
getDaemonUrl: this.hooks.getDaemonUrl,
|
|
32569
|
-
// Persona dispatch:透传 dispatch.mcp.json
|
|
32570
|
-
//
|
|
32569
|
+
// Persona dispatch:透传 dispatch.mcp.json 路径闭包;cc spawn 加 --mcp-config 让两侧
|
|
32570
|
+
// cc 都能看到 personaDispatch / personaDispatchComplete tool(按 session 身份区分用哪个)。
|
|
32571
32571
|
getDispatchMcpConfigPath: this.hooks.getDispatchMcpConfigPath,
|
|
32572
|
-
lookupDispatchByBSessionId: this.hooks.lookupDispatchByBSessionId,
|
|
32573
32572
|
// ReadyGate v2:透传 mode 让 reducer send / ready-detected 分支决定走暂存队列还是直写
|
|
32574
32573
|
mode: this.hooks.mode,
|
|
32575
32574
|
// [RG-DBG] 注入 logger 让 reducer rgDbg 走 pino 进 clawd.log;定位完跟 rgDbg 一起删
|
|
@@ -33041,9 +33040,22 @@ You may read this file with your Read tool to understand context. You do not hav
|
|
|
33041
33040
|
|
|
33042
33041
|
When done, call the MCP tool \`mcp__clawd-dispatch__personaDispatchComplete\` with your result:
|
|
33043
33042
|
- Success: { text: "...", filePaths?: ["abs/path", ...] }
|
|
33044
|
-
- Failure: { isFailure: true, reason: "..." }
|
|
33043
|
+
- Failure: { isFailure: true, reason: "..." }`;
|
|
33044
|
+
}
|
|
33045
|
+
function buildDispatchContinuationPack(args) {
|
|
33046
|
+
return `[Dispatched from owner \u2014 follow-up query]
|
|
33047
|
+
|
|
33048
|
+
Although you reported back on the previous task, the owner has a follow-up question. Continue from where you left off.
|
|
33045
33049
|
|
|
33046
|
-
|
|
33050
|
+
Owner's new message:
|
|
33051
|
+
${args.prompt}
|
|
33052
|
+
|
|
33053
|
+
Source conversation (jsonl, same path as before, may contain new messages):
|
|
33054
|
+
${args.sourceJsonlPath}
|
|
33055
|
+
|
|
33056
|
+
When done, call \`mcp__clawd-dispatch__personaDispatchComplete\` again with your result:
|
|
33057
|
+
- Success: { text: "...", filePaths?: ["abs/path", ...] }
|
|
33058
|
+
- Failure: { isFailure: true, reason: "..." }`;
|
|
33047
33059
|
}
|
|
33048
33060
|
function derivePersonaSpawnCwd(file, personaRoot) {
|
|
33049
33061
|
const personaId = file.ownerPersonaId;
|
|
@@ -33245,6 +33257,23 @@ var SessionManager = class {
|
|
|
33245
33257
|
const file = this.findOwnedSession(sessionId);
|
|
33246
33258
|
return file ? this.scopeForFile(file) : null;
|
|
33247
33259
|
}
|
|
33260
|
+
/**
|
|
33261
|
+
* 反查 (sourceSessionId, targetPersona) 最近一次被 dispatched 出的 B session。
|
|
33262
|
+
* mode='continue' 路径用:命中 → 复用该 B;不命中 → fallback 走新建路径。
|
|
33263
|
+
*
|
|
33264
|
+
* 反查源是 SessionStore 持久化字段(dispatchedFromSessionId + ownerPersonaId),不引入新表,
|
|
33265
|
+
* daemon 重启不丢。同对历史多条按 updatedAt 取最近(reducer 写入时持续更新)。
|
|
33266
|
+
*/
|
|
33267
|
+
findLatestDispatchedSession(args) {
|
|
33268
|
+
const candidates = this.listAllOwned().filter(
|
|
33269
|
+
(f) => f.dispatchedFromSessionId === args.sourceSessionId && f.ownerPersonaId === args.targetPersona
|
|
33270
|
+
);
|
|
33271
|
+
if (candidates.length === 0) return null;
|
|
33272
|
+
candidates.sort(
|
|
33273
|
+
(a, b2) => a.updatedAt > b2.updatedAt ? -1 : a.updatedAt < b2.updatedAt ? 1 : 0
|
|
33274
|
+
);
|
|
33275
|
+
return candidates[0];
|
|
33276
|
+
}
|
|
33248
33277
|
// 合并 default + 每个 persona 的 owner/ + guests/<capId>/ —— 桌面 App 主 session 列表入口.
|
|
33249
33278
|
// guest sessions 由 handlers/session.ts list 处按 ctx.principal.kind === 'guest' 做权限过滤
|
|
33250
33279
|
// (creatorPrincipalId === capId), 这里仅负责数据源汇总.
|
|
@@ -33351,10 +33380,9 @@ var SessionManager = class {
|
|
|
33351
33380
|
// 单栏 refactor (spec 2026-06-02 §5.1): 透传 daemon HTTP RPC base URL 闭包,
|
|
33352
33381
|
// reducer 把它注入 cc 子进程 env CLAWD_DAEMON_URL.
|
|
33353
33382
|
getDaemonUrl: this.deps.getDaemonUrl,
|
|
33354
|
-
// Persona dispatch
|
|
33355
|
-
//
|
|
33383
|
+
// Persona dispatch: dispatch.mcp.json 路径闭包透传给 reducer,让 cc spawn 加 --mcp-config flag。
|
|
33384
|
+
// dispatchId 不再走 cc env——complete handler 用 sessionId 反查 in-flight dispatchId 配对。
|
|
33356
33385
|
getDispatchMcpConfigPath: this.deps.dispatchMcpConfigPath ? () => this.deps.dispatchMcpConfigPath ?? null : void 0,
|
|
33357
|
-
lookupDispatchByBSessionId: this.deps.personaDispatchManager ? (bSid) => this.deps.personaDispatchManager?.lookupDispatchByBSessionId(bSid) : void 0,
|
|
33358
33386
|
// file-sharing (spec §6 PR 3):闭包 scope + sessionId,runner 只暴露 tool/relPath/cwd
|
|
33359
33387
|
onFileEdit: attachmentGroup ? (input) => attachmentGroup.onFileEdit({
|
|
33360
33388
|
scope,
|
|
@@ -34172,10 +34200,11 @@ var SessionManager = class {
|
|
|
34172
34200
|
* Persona dispatch: 启动 B session 并投递任务包当第一条 user message。
|
|
34173
34201
|
*
|
|
34174
34202
|
* - 复用 persona-owner 创建路径(cwd=personaDir、scope=persona-owner、不沙箱)
|
|
34175
|
-
* - SessionFile 上写 dispatchedFromSessionId 关联回 A(UI 折叠用;mirror 一侧 strip
|
|
34176
|
-
*
|
|
34177
|
-
*
|
|
34178
|
-
*
|
|
34203
|
+
* - SessionFile 上写 dispatchedFromSessionId 关联回 A(UI 折叠用;mirror 一侧 strip;
|
|
34204
|
+
* mode='continue' 路径 findLatestDispatchedSession 也按此字段反查复用)
|
|
34205
|
+
* - 跟 PersonaDispatchManager 登记 dispatchId → B sessionId(complete handler 用
|
|
34206
|
+
* findInflightDispatchByBSessionId 反查回 dispatchId 配对 A 的 waiter)
|
|
34207
|
+
* - 投任务包;包尾教 B 用 personaDispatchComplete tool 回传
|
|
34179
34208
|
*
|
|
34180
34209
|
* cwd 派生与 SessionManager.create 的 owner persona 路径一致(derivePersonaSpawnCwd)。
|
|
34181
34210
|
*/
|
|
@@ -34218,8 +34247,7 @@ var SessionManager = class {
|
|
|
34218
34247
|
});
|
|
34219
34248
|
const taskPack = buildDispatchTaskPack({
|
|
34220
34249
|
prompt: args.prompt,
|
|
34221
|
-
sourceJsonlPath: args.sourceJsonlPath
|
|
34222
|
-
dispatchId: args.dispatchId
|
|
34250
|
+
sourceJsonlPath: args.sourceJsonlPath
|
|
34223
34251
|
});
|
|
34224
34252
|
runner.input({ kind: "command", command: { kind: "send", text: taskPack } });
|
|
34225
34253
|
this.deps.logger?.info("dispatch.createDispatchedSession.task-pack-sent", {
|
|
@@ -34228,6 +34256,41 @@ var SessionManager = class {
|
|
|
34228
34256
|
});
|
|
34229
34257
|
return { sessionId };
|
|
34230
34258
|
}
|
|
34259
|
+
/**
|
|
34260
|
+
* mode='continue' 复用已有 B session,投 continuation pack 给 runner。
|
|
34261
|
+
*
|
|
34262
|
+
* 关键差异 vs createDispatchedSession:
|
|
34263
|
+
* - 不 newSessionId() / 不 storeFor(scope).write()——SessionFile 复用
|
|
34264
|
+
* - 跟 PersonaDispatchManager 重新登记 dispatchId → bSessionId(新 dispatchId、同 bSessionId)
|
|
34265
|
+
* - ensureRunnerForScope 在 runner 不存在时建;存在则复用
|
|
34266
|
+
* - runner.input send 投 continuationPack:runner / reducer 内部决定 cc 死活;
|
|
34267
|
+
* cc 活 → 自然续;cc 死 → spawn 时 jsonl 已存在自动 --resume <toolSessionId>
|
|
34268
|
+
*/
|
|
34269
|
+
resumeDispatchedSession(args) {
|
|
34270
|
+
if (!this.deps.personaDispatchManager) {
|
|
34271
|
+
throw new Error("resumeDispatchedSession: personaDispatchManager missing in ManagerDeps");
|
|
34272
|
+
}
|
|
34273
|
+
const file = this.findOwnedSession(args.bSessionId);
|
|
34274
|
+
if (!file) {
|
|
34275
|
+
throw new Error(`resumeDispatchedSession: B session ${args.bSessionId} not found`);
|
|
34276
|
+
}
|
|
34277
|
+
const scope = this.scopeForFile(file);
|
|
34278
|
+
this.deps.personaDispatchManager.registerBSession(args.dispatchId, args.bSessionId);
|
|
34279
|
+
this.deps.logger?.info("dispatch.resumeDispatchedSession.registered", {
|
|
34280
|
+
dispatchId: args.dispatchId,
|
|
34281
|
+
bSessionId: args.bSessionId
|
|
34282
|
+
});
|
|
34283
|
+
const runner = this.ensureRunnerForScope(file, scope);
|
|
34284
|
+
const pack = buildDispatchContinuationPack({
|
|
34285
|
+
prompt: args.prompt,
|
|
34286
|
+
sourceJsonlPath: args.sourceJsonlPath
|
|
34287
|
+
});
|
|
34288
|
+
runner.input({ kind: "command", command: { kind: "send", text: pack } });
|
|
34289
|
+
this.deps.logger?.info("dispatch.resumeDispatchedSession.continuation-sent", {
|
|
34290
|
+
dispatchId: args.dispatchId,
|
|
34291
|
+
bSessionId: args.bSessionId
|
|
34292
|
+
});
|
|
34293
|
+
}
|
|
34231
34294
|
/**
|
|
34232
34295
|
* 老板插话:把 'inject-owner-text' input 喂给对应 runner,
|
|
34233
34296
|
* runner 不存在时按 scope 懒创建。
|
|
@@ -36929,6 +36992,7 @@ var PersonaDispatchManager = class {
|
|
|
36929
36992
|
dispatchId,
|
|
36930
36993
|
sourceSessionId: args.sourceSessionId,
|
|
36931
36994
|
targetPersona: args.targetPersona,
|
|
36995
|
+
bSessionId: args.bSessionId,
|
|
36932
36996
|
waiters: [],
|
|
36933
36997
|
outcome: null
|
|
36934
36998
|
});
|
|
@@ -36959,10 +37023,22 @@ var PersonaDispatchManager = class {
|
|
|
36959
37023
|
if (!state) throw new Error(`unknown dispatchId: ${dispatchId}`);
|
|
36960
37024
|
state.bSessionId = bSessionId;
|
|
36961
37025
|
}
|
|
36962
|
-
/**
|
|
36963
|
-
|
|
37026
|
+
/**
|
|
37027
|
+
* 按 B sessionId 找当前 in-flight(未 resolved)的 dispatchId。
|
|
37028
|
+
* complete handler 用这个把 B 的 outcome 配对回 A 的 waiter——dispatchId 不再走 cc env,
|
|
37029
|
+
* 全靠 (bSessionId → in-flight dispatchId) 反查。
|
|
37030
|
+
*
|
|
37031
|
+
* 同一 bSessionId 不会同时有多个 in-flight(A 拿不到 outcome 不会发起新 dispatch;
|
|
37032
|
+
* mode='continue' 入口已防御重入)。
|
|
37033
|
+
*
|
|
37034
|
+
* 历史 entry(已 resolved)暂不清——单 daemon lifetime 累积可接受;未来用量大了加 TTL GC
|
|
37035
|
+
* (complete 后 5min 删 entry)。
|
|
37036
|
+
*/
|
|
37037
|
+
findInflightDispatchByBSessionId(bSessionId) {
|
|
36964
37038
|
for (const state of this.map.values()) {
|
|
36965
|
-
if (state.bSessionId === bSessionId
|
|
37039
|
+
if (state.bSessionId === bSessionId && state.outcome === null) {
|
|
37040
|
+
return state.dispatchId;
|
|
37041
|
+
}
|
|
36966
37042
|
}
|
|
36967
37043
|
return void 0;
|
|
36968
37044
|
}
|
|
@@ -36994,6 +37070,15 @@ function writeDispatchMcpConfig(args) {
|
|
|
36994
37070
|
|
|
36995
37071
|
// src/handlers/persona-dispatch.ts
|
|
36996
37072
|
init_protocol();
|
|
37073
|
+
var NO_PREVIOUS_PREFIX = "(no previous session found, started fresh)";
|
|
37074
|
+
function prefixNoPreviousOutcome(outcome) {
|
|
37075
|
+
if (outcome.kind === "success") {
|
|
37076
|
+
return { ...outcome, text: `${NO_PREVIOUS_PREFIX}
|
|
37077
|
+
|
|
37078
|
+
${outcome.text}` };
|
|
37079
|
+
}
|
|
37080
|
+
return { ...outcome, reason: `${NO_PREVIOUS_PREFIX} ${outcome.reason}` };
|
|
37081
|
+
}
|
|
36997
37082
|
function buildPersonaDispatchHandlers(deps) {
|
|
36998
37083
|
const { personaDispatchManager: mgr, spawnB, logger } = deps;
|
|
36999
37084
|
const run = async (frame) => {
|
|
@@ -37005,26 +37090,66 @@ function buildPersonaDispatchHandlers(deps) {
|
|
|
37005
37090
|
"personaDispatch:run requires sessionId (caller must pass x-clawd-session-id header)"
|
|
37006
37091
|
);
|
|
37007
37092
|
}
|
|
37093
|
+
const requestedMode = args.mode ?? "new";
|
|
37094
|
+
let route = "new";
|
|
37095
|
+
let bSessionId;
|
|
37096
|
+
let noPrevious = false;
|
|
37097
|
+
if (requestedMode === "continue") {
|
|
37098
|
+
const bFile = deps.findLatestDispatchedSession?.({
|
|
37099
|
+
sourceSessionId,
|
|
37100
|
+
targetPersona: args.targetPersona
|
|
37101
|
+
});
|
|
37102
|
+
if (bFile) {
|
|
37103
|
+
const inflight = deps.findInflightDispatchByBSessionId?.(bFile.sessionId);
|
|
37104
|
+
if (inflight) {
|
|
37105
|
+
logger?.warn("dispatch.run.continue.busy", {
|
|
37106
|
+
sourceSessionId,
|
|
37107
|
+
targetPersona: args.targetPersona,
|
|
37108
|
+
bSessionId: bFile.sessionId,
|
|
37109
|
+
inflightDispatchId: inflight
|
|
37110
|
+
});
|
|
37111
|
+
return {
|
|
37112
|
+
response: {
|
|
37113
|
+
type: "personaDispatch:run:ok",
|
|
37114
|
+
outcome: {
|
|
37115
|
+
kind: "failure",
|
|
37116
|
+
reason: "previous dispatch to this persona session not yet complete"
|
|
37117
|
+
}
|
|
37118
|
+
}
|
|
37119
|
+
};
|
|
37120
|
+
}
|
|
37121
|
+
route = "continue";
|
|
37122
|
+
bSessionId = bFile.sessionId;
|
|
37123
|
+
} else {
|
|
37124
|
+
noPrevious = true;
|
|
37125
|
+
}
|
|
37126
|
+
}
|
|
37008
37127
|
const { dispatchId } = mgr.start({
|
|
37009
37128
|
sourceSessionId,
|
|
37010
|
-
targetPersona: args.targetPersona
|
|
37129
|
+
targetPersona: args.targetPersona,
|
|
37130
|
+
bSessionId
|
|
37011
37131
|
});
|
|
37012
37132
|
logger?.info("dispatch.run.received", {
|
|
37013
37133
|
dispatchId,
|
|
37014
37134
|
sourceSessionId,
|
|
37015
37135
|
targetPersona: args.targetPersona,
|
|
37016
|
-
promptLen: args.prompt.length
|
|
37136
|
+
promptLen: args.prompt.length,
|
|
37137
|
+
requestedMode,
|
|
37138
|
+
route,
|
|
37139
|
+
noPrevious
|
|
37017
37140
|
});
|
|
37018
37141
|
void spawnB({
|
|
37019
37142
|
dispatchId,
|
|
37020
37143
|
sourceSessionId,
|
|
37021
37144
|
targetPersona: args.targetPersona,
|
|
37022
|
-
prompt: args.prompt
|
|
37145
|
+
prompt: args.prompt,
|
|
37146
|
+
route,
|
|
37147
|
+
bSessionId
|
|
37023
37148
|
}).then(() => {
|
|
37024
|
-
logger?.info("dispatch.spawnB.ok", { dispatchId });
|
|
37149
|
+
logger?.info("dispatch.spawnB.ok", { dispatchId, route });
|
|
37025
37150
|
}).catch((err) => {
|
|
37026
37151
|
const reason = err instanceof Error ? err.message : String(err);
|
|
37027
|
-
logger?.warn("dispatch.spawnB.failed", { dispatchId, reason });
|
|
37152
|
+
logger?.warn("dispatch.spawnB.failed", { dispatchId, route, reason });
|
|
37028
37153
|
mgr.complete(dispatchId, {
|
|
37029
37154
|
kind: "failure",
|
|
37030
37155
|
reason: `failed to spawn B: ${reason}`
|
|
@@ -37036,18 +37161,32 @@ function buildPersonaDispatchHandlers(deps) {
|
|
|
37036
37161
|
dispatchId,
|
|
37037
37162
|
outcomeKind: outcome.kind
|
|
37038
37163
|
});
|
|
37164
|
+
const finalOutcome = noPrevious ? prefixNoPreviousOutcome(outcome) : outcome;
|
|
37039
37165
|
return {
|
|
37040
|
-
response: { type: "personaDispatch:run:ok", outcome }
|
|
37166
|
+
response: { type: "personaDispatch:run:ok", outcome: finalOutcome }
|
|
37041
37167
|
};
|
|
37042
37168
|
};
|
|
37043
37169
|
const complete = async (frame) => {
|
|
37044
37170
|
const { type: _t, requestId: _r, ...rest } = frame;
|
|
37171
|
+
const sessionId = typeof rest.sessionId === "string" ? rest.sessionId : void 0;
|
|
37045
37172
|
const args = DispatchCompleteArgsSchema.parse(rest);
|
|
37173
|
+
if (!sessionId) {
|
|
37174
|
+
throw new Error(
|
|
37175
|
+
"personaDispatch:complete requires sessionId (caller must pass x-clawd-session-id header)"
|
|
37176
|
+
);
|
|
37177
|
+
}
|
|
37178
|
+
const dispatchId = mgr.findInflightDispatchByBSessionId(sessionId);
|
|
37179
|
+
if (!dispatchId) {
|
|
37180
|
+
throw new Error(
|
|
37181
|
+
`no in-flight dispatch for session ${sessionId} (already completed or B was never dispatched)`
|
|
37182
|
+
);
|
|
37183
|
+
}
|
|
37046
37184
|
logger?.info("dispatch.complete.received", {
|
|
37047
|
-
dispatchId
|
|
37185
|
+
dispatchId,
|
|
37186
|
+
sessionId,
|
|
37048
37187
|
outcomeKind: args.outcome.kind
|
|
37049
37188
|
});
|
|
37050
|
-
mgr.complete(
|
|
37189
|
+
mgr.complete(dispatchId, args.outcome);
|
|
37051
37190
|
return {
|
|
37052
37191
|
response: { type: "personaDispatch:complete:ok" }
|
|
37053
37192
|
};
|
|
@@ -45723,9 +45862,9 @@ async function startDaemon(config) {
|
|
|
45723
45862
|
// 127.0.0.1(不是 config.host)—— cc 跑在本机,loopback 最稳;外部访问限制 + http-router
|
|
45724
45863
|
// 的 isLoopback 兜底已确保安全。
|
|
45725
45864
|
getDaemonUrl: () => `http://127.0.0.1:${config.port}`,
|
|
45726
|
-
// Persona dispatch
|
|
45727
|
-
// - personaDispatchManager:
|
|
45728
|
-
//
|
|
45865
|
+
// Persona dispatch: manager 通过这两个 deps 跟 PersonaDispatchManager 协作。
|
|
45866
|
+
// - personaDispatchManager: createDispatchedSession / resumeDispatchedSession 用它
|
|
45867
|
+
// registerBSession;complete handler 用 findInflightDispatchByBSessionId 反查回 dispatchId。
|
|
45729
45868
|
// - dispatchMcpConfigPath: reducer 透传到 SpawnContext,cc spawn 加 --mcp-config flag。
|
|
45730
45869
|
personaDispatchManager,
|
|
45731
45870
|
dispatchMcpConfigPath: dispatchMcpConfigPath2,
|
|
@@ -46054,11 +46193,16 @@ async function startDaemon(config) {
|
|
|
46054
46193
|
const dispatchHandlers = buildPersonaDispatchHandlers({
|
|
46055
46194
|
personaDispatchManager,
|
|
46056
46195
|
logger,
|
|
46196
|
+
// continue 路径反查(命中 → 走 resumeDispatchedSession;不命中 → fallback 新建 + outcome 前缀)
|
|
46197
|
+
findLatestDispatchedSession: (q) => manager.findLatestDispatchedSession(q),
|
|
46198
|
+
// continue 路径防御:B 仍有 in-flight 时直接 fail(老板规则不会触发,但兜底)
|
|
46199
|
+
findInflightDispatchByBSessionId: (sid) => personaDispatchManager.findInflightDispatchByBSessionId(sid),
|
|
46057
46200
|
spawnB: async (args) => {
|
|
46058
46201
|
logger.info("dispatch.spawnB.start", {
|
|
46059
46202
|
dispatchId: args.dispatchId,
|
|
46060
46203
|
targetPersona: args.targetPersona,
|
|
46061
|
-
sourceSessionId: args.sourceSessionId
|
|
46204
|
+
sourceSessionId: args.sourceSessionId,
|
|
46205
|
+
route: args.route
|
|
46062
46206
|
});
|
|
46063
46207
|
const sourceFile = manager.findOwnedSession(args.sourceSessionId);
|
|
46064
46208
|
if (!sourceFile) {
|
|
@@ -46074,15 +46218,28 @@ async function startDaemon(config) {
|
|
|
46074
46218
|
logger.info("dispatch.spawnB.source-resolved", {
|
|
46075
46219
|
dispatchId: args.dispatchId,
|
|
46076
46220
|
sourceJsonlPath,
|
|
46077
|
-
hasToolSessionId: Boolean(sourceFile.toolSessionId)
|
|
46078
|
-
|
|
46079
|
-
manager.createDispatchedSession({
|
|
46080
|
-
dispatchId: args.dispatchId,
|
|
46081
|
-
sourceSessionId: args.sourceSessionId,
|
|
46082
|
-
targetPersona: args.targetPersona,
|
|
46083
|
-
prompt: args.prompt,
|
|
46084
|
-
sourceJsonlPath
|
|
46221
|
+
hasToolSessionId: Boolean(sourceFile.toolSessionId),
|
|
46222
|
+
route: args.route
|
|
46085
46223
|
});
|
|
46224
|
+
if (args.route === "continue") {
|
|
46225
|
+
if (!args.bSessionId) {
|
|
46226
|
+
throw new Error("continue route requires bSessionId");
|
|
46227
|
+
}
|
|
46228
|
+
manager.resumeDispatchedSession({
|
|
46229
|
+
dispatchId: args.dispatchId,
|
|
46230
|
+
bSessionId: args.bSessionId,
|
|
46231
|
+
prompt: args.prompt,
|
|
46232
|
+
sourceJsonlPath
|
|
46233
|
+
});
|
|
46234
|
+
} else {
|
|
46235
|
+
manager.createDispatchedSession({
|
|
46236
|
+
dispatchId: args.dispatchId,
|
|
46237
|
+
sourceSessionId: args.sourceSessionId,
|
|
46238
|
+
targetPersona: args.targetPersona,
|
|
46239
|
+
prompt: args.prompt,
|
|
46240
|
+
sourceJsonlPath
|
|
46241
|
+
});
|
|
46242
|
+
}
|
|
46086
46243
|
}
|
|
46087
46244
|
});
|
|
46088
46245
|
handlers = { ...handlers, ...dispatchHandlers };
|
|
@@ -39574,9 +39574,11 @@ async function handlePersonaDispatchToolCall(input, ctx) {
|
|
|
39574
39574
|
"content-type": "application/json",
|
|
39575
39575
|
"x-clawd-session-id": ctx.sessionId
|
|
39576
39576
|
},
|
|
39577
|
+
// input.mode === undefined → JSON.stringify omit;daemon parse 用 .optional() 接住缺省。
|
|
39577
39578
|
body: JSON.stringify({
|
|
39578
39579
|
targetPersona: input.targetPersona,
|
|
39579
|
-
prompt: input.prompt
|
|
39580
|
+
prompt: input.prompt,
|
|
39581
|
+
mode: input.mode
|
|
39580
39582
|
})
|
|
39581
39583
|
});
|
|
39582
39584
|
} catch (err) {
|
|
@@ -39626,16 +39628,16 @@ async function handlePersonaDispatchCompleteToolCall(input, ctx) {
|
|
|
39626
39628
|
isFailure: Boolean(input.isFailure),
|
|
39627
39629
|
textLen: input.text?.length ?? 0,
|
|
39628
39630
|
filePathCount: input.filePaths?.length ?? 0,
|
|
39629
|
-
|
|
39631
|
+
sessionId: ctx.sessionId,
|
|
39630
39632
|
daemonUrl: ctx.daemonUrl
|
|
39631
39633
|
});
|
|
39632
|
-
if (!ctx.
|
|
39633
|
-
log("personaDispatchComplete
|
|
39634
|
+
if (!ctx.sessionId) {
|
|
39635
|
+
log("personaDispatchComplete CLAWD_SESSION_ID missing");
|
|
39634
39636
|
return {
|
|
39635
39637
|
content: [
|
|
39636
39638
|
{
|
|
39637
39639
|
type: "text",
|
|
39638
|
-
text: "Error:
|
|
39640
|
+
text: "Error: CLAWD_SESSION_ID env not set; daemon cannot resolve in-flight dispatch without sessionId."
|
|
39639
39641
|
}
|
|
39640
39642
|
],
|
|
39641
39643
|
isError: true
|
|
@@ -39650,14 +39652,15 @@ async function handlePersonaDispatchCompleteToolCall(input, ctx) {
|
|
|
39650
39652
|
filePaths: input.filePaths
|
|
39651
39653
|
};
|
|
39652
39654
|
const url = `${ctx.daemonUrl}/api/rpc/personaDispatch.complete`;
|
|
39653
|
-
log("personaDispatchComplete pre-fetch", { url,
|
|
39654
|
-
const headers = { "content-type": "application/json" };
|
|
39655
|
-
if (ctx.sessionId) headers["x-clawd-session-id"] = ctx.sessionId;
|
|
39655
|
+
log("personaDispatchComplete pre-fetch", { url, sessionId: ctx.sessionId });
|
|
39656
39656
|
try {
|
|
39657
39657
|
const res = await f(url, {
|
|
39658
39658
|
method: "POST",
|
|
39659
|
-
headers
|
|
39660
|
-
|
|
39659
|
+
headers: {
|
|
39660
|
+
"content-type": "application/json",
|
|
39661
|
+
"x-clawd-session-id": ctx.sessionId
|
|
39662
|
+
},
|
|
39663
|
+
body: JSON.stringify({ outcome })
|
|
39661
39664
|
});
|
|
39662
39665
|
log("personaDispatchComplete post-fetch", { url, status: res.status });
|
|
39663
39666
|
} catch (err) {
|
|
@@ -39681,12 +39684,10 @@ async function handlePersonaDispatchCompleteToolCall(input, ctx) {
|
|
|
39681
39684
|
async function main() {
|
|
39682
39685
|
const daemonUrl = process.env.CLAWD_DAEMON_URL;
|
|
39683
39686
|
const sessionId = process.env.CLAWD_SESSION_ID;
|
|
39684
|
-
const dispatchId = process.env.CLAWD_DISPATCH_ID;
|
|
39685
39687
|
log("boot", {
|
|
39686
39688
|
pid: process.pid,
|
|
39687
39689
|
hasDaemonUrl: Boolean(daemonUrl),
|
|
39688
39690
|
hasSessionId: Boolean(sessionId),
|
|
39689
|
-
hasDispatchId: Boolean(dispatchId),
|
|
39690
39691
|
logPath: DISPATCH_LOG ?? "(stderr)"
|
|
39691
39692
|
});
|
|
39692
39693
|
if (!daemonUrl) {
|
|
@@ -39703,11 +39704,14 @@ async function main() {
|
|
|
39703
39704
|
"personaDispatch",
|
|
39704
39705
|
{
|
|
39705
39706
|
title: "Dispatch task to another persona",
|
|
39706
|
-
description: "Delegate a task to another clawd persona. Use this when the user message contains `@persona/<id>` to hand the task off to that persona. The tool blocks until the other persona finishes and returns its result.",
|
|
39707
|
+
description: "Delegate a task to another clawd persona. Use this when the user message contains `@persona/<id>` to hand the task off to that persona. The tool blocks until the other persona finishes and returns its result. Pass mode='continue' when the user's follow-up message is clearly a continuation of the task you already dispatched to this persona (the previous B session is resumed with its full context). Omit (or pass 'new') for an independent new task. Default is 'new'.",
|
|
39707
39708
|
inputSchema: {
|
|
39708
39709
|
targetPersona: external_exports.string().describe("persona id (e.g. persona-bug-fixer)"),
|
|
39709
39710
|
prompt: external_exports.string().describe(
|
|
39710
39711
|
"the task to perform, with the @persona/<id> token stripped out"
|
|
39712
|
+
),
|
|
39713
|
+
mode: external_exports.enum(["new", "continue"]).optional().describe(
|
|
39714
|
+
"continue = resume the previous B session for this (sourceSession, targetPersona) pair; new (default) = fresh B session"
|
|
39711
39715
|
)
|
|
39712
39716
|
}
|
|
39713
39717
|
},
|
|
@@ -39726,8 +39730,8 @@ async function main() {
|
|
|
39726
39730
|
}
|
|
39727
39731
|
},
|
|
39728
39732
|
// sessionId 透传:complete RPC 必须带 x-clawd-session-id header(rpc-adapter 强制)。
|
|
39729
|
-
//
|
|
39730
|
-
async (input) => handlePersonaDispatchCompleteToolCall(input, { daemonUrl, sessionId
|
|
39733
|
+
// daemon 用该 sessionId 反查 PersonaDispatchManager 里该 B 的 in-flight dispatchId 配对回 A 的 waiter。
|
|
39734
|
+
async (input) => handlePersonaDispatchCompleteToolCall(input, { daemonUrl, sessionId })
|
|
39731
39735
|
);
|
|
39732
39736
|
await server.connect(new StdioServerTransport());
|
|
39733
39737
|
}
|
package/package.json
CHANGED