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
@@ -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 { parseHookEscapes, 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, thinking_effort_requested: job.thinking_effort ?? null, hook_escapes: parseHookEscapes(job.hook_escapes), budget_source: renderBudgetSource(job) }, null, 2)}\n`;
26
26
  });
27
27
  }
@@ -15,7 +15,7 @@ import { ensurePluginPaths, resolvePluginPaths } from "../paths.js";
15
15
  import { MAX_DURATION_MS, parseSwarmArgs } from "../parsing.js";
16
16
  import { readArtifact, renderManagedJobOutput, writeArtifact } from "../render.js";
17
17
  import { hookRefusalDetails, hookRefusalRetryProtocol, maybeWarnHookMissing, verifyHookInstalled, } from "../hooks/install.js";
18
- import { assertCliResultSuccess, reassembleProseFromRecords, warnIfSessionIdMissing } from "./cli-helpers.js";
18
+ import { assertCliResultSuccess, reassembleProseFromRecords, warnIfSessionIdMissing, warnHookEscapes } from "./cli-helpers.js";
19
19
  import { buildKimiSessionTitle, syncKimiSessionTitle } from "../session-title.js";
20
20
  // /kimi:swarm — READ-ONLY parallel fan-out (kimi-code 0.12.0 AgentSwarm tool).
21
21
  //
@@ -208,9 +208,11 @@ async function runReadSwarm(parsed, objective, context) {
208
208
  command_type: "review",
209
209
  cwd: context.cwd,
210
210
  model: parsed.model ?? null,
211
- thinking: parsed.thinking ?? null,
211
+ thinking: null,
212
+ thinking_effort: parsed.thinkingEffort ?? null,
212
213
  background: false,
213
214
  budget_ms: parsed.budgetMs ?? KIMI_SWARM_DEFAULT_BUDGET_MS,
215
+ budget_source: parsed.budgetMs !== undefined ? "explicit" : "default",
214
216
  pid: process.pid,
215
217
  kimi_pid: null,
216
218
  status: "running",
@@ -307,9 +309,11 @@ async function runWriteSwarm(parsed, objective, context) {
307
309
  command_type: "rescue",
308
310
  cwd: context.cwd,
309
311
  model: parsed.model ?? null,
310
- thinking: parsed.thinking ?? null,
312
+ thinking: null,
313
+ thinking_effort: parsed.thinkingEffort ?? null,
311
314
  background: false,
312
315
  budget_ms: parsed.budgetMs ?? KIMI_SWARM_DEFAULT_BUDGET_MS,
316
+ budget_source: parsed.budgetMs !== undefined ? "explicit" : "default",
313
317
  pid: process.pid,
314
318
  kimi_pid: null,
315
319
  status: "running",
@@ -491,6 +495,7 @@ writeMode) {
491
495
  // by other labels). Only set in write mode.
492
496
  trustedWorkspaceRoot: writeMode?.workspaceRoot,
493
497
  model: job.model ?? undefined,
498
+ thinkingEffort: job.thinking_effort ?? undefined,
494
499
  logPath: job.stream_log_path,
495
500
  signal: handlers.signal,
496
501
  }, budgetMs, "swarm.prompt");
@@ -499,10 +504,11 @@ writeMode) {
499
504
  throw new RuntimeError("SWARM_CANCELLED", "Swarm cancelled by user request.", "swarm.runtime");
500
505
  }
501
506
  assertCliResultSuccess(result, "swarm.runtime");
507
+ warnHookEscapes(result, "swarm", context.stderr);
502
508
  if (result.sessionId !== undefined &&
503
509
  result.sessionId.length > 0 &&
504
510
  result.sessionId !== job.kimi_session_id) {
505
- store.updateRunningJob(job.job_id, { kimi_session_id: result.sessionId });
511
+ store.updateRunningJob(job.job_id, { kimi_session_id_source: "announced", kimi_session_id: result.sessionId });
506
512
  }
507
513
  await syncKimiSessionTitle({
508
514
  promptText: objective,
@@ -11,7 +11,7 @@ export function companionHelp(argv) {
11
11
  : 1;
12
12
  if (!hasHelpFlag(argv.slice(argumentStart)))
13
13
  return undefined;
14
- const suffix = " Use --help or -h among the leading flags (before any prompt text or --) to show this usage.";
14
+ const suffix = " Thinking effort is a per-run request, not a guarantee. Use --help or -h among the leading flags (before any prompt text or --) to show this usage.";
15
15
  const usage = (name, positionals, flags, description) => `Usage: companion.sh ${name}${positionals ? ` ${positionals}` : ""}. ${description} Supported flags: ${flags}.${suffix}`;
16
16
  switch (command) {
17
17
  case "--help":
@@ -0,0 +1,71 @@
1
+ import { decideHookOutcomePure } from "./hooks/approval-policy.js";
2
+ // Upstream decorates results it still delivers verbatim otherwise (2.2.0,
3
+ // `agent/toolDedupe/toolDedupeService.ts`): the repeat breaker appends one
4
+ // `\n\n<system-reminder>…</system-reminder>` block from the 3rd identical call
5
+ // (`appendReminder`, also for vetoed results — `finalizeToolResult` runs the
6
+ // `onDidExecuteTool` hooks on every settled result), and at the 12th call it
7
+ // replaces the result with a fixed "not executed" text (`HANDOFF_VETO_TEXT`).
8
+ // Both still mean the hook's veto stood. Re-verify each certification.
9
+ const REMINDER_SUFFIX = /^(?:\n\n<system-reminder>[\s\S]*?<\/system-reminder>)+$/;
10
+ const HANDOFF_VETO_PREFIX = "This turn was ended by the repeat breaker after the same tool call was issued ";
11
+ /** True when `content` is the hook's denial, possibly carrying upstream's repeat-breaker decoration. */
12
+ export function carriesDenial(content, reason) {
13
+ if (content === reason)
14
+ return true;
15
+ if (content.startsWith(reason) && REMINDER_SUFFIX.test(content.slice(reason.length)))
16
+ return true;
17
+ return content.startsWith(HANDOFF_VETO_PREFIX);
18
+ }
19
+ /** Detection evidence, never enforcement. Results arrive after a tool may have
20
+ * executed, and the stream carries no outcome field: a result other than the
21
+ * managed denial is also what upstream emits for calls it rejected BEFORE the
22
+ * hook ran (unknown tool, disabled tool, invalid args, sensitive path, per-tool
23
+ * resolve errors) and what a coexisting hook or permission policy emits when it
24
+ * blocks first. So an escape is reported — logged, persisted, shown — and the
25
+ * run continues; failing the job needs positive execution evidence this stream
26
+ * does not provide. Filesystem policy is never replayed here. */
27
+ export function createHookEscapeMonitor(ctx) {
28
+ const pending = new Map();
29
+ return {
30
+ observe(record) {
31
+ if (!ctx.commandLabel)
32
+ return;
33
+ if (record.role === "assistant") {
34
+ for (const call of record.tool_calls ?? []) {
35
+ let args;
36
+ try {
37
+ args = JSON.parse(call.function.arguments);
38
+ }
39
+ catch {
40
+ args = {};
41
+ }
42
+ if (typeof args !== "object" || args === null || Array.isArray(args))
43
+ args = {};
44
+ const result = decideHookOutcomePure({
45
+ hook_event_name: "PreToolUse", tool_name: call.function.name,
46
+ tool_input: args, tool_call_id: call.id, cwd: ctx.cwd,
47
+ }, ctx);
48
+ if (!("evaluator" in result) && result.decision === "deny") {
49
+ pending.set(call.id, { reason: result.reason, tool_name: call.function.name });
50
+ }
51
+ }
52
+ }
53
+ else if (record.role === "tool") {
54
+ const denied = pending.get(record.tool_call_id);
55
+ if (denied) {
56
+ pending.delete(record.tool_call_id);
57
+ if (!carriesDenial(record.content, denied.reason)) {
58
+ return { tool_call_id: record.tool_call_id, tool_name: denied.tool_name,
59
+ kind: "mismatch", content_excerpt: record.content.slice(0, 200) };
60
+ }
61
+ }
62
+ }
63
+ },
64
+ /** Every denied call still without a result; call once at settlement. */
65
+ settle() {
66
+ const unresolved = [...pending.entries()].map(([tool_call_id, { tool_name }]) => ({ tool_call_id, tool_name, kind: "unresolved" }));
67
+ pending.clear();
68
+ return unresolved;
69
+ },
70
+ };
71
+ }
@@ -81,6 +81,21 @@ const KNOWN_LABELS = new Set([
81
81
  "swarm-write",
82
82
  ]);
83
83
  export async function decideHookOutcome(input, ctx) {
84
+ const result = decideHookOutcomePure(input, ctx);
85
+ if (!("evaluator" in result))
86
+ return result;
87
+ const toolName = typeof input.tool_name === "string" ? input.tool_name : "";
88
+ if (ctx.rescueEvaluator !== undefined) {
89
+ return await ctx.rescueEvaluator(result.workspaceRoot, toolName, input.tool_input);
90
+ }
91
+ if (READ_ONLY_TOOLS.has(toolName))
92
+ return { decision: "allow" };
93
+ return {
94
+ decision: "deny",
95
+ reason: `kimi-plugin-cc safety hook: ${ctx.commandLabel} evaluator not configured; tool "${toolName}" denied as a safety default.`,
96
+ };
97
+ }
98
+ export function decideHookOutcomePure(input, ctx) {
84
99
  const label = typeof ctx.commandLabel === "string" ? ctx.commandLabel : undefined;
85
100
  // Out-of-plugin context: any kimi process spawned without our env var
86
101
  // is direct user usage. The hook MUST NOT restrict it.
@@ -176,18 +191,7 @@ export async function decideHookOutcome(input, ctx) {
176
191
  "This is a plugin misconfiguration — writes are refused rather than risk the user's working tree.",
177
192
  };
178
193
  }
179
- if (ctx.rescueEvaluator !== undefined) {
180
- return await ctx.rescueEvaluator(workspaceRoot, toolName, input.tool_input);
181
- }
182
- // Stub for callers that didn't inject the evaluator (tests of the policy
183
- // function alone). The production entry script always injects it.
184
- if (READ_ONLY_TOOLS.has(toolName)) {
185
- return { decision: "allow" };
186
- }
187
- return {
188
- decision: "deny",
189
- reason: `kimi-plugin-cc safety hook: swarm-write evaluator not configured; tool "${toolName}" denied as a safety default.`,
190
- };
194
+ return { evaluator: "rescue", workspaceRoot };
191
195
  }
192
196
  case "rescue": {
193
197
  const workspaceRoot = typeof ctx.trustedWorkspaceRoot === "string" ? ctx.trustedWorkspaceRoot : "";
@@ -200,21 +204,7 @@ export async function decideHookOutcome(input, ctx) {
200
204
  "This is a plugin misconfiguration — writes are refused rather than trusting hook payload cwd.",
201
205
  };
202
206
  }
203
- if (ctx.rescueEvaluator !== undefined) {
204
- return await ctx.rescueEvaluator(workspaceRoot, toolName, input.tool_input);
205
- }
206
- // Stub for callers that didn't inject the evaluator (e.g.,
207
- // tests that exercise only the policy function). Allow
208
- // Read/Grep/Glob and friends; deny everything else. The
209
- // production entry script wires the real evaluator from
210
- // runtime/rescue-approval.ts.
211
- if (READ_ONLY_TOOLS.has(toolName)) {
212
- return { decision: "allow" };
213
- }
214
- return {
215
- decision: "deny",
216
- reason: `kimi-plugin-cc safety hook: rescue evaluator not configured; tool "${toolName}" denied as a safety default.`,
217
- };
207
+ return { evaluator: "rescue", workspaceRoot };
218
208
  }
219
209
  default:
220
210
  if (KNOWN_LABELS.has(label)) {
@@ -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
  }
@@ -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 {
@@ -70,6 +73,11 @@ export class JobStore {
70
73
  if (!tableHasColumn(this.db, "budget_ms")) {
71
74
  this.db.exec(`ALTER TABLE jobs ADD COLUMN budget_ms INTEGER;`);
72
75
  }
76
+ for (const column of ["kimi_session_id_source", "budget_source", "thinking_effort", "hook_escapes"]) {
77
+ if (!tableHasColumn(this.db, column)) {
78
+ this.db.exec("ALTER TABLE jobs ADD COLUMN " + column + " TEXT;");
79
+ }
80
+ }
73
81
  // Additive provenance migration. Existing rows deliberately remain NULL:
74
82
  // v1.9.5 plus kimi-code 0.33/0.34 could have routed to native v2 before the
75
83
  // legacy pin landed, so a blanket historical v1 backfill would invent fact.
@@ -98,6 +106,8 @@ export class JobStore {
98
106
  CREATE INDEX IF NOT EXISTS jobs_repo_type_updated_idx
99
107
  ON jobs (repo_id, command_type, updated_at DESC);
100
108
 
109
+ CREATE INDEX IF NOT EXISTS jobs_session_id_idx ON jobs (kimi_session_id);
110
+
101
111
  CREATE INDEX IF NOT EXISTS jobs_session_idx
102
112
  ON jobs (repo_id, kimi_session_id, updated_at DESC);
103
113
 
@@ -162,15 +172,15 @@ export class JobStore {
162
172
  try {
163
173
  this.db.run(`
