@gajae-code/coding-agent 0.15.5 → 0.16.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 +85 -1
- package/dist/types/async/job-manager.d.ts +83 -0
- package/dist/types/cli/args.d.ts +2 -0
- package/dist/types/cli/auth-gateway-cli.d.ts +17 -0
- package/dist/types/cli/model-presets-cli.d.ts +9 -0
- package/dist/types/cli/root-flags.d.ts +14 -0
- package/dist/types/cli/update-cli.d.ts +15 -0
- package/dist/types/cli.d.ts +7 -0
- package/dist/types/commands/auth-gateway.d.ts +3 -0
- package/dist/types/commands/gc.d.ts +13 -0
- package/dist/types/commands/launch.d.ts +10 -0
- package/dist/types/commands/model-presets.d.ts +21 -0
- package/dist/types/config/autorouting-tier-map.d.ts +120 -0
- package/dist/types/config/file-lock.d.ts +54 -2
- package/dist/types/config/internal/model-preset-registry-test-state.d.ts +4 -0
- package/dist/types/config/internal/model-preset-registry-test-support.d.ts +3 -0
- package/dist/types/config/keybindings.d.ts +3 -2
- package/dist/types/config/model-preset-registry.d.ts +368 -0
- package/dist/types/config/model-profile-activation.d.ts +39 -0
- package/dist/types/config/model-profiles.d.ts +5 -3
- package/dist/types/config/model-registry.d.ts +17 -5
- package/dist/types/config/settings-schema.d.ts +19 -1
- package/dist/types/config/settings.d.ts +2 -0
- package/dist/types/coordinator-mcp/model-preset.d.ts +7 -5
- package/dist/types/coordinator-mcp/projection-scan.d.ts +54 -0
- package/dist/types/crash/record-loader.d.ts +15 -0
- package/dist/types/extensibility/extensions/types.d.ts +2 -5
- package/dist/types/gjc-runtime/empty-delete-gc.d.ts +58 -0
- package/dist/types/gjc-runtime/gc-runtime.d.ts +2 -0
- package/dist/types/gjc-runtime/launch-worktree-reservation.d.ts +21 -0
- package/dist/types/gjc-runtime/managed-owner-supervisor.d.ts +2 -0
- package/dist/types/gjc-runtime/session-state-lock.d.ts +35 -6
- package/dist/types/gjc-runtime/session-state-sidecar.d.ts +72 -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/index.d.ts +1 -0
- package/dist/types/main.d.ts +3 -1
- 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/components/frictionless-onboarding-selector.d.ts +10 -0
- package/dist/types/modes/components/jobs-overlay-model.d.ts +4 -2
- package/dist/types/modes/components/oauth-selector.d.ts +1 -0
- 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/jobs-observer.d.ts +36 -0
- package/dist/types/modes/print-mode.d.ts +17 -0
- package/dist/types/modes/ui-language.d.ts +7 -1
- package/dist/types/sdk/broker/broker.d.ts +80 -1
- package/dist/types/sdk/broker/endpoint-authority.d.ts +24 -0
- package/dist/types/sdk/broker/index.d.ts +3 -0
- package/dist/types/sdk/broker/lifecycle.d.ts +26 -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/state-version.d.ts +4 -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/index.d.ts +6 -0
- package/dist/types/sdk/bus/kind-aware-reconciliation.d.ts +3 -0
- package/dist/types/sdk/bus/prompt-reconciliation.d.ts +4 -0
- 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 +4 -3
- package/dist/types/sdk/cli/session-cli.d.ts +18 -2
- package/dist/types/sdk/client/client.d.ts +3 -2
- package/dist/types/sdk/client/discovery.d.ts +1 -0
- 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/host.d.ts +10 -0
- package/dist/types/sdk/host/model-pin.d.ts +7 -2
- package/dist/types/sdk/host/sdk-run-capability.d.ts +1 -0
- package/dist/types/sdk/host/session-runtime.d.ts +67 -1
- 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 +2 -0
- package/dist/types/sdk/prompt-deadline-manager.d.ts +11 -1
- package/dist/types/sdk/protocol/adapter-validation.d.ts +2 -0
- package/dist/types/sdk/router/session-router.d.ts +11 -0
- package/dist/types/sdk/session.d.ts +16 -0
- package/dist/types/sdk/startup-capability.d.ts +2 -1
- package/dist/types/session/account-inventory.d.ts +1 -0
- package/dist/types/session/agent-session.d.ts +24 -7
- package/dist/types/session/credential-store-errors.d.ts +1 -0
- package/dist/types/session/fold-coordinator.d.ts +201 -0
- package/dist/types/session/sdk-run-capability-internal.d.ts +7 -0
- package/dist/types/session/sdk-run-capability.d.ts +2 -0
- package/dist/types/session/session-manager.d.ts +65 -0
- package/dist/types/session/yield-queue.d.ts +5 -4
- package/dist/types/setup/frictionless-onboarding.d.ts +1 -0
- package/dist/types/telemetry/control.d.ts +11 -0
- package/dist/types/telemetry/events.d.ts +20 -0
- package/dist/types/telemetry/index.d.ts +3 -0
- package/dist/types/telemetry/transport.d.ts +22 -0
- package/dist/types/tools/bash-interactive.d.ts +23 -0
- package/dist/types/tools/bash.d.ts +4 -0
- package/dist/types/tools/gh.d.ts +8 -0
- package/dist/types/tools/index.d.ts +10 -5
- package/dist/types/web/search/providers/base.d.ts +9 -4
- package/dist/types/web/search/providers/xai.d.ts +2 -1
- package/dist/types/web/search/types.d.ts +14 -0
- package/package.json +10 -7
- package/scripts/generate-hotkeys-docs.ts +4 -0
- package/scripts/generate-sdk-operation-inventory.ts +9 -3
- package/scripts/verify-gjc-sdk-canonicalization.ts +35 -1
- package/src/async/job-manager.ts +671 -39
- package/src/cli/accounts-cli.ts +9 -5
- package/src/cli/args.ts +17 -0
- package/src/cli/auth-broker-cli.ts +40 -12
- package/src/cli/auth-gateway-cli.ts +273 -97
- package/src/cli/config-cli.ts +2 -2
- package/src/cli/fast-help.ts +4 -0
- package/src/cli/model-presets-cli.ts +100 -0
- package/src/cli/root-flags.ts +5 -0
- package/src/cli/update-cli.ts +117 -16
- package/src/cli.ts +6 -2
- package/src/commands/auth-gateway.ts +12 -8
- package/src/commands/gc.ts +19 -0
- package/src/commands/launch.ts +86 -18
- package/src/commands/model-presets.ts +24 -0
- package/src/commands/sdk.ts +113 -21
- package/src/config/autorouting-tier-map.ts +44 -0
- package/src/config/file-lock-gc.ts +9 -4
- package/src/config/file-lock.ts +890 -99
- package/src/config/internal/model-preset-registry-test-state.ts +28 -0
- package/src/config/internal/model-preset-registry-test-support.ts +11 -0
- package/src/config/keybindings.ts +6 -2
- package/src/config/model-preset-registry.ts +2795 -0
- package/src/config/model-profile-activation.ts +115 -28
- package/src/config/model-profile-contract.ts +4 -1
- package/src/config/model-profiles.ts +56 -14
- package/src/config/model-registry.ts +1044 -122
- package/src/config/model-resolver.ts +5 -0
- package/src/config/settings-schema.ts +19 -1
- package/src/config/settings.ts +11 -1
- package/src/coordinator-mcp/model-preset.ts +32 -17
- package/src/coordinator-mcp/projection-scan.ts +592 -0
- package/src/coordinator-mcp/question-state.ts +9 -11
- package/src/coordinator-mcp/server.ts +111 -42
- package/src/coordinator-mcp/session-reaper.ts +9 -3
- package/src/crash/index-store.ts +2 -0
- package/src/crash/record-loader.ts +10 -0
- package/src/extensibility/extensions/loader.ts +0 -1
- package/src/extensibility/extensions/types.ts +2 -3
- package/src/extensibility/gjc-plugins/runtime-adapters.ts +1 -0
- package/src/gjc-runtime/empty-delete-gc.ts +301 -0
- package/src/gjc-runtime/gc-render.ts +29 -0
- package/src/gjc-runtime/gc-runtime.ts +174 -11
- package/src/gjc-runtime/launch-worktree-reservation.ts +242 -0
- package/src/gjc-runtime/managed-owner-supervisor.ts +63 -51
- package/src/gjc-runtime/session-state-lock.ts +1321 -147
- package/src/gjc-runtime/session-state-sidecar.ts +1004 -25
- package/src/gjc-runtime/tmux-owner-isolation-cli.ts +4 -7
- package/src/gjc-runtime/tmux-owner-isolation.ts +2 -2
- package/src/gjc-runtime/tmux-sessions.ts +252 -13
- package/src/hooks/skill-state.ts +6 -0
- package/src/index.ts +1 -0
- package/src/internal-urls/docs-index.generated.ts +14 -13
- package/src/internal-urls/issue-pr-protocol.ts +4 -2
- package/src/lsp/config.ts +4 -3
- package/src/lsp/path-trust.ts +12 -6
- package/src/main.ts +66 -4
- 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 +616 -161
- package/src/modes/components/footer.ts +4 -2
- package/src/modes/components/frictionless-onboarding-selector.ts +48 -2
- package/src/modes/components/jobs-overlay-model.ts +99 -16
- package/src/modes/components/model-selector.ts +158 -18
- package/src/modes/components/oauth-selector.ts +6 -1
- 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 +29 -10
- package/src/modes/components/tool-status-header.ts +151 -15
- package/src/modes/controllers/extension-ui-controller.ts +1 -1
- package/src/modes/controllers/input-controller.ts +51 -6
- package/src/modes/controllers/selector-controller.ts +191 -96
- package/src/modes/interactive-mode.ts +14 -1
- package/src/modes/jobs-observer.ts +149 -16
- package/src/modes/print-mode.ts +64 -0
- package/src/modes/runtime-init.ts +1 -1
- package/src/modes/ui-language.ts +49 -2
- package/src/modes/utils/injected-user-submission.ts +15 -1
- package/src/prompts/system/master-mode.md +28 -0
- package/src/prompts/tools/bash.md +2 -2
- package/src/prompts/tools/fold-receipt.md +3 -0
- package/src/prompts/tools/job.md +1 -1
- package/src/sdk/acp/adapter.ts +17 -2
- package/src/sdk/broker/broker.ts +1671 -34
- package/src/sdk/broker/endpoint-authority.ts +86 -0
- package/src/sdk/broker/ensure.ts +5 -0
- package/src/sdk/broker/index.ts +3 -0
- package/src/sdk/broker/lifecycle.ts +217 -46
- 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 +548 -0
- package/src/sdk/broker/state-version.ts +26 -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 +46 -5
- package/src/sdk/bus/index.ts +51 -4
- package/src/sdk/bus/kind-aware-reconciliation.ts +36 -6
- package/src/sdk/bus/prompt-reconciliation.ts +28 -4
- package/src/sdk/bus/slack-conversation.ts +5 -1
- 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 +11 -8
- package/src/sdk/cli/session-cli.ts +276 -6
- package/src/sdk/client/client.ts +14 -8
- package/src/sdk/client/discovery.ts +3 -1
- package/src/sdk/host/control/dispatch.ts +101 -37
- 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/host.ts +27 -2
- package/src/sdk/host/model-pin.ts +185 -25
- package/src/sdk/host/sdk-run-capability.ts +1 -0
- package/src/sdk/host/session-runtime.test.ts +581 -43
- package/src/sdk/host/session-runtime.ts +655 -101
- package/src/sdk/lifecycle/broker-client.ts +28 -0
- package/src/sdk/lifecycle/service.ts +279 -20
- package/src/sdk/lifecycle-session.ts +10 -2
- package/src/sdk/prompt-deadline-manager.ts +16 -1
- package/src/sdk/protocol/adapter-validation.ts +27 -0
- package/src/sdk/protocol/operation-inventory.generated.json +53 -2
- package/src/sdk/protocol/operation-registry.ts +17 -4
- package/src/sdk/router/session-router.ts +294 -154
- package/src/sdk/session.ts +440 -167
- package/src/sdk/startup-capability.ts +1 -0
- package/src/session/account-inventory.ts +27 -11
- package/src/session/agent-session.ts +1292 -180
- package/src/session/blob-store.ts +15 -5
- package/src/session/credential-store-errors.ts +2 -0
- package/src/session/fold-coordinator.ts +433 -0
- package/src/session/internal/managed-session-storage.ts +153 -78
- package/src/session/sdk-run-capability-internal.ts +15 -0
- package/src/session/sdk-run-capability.ts +2 -0
- package/src/session/session-manager.ts +342 -28
- package/src/session/yield-queue.ts +24 -10
- package/src/setup/frictionless-onboarding.ts +2 -0
- package/src/setup/provider-presets.json +1 -1
- package/src/slash-commands/helpers/credential-switch.ts +6 -1
- package/src/task/executor.ts +19 -3
- package/src/telemetry/control.ts +26 -0
- package/src/telemetry/events.ts +1268 -0
- package/src/telemetry/index.ts +3 -0
- package/src/telemetry/transport.ts +87 -0
- package/src/tools/bash-interactive.ts +182 -85
- package/src/tools/bash.ts +1028 -109
- package/src/tools/gh.ts +104 -48
- package/src/tools/github-cache.ts +2 -2
- package/src/tools/index.ts +11 -5
- package/src/tools/tool-catalog.generated.ts +1 -1
- package/src/web/search/provider.ts +120 -11
- package/src/web/search/providers/anthropic.ts +89 -17
- package/src/web/search/providers/base.ts +9 -4
- package/src/web/search/providers/gemini.ts +87 -15
- package/src/web/search/providers/openai-compatible.ts +23 -8
- package/src/web/search/providers/xai.ts +98 -12
- package/src/web/search/types.ts +16 -0
|
@@ -5,7 +5,7 @@ import * as os from "node:os";
|
|
|
5
5
|
import * as path from "node:path";
|
|
6
6
|
import { AsyncJobManager } from "../../async";
|
|
7
7
|
import type { Settings } from "../../config/settings";
|
|
8
|
-
import type { ExtensionAPI, ExtensionContext } from "../../extensibility/extensions";
|
|
8
|
+
import type { ExtensionAPI, ExtensionContext, ExtensionTranscriptEntry } from "../../extensibility/extensions";
|
|
9
9
|
import {
|
|
10
10
|
registerOwnedRegistration,
|
|
11
11
|
resetTerminalAbortRegistriesForTests,
|
|
@@ -13,8 +13,11 @@ import {
|
|
|
13
13
|
unregisterOwnedRegistration,
|
|
14
14
|
} from "../../session/terminal-abort";
|
|
15
15
|
import { Broker } from "../broker/broker";
|
|
16
|
+
import { createKindAwareReconciliation } from "../bus/kind-aware-reconciliation";
|
|
17
|
+
import { createPromptReconciliation } from "../bus/prompt-reconciliation";
|
|
16
18
|
import { createReconciliationStore } from "../bus/reconciliation-store";
|
|
17
|
-
import {
|
|
19
|
+
import { BROKER_RUNTIME_ABORT_CAPABILITY_FIELD, setBrokerRuntimeAbortCapabilityForTest } from "./control/runtime-gate";
|
|
20
|
+
import { CursorRegistry, QueryHandlers, type QueryResponse, RevisionStore } from "./query";
|
|
18
21
|
import {
|
|
19
22
|
createInvocationReconciliation,
|
|
20
23
|
createSdkSessionRuntimeExtension,
|
|
@@ -88,6 +91,7 @@ function extensionContext(
|
|
|
88
91
|
goalState?: unknown;
|
|
89
92
|
branch?: unknown[];
|
|
90
93
|
liveState?: { isStreaming: boolean; steeringQueueDepth: number; followupQueueDepth: number };
|
|
94
|
+
transcript?: ExtensionTranscriptEntry[];
|
|
91
95
|
} = {},
|
|
92
96
|
): ExtensionContext {
|
|
93
97
|
return {
|
|
@@ -100,6 +104,7 @@ function extensionContext(
|
|
|
100
104
|
getSessionName: () => undefined,
|
|
101
105
|
getBranch: () => options.branch ?? [],
|
|
102
106
|
},
|
|
107
|
+
getTranscript: () => options.transcript ?? [],
|
|
103
108
|
getGoalState: () => options.goalState,
|
|
104
109
|
} as unknown as ExtensionContext;
|
|
105
110
|
}
|
|
@@ -146,6 +151,167 @@ async function queryGoalState(
|
|
|
146
151
|
return response.page?.items[0];
|
|
147
152
|
}
|
|
148
153
|
|
|
154
|
+
async function queryLastAssistant(ctx: ExtensionContext, sessionId: string): Promise<QueryResponse> {
|
|
155
|
+
const surface = createSdkSurfaceFactory({
|
|
156
|
+
ctx,
|
|
157
|
+
id: sessionId,
|
|
158
|
+
api: {} as ExtensionAPI,
|
|
159
|
+
}).query;
|
|
160
|
+
const store = new RevisionStore(sessionId);
|
|
161
|
+
const response = await new QueryHandlers(
|
|
162
|
+
surface,
|
|
163
|
+
sessionId,
|
|
164
|
+
store,
|
|
165
|
+
new CursorRegistry("last-assistant-test-token", store),
|
|
166
|
+
).dispatch({
|
|
167
|
+
query: "session.last_assistant",
|
|
168
|
+
connectionId: "last-assistant-test-connection",
|
|
169
|
+
});
|
|
170
|
+
return response;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
test("session.last_assistant returns the latest projected readable text past non-text assistant tails", async () => {
|
|
174
|
+
const sessionId = "last-assistant-readable-tail";
|
|
175
|
+
const transcript: ExtensionTranscriptEntry[] = [
|
|
176
|
+
{
|
|
177
|
+
id: "visible-readable",
|
|
178
|
+
role: "assistant",
|
|
179
|
+
textSummary: "first line second line",
|
|
180
|
+
ts: new Date(1).toISOString(),
|
|
181
|
+
content: [
|
|
182
|
+
{ type: "text", text: "first line" },
|
|
183
|
+
{ type: "thinking", thinking: "private reasoning" },
|
|
184
|
+
{ type: "text", text: "second line" },
|
|
185
|
+
],
|
|
186
|
+
},
|
|
187
|
+
...Array.from(
|
|
188
|
+
{ length: 2_000 },
|
|
189
|
+
(_, index): ExtensionTranscriptEntry => ({
|
|
190
|
+
id: `tool-tail-${index}`,
|
|
191
|
+
role: "assistant",
|
|
192
|
+
textSummary: "",
|
|
193
|
+
ts: new Date(index + 2).toISOString(),
|
|
194
|
+
body: index % 3 === 0 ? "" : index % 3 === 1 ? " \n\t " : undefined,
|
|
195
|
+
content:
|
|
196
|
+
index % 3 === 2
|
|
197
|
+
? [
|
|
198
|
+
{ type: "thinking", thinking: "not readable" },
|
|
199
|
+
{ type: "toolCall", id: `call-${index}`, name: "read", arguments: {} },
|
|
200
|
+
]
|
|
201
|
+
: undefined,
|
|
202
|
+
}),
|
|
203
|
+
),
|
|
204
|
+
];
|
|
205
|
+
const ctx = extensionContext(sessionId, "/tmp", {
|
|
206
|
+
transcript,
|
|
207
|
+
branch: [
|
|
208
|
+
{
|
|
209
|
+
type: "message",
|
|
210
|
+
message: { role: "assistant", content: [{ type: "text", text: "raw private tail" }] },
|
|
211
|
+
},
|
|
212
|
+
],
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
expect(await queryLastAssistant(ctx, sessionId)).toMatchObject({
|
|
216
|
+
ok: true,
|
|
217
|
+
page: { items: ["first line\nsecond line"] },
|
|
218
|
+
});
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
test("session.last_assistant returns resource_gone when the projected transcript has no readable assistant text", async () => {
|
|
222
|
+
const sessionId = "last-assistant-empty";
|
|
223
|
+
const ctx = extensionContext(sessionId, "/tmp", {
|
|
224
|
+
transcript: [
|
|
225
|
+
{
|
|
226
|
+
id: "user",
|
|
227
|
+
role: "user",
|
|
228
|
+
textSummary: "hello",
|
|
229
|
+
ts: new Date(1).toISOString(),
|
|
230
|
+
body: "hello",
|
|
231
|
+
},
|
|
232
|
+
{
|
|
233
|
+
id: "thinking-only",
|
|
234
|
+
role: "assistant",
|
|
235
|
+
textSummary: "",
|
|
236
|
+
ts: new Date(2).toISOString(),
|
|
237
|
+
content: [{ type: "thinking", thinking: "private reasoning" }],
|
|
238
|
+
},
|
|
239
|
+
],
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
expect(await queryLastAssistant(ctx, sessionId)).toMatchObject({ ok: false, error: { code: "resource_gone" } });
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
test("native prompt reconciliation fails closed for an explicitly empty assistant result", () => {
|
|
246
|
+
const reconciliation = createPromptReconciliation();
|
|
247
|
+
const correlation = { commandId: "native-empty-command", turnId: "native-empty-turn" };
|
|
248
|
+
reconciliation.admit("native-empty-ref");
|
|
249
|
+
reconciliation.noteAccepted(correlation, "native-empty-ref");
|
|
250
|
+
reconciliation.noteTransition(correlation, { type: "agent_end", finalText: "" });
|
|
251
|
+
expect(reconciliation.lookup({ clientRef: "native-empty-ref" })).toMatchObject({
|
|
252
|
+
status: "failed",
|
|
253
|
+
error: { code: "prompt_failed" },
|
|
254
|
+
receiptState: "missing",
|
|
255
|
+
});
|
|
256
|
+
reconciliation.noteTransition(correlation, { type: "agent_end", finalText: "late text" });
|
|
257
|
+
expect(reconciliation.lookup({ clientRef: "native-empty-ref" })).toMatchObject({ status: "failed" });
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
test("broker reconciliation fails closed for an empty prompt and persists the first terminal result", async () => {
|
|
261
|
+
let records: unknown[] = [];
|
|
262
|
+
const store = {
|
|
263
|
+
load: async () => records,
|
|
264
|
+
transact: async (mutator: (current: never[]) => never[]) => {
|
|
265
|
+
records = mutator(records as never);
|
|
266
|
+
},
|
|
267
|
+
} as never;
|
|
268
|
+
const reconciliation = createKindAwareReconciliation({ store });
|
|
269
|
+
const correlation = { commandId: "broker-empty-command", turnId: "broker-empty-turn" };
|
|
270
|
+
await reconciliation.noteAccepted("prompt", correlation, "broker-empty-ref");
|
|
271
|
+
await reconciliation.noteTransition("prompt", correlation, {
|
|
272
|
+
type: "agent_end",
|
|
273
|
+
content: { version: 1, type: "text", text: "", byteLength: 0, truncated: false },
|
|
274
|
+
});
|
|
275
|
+
const settled = reconciliation.lookupResult("prompt", { clientRef: "broker-empty-ref" });
|
|
276
|
+
expect(settled).toMatchObject({ status: "failed", error: { code: "prompt_failed" } });
|
|
277
|
+
await reconciliation.noteTransition("prompt", correlation, {
|
|
278
|
+
type: "agent_end",
|
|
279
|
+
content: { version: 1, type: "text", text: "late text", byteLength: 9, truncated: false },
|
|
280
|
+
});
|
|
281
|
+
const reloaded = createKindAwareReconciliation({ store });
|
|
282
|
+
await reloaded.hydrateFromStore();
|
|
283
|
+
expect(reloaded.lookupResult("prompt", { clientRef: "broker-empty-ref" })).toMatchObject({
|
|
284
|
+
status: "failed",
|
|
285
|
+
error: { code: "prompt_failed" },
|
|
286
|
+
});
|
|
287
|
+
const noOpCorrelation = { commandId: "broker-noop-command", turnId: "broker-noop-turn" };
|
|
288
|
+
await reconciliation.noteAccepted("prompt", noOpCorrelation, "broker-noop-ref");
|
|
289
|
+
await reconciliation.finalizeOutcome(
|
|
290
|
+
"prompt",
|
|
291
|
+
noOpCorrelation,
|
|
292
|
+
{ kind: "stopped", reason: "cancelled", provenance: "client_cancel" },
|
|
293
|
+
undefined,
|
|
294
|
+
"",
|
|
295
|
+
);
|
|
296
|
+
expect(reconciliation.lookupResult("prompt", { clientRef: "broker-noop-ref" })).toMatchObject({
|
|
297
|
+
status: "terminal_ok",
|
|
298
|
+
});
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
test("session-host prompt reconciliation fails closed when the accepted result is empty", async () => {
|
|
302
|
+
const reconciliation = createInvocationReconciliation();
|
|
303
|
+
const correlation = { commandId: "host-empty-command", turnId: "host-empty-turn" };
|
|
304
|
+
await reconciliation.noteAccepted("prompt", correlation, "host-empty-ref");
|
|
305
|
+
await reconciliation.noteTransition("prompt", correlation, {
|
|
306
|
+
type: "agent_end",
|
|
307
|
+
content: { version: 1, type: "text", text: "", byteLength: 0, truncated: false },
|
|
308
|
+
});
|
|
309
|
+
expect(reconciliation.lookup("prompt", { clientRef: "host-empty-ref" })).toMatchObject({
|
|
310
|
+
status: "failed",
|
|
311
|
+
error: { code: "prompt_failed" },
|
|
312
|
+
});
|
|
313
|
+
});
|
|
314
|
+
|
|
149
315
|
describe("SDK goal snapshot lifecycle", () => {
|
|
150
316
|
test("recovers an active nonzero-activity goal from the durable session projection", async () => {
|
|
151
317
|
const cwd = await mkdtemp(path.join(os.tmpdir(), "gjc-sdk-goal-recovery-"));
|
|
@@ -405,7 +571,10 @@ test("durable reload keeps agent_end terminal across a paused successor transiti
|
|
|
405
571
|
const correlation = { commandId: "durable-race-command", turnId: "durable-race-turn" };
|
|
406
572
|
await reconciliation.noteAccepted("prompt", correlation, "durable-race-ref");
|
|
407
573
|
pauseWrites = 2;
|
|
408
|
-
const terminal = reconciliation.noteTransition("prompt", correlation, {
|
|
574
|
+
const terminal = reconciliation.noteTransition("prompt", correlation, {
|
|
575
|
+
type: "agent_end",
|
|
576
|
+
content: { version: 1, type: "text", text: "completed", byteLength: 9, truncated: false },
|
|
577
|
+
});
|
|
409
578
|
await writeStarted[0]!.promise;
|
|
410
579
|
// A successor lifecycle event arrives while the terminal write is held. It
|
|
411
580
|
// must observe the staged terminal record and never resurrect in_flight state.
|
|
@@ -455,7 +624,10 @@ test("agent_end is not swallowed when deadline persistence fails", async () => {
|
|
|
455
624
|
message: "deadline",
|
|
456
625
|
});
|
|
457
626
|
await persistStarted.promise;
|
|
458
|
-
const terminal = reconciliation.noteTransition("prompt", correlation, {
|
|
627
|
+
const terminal = reconciliation.noteTransition("prompt", correlation, {
|
|
628
|
+
type: "agent_end",
|
|
629
|
+
content: { version: 1, type: "text", text: "completed", byteLength: 9, truncated: false },
|
|
630
|
+
});
|
|
459
631
|
releasePersist.resolve();
|
|
460
632
|
await expect(deadline).rejects.toThrow("held store failed");
|
|
461
633
|
await terminal;
|
|
@@ -523,13 +695,86 @@ test("agent_end upgrades a durable deadline terminal when it races a successful
|
|
|
523
695
|
code: "prompt_deadline_exceeded",
|
|
524
696
|
message: "deadline",
|
|
525
697
|
});
|
|
526
|
-
await reconciliation.noteTransition("prompt", correlation, {
|
|
698
|
+
await reconciliation.noteTransition("prompt", correlation, {
|
|
699
|
+
type: "agent_end",
|
|
700
|
+
content: { version: 1, type: "text", text: "completed", byteLength: 9, truncated: false },
|
|
701
|
+
});
|
|
527
702
|
const reloaded = createInvocationReconciliation({ store });
|
|
528
703
|
await reloaded.hydrate();
|
|
529
704
|
expect(reloaded.lookup("prompt", { clientRef: "upgrade-race-ref" })).toMatchObject({ status: "terminal_ok" });
|
|
530
705
|
});
|
|
531
706
|
|
|
532
|
-
test("
|
|
707
|
+
test("an empty late agent_end cannot upgrade a durable prompt deadline failure", async () => {
|
|
708
|
+
let records: unknown[] = [];
|
|
709
|
+
const store = {
|
|
710
|
+
path: null,
|
|
711
|
+
load: async () => records,
|
|
712
|
+
transact: async (mutator: (current: never[]) => never[]) => {
|
|
713
|
+
records = mutator(records as never);
|
|
714
|
+
},
|
|
715
|
+
} as never;
|
|
716
|
+
const reconciliation = createInvocationReconciliation({ store });
|
|
717
|
+
const correlation = { commandId: "empty-deadline-command", turnId: "empty-deadline-turn" };
|
|
718
|
+
await reconciliation.noteAccepted("prompt", correlation, "empty-deadline-ref");
|
|
719
|
+
await reconciliation.finalizeOutcome("prompt", correlation, {
|
|
720
|
+
kind: "failed",
|
|
721
|
+
code: "prompt_deadline_exceeded",
|
|
722
|
+
message: "deadline",
|
|
723
|
+
});
|
|
724
|
+
await reconciliation.noteTransition("prompt", correlation, {
|
|
725
|
+
type: "agent_end",
|
|
726
|
+
content: { version: 1, type: "text", text: "", byteLength: 0, truncated: false },
|
|
727
|
+
});
|
|
728
|
+
expect(reconciliation.lookup("prompt", { clientRef: "empty-deadline-ref" })).toMatchObject({
|
|
729
|
+
status: "failed",
|
|
730
|
+
error: { code: "prompt_deadline_exceeded" },
|
|
731
|
+
});
|
|
732
|
+
const reloaded = createInvocationReconciliation({ store });
|
|
733
|
+
await reloaded.hydrate();
|
|
734
|
+
expect(reloaded.lookup("prompt", { clientRef: "empty-deadline-ref" })).toMatchObject({
|
|
735
|
+
status: "failed",
|
|
736
|
+
error: { code: "prompt_deadline_exceeded" },
|
|
737
|
+
});
|
|
738
|
+
});
|
|
739
|
+
|
|
740
|
+
test("whitespace and provider failures stay actionable across a late empty agent_end", async () => {
|
|
741
|
+
let records: unknown[] = [];
|
|
742
|
+
const store = {
|
|
743
|
+
path: null,
|
|
744
|
+
load: async () => records,
|
|
745
|
+
transact: async (mutator: (current: never[]) => never[]) => {
|
|
746
|
+
records = mutator(records as never);
|
|
747
|
+
},
|
|
748
|
+
} as never;
|
|
749
|
+
const reconciliation = createInvocationReconciliation({ store });
|
|
750
|
+
const whitespace = { commandId: "whitespace-command", turnId: "whitespace-turn" };
|
|
751
|
+
await reconciliation.noteAccepted("prompt", whitespace, "whitespace-ref");
|
|
752
|
+
await reconciliation.noteTransition("prompt", whitespace, {
|
|
753
|
+
type: "agent_end",
|
|
754
|
+
content: { version: 1, type: "text", text: " \t\n ", byteLength: 4, truncated: false },
|
|
755
|
+
});
|
|
756
|
+
expect(reconciliation.lookup("prompt", { clientRef: "whitespace-ref" })).toMatchObject({
|
|
757
|
+
status: "failed",
|
|
758
|
+
error: { code: "prompt_failed", message: "Prompt submission failed." },
|
|
759
|
+
});
|
|
760
|
+
|
|
761
|
+
const providerFailure = { commandId: "provider-command", turnId: "provider-turn" };
|
|
762
|
+
await reconciliation.noteAccepted("prompt", providerFailure, "provider-ref");
|
|
763
|
+
await reconciliation.noteTransition("prompt", providerFailure, {
|
|
764
|
+
type: "agent_failed",
|
|
765
|
+
error: Object.assign(new Error("provider leaked detail"), { code: "provider_unavailable" }),
|
|
766
|
+
});
|
|
767
|
+
await reconciliation.noteTransition("prompt", providerFailure, {
|
|
768
|
+
type: "agent_end",
|
|
769
|
+
content: { version: 1, type: "text", text: "", byteLength: 0, truncated: false },
|
|
770
|
+
});
|
|
771
|
+
expect(reconciliation.lookup("prompt", { clientRef: "provider-ref" })).toMatchObject({
|
|
772
|
+
status: "failed",
|
|
773
|
+
error: { code: "provider_unavailable", message: "Prompt submission failed." },
|
|
774
|
+
});
|
|
775
|
+
});
|
|
776
|
+
|
|
777
|
+
test("an empty prompt terminal remains failed after a late real failure", async () => {
|
|
533
778
|
let records: unknown[] = [];
|
|
534
779
|
let writes = 0;
|
|
535
780
|
const store = {
|
|
@@ -550,9 +795,12 @@ test("failed agent_end upgrade persistence retains the deadline record for retry
|
|
|
550
795
|
code: "prompt_deadline_exceeded",
|
|
551
796
|
message: "deadline",
|
|
552
797
|
});
|
|
553
|
-
await expect(
|
|
554
|
-
"
|
|
555
|
-
|
|
798
|
+
await expect(
|
|
799
|
+
reconciliation.noteTransition("prompt", correlation, {
|
|
800
|
+
type: "agent_end",
|
|
801
|
+
content: { version: 1, type: "text", text: "completed", byteLength: 9, truncated: false },
|
|
802
|
+
}),
|
|
803
|
+
).rejects.toThrow("upgrade persist failed");
|
|
556
804
|
expect(reconciliation.lookup("prompt", { clientRef: "upgrade-failure-ref" })).toMatchObject({
|
|
557
805
|
status: "failed",
|
|
558
806
|
error: { code: "prompt_deadline_exceeded" },
|
|
@@ -563,17 +811,132 @@ test("failed agent_end upgrade persistence retains the deadline record for retry
|
|
|
563
811
|
status: "failed",
|
|
564
812
|
error: { code: "prompt_deadline_exceeded" },
|
|
565
813
|
});
|
|
566
|
-
await reconciliation.noteTransition("prompt", correlation, {
|
|
814
|
+
await reconciliation.noteTransition("prompt", correlation, {
|
|
815
|
+
type: "agent_end",
|
|
816
|
+
content: { version: 1, type: "text", text: "completed", byteLength: 9, truncated: false },
|
|
817
|
+
});
|
|
567
818
|
const reloaded = createInvocationReconciliation({ store });
|
|
568
819
|
await reloaded.hydrate();
|
|
569
820
|
expect(reloaded.lookup("prompt", { clientRef: "upgrade-failure-ref" })).toMatchObject({ status: "terminal_ok" });
|
|
570
821
|
});
|
|
571
822
|
|
|
823
|
+
test("late cancellation and non-text activity upgrade deadline terminals", async () => {
|
|
824
|
+
const makeStore = () => {
|
|
825
|
+
let records: unknown[] = [];
|
|
826
|
+
return {
|
|
827
|
+
path: null,
|
|
828
|
+
load: async () => records,
|
|
829
|
+
transact: async (mutator: (current: never[]) => never[]) => {
|
|
830
|
+
records = mutator(records as never);
|
|
831
|
+
},
|
|
832
|
+
} as never;
|
|
833
|
+
};
|
|
834
|
+
const cancelled = createInvocationReconciliation({ store: makeStore() });
|
|
835
|
+
const cancelledCorrelation = { commandId: "late-cancel-command", turnId: "late-cancel-turn" };
|
|
836
|
+
await cancelled.noteAccepted("prompt", cancelledCorrelation, "late-cancel-ref");
|
|
837
|
+
await cancelled.finalizeOutcome("prompt", cancelledCorrelation, {
|
|
838
|
+
kind: "failed",
|
|
839
|
+
code: "prompt_deadline_exceeded",
|
|
840
|
+
message: "deadline",
|
|
841
|
+
});
|
|
842
|
+
await cancelled.noteTransition("prompt", cancelledCorrelation, {
|
|
843
|
+
type: "agent_end",
|
|
844
|
+
outcome: { kind: "stopped", reason: "cancelled", provenance: "client_cancel" },
|
|
845
|
+
});
|
|
846
|
+
expect(cancelled.lookup("prompt", { clientRef: "late-cancel-ref" })).toMatchObject({ status: "terminal_ok" });
|
|
847
|
+
|
|
848
|
+
const activity = createInvocationReconciliation({ store: makeStore() });
|
|
849
|
+
const activityCorrelation = { commandId: "late-activity-command", turnId: "late-activity-turn" };
|
|
850
|
+
await activity.noteAccepted("prompt", activityCorrelation, "late-activity-ref");
|
|
851
|
+
await activity.finalizeOutcome("prompt", activityCorrelation, {
|
|
852
|
+
kind: "failed",
|
|
853
|
+
code: "prompt_deadline_exceeded",
|
|
854
|
+
message: "deadline",
|
|
855
|
+
});
|
|
856
|
+
await activity.noteTransition("prompt", activityCorrelation, { type: "agent_end", hasActivity: true });
|
|
857
|
+
expect(activity.lookup("prompt", { clientRef: "late-activity-ref" })).toMatchObject({ status: "terminal_ok" });
|
|
858
|
+
});
|
|
859
|
+
test("finalization without an explicit outcome follows the noteTransition evidence predicate", async () => {
|
|
860
|
+
const makeLocalStore = () => {
|
|
861
|
+
let records: unknown[] = [];
|
|
862
|
+
return {
|
|
863
|
+
path: null,
|
|
864
|
+
load: async () => records,
|
|
865
|
+
transact: async (mutator: (current: never[]) => never[]) => {
|
|
866
|
+
records = mutator(records as never);
|
|
867
|
+
},
|
|
868
|
+
} as never;
|
|
869
|
+
};
|
|
870
|
+
const contentBearing = createInvocationReconciliation({ store: makeLocalStore() });
|
|
871
|
+
const contentCorrelation = { commandId: "final-content-command", turnId: "final-content-turn" };
|
|
872
|
+
await contentBearing.noteAccepted("prompt", contentCorrelation, "final-content-ref");
|
|
873
|
+
await contentBearing.finalizeOutcome("prompt", contentCorrelation, undefined, undefined, undefined, {
|
|
874
|
+
content: { version: 1, type: "text", text: "completed", byteLength: 9, truncated: false },
|
|
875
|
+
});
|
|
876
|
+
expect(contentBearing.lookup("prompt", { clientRef: "final-content-ref" })).toMatchObject({
|
|
877
|
+
status: "terminal_ok",
|
|
878
|
+
});
|
|
879
|
+
|
|
880
|
+
const activityBearing = createInvocationReconciliation({ store: makeLocalStore() });
|
|
881
|
+
const activityCorrelation = { commandId: "final-activity-command", turnId: "final-activity-turn" };
|
|
882
|
+
await activityBearing.noteAccepted("prompt", activityCorrelation, "final-activity-ref");
|
|
883
|
+
await activityBearing.finalizeOutcome("prompt", activityCorrelation, undefined, undefined, undefined, {
|
|
884
|
+
hasActivity: true,
|
|
885
|
+
});
|
|
886
|
+
expect(activityBearing.lookup("prompt", { clientRef: "final-activity-ref" })).toMatchObject({
|
|
887
|
+
status: "terminal_ok",
|
|
888
|
+
});
|
|
889
|
+
|
|
890
|
+
const stoppedOutcome = createInvocationReconciliation({ store: makeLocalStore() });
|
|
891
|
+
const stoppedCorrelation = { commandId: "final-stopped-command", turnId: "final-stopped-turn" };
|
|
892
|
+
await stoppedOutcome.noteAccepted("prompt", stoppedCorrelation, "final-stopped-ref");
|
|
893
|
+
await stoppedOutcome.finalizeOutcome("prompt", stoppedCorrelation, undefined, undefined, undefined, {
|
|
894
|
+
outcomeKind: "stopped",
|
|
895
|
+
});
|
|
896
|
+
expect(stoppedOutcome.lookup("prompt", { clientRef: "final-stopped-ref" })).toMatchObject({
|
|
897
|
+
status: "terminal_ok",
|
|
898
|
+
});
|
|
899
|
+
|
|
900
|
+
const evidenceFree = createInvocationReconciliation({ store: makeLocalStore() });
|
|
901
|
+
const emptyCorrelation = { commandId: "final-empty-command", turnId: "final-empty-turn" };
|
|
902
|
+
await evidenceFree.noteAccepted("prompt", emptyCorrelation, "final-empty-ref");
|
|
903
|
+
await evidenceFree.finalizeOutcome("prompt", emptyCorrelation);
|
|
904
|
+
expect(evidenceFree.lookup("prompt", { clientRef: "final-empty-ref" })).toMatchObject({
|
|
905
|
+
status: "failed",
|
|
906
|
+
error: { code: "prompt_failed" },
|
|
907
|
+
});
|
|
908
|
+
const legacyArg5 = createInvocationReconciliation({ store: makeLocalStore() });
|
|
909
|
+
const legacyCorrelation = { commandId: "final-legacy-command", turnId: "final-legacy-turn" };
|
|
910
|
+
await legacyArg5.noteAccepted("prompt", legacyCorrelation, "final-legacy-ref");
|
|
911
|
+
await legacyArg5.finalizeOutcome("prompt", legacyCorrelation, undefined, undefined, {
|
|
912
|
+
content: { version: 1, type: "text", text: "legacy", byteLength: 6, truncated: false },
|
|
913
|
+
});
|
|
914
|
+
expect(legacyArg5.lookup("prompt", { clientRef: "final-legacy-ref" })).toMatchObject({
|
|
915
|
+
status: "terminal_ok",
|
|
916
|
+
});
|
|
917
|
+
|
|
918
|
+
const explicitFailure = createInvocationReconciliation({ store: makeLocalStore() });
|
|
919
|
+
const failureCorrelation = { commandId: "final-failure-command", turnId: "final-failure-turn" };
|
|
920
|
+
await explicitFailure.noteAccepted("prompt", failureCorrelation, "final-failure-ref");
|
|
921
|
+
await explicitFailure.finalizeOutcome("prompt", failureCorrelation, {
|
|
922
|
+
kind: "failed",
|
|
923
|
+
code: "provider_rejected",
|
|
924
|
+
message: "provider rejected",
|
|
925
|
+
});
|
|
926
|
+
expect(explicitFailure.lookup("prompt", { clientRef: "final-failure-ref" })).toMatchObject({
|
|
927
|
+
status: "failed",
|
|
928
|
+
error: { code: "provider_rejected" },
|
|
929
|
+
});
|
|
930
|
+
});
|
|
931
|
+
|
|
572
932
|
test("a reason attached after a prompt settled is never replaced by a later failure", async () => {
|
|
573
933
|
const reconciliation = createInvocationReconciliation();
|
|
574
934
|
const correlation = { commandId: "late-reason-command", turnId: "late-reason-turn" };
|
|
575
935
|
await reconciliation.noteAccepted("prompt", correlation, "late-reason-ref");
|
|
576
|
-
await reconciliation.noteTransition("prompt", correlation, {
|
|
936
|
+
await reconciliation.noteTransition("prompt", correlation, {
|
|
937
|
+
type: "agent_end",
|
|
938
|
+
content: { version: 1, type: "text", text: "completed", byteLength: 9, truncated: false },
|
|
939
|
+
});
|
|
577
940
|
const claimed = reconciliation.lookup("prompt", { clientRef: "late-reason-ref" }) as Record<string, unknown>;
|
|
578
941
|
expect(claimed).toEqual({
|
|
579
942
|
status: "terminal_ok",
|
|
@@ -649,7 +1012,10 @@ test("retains terminal host reconciliation records past the 15-minute window", a
|
|
|
649
1012
|
reconciliation.admit("skill", "aged-skill-ref");
|
|
650
1013
|
await reconciliation.noteAccepted("prompt", promptCorrelation, "aged-ref");
|
|
651
1014
|
await reconciliation.noteAccepted("skill", skillCorrelation, "aged-skill-ref");
|
|
652
|
-
await reconciliation.noteTransition("prompt", promptCorrelation, {
|
|
1015
|
+
await reconciliation.noteTransition("prompt", promptCorrelation, {
|
|
1016
|
+
type: "agent_end",
|
|
1017
|
+
content: { version: 1, type: "text", text: "completed", byteLength: 9, truncated: false },
|
|
1018
|
+
});
|
|
653
1019
|
await reconciliation.noteTransition("skill", skillCorrelation, { type: "agent_end" });
|
|
654
1020
|
|
|
655
1021
|
clock += 15 * 60_000 + 1;
|
|
@@ -679,7 +1045,10 @@ test("capacity eviction still releases the clientRef and reports honest unknown"
|
|
|
679
1045
|
const correlation = { commandId: `capacity-command-${index}`, turnId: `capacity-turn-${index}` };
|
|
680
1046
|
reconciliation.admit("prompt", `capacity-ref-${index}`);
|
|
681
1047
|
await reconciliation.noteAccepted("prompt", correlation, `capacity-ref-${index}`);
|
|
682
|
-
await reconciliation.noteTransition("prompt", correlation, {
|
|
1048
|
+
await reconciliation.noteTransition("prompt", correlation, {
|
|
1049
|
+
type: "agent_end",
|
|
1050
|
+
content: { version: 1, type: "text", text: "completed", byteLength: 9, truncated: false },
|
|
1051
|
+
});
|
|
683
1052
|
}
|
|
684
1053
|
expect(reconciliation.lookup("prompt", { clientRef: "capacity-ref-1" })).toEqual({
|
|
685
1054
|
status: "unknown",
|
|
@@ -735,6 +1104,8 @@ describe("SessionSdkSessionRuntime", () => {
|
|
|
735
1104
|
});
|
|
736
1105
|
test("SDK-only host admits, replays, and conflicts terminal abort requests durably", async () => {
|
|
737
1106
|
const cwd = await mkdtemp(path.join(os.tmpdir(), "gjc-sdk-terminal-abort-"));
|
|
1107
|
+
const operatorCapability = "runtime-terminal-abort-capability";
|
|
1108
|
+
setBrokerRuntimeAbortCapabilityForTest(operatorCapability);
|
|
738
1109
|
const handlers = new Map<string, (event: unknown, ctx: ExtensionContext) => Promise<void> | void>();
|
|
739
1110
|
const api = {
|
|
740
1111
|
on(event: string, handler: (event: unknown, ctx: ExtensionContext) => Promise<void> | void) {
|
|
@@ -770,7 +1141,13 @@ describe("SessionSdkSessionRuntime", () => {
|
|
|
770
1141
|
},
|
|
771
1142
|
abortPromptAndWaitWithTerminal: async (handle, options) => {
|
|
772
1143
|
seamCalls.push({ handle, scope: options.terminal?.scope ?? "none" });
|
|
773
|
-
return {
|
|
1144
|
+
return {
|
|
1145
|
+
status: "settled",
|
|
1146
|
+
terminalScope: {
|
|
1147
|
+
abortedAttemptEpoch: activeEpoch,
|
|
1148
|
+
lineageIdHash: "runtime-test-lineage",
|
|
1149
|
+
},
|
|
1150
|
+
};
|
|
774
1151
|
},
|
|
775
1152
|
},
|
|
776
1153
|
});
|
|
@@ -865,7 +1242,43 @@ describe("SessionSdkSessionRuntime", () => {
|
|
|
865
1242
|
}),
|
|
866
1243
|
]),
|
|
867
1244
|
);
|
|
1245
|
+
|
|
1246
|
+
// A separately connected, explicitly confirmed local operator may stop
|
|
1247
|
+
// the active turn and its exact owned work without weakening ordinary
|
|
1248
|
+
// connection ownership checks.
|
|
1249
|
+
transport.feed("local-operator", {
|
|
1250
|
+
type: "control_request",
|
|
1251
|
+
id: "terminal-operator-abort",
|
|
1252
|
+
operation: "turn.abort",
|
|
1253
|
+
input: {
|
|
1254
|
+
mode: "terminal",
|
|
1255
|
+
scope: "owned",
|
|
1256
|
+
operator: true,
|
|
1257
|
+
[BROKER_RUNTIME_ABORT_CAPABILITY_FIELD]: operatorCapability,
|
|
1258
|
+
},
|
|
1259
|
+
confirm: true,
|
|
1260
|
+
idempotencyKey: "terminal-operator-key",
|
|
1261
|
+
} as SdkFrame);
|
|
1262
|
+
await waitForFrame("terminal-operator-abort");
|
|
1263
|
+
expect(seamCalls).toEqual([
|
|
1264
|
+
{ handle: "exact-run-handle", scope: "turn" },
|
|
1265
|
+
{ handle: "later-run-handle", scope: "owned" },
|
|
1266
|
+
]);
|
|
1267
|
+
expect(transport.sent.find(frame => frame.id === "terminal-operator-abort")).toMatchObject({
|
|
1268
|
+
ok: true,
|
|
1269
|
+
result: expect.objectContaining({ turn: "stopped", ownedWork: "stopped" }),
|
|
1270
|
+
});
|
|
1271
|
+
const operatorKeyHash = createHash("sha256").update("terminal-operator-key").digest("hex");
|
|
1272
|
+
const deliveryDeadline = Date.now() + 15_000;
|
|
1273
|
+
while (
|
|
1274
|
+
reconciliationStore.snapshotTerminalScopes().find(record => record.idempotencyKeyHash === operatorKeyHash)
|
|
1275
|
+
?.responseState !== "sent"
|
|
1276
|
+
) {
|
|
1277
|
+
if (Date.now() > deliveryDeadline) throw new Error("Timed out waiting for operator delivery receipt");
|
|
1278
|
+
await Bun.sleep(20);
|
|
1279
|
+
}
|
|
868
1280
|
} finally {
|
|
1281
|
+
setBrokerRuntimeAbortCapabilityForTest(undefined);
|
|
869
1282
|
await handlers.get("session_shutdown")?.({}, ctx);
|
|
870
1283
|
await rm(cwd, { recursive: true, force: true });
|
|
871
1284
|
}
|
|
@@ -1076,7 +1489,71 @@ describe("SessionSdkSessionRuntime", () => {
|
|
|
1076
1489
|
}
|
|
1077
1490
|
});
|
|
1078
1491
|
|
|
1079
|
-
test("SDK-only
|
|
1492
|
+
test("SDK-only operator abort never adopts a turn that starts after idle admission", async () => {
|
|
1493
|
+
const cwd = await mkdtemp(path.join(os.tmpdir(), "gjc-sdk-operator-idle-race-"));
|
|
1494
|
+
const capability = "operator-idle-race-capability";
|
|
1495
|
+
setBrokerRuntimeAbortCapabilityForTest(capability);
|
|
1496
|
+
const handlers = new Map<string, (event: unknown, ctx: ExtensionContext) => Promise<void> | void>();
|
|
1497
|
+
const api = {
|
|
1498
|
+
on(event: string, handler: (event: unknown, ctx: ExtensionContext) => Promise<void> | void) {
|
|
1499
|
+
handlers.set(event, handler);
|
|
1500
|
+
},
|
|
1501
|
+
} as unknown as ExtensionAPI;
|
|
1502
|
+
const transport = memoryTransport();
|
|
1503
|
+
const reconciliationStore = createReconciliationStore({
|
|
1504
|
+
sessionFile: path.join(cwd, "session.json"),
|
|
1505
|
+
sessionId: transport.sessionId,
|
|
1506
|
+
});
|
|
1507
|
+
const seamCalls: string[] = [];
|
|
1508
|
+
let promptReads = 0;
|
|
1509
|
+
createSdkSessionRuntimeExtension(api, {
|
|
1510
|
+
agentDir: cwd,
|
|
1511
|
+
createTransport: async () => transport,
|
|
1512
|
+
terminalAbortSeams: {
|
|
1513
|
+
getReconciliationStore: () => reconciliationStore,
|
|
1514
|
+
getTerminalTurnEpoch: () => (promptReads++ === 0 ? undefined : 9),
|
|
1515
|
+
getActivePromptHandle: () => (promptReads === 0 ? undefined : "successor-handle"),
|
|
1516
|
+
getActivePromptOwnerConnectionId: () => "successor-owner",
|
|
1517
|
+
cancelPendingPreflightForTerminalAbort: () => {},
|
|
1518
|
+
abortPromptAndWaitWithTerminal: async handle => {
|
|
1519
|
+
seamCalls.push(handle);
|
|
1520
|
+
return { status: "settled", terminalScope: {} };
|
|
1521
|
+
},
|
|
1522
|
+
},
|
|
1523
|
+
});
|
|
1524
|
+
const ctx = extensionContext(transport.sessionId, cwd);
|
|
1525
|
+
try {
|
|
1526
|
+
await handlers.get("session_start")?.({}, ctx);
|
|
1527
|
+
transport.feed("local-operator", {
|
|
1528
|
+
type: "control_request",
|
|
1529
|
+
id: "operator-idle-race",
|
|
1530
|
+
operation: "turn.abort",
|
|
1531
|
+
input: {
|
|
1532
|
+
mode: "terminal",
|
|
1533
|
+
operator: true,
|
|
1534
|
+
[BROKER_RUNTIME_ABORT_CAPABILITY_FIELD]: capability,
|
|
1535
|
+
},
|
|
1536
|
+
confirm: true,
|
|
1537
|
+
idempotencyKey: "operator-idle-race-key",
|
|
1538
|
+
} as SdkFrame);
|
|
1539
|
+
const deadline = Date.now() + 15_000;
|
|
1540
|
+
while (!transport.sent.some(frame => frame.id === "operator-idle-race")) {
|
|
1541
|
+
if (Date.now() > deadline) throw new Error("Timed out waiting for operator idle-race response");
|
|
1542
|
+
await Bun.sleep(20);
|
|
1543
|
+
}
|
|
1544
|
+
expect(transport.sent.find(frame => frame.id === "operator-idle-race")).toMatchObject({
|
|
1545
|
+
ok: true,
|
|
1546
|
+
result: expect.objectContaining({ turn: "no_active_turn", terminal: "terminal_no_effect" }),
|
|
1547
|
+
});
|
|
1548
|
+
expect(seamCalls).toHaveLength(0);
|
|
1549
|
+
} finally {
|
|
1550
|
+
setBrokerRuntimeAbortCapabilityForTest(undefined);
|
|
1551
|
+
await handlers.get("session_shutdown")?.({}, ctx);
|
|
1552
|
+
await rm(cwd, { recursive: true, force: true });
|
|
1553
|
+
}
|
|
1554
|
+
});
|
|
1555
|
+
|
|
1556
|
+
test("SDK-only host re-reads the active prompt AND its owner after an owner-mismatch reservation race", async () => {
|
|
1080
1557
|
const cwd = await mkdtemp(path.join(os.tmpdir(), "gjc-sdk-terminal-owner-race-"));
|
|
1081
1558
|
const handlers = new Map<string, (event: unknown, ctx: ExtensionContext) => Promise<void> | void>();
|
|
1082
1559
|
const api = {
|
|
@@ -1870,10 +2347,10 @@ describe("SessionSdkSessionRuntime", () => {
|
|
|
1870
2347
|
prompt("conn-b", "stale-b");
|
|
1871
2348
|
await waitResponse("stale-b");
|
|
1872
2349
|
// A later AGENT-INITIATED turn starts: the pending queue is empty, so
|
|
1873
|
-
// the
|
|
2350
|
+
// the predecessor's conn-a owner is cleared and no SDK client owns it.
|
|
1874
2351
|
await handlers.get("agent_start")?.({ type: "agent_start" }, ctx);
|
|
1875
|
-
//
|
|
1876
|
-
transport.feed("conn-
|
|
2352
|
+
// A's stale owner must NOT stop the agent-initiated turn.
|
|
2353
|
+
transport.feed("conn-a", {
|
|
1877
2354
|
type: "control_request",
|
|
1878
2355
|
id: "stale-owner-abort",
|
|
1879
2356
|
operation: "turn.abort",
|
|
@@ -1987,6 +2464,40 @@ describe("SessionSdkSessionRuntime", () => {
|
|
|
1987
2464
|
}
|
|
1988
2465
|
});
|
|
1989
2466
|
|
|
2467
|
+
test("agent-initiated successor activity cannot renew a stale predecessor deadline", async () => {
|
|
2468
|
+
const cwd = await mkdtemp(path.join(os.tmpdir(), "gjc-sdk-stale-deadline-owner-"));
|
|
2469
|
+
try {
|
|
2470
|
+
const harness = await invocationHarness("stale-deadline-owner", cwd, {
|
|
2471
|
+
settings: {
|
|
2472
|
+
get: (key: string) =>
|
|
2473
|
+
key === "sdk.promptDeadlineMs" ? 25 : key === "sdk.promptMaxRuntimeMs" ? 60_000 : undefined,
|
|
2474
|
+
} as unknown as Settings,
|
|
2475
|
+
sendUserMessage: async (_content, options) => {
|
|
2476
|
+
await options?.onPreflightAcceptCommit?.();
|
|
2477
|
+
await neverSettlingPromise();
|
|
2478
|
+
},
|
|
2479
|
+
});
|
|
2480
|
+
const prompt = await harness.control("turn.prompt", { text: "predecessor" });
|
|
2481
|
+
expect(prompt.ok).toBe(true);
|
|
2482
|
+
const ids = { commandId: prompt.result?.commandId, turnId: prompt.result?.turnId };
|
|
2483
|
+
await harness.emit("agent_start");
|
|
2484
|
+
// Empty drain represents an agent-initiated successor with no SDK owner.
|
|
2485
|
+
await harness.emit("agent_start");
|
|
2486
|
+
for (let i = 0; i < 3; i += 1) {
|
|
2487
|
+
await harness.emit("tool_execution_start");
|
|
2488
|
+
await Bun.sleep(15);
|
|
2489
|
+
}
|
|
2490
|
+
expect(await settledStatus(harness, "turn.prompt_status", ids)).toMatchObject({
|
|
2491
|
+
status: "failed",
|
|
2492
|
+
error: { code: "prompt_deadline_exceeded" },
|
|
2493
|
+
});
|
|
2494
|
+
await harness.stop();
|
|
2495
|
+
} finally {
|
|
2496
|
+
await Bun.sleep(50);
|
|
2497
|
+
await rm(cwd, { recursive: true, force: true });
|
|
2498
|
+
}
|
|
2499
|
+
});
|
|
2500
|
+
|
|
1990
2501
|
test("SDK-only host retires an accepted-then-failed submission so a later agent-initiated turn is not mis-owned", async () => {
|
|
1991
2502
|
// An idle submission is accepted (pending entry pushed) and then REJECTS
|
|
1992
2503
|
// before agent_start (e.g. the busy retry expires). The failed entry must
|
|
@@ -2863,6 +3374,7 @@ describe("SessionSdkSessionRuntime", () => {
|
|
|
2863
3374
|
},
|
|
2864
3375
|
} as any;
|
|
2865
3376
|
const sessionId = "broker-recovery";
|
|
3377
|
+
const endpointUrl = "ws://127.0.0.1:1";
|
|
2866
3378
|
createSdkSessionRuntimeExtension(api, {
|
|
2867
3379
|
agentDir,
|
|
2868
3380
|
createTransport: async ({ stateRoot, token }) => ({
|
|
@@ -2874,8 +3386,8 @@ describe("SessionSdkSessionRuntime", () => {
|
|
|
2874
3386
|
start: async () => {
|
|
2875
3387
|
const endpoint = path.join(stateRoot, "sdk", `${sessionId}.json`);
|
|
2876
3388
|
await mkdir(path.dirname(endpoint), { recursive: true });
|
|
2877
|
-
await writeFile(endpoint, JSON.stringify({ sessionId, token, pid: process.pid }));
|
|
2878
|
-
return { url:
|
|
3389
|
+
await writeFile(endpoint, JSON.stringify({ sessionId, token, pid: process.pid, url: endpointUrl }));
|
|
3390
|
+
return { url: endpointUrl };
|
|
2879
3391
|
},
|
|
2880
3392
|
stop: async () => {},
|
|
2881
3393
|
}),
|
|
@@ -2932,6 +3444,7 @@ describe("SessionSdkSessionRuntime", () => {
|
|
|
2932
3444
|
createSdkSessionRuntimeExtension(api, {
|
|
2933
3445
|
agentDir,
|
|
2934
3446
|
brokerRegistrationRequired: true,
|
|
3447
|
+
lifecycleRequestId: "broker-required-marker",
|
|
2935
3448
|
createTransport: async ({ sessionId, stateRoot, token }) => ({
|
|
2936
3449
|
sessionId,
|
|
2937
3450
|
stateRoot,
|
|
@@ -2985,7 +3498,7 @@ async function invocationHarness(
|
|
|
2985
3498
|
sessionId: string,
|
|
2986
3499
|
cwd: string,
|
|
2987
3500
|
hooks: {
|
|
2988
|
-
sendUserMessage?: (content: unknown, options?: PreflightHooks & { deliverAs?: string }) => Promise<
|
|
3501
|
+
sendUserMessage?: (content: unknown, options?: PreflightHooks & { deliverAs?: string }) => Promise<unknown>;
|
|
2989
3502
|
invokeSkill?: (name: string, args?: string, options?: PreflightHooks) => Promise<unknown>;
|
|
2990
3503
|
abort?: () => void;
|
|
2991
3504
|
isIdle?: () => boolean;
|
|
@@ -3010,7 +3523,10 @@ async function invocationHarness(
|
|
|
3010
3523
|
on(event: string, handler: (event: unknown, ctx: unknown) => Promise<void> | void) {
|
|
3011
3524
|
handlers.set(event, handler);
|
|
3012
3525
|
},
|
|
3013
|
-
sendUserMessage:
|
|
3526
|
+
sendUserMessage: async (content: unknown, options?: PreflightHooks & { deliverAs?: string }) => {
|
|
3527
|
+
const result = await hooks.sendUserMessage?.(content, options);
|
|
3528
|
+
return result === undefined ? "completed" : result;
|
|
3529
|
+
},
|
|
3014
3530
|
} as unknown as ExtensionAPI;
|
|
3015
3531
|
const interceptorStore = hooks.persistInterceptor
|
|
3016
3532
|
? createInterceptorReconciliationStore(
|
|
@@ -3202,13 +3718,19 @@ async function settledStatus(
|
|
|
3202
3718
|
name: string,
|
|
3203
3719
|
input: Record<string, unknown>,
|
|
3204
3720
|
): Promise<NonNullable<ResponseFrame["result"]>> {
|
|
3205
|
-
|
|
3721
|
+
// Wall-clock budget instead of a fixed poll count: CI runners can starve the
|
|
3722
|
+
// event loop long enough to exhaust 200 ~1ms polls before a 25ms deadline
|
|
3723
|
+
// timer is dispatched, failing a correct implementation on timing alone.
|
|
3724
|
+
// The contract is unchanged — the status must still reach a terminal
|
|
3725
|
+
// reconciliation state with the asserted shape within a bounded horizon.
|
|
3726
|
+
const budgetEndsAt = Date.now() + 10_000;
|
|
3727
|
+
for (;;) {
|
|
3206
3728
|
const frame = await harness.query(name, input);
|
|
3207
3729
|
const result = frame.result;
|
|
3208
3730
|
if (result && (result.status === "failed" || result.status === "terminal_ok")) return result;
|
|
3731
|
+
if (Date.now() > budgetEndsAt) throw new Error(`${name} never reported a terminal reconciliation status`);
|
|
3209
3732
|
await Bun.sleep(1);
|
|
3210
3733
|
}
|
|
3211
|
-
throw new Error(`${name} never reported a terminal reconciliation status`);
|
|
3212
3734
|
}
|
|
3213
3735
|
|
|
3214
3736
|
function neverSettlingPromise(): Promise<void> {
|
|
@@ -3429,7 +3951,10 @@ describe("post-acceptance invocation terminalization", () => {
|
|
|
3429
3951
|
},
|
|
3430
3952
|
],
|
|
3431
3953
|
});
|
|
3432
|
-
expect(await settledStatus(harness, "turn.prompt_status", ids)).toMatchObject({
|
|
3954
|
+
expect(await settledStatus(harness, "turn.prompt_status", ids)).toMatchObject({
|
|
3955
|
+
status: "failed",
|
|
3956
|
+
error: { code: "prompt_failed" },
|
|
3957
|
+
});
|
|
3433
3958
|
await harness.stop();
|
|
3434
3959
|
} finally {
|
|
3435
3960
|
await Bun.sleep(50);
|
|
@@ -3460,7 +3985,10 @@ describe("post-acceptance invocation terminalization", () => {
|
|
|
3460
3985
|
},
|
|
3461
3986
|
],
|
|
3462
3987
|
});
|
|
3463
|
-
expect(await settledStatus(harness, "turn.prompt_status", ids)).toMatchObject({
|
|
3988
|
+
expect(await settledStatus(harness, "turn.prompt_status", ids)).toMatchObject({
|
|
3989
|
+
status: "failed",
|
|
3990
|
+
error: { code: "prompt_failed" },
|
|
3991
|
+
});
|
|
3464
3992
|
await harness.stop();
|
|
3465
3993
|
} finally {
|
|
3466
3994
|
await Bun.sleep(50);
|
|
@@ -3489,7 +4017,8 @@ describe("post-acceptance invocation terminalization", () => {
|
|
|
3489
4017
|
});
|
|
3490
4018
|
await harness.emit("agent_end", event);
|
|
3491
4019
|
expect(await settledStatus(harness, "turn.prompt_status", ids)).toMatchObject({
|
|
3492
|
-
status: "
|
|
4020
|
+
status: "failed",
|
|
4021
|
+
error: { code: "prompt_failed" },
|
|
3493
4022
|
});
|
|
3494
4023
|
await harness.stop();
|
|
3495
4024
|
} finally {
|
|
@@ -3530,7 +4059,9 @@ describe("post-acceptance invocation terminalization", () => {
|
|
|
3530
4059
|
error: { code: "provider_http_402" },
|
|
3531
4060
|
});
|
|
3532
4061
|
expect((await harness.query("turn.prompt_status", replacementIds)).result?.status).toBe("in_flight");
|
|
3533
|
-
await harness.emit("agent_end", {
|
|
4062
|
+
await harness.emit("agent_end", {
|
|
4063
|
+
messages: [{ role: "assistant", stopReason: "stop", content: "completed" }],
|
|
4064
|
+
});
|
|
3534
4065
|
expect(await settledStatus(harness, "turn.prompt_status", replacementIds)).toMatchObject({
|
|
3535
4066
|
status: "terminal_ok",
|
|
3536
4067
|
});
|
|
@@ -3656,7 +4187,9 @@ describe("post-acceptance invocation terminalization", () => {
|
|
|
3656
4187
|
expect(successor.ok).toBe(true);
|
|
3657
4188
|
const successorIds = { commandId: successor.result?.commandId, turnId: successor.result?.turnId };
|
|
3658
4189
|
await harness.emit("agent_start");
|
|
3659
|
-
await harness.emit("agent_end", {
|
|
4190
|
+
await harness.emit("agent_end", {
|
|
4191
|
+
messages: [{ role: "assistant", stopReason: "stop", content: "completed" }],
|
|
4192
|
+
});
|
|
3660
4193
|
secondInflight.resolve();
|
|
3661
4194
|
expect(await settledStatus(harness, "turn.prompt_status", successorIds)).toMatchObject({
|
|
3662
4195
|
status: "terminal_ok",
|
|
@@ -3789,7 +4322,9 @@ describe("post-acceptance invocation terminalization", () => {
|
|
|
3789
4322
|
expect(successor.ok).toBe(true);
|
|
3790
4323
|
const successorIds = { commandId: successor.result?.commandId, turnId: successor.result?.turnId };
|
|
3791
4324
|
await harness.emit("agent_start");
|
|
3792
|
-
await harness.emit("agent_end", {
|
|
4325
|
+
await harness.emit("agent_end", {
|
|
4326
|
+
messages: [{ role: "assistant", stopReason: "stop", content: "completed" }],
|
|
4327
|
+
});
|
|
3793
4328
|
secondInflight.resolve();
|
|
3794
4329
|
expect(await settledStatus(harness, "turn.prompt_status", successorIds)).toMatchObject({
|
|
3795
4330
|
status: "terminal_ok",
|
|
@@ -3938,7 +4473,7 @@ describe("post-acceptance invocation terminalization", () => {
|
|
|
3938
4473
|
status: "failed",
|
|
3939
4474
|
error: { code: "aborted" },
|
|
3940
4475
|
});
|
|
3941
|
-
await harness.emit("agent_end");
|
|
4476
|
+
await harness.emit("agent_end", { messages: [{ role: "assistant", content: "completed" }] });
|
|
3942
4477
|
expect(await settledStatus(harness, "turn.prompt_status", secondIds)).toMatchObject({
|
|
3943
4478
|
status: "terminal_ok",
|
|
3944
4479
|
});
|
|
@@ -3989,8 +4524,8 @@ describe("post-acceptance invocation terminalization", () => {
|
|
|
3989
4524
|
expect(await harness.query("turn.prompt_status", successorIds)).toMatchObject({
|
|
3990
4525
|
result: { status: expect.stringMatching(/accepted|in_flight/) },
|
|
3991
4526
|
});
|
|
3992
|
-
await harness.emit("agent_end");
|
|
3993
|
-
await harness.emit("agent_end");
|
|
4527
|
+
await harness.emit("agent_end", { messages: [{ role: "assistant", content: "completed" }] });
|
|
4528
|
+
await harness.emit("agent_end", { messages: [{ role: "assistant", content: "completed" }] });
|
|
3994
4529
|
expect(await settledStatus(harness, "turn.prompt_status", successorIds)).toMatchObject({
|
|
3995
4530
|
status: "terminal_ok",
|
|
3996
4531
|
});
|
|
@@ -4078,7 +4613,7 @@ describe("post-acceptance invocation terminalization", () => {
|
|
|
4078
4613
|
expect(accepted.ok).toBe(true);
|
|
4079
4614
|
const successorIds = { commandId: accepted.result?.commandId, turnId: accepted.result?.turnId };
|
|
4080
4615
|
await harness.emit("agent_start");
|
|
4081
|
-
await harness.emit("agent_end");
|
|
4616
|
+
await harness.emit("agent_end", { messages: [{ role: "assistant", content: "completed" }] });
|
|
4082
4617
|
expect(await settledStatus(harness, "turn.prompt_status", successorIds)).toMatchObject({
|
|
4083
4618
|
status: "terminal_ok",
|
|
4084
4619
|
});
|
|
@@ -4277,7 +4812,7 @@ describe("post-acceptance invocation terminalization", () => {
|
|
|
4277
4812
|
const accepted = await harness.control("turn.prompt", { text: "hello" });
|
|
4278
4813
|
const { commandId, turnId } = accepted.result ?? {};
|
|
4279
4814
|
await harness.emit("agent_start");
|
|
4280
|
-
await harness.emit("agent_end");
|
|
4815
|
+
await harness.emit("agent_end", { messages: [{ role: "assistant", content: "completed" }] });
|
|
4281
4816
|
const claimed = await settledStatus(harness, "turn.prompt_status", { commandId, turnId });
|
|
4282
4817
|
expect(claimed).toMatchObject({ status: "terminal_ok" });
|
|
4283
4818
|
inflight.reject(Object.assign(new Error("late provider failure"), { code: "upstream_error" }));
|
|
@@ -4445,7 +4980,7 @@ describe("accepted-control zero-execution bound (#4668)", () => {
|
|
|
4445
4980
|
const first = await harness.control("turn.prompt", { text: "first" });
|
|
4446
4981
|
expect(first.ok).toBe(true);
|
|
4447
4982
|
await harness.emit("agent_start");
|
|
4448
|
-
await harness.emit("agent_end");
|
|
4983
|
+
await harness.emit("agent_end", { messages: [{ role: "assistant", content: "completed" }] });
|
|
4449
4984
|
const followUpB = await harness.control("turn.follow_up", { text: "b" });
|
|
4450
4985
|
const followUpC = await harness.control("turn.follow_up", { text: "c" });
|
|
4451
4986
|
promoted[0]?.({ startsOwnRun: true });
|
|
@@ -4593,7 +5128,7 @@ describe("accepted-control zero-execution bound (#4668)", () => {
|
|
|
4593
5128
|
}
|
|
4594
5129
|
const midRun = await harness.query("turn.prompt_status", ids);
|
|
4595
5130
|
expect(midRun.result?.status).not.toBe("failed");
|
|
4596
|
-
await harness.emit("agent_end");
|
|
5131
|
+
await harness.emit("agent_end", { messages: [{ role: "assistant", content: "completed" }] });
|
|
4597
5132
|
const final = await harness.query("turn.prompt_status", ids);
|
|
4598
5133
|
expect(final.result?.status).toBe("terminal_ok");
|
|
4599
5134
|
await harness.stop();
|
|
@@ -4688,7 +5223,7 @@ describe("accepted-control zero-execution bound (#4668)", () => {
|
|
|
4688
5223
|
// The consuming run ends. The consumed submission must terminalize
|
|
4689
5224
|
// WITH it (terminal_ok, never a fabricated prompt_deadline_exceeded):
|
|
4690
5225
|
// with the bug it would still be parked in pending as merely accepted.
|
|
4691
|
-
await handlers.get("agent_end")?.({}, ctx);
|
|
5226
|
+
await handlers.get("agent_end")?.({ messages: [{ role: "assistant", content: "completed" }] }, ctx);
|
|
4692
5227
|
const statusOf = async (ids: { commandId?: string; turnId?: string }, frameId: string) => {
|
|
4693
5228
|
transport.feed("conn-a", {
|
|
4694
5229
|
type: "query_request",
|
|
@@ -4766,7 +5301,7 @@ describe("accepted-control zero-execution bound (#4668)", () => {
|
|
|
4766
5301
|
expect(midRun.result?.status).not.toBe("terminal_ok");
|
|
4767
5302
|
expect(midRun.result?.status).not.toBe("failed");
|
|
4768
5303
|
// The in-flight run ends: the diverted correlation terminalizes with it.
|
|
4769
|
-
await harness.emit("agent_end");
|
|
5304
|
+
await harness.emit("agent_end", { messages: [{ role: "assistant", content: "completed" }] });
|
|
4770
5305
|
expect(await settledStatus(harness, "turn.prompt_status", idsRaced)).toMatchObject({
|
|
4771
5306
|
status: "terminal_ok",
|
|
4772
5307
|
});
|
|
@@ -4825,7 +5360,7 @@ describe("accepted-control zero-execution bound (#4668)", () => {
|
|
|
4825
5360
|
expect(queued.result?.status).not.toBe("failed");
|
|
4826
5361
|
// Real consumption re-leases and attaches to the in-flight run.
|
|
4827
5362
|
consumed?.({ startsOwnRun: false });
|
|
4828
|
-
await harness.emit("agent_end");
|
|
5363
|
+
await harness.emit("agent_end", { messages: [{ role: "assistant", content: "completed" }] });
|
|
4829
5364
|
expect(await settledStatus(harness, "turn.prompt_status", idsRaced)).toMatchObject({
|
|
4830
5365
|
status: "terminal_ok",
|
|
4831
5366
|
});
|
|
@@ -5265,6 +5800,7 @@ describe("accepted-control zero-execution bound (#4668)", () => {
|
|
|
5265
5800
|
const acceptedA = (await waitFrame("settle-early-a")) as { result?: { commandId?: string; turnId?: string } };
|
|
5266
5801
|
const idsA = { commandId: acceptedA.result?.commandId, turnId: acceptedA.result?.turnId };
|
|
5267
5802
|
const statusDeadline = Date.now() + 15_000;
|
|
5803
|
+
let settledStatus: string | undefined;
|
|
5268
5804
|
for (;;) {
|
|
5269
5805
|
transport.feed("conn-a", {
|
|
5270
5806
|
type: "query_request",
|
|
@@ -5276,10 +5812,12 @@ describe("accepted-control zero-execution bound (#4668)", () => {
|
|
|
5276
5812
|
result?: { status?: string };
|
|
5277
5813
|
};
|
|
5278
5814
|
transport.sent.splice(transport.sent.indexOf(statusFrame), 1);
|
|
5279
|
-
|
|
5815
|
+
settledStatus = statusFrame.result?.status;
|
|
5816
|
+
if (statusFrame.result?.status === "terminal_ok" || statusFrame.result?.status === "failed") break;
|
|
5280
5817
|
if (Date.now() > statusDeadline) throw new Error("settled prompt never reported terminal_ok");
|
|
5281
5818
|
await Bun.sleep(20);
|
|
5282
5819
|
}
|
|
5820
|
+
expect(settledStatus).toBe("failed");
|
|
5283
5821
|
// conn-b's prompt is accepted and hangs; its run then starts.
|
|
5284
5822
|
transport.feed("conn-b", {
|
|
5285
5823
|
type: "control_request",
|
|
@@ -5954,7 +6492,7 @@ test("SDK-only host lets every connection whose follow-up was promoted abort the
|
|
|
5954
6492
|
Promise.resolve(options?.onPreflightAcceptCommit?.()).then(() => {
|
|
5955
6493
|
if (options?.onQueuedPromoted) promoted.push(options.onQueuedPromoted);
|
|
5956
6494
|
options?.onPreflightAccepted?.();
|
|
5957
|
-
return
|
|
6495
|
+
return "completed";
|
|
5958
6496
|
}),
|
|
5959
6497
|
} as unknown as ExtensionAPI;
|
|
5960
6498
|
const transport = memoryTransport();
|
|
@@ -6042,7 +6580,7 @@ test("SDK-only host lets every connection whose follow-up was promoted abort the
|
|
|
6042
6580
|
// invocation left the remaining follow-ups durably accepted — their
|
|
6043
6581
|
// result lookups would never complete and a restart would report them
|
|
6044
6582
|
// failed even though they ran in the shared turn.
|
|
6045
|
-
await handlers.get("agent_end")?.({ type: "agent_end" }, ctx);
|
|
6583
|
+
await handlers.get("agent_end")?.({ type: "agent_end", stopReason: "cancelled", messages: [] }, ctx);
|
|
6046
6584
|
{
|
|
6047
6585
|
const terminalDeadline = Date.now() + 15_000;
|
|
6048
6586
|
const promptStatuses = () =>
|