@gajae-code/coding-agent 0.13.0 → 0.13.2
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.
- package/CHANGELOG.md +34 -0
- package/dist/types/config/file-lock.d.ts +2 -0
- package/dist/types/config/keybindings.d.ts +2 -2
- package/dist/types/config/model-registry.d.ts +4 -0
- package/dist/types/config/models-config-schema.d.ts +6 -0
- package/dist/types/config/settings-schema.d.ts +17 -5
- package/dist/types/config/settings.d.ts +10 -1
- package/dist/types/edit/streaming.d.ts +5 -5
- package/dist/types/eval/py/kernel.d.ts +2 -2
- package/dist/types/eval/py/runtime.d.ts +5 -1
- package/dist/types/gjc-runtime/session-state-sidecar.d.ts +3 -0
- package/dist/types/gjc-runtime/tmux-sessions.d.ts +3 -1
- package/dist/types/modes/components/model-selector.d.ts +3 -2
- package/dist/types/runtime/process-lifecycle.d.ts +12 -31
- package/dist/types/sdk/broker/identity.d.ts +2 -1
- package/dist/types/sdk/broker/lifecycle-ledger.d.ts +17 -1
- package/dist/types/sdk/broker/lifecycle.d.ts +11 -6
- package/dist/types/sdk/broker/startup-failure.d.ts +32 -0
- package/dist/types/sdk/bus/chat-daemon-control.d.ts +2 -1
- package/dist/types/sdk/bus/conversation-store.d.ts +1 -0
- package/dist/types/sdk/bus/index.d.ts +4 -0
- package/dist/types/sdk/bus/kind-aware-reconciliation.d.ts +10 -1
- package/dist/types/sdk/bus/reconciliation-store.d.ts +2 -0
- package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +13 -2
- package/dist/types/sdk/bus/telegram-daemon.d.ts +17 -1
- package/dist/types/sdk/host/query/cursor.d.ts +4 -0
- package/dist/types/sdk/host/query/handlers.d.ts +6 -0
- package/dist/types/sdk/host/session-runtime.d.ts +7 -6
- package/dist/types/sdk/host/websocket-transport.d.ts +1 -1
- package/dist/types/sdk/index.d.ts +1 -0
- package/dist/types/sdk/turn-result.d.ts +37 -0
- package/dist/types/session/agent-session.d.ts +8 -11
- package/dist/types/session/blob-store.d.ts +6 -3
- package/dist/types/session/internal/managed-session-storage.d.ts +13 -0
- package/dist/types/slash-commands/helpers/fast-status-report.d.ts +12 -3
- package/dist/types/task/executor.d.ts +4 -7
- package/dist/types/tools/index.d.ts +1 -1
- package/dist/types/tools/read.d.ts +1 -1
- package/dist/types/tools/search.d.ts +1 -1
- package/dist/types/utils/edit-mode.d.ts +69 -0
- package/package.json +8 -8
- package/scripts/generate-hotkeys-docs.ts +8 -6
- package/src/commands/sdk.ts +23 -2
- package/src/config/file-lock.ts +25 -5
- package/src/config/keybindings.ts +4 -2
- package/src/config/model-registry.ts +20 -0
- package/src/config/models-config-schema.ts +1 -0
- package/src/config/settings-schema.ts +29 -22
- package/src/config/settings.ts +42 -10
- package/src/coordinator-mcp/server.ts +87 -106
- package/src/edit/index.ts +5 -19
- package/src/edit/renderer.ts +3 -2
- package/src/edit/streaming.ts +28 -15
- package/src/eval/py/executor.ts +108 -10
- package/src/eval/py/kernel.ts +86 -12
- package/src/eval/py/runtime.ts +200 -16
- package/src/exec/bash-executor.ts +36 -37
- package/src/gjc-runtime/gc-runtime.ts +25 -3
- package/src/gjc-runtime/managed-owner-supervisor.ts +27 -1
- package/src/gjc-runtime/session-state-sidecar.ts +110 -4
- package/src/gjc-runtime/tmux-sessions.ts +34 -5
- package/src/internal-urls/docs-index.generated.ts +9 -9
- package/src/modes/acp/acp-agent.ts +10 -2
- package/src/modes/components/model-selector.ts +28 -8
- package/src/modes/controllers/plan-mode-controller.ts +3 -3
- package/src/modes/controllers/selector-controller.ts +4 -2
- package/src/modes/interactive-mode.ts +4 -0
- package/src/runtime/process-lifecycle.ts +126 -27
- package/src/runtime-mcp/transports/stdio.ts +11 -6
- package/src/sdk/acp/adapter.ts +18 -10
- package/src/sdk/broker/broker.ts +55 -4
- package/src/sdk/broker/ensure.ts +10 -7
- package/src/sdk/broker/identity.ts +10 -0
- package/src/sdk/broker/lifecycle-ledger.ts +56 -6
- package/src/sdk/broker/lifecycle.ts +53 -10
- package/src/sdk/broker/process-incarnation.ts +6 -1
- package/src/sdk/broker/session-index.ts +28 -11
- package/src/sdk/broker/startup-failure.ts +101 -0
- package/src/sdk/broker/transport.ts +1 -0
- package/src/sdk/bus/chat-daemon-control.ts +4 -3
- package/src/sdk/bus/chat-daemon-runtime.ts +8 -1
- package/src/sdk/bus/conversation-store.ts +171 -50
- package/src/sdk/bus/index.ts +101 -47
- package/src/sdk/bus/kind-aware-reconciliation.ts +52 -2
- package/src/sdk/bus/notification-service.ts +3 -3
- package/src/sdk/bus/reconciliation-store.ts +16 -0
- package/src/sdk/bus/telegram-daemon-contract.ts +13 -2
- package/src/sdk/bus/telegram-daemon.ts +151 -41
- package/src/sdk/bus/topic-registry.ts +38 -9
- package/src/sdk/cli/session-cli.ts +20 -7
- package/src/sdk/client/discovery.ts +23 -2
- package/src/sdk/host/query/cursor.ts +4 -0
- package/src/sdk/host/query/handlers.ts +99 -100
- package/src/sdk/host/sdk-surface-parity.test.ts +1 -1
- package/src/sdk/host/session-runtime.test.ts +98 -1
- package/src/sdk/host/session-runtime.ts +57 -15
- package/src/sdk/host/websocket-transport.lifecycle.test.ts +192 -2
- package/src/sdk/host/websocket-transport.ts +54 -30
- package/src/sdk/index.ts +1 -0
- package/src/sdk/mcp/server.ts +19 -6
- package/src/sdk/protocol/operation-inventory.generated.json +1 -1
- package/src/sdk/protocol/operation-registry.ts +8 -12
- package/src/sdk/session.ts +5 -14
- package/src/sdk/turn-result.ts +53 -0
- package/src/session/agent-session.ts +112 -69
- package/src/session/blob-store.ts +91 -21
- package/src/session/internal/managed-session-storage.ts +136 -21
- package/src/session/session-manager.ts +408 -81
- package/src/session/session-storage.ts +68 -10
- package/src/slash-commands/builtin-registry.ts +43 -5
- package/src/slash-commands/helpers/fast-status-report.ts +24 -8
- package/src/task/executor.ts +25 -8
- package/src/task/index.ts +5 -4
- package/src/tools/bash.ts +11 -9
- package/src/tools/index.ts +1 -1
- package/src/tools/read.ts +12 -11
- package/src/tools/search.ts +7 -7
- package/src/utils/edit-mode.ts +180 -9
- 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"))
|
|
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(
|
|
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
|
|
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
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
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
|
|
254
|
+
await filesystem.rm(tempEndpointFile, { force: true });
|
|
231
255
|
} catch (cleanupError) {
|
|
232
|
-
failures.push(
|
|
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
|
|
259
|
-
|
|
260
|
-
|
|
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
package/src/sdk/mcp/server.ts
CHANGED
|
@@ -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
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
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.
|
|
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
|
-
|
|
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 === "
|
|
310
|
-
? ["invalid_request", "resource_gone", "
|
|
311
|
-
: id === "
|
|
312
|
-
? ["invalid_request", "resource_gone", "
|
|
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),
|
package/src/sdk/session.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
+
}
|