@gajae-code/coding-agent 0.13.0 → 0.13.1

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 (113) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/dist/types/config/file-lock.d.ts +2 -0
  3. package/dist/types/config/keybindings.d.ts +2 -2
  4. package/dist/types/config/model-registry.d.ts +4 -0
  5. package/dist/types/config/models-config-schema.d.ts +6 -0
  6. package/dist/types/config/settings-schema.d.ts +17 -5
  7. package/dist/types/config/settings.d.ts +10 -1
  8. package/dist/types/edit/streaming.d.ts +5 -5
  9. package/dist/types/eval/py/kernel.d.ts +2 -2
  10. package/dist/types/eval/py/runtime.d.ts +5 -1
  11. package/dist/types/gjc-runtime/session-state-sidecar.d.ts +3 -0
  12. package/dist/types/gjc-runtime/tmux-sessions.d.ts +3 -1
  13. package/dist/types/modes/components/model-selector.d.ts +3 -2
  14. package/dist/types/runtime/process-lifecycle.d.ts +12 -31
  15. package/dist/types/sdk/broker/identity.d.ts +2 -1
  16. package/dist/types/sdk/broker/lifecycle-ledger.d.ts +17 -1
  17. package/dist/types/sdk/broker/lifecycle.d.ts +11 -6
  18. package/dist/types/sdk/broker/startup-failure.d.ts +32 -0
  19. package/dist/types/sdk/bus/chat-daemon-control.d.ts +2 -1
  20. package/dist/types/sdk/bus/conversation-store.d.ts +1 -0
  21. package/dist/types/sdk/bus/index.d.ts +4 -0
  22. package/dist/types/sdk/bus/kind-aware-reconciliation.d.ts +10 -1
  23. package/dist/types/sdk/bus/reconciliation-store.d.ts +2 -0
  24. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +11 -2
  25. package/dist/types/sdk/bus/telegram-daemon.d.ts +17 -1
  26. package/dist/types/sdk/host/query/cursor.d.ts +4 -0
  27. package/dist/types/sdk/host/query/handlers.d.ts +6 -0
  28. package/dist/types/sdk/host/session-runtime.d.ts +7 -6
  29. package/dist/types/sdk/host/websocket-transport.d.ts +1 -1
  30. package/dist/types/sdk/index.d.ts +1 -0
  31. package/dist/types/sdk/turn-result.d.ts +37 -0
  32. package/dist/types/session/agent-session.d.ts +8 -11
  33. package/dist/types/session/blob-store.d.ts +6 -3
  34. package/dist/types/slash-commands/helpers/fast-status-report.d.ts +12 -3
  35. package/dist/types/task/executor.d.ts +4 -7
  36. package/dist/types/tools/index.d.ts +1 -1
  37. package/dist/types/tools/read.d.ts +1 -1
  38. package/dist/types/tools/search.d.ts +1 -1
  39. package/dist/types/utils/edit-mode.d.ts +69 -0
  40. package/package.json +8 -8
  41. package/scripts/generate-hotkeys-docs.ts +8 -6
  42. package/src/commands/sdk.ts +23 -2
  43. package/src/config/file-lock.ts +25 -5
  44. package/src/config/keybindings.ts +4 -2
  45. package/src/config/model-registry.ts +20 -0
  46. package/src/config/models-config-schema.ts +1 -0
  47. package/src/config/settings-schema.ts +29 -22
  48. package/src/config/settings.ts +42 -10
  49. package/src/coordinator-mcp/server.ts +84 -106
  50. package/src/edit/index.ts +5 -19
  51. package/src/edit/renderer.ts +3 -2
  52. package/src/edit/streaming.ts +28 -15
  53. package/src/eval/py/executor.ts +108 -10
  54. package/src/eval/py/kernel.ts +86 -12
  55. package/src/eval/py/runtime.ts +200 -16
  56. package/src/exec/bash-executor.ts +36 -37
  57. package/src/gjc-runtime/gc-runtime.ts +25 -3
  58. package/src/gjc-runtime/managed-owner-supervisor.ts +27 -1
  59. package/src/gjc-runtime/session-state-sidecar.ts +110 -4
  60. package/src/gjc-runtime/tmux-sessions.ts +34 -5
  61. package/src/internal-urls/docs-index.generated.ts +6 -6
  62. package/src/modes/acp/acp-agent.ts +10 -2
  63. package/src/modes/components/model-selector.ts +28 -8
  64. package/src/modes/controllers/plan-mode-controller.ts +3 -3
  65. package/src/modes/controllers/selector-controller.ts +4 -2
  66. package/src/runtime/process-lifecycle.ts +126 -27
  67. package/src/runtime-mcp/transports/stdio.ts +11 -6
  68. package/src/sdk/acp/adapter.ts +18 -10
  69. package/src/sdk/broker/broker.ts +55 -4
  70. package/src/sdk/broker/ensure.ts +10 -7
  71. package/src/sdk/broker/identity.ts +10 -0
  72. package/src/sdk/broker/lifecycle-ledger.ts +56 -6
  73. package/src/sdk/broker/lifecycle.ts +53 -10
  74. package/src/sdk/broker/session-index.ts +28 -11
  75. package/src/sdk/broker/startup-failure.ts +101 -0
  76. package/src/sdk/broker/transport.ts +1 -0
  77. package/src/sdk/bus/chat-daemon-control.ts +4 -3
  78. package/src/sdk/bus/chat-daemon-runtime.ts +8 -1
  79. package/src/sdk/bus/conversation-store.ts +171 -50
  80. package/src/sdk/bus/index.ts +101 -47
  81. package/src/sdk/bus/kind-aware-reconciliation.ts +52 -2
  82. package/src/sdk/bus/notification-service.ts +3 -3
  83. package/src/sdk/bus/reconciliation-store.ts +16 -0
  84. package/src/sdk/bus/telegram-daemon-contract.ts +11 -2
  85. package/src/sdk/bus/telegram-daemon.ts +131 -29
  86. package/src/sdk/bus/topic-registry.ts +38 -9
  87. package/src/sdk/cli/session-cli.ts +20 -7
  88. package/src/sdk/client/discovery.ts +23 -2
  89. package/src/sdk/host/query/cursor.ts +4 -0
  90. package/src/sdk/host/query/handlers.ts +99 -100
  91. package/src/sdk/host/sdk-surface-parity.test.ts +1 -1
  92. package/src/sdk/host/session-runtime.test.ts +98 -1
  93. package/src/sdk/host/session-runtime.ts +57 -15
  94. package/src/sdk/host/websocket-transport.lifecycle.test.ts +192 -2
  95. package/src/sdk/host/websocket-transport.ts +54 -30
  96. package/src/sdk/index.ts +1 -0
  97. package/src/sdk/mcp/server.ts +19 -6
  98. package/src/sdk/protocol/operation-inventory.generated.json +1 -1
  99. package/src/sdk/protocol/operation-registry.ts +8 -12
  100. package/src/sdk/session.ts +5 -14
  101. package/src/sdk/turn-result.ts +53 -0
  102. package/src/session/agent-session.ts +112 -69
  103. package/src/session/blob-store.ts +91 -21
  104. package/src/session/session-manager.ts +408 -81
  105. package/src/session/session-storage.ts +68 -10
  106. package/src/slash-commands/helpers/fast-status-report.ts +24 -8
  107. package/src/task/executor.ts +25 -8
  108. package/src/task/index.ts +0 -2
  109. package/src/tools/index.ts +1 -1
  110. package/src/tools/read.ts +12 -11
  111. package/src/tools/search.ts +7 -7
  112. package/src/utils/edit-mode.ts +180 -9
  113. package/src/web/search/providers/insane.ts +55 -1
