@cortexkit/opencode-magic-context 0.43.2 → 0.44.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/README.md +18 -2
- package/dist/agents/magic-context-prompt.d.ts +2 -0
- package/dist/agents/magic-context-prompt.d.ts.map +1 -1
- package/dist/{announcement-1a8bvswd.js → announcement-n8hs7kjw.js} +2 -2
- package/dist/config/index.d.ts +13 -0
- package/dist/config/index.d.ts.map +1 -1
- package/dist/config/live-child-output-cap.d.ts +18 -0
- package/dist/config/live-child-output-cap.d.ts.map +1 -0
- package/dist/config/project-security.d.ts +6 -3
- package/dist/config/project-security.d.ts.map +1 -1
- package/dist/config/schema/magic-context.d.ts +28 -1
- package/dist/config/schema/magic-context.d.ts.map +1 -1
- package/dist/{conflict-warning-hook-hyk9y29x.js → conflict-warning-hook-d14ah73v.js} +6 -6
- package/dist/{derive-budgets-b8ybssh9.js → derive-budgets-gd2270s0.js} +3 -3
- package/dist/features/magic-context/compaction-marker.d.ts.map +1 -1
- package/dist/features/magic-context/compartment-embedding.d.ts +3 -2
- package/dist/features/magic-context/compartment-embedding.d.ts.map +1 -1
- package/dist/features/magic-context/compartment-storage.d.ts.map +1 -1
- package/dist/features/magic-context/dreamer/classify.d.ts.map +1 -1
- package/dist/features/magic-context/dreamer/curate-category-rotation.d.ts.map +1 -1
- package/dist/features/magic-context/dreamer/curate-memory-safety.d.ts.map +1 -1
- package/dist/features/magic-context/dreamer/failed-invocation-evidence.d.ts +13 -0
- package/dist/features/magic-context/dreamer/failed-invocation-evidence.d.ts.map +1 -0
- package/dist/features/magic-context/dreamer/hidden-single-shot.d.ts.map +1 -1
- package/dist/features/magic-context/dreamer/lease.d.ts +5 -2
- package/dist/features/magic-context/dreamer/lease.d.ts.map +1 -1
- package/dist/features/magic-context/dreamer/map-memories.d.ts.map +1 -1
- package/dist/features/magic-context/dreamer/memory-claim-safety.d.ts.map +1 -1
- package/dist/features/magic-context/dreamer/module-apply.d.ts +1 -0
- package/dist/features/magic-context/dreamer/module-apply.d.ts.map +1 -1
- package/dist/features/magic-context/dreamer/open-opencode-db.d.ts +2 -0
- package/dist/features/magic-context/dreamer/open-opencode-db.d.ts.map +1 -1
- package/dist/features/magic-context/dreamer/primer-seed.d.ts +11 -5
- package/dist/features/magic-context/dreamer/primer-seed.d.ts.map +1 -1
- package/dist/features/magic-context/dreamer/refresh-primers.d.ts +2 -2
- package/dist/features/magic-context/dreamer/refresh-primers.d.ts.map +1 -1
- package/dist/features/magic-context/dreamer/retrospective-learnings.d.ts.map +1 -1
- package/dist/features/magic-context/dreamer/storage-dream-runs.d.ts +1 -1
- package/dist/features/magic-context/dreamer/storage-dream-runs.d.ts.map +1 -1
- package/dist/features/magic-context/dreamer/task-executor.d.ts.map +1 -1
- package/dist/features/magic-context/dreamer/task-scheduler.d.ts +2 -0
- package/dist/features/magic-context/dreamer/task-scheduler.d.ts.map +1 -1
- package/dist/features/magic-context/dreamer/verify.d.ts +19 -0
- package/dist/features/magic-context/dreamer/verify.d.ts.map +1 -1
- package/dist/features/magic-context/fail-closed-block.d.ts +7 -0
- package/dist/features/magic-context/fail-closed-block.d.ts.map +1 -1
- package/dist/features/magic-context/index.d.ts +1 -0
- package/dist/features/magic-context/index.d.ts.map +1 -1
- package/dist/features/magic-context/install-instance-id.d.ts +33 -0
- package/dist/features/magic-context/install-instance-id.d.ts.map +1 -0
- package/dist/features/magic-context/memory/constants.d.ts.map +1 -1
- package/dist/features/magic-context/memory/embedding-backfill.d.ts.map +1 -1
- package/dist/features/magic-context/memory/project-identity.d.ts +7 -0
- package/dist/features/magic-context/memory/project-identity.d.ts.map +1 -1
- package/dist/features/magic-context/memory/single-store-embedding-drain.d.ts +35 -0
- package/dist/features/magic-context/memory/single-store-embedding-drain.d.ts.map +1 -0
- package/dist/features/magic-context/memory/verification-paths.d.ts.map +1 -1
- package/dist/features/magic-context/message-index.d.ts +23 -0
- package/dist/features/magic-context/message-index.d.ts.map +1 -1
- package/dist/features/magic-context/migrations.d.ts +7 -0
- package/dist/features/magic-context/migrations.d.ts.map +1 -1
- package/dist/features/magic-context/mural/mural-font.generated.d.ts.map +1 -1
- package/dist/features/magic-context/overflow-detection.d.ts +31 -8
- package/dist/features/magic-context/overflow-detection.d.ts.map +1 -1
- package/dist/features/magic-context/project-embedding-registry.d.ts +40 -1
- package/dist/features/magic-context/project-embedding-registry.d.ts.map +1 -1
- package/dist/features/magic-context/search.d.ts.map +1 -1
- package/dist/features/magic-context/session-cache-ttl.d.ts +10 -0
- package/dist/features/magic-context/session-cache-ttl.d.ts.map +1 -0
- package/dist/features/magic-context/session-decision-calibration.d.ts.map +1 -1
- package/dist/features/magic-context/storage-db.d.ts +1 -1
- package/dist/features/magic-context/storage-db.d.ts.map +1 -1
- package/dist/features/magic-context/storage-meta-persisted.d.ts +20 -5
- package/dist/features/magic-context/storage-meta-persisted.d.ts.map +1 -1
- package/dist/features/magic-context/storage-meta.d.ts +1 -1
- package/dist/features/magic-context/storage-meta.d.ts.map +1 -1
- package/dist/features/magic-context/storage-notes.d.ts +1 -0
- package/dist/features/magic-context/storage-notes.d.ts.map +1 -1
- package/dist/features/magic-context/storage-primers.d.ts.map +1 -1
- package/dist/features/magic-context/storage-tags.d.ts.map +1 -1
- package/dist/features/magic-context/storage-unavailable-reason.d.ts +17 -0
- package/dist/features/magic-context/storage-unavailable-reason.d.ts.map +1 -0
- package/dist/features/magic-context/storage.d.ts +1 -1
- package/dist/features/magic-context/storage.d.ts.map +1 -1
- package/dist/features/magic-context/store-generation-rebase.d.ts +33 -1
- package/dist/features/magic-context/store-generation-rebase.d.ts.map +1 -1
- package/dist/features/magic-context/subagent-token-capture.d.ts.map +1 -1
- package/dist/features/magic-context/transform-decision-log.d.ts +1 -1
- package/dist/features/magic-context/transform-decision-log.d.ts.map +1 -1
- package/dist/features/magic-context/types.d.ts +8 -3
- package/dist/features/magic-context/types.d.ts.map +1 -1
- package/dist/features/magic-context/user-memory/storage-user-memory.d.ts.map +1 -1
- package/dist/hooks/magic-context/apply-operations.d.ts +105 -9
- package/dist/hooks/magic-context/apply-operations.d.ts.map +1 -1
- package/dist/hooks/magic-context/auto-search-runner.d.ts.map +1 -1
- package/dist/hooks/magic-context/compartment-parser.d.ts +2 -0
- package/dist/hooks/magic-context/compartment-parser.d.ts.map +1 -1
- package/dist/hooks/magic-context/compartment-runner-historian.d.ts +1 -0
- package/dist/hooks/magic-context/compartment-runner-historian.d.ts.map +1 -1
- package/dist/hooks/magic-context/compartment-runner-incremental.d.ts.map +1 -1
- package/dist/hooks/magic-context/compartment-runner-partial-recomp.d.ts.map +1 -1
- package/dist/hooks/magic-context/compartment-runner-recomp.d.ts.map +1 -1
- package/dist/hooks/magic-context/compartment-runner-types.d.ts +2 -0
- package/dist/hooks/magic-context/compartment-runner-types.d.ts.map +1 -1
- package/dist/hooks/magic-context/compartment-runner-validation.d.ts.map +1 -1
- package/dist/hooks/magic-context/context-limit-resolution.d.ts +31 -0
- package/dist/hooks/magic-context/context-limit-resolution.d.ts.map +1 -0
- package/dist/hooks/magic-context/embed-history-runner.d.ts +0 -1
- package/dist/hooks/magic-context/embed-history-runner.d.ts.map +1 -1
- package/dist/hooks/magic-context/event-handler.d.ts.map +1 -1
- package/dist/hooks/magic-context/event-resolvers.d.ts.map +1 -1
- package/dist/hooks/magic-context/execute-status.d.ts.map +1 -1
- package/dist/hooks/magic-context/final-wire-token-estimate.d.ts.map +1 -1
- package/dist/hooks/magic-context/format-embed-status.d.ts.map +1 -1
- package/dist/hooks/magic-context/heuristic-cleanup.d.ts.map +1 -1
- package/dist/hooks/magic-context/historian-host-runner.d.ts +233 -0
- package/dist/hooks/magic-context/historian-host-runner.d.ts.map +1 -0
- package/dist/hooks/magic-context/hook.d.ts +1 -3
- package/dist/hooks/magic-context/hook.d.ts.map +1 -1
- package/dist/hooks/magic-context/image-token-estimate.d.ts +9 -0
- package/dist/hooks/magic-context/image-token-estimate.d.ts.map +1 -1
- package/dist/hooks/magic-context/inject-compartments.d.ts +60 -1
- package/dist/hooks/magic-context/inject-compartments.d.ts.map +1 -1
- package/dist/hooks/magic-context/lkg-slot.d.ts +6 -0
- package/dist/hooks/magic-context/lkg-slot.d.ts.map +1 -1
- package/dist/hooks/magic-context/module-client.d.ts +21 -0
- package/dist/hooks/magic-context/module-client.d.ts.map +1 -0
- package/dist/hooks/magic-context/module-state-sync.d.ts +6 -2
- package/dist/hooks/magic-context/module-state-sync.d.ts.map +1 -1
- package/dist/hooks/magic-context/module-tool-backends.d.ts +47 -0
- package/dist/hooks/magic-context/module-tool-backends.d.ts.map +1 -0
- package/dist/hooks/magic-context/module-transport.d.ts +1 -1
- package/dist/hooks/magic-context/module-transport.d.ts.map +1 -1
- package/dist/hooks/magic-context/opencode-v1-store-fixture.d.ts +12 -0
- package/dist/hooks/magic-context/opencode-v1-store-fixture.d.ts.map +1 -0
- package/dist/hooks/magic-context/persist-filtered-noise.d.ts.map +1 -1
- package/dist/hooks/magic-context/read-session-chunk.d.ts +28 -0
- package/dist/hooks/magic-context/read-session-chunk.d.ts.map +1 -1
- package/dist/hooks/magic-context/read-session-db.d.ts +5 -1
- package/dist/hooks/magic-context/read-session-db.d.ts.map +1 -1
- package/dist/hooks/magic-context/read-session-raw.d.ts +10 -0
- package/dist/hooks/magic-context/read-session-raw.d.ts.map +1 -1
- package/dist/hooks/magic-context/rust-mode-transform.d.ts +35 -0
- package/dist/hooks/magic-context/rust-mode-transform.d.ts.map +1 -1
- package/dist/hooks/magic-context/rust-refusal-recovery.d.ts +18 -0
- package/dist/hooks/magic-context/rust-refusal-recovery.d.ts.map +1 -1
- package/dist/hooks/magic-context/rust-served-m0.d.ts +20 -0
- package/dist/hooks/magic-context/rust-served-m0.d.ts.map +1 -0
- package/dist/hooks/magic-context/store-ahead-refusal.d.ts +40 -0
- package/dist/hooks/magic-context/store-ahead-refusal.d.ts.map +1 -0
- package/dist/hooks/magic-context/strip-content.d.ts +5 -5
- package/dist/hooks/magic-context/strip-content.d.ts.map +1 -1
- package/dist/hooks/magic-context/system-prompt-hash.d.ts.map +1 -1
- package/dist/hooks/magic-context/tag-messages.d.ts +19 -0
- package/dist/hooks/magic-context/tag-messages.d.ts.map +1 -1
- package/dist/hooks/magic-context/tail-hygiene-walk.d.ts.map +1 -1
- package/dist/hooks/magic-context/tool-drop-target.d.ts +14 -0
- package/dist/hooks/magic-context/tool-drop-target.d.ts.map +1 -1
- package/dist/hooks/magic-context/tool-input-size.d.ts +21 -0
- package/dist/hooks/magic-context/tool-input-size.d.ts.map +1 -0
- package/dist/hooks/magic-context/tool-sweep-policy.d.ts.map +1 -1
- package/dist/hooks/magic-context/transform-postprocess-phase.d.ts +124 -11
- package/dist/hooks/magic-context/transform-postprocess-phase.d.ts.map +1 -1
- package/dist/hooks/magic-context/transform.d.ts +14 -0
- package/dist/hooks/magic-context/transform.d.ts.map +1 -1
- package/dist/{index-pa67brzn.js → index-0rp6pj54.js} +3 -3
- package/dist/{index-4b30nf6z.js → index-32b096cc.js} +1 -1
- package/dist/{index-wgzyeyf5.js → index-3bygzk2d.js} +23 -7
- package/dist/{index-jgnag7sa.js → index-a9vbtdzv.js} +4 -4
- package/dist/{index-bmzt6xgy.js → index-cz88kw0n.js} +13 -3
- package/dist/{index-6j8th2hd.js → index-d107az47.js} +2 -2
- package/dist/{index-g6hpfkqa.js → index-jtdrvmcn.js} +3290 -286
- package/dist/{index-9r1cbh9e.js → index-kr5qehbd.js} +1 -1
- package/dist/{index-febg06dy.js → index-kwe03yde.js} +1 -1
- package/dist/{index-tdrv6nd0.js → index-s6b7h12g.js} +352 -71
- package/dist/{index-6z9g64js.js → index-s70wtd8k.js} +4 -1
- package/dist/{index-0ccmyzbj.js → index-stb5fg12.js} +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5230 -5663
- package/dist/{message-time-backfill-0ccvyk9n.js → message-time-backfill-q374fxc5.js} +4 -4
- package/dist/plugin/dream-timer.d.ts +2 -0
- package/dist/plugin/dream-timer.d.ts.map +1 -1
- package/dist/plugin/hooks/create-session-hooks.d.ts.map +1 -1
- package/dist/plugin/messages-transform.d.ts.map +1 -1
- package/dist/plugin/native-compaction-guard.d.ts +22 -0
- package/dist/plugin/native-compaction-guard.d.ts.map +1 -0
- package/dist/plugin/rpc-handlers.d.ts +11 -0
- package/dist/plugin/rpc-handlers.d.ts.map +1 -1
- package/dist/{read-session-chunk-53a7qbrh.js → read-session-chunk-fkdv2g5b.js} +8 -4
- package/dist/{recomp-orchestrator-adsgqgse.js → recomp-orchestrator-k4h5n0p3.js} +9 -9
- package/dist/{safe-notification-target-vrkjrbgb.js → safe-notification-target-50gm9c58.js} +3 -3
- package/dist/{send-session-notification-e28r4xx2.js → send-session-notification-g2k03zyn.js} +5 -5
- package/dist/shared/cache-ttl-display.d.ts +3 -1
- package/dist/shared/cache-ttl-display.d.ts.map +1 -1
- package/dist/shared/harness.d.ts +11 -0
- package/dist/shared/harness.d.ts.map +1 -1
- package/dist/shared/index.d.ts +1 -0
- package/dist/shared/index.d.ts.map +1 -1
- package/dist/shared/logger.d.ts.map +1 -1
- package/dist/shared/model-cache-ttl.d.ts +9 -0
- package/dist/shared/model-cache-ttl.d.ts.map +1 -0
- package/dist/shared/model-suggestion-retry.d.ts +13 -1
- package/dist/shared/model-suggestion-retry.d.ts.map +1 -1
- package/dist/shared/prompt-async-transport.d.ts +25 -0
- package/dist/shared/prompt-async-transport.d.ts.map +1 -0
- package/dist/shared/rpc-server.d.ts +1 -0
- package/dist/shared/rpc-server.d.ts.map +1 -1
- package/dist/shared/rpc-types.d.ts +17 -1
- package/dist/shared/rpc-types.d.ts.map +1 -1
- package/dist/shared/rpc-utils.d.ts +3 -0
- package/dist/shared/rpc-utils.d.ts.map +1 -1
- package/dist/shared/sqlite-write-locker-test-support.d.ts +7 -0
- package/dist/shared/sqlite-write-locker-test-support.d.ts.map +1 -0
- package/dist/shared/status-summary.d.ts +9 -1
- package/dist/shared/status-summary.d.ts.map +1 -1
- package/dist/shared/status-view.d.ts +1 -1
- package/dist/shared/status-view.d.ts.map +1 -1
- package/dist/shared/tag-transcript.d.ts.map +1 -1
- package/dist/shared/user-facing-codes.d.ts +10 -5
- package/dist/shared/user-facing-codes.d.ts.map +1 -1
- package/dist/shared/window-geometry.d.ts +6 -3
- package/dist/shared/window-geometry.d.ts.map +1 -1
- package/dist/tools/ctx-expand/render.d.ts.map +1 -1
- package/dist/tools/ctx-memory/tools.d.ts.map +1 -1
- package/dist/tools/ctx-note/tools.d.ts.map +1 -1
- package/dist/tools/ctx-reduce/tools.d.ts.map +1 -1
- package/dist/tools/ctx-search/tools.d.ts.map +1 -1
- package/dist/v2/fold/boundary.d.ts +61 -0
- package/dist/v2/fold/boundary.d.ts.map +1 -0
- package/dist/v2/fold/restore.d.ts.map +1 -1
- package/dist/v2/hidden-completion.d.ts +16 -3
- package/dist/v2/hidden-completion.d.ts.map +1 -1
- package/dist/v2/hooks/context.d.ts +45 -14
- package/dist/v2/hooks/context.d.ts.map +1 -1
- package/dist/v2/hooks/hidden-child.d.ts +6 -0
- package/dist/v2/hooks/hidden-child.d.ts.map +1 -1
- package/dist/v2/hooks/payload.d.ts.map +1 -1
- package/dist/v2/hooks/rpc-live-state.d.ts +1 -1
- package/dist/v2/hooks/rpc-live-state.d.ts.map +1 -1
- package/dist/v2/hooks/rust-mode.d.ts +47 -0
- package/dist/v2/hooks/rust-mode.d.ts.map +1 -0
- package/dist/v2/hooks/session-project-backfill.d.ts +15 -0
- package/dist/v2/hooks/session-project-backfill.d.ts.map +1 -0
- package/dist/v2/hooks/storage-gate.d.ts +42 -0
- package/dist/v2/hooks/storage-gate.d.ts.map +1 -0
- package/dist/v2/hooks/storage-notice.d.ts +29 -0
- package/dist/v2/hooks/storage-notice.d.ts.map +1 -0
- package/dist/v2/hooks/store.d.ts +16 -0
- package/dist/v2/hooks/store.d.ts.map +1 -1
- package/dist/v2/hooks/tools.d.ts +11 -2
- package/dist/v2/hooks/tools.d.ts.map +1 -1
- package/dist/v2/hooks/types.d.ts +4 -0
- package/dist/v2/hooks/types.d.ts.map +1 -1
- package/dist/v2/hooks/usage-persist.d.ts +31 -0
- package/dist/v2/hooks/usage-persist.d.ts.map +1 -0
- package/dist/v2/server.js +27201 -22695
- package/dist/v2/store-reader.d.ts +9 -0
- package/dist/v2/store-reader.d.ts.map +1 -1
- package/dist/wrapup-orchestrator-35y56636.js +18 -0
- package/package.json +1 -1
- package/src/agents/magic-context-prompt.test.ts +125 -0
- package/src/agents/magic-context-prompt.ts +15 -10
- package/src/config/index.ts +40 -0
- package/src/config/live-child-output-cap.test.ts +75 -0
- package/src/config/live-child-output-cap.ts +70 -0
- package/src/config/live-run-config.test.ts +24 -11
- package/src/config/misplaced-agent-model.test.ts +61 -0
- package/src/config/project-security.test.ts +17 -0
- package/src/config/project-security.ts +28 -4
- package/src/config/schema/magic-context.test.ts +16 -1
- package/src/config/schema/magic-context.ts +35 -6
- package/src/shared/cache-ttl-display.ts +14 -20
- package/src/shared/harness.ts +14 -0
- package/src/shared/index.ts +1 -0
- package/src/shared/logger.test.ts +63 -2
- package/src/shared/logger.ts +23 -5
- package/src/shared/model-cache-ttl.test.ts +40 -0
- package/src/shared/model-cache-ttl.ts +58 -0
- package/src/shared/model-suggestion-retry.test.ts +115 -4
- package/src/shared/model-suggestion-retry.ts +57 -12
- package/src/shared/prompt-async-transport.test.ts +217 -0
- package/src/shared/prompt-async-transport.ts +242 -0
- package/src/shared/prompt-surface-a1-golden.md +17 -17
- package/src/shared/rpc-server.ts +13 -1
- package/src/shared/rpc-types.ts +18 -1
- package/src/shared/rpc-utils.ts +28 -0
- package/src/shared/sqlite-transaction-mode-guard.test.ts +198 -0
- package/src/shared/sqlite-write-locker-test-support.ts +62 -0
- package/src/shared/status-detail-text.test.ts +23 -0
- package/src/shared/status-summary.ts +25 -1
- package/src/shared/status-view.ts +1 -1
- package/src/shared/tag-transcript.ts +82 -19
- package/src/shared/user-facing-codes.ts +12 -5
- package/src/shared/window-geometry.ts +6 -3
- package/dist/v2/hooks/provider-admission.d.ts +0 -22
- package/dist/v2/hooks/provider-admission.d.ts.map +0 -1
- package/dist/wrapup-orchestrator-8xzbpvm8.js +0 -18
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { describe, expect, mock, test } from "bun:test";
|
|
2
2
|
|
|
3
|
+
import { HiddenAgentStepLimit } from "../v2/hooks/hidden-child";
|
|
3
4
|
import {
|
|
5
|
+
getPromptFailureDetail,
|
|
4
6
|
promptSyncWithModelSuggestionRetry,
|
|
5
7
|
promptSyncWithValidatedOutputRetry,
|
|
6
8
|
} from "./model-suggestion-retry";
|
|
@@ -158,6 +160,18 @@ describe("promptSyncWithModelSuggestionRetry", () => {
|
|
|
158
160
|
expect(prompt).toHaveBeenCalledTimes(0);
|
|
159
161
|
});
|
|
160
162
|
|
|
163
|
+
test("pre-aborted lease signal carries its loss reason into the prompt error", async () => {
|
|
164
|
+
const controller = new AbortController();
|
|
165
|
+
controller.abort(new Error("lease_lost: taken by holder-b"));
|
|
166
|
+
const prompt = mock(async () => {});
|
|
167
|
+
await expect(
|
|
168
|
+
promptSyncWithModelSuggestionRetry(createClient(prompt), createArgs(), {
|
|
169
|
+
signal: controller.signal,
|
|
170
|
+
}),
|
|
171
|
+
).rejects.toThrow("prompt aborted by external signal: lease_lost: taken by holder-b");
|
|
172
|
+
expect(prompt).not.toHaveBeenCalled();
|
|
173
|
+
});
|
|
174
|
+
|
|
161
175
|
test("AbortError name short-circuits", async () => {
|
|
162
176
|
const abortError = new Error("aborted by provider");
|
|
163
177
|
abortError.name = "AbortError";
|
|
@@ -280,10 +294,10 @@ describe("promptSyncWithModelSuggestionRetry", () => {
|
|
|
280
294
|
const abort = mock(async () => ({}));
|
|
281
295
|
const client = createClient(prompt as never, abort);
|
|
282
296
|
|
|
283
|
-
setTimeout(() => controller.abort(), 10);
|
|
297
|
+
setTimeout(() => controller.abort(new Error("lease_lost: taken by holder-b")), 10);
|
|
284
298
|
await expect(
|
|
285
299
|
promptSyncWithModelSuggestionRetry(client, createArgs(), { signal: controller.signal }),
|
|
286
|
-
).rejects.toThrow(
|
|
300
|
+
).rejects.toThrow("prompt aborted by external signal: lease_lost: taken by holder-b");
|
|
287
301
|
expect(abort).toHaveBeenCalledTimes(1);
|
|
288
302
|
expect((abort.mock.calls[0]?.[0] as { path: { id: string } }).path.id).toBe("ses-test");
|
|
289
303
|
});
|
|
@@ -393,6 +407,28 @@ describe("promptSyncWithModelSuggestionRetry", () => {
|
|
|
393
407
|
});
|
|
394
408
|
|
|
395
409
|
describe("promptSyncWithValidatedOutputRetry", () => {
|
|
410
|
+
test("records a step cap as step_limit without a provider error", async () => {
|
|
411
|
+
const prompt = mock(async () => {
|
|
412
|
+
throw new HiddenAgentStepLimit("dreamer-retrospective", 40);
|
|
413
|
+
});
|
|
414
|
+
const client = createClient(prompt);
|
|
415
|
+
let caught: unknown;
|
|
416
|
+
try {
|
|
417
|
+
await promptSyncWithValidatedOutputRetry(client, createArgs(), {
|
|
418
|
+
fetchOutput: async () => "unused",
|
|
419
|
+
validateOutput: (output) => output,
|
|
420
|
+
});
|
|
421
|
+
} catch (error) {
|
|
422
|
+
caught = error;
|
|
423
|
+
}
|
|
424
|
+
expect(caught).toBeInstanceOf(HiddenAgentStepLimit);
|
|
425
|
+
expect(getPromptFailureDetail(caught)).toMatchObject({
|
|
426
|
+
failureClass: "step_limit",
|
|
427
|
+
providerError: null,
|
|
428
|
+
});
|
|
429
|
+
expect(prompt).toHaveBeenCalledTimes(1);
|
|
430
|
+
});
|
|
431
|
+
|
|
396
432
|
test("surfaces a host-recorded assistant refusal even when the row has no text", async () => {
|
|
397
433
|
const client = createClient(mock(async () => ({})));
|
|
398
434
|
const refusal =
|
|
@@ -517,7 +553,7 @@ describe("promptSyncWithValidatedOutputRetry", () => {
|
|
|
517
553
|
});
|
|
518
554
|
});
|
|
519
555
|
|
|
520
|
-
test("all empty outputs surface the
|
|
556
|
+
test("all empty outputs surface the last validation failure and every attempted model", async () => {
|
|
521
557
|
const prompt = mock(async () => ({}));
|
|
522
558
|
const messages = mock(async () => "");
|
|
523
559
|
const client = createClient(prompt, undefined, messages);
|
|
@@ -533,9 +569,84 @@ describe("promptSyncWithValidatedOutputRetry", () => {
|
|
|
533
569
|
return output.trim();
|
|
534
570
|
},
|
|
535
571
|
}),
|
|
536
|
-
).rejects.toThrow(
|
|
572
|
+
).rejects.toThrow(
|
|
573
|
+
/All models exhausted \(primary, anthropic\/claude-sonnet-4-6\): empty output from anthropic\/claude-sonnet-4-6/,
|
|
574
|
+
);
|
|
537
575
|
|
|
538
576
|
expect(prompt).toHaveBeenCalledTimes(2);
|
|
539
577
|
expect(messages).toHaveBeenCalledTimes(2);
|
|
540
578
|
});
|
|
541
579
|
});
|
|
580
|
+
|
|
581
|
+
/**
|
|
582
|
+
* Bun's fetch rejects with this exact object when its default request timer
|
|
583
|
+
* fires on a plugin SDK request (the name is "TimeoutError", legacy code 23).
|
|
584
|
+
* The dreamer ledger shows it as `TimeoutError message="The operation timed out." code=23`.
|
|
585
|
+
*/
|
|
586
|
+
function hostFetchTimeout(): Error {
|
|
587
|
+
return new DOMException("The operation timed out.", "TimeoutError") as unknown as Error;
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
describe("host fetch TimeoutError is a timeout", () => {
|
|
591
|
+
test("validated retry stops the chain, aborts the child, and reports provider_timeout", async () => {
|
|
592
|
+
const prompt = mock(async () => {
|
|
593
|
+
throw hostFetchTimeout();
|
|
594
|
+
});
|
|
595
|
+
const abort = mock(async () => ({}));
|
|
596
|
+
const client = createClient(prompt, abort);
|
|
597
|
+
|
|
598
|
+
let caught: unknown;
|
|
599
|
+
try {
|
|
600
|
+
await promptSyncWithValidatedOutputRetry(client, createArgs(), {
|
|
601
|
+
fallbackModels: ["anthropic/claude-sonnet-4-6", "google/gemini-3-flash"],
|
|
602
|
+
fetchOutput: async () => "unused",
|
|
603
|
+
validateOutput: (output: string) => output,
|
|
604
|
+
});
|
|
605
|
+
} catch (error) {
|
|
606
|
+
caught = error;
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
expect((caught as Error | undefined)?.name).toBe("TimeoutError");
|
|
610
|
+
// Another model would be sent into the same still-running child session and
|
|
611
|
+
// hit the same host timer, so the chain must stop at the first attempt.
|
|
612
|
+
expect(prompt).toHaveBeenCalledTimes(1);
|
|
613
|
+
expect(getPromptFailureDetail(caught)?.failureClass).toBe("provider_timeout");
|
|
614
|
+
// The host loop keeps running after the client-side timer fires unless the
|
|
615
|
+
// child is aborted explicitly.
|
|
616
|
+
expect(abort).toHaveBeenCalledTimes(1);
|
|
617
|
+
expect(abort.mock.calls[0]?.[0]).toEqual({ path: { id: "ses-test" } });
|
|
618
|
+
});
|
|
619
|
+
|
|
620
|
+
test("unvalidated retry stops the chain and aborts the child", async () => {
|
|
621
|
+
const prompt = mock(async () => {
|
|
622
|
+
throw hostFetchTimeout();
|
|
623
|
+
});
|
|
624
|
+
const abort = mock(async () => ({}));
|
|
625
|
+
const client = createClient(prompt, abort);
|
|
626
|
+
|
|
627
|
+
await expect(
|
|
628
|
+
promptSyncWithModelSuggestionRetry(client, createArgs(), {
|
|
629
|
+
fallbackModels: ["anthropic/claude-sonnet-4-6"],
|
|
630
|
+
}),
|
|
631
|
+
).rejects.toThrow("The operation timed out.");
|
|
632
|
+
|
|
633
|
+
expect(prompt).toHaveBeenCalledTimes(1);
|
|
634
|
+
expect(abort).toHaveBeenCalledTimes(1);
|
|
635
|
+
});
|
|
636
|
+
|
|
637
|
+
test("an ordinary provider error still falls back and does not abort the child", async () => {
|
|
638
|
+
const prompt = mock(async () => {
|
|
639
|
+
if (prompt.mock.calls.length === 1) throw new Error("upstream 502");
|
|
640
|
+
return {};
|
|
641
|
+
});
|
|
642
|
+
const abort = mock(async () => ({}));
|
|
643
|
+
const client = createClient(prompt, abort);
|
|
644
|
+
|
|
645
|
+
await promptSyncWithModelSuggestionRetry(client, createArgs(), {
|
|
646
|
+
fallbackModels: ["anthropic/claude-sonnet-4-6"],
|
|
647
|
+
});
|
|
648
|
+
|
|
649
|
+
expect(prompt).toHaveBeenCalledTimes(2);
|
|
650
|
+
expect(abort).not.toHaveBeenCalled();
|
|
651
|
+
});
|
|
652
|
+
});
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { createOpencodeClient } from "@opencode-ai/sdk";
|
|
2
2
|
|
|
3
3
|
import { detectOverflow } from "../features/magic-context/overflow-detection";
|
|
4
|
+
import { HiddenAgentStepLimit } from "../v2/hooks/hidden-child";
|
|
4
5
|
import {
|
|
5
6
|
extractLatestAssistantFailure,
|
|
6
7
|
extractLatestAssistantText,
|
|
@@ -113,6 +114,7 @@ export interface ValidatedPromptRetryResult<TOutput, TValidated> {
|
|
|
113
114
|
export type PromptFailureClass =
|
|
114
115
|
| "provider_timeout"
|
|
115
116
|
| "provider_error"
|
|
117
|
+
| "step_limit"
|
|
116
118
|
| "empty_completion"
|
|
117
119
|
| "no_models"
|
|
118
120
|
| "child_aborted"
|
|
@@ -131,6 +133,29 @@ export interface PromptFailureDetail {
|
|
|
131
133
|
|
|
132
134
|
const promptFailureDetails = new WeakMap<object, PromptFailureDetail>();
|
|
133
135
|
|
|
136
|
+
/**
|
|
137
|
+
* True for a request timer that fired inside the host client rather than ours.
|
|
138
|
+
* Bun's fetch rejects with a DOMException named "TimeoutError" ("The operation
|
|
139
|
+
* timed out.", legacy code 23) when its default per-request timer expires, and
|
|
140
|
+
* `AbortSignal.timeout` produces the same shape. Some OpenCode 1 builds hand
|
|
141
|
+
* plugins an SDK client whose fetch keeps Bun's default timer (about five to six
|
|
142
|
+
* minutes), so a synchronous `session.prompt` that stays open for a whole agent
|
|
143
|
+
* loop fails this way while the host keeps running the child session.
|
|
144
|
+
*/
|
|
145
|
+
export function isHostTimeoutError(error: unknown): boolean {
|
|
146
|
+
return (
|
|
147
|
+
error !== null &&
|
|
148
|
+
typeof error === "object" &&
|
|
149
|
+
(error as { name?: unknown }).name === "TimeoutError"
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** Our own slice expiry (see `promptWithTimeout`) or a host request timer. */
|
|
154
|
+
export function isPromptTimeoutError(error: unknown): boolean {
|
|
155
|
+
if (isHostTimeoutError(error)) return true;
|
|
156
|
+
return error instanceof Error && /^prompt timed out after \d+ms$/.test(error.message);
|
|
157
|
+
}
|
|
158
|
+
|
|
134
159
|
export function getPromptFailureDetail(error: unknown): PromptFailureDetail | null {
|
|
135
160
|
return error !== null && typeof error === "object"
|
|
136
161
|
? (promptFailureDetails.get(error) ?? null)
|
|
@@ -204,6 +229,11 @@ export function parseModelSuggestion(error: unknown): ModelSuggestionInfo | null
|
|
|
204
229
|
};
|
|
205
230
|
}
|
|
206
231
|
|
|
232
|
+
function externalAbortMessage(signal: AbortSignal): string {
|
|
233
|
+
const reason = signal.reason;
|
|
234
|
+
return `prompt aborted by external signal${reason instanceof Error && /^lease_(?:lost|expired):/.test(reason.message) ? `: ${reason.message}` : ""}`;
|
|
235
|
+
}
|
|
236
|
+
|
|
207
237
|
async function promptWithTimeout(
|
|
208
238
|
client: Client,
|
|
209
239
|
args: PromptArgs,
|
|
@@ -218,7 +248,7 @@ async function promptWithTimeout(
|
|
|
218
248
|
// and avoids one wasted upstream `client.session.prompt` round-trip
|
|
219
249
|
// before `isNonRetryable` catches the cancellation at the chain loop.
|
|
220
250
|
if (signal?.aborted) {
|
|
221
|
-
throw new Error(
|
|
251
|
+
throw new Error(externalAbortMessage(signal));
|
|
222
252
|
}
|
|
223
253
|
const controller = new AbortController();
|
|
224
254
|
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
@@ -242,7 +272,7 @@ async function promptWithTimeout(
|
|
|
242
272
|
if (!transport || transport.childSessionId) {
|
|
243
273
|
await abortChildRun(client, transport?.childSessionId ?? args.path.id);
|
|
244
274
|
}
|
|
245
|
-
throw new Error(
|
|
275
|
+
throw new Error(externalAbortMessage(signal));
|
|
246
276
|
}
|
|
247
277
|
if (controller.signal.aborted) {
|
|
248
278
|
// Our timeout fired. Same problem: abort the server-side run loop, not
|
|
@@ -253,6 +283,15 @@ async function promptWithTimeout(
|
|
|
253
283
|
}
|
|
254
284
|
throw new Error(`prompt timed out after ${timeoutMs}ms`);
|
|
255
285
|
}
|
|
286
|
+
if (isHostTimeoutError(error)) {
|
|
287
|
+
// The host client's own request timer fired. Only our side of the
|
|
288
|
+
// request ended: the child's run loop is still going on the server and
|
|
289
|
+
// would keep calling the model, so stop it the same way as above. The
|
|
290
|
+
// original error is kept so the ledger still shows which timer fired.
|
|
291
|
+
if (!transport || transport.childSessionId) {
|
|
292
|
+
await abortChildRun(client, transport?.childSessionId ?? args.path.id);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
256
295
|
throw error;
|
|
257
296
|
} finally {
|
|
258
297
|
clearTimeout(timeout);
|
|
@@ -266,7 +305,7 @@ async function promptWithTimeout(
|
|
|
266
305
|
}
|
|
267
306
|
throw new Error(
|
|
268
307
|
signal?.aborted
|
|
269
|
-
?
|
|
308
|
+
? externalAbortMessage(signal)
|
|
270
309
|
: `prompt timed out after ${timeoutMs}ms`,
|
|
271
310
|
);
|
|
272
311
|
}
|
|
@@ -312,7 +351,7 @@ async function abortChildRun(client: Client, sessionId: string): Promise<void> {
|
|
|
312
351
|
* different model.
|
|
313
352
|
*/
|
|
314
353
|
function isNonRetryable(error: unknown, externalSignal?: AbortSignal): boolean {
|
|
315
|
-
if (externalSignal?.aborted) return true;
|
|
354
|
+
if (externalSignal?.aborted || error instanceof HiddenAgentStepLimit) return true;
|
|
316
355
|
|
|
317
356
|
if (error instanceof Error) {
|
|
318
357
|
if (error.name === "AbortError") return true;
|
|
@@ -321,6 +360,9 @@ function isNonRetryable(error: unknown, externalSignal?: AbortSignal): boolean {
|
|
|
321
360
|
if (error.message === "prompt aborted by external signal") return true;
|
|
322
361
|
if (/^prompt timed out after \d+ms$/.test(error.message)) return true;
|
|
323
362
|
}
|
|
363
|
+
// A host request timer is a timeout too. Retrying would send the next model
|
|
364
|
+
// into the same child session, where it meets the same timer.
|
|
365
|
+
if (isHostTimeoutError(error)) return true;
|
|
324
366
|
|
|
325
367
|
if (detectOverflow(error).isOverflow) return true;
|
|
326
368
|
|
|
@@ -350,10 +392,13 @@ function classifyPromptFailure(
|
|
|
350
392
|
externalSignal?: AbortSignal,
|
|
351
393
|
): PromptFailureClass {
|
|
352
394
|
const message = extractMessage(error);
|
|
395
|
+
if (error instanceof HiddenAgentStepLimit) return "step_limit";
|
|
353
396
|
if (externalSignal?.aborted || message === "prompt aborted by external signal") {
|
|
354
397
|
return "child_aborted";
|
|
355
398
|
}
|
|
356
|
-
if (/^prompt timed out after \d+ms$/.test(message))
|
|
399
|
+
if (/^prompt timed out after \d+ms$/.test(message) || isHostTimeoutError(error)) {
|
|
400
|
+
return "provider_timeout";
|
|
401
|
+
}
|
|
357
402
|
if (phase === "validation") {
|
|
358
403
|
if (/returned no (?:assistant )?output|no assistant output/i.test(message)) {
|
|
359
404
|
return "empty_completion";
|
|
@@ -764,11 +809,11 @@ export async function promptSyncWithValidatedOutputRetry<TOutput, TValidated = T
|
|
|
764
809
|
log(
|
|
765
810
|
`[${callContext}] all models exhausted; tried: ${failedAttempts.map((failure) => failure.attempt.label).join(", ")}; original error: ${shortErr(firstError)}; last error: ${shortErr(lastError)}`,
|
|
766
811
|
);
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
);
|
|
812
|
+
const cause = lastError ?? firstError;
|
|
813
|
+
const exhausted =
|
|
814
|
+
cause instanceof Error
|
|
815
|
+
? cause
|
|
816
|
+
: new Error(String(cause ?? "All fallback models failed validation"));
|
|
817
|
+
exhausted.message = `All models exhausted (${failedAttempts.map((failure) => failure.attempt.label).join(", ")}): ${exhausted.message}`;
|
|
818
|
+
throwWithPromptFailure(exhausted, failedAttempts, args, timeoutMs, options.transport);
|
|
774
819
|
}
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
import { describe, expect, mock, test } from "bun:test";
|
|
2
|
+
|
|
3
|
+
import { promptSyncWithValidatedOutputRetry } from "./model-suggestion-retry";
|
|
4
|
+
import {
|
|
5
|
+
createPromptAsyncTransport,
|
|
6
|
+
promptAsyncAndWaitForIdle,
|
|
7
|
+
supportsPromptAsync,
|
|
8
|
+
} from "./prompt-async-transport";
|
|
9
|
+
|
|
10
|
+
type Message = {
|
|
11
|
+
info: {
|
|
12
|
+
id: string;
|
|
13
|
+
role: string;
|
|
14
|
+
time: { created: number; completed?: number };
|
|
15
|
+
finish?: string;
|
|
16
|
+
error?: unknown;
|
|
17
|
+
};
|
|
18
|
+
parts: Array<{ type: string; text?: string }>;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* A host double for one child session. `plan` describes what each status poll
|
|
23
|
+
* after the prompt sees: a busy tick, or the moment the run settles with a
|
|
24
|
+
* final assistant message.
|
|
25
|
+
*/
|
|
26
|
+
function asyncHost(options: {
|
|
27
|
+
plan: Array<"busy" | "idle-pending" | "settle">;
|
|
28
|
+
initialMessages?: Message[];
|
|
29
|
+
answer?: string;
|
|
30
|
+
}) {
|
|
31
|
+
const messages: Message[] = [...(options.initialMessages ?? [])];
|
|
32
|
+
let polls = 0;
|
|
33
|
+
let prompted = false;
|
|
34
|
+
const promptAsync = mock(async (_request: unknown) => {
|
|
35
|
+
prompted = true;
|
|
36
|
+
messages.push({
|
|
37
|
+
info: { id: `msg_user_${messages.length}`, role: "user", time: { created: 1 } },
|
|
38
|
+
parts: [{ type: "text", text: "go" }],
|
|
39
|
+
});
|
|
40
|
+
return { data: undefined };
|
|
41
|
+
});
|
|
42
|
+
const prompt = mock(async () => ({}));
|
|
43
|
+
const abort = mock(async () => ({ data: true }));
|
|
44
|
+
const status = mock(async () => {
|
|
45
|
+
if (!prompted) return { data: {} };
|
|
46
|
+
const step = options.plan[Math.min(polls, options.plan.length - 1)];
|
|
47
|
+
polls += 1;
|
|
48
|
+
if (step === "busy") {
|
|
49
|
+
// An intermediate tool-call step is already complete while the loop runs.
|
|
50
|
+
messages.push({
|
|
51
|
+
info: {
|
|
52
|
+
id: `msg_step_${messages.length}`,
|
|
53
|
+
role: "assistant",
|
|
54
|
+
time: { created: 2, completed: 3 },
|
|
55
|
+
finish: "tool-calls",
|
|
56
|
+
},
|
|
57
|
+
parts: [{ type: "tool" }],
|
|
58
|
+
});
|
|
59
|
+
return { data: { "ses-child": { type: "busy" } } };
|
|
60
|
+
}
|
|
61
|
+
if (step === "settle" && messages.at(-1)?.info.finish !== "stop") {
|
|
62
|
+
messages.push({
|
|
63
|
+
info: {
|
|
64
|
+
id: `msg_final_${messages.length}`,
|
|
65
|
+
role: "assistant",
|
|
66
|
+
time: { created: 4, completed: 5 },
|
|
67
|
+
finish: "stop",
|
|
68
|
+
},
|
|
69
|
+
parts: [{ type: "text", text: options.answer ?? "<done/>" }],
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
return { data: {} };
|
|
73
|
+
});
|
|
74
|
+
const list = mock(async () => ({ data: [...messages] }));
|
|
75
|
+
return {
|
|
76
|
+
client: { session: { promptAsync, prompt, abort, status, messages: list } } as never,
|
|
77
|
+
promptAsync,
|
|
78
|
+
prompt,
|
|
79
|
+
abort,
|
|
80
|
+
status,
|
|
81
|
+
list,
|
|
82
|
+
polls: () => polls,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function request(signal?: AbortSignal) {
|
|
87
|
+
return {
|
|
88
|
+
path: { id: "ses-child" },
|
|
89
|
+
query: { directory: "/repo" },
|
|
90
|
+
body: { parts: [{ type: "text", text: "go" }] },
|
|
91
|
+
...(signal ? { signal } : {}),
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
describe("supportsPromptAsync", () => {
|
|
96
|
+
test("requires both prompt_async and session status", () => {
|
|
97
|
+
expect(supportsPromptAsync(asyncHost({ plan: ["settle"] }).client)).toBe(true);
|
|
98
|
+
expect(supportsPromptAsync({ session: { prompt: async () => ({}) } } as never)).toBe(false);
|
|
99
|
+
expect(supportsPromptAsync(undefined)).toBe(false);
|
|
100
|
+
expect(createPromptAsyncTransport({ session: {} } as never, "ses-child")).toBeUndefined();
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
describe("promptAsyncAndWaitForIdle", () => {
|
|
105
|
+
test("returns only after the child is idle with a settled final assistant message", async () => {
|
|
106
|
+
const host = asyncHost({ plan: ["busy", "busy", "busy", "settle"] });
|
|
107
|
+
|
|
108
|
+
await promptAsyncAndWaitForIdle(host.client, request(), { pollIntervalMs: 1 });
|
|
109
|
+
|
|
110
|
+
expect(host.promptAsync).toHaveBeenCalledTimes(1);
|
|
111
|
+
expect(host.prompt).not.toHaveBeenCalled();
|
|
112
|
+
// Completed tool-call steps during busy polls must not end the wait early.
|
|
113
|
+
expect(host.polls()).toBe(4);
|
|
114
|
+
expect(host.promptAsync.mock.calls[0]?.[0]).toMatchObject({
|
|
115
|
+
path: { id: "ses-child" },
|
|
116
|
+
query: { directory: "/repo" },
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
test("keeps waiting through an idle gap before the run starts", async () => {
|
|
121
|
+
const host = asyncHost({ plan: ["idle-pending", "idle-pending", "busy", "settle"] });
|
|
122
|
+
|
|
123
|
+
await promptAsyncAndWaitForIdle(host.client, request(), {
|
|
124
|
+
pollIntervalMs: 1,
|
|
125
|
+
startGraceMs: 60_000,
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
expect(host.polls()).toBe(4);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
test("ignores an assistant answer left by an earlier attempt in the same child", async () => {
|
|
132
|
+
const earlier: Message = {
|
|
133
|
+
info: {
|
|
134
|
+
id: "msg_old",
|
|
135
|
+
role: "assistant",
|
|
136
|
+
time: { created: 0, completed: 0 },
|
|
137
|
+
finish: "stop",
|
|
138
|
+
},
|
|
139
|
+
parts: [{ type: "text", text: "stale" }],
|
|
140
|
+
};
|
|
141
|
+
const host = asyncHost({
|
|
142
|
+
plan: ["idle-pending", "busy", "settle"],
|
|
143
|
+
initialMessages: [earlier],
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
await promptAsyncAndWaitForIdle(host.client, request(), {
|
|
147
|
+
pollIntervalMs: 1,
|
|
148
|
+
startGraceMs: 60_000,
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
expect(host.polls()).toBe(3);
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
test("fails when no run starts within the start grace", async () => {
|
|
155
|
+
const host = asyncHost({ plan: ["idle-pending"] });
|
|
156
|
+
host.promptAsync.mockImplementation(async () => ({ data: undefined }));
|
|
157
|
+
|
|
158
|
+
await expect(
|
|
159
|
+
promptAsyncAndWaitForIdle(host.client, request(), {
|
|
160
|
+
pollIntervalMs: 1,
|
|
161
|
+
startGraceMs: 20,
|
|
162
|
+
}),
|
|
163
|
+
).rejects.toThrow("did not start a run");
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
test("surfaces a rejected prompt_async request", async () => {
|
|
167
|
+
const host = asyncHost({ plan: ["settle"] });
|
|
168
|
+
host.promptAsync.mockImplementation(async () => ({
|
|
169
|
+
data: undefined,
|
|
170
|
+
error: { name: "BadRequest", data: { message: "unknown agent" } },
|
|
171
|
+
}));
|
|
172
|
+
|
|
173
|
+
await expect(
|
|
174
|
+
promptAsyncAndWaitForIdle(host.client, request(), { pollIntervalMs: 1 }),
|
|
175
|
+
).rejects.toThrow("prompt_async was rejected");
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
test("stops waiting as soon as the caller's signal aborts", async () => {
|
|
179
|
+
const host = asyncHost({ plan: ["busy"] });
|
|
180
|
+
const controller = new AbortController();
|
|
181
|
+
setTimeout(() => controller.abort(), 20);
|
|
182
|
+
|
|
183
|
+
await expect(
|
|
184
|
+
promptAsyncAndWaitForIdle(host.client, request(controller.signal), {
|
|
185
|
+
pollIntervalMs: 5,
|
|
186
|
+
}),
|
|
187
|
+
).rejects.toBeDefined();
|
|
188
|
+
});
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
describe("prompt_async transport under the retry chain", () => {
|
|
192
|
+
test("our slice stays the only timer and aborts the busy child when it expires", async () => {
|
|
193
|
+
const host = asyncHost({ plan: ["busy"] });
|
|
194
|
+
const transport = createPromptAsyncTransport(host.client, "ses-child", {
|
|
195
|
+
pollIntervalMs: 5,
|
|
196
|
+
});
|
|
197
|
+
expect(transport).toBeDefined();
|
|
198
|
+
|
|
199
|
+
let caught: unknown;
|
|
200
|
+
try {
|
|
201
|
+
await promptSyncWithValidatedOutputRetry(host.client, request(), {
|
|
202
|
+
transport,
|
|
203
|
+
timeoutMs: 60,
|
|
204
|
+
fallbackModels: ["anthropic/claude-sonnet-4-6"],
|
|
205
|
+
fetchOutput: async () => "unused",
|
|
206
|
+
validateOutput: (output: string) => output,
|
|
207
|
+
});
|
|
208
|
+
} catch (error) {
|
|
209
|
+
caught = error;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
expect((caught as Error).message).toBe("prompt timed out after 60ms");
|
|
213
|
+
expect(host.promptAsync).toHaveBeenCalledTimes(1);
|
|
214
|
+
expect(host.prompt).not.toHaveBeenCalled();
|
|
215
|
+
expect(host.abort).toHaveBeenCalledWith({ path: { id: "ses-child" } });
|
|
216
|
+
});
|
|
217
|
+
});
|