@gajae-code/coding-agent 0.17.4 → 0.17.5

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 (84) hide show
  1. package/CHANGELOG.md +73 -0
  2. package/dist/types/config/autorouting-tier-map.d.ts +1031 -3
  3. package/dist/types/config/file-lock.d.ts +4 -2
  4. package/dist/types/config/model-registry.d.ts +3 -1
  5. package/dist/types/config/model-resolver.d.ts +1 -1
  6. package/dist/types/exec/bash-executor.d.ts +2 -0
  7. package/dist/types/extensibility/gjc-plugins/runtime-adapters.d.ts +11 -2
  8. package/dist/types/gjc-runtime/state-writer.d.ts +14 -1
  9. package/dist/types/harness-control-plane/control-endpoint.d.ts +4 -1
  10. package/dist/types/harness-control-plane/finalize.d.ts +14 -3
  11. package/dist/types/main.d.ts +1 -1
  12. package/dist/types/runtime-mcp/loader.d.ts +2 -0
  13. package/dist/types/runtime-mcp/manager.d.ts +13 -6
  14. package/dist/types/sdk/broker/broker.d.ts +4 -2
  15. package/dist/types/sdk/broker/ensure.d.ts +3 -0
  16. package/dist/types/sdk/broker/identity.d.ts +2 -0
  17. package/dist/types/sdk/broker/managed-task-dag.d.ts +1335 -0
  18. package/dist/types/sdk/broker/managed-task-verification.d.ts +37 -0
  19. package/dist/types/sdk/broker/spawn-authority.d.ts +8 -0
  20. package/dist/types/sdk/broker/startup-failure.d.ts +4 -0
  21. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +5 -1
  22. package/dist/types/sdk/prompt-deadline-flush.d.ts +2 -0
  23. package/dist/types/sdk/session.d.ts +3 -3
  24. package/dist/types/session/agent-session.d.ts +7 -2
  25. package/dist/types/task/types.d.ts +2 -0
  26. package/dist/types/tools/bash-interactive.d.ts +1 -0
  27. package/dist/types/tools/fetch.d.ts +10 -1
  28. package/dist/types/tools/subagent.d.ts +8 -1
  29. package/package.json +7 -7
  30. package/src/cli/public-command-registry.ts +8 -2
  31. package/src/cli/setup-cli.ts +1 -0
  32. package/src/cli/stats-cli.ts +11 -1
  33. package/src/config/autorouting-tier-map.ts +529 -3
  34. package/src/config/file-lock.ts +59 -14
  35. package/src/config/model-discovery-manager.ts +4 -4
  36. package/src/config/model-profile-activation.ts +83 -23
  37. package/src/config/model-registry.ts +72 -12
  38. package/src/config/model-resolver.ts +6 -1
  39. package/src/coordinator-mcp/question-state.ts +4 -1
  40. package/src/coordinator-mcp/server.ts +14 -6
  41. package/src/exec/bash-executor.ts +3 -0
  42. package/src/extensibility/gjc-plugins/runtime-adapters.ts +46 -8
  43. package/src/gjc-runtime/state-writer.ts +317 -6
  44. package/src/harness-control-plane/control-endpoint.ts +24 -6
  45. package/src/harness-control-plane/finalize.ts +312 -25
  46. package/src/harness-control-plane/owner.ts +323 -89
  47. package/src/internal-urls/docs-index.generated.ts +6 -4
  48. package/src/main.ts +69 -19
  49. package/src/modes/acp/acp-agent.ts +581 -37
  50. package/src/modes/components/gjc-bundle-settings.ts +21 -1
  51. package/src/modes/components/tool-status-header.ts +72 -6
  52. package/src/modes/controllers/runtime-mcp-command-controller.ts +1 -1
  53. package/src/runtime-mcp/loader.ts +6 -3
  54. package/src/runtime-mcp/manager.ts +177 -75
  55. package/src/sdk/broker/broker.ts +1057 -150
  56. package/src/sdk/broker/ensure.ts +21 -3
  57. package/src/sdk/broker/identity.ts +13 -0
  58. package/src/sdk/broker/managed-task-dag.ts +1927 -0
  59. package/src/sdk/broker/managed-task-verification.ts +642 -0
  60. package/src/sdk/broker/spawn-authority.ts +140 -7
  61. package/src/sdk/broker/startup-failure.ts +80 -1
  62. package/src/sdk/broker/transport.ts +1 -0
  63. package/src/sdk/bus/chat-daemon-control.ts +2 -2
  64. package/src/sdk/bus/index.ts +22 -6
  65. package/src/sdk/bus/telegram-daemon-contract.ts +5 -1
  66. package/src/sdk/cli/session-cli.ts +18 -0
  67. package/src/sdk/host/session-runtime.test.ts +46 -0
  68. package/src/sdk/host/session-runtime.ts +13 -6
  69. package/src/sdk/lifecycle/service.ts +2 -0
  70. package/src/sdk/prompt-deadline-flush.ts +26 -4
  71. package/src/sdk/session.ts +587 -115
  72. package/src/session/agent-session.ts +40 -12
  73. package/src/session/internal/managed-session-scope.ts +10 -0
  74. package/src/session/internal/managed-session-storage.ts +30 -1
  75. package/src/setup/provider-presets.json +2 -1
  76. package/src/slash-commands/builtin-registry.ts +1 -1
  77. package/src/task/executor.ts +46 -17
  78. package/src/task/types.ts +2 -0
  79. package/src/tools/bash-interactive.ts +2 -0
  80. package/src/tools/bash.ts +20 -0
  81. package/src/tools/fetch.ts +21 -5
  82. package/src/tools/subagent-render.ts +24 -1
  83. package/src/tools/subagent.ts +27 -1
  84. package/src/utils/mupdf.ts +11 -4
