@gajae-code/coding-agent 0.15.6 → 0.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (167) hide show
  1. package/CHANGELOG.md +59 -0
  2. package/dist/types/cli/args.d.ts +2 -0
  3. package/dist/types/cli/root-flags.d.ts +14 -0
  4. package/dist/types/cli/update-cli.d.ts +13 -0
  5. package/dist/types/cli.d.ts +7 -0
  6. package/dist/types/commands/launch.d.ts +10 -0
  7. package/dist/types/config/autorouting-tier-map.d.ts +24 -0
  8. package/dist/types/config/file-lock.d.ts +5 -0
  9. package/dist/types/config/model-registry.d.ts +6 -0
  10. package/dist/types/config/settings-schema.d.ts +10 -1
  11. package/dist/types/config/settings.d.ts +2 -0
  12. package/dist/types/coordinator-mcp/projection-scan.d.ts +8 -0
  13. package/dist/types/crash/record-loader.d.ts +15 -0
  14. package/dist/types/gjc-runtime/launch-worktree-reservation.d.ts +21 -0
  15. package/dist/types/gjc-runtime/managed-owner-supervisor.d.ts +2 -0
  16. package/dist/types/gjc-runtime/session-state-sidecar.d.ts +66 -1
  17. package/dist/types/gjc-runtime/tmux-owner-isolation.d.ts +1 -0
  18. package/dist/types/gjc-runtime/tmux-sessions.d.ts +27 -0
  19. package/dist/types/master-mode/context.d.ts +21 -0
  20. package/dist/types/master-mode/first-request.d.ts +34 -0
  21. package/dist/types/master-mode/snapshot.d.ts +32 -0
  22. package/dist/types/modes/components/frictionless-onboarding-selector.d.ts +10 -0
  23. package/dist/types/modes/components/oauth-selector.d.ts +1 -0
  24. package/dist/types/modes/components/status-line/model-name.d.ts +18 -0
  25. package/dist/types/modes/components/status-line/priority-row.d.ts +34 -0
  26. package/dist/types/modes/components/status-line/segments.d.ts +10 -0
  27. package/dist/types/modes/print-mode.d.ts +17 -0
  28. package/dist/types/modes/ui-language.d.ts +7 -1
  29. package/dist/types/sdk/broker/broker.d.ts +77 -1
  30. package/dist/types/sdk/broker/endpoint-authority.d.ts +24 -0
  31. package/dist/types/sdk/broker/index.d.ts +3 -0
  32. package/dist/types/sdk/broker/lifecycle.d.ts +26 -0
  33. package/dist/types/sdk/broker/master-capability.d.ts +10 -0
  34. package/dist/types/sdk/broker/session-index.d.ts +40 -11
  35. package/dist/types/sdk/broker/session-scope.d.ts +72 -0
  36. package/dist/types/sdk/broker/spawn-authority.d.ts +201 -0
  37. package/dist/types/sdk/broker/spawn-substrate.d.ts +30 -0
  38. package/dist/types/sdk/broker/state-version.d.ts +4 -1
  39. package/dist/types/sdk/broker/worktree-occupancy.d.ts +28 -0
  40. package/dist/types/sdk/bus/chat-daemon-control.d.ts +3 -0
  41. package/dist/types/sdk/bus/index.d.ts +6 -0
  42. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +1 -1
  43. package/dist/types/sdk/cli/index.d.ts +1 -0
  44. package/dist/types/sdk/cli/master-cli.d.ts +63 -0
  45. package/dist/types/sdk/cli/rows.d.ts +4 -3
  46. package/dist/types/sdk/cli/session-cli.d.ts +18 -2
  47. package/dist/types/sdk/client/client.d.ts +3 -2
  48. package/dist/types/sdk/host/control/dispatch.d.ts +10 -0
  49. package/dist/types/sdk/host/control/index.d.ts +1 -1
  50. package/dist/types/sdk/host/control/operations.d.ts +2 -0
  51. package/dist/types/sdk/host/control/runtime-gate.d.ts +21 -7
  52. package/dist/types/sdk/host/host.d.ts +10 -0
  53. package/dist/types/sdk/host/session-runtime.d.ts +52 -0
  54. package/dist/types/sdk/lifecycle/broker-client.d.ts +6 -0
  55. package/dist/types/sdk/lifecycle/service.d.ts +15 -3
  56. package/dist/types/sdk/lifecycle-session.d.ts +2 -0
  57. package/dist/types/sdk/router/session-router.d.ts +2 -0
  58. package/dist/types/sdk/session.d.ts +16 -0
  59. package/dist/types/sdk/startup-capability.d.ts +2 -1
  60. package/dist/types/session/agent-session.d.ts +5 -0
  61. package/dist/types/session/credential-store-errors.d.ts +1 -0
  62. package/dist/types/session/session-manager.d.ts +65 -0
  63. package/dist/types/session/yield-queue.d.ts +5 -4
  64. package/dist/types/setup/frictionless-onboarding.d.ts +1 -0
  65. package/dist/types/tools/bash.d.ts +4 -0
  66. package/dist/types/tools/gh.d.ts +8 -0
  67. package/dist/types/tools/index.d.ts +4 -0
  68. package/package.json +7 -7
  69. package/scripts/generate-sdk-operation-inventory.ts +6 -0
  70. package/scripts/verify-gjc-sdk-canonicalization.ts +35 -1
  71. package/src/cli/accounts-cli.ts +9 -5
  72. package/src/cli/args.ts +17 -0
  73. package/src/cli/fast-help.ts +1 -0
  74. package/src/cli/root-flags.ts +5 -0
  75. package/src/cli/update-cli.ts +77 -11
  76. package/src/commands/launch.ts +86 -18
  77. package/src/commands/sdk.ts +113 -21
  78. package/src/config/autorouting-tier-map.ts +12 -0
  79. package/src/config/file-lock.ts +158 -83
  80. package/src/config/model-preset-registry.ts +1 -5
  81. package/src/config/model-profiles.ts +5 -5
  82. package/src/config/model-registry.ts +23 -0
  83. package/src/config/model-resolver.ts +5 -0
  84. package/src/config/settings-schema.ts +8 -1
  85. package/src/config/settings.ts +5 -0
  86. package/src/coordinator-mcp/projection-scan.ts +21 -0
  87. package/src/coordinator-mcp/question-state.ts +9 -11
  88. package/src/coordinator-mcp/server.ts +94 -20
  89. package/src/crash/index-store.ts +2 -0
  90. package/src/crash/record-loader.ts +10 -0
  91. package/src/gjc-runtime/launch-worktree-reservation.ts +242 -0
  92. package/src/gjc-runtime/managed-owner-supervisor.ts +63 -51
  93. package/src/gjc-runtime/session-state-lock.ts +79 -6
  94. package/src/gjc-runtime/session-state-sidecar.ts +929 -23
  95. package/src/gjc-runtime/tmux-owner-isolation-cli.ts +4 -7
  96. package/src/gjc-runtime/tmux-owner-isolation.ts +2 -2
  97. package/src/gjc-runtime/tmux-sessions.ts +252 -13
  98. package/src/hooks/skill-state.ts +6 -0
  99. package/src/internal-urls/docs-index.generated.ts +7 -7
  100. package/src/internal-urls/issue-pr-protocol.ts +4 -2
  101. package/src/main.ts +57 -2
  102. package/src/master-mode/context.ts +50 -0
  103. package/src/master-mode/first-request.ts +65 -0
  104. package/src/master-mode/snapshot.ts +205 -0
  105. package/src/modes/acp/acp-agent.ts +605 -156
  106. package/src/modes/components/footer.ts +4 -2
  107. package/src/modes/components/frictionless-onboarding-selector.ts +48 -2
  108. package/src/modes/components/oauth-selector.ts +6 -1
  109. package/src/modes/components/settings-selector.ts +1 -0
  110. package/src/modes/components/status-line/model-name.ts +53 -0
  111. package/src/modes/components/status-line/priority-row.ts +107 -0
  112. package/src/modes/components/status-line/segments.ts +23 -9
  113. package/src/modes/components/tool-status-header.ts +151 -15
  114. package/src/modes/controllers/input-controller.ts +28 -0
  115. package/src/modes/controllers/selector-controller.ts +191 -96
  116. package/src/modes/print-mode.ts +64 -0
  117. package/src/modes/ui-language.ts +49 -2
  118. package/src/prompts/system/master-mode.md +28 -0
  119. package/src/sdk/broker/broker.ts +1636 -26
  120. package/src/sdk/broker/endpoint-authority.ts +86 -0
  121. package/src/sdk/broker/ensure.ts +5 -0
  122. package/src/sdk/broker/index.ts +3 -0
  123. package/src/sdk/broker/lifecycle.ts +186 -41
  124. package/src/sdk/broker/master-capability.ts +232 -0
  125. package/src/sdk/broker/session-index.ts +121 -17
  126. package/src/sdk/broker/session-scope.ts +272 -0
  127. package/src/sdk/broker/spawn-authority.ts +1031 -0
  128. package/src/sdk/broker/spawn-substrate.ts +548 -0
  129. package/src/sdk/broker/state-version.ts +26 -14
  130. package/src/sdk/broker/transport.ts +1 -0
  131. package/src/sdk/broker/worktree-occupancy.ts +90 -0
  132. package/src/sdk/bus/chat-daemon-control.ts +5 -2
  133. package/src/sdk/bus/chat-daemon-runtime.ts +35 -4
  134. package/src/sdk/bus/index.ts +51 -4
  135. package/src/sdk/bus/telegram-daemon-contract.ts +1 -1
  136. package/src/sdk/cli/index.ts +1 -0
  137. package/src/sdk/cli/master-cli.ts +259 -0
  138. package/src/sdk/cli/rows.ts +11 -8
  139. package/src/sdk/cli/session-cli.ts +264 -5
  140. package/src/sdk/client/client.ts +14 -8
  141. package/src/sdk/host/control/dispatch.ts +99 -38
  142. package/src/sdk/host/control/index.ts +2 -0
  143. package/src/sdk/host/control/operations.ts +2 -0
  144. package/src/sdk/host/control/runtime-gate.ts +203 -11
  145. package/src/sdk/host/host.ts +27 -2
  146. package/src/sdk/host/session-runtime.test.ts +210 -6
  147. package/src/sdk/host/session-runtime.ts +291 -49
  148. package/src/sdk/lifecycle/broker-client.ts +28 -0
  149. package/src/sdk/lifecycle/service.ts +279 -20
  150. package/src/sdk/lifecycle-session.ts +10 -2
  151. package/src/sdk/protocol/operation-inventory.generated.json +51 -0
  152. package/src/sdk/protocol/operation-registry.ts +13 -3
  153. package/src/sdk/router/session-router.ts +67 -59
  154. package/src/sdk/session.ts +357 -155
  155. package/src/sdk/startup-capability.ts +1 -0
  156. package/src/session/agent-session.ts +640 -103
  157. package/src/session/blob-store.ts +15 -5
  158. package/src/session/credential-store-errors.ts +2 -0
  159. package/src/session/internal/managed-session-storage.ts +153 -78
  160. package/src/session/session-manager.ts +337 -24
  161. package/src/session/yield-queue.ts +24 -10
  162. package/src/setup/frictionless-onboarding.ts +2 -0
  163. package/src/telemetry/events.ts +1138 -12
  164. package/src/tools/bash.ts +256 -42
  165. package/src/tools/gh.ts +104 -48
  166. package/src/tools/github-cache.ts +2 -2
  167. package/src/tools/index.ts +4 -0
