@gajae-code/coding-agent 0.12.21 → 0.13.0
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 +67 -2
- package/dist/types/cli/args.d.ts +5 -2
- package/dist/types/cli/root-flags.d.ts +246 -0
- package/dist/types/cli.d.ts +11 -19
- package/dist/types/commands/daemon.d.ts +0 -30
- package/dist/types/commands/launch.d.ts +11 -19
- package/dist/types/commands/quick-lane.d.ts +32 -0
- package/dist/types/commands/sdk.d.ts +21 -15
- package/dist/types/config/file-lock-gc.d.ts +2 -0
- package/dist/types/config/model-equivalence.d.ts +20 -0
- package/dist/types/config/model-profile-activation.d.ts +62 -17
- package/dist/types/config/model-profiles.d.ts +10 -0
- package/dist/types/config/model-registry.d.ts +68 -1
- package/dist/types/config/model-resolver.d.ts +15 -4
- package/dist/types/config/models-config-schema.d.ts +7 -2
- package/dist/types/config/provider-selection-policy.d.ts +62 -0
- package/dist/types/config/settings-schema.d.ts +36 -78
- package/dist/types/config/settings.d.ts +2 -0
- package/dist/types/edit/renderer.d.ts +5 -2
- package/dist/types/edit/streaming.d.ts +13 -0
- package/dist/types/extensibility/custom-tools/types.d.ts +2 -0
- package/dist/types/extensibility/extensions/compact-handler.d.ts +2 -1
- package/dist/types/extensibility/extensions/types.d.ts +3 -0
- package/dist/types/gjc-runtime/gc-runtime.d.ts +19 -5
- package/dist/types/internal-urls/local-protocol.d.ts +1 -0
- package/dist/types/modes/components/model-selector.d.ts +0 -2
- package/dist/types/modes/components/provider-order-context.d.ts +45 -0
- package/dist/types/modes/components/provider-order-editor.d.ts +24 -0
- package/dist/types/modes/components/settings-defs.d.ts +5 -1
- package/dist/types/modes/components/settings-selector.d.ts +2 -0
- package/dist/types/quick-lane/classify.d.ts +40 -0
- package/dist/types/runtime/stt-settings-setup.d.ts +18 -0
- package/dist/types/sdk/broker/broker.d.ts +56 -2
- package/dist/types/sdk/broker/operation-receipt-ledger.d.ts +25 -0
- package/dist/types/sdk/broker/session-index.d.ts +83 -2
- package/dist/types/sdk/broker/state-version.d.ts +1 -1
- package/dist/types/sdk/bus/notification-service.d.ts +6 -0
- package/dist/types/sdk/bus/prompt-reconciliation.d.ts +8 -1
- package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +4 -1
- package/dist/types/sdk/bus/telegram-daemon.d.ts +26 -0
- package/dist/types/sdk/cli/rows.d.ts +98 -0
- package/dist/types/sdk/cli/session-cli.d.ts +35 -3
- package/dist/types/sdk/elevation/allowlist.d.ts +20 -0
- package/dist/types/sdk/elevation/audit.d.ts +93 -0
- package/dist/types/sdk/elevation/capability.d.ts +3 -0
- package/dist/types/sdk/elevation/digest.d.ts +72 -0
- package/dist/types/sdk/elevation/dispatch-receipt.d.ts +85 -0
- package/dist/types/sdk/elevation/grant-ledger.d.ts +163 -0
- package/dist/types/sdk/elevation/owner.d.ts +48 -0
- package/dist/types/sdk/guides/cache.d.ts +98 -0
- package/dist/types/sdk/guides/catalog.d.ts +156 -0
- package/dist/types/sdk/guides/cli.d.ts +39 -0
- package/dist/types/sdk/guides/manifest.d.ts +66 -0
- package/dist/types/sdk/guides/verify.d.ts +66 -0
- package/dist/types/sdk/host/control/dispatch.d.ts +8 -0
- package/dist/types/sdk/host/control/index.d.ts +1 -1
- package/dist/types/sdk/host/control/operations.d.ts +3 -2
- package/dist/types/sdk/host/query/cursor.d.ts +10 -0
- package/dist/types/sdk/host/query/handlers.d.ts +21 -0
- package/dist/types/sdk/host/session-runtime.d.ts +9 -0
- package/dist/types/sdk/mcp/server.d.ts +2 -2
- package/dist/types/sdk/prompt-status.d.ts +11 -0
- package/dist/types/sdk/session.d.ts +4 -0
- package/dist/types/sdk/transport/index.d.ts +1 -0
- package/dist/types/sdk/transport/relay.d.ts +1 -0
- package/dist/types/sdk/transport/serve-cli.d.ts +20 -1
- package/dist/types/session/agent-session.d.ts +32 -18
- package/dist/types/session/blob-store.d.ts +13 -7
- package/dist/types/session/fallback-chain-controller.d.ts +15 -0
- package/dist/types/session/internal/managed-session-storage.d.ts +25 -1
- package/dist/types/session/internal/session-memory-sidecar.d.ts +5 -5
- package/dist/types/session/session-manager.d.ts +139 -10
- package/dist/types/session/session-storage.d.ts +48 -0
- package/dist/types/setup/provider-onboarding.d.ts +7 -1
- package/dist/types/slash-commands/builtin-registry.d.ts +14 -0
- package/dist/types/stt/setup.d.ts +2 -0
- package/dist/types/task/executor.d.ts +10 -2
- package/dist/types/tools/browser/runtime-diagnostics.d.ts +65 -0
- package/dist/types/tools/browser/tab-protocol.d.ts +4 -0
- package/dist/types/tools/browser/tab-supervisor.d.ts +2 -0
- package/dist/types/tools/browser/tab-worker.d.ts +2 -0
- package/dist/types/tools/browser.d.ts +2 -0
- package/dist/types/tools/image-gen.d.ts +14 -18
- package/dist/types/tools/index.d.ts +2 -0
- package/dist/types/tools/resource-gc.d.ts +2 -0
- package/dist/types/utils/pasted-image-path.d.ts +6 -0
- package/package.json +9 -9
- package/scripts/generate-sdk-operation-inventory.ts +8 -0
- package/scripts/run-test-manifest.ts +61 -21
- package/scripts/verify-gjc-sdk-canonicalization.ts +9 -4
- package/src/cli/args.ts +62 -9
- package/src/cli/completion-cli.ts +1 -0
- package/src/cli/fast-help.ts +1 -0
- package/src/cli/root-flags.ts +100 -0
- package/src/cli/setup-cli.ts +6 -2
- package/src/cli.ts +42 -65
- package/src/commands/acp.ts +1 -1
- package/src/commands/daemon.ts +2 -31
- package/src/commands/gc.ts +1 -1
- package/src/commands/launch.ts +8 -129
- package/src/commands/quick-lane.ts +51 -0
- package/src/commands/sdk.ts +200 -26
- package/src/commands/setup.ts +4 -2
- package/src/config/file-lock-gc.ts +4 -1
- package/src/config/model-equivalence.ts +76 -7
- package/src/config/model-profile-activation.ts +1115 -265
- package/src/config/model-profiles.ts +134 -2
- package/src/config/model-registry.ts +352 -50
- package/src/config/model-resolver.ts +289 -38
- package/src/config/models-config-schema.ts +11 -4
- package/src/config/provider-selection-policy.ts +146 -0
- package/src/config/settings-schema.ts +50 -77
- package/src/config/settings.ts +40 -0
- package/src/coordinator-mcp/server.ts +32 -2
- package/src/edit/renderer.ts +175 -223
- package/src/edit/streaming.ts +108 -1
- package/src/export/html/index.ts +57 -7
- package/src/extensibility/custom-tools/types.ts +2 -0
- package/src/extensibility/extensions/compact-handler.ts +3 -2
- package/src/extensibility/extensions/runner.ts +6 -0
- package/src/extensibility/extensions/types.ts +3 -0
- package/src/gjc-runtime/gc-runtime.ts +379 -20
- package/src/gjc-runtime/launch-worktree.ts +5 -1
- package/src/gjc-runtime/team-runtime.ts +59 -2
- package/src/gjc-runtime/tmux-owner-isolation.ts +19 -0
- package/src/gjc-runtime/ultragoal-receipt-freshness.ts +16 -1
- package/src/internal-urls/docs-index.generated.ts +17 -13
- package/src/internal-urls/local-protocol.ts +1 -0
- package/src/main.ts +40 -3
- package/src/memories/index.ts +4 -2
- package/src/modes/acp/acp-agent.ts +235 -91
- package/src/modes/components/model-selector.ts +206 -42
- package/src/modes/components/provider-order-context.ts +197 -0
- package/src/modes/components/provider-order-editor.ts +441 -0
- package/src/modes/components/settings-defs.ts +21 -1
- package/src/modes/components/settings-selector.ts +45 -2
- package/src/modes/components/tool-execution.ts +47 -15
- package/src/modes/controllers/command-controller.ts +6 -3
- package/src/modes/controllers/extension-ui-controller.ts +2 -0
- package/src/modes/controllers/input-controller.ts +73 -2
- package/src/modes/controllers/selector-controller.ts +260 -153
- package/src/modes/prompt-suggestion-controller.ts +1 -1
- package/src/modes/runtime-init.ts +1 -0
- package/src/modes/utils/ui-helpers.ts +18 -0
- package/src/prompts/tools/browser.md +1 -0
- package/src/quick-lane/classify.ts +257 -0
- package/src/rlm/index.ts +1 -1
- package/src/runtime/stt-settings-setup.ts +57 -0
- package/src/sdk/acp/adapter.ts +2 -1
- package/src/sdk/broker/broker.ts +780 -12
- package/src/sdk/broker/lifecycle.ts +70 -29
- package/src/sdk/broker/operation-receipt-ledger.ts +144 -0
- package/src/sdk/broker/session-index.ts +463 -48
- package/src/sdk/broker/state-version.ts +13 -9
- package/src/sdk/broker/transport.ts +14 -1
- package/src/sdk/bus/index.ts +34 -13
- package/src/sdk/bus/kind-aware-reconciliation.ts +21 -2
- package/src/sdk/bus/notification-service.ts +47 -1
- package/src/sdk/bus/prompt-reconciliation.ts +22 -3
- package/src/sdk/bus/telegram-daemon-contract.ts +4 -1
- package/src/sdk/bus/telegram-daemon.ts +54 -3
- package/src/sdk/cli/rows.ts +247 -0
- package/src/sdk/cli/session-cli.ts +930 -79
- package/src/sdk/elevation/allowlist.ts +54 -0
- package/src/sdk/elevation/audit.ts +208 -0
- package/src/sdk/elevation/capability.ts +41 -0
- package/src/sdk/elevation/digest.ts +129 -0
- package/src/sdk/elevation/dispatch-receipt.ts +184 -0
- package/src/sdk/elevation/grant-ledger.ts +1142 -0
- package/src/sdk/elevation/owner.ts +109 -0
- package/src/sdk/guides/cache.ts +363 -0
- package/src/sdk/guides/catalog.ts +554 -0
- package/src/sdk/guides/cli.ts +235 -0
- package/src/sdk/guides/manifest.ts +152 -0
- package/src/sdk/guides/verify.ts +178 -0
- package/src/sdk/host/control/dispatch.ts +49 -1
- package/src/sdk/host/control/index.ts +1 -0
- package/src/sdk/host/control/operations.ts +8 -1
- package/src/sdk/host/query/cursor.ts +47 -3
- package/src/sdk/host/query/handlers.ts +155 -2
- package/src/sdk/host/sdk-surface-parity.test.ts +1 -0
- package/src/sdk/host/session-runtime.test.ts +391 -1
- package/src/sdk/host/session-runtime.ts +308 -73
- package/src/sdk/host/surface-policy.ts +3 -0
- package/src/sdk/host/websocket-transport.ts +7 -1
- package/src/sdk/mcp/server.ts +88 -15
- package/src/sdk/prompt-status.ts +8 -0
- package/src/sdk/protocol/operation-inventory.generated.json +62 -0
- package/src/sdk/protocol/operation-registry.ts +3 -2
- package/src/sdk/session.ts +87 -39
- package/src/sdk/transport/index.ts +1 -0
- package/src/sdk/transport/relay.ts +3 -0
- package/src/sdk/transport/serve-cli.ts +111 -24
- package/src/secrets/obfuscator.ts +92 -12
- package/src/session/agent-session.ts +322 -93
- package/src/session/blob-store.ts +60 -8
- package/src/session/fallback-chain-controller.ts +35 -0
- package/src/session/internal/managed-session-scope.ts +25 -3
- package/src/session/internal/managed-session-storage.ts +574 -73
- package/src/session/internal/session-memory-sidecar.ts +19 -29
- package/src/session/session-manager.ts +2166 -743
- package/src/session/session-storage.ts +715 -48
- package/src/setup/provider-onboarding.ts +19 -3
- package/src/setup/provider-presets.json +3 -1
- package/src/slash-commands/builtin-registry.ts +85 -3
- package/src/stt/downloader.ts +2 -3
- package/src/stt/setup.ts +29 -4
- package/src/task/executor.ts +22 -18
- package/src/task/index.ts +24 -1
- package/src/tools/archive-reader.ts +11 -2
- package/src/tools/browser/runtime-diagnostics.ts +249 -0
- package/src/tools/browser/tab-protocol.ts +4 -0
- package/src/tools/browser/tab-supervisor.ts +5 -0
- package/src/tools/browser/tab-worker.ts +36 -1
- package/src/tools/browser.ts +7 -0
- package/src/tools/image-gen.ts +110 -166
- package/src/tools/index.ts +2 -0
- package/src/tools/read.ts +1 -1
- package/src/tools/resource-gc.ts +6 -1
- package/src/tools/tool-catalog.generated.ts +5 -1
- package/src/utils/pasted-image-path.ts +93 -0
- package/src/web/search/index.ts +3 -3
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { describe, expect, test } from "bun:test";
|
|
2
|
-
import { mkdtemp, readFile, rm } from "node:fs/promises";
|
|
2
|
+
import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
|
3
3
|
import * as os from "node:os";
|
|
4
4
|
import * as path from "node:path";
|
|
5
|
+
import type { ExtensionAPI } from "../../extensibility/extensions";
|
|
6
|
+
import { Broker } from "../broker/broker";
|
|
5
7
|
import {
|
|
6
8
|
createInvocationReconciliation,
|
|
7
9
|
createSdkSessionRuntimeExtension,
|
|
@@ -79,6 +81,62 @@ test("preserves an agent failure code in host prompt reconciliation", async () =
|
|
|
79
81
|
});
|
|
80
82
|
});
|
|
81
83
|
|
|
84
|
+
test("a late agent failure never overwrites the reason an already terminal record carries", async () => {
|
|
85
|
+
const reconciliation = createInvocationReconciliation();
|
|
86
|
+
const correlation = { commandId: "first-reason-command", turnId: "first-reason-turn" };
|
|
87
|
+
await reconciliation.noteAccepted("prompt", correlation, "first-reason-ref");
|
|
88
|
+
await reconciliation.noteTransition("prompt", correlation, {
|
|
89
|
+
type: "agent_failed",
|
|
90
|
+
error: Object.assign(new Error("stream interrupted"), { code: "upstream_stream_interrupted" }),
|
|
91
|
+
});
|
|
92
|
+
const claimed = reconciliation.lookup("prompt", { clientRef: "first-reason-ref" });
|
|
93
|
+
expect(claimed).toMatchObject({
|
|
94
|
+
status: "failed",
|
|
95
|
+
error: { code: "upstream_stream_interrupted", message: "Prompt submission failed." },
|
|
96
|
+
terminalAt: expect.any(Number),
|
|
97
|
+
});
|
|
98
|
+
// First reason wins: a second, different late failure neither replaces the recorded
|
|
99
|
+
// reason nor re-stamps the terminal. The sleep makes a re-stamped terminalAt observable.
|
|
100
|
+
await Bun.sleep(2);
|
|
101
|
+
await reconciliation.noteTransition("prompt", correlation, {
|
|
102
|
+
type: "agent_failed",
|
|
103
|
+
error: Object.assign(new Error("transport reset"), { code: "transport_reset" }),
|
|
104
|
+
});
|
|
105
|
+
expect(reconciliation.lookup("prompt", { clientRef: "first-reason-ref" })).toEqual(claimed);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
test("a reason attached after a prompt settled is never replaced by a later failure", async () => {
|
|
109
|
+
const reconciliation = createInvocationReconciliation();
|
|
110
|
+
const correlation = { commandId: "late-reason-command", turnId: "late-reason-turn" };
|
|
111
|
+
await reconciliation.noteAccepted("prompt", correlation, "late-reason-ref");
|
|
112
|
+
await reconciliation.noteTransition("prompt", correlation, { type: "agent_end" });
|
|
113
|
+
const claimed = reconciliation.lookup("prompt", { clientRef: "late-reason-ref" }) as Record<string, unknown>;
|
|
114
|
+
expect(claimed).toEqual({
|
|
115
|
+
status: "terminal_ok",
|
|
116
|
+
commandId: "late-reason-command",
|
|
117
|
+
turnId: "late-reason-turn",
|
|
118
|
+
clientRef: "late-reason-ref",
|
|
119
|
+
acceptedAt: expect.any(Number),
|
|
120
|
+
terminalAt: expect.any(Number),
|
|
121
|
+
});
|
|
122
|
+
// A failure delivered after the record settled enriches it with the sanitized reason and
|
|
123
|
+
// leaves the terminal claim itself (status, terminalAt, identity) untouched.
|
|
124
|
+
await reconciliation.noteTransition("prompt", correlation, {
|
|
125
|
+
type: "agent_failed",
|
|
126
|
+
error: Object.assign(new Error("late provider failure"), { code: "upstream_error" }),
|
|
127
|
+
});
|
|
128
|
+
const enriched = reconciliation.lookup("prompt", { clientRef: "late-reason-ref" });
|
|
129
|
+
expect(enriched).toEqual({ ...claimed, error: { code: "upstream_error", message: "Prompt submission failed." } });
|
|
130
|
+
// First reason wins on the enrichment path too: a second, different late failure changes
|
|
131
|
+
// nothing. The sleep makes a re-stamped terminalAt observable.
|
|
132
|
+
await Bun.sleep(2);
|
|
133
|
+
await reconciliation.noteTransition("prompt", correlation, {
|
|
134
|
+
type: "agent_failed",
|
|
135
|
+
error: Object.assign(new Error("transport reset"), { code: "transport_reset" }),
|
|
136
|
+
});
|
|
137
|
+
expect(reconciliation.lookup("prompt", { clientRef: "late-reason-ref" })).toEqual(enriched);
|
|
138
|
+
});
|
|
139
|
+
|
|
82
140
|
test("redacts a persisted host failure during hydration", async () => {
|
|
83
141
|
const stateRoot = await mkdtemp(path.join(os.tmpdir(), "gjc-sdk-reconciliation-"));
|
|
84
142
|
const sessionId = "hydrated-failure";
|
|
@@ -206,6 +264,7 @@ describe("SessionSdkSessionRuntime", () => {
|
|
|
206
264
|
} as any;
|
|
207
265
|
const transports: Array<{ starts: number; stops: number }> = [];
|
|
208
266
|
createSdkSessionRuntimeExtension(api, {
|
|
267
|
+
agentDir: path.join(cwd, ".gjc", "agent"),
|
|
209
268
|
createTransport: async ({ sessionId, stateRoot, token }) => {
|
|
210
269
|
const stats = { starts: 0, stops: 0 };
|
|
211
270
|
const failFirstStop = transports.length === 0;
|
|
@@ -224,6 +283,9 @@ describe("SessionSdkSessionRuntime", () => {
|
|
|
224
283
|
sendFrame: () => {},
|
|
225
284
|
start: async () => {
|
|
226
285
|
stats.starts += 1;
|
|
286
|
+
const endpoint = path.join(stateRoot, "sdk", `${sessionId}.json`);
|
|
287
|
+
await mkdir(path.dirname(endpoint), { recursive: true });
|
|
288
|
+
await writeFile(endpoint, JSON.stringify({ sessionId, token, pid: process.pid }));
|
|
227
289
|
return { url: `ws://127.0.0.1:${30_000 + stats.starts}` };
|
|
228
290
|
},
|
|
229
291
|
stop: async () => {
|
|
@@ -259,4 +321,332 @@ describe("SessionSdkSessionRuntime", () => {
|
|
|
259
321
|
await rm(cwd, { recursive: true, force: true });
|
|
260
322
|
}
|
|
261
323
|
});
|
|
324
|
+
test("keeps a local SDK-only host alive through broker failure and registers after recovery", async () => {
|
|
325
|
+
const cwd = await mkdtemp(path.join(os.tmpdir(), "gjc-sdk-broker-recovery-"));
|
|
326
|
+
const agentDir = path.join(cwd, ".gjc", "agent");
|
|
327
|
+
await mkdir(path.dirname(agentDir), { recursive: true });
|
|
328
|
+
await writeFile(agentDir, "blocked");
|
|
329
|
+
const handlers = new Map<string, (event: unknown, ctx: any) => Promise<void> | void>();
|
|
330
|
+
const api = {
|
|
331
|
+
on(event: string, handler: (event: unknown, ctx: any) => Promise<void> | void) {
|
|
332
|
+
handlers.set(event, handler);
|
|
333
|
+
},
|
|
334
|
+
} as any;
|
|
335
|
+
const sessionId = "broker-recovery";
|
|
336
|
+
createSdkSessionRuntimeExtension(api, {
|
|
337
|
+
agentDir,
|
|
338
|
+
createTransport: async ({ stateRoot, token }) => ({
|
|
339
|
+
sessionId,
|
|
340
|
+
stateRoot,
|
|
341
|
+
token,
|
|
342
|
+
onFrame: () => undefined,
|
|
343
|
+
sendFrame: () => {},
|
|
344
|
+
start: async () => {
|
|
345
|
+
const endpoint = path.join(stateRoot, "sdk", `${sessionId}.json`);
|
|
346
|
+
await mkdir(path.dirname(endpoint), { recursive: true });
|
|
347
|
+
await writeFile(endpoint, JSON.stringify({ sessionId, token, pid: process.pid }));
|
|
348
|
+
return { url: "ws://127.0.0.1:1" };
|
|
349
|
+
},
|
|
350
|
+
stop: async () => {},
|
|
351
|
+
}),
|
|
352
|
+
});
|
|
353
|
+
const context = extensionContext(sessionId, cwd);
|
|
354
|
+
let broker: Broker | undefined;
|
|
355
|
+
try {
|
|
356
|
+
await handlers.get("session_start")?.({}, context);
|
|
357
|
+
await rm(agentDir);
|
|
358
|
+
await mkdir(agentDir, { recursive: true });
|
|
359
|
+
broker = new Broker({ agentDir });
|
|
360
|
+
await broker.start();
|
|
361
|
+
await handlers.get("turn_start")?.({}, context);
|
|
362
|
+
expect(await broker.handleRequest("session.get_endpoint", { sessionId, endpointGeneration: 1 })).toMatchObject(
|
|
363
|
+
{
|
|
364
|
+
ok: true,
|
|
365
|
+
result: { sessionId, token: expect.any(String) },
|
|
366
|
+
},
|
|
367
|
+
);
|
|
368
|
+
await handlers.get("session_shutdown")?.({}, context);
|
|
369
|
+
expect(await broker.handleRequest("session.get_endpoint", { sessionId, endpointGeneration: 1 })).toMatchObject(
|
|
370
|
+
{
|
|
371
|
+
ok: false,
|
|
372
|
+
error: { code: "endpoint_stale", message: "session endpoint is stale" },
|
|
373
|
+
},
|
|
374
|
+
);
|
|
375
|
+
} finally {
|
|
376
|
+
await broker?.stop();
|
|
377
|
+
await rm(cwd, { recursive: true, force: true });
|
|
378
|
+
}
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
test("rejects lifecycle-required SDK-only startup when broker registration fails", async () => {
|
|
382
|
+
const cwd = await mkdtemp(path.join(os.tmpdir(), "gjc-sdk-broker-required-"));
|
|
383
|
+
const agentDir = path.join(cwd, ".gjc", "agent");
|
|
384
|
+
await mkdir(path.dirname(agentDir), { recursive: true });
|
|
385
|
+
await writeFile(agentDir, "blocked");
|
|
386
|
+
const handlers = new Map<string, (event: unknown, ctx: any) => Promise<void> | void>();
|
|
387
|
+
const api = {
|
|
388
|
+
on(event: string, handler: (event: unknown, ctx: any) => Promise<void> | void) {
|
|
389
|
+
handlers.set(event, handler);
|
|
390
|
+
},
|
|
391
|
+
} as any;
|
|
392
|
+
createSdkSessionRuntimeExtension(api, {
|
|
393
|
+
agentDir,
|
|
394
|
+
brokerRegistrationRequired: true,
|
|
395
|
+
createTransport: async ({ sessionId, stateRoot, token }) => ({
|
|
396
|
+
sessionId,
|
|
397
|
+
stateRoot,
|
|
398
|
+
token,
|
|
399
|
+
onFrame: () => undefined,
|
|
400
|
+
sendFrame: () => {},
|
|
401
|
+
start: async () => ({ url: "ws://127.0.0.1:1" }),
|
|
402
|
+
stop: async () => {},
|
|
403
|
+
}),
|
|
404
|
+
});
|
|
405
|
+
try {
|
|
406
|
+
await expect(
|
|
407
|
+
handlers.get("session_start")?.({}, extensionContext("broker-required", cwd)),
|
|
408
|
+
).rejects.toBeDefined();
|
|
409
|
+
} finally {
|
|
410
|
+
await rm(cwd, { recursive: true, force: true });
|
|
411
|
+
}
|
|
412
|
+
});
|
|
413
|
+
});
|
|
414
|
+
interface PreflightHooks {
|
|
415
|
+
onPreflightAccepted?: () => void;
|
|
416
|
+
onPreflightAcceptCommit?: () => void | Promise<void>;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
interface ResponseFrame {
|
|
420
|
+
id?: string;
|
|
421
|
+
ok?: boolean;
|
|
422
|
+
result?: { status?: string; commandId?: string; turnId?: string; error?: { code: string; message: string } };
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
interface InvocationHarness {
|
|
426
|
+
control(operation: string, input: Record<string, unknown>): Promise<ResponseFrame>;
|
|
427
|
+
query(name: string, input: Record<string, unknown>): Promise<ResponseFrame>;
|
|
428
|
+
emit(event: string): Promise<void>;
|
|
429
|
+
stop(): Promise<void>;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
/**
|
|
433
|
+
* Drives the SDK host through its wire surface: control/query frames in,
|
|
434
|
+
* response frames out. Nothing reaches into the reconciliation maps.
|
|
435
|
+
*/
|
|
436
|
+
async function invocationHarness(
|
|
437
|
+
sessionId: string,
|
|
438
|
+
cwd: string,
|
|
439
|
+
hooks: {
|
|
440
|
+
sendUserMessage?: (content: unknown, options?: PreflightHooks & { deliverAs?: string }) => Promise<void>;
|
|
441
|
+
invokeSkill?: (name: string, args?: string, options?: PreflightHooks) => Promise<unknown>;
|
|
442
|
+
abort?: () => void;
|
|
443
|
+
},
|
|
444
|
+
): Promise<InvocationHarness> {
|
|
445
|
+
const waiters = new Map<string, (frame: ResponseFrame) => void>();
|
|
446
|
+
const handlers = new Map<string, (event: unknown, ctx: unknown) => Promise<void> | void>();
|
|
447
|
+
let deliver: ((connectionId: string, frame: SdkFrame) => void) | undefined;
|
|
448
|
+
let nextId = 0;
|
|
449
|
+
const api = {
|
|
450
|
+
on(event: string, handler: (event: unknown, ctx: unknown) => Promise<void> | void) {
|
|
451
|
+
handlers.set(event, handler);
|
|
452
|
+
},
|
|
453
|
+
sendUserMessage: hooks.sendUserMessage ?? (async () => {}),
|
|
454
|
+
} as unknown as ExtensionAPI;
|
|
455
|
+
createSdkSessionRuntimeExtension(api, {
|
|
456
|
+
agentDir: cwd,
|
|
457
|
+
createTransport: async ({ sessionId: id, stateRoot, token }) => ({
|
|
458
|
+
sessionId: id,
|
|
459
|
+
stateRoot,
|
|
460
|
+
token,
|
|
461
|
+
onFrame(handler) {
|
|
462
|
+
deliver = handler;
|
|
463
|
+
return () => {
|
|
464
|
+
if (deliver === handler) deliver = undefined;
|
|
465
|
+
};
|
|
466
|
+
},
|
|
467
|
+
sendFrame(_connectionId, frame) {
|
|
468
|
+
const response = frame as ResponseFrame;
|
|
469
|
+
if (typeof response.id === "string") waiters.get(response.id)?.(response);
|
|
470
|
+
},
|
|
471
|
+
broadcastFrame: () => {},
|
|
472
|
+
start: async () => ({ url: "ws://127.0.0.1:1" }),
|
|
473
|
+
stop: async () => {},
|
|
474
|
+
}),
|
|
475
|
+
});
|
|
476
|
+
const ctx = {
|
|
477
|
+
cwd,
|
|
478
|
+
workflowGate: undefined,
|
|
479
|
+
sdkBindings: () => (hooks.invokeSkill ? ["invokeSkill"] : []),
|
|
480
|
+
isIdle: () => true,
|
|
481
|
+
abort: hooks.abort ?? (() => {}),
|
|
482
|
+
...(hooks.invokeSkill ? { invokeSkill: hooks.invokeSkill } : {}),
|
|
483
|
+
sessionManager: { getSessionId: () => sessionId, getSessionName: () => undefined },
|
|
484
|
+
};
|
|
485
|
+
await handlers.get("session_start")?.({}, ctx);
|
|
486
|
+
const request = (frame: Record<string, unknown>): Promise<ResponseFrame> => {
|
|
487
|
+
const id = `frame-${nextId}`;
|
|
488
|
+
nextId += 1;
|
|
489
|
+
const { promise, resolve } = Promise.withResolvers<ResponseFrame>();
|
|
490
|
+
waiters.set(id, resolve);
|
|
491
|
+
deliver?.("client", { ...frame, id } as SdkFrame);
|
|
492
|
+
return promise;
|
|
493
|
+
};
|
|
494
|
+
return {
|
|
495
|
+
control: (operation, input) => request({ type: "control_request", operation, input }),
|
|
496
|
+
query: (name, input) => request({ type: "query_request", query: name, input }),
|
|
497
|
+
emit: async event => {
|
|
498
|
+
await handlers.get(event)?.({}, ctx);
|
|
499
|
+
},
|
|
500
|
+
stop: async () => {
|
|
501
|
+
await handlers.get("session_shutdown")?.({}, ctx);
|
|
502
|
+
},
|
|
503
|
+
};
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
/** Polls a status query until the invocation reports a terminal reconciliation state. */
|
|
507
|
+
async function settledStatus(
|
|
508
|
+
harness: InvocationHarness,
|
|
509
|
+
name: string,
|
|
510
|
+
input: Record<string, unknown>,
|
|
511
|
+
): Promise<NonNullable<ResponseFrame["result"]>> {
|
|
512
|
+
for (let attempt = 0; attempt < 200; attempt += 1) {
|
|
513
|
+
const frame = await harness.query(name, input);
|
|
514
|
+
const result = frame.result;
|
|
515
|
+
if (result && (result.status === "failed" || result.status === "terminal_ok")) return result;
|
|
516
|
+
await Bun.sleep(1);
|
|
517
|
+
}
|
|
518
|
+
throw new Error(`${name} never reported a terminal reconciliation status`);
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
describe("post-acceptance invocation terminalization", () => {
|
|
522
|
+
test("a prompt killed by a provider stream interrupt reports a terminal failed status", async () => {
|
|
523
|
+
const cwd = await mkdtemp(path.join(os.tmpdir(), "gjc-terminalize-prompt-"));
|
|
524
|
+
try {
|
|
525
|
+
const harness = await invocationHarness("terminalize-prompt", cwd, {
|
|
526
|
+
sendUserMessage: async (_content, options) => {
|
|
527
|
+
await options?.onPreflightAcceptCommit?.();
|
|
528
|
+
throw Object.assign(
|
|
529
|
+
new Error("upstream request failed: stream interrupted before terminal response event"),
|
|
530
|
+
{ code: "upstream_stream_interrupted" },
|
|
531
|
+
);
|
|
532
|
+
},
|
|
533
|
+
});
|
|
534
|
+
const accepted = await harness.control("turn.prompt", { text: "hello" });
|
|
535
|
+
expect(accepted.ok).toBe(true);
|
|
536
|
+
const { commandId, turnId } = accepted.result ?? {};
|
|
537
|
+
// Provider text is redacted on the wire by contract (sanitizePromptFailure);
|
|
538
|
+
// the failure reason survives as the safe-token code.
|
|
539
|
+
expect(await settledStatus(harness, "turn.prompt_status", { commandId, turnId })).toMatchObject({
|
|
540
|
+
status: "failed",
|
|
541
|
+
error: { code: "upstream_stream_interrupted", message: "Prompt submission failed." },
|
|
542
|
+
});
|
|
543
|
+
await harness.stop();
|
|
544
|
+
} finally {
|
|
545
|
+
// Let the reconciliation store finish its atomic write before the state root disappears.
|
|
546
|
+
await Bun.sleep(50);
|
|
547
|
+
await rm(cwd, { recursive: true, force: true });
|
|
548
|
+
}
|
|
549
|
+
});
|
|
550
|
+
|
|
551
|
+
test("an aborted prompt reports a terminal failed status instead of hanging", async () => {
|
|
552
|
+
const cwd = await mkdtemp(path.join(os.tmpdir(), "gjc-terminalize-abort-"));
|
|
553
|
+
try {
|
|
554
|
+
const inflight = Promise.withResolvers<void>();
|
|
555
|
+
const harness = await invocationHarness("terminalize-abort", cwd, {
|
|
556
|
+
sendUserMessage: async (_content, options) => {
|
|
557
|
+
await options?.onPreflightAcceptCommit?.();
|
|
558
|
+
await inflight.promise;
|
|
559
|
+
},
|
|
560
|
+
abort: () => inflight.reject(Object.assign(new Error("turn aborted"), { code: "aborted" })),
|
|
561
|
+
});
|
|
562
|
+
const accepted = await harness.control("turn.prompt", { text: "hello" });
|
|
563
|
+
const { commandId, turnId } = accepted.result ?? {};
|
|
564
|
+
expect(await harness.control("turn.abort", {})).toMatchObject({ ok: true });
|
|
565
|
+
expect(await settledStatus(harness, "turn.prompt_status", { commandId, turnId })).toMatchObject({
|
|
566
|
+
status: "failed",
|
|
567
|
+
error: { code: "aborted" },
|
|
568
|
+
});
|
|
569
|
+
await harness.stop();
|
|
570
|
+
} finally {
|
|
571
|
+
await Bun.sleep(50);
|
|
572
|
+
await rm(cwd, { recursive: true, force: true });
|
|
573
|
+
}
|
|
574
|
+
});
|
|
575
|
+
|
|
576
|
+
test("a failed skill invocation still reports a terminal failed status", async () => {
|
|
577
|
+
const cwd = await mkdtemp(path.join(os.tmpdir(), "gjc-terminalize-skill-"));
|
|
578
|
+
try {
|
|
579
|
+
const harness = await invocationHarness("terminalize-skill", cwd, {
|
|
580
|
+
invokeSkill: async (_name, _args, options) => {
|
|
581
|
+
await options?.onPreflightAcceptCommit?.();
|
|
582
|
+
throw Object.assign(new Error("skill provider stream interrupted"), { code: "upstream_error" });
|
|
583
|
+
},
|
|
584
|
+
});
|
|
585
|
+
const accepted = await harness.control("skill.invoke", { name: "ralplan" });
|
|
586
|
+
expect(accepted.ok).toBe(true);
|
|
587
|
+
const { commandId, turnId } = accepted.result ?? {};
|
|
588
|
+
expect(await settledStatus(harness, "skill.invoke_status", { commandId, turnId })).toMatchObject({
|
|
589
|
+
status: "failed",
|
|
590
|
+
error: { code: "upstream_error" },
|
|
591
|
+
});
|
|
592
|
+
await harness.stop();
|
|
593
|
+
} finally {
|
|
594
|
+
await Bun.sleep(50);
|
|
595
|
+
await rm(cwd, { recursive: true, force: true });
|
|
596
|
+
}
|
|
597
|
+
});
|
|
598
|
+
|
|
599
|
+
test("a pre-acceptance failure rejects the submission without creating a record", async () => {
|
|
600
|
+
const cwd = await mkdtemp(path.join(os.tmpdir(), "gjc-terminalize-preflight-"));
|
|
601
|
+
try {
|
|
602
|
+
const harness = await invocationHarness("terminalize-preflight", cwd, {
|
|
603
|
+
sendUserMessage: async () => {
|
|
604
|
+
throw Object.assign(new Error("session is busy"), { code: "busy" });
|
|
605
|
+
},
|
|
606
|
+
});
|
|
607
|
+
const rejected = await harness.control("turn.prompt", { text: "hello", clientRef: "preflight-ref" });
|
|
608
|
+
expect(rejected.ok).toBe(false);
|
|
609
|
+
const status = await harness.query("turn.prompt_status", { clientRef: "preflight-ref" });
|
|
610
|
+
expect(status.result).toEqual({ status: "unknown" });
|
|
611
|
+
await harness.stop();
|
|
612
|
+
} finally {
|
|
613
|
+
await Bun.sleep(50);
|
|
614
|
+
await rm(cwd, { recursive: true, force: true });
|
|
615
|
+
}
|
|
616
|
+
});
|
|
617
|
+
|
|
618
|
+
test("a later provider error enriches but never re-opens an already terminal prompt", async () => {
|
|
619
|
+
const cwd = await mkdtemp(path.join(os.tmpdir(), "gjc-terminalize-once-"));
|
|
620
|
+
try {
|
|
621
|
+
const inflight = Promise.withResolvers<void>();
|
|
622
|
+
const harness = await invocationHarness("terminalize-once", cwd, {
|
|
623
|
+
sendUserMessage: async (_content, options) => {
|
|
624
|
+
await options?.onPreflightAcceptCommit?.();
|
|
625
|
+
await inflight.promise;
|
|
626
|
+
},
|
|
627
|
+
});
|
|
628
|
+
const accepted = await harness.control("turn.prompt", { text: "hello" });
|
|
629
|
+
const { commandId, turnId } = accepted.result ?? {};
|
|
630
|
+
await harness.emit("agent_start");
|
|
631
|
+
await harness.emit("agent_end");
|
|
632
|
+
const claimed = await settledStatus(harness, "turn.prompt_status", { commandId, turnId });
|
|
633
|
+
expect(claimed).toMatchObject({ status: "terminal_ok" });
|
|
634
|
+
inflight.reject(Object.assign(new Error("late provider failure"), { code: "upstream_error" }));
|
|
635
|
+
await Bun.sleep(20);
|
|
636
|
+
// A lifecycle frame arriving after the terminal must not resurrect the record either.
|
|
637
|
+
await harness.emit("agent_start");
|
|
638
|
+
const settled = await harness.query("turn.prompt_status", { commandId, turnId });
|
|
639
|
+
// The late reason attaches to the settled record, so `error` is the only field that may
|
|
640
|
+
// appear; status, terminalAt, and identity stay exactly as claimed.
|
|
641
|
+
const { error: _claimedReason, ...claimedTerminal } = claimed;
|
|
642
|
+
const { error: _lateReason, ...settledTerminal } = settled.result ?? {};
|
|
643
|
+
expect(settledTerminal).toEqual(claimedTerminal);
|
|
644
|
+
// The recorded reason is the sanitized late failure: never fabricated, never raw.
|
|
645
|
+
expect(settled.result?.error).toEqual({ code: "upstream_error", message: "Prompt submission failed." });
|
|
646
|
+
await harness.stop();
|
|
647
|
+
} finally {
|
|
648
|
+
await Bun.sleep(50);
|
|
649
|
+
await rm(cwd, { recursive: true, force: true });
|
|
650
|
+
}
|
|
651
|
+
});
|
|
262
652
|
});
|