@sema-agent/core 5.62.0 → 5.64.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 +96 -0
- package/dist/agents/cascade.d.ts +5 -1
- package/dist/agents/cascade.js +6 -1
- package/dist/agents/subagent.d.ts +12 -2
- package/dist/agents/subagent.js +4 -2
- package/dist/agents/verify.d.ts +5 -1
- package/dist/agents/verify.js +5 -2
- package/dist/core/auto-compaction.d.ts +6 -4
- package/dist/core/auto-compaction.js +3 -0
- package/dist/core/checkpoint-store.d.ts +5 -1
- package/dist/core/context-edit.d.ts +36 -29
- package/dist/core/context-edit.js +3 -3
- package/dist/core/fs-write-gate-policy.d.ts +21 -0
- package/dist/core/fs-write-gate-policy.js +14 -3
- package/dist/core/hooks.d.ts +8 -5
- package/dist/core/memory-engine/engine.d.ts +11 -0
- package/dist/core/memory-engine/engine.js +29 -3
- package/dist/core/memory-engine/index.d.ts +1 -1
- package/dist/core/memory-engine/origin-clearance.d.ts +28 -0
- package/dist/core/remote-env.d.ts +34 -2
- package/dist/core/runner/prepare-config-doors.d.ts +2 -2
- package/dist/core/runner/prepare-config-doors.js +11 -8
- package/dist/core/runner/prepare-task.d.ts +87 -5
- package/dist/core/runner/prepare-task.js +174 -92
- package/dist/core/runner/prepare-workspace-restore.js +13 -0
- package/dist/core/runner/runtask.js +203 -152
- package/dist/core/trace.d.ts +5 -4
- package/dist/core/types.d.ts +38 -20
- package/dist/core/usage-window-store.d.ts +44 -12
- package/dist/core/usage-window-store.js +11 -3
- package/dist/core/workflow-run-store-contract.js +17 -0
- package/dist/core/workflow-run-store.d.ts +22 -1
- package/dist/core/workflow-run-store.js +1 -0
- package/dist/engine/harness/agent-harness.d.ts +8 -3
- package/dist/engine/harness/agent-harness.js +29 -9
- package/dist/engine/harness/types.d.ts +127 -3
- package/dist/engine/loop/agent-loop.js +47 -4
- package/dist/engine/loop/types.d.ts +67 -6
- package/dist/index.d.ts +2 -2
- package/dist/internal/harness-types.d.ts +1 -1
- package/dist/orchestration/run-workflow-tool.d.ts +6 -0
- package/dist/orchestration/run-workflow-tool.js +1 -0
- package/dist/orchestration/workflow-types.d.ts +48 -1
- package/dist/orchestration/workflow-types.js +12 -4
- package/dist/orchestration/workflow.d.ts +18 -1
- package/dist/orchestration/workflow.js +45 -19
- package/dist/prompts/default.js +1 -1
- package/dist/tools/fs/bash-readonly-classifier.d.ts +44 -1
- package/dist/tools/fs/bash-readonly-classifier.js +132 -5
- package/dist/tools/fs/fs-bash.js +9 -2
- package/dist/tools/fs/fs-write.js +19 -8
- package/dist/tools/fs/index.d.ts +1 -0
- package/dist/tools/fs/index.js +1 -0
- package/package.json +1 -1
- package/test/export-surface.snapshot.json +7 -1
|
@@ -40,6 +40,34 @@ export interface OriginClearanceRow {
|
|
|
40
40
|
tombstonedAt?: number;
|
|
41
41
|
events: OriginClearanceEvent[];
|
|
42
42
|
}
|
|
43
|
+
/**
|
|
44
|
+
* #468② — the ACCOUNT SHADOW a `memory.origin_clear_not_marked` refusal carries when the engine already
|
|
45
|
+
* held the answer: the entry's most recent COMPLETED clearance, as who asked for it and when it settled.
|
|
46
|
+
*
|
|
47
|
+
* It exists to discriminate the refusal's two very different causes without a second full read of the
|
|
48
|
+
* account: an entry that was NEVER marked (no shadow — nothing to clear, and nothing ever cleared) versus a
|
|
49
|
+
* RE-SEND of a clear that already succeeded (shadow present — the caller's own earlier call, or another
|
|
50
|
+
* host's, already did this). Absence is honest absence: the engine never opens an extra read to fill it, so
|
|
51
|
+
* a refusal raised on a path that did not need the ledger simply omits it.
|
|
52
|
+
*
|
|
53
|
+
* `requestId` is the OPENER's attribution (the `clearEntryOrigin` caller's own key — what a re-sending
|
|
54
|
+
* caller compares against), while `settledAt` is the row's terminal `done` event, which on a crash-resumed
|
|
55
|
+
* row is a LATER moment than the opening. The full row (reason, origin, custody, every event) stays one
|
|
56
|
+
* {@link OriginClearanceRow} lookup away — this shadow is a discriminator, never a replacement for it.
|
|
57
|
+
*
|
|
58
|
+
* AS-OF SEMANTICS (codex r3): the account is read ONCE per `clearEntryOrigin` call, by the probe that looks
|
|
59
|
+
* for a resumable pending row, and the committed entry is read after it. A clearance opened AND settled by
|
|
60
|
+
* ANOTHER caller inside that window is therefore not in the snapshot this shadow is derived from, so its
|
|
61
|
+
* absence means "no settled clearance as of this call's own read of the account", never "none exists" — and
|
|
62
|
+
* a fresh call answers with the newer account. The caller's OWN earlier clear (the re-send case this exists
|
|
63
|
+
* for) settled long before and is always in the snapshot. Deliberately not closed by a second read: the
|
|
64
|
+
* shadow is a convenience over the authoritative `listOriginClearances`, and buying strictness with an extra
|
|
65
|
+
* ledger read on a refusal path would cost every caller what only a concurrent third party could observe.
|
|
66
|
+
*/
|
|
67
|
+
export interface OriginClearanceShadow {
|
|
68
|
+
requestId: string;
|
|
69
|
+
settledAt: number;
|
|
70
|
+
}
|
|
43
71
|
/** Lock-free strict read of the whole account (host audit face; journal-aware, corrupt = throw). */
|
|
44
72
|
export declare function readOriginClearances(controlDir: string): OriginClearanceRow[];
|
|
45
73
|
/**
|
|
@@ -58,6 +58,13 @@ export interface WorkspaceHandle {
|
|
|
58
58
|
* `snapshotId` as corruption (fail-closed) unless the resumed env is itself non-suspendable
|
|
59
59
|
* (tolerance for park handles minted by 1.257.1 before this field existed). */
|
|
60
60
|
restoreMode?: "park_only";
|
|
61
|
+
/**
|
|
62
|
+
* design/380 O1④ — explicit identity of the physical/target device this workspace lives on
|
|
63
|
+
* (device lane; an SSH/ADB adapter MAY also stamp it). MINTER-STATED: consumers read this field,
|
|
64
|
+
* never parse an identity out of `sandboxId`. Plain string → rides CheckpointState.workspaceHandle
|
|
65
|
+
* (all-string whitelist) with zero schema movement.
|
|
66
|
+
*/
|
|
67
|
+
deviceId?: string;
|
|
61
68
|
}
|
|
62
69
|
/**
|
|
63
70
|
* A reference to a secret injected at {@link RemoteExecutionEnv.connect}/{@link RemoteExecutionEnv.postResumeInit}
|
|
@@ -213,6 +220,16 @@ export type RemoteExecutionErrorCode =
|
|
|
213
220
|
* else a retry whitelist misses them.
|
|
214
221
|
*/
|
|
215
222
|
| "transport_lost"
|
|
223
|
+
/**
|
|
224
|
+
* design/380 O12 — the op was COMMITTED to the remote target and its outcome is UNKNOWABLE (the target
|
|
225
|
+
* went unreachable / restarted / was revoked after dispatch-commit; an execStream cut where the target
|
|
226
|
+
* protocol's own server has ruled the outcome unknowable). NEVER auto-retried and deliberately NOT in
|
|
227
|
+
* {@link RETRYABLE_REMOTE_ERROR_CODES}: the command may have already executed — a retry whitelist that
|
|
228
|
+
* contained it would re-drive committed side effects. Distinct from `transport_lost` (connection story
|
|
229
|
+
* known, idempotency-gated retry after reconnect). Message MUST carry the verify-first sentence ("may
|
|
230
|
+
* have already executed on the target; verify its effect before re-running").
|
|
231
|
+
*/
|
|
232
|
+
| "outcome_unknown"
|
|
216
233
|
/** Unclassified provider/transport failure. */
|
|
217
234
|
| "unknown";
|
|
218
235
|
/**
|
|
@@ -220,8 +237,10 @@ export type RemoteExecutionErrorCode =
|
|
|
220
237
|
* can succeed (`auth_transient`: retry-exactly-once after the device is authorized; `connect_failed`: the
|
|
221
238
|
* workspace could not be reached/provisioned this attempt; `timeout`: a liveness bound tripped;
|
|
222
239
|
* `transport_lost`: re-establish the connection). Every OTHER code is permanent for this attempt
|
|
223
|
-
* (`auth_failed`/`unsupported`), caller-driven (`aborted`),
|
|
224
|
-
*
|
|
240
|
+
* (`auth_failed`/`unsupported`), caller-driven (`aborted`), unclassifiable (`unknown`), or
|
|
241
|
+
* outcome-unknowable (`outcome_unknown` — design/380 O12: the op may have ALREADY executed, so a retry
|
|
242
|
+
* re-drives committed side effects) — retrying them burns budget or, worse, re-drives a rejected
|
|
243
|
+
* credential or a committed op.
|
|
225
244
|
*
|
|
226
245
|
* ONE list, two consumers, so a caller's retry decision and its DISCLOSURE can never disagree: the engine
|
|
227
246
|
* retries only IDEMPOTENT ops on these codes (see {@link withRetry}'s red line — a snapshot-taking
|
|
@@ -405,6 +424,19 @@ export interface RemoteExecutionEnv extends ExecutionEnv {
|
|
|
405
424
|
export interface ExecutionEnvFactoryContext {
|
|
406
425
|
/** Resolved session id for the task — the stable identity of its per-task workspace. */
|
|
407
426
|
sessionId: string;
|
|
427
|
+
/**
|
|
428
|
+
* design/380 O1① — the run TREE's placement root (fixed point), minted by prepare AFTER session
|
|
429
|
+
* resolution as `internals.placementRoot ?? internals.rootSessionId ?? sessionId` — a top-level
|
|
430
|
+
* run's own resolved sessionId, every descendant's inherited root, verbatim (delegation and
|
|
431
|
+
* workflow spawn chains re-thread an explicit `placementRoot` — C12; the ONE boundary it does not
|
|
432
|
+
* yet cross is a bare DURABLE RESUME, which falls back to the checkpointed session until
|
|
433
|
+
* design/380 O1③'s checkpoint carriage lands — residual pinned). A target-bound
|
|
434
|
+
* factory (device lane) keys its placement lookup on THIS, never on `sessionId` (a child's fresh
|
|
435
|
+
* session id must not read as a new placement). Always present and non-empty — an empty value is
|
|
436
|
+
* an assembly error the engine refuses loudly at the mint (`config.placement_root_invalid`) and a
|
|
437
|
+
* factory MUST refuse loudly too (loud-bad-value posture), never default around.
|
|
438
|
+
*/
|
|
439
|
+
placementRootSessionId: string;
|
|
408
440
|
/** Caller-supplied task id, when set on the `TaskSpec`. */
|
|
409
441
|
taskId?: string;
|
|
410
442
|
/**
|
|
@@ -158,7 +158,7 @@ export interface PrepareConfigDoorsResult {
|
|
|
158
158
|
* for the door + placement reasoning). The per-run state builder consumes exactly these values —
|
|
159
159
|
* the deps bag is never re-read after this door. */
|
|
160
160
|
microCompactKnob: {
|
|
161
|
-
machine: ContextEditMachine;
|
|
161
|
+
machine: "off" | ContextEditMachine;
|
|
162
162
|
clearOnRejection: boolean;
|
|
163
163
|
};
|
|
164
164
|
/** owned — the resolved role; its `systemPrompt` seat is still read at prompt-input time. */
|
|
@@ -216,7 +216,7 @@ export interface PrepareConfigDoorsResult {
|
|
|
216
216
|
* it must refuse exactly like a malformed field, never fold to the defaults. The RESOLVED values
|
|
217
217
|
* returned here are immutable and are the state builder's ONLY source — nothing re-reads the bag. */
|
|
218
218
|
export declare function resolveMicroCompactKnob(bag: RunnerDeps["microCompact"]): {
|
|
219
|
-
machine: ContextEditMachine;
|
|
219
|
+
machine: "off" | ContextEditMachine;
|
|
220
220
|
clearOnRejection: boolean;
|
|
221
221
|
};
|
|
222
222
|
export declare function prepareConfigDoors(input: PrepareConfigDoorsInput): PrepareConfigDoorsResult;
|
|
@@ -123,8 +123,8 @@ function microCompactConfigError(field, value, legal) {
|
|
|
123
123
|
}
|
|
124
124
|
const seat = field === undefined ? "microCompact" : `microCompact.${field}`;
|
|
125
125
|
const e = new Error(`${seat} ${shown} is not ${legal} — an unevaluable declaration is refused loudly, ` +
|
|
126
|
-
`never folded to the default: a silently-ignored
|
|
127
|
-
`deployment believes
|
|
126
|
+
`never folded to the default: a silently-ignored declaration would run the DEFAULT machine ` +
|
|
127
|
+
`while the deployment believes its opt-in or opt-out took effect.`);
|
|
128
128
|
e.code = "config.microcompact_invalid";
|
|
129
129
|
return e;
|
|
130
130
|
}
|
|
@@ -133,14 +133,17 @@ export function resolveMicroCompactKnob(bag) {
|
|
|
133
133
|
throw microCompactConfigError(undefined, bag, "an object carrying optional machine/clearOnRejection keys");
|
|
134
134
|
}
|
|
135
135
|
const declaredMachine = bag?.machine;
|
|
136
|
-
if (declaredMachine !== undefined && declaredMachine !== "legacy" && declaredMachine !== "cc") {
|
|
137
|
-
throw microCompactConfigError("machine", declaredMachine, `"legacy" | "cc"`);
|
|
136
|
+
if (declaredMachine !== undefined && declaredMachine !== "off" && declaredMachine !== "legacy" && declaredMachine !== "cc") {
|
|
137
|
+
throw microCompactConfigError("machine", declaredMachine, `"off" | "legacy" | "cc"`);
|
|
138
138
|
}
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
139
|
+
return { machine: declaredMachine ?? "cc", clearOnRejection: resolveClearOnRejection(bag) };
|
|
140
|
+
}
|
|
141
|
+
function resolveClearOnRejection(bag) {
|
|
142
|
+
const declared = bag?.clearOnRejection;
|
|
143
|
+
if (declared !== undefined && typeof declared !== "boolean") {
|
|
144
|
+
throw microCompactConfigError("clearOnRejection", declared, "a boolean");
|
|
142
145
|
}
|
|
143
|
-
return
|
|
146
|
+
return declared ?? true;
|
|
144
147
|
}
|
|
145
148
|
export function prepareConfigDoors(input) {
|
|
146
149
|
const { deps, sessions, resume, internals } = input;
|
|
@@ -145,6 +145,51 @@ export declare function checkpointScopeOf(spec: {
|
|
|
145
145
|
export { resolveCheckpointStore } from "../checkpoint-store.js";
|
|
146
146
|
export { isFableFamilyModelId, resolveAttachmentsConfig, resolveModelPromptTraits, resolveTaskLimits } from "./prepare-config-doors.js";
|
|
147
147
|
export { rebaseWorkspacePath, rebaseWorkspacePathAcross } from "./prepare-workspace-restore.js";
|
|
148
|
+
/** See {@link runGuardChain}. */
|
|
149
|
+
export interface GuardChainArgs {
|
|
150
|
+
/** The post-frontier view the chain starts from. */
|
|
151
|
+
edited: AgentMessage[];
|
|
152
|
+
/** The pre-cap counterpart view (arm A's clearSource — RB-212 durable composition seat). */
|
|
153
|
+
replayed: AgentMessage[];
|
|
154
|
+
index: OccurrenceIndex;
|
|
155
|
+
microCompact: PreparedMicroCompact;
|
|
156
|
+
guardAt: number;
|
|
157
|
+
charsPerToken: number;
|
|
158
|
+
offloadStore: import("../tool-result-store.js").ToolResultStore | undefined;
|
|
159
|
+
sessionId: string;
|
|
160
|
+
taskId: string;
|
|
161
|
+
deps: RunnerDeps;
|
|
162
|
+
trimPressureRef: {
|
|
163
|
+
droppedMessages: boolean;
|
|
164
|
+
};
|
|
165
|
+
recheck: boolean | undefined;
|
|
166
|
+
signal: AbortSignal | undefined;
|
|
167
|
+
/** The r7 per-request observation buffer — the chain DROPS it on its early returns (a discarded
|
|
168
|
+
* provisional projection must not narrate itself). */
|
|
169
|
+
pendingProjectionObservations: Array<() => void>;
|
|
170
|
+
}
|
|
171
|
+
export type GuardChainOutcome = {
|
|
172
|
+
earlyReturn: {
|
|
173
|
+
messages: AgentMessage[];
|
|
174
|
+
adoptSessionRebuild?: boolean;
|
|
175
|
+
};
|
|
176
|
+
} | {
|
|
177
|
+
working: AgentMessage[];
|
|
178
|
+
trimmed: AgentMessage[];
|
|
179
|
+
trimDroppedMessages: boolean;
|
|
180
|
+
};
|
|
181
|
+
/**
|
|
182
|
+
* design/374 slice 3 — the GUARD CHAIN over one request build (D-7 case-ii transfer table),
|
|
183
|
+
* module-level per the design/238 body-span bank. design/123 D3: the chain reads the anchored
|
|
184
|
+
* coordinate re-estimated on the EDITED array (pre-anchor clears don't lower it — deliberately;
|
|
185
|
+
* trimToBudget doc). The opt-out machine = the pre-374 backstop order byte-identical
|
|
186
|
+
* (trim as the ordinary second line); otherwise arm A (blocking machine run, only when the
|
|
187
|
+
* frontier pass is off) → arm B (in-turn forced compaction behind the adopt seam) → arm C (trim,
|
|
188
|
+
* the disaster-only last resort and this chain's sole `context.trim` emitter). Returns either the
|
|
189
|
+
* post-chain view for the pipeline tail, or an EARLY hook result the context handler must return
|
|
190
|
+
* verbatim (arm-B adoption / the r8 dead-turn stand-down).
|
|
191
|
+
*/
|
|
192
|
+
export declare function runGuardChain(args: GuardChainArgs): Promise<GuardChainOutcome>;
|
|
148
193
|
export interface Prepared {
|
|
149
194
|
harness: AgentHarness;
|
|
150
195
|
/** The CONCRETE built-in session (engine-internal: prepare constructs/acquires `StoredSession` itself,
|
|
@@ -886,21 +931,38 @@ export interface Prepared {
|
|
|
886
931
|
trimPressureRef: {
|
|
887
932
|
droppedMessages: boolean;
|
|
888
933
|
};
|
|
889
|
-
/** design/374 slices 1b/2 — the microCompact machine state this run: the selected clearing
|
|
934
|
+
/** design/374 slices 1b/2/3 — the microCompact machine state this run: the selected clearing
|
|
890
935
|
* machine, the cleared-projection ledger (request-view application, durable decisions — see
|
|
891
936
|
* `context-edit.ts`'s ledger note; per-run in-memory, so durable resume / `resumeAt` rebuilds
|
|
892
937
|
* start EMPTY by construction), the last request's projection seat (what the provider actually
|
|
893
938
|
* saw — the MC-R rejection arm computes its candidates and savings on THIS view, never on the
|
|
894
|
-
* raw session rebuild),
|
|
895
|
-
* never gains an entry and every replay is a
|
|
939
|
+
* raw session rebuild), the MC-R knob, and the slice-3 arm-B seat. The explicit opt-out
|
|
940
|
+
* (`machine: "legacy"` + MC-R off) ⇒ the ledger never gains an entry and every replay is a
|
|
941
|
+
* same-reference no-op (opt-out bytes unchanged). */
|
|
896
942
|
microCompact: PreparedMicroCompact;
|
|
897
943
|
}
|
|
898
944
|
/** See {@link Prepared.microCompact}. */
|
|
899
945
|
export interface PreparedMicroCompact {
|
|
900
|
-
machine
|
|
946
|
+
/** The frontier-machine selection — `"off"` = no proactive frontier clearing (the unified
|
|
947
|
+
* machine instead gets its one blocking-point shot, slice-3 arm A). */
|
|
948
|
+
machine: "off" | ContextEditMachine;
|
|
901
949
|
/** MC-R (design/374 §3.2): one-shot clear-and-retry on a provider input-too-long rejection.
|
|
902
|
-
* Default
|
|
950
|
+
* Default true since the slice-3 flip. */
|
|
903
951
|
clearOnRejection: boolean;
|
|
952
|
+
/** design/374 slice 3 (arm B) — the in-turn forced-compaction seat: runtask wires a closure
|
|
953
|
+
* that runs the SAME forced-compaction pass the prompt-too-long recovery uses (gates included)
|
|
954
|
+
* and answers whether a compaction landed in the session. The context hook calls it when the
|
|
955
|
+
* pre-send estimate breaks the guard budget and then returns `adoptSessionRebuild` so the
|
|
956
|
+
* harness adopts the reduced transcript. `signal` is the TURN-scoped abort of the request
|
|
957
|
+
* build (r3): a turn interrupt must be able to cut the summary call short instead of waiting
|
|
958
|
+
* it out. `anchoredEstimate` is the chain's own trigger coordinate — the seat consults the
|
|
959
|
+
* §25.2 anti-thrash floor against it (an ineffective landing must not be repeated per request
|
|
960
|
+
* build; the chain's arm C owns the bounded fallback). Unwired (pure-prepare callers) ⇒ arm B
|
|
961
|
+
* declines and the chain falls to the trim last resort — same posture as a compaction-disabled
|
|
962
|
+
* run. */
|
|
963
|
+
inTurnCompactionRef: {
|
|
964
|
+
current?: (signal?: AbortSignal, anchoredEstimate?: number) => Promise<boolean>;
|
|
965
|
+
};
|
|
904
966
|
ledger: ClearedProjectionLedger;
|
|
905
967
|
projectionRef: {
|
|
906
968
|
current?: {
|
|
@@ -1436,6 +1498,26 @@ export interface RunInternals {
|
|
|
1436
1498
|
* after a restart those intermediate sessions are dead ends, and a recovery face enumerating
|
|
1437
1499
|
* "everything under this host session" needs the root anchor, not an alias walk. */
|
|
1438
1500
|
rootSessionId?: string;
|
|
1501
|
+
/**
|
|
1502
|
+
* design/380 O1② — the run tree's PLACEMENT root: the fixed point a target-bound env factory keys
|
|
1503
|
+
* its placement lookup on ({@link import("../remote-env.js").ExecutionEnvFactoryContext.placementRootSessionId}).
|
|
1504
|
+
* A SEPARATE axis from {@link rootSessionId} deliberately: that field means "member of this host
|
|
1505
|
+
* session's DELEGATION tree" and is consumed by the registry access/recovery faces — cascade rungs
|
|
1506
|
+
* and verification legs are intentionally NOT members of that tree (independent cold re-runs in
|
|
1507
|
+
* their own sessions), so widening `rootSessionId` to cover them would corrupt the recovery faces'
|
|
1508
|
+
* reading. This member says only "place me where this session was placed". Producers: the
|
|
1509
|
+
* orchestration entries (`runCascade` / `runWithVerification` family) after their first leg's
|
|
1510
|
+
* sessionId receipt, and — C12 — the delegation/workflow spawn chains, which re-thread a parent's
|
|
1511
|
+
* EXPLICIT value verbatim into child internals (ToolExecuteContext.placementRoot → childInternals;
|
|
1512
|
+
* workflow deps → shared internals base), so every descendant of a placed leg keeps the fixed
|
|
1513
|
+
* point; prepare's mint reads it first (`placementRoot ?? rootSessionId ?? sessionId`). Absent
|
|
1514
|
+
* everywhere else — the delegation lanes' `rootSessionId` fixed point then becomes the placement
|
|
1515
|
+
* root through the middle segment, unchanged. NOT yet durable (C12 residual, pinned): a bare
|
|
1516
|
+
* durable resume cannot recover it until design/380 O1③'s checkpoint carriage lands — the
|
|
1517
|
+
* resumed leg falls back to its own session; a resume caller needing continuity re-supplies the
|
|
1518
|
+
* member itself. TRUSTED run-scoped channel (never a {@link TaskSpec} field).
|
|
1519
|
+
*/
|
|
1520
|
+
placementRoot?: string;
|
|
1439
1521
|
/**
|
|
1440
1522
|
* RB-429 — the REGISTRY SCOPE this run's own background row lives in: the domain its registry-facing
|
|
1441
1523
|
* tools (TaskOutput / TaskStop / SendMessage / AgentTranscript / Monitor, and the announce listing)
|
|
@@ -345,6 +345,7 @@ function buildMicroCompactState(deps, model, sessionId, offloadStore, knob) {
|
|
|
345
345
|
clearOnRejection: knob.clearOnRejection,
|
|
346
346
|
ledger: createClearedProjectionLedger(),
|
|
347
347
|
projectionRef: {},
|
|
348
|
+
inTurnCompactionRef: {},
|
|
348
349
|
...(offloadStore ? { offloadPersist: createOffloadPersist(offloadStore, sessionId, deps.onNotice) } : {}),
|
|
349
350
|
};
|
|
350
351
|
}
|
|
@@ -363,10 +364,75 @@ function recordFrontierClears(args) {
|
|
|
363
364
|
taskId: args.taskId,
|
|
364
365
|
clearedCount: args.pass.clears.length,
|
|
365
366
|
tokensSavedEstimate: args.pass.tokensSavedEstimate,
|
|
366
|
-
trigger:
|
|
367
|
+
trigger: args.trigger,
|
|
367
368
|
ts: Date.now(),
|
|
368
369
|
}));
|
|
369
370
|
}
|
|
371
|
+
function flushProjectionObservations(pending) {
|
|
372
|
+
for (const fire of pending.splice(0)) {
|
|
373
|
+
try {
|
|
374
|
+
fire();
|
|
375
|
+
}
|
|
376
|
+
catch {
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
export async function runGuardChain(args) {
|
|
381
|
+
const { edited, microCompact, guardAt, charsPerToken, deps, taskId } = args;
|
|
382
|
+
const applyGuardTrim = (view, anchoredTotal) => {
|
|
383
|
+
let trimmed = trimToBudget(view, guardAt, anchoredTotal, charsPerToken);
|
|
384
|
+
const dropped = trimmed.length < view.length;
|
|
385
|
+
if (dropped) {
|
|
386
|
+
args.trimPressureRef.droppedMessages = true;
|
|
387
|
+
const droppedCount = view.length - trimmed.length;
|
|
388
|
+
trimmed = insertTrimNotice(trimmed);
|
|
389
|
+
emitTrace(deps.tracer, () => ({
|
|
390
|
+
kind: "context.trim",
|
|
391
|
+
version: 1,
|
|
392
|
+
taskId,
|
|
393
|
+
dropped: droppedCount,
|
|
394
|
+
ts: Date.now(),
|
|
395
|
+
}));
|
|
396
|
+
}
|
|
397
|
+
return { trimmed, dropped };
|
|
398
|
+
};
|
|
399
|
+
let working = edited;
|
|
400
|
+
let anchoredWorking = estimateContextTokens(working, charsPerToken).tokens;
|
|
401
|
+
if (microCompact.machine === "legacy") {
|
|
402
|
+
const t = applyGuardTrim(working, anchoredWorking);
|
|
403
|
+
return { working, trimmed: t.trimmed, trimDroppedMessages: t.dropped };
|
|
404
|
+
}
|
|
405
|
+
if (microCompact.machine === "off" && anchoredWorking > guardAt) {
|
|
406
|
+
let blockingPass;
|
|
407
|
+
const blockingEdited = clearStaleToolResults(working, {
|
|
408
|
+
budgetTokens: guardAt,
|
|
409
|
+
machine: "cc",
|
|
410
|
+
clearSource: args.replayed,
|
|
411
|
+
onCleared: (pass) => { blockingPass = pass; },
|
|
412
|
+
anchoredTotalTokens: anchoredWorking,
|
|
413
|
+
charsPerToken,
|
|
414
|
+
...(args.offloadStore ? { offload: { persist: createOffloadPersist(args.offloadStore, args.sessionId, deps.onNotice) } } : {}),
|
|
415
|
+
});
|
|
416
|
+
if (blockingPass !== undefined) {
|
|
417
|
+
recordFrontierClears({ pass: blockingPass, index: args.index, edited: blockingEdited, ledger: microCompact.ledger, tracer: deps.tracer, taskId, trigger: "blocking" });
|
|
418
|
+
working = blockingEdited;
|
|
419
|
+
anchoredWorking = estimateContextTokens(working, charsPerToken).tokens;
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
if (anchoredWorking > guardAt && args.recheck !== true) {
|
|
423
|
+
const compacted = (await microCompact.inTurnCompactionRef.current?.(args.signal, anchoredWorking)) === true;
|
|
424
|
+
if (compacted) {
|
|
425
|
+
args.pendingProjectionObservations.length = 0;
|
|
426
|
+
return { earlyReturn: { messages: working, adoptSessionRebuild: true } };
|
|
427
|
+
}
|
|
428
|
+
if (args.signal?.aborted === true) {
|
|
429
|
+
args.pendingProjectionObservations.length = 0;
|
|
430
|
+
return { working, trimmed: working, trimDroppedMessages: false };
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
const t = applyGuardTrim(working, anchoredWorking);
|
|
434
|
+
return { working, trimmed: t.trimmed, trimDroppedMessages: t.dropped };
|
|
435
|
+
}
|
|
370
436
|
export function gatedCallIdOf(p) {
|
|
371
437
|
if (p.suspendRef.token !== undefined)
|
|
372
438
|
return p.suspendRef.gatedCallId;
|
|
@@ -615,6 +681,15 @@ async function derivedRouteFallsBack(args) {
|
|
|
615
681
|
return false;
|
|
616
682
|
}
|
|
617
683
|
}
|
|
684
|
+
function mintPlacementRootSessionId(internals, sessionId) {
|
|
685
|
+
const placementRootSessionId = internals?.placementRoot ?? internals?.rootSessionId ?? sessionId;
|
|
686
|
+
if (placementRootSessionId === "") {
|
|
687
|
+
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
|
+
e.code = "config.placement_root_invalid";
|
|
689
|
+
throw e;
|
|
690
|
+
}
|
|
691
|
+
return placementRootSessionId;
|
|
692
|
+
}
|
|
618
693
|
export async function prepareTask(spec, deps, sessions, resume, internals, runnerSelf) {
|
|
619
694
|
const doors = prepareConfigDoors({ spec, deps, sessions, resume, internals });
|
|
620
695
|
spec = doors.spec;
|
|
@@ -723,7 +798,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
723
798
|
try {
|
|
724
799
|
ownedEnv = deps.executionEnvFactory
|
|
725
800
|
? await deps.executionEnvFactory({
|
|
726
|
-
sessionId,
|
|
801
|
+
sessionId, placementRootSessionId: mintPlacementRootSessionId(internals, sessionId),
|
|
727
802
|
taskId: spec.taskId,
|
|
728
803
|
...(internals?.isolation ? { isolation: internals.isolation } : {}),
|
|
729
804
|
...(internals?.parentCwd ? { parentCwd: internals.parentCwd } : {}),
|
|
@@ -1107,7 +1182,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
1107
1182
|
taskId: hostTaskId,
|
|
1108
1183
|
...(internals?.parentTaskId !== undefined ? { parentTaskId: internals.parentTaskId } : {}),
|
|
1109
1184
|
...(internals?.parentSessionId !== undefined ? { parentSessionId: internals.parentSessionId } : {}),
|
|
1110
|
-
...(internals?.rootSessionId !== undefined ? { rootSessionId: internals.rootSessionId } : {}),
|
|
1185
|
+
...(internals?.rootSessionId !== undefined ? { rootSessionId: internals.rootSessionId } : {}), ...(internals?.placementRoot !== undefined ? { placementRoot: internals.placementRoot } : {}),
|
|
1111
1186
|
...(internals?.explicitAgentName !== undefined ? { spawnedAgentName: internals.explicitAgentName } : {}),
|
|
1112
1187
|
...(internals?.peerSelfRef !== undefined ? { peerSelfRef: internals.peerSelfRef } : {}),
|
|
1113
1188
|
...(internals?.peerInboundChainRef !== undefined ? { peerInboundChainRef: internals.peerInboundChainRef } : {}),
|
|
@@ -1282,7 +1357,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
1282
1357
|
scope: taskScope,
|
|
1283
1358
|
notifier: deps.workflowCompletionNotifier,
|
|
1284
1359
|
originatingSessionId: sessionId,
|
|
1285
|
-
rootSessionId: internals?.rootSessionId ?? sessionId,
|
|
1360
|
+
rootSessionId: internals?.rootSessionId ?? sessionId, ...(internals?.placementRoot !== undefined ? { placementRoot: internals.placementRoot } : {}),
|
|
1286
1361
|
taskRegistry: defaultTaskRegistry,
|
|
1287
1362
|
taskNotification: internals?.onTaskNotification,
|
|
1288
1363
|
taskOwner: hostTaskId,
|
|
@@ -4621,101 +4696,108 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
4621
4696
|
const guardAt = guardBudget(model);
|
|
4622
4697
|
const microCompact = buildMicroCompactState(deps, model, sessionId, offloadStore, microCompactKnob);
|
|
4623
4698
|
const charsPerToken = model.charsPerToken ?? DEFAULT_CHARS_PER_TOKEN;
|
|
4624
|
-
harness.on("context", async ({ messages }) => {
|
|
4699
|
+
harness.on("context", async ({ messages, recheck, signal }) => {
|
|
4625
4700
|
batchHaltRef.current = undefined;
|
|
4626
|
-
const
|
|
4627
|
-
|
|
4628
|
-
|
|
4629
|
-
|
|
4630
|
-
|
|
4631
|
-
|
|
4632
|
-
|
|
4633
|
-
|
|
4634
|
-
|
|
4635
|
-
|
|
4636
|
-
|
|
4637
|
-
|
|
4638
|
-
|
|
4639
|
-
|
|
4640
|
-
|
|
4641
|
-
|
|
4642
|
-
|
|
4643
|
-
|
|
4644
|
-
|
|
4645
|
-
|
|
4646
|
-
|
|
4647
|
-
|
|
4648
|
-
|
|
4649
|
-
|
|
4650
|
-
|
|
4651
|
-
|
|
4652
|
-
|
|
4653
|
-
|
|
4654
|
-
|
|
4655
|
-
|
|
4656
|
-
: {}),
|
|
4657
|
-
|
|
4658
|
-
|
|
4659
|
-
|
|
4660
|
-
|
|
4661
|
-
|
|
4662
|
-
|
|
4663
|
-
|
|
4664
|
-
|
|
4665
|
-
|
|
4666
|
-
|
|
4667
|
-
|
|
4668
|
-
|
|
4669
|
-
|
|
4670
|
-
|
|
4671
|
-
|
|
4672
|
-
|
|
4673
|
-
|
|
4674
|
-
|
|
4675
|
-
|
|
4676
|
-
|
|
4677
|
-
|
|
4678
|
-
|
|
4679
|
-
|
|
4680
|
-
|
|
4681
|
-
|
|
4682
|
-
|
|
4683
|
-
|
|
4684
|
-
|
|
4685
|
-
|
|
4686
|
-
trimmed
|
|
4687
|
-
|
|
4688
|
-
|
|
4689
|
-
|
|
4690
|
-
|
|
4701
|
+
const pendingProjectionObservations = [];
|
|
4702
|
+
try {
|
|
4703
|
+
const healed = dropEmptyFailureAssistants(messages);
|
|
4704
|
+
const replay = replayClearedProjection(healed, microCompact.ledger);
|
|
4705
|
+
const replayed = replay.messages;
|
|
4706
|
+
const capped = await capAggregateToolResults(replayed, {
|
|
4707
|
+
store: offloadStore,
|
|
4708
|
+
sessionId,
|
|
4709
|
+
onCapped: (info) => pendingProjectionObservations.push(() => emitTrace(deps.tracer, () => ({
|
|
4710
|
+
kind: "tool_result.capped",
|
|
4711
|
+
version: 1,
|
|
4712
|
+
taskId: spec.taskId ?? sessionId,
|
|
4713
|
+
...(info.tool !== undefined ? { tool: info.tool } : {}),
|
|
4714
|
+
sizeChars: info.sizeChars,
|
|
4715
|
+
storeFallback: info.storeFallback,
|
|
4716
|
+
ts: Date.now(),
|
|
4717
|
+
}))),
|
|
4718
|
+
});
|
|
4719
|
+
const mediaCapped = capAggregateMediaBytes(capped, {
|
|
4720
|
+
limitBytes: deps.mediaByteCapBytes ?? AGGREGATE_MEDIA_BUDGET_BYTES,
|
|
4721
|
+
onStripped: (info) => {
|
|
4722
|
+
const onMediaStripped = deps.onMediaStripped;
|
|
4723
|
+
if (onMediaStripped !== undefined)
|
|
4724
|
+
pendingProjectionObservations.push(() => onMediaStripped(info));
|
|
4725
|
+
},
|
|
4726
|
+
});
|
|
4727
|
+
let ccPass;
|
|
4728
|
+
const edited = microCompact.machine === "off" ? mediaCapped : clearStaleToolResults(mediaCapped, {
|
|
4729
|
+
budgetTokens: editAt,
|
|
4730
|
+
machine: microCompact.machine,
|
|
4731
|
+
...(microCompact.machine === "cc" || microCompact.clearOnRejection ? { recognizeCcMarkers: true } : {}),
|
|
4732
|
+
...(microCompact.machine === "cc"
|
|
4733
|
+
? {
|
|
4734
|
+
clearSource: replayed,
|
|
4735
|
+
onCleared: (pass) => { ccPass = pass; },
|
|
4736
|
+
}
|
|
4737
|
+
: {}),
|
|
4738
|
+
anchoredTotalTokens: estimateContextTokens(mediaCapped, charsPerToken).tokens,
|
|
4739
|
+
charsPerToken,
|
|
4740
|
+
...(offloadStore
|
|
4741
|
+
? {
|
|
4742
|
+
offload: {
|
|
4743
|
+
persist: createOffloadPersist(offloadStore, sessionId, deps.onNotice),
|
|
4744
|
+
},
|
|
4745
|
+
}
|
|
4746
|
+
: {}),
|
|
4747
|
+
});
|
|
4748
|
+
if (ccPass !== undefined) {
|
|
4749
|
+
recordFrontierClears({ pass: ccPass, index: replay.index, edited, ledger: microCompact.ledger, tracer: deps.tracer, taskId: spec.taskId ?? sessionId, trigger: "frontier" });
|
|
4750
|
+
}
|
|
4751
|
+
const chain = await runGuardChain({
|
|
4752
|
+
edited, replayed, index: replay.index, microCompact, guardAt, charsPerToken, offloadStore,
|
|
4753
|
+
sessionId, taskId: spec.taskId ?? sessionId, deps, trimPressureRef, recheck, signal,
|
|
4754
|
+
pendingProjectionObservations,
|
|
4755
|
+
});
|
|
4756
|
+
if ("earlyReturn" in chain) {
|
|
4757
|
+
return chain.earlyReturn;
|
|
4758
|
+
}
|
|
4759
|
+
const { working, trimDroppedMessages } = chain;
|
|
4760
|
+
let trimmed = chain.trimmed;
|
|
4761
|
+
trimmed = applyGitFrameGuard({
|
|
4762
|
+
before: working,
|
|
4763
|
+
trimmed,
|
|
4764
|
+
budgetTokens: guardAt,
|
|
4765
|
+
ref: gitStatusRef,
|
|
4766
|
+
charsPerToken,
|
|
4767
|
+
onDegrade: (message) => {
|
|
4768
|
+
try {
|
|
4769
|
+
deps.onError?.(new Error(message), { phase: "degraded", sessionId, classification: "env-git-snapshot" });
|
|
4770
|
+
}
|
|
4771
|
+
catch {
|
|
4772
|
+
}
|
|
4773
|
+
},
|
|
4774
|
+
});
|
|
4775
|
+
const swept = dropOrphanToolResults(trimmed);
|
|
4776
|
+
if (swept.dropped.length > 0) {
|
|
4691
4777
|
try {
|
|
4692
|
-
deps.onError?.(new Error(
|
|
4778
|
+
deps.onError?.(new Error(`context belt: dropped ${swept.dropped.length} orphan toolResult(s) whose tool-call assistant was not in the request view — ` +
|
|
4779
|
+
swept.dropped
|
|
4780
|
+
.map((d) => `toolCallId=${d.toolCallId} at index ${d.index} (prev=${d.prevRole}, next=${d.nextRole})`)
|
|
4781
|
+
.join("; ")), { phase: "hook", sessionId });
|
|
4693
4782
|
}
|
|
4694
4783
|
catch {
|
|
4695
4784
|
}
|
|
4696
|
-
},
|
|
4697
|
-
});
|
|
4698
|
-
const swept = dropOrphanToolResults(trimmed);
|
|
4699
|
-
if (swept.dropped.length > 0) {
|
|
4700
|
-
try {
|
|
4701
|
-
deps.onError?.(new Error(`context belt: dropped ${swept.dropped.length} orphan toolResult(s) whose tool-call assistant was not in the request view — ` +
|
|
4702
|
-
swept.dropped
|
|
4703
|
-
.map((d) => `toolCallId=${d.toolCallId} at index ${d.index} (prev=${d.prevRole}, next=${d.nextRole})`)
|
|
4704
|
-
.join("; ")), { phase: "hook", sessionId });
|
|
4705
|
-
}
|
|
4706
|
-
catch {
|
|
4707
4785
|
}
|
|
4786
|
+
requestLossyRef.current =
|
|
4787
|
+
replayed !== healed ||
|
|
4788
|
+
capped !== replayed ||
|
|
4789
|
+
mediaCapped !== capped ||
|
|
4790
|
+
edited !== mediaCapped ||
|
|
4791
|
+
working !== edited ||
|
|
4792
|
+
trimDroppedMessages ||
|
|
4793
|
+
swept.dropped.length > 0 ||
|
|
4794
|
+
gitStatusRef.overBudgetShrunk === true;
|
|
4795
|
+
microCompact.projectionRef.current = { messages: swept.messages, keyOf: replay.index.keyOf };
|
|
4796
|
+
return { messages: swept.messages };
|
|
4797
|
+
}
|
|
4798
|
+
finally {
|
|
4799
|
+
flushProjectionObservations(pendingProjectionObservations);
|
|
4708
4800
|
}
|
|
4709
|
-
requestLossyRef.current =
|
|
4710
|
-
replayed !== healed ||
|
|
4711
|
-
capped !== replayed ||
|
|
4712
|
-
mediaCapped !== capped ||
|
|
4713
|
-
edited !== mediaCapped ||
|
|
4714
|
-
trimDroppedMessages ||
|
|
4715
|
-
swept.dropped.length > 0 ||
|
|
4716
|
-
gitStatusRef.overBudgetShrunk === true;
|
|
4717
|
-
microCompact.projectionRef.current = { messages: swept.messages, keyOf: replay.index.keyOf };
|
|
4718
|
-
return { messages: swept.messages };
|
|
4719
4801
|
});
|
|
4720
4802
|
const cacheBreakDetector = deps.cacheBreakDetection === false ? undefined : new CacheBreakDetector();
|
|
4721
4803
|
const cacheFingerprint = cacheBreakDetector
|
|
@@ -49,6 +49,16 @@ export async function prepareWorkspaceRestore(input) {
|
|
|
49
49
|
throw e;
|
|
50
50
|
};
|
|
51
51
|
const handle = resume.workspaceHandle;
|
|
52
|
+
const refuseIdentityMismatch = (resumedHandle) => {
|
|
53
|
+
if (resumedHandle.provider !== handle.provider) {
|
|
54
|
+
failResume(`resume workspace identity mismatch: the checkpoint records provider "${handle.provider}" but the resumed env reports "${resumedHandle.provider}" — refusing to continue a workspace on a different backend (wire the factory to rebuild the checkpointed lane and re-resume)`);
|
|
55
|
+
}
|
|
56
|
+
if (handle.deviceId !== undefined && resumedHandle.deviceId !== handle.deviceId) {
|
|
57
|
+
failResume(resumedHandle.deviceId === undefined
|
|
58
|
+
? `resume workspace identity mismatch: the checkpoint records deviceId "${handle.deviceId}" but the resumed env reports none — a device-bound workspace cannot be verified on a handle that does not state its device (absence is not a match)`
|
|
59
|
+
: `resume workspace identity mismatch: the checkpoint records deviceId "${handle.deviceId}" but the resumed env reports "${resumedHandle.deviceId}" — refusing to continue on a different physical device`);
|
|
60
|
+
}
|
|
61
|
+
};
|
|
52
62
|
if (ownedEnv === undefined || !isRemoteExecutionEnv(ownedEnv)) {
|
|
53
63
|
failResume("resume needs a RemoteExecutionEnv from executionEnvFactory to restore the workspace snapshot");
|
|
54
64
|
}
|
|
@@ -56,6 +66,7 @@ export async function prepareWorkspaceRestore(input) {
|
|
|
56
66
|
if (handle.restoreMode !== "park_only" && ownedEnv.capabilities.suspendable) {
|
|
57
67
|
failResume("checkpoint workspaceHandle has no snapshotId and is not a park_only handle, but the resumed env is suspendable — refusing to resume on a possibly-unrestored workspace (corrupt checkpoint?)");
|
|
58
68
|
}
|
|
69
|
+
refuseIdentityMismatch(ownedEnv.workspaceHandle());
|
|
59
70
|
if (handle.mountPath && handle.mountPath !== taskRootPath) {
|
|
60
71
|
taskRootPath = handle.mountPath;
|
|
61
72
|
}
|
|
@@ -68,6 +79,7 @@ export async function prepareWorkspaceRestore(input) {
|
|
|
68
79
|
if (missingHere.length > 0) {
|
|
69
80
|
failResume(`the resumed execution env cannot restore a workspace snapshot: its adapter does not implement ${missingHere.join(" or ")}. The checkpoint holds snapshot "${handle.snapshotId}" — wire an adapter that implements the full RemoteExecutionEnv restore surface and re-resume.`);
|
|
70
81
|
}
|
|
82
|
+
refuseIdentityMismatch(ownedEnv.workspaceHandle());
|
|
71
83
|
const { outcome: restored, attempts: restoreAttempts } = await restoreWorkspaceWithRetry(ownedEnv, handle.snapshotId, {
|
|
72
84
|
abortSignal: restoreSignal,
|
|
73
85
|
priorHandle: handle,
|
|
@@ -76,6 +88,7 @@ export async function prepareWorkspaceRestore(input) {
|
|
|
76
88
|
failResume(`resumeVM failed after ${restoreAttempts} attempt(s) (${restored.error.code}): ${restored.error.message}`, restored.error, remoteEnvFailureNote("resumeVM", restored.error, restoreAttempts));
|
|
77
89
|
}
|
|
78
90
|
else {
|
|
91
|
+
refuseIdentityMismatch(restored.value);
|
|
79
92
|
const restoredEnv = ownedEnv;
|
|
80
93
|
const canonicalInEnv = async (p) => {
|
|
81
94
|
try {
|