@gajae-code/coding-agent 0.16.1 → 0.16.4

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 (146) hide show
  1. package/CHANGELOG.md +70 -0
  2. package/dist/types/cli/args.d.ts +2 -2
  3. package/dist/types/cli/root-flags.d.ts +4 -2
  4. package/dist/types/cli.d.ts +1 -1
  5. package/dist/types/commands/launch.d.ts +1 -1
  6. package/dist/types/config/autorouting-tier-map.d.ts +3 -0
  7. package/dist/types/config/model-registry.d.ts +4 -0
  8. package/dist/types/config/model-resolver.d.ts +9 -0
  9. package/dist/types/config/settings-schema.d.ts +29 -1
  10. package/dist/types/discovery/claude-plugins.d.ts +3 -1
  11. package/dist/types/discovery/helpers.d.ts +2 -0
  12. package/dist/types/extensibility/gjc-plugins/mcp-policy.d.ts +16 -0
  13. package/dist/types/extensibility/gjc-plugins/paths.d.ts +2 -0
  14. package/dist/types/extensibility/runtime-skill-discovery.d.ts +1 -1
  15. package/dist/types/extensibility/shared-events.d.ts +1 -1
  16. package/dist/types/gjc-runtime/session-state-lock.d.ts +2 -0
  17. package/dist/types/modes/components/assistant-message.d.ts +1 -2
  18. package/dist/types/modes/components/settings-selector.d.ts +6 -1
  19. package/dist/types/modes/components/status-line/command.d.ts +35 -0
  20. package/dist/types/modes/components/status-line/index.d.ts +1 -0
  21. package/dist/types/modes/components/status-line/types.d.ts +5 -0
  22. package/dist/types/modes/components/tool-status-header.d.ts +14 -0
  23. package/dist/types/modes/controllers/event-controller.d.ts +6 -0
  24. package/dist/types/modes/interactive-mode.d.ts +1 -0
  25. package/dist/types/modes/types.d.ts +1 -0
  26. package/dist/types/runtime-mcp/client.d.ts +9 -1
  27. package/dist/types/runtime-mcp/manager.d.ts +7 -1
  28. package/dist/types/runtime-mcp/pool.d.ts +22 -3
  29. package/dist/types/runtime-mcp/transports/stdio.d.ts +0 -4
  30. package/dist/types/runtime-mcp/types.d.ts +22 -0
  31. package/dist/types/sdk/bus/chat-daemon-runtime.d.ts +12 -0
  32. package/dist/types/sdk/bus/slack-live-provider.d.ts +6 -0
  33. package/dist/types/sdk/bus/slack-provider.d.ts +12 -0
  34. package/dist/types/sdk/cli/rows.d.ts +7 -0
  35. package/dist/types/sdk/host/events.d.ts +1 -0
  36. package/dist/types/sdk/host/host.d.ts +2 -0
  37. package/dist/types/sdk/host/session-runtime.d.ts +23 -0
  38. package/dist/types/sdk/session-api-key.d.ts +26 -0
  39. package/dist/types/sdk/transport/serve-cli.d.ts +1 -1
  40. package/dist/types/session/agent-session.d.ts +9 -0
  41. package/dist/types/setup/paseo/announce.d.ts +111 -0
  42. package/dist/types/setup/paseo/setup-deps.d.ts +2 -1
  43. package/dist/types/system-prompt.d.ts +2 -0
  44. package/dist/types/task/executor.d.ts +2 -1
  45. package/dist/types/tools/fetch.d.ts +7 -0
  46. package/dist/types/tools/index.d.ts +8 -2
  47. package/dist/types/tools/path-utils.d.ts +2 -0
  48. package/dist/types/tools/search.d.ts +6 -0
  49. package/dist/types/utils/external-editor.d.ts +5 -0
  50. package/package.json +7 -7
  51. package/scripts/capture-status-line-custom-editor-showcase.ts +151 -0
  52. package/scripts/generate-sdk-operation-inventory.ts +9 -0
  53. package/scripts/verify-status-line-custom-editor-showcase.ts +309 -0
  54. package/src/cli/args.ts +20 -9
  55. package/src/cli/root-flags.ts +5 -3
  56. package/src/config/autorouting-tier-map.ts +1 -0
  57. package/src/config/file-lock.ts +181 -17
  58. package/src/config/model-preset-registry.ts +5 -3
  59. package/src/config/model-profiles.ts +40 -0
  60. package/src/config/model-registry.ts +57 -11
  61. package/src/config/model-resolver.ts +48 -0
  62. package/src/config/settings-schema.ts +33 -1
  63. package/src/config/settings.ts +1 -0
  64. package/src/defaults/gjc/extensions/grok-cli-vendor/src/models/catalog.ts +11 -11
  65. package/src/defaults/gjc/extensions/grok-cli-vendor/src/provider/register.ts +13 -2
  66. package/src/discovery/builtin.ts +13 -4
  67. package/src/discovery/claude-plugins.ts +12 -4
  68. package/src/discovery/helpers.ts +10 -2
  69. package/src/extensibility/gjc-plugins/compiler.ts +208 -58
  70. package/src/extensibility/gjc-plugins/installer.ts +13 -12
  71. package/src/extensibility/gjc-plugins/mcp-policy.ts +81 -52
  72. package/src/extensibility/gjc-plugins/paths.ts +13 -0
  73. package/src/extensibility/gjc-plugins/runtime-adapters.ts +758 -16
  74. package/src/extensibility/runtime-skill-discovery.ts +77 -151
  75. package/src/extensibility/shared-events.ts +1 -1
  76. package/src/extensibility/skills.ts +44 -16
  77. package/src/gjc-runtime/repository-binding.ts +1 -1
  78. package/src/gjc-runtime/session-state-lock.ts +94 -6
  79. package/src/internal-urls/docs-index.generated.ts +10 -10
  80. package/src/main.ts +72 -34
  81. package/src/modes/DESIGN.md +54 -0
  82. package/src/modes/acp/acp-agent.ts +29 -14
  83. package/src/modes/components/assistant-message.ts +22 -9
  84. package/src/modes/components/model-selector.ts +166 -33
  85. package/src/modes/components/settings-selector.ts +1075 -235
  86. package/src/modes/components/status-line/command.ts +190 -0
  87. package/src/modes/components/status-line/index.ts +1 -0
  88. package/src/modes/components/status-line/segments.ts +32 -5
  89. package/src/modes/components/status-line/types.ts +5 -0
  90. package/src/modes/components/tool-status-header.ts +393 -21
  91. package/src/modes/controllers/command-controller.ts +2 -0
  92. package/src/modes/controllers/event-controller.ts +148 -5
  93. package/src/modes/controllers/selector-controller.ts +21 -0
  94. package/src/modes/interactive-mode.ts +47 -1
  95. package/src/modes/types.ts +1 -0
  96. package/src/modes/utils/ui-helpers.ts +1 -0
  97. package/src/prompts/system/system-prompt.md +8 -0
  98. package/src/prompts/tools/bash.md +1 -1
  99. package/src/prompts/tools/job.md +1 -1
  100. package/src/prompts/tools/search.md +2 -0
  101. package/src/runtime-mcp/client.ts +60 -17
  102. package/src/runtime-mcp/manager.ts +171 -14
  103. package/src/runtime-mcp/pool.test.ts +209 -0
  104. package/src/runtime-mcp/pool.ts +224 -23
  105. package/src/runtime-mcp/transports/stdio.ts +158 -24
  106. package/src/runtime-mcp/types.ts +24 -0
  107. package/src/sdk/acp/adapter.ts +1 -1
  108. package/src/sdk/broker/broker.ts +5 -4
  109. package/src/sdk/bus/chat-daemon-control.ts +1 -1
  110. package/src/sdk/bus/chat-daemon-runtime.ts +44 -1
  111. package/src/sdk/bus/slack-daemon.ts +92 -2
  112. package/src/sdk/bus/slack-live-provider.ts +12 -0
  113. package/src/sdk/bus/slack-provider.ts +5 -0
  114. package/src/sdk/cli/rows.ts +47 -2
  115. package/src/sdk/cli/session-cli.ts +47 -23
  116. package/src/sdk/host/events.ts +25 -2
  117. package/src/sdk/host/host.ts +4 -1
  118. package/src/sdk/host/session-runtime.test.ts +36 -25
  119. package/src/sdk/host/session-runtime.ts +279 -43
  120. package/src/sdk/protocol/operation-inventory.generated.json +66 -0
  121. package/src/sdk/router/session-router.ts +60 -37
  122. package/src/sdk/session-api-key.ts +94 -0
  123. package/src/sdk/session.ts +73 -30
  124. package/src/sdk/transport/serve-cli.ts +3 -3
  125. package/src/session/agent-session.ts +123 -38
  126. package/src/session/contribution-prep.ts +214 -21
  127. package/src/setup/paseo/announce.ts +450 -0
  128. package/src/setup/paseo/setup-deps.ts +15 -3
  129. package/src/slash-commands/builtin-registry.ts +1 -0
  130. package/src/system-prompt.ts +4 -0
  131. package/src/task/executor.ts +2 -1
  132. package/src/task/index.ts +17 -4
  133. package/src/tools/bash.ts +50 -21
  134. package/src/tools/cron.ts +1 -1
  135. package/src/tools/fetch.ts +29 -1
  136. package/src/tools/index.ts +8 -2
  137. package/src/tools/job.ts +8 -2
  138. package/src/tools/monitor.ts +8 -2
  139. package/src/tools/path-utils.ts +7 -0
  140. package/src/tools/search.ts +116 -58
  141. package/src/tools/skill.ts +22 -2
  142. package/src/tools/subagent.ts +3 -1
  143. package/src/tools/tool-catalog.generated.ts +9 -2
  144. package/src/utils/external-editor.ts +16 -2
  145. package/src/web/insane/bridge.ts +113 -23
  146. package/src/web/search/providers/perplexity.ts +102 -41
