@co0ontty/wand 4.81.0 → 4.82.0

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 (64) hide show
  1. package/dist/agent-dispatch.js +7 -4
  2. package/dist/ai-team-prompts.d.ts +1 -1
  3. package/dist/ai-team-prompts.js +2 -2
  4. package/dist/ai-team-runner.d.ts +2 -0
  5. package/dist/ai-team-runner.js +60 -7
  6. package/dist/ai-team-types.d.ts +6 -0
  7. package/dist/ai-team-types.js +11 -0
  8. package/dist/build-info.json +3 -3
  9. package/dist/cli.js +11 -0
  10. package/dist/config.js +2 -0
  11. package/dist/default-employee.d.ts +11 -0
  12. package/dist/default-employee.js +33 -0
  13. package/dist/employee-knowledge-cli.d.ts +2 -0
  14. package/dist/employee-knowledge-cli.js +52 -0
  15. package/dist/employee-knowledge-content.d.ts +4 -0
  16. package/dist/employee-knowledge-content.js +23 -0
  17. package/dist/employee-knowledge-types.d.ts +15 -0
  18. package/dist/employee-knowledge-types.js +3 -0
  19. package/dist/employee-knowledge.d.ts +6 -0
  20. package/dist/employee-knowledge.js +84 -0
  21. package/dist/git-quick-commit.d.ts +2 -0
  22. package/dist/git-quick-commit.js +121 -58
  23. package/dist/iteration-log.d.ts +5 -0
  24. package/dist/iteration-log.js +12 -1
  25. package/dist/message-truncator.js +13 -9
  26. package/dist/models.d.ts +4 -0
  27. package/dist/models.js +38 -10
  28. package/dist/pi-model-discovery.d.ts +12 -0
  29. package/dist/pi-model-discovery.js +281 -0
  30. package/dist/process-manager.d.ts +23 -1
  31. package/dist/process-manager.js +68 -1
  32. package/dist/prompt-optimizer.js +10 -11
  33. package/dist/pty-turn-activity.d.ts +27 -0
  34. package/dist/pty-turn-activity.js +31 -0
  35. package/dist/server-employee-routes.js +52 -6
  36. package/dist/server-session-routes.js +11 -5
  37. package/dist/server-task-routes.js +6 -1
  38. package/dist/server-user-memory.d.ts +9 -0
  39. package/dist/server-user-memory.js +96 -0
  40. package/dist/server.js +22 -2
  41. package/dist/session-topic.js +6 -5
  42. package/dist/silicon-employee-draft.js +1 -3
  43. package/dist/storage.d.ts +38 -0
  44. package/dist/storage.js +276 -3
  45. package/dist/structured-claude-adapter.d.ts +1 -1
  46. package/dist/structured-claude-adapter.js +2 -0
  47. package/dist/structured-provider-common.d.ts +2 -2
  48. package/dist/structured-provider-common.js +6 -6
  49. package/dist/structured-session-manager.js +5 -4
  50. package/dist/task-title.js +8 -7
  51. package/dist/team-report-file-link.d.ts +6 -0
  52. package/dist/team-report-file-link.js +54 -0
  53. package/dist/team-report-preview.d.ts +3 -0
  54. package/dist/team-report-preview.js +70 -0
  55. package/dist/types.d.ts +15 -0
  56. package/dist/user-memory-types.d.ts +42 -0
  57. package/dist/user-memory-types.js +6 -0
  58. package/dist/user-memory.d.ts +30 -0
  59. package/dist/user-memory.js +190 -0
  60. package/dist/web-ui/content/ai-teams.js +13 -13
  61. package/dist/web-ui/content/scripts.js +59 -61
  62. package/dist/web-ui/content/styles.css +1 -1
  63. package/dist/web-ui/embedded-assets.js +3 -3
  64. package/package.json +1 -1
@@ -3,6 +3,7 @@ import { spawn } from "node:child_process";
3
3
  import { buildChildEnv } from "./env-utils.js";
4
4
  import { buildLanguageDirective } from "./language-prompt.js";
5
5
  import { isSessionProvider, providerCliInstalled } from "./session-provider.js";
6
+ import { asRecord } from "./structured-content.js";
6
7
  import { runGitAsync as runGitAsyncBase, runGitRawAsync as runGitRawAsyncBase, getGitErrorMessage, } from "./git-utils.js";
