kimi-plugin-cc 2.0.7 → 2.2.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 (85) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/CHANGELOG.md +50 -0
  4. package/README.md +22 -6
  5. package/agents/kimi-ask.md +3 -0
  6. package/agents/kimi-challenge.md +3 -1
  7. package/agents/kimi-rescue.md +3 -0
  8. package/agents/kimi-review.md +3 -1
  9. package/commands/ask.md +6 -2
  10. package/commands/challenge.md +6 -2
  11. package/commands/rescue.md +6 -2
  12. package/commands/review.md +6 -2
  13. package/dist/cli-client.js +36 -13
  14. package/dist/commands/ask.js +16 -7
  15. package/dist/commands/cli-helpers.js +80 -0
  16. package/dist/commands/pursue.js +4 -2
  17. package/dist/commands/repair-sessions.js +2 -1
  18. package/dist/commands/replay.js +3 -2
  19. package/dist/commands/rescue.js +19 -9
  20. package/dist/commands/result.js +7 -1
  21. package/dist/commands/review-gate.js +2 -1
  22. package/dist/commands/review.js +8 -5
  23. package/dist/commands/setup.js +2 -1
  24. package/dist/commands/status.js +2 -2
  25. package/dist/commands/swarm.js +7 -3
  26. package/dist/companion.js +6 -0
  27. package/dist/help.js +40 -0
  28. package/dist/hooks/config-safety.js +18 -9
  29. package/dist/job-store.js +43 -6
  30. package/dist/jobs.js +7 -3
  31. package/dist/kimi-engine.js +1 -0
  32. package/dist/kimi-errors.js +35 -1
  33. package/dist/kimi-timeouts.js +7 -7
  34. package/dist/parsing.js +87 -13
  35. package/dist/render.js +19 -1
  36. package/dist/session-recovery.js +203 -0
  37. package/dist/vendor/smol-toml/date.js +50 -21
  38. package/dist/vendor/smol-toml/error.js +6 -2
  39. package/dist/vendor/smol-toml/extract.js +259 -66
  40. package/dist/vendor/smol-toml/parse.js +64 -48
  41. package/dist/vendor/smol-toml/primitive.js +107 -135
  42. package/dist/vendor/smol-toml/struct.js +106 -134
  43. package/dist/vendor/smol-toml/util.js +22 -78
  44. package/dist/version.js +1 -1
  45. package/docs/migration.md +3 -3
  46. package/docs/registry-releases.md +35 -1
  47. package/docs/safety.md +49 -2
  48. package/package.json +2 -2
  49. package/plugins/kimi-codex/.codex-plugin/plugin.json +1 -1
  50. package/plugins/kimi-codex/dist/cli-client.js +36 -13
  51. package/plugins/kimi-codex/dist/commands/ask.js +16 -7
  52. package/plugins/kimi-codex/dist/commands/cli-helpers.js +80 -0
  53. package/plugins/kimi-codex/dist/commands/pursue.js +4 -2
  54. package/plugins/kimi-codex/dist/commands/repair-sessions.js +2 -1
  55. package/plugins/kimi-codex/dist/commands/replay.js +3 -2
  56. package/plugins/kimi-codex/dist/commands/rescue.js +19 -9
  57. package/plugins/kimi-codex/dist/commands/result.js +7 -1
  58. package/plugins/kimi-codex/dist/commands/review-gate.js +2 -1
  59. package/plugins/kimi-codex/dist/commands/review.js +8 -5
  60. package/plugins/kimi-codex/dist/commands/setup.js +2 -1
  61. package/plugins/kimi-codex/dist/commands/status.js +2 -2
  62. package/plugins/kimi-codex/dist/commands/swarm.js +7 -3
  63. package/plugins/kimi-codex/dist/companion.js +6 -0
  64. package/plugins/kimi-codex/dist/help.js +40 -0
  65. package/plugins/kimi-codex/dist/hooks/config-safety.js +18 -9
  66. package/plugins/kimi-codex/dist/job-store.js +43 -6
  67. package/plugins/kimi-codex/dist/jobs.js +7 -3
  68. package/plugins/kimi-codex/dist/kimi-engine.js +1 -0
  69. package/plugins/kimi-codex/dist/kimi-errors.js +35 -1
  70. package/plugins/kimi-codex/dist/kimi-timeouts.js +7 -7
  71. package/plugins/kimi-codex/dist/parsing.js +87 -13
  72. package/plugins/kimi-codex/dist/render.js +19 -1
  73. package/plugins/kimi-codex/dist/session-recovery.js +203 -0
  74. package/plugins/kimi-codex/dist/vendor/smol-toml/date.js +50 -21
  75. package/plugins/kimi-codex/dist/vendor/smol-toml/error.js +6 -2
  76. package/plugins/kimi-codex/dist/vendor/smol-toml/extract.js +259 -66
  77. package/plugins/kimi-codex/dist/vendor/smol-toml/parse.js +64 -48
  78. package/plugins/kimi-codex/dist/vendor/smol-toml/primitive.js +107 -135
  79. package/plugins/kimi-codex/dist/vendor/smol-toml/struct.js +106 -134
  80. package/plugins/kimi-codex/dist/vendor/smol-toml/util.js +22 -78
  81. package/plugins/kimi-codex/dist/version.js +1 -1
  82. package/plugins/kimi-codex/skills/kimi-ask/SKILL.md +3 -1
  83. package/plugins/kimi-codex/skills/kimi-challenge/SKILL.md +2 -1
  84. package/plugins/kimi-codex/skills/kimi-rescue/SKILL.md +3 -1
  85. package/plugins/kimi-codex/skills/kimi-review/SKILL.md +3 -2