164
174
  INSERT INTO jobs (
165
- job_id, repo_id, command_type, created_at, updated_at, cwd, model, thinking,
166
- background, pid, kimi_pid, status, kimi_session_id,
175
+ job_id, repo_id, command_type, created_at, updated_at, cwd, model, thinking, thinking_effort,
176
+ background, pid, kimi_pid, status, kimi_session_id, kimi_session_id_source, budget_source,
167
177
  operation_kind, intended_engine, observed_engine, kimi_version, system_version,
168
178
  kimi_command, kimi_prefix_args, plan_certification, resumed_from_job_id, safety_profile,
169
179
  agent_profile, prompt_digest, summary, phase, budget_ms, final_output_path, stream_log_path, error
170
180
  )
171
181
  VALUES (
172
- @job_id, @repo_id, @command_type, @created_at, @updated_at, @cwd, @model, @thinking,
173
- @background, @pid, @kimi_pid, @status, @kimi_session_id,
182
+ @job_id, @repo_id, @command_type, @created_at, @updated_at, @cwd, @model, @thinking, @thinking_effort,
183
+ @background, @pid, @kimi_pid, @status, @kimi_session_id, @kimi_session_id_source, @budget_source,
174
184
  @operation_kind, @intended_engine, @observed_engine, @kimi_version, @system_version,
175
185
  @kimi_command, @kimi_prefix_args, @plan_certification, @resumed_from_job_id, @safety_profile,
176
186
  @agent_profile, @prompt_digest, @summary, @phase, @budget_ms, @final_output_path, @stream_log_path, @error
@@ -198,6 +208,10 @@ export class JobStore {
198
208
  const row = this.db.get("SELECT * FROM jobs WHERE job_id = ?", jobId);
199
209
  return row ? hydrateRow(row) : null;
200
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
+ }
201
215
  findLatestJob(options) {
202
216
  const conditions = ["repo_id = @repo_id"];
203
217
  const params = {
@@ -247,6 +261,18 @@ export class JobStore {
247
261
  updateRunningJob(jobId, patch) {
248
262
  return this.updateWhere(jobId, patch, "status = 'running'");
249
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
+ }
250
276
  /**
251
277
  * Evidence-only backfill for pre-provenance rows. COALESCE prevents forensic
252
278
  * inspection from overwriting a field already recorded at execution time.
@@ -280,10 +306,10 @@ export class JobStore {
280
306
  return this.updateWhere(jobId, { ...patch, status: "cancelled", pid: null, kimi_pid: null }, "status = 'running'");
281
307
  }
282
308
  static JOBS_COLUMNS = new Set([
283
- "status", "summary", "phase", "budget_ms", "pid", "kimi_pid",
309
+ "status", "summary", "phase", "budget_ms", "budget_source", "kimi_session_id_source", "pid", "kimi_pid",
284
310
  "final_output_path", "stream_log_path", "kimi_session_id",
285
311
  "error", "command_type", "prompt_digest", "repo_id",
286
- "cwd", "model", "thinking", "background", "agent_profile",
312
+ "cwd", "model", "thinking", "thinking_effort", "hook_escapes", "background", "agent_profile",
287
313
  "operation_kind", "intended_engine", "observed_engine", "kimi_version",
288
314
  "system_version", "kimi_command", "kimi_prefix_args", "plan_certification",
289
315
  "resumed_from_job_id", "safety_profile",
@@ -323,8 +349,11 @@ function hydrateRow(row) {
323
349
  function serializeRecord(record) {
324
350
  return {
325
351
  ...record,
352
+ thinking_effort: record.thinking_effort ?? null,
326
353
  phase: record.phase ?? null,
327
354
  budget_ms: record.budget_ms ?? null,
355
+ budget_source: record.budget_source ?? null,
356
+ kimi_session_id_source: record.kimi_session_id_source ?? null,
328
357
  operation_kind: record.operation_kind ?? null,
329
358
  intended_engine: record.intended_engine ?? null,
330
359
  observed_engine: record.observed_engine ?? null,
@@ -562,3 +591,19 @@ export function withJobStoreSync(paths, fn) {
562
591
  store?.close();
563
592
  }
564
593
  }
594
+ /** Old non-null budgets have unknown provenance; never invent explicit intent. */
595
+ /** Parsed `hook_escapes` column for JSON envelopes: [] when NULL or unreadable. */
596
+ export function parseHookEscapes(raw) {
597
+ if (!raw)
598
+ return [];
599
+ try {
600
+ const parsed = JSON.parse(raw);
601
+ return Array.isArray(parsed) ? parsed : [];
602
+ }
603
+ catch {
604
+ return [];
605
+ }
606
+ }
607
+ export function renderBudgetSource(job) {
608
+ return job.budget_source ?? (job.budget_ms === null ? "default" : "unknown");
609
+ }
@@ -206,6 +206,8 @@ export function observedExecutionFields(result) {
206
206
  return {
207
207
  observed_engine: result.observedEngine,
208
208
  system_version: result.systemVersion ?? null,
209
+ // Detection evidence from runtime/hook-escape.ts, JSON array or NULL.
210
+ hook_escapes: result.hookEscapes && result.hookEscapes.length > 0 ? JSON.stringify(result.hookEscapes) : null,
209
211
  };
210
212
  }
211
213
  export function executionPlanFromPersisted(fields) {
@@ -45,7 +45,7 @@ export function classifyManagedCommandFailure(error, commandType, jobId, options
45
45
  if (error instanceof RuntimeError) {
46
46
  details.cause_code = error.code;
47
47
  if (error.code === "RESPONSE_TIMEOUT") {
48
- for (const key of ["budget_ms", "record_count", "partial_output_chars", "aborted", "settled_post_abort"]) {
48
+ for (const key of ["budget_ms", "record_count", "partial_output_chars", "aborted", "settled_post_abort", "hook_escapes"]) {
49
49
  if (error.details[key] !== undefined)
50
50
  details[key] = error.details[key];
51
51
  }
@@ -8,7 +8,7 @@
8
8
  // startup failures surface as CLI_SPAWN_FAILED / CLI_PROCESS_ERROR —
9
9
  // not as separate timeouts. So only the per-command response budgets
10
10
  // remain.
11
- // Budgets are sized for thinking-on workflow (the default since v1.0).
11
+ // Budgets accommodate thinking-capable workflows; per-run effort is only a request.
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
@@ -49,15 +49,11 @@ export const KIMI_SWARM_DEFAULT_BUDGET_MS = 1_800_000; // 30 minutes
49
49
  * Review-gate budget. Fires inside Claude Code's Stop hook so any value
50
50
  * above the user's perceptible wait makes the gate feel broken.
51
51
  *
52
- * kimi-code 0.1.1 has no per-spawn CLI flag for thinking control
53
- * (Round 2 multi-agent review caught this — emitting `--no-thinking`
54
- * triggers an unknown-option crash). The 8s budget assumes the user
55
- * has either `default_thinking = false` or `[thinking].mode = "off"`
56
- * in `~/.kimi-code/config.toml`, OR a non-thinking-capable model
57
- * selected for review-gate (via KIMI_PLUGIN_CC_REVIEW_GATE_MODEL).
58
- * Under thinking-on the gate will time out — the gate is fail-open on
59
- * timeout by design, so the worst case is the gate becomes
60
- * always-allow. See docs/safety.md.
52
+ * The gate requests off through KIMI_MODEL_THINKING_EFFORT by default;
53
+ * KIMI_PLUGIN_CC_REVIEW_GATE_THINKING can inherit or request another effort.
54
+ * BYO/non-trait-driven providers and always-thinking models can ignore it.
55
+ * The 8s timeout remains fail-open; a request never guarantees completion.
56
+ * Hook-enforcement escape is a separate blocking failure. See docs/safety.md.
61
57
  */
62
58
  export const KIMI_REVIEW_GATE_TIMEOUT_MS = 8_000;
63
59
  export function isTimeoutCode(code) {
@@ -198,8 +198,8 @@ export const KIMI_TESTED_MINORS = [
198
198
  // import it; `-p` stdout stays the direct PromptJsonWriter), session-lifecycle
199
199
  // changes that only HELP our cancellation story (active-turn cancel on close +
200
200
  // BACKGROUND_KEEP_ALIVE_ON_EXIT default flipped true→false), an `alwaysThinking`
201
- // model-capability flag (read-only detection, the H5 thinking-control knob is
202
- // still upstream-blocked), a SIGHUP cleanup handler (exit 129; SIGTERM still
201
+ // model-capability flag (the H5 env request ships in plugin 2.2.1,
202
+ // but always-thinking models need not honour it), a SIGHUP cleanup handler (exit 129; SIGTERM still
203
203
  // exits 143 and runs cleanup — our SIGTERM→SIGKILL reaping is unaffected), and
204
204
  // a new builtin `import-from-cc-codex` skill (not a plugin surface).
205
205
  // See .claude/kimi-code-research/reports/72-76 for the audit reports.