@clawos-dev/clawd 0.2.599 → 0.2.601

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 (2) hide show
  1. package/dist/cli.cjs +174 -37
  2. package/package.json +1 -1
package/dist/cli.cjs CHANGED
@@ -46612,6 +46612,7 @@ function applyCommand(state, command, deps) {
46612
46612
  resolvedModel: _model,
46613
46613
  // aiLabel 是旧对话内容生成的标题,/clear 后属于不存在的话题,一并清掉
46614
46614
  aiLabel: _aiLabel,
46615
+ aiLabelGeneratedAt: _titleGeneratedAt,
46615
46616
  ...rest
46616
46617
  } = next.file;
46617
46618
  void _drop;
@@ -46620,6 +46621,7 @@ function applyCommand(state, command, deps) {
46620
46621
  void _branch;
46621
46622
  void _model;
46622
46623
  void _aiLabel;
46624
+ void _titleGeneratedAt;
46623
46625
  const nextFile = {
46624
46626
  ...rest,
46625
46627
  ...command.nextToolSessionId ? { toolSessionId: command.nextToolSessionId } : {},
@@ -47686,7 +47688,7 @@ var SessionManager = class {
47686
47688
  // sessionId → SessionRunner;在 send / ensureSession 时按需创建
47687
47689
  runners = /* @__PURE__ */ new Map();
47688
47690
  // 标题生成 in-flight 防重(进程内);跨重启防重靠 SessionFile.aiLabelGeneratedAt
47689
- titleGenPending = /* @__PURE__ */ new Set();
47691
+ titleGenPending = /* @__PURE__ */ new Map();
47690
47692
  // 当同步命令执行期间,runner 的 emit-frame 临时 push 到这里;
47691
47693
  // null 表示当前是异步路径(stdout 'data' / proc 'exit' 等),走 deps.broadcastFrame
47692
47694
  currentCollector = null;
@@ -48445,6 +48447,10 @@ var SessionManager = class {
48445
48447
  }
48446
48448
  update(args) {
48447
48449
  const existing = this.getFile(args.sessionId);
48450
+ if (args.patch.model !== void 0 || args.patch.label) {
48451
+ this.titleGenPending.get(args.sessionId)?.abort();
48452
+ this.titleGenPending.delete(args.sessionId);
48453
+ }
48448
48454
  const runner = this.runners.get(args.sessionId);
48449
48455
  if (runner) {
48450
48456
  const { value, broadcast } = this.withCollector(() => {
@@ -48454,6 +48460,12 @@ var SessionManager = class {
48454
48460
  return { response: value, broadcast };
48455
48461
  }
48456
48462
  const updated = { ...existing, ...args.patch, updatedAt: nowIso2(this.deps) };
48463
+ if (args.patch.model !== void 0) {
48464
+ delete updated.resolvedModel;
48465
+ delete updated.resolvedEffort;
48466
+ } else if (args.patch.effort !== void 0) {
48467
+ delete updated.resolvedEffort;
48468
+ }
48457
48469
  this.writeOwned(updated);
48458
48470
  return { response: updated, broadcast: [] };
48459
48471
  }
@@ -48517,6 +48529,7 @@ var SessionManager = class {
48517
48529
  delete(args) {
48518
48530
  const existing = this.findOwnedSession(args.sessionId);
48519
48531
  if (!existing) throw new ClawdError(ERROR_CODES.SESSION_NOT_FOUND, args.sessionId);
48532
+ this.titleGenPending.get(args.sessionId)?.abort();
48520
48533
  const scope = this.scopeForFile(existing);
48521
48534
  const runner = this.runners.get(args.sessionId);
48522
48535
  if (runner) {
@@ -48580,7 +48593,7 @@ var SessionManager = class {
48580
48593
  /**
48581
48594
  * 首条 send 触发的智能标题自产(spec:PR #1196 追加拍板 2026-08-17)。
48582
48595
  * 背景见 tools/title-generator.ts 头注释——CC/codex 两条上游标题数据源都等不来
48583
- * 「首轮即有」,daemon 用会话同家族小模型自己生成,与主 turn 并行,几秒后经
48596
+ * 「首轮即有」,daemon 用本会话选定的引擎和模型自己生成,与主 turn 并行,完成后经
48584
48597
  * manager.update 内部通道写 aiLabel(marker patch:不刷 updatedAt、广播 session:info)。
48585
48598
  *
48586
48599
  * 跳过条件:
@@ -48595,15 +48608,20 @@ var SessionManager = class {
48595
48608
  const sessionId = file.sessionId;
48596
48609
  if (file.aiLabelGeneratedAt || file.label || file.larkChatId) return;
48597
48610
  if (this.titleGenPending.has(sessionId)) return;
48598
- this.titleGenPending.add(sessionId);
48599
- void generate(file.tool ?? "claude", text).then((title) => {
48611
+ const controller = new AbortController();
48612
+ this.titleGenPending.set(sessionId, controller);
48613
+ void generate(file, text, controller.signal).then((title) => {
48614
+ if (controller.signal.aborted) return;
48615
+ const current = this.findOwnedSession(sessionId);
48616
+ if (!current || current.label || current.aiLabelGeneratedAt || current.tool !== file.tool || current.model !== file.model) return;
48600
48617
  if (!title) return;
48601
48618
  this.applyResolvedMeta(sessionId, { aiLabel: title });
48602
48619
  this.deps.logger?.info("title-gen ok", { sessionId, tool: file.tool, title });
48603
48620
  }).catch((err) => {
48621
+ if (controller.signal.aborted) return;
48604
48622
  this.deps.logger?.warn("title-gen failed", { sessionId, message: err.message });
48605
48623
  }).finally(() => {
48606
- this.titleGenPending.delete(sessionId);
48624
+ if (this.titleGenPending.get(sessionId) === controller) this.titleGenPending.delete(sessionId);
48607
48625
  });
48608
48626
  }
48609
48627
  /**
@@ -48842,6 +48860,8 @@ var SessionManager = class {
48842
48860
  }
48843
48861
  newSession(args) {
48844
48862
  const existingFile = this.getFile(args.sessionId);
48863
+ this.titleGenPending.get(args.sessionId)?.abort();
48864
+ this.titleGenPending.delete(args.sessionId);
48845
48865
  const nextToolSessionId = this.deps.mode === "tui" && (existingFile.tool ?? "claude") === "claude" ? (0, import_node_crypto3.randomUUID)() : void 0;
48846
48866
  this.clearPendingTurnEnd(args.sessionId);
48847
48867
  const runner = this.runners.get(args.sessionId);
@@ -48862,6 +48882,8 @@ var SessionManager = class {
48862
48882
  contextWindowSize: _win,
48863
48883
  gitBranch: _branch,
48864
48884
  resolvedModel: _model,
48885
+ aiLabel: _aiLabel,
48886
+ aiLabelGeneratedAt: _titleGeneratedAt,
48865
48887
  ...rest
48866
48888
  } = existing;
48867
48889
  void _drop;
@@ -48869,6 +48891,8 @@ var SessionManager = class {
48869
48891
  void _win;
48870
48892
  void _branch;
48871
48893
  void _model;
48894
+ void _aiLabel;
48895
+ void _titleGeneratedAt;
48872
48896
  const updated = {
48873
48897
  ...rest,
48874
48898
  ...nextToolSessionId ? { toolSessionId: nextToolSessionId } : {},
@@ -48941,6 +48965,8 @@ var SessionManager = class {
48941
48965
  return { runningSessions: running };
48942
48966
  }
48943
48967
  stopAll() {
48968
+ for (const controller of this.titleGenPending.values()) controller.abort();
48969
+ this.titleGenPending.clear();
48944
48970
  for (const r2 of this.runners.values()) {
48945
48971
  r2.input({ kind: "command", command: { kind: "stop" } });
48946
48972
  }
@@ -49117,6 +49143,8 @@ var SessionManager = class {
49117
49143
  `sub-session not found: ${scopeKey(args.scope)}/${args.sessionId}`
49118
49144
  );
49119
49145
  }
49146
+ this.titleGenPending.get(args.sessionId)?.abort();
49147
+ this.titleGenPending.delete(args.sessionId);
49120
49148
  const runner = this.ensureRunnerForScope(file, args.scope);
49121
49149
  const nextToolSessionId = this.deps.mode === "tui" && (file.tool ?? "claude") === "claude" ? (0, import_node_crypto3.randomUUID)() : void 0;
49122
49150
  const { broadcast } = this.withCollector(() => {
@@ -53763,7 +53791,7 @@ function driveCodexThread(opts) {
53763
53791
  if (opts.skillsExtraRoots?.length) {
53764
53792
  void client.request("skills/extraRoots/set", { extraRoots: opts.skillsExtraRoots }).catch((e) => logger?.warn("codex skills/extraRoots/set failed", { message: e.message }));
53765
53793
  }
53766
- const threadParams = typeof opts.threadParams === "function" ? await opts.threadParams() : opts.threadParams;
53794
+ const threadParams = typeof opts.threadParams === "function" ? await opts.threadParams(client) : opts.threadParams;
53767
53795
  if (threadId) {
53768
53796
  await client.request("thread/resume", { threadId, ...threadParams });
53769
53797
  } else {
@@ -64352,6 +64380,7 @@ var CloudGatewayClient = class {
64352
64380
  * `toolSessionId`:调用方预分配的引擎会话 id(pi 的 --session-id;codex 的 thread id 是协议内的、不传),进 open 的 body `engineSessionId`
64353
64381
  * (契约 2026-09-18 §3.2)。`instructions`:会话级指令(pi-cloud 的连接 intro,工作目录写 CLOUD_WORKSPACE_SLOT),进 body `instructions`,
64354
64382
  * gateway 落成 Pod 里的 /run/agent/instructions.md(契约 §3.1);codex-cloud 不发(走协议内 developerInstructions)。
64383
+ * `expectedEngine` 是 session 创建时的引擎;gateway 在起 Pod 前与当前发布核对。
64355
64384
  */
64356
64385
  async openSession(personaId2, opts = {}) {
64357
64386
  const { apiKey, gatewayUrl } = await this.credentials();
@@ -64362,6 +64391,8 @@ var CloudGatewayClient = class {
64362
64391
  personaId: personaId2,
64363
64392
  ...opts.toolSessionId ? { engineSessionId: opts.toolSessionId } : {},
64364
64393
  ...opts.instructions ? { instructions: opts.instructions } : {},
64394
+ ...opts.purpose ? { purpose: opts.purpose } : {},
64395
+ ...opts.expectedEngine ? { expectedEngine: opts.expectedEngine } : {},
64365
64396
  logger: this.deps.logger
64366
64397
  });
64367
64398
  }
@@ -64419,7 +64450,9 @@ function parseControlLine(line) {
64419
64450
  kind: "ready",
64420
64451
  session: typeof r2.session === "string" ? r2.session : "",
64421
64452
  personaDir: typeof r2.personaDir === "string" ? r2.personaDir : "",
64422
- workDir: typeof r2.workDir === "string" ? r2.workDir : ""
64453
+ workDir: typeof r2.workDir === "string" ? r2.workDir : "",
64454
+ ...typeof r2.model === "string" ? { model: r2.model } : {},
64455
+ ...typeof r2.engine === "string" ? { engine: r2.engine } : {}
64423
64456
  };
64424
64457
  case "error":
64425
64458
  return { kind: "error", message: typeof r2.message === "string" ? r2.message : "gateway error" };
@@ -64428,7 +64461,7 @@ function parseControlLine(line) {
64428
64461
  }
64429
64462
  }
64430
64463
  function openHttpLineStream(opts) {
64431
- const { fetchImpl, gatewayUrl, apiKey, personaId: personaId2, engineSessionId, instructions, logger } = opts;
64464
+ const { fetchImpl, gatewayUrl, apiKey, personaId: personaId2, engineSessionId, instructions, purpose, expectedEngine, logger } = opts;
64432
64465
  const headers = { authorization: `Bearer ${apiKey}`, "content-type": "application/json" };
64433
64466
  const ac = new AbortController();
64434
64467
  const lineCbs = [];
@@ -64496,7 +64529,7 @@ function openHttpLineStream(opts) {
64496
64529
  res = await fetchImpl(gatewayUrl + GATEWAY_ROUTES.session(personaId2), {
64497
64530
  method: "POST",
64498
64531
  headers,
64499
- body: JSON.stringify({ ...engineSessionId ? { engineSessionId } : {}, ...instructions ? { instructions } : {} }),
64532
+ body: JSON.stringify({ ...engineSessionId ? { engineSessionId } : {}, ...instructions ? { instructions } : {}, ...purpose ? { purpose } : {}, ...expectedEngine ? { expectedEngine } : {} }),
64500
64533
  signal: ac.signal
64501
64534
  });
64502
64535
  } catch (e) {
@@ -64510,6 +64543,7 @@ function openHttpLineStream(opts) {
64510
64543
  const body = await res.json();
64511
64544
  if (typeof body.message === "string") message = body.message;
64512
64545
  else if (typeof body.error === "string") message = body.error;
64546
+ if (body.error === "SESSION_VERSION_EXPIRED" && !message.includes("SESSION_VERSION_EXPIRED")) message = `SESSION_VERSION_EXPIRED: ${message}`;
64513
64547
  } catch {
64514
64548
  }
64515
64549
  emitControl({ kind: "error", message });
@@ -64550,6 +64584,14 @@ function openHttpLineStream(opts) {
64550
64584
  });
64551
64585
  break;
64552
64586
  }
64587
+ if (expectedEngine && control.engine !== expectedEngine) {
64588
+ emitControl({ kind: "error", message: control.engine ? `SESSION_VERSION_EXPIRED: \u5F53\u524D Pod \u662F ${control.engine}\uFF0C\u8FD9\u6761\u4F1A\u8BDD\u9700\u8981 ${expectedEngine}` : "gateway \u7684 ready \u63A7\u5236\u884C\u6CA1\u5E26 engine\uFF0Cgateway \u592A\u8001\uFF0C\u5148\u5347 gateway" });
64589
+ stop = true;
64590
+ ac.abort();
64591
+ void reader.cancel().catch(() => {
64592
+ });
64593
+ break;
64594
+ }
64553
64595
  sessionId = control.session;
64554
64596
  flush();
64555
64597
  }
@@ -64613,8 +64655,8 @@ init_protocol();
64613
64655
  var import_node_fs38 = __toESM(require("fs"), 1);
64614
64656
  function cloudThreadParams(ctx, paths) {
64615
64657
  const { model: _model, ...base } = threadStartParams(ctx);
64616
- const developerInstructions = [ctx.extraSystemPrompt, workDirPrompt(paths.workDir, ctx.personaMode ?? "owner")].filter((x2) => Boolean(x2)).join("\n\n");
64617
- return { ...base, cwd: paths.personaDir, approvalPolicy: "never", sandbox: "danger-full-access", developerInstructions };
64658
+ const developerInstructions = ctx.titleOnly ? "\u53EA\u6839\u636E\u7528\u6237\u6D88\u606F\u8FD4\u56DE\u7B80\u77ED\u6807\u9898\u3002\u4E0D\u8981\u8C03\u7528\u5DE5\u5177\u3001\u8BFB\u53D6\u6587\u4EF6\u3001\u8BBF\u95EE\u7F51\u7EDC\u6216\u4FEE\u6539\u4EFB\u4F55\u5185\u5BB9\u3002" : [ctx.extraSystemPrompt, workDirPrompt(paths.workDir, ctx.personaMode ?? "owner")].filter((x2) => Boolean(x2)).join("\n\n");
64659
+ return { ...base, ...paths.model ? { model: paths.model } : {}, cwd: paths.personaDir, approvalPolicy: "never", sandbox: ctx.titleOnly ? "read-only" : "danger-full-access", developerInstructions };
64618
64660
  }
64619
64661
  function deferredLineStream() {
64620
64662
  let target = null;
@@ -64693,7 +64735,18 @@ function createCloudCodexSession(ctx, sink, deps) {
64693
64735
  sink,
64694
64736
  stream,
64695
64737
  logger: deps.logger,
64696
- threadParams: () => paths.then((p3) => cloudThreadParams(ctx, p3)),
64738
+ threadParams: async (client) => {
64739
+ const p3 = await paths;
64740
+ const params = cloudThreadParams(ctx, p3);
64741
+ if (!ctx.toolSessionId || p3.model) return params;
64742
+ const config = await client.request("config/read", {});
64743
+ if (config?.config?.model) return { ...params, model: config.config.model };
64744
+ const list = await client.request("model/list", {});
64745
+ const selected = list?.data?.find((m3) => m3.isDefault);
64746
+ const current = selected?.id || selected?.model;
64747
+ if (!current) throw new Error("\u65E0\u6CD5\u786E\u5B9A\u5F53\u524D Codex \u9ED8\u8BA4\u6A21\u578B\uFF0C\u62D2\u7EDD\u6CBF\u7528\u65E7\u4F1A\u8BDD\u6A21\u578B");
64748
+ return { ...params, model: current };
64749
+ },
64697
64750
  imageInput: cloudImageInput(deps),
64698
64751
  // skills 在 Pod 里由 persona 目录自带,本机路径对它没意义
64699
64752
  // 「让对端走人」= 断开下行流,gateway 收到就删 Job;信号对 Pod 没有意义
@@ -64701,7 +64754,9 @@ function createCloudCodexSession(ctx, sink, deps) {
64701
64754
  // 云 persona 运行看板(design 2026-09-23-cloud-persona-metrics §3.7):提问那一行带「一轮开始」头,轮末镜像顺带报这一轮的结果
64702
64755
  onTurnStarting: () => channel?.markNextLineTurnStart(),
64703
64756
  // 轮末镜像带上这一轮的结果与首字节(design 2026-09-23-cloud-persona-metrics §3.7)
64704
- onTurnCompleted: (outcome, afterFirstByteMs) => void channel?.mirror({ outcome, ...afterFirstByteMs !== void 0 ? { afterFirstByteMs } : {} }),
64757
+ onTurnCompleted: (outcome, afterFirstByteMs) => {
64758
+ if (!ctx.titleOnly) void channel?.mirror({ outcome, ...afterFirstByteMs !== void 0 ? { afterFirstByteMs } : {} });
64759
+ },
64705
64760
  now: deps.now
64706
64761
  });
64707
64762
  if (!personaId2) {
@@ -64711,12 +64766,12 @@ function createCloudCodexSession(ctx, sink, deps) {
64711
64766
  });
64712
64767
  return driver.session;
64713
64768
  }
64714
- void deps.client.openSession(personaId2).then(
64769
+ void deps.client.openSession(personaId2, { expectedEngine: "codex", ...ctx.titleOnly ? { purpose: "title" } : {} }).then(
64715
64770
  (opened) => {
64716
64771
  channel = opened;
64717
64772
  channel.onControl((c2) => {
64718
64773
  if (c2.kind === "ready") {
64719
- resolvePaths({ personaDir: c2.personaDir, workDir: c2.workDir });
64774
+ resolvePaths({ personaDir: c2.personaDir, workDir: c2.workDir, model: c2.model });
64720
64775
  } else if (c2.kind === "queued") {
64721
64776
  sink.pushEvents([{ kind: "meta-text", text: `\u4E91\u4E0A\u673A\u5668\u6709 ${c2.running} \u4E2A\u4EFB\u52A1\u5728\u8DD1\uFF0C\u7B49\u4E00\u4E2A\u7ED3\u675F\u5C31\u8F6E\u5230\u4F60` }]);
64722
64777
  } else if (c2.kind === "error") {
@@ -65431,7 +65486,9 @@ function createCloudPiSession(ctx, sink, deps) {
65431
65486
  // 云 persona 运行看板(design 2026-09-23-cloud-persona-metrics §3.7):提问那一行带「一轮开始」头,轮末镜像顺带报这一轮的结果
65432
65487
  onTurnStarting: () => channel?.markNextLineTurnStart(),
65433
65488
  // 轮末镜像带上这一轮的结果与首字节(design 2026-09-23-cloud-persona-metrics §3.7)
65434
- onTurnCompleted: (outcome, afterFirstByteMs) => void channel?.mirror({ outcome, ...afterFirstByteMs !== void 0 ? { afterFirstByteMs } : {} }),
65489
+ onTurnCompleted: (outcome, afterFirstByteMs) => {
65490
+ if (!ctx.titleOnly) void channel?.mirror({ outcome, ...afterFirstByteMs !== void 0 ? { afterFirstByteMs } : {} });
65491
+ },
65435
65492
  now: deps.now
65436
65493
  });
65437
65494
  if (!personaId2) {
@@ -65441,7 +65498,7 @@ function createCloudPiSession(ctx, sink, deps) {
65441
65498
  });
65442
65499
  return driver.session;
65443
65500
  }
65444
- void deps.client.openSession(personaId2, { toolSessionId: sessionId, instructions: cloudPiInstructions(ctx) }).then(
65501
+ void deps.client.openSession(personaId2, { toolSessionId: sessionId, instructions: ctx.titleOnly ? "" : cloudPiInstructions(ctx), expectedEngine: "pi", ...ctx.titleOnly ? { purpose: "title" } : {} }).then(
65445
65502
  (opened) => {
65446
65503
  channel = opened;
65447
65504
  channel.onControl((c2) => {
@@ -66313,10 +66370,6 @@ var import_node_fs45 = __toESM(require("fs"), 1);
66313
66370
  var import_node_os18 = __toESM(require("os"), 1);
66314
66371
  var import_node_path49 = __toESM(require("path"), 1);
66315
66372
  var TITLE_GEN_DIR_PREFIX = "clawd-title-";
66316
- var TITLE_MODEL_BY_TOOL = {
66317
- claude: "haiku",
66318
- codex: "gpt-5.4-mini"
66319
- };
66320
66373
  var TITLE_INPUT_MAX_CHARS = 400;
66321
66374
  var DEFAULT_TITLE_GEN_TIMEOUT_MS = 45e3;
66322
66375
  var outFileSeq = 0;
@@ -66337,8 +66390,8 @@ function sanitizeTitle(raw) {
66337
66390
  if (!stripped) return null;
66338
66391
  return stripped.slice(0, 60);
66339
66392
  }
66340
- function buildClaudeTitleArgs(prompt) {
66341
- return ["-p", "--model", TITLE_MODEL_BY_TOOL.claude, prompt];
66393
+ function buildClaudeTitleArgs(prompt, model) {
66394
+ return ["-p", "--no-session-persistence", "--tools", "", ...model ? ["--model", model] : [], prompt];
66342
66395
  }
66343
66396
  var CODEX_TITLE_SCHEMA = JSON.stringify({
66344
66397
  type: "object",
@@ -66358,7 +66411,7 @@ function parseTitleJson(raw) {
66358
66411
  }
66359
66412
  return sanitizeTitle(text);
66360
66413
  }
66361
- function buildCodexTitleArgs(prompt, outFile, schemaFile) {
66414
+ function buildCodexTitleArgs(prompt, outFile, schemaFile, model) {
66362
66415
  return [
66363
66416
  "exec",
66364
66417
  "--ephemeral",
@@ -66367,8 +66420,7 @@ function buildCodexTitleArgs(prompt, outFile, schemaFile) {
66367
66420
  schemaFile,
66368
66421
  "-s",
66369
66422
  "read-only",
66370
- "-m",
66371
- TITLE_MODEL_BY_TOOL.codex,
66423
+ ...model ? ["-m", model] : [],
66372
66424
  "--color",
66373
66425
  "never",
66374
66426
  "-o",
@@ -66376,10 +66428,35 @@ function buildCodexTitleArgs(prompt, outFile, schemaFile) {
66376
66428
  prompt
66377
66429
  ];
66378
66430
  }
66379
- async function generateSessionTitle(tool, firstMessage, deps = {}) {
66380
- if (tool !== "claude" && !isCodexFamily(tool)) return null;
66431
+ function buildPiTitleArgs(prompt, model) {
66432
+ return [
66433
+ "-p",
66434
+ "--mode",
66435
+ "text",
66436
+ "--no-session",
66437
+ "--no-tools",
66438
+ "--no-skills",
66439
+ "--no-context-files",
66440
+ ...model ? ["--model", model] : [],
66441
+ "--",
66442
+ prompt
66443
+ ];
66444
+ }
66445
+ async function generateSessionTitle(file, firstMessage, deps = {}) {
66446
+ const tool = file.tool ?? "claude";
66447
+ if (tool !== "claude" && tool !== "pi" && tool !== "pi-cloud" && !isCodexFamily(tool)) return null;
66381
66448
  const text = firstMessage.trim();
66382
- if (!text) return null;
66449
+ if (!text || deps.signal?.aborted) return null;
66450
+ if (tool === "codex-cloud" || tool === "pi-cloud") {
66451
+ if (!deps.generateCloud) return null;
66452
+ try {
66453
+ const raw = await deps.generateCloud(file, buildClaudeTitlePrompt(text), deps.signal);
66454
+ return raw && !deps.signal?.aborted ? parseTitleJson(raw) : null;
66455
+ } catch (err) {
66456
+ deps.logger?.warn("title-gen cloud failed", { tool, message: err.message });
66457
+ return null;
66458
+ }
66459
+ }
66383
66460
  let workDir;
66384
66461
  try {
66385
66462
  workDir = import_node_fs45.default.mkdtempSync(import_node_path49.default.join(import_node_os18.default.tmpdir(), TITLE_GEN_DIR_PREFIX));
@@ -66394,7 +66471,7 @@ async function generateSessionTitle(tool, firstMessage, deps = {}) {
66394
66471
  };
66395
66472
  const outFile = import_node_path49.default.join(workDir, `codex-title-${process.pid}-${++outFileSeq}-${Date.now()}.txt`);
66396
66473
  const schemaFile = import_node_path49.default.join(workDir, "title-schema.json");
66397
- if (isCodexFamily(tool)) {
66474
+ if (tool === "codex") {
66398
66475
  try {
66399
66476
  import_node_fs45.default.writeFileSync(schemaFile, CODEX_TITLE_SCHEMA);
66400
66477
  } catch {
@@ -66402,14 +66479,16 @@ async function generateSessionTitle(tool, firstMessage, deps = {}) {
66402
66479
  return null;
66403
66480
  }
66404
66481
  }
66405
- const cmd = tool === "claude" ? process.env.CLAUDE_BIN ?? "claude" : process.env.CODEX_BIN ?? "codex";
66406
- const args = tool === "claude" ? buildClaudeTitleArgs(buildClaudeTitlePrompt(text)) : buildCodexTitleArgs(buildTitlePrompt(text), outFile, schemaFile);
66482
+ const cmd = tool === "claude" ? process.env.CLAUDE_BIN ?? "claude" : tool === "pi" ? process.env.PI_BIN ?? "pi" : process.env.CODEX_BIN ?? "codex";
66483
+ const chosenModel = file.resolvedModel || file.model;
66484
+ const model = tool === "codex" ? sanitizeCodexModel(chosenModel) : chosenModel;
66485
+ const args = tool === "claude" ? buildClaudeTitleArgs(buildClaudeTitlePrompt(text), model) : tool === "pi" ? buildPiTitleArgs(buildClaudeTitlePrompt(text), model) : buildCodexTitleArgs(buildTitlePrompt(text), outFile, schemaFile, model);
66407
66486
  const doSpawn = deps.spawnOverride ?? ((c2, a2, o2) => (0, import_node_child_process13.spawn)(c2, a2, { ...o2, stdio: ["ignore", "pipe", "pipe"] }));
66408
66487
  const timeoutMs = deps.timeoutMs ?? DEFAULT_TITLE_GEN_TIMEOUT_MS;
66409
66488
  return await new Promise((resolve9) => {
66410
66489
  let proc;
66411
66490
  try {
66412
- proc = doSpawn(cmd, args, { cwd: workDir });
66491
+ proc = doSpawn(cmd, args, { cwd: model ? workDir : file.cwd });
66413
66492
  } catch (err) {
66414
66493
  deps.logger?.warn("title-gen spawn failed", { tool, message: err.message });
66415
66494
  cleanupWorkDir();
@@ -66421,10 +66500,13 @@ async function generateSessionTitle(tool, firstMessage, deps = {}) {
66421
66500
  if (settled) return;
66422
66501
  settled = true;
66423
66502
  clearTimeout(timer);
66503
+ deps.signal?.removeEventListener("abort", onAbort);
66424
66504
  if (proc.exitCode === null && proc.signalCode === null) proc.kill("SIGKILL");
66425
66505
  cleanupWorkDir();
66426
66506
  resolve9(title);
66427
66507
  };
66508
+ const onAbort = () => finish(null);
66509
+ deps.signal?.addEventListener("abort", onAbort, { once: true });
66428
66510
  const timer = setTimeout(() => {
66429
66511
  deps.logger?.warn("title-gen timed out", { tool, timeoutMs });
66430
66512
  finish(null);
@@ -66443,7 +66525,7 @@ async function generateSessionTitle(tool, firstMessage, deps = {}) {
66443
66525
  deps.logger?.warn("title-gen exited non-zero", { tool, code });
66444
66526
  return finish(null);
66445
66527
  }
66446
- if (tool === "claude") return finish(parseTitleJson(stdout));
66528
+ if (tool === "claude" || tool === "pi") return finish(parseTitleJson(stdout));
66447
66529
  let fileText = "";
66448
66530
  try {
66449
66531
  fileText = import_node_fs45.default.readFileSync(outFile, "utf8");
@@ -66455,6 +66537,57 @@ async function generateSessionTitle(tool, firstMessage, deps = {}) {
66455
66537
  });
66456
66538
  }
66457
66539
 
66540
+ // src/tools/cloud-title.ts
66541
+ function generateCloudTitle(file, prompt, signal, getAdapter2, timeoutMs = 45e3) {
66542
+ if (signal?.aborted) return Promise.resolve(null);
66543
+ return new Promise((resolve9) => {
66544
+ let agent;
66545
+ let settled = false;
66546
+ let finalText = "";
66547
+ let partialText = "";
66548
+ let failed = false;
66549
+ const finish = (raw) => {
66550
+ if (settled) return;
66551
+ settled = true;
66552
+ clearTimeout(timer);
66553
+ signal?.removeEventListener("abort", onAbort);
66554
+ try {
66555
+ void Promise.resolve(agent?.stop("SIGKILL")).catch(() => {
66556
+ });
66557
+ } catch {
66558
+ }
66559
+ resolve9(raw);
66560
+ };
66561
+ const onAbort = () => finish(null);
66562
+ const timer = setTimeout(() => finish(null), timeoutMs);
66563
+ signal?.addEventListener("abort", onAbort, { once: true });
66564
+ try {
66565
+ const adapter = getAdapter2(file.tool);
66566
+ if (!adapter.createSession) return finish(null);
66567
+ agent = adapter.createSession({ cwd: file.cwd, env: {}, titleOnly: true }, {
66568
+ pushEvents(events) {
66569
+ for (const event of events) {
66570
+ if (event.kind === "text") {
66571
+ if (event.partialId) partialText += event.text;
66572
+ else finalText = event.text;
66573
+ } else if (event.kind === "error" || event.kind === "tool_call" || event.kind === "permission_request") {
66574
+ failed = true;
66575
+ finish(null);
66576
+ } else if (event.kind === "turn_end") {
66577
+ finish(failed ? null : finalText || partialText || null);
66578
+ }
66579
+ }
66580
+ },
66581
+ onExit: () => finish(null),
66582
+ onError: () => finish(null)
66583
+ });
66584
+ agent.startTurn(prompt);
66585
+ } catch {
66586
+ finish(null);
66587
+ }
66588
+ });
66589
+ }
66590
+
66458
66591
  // ../node_modules/.pnpm/ws@8.20.0/node_modules/ws/wrapper.mjs
66459
66592
  var import_stream = __toESM(require_stream(), 1);
66460
66593
  var import_extension2 = __toESM(require_extension(), 1);
@@ -72959,7 +73092,7 @@ function computeMethodAccess(args) {
72959
73092
  }
72960
73093
 
72961
73094
  // src/version.ts
72962
- var version = "0.2.599".length > 0 ? "0.2.599" : "dev";
73095
+ var version = "0.2.601".length > 0 ? "0.2.601" : "dev";
72963
73096
 
72964
73097
  // src/cli-probe/probe.ts
72965
73098
  var fs83 = __toESM(require("fs"), 1);
@@ -75912,9 +76045,13 @@ async function startDaemon(config) {
75912
76045
  // per-persona 凭据注入(M1b Step 5):runner 在解释 spawn effect 时按 ctx.personaId 调,
75913
76046
  // 产出拼进 ctx.mcpConfigs。**不在 reducer 里**——渲染要读写 fs,那边是纯函数。
75914
76047
  resolvePersonaMcpConfig,
75915
- // 会话标题自产(首条 send 并行 fire,claude→haiku / codex→gpt-5.4-mini)。
76048
+ // 会话标题自产(首条 send 并行 fire,沿用会话引擎和模型)。
75916
76049
  // 不注入 = 功能关(manager 测试默认关闭防烧真调用),这里是唯一的生产装配点。
75917
- generateTitle: (tool, firstMessage) => generateSessionTitle(tool, firstMessage, { logger }),
76050
+ generateTitle: (file, firstMessage, signal) => generateSessionTitle(file, firstMessage, {
76051
+ logger,
76052
+ signal,
76053
+ generateCloud: (titleFile, prompt, titleSignal) => generateCloudTitle(titleFile, prompt, titleSignal, getAdapter)
76054
+ }),
75918
76055
  broadcastFrame: (frame, target) => {
75919
76056
  if (target === "all") {
75920
76057
  transport?.broadcastAll(frame);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clawos-dev/clawd",
3
- "version": "0.2.599",
3
+ "version": "0.2.601",
4
4
  "description": "Standalone clawd daemon — Claude Code (and future Codex) session server over WebSocket",
5
5
  "type": "module",
6
6
  "license": "MIT",