kimi-plugin-cc 2.0.8 → 2.2.1

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 (88) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/CHANGELOG.md +27 -0
  4. package/README.md +5 -5
  5. package/agents/kimi-ask.md +5 -1
  6. package/agents/kimi-challenge.md +4 -2
  7. package/agents/kimi-pursue.md +3 -1
  8. package/agents/kimi-rescue.md +5 -1
  9. package/agents/kimi-review.md +4 -2
  10. package/agents/kimi-swarm-write.md +3 -1
  11. package/agents/kimi-swarm.md +3 -1
  12. package/commands/ask.md +5 -3
  13. package/commands/challenge.md +5 -3
  14. package/commands/pursue.md +5 -1
  15. package/commands/rescue.md +5 -3
  16. package/commands/review.md +5 -3
  17. package/commands/swarm.md +5 -1
  18. package/dist/cli-client.js +42 -10
  19. package/dist/commands/ask.js +14 -7
  20. package/dist/commands/cli-helpers.js +66 -4
  21. package/dist/commands/pursue.js +7 -3
  22. package/dist/commands/rescue.js +16 -10
  23. package/dist/commands/result.js +5 -1
  24. package/dist/commands/review-gate.js +21 -14
  25. package/dist/commands/review.js +9 -5
  26. package/dist/commands/status.js +2 -2
  27. package/dist/commands/swarm.js +10 -4
  28. package/dist/help.js +1 -1
  29. package/dist/hook-escape.js +71 -0
  30. package/dist/hooks/approval-policy.js +17 -27
  31. package/dist/hooks/config-safety.js +15 -7
  32. package/dist/job-store.js +52 -7
  33. package/dist/kimi-engine.js +2 -0
  34. package/dist/kimi-errors.js +1 -1
  35. package/dist/kimi-timeouts.js +6 -10
  36. package/dist/kimi-version-probe.js +2 -2
  37. package/dist/parsing.js +59 -53
  38. package/dist/session-recovery.js +203 -0
  39. package/dist/vendor/smol-toml/date.js +50 -21
  40. package/dist/vendor/smol-toml/error.js +6 -2
  41. package/dist/vendor/smol-toml/extract.js +259 -66
  42. package/dist/vendor/smol-toml/parse.js +64 -48
  43. package/dist/vendor/smol-toml/primitive.js +107 -135
  44. package/dist/vendor/smol-toml/struct.js +106 -134
  45. package/dist/vendor/smol-toml/util.js +22 -78
  46. package/dist/version.js +1 -1
  47. package/docs/models.md +10 -0
  48. package/docs/registry-releases.md +24 -1
  49. package/docs/safety.md +59 -2
  50. package/package.json +1 -1
  51. package/plugins/kimi-codex/.codex-plugin/plugin.json +1 -1
  52. package/plugins/kimi-codex/dist/cli-client.js +42 -10
  53. package/plugins/kimi-codex/dist/commands/ask.js +14 -7
  54. package/plugins/kimi-codex/dist/commands/cli-helpers.js +66 -4
  55. package/plugins/kimi-codex/dist/commands/pursue.js +7 -3
  56. package/plugins/kimi-codex/dist/commands/rescue.js +16 -10
  57. package/plugins/kimi-codex/dist/commands/result.js +5 -1
  58. package/plugins/kimi-codex/dist/commands/review-gate.js +21 -14
  59. package/plugins/kimi-codex/dist/commands/review.js +9 -5
  60. package/plugins/kimi-codex/dist/commands/status.js +2 -2
  61. package/plugins/kimi-codex/dist/commands/swarm.js +10 -4
  62. package/plugins/kimi-codex/dist/help.js +1 -1
  63. package/plugins/kimi-codex/dist/hook-escape.js +71 -0
  64. package/plugins/kimi-codex/dist/hooks/approval-policy.js +17 -27
  65. package/plugins/kimi-codex/dist/hooks/config-safety.js +15 -7
  66. package/plugins/kimi-codex/dist/job-store.js +52 -7
  67. package/plugins/kimi-codex/dist/kimi-engine.js +2 -0
  68. package/plugins/kimi-codex/dist/kimi-errors.js +1 -1
  69. package/plugins/kimi-codex/dist/kimi-timeouts.js +6 -10
  70. package/plugins/kimi-codex/dist/kimi-version-probe.js +2 -2
  71. package/plugins/kimi-codex/dist/parsing.js +59 -53
  72. package/plugins/kimi-codex/dist/session-recovery.js +203 -0
  73. package/plugins/kimi-codex/dist/vendor/smol-toml/date.js +50 -21
  74. package/plugins/kimi-codex/dist/vendor/smol-toml/error.js +6 -2
  75. package/plugins/kimi-codex/dist/vendor/smol-toml/extract.js +259 -66
  76. package/plugins/kimi-codex/dist/vendor/smol-toml/parse.js +64 -48
  77. package/plugins/kimi-codex/dist/vendor/smol-toml/primitive.js +107 -135
  78. package/plugins/kimi-codex/dist/vendor/smol-toml/struct.js +106 -134
  79. package/plugins/kimi-codex/dist/vendor/smol-toml/util.js +22 -78
  80. package/plugins/kimi-codex/dist/version.js +1 -1
  81. package/plugins/kimi-codex/skills/kimi-ask/SKILL.md +4 -2
  82. package/plugins/kimi-codex/skills/kimi-challenge/SKILL.md +4 -2
  83. package/plugins/kimi-codex/skills/kimi-pursue/SKILL.md +3 -1
  84. package/plugins/kimi-codex/skills/kimi-rescue/SKILL.md +4 -2
  85. package/plugins/kimi-codex/skills/kimi-review/SKILL.md +4 -2
  86. package/plugins/kimi-codex/skills/kimi-setup/SKILL.md +2 -0
  87. package/plugins/kimi-codex/skills/kimi-swarm/SKILL.md +3 -1
  88. package/plugins/kimi-codex/skills/kimi-swarm-write/SKILL.md +3 -1