@@ -0,0 +1,40 @@
1
+ import { hasHelpFlag, ASK_SUPPORTED_FLAGS, RESCUE_SUPPORTED_FLAGS, REVIEW_SUPPORTED_FLAGS, PURSUE_SUPPORTED_FLAGS, SWARM_SUPPORTED_FLAGS, JOB_LOOKUP_SUPPORTED_FLAGS, SETUP_SUPPORTED_FLAGS, REPAIR_SESSIONS_SUPPORTED_FLAGS, REPLAY_SUPPORTED_FLAGS, } from "./parsing.js";
2
+ const SUBCOMMANDS = "setup, review, task, ask, status, result, cancel, replay, repair-sessions";
3
+ const TASK_KINDS = "rescue, challenge, pursue, swarm";
4
+ /** Help is a dispatch-only, side-effect-free exception to normal argument parsing. */
5
+ export function companionHelp(argv) {
6
+ const [command, kind] = argv;
7
+ // Scan only the argument region: the subcommand (and a task kind) are
8
+ // positional names, not prose, so they must not end the flag region.
9
+ const argumentStart = command === "--help" || command === "-h" ? 0
10
+ : command === "task" && kind !== undefined && !kind.startsWith("-") ? 2
11
+ : 1;
12
+ if (!hasHelpFlag(argv.slice(argumentStart)))
13
+ return undefined;
14
+ const suffix = " Use --help or -h among the leading flags (before any prompt text or --) to show this usage.";
15
+ const usage = (name, positionals, flags, description) => `Usage: companion.sh ${name}${positionals ? ` ${positionals}` : ""}. ${description} Supported flags: ${flags}.${suffix}`;
16
+ switch (command) {
17
+ case "--help":
18
+ case "-h":
19
+ return `Usage: companion.sh <subcommand> [arguments]. Subcommands: ${SUBCOMMANDS}. Use <subcommand> --help for usage.`;
20
+ case "task":
21
+ switch (kind) {
22
+ case "rescue": return usage("task rescue", "[options] [--] <task>", RESCUE_SUPPORTED_FLAGS, "Write-capable task; default budget 45m. Backgrounding does not extend the budget.");
23
+ case "challenge": return usage("task challenge", "[options] [--] [focus]", REVIEW_SUPPORTED_FLAGS, "Read-only challenge; default budget 30m. Foreground-only (no --background/--wait).");
24
+ case "pursue": return usage("task pursue", "[options] [--] <objective>", PURSUE_SUPPORTED_FLAGS, "Autonomous goal; default budget 45m. Runtime foreground-only.");
25
+ case "swarm": return usage("task swarm", "[options] [--] <objective>", SWARM_SUPPORTED_FLAGS, "Parallel task; default budget 30m. Runtime foreground-only; --write produces a patch.");
26
+ default: return `Usage: companion.sh task <kind> [arguments]. Task kinds: ${TASK_KINDS}. Use task <kind> --help for usage.`;
27
+ }
28
+ case "ask": return usage(command, "[options] [--] [prompt]", ASK_SUPPORTED_FLAGS, "Read-only question; default budget 30m. Backgrounding does not extend the budget.");
29
+ case "review": return usage(command, "[options] [--] [focus]", REVIEW_SUPPORTED_FLAGS, "Read-only review; default budget 30m. Foreground-only (no --background/--wait).");
30
+ case "setup": return usage(command, "[options]", SETUP_SUPPORTED_FLAGS, "Install, check, or remove this host's managed hook, or list configured models.");
31
+ case "status":
32
+ case "result":
33
+ case "cancel":
34
+ return usage(command, "[job-id] [options]", JOB_LOOKUP_SUPPORTED_FLAGS, command === "status" ? "Show job state as JSON." : command === "result" ? "Read a terminal artifact, optionally in a JSON envelope." : "Cancel a running job; without an id, target the repository's latest running job.");
35
+ case "replay": return usage(command, "<job-id>", REPLAY_SUPPORTED_FLAGS, "Replay a completed job's persisted stream log.");
36
+ case "repair-sessions": return usage(command, "[options]", REPAIR_SESSIONS_SUPPORTED_FLAGS, "Preview session metadata repair; --apply writes, --all selects all host-owned repositories.");
37
+ case "worker": return "Usage: companion.sh worker <ask|rescue> <job-id>. Internal detached-worker entry point; launched by ask/rescue, not manually.";
38
+ default: return undefined;
39
+ }
40
+ }
@@ -417,15 +417,23 @@ export function normalizeTopLevelInlineHooks(contents, lineEnding) {
417
417
  entryCount: hooks.length,
418
418
  };
419
419
  }
