@gajae-code/coding-agent 0.18.6 → 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 (58) hide show
  1. package/CHANGELOG.md +39 -0
  2. package/dist/types/capability/types.d.ts +4 -0
  3. package/dist/types/config/settings.d.ts +4 -0
  4. package/dist/types/discovery/helpers.d.ts +2 -2
  5. package/dist/types/runtime-mcp/elicitation.d.ts +12 -0
  6. package/dist/types/sdk/broker/lifecycle.d.ts +9 -0
  7. package/dist/types/sdk/broker/task-artifact-owner-validation.d.ts +55 -0
  8. package/dist/types/sdk/bus/telegram-daemon.d.ts +3 -2
  9. package/dist/types/sdk/host/host.d.ts +6 -0
  10. package/dist/types/sdk/router/session-router.d.ts +7 -0
  11. package/dist/types/session/internal/managed-session-storage.d.ts +11 -1
  12. package/dist/types/session/internal/task-artifact-owner-access.d.ts +17 -0
  13. package/dist/types/session/internal/task-artifact-owner-transcript.d.ts +3 -0
  14. package/dist/types/session/session-retirement.d.ts +47 -0
  15. package/dist/types/session/session-storage.d.ts +0 -46
  16. package/dist/types/session/session-transcript-header.d.ts +2 -0
  17. package/dist/types/session/task-artifact-owner-codec.d.ts +107 -0
  18. package/dist/types/session/task-artifact-owner-retirement.d.ts +7 -0
  19. package/dist/types/session/task-artifact-owner.d.ts +10 -0
  20. package/package.json +7 -7
  21. package/scripts/resident-memory-bench.ts +174 -20
  22. package/src/capability/index.ts +1 -0
  23. package/src/capability/types.ts +4 -0
  24. package/src/cli/config-cli.ts +8 -18
  25. package/src/cli-main.ts +7 -8
  26. package/src/commands/sdk.ts +112 -21
  27. package/src/config/settings.ts +44 -4
  28. package/src/discovery/helpers.ts +9 -3
  29. package/src/discovery/ssh.ts +8 -3
  30. package/src/gjc-runtime/gc-runtime.ts +2 -1
  31. package/src/hindsight/config.ts +9 -3
  32. package/src/internal-urls/docs-index.generated.ts +2 -2
  33. package/src/modes/acp/acp-agent.ts +75 -3
  34. package/src/runtime-mcp/client.ts +13 -4
  35. package/src/runtime-mcp/elicitation.ts +142 -0
  36. package/src/sdk/broker/broker.ts +8 -1
  37. package/src/sdk/broker/lifecycle.ts +109 -7
  38. package/src/sdk/broker/task-artifact-owner-validation.ts +259 -0
  39. package/src/sdk/bus/chat-daemon-control.ts +2 -2
  40. package/src/sdk/bus/chat-daemon-runtime.ts +3 -0
  41. package/src/sdk/bus/index.ts +3 -3
  42. package/src/sdk/bus/telegram-daemon.ts +4 -2
  43. package/src/sdk/host/host.ts +6 -0
  44. package/src/sdk/host/session-runtime.test.ts +43 -2
  45. package/src/sdk/host/session-runtime.ts +2 -2
  46. package/src/sdk/mcp/server.ts +15 -3
  47. package/src/sdk/router/session-router.ts +19 -2
  48. package/src/sdk/session.ts +14 -0
  49. package/src/session/agent-session.ts +57 -6
  50. package/src/session/internal/managed-session-storage.ts +107 -1
  51. package/src/session/internal/task-artifact-owner-access.ts +229 -0
  52. package/src/session/internal/task-artifact-owner-transcript.ts +68 -0
  53. package/src/session/session-retirement.ts +155 -0
  54. package/src/session/session-storage.ts +1 -160
  55. package/src/session/session-transcript-header.ts +17 -0
  56. package/src/session/task-artifact-owner-codec.ts +578 -0
  57. package/src/session/task-artifact-owner-retirement.ts +475 -0
  58. package/src/session/task-artifact-owner.ts +289 -0
