@clawos-dev/clawd 0.2.191 → 0.2.192-beta.386.33a5833
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
CHANGED
|
@@ -5996,7 +5996,7 @@ var init_feishu_auth = __esm({
|
|
|
5996
5996
|
});
|
|
5997
5997
|
|
|
5998
5998
|
// ../protocol/src/dispatch.ts
|
|
5999
|
-
var DispatchOutcomeSchema, DispatchRunArgsSchema, DispatchCompleteArgsSchema;
|
|
5999
|
+
var DispatchOutcomeSchema, DispatchRunArgsSchema, DispatchRunResponseSchema, DispatchCompleteArgsSchema;
|
|
6000
6000
|
var init_dispatch = __esm({
|
|
6001
6001
|
"../protocol/src/dispatch.ts"() {
|
|
6002
6002
|
"use strict";
|
|
@@ -6017,10 +6017,21 @@ var init_dispatch = __esm({
|
|
|
6017
6017
|
prompt: external_exports.string(),
|
|
6018
6018
|
// 跨设备 dispatch:非空 = A 角色,转发到该 contact deviceId 的 peer daemon;
|
|
6019
6019
|
// 省略 = 本地 dispatch / B 角色本地执行。A 转发给 B 时 body 不带此字段。
|
|
6020
|
-
targetDeviceId: external_exports.string().min(1).optional()
|
|
6020
|
+
targetDeviceId: external_exports.string().min(1).optional(),
|
|
6021
|
+
// 精确寻址已知 B session(复用其上下文):
|
|
6022
|
+
// - 本地(targetDeviceId 缺省):daemon 校验目标为本机 session 且
|
|
6023
|
+
// dispatchedFromSessionId 匹配 A 的 sessionId(防越权)
|
|
6024
|
+
// - 跨设备(targetDeviceId 非空):peer daemon 在其本地按同规则校验
|
|
6025
|
+
// creatorPrincipalId === A.deviceId
|
|
6026
|
+
// - 省略 = 新建 B session
|
|
6027
|
+
targetSessionId: external_exports.string().min(1).optional()
|
|
6028
|
+
});
|
|
6029
|
+
DispatchRunResponseSchema = external_exports.object({
|
|
6030
|
+
type: external_exports.literal("personaDispatch:run:ok"),
|
|
6031
|
+
outcome: DispatchOutcomeSchema,
|
|
6032
|
+
dispatchedSessionId: external_exports.string().min(1).optional()
|
|
6021
6033
|
});
|
|
6022
6034
|
DispatchCompleteArgsSchema = external_exports.object({
|
|
6023
|
-
dispatchId: external_exports.string().min(1),
|
|
6024
6035
|
outcome: DispatchOutcomeSchema
|
|
6025
6036
|
});
|
|
6026
6037
|
}
|
|
@@ -41552,8 +41563,6 @@ function buildSpawnContext(state, deps) {
|
|
|
41552
41563
|
env.CLAWD_SESSION_ID = file.sessionId;
|
|
41553
41564
|
const daemonUrl = deps.getDaemonUrl?.() ?? null;
|
|
41554
41565
|
if (daemonUrl) env.CLAWD_DAEMON_URL = daemonUrl;
|
|
41555
|
-
const dispatchId = deps.lookupDispatchByBSessionId?.(file.sessionId);
|
|
41556
|
-
if (dispatchId) env.CLAWD_DISPATCH_ID = dispatchId;
|
|
41557
41566
|
const personaId = file.ownerPersonaId;
|
|
41558
41567
|
if (personaId) env.CLAWD_PERSONA_ID = personaId;
|
|
41559
41568
|
const dispatchMcpConfigPath2 = deps.getDispatchMcpConfigPath?.() ?? null;
|
|
@@ -42359,14 +42368,14 @@ var SessionRunner = class {
|
|
|
42359
42368
|
// 单栏 refactor (spec 2026-06-02 §5.1): cc 子进程 env 注入 CLAWD_DAEMON_URL,让 assistant
|
|
42360
42369
|
// curl daemon HTTP RPC adapter (/api/rpc/<method>) 触发管理操作。null = HTTP adapter 未启。
|
|
42361
42370
|
getDaemonUrl: this.hooks.getDaemonUrl,
|
|
42362
|
-
// Persona dispatch:透传 dispatch.mcp.json
|
|
42363
|
-
//
|
|
42371
|
+
// Persona dispatch:透传 dispatch.mcp.json 路径闭包,让 cc spawn 加 --mcp-config flag
|
|
42372
|
+
// 使两侧 cc 都能看到 personaDispatch / personaDispatchComplete tool(按 session 身份分工用哪个)。
|
|
42373
|
+
// dispatchId 不注 cc env——complete handler 用 sessionId 反查 in-flight dispatchId 配对。
|
|
42364
42374
|
getDispatchMcpConfigPath: this.hooks.getDispatchMcpConfigPath,
|
|
42365
42375
|
getShiftMcpConfigPath: this.hooks.getShiftMcpConfigPath,
|
|
42366
42376
|
getInboxMcpConfigPath: this.hooks.getInboxMcpConfigPath,
|
|
42367
42377
|
// Ticket MCP:透传 ticket.mcp.json 路径闭包(reducer 内做 persona-ticket-manager gating)
|
|
42368
42378
|
getTicketMcpConfigPath: this.hooks.getTicketMcpConfigPath,
|
|
42369
|
-
lookupDispatchByBSessionId: this.hooks.lookupDispatchByBSessionId,
|
|
42370
42379
|
// ReadyGate v2:透传 mode 让 reducer send / ready-detected 分支决定走暂存队列还是直写
|
|
42371
42380
|
mode: this.hooks.mode,
|
|
42372
42381
|
// [RG-DBG] 注入 logger 让 reducer rgDbg 走 pino 进 clawd.log;定位完跟 rgDbg 一起删
|
|
@@ -42850,9 +42859,22 @@ You may read this file with your Read tool to understand context. You do not hav
|
|
|
42850
42859
|
|
|
42851
42860
|
When done, call the MCP tool \`mcp__clawd-dispatch__personaDispatchComplete\` with your result:
|
|
42852
42861
|
- Success: { text: "...", filePaths?: ["abs/path", ...] }
|
|
42853
|
-
- Failure: { isFailure: true, reason: "..." }
|
|
42862
|
+
- Failure: { isFailure: true, reason: "..." }`;
|
|
42863
|
+
}
|
|
42864
|
+
function buildDispatchContinuationPack(args) {
|
|
42865
|
+
return `[Dispatched from owner \u2014 follow-up query]
|
|
42866
|
+
|
|
42867
|
+
Although you reported back on the previous task, the owner has a follow-up question. Continue from where you left off.
|
|
42868
|
+
|
|
42869
|
+
Owner's new message:
|
|
42870
|
+
${args.prompt}
|
|
42854
42871
|
|
|
42855
|
-
|
|
42872
|
+
Source conversation (jsonl, same path as before, may contain new messages):
|
|
42873
|
+
${args.sourceJsonlPath}
|
|
42874
|
+
|
|
42875
|
+
When done, call \`mcp__clawd-dispatch__personaDispatchComplete\` again with your result:
|
|
42876
|
+
- Success: { text: "...", filePaths?: ["abs/path", ...] }
|
|
42877
|
+
- Failure: { isFailure: true, reason: "..." }`;
|
|
42856
42878
|
}
|
|
42857
42879
|
function derivePersonaSpawnCwd(file, personaRoot) {
|
|
42858
42880
|
const personaId = file.ownerPersonaId;
|
|
@@ -43173,13 +43195,13 @@ var SessionManager = class {
|
|
|
43173
43195
|
// 单栏 refactor (spec 2026-06-02 §5.1): 透传 daemon HTTP RPC base URL 闭包,
|
|
43174
43196
|
// reducer 把它注入 cc 子进程 env CLAWD_DAEMON_URL.
|
|
43175
43197
|
getDaemonUrl: this.deps.getDaemonUrl,
|
|
43176
|
-
// Persona dispatch
|
|
43177
|
-
//
|
|
43198
|
+
// Persona dispatch: dispatch.mcp.json 路径闭包透传给 reducer,让 cc spawn 加
|
|
43199
|
+
// --mcp-config flag 挂 dispatch MCP server(两侧 cc 都挂,按 session 身份分工调 tool)。
|
|
43200
|
+
// dispatchId 不再走 cc env——complete handler 用 sessionId 反查 in-flight dispatchId 配对。
|
|
43178
43201
|
getDispatchMcpConfigPath: this.deps.dispatchMcpConfigPath ? () => this.deps.dispatchMcpConfigPath ?? null : void 0,
|
|
43179
43202
|
getTicketMcpConfigPath: this.deps.ticketMcpConfigPath ? () => this.deps.ticketMcpConfigPath ?? null : void 0,
|
|
43180
43203
|
getShiftMcpConfigPath: this.deps.shiftMcpConfigPath ? () => this.deps.shiftMcpConfigPath ?? null : void 0,
|
|
43181
43204
|
getInboxMcpConfigPath: this.deps.inboxMcpConfigPath ? () => this.deps.inboxMcpConfigPath ?? null : void 0,
|
|
43182
|
-
lookupDispatchByBSessionId: this.deps.personaDispatchManager ? (bSid) => this.deps.personaDispatchManager?.lookupDispatchByBSessionId(bSid) : void 0,
|
|
43183
43205
|
// file-sharing (spec §6 PR 3):闭包 scope + sessionId,runner 只暴露 tool/relPath/cwd
|
|
43184
43206
|
onFileEdit: attachmentGroup ? (input) => attachmentGroup.onFileEdit({
|
|
43185
43207
|
scope,
|
|
@@ -44019,10 +44041,11 @@ var SessionManager = class {
|
|
|
44019
44041
|
* Persona dispatch: 启动 B session 并投递任务包当第一条 user message。
|
|
44020
44042
|
*
|
|
44021
44043
|
* - 复用 persona-owner 创建路径(cwd=personaDir、scope=persona-owner、不沙箱)
|
|
44022
|
-
* - SessionFile 上写 dispatchedFromSessionId 关联回 A(UI 折叠用;mirror 一侧 strip
|
|
44023
|
-
*
|
|
44024
|
-
*
|
|
44025
|
-
*
|
|
44044
|
+
* - SessionFile 上写 dispatchedFromSessionId 关联回 A(UI 折叠用;mirror 一侧 strip;
|
|
44045
|
+
* targetSessionId 复用路径也按此字段校验寻址来源)
|
|
44046
|
+
* - 跟 PersonaDispatchManager 登记 dispatchId → B sessionId(complete handler 用
|
|
44047
|
+
* findInflightDispatchByBSessionId 反查回 dispatchId 配对回 A 的 waiter)
|
|
44048
|
+
* - 投任务包;包尾教 B 用 personaDispatchComplete tool 回传
|
|
44026
44049
|
*
|
|
44027
44050
|
* cwd 派生与 SessionManager.create 的 owner persona 路径一致(derivePersonaSpawnCwd)。
|
|
44028
44051
|
*/
|
|
@@ -44077,8 +44100,7 @@ var SessionManager = class {
|
|
|
44077
44100
|
});
|
|
44078
44101
|
const taskPack = buildDispatchTaskPack({
|
|
44079
44102
|
prompt: args.prompt,
|
|
44080
|
-
sourceJsonlPath: args.sourceJsonlPath
|
|
44081
|
-
dispatchId: args.dispatchId
|
|
44103
|
+
sourceJsonlPath: args.sourceJsonlPath
|
|
44082
44104
|
});
|
|
44083
44105
|
runner.input({ kind: "command", command: { kind: "send", text: taskPack } });
|
|
44084
44106
|
this.deps.logger?.info("dispatch.createDispatchedSession.task-pack-sent", {
|
|
@@ -44087,6 +44109,59 @@ var SessionManager = class {
|
|
|
44087
44109
|
});
|
|
44088
44110
|
return { sessionId };
|
|
44089
44111
|
}
|
|
44112
|
+
/**
|
|
44113
|
+
* targetSessionId 精确寻址已知 B session 复用其上下文。投 continuation pack 给 runner。
|
|
44114
|
+
*
|
|
44115
|
+
* 关键差异 vs createDispatchedSession:
|
|
44116
|
+
* - 不 newSessionId() / 不写 SessionFile —— B session 复用(走 findOwnedSession)
|
|
44117
|
+
* - 权限校验:本地 dispatch → SessionFile.dispatchedFromSessionId === A 的 sessionId;
|
|
44118
|
+
* 跨设备 B 角色 → SessionFile.creatorPrincipalId === A 的 deviceId
|
|
44119
|
+
* - 跟 PersonaDispatchManager 重新登记(新 dispatchId 复用同 bSessionId)
|
|
44120
|
+
* - ensureRunnerForScope: cc 活复用;cc 死时首次 send 触发 spawn,jsonl 已存在自动 --resume
|
|
44121
|
+
*
|
|
44122
|
+
* @param callerIdentity 权限校验依据:
|
|
44123
|
+
* - kind='local': A 的 sessionId(本机 dispatch),校验 dispatchedFromSessionId 匹配
|
|
44124
|
+
* - kind='guest': A 的 deviceId(跨设备转发进来的 A),校验 creatorPrincipalId 匹配
|
|
44125
|
+
*/
|
|
44126
|
+
resumeDispatchedSession(args) {
|
|
44127
|
+
if (!this.deps.personaDispatchManager) {
|
|
44128
|
+
throw new Error("resumeDispatchedSession: personaDispatchManager missing in ManagerDeps");
|
|
44129
|
+
}
|
|
44130
|
+
const file = this.findOwnedSession(args.bSessionId);
|
|
44131
|
+
if (!file) {
|
|
44132
|
+
throw new Error(`resumeDispatchedSession: B session ${args.bSessionId} not found`);
|
|
44133
|
+
}
|
|
44134
|
+
if (args.callerIdentity.kind === "local") {
|
|
44135
|
+
if (file.dispatchedFromSessionId !== args.callerIdentity.sourceSessionId) {
|
|
44136
|
+
throw new Error(
|
|
44137
|
+
`resumeDispatchedSession: session ${args.bSessionId} not dispatched by caller (dispatchedFromSessionId mismatch)`
|
|
44138
|
+
);
|
|
44139
|
+
}
|
|
44140
|
+
} else {
|
|
44141
|
+
if (file.creatorPrincipalId !== args.callerIdentity.sourcePrincipalId) {
|
|
44142
|
+
throw new Error(
|
|
44143
|
+
`resumeDispatchedSession: session ${args.bSessionId} not dispatched by caller device (creatorPrincipalId mismatch)`
|
|
44144
|
+
);
|
|
44145
|
+
}
|
|
44146
|
+
}
|
|
44147
|
+
const scope = this.scopeForFile(file);
|
|
44148
|
+
this.deps.personaDispatchManager.registerBSession(args.dispatchId, args.bSessionId);
|
|
44149
|
+
this.deps.logger?.info("dispatch.resumeDispatchedSession.registered", {
|
|
44150
|
+
dispatchId: args.dispatchId,
|
|
44151
|
+
bSessionId: args.bSessionId
|
|
44152
|
+
});
|
|
44153
|
+
const runner = this.ensureRunnerForScope(file, scope);
|
|
44154
|
+
const pack = buildDispatchContinuationPack({
|
|
44155
|
+
prompt: args.prompt,
|
|
44156
|
+
sourceJsonlPath: args.sourceJsonlPath
|
|
44157
|
+
});
|
|
44158
|
+
runner.input({ kind: "command", command: { kind: "send", text: pack } });
|
|
44159
|
+
this.deps.logger?.info("dispatch.resumeDispatchedSession.continuation-sent", {
|
|
44160
|
+
dispatchId: args.dispatchId,
|
|
44161
|
+
bSessionId: args.bSessionId
|
|
44162
|
+
});
|
|
44163
|
+
return { sessionId: args.bSessionId };
|
|
44164
|
+
}
|
|
44090
44165
|
/**
|
|
44091
44166
|
* shift v1 (spec 2026-06-24-clawd-shift):到点 fire 时由 ShiftScheduler 调,
|
|
44092
44167
|
* 起一个新 cc session 跑 prompt(fire-and-forget,不绑 dispatchId / 不挂 caller)。
|
|
@@ -47493,6 +47568,7 @@ var PersonaDispatchManager = class {
|
|
|
47493
47568
|
dispatchId,
|
|
47494
47569
|
sourceSessionId: args.sourceSessionId,
|
|
47495
47570
|
targetPersona: args.targetPersona,
|
|
47571
|
+
bSessionId: args.bSessionId,
|
|
47496
47572
|
waiters: [],
|
|
47497
47573
|
outcome: null
|
|
47498
47574
|
});
|
|
@@ -47523,10 +47599,22 @@ var PersonaDispatchManager = class {
|
|
|
47523
47599
|
if (!state) throw new Error(`unknown dispatchId: ${dispatchId}`);
|
|
47524
47600
|
state.bSessionId = bSessionId;
|
|
47525
47601
|
}
|
|
47526
|
-
/**
|
|
47527
|
-
|
|
47602
|
+
/**
|
|
47603
|
+
* 按 B sessionId 找当前 in-flight(未 resolved)的 dispatchId。complete handler 用这个把
|
|
47604
|
+
* B 的 outcome 配对回 A 的 waiter —— dispatchId 不再走 cc env,全靠
|
|
47605
|
+
* (bSessionId → in-flight dispatchId) 反查。
|
|
47606
|
+
*
|
|
47607
|
+
* 同一 bSessionId 不会同时有多个 in-flight:A 拿不到 outcome 不会发起新 dispatch;
|
|
47608
|
+
* 老板规则下 A 一次只能挂一个 waiter。
|
|
47609
|
+
*
|
|
47610
|
+
* 历史 entry(已 resolved)暂不清 —— 单 daemon lifetime 累积可接受;未来用量大了加
|
|
47611
|
+
* TTL GC(complete 后 N min 删 entry)。
|
|
47612
|
+
*/
|
|
47613
|
+
findInflightDispatchByBSessionId(bSessionId) {
|
|
47528
47614
|
for (const state of this.map.values()) {
|
|
47529
|
-
if (state.bSessionId === bSessionId
|
|
47615
|
+
if (state.bSessionId === bSessionId && state.outcome === null) {
|
|
47616
|
+
return state.dispatchId;
|
|
47617
|
+
}
|
|
47530
47618
|
}
|
|
47531
47619
|
return void 0;
|
|
47532
47620
|
}
|
|
@@ -48125,6 +48213,28 @@ function canAccessPersona(grants, personaId, action) {
|
|
|
48125
48213
|
}
|
|
48126
48214
|
|
|
48127
48215
|
// src/handlers/persona-dispatch.ts
|
|
48216
|
+
function assertLocalReuseAllowed(bFile, sourceSessionId) {
|
|
48217
|
+
if (bFile.dispatchedFromSessionId !== sourceSessionId) {
|
|
48218
|
+
throw new ClawdError(
|
|
48219
|
+
ERROR_CODES.UNAUTHORIZED,
|
|
48220
|
+
`targetSessionId not dispatched by caller (dispatchedFromSessionId mismatch)`
|
|
48221
|
+
);
|
|
48222
|
+
}
|
|
48223
|
+
}
|
|
48224
|
+
function assertGuestReuseAllowed(bFile, sourcePrincipalId, targetPersona) {
|
|
48225
|
+
if (bFile.ownerPersonaId !== targetPersona) {
|
|
48226
|
+
throw new ClawdError(
|
|
48227
|
+
ERROR_CODES.UNAUTHORIZED,
|
|
48228
|
+
`targetSessionId does not belong to targetPersona`
|
|
48229
|
+
);
|
|
48230
|
+
}
|
|
48231
|
+
if (bFile.creatorPrincipalId !== sourcePrincipalId) {
|
|
48232
|
+
throw new ClawdError(
|
|
48233
|
+
ERROR_CODES.UNAUTHORIZED,
|
|
48234
|
+
`targetSessionId not dispatched by caller device (creatorPrincipalId mismatch)`
|
|
48235
|
+
);
|
|
48236
|
+
}
|
|
48237
|
+
}
|
|
48128
48238
|
function buildPersonaDispatchHandlers(deps) {
|
|
48129
48239
|
const { personaDispatchManager: mgr, spawnB, logger } = deps;
|
|
48130
48240
|
const run = async (frame, _client, ctx) => {
|
|
@@ -48143,15 +48253,21 @@ function buildPersonaDispatchHandlers(deps) {
|
|
|
48143
48253
|
}
|
|
48144
48254
|
logger?.info("dispatch.run.forward", {
|
|
48145
48255
|
targetDeviceId: args.targetDeviceId,
|
|
48146
|
-
targetPersona: args.targetPersona
|
|
48256
|
+
targetPersona: args.targetPersona,
|
|
48257
|
+
hasTargetSessionId: Boolean(args.targetSessionId)
|
|
48147
48258
|
});
|
|
48148
|
-
const outcome2 = await deps.forwardToPeer({
|
|
48259
|
+
const { outcome: outcome2, dispatchedSessionId } = await deps.forwardToPeer({
|
|
48149
48260
|
targetDeviceId: args.targetDeviceId,
|
|
48150
48261
|
targetPersona: args.targetPersona,
|
|
48151
|
-
prompt: args.prompt
|
|
48262
|
+
prompt: args.prompt,
|
|
48263
|
+
targetSessionId: args.targetSessionId
|
|
48152
48264
|
});
|
|
48153
48265
|
return {
|
|
48154
|
-
response: {
|
|
48266
|
+
response: {
|
|
48267
|
+
type: "personaDispatch:run:ok",
|
|
48268
|
+
outcome: outcome2,
|
|
48269
|
+
...dispatchedSessionId ? { dispatchedSessionId } : {}
|
|
48270
|
+
}
|
|
48155
48271
|
};
|
|
48156
48272
|
}
|
|
48157
48273
|
if (ctx?.principal.kind === "guest") {
|
|
@@ -48162,34 +48278,65 @@ function buildPersonaDispatchHandlers(deps) {
|
|
|
48162
48278
|
`persona not dispatchable: ${args.targetPersona}`
|
|
48163
48279
|
);
|
|
48164
48280
|
}
|
|
48281
|
+
const guestSourceId = ctx.principal.id;
|
|
48282
|
+
let bSessionId2;
|
|
48283
|
+
let route2 = "new";
|
|
48284
|
+
if (args.targetSessionId) {
|
|
48285
|
+
if (!deps.findOwnedSession) {
|
|
48286
|
+
throw new Error("targetSessionId reuse not wired (findOwnedSession missing)");
|
|
48287
|
+
}
|
|
48288
|
+
const bFile = deps.findOwnedSession(args.targetSessionId);
|
|
48289
|
+
if (!bFile) {
|
|
48290
|
+
throw new ClawdError(
|
|
48291
|
+
ERROR_CODES.SESSION_NOT_FOUND,
|
|
48292
|
+
`targetSessionId not found: ${args.targetSessionId}`
|
|
48293
|
+
);
|
|
48294
|
+
}
|
|
48295
|
+
assertGuestReuseAllowed(bFile, guestSourceId, args.targetPersona);
|
|
48296
|
+
route2 = "resume";
|
|
48297
|
+
bSessionId2 = args.targetSessionId;
|
|
48298
|
+
}
|
|
48165
48299
|
const { dispatchId: dispatchId2 } = mgr.start({
|
|
48166
|
-
sourceSessionId:
|
|
48167
|
-
targetPersona: args.targetPersona
|
|
48300
|
+
sourceSessionId: guestSourceId,
|
|
48301
|
+
targetPersona: args.targetPersona,
|
|
48302
|
+
bSessionId: bSessionId2
|
|
48168
48303
|
});
|
|
48169
48304
|
logger?.info("dispatch.run.received.guest", {
|
|
48170
48305
|
dispatchId: dispatchId2,
|
|
48171
|
-
sourcePrincipal:
|
|
48306
|
+
sourcePrincipal: guestSourceId,
|
|
48172
48307
|
targetPersona: args.targetPersona,
|
|
48173
|
-
promptLen: args.prompt.length
|
|
48308
|
+
promptLen: args.prompt.length,
|
|
48309
|
+
route: route2
|
|
48174
48310
|
});
|
|
48175
|
-
|
|
48176
|
-
|
|
48177
|
-
|
|
48178
|
-
|
|
48179
|
-
|
|
48180
|
-
|
|
48181
|
-
|
|
48182
|
-
|
|
48311
|
+
let resolvedBSessionId2 = bSessionId2;
|
|
48312
|
+
try {
|
|
48313
|
+
const { bSessionId: minted } = await spawnB({
|
|
48314
|
+
dispatchId: dispatchId2,
|
|
48315
|
+
sourceSessionId: guestSourceId,
|
|
48316
|
+
targetPersona: args.targetPersona,
|
|
48317
|
+
prompt: args.prompt,
|
|
48318
|
+
route: route2,
|
|
48319
|
+
bSessionId: bSessionId2,
|
|
48320
|
+
guestPrincipalId: guestSourceId,
|
|
48321
|
+
guestDisplayName: ctx.principal.displayName
|
|
48322
|
+
});
|
|
48323
|
+
resolvedBSessionId2 = minted;
|
|
48324
|
+
logger?.info("dispatch.spawnB.ok", { dispatchId: dispatchId2, route: route2 });
|
|
48325
|
+
} catch (err) {
|
|
48183
48326
|
const reason = err instanceof Error ? err.message : String(err);
|
|
48184
|
-
logger?.warn("dispatch.spawnB.failed", { dispatchId: dispatchId2, reason });
|
|
48327
|
+
logger?.warn("dispatch.spawnB.failed", { dispatchId: dispatchId2, route: route2, reason });
|
|
48185
48328
|
mgr.complete(dispatchId2, {
|
|
48186
48329
|
kind: "failure",
|
|
48187
48330
|
reason: `failed to spawn B: ${reason}`
|
|
48188
48331
|
});
|
|
48189
|
-
}
|
|
48332
|
+
}
|
|
48190
48333
|
const outcome2 = await mgr.wait(dispatchId2);
|
|
48191
48334
|
return {
|
|
48192
|
-
response: {
|
|
48335
|
+
response: {
|
|
48336
|
+
type: "personaDispatch:run:ok",
|
|
48337
|
+
outcome: outcome2,
|
|
48338
|
+
...resolvedBSessionId2 ? { dispatchedSessionId: resolvedBSessionId2 } : {}
|
|
48339
|
+
}
|
|
48193
48340
|
};
|
|
48194
48341
|
}
|
|
48195
48342
|
if (!sourceSessionId) {
|
|
@@ -48197,31 +48344,61 @@ function buildPersonaDispatchHandlers(deps) {
|
|
|
48197
48344
|
"personaDispatch:run requires sessionId (caller must pass x-clawd-session-id header)"
|
|
48198
48345
|
);
|
|
48199
48346
|
}
|
|
48347
|
+
let bSessionId;
|
|
48348
|
+
let route = "new";
|
|
48349
|
+
if (args.targetSessionId) {
|
|
48350
|
+
if (!deps.findOwnedSession) {
|
|
48351
|
+
throw new Error("targetSessionId reuse not wired (findOwnedSession missing)");
|
|
48352
|
+
}
|
|
48353
|
+
const bFile = deps.findOwnedSession(args.targetSessionId);
|
|
48354
|
+
if (!bFile) {
|
|
48355
|
+
throw new ClawdError(
|
|
48356
|
+
ERROR_CODES.SESSION_NOT_FOUND,
|
|
48357
|
+
`targetSessionId not found: ${args.targetSessionId}`
|
|
48358
|
+
);
|
|
48359
|
+
}
|
|
48360
|
+
if (bFile.ownerPersonaId !== args.targetPersona) {
|
|
48361
|
+
throw new ClawdError(
|
|
48362
|
+
ERROR_CODES.UNAUTHORIZED,
|
|
48363
|
+
`targetSessionId does not belong to targetPersona`
|
|
48364
|
+
);
|
|
48365
|
+
}
|
|
48366
|
+
assertLocalReuseAllowed(bFile, sourceSessionId);
|
|
48367
|
+
route = "resume";
|
|
48368
|
+
bSessionId = args.targetSessionId;
|
|
48369
|
+
}
|
|
48200
48370
|
const { dispatchId } = mgr.start({
|
|
48201
48371
|
sourceSessionId,
|
|
48202
|
-
targetPersona: args.targetPersona
|
|
48372
|
+
targetPersona: args.targetPersona,
|
|
48373
|
+
bSessionId
|
|
48203
48374
|
});
|
|
48204
48375
|
logger?.info("dispatch.run.received", {
|
|
48205
48376
|
dispatchId,
|
|
48206
48377
|
sourceSessionId,
|
|
48207
48378
|
targetPersona: args.targetPersona,
|
|
48208
|
-
promptLen: args.prompt.length
|
|
48379
|
+
promptLen: args.prompt.length,
|
|
48380
|
+
route
|
|
48209
48381
|
});
|
|
48210
|
-
|
|
48211
|
-
|
|
48212
|
-
|
|
48213
|
-
|
|
48214
|
-
|
|
48215
|
-
|
|
48216
|
-
|
|
48217
|
-
|
|
48382
|
+
let resolvedBSessionId = bSessionId;
|
|
48383
|
+
try {
|
|
48384
|
+
const { bSessionId: minted } = await spawnB({
|
|
48385
|
+
dispatchId,
|
|
48386
|
+
sourceSessionId,
|
|
48387
|
+
targetPersona: args.targetPersona,
|
|
48388
|
+
prompt: args.prompt,
|
|
48389
|
+
route,
|
|
48390
|
+
bSessionId
|
|
48391
|
+
});
|
|
48392
|
+
resolvedBSessionId = minted;
|
|
48393
|
+
logger?.info("dispatch.spawnB.ok", { dispatchId, route });
|
|
48394
|
+
} catch (err) {
|
|
48218
48395
|
const reason = err instanceof Error ? err.message : String(err);
|
|
48219
|
-
logger?.warn("dispatch.spawnB.failed", { dispatchId, reason });
|
|
48396
|
+
logger?.warn("dispatch.spawnB.failed", { dispatchId, route, reason });
|
|
48220
48397
|
mgr.complete(dispatchId, {
|
|
48221
48398
|
kind: "failure",
|
|
48222
48399
|
reason: `failed to spawn B: ${reason}`
|
|
48223
48400
|
});
|
|
48224
|
-
}
|
|
48401
|
+
}
|
|
48225
48402
|
logger?.info("dispatch.run.waiting", { dispatchId });
|
|
48226
48403
|
const outcome = await mgr.wait(dispatchId);
|
|
48227
48404
|
logger?.info("dispatch.run.resolved", {
|
|
@@ -48229,17 +48406,34 @@ function buildPersonaDispatchHandlers(deps) {
|
|
|
48229
48406
|
outcomeKind: outcome.kind
|
|
48230
48407
|
});
|
|
48231
48408
|
return {
|
|
48232
|
-
response: {
|
|
48409
|
+
response: {
|
|
48410
|
+
type: "personaDispatch:run:ok",
|
|
48411
|
+
outcome,
|
|
48412
|
+
...resolvedBSessionId ? { dispatchedSessionId: resolvedBSessionId } : {}
|
|
48413
|
+
}
|
|
48233
48414
|
};
|
|
48234
48415
|
};
|
|
48235
48416
|
const complete = async (frame) => {
|
|
48236
48417
|
const { type: _t, requestId: _r, ...rest } = frame;
|
|
48418
|
+
const sessionId = typeof rest.sessionId === "string" ? rest.sessionId : void 0;
|
|
48237
48419
|
const args = DispatchCompleteArgsSchema.parse(rest);
|
|
48420
|
+
if (!sessionId) {
|
|
48421
|
+
throw new Error(
|
|
48422
|
+
"personaDispatch:complete requires sessionId (caller must pass x-clawd-session-id header)"
|
|
48423
|
+
);
|
|
48424
|
+
}
|
|
48425
|
+
const dispatchId = mgr.findInflightDispatchByBSessionId(sessionId);
|
|
48426
|
+
if (!dispatchId) {
|
|
48427
|
+
throw new Error(
|
|
48428
|
+
`no in-flight dispatch for session ${sessionId} (already completed or B was never dispatched)`
|
|
48429
|
+
);
|
|
48430
|
+
}
|
|
48238
48431
|
logger?.info("dispatch.complete.received", {
|
|
48239
|
-
dispatchId
|
|
48432
|
+
dispatchId,
|
|
48433
|
+
sessionId,
|
|
48240
48434
|
outcomeKind: args.outcome.kind
|
|
48241
48435
|
});
|
|
48242
|
-
mgr.complete(
|
|
48436
|
+
mgr.complete(dispatchId, args.outcome);
|
|
48243
48437
|
return {
|
|
48244
48438
|
response: { type: "personaDispatch:complete:ok" }
|
|
48245
48439
|
};
|
|
@@ -48269,25 +48463,37 @@ async function forwardDispatchToPeer(args) {
|
|
|
48269
48463
|
authorization: `Bearer ${args.contact.connectToken}`
|
|
48270
48464
|
},
|
|
48271
48465
|
// 注意:不带 targetDeviceId —— B 端据此判定为本地执行(B 角色)。
|
|
48272
|
-
|
|
48466
|
+
// targetSessionId 透传:peer 那台机会按 creatorPrincipalId === A.deviceId 校验。
|
|
48467
|
+
body: JSON.stringify({
|
|
48468
|
+
targetPersona: args.targetPersona,
|
|
48469
|
+
prompt: args.prompt,
|
|
48470
|
+
...args.targetSessionId ? { targetSessionId: args.targetSessionId } : {}
|
|
48471
|
+
})
|
|
48273
48472
|
});
|
|
48274
48473
|
} catch (err) {
|
|
48275
48474
|
const msg = err instanceof Error ? err.message : String(err);
|
|
48276
|
-
return { kind: "failure", reason: `forward to peer failed: ${msg}` };
|
|
48475
|
+
return { outcome: { kind: "failure", reason: `forward to peer failed: ${msg}` } };
|
|
48277
48476
|
}
|
|
48278
48477
|
let json;
|
|
48279
48478
|
try {
|
|
48280
48479
|
json = await res.json();
|
|
48281
48480
|
} catch {
|
|
48282
48481
|
return {
|
|
48283
|
-
|
|
48284
|
-
|
|
48482
|
+
outcome: {
|
|
48483
|
+
kind: "failure",
|
|
48484
|
+
reason: `peer returned non-JSON response (HTTP ${res.status})`
|
|
48485
|
+
}
|
|
48285
48486
|
};
|
|
48286
48487
|
}
|
|
48287
48488
|
if (json.ok === false) {
|
|
48288
|
-
return {
|
|
48489
|
+
return {
|
|
48490
|
+
outcome: { kind: "failure", reason: `peer rejected: ${json.error}: ${json.message}` }
|
|
48491
|
+
};
|
|
48289
48492
|
}
|
|
48290
|
-
return
|
|
48493
|
+
return {
|
|
48494
|
+
outcome: json.result.outcome,
|
|
48495
|
+
dispatchedSessionId: json.result.dispatchedSessionId
|
|
48496
|
+
};
|
|
48291
48497
|
}
|
|
48292
48498
|
async function forwardInboxPostToPeer(args) {
|
|
48293
48499
|
const f = args.fetchImpl ?? fetch;
|
|
@@ -57625,9 +57831,9 @@ async function startDaemon(config) {
|
|
|
57625
57831
|
// 127.0.0.1(不是 config.host)—— cc 跑在本机,loopback 最稳;外部访问限制 + http-router
|
|
57626
57832
|
// 的 isLoopback 兜底已确保安全。
|
|
57627
57833
|
getDaemonUrl: () => `http://127.0.0.1:${config.port}`,
|
|
57628
|
-
// Persona dispatch
|
|
57629
|
-
// - personaDispatchManager:
|
|
57630
|
-
//
|
|
57834
|
+
// Persona dispatch: manager 通过这两个 deps 跟 PersonaDispatchManager 协作。
|
|
57835
|
+
// - personaDispatchManager: createDispatchedSession / resumeDispatchedSession 用它
|
|
57836
|
+
// registerBSession;complete handler 用 findInflightDispatchByBSessionId 反查回 dispatchId。
|
|
57631
57837
|
// - dispatchMcpConfigPath: reducer 透传到 SpawnContext,cc spawn 加 --mcp-config flag。
|
|
57632
57838
|
personaDispatchManager,
|
|
57633
57839
|
dispatchMcpConfigPath: dispatchMcpConfigPath2,
|
|
@@ -58009,7 +58215,8 @@ async function startDaemon(config) {
|
|
|
58009
58215
|
logger.info("dispatch.spawnB.start", {
|
|
58010
58216
|
dispatchId: args.dispatchId,
|
|
58011
58217
|
targetPersona: args.targetPersona,
|
|
58012
|
-
sourceSessionId: args.sourceSessionId
|
|
58218
|
+
sourceSessionId: args.sourceSessionId,
|
|
58219
|
+
route: args.route
|
|
58013
58220
|
});
|
|
58014
58221
|
const sourceFile = manager.findOwnedSession(args.sourceSessionId);
|
|
58015
58222
|
if (!sourceFile && !args.guestPrincipalId) {
|
|
@@ -58028,9 +58235,23 @@ async function startDaemon(config) {
|
|
|
58028
58235
|
logger.info("dispatch.spawnB.source-resolved", {
|
|
58029
58236
|
dispatchId: args.dispatchId,
|
|
58030
58237
|
sourceJsonlPath,
|
|
58031
|
-
hasToolSessionId: Boolean(sourceFile?.toolSessionId)
|
|
58238
|
+
hasToolSessionId: Boolean(sourceFile?.toolSessionId),
|
|
58239
|
+
route: args.route
|
|
58032
58240
|
});
|
|
58033
|
-
|
|
58241
|
+
if (args.route === "resume") {
|
|
58242
|
+
if (!args.bSessionId) {
|
|
58243
|
+
throw new Error("resume route requires bSessionId");
|
|
58244
|
+
}
|
|
58245
|
+
const { sessionId: sessionId2 } = manager.resumeDispatchedSession({
|
|
58246
|
+
dispatchId: args.dispatchId,
|
|
58247
|
+
bSessionId: args.bSessionId,
|
|
58248
|
+
prompt: args.prompt,
|
|
58249
|
+
sourceJsonlPath,
|
|
58250
|
+
callerIdentity: args.guestPrincipalId ? { kind: "guest", sourcePrincipalId: args.guestPrincipalId } : { kind: "local", sourceSessionId: args.sourceSessionId }
|
|
58251
|
+
});
|
|
58252
|
+
return { bSessionId: sessionId2 };
|
|
58253
|
+
}
|
|
58254
|
+
const { sessionId } = manager.createDispatchedSession({
|
|
58034
58255
|
dispatchId: args.dispatchId,
|
|
58035
58256
|
sourceSessionId: args.sourceSessionId,
|
|
58036
58257
|
targetPersona: args.targetPersona,
|
|
@@ -58040,24 +58261,30 @@ async function startDaemon(config) {
|
|
|
58040
58261
|
guestPrincipalId: args.guestPrincipalId,
|
|
58041
58262
|
guestDisplayName: args.guestDisplayName
|
|
58042
58263
|
});
|
|
58264
|
+
return { bSessionId: sessionId };
|
|
58043
58265
|
},
|
|
58044
58266
|
// A 角色:从 ContactStore 取 peer 可达 URL + connect token,转发到对端 daemon /rpc。
|
|
58045
|
-
forwardToPeer: async ({ targetDeviceId, targetPersona, prompt }) => {
|
|
58267
|
+
forwardToPeer: async ({ targetDeviceId, targetPersona, prompt, targetSessionId }) => {
|
|
58046
58268
|
const contact = contactStore.get(targetDeviceId);
|
|
58047
58269
|
if (!contact || !contact.remoteUrl || !contact.connectToken) {
|
|
58048
58270
|
return {
|
|
58049
|
-
|
|
58050
|
-
|
|
58271
|
+
outcome: {
|
|
58272
|
+
kind: "failure",
|
|
58273
|
+
reason: `unknown or unreachable contact: ${targetDeviceId}`
|
|
58274
|
+
}
|
|
58051
58275
|
};
|
|
58052
58276
|
}
|
|
58053
58277
|
return forwardDispatchToPeer({
|
|
58054
58278
|
contact: { remoteUrl: contact.remoteUrl, connectToken: contact.connectToken },
|
|
58055
58279
|
targetPersona,
|
|
58056
|
-
prompt
|
|
58280
|
+
prompt,
|
|
58281
|
+
targetSessionId
|
|
58057
58282
|
});
|
|
58058
58283
|
},
|
|
58059
58284
|
// B 角色:判断 targetPersona 是否 public(跨设备授权边界,private 拒)。
|
|
58060
|
-
getPersonaPublic: (personaId) => personaRegistry.get(personaId)?.public ?? false
|
|
58285
|
+
getPersonaPublic: (personaId) => personaRegistry.get(personaId)?.public ?? false,
|
|
58286
|
+
// targetSessionId 复用路径的权限校验数据源:读 B session 的 SessionFile。
|
|
58287
|
+
findOwnedSession: (sessionId) => manager.findOwnedSession(sessionId)
|
|
58061
58288
|
});
|
|
58062
58289
|
handlers = { ...handlers, ...dispatchHandlers };
|
|
58063
58290
|
const shiftHandlers = buildShiftInternalHandlers({
|