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.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +27 -0
- package/README.md +5 -5
- package/agents/kimi-ask.md +5 -1
- package/agents/kimi-challenge.md +4 -2
- package/agents/kimi-pursue.md +3 -1
- package/agents/kimi-rescue.md +5 -1
- package/agents/kimi-review.md +4 -2
- package/agents/kimi-swarm-write.md +3 -1
- package/agents/kimi-swarm.md +3 -1
- package/commands/ask.md +5 -3
- package/commands/challenge.md +5 -3
- package/commands/pursue.md +5 -1
- package/commands/rescue.md +5 -3
- package/commands/review.md +5 -3
- package/commands/swarm.md +5 -1
- package/dist/cli-client.js +42 -10
- package/dist/commands/ask.js +14 -7
- package/dist/commands/cli-helpers.js +66 -4
- package/dist/commands/pursue.js +7 -3
- package/dist/commands/rescue.js +16 -10
- package/dist/commands/result.js +5 -1
- package/dist/commands/review-gate.js +21 -14
- package/dist/commands/review.js +9 -5
- package/dist/commands/status.js +2 -2
- package/dist/commands/swarm.js +10 -4
- package/dist/help.js +1 -1
- package/dist/hook-escape.js +71 -0
- package/dist/hooks/approval-policy.js +17 -27
- package/dist/hooks/config-safety.js +15 -7
- package/dist/job-store.js +52 -7
- package/dist/kimi-engine.js +2 -0
- package/dist/kimi-errors.js +1 -1
- package/dist/kimi-timeouts.js +6 -10
- package/dist/kimi-version-probe.js +2 -2
- package/dist/parsing.js +59 -53
- package/dist/session-recovery.js +203 -0
- package/dist/vendor/smol-toml/date.js +50 -21
- package/dist/vendor/smol-toml/error.js +6 -2
- package/dist/vendor/smol-toml/extract.js +259 -66
- package/dist/vendor/smol-toml/parse.js +64 -48
- package/dist/vendor/smol-toml/primitive.js +107 -135
- package/dist/vendor/smol-toml/struct.js +106 -134
- package/dist/vendor/smol-toml/util.js +22 -78
- package/dist/version.js +1 -1
- package/docs/models.md +10 -0
- package/docs/registry-releases.md +24 -1
- package/docs/safety.md +59 -2
- package/package.json +1 -1
- package/plugins/kimi-codex/.codex-plugin/plugin.json +1 -1
- package/plugins/kimi-codex/dist/cli-client.js +42 -10
- package/plugins/kimi-codex/dist/commands/ask.js +14 -7
- package/plugins/kimi-codex/dist/commands/cli-helpers.js +66 -4
- package/plugins/kimi-codex/dist/commands/pursue.js +7 -3
- package/plugins/kimi-codex/dist/commands/rescue.js +16 -10
- package/plugins/kimi-codex/dist/commands/result.js +5 -1
- package/plugins/kimi-codex/dist/commands/review-gate.js +21 -14
- package/plugins/kimi-codex/dist/commands/review.js +9 -5
- package/plugins/kimi-codex/dist/commands/status.js +2 -2
- package/plugins/kimi-codex/dist/commands/swarm.js +10 -4
- package/plugins/kimi-codex/dist/help.js +1 -1
- package/plugins/kimi-codex/dist/hook-escape.js +71 -0
- package/plugins/kimi-codex/dist/hooks/approval-policy.js +17 -27
- package/plugins/kimi-codex/dist/hooks/config-safety.js +15 -7
- package/plugins/kimi-codex/dist/job-store.js +52 -7
- package/plugins/kimi-codex/dist/kimi-engine.js +2 -0
- package/plugins/kimi-codex/dist/kimi-errors.js +1 -1
- package/plugins/kimi-codex/dist/kimi-timeouts.js +6 -10
- package/plugins/kimi-codex/dist/kimi-version-probe.js +2 -2
- package/plugins/kimi-codex/dist/parsing.js +59 -53
- package/plugins/kimi-codex/dist/session-recovery.js +203 -0
- package/plugins/kimi-codex/dist/vendor/smol-toml/date.js +50 -21
- package/plugins/kimi-codex/dist/vendor/smol-toml/error.js +6 -2
- package/plugins/kimi-codex/dist/vendor/smol-toml/extract.js +259 -66
- package/plugins/kimi-codex/dist/vendor/smol-toml/parse.js +64 -48
- package/plugins/kimi-codex/dist/vendor/smol-toml/primitive.js +107 -135
- package/plugins/kimi-codex/dist/vendor/smol-toml/struct.js +106 -134
- package/plugins/kimi-codex/dist/vendor/smol-toml/util.js +22 -78
- package/plugins/kimi-codex/dist/version.js +1 -1
- package/plugins/kimi-codex/skills/kimi-ask/SKILL.md +4 -2
- package/plugins/kimi-codex/skills/kimi-challenge/SKILL.md +4 -2
- package/plugins/kimi-codex/skills/kimi-pursue/SKILL.md +3 -1
- package/plugins/kimi-codex/skills/kimi-rescue/SKILL.md +4 -2
- package/plugins/kimi-codex/skills/kimi-review/SKILL.md +4 -2
- package/plugins/kimi-codex/skills/kimi-setup/SKILL.md +2 -0
- package/plugins/kimi-codex/skills/kimi-swarm/SKILL.md +3 -1
- 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
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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})?[
|
|
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
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
}
|