kimi-plugin-cc 2.4.4 → 2.5.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 (119) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/CHANGELOG.md +21 -0
  4. package/README.md +50 -34
  5. package/dist/commands/swarm.js +1 -1
  6. package/dist/dsh/companion.js +174 -0
  7. package/dist/dsh/index.js +156 -0
  8. package/dist/hooks/config-safety.js +3 -2
  9. package/dist/hooks/install-paths.js +2 -2
  10. package/dist/kimi-engine.js +1 -0
  11. package/dist/swarm-activation.js +1 -1
  12. package/dist/version.js +1 -1
  13. package/docs/migration.md +4 -4
  14. package/docs/models.md +5 -1
  15. package/docs/registry-releases.md +20 -17
  16. package/docs/safety.md +28 -15
  17. package/docs/swarm-recovery.md +1 -1
  18. package/docs/upstream-2.4.1-certification.md +54 -0
  19. package/package.json +41 -6
  20. package/plugins/kimi-codex/.codex-plugin/plugin.json +1 -1
  21. package/plugins/kimi-codex/dist/commands/swarm.js +1 -1
  22. package/plugins/kimi-codex/dist/dsh/companion.js +174 -0
  23. package/plugins/kimi-codex/dist/dsh/index.js +156 -0
  24. package/plugins/kimi-codex/dist/hooks/config-safety.js +3 -2
  25. package/plugins/kimi-codex/dist/hooks/install-paths.js +2 -2
  26. package/plugins/kimi-codex/dist/kimi-engine.js +1 -0
  27. package/plugins/kimi-codex/dist/swarm-activation.js +1 -1
  28. package/plugins/kimi-codex/dist/version.js +1 -1
  29. package/plugins/kimi-dsh/LICENSE +201 -0
  30. package/plugins/kimi-dsh/README.md +80 -0
  31. package/plugins/kimi-dsh/cordis.patch.yml +45 -0
  32. package/plugins/kimi-dsh/dist/background-spawn.js +216 -0
  33. package/plugins/kimi-dsh/dist/cli-cancellation.js +43 -0
  34. package/plugins/kimi-dsh/dist/cli-client.js +876 -0
  35. package/plugins/kimi-dsh/dist/commands/ask.js +336 -0
  36. package/plugins/kimi-dsh/dist/commands/cancel.js +216 -0
  37. package/plugins/kimi-dsh/dist/commands/cli-helpers.js +201 -0
  38. package/plugins/kimi-dsh/dist/commands/models.js +140 -0
  39. package/plugins/kimi-dsh/dist/commands/not-implemented.js +4 -0
  40. package/plugins/kimi-dsh/dist/commands/pursue.js +307 -0
  41. package/plugins/kimi-dsh/dist/commands/recover-swarm.js +135 -0
  42. package/plugins/kimi-dsh/dist/commands/registry.js +74 -0
  43. package/plugins/kimi-dsh/dist/commands/repair-sessions.js +160 -0
  44. package/plugins/kimi-dsh/dist/commands/replay.js +124 -0
  45. package/plugins/kimi-dsh/dist/commands/rescue.js +347 -0
  46. package/plugins/kimi-dsh/dist/commands/result.js +66 -0
  47. package/plugins/kimi-dsh/dist/commands/review-gate.js +390 -0
  48. package/plugins/kimi-dsh/dist/commands/review.js +220 -0
  49. package/plugins/kimi-dsh/dist/commands/setup.js +1047 -0
  50. package/plugins/kimi-dsh/dist/commands/status.js +27 -0
  51. package/plugins/kimi-dsh/dist/commands/swarm.js +754 -0
  52. package/plugins/kimi-dsh/dist/commands/worker.js +25 -0
  53. package/plugins/kimi-dsh/dist/companion.js +97 -0
  54. package/plugins/kimi-dsh/dist/config.js +22 -0
  55. package/plugins/kimi-dsh/dist/dsh/companion.js +174 -0
  56. package/plugins/kimi-dsh/dist/dsh/index.js +156 -0
  57. package/plugins/kimi-dsh/dist/errors.js +40 -0
  58. package/plugins/kimi-dsh/dist/git.js +224 -0
  59. package/plugins/kimi-dsh/dist/help.js +43 -0
  60. package/plugins/kimi-dsh/dist/hook-escape.js +71 -0
  61. package/plugins/kimi-dsh/dist/hooks/approval-hook.js +101 -0
  62. package/plugins/kimi-dsh/dist/hooks/approval-policy.js +211 -0
  63. package/plugins/kimi-dsh/dist/hooks/config-safety.js +653 -0
  64. package/plugins/kimi-dsh/dist/hooks/install-paths.js +428 -0
  65. package/plugins/kimi-dsh/dist/hooks/install.js +210 -0
  66. package/plugins/kimi-dsh/dist/hooks/managed-block.js +619 -0
  67. package/plugins/kimi-dsh/dist/hooks/review-gate-stop.js +19 -0
  68. package/plugins/kimi-dsh/dist/job-id.js +12 -0
  69. package/plugins/kimi-dsh/dist/job-store.js +659 -0
  70. package/plugins/kimi-dsh/dist/jobs.js +217 -0
  71. package/plugins/kimi-dsh/dist/kimi-command.js +70 -0
  72. package/plugins/kimi-dsh/dist/kimi-engine.js +632 -0
  73. package/plugins/kimi-dsh/dist/kimi-errors.js +260 -0
  74. package/plugins/kimi-dsh/dist/kimi-home.js +9 -0
  75. package/plugins/kimi-dsh/dist/kimi-timeouts.js +52 -0
  76. package/plugins/kimi-dsh/dist/kimi-version-probe.js +216 -0
  77. package/plugins/kimi-dsh/dist/logging.js +22 -0
  78. package/plugins/kimi-dsh/dist/mcp/binding.js +58 -0
  79. package/plugins/kimi-dsh/dist/mcp/job-control.js +345 -0
  80. package/plugins/kimi-dsh/dist/mcp/launcher.js +257 -0
  81. package/plugins/kimi-dsh/dist/mcp/operations.js +301 -0
  82. package/plugins/kimi-dsh/dist/mcp/protocol.js +275 -0
  83. package/plugins/kimi-dsh/dist/mcp/server.js +8 -0
  84. package/plugins/kimi-dsh/dist/mcp/stdio.js +78 -0
  85. package/plugins/kimi-dsh/dist/mcp/supervised-child.js +15 -0
  86. package/plugins/kimi-dsh/dist/mcp/tool-result.js +7 -0
  87. package/plugins/kimi-dsh/dist/mcp/workspace.js +34 -0
  88. package/plugins/kimi-dsh/dist/model-command.js +32 -0
  89. package/plugins/kimi-dsh/dist/native-v2-preflight.js +417 -0
  90. package/plugins/kimi-dsh/dist/package.json +1 -0
  91. package/plugins/kimi-dsh/dist/parsing.js +620 -0
  92. package/plugins/kimi-dsh/dist/paths.js +27 -0
  93. package/plugins/kimi-dsh/dist/plugin-data.js +98 -0
  94. package/plugins/kimi-dsh/dist/process-tree.js +479 -0
  95. package/plugins/kimi-dsh/dist/prompt-size.js +18 -0
  96. package/plugins/kimi-dsh/dist/render.js +177 -0
  97. package/plugins/kimi-dsh/dist/rescue-approval.js +865 -0
  98. package/plugins/kimi-dsh/dist/schemas/review-gate-output.js +43 -0
  99. package/plugins/kimi-dsh/dist/session-recovery.js +200 -0
  100. package/plugins/kimi-dsh/dist/session-title.js +293 -0
  101. package/plugins/kimi-dsh/dist/stream-json.js +346 -0
  102. package/plugins/kimi-dsh/dist/swarm-activation.js +834 -0
  103. package/plugins/kimi-dsh/dist/swarm-recovery.js +134 -0
  104. package/plugins/kimi-dsh/dist/types.js +1 -0
  105. package/plugins/kimi-dsh/dist/vendor/shell-quote/LICENSE +24 -0
  106. package/plugins/kimi-dsh/dist/vendor/shell-quote/index.js +1 -0
  107. package/plugins/kimi-dsh/dist/vendor/shell-quote/parse.js +229 -0
  108. package/plugins/kimi-dsh/dist/vendor/smol-toml/LICENSE +24 -0
  109. package/plugins/kimi-dsh/dist/vendor/smol-toml/date.js +156 -0
  110. package/plugins/kimi-dsh/dist/vendor/smol-toml/error.js +67 -0
  111. package/plugins/kimi-dsh/dist/vendor/smol-toml/extract.js +293 -0
  112. package/plugins/kimi-dsh/dist/vendor/smol-toml/parse.js +164 -0
  113. package/plugins/kimi-dsh/dist/vendor/smol-toml/primitive.js +151 -0
  114. package/plugins/kimi-dsh/dist/vendor/smol-toml/struct.js +156 -0
  115. package/plugins/kimi-dsh/dist/vendor/smol-toml/util.js +57 -0
  116. package/plugins/kimi-dsh/dist/version.js +7 -0
  117. package/plugins/kimi-dsh/package.json +48 -0
  118. package/plugins/kimi-dsh/scripts/companion.sh +37 -0
  119. package/plugins/kimi-dsh/scripts/review-gate-hook.sh +32 -0
