@bitkyc08/opencodex 2.7.42 → 2.7.43

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 (143) hide show
  1. package/README.md +8 -1
  2. package/bin/ocx.mjs +13 -14
  3. package/gui/dist/assets/index-Czw-jpTU.css +1 -0
  4. package/gui/dist/assets/index-cmds12BG.js +67 -0
  5. package/gui/dist/index.html +2 -2
  6. package/package.json +1 -1
  7. package/src/AGENTS.md +28 -0
  8. package/src/adapters/anthropic.ts +15 -6
  9. package/src/adapters/cursor/native-exec-shell.ts +18 -6
  10. package/src/adapters/cursor/protobuf-events.ts +24 -2
  11. package/src/adapters/cursor/protobuf-request.ts +1 -2
  12. package/src/adapters/cursor/tool-definitions.ts +68 -29
  13. package/src/adapters/google-wire-compiler.ts +4 -0
  14. package/src/adapters/google.ts +128 -2
  15. package/src/adapters/identity.ts +12 -2
  16. package/src/adapters/kiro.ts +49 -6
  17. package/src/adapters/mimo-free.ts +2 -0
  18. package/src/adapters/openai-responses.ts +246 -59
  19. package/src/claude/agents-inject.ts +5 -0
  20. package/src/claude/inbound.ts +26 -9
  21. package/src/cli/account-auth.ts +1 -1
  22. package/src/cli/agent-driven.ts +37 -0
  23. package/src/cli/claude.ts +3 -3
  24. package/src/cli/doctor.ts +23 -18
  25. package/src/cli/help.ts +42 -6
  26. package/src/cli/index.ts +88 -19
  27. package/src/cli/interactive-confirm.ts +129 -0
  28. package/src/cli/opencode.ts +701 -0
  29. package/src/cli/provider-runtime.ts +3 -0
  30. package/src/cli/provider.ts +31 -10
  31. package/src/cli/star-prompt.ts +55 -16
  32. package/src/cli/status.ts +47 -13
  33. package/src/codex/account-id.ts +34 -0
  34. package/src/codex/account-lifecycle.ts +4 -1
  35. package/src/codex/account-namespace-match.ts +63 -0
  36. package/src/codex/account-namespaces.ts +149 -0
  37. package/src/codex/account-pause.ts +20 -0
  38. package/src/codex/account-usability.ts +6 -1
  39. package/src/codex/app-server-processes.ts +511 -0
  40. package/src/codex/auth-api.ts +293 -34
  41. package/src/codex/auth-collision.ts +2 -1
  42. package/src/codex/auth-context.ts +60 -17
  43. package/src/codex/catalog/parsing.ts +16 -1
  44. package/src/codex/catalog/provider-fetch.ts +48 -12
  45. package/src/codex/catalog/sync.ts +18 -3
  46. package/src/codex/catalog.ts +1 -1
  47. package/src/codex/history-provider.ts +145 -1
  48. package/src/codex/inject.ts +114 -14
  49. package/src/codex/main-account.ts +2 -8
  50. package/src/codex/pool-rotation.ts +186 -0
  51. package/src/codex/quota.ts +15 -0
  52. package/src/codex/routing.ts +695 -106
  53. package/src/codex/shim.ts +4 -1
  54. package/src/codex/subagent-defaults.ts +550 -0
  55. package/src/codex/subagent-model-fallback.ts +2 -0
  56. package/src/codex/sync.ts +3 -0
  57. package/src/config.ts +566 -25
  58. package/src/images/artifacts.ts +516 -0
  59. package/src/images/fulfill-video.ts +163 -0
  60. package/src/images/fulfill.ts +111 -0
  61. package/src/images/index.ts +4 -0
  62. package/src/images/loop.ts +789 -0
  63. package/src/images/plan.ts +133 -0
  64. package/src/images/synthetic-tool.ts +133 -0
  65. package/src/images/types.ts +41 -0
  66. package/src/images/xai-client.ts +141 -0
  67. package/src/images/xai-video-client.ts +163 -0
  68. package/src/lib/admin-secrets.ts +25 -0
  69. package/src/lib/config-ownership.ts +327 -0
  70. package/src/lib/crash-guard.ts +2 -0
  71. package/src/lib/destination-policy.ts +122 -4
  72. package/src/lib/pinned-http.ts +151 -0
  73. package/src/lib/process-control.ts +2 -2
  74. package/src/lib/provider-outbound.ts +164 -0
  75. package/src/lib/provider-url.ts +14 -0
  76. package/src/lib/proxy-env.ts +18 -0
  77. package/src/lib/windows-elevation.ts +81 -3
  78. package/src/lib/windows-secret-acl.ts +189 -12
  79. package/src/lib/winsw.ts +2 -0
  80. package/src/oauth/anthropic-routing.ts +570 -0
  81. package/src/oauth/health.ts +6 -0
  82. package/src/oauth/index.ts +283 -72
  83. package/src/oauth/key-providers.ts +17 -6
  84. package/src/oauth/kimi.ts +2 -0
  85. package/src/oauth/kiro-credentials.ts +322 -10
  86. package/src/oauth/kiro.ts +423 -43
  87. package/src/oauth/login-cli.ts +32 -5
  88. package/src/oauth/store.ts +54 -4
  89. package/src/oauth/types.ts +11 -0
  90. package/src/providers/alibaba-region-migration.ts +16 -3
  91. package/src/providers/antigravity-models.ts +3 -0
  92. package/src/providers/api-keys.ts +13 -6
  93. package/src/providers/derive.ts +6 -0
  94. package/src/providers/key-failover.ts +24 -4
  95. package/src/providers/quota.ts +233 -29
  96. package/src/providers/registry.ts +11 -3
  97. package/src/responses/parser.ts +11 -0
  98. package/src/responses/state.ts +22 -8
  99. package/src/responses/tool-groups.ts +19 -0
  100. package/src/router.ts +14 -4
  101. package/src/server/auth-cors.ts +110 -22
  102. package/src/server/claude-messages.ts +8 -1
  103. package/src/server/gui-static.ts +30 -6
  104. package/src/server/images.ts +303 -9
  105. package/src/server/index.ts +77 -9
  106. package/src/server/lifecycle.ts +25 -1
  107. package/src/server/management/agent-settings-routes.ts +28 -4
  108. package/src/server/management/combo-routes.ts +7 -0
  109. package/src/server/management/config-routes.ts +5 -2
  110. package/src/server/management/logs-usage-routes.ts +167 -3
  111. package/src/server/management/oauth-account-routes.ts +163 -17
  112. package/src/server/management/provider-routes.ts +35 -7
  113. package/src/server/management/system-restart.ts +172 -0
  114. package/src/server/management/system-routes.ts +33 -10
  115. package/src/server/management-api.ts +2 -2
  116. package/src/server/management-auth.ts +216 -0
  117. package/src/server/proxy-liveness.ts +14 -3
  118. package/src/server/responses/compact.ts +21 -13
  119. package/src/server/responses/core.ts +583 -152
  120. package/src/server/responses-image-gen-repair.ts +118 -0
  121. package/src/server/responses-item-id-repair.ts +10 -85
  122. package/src/server/sse-payload-rewrite.ts +116 -0
  123. package/src/server/system-env.ts +28 -10
  124. package/src/service.ts +49 -2
  125. package/src/storage/cleanup-job.ts +57 -0
  126. package/src/storage/cleanup.ts +1504 -28
  127. package/src/storage/policy-job.ts +366 -0
  128. package/src/storage/policy-scheduler.ts +40 -0
  129. package/src/storage/policy-worker.ts +53 -0
  130. package/src/storage/policy.ts +522 -0
  131. package/src/storage/restore-job.ts +242 -0
  132. package/src/storage/restore-worker.ts +52 -0
  133. package/src/storage/storage-mutation-coordinator.ts +109 -0
  134. package/src/tray/windows.ts +2 -0
  135. package/src/types.ts +96 -1
  136. package/src/update/index.ts +36 -18
  137. package/src/update/job.ts +103 -12
  138. package/src/update/npm-invocation.d.mts +23 -0
  139. package/src/update/npm-invocation.mjs +94 -0
  140. package/src/usage/debug.ts +2 -0
  141. package/src/usage/log.ts +4 -0
  142. package/gui/dist/assets/index-Bl_VBGoI.js +0 -65
  143. package/gui/dist/assets/index-DfVGuN88.css +0 -1
