@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,154 @@
1
+ import type { AgentHarness, AgentTool, ExecutionEnv, ThinkingLevel } from "../../internal/harness.js";
2
+ import type { Model } from "../../internal/llm.js";
3
+ import { type AssembledPrompt, type AssembleInputs } from "../../prompt-assembly/assemble.js";
4
+ import { type PromptEpochArtifact } from "../../prompt-assembly/artifact.js";
5
+ import type { EpochDeclaredSections } from "../../prompt-assembly/epoch.js";
6
+ import { type TurnPromptSnapshot } from "../../prompt-assembly/turn-snapshot.js";
7
+ import type { PromptTextDeclaration } from "../../prompts/default.js";
8
+ import { CacheBreakDetector, type ToolFingerprintInput } from "../cache-break-detector.js";
9
+ import { type ToolPolicy } from "../tool-policy.js";
10
+ import type { RunnerDeps, TaskSpec } from "../types.js";
11
+ import type { CwdRef } from "../../tools/fs/fs-shared.js";
12
+ import { type ReadFace } from "../../tools/fs/index.js";
13
+ import type { ReadDenyMatcher } from "../../tools/fs/read-deny.js";
14
+ import { type ReadFileState } from "../../tools/fs/safety.js";
15
+ import type { Prepared } from "./contracts.js";
16
+ export interface PrepareTurnWiringInput {
17
+ /** borrowed-readonly — the deployment seats: `cacheBreakDetection` (the detector knob), `onError` (the attachment-skip
18
+ * degraded line), `promptSource` (the center candidate's state and current artifact). Receiver preserved. */
19
+ deps: Pick<RunnerDeps, "cacheBreakDetection" | "onError" | "promptSource">;
20
+ /** borrowed-mutable — the LIVE system prompt (the prompt-assembly phase's accessor cell). Read: the fingerprint, the
21
+ * snapshot's stable text, the overhead term. Writer here: the center candidate's `apply` (the boundary swap). */
22
+ systemPromptSeat: {
23
+ current: string;
24
+ };
25
+ /** borrowed-readonly — the FINAL wire tool list (the fingerprint's and the snapshot's tool wire, the overhead sum). */
26
+ harnessTools: ReadonlyArray<AgentTool>;
27
+ /** borrowed-mutable — the fingerprint ref the tool-disclosure phase minted. Writers: THIS phase seeds `current`; the
28
+ * candidate's `apply` rewrites `current.systemPrompt`; the disclosure phase's rebuild seam refreshes `current.tools`. */
29
+ fpRef: {
30
+ current?: {
31
+ systemPrompt: string;
32
+ tools: ToolFingerprintInput[];
33
+ };
34
+ };
35
+ /** borrowed-readonly — the task model: `provider` / `id` / `maxTokens` / `api` (the snapshot's identity elements). */
36
+ model: Pick<Model, "provider" | "id" | "maxTokens" | "api">;
37
+ /** borrowed-readonly — the resolved epoch's artifact digest (the snapshot's identity element). */
38
+ epochArtifactDigestForSnapshot: string;
39
+ /** borrowed-readonly — the resolved prompt profile (a snapshot element). */
40
+ promptProfile: "simple" | "classic";
41
+ /** borrowed-readonly — the fable mitigations switch (a snapshot element). */
42
+ fableMitigations: boolean;
43
+ /** borrowed-readonly — the assembled prompt's physical block face, or undefined (the snapshot's layout groups). */
44
+ systemBlocks: AssembledPrompt["systemBlocks"];
45
+ /** borrowed-readonly — the resolved thinking level, or undefined (a snapshot element). */
46
+ thinking: ThinkingLevel | undefined;
47
+ /** borrowed-readonly — the REBOUND spec; only `outputSchema` (presence) and `taskId` (the snapshot's identity). */
48
+ spec: Pick<TaskSpec, "outputSchema" | "taskId">;
49
+ /** borrowed-readonly — the acquired session id (the snapshot; the attachment-skip attribution). */
50
+ sessionId: string;
51
+ /** borrowed-mutable — the snapshot ref the tool-disclosure phase minted. Writers: THIS phase seeds `current`; the candidate's
52
+ * `apply` calls `refreshStableSystem`; the disclosure phase's rebuild seam calls `refreshTools`; the run loop assigns
53
+ * `current.onChange` (the snapshot-changed trace emitter) once Prepared is built. */
54
+ turnSnapshotRef: {
55
+ current?: TurnPromptSnapshot;
56
+ };
57
+ /** borrowed-mutable — the composition manifest (`Prepared.promptManifest`). Writer here: the identity/lowering BACKFILL
58
+ * (the snapshot needs the final wire list, which exists only now). */
59
+ promptManifest: Prepared["promptManifest"];
60
+ /** borrowed-readonly — the model's structural coefficient (the context lane's), the overhead term's coordinate. */
61
+ charsPerToken: number;
62
+ /** borrowed-readonly — whether an execution env is wired (every reader exists only then). */
63
+ handsEnabled: boolean;
64
+ /** borrowed-readonly — the run's resolved env (the readers read and stat through it). */
65
+ executionEnv: ExecutionEnv;
66
+ /** borrowed-readonly — the canonical attachment root, or undefined (the readers' containment fence). */
67
+ attachmentRootCanonical: string | undefined;
68
+ /** borrowed-readonly — the canonical extra write roots (the fence's root set). */
69
+ additionalRootsCanonical: ReadonlyArray<string>;
70
+ /** borrowed-readonly — the canonical extra read roots (the fence's root set). */
71
+ additionalReadRootsCanonical: ReadonlyArray<string>;
72
+ /** borrowed-readonly — the compiled read-deny judge, or undefined (the reader's withheld arm). */
73
+ readDenyMatcher: ReadDenyMatcher | undefined;
74
+ /** borrowed-readonly — the resolved read face, or undefined (the fence's ninth argument). */
75
+ resolvedReadFace: ReadFace | undefined;
76
+ /** borrowed-readonly — the tracked-cwd seat, read per call by the kept-tail normalizer (never written here). */
77
+ handsCwdRef: CwdRef | undefined;
78
+ /** borrowed-mutable — the live read-file state, or undefined. Writers here, at RUN time: `detectExternalChanges` evicts a
79
+ * not-found entry; `onCompactionApplied` applies a landed compaction to it. The hands phase mints it. */
80
+ readFileStateForCheckpoint: ReadFileState | undefined;
81
+ /** borrowed-readonly — the deny-narrowing fold, or undefined (the changed-files deny door's authority). */
82
+ denyNarrowingPolicy: ToolPolicy | undefined;
83
+ /** borrowed-readonly — the run's abort controller; only `signal` (the deny door's and the stat's signal). */
84
+ abortController: Pick<AbortController, "signal">;
85
+ /** borrowed-readonly — the per-task identity (the background listing's owner). */
86
+ hostTaskId: string;
87
+ /** borrowed-readonly — the registry scope the background listing reads under. */
88
+ taskScope: string;
89
+ /** borrowed-readonly — the prompt-assembly phase's re-invokable assemble-inputs builder (the candidate re-assembles). */
90
+ buildAssembleInputs: (centerDecls?: PromptTextDeclaration[]) => AssembleInputs;
91
+ /** borrowed-mutable — the LIVE center adoption (the prompt-assembly phase's accessor cell). Read: the candidate's arming and
92
+ * same-artifact test. Writer here: the candidate's `apply` (the boundary swap / the explicit-disable rollback). */
93
+ centerAdoptionRef: {
94
+ current: {
95
+ artifact: PromptEpochArtifact;
96
+ sourceRevision?: string;
97
+ } | undefined;
98
+ };
99
+ /** borrowed-readonly — the harness ref (the candidate's `apply` calls `setSystemPrompt` on the current harness). */
100
+ harnessRef: {
101
+ current?: AgentHarness;
102
+ };
103
+ /** borrowed-readonly — the provider-declared sections in the epoch shape (the candidate's declared-sections TYPE). */
104
+ epochDeclaredSections: EpochDeclaredSections;
105
+ /** borrowed-readonly — the ONE carrier-face expression both delegation mounts consume (the observation seat). */
106
+ carrierReadFace: () => ReadFace | undefined;
107
+ /** borrowed-readonly — the normalized deny additions (the observation seat copies them). */
108
+ readDenyAdditionsNormalized: ReadonlyArray<{
109
+ pattern: string;
110
+ caseSensitive: boolean;
111
+ }>;
112
+ /** borrowed-mutable — the holder of the built Prepared. Writer: the DRIVER fills `current` right after `buildPrepared()`;
113
+ * the candidate's `apply` then writes `current.epochDeclaredSections` and `current.promptOverheadTokens` at a boundary. */
114
+ preparedHolder: {
115
+ current?: Prepared;
116
+ };
117
+ }
118
+ export interface PrepareTurnWiringResult {
119
+ /** owned — the two-phase cache-break detector (`Prepared.cacheBreakDetector`), or undefined when the knob is off. */
120
+ cacheBreakDetector: CacheBreakDetector | undefined;
121
+ /** borrowed-mutable — the prefix fingerprint (`Prepared.cacheFingerprint`; the same object `fpRef.current` holds), or
122
+ * undefined. Writers after this phase: the candidate's `apply` (systemPrompt), the disclosure rebuild seam (tools). */
123
+ cacheFingerprint: {
124
+ systemPrompt: string;
125
+ tools: ToolFingerprintInput[];
126
+ } | undefined;
127
+ /** owned — the fixed prompt-overhead term in the compaction trigger's coordinate (`Prepared.promptOverheadTokens`). */
128
+ promptOverheadTokens: number;
129
+ /** owned — the fenced working-file reader, or undefined without hands (`Prepared.readTaskFile`). */
130
+ readTaskFile: Prepared["readTaskFile"];
131
+ /** owned — the kept-tail path normalizer, or undefined without hands. */
132
+ normalizeAttachmentPath: Prepared["normalizeAttachmentPath"];
133
+ /** owned — the Read tool's dedup-stub predicate, or undefined without hands. */
134
+ isDedupStubResult: Prepared["isDedupStubResult"];
135
+ /** owned — the most-recently-read files projection, or undefined without read state. */
136
+ recentlyReadFiles: Prepared["recentlyReadFiles"];
137
+ /** owned — the landed-compaction read-state hook, or undefined without read state. */
138
+ onCompactionApplied: Prepared["onCompactionApplied"];
139
+ /** owned — the boundary-time changed-files scan, or undefined without read state. */
140
+ detectExternalChanges: Prepared["detectExternalChanges"];
141
+ /** owned — the post-compact background-task listing (`Prepared.listBackgroundTasks`). */
142
+ listBackgroundTasks: Prepared["listBackgroundTasks"];
143
+ /** owned — the compaction-boundary center adoption candidate, or undefined (no source, or no adoption). */
144
+ centerCompactionCandidate: Prepared["centerCompactionCandidate"];
145
+ /** owned — the effective read face observed for the result seats (`Prepared.effectiveReadFace`), or undefined. */
146
+ effectiveReadFaceObserved: ReadFace | undefined;
147
+ /** owned — the effective read-deny patterns observed (fresh copies), or undefined when none. */
148
+ effectiveReadDenyObserved: Array<{
149
+ pattern: string;
150
+ caseSensitive: boolean;
151
+ }> | undefined;
152
+ }
153
+ /** The M22 phase body — prepareTask's turn-wiring stretch, verbatim (see the module header). */
154
+ export declare function prepareTurnWiring(input: PrepareTurnWiringInput): PrepareTurnWiringResult;
@@ -0,0 +1,201 @@
1
+ import { createHash } from "node:crypto";
2
+ import { assemblePrompt } from "../../prompt-assembly/assemble.js";
3
+ import { artifactDeclarations } from "../../prompt-assembly/artifact.js";
4
+ import { buildTurnPromptSnapshot } from "../../prompt-assembly/turn-snapshot.js";
5
+ import { CacheBreakDetector, toolsToFingerprintInputs } from "../cache-break-detector.js";
6
+ import { defaultTaskRegistry } from "../task-registry.js";
7
+ import { refuseOutOfContractDecision } from "../tool-policy.js";
8
+ import { decodeTextBytes } from "../../tools/fs/encoding.js";
9
+ import { applyCompactionToReadFileState, isReadDedupStubResult } from "../../tools/fs/index.js";
10
+ import { resolveKey } from "../../tools/fs/safety.js";
11
+ import { PROMPT_HASH_SALT } from "./prompt-hash-salt.js";
12
+ import { CHANGED_FILES_MTIME_EPS_MS } from "./turn-attachments.js";
13
+ export function prepareTurnWiring(input) {
14
+ const { deps, systemPromptSeat, harnessTools, fpRef, model, epochArtifactDigestForSnapshot, promptProfile, fableMitigations, systemBlocks, thinking, spec, sessionId, turnSnapshotRef, promptManifest, charsPerToken, handsEnabled, executionEnv, attachmentRootCanonical, additionalRootsCanonical, additionalReadRootsCanonical, readDenyMatcher, resolvedReadFace, handsCwdRef, readFileStateForCheckpoint, denyNarrowingPolicy, abortController, hostTaskId, taskScope, buildAssembleInputs, centerAdoptionRef, harnessRef, epochDeclaredSections, carrierReadFace, readDenyAdditionsNormalized, preparedHolder } = input;
15
+ const cacheBreakDetector = deps.cacheBreakDetection === false ? undefined : new CacheBreakDetector();
16
+ const cacheFingerprint = cacheBreakDetector
17
+ ? { systemPrompt: systemPromptSeat.current, tools: toolsToFingerprintInputs(harnessTools) }
18
+ : undefined;
19
+ fpRef.current = cacheFingerprint;
20
+ const turnSnapshot = buildTurnPromptSnapshot({
21
+ provider: model.provider,
22
+ modelId: model.id,
23
+ artifactDigest: epochArtifactDigestForSnapshot,
24
+ promptProfile,
25
+ fableMitigations,
26
+ layoutGroups: (systemBlocks ?? [{ groupId: "body", cacheControlBoundary: true }]).map((b) => ({ groupId: b.groupId, cacheControlBoundary: b.cacheControlBoundary })),
27
+ stableSystemText: systemPromptSeat.current,
28
+ toolWire: toolsToFingerprintInputs(harnessTools),
29
+ thinkingLevel: (thinking ?? "off"),
30
+ maxTokens: model.maxTokens,
31
+ hasOutputSchema: spec.outputSchema !== undefined,
32
+ sessionId,
33
+ taskId: spec.taskId,
34
+ }, model.api, (text) => `salted:${createHash("sha256").update(PROMPT_HASH_SALT).update(text).digest("hex")}`);
35
+ turnSnapshotRef.current = turnSnapshot;
36
+ promptManifest.snapshot = { cacheIdentity: turnSnapshot.cacheIdentity, elements: { ...turnSnapshot.elements } };
37
+ promptManifest.lowering = turnSnapshot.lowering;
38
+ const promptOverheadTokens = Math.ceil((systemPromptSeat.current.length +
39
+ harnessTools.reduce((a, t) => a + t.name.length + (t.description?.length ?? 0) + JSON.stringify(t.parameters ?? {}).length, 0)) / charsPerToken);
40
+ const announceAttachmentSkip = (path, reason) => {
41
+ deps.onError?.(new Error(`working-file attachment skipped — ${path}: ${reason}`), {
42
+ phase: "degraded",
43
+ sessionId,
44
+ classification: "attachment-read",
45
+ });
46
+ };
47
+ const readTaskFile = handsEnabled
48
+ ? async (path) => {
49
+ try {
50
+ let key = path;
51
+ if (attachmentRootCanonical !== undefined) {
52
+ const rk = await resolveKey(executionEnv, attachmentRootCanonical, path, undefined, undefined, [...additionalRootsCanonical, ...additionalReadRootsCanonical], undefined, readDenyMatcher, resolvedReadFace);
53
+ if (!rk.ok) {
54
+ announceAttachmentSkip(path, rk.violation.message);
55
+ if (rk.violation.code === "read_path_denied" && rk.violation.pattern !== undefined) {
56
+ return { withheld: { pattern: rk.violation.pattern } };
57
+ }
58
+ return null;
59
+ }
60
+ key = rk.key;
61
+ }
62
+ const r = await executionEnv.readBinaryFile(key);
63
+ if (!r.ok) {
64
+ announceAttachmentSkip(path, r.error.message);
65
+ return null;
66
+ }
67
+ const d = decodeTextBytes(r.value);
68
+ if (d.malformed) {
69
+ announceAttachmentSkip(path, "malformed text encoding (truncated UTF-16 body)");
70
+ return null;
71
+ }
72
+ return d.text;
73
+ }
74
+ catch (err) {
75
+ announceAttachmentSkip(path, err instanceof Error ? err.message : String(err));
76
+ return null;
77
+ }
78
+ }
79
+ : undefined;
80
+ const normalizeAttachmentPath = handsEnabled
81
+ ? async (path) => {
82
+ if (attachmentRootCanonical === undefined)
83
+ return path;
84
+ try {
85
+ const rk = await resolveKey(executionEnv, attachmentRootCanonical, path, undefined, handsCwdRef?.current, [...additionalRootsCanonical, ...additionalReadRootsCanonical], undefined, undefined, resolvedReadFace);
86
+ return rk.ok ? rk.key : path;
87
+ }
88
+ catch {
89
+ return path;
90
+ }
91
+ }
92
+ : undefined;
93
+ const isDedupStubResult = handsEnabled ? isReadDedupStubResult : undefined;
94
+ const recentlyReadFiles = handsEnabled && readFileStateForCheckpoint
95
+ ? () => [...readFileStateForCheckpoint.entries()]
96
+ .filter(([, v]) => v.seededFromContext !== true)
97
+ .sort((a, b) => (b[1].lastReadAt ?? 0) - (a[1].lastReadAt ?? 0))
98
+ .map(([path]) => path)
99
+ : undefined;
100
+ const onCompactionApplied = handsEnabled && readFileStateForCheckpoint
101
+ ? (attachedComplete, preserveReadState) => applyCompactionToReadFileState(readFileStateForCheckpoint, attachedComplete, preserveReadState)
102
+ : undefined;
103
+ const changedFilesReadDenied = denyNarrowingPolicy === undefined
104
+ ? undefined
105
+ : async (path) => {
106
+ try {
107
+ const d = refuseOutOfContractDecision(await denyNarrowingPolicy.check({ toolName: "Read", args: { file_path: path }, toolCallId: "changed-files-visibility-probe" }, abortController.signal));
108
+ return d.action === "deny";
109
+ }
110
+ catch {
111
+ return false;
112
+ }
113
+ };
114
+ const detectExternalChanges = handsEnabled && readFileStateForCheckpoint
115
+ ? async (maxFiles) => {
116
+ const candidates = [...readFileStateForCheckpoint.entries()]
117
+ .filter(([, e]) => e.lastReadAt !== undefined)
118
+ .filter(([, e]) => e.truncated !== true)
119
+ .sort((a, b) => (b[1].lastReadAt ?? 0) - (a[1].lastReadAt ?? 0))
120
+ .slice(0, Math.max(0, maxFiles));
121
+ const changed = [];
122
+ const evicted = [];
123
+ for (const [path, entry] of candidates) {
124
+ if (changedFilesReadDenied !== undefined && (await changedFilesReadDenied(path)))
125
+ continue;
126
+ try {
127
+ const info = await executionEnv.fileInfo(path, abortController.signal);
128
+ if (!info.ok) {
129
+ if (info.error.code === "not_found") {
130
+ readFileStateForCheckpoint.delete(path);
131
+ evicted.push(path);
132
+ }
133
+ continue;
134
+ }
135
+ if (info.value.mtimeMs > (entry.lastReadAt ?? 0) + CHANGED_FILES_MTIME_EPS_MS) {
136
+ changed.push({ path, mtimeMs: info.value.mtimeMs });
137
+ }
138
+ }
139
+ catch {
140
+ }
141
+ }
142
+ return { changed, evicted };
143
+ }
144
+ : undefined;
145
+ const listBackgroundTasks = () => defaultTaskRegistry
146
+ .list({ owner: hostTaskId, scope: taskScope, sessionId })
147
+ .filter((t) => t.status === "pending" || t.status === "running" || t.status === "parked")
148
+ .map((t) => ({ id: t.task_id, ...(t.description !== undefined ? { description: t.description } : {}), status: t.status }));
149
+ const overheadState = { promptChars: 0 };
150
+ const centerCompactionCandidate = deps.promptSource !== undefined && centerAdoptionRef.current !== undefined
151
+ ? () => {
152
+ const state = deps.promptSource.sourceState?.() ?? "unavailable";
153
+ const cand = deps.promptSource.current();
154
+ const toBundled = state === "disabled";
155
+ if (!toBundled && (cand === undefined || cand.artifact.artifactDigest === centerAdoptionRef.current.artifact.artifactDigest))
156
+ return undefined;
157
+ let a2;
158
+ try {
159
+ a2 = assemblePrompt(buildAssembleInputs(toBundled ? undefined : artifactDeclarations(cand.artifact)));
160
+ }
161
+ catch {
162
+ return undefined;
163
+ }
164
+ if (!toBundled && a2.centerMounted !== true)
165
+ return undefined;
166
+ const declared2 = (a2.sections ?? [])
167
+ .filter((sec) => sec.origin === "provider")
168
+ .map((sec) => ({ id: sec.id, slot: sec.slot, chars: sec.text.length, ...(sec.declaredContentHash !== undefined ? { contentHash: sec.declaredContentHash } : {}) }));
169
+ const overheadTokensAfter = preparedHolder.current.promptOverheadTokens -
170
+ Math.ceil(overheadState.promptChars / charsPerToken) +
171
+ Math.ceil(a2.systemPrompt.length / charsPerToken);
172
+ return {
173
+ ...(toBundled
174
+ ? { clear: true }
175
+ : {
176
+ centerArtifactDigest: cand.artifact.artifactDigest,
177
+ ...(cand.sourceRevision !== undefined ? { sourceRevision: cand.sourceRevision } : {}),
178
+ }),
179
+ declaredSections: declared2,
180
+ overheadTokensAfter,
181
+ apply: (committedArtifactDigest) => {
182
+ systemPromptSeat.current = a2.systemPrompt;
183
+ harnessRef.current?.setSystemPrompt(a2.systemPrompt, a2.systemBlocks);
184
+ if (fpRef.current)
185
+ fpRef.current.systemPrompt = a2.systemPrompt;
186
+ turnSnapshotRef.current?.refreshStableSystem(a2.systemPrompt, committedArtifactDigest);
187
+ preparedHolder.current.epochDeclaredSections = declared2;
188
+ preparedHolder.current.promptOverheadTokens = overheadTokensAfter;
189
+ overheadState.promptChars = a2.systemPrompt.length;
190
+ centerAdoptionRef.current = toBundled
191
+ ? undefined
192
+ : { artifact: cand.artifact, ...(cand.sourceRevision !== undefined ? { sourceRevision: cand.sourceRevision } : {}) };
193
+ },
194
+ };
195
+ }
196
+ : undefined;
197
+ overheadState.promptChars = systemPromptSeat.current.length;
198
+ const effectiveReadFaceObserved = carrierReadFace();
199
+ const effectiveReadDenyObserved = readDenyAdditionsNormalized.length > 0 ? readDenyAdditionsNormalized.map((e) => ({ ...e })) : undefined;
200
+ return { cacheBreakDetector, cacheFingerprint, promptOverheadTokens, readTaskFile, normalizeAttachmentPath, isDedupStubResult, recentlyReadFiles, onCompactionApplied, detectExternalChanges, listBackgroundTasks, centerCompactionCandidate, effectiveReadFaceObserved, effectiveReadDenyObserved };
201
+ }
@@ -25,7 +25,7 @@ import type { LockedPreflight } from "../locked-config.js";
25
25
  import type { MaterializedMcp } from "../mcp.js";
