@sema-agent/core 7.31.1 → 7.32.1

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 (87) hide show
  1. package/CHANGELOG.md +75 -0
  2. package/dist/agents/inherited-tool-pool.d.ts +28 -0
  3. package/dist/agents/inherited-tool-pool.js +15 -0
  4. package/dist/agents/list-agents-tool.d.ts +7 -0
  5. package/dist/agents/list-agents-tool.js +9 -2
  6. package/dist/agents/peer-session-drain.d.ts +2 -0
  7. package/dist/agents/peer-session-drain.js +1 -0
  8. package/dist/agents/send-message-tool.d.ts +17 -0
  9. package/dist/agents/send-message-tool.js +11 -4
  10. package/dist/agents/subagent.d.ts +10 -1
  11. package/dist/agents/subagent.js +24 -8
  12. package/dist/core/ask-origin.d.ts +15 -6
  13. package/dist/core/ask-origin.js +15 -9
  14. package/dist/core/background-shell.d.ts +61 -0
  15. package/dist/core/background-shell.js +14 -0
  16. package/dist/core/checkpoint-store.d.ts +5 -0
  17. package/dist/core/checkpoint-store.js +3 -2
  18. package/dist/core/engine-notice.d.ts +9 -0
  19. package/dist/core/gate-lanes.js +1 -1
  20. package/dist/core/governance-codes.d.ts +1 -1
  21. package/dist/core/governance-codes.js +4 -0
  22. package/dist/core/hooks.d.ts +2 -0
  23. package/dist/core/read-root-grant-screen.d.ts +23 -0
  24. package/dist/core/read-root-grant-screen.js +17 -0
  25. package/dist/core/runner/contracts.d.ts +1 -1
  26. package/dist/core/runner/prepare-artifact.d.ts +12 -7
  27. package/dist/core/runner/prepare-artifact.js +10 -8
  28. package/dist/core/runner/prepare-ask-lane.d.ts +3 -0
  29. package/dist/core/runner/prepare-ask-lane.js +5 -1
  30. package/dist/core/runner/prepare-boundary-parks.js +5 -5
  31. package/dist/core/runner/prepare-delegation-surface.d.ts +18 -0
  32. package/dist/core/runner/prepare-delegation-surface.js +6 -25
  33. package/dist/core/runner/prepare-hands-readface.d.ts +12 -89
  34. package/dist/core/runner/prepare-hands-readface.js +14 -48
  35. package/dist/core/runner/prepare-inherited-gate.d.ts +3 -1
  36. package/dist/core/runner/prepare-inherited-gate.js +2 -1
  37. package/dist/core/runner/prepare-park-ask.js +6 -4
  38. package/dist/core/runner/prepare-policy-chain.js +4 -0
  39. package/dist/core/runner/prepare-task.js +5 -3
  40. package/dist/core/runner/read-root-grant.d.ts +66 -0
  41. package/dist/core/runner/read-root-grant.js +77 -0
  42. package/dist/core/runner/run-notification-lane.d.ts +1 -1
  43. package/dist/core/runner/run-settle-and-teardown.js +3 -11
  44. package/dist/core/runner/runtask.js +1 -1
  45. package/dist/core/runner/send-message-continuation.d.ts +45 -0
  46. package/dist/core/runner/send-message-continuation.js +27 -0
  47. package/dist/core/runner/session-read-file-states.d.ts +91 -0
  48. package/dist/core/runner/session-read-file-states.js +43 -0
  49. package/dist/core/store-contracts/checkpoint-store-contract.d.ts +2 -0
  50. package/dist/core/store-contracts/checkpoint-store-contract.js +19 -0
  51. package/dist/core/task-registry-monitor.js +3 -0
  52. package/dist/core/task-registry-shared.d.ts +17 -4
  53. package/dist/core/task-registry.d.ts +68 -29
  54. package/dist/core/task-registry.js +78 -28
  55. package/dist/core/task-spec.d.ts +5 -3
  56. package/dist/core/tool-policy.d.ts +23 -0
  57. package/dist/core/tool-policy.js +15 -4
  58. package/dist/core/tool-spec.d.ts +2 -0
  59. package/dist/engine/execution-env/node-execution-env.d.ts +13 -3
  60. package/dist/engine/execution-env/node-execution-env.js +26 -0
  61. package/dist/index.d.ts +4 -3
  62. package/dist/index.js +2 -1
  63. package/dist/scenarios/full-body.d.ts +5 -2
  64. package/dist/scenarios/full-body.js +14 -7
  65. package/dist/tools/fs/fs-bash-background-text.d.ts +16 -0
  66. package/dist/tools/fs/fs-bash-background-text.js +22 -0
  67. package/dist/tools/fs/fs-bash.d.ts +6 -0
  68. package/dist/tools/fs/fs-bash.js +9 -27
  69. package/dist/tools/fs/fs-read.d.ts +2 -2
  70. package/dist/tools/fs/fs-search-tools.d.ts +3 -2
  71. package/dist/tools/fs/index.d.ts +8 -1
  72. package/dist/tools/fs/index.js +2 -3
  73. package/dist/tools/fs/repo-map.d.ts +2 -1
  74. package/dist/tools/fs/safety.d.ts +3 -1
  75. package/dist/tools/fs/safety.js +2 -1
  76. package/dist/tools/fs/shell-read-check.d.ts +3 -0
  77. package/dist/tools/fs/shell-read-check.js +1 -1
  78. package/dist/tools/fs/shell-read-establish.d.ts +3 -1
  79. package/dist/tools/fs/shell-read-establish.js +2 -2
  80. package/dist/tools/monitor.d.ts +5 -4
  81. package/dist/tools/monitor.js +5 -4
  82. package/dist/tools/todo.d.ts +3 -3
  83. package/dist/tools/todo.js +5 -4
  84. package/package.json +1 -1
  85. package/test/export-surface.snapshot.json +13 -1
  86. package/wire-changes/core@7.32.0.json +850 -0
  87. package/wire-changes/core@7.32.1.json +78 -0
@@ -8,6 +8,13 @@ export class BackgroundShellError extends Error {
8
8
  this.name = "BackgroundShellError";
9
9
  }
10
10
  }