@@ -1,9 +1,16 @@
1
- import { existsSync, readFileSync } from "node:fs";
1
+ import { randomUUID } from "node:crypto";
2
+ import { chmodSync, closeSync, existsSync, fsyncSync, linkSync, openSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
2
3
  import { homedir } from "node:os";
3
4
  import { isAbsolute, join } from "node:path";
4
5
  import { Database } from "bun:sqlite";
5
6
 
6
7
  const DEFAULT_EXPIRES_MS = 3600_000;
8
+ const KIRO_CLI_RECOVERY_SUFFIX = ".opencodex-recovery";
9
+ const KIRO_CLI_RECOVERY_HEADER_V1 = Buffer.from("opencodex-kiro-session-v1\n", "utf8");
10
+ const KIRO_CLI_RECOVERY_HEADER = Buffer.from("opencodex-kiro-session-v2\n", "utf8");
11
+ const KIRO_CLI_RECOVERY_PROCESS_INSTANCE = randomUUID();
12
+ const KIRO_CLI_RECOVERY_PROCESS_INSTANCE_PATTERN = /^[A-Za-z0-9._-]{1,128}$/;
13
+ const SQLITE_DATABASE_HEADER = Buffer.from("SQLite format 3\0", "binary");
7
14
  const KIRO_REGION_PATTERN = /^[a-z]{2}(?:-[a-z]+)+-\d$/;
8
15
  const CLIENT_ID_HASH_PATTERN = /^[A-Za-z0-9_-]{1,128}$/;
9
16
  const TOKEN_KEYS = [
@@ -49,6 +56,13 @@ export interface ImportedKiroCredential {
49
56
  clientSecret?: string;
50
57
  }
51
58
 
59
+ /** Opaque backup retained on disk while a forced Kiro CLI login is pending persistence. */
60
+ export interface KiroCliSessionSnapshot {
61
+ readonly path: string;
62
+ readonly database: Buffer;
63
+ readonly recoveryPath: string;
64
+ }
65
+
52
66
  type JsonObject = Record<string, unknown>;
53
67
 
54
68
  function userHome(): string {
@@ -108,18 +122,30 @@ function jsonCredentialPaths(): string[] {
108
122
  .map(expandPath);
109
123
  }
110
124
 
111
- function sqliteEntries(): Array<{ location: KiroImportDiagnostic["location"]; path: string }> {
125
+ function nativeKiroCliSessionEntries(): Array<{ location: "kiro-cli-data" | "kiro-cli-linux-data"; path: string }> {
112
126
  const home = userHome();
113
- const configured = process.env.KIROCLI_DB_PATH?.trim() || process.env.KIRO_CLI_DB_FILE?.trim();
114
- if (configured) return [{ location: "kiro-cli-db-env", path: expandPath(configured) }];
127
+ // Only the stores that `kiro-cli logout` / `kiro-cli login` themselves mutate. Import fallbacks
128
+ // (Amazon Q / SSO cache) and KIROCLI_DB_PATH selectors must not be snapshotted for rollback.
115
129
  return [
116
130
  { location: "kiro-cli-data", path: join(home, "Library", "Application Support", "kiro-cli", "data.sqlite3") },
117
131
  { location: "kiro-cli-linux-data", path: join(home, ".local", "share", "kiro-cli", "data.sqlite3") },
118
- { location: "amazon-q-data", path: join(home, ".local", "share", "amazon-q", "data.sqlite3") },
119
- { location: "kiro-sso-cache", path: join(home, ".kiro", "sso", "cache.db") },
120
132
  ];
121
133
  }
122
134
 
135
+ function sqliteEntries(): Array<{ location: KiroImportDiagnostic["location"]; path: string }> {
136
+ const configured = process.env.KIROCLI_DB_PATH?.trim() || process.env.KIRO_CLI_DB_FILE?.trim();
137
+ if (configured) return [{ location: "kiro-cli-db-env", path: expandPath(configured) }];
138
+ return [
139
+ ...nativeKiroCliSessionEntries(),
140
+ { location: "amazon-q-data", path: join(userHome(), ".local", "share", "amazon-q", "data.sqlite3") },
141
+ { location: "kiro-sso-cache", path: join(userHome(), ".kiro", "sso", "cache.db") },
142
+ ];
143
+ }
144
+
145
+ function kiroCliImportSelectorConfigured(): boolean {
146
+ return Boolean(process.env.KIROCLI_DB_PATH?.trim() || process.env.KIRO_CLI_DB_FILE?.trim());
147
+ }
148
+
123
149
  function selectTokenRow(db: Database): { value: string } | null | "ambiguous" | "selected_missing" {
124
150
  const rows = db.query("SELECT key, value FROM auth_kv WHERE key LIKE ? ORDER BY key ASC").all("%:token") as Array<{ key: string; value: string }>;
125
151
  const selectedKey = process.env.KIROCLI_TOKEN_KEY?.trim();
@@ -210,7 +236,16 @@ function readStateProfile(db: Database): { profileArn?: string; apiRegion?: stri
210
236
  }
211
237
  }
212
238
 
213
- function readSqliteCredentials(diagnostics: KiroImportDiagnostic[]): ImportedKiroCredential | undefined {
239
+ interface LocatedKiroCliCredential {
240
+ credential: ImportedKiroCredential;
241
+ path: string;
242
+ database?: Buffer;
243
+ }
244
+
245
+ function readSqliteCredentials(
246
+ diagnostics: KiroImportDiagnostic[],
247
+ includeSnapshot = false,
248
+ ): LocatedKiroCliCredential | undefined {
214
249
  for (const { location, path } of sqliteEntries()) {
215
250
  if (!existsSync(path)) {
216
251
  diagnostics.push({ location, status: "missing" });
@@ -261,7 +296,13 @@ function readSqliteCredentials(diagnostics: KiroImportDiagnostic[]): ImportedKir
261
296
  const merged = { ...registrationData, ...tokenData, ...profile };
262
297
  const credential = credentialFromJson(merged, "sqlite");
263
298
  diagnostics.push({ location, status: credential ? "token_found" : "token_missing" });
264
- if (credential) return credential;
299
+ if (credential) {
300
+ return {
301
+ credential,
302
+ path,
303
+ ...(includeSnapshot ? { database: db.serialize() } : {}),
304
+ };
305
+ }
265
306
  } catch (error) {
266
307
  if (error instanceof Error && error.message.includes("KIROCLI_TOKEN_KEY")) throw error;
267
308
  diagnostics.push({ location, status: "schema_mismatch" });
@@ -277,12 +318,12 @@ export function inspectKiroCredentialSources(): { credential: ImportedKiroCreden
277
318
  const json = readJsonCredentials(diagnostics);
278
319
  if (json) return { credential: json, diagnostics };
279
320
  const sqlite = readSqliteCredentials(diagnostics);
280
- return { credential: sqlite ?? null, diagnostics };
321
+ return { credential: sqlite?.credential ?? null, diagnostics };
281
322
  }
282
323
 
283
324
  export function inspectKiroCliSqliteSources(): { credential: ImportedKiroCredential | null; diagnostics: KiroImportDiagnostic[] } {
284
325
  const diagnostics: KiroImportDiagnostic[] = [];
285
- return { credential: readSqliteCredentials(diagnostics) ?? null, diagnostics };
326
+ return { credential: readSqliteCredentials(diagnostics)?.credential ?? null, diagnostics };
286
327
  }
287
328
 
288
329
  export function readImportedKiroCredential(): ImportedKiroCredential | null {
@@ -292,3 +333,274 @@ export function readImportedKiroCredential(): ImportedKiroCredential | null {
292
333
  export function readKiroCliSqliteCredential(): ImportedKiroCredential | null {
293
334
  return inspectKiroCliSqliteSources().credential;
294
335
  }
336
+
337
+ /** Capture the complete active CLI database so a failed account switch can restore it exactly. */
338
+ export function snapshotKiroCliSession(): KiroCliSessionSnapshot | null {
339
+ return inspectKiroCliSessionSnapshot().snapshot;
340
+ }
341
+
342
+ /**
343
+ * A store that exists but cannot be captured must never be logged out of: the recovery contract
344
+ * promises an exact restore, and without a snapshot a later failure would destroy the session for
345
+ * good. `missing`/`token_missing` are the only statuses that mean "there is nothing to lose".
346
+ */
347
+ const KIRO_UNSNAPSHOTTABLE_SESSION_STATUSES: ReadonlySet<KiroDiagnosticStatus> = new Set([
348
+ "unreadable",
349
+ "schema_mismatch",
350
+ "invalid_json",
351
+ "token_ambiguous",
352
+ "token_key_missing",
353
+ "token_found",
354
+ ]);
355
+
356
+ /**
357
+ * Capture the active CLI session and report why capture failed. `blocked` is true when a session
358
+ * store is present but could not be snapshotted (unreadable / schema-mismatched / ambiguous), so
359
+ * callers can abort before mutating it.
360
+ *
361
+ * Forced-login rollback must target the exact database `kiro-cli` mutates. Custom import selectors
362
+ * and lower-priority Amazon Q / SSO caches are never used here: snapshotting the wrong file would
363
+ * leave the real CLI session switched or lost after failure.
364
+ */
365
+ export function inspectKiroCliSessionSnapshot(): {
366
+ snapshot: KiroCliSessionSnapshot | null;
367
+ diagnostics: KiroImportDiagnostic[];
368
+ blocked: boolean;
369
+ } {
370
+ const diagnostics: KiroImportDiagnostic[] = [];
371
+ if (kiroCliImportSelectorConfigured()) {
372
+ diagnostics.push({ location: "kiro-cli-db-env", status: "token_ambiguous" });
373
+ return { snapshot: null, diagnostics, blocked: true };
374
+ }
375
+
376
+ for (const { location, path } of nativeKiroCliSessionEntries()) {
377
+ if (!existsSync(path)) {
378
+ diagnostics.push({ location, status: "missing" });
379
+ continue;
380
+ }
381
+
382
+ let db: Database | undefined;
383
+ try {
384
+ db = new Database(path, { readonly: true });
385
+ try { db.exec("PRAGMA busy_timeout = 5000"); } catch { /* read-only best effort */ }
386
+ } catch {
387
+ diagnostics.push({ location, status: "unreadable" });
388
+ return { snapshot: null, diagnostics, blocked: true };
389
+ }
390
+
391
+ try {
392
+ const row = selectTokenRow(db);
393
+ if (row === "ambiguous") {
394
+ diagnostics.push({ location, status: "token_ambiguous" });
395
+ return { snapshot: null, diagnostics, blocked: true };
396
+ }
397
+ if (row === "selected_missing") {
398
+ diagnostics.push({ location, status: "token_key_missing" });
399
+ return { snapshot: null, diagnostics, blocked: true };
400
+ }
401
+ if (!row) {
402
+ // An existing CLI database with no recognized token is still what logout mutates. Falling
403
+ // through to Amazon Q / another platform path would snapshot the wrong store.
404
+ diagnostics.push({ location, status: "token_missing" });
405
+ return { snapshot: null, diagnostics, blocked: true };
406
+ }
407
+ try {
408
+ JSON.parse(row.value);
409
+ } catch {
410
+ diagnostics.push({ location, status: "invalid_json" });
411
+ return { snapshot: null, diagnostics, blocked: true };
412
+ }
413
+ const database = db.serialize();
414
+ diagnostics.push({ location, status: "token_found" });
415
+ return {
416
+ snapshot: {
417
+ path,
418
+ database,
419
+ recoveryPath: `${path}${KIRO_CLI_RECOVERY_SUFFIX}`,
420
+ },
421
+ diagnostics,
422
+ blocked: false,
423
+ };
424
+ } catch (error) {
425
+ if (error instanceof Error && error.message.includes("KIROCLI_TOKEN_KEY")) {
426
+ diagnostics.push({ location, status: "token_key_missing" });
427
+ return { snapshot: null, diagnostics, blocked: true };
428
+ }
429
+ diagnostics.push({ location, status: "schema_mismatch" });
430
+ return { snapshot: null, diagnostics, blocked: true };
431
+ } finally {
432
+ db.close();
433
+ }
434
+ }
435
+
436
+ return {
437
+ snapshot: null,
438
+ diagnostics,
439
+ blocked: diagnostics.some(entry => KIRO_UNSNAPSHOTTABLE_SESSION_STATUSES.has(entry.status)),
440
+ };
441
+ }
442
+
443
+ /** Persist a complete, private recovery image before kiro-cli is allowed to mutate its store. */
444
+ export function persistKiroCliSessionRecovery(snapshot: KiroCliSessionSnapshot): void {
445
+ if (existsSync(snapshot.recoveryPath)) {
446
+ throw new Error("Kiro CLI session recovery is already pending.");
447
+ }
448
+ const nonce = `${process.pid}.${Date.now()}.${Math.random().toString(16).slice(2)}`;
449
+ const staged = `${snapshot.recoveryPath}.${nonce}.tmp`;
450
+ try {
451
+ writeFileSync(staged, Buffer.concat([
452
+ KIRO_CLI_RECOVERY_HEADER,
453
+ Buffer.from(`${process.pid}\n`, "utf8"),
454
+ Buffer.from(`${KIRO_CLI_RECOVERY_PROCESS_INSTANCE}\n`, "utf8"),
455
+ snapshot.database,
456
+ ]), { flag: "wx", mode: 0o600 });
457
+ try { chmodSync(staged, 0o600); } catch { /* platform may ignore chmod */ }
458
+ const fd = openSync(staged, "r+");
459
+ try { fsyncSync(fd); } finally { closeSync(fd); }
460
+ // A hard link publishes the fsynced inode atomically and, unlike rename, can never replace
461
+ // another process's live recovery transaction if both raced past the initial existence check.
462
+ linkSync(staged, snapshot.recoveryPath);
463
+ } finally {
464
+ rmSync(staged, { force: true });
465
+ }
466
+ }
467
+
468
+ /** Remove recovery data only after the corresponding login transaction has settled. */
469
+ export function discardKiroCliSessionRecovery(snapshot: KiroCliSessionSnapshot): void {
470
+ rmSync(snapshot.recoveryPath, { force: true });
471
+ }
472
+
473
+ interface ParsedKiroCliSessionRecovery {
474
+ ownerPid: number;
475
+ ownerProcessInstance?: string;
476
+ database: Buffer;
477
+ }
478
+
479
+ function parseKiroCliSessionRecovery(payload: Buffer): ParsedKiroCliSessionRecovery | null {
480
+ const isV2 = payload.subarray(0, KIRO_CLI_RECOVERY_HEADER.length).equals(KIRO_CLI_RECOVERY_HEADER);
481
+ const isV1 = payload.subarray(0, KIRO_CLI_RECOVERY_HEADER_V1.length).equals(KIRO_CLI_RECOVERY_HEADER_V1);
482
+ if (!isV2 && !isV1) return null;
483
+ const headerLength = isV2 ? KIRO_CLI_RECOVERY_HEADER.length : KIRO_CLI_RECOVERY_HEADER_V1.length;
484
+ const ownerEnd = payload.indexOf(0x0a, headerLength);
485
+ if (ownerEnd <= headerLength) return null;
486
+ const ownerPid = Number(payload.subarray(headerLength, ownerEnd).toString("utf8"));
487
+ let databaseStart = ownerEnd + 1;
488
+ let ownerProcessInstance: string | undefined;
489
+ if (isV2) {
490
+ const instanceEnd = payload.indexOf(0x0a, databaseStart);
491
+ if (instanceEnd <= databaseStart) return null;
492
+ ownerProcessInstance = payload.subarray(databaseStart, instanceEnd).toString("utf8");
493
+ if (!KIRO_CLI_RECOVERY_PROCESS_INSTANCE_PATTERN.test(ownerProcessInstance)) return null;
494
+ databaseStart = instanceEnd + 1;
495
+ }
496
+ const database = payload.subarray(databaseStart);
497
+ if (
498
+ !Number.isSafeInteger(ownerPid) || ownerPid <= 0 ||
499
+ !database.subarray(0, SQLITE_DATABASE_HEADER.length).equals(SQLITE_DATABASE_HEADER)
500
+ ) {
501
+ return null;
502
+ }
503
+ return { ownerPid, ...(ownerProcessInstance ? { ownerProcessInstance } : {}), database };
504
+ }
505
+
506
+ function isKiroRecoveryOwnerAlive(ownerPid: number, ownerProcessInstance?: string): boolean {
507
+ // A restarted supervisor may reuse the exact same PID (commonly PID 1). Only this process
508
+ // instance's nonce proves that a same-PID recovery transaction is still live.
509
+ if (ownerPid === process.pid) return ownerProcessInstance === KIRO_CLI_RECOVERY_PROCESS_INSTANCE;
510
+ try {
511
+ process.kill(ownerPid, 0);
512
+ return true;
513
+ } catch (error) {
514
+ if (error && typeof error === "object" && "code" in error) return error.code !== "ESRCH";
515
+ return true;
516
+ }
517
+ }
518
+
519
+ /** Restore a previously captured CLI database after every kiro-cli child process has exited. */
520
+ export function restoreKiroCliSession(snapshot: KiroCliSessionSnapshot): void {
521
+ const nonce = `${process.pid}.${Date.now()}.${Math.random().toString(16).slice(2)}`;
522
+ const staged = `${snapshot.path}.ocx-restore.${nonce}.tmp`;
523
+ const displacedBase = `${snapshot.path}.ocx-restore.${nonce}.new`;
524
+ const displaced: Array<{ current: string; backup: string }> = [];
525
+ let published = false;
526
+ try {
527
+ writeFileSync(staged, snapshot.database, { mode: 0o600 });
528
+ try { chmodSync(staged, 0o600); } catch { /* platform may ignore chmod */ }
529
+ for (const suffix of ["", "-wal", "-shm", "-journal"]) {
530
+ const current = `${snapshot.path}${suffix}`;
531
+ if (!existsSync(current)) continue;
532
+ const backup = `${displacedBase}${suffix || ".db"}`;
533
+ renameSync(current, backup);
534
+ displaced.push({ current, backup });
535
+ }
536
+ renameSync(staged, snapshot.path);
537
+ published = true;
538
+ } catch (error) {
539
+ const recoveryErrors: unknown[] = [];
540
+ for (const { current, backup } of [...displaced].reverse()) {
541
+ if (!existsSync(backup) || existsSync(current)) continue;
542
+ try { renameSync(backup, current); } catch (recoveryError) { recoveryErrors.push(recoveryError); }
543
+ }
544
+ if (recoveryErrors.length > 0) throw new AggregateError([error, ...recoveryErrors], "Kiro CLI session restore failed during rollback.");
545
+ throw error;
546
+ } finally {
547
+ rmSync(staged, { force: true });
548
+ if (published) {
549
+ for (const { backup } of displaced) {
550
+ try { rmSync(backup, { force: true }); } catch { /* restored database is already published */ }
551
+ }
552
+ }
553
+ }
554
+ }
555
+
556
+ /** Restore a transaction abandoned by a crashed process before starting another forced login. */
557
+ export function restoreStaleKiroCliSessionRecovery(): boolean {
558
+ for (const { path } of nativeKiroCliSessionEntries()) {
559
+ const recoveryPath = `${path}${KIRO_CLI_RECOVERY_SUFFIX}`;
560
+ if (!existsSync(recoveryPath)) continue;
561
+ const payload = readFileSync(recoveryPath);
562
+ const recovery = parseKiroCliSessionRecovery(payload);
563
+ if (!recovery) {
564
+ throw new Error(
565
+ `Kiro CLI session recovery data is invalid: ${recoveryPath}. Remove this file to continue.`,
566
+ );
567
+ }
568
+ if (isKiroRecoveryOwnerAlive(recovery.ownerPid, recovery.ownerProcessInstance)) {
569
+ throw new Error(
570
+ `Another Kiro CLI login transaction is still in progress (pid ${recovery.ownerPid}, ${recoveryPath}).`,
571
+ );
572
+ }
573
+ // Atomically claim the marker before restoring so a concurrent process cannot restore the
574
+ // same stale image (and later delete a newer recovery file published by the winner).
575
+ const claimedPath = `${recoveryPath}.claimed.${process.pid}.${Date.now()}`;
576
+ try {
577
+ renameSync(recoveryPath, claimedPath);
578
+ } catch {
579
+ continue;
580
+ }
581
+ try {
582
+ const claimed = readFileSync(claimedPath);
583
+ if (!claimed.equals(payload)) {
584
+ // Put the unexpected claim back so an operator / later process can inspect it.
585
+ try { renameSync(claimedPath, recoveryPath); } catch { /* keep claimed path for manual recovery */ }
586
+ throw new Error(
587
+ `Kiro CLI session recovery data changed while claiming ${recoveryPath}. Remove leftover claim files under the kiro-cli data directory and retry.`,
588
+ );
589
+ }
590
+ restoreKiroCliSession({
591
+ path,
592
+ database: recovery.database,
593
+ recoveryPath: claimedPath,
594
+ });
595
+ rmSync(claimedPath, { force: true });
596
+ return true;
597
+ } catch (error) {
598
+ // Preserve the only copy of the prior database when restore fails (disk full, permissions).
599
+ if (existsSync(claimedPath) && !existsSync(recoveryPath)) {
600
+ try { renameSync(claimedPath, recoveryPath); } catch { /* claimed file remains for manual recovery */ }
601
+ }
602
+ throw error;
603
+ }
604
+ }
605
+ return false;
606
+ }