kimi-plugin-cc 2.0.8 → 2.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (88) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/CHANGELOG.md +27 -0
  4. package/README.md +5 -5
  5. package/agents/kimi-ask.md +5 -1
  6. package/agents/kimi-challenge.md +4 -2
  7. package/agents/kimi-pursue.md +3 -1
  8. package/agents/kimi-rescue.md +5 -1
  9. package/agents/kimi-review.md +4 -2
  10. package/agents/kimi-swarm-write.md +3 -1
  11. package/agents/kimi-swarm.md +3 -1
  12. package/commands/ask.md +5 -3
  13. package/commands/challenge.md +5 -3
  14. package/commands/pursue.md +5 -1
  15. package/commands/rescue.md +5 -3
  16. package/commands/review.md +5 -3
  17. package/commands/swarm.md +5 -1
  18. package/dist/cli-client.js +42 -10
  19. package/dist/commands/ask.js +14 -7
  20. package/dist/commands/cli-helpers.js +66 -4
  21. package/dist/commands/pursue.js +7 -3
  22. package/dist/commands/rescue.js +16 -10
  23. package/dist/commands/result.js +5 -1
  24. package/dist/commands/review-gate.js +21 -14
  25. package/dist/commands/review.js +9 -5
  26. package/dist/commands/status.js +2 -2
  27. package/dist/commands/swarm.js +10 -4
  28. package/dist/help.js +1 -1
  29. package/dist/hook-escape.js +71 -0
  30. package/dist/hooks/approval-policy.js +17 -27
  31. package/dist/hooks/config-safety.js +15 -7
  32. package/dist/job-store.js +52 -7
  33. package/dist/kimi-engine.js +2 -0
  34. package/dist/kimi-errors.js +1 -1
  35. package/dist/kimi-timeouts.js +6 -10
  36. package/dist/kimi-version-probe.js +2 -2
  37. package/dist/parsing.js +59 -53
  38. package/dist/session-recovery.js +203 -0
  39. package/dist/vendor/smol-toml/date.js +50 -21
  40. package/dist/vendor/smol-toml/error.js +6 -2
  41. package/dist/vendor/smol-toml/extract.js +259 -66
  42. package/dist/vendor/smol-toml/parse.js +64 -48
  43. package/dist/vendor/smol-toml/primitive.js +107 -135
  44. package/dist/vendor/smol-toml/struct.js +106 -134
  45. package/dist/vendor/smol-toml/util.js +22 -78
  46. package/dist/version.js +1 -1
  47. package/docs/models.md +10 -0
  48. package/docs/registry-releases.md +24 -1
  49. package/docs/safety.md +59 -2
  50. package/package.json +1 -1
  51. package/plugins/kimi-codex/.codex-plugin/plugin.json +1 -1
  52. package/plugins/kimi-codex/dist/cli-client.js +42 -10
  53. package/plugins/kimi-codex/dist/commands/ask.js +14 -7
  54. package/plugins/kimi-codex/dist/commands/cli-helpers.js +66 -4
  55. package/plugins/kimi-codex/dist/commands/pursue.js +7 -3
  56. package/plugins/kimi-codex/dist/commands/rescue.js +16 -10
  57. package/plugins/kimi-codex/dist/commands/result.js +5 -1
  58. package/plugins/kimi-codex/dist/commands/review-gate.js +21 -14
  59. package/plugins/kimi-codex/dist/commands/review.js +9 -5
  60. package/plugins/kimi-codex/dist/commands/status.js +2 -2
  61. package/plugins/kimi-codex/dist/commands/swarm.js +10 -4
  62. package/plugins/kimi-codex/dist/help.js +1 -1
  63. package/plugins/kimi-codex/dist/hook-escape.js +71 -0
  64. package/plugins/kimi-codex/dist/hooks/approval-policy.js +17 -27
  65. package/plugins/kimi-codex/dist/hooks/config-safety.js +15 -7
  66. package/plugins/kimi-codex/dist/job-store.js +52 -7
  67. package/plugins/kimi-codex/dist/kimi-engine.js +2 -0
  68. package/plugins/kimi-codex/dist/kimi-errors.js +1 -1
  69. package/plugins/kimi-codex/dist/kimi-timeouts.js +6 -10
  70. package/plugins/kimi-codex/dist/kimi-version-probe.js +2 -2
  71. package/plugins/kimi-codex/dist/parsing.js +59 -53
  72. package/plugins/kimi-codex/dist/session-recovery.js +203 -0
  73. package/plugins/kimi-codex/dist/vendor/smol-toml/date.js +50 -21
  74. package/plugins/kimi-codex/dist/vendor/smol-toml/error.js +6 -2
  75. package/plugins/kimi-codex/dist/vendor/smol-toml/extract.js +259 -66
  76. package/plugins/kimi-codex/dist/vendor/smol-toml/parse.js +64 -48
  77. package/plugins/kimi-codex/dist/vendor/smol-toml/primitive.js +107 -135
  78. package/plugins/kimi-codex/dist/vendor/smol-toml/struct.js +106 -134
  79. package/plugins/kimi-codex/dist/vendor/smol-toml/util.js +22 -78
  80. package/plugins/kimi-codex/dist/version.js +1 -1
  81. package/plugins/kimi-codex/skills/kimi-ask/SKILL.md +4 -2
  82. package/plugins/kimi-codex/skills/kimi-challenge/SKILL.md +4 -2
  83. package/plugins/kimi-codex/skills/kimi-pursue/SKILL.md +3 -1
  84. package/plugins/kimi-codex/skills/kimi-rescue/SKILL.md +4 -2
  85. package/plugins/kimi-codex/skills/kimi-review/SKILL.md +4 -2
  86. package/plugins/kimi-codex/skills/kimi-setup/SKILL.md +2 -0
  87. package/plugins/kimi-codex/skills/kimi-swarm/SKILL.md +3 -1
  88. package/plugins/kimi-codex/skills/kimi-swarm-write/SKILL.md +3 -1
