@co0ontty/wand 4.80.0 → 4.80.1-beta.g1fbbefb

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 (51) hide show
  1. package/dist/ai-team-availability.js +0 -1
  2. package/dist/ai-team-live.d.ts +3 -1
  3. package/dist/ai-team-live.js +31 -2
  4. package/dist/ai-team-runner.d.ts +2 -1
  5. package/dist/ai-team-runner.js +17 -7
  6. package/dist/ai-team-types.d.ts +8 -0
  7. package/dist/ai-team-types.js +4 -0
  8. package/dist/build-info.json +4 -4
  9. package/dist/claude-pty-bridge.js +16 -1
  10. package/dist/cli.js +0 -9
  11. package/dist/config.d.ts +2 -10
  12. package/dist/config.js +1 -73
  13. package/dist/git-quick-commit.d.ts +3 -6
  14. package/dist/git-quick-commit.js +38 -75
  15. package/dist/language-prompt.d.ts +1 -1
  16. package/dist/language-prompt.js +1 -1
  17. package/dist/native-session-title.d.ts +17 -0
  18. package/dist/native-session-title.js +257 -0
  19. package/dist/process-manager.d.ts +3 -0
  20. package/dist/process-manager.js +37 -6
  21. package/dist/server-session-routes.js +4 -29
  22. package/dist/server-settings-routes.js +2 -177
  23. package/dist/server-task-routes.js +1 -1
  24. package/dist/server-workspace-routes.js +1 -1
  25. package/dist/server.js +1 -13
  26. package/dist/session-ai-context.d.ts +3 -7
  27. package/dist/session-ai-context.js +0 -25
  28. package/dist/session-topic.d.ts +19 -0
  29. package/dist/session-topic.js +53 -3
  30. package/dist/session-transport.js +0 -1
  31. package/dist/silicon-employee-draft.d.ts +1 -1
  32. package/dist/storage.js +12 -18
  33. package/dist/structured-claude-adapter.d.ts +0 -6
  34. package/dist/structured-claude-adapter.js +1 -5
  35. package/dist/structured-provider-common.d.ts +8 -4
  36. package/dist/structured-provider-common.js +14 -15
  37. package/dist/structured-session-manager.d.ts +4 -40
  38. package/dist/structured-session-manager.js +53 -773
  39. package/dist/types.d.ts +12 -27
  40. package/dist/web-ui/content/ai-teams.js +9 -9
  41. package/dist/web-ui/content/scripts.js +70 -70
  42. package/dist/web-ui/content/styles.css +1 -1
  43. package/dist/web-ui/embedded-assets.js +3 -3
  44. package/dist/ws-broadcast.js +19 -7
  45. package/package.json +1 -2
  46. package/dist/claude-sdk-runner.d.ts +0 -59
  47. package/dist/claude-sdk-runner.js +0 -188
  48. package/dist/claude-skills.d.ts +0 -6
  49. package/dist/claude-skills.js +0 -53
  50. package/dist/system-ai.d.ts +0 -27
  51. package/dist/system-ai.js +0 -424
@@ -1,8 +1,7 @@
1
1
  import { existsSync } from "node:fs";
2
2
  import { spawn } from "node:child_process";
3
- import { ClaudeRunError, runClaudePrint } from "./claude-sdk-runner.js";
4
- import { callSystemAiTextWithFallback } from "./system-ai.js";
5
3
  import { buildChildEnv } from "./env-utils.js";
4
+ import { buildLanguageDirective } from "./language-prompt.js";
6
5
  import { isSessionProvider, providerCliInstalled } from "./session-provider.js";
7
6
  import { runGitAsync as runGitAsyncBase, runGitRawAsync as runGitRawAsyncBase, getGitErrorMessage, } from "./git-utils.js";
8
7
  import { composeSystemFallback, systemPromptFlag, thinkingEffortToClaudeCliEffort, thinkingEffortToCodexReasoningEffort, thinkingEffortToGrokEffort, thinkingEffortToOpenCodeVariant, thinkingEffortToQoderEffort, thinkingEffortToPiLevel, } from "./structured-provider-common.js";
@@ -17,10 +16,6 @@ const MAX_FILE_ENTRIES = 200;
17
16
  // 30s 在 API 抖动时不够用,放宽到 60s。
18
17
  const CLAUDE_MESSAGE_TIMEOUT_MS = 60_000;
19
18
  const CODEX_MESSAGE_TIMEOUT_MS = 60_000;
20
- // API mode can contain several profiles. Keep each probe bounded so the whole
21
- // chain can still reach the current-session CLI before native clients' 180s
22
- // request deadline.
23
- const DIRECT_API_PROFILE_TIMEOUT_MS = 20_000;
24
19
  const QUICK_COMMIT_CLI_TIMEOUT_MS = 120_000;
25
20
  const MAX_DIFF_FOR_AI = 100_000;
26
21
  // 迭代提示词模式下只给文件清单做核对,不需要完整 diff:单次上限比 diff 小一个量级。
