@sema-agent/core 7.5.1 → 7.5.2

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.
Files changed (66) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/dist/core/protocol-table.d.ts +5 -0
  3. package/dist/core/protocol-table.js +1 -0
  4. package/dist/core/runner/abort-race.d.ts +41 -0
  5. package/dist/core/runner/abort-race.js +38 -0
  6. package/dist/core/runner/checkpoint-scope.d.ts +15 -3
  7. package/dist/core/runner/checkpoint-scope.js +3 -0
  8. package/dist/core/runner/compaction-call-options.d.ts +1 -1
  9. package/dist/core/runner/content-ask-bindings.d.ts +27 -0
  10. package/dist/core/runner/content-ask-bindings.js +1 -0
  11. package/dist/core/runner/contracts.d.ts +118 -6
  12. package/dist/core/runner/denial-limit-arms.d.ts +23 -0
  13. package/dist/core/runner/denial-limit-arms.js +21 -0
  14. package/dist/core/runner/inherited-ask-grants.d.ts +46 -0
  15. package/dist/core/runner/inherited-ask-grants.js +29 -0
  16. package/dist/core/runner/park-commit.d.ts +108 -0
  17. package/dist/core/runner/park-commit.js +32 -0
  18. package/dist/core/runner/{prepare-permission-rules.d.ts → permission-rule-lanes.d.ts} +109 -3
  19. package/dist/core/runner/{prepare-permission-rules.js → permission-rule-lanes.js} +47 -1
  20. package/dist/core/runner/prepare-ask-lane.d.ts +110 -0
  21. package/dist/core/runner/prepare-ask-lane.js +133 -0
  22. package/dist/core/runner/prepare-boundary-parks.d.ts +105 -0
  23. package/dist/core/runner/prepare-boundary-parks.js +169 -0
  24. package/dist/core/runner/prepare-context-lane.d.ts +119 -0
  25. package/dist/core/runner/prepare-context-lane.js +230 -0
  26. package/dist/core/runner/prepare-gate-stations.d.ts +177 -0
  27. package/dist/core/runner/prepare-gate-stations.js +290 -0
  28. package/dist/core/runner/prepare-hands-readface.d.ts +4 -4
  29. package/dist/core/runner/prepare-inherited-gate.d.ts +4 -4
  30. package/dist/core/runner/prepare-memory-engine-session.d.ts +84 -0
  31. package/dist/core/runner/prepare-memory-engine-session.js +233 -0
  32. package/dist/core/runner/prepare-park-ask.d.ts +164 -0
  33. package/dist/core/runner/prepare-park-ask.js +377 -0
  34. package/dist/core/runner/prepare-policy-chain.d.ts +208 -0
  35. package/dist/core/runner/prepare-policy-chain.js +584 -0
  36. package/dist/core/runner/prepare-project-context.d.ts +1 -13
  37. package/dist/core/runner/prepare-project-context.js +1 -3
  38. package/dist/core/runner/prepare-prompt-assembly.d.ts +95 -0
  39. package/dist/core/runner/prepare-prompt-assembly.js +162 -0
  40. package/dist/core/runner/prepare-prompt-inputs.d.ts +1 -20
  41. package/dist/core/runner/prepare-protocol-tools.d.ts +3 -3
  42. package/dist/core/runner/prepare-protocol-tools.js +0 -3
  43. package/dist/core/runner/prepare-question-face.d.ts +3 -21
  44. package/dist/core/runner/prepare-question-face.js +2 -1
  45. package/dist/core/runner/prepare-safety-scan.d.ts +0 -5
  46. package/dist/core/runner/prepare-safety-scan.js +1 -2
  47. package/dist/core/runner/prepare-suspend-saga.d.ts +170 -0
  48. package/dist/core/runner/prepare-suspend-saga.js +308 -0
  49. package/dist/core/runner/prepare-task.d.ts +9 -136
  50. package/dist/core/runner/prepare-task.js +44 -2741
  51. package/dist/core/runner/prepare-turn-wiring.d.ts +154 -0
  52. package/dist/core/runner/prepare-turn-wiring.js +201 -0
  53. package/dist/core/runner/prepare-wiring-manifest.d.ts +11 -3
  54. package/dist/core/runner/prepare-wiring-manifest.js +9 -2
  55. package/dist/core/runner/prepare-workspace-restore.d.ts +2 -29
  56. package/dist/core/runner/prepare-workspace-restore.js +3 -16
  57. package/dist/core/runner/prompt-hash-salt.d.ts +1 -0
  58. package/dist/core/runner/prompt-hash-salt.js +2 -0
  59. package/dist/core/runner/remote-env-retry.d.ts +29 -0
  60. package/dist/core/runner/remote-env-retry.js +16 -0
  61. package/dist/core/runner/runtask.js +1 -1
  62. package/dist/core/session.d.ts +12 -0
  63. package/dist/core/session.js +3 -0
  64. package/package.json +1 -1
  65. /package/dist/core/runner/{prepare-announce-once.d.ts → announce-once-ledger.d.ts} +0 -0
  66. /package/dist/core/runner/{prepare-announce-once.js → announce-once-ledger.js} +0 -0
