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
@@ -84,7 +84,9 @@ export async function runCliPrompt(opts) {
84
84
  });
85
85
  }
86
86
  let child;
87
+ let spawnedAt;
87
88
  try {
89
+ spawnedAt = Date.now();
88
90
  child = spawn(opts.command, args, {
89
91
  cwd: opts.cwd,
90
92
  env,
@@ -329,7 +331,7 @@ export async function runCliPrompt(opts) {
329
331
  return;
330
332
  }
331
333
  if (kind === "resolve")
332
- resolve(payload);
334
+ resolve({ ...payload, settledAt: Date.now() });
333
335
  else
334
336
  reject(payload);
335
337
  });
@@ -580,6 +582,8 @@ export async function runCliPrompt(opts) {
580
582
  // The result itself is in memory; diagnostics are best-effort.
581
583
  await raceWithTimeout(logChain, LOG_DRAIN_TIMEOUT_MS);
582
584
  settle("resolve", {
585
+ spawnedAt,
586
+ settledAt: 0, // Filled at settlement, after the teardown barrier.
583
587
  sessionId,
584
588
  goalSummary: announcedGoalSummary,
585
589
  systemVersion: announcedSystemVersion,
@@ -656,6 +660,27 @@ export function requireSessionId(result, context) {
656
660
  },
657
661
  });
658
662
  }
663
+ /** Settled timeout data; stream records are deliberately non-serializable.
664
+ * Without a partial result (teardown rejection), aborted means abort was issued,
665
+ * not that the process was observed aborting. */
666
+ export class ResponseTimeoutError extends RuntimeError {
667
+ constructor(budgetMs, stage, commandLabel, partialResult, cause) {
668
+ super("RESPONSE_TIMEOUT", `${stage} timed out after ${budgetMs}ms.`, stage, {
669
+ ...(cause instanceof Error ? { cause } : {}),
670
+ details: {
671
+ budget_ms: budgetMs,
672
+ command_label: commandLabel ?? null,
673
+ record_count: partialResult?.records.length ?? 0,
674
+ partial_output_chars: partialResult?.records.reduce((count, record) => count + (record.role === "assistant" && typeof record.content === "string" ? record.content.length : 0), 0) ?? 0,
675
+ aborted: partialResult?.aborted ?? true,
676
+ ...(partialResult ? { settled_post_abort: true } : {}),
677
+ },
678
+ });
679
+ this.name = "ResponseTimeoutError";
680
+ // Never serialize stream records into job error details or error envelopes.
681
+ Object.defineProperty(this, "partialResult", { value: partialResult, enumerable: false });
682
+ }
683
+ }
659
684
  /**
660
685
  * Run `kimi -p` under a budget that also tells the subprocess to die
661
686
  * when the budget expires.
@@ -682,7 +707,8 @@ export function requireSessionId(result, context) {
682
707
  * subprocess/tree teardown, then reject with
683
708
  * `RuntimeError("RESPONSE_TIMEOUT", …)` matching the legacy
684
709
  * `withTimeout` code shape.
685
- * - Result success path is unchanged from `runCliPrompt`.
710
+ * - A clean non-aborted exit-0 result wins same-tick budget expiry.
711
+ * Otherwise a settled result on timeout is carried as partial output.
686
712
  */
687
713
  export async function runCliPromptWithBudget(opts, budgetMs, stage) {
688
714
  const controller = new AbortController();
@@ -698,12 +724,7 @@ export async function runCliPromptWithBudget(opts, budgetMs, stage) {
698
724
  controller.abort();
699
725
  }, budgetMs);
700
726
  timer.unref();
701
- const timeoutError = (raceDetected = false) => new RuntimeError("RESPONSE_TIMEOUT", `${stage} timed out after ${budgetMs}ms${raceDetected ? " (race detected post-result)" : ""}.`, stage, {
702
- details: {
703
- budget_ms: budgetMs,
704
- command_label: opts.commandLabel ?? null,
705
- },
706
- });
727
+ const timeoutError = (partialResult, cause) => new ResponseTimeoutError(budgetMs, stage, opts.commandLabel, partialResult, cause);
707
728
  try {
708
729
  let result;
709
730
  try {
@@ -714,13 +735,13 @@ export async function runCliPromptWithBudget(opts, budgetMs, stage) {
714
735
  }
715
736
  catch (error) {
716
737
  if (timeoutFired)
717
- throw timeoutError();
738
+ throw timeoutError(undefined, error);
718
739
  throw error;
719
740
  }
720
- if (timeoutFired) {
721
- // The timer and close can become runnable in the same event-loop turn.
722
- // Teardown has still completed because runCliPrompt settled first.
723
- throw timeoutError(true);
741
+ if (timeoutFired && (result.aborted || result.exitCode !== 0)) {
742
+ // A clean, non-aborted result won the settle/timeout boundary and is success.
743
+ // Budget aborts normally resolve with records, after owned teardown settles.
744
+ throw timeoutError(result);
724
745
  }
725
746
  return result;
726
747
  }
@@ -770,6 +791,8 @@ function buildArgs(opts) {
770
791
  }
771
792
  function buildEnv(opts) {
772
793
  const env = { ...opts.env };
794
+ // 2.2.0 audit: ambient trust starts project MCP stdio servers outside PreToolUse.
795
+ delete env.KIMI_CODE_TRUST_WORKSPACE;
773
796
  // kimi-code 0.33.0 inverted its CLI engine default: an unflagged `kimi -p`
774
797
  // now selects native agent-core-v2, while this upstream-documented flag pins
775
798
  // the legacy-v1 path. Native v2 still has a final-allow plan-file path that
@@ -1,3 +1,4 @@
1
+ import { companionHelp } from "../help.js";
1
2
  import { randomUUID } from "node:crypto";
2
3
  import path from "node:path";
3
4
  import { createCliCancellationHandlers } from "../cli-cancellation.js";
@@ -16,7 +17,7 @@ import { readArtifact, renderManagedJobOutput, writeArtifact } from "../render.j
16
17
  import { classifyManagedCommandFailure } from "../kimi-errors.js";
17
18
  import { startBackgroundJob } from "../background-spawn.js";
18
19
  import { hookRefusalDetails, hookRefusalRetryProtocol, maybeWarnHookMissing, verifyHookInstalled, } from "../hooks/install.js";
19
- import { assertCliResultSuccess, reassembleProseFromRecords, warnIfSessionIdMissing } from "./cli-helpers.js";
20
+ import { warnInheritedBudget, captureTimeoutPartialOutput, assertCliResultSuccess, reassembleProseFromRecords, warnIfSessionIdMissing } from "./cli-helpers.js";
20
21
  import { buildKimiSessionTitle, syncKimiSessionTitle } from "../session-title.js";
21
22
  // v1.0 cutover note (PR 2):
22
23
  //
@@ -46,6 +47,8 @@ const ASK_PROMPT_PREFIX = [
46
47
  ].join("\n");
47
48
  export async function runAsk(argv, context) {
48
49
  const parsed = parseAskArgs(argv);
50
+ if (parsed.help)
51
+ return companionHelp(["ask", ...argv]);
49
52
  if (context.env.KIMI_PLUGIN_CC_SKIP_HOOK_CHECK !== "1") {
50
53
  await requireAskHookInstalled(context);
51
54
  }
@@ -76,10 +79,14 @@ export async function runAsk(argv, context) {
76
79
  model: parsed.model ?? null,
77
80
  thinking: parsed.thinking ?? null,
78
81
  background: parsed.background,
82
+ budget_ms: parsed.budgetMs ?? (sessionResolution.sourceJob?.operation_kind === "ask" ? sessionResolution.sourceJob.budget_ms : null) ?? null,
83
+ budget_source: parsed.budgetMs !== undefined ? "explicit"
84
+ : sessionResolution.sourceJob?.operation_kind === "ask" && sessionResolution.sourceJob.budget_ms !== null ? "inherited" : "default",
79
85
  pid: parsed.background ? null : process.pid,
80
86
  kimi_pid: null,
81
87
  status: "running",
82
88
  kimi_session_id: sessionResolution.kimiSessionId,
89
+ kimi_session_id_source: sessionResolution.sourceJob?.kimi_session_id_source ?? null,
83
90
  ...persistedExecutionPlanFields(executionPlan),
84
91
  agent_profile: ASK_AGENT_PROFILE_PLACEHOLDER,
85
92
  prompt_digest: digestPrompt(askPrompt),
@@ -101,6 +108,7 @@ export async function runAsk(argv, context) {
101
108
  await markJobFailed(store, paths, job, classified, askConfig.cancellation.failedSummary, { phase: "failed" });
102
109
  throw classified;
103
110
  }
111
+ warnInheritedBudget(job, context.stderr);
104
112
  if (parsed.background) {
105
113
  return startBackgroundJob(job, askPrompt, context, paths, {
106
114
  workerKind: "ask",
@@ -169,7 +177,7 @@ export async function executeAskJob(jobId, prompt, context, options) {
169
177
  resumeSessionId: job.kimi_session_id ?? undefined,
170
178
  logPath: job.stream_log_path,
171
179
  signal: handlers.signal,
172
- }, KIMI_ASK_PROMPT_TIMEOUT_MS, "ask.prompt");
180
+ }, job.budget_ms ?? KIMI_ASK_PROMPT_TIMEOUT_MS, "ask.prompt");
173
181
  store.updateRunningJob(job.job_id, observedExecutionFields(result));
174
182
  if (handlers.cancelling) {
175
183
  throw new RuntimeError(cancel.errorCodes.cancelled, cancel.cancelMessages.afterPrompt, "ask.runtime");
@@ -180,12 +188,11 @@ export async function executeAskJob(jobId, prompt, context, options) {
180
188
  // sessions it should match the input, but capture anyway in case
181
189
  // kimi-code mints a new id on each turn.
182
190
  if (result.sessionId !== undefined &&
183
- result.sessionId.length > 0 &&
184
- result.sessionId !== job.kimi_session_id) {
191
+ result.sessionId.length > 0) {
185
192
  // length>0 guard: empty-string capture would poison the row. The
186
193
  // warning helper's null-ish check would also fire but only after
187
194
  // the bad write. (Kimi alpha.4 challenge finding #3.)
188
- store.updateRunningJob(job.job_id, { kimi_session_id: result.sessionId });
195
+ store.updateRunningJob(job.job_id, { kimi_session_id_source: "announced", kimi_session_id: result.sessionId });
189
196
  }
190
197
  await syncKimiSessionTitle({
191
198
  promptText: prompt,
@@ -215,6 +222,9 @@ export async function executeAskJob(jobId, prompt, context, options) {
215
222
  }) ?? job);
216
223
  }
217
224
  catch (error) {
225
+ const classified = error instanceof RuntimeError && error.stage === "ask.hook-check" ? error
226
+ : classifyManagedCommandFailure(error, "ask", job.job_id, { background: job.background, budgetMs: job.budget_ms ?? KIMI_ASK_PROMPT_TIMEOUT_MS });
227
+ const partialOutput = await captureTimeoutPartialOutput(classified, store, job, context.env, handlers?.signal);
218
228
  if (handlers?.cancelling) {
219
229
  const cancelledError = new RuntimeError(cancel.errorCodes.cancelled, cancel.cancelMessages.default, "ask.runtime", error instanceof Error ? { cause: error } : undefined);
220
230
  return await markJobCancelled(store, paths, job, cancel.cancelledSummary, cancelledError, { phase: "cancelled" });
@@ -227,8 +237,7 @@ export async function executeAskJob(jobId, prompt, context, options) {
227
237
  await markJobFailed(store, paths, job, error, cancel.failedSummary, { phase: "failed" });
228
238
  throw error;
229
239
  }
230
- const classified = classifyManagedCommandFailure(error, "ask", job.job_id);
231
- await markJobFailed(store, paths, job, classified, cancel.failedSummary, { phase: "failed" });
240
+ await markJobFailed(store, paths, job, classified, cancel.failedSummary, { phase: "failed", partialOutput });
232
241
  throw classified;
233
242
  }
234
243
  finally {
@@ -1,4 +1,9 @@
1
1
  import { RuntimeError } from "../errors.js";
2
+ import { ResponseTimeoutError } from "../cli-client.js";
3
+ import { observedExecutionFields } from "../kimi-engine.js";
4
+ import { resolveKimiHome } from "../kimi-home.js";
5
+ import { recoverSessionIdFromStore } from "../session-recovery.js";
6
+ import { formatBudgetDuration } from "../render.js";
2
7
  /**
3
8
  * Reassemble prose final text from cli-client records.
4
9
  *
@@ -110,3 +115,78 @@ export function assertCliResultSuccess(result, stage) {
110
115
  });
111
116
  }
112
117
  }
118
+ /** Timeout-only capture. Cancellation takes precedence, including while the
119
+ * asynchronous store scan is in flight. No title sync on failure. */
120
+ export async function captureTimeoutPartialOutput(classified, store, job, env, cancellationSignal) {
121
+ if (cancellationSignal?.aborted)
122
+ return undefined;
123
+ const timeout = classified.cause;
124
+ if (!(timeout instanceof ResponseTimeoutError) || !timeout.partialResult)
125
+ return undefined;
126
+ const result = timeout.partialResult;
127
+ let sessionId = result.sessionId;
128
+ let source = sessionId ? "announced" : null;
129
+ const details = classified instanceof RuntimeError ? classified.details : timeout.details;
130
+ details.kimi_session_id_source = job.kimi_session_id_source;
131
+ // The persisted plan is plugin-owned. Recovery never runs on cancellation,
132
+ // v1, successful turns, teardown rejection, or an already-known resume id.
133
+ if (!sessionId && !job.kimi_session_id && job.intended_engine === "native-v2") {
134
+ // Without the system.version marker the row has no proven engine, and
135
+ // resume would refuse the id as lineage-unknown; do not advertise one.
136
+ if (result.observedEngine === null) {
137
+ details.session_recovery = "engine_unobserved";
138
+ }
139
+ else {
140
+ const recovered = await recoverSessionIdFromStore({
141
+ kimiHome: resolveKimiHome(env, job.cwd), cwd: job.cwd,
142
+ spawnedAt: result.spawnedAt, settledAt: result.settledAt, promptDigest: job.prompt_digest,
143
+ store, jobId: job.job_id,
144
+ });
145
+ sessionId = recovered.sessionId;
146
+ if (sessionId)
147
+ source = "recovered_session_dir";
148
+ else
149
+ details.session_recovery = recovered.reason;
150
+ }
151
+ }
152
+ if (cancellationSignal?.aborted)
153
+ return undefined;
154
+ // Best-effort metadata only. The row is still `running`, and a session id
155
+ // written here can collide with `jobs_running_session_idx` when a resume of
156
+ // the same session was started while this job ran. A store error must never
157
+ // prevent markJobFailed from running — the partial artifact matters more
158
+ // than the metadata, so swallow and fall through.
159
+ try {
160
+ store.updateRunningJob(job.job_id, observedExecutionFields(result));
161
+ if (sessionId) {
162
+ const updated = source === "recovered_session_dir"
163
+ ? store.claimRecoveredSessionId(job.job_id, sessionId)
164
+ : store.updateRunningJob(job.job_id, { kimi_session_id: sessionId, kimi_session_id_source: source });
165
+ if (updated?.kimi_session_id === sessionId && updated.kimi_session_id_source === source) {
166
+ details.kimi_session_id = sessionId;
167
+ details.kimi_session_id_source = source;
168
+ }
169
+ else
170
+ details[source === "recovered_session_dir" ? "session_recovery" : "session_id_persistence"] = "persistence_failed";
171
+ }
172
+ }
173
+ catch {
174
+ // `session_recovery` describes only the recovery branch; an announced id or
175
+ // the provenance update failing is a different fact.
176
+ details[source === "recovered_session_dir" ? "session_recovery" : "session_id_persistence"] = "persistence_failed";
177
+ // Diagnostics stay in the stream log; the failure transition proceeds.
178
+ }
179
+ return {
180
+ text: reassembleProseFromRecords(result.records),
181
+ toolCalls: result.records.some((record) => record.role === "tool" ||
182
+ (record.role === "assistant" && Array.isArray(record.tool_calls) &&
183
+ record.tool_calls.length > 0)),
184
+ recordCount: result.records.length,
185
+ budgetMs: timeout.details.budget_ms,
186
+ };
187
+ }
188
+ export function warnInheritedBudget(job, stderr) {
189
+ if (job.budget_source === "inherited" && job.budget_ms !== null) {
190
+ stderr.write(`[${job.command_type}] budget ${formatBudgetDuration(job.budget_ms)} inherited from job ${job.resumed_from_job_id}; pass --budget to override\n`);
191
+ }
192
+ }
@@ -143,6 +143,8 @@ export async function runPursue(argv, context) {
143
143
  model: parsed.model ?? null,
144
144
  thinking: parsed.thinking ?? null,
145
145
  background: false,
146
+ budget_ms: parsed.budgetMs ?? KIMI_PURSUE_DEFAULT_BUDGET_MS,
147
+ budget_source: parsed.budgetMs !== undefined ? "explicit" : "default",
146
148
  pid: process.pid,
147
149
  kimi_pid: null,
148
150
  status: "running",
@@ -168,7 +170,7 @@ export async function runPursue(argv, context) {
168
170
  await markJobFailed(store, paths, job, classified, "Pursue failed.", { phase: "failed" });
169
171
  throw classified;
170
172
  }
171
- const completed = await executePursueJob(job.job_id, prompt, objective, parsed.budgetMs ?? KIMI_PURSUE_DEFAULT_BUDGET_MS, context);
173
+ const completed = await executePursueJob(job.job_id, prompt, objective, job.budget_ms, context);
172
174
  if (!completed.final_output_path) {
173
175
  throw new RuntimeError("PURSUE_RESULT_MISSING", "Pursue finished without a rendered result.", "pursue.result");
174
176
  }
@@ -249,7 +251,7 @@ async function executePursueJob(jobId, prompt, objective, budgetMs, context) {
249
251
  if (result.sessionId !== undefined &&
250
252
  result.sessionId.length > 0 &&
251
253
  result.sessionId !== job.kimi_session_id) {
252
- store.updateRunningJob(job.job_id, { kimi_session_id: result.sessionId });
254
+ store.updateRunningJob(job.job_id, { kimi_session_id_source: "announced", kimi_session_id: result.sessionId });
253
255
  }
254
256
  await syncKimiSessionTitle({
255
257
  promptText: objective,
@@ -1,3 +1,4 @@
1
+ import { REPAIR_SESSIONS_SUPPORTED_FLAGS } from "../parsing.js";
1
2
  import { constants } from "node:fs";
2
3
  import { lstat, open } from "node:fs/promises";
3
4
  import { createRequire } from "node:module";
@@ -15,7 +16,7 @@ export async function runRepairSessions(argv, context) {
15
16
  const seen = new Set();
16
17
  for (const arg of argv) {
17
18
  if ((arg !== "--apply" && arg !== "--all") || seen.has(arg)) {
18
- throw new RuntimeError("INVALID_ARGS", "Usage: repair-sessions [--apply] [--all]", "repair-sessions.args");
19
+ throw new RuntimeError("INVALID_ARGS", `Usage: repair-sessions. Supported flags: ${REPAIR_SESSIONS_SUPPORTED_FLAGS}.`, "repair-sessions.args");
19
20
  }
20
21
  seen.add(arg);
21
22
  }
@@ -1,3 +1,4 @@
1
+ import { REPLAY_SUPPORTED_FLAGS } from "../parsing.js";
1
2
  import { access, stat, readFile } from "node:fs/promises";
2
3
  import { constants as fsConstants } from "node:fs";
3
4
  import { RuntimeError } from "../errors.js";
@@ -9,8 +10,8 @@ import { sweepStaleJobs } from "../jobs.js";
9
10
  const MAX_REPLAY_LOG_BYTES = 32 * 1024 * 1024;
10
11
  export async function runReplay(argv, context) {
11
12
  const [jobId, ...rest] = argv;
12
- if (!jobId || rest.length > 0) {
13
- throw new RuntimeError("INVALID_ARGS", "replay expects exactly one job id: replay <job-id>.", "replay.parse");
13
+ if (!jobId || jobId.startsWith("-") || rest.length > 0) {
14
+ throw new RuntimeError("INVALID_ARGS", `replay expects exactly one job id: replay <job-id>. Supported flags: ${REPLAY_SUPPORTED_FLAGS}.`, "replay.parse");
14
15
  }
15
16
  const paths = resolvePluginPaths(context.env);
16
17
  await ensurePluginPaths(paths);
@@ -1,3 +1,4 @@
1
+ import { companionHelp } from "../help.js";
1
2
  import { randomUUID } from "node:crypto";
2
3
  import path from "node:path";
3
4
  import { createCliCancellationHandlers } from "../cli-cancellation.js";
@@ -16,7 +17,7 @@ import { parseRescueArgs } from "../parsing.js";
16
17
  import { readArtifact, renderManagedJobOutput, writeArtifact } from "../render.js";
17
18
  import { startBackgroundJob } from "../background-spawn.js";
18
19
  import { hookRefusalDetails, hookRefusalRetryProtocol, maybeWarnHookMissing, verifyHookInstalled, } from "../hooks/install.js";
19
- import { assertCliResultSuccess, reassembleProseFromRecords, warnIfSessionIdMissing } from "./cli-helpers.js";
20
+ import { warnInheritedBudget, captureTimeoutPartialOutput, assertCliResultSuccess, reassembleProseFromRecords, warnIfSessionIdMissing } from "./cli-helpers.js";
20
21
  import { buildKimiSessionTitle, syncKimiSessionTitle } from "../session-title.js";
21
22
  export { describeMissingResult } from "../background-spawn.js";
22
23
  // v1.0 cutover note (PR 3):
@@ -41,6 +42,8 @@ const RESCUE_SUMMARY_MAX = 120;
41
42
  const RESCUE_AGENT_PROFILE_PLACEHOLDER = "<cli-client>";
42
43
  export async function runRescue(argv, context) {
43
44
  const parsed = parseRescueArgs(argv);
45
+ if (parsed.help)
46
+ return companionHelp(["task", "rescue", ...argv]);
44
47
  const paths = resolvePluginPaths(context.env);
45
48
  await ensurePluginPaths(paths);
46
49
  const repoIdentity = await resolveRepoIdentity(context.cwd);
@@ -68,10 +71,14 @@ export async function runRescue(argv, context) {
68
71
  model: parsed.model ?? null,
69
72
  thinking: parsed.thinking ?? null,
70
73
  background: parsed.background,
74
+ budget_ms: parsed.budgetMs ?? (sessionResolution.sourceJob?.operation_kind === "rescue" ? sessionResolution.sourceJob.budget_ms : null) ?? null,
75
+ budget_source: parsed.budgetMs !== undefined ? "explicit"
76
+ : sessionResolution.sourceJob?.operation_kind === "rescue" && sessionResolution.sourceJob.budget_ms !== null ? "inherited" : "default",
71
77
  pid: parsed.background ? null : process.pid,
72
78
  kimi_pid: null,
73
79
  status: "running",
74
80
  kimi_session_id: sessionResolution.kimiSessionId,
81
+ kimi_session_id_source: sessionResolution.sourceJob?.kimi_session_id_source ?? null,
75
82
  ...persistedExecutionPlanFields(executionPlan),
76
83
  agent_profile: RESCUE_AGENT_PROFILE_PLACEHOLDER,
77
84
  prompt_digest: digestPrompt(prompt),
@@ -93,6 +100,7 @@ export async function runRescue(argv, context) {
93
100
  await markJobFailed(store, paths, job, classified, rescueConfig.cancellation.failedSummary, { phase: "failed" });
94
101
  throw classified;
95
102
  }
103
+ warnInheritedBudget(job, context.stderr);
96
104
  if (parsed.background) {
97
105
  return startBackgroundJob(job, prompt, context, paths, {
98
106
  workerKind: "rescue",
@@ -185,18 +193,17 @@ export async function executeRescueJob(jobId, prompt, context, options) {
185
193
  signal: handlers.signal,
186
194
  // SIGKILL escalation defaults to 1500ms inside cli-client —
187
195
  // matches v0.4's cancellation.ts behavior for the wire client.
188
- }, KIMI_RESCUE_PROMPT_TIMEOUT_MS, "rescue.prompt");
196
+ }, job.budget_ms ?? KIMI_RESCUE_PROMPT_TIMEOUT_MS, "rescue.prompt");
189
197
  store.updateRunningJob(job.job_id, observedExecutionFields(result));
190
198
  if (handlers.cancelling) {
191
199
  throw new RuntimeError(cancel.errorCodes.cancelled, cancel.cancelMessages.afterPrompt, "rescue.runtime");
192
200
  }
193
201
  assertCliResultSuccess(result, "rescue.runtime");
194
202
  if (result.sessionId !== undefined &&
195
- result.sessionId.length > 0 &&
196
- result.sessionId !== job.kimi_session_id) {
203
+ result.sessionId.length > 0) {
197
204
  // length>0 guard: empty-string capture would poison the row. (Kimi
198
205
  // alpha.4 challenge finding #3.)
199
- store.updateRunningJob(job.job_id, { kimi_session_id: result.sessionId });
206
+ store.updateRunningJob(job.job_id, { kimi_session_id_source: "announced", kimi_session_id: result.sessionId });
200
207
  }
201
208
  await syncKimiSessionTitle({
202
209
  promptText: prompt,
@@ -242,14 +249,17 @@ export async function executeRescueJob(jobId, prompt, context, options) {
242
249
  }) ?? job);
243
250
  }
244
251
  catch (error) {
252
+ const classified = classifyManagedCommandFailure(error, "rescue", job.job_id, {
253
+ preserveStage: true,
254
+ background: job.background,
255
+ budgetMs: job.budget_ms ?? KIMI_RESCUE_PROMPT_TIMEOUT_MS,
256
+ });
257
+ const partialOutput = await captureTimeoutPartialOutput(classified, store, job, context.env, handlers.signal);
245
258
  if (handlers.cancelling) {
246
259
  const cancelledError = new RuntimeError(cancel.errorCodes.cancelled, cancel.cancelMessages.default, "rescue.runtime", error instanceof Error ? { cause: error } : undefined);
247
260
  return await markJobCancelled(store, paths, job, cancel.cancelledSummary, cancelledError, { phase: "cancelled" });
248
261
  }
249
- const classified = classifyManagedCommandFailure(error, "rescue", job.job_id, {
250
- preserveStage: true,
251
- });
252
- return await markJobFailed(store, paths, job, classified, cancel.failedSummary, { phase: "failed" });
262
+ return await markJobFailed(store, paths, job, classified, cancel.failedSummary, { phase: "failed", partialOutput });
253
263
  }
254
264
  finally {
255
265
  handlers.dispose();
@@ -2,7 +2,7 @@ import { RuntimeError } from "../errors.js";
2
2
  import { resolveRepoIdentity } from "../git.js";
3
3
  import { sweepStaleJobs } from "../jobs.js";
4
4
  import { reconcileHistoricalJobProvenance } from "../kimi-engine.js";
5
- import { withJobStore } from "../job-store.js";
5
+ import { renderBudgetSource, withJobStore } from "../job-store.js";
6
6
  import { ensurePluginPaths, resolvePluginPaths } from "../paths.js";
7
7
  import { parseJobLookupArgs } from "../parsing.js";
8
8
  import { readArtifact, renderTerminalJobArtifact } from "../render.js";
@@ -42,6 +42,12 @@ function renderResultEnvelope(job, artifactPath, body) {
42
42
  job_id: job.job_id,
43
43
  kind: job.command_type,
44
44
  status: job.status,
45
+ budget_ms: job.budget_ms,
46
+ budget_source: renderBudgetSource(job),
47
+ kimi_session_id_source: job.kimi_session_id_source,
48
+ kimi_session_id: job.kimi_session_id,
49
+ // True when a budget timeout preserved assistant prose in the artifact.
50
+ partial_output: job.status === "failed" && Number(job.error?.details?.partial_output_chars ?? 0) > 0,
45
51
  summary: job.summary,
46
52
  error: job.error,
47
53
  artifact_path: artifactPath,
@@ -153,6 +153,7 @@ async function executeReviewGate(payload, assistantMessage, context) {
153
153
  kimi_pid: null,
154
154
  status: "running",
155
155
  kimi_session_id: null,
156
+ budget_source: "default",
156
157
  ...persistedExecutionPlanFields(executionPlan),
157
158
  agent_profile: REVIEW_GATE_AGENT_PROFILE_PLACEHOLDER,
158
159
  prompt_digest: digestPrompt(prompt),
@@ -195,7 +196,7 @@ async function executeReviewGate(payload, assistantMessage, context) {
195
196
  // can't synthesize empty strings today, but the guard locks the
196
197
  // SQLite contract independent of upstream behavior).
197
198
  activeStore.updateRunningJob(job.job_id, {
198
- kimi_session_id: result.sessionId,
199
+ kimi_session_id_source: "announced", kimi_session_id: result.sessionId,
199
200
  });
200
201
  }
201
202
  const finalText = reassembleProseFromRecords(result.records);
@@ -17,7 +17,7 @@ import { renderManagedJobOutput, writeArtifact } from "../render.js";
17
17
  import { RuntimeError } from "../errors.js";
18
18
  import { resolveRepoIdentity } from "../git.js";
19
19
  import { hookRefusalDetails, hookRefusalRetryProtocol, maybeWarnHookMissing, verifyHookInstalled, } from "../hooks/install.js";
20
- import { assertCliResultSuccess, reassembleProseFromRecords, warnIfSessionIdMissing } from "./cli-helpers.js";
20
+ import { captureTimeoutPartialOutput, assertCliResultSuccess, reassembleProseFromRecords, warnIfSessionIdMissing } from "./cli-helpers.js";
21
21
  import { buildKimiSessionTitle, syncKimiSessionTitle } from "../session-title.js";
22
22
  // v1.0 cutover note (PR 2):
23
23
  //
@@ -81,6 +81,8 @@ export async function runReview(argv, context, commandType) {
81
81
  model: parsed.model ?? null,
82
82
  thinking: parsed.thinking ?? null,
83
83
  background: false,
84
+ budget_ms: parsed.budgetMs ?? null,
85
+ budget_source: parsed.budgetMs !== undefined ? "explicit" : "default",
84
86
  pid: process.pid,
85
87
  kimi_pid: null,
86
88
  status: "running",
@@ -113,7 +115,7 @@ export async function runReview(argv, context, commandType) {
113
115
  model: parsed.model,
114
116
  logPath,
115
117
  signal: handlers.signal,
116
- }, KIMI_REVIEW_PROMPT_TIMEOUT_MS, `${commandType}.prompt`);
118
+ }, job.budget_ms ?? KIMI_REVIEW_PROMPT_TIMEOUT_MS, `${commandType}.prompt`);
117
119
  store.updateRunningJob(job.job_id, observedExecutionFields(result));
118
120
  if (handlers.cancelling) {
119
121
  throw new RuntimeError(cancel.errorCodes.cancelled, cancel.cancelMessages.afterPrompt, `${commandType}.runtime`);
@@ -127,7 +129,7 @@ export async function runReview(argv, context, commandType) {
127
129
  // poison the row — consumers reading SQLite distinguish "missing"
128
130
  // (NULL) from "captured" (non-empty UUID) only via NULL-ness.
129
131
  // (Kimi alpha.4 challenge finding #3.)
130
- store.updateRunningJob(job.job_id, { kimi_session_id: result.sessionId });
132
+ store.updateRunningJob(job.job_id, { kimi_session_id_source: "announced", kimi_session_id: result.sessionId });
131
133
  }
132
134
  await syncKimiSessionTitle({
133
135
  promptText: previewPrompt,
@@ -158,13 +160,14 @@ export async function runReview(argv, context, commandType) {
158
160
  return rendered.output;
159
161
  }
160
162
  catch (error) {
163
+ const classified = classifyManagedCommandFailure(error, commandType, job.job_id, { background: job.background, budgetMs: job.budget_ms ?? KIMI_REVIEW_PROMPT_TIMEOUT_MS });
164
+ const partialOutput = await captureTimeoutPartialOutput(classified, store, job, context.env, handlers.signal);
161
165
  if (handlers.cancelling) {
162
166
  const cancelledError = new RuntimeError(cancel.errorCodes.cancelled, cancel.cancelMessages.default, `${commandType}.runtime`, error instanceof Error ? { cause: error } : undefined);
163
167
  await markJobCancelled(store, paths, job, cancel.cancelledSummary, cancelledError);
164
168
  throw cancelledError;
165
169
  }
166
- const classified = classifyManagedCommandFailure(error, commandType, job.job_id);
167
- await markJobFailed(store, paths, job, classified, cancel.failedSummary);
170
+ await markJobFailed(store, paths, job, classified, cancel.failedSummary, { partialOutput });
168
171
  throw classified;
169
172
  }
170
173
  finally {
@@ -1,3 +1,4 @@
1
+ import { SETUP_SUPPORTED_FLAGS } from "../parsing.js";
1
2
  // Managed-block installer for the kimi-code PreToolUse hook.
2
3
  //
3
4
  // Replaces the v0.4 Wire-based setup probe. In v1.0 the load-bearing
@@ -127,7 +128,7 @@ function parseArgs(argv) {
127
128
  disableReviewGate = true;
128
129
  break;
129
130
  default:
130
- throw new RuntimeError("INVALID_ARGS", `Unknown setup flag ${token}. Supported flags: --check, --uninstall, --all (with --uninstall), --enable-review-gate, --disable-review-gate.`, "setup.parse");
131
+ throw new RuntimeError("INVALID_ARGS", `Unknown setup flag ${token}. Supported flags: ${SETUP_SUPPORTED_FLAGS}.`, "setup.parse");
131
132
  }
132
133
  }
133
134
  if (enableReviewGate && disableReviewGate) {
@@ -2,7 +2,7 @@ import { RuntimeError } from "../errors.js";
2
2
  import { resolveRepoIdentity } from "../git.js";
3
3
  import { sweepStaleJobs } from "../jobs.js";
4
4
  import { reconcileHistoricalJobProvenance } from "../kimi-engine.js";
5
- import { withJobStore } from "../job-store.js";
5
+ import { renderBudgetSource, withJobStore } from "../job-store.js";
6
6
  import { ensurePluginPaths, resolvePluginPaths } from "../paths.js";
7
7
  import { parseJobLookupArgs } from "../parsing.js";
8
8
  export async function runStatus(argv, context) {
@@ -22,6 +22,6 @@ export async function runStatus(argv, context) {
22
22
  throw new RuntimeError("JOB_NOT_FOUND", "No matching job was found for status.", "status.lookup");
23
23
  }
24
24
  const job = await reconcileHistoricalJobProvenance(store, found);
25
- return `${JSON.stringify(job, null, 2)}\n`;
25
+ return `${JSON.stringify({ ...job, budget_source: renderBudgetSource(job) }, null, 2)}\n`;
26
26
  });
27
27
  }
@@ -210,6 +210,8 @@ async function runReadSwarm(parsed, objective, context) {
210
210
  model: parsed.model ?? null,
211
211
  thinking: parsed.thinking ?? null,
212
212
  background: false,
213
+ budget_ms: parsed.budgetMs ?? KIMI_SWARM_DEFAULT_BUDGET_MS,
214
+ budget_source: parsed.budgetMs !== undefined ? "explicit" : "default",
213
215
  pid: process.pid,
214
216
  kimi_pid: null,
215
217
  status: "running",
@@ -235,7 +237,7 @@ async function runReadSwarm(parsed, objective, context) {
235
237
  await markJobFailed(store, paths, job, classified, "Swarm failed.", { phase: "failed" });
236
238
  throw classified;
237
239
  }
238
- const completed = await executeSwarmJob(job.job_id, prompt, objective, parsed.budgetMs ?? KIMI_SWARM_DEFAULT_BUDGET_MS, resolveSwarmMaxConcurrency(parsed.maxConcurrency), context);
240
+ const completed = await executeSwarmJob(job.job_id, prompt, objective, job.budget_ms, resolveSwarmMaxConcurrency(parsed.maxConcurrency), context);
239
241
  if (!completed.final_output_path) {
240
242
  throw new RuntimeError("SWARM_RESULT_MISSING", "Swarm finished without a rendered result.", "swarm.result");
241
243
  }
@@ -308,6 +310,8 @@ async function runWriteSwarm(parsed, objective, context) {
308
310
  model: parsed.model ?? null,
309
311
  thinking: parsed.thinking ?? null,
310
312
  background: false,
313
+ budget_ms: parsed.budgetMs ?? KIMI_SWARM_DEFAULT_BUDGET_MS,
314
+ budget_source: parsed.budgetMs !== undefined ? "explicit" : "default",
311
315
  pid: process.pid,
312
316
  kimi_pid: null,
313
317
  status: "running",
@@ -335,7 +339,7 @@ async function runWriteSwarm(parsed, objective, context) {
335
339
  });
336
340
  throw classified;
337
341
  }
338
- const completed = await executeSwarmJob(job.job_id, prompt, objective, parsed.budgetMs ?? KIMI_SWARM_DEFAULT_BUDGET_MS, resolveSwarmWriteMaxConcurrency(parsed.maxConcurrency), context, { spawnCwd: worktreePath, workspaceRoot: worktreePath });
342
+ const completed = await executeSwarmJob(job.job_id, prompt, objective, job.budget_ms, resolveSwarmWriteMaxConcurrency(parsed.maxConcurrency), context, { spawnCwd: worktreePath, workspaceRoot: worktreePath });
339
343
  if (!completed.final_output_path) {
340
344
  throw new RuntimeError("SWARM_RESULT_MISSING", "Swarm (write) finished without a rendered result.", "swarm.result");
341
345
  }
@@ -500,7 +504,7 @@ writeMode) {
500
504
  if (result.sessionId !== undefined &&
501
505
  result.sessionId.length > 0 &&
502
506
  result.sessionId !== job.kimi_session_id) {
503
- store.updateRunningJob(job.job_id, { kimi_session_id: result.sessionId });
507
+ store.updateRunningJob(job.job_id, { kimi_session_id_source: "announced", kimi_session_id: result.sessionId });
504
508
  }
505
509
  await syncKimiSessionTitle({
506
510
  promptText: objective,
@@ -1,3 +1,4 @@
1
+ import { companionHelp } from "./help.js";
1
2
  import { runAsk } from "./commands/ask.js";
2
3
  import { runModelInventory } from "./commands/models.js";
3
4
  import { runCancel } from "./commands/cancel.js";
@@ -15,6 +16,11 @@ import { runWorker } from "./commands/worker.js";
15
16
  import { formatError, RuntimeError } from "./errors.js";
16
17
  import { resolveKimiHome } from "./kimi-home.js";
17
18
  async function main(argv) {
19
+ const help = companionHelp(argv);
20
+ if (help !== undefined) {
21
+ process.stdout.write(`${help}\n`);
22
+ return;
23
+ }
18
24
  const [command, ...rest] = argv;
19
25
  const cwd = process.env.KIMI_PLUGIN_CC_WORKSPACE_CWD || process.cwd();
20
26
  const context = {