@gajae-code/coding-agent 0.16.0 → 0.16.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +41 -1
- package/dist/types/async/job-manager.d.ts +32 -1
- package/dist/types/browser-backend.d.ts +12 -0
- package/dist/types/cli/update-cli.d.ts +1 -0
- package/dist/types/commands/sdk.d.ts +2 -0
- package/dist/types/config/billing-path.d.ts +25 -0
- package/dist/types/config/config-schema-version.d.ts +13 -1
- package/dist/types/config/model-registry.d.ts +16 -0
- package/dist/types/config/settings-schema.d.ts +17 -7
- package/dist/types/extensibility/extensions/types.d.ts +8 -3
- package/dist/types/gjc-runtime/launch-worktree.d.ts +25 -1
- package/dist/types/gjc-runtime/session-state-lock.d.ts +22 -0
- package/dist/types/main.d.ts +1 -0
- package/dist/types/modes/acp/acp-agent.d.ts +21 -21
- package/dist/types/modes/components/model-selector.d.ts +4 -0
- package/dist/types/modes/components/session-selector.d.ts +1 -1
- package/dist/types/modes/components/sessions-dashboard.d.ts +2 -1
- package/dist/types/modes/interactive-mode.d.ts +16 -2
- package/dist/types/modes/theme/theme.d.ts +2 -1
- package/dist/types/modes/types.d.ts +2 -0
- package/dist/types/sdk/broker/broker.d.ts +2 -1
- package/dist/types/sdk/broker/ensure.d.ts +3 -0
- package/dist/types/sdk/broker/lifecycle-ledger.d.ts +6 -0
- package/dist/types/sdk/broker/lifecycle.d.ts +14 -0
- package/dist/types/sdk/broker/startup-budget.d.ts +52 -0
- package/dist/types/sdk/broker/state-version.d.ts +17 -0
- package/dist/types/sdk/bus/discord-daemon.d.ts +1 -1
- package/dist/types/sdk/bus/index.d.ts +3 -1
- package/dist/types/sdk/bus/kind-aware-reconciliation.d.ts +1 -1
- package/dist/types/sdk/bus/prompt-reconciliation.d.ts +1 -1
- package/dist/types/sdk/bus/slack-conversation.d.ts +1 -1
- package/dist/types/sdk/bus/slack-daemon.d.ts +1 -1
- package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +1 -1
- package/dist/types/sdk/cli/rows.d.ts +8 -2
- package/dist/types/sdk/cli/session-cli.d.ts +2 -1
- package/dist/types/sdk/host/events.d.ts +1 -0
- package/dist/types/sdk/host/query/cursor.d.ts +4 -2
- package/dist/types/sdk/host/query/handlers.d.ts +1 -0
- package/dist/types/sdk/host/query/revision-store.d.ts +1 -0
- package/dist/types/sdk/host/session-runtime.d.ts +18 -17
- package/dist/types/sdk/lifecycle-session.d.ts +1 -1
- package/dist/types/sdk/prompt-deadline-manager.d.ts +2 -5
- package/dist/types/sdk/receipt-state.d.ts +2 -0
- package/dist/types/sdk/router/session-router.d.ts +4 -0
- package/dist/types/sdk/turn-result.d.ts +6 -0
- package/dist/types/session/agent-session.d.ts +47 -7
- package/dist/types/session/fold-coordinator.d.ts +29 -2
- package/dist/types/session/session-manager.d.ts +14 -2
- package/dist/types/tools/ask.d.ts +1 -0
- package/dist/types/tools/bash.d.ts +12 -0
- package/dist/types/tools/browser/launch.d.ts +10 -0
- package/dist/types/tools/browser/registry.d.ts +8 -0
- package/dist/types/tools/index.d.ts +6 -10
- package/dist/types/tools/job.d.ts +3 -1
- package/dist/types/utils/external-editor.d.ts +7 -0
- package/examples/extensions/README.md +1 -1
- package/examples/extensions/pirate.ts +7 -4
- package/package.json +7 -7
- package/scripts/generate-sdk-operation-inventory.ts +7 -1
- package/src/async/job-manager.ts +107 -9
- package/src/browser-backend.ts +29 -0
- package/src/cli/plugin-cli.ts +29 -0
- package/src/cli/update-cli.ts +63 -12
- package/src/commands/sdk.ts +22 -7
- package/src/config/billing-path.ts +42 -0
- package/src/config/config-schema-version.ts +18 -1
- package/src/config/model-preset-registry.ts +177 -18
- package/src/config/model-registry.ts +268 -42
- package/src/config/settings-schema.ts +19 -7
- package/src/config/settings.ts +46 -15
- package/src/coordinator-mcp/projection-scan.ts +6 -6
- package/src/coordinator-mcp/server.ts +21 -1
- package/src/defaults/gjc/skills/deep-interview/SKILL.md +1 -1
- package/src/extensibility/extensions/runner.ts +27 -0
- package/src/extensibility/extensions/types.ts +8 -3
- package/src/extensibility/gjc-plugins/installer.ts +51 -11
- package/src/extensibility/runtime-skill-discovery.ts +110 -3
- package/src/extensibility/skills.ts +22 -12
- package/src/gjc-runtime/launch-worktree.ts +285 -4
- package/src/gjc-runtime/session-state-lock.ts +154 -28
- package/src/gjc-runtime/session-state-sidecar.ts +10 -17
- package/src/gjc-runtime/tmux-owner-isolation.ts +16 -1
- package/src/internal-urls/docs-index.generated.ts +10 -10
- package/src/main.ts +4 -1
- package/src/modes/acp/acp-agent.ts +5 -5
- package/src/modes/components/model-selector.ts +76 -8
- package/src/modes/components/session-selector.ts +23 -15
- package/src/modes/components/sessions-dashboard.ts +8 -5
- package/src/modes/controllers/command-controller.ts +1 -1
- package/src/modes/controllers/event-controller.ts +30 -4
- package/src/modes/controllers/extension-ui-controller.ts +13 -13
- package/src/modes/controllers/input-controller.ts +2 -2
- package/src/modes/controllers/selector-controller.ts +10 -3
- package/src/modes/interactive-mode.ts +36 -12
- package/src/modes/prompt-action-autocomplete.ts +30 -4
- package/src/modes/runtime-init.ts +12 -9
- package/src/modes/theme/theme.ts +5 -0
- package/src/modes/types.ts +2 -0
- package/src/modes/utils/ui-helpers.ts +22 -3
- package/src/prompts/agent-fragments/browser-aside.md +22 -0
- package/src/prompts/tools/ask.md +1 -1
- package/src/prompts/tools/bash.md +1 -0
- package/src/prompts/tools/job.md +1 -1
- package/src/sdk/acp/adapter.ts +5 -0
- package/src/sdk/broker/broker.ts +10 -1
- package/src/sdk/broker/ensure.ts +76 -3
- package/src/sdk/broker/lifecycle-ledger.ts +6 -0
- package/src/sdk/broker/lifecycle.ts +254 -34
- package/src/sdk/broker/spawn-substrate.ts +45 -2
- package/src/sdk/broker/startup-budget.ts +144 -2
- package/src/sdk/broker/state-version.ts +17 -0
- package/src/sdk/bus/chat-daemon-control.ts +2 -2
- package/src/sdk/bus/chat-daemon-runtime.ts +111 -24
- package/src/sdk/bus/discord-daemon.ts +50 -10
- package/src/sdk/bus/index.ts +33 -3
- package/src/sdk/bus/kind-aware-reconciliation.ts +296 -48
- package/src/sdk/bus/lifecycle-commands.ts +4 -0
- package/src/sdk/bus/notification-service.ts +22 -5
- package/src/sdk/bus/prompt-reconciliation.ts +7 -2
- package/src/sdk/bus/reconciliation-store.ts +55 -31
- package/src/sdk/bus/slack-conversation.ts +1 -1
- package/src/sdk/bus/slack-daemon.ts +72 -38
- package/src/sdk/bus/telegram-daemon-contract.ts +1 -1
- package/src/sdk/cli/rows.ts +30 -7
- package/src/sdk/cli/session-cli.ts +33 -21
- package/src/sdk/host/control/dispatch.ts +2 -2
- package/src/sdk/host/events.ts +1 -0
- package/src/sdk/host/host.ts +2 -0
- package/src/sdk/host/query/cursor.ts +52 -7
- package/src/sdk/host/query/handlers.ts +33 -12
- package/src/sdk/host/query/revision-store.ts +12 -0
- package/src/sdk/host/session-runtime.test.ts +463 -8
- package/src/sdk/host/session-runtime.ts +383 -54
- package/src/sdk/host/surface-policy.ts +3 -3
- package/src/sdk/lifecycle-session.ts +7 -2
- package/src/sdk/prompt-deadline-manager.ts +2 -1
- package/src/sdk/protocol/operation-inventory.generated.json +54 -3
- package/src/sdk/protocol/operation-registry.ts +1 -1
- package/src/sdk/receipt-state.ts +10 -0
- package/src/sdk/router/session-router.ts +152 -59
- package/src/sdk/session.ts +60 -41
- package/src/sdk/turn-result.ts +22 -0
- package/src/session/agent-session.ts +1033 -260
- package/src/session/fold-coordinator.ts +40 -3
- package/src/session/session-manager.ts +517 -77
- package/src/slash-commands/builtin-registry.ts +41 -0
- package/src/task/executor.ts +9 -4
- package/src/tools/ask-contract.test.ts +64 -0
- package/src/tools/ask-contract.ts +36 -1
- package/src/tools/ask.ts +10 -8
- package/src/tools/bash-interactive.ts +11 -2
- package/src/tools/bash.ts +156 -33
- package/src/tools/browser/launch.ts +37 -0
- package/src/tools/browser/registry.ts +99 -9
- package/src/tools/cron.ts +6 -4
- package/src/tools/descriptors.test.ts +15 -0
- package/src/tools/descriptors.ts +5 -1
- package/src/tools/index.ts +6 -6
- package/src/tools/job.ts +4 -1
- package/src/tools/monitor.ts +12 -9
- package/src/tools/resolve.ts +30 -10
- package/src/tools/tool-catalog.generated.ts +2 -2
- package/src/utils/external-editor.ts +72 -1
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import { describe, expect, test } from "bun:test";
|
|
1
|
+
import { describe, expect, spyOn, test } from "bun:test";
|
|
2
2
|
import { createHash } from "node:crypto";
|
|
3
|
-
import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
|
3
|
+
import { mkdir, mkdtemp, readdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
4
4
|
import * as os from "node:os";
|
|
5
5
|
import * as path from "node:path";
|
|
6
|
+
import { logger } from "@gajae-code/utils";
|
|
6
7
|
import { AsyncJobManager } from "../../async";
|
|
7
8
|
import type { Settings } from "../../config/settings";
|
|
8
9
|
import type { ExtensionAPI, ExtensionContext, ExtensionTranscriptEntry } from "../../extensibility/extensions";
|
|
@@ -254,7 +255,10 @@ test("native prompt reconciliation fails closed for an explicitly empty assistan
|
|
|
254
255
|
receiptState: "missing",
|
|
255
256
|
});
|
|
256
257
|
reconciliation.noteTransition(correlation, { type: "agent_end", finalText: "late text" });
|
|
257
|
-
expect(reconciliation.lookup({ clientRef: "native-empty-ref" })).toMatchObject({
|
|
258
|
+
expect(reconciliation.lookup({ clientRef: "native-empty-ref" })).toMatchObject({
|
|
259
|
+
status: "failed",
|
|
260
|
+
receiptState: "present",
|
|
261
|
+
});
|
|
258
262
|
});
|
|
259
263
|
|
|
260
264
|
test("broker reconciliation fails closed for an empty prompt and persists the first terminal result", async () => {
|
|
@@ -283,7 +287,9 @@ test("broker reconciliation fails closed for an empty prompt and persists the fi
|
|
|
283
287
|
expect(reloaded.lookupResult("prompt", { clientRef: "broker-empty-ref" })).toMatchObject({
|
|
284
288
|
status: "failed",
|
|
285
289
|
error: { code: "prompt_failed" },
|
|
290
|
+
content: { text: "late text", byteLength: 9, truncated: false },
|
|
286
291
|
});
|
|
292
|
+
expect(reloaded.lookup("prompt", { clientRef: "broker-empty-ref" })).toMatchObject({ receiptState: "present" });
|
|
287
293
|
const noOpCorrelation = { commandId: "broker-noop-command", turnId: "broker-noop-turn" };
|
|
288
294
|
await reconciliation.noteAccepted("prompt", noOpCorrelation, "broker-noop-ref");
|
|
289
295
|
await reconciliation.finalizeOutcome(
|
|
@@ -446,6 +452,42 @@ test("SDK-only finalizeOutcome keeps legacy recordError positional compatibility
|
|
|
446
452
|
});
|
|
447
453
|
});
|
|
448
454
|
|
|
455
|
+
test("SDK-only terminal outcomes survive result projection and durable reload", async () => {
|
|
456
|
+
let records: unknown[] = [];
|
|
457
|
+
const store = {
|
|
458
|
+
path: null,
|
|
459
|
+
load: async () => records,
|
|
460
|
+
transact: async (mutator: (current: never[]) => never[]) => {
|
|
461
|
+
records = mutator(records as never);
|
|
462
|
+
},
|
|
463
|
+
} as never;
|
|
464
|
+
const stopped = { kind: "stopped", reason: "cancelled", provenance: "client_cancel" } as const;
|
|
465
|
+
const reconciliation = createInvocationReconciliation({ store });
|
|
466
|
+
const correlation = { commandId: "outcome-command", turnId: "outcome-turn" };
|
|
467
|
+
await reconciliation.noteAccepted("prompt", correlation, "outcome-ref");
|
|
468
|
+
await reconciliation.claimPendingOutcome("prompt", correlation, stopped);
|
|
469
|
+
await reconciliation.finalizeOutcome("prompt", correlation);
|
|
470
|
+
const projected = reconciliation.lookupResult("prompt", { clientRef: "outcome-ref" });
|
|
471
|
+
expect(projected).toMatchObject({ status: "terminal_ok", outcome: stopped, receiptState: "unknown" });
|
|
472
|
+
expect(reconciliation.lookup("prompt", { clientRef: "outcome-ref" })).toMatchObject({ outcome: stopped });
|
|
473
|
+
|
|
474
|
+
const reopened = createInvocationReconciliation({ store });
|
|
475
|
+
await reopened.hydrate();
|
|
476
|
+
expect(reopened.lookupResult("prompt", { clientRef: "outcome-ref" })).toMatchObject({
|
|
477
|
+
status: "terminal_ok",
|
|
478
|
+
outcome: stopped,
|
|
479
|
+
});
|
|
480
|
+
|
|
481
|
+
const direct = createInvocationReconciliation();
|
|
482
|
+
const directCorrelation = { commandId: "direct-outcome-command", turnId: "direct-outcome-turn" };
|
|
483
|
+
await direct.noteAccepted("prompt", directCorrelation, "direct-outcome-ref");
|
|
484
|
+
await direct.noteTransition("prompt", directCorrelation, { type: "agent_end", outcome: stopped });
|
|
485
|
+
expect(direct.lookupResult("prompt", { clientRef: "direct-outcome-ref" })).toMatchObject({
|
|
486
|
+
status: "terminal_ok",
|
|
487
|
+
outcome: stopped,
|
|
488
|
+
});
|
|
489
|
+
});
|
|
490
|
+
|
|
449
491
|
test("a late agent failure never overwrites the reason an already terminal record carries", async () => {
|
|
450
492
|
const reconciliation = createInvocationReconciliation();
|
|
451
493
|
const correlation = { commandId: "first-reason-command", turnId: "first-reason-turn" };
|
|
@@ -493,7 +535,7 @@ test("a failed acceptance persist rolls back the provisional record and reservat
|
|
|
493
535
|
"accept persist failed",
|
|
494
536
|
);
|
|
495
537
|
reconciliation.release("prompt", "accept-failed-ref");
|
|
496
|
-
expect(reconciliation.lookup("prompt", first)).toEqual({ status: "unknown" });
|
|
538
|
+
expect(reconciliation.lookup("prompt", first)).toEqual({ status: "unknown", receiptState: "unknown" });
|
|
497
539
|
|
|
498
540
|
const retry = { commandId: "accept-retry-command", turnId: "accept-retry-turn" };
|
|
499
541
|
reconciliation.admit("prompt", "accept-failed-ref");
|
|
@@ -548,6 +590,54 @@ test("a failed uncertainty persist restores the durable deadline terminal until
|
|
|
548
590
|
]);
|
|
549
591
|
});
|
|
550
592
|
|
|
593
|
+
test("SDK-only markUncertain persists an active-schema record without quarantining siblings", async () => {
|
|
594
|
+
const root = await mkdtemp(path.join(os.tmpdir(), "gjc-sdk-uncertain-reload-"));
|
|
595
|
+
try {
|
|
596
|
+
const sessionFile = path.join(root, "session.jsonl");
|
|
597
|
+
await Bun.write(sessionFile, "");
|
|
598
|
+
const store = createReconciliationStore({ sessionFile, sessionId: "uncertain-reload" });
|
|
599
|
+
const reconciliation = createInvocationReconciliation({ store });
|
|
600
|
+
const correlation = { commandId: "uncertain-reload-command", turnId: "uncertain-reload-turn" };
|
|
601
|
+
const sibling = { commandId: "uncertain-sibling-command", turnId: "uncertain-sibling-turn" };
|
|
602
|
+
await reconciliation.noteAccepted("prompt", correlation, "uncertain-reload-ref");
|
|
603
|
+
await reconciliation.finalizeOutcome("prompt", correlation, {
|
|
604
|
+
kind: "failed",
|
|
605
|
+
code: "prompt_deadline_exceeded",
|
|
606
|
+
message: "deadline",
|
|
607
|
+
});
|
|
608
|
+
await reconciliation.noteAccepted("skill", sibling, "uncertain-sibling-ref");
|
|
609
|
+
await reconciliation.finalizeOutcome("skill", sibling, {
|
|
610
|
+
kind: "stopped",
|
|
611
|
+
code: "cancelled",
|
|
612
|
+
message: "cancelled",
|
|
613
|
+
});
|
|
614
|
+
|
|
615
|
+
await reconciliation.markUncertain("prompt", correlation, undefined, 5_000);
|
|
616
|
+
const active = store
|
|
617
|
+
.snapshot()
|
|
618
|
+
.find(record => record.kind === "prompt" && record.commandId === correlation.commandId) as
|
|
619
|
+
| Record<string, unknown>
|
|
620
|
+
| undefined;
|
|
621
|
+
expect(active).toMatchObject({ status: "accepted", deadlineRecoveryPending: true, deadlineMaxAt: 5_000 });
|
|
622
|
+
expect(active?.terminalAt).toBeUndefined();
|
|
623
|
+
expect(active?.receiptState).toBeUndefined();
|
|
624
|
+
expect(active?.outcome).toBeUndefined();
|
|
625
|
+
expect(active?.pendingOutcome).toBeUndefined();
|
|
626
|
+
expect(active?.pendingReceiptState).toBeUndefined();
|
|
627
|
+
|
|
628
|
+
const reopenedStore = createReconciliationStore({ sessionFile, sessionId: "uncertain-reload" });
|
|
629
|
+
const reopened = createInvocationReconciliation({ store: reopenedStore });
|
|
630
|
+
await reopened.hydrate();
|
|
631
|
+
expect(reopened.lookup("prompt", { clientRef: "uncertain-reload-ref" })).toMatchObject({ status: "accepted" });
|
|
632
|
+
expect(reopened.lookup("skill", { clientRef: "uncertain-sibling-ref" })).toMatchObject({
|
|
633
|
+
status: "terminal_ok",
|
|
634
|
+
});
|
|
635
|
+
expect((await readdir(root)).some(name => name.includes("corrupt"))).toBe(false);
|
|
636
|
+
} finally {
|
|
637
|
+
await rm(root, { recursive: true, force: true });
|
|
638
|
+
}
|
|
639
|
+
});
|
|
640
|
+
|
|
551
641
|
test("durable reload keeps agent_end terminal across a paused successor transition", async () => {
|
|
552
642
|
let records: unknown[] = [];
|
|
553
643
|
let pauseWrites = 0;
|
|
@@ -911,8 +1001,13 @@ test("finalization without an explicit outcome follows the noteTransition eviden
|
|
|
911
1001
|
await legacyArg5.finalizeOutcome("prompt", legacyCorrelation, undefined, undefined, {
|
|
912
1002
|
content: { version: 1, type: "text", text: "legacy", byteLength: 6, truncated: false },
|
|
913
1003
|
});
|
|
914
|
-
|
|
1004
|
+
await legacyArg5.noteTransition("prompt", legacyCorrelation, {
|
|
1005
|
+
type: "agent_end",
|
|
1006
|
+
content: { version: 1, type: "text", text: "later", byteLength: 5, truncated: false },
|
|
1007
|
+
});
|
|
1008
|
+
expect(legacyArg5.lookupResult("prompt", { clientRef: "final-legacy-ref" })).toMatchObject({
|
|
915
1009
|
status: "terminal_ok",
|
|
1010
|
+
content: { text: "legacy", byteLength: 6, truncated: false },
|
|
916
1011
|
});
|
|
917
1012
|
|
|
918
1013
|
const explicitFailure = createInvocationReconciliation({ store: makeLocalStore() });
|
|
@@ -924,9 +1019,58 @@ test("finalization without an explicit outcome follows the noteTransition eviden
|
|
|
924
1019
|
message: "provider rejected",
|
|
925
1020
|
});
|
|
926
1021
|
expect(explicitFailure.lookup("prompt", { clientRef: "final-failure-ref" })).toMatchObject({
|
|
1022
|
+
status: "failed",
|
|
1023
|
+
receiptState: "unknown",
|
|
1024
|
+
outcome: { kind: "failed", code: "prompt_failed", provenance: "agent_failed" },
|
|
1025
|
+
error: { code: "prompt_failed" },
|
|
1026
|
+
});
|
|
1027
|
+
});
|
|
1028
|
+
|
|
1029
|
+
test("provider failures outrank later stopped outcomes in both SDK-only terminal paths", async () => {
|
|
1030
|
+
const providerError = Object.assign(new Error("provider rejected"), { code: "provider_rejected" });
|
|
1031
|
+
const transition = createInvocationReconciliation();
|
|
1032
|
+
const transitionCorrelation = { commandId: "precedence-transition-command", turnId: "precedence-transition-turn" };
|
|
1033
|
+
await transition.noteAccepted("prompt", transitionCorrelation, "precedence-transition-ref");
|
|
1034
|
+
await transition.noteTransition("prompt", transitionCorrelation, { type: "agent_failed", error: providerError });
|
|
1035
|
+
await transition.noteTransition("prompt", transitionCorrelation, {
|
|
1036
|
+
type: "agent_end",
|
|
1037
|
+
outcome: { kind: "stopped", reason: "cancelled", provenance: "client_cancel" },
|
|
1038
|
+
});
|
|
1039
|
+
expect(transition.lookupResult("prompt", { clientRef: "precedence-transition-ref" })).toMatchObject({
|
|
1040
|
+
status: "failed",
|
|
1041
|
+
error: { code: "provider_rejected" },
|
|
1042
|
+
});
|
|
1043
|
+
|
|
1044
|
+
const finalized = createInvocationReconciliation();
|
|
1045
|
+
const finalizedCorrelation = { commandId: "precedence-finalize-command", turnId: "precedence-finalize-turn" };
|
|
1046
|
+
await finalized.noteAccepted("prompt", finalizedCorrelation, "precedence-finalize-ref");
|
|
1047
|
+
await finalized.noteTransition("prompt", finalizedCorrelation, { type: "agent_failed", error: providerError });
|
|
1048
|
+
await finalized.claimPendingOutcome("prompt", finalizedCorrelation, {
|
|
1049
|
+
kind: "failed",
|
|
1050
|
+
code: "prompt_deadline_exceeded",
|
|
1051
|
+
message: "deadline",
|
|
1052
|
+
});
|
|
1053
|
+
await finalized.finalizeOutcome("prompt", finalizedCorrelation);
|
|
1054
|
+
expect(finalized.lookupResult("prompt", { clientRef: "precedence-finalize-ref" })).toMatchObject({
|
|
927
1055
|
status: "failed",
|
|
928
1056
|
error: { code: "provider_rejected" },
|
|
929
1057
|
});
|
|
1058
|
+
|
|
1059
|
+
const generic = createInvocationReconciliation();
|
|
1060
|
+
const genericCorrelation = { commandId: "precedence-generic-command", turnId: "precedence-generic-turn" };
|
|
1061
|
+
await generic.noteAccepted("prompt", genericCorrelation, "precedence-generic-ref");
|
|
1062
|
+
await generic.noteTransition("prompt", genericCorrelation, {
|
|
1063
|
+
type: "agent_failed",
|
|
1064
|
+
error: Object.assign(new Error("generic failure"), { code: "agent_failed" }),
|
|
1065
|
+
});
|
|
1066
|
+
await generic.noteTransition("prompt", genericCorrelation, {
|
|
1067
|
+
type: "agent_end",
|
|
1068
|
+
outcome: { kind: "stopped", reason: "cancelled", provenance: "client_cancel" },
|
|
1069
|
+
});
|
|
1070
|
+
expect(generic.lookupResult("prompt", { clientRef: "precedence-generic-ref" })).toMatchObject({
|
|
1071
|
+
status: "failed",
|
|
1072
|
+
error: { code: "agent_failed", message: "Prompt submission failed." },
|
|
1073
|
+
});
|
|
930
1074
|
});
|
|
931
1075
|
|
|
932
1076
|
test("a reason attached after a prompt settled is never replaced by a later failure", async () => {
|
|
@@ -945,6 +1089,8 @@ test("a reason attached after a prompt settled is never replaced by a later fail
|
|
|
945
1089
|
clientRef: "late-reason-ref",
|
|
946
1090
|
acceptedAt: expect.any(Number),
|
|
947
1091
|
terminalAt: expect.any(Number),
|
|
1092
|
+
receiptState: "present",
|
|
1093
|
+
content: { version: 1, type: "text", text: "completed", byteLength: 9, truncated: false },
|
|
948
1094
|
});
|
|
949
1095
|
// A failure delivered after the record settled enriches it with the sanitized reason and
|
|
950
1096
|
// leaves the terminal claim itself (status, terminalAt, identity) untouched.
|
|
@@ -1052,6 +1198,7 @@ test("capacity eviction still releases the clientRef and reports honest unknown"
|
|
|
1052
1198
|
}
|
|
1053
1199
|
expect(reconciliation.lookup("prompt", { clientRef: "capacity-ref-1" })).toEqual({
|
|
1054
1200
|
status: "unknown",
|
|
1201
|
+
receiptState: "unknown",
|
|
1055
1202
|
});
|
|
1056
1203
|
expect(() => reconciliation.admit("prompt", "capacity-ref-1")).not.toThrow();
|
|
1057
1204
|
const retained = {
|
|
@@ -1102,6 +1249,110 @@ describe("SessionSdkSessionRuntime", () => {
|
|
|
1102
1249
|
);
|
|
1103
1250
|
await runtime.stop();
|
|
1104
1251
|
});
|
|
1252
|
+
test("SDK-only host publishes one replayable bash_folded frame per fold and drops the subscription on shutdown", async () => {
|
|
1253
|
+
const cwd = await mkdtemp(path.join(os.tmpdir(), "gjc-sdk-bash-folded-"));
|
|
1254
|
+
const handlers = new Map<string, (event: unknown, ctx: ExtensionContext) => Promise<void> | void>();
|
|
1255
|
+
const api = {
|
|
1256
|
+
on(event: string, handler: (event: unknown, ctx: ExtensionContext) => Promise<void> | void) {
|
|
1257
|
+
handlers.set(event, handler);
|
|
1258
|
+
},
|
|
1259
|
+
sendUserMessage: async () => {},
|
|
1260
|
+
} as unknown as ExtensionAPI;
|
|
1261
|
+
const transport = memoryTransport();
|
|
1262
|
+
createSdkSessionRuntimeExtension(api, { agentDir: cwd, createTransport: async () => transport });
|
|
1263
|
+
const listeners = new Set<(event: { jobId: string; generation: string; reason: string }) => void>();
|
|
1264
|
+
const ctx = {
|
|
1265
|
+
...extensionContext(transport.sessionId, cwd),
|
|
1266
|
+
onJobFold: (listener: (event: { jobId: string; generation: string; reason: string }) => void) => {
|
|
1267
|
+
listeners.add(listener);
|
|
1268
|
+
return () => listeners.delete(listener);
|
|
1269
|
+
},
|
|
1270
|
+
} as unknown as ExtensionContext;
|
|
1271
|
+
try {
|
|
1272
|
+
await handlers.get("session_start")?.({}, ctx);
|
|
1273
|
+
expect(listeners.size).toBe(1);
|
|
1274
|
+
for (const listener of listeners) listener({ jobId: "bg_7", generation: "bg_7:1", reason: "steer" });
|
|
1275
|
+
transport.feed("client", { type: "event_replay", id: "fold-replay", sinceSeq: 0 });
|
|
1276
|
+
await Bun.sleep(0);
|
|
1277
|
+
const replay = transport.sent.find(
|
|
1278
|
+
frame => frame.type === "event_replay_result" && frame.id === "fold-replay",
|
|
1279
|
+
) as { events?: Array<{ kind?: unknown; payload?: Record<string, unknown> }> } | undefined;
|
|
1280
|
+
const folded = replay?.events?.filter(event => event.kind === "bash_folded") ?? [];
|
|
1281
|
+
expect(folded).toEqual([
|
|
1282
|
+
expect.objectContaining({
|
|
1283
|
+
kind: "bash_folded",
|
|
1284
|
+
payload: {
|
|
1285
|
+
type: "bash_folded",
|
|
1286
|
+
sessionId: transport.sessionId,
|
|
1287
|
+
jobId: "bg_7",
|
|
1288
|
+
generation: "bg_7:1",
|
|
1289
|
+
reason: "steer",
|
|
1290
|
+
},
|
|
1291
|
+
}),
|
|
1292
|
+
]);
|
|
1293
|
+
} finally {
|
|
1294
|
+
await handlers.get("session_shutdown")?.({}, ctx);
|
|
1295
|
+
}
|
|
1296
|
+
expect(listeners.size).toBe(0);
|
|
1297
|
+
});
|
|
1298
|
+
|
|
1299
|
+
test("SDK-only host logs a bash_folded publication failure and keeps the fold replayable", async () => {
|
|
1300
|
+
const cwd = await mkdtemp(path.join(os.tmpdir(), "gjc-sdk-bash-folded-fail-"));
|
|
1301
|
+
const handlers = new Map<string, (event: unknown, ctx: ExtensionContext) => Promise<void> | void>();
|
|
1302
|
+
const api = {
|
|
1303
|
+
on(event: string, handler: (event: unknown, ctx: ExtensionContext) => Promise<void> | void) {
|
|
1304
|
+
handlers.set(event, handler);
|
|
1305
|
+
},
|
|
1306
|
+
sendUserMessage: async () => {},
|
|
1307
|
+
} as unknown as ExtensionAPI;
|
|
1308
|
+
const transport = memoryTransport();
|
|
1309
|
+
const broadcastFrame = transport.broadcastFrame?.bind(transport);
|
|
1310
|
+
let failNextBroadcast = false;
|
|
1311
|
+
transport.broadcastFrame = frame => {
|
|
1312
|
+
if (failNextBroadcast && frame.kind === "bash_folded") {
|
|
1313
|
+
failNextBroadcast = false;
|
|
1314
|
+
throw new Error("broadcast socket closed");
|
|
1315
|
+
}
|
|
1316
|
+
broadcastFrame?.(frame);
|
|
1317
|
+
};
|
|
1318
|
+
createSdkSessionRuntimeExtension(api, { agentDir: cwd, createTransport: async () => transport });
|
|
1319
|
+
const listeners = new Set<(event: { jobId: string; generation: string; reason: string }) => void>();
|
|
1320
|
+
const ctx = {
|
|
1321
|
+
...extensionContext(transport.sessionId, cwd),
|
|
1322
|
+
onJobFold: (listener: (event: { jobId: string; generation: string; reason: string }) => void) => {
|
|
1323
|
+
listeners.add(listener);
|
|
1324
|
+
return () => listeners.delete(listener);
|
|
1325
|
+
},
|
|
1326
|
+
} as unknown as ExtensionContext;
|
|
1327
|
+
const warnings: Array<{ message: string; data?: Record<string, unknown> }> = [];
|
|
1328
|
+
const warn = spyOn(logger, "warn").mockImplementation((message, data) => {
|
|
1329
|
+
warnings.push({ message: String(message), data: data as Record<string, unknown> | undefined });
|
|
1330
|
+
});
|
|
1331
|
+
try {
|
|
1332
|
+
await handlers.get("session_start")?.({}, ctx);
|
|
1333
|
+
failNextBroadcast = true;
|
|
1334
|
+
// The listener call must not throw back into the manager: publication failure is evidence, not a fold error.
|
|
1335
|
+
for (const listener of listeners) listener({ jobId: "bg_9", generation: "bg_9:1", reason: "chord" });
|
|
1336
|
+
expect(warnings.filter(entry => entry.message === "sdk: bash_folded publication failed")).toEqual([
|
|
1337
|
+
expect.objectContaining({
|
|
1338
|
+
data: expect.objectContaining({ jobId: "bg_9", reason: "chord", error: "broadcast socket closed" }),
|
|
1339
|
+
}),
|
|
1340
|
+
]);
|
|
1341
|
+
// The frame was appended to the ring before the broadcast failed, so it still replays.
|
|
1342
|
+
transport.feed("client", { type: "event_replay", id: "fold-replay-fail", sinceSeq: 0 });
|
|
1343
|
+
await Bun.sleep(0);
|
|
1344
|
+
const replay = transport.sent.find(
|
|
1345
|
+
frame => frame.type === "event_replay_result" && frame.id === "fold-replay-fail",
|
|
1346
|
+
) as { events?: Array<{ kind?: unknown; payload?: Record<string, unknown> }> } | undefined;
|
|
1347
|
+
expect(replay?.events?.filter(event => event.kind === "bash_folded")).toEqual([
|
|
1348
|
+
expect.objectContaining({ payload: expect.objectContaining({ jobId: "bg_9", reason: "chord" }) }),
|
|
1349
|
+
]);
|
|
1350
|
+
} finally {
|
|
1351
|
+
warn.mockRestore();
|
|
1352
|
+
await handlers.get("session_shutdown")?.({}, ctx);
|
|
1353
|
+
}
|
|
1354
|
+
});
|
|
1355
|
+
|
|
1105
1356
|
test("SDK-only host admits, replays, and conflicts terminal abort requests durably", async () => {
|
|
1106
1357
|
const cwd = await mkdtemp(path.join(os.tmpdir(), "gjc-sdk-terminal-abort-"));
|
|
1107
1358
|
const operatorCapability = "runtime-terminal-abort-capability";
|
|
@@ -3510,6 +3761,8 @@ async function invocationHarness(
|
|
|
3510
3761
|
onLifecycleDrainTimeout?: () => void;
|
|
3511
3762
|
onFailureDiagnosticKeyCount?: (count: number) => void;
|
|
3512
3763
|
agentFailedWriteFailures?: number;
|
|
3764
|
+
branch?: unknown[];
|
|
3765
|
+
onInvocationCompletionReconciled?: (kind: string, correlation: { commandId: string; turnId: string }) => void;
|
|
3513
3766
|
},
|
|
3514
3767
|
): Promise<InvocationHarness> {
|
|
3515
3768
|
const waiters = new Map<string, (frame: ResponseFrame) => void>();
|
|
@@ -3552,6 +3805,9 @@ async function invocationHarness(
|
|
|
3552
3805
|
...(hooks.onFailureDiagnosticKeyCount
|
|
3553
3806
|
? { onFailureDiagnosticKeyCountForTests: hooks.onFailureDiagnosticKeyCount }
|
|
3554
3807
|
: {}),
|
|
3808
|
+
...(hooks.onInvocationCompletionReconciled
|
|
3809
|
+
? { onInvocationCompletionReconciledForTests: hooks.onInvocationCompletionReconciled }
|
|
3810
|
+
: {}),
|
|
3555
3811
|
...(hooks.settings ? { settings: hooks.settings } : {}),
|
|
3556
3812
|
createTransport: async ({ sessionId: id, stateRoot, token }) => ({
|
|
3557
3813
|
sessionId: id,
|
|
@@ -3590,7 +3846,7 @@ async function invocationHarness(
|
|
|
3590
3846
|
getSessionId: () => sessionId,
|
|
3591
3847
|
getSessionFile: () => path.join(cwd, ".gjc", "state", `${sessionId}.jsonl`),
|
|
3592
3848
|
getSessionName: () => undefined,
|
|
3593
|
-
getBranch: () => [],
|
|
3849
|
+
getBranch: () => hooks.branch ?? [],
|
|
3594
3850
|
},
|
|
3595
3851
|
};
|
|
3596
3852
|
await handlers.get("session_start")?.({}, ctx);
|
|
@@ -4465,7 +4721,9 @@ describe("post-acceptance invocation terminalization", () => {
|
|
|
4465
4721
|
expect(secondIds.turnId).not.toBe(firstIds.turnId);
|
|
4466
4722
|
await harness.emit("agent_start");
|
|
4467
4723
|
await successorStarted.promise;
|
|
4468
|
-
await harness.emit("agent_end"
|
|
4724
|
+
await harness.emit("agent_end", {
|
|
4725
|
+
messages: [{ role: "assistant", content: "predecessor delayed" }],
|
|
4726
|
+
});
|
|
4469
4727
|
expect(await harness.query("turn.prompt_status", secondIds)).toMatchObject({
|
|
4470
4728
|
result: { status: expect.stringMatching(/accepted|in_flight/) },
|
|
4471
4729
|
});
|
|
@@ -4473,10 +4731,23 @@ describe("post-acceptance invocation terminalization", () => {
|
|
|
4473
4731
|
status: "failed",
|
|
4474
4732
|
error: { code: "aborted" },
|
|
4475
4733
|
});
|
|
4734
|
+
const firstResult = await harness.query("turn.result", { kind: "prompt", ...firstIds });
|
|
4735
|
+
expect(firstResult).toMatchObject({
|
|
4736
|
+
result: { status: "failed", content: { text: "predecessor delayed" } },
|
|
4737
|
+
});
|
|
4476
4738
|
await harness.emit("agent_end", { messages: [{ role: "assistant", content: "completed" }] });
|
|
4477
4739
|
expect(await settledStatus(harness, "turn.prompt_status", secondIds)).toMatchObject({
|
|
4478
4740
|
status: "terminal_ok",
|
|
4479
4741
|
});
|
|
4742
|
+
expect(await harness.query("turn.result", { kind: "prompt", ...secondIds })).toMatchObject({
|
|
4743
|
+
result: {
|
|
4744
|
+
status: "terminal_ok",
|
|
4745
|
+
content: { text: "completed", byteLength: 9, truncated: false },
|
|
4746
|
+
},
|
|
4747
|
+
});
|
|
4748
|
+
expect(await harness.query("turn.result", { kind: "prompt", ...firstIds })).toMatchObject({
|
|
4749
|
+
result: { content: { text: "predecessor delayed" } },
|
|
4750
|
+
});
|
|
4480
4751
|
expect(prompts).toBe(2);
|
|
4481
4752
|
await harness.stop();
|
|
4482
4753
|
} finally {
|
|
@@ -4673,6 +4944,190 @@ describe("post-acceptance invocation terminalization", () => {
|
|
|
4673
4944
|
await rm(cwd, { recursive: true, force: true });
|
|
4674
4945
|
}
|
|
4675
4946
|
});
|
|
4947
|
+
test("captures exact bounded content from actual prompt agent_end messages", async () => {
|
|
4948
|
+
const cases = [
|
|
4949
|
+
{ name: "short", agent: "SDK_OK", expected: "SDK_OK", bytes: 6, truncated: false },
|
|
4950
|
+
{ name: "max", agent: "x".repeat(16_384), expected: "x".repeat(16_384), bytes: 16_384, truncated: false },
|
|
4951
|
+
{ name: "blank", agent: " ", expected: undefined, bytes: 0, truncated: false },
|
|
4952
|
+
{
|
|
4953
|
+
name: "overflow",
|
|
4954
|
+
agent: `${"😀".repeat(4_096)}tail`,
|
|
4955
|
+
expected: "😀".repeat(4_096),
|
|
4956
|
+
bytes: 16_384,
|
|
4957
|
+
truncated: true,
|
|
4958
|
+
},
|
|
4959
|
+
] as const;
|
|
4960
|
+
for (const testCase of cases) {
|
|
4961
|
+
const cwd = await mkdtemp(path.join(os.tmpdir(), `gjc-terminal-content-${testCase.name}-`));
|
|
4962
|
+
try {
|
|
4963
|
+
const harness = await invocationHarness(`terminal-content-${testCase.name}`, cwd, {
|
|
4964
|
+
sendUserMessage: async (_content, options) => {
|
|
4965
|
+
await options?.onPreflightAcceptCommit?.();
|
|
4966
|
+
await Promise.withResolvers<void>().promise;
|
|
4967
|
+
},
|
|
4968
|
+
});
|
|
4969
|
+
const accepted = await harness.control("turn.prompt", { text: "hello" });
|
|
4970
|
+
await harness.emit("agent_start");
|
|
4971
|
+
await harness.emit("agent_end", { messages: [{ role: "assistant", content: testCase.agent }] });
|
|
4972
|
+
const result = await settledStatus(harness, "turn.result", {
|
|
4973
|
+
kind: "prompt",
|
|
4974
|
+
commandId: accepted.result?.commandId,
|
|
4975
|
+
turnId: accepted.result?.turnId,
|
|
4976
|
+
});
|
|
4977
|
+
if (testCase.expected === undefined) {
|
|
4978
|
+
expect(result).toMatchObject({ status: "failed" });
|
|
4979
|
+
expect((result as Record<string, unknown>).content).toBeUndefined();
|
|
4980
|
+
} else {
|
|
4981
|
+
expect(result).toMatchObject({
|
|
4982
|
+
status: "terminal_ok",
|
|
4983
|
+
content: {
|
|
4984
|
+
text: testCase.expected,
|
|
4985
|
+
byteLength: testCase.bytes,
|
|
4986
|
+
truncated: testCase.truncated,
|
|
4987
|
+
},
|
|
4988
|
+
});
|
|
4989
|
+
}
|
|
4990
|
+
await harness.stop();
|
|
4991
|
+
} finally {
|
|
4992
|
+
await rm(cwd, { recursive: true, force: true });
|
|
4993
|
+
}
|
|
4994
|
+
}
|
|
4995
|
+
});
|
|
4996
|
+
test("does not reuse a previous branch assistant for a new textless prompt", async () => {
|
|
4997
|
+
const cwd = await mkdtemp(path.join(os.tmpdir(), "gjc-terminal-order-previous-assistant-"));
|
|
4998
|
+
try {
|
|
4999
|
+
const harness = await invocationHarness("terminal-order-previous-assistant", cwd, {
|
|
5000
|
+
branch: [{ type: "message", message: { role: "assistant", content: "PREVIOUS" } }],
|
|
5001
|
+
sendUserMessage: async (_content, options) => {
|
|
5002
|
+
await options?.onPreflightAcceptCommit?.();
|
|
5003
|
+
await Promise.withResolvers<void>().promise;
|
|
5004
|
+
},
|
|
5005
|
+
});
|
|
5006
|
+
const accepted = await harness.control("turn.prompt", { text: "textless" });
|
|
5007
|
+
const selector = {
|
|
5008
|
+
kind: "prompt" as const,
|
|
5009
|
+
commandId: accepted.result?.commandId,
|
|
5010
|
+
turnId: accepted.result?.turnId,
|
|
5011
|
+
};
|
|
5012
|
+
await harness.emit("agent_start");
|
|
5013
|
+
await harness.emit("agent_end", { messages: [] });
|
|
5014
|
+
const result = await settledStatus(harness, "turn.result", selector);
|
|
5015
|
+
expect((result as Record<string, unknown>).content).toBeUndefined();
|
|
5016
|
+
await harness.stop();
|
|
5017
|
+
} finally {
|
|
5018
|
+
await rm(cwd, { recursive: true, force: true });
|
|
5019
|
+
}
|
|
5020
|
+
});
|
|
5021
|
+
test("reconciles contract-valid skill completion and agent_end ordering", async () => {
|
|
5022
|
+
const cases = [
|
|
5023
|
+
{
|
|
5024
|
+
name: "completion-real-agent-real",
|
|
5025
|
+
order: "completion-first",
|
|
5026
|
+
completion: "completion",
|
|
5027
|
+
agent: "agent",
|
|
5028
|
+
expected: "completion",
|
|
5029
|
+
},
|
|
5030
|
+
{
|
|
5031
|
+
name: "completion-blank-agent-real",
|
|
5032
|
+
order: "completion-first",
|
|
5033
|
+
completion: " ",
|
|
5034
|
+
agent: "agent",
|
|
5035
|
+
expected: "agent",
|
|
5036
|
+
},
|
|
5037
|
+
{
|
|
5038
|
+
name: "completion-none-agent-real",
|
|
5039
|
+
order: "completion-first",
|
|
5040
|
+
completion: null,
|
|
5041
|
+
agent: "agent",
|
|
5042
|
+
expected: "agent",
|
|
5043
|
+
},
|
|
5044
|
+
{
|
|
5045
|
+
name: "agent-real-completion-real",
|
|
5046
|
+
order: "agent-first",
|
|
5047
|
+
completion: "completion",
|
|
5048
|
+
agent: "agent",
|
|
5049
|
+
expected: "agent",
|
|
5050
|
+
},
|
|
5051
|
+
{
|
|
5052
|
+
name: "agent-real-completion-blank",
|
|
5053
|
+
order: "agent-first",
|
|
5054
|
+
completion: " ",
|
|
5055
|
+
agent: "agent",
|
|
5056
|
+
expected: "agent",
|
|
5057
|
+
},
|
|
5058
|
+
{
|
|
5059
|
+
name: "agent-blank-completion-real",
|
|
5060
|
+
order: "agent-first",
|
|
5061
|
+
completion: "completion",
|
|
5062
|
+
agent: " ",
|
|
5063
|
+
expected: "completion",
|
|
5064
|
+
},
|
|
5065
|
+
{
|
|
5066
|
+
name: "agent-blank-completion-none",
|
|
5067
|
+
order: "agent-first",
|
|
5068
|
+
completion: null,
|
|
5069
|
+
agent: " ",
|
|
5070
|
+
expected: undefined,
|
|
5071
|
+
},
|
|
5072
|
+
{
|
|
5073
|
+
name: "agent-none-completion-real",
|
|
5074
|
+
order: "agent-first",
|
|
5075
|
+
completion: "completion",
|
|
5076
|
+
agent: null,
|
|
5077
|
+
expected: "completion",
|
|
5078
|
+
},
|
|
5079
|
+
{
|
|
5080
|
+
name: "agent-none-completion-blank",
|
|
5081
|
+
order: "agent-first",
|
|
5082
|
+
completion: " ",
|
|
5083
|
+
agent: null,
|
|
5084
|
+
expected: undefined,
|
|
5085
|
+
},
|
|
5086
|
+
] as const;
|
|
5087
|
+
for (const testCase of cases) {
|
|
5088
|
+
const cwd = await mkdtemp(path.join(os.tmpdir(), `gjc-skill-terminal-order-${testCase.name}-`));
|
|
5089
|
+
const completion = Promise.withResolvers<unknown>();
|
|
5090
|
+
const completionReconciled = Promise.withResolvers<void>();
|
|
5091
|
+
try {
|
|
5092
|
+
const harness = await invocationHarness(`skill-terminal-order-${testCase.name}`, cwd, {
|
|
5093
|
+
onInvocationCompletionReconciled: kind => {
|
|
5094
|
+
if (kind === "skill") completionReconciled.resolve();
|
|
5095
|
+
},
|
|
5096
|
+
invokeSkill: async (_name, _args, options) => {
|
|
5097
|
+
await options?.onPreflightAcceptCommit?.();
|
|
5098
|
+
return await completion.promise;
|
|
5099
|
+
},
|
|
5100
|
+
});
|
|
5101
|
+
const accepted = await harness.control("skill.invoke", { name: "ralplan" });
|
|
5102
|
+
const selector = {
|
|
5103
|
+
kind: "skill" as const,
|
|
5104
|
+
commandId: accepted.result?.commandId,
|
|
5105
|
+
turnId: accepted.result?.turnId,
|
|
5106
|
+
};
|
|
5107
|
+
await harness.emit("agent_start");
|
|
5108
|
+
const emitAgentEnd = () =>
|
|
5109
|
+
harness.emit("agent_end", {
|
|
5110
|
+
messages: testCase.agent === null ? [] : [{ role: "assistant", content: testCase.agent }],
|
|
5111
|
+
});
|
|
5112
|
+
if (testCase.order === "completion-first") {
|
|
5113
|
+
completion.resolve(testCase.completion);
|
|
5114
|
+
await completionReconciled.promise;
|
|
5115
|
+
await emitAgentEnd();
|
|
5116
|
+
} else {
|
|
5117
|
+
await emitAgentEnd();
|
|
5118
|
+
completion.resolve(testCase.completion);
|
|
5119
|
+
await completionReconciled.promise;
|
|
5120
|
+
}
|
|
5121
|
+
const result = await harness.query("turn.result", selector);
|
|
5122
|
+
const content = (result.result as { content?: { text?: string } } | undefined)?.content;
|
|
5123
|
+
expect(content?.text).toBe(testCase.expected);
|
|
5124
|
+
await harness.stop();
|
|
5125
|
+
} finally {
|
|
5126
|
+
completion.resolve(undefined);
|
|
5127
|
+
await rm(cwd, { recursive: true, force: true });
|
|
5128
|
+
}
|
|
5129
|
+
}
|
|
5130
|
+
}, 30_000);
|
|
4676
5131
|
test("a queued follow-up prompt is not terminalized before the turn runs", async () => {
|
|
4677
5132
|
const cwd = await mkdtemp(path.join(os.tmpdir(), "gjc-terminalize-followup-"));
|
|
4678
5133
|
try {
|
|
@@ -4791,7 +5246,7 @@ describe("post-acceptance invocation terminalization", () => {
|
|
|
4791
5246
|
const rejected = await harness.control("turn.prompt", { text: "hello", clientRef: "preflight-ref" });
|
|
4792
5247
|
expect(rejected.ok).toBe(false);
|
|
4793
5248
|
const status = await harness.query("turn.prompt_status", { clientRef: "preflight-ref" });
|
|
4794
|
-
expect(status.result).toEqual({ status: "unknown" });
|
|
5249
|
+
expect(status.result as Record<string, unknown>).toEqual({ status: "unknown", receiptState: "unknown" });
|
|
4795
5250
|
await harness.stop();
|
|
4796
5251
|
} finally {
|
|
4797
5252
|
await Bun.sleep(50);
|