@clawos-dev/clawd 0.2.619 → 0.2.620

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.
Files changed (31) hide show
  1. package/dist/app-builder-plugin/mcp-server.cjs +6 -15
  2. package/dist/cli.cjs +150 -699
  3. package/dist/dispatch/mcp-server.cjs +6 -28
  4. package/dist/persona-defaults/persona-app-builder/.claude/skills/app-builder-projects/scripts/collect-files.mjs +6 -15
  5. package/dist/persona-defaults/persona-app-builder/.claude/skills/app-builder-projects/scripts/list-projects.mjs +1 -2
  6. package/dist/persona-defaults/persona-app-builder/.claude/skills/app-builder-projects/scripts/record-push.mjs +3 -5
  7. package/dist/persona-defaults/persona-app-builder/.claude/skills/app-builder-projects/scripts/render-sql.mjs +4 -6
  8. package/dist/persona-defaults/persona-app-builder/extension-kit/examples/nestjs-react/server/src/app.module.ts +1 -7
  9. package/dist/persona-defaults/persona-app-builder/extension-kit/examples/nestjs-react/server/src/main.ts +3 -9
  10. package/dist/persona-defaults/persona-app-builder/extension-kit/examples/nestjs-react/server/src/polyfill.ts +3 -9
  11. package/dist/persona-defaults/persona-app-builder/extension-kit/examples/nestjs-react/server/src/table-name.ts +2 -5
  12. package/dist/persona-defaults/persona-app-builder/extension-kit/examples/nestjs-react/web/src/App.jsx +1 -8
  13. package/dist/persona-defaults/persona-app-builder/mcp/clawd-app-builder/mcp-server.cjs +6 -15
  14. package/dist/persona-defaults/persona-clawd-butler/.claude/skills/clawd-session-import/scripts/link-sessions.mjs +7 -23
  15. package/dist/persona-defaults/persona-clawd-butler/.claude/skills/scan-sessions-suggest-personas/scripts/extract-signals.mjs +6 -14
  16. package/dist/persona-defaults/persona-dataclaw-builder/.claude/skills/app-builder-projects/scripts/collect-files.mjs +6 -15
  17. package/dist/persona-defaults/persona-dataclaw-builder/.claude/skills/app-builder-projects/scripts/list-projects.mjs +1 -2
  18. package/dist/persona-defaults/persona-dataclaw-builder/.claude/skills/app-builder-projects/scripts/record-push.mjs +3 -5
  19. package/dist/persona-defaults/persona-dataclaw-builder/.claude/skills/app-builder-projects/scripts/render-sql.mjs +4 -6
  20. package/dist/persona-defaults/persona-dataclaw-builder/extension-kit/examples/nestjs-react/server/src/app.module.ts +2 -6
  21. package/dist/persona-defaults/persona-dataclaw-builder/extension-kit/examples/nestjs-react/server/src/dataclaw/dataclaw.client.ts +2 -4
  22. package/dist/persona-defaults/persona-dataclaw-builder/extension-kit/examples/nestjs-react/server/src/dataclaw/dataclaw.controller.ts +1 -2
  23. package/dist/persona-defaults/persona-dataclaw-builder/extension-kit/examples/nestjs-react/server/src/main.ts +4 -11
  24. package/dist/persona-defaults/persona-dataclaw-builder/extension-kit/examples/nestjs-react/server/src/polyfill.ts +1 -5
  25. package/dist/persona-defaults/persona-dataclaw-builder/extension-kit/examples/nestjs-react/web/src/api.js +1 -4
  26. package/dist/persona-defaults/persona-dataclaw-builder/extension-kit/examples/nestjs-react/web/src/components/BarChart.jsx +0 -1
  27. package/dist/persona-defaults/persona-dataclaw-builder/extension-kit/examples/nestjs-react/web/src/components/ResultCards.jsx +0 -1
  28. package/dist/persona-defaults/persona-dataclaw-builder/extension-kit/examples/nestjs-react/web/src/components/ResultTable.jsx +0 -1
  29. package/dist/persona-defaults/persona-dataclaw-builder/extension-kit/examples/nestjs-react/web/src/enums.js +2 -3
  30. package/dist/persona-defaults/persona-dataclaw-builder/mcp/clawd-app-builder/mcp-server.cjs +6 -15
  31. package/package.json +1 -1