11
+ export function retainsBackgroundProcesses(lanes) {
12
+ return (lanes.env === true || lanes.task === true || lanes.marked === true) && lanes.envDying !== true;
13
+ }
14
+ export function envDeclaresRetain(env) {
15
+ const caps = env.backgroundCapabilities;
16
+ return typeof caps === "object" && caps !== null && caps.retainBackgroundProcesses === true;
17
+ }
11
18
  export function hasBackgroundShell(env) {
12
19
  const e = env;
13
20
  return (typeof e.spawnBackground === "function" &&
@@ -26,3 +33,10 @@ export async function sweepBackgroundShells(env, attribution, opts) {
26
33
  }
27
34
  await env.disposeBackgroundShells(opts?.except !== undefined ? { except: opts.except } : undefined);
28
35
  }
36
+ export async function sweepRunBoundaryBackgroundShells(env, registry, lanes) {
37
+ const envDying = lanes.envDying === true;
38
+ if (retainsBackgroundProcesses({ env: envDeclaresRetain(env), task: lanes.task, envDying }))
39
+ return;
40
+ const except = envDying ? [] : registry.runningShellIds(env);
41
+ await sweepBackgroundShells(env, registry, except.length > 0 ? { except } : undefined);
42
+ }
@@ -816,6 +816,9 @@ export type PendingAction = {
816
816
  * when a person's rule also speaks — never a substitute for this one). Display metadata: no gate, CAS or
817
817
  * resume path reads it. */
818
818
  mandated?: true;
819
+ /** The PARK twin of `AskRequest.mandate` (same derivation, same precedent): the table word the bit stands on,
820
+ * present only when it is the whole reason. Written only as a member of the closed set. */
821
+ mandate?: import("./ask-origin.js").PersistedRuleMandate;
819
822
  /** The PARK twin of `AskRequest.denialLimitFallback`: present
820
823
  * ⇔ this parked ask is the classifier DENIAL-LIMIT fallback, carrying the counts that tripped
821
824
  * the bound. Its `autoDenyAfterMs` is ALWAYS `0` on a row: the window is a fact of the ask's
@@ -1947,6 +1950,8 @@ export interface CheckpointSummary {
1947
1950
  * OMITTED (the {@link toolCallId} absence-shape contract), screened to the literal `true` — a pre-#1079 row
1948
1951
  * reads absent (unreported). ECHO-ONLY; nothing gates on it. */
1949
1952
  mandated?: true;
1953
+ /** The row's `PendingAction.tool_approval.mandate`, echoed ONLY as a member of the closed set. ECHO-ONLY. */
1954
+ mandate?: import("./ask-origin.js").PersistedRuleMandate;
1950
1955
  /** The row's `PendingAction.tool_approval.denialLimitFallback`, projected BESIDE the preview
1951
1956
  * for the same one-call reason: a card that shows the fallback's counts and limit must not re-open
1952
1957
  * the row to learn them (`autoDenyAfterMs` is `0` on every row — see the row member). Pure echo;
@@ -3,7 +3,7 @@ import { uuidv7 } from "../internal/harness.js";
3
3
  import { PROBE_CAUSE_PATH_MAX, PROBE_REASON_MAX, inlineUntrusted, sanitizeUntrustedText } from "./untrusted-text.js";
4
4
  import { carriesBidiControls } from "./tool-policy.js";
5
5
  import { readDenialLimitFallback } from "./auto-mode.js";
6
- import { isAskOrigin, isRuleStoreUnreadable } from "./ask-origin.js";
6
+ import { isAskOrigin, isPersistedRuleMandate, isRuleStoreUnreadable } from "./ask-origin.js";
7
7
  import { checkpointExecutionRecorded, executionRecordDisposition, ExecutionOutcomeConflictError, } from "./checkpoint-execution-record.js";
8
8
  import { renderUntrustedCommandText, stripFormatCharacters } from "./permission-rule-model.js";
9
9
  import { redactSecrets } from "./untrusted-egress.js";
@@ -305,7 +305,8 @@ export function summarizeCheckpoint(cp) {
305
305
  ...(() => {
306
306
  const origin = tool?.origin;
307
307
  const ruleStoreUnreadable = tool?.ruleStoreUnreadable;
308
- return { ...(isAskOrigin(origin) ? { origin } : {}), ...(isRuleStoreUnreadable(ruleStoreUnreadable) ? { ruleStoreUnreadable } : {}) };
308
+ const mandate = tool?.mandate;
309
+ return { ...(isAskOrigin(origin) ? { origin } : {}), ...(isRuleStoreUnreadable(ruleStoreUnreadable) ? { ruleStoreUnreadable } : {}), ...(isPersistedRuleMandate(mandate) ? { mandate } : {}) };
309
310
  })(),
310
311
  ...(hasBidi ? { hasBidiControls: true } : {}),
311
312
  };
@@ -400,6 +400,15 @@ export interface EngineNotice {
400
400
  * AFTER the loop's end is refused typed (`steering.not_running`, every verb alike) and mints
401
401
  * nothing: the refusal itself is the loud answer.
402
402
  *
403
+ * - `"approval.read_root_granted"` / `"approval.read_root_grant_rejected"` — an approver's `readRootGrant`
404
+ * (`AskOutcome`). GRANTED: it equalled the card's `readRootCandidate.dir` beside `allow: true`; this run's READ
405
+ * directories now include it (never the write roots; this run only); `detail: { dir, covers?, root, toolName,
406
+ * toolCallId, sessionId, runId, taskId? }`, `root` = the canonical spelling held. REJECTED: nothing changed,
407
+ * the verdict stands; `detail: { reason, offered?, requested?, …same ids }`, `reason` ∈ `on_refusal` |
408
+ * `not_a_string` | `no_candidate` | `mismatch` | `unresolvable` | `resolves_elsewhere` | `admits_artifact_store`
409
+ * (the directory contains the wired local artifact store, lies inside it or is it) | `no_read_roots`. Once per
410
+ * answered ask. Audience `"user"`.
411
+ *
403
412
  * - `"checkpoint.execution_outcome_unrecorded"` — the resumed leg's pending-call resolver disposed
404
413
  * the decided action (executed it, delivered the refusal, or vetoed the person's yes) and filed
405
414
  * the frame's record through `CheckpointStore.recordExecutionOutcome`, and the store answered
@@ -467,7 +467,7 @@ export async function runGateLanes(pass) {
467
467
  ...(d.action === "ask" && d.denialLimitFallback !== undefined ? { denialLimitFallback: unarmedWindow(d.denialLimitFallback) } : {}),
468
468
  ...(d.action === "ask" && d.origin !== undefined ? { origin: d.origin } : {}),
469
469
  ...(d.action === "ask" && d.ruleStoreUnreadable !== undefined ? { ruleStoreUnreadable: d.ruleStoreUnreadable } : {}),
470
- ...(d.action === "ask" && d.mandated === true ? { mandated: true } : {}),
470
+ ...(d.action === "ask" && d.mandated === true ? { mandated: true, ...(d.mandate !== undefined ? { mandate: d.mandate } : {}) } : {}),
471
471
  });
472
472
  if (suspendAsk && pass.decision.action === "ask") {
473
473
  const parkArgs = [pass.req, pass.currentInput, safety, undefined, realApprovalOf(pass.decision), pass.decision.action === "ask" ? pass.decision.persistedRuleShadowed : undefined, pass.decision.action === "ask" ? pass.decision.decisionReason : undefined, pass.decision.action === "ask" ? pass.decision.probeReason : undefined, pass.decision.action === "ask" ? pass.decision.probeCause : undefined, pass.decision.action === "ask" ? pass.decision.segmentCoverage : undefined, pass.decision.action === "ask" ? pass.decision.matchedAskRule : undefined, pass.decision.action === "ask" ? pass.decision.probeMandated : undefined];
@@ -119,7 +119,7 @@ export type NoticeAudience = "user" | "operator";
119
119
  * the union's catalog ({@link EngineNotice.code}). The groups below say only which family a row joins,
120
120
  * so the catalog is not written a second time here and the two cannot drift.
121
121
  */
122
- export declare const ENGINE_NOTICE_CODES: readonly ["config.autocompact_window_clamped", "config.env_timeout_discarded", "config.materialize_env_discarded", "config.models_swapped", "config.context_settings_swapped", "config.read_face_swapped", "config.read_face_deployment_clamped", "config.write_face_swapped", "config.write_face_deployment_clamped", "config.tool_model_gate_removed", "config.tool_model_gate_unknown_class", "config.tool_model_gate_env_invalid", "config.artifact_host_invalid", "config.task_root_not_canonical", "config.write_protection_target_view_absent", "config.write_protection_unresolved", "config.execution_env_capability_invalid", "config.tool_card_undeclared", "config.tool_face_undeclared", "config.tool_face_invalid", "config.forward_sink_failed", "config.durable_gate_unavailable", "config.peer_admission_out_of_range", "config.peer_lane_unmounted", "config.secret_env_scrubbed", "peer.inbound_disposition", "peer.held_settled", "peer.idle_subscription", "classifier.denial_limit", "classifier.fallback", "classifier.role_defaulted", "classifier.contract_broken", "checkpoint.execution_outcome_unrecorded", "delegation.transcript_integrity", "delegation.ask_unresolvable", "mcp.injection_dropped", "mcp.revocation_probe_failed", "mcp.server_redialed", "workflow.governance_key_stripped", "workflow.agent_option_ignored", "memory.session_polluted", "memory.harvest_quarantined", "memory.delegation_static_mark_waived", "memory.content_class_declared", "memory.hold_opened", "memory.hold_released", "memory.hold_disposed", "memory.consolidation_recommended", "memory.consolidation_committed", "memory.consolidation_conflict", "memory.consolidation_incomplete", "memory.consolidation_refused", "memory.consolidation_withheld", "memory.restore_withheld", "memory.project_scope_absent", "route.fallback_to_primary", "route.classifier_unroutable", "route.base_url_changed_key_unchanged", "route.response_model_mismatch", "task.user_steer_undrained", "task.user_followup_undrained", "steering.parked_input_blocked", "task.turn_interrupted", "task.halt_unconsumed", "task.late_approval", "task.interrupt_unconsumed", "memory.capture_opted_out", "memory.capture_optout_unpersisted", "skills.listing_truncated", "agents.listing_truncated", "tool_result.offload_put_failed"];
122
+ export declare const ENGINE_NOTICE_CODES: readonly ["config.autocompact_window_clamped", "config.env_timeout_discarded", "config.materialize_env_discarded", "config.models_swapped", "config.context_settings_swapped", "config.read_face_swapped", "config.read_face_deployment_clamped", "config.write_face_swapped", "config.write_face_deployment_clamped", "config.tool_model_gate_removed", "config.tool_model_gate_unknown_class", "config.tool_model_gate_env_invalid", "config.artifact_host_invalid", "config.task_root_not_canonical", "config.write_protection_target_view_absent", "config.write_protection_unresolved", "config.execution_env_capability_invalid", "config.tool_card_undeclared", "config.tool_face_undeclared", "config.tool_face_invalid", "config.forward_sink_failed", "config.durable_gate_unavailable", "config.peer_admission_out_of_range", "config.peer_lane_unmounted", "config.secret_env_scrubbed", "peer.inbound_disposition", "peer.held_settled", "peer.idle_subscription", "classifier.denial_limit", "classifier.fallback", "classifier.role_defaulted", "classifier.contract_broken", "checkpoint.execution_outcome_unrecorded", "delegation.transcript_integrity", "delegation.ask_unresolvable", "mcp.injection_dropped", "mcp.revocation_probe_failed", "mcp.server_redialed", "workflow.governance_key_stripped", "workflow.agent_option_ignored", "memory.session_polluted", "memory.harvest_quarantined", "memory.delegation_static_mark_waived", "memory.content_class_declared", "memory.hold_opened", "memory.hold_released", "memory.hold_disposed", "memory.consolidation_recommended", "memory.consolidation_committed", "memory.consolidation_conflict", "memory.consolidation_incomplete", "memory.consolidation_refused", "memory.consolidation_withheld", "memory.restore_withheld", "memory.project_scope_absent", "route.fallback_to_primary", "route.classifier_unroutable", "route.base_url_changed_key_unchanged", "route.response_model_mismatch", "task.user_steer_undrained", "task.user_followup_undrained", "steering.parked_input_blocked", "task.turn_interrupted", "task.halt_unconsumed", "task.late_approval", "task.interrupt_unconsumed", "approval.read_root_granted", "approval.read_root_grant_rejected", "memory.capture_opted_out", "memory.capture_optout_unpersisted", "skills.listing_truncated", "agents.listing_truncated", "tool_result.offload_put_failed"];
123
123
  /** A code this engine mints (see {@link ENGINE_NOTICE_CODES}). NOT the type of
124
124
  * `EngineNotice.code`, which stays `string` — a host forwarding its own notices through the same
125
125
  * sink is a supported shape, and narrowing that field would break it. */
@@ -169,6 +169,8 @@ export const ENGINE_NOTICE_CODES = [
169
169
  "task.halt_unconsumed",
170
170
  "task.late_approval",
171
171
  "task.interrupt_unconsumed",
172
+ "approval.read_root_granted",
173
+ "approval.read_root_grant_rejected",
172
174
  "memory.capture_opted_out",
173
175
  "memory.capture_optout_unpersisted",
174
176
  "skills.listing_truncated",
@@ -193,6 +195,8 @@ const NOTICE_AUDIENCE_TABLE = {
193
195
  "task.halt_unconsumed": "user",
194
196
  "task.late_approval": "user",
195
197
  "task.interrupt_unconsumed": "user",
198
+ "approval.read_root_granted": "user",
199
+ "approval.read_root_grant_rejected": "user",
196
200
  "config.durable_gate_unavailable": "user",
197
201
  "memory.capture_opted_out": "user",
198
202
  "memory.capture_optout_unpersisted": "user",
@@ -1714,6 +1714,8 @@ export interface AskCarry {
1714
1714
  /** The stamped mandate ({@link import("./tool-policy.js").AskRequest.mandated}) — the park site's
1715
1715
  * rule-offer facts read it, so a parked mandated ask offers no rule, like the live card. */
1716
1716
  readonly mandated?: true;
1717
+ /** The table word stamped beside it ({@link import("./tool-policy.js").AskRequest.mandate}), for the row's twin. */
1718
+ readonly mandate?: import("./ask-origin.js").PersistedRuleMandate;
1717
1719
  }
1718
1720
  /** What a durable row persists of an {@link AskCarry}: the fallback member COPIED (the row never aliases
1719
1721
  * the gate's object; its window is already `0` — the gate's carry mint is the one writer of that
@@ -0,0 +1,23 @@
1
+ /**
2
+ * The resolver's reading of an approver's `readRootGrant` (`AskOutcome`) against the card it answered. It only
3
+ * READS: widening a run's read roots is that run's own station's act, and the resolver holds no run.
4
+ */
5
+ import type { ReadRootGrantCandidate } from "./types.js";
6
+ /** `granted` ⇔ an `allow` whose grant is byte-equal to the request's `readRootCandidate.dir` (the candidate rides
7
+ * back whole, `covers` included); otherwise `rejected` with ONE reason, the card's `offered` dir when it had one,
8
+ * and the `requested` grant when it was a string (verbatim host text — a notice bounds it). */
9
+ export type ReadRootGrantScreen = {
10
+ readonly verdict: "granted";
11
+ readonly candidate: ReadRootGrantCandidate;
12
+ } | {
13
+ readonly verdict: "rejected";
14
+ readonly reason: "on_refusal" | "not_a_string" | "no_candidate" | "mismatch";
15
+ readonly offered?: string;
16
+ readonly requested?: string;
17
+ };
18
+ /** Screen one grant read; `{}` when the answer carried none. */
19
+ export declare function readRootGrantSeat(grant: unknown, candidate: ReadRootGrantCandidate | undefined, allowed: boolean): {
20
+ readRootGrant?: ReadRootGrantScreen;
21
+ };
22
+ /** An engine-owned copy of a card's offer (one read per member), taken before an approver can touch the object. */
23
+ export declare function ownReadRootCandidate(c: ReadRootGrantCandidate | undefined): ReadRootGrantCandidate | undefined;
@@ -0,0 +1,17 @@
1
+ export function readRootGrantSeat(grant, candidate, allowed) {
2
+ if (grant === undefined)
3
+ return {};
4
+ const offered = candidate !== undefined ? { offered: candidate.dir } : {};
5
+ const requested = typeof grant === "string" ? { requested: grant } : {};
6
+ const reason = !allowed ? "on_refusal" : typeof grant !== "string" ? "not_a_string" : candidate === undefined ? "no_candidate" : grant !== candidate.dir ? "mismatch" : undefined;
7
+ if (reason !== undefined)
8
+ return { readRootGrant: { verdict: "rejected", reason, ...offered, ...requested } };
9
+ return { readRootGrant: { verdict: "granted", candidate: candidate } };
10
+ }
11
+ export function ownReadRootCandidate(c) {
12
+ if (c === undefined)
13
+ return undefined;
14
+ const dir = c.dir;
15
+ const covers = c.covers;
16
+ return typeof dir === "string" ? { dir, clearsThisAsk: true, ...(covers === "exact" ? { covers } : {}) } : undefined;
17
+ }
@@ -52,7 +52,7 @@ import type { WiringManifest } from "../wiring-manifest.js";
52
52
  import type { ActiveSkillFrame } from "./active-skill-scope.js";
53
53
  import type { BudgetAxis } from "./assemble-result.js";
54
54
  import type { GitStatusLaneRef } from "./git-status-frame.js";
55
- import type { SessionReadFileStates } from "./prepare-hands-readface.js";
55
+ import type { SessionReadFileStates } from "./session-read-file-states.js";
56
56
  import type { BlockedRef, OutputRef, SkillListingEntry } from "./synthetic-tools.js";
57
57
  import type { createAttachmentState, WriteFamily } from "./turn-attachments.js";
58
58
  import type { cacheFamilyOf } from "./usage-accounting.js";
@@ -6,11 +6,10 @@
6
6
  * seat uses. The tool's local-file read goes through the run's fs fence (root containment, read-deny, read
7
7
  * face) exactly as `Read` does, then reads the bytes once from the execution env. A local stub's store
8
8
  * (`capabilities.storageDir`) must lie OUTSIDE every root the run may write or read — a ledger the model can
9
- * edit through the fs tools is not a trust boundary — and a store inside one refuses the mount, naming both.
10
- * The store is a path on THIS process's filesystem; a root is the execution env's canonical key. The two are
11
- * compared as identities under one spelling family only when both spell the same family (`within` reads both
12
- * under the root's family); a root of the other family names a filesystem the store cannot be on, and no
13
- * containment claim is made against it.
9
+ * edit or read through the fs tools is not a trust boundary — judged by {@link rootAdmittingArtifactStore} at
10
+ * every point a root is admitted: here over the prepare-time roots (a store that overlaps one refuses the mount,
11
+ * naming both), and at an in-flight read-root grant over the granted root (the grant is refused). The phase
12
+ * publishes the declared store into the run's {@link ArtifactStoreRef} for that second point.
14
13
  * Synchronous: the stretch has no await (the file read is a closure the tool calls later).
15
14
  */
16
15
  import type { ExecutionEnv } from "../../internal/harness.js";
@@ -19,6 +18,7 @@ import { type ArtifactPlane } from "../artifact-host.js";
19
18
  import { RosterBuilder } from "../tool-roster.js";
20
19
  import type { AgentTool, RunnerDeps } from "../types.js";
21
20
  import type { ReadDenyMatcher, ContainmentFace } from "../../tools/fs/index.js";
21
+ import { type ArtifactStoreRef } from "./read-root-grant.js";
22
22
  import type { Prepared } from "./contracts.js";
23
23
  export interface PrepareArtifactInput {
24
24
  /** borrowed-readonly — the deployment's hosting seat and the notice sink the refusal arm announces on. */
@@ -38,8 +38,9 @@ export interface PrepareArtifactInput {
38
38
  executionEnv: ExecutionEnv;
39
39
  /** borrowed-readonly — the canonical task root the fs fence contains reads to; undefined ⇒ no local-file publish. */
40
40
  rootCanonical: string | undefined;
41
- /** borrowed-readonly — the extra containment roots (write and read-only) the fence admits. */
42
- additionalRootsCanonical: readonly string[];
41
+ /** borrowed-readonly — the extra containment roots (write and read-only) the fence admits, read per call (the run's
42
+ * live set: an in-flight read-root grant widens it). */
43
+ additionalRootsCanonical: () => readonly string[];
43
44
  /** borrowed-readonly — the read-deny matcher in force for the run, when any. */
44
45
  readDenyMatcher: ReadDenyMatcher | undefined;
45
46
  /** borrowed-readonly — the resolved read face (`"open"` lifts root containment for reads). */
@@ -52,6 +53,10 @@ export interface PrepareArtifactInput {
52
53
  firstPartyOffload: (tool: AgentTool) => AgentTool;
53
54
  /** borrowed-mutable — the env-hand membership set the memory content-origin face reads (a tool that reads the execution env's files is a hand); writer here: `add("Artifact")` on the mount. */
54
55
  envHandToolNames: Set<string>;
56
+ /** borrowed-mutable — the run's store cell the in-flight read-root grant reads (minted by the hands phase; absent on
57
+ * a hands-less leg, which has no grant); writer here: the declared `storageDir`'s physical path once the
58
+ * declaration validates. */
59
+ artifactStoreRef?: ArtifactStoreRef;
55
60
  }
56
61
  export interface PrepareArtifactResult {
57
62
  /** owned — the mounted face's plane, or undefined when nothing mounted (absent seat, or an invalid one). */
@@ -5,11 +5,12 @@ import { RosterBuilder } from "../tool-roster.js";
5
5
  import { defineTool } from "../tools.js";
6
6
  import { inlineUntrusted } from "../untrusted-text.js";
7
7
  import { physicalPathOfExisting } from "../physical-path.js";
8
- import { pathFamilyOf, resolveKey, violationDetails, violationText, withinAnyRoot } from "../../tools/fs/safety.js";
8
+ import { resolveKey, violationDetails, violationText } from "../../tools/fs/safety.js";
9
+ import { rootAdmittingArtifactStore } from "./read-root-grant.js";
9
10
  import { createArtifactTool } from "../../tools/artifact/artifact-tool.js";
10
11
  import { thrownText } from "../thrown-value.js";
11
12
  export function prepareArtifact(input) {
12
- const { deps, runId, taskId, sessionId, agentName, onAsk, executionEnv, rootCanonical, additionalRootsCanonical, readDenyMatcher, resolvedReadFace, handsCwdRef, roster, firstPartyOffload, envHandToolNames } = input;
13
+ const { deps, runId, taskId, sessionId, agentName, onAsk, executionEnv, rootCanonical, additionalRootsCanonical, readDenyMatcher, resolvedReadFace, handsCwdRef, roster, firstPartyOffload, envHandToolNames, artifactStoreRef } = input;
13
14
  const host = deps.artifactHost;
14
15
  if (host === undefined)
15
16
  return { artifactPlane: undefined };
@@ -25,12 +26,13 @@ export function prepareArtifact(input) {
25
26
  if (!verdict.ok)
26
27
  return refuse(verdict.problem);
27
28
  const storageDir = verdict.capabilities.storageDir;
28
- if (storageDir !== undefined && rootCanonical !== undefined) {
29
- const real = physicalPathOfExisting(storageDir);
30
- const storeFamily = pathFamilyOf({ root: real });
31
- const inside = [rootCanonical, ...additionalRootsCanonical].find((root) => pathFamilyOf({ root }) === storeFamily && withinAnyRoot([root], real));
32
- if (inside !== undefined)
33
- return refuse(`the local artifact store ${storageDir} lies inside a root this run may reach through its file tools (${inside}); put the store outside every task root and additional directory`);
29
+ const storePhysical = storageDir !== undefined && rootCanonical !== undefined ? physicalPathOfExisting(storageDir) : undefined;
30
+ if (artifactStoreRef !== undefined)
31
+ artifactStoreRef.current = storePhysical;
32
+ if (storePhysical !== undefined && rootCanonical !== undefined) {
33
+ const overlapping = rootAdmittingArtifactStore(storePhysical, [rootCanonical, ...additionalRootsCanonical()]);
34
+ if (overlapping !== undefined)
35
+ return refuse(`the local artifact store ${storageDir} and a root this run may reach through its file tools (${overlapping}) overlap — the store lies inside a root, or the root inside the store; put the store outside every task root and additional directory`);
34
36
  }
35
37
  try {
36
38
  host.onMount?.(runId);
@@ -26,6 +26,7 @@ import type { RunnerDeps, TaskSpec } from "../types.js";
26
26
  import type { CwdRef } from "../../tools/fs/fs-shared.js";
27
27
  import type { AskLane, Prepared } from "./contracts.js";
28
28
  import { consumeInheritedAskGrant } from "./inherited-ask-grants.js";
29
+ import { type GrantReadRoot } from "./read-root-grant.js";
29
30
  import type { createRuleOffersOf } from "./permission-rule-lanes.js";
30
31
  export interface PrepareAskLaneInput {
31
32
  /** borrowed-readonly — the wiring-manifest phase's ONE activation predicate; false ⇒ this phase builds nothing and
@@ -100,6 +101,8 @@ export interface PrepareAskLaneInput {
100
101
  hookTimeoutMs: number;
101
102
  /** borrowed-readonly — the own hook-crash reporter (the deny observer's swallow-guarded report sink). */
102
103
  notifyOwnHookCrash: (err: unknown) => void;
104
+ /** borrowed-readonly — the hands phase's in-flight read-root widening (an approver's accepted `readRootGrant`), or undefined (hands-less). */
105
+ grantReadRoot: GrantReadRoot | undefined;
103
106
  }
104
107
  export interface PrepareAskLaneResult {
105
108
  /** owned — the lane, or undefined exactly when `gateMachineryActive` is false (then the suspend saga, the boundary
@@ -5,6 +5,7 @@ import { composeCallSignal, raceAbort } from "./abort-race.js";
5
5
  import { askMandateMember } from "../ask-origin.js";
6
6
  import { gateAskCarry, lateAskSettlementObserver } from "./denial-limit-arms.js";
7
7
  import { consumeInheritedAskGrant } from "./inherited-ask-grants.js";
8
+ import { settleReadRootGrant } from "./read-root-grant.js";
8
9
  import { asThrownError } from "../thrown-value.js";
9
10
  function createDenyObserverNotifier(hooks, hookTimeoutMs, signal, report) {
10
11
  if (hooks?.permissionDenied === undefined)
@@ -57,7 +58,7 @@ function resolveApprovalPreview(tools, toolName, args) {
57
58
  }
58
59
  }
59
60
  export function prepareAskLane(input) {
60
- const { gateMachineryActive, abortController, effectivePolicy, budgetSnapshot, handsCwdRef, advertisedWritableDirs, tools, inheritedUnavailableAsks, inheritedAskGrants, onAsk, humanReviewRef, now, ruleOffersOf, askSourceIdentity, riskAxesOf, autoModeDenialTracking, spec, deps, sessionId, runId, hooks, hookTimeoutMs, notifyOwnHookCrash } = input;
61
+ const { gateMachineryActive, abortController, effectivePolicy, budgetSnapshot, handsCwdRef, advertisedWritableDirs, tools, inheritedUnavailableAsks, inheritedAskGrants, onAsk, humanReviewRef, now, ruleOffersOf, askSourceIdentity, riskAxesOf, autoModeDenialTracking, spec, deps, sessionId, runId, hooks, hookTimeoutMs, notifyOwnHookCrash, grantReadRoot } = input;
61
62
  if (!gateMachineryActive)
62
63
  return { askLane: undefined };
63
64
  const composedCallSignal = (callSignal) => composeCallSignal(abortController.signal, callSignal);
@@ -109,6 +110,9 @@ export function prepareAskLane(input) {
109
110
  ...(decision.action === "ask" && decision.ruleEvidence !== undefined ? { ruleEvidence: decision.ruleEvidence } : {}),
110
111
  }, onAsk, composedCallSignal(callSignal), lateAskSettlementObserver({ toolName: req.toolName, toolCallId: req.toolCallId, sessionId, runId, ...(spec.taskId !== undefined ? { taskId: spec.taskId } : {}), onNotice: deps.onNotice, onError: deps.onError }));
111
112
  const waitMs = Math.max(0, now() - t0);
113
+ const granting = settleReadRootGrant(resolved.readRootGrant, grantReadRoot, { toolName: req.toolName, toolCallId: req.toolCallId, sessionId, runId, ...(spec.taskId !== undefined ? { taskId: spec.taskId } : {}), onNotice: deps.onNotice, signal: composedCallSignal(callSignal) });
114
+ if (granting !== undefined)
115
+ await granting;
112
116
  if (resolved.approverUnavailable !== true && resolved.settlement?.kind !== "task_aborted") {
113
117
  humanReviewRef.count += 1;
114
118
  humanReviewRef.totalWaitMs += waitMs;
@@ -1,4 +1,4 @@
1
- import { hasBackgroundShell, sweepBackgroundShells } from "../background-shell.js";
1
+ import { hasBackgroundShell, sweepBackgroundShells, sweepRunBoundaryBackgroundShells } from "../background-shell.js";
2
2
  import { checkpointVersionFor, debitLedger, mintCheckpointId, mintCheckpointToken, } from "../checkpoint-store.js";
3
3
  import { defaultTaskRegistry } from "../task-registry.js";
4
4
  import { checkpointScopeOf } from "./checkpoint-scope.js";
@@ -36,8 +36,8 @@ export function prepareBoundaryParks(input) {
36
36
  remoteHandle = { ...suspendableEnv.workspaceHandle(), snapshotId: snap.value };
37
37
  }
38
38
  else if (parkOnlyRemoteEnv !== undefined) {
39
- if (hasBackgroundShell(parkOnlyRemoteEnv) && spec.retainBackgroundProcesses !== true)
40
- await sweepBackgroundShells(parkOnlyRemoteEnv, defaultTaskRegistry);
39
+ if (hasBackgroundShell(parkOnlyRemoteEnv))
40
+ await sweepRunBoundaryBackgroundShells(parkOnlyRemoteEnv, defaultTaskRegistry, { task: spec.retainBackgroundProcesses === true });
41
41
  remoteHandle = parkOnlyHandle(parkOnlyRemoteEnv);
42
42
  }
43
43
  const token = mintCheckpointToken();
@@ -119,8 +119,8 @@ export function prepareBoundaryParks(input) {
119
119
  remoteHandle = { ...suspendableEnv.workspaceHandle(), snapshotId: snap.value };
120
120
  }
121
121
  else if (parkOnlyRemoteEnv !== undefined) {
122
- if (hasBackgroundShell(parkOnlyRemoteEnv) && spec.retainBackgroundProcesses !== true)
123
- await sweepBackgroundShells(parkOnlyRemoteEnv, defaultTaskRegistry);
122
+ if (hasBackgroundShell(parkOnlyRemoteEnv))
123
+ await sweepRunBoundaryBackgroundShells(parkOnlyRemoteEnv, defaultTaskRegistry, { task: spec.retainBackgroundProcesses === true });
124
124
  remoteHandle = parkOnlyHandle(parkOnlyRemoteEnv);
125
125
  }
126
126
  const token = mintCheckpointToken();
@@ -1,3 +1,21 @@
1
+ /**
2
+ * PrepareTask's DELEGATION-SURFACE phase, verbatim from the driver: the surface's two doors
3
+ * (the background/workflow door the read/kill faces keep; the delegated-child arm SendMessage additionally mounts
4
+ * on), the TaskOutput / TaskStop mounts, the SendMessage mount with its tier-3 revival spawner and peer seats, the
5
+ * AgentTranscript residual lane, the peer lane's ListAgents, the Monitor tool, the EnterWorktree / ExitWorktree pair
6
+ * with the worktree session seat it seeds from the checkpoint or the workspace snapshot, and the offload reader. The
7
+ * revival spawner and the continuation tier it yields are machinery below (send-message-continuation.ts). The
8
+ * interface is the dependency list the segment had implicitly.
9
+ *
10
+ * SYNCHRONOUS FUNCTION, SYNCHRONOUS CALL: the stretch has no await, so the phase adds no yield between the hands
11
+ * mount before it and the question-face phase after it (a stretch with no await is extracted as
12
+ * a sync function). Read-stability of the host handles for the call: {@link RunInternals}
13
+ * (`@contract prepare.deps-read-stable`).
14
+ *
15
+ * The roster (`tools`) is borrowed-mutable here — the writer table lives on {@link Prepared.tools};
16
+ * this phase's rows are the eight mounts named above. `childPolicy` is NOT a seat yet: the
17
+ * delegation carriers still ride the enriched ctx's individual clamp fields, as they did in the driver.
18
+ */
1
19
  import type { AgentTool, ExecutionEnv, WorkspaceState } from "../../internal/harness.js";
2
20
  import { type PeerLaneRefs } from "../../agents/peer-session-drain.js";
3
21
  import type { SubagentRetainLedger } from "../../agents/retain-ledger.js";
@@ -1,4 +1,3 @@
1
- import { randomBytes } from "node:crypto";
2
1
  import { createAgentTranscriptTool, AGENT_TRANSCRIPT_TOOL_NAME } from "../../agents/agent-transcript-tool.js";
3
2
  import { createSendMessageTool, SEND_MESSAGE_TOOL_NAME } from "../../agents/send-message-tool.js";
4
3
  import { listAgentsMountable, mountListAgents, peerLaneSendMessageSeats } from "../../agents/peer-session-drain.js";
@@ -11,30 +10,12 @@ import { createReadToolResultTool } from "../tool-result-store.js";
11
10
  import { emitTrace } from "../trace.js";
12
11
  import { deliverEngineNotice } from "../types.js";
13
12
  import { RosterBuilder } from "../tool-roster.js";
14
- function buildTier3ReviveSpawn(delegationForRevive, enrichSpecToolCtx) {
15
- if (delegationForRevive === undefined)
16
- return undefined;
17
- return async (req) => {
18
- const out = await delegationForRevive.execute({
19
- description: req.row.description ?? "revived teammate",
20
- prompt: req.prompt,
21
- run_in_background: true,
22
- ...(req.row.agentType !== undefined ? { subagent_type: req.row.agentType } : {}),
23
- ...(req.row.name !== undefined ? { name: req.row.name } : {}),
24
- ...(req.row.model !== undefined ? { model: req.row.model } : {}),
25
- }, enrichSpecToolCtx({ toolCallId: `rv-${randomBytes(8).toString("hex")}`, reviveClaim: { row: req.row, rev: req.rev, ...(req.peerSeed !== undefined ? { peerSeed: req.peerSeed } : {}) } }));
26
- const o = (typeof out === "string" ? { content: out } : out);
27
- return {
28
- content: typeof o.content === "string" ? o.content : JSON.stringify(o.content),
29
- ...(o.isError === true ? { isError: true } : {}),
30
- ...(o.details !== undefined ? { details: o.details } : {}),
31
- };
32
- };
33
- }
13
+ import { sendMessageContinuationSeats } from "./send-message-continuation.js";
34
14
  export function prepareDelegationSurface(input) {
35
15
  const { spec, deps, internals, resume, sessionId, hostTaskId, taskScope, taskRootFinal, taskRootCanonical, executionEnv, runnerSelf, backgroundTaskToolsActive, workflowToolsActive, peerLaneActive, peerSendMessageBuiltIn, peerLaneRefs, sessionBox, roster, toolEffects, axisExplicitNegatives, envHandToolNames, toolFaceSnapshot, firstPartyOffload, offloadStore, subagentRetain, enrichSpecToolCtx, handsCwdRef, wsSnapshot, rebaseWsPath, rebaseRestoredPath, workspaceStateSettle } = input;
36
16
  let worktreeSessionRef;
37
17
  const delegationSurfaceActive = backgroundTaskToolsActive || workflowToolsActive;
18
+ let sendMessageTier;
38
19
  if (delegationSurfaceActive || internals?.parentNotify !== undefined || peerLaneActive) {
39
20
  if (delegationSurfaceActive) {
40
21
  roster.mountAll([
@@ -44,8 +25,8 @@ export function prepareDelegationSurface(input) {
44
25
  }
45
26
  if (runnerSelf && !(spec.tools ?? []).some((t) => t.name === SEND_MESSAGE_TOOL_NAME)) {
46
27
  axisExplicitNegatives.set(SEND_MESSAGE_TOOL_NAME, { ...axisExplicitNegatives.get(SEND_MESSAGE_TOOL_NAME), egress: false });
47
- const delegationForRevive = (spec.tools ?? []).find((t) => t.agentListing !== undefined);
48
- const reviveSpawn = deps.backgroundAgentStore !== undefined && deps.mailboxStore !== undefined ? buildTier3ReviveSpawn(delegationForRevive, enrichSpecToolCtx) : undefined;
28
+ const { reviveSpawn, tier } = sendMessageContinuationSeats({ specTools: spec.tools ?? [], agentStore: deps.backgroundAgentStore, mailbox: deps.mailboxStore, runner: runnerSelf, enrichSpecToolCtx });
29
+ sendMessageTier = tier;
49
30
  roster.mount(firstPartyOffload(createSendMessageTool({
50
31
  runner: runnerSelf,
51
32
  registry: defaultTaskRegistry,
@@ -103,7 +84,7 @@ export function prepareDelegationSurface(input) {
103
84
  toolEffects.set(READ_INBOX_TOOL_NAME, "read");
104
85
  roster.mount(firstPartyOffload(createReadInboxTool({ pull: () => peerLaneRefs.pull })), { source: "builtin", mountedBy: "peerLane" });
105
86
  }
106
- const listAgents = (peerLaneActive || delegationSurfaceActive) && peerSendMessageBuiltIn && listAgentsMountable({ exclude: toolFaceSnapshot.exclude, specTools: spec.tools ?? [] }) ? mountListAgents({ ...(peerLaneActive ? { peerDirectory: deps.peerDirectory } : {}), freshWindowMs: resolvePeerLaneConfig(deps.peerLane, deps.onNotice).listingFreshWindowMs, sessionId, scope: taskScope, hostTaskId, ...(internals?.parentTaskId !== undefined ? { parentTaskId: internals.parentTaskId } : {}), ...(internals?.parentSessionId !== undefined ? { parentSessionId: internals.parentSessionId } : {}), registry: defaultTaskRegistry, ...(deps.rosterStore !== undefined ? { roster: deps.rosterStore } : {}), specTools: spec.tools ?? [], toolEffects }) : undefined;
87
+ const listAgents = (peerLaneActive || delegationSurfaceActive) && peerSendMessageBuiltIn && listAgentsMountable({ exclude: toolFaceSnapshot.exclude, specTools: spec.tools ?? [] }) ? mountListAgents({ ...(peerLaneActive ? { peerDirectory: deps.peerDirectory } : {}), freshWindowMs: resolvePeerLaneConfig(deps.peerLane, deps.onNotice).listingFreshWindowMs, sessionId, scope: taskScope, hostTaskId, ...(internals?.parentTaskId !== undefined ? { parentTaskId: internals.parentTaskId } : {}), ...(internals?.parentSessionId !== undefined ? { parentSessionId: internals.parentSessionId } : {}), registry: defaultTaskRegistry, ...(deps.rosterStore !== undefined ? { roster: deps.rosterStore } : {}), ...(sendMessageTier !== undefined ? { continuationTier: sendMessageTier } : {}), specTools: spec.tools ?? [], toolEffects }) : undefined;
107
88
  if (listAgents !== undefined)
108
89
  roster.mount(firstPartyOffload(listAgents), { source: "builtin", mountedBy: "peerLane" });
109
90
  if (backgroundTaskToolsActive) {
@@ -118,7 +99,7 @@ export function prepareDelegationSurface(input) {
118
99
  ...(handsCwdRef !== undefined ? { cwdRef: handsCwdRef } : {}),
119
100
  rootCanonical: taskRootCanonical,
120
101
  ...(offloadStore !== undefined ? { toolResultStore: offloadStore } : {}),
121
- ...(spec.retainBackgroundProcesses === true ? { retainBackgroundProcesses: true } : {}),
102
+ retainBackgroundProcesses: spec.retainBackgroundProcesses === true,
122
103
  })), { source: "builtin", mountedBy: "backgroundTasks" });
123
104
  }
124
105
  if (handsCwdRef !== undefined) {
@@ -5,6 +5,7 @@ import type { RunnerDeps, ShellGateDoctrine, TaskSpec } from "../types.js";
5
5
  import type { Model } from "../../internal/llm.js";
6
6
  import type { AgentHarness, AgentTool, ExecutionEnv, WorkspaceState } from "../../internal/harness.js";
7
7
  import type { AdvertisedWritableDirs } from "./advertised-writable-dirs.js";
8
+ import { type ArtifactStoreRef, type GrantReadRoot } from "./read-root-grant.js";
8
9
  import type { StoredSession } from "../session.js";
9
10
  import type { IrreversibilityTier, PrepareResume, ReversibilityProbes, RunInternals, ToolFaceSnapshot, ToolProgressLane } from "./contracts.js";
10
11
  import { RosterBuilder } from "../tool-roster.js";
@@ -14,91 +15,6 @@ import { RosterBuilder } from "../tool-roster.js";
14
15
  * the console arm's key is the process itself, which only this seam can refresh. Moved WITH the
15
16
  * phase; prepare-task.ts keeps a re-export so the existing import sites stand. */
16
17
  export declare function __resetReadFaceClampAnnouncement(): void;
17
- /**
18
- * The Runner's per-session read-file-state seats (CC parity: readFileState is minted
19
- * once per REPL session and mutated across turns; it has NO turn-boundary and NO abort clear site —
20
- * its only clears are compaction/fork/subagent-cleanup/dispose). sema's map stays per-task (the §11
21
- * ruling-A closure wiring), so the SESSION scope lives here instead: prepare adopts each task's live
22
- * map under its sessionId, and the next same-session, same-Runner task INHERITS it (through the same
23
- * containment judgment the transcript-replay seed applies — see `inheritSessionReadFileState`).
24
- *
25
- * This seat is a CACHE, never an authority:
26
- * - a durable resume's checkpoint seed stays the sole authority on that leg (the seed was cut
27
- * atomically with the leaf; this seat may be older OR newer than it);
28
- * - a cross-process continuation misses here and falls back to the transcript replay (rung-2);
29
- * - eviction (the LRU cap, a Runner restart) degrades to that same replay — never to wrong state.
30
- *
31
- * Bounded: an LRU over sessionIds (insertion-order refresh on read), because the SessionStore's own
32
- * eviction is not observable from here — a dropped seat costs one transcript replay, nothing else.
33
- * The stored value is the task's LIVE map (object identity), so in-turn mutations — tool write-backs,
34
- * and above all the compaction clear — are the seat's truth by construction; a snapshot copy
35
- * here would resurrect summarized-away reads on the next turn.
36
- *
37
- * BRANCH BINDING: a sessionId alone does not name a transcript — the same id
38
- * can be rebranched (`resumeAt`), rewound out-of-band, or DELETED and re-created (`release()` + the
39
- * default create-on-miss acquire), and the seat's map would then vouch for reads the CURRENT branch
40
- * never saw. Each seat therefore records `anchorLeafId` — a leaf on the lineage its task produced —
41
- * and inheritance must first prove the CURRENT branch still descends from that anchor
42
- * (see {@link sessionSeatAnchorHolds}). The anchor is written twice: at ADOPT (the leaf at prepare;
43
- * `null` when the branch was still empty) and SEALED at the task's settle to the task's own tail
44
- * (`reanchor`). A `null` anchor is NEVER inheritable ("the branch is non-empty now" proves
45
- * nothing about WHOSE messages those are — another Runner repopulating a released id, or a session
46
- * import, makes a non-empty branch that never contained this seat's reads). An unprovable anchor
47
- * falls back to the transcript replay, which reads the current branch by construction — cost, never
48
- * wrongness.
49
- */
50
- export declare class SessionReadFileStates {
51
- private readonly seats;
52
- private static readonly CAP;
53
- /** The session's seat row, or undefined (cross-process / evicted / first turn) ⇒ caller replays.
54
- * The caller must verify the anchor against the session's CURRENT branch before using `state`. */
55
- get(sessionId: string): {
56
- state: ReadFileState;
57
- anchorLeafId: string | null;
58
- } | undefined;
59
- /** Adopt THIS task's live map as the session's seat (every hands leg: fresh, continuation, resume).
60
- * `anchorLeafId` = the session's leaf as read at THIS prepare (null = the branch was empty; a
61
- * null-anchored seat is not inheritable until the settle-time {@link reanchor} names a real leaf). */
62
- adopt(sessionId: string, state: ReadFileState, anchorLeafId: string | null): void;
63
- /** The settle-time SEAL: re-anchor the seat to the finished task's own tail, so a
64
- * first-task (null-anchor) seat becomes inheritable only through a leaf its task provably wrote.
65
- * Identity-guarded: touches the row only while it still holds THIS task's map (a later prepare on
66
- * the same session has already replaced the seat and owns its own anchor). */
67
- reanchor(sessionId: string, state: ReadFileState, anchorLeafId: string): void;
68
- }
69
- /**
70
- * Does the session's CURRENT branch still descend from the seat's anchor?
71
- * TRUE is the licence to inherit; anything unprovable answers FALSE (the caller then replays the
72
- * transcript — the fail-closed direction costs a rebuild, never admits a wrong entry).
73
- *
74
- * - anchor `null`: NEVER inheritable (r2). The seat was adopted on an empty branch and its task
75
- * never sealed (crashed pre-settle, or the leaf read failed) — "the branch is non-empty now"
76
- * would also be true of a released-and-repopulated id or an imported log, whose messages are not
77
- * this seat's lineage. The settle-time reanchor is what upgrades such a seat to a real leaf.
78
- * - anchor non-null: holds iff walking parentIds from the current leaf reaches the anchor within
79
- * a bounded number of steps (the walk length is one task's appends in the ordinary continuation).
80
- * A rebranch/rewind moved the leaf OFF the anchor's subtree ⇒ the walk misses ⇒ no inherit; a
81
- * recreated/foreign log does not contain the anchor ⇒ same answer; a re-IMPORT of the same log
82
- * keeps the anchor's lineage ⇒ inheritance stays correct there by the same test.
83
- * - a COMPACTION on the walked segment (A-082 idx2): refuses, even though the branch is perfectly
84
- * intact. The walked segment is exactly what landed AFTER this seat was anchored, and a
85
- * compaction there summarized older Read results OUT of the context the model will be shown —
86
- * the very state the compaction clear removes from the live map at the landing site. That clear reaches the seat
87
- * only when the SAME Runner did both (the seat IS that map, by object identity); a second Runner
88
- * on the shared durable session clears its own map and commits the compaction as an ORDINARY
89
- * LINEAR APPEND, which every other test here passes. Inheriting across it would let requireRead
90
- * and checkStale both pass on a read whose card the model no longer has — a blind edit from a
91
- * summarized-away recollection, the exact failure the compaction clear exists to prevent. Refusing
92
- * costs one transcript replay, and the replay reads the POST-compaction branch by construction,
93
- * so the fail-closed direction is also the CORRECT state. A compaction at or ABOVE the anchor is
94
- * not on the walked segment and does not refuse: the seat that observed it already carries its
95
- * clear (P-483-4), and an ordinary foreign turn with no compaction still inherits (G-483-5) —
96
- * dropping every seat a foreign Runner touched would restore the cross-turn read-state amnesia this seat-anchor check already ended.
97
- */
98
- export declare function sessionSeatAnchorHolds(getEntry: (id: string) => Promise<{
99
- parentId: string | null;
100
- type?: string;
101
- } | null | undefined>, anchorLeafId: string | null, currentLeafId: string | null): Promise<boolean>;
102
18
  export interface PrepareHandsReadFaceInput {
103
19
  /** owned (value) — `ownedEnv !== undefined || deps.executionEnv !== undefined`, computed in the
104
20
  * driver: the phase's own branch condition (hands mount vs the hands-less resolver arm). */
@@ -149,8 +65,9 @@ export interface PrepareHandsReadFaceInput {
149
65
  * (the Read tool's document-degradation fork). */
150
66
  model: Model;
151
67
  /** borrowed-readonly — the task-spec seats this phase reads, as a Pick over the SAME spec object
152
- * (R-4). `tools` feeds only the band shadow check; `envFacts` only the advertised scratchpad. */
153
- spec: Pick<TaskSpec, "additionalDirectories" | "additionalReadDirectories" | "envFacts" | "handsReadOnly" | "readDenyPatterns" | "readFace" | "writeFace" | "oneShot" | "resumeAt" | "sessionId" | "tools">;
68
+ * (R-4). `tools` feeds only the band shadow check; `envFacts` only the advertised scratchpad;
69
+ * `retainBackgroundProcesses` only the band's retain lane (Bash's lifetime sentence + row stamp). */
70
+ spec: Pick<TaskSpec, "additionalDirectories" | "additionalReadDirectories" | "envFacts" | "handsReadOnly" | "readDenyPatterns" | "readFace" | "writeFace" | "oneShot" | "resumeAt" | "retainBackgroundProcesses" | "sessionId" | "tools">;
154
71
  /** borrowed-readonly — the deployment seats this phase reads, as a Pick over the SAME deps
155
72
  * object (R-4: `deps.onError?.()` keeps its receiver; a new deps read must widen this Pick).
156
73
  * `readDenyBuiltinTiers`/`readDenyBuiltinExclude` are SNAPSHOTTED before the first await that
@@ -302,8 +219,14 @@ export interface PrepareHandsReadFaceResult {
302
219
  * ARRAY identity. Settled here (no mutation after return). */
303
220
  additionalRootsCanonical: string[];
304
221
  /** owned — the read-only arm of the fence widening (2026-08-05 both-arms ruling); same identity
305
- * contract as above. */
222
+ * contract as above, and the run's LIVE read-root set: {@link grantReadRoot} is its one writer after prepare,
223
+ * and every read face reads it per call (never a copy). */
306
224
  additionalReadRootsCanonical: string[];
225
+ /** owned — widen {@link additionalReadRootsCanonical} by one granted candidate (an approver's in-flight
226
+ * `readRootGrant`); undefined ⇔ hands-less (no read face to widen). */
227
+ grantReadRoot: GrantReadRoot | undefined;
228
+ /** owned — the store cell {@link grantReadRoot} judges against; the driver threads it to its one writer, the Artifact phase. */
229
+ artifactStoreRef: ArtifactStoreRef | undefined;
307
230
  /** owned — the canonical containment root, published so the working-file ATTACHMENT
308
231
  * reader applies the same containment the hands tools do. Undefined ⇔ hands-less. */
309
232
  attachmentRootCanonical: string | undefined;
@@ -326,7 +249,7 @@ export interface PrepareHandsReadFaceResult {
326
249
  * a hands-less leg (nothing mounts on the env; the driver's env mounts are hands-gated too). */
327
250
  envHandToolNames: Set<string>;
328
251
  /** owned — the settle-time SEAT SEAL: re-anchors the session seat to the
329
- * task's OWN last-committed entry (see {@link SessionReadFileStates.reanchor}). The driver calls
252
+ * task's OWN last-committed entry (see {@link import("./session-read-file-states.js").SessionReadFileStates.reanchor}). The driver calls
330
253
  * it once at the run's settle (every terminal, suspend included), under the session lock, passing
331
254
  * the last entryId ITS OWN run committed (the message_committed lane) — never a re-read of the
332
255
  * shared session leaf, which a concurrent writer on another Runner could have moved onto a branch