@@ -0,0 +1,169 @@
1
+ import { hasBackgroundShell, sweepBackgroundShells } from "../background-shell.js";
2
+ import { BINDING_CHECKPOINT_VERSION, debitLedger, F012_CHECKPOINT_VERSION, FACE_CHECKPOINT_VERSION, mintCheckpointId, mintCheckpointToken, ORG_ADMISSION_CHECKPOINT_VERSION, RESOURCE_CHECKPOINT_VERSION, TOKEN_CHECKPOINT_VERSION, } from "../checkpoint-store.js";
3
+ import { defaultTaskRegistry } from "../task-registry.js";
4
+ import { checkpointScopeOf } from "./checkpoint-scope.js";
5
+ import { DEFAULT_RESOURCE_TTL_MS, publishCommittedSuspend, sanitizedTtlMs, USAGE_WINDOW_REAP_MARGIN_MS } from "./park-commit.js";
6
+ import { remoteEnvFailureNote } from "./remote-env-retry.js";
7
+ export function prepareBoundaryParks(input) {
8
+ const { saga, spec, checkpointStore, abortController, harness, session, sessions, sessionId, deps, priorLedger, maxSlices, maxSuspends, resourceTotal, priorSuspendCount, suspendChainBase, humanReviewRef, liveSpendRef, now, faceCheckpointState, f012CheckpointState, orgAdmissionCheckpointState, suspendRef, reviewRef, remoteEnvFailures, resourceSuspendEligible, durableSuspendInfraReady, incompleteSuspendAdapter } = input;
9
+ if (saga === undefined)
10
+ return { suspendForResource: undefined, suspendForPlatformLimit: undefined, suspendForReview: undefined };
11
+ const { inFlightSpendMicroUsd, serializeCheckpointState, commitSuspendSaga, suspendLoopCapHit, suspendableEnv, parkOnlyRemoteEnv, parkOnlyHandle } = saga;
12
+ const mintResourceSuspend = async (reason, sliceSpend, hint) => {
13
+ const rs = spec.resourceSuspend;
14
+ if (!checkpointStore)
15
+ return false;
16
+ const scope = rs?.scope ?? checkpointScopeOf(spec);
17
+ if (abortController.signal.aborted)
18
+ return false;
19
+ if (suspendLoopCapHit(priorLedger?.sliceCount ?? 0, maxSlices, " (resource slice limit)"))
20
+ return false;
21
+ const leafId = await session.getLeafId();
22
+ if (!leafId)
23
+ return false;
24
+ if (abortController.signal.aborted)
25
+ return false;
26
+ let remoteHandle;
27
+ if (suspendableEnv !== undefined) {
28
+ if (hasBackgroundShell(suspendableEnv))
29
+ await sweepBackgroundShells(suspendableEnv, defaultTaskRegistry);
30
+ const snap = await suspendableEnv.suspendVM({ abortSignal: abortController.signal });
31
+ if (!snap.ok) {
32
+ remoteEnvFailures.push(remoteEnvFailureNote("suspendVM", snap.error, 1));
33
+ deps.onError?.(new Error(`resource suspendVM failed (${snap.error.code}): ${snap.error.message}`, { cause: snap.error }), { phase: "config", sessionId });
34
+ return false;
35
+ }
36
+ remoteHandle = { ...suspendableEnv.workspaceHandle(), snapshotId: snap.value };
37
+ }
38
+ else if (parkOnlyRemoteEnv !== undefined) {
39
+ if (hasBackgroundShell(parkOnlyRemoteEnv) && spec.retainBackgroundProcesses !== true)
40
+ await sweepBackgroundShells(parkOnlyRemoteEnv, defaultTaskRegistry);
41
+ remoteHandle = parkOnlyHandle(parkOnlyRemoteEnv);
42
+ }
43
+ const token = mintCheckpointToken();
44
+ const gate = { kind: "resource_limit", reason, ...(hint !== undefined ? { resumeAfterMs: hint.resumeAfterMs } : {}) };
45
+ const resourceLedgerOut = debitLedger(priorLedger, sliceSpend, resourceTotal);
46
+ const mintedAt = Date.now();
47
+ const cp = {
48
+ token,
49
+ checkpointId: mintCheckpointId(),
50
+ version: faceCheckpointState() ? FACE_CHECKPOINT_VERSION : f012CheckpointState() ? F012_CHECKPOINT_VERSION : orgAdmissionCheckpointState() ? ORG_ADMISSION_CHECKPOINT_VERSION : resourceLedgerOut.totalTokens !== undefined ? TOKEN_CHECKPOINT_VERSION : RESOURCE_CHECKPOINT_VERSION,
51
+ scope,
52
+ sessionId,
53
+ leafId,
54
+ gate,
55
+ pendingAction: { kind: "resource_limit", reason },
56
+ state: serializeCheckpointState(remoteHandle),
57
+ status: "pending",
58
+ createdAt: mintedAt,
59
+ deadline: mintedAt + Math.max(sanitizedTtlMs(rs?.ttlMs) ?? DEFAULT_RESOURCE_TTL_MS, hint !== undefined ? hint.resumeAfterMs + USAGE_WINDOW_REAP_MARGIN_MS : 0),
60
+ suspendCount: priorSuspendCount,
61
+ humanReview: humanReviewRef.count > 0
62
+ ? { count: humanReviewRef.count, totalWaitMs: humanReviewRef.totalWaitMs, gates: [...humanReviewRef.gates] }
63
+ : undefined,
64
+ resourceLedger: resourceLedgerOut,
65
+ ...(spec.durableApproval !== undefined ? { durableApproval: { ...spec.durableApproval } } : {}),
66
+ ...(spec.principal ? { principal: spec.principal } : {}),
67
+ };
68
+ if ((await commitSuspendSaga(token, cp, suspendableEnv, remoteHandle)).tag !== "committed")
69
+ return false;
70
+ publishCommittedSuspend({ suspendRef, reviewRef }, token, gate, scope, remoteHandle, cp.checkpointId, cp.pendingAction);
71
+ try {
72
+ await sessions.pin?.(sessionId);
73
+ }
74
+ catch (pinErr) {
75
+ deps.onError?.(pinErr, { phase: "config", sessionId });
76
+ }
77
+ harness.requestStopAfterTurn();
78
+ return true;
79
+ };
80
+ const suspendForResource = resourceSuspendEligible ? mintResourceSuspend : undefined;
81
+ const suspendForPlatformLimit = durableSuspendInfraReady ? mintResourceSuspend : undefined;
82
+ const suspendForReview = checkpointStore
83
+ ? async (reason) => {
84
+ if (!checkpointStore)
85
+ return false;
86
+ if (abortController.signal.aborted)
87
+ return false;
88
+ if (incompleteSuspendAdapter !== undefined) {
89
+ try {
90
+ deps.onError?.(new Error(`plan_review park skipped: the per-task execution env declares capabilities.suspendable but its adapter does not implement ${incompleteSuspendAdapter.join(" or ")}, so its workspace could not be restored on resume (the plan-review request was dropped; the run continues without pausing)`), { phase: "config", sessionId });
91
+ }
92
+ catch {
93
+ }
94
+ return false;
95
+ }
96
+ if (suspendLoopCapHit(suspendChainBase(), maxSuspends, " for a plan_review (likely a resume/restart loop)."))
97
+ return false;
98
+ const leafId = await session.getLeafId();
99
+ if (!leafId) {
100
+ try {
101
+ deps.onError?.(new Error("plan_review park skipped: no committed session leaf (the plan-review request was dropped; the run continues without pausing)"), { phase: "config", sessionId });
102
+ }
103
+ catch {
104
+ }
105
+ return false;
106
+ }
107
+ if (abortController.signal.aborted)
108
+ return false;
109
+ let remoteHandle;
110
+ if (suspendableEnv !== undefined) {
111
+ if (hasBackgroundShell(suspendableEnv))
112
+ await sweepBackgroundShells(suspendableEnv, defaultTaskRegistry);
113
+ const snap = await suspendableEnv.suspendVM({ abortSignal: abortController.signal });
114
+ if (!snap.ok) {
115
+ remoteEnvFailures.push(remoteEnvFailureNote("suspendVM", snap.error, 1));
116
+ deps.onError?.(new Error(`plan_review suspendVM failed (${snap.error.code}): ${snap.error.message}`, { cause: snap.error }), { phase: "config", sessionId });
117
+ return false;
118
+ }
119
+ remoteHandle = { ...suspendableEnv.workspaceHandle(), snapshotId: snap.value };
120
+ }
121
+ else if (parkOnlyRemoteEnv !== undefined) {
122
+ if (hasBackgroundShell(parkOnlyRemoteEnv) && spec.retainBackgroundProcesses !== true)
123
+ await sweepBackgroundShells(parkOnlyRemoteEnv, defaultTaskRegistry);
124
+ remoteHandle = parkOnlyHandle(parkOnlyRemoteEnv);
125
+ }
126
+ const token = mintCheckpointToken();
127
+ const scope = checkpointScopeOf(spec);
128
+ const gate = { kind: "plan_review", reason: reason ?? `plan review requested` };
129
+ const reviewLedger = debitLedger(priorLedger, liveSpendRef.get?.() ?? { costMicroUsd: 0, tokens: 0, turns: 0, walltimeMs: 0 }, resourceTotal, { countSlice: false });
130
+ const mintedAt = Date.now();
131
+ const cp = {
132
+ token,
133
+ checkpointId: mintCheckpointId(),
134
+ version: faceCheckpointState() ? FACE_CHECKPOINT_VERSION : f012CheckpointState() ? F012_CHECKPOINT_VERSION : orgAdmissionCheckpointState() ? ORG_ADMISSION_CHECKPOINT_VERSION : reviewLedger.totalTokens !== undefined ? TOKEN_CHECKPOINT_VERSION : BINDING_CHECKPOINT_VERSION,
135
+ scope,
136
+ sessionId,
137
+ leafId,
138
+ gate,
139
+ pendingAction: { kind: "plan_review" },
140
+ state: serializeCheckpointState(remoteHandle, inFlightSpendMicroUsd()),
141
+ status: "pending",
142
+ createdAt: mintedAt,
143
+ suspendedAt: now(),
144
+ deadline: mintedAt + (sanitizedTtlMs(spec.durableApproval?.ttlMs) ?? DEFAULT_RESOURCE_TTL_MS),
145
+ suspendCount: suspendChainBase() + 1,
146
+ humanReview: humanReviewRef.count > 0
147
+ ? { count: humanReviewRef.count, totalWaitMs: humanReviewRef.totalWaitMs, gates: [...humanReviewRef.gates] }
148
+ : undefined,
149
+ resourceLedger: reviewLedger,
150
+ sourceTaskId: sessionId,
151
+ ...(spec.principal ? { principal: spec.principal } : {}),
152
+ ...(spec.durableApproval !== undefined ? { durableApproval: { ...spec.durableApproval } } : {}),
153
+ };
154
+ if ((await commitSuspendSaga(token, cp, suspendableEnv, remoteHandle)).tag !== "committed")
155
+ return false;
156
+ publishCommittedSuspend({ suspendRef, reviewRef }, token, gate, scope, remoteHandle, cp.checkpointId, cp.pendingAction);
157
+ try {
158
+ await sessions.pin?.(sessionId);
159
+ }
160
+ catch (pinErr) {
161
+ deps.onError?.(pinErr, { phase: "config", sessionId });
162
+ }
163
+ abortController.abort();
164
+ void harness.abort();
165
+ return true;
166
+ }
167
+ : undefined;
168
+ return { suspendForResource, suspendForPlatformLimit, suspendForReview };
169
+ }
@@ -0,0 +1,119 @@
1
+ /**
2
+ * design/390 §1.2 M21 — prepareTask's CONTEXT-LANE phase, verbatim from the driver: the edit/guard budgets, the
3
+ * microCompact machine state with the trigger-window clamp's one loud outlet, the model's chars-per-token coordinate,
4
+ * and the `context` handler registered on the harness — heal → ledger replay → aggregate tool-result and media caps →
5
+ * frontier clear → the guard chain (arms A/B/C) → the git-frame guard → the orphan belt → the lossy-projection flag →
6
+ * the projection seat. The four module-level helpers only this lane called moved with it (the machine-state builder,
7
+ * the frontier-clear recorder, the observation-buffer flush, the guard chain with its two types). The interface is the
8
+ * dependency list the segment had implicitly (design/238 R-1).
9
+ *
10
+ * SYNCHRONOUS FUNCTION, SYNCHRONOUS CALL: the stretch has no await of its own (the handler it registers is async and
11
+ * runs per provider request), so the phase adds no yield between the gate stations before it and the turn wiring after
12
+ * it (design/390 §1.5). Read-stability of the host handles: {@link RunInternals} (`@contract prepare.deps-read-stable`).
13
+ *
14
+ * REGISTRATION SIDE EFFECT, OWNED: this phase registers the ONE `context` handler on the harness; the harness owns the
15
+ * listener for the run's life (it is never unregistered — the harness is per prepare). No other station registers on
16
+ * that hook.
17
+ */
18
+ import { type AgentHarness, type AgentMessage } from "../../internal/harness.js";
19
+ import type { Model } from "../../internal/llm.js";
20
+ import { type ContextEditMachine, type OccurrenceIndex } from "../context-edit.js";
21
+ import { type ToolResultStore } from "../tool-result-store.js";
22
+ import { type RunnerDeps } from "../types.js";
23
+ import type { Prepared, PreparedMicroCompact } from "./contracts.js";
24
+ import { type GitStatusLaneRef } from "./git-status-frame.js";
25
+ export interface PrepareContextLaneInput {
26
+ /** borrowed-readonly — the task model: the budgets, the trigger-window geometry and `charsPerToken` are read off it. */
27
+ model: Model;
28
+ /** borrowed-readonly — the deployment seats, whole: `onNotice` (the clamp outlet, the offload-loss announcement), `tracer`
29
+ * (the cap / clear / trim frames), `mediaByteCapBytes` and `onMediaStripped` (the media cap), `onError` (the belt line, the
30
+ * git-frame degrade) — the guard chain and the machine-state builder read through the whole record. */
31
+ deps: RunnerDeps;
32
+ /** borrowed-readonly — the acquired session id (the offload persist key; every disclosure's attribution). */
33
+ sessionId: string;
34
+ /** borrowed-readonly — the run's invocation id (the clamp notice's per-run claim). */
35
+ runId: string;
36
+ /** borrowed-readonly — the per-task identity (the trace frames' taskId). */
37
+ hostTaskId: string;
38
+ /** borrowed-readonly — the run's offload store, or undefined (the caps' and the machine's persist seat). */
39
+ offloadStore: ToolResultStore | undefined;
40
+ /** borrowed-readonly — the doors' RESOLVED microCompact knob (immutable values, never the deps bag). */
41
+ microCompactKnob: {
42
+ machine: "off" | ContextEditMachine;
43
+ clearOnRejection: boolean;
44
+ };
45
+ /** borrowed-mutable — the built harness. Writer here: `on("context", …)` — the ONE registration of the request-build
46
+ * handler (the harness owns the listener; nothing unregisters it). */
47
+ harness: AgentHarness;
48
+ /** borrowed-mutable — the turn's halt fact (`Prepared.batchHaltRef`). Writer here: the handler CLEARS it at every new
49
+ * provider request (a new request means the halted boundary was legitimately passed). The judgment site sets it. */
50
+ batchHaltRef: Prepared["batchHaltRef"];
51
+ /** borrowed-mutable — the git-status lane ref. Writers here: the git-frame guard's degrade latch (`overBudgetShrunk`) inside
52
+ * the handler; the prompt-inputs phase seeds it. */
53
+ gitStatusRef: GitStatusLaneRef;
54
+ /** borrowed-mutable — the per-request lossy-projection flag. Writer here: the handler re-derives it at every request
55
+ * build; the provider-seam recording shim reads it immediately after. */
56
+ requestLossyRef: {
57
+ current: boolean;
58
+ };
59
+ /** borrowed-mutable — the trim-pressure flag (`Prepared.trimPressureRef`). Writer here: the guard chain's trim arm sets
60
+ * `droppedMessages`; the run loop's boundary reads and clears it. */
61
+ trimPressureRef: {
62
+ droppedMessages: boolean;
63
+ };
64
+ }
65
+ export interface PrepareContextLaneResult {
66
+ /** owned — the per-run microCompact machine state (`Prepared.microCompact`): machine, MC-R flag, ledger, the projection
67
+ * and in-turn-compaction seats the handler and the runtask arms fill. */
68
+ microCompact: PreparedMicroCompact;
69
+ /** owned — the model's structural coefficient (`charsPerToken ?? DEFAULT`), the ONE coordinate the request-layer
70
+ * defenses here and the prompt-overhead term downstream share with the compaction trigger estimate. */
71
+ charsPerToken: number;
72
+ }
73
+ /** The M21 phase body — prepareTask's context-lane stretch, verbatim (see the module header). */
74
+ export declare function prepareContextLane(input: PrepareContextLaneInput): PrepareContextLaneResult;
75
+ /** See {@link runGuardChain}. */
76
+ export interface GuardChainArgs {
77
+ /** The post-frontier view the chain starts from. */
78
+ edited: AgentMessage[];
79
+ /** The pre-cap counterpart view (arm A's clearSource — RB-212 durable composition seat). */
80
+ replayed: AgentMessage[];
81
+ index: OccurrenceIndex;
82
+ microCompact: PreparedMicroCompact;
83
+ guardAt: number;
84
+ charsPerToken: number;
85
+ offloadStore: import("../tool-result-store.js").ToolResultStore | undefined;
86
+ sessionId: string;
87
+ taskId: string;
88
+ deps: RunnerDeps;
89
+ trimPressureRef: {
90
+ droppedMessages: boolean;
91
+ };
92
+ recheck: boolean | undefined;
93
+ signal: AbortSignal | undefined;
94
+ /** The r7 per-request observation buffer — the chain DROPS it on its early returns (a discarded
95
+ * provisional projection must not narrate itself). */
96
+ pendingProjectionObservations: Array<() => void>;
97
+ }
98
+ export type GuardChainOutcome = {
99
+ earlyReturn: {
100
+ messages: AgentMessage[];
101
+ adoptSessionRebuild?: boolean;
102
+ };
103
+ } | {
104
+ working: AgentMessage[];
105
+ trimmed: AgentMessage[];
106
+ trimDroppedMessages: boolean;
107
+ };
108
+ /**
109
+ * design/374 slice 3 — the GUARD CHAIN over one request build (D-7 case-ii transfer table),
110
+ * module-level per the design/238 body-span bank. design/123 D3: the chain reads the anchored
111
+ * coordinate re-estimated on the EDITED array (pre-anchor clears don't lower it — deliberately;
112
+ * trimToBudget doc). The opt-out machine = the pre-374 backstop order byte-identical
113
+ * (trim as the ordinary second line); otherwise arm A (blocking machine run, only when the
114
+ * frontier pass is off) → arm B (in-turn forced compaction behind the adopt seam) → arm C (trim,
115
+ * the disaster-only last resort and this chain's sole `context.trim` emitter). Returns either the
116
+ * post-chain view for the pipeline tail, or an EARLY hook result the context handler must return
117
+ * verbatim (arm-B adoption / the r8 dead-turn stand-down).
118
+ */
119
+ export declare function runGuardChain(args: GuardChainArgs): Promise<GuardChainOutcome>;
@@ -0,0 +1,230 @@
1
+ import { DEFAULT_CHARS_PER_TOKEN, estimateContextTokens } from "../../internal/harness.js";
2
+ import { clearStaleToolResults, createClearedProjectionLedger, dropEmptyFailureAssistants, editBudget, replayClearedProjection, resolveTriggerWindow } from "../context-edit.js";
3
+ import { dropOrphanToolResults, guardBudget, insertTrimNotice, trimToBudget } from "../context-guard.js";
4
+ import { AGGREGATE_MEDIA_BUDGET_BYTES, capAggregateMediaBytes } from "../media-byte-cap.js";
5
+ import { capAggregateToolResults } from "../tool-result-budget.js";
6
+ import { createOffloadPersist } from "../tool-result-store.js";
7
+ import { emitTrace } from "../trace.js";
8
+ import { deliverEngineNotice } from "../types.js";
9
+ import { applyGitFrameGuard } from "./git-status-frame.js";
10
+ export function prepareContextLane(input) {
11
+ const { model, deps, sessionId, runId, hostTaskId, offloadStore, microCompactKnob, harness, batchHaltRef, gitStatusRef, requestLossyRef, trimPressureRef } = input;
12
+ const editAt = editBudget(model);
13
+ const guardAt = guardBudget(model);
14
+ const microCompact = buildMicroCompactState(deps, model, sessionId, runId, offloadStore, microCompactKnob);
15
+ const charsPerToken = model.charsPerToken ?? DEFAULT_CHARS_PER_TOKEN;
16
+ harness.on("context", async ({ messages, recheck, signal }) => {
17
+ batchHaltRef.current = undefined;
18
+ const pendingProjectionObservations = [];
19
+ try {
20
+ const healed = dropEmptyFailureAssistants(messages);
21
+ const replay = replayClearedProjection(healed, microCompact.ledger);
22
+ const replayed = replay.messages;
23
+ const capped = await capAggregateToolResults(replayed, {
24
+ store: offloadStore,
25
+ sessionId,
26
+ onCapped: (info) => pendingProjectionObservations.push(() => emitTrace(deps.tracer, () => ({
27
+ kind: "tool_result.capped",
28
+ version: 1,
29
+ taskId: hostTaskId,
30
+ ...(info.tool !== undefined ? { tool: info.tool } : {}),
31
+ sizeChars: info.sizeChars,
32
+ storeFallback: info.storeFallback,
33
+ ts: Date.now(),
34
+ }))),
35
+ });
36
+ const mediaCapped = capAggregateMediaBytes(capped, {
37
+ limitBytes: deps.mediaByteCapBytes ?? AGGREGATE_MEDIA_BUDGET_BYTES,
38
+ onStripped: (info) => {
39
+ const onMediaStripped = deps.onMediaStripped;
40
+ if (onMediaStripped !== undefined)
41
+ pendingProjectionObservations.push(() => onMediaStripped(info));
42
+ },
43
+ });
44
+ let ccPass;
45
+ const edited = microCompact.machine === "off" ? mediaCapped : clearStaleToolResults(mediaCapped, {
46
+ budgetTokens: editAt,
47
+ machine: microCompact.machine,
48
+ ...(microCompact.machine === "cc" || microCompact.clearOnRejection ? { recognizeCcMarkers: true } : {}),
49
+ ...(microCompact.machine === "cc"
50
+ ? {
51
+ clearSource: replayed,
52
+ onCleared: (pass) => { ccPass = pass; },
53
+ }
54
+ : {}),
55
+ anchoredTotalTokens: estimateContextTokens(mediaCapped, charsPerToken).tokens,
56
+ charsPerToken,
57
+ ...(offloadStore
58
+ ? {
59
+ offload: {
60
+ persist: createOffloadPersist(offloadStore, sessionId, deps.onNotice),
61
+ },
62
+ }
63
+ : {}),
64
+ });
65
+ if (ccPass !== undefined) {
66
+ recordFrontierClears({ pass: ccPass, index: replay.index, edited, ledger: microCompact.ledger, tracer: deps.tracer, taskId: hostTaskId, trigger: "frontier" });
67
+ }
68
+ const chain = await runGuardChain({
69
+ edited, replayed, index: replay.index, microCompact, guardAt, charsPerToken, offloadStore,
70
+ sessionId, taskId: hostTaskId, deps, trimPressureRef, recheck, signal,
71
+ pendingProjectionObservations,
72
+ });
73
+ if ("earlyReturn" in chain) {
74
+ return chain.earlyReturn;
75
+ }
76
+ const { working, trimDroppedMessages } = chain;
77
+ let trimmed = chain.trimmed;
78
+ trimmed = applyGitFrameGuard({
79
+ before: working,
80
+ trimmed,
81
+ budgetTokens: guardAt,
82
+ ref: gitStatusRef,
83
+ charsPerToken,
84
+ onDegrade: (message) => {
85
+ try {
86
+ deps.onError?.(new Error(message), { phase: "degraded", sessionId, classification: "env-git-snapshot" });
87
+ }
88
+ catch {
89
+ }
90
+ },
91
+ });
92
+ const swept = dropOrphanToolResults(trimmed);
93
+ if (swept.dropped.length > 0) {
94
+ try {
95
+ deps.onError?.(new Error(`context belt: dropped ${swept.dropped.length} orphan toolResult(s) whose tool-call assistant was not in the request view — ` +
96
+ swept.dropped
97
+ .map((d) => `toolCallId=${d.toolCallId} at index ${d.index} (prev=${d.prevRole}, next=${d.nextRole})`)
98
+ .join("; ")), { phase: "hook", sessionId });
99
+ }
100
+ catch {
101
+ }
102
+ }
103
+ requestLossyRef.current =
104
+ replayed !== healed ||
105
+ capped !== replayed ||
106
+ mediaCapped !== capped ||
107
+ edited !== mediaCapped ||
108
+ working !== edited ||
109
+ trimDroppedMessages ||
110
+ swept.dropped.length > 0 ||
111
+ gitStatusRef.overBudgetShrunk === true;
112
+ microCompact.projectionRef.current = { messages: swept.messages, keyOf: replay.index.keyOf };
113
+ return { messages: swept.messages };
114
+ }
115
+ finally {
116
+ flushProjectionObservations(pendingProjectionObservations);
117
+ }
118
+ });
119
+ return { microCompact, charsPerToken };
120
+ }
121
+ function buildMicroCompactState(deps, model, sessionId, runId, offloadStore, knob) {
122
+ const triggerWindow = resolveTriggerWindow(model);
123
+ if (triggerWindow.clamped) {
124
+ deliverEngineNotice(deps.onNotice, {
125
+ code: "config.autocompact_window_clamped",
126
+ message: `model "${model.id}" declares autoCompactTokens=${triggerWindow.declaredAutoCompactTokens} ABOVE its physical window ` +
127
+ `${triggerWindow.physicalWindow}; the autocompact window only ever LOWERS the trigger-side geometry, so the value was ` +
128
+ `clamped to the physical window — fix the model config (declare a value at or below the physical window, or omit it)`,
129
+ detail: {
130
+ modelId: model.id,
131
+ declaredAutoCompactTokens: triggerWindow.declaredAutoCompactTokens,
132
+ physicalWindow: triggerWindow.physicalWindow,
133
+ sessionId,
134
+ runId,
135
+ },
136
+ });
137
+ }
138
+ return {
139
+ machine: knob.machine,
140
+ clearOnRejection: knob.clearOnRejection,
141
+ ledger: createClearedProjectionLedger(),
142
+ projectionRef: {},
143
+ inTurnCompactionRef: {},
144
+ ...(offloadStore ? { offloadPersist: createOffloadPersist(offloadStore, sessionId, deps.onNotice) } : {}),
145
+ };
146
+ }
147
+ function recordFrontierClears(args) {
148
+ for (const { index } of args.pass.clears) {
149
+ const at = args.index.keyAt(index);
150
+ const clearedMsg = args.edited[index];
151
+ const markerText = clearedMsg.content?.[0]?.text;
152
+ if (at !== undefined && typeof markerText === "string") {
153
+ args.ledger.entries.set(at.key, { marker: markerText, groupCount: at.groupCount, fp: at.fp });
154
+ }
155
+ }
156
+ emitTrace(args.tracer, () => ({
157
+ kind: "context.mc_clear",
158
+ version: 1,
159
+ taskId: args.taskId,
160
+ clearedCount: args.pass.clears.length,
161
+ tokensSavedEstimate: args.pass.tokensSavedEstimate,
162
+ trigger: args.trigger,
163
+ ts: Date.now(),
164
+ }));
165
+ }
166
+ function flushProjectionObservations(pending) {
167
+ for (const fire of pending.splice(0)) {
168
+ try {
169
+ fire();
170
+ }
171
+ catch {
172
+ }
173
+ }
174
+ }
175
+ export async function runGuardChain(args) {
176
+ const { edited, microCompact, guardAt, charsPerToken, deps, taskId } = args;
177
+ const applyGuardTrim = (view, anchoredTotal) => {
178
+ let trimmed = trimToBudget(view, guardAt, anchoredTotal, charsPerToken);
179
+ const dropped = trimmed.length < view.length;
180
+ if (dropped) {
181
+ args.trimPressureRef.droppedMessages = true;
182
+ const droppedCount = view.length - trimmed.length;
183
+ trimmed = insertTrimNotice(trimmed);
184
+ emitTrace(deps.tracer, () => ({
185
+ kind: "context.trim",
186
+ version: 1,
187
+ taskId,
188
+ dropped: droppedCount,
189
+ ts: Date.now(),
190
+ }));
191
+ }
192
+ return { trimmed, dropped };
193
+ };
194
+ let working = edited;
195
+ let anchoredWorking = estimateContextTokens(working, charsPerToken).tokens;
196
+ if (microCompact.machine === "legacy") {
197
+ const t = applyGuardTrim(working, anchoredWorking);
198
+ return { working, trimmed: t.trimmed, trimDroppedMessages: t.dropped };
199
+ }
200
+ if (microCompact.machine === "off" && anchoredWorking > guardAt) {
201
+ let blockingPass;
202
+ const blockingEdited = clearStaleToolResults(working, {
203
+ budgetTokens: guardAt,
204
+ machine: "cc",
205
+ clearSource: args.replayed,
206
+ onCleared: (pass) => { blockingPass = pass; },
207
+ anchoredTotalTokens: anchoredWorking,
208
+ charsPerToken,
209
+ ...(args.offloadStore ? { offload: { persist: createOffloadPersist(args.offloadStore, args.sessionId, deps.onNotice) } } : {}),
210
+ });
211
+ if (blockingPass !== undefined) {
212
+ recordFrontierClears({ pass: blockingPass, index: args.index, edited: blockingEdited, ledger: microCompact.ledger, tracer: deps.tracer, taskId, trigger: "blocking" });
213
+ working = blockingEdited;
214
+ anchoredWorking = estimateContextTokens(working, charsPerToken).tokens;
215
+ }
216
+ }
217
+ if (anchoredWorking > guardAt && args.recheck !== true) {
218
+ const compacted = (await microCompact.inTurnCompactionRef.current?.(args.signal, anchoredWorking)) === true;
219
+ if (compacted) {
220
+ args.pendingProjectionObservations.length = 0;
221
+ return { earlyReturn: { messages: working, adoptSessionRebuild: true } };
222
+ }
223
+ if (args.signal?.aborted === true) {
224
+ args.pendingProjectionObservations.length = 0;
225
+ return { working, trimmed: working, trimDroppedMessages: false };
226
+ }
227
+ }
228
+ const t = applyGuardTrim(working, anchoredWorking);
229
+ return { working, trimmed: t.trimmed, trimDroppedMessages: t.dropped };
230
+ }