@@ -21,38 +21,30 @@ function looksLikeFlag(token) {
21
21
  // Short flag: a single dash followed by exactly one letter (`-r`, `-m`).
22
22
  return /^-[a-zA-Z]$/.test(token);
23
23
  }
24
- export const ASK_SUPPORTED_FLAGS = "-m/--model <name>, --budget <30m|1h|90s>, --background, --wait, --fresh, -r, --resume <job-id|session-id>";
25
- export const RESCUE_SUPPORTED_FLAGS = "-m/--model <name>, --budget <30m|1h|90s>, --background, --wait, --fresh, -r, --resume <job-id|session-id>";
26
- export const REVIEW_SUPPORTED_FLAGS = "--base <ref>, -m/--model <name>, --budget <30m|1h|90s>";
24
+ export const ASK_SUPPORTED_FLAGS = "-m/--model <name>, --thinking <off|low|medium|high>, --budget <30m|1h|90s>, --background, --wait, --fresh, -r, --resume <job-id|session-id>";
25
+ export const RESCUE_SUPPORTED_FLAGS = "-m/--model <name>, --thinking <off|low|medium|high>, --budget <30m|1h|90s>, --background, --wait, --fresh, -r, --resume <job-id|session-id>";
26
+ export const REVIEW_SUPPORTED_FLAGS = "--base <ref>, -m/--model <name>, --thinking <off|low|medium|high>, --budget <30m|1h|90s>";
27
27
  export const JOB_LOOKUP_SUPPORTED_FLAGS = "--type <review|challenge|rescue|review_gate|ask>, --json";
28
28
  export const SETUP_SUPPORTED_FLAGS = "--check, --uninstall, --all (with --uninstall), --enable-review-gate, --disable-review-gate, --models [--json] (without other setup flags)";
29
29
  export const REPAIR_SESSIONS_SUPPORTED_FLAGS = "--apply, --all";
30
30
  export const REPLAY_SUPPORTED_FLAGS = "none (one job-id required)";
