@gajae-code/coding-agent 0.12.21 → 0.13.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 (222) hide show
  1. package/CHANGELOG.md +67 -2
  2. package/dist/types/cli/args.d.ts +5 -2
  3. package/dist/types/cli/root-flags.d.ts +246 -0
  4. package/dist/types/cli.d.ts +11 -19
  5. package/dist/types/commands/daemon.d.ts +0 -30
  6. package/dist/types/commands/launch.d.ts +11 -19
  7. package/dist/types/commands/quick-lane.d.ts +32 -0
  8. package/dist/types/commands/sdk.d.ts +21 -15
  9. package/dist/types/config/file-lock-gc.d.ts +2 -0
  10. package/dist/types/config/model-equivalence.d.ts +20 -0
  11. package/dist/types/config/model-profile-activation.d.ts +62 -17
  12. package/dist/types/config/model-profiles.d.ts +10 -0
  13. package/dist/types/config/model-registry.d.ts +68 -1
  14. package/dist/types/config/model-resolver.d.ts +15 -4
  15. package/dist/types/config/models-config-schema.d.ts +7 -2
  16. package/dist/types/config/provider-selection-policy.d.ts +62 -0
  17. package/dist/types/config/settings-schema.d.ts +36 -78
  18. package/dist/types/config/settings.d.ts +2 -0
  19. package/dist/types/edit/renderer.d.ts +5 -2
  20. package/dist/types/edit/streaming.d.ts +13 -0
  21. package/dist/types/extensibility/custom-tools/types.d.ts +2 -0
  22. package/dist/types/extensibility/extensions/compact-handler.d.ts +2 -1
  23. package/dist/types/extensibility/extensions/types.d.ts +3 -0
  24. package/dist/types/gjc-runtime/gc-runtime.d.ts +19 -5
  25. package/dist/types/internal-urls/local-protocol.d.ts +1 -0
  26. package/dist/types/modes/components/model-selector.d.ts +0 -2
  27. package/dist/types/modes/components/provider-order-context.d.ts +45 -0
  28. package/dist/types/modes/components/provider-order-editor.d.ts +24 -0
  29. package/dist/types/modes/components/settings-defs.d.ts +5 -1
  30. package/dist/types/modes/components/settings-selector.d.ts +2 -0
  31. package/dist/types/quick-lane/classify.d.ts +40 -0
  32. package/dist/types/runtime/stt-settings-setup.d.ts +18 -0
  33. package/dist/types/sdk/broker/broker.d.ts +56 -2
  34. package/dist/types/sdk/broker/operation-receipt-ledger.d.ts +25 -0
  35. package/dist/types/sdk/broker/session-index.d.ts +83 -2
  36. package/dist/types/sdk/broker/state-version.d.ts +1 -1
  37. package/dist/types/sdk/bus/notification-service.d.ts +6 -0
  38. package/dist/types/sdk/bus/prompt-reconciliation.d.ts +8 -1
  39. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +4 -1
  40. package/dist/types/sdk/bus/telegram-daemon.d.ts +26 -0
  41. package/dist/types/sdk/cli/rows.d.ts +98 -0
  42. package/dist/types/sdk/cli/session-cli.d.ts +35 -3
  43. package/dist/types/sdk/elevation/allowlist.d.ts +20 -0
  44. package/dist/types/sdk/elevation/audit.d.ts +93 -0
  45. package/dist/types/sdk/elevation/capability.d.ts +3 -0
  46. package/dist/types/sdk/elevation/digest.d.ts +72 -0
  47. package/dist/types/sdk/elevation/dispatch-receipt.d.ts +85 -0
  48. package/dist/types/sdk/elevation/grant-ledger.d.ts +163 -0
  49. package/dist/types/sdk/elevation/owner.d.ts +48 -0
  50. package/dist/types/sdk/guides/cache.d.ts +98 -0
  51. package/dist/types/sdk/guides/catalog.d.ts +156 -0
  52. package/dist/types/sdk/guides/cli.d.ts +39 -0
  53. package/dist/types/sdk/guides/manifest.d.ts +66 -0
  54. package/dist/types/sdk/guides/verify.d.ts +66 -0
  55. package/dist/types/sdk/host/control/dispatch.d.ts +8 -0
  56. package/dist/types/sdk/host/control/index.d.ts +1 -1
  57. package/dist/types/sdk/host/control/operations.d.ts +3 -2
  58. package/dist/types/sdk/host/query/cursor.d.ts +10 -0
  59. package/dist/types/sdk/host/query/handlers.d.ts +21 -0
  60. package/dist/types/sdk/host/session-runtime.d.ts +9 -0
  61. package/dist/types/sdk/mcp/server.d.ts +2 -2
  62. package/dist/types/sdk/prompt-status.d.ts +11 -0
  63. package/dist/types/sdk/session.d.ts +4 -0
  64. package/dist/types/sdk/transport/index.d.ts +1 -0
  65. package/dist/types/sdk/transport/relay.d.ts +1 -0
  66. package/dist/types/sdk/transport/serve-cli.d.ts +20 -1
  67. package/dist/types/session/agent-session.d.ts +32 -18
  68. package/dist/types/session/blob-store.d.ts +13 -7
  69. package/dist/types/session/fallback-chain-controller.d.ts +15 -0
  70. package/dist/types/session/internal/managed-session-storage.d.ts +25 -1
  71. package/dist/types/session/internal/session-memory-sidecar.d.ts +5 -5
  72. package/dist/types/session/session-manager.d.ts +139 -10
  73. package/dist/types/session/session-storage.d.ts +48 -0
  74. package/dist/types/setup/provider-onboarding.d.ts +7 -1
  75. package/dist/types/slash-commands/builtin-registry.d.ts +14 -0
  76. package/dist/types/stt/setup.d.ts +2 -0
  77. package/dist/types/task/executor.d.ts +10 -2
  78. package/dist/types/tools/browser/runtime-diagnostics.d.ts +65 -0
  79. package/dist/types/tools/browser/tab-protocol.d.ts +4 -0
  80. package/dist/types/tools/browser/tab-supervisor.d.ts +2 -0
  81. package/dist/types/tools/browser/tab-worker.d.ts +2 -0
  82. package/dist/types/tools/browser.d.ts +2 -0
  83. package/dist/types/tools/image-gen.d.ts +14 -18
  84. package/dist/types/tools/index.d.ts +2 -0
  85. package/dist/types/tools/resource-gc.d.ts +2 -0
  86. package/dist/types/utils/pasted-image-path.d.ts +6 -0
  87. package/package.json +9 -9
  88. package/scripts/generate-sdk-operation-inventory.ts +8 -0
  89. package/scripts/run-test-manifest.ts +61 -21
  90. package/scripts/verify-gjc-sdk-canonicalization.ts +9 -4
  91. package/src/cli/args.ts +62 -9
  92. package/src/cli/completion-cli.ts +1 -0
  93. package/src/cli/fast-help.ts +1 -0
  94. package/src/cli/root-flags.ts +100 -0
  95. package/src/cli/setup-cli.ts +6 -2
  96. package/src/cli.ts +42 -65
  97. package/src/commands/acp.ts +1 -1
  98. package/src/commands/daemon.ts +2 -31
  99. package/src/commands/gc.ts +1 -1
  100. package/src/commands/launch.ts +8 -129
  101. package/src/commands/quick-lane.ts +51 -0
  102. package/src/commands/sdk.ts +200 -26
  103. package/src/commands/setup.ts +4 -2
  104. package/src/config/file-lock-gc.ts +4 -1
  105. package/src/config/model-equivalence.ts +76 -7
  106. package/src/config/model-profile-activation.ts +1115 -265
  107. package/src/config/model-profiles.ts +134 -2
  108. package/src/config/model-registry.ts +352 -50
  109. package/src/config/model-resolver.ts +289 -38
  110. package/src/config/models-config-schema.ts +11 -4
  111. package/src/config/provider-selection-policy.ts +146 -0
  112. package/src/config/settings-schema.ts +50 -77
  113. package/src/config/settings.ts +40 -0
  114. package/src/coordinator-mcp/server.ts +32 -2
  115. package/src/edit/renderer.ts +175 -223
  116. package/src/edit/streaming.ts +108 -1
  117. package/src/export/html/index.ts +57 -7
  118. package/src/extensibility/custom-tools/types.ts +2 -0
  119. package/src/extensibility/extensions/compact-handler.ts +3 -2
  120. package/src/extensibility/extensions/runner.ts +6 -0
  121. package/src/extensibility/extensions/types.ts +3 -0
  122. package/src/gjc-runtime/gc-runtime.ts +379 -20
  123. package/src/gjc-runtime/launch-worktree.ts +5 -1
  124. package/src/gjc-runtime/team-runtime.ts +59 -2
  125. package/src/gjc-runtime/tmux-owner-isolation.ts +19 -0
  126. package/src/gjc-runtime/ultragoal-receipt-freshness.ts +16 -1
  127. package/src/internal-urls/docs-index.generated.ts +17 -13
  128. package/src/internal-urls/local-protocol.ts +1 -0
  129. package/src/main.ts +40 -3
  130. package/src/memories/index.ts +4 -2
  131. package/src/modes/acp/acp-agent.ts +235 -91
  132. package/src/modes/components/model-selector.ts +206 -42
  133. package/src/modes/components/provider-order-context.ts +197 -0
  134. package/src/modes/components/provider-order-editor.ts +441 -0
  135. package/src/modes/components/settings-defs.ts +21 -1
  136. package/src/modes/components/settings-selector.ts +45 -2
  137. package/src/modes/components/tool-execution.ts +47 -15
  138. package/src/modes/controllers/command-controller.ts +6 -3
  139. package/src/modes/controllers/extension-ui-controller.ts +2 -0
  140. package/src/modes/controllers/input-controller.ts +73 -2
  141. package/src/modes/controllers/selector-controller.ts +260 -153
  142. package/src/modes/prompt-suggestion-controller.ts +1 -1
  143. package/src/modes/runtime-init.ts +1 -0
  144. package/src/modes/utils/ui-helpers.ts +18 -0
  145. package/src/prompts/tools/browser.md +1 -0
  146. package/src/quick-lane/classify.ts +257 -0
  147. package/src/rlm/index.ts +1 -1
  148. package/src/runtime/stt-settings-setup.ts +57 -0
  149. package/src/sdk/acp/adapter.ts +2 -1
  150. package/src/sdk/broker/broker.ts +780 -12
  151. package/src/sdk/broker/lifecycle.ts +70 -29
  152. package/src/sdk/broker/operation-receipt-ledger.ts +144 -0
  153. package/src/sdk/broker/session-index.ts +463 -48
  154. package/src/sdk/broker/state-version.ts +13 -9
  155. package/src/sdk/broker/transport.ts +14 -1
  156. package/src/sdk/bus/index.ts +34 -13
  157. package/src/sdk/bus/kind-aware-reconciliation.ts +21 -2
  158. package/src/sdk/bus/notification-service.ts +47 -1
  159. package/src/sdk/bus/prompt-reconciliation.ts +22 -3
  160. package/src/sdk/bus/telegram-daemon-contract.ts +4 -1
  161. package/src/sdk/bus/telegram-daemon.ts +54 -3
  162. package/src/sdk/cli/rows.ts +247 -0
  163. package/src/sdk/cli/session-cli.ts +930 -79
  164. package/src/sdk/elevation/allowlist.ts +54 -0
  165. package/src/sdk/elevation/audit.ts +208 -0
  166. package/src/sdk/elevation/capability.ts +41 -0
  167. package/src/sdk/elevation/digest.ts +129 -0
  168. package/src/sdk/elevation/dispatch-receipt.ts +184 -0
  169. package/src/sdk/elevation/grant-ledger.ts +1142 -0
  170. package/src/sdk/elevation/owner.ts +109 -0
  171. package/src/sdk/guides/cache.ts +363 -0
  172. package/src/sdk/guides/catalog.ts +554 -0
  173. package/src/sdk/guides/cli.ts +235 -0
  174. package/src/sdk/guides/manifest.ts +152 -0
  175. package/src/sdk/guides/verify.ts +178 -0
  176. package/src/sdk/host/control/dispatch.ts +49 -1
  177. package/src/sdk/host/control/index.ts +1 -0
  178. package/src/sdk/host/control/operations.ts +8 -1
  179. package/src/sdk/host/query/cursor.ts +47 -3
  180. package/src/sdk/host/query/handlers.ts +155 -2
  181. package/src/sdk/host/sdk-surface-parity.test.ts +1 -0
  182. package/src/sdk/host/session-runtime.test.ts +391 -1
  183. package/src/sdk/host/session-runtime.ts +308 -73
  184. package/src/sdk/host/surface-policy.ts +3 -0
  185. package/src/sdk/host/websocket-transport.ts +7 -1
  186. package/src/sdk/mcp/server.ts +88 -15
  187. package/src/sdk/prompt-status.ts +8 -0
  188. package/src/sdk/protocol/operation-inventory.generated.json +62 -0
  189. package/src/sdk/protocol/operation-registry.ts +3 -2
  190. package/src/sdk/session.ts +87 -39
  191. package/src/sdk/transport/index.ts +1 -0
  192. package/src/sdk/transport/relay.ts +3 -0
  193. package/src/sdk/transport/serve-cli.ts +111 -24
  194. package/src/secrets/obfuscator.ts +92 -12
  195. package/src/session/agent-session.ts +322 -93
  196. package/src/session/blob-store.ts +60 -8
  197. package/src/session/fallback-chain-controller.ts +35 -0
  198. package/src/session/internal/managed-session-scope.ts +25 -3
  199. package/src/session/internal/managed-session-storage.ts +574 -73
  200. package/src/session/internal/session-memory-sidecar.ts +19 -29
  201. package/src/session/session-manager.ts +2166 -743
  202. package/src/session/session-storage.ts +715 -48
  203. package/src/setup/provider-onboarding.ts +19 -3
  204. package/src/setup/provider-presets.json +3 -1
  205. package/src/slash-commands/builtin-registry.ts +85 -3
  206. package/src/stt/downloader.ts +2 -3
  207. package/src/stt/setup.ts +29 -4
  208. package/src/task/executor.ts +22 -18
  209. package/src/task/index.ts +24 -1
  210. package/src/tools/archive-reader.ts +11 -2
  211. package/src/tools/browser/runtime-diagnostics.ts +249 -0
  212. package/src/tools/browser/tab-protocol.ts +4 -0
  213. package/src/tools/browser/tab-supervisor.ts +5 -0
  214. package/src/tools/browser/tab-worker.ts +36 -1
  215. package/src/tools/browser.ts +7 -0
  216. package/src/tools/image-gen.ts +110 -166
  217. package/src/tools/index.ts +2 -0
  218. package/src/tools/read.ts +1 -1
  219. package/src/tools/resource-gc.ts +6 -1
  220. package/src/tools/tool-catalog.generated.ts +5 -1
  221. package/src/utils/pasted-image-path.ts +93 -0
  222. package/src/web/search/index.ts +3 -3