@@ -0,0 +1,259 @@
1
+ import type { NativeExactUnlinkResult } from "@gajae-code/natives";
2
+ import {
3
+ assertSessionRoot,
4
+ parseTaskArtifactOwnerDeletionEvidence,
5
+ parseTaskArtifactOwnerRetirementContinuation,
6
+ parseTaskArtifactOwnerRetirementOutcome,
7
+ type TaskArtifactOwnerDeletionEvidence,
8
+ type TaskArtifactOwnerRetirementContinuation,
9
+ type TaskArtifactOwnerRetirementOutcome,
10
+ type TaskArtifactOwnerStorageContext,
11
+ } from "../../session/task-artifact-owner-codec";
12
+
13
+ export type BrokerCleanupPhase = "artifacts" | "transcript" | "metadata" | "lifecycle";
14
+
15
+ /** Exact owner-disposition wire shape. Evidence and continuation are supplied separately by cleanup replay. */
16
+ export type BrokerTaskArtifactOwnerRetirementDisposition =
17
+ | { readonly kind: "completed"; readonly nativeOutcome?: NativeExactUnlinkResult }
18
+ | {
19
+ readonly kind: "payload_retired";
20
+ readonly namespace: "retained";
21
+ readonly nativeOutcome: NativeExactUnlinkResult;
22
+ }
23
+ | { readonly kind: "cleanup_pending"; readonly nativeOutcome?: NativeExactUnlinkResult }
24
+ | { readonly kind: "uncertain"; readonly reason: string; readonly nativeOutcome?: NativeExactUnlinkResult };
25
+
26
+ /** Owner-only projection of durable cleanup state accepted by the pure validator. */
27
+ export interface BrokerTaskArtifactOwnerCleanupFields {
28
+ readonly taskArtifactOwnerDeletionEvidence?: unknown;
29
+ readonly taskArtifactOwnerRetirementContinuation?: unknown;
30
+ readonly taskArtifactOwnerRetirementOutcome?: unknown;
31
+ readonly taskArtifactOwnerPayloadRetired?: unknown;
32
+ readonly taskArtifactOwnerNamespaceRetained?: unknown;
33
+ readonly taskArtifactOwnerRetired?: unknown;
34
+ readonly taskArtifactOwnerTranscriptDeleted?: unknown;
35
+ readonly taskArtifactOwnerCleanupError?: unknown;
36
+ }
37
+
38
+ /** Fully decoded owner-only cleanup state for an authorized session and phase. */
39
+ export interface BrokerTaskArtifactOwnerCleanupValidation {
40
+ readonly deletionEvidence?: TaskArtifactOwnerDeletionEvidence;
41
+ readonly retirementContinuation?: TaskArtifactOwnerRetirementContinuation;
42
+ readonly retirementOutcome?: TaskArtifactOwnerRetirementOutcome;
43
+ readonly payloadRetired?: true;
44
+ readonly namespaceRetained?: true;
45
+ readonly retired?: true;
46
+ readonly transcriptDeleted?: true;
47
+ readonly cleanupDiagnostic?: string;
48
+ }
49
+
50
+ function isRecord(value: unknown): value is Record<string, unknown> {
51
+ return typeof value === "object" && value !== null && !Array.isArray(value);
52
+ }
53
+
54
+ function hasExactKeys(value: Record<string, unknown>, keys: readonly string[]): boolean {
55
+ return Object.keys(value).length === keys.length && keys.every(key => Object.hasOwn(value, key));
56
+ }
57
+
58
+ function invalidDisposition(): never {
59
+ throw new Error("task_artifact_owner_retirement_disposition_invalid");
60
+ }
61
+
62
+ /**
63
+ * Decode the owner-only wire disposition using its exact trusted evidence and continuation.
64
+ * A decoded `completed` value is historical DTO data, not proof of physical retirement or writer closure.
65
+ */
66
+ export function parseBrokerTaskArtifactOwnerRetirementDisposition(
67
+ value: unknown,
68
+ context: TaskArtifactOwnerStorageContext,
69
+ evidence: TaskArtifactOwnerDeletionEvidence,
70
+ continuation: TaskArtifactOwnerRetirementContinuation | undefined,
71
+ ): TaskArtifactOwnerRetirementOutcome {
72
+ assertSessionRoot(context);
73
+ if (!isRecord(value)) return invalidDisposition();
74
+ const kind = value.kind;
75
+ let expectedKeys: readonly string[];
76
+ switch (kind) {
77
+ case "completed":
78
+ case "cleanup_pending":
79
+ expectedKeys = Object.hasOwn(value, "nativeOutcome") ? ["kind", "nativeOutcome"] : ["kind"];
80
+ break;
81
+ case "payload_retired":
82
+ expectedKeys = ["kind", "namespace", "nativeOutcome"];
83
+ break;
84
+ case "uncertain":
85
+ expectedKeys = Object.hasOwn(value, "nativeOutcome")
86
+ ? ["kind", "reason", "nativeOutcome"]
87
+ : ["kind", "reason"];
88
+ break;
89
+ default:
90
+ return invalidDisposition();
91
+ }
92
+ if (!hasExactKeys(value, expectedKeys)) return invalidDisposition();
93
+ const reconstructed = {
94
+ ...value,
95
+ evidence,
96
+ ...(continuation !== undefined ? { continuation } : {}),
97
+ };
98
+ return parseTaskArtifactOwnerRetirementOutcome(context, evidence, reconstructed);
99
+ }
100
+
101
+ function serializeNativeOutcome(outcome: NativeExactUnlinkResult): NativeExactUnlinkResult {
102
+ return Object.freeze({
103
+ ok: outcome.ok,
104
+ ...(outcome.code !== undefined ? { code: outcome.code } : {}),
105
+ ...(outcome.detachedPath !== undefined ? { detachedPath: outcome.detachedPath } : {}),
106
+ ...(outcome.retainedSuccessorPath !== undefined ? { retainedSuccessorPath: outcome.retainedSuccessorPath } : {}),
107
+ ...(outcome.retainedPlaceholderPath !== undefined
108
+ ? { retainedPlaceholderPath: outcome.retainedPlaceholderPath }
109
+ : {}),
110
+ ...(outcome.retainedUnknownPath !== undefined ? { retainedUnknownPath: outcome.retainedUnknownPath } : {}),
111
+ ...(outcome.payloadDurable !== undefined ? { payloadDurable: outcome.payloadDurable } : {}),
112
+ ...(outcome.windowsErrorCode !== undefined ? { windowsErrorCode: outcome.windowsErrorCode } : {}),
113
+ }) as NativeExactUnlinkResult;
114
+ }
115
+
116
+ /** Project an already validated internal outcome to the exact broker wire disposition. */
117
+ export function serializeBrokerTaskArtifactOwnerRetirementDisposition(
118
+ outcome: TaskArtifactOwnerRetirementOutcome,
119
+ ): BrokerTaskArtifactOwnerRetirementDisposition {
120
+ switch (outcome.kind) {
121
+ case "completed":
122
+ return Object.freeze({
123
+ kind: outcome.kind,
124
+ ...(outcome.nativeOutcome ? { nativeOutcome: serializeNativeOutcome(outcome.nativeOutcome) } : {}),
125
+ });
126
+ case "payload_retired":
127
+ return Object.freeze({
128
+ kind: outcome.kind,
129
+ namespace: outcome.namespace,
130
+ nativeOutcome: serializeNativeOutcome(outcome.nativeOutcome),
131
+ });
132
+ case "cleanup_pending":
133
+ return Object.freeze({
134
+ kind: outcome.kind,
135
+ ...(outcome.nativeOutcome ? { nativeOutcome: serializeNativeOutcome(outcome.nativeOutcome) } : {}),
136
+ });
137
+ case "uncertain":
138
+ return Object.freeze({
139
+ kind: outcome.kind,
140
+ reason: outcome.reason,
141
+ ...(outcome.nativeOutcome ? { nativeOutcome: serializeNativeOutcome(outcome.nativeOutcome) } : {}),
142
+ });
143
+ default: {
144
+ const exhaustive: never = outcome;
145
+ throw new Error(`task_artifact_owner_retirement_outcome_unknown:${String(exhaustive)}`);
146
+ }
147
+ }
148
+ }
149
+
150
+ const OWNER_CLEANUP_KEYS = new Set([
151
+ "taskArtifactOwnerDeletionEvidence",
152
+ "taskArtifactOwnerRetirementContinuation",
153
+ "taskArtifactOwnerRetirementOutcome",
154
+ "taskArtifactOwnerPayloadRetired",
155
+ "taskArtifactOwnerNamespaceRetained",
156
+ "taskArtifactOwnerRetired",
157
+ "taskArtifactOwnerTranscriptDeleted",
158
+ "taskArtifactOwnerCleanupError",
159
+ ]);
160
+
161
+ /**
162
+ * Strictly decode the owner-field projection of cleanup state against trusted session, scope context, phase,
163
+ * and artifact-removal state. Empty/legacy owner fields decode to an empty result. A decoded completion
164
+ * remains historical data: any later effect must separately verify current physical absence, fresh scrub
165
+ * proof, native success and current owner-parent/side-path authority before it mutates storage or transcripts.
166
+ */
167
+ export function decodeBrokerTaskArtifactOwnerCleanupFields(
168
+ value: unknown,
169
+ context: TaskArtifactOwnerStorageContext,
170
+ expectedSessionId: string,
171
+ phase: BrokerCleanupPhase,
172
+ artifactsRemoved: boolean | undefined,
173
+ ): BrokerTaskArtifactOwnerCleanupValidation {
174
+ assertSessionRoot(context);
175
+ if (!isRecord(value) || Object.keys(value).some(key => !OWNER_CLEANUP_KEYS.has(key)))
176
+ throw new Error("task_artifact_owner_cleanup_fields_invalid");
177
+ if (expectedSessionId.length === 0 || (artifactsRemoved !== undefined && typeof artifactsRemoved !== "boolean"))
178
+ throw new Error("task_artifact_owner_cleanup_fields_invalid");
179
+
180
+ const has = (key: string) => Object.hasOwn(value, key);
181
+ const trueFlag = (key: string): true | undefined => {
182
+ if (!has(key)) return undefined;
183
+ if (value[key] !== true) throw new Error("task_artifact_owner_cleanup_flags_invalid");
184
+ return true;
185
+ };
186
+ let deletionEvidence: TaskArtifactOwnerDeletionEvidence | undefined;
187
+ if (has("taskArtifactOwnerDeletionEvidence")) {
188
+ deletionEvidence = parseTaskArtifactOwnerDeletionEvidence(value.taskArtifactOwnerDeletionEvidence);
189
+ if (deletionEvidence.sessionId !== expectedSessionId)
190
+ throw new Error("task_artifact_owner_cleanup_session_mismatch");
191
+ }
192
+ let retirementContinuation: TaskArtifactOwnerRetirementContinuation | undefined;
193
+ if (has("taskArtifactOwnerRetirementContinuation")) {
194
+ if (!deletionEvidence) throw new Error("task_artifact_owner_cleanup_evidence_missing");
195
+ retirementContinuation = parseTaskArtifactOwnerRetirementContinuation(
196
+ context,
197
+ deletionEvidence,
198
+ value.taskArtifactOwnerRetirementContinuation,
199
+ );
200
+ }
201
+ let retirementOutcome: TaskArtifactOwnerRetirementOutcome | undefined;
202
+ if (has("taskArtifactOwnerRetirementOutcome")) {
203
+ if (!deletionEvidence) throw new Error("task_artifact_owner_cleanup_evidence_missing");
204
+ retirementOutcome = parseBrokerTaskArtifactOwnerRetirementDisposition(
205
+ value.taskArtifactOwnerRetirementOutcome,
206
+ context,
207
+ deletionEvidence,
208
+ retirementContinuation,
209
+ );
210
+ }
211
+
212
+ const payloadRetired = trueFlag("taskArtifactOwnerPayloadRetired");
213
+ const namespaceRetained = trueFlag("taskArtifactOwnerNamespaceRetained");
214
+ const retired = trueFlag("taskArtifactOwnerRetired");
215
+ const transcriptDeleted = trueFlag("taskArtifactOwnerTranscriptDeleted");
216
+ let cleanupDiagnostic: string | undefined;
217
+ if (has("taskArtifactOwnerCleanupError")) {
218
+ const diagnostic = value.taskArtifactOwnerCleanupError;
219
+ if (typeof diagnostic !== "string" || diagnostic.length === 0 || diagnostic.length > 4096)
220
+ throw new Error("task_artifact_owner_cleanup_diagnostic_invalid");
221
+ cleanupDiagnostic = diagnostic;
222
+ }
223
+
224
+ if (
225
+ (retired !== undefined && (payloadRetired !== undefined || namespaceRetained !== undefined)) ||
226
+ (retired !== undefined && retirementOutcome?.kind !== "completed") ||
227
+ (payloadRetired !== undefined) !== (namespaceRetained !== undefined) ||
228
+ ((retired !== undefined ||
229
+ payloadRetired !== undefined ||
230
+ namespaceRetained !== undefined ||
231
+ transcriptDeleted !== undefined) &&
232
+ (!deletionEvidence || !retirementOutcome)) ||
233
+ (transcriptDeleted !== undefined &&
234
+ (phase !== "artifacts" || artifactsRemoved !== true || retirementOutcome === undefined))
235
+ )
236
+ throw new Error("task_artifact_owner_cleanup_state_invalid");
237
+
238
+ if (retirementOutcome) {
239
+ if (
240
+ (retired !== undefined && retirementOutcome.kind !== "completed") ||
241
+ (retirementOutcome.kind === "completed" && retired === undefined) ||
242
+ (payloadRetired !== undefined &&
243
+ (retirementOutcome.kind !== "payload_retired" || retirementContinuation === undefined)) ||
244
+ (retirementOutcome.kind === "payload_retired" && payloadRetired === undefined)
245
+ )
246
+ throw new Error("task_artifact_owner_cleanup_disposition_mismatch");
247
+ }
248
+
249
+ return Object.freeze({
250
+ deletionEvidence,
251
+ retirementContinuation,
252
+ retirementOutcome,
253
+ payloadRetired,
254
+ namespaceRetained,
255
+ retired,
256
+ transcriptDeleted,
257
+ cleanupDiagnostic,
258
+ });
259
+ }
@@ -141,8 +141,8 @@ export type ChatDaemonAction = "stop" | "reload";
141
141
  * crashes, recovery, and attachment retirement cannot replay ambiguous work.