31
- // Thinking-on is always-on for user-facing commands in v1.0. review-gate
32
- // pins thinking=false via the cli-client options bag instead of an argv
33
- // flag (see runtime/cli-client.ts::CliClientOptions). The parser rejects
34
- // --thinking and --no-thinking with a hard error so a documented-removed
35
- // flag doesn't get silently accepted as a no-op. Both bare (`--thinking`)
36
- // and assignment (`--thinking=true`) forms are rejected — otherwise the
37
- // assignment form would fall through to the generic "Unknown flag" path
38
- // and a wrapper agent reading the error would think the cure is to drop
39
- // the `=value` rather than the flag itself.
40
- const THINKING_FLAG_REMOVED_MESSAGE = "--thinking / --no-thinking were removed in v1.0. Thinking is always on for user-facing commands.";
41
- function isRemovedThinkingFlag(token) {
42
- return (token === "--thinking" ||
43
- token === "--no-thinking" ||
44
- token.startsWith("--thinking=") ||
45
- token.startsWith("--no-thinking="));
31
+ /** Validate syntax only; upstream per-model supportEfforts remains authoritative. */
32
+ export function validateThinkingEffort(value, stage = "args.parse", lowercase = true) {
33
+ const effort = lowercase ? value?.toLowerCase() : value;
34
+ if (!effort || !/^[a-z][a-z0-9-]{0,31}$/.test(effort)) {
35
+ throw new RuntimeError("INVALID_ARGS", "--thinking requires an effort matching [a-z][a-z0-9-]{0,31} (e.g. off, low, medium, high).", stage);
36
+ }
37
+ return effort;
46
38
  }
47
- export const PURSUE_SUPPORTED_FLAGS = "-m/--model <name>, --budget <30m|1h|90s>, --turns <N>";
48
- export const SWARM_SUPPORTED_FLAGS = "-m/--model <name>, --budget <30m|1h|90s>, --cap <N>, --max-concurrency <N>, --write";
39
+ export const PURSUE_SUPPORTED_FLAGS = "-m/--model <name>, --thinking <off|low|medium|high>, --budget <30m|1h|90s>, --turns <N>";
40
+ export const SWARM_SUPPORTED_FLAGS = "-m/--model <name>, --thinking <off|low|medium|high>, --budget <30m|1h|90s>, --cap <N>, --max-concurrency <N>, --write";
49
41
  /**
50
42
  * Parser for /kimi:swarm (read-only parallel fan-out). Foreground-only:
51
43
  * no --background/--fresh/--resume. Trailing tokens are the objective.
52
44
  */
53
45
  export function parseSwarmArgs(argv) {
54
46
  let model;
55
- const thinking = true;
47
+ let thinkingEffort;
56
48
  let budgetMs;
57
49
  let cap;
58
50
  let maxConcurrency;
@@ -60,6 +52,13 @@ export function parseSwarmArgs(argv) {
60
52
  let trailingTokens;
61
53
  for (let index = 0; index < argv.length; index += 1) {
62
54
  const token = argv[index];
55
+ if (token === "--thinking" || token.startsWith("--thinking=")) {
56
+ thinkingEffort = validateThinkingEffort(token === "--thinking" ? argv[++index] : token.slice("--thinking=".length));
57
+ continue;
58
+ }
59
+ if (token === "--no-thinking" || token.startsWith("--no-thinking=")) {
60
+ throw new RuntimeError("INVALID_ARGS", "--no-thinking is unsupported; use --thinking off", "args.parse");
61
+ }
63
62
  if (token === "--") {
64
63
  trailingTokens = argv.slice(index + 1);
65
64
  break;
@@ -105,9 +104,6 @@ export function parseSwarmArgs(argv) {
105
104
  break;
106
105
  }
107
106
  default:
108
- if (isRemovedThinkingFlag(token)) {
109
- throw new RuntimeError("INVALID_ARGS", THINKING_FLAG_REMOVED_MESSAGE, "swarm.parse");
110
- }
111
107
  if (looksLikeFlag(token)) {
112
108
  throw new RuntimeError("INVALID_ARGS", `Unknown flag ${token} for swarm. Supported flags: ${SWARM_SUPPORTED_FLAGS}. Use \`--\` before flag-shaped objective text to pass it through.`, "swarm.parse");
113
109
  }
@@ -122,7 +118,7 @@ export function parseSwarmArgs(argv) {
122
118
  maxConcurrency,
123
119
  write,
124
120
  model,
125
- thinking,
121
+ thinkingEffort,
126
122
  objective: trailingTokens?.join(" "),
127
123
  };
128
124
  }
@@ -186,12 +182,19 @@ export function parseDurationMs(token, flag, stage, budgetKind) {
186
182
  */
187
183
  export function parsePursueArgs(argv) {
188
184
  let model;
189
- const thinking = true;
185
+ let thinkingEffort;
190
186
  let budgetMs;
191
187
  let turns;
192
188
  let trailingTokens;
193
189
  for (let index = 0; index < argv.length; index += 1) {
194
190
  const token = argv[index];
191
+ if (token === "--thinking" || token.startsWith("--thinking=")) {
192
+ thinkingEffort = validateThinkingEffort(token === "--thinking" ? argv[++index] : token.slice("--thinking=".length));
193
+ continue;
194
+ }
195
+ if (token === "--no-thinking" || token.startsWith("--no-thinking=")) {
196
+ throw new RuntimeError("INVALID_ARGS", "--no-thinking is unsupported; use --thinking off", "args.parse");
197
+ }
195
198
  if (token === "--") {
196
199
  trailingTokens = argv.slice(index + 1);
197
200
  break;
@@ -237,9 +240,6 @@ export function parsePursueArgs(argv) {
237
240
  break;
238
241
  }
239
242
  default:
240
- if (isRemovedThinkingFlag(token)) {
241
- throw new RuntimeError("INVALID_ARGS", THINKING_FLAG_REMOVED_MESSAGE, "pursue.parse");
242
- }
243
243
  if (looksLikeFlag(token)) {
244
244
  throw new RuntimeError("INVALID_ARGS", `Unknown flag ${token} for pursue. Supported flags: ${PURSUE_SUPPORTED_FLAGS}. Use \`--\` before flag-shaped objective text to pass it through.`, "pursue.parse");
245
245
  }
@@ -252,12 +252,12 @@ export function parsePursueArgs(argv) {
252
252
  budgetMs,
253
253
  turns,
254
254
  model,
255
- thinking,
255
+ thinkingEffort,
256
256
  objective: trailingTokens?.join(" "),
257
257
  };
258
258
  }
259
259
  /** Help before -- wins even over malformed flags or prose trailing-token mode. */
260
- const VALUE_TAKING_FLAGS = new Set(["-m", "--model", "--budget", "--resume", "--base", "--cap", "--max-concurrency", "--turns", "--type"]);
260
+ const VALUE_TAKING_FLAGS = new Set(["--thinking", "-m", "--model", "--budget", "--resume", "--base", "--cap", "--max-concurrency", "--turns", "--type"]);
261
261
  /**
262
262
  * `--help` / `-h` counts only inside the leading flag region: before `--`
263
263
  * and before the first prose token. The value of a value-taking flag is
@@ -289,10 +289,7 @@ export function parseAskArgs(argv) {
289
289
  return { help: true, background: false, wait: false, fresh: false, resume: false };
290
290
  let budgetMs;
291
291
  let model;
292
- // Thinking is on by default across ask/review/challenge/rescue. The
293
- // parser rejects --thinking / --no-thinking; review-gate's runtime
294
- // caller pins thinking=false via CliClientOptions instead.
295
- let thinking = true;
292
+ let thinkingEffort;
296
293
  let background = false;
297
294
  let wait = false;
298
295
  let fresh = false;
@@ -302,6 +299,13 @@ export function parseAskArgs(argv) {
302
299
  let trailingTokens;
303
300
  for (let index = 0; index < argv.length; index += 1) {
304
301
  const token = argv[index];
302
+ if (token === "--thinking" || token.startsWith("--thinking=")) {
303
+ thinkingEffort = validateThinkingEffort(token === "--thinking" ? argv[++index] : token.slice("--thinking=".length));
304
+ continue;
305
+ }
306
+ if (token === "--no-thinking" || token.startsWith("--no-thinking=")) {
307
+ throw new RuntimeError("INVALID_ARGS", "--no-thinking is unsupported; use --thinking off", "args.parse");
308
+ }
305
309
  if (token === "--") {
306
310
  trailingTokens = argv.slice(index + 1);
307
311
  break;
@@ -357,9 +361,6 @@ export function parseAskArgs(argv) {
357
361
  break;
358
362
  }
359
363
  default:
360
- if (isRemovedThinkingFlag(token)) {
361
- throw new RuntimeError("INVALID_ARGS", THINKING_FLAG_REMOVED_MESSAGE, "args.parse");
362
- }
363
364
  if (looksLikeFlag(token)) {
364
365
  throw new RuntimeError("INVALID_ARGS", `Unknown flag ${token} for ask. Supported flags: ${ASK_SUPPORTED_FLAGS}. Use \`--\` before flag-shaped prompt text to pass it through.`, "args.parse");
365
366
  }
@@ -382,7 +383,7 @@ export function parseAskArgs(argv) {
382
383
  resume,
383
384
  resumeTarget,
384
385
  model,
385
- thinking,
386
+ thinkingEffort,
386
387
  prompt: trailingTokens?.join(" "),
387
388
  };
388
389
  }
@@ -394,7 +395,7 @@ export function parseReviewArgs(argv, commandName = "review") {
394
395
  background: parsed.background,
395
396
  wait: parsed.wait,
396
397
  model: parsed.model,
397
- thinking: parsed.thinking,
398
+ thinkingEffort: parsed.thinkingEffort,
398
399
  focus: parsed.trailingText,
399
400
  };
400
401
  }
@@ -403,7 +404,7 @@ export function parseRescueArgs(argv) {
403
404
  return { help: true, background: false, wait: false, fresh: false, resume: false };
404
405
  let budgetMs;
405
406
  let model;
406
- let thinking = true;
407
+ let thinkingEffort;
407
408
  let background = false;
408
409
  let wait = false;
409
410
  let fresh = false;
@@ -413,6 +414,13 @@ export function parseRescueArgs(argv) {
413
414
  let trailingTokens;
414
415
  for (let index = 0; index < argv.length; index += 1) {
415
416
  const token = argv[index];
417
+ if (token === "--thinking" || token.startsWith("--thinking=")) {
418
+ thinkingEffort = validateThinkingEffort(token === "--thinking" ? argv[++index] : token.slice("--thinking=".length));
419
+ continue;
420
+ }
421
+ if (token === "--no-thinking" || token.startsWith("--no-thinking=")) {
422
+ throw new RuntimeError("INVALID_ARGS", "--no-thinking is unsupported; use --thinking off", "args.parse");
423
+ }
416
424
  if (token === "--") {
417
425
  trailingTokens = argv.slice(index + 1);
418
426
  break;
@@ -468,9 +476,6 @@ export function parseRescueArgs(argv) {
468
476
  break;
469
477
  }
470
478
  default:
471
- if (isRemovedThinkingFlag(token)) {
472
- throw new RuntimeError("INVALID_ARGS", THINKING_FLAG_REMOVED_MESSAGE, "args.parse");
473
- }
474
479
  if (looksLikeFlag(token)) {
475
480
  throw new RuntimeError("INVALID_ARGS", `Unknown flag ${token} for rescue. Supported flags: ${RESCUE_SUPPORTED_FLAGS}. Use \`--\` before flag-shaped prompt text to pass it through.`, "args.parse");
476
481
  }
@@ -493,7 +498,7 @@ export function parseRescueArgs(argv) {
493
498
  resume,
494
499
  resumeTarget,
495
500
  model,
496
- thinking,
501
+ thinkingEffort,
497
502
  prompt: trailingTokens?.join(" "),
498
503
  };
499
504
  }
@@ -540,21 +545,25 @@ function isManagedCommandType(value) {
540
545
  function parseKnownFlags(argv, knownFlags, commandName) {
541
546
  let budgetMs;
542
547
  let model;
543
- let thinking = true;
548
+ let thinkingEffort;
544
549
  let base;
545
550
  let background = false;
546
551
  let wait = false;
547
552
  let trailingTokens;
548
553
  for (let index = 0; index < argv.length; index += 1) {
549
554
  const token = argv[index];
555
+ if (token === "--thinking" || token.startsWith("--thinking=")) {
556
+ thinkingEffort = validateThinkingEffort(token === "--thinking" ? argv[++index] : token.slice("--thinking=".length));
557
+ continue;
558
+ }
559
+ if (token === "--no-thinking" || token.startsWith("--no-thinking=")) {
560
+ throw new RuntimeError("INVALID_ARGS", "--no-thinking is unsupported; use --thinking off", "args.parse");
561
+ }
550
562
  if (token === "--") {
551
563
  trailingTokens = argv.slice(index + 1);
552
564
  break;
553
565
  }
554
566
  if (!knownFlags.has(token)) {
555
- if (isRemovedThinkingFlag(token)) {
556
- throw new RuntimeError("INVALID_ARGS", THINKING_FLAG_REMOVED_MESSAGE, "args.parse");
557
- }
558
567
  if (looksLikeFlag(token)) {
559
568
  // Wrapper agents routinely hallucinate flags like `--file` /
560
569
  // `--context` because that shape is common in other CLIs. Silently
@@ -610,9 +619,6 @@ function parseKnownFlags(argv, knownFlags, commandName) {
610
619
  // The early `if (!knownFlags.has(token))` gate above means we only
611
620
  // reach this switch with tokens that ARE in knownFlags, and every
612
621
  // member is handled above. A `default` arm would be unreachable.
613
- // --thinking and --no-thinking are NOT in knownFlags any more (v1.0
614
- // removed them); the early gate above hard-fails on them with
615
- // THINKING_FLAG_REMOVED_MESSAGE.
616
622
  }
617
623
  }
618
624
  return {
@@ -621,7 +627,7 @@ function parseKnownFlags(argv, knownFlags, commandName) {
621
627
  background,
622
628
  wait,
623
629
  model,
624
- thinking,
630
+ thinkingEffort,
625
631
  trailingText: trailingTokens?.join(" "),
626
632
  };
627
633
  }
@@ -0,0 +1,203 @@
1
+ import { constants } from "node:fs";
2
+ import { lstat, open, opendir, realpath } from "node:fs/promises";
3
+ import path from "node:path";
4
+ import { digestPrompt } from "./jobs.js";
5
+ // Total entries visited across both enumeration levels. A real store holds a
6
+ // few thousand sessions (2,494 entries on the reference host when this was
7
+ // written, growing by one per plugin run), so the preflight's 4,096 would have
8
+ // switched recovery off within months; the cap is a guard against a
9
+ // pathological tree, not a size budget. opendir streams, nothing is buffered.
10
+ // Dot-entries (`.index-dirty`, `.index-cache`) are upstream index state, never
11
+ // workspaces, and are skipped without being counted. Directories whose mtime
12
+ // predates the spawn cannot have been created by this run and are skipped
13
+ // before any read, so the byte budget scales with concurrent activity, not
14
+ // with store size.
15
+ const DIR_ENUM_CAP = 65_536;
16
+ const STATE_MAX_BYTES = 1024 * 1024;
17
+ const JOURNAL_PREFIX_BYTES = 4 * 1024 * 1024; // The prompt is argv-bounded (< 1 MiB); the first user message follows a few config records.
18
+ const SCAN_MAX_BYTES = 64 * 1024 * 1024;
19
+ const MTIME_SLACK_MS = 1000; // Filesystems with coarse mtime granularity.
20
+ const SESSION_ID = /^session_[0-9a-f-]{36}$/;
21
+ /** Metadata recovery only, never a substitute for resume lineage/plan scans.
22
+ * A capped or unreadable enumeration cannot establish uniqueness and refuses;
23
+ * skipping symlinked or dot entries is not partial — neither can be a session
24
+ * the child created.
25
+ *
26
+ * A candidate is bound to THIS job by content, not only by time: the first
27
+ * user message in the session's main journal must carry a text item whose
28
+ * sha256 equals the job's `prompt_digest` (the exact `-p` argument). Time and
29
+ * cwd alone would admit a concurrent TUI session, another host's finished job
30
+ * (host-local stores cannot see each other's session ids), or a `state.json`
31
+ * planted by repository code running under rescue. The digest is not unique
32
+ * on its own either (the same prompt can run twice), so the window and the
33
+ * exactly-one rule stay in force. */
34
+ export async function recoverSessionIdFromStore(options) {
35
+ const entryCap = options.entryCap ?? DIR_ENUM_CAP;
36
+ let entries = 0;
37
+ let bytes = 0;
38
+ let digestMismatches = 0;
39
+ let missingJournals = 0;
40
+ const candidates = [];
41
+ const capped = new Error("enumeration_capped");
42
+ async function* directories(dir) {
43
+ if ((await lstat(dir)).isSymbolicLink())
44
+ throw new Error("unreadable");
45
+ const handle = await opendir(dir);
46
+ for await (const entry of handle) {
47
+ if (entry.name.startsWith("."))
48
+ continue;
49
+ if (++entries > entryCap)
50
+ throw capped;
51
+ // A symlinked entry is never a session the child created; skipping it
52
+ // keeps one stray link from disabling recovery host-wide. Directories
53
+ // we descend are still lstat-anchored above.
54
+ if (entry.isSymbolicLink())
55
+ continue;
56
+ if (entry.isDirectory())
57
+ yield entry.name;
58
+ }
59
+ }
60
+ // Bounded no-follow read of a regular file. Returns undefined when the file
61
+ // is absent; throws "unreadable" for anything that is not a plain file. With
62
+ // `prefix`, a larger file yields its first `maxBytes` instead of refusing.
63
+ async function readBounded(file, maxBytes, prefix = false) {
64
+ let handle;
65
+ try {
66
+ handle = await open(file, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);
67
+ }
68
+ catch (error) {
69
+ if (error.code === "ENOENT")
70
+ return undefined;
71
+ throw error;
72
+ }
73
+ try {
74
+ const stat = await handle.stat();
75
+ if (!stat.isFile() || (stat.size > maxBytes && !prefix))
76
+ throw new Error("unreadable");
77
+ const size = Math.min(stat.size, maxBytes);
78
+ bytes += size;
79
+ if (bytes > SCAN_MAX_BYTES)
80
+ throw capped;
81
+ const buffer = Buffer.alloc(size);
82
+ let offset = 0;
83
+ while (offset < buffer.length) {
84
+ const read = await handle.read(buffer, offset, buffer.length - offset, offset);
85
+ if (!read.bytesRead)
86
+ break;
87
+ offset += read.bytesRead;
88
+ }
89
+ return buffer.subarray(0, offset);
90
+ }
91
+ finally {
92
+ await handle.close();
93
+ }
94
+ }
95
+ try {
96
+ if (!Number.isFinite(options.spawnedAt) || !Number.isFinite(options.settledAt) ||
97
+ options.settledAt < options.spawnedAt || !/^[0-9a-f]{64}$/.test(options.promptDigest)) {
98
+ return { reason: "none" };
99
+ }
100
+ const cwd = await realpath(options.cwd);
101
+ const sessions = path.join(options.kimiHome, "sessions");
102
+ // Missing nested dirs mean an incomplete scan; only a missing root is normal.
103
+ try {
104
+ await lstat(sessions);
105
+ }
106
+ catch (error) {
107
+ if (error.code === "ENOENT")
108
+ return { reason: "none" };
109
+ throw error;
110
+ }
111
+ for await (const workspace of directories(sessions)) {
112
+ const workspacePath = path.join(sessions, workspace);
113
+ for await (const session of directories(workspacePath)) {
114
+ if (!SESSION_ID.test(session))
115
+ continue;
116
+ const sessionPath = path.join(workspacePath, session);
117
+ // A directory this run created was modified at or after the spawn.
118
+ if ((await lstat(sessionPath)).mtimeMs < options.spawnedAt - MTIME_SLACK_MS)
119
+ continue;
120
+ const raw = await readBounded(path.join(sessionPath, "state.json"), STATE_MAX_BYTES);
121
+ if (!raw)
122
+ continue;
123
+ let state;
124
+ try {
125
+ state = JSON.parse(raw.toString("utf8"));
126
+ }
127
+ catch {
128
+ continue;
129
+ }
130
+ if (!state || typeof state !== "object")
131
+ continue;
132
+ const value = state;
133
+ // The child writes createdAt after the parent stamped spawnedAt on the
134
+ // same clock, so causality orders them; no pre-spawn slack is justified.
135
+ if (value.version !== 2 || typeof value.cwd !== "string" || !path.isAbsolute(value.cwd) ||
136
+ typeof value.id !== "string" || !SESSION_ID.test(value.id) || value.id !== session ||
137
+ typeof value.createdAt !== "number" || !Number.isFinite(value.createdAt) ||
138
+ value.createdAt < options.spawnedAt || value.createdAt > options.settledAt)
139
+ continue;
140
+ let stateCwd;
141
+ try {
142
+ stateCwd = await realpath(value.cwd);
143
+ }
144
+ catch {
145
+ continue;
146
+ }
147
+ if (stateCwd !== cwd || options.store.hasSessionId(value.id, options.jobId))
148
+ continue;
149
+ const journal = await readBounded(path.join(sessionPath, "agents", "main", "wire.jsonl"), JOURNAL_PREFIX_BYTES, true);
150
+ if (!journal) {
151
+ missingJournals += 1;
152
+ continue;
153
+ }
154
+ // A prefix cut mid-record must never be parsed as a complete line.
155
+ const text = journal.length === JOURNAL_PREFIX_BYTES
156
+ ? journal.subarray(0, journal.lastIndexOf(0x0a) + 1).toString("utf8") : journal.toString("utf8");
157
+ if (!firstUserMessageMatches(text, options.promptDigest)) {
158
+ digestMismatches += 1;
159
+ continue;
160
+ }
161
+ candidates.push(value.id);
162
+ }
163
+ }
164
+ if (candidates.length === 1)
165
+ return { sessionId: candidates[0] };
166
+ if (candidates.length > 1)
167
+ return { reason: `ambiguous:${candidates.length}` };
168
+ if (digestMismatches > 0)
169
+ return { reason: `digest_mismatch:${digestMismatches}` };
170
+ return { reason: missingJournals > 0 ? `journal_missing:${missingJournals}` : "none" };
171
+ }
172
+ catch (error) {
173
+ return { reason: error === capped ? "enumeration_capped" : "unreadable" };
174
+ }
175
+ }
176
+ /** True when the FIRST user message in a native-v2 main journal carries a
177
+ * text item equal to the prompt the job spawned with. Later user turns are
178
+ * never consulted: only the opening prompt binds a session to its spawn. */
179
+ export function firstUserMessageMatches(journal, promptDigest) {
180
+ for (const line of journal.split("\n")) {
181
+ if (!line.trim())
182
+ continue;
183
+ let record;
184
+ try {
185
+ record = JSON.parse(line);
186
+ }
187
+ catch {
188
+ continue;
189
+ }
190
+ if (!record || typeof record !== "object")
191
+ continue;
192
+ const message = record.message;
193
+ if (!message || typeof message !== "object" || message.role !== "user")
194
+ continue;
195
+ const content = message.content;
196
+ if (!Array.isArray(content))
197
+ return false;
198
+ return content.some((item) => item && typeof item === "object" && item.type === "text" &&
199
+ typeof item.text === "string" &&
200
+ digestPrompt(item.text) === promptDigest);
201
+ }
202
+ return false;
203
+ }
@@ -25,39 +25,68 @@
25
25
  * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
26
  * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
27
  */
28
- let DATE_TIME_RE = /^(\d{4}-\d{2}-\d{2})?[T ]?(?:(\d{2}):\d{2}(?::\d{2}(?:\.\d+)?)?)?(Z|[-+]\d{2}:\d{2})?$/i;
28
+ let DATE_TIME_RE = /^(\d{4}-\d{2}-\d{2})?[Tt ]?(?:(\d{2}):\d{2}(?::\d{2}(?:\.\d+)?)?)?(Z|z|[-+]\d{2}:\d{2})?$/i;
29
29
  export class TomlDate extends Date {
30
30
  #hasDate = false;
31
31
  #hasTime = false;
32
32
  #offset = null;
33
- constructor(date) {
33
+ constructor(date, fasttype, unsafeDelim) {
34
34
  let hasDate = true;
35
35
  let hasTime = true;
36
36
  let offset = 'Z';
37
+ let c;
37
38
  if (typeof date === 'string') {
38
- let match = date.match(DATE_TIME_RE);
39
- if (match) {
40
- if (!match[1]) {
41
- hasDate = false;
42
- date = `0000-01-01T${date}`;
39
+ if (fasttype)
40
+ prep: {
41
+ // Date-time
42
+ if (fasttype < 3) {
43
+ if (+date.slice(11, 13) > 23) {
44
+ date = '';
45
+ break prep;
46
+ }
47
+ // Local
48
+ if (fasttype === 2) {
49
+ offset = null;
50
+ date += 'Z';
51
+ }
52
+ // Offset; keep track of offset if not Z
53
+ else if ((c = date.charCodeAt(date.length - 1)) !== 0x5a /* Z */ && c !== 0x7a /* z */) {
54
+ offset = date.slice(date.length - 6);
55
+ }
56
+ if (unsafeDelim)
57
+ date = date.slice(0, 10) + 'T' + date.slice(11);
58
+ }
59
+ // Time
60
+ else if (fasttype === 4) {
61
+ date = +date.slice(0, 2) > 23 ? '' : `0000-01-01T${date}Z`;
62
+ }
63
+ hasDate = fasttype !== 4;
64
+ hasTime = fasttype !== 3;
43
65
  }
44
- hasTime = !!match[2];
45
- // Make sure to use T instead of a space. Breaks in case of extreme values otherwise.
46
- hasTime && date[10] === ' ' && (date = date.replace(' ', 'T'));
47
- // Do not allow rollover hours.
48
- if (match[2] && +match[2] > 23) {
49
- date = '';
66
+ else {
67
+ let match = date.match(DATE_TIME_RE);
68
+ if (match) {
69
+ if (!match[1]) {
70
+ hasDate = false;
71
+ date = `0000-01-01T${date}`;
72
+ }
73
+ hasTime = !!match[2];
74
+ // Make sure to use T instead of a space. Breaks in case of extreme values otherwise.
75
+ hasTime && date[10] === ' ' && (date = date.replace(' ', 'T'));
76
+ // Do not allow rollover hours.
77
+ if (match[2] && +match[2] > 23) {
78
+ date = '';
79
+ }
80
+ else {
81
+ offset = match[3] || null;
82
+ if (!offset && hasTime)
83
+ date += 'Z';
84
+ }
50
85
  }
51
86
  else {
52
- offset = match[3] || null;
53
- date = date.toUpperCase();
54
- if (!offset && hasTime)
55
- date += 'Z';
87
+ date = '';
56
88
  }
57
89
  }
58
- else {
59
- date = '';
60
- }
61
90
  }
62
91
  super(date);
63
92
  if (!isNaN(this.getTime())) {
@@ -93,7 +122,7 @@ export class TomlDate extends Date {
93
122
  if (this.#offset === null)
94
123
  return iso.slice(0, -1);
95
124
  // Offset DateTime
96
- if (this.#offset === 'Z')
125
+ if (this.#offset === 'Z' || this.#offset === 'z')
97
126
  return iso;
98
127
  // This part is quite annoying: JS strips the original timezone from the ISO string representation
99
128
  // Instead of using a "modified" date and "Z", we restore the representation "as authored"
@@ -26,11 +26,11 @@
26
26
  * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
27
  */
28
28
  function getLineColFromPtr(string, ptr) {
29
- let lines = string.slice(0, ptr).split(/\r\n|\n|\r/g);
29
+ let lines = string.slice(0, ptr).split(/\r?\n/);
30
30
  return [lines.length, lines.pop().length + 1];
31
31
  }
32
32
  function makeCodeBlock(string, line, column) {
33
- let lines = string.split(/\r\n|\n|\r/g);
33
+ let lines = string.split(/\r?\n/);
34
34
  let codeblock = '';
35
35
  let numberLen = (Math.log10(line + 1) | 0) + 1;
36
36
  for (let i = line - 1; i <= line + 1; i++) {
@@ -60,4 +60,8 @@ export class TomlError extends Error {
60
60
  this.column = column;
61
61
  this.codeblock = codeblock;
62
62
  }
63
+ /** @internal */
64
+ static x(message, ctx, ptr) {
65
+ throw new TomlError(message, { toml: ctx.s, ptr: ptr ?? ctx.p });
66
+ }
63
67
  }