@sema-agent/core 5.64.0 → 5.65.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 +24 -0
- package/dist/core/checkpoint-store.d.ts +95 -4
- package/dist/core/hooks.d.ts +9 -1
- package/dist/core/hooks.js +1 -1
- package/dist/core/memory-engine/dual-root.js +3 -0
- package/dist/core/memory-engine/engine.d.ts +1 -0
- package/dist/core/memory-engine/engine.js +9 -5
- package/dist/core/memory-engine/types.d.ts +16 -0
- package/dist/core/remote-env.d.ts +3 -3
- package/dist/core/runner/prepare-hands-readface.d.ts +9 -0
- package/dist/core/runner/prepare-hands-readface.js +4 -1
- package/dist/core/runner/prepare-memory.js +1 -1
- package/dist/core/runner/prepare-task.d.ts +15 -4
- package/dist/core/runner/prepare-task.js +46 -20
- package/dist/core/runner/runtask.d.ts +18 -0
- package/dist/core/runner/runtask.js +150 -3
- package/dist/core/stub-env.d.ts +4 -0
- package/dist/core/stub-env.js +1 -0
- package/dist/core/task-registry-shared.js +20 -2
- package/dist/core/types.d.ts +91 -8
- package/dist/core/workflow-run-store-contract.js +13 -0
- package/dist/core/workflow-run-store.d.ts +17 -0
- package/dist/core/workflow-run-store.js +1 -0
- package/dist/engine/harness/types.d.ts +18 -0
- package/dist/index.d.ts +1 -1
- package/dist/orchestration/workflow-types.d.ts +35 -0
- package/dist/orchestration/workflow-types.js +2 -2
- package/dist/orchestration/workflow.js +20 -1
- package/dist/prompts/default.d.ts +8 -0
- package/dist/prompts/default.js +3 -0
- package/package.json +1 -1
- package/test/export-surface.snapshot.json +5 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,29 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 5.65.0 — 2026-08-28
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
- design/380 batch 2 — the device-lane durable half:
|
|
7
|
+
- `CheckpointState.placementRootSessionId` (additive): stamped at every suspend mint, restored into the internals binding on resume, chain-stable across arbitrary park depth. A resume whose EXPLICIT `internals.placementRoot` contradicts the recorded root refuses pre-CAS with the NEW code `resume.placement_mismatch` (closed-set addition to `CheckpointError`), positioned in the identity block above the usage-window rung. A supplied `rootSessionId` is delegation identity, never a placement claim — the engine's own parked-agent revive lands at the recorded root. Legacy rows keep the historic fold byte-identical.
|
|
8
|
+
- `RunnerDeps.resumePreflight` + `resumePreflightTimeoutMs`, NEW public exports `ResumePreflightInfo` / `ResumePreflightVerdict`: a deployment callback screening every redeem attempt as the last pre-CAS rung, reading facts off the persisted row only. Non-answers (throw/timeout/unreadable verdict — including a terminal verdict without a readable message) fail closed as the NEW code `resume.preflight_rejected` on the retry-later arm; only an explicit terminal disposition settles the row, honoring the fence-winner duties (same-process paused env destroyed exactly once; cross-Runner losers retire their reap records). The deadline knob validates its full admissible range loudly and folds to its own 10s default.
|
|
9
|
+
- `ExecutionEnv.externalContentTarget` (adapter-declared) + `CheckpointState.externalContentTarget` (monotonic bit): the run's env-MOUNTED tool band (fs/bash, Monitor, worktree, LSP — the roster is minted at the mount sites) classifies as external for memory governance, ORs into the `execIsExternalContent` channel, persists across park/resume, and derives one neutral environment-facts prompt line. Orchestration tools never enter the roster. Undeclared deployments are byte-identical.
|
|
10
|
+
- `WorkflowRun.timeoutInterruption` + `WorkflowRunSummary.timeoutInterruption` (#485 core half): a run abandoned by `totalTimeoutMs` carries `{timeoutMs, agentsCompleted, agentsFailed, agentsInFlight}` stamped once at the failure terminal (before the abandonment fold — the seat describes the world the deadline found); record face, list projection (cross-backend contract case), and the poll `note` read the same seat. The status enum is unchanged.
|
|
11
|
+
- `HarvestReport.reportId` (#479): engine-minted uuidv7 per-occurrence identity on all five report-derived memory notices as flat `detail.reportId`; the dedup key is `(code, detail.reportId)`.
|
|
12
|
+
- The workflow poll receipt disclosed a terminal budget overshoot (#478): one additive sentence off the summary seat; seatless runs byte-identical.
|
|
13
|
+
|
|
14
|
+
### Changed
|
|
15
|
+
- Workflow budget narration labels its unit honestly (#487, case A): four sema-authored faces now say "total tokens"; the CC-verbatim `WorkflowBudgetExceededError` message is deliberately frozen (anchor-inherited label, pinned byte-exact).
|
|
16
|
+
- The read-boundary positive-control pins converted to a differential invariant (test-only).
|
|
17
|
+
|
|
18
|
+
### Fixed
|
|
19
|
+
- Merged-code rescan batch (A-081, 7 confirmed pre-ship): the placement rung no longer bricks parked durable agents revived under an explicit spawner placement root (P1); the O9a roster is minted at the mount sites instead of a static table, closing the Monitor/worktree/LSP under-marking hole AND the TaskOutput false-positive (P1); the terminal preflight expire honors the design/51 fence-winner duties (paused env reclaimed, P2); the preflight deadline knob's silent double-validation band closed (P2); empty-string placement anchors read uniformly as absence; a terminal verdict without a readable message never terminalizes a row; rung order fixed so a deterministic dead claim is never masked by a retryable code.
|
|
20
|
+
|
|
21
|
+
### BREAKING/adaptation notes
|
|
22
|
+
- `CheckpointError` closed set +2 (`resume.placement_mismatch`, `resume.preflight_rejected`); `detail.retryAfterMs` now rides two codes.
|
|
23
|
+
- `SerializedCheckpointState` gains two REQUIRED keys (`placementRootSessionId`, `externalContentTarget`) — out-of-tree builders/`satisfies` sites gain compile errors (the mapped type's documented non-homomorphic contract).
|
|
24
|
+
- Export surface +2 (`ResumePreflightInfo`, `ResumePreflightVerdict`).
|
|
25
|
+
- A durable resume whose explicit placement anchor contradicts the recorded root now refuses loudly (named narrowing; re-pin old-behavior probes).
|
|
26
|
+
|
|
3
27
|
## 5.64.0 — 2026-08-27
|
|
4
28
|
|
|
5
29
|
### BREAKING
|
|
@@ -1160,6 +1160,43 @@ export interface CheckpointState {
|
|
|
1160
1160
|
* delegated child (never `false` — presence-coded like the other monotonic bits here).
|
|
1161
1161
|
*/
|
|
1162
1162
|
isDelegatedChild?: true;
|
|
1163
|
+
/**
|
|
1164
|
+
* design/380 O1③ — the run TREE's placement fixed point at suspend: the SAME three-segment fold the
|
|
1165
|
+
* env-factory ctx mint uses (explicit `RunInternals.placementRoot` > the delegation lanes'
|
|
1166
|
+
* `rootSessionId` > the leg's own resolved session), stamped from TRUSTED internals so a durable
|
|
1167
|
+
* resume can restore it instead of falling back to the parked leg's own session (a target-bound
|
|
1168
|
+
* factory keys its placement lookup on the fixed point — the self fallback read a resumed leg as a
|
|
1169
|
+
* NEW placement). Restore is the `isDelegatedChild` posture replayed on the placement axis: an
|
|
1170
|
+
* EXPLICIT live `internals.placementRoot` wins when re-supplied; otherwise THIS key restores into
|
|
1171
|
+
* `internals.placementRoot` at prepare so descendants and the NEXT re-suspend mint inherit it too
|
|
1172
|
+
* — outranking a supplied `rootSessionId`, which is DELEGATION IDENTITY (the peer/registry axis a
|
|
1173
|
+
* revive leg re-supplies from its row), never a placement claim (rescan C1: reading it as one
|
|
1174
|
+
* permanently stuck every parked agent spawned under an explicit placement root). The resume entry
|
|
1175
|
+
* ALSO reads this key as an identity record: a resume whose supplied `internals.placementRoot`
|
|
1176
|
+
* CONTRADICTS it is refused pre-CAS (`resume.placement_mismatch`); either side absent skips the
|
|
1177
|
+
* check (an older row / a claim-less resume — the honest degradation arm), and `""` reads as
|
|
1178
|
+
* absence (the identity fields' discipline). Schema ADDITION (additive, old readers ignore it — the Q5 posture;
|
|
1179
|
+
* deliberately NOT version-gated: same rolling-deployment account as {@link Checkpoint.principal},
|
|
1180
|
+
* whose doc records version-gating as a design candidate — any new stamp is a lattice-wide window
|
|
1181
|
+
* rewrite, owned by that ticket, not ad-hoc here). Absent ⇒ minted before the field, or the fold
|
|
1182
|
+
* resolved empty on a factory-less run (never persisted as garbage).
|
|
1183
|
+
*/
|
|
1184
|
+
placementRootSessionId?: string;
|
|
1185
|
+
/**
|
|
1186
|
+
* design/380 O9a — MONOTONIC presence bit: this run's hand-tool content fold was EXTERNAL-TARGET
|
|
1187
|
+
* armed (the env declared `ExecutionEnv.externalContentTarget`, or an earlier leg of this suspend
|
|
1188
|
+
* chain did). Stamped at the suspend mint from the run's RESOLVED fold state — never from first
|
|
1189
|
+
* tool use, so a park landing BEFORE the first hand call is covered too. Presence-coded like
|
|
1190
|
+
* {@link isDelegatedChild} (absent = never armed; never `false`). The resume side folds
|
|
1191
|
+
* **persisted OR live declaration** (a monotonic join, never a downgrade): a run minted under a
|
|
1192
|
+
* declaring adapter keeps its memory fold and its prompt disclosure when resumed on an
|
|
1193
|
+
* undeclared/older adapter — the downgrade is closed in the derivation itself, deliberately
|
|
1194
|
+
* WITHOUT a new refusal arm (one fewer failure path, same safety). Feeds the memory face and the
|
|
1195
|
+
* prompt disclosure only — no gate/policy/roster behavior reads it (the design/378 red line).
|
|
1196
|
+
* Schema ADDITION (additive, Q5 posture; the version-gate residual is shared with
|
|
1197
|
+
* {@link placementRootSessionId} above and owned by the same version-lattice ticket).
|
|
1198
|
+
*/
|
|
1199
|
+
externalContentTarget?: true;
|
|
1163
1200
|
}
|
|
1164
1201
|
/**
|
|
1165
1202
|
* {@link CheckpointState} with **every** field made required-PRESENT, while each value keeps its original
|
|
@@ -1869,6 +1906,30 @@ export declare class CheckpointError extends Error {
|
|
|
1869
1906
|
* refusal exists to close. Rejected PRE-CAS (the checkpoint stays `pending`); re-resume with the
|
|
1870
1907
|
* original principal, or omit the field to inherit the recorded one. */
|
|
1871
1908
|
| "resume.principal_mismatch"
|
|
1909
|
+
/** design/380 O1③ (placement identity continuity — the `resume.principal_mismatch` rung's twin
|
|
1910
|
+
* on the placement axis): the resume supplied trusted internals whose EXPLICIT
|
|
1911
|
+
* `placementRoot` differs from the {@link CheckpointState.placementRootSessionId} recorded at
|
|
1912
|
+
* the suspend mint. A target-bound env factory keys its placement lookup on that fixed point,
|
|
1913
|
+
* so silently resuming under a different one would re-place the leg — and every descendant
|
|
1914
|
+
* the child-internals chain hands it to — on another target than the parked workspace lives
|
|
1915
|
+
* on. A supplied `rootSessionId` deliberately does NOT take this refusal (rescan C1): it is
|
|
1916
|
+
* delegation identity, re-supplied by every revive leg, and legitimately differs from the
|
|
1917
|
+
* recorded root whenever the spawner carried an explicit placement — the recorded root simply
|
|
1918
|
+
* outranks it in prepare's restore fold. Rejected PRE-CAS (the checkpoint stays `pending`);
|
|
1919
|
+
* re-resume with the recorded root, or omit `internals.placementRoot` to inherit it. Either
|
|
1920
|
+
* side absent (an older row / a claim-less resume) skips the check, and `""` reads as absence
|
|
1921
|
+
* — the honest degradation arm. */
|
|
1922
|
+
| "resume.placement_mismatch"
|
|
1923
|
+
/** design/380 O2: the deployment's `RunnerDeps.resumePreflight` refused this resume — an
|
|
1924
|
+
* explicit refusal verdict, a throw, a timeout past its bounded deadline, or an unreadable
|
|
1925
|
+
* answer (all fail-closed). The LAST rung before the CAS, so nothing was consumed. Two arms,
|
|
1926
|
+
* discriminated by the message: the default `retry_later` arm leaves the checkpoint `pending`
|
|
1927
|
+
* and the SAME token redeemable once the obstacle clears (`detail.retryAfterMs` carries the
|
|
1928
|
+
* deployment's wait hint when it gave one — the #449 G1 carrier); the explicit `terminal` arm
|
|
1929
|
+
* has ALREADY settled the row out of `pending` via the single-shot `expire` CAS (reaper
|
|
1930
|
+
* parity) before this throw — the token is NOT redeemable, by the deployment's own permanent
|
|
1931
|
+
* verdict (a dead binding must not be redialed forever). */
|
|
1932
|
+
| "resume.preflight_rejected"
|
|
1872
1933
|
/** F-012 L3: the resume carried an approver EDIT (`updatedInput`) and a FROZEN inherited ancestor
|
|
1873
1934
|
* constraint (the persisted projection chain) — or the deployment's resume-edit policy — DENIES the
|
|
1874
1935
|
* edited args. Refused PRE-CAS with fresh-redecision semantics: the checkpoint stays `pending` and
|
|
@@ -1896,8 +1957,11 @@ export declare class CheckpointError extends Error {
|
|
|
1896
1957
|
* discriminant. */
|
|
1897
1958
|
readonly detail?: {
|
|
1898
1959
|
field?: "boundCallId" | "boundInputHash" | "answer" | "settledBy" | "approver";
|
|
1899
|
-
/** #449 G1 — on `resume.usage_window_exhausted
|
|
1900
|
-
* (the same wait a suspended row carries as `gate.resumeAfterMs`).
|
|
1960
|
+
/** #449 G1 — on `resume.usage_window_exhausted`: how long until the binding window frees
|
|
1961
|
+
* (the same wait a suspended row carries as `gate.resumeAfterMs`). design/380 O2 — on a
|
|
1962
|
+
* retry-later `resume.preflight_rejected`: the deployment preflight's own wait hint, when it
|
|
1963
|
+
* gave one (same carrier, same semantics: retry the SAME token after the wait). Absent on
|
|
1964
|
+
* every other code. */
|
|
1901
1965
|
retryAfterMs?: number;
|
|
1902
1966
|
/** WHICH pre-CAS refusal arm fired, where one `code` covers several (requested 2026-08-10: a
|
|
1903
1967
|
* deployment retry policy needs to tell "a newer worker can redeem this row" from "this row is
|
|
@@ -2024,6 +2088,30 @@ export declare class CheckpointError extends Error {
|
|
|
2024
2088
|
* refusal exists to close. Rejected PRE-CAS (the checkpoint stays `pending`); re-resume with the
|
|
2025
2089
|
* original principal, or omit the field to inherit the recorded one. */
|
|
2026
2090
|
| "resume.principal_mismatch"
|
|
2091
|
+
/** design/380 O1③ (placement identity continuity — the `resume.principal_mismatch` rung's twin
|
|
2092
|
+
* on the placement axis): the resume supplied trusted internals whose EXPLICIT
|
|
2093
|
+
* `placementRoot` differs from the {@link CheckpointState.placementRootSessionId} recorded at
|
|
2094
|
+
* the suspend mint. A target-bound env factory keys its placement lookup on that fixed point,
|
|
2095
|
+
* so silently resuming under a different one would re-place the leg — and every descendant
|
|
2096
|
+
* the child-internals chain hands it to — on another target than the parked workspace lives
|
|
2097
|
+
* on. A supplied `rootSessionId` deliberately does NOT take this refusal (rescan C1): it is
|
|
2098
|
+
* delegation identity, re-supplied by every revive leg, and legitimately differs from the
|
|
2099
|
+
* recorded root whenever the spawner carried an explicit placement — the recorded root simply
|
|
2100
|
+
* outranks it in prepare's restore fold. Rejected PRE-CAS (the checkpoint stays `pending`);
|
|
2101
|
+
* re-resume with the recorded root, or omit `internals.placementRoot` to inherit it. Either
|
|
2102
|
+
* side absent (an older row / a claim-less resume) skips the check, and `""` reads as absence
|
|
2103
|
+
* — the honest degradation arm. */
|
|
2104
|
+
| "resume.placement_mismatch"
|
|
2105
|
+
/** design/380 O2: the deployment's `RunnerDeps.resumePreflight` refused this resume — an
|
|
2106
|
+
* explicit refusal verdict, a throw, a timeout past its bounded deadline, or an unreadable
|
|
2107
|
+
* answer (all fail-closed). The LAST rung before the CAS, so nothing was consumed. Two arms,
|
|
2108
|
+
* discriminated by the message: the default `retry_later` arm leaves the checkpoint `pending`
|
|
2109
|
+
* and the SAME token redeemable once the obstacle clears (`detail.retryAfterMs` carries the
|
|
2110
|
+
* deployment's wait hint when it gave one — the #449 G1 carrier); the explicit `terminal` arm
|
|
2111
|
+
* has ALREADY settled the row out of `pending` via the single-shot `expire` CAS (reaper
|
|
2112
|
+
* parity) before this throw — the token is NOT redeemable, by the deployment's own permanent
|
|
2113
|
+
* verdict (a dead binding must not be redialed forever). */
|
|
2114
|
+
| "resume.preflight_rejected"
|
|
2027
2115
|
/** F-012 L3: the resume carried an approver EDIT (`updatedInput`) and a FROZEN inherited ancestor
|
|
2028
2116
|
* constraint (the persisted projection chain) — or the deployment's resume-edit policy — DENIES the
|
|
2029
2117
|
* edited args. Refused PRE-CAS with fresh-redecision semantics: the checkpoint stays `pending` and
|
|
@@ -2051,8 +2139,11 @@ export declare class CheckpointError extends Error {
|
|
|
2051
2139
|
* discriminant. */
|
|
2052
2140
|
detail?: {
|
|
2053
2141
|
field?: "boundCallId" | "boundInputHash" | "answer" | "settledBy" | "approver";
|
|
2054
|
-
/** #449 G1 — on `resume.usage_window_exhausted
|
|
2055
|
-
* (the same wait a suspended row carries as `gate.resumeAfterMs`).
|
|
2142
|
+
/** #449 G1 — on `resume.usage_window_exhausted`: how long until the binding window frees
|
|
2143
|
+
* (the same wait a suspended row carries as `gate.resumeAfterMs`). design/380 O2 — on a
|
|
2144
|
+
* retry-later `resume.preflight_rejected`: the deployment preflight's own wait hint, when it
|
|
2145
|
+
* gave one (same carrier, same semantics: retry the SAME token after the wait). Absent on
|
|
2146
|
+
* every other code. */
|
|
2056
2147
|
retryAfterMs?: number;
|
|
2057
2148
|
/** WHICH pre-CAS refusal arm fired, where one `code` covers several (requested 2026-08-10: a
|
|
2058
2149
|
* deployment retry policy needs to tell "a newer worker can redeem this row" from "this row is
|
package/dist/core/hooks.d.ts
CHANGED
|
@@ -278,6 +278,10 @@ export interface Hooks {
|
|
|
278
278
|
* answer in a second says so with `timeoutMs`.
|
|
279
279
|
*/
|
|
280
280
|
export declare const DEFAULT_HOOK_TIMEOUT_MS = 600000;
|
|
281
|
+
/** `setTimeout`'s own ceiling: above it the platform silently clamps to ~1ms, which would turn a
|
|
282
|
+
* deployment's "very long bound" into an instant expiry it never asked for. Refused loudly instead
|
|
283
|
+
* (same ceiling, same posture as the gate's `approvalTimeoutMs`). */
|
|
284
|
+
export declare const MAX_HOOK_TIMEOUT_MS = 2147483647;
|
|
281
285
|
/**
|
|
282
286
|
* How long an observation seat still gets when the task is cancelled `elapsed` ms into its wait — the
|
|
283
287
|
* grace, capped by what is LEFT of the declared deadline.
|
|
@@ -303,7 +307,11 @@ export declare function postAbortGraceMs(remainingMs: number): number;
|
|
|
303
307
|
*/
|
|
304
308
|
export declare function resolveHookTimeoutMs(supplied: number | undefined, report?: (err: unknown) => void, owner?: object): number;
|
|
305
309
|
/** The ten {@link Hooks} callback seats, named so a bound wait can say WHICH one did not answer. */
|
|
306
|
-
export type HookSeatName = "preToolUse" | "postToolUse" | "postToolUseFailure" | "postToolBatch" | "userPromptSubmit" | "stop" | "stopFailure" | "preCompact" | "postCompact" | "permissionDenied"
|
|
310
|
+
export type HookSeatName = "preToolUse" | "postToolUse" | "postToolUseFailure" | "postToolBatch" | "userPromptSubmit" | "stop" | "stopFailure" | "preCompact" | "postCompact" | "permissionDenied"
|
|
311
|
+
/** design/380 O2 — not a {@link Hooks} record seat: the `RunnerDeps.resumePreflight` deployment
|
|
312
|
+
* callback, run through the same bounded-wait home so the pre-CAS resume ladder cannot grow its
|
|
313
|
+
* own dialect of "a deployment callback gets a deadline". */
|
|
314
|
+
| "resumePreflight";
|
|
307
315
|
/** What one bounded seat invocation produced: the seat's own value, or the fact that the wait ended
|
|
308
316
|
* first. A seat that THREW is neither — the throw propagates out of {@link runHookSeat} unchanged, so
|
|
309
317
|
* each site's existing crash arm keeps owning it. */
|
package/dist/core/hooks.js
CHANGED
|
@@ -7,7 +7,7 @@ import { ASK_USER_QUESTION_TOOL_NAME } from "./ask-question.js";
|
|
|
7
7
|
import { createSafeNotifier } from "./safe-notify.js";
|
|
8
8
|
import { ORG_ADJUDICATION_TIMEOUT_MS, ORG_RULE_DECISION_REASON, ORG_UNAVAILABLE_DECISION_REASON, settleOrgVerdictWithin } from "./permission-rule-org.js";
|
|
9
9
|
export const DEFAULT_HOOK_TIMEOUT_MS = 600_000;
|
|
10
|
-
const MAX_HOOK_TIMEOUT_MS = 2_147_483_647;
|
|
10
|
+
export const MAX_HOOK_TIMEOUT_MS = 2_147_483_647;
|
|
11
11
|
const POST_ABORT_GRACE_MS = 1_000;
|
|
12
12
|
export function postAbortGraceMs(remainingMs) {
|
|
13
13
|
return Math.max(0, Math.min(POST_ABORT_GRACE_MS, remainingMs));
|
|
@@ -63,6 +63,9 @@ export function mergeHarvestReports(a, b) {
|
|
|
63
63
|
restored: [...a.restored, ...b.restored],
|
|
64
64
|
warnings: [...a.warnings, ...b.warnings],
|
|
65
65
|
};
|
|
66
|
+
const reportId = a.reportId ?? b.reportId;
|
|
67
|
+
if (reportId !== undefined)
|
|
68
|
+
merged.reportId = reportId;
|
|
66
69
|
const incident = a.incident ?? b.incident;
|
|
67
70
|
if (incident !== undefined)
|
|
68
71
|
merged.incident = incident;
|
|
@@ -443,6 +443,7 @@ export declare function memoryHarvestQuarantinedNotice(input: {
|
|
|
443
443
|
sessionId?: string;
|
|
444
444
|
provenance?: "off" | "carry";
|
|
445
445
|
indexRolledBack?: boolean;
|
|
446
|
+
reportId?: string;
|
|
446
447
|
}): EngineNotice;
|
|
447
448
|
/**
|
|
448
449
|
* design/336 §6.3 — the hold family's operator notices, derived from ONE harvest report's
|
|
@@ -111,12 +111,13 @@ export function pollutionContainmentCounts(report) {
|
|
|
111
111
|
}
|
|
112
112
|
export function memoryHarvestQuarantinedNotice(input) {
|
|
113
113
|
const reason = input.reason !== undefined ? inlineUntrusted(input.reason, 200) : undefined;
|
|
114
|
+
const reportId = input.reportId !== undefined ? { reportId: input.reportId } : {};
|
|
114
115
|
if (input.count === 0 && input.indexRolledBack === true) {
|
|
115
116
|
return {
|
|
116
117
|
code: "memory.harvest_quarantined",
|
|
117
118
|
message: `Memory harvest rolled the derived index (MEMORY.md) back to its materialize-time baseline${reason !== undefined ? ` (${reason})` : ""}: ` +
|
|
118
119
|
`this session's index prose additions were captured to control-plane quarantine and not retained. No entry files were withheld.`,
|
|
119
|
-
detail: { count: 0, moved: 0, escalated: 0, indexRolledBack: true, ...(reason !== undefined ? { reason } : {}), ...(input.sessionId !== undefined ? { sessionId: input.sessionId } : {}) },
|
|
120
|
+
detail: { count: 0, moved: 0, escalated: 0, indexRolledBack: true, ...reportId, ...(reason !== undefined ? { reason } : {}), ...(input.sessionId !== undefined ? { sessionId: input.sessionId } : {}) },
|
|
120
121
|
};
|
|
121
122
|
}
|
|
122
123
|
const lead = input.provenance === "carry"
|
|
@@ -135,6 +136,7 @@ export function memoryHarvestQuarantinedNotice(input) {
|
|
|
135
136
|
count: input.count,
|
|
136
137
|
moved: input.moved,
|
|
137
138
|
escalated: input.escalated,
|
|
139
|
+
...reportId,
|
|
138
140
|
...(reason !== undefined ? { reason } : {}),
|
|
139
141
|
...(input.sessionId !== undefined ? { sessionId: input.sessionId } : {}),
|
|
140
142
|
},
|
|
@@ -145,6 +147,7 @@ export function memoryHoldNotices(report, sessionId) {
|
|
|
145
147
|
if (c === undefined)
|
|
146
148
|
return [];
|
|
147
149
|
const caps = (paths) => paths.slice(0, 20).map((p) => inlineUntrusted(p, 160));
|
|
150
|
+
const reportId = report.reportId !== undefined ? { reportId: report.reportId } : {};
|
|
148
151
|
const notices = [];
|
|
149
152
|
if (c.heldInstruction.length > 0) {
|
|
150
153
|
notices.push({
|
|
@@ -152,14 +155,14 @@ export function memoryHoldNotices(report, sessionId) {
|
|
|
152
155
|
message: `Memory harvest HELD ${c.heldInstruction.length} instruction-form entry file(s): this session's delegation outcome ` +
|
|
153
156
|
`is still pending, so instruction entries wait off the model-visible plane and commit automatically once every ` +
|
|
154
157
|
`delegation settles clean (a dirty or expired settlement disposes them to quarantine, announced; resolveHold is the host valve).`,
|
|
155
|
-
detail: { count: c.heldInstruction.length, paths: caps(c.heldInstruction), ...(sessionId !== undefined ? { sessionId } : {}) },
|
|
158
|
+
detail: { count: c.heldInstruction.length, paths: caps(c.heldInstruction), ...reportId, ...(sessionId !== undefined ? { sessionId } : {}) },
|
|
156
159
|
});
|
|
157
160
|
}
|
|
158
161
|
if (c.releasedHolds.length > 0) {
|
|
159
162
|
notices.push({
|
|
160
163
|
code: "memory.hold_released",
|
|
161
164
|
message: `Memory harvest RELEASED ${c.releasedHolds.length} held instruction entry file(s) — their writer sessions settled clean (or a host valve released them) and the entries re-walked the full gate set and committed.`,
|
|
162
|
-
detail: { count: c.releasedHolds.length, paths: caps(c.releasedHolds), ...(sessionId !== undefined ? { sessionId } : {}) },
|
|
165
|
+
detail: { count: c.releasedHolds.length, paths: caps(c.releasedHolds), ...reportId, ...(sessionId !== undefined ? { sessionId } : {}) },
|
|
163
166
|
});
|
|
164
167
|
}
|
|
165
168
|
if (c.disposedHolds.length > 0) {
|
|
@@ -168,7 +171,7 @@ export function memoryHoldNotices(report, sessionId) {
|
|
|
168
171
|
message: `Memory harvest DISPOSED ${c.disposedHolds.length} held instruction entry file(s) to control-plane quarantine ` +
|
|
169
172
|
`(terminals: ${[...new Set(c.disposedHolds.map((d) => d.terminal))].join(", ")}). An "expired" terminal is a TIMEOUT, not a ` +
|
|
170
173
|
`conviction — resolveHold(holdId, "release") commits such an entry with a "static"-cause marker after host review; the bytes are never silently dropped.`,
|
|
171
|
-
detail: { count: c.disposedHolds.length, disposed: c.disposedHolds.slice(0, 20).map((d) => ({ path: inlineUntrusted(d.relPath, 160), terminal: d.terminal })), ...(sessionId !== undefined ? { sessionId } : {}) },
|
|
174
|
+
detail: { count: c.disposedHolds.length, disposed: c.disposedHolds.slice(0, 20).map((d) => ({ path: inlineUntrusted(d.relPath, 160), terminal: d.terminal })), ...reportId, ...(sessionId !== undefined ? { sessionId } : {}) },
|
|
172
175
|
});
|
|
173
176
|
}
|
|
174
177
|
return notices;
|
|
@@ -180,7 +183,7 @@ export function memoryConsolidationRecommendedNotice(report, sessionId) {
|
|
|
180
183
|
return {
|
|
181
184
|
code: "memory.consolidation_recommended",
|
|
182
185
|
message: `Memory consolidation is recommended for scope ${inlineUntrusted(rec.scope, 80)}: ${rec.sessionsSince} distinct session(s) harvested since the last run and the time gate is open. The host decides — nothing runs automatically.`,
|
|
183
|
-
detail: { scope: inlineUntrusted(rec.scope, 80), sessionsSince: rec.sessionsSince, ...(sessionId !== undefined ? { sessionId } : {}) },
|
|
186
|
+
detail: { scope: inlineUntrusted(rec.scope, 80), sessionsSince: rec.sessionsSince, ...(report.reportId !== undefined ? { reportId: report.reportId } : {}), ...(sessionId !== undefined ? { sessionId } : {}) },
|
|
184
187
|
};
|
|
185
188
|
}
|
|
186
189
|
export function memoryConsolidationCommittedNotice(input) {
|
|
@@ -1279,6 +1282,7 @@ export class MemoryEngine {
|
|
|
1279
1282
|
arr.push(rel);
|
|
1280
1283
|
};
|
|
1281
1284
|
const report = {
|
|
1285
|
+
reportId: uuidv7(),
|
|
1282
1286
|
ok: true,
|
|
1283
1287
|
patches: { add: 0, update: 0, delete: 0 },
|
|
1284
1288
|
conflicts: [],
|
|
@@ -497,6 +497,22 @@ export interface MemoryAnnouncement {
|
|
|
497
497
|
* structure; the runner's boundary-announcement wiring (attachments member) lands in S2 (TODO(S2)).
|
|
498
498
|
*/
|
|
499
499
|
export interface HarvestReport {
|
|
500
|
+
/** #479 — this report's own per-occurrence identity: a uuidv7 minted ONCE where the engine
|
|
501
|
+
* creates the report (`harvestCore`'s single literal — every exit path, including the refused
|
|
502
|
+
* and zero-admission arms, returns that one object), and read wherever a derived face needs to
|
|
503
|
+
* name this harvest: the `memory.harvest_quarantined` + `memory.hold_*` notice family carries
|
|
504
|
+
* it as `detail.reportId`. Occurrence semantics, both directions: two harvests whose counts and
|
|
505
|
+
* paths happen to coincide are two facts and get two ids (the un-dedupable five-key repeat this
|
|
506
|
+
* member exists to break), while every notice derived from ONE report shares the one id — a
|
|
507
|
+
* consumer's dedup key is therefore `(code, reportId)`, never `reportId` alone. The report is
|
|
508
|
+
* transient (delivered via `RunnerDeps.onMemoryHarvestReport` and the notice lane; the engine
|
|
509
|
+
* persists no report row), so replay-stability comes from the id riding whatever record the
|
|
510
|
+
* HOST persists: a durable replay re-reads the recorded value, a genuinely new harvest mints a
|
|
511
|
+
* new one — per-delivery minting (the fail-open shape) is exactly what this seat avoids.
|
|
512
|
+
* Optional in the TYPE only for hand-built reports (external constructors,
|
|
513
|
+
* `mergeHarvestReports` inputs); every engine-minted report carries it, and the notice
|
|
514
|
+
* factories OMIT the detail key rather than invent one when it is absent. */
|
|
515
|
+
reportId?: string;
|
|
500
516
|
/** False ⇔ the whole harvest was refused (deletion fuse / missing dir / corrupt sidecar incident). */
|
|
501
517
|
ok: boolean;
|
|
502
518
|
/** Set when a fuse tripped (§2.4 删除防线 / B3 fail-closed): the harvest is refused wholesale, NO
|
|
@@ -428,9 +428,9 @@ export interface ExecutionEnvFactoryContext {
|
|
|
428
428
|
* design/380 O1① — the run TREE's placement root (fixed point), minted by prepare AFTER session
|
|
429
429
|
* resolution as `internals.placementRoot ?? internals.rootSessionId ?? sessionId` — a top-level
|
|
430
430
|
* run's own resolved sessionId, every descendant's inherited root, verbatim (delegation and
|
|
431
|
-
* workflow spawn chains re-thread an explicit `placementRoot` — C12;
|
|
432
|
-
*
|
|
433
|
-
*
|
|
431
|
+
* workflow spawn chains re-thread an explicit `placementRoot` — C12; a bare DURABLE RESUME crosses
|
|
432
|
+
* too since design/380 O1③: the suspend mint stamps the fixed point on the checkpoint and the
|
|
433
|
+
* resume restores it into trusted internals — formerly the pinned residual). A target-bound
|
|
434
434
|
* factory (device lane) keys its placement lookup on THIS, never on `sessionId` (a child's fresh
|
|
435
435
|
* session id must not read as a new placement). Always present and non-empty — an empty value is
|
|
436
436
|
* an assembly error the engine refuses loudly at the mint (`config.placement_root_invalid`) and a
|
|
@@ -181,6 +181,15 @@ export interface PrepareHandsReadFaceResult {
|
|
|
181
181
|
shellGatedBash: boolean;
|
|
182
182
|
/** write-back — the Monitor twin of the bit above. */
|
|
183
183
|
shellGatedMonitor: boolean;
|
|
184
|
+
/** owned (design/380 rescan C4) — the ENV-HAND tool names, minted AT THE MOUNT: every tool this
|
|
185
|
+
* phase mounted over the ExecutionEnv (the fs/bash band), later grown by the driver's own env
|
|
186
|
+
* mounts (Monitor, the worktree pair — the SAME Set identity, mutated at those mount sites).
|
|
187
|
+
* Consumed by the O9a external-content-target fold: these are the tools whose RESULTS are the
|
|
188
|
+
* target's bytes, so membership must be what actually mounted on the env — never a static table
|
|
189
|
+
* (the wake/resume reconciliation table lists TaskOutput/TaskStop, which are orchestration, and
|
|
190
|
+
* misses Monitor/worktree, which are hands — both directions wrong for this consumer). Empty on
|
|
191
|
+
* a hands-less leg (nothing mounts on the env; the driver's env mounts are hands-gated too). */
|
|
192
|
+
envHandToolNames: Set<string>;
|
|
184
193
|
}
|
|
185
194
|
export declare function resolveHandsLessReadFace(input: PrepareHandsReadFaceInput): PrepareHandsReadFaceResult;
|
|
186
195
|
/** The hands-MOUNT branch — the async half of the phase (see the module header for the contract).
|
|
@@ -71,11 +71,13 @@ export function resolveHandsLessReadFace(input) {
|
|
|
71
71
|
additionalReadRootsCanonical: [],
|
|
72
72
|
attachmentRootCanonical: undefined,
|
|
73
73
|
readDenyMatcher: undefined,
|
|
74
|
+
envHandToolNames: new Set(),
|
|
74
75
|
shellGatedBash: false,
|
|
75
76
|
shellGatedMonitor: false,
|
|
76
77
|
};
|
|
77
78
|
}
|
|
78
79
|
export async function prepareHandsMount(input) {
|
|
80
|
+
const envHandToolNames = new Set();
|
|
79
81
|
const { handsEnabled, executionEnv, taskRootFinal, rebaseRestoredPath, resume, session, sessionId, hostTaskId, taskScope, fullShellReachable, effectiveShellGate, toolFaceSnapshot, model, spec, deps, internals, memoryWriteGateRef, firstPartyOffload, tools, egressTools, irreversibilityTier, irreversibleTools, reversibilityProbes } = input;
|
|
80
82
|
assertStringArraySeat(spec.additionalDirectories, "TaskSpec.additionalDirectories", "strings");
|
|
81
83
|
assertStringArraySeat(spec.additionalReadDirectories, "TaskSpec.additionalReadDirectories", "strings");
|
|
@@ -257,7 +259,7 @@ export async function prepareHandsMount(input) {
|
|
|
257
259
|
deps.onError?.(new Error(`spec.tools ${shadowed.length > 1 ? "entries" : "entry"} ${names} collide${shadowed.length > 1 ? "" : "s"} with the built-in hands tool band — the built-in wins (registered later; the harness tool Map is last-write-wins) and the caller tool never dispatches this task. Rename the caller tool, or don't inject executionEnv if you intend to replace the band.`), { phase: "config", sessionId });
|
|
258
260
|
}
|
|
259
261
|
}
|
|
260
|
-
tools.push(...band.map((t) => firstPartyOffload(t)));
|
|
262
|
+
tools.push(...band.map((t) => (envHandToolNames.add(t.name), firstPartyOffload(t))));
|
|
261
263
|
const shellGate = effectiveShellGate;
|
|
262
264
|
if (shellGate === "off" && !(executionEnv instanceof StubExecutionEnv) && spec.handsReadOnly !== true) {
|
|
263
265
|
deps.onError?.(new Error(`shell gate doctrine is "off" while a real writable shell (Bash) is mounted — no shell safety-axis ` +
|
|
@@ -304,6 +306,7 @@ export async function prepareHandsMount(input) {
|
|
|
304
306
|
additionalReadRootsCanonical,
|
|
305
307
|
attachmentRootCanonical,
|
|
306
308
|
readDenyMatcher,
|
|
309
|
+
envHandToolNames,
|
|
307
310
|
shellGatedBash,
|
|
308
311
|
shellGatedMonitor,
|
|
309
312
|
};
|
|
@@ -314,7 +314,7 @@ export async function prepareMemory(input) {
|
|
|
314
314
|
}
|
|
315
315
|
catch {
|
|
316
316
|
}
|
|
317
|
-
deliverEngineNotice(deps.onNotice, memoryHarvestQuarantinedNotice({ count, moved, escalated, ...(reason !== undefined ? { reason } : {}), sessionId, provenance: input.memoryProvenance, ...(indexOnly ? { indexRolledBack: true } : {}) }));
|
|
317
|
+
deliverEngineNotice(deps.onNotice, memoryHarvestQuarantinedNotice({ count, moved, escalated, ...(report.reportId !== undefined ? { reportId: report.reportId } : {}), ...(reason !== undefined ? { reason } : {}), sessionId, provenance: input.memoryProvenance, ...(indexOnly ? { indexRolledBack: true } : {}) }));
|
|
318
318
|
}
|
|
319
319
|
for (const notice of memoryHoldNotices(report, sessionId))
|
|
320
320
|
deliverEngineNotice(deps.onNotice, notice);
|
|
@@ -1512,10 +1512,11 @@ export interface RunInternals {
|
|
|
1512
1512
|
* workflow deps → shared internals base), so every descendant of a placed leg keeps the fixed
|
|
1513
1513
|
* point; prepare's mint reads it first (`placementRoot ?? rootSessionId ?? sessionId`). Absent
|
|
1514
1514
|
* everywhere else — the delegation lanes' `rootSessionId` fixed point then becomes the placement
|
|
1515
|
-
* root through the middle segment, unchanged.
|
|
1516
|
-
*
|
|
1517
|
-
*
|
|
1518
|
-
* member
|
|
1515
|
+
* root through the middle segment, unchanged. DURABLE since design/380 O1③ (the former C12
|
|
1516
|
+
* residual, fulfilled): the suspend mint stamps the resolved fixed point as
|
|
1517
|
+
* `CheckpointState.placementRootSessionId`, and a bare durable resume restores it into this
|
|
1518
|
+
* member (live internals win when re-supplied; a CONTRADICTING re-supply refuses pre-CAS,
|
|
1519
|
+
* `resume.placement_mismatch`). TRUSTED run-scoped channel (never a {@link TaskSpec} field).
|
|
1519
1520
|
*/
|
|
1520
1521
|
placementRoot?: string;
|
|
1521
1522
|
/**
|
|
@@ -1759,6 +1760,16 @@ export declare function batchContextAt(messages: AgentMessage[], currentId: stri
|
|
|
1759
1760
|
batchToolCallIds: string[];
|
|
1760
1761
|
completedCallIds: string[];
|
|
1761
1762
|
};
|
|
1763
|
+
/**
|
|
1764
|
+
* rescan C5 — the ONE spelling of the placement fields' empty-string discipline: `""` is absence
|
|
1765
|
+
* wearing clothes (the resume entry's principal-rung posture), and every placement read that must
|
|
1766
|
+
* treat it so — the resume rung's two sides (runtask), the restore fold's seed and the suspend
|
|
1767
|
+
* stamp — goes through THIS helper, so three sites cannot drift into three readings. Deliberately
|
|
1768
|
+
* NOT applied to the live `internals.placementRoot`/`rootSessionId` reads of the RESOLUTION fold:
|
|
1769
|
+
* an empty supplied claim there is an assembly error {@link mintPlacementRootSessionId} refuses
|
|
1770
|
+
* loudly on the factory path, and normalizing it away would silently repair what should be loud.
|
|
1771
|
+
*/
|
|
1772
|
+
export declare function placementValueOrAbsent(value: string | undefined): string | undefined;
|
|
1762
1773
|
/**
|
|
1763
1774
|
* Build everything a task run needs (council design/34 ②: a free function with EXPLICIT deps, not a
|
|
1764
1775
|
* Runner method — testable and decoupled). Resolves the model/role/thinking, acquires + reconciles the
|
|
@@ -681,14 +681,34 @@ async function derivedRouteFallsBack(args) {
|
|
|
681
681
|
return false;
|
|
682
682
|
}
|
|
683
683
|
}
|
|
684
|
-
function mintPlacementRootSessionId(
|
|
685
|
-
|
|
686
|
-
if (placementRootSessionId === "") {
|
|
684
|
+
function mintPlacementRootSessionId(placementRootResolved) {
|
|
685
|
+
if (placementRootResolved === "") {
|
|
687
686
|
const e = new Error("placement root resolved EMPTY (internals.placementRoot / internals.rootSessionId carries an empty string) — an empty fixed point cannot key a placement lookup; fix the spawning lane instead of defaulting around it.");
|
|
688
687
|
e.code = "config.placement_root_invalid";
|
|
689
688
|
throw e;
|
|
690
689
|
}
|
|
691
|
-
return
|
|
690
|
+
return placementRootResolved;
|
|
691
|
+
}
|
|
692
|
+
function restorePlacementRoot(internals, seedPlacementRoot) {
|
|
693
|
+
if (internals?.placementRoot !== undefined)
|
|
694
|
+
return internals;
|
|
695
|
+
const seed = placementValueOrAbsent(seedPlacementRoot);
|
|
696
|
+
if (seed === undefined)
|
|
697
|
+
return internals;
|
|
698
|
+
return { ...(internals ?? {}), placementRoot: seed };
|
|
699
|
+
}
|
|
700
|
+
export function placementValueOrAbsent(value) {
|
|
701
|
+
return value || undefined;
|
|
702
|
+
}
|
|
703
|
+
function stampPlacementRootSessionId(placementRootResolved) {
|
|
704
|
+
return placementValueOrAbsent(placementRootResolved);
|
|
705
|
+
}
|
|
706
|
+
function refuseRequireExistingWithoutSession(spec) {
|
|
707
|
+
if (spec.requireExistingSession && !spec.sessionId) {
|
|
708
|
+
const e = new Error(`requireExistingSession requires a sessionId — cannot require an existing session without one (design/114 Phase3)`);
|
|
709
|
+
e.code = "resume.session_not_found";
|
|
710
|
+
throw e;
|
|
711
|
+
}
|
|
692
712
|
}
|
|
693
713
|
export async function prepareTask(spec, deps, sessions, resume, internals, runnerSelf) {
|
|
694
714
|
const doors = prepareConfigDoors({ spec, deps, sessions, resume, internals });
|
|
@@ -698,15 +718,12 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
698
718
|
const { toolEffects, egressTools, irreversibleTools, irreversibilityTier, axisExplicitNegatives, reversibilityProbes, ownToolNames } = prepareSafetyScan({ spec, deps });
|
|
699
719
|
let shellGatedBash = false;
|
|
700
720
|
let shellGatedMonitor = false;
|
|
701
|
-
|
|
702
|
-
const e = new Error(`requireExistingSession requires a sessionId — cannot require an existing session without one (design/114 Phase3)`);
|
|
703
|
-
e.code = "resume.session_not_found";
|
|
704
|
-
throw e;
|
|
705
|
-
}
|
|
721
|
+
refuseRequireExistingWithoutSession(spec);
|
|
706
722
|
const { acquired, session, conflictRef, wakeRecovered, resumeAtBeforeParentId } = await prepareAcquireReconcile({ sessions, spec, resume, toolEffects, ...(internals?.sessionPlacement !== undefined ? { placement: internals.sessionPlacement } : {}), ...(() => { const g = durableParkGapFor(deps, spec); return g !== undefined ? { durableParkGap: g } : {}; })() });
|
|
707
723
|
const sessionId = acquired.sessionId;
|
|
708
724
|
const hostTaskId = spec.taskId ?? sessionId;
|
|
709
725
|
const delegation = effectiveDelegationFacts(internals, resume?.seed.isDelegatedChild);
|
|
726
|
+
const placementRootResolved = (internals = restorePlacementRoot(internals, resume?.seed.placementRootSessionId))?.placementRoot ?? internals?.rootSessionId ?? sessionId;
|
|
710
727
|
let effectiveCompModel = compModel;
|
|
711
728
|
if (spec.compactionModel === undefined &&
|
|
712
729
|
compModel !== undefined &&
|
|
@@ -798,7 +815,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
798
815
|
try {
|
|
799
816
|
ownedEnv = deps.executionEnvFactory
|
|
800
817
|
? await deps.executionEnvFactory({
|
|
801
|
-
sessionId, placementRootSessionId: mintPlacementRootSessionId(
|
|
818
|
+
sessionId, placementRootSessionId: mintPlacementRootSessionId(placementRootResolved),
|
|
802
819
|
taskId: spec.taskId,
|
|
803
820
|
...(internals?.isolation ? { isolation: internals.isolation } : {}),
|
|
804
821
|
...(internals?.parentCwd ? { parentCwd: internals.parentCwd } : {}),
|
|
@@ -812,6 +829,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
812
829
|
throw factoryErr;
|
|
813
830
|
}
|
|
814
831
|
const executionEnv = ownedEnv ?? deps.executionEnv ?? new StubExecutionEnv();
|
|
832
|
+
const externalContentTargetActive = executionEnv.externalContentTarget === true || resume?.seed.externalContentTarget === true;
|
|
815
833
|
if (internals?.requestedCwd !== undefined) {
|
|
816
834
|
const canon = async (p) => {
|
|
817
835
|
const r = await executionEnv.canonicalPath(p);
|
|
@@ -1605,7 +1623,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
1605
1623
|
const memoryWriteGateRef = {};
|
|
1606
1624
|
const handsReadFaceInput = { handsEnabled, executionEnv, taskRootFinal, rebaseRestoredPath, resume, session, sessionId, hostTaskId, taskScope, fullShellReachable, effectiveShellGate, toolFaceSnapshot, model, spec, deps, internals, memoryWriteGateRef, firstPartyOffload, tools, egressTools, irreversibilityTier, irreversibleTools, reversibilityProbes, reminderMark, reminderDisclosureCounts };
|
|
1607
1625
|
const handsReadFace = handsEnabled ? await prepareHandsMount(handsReadFaceInput) : resolveHandsLessReadFace(handsReadFaceInput);
|
|
1608
|
-
const { readFileStateForCheckpoint, seedContextFiles, handsCwdRef, workspaceStateSettle, wsSnapshot, rebaseWsPath, backgroundTaskToolsActive, additionalRootsCanonical, additionalReadRootsCanonical, attachmentRootCanonical, readDenyMatcher } = handsReadFace;
|
|
1626
|
+
const { readFileStateForCheckpoint, seedContextFiles, handsCwdRef, workspaceStateSettle, wsSnapshot, rebaseWsPath, backgroundTaskToolsActive, additionalRootsCanonical, additionalReadRootsCanonical, attachmentRootCanonical, readDenyMatcher, envHandToolNames } = handsReadFace;
|
|
1609
1627
|
resolvedReadFace = handsReadFace.resolvedReadFace;
|
|
1610
1628
|
readDenyAdditionsNormalized = handsReadFace.readDenyAdditionsNormalized;
|
|
1611
1629
|
handsLessResolvedFace = handsReadFace.handsLessResolvedFace;
|
|
@@ -1689,6 +1707,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
1689
1707
|
}
|
|
1690
1708
|
if (backgroundTaskToolsActive) {
|
|
1691
1709
|
toolEffects.set("Monitor", "write");
|
|
1710
|
+
envHandToolNames.add("Monitor");
|
|
1692
1711
|
tools.push(firstPartyOffload(createMonitorTool(executionEnv, {
|
|
1693
1712
|
registry: defaultTaskRegistry,
|
|
1694
1713
|
owner: hostTaskId,
|
|
@@ -1724,7 +1743,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
1724
1743
|
};
|
|
1725
1744
|
if (workspaceStateSettle !== undefined)
|
|
1726
1745
|
workspaceStateSettle.restoredWorktreeDir = worktreeSessionRef.current?.worktreeDir;
|
|
1727
|
-
tools.push(...createWorktreeTools(executionEnv, { repoRoot: taskRootFinal, cwdRef: handsCwdRef, session: worktreeSessionRef }).map((t) => firstPartyOffload(t)));
|
|
1746
|
+
tools.push(...createWorktreeTools(executionEnv, { repoRoot: taskRootFinal, cwdRef: handsCwdRef, session: worktreeSessionRef }).map((t) => (envHandToolNames.add(t.name), firstPartyOffload(t))));
|
|
1728
1747
|
}
|
|
1729
1748
|
if (offloadStore)
|
|
1730
1749
|
tools.push(createReadToolResultTool(offloadStore));
|
|
@@ -1809,7 +1828,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
1809
1828
|
...(spec.principal !== undefined ? { principal: spec.principal } : {}),
|
|
1810
1829
|
...(sessionId !== undefined ? { sessionId } : {}),
|
|
1811
1830
|
requestStopAfterTurn,
|
|
1812
|
-
}));
|
|
1831
|
+
}).map((t) => (envHandToolNames.add(t.name), t)));
|
|
1813
1832
|
}
|
|
1814
1833
|
if (internals?.insideFork !== true &&
|
|
1815
1834
|
runnerSelf &&
|
|
@@ -1823,6 +1842,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
1823
1842
|
if (lspManager) {
|
|
1824
1843
|
const lspRoot = taskRootFinal;
|
|
1825
1844
|
tools.push(createLspTool(lspManager, { isPathIgnored: gitCheckIgnoreFilter(executionEnv, lspRoot), env: executionEnv }));
|
|
1845
|
+
envHandToolNames.add("LSP");
|
|
1826
1846
|
}
|
|
1827
1847
|
const lspDiagnostics = spec.lspDiagnostics !== false && lspManager?.diagnostics !== undefined && handsEnabled && spec.handsReadOnly !== true
|
|
1828
1848
|
? lspManager.diagnostics
|
|
@@ -2098,6 +2118,8 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
2098
2118
|
envFacts.sandboxEgress = spec.envFacts.egress;
|
|
2099
2119
|
if (spec.envFacts?.scratchpadDir)
|
|
2100
2120
|
envFacts.scratchpadDir = spec.envFacts.scratchpadDir;
|
|
2121
|
+
if (externalContentTargetActive)
|
|
2122
|
+
envFacts.externalContentTarget = true;
|
|
2101
2123
|
if (resume !== undefined && spec.envFacts?.resumeFacts) {
|
|
2102
2124
|
const rf = spec.envFacts.resumeFacts;
|
|
2103
2125
|
const copy = {};
|
|
@@ -2278,7 +2300,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
2278
2300
|
let staticMarkWaiverAnnounced = false;
|
|
2279
2301
|
const effectiveSafety = narrowContentSafety(delegationProvenanceChannel?.contentSafety, memoryEngineSession?.contentSafety);
|
|
2280
2302
|
const trustedTools = effectiveSafety.trustedTools;
|
|
2281
|
-
const execIsExternalContent = effectiveSafety.execIsExternalContent;
|
|
2303
|
+
const execIsExternalContent = effectiveSafety.execIsExternalContent || externalContentTargetActive;
|
|
2282
2304
|
provenanceForChildrenRef.current = { trustedTools: [...trustedTools], execIsExternalContent };
|
|
2283
2305
|
announceDeclaredMcpContentClasses({
|
|
2284
2306
|
entries: lockedPreflight.mcp,
|
|
@@ -2300,12 +2322,14 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
2300
2322
|
};
|
|
2301
2323
|
const callerToolNames = new Set((spec.tools ?? []).map((t) => t.name));
|
|
2302
2324
|
const contentOriginWrapped = new WeakSet();
|
|
2303
|
-
const classOf = (t) =>
|
|
2304
|
-
|
|
2305
|
-
|
|
2306
|
-
|
|
2307
|
-
|
|
2308
|
-
|
|
2325
|
+
const classOf = (t) => externalContentTargetActive && envHandToolNames.has(t.name)
|
|
2326
|
+
? "external"
|
|
2327
|
+
: classifyToolContentOrigin({
|
|
2328
|
+
declared: t.contentOrigin,
|
|
2329
|
+
isProtocolTool: protocolOf(t.name) !== undefined,
|
|
2330
|
+
isCallerTool: callerToolNames.has(t.name),
|
|
2331
|
+
trusted: trustedTools.has(t.name),
|
|
2332
|
+
});
|
|
2309
2333
|
const delegationByName = new Map();
|
|
2310
2334
|
for (const t of spec.tools ?? []) {
|
|
2311
2335
|
const withFaces = t;
|
|
@@ -3897,6 +3921,8 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
3897
3921
|
gitAnnouncement: gitStatusRef.announced !== undefined ? { ...gitStatusRef.announced } : undefined,
|
|
3898
3922
|
delegationProvenance: internals?.delegationProvenance !== undefined ? { ...internals.delegationProvenance.ref.current } : undefined,
|
|
3899
3923
|
isDelegatedChild: hookIdentity.isDelegatedChild ? true : undefined,
|
|
3924
|
+
placementRootSessionId: stampPlacementRootSessionId(placementRootResolved),
|
|
3925
|
+
externalContentTarget: externalContentTargetActive ? true : undefined,
|
|
3900
3926
|
});
|
|
3901
3927
|
const commitSuspendSaga = async (token, cp, remoteEnv, remoteHandle) => {
|
|
3902
3928
|
if (!checkpointStore)
|
|
@@ -179,6 +179,24 @@ export declare class Runner {
|
|
|
179
179
|
* observed `already_resolved` and by the FIFO cap — an evicted entry only ever degrades to the explicit
|
|
180
180
|
* re-supply requirement (fail-loud), never fail-open. */
|
|
181
181
|
private readonly parentConstraintRegistry;
|
|
182
|
+
/** rescan C2 — same-process suspended-env reap handles by token: the design/51 fence contract is
|
|
183
|
+
* "only the expire WINNER destroys the paused env", and the terminal resume-preflight arm
|
|
184
|
+
* (resumeStream) can now BE that winner — without this registry the losing stream-side
|
|
185
|
+
* reapSuspended returned early and the paused env this process holds leaked forever. Registered
|
|
186
|
+
* when a suspend publishes its reap handle; retired when the stream's own reap runs, when a
|
|
187
|
+
* resume wins the resolve CAS (the restore path owns the env from there — a later reopen keeps
|
|
188
|
+
* the ORIGINAL stream's closure as the reaping owner, deliberately), and when the terminal arm
|
|
189
|
+
* wins. Bounded by live suspended streams (the closures hold the same handles). */
|
|
190
|
+
private readonly suspendedEnvReaps;
|
|
191
|
+
/** rescan C2 review r3 — SAME-RUNNER claim marks for {@link suspendedEnvReaps}: a token appears
|
|
192
|
+
* here from the moment an in-process actor ATTEMPTS its settling CAS (the terminal preflight's
|
|
193
|
+
* expire; a resume's resolve) until that actor's duty decision is done. The losing stream-side
|
|
194
|
+
* reap preserves a record ONLY while a local claim is in flight — a CROSS-Runner settlement
|
|
195
|
+
* leaves no local claim, so the loser retires the record (drops the strong ref; physical cleanup
|
|
196
|
+
* belongs to the winning owner, unprovable from here) instead of retaining env handles without
|
|
197
|
+
* bound in a long-lived Runner. A stale claim's failure direction is retention (the status quo),
|
|
198
|
+
* never a wrong destroy. */
|
|
199
|
+
private readonly locallyClaimedTokens;
|
|
182
200
|
private static readonly PARENT_CONSTRAINT_REGISTRY_CAP;
|
|
183
201
|
constructor(deps: RunnerDeps);
|
|
184
202
|
/** design/73 §1 v1 — fire `RunnerDeps.onTaskOutcome` through the single swallow-guarded chokepoint
|