@gajae-code/coding-agent 0.15.6 → 0.16.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.
- package/CHANGELOG.md +99 -0
- package/dist/types/async/job-manager.d.ts +32 -1
- package/dist/types/browser-backend.d.ts +12 -0
- package/dist/types/cli/args.d.ts +2 -0
- package/dist/types/cli/root-flags.d.ts +14 -0
- package/dist/types/cli/update-cli.d.ts +14 -0
- package/dist/types/cli.d.ts +7 -0
- package/dist/types/commands/launch.d.ts +10 -0
- package/dist/types/commands/sdk.d.ts +2 -0
- package/dist/types/config/autorouting-tier-map.d.ts +24 -0
- package/dist/types/config/billing-path.d.ts +25 -0
- package/dist/types/config/config-schema-version.d.ts +13 -1
- package/dist/types/config/file-lock.d.ts +5 -0
- package/dist/types/config/model-registry.d.ts +22 -0
- package/dist/types/config/settings-schema.d.ts +27 -8
- package/dist/types/config/settings.d.ts +2 -0
- package/dist/types/coordinator-mcp/projection-scan.d.ts +8 -0
- package/dist/types/crash/record-loader.d.ts +15 -0
- package/dist/types/extensibility/extensions/types.d.ts +8 -3
- package/dist/types/gjc-runtime/launch-worktree-reservation.d.ts +21 -0
- package/dist/types/gjc-runtime/launch-worktree.d.ts +25 -1
- package/dist/types/gjc-runtime/managed-owner-supervisor.d.ts +2 -0
- package/dist/types/gjc-runtime/session-state-lock.d.ts +22 -0
- package/dist/types/gjc-runtime/session-state-sidecar.d.ts +66 -1
- package/dist/types/gjc-runtime/tmux-owner-isolation.d.ts +1 -0
- package/dist/types/gjc-runtime/tmux-sessions.d.ts +27 -0
- package/dist/types/main.d.ts +1 -0
- package/dist/types/master-mode/context.d.ts +21 -0
- package/dist/types/master-mode/first-request.d.ts +34 -0
- package/dist/types/master-mode/snapshot.d.ts +32 -0
- package/dist/types/modes/acp/acp-agent.d.ts +21 -21
- package/dist/types/modes/components/frictionless-onboarding-selector.d.ts +10 -0
- package/dist/types/modes/components/model-selector.d.ts +4 -0
- package/dist/types/modes/components/oauth-selector.d.ts +1 -0
- package/dist/types/modes/components/session-selector.d.ts +1 -1
- package/dist/types/modes/components/sessions-dashboard.d.ts +2 -1
- package/dist/types/modes/components/status-line/model-name.d.ts +18 -0
- package/dist/types/modes/components/status-line/priority-row.d.ts +34 -0
- package/dist/types/modes/components/status-line/segments.d.ts +10 -0
- package/dist/types/modes/interactive-mode.d.ts +16 -2
- package/dist/types/modes/print-mode.d.ts +17 -0
- package/dist/types/modes/theme/theme.d.ts +2 -1
- package/dist/types/modes/types.d.ts +2 -0
- package/dist/types/modes/ui-language.d.ts +7 -1
- package/dist/types/sdk/broker/broker.d.ts +79 -2
- package/dist/types/sdk/broker/endpoint-authority.d.ts +24 -0
- package/dist/types/sdk/broker/ensure.d.ts +3 -0
- package/dist/types/sdk/broker/index.d.ts +3 -0
- package/dist/types/sdk/broker/lifecycle-ledger.d.ts +6 -0
- package/dist/types/sdk/broker/lifecycle.d.ts +40 -0
- package/dist/types/sdk/broker/master-capability.d.ts +10 -0
- package/dist/types/sdk/broker/session-index.d.ts +40 -11
- package/dist/types/sdk/broker/session-scope.d.ts +72 -0
- package/dist/types/sdk/broker/spawn-authority.d.ts +201 -0
- package/dist/types/sdk/broker/spawn-substrate.d.ts +30 -0
- package/dist/types/sdk/broker/startup-budget.d.ts +52 -0
- package/dist/types/sdk/broker/state-version.d.ts +21 -1
- package/dist/types/sdk/broker/worktree-occupancy.d.ts +28 -0
- package/dist/types/sdk/bus/chat-daemon-control.d.ts +3 -0
- package/dist/types/sdk/bus/discord-daemon.d.ts +1 -1
- package/dist/types/sdk/bus/index.d.ts +9 -1
- package/dist/types/sdk/bus/kind-aware-reconciliation.d.ts +1 -1
- package/dist/types/sdk/bus/prompt-reconciliation.d.ts +1 -1
- package/dist/types/sdk/bus/slack-conversation.d.ts +1 -1
- package/dist/types/sdk/bus/slack-daemon.d.ts +1 -1
- package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +1 -1
- package/dist/types/sdk/cli/index.d.ts +1 -0
- package/dist/types/sdk/cli/master-cli.d.ts +63 -0
- package/dist/types/sdk/cli/rows.d.ts +12 -5
- package/dist/types/sdk/cli/session-cli.d.ts +20 -3
- package/dist/types/sdk/client/client.d.ts +3 -2
- package/dist/types/sdk/host/control/dispatch.d.ts +10 -0
- package/dist/types/sdk/host/control/index.d.ts +1 -1
- package/dist/types/sdk/host/control/operations.d.ts +2 -0
- package/dist/types/sdk/host/control/runtime-gate.d.ts +21 -7
- package/dist/types/sdk/host/events.d.ts +1 -0
- package/dist/types/sdk/host/host.d.ts +10 -0
- package/dist/types/sdk/host/query/cursor.d.ts +4 -2
- package/dist/types/sdk/host/query/handlers.d.ts +1 -0
- package/dist/types/sdk/host/query/revision-store.d.ts +1 -0
- package/dist/types/sdk/host/session-runtime.d.ts +70 -17
- package/dist/types/sdk/lifecycle/broker-client.d.ts +6 -0
- package/dist/types/sdk/lifecycle/service.d.ts +15 -3
- package/dist/types/sdk/lifecycle-session.d.ts +3 -1
- package/dist/types/sdk/prompt-deadline-manager.d.ts +2 -5
- package/dist/types/sdk/receipt-state.d.ts +2 -0
- package/dist/types/sdk/router/session-router.d.ts +6 -0
- package/dist/types/sdk/session.d.ts +16 -0
- package/dist/types/sdk/startup-capability.d.ts +2 -1
- package/dist/types/sdk/turn-result.d.ts +6 -0
- package/dist/types/session/agent-session.d.ts +52 -7
- package/dist/types/session/credential-store-errors.d.ts +1 -0
- package/dist/types/session/fold-coordinator.d.ts +29 -2
- package/dist/types/session/session-manager.d.ts +79 -2
- package/dist/types/session/yield-queue.d.ts +5 -4
- package/dist/types/setup/frictionless-onboarding.d.ts +1 -0
- package/dist/types/tools/ask.d.ts +1 -0
- package/dist/types/tools/bash.d.ts +16 -0
- package/dist/types/tools/browser/launch.d.ts +10 -0
- package/dist/types/tools/browser/registry.d.ts +8 -0
- package/dist/types/tools/gh.d.ts +8 -0
- package/dist/types/tools/index.d.ts +10 -10
- package/dist/types/tools/job.d.ts +3 -1
- package/dist/types/utils/external-editor.d.ts +7 -0
- package/examples/extensions/README.md +1 -1
- package/examples/extensions/pirate.ts +7 -4
- package/package.json +7 -7
- package/scripts/generate-sdk-operation-inventory.ts +13 -1
- package/scripts/verify-gjc-sdk-canonicalization.ts +35 -1
- package/src/async/job-manager.ts +107 -9
- package/src/browser-backend.ts +29 -0
- package/src/cli/accounts-cli.ts +9 -5
- package/src/cli/args.ts +17 -0
- package/src/cli/fast-help.ts +1 -0
- package/src/cli/plugin-cli.ts +29 -0
- package/src/cli/root-flags.ts +5 -0
- package/src/cli/update-cli.ts +140 -23
- package/src/commands/launch.ts +86 -18
- package/src/commands/sdk.ts +135 -28
- package/src/config/autorouting-tier-map.ts +12 -0
- package/src/config/billing-path.ts +42 -0
- package/src/config/config-schema-version.ts +18 -1
- package/src/config/file-lock.ts +158 -83
- package/src/config/model-preset-registry.ts +178 -23
- package/src/config/model-profiles.ts +5 -5
- package/src/config/model-registry.ts +291 -42
- package/src/config/model-resolver.ts +5 -0
- package/src/config/settings-schema.ts +27 -8
- package/src/config/settings.ts +51 -15
- package/src/coordinator-mcp/projection-scan.ts +27 -6
- package/src/coordinator-mcp/question-state.ts +9 -11
- package/src/coordinator-mcp/server.ts +115 -21
- package/src/crash/index-store.ts +2 -0
- package/src/crash/record-loader.ts +10 -0
- package/src/defaults/gjc/skills/deep-interview/SKILL.md +1 -1
- package/src/extensibility/extensions/runner.ts +27 -0
- package/src/extensibility/extensions/types.ts +8 -3
- package/src/extensibility/gjc-plugins/installer.ts +51 -11
- package/src/extensibility/runtime-skill-discovery.ts +110 -3
- package/src/extensibility/skills.ts +22 -12
- package/src/gjc-runtime/launch-worktree-reservation.ts +242 -0
- package/src/gjc-runtime/launch-worktree.ts +285 -4
- package/src/gjc-runtime/managed-owner-supervisor.ts +63 -51
- package/src/gjc-runtime/session-state-lock.ts +213 -14
- package/src/gjc-runtime/session-state-sidecar.ts +938 -39
- package/src/gjc-runtime/tmux-owner-isolation-cli.ts +4 -7
- package/src/gjc-runtime/tmux-owner-isolation.ts +18 -3
- package/src/gjc-runtime/tmux-sessions.ts +252 -13
- package/src/hooks/skill-state.ts +6 -0
- package/src/internal-urls/docs-index.generated.ts +15 -15
- package/src/internal-urls/issue-pr-protocol.ts +4 -2
- package/src/main.ts +61 -3
- package/src/master-mode/context.ts +50 -0
- package/src/master-mode/first-request.ts +65 -0
- package/src/master-mode/snapshot.ts +205 -0
- package/src/modes/acp/acp-agent.ts +610 -161
- package/src/modes/components/footer.ts +4 -2
- package/src/modes/components/frictionless-onboarding-selector.ts +48 -2
- package/src/modes/components/model-selector.ts +76 -8
- package/src/modes/components/oauth-selector.ts +6 -1
- package/src/modes/components/session-selector.ts +23 -15
- package/src/modes/components/sessions-dashboard.ts +8 -5
- package/src/modes/components/settings-selector.ts +1 -0
- package/src/modes/components/status-line/model-name.ts +53 -0
- package/src/modes/components/status-line/priority-row.ts +107 -0
- package/src/modes/components/status-line/segments.ts +23 -9
- package/src/modes/components/tool-status-header.ts +151 -15
- package/src/modes/controllers/command-controller.ts +1 -1
- package/src/modes/controllers/event-controller.ts +30 -4
- package/src/modes/controllers/extension-ui-controller.ts +13 -13
- package/src/modes/controllers/input-controller.ts +30 -2
- package/src/modes/controllers/selector-controller.ts +200 -98
- package/src/modes/interactive-mode.ts +36 -12
- package/src/modes/print-mode.ts +64 -0
- package/src/modes/prompt-action-autocomplete.ts +30 -4
- package/src/modes/runtime-init.ts +12 -9
- package/src/modes/theme/theme.ts +5 -0
- package/src/modes/types.ts +2 -0
- package/src/modes/ui-language.ts +49 -2
- package/src/modes/utils/ui-helpers.ts +22 -3
- package/src/prompts/agent-fragments/browser-aside.md +22 -0
- package/src/prompts/system/master-mode.md +28 -0
- package/src/prompts/tools/ask.md +1 -1
- package/src/prompts/tools/bash.md +1 -0
- package/src/prompts/tools/job.md +1 -1
- package/src/sdk/acp/adapter.ts +5 -0
- package/src/sdk/broker/broker.ts +1646 -27
- package/src/sdk/broker/endpoint-authority.ts +86 -0
- package/src/sdk/broker/ensure.ts +81 -3
- package/src/sdk/broker/index.ts +3 -0
- package/src/sdk/broker/lifecycle-ledger.ts +6 -0
- package/src/sdk/broker/lifecycle.ts +440 -75
- package/src/sdk/broker/master-capability.ts +232 -0
- package/src/sdk/broker/session-index.ts +121 -17
- package/src/sdk/broker/session-scope.ts +272 -0
- package/src/sdk/broker/spawn-authority.ts +1031 -0
- package/src/sdk/broker/spawn-substrate.ts +591 -0
- package/src/sdk/broker/startup-budget.ts +144 -2
- package/src/sdk/broker/state-version.ts +43 -14
- package/src/sdk/broker/transport.ts +1 -0
- package/src/sdk/broker/worktree-occupancy.ts +90 -0
- package/src/sdk/bus/chat-daemon-control.ts +5 -2
- package/src/sdk/bus/chat-daemon-runtime.ts +136 -18
- package/src/sdk/bus/discord-daemon.ts +50 -10
- package/src/sdk/bus/index.ts +84 -7
- package/src/sdk/bus/kind-aware-reconciliation.ts +296 -48
- package/src/sdk/bus/lifecycle-commands.ts +4 -0
- package/src/sdk/bus/notification-service.ts +22 -5
- package/src/sdk/bus/prompt-reconciliation.ts +7 -2
- package/src/sdk/bus/reconciliation-store.ts +55 -31
- package/src/sdk/bus/slack-conversation.ts +1 -1
- package/src/sdk/bus/slack-daemon.ts +72 -38
- package/src/sdk/bus/telegram-daemon-contract.ts +1 -1
- package/src/sdk/cli/index.ts +1 -0
- package/src/sdk/cli/master-cli.ts +259 -0
- package/src/sdk/cli/rows.ts +41 -15
- package/src/sdk/cli/session-cli.ts +297 -26
- package/src/sdk/client/client.ts +14 -8
- package/src/sdk/host/control/dispatch.ts +101 -40
- package/src/sdk/host/control/index.ts +2 -0
- package/src/sdk/host/control/operations.ts +2 -0
- package/src/sdk/host/control/runtime-gate.ts +203 -11
- package/src/sdk/host/events.ts +1 -0
- package/src/sdk/host/host.ts +29 -2
- package/src/sdk/host/query/cursor.ts +52 -7
- package/src/sdk/host/query/handlers.ts +33 -12
- package/src/sdk/host/query/revision-store.ts +12 -0
- package/src/sdk/host/session-runtime.test.ts +673 -14
- package/src/sdk/host/session-runtime.ts +674 -103
- package/src/sdk/host/surface-policy.ts +3 -3
- package/src/sdk/lifecycle/broker-client.ts +28 -0
- package/src/sdk/lifecycle/service.ts +279 -20
- package/src/sdk/lifecycle-session.ts +17 -4
- package/src/sdk/prompt-deadline-manager.ts +2 -1
- package/src/sdk/protocol/operation-inventory.generated.json +105 -3
- package/src/sdk/protocol/operation-registry.ts +14 -4
- package/src/sdk/receipt-state.ts +10 -0
- package/src/sdk/router/session-router.ts +219 -118
- package/src/sdk/session.ts +416 -195
- package/src/sdk/startup-capability.ts +1 -0
- package/src/sdk/turn-result.ts +22 -0
- package/src/session/agent-session.ts +1628 -318
- package/src/session/blob-store.ts +15 -5
- package/src/session/credential-store-errors.ts +2 -0
- package/src/session/fold-coordinator.ts +40 -3
- package/src/session/internal/managed-session-storage.ts +153 -78
- package/src/session/session-manager.ts +850 -97
- package/src/session/yield-queue.ts +24 -10
- package/src/setup/frictionless-onboarding.ts +2 -0
- package/src/slash-commands/builtin-registry.ts +41 -0
- package/src/task/executor.ts +9 -4
- package/src/telemetry/events.ts +1138 -12
- package/src/tools/ask-contract.test.ts +64 -0
- package/src/tools/ask-contract.ts +36 -1
- package/src/tools/ask.ts +10 -8
- package/src/tools/bash-interactive.ts +11 -2
- package/src/tools/bash.ts +412 -75
- package/src/tools/browser/launch.ts +37 -0
- package/src/tools/browser/registry.ts +99 -9
- package/src/tools/cron.ts +6 -4
- package/src/tools/descriptors.test.ts +15 -0
- package/src/tools/descriptors.ts +5 -1
- package/src/tools/gh.ts +104 -48
- package/src/tools/github-cache.ts +2 -2
- package/src/tools/index.ts +10 -6
- package/src/tools/job.ts +4 -1
- package/src/tools/monitor.ts +12 -9
- package/src/tools/resolve.ts +30 -10
- package/src/tools/tool-catalog.generated.ts +2 -2
- package/src/utils/external-editor.ts +72 -1
|
@@ -1,11 +1,12 @@
|
|
|
1
|
-
import { describe, expect, test } from "bun:test";
|
|
1
|
+
import { describe, expect, spyOn, test } from "bun:test";
|
|
2
2
|
import { createHash } from "node:crypto";
|
|
3
|
-
import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
|
3
|
+
import { mkdir, mkdtemp, readdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
4
4
|
import * as os from "node:os";
|
|
5
5
|
import * as path from "node:path";
|
|
6
|
+
import { logger } from "@gajae-code/utils";
|
|
6
7
|
import { AsyncJobManager } from "../../async";
|
|
7
8
|
import type { Settings } from "../../config/settings";
|
|
8
|
-
import type { ExtensionAPI, ExtensionContext } from "../../extensibility/extensions";
|
|
9
|
+
import type { ExtensionAPI, ExtensionContext, ExtensionTranscriptEntry } from "../../extensibility/extensions";
|
|
9
10
|
import {
|
|
10
11
|
registerOwnedRegistration,
|
|
11
12
|
resetTerminalAbortRegistriesForTests,
|
|
@@ -16,7 +17,8 @@ import { Broker } from "../broker/broker";
|
|
|
16
17
|
import { createKindAwareReconciliation } from "../bus/kind-aware-reconciliation";
|
|
17
18
|
import { createPromptReconciliation } from "../bus/prompt-reconciliation";
|
|
18
19
|
import { createReconciliationStore } from "../bus/reconciliation-store";
|
|
19
|
-
import {
|
|
20
|
+
import { BROKER_RUNTIME_ABORT_CAPABILITY_FIELD, setBrokerRuntimeAbortCapabilityForTest } from "./control/runtime-gate";
|
|
21
|
+
import { CursorRegistry, QueryHandlers, type QueryResponse, RevisionStore } from "./query";
|
|
20
22
|
import {
|
|
21
23
|
createInvocationReconciliation,
|
|
22
24
|
createSdkSessionRuntimeExtension,
|
|
@@ -90,6 +92,7 @@ function extensionContext(
|
|
|
90
92
|
goalState?: unknown;
|
|
91
93
|
branch?: unknown[];
|
|
92
94
|
liveState?: { isStreaming: boolean; steeringQueueDepth: number; followupQueueDepth: number };
|
|
95
|
+
transcript?: ExtensionTranscriptEntry[];
|
|
93
96
|
} = {},
|
|
94
97
|
): ExtensionContext {
|
|
95
98
|
return {
|
|
@@ -102,6 +105,7 @@ function extensionContext(
|
|
|
102
105
|
getSessionName: () => undefined,
|
|
103
106
|
getBranch: () => options.branch ?? [],
|
|
104
107
|
},
|
|
108
|
+
getTranscript: () => options.transcript ?? [],
|
|
105
109
|
getGoalState: () => options.goalState,
|
|
106
110
|
} as unknown as ExtensionContext;
|
|
107
111
|
}
|
|
@@ -148,6 +152,97 @@ async function queryGoalState(
|
|
|
148
152
|
return response.page?.items[0];
|
|
149
153
|
}
|
|
150
154
|
|
|
155
|
+
async function queryLastAssistant(ctx: ExtensionContext, sessionId: string): Promise<QueryResponse> {
|
|
156
|
+
const surface = createSdkSurfaceFactory({
|
|
157
|
+
ctx,
|
|
158
|
+
id: sessionId,
|
|
159
|
+
api: {} as ExtensionAPI,
|
|
160
|
+
}).query;
|
|
161
|
+
const store = new RevisionStore(sessionId);
|
|
162
|
+
const response = await new QueryHandlers(
|
|
163
|
+
surface,
|
|
164
|
+
sessionId,
|
|
165
|
+
store,
|
|
166
|
+
new CursorRegistry("last-assistant-test-token", store),
|
|
167
|
+
).dispatch({
|
|
168
|
+
query: "session.last_assistant",
|
|
169
|
+
connectionId: "last-assistant-test-connection",
|
|
170
|
+
});
|
|
171
|
+
return response;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
test("session.last_assistant returns the latest projected readable text past non-text assistant tails", async () => {
|
|
175
|
+
const sessionId = "last-assistant-readable-tail";
|
|
176
|
+
const transcript: ExtensionTranscriptEntry[] = [
|
|
177
|
+
{
|
|
178
|
+
id: "visible-readable",
|
|
179
|
+
role: "assistant",
|
|
180
|
+
textSummary: "first line second line",
|
|
181
|
+
ts: new Date(1).toISOString(),
|
|
182
|
+
content: [
|
|
183
|
+
{ type: "text", text: "first line" },
|
|
184
|
+
{ type: "thinking", thinking: "private reasoning" },
|
|
185
|
+
{ type: "text", text: "second line" },
|
|
186
|
+
],
|
|
187
|
+
},
|
|
188
|
+
...Array.from(
|
|
189
|
+
{ length: 2_000 },
|
|
190
|
+
(_, index): ExtensionTranscriptEntry => ({
|
|
191
|
+
id: `tool-tail-${index}`,
|
|
192
|
+
role: "assistant",
|
|
193
|
+
textSummary: "",
|
|
194
|
+
ts: new Date(index + 2).toISOString(),
|
|
195
|
+
body: index % 3 === 0 ? "" : index % 3 === 1 ? " \n\t " : undefined,
|
|
196
|
+
content:
|
|
197
|
+
index % 3 === 2
|
|
198
|
+
? [
|
|
199
|
+
{ type: "thinking", thinking: "not readable" },
|
|
200
|
+
{ type: "toolCall", id: `call-${index}`, name: "read", arguments: {} },
|
|
201
|
+
]
|
|
202
|
+
: undefined,
|
|
203
|
+
}),
|
|
204
|
+
),
|
|
205
|
+
];
|
|
206
|
+
const ctx = extensionContext(sessionId, "/tmp", {
|
|
207
|
+
transcript,
|
|
208
|
+
branch: [
|
|
209
|
+
{
|
|
210
|
+
type: "message",
|
|
211
|
+
message: { role: "assistant", content: [{ type: "text", text: "raw private tail" }] },
|
|
212
|
+
},
|
|
213
|
+
],
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
expect(await queryLastAssistant(ctx, sessionId)).toMatchObject({
|
|
217
|
+
ok: true,
|
|
218
|
+
page: { items: ["first line\nsecond line"] },
|
|
219
|
+
});
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
test("session.last_assistant returns resource_gone when the projected transcript has no readable assistant text", async () => {
|
|
223
|
+
const sessionId = "last-assistant-empty";
|
|
224
|
+
const ctx = extensionContext(sessionId, "/tmp", {
|
|
225
|
+
transcript: [
|
|
226
|
+
{
|
|
227
|
+
id: "user",
|
|
228
|
+
role: "user",
|
|
229
|
+
textSummary: "hello",
|
|
230
|
+
ts: new Date(1).toISOString(),
|
|
231
|
+
body: "hello",
|
|
232
|
+
},
|
|
233
|
+
{
|
|
234
|
+
id: "thinking-only",
|
|
235
|
+
role: "assistant",
|
|
236
|
+
textSummary: "",
|
|
237
|
+
ts: new Date(2).toISOString(),
|
|
238
|
+
content: [{ type: "thinking", thinking: "private reasoning" }],
|
|
239
|
+
},
|
|
240
|
+
],
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
expect(await queryLastAssistant(ctx, sessionId)).toMatchObject({ ok: false, error: { code: "resource_gone" } });
|
|
244
|
+
});
|
|
245
|
+
|
|
151
246
|
test("native prompt reconciliation fails closed for an explicitly empty assistant result", () => {
|
|
152
247
|
const reconciliation = createPromptReconciliation();
|
|
153
248
|
const correlation = { commandId: "native-empty-command", turnId: "native-empty-turn" };
|
|
@@ -160,7 +255,10 @@ test("native prompt reconciliation fails closed for an explicitly empty assistan
|
|
|
160
255
|
receiptState: "missing",
|
|
161
256
|
});
|
|
162
257
|
reconciliation.noteTransition(correlation, { type: "agent_end", finalText: "late text" });
|
|
163
|
-
expect(reconciliation.lookup({ clientRef: "native-empty-ref" })).toMatchObject({
|
|
258
|
+
expect(reconciliation.lookup({ clientRef: "native-empty-ref" })).toMatchObject({
|
|
259
|
+
status: "failed",
|
|
260
|
+
receiptState: "present",
|
|
261
|
+
});
|
|
164
262
|
});
|
|
165
263
|
|
|
166
264
|
test("broker reconciliation fails closed for an empty prompt and persists the first terminal result", async () => {
|
|
@@ -189,7 +287,9 @@ test("broker reconciliation fails closed for an empty prompt and persists the fi
|
|
|
189
287
|
expect(reloaded.lookupResult("prompt", { clientRef: "broker-empty-ref" })).toMatchObject({
|
|
190
288
|
status: "failed",
|
|
191
289
|
error: { code: "prompt_failed" },
|
|
290
|
+
content: { text: "late text", byteLength: 9, truncated: false },
|
|
192
291
|
});
|
|
292
|
+
expect(reloaded.lookup("prompt", { clientRef: "broker-empty-ref" })).toMatchObject({ receiptState: "present" });
|
|
193
293
|
const noOpCorrelation = { commandId: "broker-noop-command", turnId: "broker-noop-turn" };
|
|
194
294
|
await reconciliation.noteAccepted("prompt", noOpCorrelation, "broker-noop-ref");
|
|
195
295
|
await reconciliation.finalizeOutcome(
|
|
@@ -352,6 +452,42 @@ test("SDK-only finalizeOutcome keeps legacy recordError positional compatibility
|
|
|
352
452
|
});
|
|
353
453
|
});
|
|
354
454
|
|
|
455
|
+
test("SDK-only terminal outcomes survive result projection and durable reload", async () => {
|
|
456
|
+
let records: unknown[] = [];
|
|
457
|
+
const store = {
|
|
458
|
+
path: null,
|
|
459
|
+
load: async () => records,
|
|
460
|
+
transact: async (mutator: (current: never[]) => never[]) => {
|
|
461
|
+
records = mutator(records as never);
|
|
462
|
+
},
|
|
463
|
+
} as never;
|
|
464
|
+
const stopped = { kind: "stopped", reason: "cancelled", provenance: "client_cancel" } as const;
|
|
465
|
+
const reconciliation = createInvocationReconciliation({ store });
|
|
466
|
+
const correlation = { commandId: "outcome-command", turnId: "outcome-turn" };
|
|
467
|
+
await reconciliation.noteAccepted("prompt", correlation, "outcome-ref");
|
|
468
|
+
await reconciliation.claimPendingOutcome("prompt", correlation, stopped);
|
|
469
|
+
await reconciliation.finalizeOutcome("prompt", correlation);
|
|
470
|
+
const projected = reconciliation.lookupResult("prompt", { clientRef: "outcome-ref" });
|
|
471
|
+
expect(projected).toMatchObject({ status: "terminal_ok", outcome: stopped, receiptState: "unknown" });
|
|
472
|
+
expect(reconciliation.lookup("prompt", { clientRef: "outcome-ref" })).toMatchObject({ outcome: stopped });
|
|
473
|
+
|
|
474
|
+
const reopened = createInvocationReconciliation({ store });
|
|
475
|
+
await reopened.hydrate();
|
|
476
|
+
expect(reopened.lookupResult("prompt", { clientRef: "outcome-ref" })).toMatchObject({
|
|
477
|
+
status: "terminal_ok",
|
|
478
|
+
outcome: stopped,
|
|
479
|
+
});
|
|
480
|
+
|
|
481
|
+
const direct = createInvocationReconciliation();
|
|
482
|
+
const directCorrelation = { commandId: "direct-outcome-command", turnId: "direct-outcome-turn" };
|
|
483
|
+
await direct.noteAccepted("prompt", directCorrelation, "direct-outcome-ref");
|
|
484
|
+
await direct.noteTransition("prompt", directCorrelation, { type: "agent_end", outcome: stopped });
|
|
485
|
+
expect(direct.lookupResult("prompt", { clientRef: "direct-outcome-ref" })).toMatchObject({
|
|
486
|
+
status: "terminal_ok",
|
|
487
|
+
outcome: stopped,
|
|
488
|
+
});
|
|
489
|
+
});
|
|
490
|
+
|
|
355
491
|
test("a late agent failure never overwrites the reason an already terminal record carries", async () => {
|
|
356
492
|
const reconciliation = createInvocationReconciliation();
|
|
357
493
|
const correlation = { commandId: "first-reason-command", turnId: "first-reason-turn" };
|
|
@@ -399,7 +535,7 @@ test("a failed acceptance persist rolls back the provisional record and reservat
|
|
|
399
535
|
"accept persist failed",
|
|
400
536
|
);
|
|
401
537
|
reconciliation.release("prompt", "accept-failed-ref");
|
|
402
|
-
expect(reconciliation.lookup("prompt", first)).toEqual({ status: "unknown" });
|
|
538
|
+
expect(reconciliation.lookup("prompt", first)).toEqual({ status: "unknown", receiptState: "unknown" });
|
|
403
539
|
|
|
404
540
|
const retry = { commandId: "accept-retry-command", turnId: "accept-retry-turn" };
|
|
405
541
|
reconciliation.admit("prompt", "accept-failed-ref");
|
|
@@ -454,6 +590,54 @@ test("a failed uncertainty persist restores the durable deadline terminal until
|
|
|
454
590
|
]);
|
|
455
591
|
});
|
|
456
592
|
|
|
593
|
+
test("SDK-only markUncertain persists an active-schema record without quarantining siblings", async () => {
|
|
594
|
+
const root = await mkdtemp(path.join(os.tmpdir(), "gjc-sdk-uncertain-reload-"));
|
|
595
|
+
try {
|
|
596
|
+
const sessionFile = path.join(root, "session.jsonl");
|
|
597
|
+
await Bun.write(sessionFile, "");
|
|
598
|
+
const store = createReconciliationStore({ sessionFile, sessionId: "uncertain-reload" });
|
|
599
|
+
const reconciliation = createInvocationReconciliation({ store });
|
|
600
|
+
const correlation = { commandId: "uncertain-reload-command", turnId: "uncertain-reload-turn" };
|
|
601
|
+
const sibling = { commandId: "uncertain-sibling-command", turnId: "uncertain-sibling-turn" };
|
|
602
|
+
await reconciliation.noteAccepted("prompt", correlation, "uncertain-reload-ref");
|
|
603
|
+
await reconciliation.finalizeOutcome("prompt", correlation, {
|
|
604
|
+
kind: "failed",
|
|
605
|
+
code: "prompt_deadline_exceeded",
|
|
606
|
+
message: "deadline",
|
|
607
|
+
});
|
|
608
|
+
await reconciliation.noteAccepted("skill", sibling, "uncertain-sibling-ref");
|
|
609
|
+
await reconciliation.finalizeOutcome("skill", sibling, {
|
|
610
|
+
kind: "stopped",
|
|
611
|
+
code: "cancelled",
|
|
612
|
+
message: "cancelled",
|
|
613
|
+
});
|
|
614
|
+
|
|
615
|
+
await reconciliation.markUncertain("prompt", correlation, undefined, 5_000);
|
|
616
|
+
const active = store
|
|
617
|
+
.snapshot()
|
|
618
|
+
.find(record => record.kind === "prompt" && record.commandId === correlation.commandId) as
|
|
619
|
+
| Record<string, unknown>
|
|
620
|
+
| undefined;
|
|
621
|
+
expect(active).toMatchObject({ status: "accepted", deadlineRecoveryPending: true, deadlineMaxAt: 5_000 });
|
|
622
|
+
expect(active?.terminalAt).toBeUndefined();
|
|
623
|
+
expect(active?.receiptState).toBeUndefined();
|
|
624
|
+
expect(active?.outcome).toBeUndefined();
|
|
625
|
+
expect(active?.pendingOutcome).toBeUndefined();
|
|
626
|
+
expect(active?.pendingReceiptState).toBeUndefined();
|
|
627
|
+
|
|
628
|
+
const reopenedStore = createReconciliationStore({ sessionFile, sessionId: "uncertain-reload" });
|
|
629
|
+
const reopened = createInvocationReconciliation({ store: reopenedStore });
|
|
630
|
+
await reopened.hydrate();
|
|
631
|
+
expect(reopened.lookup("prompt", { clientRef: "uncertain-reload-ref" })).toMatchObject({ status: "accepted" });
|
|
632
|
+
expect(reopened.lookup("skill", { clientRef: "uncertain-sibling-ref" })).toMatchObject({
|
|
633
|
+
status: "terminal_ok",
|
|
634
|
+
});
|
|
635
|
+
expect((await readdir(root)).some(name => name.includes("corrupt"))).toBe(false);
|
|
636
|
+
} finally {
|
|
637
|
+
await rm(root, { recursive: true, force: true });
|
|
638
|
+
}
|
|
639
|
+
});
|
|
640
|
+
|
|
457
641
|
test("durable reload keeps agent_end terminal across a paused successor transition", async () => {
|
|
458
642
|
let records: unknown[] = [];
|
|
459
643
|
let pauseWrites = 0;
|
|
@@ -817,8 +1001,13 @@ test("finalization without an explicit outcome follows the noteTransition eviden
|
|
|
817
1001
|
await legacyArg5.finalizeOutcome("prompt", legacyCorrelation, undefined, undefined, {
|
|
818
1002
|
content: { version: 1, type: "text", text: "legacy", byteLength: 6, truncated: false },
|
|
819
1003
|
});
|
|
820
|
-
|
|
1004
|
+
await legacyArg5.noteTransition("prompt", legacyCorrelation, {
|
|
1005
|
+
type: "agent_end",
|
|
1006
|
+
content: { version: 1, type: "text", text: "later", byteLength: 5, truncated: false },
|
|
1007
|
+
});
|
|
1008
|
+
expect(legacyArg5.lookupResult("prompt", { clientRef: "final-legacy-ref" })).toMatchObject({
|
|
821
1009
|
status: "terminal_ok",
|
|
1010
|
+
content: { text: "legacy", byteLength: 6, truncated: false },
|
|
822
1011
|
});
|
|
823
1012
|
|
|
824
1013
|
const explicitFailure = createInvocationReconciliation({ store: makeLocalStore() });
|
|
@@ -830,9 +1019,58 @@ test("finalization without an explicit outcome follows the noteTransition eviden
|
|
|
830
1019
|
message: "provider rejected",
|
|
831
1020
|
});
|
|
832
1021
|
expect(explicitFailure.lookup("prompt", { clientRef: "final-failure-ref" })).toMatchObject({
|
|
1022
|
+
status: "failed",
|
|
1023
|
+
receiptState: "unknown",
|
|
1024
|
+
outcome: { kind: "failed", code: "prompt_failed", provenance: "agent_failed" },
|
|
1025
|
+
error: { code: "prompt_failed" },
|
|
1026
|
+
});
|
|
1027
|
+
});
|
|
1028
|
+
|
|
1029
|
+
test("provider failures outrank later stopped outcomes in both SDK-only terminal paths", async () => {
|
|
1030
|
+
const providerError = Object.assign(new Error("provider rejected"), { code: "provider_rejected" });
|
|
1031
|
+
const transition = createInvocationReconciliation();
|
|
1032
|
+
const transitionCorrelation = { commandId: "precedence-transition-command", turnId: "precedence-transition-turn" };
|
|
1033
|
+
await transition.noteAccepted("prompt", transitionCorrelation, "precedence-transition-ref");
|
|
1034
|
+
await transition.noteTransition("prompt", transitionCorrelation, { type: "agent_failed", error: providerError });
|
|
1035
|
+
await transition.noteTransition("prompt", transitionCorrelation, {
|
|
1036
|
+
type: "agent_end",
|
|
1037
|
+
outcome: { kind: "stopped", reason: "cancelled", provenance: "client_cancel" },
|
|
1038
|
+
});
|
|
1039
|
+
expect(transition.lookupResult("prompt", { clientRef: "precedence-transition-ref" })).toMatchObject({
|
|
1040
|
+
status: "failed",
|
|
1041
|
+
error: { code: "provider_rejected" },
|
|
1042
|
+
});
|
|
1043
|
+
|
|
1044
|
+
const finalized = createInvocationReconciliation();
|
|
1045
|
+
const finalizedCorrelation = { commandId: "precedence-finalize-command", turnId: "precedence-finalize-turn" };
|
|
1046
|
+
await finalized.noteAccepted("prompt", finalizedCorrelation, "precedence-finalize-ref");
|
|
1047
|
+
await finalized.noteTransition("prompt", finalizedCorrelation, { type: "agent_failed", error: providerError });
|
|
1048
|
+
await finalized.claimPendingOutcome("prompt", finalizedCorrelation, {
|
|
1049
|
+
kind: "failed",
|
|
1050
|
+
code: "prompt_deadline_exceeded",
|
|
1051
|
+
message: "deadline",
|
|
1052
|
+
});
|
|
1053
|
+
await finalized.finalizeOutcome("prompt", finalizedCorrelation);
|
|
1054
|
+
expect(finalized.lookupResult("prompt", { clientRef: "precedence-finalize-ref" })).toMatchObject({
|
|
833
1055
|
status: "failed",
|
|
834
1056
|
error: { code: "provider_rejected" },
|
|
835
1057
|
});
|
|
1058
|
+
|
|
1059
|
+
const generic = createInvocationReconciliation();
|
|
1060
|
+
const genericCorrelation = { commandId: "precedence-generic-command", turnId: "precedence-generic-turn" };
|
|
1061
|
+
await generic.noteAccepted("prompt", genericCorrelation, "precedence-generic-ref");
|
|
1062
|
+
await generic.noteTransition("prompt", genericCorrelation, {
|
|
1063
|
+
type: "agent_failed",
|
|
1064
|
+
error: Object.assign(new Error("generic failure"), { code: "agent_failed" }),
|
|
1065
|
+
});
|
|
1066
|
+
await generic.noteTransition("prompt", genericCorrelation, {
|
|
1067
|
+
type: "agent_end",
|
|
1068
|
+
outcome: { kind: "stopped", reason: "cancelled", provenance: "client_cancel" },
|
|
1069
|
+
});
|
|
1070
|
+
expect(generic.lookupResult("prompt", { clientRef: "precedence-generic-ref" })).toMatchObject({
|
|
1071
|
+
status: "failed",
|
|
1072
|
+
error: { code: "agent_failed", message: "Prompt submission failed." },
|
|
1073
|
+
});
|
|
836
1074
|
});
|
|
837
1075
|
|
|
838
1076
|
test("a reason attached after a prompt settled is never replaced by a later failure", async () => {
|
|
@@ -851,6 +1089,8 @@ test("a reason attached after a prompt settled is never replaced by a later fail
|
|
|
851
1089
|
clientRef: "late-reason-ref",
|
|
852
1090
|
acceptedAt: expect.any(Number),
|
|
853
1091
|
terminalAt: expect.any(Number),
|
|
1092
|
+
receiptState: "present",
|
|
1093
|
+
content: { version: 1, type: "text", text: "completed", byteLength: 9, truncated: false },
|
|
854
1094
|
});
|
|
855
1095
|
// A failure delivered after the record settled enriches it with the sanitized reason and
|
|
856
1096
|
// leaves the terminal claim itself (status, terminalAt, identity) untouched.
|
|
@@ -958,6 +1198,7 @@ test("capacity eviction still releases the clientRef and reports honest unknown"
|
|
|
958
1198
|
}
|
|
959
1199
|
expect(reconciliation.lookup("prompt", { clientRef: "capacity-ref-1" })).toEqual({
|
|
960
1200
|
status: "unknown",
|
|
1201
|
+
receiptState: "unknown",
|
|
961
1202
|
});
|
|
962
1203
|
expect(() => reconciliation.admit("prompt", "capacity-ref-1")).not.toThrow();
|
|
963
1204
|
const retained = {
|
|
@@ -1008,8 +1249,114 @@ describe("SessionSdkSessionRuntime", () => {
|
|
|
1008
1249
|
);
|
|
1009
1250
|
await runtime.stop();
|
|
1010
1251
|
});
|
|
1252
|
+
test("SDK-only host publishes one replayable bash_folded frame per fold and drops the subscription on shutdown", async () => {
|
|
1253
|
+
const cwd = await mkdtemp(path.join(os.tmpdir(), "gjc-sdk-bash-folded-"));
|
|
1254
|
+
const handlers = new Map<string, (event: unknown, ctx: ExtensionContext) => Promise<void> | void>();
|
|
1255
|
+
const api = {
|
|
1256
|
+
on(event: string, handler: (event: unknown, ctx: ExtensionContext) => Promise<void> | void) {
|
|
1257
|
+
handlers.set(event, handler);
|
|
1258
|
+
},
|
|
1259
|
+
sendUserMessage: async () => {},
|
|
1260
|
+
} as unknown as ExtensionAPI;
|
|
1261
|
+
const transport = memoryTransport();
|
|
1262
|
+
createSdkSessionRuntimeExtension(api, { agentDir: cwd, createTransport: async () => transport });
|
|
1263
|
+
const listeners = new Set<(event: { jobId: string; generation: string; reason: string }) => void>();
|
|
1264
|
+
const ctx = {
|
|
1265
|
+
...extensionContext(transport.sessionId, cwd),
|
|
1266
|
+
onJobFold: (listener: (event: { jobId: string; generation: string; reason: string }) => void) => {
|
|
1267
|
+
listeners.add(listener);
|
|
1268
|
+
return () => listeners.delete(listener);
|
|
1269
|
+
},
|
|
1270
|
+
} as unknown as ExtensionContext;
|
|
1271
|
+
try {
|
|
1272
|
+
await handlers.get("session_start")?.({}, ctx);
|
|
1273
|
+
expect(listeners.size).toBe(1);
|
|
1274
|
+
for (const listener of listeners) listener({ jobId: "bg_7", generation: "bg_7:1", reason: "steer" });
|
|
1275
|
+
transport.feed("client", { type: "event_replay", id: "fold-replay", sinceSeq: 0 });
|
|
1276
|
+
await Bun.sleep(0);
|
|
1277
|
+
const replay = transport.sent.find(
|
|
1278
|
+
frame => frame.type === "event_replay_result" && frame.id === "fold-replay",
|
|
1279
|
+
) as { events?: Array<{ kind?: unknown; payload?: Record<string, unknown> }> } | undefined;
|
|
1280
|
+
const folded = replay?.events?.filter(event => event.kind === "bash_folded") ?? [];
|
|
1281
|
+
expect(folded).toEqual([
|
|
1282
|
+
expect.objectContaining({
|
|
1283
|
+
kind: "bash_folded",
|
|
1284
|
+
payload: {
|
|
1285
|
+
type: "bash_folded",
|
|
1286
|
+
sessionId: transport.sessionId,
|
|
1287
|
+
jobId: "bg_7",
|
|
1288
|
+
generation: "bg_7:1",
|
|
1289
|
+
reason: "steer",
|
|
1290
|
+
},
|
|
1291
|
+
}),
|
|
1292
|
+
]);
|
|
1293
|
+
} finally {
|
|
1294
|
+
await handlers.get("session_shutdown")?.({}, ctx);
|
|
1295
|
+
}
|
|
1296
|
+
expect(listeners.size).toBe(0);
|
|
1297
|
+
});
|
|
1298
|
+
|
|
1299
|
+
test("SDK-only host logs a bash_folded publication failure and keeps the fold replayable", async () => {
|
|
1300
|
+
const cwd = await mkdtemp(path.join(os.tmpdir(), "gjc-sdk-bash-folded-fail-"));
|
|
1301
|
+
const handlers = new Map<string, (event: unknown, ctx: ExtensionContext) => Promise<void> | void>();
|
|
1302
|
+
const api = {
|
|
1303
|
+
on(event: string, handler: (event: unknown, ctx: ExtensionContext) => Promise<void> | void) {
|
|
1304
|
+
handlers.set(event, handler);
|
|
1305
|
+
},
|
|
1306
|
+
sendUserMessage: async () => {},
|
|
1307
|
+
} as unknown as ExtensionAPI;
|
|
1308
|
+
const transport = memoryTransport();
|
|
1309
|
+
const broadcastFrame = transport.broadcastFrame?.bind(transport);
|
|
1310
|
+
let failNextBroadcast = false;
|
|
1311
|
+
transport.broadcastFrame = frame => {
|
|
1312
|
+
if (failNextBroadcast && frame.kind === "bash_folded") {
|
|
1313
|
+
failNextBroadcast = false;
|
|
1314
|
+
throw new Error("broadcast socket closed");
|
|
1315
|
+
}
|
|
1316
|
+
broadcastFrame?.(frame);
|
|
1317
|
+
};
|
|
1318
|
+
createSdkSessionRuntimeExtension(api, { agentDir: cwd, createTransport: async () => transport });
|
|
1319
|
+
const listeners = new Set<(event: { jobId: string; generation: string; reason: string }) => void>();
|
|
1320
|
+
const ctx = {
|
|
1321
|
+
...extensionContext(transport.sessionId, cwd),
|
|
1322
|
+
onJobFold: (listener: (event: { jobId: string; generation: string; reason: string }) => void) => {
|
|
1323
|
+
listeners.add(listener);
|
|
1324
|
+
return () => listeners.delete(listener);
|
|
1325
|
+
},
|
|
1326
|
+
} as unknown as ExtensionContext;
|
|
1327
|
+
const warnings: Array<{ message: string; data?: Record<string, unknown> }> = [];
|
|
1328
|
+
const warn = spyOn(logger, "warn").mockImplementation((message, data) => {
|
|
1329
|
+
warnings.push({ message: String(message), data: data as Record<string, unknown> | undefined });
|
|
1330
|
+
});
|
|
1331
|
+
try {
|
|
1332
|
+
await handlers.get("session_start")?.({}, ctx);
|
|
1333
|
+
failNextBroadcast = true;
|
|
1334
|
+
// The listener call must not throw back into the manager: publication failure is evidence, not a fold error.
|
|
1335
|
+
for (const listener of listeners) listener({ jobId: "bg_9", generation: "bg_9:1", reason: "chord" });
|
|
1336
|
+
expect(warnings.filter(entry => entry.message === "sdk: bash_folded publication failed")).toEqual([
|
|
1337
|
+
expect.objectContaining({
|
|
1338
|
+
data: expect.objectContaining({ jobId: "bg_9", reason: "chord", error: "broadcast socket closed" }),
|
|
1339
|
+
}),
|
|
1340
|
+
]);
|
|
1341
|
+
// The frame was appended to the ring before the broadcast failed, so it still replays.
|
|
1342
|
+
transport.feed("client", { type: "event_replay", id: "fold-replay-fail", sinceSeq: 0 });
|
|
1343
|
+
await Bun.sleep(0);
|
|
1344
|
+
const replay = transport.sent.find(
|
|
1345
|
+
frame => frame.type === "event_replay_result" && frame.id === "fold-replay-fail",
|
|
1346
|
+
) as { events?: Array<{ kind?: unknown; payload?: Record<string, unknown> }> } | undefined;
|
|
1347
|
+
expect(replay?.events?.filter(event => event.kind === "bash_folded")).toEqual([
|
|
1348
|
+
expect.objectContaining({ payload: expect.objectContaining({ jobId: "bg_9", reason: "chord" }) }),
|
|
1349
|
+
]);
|
|
1350
|
+
} finally {
|
|
1351
|
+
warn.mockRestore();
|
|
1352
|
+
await handlers.get("session_shutdown")?.({}, ctx);
|
|
1353
|
+
}
|
|
1354
|
+
});
|
|
1355
|
+
|
|
1011
1356
|
test("SDK-only host admits, replays, and conflicts terminal abort requests durably", async () => {
|
|
1012
1357
|
const cwd = await mkdtemp(path.join(os.tmpdir(), "gjc-sdk-terminal-abort-"));
|
|
1358
|
+
const operatorCapability = "runtime-terminal-abort-capability";
|
|
1359
|
+
setBrokerRuntimeAbortCapabilityForTest(operatorCapability);
|
|
1013
1360
|
const handlers = new Map<string, (event: unknown, ctx: ExtensionContext) => Promise<void> | void>();
|
|
1014
1361
|
const api = {
|
|
1015
1362
|
on(event: string, handler: (event: unknown, ctx: ExtensionContext) => Promise<void> | void) {
|
|
@@ -1045,7 +1392,13 @@ describe("SessionSdkSessionRuntime", () => {
|
|
|
1045
1392
|
},
|
|
1046
1393
|
abortPromptAndWaitWithTerminal: async (handle, options) => {
|
|
1047
1394
|
seamCalls.push({ handle, scope: options.terminal?.scope ?? "none" });
|
|
1048
|
-
return {
|
|
1395
|
+
return {
|
|
1396
|
+
status: "settled",
|
|
1397
|
+
terminalScope: {
|
|
1398
|
+
abortedAttemptEpoch: activeEpoch,
|
|
1399
|
+
lineageIdHash: "runtime-test-lineage",
|
|
1400
|
+
},
|
|
1401
|
+
};
|
|
1049
1402
|
},
|
|
1050
1403
|
},
|
|
1051
1404
|
});
|
|
@@ -1140,7 +1493,43 @@ describe("SessionSdkSessionRuntime", () => {
|
|
|
1140
1493
|
}),
|
|
1141
1494
|
]),
|
|
1142
1495
|
);
|
|
1496
|
+
|
|
1497
|
+
// A separately connected, explicitly confirmed local operator may stop
|
|
1498
|
+
// the active turn and its exact owned work without weakening ordinary
|
|
1499
|
+
// connection ownership checks.
|
|
1500
|
+
transport.feed("local-operator", {
|
|
1501
|
+
type: "control_request",
|
|
1502
|
+
id: "terminal-operator-abort",
|
|
1503
|
+
operation: "turn.abort",
|
|
1504
|
+
input: {
|
|
1505
|
+
mode: "terminal",
|
|
1506
|
+
scope: "owned",
|
|
1507
|
+
operator: true,
|
|
1508
|
+
[BROKER_RUNTIME_ABORT_CAPABILITY_FIELD]: operatorCapability,
|
|
1509
|
+
},
|
|
1510
|
+
confirm: true,
|
|
1511
|
+
idempotencyKey: "terminal-operator-key",
|
|
1512
|
+
} as SdkFrame);
|
|
1513
|
+
await waitForFrame("terminal-operator-abort");
|
|
1514
|
+
expect(seamCalls).toEqual([
|
|
1515
|
+
{ handle: "exact-run-handle", scope: "turn" },
|
|
1516
|
+
{ handle: "later-run-handle", scope: "owned" },
|
|
1517
|
+
]);
|
|
1518
|
+
expect(transport.sent.find(frame => frame.id === "terminal-operator-abort")).toMatchObject({
|
|
1519
|
+
ok: true,
|
|
1520
|
+
result: expect.objectContaining({ turn: "stopped", ownedWork: "stopped" }),
|
|
1521
|
+
});
|
|
1522
|
+
const operatorKeyHash = createHash("sha256").update("terminal-operator-key").digest("hex");
|
|
1523
|
+
const deliveryDeadline = Date.now() + 15_000;
|
|
1524
|
+
while (
|
|
1525
|
+
reconciliationStore.snapshotTerminalScopes().find(record => record.idempotencyKeyHash === operatorKeyHash)
|
|
1526
|
+
?.responseState !== "sent"
|
|
1527
|
+
) {
|
|
1528
|
+
if (Date.now() > deliveryDeadline) throw new Error("Timed out waiting for operator delivery receipt");
|
|
1529
|
+
await Bun.sleep(20);
|
|
1530
|
+
}
|
|
1143
1531
|
} finally {
|
|
1532
|
+
setBrokerRuntimeAbortCapabilityForTest(undefined);
|
|
1144
1533
|
await handlers.get("session_shutdown")?.({}, ctx);
|
|
1145
1534
|
await rm(cwd, { recursive: true, force: true });
|
|
1146
1535
|
}
|
|
@@ -1351,7 +1740,71 @@ describe("SessionSdkSessionRuntime", () => {
|
|
|
1351
1740
|
}
|
|
1352
1741
|
});
|
|
1353
1742
|
|
|
1354
|
-
test("SDK-only
|
|
1743
|
+
test("SDK-only operator abort never adopts a turn that starts after idle admission", async () => {
|
|
1744
|
+
const cwd = await mkdtemp(path.join(os.tmpdir(), "gjc-sdk-operator-idle-race-"));
|
|
1745
|
+
const capability = "operator-idle-race-capability";
|
|
1746
|
+
setBrokerRuntimeAbortCapabilityForTest(capability);
|
|
1747
|
+
const handlers = new Map<string, (event: unknown, ctx: ExtensionContext) => Promise<void> | void>();
|
|
1748
|
+
const api = {
|
|
1749
|
+
on(event: string, handler: (event: unknown, ctx: ExtensionContext) => Promise<void> | void) {
|
|
1750
|
+
handlers.set(event, handler);
|
|
1751
|
+
},
|
|
1752
|
+
} as unknown as ExtensionAPI;
|
|
1753
|
+
const transport = memoryTransport();
|
|
1754
|
+
const reconciliationStore = createReconciliationStore({
|
|
1755
|
+
sessionFile: path.join(cwd, "session.json"),
|
|
1756
|
+
sessionId: transport.sessionId,
|
|
1757
|
+
});
|
|
1758
|
+
const seamCalls: string[] = [];
|
|
1759
|
+
let promptReads = 0;
|
|
1760
|
+
createSdkSessionRuntimeExtension(api, {
|
|
1761
|
+
agentDir: cwd,
|
|
1762
|
+
createTransport: async () => transport,
|
|
1763
|
+
terminalAbortSeams: {
|
|
1764
|
+
getReconciliationStore: () => reconciliationStore,
|
|
1765
|
+
getTerminalTurnEpoch: () => (promptReads++ === 0 ? undefined : 9),
|
|
1766
|
+
getActivePromptHandle: () => (promptReads === 0 ? undefined : "successor-handle"),
|
|
1767
|
+
getActivePromptOwnerConnectionId: () => "successor-owner",
|
|
1768
|
+
cancelPendingPreflightForTerminalAbort: () => {},
|
|
1769
|
+
abortPromptAndWaitWithTerminal: async handle => {
|
|
1770
|
+
seamCalls.push(handle);
|
|
1771
|
+
return { status: "settled", terminalScope: {} };
|
|
1772
|
+
},
|
|
1773
|
+
},
|
|
1774
|
+
});
|
|
1775
|
+
const ctx = extensionContext(transport.sessionId, cwd);
|
|
1776
|
+
try {
|
|
1777
|
+
await handlers.get("session_start")?.({}, ctx);
|
|
1778
|
+
transport.feed("local-operator", {
|
|
1779
|
+
type: "control_request",
|
|
1780
|
+
id: "operator-idle-race",
|
|
1781
|
+
operation: "turn.abort",
|
|
1782
|
+
input: {
|
|
1783
|
+
mode: "terminal",
|
|
1784
|
+
operator: true,
|
|
1785
|
+
[BROKER_RUNTIME_ABORT_CAPABILITY_FIELD]: capability,
|
|
1786
|
+
},
|
|
1787
|
+
confirm: true,
|
|
1788
|
+
idempotencyKey: "operator-idle-race-key",
|
|
1789
|
+
} as SdkFrame);
|
|
1790
|
+
const deadline = Date.now() + 15_000;
|
|
1791
|
+
while (!transport.sent.some(frame => frame.id === "operator-idle-race")) {
|
|
1792
|
+
if (Date.now() > deadline) throw new Error("Timed out waiting for operator idle-race response");
|
|
1793
|
+
await Bun.sleep(20);
|
|
1794
|
+
}
|
|
1795
|
+
expect(transport.sent.find(frame => frame.id === "operator-idle-race")).toMatchObject({
|
|
1796
|
+
ok: true,
|
|
1797
|
+
result: expect.objectContaining({ turn: "no_active_turn", terminal: "terminal_no_effect" }),
|
|
1798
|
+
});
|
|
1799
|
+
expect(seamCalls).toHaveLength(0);
|
|
1800
|
+
} finally {
|
|
1801
|
+
setBrokerRuntimeAbortCapabilityForTest(undefined);
|
|
1802
|
+
await handlers.get("session_shutdown")?.({}, ctx);
|
|
1803
|
+
await rm(cwd, { recursive: true, force: true });
|
|
1804
|
+
}
|
|
1805
|
+
});
|
|
1806
|
+
|
|
1807
|
+
test("SDK-only host re-reads the active prompt AND its owner after an owner-mismatch reservation race", async () => {
|
|
1355
1808
|
const cwd = await mkdtemp(path.join(os.tmpdir(), "gjc-sdk-terminal-owner-race-"));
|
|
1356
1809
|
const handlers = new Map<string, (event: unknown, ctx: ExtensionContext) => Promise<void> | void>();
|
|
1357
1810
|
const api = {
|
|
@@ -3172,6 +3625,7 @@ describe("SessionSdkSessionRuntime", () => {
|
|
|
3172
3625
|
},
|
|
3173
3626
|
} as any;
|
|
3174
3627
|
const sessionId = "broker-recovery";
|
|
3628
|
+
const endpointUrl = "ws://127.0.0.1:1";
|
|
3175
3629
|
createSdkSessionRuntimeExtension(api, {
|
|
3176
3630
|
agentDir,
|
|
3177
3631
|
createTransport: async ({ stateRoot, token }) => ({
|
|
@@ -3183,8 +3637,8 @@ describe("SessionSdkSessionRuntime", () => {
|
|
|
3183
3637
|
start: async () => {
|
|
3184
3638
|
const endpoint = path.join(stateRoot, "sdk", `${sessionId}.json`);
|
|
3185
3639
|
await mkdir(path.dirname(endpoint), { recursive: true });
|
|
3186
|
-
await writeFile(endpoint, JSON.stringify({ sessionId, token, pid: process.pid }));
|
|
3187
|
-
return { url:
|
|
3640
|
+
await writeFile(endpoint, JSON.stringify({ sessionId, token, pid: process.pid, url: endpointUrl }));
|
|
3641
|
+
return { url: endpointUrl };
|
|
3188
3642
|
},
|
|
3189
3643
|
stop: async () => {},
|
|
3190
3644
|
}),
|
|
@@ -3241,6 +3695,7 @@ describe("SessionSdkSessionRuntime", () => {
|
|
|
3241
3695
|
createSdkSessionRuntimeExtension(api, {
|
|
3242
3696
|
agentDir,
|
|
3243
3697
|
brokerRegistrationRequired: true,
|
|
3698
|
+
lifecycleRequestId: "broker-required-marker",
|
|
3244
3699
|
createTransport: async ({ sessionId, stateRoot, token }) => ({
|
|
3245
3700
|
sessionId,
|
|
3246
3701
|
stateRoot,
|
|
@@ -3306,6 +3761,8 @@ async function invocationHarness(
|
|
|
3306
3761
|
onLifecycleDrainTimeout?: () => void;
|
|
3307
3762
|
onFailureDiagnosticKeyCount?: (count: number) => void;
|
|
3308
3763
|
agentFailedWriteFailures?: number;
|
|
3764
|
+
branch?: unknown[];
|
|
3765
|
+
onInvocationCompletionReconciled?: (kind: string, correlation: { commandId: string; turnId: string }) => void;
|
|
3309
3766
|
},
|
|
3310
3767
|
): Promise<InvocationHarness> {
|
|
3311
3768
|
const waiters = new Map<string, (frame: ResponseFrame) => void>();
|
|
@@ -3348,6 +3805,9 @@ async function invocationHarness(
|
|
|
3348
3805
|
...(hooks.onFailureDiagnosticKeyCount
|
|
3349
3806
|
? { onFailureDiagnosticKeyCountForTests: hooks.onFailureDiagnosticKeyCount }
|
|
3350
3807
|
: {}),
|
|
3808
|
+
...(hooks.onInvocationCompletionReconciled
|
|
3809
|
+
? { onInvocationCompletionReconciledForTests: hooks.onInvocationCompletionReconciled }
|
|
3810
|
+
: {}),
|
|
3351
3811
|
...(hooks.settings ? { settings: hooks.settings } : {}),
|
|
3352
3812
|
createTransport: async ({ sessionId: id, stateRoot, token }) => ({
|
|
3353
3813
|
sessionId: id,
|
|
@@ -3386,7 +3846,7 @@ async function invocationHarness(
|
|
|
3386
3846
|
getSessionId: () => sessionId,
|
|
3387
3847
|
getSessionFile: () => path.join(cwd, ".gjc", "state", `${sessionId}.jsonl`),
|
|
3388
3848
|
getSessionName: () => undefined,
|
|
3389
|
-
getBranch: () => [],
|
|
3849
|
+
getBranch: () => hooks.branch ?? [],
|
|
3390
3850
|
},
|
|
3391
3851
|
};
|
|
3392
3852
|
await handlers.get("session_start")?.({}, ctx);
|
|
@@ -4261,7 +4721,9 @@ describe("post-acceptance invocation terminalization", () => {
|
|
|
4261
4721
|
expect(secondIds.turnId).not.toBe(firstIds.turnId);
|
|
4262
4722
|
await harness.emit("agent_start");
|
|
4263
4723
|
await successorStarted.promise;
|
|
4264
|
-
await harness.emit("agent_end"
|
|
4724
|
+
await harness.emit("agent_end", {
|
|
4725
|
+
messages: [{ role: "assistant", content: "predecessor delayed" }],
|
|
4726
|
+
});
|
|
4265
4727
|
expect(await harness.query("turn.prompt_status", secondIds)).toMatchObject({
|
|
4266
4728
|
result: { status: expect.stringMatching(/accepted|in_flight/) },
|
|
4267
4729
|
});
|
|
@@ -4269,10 +4731,23 @@ describe("post-acceptance invocation terminalization", () => {
|
|
|
4269
4731
|
status: "failed",
|
|
4270
4732
|
error: { code: "aborted" },
|
|
4271
4733
|
});
|
|
4734
|
+
const firstResult = await harness.query("turn.result", { kind: "prompt", ...firstIds });
|
|
4735
|
+
expect(firstResult).toMatchObject({
|
|
4736
|
+
result: { status: "failed", content: { text: "predecessor delayed" } },
|
|
4737
|
+
});
|
|
4272
4738
|
await harness.emit("agent_end", { messages: [{ role: "assistant", content: "completed" }] });
|
|
4273
4739
|
expect(await settledStatus(harness, "turn.prompt_status", secondIds)).toMatchObject({
|
|
4274
4740
|
status: "terminal_ok",
|
|
4275
4741
|
});
|
|
4742
|
+
expect(await harness.query("turn.result", { kind: "prompt", ...secondIds })).toMatchObject({
|
|
4743
|
+
result: {
|
|
4744
|
+
status: "terminal_ok",
|
|
4745
|
+
content: { text: "completed", byteLength: 9, truncated: false },
|
|
4746
|
+
},
|
|
4747
|
+
});
|
|
4748
|
+
expect(await harness.query("turn.result", { kind: "prompt", ...firstIds })).toMatchObject({
|
|
4749
|
+
result: { content: { text: "predecessor delayed" } },
|
|
4750
|
+
});
|
|
4276
4751
|
expect(prompts).toBe(2);
|
|
4277
4752
|
await harness.stop();
|
|
4278
4753
|
} finally {
|
|
@@ -4469,6 +4944,190 @@ describe("post-acceptance invocation terminalization", () => {
|
|
|
4469
4944
|
await rm(cwd, { recursive: true, force: true });
|
|
4470
4945
|
}
|
|
4471
4946
|
});
|
|
4947
|
+
test("captures exact bounded content from actual prompt agent_end messages", async () => {
|
|
4948
|
+
const cases = [
|
|
4949
|
+
{ name: "short", agent: "SDK_OK", expected: "SDK_OK", bytes: 6, truncated: false },
|
|
4950
|
+
{ name: "max", agent: "x".repeat(16_384), expected: "x".repeat(16_384), bytes: 16_384, truncated: false },
|
|
4951
|
+
{ name: "blank", agent: " ", expected: undefined, bytes: 0, truncated: false },
|
|
4952
|
+
{
|
|
4953
|
+
name: "overflow",
|
|
4954
|
+
agent: `${"😀".repeat(4_096)}tail`,
|
|
4955
|
+
expected: "😀".repeat(4_096),
|
|
4956
|
+
bytes: 16_384,
|
|
4957
|
+
truncated: true,
|
|
4958
|
+
},
|
|
4959
|
+
] as const;
|
|
4960
|
+
for (const testCase of cases) {
|
|
4961
|
+
const cwd = await mkdtemp(path.join(os.tmpdir(), `gjc-terminal-content-${testCase.name}-`));
|
|
4962
|
+
try {
|
|
4963
|
+
const harness = await invocationHarness(`terminal-content-${testCase.name}`, cwd, {
|
|
4964
|
+
sendUserMessage: async (_content, options) => {
|
|
4965
|
+
await options?.onPreflightAcceptCommit?.();
|
|
4966
|
+
await Promise.withResolvers<void>().promise;
|
|
4967
|
+
},
|
|
4968
|
+
});
|
|
4969
|
+
const accepted = await harness.control("turn.prompt", { text: "hello" });
|
|
4970
|
+
await harness.emit("agent_start");
|
|
4971
|
+
await harness.emit("agent_end", { messages: [{ role: "assistant", content: testCase.agent }] });
|
|
4972
|
+
const result = await settledStatus(harness, "turn.result", {
|
|
4973
|
+
kind: "prompt",
|
|
4974
|
+
commandId: accepted.result?.commandId,
|
|
4975
|
+
turnId: accepted.result?.turnId,
|
|
4976
|
+
});
|
|
4977
|
+
if (testCase.expected === undefined) {
|
|
4978
|
+
expect(result).toMatchObject({ status: "failed" });
|
|
4979
|
+
expect((result as Record<string, unknown>).content).toBeUndefined();
|
|
4980
|
+
} else {
|
|
4981
|
+
expect(result).toMatchObject({
|
|
4982
|
+
status: "terminal_ok",
|
|
4983
|
+
content: {
|
|
4984
|
+
text: testCase.expected,
|
|
4985
|
+
byteLength: testCase.bytes,
|
|
4986
|
+
truncated: testCase.truncated,
|
|
4987
|
+
},
|
|
4988
|
+
});
|
|
4989
|
+
}
|
|
4990
|
+
await harness.stop();
|
|
4991
|
+
} finally {
|
|
4992
|
+
await rm(cwd, { recursive: true, force: true });
|
|
4993
|
+
}
|
|
4994
|
+
}
|
|
4995
|
+
});
|
|
4996
|
+
test("does not reuse a previous branch assistant for a new textless prompt", async () => {
|
|
4997
|
+
const cwd = await mkdtemp(path.join(os.tmpdir(), "gjc-terminal-order-previous-assistant-"));
|
|
4998
|
+
try {
|
|
4999
|
+
const harness = await invocationHarness("terminal-order-previous-assistant", cwd, {
|
|
5000
|
+
branch: [{ type: "message", message: { role: "assistant", content: "PREVIOUS" } }],
|
|
5001
|
+
sendUserMessage: async (_content, options) => {
|
|
5002
|
+
await options?.onPreflightAcceptCommit?.();
|
|
5003
|
+
await Promise.withResolvers<void>().promise;
|
|
5004
|
+
},
|
|
5005
|
+
});
|
|
5006
|
+
const accepted = await harness.control("turn.prompt", { text: "textless" });
|
|
5007
|
+
const selector = {
|
|
5008
|
+
kind: "prompt" as const,
|
|
5009
|
+
commandId: accepted.result?.commandId,
|
|
5010
|
+
turnId: accepted.result?.turnId,
|
|
5011
|
+
};
|
|
5012
|
+
await harness.emit("agent_start");
|
|
5013
|
+
await harness.emit("agent_end", { messages: [] });
|
|
5014
|
+
const result = await settledStatus(harness, "turn.result", selector);
|
|
5015
|
+
expect((result as Record<string, unknown>).content).toBeUndefined();
|
|
5016
|
+
await harness.stop();
|
|
5017
|
+
} finally {
|
|
5018
|
+
await rm(cwd, { recursive: true, force: true });
|
|
5019
|
+
}
|
|
5020
|
+
});
|
|
5021
|
+
test("reconciles contract-valid skill completion and agent_end ordering", async () => {
|
|
5022
|
+
const cases = [
|
|
5023
|
+
{
|
|
5024
|
+
name: "completion-real-agent-real",
|
|
5025
|
+
order: "completion-first",
|
|
5026
|
+
completion: "completion",
|
|
5027
|
+
agent: "agent",
|
|
5028
|
+
expected: "completion",
|
|
5029
|
+
},
|
|
5030
|
+
{
|
|
5031
|
+
name: "completion-blank-agent-real",
|
|
5032
|
+
order: "completion-first",
|
|
5033
|
+
completion: " ",
|
|
5034
|
+
agent: "agent",
|
|
5035
|
+
expected: "agent",
|
|
5036
|
+
},
|
|
5037
|
+
{
|
|
5038
|
+
name: "completion-none-agent-real",
|
|
5039
|
+
order: "completion-first",
|
|
5040
|
+
completion: null,
|
|
5041
|
+
agent: "agent",
|
|
5042
|
+
expected: "agent",
|
|
5043
|
+
},
|
|
5044
|
+
{
|
|
5045
|
+
name: "agent-real-completion-real",
|
|
5046
|
+
order: "agent-first",
|
|
5047
|
+
completion: "completion",
|
|
5048
|
+
agent: "agent",
|
|
5049
|
+
expected: "agent",
|
|
5050
|
+
},
|
|
5051
|
+
{
|
|
5052
|
+
name: "agent-real-completion-blank",
|
|
5053
|
+
order: "agent-first",
|
|
5054
|
+
completion: " ",
|
|
5055
|
+
agent: "agent",
|
|
5056
|
+
expected: "agent",
|
|
5057
|
+
},
|
|
5058
|
+
{
|
|
5059
|
+
name: "agent-blank-completion-real",
|
|
5060
|
+
order: "agent-first",
|
|
5061
|
+
completion: "completion",
|
|
5062
|
+
agent: " ",
|
|
5063
|
+
expected: "completion",
|
|
5064
|
+
},
|
|
5065
|
+
{
|
|
5066
|
+
name: "agent-blank-completion-none",
|
|
5067
|
+
order: "agent-first",
|
|
5068
|
+
completion: null,
|
|
5069
|
+
agent: " ",
|
|
5070
|
+
expected: undefined,
|
|
5071
|
+
},
|
|
5072
|
+
{
|
|
5073
|
+
name: "agent-none-completion-real",
|
|
5074
|
+
order: "agent-first",
|
|
5075
|
+
completion: "completion",
|
|
5076
|
+
agent: null,
|
|
5077
|
+
expected: "completion",
|
|
5078
|
+
},
|
|
5079
|
+
{
|
|
5080
|
+
name: "agent-none-completion-blank",
|
|
5081
|
+
order: "agent-first",
|
|
5082
|
+
completion: " ",
|
|
5083
|
+
agent: null,
|
|
5084
|
+
expected: undefined,
|
|
5085
|
+
},
|
|
5086
|
+
] as const;
|
|
5087
|
+
for (const testCase of cases) {
|
|
5088
|
+
const cwd = await mkdtemp(path.join(os.tmpdir(), `gjc-skill-terminal-order-${testCase.name}-`));
|
|
5089
|
+
const completion = Promise.withResolvers<unknown>();
|
|
5090
|
+
const completionReconciled = Promise.withResolvers<void>();
|
|
5091
|
+
try {
|
|
5092
|
+
const harness = await invocationHarness(`skill-terminal-order-${testCase.name}`, cwd, {
|
|
5093
|
+
onInvocationCompletionReconciled: kind => {
|
|
5094
|
+
if (kind === "skill") completionReconciled.resolve();
|
|
5095
|
+
},
|
|
5096
|
+
invokeSkill: async (_name, _args, options) => {
|
|
5097
|
+
await options?.onPreflightAcceptCommit?.();
|
|
5098
|
+
return await completion.promise;
|
|
5099
|
+
},
|
|
5100
|
+
});
|
|
5101
|
+
const accepted = await harness.control("skill.invoke", { name: "ralplan" });
|
|
5102
|
+
const selector = {
|
|
5103
|
+
kind: "skill" as const,
|
|
5104
|
+
commandId: accepted.result?.commandId,
|
|
5105
|
+
turnId: accepted.result?.turnId,
|
|
5106
|
+
};
|
|
5107
|
+
await harness.emit("agent_start");
|
|
5108
|
+
const emitAgentEnd = () =>
|
|
5109
|
+
harness.emit("agent_end", {
|
|
5110
|
+
messages: testCase.agent === null ? [] : [{ role: "assistant", content: testCase.agent }],
|
|
5111
|
+
});
|
|
5112
|
+
if (testCase.order === "completion-first") {
|
|
5113
|
+
completion.resolve(testCase.completion);
|
|
5114
|
+
await completionReconciled.promise;
|
|
5115
|
+
await emitAgentEnd();
|
|
5116
|
+
} else {
|
|
5117
|
+
await emitAgentEnd();
|
|
5118
|
+
completion.resolve(testCase.completion);
|
|
5119
|
+
await completionReconciled.promise;
|
|
5120
|
+
}
|
|
5121
|
+
const result = await harness.query("turn.result", selector);
|
|
5122
|
+
const content = (result.result as { content?: { text?: string } } | undefined)?.content;
|
|
5123
|
+
expect(content?.text).toBe(testCase.expected);
|
|
5124
|
+
await harness.stop();
|
|
5125
|
+
} finally {
|
|
5126
|
+
completion.resolve(undefined);
|
|
5127
|
+
await rm(cwd, { recursive: true, force: true });
|
|
5128
|
+
}
|
|
5129
|
+
}
|
|
5130
|
+
}, 30_000);
|
|
4472
5131
|
test("a queued follow-up prompt is not terminalized before the turn runs", async () => {
|
|
4473
5132
|
const cwd = await mkdtemp(path.join(os.tmpdir(), "gjc-terminalize-followup-"));
|
|
4474
5133
|
try {
|
|
@@ -4587,7 +5246,7 @@ describe("post-acceptance invocation terminalization", () => {
|
|
|
4587
5246
|
const rejected = await harness.control("turn.prompt", { text: "hello", clientRef: "preflight-ref" });
|
|
4588
5247
|
expect(rejected.ok).toBe(false);
|
|
4589
5248
|
const status = await harness.query("turn.prompt_status", { clientRef: "preflight-ref" });
|
|
4590
|
-
expect(status.result).toEqual({ status: "unknown" });
|
|
5249
|
+
expect(status.result as Record<string, unknown>).toEqual({ status: "unknown", receiptState: "unknown" });
|
|
4591
5250
|
await harness.stop();
|
|
4592
5251
|
} finally {
|
|
4593
5252
|
await Bun.sleep(50);
|