420
+ // Same defaults as smol-toml 1.9.1 parse(); this walker only locates assignments.
421
+ // tests/runtime/toml-walker-context.test.ts asserts these against the vendored parse().
422
+ export function assignmentParseContext(s) {
423
+ return { s, p: s.charCodeAt(0) === 0xfeff ? 1 : 0, d: 1_000, bi: false, ld: true, uk: 0 };
424
+ }
420
425
  function findTopLevelHooksAssignment(contents) {
421
- let ptr = skipVoid(contents, 0);
422
- while (ptr < contents.length) {
426
+ const ctx = assignmentParseContext(contents);
427
+ skipVoid(ctx);
428
+ while (ctx.p < contents.length) {
423
429
  // A root-level direct assignment cannot resume after entering a table.
424
- if (contents[ptr] === "[")
430
+ if (contents[ctx.p] === "[")
425
431
  return null;
426
- const declarationStart = ptr;
427
- const [key, valueStart] = parseKey(contents, ptr);
428
- const [, valueEnd] = extractValue(contents, valueStart, undefined, 1_000, undefined);
432
+ const declarationStart = ctx.p;
433
+ const key = parseKey(ctx);
434
+ skipVoid(ctx, true, true);
435
+ extractValue(ctx, undefined);
436
+ const valueEnd = ctx.p;
429
437
  if (key.length === 1 && key[0] === "hooks") {
430
438
  const lineStart = contents.lastIndexOf("\n", declarationStart - 1) + 1;
431
439
  const start = /^[\t ]*$/.test(contents.slice(lineStart, declarationStart))
@@ -445,7 +453,7 @@ function findTopLevelHooksAssignment(contents) {
445
453
  }
446
454
  return { start, end, hadLineEnding };
447
455
  }
448
- ptr = skipVoid(contents, valueEnd);
456
+ skipVoid(ctx);
449
457
  }
450
458
  return null;
451
459
  }
@@ -585,14 +593,15 @@ function validateVersionSensitiveEvents(events, version) {
585
593
  return invalidHookSet(`version-sensitive event ${JSON.stringify(first.event)} requires a verified kimi-code version`, first.entry, first.line);
586
594
  }
587
595
  // Exact schemas reviewed at 2.0.0 (1b89e4b0), 2.0.1 (caf7d4e2),
588
- // 2.0.2 (9d07f634), 2.1.0 (52437299), and 2.1.1 (f67e6398):
596
+ // 2.0.2 (9d07f634), 2.1.0 (52437299), 2.1.1 (f67e6398) and 2.2.0 (472192c3):
589
597
  // the additive events and HookDefSchema are unchanged. Require the full
590
598
  // version string AND matching components; no inferred 2.x compatibility.
591
599
  if (version.major === 2 && ((version.minor === 0 && ((version.version === "2.0.0" && version.patch === 0) ||
592
600
  (version.version === "2.0.1" && version.patch === 1) ||
593
601
  (version.version === "2.0.2" && version.patch === 2))) ||
594
602
  (version.minor === 1 && ((version.version === "2.1.0" && version.patch === 0) ||
595
- (version.version === "2.1.1" && version.patch === 1))))) {
603
+ (version.version === "2.1.1" && version.patch === 1))) ||
604
+ (version.minor === 2 && ((version.version === "2.2.0" && version.patch === 0))))) {
596
605
  return { valid: true };
597
606
  }
