@gajae-code/coding-agent 0.18.5 → 0.18.7

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 (133) hide show
  1. package/CHANGELOG.md +140 -0
  2. package/dist/types/capability/types.d.ts +4 -0
  3. package/dist/types/cli-commands.d.ts +10 -0
  4. package/dist/types/cli-ordinary.d.ts +13 -0
  5. package/dist/types/cli.d.ts +12 -3
  6. package/dist/types/commands/contribution-prep.d.ts +0 -3
  7. package/dist/types/commands/sdk.d.ts +3 -0
  8. package/dist/types/commands/update.d.ts +1 -0
  9. package/dist/types/config/model-profile-activation.d.ts +19 -0
  10. package/dist/types/config/settings.d.ts +4 -0
  11. package/dist/types/coordinator-mcp/session-reaper.d.ts +5 -0
  12. package/dist/types/discovery/helpers.d.ts +2 -2
  13. package/dist/types/edit/modes/replace.d.ts +3 -3
  14. package/dist/types/edit/streaming.d.ts +2 -2
  15. package/dist/types/exec/bash-shell-supervisor.d.ts +12 -0
  16. package/dist/types/runtime-mcp/elicitation.d.ts +12 -0
  17. package/dist/types/sdk/broker/discovery.d.ts +8 -0
  18. package/dist/types/sdk/broker/ensure.d.ts +1 -0
  19. package/dist/types/sdk/broker/lifecycle-ledger.d.ts +4 -0
  20. package/dist/types/sdk/broker/lifecycle.d.ts +9 -0
  21. package/dist/types/sdk/broker/runtime.d.ts +1 -1
  22. package/dist/types/sdk/broker/task-artifact-owner-validation.d.ts +55 -0
  23. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +1 -1
  24. package/dist/types/sdk/bus/telegram-daemon.d.ts +4 -2
  25. package/dist/types/sdk/cli/diagnostics-cli.d.ts +18 -0
  26. package/dist/types/sdk/diagnostics/observe-broker.d.ts +185 -0
  27. package/dist/types/sdk/host/control/operations.d.ts +1 -1
  28. package/dist/types/sdk/host/host.d.ts +6 -0
  29. package/dist/types/sdk/index.d.ts +1 -0
  30. package/dist/types/sdk/router/session-router.d.ts +9 -0
  31. package/dist/types/sdk/session.d.ts +13 -0
  32. package/dist/types/session/agent-session.d.ts +5 -1
  33. package/dist/types/session/internal/managed-session-storage.d.ts +11 -1
  34. package/dist/types/session/internal/task-artifact-owner-access.d.ts +17 -0
  35. package/dist/types/session/internal/task-artifact-owner-transcript.d.ts +3 -0
  36. package/dist/types/session/session-retirement.d.ts +47 -0
  37. package/dist/types/session/session-storage.d.ts +0 -46
  38. package/dist/types/session/session-transcript-header.d.ts +2 -0
  39. package/dist/types/session/task-artifact-owner-codec.d.ts +107 -0
  40. package/dist/types/session/task-artifact-owner-retirement.d.ts +7 -0
  41. package/dist/types/session/task-artifact-owner.d.ts +10 -0
  42. package/dist/types/tools/ask-contract.d.ts +75 -254
  43. package/dist/types/tools/bash.d.ts +1 -1
  44. package/package.json +7 -7
  45. package/scripts/build-sdk-package-smoke.ts +5 -0
  46. package/scripts/resident-memory-bench.ts +174 -20
  47. package/src/capability/index.ts +1 -0
  48. package/src/capability/types.ts +4 -0
  49. package/src/cli/config-cli.ts +8 -18
  50. package/src/cli/public-command-entry.ts +5 -0
  51. package/src/cli/ssh-cli.ts +6 -3
  52. package/src/cli/web-search-cli.ts +9 -3
  53. package/src/cli-commands.ts +14 -0
  54. package/src/cli-main.ts +7 -8
  55. package/src/cli-ordinary.ts +197 -0
  56. package/src/cli.ts +25 -182
  57. package/src/commands/contribution-prep.ts +1 -3
  58. package/src/commands/sdk.ts +171 -21
  59. package/src/commands/update.ts +15 -1
  60. package/src/config/file-lock.ts +91 -2
  61. package/src/config/model-profile-activation.ts +70 -4
  62. package/src/config/model-profile-contract.ts +4 -1
  63. package/src/config/model-profiles.ts +23 -31
  64. package/src/config/model-resolver.ts +9 -1
  65. package/src/config/settings.ts +44 -4
  66. package/src/coordinator-mcp/event-webhook.ts +19 -14
  67. package/src/coordinator-mcp/policy.ts +14 -4
  68. package/src/coordinator-mcp/server.ts +142 -7
  69. package/src/coordinator-mcp/session-reaper.ts +9 -1
  70. package/src/defaults/gjc/skills/deep-interview/SKILL.md +1 -1
  71. package/src/discovery/helpers.ts +9 -3
  72. package/src/discovery/ssh.ts +8 -3
  73. package/src/edit/modes/replace.ts +6 -6
  74. package/src/edit/streaming.ts +15 -15
  75. package/src/exec/bash-shell-supervisor.ts +60 -3
  76. package/src/gjc-runtime/gc-runtime.ts +2 -1
  77. package/src/hindsight/config.ts +9 -3
  78. package/src/internal-urls/docs-index.generated.ts +11 -10
  79. package/src/memories/index.ts +8 -2
  80. package/src/modes/acp/acp-agent.ts +724 -370
  81. package/src/modes/components/tool-execution.ts +1 -1
  82. package/src/prompts/tools/ask.md +2 -0
  83. package/src/runtime-mcp/client.ts +13 -4
  84. package/src/runtime-mcp/elicitation.ts +142 -0
  85. package/src/sdk/broker/broker.ts +54 -1
  86. package/src/sdk/broker/daemon-entry.ts +67 -6
  87. package/src/sdk/broker/discovery.ts +8 -0
  88. package/src/sdk/broker/ensure.ts +156 -20
  89. package/src/sdk/broker/lifecycle-ledger.ts +110 -12
  90. package/src/sdk/broker/lifecycle.ts +334 -348
  91. package/src/sdk/broker/runtime.ts +5 -1
  92. package/src/sdk/broker/task-artifact-owner-validation.ts +259 -0
  93. package/src/sdk/broker/transport.ts +5 -1
  94. package/src/sdk/bus/chat-daemon-control.ts +2 -2
  95. package/src/sdk/bus/chat-daemon-runtime.ts +3 -0
  96. package/src/sdk/bus/index.ts +3 -3
  97. package/src/sdk/bus/telegram-daemon-contract.ts +1 -1
  98. package/src/sdk/bus/telegram-daemon-control.ts +15 -12
  99. package/src/sdk/bus/telegram-daemon.ts +38 -3
  100. package/src/sdk/cli/diagnostics-cli.ts +247 -0
  101. package/src/sdk/diagnostics/observe-broker.ts +654 -0
  102. package/src/sdk/host/control/dispatch.ts +52 -4
  103. package/src/sdk/host/control/operations.ts +1 -1
  104. package/src/sdk/host/host.ts +6 -0
  105. package/src/sdk/host/session-runtime.test.ts +83 -4
  106. package/src/sdk/host/session-runtime.ts +15 -7
  107. package/src/sdk/index.ts +1 -0
  108. package/src/sdk/lifecycle-session.ts +1 -0
  109. package/src/sdk/mcp/server.ts +15 -3
  110. package/src/sdk/protocol/operation-registry.ts +1 -1
  111. package/src/sdk/router/session-router.ts +26 -3
  112. package/src/sdk/session.ts +51 -8
  113. package/src/session/agent-session.ts +169 -43
  114. package/src/session/blob-store.ts +78 -33
  115. package/src/session/internal/managed-session-storage.ts +203 -4
  116. package/src/session/internal/task-artifact-owner-access.ts +229 -0
  117. package/src/session/internal/task-artifact-owner-transcript.ts +68 -0
  118. package/src/session/session-retirement.ts +155 -0
  119. package/src/session/session-storage.ts +1 -160
  120. package/src/session/session-transcript-header.ts +17 -0
  121. package/src/session/task-artifact-owner-codec.ts +578 -0
  122. package/src/session/task-artifact-owner-retirement.ts +475 -0
  123. package/src/session/task-artifact-owner.ts +289 -0
  124. package/src/setup/hermes/templates/operator-instructions.v1.md +19 -1
  125. package/src/setup/hermes-setup.ts +4 -1
  126. package/src/slash-commands/builtin-registry.ts +8 -5
  127. package/src/tools/ask-contract.test.ts +76 -1
  128. package/src/tools/ask-contract.ts +57 -6
  129. package/src/tools/ask.ts +16 -7
  130. package/src/tools/bash.ts +38 -29
  131. package/src/tools/descriptors.test.ts +29 -0
  132. package/src/tools/tool-catalog.generated.ts +7 -4
  133. package/src/web/search/index.ts +2 -2