@@ -1,9 +1,9 @@
1
1
  import * as crypto from "node:crypto";
2
- import type { BigIntStats, Stats } from "node:fs";
2
+ import type { BigIntStats } from "node:fs";
3
3
  import * as fs from "node:fs/promises";
4
4
  import * as path from "node:path";
5
- import type { NativeDirectoryTreeResult, NativeExactUnlinkResult } from "@gajae-code/natives";
6
- import { renameNoReplacePathAsync } from "@gajae-code/natives";
5
+ import type { NativeDirectoryTreeResult, NativeExactUnlinkResult, NativeNoReplaceResult } from "@gajae-code/natives";
6
+ import { renameDirectoryNoReplacePathAsync, renameNoReplacePathAsync } from "@gajae-code/natives";
7
7
  import { isEnoent } from "@gajae-code/utils/fs-error";
8
8
  import { nativeProcessBindings } from "@gajae-code/utils/native-process";
9
9
 
@@ -49,6 +49,10 @@ type LockInfo = FileLockOwnerToken;
49
49
 
50
50
  export const FileLockTestHooks: {
51
51
  afterParentMkdir?: (lockPath: string) => void | Promise<void>;
52
+ nativePublicationBindings?: () => {
53
+ renameNoReplacePathAsync: typeof renameNoReplacePathAsync;
54
+ renameDirectoryNoReplacePathAsync: typeof renameDirectoryNoReplacePathAsync;
55
+ };
52
56
  nativeQuarantineBindings?: () => NativeFileLockBindings;
53
57
  } = {};