@@ -2,6 +2,7 @@ import { describe, expect, test } from "bun:test";
2
2
  import * as fs from "node:fs/promises";
3
3
  import * as os from "node:os";
4
4
  import * as path from "node:path";
5
+ import { SdkClient } from "../client/client";
5
6
  import { SessionSdkSessionRuntime, type SessionSdkTransport } from "./session-runtime";
6
7
  import { createSdkWebSocketTransport, type SdkWebSocketTransportDependencies } from "./websocket-transport";
7
8
 
@@ -39,11 +40,117 @@ describe("SDK WebSocket transport lifecycle", () => {
39
40
  const endpoints = await Promise.all([transport.start(), transport.start(), transport.start()]);
40
41
  expect(new Set(endpoints.map(endpoint => endpoint.url)).size).toBe(1);
41
42
  const endpointPath = path.join(stateRoot, "sdk", "concurrent-start.json");
42
- expect(JSON.parse(await fs.readFile(endpointPath, "utf8")).url).toBe(endpoints[0]?.url);
43
+ expect(JSON.parse(await fs.readFile(endpointPath, "utf8"))).toMatchObject({
44
+ sessionId: "concurrent-start",
45
+ url: endpoints[0]?.url,
46
+ });
43
47
  await transport.stop();
44
48
  await expect(fs.stat(endpointPath)).rejects.toMatchObject({ code: "ENOENT" });
45
49
  await fs.rm(stateRoot, { recursive: true, force: true });
46
50
  });