@@ -93,6 +93,7 @@ const IDEMPOTENCY_TTL_MS = 15 * 60 * 1_000;
93
93
  const MAX_IDEMPOTENCY_ENTRIES = 256;
94
94
 
95
95
  const sessionChains = new WeakMap<ControlSurface, Promise<void>>();
96
+ const pendingReplacementBarriers = new WeakMap<ControlSurface, Promise<void>>();
96
97
  interface IdempotencyEntry {
97
98
  hash: string;
98
99
  expiresAt: number;
@@ -189,6 +190,19 @@ function invalidInput(message: string): never {
189
190
  throw new TypedControlError("invalid_input", message);
190
191
  }
191
192
 
193
+ const EXPECTED_SDK_RUN_TOKEN_PATTERN = /^[A-Za-z0-9][A-Za-z0-9_.:-]{0,127}:[A-Za-z0-9][A-Za-z0-9_.:-]{0,127}$/;
194
+
195
+ function enqueueReplacementBarrier(surface: ControlSurface): () => void {
196
+ const previous = pendingReplacementBarriers.get(surface) ?? Promise.resolve();
197
+ const { promise: current, resolve: resolveCurrent } = Promise.withResolvers<void>();
198
+ const barrier = previous.then(() => current);
199
+ pendingReplacementBarriers.set(surface, barrier);
200
+ return () => {
201
+ resolveCurrent();
202
+ if (pendingReplacementBarriers.get(surface) === barrier) pendingReplacementBarriers.delete(surface);
203
+ };
204
+ }
205
+
192
206
  /**
193
207
  * C04 `turn.abort` dispatch.
194
208
  *
@@ -244,7 +258,11 @@ function invoke(
244
258
  case "turn.prompt":
245
259
  return surface.prompt(text(input), input.images, input.clientRef as string | undefined);
246
260
  case "turn.steer":
247
- return surface.steer(text(input), typeof input.clientRef === "string" ? input.clientRef : undefined);
261
+ return surface.steer(
262
+ text(input),
263
+ typeof input.clientRef === "string" ? input.clientRef : undefined,
264
+ Object.hasOwn(input, "expectedSdkRunToken") ? (input.expectedSdkRunToken as string) : undefined,
265
+ );
248
266
  case "turn.follow_up":
249
267
  return surface.followUp(text(input));
250
268
  case "turn.abort":
@@ -486,6 +504,15 @@ export function dispatchControl(
486
504
  );
487
505
  if (!isInput(request.input))
488
506
  return Promise.resolve(failure(request.id, "invalid_input", "Control input must be an object."));
507
+ if (
508
+ row.sdkId === "turn.steer" &&
509
+ Object.hasOwn(request.input, "expectedSdkRunToken") &&
510
+ (typeof request.input.expectedSdkRunToken !== "string" ||
511
+ !EXPECTED_SDK_RUN_TOKEN_PATTERN.test(request.input.expectedSdkRunToken))
512
+ )
513
+ return Promise.resolve(
514
+ failure(request.id, "invalid_input", "expectedSdkRunToken must be a commandId:turnId pair."),
515
+ );
489
516
  if (
490
517
  row.sdkId === "turn.abort" &&
491
518
  ((brokerAbortFieldPresent && !brokerAbortAuthorized) ||
@@ -538,10 +565,31 @@ export function dispatchControl(
538
565
  const work = () => execute(surface, row, dispatchRequest);
539
566
  if (row.sdkId === "turn.abort_and_prompt") {
540
567
  const cancellable = surface as PreflightCancellableSurface;
541
- if (Object.hasOwn(cancellable, "cancelPendingPreflights")) cancellable.cancelPendingPreflights?.();
542
- return serialize(surface, work);
568
+ const releaseBarrier = enqueueReplacementBarrier(surface);
569
+ try {
570
+ if (Object.hasOwn(cancellable, "cancelPendingPreflights")) cancellable.cancelPendingPreflights?.();
571
+ return serialize(surface, async () => {
572
+ try {
573
+ return await work();
574
+ } finally {
575
+ releaseBarrier();
576
+ }
577
+ });
578
+ } catch (error) {
579
+ releaseBarrier();
580
+ throw error;
581
+ }
543
582
  }
544
583
  if (row.idempotency === "idempotent" && dispatchRequest.idempotencyKey)
545
584
  return idempotent(surface, row, dispatchRequest, work);
546
- return row.idempotency === "ordered" && row.sdkId !== "retry.now" ? serialize(surface, work) : work();
585
+ if (row.sdkId === "turn.steer") {
586
+ const replacementBarrier = pendingReplacementBarriers.get(surface);
587
+ return replacementBarrier ? replacementBarrier.then(work) : work();
588
+ }
589
+ // Active feedback must reach the worker even while an earlier ordered control
590
+ // waits for that worker. Correlated steer admission remains deduplicated by
591
+ // the runtime reconciliation store, independently of this dispatch chain.
592
+ return row.idempotency === "ordered" && row.sdkId !== "retry.now" && row.sdkId !== "turn.steer"
593
+ ? serialize(surface, work)
594
+ : work();
547
595
  }
@@ -22,7 +22,7 @@ export type ControlInput = Record<string, unknown>;
22
22
  */
23
23
  export interface ControlSurface {
24
24
  prompt(text: string, images?: ControlValue, clientRef?: string): Promise<ControlValue> | ControlValue;
25
- steer(text: string, clientRef?: string): Promise<ControlValue> | ControlValue;
25
+ steer(text: string, clientRef?: string, expectedSdkRunToken?: string): Promise<ControlValue> | ControlValue;
26
26
  followUp(text: string): Promise<ControlValue> | ControlValue;
27
27
  abort(): Promise<ControlValue> | ControlValue;
28
28
  /** Terminal abort: stop the current root turn (and optionally exact owned work). */
@@ -107,6 +107,12 @@ export const TOOL_ACTIVITY_CAPABILITY = "tool_activity_v2";
107
107
  export const TURN_STREAM_CAPABILITY = "turn_stream";
108
108
  /** Capability used by notification adapters that observe a host without owning its work. */
109
109
  export const SESSION_HOST_OBSERVER_CAPABILITY = "session_host_observer_v1";
110
+ /**
111
+ * Opts notification adapters into positioned-only delivery for matching live
112
+ * effects: the host excludes a negotiating connection from the raw native leg
113
+ * of every frame its positioned envelope already reached.
114
+ */
115
+ export const POSITIONED_NOTIFICATION_EFFECTS_CAPABILITY = "positioned_notification_effects_v1";
110
116
  export const CAP_GATED_FRAME_KINDS: ReadonlySet<string> = new Set(["tool_activity", "reasoning_summary"]);
111
117
  const EMPTY_CAPABILITIES: ReadonlySet<string> = new Set();
112
118
 
@@ -3,8 +3,9 @@ import { createHash } from "node:crypto";
3
3
  import { chmod, mkdir, mkdtemp, readdir, readFile, rm, writeFile } from "node:fs/promises";
4
4
  import * as os from "node:os";
5
5
  import * as path from "node:path";
6
- import { markNonDispatchedToolEvent } from "@gajae-code/agent-core";
6
+ import { markNonDispatchedToolEvent, ThinkingLevel } from "@gajae-code/agent-core";
7
7
  import { logger } from "@gajae-code/utils";
8
+ import { createTestSession } from "../../../test/utilities";
8
9
  import { AsyncJobManager } from "../../async";
9
10
  import type { Settings } from "../../config/settings";
10
11
  import type { ExtensionAPI, ExtensionContext, ExtensionTranscriptEntry } from "../../extensibility/extensions";
@@ -103,7 +104,46 @@ test("runtime capabilities preserve explicit primary control surface", () => {
103
104
  expect(createSdkCapabilities(policy, false, "cli")).toMatchObject({ primaryControlSurface: "cli" });
104
105
  });
105
106
 
106
- function memoryTransport(): SessionSdkTransport & {
107
+ test("thinking.set uses the control setter and reports the applied session level", async () => {
108
+ const sessionContext = await createTestSession({ inMemory: true });
109
+ const { session, tempDir: cwd, cleanup } = sessionContext;
110
+ const handlers = new Map<string, (event: unknown, ctx: ExtensionContext) => Promise<void> | void>();
111
+ const api = {
112
+ on(event: string, handler: (event: unknown, ctx: ExtensionContext) => Promise<void> | void) {
113
+ handlers.set(event, handler);
114
+ },
115
+ sendUserMessage: async () => {},
116
+ setThinkingLevel: () => {
117
+ throw new Error("thinking.set must not use the legacy setter");
118
+ },
119
+ setThinkingLevelForControl: async (level: ThinkingLevel, persist: boolean) => {
120
+ expect(persist).toBe(false);
121
+ await session.setThinkingLevelForControl(level, persist);
122
+ },
123
+ } as unknown as ExtensionAPI;
124
+ const response = Promise.withResolvers<SdkFrame>();
125
+ const transport = memoryTransport(frame => {
126
+ if (frame.type === "control_response" && frame.id === "thinking-set") response.resolve(frame);
127
+ });
128
+ createSdkSessionRuntimeExtension(api, { agentDir: cwd, createTransport: async () => transport });
129
+ const ctx = extensionContext(transport.sessionId, cwd);
130
+ try {
131
+ await handlers.get("session_start")?.({}, ctx);
132
+ transport.feed("client", {
133
+ type: "control_request",
134
+ id: "thinking-set",
135
+ operation: "thinking.set",
136
+ input: { level: ThinkingLevel.Medium },
137
+ } as SdkFrame);
138
+ await response.promise;
139
+ expect(session.thinkingLevel).toBe(ThinkingLevel.Medium);
140
+ } finally {
141
+ await handlers.get("session_shutdown")?.({}, ctx);
142
+ await cleanup();
143
+ }
144
+ });
145
+
146
+ function memoryTransport(onSend?: (frame: SdkFrame) => void): SessionSdkTransport & {
107
147
  feed(connectionId: string, frame: SdkFrame): void;
108
148
  readonly sent: SdkFrame[];
109
149
  readonly broadcasts: SdkFrame[];
@@ -126,6 +166,7 @@ function memoryTransport(): SessionSdkTransport & {
126
166
  },
127
167
  sendFrame(_connectionId, frame) {
128
168
  sent.push(frame);
169
+ onSend?.(frame);
129
170
  },
130
171
  start: async () => {
131
172
  started = true;
@@ -290,7 +331,7 @@ test("session.last_assistant returns the latest projected readable text past non
290
331
  });
291
332
  });
292
333
 
293
- test("session.last_assistant returns resource_gone when the projected transcript has no readable assistant text", async () => {
334
+ test("session.last_assistant returns null when the projected transcript has no readable assistant text", async () => {
294
335
  const sessionId = "last-assistant-empty";
295
336
  const ctx = extensionContext(sessionId, "/tmp", {
296
337
  transcript: [
@@ -311,7 +352,7 @@ test("session.last_assistant returns resource_gone when the projected transcript
311
352
  ],
312
353
  });
313
354
 
314
- expect(await queryLastAssistant(ctx, sessionId)).toMatchObject({ ok: false, error: { code: "resource_gone" } });
355
+ expect(await queryLastAssistant(ctx, sessionId)).toMatchObject({ ok: true, page: { items: [null] } });
315
356
  });
316
357
 
317
358
  test("native prompt reconciliation fails closed for an explicitly empty assistant result", () => {
@@ -4142,6 +4183,7 @@ describe("SessionSdkSessionRuntime", () => {
4142
4183
  interface PreflightHooks {
4143
4184
  onPreflightAccepted?: () => void;
4144
4185
  onPreflightAcceptCommit?: () => void | Promise<void>;
4186
+ expectedSdkRunToken?: string;
4145
4187
  }
4146
4188
 
4147
4189
  interface ResponseFrame {
@@ -4482,6 +4524,43 @@ function neverSettlingPromise(): Promise<void> {
4482
4524
  return Promise.withResolvers<void>().promise;
4483
4525
  }
4484
4526
 
4527
+ test("SDK turn.steer preserves its expected run token and propagates a stale-run rejection", async () => {
4528
+ const cwd = await mkdtemp(path.join(os.tmpdir(), "gjc-turn-steer-run-token-"));
4529
+ const staleToken = "ended-command:ended-turn";
4530
+ const clientRef = "stale-steer-ref";
4531
+ const calls: Array<{ content: unknown; deliverAs?: string; expectedSdkRunToken?: string }> = [];
4532
+ let harness: InvocationHarness | undefined;
4533
+ try {
4534
+ harness = await invocationHarness("steer-run-token", cwd, {
4535
+ sendUserMessage: async (content, options) => {
4536
+ calls.push({
4537
+ content,
4538
+ deliverAs: options?.deliverAs,
4539
+ expectedSdkRunToken: options?.expectedSdkRunToken,
4540
+ });
4541
+ if (options?.expectedSdkRunToken === staleToken)
4542
+ throw Object.assign(new Error("The expected SDK run is not active."), { code: "turn_not_active" });
4543
+ return "completed";
4544
+ },
4545
+ });
4546
+
4547
+ const rejected = await harness.control("turn.steer", {
4548
+ text: "stale steer",
4549
+ expectedSdkRunToken: staleToken,
4550
+ clientRef,
4551
+ });
4552
+ expect(rejected).toMatchObject({ ok: false, error: { code: "turn_not_active" } });
4553
+ expect(calls).toEqual([{ content: "stale steer", deliverAs: "steer", expectedSdkRunToken: staleToken }]);
4554
+ expect(await harness.query("turn.steer_status", { clientRef })).toMatchObject({
4555
+ ok: true,
4556
+ result: { status: "rejected" },
4557
+ });
4558
+ } finally {
4559
+ if (harness) await harness.stop();
4560
+ await rm(cwd, { recursive: true, force: true });
4561
+ }
4562
+ });
4563
+
4485
4564
  describe("post-acceptance invocation terminalization", () => {
4486
4565
  test.each([
4487
4566
  { status: 402, code: "provider_http_402" },
@@ -2059,7 +2059,7 @@ function createQuerySurface(
2059
2059
  : entry.content?.flatMap(block => (block.type === "text" ? [block.text] : [])).join("\n");
2060
2060
  if (text !== undefined && text.trim().length > 0) return text;
2061
2061
  }
2062
- return undefined;
2062
+ return null;
2063
2063
  };
2064
2064
  const getProfileCredentialSessionId = () => ctx.credentialSessionId ?? id;
2065
2065
  const profileSettings = (options.settings ?? ctx.settings) as Pick<Settings, "get"> | undefined;
@@ -2914,6 +2914,7 @@ function createControlSurface(
2914
2914
  };
2915
2915
  type InternalSendOptions = NonNullable<Parameters<ExtensionAPI["sendUserMessage"]>[1]> & {
2916
2916
  sdkRunCapability?: SdkRunCapability;
2917
+ expectedSdkRunToken?: string;
2917
2918
  };
2918
2919
  type InternalSdkApi = Omit<ExtensionAPI, "sendUserMessage"> & {
2919
2920
  sendUserMessage: (
@@ -4243,23 +4244,30 @@ function createControlSurface(
4243
4244
  ),
4244
4245
  );
4245
4246
  },
4246
- steer: async (text, clientRef) => {
4247
+ steer: async (text, clientRef, expectedSdkRunToken) => {
4247
4248
  const invalid = validateRequiredPromptText("turn.steer", { text });
4248
4249
  if (invalid) throw Object.assign(new Error(invalid.message), { code: invalid.code });
4249
4250
  const retainedClientRef = normalizeClientRef(clientRef);
4251
+ const sendSteer = () =>
4252
+ sendSdkUserMessage(text, {
4253
+ deliverAs: "steer",
4254
+ ...(expectedSdkRunToken === undefined ? {} : { expectedSdkRunToken }),
4255
+ });
4250
4256
  if (retainedClientRef === undefined) {
4251
4257
  const correlation = newCorrelation();
4252
- await sendSdkUserMessage(text, { deliverAs: "steer" });
4258
+ await sendSteer();
4253
4259
  return { accepted: true, ...correlation };
4254
4260
  }
4255
4261
  const durable = steerReconciliation;
4256
4262
  const reservation = await durable.reserveSteer(retainedClientRef, text);
4257
4263
  if (reservation.replay) return { accepted: reservation.result.status === "accepted", ...reservation.result };
4258
4264
  try {
4259
- await sendSdkUserMessage(text, { deliverAs: "steer" });
4265
+ await sendSteer();
4260
4266
  return { accepted: true, ...(await durable.settleSteer(retainedClientRef, "accepted")) };
4261
4267
  } catch (error) {
4262
- return { accepted: false, ...(await durable.settleSteer(retainedClientRef, "rejected", error)) };
4268
+ const settled = await durable.settleSteer(retainedClientRef, "rejected", error);
4269
+ if (expectedSdkRunToken !== undefined) throw error;
4270
+ return { accepted: false, ...settled };
4263
4271
  }
4264
4272
  },
4265
4273
  followUp: async text => {
@@ -4364,8 +4372,8 @@ function createControlSurface(
4364
4372
  },
4365
4373
  setModelProfile: id => (ctx.setModelProfile ? ctx.setModelProfile(id) : unavailable("model.profile.set")()),
4366
4374
  cycleModel: () => (ctx.cycleModel ? ctx.cycleModel() : unavailable("model.cycle")()),
4367
- setThinking: level => {
4368
- api.setThinkingLevel(level as never);
4375
+ setThinking: async level => {
4376
+ await api.setThinkingLevelForControl(level as ThinkingLevel, false);
4369
4377
  return { changed: true };
4370
4378
  },
4371
4379
  cycleThinking: () =>
package/src/sdk/index.ts CHANGED
@@ -21,6 +21,7 @@ export type {
21
21
  TrackedSendUserMessageOptions,
22
22
  } from "../session/agent-session";
23
23
  export * as bus from "./bus";
24
+ export * from "./diagnostics/observe-broker";
24
25
  export * as host from "./host";
25
26
  export * as lifecycle from "./lifecycle";
26
27
  export * as mcp from "./mcp";
@@ -123,6 +123,7 @@ export async function createLifecycleAgentSession(
123
123
  // inside the broker's readiness window is what kills the child at the
124
124
  // cutoff; the host resumes it once readiness is published.
125
125
  deferMemoryBackendStartup: true,
126
+ deferOptionalModelRefresh: true,
126
127
  [lifecycleStartupCapabilityOption]: capability,
127
128
  ...(mcpStartupTimeoutMs !== undefined ? { [lifecycleMcpStartupTimeoutOption]: mcpStartupTimeoutMs } : {}),
128
129
  } as CreateAgentSessionOptions & {
@@ -1,4 +1,4 @@
1
- import { getAgentDir } from "@gajae-code/utils";
1
+ import { getAgentDir, VERSION } from "@gajae-code/utils";
2
2
  import { ensureBroker } from "../broker/ensure";
3
3
  import { lifecycleRequestTimeoutMs } from "../broker/startup-budget";
4
4
  import { SdkClientError } from "../client/client";
@@ -14,6 +14,8 @@ import { type SessionAttachment, SessionRouter, SessionRouterError } from "../ro
14
14
  import { SessionListTraversalError, sessionListPageFromResponse, traverseSessionList } from "../session-list";
15
15
 
16
16
  const PROTOCOL_VERSION = "2024-11-05";
17
+ const SUPPORTED_PROTOCOL_VERSIONS = [PROTOCOL_VERSION, "2025-03-26", "2025-06-18"] as const;
18
+ const NEWEST_PROTOCOL_VERSION = SUPPORTED_PROTOCOL_VERSIONS[SUPPORTED_PROTOCOL_VERSIONS.length - 1];
17
19
  const SERVER_NAME = "gjc-sdk-mcp";
18
20
  const ENDPOINT_CREDENTIAL_OPERATION = "session.get_endpoint";
19
21
 
@@ -107,6 +109,16 @@ function isObject(value: unknown): value is Arguments {
107
109
  return typeof value === "object" && value !== null && !Array.isArray(value);
108
110
  }
109
111
 
112
+ function negotiatedProtocolVersion(params: unknown): string {
113
+ const requestedProtocolVersion = isObject(params) ? params.protocolVersion : undefined;
114
+ if (
115
+ typeof requestedProtocolVersion === "string" &&
116
+ (SUPPORTED_PROTOCOL_VERSIONS as readonly string[]).includes(requestedProtocolVersion)
117
+ )
118
+ return requestedProtocolVersion;
119
+ return NEWEST_PROTOCOL_VERSION;
120
+ }
121
+
110
122
  function asString(args: Arguments, name: string): string | null {
111
123
  return typeof args[name] === "string" && args[name] ? (args[name] as string) : null;
112
124
  }
@@ -389,9 +401,9 @@ export function createSdkMcpServer(options: SdkMcpServerOptions = {}) {
389
401
  jsonrpc: "2.0",
390
402
  id,
391
403
  result: {
392
- protocolVersion: PROTOCOL_VERSION,
404
+ protocolVersion: negotiatedProtocolVersion(request.params),
393
405
  capabilities: { tools: {} },
394
- serverInfo: { name: SERVER_NAME },
406
+ serverInfo: { name: SERVER_NAME, version: VERSION },
395
407
  },
396
408
  };
397
409
  if (request.method === "tools/list")
@@ -267,7 +267,7 @@ function controlErrors(id: string): string[] {
267
267
  // refuses a session with no selected model before admission. The code is
268
268
  // appended: ACP machine-only mapping reads errorCodes[0].
269
269
  C01: ["client_ref_conflict", "reconciliation_capacity", "reconciliation_persist_failed", "model_not_selected"],
270
- C02: ["client_ref_conflict", "reconciliation_capacity", "reconciliation_persist_failed"],
270
+ C02: ["turn_not_active", "client_ref_conflict", "reconciliation_capacity", "reconciliation_persist_failed"],
271
271
  C09: ["client_ref_conflict", "reconciliation_capacity", "reconciliation_persist_failed"],
272
272
  C06: ["action_claimed"],
273
273
  C07: ["action_claimed", "terminal_uncertain"],
@@ -25,7 +25,11 @@ import {
25
25
  readSdkSessionEndpoint,
26
26
  type SdkSessionEndpoint,
27
27
  } from "../client/discovery";
28
- import { SESSION_HOST_OBSERVER_CAPABILITY, TURN_STREAM_CAPABILITY } from "../host/host";
28
+ import {
29
+ POSITIONED_NOTIFICATION_EFFECTS_CAPABILITY,
30
+ SESSION_HOST_OBSERVER_CAPABILITY,
31
+ TURN_STREAM_CAPABILITY,
32
+ } from "../host/host";
29
33
  import {
30
34
  type ActivatedPreparedSession,
31
35
  type PreparedSessionActivationClient,
@@ -287,8 +291,17 @@ export interface SessionRouterOptions {
287
291
  agentDir: string;
288
292
  /** Limits attachment to exact ids selected by a Broker-scoped operation. */
289
293
  sessionIds?: readonly string[];
294
+ /** Dynamically limits attachment to sessions currently owned by the caller. */
295
+ attachFilter?: (sessionId: string) => boolean;
290
296
  /** Marks default SDK clients as notification-only observers; demanding by default. */
291
297
  observer?: boolean;
298
+ /**
299
+ * Negotiates positioned-only notification effects on default SDK clients, so the
300
+ * host stops sending them the raw native copy of frames whose positioned
301
+ * envelope already reached them. Only notification-effect publishers opt in;
302
+ * ordinary SDK observers keep both public surfaces.
303
+ */
304
+ positionedNotificationEffects?: boolean;
292
305
  deps?: SessionRouterDeps;
293
306
  /** Runtime-specific identity validation; Router supplies a conservative fallback. */
294
307
  correlateFrame?: SessionRouterFrameCorrelator;
@@ -615,7 +628,9 @@ type AdoptedSession = {
615
628
  export class SessionRouter {
616
629
  readonly #agentDir: string;
617
630
  readonly #sessionIds: ReadonlySet<string> | undefined;
631
+ readonly #attachFilter: ((sessionId: string) => boolean) | undefined;
618
632
  readonly #observer: boolean;
633
+ readonly #positionedNotificationEffects: boolean;
619
634
  readonly #deps: SessionRouterDeps;
620
635
  readonly #correlateFrame: SessionRouterFrameCorrelator;
621
636
  readonly #index: SessionIndex;
@@ -657,7 +672,9 @@ export class SessionRouter {
657
672
  constructor(options: SessionRouterOptions) {
658
673
  this.#agentDir = options.agentDir;
659
674
  this.#sessionIds = options.sessionIds === undefined ? undefined : new Set(options.sessionIds);
675
+ this.#attachFilter = options.attachFilter;
660
676
  this.#observer = options.observer === true;
677
+ this.#positionedNotificationEffects = options.positionedNotificationEffects === true;
661
678
  this.#deps = options.deps ?? {};
662
679
  this.#correlateFrame = options.correlateFrame ?? fallbackCorrelation;
663
680
  this.#index = this.#deps.createIndex?.(options.agentDir) ?? new DefaultSessionIndex(options.agentDir);
@@ -797,6 +814,7 @@ export class SessionRouter {
797
814
  if (
798
815
  sessionId !== fallback.sessionId ||
799
816
  (this.#sessionIds !== undefined && !this.#sessionIds.has(sessionId ?? "")) ||
817
+ (this.#attachFilter !== undefined && !this.#attachFilter(sessionId ?? "")) ||
800
818
  endpointGeneration === undefined ||
801
819
  pid === undefined ||
802
820
  endpointMtimeMs === undefined ||
@@ -1294,7 +1312,8 @@ export class SessionRouter {
1294
1312
  session.live &&
1295
1313
  isSessionAuthorityEligible(session) &&
1296
1314
  !session.terminalUncertain &&
1297
- (this.#sessionIds === undefined || this.#sessionIds.has(session.sessionId)),
1315
+ (this.#sessionIds === undefined || this.#sessionIds.has(session.sessionId)) &&
1316
+ (this.#attachFilter === undefined || this.#attachFilter(session.sessionId)),
1298
1317
  );
1299
1318
  const liveIds = new Set(live.map(session => session.sessionId));
1300
1319
  const attachedIds = new Set<string>();
@@ -1524,9 +1543,13 @@ export class SessionRouter {
1524
1543
  endpointMtimeMs,
1525
1544
  });
1526
1545
  } else {
1546
+ const capabilities = [
1547
+ ...(this.#observer ? [SESSION_HOST_OBSERVER_CAPABILITY, TURN_STREAM_CAPABILITY] : []),
1548
+ ...(this.#positionedNotificationEffects ? [POSITIONED_NOTIFICATION_EFFECTS_CAPABILITY] : []),
1549
+ ];
1527
1550
  const defaultClient = new SdkClient(endpoint.url, endpoint.token, {
1528
1551
  ...ACP_SESSION_RECONNECT,
1529
- ...(this.#observer ? { capabilities: [SESSION_HOST_OBSERVER_CAPABILITY, TURN_STREAM_CAPABILITY] } : {}),
1552
+ ...(capabilities.length > 0 ? { capabilities } : {}),
1530
1553
  });
1531
1554
  transport = defaultClient;
1532
1555
  connection = defaultClient.connect().then(() => defaultClient);
@@ -59,7 +59,11 @@ import { loadCapability, reset as resetCapabilities } from "../capability";
59
59
  import { type Rule, ruleCapability, setActiveRules } from "../capability/rule";
60
60
  import type { SourceMeta } from "../capability/types";
61
61
  import { AUTOROUTING_INACTIVE_WARNING } from "../config/autorouting-contract";
62
- import { ModelProfileCredentialError, resolveMissingSessionModelRecovery } from "../config/model-profile-activation";
62
+ import {
63
+ isSessionCredentialPinBlocking,
64
+ ModelProfileCredentialError,
65
+ resolveMissingSessionModelRecovery,
66
+ } from "../config/model-profile-activation";
63
67
  import { resolveModelProfileName } from "../config/model-profile-contract";
64
68
  import { resolveProfileBindings } from "../config/model-profiles";
65
69
  import { kNoAuth, ModelRegistry } from "../config/model-registry";
@@ -157,6 +161,7 @@ import {
157
161
  type MCPToolCache,
158
162
  resolveMCPToolCache,
159
163
  } from "../runtime-mcp";
164
+ import { createMCPFormInputHandler } from "../runtime-mcp/elicitation";
160
165
  import type { MCPLoadResult } from "../runtime-mcp/manager";
161
166
  import { MCP_STARTUP_WAIT_GRACE_MS } from "../runtime-mcp/startup-policy";
162
167
  import type { MCPServerConfig } from "../runtime-mcp/types";
@@ -577,6 +582,12 @@ export interface CreateAgentSessionOptions {
577
582
  * @internal CLI-only ordering guard; SDK callers retain immediate startup by default.
578
583
  */
579
584
  deferMemoryBackendStartup?: boolean;
585
+ /**
586
+ * Skip optional model-catalog discovery during lifecycle startup. ACP hosts must
587
+ * publish their endpoint before unrelated provider credential refreshes begin.
588
+ * @internal lifecycle-only startup guard.
589
+ */
590
+ deferOptionalModelRefresh?: boolean;
580
591
 
581
592
  /** Enable LSP integration (tool, formatting, diagnostics, warmup). Default: true */
582
593
  enableLsp?: boolean;
@@ -695,6 +706,22 @@ export interface CreateAgentSessionResult {
695
706
  gjcRuntimeSnapshot?: GjcRuntimeSnapshotProvider;
696
707
  }
697
708
 
709
+ /**
710
+ * Start optional provider/model discovery only when it is safe to add work to
711
+ * the caller's startup graph. Lifecycle hosts deliberately skip this during
712
+ * construction: discovery preflights every configured provider and may refresh
713
+ * an unrelated OAuth credential while several hosts are opening the same DB.
714
+ */
715
+ export function startOptionalModelRefresh(
716
+ modelRegistry: Pick<ModelRegistry, "refreshInBackground">,
717
+ credentialSessionId: string | undefined,
718
+ deferred: boolean,
719
+ ): boolean {
720
+ if (deferred) return false;
721
+ modelRegistry.refreshInBackground("online-if-uncached", credentialSessionId);
722
+ return true;
723
+ }
724
+
698
725
  export interface DeferredMcpConfigStartupResult {
699
726
  loadedToolCount: number;
700
727
  hasErrors: boolean;
@@ -1970,11 +1997,15 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
1970
1997
  options.preferredCredentialSelector.selector,
1971
1998
  );
1972
1999
  }
2000
+ // A runtime key or this registry's literal models.yml apiKey outranks an
2001
+ // unavailable pin; without one the provider stays blocked (never retargeted).
2002
+ const isCredentialPinBlocking = (provider: string): boolean =>
2003
+ isSessionCredentialPinBlocking(modelRegistry, provider, credentialSessionId);
1973
2004
  const modelApiKeyAvailability = new Map<string, boolean>();
1974
2005
  const getModelAvailabilityKey = (candidate: Model): string =>
1975
2006
  `${candidate.provider}\u0000${candidate.baseUrl ?? ""}`;
1976
2007
  const hasModelApiKey = async (candidate: Model): Promise<boolean> => {
1977
- if (authStorage.hasSessionCredentialUnavailable(candidate.provider, credentialSessionId)) return false;
2008
+ if (isCredentialPinBlocking(candidate.provider)) return false;
1978
2009
  const availabilityKey = getModelAvailabilityKey(candidate);
1979
2010
  const cached = modelApiKeyAvailability.get(availabilityKey);
1980
2011
  if (cached !== undefined) {
@@ -2000,6 +2031,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
2000
2031
  const key = await modelRegistry
2001
2032
  .getApiKey(candidate, credentialSessionId, { credentialSelector })
2002
2033
  .catch(error => {
2034
+ if (isCredentialPinBlocking(candidate.provider)) return undefined;
2003
2035
  if (credentialSelector) {
2004
2036
  logger.debug("Credential selector did not match model availability candidate", {
2005
2037
  provider: candidate.provider,
@@ -2148,7 +2180,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
2148
2180
  settings,
2149
2181
  );
2150
2182
  await refreshMissingQualifiedModelProviders(startupModelSelectors, modelRegistry, credentialSessionId);
2151
- modelRegistry.refreshInBackground("online-if-uncached", credentialSessionId);
2183
+ startOptionalModelRefresh(modelRegistry, credentialSessionId, options.deferOptionalModelRefresh === true);
2152
2184
  }
2153
2185
 
2154
2186
  const hasExplicitModel = options.model !== undefined || options.modelPattern !== undefined;
@@ -2241,8 +2273,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
2241
2273
  {
2242
2274
  managedFallback: defaultModelEntries.length > 1,
2243
2275
  canonicalSessionId: providerSessionId,
2244
- isCredentialUnavailable: provider =>
2245
- authStorage.hasSessionCredentialUnavailable(provider, credentialSessionId),
2276
+ isCredentialUnavailable: isCredentialPinBlocking,
2246
2277
  ...(persistedProfileOwnsDefault ? { aliasIntent: "preset-equivalent" as const } : {}),
2247
2278
  },
2248
2279
  );
@@ -2297,7 +2328,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
2297
2328
  !hasExplicitModel &&
2298
2329
  !model &&
2299
2330
  defaultRoleSpec.model &&
2300
- !authStorage.hasSessionCredentialUnavailable(defaultRoleSpec.model.provider, credentialSessionId) &&
2331
+ !isCredentialPinBlocking(defaultRoleSpec.model.provider) &&
2301
2332
  (!preferredCredentialProvider || defaultRoleSpec.model.provider === preferredCredentialProvider)
2302
2333
  ) {
2303
2334
  const settingsDefaultModel = defaultRoleSpec.model;
@@ -2623,7 +2654,19 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
2623
2654
  let ownedMcpManagerTools: readonly CustomTool[] = [];
2624
2655
  let publishOwnedMcpTools = false;
2625
2656
  const notificationDebounceTimers = new Map<string, Timer>();
2657
+ const installMcpInputHandler = (manager: MCPManager): void => {
2658
+ manager.setInputRequestHandler(
2659
+ createMCPFormInputHandler({
2660
+ getUi: () => {
2661
+ const context = toolContextStore.getContext();
2662
+ return { ui: context.ui, hasUI: context.hasUI === true };
2663
+ },
2664
+ getAskAnswerSource: () => session.getAskAnswerSource(),
2665
+ }),
2666
+ );
2667
+ };
2626
2668
  const wireMcpManagerCallbacks = (manager: MCPManager): void => {
2669
+ installMcpInputHandler(manager);
2627
2670
  manager.setOnPromptsChanged(serverName => {
2628
2671
  const promptCommands = buildMCPPromptCommands(manager);
2629
2672
  session.setMCPPromptCommands(promptCommands);
@@ -3655,6 +3698,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
3655
3698
  : {}),
3656
3699
  });
3657
3700
  owned.setAuthStorage(authStorage);
3701
+ installMcpInputHandler(owned);
3658
3702
  mcpManager = owned;
3659
3703
  ownsMcpManager = true;
3660
3704
  registerOwnedMcpManagerCleanup(owned);
@@ -4236,8 +4280,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
4236
4280
  {
4237
4281
  managedFallback: defaultModelEntries.length > 1,
4238
4282
  canonicalSessionId: providerSessionId,
4239
- isCredentialUnavailable: provider =>
4240
- authStorage.hasSessionCredentialUnavailable(provider, credentialSessionId),
4283
+ isCredentialUnavailable: isCredentialPinBlocking,
4241
4284
  ...(persistedProfileOwnsDefault ? { aliasIntent: "preset-equivalent" as const } : {}),
4242
4285
  },
4243
4286
  );