142
142
  */
143
143
  export const CHAT_DAEMON_GENERATIONS: Readonly<Record<ChatDaemonKind, number>> = {
144
- discord: 82,
145
- slack: 89,
144
+ discord: 83,
145
+ slack: 90,
146
146
  };
147
147
 
148
148
  export function chatDaemonGeneration(kind: ChatDaemonKind): number {
@@ -318,6 +318,9 @@ export class ChatDaemonRuntime {
318
318
  this.#router = new SessionRouter({
319
319
  agentDir: input.agentDir,
320
320
  observer: true,
321
+ // A lean final answer carries no messageRef, so the raw native copy of a
322
+ // positioned frame has no identity to collapse onto and would post twice.
323
+ positionedNotificationEffects: true,
321
324
  correlateFrame,
322
325
  deps: {
323
326
  ...deps.routerDeps,
@@ -105,6 +105,7 @@ import {
105
105
  canDeliverSdkEvent,
106
106
  createSdkSurfaceFactory,
107
107
  masterAttestationForEffectiveHost,
108
+ POSITIONED_NOTIFICATION_EFFECTS_CAPABILITY,
108
109
  reattestMasterSessionIdentity,
109
110
  SESSION_HOST_OBSERVER_CAPABILITY,
110
111
  type SessionSdkHost,
@@ -204,7 +205,6 @@ import {
204
205
  ASK_SELECTED_ACK_CAPABILITY,
205
206
  type EnsureDaemonResult,
206
207
  ensureTelegramDaemonRunningDetailed,
207
- POSITIONED_NOTIFICATION_EFFECTS_CAPABILITY,
208
208
  } from "./telegram-daemon";
209
209
 
210
210
  export type {
@@ -3722,8 +3722,8 @@ function sdkControlSurface(
3722
3722
  return unavailable("model.cycle", "no session model-cycle seam is installed")();
3723
3723
  return { changed: (await ctx.cycleModel()) !== undefined };
3724
3724
  },
3725
- setThinking: level => {
3726
- api.setThinkingLevel(level as ThinkingLevel);
3725
+ setThinking: async level => {
3726
+ await api.setThinkingLevelForControl(level as ThinkingLevel, false);
3727
3727
  return { changed: true };
3728
3728
  },
3729
3729
  cycleThinking: () => {
@@ -71,6 +71,7 @@ export {
71
71
  TELEGRAM_TRANSPORT_GENERATION,
72
72
  } from "./telegram-daemon-contract";
73
73
 
74
+ import { POSITIONED_NOTIFICATION_EFFECTS_CAPABILITY } from "../host/host";
74
75
  import {
75
76
  type AgentDirSessionLifecycleService,
76
77
  createSessionLifecycleService,
@@ -303,8 +304,9 @@ export const ASK_CONTROLS_CAPABILITY = "ask_controls_v1";
303
304
  export const TOOL_ACTIVITY_CAPABILITY = "tool_activity_v2";
304
305
  /** Receive-only compatibility capability for pre-v2 hosts. */
305
306
  export const LEGACY_TOOL_ACTIVITY_CAPABILITY = "tool_activity_v1";
306
- /** Opts notification adapters into positioned-only delivery for matching live effects. */
307
- export const POSITIONED_NOTIFICATION_EFFECTS_CAPABILITY = "positioned_notification_effects_v1";
307
+ /** Public `sdk/bus/telegram-daemon` path for the canonical `sdk/host` capability token. */
308
+ export { POSITIONED_NOTIFICATION_EFFECTS_CAPABILITY };
309
+
308
310
  type ToolActivityCapability = "v1" | "v2";
309
311
 
310
312
  function negotiateToolActivityCapability(
@@ -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;
@@ -4372,8 +4372,8 @@ function createControlSurface(
4372
4372
  },
4373
4373
  setModelProfile: id => (ctx.setModelProfile ? ctx.setModelProfile(id) : unavailable("model.profile.set")()),
4374
4374
  cycleModel: () => (ctx.cycleModel ? ctx.cycleModel() : unavailable("model.cycle")()),
4375
- setThinking: level => {
4376
- api.setThinkingLevel(level as never);
4375
+ setThinking: async level => {
4376
+ await api.setThinkingLevelForControl(level as ThinkingLevel, false);
4377
4377
  return { changed: true };
4378
4378
  },
4379
4379
  cycleThinking: () =>
@@ -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")
@@ -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,
@@ -291,6 +295,13 @@ export interface SessionRouterOptions {
291
295
  attachFilter?: (sessionId: string) => boolean;
292
296
  /** Marks default SDK clients as notification-only observers; demanding by default. */
293
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;
294
305
  deps?: SessionRouterDeps;
295
306
  /** Runtime-specific identity validation; Router supplies a conservative fallback. */
296
307
  correlateFrame?: SessionRouterFrameCorrelator;
@@ -619,6 +630,7 @@ export class SessionRouter {
619
630
  readonly #sessionIds: ReadonlySet<string> | undefined;
620
631
  readonly #attachFilter: ((sessionId: string) => boolean) | undefined;
621
632
  readonly #observer: boolean;
633
+ readonly #positionedNotificationEffects: boolean;
622
634
  readonly #deps: SessionRouterDeps;
623
635
  readonly #correlateFrame: SessionRouterFrameCorrelator;
624
636
  readonly #index: SessionIndex;
@@ -662,6 +674,7 @@ export class SessionRouter {
662
674
  this.#sessionIds = options.sessionIds === undefined ? undefined : new Set(options.sessionIds);
663
675
  this.#attachFilter = options.attachFilter;
664
676
  this.#observer = options.observer === true;
677
+ this.#positionedNotificationEffects = options.positionedNotificationEffects === true;
665
678
  this.#deps = options.deps ?? {};
666
679
  this.#correlateFrame = options.correlateFrame ?? fallbackCorrelation;
667
680
  this.#index = this.#deps.createIndex?.(options.agentDir) ?? new DefaultSessionIndex(options.agentDir);
@@ -1530,9 +1543,13 @@ export class SessionRouter {
1530
1543
  endpointMtimeMs,
1531
1544
  });
1532
1545
  } else {
1546
+ const capabilities = [
1547
+ ...(this.#observer ? [SESSION_HOST_OBSERVER_CAPABILITY, TURN_STREAM_CAPABILITY] : []),
1548
+ ...(this.#positionedNotificationEffects ? [POSITIONED_NOTIFICATION_EFFECTS_CAPABILITY] : []),
1549
+ ];
1533
1550
  const defaultClient = new SdkClient(endpoint.url, endpoint.token, {
1534
1551
  ...ACP_SESSION_RECONNECT,
1535
- ...(this.#observer ? { capabilities: [SESSION_HOST_OBSERVER_CAPABILITY, TURN_STREAM_CAPABILITY] } : {}),
1552
+ ...(capabilities.length > 0 ? { capabilities } : {}),
1536
1553
  });
1537
1554
  transport = defaultClient;
1538
1555
  connection = defaultClient.connect().then(() => defaultClient);
@@ -161,6 +161,7 @@ import {
161
161
  type MCPToolCache,
162
162
  resolveMCPToolCache,
163
163
  } from "../runtime-mcp";
164
+ import { createMCPFormInputHandler } from "../runtime-mcp/elicitation";
164
165
  import type { MCPLoadResult } from "../runtime-mcp/manager";
165
166
  import { MCP_STARTUP_WAIT_GRACE_MS } from "../runtime-mcp/startup-policy";
166
167
  import type { MCPServerConfig } from "../runtime-mcp/types";
@@ -2653,7 +2654,19 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
2653
2654
  let ownedMcpManagerTools: readonly CustomTool[] = [];
2654
2655
  let publishOwnedMcpTools = false;
2655
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
+ };
2656
2668
  const wireMcpManagerCallbacks = (manager: MCPManager): void => {
2669
+ installMcpInputHandler(manager);
2657
2670
  manager.setOnPromptsChanged(serverName => {
2658
2671
  const promptCommands = buildMCPPromptCommands(manager);
2659
2672
  session.setMCPPromptCommands(promptCommands);
@@ -3685,6 +3698,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
3685
3698
  : {}),
3686
3699
  });
3687
3700
  owned.setAuthStorage(authStorage);
3701
+ installMcpInputHandler(owned);
3688
3702
  mcpManager = owned;
3689
3703
  ownsMcpManager = true;
3690
3704
  registerOwnedMcpManagerCleanup(owned);