26
26
  import type { SessionStore } from "../session.js";
27
27
  import { type ToolResultStore } from "../tool-result-store.js";
28
- import { type ApprovalSettledBy, type OnAsk } from "../tool-policy.js";
28
+ import { type ApprovalSettledBy, type OnAsk, type ToolPolicy } from "../tool-policy.js";
29
29
  import type { RunnerDeps, RuntimeCaps, TaskSpec, ToolEffect, ToolExecuteContext } from "../types.js";
30
30
  import { type UsageWindow } from "../usage-window-store.js";
31
31
  import { type AutoModeArmReason, type WiringManifest, type WiringManifestMcpEntry } from "../wiring-manifest.js";
@@ -33,8 +33,9 @@ import type { AnnounceOnceSink, Prepared, PrepareResume, RunInternals, UsageGove
33
33
  export interface PrepareWiringManifestInput {
34
34
  /** borrowed-readonly — the REBOUND spec. Read: `onAsk` (the own seat), `onQuestion` (the question seam), `handsReadOnly` / `tools` (the ungated scan),
35
35
  * `resourceSuspend` (the task-axis lane), `durableApproval` / `principal` (the synthesis and the notice), `interactiveTools` /
36
- * `mcp` / `checkpointStore` (manifest cells and the notice's cause). Never mutated. */
37
- spec: Pick<TaskSpec, "onAsk" | "onQuestion" | "handsReadOnly" | "tools" | "resourceSuspend" | "durableApproval" | "principal" | "interactiveTools" | "mcp" | "checkpointStore">;
36
+ * `mcp` / `checkpointStore` (manifest cells and the notice's cause), `enablePlanMode` (the gate-machinery predicate's
37
+ * plan-mode term). Never mutated. */
38
+ spec: Pick<TaskSpec, "onAsk" | "onQuestion" | "handsReadOnly" | "tools" | "resourceSuspend" | "durableApproval" | "principal" | "interactiveTools" | "mcp" | "checkpointStore" | "enablePlanMode">;
38
39
  /** borrowed-readonly — the deployment seats, whole: `onAsk` (the seat's deps half), `onError` (the warning), `executionEnv`
39
40
  * (the hook-env predicate), `checkpointStore` (durable infra), the manifest's dozen wiring facts (`backgroundAgentStore`,
40
41
  * `permissionRuleStore`, `permissionRuleSyncWired`, `onBackgroundChildEvent`, `lockedConfig`, `compliancePostureResolver`,
@@ -61,6 +62,9 @@ export interface PrepareWiringManifestInput {
61
62
  preToolUseObservational: boolean;
62
63
  /** borrowed-readonly — the caller-facing policy layers; only their COUNT is read (an effect-aware gate exists). */
63
64
  policyLayers: ReadonlyArray<unknown>;
65
+ /** borrowed-readonly — the policy chain's effective policy, or undefined; only its PRESENCE is read (the first term of the
66
+ * gate-machinery predicate below). Never called here. */
67
+ effectivePolicy: ToolPolicy | undefined;
64
68
  /** borrowed-readonly — whether an execution env is wired (the hand band exists at all). */
65
69
  handsEnabled: boolean;
66
70
  /** borrowed-readonly — the materialized MCP handle: `tools` (the destructive-ungated scan) and `statuses` (the manifest face). */
@@ -160,6 +164,10 @@ export interface PrepareWiringManifestResult {
160
164
  wiringManifest: WiringManifest;
161
165
  /** owned — THE park predicate as one named const (`parkLane.effective === true`). */
162
166
  parkLaneArmed: boolean;
167
+ /** owned — THE gate-machinery activation predicate as one named const: a policy, a screening hook, an egress or
168
+ * irreversible tool, an eligible resource lane, an armed platform lane, or plan mode. The ask lane, the suspend saga,
169
+ * the boundary parks, the park closure and the gate station are built exactly when it holds. */
170
+ gateMachineryActive: boolean;
163
171
  /** owned — THE identity mint of this leg (`Prepared.hookIdentity`), one frozen envelope every hook station reuses. */
164
172
  hookIdentity: HookInvocationIdentity;
165
173
  /** owned — the read-only env capability face every hook context carries, or undefined (no hook consumer, or no env wired). */
@@ -109,7 +109,7 @@ function announceDurableGateUnavailable(args) {
109
109
  });
110
110
  }
111
111
  export function prepareWiringManifest(input) {
112
- const { spec, deps, internals, resume, sessionId, runId, hostTaskId, taskScope, hooks, preToolUseObservational, policyLayers, handsEnabled, mcp, irreversibleTools, egressTools, toolEffects, ownedEnv, executionEnv, offloadStore, onceLedger, usageWindows, envLifetimeSuspendAt, runtimeCaps, resolvedInteractionPosture, durableQuestionFace, liveQuestionFace, checkpointStore, sessions, modelGateManifest, autoModeArmReason, lockedPreflight, peerLaneRefs, peerLaneActive, harness, suspendRef, reviewRef, frozenOnAsk, liveInheritedGate, delegation } = input;
112
+ const { spec, deps, internals, resume, sessionId, runId, hostTaskId, taskScope, hooks, preToolUseObservational, policyLayers, effectivePolicy, handsEnabled, mcp, irreversibleTools, egressTools, toolEffects, ownedEnv, executionEnv, offloadStore, onceLedger, usageWindows, envLifetimeSuspendAt, runtimeCaps, resolvedInteractionPosture, durableQuestionFace, liveQuestionFace, checkpointStore, sessions, modelGateManifest, autoModeArmReason, lockedPreflight, peerLaneRefs, peerLaneActive, harness, suspendRef, reviewRef, frozenOnAsk, liveInheritedGate, delegation } = input;
113
113
  const onAsk = spec.onAsk ?? deps.onAsk;
114
114
  const handWriteTools = handsEnabled && spec.handsReadOnly !== true
115
115
  ? Object.keys(HAND_TOOL_EFFECTS).filter((name) => {
@@ -224,6 +224,13 @@ export function prepareWiringManifest(input) {
224
224
  });
225
225
  }
226
226
  const parkLaneArmed = wiringManifest.parkLane.effective === true;
227
+ const gateMachineryActive = effectivePolicy !== undefined ||
228
+ hooks?.preToolUse !== undefined ||
229
+ egressTools.size > 0 ||
230
+ irreversibleTools.size > 0 ||
231
+ resourceSuspendEligible ||
232
+ platformSuspendArmed ||
233
+ spec.enablePlanMode === true;
227
234
  announceDurableGateUnavailable({ onNotice: deps.onNotice, forceDurableGate: runtimeCaps?.forceDurableGate === true, storeWired: checkpointStore !== undefined, taskStoreNull: spec.checkpointStore === null, liveApprover: isLiveApproverSeat(frozenOnAsk), liveQuestionFace: liveQuestionFace !== undefined, parentConstraints: liveInheritedGate?.parentConstraints, sessionId, runId, principal: spec.principal });
228
235
  const hookIdentity = mintHookInvocationIdentity({
229
236
  sessionId,
@@ -236,5 +243,5 @@ export function prepareWiringManifest(input) {
236
243
  });
237
244
  const hookContextConsumerWired = hooks?.preToolUse !== undefined || hooks?.postToolUse !== undefined || hooks?.postToolUseFailure !== undefined;
238
245
  const hookEnvFace = hookContextConsumerWired && (ownedEnv ?? deps.executionEnv) != null ? createHookEnvCapabilities(executionEnv) : undefined;
239
- return { onAsk, preToolContexts, blockedToolCalls, approvalSettlement, humanBareRejections, batchHaltRef, blockedTracked, incompleteSuspendAdapter, durableSuspendInfraReady, resourceSuspendEligible, usageGovernance, platformSuspendArmed, durableApproval, wiringManifest, parkLaneArmed, hookIdentity, hookEnvFace };
246
+ return { onAsk, preToolContexts, blockedToolCalls, approvalSettlement, humanBareRejections, batchHaltRef, blockedTracked, incompleteSuspendAdapter, durableSuspendInfraReady, resourceSuspendEligible, usageGovernance, platformSuspendArmed, durableApproval, wiringManifest, parkLaneArmed, gateMachineryActive, hookIdentity, hookEnvFace };
240
247
  }
@@ -1,34 +1,7 @@
1
- import type { RemoteExecutionEnv, RemoteExecutionError, SnapshotId, VmLifecycleOptions } from "../remote-env.js";
2
1
  import type { ExecutionEnv } from "../../internal/harness.js";
3
- import type { RemoteEnvFailureNote, RunnerDeps, TaskSpec } from "../types.js";
2
+ import type { RunnerDeps, TaskSpec } from "../types.js";
4
3
  import type { PrepareResume, RunInternals } from "./contracts.js";
5
- /**
6
- * RB-439-a — project one failed remote lifecycle call onto the caller-facing note. ONE builder for all
7
- * three ops so the `retryable` classification can never drift between the lane that reports a `suspendVM`
8
- * refusal and the lane that reports a failed restore.
9
- */
10
- export declare function remoteEnvFailureNote(op: RemoteEnvFailureNote["op"], error: RemoteExecutionError, attempts: number): RemoteEnvFailureNote;
11
- /**
12
- * RB-439-a — `resumeVM` with a bounded retry on the TRANSIENT codes, plus the attempt count for the
13
- * caller-facing note.
14
- *
15
- * Why this op and no other: `resumeVM` is the one lifecycle call the seam documents as "idempotent +
16
- * re-entrant" (a replica that crashed mid-suspend can be superseded by another resuming the SAME
17
- * snapshot), which is exactly `withRetry`'s red line — retry idempotent establishment, never a
18
- * side-effecting op. `suspendVM` TAKES a snapshot and `postResumeInit` re-injects secrets under an
19
- * ordering red line; neither is retried here, they are disclosed with `retryable` instead so the caller
20
- * decides. A code outside the retryable family (`unsupported`, `auth_failed`) returns on the first
21
- * attempt — `withRetry` will not spend a second call on a permanent refusal.
22
- */
23
- export declare function restoreWorkspaceWithRetry(env: RemoteExecutionEnv, snapshotId: SnapshotId, options: VmLifecycleOptions,
24
- /** design/384 slice 2 (additive): observe each attempt AS IT STARTS. The returned `attempts` is
25
- * only readable after settlement, and a caller whose wait on this promise is BOUNDED (the park
26
- * compensation) must report the live attempt count when its bound fires mid-call — without this
27
- * seat a deaf second attempt was reported as `attempts: 1`. */
28
- onAttempt?: (attempt: number) => void): Promise<{
29
- outcome: Awaited<ReturnType<RemoteExecutionEnv["resumeVM"]>>;
30
- attempts: number;
31
- }>;
4
+ export { remoteEnvFailureNote, restoreWorkspaceWithRetry } from "./remote-env-retry.js";
32
5
  export interface PrepareWorkspaceRestoreInput {
33
6
  /** borrowed-readonly (the ownership-ref PROTOCOL stays with the driver — D-8 案①): the env this
34
7
  * task owns, when the factory minted one. This phase reads and drives it (`resumeVM`,
@@ -1,20 +1,7 @@
1
- import { isRemoteExecutionEnv, isRetryableRemoteErrorCode, missingRestoreSurface, RETRYABLE_REMOTE_ERROR_CODES } from "../remote-env.js";
2
- import { withRetry } from "../with-retry.js";
1
+ import { isRemoteExecutionEnv, missingRestoreSurface } from "../remote-env.js";
2
+ import { remoteEnvFailureNote, restoreWorkspaceWithRetry } from "./remote-env-retry.js";
3
3
  import { rebaseWorkspacePathAcross } from "./workspace-path.js";
4
- export function remoteEnvFailureNote(op, error, attempts) {
5
- return { op, code: error.code, retryable: isRetryableRemoteErrorCode(error.code), attempts, message: error.message };
6
- }
7
- const REMOTE_RESTORE_MAX_ATTEMPTS = 2;
8
- const REMOTE_RESTORE_BACKOFF_MS = 200;
9
- export async function restoreWorkspaceWithRetry(env, snapshotId, options, onAttempt) {
10
- let attempts = 0;
11
- const outcome = await withRetry(async (attempt) => {
12
- attempts = attempt;
13
- onAttempt?.(attempt);
14
- return env.resumeVM(snapshotId, options);
15
- }, { retryableCodes: RETRYABLE_REMOTE_ERROR_CODES, maxAttempts: REMOTE_RESTORE_MAX_ATTEMPTS, backoffMs: () => REMOTE_RESTORE_BACKOFF_MS }, { ...(options.abortSignal !== undefined ? { signal: options.abortSignal } : {}) });
16
- return { outcome, attempts };
17
- }
4
+ export { remoteEnvFailureNote, restoreWorkspaceWithRetry } from "./remote-env-retry.js";
18
5
  export async function prepareWorkspaceRestore(input) {
19
6
  const { ownedEnv, deps, spec, internals, resume, abortController, sessionId } = input;
20
7
  let taskRootPath = input.taskRootInitial;
@@ -0,0 +1 @@
1
+ export declare const PROMPT_HASH_SALT: Buffer<ArrayBufferLike>;
@@ -0,0 +1,2 @@
1
+ import { randomBytes } from "node:crypto";
2
+ export const PROMPT_HASH_SALT = randomBytes(16);
@@ -0,0 +1,29 @@
1
+ import type { RemoteExecutionEnv, RemoteExecutionError, SnapshotId, VmLifecycleOptions } from "../remote-env.js";
2
+ import type { RemoteEnvFailureNote } from "../types.js";
3
+ /**
4
+ * RB-439-a — project one failed remote lifecycle call onto the caller-facing note. ONE builder for all
5
+ * three ops so the `retryable` classification can never drift between the lane that reports a `suspendVM`
6
+ * refusal and the lane that reports a failed restore.
7
+ */
8
+ export declare function remoteEnvFailureNote(op: RemoteEnvFailureNote["op"], error: RemoteExecutionError, attempts: number): RemoteEnvFailureNote;
9
+ /**
10
+ * RB-439-a — `resumeVM` with a bounded retry on the TRANSIENT codes, plus the attempt count for the
11
+ * caller-facing note.
12
+ *
13
+ * Why this op and no other: `resumeVM` is the one lifecycle call the seam documents as "idempotent +
14
+ * re-entrant" (a replica that crashed mid-suspend can be superseded by another resuming the SAME
15
+ * snapshot), which is exactly `withRetry`'s red line — retry idempotent establishment, never a
16
+ * side-effecting op. `suspendVM` TAKES a snapshot and `postResumeInit` re-injects secrets under an
17
+ * ordering red line; neither is retried here, they are disclosed with `retryable` instead so the caller
18
+ * decides. A code outside the retryable family (`unsupported`, `auth_failed`) returns on the first
19
+ * attempt — `withRetry` will not spend a second call on a permanent refusal.
20
+ */
21
+ export declare function restoreWorkspaceWithRetry(env: RemoteExecutionEnv, snapshotId: SnapshotId, options: VmLifecycleOptions,
22
+ /** design/384 slice 2 (additive): observe each attempt AS IT STARTS. The returned `attempts` is
23
+ * only readable after settlement, and a caller whose wait on this promise is BOUNDED (the park
24
+ * compensation) must report the live attempt count when its bound fires mid-call — without this
25
+ * seat a deaf second attempt was reported as `attempts: 1`. */
26
+ onAttempt?: (attempt: number) => void): Promise<{
27
+ outcome: Awaited<ReturnType<RemoteExecutionEnv["resumeVM"]>>;
28
+ attempts: number;
29
+ }>;
@@ -0,0 +1,16 @@
1
+ import { isRetryableRemoteErrorCode, RETRYABLE_REMOTE_ERROR_CODES } from "../remote-env.js";
2
+ import { withRetry } from "../with-retry.js";
3
+ export function remoteEnvFailureNote(op, error, attempts) {
4
+ return { op, code: error.code, retryable: isRetryableRemoteErrorCode(error.code), attempts, message: error.message };
5
+ }
6
+ const REMOTE_RESTORE_MAX_ATTEMPTS = 2;
7
+ const REMOTE_RESTORE_BACKOFF_MS = 200;
8
+ export async function restoreWorkspaceWithRetry(env, snapshotId, options, onAttempt) {
9
+ let attempts = 0;
10
+ const outcome = await withRetry(async (attempt) => {
11
+ attempts = attempt;
12
+ onAttempt?.(attempt);
13
+ return env.resumeVM(snapshotId, options);
14
+ }, { retryableCodes: RETRYABLE_REMOTE_ERROR_CODES, maxAttempts: REMOTE_RESTORE_MAX_ATTEMPTS, backoffMs: () => REMOTE_RESTORE_BACKOFF_MS }, { ...(options.abortSignal !== undefined ? { signal: options.abortSignal } : {}) });
15
+ return { outcome, attempts };
16
+ }
@@ -47,7 +47,7 @@ import { hasBackgroundShell, sweepBackgroundShells } from "../background-shell.j
47
47
  import { cloneObserverInput, formatHookFeedback, hookSeatExpiredError, MAX_HOOK_TIMEOUT_MS, mintHookInvocationIdentity, resolveHookTimeoutMs, runHookSeat } from "../hooks.js";
48
48
  import { buildHumanInputEvent, frameMidTurnUserInput, projectHumanInput } from "../human-input-projection.js";
49
49
  import { delimitUntrusted, inlineUntrusted, REVIEWER_NOTE_MAX_BODY, sanitizeUntrustedText, SHELLED_BODY_ENVELOPE_TAGS } from "../untrusted-text.js";
50
- import { droppedToolDigest, withAnnounceDigests } from "./prepare-announce-once.js";
50
+ import { droppedToolDigest, withAnnounceDigests } from "./announce-once-ledger.js";
51
51
  import { appendInterruptionMarker, reconcileInterruptedSession } from "../session-reconcile.js";
52
52
  import { RunnerSharedToolResultStore } from "../tool-result-store.js";
53
53
  import { formatDiagnosticsBlock } from "../lsp-diagnostics.js";
@@ -312,3 +312,15 @@ export declare function boundedTail(pathEntries: readonly SessionTreeEntry[]): {
312
312
  tail: SessionTreeEntry[];
313
313
  floorEntryId: string | null;
314
314
  };
315
+ /**
316
+ * The ONE "has this session seen conversation?" predicate — the freshness question is asked three
317
+ * times in one prepare (project-memory phase, design/148 center adoption, the epoch-pin
318
+ * discriminator), and an entry-COUNT proxy at ANY of them re-creates the same lie: a host may write
319
+ * session_info / model_change / … rows before the first turn (naming a new chat, recording a model
320
+ * pick), and none of that is evidence of a prior session (5.29 merge-rescan: the count proxy
321
+ * survived at the adoption arm after the pin arm was fixed, silently costing a pre-named session
322
+ * its published center prompt for life). Conversation content — and only that — is the evidence.
323
+ */
324
+ export declare function hasConversationContent(branch: ReadonlyArray<{
325
+ type: string;
326
+ }>): boolean;
@@ -24,3 +24,6 @@ export function boundedTail(pathEntries) {
24
24
  }
25
25
  return { tail: pathEntries.slice(floorIdx), floorEntryId };
26
26
  }
27
+ export function hasConversationContent(branch) {
28
+ return branch.some((e) => e.type === "message" || e.type === "custom_message" || e.type === "compaction");
29
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sema-agent/core",
3
- "version": "7.5.1",
3
+ "version": "7.5.2",
4
4
  "description": "Stateless, task-oriented AI agent core",
5
5
  "type": "module",
6
6
  "license": "BUSL-1.1",