package/dist/cli.cjs CHANGED
@@ -15656,18 +15656,9 @@ var init_claude_history = __esm({
15656
15656
  this.fileHistoryRoot = import_node_path5.default.join(base, "file-history");
15657
15657
  }
15658
15658
  /**
15659
- * [T-17] 定位一条 session 的 transcript 所在 project 目录,带 relocation 兜底。
15660
- *
15661
- * 常规路径:`<projectsRoot>/cwdToHashDir(cwd)`。但 CC 中途改 cwd(典型:会话里
15662
- * EnterWorktree)会把 `<tsid>.jsonl` 挪到新 hash 目录,而调用方手里的 cwd 可能仍是老值——
15663
- * persona session 尤其顽固:fork 出的新 session 因带 ownerPersonaId 被 manager 强制
15664
- * 改写 cwd 回 persona 根(session/manager.ts derivePersonaSpawnCwd),与 jsonl 实际
15665
- * 落点(源文件所在目录,见 session/fork.ts)永久分家。
15666
- * 拼不出路径时按 tsid 全局兜底(与 fork.ts / observer 同款),命中则用它所在目录。
15667
- *
15668
- * 兜底失败仍返回按 cwd 算的目录:下游 readJsonlLines / readdir 都容忍不存在,
15669
- * 保持"查无此 session 返回空"的既有语义,不抛。
15670
- * bug: quick-ask-blank-after-worktree(fork 出的快速问答面板整片空白)
15659
+ * Resolve the transcript directory, falling back to a global tsid search when cwd is stale.
15660
+ * EnterWorktree relocation and persona forks can leave cwd pointing at the original directory.
15661
+ * If no transcript is found, return the cwd-derived directory; readers tolerate it being absent.
15671
15662
  */
15672
15663
  resolveSessionDir(cwd, toolSessionId) {
15673
15664
  const byCwd = import_node_path5.default.join(this.projectsRoot, cwdToHashDir(cwd));
@@ -15676,14 +15667,8 @@ var init_claude_history = __esm({
15676
15667
  return found ? import_node_path5.default.dirname(found) : byCwd;
15677
15668
  }
15678
15669
  /**
15679
- * 定位一条 session 的 transcript 文件;不存在返回 null。
15680
- *
15681
- * 对外开这一口是为了让 adapter 之外的消费者**不必自己拼路径**:
15682
- * 「按 cwd 直取 → 按 tsid 全局兜底」这套 relocation 逻辑曾被抄成第二份,
15683
- * 规则一改就静默漂移。路径编码(cwdToHashDir)与兜底顺序全仓只此一处。
15684
- *
15685
- * 返回 null 而不是不存在的路径:调用方要能区分「还没写盘」与「读到空 transcript」——
15686
- * 后者会让核验把一条刚 spawn 的线判成假完成。
15670
+ * Resolve through the shared cwd/tsid relocation logic.
15671
+ * Return null before the file exists so callers can distinguish not-yet-written from empty history.
15687
15672
  */
15688
15673
  resolveTranscriptFile(cwd, toolSessionId) {
15689
15674
  const file = import_node_path5.default.join(this.resolveSessionDir(cwd, toolSessionId), `${toolSessionId}.jsonl`);
@@ -15972,9 +15957,8 @@ var init_claude_history = __esm({
15972
15957
  return result;
15973
15958
  }
15974
15959
  /**
15975
- * 对齐 CC `fileHistoryGetDiffStats`:以 snapshot 时刻的备份文件为旧版,磁盘当前为新版,
15976
- * 用 diff.structuredPatch 算每个文件的 hunks + 累加 +/-。磁盘/备份任何 IO 错误降级为
15977
- * 该文件跳过而不是整体失败
15960
+ * Preview the rewind: current content is old, snapshot backup is new.
15961
+ * structuredPatch supplies hunks and +/- totals; unreadable files are skipped individually.
15978
15962
  */
15979
15963
  computeRewindDiff(args) {
15980
15964
  const snapshots = this.readFileHistorySnapshots({
@@ -16607,10 +16591,8 @@ var init_claude = __esm({
16607
16591
  label: "Effort",
16608
16592
  description: "Reasoning effort level",
16609
16593
  scope: "tool-specific",
16610
- // CC `--effort` 值域(claude --help, 2.1.187):low / medium / high / xhigh / max。
16611
- // 2026-08-12:删掉 '' 项(理由同 CLAUDE_MODELS)。default 取 high —— CC 出厂默认虽是
16612
- // xhigh,但不传 --effort 时实际生效的是 ~/.claude/settings.json 的 effortLevel,
16613
- // 而它的常见值就是 high;用 high 作 persona 补齐值对既有会话行为最中性。
16594
+ // CC 2.1.187 --effort values. Use high when backfilling persona defaults to match common
16595
+ // settings.json effortLevel; omitting --effort would instead follow that file.
16614
16596
  options: [
16615
16597
  { value: "low", label: "Low" },
16616
16598
  { value: "medium", label: "Medium" },
@@ -16681,7 +16663,6 @@ var init_claude = __esm({
16681
16663
  this.logger?.info("claude spawn", { cmd, args, cwd: ctx.cwd });
16682
16664
  return child;
16683
16665
  }
16684
- // 暴露内部工具给测试直接校验 args 组合
16685
16666
  buildSpawnArgs(ctx) {
16686
16667
  return buildSpawnArgs(ctx);
16687
16668
  }
@@ -45376,7 +45357,6 @@ var SessionStoreFactory = class {
45376
45357
  dataDir;
45377
45358
  bareStore = null;
45378
45359
  vmOwnerStores = /* @__PURE__ */ new Map();
45379
- // vmGuest 索引 key = `${pid}::${capId}`(按 cap.id 隔离,一张 cap 一个目录)
45380
45360
  vmGuestStores = /* @__PURE__ */ new Map();
45381
45361
  constructor(opts) {
45382
45362
  this.dataDir = opts.dataDir;
@@ -45471,9 +45451,7 @@ function buildGuestSettingsV1() {
45471
45451
  sandbox: {
45472
45452
  enabled: true,
45473
45453
  autoAllowBashIfSandboxed: true,
45474
- // Bash 自动过沙箱;文件工具 dontAsk 自动拒(最严格只 Bash)
45475
45454
  allowUnsandboxedCommands: false,
45476
- // 关 dangerouslyDisableSandbox 逃逸口
45477
45455
  filesystem: {
45478
45456
  denyRead: ["~/"],
45479
45457
  denyWrite: [],
@@ -45954,13 +45932,11 @@ function cloneState(s2) {
45954
45932
  // 浅拷贝:下方 trackBackgroundTask 在 next 上原地增删,不能共享调用方数组(纯函数契约)
45955
45933
  pendingBgTasks: s2.pendingBgTasks ? s2.pendingBgTasks.slice() : void 0,
45956
45934
  stdinClosed: s2.stdinClosed,
45957
- // ReadyGate v2:readyForSend 是基础类型直接复制;pendingSend 必须浅拷贝防止
45958
- // reduce 内 push/shift 在调用方原 state 上副作用(reducer 纯函数契约)
45935
+ // 队列须复制,避免 push / shift 修改调用方 state;基础类型可直接复用。
45959
45936
  readyForSend: s2.readyForSend,
45960
45937
  pendingSend: s2.pendingSend.slice(),
45961
45938
  // 同理浅拷贝:入队 / 出队都在 next 上原地改
45962
45939
  queuedSends: s2.queuedSends.slice(),
45963
- // per-turn sender (spec §4.3):SenderInfo 只在 case 'send' 覆盖,其它 kind 直接复用引用
45964
45940
  currentTurnSender: s2.currentTurnSender
45965
45941
  };
45966
45942
  }
@@ -46134,12 +46110,8 @@ function buildSpawnContext(state, deps) {
46134
46110
  permissionMode: file.permissionMode,
46135
46111
  effort: file.effort,
46136
46112
  env,
46137
- // Phase 2 capability platform (plan §3): personaRoot 透传给 buildSpawnArgs;
46138
- // 内部 shouldSandbox(cwd, personaRoot) 决定是否注入 --settings sandbox-settings.
46139
46113
  personaRoot: deps.personaRoot,
46140
46114
  ...mcpConfigs.length > 0 ? { mcpConfigs } : {},
46141
- // shift v1:把 SessionFile.ownerPersonaId 透传到 SpawnContext;buildSpawnArgs 等下游
46142
- // 不直接读这个字段(只读 env),但保留显式字段方便 daemon 内部诊断/日志。
46143
46115
  personaId: personaId2
46144
46116
  };
46145
46117
  const meta = state.subSessionMeta;
@@ -46501,8 +46473,7 @@ function applyCommand(state, command, deps) {
46501
46473
  });
46502
46474
  return { state: next, effects };
46503
46475
  }
46504
- // 撤队列(spec 2026-08-31-chat-input-queue-design):带 id 删一条,不带 id 清空整队。
46505
- // 中断也走这条(manager.interrupt 转发),"中断 = 队列作废"是老板拍的语义。
46476
+ // 中断也通过 dequeue 撤队列。
46506
46477
  case "dequeue": {
46507
46478
  if (next.queuedSends.length === 0) return { state: next, effects };
46508
46479
  next.queuedSends = command.id ? next.queuedSends.filter((q3) => q3.id !== command.id) : [];
@@ -46804,11 +46775,7 @@ function reduceSessionInner(state, input, deps) {
46804
46775
  const eventEffects = [
46805
46776
  ...pushEventToBufferInPlace(next, { kind: "error", message: input.message }, deps),
46806
46777
  ...pushEventToBufferInPlace(next, { kind: "turn_end" }, deps)
46807
- // 丢掉 turn_end 顺带产出的 append-turn-duration:那条 effect 会往用户真实的 CC
46808
- // jsonl 追加一行 turn_duration(runner.ts case 'append-turn-duration',resume 场景
46809
- // toolSessionId 已 stamp 时会真落盘)。它的用途是"SDK 模式 CC 不写 turn 边界、
46810
- // daemon 补写",而这里 CC 根本没跑过这一轮——补边界等于往历史里插一个空 turn,
46811
- // 且不可回滚。proc-exit 的 interrupted 注入走同一条路但语义不同:那边 turn 真发生过。
46778
+ // 此轮未真正执行,不能向真实 CC jsonl 补空 turn 边界;与发生过执行的 proc-exit 中断不同。
46812
46779
  ].filter((e) => e.kind !== "append-turn-duration");
46813
46780
  return {
46814
46781
  state: next,
@@ -47193,28 +47160,19 @@ var SessionRunner = class {
47193
47160
  // sub-session idle-kill timer ref;key = sessionId(实际同一 runner 只对应一个 session,
47194
47161
  // 但 reducer Effect schema 带 sessionId 作为唯一键,对齐 schedule/cancel 配对)
47195
47162
  idleKillTimers = /* @__PURE__ */ new Map();
47196
- // file-sharing 待配对的 file-edit tool_use(spec §6 PR 3):在同一 session 流里
47197
- // tool_use(kind='tool_call')与 tool_result 通过 toolUseId 配对。tool_use 提前到,
47198
- // 等 tool_result 来时反查 path 调 onFileEdit。条目 in-flight 期间很少(个位数),
47199
- // 不需要 LRU;session 退出时整个 runner 销毁自然清理。
47163
+ // 按 toolUseId 等待成功的编辑结果才回写文件清单;退出时清理,避免迟到结果重建已删清单。
47200
47164
  pendingFileEdits = /* @__PURE__ */ new Map();
47201
47165
  getState() {
47202
47166
  return this.state;
47203
47167
  }
47204
- // 外部推送 input 到 reducer,然后执行产出的 effects
47205
47168
  input(inputMsg) {
47206
47169
  const adapter = this.hooks.adapter;
47207
47170
  const deps = {
47208
- // file-sharing (spec §6 PR 3):在 stdout-line 解析时同步观察 file-edit 工具事件,
47209
- // 配对 tool_use ↔ tool_result 调 onFileEdit。原 reducer 路径不变。
47210
47171
  parseLine: (l2) => {
47211
47172
  const events = adapter.parseLine(l2);
47212
47173
  if (this.hooks.onFileEdit) this.observeForFileEdit(events);
47213
47174
  return events;
47214
47175
  },
47215
- // Persona dispatch (Task 9):原 in-place `@persona/<id>` 注入路径已删,cc 自己看到字面
47216
- // token 会调 personaDispatch MCP tool 委派(dispatch.mcp.json 通过 cc --mcp-config 加载,
47217
- // CLAUDE.md 教 cc 触发条件)。encodeStdin 现在是 adapter 的简单 passthrough。
47218
47176
  encodeStdin: (t2, ctx) => adapter.encodeStdin(t2, ctx) ?? "",
47219
47177
  bufferCap: this.hooks.bufferCap ?? 500,
47220
47178
  now: this.hooks.now ?? Date.now,
@@ -47224,20 +47182,11 @@ var SessionRunner = class {
47224
47182
  ownerDeviceId: this.hooks.ownerDeviceId,
47225
47183
  ownerFeishuUnionId: this.hooks.ownerFeishuUnionId,
47226
47184
  personaRoot: this.hooks.personaRoot,
47227
- // 单栏 refactor (spec 2026-06-02 §5.1): cc 子进程 env 注入 CLAWD_DAEMON_URL,让 assistant
47228
- // MCP server 拼 /rpc/<method>(Bearer)调 daemon。null = HTTP RPC 未启。
47229
47185
  getDaemonUrl: this.hooks.getDaemonUrl,
47230
- // clawd 管家 (spec 2026-07-22-clawd-butler-persona §3.1): cc 子进程 env 注入 CLAWOS_API,
47231
- // 让 clawd 管家 skill fetch `${CLAWOS_API}/docs/**` 线上文档。
47232
47186
  getClawosApi: this.hooks.getClawosApi,
47233
- // daemon 级 MCP server 列表闭包透传;见 reducer.buildSpawnContext 派生
47234
- // SpawnContext.mcpConfigs + 按 DISPATCH_MCP_ID 判断 dispatch 教学注入
47235
47187
  getMcpConfigs: this.hooks.getMcpConfigs,
47236
- // 人格补充层读闭包透传;reducer.buildSpawnContext 按 SessionFile.ownerPersonaId 查,
47237
- // 有内容就作为最后一段追加进 extraSystemPrompt
47238
47188
  readPersonalityLocal: this.hooks.readPersonalityLocal,
47239
47189
  lookupDispatchByBSessionId: this.hooks.lookupDispatchByBSessionId,
47240
- // ReadyGate v2:透传 mode 让 reducer send / ready-detected 分支决定走暂存队列还是直写
47241
47190
  mode: this.hooks.mode,
47242
47191
  // [RG-DBG] 注入 logger 让 reducer rgDbg 走 pino 进 clawd.log;定位完跟 rgDbg 一起删
47243
47192
  logger: this.hooks.logger
@@ -47263,9 +47212,7 @@ var SessionRunner = class {
47263
47212
  this.recoveringCloud = false;
47264
47213
  }
47265
47214
  }
47266
- // 等子进程退出(procAlive=false)。manager.stop 在发完 SIGTERM 后调它确保
47267
- // 真停下来再 ack 给前端,从而避免"删 worktree 时 CC 还活着持有文件锁"的 race。
47268
- // 已经停止则立即 resolve;超时(默认 3000ms)抛错由上层透传成 RPC error
47215
+ // 等 procAlive=false 后返回,避免停止 RPC ack 后仍持文件锁;已停止立即 resolve,超时报错。
47269
47216
  async waitUntilStopped(timeoutMs = DEFAULT_WAIT_STOP_TIMEOUT_MS) {
47270
47217
  if (!this.state.procAlive) return;
47271
47218
  return new Promise((resolve9, reject) => {
@@ -47323,15 +47270,8 @@ var SessionRunner = class {
47323
47270
  if (this.state.procAlive) this.input({ kind: "proc-exit", code: null });
47324
47271
  }
47325
47272
  /**
47326
- * file-sharing tool_use ↔ tool_result 配对(spec §6 PR 3)。
47327
- *
47328
- * 1) tool_call kind + tool ∈ FILE_EDIT_TOOLS → cache toolUseId → { tool, relPath }
47329
- * relPath 从 input 里取(Write/Edit/MultiEdit 是 file_path,NotebookEdit 是 notebook_path)。
47330
- * 2) tool_result kind + cache 命中 + 非 error → 调 onFileEdit + 删 cache
47331
- * 3) tool_result kind + cache 命中 + 是 error → 删 cache,不调(spec 只要"成功"才入清单)
47332
- * 4) tool_call 但 input 没有 path → 不 cache(防御)
47333
- *
47334
- * 不破坏 reducer 路径——纯只读扫描 events 数组。
47273
+ * 只读配对 tool_call / tool_result;有路径且成功的文件编辑调用 onFileEdit。
47274
+ * 错误结果只清 cache,缺路径不登记,不改变 reducer 事件流。
47335
47275
  */
47336
47276
  observeForFileEdit(events) {
47337
47277
  const cb = this.hooks.onFileEdit;
@@ -47367,9 +47307,7 @@ var SessionRunner = class {
47367
47307
  }
47368
47308
  }
47369
47309
  }
47370
- // 向子进程 stdin 写一条 CC IPC `control_request` 并返回 Promise<response>
47371
- // —— CC 侧会异步回写匹配 request_id 的 `control_response` 帧,
47372
- // 在 stdout 行处理入口被 tryHandleControlResponse 拦截并 resolve pending
47310
+ // CC control_request 按 request_id 匹配异步 control_response;退出或超时拒绝等待者。
47373
47311
  sendControlRequest(subtype, extraRequestFields = {}, opts = {}) {
47374
47312
  const proc = this.proc;
47375
47313
  if (!proc || !this.state.procAlive) {
@@ -47438,7 +47376,6 @@ var SessionRunner = class {
47438
47376
  }
47439
47377
  this.pendingControlRequests.clear();
47440
47378
  }
47441
- // 执行单个 effect。每种 effect 对应一次具体副作用,不跨种类组合
47442
47379
  applyEffect(effect) {
47443
47380
  switch (effect.kind) {
47444
47381
  case "spawn":
@@ -47656,12 +47593,8 @@ var SessionRunner = class {
47656
47593
  });
47657
47594
  }
47658
47595
  /**
47659
- * stdin 唯一写入口。stdin 已 EOF(close-stdin 收进程收尾中)时丢弃并告警——写进已关闭的
47660
- * 管道会抛 EPIPE 把 daemon 打崩。reducer 侧保证正常路径不会撞上(send 遇 stdinClosed 先
47661
- * kill 再 spawn),这道闸兜的是 permission / 问答回写等异步路径:它们的 effect 可能在
47662
- * turn_end 关 stdin 之后才被执行。
47663
- *
47664
- * recorder tap 收在这里,保证"写成功才录",录制流与真实字节流不分叉。
47596
+ * stdin 唯一入口:EOF 后拒绝权限 / 问答等迟到写入,避免 EPIPE。
47597
+ * 正常 send 由 reducer 先收旧进程再 spawn;仅成功写入才 tap recorder。
47665
47598
  */
47666
47599
  writeStdin(payload) {
47667
47600
  const stdin = this.proc?.stdin;
@@ -47676,10 +47609,8 @@ var SessionRunner = class {
47676
47609
  this.recorder?.tapStdinWrite(payload);
47677
47610
  return true;
47678
47611
  }
47679
- // kill 当前 proc:单发 signal,没有"先 TERM 后 KILL"的兜底。
47680
- // reducer 推下来的 signal 是 SIGKILL(见 reducer.ts 各 kill 推送处)——选择 SIGKILL
47681
- // 是因为 CC 在 streaming hang / 等 control_response / API 半连接 等场景下不响应 SIGTERM,
47682
- // 直接 SIGKILL 一次到位。代价是 CC 来不及落盘当前轮 jsonl,但 observer 路径已经回灌过。
47612
+ // 按 reducer 指定信号 kill,不升级或另设策略;SIGKILL 避免 CC 挂起时不响应 SIGTERM。
47613
+ // 代价是当前轮 CC 尾部 jsonl 可能未落盘。
47683
47614
  doKill(signal) {
47684
47615
  if (!this.proc) return;
47685
47616
  try {
@@ -47776,16 +47707,13 @@ function makeInitialState(file, subSessionMeta) {
47776
47707
  procAlive: false,
47777
47708
  seenUuids: [],
47778
47709
  subSessionMeta,
47779
- // 后台任务台账(close-stdin 判据);由 subagent_progress 事件流维护,spawn 时天然为空
47780
47710
  pendingBgTasks: [],
47781
47711
  stdinClosed: false,
47782
47712
  // ReadyGate v2:默认值兼容 SDK happy path(直写 stdin,不走队列)。
47783
47713
  // TUI 模式由 reducer send 分支按 deps.mode='tui' 主动翻 readyForSend=false
47784
47714
  readyForSend: true,
47785
47715
  pendingSend: [],
47786
- // 输入框排队队列(spec 2026-08-31-chat-input-queue-design):初始空
47787
47716
  queuedSends: [],
47788
- // per-turn sender 2026-07-08 spec §4.3:初始 null;case 'send' 首次覆盖
47789
47717
  currentTurnSender: null
47790
47718
  };
47791
47719
  }
@@ -47808,76 +47736,31 @@ var SessionManager = class {
47808
47736
  // tool id → capabilities 快照(仅 capabilities:get RPC 缓存用;contextWindowSize 走 adapter.resolveContextWindow,无需缓存)。
47809
47737
  // loadedAt 给 engine:status 显示「模型表上次刷新时间」;refresh 走 getCapabilities({ refresh:true }) 整条覆盖
47810
47738
  capabilitiesCache = /* @__PURE__ */ new Map();
47811
- // sessionId → (synth uuid → real uuid):
47812
- // synth uuid = daemon 在 reducer 'send' 命令里生成、写进 buffer 里 user_text 事件的 uuid
47813
- // real uuid = CC 写 jsonl 时给 user 行分配的 uuid(也是 file-history-snapshot.messageId 的来源)
47814
- // 由 observer 监听 jsonl user 行后调 recordRealUserUuid 建立映射;rewind 系列 RPC 在
47815
- // 入参 / 出参做转译,保证 UI 看到的 uuid 始终是 events 流里的 synth uuid
47739
+ // observer 建立 synth user uuid → jsonl real uuid 映射;rewind 对外始终使用 UI 的 synth uuid。
47816
47740
  realUuidBySynth = /* @__PURE__ */ new Map();
47817
- // observer 收到 `turn_duration` 信号但屏幕还没稳定 5s → 挂进 pending,等 `notifyScreenIdle`
47818
- // (屏幕 armed=true 触发点)flush 成真正的 turn_end 进 reducer。
47819
- //
47820
- // 语义澄清:`turn_duration` 是 CC 报的原始信号("本轮 API 调用完了"),**不代表 turn 真的结束**
47821
- //(背景 agent 可能还在跑)。屏幕 5s 稳定 + 无 popup 才是"确认信号"。
47822
- // 两者 AND 后 daemon 才产生真正的 `turn_end` 事件送给 reducer。
47823
- //
47824
- // pending 不设截止时间:屏幕稳定的时机由 CC UI 决定,可能几秒也可能几分钟。观察者以
47825
- // "屏幕真的稳定"作为触发点,signal-driven 而非 timer-driven。
47826
- //
47827
- // 清理点:newSession / stop / session-delete / stopAll。
47828
- // sessionId → 被 gate drop 的 turn_end 携带的 durationMs(可能 undefined);flush 注入时回填
47741
+ // TUI turn_duration 必须与屏幕稳定、无 popup 同时成立才形成 turn_end。
47742
+ // gate 未开时暂存时长,等 notifyScreenIdle 确认;不设截止时间、不凭屏幕信号单独补 turn_end。
47743
+ // new / stop / delete / stopAll 清理。
47829
47744
  pendingTurnDurationSignals = /* @__PURE__ */ new Map();
47830
- // SessionStore 按 scope 派生(root = <dataDir>/sessions/<scopeSubPath>/)。
47831
- // default scope 直接复用 deps.store;persona scope(owner / listener)第一次访问时按需创建并缓存。
47832
- // 取代旧的 storesByAgent —— agentId 概念由 SessionScope 取代,路径即身份,
47833
- // SubSessionMeta 由 scope 派生(metaFromScope),不再需要运行时 cache 或 ownerPersonaId 兜底。
47745
+ // 无 storeFactory 时按 scope 缓存 SessionStore;default 复用 deps.store。
47834
47746
  storesByScope = /* @__PURE__ */ new Map();
47835
- // persona-bound transport 订阅器:sessionId → Set<listener>。
47836
- // 透传 owner 路径白名单 EventFrame(routeFromRunner 决定哪些 type 进入),listener 端按
47837
- // `frame.type` narrow 出 'session:event'(ParsedEvent)/ 'session:status'(procAlive 同步)等。
47838
- // 这样 owner / listener 共用同一组事件字面量,新增跨端信号时只需在 fan-out 白名单里加 type。
47747
+ // persona-bound 订阅器与 wire 广播并行,信号类型由 routeFromRunner 的白名单限定。
47839
47748
  eventSubscribers = /* @__PURE__ */ new Map();
47840
- // TUI 模式:toolSessionId → PtyTransport,由 ClaudeTuiAdapter spawn 时通过
47841
- // onPtyRegister callback 写入。handler 处理 tui:pty:input / tui:pty:resize 时
47842
- // 走 getPtyBySessionId 反查,找到 transport 调 pty.write / pty.resize。
47843
- // 进程 exit 时由 onPtyUnregister 清理;同一 toolSessionId 重 spawn 会覆盖
47749
+ // adapter 注册 toolSessionId → pty;exit 清理,respawn 覆盖,RPC 按 sessionId 反查。
47844
47750
  ptyTransports = /* @__PURE__ */ new Map();
47845
- // TUI 模式:toolSessionId → 当前屏幕 + popup 快照闭包。ClaudeTuiAdapter spawn 时通过
47846
- // onPtyReplayRegister callback 写入;闭包持引用 adapter 内部 @xterm/headless detector,
47847
- // 调用时立即 serialize 当前 buffer + scrollback。
47848
- // 新 client 通过 session:subscribe 接入时由 onSubscribe hook 取 getPtyReplay(sessionId)
47849
- // 把屏幕快照定向 emit,解决"刷新页面 / 第二个 tab 接入时 xterm 白屏"
47751
+ // 当前屏幕快照闭包;新订阅者定向回放,避免刷新或新 tab 只收到增量而白屏。
47850
47752
  ptyReplaysByToolSid = /* @__PURE__ */ new Map();
47851
- // TUI 模式:toolSessionId → @xterm/headless TerminalSurface(单一虚拟终端,popup / ready
47852
- // observer 都从这一份 buffer 派生状态)。ClaudeTuiAdapter spawn 时通过 onSurfaceRegister
47853
- // callback 写入;exit 时 onSurfaceUnregister 清理。
47854
- // manager.resizePty 用此映射把 UI 的 session:pty:resize 同步到 surface 内部 buffer,
47855
- // 保证 snapshot serialize 时 cursor positioning 与 UI xterm 实际尺寸一致。
47753
+ // pty 与 detector 共用的终端镜像;resize 必须同步,保证 snapshot 光标位置与 UI 一致。
47856
47754
  surfacesByToolSid = /* @__PURE__ */ new Map();
47857
- // TUI 模式:sessionId → UI 最近一次上报的 xterm 尺寸。resizePty 入口无条件记录(含 pty
47858
- // 尚未 spawn / runner 尚未创建的早到 resize),registerPty / registerSurface 时回放——
47859
- // 修「mount resize 早于 lazy spawn 被丢」与「respawn 回到 120×40 后无人补发」的尺寸错配。
47860
- // 键用 sessionId 而非 toolSessionId:/clear 会 reassign tsid,sessionId 跨 respawn 稳定
47755
+ // 早于 lazy spawn 的 UI resize 先缓存,注册 pty / surface 时回放。
47756
+ // 用跨 respawn 稳定的 sessionId 作键,不能用 /clear 会变化的 toolSessionId。
47861
47757
  lastUiSizeBySessionId = /* @__PURE__ */ new Map();
47862
- // 仅 mode='tui' 需要:index.ts 装配阶段在 observer 构造后注入。
47863
- // observer.onEvent 要回灌 manager.feedObserverEvents,所以 observer 必须晚于 manager
47864
- // 构造;反过来 manager 又要在 mode='tui' 下 spawn session 时 attach observer。setter 解循环
47758
+ // 通过 setter 注入 observer,避免 observer 回调依赖 manager 形成构造循环。
47865
47759
  attachedObserver = null;
47866
47760
  /** 仅 mode='tui' 走这条路径;调用方负责保证在使用前已 attach */
47867
47761
  attachObserver(observer) {
47868
47762
  this.attachedObserver = observer;
47869
47763
  }
47870
- // 按 scope 拿对应的 SessionStore.
47871
- // Phase 2 (capability platform plan §1) 路由切到 SessionStoreFactory:
47872
- // default → factory.forBare() <dataDir>/sessions/
47873
- // persona/<pid>/owner → factory.forVmOwner(pid) <dataDir>/personas/<pid>/.clawd/sessions/owner/
47874
- // persona/<pid>/guest → factory.forVmGuest(pid, capId) <dataDir>/personas/<pid>/.clawd/sessions/guests/<capId>/
47875
- // 缺省 (无 factory 注入): 回退 dataDir+scope 老路径 (向后兼容旧 spec).
47876
- /**
47877
- * cwd 必须是本机真实目录——除非 adapter 是远端的(云上 persona,ToolAdapter.remote):那时 cwd 是
47878
- * 「会话在哪跑」(gateway 上的地址),本机没有对应目录,stat 必然失败但那不是错。
47879
- * 三个建会话入口(create / linkExisting / createFromExternal)共用这一处。
47880
- */
47881
47764
  /**
47882
47765
  * persona 会话的 cwd,四个建会话入口(create / dispatch / shift / lark)共用:本机 persona 是
47883
47766
  * personaRoot/<id>,云上 persona 是 gateway 上的地址(PersonaSessionDefaults.cloud)。
@@ -47889,6 +47772,7 @@ var SessionManager = class {
47889
47772
  defaults?.cloud
47890
47773
  );
47891
47774
  }
47775
+ /** 远端 adapter 的 cwd 属于 gateway;仅对本机 adapter 检查目录存在性。 */
47892
47776
  assertLocalCwd(cwd, tool) {
47893
47777
  if (this.deps.getAdapter(tool).remote) return;
47894
47778
  try {
@@ -47897,6 +47781,7 @@ var SessionManager = class {
47897
47781
  throw new ClawdError(ERROR_CODES.INVALID_CWD, `cwd not a directory: ${cwd}`);
47898
47782
  }
47899
47783
  }
47784
+ /** 优先按 factory 路由;缺省时普通 session 用 deps.store,其余按 dataDir + scope 缓存。 */
47900
47785
  storeFor(scope) {
47901
47786
  if (this.deps.storeFactory) {
47902
47787
  if (scope.kind === "default") return this.deps.storeFactory.forBare();
@@ -47926,15 +47811,10 @@ var SessionManager = class {
47926
47811
  capabilitiesLoadedAt(tool) {
47927
47812
  return this.capabilitiesCache.get(tool)?.loadedAt;
47928
47813
  }
47929
- // 由 SessionFile 反推 scope 的 thin wrapper —— 决策矩阵收敛到 scope.scopeForFile
47930
- // free function(scope.spec.ts transition 表驱动测试),manager 只负责注入 deps.
47931
47814
  scopeForFile(file) {
47932
47815
  return scopeForFile(file, this.deps.ownerPrincipalId);
47933
47816
  }
47934
- // 扫所有 persona 命名空间. 用于 findOwnedSession / listAllOwned 跨 scope 查询.
47935
- // Phase 2 capability platform: 新布局下 persona 资产在 <dataDir>/personas/<pid>/,
47936
- // 直接 readdir 这个目录拿所有 personaId. 老布局 (无 storeFactory) fallback 扫
47937
- // <dataDir>/sessions/ 列子目录 (排除 'default').
47817
+ // 枚举 persona:有 factory 时扫 personas/,兼容模式扫 sessions/(排除 default)。
47938
47818
  listPersonaIdsOnDisk() {
47939
47819
  if (!this.deps.dataDir) return [];
47940
47820
  const root = this.deps.storeFactory ? import_node_path9.default.join(this.deps.dataDir, "personas") : import_node_path9.default.join(this.deps.dataDir, "sessions");
@@ -47948,9 +47828,7 @@ var SessionManager = class {
47948
47828
  }
47949
47829
  return entries.filter((e) => e.isDirectory() && e.name !== "default").map((e) => e.name);
47950
47830
  }
47951
- // 扫某 persona 下 guests/ 子目录, 拿所有 capId.
47952
- // 路径: <dataDir>/personas/<pid>/.clawd/sessions/guests/<capId>/
47953
- // 只在 storeFactory 注入 (新布局) 下生效, 老布局无 guest 目录.
47831
+ // 仅 factory 布局枚举 persona 的 guests/<capId>/。
47954
47832
  listGuestCapIdsForPersona(personaId2) {
47955
47833
  if (!this.deps.dataDir || !this.deps.storeFactory) return [];
47956
47834
  const root = import_node_path9.default.join(
@@ -47971,18 +47849,8 @@ var SessionManager = class {
47971
47849
  }
47972
47850
  return entries.filter((e) => e.isDirectory()).map((e) => e.name);
47973
47851
  }
47974
- // owner / default + persona guests 三大分类下按 sessionId 找文件——前端只传 sessionId 不带 scope,
47975
- // SessionManager 在这里定位。四层快慢路径:
47976
- // 1) active runner(用户当前在用的 session):runner.state.file 是 SessionFile 内存权威副本,
47977
- // 直接返回,零磁盘 I/O。覆盖 attachment / file-sharing 等"用户操作 → 紧接着 RPC"的
47978
- // 绝大多数场景
47979
- // 2) default scope 磁盘:inactive default session 命中
47980
- // 3) 所有 persona owner 目录扫盘:inactive persona owner session 命中
47981
- // 4) 每个 persona 的 guests/<capId>/ 扫盘:guest 通过我颁发 token 接进来创建的 session
47982
- // listener sub-session 不走这条——transport 始终明确传 (personaId, sessionId)。
47983
- //
47984
- // 公开方法:attachment / file-sharing handler 通过 deps 闭包消费,不应直接持 SessionStore
47985
- // (持 default-only store 是 file-sharing v1 的预存 bug 根因——见 fix #703)
47852
+ // sessionId 跨 scope 定位:优先 runner 的内存权威副本,再扫 default / persona owner / guest 存储。
47853
+ // 调用方通过 manager 闭包访问,不能持 default-only store 漏掉 persona 会话。
47986
47854
  findOwnedSession(sessionId) {
47987
47855
  const runner = this.runners.get(sessionId);
47988
47856
  if (runner) return runner.getState().file;
@@ -48000,17 +47868,12 @@ var SessionManager = class {
48000
47868
  }
48001
47869
  return null;
48002
47870
  }
48003
- // findOwnedSession + scopeForFile 收口。把"sessionId → SessionScope"的派生
48004
- // 集中到 manager(scope 单一来源),调用方拿 scope 不再自己拼 object 也不依赖
48005
- // ownerPersonaId 字段语义。给 attachment handler 通过 deps.getSessionScope 闭包消费。
47871
+ // scope 的派生统一走 scopeForFile,调用方不自行按 ownerPersonaId 拼 scope。
48006
47872
  findOwnedSessionScope(sessionId) {
48007
47873
  const file = this.findOwnedSession(sessionId);
48008
47874
  return file ? this.scopeForFile(file) : null;
48009
47875
  }
48010
- // 合并 default + 每个 persona 的 owner/ + guests/<capId>/ —— 桌面 App 主 session 列表入口.
48011
- // guest sessions 由 handlers/session.ts list 处按 ctx.principal.kind === 'guest' 做权限过滤
48012
- // (creatorPrincipalId === capId), 这里仅负责数据源汇总.
48013
- // listener sub-session 不走这条(listener 走 persona:listSubSessions RPC 单独入口)。
47876
+ // 汇总 default / persona owner / guest;这里只汇总,调用方 handler 负责按身份过滤。
48014
47877
  listAllOwned() {
48015
47878
  const out = [];
48016
47879
  out.push(...this.deps.store.list());
@@ -48038,8 +47901,6 @@ var SessionManager = class {
48038
47901
  cwd: file.cwd
48039
47902
  }));
48040
47903
  }
48041
- // 写回 SessionFile,自动根据 ownerPersonaId / creatorPrincipalId 派生写入 scope.
48042
- // 调用方原先都是 `deps.store.write(updated)`,全部走这里收口路由.
48043
47904
  writeOwned(file) {
48044
47905
  return this.storeFor(this.scopeForFile(file)).write(file);
48045
47906
  }
@@ -48056,12 +47917,7 @@ var SessionManager = class {
48056
47917
  }
48057
47918
  return false;
48058
47919
  }
48059
- // 创建 runner 时包一层 broadcast hook:所有外出 frame 统一走 routeFromRunner,
48060
- // 经过 compressFrameForWire 后决定是 push collector 还是走 deps.broadcastFrame。
48061
- // scope 决定两件事:SessionStore 路由 + SubSessionMeta 派生(metaFromScope)。
48062
- // - default → 复用 deps.store,无 subSessionMeta(普通 session)
48063
- // - persona/owner → personas/<pid>/.clawd/sessions/owner/,meta={idleKillDelayMs:<config 默认 6h>, personaMode:'owner'}
48064
- // - persona/guest → personas/<pid>/.clawd/sessions/guests/<capId>/,meta={idleKillDelayMs:10min, personaMode:'guest', extraSettings:<sandbox>}
47920
+ // 按 scope 装配存储和运行时 meta;广播统一经 routeFromRunner 压缩和路由。
48065
47921
  newRunner(file, scope) {
48066
47922
  const adapter = this.deps.getAdapter(file.tool ?? "claude");
48067
47923
  const store = this.storeFor(scope);
@@ -48125,36 +47981,19 @@ var SessionManager = class {
48125
47981
  spawnOverride: this.deps.spawnOverride,
48126
47982
  now: this.deps.now,
48127
47983
  bufferCap: this.deps.bufferCap,
48128
- // adapter 自己持有模型表 + 兜底逻辑(contains / opus-1M / [1m] 等),manager 不再 cache 转发
48129
47984
  resolveContextWindow: (tool, modelId) => this.deps.getAdapter(tool).resolveContextWindow(modelId),
48130
47985
  dataDir: this.deps.dataDir,
48131
- // personaStore 不再透给 runner hooks(in-place mention 注入路径已删,runner 不用);
48132
- // manager 自身的 sandbox 派生仍直接读 this.deps.personaStore。
48133
47986
  ownerDisplayName: this.deps.ownerDisplayName,
48134
- // T-31 persona prompt injection:owner 设备 id(= ownerPrincipalId)+ 飞书 union_id
48135
- // 供 reducer 组 promptArgs 时作为「本 persona 的 owner」身份卡数据源(guest session 用)
48136
- // 或「当前连接方」身份卡的兜底源(owner session 老路径未 stamp caller 时)。
48137
47987
  ownerDeviceId: this.deps.ownerPrincipalId,
48138
47988
  ownerFeishuUnionId: this.deps.ownerFeishuUnionId,
48139
- // Phase 2 capability platform (plan §3): 透传 personaRoot, buildSpawnArgs 据
48140
- // cwd 是否在 personaRoot 下自动决定是否注入 --settings sandbox-settings.json.
48141
47989
  personaRoot: this.deps.personaRoot,
48142
- // 单栏 refactor (spec 2026-06-02 §5.1): 透传 daemon HTTP RPC base URL 闭包,
48143
- // reducer 把它注入 cc 子进程 env CLAWD_DAEMON_URL.
48144
47990
  getDaemonUrl: this.deps.getDaemonUrl,
48145
- // clawd 管家 (spec 2026-07-22-clawd-butler-persona §3.1): 透传 clawos 云 api base URL
48146
- // 闭包, reducer 把它注入 cc 子进程 env CLAWOS_API, skill fetch 线上 md 文档用。
48147
47991
  getClawosApi: this.deps.getClawosApi,
48148
- // daemon 级 MCP server 列表闭包透传给 reducer,cc spawn 逐项加 --mcp-config flag;
48149
- // B session 的 CLAWD_DISPATCH_ID env 由下方 lookupDispatchByBSessionId 反查闭包负责。
48150
47992
  getMcpConfigs: () => this.deps.mcpConfigs ?? [],
48151
- // persona 级凭据注入(M1b Step 5):闭包直接透传,manager 不碰凭据值本身。
48152
47993
  resolvePersonaMcpConfig: this.deps.resolvePersonaMcpConfig,
48153
- // 人格补充层:只把 store 的这一个读方法窄化成闭包给 runner/reducer(不把整个 personaStore
48154
- // 递下去——in-place mention 注入那次的教训,见上面那条注释)。缺 store = 不注补充段。
47994
+ // 只传人格补充层读闭包,避免把整个 store 下沉;缺 store 不追加。
48155
47995
  readPersonalityLocal: this.deps.personaStore ? (id) => this.deps.personaStore?.readPersonalityLocal(id) ?? null : void 0,
48156
47996
  lookupDispatchByBSessionId: this.deps.personaDispatchManager ? (bSid) => this.deps.personaDispatchManager?.lookupDispatchByBSessionId(bSid) : void 0,
48157
- // file-sharing (spec §6 PR 3):闭包 scope + sessionId,runner 只暴露 tool/relPath/cwd
48158
47997
  onFileEdit: attachmentGroup ? (input) => attachmentGroup.onFileEdit({
48159
47998
  scope,
48160
47999
  sessionId: file.sessionId,
@@ -48162,8 +48001,6 @@ var SessionManager = class {
48162
48001
  relPath: input.relPath,
48163
48002
  cwd: input.cwd
48164
48003
  }) : void 0,
48165
- // ReadyGate v2:透传 daemon mode,runner 装配 ReducerDeps 时让 reducer 判断
48166
- // send / ready-detected 分支走暂存队列还是直写
48167
48004
  mode: this.deps.mode
48168
48005
  });
48169
48006
  if (this.deps.mode === "tui" && (file.tool ?? "claude") === "claude" && !file.toolSessionId) {
@@ -48469,25 +48306,8 @@ var SessionManager = class {
48469
48306
  const file = this.getFile(args.sessionId);
48470
48307
  return { response: file, broadcast: [] };
48471
48308
  }
48472
- // mirror peer sessions (2026-05-26):把来自朋友 daemon 的 SessionFile(originOwnerPrincipalId
48473
- // 非空)持久化到本机 default store。dumb write,**不广播 push 帧**——UI 是写入唯一源,
48474
- // 双写完成后通过 cache.setData 同步 sidebar 本地 state。
48475
- //
48476
- // 强制走 deps.store(即 default scope),不走 scopeForFile:mirror 物理落 default 即可。
48477
- // 远端 creator / origin 字段原值保留(不被本机 ctx 派生覆写)。
48478
- //
48479
- // strip 远端 personaId / dispatchedFromSessionId / shiftFiredFromSessionId:mirror
48480
- // SessionFile 上这些字段都指向朋友 daemon 上的本地 id,本机不持有对应 persona /
48481
- // 源 session。留着会让按这些字段反查的本机消费者(MyPersonasSection 按 ownerPersonaId
48482
- // 反查 persona、sidebar 按 dispatchedFromSessionId / shiftFiredFromSessionId 折叠
48483
- // sub-session)走错分支。写入前清掉。#750 一族防线。
48484
- //
48485
- // shift fire 是本地 daemon scheduler 行为,理论上不产生 mirror SessionFile,但留 strip
48486
- // 防御(同 dispatchedFromSessionId 处理姿势对齐)。
48487
- //
48488
- // 与 manager.create / manager.update 区别:
48489
- // - 不走 newRunner / attachObserver / cwd 校验(mirror 是纯 metadata 层)
48490
- // - 不发 session:info 帧(manager.create / .update 通过 reducer/wireCollector 发 push)
48309
+ // mirror 仅持久化远端元数据到 default store,不建 runner、校验 cwd 或广播;UI 自行更新缓存。
48310
+ // 保留远端 creator / origin,移除只在对端有效的 persona / dispatch / shift 关联,防本机错误归组。
48491
48311
  mirrorUpsert(file) {
48492
48312
  const {
48493
48313
  ownerPersonaId: _strippedPid,
@@ -48497,9 +48317,7 @@ var SessionManager = class {
48497
48317
  } = file;
48498
48318
  return this.deps.store.write(rest);
48499
48319
  }
48500
- // mirror peer sessions:删本机 mirror SessionFile。mirror 一律落 default(mirrorUpsert
48501
- // 强制走 deps.store + strip ownerPersonaId),删也只删 default。idempotent:找不到
48502
- // 不 throw(store.delete 内部 ENOENT 兜底返 false 不 throw)。**不广播 session:deleted**。
48320
+ // mirror 仅删 default store;不存在即 no-op,不广播删除帧。
48503
48321
  mirrorRemove(sessionId) {
48504
48322
  this.deps.store.delete(sessionId);
48505
48323
  }
@@ -48600,16 +48418,8 @@ var SessionManager = class {
48600
48418
  this.writeOwned({ ...existing, cwd, updatedAt: nowIso2(this.deps) });
48601
48419
  }
48602
48420
  /**
48603
- * 外部探测来的会话元信息回填入口(当前唯一消费方:codex 会话打开时从 rollout 读到的
48604
- * 实际 model / effort,见 tools/codex-turn-context.ts)。走 reducer 的 meta_update 通道 →
48605
- * persist-file + session:info,与 CC 侧 `parseLine` 抽出来的 meta_update 完全同路。
48606
- *
48607
- * 与 `feedObserverEvents` 的区别:这不是 observer 回灌——不带 uuid、不参与 turn 判定,
48608
- * 也不受那条 SDK 模式的 procAlive 守卫(守卫是为"daemon 自己在消费 stdout,observer 读到的
48609
- * 是同一帧副本"设的;主动探测来的 meta 没有重复源,被吞掉就永远补不上)。
48610
- *
48611
- * 无 runner 时先 ensureSession(只建 state 容器不 spawn,同 session:observe 的前置),
48612
- * 否则 meta_update 无处可落。幂等由 reducer 的 shallowEqMeta 保证;session 不存在静默返回。
48421
+ * 主动探测元信息走 meta_update,不带 uuid、不参与 turn 判定,也不受 observer 双源去重 gate。
48422
+ * 无 runner 时只建 state 容器;reducer 保证浅比较幂等,session 不存在则忽略。
48613
48423
  */
48614
48424
  applyResolvedMeta(sessionId, patch) {
48615
48425
  let runner = this.runners.get(sessionId);
@@ -48716,16 +48526,8 @@ var SessionManager = class {
48716
48526
  return { response: { ok: true }, broadcast };
48717
48527
  }
48718
48528
  /**
48719
- * 首条 send 触发的智能标题自产(spec:PR #1196 追加拍板 2026-08-17)。
48720
- * 背景见 tools/title-generator.ts 头注释——CC/codex 两条上游标题数据源都等不来
48721
- * 「首轮即有」,daemon 用本会话选定的引擎和模型自己生成,与主 turn 并行,完成后经
48722
- * manager.update 内部通道写 aiLabel(marker patch:不刷 updatedAt、广播 session:info)。
48723
- *
48724
- * 跳过条件:
48725
- * - aiLabelGeneratedAt 已盖章(成功过;失败不盖章 → 下次 send 自然重试)
48726
- * - label 非空(用户手输/dispatch 显式命名,显示优先级下 aiLabel 永远不可见,纯浪费)
48727
- * - larkChatId 非空(lark 群会话显示群名是产品语义,AI 标题反而会遮住群名)
48728
- * - 进程内已有同 session 的 in-flight 调用
48529
+ * 首条 send 并行生成标题,用 meta_update 写 aiLabel,不刷新活动时间。
48530
+ * 成功标记、用户 label、群聊名称或已有 in-flight 调用均跳过;失败不盖章,后续可重试。
48729
48531
  */
48730
48532
  maybeGenerateTitle(file, text) {
48731
48533
  const generate = this.deps.generateTitle;
@@ -48780,19 +48582,8 @@ var SessionManager = class {
48780
48582
  });
48781
48583
  }
48782
48584
  /**
48783
- * 把一段 plain text 以 owner sender 注入指定 session 当新 turn——dispatch 结果投递
48784
- * (deliverPending)与续派任务包(handlers/persona-dispatch 经 sendFollowUp 闭包)共用。
48785
- * session 文件不存在或 send 抛错 → false(记 log,不 throw)。直接 runner.input
48786
- * (无 collector),frames 经 routeFromRunner 正常广播上 wire。
48787
- *
48788
- * 来源分型(L2):`meta` 显式声明"这条注入是什么"('dispatch-task' / 'dispatch-result'),
48789
- * reducer 原样 stamp 到 synth 的 user_text,与 history 侧 classifyUserMessage 从信封
48790
- * 解出的值必须一致。
48791
- *
48792
- * sender 仍是 owner —— **sender 与 origin 是两个正交维度**:sender 回答"以谁的身份跑
48793
- * 这一轮"(MCP endpoint / ticket api 主键要它,daemon 是代 owner 触发的,伪造 'persona'
48794
- * 会污染下游主键),origin 回答"这条消息从哪来",后者由 meta + 信封表达。渲染层按
48795
- * origin 分 owner / guest / persona 三档,persona 档正是 meta 命中 dispatch-* 这一支。
48585
+ * 以 owner 身份注入新 turn;无 session 或发送失败返回 false,帧走异步广播。
48586
+ * meta 来源分型须与历史信封分类一致。sender 表达执行身份,origin 表达消息来源,不能混用。
48796
48587
  */
48797
48588
  injectIntoSession(sessionId, text, meta) {
48798
48589
  if (!this.deps.ownerPrincipalId) {
@@ -48827,18 +48618,13 @@ var SessionManager = class {
48827
48618
  }
48828
48619
  }
48829
48620
  /**
48830
- * 只读拿 SessionState;session 不存在返 null(不 throw)。
48831
- * handlers/session.ts:currentTurnSender 用(loopback HTTP RPC 绝对不能因不存在的 sid 崩)。
48832
- * 2026-07-08 spec §4.6。
48621
+ * session 不存在返回 null;currentTurnSender 的 loopback RPC 依赖此安全查询。
48833
48622
  */
48834
48623
  tryGetState(sessionId) {
48835
48624
  const runner = this.runners.get(sessionId);
48836
48625
  return runner?.getState() ?? null;
48837
48626
  }
48838
- // 等子进程真退出再 ack。reducer 推 SIGKILL effect(见 reducer.ts),内核立即回收子进程,
48839
- // proc.on('exit') 几乎瞬时触发;waitUntilStopped 的 3s 超时只在子进程已是 zombie 或
48840
- // 极端 OS 异常时才会用上。这样修掉了"前端 sessionStop 还没等 CC 退就去删 worktree
48841
- // 撞文件锁"的 race,同时也覆盖 CC streaming hang / 幽灵进程这类 SIGTERM 杀不动的 case
48627
+ // 等真实 proc-exit 再 ack,避免调用方开始删除 worktree 时进程仍持文件锁;超时向上报错。
48842
48628
  async stop(args) {
48843
48629
  const file = this.findOwnedSession(args.sessionId);
48844
48630
  const runner = this.runners.get(args.sessionId);
@@ -48859,13 +48645,8 @@ var SessionManager = class {
48859
48645
  await runner.waitUntilStopped();
48860
48646
  return { response: { ok: true }, broadcast };
48861
48647
  }
48862
- // 中断当前轮次
48863
- // - SDK 模式:发 CC IPC `control_request.subtype=interrupt`(走 stdin stream-json 通道),不杀进程
48864
- // - TUI 模式(pty 已注册):直接走 stop 的 SIGKILL 路径。
48865
- // 原本写 ESC(\x1b)给 pty 让 CC TUI 自己处理,但 CC TUI 的 ESC 语义随状态切换
48866
- // (中断 turn / 清 prompt / 弹菜单),单次 ESC 经常只完成其中一步,用户得反复按。
48867
- // 统一改成 kill 进程:行为可预测,下次 send 自动 respawn。
48868
- // 失败情况(session 不存在 / 没活进程 / 超时 / 进程半路退出)都 throw ClawdError
48648
+ // SDK 发送 interrupt 控制帧;TUI 走 stop / SIGKILL(ESC 的行为取决于界面状态)。
48649
+ // session 不存在、无活进程、超时或中途退出时抛 ClawdError。
48869
48650
  async interrupt(args) {
48870
48651
  this.getFile(args.sessionId);
48871
48652
  const runner = this.runners.get(args.sessionId);
@@ -49143,22 +48924,14 @@ var SessionManager = class {
49143
48924
  return this.runners.get(sessionId);
49144
48925
  }
49145
48926
  /**
49146
- * 拿当前 sub-session 的 wire 形态 status(已 compress:内部 reducer 'spawning' / 'running-idle' /
49147
- * 'stopping' 等都压平)。无 runner 返回 'idle'(没 spawn 过)。
49148
- *
49149
- * 用途:listener 端 subscribe 时补推一次当前 status —— sessionManager.subscribe 注册的
49150
- * callback 只接未来 emit,不重放当前 status;不补推则 listener 进入时如果 sub-session
49151
- * 已 running,永远拿不到 procAlive 信号。owner 路径在 LocalWsServer.onSubscribe hook
49152
- * 重放 pendingQuestions(同一思路)。
48927
+ * 订阅只接未来帧,接入时需补推当前 wire status;running-idle 保留,无 runner 返回 idle。
49153
48928
  */
49154
48929
  getCurrentStatus(sessionId) {
49155
48930
  const runner = this.runners.get(sessionId);
49156
48931
  if (!runner) return "idle";
49157
48932
  return compressStatus(runner.getState().status);
49158
48933
  }
49159
- // 给 observer 用:保证有 runner 但不 spawn(仅创建 state 容器)。
49160
- // observer 喂事件路径只走 default + owner(普通 + owner persona session)
49161
- // ——listener sub-session 的 observer 路径走 ensureRunnerForScope。
48934
+ // 仅创建 state 容器、不 spawn;由持久化 SessionFile 派生 scope。
49162
48935
  ensureSession(file) {
49163
48936
  let r2 = this.runners.get(file.sessionId);
49164
48937
  if (r2) return r2;
@@ -49166,15 +48939,8 @@ var SessionManager = class {
49166
48939
  this.runners.set(file.sessionId, r2);
49167
48940
  return r2;
49168
48941
  }
49169
- // ---------------- scope-explicit sub-session API ----------------
49170
- //
49171
- // 区别于 owner / default 单 sessionId 入口,这些 API 接 SessionScope 参数 ——
49172
- // 调用方(PersonaManager / observer 等)明确传 scope,由 storeFor 路由到正确目录:
49173
- // - {kind:'persona', personaId, mode:'guest', capId} → guests/<capId>/,meta={idleKill:10min, sandbox}
49174
- // - {kind:'persona', personaId, mode:'owner'} → owner/,meta={idleKill:<config 默认 6h>}
49175
- //
49176
- // SubSessionMeta 由 scope → metaFromScope 派生,不进 SessionFile schema。
49177
- /** 按 scope 读取 SessionFile;不存在返回 null */
48942
+ // 显式 scope API
48943
+ // 按给定身份路由 store,运行时 meta 由 scope 派生,不写入 SessionFile。
49178
48944
  readForScope(sessionId, scope) {
49179
48945
  return this.storeFor(scope).read(sessionId);
49180
48946
  }
@@ -49208,15 +48974,8 @@ var SessionManager = class {
49208
48974
  return store.write(file);
49209
48975
  }
49210
48976
  /**
49211
- * persona-bound transport 用:读取 sub-session 的历史 ParsedEvent[]。
49212
- * 实现策略:保证 runner 存在(buffer 是 reducer 唯一权威源),返回 buffer 里所有事件。
49213
- * - 第一次访问:lazy ensureSession,buffer 为空 → 返回 []。
49214
- * - 之前 send 过 / observer 喂过:buffer 已经有累积事件,直接返回。
49215
- *
49216
- * 不读 jsonl:
49217
- * 1. observer 路径在 spawn 后会自动接管 jsonl 回灌,进 reducer buffer。
49218
- * 2. 第一次握手时 sub-session 还没 spawn → toolSessionId 为空 → jsonl 不存在。
49219
- * sessionFile 不存在抛 SESSION_NOT_FOUND;上层应先调 createForScope。
48977
+ * 从 reducer buffer 读取历史,与实时订阅保持同源;不读 jsonl。
48978
+ * 初次访问仅建 runner,buffer 可为空;session 不存在抛 SESSION_NOT_FOUND。
49220
48979
  */
49221
48980
  readHistoryEventsForScope(sessionId, scope) {
49222
48981
  const file = this.storeFor(scope).read(sessionId);
@@ -49230,20 +48989,8 @@ var SessionManager = class {
49230
48989
  return runner.getState().buffer.map((e) => e.event);
49231
48990
  }
49232
48991
  /**
49233
- * persona-bound transport 用:基于 readHistoryEventsForScope 的 buffer 切片视图,支持分页。
49234
- *
49235
- * Why a new method instead of reusing `history:read` RPC handler:
49236
- * - `history:read` 走 HandlerDeps.history.read(),读 CC 写盘的 jsonl 文件
49237
- * - 本方法走 reducer buffer(与实时 `subscribe` 同一数据源)
49238
- * 两条路径数据形态、时序、字段不同,不能共享 helper。persona-bound 客户端必须
49239
- * 走 buffer 路径,才能保证首屏回放和实时帧无 gap、无重复。
49240
- *
49241
- * 切片语义:clip 安全边界,response.offset 保留请求值便于客户端识别越界请求。
49242
- * - offset >= total → events: [], offset: 请求值, total: 当前 buffer 长度
49243
- * - offset + limit > total → 返回 tail [offset, total),长度 = total - offset
49244
- * - 正常 → 返回 [offset, offset + limit)
49245
- *
49246
- * SESSION_NOT_FOUND 行为继承 readHistoryEventsForScope(store.read 返回 null 即抛)。
48992
+ * 分页读取 reducer buffer;不能复用读取 jsonl 的 history:read,否则首屏与实时流可能错位。
48993
+ * 超出尾部返回空或剩余 tail,offset 保留请求值;不存在时抛 SESSION_NOT_FOUND。
49247
48994
  */
49248
48995
  readHistoryPageForScope(sessionId, scope, limit, offset) {
49249
48996
  const all = this.readHistoryEventsForScope(sessionId, scope);
@@ -49253,11 +49000,7 @@ var SessionManager = class {
49253
49000
  return { events: all.slice(start, end), offset, total };
49254
49001
  }
49255
49002
  /**
49256
- * persona-bound transport 用:订阅 sub-session 的 owner-side EventFrame(白名单透传)。
49257
- * 与 owner 路径共享同一组字面量,listener callback 拿到的 frame.type 当前可能是
49258
- * 'session:event'(ParsedEvent 流)或 'session:status'(procAlive)。
49259
- * 返回 unsubscribe;不破坏现有 wire 广播路径——routeFromRunner 同时 fan-out 到
49260
- * eventSubscribers 和 deps.broadcastFrame
49003
+ * 同步订阅 routeFromRunner 白名单帧并返回 unsubscribe;与 wire 广播并行,不重放当前状态。
49261
49004
  */
49262
49005
  subscribe(sessionId, _scope, listener) {
49263
49006
  let subs = this.eventSubscribers.get(sessionId);
@@ -49375,15 +49118,8 @@ var SessionManager = class {
49375
49118
  return runner;
49376
49119
  }
49377
49120
  /**
49378
- * Persona dispatch: 启动 B session 并投递任务包当第一条 user message。
49379
- *
49380
- * - 复用 persona-owner 创建路径(cwd=personaDir、scope=persona-owner、不沙箱)
49381
- * - SessionFile 上写 dispatchedFromSessionId 关联回 A(UI 折叠用;mirror 一侧 strip)
49382
- * - 跟 PersonaDispatchManager 双向登记 dispatchId ↔ B sessionId,让后续 reducer
49383
- * buildSpawnContext 能反查到 dispatchId 注 CLAWD_DISPATCH_ID env
49384
- * - 投 inject-owner-text 任务包;包尾教 B 用 personaDispatchComplete tool 回传
49385
- *
49386
- * cwd 派生与 SessionManager.create 的 owner persona 路径一致(derivePersonaSpawnCwd)。
49121
+ * 创建 worker session 并通过 send 启动任务包;本地 owner、跨设备 guest。
49122
+ * 持久化来源关联,登记 dispatch ↔ session 供 env 注入;不能用只写 buffer 的 inject-owner-text。
49387
49123
  */
49388
49124
  createDispatchedSession(args) {
49389
49125
  if (!this.deps.personaRoot) {
@@ -49411,8 +49147,6 @@ var SessionManager = class {
49411
49147
  const file = {
49412
49148
  sessionId,
49413
49149
  cwd,
49414
- // dispatch B session 跟随 persona 绑定的 tool(codex persona 用 codex spawn);
49415
- // UI 手建路径另允许通过 session:create 显式覆盖,dispatch 走 args.tool 一次性 override。
49416
49150
  tool: effectiveTool,
49417
49151
  ownerPersonaId: args.targetPersona,
49418
49152
  dispatchedFromSessionId: args.sourceSessionId,
@@ -49422,16 +49156,11 @@ var SessionManager = class {
49422
49156
  ...args.guestPrincipalId ? {
49423
49157
  creatorPrincipalId: args.guestPrincipalId,
49424
49158
  ...args.guestDisplayName ? { creatorDisplayName: args.guestDisplayName } : {},
49425
- // T-31: A 的飞书 union_id stamp 到 B 的 SessionFile;guest meta 由 newRunner 派生
49426
49159
  ...args.guestFeishuUnionId ? { creatorFeishuUnionId: args.guestFeishuUnionId } : {}
49427
49160
  } : {},
49428
- // T-20 的 dispatch model override 优先;缺省回落 persona 默认值(spec 2026-08-12)。
49429
- // effort 没有 override 参数(续派也不允许改),直接取 persona 默认值。
49430
- // engine 被 override 掉时 persona 的 model / effort 一概不继承(见 engineOverridden)。
49431
49161
  ...personaDefaultFields(engineOverridden ? void 0 : personaDefaults, {
49432
49162
  model: args.model
49433
49163
  }),
49434
- // butler 顶部入口:dispatch 传下来的 label 落盘,sidebar 显示(否则兜底 cwd-basename)
49435
49164
  ...args.label ? { label: args.label } : {},
49436
49165
  createdAt: now,
49437
49166
  updatedAt: now
@@ -49476,8 +49205,6 @@ var SessionManager = class {
49476
49205
  }
49477
49206
  runner.input({
49478
49207
  kind: "command",
49479
- // 来源分型(L2):realtime 路径显式带 meta,与 history 侧 classifyUserMessage
49480
- // 从信封解出的值一致 —— 两路不一致会打歪 trimHistoryOverlap 的 anchor。
49481
49208
  command: {
49482
49209
  kind: "send",
49483
49210
  text: taskPack,
@@ -49492,17 +49219,8 @@ var SessionManager = class {
49492
49219
  return { sessionId };
49493
49220
  }
49494
49221
  /**
49495
- * shift v1 (spec 2026-06-24-clawd-shift):到点 fire 时由 ShiftScheduler 调,
49496
- * 起一个新 cc session 跑 prompt(fire-and-forget,不绑 dispatchId / 不挂 caller)。
49497
- * 返回 sessionId + ended Promise(cc 结束时 resolve)+ kill closure(timeout 兜底)。
49498
- *
49499
- * 跟 createDispatchedSession 的差异:
49500
- * - 不带 dispatchId / source / task pack;prompt 直接当第一条 user message
49501
- * - 不调 personaDispatchManager.registerBSession
49502
- * - 多返一个 ended Promise,监听 session:status 帧翻译成 shift 业务终态 union
49503
- * (ShiftSessionEnded:{ok:true} | {error:string}),不经 exit code 中转
49504
- * - turn-idle 到达即视为业务完成 → settle {ok:true} + fire-and-forget kill cc
49505
- * - kill 通过 runner.input({command:{kind:'stop'}}) 触发 SIGKILL → proc-exit
49222
+ * ShiftScheduler 启动一次性会话,以 prompt 首发,不登记 dispatch。
49223
+ * turn-idle 即完成并异步停止进程;返回业务 ended Promise 与供超时使用的 kill 闭包,不以 exit code 判成败。
49506
49224
  */
49507
49225
  createShiftSession(args) {
49508
49226
  if (!this.deps.personaRoot) {
@@ -49520,10 +49238,8 @@ var SessionManager = class {
49520
49238
  const file = {
49521
49239
  sessionId,
49522
49240
  cwd,
49523
- // shift 定时 spawn 同样跟随 persona 绑定 tool(对齐 dispatch B session)
49524
49241
  tool: personaDefaults?.tool ?? "claude",
49525
49242
  ownerPersonaId: args.targetPersona,
49526
- // persona 绑定的 model / effort 快照进 SessionFile(spec 2026-08-12)
49527
49243
  ...personaDefaultFields(personaDefaults),
49528
49244
  ...args.sourceSessionId ? { shiftFiredFromSessionId: args.sourceSessionId } : {},
49529
49245
  ...args.deployId ? { deployId: args.deployId } : {},
@@ -49590,19 +49306,8 @@ var SessionManager = class {
49590
49306
  return { sessionId, ended, kill: killImpl };
49591
49307
  }
49592
49308
  /**
49593
- * LarkBotConnector(spec 2026-08-05 §5.3):把一条飞书群 @ 消息作为一个 turn 驱动
49594
- * 该群的专属 guest session,turn-idle 后从 reducer buffer 提取回复文本。
49595
- *
49596
- * 跟 createShiftSession 的差异:
49597
- * - **find-or-create**:按 capId `lark-<chatId>` 找现存 session,没有才建;daemon
49598
- * `create` 的无 dedupe 语义不变——dedupe 是本方法私有实现。
49599
- * 粒度由云端给的 chatId 决定,daemon 只当它是不透明 key:群消息是虚拟三段式
49600
- * `<真实 chat_id>:<thread_id>:<open_id>`(spec 2026-09-10,一话题一人一条),
49601
- * p2p 是裸 chat_id,文档评论是 `doc:<fileToken>`
49602
- * - guest scope + 沙箱:群成员 query 不带 owner 权限;10min guest idle-kill + resume
49603
- * - turn-idle 后**不 kill**(session 常驻,记忆即价值);TUI 下延迟 settleDelayMs 等
49604
- * 尾随 text 落盘(schemas.ts turn_end 注释 ②)再读 buffer,SDK / codex 同步读
49605
- * - 回复提取:extractLarkReplyText(最后一条 user_text 之后的 text 事件拼接)
49309
+ * 按 lark-<chatId> 复用 guest 沙箱会话;chatId 作为不透明 key,可含话题、用户或文档维度。
49310
+ * turn-idle 后提取最后一条 user_text 的回复,不主动 kill;TUI 的尾随 text 窗口见 settleDelayMs。
49606
49311
  */
49607
49312
  runLarkChatTurn(args) {
49608
49313
  if (!this.deps.personaRoot) {
@@ -49625,8 +49330,6 @@ var SessionManager = class {
49625
49330
  cwd: this.personaCwdFor(args.personaId, personaDefaults),
49626
49331
  tool: personaDefaults?.tool ?? "claude",
49627
49332
  ownerPersonaId: args.personaId,
49628
- // persona 绑定的 model / effort 快照进 SessionFile(spec 2026-08-12)——
49629
- // 飞书里跟 persona 聊天用的模型,与 UI 里给它配的一致
49630
49333
  ...personaDefaultFields(personaDefaults),
49631
49334
  // guest 三件套:creatorPrincipalId=capId(scopeForFile 回落防线)、
49632
49335
  // creatorDisplayName(reducer guest 分支硬性要求,缺则 spawn 抛)
@@ -49730,12 +49433,8 @@ var SessionManager = class {
49730
49433
  if (!runner) return;
49731
49434
  runner.feedStdoutLine(line);
49732
49435
  }
49733
- // observer 喂入一行 JSONL 解析后的 ParsedEvent 数组(一帧的所有事件共享 top-level uuid)。
49734
- //
49735
- // 双层防护对齐 openclaw-plugin-clawos:
49736
- // 1. 主防御:runner.procAlive 时直接丢弃——daemon 自己在 spawn CC 并消费 stdout,
49737
- // observer 读 JSONL 100% 是重复内容(来自同一逻辑帧),从源头不让它进 reducer。
49738
- // 2. 兜底:reducer 的 batch uuid dedup 处理 managed 刚 stop / 切换的窗口期事件。
49436
+ // SDK 活进程已消费 stdout,observer 副本由 procAlive gate 丢弃;TUI 则由 observer 唯一供业务事件。
49437
+ // 切换窗口的重复帧由 reducer batch uuid 去重。
49739
49438
  feedObserverEvents(sessionId, events) {
49740
49439
  if (events.length === 0) return;
49741
49440
  const runner = this.runners.get(sessionId);
@@ -49818,17 +49517,8 @@ var SessionManager = class {
49818
49517
  runner.feedObserverEvents(feedEvents);
49819
49518
  }
49820
49519
  /**
49821
- * `ClaudeTuiAdapter.observeScreenIdle` fire triggered → armed=true 时调用(一次性触发点)。
49822
- *
49823
- * 语义:屏幕真的稳定了 5s。查有没有 pending 的 turn_duration 信号:
49824
- * - 有 → 之前收到的 turn_duration 被屏幕稳定**确认**了,flush 成 turn_end 进 reducer
49825
- * - 无 → 屏幕稳定但从没收到 turn_duration → noop(不生成 turn_end;补偿路径的错误做法)
49826
- *
49827
- * pending 集合是**必要前提**:turn_end 只能来自"observer 收 turn_duration + 屏幕后续稳定"
49828
- * 双源确认,任何一个缺少都不能 emit。这跟 PR #962 拆掉的 dispatchTurnIdle "屏幕静止就补
49829
- * turn_end" 语义不同。
49830
- *
49831
- * 仅 TUI 模式;SDK / codex 没有屏幕信号也就不会触发本方法。
49520
+ * TUI 屏幕稳定时,仅 flush 已 pending 的 turn_duration;没有信号即 no-op。
49521
+ * 屏幕稳定自身不是 turn_end 证据,不能补造结束事件。
49832
49522
  */
49833
49523
  notifyScreenIdle(toolSessionId) {
49834
49524
  if (this.deps.mode !== "tui") return;
@@ -49913,8 +49603,7 @@ var SessionManager = class {
49913
49603
  }
49914
49604
  return { response: { ok: true }, broadcast };
49915
49605
  }
49916
- // 权限决定:dispatcher 收到 permission:respond 时调用
49917
- // 先查 state.pendingPermissions:不存在 → 抛 PERMISSION_REQUEST_STALE
49606
+ // 无 runner 时权限请求已失效;已有 runner 且请求已被另一端回答则幂等 no-op。
49918
49607
  respondPermission(args) {
49919
49608
  const runner = this.runners.get(args.sessionId);
49920
49609
  if (!runner) {
@@ -50007,7 +49696,6 @@ var SessionManager = class {
50007
49696
  const size = sid ? this.lastUiSizeBySessionId.get(sid) : void 0;
50008
49697
  if (size) surface.resize(size.cols, size.rows);
50009
49698
  }
50010
- /** spawn 进程 exit 时清理 */
50011
49699
  unregisterSurface(toolSessionId) {
50012
49700
  this.surfacesByToolSid.delete(toolSessionId);
50013
49701
  }
@@ -50035,12 +49723,7 @@ var SessionManager = class {
50035
49723
  return true;
50036
49724
  }
50037
49725
  /**
50038
- * ClaudeTuiAdapter onPtyData callback:把 pty 字节流以 session:pty wire 帧广播给订阅者。
50039
- * seq 来自 adapter spawn 路径维护的 per-session counter,与 detector.parseSeq 同源。
50040
- * UI 端 snapshot/incremental dedup 必须靠这个 seq。
50041
- * 仅 mode='tui' 时发——SDK spawn 模式根本没 pty。
50042
- * payload 永远只有 `{ sessionId, data, seq }`,data = base64 UTF-8 字节流。
50043
- * 详见 protocol/src/events.ts session:pty JSDoc
49726
+ * TUI 增量 pty 字节流,seq 与 detector 共源,供 UI 与快照去重;载荷规则见 protocol/events.ts。
50044
49727
  */
50045
49728
  emitPtyChunk(toolSessionId, b64, seq) {
50046
49729
  if (this.deps.mode !== "tui") return;
@@ -50052,11 +49735,7 @@ var SessionManager = class {
50052
49735
  );
50053
49736
  }
50054
49737
  /**
50055
- * ClaudeTuiAdapter onPopupTransition callback:把 footer 检测结果转成 session:control wire 帧。
50056
- * visible=true → kind='popup_open' + popupKind
50057
- * visible=false → kind='popup_close' + popupKind
50058
- * 仅 mode='tui' 时发。新增 popup 类型只需扩 protocol POPUP_KIND_VALUES,不加帧名。
50059
- * 详见 protocol/src/events.ts session:control JSDoc
49738
+ * TUI popup transition 复用 session:control;新 popup 类型扩 POPUP_KIND_VALUES,规则见 protocol/events.ts。
50060
49739
  */
50061
49740
  emitPopupTransition(toolSessionId, popupKind, visible) {
50062
49741
  if (this.deps.mode !== "tui") return;
@@ -50073,9 +49752,7 @@ var SessionManager = class {
50073
49752
  );
50074
49753
  }
50075
49754
  /**
50076
- * ClaudeTuiAdapter onReady callback(ReadyGate v2):ReadyDetector 扫到 cc TUI 主输入框 +
50077
- * quiet window 通过时投递 reducer 'ready-detected' input,触发 pendingSend 队列出队 flush。
50078
- * 仅 mode='tui' 时启用;SDK 模式 reducer 内 early return 也无害(双层兜底)。
49755
+ * TUI ready 检测转发给 reducer,触发 pendingSend 出队;SDK 跳过。
50079
49756
  */
50080
49757
  dispatchReadyDetected(toolSessionId) {
50081
49758
  if (this.deps.mode !== "tui") {
@@ -50433,26 +50110,13 @@ var PersonaStore = class {
50433
50110
  return path16.join(this.personaDir(personaId2), "AGENTS.md");
50434
50111
  }
50435
50112
  /**
50436
- * persona 级 sandbox base 落盘路径 —— 故意放 `.clawd/` 而非 `.claude/`,让 CC 的 project
50437
- * source 自动发现扫不到(owner `--setting-sources user,project,local` 不会顺手带进来)。
50438
- * 这份是 guest 沙箱的 **base 真源**(manager compose 读它当 base);guest 实际加载的是 manager
50439
- * spawn 前 per-guest 动态拼到各自 session 目录的那份(base + 强制底座 + 本 guest userWorkDir carve)。
50113
+ * guest sandbox base 放 .clawd/,避免 CC project settings 自动加载。
50114
+ * manager 读取后叠加强制底座和 userWorkDir,以 inline settings 注入会话。
50440
50115
  */
50441
50116
  sandboxSettingsPath(personaId2) {
50442
50117
  return path16.join(this.personaDir(personaId2), ".clawd", "sandbox-settings.json");
50443
50118
  }
50444
- /**
50445
- * 人格补充层落盘路径 —— `<personaDir>/.clawd/personality.local.md`。
50446
- *
50447
- * 位置不是随手挑的,两条既有约定叠出来的:`.clawd/` 是 daemon 声明「不动」的 persona 私有区
50448
- * (不在 seed.ts 的 DAEMON_MANAGED_PATHS 里),`.local` 是仓内已成立的「本机个人 override
50449
- * 永不被 OTA 覆盖」后缀。于是内置 persona 的 CLAUDE.md 继续被
50450
- * refresh 覆盖成 bundle 新版、用户写的这层原样活着。
50451
- *
50452
- * 同 sandbox-settings.json 的理由:放 `.clawd/` 而非 persona 根目录,让 CC 的 project
50453
- * setting source 自动发现扫不到——这层只经 SpawnContext.extraSystemPrompt 注入
50454
- * (见 local-personality-prompt.ts),通道唯一,不会被当第二份 CLAUDE.md 重复加载。
50455
- */
50119
+ /** 用户人格补充层:OTA 不覆盖,只经 extraSystemPrompt 注入,避免重复加载人格。 */
50456
50120
  personalityLocalPath(personaId2) {
50457
50121
  return path16.join(this.personaDir(personaId2), ".clawd", "personality.local.md");
50458
50122
  }
@@ -50474,10 +50138,7 @@ var PersonaStore = class {
50474
50138
  if (!fs13.existsSync(p3)) return null;
50475
50139
  return fs13.readFileSync(p3, "utf8");
50476
50140
  }
50477
- /**
50478
- * 写人格补充层。**纯空白 = 清空**:删文件而不是留个空文件——「有没有补充」的唯一判据
50479
- * 是文件在不在,留空文件会让下游多一种要判空的状态。persona 目录还没建时自建 `.clawd/`。
50480
- */
50141
+ /** 纯空白表示清空,删除文件;缺目录时创建 .clawd/。 */
50481
50142
  writePersonalityLocal(personaId2, body) {
50482
50143
  const p3 = this.personalityLocalPath(personaId2);
50483
50144
  if (!body.trim()) {
@@ -50487,12 +50148,7 @@ var PersonaStore = class {
50487
50148
  fs13.mkdirSync(path16.dirname(p3), { recursive: true });
50488
50149
  this.atomicWrite(p3, body);
50489
50150
  }
50490
- /**
50491
- * 历史 persona 补 AGENTS.md:人格双写镜像是 codex 适配后才加的,更早落盘的 persona 只有
50492
- * CLAUDE.md。codex 原生读 cwd 的 AGENTS.md,缺失则切到 codex 后人格丢失。daemon 启动跑一次
50493
- * migrate(见 seed.migrateAgentsMirror)。幂等:已有 AGENTS.md(含用户手改的)不覆盖;
50494
- * 无 CLAUDE.md 不凭空造。返回是否实际补写。
50495
- */
50151
+ /** 补缺失的 AGENTS.md 镜像;已有文件(含用户编辑)不覆盖,无 CLAUDE.md 则不生成。 */
50496
50152
  ensureAgentsMirror(personaId2) {
50497
50153
  const claudeMd = this.claudeMdPath(personaId2);
50498
50154
  const agentsMd = this.agentsMdPath(personaId2);
@@ -50502,11 +50158,8 @@ var PersonaStore = class {
50502
50158
  return true;
50503
50159
  }
50504
50160
  /**
50505
- * 覆盖式写 persona 级 sandbox 配置(seed 给有 profile 的 persona 写 base⊕profile;owner 之后可手编)。
50506
- * 这份是 guest 沙箱的 **base 真源**:manager spawn 时 composeGuestSandbox 读它 + 叠 sandbox 强制底座
50507
- * (enabled/denyRead/denyWrite/userWorkDir owner 削弱不了)→ 写 per-guest 文件给 CC。
50508
- * 写入时强制注入本 persona 的**基线**(Edit-deny + allowRead,见 injectPersonaBaseline),
50509
- * 让落盘 base 就是 source of truth(compose 不再运行时替换 permission deny / 注入 persona 读权)。
50161
+ * 覆盖 guest sandbox base,并注入 persona 的 Edit-deny、OS 与工具层读权。
50162
+ * manager 再叠不可削弱的强制底座和 userWorkDir,以 inline settings 注入 CC。
50510
50163
  */
50511
50164
  writeSandboxSettings(personaId2, settings) {
50512
50165
  this.atomicWrite(
@@ -50515,12 +50168,8 @@ var PersonaStore = class {
50515
50168
  );
50516
50169
  }
50517
50170
  /**
50518
- * persona 基线注入:Edit-deny(绝对 '~/<persona>/**')并进 permissions.deny + 剥掉历史相对
50519
- * 'Edit(./**)';persona 目录 '~/' 路径 union 进 sandbox.filesystem.allowRead(guest 读权
50520
- * 落盘即真源,compose 不再运行时注入——spec 2026-08-06-persona-workdir-discipline §1-§2);
50521
- * `Read(~/<persona>/**)` union 进 permissions.allow——OS 沙箱层可读只放行了 Bash,CC 自带
50522
- * Read 工具在最严格 base(dontAsk + 无 allow)下仍被拒,工具层与 OS 层必须同构放行
50523
- * (guest「自由读 persona 目录、只写自己 userWorkDir」,spec 2026-08-06-lark-dm §P10 通用化)。
50171
+ * 去掉历史相对 Edit-deny,加入 persona 绝对路径的 Edit-deny。
50172
+ * 同时放行 OS allowRead 和工具层 Read(persona/**);只开 OS 读权不能放行 CC Read。
50524
50173
  */
50525
50174
  injectPersonaBaseline(personaId2, settings) {
50526
50175
  const dir = this.personaDir(personaId2);
@@ -50589,10 +50238,7 @@ var PersonaStore = class {
50589
50238
  if (fs13.existsSync(agentsMd)) return fs13.readFileSync(agentsMd, "utf8");
50590
50239
  return null;
50591
50240
  }
50592
- /**
50593
- * 读 sandbox-settings.json 当前内容;文件不存在 / JSON 解析失败均返回 null。
50594
- * owner 手改文件可能写出非法形状,daemon 不强校验,UI 拿到 null → 显示空状态。
50595
- */
50241
+ /** 缺失或读/解析失败返回 null;owner 手编的内容不强校验形状。 */
50596
50242
  readSandboxSettings(personaId2) {
50597
50243
  const p3 = this.sandboxSettingsPath(personaId2);
50598
50244
  if (!fs13.existsSync(p3)) return null;
@@ -50613,17 +50259,7 @@ var PersonaStore = class {
50613
50259
  claudeSettingsPath(personaId2) {
50614
50260
  return path16.join(this.personaDir(personaId2), ".claude", "settings.json");
50615
50261
  }
50616
- /**
50617
- * 读取 persona 的 `.claude/settings.json` 中 `enabledPlugins` map,把 value === true
50618
- * 的 key 转成 `{ id }[]` 给 UI 做只读展示。
50619
- *
50620
- * 兜底范围(来自老板规则:不要无事实依据的兜底):
50621
- * - 文件不存在 → [](plugin 未被启用是合法初始态)
50622
- * - JSON 解析失败 → [](owner 手改文件,schema 漂移交给 UI 兜底)
50623
- * - `enabledPlugins` 字段缺失 / 非 object → []
50624
- * - 单 value 非 boolean → 跳过该 entry
50625
- * 其他错误(fs 权限等)原样抛出,由上层 handler 转 ws error 帧。
50626
- */
50262
+ /** 取 enabledPlugins 中非空且 value === true 的 id;文件缺失、读/解析失败或字段非对象均返回 []。 */
50627
50263
  readEnabledPlugins(personaId2) {
50628
50264
  const p3 = this.claudeSettingsPath(personaId2);
50629
50265
  if (!fs13.existsSync(p3)) return [];
@@ -50672,11 +50308,7 @@ var PersonaRegistry = class {
50672
50308
  cache = /* @__PURE__ */ new Map();
50673
50309
  /** 上一次从 gateway 拉到的云 persona;daemon 刚起还没拉到 = 空 */
50674
50310
  cloud = [];
50675
- /**
50676
- * 从 store 全量重建缓存(boot 时 + 测试 setup 时),再合进云快照。
50677
- * 单个 persona 读失败(损坏的 persona.json / schema 不匹配)不应阻塞 daemon 启动 ——
50678
- * try/catch 隔离,让其它 persona 仍能加载。
50679
- */
50311
+ /** 重建时隔离单个人格的读取失败,避免阻塞其他 persona 加载。 */
50680
50312
  reload() {
50681
50313
  this.cache.clear();
50682
50314
  for (const id of this.store.list()) {
@@ -50690,7 +50322,7 @@ var PersonaRegistry = class {
50690
50322
  if (!this.cache.has(c2.personaId)) this.cache.set(c2.personaId, c2);
50691
50323
  }
50692
50324
  }
50693
- /** 换一份云快照并重建缓存。传空数组 = 云上一个都没有(或 gateway 拉不到时保留上次:由调用方决定传什么)。 */
50325
+ /** 替换云快照并重建;返回被本机同名条目盖掉的云 personaId。 */
50694
50326
  setCloudSnapshot(list) {
50695
50327
  this.cloud = list.filter((p3) => p3.cloud);
50696
50328
  this.reload();
@@ -50746,13 +50378,7 @@ var DEFAULT_PERSONAS = [
50746
50378
  sandboxProfile: DEFAULT_BYPASS_PROFILE
50747
50379
  },
50748
50380
  {
50749
- // clawd 管家 (chief-of-staff):spec 2026-07-22-clawd-butler-persona
50750
- // 新 personaId 独立于老 persona-clawd-helper(老 helper seed entry 已删;老用户机器上
50751
- // 若有残留目录,是他们自留物,daemon 不再触碰)。bundle 里 CLAUDE.md 骨架 + 6 个
50752
- // prebuild skill:新装用户走 copyBundleExtras,存量用户走 refreshDaemonManagedDirs
50753
- // (`.claude/skills` 在 DAEMON_MANAGED_PATHS 里),都装到 `.claude/skills/`。
50754
- // 前 5 个 skill fetch 用 daemon 注入的 env CLAWOS_API 拼 `${CLAWOS_API}/api/docs/**`
50755
- // 拉线上手册;clawd-session-import 自带 scripts/link-sessions.mjs,只依赖 node 内置模块。
50381
+ // 管家沿用独立 id;旧 helper 残留不纳管,产品 skills 经 seed / refresh 安装。
50756
50382
  personaId: "persona-clawd-butler",
50757
50383
  label: "clawd \u7BA1\u5BB6",
50758
50384
  model: "opus",
@@ -50771,22 +50397,16 @@ var DEFAULT_PERSONAS = [
50771
50397
  sandboxProfile: DEFAULT_BYPASS_PROFILE
50772
50398
  },
50773
50399
  {
50774
- // app-builder:全栈应用搭建师,绑 extension-kit 模板 + multi-project picker
50775
- // spec: superpowers/specs/2026-06-01-app-builder-project-picker-design.md
50776
- // superpowers/specs/2026-06-02-app-builder-single-pane-default-design.md
50777
50400
  personaId: "persona-app-builder",
50778
50401
  label: "App Builder",
50779
50402
  model: "opus",
50780
50403
  effort: "high",
50781
50404
  iconKey: "assist",
50782
50405
  public: false,
50783
- // 开发型 profile:在最严格 base(已含 Bash+文件工具)上追加 web 工具 + node/pnpm 工具链精确路径
50784
- // + 出站全放。工具链只列精确目录(~/.nvm / pnpm store / ~/.npm),**不放宽 dot 目录**(~/.config 含
50785
- // gh/gcloud 凭证、~/.local/bin 在 PATH 可被植入),避免 secrets 暴露 + 提权。mac/linux 两套 pnpm store 都列。
50406
+ // 工具链只放行精确目录,不放宽 ~/.config 或 ~/.local/bin,避免暴露凭据和可执行植入。
50786
50407
  sandboxProfile: {
50787
- // base 只 Bash,开发型在此放开文件工具 + web 工具(OS 沙箱 carve 兜底文件边界)。
50788
- // .secrets 只 app-builder 有 → 这里加 deny(其余 persona base 不默认带 .secrets)。
50789
- // defaultMode bypassPermissions:开发流频繁用工具,免逐次审批;deny 规则仍生效、OS 沙箱照管(已验证)。
50408
+ // 增加通用文件/web 工具;OS carve 约束文件范围,.secrets 另拒读。
50409
+ // bypassPermissions 免逐次审批,显式 deny 与 OS 沙箱仍生效。
50790
50410
  permissions: {
50791
50411
  defaultMode: "bypassPermissions",
50792
50412
  allow: ["Read", "Edit", "Write", "Glob", "Grep", "WebFetch", "WebSearch"]
@@ -50798,11 +50418,8 @@ var DEFAULT_PERSONAS = [
50798
50418
  allowRead: ["~/.npmrc", "~/Library/Preferences/pnpm", "~/.nvm", "~/Library/pnpm", "~/.local/share/pnpm", "~/.local/state/pnpm", "~/.npm", "~/.pnpm-store"],
50799
50419
  allowWrite: ["~/Library/pnpm", "~/.local/share/pnpm", "~/.local/state/pnpm", "~/.npm", "~/.pnpm-store"]
50800
50420
  },
50801
- // 网络:CC 沙箱出站强制走它自管的 MITM proxy 按 allowedDomains 过滤,**没有全开档**——'*' 或空白名单
50802
- // 都被全拦(matcher JL8:plain 域名只精确匹配,'*.xxx' 是 host.endsWith('.xxx'),且对裸 IP 返回 false)。
50803
- // 用一组常见顶级域通配近似"全放行",几乎零维护即可覆盖 npm/pnpm/git 全生态(registry.npmjs.org→*.org、
50804
- // npm.jsr.io→*.io、github/githubusercontent→*.com…)。代价:仍挡掉直连 IP 外联(正常包安装都走域名,
50805
- // 反而是一道防裸 IP 外泄的兜底)。需要别的 TLD 往这里加。allowLocalBinding 控本机 bind,与出站正交。
50421
+ // CC allowedDomains 只支持具体域名或 *.后缀,'*' / 空列表不能用于全开。
50422
+ // TLD 通配覆盖常用包源,仍拒绝裸 IP;allowLocalBinding 与出站授权独立。
50806
50423
  network: {
50807
50424
  allowedDomains: ["*.com", "*.org", "*.io", "*.net", "*.dev", "*.app", "*.cn", "*.co", "*.chat", "*.ai"],
50808
50425
  allowLocalBinding: true
@@ -50816,17 +50433,13 @@ var DEFAULT_PERSONAS = [
50816
50433
  }
50817
50434
  },
50818
50435
  {
50819
- // 数据查询搭建师:复用 app-builder 流水线生成 dataclaw 只读查询页
50820
- // spec: clawd/superpowers/specs/2026-06-14-dataclaw-builder-design.md
50821
50436
  personaId: "persona-dataclaw-builder",
50822
50437
  label: "\u6570\u636E\u67E5\u8BE2\u642D\u5EFA\u5E08",
50823
50438
  model: "opus",
50824
50439
  effort: "high",
50825
50440
  iconKey: "assist",
50826
50441
  public: false,
50827
- // 同 app-builder 的开发型 profile(逐字段照抄):web 工具 + node/pnpm 工具链精确路径
50828
- // + 出站 TLD 通配白名单。复用同一条流水线 → 同一套沙箱授权。
50829
- // 注:不带 .secrets deny —— dataclaw-builder 的 apikey 走生成 app 的 server/.env,persona .secrets 已清空。
50442
+ // 同 app-builder 的路径与域名授权;apikey 在生成项目 server/.env,无需 persona .secrets carve。
50830
50443
  sandboxProfile: {
50831
50444
  permissions: {
50832
50445
  defaultMode: "bypassPermissions",
@@ -50850,25 +50463,17 @@ var DEFAULT_PERSONAS = [
50850
50463
  }
50851
50464
  },
50852
50465
  {
50853
- // HTML PPT 制作师:把想法/文字/旧 PPT 转成单文件 HTML 演示稿
50854
- // bundle 含 frontend-slides skill(MIT,Zara Zhang),daemon ship 进 .claude/skills/
50466
+ // frontend-slides(MIT,Zara Zhang)不随 bundle 发布,由人格引导按需安装上游。
50855
50467
  personaId: "persona-html-slides",
50856
50468
  label: "HTML PPT \u5236\u4F5C\u5E08",
50857
50469
  model: "opus",
50858
50470
  effort: "high",
50859
50471
  iconKey: "doc",
50860
50472
  public: false,
50861
- // 与 app-builder 对齐"调用阿里云的工作方式"(owner 2026-06-17 要求):开发型 profile —— 放开文件/web
50862
- // 工具 + node/pnpm 工具链精确路径 + 出站 TLD 通配白名单。html-slides 需要联网(git clone frontend-slides
50863
- // skill、调阿里云 CLI/SDK)。
50864
- // 阿里云凭证:**凭据表单源**($HOME/.clawd/secrets/aliyun.env,使用者自己写;bundle 不 ship AK,见
50865
- // specs/2026-09-18-aliyun-ak-leak-rotate-design.md;deploy-kit 下那份 .secrets/ 已随 design 2026-09-19-persona-mcp-single-source 删),
50866
- // 不在 persona 目录另存一份。app-builder 的 FC 部署由 clawd-app-builder MCP server 起(沙箱外、AK 在它 env 里)故其 profile
50867
- // 无需 carve 这个路径;html-slides 没有 daemon 侧流程、要在 cc 沙箱内直接 `set -a; . $HOME/.clawd/secrets/aliyun.env; set +a`
50868
- // 后调 aliyun CLI(env 凭证绕过沙箱外的 ~/.aliyun/),所以这里**额外**把这个凭证文件加进 allowRead
50869
- // (denyRead '~/' 之上的精确凿洞,单文件、最小暴露,同 ~/.npmrc 先例)。
50870
- // excludedCommands: ['aliyun *'] —— 额外把 aliyun CLI 放沙箱外跑(逃出 seatbelt 直接用宿主凭证/网络),
50871
- // 与上面沙箱内 env 凭证方案并存,按运行时实际命令前缀匹配生效。
50473
+ // 文件/web 工具与包管理器授权同 app-builder。
50474
+ // html-slides 在 CC 内调用阿里云,精确放行 ~/.clawd/secrets/aliyun.env;bundle 不带 AK。
50475
+ // app-builder 的部署则由沙箱外 MCP 持凭据执行,不需要该 carve。
50476
+ // excludedCommands 同时允许 aliyun 命令逃出 OS 沙箱使用宿主凭据/网络。
50872
50477
  sandboxProfile: {
50873
50478
  permissions: {
50874
50479
  defaultMode: "bypassPermissions",
@@ -50893,12 +50498,7 @@ var DEFAULT_PERSONAS = [
50893
50498
  }
50894
50499
  },
50895
50500
  {
50896
- // Master:spec superpowers/specs/2026-08-21-channel-master-m2-design.md
50897
- // 代替老板操盘一群 worker persona:拆线 / 派活 / **验收 / 打回** / 收敛 / 上报。
50898
- // 判断力靠 CLAUDE.md 人格 + .claude/agents/line-watcher.md(核验 subagent),
50899
- // 纪律靠 daemon 侧 zod(`topic:*` 的 reason / acceptanceCriteria 非空在 RPC 入口拒,
50900
- // 没有绕过路径)+ `master:authorityCheck` 查表。M1 那三个 bundle 内 .mjs 脚本已随
50901
- // M2「MCP 化」整体拆除,别再照那套找。
50501
+ // Master 人格/line-watcher 提供判断;topic RPC schema 与 master:authorityCheck 约束操作。
50902
50502
  personaId: "persona-master",
50903
50503
  label: "Master",
50904
50504
  model: "opus",
@@ -51525,14 +51125,7 @@ var PersonaManager = class {
51525
51125
  this.deps = deps;
51526
51126
  }
51527
51127
  deps;
51528
- /**
51529
- * slug 是 persona 的身份,label 只是显示名——两者刻意分开:id 落进文件路径 /
51530
- * 分享链接且创建后不可改,label 可中文、可随时改。此前 id 从 label 推导,
51531
- * 非 ascii 被剥空后一律 fallback 成 `persona-persona`。
51532
- *
51533
- * 撞名直接抛错而非改名:id 由调用方显式指定,静默换成带后缀的 id 等于把
51534
- * 用户的选择偷偷改掉。字符集校验在 PersonaCreateArgsSchema(路径穿越边界)。
51535
- */
51128
+ /** slug 是不可变的路径/分享身份,label 可改;撞名直接拒绝,路径字符由 RPC schema 校验。 */
51536
51129
  create(args) {
51537
51130
  const personaId2 = `persona-${args.slug}`;
51538
51131
  if (this.deps.store.has(personaId2)) {
@@ -51543,10 +51136,7 @@ var PersonaManager = class {
51543
51136
  personaId: personaId2,
51544
51137
  label: args.label,
51545
51138
  model: args.model,
51546
- // effort 兜底 high:persona 一律持有具体档位(spec 2026-08-12),UI 会带界面上高亮的
51547
- // 那一档,但 persona:create 也可能来自 butler 的 persona-authoring skill 这类不带该
51548
- // 字段的调用方——不兜的话要等下次 daemon 启动 backfill 才补上,中间建的 session 就漏了。
51549
- // 取值理由与 backfillPersonaSessionDefaults 一致(见那里的注释)。
51139
+ // 创建时即补 high,避免不带 effort 的调用方建出缺档位会话;理由同 backfillPersonaSessionDefaults。
51550
51140
  effort: args.effort ?? "high",
51551
51141
  tool: args.tool,
51552
51142
  public: args.public ?? false,
@@ -51600,10 +51190,7 @@ var PersonaManager = class {
51600
51190
  listSkills(personaId2) {
51601
51191
  return listSkillsForDir(this.deps.store.skillsDir(personaId2), "project");
51602
51192
  }
51603
- /**
51604
- * 读取 persona `.claude/settings.json` 中已启用的插件列表(value === true 的 key)。
51605
- * 文件不存在 / 解析失败 / 字段缺失 → 空数组。供 persona:get handler 拼响应。
51606
- */
51193
+ /** 已启用插件的读取与容错语义见 PersonaStore.readEnabledPlugins。 */
51607
51194
  listEnabledPlugins(personaId2) {
51608
51195
  return this.deps.store.readEnabledPlugins(personaId2);
51609
51196
  }
@@ -51612,22 +51199,9 @@ var PersonaManager = class {
51612
51199
  return this.deps.store.readCodexSandboxSettings(personaId2);
51613
51200
  }
51614
51201
  /**
51615
- * 局部合并写 codex guest 沙箱配置,返回合并后的全量。
51616
- *
51617
- * **必须是合并不是覆盖**:UI 的「访客联网」开关只发 `{network}`,整体覆盖会静默抹掉 owner
51618
- * 手配的 writableRoots / denyRead(丢的是安全边界,且用户不会立刻发现)。
51619
- * 不走 PersonaManager.update 是因为它管的是 PersonaFile meta + 人格两层文本,
51620
- * 而这份配置是另一个文件、另一套 schema。
51621
- *
51622
- * **读不到时 base 用 CODEX_SANDBOX_DEFAULT 而不是 `{}`**:`{}` 会让这次「合并」实际写出一份
51623
- * 只有 patch 字段的残缺配置(例如只有 `{network:true}`,没有包管理器缓存目录 → `npm install`
51624
- * EPERM),而且从此 migrateCodexSandbox 见文件已存在就跳过,UI 上再也补不回来。
51625
- * 正常路径下文件在 create 时就已物化,这条兜底只在文件被删 / 被改坏时触发。
51626
- *
51627
- * ⚠️ 已知限制:`CodexSandboxSettingsSchema` 是 `.strict()` 的,owner 手编时多写一个键
51628
- * (注释键 / 历史字段)会让整份文件读成 null,于是这次合并把它替换成默认值 ⊕ patch。
51629
- * 已有测试锁定该 strict 语义(store.spec「schema 校验失败 → 返回 null」),本次不动它——
51630
- * 至少残缺配置不会再被写出去。要放宽得单独评估。
51202
+ * 合并局部 patch,保留未改的 writableRoots / denyRead;UI 联网开关仅传 network。
51203
+ * 缺失或无效时以完整 CODEX_SANDBOX_DEFAULT 为 base,避免写出缺缓存目录的配置。
51204
+ * 注意 strict schema:手编的未知键会使整份读成 null,本次合并将覆盖为默认值 ⊕ patch。
51631
51205
  */
51632
51206
  mergeCodexSandbox(personaId2, patch) {
51633
51207
  if (!this.deps.registry.get(personaId2)) throw new Error(`persona not found: ${personaId2}`);
@@ -51640,12 +51214,7 @@ var PersonaManager = class {
51640
51214
  readSandboxSettings(personaId2) {
51641
51215
  return this.deps.store.readSandboxSettings(personaId2);
51642
51216
  }
51643
- /**
51644
- * 删除 persona。
51645
- * PersonaStore.remove(personaId) 级联删整个 <personaRoot>/<personaId>/ 目录
51646
- * (含 .clawd/sessions/owner/ 和 .clawd/sessions/guests/<capId>/),rmSync
51647
- * recursive force 一锅清。目录外的那份(群文件清单)交给 onPersonaDeleted。
51648
- */
51217
+ /** 级联删 persona 目录(含 owner/guest 会话);目录外群文件清单交给 onPersonaDeleted。 */
51649
51218
  delete(personaId2) {
51650
51219
  this.deps.store.remove(personaId2);
51651
51220
  this.deps.registry.remove(personaId2);
@@ -52427,7 +51996,6 @@ var CodexHistoryReader = class {
52427
51996
  toolSessionId: f2.threadId,
52428
51997
  summary: summary || "(\u7A7A\u4F1A\u8BDD)",
52429
51998
  messageCount: 0,
52430
- // 与此前 thread/list 一致:列表不数条数
52431
51999
  updatedAt: new Date(f2.mtimeMs).toISOString()
52432
52000
  };
52433
52001
  });
@@ -53934,8 +53502,7 @@ function threadStartParams(ctx) {
53934
53502
  // sandbox 字段**(避免 sandbox mode 覆盖 profile);owner / 普通会话 → danger-full-access
53935
53503
  // (owner 全权,对齐 CC owner 无 OS 沙箱),不读 permissionMode(那是审批预设,进 approvalPolicy)。
53936
53504
  ...ctx.personaMode === "guest" ? {} : { sandbox: "danger-full-access" },
53937
- // 连接 intro(owner/guest 人称差异化)走 developerInstructions(developer 级、不进消息流、不显示在 UI),
53938
- // 取代旧的"折进首条 turn 文本"(那个被当用户消息显示)。thread/start + thread/resume 都带,幂等。
53505
+ // Connection identity belongs in developerInstructions, outside user history; include on start/resume.
53939
53506
  ...ctx.extraSystemPrompt ? { developerInstructions: ctx.extraSystemPrompt } : {}
53940
53507
  };
53941
53508
  return params;
@@ -54067,8 +53634,7 @@ function driveCodexThread(opts) {
54067
53634
  void client.request("turn/start", {
54068
53635
  threadId,
54069
53636
  input: turnStartInput(text, opts.imageInput),
54070
- // TurnStartParams 字段是 `effort`(不是 `reasoningEffort`,0.138 改名)——传错名字
54071
- // codex 静默忽略, reasoning effort 改了不生效。"override for this turn and subsequent turns"。
53637
+ // Wire field is effort (v2 already used it before 0.138); unknown reasoningEffort is silently ignored.
54072
53638
  ...effort ? { effort } : {}
54073
53639
  }).catch((e) => {
54074
53640
  if (exited) return;
@@ -54407,8 +53973,6 @@ function createCodexSession(ctx, sink, deps = {}) {
54407
53973
  sink,
54408
53974
  logger: deps.logger,
54409
53975
  stream: stdioLineStream(proc),
54410
- // 连接 intro(ctx.extraSystemPrompt,owner/guest 人称)经 threadStartParams 的
54411
- // developerInstructions 传入(developer 级、不进消息流、不显示在 UI、跨 resume 幂等)。
54412
53976
  threadParams: threadStartParams(ctx),
54413
53977
  // persona 惯例装在 <cwd>/.claude/skills/,经 extraRoots 注入 discovery
54414
53978
  // (skills:list RPC 侧 codex-skills.ts 同款注入,两处保持一致)。
@@ -54499,7 +54063,6 @@ var EFFORT_LABELS = {
54499
54063
  };
54500
54064
  var CODEX_DEFAULT_MODEL_ENTRY = {
54501
54065
  id: "",
54502
- // 不写 'Default'——说清楚是谁决定的(对齐 claude 侧「由 CC 决定」)
54503
54066
  label: "\u7531 config.toml \u51B3\u5B9A",
54504
54067
  description: "\u4E0D\u6307\u5B9A\u6A21\u578B\uFF0C\u7528 codex config.toml \u91CC\u914D\u7F6E\u7684\u90A3\u4E2A",
54505
54068
  contextWindowSize: CODEX_CONTEXT_WINDOW,
@@ -54529,9 +54092,7 @@ function mapModelList(raw) {
54529
54092
  ...typeof e.description === "string" ? { description: e.description } : {},
54530
54093
  contextWindowSize: CODEX_CONTEXT_WINDOW,
54531
54094
  ...efforts && efforts.length > 0 ? { efforts } : {},
54532
- // codex 自己标的默认型号。以前空项占着 default:true,这个字段一直没人读;
54533
- // 空项删掉后它是「哪个是默认」的唯一权威来源——新建 persona 的预选、
54534
- // backfill 补 codex model 都依赖它。
54095
+ // Preserve the engine's default for persona preselection/backfill.
54535
54096
  ...e.isDefault === true ? { default: true } : {},
54536
54097
  ...typeof e.defaultReasoningEffort === "string" ? { defaultEffort: e.defaultReasoningEffort } : {}
54537
54098
  });
@@ -54681,7 +54242,6 @@ var CODEX_CAPABILITIES_BASE = {
54681
54242
  tool: "codex",
54682
54243
  toolSessionIdLabel: "Codex Thread ID",
54683
54244
  permissionModes: CODEX_APPROVAL_PRESETS,
54684
- // 本机 Codex 支持原生 child thread、已完成 turn 的 fork;rewind / TUI / observe / 文件分享仍关闭。
54685
54245
  features: { rewind: false, subagents: true, tui: false, observe: false, fileSharing: false, fork: true },
54686
54246
  configSchema: [
54687
54247
  {
@@ -54690,7 +54250,6 @@ var CODEX_CAPABILITIES_BASE = {
54690
54250
  label: "Model",
54691
54251
  scope: "core",
54692
54252
  options: [],
54693
- // 运行时填充,见 buildCapabilities
54694
54253
  default: ""
54695
54254
  },
54696
54255
  {
@@ -54706,7 +54265,6 @@ var CODEX_CAPABILITIES_BASE = {
54706
54265
  type: "select",
54707
54266
  label: "Reasoning effort",
54708
54267
  scope: "tool-specific",
54709
- // 渲染在 Advanced 区(与 claude effort 一致)
54710
54268
  options: CODEX_EFFORTS,
54711
54269
  default: "medium"
54712
54270
  }
@@ -54737,7 +54295,7 @@ async function probeCodex(env = process.env) {
54737
54295
  }
54738
54296
  var CodexAdapter = class {
54739
54297
  id = "codex";
54740
- // 历史读取器:history:read 按 session.tool 路由到这里(thread/read)。
54298
+ // session.tool routes local history reads to the rollout reader.
54741
54299
  historyReader;
54742
54300
  probeCache = null;
54743
54301
  logger;
@@ -54800,10 +54358,7 @@ var CodexAdapter = class {
54800
54358
  return [];
54801
54359
  }
54802
54360
  }
54803
- /**
54804
- * provider 订阅侧全集。未配自定义 provider(官方用户)或任何失败都返回 []。
54805
- * 见 codex-provider-models.ts —— model/list 只报 codex 内置型号,代理转发的模型全靠这里。
54806
- */
54361
+ /** Provider models: unconfigured returns []; failures return [] unless strict refresh requests a throw. */
54807
54362
  async loadProviderModels(strict) {
54808
54363
  try {
54809
54364
  const raw = this.configQueryOverride ? await this.configQueryOverride() : await queryCodexAppServer(process.cwd(), "config/read", {}, {
@@ -55911,7 +55466,6 @@ var ClaudeTuiAdapter = class extends ClaudeAdapter {
55911
55466
  });
55912
55467
  this.tuiOpts.onPopupTransition(ctx.toolSessionId, kind, visible);
55913
55468
  },
55914
- // popup_close 信号源:ready observer 扫到 ❯ + ─{20,} 输入框结构 → 输入框可见 → popup 已关
55915
55469
  getInputFrameVisible: () => readyObserver.inputFrameVisible
55916
55470
  });
55917
55471
  const screenIdleObserver = observeScreenIdle(surface, {
@@ -55991,26 +55545,10 @@ var ClaudeTuiAdapter = class extends ClaudeAdapter {
55991
55545
  });
55992
55546
  return ptyChild;
55993
55547
  }
55994
- // parseLine 有两个调用路径,要正确区分:
55995
- // 1. runner stdout 路径(splitStdoutChunk → 'stdout-line' → reducer.deps.parseLine):
55996
- // TUI 模式 stdout 是 ANSI 字节流,按 \n 切出来全是乱码行,父类 JSON.parse 失败返 [] → 安全
55997
- // 2. observer 路径(session-observer.poll → adapter.parseLine 解析 JSONL 单行):
55998
- // 合法 JSON,父类正常解析返业务 ParsedEvent —— observer 是 TUI 模式业务事件的唯一来源
55999
- //
56000
- // **TUI user 行字符串 content 兼容**(CC v2.1.141 实测):xterm 手输的 user 行写盘形如
56001
- // {"type":"user","message":{"role":"user","content":"老板手输的 prompt"},"uuid":"...",...}
56002
- // content 是字符串而非数组。父类(SDK 模式)的 parseLine 在 `if (!Array.isArray(content))`
56003
- // 处直接 return,user_text 永远进不了 reducer → MessageList 不显示用户消息。
56004
- // 这里 override 检测字符串 content 的 user 行,包装成单元素 [{type:"text",text:...}]
56005
- // 数组形式让父类正常走 user 分支产 user_text。其它形态(tool_result / array content /
56006
- // 非 user 行)原样落父类。
56007
- //
56008
- // 关于 user_text dedup:观察者会抓到两类 user 行:
56009
- // - 结构化输入路径 (session:send):daemon reducer 已 synthesize user_text 上屏;observer
56010
- // 抓到的 jsonl real user 行 = 这次 synth 的副本,**必须丢**(不丢 UI 会重复上屏)
56011
- // - xterm 输入路径 (session:pty:input):daemon 没 synth,observer 是唯一来源,**必须保留**
56012
- // 这层判断需要 sessionId + realUuidBySynth map,parseLine 拿不到,统一由 manager.feedObserverEvents
56013
- // 按 real uuid 是否已被映射到 synth 决定。这里只做基础 parse + JSON 校验
55548
+ // PTY stdout is ANSI and yields no business events; observer JSONL is the TUI event source.
55549
+ // TUI user rows can have string content (CC 2.1.141); wrap it for the parent's array parser.
55550
+ // Manager handles user_text dedup using the synth/real UUID mapping: session:send has a
55551
+ // synthesized copy, whereas xterm input has only the observer row.
56014
55552
  parseLine(line) {
56015
55553
  const trimmed = line.trim();
56016
55554
  if (!trimmed || trimmed[0] !== "{") return super.parseLine(line);
@@ -56138,9 +55676,7 @@ var PersonaDispatchManager = class {
56138
55676
  if (sourceSessionId) this.deps.onCompleted?.(sourceSessionId);
56139
55677
  }
56140
55678
  /**
56141
- * 续派(spec 2026-07-21):complete 已清掉 map 条目,续派前重建——bSessionId 映射
56142
- * 保证 worker 进程死后 respawn 时 reducer lookupDispatchByBSessionId 仍注回同一
56143
- * CLAWD_DISPATCH_ID env。台账行的复位由 handler 直接走 store(本方法只管内存态)。
55679
+ * 续派重建 worker 映射,确保 respawn 仍注入原 CLAWD_DISPATCH_ID;台账复位由 handler 负责。
56144
55680
  */
56145
55681
  reopen(dispatchId, args) {
56146
55682
  this.map.set(dispatchId, {
@@ -56341,17 +55877,9 @@ function createChannelStore(deps) {
56341
55877
  ...deps,
56342
55878
  key: "channels",
56343
55879
  version: 2,
56344
- // v1 → v2(2026-08-26):成员表换形,本机与远程 persona 并进一张 memberPersonas。
56345
- // memberContactDeviceIds 原样留着 —— 它的语义从「可派活给这台设备上的任意 persona」
56346
- // 收窄成「可发 DM」,收窄不需要动数据。
56347
- //
56348
- // 存量频道因此失去跨设备派活能力,这是有意的:要补偿就得在这里去拉对方 daemon 的
56349
- // persona 列表(对方不一定在线),而「他家所有 persona 自动进名单」正是要废掉的
56350
- // 那个隐含推导。老板重新勾一次即可。
56351
- //
56352
- // ⚠️ 写法注意(给下一次 v2→v3 的人):这里是**先解构后覆盖** —— `...rest` 会把
56353
- // 旧行里的同名字段一起带进来,紧跟着的赋值再盖掉它。加新字段时确认清楚是想覆盖
56354
- // 还是想保留,别无意中把 rest 里已有的值清空。
55880
+ // v1→v2 将 memberPersonaIds 转成 memberPersonas;保留 contact 设备名单供 DM 使用。
55881
+ // 不从设备名单隐式授权全部远端 persona:需用户明确重新选择,迁移不依赖远端在线。
55882
+ // rest 先展开,再覆盖迁移字段;扩充格式时注意保留已有字段。
56355
55883
  migrate: (rows) => rows.map((c2) => {
56356
55884
  const { memberPersonaIds, ...rest } = c2;
56357
55885
  return {
@@ -56422,21 +55950,9 @@ function createTopicStore(deps) {
56422
55950
  };
56423
55951
  return {
56424
55952
  /**
56425
- * 读盘后两件归一,**落盘口径不同**:一只改内存(下一次任何写操作自然带出去),
56426
- * 二补过就立刻回写。理由见各自那段。
56427
- *
56428
- * 一、存量 `state: 'archived'` 归成 `'done'`。归档已改由该话题 Master 会话的 `archivedAt`
56429
- * 承载(spec 2026-08-25-topic-archive-and-pin),`TOPIC_STATES` 随之退掉那一档。json-store
56430
- * 读盘本身不过 zod,但 `topic:list` 的响应要过 `TopicListResultSchema.parse` —— 留着这个
56431
- * 值会让**整个频道**的话题列表 parse 失败。
56432
- *
56433
- * 二、补/纠线号水位。放在读盘而不是发号时:水位一旦落后于已发出的号,下一条新线就会捡回
56434
- * 一个已经写进流水的号,那是「留档被读坏」而不是「少个号」,越早接上越好。
56435
- *
56436
- * **补过就立刻固化**(这一条与 archived 那条不同,不能只留在内存里):水位不落盘的话,
56437
- * 下次启动还得靠 lineMeta 重算,而 lineMeta 一旦丢了最大号那条,重算出来的水位就退,
56438
- * 退了就把已经发出去的号又发一遍。不变式不能挂在「lineMeta 一直完好」这个外部事实上。
56439
- * 水位已经合法的话题不算改动,不会因此每次启动都写一次盘。
55953
+ * archived 归一为 done,避免旧值使 topic:list 响应整体 parse 失败;归档标记在 Master 会话上。
55954
+ * 补正水位后 scheduleFlush,尽早持久化;否则重启时再从 lineMeta 推导可能因删线复用旧号。
55955
+ * 水位合法时不触发写盘;仅改 archived 时等待后续业务写操作带出。
56440
55956
  */
56441
55957
  async load() {
56442
55958
  await file.load();
@@ -56464,7 +55980,6 @@ function createTopicStore(deps) {
56464
55980
  acceptanceCriteria: [],
56465
55981
  activity: [],
56466
55982
  lineMeta: {},
56467
- // 新话题的第一条线拿 1
56468
55983
  nextLineOrdinal: 1,
56469
55984
  state: "planning",
56470
55985
  createdAt: iso,
@@ -57445,7 +56960,7 @@ function sanitizeForQuote(def) {
57445
56960
  return Object.fromEntries(
57446
56961
  Object.entries(obj).map(([k3, v3]) => [
57447
56962
  k3,
57448
- // 用一份新正则做 test:全局正则的 lastIndex 会串进后面的 matchAll(mcp-secret-render.ts 同款写法)
56963
+ // 新正则避免全局 lastIndex 在 test / matchAll 间串扰。
57449
56964
  typeof v3 === "string" && v3.length > 0 && !new RegExp(PLACEHOLDER2).test(v3) ? REDACTED : v3
57450
56965
  ])
57451
56966
  );
@@ -57683,8 +57198,7 @@ var PackageFindingSchema = EnvPackageSchema.omit({ localVersion: true, artifact:
57683
57198
  var FINDING_SCHEMAS = {
57684
57199
  package: PackageFindingSchema,
57685
57200
  secret: EnvSecretSchema,
57686
- // butler 报的是 CLI 连接器的「名字 + 它读的环境变量名」,kind / runner / id 由 daemon 补
57687
- //(handlers/deploy.ts 的 cliConnectorFromFinding)。MCP 连接器不经上报口,由 scanMcpConnectors 机械列出。
57201
+ // 仅收 CLI 连接器发现;MCP 由 scanMcpConnectors 从配置列出。
57688
57202
  connector: CliConnectorFindingSchema
57689
57203
  };
57690
57204
  function parseFinding(kind, item) {
@@ -61608,13 +61122,11 @@ var L2 = [
61608
61122
  "",
61609
61123
  "jobs:",
61610
61124
  " build:",
61611
- // name 就是段 key:daemon 按 jobs API 回的 name 认段(那接口不返回 job 的 key)。
61612
- // 中文说明归 clawd 的上云页面,这里保持机器好认。
61125
+ // jobs API 只返 name 不返 key,故 name 必须用段 key。
61613
61126
  " name: build",
61614
61127
  " runs-on: ubuntu-latest",
61615
- // ACR 个人版的构建按账号串行:底座(8 分钟)/ api 在前面排着时要等它们先构完。
61128
+ // ACR 账号内串行构建,需给其他镜像排队留时间。
61616
61129
  " timeout-minutes: 40",
61617
- // 没有 outputs:——smoke / deploy 段自己读契约、自己解析 digest(文件头)。
61618
61130
  " steps:",
61619
61131
  ...CONTRACT_STEPS,
61620
61132
  " - name: \u51ED\u636E\u9F50\u4E0D\u9F50\uFF08\u7F3A\u54EA\u51E0\u4E2A\u540D\u5B57\u4E00\u6B21\u5217\u5168\uFF09",
@@ -61651,7 +61163,7 @@ var L2 = [
61651
61163
  " ALIBABA_CLOUD_ACCESS_KEY_ID: ${{ secrets.ALI_ACS_ACCESS_KEY_ID }}",
61652
61164
  " ALIBABA_CLOUD_ACCESS_KEY_SECRET: ${{ secrets.ALI_ACS_ACCESS_KEY_SECRET }}",
61653
61165
  " run: |",
61654
- // pipefail:ls-remote 那条管道里 cut 永远退 0,没有 pipefail 的话 ls-remote 挂了 TIP 是空串、红在「main 已经推进到 」这句误导人的话上
61166
+ // pipefail 让 ls-remote 失败正确传播,避免被 cut 的成功状态掩盖。
61655
61167
  " set -eo pipefail",
61656
61168
  ' REPO="$ACR_NAMESPACE/${{ steps.plan.outputs.persona_id }}"',
61657
61169
  ' node clawos/cloud-host/pipeline/acr.mjs ensure-repo --repo "$REPO" \\',
@@ -61678,9 +61190,7 @@ var L2 = [
61678
61190
  " - name: \u4E34\u65F6\u767B\u8BB0 \u2192 \u8D77\u4E00\u6B21 Pod \u2192 \u64A4\u9500",
61679
61191
  " # \u767B\u8BB0\u6B63\u5F0F\u53EA\u662F\u5F80 gateway \u5199\u4E00\u884C\uFF0Cgateway \u4E0D\u78B0\u955C\u50CF\uFF1B\u96C6\u7FA4\u62C9\u955C\u50CF\u53D1\u751F\u5728\u7B2C\u4E00\u4E2A\u4EFB\u52A1\u5230\u6765\u65F6\u3002",
61680
61192
  " # \u6CA1\u6709\u8FD9\u4E00\u6B65\uFF0C\u6D41\u6C34\u7EBF\u7EFF\u4E86\u53EA\u4EE3\u8868\u300C\u767B\u8BB0\u4E86\u300D\u3002\u955C\u50CF\u7528 digest\uFF08\u96C6\u7FA4 Job \u662F IfNotPresent\uFF0C\u79FB\u52A8 tag \u4F1A\u4E00\u76F4\u8DD1\u65E7\u7684\uFF09\u3002",
61681
- // 模型 / 档位 / 连接器 smoke 也要:试跑那次要跟正式那次拿到同一份,否则验的不是将要上线的东西。
61682
- // 名字形状 deploy-file.mjs 已卡死(persona_id [a-z0-9-]、model / effort [A-Za-z0-9._/-](`/` 是 pi 的 provider/id,不是元字符)、size 四档枚举、task_concurrency 正整数、connectors 无单引号
61683
- // 无换行),所以单引号包住直接拼进命令行是安全的。
61193
+ // smoke 与正式发布用同份模型/档位/连接器;deploy-file.mjs 拒绝单引号/换行等危险值,方可单引号拼参。
61684
61194
  " env:",
61685
61195
  " CLAWD_GATEWAY_URL: ${{ vars.CLAWD_GATEWAY_URL }}",
61686
61196
  " CLAWD_GATEWAY_ADMIN_KEY: ${{ secrets.CLAWD_GATEWAY_ADMIN_KEY }}",
@@ -63028,13 +62538,11 @@ var SubscriptionChannelStore = class {
63028
62538
  const data = this.readOrCreate();
63029
62539
  return data.channels.find((c2) => c2.id === id) ?? null;
63030
62540
  }
63031
- /** 获取当前激活的通道实体 */
63032
62541
  getActive() {
63033
62542
  const data = this.readOrCreate();
63034
62543
  if (!data.activeChannelId) return null;
63035
62544
  return data.channels.find((c2) => c2.id === data.activeChannelId) ?? null;
63036
62545
  }
63037
- /** 保存(新增或更新)通道 */
63038
62546
  save(input) {
63039
62547
  const name = input.name.trim();
63040
62548
  if (!name) throw new Error("\u901A\u9053\u540D\u79F0\u4E0D\u80FD\u4E3A\u7A7A");
@@ -63082,7 +62590,6 @@ var SubscriptionChannelStore = class {
63082
62590
  this.write(data);
63083
62591
  return newChannel;
63084
62592
  }
63085
- /** 删除通道 */
63086
62593
  delete(id) {
63087
62594
  const data = this.readOrCreate();
63088
62595
  const filtered = data.channels.filter((c2) => c2.id !== id);
@@ -63322,15 +62829,7 @@ function createGithubIdentity(deps) {
63322
62829
  await adopt(token);
63323
62830
  return status();
63324
62831
  },
63325
- /**
63326
- * 本机还没有身份时先问中心有没有本人现成的 token(换台机器授权过 / 上次跳回来了但 daemon 没轮到):
63327
- * 有且 GitHub 认就直接领、不跳 GitHub。本机已有身份(能用的 = 用户要换一个;失效的 = 要重新授权,而中心那份就是这把死的)
63328
- * 一律跳 GitHub 重来。进 pending 之后每次 status() 顺手问一次中心领没领到。
63329
- *
63330
- * 跳 GitHub 的那一次**直接回 pending 带链接**,不经 status():页面只在拿到 pending 时才开授权页
63331
- * (ui hooks/useGithubAuth.ts),而 status() 在本机身份还能用时会照常显示它——「换一个」走 status() 就把链接吞了,
63332
- * 用户点了没跳 GitHub、卡片直接显示已关联,中心那把死 token 永远换不掉(2026-09-17 实测)。之后的轮询仍走 status()。
63333
- */
62832
+ // 已有身份时重新授权;直接返回 pending 链接,不能经 status() 被旧的 authorized 状态遮掉。
63334
62833
  async oauthStart(returnTo) {
63335
62834
  const local = await refresh();
63336
62835
  if (!local && await claimCenterToken()) return status();
@@ -64876,7 +64375,6 @@ async function forwardDispatchToPeer(args) {
64876
64375
  body: JSON.stringify({
64877
64376
  targetPersona: args.targetPersona,
64878
64377
  prompt: args.prompt,
64879
- // T-20: 条件展开防 undefined 污染 JSON。远端 daemon handler 会再校验一次 model 合法性。
64880
64378
  ...args.model ? { model: args.model } : {}
64881
64379
  })
64882
64380
  });
@@ -65673,7 +65171,7 @@ var PiRpcClient = class {
65673
65171
  if (this.closed || this.inputClosed) return;
65674
65172
  this.write(frame);
65675
65173
  }
65676
- /** 关写端 = pi 的优雅退出信号(本机 stdin EOF → 进程退出 code 0;云上 = 断开会话通道) */
65174
+ /** Close the write end for Pi's graceful stdin-EOF shutdown. */
65677
65175
  endInput() {
65678
65176
  if (this.inputClosed) return;
65679
65177
  this.inputClosed = true;
@@ -65966,7 +65464,6 @@ function drivePiSession(opts) {
65966
65464
  void client.request({ type: "abort" }).catch(() => {
65967
65465
  });
65968
65466
  },
65969
- // 信号由 runner 透传 reducer 的 kill effect;怎么落地由壳定(本机 endInput + 宽限 + kill;云上关流)
65970
65467
  async stop(signal = "SIGTERM") {
65971
65468
  await opts.kill(signal);
65972
65469
  }
@@ -66081,7 +65578,7 @@ function mapPiModels(raw) {
66081
65578
  const efforts = levels.length ? levels.map((v3) => PI_THINKING_EFFORTS.find((e) => e.value === v3) ?? { value: v3, label: v3 }) : m3.reasoning === true ? PI_THINKING_EFFORTS : [PI_THINKING_EFFORTS[0]];
66082
65579
  out.push({
66083
65580
  id: `${provider}/${id}`,
66084
- // 与 --model 同一串
65581
+ // Ids use provider/model, matching --model.
66085
65582
  label: str2(m3.name) || id,
66086
65583
  contextWindowSize: cw && cw > 0 ? cw : PI_DEFAULT_CONTEXT_WINDOW,
66087
65584
  efforts,
@@ -66097,7 +65594,6 @@ function buildPiCapabilities(models) {
66097
65594
  toolSessionIdLabel: "pi Session ID",
66098
65595
  models,
66099
65596
  permissionModes: [PI_FULL_ACCESS],
66100
- // 无 file-history-snapshot / 无 pty / observer 是 CC jsonl 解析器 / 无文件分享 / fork 是 claude jsonl 专用 / 无子 agent
66101
65597
  features: { rewind: false, subagents: false, tui: false, observe: false, fileSharing: false, fork: false },
66102
65598
  configSchema: [
66103
65599
  {
@@ -66293,7 +65789,6 @@ var CLOUD_PI_CAPABILITIES = {
66293
65789
  toolSessionIdLabel: "pi Session ID",
66294
65790
  models: [],
66295
65791
  permissionModes: [CLOUD_APPROVAL2],
66296
- // 无 rewind / 无子 agent(pi 核心没有)/ 无 pty / observer 是 CC jsonl 解析器 / 无文件分享 / fork 是 claude jsonl 专用
66297
65792
  features: { rewind: false, subagents: false, tui: false, observe: false, fileSharing: false, fork: false },
66298
65793
  configSchema: [
66299
65794
  {
@@ -66786,8 +66281,7 @@ var SubagentMetaObserver = class {
66786
66281
  w3.pollTimer = setInterval(() => this.scan(w3), 200);
66787
66282
  return { dirPath };
66788
66283
  }
66789
- // fs.watch 必须在目录存在后调用;首次启动时 subagents/ 通常不存在 —— attachWatcher
66790
- // 失败就交给 pollTimer 200ms 兜底,目录一出现就被下次 poll 捡起来,那时也尝试 attach
66284
+ // 首次 subagents/ 目录通常还不存在;watch 失败后由 poll 重试 attach。
66791
66285
  attachWatcher(w3) {
66792
66286
  if (w3.watcher) return;
66793
66287
  try {
@@ -66871,9 +66365,6 @@ var SessionObserver = class {
66871
66365
  opts;
66872
66366
  home;
66873
66367
  watches = /* @__PURE__ */ new Map();
66874
- // 子 observer:监听 <projectsRoot>/<hashDir>/<toolSessionId>/subagents/ 目录的 meta.json,
66875
- // 仅 TUI 模式启用——SDK 模式靠 `system.task_started` 帧(claude.ts:466)emit subagent_progress
66876
- // 已经能解锁 AgentCallLine 抽屉入口,再跑 meta watcher 会让 ring buffer 留双帧
66877
66368
  metaObserver;
66878
66369
  resolveJsonlPath(cwd, toolSessionId, override) {
66879
66370
  if (override) return override;
@@ -66922,11 +66413,8 @@ var SessionObserver = class {
66922
66413
  });
66923
66414
  return { filePath };
66924
66415
  }
66925
- // 读 JSONL 文件尾部 N 行,把每行的 meta_update 字段**合并成一个 patch** 喂给 reducer。
66926
- // 每行 JSONL(assistant / user / system / ...)都会携带顶层 gitBranch 等元信息 →
66927
- // parseLine 对每行都产一条 meta_update,N=200 行就是 200 条几乎相同的 patch。
66928
- // reducer.shallowEqMeta 会 dedup 但 CPU + log 仍吵。改成同 patch 字段保留最新值合并成单条。
66929
- // 异常静默吞,不阻塞 watcher 启动
66416
+ // 合并尾部 meta_update 为单条 patch,各字段取最后出现的值;逐行发送会造成重复 CPU/log。
66417
+ // 读取或解析失败不阻塞 watcher 启动。
66930
66418
  hydrateMetaTail(w3, maxLines = 200) {
66931
66419
  try {
66932
66420
  const raw = import_node_fs45.default.readFileSync(w3.filePath, "utf8");
@@ -66995,10 +66483,8 @@ var SessionObserver = class {
66995
66483
  }
66996
66484
  if (w3.pendingCwdDerive) this.maybeDeriveCwd(w3);
66997
66485
  }
66998
- // [T-17] 文件消失后的兜底重定位:按 tsid 全局扫描 → 切 watcher → lastSize/buf 保留接续读
66999
- // (CC 是 move,内容前缀不变,不重复喂;异常变小走 poll 的截断重建分支,上游 uuid dedup 兜底)。
67000
- // 检测点唯一(poll ENOENT),fs.watch rename 事件也 funnel 到 poll,天然幂等;scan 加 1s
67001
- // rate-limit 防 ENOENT 持续期(如 cleanupPeriodDays 真删除)反复 readdir 空转。
66486
+ // CC relocation 是 move,前缀不变:切 watcher 时保留 lastSize/buf 接续读,避免重复。
66487
+ // 文件变小由 poll 按截断处理,uuid dedup 兜底;扫描限频,避免真正删除后持续空转。
67002
66488
  maybeRelocate(w3) {
67003
66489
  const now = Date.now();
67004
66490
  if (now - w3.lastRelocateScanMs < 1e3) return;
@@ -67045,9 +66531,7 @@ var SessionObserver = class {
67045
66531
  toolSessionId: w3.toolSessionId
67046
66532
  });
67047
66533
  }
67048
- // 解析 JSONL 单行:仅当是主链 user 文本行(非 sidechain / 非 sub-agent / message.role='user'
67049
- // 且 content 含 text part)时,抽出 real uuid + 第一段 text + timestamp 上报。
67050
- // 不依赖 adapter——这是 CC JSONL 格式的兜底识别,与 observer 整体的 CC 耦合一致
66534
+ // CC JSONL 格式兜底,不依赖 adapter;只上报主链 user 文本的 real uuid、首段 text 和 timestamp。
67051
66535
  maybeReportUserMessage(sessionId, line) {
67052
66536
  const cb = this.opts.onUserMessageObserved;
67053
66537
  if (!cb) return;
@@ -68025,11 +67509,7 @@ var InboxStore = class {
68025
67509
  fs58.mkdirSync(this.dirPath(), { recursive: true });
68026
67510
  }
68027
67511
  dataDir;
68028
- /**
68029
- * 列出某 peer 的消息, 按 createdAt 升序.
68030
- * sinceCreatedAt 缺省时返回全量; 提供时仅返回 createdAt > sinceCreatedAt 的增量.
68031
- * 文件不存在时返回 [] (不抛, 不创建).
68032
- */
67512
+ /** sinceCreatedAt 缺省读全量,否则仅读时间戳更大的消息;读取失败返回空数组。 */
68033
67513
  list(peerDeviceId, sinceCreatedAt) {
68034
67514
  const file = this.filePath(peerDeviceId);
68035
67515
  let raw;
@@ -68043,9 +67523,6 @@ var InboxStore = class {
68043
67523
  if (sinceCreatedAt === void 0) return all;
68044
67524
  return all.filter((m3) => m3.createdAt > sinceCreatedAt);
68045
67525
  }
68046
- /**
68047
- * 列出所有 peer 的 peerDeviceId (扫目录所有 *.jsonl 文件名).
68048
- */
68049
67526
  listAllPeerDeviceIds() {
68050
67527
  const dir = this.dirPath();
68051
67528
  let entries;
@@ -68057,10 +67534,7 @@ var InboxStore = class {
68057
67534
  }
68058
67535
  return entries.filter((name) => name.endsWith(".jsonl")).map((name) => name.slice(0, -".jsonl".length));
68059
67536
  }
68060
- /**
68061
- * 追加一条消息,按 message.id 幂等去重。
68062
- * 双投 + 重试场景下保护 — 同 id 已存在则 no-op,不重写不覆盖。
68063
- */
67537
+ /** 按 message.id 幂等追加;双投 / 重试不会重复写入或覆盖。 */
68064
67538
  append(message) {
68065
67539
  const existing = this.list(message.peerDeviceId);
68066
67540
  if (existing.some((m3) => m3.id === message.id)) return;
@@ -68072,11 +67546,7 @@ var InboxStore = class {
68072
67546
  } catch {
68073
67547
  }
68074
67548
  }
68075
- /**
68076
- * 把该 peer 所有 createdAt<=upToCreatedAt 且 sender!=readerDeviceId 的消息
68077
- * readBy[readerDeviceId] 写成 upToCreatedAt. O(N) rewrite 整个文件.
68078
- * 返写入了几条.
68079
- */
67549
+ /** 对方消息的 readBy 水位只升不降;整文件重写,返回实际修改条数。 */
68080
67550
  markRead(peerDeviceId, readerDeviceId, upToCreatedAt) {
68081
67551
  const messages = this.list(peerDeviceId);
68082
67552
  let changed = 0;
@@ -68092,9 +67562,6 @@ var InboxStore = class {
68092
67562
  this.rewriteFile(peerDeviceId, next);
68093
67563
  return changed;
68094
67564
  }
68095
- /**
68096
- * 删整个 peer 的 jsonl (peer:remove cascade). 不存在 no-op.
68097
- */
68098
67565
  removeByPeerDeviceId(peerDeviceId) {
68099
67566
  const file = this.filePath(peerDeviceId);
68100
67567
  try {
@@ -68146,13 +67613,7 @@ var InboxManager = class {
68146
67613
  }
68147
67614
  store;
68148
67615
  broadcast;
68149
- /**
68150
- * 写一条消息到 peer.jsonl + broadcast 给本机所有 owner ws。
68151
- * 按 id 幂等:同 id 已落盘则 no-op(不广播、不重写)。
68152
- *
68153
- * caller 必须已通过 handler 鉴权
68154
- * (owner ctx 必填 args.peerOwnerId;guest ctx 由 handler 反查 peer-store)。
68155
- */
67616
+ /** 按 id 幂等写入并广播;调用方须先鉴权并确定 peerDeviceId / senderDeviceId。 */
68156
67617
  postMessage(args) {
68157
67618
  const beforeLen = this.store.list(args.peerDeviceId).length;
68158
67619
  const msg = {
@@ -68177,10 +67638,7 @@ var InboxManager = class {
68177
67638
  list(peerDeviceId, sinceCreatedAt) {
68178
67639
  return this.store.list(peerDeviceId, sinceCreatedAt);
68179
67640
  }
68180
- /**
68181
- * 标已读. 返写入了几条 (sender !== self 且 createdAt <= upToCreatedAt 的消息).
68182
- * broadcast 已更新 readBy 的每条消息 (单帧一消息, 复用 inbox:event 通道).
68183
- */
67641
+ /** 更新对方消息的已读水位,返回实际修改条数,并逐消息广播 readBy 变更。 */
68184
67642
  markRead(args) {
68185
67643
  const before = this.store.list(args.peerDeviceId);
68186
67644
  const changed = this.store.markRead(args.peerDeviceId, args.readerDeviceId, args.upToCreatedAt);
@@ -68199,7 +67657,6 @@ var InboxManager = class {
68199
67657
  }
68200
67658
  return changed;
68201
67659
  }
68202
- /** peer:remove cascade. 删整个 peer 的 jsonl. */
68203
67660
  removeChannel(peerDeviceId) {
68204
67661
  this.store.removeByPeerDeviceId(peerDeviceId);
68205
67662
  }
@@ -70866,7 +70323,6 @@ function buildDeviceHandlers(deps) {
70866
70323
  return {
70867
70324
  response: {
70868
70325
  type: "device:connect:ok",
70869
- // 返回连接的设备标识(= 请求的 args.deviceId)
70870
70326
  deviceId: args.deviceId,
70871
70327
  name: contact.displayName,
70872
70328
  url: contact.remoteUrl
@@ -73739,12 +73195,7 @@ var METHOD_GRANT_MAP = {
73739
73195
  // 跨设备 exec(spec 2026-07-16):public——"能连上=有 auth",业务在 handler 校
73740
73196
  // (ensureGuest + remoteAccessAllowed),跟 inbox:postMessage 同模式。
73741
73197
  "peerExec:run": { kind: "public" },
73742
- // 2026-05-26 修 clawd people TUI 渲染 mix 串扰回归(参考 f1133ff0 原 owner 路径
73743
- // snapshot/buffer/flush 修复):之前三件套 ADMIN_ANY 把 guest 拦在 dispatcher 401,
73744
- // 导致 XtermPanel 重挂载拉 snapshot 在 guest 路径不生效,老的字节流断裂串扰回来。
73745
- // 同一个 session 永远只有一个操作者(owner 不进 guest 创建的 session),并发字节流
73746
- // 冲突不存在;guest 在 chat 模式下已能 session:send/interrupt,PTY 域不开放纯属
73747
- // 历史遗留的不对称。handler 端通过 ensureSessionAccess 验权对齐其他 session:* RPC。
73198
+ // PTY 与其他 session RPC 一样,由 handler 通过 ensureSessionAccess 校验会话访问权限。
73748
73199
  "session:pty:input": CAPABILITY_SCOPED,
73749
73200
  "session:pty:resize": CAPABILITY_SCOPED,
73750
73201
  "session:pty:snapshot": CAPABILITY_SCOPED,
@@ -73838,7 +73289,7 @@ function computeMethodAccess(args) {
73838
73289
  }
73839
73290
 
73840
73291
  // src/version.ts
73841
- var version = "0.2.619".length > 0 ? "0.2.619" : "dev";
73292
+ var version = "0.2.620".length > 0 ? "0.2.620" : "dev";
73842
73293
 
73843
73294
  // src/cli-probe/probe.ts
73844
73295
  var fs84 = __toESM(require("fs"), 1);