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