@@ -283,31 +278,37 @@ export class QuickCommitError extends Error {
283
278
  }
284
279
  }
285
280
  // ── AI commit message generation ──
281
+ /**
282
+ * 一次性 Claude 文本生成(commit message / tag / 提示词优化…)。
283
+ *
284
+ * 直接 spawn 本机 `claude -p`,与 PTY / 结构化会话同源:认证、模型、`~/.claude`
285
+ * 配置完全一致。对齐原来 SDK 时代的行为:
286
+ * - `--tools ""` 关掉全部工具:纯文本生成不需要工具,也避免一次性短调用顺手读文件;
287
+ * - `--no-session-persistence`:不污染 `~/.claude/projects/` 的会话历史;
288
+ * - `--strict-mcp-config`(不给 --mcp-config):跳过用户配置的 MCP,省启动时间与 token。
289
+ * 语言指令与任务规则都走 `--append-system-prompt`,任务自己的输出格式排在后面。
290
+ */
286
291
  async function callClaudeText(request, cwd, language, opts) {
287
- try {
288
- const effort = thinkingEffortToClaudeCliEffort(opts.thinkingEffort ?? "off");
289
- return await runClaudePrint(request.prompt, {
290
- cwd,
291
- timeoutMs: CLAUDE_MESSAGE_TIMEOUT_MS,
292
- language,
293
- systemInstructions: request.system,
294
- model: opts.model ?? undefined,
295
- ...(effort ? { effort } : {}),
296
- });
297
- }
298
- catch (error) {
299
- if (error instanceof ClaudeRunError) {
300
- // 把通用 ClaudeRunError 翻译成 quick-commit 自己的错误码 + 中文话术。
301
- if (error.code === "CLAUDE_TIMEOUT") {
302
- throw new QuickCommitError("Claude 生成超时,请手动填写 commit message。", "CLAUDE_TIMEOUT");
303
- }
304
- if (error.code === "CLAUDE_EMPTY_RESULT") {
305
- throw new QuickCommitError("Claude 返回了空的 commit message。", "EMPTY_AI_MESSAGE");
306
- }
307
- throw new QuickCommitError(error.message, error.code);
308
- }
309
- throw error;
310
- }
292
+ const args = ["-p", "--output-format", "text", "--tools", "", "--no-session-persistence", "--strict-mcp-config"];
293
+ const languageDirective = language ? buildLanguageDirective(language) : "";
294
+ const systemPrompt = [languageDirective, (request.system ?? "").trim()].filter(Boolean).join("\n\n");
295
+ if (systemPrompt)
296
+ args.push("--append-system-prompt", systemPrompt);
297
+ const model = opts.model?.trim();
298
+ if (model && model !== "default")
299
+ args.push("--model", model);
300
+ const effort = thinkingEffortToClaudeCliEffort(opts.thinkingEffort ?? "off");
301
+ if (effort)
302
+ args.push("--effort", effort);
303
+ const stdout = await runCliText("claude", args, request.prompt, {
304
+ cwd,
305
+ timeoutMs: CLAUDE_MESSAGE_TIMEOUT_MS,
306
+ inheritEnv: opts.inheritEnv,
307
+ });
308
+ const text = stdout.trim();
309
+ if (!text)
310
+ throw new QuickCommitError("Claude 返回了空的 commit message。", "EMPTY_AI_MESSAGE");
311
+ return text;
311
312
  }