51
+ test("publishes a complete mode-600 endpoint through one atomic rename", async () => {
52
+ const stateRoot = await tempStateRoot();
53
+ const endpointPath = path.join(stateRoot, "sdk", "atomic-publication.json");
54
+ const renameEntered = Promise.withResolvers<void>();
55
+ const releaseRename = Promise.withResolvers<void>();
56
+ const transport = await createSdkWebSocketTransport({
57
+ sessionId: "atomic-publication",
58
+ stateRoot,
59
+ token: "secret-token",
60
+ filesystem: {
61
+ mkdir: fs.mkdir,
62
+ writeFile: fs.writeFile,
63
+ chmod: fs.chmod,
64
+ rename: async (from, to) => {
65
+ expect(to).toBe(endpointPath);
66
+ expect((await fs.stat(from)).mode & 0o777).toBe(0o600);
67
+ expect(JSON.parse(await fs.readFile(from, "utf8"))).toMatchObject({
68
+ sessionId: "atomic-publication",
69
+ token: "secret-token",
70
+ });
71
+ renameEntered.resolve();
72
+ await releaseRename.promise;
73
+ await fs.rename(from, to);
74
+ },
75
+ readFile: fs.readFile,
76
+ rm: fs.rm,
77
+ },
78
+ });
79
+ const start = transport.start();
80
+ try {
81
+ await renameEntered.promise;
82
+ await expect(fs.stat(endpointPath)).rejects.toMatchObject({ code: "ENOENT" });
83
+ expect((await fs.readdir(path.dirname(endpointPath))).filter(file => file.endsWith(".tmp"))).toHaveLength(1);
84
+ releaseRename.resolve();
85
+ await start;
86
+ expect(JSON.parse(await fs.readFile(endpointPath, "utf8"))).toMatchObject({
87
+ sessionId: "atomic-publication",
88
+ token: "secret-token",
89
+ });
90
+ } finally {
91
+ releaseRename.resolve();
92
+ await start.catch(() => undefined);
93
+ await transport.stop().catch(() => undefined);
94
+ await fs.rm(stateRoot, { recursive: true, force: true });
95
+ }
96
+ });
97
+ test("six isolated roots publish unique endpoints that accept metadata and prompt controls", async () => {
98
+ const roots = await Promise.all(Array.from({ length: 6 }, () => tempStateRoot()));
99
+ const transports = await Promise.all(
100
+ roots.map((stateRoot, index) =>
101
+ createSdkWebSocketTransport({
102
+ sessionId: `isolated-lane-${index + 1}`,
103
+ stateRoot,
104
+ token: `token-${index + 1}`,
105
+ }),
106
+ ),
107
+ );
108
+ const frameDisposers = transports.map(transport =>
109
+ transport.onFrame((connectionId, frame) => {
110
+ if (frame.type !== "query_request" && frame.type !== "control_request") return;
111
+ void transport.sendFrame(connectionId, {
112
+ type: frame.type === "query_request" ? "query_response" : "control_response",
113
+ id: frame.id,
114
+ ok: true,
115
+ result: frame.type === "query_request" ? { sessionId: transport.sessionId } : { accepted: true },
116
+ });
117
+ }),
118
+ );
119
+ const clients: SdkClient[] = [];
120
+ try {
121
+ const endpoints = await Promise.all(transports.map(transport => transport.start()));
122
+ expect(new Set(endpoints.map(endpoint => endpoint.url)).size).toBe(6);
123
+ await Promise.all(
124
+ roots.map(async (stateRoot, index) => {
125
+ const sessionId = `isolated-lane-${index + 1}`;
126
+ const record = JSON.parse(await fs.readFile(path.join(stateRoot, "sdk", `${sessionId}.json`), "utf8"));
127
+ expect(record).toMatchObject({ sessionId, url: endpoints[index]?.url, token: `token-${index + 1}` });
128
+ await probeWebSocketEndpoint(endpoints[index]!.url, `token-${index + 1}`);
129
+ }),
130
+ );
131
+ const connected = await Promise.all(
132
+ endpoints.map((endpoint, index) => SdkClient.connect(endpoint.url, `token-${index + 1}`)),
133
+ );
134
+ clients.push(...connected);
135
+ await Promise.all(
136
+ connected.flatMap((client, index) => [
137
+ expect(client.query("session.metadata")).resolves.toMatchObject({
138
+ ok: true,
139
+ result: { sessionId: `isolated-lane-${index + 1}` },
140
+ }),
141
+ expect(client.control("turn.prompt", { text: `probe lane ${index + 1}` })).resolves.toMatchObject({
142
+ ok: true,
143
+ result: { accepted: true },
144
+ }),
145
+ ]),
146
+ );
147
+ } finally {
148
+ await Promise.all(clients.map(client => client.close()));
149
+ for (const dispose of frameDisposers) dispose?.();
150
+ await Promise.all(transports.map(transport => transport.stop().catch(() => undefined)));
151
+ await Promise.all(roots.map(root => fs.rm(root, { recursive: true, force: true })));
152
+ }
153
+ });
47
154
 