@@ -2841,6 +2841,50 @@
2841
2841
  "testIds": "not_applicable"
2842
2842
  }
2843
2843
  },
2844
+ {
2845
+ "sourceId": "agent_session:awaitDisposeCompletion",
2846
+ "sourceFile": "packages/coding-agent/src/session/agent-session.ts",
2847
+ "sourceKind": "agent_session",
2848
+ "decision": "exclude",
2849
+ "rationale": "test-only disposal completion seam, not a user-facing SDK control seam",
2850
+ "exclusionMetadata": {
2851
+ "adapterMappings": "not_applicable",
2852
+ "testIds": "not_applicable"
2853
+ }
2854
+ },
2855
+ {
2856
+ "sourceId": "agent_session:setDisposeTimeoutForTests",
2857
+ "sourceFile": "packages/coding-agent/src/session/agent-session.ts",
2858
+ "sourceKind": "agent_session",
2859
+ "decision": "exclude",
2860
+ "rationale": "test-only disposal timeout seam, not a user-facing SDK control seam",
2861
+ "exclusionMetadata": {
2862
+ "adapterMappings": "not_applicable",
2863
+ "testIds": "not_applicable"
2864
+ }
2865
+ },
2866
+ {
2867
+ "sourceId": "agent_session:trackPostPromptTaskForTests",
2868
+ "sourceFile": "packages/coding-agent/src/session/agent-session.ts",
2869
+ "sourceKind": "agent_session",
2870
+ "decision": "exclude",
2871
+ "rationale": "test-only post-prompt task seam, not a user-facing SDK control seam",
2872
+ "exclusionMetadata": {
2873
+ "adapterMappings": "not_applicable",
2874
+ "testIds": "not_applicable"
2875
+ }
2876
+ },
2877
+ {
2878
+ "sourceId": "agent_session:requestWorkerIntegrationForTests",
2879
+ "sourceFile": "packages/coding-agent/src/session/agent-session.ts",
2880
+ "sourceKind": "agent_session",
2881
+ "decision": "exclude",
2882
+ "rationale": "test-only worker integration seam, not a user-facing SDK control seam",
2883
+ "exclusionMetadata": {
2884
+ "adapterMappings": "not_applicable",
2885
+ "testIds": "not_applicable"
2886
+ }
2887
+ },
2844
2888
  {
2845
2889
  "sourceId": "agent_session:closeWriterStrict",
2846
2890
  "sourceFile": "packages/coding-agent/src/session/agent-session.ts",
@@ -2885,6 +2929,17 @@
2885
2929
  "testIds": "not_applicable"
2886
2930
  }