54
58
 
@@ -423,6 +427,118 @@ async function ensureLockParent(directory: string): Promise<void> {
423
427
  }
424
428
  }
425
429
 
430
+ function isPlainRecord(value: unknown): value is Record<string, unknown> {
431
+ return (
432
+ typeof value === "object" &&
433
+ value !== null &&
434
+ !Array.isArray(value) &&
435
+ Object.getPrototypeOf(value) === Object.prototype
436
+ );
437
+ }
438
+
439
+ function isValidNativeNoReplaceResult(value: unknown): value is NativeNoReplaceResult {
440
+ if (!isPlainRecord(value)) return false;
441
+ const expectedKeys = [
442
+ "ok",
443
+ "code",
444
+ "mutationState",
445
+ "durabilityState",
446
+ "reason",
447
+ "primitive",
448
+ "phase",
449
+ "diagnostic",
450
+ ];
451
+ if (Object.keys(value).some(key => !expectedKeys.includes(key))) return false;
452
+ if (
453
+ typeof value.ok !== "boolean" ||
454
+ (value.code !== undefined && (typeof value.code !== "string" || !/^[a-z0-9_]{1,64}$/.test(value.code))) ||
455
+ !(["not_committed", "committed", "unknown"] as const).includes(value.mutationState as never) ||
456
+ !(["not_attempted", "proven", "not_provable"] as const).includes(value.durabilityState as never) ||
457
+ !(
458
+ [
459
+ "none",
460
+ "destination_exists",
461
+ "atomic_unavailable",
462
+ "cross_device",
463
+ "permission_denied",
464
+ "io_failure",
465
+ "invalid_request",
466
+ "interrupted",
467
+ "identity_violation",
468
+ "durability_not_provable",
469
+ "unknown",
470
+ ] as const
471
+ ).includes(value.reason as never) ||
472
+ !(
473
+ [
474
+ "renameat2_noreplace",
475
+ "linkat_noreplace",
476
+ "mkdirat_renameat_noreplace",
477
+ "renameatx_np_excl",
478
+ "windows_rename_noreplace",
479
+ "unsupported",
480
+ "unknown",
481
+ ] as const
482
+ ).includes(value.primitive as never) ||
483
+ !(
484
+ [
485
+ "preflight",
486
+ "file_sync",
487
+ "rename",
488
+ "source_unlink",
489
+ "source_parent_sync",
490
+ "destination_parent_sync",
491
+ "terminal_identity",
492
+ "complete",
493
+ "unknown",
494
+ ] as const
495
+ ).includes(value.phase as never)
496
+ )
497
+ return false;
498
+ if (!isPlainRecord(value.diagnostic)) return false;
499
+ if (
500
+ Object.keys(value.diagnostic).some(
501
+ key => !["schemaVersion", "collectionState", "osCode", "syncFailures"].includes(key),
502
+ ) ||
503
+ value.diagnostic.schemaVersion !== 1 ||
504
+ !(["complete", "partial", "unavailable"] as const).includes(value.diagnostic.collectionState as never) ||
505
+ (value.diagnostic.osCode !== undefined &&
506
+ (typeof value.diagnostic.osCode !== "number" || !Number.isInteger(value.diagnostic.osCode))) ||
507
+ (value.diagnostic.syncFailures !== undefined && !Array.isArray(value.diagnostic.syncFailures))
508
+ )
509
+ return false;
510
+ if (value.ok)
511
+ return (
512
+ value.mutationState === "committed" &&
513
+ value.reason === "none" &&
514
+ value.phase === "complete" &&
515
+ (value.durabilityState === "not_attempted" || value.durabilityState === "proven")
516
+ );
517
+ if (value.mutationState === "not_committed") return value.durabilityState === "not_attempted";
518
+ return value.mutationState === "unknown" && value.durabilityState === "not_provable";
519
+ }
520
+
521
+ /**
522
+ * Only a complete native envelope proving that no namespace mutation happened
523
+ * may authorize the directory fallback. A legacy or malformed result is
524
+ * treated as an unknown publication outcome and never followed by another
525
+ * mutating primitive.
526
+ */
527
+ function isPreMutationUnsupportedRenameResult(value: unknown): value is NativeNoReplaceResult {
528
+ if (!isValidNativeNoReplaceResult(value)) return false;
529
+ if (
530
+ value.ok !== false ||
531
+ (value.code !== "invalid_request" && value.code !== "atomic_unavailable") ||
532
+ value.mutationState !== "not_committed" ||
533
+ value.durabilityState !== "not_attempted" ||
534
+ value.reason !== value.code ||
535
+ (value.primitive !== "renameat2_noreplace" && value.primitive !== "renameatx_np_excl") ||
536
+ (value.phase !== "preflight" && value.phase !== "rename")
537
+ )
538
+ return false;
539
+ return true;
540
+ }
541
+
426
542
  async function localLockKey(lockPath: string): Promise<string> {
427
543
  try {
428
544
  return normalizeLockKey(await fs.realpath(lockPath));
@@ -452,17 +568,9 @@ function ownerIncarnationChanged(owner: FileLockOwnerToken, startTimeCache?: Map
452
568
  /** Outcome of a guarded lock-dir removal attempt (`removeFileLockDirForGc`). */
453
569
  export type FileLockGcRemoval = "removed" | "owner_changed" | "missing" | "cleanup_failed";
454
570
 
455
- interface LockDirStatToken {
456
- dev: number;
457
- ino: number;
458
- mtimeMs: number;
459
- ctimeMs: number;
460
- }
461
-
462
571
  type LockStaleSnapshot =
463
572
  | { stale: false }
464
- | { stale: true; owner: FileLockOwnerToken; identity: GenericFileLockDirIdentity }
465
- | { stale: true; owner: null; stat: LockDirStatToken };
573
+ | { stale: true; owner: FileLockOwnerToken; identity: GenericFileLockDirIdentity };
466
574
 
467
575
  /** Identity evidence carried by the generic stale verdict into a later removal. */
468
576
  export interface GenericFileLockDirIdentity {
@@ -634,22 +742,9 @@ function ownerLiveness(pid: number): OwnerLiveness {
634
742
  }
635
743
  }
636
744
 
637
- function statToken(stats: Stats): LockDirStatToken {
638
- return {
639
- dev: stats.dev,
640
- ino: stats.ino,
641
- mtimeMs: stats.mtimeMs,
642
- ctimeMs: stats.ctimeMs,
643
- };
644
- }
645
-
646
- function sameStatToken(a: LockDirStatToken, b: LockDirStatToken): boolean {
647
- return a.dev === b.dev && a.ino === b.ino && a.mtimeMs === b.mtimeMs && a.ctimeMs === b.ctimeMs;
648
- }
649
-
650
745
  async function staleLockSnapshot(
651
746
  lockPath: string,
652
- staleMs: number,
747
+ _staleMs: number,
653
748
  ownerHostId?: string,
654
749
  previousOwnerHostIds: readonly string[] = [],
655
750
  startTimeCache?: Map<string, string | null>,
@@ -673,26 +768,12 @@ async function staleLockSnapshot(
673
768
  if (isTransientReleaseError(error)) return { stale: false };
674
769
  throw error;
675
770
  }
676
- if (!info && ownerHostId !== undefined) return { stale: false };
677
771
  if (!info) {
678
- // A present but malformed owner record is not equivalent to a lock directory
679
- // caught between mkdir and metadata publication. Refuse stale fallback when the
680
- // metadata path exists: an unreadable live holder must never be reclaimed by
681
- // elapsed mtime alone.
682
- try {
683
- await fs.lstat(path.join(lockPath, "info"));
684
- return { stale: false };
685
- } catch (error) {
686
- if (!isEnoent(error)) throw error;
687
- }
688
- try {
689
- const stats = await fs.stat(lockPath);
690
- if (Date.now() - stats.mtimeMs <= staleMs) return { stale: false };
691
- return { stale: true, owner: null, stat: statToken(stats) };
692
- } catch (err) {
693
- if (isEnoent(err)) return { stale: false };
694
- throw err;
695
- }
772
+ // A directory without a valid owner record is either a contender between
773
+ // native mkdirat ownership and metadata publication, or malformed/foreign
774
+ // state. Neither case carries enough identity evidence for stale removal;
775
+ // elapsed mtime must never make this empty namespace reclaimable.
776
+ return { stale: false };
696
777
  }
697
778
 
698
779
  // A host-qualified lock may only be reclaimed after proving that its owner is
@@ -741,38 +822,7 @@ async function staleLockSnapshot(
741
822
 
742
823
  async function removeStaleLockForAcquire(lockPath: string, snapshot: LockStaleSnapshot): Promise<boolean> {
743
824
  if (!snapshot.stale) return false;
744
- if (snapshot.owner) {
745
- return (await removeFileLockDirForGc(lockPath, snapshot.owner, snapshot.identity)) === "removed";
746
- }
747
-
748
- let currentInfo: LockInfo | null;
749
- try {
750
- currentInfo = await readLockInfo(lockPath);
751
- } catch (error) {
752
- if (isTransientReleaseError(error)) return false;
753
- throw error;
754
- }
755
- if (currentInfo) return false;
756
- try {
757
- const currentStats = await fs.lstat(lockPath);
758
- if (currentStats.isSymbolicLink() || !currentStats.isDirectory()) return false;
759
- if (!sameStatToken(statToken(currentStats), snapshot.stat)) return false;
760
- const nativeCapturePath = await canonicalLockPathPreservingFinal(lockPath);
761
- const captured = nativeFileLockBindings().snapshotDirectoryTree(nativeCapturePath);
762
- if (captured.code === "sharing_violation") throwTransientNativeResult(captured.code);
763
- if (!captured.ok || !captured.snapshot) return false;
764
- if (
765
- captured.snapshot.rootDev !== String(currentStats.dev) ||
766
- captured.snapshot.rootIno !== String(currentStats.ino)
767
- )
768
- return false;
769
- const removed = nativeFileLockBindings().exactRemoveDirectoryTree(nativeCapturePath, captured.snapshot);
770
- return removed.ok || removed.code === "not_found";
771
- } catch (err) {
772
- if (isEnoent(err)) return false;
773
- if (isTransientReleaseError(err)) return false;
774
- throw err;
775
- }
825
+ return (await removeFileLockDirForGc(lockPath, snapshot.owner, snapshot.identity)) === "removed";
776
826
  }
777
827
 
778
828
  /**
@@ -812,7 +862,7 @@ export async function genericFileLockDirStaleVerdict(
812
862
  ownerHostId?: string,
813
863
  ): Promise<GenericFileLockDirStaleVerdict> {
814
864
  const verdict = await staleLockSnapshot(lockDir, staleMs, ownerHostId);
815
- if (!verdict.stale || verdict.owner === null) return { stale: false };
865
+ if (!verdict.stale) return { stale: false };
816
866
  return {
817
867
  stale: true,
818
868
  identity: verdict.identity,
@@ -841,11 +891,32 @@ async function tryAcquireLock(
841
891
  // already-created staging entry and its parent before publication so aliases
842
892
  // retain the same lock identity as the ordinary path.
843
893
  const canonicalParent = await fs.realpath(path.dirname(lockPath));
844
- const published = await renameNoReplacePathAsync(
845
- await fs.realpath(pendingPath),
846
- path.join(canonicalParent, path.basename(lockPath)),
847
- );
848
- if (!published.ok) {
894
+ const destinationPath = path.join(canonicalParent, path.basename(lockPath));
895
+ // Resolve only the stable parent: resolving the mutable staging final
896
+ // component would follow an attacker-replaced symlink before native no-follow
897
+ // validation gets a chance to reject it.
898
+ const canonicalPendingPath = path.join(canonicalParent, path.basename(pendingPath));
899
+ const publication = FileLockTestHooks.nativePublicationBindings?.() ?? {
900
+ renameNoReplacePathAsync,
901
+ renameDirectoryNoReplacePathAsync,
902
+ };
903
+ const published = await publication.renameNoReplacePathAsync(canonicalPendingPath, destinationPath);
904
+ let publishedSuccessfully = published.ok;
905
+ if (!published.ok && isPreMutationUnsupportedRenameResult(published)) {
906
+ const fallback = await publication.renameDirectoryNoReplacePathAsync(canonicalPendingPath, destinationPath);
907
+ if (fallback.ok) {
908
+ publishedSuccessfully = true;
909
+ } else if (fallback.reason === "destination_exists") {
910
+ return null;
911
+ } else {
912
+ const failure = new Error(
913
+ `Failed to publish file lock: ${fallback.code ?? fallback.reason ?? "unknown"}.`,
914
+ ) as NodeJS.ErrnoException;
915
+ if (fallback.code) failure.code = fallback.code;
916
+ throw failure;
917
+ }
918
+ }
919
+ if (!publishedSuccessfully) {
849
920
  if (published.reason === "destination_exists") return null;
850
921
  const failure = new Error(
851
922
  `Failed to publish file lock: ${published.code ?? published.reason ?? "unknown"}.`,
@@ -1103,6 +1174,10 @@ async function lockHolderDescription(lockPath: string): Promise<string> {
1103
1174
  }
1104
1175
 
1105
1176
  async function acquireLock(filePath: string, options: FileLockOptions = {}): Promise<() => Promise<void>> {
1177
+ const requestedFilePath: unknown = filePath;
1178
+ if (typeof requestedFilePath !== "string" || requestedFilePath.length === 0 || !path.isAbsolute(requestedFilePath))
1179
+ throw new TypeError("filePath must be a non-empty absolute path");
1180
+ if (requestedFilePath.includes("\0")) throw new TypeError("filePath must not contain NUL bytes");
1106
1181
  if (options.ownerHostId !== undefined && !options.ownerHostId) throw new Error("ownerHostId must be non-empty");
1107
1182
  if (options.previousOwnerHostIds?.some(hostId => !hostId))
1108
1183
  throw new Error("previousOwnerHostIds must contain only non-empty identities");
@@ -1167,11 +1167,7 @@ function recoverStateCopiesSync(
1167
1167
  (!primaryEvidence.allGenerationsVerified || primaryEvidence.duplicateRevision)
1168
1168
  )
1169
1169
  if (!options.allowInvalidPrimaryFallback || !backupEvidence?.fullyVerified) {
1170
- try {
1171
- validateStateGenerations(primaryEvidence.state, trustedKeys);
1172
- } catch (error) {
1173
- throw error;
1174
- }
1170
+ validateStateGenerations(primaryEvidence.state, trustedKeys);
1175
1171
  }
1176
1172
  if (
1177
1173
  backupEvidence &&
@@ -241,11 +241,11 @@ export const BUILTIN_MODEL_PROFILES: readonly ModelProfileDefinition[] = [
241
241
  architect: ["anthropic/claude-opus-5:xhigh", "anthropic/claude-opus-4-6:xhigh"],
242
242
  }),
243
243
  profile("claude-fable", ["anthropic"], {
244
- default: "anthropic/claude-fable-5:xhigh",
244
+ default: "anthropic/claude-fable-5-1:xhigh",
245
245
  executor: "anthropic/claude-sonnet-5",
246
- planner: "anthropic/claude-fable-5:low",
247
- critic: "anthropic/claude-fable-5:high",
248
- architect: "anthropic/claude-fable-5:xhigh",
246
+ planner: "anthropic/claude-fable-5-1:low",
247
+ critic: "anthropic/claude-fable-5-1:high",
248
+ architect: "anthropic/claude-fable-5-1:xhigh",
249
249
  }),
250
250
  profile("glm-eco", ["zai"], {
251
251
  default: "zai/glm-5.3-flash:low",
@@ -482,7 +482,7 @@ export const BUILTIN_MODEL_PROFILES: readonly ModelProfileDefinition[] = [
482
482
  architect: "openai-codex/gpt-5.6-sol:high",
483
483
  }),
484
484
  profile("fable-opus-codex", ["anthropic", "openai-codex"], {
485
- default: "anthropic/claude-fable-5:high",
485
+ default: "anthropic/claude-fable-5-1:high",
486
486
  executor: "openai-codex/gpt-5.6-terra:medium",
487
487
  planner: "anthropic/claude-opus-5:medium",
488
488
  critic: "anthropic/claude-opus-5:high",
@@ -1192,6 +1192,7 @@ type CustomModelOverlay = {
1192
1192
  wireModelId?: string;
1193
1193
  requestTransform?: ModelRequestTransform;
1194
1194
  cacheRetention?: CacheRetention;
1195
+ transport?: Model<Api>["transport"];
1195
1196
  isOAuth?: boolean;
1196
1197
  };
1197
1198
 
@@ -1303,6 +1304,7 @@ function buildCustomModelOverlay(
1303
1304
  providerRequestTransform: ModelRequestTransform | undefined,
1304
1305
  providerAuth: ProviderAuthMode | undefined,
1305
1306
  providerCacheRetention: CacheRetention | undefined,
1307
+ providerTransport: Model<Api>["transport"] | undefined,
1306
1308
  modelDef: CustomModelDefinitionLike,
1307
1309
  ): CustomModelOverlay | undefined {
1308
1310
  const api = modelDef.api ?? providerApi;
@@ -1331,6 +1333,7 @@ function buildCustomModelOverlay(
1331
1333
  contextPromotionTarget: modelDef.contextPromotionTarget,
1332
1334
  premiumMultiplier: modelDef.premiumMultiplier,
1333
1335
  cacheRetention: modelDef.cacheRetention ?? providerCacheRetention,
1336
+ transport: providerTransport,
1334
1337
  isOAuth: resolveCustomModelIsOAuth(api, providerAuth),
1335
1338
  };
1336
1339
  }
@@ -1466,6 +1469,7 @@ function finalizeCustomModel(model: CustomModelOverlay, options: CustomModelBuil
1466
1469
  wireModelId: resolvedModel.wireModelId,
1467
1470
  requestTransform: resolvedModel.requestTransform,
1468
1471
  cacheRetention: resolvedModel.cacheRetention ?? reference?.cacheRetention,
1472
+ transport: resolvedModel.transport,
1469
1473
  premiumMultiplier: resolvedModel.premiumMultiplier,
1470
1474
  isOAuth: resolvedModel.isOAuth,
1471
1475
  } as Model<Api>);
@@ -2291,6 +2295,7 @@ export class ModelRegistry {
2291
2295
  // re-merge bundled transport metadata here.
2292
2296
  headers: customModel.headers,
2293
2297
  compat: customModel.compat,
2298
+ transport: customModel.transport,
2294
2299
  contextPromotionTarget: customModel.contextPromotionTarget ?? existingModel.contextPromotionTarget,
2295
2300
  wireModelId: customModel.wireModelId,
2296
2301
  requestTransform: customModel.requestTransform,
@@ -4452,6 +4457,7 @@ export class ModelRegistry {
4452
4457
  providerConfig.requestTransform,
4453
4458
  (providerConfig.auth as ProviderAuthMode | undefined) ?? undefined,
4454
4459
  providerConfig.cacheRetention,
4460
+ providerConfig.transport,
4455
4461
  modelDef as CustomModelDefinitionLike,
4456
4462
  );
4457
4463
  if (!model) continue;
@@ -4584,6 +4590,22 @@ export class ModelRegistry {
4584
4590
  autoroutingProviderOrder(): readonly string[] {
4585
4591
  return projectCatalogProviderOrder(getConfiguredProviderOrderFromSettings(), this.#models);
4586
4592
  }
4593
+
4594
+ /**
4595
+ * Provider priority for automatic model selection. Explicit provider order
4596
+ * wins first, followed by omitted OAuth providers, then other providers;
4597
+ * catalog order breaks ties within each policy band.
4598
+ */
4599
+ automaticProviderOrder(sessionId?: string): readonly string[] {
4600
+ const policy = this.#buildProviderSelectionPolicy(sessionId);
4601
+ return [...policy.orderedProviders()].sort((left, right) => {
4602
+ const rankDifference = policy.rank(left) - policy.rank(right);
4603
+ return rankDifference !== 0
4604
+ ? rankDifference
4605
+ : policy.providerCatalogIndex(left) - policy.providerCatalogIndex(right);
4606
+ });
4607
+ }
4608
+
4587
4609
  #providerRankMap(policy: ProviderSelectionPolicy): Map<string, number> {
4588
4610
  const providerRank = new Map<string, number>();
4589
4611
  for (const provider of policy.orderedProviders()) {
@@ -5608,6 +5630,7 @@ export class ModelRegistry {
5608
5630
  config.requestTransform,
5609
5631
  undefined,
5610
5632
  undefined,
5633
+ config.transport,
5611
5634
  modelDef as CustomModelDefinitionLike,
5612
5635
  );
5613
5636
  if (!overlay) {
@@ -1580,6 +1580,11 @@ export async function findInitialModel(options: {
1580
1580
  thinkingLevel = resolveThinkingLevelForModel(found, defaultThinkingSelector);
1581
1581
  return { model, thinkingLevel, fallbackMessage: undefined };
1582
1582
  }
1583
+ return {
1584
+ model: undefined,
1585
+ thinkingLevel: undefined,
1586
+ fallbackMessage: `Model ${defaultProvider}/${defaultModelId} not found`,
1587
+ };
1583
1588
  }
1584
1589
 
1585
1590
  // 4. Try first available model with valid API key
@@ -421,6 +421,12 @@ export const SETTINGS_SCHEMA = {
421
421
  description: "Hard maximum runtime for an SDK prompt from acceptance, bounding progress-aware renewals.",
422
422
  validate: (value: number) => Number.isSafeInteger(value) && value >= 60_000 && value <= 86_400_000,
423
423
  },
424
+ "sdk.masterOrphanGraceMs": {
425
+ type: "number",
426
+ default: 120_000,
427
+ description: "Grace period before a master-spawned child orphaned by confirmed master loss is closed.",
428
+ validate: (value: number) => Number.isSafeInteger(value) && value >= 60_000 && value <= 3_600_000,
429
+ },
424
430
 
425
431
  // Notifications (shared daemon with Telegram/Discord/Slack presentation adapters)
426
432
  "notifications.enabled": { type: "boolean", default: false },
@@ -745,7 +751,7 @@ export const SETTINGS_SCHEMA = {
745
751
  // ────────────────────────────────────────────────────────────────────────
746
752
  "ui.language": {
747
753
  type: "enum",
748
- values: ["en", "ko", "ja"] as const,
754
+ values: ["en", "ko", "zh", "ja"] as const,
749
755
  default: "en",
750
756
  ui: {
751
757
  tab: "appearance",
@@ -754,6 +760,7 @@ export const SETTINGS_SCHEMA = {
754
760
  options: [
755
761
  { value: "en", label: "English" },
756
762
  { value: "ko", label: "Korean (한국어)" },
763
+ { value: "zh", label: "Chinese (简体中文)" },
757
764
  { value: "ja", label: "Japanese (日本語)" },
758
765
  ],
759
766
  },
@@ -748,6 +748,11 @@ export class Settings implements NotificationSettingsReader {
748
748
  return !this.#persist || !this.#hasRecoveredConfigSyntax;
749
749
  }
750
750
 
751
+ /** Whether this instance can publish a mutation to a durable config file. */
752
+ canPersistDurableConfig(): boolean {
753
+ return this.#persist && !this.#hasRecoveredConfigSyntax;
754
+ }
755
+
751
756
  /**
752
757
  * Set a setting value (sync).
753
758
  * Updates global settings and reserves its background persistence slot before
@@ -36,6 +36,8 @@ function projectionPlatform(): NodeJS.Platform {
36
36
 
37
37
  /** Post-filter parse-candidate cap. Exhaustion returns an explicit incomplete result. */
38
38
  export const COORDINATOR_JSON_SCAN_CAP = 10_000;
39
+ /** Number of fresh authoritative attempts allowed for a scan that observed churn. */
40
+ export const COORDINATOR_JSON_SCAN_RETRY_ATTEMPTS = 3;
39
41
 
40
42
  export interface ProjectionScanStat {
41
43
  size: number | bigint;
@@ -569,3 +571,22 @@ export async function listCoordinatorJsonFiles(
569
571
  await authority?.close().catch(() => undefined);
570
572
  }
571
573
  }
574
+
575
+ /**
576
+ * Retry only scans invalidated by observed directory/file churn. A candidate cap,
577
+ * unsupported authority, parse error, or any other failure remains fail-closed.
578
+ * Each attempt acquires a new authority, so no partial result crosses attempts.
579
+ */
580
+ export async function listCoordinatorJsonFilesWithRetry(
581
+ dir: string,
582
+ io: ProjectionScanFs = defaultFs,
583
+ cap: number = COORDINATOR_JSON_SCAN_CAP,
584
+ maxAttempts: number = COORDINATOR_JSON_SCAN_RETRY_ATTEMPTS,
585
+ ): Promise<ProjectionScanResult> {
586
+ const attempts = Math.max(1, Math.floor(maxAttempts));
587
+ let scan = await listCoordinatorJsonFiles(dir, io, cap);
588
+ for (let attempt = 1; attempt < attempts && scan.incomplete && scan.raced > 0; attempt++) {
589
+ scan = await listCoordinatorJsonFiles(dir, io, cap);
590
+ }
591
+ return scan;
592
+ }
@@ -1029,7 +1029,7 @@ function assertTransaction(transaction: CoordinatorSessionTransactionV1, namespa
1029
1029
  const rejectedHistorical = request.phase === "rejected";
1030
1030
  if (
1031
1031
  rejectedHistorical
1032
- ? !request.safe_receipt || request.safe_receipt.status !== "rejected"
1032
+ ? request.safe_receipt?.status !== "rejected"
1033
1033
  : question.answer_request_id !== request.request_id ||
1034
1034
  question.claim_fence_epoch !== request.claim_fence_epoch
1035
1035
  )
@@ -1083,7 +1083,7 @@ function assertTransaction(transaction: CoordinatorSessionTransactionV1, namespa
1083
1083
  event.transaction_revision > transaction.revision ||
1084
1084
  typeof event.kind !== "string" ||
1085
1085
  event.kind.length === 0 ||
1086
- !["turn", "question", "report", "session", "deletion"].includes(event.entity) ||
1086
+ !isOutboxEntity(event.entity) ||
1087
1087
  typeof event.entity_id !== "string" ||
1088
1088
  event.entity_id.length === 0 ||
1089
1089
  !isRecord(event.payload) ||
@@ -1296,7 +1296,7 @@ export async function listCanonicalActiveSessions(
1296
1296
  paths.registryLock,
1297
1297
  async () => {
1298
1298
  const registry = await readJson<NamespaceRegistryV1>(paths.registry);
1299
- if (!registry || registry.schema_version !== 1 || registry.namespace_id !== path.basename(paths.root))
1299
+ if (registry?.schema_version !== 1 || registry.namespace_id !== path.basename(paths.root))
1300
1300
  throw new Error("state_corrupt");
1301
1301
  return Object.values(registry.roster ?? {})
1302
1302
  .filter(entry => entry.active || entry.dirty)
@@ -1333,7 +1333,7 @@ export async function readDeliveryDiscoveryCursor(
1333
1333
  paths.registryLock,
1334
1334
  async () => {
1335
1335
  const registry = await readJson<NamespaceRegistryV1>(paths.registry);
1336
- if (!registry || registry.schema_version !== 1 || registry.namespace_id !== path.basename(paths.root))
1336
+ if (registry?.schema_version !== 1 || registry.namespace_id !== path.basename(paths.root))
1337
1337
  throw new Error("state_corrupt");
1338
1338
  return registry.delivery_discovery_cursor ?? "";
1339
1339
  },
@@ -1660,7 +1660,7 @@ export async function startCreationRemote(
1660
1660
  ): Promise<CreationRequestV1> {
1661
1661
  return await withNamespaceRegistry(paths, async registry => {
1662
1662
  const request = registry.creations[keyDigest];
1663
- if (!request || !request.sidecar_verifier) throw new Error("state_corrupt");
1663
+ if (!request?.sidecar_verifier) throw new Error("state_corrupt");
1664
1664
  if (
1665
1665
  request.phase === "claimed" &&
1666
1666
  (request.sidecar_verifier.key_id !== expectedVerifier.key_id ||
@@ -1687,8 +1687,7 @@ export async function reconcileCreationRemoteVerifier(
1687
1687
  ): Promise<CreationRequestV1> {
1688
1688
  return await withNamespaceRegistry(paths, async registry => {
1689
1689
  const request = registry.creations[keyDigest];
1690
- if (!request || !request.sidecar_verifier || request.phase !== "remote_started")
1691
- throw new Error("terminal_uncertain");
1690
+ if (!request?.sidecar_verifier || request.phase !== "remote_started") throw new Error("terminal_uncertain");
1692
1691
  if (!/^[a-f0-9]{64}$/.test(usedKeyId)) throw new Error("terminal_uncertain");
1693
1692
  if (usedKeyId === candidate.key_id) request.sidecar_verifier = candidate;
1694
1693
  else if (usedKeyId !== request.sidecar_verifier.key_id) throw new Error("terminal_uncertain");
@@ -2241,7 +2240,7 @@ export async function enumeratePublicDeliveries(
2241
2240
  paths.registryLock,
2242
2241
  async () => {
2243
2242
  const registry = await readJson<NamespaceRegistryV1>(paths.registry);
2244
- if (!registry || registry.schema_version !== 1 || registry.namespace_id !== path.basename(paths.root))
2243
+ if (registry?.schema_version !== 1 || registry.namespace_id !== path.basename(paths.root))
2245
2244
  throw new Error("state_corrupt");
2246
2245
  return [...new Set([...Object.keys(registry.roster ?? {}), ...Object.keys(registry.retained_sessions ?? {})])]
2247
2246
  .filter(name => COORDINATOR_SESSION_ID_PATTERN.test(name))
@@ -2526,7 +2525,7 @@ export async function replaceCreationRetirementIntent(
2526
2525
  if (request.phase !== "remote_started" && request.phase !== "uncertain" && request.phase !== "retired")
2527
2526
  throw new Error("retire_not_allowed");
2528
2527
  const staged = request.retirement_intent;
2529
- if (!staged || staged.phase !== "intent" || staged.broker_proof) throw new Error("state_corrupt");
2528
+ if (staged?.phase !== "intent" || staged.broker_proof) throw new Error("state_corrupt");
2530
2529
  if (staged.retirement_key_digest !== retirementKeyDigest) throw new Error("idempotency_conflict");
2531
2530
  assertCreationRetirementProofMatches(request, proof, false);
2532
2531
  staged.phase = "pre_effect_rejected";
@@ -2716,8 +2715,7 @@ export async function removeSessionTransaction(
2716
2715
  // exporter lease expires instead of deleting an undelivered event.
2717
2716
  if (Object.values(transaction.outbox).some(event => event.public_delivery.state !== "acknowledged"))
2718
2717
  return false;
2719
- await fs.rm(file, { force: true });
2720
- await syncCoordinatorDirectory(path.dirname(file));
2718
+ await removeCoordinatorStateFile(file);
2721
2719
  delete registry.roster?.[sessionId];
2722
2720
  delete registry.retained_sessions?.[sessionId];
2723
2721
  return true;