598
607
  if (version.major !== 0) {
@@ -11,8 +11,11 @@ export class JobStore {
11
11
  let db;
12
12
  try {
13
13
  db = createSqliteAdapter(paths.stateDbPath);
14
- db.pragma("journal_mode = WAL");
14
+ // busy_timeout FIRST: switching the journal mode takes a brief write lock,
15
+ // and a second opener (status/result racing a worker) otherwise fails
16
+ // immediately with SQLITE_BUSY instead of waiting (seen 2026-10-14).
15
17
  db.pragma("busy_timeout = 5000");
18
+ db.pragma("journal_mode = WAL");
16
19
  }
17
20
  catch (error) {
18
21
  try {
@@ -58,6 +61,7 @@ export class JobStore {
58
61
  prompt_digest TEXT NOT NULL,
59
62
  summary TEXT NOT NULL,
60
63
  phase TEXT,
64
+ budget_ms INTEGER,
61
65
  final_output_path TEXT,
62
66
  stream_log_path TEXT NOT NULL,
63
67
  error TEXT
@@ -66,6 +70,14 @@ export class JobStore {
66
70
  if (!tableHasColumn(this.db, "phase")) {
67
71
  this.db.exec(`ALTER TABLE jobs ADD COLUMN phase TEXT;`);
68
72
  }
73
+ if (!tableHasColumn(this.db, "budget_ms")) {
74
+ this.db.exec(`ALTER TABLE jobs ADD COLUMN budget_ms INTEGER;`);
75
+ }
76
+ for (const column of ["kimi_session_id_source", "budget_source"]) {
77
+ if (!tableHasColumn(this.db, column)) {
78
+ this.db.exec("ALTER TABLE jobs ADD COLUMN " + column + " TEXT;");
79
+ }
80
+ }
69
81
  // Additive provenance migration. Existing rows deliberately remain NULL:
70
82
  // v1.9.5 plus kimi-code 0.33/0.34 could have routed to native v2 before the
71
83
  // legacy pin landed, so a blanket historical v1 backfill would invent fact.
@@ -94,6 +106,8 @@ export class JobStore {
94
106
  CREATE INDEX IF NOT EXISTS jobs_repo_type_updated_idx
95
107
  ON jobs (repo_id, command_type, updated_at DESC);
96
108
 
109
+ CREATE INDEX IF NOT EXISTS jobs_session_id_idx ON jobs (kimi_session_id);
110
+
97
111
  CREATE INDEX IF NOT EXISTS jobs_session_idx
98
112
  ON jobs (repo_id, kimi_session_id, updated_at DESC);
99
113
 
@@ -159,17 +173,17 @@ export class JobStore {
159
173
  this.db.run(`
160
174
  INSERT INTO jobs (
161
175
  job_id, repo_id, command_type, created_at, updated_at, cwd, model, thinking,
162
- background, pid, kimi_pid, status, kimi_session_id,
176
+ background, pid, kimi_pid, status, kimi_session_id, kimi_session_id_source, budget_source,
163
177
  operation_kind, intended_engine, observed_engine, kimi_version, system_version,
164
178
  kimi_command, kimi_prefix_args, plan_certification, resumed_from_job_id, safety_profile,
165
- agent_profile, prompt_digest, summary, phase, final_output_path, stream_log_path, error
179
+ agent_profile, prompt_digest, summary, phase, budget_ms, final_output_path, stream_log_path, error
166
180
  )
167
181
  VALUES (
168
182
  @job_id, @repo_id, @command_type, @created_at, @updated_at, @cwd, @model, @thinking,
169
- @background, @pid, @kimi_pid, @status, @kimi_session_id,
183
+ @background, @pid, @kimi_pid, @status, @kimi_session_id, @kimi_session_id_source, @budget_source,
170
184
  @operation_kind, @intended_engine, @observed_engine, @kimi_version, @system_version,
171
185
  @kimi_command, @kimi_prefix_args, @plan_certification, @resumed_from_job_id, @safety_profile,
172
- @agent_profile, @prompt_digest, @summary, @phase, @final_output_path, @stream_log_path, @error
186
+ @agent_profile, @prompt_digest, @summary, @phase, @budget_ms, @final_output_path, @stream_log_path, @error
173
187
  )
174
188
  `, {
175
189
  ...serializeRecord(input),
@@ -194,6 +208,10 @@ export class JobStore {
194
208
  const row = this.db.get("SELECT * FROM jobs WHERE job_id = ?", jobId);
195
209
  return row ? hydrateRow(row) : null;
196
210
  }
211
+ /** Global across repos and terminal states; optionally exclude the current row. */
212
+ hasSessionId(sessionId, exceptJobId = "") {
213
+ return Boolean(this.db.get("SELECT 1 FROM jobs WHERE kimi_session_id = ? AND job_id != ? LIMIT 1", sessionId, exceptJobId));
214
+ }
197
215
  findLatestJob(options) {
198
216
  const conditions = ["repo_id = @repo_id"];
199
217
  const params = {
@@ -243,6 +261,18 @@ export class JobStore {
243
261
  updateRunningJob(jobId, patch) {
244
262
  return this.updateWhere(jobId, patch, "status = 'running'");
245
263
  }
264
+ /** Recheck uniqueness atomically: another worker can claim an ID during the
265
+ * asynchronous directory scan. Ordinary announced/resumed IDs may be shared. */
266
+ claimRecoveredSessionId(jobId, sessionId) {
267
+ this.db.run(`UPDATE jobs
268
+ SET kimi_session_id = @session_id, kimi_session_id_source = 'recovered_session_dir',
269
+ updated_at = @updated_at
270
+ WHERE job_id = @job_id AND status = 'running' AND kimi_session_id IS NULL
271
+ AND NOT EXISTS (SELECT 1 FROM jobs WHERE kimi_session_id = @session_id AND job_id != @job_id)`, { job_id: jobId, session_id: sessionId, updated_at: new Date().toISOString() });
272
+ const job = this.getJob(jobId);
273
+ return job?.status === "running" && job.kimi_session_id === sessionId &&
274
+ job.kimi_session_id_source === "recovered_session_dir" ? job : null;
275
+ }
246
276
  /**
247
277
  * Evidence-only backfill for pre-provenance rows. COALESCE prevents forensic
248
278
  * inspection from overwriting a field already recorded at execution time.
@@ -276,7 +306,7 @@ export class JobStore {
276
306
  return this.updateWhere(jobId, { ...patch, status: "cancelled", pid: null, kimi_pid: null }, "status = 'running'");
277
307
  }
278
308
  static JOBS_COLUMNS = new Set([
279
- "status", "summary", "phase", "pid", "kimi_pid",
309
+ "status", "summary", "phase", "budget_ms", "budget_source", "kimi_session_id_source", "pid", "kimi_pid",
280
310
  "final_output_path", "stream_log_path", "kimi_session_id",
281
311
  "error", "command_type", "prompt_digest", "repo_id",
282
312
  "cwd", "model", "thinking", "background", "agent_profile",
@@ -320,6 +350,9 @@ function serializeRecord(record) {
320
350
  return {
321
351
  ...record,
322
352
  phase: record.phase ?? null,
353
+ budget_ms: record.budget_ms ?? null,
354
+ budget_source: record.budget_source ?? null,
355
+ kimi_session_id_source: record.kimi_session_id_source ?? null,
323
356
  operation_kind: record.operation_kind ?? null,
324
357
  intended_engine: record.intended_engine ?? null,
325
358
  observed_engine: record.observed_engine ?? null,
@@ -557,3 +590,7 @@ export function withJobStoreSync(paths, fn) {
557
590
  store?.close();
558
591
  }
559
592
  }
593
+ /** Old non-null budgets have unknown provenance; never invent explicit intent. */
594
+ export function renderBudgetSource(job) {
595
+ return job.budget_source ?? (job.budget_ms === null ? "default" : "unknown");
596
+ }
@@ -1,6 +1,6 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import { RuntimeError } from "./errors.js";
3
- import { renderTerminalJobArtifact, writeArtifact } from "./render.js";
3
+ import { formatBudgetDuration, renderTerminalJobArtifact, writeArtifact } from "./render.js";
4
4
  export function digestPrompt(prompt) {
5
5
  return createHash("sha256").update(prompt).digest("hex");
6
6
  }
@@ -30,10 +30,14 @@ export function normalizeJobError(error) {
30
30
  };
31
31
  }
32
32
  export async function markJobFailed(store, paths, job, error, summary = "Job failed.", options) {
33
+ if (options?.partialOutput) {
34
+ const label = job.command_type[0].toUpperCase() + job.command_type.slice(1);
35
+ summary = `${label} timed out after ${formatBudgetDuration(options.partialOutput.budgetMs)}; partial output captured (${options.partialOutput.text.trim().length} chars).`;
36
+ }
33
37
  const normalized = normalizeJobError(error);
34
38
  const phase = options?.phase;
35
39
  const artifactJob = {
36
- ...job,
40
+ ...(store.getJob(job.job_id) ?? job),
37
41
  status: "failed",
38
42
  summary,
39
43
  error: normalized,
@@ -42,7 +46,7 @@ export async function markJobFailed(store, paths, job, error, summary = "Job fai
42
46
  kimi_pid: null,
43
47
  ...(phase !== undefined ? { phase } : {}),
44
48
  };
45
- const artifactPath = await writeArtifact(paths, artifactJob, renderTerminalJobArtifact(artifactJob));
49
+ const artifactPath = await writeArtifact(paths, artifactJob, renderTerminalJobArtifact(artifactJob, options?.partialOutput));
46
50
  return (store.markFailed(job.job_id, {
47
51
  summary,
48
52
  error: normalized,
@@ -38,6 +38,7 @@ export const NATIVE_V2_CERTIFIED_VERSIONS = Object.freeze([
38
38
  "2.0.2",
39
39
  "2.1.0",
40
40
  "2.1.1",
41
+ "2.2.0",
41
42
  ]);
42
43
  /** Per-operation native-v2 certification. One green operation never certifies another. */
43
44
  export const NATIVE_V2_CERTIFIED = new Map([
@@ -1,9 +1,35 @@
1
1
  import { RuntimeError, formatError } from "./errors.js";
2
+ const BUDGET_CEILING_MS = 24 * 60 * 60 * 1_000;
3
+ function formatBudgetMs(ms) {
4
+ if (ms % 3_600_000 === 0)
5
+ return `${ms / 3_600_000}h`;
6
+ if (ms % 60_000 === 0)
7
+ return `${ms / 60_000}m`;
8
+ return `${Math.ceil(ms / 1_000)}s`;
9
+ }
10
+ /**
11
+ * Suggest a concrete next budget from the one that just expired: double it,
12
+ * capped at the 24h ceiling. At the ceiling there is nothing left to raise,
13
+ * so the only honest advice is to narrow the prompt.
14
+ */
15
+ function budgetAdvice(budgetMs) {
16
+ if (budgetMs === undefined || !Number.isFinite(budgetMs) || budgetMs <= 0)
17
+ return "raise the budget (`--budget <dur>`, at most 24h)";
18
+ if (budgetMs >= BUDGET_CEILING_MS)
19
+ return "note the budget is already at the 24h ceiling, so narrowing the prompt is the only remaining lever";
20
+ return `raise the budget (e.g. \`--budget ${formatBudgetMs(Math.min(budgetMs * 2, BUDGET_CEILING_MS))}\`)`;
21
+ }
2
22
  export function classifyManagedCommandFailure(error, commandType, jobId, options) {
3
23
  const classification = classifyKimiAvailability(error);
4
24
  if (!classification) {
5
25
  return error instanceof Error ? error : new Error(String(error));
6
26
  }
27
+ if (classification.kind === "response_timeout" && commandType !== "review_gate" && options?.background !== undefined) {
28
+ const foregroundAdvice = !options?.background && (commandType === "ask" || commandType === "rescue")
29
+ ? " Or run with --background so the caller's shell timeout is not the limit."
30
+ : "";
31
+ classification.nextStep = `Narrow the prompt or ${budgetAdvice(options.budgetMs)}. Partial output, if any, is in the artifact (\`result ${jobId}\`).${foregroundAdvice}`;
32
+ }
7
33
  const label = formatCommandLabel(commandType);
8
34
  const stage = options?.preserveStage && error instanceof RuntimeError
9
35
  ? error.stage
@@ -18,6 +44,14 @@ export function classifyManagedCommandFailure(error, commandType, jobId, options
18
44
  const details = { availability: classification.kind };
19
45
  if (error instanceof RuntimeError) {
20
46
  details.cause_code = error.code;
47
+ if (error.code === "RESPONSE_TIMEOUT") {
48
+ for (const key of ["budget_ms", "record_count", "partial_output_chars", "aborted", "settled_post_abort"]) {
49
+ if (error.details[key] !== undefined)
50
+ details[key] = error.details[key];
51
+ }
52
+ if (details.budget_ms === undefined && options?.budgetMs !== undefined)
53
+ details.budget_ms = options.budgetMs;
54
+ }
21
55
  }
22
56
  if (error instanceof Error && error.message.length > 0) {
23
57
  details.cause_message = truncateCauseMessageForDetails(error.message);
@@ -170,7 +204,7 @@ function classifyKimiAvailability(error) {
170
204
  return {
171
205
  kind: "response_timeout",
172
206
  summary: "Kimi started and accepted the prompt but never returned a final response.",
173
- nextStep: "Reduce the prompt scope (or for /kimi:ask and /kimi:rescue, retry with --background to detach). If the response still hangs after a fresh run, check local Kimi version and report upstream.",
207
+ nextStep: "Narrow the prompt or raise the budget (`--budget <dur>`, at most 24h). Check the job artifact (`result <jobId>`) for any captured partial output. Backgrounding does not extend the runtime budget.",
174
208
  runtimeProbe: "ok",
175
209
  authProbe: "ok",
176
210
  };
@@ -12,23 +12,23 @@
12
12
  // kimi-code spends real wallclock on extended reasoning before emitting
13
13
  // the first assistant record; pre-alpha.4 budgets (300s/600s) were sized
14
14
  // against the non-thinking path and timed out in production smoke
15
- // testing under default flags. Raised to 900/1800/1800 so a real
16
- // thinking-on review/rescue session has headroom; review-gate stays at
15
+ // testing under default flags. Defaults are now 1800/1800/2700 seconds
16
+ // for ask/review/rescue to allow repo-wide work; review-gate stays at
17
17
  // 8s because it fires inside Claude Code's Stop hook and is forced to
18
18
  // thinking-off in its own caller.
19
19
  /** Ask budget. Conversational; thinking-on free-form reasoning. */
20
- export const KIMI_ASK_PROMPT_TIMEOUT_MS = 900_000;
20
+ export const KIMI_ASK_PROMPT_TIMEOUT_MS = 1_800_000; // 30 minutes
21
21
  /** Review/challenge budget. Thinking-on single-turn analysis over the workspace. */
22
22
  export const KIMI_REVIEW_PROMPT_TIMEOUT_MS = 1_800_000;
23
23
  /** Rescue budget. Thinking-on multi-step apply/test/verify loops. */
24
- export const KIMI_RESCUE_PROMPT_TIMEOUT_MS = 1_800_000;
24
+ export const KIMI_RESCUE_PROMPT_TIMEOUT_MS = 2_700_000; // 45 minutes
25
25
  /**
26
26
  * Pursue (autonomous goal mode) DEFAULT wall-clock ceiling when the user
27
27
  * passes no `--budget`. This is the ONLY hard bound on an autonomous goal —
28
28
  * headless goal create sets no token/turn budget (only the model, via
29
- * SetGoalBudget, or this AbortController stops the loop). Larger than the
30
- * single-turn rescue budget (a goal spans multiple continuation turns) but
31
- * deliberately finite. Overridable per-job via `--budget` (parsed to ms in
29
+ * SetGoalBudget, or this AbortController stops the loop). The same duration as
30
+ * the single-turn rescue default, but a safety ceiling rather than a patience
31
+ * knob. Deliberately finite. Overridable per-job via `--budget` (parsed to ms in
32
32
  * parsePursueArgs). See runtime/commands/pursue.ts and docs/safety.md.
33
33
  */
34
34
  export const KIMI_PURSUE_DEFAULT_BUDGET_MS = 2_700_000; // 45 minutes
@@ -21,8 +21,13 @@ 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
- const ASK_SUPPORTED_FLAGS = "-m/--model <name>, --background, --wait, --fresh, -r, --resume <job-id|session-id>";
25
- const RESCUE_SUPPORTED_FLAGS = "-m/--model <name>, --background, --wait, --fresh, -r, --resume <job-id|session-id>";
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>";
27
+ export const JOB_LOOKUP_SUPPORTED_FLAGS = "--type <review|challenge|rescue|review_gate|ask>, --json";
28
+ export const SETUP_SUPPORTED_FLAGS = "--check, --uninstall, --all (with --uninstall), --enable-review-gate, --disable-review-gate, --models [--json] (without other setup flags)";
29
+ export const REPAIR_SESSIONS_SUPPORTED_FLAGS = "--apply, --all";
30
+ export const REPLAY_SUPPORTED_FLAGS = "none (one job-id required)";
26
31
  // Thinking-on is always-on for user-facing commands in v1.0. review-gate
27
32
  // pins thinking=false via the cli-client options bag instead of an argv
28
33
  // flag (see runtime/cli-client.ts::CliClientOptions). The parser rejects
@@ -39,8 +44,8 @@ function isRemovedThinkingFlag(token) {
39
44
  token.startsWith("--thinking=") ||
40
45
  token.startsWith("--no-thinking="));
41
46
  }
42
- const PURSUE_SUPPORTED_FLAGS = "-m/--model <name>, --budget <30m|1h|90s>, --turns <N>";
43
- const SWARM_SUPPORTED_FLAGS = "-m/--model <name>, --budget <30m|1h|90s>, --cap <N>, --max-concurrency <N>, --write";
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";
44
49
  /**
45
50
  * Parser for /kimi:swarm (read-only parallel fan-out). Foreground-only:
46
51
  * no --background/--fresh/--resume. Trailing tokens are the objective.
@@ -85,7 +90,7 @@ export function parseSwarmArgs(argv) {
85
90
  if (!value || value.startsWith("-")) {
86
91
  throw new RuntimeError("INVALID_ARGS", "--budget requires a duration (e.g. 30m, 1h, 90s).", "swarm.parse");
87
92
  }
88
- budgetMs = parseDurationMs(value, "--budget");
93
+ budgetMs = parseDurationMs(value, "--budget", "swarm.parse", "safety");
89
94
  index += 1;
90
95
  break;
91
96
  }
@@ -154,20 +159,23 @@ export const MAX_DURATION_MS = 24 * 60 * 60 * 1_000;
154
159
  * into milliseconds. Throws INVALID_ARGS on a malformed, non-positive, or
155
160
  * absurdly large value (> {@link MAX_DURATION_MS}).
156
161
  */
157
- export function parseDurationMs(token, flag) {
162
+ export function parseDurationMs(token, flag, stage, budgetKind) {
163
+ const limitReason = budgetKind === "safety"
164
+ ? "the budget is a safety ceiling, not a scheduler"
165
+ : "the budget is a response-time limit; a timed-out job is still a safe failure";
158
166
  const match = /^(\d+)(s|m|h)?$/.exec(token.trim());
159
167
  if (!match) {
160
- throw new RuntimeError("INVALID_ARGS", `${flag} expects a duration like 30m, 1h, or 90s (bare number = minutes); got "${token}".`, "pursue.parse");
168
+ throw new RuntimeError("INVALID_ARGS", `${flag} expects a duration like 30m, 1h, or 90s (bare number = minutes); got "${token}".`, stage);
161
169
  }
162
170
  const value = Number(match[1]);
163
171
  const unit = match[2] ?? "m";
164
172
  if (!Number.isFinite(value) || value <= 0) {
165
- throw new RuntimeError("INVALID_ARGS", `${flag} must be a positive duration.`, "pursue.parse");
173
+ throw new RuntimeError("INVALID_ARGS", `${flag} must be a positive duration.`, stage);
166
174
  }
167
175
  const unitMs = unit === "s" ? 1_000 : unit === "h" ? 3_600_000 : 60_000;
168
176
  const ms = value * unitMs;
169
177
  if (ms > MAX_DURATION_MS) {
170
- throw new RuntimeError("INVALID_ARGS", `${flag} must be at most 24h (got "${token}"); the budget is a safety ceiling, not a scheduler.`, "pursue.parse");
178
+ throw new RuntimeError("INVALID_ARGS", `${flag} must be at most 24h (got "${token}"); ${limitReason}.`, stage);
171
179
  }
172
180
  return ms;
173
181
  }
@@ -211,7 +219,7 @@ export function parsePursueArgs(argv) {
211
219
  if (!value || value.startsWith("-")) {
212
220
  throw new RuntimeError("INVALID_ARGS", "--budget requires a duration (e.g. 30m, 1h, 90s).", "pursue.parse");
213
221
  }
214
- budgetMs = parseDurationMs(value, "--budget");
222
+ budgetMs = parseDurationMs(value, "--budget", "pursue.parse", "safety");
215
223
  index += 1;
216
224
  break;
217
225
  }
@@ -248,7 +256,38 @@ export function parsePursueArgs(argv) {
248
256
  objective: trailingTokens?.join(" "),
249
257
  };
250
258
  }
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"]);
261
+ /**
262
+ * `--help` / `-h` counts only inside the leading flag region: before `--`
263
+ * and before the first prose token. The value of a value-taking flag is
264
+ * skipped, so `-m kimi -h` is still a help request (`kimi` is a value, not
265
+ * prose) and `--budget --help` asks for help rather than failing on a bad
266
+ * duration. Once prose starts (`ask what does du -h print`), nothing after it
267
+ * is a flag — the whole remainder is the prompt.
268
+ */
269
+ export function hasHelpFlag(argv) {
270
+ for (let index = 0; index < argv.length; index += 1) {
271
+ const token = argv[index];
272
+ if (token === "--")
273
+ return false;
274
+ if (token === "--help" || token === "-h")
275
+ return true;
276
+ if (!token.startsWith("-"))
277
+ return false;
278
+ if (VALUE_TAKING_FLAGS.has(token)) {
279
+ const value = argv[index + 1];
280
+ if (value === "--help" || value === "-h")
281
+ return true;
282
+ index += 1;
283
+ }
284
+ }
285
+ return false;
286
+ }
251
287
  export function parseAskArgs(argv) {
288
+ if (hasHelpFlag(argv))
289
+ return { help: true, background: false, wait: false, fresh: false, resume: false };
290
+ let budgetMs;
252
291
  let model;
253
292
  // Thinking is on by default across ask/review/challenge/rescue. The
254
293
  // parser rejects --thinking / --no-thinking; review-gate's runtime
@@ -272,6 +311,15 @@ export function parseAskArgs(argv) {
272
311
  break;
273
312
  }
274
313
  switch (token) {
314
+ case "--budget": {
315
+ const value = argv[index + 1];
316
+ if (!value || value.startsWith("-")) {
317
+ throw new RuntimeError("INVALID_ARGS", "--budget requires a duration (e.g. 30m, 1h, 90s).", "ask.parse");
318
+ }
319
+ budgetMs = parseDurationMs(value, "--budget", "ask.parse", "response");
320
+ index += 1;
321
+ break;
322
+ }
275
323
  case "-m":
276
324
  case "--model": {
277
325
  const value = argv[index + 1];
@@ -327,6 +375,7 @@ export function parseAskArgs(argv) {
327
375
  throw new RuntimeError("INVALID_ARGS", "--resume only accepts a job-id or session-id. Use -r to resume the latest ask session with a prompt.", "ask.parse");
328
376
  }
329
377
  return {
378
+ budgetMs,
330
379
  background,
331
380
  wait,
332
381
  fresh,
@@ -338,8 +387,9 @@ export function parseAskArgs(argv) {
338
387
  };
339
388
  }
340
389
  export function parseReviewArgs(argv, commandName = "review") {
341
- const parsed = parseKnownFlags(argv, new Set(["--base", "--background", "--wait", "-m", "--model"]), commandName);
390
+ const parsed = parseKnownFlags(argv, new Set(["--budget", "--base", "--background", "--wait", "-m", "--model"]), commandName);
342
391
  return {
392
+ budgetMs: parsed.budgetMs,
343
393
  base: parsed.base,
344
394
  background: parsed.background,
345
395
  wait: parsed.wait,
@@ -349,6 +399,9 @@ export function parseReviewArgs(argv, commandName = "review") {
349
399
  };
350
400
  }
351
401
  export function parseRescueArgs(argv) {
402
+ if (hasHelpFlag(argv))
403
+ return { help: true, background: false, wait: false, fresh: false, resume: false };
404
+ let budgetMs;
352
405
  let model;
353
406
  let thinking = true;
354
407
  let background = false;
@@ -369,6 +422,15 @@ export function parseRescueArgs(argv) {
369
422
  break;
370
423
  }
371
424
  switch (token) {
425
+ case "--budget": {
426
+ const value = argv[index + 1];
427
+ if (!value || value.startsWith("-")) {
428
+ throw new RuntimeError("INVALID_ARGS", "--budget requires a duration (e.g. 30m, 1h, 90s).", "rescue.parse");
429
+ }
430
+ budgetMs = parseDurationMs(value, "--budget", "rescue.parse", "response");
431
+ index += 1;
432
+ break;
433
+ }
372
434
  case "-m":
373
435
  case "--model": {
374
436
  const value = argv[index + 1];
@@ -424,6 +486,7 @@ export function parseRescueArgs(argv) {
424
486
  throw new RuntimeError("INVALID_ARGS", "--resume only accepts a job-id or session-id. Use -r to resume the latest rescue session with a prompt.", "rescue.parse");
425
487
  }
426
488
  return {
489
+ budgetMs,
427
490
  background,
428
491
  wait,
429
492
  fresh,
@@ -454,7 +517,7 @@ export function parseJobLookupArgs(argv) {
454
517
  continue;
455
518
  }
456
519
  if (token.startsWith("-")) {
457
- throw new RuntimeError("INVALID_ARGS", `Unknown flag ${token}. Supported flags: --type <review|challenge|rescue|review_gate|ask>, --json.`, "args.parse");
520
+ throw new RuntimeError("INVALID_ARGS", `Unknown flag ${token}. Supported flags: ${JOB_LOOKUP_SUPPORTED_FLAGS}.`, "args.parse");
458
521
  }
459
522
  if (jobId) {
460
523
  throw new RuntimeError("INVALID_ARGS", "Only one optional job id may be supplied.", "args.parse");
@@ -475,6 +538,7 @@ function isManagedCommandType(value) {
475
538
  value === "ask");
476
539
  }
477
540
  function parseKnownFlags(argv, knownFlags, commandName) {
541
+ let budgetMs;
478
542
  let model;
479
543
  let thinking = true;
480
544
  let base;
@@ -497,12 +561,21 @@ function parseKnownFlags(argv, knownFlags, commandName) {
497
561
  // slurping them into focus text produced a bloated prompt that
498
562
  // looked like a hang. Hard-fail with the supported list inline so
499
563
  // the error message itself is the correction.
500
- throw new RuntimeError("INVALID_ARGS", `Unknown flag ${token} for ${commandName}. Supported flags: --base <ref>, -m/--model <name>. Use \`--\` before flag-shaped focus text to pass it through.`, "args.parse");
564
+ throw new RuntimeError("INVALID_ARGS", `Unknown flag ${token} for ${commandName}. Supported flags: ${REVIEW_SUPPORTED_FLAGS}. Use \`--\` before flag-shaped focus text to pass it through.`, "args.parse");
501
565
  }
502
566
  trailingTokens = argv.slice(index);
503
567
  break;
504
568
  }
505
569
  switch (token) {
570
+ case "--budget": {
571
+ const value = argv[index + 1];
572
+ if (!value || value.startsWith("-")) {
573
+ throw new RuntimeError("INVALID_ARGS", "--budget requires a duration (e.g. 30m, 1h, 90s).", `${commandName}.parse`);
574
+ }
575
+ budgetMs = parseDurationMs(value, "--budget", `${commandName}.parse`, "response");
576
+ index += 1;
577
+ break;
578
+ }
506
579
  case "-m":
507
580
  case "--model": {
508
581
  const value = argv[index + 1];
@@ -543,6 +616,7 @@ function parseKnownFlags(argv, knownFlags, commandName) {
543
616
  }
544
617
  }
545
618
  return {
619
+ budgetMs,
546
620
  base,
547
621
  background,
548
622
  wait,
@@ -126,7 +126,16 @@ export function renderReviewGateArtifact(job, output) {
126
126
  }
127
127
  return `${lines.join("\n")}\n`;
128
128
  }
129
- export function renderTerminalJobArtifact(job) {
129
+ export function formatBudgetDuration(ms) {
130
+ if (ms % 3_600_000 === 0)
131
+ return `${ms / 3_600_000}h`;
132
+ if (ms % 60_000 === 0)
133
+ return `${ms / 60_000}m`;
134
+ if (ms % 1_000 === 0)
135
+ return `${ms / 1_000}s`;
136
+ return `${ms}ms`;
137
+ }
138
+ export function renderTerminalJobArtifact(job, partialOutput) {
130
139
  const lines = [
131
140
  `# ${capitalize(job.status)} Job`,
132
141
  "",
@@ -149,6 +158,15 @@ export function renderTerminalJobArtifact(job) {
149
158
  }
150
159
  }
151
160
  }
161
+ if (partialOutput) {
162
+ lines.push("", `## Partial output (budget exhausted after ${formatBudgetDuration(partialOutput.budgetMs)} — NOT a final answer)`, "", partialOutput.text.trim()
163
+ ? partialOutput.text
164
+ : partialOutput.recordCount === 0
165
+ ? "No stream records arrived before the budget expired (the model had not produced any output yet)."
166
+ : partialOutput.toolCalls
167
+ ? "No assistant prose was emitted before the budget (tool calls only)."
168
+ : "No assistant prose was emitted before the budget.");
169
+ }
152
170
  return lines.join("\n");
153
171
  }
154
172
  function capitalize(value) {