2887
2931
  },
2932
+ {
2933
+ "sourceId": "agent_session:runWithPromptAdmissionForTests",
2934
+ "sourceFile": "packages/coding-agent/src/session/agent-session.ts",
2935
+ "sourceKind": "agent_session",
2936
+ "decision": "exclude",
2937
+ "rationale": "test-only prompt admission seam, not a user-facing SDK control seam",
2938
+ "exclusionMetadata": {
2939
+ "adapterMappings": "not_applicable",
2940
+ "testIds": "not_applicable"
2941
+ }
2942
+ },
2888
2943
  {
2889
2944
  "sourceId": "agent_session:queueCoordinatorRuntimeStatePersistForTests",
2890
2945
  "sourceFile": "packages/coding-agent/src/session/agent-session.ts",
@@ -3912,6 +3967,17 @@
3912
3967
  "testIds": "not_applicable"
3913
3968
  }
3914
3969
  },
3970
+ {
3971
+ "sourceId": "agent_session:reloadSkills",
3972
+ "sourceFile": "packages/coding-agent/src/session/agent-session.ts",
3973
+ "sourceKind": "agent_session",
3974
+ "decision": "exclude",
3975
+ "rationale": "internal marketplace lifecycle refresh for the session skill catalog; not an independent SDK ingress operation",
3976
+ "exclusionMetadata": {
3977
+ "adapterMappings": "not_applicable",
3978
+ "testIds": "not_applicable"
3979
+ }
3980
+ },
3915
3981
  {
3916
3982
  "sourceId": "agent_session:retireWorkspaceTreeForRescope",
3917
3983
  "sourceFile": "packages/coding-agent/src/session/agent-session.ts",
@@ -338,6 +338,8 @@ type AttachedSession = {
338
338
  replayPending: boolean;
339
339
  replayPendingSince?: number;
340
340
  replayPendingToken?: object;
341
+ drainingHeldSeq?: number;
342
+ drainingHeldBatch?: HeldFrame[];
341
343
  readyTail: Promise<void>;
342
344
  readonly publication: { promise: Promise<void>; resolve: () => void; reject: (reason?: unknown) => void };
343
345
  dispose: () => void;
@@ -1899,44 +1901,53 @@ export class SessionRouter {
1899
1901
  }
1900
1902
 
1901
1903
  async #reinitializeAttachment(attached: AttachedSession): Promise<void> {
1902
- const previous = attached.readyTail;
1904
+ // A prior ready tail may be waiting for publication work carried by the dead
1905
+ // socket. Chaining the reconnect behind it prevents the replacement from even
1906
+ // issuing replay. Replace its hold-array identity to fence that tail, but carry
1907
+ // forward frames it had not started draining; the active draining sequence is
1908
+ // already represented by the independent frame tail.
1909
+ if (attached.barrier.held) {
1910
+ const carried = [...(attached.drainingHeldBatch ?? []), ...attached.barrier.held];
1911
+ attached.drainingHeldBatch = undefined;
1912
+ attached.barrier.held = carried
1913
+ .filter(entry => entry.seq !== attached.drainingHeldSeq)
1914
+ .filter((entry, index, entries) => entries.findIndex(candidate => candidate.seq === entry.seq) === index);
1915
+ }
1903
1916
  const replayToken = {};
1904
1917
  attached.replayPending = true;
1905
1918
  attached.replayPendingSince = Date.now();
1906
1919
  attached.replayPendingToken = replayToken;
1907
- const current = previous
1908
- .catch(() => undefined)
1909
- .then(async () => {
1910
- if (!this.#attachmentLive(attached)) return;
1911
- const proven = await this.#readProvenEndpoint(attached.indexed);
1912
- if (
1913
- !proven ||
1914
- proven.endpoint.url !== attached.endpoint.url ||
1915
- proven.endpoint.token !== attached.endpoint.token ||
1916
- proven.endpoint.pid !== attached.pid ||
1917
- !sameEndpointIdentity(attached.endpointIdentity, proven.identity)
1918
- ) {
1919
- await this.#retireAttachment(attached, proven ? "replaced_same_generation" : undefined);
1920
- return;
1921
- }
1922
- attached.initializingPublication = true;
1923
- try {
1924
- void Promise.resolve()
1925
- .then(() => this.#deps.onNotificationSubscriptionReady?.(attached.notificationSubscription))
1926
- .catch(error => {
1927
- this.#detachNotification(attached, "cancelled");
1928
- logger.warn(`SDK notification subscription reconnect hook failed locally: ${String(error)}`);
1929
- });
1930
- await this.#deps.onAttachmentReady?.(attached.capability);
1931
- await this.#deps.onReplayReady?.();
1932
- } catch {
1933
- await this.#retireAttachment(attached);
1934
- return;
1935
- } finally {
1936
- attached.initializingPublication = false;
1937
- }
1938
- if (this.#attachmentLive(attached)) await this.#replayAttachment(attached, attached.cursor.seq);
1939
- });
1920
+ const current = Promise.resolve().then(async () => {
1921
+ if (!this.#attachmentLive(attached)) return;
1922
+ const proven = await this.#readProvenEndpoint(attached.indexed);
1923
+ if (
1924
+ !proven ||
1925
+ proven.endpoint.url !== attached.endpoint.url ||
1926
+ proven.endpoint.token !== attached.endpoint.token ||
1927
+ proven.endpoint.pid !== attached.pid ||
1928
+ !sameEndpointIdentity(attached.endpointIdentity, proven.identity)
1929
+ ) {
1930
+ await this.#retireAttachment(attached, proven ? "replaced_same_generation" : undefined);
1931
+ return;
1932
+ }
1933
+ attached.initializingPublication = true;
1934
+ try {
1935
+ void Promise.resolve()
1936
+ .then(() => this.#deps.onNotificationSubscriptionReady?.(attached.notificationSubscription))
1937
+ .catch(error => {
1938
+ this.#detachNotification(attached, "cancelled");
1939
+ logger.warn(`SDK notification subscription reconnect hook failed locally: ${String(error)}`);
1940
+ });
1941
+ await this.#deps.onAttachmentReady?.(attached.capability);
1942
+ await this.#deps.onReplayReady?.();
1943
+ } catch {
1944
+ await this.#retireAttachment(attached);
1945
+ return;
1946
+ } finally {
1947
+ attached.initializingPublication = false;
1948
+ }
1949
+ if (this.#attachmentLive(attached)) await this.#replayAttachment(attached, attached.cursor.seq);
1950
+ });
1940
1951
  attached.readyTail = current.finally(() => {
1941
1952
  if (attached.replayPendingToken === replayToken) {
1942
1953
  attached.replayPending = false;
@@ -2294,7 +2305,19 @@ export class SessionRouter {
2294
2305
  return;
2295
2306
  }
2296
2307
  const batch = held.splice(0, held.length).sort((left, right) => left.seq - right.seq);
2297
- for (const entry of batch) await this.#enqueueFrame(attached, entry.frame, "ordered");
2308
+ attached.drainingHeldBatch = batch;
2309
+ for (;;) {
2310
+ const entry = batch.shift();
2311
+ if (!entry) break;
2312
+ attached.drainingHeldSeq = entry.seq;
2313
+ try {
2314
+ await this.#enqueueFrame(attached, entry.frame, "ordered");
2315
+ } finally {
2316
+ if (attached.drainingHeldSeq === entry.seq) attached.drainingHeldSeq = undefined;
2317
+ }
2318
+ if (attached.barrier.held !== held) return;
2319
+ }
2320
+ if (attached.drainingHeldBatch === batch) attached.drainingHeldBatch = undefined;
2298
2321
  }
2299
2322
  }
2300
2323
 
@@ -2374,7 +2397,7 @@ export class SessionRouter {
2374
2397
  this.#failBarrier(attached, "replay response contained a malformed event");
2375
2398
  return;
2376
2399
  }
2377
- let previousSeq = sinceSeq;
2400
+ let previousSeq = -1;
2378
2401
  for (const event of events) {
2379
2402
  if (event.type !== "event") {
2380
2403
  this.#failBarrier(attached, "replay response contained a non-event frame");
@@ -2411,7 +2434,7 @@ export class SessionRouter {
2411
2434
  }
2412
2435
  const retained = events
2413
2436
  .map(event => readSequence(event.seq))
2414
- .find(seq => seq !== undefined && seq <= gap.toSeq);
2437
+ .find(seq => seq !== undefined && seq >= gap.fromSeq && seq <= gap.toSeq);
2415
2438
  if (retained !== undefined) {
2416
2439
  this.#failBarrier(
2417
2440
  attached,
@@ -0,0 +1,94 @@
1
+ /**
2
+ * AgentLoop calls `getApiKey(provider)` before `streamFn`.
3
+ * Resolve/stream already use `getApiKey(model)`. If those two lookups diverge,
4
+ * the child dies locally (~8ms, 0 tokens, sanitized "Agent run failed.")
5
+ * and never reaches the working model-scoped path (#5081).
6
+ */
7
+ export type SessionApiKeyModel = {
8
+ provider: string;
9
+ baseUrl?: string;
10
+ };
11
+
12
+ export type SessionApiKeyRegistry = {
13
+ getApiKey(
14
+ model: SessionApiKeyModel,
15
+ sessionId?: string,
16
+ options?: { signal?: AbortSignal },
17
+ ): Promise<string | undefined>;
18
+ getApiKeyForProvider(
19
+ provider: string,
20
+ sessionId?: string,
21
+ baseUrl?: string,
22
+ options?: { signal?: AbortSignal },
23
+ ): Promise<string | undefined>;
24
+ };
25
+
26
+ export type SessionApiKeyResolution = {
27
+ apiKey: string;
28
+ credentialSessionId: string | undefined;
29
+ };
30
+
31
+ export type SessionApiKeyCredentialType = "api_key" | "oauth" | undefined;
32
+
33
+ export function isStableSessionApiKeyCredentialType(
34
+ previous: SessionApiKeyCredentialType,
35
+ replacement: SessionApiKeyCredentialType,
36
+ ): boolean {
37
+ return previous === replacement;
38
+ }
39
+
40
+ export function resolveLiveSessionApiKeyModel(
41
+ live: SessionApiKeyModel | undefined,
42
+ captured: SessionApiKeyModel | undefined,
43
+ provider: string,
44
+ ): SessionApiKeyModel | undefined {
45
+ if (live?.provider === provider) return live;
46
+ if (captured?.provider === provider) return captured;
47
+ return undefined;
48
+ }
49
+
50
+ async function lookupOnce(
51
+ registry: SessionApiKeyRegistry,
52
+ provider: string,
53
+ sessionId: string | undefined,
54
+ model: SessionApiKeyModel | undefined,
55
+ signal: AbortSignal | undefined,
56
+ ): Promise<string | undefined> {
57
+ signal?.throwIfAborted();
58
+ const matchingModel = model?.provider === provider ? model : undefined;
59
+ const options = signal ? { signal } : undefined;
60
+ let key: string | undefined;
61
+ if (matchingModel) {
62
+ key = await registry.getApiKey(matchingModel, sessionId, options);
63
+ signal?.throwIfAborted();
64
+ }
65
+ if (!key) {
66
+ key = await registry.getApiKeyForProvider(provider, sessionId, matchingModel?.baseUrl, options);
67
+ signal?.throwIfAborted();
68
+ }
69
+ return key;
70
+ }
71
+
72
+ export async function lookupSessionApiKey(
73
+ registry: SessionApiKeyRegistry,
74
+ provider: string,
75
+ sessionId: string | undefined,
76
+ model: SessionApiKeyModel | undefined,
77
+ allowUnscopedRetry = true,
78
+ signal?: AbortSignal,
79
+ ): Promise<SessionApiKeyResolution> {
80
+ let key = await lookupOnce(registry, provider, sessionId, model, signal);
81
+ // Architect children inherit the parent SID. Direct `-p --no-session` does
82
+ // not. Scoped miss then global/broker hit is the remaining 0-token death
83
+ // after #5105 (jsonl: Agent run failed / output 0).
84
+ if (!key && sessionId && allowUnscopedRetry) {
85
+ key = await lookupOnce(registry, provider, undefined, model, signal);
86
+ if (key) return { apiKey: key, credentialSessionId: undefined };
87
+ }
88
+ if (!key) {
89
+ throw Object.assign(new Error(`No API key found for provider "${provider}"`), {
90
+ code: "provider_unavailable",
91
+ });
92
+ }
93
+ return { apiKey: key, credentialSessionId: sessionId };
94
+ }
@@ -197,6 +197,11 @@ import { EventBus } from "../utils/event-bus";
197
197
  import { buildNamedToolChoice, buildNamedToolChoiceResult } from "../utils/tool-choice";
198
198
  import type { WorkspaceTree } from "../workspace-tree";
199
199
  import { createSessionLifecycleService } from "./lifecycle/client";
200
+ import {
201
+ isStableSessionApiKeyCredentialType,
202
+ lookupSessionApiKey,
203
+ resolveLiveSessionApiKeyModel,
204
+ } from "./session-api-key";
200
205
  import {
201
206
  attachLifecycleStartupCapability,
202
207
  lifecycleMcpStartupTimeoutOption,
@@ -2338,15 +2343,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
2338
2343
  }
2339
2344
  if (options.skills === undefined && settings.get("skills.enabled")) {
2340
2345
  try {
2341
- const reloaded = await loadSkills({
2342
- ...settings.getGroup("skills"),
2343
- agentDir,
2344
- cwd: to,
2345
- disabledExtensions: settings.get("disabledExtensions"),
2346
- });
2347
- skills = withEmbeddedDefaultGjcSkills(reloaded.skills);
2348
- if (!options.parentTaskPrefix) setActiveSkills(skills);
2349
- await session?.replaceSkills(skills);
2346
+ await session?.reloadSkills(to);
2350
2347
  } catch (error) {
2351
2348
  logger.warn("Failed to reload skills after session rescope", { error: safeErrorForLog(error) });
2352
2349
  }
@@ -2708,19 +2705,19 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
2708
2705
  else signal.addEventListener("abort", () => resolve(), { once: true });
2709
2706
  return promise;
2710
2707
  },
2708
+ getSessionId: () => sessionManager.getSessionId(),
2711
2709
  // Subagents inherit the parent's manager; its registered endpoint is
2712
2710
  // authoritative for owned-registration keying and endpoint-first
2713
2711
  // manager lookup (the child's own id is never registered), so tools
2714
2712
  // pass the same endpoint the manager's completion callback resolves
2715
2713
  // (review thread P1). For a top-level session this equals the
2716
2714
  // session id.
2717
- getSessionId: () => AsyncJobManager.endpointIdOf(asyncJobManager) ?? asyncJobEndpointId,
2715
+ getAsyncEndpointId: () => AsyncJobManager.endpointIdOf(asyncJobManager) ?? asyncJobEndpointId,
2718
2716
  registerForegroundFoldParticipant: adapter =>
2719
2717
  session?.registerForegroundFoldParticipant(adapter) ?? (() => {}),
2720
2718
  hasForegroundBashBackgroundRequestHandler: () => session?.hasForegroundBashBackgroundRequestHandler() ?? false,
2721
2719
  requestForegroundBashBackground: (reason, adapter) =>
2722
2720
  Promise.resolve(session?.requestForegroundBashBackground(reason, adapter) ?? false),
2723
- getToolInterruptPolicy: () => agent?.getToolInterruptPolicy() ?? "abort_tools",
2724
2721
 
2725
2722
  getCredentialSessionId: () => session?.credentialSessionId ?? credentialSessionId,
2726
2723
  getMcpManager: () => mcpManager ?? options.inheritedMcpManager,
@@ -3925,6 +3922,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
3925
3922
  appendSystemPrompt: appendPrompt,
3926
3923
  pluginAppendices: pluginSystemAppendices,
3927
3924
  repeatToolDescriptions,
3925
+ reasoningLanguage: settings.get("reasoningLanguage"),
3928
3926
  intentField,
3929
3927
  toolDiscoveryActive: effectiveDiscoveryMode === "all" || mcpDiscoveryEnabled,
3930
3928
  eagerTasks: resolveEagerTasks(),
@@ -4258,6 +4256,32 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
4258
4256
  ).seedNormalizedMessages(options.forkContextSeed.messages);
4259
4257
  }
4260
4258
 
4259
+ const resolvedCredentialScopes = new Map<string, string | undefined>();
4260
+ const resolveAgentApiKey = async (provider: string, signal?: AbortSignal): Promise<string> => {
4261
+ const liveModel = resolveLiveSessionApiKeyModel(sessionAgent?.state.model, model, provider);
4262
+ const hasExplicitScopePolicy =
4263
+ modelRegistry.authStorage.hasSessionCredentialSelector(provider, credentialSessionId) ||
4264
+ modelRegistry.authStorage.hasSessionCredentialAuto(provider, credentialSessionId);
4265
+ const resolution = await lookupSessionApiKey(
4266
+ modelRegistry,
4267
+ provider,
4268
+ credentialSessionId,
4269
+ liveModel,
4270
+ !hasExplicitScopePolicy,
4271
+ signal,
4272
+ );
4273
+ resolvedCredentialScopes.set(provider, resolution.credentialSessionId);
4274
+ return resolution.apiKey;
4275
+ };
4276
+ const resolvedCredentialType = (provider: string): "api_key" | "oauth" | undefined => {
4277
+ const scope = resolvedCredentialScopes.has(provider)
4278
+ ? resolvedCredentialScopes.get(provider)
4279
+ : credentialSessionId;
4280
+ return modelRegistry.authStorage.getEffectiveCredentialType(provider, scope, {
4281
+ owner: modelRegistry.getAuthStorageOwner(),
4282
+ });
4283
+ };
4284
+
4261
4285
  agent = new Agent({
4262
4286
  initialState: {
4263
4287
  systemPrompt,
@@ -4296,44 +4320,42 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
4296
4320
  shouldPause: options.shouldPause,
4297
4321
  preferWebsockets: preferOpenAICodexWebsockets,
4298
4322
  getToolContext: tc => toolContextStore.getContext(tc),
4299
- getApiKey: async provider => {
4323
+ getApiKey: provider => {
4300
4324
  // AgentLoop asks by provider, but the active model carries the
4301
4325
  // model-scoped credential selector. Read it at call time so model
4302
4326
  // changes are honored after /new, fork, resume, or branch switches.
4303
- const liveModel = sessionAgent?.state.model ?? model;
4304
- const key =
4305
- liveModel?.provider === provider
4306
- ? await modelRegistry.getApiKey(liveModel, credentialSessionId)
4307
- : undefined;
4308
- const providerKey = key ?? (await modelRegistry.getApiKeyForProvider(provider, credentialSessionId));
4309
- if (!providerKey) {
4310
- throw new Error(`No API key found for provider "${provider}"`);
4311
- }
4312
- return providerKey;
4327
+ return resolveAgentApiKey(provider);
4313
4328
  },
4314
- getAuthCredentialType: provider => modelRegistry.getSessionCredentialType(provider, credentialSessionId),
4329
+ getAuthCredentialType: resolvedCredentialType,
4315
4330
  streamFn: async (streamModel, context, streamOptions) => {
4316
4331
  const requestStartedAt = performance.now();
4317
4332
  let stream: Awaited<ReturnType<typeof streamSimple>>;
4318
4333
  try {
4319
- const effectiveApiKey = await modelRegistry.getApiKey(streamModel, credentialSessionId);
4320
4334
  stream = await streamSimple(streamModel, context, {
4321
4335
  ...streamOptions,
4322
- apiKey: effectiveApiKey,
4323
- authCredentialType: modelRegistry.getSessionCredentialType(streamModel.provider, credentialSessionId),
4324
4336
  onAuthError: async (provider, oldKey, error) => {
4337
+ const resolvedScope = resolvedCredentialScopes.has(provider)
4338
+ ? resolvedCredentialScopes.get(provider)
4339
+ : credentialSessionId;
4325
4340
  await modelRegistry.authStorage.invalidateCredentialMatching(provider, oldKey, {
4326
4341
  signal: streamOptions?.signal,
4327
- sessionId: credentialSessionId,
4342
+ sessionId: resolvedScope,
4328
4343
  owner: modelRegistry.getAuthStorageOwner(),
4329
4344
  });
4330
4345
  logger.debug("Retrying provider request after credential invalidation", {
4331
4346
  provider,
4332
4347
  error: error instanceof Error ? error.message : String(error),
4333
4348
  });
4334
- return modelRegistry.getApiKey(streamModel, credentialSessionId, {
4335
- signal: streamOptions?.signal,
4336
- });
4349
+ const replacementApiKey = await resolveAgentApiKey(provider, streamOptions?.signal);
4350
+ if (
4351
+ !isStableSessionApiKeyCredentialType(
4352
+ streamOptions?.authCredentialType,
4353
+ resolvedCredentialType(provider),
4354
+ )
4355
+ ) {
4356
+ return undefined;
4357
+ }
4358
+ return replacementApiKey;
4337
4359
  },
4338
4360
  });
4339
4361
  } catch (error) {
@@ -4460,6 +4482,27 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
4460
4482
  customCommands: customCommandsResult.commands,
4461
4483
  skills,
4462
4484
  skillWarnings,
4485
+ reloadSkills:
4486
+ options.skills === undefined
4487
+ ? async reloadCwd => {
4488
+ if (!settings.get("skills.enabled")) {
4489
+ return { skills: getEmbeddedDefaultGjcSkills(), warnings: [] };
4490
+ } else {
4491
+ const reloaded = await loadSkills({
4492
+ ...settings.getGroup("skills"),
4493
+ agentDir,
4494
+ cwd: reloadCwd,
4495
+ disabledExtensions: settings.get("disabledExtensions"),
4496
+ });
4497
+ return { skills: withEmbeddedDefaultGjcSkills(reloaded.skills), warnings: reloaded.warnings };
4498
+ }
4499
+ }
4500
+ : undefined,
4501
+ onSkillsReloaded: (reloadedSkills, reloadedWarnings) => {
4502
+ skills = reloadedSkills;
4503
+ skillWarnings = reloadedWarnings;
4504
+ if (!options.parentTaskPrefix) setActiveSkills(reloadedSkills);
4505
+ },
4463
4506
  skillsSettings: settings.getGroup("skills"),
4464
4507
  modelRegistry,
4465
4508
  taskDepth,
@@ -92,10 +92,10 @@ function brokerSessionRows(sessions: readonly unknown[]): BrokerSessionRow[] {
92
92
  }
93
93
 
94
94
  /** Exhausts strict broker `session.list` pages into one full session snapshot. */
95
- export async function listBrokerSessions(broker: SdkClient): Promise<BrokerSessionRow[]> {
95
+ export async function listBrokerSessions(broker: SdkClient, explicitSessionId?: string): Promise<BrokerSessionRow[]> {
96
96
  try {
97
97
  const pages = await traverseSessionList(
98
- {},
98
+ { ...(explicitSessionId ? { resolveSessionId: explicitSessionId } : {}) },
99
99
  async input => await broker.global("session.list", input),
100
100
  response => {
101
101
  brokerResult(response);
@@ -147,7 +147,7 @@ export async function runSdkServe(argv: string[]): Promise<void> {
147
147
  throw new Error("broker_unavailable: SDK broker is not reachable");
148
148
  }
149
149
  try {
150
- const sessionId = selectBrokerSession(await listBrokerSessions(broker), parsed.sessionId);
150
+ const sessionId = selectBrokerSession(await listBrokerSessions(broker, parsed.sessionId), parsed.sessionId);
151
151
  const endpoint = brokerResult(await broker.global("session.get_endpoint", { sessionId }));
152
152
  const url = typeof endpoint.url === "string" && endpoint.url ? endpoint.url : undefined;
153
153
  const token = typeof endpoint.token === "string" ? endpoint.token : "";