@@ -0,0 +1,659 @@
1
+ import { createRequire } from "node:module";
2
+ import { RuntimeError, formatError } from "./errors.js";
3
+ export class JobStore {
4
+ db;
5
+ constructor(paths) {
6
+ // Open the adapter into a local before assigning to this.db so the
7
+ // catch can release the handle even if pragma throws AFTER the
8
+ // adapter opened — `journal_mode = WAL` on a read-only mount, for
9
+ // example, would otherwise leak the handle because the constructor
10
+ // exits via throw before any caller reference exists for cleanup.
11
+ let db;
12
+ try {
13
+ db = createSqliteAdapter(paths.stateDbPath);
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).
17
+ db.pragma("busy_timeout = 5000");
18
+ db.pragma("journal_mode = WAL");
19
+ }
20
+ catch (error) {
21
+ try {
22
+ db?.close();
23
+ }
24
+ catch {
25
+ // Best-effort — original error takes priority over close failure.
26
+ }
27
+ throw translateSqliteError(error);
28
+ }
29
+ this.db = db;
30
+ // Every `this.db.exec` / `tableHasColumn` below can throw on a
31
+ // corrupt DB, full disk, mid-migration failure, etc. Catch
32
+ // internally, close the adapter, and rethrow the translated error
33
+ // so the SQLite handle never leaks past a failed migration.
34
+ try {
35
+ this.db.exec(`
36
+ CREATE TABLE IF NOT EXISTS jobs (
37
+ job_id TEXT PRIMARY KEY,
38
+ repo_id TEXT NOT NULL,
39
+ command_type TEXT NOT NULL,
40
+ created_at TEXT NOT NULL,
41
+ updated_at TEXT NOT NULL,
42
+ cwd TEXT NOT NULL,
43
+ model TEXT,
44
+ thinking INTEGER,
45
+ background INTEGER NOT NULL,
46
+ pid INTEGER,
47
+ kimi_pid INTEGER,
48
+ status TEXT NOT NULL,
49
+ kimi_session_id TEXT,
50
+ operation_kind TEXT,
51
+ intended_engine TEXT,
52
+ observed_engine TEXT,
53
+ kimi_version TEXT,
54
+ system_version TEXT,
55
+ kimi_command TEXT,
56
+ kimi_prefix_args TEXT,
57
+ plan_certification TEXT,
58
+ resumed_from_job_id TEXT,
59
+ safety_profile TEXT,
60
+ agent_profile TEXT NOT NULL,
61
+ prompt_digest TEXT NOT NULL,
62
+ summary TEXT NOT NULL,
63
+ phase TEXT,
64
+ budget_ms INTEGER,
65
+ final_output_path TEXT,
66
+ stream_log_path TEXT NOT NULL,
67
+ error TEXT
68
+ );
69
+ `);
70
+ if (!tableHasColumn(this.db, "phase")) {
71
+ this.db.exec(`ALTER TABLE jobs ADD COLUMN phase TEXT;`);
72
+ }
73
+ if (!tableHasColumn(this.db, "budget_ms")) {
74
+ this.db.exec(`ALTER TABLE jobs ADD COLUMN budget_ms INTEGER;`);
75
+ }
76
+ for (const column of ["kimi_session_id_source", "budget_source", "thinking_effort", "hook_escapes"]) {
77
+ if (!tableHasColumn(this.db, column)) {
78
+ this.db.exec("ALTER TABLE jobs ADD COLUMN " + column + " TEXT;");
79
+ }
80
+ }
81
+ // Additive provenance migration. Existing rows deliberately remain NULL:
82
+ // v1.9.5 plus kimi-code 0.33/0.34 could have routed to native v2 before the
83
+ // legacy pin landed, so a blanket historical v1 backfill would invent fact.
84
+ const provenanceColumns = [
85
+ "operation_kind",
86
+ "intended_engine",
87
+ "observed_engine",
88
+ "kimi_version",
89
+ "system_version",
90
+ "kimi_command",
91
+ "kimi_prefix_args",
92
+ "plan_certification",
93
+ "resumed_from_job_id",
94
+ "safety_profile",
95
+ ];
96
+ for (const column of provenanceColumns) {
97
+ if (!tableHasColumn(this.db, column)) {
98
+ this.db.exec(`ALTER TABLE jobs ADD COLUMN ${column} TEXT;`);
99
+ }
100
+ }
101
+ this.db.exec(`
102
+ CREATE TABLE IF NOT EXISTS mcp_tasks (
103
+ task_id TEXT PRIMARY KEY,
104
+ operation_kind TEXT NOT NULL,
105
+ repo_id TEXT NOT NULL,
106
+ workspace_cwd TEXT NOT NULL,
107
+ companion_pid INTEGER,
108
+ companion_identity TEXT,
109
+ kimi_identity TEXT,
110
+ stdout_path TEXT NOT NULL,
111
+ stderr_path TEXT NOT NULL,
112
+ launch_state TEXT NOT NULL,
113
+ launch_error TEXT,
114
+ created_at TEXT NOT NULL,
115
+ attached_at TEXT,
116
+ updated_at TEXT NOT NULL
117
+ );
118
+
119
+ CREATE INDEX IF NOT EXISTS mcp_tasks_workspace_updated_idx
120
+ ON mcp_tasks (workspace_cwd, updated_at DESC);
121
+
122
+ CREATE INDEX IF NOT EXISTS jobs_repo_updated_idx
123
+ ON jobs (repo_id, updated_at DESC);
124
+
125
+ CREATE TABLE IF NOT EXISTS swarm_recovery (job_id TEXT PRIMARY KEY, payload TEXT NOT NULL, recovery_owner TEXT);
126
+
127
+ CREATE INDEX IF NOT EXISTS jobs_repo_type_updated_idx
128
+ ON jobs (repo_id, command_type, updated_at DESC);
129
+
130
+ CREATE INDEX IF NOT EXISTS jobs_session_id_idx ON jobs (kimi_session_id);
131
+
132
+ CREATE INDEX IF NOT EXISTS jobs_session_idx
133
+ ON jobs (repo_id, kimi_session_id, updated_at DESC);
134
+
135
+ DROP INDEX IF EXISTS jobs_running_rescue_session_idx;
136
+
137
+ -- Reconcile orphaned duplicate running rows before creating the unique
138
+ -- partial index. A 0.1.3 database with a hard-crashed worker can contain
139
+ -- multiple (repo_id, command_type, kimi_session_id) rows at status='running',
140
+ -- which would fail the CREATE UNIQUE INDEX at open and brick the store.
141
+ -- Keep the most recently updated row per group; mark the rest as failed.
142
+ UPDATE jobs
143
+ SET status = 'failed',
144
+ updated_at = strftime('%Y-%m-%dT%H:%M:%fZ', 'now'),
145
+ error = COALESCE(
146
+ error,
147
+ '{"code":"JOB_STORE_ORPHANED_ON_UPGRADE","message":"Orphaned duplicate running job reconciled on 0.1.4 schema upgrade","stage":"job-store.migrate"}'
148
+ )
149
+ WHERE status = 'running'
150
+ AND command_type IN ('rescue', 'ask')
151
+ AND kimi_session_id IS NOT NULL
152
+ AND rowid NOT IN (
153
+ SELECT rowid FROM (
154
+ SELECT rowid,
155
+ ROW_NUMBER() OVER (
156
+ PARTITION BY repo_id, command_type, kimi_session_id
157
+ ORDER BY updated_at DESC, rowid DESC
158
+ ) AS rn
159
+ FROM jobs
160
+ WHERE status = 'running'
161
+ AND command_type IN ('rescue', 'ask')
162
+ AND kimi_session_id IS NOT NULL
163
+ )
164
+ WHERE rn = 1
165
+ );
166
+
167
+ CREATE UNIQUE INDEX IF NOT EXISTS jobs_running_session_idx
168
+ ON jobs (repo_id, command_type, kimi_session_id)
169
+ WHERE status = 'running' AND command_type IN ('rescue', 'ask');
170
+
171
+ -- 0.1.6 migration: rename adversarial_review to challenge
172
+ UPDATE jobs SET command_type = 'challenge' WHERE command_type = 'adversarial_review';
173
+ `);
174
+ if (!mcpTableHasColumn(this.db, "kimi_identity")) {
175
+ this.db.exec("ALTER TABLE mcp_tasks ADD COLUMN kimi_identity TEXT;");
176
+ }
177
+ }
178
+ catch (error) {
179
+ // Migration failed — close the adapter so the OS handle is released
180
+ // before propagating. Wrap close() in its own try so a double-fault
181
+ // doesn't mask the original migration error.
182
+ try {
183
+ this.db.close();
184
+ }
185
+ catch {
186
+ // intentionally swallow — original error takes priority
187
+ }
188
+ throw translateSqliteError(error);
189
+ }
190
+ }
191
+ getSwarmRecovery(jobId) {
192
+ const row = this.db.get("SELECT payload, recovery_owner FROM swarm_recovery WHERE job_id = ?", jobId);
193
+ return row ? { record: JSON.parse(row.payload), owner: row.recovery_owner } : null;
194
+ }
195
+ saveSwarmRecovery(jobId, record) {
196
+ this.db.run("INSERT INTO swarm_recovery (job_id, payload) VALUES (?, ?) ON CONFLICT(job_id) DO UPDATE SET payload = excluded.payload", [jobId, JSON.stringify(record)]);
197
+ }
198
+ claimSwarmRecovery(jobId, expected, owner) {
199
+ this.db.run("UPDATE swarm_recovery SET recovery_owner = ? WHERE job_id = ? AND recovery_owner IS ?", [owner, jobId, expected]);
200
+ return this.db.get("SELECT changes() AS count").count === 1;
201
+ }
202
+ close() {
203
+ this.db.close();
204
+ }
205
+ createJob(input) {
206
+ const now = new Date().toISOString();
207
+ try {
208
+ this.db.run(`
209
+ INSERT INTO jobs (
210
+ job_id, repo_id, command_type, created_at, updated_at, cwd, model, thinking, thinking_effort,
211
+ background, pid, kimi_pid, status, kimi_session_id, kimi_session_id_source, budget_source,
212
+ operation_kind, intended_engine, observed_engine, kimi_version, system_version,
213
+ kimi_command, kimi_prefix_args, plan_certification, resumed_from_job_id, safety_profile,
214
+ agent_profile, prompt_digest, summary, phase, budget_ms, final_output_path, stream_log_path, error
215
+ )
216
+ VALUES (
217
+ @job_id, @repo_id, @command_type, @created_at, @updated_at, @cwd, @model, @thinking, @thinking_effort,
218
+ @background, @pid, @kimi_pid, @status, @kimi_session_id, @kimi_session_id_source, @budget_source,
219
+ @operation_kind, @intended_engine, @observed_engine, @kimi_version, @system_version,
220
+ @kimi_command, @kimi_prefix_args, @plan_certification, @resumed_from_job_id, @safety_profile,
221
+ @agent_profile, @prompt_digest, @summary, @phase, @budget_ms, @final_output_path, @stream_log_path, @error
222
+ )
223
+ `, {
224
+ ...serializeRecord(input),
225
+ created_at: now,
226
+ updated_at: now,
227
+ });
228
+ }
229
+ catch (error) {
230
+ if (isSqliteConstraintError(error)) {
231
+ if (input.command_type === "rescue") {
232
+ throw new RuntimeError("RESCUE_ALREADY_RUNNING", `A rescue job for session ${input.kimi_session_id ?? "<unknown>"} is already running in this repo. Use /kimi:status to find it.`, "rescue.resume", error instanceof Error ? { cause: error } : undefined);
233
+ }
234
+ if (input.command_type === "ask") {
235
+ throw new RuntimeError("ASK_ALREADY_RUNNING", `An ask session ${input.kimi_session_id ?? "<unknown>"} is already running in this repo. Use /kimi:status to find it.`, "ask.resume", error instanceof Error ? { cause: error } : undefined);
236
+ }
237
+ }
238
+ throw translateSqliteError(error);
239
+ }
240
+ return this.getJob(input.job_id);
241
+ }
242
+ getJob(jobId) {
243
+ const row = this.db.get("SELECT * FROM jobs WHERE job_id = ?", jobId);
244
+ return row ? hydrateRow(row) : null;
245
+ }
246
+ /** Global across repos and terminal states; optionally exclude the current row. */
247
+ hasSessionId(sessionId, exceptJobId = "") {
248
+ return Boolean(this.db.get("SELECT 1 FROM jobs WHERE kimi_session_id = ? AND job_id != ? LIMIT 1", sessionId, exceptJobId));
249
+ }
250
+ findLatestJob(options) {
251
+ const conditions = ["repo_id = @repo_id"];
252
+ const params = {
253
+ repo_id: options.repoId,
254
+ };
255
+ if (options.commandType) {
256
+ conditions.push("command_type = @command_type");
257
+ params.command_type = options.commandType;
258
+ }
259
+ if (options.terminalOnly) {
260
+ conditions.push("status IN ('completed', 'failed', 'cancelled')");
261
+ }
262
+ if (options.runningOnly) {
263
+ conditions.push("status = 'running'");
264
+ }
265
+ const row = this.db.get(`SELECT * FROM jobs WHERE ${conditions.join(" AND ")} ORDER BY updated_at DESC LIMIT 1`, params);
266
+ return row ? hydrateRow(row) : null;
267
+ }
268
+ listRunningJobsWithProcessHints() {
269
+ const rows = this.db.all(`SELECT * FROM jobs WHERE status = 'running' AND (pid IS NOT NULL OR kimi_pid IS NOT NULL)`);
270
+ return rows.map(hydrateRow);
271
+ }
272
+ findRescueJobBySession(repoId, sessionId) {
273
+ const row = this.db.get(`
274
+ SELECT *
275
+ FROM jobs
276
+ WHERE repo_id = ?
277
+ AND command_type = 'rescue'
278
+ AND kimi_session_id = ?
279
+ ORDER BY updated_at DESC
280
+ LIMIT 1
281
+ `, repoId, sessionId);
282
+ return row ? hydrateRow(row) : null;
283
+ }
284
+ findAskJobBySession(repoId, sessionId) {
285
+ const row = this.db.get(`
286
+ SELECT *
287
+ FROM jobs
288
+ WHERE repo_id = ?
289
+ AND command_type = 'ask'
290
+ AND kimi_session_id = ?
291
+ ORDER BY updated_at DESC
292
+ LIMIT 1
293
+ `, repoId, sessionId);
294
+ return row ? hydrateRow(row) : null;
295
+ }
296
+ createMcpTask(input) {
297
+ const now = new Date().toISOString();
298
+ try {
299
+ this.db.run(`
300
+ INSERT INTO mcp_tasks (
301
+ task_id, operation_kind, repo_id, workspace_cwd, companion_pid, companion_identity, kimi_identity,
302
+ stdout_path, stderr_path, launch_state, launch_error, created_at, attached_at, updated_at
303
+ ) VALUES (
304
+ @task_id, @operation_kind, @repo_id, @workspace_cwd, NULL, NULL, NULL,
305
+ @stdout_path, @stderr_path, 'starting', NULL, @created_at, NULL, @updated_at
306
+ )
307
+ `, { ...input, created_at: now, updated_at: now });
308
+ }
309
+ catch (error) {
310
+ throw translateSqliteError(error);
311
+ }
312
+ return this.getMcpTask(input.task_id);
313
+ }
314
+ getMcpTask(taskId) {
315
+ return this.db.get("SELECT * FROM mcp_tasks WHERE task_id = ?", taskId) ?? null;
316
+ }
317
+ updateMcpTask(taskId, patch) {
318
+ const updates = ["updated_at = @updated_at"];
319
+ const params = { task_id: taskId, updated_at: new Date().toISOString() };
320
+ for (const key of ["companion_pid", "companion_identity", "kimi_identity", "launch_state", "launch_error", "attached_at"]) {
321
+ const value = patch[key];
322
+ if (value === undefined)
323
+ continue;
324
+ updates.push(`${key} = @${key}`);
325
+ params[key] = value;
326
+ }
327
+ this.db.run(`UPDATE mcp_tasks SET ${updates.join(", ")} WHERE task_id = @task_id`, params);
328
+ return this.getMcpTask(taskId);
329
+ }
330
+ updateRunningJob(jobId, patch) {
331
+ return this.updateWhere(jobId, patch, "status = 'running'");
332
+ }
333
+ /** Recheck uniqueness atomically: another worker can claim an ID during the
334
+ * asynchronous directory scan. Ordinary announced/resumed IDs may be shared. */
335
+ claimRecoveredSessionId(jobId, sessionId) {
336
+ this.db.run(`UPDATE jobs
337
+ SET kimi_session_id = @session_id, kimi_session_id_source = 'recovered_session_dir',
338
+ updated_at = @updated_at
339
+ WHERE job_id = @job_id AND status = 'running' AND kimi_session_id IS NULL
340
+ 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() });
341
+ const job = this.getJob(jobId);
342
+ return job?.status === "running" && job.kimi_session_id === sessionId &&
343
+ job.kimi_session_id_source === "recovered_session_dir" ? job : null;
344
+ }
345
+ /**
346
+ * Evidence-only backfill for pre-provenance rows. COALESCE prevents forensic
347
+ * inspection from overwriting a field already recorded at execution time.
348
+ * updated_at is intentionally unchanged so a status lookup cannot reorder
349
+ * historical jobs merely because their logs were inspected.
350
+ */
351
+ backfillHistoricalProvenance(jobId, patch) {
352
+ this.db.run(`
353
+ UPDATE jobs
354
+ SET operation_kind = COALESCE(operation_kind, @operation_kind),
355
+ observed_engine = COALESCE(observed_engine, @observed_engine),
356
+ kimi_version = COALESCE(kimi_version, @kimi_version),
357
+ system_version = COALESCE(system_version, @system_version)
358
+ WHERE job_id = @job_id
359
+ `, {
360
+ job_id: jobId,
361
+ operation_kind: patch.operation_kind ?? null,
362
+ observed_engine: patch.observed_engine ?? null,
363
+ kimi_version: patch.kimi_version ?? null,
364
+ system_version: patch.system_version ?? null,
365
+ });
366
+ return this.getJob(jobId);
367
+ }
368
+ markCompleted(jobId, patch) {
369
+ return this.updateWhere(jobId, { ...patch, status: "completed", pid: null, kimi_pid: null }, "status = 'running'");
370
+ }
371
+ markFailed(jobId, patch) {
372
+ return this.updateWhere(jobId, { ...patch, status: "failed", pid: null, kimi_pid: null }, "status = 'running'");
373
+ }
374
+ markCancelled(jobId, patch) {
375
+ return this.updateWhere(jobId, { ...patch, status: "cancelled", pid: null, kimi_pid: null }, "status = 'running'");
376
+ }
377
+ static JOBS_COLUMNS = new Set([
378
+ "status", "summary", "phase", "budget_ms", "budget_source", "kimi_session_id_source", "pid", "kimi_pid",
379
+ "final_output_path", "stream_log_path", "kimi_session_id",
380
+ "error", "command_type", "prompt_digest", "repo_id",
381
+ "cwd", "model", "thinking", "thinking_effort", "hook_escapes", "background", "agent_profile",
382
+ "operation_kind", "intended_engine", "observed_engine", "kimi_version",
383
+ "system_version", "kimi_command", "kimi_prefix_args", "plan_certification",
384
+ "resumed_from_job_id", "safety_profile",
385
+ ]);
386
+ updateWhere(jobId, patch, whereClause) {
387
+ const updates = ["updated_at = @updated_at"];
388
+ const params = {
389
+ job_id: jobId,
390
+ updated_at: new Date().toISOString(),
391
+ };
392
+ for (const [key, value] of Object.entries(serializePatch(patch))) {
393
+ if (key === "job_id" || key === "created_at" || key === "updated_at" || value === undefined) {
394
+ continue;
395
+ }
396
+ if (!JobStore.JOBS_COLUMNS.has(key))
397
+ continue;
398
+ updates.push(`${key} = @${key}`);
399
+ params[key] = value;
400
+ }
401
+ const statement = `
402
+ UPDATE jobs
403
+ SET ${updates.join(", ")}
404
+ WHERE job_id = @job_id${whereClause ? ` AND ${whereClause}` : ""}
405
+ `;
406
+ this.db.run(statement, params);
407
+ return this.getJob(jobId);
408
+ }
409
+ }
410
+ function hydrateRow(row) {
411
+ return {
412
+ ...row,
413
+ thinking: row.thinking === null ? null : Boolean(row.thinking),
414
+ background: Boolean(row.background),
415
+ error: row.error ? JSON.parse(row.error) : null,
416
+ };
417
+ }
418
+ function serializeRecord(record) {
419
+ return {
420
+ ...record,
421
+ thinking_effort: record.thinking_effort ?? null,
422
+ phase: record.phase ?? null,
423
+ budget_ms: record.budget_ms ?? null,
424
+ budget_source: record.budget_source ?? null,
425
+ kimi_session_id_source: record.kimi_session_id_source ?? null,
426
+ operation_kind: record.operation_kind ?? null,
427
+ intended_engine: record.intended_engine ?? null,
428
+ observed_engine: record.observed_engine ?? null,
429
+ kimi_version: record.kimi_version ?? null,
430
+ system_version: record.system_version ?? null,
431
+ kimi_command: record.kimi_command ?? null,
432
+ kimi_prefix_args: record.kimi_prefix_args ?? null,
433
+ plan_certification: record.plan_certification ?? null,
434
+ resumed_from_job_id: record.resumed_from_job_id ?? null,
435
+ safety_profile: record.safety_profile ?? null,
436
+ thinking: record.thinking === null ? null : Number(record.thinking),
437
+ background: Number(record.background),
438
+ error: record.error ? JSON.stringify(record.error) : null,
439
+ };
440
+ }
441
+ function serializePatch(patch) {
442
+ const result = {};
443
+ for (const [key, value] of Object.entries(patch)) {
444
+ if (key === "thinking") {
445
+ result[key] = value === null ? null : Number(Boolean(value));
446
+ continue;
447
+ }
448
+ if (key === "background") {
449
+ result[key] = Number(Boolean(value));
450
+ continue;
451
+ }
452
+ if (key === "error") {
453
+ result[key] = value ? JSON.stringify(value) : null;
454
+ continue;
455
+ }
456
+ result[key] = value;
457
+ }
458
+ return result;
459
+ }
460
+ function createSqliteAdapter(filename) {
461
+ const require = createRequire(import.meta.url);
462
+ if (typeof Bun !== "undefined") {
463
+ const { Database } = require("bun:sqlite");
464
+ const db = new Database(filename);
465
+ return {
466
+ pragma(statement) {
467
+ db.exec(`PRAGMA ${statement}`);
468
+ },
469
+ exec(statement) {
470
+ db.exec(statement);
471
+ },
472
+ run(statement, params) {
473
+ const query = db.query(rewriteNamedParamsForBun(statement));
474
+ if (Array.isArray(params)) {
475
+ query.run(...params);
476
+ return;
477
+ }
478
+ query.run(rewriteNamedBindingsForBun((params ?? {})));
479
+ },
480
+ get(statement, ...params) {
481
+ const query = db.query(rewriteNamedParamsForBun(statement));
482
+ if (params.length === 1) {
483
+ const first = params[0];
484
+ return query.get(typeof first === "object" && first !== null && !Array.isArray(first)
485
+ ? rewriteNamedBindingsForBun(first)
486
+ : first);
487
+ }
488
+ return query.get(...params);
489
+ },
490
+ all(statement, ...params) {
491
+ const query = db.query(rewriteNamedParamsForBun(statement));
492
+ if (params.length === 1) {
493
+ const first = params[0];
494
+ return query.all(typeof first === "object" && first !== null && !Array.isArray(first)
495
+ ? rewriteNamedBindingsForBun(first)
496
+ : first);
497
+ }
498
+ return query.all(...params);
499
+ },
500
+ close() {
501
+ db.close();
502
+ },
503
+ };
504
+ }
505
+ // Node 22.5+ ships node:sqlite as a built-in. We use it via createRequire so the compiled
506
+ // dist/ has zero native dependencies — no `bun install` step required for drop-in plugin
507
+ // installs, no platform-specific compiled binaries to ship. See package.json engines.node.
508
+ let nodeSqlite;
509
+ try {
510
+ nodeSqlite = require("node:sqlite");
511
+ }
512
+ catch (error) {
513
+ throw new RuntimeError("JOB_STORE_UNSUPPORTED_RUNTIME", "kimi-plugin-cc requires Node >= 22.5 for its built-in node:sqlite module. Upgrade Node, or set KIMI_PLUGIN_CC_NODE_BIN to a qualifying binary.", "job-store", error instanceof Error ? { cause: error } : undefined);
514
+ }
515
+ const db = new nodeSqlite.DatabaseSync(filename);
516
+ return {
517
+ pragma(statement) {
518
+ db.exec(`PRAGMA ${statement}`);
519
+ },
520
+ exec(statement) {
521
+ db.exec(statement);
522
+ },
523
+ run(statement, params) {
524
+ // Cast through `any` so the prepared-statement methods are called as methods (preserving
525
+ // `this`). Pulling them off the instance via `const fn = stmt.run` strips the binding and
526
+ // node:sqlite throws "Illegal invocation".
527
+ const stmt = db.prepare(statement);
528
+ if (Array.isArray(params)) {
529
+ stmt.run(...params);
530
+ return;
531
+ }
532
+ stmt.run(normalizeNodeSqliteBindings((params ?? {})));
533
+ },
534
+ get(statement, ...params) {
535
+ const stmt = db.prepare(statement);
536
+ if (params.length === 1 && isPlainObject(params[0])) {
537
+ return stmt.get(normalizeNodeSqliteBindings(params[0]));
538
+ }
539
+ return stmt.get(...params);
540
+ },
541
+ all(statement, ...params) {
542
+ const stmt = db.prepare(statement);
543
+ if (params.length === 1 && isPlainObject(params[0])) {
544
+ return stmt.all(normalizeNodeSqliteBindings(params[0]));
545
+ }
546
+ return stmt.all(...params);
547
+ },
548
+ close() {
549
+ db.close();
550
+ },
551
+ };
552
+ }
553
+ // node:sqlite's prepared-statement binding API accepts objects whose keys omit the parameter
554
+ // prefix (so SQL `@job_id` binds from `{ job_id: ... }`). Normalize optional values to null,
555
+ // coerce booleans to 0/1, and fail fast on unsupported binding types before they reach the
556
+ // native layer as opaque SQLITE_MISMATCH errors.
557
+ function normalizeNodeSqliteBindings(bindings) {
558
+ const result = {};
559
+ for (const [key, value] of Object.entries(bindings)) {
560
+ if (value === undefined) {
561
+ result[key] = null;
562
+ continue;
563
+ }
564
+ if (value === null) {
565
+ result[key] = null;
566
+ continue;
567
+ }
568
+ if (typeof value === "boolean") {
569
+ result[key] = value ? 1 : 0;
570
+ continue;
571
+ }
572
+ if (typeof value === "string" ||
573
+ typeof value === "number" ||
574
+ typeof value === "bigint" ||
575
+ value instanceof Uint8Array) {
576
+ result[key] = value;
577
+ continue;
578
+ }
579
+ throw new Error(`node:sqlite binding for ${key} has unsupported type ${typeof value} (${value?.constructor?.name ?? "unknown"})`);
580
+ }
581
+ return result;
582
+ }
583
+ function isPlainObject(value) {
584
+ return typeof value === "object" && value !== null && !Array.isArray(value);
585
+ }
586
+ function rewriteNamedParamsForBun(statement) {
587
+ return statement.replaceAll(/@([A-Za-z0-9_]+)/g, (_, key) => `$${key}`);
588
+ }
589
+ function rewriteNamedBindingsForBun(bindings) {
590
+ return Object.fromEntries(Object.entries(bindings).map(([key, value]) => [`$${key}`, value]));
591
+ }
592
+ function tableHasColumn(db, columnName) {
593
+ const columns = db.all(`PRAGMA table_info(jobs)`);
594
+ return columns.some((column) => column.name === columnName);
595
+ }
596
+ function mcpTableHasColumn(db, columnName) {
597
+ return db.all("PRAGMA table_info(mcp_tasks)").some((column) => column.name === columnName);
598
+ }
599
+ function isSqliteConstraintError(error) {
600
+ const message = formatError(error).toLowerCase();
601
+ return message.includes("unique constraint") || message.includes("constraint failed");
602
+ }
603
+ function isSqliteBusyError(error) {
604
+ const message = formatError(error).toLowerCase();
605
+ return message.includes("database is locked") || message.includes("busy");
606
+ }
607
+ function translateSqliteError(error) {
608
+ if (isSqliteBusyError(error)) {
609
+ return new RuntimeError("JOB_STORE_BUSY", "The plugin job store is locked by another process. Wait a moment and retry, or check for stuck rescue workers with /kimi:status.", "job-store", error instanceof Error ? { cause: error } : undefined);
610
+ }
611
+ if (error instanceof RuntimeError) {
612
+ return error;
613
+ }
614
+ return new RuntimeError("JOB_STORE_ERROR", `Job store operation failed: ${formatError(error)}`, "job-store", error instanceof Error ? { cause: error } : undefined);
615
+ }
616
+ /**
617
+ * Run a function with a temporary `JobStore`, closing its handle on return or
618
+ * throw after construction succeeds. The constructor owns cleanup when opening
619
+ * or migrating the store fails.
620
+ */
621
+ export async function withJobStore(paths, fn) {
622
+ // Keep construction inside try: the constructor cleans up its own failures,
623
+ // while this finally closes every successfully constructed store.
624
+ let store;
625
+ try {
626
+ store = new JobStore(paths);
627
+ return await fn(store);
628
+ }
629
+ finally {
630
+ store?.close();
631
+ }
632
+ }
633
+ /** Synchronous companion to `withJobStore` for callers that don't await inside. */
634
+ export function withJobStoreSync(paths, fn) {
635
+ let store;
636
+ try {
637
+ store = new JobStore(paths);
638
+ return fn(store);
639
+ }
640
+ finally {
641
+ store?.close();
642
+ }
643
+ }
644
+ /** Parsed `hook_escapes` column for JSON envelopes: [] when NULL or unreadable. */
645
+ export function parseHookEscapes(raw) {
646
+ if (!raw)
647
+ return [];
648
+ try {
649
+ const parsed = JSON.parse(raw);
650
+ return Array.isArray(parsed) ? parsed : [];
651
+ }
652
+ catch {
653
+ return [];
654
+ }
655
+ }
656
+ /** Old non-null budgets have unknown provenance; never invent explicit intent. */
657
+ export function renderBudgetSource(job) {
658
+ return job.budget_source ?? (job.budget_ms === null ? "default" : "unknown");
659
+ }