48
155
  test("start waits for a pending stop before publishing a probeable replacement endpoint", async () => {
49
156
  const stateRoot = await tempStateRoot();
@@ -80,7 +187,7 @@ describe("SDK WebSocket transport lifecycle", () => {
80
187
  try {
81
188
  const first = await transport.start();
82
189
  const stopPromise = transport.stop();
83
- await Bun.sleep(0);
190
+ for (let attempt = 0; attempt < 100 && !stopEntered; attempt += 1) await Bun.sleep(1);
84
191
  expect(stopEntered).toBe(true);
85
192
  let secondResolved = false;
86
193
  const secondPromise = transport.start().then(endpoint => {
@@ -120,6 +227,8 @@ describe("SDK WebSocket transport lifecycle", () => {
120
227
  chmod: async () => {
121
228
  throw Object.assign(new Error("chmod injected failure"), { code: "EACCES" });
122
229
  },
230
+ rename: real.rename,
231
+ readFile: real.readFile,
123
232
  rm: real.rm,
124
233
  },
125
234
  };
@@ -137,6 +246,85 @@ describe("SDK WebSocket transport lifecycle", () => {
137
246
  await fs.rm(stateRoot, { recursive: true, force: true });
138
247
  });
139
248
 
249
+ test("failed duplicate startup preserves a canonical endpoint it did not publish", async () => {
250
+ const stateRoot = await tempStateRoot();
251
+ const endpointPath = path.join(stateRoot, "sdk", "duplicate-cleanup.json");
252
+ await fs.mkdir(path.dirname(endpointPath), { recursive: true });
253
+ const canonicalEndpoint = JSON.stringify({
254
+ version: 1,
255
+ sessionId: "duplicate-cleanup",
256
+ url: "ws://127.0.0.1:12345/",
257
+ token: "canonical-token",
258
+ pid: process.pid,
259
+ });
260
+ await fs.writeFile(endpointPath, canonicalEndpoint, { mode: 0o600 });
261
+ const transport = await createSdkWebSocketTransport({
262
+ sessionId: "duplicate-cleanup",
263
+ stateRoot,
264
+ token: "duplicate-token",
265
+ filesystem: {
266
+ mkdir: fs.mkdir,
267
+ writeFile: async () => {
268
+ throw Object.assign(new Error("duplicate startup failed"), { code: "EIO" });
269
+ },
270
+ chmod: fs.chmod,
271
+ rename: fs.rename,
272
+ readFile: fs.readFile,
273
+ rm: fs.rm,
274
+ },
275
+ });
276
+ try {
277
+ await expect(transport.start()).rejects.toMatchObject({ code: "endpoint_write_failed" });
278
+ expect(await fs.readFile(endpointPath, "utf8")).toBe(canonicalEndpoint);
279
+ } finally {
280
+ await transport.stop();
281
+ await fs.rm(stateRoot, { recursive: true, force: true });
282
+ }
283
+ });
284
+
285
+ test("serializes a successor publication behind an owner endpoint cleanup", async () => {
286
+ const stateRoot = await tempStateRoot();
287
+ const sessionId = "serialized-cleanup";
288
+ const endpointPath = path.join(stateRoot, "sdk", `${sessionId}.json`);
289
+ const readEntered = Promise.withResolvers<void>();
290
+ const releaseRead = Promise.withResolvers<void>();
291
+ const owner = await createSdkWebSocketTransport({
292
+ sessionId,
293
+ stateRoot,
294
+ token: "owner-token",
295
+ filesystem: {
296
+ mkdir: fs.mkdir,
297
+ writeFile: fs.writeFile,
298
+ chmod: fs.chmod,
299
+ rename: fs.rename,
300
+ readFile: (async (...args: Parameters<typeof fs.readFile>) => {
301
+ readEntered.resolve();
302
+ await releaseRead.promise;
303
+ return await fs.readFile(...args);
304
+ }) as typeof fs.readFile,
305
+ rm: fs.rm,
306
+ },
307
+ });
308
+ const successor = await createSdkWebSocketTransport({ sessionId, stateRoot, token: "successor-token" });
309
+ try {
310
+ await owner.start();
311
+ const stop = owner.stop();
312
+ await readEntered.promise;
313
+ const start = successor.start();
314
+ await Bun.sleep(20);
315
+ expect(JSON.parse(await fs.readFile(endpointPath, "utf8"))).toMatchObject({ token: "owner-token" });
316
+ releaseRead.resolve();
317
+ await stop;
318
+ await start;
319
+ expect(JSON.parse(await fs.readFile(endpointPath, "utf8"))).toMatchObject({ token: "successor-token" });
320
+ } finally {
321
+ releaseRead.resolve();
322
+ await owner.stop().catch(() => undefined);
323
+ await successor.stop().catch(() => undefined);
324
+ await fs.rm(stateRoot, { recursive: true, force: true });
325
+ }
326
+ });
327
+
140
328
  test("endpoint removal failures are typed and do not prevent server release", async () => {
141
329
  const stateRoot = await tempStateRoot();
142
330
  let rmCalls = 0;
@@ -146,6 +334,8 @@ describe("SDK WebSocket transport lifecycle", () => {
146
334
  mkdir: real.mkdir,
147
335
  writeFile: real.writeFile,
148
336
  chmod: real.chmod,
337
+ rename: real.rename,
338
+ readFile: real.readFile,
149
339
  rm: async (...args: Parameters<typeof real.rm>) => {
150
340
  rmCalls += 1;
151
341
  if (rmCalls === 1) throw Object.assign(new Error("rm injected failure"), { code: "EIO" });
@@ -1,6 +1,7 @@
1
1
  import { randomUUID } from "node:crypto";
2
2
  import * as fs from "node:fs/promises";
3
3
  import * as path from "node:path";
4
+ import { withFileLock } from "../../config/file-lock";
4
5
  import type { SessionSdkTransport } from "./session-runtime";
5
6
  import type { SdkFrame } from "./types";
6
7
 
@@ -8,7 +9,7 @@ type SocketData = { connectionId: string };
8
9
  type Socket = { readonly data: SocketData; send(message: string): void; close(): void; terminate?(): void };
9
10
 
10
11
  export interface SdkWebSocketTransportDependencies {
11
- readonly filesystem?: Pick<typeof fs, "mkdir" | "writeFile" | "chmod" | "rm">;
12
+ readonly filesystem?: Pick<typeof fs, "mkdir" | "writeFile" | "chmod" | "rename" | "readFile" | "rm">;
12
13
  readonly serve?: typeof Bun.serve;
13
14
  }
14
15
  type SdkServer = ReturnType<typeof Bun.serve<SocketData>>;
@@ -70,6 +71,7 @@ export async function createSdkWebSocketTransport(
70
71
  let started = false;
71
72
  let startPromise: Promise<{ url: string }> | undefined;
72
73
  let stopPromise: Promise<void> | undefined;
74
+ let publishedEndpoint: string | undefined;
73
75
 
74
76
  const stopServer = async (current: SdkServer): Promise<void> => {
75
77
  try {
@@ -96,10 +98,18 @@ export async function createSdkWebSocketTransport(
96
98
  if (current) await stopServer(current);
97
99
  };
98
100
 
99
- const removeEndpoint = async (): Promise<void> => {
101
+ const removePublishedEndpoint = async (): Promise<void> => {
102
+ if (publishedEndpoint === undefined) return;
100
103
  try {
104
+ const endpoint = await filesystem.readFile(endpointFile, "utf8");
105
+ if (endpoint !== publishedEndpoint) return;
101
106
  await filesystem.rm(endpointFile, { force: true });
107
+ publishedEndpoint = undefined;
102
108
  } catch (error) {
109
+ if ((error as NodeJS.ErrnoException).code === "ENOENT") {
110
+ publishedEndpoint = undefined;
111
+ return;
112
+ }
103
113
  throw asLifecycleError("endpoint_remove_failed", "SDK endpoint file removal failed.", error);
104
114
  }
105
115
  };
@@ -140,6 +150,7 @@ export async function createSdkWebSocketTransport(
140
150
  if (started && server) return { url: endpointUrl(server) };
141
151
 
142
152
  const pending = (async (): Promise<{ url: string }> => {
153
+ const tempEndpointFile = `${endpointFile}.${randomUUID()}.tmp`;
143
154
  let localServer: SdkServer | undefined;
144
155
  const failures: unknown[] = [];
145
156
  try {
@@ -195,26 +206,39 @@ export async function createSdkWebSocketTransport(
195
206
  });
196
207
  server = localServer;
197
208
  const url = endpointUrl(localServer);
198
- try {
199
- await filesystem.writeFile(
200
- endpointFile,
201
- JSON.stringify({
202
- version: 1,
203
- sessionId: input.sessionId,
204
- url,
205
- token: input.token,
206
- pid: process.pid,
207
- }),
208
- "utf8",
209
- );
210
- } catch (error) {
211
- throw asLifecycleError("endpoint_write_failed", "SDK endpoint file publication failed.", error);
212
- }
213
- try {
214
- await filesystem.chmod(endpointFile, 0o600);
215
- } catch (error) {
216
- throw asLifecycleError("endpoint_chmod_failed", "SDK endpoint file permission update failed.", error);
217
- }
209
+ const endpoint = JSON.stringify({
210
+ version: 1,
211
+ sessionId: input.sessionId,
212
+ url,
213
+ token: input.token,
214
+ pid: process.pid,
215
+ });
216
+ await withFileLock(endpointFile, async () => {
217
+ try {
218
+ await filesystem.writeFile(tempEndpointFile, endpoint, {
219
+ encoding: "utf8",
220
+ mode: 0o600,
221
+ flag: "wx",
222
+ });
223
+ } catch (error) {
224
+ throw asLifecycleError("endpoint_write_failed", "SDK endpoint file publication failed.", error);
225
+ }
226
+ try {
227
+ await filesystem.chmod(tempEndpointFile, 0o600);
228
+ } catch (error) {
229
+ throw asLifecycleError(
230
+ "endpoint_chmod_failed",
231
+ "SDK endpoint file permission update failed.",
232
+ error,
233
+ );
234
+ }
235
+ try {
236
+ await filesystem.rename(tempEndpointFile, endpointFile);
237
+ } catch (error) {
238
+ throw asLifecycleError("endpoint_write_failed", "SDK endpoint file publication failed.", error);
239
+ }
240
+ publishedEndpoint = endpoint;
241
+ });
218
242
  started = true;
219
243
  return { url };
220
244
  } catch (error) {
@@ -227,9 +251,11 @@ export async function createSdkWebSocketTransport(
227
251
  failures.push(cleanupError);
228
252
  }
229
253
  try {
230
- await removeEndpoint();
254
+ await filesystem.rm(tempEndpointFile, { force: true });
231
255
  } catch (cleanupError) {
232
- failures.push(cleanupError);
256
+ failures.push(
257
+ asLifecycleError("endpoint_remove_failed", "SDK endpoint temp file removal failed.", cleanupError),
258
+ );
233
259
  }
234
260
  started = false;
235
261
  server = undefined;
@@ -255,12 +281,10 @@ export async function createSdkWebSocketTransport(
255
281
  if (startPromise) await startPromise.catch(() => undefined);
256
282
  const failures: unknown[] = [];
257
283
  try {
258
- await closeServer();
259
- } catch (error) {
260
- failures.push(error);
261
- }
262
- try {
263
- await removeEndpoint();
284
+ await withFileLock(endpointFile, async () => {
285
+ await closeServer();
286
+ await removePublishedEndpoint();
287
+ });
264
288
  } catch (error) {
265
289
  failures.push(error);
266
290
  }
package/src/sdk/index.ts CHANGED
@@ -25,3 +25,4 @@ export * from "./prompt-status";
25
25
  export type { ActiveProviderConnectionKind, ActiveProviderDescriptor } from "./providers";
26
26
  export * from "./session";
27
27
  export * from "./session-directory";
28
+ export * from "./turn-result";
@@ -2,7 +2,7 @@ import path from "node:path";
2
2
  import { getAgentDir } from "@gajae-code/utils";
3
3
  import { ensureBroker } from "../broker/ensure";
4
4
  import { lifecycleRequestTimeoutMs } from "../broker/startup-budget";
5
- import { SdkClient, SdkClientError } from "../client/client";
5
+ import { SdkClient, SdkClientError, type SdkSentRecord } from "../client/client";
6
6
  import { readSdkBrokerDiscovery, SdkDiscoveryError } from "../client/discovery";
7
7
  import { validateAdapterControl, validateAdapterSecretFields } from "../protocol/adapter-validation";
8
8
  import { adapterDispositionError, findOperation } from "../protocol/operation-registry";
@@ -327,11 +327,24 @@ export function createSdkMcpServer(options: SdkMcpServerOptions = {}) {
327
327
  const broker = await readSdkBrokerDiscovery(agentDir);
328
328
  if (!broker) return { ok: false, error: { code: "not_found", message: "SDK broker not found" } };
329
329
  client = await connect(broker.url, broker.token);
330
- const timeoutMs = lifecycleRequestTimeoutMs(operation, input);
331
- const response = await client.global(operation, input, {
332
- idempotencyKey,
333
- ...(timeoutMs === undefined ? {} : { timeoutMs }),
334
- });
330
+ let response: unknown;
331
+ try {
332
+ const timeoutMs = lifecycleRequestTimeoutMs(operation, input);
333
+ response = await client.global(operation, input, {
334
+ idempotencyKey,
335
+ ...(timeoutMs === undefined ? {} : { timeoutMs }),
336
+ });
337
+ } catch (error) {
338
+ if (
339
+ isLifecycleOperation(operation) &&
340
+ error instanceof SdkClientError &&
341
+ error.code === "uncertain_after_send" &&
342
+ error.details &&
343
+ typeof error.details === "object"
344
+ )
345
+ response = await client.lookupLifecycle(error.details as SdkSentRecord);
346
+ else throw error;
347
+ }
335
348
  return isLifecycleOperation(operation) ? redactLifecycleCredentials(response) : response;
336
349
  } catch (error) {
337
350
  return resultError(error);
@@ -1534,7 +1534,7 @@
1534
1534
  "sourceFile": "packages/coding-agent/src/sdk/protocol/operation-registry.ts",
1535
1535
  "sourceKind": "registry",
1536
1536
  "decision": "include",
1537
- "sdkId": "turn.prompt_status",
1537
+ "sdkId": "turn.result",
1538
1538
  "adapterMappings": {
1539
1539
  "telegram": "prohibited",
1540
1540
  "discord": "prohibited",
@@ -155,15 +155,10 @@ const queries = [
155
155
  ["resource.body", "Read a bounded resource continuation."],
156
156
  ["artifact.read", "Read a bounded artifact range."],
157
157
  ["runtime.jobs.list", "List managed jobs."],
158
- [
159
- "turn.prompt_status",
160
- "Read the authoritative reconciliation status of a submitted prompt by command/turn IDs or clientRef.",
161
- ],
158
+ ["turn.result", "Read the authoritative durable result of a submitted turn by command/turn IDs or clientRef."],
162
159
  ["models.profiles.list", "List the effective built-in and configured model profiles for this session."],
163
- [
164
- "skill.invoke_status",
165
- "Read the authoritative reconciliation status of a skill.invoke by command/turn IDs or clientRef.",
166
- ],
160
+ ["skill.invoke_status", "Legacy alias for the authoritative turn result query."],
161
+
167
162
  ["providers.list/active", "List active providers."],
168
163
  ["session.checkpoint", "Resolve a durable transcript checkpoint into a connection-owned replay cursor."],
169
164
  ] as const;
@@ -240,6 +235,7 @@ function queryContinuityClass(id: string): QueryContinuityClass {
240
235
  if (["Q01", "Q02"].includes(id)) return "stable_prefix";
241
236
  if (["Q04", "Q05", "Q06", "Q07", "Q08", "Q11", "Q12", "Q13", "Q20", "Q21", "Q22", "Q23", "Q27", "Q30"].includes(id))
242
237
  return "retained_revision";
238
+
243
239
  if (id === "Q24") return "content_addressed";
244
240
  return "scalar_snapshot";
245
241
  }
@@ -306,10 +302,10 @@ export const OPERATIONS: readonly Operation[] = [
306
302
  description,
307
303
  idempotency: "idempotent" as const,
308
304
  errorCodes:
309
- id === "Q27"
310
- ? ["invalid_request", "resource_gone", "model_profile_registry_error"]
311
- : id === "Q29"
312
- ? ["invalid_request", "resource_gone", "internal"]
305
+ id === "Q29"
306
+ ? ["invalid_request", "resource_gone", "internal"]
307
+ : id === "Q27"
308
+ ? ["invalid_request", "resource_gone", "model_profile_registry_error"]
313
309
  : ["invalid_request", "resource_gone"],
314
310
  continuityClass: queryContinuityClass(id),
315
311
  adapterDispositions: queryDisposition(id),
@@ -116,8 +116,7 @@ import {
116
116
  } from "../sdk/bus/config";
117
117
  import { NotificationSessionController } from "../sdk/bus/session-control";
118
118
  import { shouldHostSdk } from "../sdk/host";
119
- import { createSdkSessionRuntimeExtension, registerSdkOnlyNotificationCommand } from "../sdk/host/session-runtime";
120
- import { createSdkWebSocketTransport } from "../sdk/host/websocket-transport";
119
+
121
120
  import type { SecretObfuscator } from "../secrets";
122
121
  import { AgentSession, type ForkContextSeed } from "../session/agent-session";
123
122
  import { resolveAuthBrokerConfig } from "../session/auth-broker-config";
@@ -1710,7 +1709,8 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
1710
1709
  // (serviceTier === undefined) would be resurrected to the startup value.
1711
1710
  return agent ? agent.serviceTier : initialServiceTier;
1712
1711
  },
1713
- isFastForSubagentProvider: provider => session?.isFastForSubagentProvider(provider) ?? false,
1712
+ isFastForSubagentProvider: (provider, supportsServiceTier) =>
1713
+ session?.isFastForSubagentProvider(provider, supportsServiceTier) ?? false,
1714
1714
  getAgentId: () => resolvedAgentId,
1715
1715
  bashAllowedPrefixes: options.bashAllowedPrefixes,
1716
1716
  bashRestrictionProfile: options.bashRestrictionProfile,
@@ -2119,7 +2119,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
2119
2119
  shouldHostSdk(notificationCfg, isTopLevelSdkSession) && (options.sdkHostModeSupported ?? true);
2120
2120
  const notificationAdapterService = createLazyService({
2121
2121
  id: "sdk.notifications.adapters",
2122
- enabled: () => notificationsExtensionEligible,
2122
+ enabled: () => notificationsExtensionEligible || sdkHostEligible,
2123
2123
  initialize: async () => ({
2124
2124
  value: (await import("../sdk/bus")).createNotificationsExtension,
2125
2125
  }),
@@ -2130,7 +2130,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
2130
2130
  if (lifecycleStartupCapability) attachLifecycleStartupCapability(api, lifecycleStartupCapability);
2131
2131
  if (lifecycleStartupCapability && process.env.GJC_SDK_TEST_FACTORY_FAILURE === cwd)
2132
2132
  throw new Error(process.env.GJC_SDK_TEST_FACTORY_SECRET ?? "Lifecycle factory test failure.");
2133
- if (notificationsExtensionEligible) {
2133
+ if (notificationsExtensionEligible || sdkHostEligible) {
2134
2134
  const createNotificationsExtension = await notificationAdapterService.get("session-extension");
2135
2135
  createNotificationsExtension(api, {
2136
2136
  settings,
@@ -2148,15 +2148,6 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
2148
2148
  return { replyText };
2149
2149
  },
2150
2150
  });
2151
- } else if (sdkHostEligible) {
2152
- registerSdkOnlyNotificationCommand(api);
2153
- createSdkSessionRuntimeExtension(api, {
2154
- agentDir,
2155
- brokerRegistrationRequired: lifecycleStartupCapability !== undefined,
2156
- createTransport: input => createSdkWebSocketTransport(input),
2157
- settings,
2158
- configOverrides: new Map(),
2159
- });
2160
2151
  }
2161
2152
  } catch (error) {
2162
2153
  lifecycleStartupCapability?.settleFailure(
@@ -0,0 +1,53 @@
1
+ /** Canonical durable turn-result DTOs. */
2
+ export const TURN_RESULT_VERSION = 1 as const;
3
+ export const TURN_RESULT_CONTENT_MAX_BYTES = 16_384;
4
+ export const TURN_RESULT_ERROR_MESSAGE_MAX_LENGTH = 512;
5
+ export type TurnResultSelector =
6
+ | { kind: "prompt" | "skill"; clientRef: string }
7
+ | { kind: "prompt" | "skill"; commandId: string; turnId: string };
8
+ export type TurnResultStatus = "unknown" | "accepted" | "in_flight" | "terminal_ok" | "failed";
9
+ export interface TurnResultContent {
10
+ version: typeof TURN_RESULT_VERSION;
11
+ type: "text";
12
+ text: string;
13
+ byteLength: number;
14
+ truncated: boolean;
15
+ }
16
+ export interface TurnResultError {
17
+ code: string;
18
+ message: string;
19
+ }
20
+ export interface TurnResultPage {
21
+ status: TurnResultStatus;
22
+ kind?: "prompt" | "skill";
23
+ commandId?: string;
24
+ turnId?: string;
25
+ clientRef?: string;
26
+ acceptedAt?: number;
27
+ startedAt?: number;
28
+ terminalAt?: number;
29
+ content?: TurnResultContent;
30
+ error?: TurnResultError;
31
+ }
32
+ export function sanitizeTurnResultContent(text: unknown): TurnResultContent | undefined {
33
+ if (typeof text !== "string") return undefined;
34
+ const bytes = new TextEncoder().encode(text);
35
+ const truncated = bytes.length > TURN_RESULT_CONTENT_MAX_BYTES;
36
+ let bounded = text;
37
+ if (truncated) {
38
+ let end = 0;
39
+ for (const character of text) {
40
+ const next = end + new TextEncoder().encode(character).length;
41
+ if (next > TURN_RESULT_CONTENT_MAX_BYTES) break;
42
+ end = next;
43
+ }
44
+ bounded = new TextDecoder().decode(bytes.slice(0, end));
45
+ }
46
+ return {
47
+ version: TURN_RESULT_VERSION,
48
+ type: "text",
49
+ text: bounded,
50
+ byteLength: new TextEncoder().encode(bounded).length,
51
+ truncated,
52
+ };
53
+ }