@@ -8,11 +8,14 @@ import type { Api, Model } from "@gajae-code/ai/core";
8
8
  import { logger } from "@gajae-code/utils";
9
9
  import { isModelProfileProviderAvailable, projectModelProfileCatalog } from "../../config/model-profile-contract";
10
10
  import { type ModelProfileDefinition, resolveProfileBindings } from "../../config/model-profiles";
11
- import { resolveModelChainWithAuth } from "../../config/model-resolver";
12
- import { normalizeModelSelectorValue } from "../../config/model-selector-value";
11
+ import { resolveModelChainWithAuth, splitSelectorThinkingSuffix } from "../../config/model-resolver";
12
+ import { type ModelSelectorValue, normalizeModelSelectorValue } from "../../config/model-selector-value";
13
13
  import { type Settings, validateSettingPatch } from "../../config/settings";
14
14
  import type { ExtensionAPI, ExtensionCommandContext, ExtensionContext } from "../../extensibility/extensions";
15
15
  import { parseThinkingLevel } from "../../thinking";
16
+ import { ensureBroker } from "../broker/ensure";
17
+ import { SessionIndex } from "../broker/session-index";
18
+ import { elevationAuthorityPath, verifyElevationCapability } from "../elevation/capability";
16
19
  import {
17
20
  collectAuthenticatedProfileProviders,
18
21
  parseSyntheticModelId,
@@ -24,9 +27,9 @@ import {
24
27
  import { projectQ10Models } from "../models.js";
25
28
  import { formatPromptFailureForLocalLog, sanitizePromptFailure } from "../prompt-failure";
26
29
  import { OPERATIONS } from "../protocol/operation-registry";
27
- import { type ControlSurface, dispatchControl } from "./control";
30
+ import { type ControlSurface, controlRequestFromFrame, dispatchControl } from "./control";
28
31
  import { SessionSdkHost, type SessionSdkHostOptions } from "./host";
29
- import { CursorRegistry, QueryHandlers, RevisionStore, type SessionSurface } from "./query";
32
+ import { CursorRegistry, QueryHandlers, RevisionStore, type SdkCheckpointRecord, type SessionSurface } from "./query";
30
33
  import {
31
34
  createSdkCapabilities,
32
35
  createSdkSurfacePolicyForContext,
@@ -217,6 +220,10 @@ export class SessionSdkSessionRuntime {
217
220
 
218
221
  /** Narrow extension-facing factory for the SDK-only session path. */
219
222
  export interface CreateSdkSessionRuntimeOptions {
223
+ /** Authoritative broker state root for this session's endpoint lifecycle. */
224
+ agentDir: string;
225
+ /** Lifecycle-owned sessions require broker publication before they become usable. */
226
+ brokerRegistrationRequired?: boolean;
220
227
  createTransport(input: {
221
228
  sessionId: string;
222
229
  stateRoot: string;
@@ -411,7 +418,24 @@ export function createInvocationReconciliation(
411
418
  async noteTransition(kind, correlation, frame) {
412
419
  if (!correlation) return;
413
420
  const record = records.get(key(kind, correlation));
414
- if (!record || record.terminalAt !== undefined) return;
421
+ if (!record) return;
422
+ if (record.terminalAt !== undefined) {
423
+ // Same late agent_failed enrichment as the kind-aware bus reconciler: a
424
+ // failure reason may arrive on a different delivery path than the one that
425
+ // claimed the terminal. Enrich the settled record instead of dropping it;
426
+ // never resurrect (status/terminalAt untouched), and first reason wins.
427
+ if (frame.type === "agent_failed" && record.error === undefined) {
428
+ logger.error("SDK invocation failed (late)", {
429
+ kind,
430
+ commandId: correlation.commandId,
431
+ turnId: correlation.turnId,
432
+ error: formatPromptFailureForLocalLog(frame.error),
433
+ });
434
+ record.error = sanitizePromptFailure(frame.error);
435
+ await persist();
436
+ }
437
+ return;
438
+ }
415
439
  if (frame.type === "agent_start") {
416
440
  record.status = "in_flight";
417
441
  record.startedAt = Date.now();
@@ -466,6 +490,8 @@ export interface SdkSurfaceFactoryOptions {
466
490
  promptStatusLookup?: (selector: { commandId?: string; turnId?: string; clientRef?: string }) => unknown;
467
491
  skillStatusLookup?: (selector: { commandId?: string; turnId?: string; clientRef?: string }) => unknown;
468
492
  hostTools?: boolean | (() => boolean);
493
+ /** Q30 event-ring watermark source (the live session event stream); synchronous. */
494
+ getEventWatermark?: () => { generation: number; seq: number };
469
495
  }
470
496
 
471
497
  /** Shared policy, capability, and query-surface factory for every SDK transport. */
@@ -490,6 +516,8 @@ function createQuerySurface(
490
516
  promptStatusLookup?: (selector: { commandId?: string; turnId?: string; clientRef?: string }) => unknown;
491
517
  skillStatusLookup?: (selector: { commandId?: string; turnId?: string; clientRef?: string }) => unknown;
492
518
  hostTools?: boolean | (() => boolean);
519
+ /** Q30 event-ring watermark source (the live session event stream); synchronous. */
520
+ getEventWatermark?: () => { generation: number; seq: number };
493
521
  } = {},
494
522
  ): SessionSurface {
495
523
  const policy =
@@ -528,6 +556,63 @@ function createQuerySurface(
528
556
  }
529
557
  return undefined;
530
558
  };
559
+ const getProfileCredentialSessionId = () => ctx.credentialSessionId ?? id;
560
+ const resolveProfileAvailability = async (
561
+ profile: ModelProfileDefinition,
562
+ authenticatedProviders: ReadonlySet<string>,
563
+ ): Promise<{ available: boolean; defaultModel?: Model<Api> }> => {
564
+ const rewriteSelectorProvider = (selector: string): string => {
565
+ const slash = selector.indexOf("/");
566
+ if (slash < 0) return selector;
567
+ const provider = selector.slice(0, slash);
568
+ if (authenticatedProviders.has(provider)) return selector;
569
+ const group = (profile.alternativeProviderGroups ?? []).find(candidates => candidates.includes(provider));
570
+ if (!group) return selector;
571
+ const replacement = group.find(candidate => authenticatedProviders.has(candidate));
572
+ return replacement ? replacement + selector.slice(slash) : selector;
573
+ };
574
+ try {
575
+ const bindings = resolveProfileBindings(profile);
576
+ const assignments: Array<{ value: ModelSelectorValue; isDefault: boolean }> = [];
577
+ if (bindings.defaultSelector !== undefined) {
578
+ assignments.push({ value: bindings.defaultSelector, isDefault: true });
579
+ }
580
+ for (const value of Object.values(bindings.modelRoles)) assignments.push({ value, isDefault: false });
581
+ for (const value of Object.values(bindings.agentModelOverrides)) assignments.push({ value, isDefault: false });
582
+ let defaultModel: Model<Api> | undefined;
583
+ for (const assignment of assignments) {
584
+ const selectors = normalizeModelSelectorValue(assignment.value).map(rewriteSelectorProvider);
585
+ const hasBareSelector = selectors.some(selector => {
586
+ const suffix = splitSelectorThinkingSuffix(selector);
587
+ const identity = suffix.thinkingLevel ? suffix.selector : selector;
588
+ return !identity.includes("/");
589
+ });
590
+ if (!assignment.isDefault && !hasBareSelector) continue;
591
+ const resolution = await resolveModelChainWithAuth(
592
+ selectors,
593
+ {
594
+ ...ctx.modelRegistry,
595
+ getAvailable: () => ctx.modelRegistry.getAvailable(),
596
+ getApiKey: (model: Model<Api>, sessionId?: string) =>
597
+ ctx.modelRegistry.getApiKeyForProvider(model.provider, sessionId, model.baseUrl),
598
+ },
599
+ options.settings,
600
+ getProfileCredentialSessionId(),
601
+ {
602
+ managedFallback: true,
603
+ aliasIntent: "preset-equivalent",
604
+ canonicalSessionId: null,
605
+ credentialSessionId: getProfileCredentialSessionId(),
606
+ },
607
+ );
608
+ if (!resolution.model) return { available: false };
609
+ if (assignment.isDefault) defaultModel = resolution.model;
610
+ }
611
+ return { available: true, defaultModel };
612
+ } catch {
613
+ return { available: false };
614
+ }
615
+ };
531
616
  const getDiff = async () => {
532
617
  try {
533
618
  const { stdout } = await execFileAsync("git", ["diff", "--no-ext-diff"], {
@@ -620,61 +705,27 @@ function createQuerySurface(
620
705
  let authenticatedProviders: ReadonlySet<string>;
621
706
  try {
622
707
  authenticatedProviders = await collectAuthenticatedProfileProviders(profiles, provider =>
623
- ctx.modelRegistry.getApiKeyForProvider(provider, id),
708
+ ctx.modelRegistry.getApiKeyForProvider(provider, getProfileCredentialSessionId()),
624
709
  );
625
710
  } catch {
626
711
  // Availability join failed: degrade only the synthetic facade,
627
712
  // retain concrete rows and the active marker readback.
628
713
  return degraded();
629
714
  }
630
- // Resolve each profile's default model exactly like profile activation:
631
- // walk the default mapping chain, rewrite alternative-group providers
632
- // to their authenticated member, and use the same pattern-aware,
633
- // managed-fallback-eligible resolver so Q10 reports the limits of the
634
- // model the profile will actually activate (glob defaults such as
635
- // `provider/gpt-*` and Cursor-managed-fallback skips included).
636
715
  const resolvedDefaultModels = new Map<string, Model<Api>>();
637
- const rewriteSelectorProvider = (selector: string, profile: ModelProfileDefinition): string => {
638
- const slash = selector.indexOf("/");
639
- if (slash < 0) return selector;
640
- const provider = selector.slice(0, slash);
641
- if (authenticatedProviders.has(provider)) return selector;
642
- const group = (profile.alternativeProviderGroups ?? []).find(candidates => candidates.includes(provider));
643
- if (!group) return selector;
644
- const replacement = group.find(candidate => authenticatedProviders.has(candidate));
645
- return replacement ? replacement + selector.slice(slash) : selector;
646
- };
716
+ const fullyResolvedProfiles = new Set<string>();
647
717
  await Promise.all(
648
718
  [...profiles.entries()].map(async ([name, profile]) => {
649
- try {
650
- const defaultSelector = resolveProfileBindings(profile).defaultSelector;
651
- if (defaultSelector === undefined) return; // role-only profile
652
- const selectors = normalizeModelSelectorValue(defaultSelector).map(selector =>
653
- rewriteSelectorProvider(selector, profile),
654
- );
655
- const resolution = await resolveModelChainWithAuth(
656
- selectors,
657
- {
658
- ...ctx.modelRegistry,
659
- getAvailable: () => ctx.modelRegistry.getAll(),
660
- getApiKey: (model: Model<Api>, sessionId?: string) =>
661
- ctx.modelRegistry.getApiKeyForProvider(model.provider, sessionId, model.baseUrl),
662
- },
663
- options.settings,
664
- id,
665
- { managedFallback: true },
666
- );
667
- if (resolution.model) resolvedDefaultModels.set(name, resolution.model);
668
- } catch {
669
- // A provider whose credential state cannot be read must not
670
- // fail the whole Q10 query: skip this profile's metadata
671
- // resolution and degrade only its synthetic row.
672
- }
719
+ const result = await resolveProfileAvailability(profile, authenticatedProviders);
720
+ if (!result.available) return;
721
+ fullyResolvedProfiles.add(name);
722
+ if (result.defaultModel) resolvedDefaultModels.set(name, result.defaultModel);
673
723
  }),
674
724
  );
675
725
  const availableProfileIds = new Set<string>();
676
726
  for (const [name, profile] of profiles) {
677
727
  if (!isModelProfileProviderAvailable(profile, authenticatedProviders)) continue;
728
+ if (!fullyResolvedProfiles.has(name)) continue;
678
729
  // A profile with a default mapping is selectable only when its
679
730
  // default chain actually resolves to an authenticated model:
680
731
  // activation rejects unresolvable defaults even when the
@@ -737,12 +788,35 @@ function createQuerySurface(
737
788
  getModelProfiles: async () => {
738
789
  const profiles = ctx.modelRegistry.getModelProfiles();
739
790
  const authenticatedProviders = await collectAuthenticatedProfileProviders(profiles, provider =>
740
- ctx.modelRegistry.getApiKeyForProvider(provider, id),
791
+ ctx.modelRegistry.getApiKeyForProvider(provider, getProfileCredentialSessionId()),
741
792
  );
742
- return projectModelProfileCatalog(profiles, ctx.modelRegistry.getError()).map(item => ({
743
- ...item,
744
- available: isModelProfileProviderAvailable(profiles.get(item.id)!, authenticatedProviders),
745
- })) as unknown[];
793
+ return (await Promise.all(
794
+ projectModelProfileCatalog(profiles, ctx.modelRegistry.getError()).map(async item => ({
795
+ ...item,
796
+ available:
797
+ isModelProfileProviderAvailable(profiles.get(item.id)!, authenticatedProviders) &&
798
+ (
799
+ await resolveProfileAvailability(profiles.get(item.id)!, authenticatedProviders)
800
+ ).available,
801
+ })),
802
+ )) as unknown[];
803
+ },
804
+ getCheckpointSnapshot: (): { entries: unknown[]; watermark: SdkCheckpointRecord } => {
805
+ // Q30 atomic checkpoint capture (C9): the transcript entries and the
806
+ // event-ring watermark are read in one synchronous call from the host
807
+ // event stream, so the snapshot revision and the subscribe position
808
+ // can never straddle a concurrent append.
809
+ const entries =
810
+ typeof (ctx as Partial<ExtensionContext>).getTranscript === "function" ? ctx.getTranscript() : [];
811
+ const watermark = options.getEventWatermark?.() ?? { generation: 0, seq: 0 };
812
+ return {
813
+ entries,
814
+ watermark: {
815
+ revision: Array.isArray(entries) ? entries.length : 0,
816
+ generation: watermark.generation,
817
+ seq: watermark.seq,
818
+ },
819
+ };
746
820
  },
747
821
  installedQueries: policy.installedQueries,
748
822
  };
@@ -774,6 +848,7 @@ export function createSdkSurfaceFactory(
774
848
  promptStatusLookup: options.promptStatusLookup,
775
849
  skillStatusLookup: options.skillStatusLookup,
776
850
  hostTools: options.hostTools,
851
+ getEventWatermark: options.getEventWatermark,
777
852
  });
778
853
  return {
779
854
  policy,
@@ -837,6 +912,52 @@ function containsSecretConfigKey(value: unknown, seen = new Set<object>()): bool
837
912
  containsSecretConfigKey(nested, seen),
838
913
  );
839
914
  }
915
+ async function resolveSdkWorkflowGate(
916
+ ctx: ExtensionContext,
917
+ operation: "workflow.gate_answer" | "workflow.plan_approve",
918
+ id: string,
919
+ answer: unknown,
920
+ expectedSessionId: string | undefined,
921
+ idempotencyKey: string,
922
+ canResolve: () => boolean,
923
+ ): Promise<unknown> {
924
+ if (!canResolve())
925
+ throw Object.assign(new Error("Workflow gate is no longer answerable."), { code: "resource_gone" });
926
+ if (expectedSessionId !== undefined && expectedSessionId !== ctx.sessionManager.getSessionId())
927
+ throw Object.assign(new Error("Workflow gate session does not match this endpoint."), { code: "resource_gone" });
928
+ if (expectedSessionId === undefined) logger.warn("workflow_control_missing_expected_session_id", { operation });
929
+ const workflowGate = ctx.workflowGate;
930
+ if (
931
+ typeof workflowGate?.resolveGate !== "function" ||
932
+ typeof workflowGate.recoverAcceptedGates !== "function" ||
933
+ typeof workflowGate.lookupCompletedResolution !== "function" ||
934
+ typeof workflowGate.prepareTerminalization !== "function" ||
935
+ typeof workflowGate.clearPreparedTerminalization !== "function"
936
+ )
937
+ throw Object.assign(new Error("Workflow gates are unavailable for this session."), { code: "resource_gone" });
938
+ const response = { gate_id: id, answer, idempotency_key: idempotencyKey };
939
+ const completed = workflowGate.lookupCompletedResolution(response);
940
+ if (completed.kind === "completed") return completed.resolution;
941
+ if (completed.kind === "accepted_incomplete") {
942
+ await workflowGate.recoverAcceptedGates();
943
+ const recovered = workflowGate.lookupCompletedResolution(response);
944
+ if (recovered.kind === "completed") return recovered.resolution;
945
+ throw Object.assign(new Error("Workflow gate resolution outcome is uncertain."), { code: "terminal_uncertain" });
946
+ }
947
+ if (!workflowGate.prepareTerminalization(id, "not_published"))
948
+ throw Object.assign(new Error("Workflow gate is no longer answerable."), { code: "resource_gone" });
949
+ try {
950
+ const resolution = await workflowGate.resolveGate(response);
951
+ if ((resolution as { status?: unknown }).status === "rejected") workflowGate.clearPreparedTerminalization(id);
952
+ return resolution;
953
+ } catch (error) {
954
+ const stillPending = workflowGate.listPendingGates?.().some(gate => gate.gate_id === id) === true;
955
+ if (stillPending) workflowGate.clearPreparedTerminalization(id);
956
+ else workflowGate.quarantineGate?.(id);
957
+ throw error;
958
+ }
959
+ }
960
+
840
961
  function createControlSurface(
841
962
  ctx: ExtensionContext,
842
963
  api: ExtensionAPI,
@@ -846,6 +967,9 @@ function createControlSurface(
846
967
  settings?: Settings,
847
968
  configOverrides?: Map<string, unknown>,
848
969
  configRevision: { current: number } = { current: 0 },
970
+ elevationAuthorityToken?: string,
971
+ canResolveGate: () => boolean = () => true,
972
+ trackGateResolution: <T>(resolution: Promise<T>) => Promise<T> = async resolution => await resolution,
849
973
  ): ControlSurface {
850
974
  const surfacePolicy =
851
975
  policy ?? createSdkSurfacePolicyForContext(ctx, hasSdkWorkflowGateCapability(ctx.workflowGate));
@@ -942,7 +1066,7 @@ function createControlSurface(
942
1066
  try {
943
1067
  const submission = Promise.resolve(
944
1068
  run({
945
- onPreflightAccepted: () => void accept().catch(() => undefined),
1069
+ onPreflightAccepted: () => void accept().catch(error => preflight.reject(error)),
946
1070
  onPreflightAcceptCommit: accept,
947
1071
  }),
948
1072
  );
@@ -953,7 +1077,7 @@ function createControlSurface(
953
1077
  return;
954
1078
  }
955
1079
  if (allowCompletionFallback) {
956
- void accept().catch(() => undefined);
1080
+ void accept().catch(error => preflight.reject(error));
957
1081
  return;
958
1082
  }
959
1083
  settled = true;
@@ -965,8 +1089,11 @@ function createControlSurface(
965
1089
  },
966
1090
  error => {
967
1091
  if (settled) {
968
- if (kind === "skill")
969
- void reconciliation.noteTransition(kind, correlation, { type: "agent_failed", error });
1092
+ // The submission promise rejects after preflight acceptance only when the
1093
+ // work itself is over (provider stream interrupt, abort, queue failure).
1094
+ // Every kind must terminalize here or the record stays non-terminal
1095
+ // forever; `noteTransition` ignores an already-terminal record.
1096
+ void reconciliation.noteTransition(kind, correlation, { type: "agent_failed", error });
970
1097
  return;
971
1098
  }
972
1099
  settled = true;
@@ -986,6 +1113,9 @@ function createControlSurface(
986
1113
  }
987
1114
  };
988
1115
  return {
1116
+ authorizeElevationClaim: (sdkId, input, capability) =>
1117
+ elevationAuthorityToken !== undefined &&
1118
+ verifyElevationCapability(elevationAuthorityToken, capability, sdkId, input),
989
1119
  prompt: async (text, images, clientRef) =>
990
1120
  submit("prompt", clientRef, options =>
991
1121
  api.sendUserMessage(
@@ -1008,8 +1138,22 @@ function createControlSurface(
1008
1138
  return await submit("prompt", undefined, options => api.sendUserMessage(text, options));
1009
1139
  },
1010
1140
  answerAsk: unavailable("ask.answer"),
1011
- answerGate: unavailable("workflow.gate_answer"),
1012
- approvePlan: unavailable("workflow.plan_approve"),
1141
+ answerGate: async (id, response, expectedSessionId, idempotencyKey) =>
1142
+ await trackGateResolution(
1143
+ resolveSdkWorkflowGate(
1144
+ ctx,
1145
+ "workflow.gate_answer",
1146
+ id,
1147
+ response,
1148
+ expectedSessionId,
1149
+ idempotencyKey ?? id,
1150
+ canResolveGate,
1151
+ ),
1152
+ ),
1153
+ approvePlan: async (id, choice, expectedSessionId) =>
1154
+ await trackGateResolution(
1155
+ resolveSdkWorkflowGate(ctx, "workflow.plan_approve", id, choice, expectedSessionId, id, canResolveGate),
1156
+ ),
1013
1157
  invokeSkill: async (name, args, clientRef) => {
1014
1158
  if (!ctx.invokeSkill) return unavailable("skill.invoke")();
1015
1159
  if (args !== undefined && typeof args !== "string")
@@ -1191,6 +1335,9 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
1191
1335
  cursors: CursorRegistry;
1192
1336
  reconciliation: InvocationReconciliation;
1193
1337
  pending: Array<{ kind: InvocationKind; correlation: InvocationCorrelation }>;
1338
+ registerBroker: () => Promise<void>;
1339
+ fenceGateResolutions: () => void;
1340
+ waitForGateResolutionQuiescence: () => Promise<void>;
1194
1341
  activeInvocation?: { kind: InvocationKind; correlation: InvocationCorrelation };
1195
1342
  disposeGate?: () => void;
1196
1343
  }
@@ -1209,9 +1356,12 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
1209
1356
  };
1210
1357
  api.on("agent_start", async (_event, ctx) => await emitLifecycle("agent_start", ctx));
1211
1358
  api.on("agent_end", async (_event, ctx) => await emitLifecycle("agent_end", ctx));
1212
- api.on("turn_start", (_event, ctx) =>
1213
- active?.runtime.emitEvent({ type: "turn_start", sessionId: ctx.sessionManager.getSessionId() }),
1214
- );
1359
+ api.on("turn_start", async (_event, ctx) => {
1360
+ const current = active;
1361
+ if (!current) return;
1362
+ await current.registerBroker();
1363
+ current.runtime.emitEvent({ type: "turn_start", sessionId: ctx.sessionManager.getSessionId() });
1364
+ });
1215
1365
  api.on("turn_end", (_event, ctx) =>
1216
1366
  active?.runtime.emitEvent({ type: "turn_end", sessionId: ctx.sessionManager.getSessionId() }),
1217
1367
  );
@@ -1227,6 +1377,11 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
1227
1377
  const sessionId = ctx.sessionManager.getSessionId();
1228
1378
  const stateRoot = path.join(ctx.cwd, ".gjc", "state");
1229
1379
  const token = crypto.randomBytes(24).toString("base64url");
1380
+ const elevationAuthorityToken = crypto.randomBytes(32).toString("base64url");
1381
+ const elevationAuthorityFile = elevationAuthorityPath(stateRoot, sessionId);
1382
+ await fs.mkdir(path.dirname(elevationAuthorityFile), { recursive: true, mode: 0o700 });
1383
+ await Bun.write(elevationAuthorityFile, `${JSON.stringify({ version: 1, token: elevationAuthorityToken })}\n`);
1384
+ await fs.chmod(elevationAuthorityFile, 0o600);
1230
1385
  const transport = await options.createTransport({ sessionId, stateRoot, token });
1231
1386
  const revisions = new RevisionStore(sessionId, Date.now, { storageDir: stateRoot });
1232
1387
  const cursors = new CursorRegistry(token, revisions);
@@ -1234,6 +1389,25 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
1234
1389
  await reconciliation.hydrate();
1235
1390
  const pending: Array<{ kind: InvocationKind; correlation: InvocationCorrelation }> = [];
1236
1391
  const configRevision = { current: 0 };
1392
+ // The event-ring watermark lives on the runtime host, which is constructed
1393
+ // after the surface factory; the lazy source indirection lets the surface
1394
+ // read the live generation/seq once the runtime exists (Q30 atomic
1395
+ // capture, C9).
1396
+ let eventWatermarkSource: () => { generation: number; seq: number } = () => ({ generation: 0, seq: 0 });
1397
+ let acceptingGateResolutions = true;
1398
+ const inFlightGateResolutions = new Set<Promise<unknown>>();
1399
+ const trackGateResolution = <T>(resolution: Promise<T>): Promise<T> => {
1400
+ const tracked = resolution.finally(() => inFlightGateResolutions.delete(tracked));
1401
+ inFlightGateResolutions.add(tracked);
1402
+ return tracked;
1403
+ };
1404
+ const waitForGateResolutionQuiescence = async (): Promise<void> => {
1405
+ const settled = Promise.allSettled(inFlightGateResolutions);
1406
+ const timeout = Bun.sleep(5_000).then(() => {
1407
+ throw new Error("Timed out waiting for SDK workflow gate resolutions to settle.");
1408
+ });
1409
+ await Promise.race([settled, timeout]);
1410
+ };
1237
1411
  const surfaceFactory = createSdkSurfaceFactory({
1238
1412
  ctx,
1239
1413
  id: sessionId,
@@ -1243,6 +1417,7 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
1243
1417
  skillStatusLookup: selector => reconciliation.lookup("skill", selector),
1244
1418
  configOverrides: options.configOverrides,
1245
1419
  settings: options.settings,
1420
+ getEventWatermark: () => eventWatermarkSource(),
1246
1421
  });
1247
1422
  const queryHandlers = new QueryHandlers(surfaceFactory.query, sessionId, revisions, cursors);
1248
1423
  const controlSurface = createControlSurface(
@@ -1256,6 +1431,9 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
1256
1431
  options.settings,
1257
1432
  options.configOverrides,
1258
1433
  configRevision,
1434
+ elevationAuthorityToken,
1435
+ () => acceptingGateResolutions,
1436
+ trackGateResolution,
1259
1437
  );
1260
1438
  let runtime: SessionSdkSessionRuntime;
1261
1439
  const installProviderDefinitions = (capability: string, definitions: unknown): void => {
@@ -1325,22 +1503,23 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
1325
1503
  runtime = new SessionSdkSessionRuntime({
1326
1504
  transport,
1327
1505
  control: async (_connectionId, frame) => {
1328
- const request = frame as Record<string, unknown>;
1506
+ const request = controlRequestFromFrame(frame as Record<string, unknown>);
1329
1507
  return dispatchControl(
1330
1508
  controlSurface,
1331
1509
  OPERATIONS.find(operation => operation.kind === "control" && operation.sdkId === request.operation),
1332
- {
1333
- id: typeof request.id === "string" ? request.id : "",
1334
- operation: typeof request.operation === "string" ? request.operation : "",
1335
- input: request.input,
1336
- expectedRevision: typeof request.expectedRevision === "string" ? request.expectedRevision : undefined,
1337
- idempotencyKey: typeof request.idempotencyKey === "string" ? request.idempotencyKey : undefined,
1338
- confirm: request.confirm === true,
1339
- },
1510
+ request,
1340
1511
  );
1341
1512
  },
1342
1513
  query: async (connectionId, frame) => {
1343
1514
  const request = frame as Record<string, unknown>;
1515
+ // D1: a non-string top-level cursor is rejected, never silently
1516
+ // dropped into a fresh-snapshot query.
1517
+ if (request.cursor !== undefined && typeof request.cursor !== "string")
1518
+ return {
1519
+ id: typeof request.id === "string" ? request.id : undefined,
1520
+ ok: false,
1521
+ error: { code: "invalid_input", message: "cursor must be a non-empty string" },
1522
+ };
1344
1523
  return queryHandlers.dispatch({
1345
1524
  id: typeof request.id === "string" ? request.id : undefined,
1346
1525
  query: typeof request.query === "string" ? request.query : "",
@@ -1359,12 +1538,54 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
1359
1538
  if (request.operation === "session.close" && response.ok === true) ctx.shutdown();
1360
1539
  },
1361
1540
  });
1541
+ // The runtime host now owns the live event stream; point the surface's
1542
+ // checkpoint watermark at the real generation/seq (C9).
1543
+ eventWatermarkSource = () => ({
1544
+ generation: runtime.host.events.generation,
1545
+ seq: runtime.host.events.sequence,
1546
+ });
1362
1547
  const disposeGate = ctx.workflowGate?.onGateEmitted?.(gate =>
1363
1548
  runtime.emitEvent({ kind: "workflow_gate", payload: gate }),
1364
1549
  );
1365
- active = { runtime, revisions, cursors, reconciliation, pending, disposeGate };
1550
+ let brokerRegistered = false;
1551
+ const registerBroker = async (): Promise<void> => {
1552
+ if (brokerRegistered) return;
1553
+ try {
1554
+ await ensureBroker({ agentDir: options.agentDir });
1555
+ const index = await new SessionIndex(options.agentDir).open();
1556
+ const locator = { repo: path.resolve(ctx.cwd), stateRoot };
1557
+ await runtime.registerWithBroker({
1558
+ register: async input => {
1559
+ const endpointMtimeMs = (await fs.stat(path.join(input.stateRoot, "sdk", `${input.sessionId}.json`)))
1560
+ .mtimeMs;
1561
+ await index.append({ type: "host_registered", ...input, locator, pid: process.pid, endpointMtimeMs });
1562
+ },
1563
+ unregister: async input => {
1564
+ await index.append({ type: "host_unregistered", ...input, locator, pid: process.pid });
1565
+ },
1566
+ });
1567
+ brokerRegistered = true;
1568
+ } catch (error) {
1569
+ if (options.brokerRegistrationRequired) throw error;
1570
+ logger.warn(`sdk broker registration unavailable: ${String(error)}`);
1571
+ }
1572
+ };
1573
+ active = {
1574
+ runtime,
1575
+ revisions,
1576
+ cursors,
1577
+ reconciliation,
1578
+ pending,
1579
+ registerBroker,
1580
+ fenceGateResolutions: () => {
1581
+ acceptingGateResolutions = false;
1582
+ },
1583
+ waitForGateResolutionQuiescence,
1584
+ disposeGate,
1585
+ };
1366
1586
  try {
1367
1587
  await runtime.start();
1588
+ await registerBroker();
1368
1589
  } catch (error) {
1369
1590
  active = undefined;
1370
1591
  disposeGate?.();
@@ -1375,7 +1596,19 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
1375
1596
  code: errorCode(cleanupError),
1376
1597
  error: String(cleanupError),
1377
1598
  });
1378
- active = { runtime, revisions, cursors, reconciliation, pending, disposeGate };
1599
+ active = {
1600
+ runtime,
1601
+ revisions,
1602
+ cursors,
1603
+ reconciliation,
1604
+ pending,
1605
+ registerBroker,
1606
+ fenceGateResolutions: () => {
1607
+ acceptingGateResolutions = false;
1608
+ },
1609
+ waitForGateResolutionQuiescence,
1610
+ disposeGate,
1611
+ };
1379
1612
  throw new AggregateError([error, cleanupError], "SDK runtime startup failed and cleanup failed.");
1380
1613
  }
1381
1614
  cursors.close();
@@ -1385,10 +1618,12 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
1385
1618
  };
1386
1619
  const stopActive = async (): Promise<void> => {
1387
1620
  const current = active;
1388
- active = undefined;
1389
1621
  if (!current) return;
1390
- current.disposeGate?.();
1622
+ current.fenceGateResolutions();
1391
1623
  try {
1624
+ await current.waitForGateResolutionQuiescence();
1625
+ active = undefined;
1626
+ current.disposeGate?.();
1392
1627
  await current.runtime.stop();
1393
1628
  } catch (error) {
1394
1629
  logger.error("sdk runtime stop failed", { code: errorCode(error), error: String(error) });
@@ -58,6 +58,9 @@ const CONTROL_BINDINGS: Readonly<Record<string, string | undefined>> = {
58
58
 
59
59
  // Resource queries (`artifact.read`, `runtime.jobs.list`) remain dispatchable when their
60
60
  // backing session resource is absent so their handlers can return `resource_gone`.
61
+ // `session.checkpoint` (Q30) is likewise unconditionally installed: it is an SDK-native
62
+ // replay authority that degrades to the live transcript head when the host publishes no
63
+ // durable checkpoint, so it must never be hidden behind a binding gate.
61
64
  const QUERY_BINDINGS: Readonly<Record<string, string | undefined>> = {
62
65
  "skill.list/state": "getSkillState",
63
66
  "config.list/get": "getConfigItems",
@@ -198,7 +198,13 @@ export async function createSdkWebSocketTransport(
198
198
  try {
199
199
  await filesystem.writeFile(
200
200
  endpointFile,
201
- JSON.stringify({ version: 1, url, token: input.token, pid: process.pid }),
201
+ JSON.stringify({
202
+ version: 1,
203
+ sessionId: input.sessionId,
204
+ url,
205
+ token: input.token,
206
+ pid: process.pid,
207
+ }),
202
208
  "utf8",
203
209
  );
204
210
  } catch (error) {