@@ -19,6 +19,7 @@
19
19
  import { spawn } from "node:child_process";
20
20
  import { appendFile, mkdir } from "node:fs/promises";
21
21
  import path from "node:path";
22
+ import { createHookEscapeMonitor } from "./hook-escape.js";
22
23
  import { RuntimeError, formatError } from "./errors.js";
23
24
  import { assertNoUnsafeExperimentalSelector, assertExecutionPlanMatchesSpawn, } from "./kimi-engine.js";
24
25
  import { assertPrefixArgsSafe } from "./kimi-command.js";
@@ -84,7 +85,9 @@ export async function runCliPrompt(opts) {
84
85
  });
85
86
  }
86
87
  let child;
88
+ let spawnedAt;
87
89
  try {
90
+ spawnedAt = Date.now();
88
91
  child = spawn(opts.command, args, {
89
92
  cwd: opts.cwd,
90
93
  env,
@@ -124,6 +127,14 @@ export async function runCliPrompt(opts) {
124
127
  let announcedSessionId;
125
128
  let announcedGoalSummary;
126
129
  let announcedSystemVersion;
130
+ const hookMonitor = createHookEscapeMonitor({ commandLabel: opts.commandLabel,
131
+ operationKind: opts.executionPlan.operationKind, trustedWorkspaceRoot: opts.trustedWorkspaceRoot, cwd: opts.cwd });
132
+ const hookEscapes = [];
133
+ const noteHookEscape = (escape) => {
134
+ hookEscapes.push(escape);
135
+ appendLogLine({ event: "hook_escape", ...escape, command_label: opts.commandLabel,
136
+ operation_kind: opts.executionPlan.operationKind });
137
+ };
127
138
  let engineMismatchDetected = false;
128
139
  let handleEngineMismatch;
129
140
  const plannedEngine = opts.executionPlan.intendedEngine;
@@ -281,6 +292,9 @@ export async function runCliPrompt(opts) {
281
292
  }
282
293
  if (requireMarkerBeforeOutput(`a ${outcome.record.role} record`))
283
294
  return;
295
+ const escape = hookMonitor.observe(outcome.record);
296
+ if (escape)
297
+ noteHookEscape(escape);
284
298
  records.push(outcome.record);
285
299
  appendLogLine({ event: "record", record: outcome.record });
286
300
  invokeOnRecord(outcome.record);
@@ -312,6 +326,13 @@ export async function runCliPrompt(opts) {
312
326
  const settle = (kind, payload) => {
313
327
  if (settlementStarted)
314
328
  return;
329
+ // Denied calls still without a result (a cancel/budget race, an abnormal
330
+ // exit, or a turn that ended without the denial the hook owes) are reported
331
+ // alongside the real outcome, never instead of it.
332
+ for (const escape of hookMonitor.settle())
333
+ noteHookEscape(escape);
334
+ if (kind === "resolve" && hookEscapes.length > 0)
335
+ payload = { ...payload, hookEscapes: [...hookEscapes] };
315
336
  settlementStarted = true;
316
337
  opts.signal?.removeEventListener("abort", onAbort);
317
338
  // Abort teardown can outlive the direct child's close event while a
@@ -329,7 +350,7 @@ export async function runCliPrompt(opts) {
329
350
  return;
330
351
  }
331
352
  if (kind === "resolve")
332
- resolve(payload);
353
+ resolve({ ...payload, settledAt: Date.now() });
333
354
  else
334
355
  reject(payload);
335
356
  });
@@ -470,6 +491,11 @@ export async function runCliPrompt(opts) {
470
491
  }
471
492
  }
472
493
  };
494
+ const stopForPolicyFailure = (error) => {
495
+ engineMismatchDetected = true; // Stops all further consumer output.
496
+ cancellationTeardown ??= teardownChildTree().then(() => ({ ok: true }), (error) => ({ ok: false, error }));
497
+ settle("reject", error);
498
+ };
473
499
  let engineMismatchError;
474
500
  handleEngineMismatch = (mismatch) => {
475
501
  if (engineMismatchError !== undefined)
@@ -491,8 +517,7 @@ export async function runCliPrompt(opts) {
491
517
  system_version: mismatch.systemVersion ?? null,
492
518
  reason: mismatch.reason,
493
519
  });
494
- cancellationTeardown = teardownChildTree().then(() => ({ ok: true }), (error) => ({ ok: false, error }));
495
- settle("reject", engineMismatchError);
520
+ stopForPolicyFailure(engineMismatchError);
496
521
  };
497
522
  const onAbort = () => {
498
523
  if (aborted)
@@ -580,6 +605,8 @@ export async function runCliPrompt(opts) {
580
605
  // The result itself is in memory; diagnostics are best-effort.
581
606
  await raceWithTimeout(logChain, LOG_DRAIN_TIMEOUT_MS);
582
607
  settle("resolve", {
608
+ spawnedAt,
609
+ settledAt: 0, // Filled at settlement, after the teardown barrier.
583
610
  sessionId,
584
611
  goalSummary: announcedGoalSummary,
585
612
  systemVersion: announcedSystemVersion,
@@ -656,7 +683,9 @@ export function requireSessionId(result, context) {
656
683
  },
657
684
  });
658
685
  }
659
- /** Settled timeout data; stream records are deliberately non-serializable. */
686
+ /** Settled timeout data; stream records are deliberately non-serializable.
687
+ * Without a partial result (teardown rejection), aborted means abort was issued,
688
+ * not that the process was observed aborting. */
660
689
  export class ResponseTimeoutError extends RuntimeError {
661
690
  constructor(budgetMs, stage, commandLabel, partialResult, cause) {
662
691
  super("RESPONSE_TIMEOUT", `${stage} timed out after ${budgetMs}ms.`, stage, {
@@ -666,8 +695,9 @@ export class ResponseTimeoutError extends RuntimeError {
666
695
  command_label: commandLabel ?? null,
667
696
  record_count: partialResult?.records.length ?? 0,
668
697
  partial_output_chars: partialResult?.records.reduce((count, record) => count + (record.role === "assistant" && typeof record.content === "string" ? record.content.length : 0), 0) ?? 0,
669
- aborted: true,
698
+ aborted: partialResult?.aborted ?? true,
670
699
  ...(partialResult ? { settled_post_abort: true } : {}),
700
+ ...(partialResult?.hookEscapes?.length ? { hook_escapes: partialResult.hookEscapes } : {}),
671
701
  },
672
702
  });
673
703
  this.name = "ResponseTimeoutError";
@@ -772,11 +802,11 @@ function buildArgs(opts) {
772
802
  args.push("--skills-dir", dir);
773
803
  }
774
804
  }
775
- // NOTE: `opts.thinking === false` is intentionally NOT translated into a
776
- // `--no-thinking` argv flag. kimi-code 0.1.1 rejects unknown options and
777
- // has no per-spawn thinking control — the Round 2 multi-agent review
778
- // caught this. The option stays in CliClientOptions for future wiring
779
- // when upstream adds the flag (see field doc).
805
+ // NOTE: thinking effort is requested through the env binding, never CLI argv.
806
+ // Non-trait-driven providers or a base level already off ignore it silently;
807
+ // an alwaysThinking model receives off unnormalized. stream-json carries no
808
+ // thinking deltas (writeThinkingDelta() {} in the JSON writer), so the plugin
809
+ // cannot observe whether the request was honoured.
780
810
  // -p accepts the prompt as the option value. We pass it as a separate argv
781
811
  // entry so prompts with leading dashes or whitespace are not interpreted as
782
812
  // additional flags.
@@ -785,6 +815,8 @@ function buildArgs(opts) {
785
815
  }
786
816
  function buildEnv(opts) {
787
817
  const env = { ...opts.env };
818
+ if (opts.thinkingEffort !== undefined)
819
+ env.KIMI_MODEL_THINKING_EFFORT = opts.thinkingEffort;
788
820
  // 2.2.0 audit: ambient trust starts project MCP stdio servers outside PreToolUse.
789
821
  delete env.KIMI_CODE_TRUST_WORKSPACE;
790
822
  // kimi-code 0.33.0 inverted its CLI engine default: an unflagged `kimi -p`
@@ -17,7 +17,7 @@ import { readArtifact, renderManagedJobOutput, writeArtifact } from "../render.j
17
17
  import { classifyManagedCommandFailure } from "../kimi-errors.js";
18
18
  import { startBackgroundJob } from "../background-spawn.js";
19
19
  import { hookRefusalDetails, hookRefusalRetryProtocol, maybeWarnHookMissing, verifyHookInstalled, } from "../hooks/install.js";
20
- import { captureTimeoutPartialOutput, assertCliResultSuccess, reassembleProseFromRecords, warnIfSessionIdMissing } from "./cli-helpers.js";
20
+ import { warnInheritedBudget, captureTimeoutPartialOutput, assertCliResultSuccess, reassembleProseFromRecords, warnIfSessionIdMissing, warnHookEscapes } from "./cli-helpers.js";
21
21
  import { buildKimiSessionTitle, syncKimiSessionTitle } from "../session-title.js";
22
22
  // v1.0 cutover note (PR 2):
23
23
  //
@@ -77,13 +77,17 @@ export async function runAsk(argv, context) {
77
77
  command_type: "ask",
78
78
  cwd: context.cwd,
79
79
  model: parsed.model ?? null,
80
- thinking: parsed.thinking ?? null,
80
+ thinking: null,
81
+ thinking_effort: parsed.thinkingEffort ?? null,
81
82
  background: parsed.background,
82
83
  budget_ms: parsed.budgetMs ?? (sessionResolution.sourceJob?.operation_kind === "ask" ? sessionResolution.sourceJob.budget_ms : null) ?? null,
84
+ budget_source: parsed.budgetMs !== undefined ? "explicit"
85
+ : sessionResolution.sourceJob?.operation_kind === "ask" && sessionResolution.sourceJob.budget_ms !== null ? "inherited" : "default",
83
86
  pid: parsed.background ? null : process.pid,
84
87
  kimi_pid: null,
85
88
  status: "running",
86
89
  kimi_session_id: sessionResolution.kimiSessionId,
90
+ kimi_session_id_source: sessionResolution.sourceJob?.kimi_session_id_source ?? null,
87
91
  ...persistedExecutionPlanFields(executionPlan),
88
92
  agent_profile: ASK_AGENT_PROFILE_PLACEHOLDER,
89
93
  prompt_digest: digestPrompt(askPrompt),
@@ -105,6 +109,7 @@ export async function runAsk(argv, context) {
105
109
  await markJobFailed(store, paths, job, classified, askConfig.cancellation.failedSummary, { phase: "failed" });
106
110
  throw classified;
107
111
  }
112
+ warnInheritedBudget(job, context.stderr);
108
113
  if (parsed.background) {
109
114
  return startBackgroundJob(job, askPrompt, context, paths, {
110
115
  workerKind: "ask",
@@ -170,6 +175,7 @@ export async function executeAskJob(jobId, prompt, context, options) {
170
175
  prompt,
171
176
  commandLabel: "ask",
172
177
  model: job.model ?? undefined,
178
+ thinkingEffort: job.thinking_effort ?? undefined,
173
179
  resumeSessionId: job.kimi_session_id ?? undefined,
174
180
  logPath: job.stream_log_path,
175
181
  signal: handlers.signal,
@@ -179,17 +185,17 @@ export async function executeAskJob(jobId, prompt, context, options) {
179
185
  throw new RuntimeError(cancel.errorCodes.cancelled, cancel.cancelMessages.afterPrompt, "ask.runtime");
180
186
  }
181
187
  assertCliResultSuccess(result, "ask.runtime");
188
+ warnHookEscapes(result, "ask", context.stderr);
182
189
  // Persist whichever session id kimi actually announced. For fresh
183
190
  // sessions this is the only chance to record it. For resumed
184
191
  // sessions it should match the input, but capture anyway in case
185
192
  // kimi-code mints a new id on each turn.
186
193
  if (result.sessionId !== undefined &&
187
- result.sessionId.length > 0 &&
188
- result.sessionId !== job.kimi_session_id) {
194
+ result.sessionId.length > 0) {
189
195
  // length>0 guard: empty-string capture would poison the row. The
190
196
  // warning helper's null-ish check would also fire but only after
191
197
  // the bad write. (Kimi alpha.4 challenge finding #3.)
192
- store.updateRunningJob(job.job_id, { kimi_session_id: result.sessionId });
198
+ store.updateRunningJob(job.job_id, { kimi_session_id_source: "announced", kimi_session_id: result.sessionId });
193
199
  }
194
200
  await syncKimiSessionTitle({
195
201
  promptText: prompt,
@@ -219,6 +225,9 @@ export async function executeAskJob(jobId, prompt, context, options) {
219
225
  }) ?? job);
220
226
  }
221
227
  catch (error) {
228
+ const classified = error instanceof RuntimeError && error.stage === "ask.hook-check" ? error
229
+ : classifyManagedCommandFailure(error, "ask", job.job_id, { background: job.background, budgetMs: job.budget_ms ?? KIMI_ASK_PROMPT_TIMEOUT_MS });
230
+ const partialOutput = await captureTimeoutPartialOutput(classified, store, job, context.env, handlers?.signal);
222
231
  if (handlers?.cancelling) {
223
232
  const cancelledError = new RuntimeError(cancel.errorCodes.cancelled, cancel.cancelMessages.default, "ask.runtime", error instanceof Error ? { cause: error } : undefined);
224
233
  return await markJobCancelled(store, paths, job, cancel.cancelledSummary, cancelledError, { phase: "cancelled" });
@@ -231,8 +240,6 @@ export async function executeAskJob(jobId, prompt, context, options) {
231
240
  await markJobFailed(store, paths, job, error, cancel.failedSummary, { phase: "failed" });
232
241
  throw error;
233
242
  }
234
- const classified = classifyManagedCommandFailure(error, "ask", job.job_id, { background: job.background, budgetMs: job.budget_ms ?? KIMI_ASK_PROMPT_TIMEOUT_MS });
235
- const partialOutput = captureTimeoutPartialOutput(classified, store, job);
236
243
  await markJobFailed(store, paths, job, classified, cancel.failedSummary, { phase: "failed", partialOutput });
237
244
  throw classified;
238
245
  }
@@ -1,6 +1,9 @@
1
1
  import { RuntimeError } from "../errors.js";
2
2
  import { ResponseTimeoutError } from "../cli-client.js";
3
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";
4
7
  /**
5
8
  * Reassemble prose final text from cli-client records.
6
9
  *
@@ -112,12 +115,42 @@ export function assertCliResultSuccess(result, stage) {
112
115
  });
113
116
  }
114
117
  }
115
- /** Timeout-only capture, called after cancellation has taken precedence. No title sync on failure. */
116
- export function captureTimeoutPartialOutput(classified, store, job) {
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;
117
123
  const timeout = classified.cause;
118
124
  if (!(timeout instanceof ResponseTimeoutError) || !timeout.partialResult)
119
125
  return undefined;
120
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;
121
154
  // Best-effort metadata only. The row is still `running`, and a session id
122
155
  // written here can collide with `jobs_running_session_idx` when a resume of
123
156
  // the same session was started while this job ran. A store error must never
@@ -125,11 +158,22 @@ export function captureTimeoutPartialOutput(classified, store, job) {
125
158
  // than the metadata, so swallow and fall through.
126
159
  try {
127
160
  store.updateRunningJob(job.job_id, observedExecutionFields(result));
128
- if (result.sessionId !== undefined && result.sessionId.length > 0 && result.sessionId !== job.kimi_session_id) {
129
- store.updateRunningJob(job.job_id, { kimi_session_id: result.sessionId });
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";
130
171
  }
131
172
  }
132
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";
133
177
  // Diagnostics stay in the stream log; the failure transition proceeds.
134
178
  }
135
179
  return {
@@ -141,3 +185,21 @@ export function captureTimeoutPartialOutput(classified, store, job) {
141
185
  budgetMs: timeout.details.budget_ms,
142
186
  };
143
187
  }
188
+ /** One-line human summary of hook-escape findings, or undefined when none. */
189
+ export function describeHookEscapes(result) {
190
+ const escapes = result.hookEscapes ?? [];
191
+ if (escapes.length === 0)
192
+ return undefined;
193
+ const first = escapes[0];
194
+ return `safety hook notice: ${escapes.length} policy-denied tool call(s) did not come back as the managed hook's denial (first: ${first.tool_name} ${first.kind}). Detection evidence only — the call may have been rejected upstream before the hook ran, blocked by another hook, or executed because the hook drifted. Run /kimi:setup --check; details in \`result <jobId> --json\` (hook_escapes).`;
195
+ }
196
+ export function warnHookEscapes(result, commandType, stderr) {
197
+ const note = describeHookEscapes(result);
198
+ if (note !== undefined)
199
+ stderr.write(`[${commandType}] ${note}\n`);
200
+ }
201
+ export function warnInheritedBudget(job, stderr) {
202
+ if (job.budget_source === "inherited" && job.budget_ms !== null) {
203
+ stderr.write(`[${job.command_type}] budget ${formatBudgetDuration(job.budget_ms)} inherited from job ${job.resumed_from_job_id}; pass --budget to override\n`);
204
+ }
205
+ }
@@ -14,7 +14,7 @@ import { ensurePluginPaths, resolvePluginPaths } from "../paths.js";
14
14
  import { parsePursueArgs } from "../parsing.js";
15
15
  import { readArtifact, renderManagedJobOutput, writeArtifact } from "../render.js";
16
16
  import { hookRefusalDetails, hookRefusalRetryProtocol, maybeWarnHookMissing, verifyHookInstalled, } from "../hooks/install.js";
17
- import { assertCliResultSuccess, reassembleProseFromRecords, warnIfSessionIdMissing } from "./cli-helpers.js";
17
+ import { assertCliResultSuccess, reassembleProseFromRecords, warnIfSessionIdMissing, warnHookEscapes } from "./cli-helpers.js";
18
18
  import { buildKimiSessionTitle, syncKimiSessionTitle } from "../session-title.js";
19
19
  // /kimi:pursue — autonomous goal mode (kimi-code 0.8.0+ headless `/goal`).
20
20
  //
@@ -141,9 +141,11 @@ export async function runPursue(argv, context) {
141
141
  command_type: "rescue",
142
142
  cwd: context.cwd,
143
143
  model: parsed.model ?? null,
144
- thinking: parsed.thinking ?? null,
144
+ thinking: null,
145
+ thinking_effort: parsed.thinkingEffort ?? null,
145
146
  background: false,
146
147
  budget_ms: parsed.budgetMs ?? KIMI_PURSUE_DEFAULT_BUDGET_MS,
148
+ budget_source: parsed.budgetMs !== undefined ? "explicit" : "default",
147
149
  pid: process.pid,
148
150
  kimi_pid: null,
149
151
  status: "running",
@@ -232,10 +234,12 @@ async function executePursueJob(jobId, prompt, objective, budgetMs, context) {
232
234
  commandLabel: "rescue",
233
235
  trustedWorkspaceRoot: job.cwd,
234
236
  model: job.model ?? undefined,
237
+ thinkingEffort: job.thinking_effort ?? undefined,
235
238
  logPath: job.stream_log_path,
236
239
  signal: handlers.signal,
237
240
  }, budgetMs, "pursue.prompt");
238
241
  store.updateRunningJob(job.job_id, observedExecutionFields(result));
242
+ warnHookEscapes(result, "pursue", context.stderr);
239
243
  if (handlers.cancelling) {
240
244
  throw new RuntimeError("PURSUE_CANCELLED", "Pursue cancelled by user request.", "pursue.runtime");
241
245
  }
@@ -250,7 +254,7 @@ async function executePursueJob(jobId, prompt, objective, budgetMs, context) {
250
254
  if (result.sessionId !== undefined &&
251
255
  result.sessionId.length > 0 &&
252
256
  result.sessionId !== job.kimi_session_id) {
253
- store.updateRunningJob(job.job_id, { kimi_session_id: result.sessionId });
257
+ store.updateRunningJob(job.job_id, { kimi_session_id_source: "announced", kimi_session_id: result.sessionId });
254
258
  }
255
259
  await syncKimiSessionTitle({
256
260
  promptText: objective,
@@ -17,7 +17,7 @@ import { parseRescueArgs } from "../parsing.js";
17
17
  import { readArtifact, renderManagedJobOutput, writeArtifact } from "../render.js";
18
18
  import { startBackgroundJob } from "../background-spawn.js";
19
19
  import { hookRefusalDetails, hookRefusalRetryProtocol, maybeWarnHookMissing, verifyHookInstalled, } from "../hooks/install.js";
20
- import { captureTimeoutPartialOutput, assertCliResultSuccess, reassembleProseFromRecords, warnIfSessionIdMissing } from "./cli-helpers.js";
20
+ import { warnInheritedBudget, captureTimeoutPartialOutput, assertCliResultSuccess, reassembleProseFromRecords, warnIfSessionIdMissing, warnHookEscapes } from "./cli-helpers.js";
21
21
  import { buildKimiSessionTitle, syncKimiSessionTitle } from "../session-title.js";
22
22
  export { describeMissingResult } from "../background-spawn.js";
23
23
  // v1.0 cutover note (PR 3):
@@ -69,13 +69,17 @@ export async function runRescue(argv, context) {
69
69
  command_type: "rescue",
70
70
  cwd: context.cwd,
71
71
  model: parsed.model ?? null,
72
- thinking: parsed.thinking ?? null,
72
+ thinking: null,
73
+ thinking_effort: parsed.thinkingEffort ?? null,
73
74
  background: parsed.background,
74
75
  budget_ms: parsed.budgetMs ?? (sessionResolution.sourceJob?.operation_kind === "rescue" ? sessionResolution.sourceJob.budget_ms : null) ?? null,
76
+ budget_source: parsed.budgetMs !== undefined ? "explicit"
77
+ : sessionResolution.sourceJob?.operation_kind === "rescue" && sessionResolution.sourceJob.budget_ms !== null ? "inherited" : "default",
75
78
  pid: parsed.background ? null : process.pid,
76
79
  kimi_pid: null,
77
80
  status: "running",
78
81
  kimi_session_id: sessionResolution.kimiSessionId,
82
+ kimi_session_id_source: sessionResolution.sourceJob?.kimi_session_id_source ?? null,
79
83
  ...persistedExecutionPlanFields(executionPlan),
80
84
  agent_profile: RESCUE_AGENT_PROFILE_PLACEHOLDER,
81
85
  prompt_digest: digestPrompt(prompt),
@@ -97,6 +101,7 @@ export async function runRescue(argv, context) {
97
101
  await markJobFailed(store, paths, job, classified, rescueConfig.cancellation.failedSummary, { phase: "failed" });
98
102
  throw classified;
99
103
  }
104
+ warnInheritedBudget(job, context.stderr);
100
105
  if (parsed.background) {
101
106
  return startBackgroundJob(job, prompt, context, paths, {
102
107
  workerKind: "rescue",
@@ -184,6 +189,7 @@ export async function executeRescueJob(jobId, prompt, context, options) {
184
189
  commandLabel: "rescue",
185
190
  trustedWorkspaceRoot: job.cwd,
186
191
  model: job.model ?? undefined,
192
+ thinkingEffort: job.thinking_effort ?? undefined,
187
193
  resumeSessionId: job.kimi_session_id ?? undefined,
188
194
  logPath: job.stream_log_path,
189
195
  signal: handlers.signal,
@@ -195,12 +201,12 @@ export async function executeRescueJob(jobId, prompt, context, options) {
195
201
  throw new RuntimeError(cancel.errorCodes.cancelled, cancel.cancelMessages.afterPrompt, "rescue.runtime");
196
202
  }
197
203
  assertCliResultSuccess(result, "rescue.runtime");
204
+ warnHookEscapes(result, "rescue", context.stderr);
198
205
  if (result.sessionId !== undefined &&
199
- result.sessionId.length > 0 &&
200
- result.sessionId !== job.kimi_session_id) {
206
+ result.sessionId.length > 0) {
201
207
  // length>0 guard: empty-string capture would poison the row. (Kimi
202
208
  // alpha.4 challenge finding #3.)
203
- store.updateRunningJob(job.job_id, { kimi_session_id: result.sessionId });
209
+ store.updateRunningJob(job.job_id, { kimi_session_id_source: "announced", kimi_session_id: result.sessionId });
204
210
  }
205
211
  await syncKimiSessionTitle({
206
212
  promptText: prompt,
@@ -246,16 +252,16 @@ export async function executeRescueJob(jobId, prompt, context, options) {
246
252
  }) ?? job);
247
253
  }
248
254
  catch (error) {
249
- if (handlers.cancelling) {
250
- const cancelledError = new RuntimeError(cancel.errorCodes.cancelled, cancel.cancelMessages.default, "rescue.runtime", error instanceof Error ? { cause: error } : undefined);
251
- return await markJobCancelled(store, paths, job, cancel.cancelledSummary, cancelledError, { phase: "cancelled" });
252
- }
253
255
  const classified = classifyManagedCommandFailure(error, "rescue", job.job_id, {
254
256
  preserveStage: true,
255
257
  background: job.background,
256
258
  budgetMs: job.budget_ms ?? KIMI_RESCUE_PROMPT_TIMEOUT_MS,
257
259
  });
258
- const partialOutput = captureTimeoutPartialOutput(classified, store, job);
260
+ const partialOutput = await captureTimeoutPartialOutput(classified, store, job, context.env, handlers.signal);
261
+ if (handlers.cancelling) {
262
+ const cancelledError = new RuntimeError(cancel.errorCodes.cancelled, cancel.cancelMessages.default, "rescue.runtime", error instanceof Error ? { cause: error } : undefined);
263
+ return await markJobCancelled(store, paths, job, cancel.cancelledSummary, cancelledError, { phase: "cancelled" });
264
+ }
259
265
  return await markJobFailed(store, paths, job, classified, cancel.failedSummary, { phase: "failed", partialOutput });
260
266
  }
261
267
  finally {
@@ -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, parseHookEscapes } 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";
@@ -43,6 +43,10 @@ function renderResultEnvelope(job, artifactPath, body) {
43
43
  kind: job.command_type,
44
44
  status: job.status,
45
45
  budget_ms: job.budget_ms,
46
+ budget_source: renderBudgetSource(job),
47
+ thinking_effort_requested: job.thinking_effort ?? null,
48
+ hook_escapes: parseHookEscapes(job.hook_escapes),
49
+ kimi_session_id_source: job.kimi_session_id_source,
46
50
  kimi_session_id: job.kimi_session_id,
47
51
  // True when a budget timeout preserved assistant prose in the artifact.
48
52
  partial_output: job.status === "failed" && Number(job.error?.details?.partial_output_chars ?? 0) > 0,
@@ -15,9 +15,10 @@ import { KIMI_REVIEW_GATE_TIMEOUT_MS, isTimeoutCode } from "../kimi-timeouts.js"
15
15
  import { writeInvocationLogHeader } from "../logging.js";
16
16
  import { ensurePluginPaths, resolvePluginPaths } from "../paths.js";
17
17
  import { renderManagedJobOutput, writeArtifact, } from "../render.js";
18
+ import { validateThinkingEffort } from "../parsing.js";
18
19
  import { maybeWarnHookMissing, verifyHookInstalled } from "../hooks/install.js";
19
20
  import { resolveKimiHome } from "../kimi-home.js";
20
- import { assertCliResultSuccess, reassembleProseFromRecords } from "./cli-helpers.js";
21
+ import { assertCliResultSuccess, describeHookEscapes, reassembleProseFromRecords } from "./cli-helpers.js";
21
22
  const REVIEW_GATE_AGENT_PROFILE_PLACEHOLDER = "<cli-client>";
22
23
  // v1.0 cutover note (PR 2):
23
24
  //
@@ -82,18 +83,19 @@ export async function runReviewGateStopHook(payload, context) {
82
83
  ...payload,
83
84
  cwd,
84
85
  }, assistantMessage, context);
86
+ const note = output.hookEscapeNote !== undefined ? `Kimi review gate ${output.hookEscapeNote}` : undefined;
85
87
  if (output.decision === "BLOCK" && output.confidence === "high") {
86
88
  return {
87
89
  decision: "block",
88
90
  reason: buildBlockReason(output),
91
+ ...(note !== undefined ? { systemMessage: note } : {}),
89
92
  };
90
93
  }
91
94
  if (output.decision === "BLOCK") {
92
- return {
93
- systemMessage: `Kimi review gate noted concerns but allowed stop: ${output.summary}`,
94
- };
95
+ const concern = `Kimi review gate noted concerns but allowed stop: ${output.summary}`;
96
+ return { systemMessage: note !== undefined ? `${concern}\n${note}` : concern };
95
97
  }
96
- return {};
98
+ return note !== undefined ? { systemMessage: note } : {};
97
99
  }
98
100
  catch (error) {
99
101
  return {
@@ -105,6 +107,7 @@ function reviewGateSkipped(reason) {
105
107
  return { systemMessage: `review-gate skipped: ${reason}` };
106
108
  }
107
109
  async function executeReviewGate(payload, assistantMessage, context) {
110
+ let hookEscapeNote;
108
111
  const paths = resolvePluginPaths(context.env);
109
112
  await ensurePluginPaths(paths);
110
113
  const repoIdentity = await resolveRepoIdentity(payload.cwd);
@@ -121,6 +124,10 @@ async function executeReviewGate(payload, assistantMessage, context) {
121
124
  cwd: payload.cwd,
122
125
  repoRoot: repoIdentity.repoRoot,
123
126
  });
127
+ // Unset or empty → request "off" (the 8 s budget is why); "inherit" → export
128
+ // nothing; anything else is lowercased and syntax-checked like --thinking.
129
+ const gateThinking = context.env.KIMI_PLUGIN_CC_REVIEW_GATE_THINKING?.trim() || "off";
130
+ const thinkingEffort = gateThinking.toLowerCase() === "inherit" ? undefined : validateThinkingEffort(gateThinking, "review_gate.parse");
124
131
  const configuredModel = context.env.KIMI_PLUGIN_CC_REVIEW_GATE_MODEL;
125
132
  const model = configuredModel?.trim() ? configuredModel : undefined;
126
133
  const executionPlan = await prepareKimiExecutionPlan({
@@ -148,11 +155,13 @@ async function executeReviewGate(payload, assistantMessage, context) {
148
155
  cwd: payload.cwd,
149
156
  model: model ?? null,
150
157
  thinking: false,
158
+ thinking_effort: thinkingEffort ?? null,
151
159
  background: false,
152
160
  pid: null,
153
161
  kimi_pid: null,
154
162
  status: "running",
155
163
  kimi_session_id: null,
164
+ budget_source: "default",
156
165
  ...persistedExecutionPlanFields(executionPlan),
157
166
  agent_profile: REVIEW_GATE_AGENT_PROFILE_PLACEHOLDER,
158
167
  prompt_digest: digestPrompt(prompt),
@@ -177,17 +186,15 @@ async function executeReviewGate(payload, assistantMessage, context) {
177
186
  prompt,
178
187
  commandLabel: "review_gate",
179
188
  model,
180
- // Intent: thinking-off so the 8s Stop-hook budget is achievable.
181
- // Mechanism: currently advisory — kimi-code 0.1.1 has no CLI
182
- // flag for this; control is config-based (default_thinking /
183
- // [thinking].mode). The field carries intent through the
184
- // contract; when upstream adds a per-spawn flag, buildArgs will
185
- // translate it. (Round 2 Codex finding.)
186
- thinking: false,
189
+ // Requested via env; provider/model may ignore it. The 8s budget still applies.
190
+ thinkingEffort,
187
191
  logPath,
188
192
  }, KIMI_REVIEW_GATE_TIMEOUT_MS, "review_gate.runtime");
189
193
  activeStore.updateRunningJob(job.job_id, observedExecutionFields(result));
190
194
  assertCliResultSuccess(result, "review_gate.runtime");
195
+ // Detection evidence only (runtime/hook-escape.ts): the gate stays fail-open
196
+ // and annotates its decision; the finding is also on the job row.
197
+ hookEscapeNote = describeHookEscapes(result);
191
198
  if (result.sessionId !== undefined && result.sessionId.length > 0) {
192
199
  // length>0 guard matches the other commands (Kimi alpha.4
193
200
  // challenge finding #3 — defense-in-depth, since
@@ -195,7 +202,7 @@ async function executeReviewGate(payload, assistantMessage, context) {
195
202
  // can't synthesize empty strings today, but the guard locks the
196
203
  // SQLite contract independent of upstream behavior).
197
204
  activeStore.updateRunningJob(job.job_id, {
198
- kimi_session_id: result.sessionId,
205
+ kimi_session_id_source: "announced", kimi_session_id: result.sessionId,
199
206
  });
200
207
  }
201
208
  const finalText = reassembleProseFromRecords(result.records);
@@ -216,7 +223,7 @@ async function executeReviewGate(payload, assistantMessage, context) {
216
223
  final_output_path: artifactPath,
217
224
  error: null,
218
225
  });
219
- return rendered.output;
226
+ return { ...rendered.output, hookEscapeNote };
220
227
  }
221
228
  catch (error) {
222
229
  const classified = classifyManagedCommandFailure(error, "review_gate", job.job_id);
@@ -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 { captureTimeoutPartialOutput, assertCliResultSuccess, reassembleProseFromRecords, warnIfSessionIdMissing } from "./cli-helpers.js";
20
+ import { captureTimeoutPartialOutput, assertCliResultSuccess, reassembleProseFromRecords, warnIfSessionIdMissing, warnHookEscapes } from "./cli-helpers.js";
21
21
  import { buildKimiSessionTitle, syncKimiSessionTitle } from "../session-title.js";
22
22
  // v1.0 cutover note (PR 2):
23
23
  //
@@ -79,9 +79,11 @@ export async function runReview(argv, context, commandType) {
79
79
  command_type: commandType,
80
80
  cwd: context.cwd,
81
81
  model: parsed.model ?? null,
82
- thinking: parsed.thinking ?? null,
82
+ thinking: null,
83
+ thinking_effort: parsed.thinkingEffort ?? null,
83
84
  background: false,
84
85
  budget_ms: parsed.budgetMs ?? null,
86
+ budget_source: parsed.budgetMs !== undefined ? "explicit" : "default",
85
87
  pid: process.pid,
86
88
  kimi_pid: null,
87
89
  status: "running",
@@ -112,6 +114,7 @@ export async function runReview(argv, context, commandType) {
112
114
  prompt: previewPrompt,
113
115
  commandLabel: commandType,
114
116
  model: parsed.model,
117
+ thinkingEffort: parsed.thinkingEffort,
115
118
  logPath,
116
119
  signal: handlers.signal,
117
120
  }, job.budget_ms ?? KIMI_REVIEW_PROMPT_TIMEOUT_MS, `${commandType}.prompt`);
@@ -120,6 +123,7 @@ export async function runReview(argv, context, commandType) {
120
123
  throw new RuntimeError(cancel.errorCodes.cancelled, cancel.cancelMessages.afterPrompt, `${commandType}.runtime`);
121
124
  }
122
125
  assertCliResultSuccess(result, `${commandType}.runtime`);
126
+ warnHookEscapes(result, commandType, context.stderr);
123
127
  // Persist whichever session id kimi announced. Review never resumes,
124
128
  // but the id is the only handle for post-hoc replay against
125
129
  // ~/.kimi-code/sessions/.
@@ -128,7 +132,7 @@ export async function runReview(argv, context, commandType) {
128
132
  // poison the row — consumers reading SQLite distinguish "missing"
129
133
  // (NULL) from "captured" (non-empty UUID) only via NULL-ness.
130
134
  // (Kimi alpha.4 challenge finding #3.)
131
- store.updateRunningJob(job.job_id, { kimi_session_id: result.sessionId });
135
+ store.updateRunningJob(job.job_id, { kimi_session_id_source: "announced", kimi_session_id: result.sessionId });
132
136
  }
133
137
  await syncKimiSessionTitle({
134
138
  promptText: previewPrompt,
@@ -159,13 +163,13 @@ export async function runReview(argv, context, commandType) {
159
163
  return rendered.output;
160
164
  }
161
165
  catch (error) {
166
+ const classified = classifyManagedCommandFailure(error, commandType, job.job_id, { background: job.background, budgetMs: job.budget_ms ?? KIMI_REVIEW_PROMPT_TIMEOUT_MS });
167
+ const partialOutput = await captureTimeoutPartialOutput(classified, store, job, context.env, handlers.signal);
162
168
  if (handlers.cancelling) {
163
169
  const cancelledError = new RuntimeError(cancel.errorCodes.cancelled, cancel.cancelMessages.default, `${commandType}.runtime`, error instanceof Error ? { cause: error } : undefined);
164
170
  await markJobCancelled(store, paths, job, cancel.cancelledSummary, cancelledError);
165
171
  throw cancelledError;
166
172
  }
167
- const classified = classifyManagedCommandFailure(error, commandType, job.job_id, { background: job.background, budgetMs: job.budget_ms ?? KIMI_REVIEW_PROMPT_TIMEOUT_MS });
168
- const partialOutput = captureTimeoutPartialOutput(classified, store, job);
169
173
  await markJobFailed(store, paths, job, classified, cancel.failedSummary, { partialOutput });
170
174
  throw classified;
171
175
  }