@@ -44,12 +44,14 @@ export type FileLockAcquireReason = "acquire_timeout" | "orphan_transition";
44
44
  * generation); this records the cause that exhaustion must report.
45
45
  */
46
46
  export interface FileLockStaleRemovalFailure {
47
- /** Guarded-removal outcome the acquire path can record, or `"error"` when the removal attempt threw. */
48
- outcome: "cleanup_failed" | "error";
47
+ /** Guarded-removal outcome; refusals are surfaced only after exhaustion revalidates the dead generation. */
48
+ outcome: "cleanup_failed" | "owner_changed" | "missing" | "error";
49
49
  /** Native/errno code carried by the refusal, when one was present. */
50
50
  code?: string;
51
51
  /** Human-readable cause surfaced to the operator at exhaustion. */
52
52
  message: string;
53
+ /** Platform-specific manual cleanup command, set only when the same dead owner persists at exhaustion. */
54
+ manualCleanupCommand?: string;
53
55
  }
54
56
 
55
57
  export class FileLockAcquireError extends Error {
@@ -70,10 +72,12 @@ export class FileLockAcquireError extends Error {
70
72
  removalFailure.code ? ` [${removalFailure.code}]` : ""
71
73
  })`
72
74
  : "";
75
+ const cleanupDetail = removalFailure?.manualCleanupCommand
76
+ ? `; manual cleanup is appropriate only after independently verifying this exact directory still belongs to the dead owner and no successor has taken it: ${removalFailure.manualCleanupCommand}`
77
+ : "";
73
78
  super(
74
79
  `Failed to acquire lock for ${filePath} after ${attempts} attempts: ${detail}${removalDetail} (${lockPath}); ` +
75
- `a live owner is never displaced — if this is an SDK broker (gjc sdk session list), it must finish or be stopped before retrying; ` +
76
- `the lock is a directory, remove it only by deleting the directory (${lockPath}) once no live owner remains`,
80
+ `a live owner is never displaced — if this is an SDK broker (gjc sdk session list), it must finish or be stopped before retrying${cleanupDetail}`,
77
81
  );
78
82
  this.code = reason;
79
83
  this.name = "FileLockAcquireError";
@@ -1069,6 +1073,10 @@ function ownerIncarnationChanged(owner: FileLockOwnerToken, startTimeCache?: Map
1069
1073
  return currentStartTime !== null && currentStartTime !== owner.start_time;
1070
1074
  }
1071
1075
 
1076
+ function ownerGenerationIsDead(owner: FileLockOwnerToken): boolean {
1077
+ return ownerLiveness(owner.pid) === "dead" || ownerIncarnationChanged(owner);
1078
+ }
1079
+
1072
1080
  /** Outcome of a guarded lock-dir removal attempt (`removeFileLockDirForGc`). */
1073
1081
  export type FileLockGcRemoval = "removed" | "owner_changed" | "missing" | "cleanup_failed";
1074
1082
 
@@ -1550,13 +1558,17 @@ async function staleLockSnapshot(
1550
1558
 
1551
1559
  type StaleLockRemovalAttempt = { removed: true } | { removed: false; failure?: FileLockStaleRemovalFailure };
1552
1560
 
1553
- type RecordedStaleRemovalFailure = { owner: FileLockOwnerToken; failure: FileLockStaleRemovalFailure };
1561
+ type RecordedStaleRemovalFailure = {
1562
+ owner: FileLockOwnerToken;
1563
+ identity: GenericFileLockDirIdentity;
1564
+ failure: FileLockStaleRemovalFailure;
1565
+ };
1554
1566
 
1555
1567
  /**
1556
- * A recorded refusal describes exactly one dead owner generation. Re-read the pathname at
1557
- * exhaustion and report it only while that same generation still owns the directory, so a
1558
- * refusal recorded for a dead generation is never pinned onto a live successor that took
1559
- * the pathname over before the budget ran out.
1568
+ * A recorded refusal describes exactly one dead owner generation and directory identity.
1569
+ * Re-read the pathname at exhaustion and report it only while both still match, so a refusal
1570
+ * recorded for a dead generation is never pinned onto a successor that took the pathname over
1571
+ * before the budget ran out, even if its legacy owner bytes are identical.
1560
1572
  */
1561
1573
  async function staleRemovalFailureForCurrentGeneration(
1562
1574
  lockPath: string,
@@ -1564,13 +1576,31 @@ async function staleRemovalFailureForCurrentGeneration(
1564
1576
  ): Promise<FileLockStaleRemovalFailure | undefined> {
1565
1577
  if (!recorded) return undefined;
1566
1578
  try {
1567
- const current = await readLockInfo(lockPath);
1568
- return current && sameFileLockOwnerToken(current, recorded.owner) ? recorded.failure : undefined;
1579
+ const observation = await readLockInfoObservation(lockPath);
1580
+ if (!observation) return undefined;
1581
+ const current = parseLockInfoBytes(observation.bytes);
1582
+ if (!current || !sameFileLockOwnerToken(current, recorded.owner) || !ownerGenerationIsDead(current))
1583
+ return undefined;
1584
+ const currentIdentity = fileLockDirIdentityFromPathState(observation.state, observation.bytes);
1585
+ if (!sameGenericFileLockDirIdentity(recorded.identity, currentIdentity)) return undefined;
1586
+ return {
1587
+ ...recorded.failure,
1588
+ manualCleanupCommand: manualLockCleanupCommand(lockPath),
1589
+ };
1569
1590
  } catch {
1570
1591
  return undefined;
1571
1592
  }
1572
1593
  }
1573
1594
 
1595
+ function manualLockCleanupCommand(lockPath: string): string {
1596
+ if (process.platform === "win32") {
1597
+ const quotedPath = lockPath.replace(/'/g, "''");
1598
+ return `Remove-Item -LiteralPath '${quotedPath}' -Recurse -Force`;
1599
+ }
1600
+ const quotedPath = lockPath.replace(/'/g, "'\\''");
1601
+ return `rm -rf -- '${quotedPath}'`;
1602
+ }
1603
+
1574
1604
  async function removeStaleLockForAcquire(
1575
1605
  lockPath: string,
1576
1606
  snapshot: LockStaleSnapshot,
@@ -1579,8 +1609,21 @@ async function removeStaleLockForAcquire(
1579
1609
  try {
1580
1610
  const outcome = await removeFileLockDirForGc(lockPath, snapshot.owner, snapshot.identity);
1581
1611
  if (outcome === "removed") return { removed: true };
1582
- // `owner_changed`/`missing` mean a successor or a concurrent reclaimer already
1583
- // owns the pathname; that is ordinary contention, not a host removal failure.
1612
+ // Either refusal can mean a successor or an incomplete publication owns the path;
1613
+ // keep it only as a diagnostic candidate and expose it at exhaustion if the same
1614
+ // dead owner still persists. Never fall back to path-only deletion.
1615
+ if (outcome === "owner_changed" || outcome === "missing") {
1616
+ return {
1617
+ removed: false,
1618
+ failure: {
1619
+ outcome,
1620
+ message:
1621
+ outcome === "owner_changed"
1622
+ ? "the identity-bound removal guard returned owner_changed; no path-only fallback was attempted"
1623
+ : "the guarded removal could not read the owner record; no path-only fallback was attempted",
1624
+ },
1625
+ };
1626
+ }
1584
1627
  if (outcome !== "cleanup_failed") return { removed: false };
1585
1628
  return {
1586
1629
  removed: false,
@@ -2207,7 +2250,9 @@ export async function acquireFileLock(filePath: string, options: FileLockOptions
2207
2250
  continue;
2208
2251
  }
2209
2252
  staleRemovalFailure =
2210
- stale.stale && staleRemoval.failure ? { owner: stale.owner, failure: staleRemoval.failure } : undefined;
2253
+ stale.stale && staleRemoval.failure
2254
+ ? { owner: stale.owner, identity: stale.identity, failure: staleRemoval.failure }
2255
+ : undefined;
2211
2256
  if (!opts.signal) {
2212
2257
  await Bun.sleep(opts.retryDelayMs);
2213
2258
  continue;
@@ -222,11 +222,11 @@ export class ModelDiscoveryManager<TProvider extends DiscoveryProvider> {
222
222
  ? result.stale
223
223
  ? "cached"
224
224
  : "ok"
225
- : result.fetched
225
+ : result.fetched ||
226
+ (result.cacheFresh && result.cacheAuthoritative && result.dynamicModelIds?.length === 0)
226
227
  ? "empty"
227
- : // No fetch happened (non-authoritative retry backoff, or an
228
- // ineligible cache) and no eligible row served models: the
229
- // provider is unvalidated, not authoritatively empty.
228
+ : // No fetch happened and no fresh authoritative empty row
229
+ // attested to this result, so the provider is unvalidated.
230
230
  "unavailable";
231
231
  const state: ProviderDiscoveryState = {
232
232
  provider: provider.provider,
@@ -1528,15 +1528,58 @@ export async function prepareModelProfileActivation(
1528
1528
  previousDefaultFallbackRuntimeState: options.session.getDefaultFallbackRuntimeState?.(),
1529
1529
  };
1530
1530
  } catch (error) {
1531
- restoreCanonicalVariant(options.modelRegistry, options.session.sessionId, previousCanonicalVariant);
1531
+ try {
1532
+ restoreCanonicalVariant(options.modelRegistry, options.session.sessionId, previousCanonicalVariant);
1533
+ } catch (rollbackError) {
1534
+ throw incompleteModelProfileRollbackError("preparation", "profile preflight", error, [
1535
+ { stage: "restore canonical model variant", error: rollbackError },
1536
+ ]);
1537
+ }
1532
1538
  throw error;
1533
1539
  }
1534
1540
  }
1535
1541
 
1542
+ function modelProfileFailureReason(error: unknown, stage: string): string {
1543
+ try {
1544
+ if (error instanceof ModelProfileCredentialError) return "required credentials unavailable";
1545
+ if (error instanceof Error && error.message.startsWith("No API key for ")) return "credentials unavailable";
1546
+ } catch {
1547
+ // An exotic error getter must not replace the activation failure.
1548
+ }
1549
+ return stage.includes("settings") || stage.startsWith("persist ") || stage.endsWith(" setting")
1550
+ ? "settings write failed"
1551
+ : "operation failed";
1552
+ }
1553
+
1554
+ class ModelProfileActivationStageError extends Error {
1555
+ readonly stage: string;
1556
+
1557
+ constructor(stage: string, error: unknown) {
1558
+ super(`${stage} (${modelProfileFailureReason(error, stage)})`, { cause: error });
1559
+ this.name = "ModelProfileActivationStageError";
1560
+ this.stage = stage;
1561
+ }
1562
+ }
1563
+
1564
+ function incompleteModelProfileRollbackError(
1565
+ phase: "preparation" | "activation",
1566
+ stage: string,
1567
+ error: unknown,
1568
+ rollbackErrors: readonly { stage: string; error: unknown }[],
1569
+ ): AggregateError {
1570
+ const primary = new ModelProfileActivationStageError(stage, error);
1571
+ const incomplete = rollbackErrors.map(failure => new ModelProfileActivationStageError(failure.stage, failure.error));
1572
+ return new AggregateError(
1573
+ [primary, ...incomplete],
1574
+ `Model profile ${phase} failed at ${primary.message}; rollback incomplete: ${incomplete.map(failure => failure.message).join("; ")}. Prior state may be unverified.`,
1575
+ );
1576
+ }
1577
+
1536
1578
  export async function applyPreparedModelProfileActivation(
1537
1579
  prepared: PreparedModelProfileActivation,
1538
1580
  options: ApplyModelProfileActivationOptions = {},
1539
1581
  ): Promise<void> {
1582
+ let activationStage = "default chain";
1540
1583
  let modelMutationStarted = false;
1541
1584
  let overridesChanged = false;
1542
1585
  let modelRolesChanged = false;
@@ -1567,15 +1610,21 @@ export async function applyPreparedModelProfileActivation(
1567
1610
  }
1568
1611
  }
1569
1612
  if (prepared.defaultModel) {
1570
- modelMutationStarted = true;
1613
+ activationStage = "model selection";
1571
1614
  await prepared.session.setModelTemporary(
1572
1615
  prepared.defaultModel,
1573
1616
  options.thinkingLevelOverride ?? prepared.defaultThinkingLevel,
1574
- { cause: "profile-activation" },
1617
+ {
1618
+ cause: "profile-activation",
1619
+ onMutationStarted: () => {
1620
+ modelMutationStarted = true;
1621
+ },
1622
+ },
1575
1623
  );
1576
1624
  }
1577
1625
  // Always reinstall the model role layer from the durable base plus the
1578
1626
  // new profile's roles so omitted roles from the previous profile are dropped.
1627
+ activationStage = "model role overrides";
1579
1628
  prepared.settings.override("modelRoles", {
1580
1629
  ...prepared.baseModelRoles,
1581
1630
  ...prepared.modelRoles,
@@ -1584,6 +1633,7 @@ export async function applyPreparedModelProfileActivation(
1584
1633
  // Always reinstall the agent role layer from the durable base plus the
1585
1634
  // new profile's roles: a default-only or role-free successor must drop
1586
1635
  // the previous profile's role-agent mappings rather than inheriting them.
1636
+ activationStage = "agent role overrides";
1587
1637
  prepared.settings.override("task.agentModelOverrides", {
1588
1638
  ...prepared.baseAgentModelOverrides,
1589
1639
  ...prepared.agentModelOverrides,
@@ -1591,65 +1641,74 @@ export async function applyPreparedModelProfileActivation(
1591
1641
  overridesChanged = true;
1592
1642
  if (options.persistDefault) {
1593
1643
  persistentMutationStarted = true;
1644
+ activationStage = "persist model roles";
1594
1645
  prepared.settings.set("modelRoles", {});
1646
+ activationStage = "persist agent roles";
1595
1647
  prepared.settings.set("task.agentModelOverrides", {});
1596
1648
  if (prepared.defaultThinkingLevel !== undefined && prepared.defaultThinkingLevel !== ThinkingLevel.Inherit) {
1649
+ activationStage = "persist thinking level";
1597
1650
  prepared.settings.set("defaultThinkingLevel", prepared.defaultThinkingLevel);
1598
1651
  }
1652
+ activationStage = "persist default profile";
1599
1653
  prepared.settings.set("modelProfile.default", prepared.profileName);
1654
+ activationStage = "forward settings flush";
1600
1655
  await prepared.settings.flushOrThrow();
1601
1656
  }
1657
+ activationStage = "active profile marker";
1602
1658
  prepared.session.setActiveModelProfile?.(prepared.profileName);
1603
1659
  if (prepared.defaultModel) {
1660
+ activationStage = "canonical model variant";
1604
1661
  prepared.modelRegistry.seedCanonicalVariant?.(prepared.session.sessionId, prepared.defaultModel);
1605
1662
  resumeDefaultChanged = true;
1663
+ activationStage = "resume default model";
1606
1664
  prepared.session.recordResumeDefaultModel?.(`${prepared.defaultModel.provider}/${prepared.defaultModel.id}`);
1607
1665
  }
1666
+ activationStage = "installed role tracking";
1608
1667
  prepared.session.noteProfileInstalledOverrides?.(
1609
1668
  Object.keys(prepared.modelRoles),
1610
1669
  Object.keys(prepared.agentModelOverrides),
1611
1670
  prepared.previousModel,
1612
1671
  );
1613
1672
  } catch (error) {
1614
- const rollbackErrors: unknown[] = [];
1615
- const restore = (action: () => void): void => {
1673
+ const rollbackErrors: Array<{ stage: string; error: unknown }> = [];
1674
+ const restore = (stage: string, action: () => void): void => {
1616
1675
  try {
1617
1676
  action();
1618
1677
  } catch (rollbackError) {
1619
- rollbackErrors.push(rollbackError);
1678
+ rollbackErrors.push({ stage, error: rollbackError });
1620
1679
  }
1621
1680
  };
1622
1681
  if (persistentMutationStarted) {
1623
- restore(() =>
1682
+ restore("restore default profile setting", () =>
1624
1683
  prepared.previousPersistedDefaultProfile === undefined
1625
1684
  ? prepared.settings.unset("modelProfile.default")
1626
1685
  : prepared.settings.set("modelProfile.default", prepared.previousPersistedDefaultProfile),
1627
1686
  );
1628
- restore(() =>
1687
+ restore("restore model role setting", () =>
1629
1688
  prepared.previousPersistedModelRoles === undefined
1630
1689
  ? prepared.settings.unset("modelRoles")
1631
1690
  : prepared.settings.set("modelRoles", prepared.previousPersistedModelRoles),
1632
1691
  );
1633
- restore(() =>
1692
+ restore("restore agent role setting", () =>
1634
1693
  prepared.previousPersistedAgentModelOverrides === undefined
1635
1694
  ? prepared.settings.unset("task.agentModelOverrides")
1636
1695
  : prepared.settings.set("task.agentModelOverrides", prepared.previousPersistedAgentModelOverrides),
1637
1696
  );
1638
- restore(() =>
1697
+ restore("restore thinking level setting", () =>
1639
1698
  prepared.previousPersistedDefaultThinkingLevel === undefined
1640
1699
  ? prepared.settings.unset("defaultThinkingLevel")
1641
1700
  : prepared.settings.set("defaultThinkingLevel", prepared.previousPersistedDefaultThinkingLevel),
1642
1701
  );
1643
1702
  }
1644
1703
  if (modelRolesChanged) {
1645
- restore(() =>
1704
+ restore("restore model role overrides", () =>
1646
1705
  prepared.previousModelRolesOverride === undefined
1647
1706
  ? prepared.settings.clearOverride("modelRoles")
1648
1707
  : prepared.settings.override("modelRoles", prepared.previousModelRolesOverride),
1649
1708
  );
1650
1709
  }
1651
1710
  if (overridesChanged) {
1652
- restore(() =>
1711
+ restore("restore agent role overrides", () =>
1653
1712
  prepared.previousAgentModelOverridesOverride === undefined
1654
1713
  ? prepared.settings.clearOverride("task.agentModelOverrides")
1655
1714
  : prepared.settings.override("task.agentModelOverrides", prepared.previousAgentModelOverridesOverride),
@@ -1670,15 +1729,17 @@ export async function applyPreparedModelProfileActivation(
1670
1729
  throw new Error("Model-less profile activation rollback is unavailable");
1671
1730
  }
1672
1731
  } catch (rollbackError) {
1673
- rollbackErrors.push(rollbackError);
1732
+ rollbackErrors.push({ stage: "restore live model", error: rollbackError });
1674
1733
  }
1675
1734
  }
1676
1735
  if (resumeDefaultChanged) {
1677
- restore(() => prepared.session.recordResumeDefaultModel?.(prepared.previousSessionDefaultModel));
1736
+ restore("restore resume default", () =>
1737
+ prepared.session.recordResumeDefaultModel?.(prepared.previousSessionDefaultModel),
1738
+ );
1678
1739
  }
1679
1740
  if (defaultChainChanged) {
1680
1741
  const previousChain = prepared.previousDefaultChainState;
1681
- restore(() =>
1742
+ restore("restore default chain", () =>
1682
1743
  prepared.session.setConfiguredModelChain(
1683
1744
  "default",
1684
1745
  previousChain?.entries ??
@@ -1691,26 +1752,25 @@ export async function applyPreparedModelProfileActivation(
1691
1752
  );
1692
1753
  }
1693
1754
  if (prepared.previousDefaultFallbackRuntimeState) {
1694
- restore(() =>
1755
+ restore("restore fallback runtime", () =>
1695
1756
  prepared.session.restoreDefaultFallbackRuntimeState?.(prepared.previousDefaultFallbackRuntimeState!),
1696
1757
  );
1697
1758
  }
1698
- restore(() => prepared.session.setActiveModelProfile?.(prepared.previousActiveModelProfile));
1699
- restore(() =>
1759
+ restore("restore active profile", () =>
1760
+ prepared.session.setActiveModelProfile?.(prepared.previousActiveModelProfile),
1761
+ );
1762
+ restore("restore canonical model variant", () =>
1700
1763
  restoreCanonicalVariant(prepared.modelRegistry, prepared.session.sessionId, prepared.previousCanonicalVariant),
1701
1764
  );
1702
1765
  if (persistentMutationStarted) {
1703
1766
  try {
1704
1767
  await prepared.settings.flushOrThrow();
1705
1768
  } catch (rollbackError) {
1706
- rollbackErrors.push(rollbackError);
1769
+ rollbackErrors.push({ stage: "restore settings flush", error: rollbackError });
1707
1770
  }
1708
1771
  }
1709
1772
  if (rollbackErrors.length > 0) {
1710
- throw new AggregateError(
1711
- [error, ...rollbackErrors],
1712
- "Model profile activation failed and rollback was incomplete",
1713
- );
1773
+ throw incompleteModelProfileRollbackError("activation", activationStage, error, rollbackErrors);
1714
1774
  }
1715
1775
  throw error;
1716
1776
  }
@@ -83,6 +83,8 @@ import { ModelDiscoveryManager, type ProviderDiscoveryState } from "./model-disc
83
83
  import {
84
84
  loadAcceptedModelPresetProfiles,
85
85
  type ModelPresetRegistryDependencies,
86
+ type ModelPresetRegistryRefreshResult,
87
+ refreshModelPresetRegistry,
86
88
  refreshModelPresetRegistryInBackground,
87
89
  } from "./model-preset-registry";
88
90
 
@@ -1902,6 +1904,17 @@ export class ModelRegistry {
1902
1904
  });
1903
1905
  }
1904
1906
 
1907
+ /** Refresh the signed profile catalog online, then publish its accepted snapshot. */
1908
+ async refreshModelPresetProfilesFromRegistry(): Promise<ModelPresetRegistryRefreshResult> {
1909
+ const result = await refreshModelPresetRegistry({
1910
+ ...this.#modelPresetRegistryDependencies,
1911
+ agentDir: this.#modelPresetRegistryAgentDir,
1912
+ knownManifestSha256: this.#loadedModelPresetRegistryManifestSha256,
1913
+ });
1914
+ await this.refreshStatic();
1915
+ return result;
1916
+ }
1917
+
1905
1918
  /**
1906
1919
  * Reload models from disk and refresh runtime provider discovery state.
1907
1920
  */
@@ -2547,12 +2560,20 @@ export class ModelRegistry {
2547
2560
  for (const providerConfig of this.#discoveryManager.providers) {
2548
2561
  if (providerId && providerConfig.provider !== providerId) continue;
2549
2562
  let expectedProvenance: string | undefined;
2563
+ let effectiveConfig: DiscoveryProviderConfig | undefined;
2564
+ let expectedAuthGeneration: string | undefined;
2565
+ let expectedEndpoint: string | undefined;
2550
2566
  try {
2551
- const effectiveConfig = this.#effectiveDiscoveryProviderConfig(providerConfig);
2567
+ effectiveConfig = this.#effectiveDiscoveryProviderConfig(providerConfig);
2568
+ const effectiveAuthGeneration =
2569
+ authEvidence ?? this.#getProviderEvidenceGeneration(effectiveConfig.provider);
2570
+ const effectiveEndpoint = this.#normalizeDiscoveryEvidenceEndpoint(effectiveConfig.baseUrl ?? "");
2571
+ expectedAuthGeneration = effectiveAuthGeneration;
2572
+ expectedEndpoint = effectiveEndpoint;
2552
2573
  expectedProvenance = fingerprintConfiguredDiscoveryRequestShape(
2553
2574
  effectiveConfig,
2554
- authEvidence ?? this.#getProviderEvidenceGeneration(effectiveConfig.provider),
2555
- this.#normalizeDiscoveryEvidenceEndpoint(effectiveConfig.baseUrl ?? ""),
2575
+ effectiveAuthGeneration,
2576
+ effectiveEndpoint,
2556
2577
  );
2557
2578
  } catch {
2558
2579
  // A context that cannot be fully derived cannot vouch for a cache row.
@@ -2561,6 +2582,30 @@ export class ModelRegistry {
2561
2582
  const models = publishDiscoveryState
2562
2583
  ? this.#discoveryManager.loadCached(providerConfig, this.#cacheDbPath, expectedProvenance)
2563
2584
  : this.#readCachedDiscoverableModels(providerConfig, expectedProvenance);
2585
+ if (publishDiscoveryState) {
2586
+ const cache =
2587
+ effectiveConfig !== undefined
2588
+ ? readModelCache<Api>(effectiveConfig.provider, 24 * 60 * 60 * 1000, Date.now, this.#cacheDbPath)
2589
+ : null;
2590
+ if (
2591
+ effectiveConfig !== undefined &&
2592
+ expectedAuthGeneration !== undefined &&
2593
+ expectedEndpoint !== undefined &&
2594
+ expectedProvenance !== undefined &&
2595
+ cache?.fresh === true &&
2596
+ cache.authoritative === true &&
2597
+ cache.dynamicModelIds !== undefined &&
2598
+ cache.dynamicModelProvenance === expectedProvenance
2599
+ ) {
2600
+ this.#configuredDiscoveryEvidence.set(effectiveConfig.provider, {
2601
+ authGeneration: expectedAuthGeneration,
2602
+ endpoint: expectedEndpoint,
2603
+ modelIds: new Set(cache.dynamicModelIds),
2604
+ });
2605
+ } else {
2606
+ this.#configuredDiscoveryEvidence.delete(providerConfig.provider);
2607
+ }
2608
+ }
2564
2609
  // Cache rows persist sanitized transport metadata (no headers), so a
2565
2610
  // rebooted registry re-derives the provider transport override from the
2566
2611
  // same source the live publish path uses — mirroring the cached
@@ -3293,16 +3338,24 @@ export class ModelRegistry {
3293
3338
  const currentEndpoint = this.#normalizeDiscoveryEvidenceEndpoint(
3294
3339
  this.#effectiveDiscoveryProviderConfig(provider).baseUrl ?? "",
3295
3340
  );
3341
+ const authoritativeState =
3342
+ state !== undefined &&
3343
+ state.error === undefined &&
3344
+ !state.stale &&
3345
+ ((state.status === "ok" && discovery.fetched) ||
3346
+ state.status === "empty" ||
3347
+ (state.status === "ok" && !discovery.fetched));
3296
3348
  if (
3297
3349
  evidence !== undefined &&
3298
- (state?.status === "ok" || state?.status === "empty") &&
3299
- discovery.fetched &&
3350
+ authoritativeState &&
3300
3351
  currentAuthGeneration === evidence.authGeneration &&
3301
3352
  currentEndpoint === evidence.endpoint
3302
3353
  ) {
3303
3354
  this.#configuredDiscoveryEvidence.set(provider.provider, evidence);
3304
3355
  } else if (
3305
- (state?.status !== "cached" && !(state?.status === "ok" && !discovery.fetched)) ||
3356
+ (state?.status !== "cached" &&
3357
+ state?.status !== "empty" &&
3358
+ !(state?.status === "ok" && !discovery.fetched)) ||
3306
3359
  state.error !== undefined ||
3307
3360
  this.#configuredDiscoveryEvidence.get(provider.provider)?.authGeneration !== currentAuthGeneration ||
3308
3361
  this.#configuredDiscoveryEvidence.get(provider.provider)?.endpoint !== currentEndpoint
@@ -5425,6 +5478,10 @@ export class ModelRegistry {
5425
5478
  */
5426
5479
  getAvailableForProfileActivation(): Model<Api>[] {
5427
5480
  const bundledIdsByProvider = new Map<string, Set<string>>();
5481
+ // Evidence staleness depends only on the provider, and resolving it scans the
5482
+ // catalog for the provider base URL. Decide it once per provider per call so a
5483
+ // multi-thousand-model catalog is not rescanned for every model.
5484
+ const staleEvidenceByProvider = new Map<string, boolean>();
5428
5485
  return this.getAvailable().filter(model => {
5429
5486
  const evidence = this.#descriptorDiscoveryEvidence.get(model.provider);
5430
5487
  if (!evidence?.profileFresh || evidence.profileModelIds === undefined) return true;
@@ -5439,12 +5496,15 @@ export class ModelRegistry {
5439
5496
  bundledIdsByProvider.set(model.provider, bundledModelIds);
5440
5497
  }
5441
5498
  if (!bundledModelIds.has(model.id)) return true;
5442
- if (
5443
- evidence.authGeneration !== this.#getProviderEvidenceGeneration(model.provider) ||
5444
- evidence.profileEndpoint !==
5445
- this.#normalizeDiscoveryEvidenceEndpoint(this.#getProviderBaseUrlForDiscovery(model.provider) ?? "")
5446
- )
5447
- return true;
5499
+ let staleEvidence = staleEvidenceByProvider.get(model.provider);
5500
+ if (staleEvidence === undefined) {
5501
+ staleEvidence =
5502
+ evidence.authGeneration !== this.#getProviderEvidenceGeneration(model.provider) ||
5503
+ evidence.profileEndpoint !==
5504
+ this.#normalizeDiscoveryEvidenceEndpoint(this.#getProviderBaseUrlForDiscovery(model.provider) ?? "");
5505
+ staleEvidenceByProvider.set(model.provider, staleEvidence);
5506
+ }
5507
+ if (staleEvidence) return true;
5448
5508
  return evidence.profileModelIds.has(model.id);
5449
5509
  });
5450
5510
  }
@@ -87,6 +87,7 @@ export function resolveStartupModelRefreshSelectors(
87
87
  export async function refreshMissingQualifiedModelProviders(
88
88
  selectors: ModelSelectorValue | undefined,
89
89
  modelRegistry: Pick<ModelRegistry, "getAvailable" | "getRefreshableProviders" | "refreshProvider">,
90
+ credentialSessionId?: string,
90
91
  ): Promise<boolean> {
91
92
  const refreshedProviders = new Set<string>();
92
93
  for (const selector of normalizeModelSelectorValue(selectors)) {
@@ -104,7 +105,11 @@ export async function refreshMissingQualifiedModelProviders(
104
105
  )
105
106
  continue;
106
107
 
107
- await modelRegistry.refreshProvider(provider, "online-if-uncached");
108
+ if (credentialSessionId === undefined) {
109
+ await modelRegistry.refreshProvider(provider, "online-if-uncached");
110
+ } else {
111
+ await modelRegistry.refreshProvider(provider, "online-if-uncached", credentialSessionId);
112
+ }
108
113
  refreshedProviders.add(provider);
109
114
  }
110
115
  return refreshedProviders.size > 0;
@@ -1879,7 +1879,10 @@ export async function bindCreationRequest(
1879
1879
  )
1880
1880
  )
1881
1881
  throw new Error("session_closing");
1882
- if (request.canonical_create_intent && canonicalJson(request.canonical_create_intent) !== canonicalJson(intent))
1882
+ if (
1883
+ request.canonical_create_intent &&
1884
+ creationIntentDigest(request.canonical_create_intent) !== creationIntentDigest(intent)
1885
+ )
1883
1886
  throw new Error("idempotency_conflict");
1884
1887
  request.canonical_create_intent ??= intent;
1885
1888
  request.session_id = session.session_id;
@@ -10995,16 +10995,24 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
10995
10995
  }
10996
10996
  if (status !== "accepted")
10997
10997
  throw new SdkClientError("terminal_uncertain", "Workflow gate answer was not accepted.");
10998
- const resolvedAt =
10999
- typeof resolution?.resolved_at === "string" ? resolution.resolved_at : new Date().toISOString();
10998
+ const reportedResolvedAt =
10999
+ typeof resolution?.resolved_at === "string" &&
11000
+ Number.isFinite(Date.parse(resolution.resolved_at))
11001
+ ? resolution.resolved_at
11002
+ : undefined;
11003
+ // The gate timestamp is remote metadata; retention must age the local receipt from
11004
+ // the coordinator's own persistence time so a skewed or replayed response cannot
11005
+ // delete a fresh answer.
11006
+ const persistedAt = new Date().toISOString();
11007
+ const resolvedAt = reportedResolvedAt ?? persistedAt;
11000
11008
  await withAdmittedSessionTransaction(questionPaths, sessionId, async transaction => {
11001
11009
  const question = transaction.canonical.questions[questionId];
11002
11010
  if (!question || question.claim_fence_epoch !== (claimed as { fence: number }).fence)
11003
11011
  throw new Error("terminal_uncertain");
11004
11012
  question.status = "answered";
11005
- question.answered_at = resolvedAt;
11006
- question.updated_at = resolvedAt;
11007
- question.history.push({ at: resolvedAt, status: "answered", reason: null });
11013
+ question.answered_at = persistedAt;
11014
+ question.updated_at = persistedAt;
11015
+ question.history.push({ at: persistedAt, status: "answered", reason: null });
11008
11016
  const authority = transaction.canonical.gate_authorities[question.authority_id];
11009
11017
  if (authority)
11010
11018
  authority.outcome = { state: "answered", turn_id: turnId, question_id: questionId };
@@ -11022,7 +11030,7 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
11022
11030
  resolved_at: resolvedAt,
11023
11031
  };
11024
11032
  request.phase = "completed";
11025
- request.updated_at = resolvedAt;
11033
+ request.updated_at = persistedAt;
11026
11034
  });
11027
11035
  return {
11028
11036
  ok: true,
@@ -319,6 +319,8 @@ export interface BashExecutorOptions {
319
319
  sessionKey?: string;
320
320
  /** Additional environment variables to inject */
321
321
  env?: Record<string, string>;
322
+ /** Environment names to hide from this command's inherited environment. */
323
+ unsetEnv?: string[];
322
324
  /** Artifact path/id for full output storage */
323
325
  artifactPath?: string;
324
326
  artifactId?: string;
@@ -578,6 +580,7 @@ export async function executeBash(command: string, options?: BashExecutorOptions
578
580
  command: finalCommand,
579
581
  cwd: commandCwd,
580
582
  env: commandEnv,
583
+ unsetEnv: options?.unsetEnv,
581
584
  timeoutMs: executionTimeoutMs,
582
585
  signal: runAbortController.signal,
583
586
  };