@gajae-code/coding-agent 0.15.5 → 0.15.6
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 +26 -1
- package/dist/types/async/job-manager.d.ts +83 -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/update-cli.d.ts +2 -0
- package/dist/types/commands/auth-gateway.d.ts +3 -0
- package/dist/types/commands/gc.d.ts +13 -0
- package/dist/types/commands/model-presets.d.ts +21 -0
- package/dist/types/config/autorouting-tier-map.d.ts +96 -0
- package/dist/types/config/file-lock.d.ts +49 -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 +11 -5
- package/dist/types/config/settings-schema.d.ts +9 -0
- package/dist/types/coordinator-mcp/model-preset.d.ts +7 -5
- package/dist/types/coordinator-mcp/projection-scan.d.ts +46 -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/session-state-lock.d.ts +35 -6
- package/dist/types/gjc-runtime/session-state-sidecar.d.ts +6 -0
- package/dist/types/index.d.ts +1 -0
- package/dist/types/main.d.ts +3 -1
- package/dist/types/modes/components/jobs-overlay-model.d.ts +4 -2
- package/dist/types/modes/jobs-observer.d.ts +36 -0
- package/dist/types/sdk/broker/broker.d.ts +3 -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/client/discovery.d.ts +1 -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 +15 -1
- 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 +9 -0
- package/dist/types/session/account-inventory.d.ts +1 -0
- package/dist/types/session/agent-session.d.ts +19 -7
- 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/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/index.d.ts +6 -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 +3 -3
- package/src/async/job-manager.ts +671 -39
- 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 +3 -0
- package/src/cli/model-presets-cli.ts +100 -0
- package/src/cli/update-cli.ts +40 -5
- package/src/cli.ts +6 -2
- package/src/commands/auth-gateway.ts +12 -8
- package/src/commands/gc.ts +19 -0
- package/src/commands/model-presets.ts +24 -0
- package/src/config/autorouting-tier-map.ts +32 -0
- package/src/config/file-lock-gc.ts +9 -4
- package/src/config/file-lock.ts +775 -59
- 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 +2799 -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 +51 -9
- package/src/config/model-registry.ts +1021 -122
- package/src/config/settings-schema.ts +11 -0
- package/src/config/settings.ts +6 -1
- package/src/coordinator-mcp/model-preset.ts +32 -17
- package/src/coordinator-mcp/projection-scan.ts +571 -0
- package/src/coordinator-mcp/server.ts +19 -24
- package/src/coordinator-mcp/session-reaper.ts +9 -3
- 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/session-state-lock.ts +1242 -141
- package/src/gjc-runtime/session-state-sidecar.ts +76 -3
- package/src/index.ts +1 -0
- package/src/internal-urls/docs-index.generated.ts +9 -8
- package/src/lsp/config.ts +4 -3
- package/src/lsp/path-trust.ts +12 -6
- package/src/main.ts +9 -2
- package/src/modes/acp/acp-agent.ts +11 -5
- package/src/modes/components/jobs-overlay-model.ts +99 -16
- package/src/modes/components/model-selector.ts +158 -18
- package/src/modes/components/status-line/segments.ts +6 -1
- package/src/modes/controllers/extension-ui-controller.ts +1 -1
- package/src/modes/controllers/input-controller.ts +23 -6
- package/src/modes/interactive-mode.ts +14 -1
- package/src/modes/jobs-observer.ts +149 -16
- package/src/modes/runtime-init.ts +1 -1
- package/src/modes/utils/injected-user-submission.ts +15 -1
- 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 +48 -21
- package/src/sdk/broker/lifecycle.ts +32 -6
- package/src/sdk/bus/chat-daemon-control.ts +2 -2
- package/src/sdk/bus/chat-daemon-runtime.ts +11 -1
- 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/cli/session-cli.ts +12 -1
- package/src/sdk/client/discovery.ts +3 -1
- package/src/sdk/host/control/dispatch.ts +3 -0
- 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 +371 -37
- package/src/sdk/host/session-runtime.ts +364 -52
- 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 +2 -2
- package/src/sdk/protocol/operation-registry.ts +4 -1
- package/src/sdk/router/session-router.ts +228 -96
- package/src/sdk/session.ts +83 -12
- package/src/session/account-inventory.ts +27 -11
- package/src/session/agent-session.ts +678 -103
- package/src/session/fold-coordinator.ts +433 -0
- 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 +5 -4
- 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 +142 -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 +772 -67
- package/src/tools/index.ts +7 -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
|
@@ -13,6 +13,8 @@ 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
19
|
import { CursorRegistry, QueryHandlers, RevisionStore } from "./query";
|
|
18
20
|
import {
|
|
@@ -146,6 +148,76 @@ async function queryGoalState(
|
|
|
146
148
|
return response.page?.items[0];
|
|
147
149
|
}
|
|
148
150
|
|
|
151
|
+
test("native prompt reconciliation fails closed for an explicitly empty assistant result", () => {
|
|
152
|
+
const reconciliation = createPromptReconciliation();
|
|
153
|
+
const correlation = { commandId: "native-empty-command", turnId: "native-empty-turn" };
|
|
154
|
+
reconciliation.admit("native-empty-ref");
|
|
155
|
+
reconciliation.noteAccepted(correlation, "native-empty-ref");
|
|
156
|
+
reconciliation.noteTransition(correlation, { type: "agent_end", finalText: "" });
|
|
157
|
+
expect(reconciliation.lookup({ clientRef: "native-empty-ref" })).toMatchObject({
|
|
158
|
+
status: "failed",
|
|
159
|
+
error: { code: "prompt_failed" },
|
|
160
|
+
receiptState: "missing",
|
|
161
|
+
});
|
|
162
|
+
reconciliation.noteTransition(correlation, { type: "agent_end", finalText: "late text" });
|
|
163
|
+
expect(reconciliation.lookup({ clientRef: "native-empty-ref" })).toMatchObject({ status: "failed" });
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
test("broker reconciliation fails closed for an empty prompt and persists the first terminal result", async () => {
|
|
167
|
+
let records: unknown[] = [];
|
|
168
|
+
const store = {
|
|
169
|
+
load: async () => records,
|
|
170
|
+
transact: async (mutator: (current: never[]) => never[]) => {
|
|
171
|
+
records = mutator(records as never);
|
|
172
|
+
},
|
|
173
|
+
} as never;
|
|
174
|
+
const reconciliation = createKindAwareReconciliation({ store });
|
|
175
|
+
const correlation = { commandId: "broker-empty-command", turnId: "broker-empty-turn" };
|
|
176
|
+
await reconciliation.noteAccepted("prompt", correlation, "broker-empty-ref");
|
|
177
|
+
await reconciliation.noteTransition("prompt", correlation, {
|
|
178
|
+
type: "agent_end",
|
|
179
|
+
content: { version: 1, type: "text", text: "", byteLength: 0, truncated: false },
|
|
180
|
+
});
|
|
181
|
+
const settled = reconciliation.lookupResult("prompt", { clientRef: "broker-empty-ref" });
|
|
182
|
+
expect(settled).toMatchObject({ status: "failed", error: { code: "prompt_failed" } });
|
|
183
|
+
await reconciliation.noteTransition("prompt", correlation, {
|
|
184
|
+
type: "agent_end",
|
|
185
|
+
content: { version: 1, type: "text", text: "late text", byteLength: 9, truncated: false },
|
|
186
|
+
});
|
|
187
|
+
const reloaded = createKindAwareReconciliation({ store });
|
|
188
|
+
await reloaded.hydrateFromStore();
|
|
189
|
+
expect(reloaded.lookupResult("prompt", { clientRef: "broker-empty-ref" })).toMatchObject({
|
|
190
|
+
status: "failed",
|
|
191
|
+
error: { code: "prompt_failed" },
|
|
192
|
+
});
|
|
193
|
+
const noOpCorrelation = { commandId: "broker-noop-command", turnId: "broker-noop-turn" };
|
|
194
|
+
await reconciliation.noteAccepted("prompt", noOpCorrelation, "broker-noop-ref");
|
|
195
|
+
await reconciliation.finalizeOutcome(
|
|
196
|
+
"prompt",
|
|
197
|
+
noOpCorrelation,
|
|
198
|
+
{ kind: "stopped", reason: "cancelled", provenance: "client_cancel" },
|
|
199
|
+
undefined,
|
|
200
|
+
"",
|
|
201
|
+
);
|
|
202
|
+
expect(reconciliation.lookupResult("prompt", { clientRef: "broker-noop-ref" })).toMatchObject({
|
|
203
|
+
status: "terminal_ok",
|
|
204
|
+
});
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
test("session-host prompt reconciliation fails closed when the accepted result is empty", async () => {
|
|
208
|
+
const reconciliation = createInvocationReconciliation();
|
|
209
|
+
const correlation = { commandId: "host-empty-command", turnId: "host-empty-turn" };
|
|
210
|
+
await reconciliation.noteAccepted("prompt", correlation, "host-empty-ref");
|
|
211
|
+
await reconciliation.noteTransition("prompt", correlation, {
|
|
212
|
+
type: "agent_end",
|
|
213
|
+
content: { version: 1, type: "text", text: "", byteLength: 0, truncated: false },
|
|
214
|
+
});
|
|
215
|
+
expect(reconciliation.lookup("prompt", { clientRef: "host-empty-ref" })).toMatchObject({
|
|
216
|
+
status: "failed",
|
|
217
|
+
error: { code: "prompt_failed" },
|
|
218
|
+
});
|
|
219
|
+
});
|
|
220
|
+
|
|
149
221
|
describe("SDK goal snapshot lifecycle", () => {
|
|
150
222
|
test("recovers an active nonzero-activity goal from the durable session projection", async () => {
|
|
151
223
|
const cwd = await mkdtemp(path.join(os.tmpdir(), "gjc-sdk-goal-recovery-"));
|
|
@@ -405,7 +477,10 @@ test("durable reload keeps agent_end terminal across a paused successor transiti
|
|
|
405
477
|
const correlation = { commandId: "durable-race-command", turnId: "durable-race-turn" };
|
|
406
478
|
await reconciliation.noteAccepted("prompt", correlation, "durable-race-ref");
|
|
407
479
|
pauseWrites = 2;
|
|
408
|
-
const terminal = reconciliation.noteTransition("prompt", correlation, {
|
|
480
|
+
const terminal = reconciliation.noteTransition("prompt", correlation, {
|
|
481
|
+
type: "agent_end",
|
|
482
|
+
content: { version: 1, type: "text", text: "completed", byteLength: 9, truncated: false },
|
|
483
|
+
});
|
|
409
484
|
await writeStarted[0]!.promise;
|
|
410
485
|
// A successor lifecycle event arrives while the terminal write is held. It
|
|
411
486
|
// must observe the staged terminal record and never resurrect in_flight state.
|
|
@@ -455,7 +530,10 @@ test("agent_end is not swallowed when deadline persistence fails", async () => {
|
|
|
455
530
|
message: "deadline",
|
|
456
531
|
});
|
|
457
532
|
await persistStarted.promise;
|
|
458
|
-
const terminal = reconciliation.noteTransition("prompt", correlation, {
|
|
533
|
+
const terminal = reconciliation.noteTransition("prompt", correlation, {
|
|
534
|
+
type: "agent_end",
|
|
535
|
+
content: { version: 1, type: "text", text: "completed", byteLength: 9, truncated: false },
|
|
536
|
+
});
|
|
459
537
|
releasePersist.resolve();
|
|
460
538
|
await expect(deadline).rejects.toThrow("held store failed");
|
|
461
539
|
await terminal;
|
|
@@ -523,13 +601,86 @@ test("agent_end upgrades a durable deadline terminal when it races a successful
|
|
|
523
601
|
code: "prompt_deadline_exceeded",
|
|
524
602
|
message: "deadline",
|
|
525
603
|
});
|
|
526
|
-
await reconciliation.noteTransition("prompt", correlation, {
|
|
604
|
+
await reconciliation.noteTransition("prompt", correlation, {
|
|
605
|
+
type: "agent_end",
|
|
606
|
+
content: { version: 1, type: "text", text: "completed", byteLength: 9, truncated: false },
|
|
607
|
+
});
|
|
527
608
|
const reloaded = createInvocationReconciliation({ store });
|
|
528
609
|
await reloaded.hydrate();
|
|
529
610
|
expect(reloaded.lookup("prompt", { clientRef: "upgrade-race-ref" })).toMatchObject({ status: "terminal_ok" });
|
|
530
611
|
});
|
|
531
612
|
|
|
532
|
-
test("
|
|
613
|
+
test("an empty late agent_end cannot upgrade a durable prompt deadline failure", async () => {
|
|
614
|
+
let records: unknown[] = [];
|
|
615
|
+
const store = {
|
|
616
|
+
path: null,
|
|
617
|
+
load: async () => records,
|
|
618
|
+
transact: async (mutator: (current: never[]) => never[]) => {
|
|
619
|
+
records = mutator(records as never);
|
|
620
|
+
},
|
|
621
|
+
} as never;
|
|
622
|
+
const reconciliation = createInvocationReconciliation({ store });
|
|
623
|
+
const correlation = { commandId: "empty-deadline-command", turnId: "empty-deadline-turn" };
|
|
624
|
+
await reconciliation.noteAccepted("prompt", correlation, "empty-deadline-ref");
|
|
625
|
+
await reconciliation.finalizeOutcome("prompt", correlation, {
|
|
626
|
+
kind: "failed",
|
|
627
|
+
code: "prompt_deadline_exceeded",
|
|
628
|
+
message: "deadline",
|
|
629
|
+
});
|
|
630
|
+
await reconciliation.noteTransition("prompt", correlation, {
|
|
631
|
+
type: "agent_end",
|
|
632
|
+
content: { version: 1, type: "text", text: "", byteLength: 0, truncated: false },
|
|
633
|
+
});
|
|
634
|
+
expect(reconciliation.lookup("prompt", { clientRef: "empty-deadline-ref" })).toMatchObject({
|
|
635
|
+
status: "failed",
|
|
636
|
+
error: { code: "prompt_deadline_exceeded" },
|
|
637
|
+
});
|
|
638
|
+
const reloaded = createInvocationReconciliation({ store });
|
|
639
|
+
await reloaded.hydrate();
|
|
640
|
+
expect(reloaded.lookup("prompt", { clientRef: "empty-deadline-ref" })).toMatchObject({
|
|
641
|
+
status: "failed",
|
|
642
|
+
error: { code: "prompt_deadline_exceeded" },
|
|
643
|
+
});
|
|
644
|
+
});
|
|
645
|
+
|
|
646
|
+
test("whitespace and provider failures stay actionable across a late empty agent_end", async () => {
|
|
647
|
+
let records: unknown[] = [];
|
|
648
|
+
const store = {
|
|
649
|
+
path: null,
|
|
650
|
+
load: async () => records,
|
|
651
|
+
transact: async (mutator: (current: never[]) => never[]) => {
|
|
652
|
+
records = mutator(records as never);
|
|
653
|
+
},
|
|
654
|
+
} as never;
|
|
655
|
+
const reconciliation = createInvocationReconciliation({ store });
|
|
656
|
+
const whitespace = { commandId: "whitespace-command", turnId: "whitespace-turn" };
|
|
657
|
+
await reconciliation.noteAccepted("prompt", whitespace, "whitespace-ref");
|
|
658
|
+
await reconciliation.noteTransition("prompt", whitespace, {
|
|
659
|
+
type: "agent_end",
|
|
660
|
+
content: { version: 1, type: "text", text: " \t\n ", byteLength: 4, truncated: false },
|
|
661
|
+
});
|
|
662
|
+
expect(reconciliation.lookup("prompt", { clientRef: "whitespace-ref" })).toMatchObject({
|
|
663
|
+
status: "failed",
|
|
664
|
+
error: { code: "prompt_failed", message: "Prompt submission failed." },
|
|
665
|
+
});
|
|
666
|
+
|
|
667
|
+
const providerFailure = { commandId: "provider-command", turnId: "provider-turn" };
|
|
668
|
+
await reconciliation.noteAccepted("prompt", providerFailure, "provider-ref");
|
|
669
|
+
await reconciliation.noteTransition("prompt", providerFailure, {
|
|
670
|
+
type: "agent_failed",
|
|
671
|
+
error: Object.assign(new Error("provider leaked detail"), { code: "provider_unavailable" }),
|
|
672
|
+
});
|
|
673
|
+
await reconciliation.noteTransition("prompt", providerFailure, {
|
|
674
|
+
type: "agent_end",
|
|
675
|
+
content: { version: 1, type: "text", text: "", byteLength: 0, truncated: false },
|
|
676
|
+
});
|
|
677
|
+
expect(reconciliation.lookup("prompt", { clientRef: "provider-ref" })).toMatchObject({
|
|
678
|
+
status: "failed",
|
|
679
|
+
error: { code: "provider_unavailable", message: "Prompt submission failed." },
|
|
680
|
+
});
|
|
681
|
+
});
|
|
682
|
+
|
|
683
|
+
test("an empty prompt terminal remains failed after a late real failure", async () => {
|
|
533
684
|
let records: unknown[] = [];
|
|
534
685
|
let writes = 0;
|
|
535
686
|
const store = {
|
|
@@ -550,9 +701,12 @@ test("failed agent_end upgrade persistence retains the deadline record for retry
|
|
|
550
701
|
code: "prompt_deadline_exceeded",
|
|
551
702
|
message: "deadline",
|
|
552
703
|
});
|
|
553
|
-
await expect(
|
|
554
|
-
"
|
|
555
|
-
|
|
704
|
+
await expect(
|
|
705
|
+
reconciliation.noteTransition("prompt", correlation, {
|
|
706
|
+
type: "agent_end",
|
|
707
|
+
content: { version: 1, type: "text", text: "completed", byteLength: 9, truncated: false },
|
|
708
|
+
}),
|
|
709
|
+
).rejects.toThrow("upgrade persist failed");
|
|
556
710
|
expect(reconciliation.lookup("prompt", { clientRef: "upgrade-failure-ref" })).toMatchObject({
|
|
557
711
|
status: "failed",
|
|
558
712
|
error: { code: "prompt_deadline_exceeded" },
|
|
@@ -563,17 +717,132 @@ test("failed agent_end upgrade persistence retains the deadline record for retry
|
|
|
563
717
|
status: "failed",
|
|
564
718
|
error: { code: "prompt_deadline_exceeded" },
|
|
565
719
|
});
|
|
566
|
-
await reconciliation.noteTransition("prompt", correlation, {
|
|
720
|
+
await reconciliation.noteTransition("prompt", correlation, {
|
|
721
|
+
type: "agent_end",
|
|
722
|
+
content: { version: 1, type: "text", text: "completed", byteLength: 9, truncated: false },
|
|
723
|
+
});
|
|
567
724
|
const reloaded = createInvocationReconciliation({ store });
|
|
568
725
|
await reloaded.hydrate();
|
|
569
726
|
expect(reloaded.lookup("prompt", { clientRef: "upgrade-failure-ref" })).toMatchObject({ status: "terminal_ok" });
|
|
570
727
|
});
|
|
571
728
|
|
|
729
|
+
test("late cancellation and non-text activity upgrade deadline terminals", async () => {
|
|
730
|
+
const makeStore = () => {
|
|
731
|
+
let records: unknown[] = [];
|
|
732
|
+
return {
|
|
733
|
+
path: null,
|
|
734
|
+
load: async () => records,
|
|
735
|
+
transact: async (mutator: (current: never[]) => never[]) => {
|
|
736
|
+
records = mutator(records as never);
|
|
737
|
+
},
|
|
738
|
+
} as never;
|
|
739
|
+
};
|
|
740
|
+
const cancelled = createInvocationReconciliation({ store: makeStore() });
|
|
741
|
+
const cancelledCorrelation = { commandId: "late-cancel-command", turnId: "late-cancel-turn" };
|
|
742
|
+
await cancelled.noteAccepted("prompt", cancelledCorrelation, "late-cancel-ref");
|
|
743
|
+
await cancelled.finalizeOutcome("prompt", cancelledCorrelation, {
|
|
744
|
+
kind: "failed",
|
|
745
|
+
code: "prompt_deadline_exceeded",
|
|
746
|
+
message: "deadline",
|
|
747
|
+
});
|
|
748
|
+
await cancelled.noteTransition("prompt", cancelledCorrelation, {
|
|
749
|
+
type: "agent_end",
|
|
750
|
+
outcome: { kind: "stopped", reason: "cancelled", provenance: "client_cancel" },
|
|
751
|
+
});
|
|
752
|
+
expect(cancelled.lookup("prompt", { clientRef: "late-cancel-ref" })).toMatchObject({ status: "terminal_ok" });
|
|
753
|
+
|
|
754
|
+
const activity = createInvocationReconciliation({ store: makeStore() });
|
|
755
|
+
const activityCorrelation = { commandId: "late-activity-command", turnId: "late-activity-turn" };
|
|
756
|
+
await activity.noteAccepted("prompt", activityCorrelation, "late-activity-ref");
|
|
757
|
+
await activity.finalizeOutcome("prompt", activityCorrelation, {
|
|
758
|
+
kind: "failed",
|
|
759
|
+
code: "prompt_deadline_exceeded",
|
|
760
|
+
message: "deadline",
|
|
761
|
+
});
|
|
762
|
+
await activity.noteTransition("prompt", activityCorrelation, { type: "agent_end", hasActivity: true });
|
|
763
|
+
expect(activity.lookup("prompt", { clientRef: "late-activity-ref" })).toMatchObject({ status: "terminal_ok" });
|
|
764
|
+
});
|
|
765
|
+
test("finalization without an explicit outcome follows the noteTransition evidence predicate", async () => {
|
|
766
|
+
const makeLocalStore = () => {
|
|
767
|
+
let records: unknown[] = [];
|
|
768
|
+
return {
|
|
769
|
+
path: null,
|
|
770
|
+
load: async () => records,
|
|
771
|
+
transact: async (mutator: (current: never[]) => never[]) => {
|
|
772
|
+
records = mutator(records as never);
|
|
773
|
+
},
|
|
774
|
+
} as never;
|
|
775
|
+
};
|
|
776
|
+
const contentBearing = createInvocationReconciliation({ store: makeLocalStore() });
|
|
777
|
+
const contentCorrelation = { commandId: "final-content-command", turnId: "final-content-turn" };
|
|
778
|
+
await contentBearing.noteAccepted("prompt", contentCorrelation, "final-content-ref");
|
|
779
|
+
await contentBearing.finalizeOutcome("prompt", contentCorrelation, undefined, undefined, undefined, {
|
|
780
|
+
content: { version: 1, type: "text", text: "completed", byteLength: 9, truncated: false },
|
|
781
|
+
});
|
|
782
|
+
expect(contentBearing.lookup("prompt", { clientRef: "final-content-ref" })).toMatchObject({
|
|
783
|
+
status: "terminal_ok",
|
|
784
|
+
});
|
|
785
|
+
|
|
786
|
+
const activityBearing = createInvocationReconciliation({ store: makeLocalStore() });
|
|
787
|
+
const activityCorrelation = { commandId: "final-activity-command", turnId: "final-activity-turn" };
|
|
788
|
+
await activityBearing.noteAccepted("prompt", activityCorrelation, "final-activity-ref");
|
|
789
|
+
await activityBearing.finalizeOutcome("prompt", activityCorrelation, undefined, undefined, undefined, {
|
|
790
|
+
hasActivity: true,
|
|
791
|
+
});
|
|
792
|
+
expect(activityBearing.lookup("prompt", { clientRef: "final-activity-ref" })).toMatchObject({
|
|
793
|
+
status: "terminal_ok",
|
|
794
|
+
});
|
|
795
|
+
|
|
796
|
+
const stoppedOutcome = createInvocationReconciliation({ store: makeLocalStore() });
|
|
797
|
+
const stoppedCorrelation = { commandId: "final-stopped-command", turnId: "final-stopped-turn" };
|
|
798
|
+
await stoppedOutcome.noteAccepted("prompt", stoppedCorrelation, "final-stopped-ref");
|
|
799
|
+
await stoppedOutcome.finalizeOutcome("prompt", stoppedCorrelation, undefined, undefined, undefined, {
|
|
800
|
+
outcomeKind: "stopped",
|
|
801
|
+
});
|
|
802
|
+
expect(stoppedOutcome.lookup("prompt", { clientRef: "final-stopped-ref" })).toMatchObject({
|
|
803
|
+
status: "terminal_ok",
|
|
804
|
+
});
|
|
805
|
+
|
|
806
|
+
const evidenceFree = createInvocationReconciliation({ store: makeLocalStore() });
|
|
807
|
+
const emptyCorrelation = { commandId: "final-empty-command", turnId: "final-empty-turn" };
|
|
808
|
+
await evidenceFree.noteAccepted("prompt", emptyCorrelation, "final-empty-ref");
|
|
809
|
+
await evidenceFree.finalizeOutcome("prompt", emptyCorrelation);
|
|
810
|
+
expect(evidenceFree.lookup("prompt", { clientRef: "final-empty-ref" })).toMatchObject({
|
|
811
|
+
status: "failed",
|
|
812
|
+
error: { code: "prompt_failed" },
|
|
813
|
+
});
|
|
814
|
+
const legacyArg5 = createInvocationReconciliation({ store: makeLocalStore() });
|
|
815
|
+
const legacyCorrelation = { commandId: "final-legacy-command", turnId: "final-legacy-turn" };
|
|
816
|
+
await legacyArg5.noteAccepted("prompt", legacyCorrelation, "final-legacy-ref");
|
|
817
|
+
await legacyArg5.finalizeOutcome("prompt", legacyCorrelation, undefined, undefined, {
|
|
818
|
+
content: { version: 1, type: "text", text: "legacy", byteLength: 6, truncated: false },
|
|
819
|
+
});
|
|
820
|
+
expect(legacyArg5.lookup("prompt", { clientRef: "final-legacy-ref" })).toMatchObject({
|
|
821
|
+
status: "terminal_ok",
|
|
822
|
+
});
|
|
823
|
+
|
|
824
|
+
const explicitFailure = createInvocationReconciliation({ store: makeLocalStore() });
|
|
825
|
+
const failureCorrelation = { commandId: "final-failure-command", turnId: "final-failure-turn" };
|
|
826
|
+
await explicitFailure.noteAccepted("prompt", failureCorrelation, "final-failure-ref");
|
|
827
|
+
await explicitFailure.finalizeOutcome("prompt", failureCorrelation, {
|
|
828
|
+
kind: "failed",
|
|
829
|
+
code: "provider_rejected",
|
|
830
|
+
message: "provider rejected",
|
|
831
|
+
});
|
|
832
|
+
expect(explicitFailure.lookup("prompt", { clientRef: "final-failure-ref" })).toMatchObject({
|
|
833
|
+
status: "failed",
|
|
834
|
+
error: { code: "provider_rejected" },
|
|
835
|
+
});
|
|
836
|
+
});
|
|
837
|
+
|
|
572
838
|
test("a reason attached after a prompt settled is never replaced by a later failure", async () => {
|
|
573
839
|
const reconciliation = createInvocationReconciliation();
|
|
574
840
|
const correlation = { commandId: "late-reason-command", turnId: "late-reason-turn" };
|
|
575
841
|
await reconciliation.noteAccepted("prompt", correlation, "late-reason-ref");
|
|
576
|
-
await reconciliation.noteTransition("prompt", correlation, {
|
|
842
|
+
await reconciliation.noteTransition("prompt", correlation, {
|
|
843
|
+
type: "agent_end",
|
|
844
|
+
content: { version: 1, type: "text", text: "completed", byteLength: 9, truncated: false },
|
|
845
|
+
});
|
|
577
846
|
const claimed = reconciliation.lookup("prompt", { clientRef: "late-reason-ref" }) as Record<string, unknown>;
|
|
578
847
|
expect(claimed).toEqual({
|
|
579
848
|
status: "terminal_ok",
|
|
@@ -649,7 +918,10 @@ test("retains terminal host reconciliation records past the 15-minute window", a
|
|
|
649
918
|
reconciliation.admit("skill", "aged-skill-ref");
|
|
650
919
|
await reconciliation.noteAccepted("prompt", promptCorrelation, "aged-ref");
|
|
651
920
|
await reconciliation.noteAccepted("skill", skillCorrelation, "aged-skill-ref");
|
|
652
|
-
await reconciliation.noteTransition("prompt", promptCorrelation, {
|
|
921
|
+
await reconciliation.noteTransition("prompt", promptCorrelation, {
|
|
922
|
+
type: "agent_end",
|
|
923
|
+
content: { version: 1, type: "text", text: "completed", byteLength: 9, truncated: false },
|
|
924
|
+
});
|
|
653
925
|
await reconciliation.noteTransition("skill", skillCorrelation, { type: "agent_end" });
|
|
654
926
|
|
|
655
927
|
clock += 15 * 60_000 + 1;
|
|
@@ -679,7 +951,10 @@ test("capacity eviction still releases the clientRef and reports honest unknown"
|
|
|
679
951
|
const correlation = { commandId: `capacity-command-${index}`, turnId: `capacity-turn-${index}` };
|
|
680
952
|
reconciliation.admit("prompt", `capacity-ref-${index}`);
|
|
681
953
|
await reconciliation.noteAccepted("prompt", correlation, `capacity-ref-${index}`);
|
|
682
|
-
await reconciliation.noteTransition("prompt", correlation, {
|
|
954
|
+
await reconciliation.noteTransition("prompt", correlation, {
|
|
955
|
+
type: "agent_end",
|
|
956
|
+
content: { version: 1, type: "text", text: "completed", byteLength: 9, truncated: false },
|
|
957
|
+
});
|
|
683
958
|
}
|
|
684
959
|
expect(reconciliation.lookup("prompt", { clientRef: "capacity-ref-1" })).toEqual({
|
|
685
960
|
status: "unknown",
|
|
@@ -1870,10 +2145,10 @@ describe("SessionSdkSessionRuntime", () => {
|
|
|
1870
2145
|
prompt("conn-b", "stale-b");
|
|
1871
2146
|
await waitResponse("stale-b");
|
|
1872
2147
|
// A later AGENT-INITIATED turn starts: the pending queue is empty, so
|
|
1873
|
-
// the
|
|
2148
|
+
// the predecessor's conn-a owner is cleared and no SDK client owns it.
|
|
1874
2149
|
await handlers.get("agent_start")?.({ type: "agent_start" }, ctx);
|
|
1875
|
-
//
|
|
1876
|
-
transport.feed("conn-
|
|
2150
|
+
// A's stale owner must NOT stop the agent-initiated turn.
|
|
2151
|
+
transport.feed("conn-a", {
|
|
1877
2152
|
type: "control_request",
|
|
1878
2153
|
id: "stale-owner-abort",
|
|
1879
2154
|
operation: "turn.abort",
|
|
@@ -1987,6 +2262,40 @@ describe("SessionSdkSessionRuntime", () => {
|
|
|
1987
2262
|
}
|
|
1988
2263
|
});
|
|
1989
2264
|
|
|
2265
|
+
test("agent-initiated successor activity cannot renew a stale predecessor deadline", async () => {
|
|
2266
|
+
const cwd = await mkdtemp(path.join(os.tmpdir(), "gjc-sdk-stale-deadline-owner-"));
|
|
2267
|
+
try {
|
|
2268
|
+
const harness = await invocationHarness("stale-deadline-owner", cwd, {
|
|
2269
|
+
settings: {
|
|
2270
|
+
get: (key: string) =>
|
|
2271
|
+
key === "sdk.promptDeadlineMs" ? 25 : key === "sdk.promptMaxRuntimeMs" ? 60_000 : undefined,
|
|
2272
|
+
} as unknown as Settings,
|
|
2273
|
+
sendUserMessage: async (_content, options) => {
|
|
2274
|
+
await options?.onPreflightAcceptCommit?.();
|
|
2275
|
+
await neverSettlingPromise();
|
|
2276
|
+
},
|
|
2277
|
+
});
|
|
2278
|
+
const prompt = await harness.control("turn.prompt", { text: "predecessor" });
|
|
2279
|
+
expect(prompt.ok).toBe(true);
|
|
2280
|
+
const ids = { commandId: prompt.result?.commandId, turnId: prompt.result?.turnId };
|
|
2281
|
+
await harness.emit("agent_start");
|
|
2282
|
+
// Empty drain represents an agent-initiated successor with no SDK owner.
|
|
2283
|
+
await harness.emit("agent_start");
|
|
2284
|
+
for (let i = 0; i < 3; i += 1) {
|
|
2285
|
+
await harness.emit("tool_execution_start");
|
|
2286
|
+
await Bun.sleep(15);
|
|
2287
|
+
}
|
|
2288
|
+
expect(await settledStatus(harness, "turn.prompt_status", ids)).toMatchObject({
|
|
2289
|
+
status: "failed",
|
|
2290
|
+
error: { code: "prompt_deadline_exceeded" },
|
|
2291
|
+
});
|
|
2292
|
+
await harness.stop();
|
|
2293
|
+
} finally {
|
|
2294
|
+
await Bun.sleep(50);
|
|
2295
|
+
await rm(cwd, { recursive: true, force: true });
|
|
2296
|
+
}
|
|
2297
|
+
});
|
|
2298
|
+
|
|
1990
2299
|
test("SDK-only host retires an accepted-then-failed submission so a later agent-initiated turn is not mis-owned", async () => {
|
|
1991
2300
|
// An idle submission is accepted (pending entry pushed) and then REJECTS
|
|
1992
2301
|
// before agent_start (e.g. the busy retry expires). The failed entry must
|
|
@@ -2985,7 +3294,7 @@ async function invocationHarness(
|
|
|
2985
3294
|
sessionId: string,
|
|
2986
3295
|
cwd: string,
|
|
2987
3296
|
hooks: {
|
|
2988
|
-
sendUserMessage?: (content: unknown, options?: PreflightHooks & { deliverAs?: string }) => Promise<
|
|
3297
|
+
sendUserMessage?: (content: unknown, options?: PreflightHooks & { deliverAs?: string }) => Promise<unknown>;
|
|
2989
3298
|
invokeSkill?: (name: string, args?: string, options?: PreflightHooks) => Promise<unknown>;
|
|
2990
3299
|
abort?: () => void;
|
|
2991
3300
|
isIdle?: () => boolean;
|
|
@@ -3010,7 +3319,10 @@ async function invocationHarness(
|
|
|
3010
3319
|
on(event: string, handler: (event: unknown, ctx: unknown) => Promise<void> | void) {
|
|
3011
3320
|
handlers.set(event, handler);
|
|
3012
3321
|
},
|
|
3013
|
-
sendUserMessage:
|
|
3322
|
+
sendUserMessage: async (content: unknown, options?: PreflightHooks & { deliverAs?: string }) => {
|
|
3323
|
+
const result = await hooks.sendUserMessage?.(content, options);
|
|
3324
|
+
return result === undefined ? "completed" : result;
|
|
3325
|
+
},
|
|
3014
3326
|
} as unknown as ExtensionAPI;
|
|
3015
3327
|
const interceptorStore = hooks.persistInterceptor
|
|
3016
3328
|
? createInterceptorReconciliationStore(
|
|
@@ -3202,13 +3514,19 @@ async function settledStatus(
|
|
|
3202
3514
|
name: string,
|
|
3203
3515
|
input: Record<string, unknown>,
|
|
3204
3516
|
): Promise<NonNullable<ResponseFrame["result"]>> {
|
|
3205
|
-
|
|
3517
|
+
// Wall-clock budget instead of a fixed poll count: CI runners can starve the
|
|
3518
|
+
// event loop long enough to exhaust 200 ~1ms polls before a 25ms deadline
|
|
3519
|
+
// timer is dispatched, failing a correct implementation on timing alone.
|
|
3520
|
+
// The contract is unchanged — the status must still reach a terminal
|
|
3521
|
+
// reconciliation state with the asserted shape within a bounded horizon.
|
|
3522
|
+
const budgetEndsAt = Date.now() + 10_000;
|
|
3523
|
+
for (;;) {
|
|
3206
3524
|
const frame = await harness.query(name, input);
|
|
3207
3525
|
const result = frame.result;
|
|
3208
3526
|
if (result && (result.status === "failed" || result.status === "terminal_ok")) return result;
|
|
3527
|
+
if (Date.now() > budgetEndsAt) throw new Error(`${name} never reported a terminal reconciliation status`);
|
|
3209
3528
|
await Bun.sleep(1);
|
|
3210
3529
|
}
|
|
3211
|
-
throw new Error(`${name} never reported a terminal reconciliation status`);
|
|
3212
3530
|
}
|
|
3213
3531
|
|
|
3214
3532
|
function neverSettlingPromise(): Promise<void> {
|
|
@@ -3429,7 +3747,10 @@ describe("post-acceptance invocation terminalization", () => {
|
|
|
3429
3747
|
},
|
|
3430
3748
|
],
|
|
3431
3749
|
});
|
|
3432
|
-
expect(await settledStatus(harness, "turn.prompt_status", ids)).toMatchObject({
|
|
3750
|
+
expect(await settledStatus(harness, "turn.prompt_status", ids)).toMatchObject({
|
|
3751
|
+
status: "failed",
|
|
3752
|
+
error: { code: "prompt_failed" },
|
|
3753
|
+
});
|
|
3433
3754
|
await harness.stop();
|
|
3434
3755
|
} finally {
|
|
3435
3756
|
await Bun.sleep(50);
|
|
@@ -3460,7 +3781,10 @@ describe("post-acceptance invocation terminalization", () => {
|
|
|
3460
3781
|
},
|
|
3461
3782
|
],
|
|
3462
3783
|
});
|
|
3463
|
-
expect(await settledStatus(harness, "turn.prompt_status", ids)).toMatchObject({
|
|
3784
|
+
expect(await settledStatus(harness, "turn.prompt_status", ids)).toMatchObject({
|
|
3785
|
+
status: "failed",
|
|
3786
|
+
error: { code: "prompt_failed" },
|
|
3787
|
+
});
|
|
3464
3788
|
await harness.stop();
|
|
3465
3789
|
} finally {
|
|
3466
3790
|
await Bun.sleep(50);
|
|
@@ -3489,7 +3813,8 @@ describe("post-acceptance invocation terminalization", () => {
|
|
|
3489
3813
|
});
|
|
3490
3814
|
await harness.emit("agent_end", event);
|
|
3491
3815
|
expect(await settledStatus(harness, "turn.prompt_status", ids)).toMatchObject({
|
|
3492
|
-
status: "
|
|
3816
|
+
status: "failed",
|
|
3817
|
+
error: { code: "prompt_failed" },
|
|
3493
3818
|
});
|
|
3494
3819
|
await harness.stop();
|
|
3495
3820
|
} finally {
|
|
@@ -3530,7 +3855,9 @@ describe("post-acceptance invocation terminalization", () => {
|
|
|
3530
3855
|
error: { code: "provider_http_402" },
|
|
3531
3856
|
});
|
|
3532
3857
|
expect((await harness.query("turn.prompt_status", replacementIds)).result?.status).toBe("in_flight");
|
|
3533
|
-
await harness.emit("agent_end", {
|
|
3858
|
+
await harness.emit("agent_end", {
|
|
3859
|
+
messages: [{ role: "assistant", stopReason: "stop", content: "completed" }],
|
|
3860
|
+
});
|
|
3534
3861
|
expect(await settledStatus(harness, "turn.prompt_status", replacementIds)).toMatchObject({
|
|
3535
3862
|
status: "terminal_ok",
|
|
3536
3863
|
});
|
|
@@ -3656,7 +3983,9 @@ describe("post-acceptance invocation terminalization", () => {
|
|
|
3656
3983
|
expect(successor.ok).toBe(true);
|
|
3657
3984
|
const successorIds = { commandId: successor.result?.commandId, turnId: successor.result?.turnId };
|
|
3658
3985
|
await harness.emit("agent_start");
|
|
3659
|
-
await harness.emit("agent_end", {
|
|
3986
|
+
await harness.emit("agent_end", {
|
|
3987
|
+
messages: [{ role: "assistant", stopReason: "stop", content: "completed" }],
|
|
3988
|
+
});
|
|
3660
3989
|
secondInflight.resolve();
|
|
3661
3990
|
expect(await settledStatus(harness, "turn.prompt_status", successorIds)).toMatchObject({
|
|
3662
3991
|
status: "terminal_ok",
|
|
@@ -3789,7 +4118,9 @@ describe("post-acceptance invocation terminalization", () => {
|
|
|
3789
4118
|
expect(successor.ok).toBe(true);
|
|
3790
4119
|
const successorIds = { commandId: successor.result?.commandId, turnId: successor.result?.turnId };
|
|
3791
4120
|
await harness.emit("agent_start");
|
|
3792
|
-
await harness.emit("agent_end", {
|
|
4121
|
+
await harness.emit("agent_end", {
|
|
4122
|
+
messages: [{ role: "assistant", stopReason: "stop", content: "completed" }],
|
|
4123
|
+
});
|
|
3793
4124
|
secondInflight.resolve();
|
|
3794
4125
|
expect(await settledStatus(harness, "turn.prompt_status", successorIds)).toMatchObject({
|
|
3795
4126
|
status: "terminal_ok",
|
|
@@ -3938,7 +4269,7 @@ describe("post-acceptance invocation terminalization", () => {
|
|
|
3938
4269
|
status: "failed",
|
|
3939
4270
|
error: { code: "aborted" },
|
|
3940
4271
|
});
|
|
3941
|
-
await harness.emit("agent_end");
|
|
4272
|
+
await harness.emit("agent_end", { messages: [{ role: "assistant", content: "completed" }] });
|
|
3942
4273
|
expect(await settledStatus(harness, "turn.prompt_status", secondIds)).toMatchObject({
|
|
3943
4274
|
status: "terminal_ok",
|
|
3944
4275
|
});
|
|
@@ -3989,8 +4320,8 @@ describe("post-acceptance invocation terminalization", () => {
|
|
|
3989
4320
|
expect(await harness.query("turn.prompt_status", successorIds)).toMatchObject({
|
|
3990
4321
|
result: { status: expect.stringMatching(/accepted|in_flight/) },
|
|
3991
4322
|
});
|
|
3992
|
-
await harness.emit("agent_end");
|
|
3993
|
-
await harness.emit("agent_end");
|
|
4323
|
+
await harness.emit("agent_end", { messages: [{ role: "assistant", content: "completed" }] });
|
|
4324
|
+
await harness.emit("agent_end", { messages: [{ role: "assistant", content: "completed" }] });
|
|
3994
4325
|
expect(await settledStatus(harness, "turn.prompt_status", successorIds)).toMatchObject({
|
|
3995
4326
|
status: "terminal_ok",
|
|
3996
4327
|
});
|
|
@@ -4078,7 +4409,7 @@ describe("post-acceptance invocation terminalization", () => {
|
|
|
4078
4409
|
expect(accepted.ok).toBe(true);
|
|
4079
4410
|
const successorIds = { commandId: accepted.result?.commandId, turnId: accepted.result?.turnId };
|
|
4080
4411
|
await harness.emit("agent_start");
|
|
4081
|
-
await harness.emit("agent_end");
|
|
4412
|
+
await harness.emit("agent_end", { messages: [{ role: "assistant", content: "completed" }] });
|
|
4082
4413
|
expect(await settledStatus(harness, "turn.prompt_status", successorIds)).toMatchObject({
|
|
4083
4414
|
status: "terminal_ok",
|
|
4084
4415
|
});
|
|
@@ -4277,7 +4608,7 @@ describe("post-acceptance invocation terminalization", () => {
|
|
|
4277
4608
|
const accepted = await harness.control("turn.prompt", { text: "hello" });
|
|
4278
4609
|
const { commandId, turnId } = accepted.result ?? {};
|
|
4279
4610
|
await harness.emit("agent_start");
|
|
4280
|
-
await harness.emit("agent_end");
|
|
4611
|
+
await harness.emit("agent_end", { messages: [{ role: "assistant", content: "completed" }] });
|
|
4281
4612
|
const claimed = await settledStatus(harness, "turn.prompt_status", { commandId, turnId });
|
|
4282
4613
|
expect(claimed).toMatchObject({ status: "terminal_ok" });
|
|
4283
4614
|
inflight.reject(Object.assign(new Error("late provider failure"), { code: "upstream_error" }));
|
|
@@ -4445,7 +4776,7 @@ describe("accepted-control zero-execution bound (#4668)", () => {
|
|
|
4445
4776
|
const first = await harness.control("turn.prompt", { text: "first" });
|
|
4446
4777
|
expect(first.ok).toBe(true);
|
|
4447
4778
|
await harness.emit("agent_start");
|
|
4448
|
-
await harness.emit("agent_end");
|
|
4779
|
+
await harness.emit("agent_end", { messages: [{ role: "assistant", content: "completed" }] });
|
|
4449
4780
|
const followUpB = await harness.control("turn.follow_up", { text: "b" });
|
|
4450
4781
|
const followUpC = await harness.control("turn.follow_up", { text: "c" });
|
|
4451
4782
|
promoted[0]?.({ startsOwnRun: true });
|
|
@@ -4593,7 +4924,7 @@ describe("accepted-control zero-execution bound (#4668)", () => {
|
|
|
4593
4924
|
}
|
|
4594
4925
|
const midRun = await harness.query("turn.prompt_status", ids);
|
|
4595
4926
|
expect(midRun.result?.status).not.toBe("failed");
|
|
4596
|
-
await harness.emit("agent_end");
|
|
4927
|
+
await harness.emit("agent_end", { messages: [{ role: "assistant", content: "completed" }] });
|
|
4597
4928
|
const final = await harness.query("turn.prompt_status", ids);
|
|
4598
4929
|
expect(final.result?.status).toBe("terminal_ok");
|
|
4599
4930
|
await harness.stop();
|
|
@@ -4688,7 +5019,7 @@ describe("accepted-control zero-execution bound (#4668)", () => {
|
|
|
4688
5019
|
// The consuming run ends. The consumed submission must terminalize
|
|
4689
5020
|
// WITH it (terminal_ok, never a fabricated prompt_deadline_exceeded):
|
|
4690
5021
|
// with the bug it would still be parked in pending as merely accepted.
|
|
4691
|
-
await handlers.get("agent_end")?.({}, ctx);
|
|
5022
|
+
await handlers.get("agent_end")?.({ messages: [{ role: "assistant", content: "completed" }] }, ctx);
|
|
4692
5023
|
const statusOf = async (ids: { commandId?: string; turnId?: string }, frameId: string) => {
|
|
4693
5024
|
transport.feed("conn-a", {
|
|
4694
5025
|
type: "query_request",
|
|
@@ -4766,7 +5097,7 @@ describe("accepted-control zero-execution bound (#4668)", () => {
|
|
|
4766
5097
|
expect(midRun.result?.status).not.toBe("terminal_ok");
|
|
4767
5098
|
expect(midRun.result?.status).not.toBe("failed");
|
|
4768
5099
|
// The in-flight run ends: the diverted correlation terminalizes with it.
|
|
4769
|
-
await harness.emit("agent_end");
|
|
5100
|
+
await harness.emit("agent_end", { messages: [{ role: "assistant", content: "completed" }] });
|
|
4770
5101
|
expect(await settledStatus(harness, "turn.prompt_status", idsRaced)).toMatchObject({
|
|
4771
5102
|
status: "terminal_ok",
|
|
4772
5103
|
});
|
|
@@ -4825,7 +5156,7 @@ describe("accepted-control zero-execution bound (#4668)", () => {
|
|
|
4825
5156
|
expect(queued.result?.status).not.toBe("failed");
|
|
4826
5157
|
// Real consumption re-leases and attaches to the in-flight run.
|
|
4827
5158
|
consumed?.({ startsOwnRun: false });
|
|
4828
|
-
await harness.emit("agent_end");
|
|
5159
|
+
await harness.emit("agent_end", { messages: [{ role: "assistant", content: "completed" }] });
|
|
4829
5160
|
expect(await settledStatus(harness, "turn.prompt_status", idsRaced)).toMatchObject({
|
|
4830
5161
|
status: "terminal_ok",
|
|
4831
5162
|
});
|
|
@@ -5265,6 +5596,7 @@ describe("accepted-control zero-execution bound (#4668)", () => {
|
|
|
5265
5596
|
const acceptedA = (await waitFrame("settle-early-a")) as { result?: { commandId?: string; turnId?: string } };
|
|
5266
5597
|
const idsA = { commandId: acceptedA.result?.commandId, turnId: acceptedA.result?.turnId };
|
|
5267
5598
|
const statusDeadline = Date.now() + 15_000;
|
|
5599
|
+
let settledStatus: string | undefined;
|
|
5268
5600
|
for (;;) {
|
|
5269
5601
|
transport.feed("conn-a", {
|
|
5270
5602
|
type: "query_request",
|
|
@@ -5276,10 +5608,12 @@ describe("accepted-control zero-execution bound (#4668)", () => {
|
|
|
5276
5608
|
result?: { status?: string };
|
|
5277
5609
|
};
|
|
5278
5610
|
transport.sent.splice(transport.sent.indexOf(statusFrame), 1);
|
|
5279
|
-
|
|
5611
|
+
settledStatus = statusFrame.result?.status;
|
|
5612
|
+
if (statusFrame.result?.status === "terminal_ok" || statusFrame.result?.status === "failed") break;
|
|
5280
5613
|
if (Date.now() > statusDeadline) throw new Error("settled prompt never reported terminal_ok");
|
|
5281
5614
|
await Bun.sleep(20);
|
|
5282
5615
|
}
|
|
5616
|
+
expect(settledStatus).toBe("failed");
|
|
5283
5617
|
// conn-b's prompt is accepted and hangs; its run then starts.
|
|
5284
5618
|
transport.feed("conn-b", {
|
|
5285
5619
|
type: "control_request",
|
|
@@ -5954,7 +6288,7 @@ test("SDK-only host lets every connection whose follow-up was promoted abort the
|
|
|
5954
6288
|
Promise.resolve(options?.onPreflightAcceptCommit?.()).then(() => {
|
|
5955
6289
|
if (options?.onQueuedPromoted) promoted.push(options.onQueuedPromoted);
|
|
5956
6290
|
options?.onPreflightAccepted?.();
|
|
5957
|
-
return
|
|
6291
|
+
return "completed";
|
|
5958
6292
|
}),
|
|
5959
6293
|
} as unknown as ExtensionAPI;
|
|
5960
6294
|
const transport = memoryTransport();
|
|
@@ -6042,7 +6376,7 @@ test("SDK-only host lets every connection whose follow-up was promoted abort the
|
|
|
6042
6376
|
// invocation left the remaining follow-ups durably accepted — their
|
|
6043
6377
|
// result lookups would never complete and a restart would report them
|
|
6044
6378
|
// failed even though they ran in the shared turn.
|
|
6045
|
-
await handlers.get("agent_end")?.({ type: "agent_end" }, ctx);
|
|
6379
|
+
await handlers.get("agent_end")?.({ type: "agent_end", stopReason: "cancelled", messages: [] }, ctx);
|
|
6046
6380
|
{
|
|
6047
6381
|
const terminalDeadline = Date.now() + 15_000;
|
|
6048
6382
|
const promptStatuses = () =>
|