312
313
  function stripFences(raw) {
313
314
  return raw.trim().replace(/^```(?:json)?\s*/i, "").replace(/\s*```$/i, "").trim();
@@ -657,17 +658,6 @@ async function callCliAiText(request, cwd, language, opts) {
657
658
  return callGeminiText(request, cwd, opts);
658
659
  return callClaudeText(request, cwd, language, opts);
659
660
  }
660
- async function callDirectApiText(request, systemAi) {
661
- const text = await callSystemAiTextWithFallback(request, systemAi, DIRECT_API_PROFILE_TIMEOUT_MS);
662
- if (!text.trim()) {
663
- throw new QuickCommitError("直连 API 返回了空结果。", "EMPTY_AI_MESSAGE");
664
- }
665
- return text;
666
- }
667
- function aiFallbackFailed(primary, primaryError, fallbackError) {
668
- const fallback = primary === "直连 API" ? "CLI" : "直连 API";
669
- return new QuickCommitError(`${primary} 与 ${fallback} 均失败:${getGitErrorMessage(primaryError)};${getGitErrorMessage(fallbackError)}`, "AI_FALLBACK_FAILED");
670
- }
671
661
  /**
672
662
  * 多候选降级时给整条链的时间上限。原生客户端的请求宽限约 180s,
673
663
  * 候选再多也不能把整个请求拖死;单次仍受各自 CLI 超时保护。
@@ -729,28 +719,11 @@ async function callCliCandidates(request, cwd, language, opts) {
729
719
  throw new QuickCommitError(`所有 CLI 候选均失败:${errors.join(";")}`, "AI_FALLBACK_FAILED");
730
720
  }
731
721
  /**
732
- * Run a lightweight AI request through the selected Commit source. API mode
733
- * exhausts its profile chain and then falls back once to the system employee's
734
- * CLI chain. CLI mode walks that chain in order.
722
+ * Run a lightweight AI request through the system employee's CLI chain.
723
+ * 所有 Wand 自有文本调用都走本机已安装的 CLI,没有任何直连 API 分支。
735
724
  */
736
725
  export async function callConfiguredAiText(request, cwd, language, opts) {
737
- const effective = withOpsPersona(request, opts.opsPersona);
738
- if (opts.systemAi?.enabled) {
739
- try {
740
- return await callDirectApiText(effective, opts.systemAi);
741
- }
742
- catch (apiError) {
743
- try {
744
- // The user selected the direct API first. If it is unavailable or
745
- // empty, retry this exact request through their selected CLI chain.
746
- return await callCliCandidates(effective, cwd, language, opts);
747
- }
748
- catch (cliError) {
749
- throw aiFallbackFailed("直连 API", apiError, cliError);
750
- }
751
- }
752
- }
753
- return callCliCandidates(effective, cwd, language, opts);
726
+ return callCliCandidates(withOpsPersona(request, opts.opsPersona), cwd, language, opts);
754
727
  }
755
728
  /** Read the unstaged + staged tree without touching the index. */
756
729
  async function collectWorkingTreeDiff(cwd) {
@@ -1037,13 +1010,7 @@ export async function runTagHead(opts) {
1037
1010
  catch {
1038
1011
  headSubject = "";
1039
1012
  }
1040
- tagName = await generateTagAfterCommit(cwd, language, headSubject || "", {
1041
- provider: opts.provider,
1042
- model: opts.model,
1043
- thinkingEffort: opts.thinkingEffort,
1044
- inheritEnv: opts.inheritEnv,
1045
- systemAi: opts.systemAi,
1046
- });
1013
+ tagName = await generateTagAfterCommit(cwd, language, headSubject || "", opts);
1047
1014
  }
1048
1015
  if (!tagName) {
1049
1016
  throw new QuickCommitError("请填写 tag 名称,或开启 AI 生成。", "EMPTY_TAG");
@@ -1507,13 +1474,9 @@ export async function runQuickCommitWithFallback(opts) {
1507
1474
  }
1508
1475
  export async function runQuickCommit(opts) {
1509
1476
  const { cwd, language, autoMessage, customMessage, tag, autoTag, push, submodule } = opts;
1510
- const ai = {
1511
- provider: opts.provider,
1512
- model: opts.model,
1513
- thinkingEffort: opts.thinkingEffort,
1514
- inheritEnv: opts.inheritEnv,
1515
- systemAi: opts.systemAi,
1516
- };
1477
+ // QuickCommitOptions 本身带全套 AI 字段(模型、系统运维人设、候选链):
1478
+ // 逐字段抄一份会静默丢掉新增字段,直接复用同一个对象。
1479
+ const ai = opts;
1517
1480
  await assertGitWorkTreeAsync(cwd);
1518
1481
  // 先 add 一次让我们能在 collectStagedDiff 看到完整改动(包含 submodule 指针),
1519
1482
  // AI 生成 message 时也基于这个 staged diff。
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * 强语言指令生成器:供 PTY runner、structured-session-manager(CLI + SDK 两个分支)、
3
- * 一次性 SDK 调用(claude-sdk-runner.ts)共用。
3
+ * 会话与一次性 CLI 调用(git-quick-commit 等)共用。
4
4
  *
5
5
  * 原本各 runner 散落写 "请使用中文回复" 这种软指令,Claude 写技术内容时还是会条件
6
6
  * 反射切英文("Now let me ..."、"OK, ..."),用户设置中文也照样夹英文。
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * 强语言指令生成器:供 PTY runner、structured-session-manager(CLI + SDK 两个分支)、
3
- * 一次性 SDK 调用(claude-sdk-runner.ts)共用。
3
+ * 会话与一次性 CLI 调用(git-quick-commit 等)共用。
4
4
  *
5
5
  * 原本各 runner 散落写 "请使用中文回复" 这种软指令,Claude 写技术内容时还是会条件
6
6
  * 反射切英文("Now let me ..."、"OK, ..."),用户设置中文也照样夹英文。
@@ -0,0 +1,17 @@
1
+ import type { SessionProvider } from "./types.js";
2
+ /** 各 provider 状态根目录,测试可整体替换。 */
3
+ export interface NativeTitleHomes {
4
+ codexHome?: string;
5
+ qoderProjectsDirs?: string[];
6
+ openCodeDatabasePath?: string;
7
+ grokSessionsDir?: string;
8
+ }
9
+ /**
10
+ * provider 自己在运行期写下的会话标题:
11
+ * codex `session_index.jsonl` 的 thread_name、qoder 转录里的 ai-title、
12
+ * OpenCode SQLite 的 session.title、grok `summary.json` 的 generated_title。
13
+ * Claude / Pi / Gemini 没有这个入口,返回空串让调用方回退系统硅基员工。
14
+ */
15
+ export declare function readNativeSessionTitle(provider: SessionProvider, providerSessionId: string | null | undefined, cwd: string, homes?: NativeTitleHomes): Promise<string>;
16
+ /** 只用于测试与降级:清掉按文件指纹缓存的原生标题。 */
17
+ export declare function clearNativeSessionTitleCache(): void;
@@ -0,0 +1,257 @@
1
+ import { existsSync, openSync, readSync, closeSync, readdirSync, statSync } from "node:fs";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+ import { DatabaseSync } from "node:sqlite";
5
+ import { isPlausibleTaskTitle } from "./task-title.js";
6
+ const SESSION_ID_PATTERN = /^[a-z0-9][a-z0-9._:-]{0,199}$/i;
7
+ const UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
8
+ const READ_CAP_BYTES = 2 * 1024 * 1024;
9
+ const GROK_WALK_LIMIT = 4_000;
10
+ /**
11
+ * provider 自己在运行期写下的会话标题:
12
+ * codex `session_index.jsonl` 的 thread_name、qoder 转录里的 ai-title、
13
+ * OpenCode SQLite 的 session.title、grok `summary.json` 的 generated_title。
14
+ * Claude / Pi / Gemini 没有这个入口,返回空串让调用方回退系统硅基员工。
15
+ */
16
+ export async function readNativeSessionTitle(provider, providerSessionId, cwd, homes = {}) {
17
+ const id = providerSessionId?.trim() ?? "";
18
+ if (!id || !SESSION_ID_PATTERN.test(id))
19
+ return "";
20
+ switch (provider) {
21
+ case "codex":
22
+ return readCodexNativeTitle(id, homes.codexHome);
23
+ case "qoder":
24
+ return readQoderNativeTitle(id, homes.qoderProjectsDirs);
25
+ case "opencode":
26
+ return readOpenCodeNativeTitle(id, homes.openCodeDatabasePath);
27
+ case "grok":
28
+ return readGrokNativeTitle(id, cwd, homes.grokSessionsDir);
29
+ default:
30
+ return "";
31
+ }
32
+ }
33
+ function acceptNativeTitle(value) {
34
+ if (typeof value !== "string")
35
+ return "";
36
+ const title = value.replace(/\s+/g, " ").trim();
37
+ return title && isPlausibleTaskTitle(title) ? title : "";
38
+ }
39
+ function readTail(filePath, maxBytes) {
40
+ let fd = null;
41
+ try {
42
+ const stats = statSync(filePath);
43
+ const length = Math.min(maxBytes, Math.max(0, stats.size));
44
+ const buffer = Buffer.alloc(length);
45
+ fd = openSync(filePath, "r");
46
+ const readAt = stats.size - length;
47
+ const bytesRead = readSync(fd, buffer, 0, length, readAt);
48
+ return buffer.toString("utf8", 0, bytesRead);
49
+ }
50
+ catch {
51
+ return "";
52
+ }
53
+ finally {
54
+ if (fd !== null) {
55
+ try {
56
+ closeSync(fd);
57
+ }
58
+ catch { /* 只读兜底 */ }
59
+ }
60
+ }
61
+ }
62
+ function readHead(filePath, maxBytes) {
63
+ let fd = null;
64
+ try {
65
+ const stats = statSync(filePath);
66
+ fd = openSync(filePath, "r");
67
+ const buffer = Buffer.alloc(Math.min(maxBytes, Math.max(1, stats.size)));
68
+ const bytesRead = readSync(fd, buffer, 0, buffer.length, 0);
69
+ return buffer.toString("utf8", 0, bytesRead);
70
+ }
71
+ catch {
72
+ return "";
73
+ }
74
+ finally {
75
+ if (fd !== null) {
76
+ try {
77
+ closeSync(fd);
78
+ }
79
+ catch { /* 只读兜底 */ }
80
+ }
81
+ }
82
+ }
83
+ const nativeTitleCache = new Map();
84
+ function fingerprint(filePath) {
85
+ try {
86
+ const stats = statSync(filePath);
87
+ return `${stats.mtimeMs}:${stats.size}`;
88
+ }
89
+ catch {
90
+ return "missing";
91
+ }
92
+ }
93
+ /** 同一会话在同一次生成里会被反复问到(标题、快捷提交、resume),按文件指纹缓存。 */
94
+ function cachedTitle(cacheKey, sourcePath, read) {
95
+ const key = `${cacheKey}|${sourcePath}`;
96
+ const stamp = fingerprint(sourcePath);
97
+ const hit = nativeTitleCache.get(key);
98
+ if (hit && hit.key === stamp)
99
+ return hit.title;
100
+ const title = read();
101
+ if (nativeTitleCache.size > 512)
102
+ nativeTitleCache.clear();
103
+ nativeTitleCache.set(key, { key: stamp, title });
104
+ return title;
105
+ }
106
+ function readCodexNativeTitle(threadId, codexHome) {
107
+ if (!UUID_PATTERN.test(threadId))
108
+ return "";
109
+ const index = path.join(codexHome ?? path.join(os.homedir(), ".codex"), "session_index.jsonl");
110
+ if (!existsSync(index))
111
+ return "";
112
+ return cachedTitle(`codex:${threadId}`, index, () => {
113
+ let title = "";
114
+ for (const line of readTail(index, READ_CAP_BYTES).split("\n")) {
115
+ if (!line.includes(threadId))
116
+ continue;
117
+ try {
118
+ const parsed = JSON.parse(line);
119
+ if (parsed.id === threadId)
120
+ title = acceptNativeTitle(parsed.thread_name);
121
+ }
122
+ catch {
123
+ continue;
124
+ }
125
+ }
126
+ return title;
127
+ });
128
+ }
129
+ function readQoderNativeTitle(sessionId, projectsDirs) {
130
+ const dirs = projectsDirs ?? [
131
+ path.join(os.homedir(), ".qoder", "projects"),
132
+ path.join(os.homedir(), ".qoder-cn", "projects"),
133
+ ];
134
+ for (const projectsDir of dirs) {
135
+ let projects;
136
+ try {
137
+ projects = readdirSync(projectsDir);
138
+ }
139
+ catch {
140
+ continue;
141
+ }
142
+ for (const project of projects) {
143
+ const transcript = path.join(projectsDir, project, `${sessionId}.jsonl`);
144
+ if (!existsSync(transcript))
145
+ continue;
146
+ const title = cachedTitle(`qoder:${sessionId}`, transcript, () => {
147
+ let found = "";
148
+ for (const line of readHead(transcript, READ_CAP_BYTES).split("\n")) {
149
+ if (!line.includes('"ai-title"'))
150
+ continue;
151
+ try {
152
+ const parsed = JSON.parse(line);
153
+ if (parsed.type === "ai-title")
154
+ found = acceptNativeTitle(parsed.aiTitle);
155
+ }
156
+ catch {
157
+ continue;
158
+ }
159
+ }
160
+ return found;
161
+ });
162
+ if (title)
163
+ return title;
164
+ }
165
+ }
166
+ return "";
167
+ }
168
+ /** OpenCode 未命名会话的占位标题不能当原生标题用。 */
169
+ const OPENCODE_PLACEHOLDER_TITLE = /^(?:new session|untitled session)\b/i;
170
+ function readOpenCodeNativeTitle(sessionId, databasePath) {
171
+ const dbPath = databasePath ?? path.join(process.env.XDG_DATA_HOME?.trim() || path.join(os.homedir(), ".local", "share"), "opencode", "opencode.db");
172
+ if (!existsSync(dbPath))
173
+ return "";
174
+ return cachedTitle(`opencode:${sessionId}`, dbPath, () => {
175
+ let database = null;
176
+ try {
177
+ database = new DatabaseSync(dbPath, { readOnly: true });
178
+ const row = database.prepare("SELECT title FROM session WHERE id = ?").get(sessionId);
179
+ const title = acceptNativeTitle(row?.title);
180
+ return title && !OPENCODE_PLACEHOLDER_TITLE.test(title) ? title : "";
181
+ }
182
+ catch {
183
+ return "";
184
+ }
185
+ finally {
186
+ try {
187
+ database?.close();
188
+ }
189
+ catch { /* 只读探测 */ }
190
+ }
191
+ });
192
+ }
193
+ function grokSessionDirs(grokSessionsDir, cwd) {
194
+ const encoded = encodeURIComponent(cwd);
195
+ const primary = path.join(grokSessionsDir, encoded);
196
+ if (existsSync(primary))
197
+ return [primary];
198
+ const visited = [];
199
+ let queue = [{ dir: grokSessionsDir, depth: 0 }];
200
+ while (queue.length > 0 && visited.length < GROK_WALK_LIMIT) {
201
+ const next = [];
202
+ for (const item of queue) {
203
+ let entries;
204
+ try {
205
+ entries = readdirSync(item.dir);
206
+ }
207
+ catch {
208
+ continue;
209
+ }
210
+ for (const entry of entries) {
211
+ const full = path.join(item.dir, entry);
212
+ let stats;
213
+ try {
214
+ stats = statSync(full);
215
+ }
216
+ catch {
217
+ continue;
218
+ }
219
+ if (!stats.isDirectory())
220
+ continue;
221
+ visited.push(full);
222
+ if (item.depth < 2)
223
+ next.push({ dir: full, depth: item.depth + 1 });
224
+ }
225
+ }
226
+ queue = next;
227
+ }
228
+ return visited;
229
+ }
230
+ function readGrokNativeTitle(sessionId, cwd, grokSessionsDir) {
231
+ if (!UUID_PATTERN.test(sessionId))
232
+ return "";
233
+ const root = grokSessionsDir ?? path.join(os.homedir(), ".grok", "sessions");
234
+ if (!existsSync(root))
235
+ return "";
236
+ for (const dir of grokSessionDirs(root, cwd)) {
237
+ const summary = path.join(dir, sessionId, "summary.json");
238
+ if (!existsSync(summary))
239
+ continue;
240
+ const title = cachedTitle(`grok:${sessionId}`, summary, () => {
241
+ try {
242
+ const parsed = JSON.parse(readHead(summary, 65_536));
243
+ return acceptNativeTitle(parsed.generated_title);
244
+ }
245
+ catch {
246
+ return "";
247
+ }
248
+ });
249
+ if (title)
250
+ return title;
251
+ }
252
+ return "";
253
+ }
254
+ /** 只用于测试与降级:清掉按文件指纹缓存的原生标题。 */
255
+ export function clearNativeSessionTitleCache() {
256
+ nativeTitleCache.clear();
257
+ }
@@ -37,6 +37,7 @@ export declare class ProcessManager extends EventEmitter {
37
37
  /** 启动时被识别为孤儿 PTY 并标记为 exited 的旧会话数(旧服务器进程已死) */
38
38
  private orphanRecoveredCount;
39
39
  private readonly topicCoordinator;
40
+ private readonly nativeTitles;
40
41
  private disposed;
41
42
  private readonly terminalHost;
42
43
  constructor(config: WandConfig, storage: WandStorage, configDir?: string, terminalHost?: TerminalHost);
@@ -145,6 +146,8 @@ export declare class ProcessManager extends EventEmitter {
145
146
  */
146
147
  setWorktreeMergeState(id: string, status: SessionSnapshot["worktreeMergeStatus"], info: SessionSnapshot["worktreeMergeInfo"]): SessionSnapshot | null;
147
148
  private maybeGenerateSessionTopic;
149
+ /** 原生标题优先于模型标题:只受「不能是任务名/目录名」约束,命中且变化时才落库。 */
150
+ private applyNativeSessionTitle;
148
151
  private defaultAutonomyPolicy;
149
152
  resolveEscalation(id: string, requestId: string, resolution?: PermissionResolution): SessionSnapshot;
150
153
  approvePermission(id: string): SessionSnapshot;
@@ -14,11 +14,12 @@ import { buildLanguageDirective, buildManagedAutonomyDirective } from "./languag
14
14
  import { prepareSessionWorktree } from "./git-worktree.js";
15
15
  import { getProviderCommandSessionId, getProviderResumeCommandSessionId } from "./resume-policy.js";
16
16
  import { commandWithSystemPrompt, composeSystemFallback, normalizeThinkingEffort, systemPromptFlag, thinkingEffortToClaudeCliEffort, thinkingEffortToClaudeSlashEffort, thinkingEffortToCodexReasoningEffort, thinkingEffortToGrokEffort, thinkingEffortToPiLevel, thinkingEffortToQoderEffort } from "./structured-provider-common.js";
17
- import { consumePtyInputForTopic, createPtyTopicLineBuffer, provisionalSessionTopic, SessionTopicCoordinator, sessionTopicBlocklistForSnapshot, shouldAcceptGeneratedSessionTitle, shouldGenerateSessionTopicFromInput, } from "./session-topic.js";
17
+ import { consumePtyInputForTopic, createPtyTopicLineBuffer, provisionalSessionTopic, SessionNativeTitleTracker, SessionTopicCoordinator, sessionTopicBlocklistForSnapshot, shouldAcceptGeneratedSessionTitle, shouldGenerateSessionTopicFromInput, } from "./session-topic.js";
18
+ import { readNativeSessionTitle } from "./native-session-title.js";
18
19
  import { getErrorMessage } from "./error-utils.js";
19
20
  import { recordIterationPrompt } from "./iteration-log.js";
20
21
  import { describePtySpawnFailure } from "./ensure-node-pty-helper.js";
21
- import { resolveSystemAiContext } from "./session-ai-context.js";
22
+ import { resolveSessionProvider, resolveSystemAiContext } from "./session-ai-context.js";
22
23
  import { resolveSessionCwd } from "./session-cwd.js";
23
24
  import { inferProviderFromCommand } from "./session-provider.js";
24
25
  import { PtyTerminalState } from "./pty-terminal-state.js";
@@ -547,6 +548,7 @@ export class ProcessManager extends EventEmitter {
547
548
  /** 启动时被识别为孤儿 PTY 并标记为 exited 的旧会话数(旧服务器进程已死) */
548
549
  orphanRecoveredCount = 0;
549
550
  topicCoordinator = new SessionTopicCoordinator();
551
+ nativeTitles = new SessionNativeTitleTracker();
550
552
  disposed = false;
551
553
  terminalHost;
552
554
  constructor(config, storage, configDir, terminalHost) {
@@ -930,6 +932,7 @@ export class ProcessManager extends EventEmitter {
930
932
  }
931
933
  }
932
934
  this.topicCoordinator.clear();
935
+ this.nativeTitles.clear();
933
936
  this.removeAllListeners("process");
934
937
  this.logger.dispose();
935
938
  this.terminalHost.disconnect();
@@ -2015,8 +2018,11 @@ export class ProcessManager extends EventEmitter {
2015
2018
  // 迭代提示词记录:和会话标题共用同一套「有没有信息量」判断,用户不用多做一步。
2016
2019
  recordIterationPrompt(this.storage, record, prompt, "session");
2017
2020
  const blockedTitles = sessionTopicBlocklistForSnapshot(record, this.storage);
2021
+ // CLI 自己起了名字就归它,模型标题与输入首行都不再覆盖;取不到原生标题才走系统硅基员工。
2022
+ const hasNativeTitle = this.nativeTitles.has(id);
2018
2023
  const provisional = provisionalSessionTopic(prompt, blockedTitles);
2019
2024
  if (provisional
2025
+ && !hasNativeTitle
2020
2026
  && shouldGenerateSessionTopicFromInput(prompt)
2021
2027
  && (record.title !== provisional.title || record.description !== provisional.description)) {
2022
2028
  this.setSessionTopic(id, provisional.title, provisional.description);
@@ -2027,12 +2033,22 @@ export class ProcessManager extends EventEmitter {
2027
2033
  cwd: record.cwd,
2028
2034
  language: this.config.language,
2029
2035
  ai: resolveSystemAiContext(record, this.config, this.storage.getSystemSiliconEmployee()),
2036
+ readNativeTitle: () => readNativeSessionTitle(resolveSessionProvider(record), record.claudeSessionId, record.cwd),
2037
+ hasNativeTitle,
2030
2038
  onGenerating: (generating) => {
2031
2039
  if (!this.disposed)
2032
2040
  this.setSessionTopicGenerating(id, generating);
2033
2041
  },
2034
- onTopic: ({ title, description }) => {
2035
- if (!this.disposed && this.sessions.has(id) && shouldAcceptGeneratedSessionTitle(title, blockedTitles)) {
2042
+ onTopic: ({ title, description, source }) => {
2043
+ if (this.disposed || !this.sessions.has(id))
2044
+ return;
2045
+ if (source === "native") {
2046
+ this.applyNativeSessionTitle(id, title, blockedTitles);
2047
+ return;
2048
+ }
2049
+ if (this.nativeTitles.has(id))
2050
+ return;
2051
+ if (shouldAcceptGeneratedSessionTitle(title, blockedTitles)) {
2036
2052
  this.setSessionTopic(id, title, description);
2037
2053
  }
2038
2054
  },
@@ -2041,6 +2057,16 @@ export class ProcessManager extends EventEmitter {
2041
2057
  },
2042
2058
  });
2043
2059
  }
2060
+ /** 原生标题优先于模型标题:只受「不能是任务名/目录名」约束,命中且变化时才落库。 */
2061
+ applyNativeSessionTitle(id, title, blockedTitles) {
2062
+ this.nativeTitles.record(id, title);
2063
+ const current = this.sessions.get(id);
2064
+ if (!current || !shouldAcceptGeneratedSessionTitle(title, blockedTitles))
2065
+ return;
2066
+ if (current.title === title && current.description === title)
2067
+ return;
2068
+ this.setSessionTopic(id, title, title);
2069
+ }
2044
2070
  defaultAutonomyPolicy(mode) {
2045
2071
  if (mode === "agent" || mode === "agent-max" || mode === "managed" || mode === "native" || mode === "full-access") {
2046
2072
  return "agent";
@@ -2288,8 +2314,10 @@ export class ProcessManager extends EventEmitter {
2288
2314
  case "output.chat": {
2289
2315
  record.output = record.ptyBridge?.getRawOutput() ?? record.output;
2290
2316
  const rawMessages = record.ptyBridge?.getMessages() ?? [];
2291
- const isStreaming = record.status === "running";
2292
2317
  const bridgeData = event.data;
2318
+ const sequenceChanged = record.ptyChatMessageCount !== rawMessages.length;
2319
+ const isStreaming = record.status === "running" && bridgeData?.isResponding === true;
2320
+ record.ptyChatMessageCount = rawMessages.length;
2293
2321
  // 每轮忙碌信号:turn 开始置 true,回复结束清 false。只在翻转时广播,
2294
2322
  // 避免流式期间每个 chunk 都发 status 事件。
2295
2323
  const responding = bridgeData?.isResponding === true;
@@ -2310,7 +2338,10 @@ export class ProcessManager extends EventEmitter {
2310
2338
  if (bridgeData && typeof bridgeData.isResponding === "boolean") {
2311
2339
  data.isResponding = bridgeData.isResponding;
2312
2340
  }
2313
- if (isStreaming && rawMessages.length > 0) {
2341
+ // A new round can append user + assistant at once. Last-only payloads
2342
+ // cannot establish that sequence for any of the existing clients.
2343
+ // Idle is a turn boundary even when the shell process remains running.
2344
+ if (isStreaming && !sequenceChanged && rawMessages.length > 0) {
2314
2345
  data.incremental = true;
2315
2346
  const lastTurn = rawMessages[rawMessages.length - 1];
2316
2347
  const truncatedLast = truncateMessagesForTransport([lastTurn], this.config.cardDefaults ?? {}, 0);
@@ -5,7 +5,7 @@ import { alignedBlockStart, blockWindowMessagesForTransport, sliceTurnBlocksForT
5
5
  import { toSessionDetailDTO, toSessionListItemDTO } from "./session-transport.js";
6
6
  import { checkSessionWorktreeMergeabilityAsync, cleanupSessionWorktreeAsync, getWorktreeMergeErrorCode, mergeSessionWorktreeAsync, WorktreeMergeError, } from "./git-worktree.js";
7
7
  import { resolveSessionCwd } from "./session-cwd.js";
8
- import { resolveCommitAiContext } from "./session-ai-context.js";
8
+ import { resolveSystemAiContext } from "./session-ai-context.js";
9
9
  import { getGitStatusAsync, QuickCommitError, runQuickCommitWithFallback, runTagHead, runPush, generateCommitMessageOnly, } from "./git-quick-commit.js";
10
10
  import { getErrorMessage } from "./error-utils.js";
11
11
  import { archiveCommitTasks } from "./commit-task-archive.js";
@@ -111,24 +111,6 @@ function getInputErrorResponse(error, sessionId) {
111
111
  },
112
112
  };
113
113
  }
114
- function parseClaudeSdkSkills(value) {
115
- if (!Array.isArray(value)) {
116
- throw new Error("skills 必须是数组。");
117
- }
118
- if (value.length > 50) {
119
- throw new Error("最多选择 50 个 skills。");
120
- }
121
- const names = new Set();
122
- for (const item of value) {
123
- if (typeof item !== "string")
124
- throw new Error("skills 只能包含字符串。");
125
- const name = item.trim();
126
- if (!name || name.length > 128)
127
- throw new Error("skill 名称无效。");
128
- names.add(name);
129
- }
130
- return Array.from(names);
131
- }
132
114
  function getInputDebugMeta(error) {
133
115
  if (error instanceof Error) {
134
116
  return { name: error.name, message: error.message, stack: error.stack };
@@ -630,17 +612,10 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
630
612
  res.status(404).json({ error: "未找到该结构化会话。" });
631
613
  return;
632
614
  }
633
- const hasSkills = Object.prototype.hasOwnProperty.call(req.body ?? {}, "skills");
634
- if (hasSkills && (session.provider !== "claude" || session.runner !== "claude-sdk")) {
635
- res.status(400).json({ error: "skills 仅支持 Claude SDK 结构化会话。" });
636
- return;
637
- }
638
- const skills = hasSkills ? parseClaudeSdkSkills(req.body.skills) : [];
639
615
  const snapshot = await structured.sendMessage(req.params.id, input, {
640
616
  interrupt,
641
617
  preserveQueue,
642
618
  idempotencyKey,
643
- skills,
644
619
  });
645
620
  res.json(sessionResponseDTO(snapshot));
646
621
  }
@@ -887,7 +862,7 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
887
862
  }
888
863
  const body = (req.body ?? {});
889
864
  try {
890
- const ai = resolveCommitAiContext(snapshot, config, undefined, storage.getSystemSiliconEmployee());
865
+ const ai = resolveSystemAiContext(snapshot, config, storage.getSystemSiliconEmployee());
891
866
  // 自动 message + 迭代模式:拿本轮提示词当输入;提交成功后标记已用掉。
892
867
  const context = await iterationCommitInput(storage, snapshot, body);
893
868
  const result = await runQuickCommitWithFallback({
@@ -983,7 +958,7 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
983
958
  }
984
959
  const body = (req.body ?? {});
985
960
  try {
986
- const ai = resolveCommitAiContext(snapshot, config, undefined, storage.getSystemSiliconEmployee());
961
+ const ai = resolveSystemAiContext(snapshot, config, storage.getSystemSiliconEmployee());
987
962
  const context = await iterationCommitInput(storage, snapshot, body);
988
963
  const result = await generateCommitMessageOnly(snapshot.cwd, config.language ?? "", {
989
964
  ...ai,
@@ -1020,7 +995,7 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
1020
995
  }
1021
996
  const body = (req.body ?? {});
1022
997
  try {
1023
- const ai = resolveCommitAiContext(snapshot, config, undefined, storage.getSystemSiliconEmployee());
998
+ const ai = resolveSystemAiContext(snapshot, config, storage.getSystemSiliconEmployee());
1024
999
  const result = await runTagHead({
1025
1000
  cwd: snapshot.cwd,
1026
1001
  language: config.language ?? "",