7
8
  import { composeSystemFallback, systemPromptFlag, thinkingEffortToClaudeCliEffort, thinkingEffortToCodexReasoningEffort, thinkingEffortToGrokEffort, thinkingEffortToOpenCodeVariant, thinkingEffortToQoderEffort, thinkingEffortToPiLevel, } from "./structured-provider-common.js";
8
9
  /** provider 缺省值:无法识别时回落到 Claude。 */
@@ -277,7 +278,9 @@ export class QuickCommitError extends Error {
277
278
  this.name = "QuickCommitError";
278
279
  }
279
280
  }
280
- // ── AI commit message generation ──
281
+ function cliTextTimeoutMs(defaultMs, opts) {
282
+ return opts.deadline === undefined ? defaultMs : Math.min(defaultMs, Math.max(1, opts.deadline - Date.now()));
283
+ }
281
284
  /**
282
285
  * 一次性 Claude 文本生成(commit message / tag / 提示词优化…)。
283
286
  *
@@ -302,7 +305,7 @@ async function callClaudeText(request, cwd, language, opts) {
302
305
  args.push("--effort", effort);
303
306
  const stdout = await runCliText("claude", args, request.prompt, {
304
307
  cwd,
305
- timeoutMs: CLAUDE_MESSAGE_TIMEOUT_MS,
308
+ timeoutMs: cliTextTimeoutMs(CLAUDE_MESSAGE_TIMEOUT_MS, opts),
306
309
  inheritEnv: opts.inheritEnv,
307
310
  });
308
311
  const text = stdout.trim();
@@ -316,7 +319,72 @@ function stripFences(raw) {
316
319
  function normalizeAiText(raw) {
317
320
  return stripFences(raw).replace(/^["'`]+|["'`]+$/g, "").trim();
318
321
  }
322
+ /** Read explicit protocol failures, not error-looking prose that could be legitimate output. */
323
+ function cliProtocolErrorText(value) {
324
+ if (typeof value === "string")
325
+ return value.trim();
326
+ if (Array.isArray(value))
327
+ return value.map(cliProtocolErrorText).filter(Boolean).join("\n");
328
+ const record = asRecord(value);
329
+ if (!record)
330
+ return "";
331
+ for (const key of ["message", "error", "errors", "data", "result"]) {
332
+ const text = cliProtocolErrorText(record[key]);
333
+ if (text)
334
+ return text;
335
+ }
336
+ return "";
337
+ }
338
+ /** A zero process exit does not imply a successful model response. */
339
+ function assertCliProtocolSucceeded(stdout, provider) {
340
+ let failure = null;
341
+ const failed = (value) => cliProtocolErrorText(value) || `${provider} CLI execution failed`;
342
+ for (const line of stdout.split(/\r?\n/)) {
343
+ let event;
344
+ try {
345
+ event = asRecord(JSON.parse(line));
346
+ }
347
+ catch {
348
+ continue;
349
+ }
350
+ if (!event)
351
+ continue;
352
+ if (provider === "codex") {
353
+ if (event.type === "error" || event.type === "turn.failed")
354
+ failure = failed(event);
355
+ if (event.type === "turn.completed")
356
+ failure = null;
357
+ }
358
+ else if (provider === "qoder" && event.type === "result") {
359
+ failure = event.is_error === true || event.subtype !== "success" ? failed(event) : null;
360
+ }
361
+ else if (provider === "pi") {
362
+ const messages = event.type === "agent_end" && Array.isArray(event.messages)
363
+ ? event.messages
364
+ : event.type === "message_end" || event.type === "turn_end" ? [event.message] : [];
365
+ for (const raw of messages) {
366
+ const message = asRecord(raw);
367
+ if (message?.role !== "assistant")
368
+ continue;
369
+ failure = message.stopReason === "error" || message.stopReason === "aborted"
370
+ ? failed(message.errorMessage) : null;
371
+ }
372
+ }
373
+ else if (provider === "gemini") {
374
+ if (event.type === "error" && event.severity === "error")
375
+ failure = failed(event);
376
+ if (event.type === "result")
377
+ failure = event.status === "error" ? failed(event) : null;
378
+ }
379
+ else if ((provider === "grok" || provider === "opencode") && event.type === "error") {
380
+ failure = failed(event);
381
+ }
382
+ }
383
+ if (failure)
384
+ throw new QuickCommitError(`${provider} CLI 失败:${failure}`, "CLAUDE_CLI_FAILED");
385
+ }
319
386
  function extractCodexText(stdout) {
387
+ assertCliProtocolSucceeded(stdout, "codex");
320
388
  let lastAgentText = "";
321
389
  for (const line of stdout.split(/\r?\n/)) {
322
390
  const trimmed = line.trim();
@@ -343,6 +411,7 @@ function extractCodexText(stdout) {
343
411
  return "";
344
412
  }
345
413
  function extractOpenCodeText(stdout) {
414
+ assertCliProtocolSucceeded(stdout, "opencode");
346
415
  const texts = [];
347
416
  for (const line of stdout.split(/\r?\n/)) {
348
417
  const trimmed = line.trim();
@@ -361,6 +430,7 @@ function extractOpenCodeText(stdout) {
361
430
  return texts.join("\n").trim();
362
431
  }
363
432
  function extractGrokText(stdout) {
433
+ assertCliProtocolSucceeded(stdout, "grok");
364
434
  let text = "";
365
435
  for (const line of stdout.split(/\r?\n/)) {
366
436
  const trimmed = line.trim();
@@ -378,6 +448,7 @@ function extractGrokText(stdout) {
378
448
  return text.trim();
379
449
  }
380
450
  function extractQoderText(stdout) {
451
+ assertCliProtocolSucceeded(stdout, "qoder");
381
452
  let resultText = "";
382
453
  const assistantTexts = [];
383
454
  for (const line of stdout.split(/\r?\n/)) {
@@ -408,6 +479,7 @@ function extractQoderText(stdout) {
408
479
  return (resultText || assistantTexts.join("\n")).trim();
409
480
  }
410
481
  function extractPiText(stdout) {
482
+ assertCliProtocolSucceeded(stdout, "pi");
411
483
  let text = "";
412
484
  for (const line of stdout.split(/\r?\n/)) {
413
485
  try {
@@ -425,6 +497,7 @@ function extractPiText(stdout) {
425
497
  }
426
498
  /** Gemini `--output-format stream-json`:assistant message 是增量 chunk,拼接即可。 */
427
499
  function extractGeminiText(stdout) {
500
+ assertCliProtocolSucceeded(stdout, "gemini");
428
501
  let text = "";
429
502
  for (const line of stdout.split(/\r?\n/)) {
430
503
  const trimmed = line.trim();
@@ -574,7 +647,7 @@ async function callCodexText(request, cwd, opts) {
574
647
  args.push("-");
575
648
  const stdout = await runCliText("codex", args, contentWithSystemPrompt("codex", request), {
576
649
  cwd,
577
- timeoutMs: CODEX_MESSAGE_TIMEOUT_MS,
650
+ timeoutMs: cliTextTimeoutMs(CODEX_MESSAGE_TIMEOUT_MS, opts),
578
651
  inheritEnv: opts.inheritEnv,
579
652
  });
580
653
  const text = extractCodexText(stdout);
@@ -593,7 +666,7 @@ async function callOpenCodeText(request, cwd, opts) {
593
666
  args.push("--variant", variant);
594
667
  const stdout = await runCliText("opencode", args, contentWithSystemPrompt("opencode", request), {
595
668
  cwd,
596
- timeoutMs: CODEX_MESSAGE_TIMEOUT_MS,
669
+ timeoutMs: cliTextTimeoutMs(CODEX_MESSAGE_TIMEOUT_MS, opts),
597
670
  inheritEnv: opts.inheritEnv,
598
671
  });
599
672
  const text = extractOpenCodeText(stdout);
@@ -604,7 +677,7 @@ async function callOpenCodeText(request, cwd, opts) {
604
677
  async function callGrokText(request, cwd, opts) {
605
678
  const stdout = await runCliText("grok", buildGrokTextArgs(request, opts), "", {
606
679
  cwd,
607
- timeoutMs: CODEX_MESSAGE_TIMEOUT_MS,
680
+ timeoutMs: cliTextTimeoutMs(CODEX_MESSAGE_TIMEOUT_MS, opts),
608
681
  inheritEnv: opts.inheritEnv,
609
682
  });
610
683
  const text = extractGrokText(stdout);
@@ -615,7 +688,7 @@ async function callGrokText(request, cwd, opts) {
615
688
  async function callQoderText(request, cwd, opts) {
616
689
  const stdout = await runCliText("qodercli", buildQoderTextArgs(request, opts), "", {
617
690
  cwd,
618
- timeoutMs: CODEX_MESSAGE_TIMEOUT_MS,
691
+ timeoutMs: cliTextTimeoutMs(CODEX_MESSAGE_TIMEOUT_MS, opts),
619
692
  inheritEnv: opts.inheritEnv,
620
693
  });
621
694
  const text = extractQoderText(stdout);
@@ -624,7 +697,9 @@ async function callQoderText(request, cwd, opts) {
624
697
  return text;
625
698
  }
626
699
  async function callPiText(request, cwd, opts) {
627
- const stdout = await runCliText("pi", buildPiTextArgs(request, opts), "", { cwd, timeoutMs: CODEX_MESSAGE_TIMEOUT_MS, inheritEnv: opts.inheritEnv });
700
+ const stdout = await runCliText("pi", buildPiTextArgs(request, opts), "", {
701
+ cwd, timeoutMs: cliTextTimeoutMs(CODEX_MESSAGE_TIMEOUT_MS, opts), inheritEnv: opts.inheritEnv,
702
+ });
628
703
  const text = extractPiText(stdout);
629
704
  if (!text)
630
705
  throw new QuickCommitError("Pi 返回了空的 commit message。", "EMPTY_AI_MESSAGE");
@@ -633,7 +708,7 @@ async function callPiText(request, cwd, opts) {
633
708
  async function callGeminiText(request, cwd, opts) {
634
709
  const stdout = await runCliText("gemini", buildGeminiTextArgs(opts), contentWithSystemPrompt("gemini", request), {
635
710
  cwd,
636
- timeoutMs: CODEX_MESSAGE_TIMEOUT_MS,
711
+ timeoutMs: cliTextTimeoutMs(CODEX_MESSAGE_TIMEOUT_MS, opts),
637
712
  inheritEnv: opts.inheritEnv,
638
713
  });
639
714
  const text = extractGeminiText(stdout);
@@ -689,41 +764,43 @@ function singleCandidate(opts) {
689
764
  };
690
765
  }
691
766
  /**
692
- * 按顺序尝试候选:跳过没安装的 CLI,逐条收集错误,成功即返回。
767
+ * 按顺序尝试候选:跳过没安装的 CLI,协议与输出校验均成功才返回。
693
768
  * 只有一条候选时保持原行为(错误原样抛出,不摘要)。
769
+ * 仅用于一次性文本生成;执行工具的任务不能使用这条无条件重试路径。
694
770
  */
695
- async function callCliCandidates(request, cwd, language, opts) {
771
+ async function callCliCandidates(request, cwd, language, opts, parseOutput) {
696
772
  const chain = opts.cliCandidates?.length ? opts.cliCandidates : [singleCandidate(opts)];
697
773
  if (chain.length === 1) {
698
- return callCliAiText(request, cwd, language, candidateOptions(opts, chain[0]));
774
+ return parseOutput(await callCliAiText(request, cwd, language, candidateOptions(opts, chain[0])));
699
775
  }
700
776
  const installed = chain.filter((candidate) => providerCliInstalled(candidate.provider));
701
777
  const attempts = installed.length ? installed : chain;
702
778
  const deadline = Date.now() + CLI_CHAIN_BUDGET_MS;
703
779
  const errors = [];
704
780
  for (const candidate of attempts) {
705
- if (Date.now() > deadline) {
781
+ if (Date.now() >= deadline) {
706
782
  errors.push("已超过等待上限");
707
783
  break;
708
784
  }
709
785
  try {
710
- const text = await callCliAiText(request, cwd, language, candidateOptions(opts, candidate));
711
- if (text.trim())
712
- return text;
713
- errors.push(`${candidate.provider}: 返回空结果`);
786
+ const text = await callCliAiText(request, cwd, language, { ...candidateOptions(opts, candidate), deadline });
787
+ const result = await parseOutput(text);
788
+ if (candidate !== attempts[0]) {
789
+ console.info(`[SystemAi] 候选 ${chain.indexOf(candidate) + 1}/${chain.length} (${candidate.provider}) 成功`);
790
+ }
791
+ return result;
714
792
  }
715
793
  catch (error) {
716
794
  errors.push(`${candidate.provider}: ${getGitErrorMessage(error)}`);
795
+ // 不记录提示词、路径、CLI 原始响应或可能带鉴权信息的错误正文。
796
+ const code = error instanceof QuickCommitError ? error.code : "INVALID_AI_RESULT";
797
+ console.warn(`[SystemAi] 候选 ${chain.indexOf(candidate) + 1}/${chain.length} (${candidate.provider}) 失败: ${code}`);
717
798
  }
718
799
  }
719
800
  throw new QuickCommitError(`所有 CLI 候选均失败:${errors.join(";")}`, "AI_FALLBACK_FAILED");
720
801
  }
721
- /**
722
- * Run a lightweight AI request through the system employee's CLI chain.
723
- * 所有 Wand 自有文本调用都走本机已安装的 CLI,没有任何直连 API 分支。
724
- */
725
- export async function callConfiguredAiText(request, cwd, language, opts) {
726
- return callCliCandidates(withOpsPersona(request, opts.opsPersona), cwd, language, opts);
802
+ export async function callConfiguredAiText(request, cwd, language, opts, parseOutput = (raw) => raw) {
803
+ return callCliCandidates(withOpsPersona(request, opts.opsPersona), cwd, language, opts, parseOutput);
727
804
  }
728
805
  /** Read the unstaged + staged tree without touching the index. */
729
806
  async function collectWorkingTreeDiff(cwd) {
@@ -814,12 +891,12 @@ function commitTaskLine(lang, usedIteration) {
814
891
  async function generateCommitMessage(cwd, language, ai = {}, options = {}) {
815
892
  const { input, usedIteration } = await buildCommitPromptInput(cwd, options);
816
893
  const lang = language.trim() || "中文";
817
- const raw = await callConfiguredAiText({ system: commitTaskLine(lang, usedIteration), prompt: input }, cwd, language, ai);
818
- const message = normalizeAiText(raw);
819
- if (!message) {
820
- throw new QuickCommitError("AI 返回了空的 commit message。", "EMPTY_AI_MESSAGE");
821
- }
822
- return message;
894
+ return callConfiguredAiText({ system: commitTaskLine(lang, usedIteration), prompt: input }, cwd, language, ai, (raw) => {
895
+ const message = normalizeAiText(raw);
896
+ if (!message)
897
+ throw new QuickCommitError("AI 返回了空的 commit message。", "EMPTY_AI_MESSAGE");
898
+ return message;
899
+ });
823
900
  }
824
901
  function tryParseJson(raw) {
825
902
  let text = raw.trim();
@@ -867,23 +944,15 @@ async function generateCommitMessageWithTag(cwd, language, ai = {}, options = {}
867
944
 
868
945
  请严格输出**单行 JSON 对象**,不要 Markdown 代码块、不要任何解释文字、不要多余引号。格式:
869
946
  {"message":"...","tag":"v1.2.3"}`;
870
- const raw = await callConfiguredAiText({ system, prompt: `${tagHint}\n\n${input}` }, cwd, language, ai);
871
- const parsed = tryParseJson(raw);
872
- let message;
873
- let suggestedTag;
874
- if (parsed && typeof parsed.message === "string") {
875
- message = normalizeAiText(parsed.message);
876
- suggestedTag = sanitizeSuggestedTag(parsed.tag);
877
- }
878
- else {
879
- // Fallback: treat whole output as message, no tag suggestion
880
- message = normalizeAiText(raw);
881
- suggestedTag = undefined;
882
- }
883
- if (!message) {
884
- throw new QuickCommitError("AI 返回了空的 commit message。", "EMPTY_AI_MESSAGE");
885
- }
886
- return { message, suggestedTag };
947
+ return callConfiguredAiText({ system, prompt: `${tagHint}\n\n${input}` }, cwd, language, ai, (raw) => {
948
+ const parsed = tryParseJson(raw);
949
+ // Preserve the plain-message fallback for older CLIs that do not return JSON.
950
+ const message = normalizeAiText(parsed && typeof parsed.message === "string" ? parsed.message : raw);
951
+ const suggestedTag = parsed && typeof parsed.message === "string" ? sanitizeSuggestedTag(parsed.tag) : undefined;
952
+ if (!message)
953
+ throw new QuickCommitError("AI 返回了空的 commit message。", "EMPTY_AI_MESSAGE");
954
+ return { message, suggestedTag };
955
+ });
887
956
  }
888
957
  export async function generateCommitMessageOnly(cwd, language, ai = {}, options = {}) {
889
958
  if (!cwd || !existsSync(cwd)) {
@@ -932,19 +1001,13 @@ async function generateTagAfterCommit(cwd, language, commitMessage, ai = {}) {
932
1001
  请用${lang}思考但严格输出**单行 JSON 对象**,不要 Markdown 代码块、不要任何解释文字、不要多余引号。格式:
933
1002
  {"tag":"v1.2.3"}`;
934
1003
  const prompt = `${tagHint}\n\ncommit message:${commitMessage}\n\ngit diff:\n${diff}`;
935
- const raw = await callConfiguredAiText({ system, prompt }, cwd, language, ai);
936
- const parsed = tryParseJson(raw);
937
- let suggested;
938
- if (parsed && typeof parsed.tag === "string") {
939
- suggested = sanitizeSuggestedTag(parsed.tag);
940
- }
941
- else {
942
- suggested = sanitizeSuggestedTag(raw);
943
- }
944
- if (!suggested) {
945
- throw new QuickCommitError("AI 没有给出合法的 tag,请手动填写。", "INVALID_AI_TAG");
946
- }
947
- return suggested;
1004
+ return callConfiguredAiText({ system, prompt }, cwd, language, ai, (raw) => {
1005
+ const parsed = tryParseJson(raw);
1006
+ const suggested = sanitizeSuggestedTag(parsed && typeof parsed.tag === "string" ? parsed.tag : raw);
1007
+ if (!suggested)
1008
+ throw new QuickCommitError("AI 没有给出合法的 tag,请手动填写。", "INVALID_AI_TAG");
1009
+ return suggested;
1010
+ });
948
1011
  }
949
1012
  /**
950
1013
  * Push current branch (with upstream auto-setup) and/or tags.
@@ -1,4 +1,5 @@
1
1
  import type { WandStorage } from "./storage.js";
2
+ import type { SessionSource } from "./types.js";
2
3
  import { type WandIterationPrompt, type WandIterationPromptSource } from "./task-types.js";
3
4
  /** commit 生成的输入模式:iteration = 只用本轮提示词(默认,省 token);diff = 读完整 diff。 */
4
5
  export type CommitContextMode = "iteration" | "diff";
@@ -13,6 +14,10 @@ export interface IterationPromptSession {
13
14
  cwd?: string;
14
15
  workspaceId?: string;
15
16
  workspaceTaskId?: string;
17
+ sessionSource?: SessionSource;
18
+ automationId?: string;
19
+ interactiveShell?: boolean;
20
+ employeeId?: string;
16
21
  }
17
22
  export interface IterationPromptTarget {
18
23
  milestoneId: string;
@@ -9,6 +9,9 @@ import path from "node:path";
9
9
  import { getErrorMessage } from "./error-utils.js";
10
10
  import { runGitAsync } from "./git-utils.js";
11
11
  import { shouldGenerateSessionTopicFromInput, summarizeSessionTitleFromInput } from "./session-topic.js";
12
+ import { recordUserMemory } from "./user-memory.js";
13
+ import { DEFAULT_EMPLOYEE_ID } from "./ai-team-types.js";
14
+ import { explicitEmployeeMemoryContent } from "./employee-knowledge-content.js";
12
15
  import { WAND_ITERATION_PROMPT_DETAIL_MAX_LENGTH, WAND_ITERATION_PROMPT_TITLE_MAX_LENGTH, } from "./task-types.js";
13
16
  /** 用户在上次选择后记住的模式;两条互不影响,互相切不会被覆盖。 */
14
17
  export const COMMIT_CONTEXT_MODE_PREF_KEY = "pref:commitContextMode";
@@ -119,11 +122,19 @@ export function whenIterationPromptsSettled() {
119
122
  */
120
123
  export function recordIterationPrompt(storage, session, prompt, source = "session", options = {}) {
121
124
  const raw = prompt?.trim();
122
- if (!raw)
125
+ if (!raw || explicitEmployeeMemoryContent(raw))
123
126
  return;
127
+ // 明确记忆不是代码变更意图,不进入共享迭代/commit 摘要。
124
128
  // 与会话标题共用同一把尺子:太短的输入不值得进迭代记录。
125
129
  if (!options.skipFilter && !shouldGenerateSessionTopicFromInput(raw))
126
130
  return;
131
+ // Only real interactive user prompts; never learn from team-generated instructions or shell input.
132
+ if (((!session.sessionSource || session.sessionSource === "interactive") && !session.automationId
133
+ || session.automationId?.startsWith("wand-task:"))
134
+ && (!session.employeeId || session.employeeId === DEFAULT_EMPLOYEE_ID)
135
+ && !session.interactiveShell && !/^[\/!]/.test(raw)) {
136
+ recordUserMemory(storage, "session.prompt", raw, session.id);
137
+ }
127
138
  const run = async () => {
128
139
  try {
129
140
  const detail = clip(cleanIterationPromptText(raw), WAND_ITERATION_PROMPT_DETAIL_MAX_LENGTH);
@@ -378,19 +378,23 @@ function toolActivity(use, hasImage) {
378
378
  const occurredAt = use.occurredAt ?? use.activity?.occurredAt;
379
379
  return {
380
380
  kind,
381
- label: activityLabel(kind),
381
+ label: activityLabel(kind, use, path),
382
382
  ...(fileKey ? { fileKey } : {}),
383
383
  ...(hasImage || imagePath || use.activity?.hasImage ? { hasImage: true } : {}),
384
- ...(kind === "run_command" && occurredAt ? { occurredAt } : {}),
384
+ ...(occurredAt ? { occurredAt } : {}),
385
385
  };
386
386
  }
387
- function activityLabel(kind) {
388
- switch (kind) {
389
- case "edit_file": return "修改文件";
390
- case "read_file": return "查看文件";
391
- case "run_command": return "运行命令";
392
- default: return "使用工具";
393
- }
387
+ function activityLabel(kind, use, path) {
388
+ // Only a bounded file/tool identity belongs in the timeline, never commands,
389
+ // patches, search queries or output. Keep labels stable when projecting twice.
390
+ if (!path && use.activity?.label)
391
+ return use.activity.label;
392
+ const file = path.replace(/\\/g, "/").split("/").filter(Boolean).slice(-2).join("/");
393
+ const name = use.name.replace(/\s+/g, " ").trim().slice(0, 80) || "工具";
394
+ const label = kind === "edit_file" ? `修改 ${file}`
395
+ : kind === "read_file" ? `查看 ${file}`
396
+ : kind === "run_command" ? `运行命令 · ${name}` : `调用 ${name}`;
397
+ return label.length > 120 ? `${label.slice(0, 119)}…` : label;
394
398
  }
395
399
  const INDEPENDENT_TOOL_NAMES = new Set([
396
400
  "AskUserQuestion", "TodoWrite", "TaskCreate", "TaskUpdate", "TaskList",
package/dist/models.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import { type PiModelEndpointDiscoveryOptions } from "./pi-model-discovery.js";
1
2
  import { ClaudeModelInfo } from "./types.js";
2
3
  /**
3
4
  * The complete server-side model catalog. Keep this separate from the Claude
@@ -36,6 +37,9 @@ export interface ModelRefreshOptions {
36
37
  commandRunner?: ModelCommandRunner;
37
38
  modelsApi?: ClaudeModelsApi;
38
39
  verifyClaudeCandidates?: boolean;
40
+ piEndpointDiscovery?: PiModelEndpointDiscoveryOptions & {
41
+ enabled: boolean;
42
+ };
39
43
  now?: () => Date;
40
44
  }
41
45
  export interface ThinkingEffortLevel {
package/dist/models.js CHANGED
@@ -3,6 +3,7 @@ import { createHash } from "node:crypto";
3
3
  import { promisify } from "node:util";
4
4
  import Anthropic from "@anthropic-ai/sdk";
5
5
  import { resolveChildEnv } from "./env-utils.js";
6
+ import { discoverPiEndpointModels } from "./pi-model-discovery.js";
6
7
  import { extractSemver } from "./version-utils.js";
7
8
  const execFileAsync = promisify(execFile);
8
9
  const CLAUDE_VERIFICATION_CACHE_KEY = "claude-model-verifications-v1";
@@ -384,20 +385,47 @@ async function probeQoderModels(runner, env) {
384
385
  return { ok: false };
385
386
  }
386
387
  }
387
- async function probePiModels(runner, env) {
388
+ async function probePiModels(runner, env, options) {
388
389
  const rpcText = await commandText(runner, "pi", ["--mode", "rpc", "--no-session"], env, 12_000, `${JSON.stringify({ id: "wand-models", type: "get_available_models" })}\n`);
389
390
  const fromRpc = parsePiRpcModels(rpcText);
390
- if (fromRpc.length > 1 || (fromRpc.length === 1 && fromRpc[0]?.id !== "default")) {
391
- return { ok: true, value: fromRpc };
392
- }
393
- try {
394
- const { stdout } = await runner("pi", ["--list-models"], { env, timeout: 8000 });
395
- const parsed = parsePiModels(stdout);
396
- return parsed.some((model) => model.id !== "default") ? { ok: true, value: parsed } : { ok: false };
391
+ let configuredModels = fromRpc.length > 1 || (fromRpc.length === 1 && fromRpc[0]?.id !== "default")
392
+ ? fromRpc
393
+ : [];
394
+ if (!configuredModels.length) {
395
+ try {
396
+ const { stdout } = await runner("pi", ["--list-models"], { env, timeout: 8000 });
397
+ const parsed = parsePiModels(stdout);
398
+ if (parsed.some((model) => model.id !== "default"))
399
+ configuredModels = parsed;
400
+ }
401
+ catch {
402
+ // The remote endpoint catalog can still be useful when the Pi CLI is temporarily unavailable.
403
+ }
397
404
  }
398
- catch {
405
+ const endpointModels = options.piEndpointDiscovery?.enabled
406
+ ? await discoverPiEndpointModels({
407
+ agentDir: options.piEndpointDiscovery.agentDir,
408
+ env,
409
+ fetchImpl: options.piEndpointDiscovery.fetchImpl,
410
+ })
411
+ : [];
412
+ if (!configuredModels.length && !endpointModels.length)
399
413
  return { ok: false };
414
+ return { ok: true, value: mergePiModelCatalog(configuredModels, endpointModels) };
415
+ }
416
+ function mergePiModelCatalog(configuredModels, endpointModels) {
417
+ const defaultModel = configuredModels.find((model) => model.id === "default")
418
+ ?? cloneModels(PI_FALLBACK_MODELS)[0];
419
+ const models = new Map();
420
+ for (const model of configuredModels) {
421
+ if (model.id !== "default")
422
+ models.set(model.id, model);
423
+ }
424
+ for (const model of endpointModels) {
425
+ if (model.id !== "default" && !models.has(model.id))
426
+ models.set(model.id, model);
400
427
  }
428
+ return [defaultModel, ...models.values()];
401
429
  }
402
430
  /** 从 CLI 的 help / 非法档位报错里抽出 `low, medium, high` 这种列表。 */
403
431
  export function parseCliEffortList(text) {
@@ -1025,7 +1053,7 @@ async function discoverModelCache(options, previous) {
1025
1053
  probeOpenCode(runner, env),
1026
1054
  probeGrokModels(runner, env),
1027
1055
  probeQoderModels(runner, env),
1028
- probePiModels(runner, env),
1056
+ probePiModels(runner, env, options),
1029
1057
  listClaudeModelsFromApi(options, env),
1030
1058
  probeEffortList(runner, env, "claude", ["--effort", "__wand_probe__", "--help"]),
1031
1059
  probeEffortList(runner, env, "grok", ["--effort", "__wand_probe__", "-p", "x", "--output-format", "streaming-json", "--max-turns", "1"]),
@@ -0,0 +1,12 @@
1
+ import type { ClaudeModelInfo } from "./types.js";
2
+ export interface PiModelEndpointDiscoveryOptions {
3
+ /** Defaults to PI_CODING_AGENT_DIR or ~/.pi/agent. */
4
+ agentDir?: string;
5
+ env?: NodeJS.ProcessEnv;
6
+ fetchImpl?: typeof fetch;
7
+ }
8
+ /**
9
+ * Discovers additional chat model IDs exposed by the API endpoints configured in Pi's models.json.
10
+ * The endpoint's key stays in memory and is never returned, logged, or persisted in the model catalog.
11
+ */
12
+ export declare function discoverPiEndpointModels(options?: PiModelEndpointDiscoveryOptions): Promise<ClaudeModelInfo[]>;