@gajae-code/coding-agent 0.15.5 → 0.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +85 -1
- package/dist/types/async/job-manager.d.ts +83 -0
- package/dist/types/cli/args.d.ts +2 -0
- package/dist/types/cli/auth-gateway-cli.d.ts +17 -0
- package/dist/types/cli/model-presets-cli.d.ts +9 -0
- package/dist/types/cli/root-flags.d.ts +14 -0
- package/dist/types/cli/update-cli.d.ts +15 -0
- package/dist/types/cli.d.ts +7 -0
- package/dist/types/commands/auth-gateway.d.ts +3 -0
- package/dist/types/commands/gc.d.ts +13 -0
- package/dist/types/commands/launch.d.ts +10 -0
- package/dist/types/commands/model-presets.d.ts +21 -0
- package/dist/types/config/autorouting-tier-map.d.ts +120 -0
- package/dist/types/config/file-lock.d.ts +54 -2
- package/dist/types/config/internal/model-preset-registry-test-state.d.ts +4 -0
- package/dist/types/config/internal/model-preset-registry-test-support.d.ts +3 -0
- package/dist/types/config/keybindings.d.ts +3 -2
- package/dist/types/config/model-preset-registry.d.ts +368 -0
- package/dist/types/config/model-profile-activation.d.ts +39 -0
- package/dist/types/config/model-profiles.d.ts +5 -3
- package/dist/types/config/model-registry.d.ts +17 -5
- package/dist/types/config/settings-schema.d.ts +19 -1
- package/dist/types/config/settings.d.ts +2 -0
- package/dist/types/coordinator-mcp/model-preset.d.ts +7 -5
- package/dist/types/coordinator-mcp/projection-scan.d.ts +54 -0
- package/dist/types/crash/record-loader.d.ts +15 -0
- package/dist/types/extensibility/extensions/types.d.ts +2 -5
- package/dist/types/gjc-runtime/empty-delete-gc.d.ts +58 -0
- package/dist/types/gjc-runtime/gc-runtime.d.ts +2 -0
- package/dist/types/gjc-runtime/launch-worktree-reservation.d.ts +21 -0
- package/dist/types/gjc-runtime/managed-owner-supervisor.d.ts +2 -0
- package/dist/types/gjc-runtime/session-state-lock.d.ts +35 -6
- package/dist/types/gjc-runtime/session-state-sidecar.d.ts +72 -1
- package/dist/types/gjc-runtime/tmux-owner-isolation.d.ts +1 -0
- package/dist/types/gjc-runtime/tmux-sessions.d.ts +27 -0
- package/dist/types/index.d.ts +1 -0
- package/dist/types/main.d.ts +3 -1
- package/dist/types/master-mode/context.d.ts +21 -0
- package/dist/types/master-mode/first-request.d.ts +34 -0
- package/dist/types/master-mode/snapshot.d.ts +32 -0
- package/dist/types/modes/components/frictionless-onboarding-selector.d.ts +10 -0
- package/dist/types/modes/components/jobs-overlay-model.d.ts +4 -2
- package/dist/types/modes/components/oauth-selector.d.ts +1 -0
- package/dist/types/modes/components/status-line/model-name.d.ts +18 -0
- package/dist/types/modes/components/status-line/priority-row.d.ts +34 -0
- package/dist/types/modes/components/status-line/segments.d.ts +10 -0
- package/dist/types/modes/jobs-observer.d.ts +36 -0
- package/dist/types/modes/print-mode.d.ts +17 -0
- package/dist/types/modes/ui-language.d.ts +7 -1
- package/dist/types/sdk/broker/broker.d.ts +80 -1
- package/dist/types/sdk/broker/endpoint-authority.d.ts +24 -0
- package/dist/types/sdk/broker/index.d.ts +3 -0
- package/dist/types/sdk/broker/lifecycle.d.ts +26 -0
- package/dist/types/sdk/broker/master-capability.d.ts +10 -0
- package/dist/types/sdk/broker/session-index.d.ts +40 -11
- package/dist/types/sdk/broker/session-scope.d.ts +72 -0
- package/dist/types/sdk/broker/spawn-authority.d.ts +201 -0
- package/dist/types/sdk/broker/spawn-substrate.d.ts +30 -0
- package/dist/types/sdk/broker/state-version.d.ts +4 -1
- package/dist/types/sdk/broker/worktree-occupancy.d.ts +28 -0
- package/dist/types/sdk/bus/chat-daemon-control.d.ts +3 -0
- package/dist/types/sdk/bus/index.d.ts +6 -0
- package/dist/types/sdk/bus/kind-aware-reconciliation.d.ts +3 -0
- package/dist/types/sdk/bus/prompt-reconciliation.d.ts +4 -0
- package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +1 -1
- package/dist/types/sdk/cli/index.d.ts +1 -0
- package/dist/types/sdk/cli/master-cli.d.ts +63 -0
- package/dist/types/sdk/cli/rows.d.ts +4 -3
- package/dist/types/sdk/cli/session-cli.d.ts +18 -2
- package/dist/types/sdk/client/client.d.ts +3 -2
- package/dist/types/sdk/client/discovery.d.ts +1 -0
- package/dist/types/sdk/host/control/dispatch.d.ts +10 -0
- package/dist/types/sdk/host/control/index.d.ts +1 -1
- package/dist/types/sdk/host/control/operations.d.ts +2 -0
- package/dist/types/sdk/host/control/runtime-gate.d.ts +21 -7
- package/dist/types/sdk/host/host.d.ts +10 -0
- package/dist/types/sdk/host/model-pin.d.ts +7 -2
- package/dist/types/sdk/host/sdk-run-capability.d.ts +1 -0
- package/dist/types/sdk/host/session-runtime.d.ts +67 -1
- package/dist/types/sdk/lifecycle/broker-client.d.ts +6 -0
- package/dist/types/sdk/lifecycle/service.d.ts +15 -3
- package/dist/types/sdk/lifecycle-session.d.ts +2 -0
- package/dist/types/sdk/prompt-deadline-manager.d.ts +11 -1
- package/dist/types/sdk/protocol/adapter-validation.d.ts +2 -0
- package/dist/types/sdk/router/session-router.d.ts +11 -0
- package/dist/types/sdk/session.d.ts +16 -0
- package/dist/types/sdk/startup-capability.d.ts +2 -1
- package/dist/types/session/account-inventory.d.ts +1 -0
- package/dist/types/session/agent-session.d.ts +24 -7
- package/dist/types/session/credential-store-errors.d.ts +1 -0
- package/dist/types/session/fold-coordinator.d.ts +201 -0
- package/dist/types/session/sdk-run-capability-internal.d.ts +7 -0
- package/dist/types/session/sdk-run-capability.d.ts +2 -0
- package/dist/types/session/session-manager.d.ts +65 -0
- package/dist/types/session/yield-queue.d.ts +5 -4
- package/dist/types/setup/frictionless-onboarding.d.ts +1 -0
- package/dist/types/telemetry/control.d.ts +11 -0
- package/dist/types/telemetry/events.d.ts +20 -0
- package/dist/types/telemetry/index.d.ts +3 -0
- package/dist/types/telemetry/transport.d.ts +22 -0
- package/dist/types/tools/bash-interactive.d.ts +23 -0
- package/dist/types/tools/bash.d.ts +4 -0
- package/dist/types/tools/gh.d.ts +8 -0
- package/dist/types/tools/index.d.ts +10 -5
- package/dist/types/web/search/providers/base.d.ts +9 -4
- package/dist/types/web/search/providers/xai.d.ts +2 -1
- package/dist/types/web/search/types.d.ts +14 -0
- package/package.json +10 -7
- package/scripts/generate-hotkeys-docs.ts +4 -0
- package/scripts/generate-sdk-operation-inventory.ts +9 -3
- package/scripts/verify-gjc-sdk-canonicalization.ts +35 -1
- package/src/async/job-manager.ts +671 -39
- package/src/cli/accounts-cli.ts +9 -5
- package/src/cli/args.ts +17 -0
- package/src/cli/auth-broker-cli.ts +40 -12
- package/src/cli/auth-gateway-cli.ts +273 -97
- package/src/cli/config-cli.ts +2 -2
- package/src/cli/fast-help.ts +4 -0
- package/src/cli/model-presets-cli.ts +100 -0
- package/src/cli/root-flags.ts +5 -0
- package/src/cli/update-cli.ts +117 -16
- package/src/cli.ts +6 -2
- package/src/commands/auth-gateway.ts +12 -8
- package/src/commands/gc.ts +19 -0
- package/src/commands/launch.ts +86 -18
- package/src/commands/model-presets.ts +24 -0
- package/src/commands/sdk.ts +113 -21
- package/src/config/autorouting-tier-map.ts +44 -0
- package/src/config/file-lock-gc.ts +9 -4
- package/src/config/file-lock.ts +890 -99
- package/src/config/internal/model-preset-registry-test-state.ts +28 -0
- package/src/config/internal/model-preset-registry-test-support.ts +11 -0
- package/src/config/keybindings.ts +6 -2
- package/src/config/model-preset-registry.ts +2795 -0
- package/src/config/model-profile-activation.ts +115 -28
- package/src/config/model-profile-contract.ts +4 -1
- package/src/config/model-profiles.ts +56 -14
- package/src/config/model-registry.ts +1044 -122
- package/src/config/model-resolver.ts +5 -0
- package/src/config/settings-schema.ts +19 -1
- package/src/config/settings.ts +11 -1
- package/src/coordinator-mcp/model-preset.ts +32 -17
- package/src/coordinator-mcp/projection-scan.ts +592 -0
- package/src/coordinator-mcp/question-state.ts +9 -11
- package/src/coordinator-mcp/server.ts +111 -42
- package/src/coordinator-mcp/session-reaper.ts +9 -3
- package/src/crash/index-store.ts +2 -0
- package/src/crash/record-loader.ts +10 -0
- package/src/extensibility/extensions/loader.ts +0 -1
- package/src/extensibility/extensions/types.ts +2 -3
- package/src/extensibility/gjc-plugins/runtime-adapters.ts +1 -0
- package/src/gjc-runtime/empty-delete-gc.ts +301 -0
- package/src/gjc-runtime/gc-render.ts +29 -0
- package/src/gjc-runtime/gc-runtime.ts +174 -11
- package/src/gjc-runtime/launch-worktree-reservation.ts +242 -0
- package/src/gjc-runtime/managed-owner-supervisor.ts +63 -51
- package/src/gjc-runtime/session-state-lock.ts +1321 -147
- package/src/gjc-runtime/session-state-sidecar.ts +1004 -25
- package/src/gjc-runtime/tmux-owner-isolation-cli.ts +4 -7
- package/src/gjc-runtime/tmux-owner-isolation.ts +2 -2
- package/src/gjc-runtime/tmux-sessions.ts +252 -13
- package/src/hooks/skill-state.ts +6 -0
- package/src/index.ts +1 -0
- package/src/internal-urls/docs-index.generated.ts +14 -13
- package/src/internal-urls/issue-pr-protocol.ts +4 -2
- package/src/lsp/config.ts +4 -3
- package/src/lsp/path-trust.ts +12 -6
- package/src/main.ts +66 -4
- package/src/master-mode/context.ts +50 -0
- package/src/master-mode/first-request.ts +65 -0
- package/src/master-mode/snapshot.ts +205 -0
- package/src/modes/acp/acp-agent.ts +616 -161
- package/src/modes/components/footer.ts +4 -2
- package/src/modes/components/frictionless-onboarding-selector.ts +48 -2
- package/src/modes/components/jobs-overlay-model.ts +99 -16
- package/src/modes/components/model-selector.ts +158 -18
- package/src/modes/components/oauth-selector.ts +6 -1
- package/src/modes/components/settings-selector.ts +1 -0
- package/src/modes/components/status-line/model-name.ts +53 -0
- package/src/modes/components/status-line/priority-row.ts +107 -0
- package/src/modes/components/status-line/segments.ts +29 -10
- package/src/modes/components/tool-status-header.ts +151 -15
- package/src/modes/controllers/extension-ui-controller.ts +1 -1
- package/src/modes/controllers/input-controller.ts +51 -6
- package/src/modes/controllers/selector-controller.ts +191 -96
- package/src/modes/interactive-mode.ts +14 -1
- package/src/modes/jobs-observer.ts +149 -16
- package/src/modes/print-mode.ts +64 -0
- package/src/modes/runtime-init.ts +1 -1
- package/src/modes/ui-language.ts +49 -2
- package/src/modes/utils/injected-user-submission.ts +15 -1
- package/src/prompts/system/master-mode.md +28 -0
- package/src/prompts/tools/bash.md +2 -2
- package/src/prompts/tools/fold-receipt.md +3 -0
- package/src/prompts/tools/job.md +1 -1
- package/src/sdk/acp/adapter.ts +17 -2
- package/src/sdk/broker/broker.ts +1671 -34
- package/src/sdk/broker/endpoint-authority.ts +86 -0
- package/src/sdk/broker/ensure.ts +5 -0
- package/src/sdk/broker/index.ts +3 -0
- package/src/sdk/broker/lifecycle.ts +217 -46
- package/src/sdk/broker/master-capability.ts +232 -0
- package/src/sdk/broker/session-index.ts +121 -17
- package/src/sdk/broker/session-scope.ts +272 -0
- package/src/sdk/broker/spawn-authority.ts +1031 -0
- package/src/sdk/broker/spawn-substrate.ts +548 -0
- package/src/sdk/broker/state-version.ts +26 -14
- package/src/sdk/broker/transport.ts +1 -0
- package/src/sdk/broker/worktree-occupancy.ts +90 -0
- package/src/sdk/bus/chat-daemon-control.ts +5 -2
- package/src/sdk/bus/chat-daemon-runtime.ts +46 -5
- package/src/sdk/bus/index.ts +51 -4
- package/src/sdk/bus/kind-aware-reconciliation.ts +36 -6
- package/src/sdk/bus/prompt-reconciliation.ts +28 -4
- package/src/sdk/bus/slack-conversation.ts +5 -1
- package/src/sdk/bus/telegram-daemon-contract.ts +1 -1
- package/src/sdk/cli/index.ts +1 -0
- package/src/sdk/cli/master-cli.ts +259 -0
- package/src/sdk/cli/rows.ts +11 -8
- package/src/sdk/cli/session-cli.ts +276 -6
- package/src/sdk/client/client.ts +14 -8
- package/src/sdk/client/discovery.ts +3 -1
- package/src/sdk/host/control/dispatch.ts +101 -37
- package/src/sdk/host/control/index.ts +2 -0
- package/src/sdk/host/control/operations.ts +2 -0
- package/src/sdk/host/control/runtime-gate.ts +203 -11
- package/src/sdk/host/host.ts +27 -2
- package/src/sdk/host/model-pin.ts +185 -25
- package/src/sdk/host/sdk-run-capability.ts +1 -0
- package/src/sdk/host/session-runtime.test.ts +581 -43
- package/src/sdk/host/session-runtime.ts +655 -101
- package/src/sdk/lifecycle/broker-client.ts +28 -0
- package/src/sdk/lifecycle/service.ts +279 -20
- package/src/sdk/lifecycle-session.ts +10 -2
- package/src/sdk/prompt-deadline-manager.ts +16 -1
- package/src/sdk/protocol/adapter-validation.ts +27 -0
- package/src/sdk/protocol/operation-inventory.generated.json +53 -2
- package/src/sdk/protocol/operation-registry.ts +17 -4
- package/src/sdk/router/session-router.ts +294 -154
- package/src/sdk/session.ts +440 -167
- package/src/sdk/startup-capability.ts +1 -0
- package/src/session/account-inventory.ts +27 -11
- package/src/session/agent-session.ts +1292 -180
- package/src/session/blob-store.ts +15 -5
- package/src/session/credential-store-errors.ts +2 -0
- package/src/session/fold-coordinator.ts +433 -0
- package/src/session/internal/managed-session-storage.ts +153 -78
- package/src/session/sdk-run-capability-internal.ts +15 -0
- package/src/session/sdk-run-capability.ts +2 -0
- package/src/session/session-manager.ts +342 -28
- package/src/session/yield-queue.ts +24 -10
- package/src/setup/frictionless-onboarding.ts +2 -0
- package/src/setup/provider-presets.json +1 -1
- package/src/slash-commands/helpers/credential-switch.ts +6 -1
- package/src/task/executor.ts +19 -3
- package/src/telemetry/control.ts +26 -0
- package/src/telemetry/events.ts +1268 -0
- package/src/telemetry/index.ts +3 -0
- package/src/telemetry/transport.ts +87 -0
- package/src/tools/bash-interactive.ts +182 -85
- package/src/tools/bash.ts +1028 -109
- package/src/tools/gh.ts +104 -48
- package/src/tools/github-cache.ts +2 -2
- package/src/tools/index.ts +11 -5
- package/src/tools/tool-catalog.generated.ts +1 -1
- package/src/web/search/provider.ts +120 -11
- package/src/web/search/providers/anthropic.ts +89 -17
- package/src/web/search/providers/base.ts +9 -4
- package/src/web/search/providers/gemini.ts +87 -15
- package/src/web/search/providers/openai-compatible.ts +23 -8
- package/src/web/search/providers/xai.ts +98 -12
- package/src/web/search/types.ts +16 -0
|
@@ -6,8 +6,13 @@ import { exactUnlink } from "@gajae-code/natives";
|
|
|
6
6
|
import { isEnoent, logger, postmortem } from "@gajae-code/utils";
|
|
7
7
|
|
|
8
8
|
const BLOB_PREFIX = "blob:sha256:";
|
|
9
|
+
const CANONICAL_BLOB_NAME = /^[0-9a-f]{64}$/;
|
|
9
10
|
const TAKE_BLOB_BUFFER_OWNERSHIP = Symbol("takeBlobBufferOwnership");
|
|
10
11
|
|
|
12
|
+
function isCanonicalBlobHash(hash: string): boolean {
|
|
13
|
+
return hash.length === 64 && CANONICAL_BLOB_NAME.test(hash);
|
|
14
|
+
}
|
|
15
|
+
|
|
11
16
|
/**
|
|
12
17
|
* Owner-only permissions for on-disk blob directories and files.
|
|
13
18
|
*
|
|
@@ -1126,6 +1131,7 @@ export class BlobStore {
|
|
|
1126
1131
|
|
|
1127
1132
|
/** Read blob by hash, returns Buffer or null if not found. */
|
|
1128
1133
|
async get(hash: string): Promise<Buffer | null> {
|
|
1134
|
+
if (!isCanonicalBlobHash(hash)) return null;
|
|
1129
1135
|
const blobPath = path.join(this.dir, hash);
|
|
1130
1136
|
try {
|
|
1131
1137
|
const file = Bun.file(blobPath);
|
|
@@ -1139,6 +1145,7 @@ export class BlobStore {
|
|
|
1139
1145
|
|
|
1140
1146
|
/** Synchronously read blob by hash, returns Buffer or null if not found. */
|
|
1141
1147
|
getSync(hash: string): Buffer | null {
|
|
1148
|
+
if (!isCanonicalBlobHash(hash)) return null;
|
|
1142
1149
|
const blobPath = path.join(this.dir, hash);
|
|
1143
1150
|
try {
|
|
1144
1151
|
return fs.readFileSync(blobPath);
|
|
@@ -1155,6 +1162,7 @@ export class BlobStore {
|
|
|
1155
1162
|
|
|
1156
1163
|
/** Synchronously read blob by hash and verify its content hash; returns null if not found. */
|
|
1157
1164
|
getCheckedSync(hash: string): Buffer | null {
|
|
1165
|
+
if (!isCanonicalBlobHash(hash)) return null;
|
|
1158
1166
|
const blobPath = path.join(this.dir, hash);
|
|
1159
1167
|
try {
|
|
1160
1168
|
return verifyBlobFileSync(hash, blobPath);
|
|
@@ -1166,6 +1174,7 @@ export class BlobStore {
|
|
|
1166
1174
|
|
|
1167
1175
|
/** Check if a blob exists. */
|
|
1168
1176
|
async has(hash: string): Promise<boolean> {
|
|
1177
|
+
if (!isCanonicalBlobHash(hash)) return false;
|
|
1169
1178
|
try {
|
|
1170
1179
|
await fsp.access(path.join(this.dir, hash));
|
|
1171
1180
|
return true;
|
|
@@ -1321,6 +1330,7 @@ export class EphemeralBlobStore extends BlobStore {
|
|
|
1321
1330
|
}
|
|
1322
1331
|
|
|
1323
1332
|
getSync(hash: string): Buffer | null {
|
|
1333
|
+
if (!isCanonicalBlobHash(hash)) return null;
|
|
1324
1334
|
const cached = this.#bufferCache.get(hash);
|
|
1325
1335
|
if (cached) {
|
|
1326
1336
|
const blobPath = path.join(this.dir, hash);
|
|
@@ -1374,7 +1384,7 @@ export class EphemeralBlobStore extends BlobStore {
|
|
|
1374
1384
|
const blobPath = path.join(this.dir, entry);
|
|
1375
1385
|
const stat = fs.lstatSync(blobPath);
|
|
1376
1386
|
if (
|
|
1377
|
-
|
|
1387
|
+
!isCanonicalBlobHash(entry) ||
|
|
1378
1388
|
!stat.isFile() ||
|
|
1379
1389
|
stat.isSymbolicLink() ||
|
|
1380
1390
|
stat.uid !== uid ||
|
|
@@ -1534,13 +1544,14 @@ export class ResidentBlobMissingError extends Error {
|
|
|
1534
1544
|
|
|
1535
1545
|
/** Check if a data string is a blob reference. */
|
|
1536
1546
|
export function isBlobRef(data: string): boolean {
|
|
1537
|
-
return data
|
|
1547
|
+
return parseBlobRef(data) !== null;
|
|
1538
1548
|
}
|
|
1539
1549
|
|
|
1540
1550
|
/** Extract the SHA-256 hash from a blob reference string. */
|
|
1541
1551
|
export function parseBlobRef(data: string): string | null {
|
|
1542
1552
|
if (!data.startsWith(BLOB_PREFIX)) return null;
|
|
1543
|
-
|
|
1553
|
+
const hash = data.slice(BLOB_PREFIX.length);
|
|
1554
|
+
return isCanonicalBlobHash(hash) ? hash : null;
|
|
1544
1555
|
}
|
|
1545
1556
|
|
|
1546
1557
|
/** Identify provider transport image data URLs so persistence can externalize and restore them losslessly. */
|
|
@@ -1731,7 +1742,6 @@ export interface CanonicalBlobEntry {
|
|
|
1731
1742
|
readonly nlink: bigint;
|
|
1732
1743
|
}
|
|
1733
1744
|
|
|
1734
|
-
const CANONICAL_BLOB_NAME = /^[0-9a-f]{64}$/;
|
|
1735
1745
|
const BLOB_REFERENCE = /blob:sha256:([0-9a-f]{64})/g;
|
|
1736
1746
|
|
|
1737
1747
|
/** Longest possible `blob:sha256:<hash>` reference, used to bound chunked scans. */
|
|
@@ -1758,7 +1768,7 @@ export async function listCanonicalBlobs(dir: string): Promise<CanonicalBlobEntr
|
|
|
1758
1768
|
|
|
1759
1769
|
const entries: CanonicalBlobEntry[] = [];
|
|
1760
1770
|
for (const name of names) {
|
|
1761
|
-
if (!
|
|
1771
|
+
if (!isCanonicalBlobHash(name)) continue;
|
|
1762
1772
|
const blobPath = path.join(dir, name);
|
|
1763
1773
|
let stat: fs.BigIntStats;
|
|
1764
1774
|
try {
|
|
@@ -0,0 +1,433 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session-owned fold/wake coordinator.
|
|
3
|
+
*
|
|
4
|
+
* Folding a running foreground wait is a lifecycle handoff across three
|
|
5
|
+
* independently scheduled domains — the tool observer, the job owner, and
|
|
6
|
+
* agent-turn admission — so it needs ONE linearization point. That is this
|
|
7
|
+
* coordinator: a synchronous prologue claims the fold and arms the steering
|
|
8
|
+
* fence with no `await` in between, and only then does an async tail capture and
|
|
9
|
+
* persist the receipt.
|
|
10
|
+
*
|
|
11
|
+
* Two invariants are load-bearing and easy to get wrong:
|
|
12
|
+
*
|
|
13
|
+
* 1. **Identity is the `AsyncJob` instance, acquired once.** Job ids and
|
|
14
|
+
* generations are manager-local, so two managers can mint the same
|
|
15
|
+
* `bg_1`/`job:1` pair. Slots are therefore keyed by the object itself,
|
|
16
|
+
* obtained exactly once through the registration-bound
|
|
17
|
+
* {@link FoldAdapter.getJob} and retained for the rest of the transaction.
|
|
18
|
+
* Nothing in the fold path may consult an ambient, singleton, or
|
|
19
|
+
* endpoint-resolved manager.
|
|
20
|
+
* 2. **A delivery callback can run more than once.** The async job manager
|
|
21
|
+
* re-pushes the SAME delivery object on retry, so the receipt is attached to
|
|
22
|
+
* a carrier keyed by that object BEFORE the slot is released. A retried
|
|
23
|
+
* delivery therefore still carries its receipt and can never silently
|
|
24
|
+
* degrade into an ordinary receipt-less completion.
|
|
25
|
+
*/
|
|
26
|
+
import type { AsyncJob } from "../async/job-manager";
|
|
27
|
+
import foldReceiptPrompt from "../prompts/tools/fold-receipt.md" with { type: "text" };
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Idle-flush merge window.
|
|
31
|
+
*
|
|
32
|
+
* Completions that land within this window coalesce into ONE wake turn. The
|
|
33
|
+
* previous 1ms debounce meant staggered completions each bought their own full
|
|
34
|
+
* LLM turn. Deliberately a fixed internal constant, not a setting: the contract
|
|
35
|
+
* is one merge window, and a per-user knob would let wake behavior differ
|
|
36
|
+
* between sessions.
|
|
37
|
+
*/
|
|
38
|
+
export const FOLD_WAKE_MERGE_WINDOW_MS = 800;
|
|
39
|
+
|
|
40
|
+
/** The wait kinds that can be folded. `task`/`subagent` awaits are a non-goal. */
|
|
41
|
+
export type FoldWaitKind = "bash-managed" | "client-terminal" | "bash-pty";
|
|
42
|
+
|
|
43
|
+
/** Outcome of settling the foreground caller. Exactly one party may settle it. */
|
|
44
|
+
export type ForegroundSettleOutcome = "resolved" | "already-settled";
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* How a folded job's output is retrieved later. Deliberately a structured
|
|
48
|
+
* handle over already-implemented paths rather than an invented URI scheme:
|
|
49
|
+
* there is no `job://` resolver, and inventing one would make the reference
|
|
50
|
+
* unresolvable in exactly the degraded case it exists for.
|
|
51
|
+
*/
|
|
52
|
+
export interface JobOutputRetrieval {
|
|
53
|
+
jobId: string;
|
|
54
|
+
generation: string;
|
|
55
|
+
/** Human/model-facing retrieval instruction naming a real mechanism. */
|
|
56
|
+
instruction: string;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Durable record captured at fold time and replayed into the wake turn. */
|
|
60
|
+
export interface FoldReceipt {
|
|
61
|
+
jobId: string;
|
|
62
|
+
jobGeneration: string;
|
|
63
|
+
kind: FoldWaitKind;
|
|
64
|
+
label: string;
|
|
65
|
+
outputRef: JobOutputRetrieval;
|
|
66
|
+
/** What the interrupted turn still intended to do, so the wake can finish it. */
|
|
67
|
+
remainingIntent: string | undefined;
|
|
68
|
+
foldedAt: number;
|
|
69
|
+
/** Only command kinds can change directory; task-style waits never do. */
|
|
70
|
+
cwdSensitive: boolean;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** A terminal completion observed for a folded job. */
|
|
74
|
+
export interface ForegroundTerminalPayload {
|
|
75
|
+
jobId: string;
|
|
76
|
+
generation: string;
|
|
77
|
+
text: string;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Per-wait fold capability. `getJob` is bound at registration over the manager
|
|
82
|
+
* that registered THIS job, which is what makes cross-manager identity safe.
|
|
83
|
+
*/
|
|
84
|
+
export interface FoldAdapter {
|
|
85
|
+
readonly kind: FoldWaitKind;
|
|
86
|
+
readonly jobId: string;
|
|
87
|
+
readonly jobGeneration: string;
|
|
88
|
+
readonly label: string;
|
|
89
|
+
readonly cwdSensitive: boolean;
|
|
90
|
+
/** Signal for the foreground wait; aborting capture must roll back the fold. */
|
|
91
|
+
readonly signal?: AbortSignal;
|
|
92
|
+
/** True only when the adapter was created by a model-owned Agent turn. */
|
|
93
|
+
readonly originatingTurn?: boolean;
|
|
94
|
+
readonly outputRef: JobOutputRetrieval;
|
|
95
|
+
/** Registration-bound resolver: the job for this adapter, or undefined once evicted or reused. */
|
|
96
|
+
readonly getJob: () => AsyncJob | undefined;
|
|
97
|
+
/** Settle the foreground caller with the fold result. Idempotent. */
|
|
98
|
+
readonly detachObserver: (receipt: FoldReceipt) => ForegroundSettleOutcome;
|
|
99
|
+
/** Hand a parked completion back to a still-attached observer. Idempotent. */
|
|
100
|
+
readonly resolveForegroundObserver: (payload: ForegroundTerminalPayload) => ForegroundSettleOutcome;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export type FoldRequestResult =
|
|
104
|
+
| { status: "folded"; receipt: FoldReceipt }
|
|
105
|
+
| { status: "unavailable"; reason: string }
|
|
106
|
+
| { status: "already-terminal"; reason: string }
|
|
107
|
+
| { status: "capture-failed"; reason: string };
|
|
108
|
+
|
|
109
|
+
/** What a delivery should carry, decided by the durable slot state (T2). */
|
|
110
|
+
export type FoldDeliveryDisposition =
|
|
111
|
+
| { kind: "ordinary" }
|
|
112
|
+
| { kind: "parked" }
|
|
113
|
+
| { kind: "receipt"; receipt: FoldReceipt; text: string };
|
|
114
|
+
|
|
115
|
+
interface ReservedSlot {
|
|
116
|
+
state: "reserved";
|
|
117
|
+
adapter: FoldAdapter;
|
|
118
|
+
parked: ForegroundTerminalPayload | undefined;
|
|
119
|
+
parkedAt: number | undefined;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
interface PresentSlot {
|
|
123
|
+
state: "present";
|
|
124
|
+
adapter: FoldAdapter;
|
|
125
|
+
receipt: FoldReceipt;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
type FoldSlot = ReservedSlot | PresentSlot;
|
|
129
|
+
|
|
130
|
+
export interface FoldCoordinatorDeps {
|
|
131
|
+
/**
|
|
132
|
+
* Fence old-turn steering admission and return the release handle. Armed
|
|
133
|
+
* synchronously inside the prologue because the agent loop polls steering
|
|
134
|
+
* upstream of its pause checkpoint; fencing there keeps queued steering
|
|
135
|
+
* intact instead of draining it.
|
|
136
|
+
*/
|
|
137
|
+
armSteeringFence: () => () => void;
|
|
138
|
+
/** Whether this fold belongs to a currently running Agent turn. */
|
|
139
|
+
hasActiveTurn?: () => boolean;
|
|
140
|
+
/** Arm the cooperative stop so the turn ends after the fold result, never via abort. */
|
|
141
|
+
requestStop: () => void;
|
|
142
|
+
/** Capture what the interrupted turn still intended to accomplish. */
|
|
143
|
+
captureRemainingIntent: () => Promise<string | undefined> | string | undefined;
|
|
144
|
+
/**
|
|
145
|
+
* Push a parked completion into the wake path. The delivery seam parks a
|
|
146
|
+
* completion that arrives before the receipt exists; when the fold later
|
|
147
|
+
* replays it (A6), THIS is what makes the wake happen rather than relying on
|
|
148
|
+
* an unrelated idle rearm to rescue it.
|
|
149
|
+
*/
|
|
150
|
+
deliverParked: (job: AsyncJob, disposition: { kind: "receipt"; receipt: FoldReceipt; text: string }) => void;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** Why a slot is being retired, which decides whether retiring is safe. */
|
|
154
|
+
export type FoldRetireReason = "cancel" | "evict";
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Render a fold receipt for the wake turn.
|
|
158
|
+
*
|
|
159
|
+
* Carries the job id, a real retrieval handle, and the captured remaining
|
|
160
|
+
* intent, so the wake turn can finish the ORIGINAL task instead of only
|
|
161
|
+
* reporting that a command ended. The cwd caveat is included only for wait
|
|
162
|
+
* kinds that can actually change directory.
|
|
163
|
+
*/
|
|
164
|
+
export function describeFoldReceipt(receipt: FoldReceipt): string {
|
|
165
|
+
const cwdNotice = receipt.cwdSensitive
|
|
166
|
+
? "Session cwd is unchanged; any directory change made by the folded command does not apply to later commands.\n"
|
|
167
|
+
: "";
|
|
168
|
+
const intentNotice = receipt.remainingIntent
|
|
169
|
+
? `Complete the original request, which was: ${receipt.remainingIntent}`
|
|
170
|
+
: "";
|
|
171
|
+
return foldReceiptPrompt
|
|
172
|
+
.replace("{{kind}}", () => receipt.kind)
|
|
173
|
+
.replace("{{jobId}}", () => receipt.jobId)
|
|
174
|
+
.replace("{{outputInstruction}}", () => receipt.outputRef.instruction)
|
|
175
|
+
.replace("{{cwdNotice}}", () => cwdNotice)
|
|
176
|
+
.replace("{{intentNotice}}", () => intentNotice)
|
|
177
|
+
.trim();
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export class FoldCoordinator {
|
|
181
|
+
/**
|
|
182
|
+
* Slots and receipt carriers are keyed by the `AsyncJob` INSTANCE, so two
|
|
183
|
+
* managers minting identical ids/generations can never collide, and an entry
|
|
184
|
+
* cannot outlive the object it belongs to.
|
|
185
|
+
*/
|
|
186
|
+
readonly #slots = new WeakMap<AsyncJob, FoldSlot>();
|
|
187
|
+
readonly #carriers = new WeakMap<AsyncJob, FoldReceipt>();
|
|
188
|
+
readonly #participants = new Map<number, FoldAdapter>();
|
|
189
|
+
#nextParticipantId = 0;
|
|
190
|
+
readonly #folding = new WeakSet<AsyncJob>();
|
|
191
|
+
/** Jobs whose completion notice has already been emitted. */
|
|
192
|
+
readonly #noticed = new WeakSet<AsyncJob>();
|
|
193
|
+
/** Jobs whose receipt-bearing completion has already been queued. */
|
|
194
|
+
readonly #delivered = new WeakSet<AsyncJob>();
|
|
195
|
+
readonly #deps: FoldCoordinatorDeps;
|
|
196
|
+
|
|
197
|
+
constructor(deps: FoldCoordinatorDeps) {
|
|
198
|
+
this.#deps = deps;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/** Register a foldable wait. The most recently registered wait is the default target. */
|
|
202
|
+
registerParticipant(adapter: FoldAdapter): () => void {
|
|
203
|
+
const key = ++this.#nextParticipantId;
|
|
204
|
+
this.#participants.set(key, adapter);
|
|
205
|
+
return () => {
|
|
206
|
+
if (this.#participants.get(key) === adapter) this.#participants.delete(key);
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/** Whether any wait is currently foldable, for the key-availability gate. */
|
|
211
|
+
hasFoldableParticipant(): boolean {
|
|
212
|
+
return this.#participants.size > 0;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/** Deterministic target: newest registration wins, so the chord folds what the user is watching. */
|
|
216
|
+
resolveTarget(): FoldAdapter | undefined {
|
|
217
|
+
let target: FoldAdapter | undefined;
|
|
218
|
+
for (const adapter of this.#participants.values()) target = adapter;
|
|
219
|
+
return target;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* T1. Synchronous prologue then async durability tail.
|
|
224
|
+
*
|
|
225
|
+
* Nothing awaits before the slot is reserved and the fence is armed, so a
|
|
226
|
+
* completion or a steering submission that races the capture cannot slip
|
|
227
|
+
* past into the old turn.
|
|
228
|
+
*/
|
|
229
|
+
async requestFold(explicit?: FoldAdapter): Promise<FoldRequestResult> {
|
|
230
|
+
// S1 resolve target.
|
|
231
|
+
const adapter = explicit ?? this.resolveTarget();
|
|
232
|
+
if (!adapter) return { status: "unavailable", reason: "no foldable wait is registered" };
|
|
233
|
+
|
|
234
|
+
// S2 acquire identity ONCE through the registration-bound resolver.
|
|
235
|
+
const job = adapter.getJob();
|
|
236
|
+
if (!job) return { status: "already-terminal", reason: "the wait already settled or was evicted" };
|
|
237
|
+
if (job.generation !== adapter.jobGeneration) {
|
|
238
|
+
return { status: "already-terminal", reason: "the job id was reused under a new generation" };
|
|
239
|
+
}
|
|
240
|
+
if (this.#folding.has(job) || this.#slots.has(job)) {
|
|
241
|
+
return { status: "unavailable", reason: "a fold is already in progress for this wait" };
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// S3 claim + reserve, S4 hand-off is the reservation itself, S5 arm the fence.
|
|
245
|
+
this.#folding.add(job);
|
|
246
|
+
this.#slots.set(job, { state: "reserved", adapter, parked: undefined, parkedAt: undefined });
|
|
247
|
+
const fencesOriginatingTurn = adapter.originatingTurn ?? this.#deps.hasActiveTurn?.() ?? true;
|
|
248
|
+
const releaseFence = fencesOriginatingTurn ? this.#deps.armSteeringFence() : () => {};
|
|
249
|
+
|
|
250
|
+
let remainingIntent: string | undefined;
|
|
251
|
+
try {
|
|
252
|
+
// A1 capture.
|
|
253
|
+
const signal = adapter.signal;
|
|
254
|
+
if (signal?.aborted) throw new Error("fold capture aborted");
|
|
255
|
+
const capture = Promise.resolve(this.#deps.captureRemainingIntent());
|
|
256
|
+
if (!fencesOriginatingTurn) {
|
|
257
|
+
remainingIntent = undefined;
|
|
258
|
+
} else if (!signal) {
|
|
259
|
+
remainingIntent = await capture;
|
|
260
|
+
} else {
|
|
261
|
+
const aborted = Promise.withResolvers<never>();
|
|
262
|
+
const onAbort = () => aborted.reject(new Error("fold capture aborted"));
|
|
263
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
264
|
+
try {
|
|
265
|
+
remainingIntent = await Promise.race([capture, aborted.promise]);
|
|
266
|
+
} finally {
|
|
267
|
+
signal.removeEventListener("abort", onAbort);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
} catch (error) {
|
|
271
|
+
return this.#rollback(job, adapter, releaseFence, error instanceof Error ? error.message : String(error));
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// T-R3a: revalidate the reservation after the await. A cancellation during
|
|
275
|
+
// capture already retired the slot; it must NOT be recreated here, and the
|
|
276
|
+
// participant must not be left advertised as folding.
|
|
277
|
+
const current = this.#slots.get(job);
|
|
278
|
+
if (!current || current.adapter !== adapter) {
|
|
279
|
+
this.#folding.delete(job);
|
|
280
|
+
releaseFence();
|
|
281
|
+
return { status: "already-terminal", reason: "the wait settled while its receipt was being captured" };
|
|
282
|
+
}
|
|
283
|
+
if (adapter.signal?.aborted || job.status !== "running") {
|
|
284
|
+
return this.#rollback(job, adapter, releaseFence, "the wait settled while its receipt was being captured");
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
const receipt: FoldReceipt = {
|
|
288
|
+
jobId: adapter.jobId,
|
|
289
|
+
jobGeneration: adapter.jobGeneration,
|
|
290
|
+
kind: adapter.kind,
|
|
291
|
+
label: adapter.label,
|
|
292
|
+
outputRef: adapter.outputRef,
|
|
293
|
+
remainingIntent,
|
|
294
|
+
foldedAt: Date.now(),
|
|
295
|
+
cwdSensitive: adapter.cwdSensitive,
|
|
296
|
+
};
|
|
297
|
+
|
|
298
|
+
// A2 promote to present, preserving any payload parked during capture.
|
|
299
|
+
const parked = current.state === "reserved" ? current.parked : undefined;
|
|
300
|
+
this.#slots.set(job, { state: "present", adapter, receipt });
|
|
301
|
+
|
|
302
|
+
// A3 detach the observer, A4 arm the cooperative stop, A5 leave folding.
|
|
303
|
+
const detached = adapter.detachObserver(receipt);
|
|
304
|
+
if (detached === "already-settled") {
|
|
305
|
+
this.#slots.delete(job);
|
|
306
|
+
this.#folding.delete(job);
|
|
307
|
+
releaseFence();
|
|
308
|
+
return { status: "already-terminal", reason: "the foreground observer already settled" };
|
|
309
|
+
}
|
|
310
|
+
if (fencesOriginatingTurn) this.#deps.requestStop();
|
|
311
|
+
this.#folding.delete(job);
|
|
312
|
+
|
|
313
|
+
// A6 flush: replay a parked completion through the same T2 branch so it
|
|
314
|
+
// takes the receipt exactly once instead of being handled specially — and
|
|
315
|
+
// push it into the wake path, because the delivery seam already returned
|
|
316
|
+
// early on it and will not retry.
|
|
317
|
+
if (parked) {
|
|
318
|
+
const replayed = this.onDelivery(job, parked.text);
|
|
319
|
+
if (replayed.kind === "receipt") this.#deps.deliverParked(job, replayed);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// A7 the fence stays armed until the turn actually stops; releasing here
|
|
323
|
+
// would readmit old-turn steering into the run being wound down.
|
|
324
|
+
return { status: "folded", receipt };
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* T2. Decide what a delivery carries, branching on the DURABLE slot state
|
|
329
|
+
* rather than on transient hand-off state.
|
|
330
|
+
*
|
|
331
|
+
* `present` attaches the receipt to the retry-stable carrier BEFORE clearing
|
|
332
|
+
* the slot, so a retried invocation still finds it.
|
|
333
|
+
*/
|
|
334
|
+
onDelivery(job: AsyncJob, text: string): FoldDeliveryDisposition {
|
|
335
|
+
const carried = this.#carriers.get(job);
|
|
336
|
+
if (carried) return { kind: "receipt", receipt: carried, text };
|
|
337
|
+
|
|
338
|
+
const slot = this.#slots.get(job);
|
|
339
|
+
if (!slot) return { kind: "ordinary" };
|
|
340
|
+
|
|
341
|
+
if (slot.state === "reserved") {
|
|
342
|
+
// Park: the receipt does not exist yet, so enqueue nothing. A6 replays it.
|
|
343
|
+
slot.parked = { jobId: slot.adapter.jobId, generation: slot.adapter.jobGeneration, text };
|
|
344
|
+
slot.parkedAt = Date.now();
|
|
345
|
+
const timer = setTimeout(() => this.retire(job, "evict"), FOLD_WAKE_MERGE_WINDOW_MS * 4);
|
|
346
|
+
timer.unref();
|
|
347
|
+
return { kind: "parked" };
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// Attach first, then release the slot: order matters for retries.
|
|
351
|
+
this.#carriers.set(job, slot.receipt);
|
|
352
|
+
this.#slots.delete(job);
|
|
353
|
+
return { kind: "receipt", receipt: slot.receipt, text };
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/**
|
|
357
|
+
* Retirement. T-R2: never drop a slot that holds an undelivered payload or
|
|
358
|
+
* that a still-possible delivery could consume.
|
|
359
|
+
*
|
|
360
|
+
* - `cancel` retires freely, because a cancelled job enqueues no delivery at
|
|
361
|
+
* all, so no later consumer can arrive.
|
|
362
|
+
* - `evict` only retires when the job can no longer deliver. An evicted
|
|
363
|
+
* record still delivers with its retained job object, so a completed or
|
|
364
|
+
* failed job keeps its slot.
|
|
365
|
+
*/
|
|
366
|
+
retire(job: AsyncJob, reason: FoldRetireReason): boolean {
|
|
367
|
+
const slot = this.#slots.get(job);
|
|
368
|
+
if (!slot) return false;
|
|
369
|
+
|
|
370
|
+
if (reason === "cancel") {
|
|
371
|
+
this.#slots.delete(job);
|
|
372
|
+
this.#folding.delete(job);
|
|
373
|
+
return true;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
if (slot.state === "reserved" && slot.parked !== undefined) {
|
|
377
|
+
const parkedAt = slot.parkedAt ?? Date.now();
|
|
378
|
+
if (Date.now() - parkedAt < FOLD_WAKE_MERGE_WINDOW_MS * 4) return false;
|
|
379
|
+
const parked = slot.parked;
|
|
380
|
+
this.#slots.delete(job);
|
|
381
|
+
this.#folding.delete(job);
|
|
382
|
+
slot.adapter.resolveForegroundObserver(parked);
|
|
383
|
+
return true;
|
|
384
|
+
}
|
|
385
|
+
if (job.status === "cancelled" || job.status === "paused") {
|
|
386
|
+
this.#slots.delete(job);
|
|
387
|
+
this.#folding.delete(job);
|
|
388
|
+
return true;
|
|
389
|
+
}
|
|
390
|
+
return false;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
/**
|
|
394
|
+
* Claim the single completion notice for a job.
|
|
395
|
+
*
|
|
396
|
+
* Returns true exactly once per job. A delivery can be retried with the same
|
|
397
|
+
* object, so an unguarded notice would repeat for one completion.
|
|
398
|
+
*/
|
|
399
|
+
claimCompletionNotice(job: AsyncJob): boolean {
|
|
400
|
+
if (this.#noticed.has(job)) return false;
|
|
401
|
+
this.#noticed.add(job);
|
|
402
|
+
return true;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
/** Claim the receipt-bearing queue entry once across manager delivery retries. */
|
|
406
|
+
claimCompletionDelivery(job: AsyncJob): boolean {
|
|
407
|
+
if (this.#delivered.has(job)) return false;
|
|
408
|
+
this.#delivered.add(job);
|
|
409
|
+
return true;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
/** Test/diagnostic view of the durable slot state for a job. */
|
|
413
|
+
slotStateFor(job: AsyncJob): "none" | "reserved-empty" | "reserved-parked" | "present" | "carried" {
|
|
414
|
+
const slot = this.#slots.get(job);
|
|
415
|
+
if (slot?.state === "present") return "present";
|
|
416
|
+
if (slot?.state === "reserved") return slot.parked === undefined ? "reserved-empty" : "reserved-parked";
|
|
417
|
+
return this.#carriers.has(job) ? "carried" : "none";
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
#rollback(job: AsyncJob, adapter: FoldAdapter, releaseFence: () => void, reason: string): FoldRequestResult {
|
|
421
|
+
const slot = this.#slots.get(job);
|
|
422
|
+
// R1/R2 return the claim and drop the reservation, R3 release the fence so
|
|
423
|
+
// the preserved steering becomes admissible again, R5 never arm the stop.
|
|
424
|
+
this.#slots.delete(job);
|
|
425
|
+
this.#folding.delete(job);
|
|
426
|
+
releaseFence();
|
|
427
|
+
// R4 hand a parked completion back to the STILL-ATTACHED observer, which
|
|
428
|
+
// owns its own acknowledgement path. The coordinator enqueues nothing, so
|
|
429
|
+
// it cannot race that observer.
|
|
430
|
+
if (slot?.state === "reserved" && slot.parked) adapter.resolveForegroundObserver(slot.parked);
|
|
431
|
+
return { status: "capture-failed", reason };
|
|
432
|
+
}
|
|
433
|
+
}
|