@sema-agent/core 7.49.5 → 7.50.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +138 -0
- package/dist/agents/observer.js +3 -10
- package/dist/agents/subagent-steps.d.ts +6 -14
- package/dist/agents/subagent-steps.js +3 -17
- package/dist/brain/stream-shared.d.ts +1 -1
- package/dist/core/allow-entry.d.ts +1 -1
- package/dist/core/allow-entry.js +1 -1
- package/dist/core/ask-origin.d.ts +15 -11
- package/dist/core/ask-origin.js +6 -14
- package/dist/core/auto-mode-prompt.js +6 -2
- package/dist/core/checkpoint-store.d.ts +6 -4
- package/dist/core/checkpoint-store.js +1 -1
- package/dist/core/context-edit.d.ts +2 -2
- package/dist/core/decision-receipt.d.ts +2 -2
- package/dist/core/decision-receipt.js +30 -11
- package/dist/core/delegation-frames.d.ts +66 -18
- package/dist/core/delegation-frames.js +22 -0
- package/dist/core/engine-notice.d.ts +7 -5
- package/dist/core/frame-settlement.d.ts +6 -1
- package/dist/core/gate-fold.js +19 -13
- package/dist/core/gate-lanes.js +12 -11
- package/dist/core/gate-outcome.d.ts +2 -2
- package/dist/core/governance-codes.js +2 -0
- package/dist/core/hooks.d.ts +28 -20
- package/dist/core/human-input-projection.js +2 -0
- package/dist/core/lsp-diagnostics.d.ts +3 -0
- package/dist/core/lsp-diagnostics.js +6 -0
- package/dist/core/lsp-session.d.ts +39 -17
- package/dist/core/lsp-session.js +118 -58
- package/dist/core/lsp.d.ts +83 -3
- package/dist/core/lsp.js +146 -16
- package/dist/core/mcp.js +2 -1
- package/dist/core/media-byte-cap.d.ts +14 -0
- package/dist/core/media-byte-cap.js +15 -3
- package/dist/core/memory-engine/consolidation-driver.d.ts +23 -1
- package/dist/core/memory-engine/consolidation-driver.js +54 -6
- package/dist/core/memory-engine/consolidation-write-gate.d.ts +46 -0
- package/dist/core/memory-engine/consolidation-write-gate.js +82 -0
- package/dist/core/memory-engine/consolidation.d.ts +17 -0
- package/dist/core/memory-engine/consolidation.js +1 -0
- package/dist/core/memory-engine/distiller.d.ts +8 -2
- package/dist/core/memory-engine/distiller.js +8 -6
- package/dist/core/memory-engine/engine.d.ts +37 -4
- package/dist/core/memory-engine/engine.js +127 -38
- package/dist/core/memory-engine/file-backend.d.ts +8 -3
- package/dist/core/memory-engine/file-backend.js +44 -16
- package/dist/core/memory-engine/index.d.ts +2 -2
- package/dist/core/memory-engine/index.js +1 -1
- package/dist/core/memory-engine/landing-admission.d.ts +65 -0
- package/dist/core/memory-engine/landing-admission.js +68 -0
- package/dist/core/memory-engine/types.d.ts +23 -0
- package/dist/core/mode-policy.d.ts +3 -2
- package/dist/core/mode-policy.js +2 -0
- package/dist/core/permission-rule-model.d.ts +19 -13
- package/dist/core/permission-rule-model.js +7 -4
- package/dist/core/permission-rule-provider.d.ts +11 -0
- package/dist/core/permission-rules.d.ts +1 -1
- package/dist/core/permission-rules.js +5 -2
- package/dist/core/persisted-rule-arms.d.ts +15 -7
- package/dist/core/persisted-rule-arms.js +13 -6
- package/dist/core/reminder-mint.d.ts +5 -0
- package/dist/core/reminder-mint.js +1 -0
- package/dist/core/retired-keys.d.ts +8 -0
- package/dist/core/retired-keys.js +4 -0
- package/dist/core/rule-lane-reads.d.ts +4 -4
- package/dist/core/rule-lane-reads.js +10 -1
- package/dist/core/runner/construction-doors.d.ts +10 -4
- package/dist/core/runner/construction-doors.js +25 -2
- package/dist/core/runner/contracts.d.ts +20 -3
- package/dist/core/runner/env-capability-census.js +1 -1
- package/dist/core/runner/gate-exit.js +2 -1
- package/dist/core/runner/memory-consolidation-driver.d.ts +6 -4
- package/dist/core/runner/memory-consolidation-driver.js +5 -2
- package/dist/core/runner/memory-consolidation-write-judge.d.ts +17 -0
- package/dist/core/runner/memory-consolidation-write-judge.js +14 -0
- package/dist/core/runner/permission-rule-lanes.d.ts +16 -22
- package/dist/core/runner/permission-rule-lanes.js +13 -43
- package/dist/core/runner/person-path-rows.d.ts +244 -0
- package/dist/core/runner/person-path-rows.js +387 -0
- package/dist/core/runner/prepare-acquire-reconcile.js +8 -1
- package/dist/core/runner/prepare-artifact.d.ts +5 -3
- package/dist/core/runner/prepare-artifact.js +2 -8
- package/dist/core/runner/prepare-caps-and-workflow.d.ts +4 -3
- package/dist/core/runner/prepare-caps-and-workflow.js +1 -1
- package/dist/core/runner/prepare-context-lane.js +2 -2
- package/dist/core/runner/prepare-gate-stations.d.ts +3 -3
- package/dist/core/runner/prepare-gate-stations.js +1 -1
- package/dist/core/runner/prepare-hands-readface.d.ts +19 -10
- package/dist/core/runner/prepare-hands-readface.js +57 -43
- package/dist/core/runner/prepare-lsp.d.ts +15 -13
- package/dist/core/runner/prepare-lsp.js +11 -11
- package/dist/core/runner/prepare-policy-chain.d.ts +8 -4
- package/dist/core/runner/prepare-policy-chain.js +6 -7
- package/dist/core/runner/prepare-run-refs.js +1 -1
- package/dist/core/runner/prepare-task.js +24 -16
- package/dist/core/runner/prepare-turn-wiring.d.ts +13 -6
- package/dist/core/runner/prepare-turn-wiring.js +19 -8
- package/dist/core/runner/read-root-grant.d.ts +1 -25
- package/dist/core/runner/read-root-grant.js +1 -17
- package/dist/core/runner/retired-runner-deps.d.ts +2 -2
- package/dist/core/runner/run-brain-sinks.d.ts +1 -5
- package/dist/core/runner/run-brain-sinks.js +2 -4
- package/dist/core/runner/run-harness-handlers.js +5 -4
- package/dist/core/runner/run-identity-wiring.d.ts +4 -4
- package/dist/core/runner/run-identity-wiring.js +10 -1
- package/dist/core/runner/run-leg.d.ts +1 -1
- package/dist/core/runner/run-leg.js +22 -9
- package/dist/core/runner/run-settle-and-teardown.d.ts +0 -4
- package/dist/core/runner/run-settle-and-teardown.js +7 -4
- package/dist/core/runner/run-terminal-adoption.d.ts +1 -1
- package/dist/core/runner/run-terminal-adoption.js +1 -1
- package/dist/core/runner/run-turn-boundary.js +2 -2
- package/dist/core/runner/runtask.js +8 -8
- package/dist/core/runner/session-file-state-replay.d.ts +14 -4
- package/dist/core/runner/session-file-state-replay.js +8 -0
- package/dist/core/runner/stream-steer-verb.js +3 -0
- package/dist/core/runner/tool-end-body.d.ts +5 -4
- package/dist/core/runner/tool-end-body.js +4 -4
- package/dist/core/runner/tool-output-projection.d.ts +6 -4
- package/dist/core/runner/tool-output-projection.js +66 -47
- package/dist/core/runner-deps.d.ts +6 -3
- package/dist/core/shared-memory/types.d.ts +2 -2
- package/dist/core/shell-read-silence.d.ts +23 -0
- package/dist/core/shell-read-silence.js +18 -0
- package/dist/core/synthetic-read-verdict.d.ts +80 -21
- package/dist/core/synthetic-read-verdict.js +89 -18
- package/dist/core/task-event.d.ts +113 -25
- package/dist/core/task-spec.d.ts +44 -0
- package/dist/core/task-tool-shape.d.ts +1 -1
- package/dist/core/tool-catalog-entries.js +16 -2
- package/dist/core/tool-policy.d.ts +25 -15
- package/dist/core/tool-policy.js +9 -13
- package/dist/core/trace.d.ts +3 -2
- package/dist/core/types.d.ts +3 -3
- package/dist/core/types.js +1 -1
- package/dist/core/untrusted-text.d.ts +2 -2
- package/dist/core/user-attachment.d.ts +164 -0
- package/dist/core/user-attachment.js +437 -0
- package/dist/engine/compaction/compaction.js +10 -3
- package/dist/engine/compaction/utils.js +6 -0
- package/dist/engine/execution-env/node-execution-env.d.ts +19 -1
- package/dist/engine/execution-env/node-execution-env.js +125 -0
- package/dist/engine/harness/agent-harness.d.ts +12 -2
- package/dist/engine/harness/agent-harness.js +15 -3
- package/dist/engine/harness/types.d.ts +21 -0
- package/dist/engine/llm/content-block-render.d.ts +15 -0
- package/dist/engine/llm/content-block-render.js +10 -0
- package/dist/engine/llm/index.d.ts +1 -0
- package/dist/engine/llm/index.js +1 -0
- package/dist/engine/llm/types.d.ts +80 -4
- package/dist/engine/llm/types.js +43 -1
- package/dist/engine/session/import-validate.js +2 -0
- package/dist/index.d.ts +6 -4
- package/dist/index.js +2 -1
- package/dist/internal/harness-types.d.ts +1 -1
- package/dist/internal/llm.d.ts +2 -2
- package/dist/internal/llm.js +1 -1
- package/dist/orchestration/workflow.d.ts +4 -5
- package/dist/orchestration/workflow.js +2 -2
- package/dist/prompts/default.d.ts +1 -1
- package/dist/tools/artifact/artifact-tool.js +1 -1
- package/dist/tools/fs/bash-readonly-classifier.d.ts +1 -1
- package/dist/tools/fs/dir-entries.d.ts +4 -10
- package/dist/tools/fs/dir-entries.js +4 -11
- package/dist/tools/fs/file-read-check.js +3 -3
- package/dist/tools/fs/fs-pdf.js +2 -4
- package/dist/tools/fs/fs-read.d.ts +47 -4
- package/dist/tools/fs/fs-read.js +63 -19
- package/dist/tools/fs/fs-search-tools.d.ts +3 -3
- package/dist/tools/fs/fs-search-tools.js +7 -2
- package/dist/tools/fs/fs-write.d.ts +1 -1
- package/dist/tools/fs/fs-write.js +7 -6
- package/dist/tools/fs/index.d.ts +7 -1
- package/dist/tools/fs/index.js +56 -34
- package/dist/tools/fs/read-deny-home.d.ts +19 -1
- package/dist/tools/fs/read-deny-home.js +32 -22
- package/dist/tools/fs/read-deny.d.ts +14 -0
- package/dist/tools/fs/read-deny.js +8 -0
- package/dist/tools/fs/repo-map.d.ts +3 -2
- package/dist/tools/fs/repo-map.js +11 -3
- package/dist/tools/fs/safety.d.ts +41 -2
- package/dist/tools/fs/safety.js +12 -3
- package/dist/tools/fs/shell-read-check.d.ts +12 -3
- package/dist/tools/fs/shell-read-check.js +28 -15
- package/dist/tools/fs/shell-read-device.d.ts +1 -1
- package/dist/tools/fs/shell-read-judge.d.ts +9 -4
- package/dist/tools/fs/shell-read-judge.js +1 -1
- package/dist/tools/loop-tick.d.ts +2 -2
- package/package.json +3 -1
- package/test/export-surface.snapshot.json +39 -1
- package/wire-changes/core@7.50.0.json +1699 -0
- package/dist/core/runner/persisted-path-rows.d.ts +0 -88
- package/dist/core/runner/persisted-path-rows.js +0 -108
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,141 @@
|
|
|
1
|
+
## 7.50.0 — 2026-10-13
|
|
2
|
+
|
|
3
|
+
**Read face, rule lifetime and consolidation wave — BREAKING.** A person's standing path rules (a rule store with a durable partition; a session-only store has no personal lane, as before) now judge every tool on their face at the gate, are read once per call and fail closed when they cannot be read; RepoMap and LSP join the read face; background memory consolidation asks the same rules before it writes; a delegated child's start is one frame; a host can attach files to an objective; `tool_end.output` carries images. Every entry below states Before / Now / who is hurt / compensation; this head lists the package exports, the hard-breaking items (each open to veto; rulings in parentheses), the closed sets that moved, the behaviour narrowings, what is not in this release and the known limits.
|
|
4
|
+
|
|
5
|
+
**Package exports** (`npm run handoff:diff v7.49.5 <RC>`): 2609 → 2628 names — **+19 / −0**: `USER_ATTACHMENT_SEAT`, `USER_ATTACHMENT_KINDS`, `USER_ATTACHMENT_SUPPLIES`, `USER_ATTACHMENT_OUTCOMES`, `USER_ATTACHMENTS_MAX`, `USER_ATTACHMENT_LABEL_MAX` and the types `UserAttachment`, `UserAttachmentDeclaration`, `UserAttachmentKind`, `UserAttachmentSupply`, `UserAttachmentOutcome`, `UserAttachmentObservation`, `HumanInputAttachment`, `UserMessageAttachment`, `BoundFileObject` (#1311, fifteen); `CONSOLIDATION_WRITE_REFUSED_CODE` and the types `ConsolidationWriteJudge`, `ConsolidationWriteGate`, `ConsolidationWriteTarget` (#1653, four). Members removed: `PermissionResult`'s ask arm `guard` (so also on `PreToolUseResult`, which is `PermissionResult & { additionalContext? }`, and on the `HeldQuestion` union, which carries `PermissionResult`), replaced by `guards`. Members now required: `RunMemoryConsolidationOptions.writeJudge`, `ConsolidationDriverEngine.consolidationWriteHome`, `DriveConsolidationOptions.writeGate`, and `writeGate` in the options of `MemoryEngine.commitConsolidationPlan` / `resolveConsolidationPlan` and of `ConsolidationDriveEngine.commitConsolidationPlan`; `MemoryEngine.reconcileConsolidation` and `ConsolidationDriveEngine.reconcileConsolidation` take a new required second parameter `{ writeGate }`. Function shapes: `resolveMemoryConsolidationDriver` returns `Omit<RunMemoryConsolidationOptions, "writeJudge">`; `LspSession.request`, `TransportLspSession.request` and `TransportLspSession.notifyFileChanged` take an optional trailing `gate`; `TransportLspSession.warmOpen` resolves `{ opened, failed, deferred }`; `MemoryBackend.applyPatches` (and `FileMemoryEngineBackend.applyPatches`) options gain optional `admit` / `confirm`. Members added (optional on interfaces, concrete on classes): `TaskSpec.userAttachments`, `UserMessage.attachment`, `UserPromptSubmitContext.userAttachments`, `FileSystem.openBound` (and on `ExecutionEnv`, `RemoteExecutionEnv`, `NodeExecutionEnv`, which also gains `listOpenDirectory`), `LspToolOptions.readFace`, `PersistedRuleHit.denial`, `PersistedRuleVerdict.denial`, `EngineAttestation.pathDenial`, `ShellReadVerdict.environmentSilent` / `.rowsUnread`, `ShellReadBoundary.unreadPartitions`, `ReadDenyMatcher.forCall`, `WriteDenyMatcher.forCall`, `AskClassFacts.ruleStoreUnreadable`, `MemoryBackend.admitsLandings` (and `FileMemoryEngineBackend`), `ConsolidationPlanFile.applier`, `MemoryEngine.consolidationWriteHome`, `human_input.attachments`. A new `TaskEvent` arm, `task_started` (its `type`, `taskId`, `sessionId`, `model` and `startedAt` always present, the rest optional).
|
|
6
|
+
|
|
7
|
+
**Hard-breaking (each open to veto).**
|
|
8
|
+
1. **A person's persisted deny row refuses the call's own target at the gate for every tool on the row's face; RepoMap is on the read face (#1293 a).** Who needs it: a person whose `Read(<path>)` / `Edit(<path>)` deny row means "no tool reads / writes this" — it refused only the row's own tool at the gate, and a caller's tool declaring a path target not at all. Who is hurt: a consumer that identifies a person's deny by the search tool's `errorCode: "read_path_denied"` with a gate record `allowed`; a counter of gate refusals (`deniedBy: "persisted_rule"`) that now sees these calls; a deployment whose environment declares no home while a person keeps a `~/…` deny row (now every file-tool call the row speaks for is a real-approval question, not only Read's — and since #1653 r2 no mode releases it: v7.49.5 released the Read under `bypassPermissions` with a disclosure and ran the others, now each call is asked and, where nobody can answer, refused; server declares no home for its k8s, local-docker, ssh, adb and device lanes unless `REMOTE_EXEC_HOME_DIR` is set — `executionLaneHomeDir`, sema-server `origin/main` 57356167; ticket S-1519 in DEBTS-server, 7.127.0); a run that used RepoMap as a free read under a `Read` ask row (now asked). Compensation: read who refused from `tool_end.gate.disposition.deniedBy`; to keep RepoMap out of a directory outside the roots, use the read-only band or a `Read(<path>)` deny row.
|
|
9
|
+
2. **LSP is on the read face (#1293 c; C-R428).** Who needs it: every deployment with LSP enabled — it was the one reader outside the read face (no path rule, no read station, no withholding). Who is hurt: a run that reached a denied, asked or out-of-root file through LSP (now refused, asked, or asked as out-of-root); (not a hands-less leg: it lost LSP in 7.49.2 already, `lsp: { mounted: false }`, and the mount now reads the read face, `lspManager && readFace`, with the same result; server's hands-less twin drops `lspManager` itself, `src/capabilities/hands-lane.ts` `withoutHands`; and a host deriving the hand band from the catalog has had LSP in it, effect `read`, since 7.49.2); a caller of `TransportLspSession.warmOpen` that read `opened` (now 0; the carried count is `deferred` — server `src/lsp/manager.ts:219` awaits it without reading the counts). a deployment whose execution environment has no `openBound` (open to veto): every gated LSP file read there reports `file_sync_failed` ("this environment cannot bind …"), so LSP answers nothing about a file — server `origin/main` `57356167` has none on `RemoteHostExecutionEnv` (host lane) nor on the e2b / k8s environments, the dependency the attachment seat already carries (server S-1513; both land in server's 7.50.0 pickup); a third-party `LspSession` honouring the gate (it receives an admission with the held object, not a yes / no); a deployment `readText` no longer consulted for a gated read. Compensation: grant the read root where LSP must read outside it (the out-of-root card offers it); mount hands where LSP is wanted; read `deferred`; implement `FileSystem.openBound` on the environment LSP runs under (core's `NodeExecutionEnv` has it).
|
|
10
|
+
3. **The person's rows are read once per call, and one failure law governs a read that fails (#1293 b; C-R422, C-R434).** Who needs it: every check of a call (the shell and file read stations, the rule lanes, the tools' deny tables, read directories) judges the same rows, and a partition that cannot be read can no longer be read as "no rule". Who is hurt: a run whose store is flaky — a deny row last read keeps refusing at the gate; while a partition cannot be read every command the shell read station would have vouched for is asked (refused on the read-only face and where nobody can answer; a refusal or question it finds stands), and the Artifact tool's local read, the compaction summary's working-file reader, the user-attachment seat's reader (#1311) and a language server's session read refuse; a deny row just removed keeps refusing until a read of its partition succeeds; a `cat` / Grep that followed the prepare read now follows the store as of the call, and a compaction re-attachment follows the run's newest read. Compensation: make the store readable; the run's one `permission.rule_store_unavailable` notice says which rows still apply (the deny and ask rows last read).
|
|
11
|
+
4. **`PermissionResult`'s ask arm `guard?` is now `guards?: readonly AskGuard[]`, and a decision that still carries `guard` is refused by name (#1293 b; C-R440).** Who needs it: one ask may stand on several integrity guards, each keeping its class (an environment's silence beside a session-transcript write); and a retired member the engine dropped unread would turn the ask into one without its guard's class (its arbitration row would be the remaining classes', `ordinary` when none: a rule or the read-only command arm clears it, `bypassPermissions` releases it, the classifier may answer it). Who is hurt: a host policy or PreToolUse hook that wrote or read `guard` — a decision literal annotated `PermissionResult` gets a compile error, and an inferred return (an arrow inside a `ToolPolicy` literal) still compiles, but at runtime the decision is denied before any mode is read, with a message that names `guard` and `guards` (a deny that a later rule layer also raises on the call replaces the text, never the refusal) and the call does not run (the receipt that already refuses the retired `reason` text field; `RETIRED_DECISION_MEMBERS`). No neighbour reads or writes it (`origin/main` grep, 10-13: server / cli / client-core `src/` none). Compensation: a policy writes `guards: ["<word>"]`; a PreToolUse hook drops the member (the fold has never carried a hook's guard — its ask is classed `hook`); read classes off `classes` / `askClass`.
|
|
12
|
+
5. **A delegated child's start is one `task_started` frame per cycle; the lifecycle seat's `spawn` is its projection and is not delivered for a durable resume leg (#1639; C-R404).** Who needs it: every consumer that opens a child row — the row opens at the start, not one model turn later (CC 2.1.296 `system/task_started`). Who is hurt: a consumer of `RunnerDeps.onDelegationLifecycle` that pairs every terminal with a spawn of the same leg or counts spawns as legs (a parked-and-redeemed delegation reads `spawn, terminal, terminal`); an exhaustive switch over `TaskEvent["type"]` (server's two arm gates, `src/trace/core-keyset-guard.ts` and `src/trace/model-segment.ts`, compile-exhaustive; client-core's event view, exhaustive over the SDK union, red after the SDK bump; cli has no such switch over core's `TaskEvent["type"]` — its `task_started` hits are CC's SDK `system/task_started`) until it handles the arm. Compensation: count legs by `terminal`; project `task_started` to open the row; a consumer that ignores unknown frames is unchanged.
|
|
13
|
+
6. **`runMemoryConsolidationDriver` requires `opts.writeJudge` and asks it for every file a plan writes or removes (#1653; C-R421).** Who needs it: a person who denies (or asks about) a path in the memory library — the background run now stops where a model's own `Write` there stops (judged by path), and where that `Write` would only be asked (nobody is present to answer the background run). Who is hurt: a host that calls the verb itself — server's operator-consolidation boot module (`src/boot/memory-consolidation.ts:915`, with options from `resolveMemoryConsolidationDriver` at `:176` / `:246`, whose return type now omits `writeJudge`): a compile error and, at runtime, `config.memory_consolidation_driver`; a host implementing `ConsolidationDriverEngine` (+ required `consolidationWriteHome(scope)`); a deployment whose person rule or caller / inherited policy names (or asks about) a path in the library — consolidation stops at each task end (`memory.consolidation_refused` when the early question refuses, `memory.consolidation_incomplete` with `stop: "refused"` when the apply does); an org layer names no path (its grammar carries `Bash` rows only; a fetched snapshot with an `Edit(…)` org row is refused whole; the org layer then keeps serving the last valid snapshot while it is within the staleness bound, and where there is none it is unavailable, so every call is a real-approval question and every consolidation is refused as governance unavailable). Compensation: pass `writeJudge` (a host with no rules: `async () => ({ verdict: "no_objection" })`); catch `ConsolidationRefusedError` whose `code` is `CONSOLIDATION_WRITE_REFUSED_CODE`.
|
|
14
|
+
7. **Every verb that may apply a consolidation plan requires a write gate (#1653).** Who needs it: every apply asks where each file lands — in v7.49.5 these verbs took no gate and applied ungated (recovery replays included). Who is hurt: a host that calls `commitConsolidationPlan` / `reconcileConsolidation` / `resolveConsolidationPlan` / `driveConsolidationToFixpoint` itself or implements `ConsolidationDriveEngine` — a compile error, and untyped `config.memory_consolidation_write_gate` before anything is claimed (no `src/` caller in server, cli or client-core, `origin/main` 10-13). Compensation: pass the gate a write runs under; a host with no rules passes `async () => {}`.
|
|
15
|
+
8. **A rule store (or one partition of it) that cannot be read makes each call it serves a real-approval question in every mode, a deny row last read aside, until a read succeeds (#1653 r1); `rule_unreadable` is bypass-immune and leaves `MODE_RELEASE_REASONS` (#1653 r2; C-R434).** Who needs it: an unattended `bypassPermissions` deployment — "the rules did not answer" has one posture whether the store or the call could not be read. Who is hurt: a bypass or headless deployment that kept running while its store was down (r1: each call the lane judges now stops — a person is asked, with nobody to ask it is refused — until a read succeeds; v7.49.5 skipped the rows it could not read and judged the call on the rest); client-core's generated mirrors (`src/generated/engineFactTables.ts` `CORE_MODE_RELEASE_REASONS` and the `rule_unreadable` arbitration row, `src/generated/engineAskFaceTables.ts`) and its automatic-answer verdict; cli pins that expect the class to be answerable under bypass (`src/sema/engineGateSyncAllow.test.ts`); server wire fixtures that build a `released` outcome over the class; any host relying on bypass over an unreadable rule. Compensation: attach an approver or repair the store (the run's one `permission.rule_store_unavailable` notice names the sources whose partition could not be read); regenerate the mirrored tables from this release's exports; drop the class from release expectations; treat it like `governance_unavailable` (it stays its own class).
|
|
16
|
+
9. **`tool_end.output` keeps every block in its slot: admitted images and documents ride by block under the leg's media budget, and the 16384 cap counts text characters, cutting in place (#1668).** Who needs it: an SSE / replay surface that shows the image the model was shown. Who is hurt: a consumer that reads `truncated: true` as "`output` is a string" (a text-only blocks ARRAY over the cap is now that array cut in place; a string result is byte-identical to v7.49.5), or that bounds a frame by the 16384 cap (one event can now carry media up to the leg's `mediaByteCapBytes`, default 30 MiB decoded = 41 943 040 bytes of base64 on the wire; `0` means no budget on both the request strip and this projection); server's durable ledger stores media as descriptors and strips `data` (S-1518); the observer digest renders a media block as its marker, never its bytes (C-R441). Compensation: handle both shapes of `output`; render a block with `truncated: true` and no `data` from its `mimeType` and `bytes`; bound a frame by the blocks' `data` lengths; a durable consumer may keep the descriptor and drop `data`.
|
|
17
|
+
10. **`RunnerDeps` is read once at `new Runner` into the plain-data copy the Runner keeps, and a deps object that is not a plain object is refused (7.50.0 review wave; C-R443).** Who needs it: every deployment — a seat is what was read at construction (each own seat, enumerable or not; a getter invoked once; defined on the copy so no setter runs), and the value doors (`agentConfigDir`, `mediaByteCapBytes`) judge exactly that. Who is hurt: a deployment that constructs a Runner from a class instance or an `Object.create(<defaults>)` object (a seat its prototype would supply is never read) now gets `config.invalid_argument` at `new Runner`; one that mutated its deps object after construction, or passed a non-number / `NaN` / negative `mediaByteCapBytes` (v7.49.5: silently "no budget"), is refused or no longer seen. No sibling constructs a Runner from anything but a literal or a spread (server 13 sites, cli / client-core none). Compensation: pass a plain object (spread a class instance's own fields into a literal); use `swapDeps` to change a seat after construction; fix a bad budget value.
|
|
18
|
+
|
|
19
|
+
**Closed sets and vocabularies.** `TaskEvent["type"]` +`task_started`; `MODE_RELEASE_REASONS` −`rule_unreadable` (`ModeReleaseReason` narrows; `MODE_ARBITRATION.rule_unreadable.bypassImmune` `false` → `true`; `releasableAskClassesFor("shell" | "other")` −`rule_unreadable`); `PermissionResult.ruleUnreadable` +`persisted_rows`; `CHECKPOINT_CONFIG_REFUSAL_CODES` +`config.user_attachment_invalid`; new: `AskGuard` (the five guard words, now the element type of `guards`; not exported from the package root), `USER_ATTACHMENT_KINDS` / `UserAttachmentKind`, `USER_ATTACHMENT_SUPPLIES` / `UserAttachmentSupply`, `UserAttachmentOutcome`; the read layer's `ReadKind` +`directory`; engine-internal `SYNTHETIC_READ_UNANSWERABLE_CAUSES` +`rewritten` +`store_unreadable`. New codes: `config.user_attachment_invalid`, `memory.consolidation_write_refused`, `config.memory_consolidation_write_gate`, `memory.landing_unstable`. An additive member with one compile-exhaustive consumer: `UserMessage.attachment` (server's session-view read treatment, `src/audit.ts` `MESSAGE_READ_TREATMENT.user`, a `LeafTable` over the keys of core's user message — by its own doc a key it does not list is a compile error — adds the row; sema-server `origin/main` 57356167).
|
|
20
|
+
|
|
21
|
+
**Behaviour narrowings — each in its entry below.** A person's deny refuses Grep / Glob / RepoMap / the write tools / a caller's path tool at the gate, with no card (#1293 a); a `Read` ask row and the out-of-root question reach RepoMap (#1293 a; the one loosening: an out-of-root RepoMap `path` is now an answerable question, not a refusal); `Read` rows and the out-of-root question reach LSP and listed paths in denied files are withheld (#1293 c); a row gained mid-run applies from the next call everywhere, and beside an unreadable partition the shell read station vouches for nothing (#1293 b); a rule store that cannot be read makes each call it serves a real-approval question in every mode, a deny row last read aside, until a read succeeds (#1653 r1, reverses the 7.34.0 skip; C-R434); `rule_unreadable` is not released by `bypassPermissions` (#1653 r2); background consolidation stops on a rule naming the library (#1653); a steer whose options carry a defined `userAttachments` and a resume config carrying an attachment entry are refused (#1311); the Read / notebook / PDF trailers and the MCP disclosure block open after a blank line (#1290; a hook's feedback block appended to a tool result does not yet). Widening: a delegated leg with no spawning call now emits its ticks and terminal tick (#1639).
|
|
22
|
+
|
|
23
|
+
**Not in this release.** TaskOutput retirement (#1608), one read-only judge (#1438), skill-root containment (#1625), Windows rule-directory ACL check (#1703, C-R436), an environment's zero answer read as `unanswered` rather than lexically (#1694), `tee` / `cp` / `mv` operand table (#1695), agent-teams cells #1696 / #1697 — all windowed 7.50.0, none merged; #1698 / #1699 are windowed 7.51.0. Opened by the review wave for 7.51.0: #1708 (the resumed leg's belt reads a station's silence two ways — a judge that RETURNS `unanswered` on a seen call passes where the fallback after a throw refuses; pre-existing, the same on v7.49.5) and #1709 (a host-mounted station's `vouch` beside a finding other than `none`, or beside a silence mark, is taken at its word; the engine's own stations never spell it). Opened for 7.50.1 (patch): #1710 — the adjudicate seat races the policy chain with the two-state `raceAbort`, so a policy that THROWS is reported as "policy check aborted (task timed out or cancelled)" with the error unlogged (pre-existing; the file's own `RacedSettlement` note names the three-state race a decision seat must use).
|
|
24
|
+
|
|
25
|
+
**Known limits added** (`docs/KNOWN-LIMITS.md`): KL-1293b-a / -b, KL-1293c-a / -b / -e (and -c, closed in the same release by #1293 b), KL-1293d-a, KL-1311a2-a … -h, KL-1639-a / -b / -c, KL-1653-b / -c / -d / -e / -f / -h / -i / -j / -k, KL-1668-b / -c. Retired: KL-1291-b; closed: KL-1674-a (LSP read face landed), KL-1668-a (the `tool_end` projection reads the leg's media budget), KL-7340rj-a and KL-7340rg-c (the person's rows have one lifetime and one failure law, #1293 b); narrowed / restated: KL-1291-a (② narrowed to the shell's file commands; ① gains the LSP clause), KL-1111i (`guards`), KL-958-b (anchor wording); added by the review wave: KL-1708-a (the belt's two readings of a station's silence), KL-1709-a (a host station's `vouch` beside a contradicting finding or a silence mark).
|
|
26
|
+
|
|
27
|
+
### A person's persisted deny row refuses the call's own target at the gate, for every tool on the row's face; RepoMap is on the read face (#1293, first car; **BREAKING**; behavior change; hard-breaking: one, open to veto; closed sets: none; exports: +0 names, 0 removed; members: none public)
|
|
28
|
+
|
|
29
|
+
**Hard-breaking (open to veto).** (1) **What changed:** a person's persisted `Read(<path>)` / `Edit(<path>)` DENY row now refuses Grep, Glob, RepoMap, Write, Edit, NotebookEdit and a caller's or MCP tool that declares a read or write path target at the gate (`tool_end.gate.disposition` = `denied`, `deniedBy: "persisted_rule"`, the tool does not run) — it used to refuse them inside the tool (an error result, `errorCode: "read_path_denied"` for the search tools, gate record `allowed`) and refused a caller's tool not at all; and RepoMap joins the read face, so a `Read` ask or deny row, the rule policy's rows and the out-of-root question all reach it. (2) **Who breaks:** a consumer that identifies a person's deny by the search tool's `errorCode: "read_path_denied"`, its `structured` card (`type: "read_path_denied"` with `target` / `pattern` — the gate's refusal of a read carries the same card, see the next entry) or by a gate record `allowed` + `isError`; a counter or list keyed on gate refusals (`deniedBy: "persisted_rule"`) that now sees these calls; a deployment whose execution environment declares no home while a person keeps a `~/…` deny row (it asked on every Read call — and a `bypassPermissions` run released that Read with a disclosure, ran the other file tools unasked — and now every file-tool call the row speaks for is asked, bypass included: see #1653 r2 below, where the two meet); a run that used RepoMap as a free read of a directory a `Read` ask row guards (it is now asked) or whose `path` outside the roots was refused (it is now an ordinary question, answerable by a person, a persisted `Read(//dir/**)` row, the classifier or the permission mode — the one loosening of this entry). (3) **How to migrate:** read who refused a call from `tool_end.gate.disposition.deniedBy`, not from the tool result's error code; to keep RepoMap out of a directory outside the roots, leave the read-only band (`handsReadOnly`, which has no question channel and keeps the tool's own refusal) or deny the path with a `Read(<path>)` row.
|
|
30
|
+
|
|
31
|
+
- **Before (v7.49.5; the same in v7.49.0).** A face-keyed path row speaks for every call whose declared path target is on a face its behavior's column holds; the rule policy's lane read both tightening columns, the person's persisted lane read the ASK column only. A persisted `Read(secret/**)` deny therefore refused the Read tool at the gate and nothing else: Grep / Glob / the write tools met it inside the tool (the run's deny tables, taken at prepare), and a caller's tool declaring a read target met it nowhere (the file was read, no card). Beside a same-path ask row a Grep showed the card first and was refused only after the person said yes. RepoMap declared no path target: no row, no question and no admission seat reached it, its `path` resolved against the root inside the tool, and a `Read` ask row on the directory it mapped raised no card.
|
|
32
|
+
- **Now.** `adjudicatePersistedPathRules` (the exported judge — a caller passing `face` gets the deny verdict for another tool's row where it used to get none) reads `PATH_RULE_FACES[rule.tool][behavior]` for deny and for ask — the sentence `contentLaneOf` speaks for the policy — and the lane's door admits a call on a face any tightening column holds (`faceAskSpeaksFor` → `faceRowsSpeakFor`). The run's deny tables are unchanged and keep the four jobs the gate cannot do (a search's results, a shell command's operands, execution-time re-judgment, keeping a refused target out of the out-of-root question). RepoMap's catalog row declares `pathTarget: { param: "path", access: "read", base: "root", absent: "base" }` — the same declaration as Grep's; the tool reads it through `pathTargetValue` / `pathTargetBaseOf` and the toolkit hands it the file read station's admission seat, so its membership of the read face, of the path grammar (`RepoMap(<path>)` is a valid path rule) and of the out-of-root question is derived from the catalog, not listed. A `~/` row on a leg that declares no home is read UNREADABLE at the gate — a real approval question for every call the row speaks for (the Read tool's call always was one; now Grep, Glob, RepoMap, the write tools and a caller's tool declaring a path target are too) — and the prepare disclosure says so (`(the gate asks)`). Since #1653 r2 (below) no mode releases that question: under `bypassPermissions` each of those calls is asked, and refused where nobody can answer (v7.49.5 released the Read with a disclosure and ran Grep and Write unasked). The resumed-call belt runs the same lane, so a deny row the store gained while a Grep was parked vetoes it on resume.
|
|
33
|
+
- **Who is hurt.** See the hard-breaking paragraph. A host that counts or lists gate refusals sees these calls as refusals (they were tool errors with the gate record `allowed`); a Grep / Glob / RepoMap / write call that a person's deny row refuses raises no card where a deny + ask pair used to raise one first; the `read_path_denied` structured card rides the refusals made inside the tool (the deny tables) and, with the next entry, the gate's refusals of a read. Consumers on other repositories are not read here (inferred, not checked).
|
|
34
|
+
- **Compensation.** None needed for the refusals (the same calls end refused, earlier); hosts that matched the error code read `deniedBy`. A library-direct `createHandsToolkit` without `readAdmission` keeps refusing every out-of-root RepoMap target (the library-direct contract, as for Grep / Glob).
|
|
35
|
+
- **Pins.** `test/backlog1293a-read-face-lanes.test.ts` (arm × Read / Grep / RepoMap; G1–G8, G20); `test/backlog1291-read-ask-covers-search-tools.test.ts` (three cells re-pinned: RepoMap asked, a caller's reader refused, deny + ask refused without a card). Known limits: KL-1291-b retired; KL-1291-a ② narrowed to the shell's file commands and LSP (LSP: the entry below).
|
|
36
|
+
|
|
37
|
+
### LSP reads on the read face: its target is judged at the gate and in the tool, its session reads through the caller's read face, and paths in denied files are withheld (#1293, LSP car; **BREAKING**; behavior change; hard-breaking: one, open to veto; closed sets: none; exports: +0 names, 0 removed; members: `LspToolOptions.readFace` and the `gate` parameter of `LspSession.request` / `TransportLspSession.notifyFileChanged` added, optional; `TransportLspSession.warmOpen`'s result gains `deferred`)
|
|
38
|
+
|
|
39
|
+
**Security face first — a leg with `hands: none` has no LSP, as since 7.49.2 (#1674, the guard `lspManager && handsEnabled`); this release replaces that guard with the read face: the mount reads `lspManager && readFace`, and the read face exists only where hands mount.** Nothing changes for a hands-less leg between v7.49.5 and this release: the manifest already reports `lsp: { mounted: false }` there. (Up to v7.49.1 a hands-less runner still carried `lspManager`, and on the host lane its LSP session read the local disk through `defaultLspReadText` with no read-face fence. @server: its hands-less twin now drops `lspManager` together with the execution env — `withoutHands`, `src/capabilities/hands-lane.ts`, called at `src/boot/stage-06-runners.ts` — sema-server `origin/main` 57356167.) **Who needs it:** every deployment with a hands-less leg and LSP enabled — the leg had no read face, so LSP was the one reader outside it. **Who is hurt:** nobody newly — a server `hands: none` scenario lost the LSP tool in 7.49.2. **Compensation:** a scenario that needs LSP runs with hands, or a library host mounts `createLspTool` with a `readFace` seat; a deployment-side drop of `lspManager` on its hands-less Runner is the matching shape there.
|
|
40
|
+
|
|
41
|
+
**Hard-breaking (open to veto).** (1) **What changed:** LSP's `filePath` is a declared read target (catalog `pathTarget`, `base: "cwd"`), so `Read(<path>)` deny and ask rows — the caller's policy and the person's store — and the out-of-root question reach it at the gate, as they reach Read; the tool resolves `filePath` against the tracked working directory (it used `env.cwd`, so after a `cd` the gate and the tool named two files) and fences it at execution as Read fences its own (containment with the file read station's one-read admission, the read-deny table, blocked devices); the in-process session (`TransportLspSession`) reads an opened file only from the object the calling run's read face admitted — its gate judges the name, opens the object that judgment named and holds it (the environment's `openBound`, the same bound read a user attachment makes), judges the name again (it must still land on the held key), and the session reads that held object, never the name again (a link re-pointed after the judgment reads nothing, or the object that was judged); an environment without `openBound` cannot bind, so the session reads nothing there (the op reports `file_sync_failed`, "this environment cannot bind …"; there is no read by name); the gate answers `LspReadAdmission` (`{ ok: true, key, object }` with the held `BoundFileObject`, or `{ ok: false, reason }`), not a boolean; a caller with no gate (no read face) still reads by name through the deployment's `readText` — and the session closes (`didClose`) a document the gate does not admit, and a session healed after a crash carries its files without reading them — the next gated sync opens them; locations, symbols and drained diagnostics in a file the read-deny table reaches — on any view of it: the spelling the server used, the canonical target, the home view — are withheld, a file whose views cannot be read (or a batch that outlives the gate's deadline) is withheld as unreadable, and the result says how many of each; the mount guard is the read face, which exists only on a leg with hands (a hands-less leg has had no LSP since 7.49.2). (2) **Who is hurt:** a run that reached a denied, asked or out-of-root file through LSP (now refused, asked, or asked as out-of-root); a caller of `warmOpen` that logged `opened` (now 0; the carried count is `deferred`); **a deployment whose execution environment has no `openBound` (open to veto)** — every gated LSP file read there reports `file_sync_failed` ("this environment cannot bind …"), so LSP answers nothing about a file: server `origin/main` `57356167` has no `openBound` on `RemoteHostExecutionEnv` (the host lane, `NodeLspManager`) nor on the e2b / k8s environments (`E2bLspManager`) — the dependency the attachment seat already carries (server S-1513); the two land together in server's 7.50.0 pickup; a third-party `LspSession` that honours the gate (it now receives an admission to read and close, not a yes / no); a deployment `readText` (`NodeLspManagerOptions.readText`, `TransportLspSession`'s third constructor argument), no longer consulted for a gated read. (3) **How to migrate:** grant the read root where LSP must read outside it (the out-of-root card carries the grant); mount hands where LSP is wanted; read `deferred` in a heal log; implement `FileSystem.openBound` on the environment LSP runs under (core's `NodeExecutionEnv` has it); a session that honours the gate reads `object.readBytes(MAX_LSP_FILE_BYTES)` from an `ok` admission and closes the object.
|
|
42
|
+
|
|
43
|
+
- **Before (v7.49.5; the same in v7.49.0, but that since 7.49.2 only a leg with hands mounts the tool).** The LSP catalog row declared no path target, so no path rule (`Read(<path>)` and the other face-keyed rows) and no read station saw its calls (a bare `LSP` rule or an `LSP(<param>:<pattern>)` rule already applied); the tool joined `filePath` to `env.cwd`, opened it with no fence, and its session re-read every opened file before every operation, whoever asked; results were filtered by `git check-ignore` only. The edit nudge resolved against the tracked cwd, the tool against `env.cwd`.
|
|
44
|
+
- **Now.** One base for the tool, the nudge and the gate (`lspBaseOf` over the declaration). `LspToolOptions.readFace` (root, read roots, read-deny table, containment face, tracked cwd, station admission — the fs read tools' seats) is what the Runner hands the mount; absent, the tool behaves as before (the library-direct contract, as `createHandsToolkit` states for its own seats); present without `env`, `createLspTool` throws `TypeError`. The session's one read port asks the caller's gate (`LspSession.request`'s fourth parameter; `notifyFileChanged`'s third) and reads only the held object the gate admits (`lspReadGateOf`: judge, `openBound`, judge again on the held key); a caller with no gate reads by name as before. Each sync of a document takes the next generation of that document and publishes only if it is still the newest one when its read settles — a read of an object replaced since (an atomic save) never lands after a newer one's, a superseded sync publishes nothing and closes its object (its waiters take the newest sync's outcome), and no sync waits for another's read, so a read that never settles holds up no other caller; only by-name syncs share a read in flight. On Linux `openBound` checks the held descriptor's own path (`/proc/self/fd/<fd>`) against the key (KL-1311a2-h now applies to non-Linux platforms only: darwin, win32). With a read face, the tool's file preconditions ("File does not exist", "Path is not a file", "File too large") read the object its judgment named, held at the judged key — not the spelling again; a mount with no read face reads them by name as before. `Prepared.lspDiagnostics.withheldOf` (the same every-view batch judgment the tool's results go through) filters a drain before the `<new-diagnostics>` block and the `diagnostics` frame. Rule acceptance: `LSP(<path>)` is a path rule now (it was refused); `LSP(<param>:<pattern>)` policy rules keep their meaning (the row declares `operation`, `filePath`, `line`, `character`, `query`).
|
|
45
|
+
- **Who is hurt.** See the hard-breaking paragraph. A gate-refusal counter sees LSP refusals it never saw (they were reads); the tool roster row for LSP carries a `pathTarget` (an added member on the wire).
|
|
46
|
+
- **Compensation.** None for refusals (the same files Read could not read); the station's out-of-root card offers the read root.
|
|
47
|
+
- **Reference.** CC 2.1.296 judges LSP's `filePath` with the same read-permission function its file tools use (`cli296.js` L255538 @3468, `checkPermissions` over `getPath`) — followed. CC filters LSP results by `git check-ignore` only (@7925); withholding the paths a read-deny row reaches, and gating the session's re-reads, is stricter than CC (the traversal's "a deny descends into every listed path" applied to LSP; divergence registered by the release).
|
|
48
|
+
- **Pins.** `test/backlog1293c-lsp-read-face.test.ts` (deny / ask × policy / store against Read; G9–G16; the session's ports incl. carried files; G11 the tool's own fence refuses before any session is asked, `errorCode: "read_path_denied"`); `test/backlog1293c-lsp-bound-read.test.ts` (a link re-pointed between the session's judgment and the open is refused, after it the held object is read, an environment without `openBound` reads nothing; the gate's admission; a read that settles after a newer sync of its document started publishes nothing, and a cancelled or stalled read holds up no other caller, same document or another); `test/backlog1311a2-openbound-linux.test.ts` (Linux: an ancestor linked for the open and put back is refused by the descriptor's path; a leaf link is never followed); re-pinned: `test/prepare-lsp.test.ts` (the nudge's gate answers an admission), `test/lsp-diagnostics.test.ts` (the two-run cell edits to a text the session has not seen — the nudge reads the file, not the deployment's `readText`), `test/prepare-lsp.test.ts`, `test/prepare-lsp-transcript.test.ts`, `test/prepare-lsp-ignore-filter.test.ts`, `test/backlog789-wiring-manifest-hooks-lsp.test.ts`, `test/public-knob-liveness.test.ts`, `test/ref-c3-lexical-fold-convergence.test.ts`, `test/tool-catalog.test.ts`. Known limits: KL-1291-a ② LSP clause retired, KL-1291-a ① gains its LSP clause (an ask does not descend into re-synced documents); KL-1293c-a (hover text, shared sessions), -b (third-party session reads), -e (a link chain the environment cannot read: a deny on a middle hop is not judged); KL-1293c-c (the table's prepare-time lifetime) is CLOSED in this release by #1293 b.
|
|
49
|
+
|
|
50
|
+
### A rule lane's refusal of a path carries the same structured card the tools' own refusals carry, minted from the evidence the judge found it with (#1293, fourth car; behavior change; hard-breaking: none; closed sets: none; exports: +0 names, 0 removed; members: `PersistedRuleHit.denial?`, `EngineAttestation.pathDenial?`, `PersistedRuleVerdict.denial?` — optional, additive)
|
|
51
|
+
|
|
52
|
+
- **Before (the previous entry).** The tools' own resolution and the shell station refused a read with the structured `read_path_denied` card (`target`, `pattern`), but the gate's rule lanes — the caller's rule policy and the person's persisted rows — refused the same read with a sentence and a `deniedBy` word only, so *where* a refusal was made decided whether its coordinates reached the wire; with the previous entry moving a large class of refusals from the tool to the gate, they would have lost the card. The judge found the reaching row and the view it met, and dropped both.
|
|
53
|
+
- **Now.** The judge returns where the reaching row met the call — the rule text, the view of the target it met, the face the call is on (`PersistedRuleVerdict.denial`, the same triple on the rule policy's deny as `EngineAttestation.pathDenial`) — and the deny carries it. The gate's exit, the approved-edit round and the resumed leg's belt each mint the card from the deny they read: one `read_path_denied` card `{ type, code, target: <the view the row met>, pattern: <the rule's text> }`, written in one place (`readPathDeniedCard`, `src/tools/fs/safety.ts`, which the tools' own refusal calls too), and none of them judges the target a second time. The card's presence follows the FACE, never the station: a read face carries it; a write face has none (as the tools' own write refusal has none — no `write_path_denied` card type is minted). `deniedBy` and the refusal's sentence are unchanged; `tool_end.errorCode` is the card's code (`read_path_denied`) on a refused read, absent on a refused write. A host's own persisted-rule lane may report the same member on a deny hit (`PersistedRuleHit.denial`); the lane-answer screen copies it only for a deny, only when its rule is one of the hit's own members, and drops anything else (the refusal itself stays).
|
|
54
|
+
- **Who is hurt.** A consumer that treated `errorCode: "read_path_denied"` on a tool frame as "refused inside the tool" now also sees it on a gate refusal (`gate.disposition.deniedBy` says which layer refused — read that). The rule policy's refusal of the Read tool, Grep, Glob and a caller's reader gains a card it never had (additive). Consumers on other repositories are not read here (inferred, not checked).
|
|
55
|
+
- **Compensation.** None needed; the write face's structured coordinates are not carried (a separate decision, not made here).
|
|
56
|
+
- **Pins.** `test/backlog1293d-denial-card-evidence.test.ts` (gate exit: policy / store × Read, Grep, Glob; the tools' own card's key set; a link spelling; the write face, a caller's reader and writer; both sources on one target; the approved-edit round; the resumed belt; the lane-answer screen; one mint); `test/backlog1293a-read-face-lanes.test.ts` and `test/backlog1291-read-ask-covers-search-tools.test.ts` re-pinned where they asserted the absence of a gate card.
|
|
57
|
+
|
|
58
|
+
### One read of the person's rules per call, for every check of the call; one failure law: what could not be read keeps its deny and ask rows as last read and loses its allow rows (#1293, lifetime car; **BREAKING**; behavior change; hard-breaking: two, open to veto; closed sets: one — `PermissionResult`'s ask arm `ruleUnreadable` (and the decision receipt's words) gain `"persisted_rows"`; exports: +0 names from the package root, 0 removed; members: `PermissionResult`'s ask arm `guard?` becomes `guards?: readonly AskGuard[]` (`AskGuard` is not re-exported from the package root; hard-breaking, second paragraph below), `ShellReadVerdict.environmentSilent?` / `rowsUnread?` (`true`-only marks), `ReadDenyMatcher.forCall?`, `WriteDenyMatcher.forCall?`, `LspReadFace.ready?` (`(toolCallId: string | undefined, signal?) => Promise<void>`: a call's own tables, or with no call the run's newest), `ShellReadBoundary.unreadPartitions?` (rows the deny seats could not read: a guard of unknown reach) — optional, additive; `ShellReadStationOptions.boundaryOf` gains a third parameter, the call's id)
|
|
59
|
+
|
|
60
|
+
**Hard-breaking (open to veto).** (1) **What changed:** the person's persisted rows are read ONCE per call and that one read is what every check of the call judges with — the shell and file read stations, the org layer, the persisted rule lane, and the tool's own tables when it runs (the read deny table, the write deny table, the `Read(//dir/**)` read directories); a row the store gains or loses mid-run applies from the next call everywhere (it used to reach the rule lane at once and the stations, the tools' tables, attachments and the Artifact tool's local read only at the next run's prepare). And when the store, or one partition of it, cannot be read, the deny and ask rows of the run's last successful read of that partition still apply while its allow rows (read directories included) do not — everywhere: the rule lane used to skip what it could not read for that call — the whole store, or that partition's rows, its deny rows too — and the tables used to keep the prepare read whole for the run. (2) **Who breaks:** a run whose store is flaky now refuses a call it used to let through (the person's deny row last read keeps refusing, at the gate) and asks where it used to serve (a read directory is withdrawn while its partition is unread); **while a partition of the store cannot be read, every command the shell read station would have vouched for is asked about — one that reads or writes no file included, since a command row (`Bash(date)`) may sit in what could not be read — (refused on the read-only face, and wherever nobody can answer), and the Artifact tool's local read, the compaction summary's working-file reader, the user-attachment seat's reader (#1311) and a language server's session read refuse — until a read of that partition succeeds, before any read of it succeeded too: a row the engine cannot see may forbid the read, so its silence is no yes** (the reference implementation skips a settings file it cannot read, the rows taking no part — a registered divergence), and the run's one `permission.rule_store_unavailable` notice now says so ("While they cannot be read, every call your rules judge (a file read, a write, a command) is asked about rather than allowed, until a read of them succeeds."); a deny row the person just removed keeps refusing until a read of its partition succeeds; a `cat` / Grep that used to follow the prepare read's tables now follows the store as of the call, and a compaction re-attachment follows the run's newest read. (3) **How to migrate:** nothing to configure; a host that wants a removed deny row gone at once makes its store readable — the run's one `permission.rule_store_unavailable` notice now says which rows still apply.
|
|
61
|
+
|
|
62
|
+
**Hard-breaking (open to veto), second.** (1) **What changed:** `PermissionResult`'s ask arm `guard?` (one integrity-guard word) is now `guards?: readonly AskGuard[]` — a set, the same five words: one ask may stand on several guards and each keeps its class (in v7.49.5 an environment's silence beside a session-transcript write rode the fold's class list, `foldedAskClasses`, instead of standing as a guard of its own); the decision receipt holds the member as a list, and refuses the retired one by name; a receipt's lists are frozen (every array it holds, `guards` among them), so a word edited into a receipt and handed back cannot ride its fast path. (2) **Who breaks:** a host policy or hook that wrote or read `guard` on a decision — the member is engine-stamped and fold-internal by contract, and no neighbour reads it (server / cli / client-core `origin/main` grep: none). A decision that still carries it — as its own member, through an accessor, or from its prototype (a class getter), by the one presence law the receipt reads a contract member by — compiles where its type is inferred and is denied at runtime, before any mode is read (`bypassPermissions` included), with a message that names `guard` and its successor `guards`; the call does not run (C-R440, open to veto: the alternative, dropping the member unread, turned the ask into one without its guard's class, which a read-only command arm clears and `bypassPermissions` releases). (3) **How to migrate:** a policy writes `guards: ["<word>"]`; a hook drops the member (the fold has never carried a hook's guard); read the classes off `classes` / `askClass`, as before.
|
|
63
|
+
|
|
64
|
+
- **Before (v7.49.5; the same in v7.49.0).** Two lifetimes and two failure answers for one set of rows: the rule lanes read the store per call (the org lane's read handed to the personal lane by call id) and skipped everything a failed read held, deny rows included; the run's tables read it once at prepare (`persistedPathRowsOf`) and kept that read whole for the run ("a row the store gains mid-run applies from the next prepare"), or held none of a failed partition's rows. The shell station judged a call against the prepare tables while the lane judged it against the call's own read.
|
|
65
|
+
- **Now.** One reader per run (`src/core/runner/person-path-rows.ts`): a call's read is taken by whichever of its consumers asks first and handed to the rest (one store query per call); every read's outcome lands in the run's unit per partition (durable, session), only where it is newer by the order the reads started — a read that answers late never replaces a later one; a reader that is no call (the compaction summary's working-file reader, the user-attachment seat's reader, the Artifact tool's local read, the read-root grant's offer, the auto-mode classifier's table, a language server's session read, the prompt's read-directory line, a station judgment that is no call) reads the unit's newest tables: when a read lands rows of a new content, those tables compile and such a reader waits for them (at most `PATH_TABLES_READY_BOUND_MS`, the one bound every wait on path tables takes — 30 s; the wait ends with the run, and a reader cut while it waits reads nothing), then judges with them; a read of them while they still compile, or once they could not be compiled, refuses — it never judges with the tables of the rows they replace. A read that brings the same rows keeps the tables, with no wait. Beside a partition that could not be read, no reader takes the tables' silence as a yes: those rows are a guard of unknown reach on the shell read station's boundary (`ShellReadBoundary.unreadPartitions`, from the call's read, or the run's latest for a judgment that is no call) — the station guards on it whatever else it can see, and what it would vouch for is answered as a judgment it could not make (a question; on the read-only face a refusal), in its judgment and in its fallback; a refusal or a question it found stands; the station's answer carries two independent facts and the gate classes every question it keeps or mints by each — rows it could not read (`ShellReadVerdict.rowsUnread`, on any verdict judged beside them, whatever its finding) are the rule lane's class, `rule_unreadable` (`ruleUnreadable: "persisted_rows"`), and like the environment's silence a question only a person answers (the real-approval bit: a host's blanket `onAsk: "allow"` does not answer it); the `probe` mark, the class of a question a boundary the engine could read raises, follows the finding and never the silence (a finding the station could not make — `unanswered`: the environment's silence, or the rows' silence that turns a vouch into one — stamps none; a boundary it did read keeps it, so an out-of-root read found beside unread rows is classed `probe` and `rule_unreadable`, beside `governance_unavailable`); an environment that did not answer (`ShellReadVerdict.environmentSilent`: a resolver, listing or identity lookup without an answer, a bound reached, a judge that did not answer) keeps the class it has had since 7.49.3, `unverifiable_shell_text` — the finding `unanswered` is itself the silence, and the marks only pick whose: an `unanswered` verdict with neither mark (a host-mounted station) is the environment's, and rows unread beside what would have been a vouch are the same finding but the rules' silence, a class of their own and not this one — and the gate reads the finding ONCE: `refuse` first (a refusal stays one), then `unanswered` before the `vouch` / `mandated` / `ordinary` arms — a station that spells `vouch`, `mandated` or `ordinary` beside `unanswered` asks the same question (a vouch for what nobody judged is no vouch); nothing is admitted, no read-root grant is offered; an ask's guards are a set (`PermissionResult.guards`), so that silence stands beside a guard the question already carries (a session-transcript write) — and a reader that is no call refuses its judgments while the run's latest read of the partition stands unread (what the tables describe — their rows, read directories, home views — is still read, so a run whose store is down still prepares). The disclosure of a mode release past the read-deny table (`permission.read_deny_unapplied`) names the rows of the table the released call was judged with. The tables are one facade compiled once per content (row text × scope; the read directories), placed at their homes per compile; the band takes the Runner's tables instead of compiling its own copy. Every tool that holds a table takes the call's own at the start of its execution (`seatForCall`); a call's read is released when the leg's occurrence table no longer holds that attempt (the call ended, or a new call reused its id); a judgment that is no call of the leg (the changed-files visibility probe's fixed id, a gate outside the loop) keeps nothing — its rule lanes take a read of their own, its read station the run's latest tables. A tool waits for its call's tables (or its cancellation) before it takes them; a compile that never settles fails at the gate's bound. A read that has not answered one second before the rule lanes' own wait ends (`PERSON_READ_BOUND_MS`, derived from that wait) is judged by the failure law, so the deny and ask rows last read reach the lane in time; the lanes wait for the rows only, the stations and the band's tools for the call's tables, and a call whose tables cannot be compiled is refused — never judged with another read's tables — and reported on `onError`. An anonymous task's reads carry no personal row, whichever consumer took them. One sentence for a failed read, at prepare or at a call: "…none of their allow rows (Read directory rows included) applies, while the deny and ask rows most recently read from them this run still apply — a deny row you removed since that read keeps refusing until then. While they cannot be read, every call your rules judge (a file read, a write, a command) is asked about rather than allowed, until a read of them succeeds. Every later check reads them again." The person's read directory that admits the local artifact store is no read root wherever the roots are read (it used to be withdrawn once, from the prepare read).
|
|
66
|
+
- **Who is hurt.** See the hard-breaking paragraph. A host that implemented `ShellReadStationOptions.boundaryOf` keeps working (the added parameter can be ignored); a host matching the old notice sentences reads the new one. A judgment that is no model call (the user-attachment admission, the consolidation write probe) takes a read of the store for each rule lane it asks — the personal lane and, where an org partition is wired, the org lane — so its lanes can stand on different snapshots of the store, and the run's newest tables its file station judges with are a third reading (measured for the attachment on a real Runner: one extra durable read with a durable partition alone, two reads and two org-snapshot fetches with an org partition beside it; the write probe's lanes are read the same way, not measured); each lane fails closed on its own failure and a deny from any of them stands, so both directions fail closed. Tracked as #1705 (7.51.0: a judgment-scoped pin, with #1702); no code change in this release.
|
|
67
|
+
- **Compensation.** The notice says which rows still apply (the deny and ask rows last read; it does not list them); a store that answers again restores every row as read on the next call.
|
|
68
|
+
- **Pins.** `test/backlog1293b-person-path-rows.test.ts` (one read for three consumers; release when the attempt ends or its id is reused; a fixed-id visibility probe sees a row a later call read; a judgment that is no call keeps nothing; the anonymous floor; a failed compile refuses; the read bound inside the lanes' wait; the compile's bound; one compile per content; a call keeps its own read; a reader that is no call is refused while the unit's newest tables compile or after they could not be, waits for them and judges with them once compiled, and keeps the tables at once when a read brings the same rows; G18 the store gains then loses a row; G19 a removed row on the very next `cat`; G21 a failed durable partition beside a read session partition, unit and lane; G22 two calls judged at once, the late read; G23 the Artifact tool's local read of a file a new deny row covers while its tables compile — waits and is refused, refused when they cannot be compiled, served at once when the rows did not change; G24 a released call's disclosure while the run's newest tables compile — delivered, no observer failure; G25 a store that throws from its first read and a session partition that throws after one empty read — `cat` asked on both read faces under default, refused on the read-only face, served unasked once the partition reads again; the station's fallback with a boundary seat that throws answers closed; a boundary with both faces open, no deny row and no write judgment still guards the rows it could not read; G26 the station's two silences — the environment's under bypass refused on both read faces, the rules' classed `rule_unreadable`; G27 rows unread beside an out-of-root read and an unplaced read keep the rules' class; G28 an environment's silence beside a transcript-write guard: both guards, no mode or classifier clears it; G29 the rows' silence — beside a vouch, an out-of-root read, a declined scan, over an allow and over a host's ask — is refused by a blanket `onAsk: "allow"`, the call does not run); re-pinned: `test/backlog7340rj-unread-path-rows.test.ts`, `test/backlog1144-persisted-path-deny-rows.test.ts`, `test/rev7340rm-rule-store-partitions.test.ts`, `test/backlog768-bash-canonical-recheck.test.ts`, `test/prepare-task-phase-pins.test.ts`, `test/prepare-turn-wiring.test.ts`, `test/prepare-artifact.test.ts`, `test/prepare-caps-and-workflow.test.ts`, `test/backlog955-per-lookup-and-identity-column.test.ts` (the fallback's write-guard clause read off the station's one unanswered builder); `test/backlog1653-rows-unread-no-probe.test.ts` (the station stamps no `probe` mark on an `unanswered` finding, whichever silence made it, and an out-of-root read keeps its `probe` beside a healthy store and beside unread rows); `test/backlog1293b-retired-guard-member.test.ts` (the retired `guard` is refused by name on the policy fold, the hook seat and a host-driven `runToolGate`, in `default` and `bypassPermissions`, whether the decision owns it, holds it through an accessor or gets it from a prototype); `test/backlog1293b-receipt-lists-frozen.test.ts` (every list of a receipt is frozen, on both seams); `test/backlog1653-unanswered-finding-is-silence.test.ts` (a station verdict `unanswered` with no mark is the environment's silence: the real-approval bit and the class, a stored allow rule and `bypassPermissions` do not clear it; the marks only pick whose). Known limits: KL-1293c-c (the LSP table's prepare-time lifetime) CLOSED; KL-1293b-a / KL-1293b-b added.
|
|
69
|
+
|
|
70
|
+
### The user-attachment seat: a file the user attached reaches the model as its own marked message after the objective (#1311 car A2, with #1290; additive seat; behavior narrowing: two; closed sets: `CHECKPOINT_CONFIG_REFUSAL_CODES` +1, three new exported sets; exports: +15 names (with `BoundFileObject`), 0 removed; members: `TaskSpec.userAttachments`, `UserMessage.attachment`, `human_input.attachments`, `UserPromptSubmitContext.userAttachments`) (@server @client-core @cli)
|
|
71
|
+
|
|
72
|
+
**`UserMessage` gains a member (one exhaustive consumer).** (1) **What changed:** `UserMessage.attachment?: UserMessageAttachment` — the delivery record an attachment frame carries in the transcript (never on a provider wire; stripped at import). (2) **Why:** "what was delivered" has one source — the read-state commit and rebuild, the `human_input` projection, the derived views, compaction cut points and `resumeAt` read this record instead of the frame's text. (3) **What to do:** a table that maps every key of `UserMessage` (server's session-view read treatment, `src/audit.ts` `MESSAGE_READ_TREATMENT.user`) adds the row — the record names a path (`label`, `observation.key`); a replay UI renders a frame as an attachment card from it, not as the user's bubble.
|
|
73
|
+
|
|
74
|
+
- **Before (v7.49.5; the same in v7.49.0).** No seat. A host that wanted the model to see a file spliced it into the objective (a data envelope, an objective tail), so the engine's read rules never judged it, the session's declaration read any reminder-shaped wrapping as data, and nothing recorded what was delivered.
|
|
75
|
+
- **Now.** `TaskSpec.userAttachments` (≤ 20), three arms by who holds the bytes: `{ path, lines? }` — the engine reads it with the Read tool's own read (the leg's one reader, the one the Read tool reads through) under the run's synthetic-Read judge; `{ name, text, startLine? }` and `{ name, image }` — bytes the host holds. After the UserPromptSubmit hook lets the objective through (the hook sees `ctx.userAttachments`: paths, names and byte counts, never bytes) the engine reads every attachment, prices the frames in the pre-call gate, and sends one `engine-note` user message per attachment right after the human message: a marked static opening sentence, `Path: "…"` / `Name: "…"` (a lossless quoted label), the data (the Read tool's own blocks, a directory listing one level deep with withheld entries counted and never named, a PDF as a reference, or the refusal sentence), the engine's notes as marked static sentences, `End of attachment n.` marked. A deny anywhere, a question for someone other than the asker (real-approval, deployment-authored, an ancestor's, an organization rule's ask), a layer that cannot answer, a path outside the roots or the read-deny table ⇒ not attached, said in the frame. A read commits (read state, disclosure counters) when its frame is in the transcript, keyed by `(inputId, ordinal)` — never at read time. `human_input{applied}.attachments: [{ ordinal, kind?, outcome, label }]` is what the engine read and will send. Continuation rebuilds read state from the frames' records; compaction never cuts a human message from its frames; the summarizer and the classifier window render a frame from its record as the user's material (`user attachment n of N (…): "<label>"`); `resumeAt` refuses a frame target, `"before"` drops the group, the inclusive mode keeps it; the import door strips the record. A steer with `userAttachments` is refused (`steering.invalid_content`): attachments ride the next objective. Package root: `USER_ATTACHMENT_SEAT` (the descriptor a host reads per attachment), `USER_ATTACHMENT_KINDS` / `_SUPPLIES` / `_OUTCOMES` (the last is the Read layer's own outcome array), `USER_ATTACHMENTS_MAX`, `USER_ATTACHMENT_LABEL_MAX`, and the types `UserAttachment`, `UserAttachmentDeclaration`, `HumanInputAttachment`, `UserMessageAttachment`, `UserAttachmentKind` / `Supply` / `Outcome` / `Observation`.
|
|
76
|
+
- **The door.** `config.user_attachment_invalid`, the message opening with the field path (`TaskSpec.userAttachments[2].text: …`): a malformed entry, an unknown member, two arms in one entry, an over-limit text (rendered with line numbers, > 100 000), a host image the API would not accept, a path entry on a run without hands, any entry on a durable resume leg (refused before the claim: `checkpoint.config_refused`, `detail.code` = the same word — `CHECKPOINT_CONFIG_REFUSAL_CODES` +1).
|
|
77
|
+
- **The synthetic-Read judge has a fourth answer.** `mandated_ask` — no layer refuses, and one would put the read to a person no rule may answer for (the ask's own mandate reading; an organization rule's ask; any ask a live inherited parent policy answers — the ancestor's approver's question); a deny still wins. A policy layer that answers with a rewrite of the arguments (`updatedInput`) has not answered for the probed read: `unanswerable`, cause `rewritten` (`SYNTHETIC_READ_UNANSWERABLE_CAUSES` +1, engine-internal). The changed-file notice reads it as no objection (unchanged direction). Every answer but `unanswerable` names the location the layers judged (`judged`), and the attachment read is of the object held open there: the path is resolved once to its canonical key, the object at the key is opened and HELD first (the leaf link not followed; right after the open the key must still be canonical and name the object opened), judged while held, and then every byte, the size and a directory's entries come from that held object — a file unlinked and recreated, an ancestor swapped or a large PDF replaced after the open still reads, sizes or lists the object that was opened; a judged location other than the key ⇒ not attached. A PDF over the inline size is a reference: its size from the held object, its page count unknown, no byte read. An environment without the member ⇒ not attached, the frame says it cannot bind (the large PDF included). **One optional `FileSystem` member, `openBound(path)`, returning the held object (`BoundFileObject`: `identity`, `kind`, `size`, `readBytes(max)`, `list`, `close`; the type exported).** `readBytes` takes the lane's own byte cap (the cap the size check admitted against — the text, image or PDF-count cap): a held file grown past it after it was sized, sparse growth included, or a size-0 stream that passes it, is refused (`invalid`) without buffering more than the cap. The node environment lists a held directory through its descriptor on Linux; on darwin (no open-directory enumeration in Node) it lists the path once and requires the path to name the held object afterwards (KL-1311a2-g); win32 holds files only. The open-time check is made on the path (KL-1311a2-h).
|
|
78
|
+
- **One rule for a marked block after data (#1290).** The engine's trailers that follow data in the same message or result open after a blank line, whatever the data ends with (the completions lanes join blocks with no separator): the Read text disclosure trailer (`\n` → `\n\n`), the notebook trailer block, the PDF trailer and the MCP disclosure block now begin with `\n\n`; the frame's notes and closing block too. A hook's feedback block appended to a tool result (`PreToolUse` / `PostToolUse` `additionalContext`) is not on this rule yet: it is appended as its own text block with no leading separator. **Narrowing:** a host comparing those result bytes sees one more newline.
|
|
79
|
+
- **A directory is judged in one place.** `readForModel` stats once after resolution and answers a directory as `kind: "directory"` before the lanes split (it was three refusals, one per lane); the Read tool's sentence is byte for byte the same. Internal.
|
|
80
|
+
- **Who is hurt.** server's compile-exhaustive `UserMessage` key table (above; by its `LeafTable` type a key it does not list is a compile error — inferred from the types, not built against this release), and its `human_input` projection, a key whitelist (`src/trace/project.ts` `humanInputEventData`) that drops `attachments` in silence — no compile error tells it; a resume that re-supplies a config carrying `userAttachments` entries (refused before the claim); a host matching the Read / notebook / PDF / MCP trailer bytes (one more newline).
|
|
81
|
+
- **Compensation.** Declare attachments on a new objective; compare trailer text by its marked block, not by its leading separator.
|
|
82
|
+
- **Consumers.** server — a remote `ExecutionEnv` must implement `openBound` (the held object, the same `BoundFileObject` members) for a path attachment to be attached there (without them every path attachment is "not attached — this environment cannot bind a read to the file that was checked"; host-held text and images are unaffected); request key closure + pass-through of `userAttachments`; a resume's `taskConfig` leaves `userAttachments` out with the objective (`ResumeTaskConfig` omits it; a config that carries an entry is refused before the claim, an empty array is no attachment); the steer route refuses a `userAttachments` key loudly (the engine cannot see a key the route drops); the session-view read treatment (`MESSAGE_READ_TREATMENT.user`) classifies `attachment` (its `label` and `observation.key` carry a path) and `humanInputEventData` carries `attachments`; `/v1/capabilities.userAttachments` from `USER_ATTACHMENT_SEAT` (`supplies` lists `path` only where runs have hands). client-core — the capability reader's present arm; per-attachment split by who holds the bytes (a file the engine can reach ⇒ a `path` entry); the request builder; the `human_input.attachments` mirror; the vocabulary mirrors and the new code. cli — through client-core: path entries on a local engine, the shell's own reads and "already attached" bookkeeping retire for the seat's kinds; attachment cards (including "not attached") from `human_input.attachments`.
|
|
83
|
+
- **Known limits.** KL-1311a2-a (read after the hook; no separate time limit) · b (import always strips the record) · c (a frame whose append fails is not re-sent) · d (the engine cannot force a reachable file onto the `path` arm) · e (a `.pdf`-named file is referenced without a signature check) · f (the data region is the Read bytes, in-line CR / U+2028 / U+2029 raw) · g (darwin: a directory listing is bound by identity before and after one listing, not through the held descriptor) · h (the open-time check is made on the path: an ancestor flipped to a link and back between the open and its check).
|
|
84
|
+
- Pins: `test/backlog1311a2-attachment-frame.test.ts` (U1–U10 the mint over a real reader and judge; D1 the door; R1–R8 real Runner: request body, `human_input`, transcript record, delivery commit, hook, pre-call refusal, steer, no-hands door, `resumeAt` both modes, rebuild in a second process, fork; V1–V3 views, cut points, import, strip; B1 the three brain lanes; B2 Read / notebook / MCP trailers); `test/public-knob-liveness.test.ts` (+`userAttachments`).
|
|
85
|
+
|
|
86
|
+
### A delegated child's start is one frame, `task_started`, minted once per cycle at its spawn station; the lifecycle seat's `spawn` phase is its projection (#1639; **BREAKING**; behavior change; hard-breaking: one, open to veto; closed sets: `TaskEvent` +1 arm; exports: +0 names, 0 removed; members: `TaskEvent` arm `task_started`)
|
|
87
|
+
|
|
88
|
+
**Hard-breaking (open to veto).** (1) **What changed:** `RunnerDeps.onDelegationLifecycle` delivers its `spawn` phase once per started CYCLE — a first spawn or a revival of a completed child — and no longer for a durable RESUME leg; the resume leg still delivers its `terminal` (`identity.legKind: "resume"`), so a parked-and-redeemed delegation reads `spawn, terminal(suspended), terminal` where it read `spawn, terminal, spawn, terminal`. (2) **Who breaks:** a consumer of the lifecycle seat that pairs every terminal with a preceding spawn of the same leg, or counts spawn frames to count legs (no `src/` consumer in server, cli or client-core on their current main; the seat's only outside reader is a test-repo driver that tallies `phase/legKind`). (3) **How to migrate:** count legs by `terminal` frames (one per leg, resume legs included), and treat `spawn` as "a cycle started"; correlate a resume leg's terminal to its cycle by `identity.taskId` / `identity.parentToolCallId`.
|
|
89
|
+
|
|
90
|
+
**Behavior widening — the child row frames have one eligibility.** A delegated leg that has no spawning tool call — an agent of a workflow a host started directly, a deps-only durable resume of a delegated leg, a run driven with the exported delegated-child fixtures — now emits its `task_progress` turn ticks and its terminal tick, where it emitted none: its start, its ticks and its terminal tick all read one predicate (the leg is a delegated child), and the spawning call's id is attribution carried on the frames when it exists, never eligibility. A panel that opens a row from `task_started` therefore gets the tick that closes it, with two exceptions: a run that throws after its start frame and before it has settled ends with the lifecycle seat's `terminal` and no closing tick (KL-1639-c), and a durable pause emits none (the row stays running until the resumed leg's terminal tick). **Who needs it:** every consumer that opens a child row — a row it can open is a row it can close. **Who is hurt:** a consumer that relied on such legs never ticking (the server and client-core `src/` pass no spawning call through run internals, so none was found); a run that sets `parentToolCallId` on its internals without the delegation fact now mints no row frames at all (no core lane does). **Compensation:** none needed.
|
|
91
|
+
|
|
92
|
+
- **Before (v7.49.5; the same in v7.49.0).** A foreground (synchronous) child's first frame that said who it is was its first `task_progress`, minted at the end of its first model turn — a panel opened the child's row one model turn late. Background children had a row-birth frame on the process observer (`BackgroundChildEvent` `spawn`) and every child leg had a lifecycle `spawn` on the deps seat, each emitted on its own; the stream carried no start at all.
|
|
93
|
+
- **Now.** One `TaskEvent` arm, `task_started`, is minted at the child leg's spawn station (`run-identity-wiring.ts`) for every delegation lane — foreground, background, fork, workflow agent, named teammate — strictly after the leg's `wiring_manifest` and before anything its first turn produces, and takes the two doors `task_progress` takes: the child's stream and the forward channel (the parent's forward filter passes it beside `task_progress`; the workflow lane stamps `workflowRunId` / `workflowAgentLabel` on it as on ticks; a background lane's forward tags it with the row's `bgAgentId`). Members are read off values the leg already holds: `taskId` / `sessionId` (run domain), `taskType` and `seq` (each present only where the spawner threaded one: absent on a foreground child's first cycle, as on its ticks; a revived child's start carries the bumped `seq` — the cycle counter `subagent.ts` raises at the wake and hands to the runner), the parentage ids, `name` (the identity envelope's bounded label, absent when it bounds to nothing — the tick's law), `spawnName` / `agentType` (the spawner's seats the ticks and the fleet row read), the prepared `model`, `startedAt`, and the event identity. Once per cycle: a revival mints again with the bumped `seq`; a durable resume leg mints nothing; a child whose prepare throws mints nothing. Live only — never persisted, never in a checkpoint. The lifecycle `spawn` phase is delivered from the same mint, under the same gate. `BackgroundChildEvent` `spawn` is unchanged: it announces a fleet ROW's birth (at launch, before the run prepares; `wa*` rows include agents replayed from the journal that never run), which is a different fact.
|
|
94
|
+
- **Who is hurt.** See the hard-breaking paragraph for the lifecycle seat. A consumer with an exhaustive switch over `TaskEvent["type"]` (server's two arm gates, `src/trace/core-keyset-guard.ts` and `src/trace/model-segment.ts`; client-core's event view, exhaustive over the SDK union and red after the SDK bump; cli has no such switch — its `task_started` hits are CC's SDK `system/task_started`) fails to compile until it handles `task_started`; a deployment's forward sink receives one more frame per child cycle, ahead of the child's ticks.
|
|
95
|
+
- **Compensation.** Project `task_started` to open the child's row (CC's `system/task_started`: `task_id` → `taskId` or the `bgAgentId` tag, `tool_use_id` → `parentToolCallId`, `description` → `name`, `subagent_type` → `agentType`, `is_backgrounded` → `taskType` present, `run_id` → `seq`); a consumer that ignores unknown frames is unchanged.
|
|
96
|
+
- **Pins.** `test/backlog1639-task-started-frame.test.ts` (own-stream order and members; foreground / named / fork / background / workflow lanes forward it first; background row birth precedes it; revival re-mints with `seq` 2; a durable resume leg and a journal-replayed workflow row mint none; none in the checkpoint; a depth-2 grandchild's start crosses both forward filters, nested under the child; member parity with the leg's own ticks; a directly-started workflow agent's row closes with a terminal tick, completed and failed; a spawning call id without the delegation fact mints nothing); `test/delegation-lifecycle-seat.test.ts` (two cells re-pinned: a resume leg delivers its terminal only); `test/defectscan-task-event-surface.test.ts` (arm table +1); `test/streaming.test.ts`, `test/task-progress-tasktype-discriminator.test.ts`, `test/backlog802-forward-sink-containment.test.ts` (fixtures re-pinned to the shape every core lane threads: the delegation fact beside the spawning call id); `test/subagent-content-forward.test.ts`, `test/text-end-447.test.ts`, `test/backlog741-tool-progress.test.ts` (the default forward is the row family: start + ticks); `test/design393-s2-run-lanes.test.ts`, `test/design393-s5-run-seat-lanes.test.ts` (the retired `subagentName` / `parentToolCallId` phase seats).
|
|
97
|
+
### Memory consolidation asks the person's path rules for every write it makes: one write judge, required at the verb (#1653; **BREAKING**; behavior change; hard-breaking: two, open to veto; closed sets: none; exports: +4 names, 0 removed; members: `RunMemoryConsolidationOptions.writeJudge` required, `ConsolidationDriverEngine.consolidationWriteHome` required, `opts.writeGate` required on `MemoryEngine.commitConsolidationPlan` / `reconcileConsolidation` / `resolveConsolidationPlan`, on `driveConsolidationToFixpoint`'s options and on the `ConsolidationDriveEngine` verbs; +4 optional: `MemoryBackend.admitsLandings?`, `applyPatches` `opts.admit?` / `opts.confirm?`, `ConsolidationPlanFile.applier?`)
|
|
98
|
+
|
|
99
|
+
**Hard-breaking, the second (open to veto).** (1) **What changed:** every verb that may apply a plan carries the write gate — `commitConsolidationPlan(token, proposal, { requestId, writeGate })`, `reconcileConsolidation(scope, { writeGate })`, `resolveConsolidationPlan(planId, action, { requestId, writeGate })` (the `discard` valve too: one shape for the verb), `driveConsolidationToFixpoint(engine, scope, { …, writeGate })`; a gate that is absent or not a function refuses with `ConsolidationRefusedError` code `config.memory_consolidation_write_gate` before the verb claims a seat, freezes, claims a plan or writes anything. Before (v7.49.5), these verbs took no gate at all: every apply was ungated (on the File backend, several products in one commit record its recovery replays without judgment). (2) **Who breaks:** a host that calls these verbs itself (a maintenance endpoint, the host valve) or implements `ConsolidationDriveEngine` — a compile error and, untyped, the coded refusal. Neighbour read (origin/main, read-only): sema-server calls `runMemoryConsolidationDriver` only (it already passes the judge) — its `resolveConsolidationPlan` hits are a contract document and a test description; sema-cli, client-core and sdk have none. (3) **How to migrate:** pass the gate a write runs under; a host that holds no rules passes `async () => {}` and so says it.
|
|
100
|
+
|
|
101
|
+
**Hard-breaking (open to veto).** (1) **What changed:** `runMemoryConsolidationDriver` now requires `opts.writeJudge` — a seat that answers, for one target `{ scope, path }`, whether the run may write there — and asks it in two places over a run: for the library directory before it claims a run account, takes the engine's seat or makes a model call (an early refusal that spends nothing), and — the authority — for every entry file a plan would write or remove, where the plan is APPLIED, whichever entry applies it (a commit, `reconcileConsolidation`'s replay of a suspended plan, the drive's recovery, the valve's retry): each product's final spelling (the backend resolves it, a collision suffix included), each directed change's file (a rewrite's target and the file it leaves when it moves the entry, the file a forget removes), all of them asked before any of the plan is written (the directed targets up front, as the store holds them; the products at the backend's apply), and a spelling that moves while the judge is awaited is asked about again, never renamed after it was judged. Anything but `no_objection` (a deny, an ask nobody is present to answer, an unreadable rule source, a throw, a value outside the closed set) refuses with `memory.consolidation_write_refused`: at the first question the run never starts; at the apply the pass writes nothing, the plan is DISCARDED (a suspended plan is no grant that outlives the rule that now forbids it) and the seat and lease are released — products an earlier pass of the same plan had landed before a crash or a failure cut it short stay, are booked applied and are NAMED in the refusal (it never says nothing was written when something was) — and the run settles `refused` with the code in `stopDetail` and the existing partial-fold `memory.consolidation_incomplete` notice — the same receipt when the refused apply is the replay of a plan an earlier run left seated, refused before this run's first cycle (it used to throw out of the run) (cycles that already committed stand: the drive is add-only and has no cross-cycle rollback). An absent or non-function seat is refused (`config.memory_consolidation_driver`), never read as "no rules apply"; `resolveMemoryConsolidationDriver(deps)` returns the options without `writeJudge`. The Runner's auto lane passes the leg's own judge, so a person's standing `Edit(<path>)` deny or ask row or `Read(<path>)` deny row (a `Read(<path>)` ask row speaks for reads only, not for this write) — a directory rule, a glob, or one product file — and the deployment's caller / inherited policies now stop the background run wherever they stop a model's own `Write` to that path (judged on the path alone: the probe is a `Write` of that path with empty content, so a rule that reads what is written is not applied to the bytes) — and, because nobody is present to answer it, wherever that `Write` would only be asked: a caller-policy ask that `bypassPermissions` releases on the live gate refuses the run, and so does a person's own `ask` row, which the live gate would put to the person present; the judge consults the layers a refusal can come from (the skill rows, the deny-narrowing fold, the caller and inherited policies, the person's lane, the org lane) and not PreToolUse hooks, the permission mode's write gate, the integrity guards or the write-protection table, which a live `Write` meets; an org partition names no path — its grammar carries `Bash` rows only — and stops the run only when it cannot be read: governance unavailable. (2) **Who breaks:** a host that calls `runMemoryConsolidationDriver` itself (a maintenance endpoint, a script) — a compile error, and at runtime `config.memory_consolidation_driver`; a host that spreads `resolveMemoryConsolidationDriver(deps)` into that call; a host that implements `ConsolidationDriveEngine` / `ConsolidationDriverEngine` itself (the latter gains the required `consolidationWriteHome(scope)`); a deployment whose person-rule or policy names (or asks about) a path in the memory library (an org layer names no path), including an approval-type caller policy that asks on every write — its automatic consolidation now stops, at each task end while a recommendation is owed, with a `memory.consolidation_refused` notice (`detail.refusalCode: "memory.consolidation_write_refused"`) when the library directory is named and a `stop: "refused"` `memory.consolidation_incomplete` notice when only a product file is. (3) **How to migrate:** pass `writeJudge` — a host that holds no rules passes `async () => ({ verdict: "no_objection" })` and so says it; a host that holds rules answers `no_objection` / `deny` / `ask` / `unanswerable` per target path; to let the run write, remove or narrow the rule. Catch `ConsolidationRefusedError` whose `code` is `CONSOLIDATION_WRITE_REFUSED_CODE` (new export) and convert it with `memoryConsolidationRefusedNotice`.
|
|
102
|
+
- **Before (v7.49.5; the same in v7.49.0).** A model's `Write` into the memory library met the person's path rows and the deployment's policy at the gate; the background consolidation run committed into the same library through the engine without reading either. "May this path be written?" had two readers, and a person who denied `Edit(//<library>/**)` still saw the run fold their library.
|
|
103
|
+
- **Now.** One judge body, two faces (`createSyntheticReadJudge` / `createSyntheticWriteJudge`): the same layers (skill rows, the deny-narrowing fold, caller and inherited policies, the person's persisted lane, the org lane), the same reads, one walk, one precedence (a deny anywhere beats a held ask, a held ask beats a layer that could not answer; the walk goes on past an ask). The faces differ only in which asks they hold: the write face holds every ask (no approver is present; it answers `ask`), the read face only a mandated one (`mandated_ask`); a persisted store that cannot be read is `unanswerable` on both rather than silent. It judges a `Write` carrying the facts a live write carries (the roster's `Write` face, tracked cwd and root, the advertised writable directories, the data root's exemption); it reads the person's rules and the org's through the same two lanes built over the reader the run's cut does not end (the run's signal has already fired when the settle tail asks; an org partition whose rules cannot be read refuses the run as governance unavailable). The freeze names nothing and asks nothing; the authority is the backend's apply: `MemoryBackend.applyPatches` takes an optional `opts.admit(landings)` and the File backend (which declares the new optional `admitsLandings: true`) plans the batch under its transaction lock, leaves the lock to put every ADD's final `{ id, scope, slug }` to `admit`, and writes only spellings `admit` resolved (up to four rounds if names keep moving, then a coded `memory.landing_unstable`, nothing written). A backend that does not declare it is not handed `admit`: the engine asks the gate about all the frozen spellings, just before the batch (KL-1653-c). Asked whole, an admitted batch COMMITS ONE LANDING AT A TIME — the File backend cuts its transaction per landing, a product or a directed change (each its own journal, so its crash recovery, which replays a journal with nobody to ask, never carries one landing behind another), and a backend without the capability is handed the plan's writes one call each — so a crash between two of them leaves the later one unwritten until the next apply asks about it again; recovery completes at most the one product whose commit had begun (KL-1653-i). What the plan has already put in the store is read back after the store replays its commits: a refusal names those products and the directed changes already in their planned state (booked applied / satisfied), and a product batch that threw credentials the lineage write-ahead of exactly the products the store holds (it used to discard it as if nothing had committed). Every verb that may apply a plan requires the gate (the second hard-breaking paragraph). A plan's application has ONE holder: each apply claims the plan (`ConsolidationPlanFile.applier`, last claimer wins), every write of its book is fenced on the claim, and every commit of the plan's writes (products and directed patches) is fenced on the holder — a backend that declares the capability calls the caller's holder check (`applyPatches` `opts.confirm`) under its transaction lock before each commit, a backend that does not is sent the writes one call each with the check before each (a write already waiting inside such a backend is not fenced: KL-1653-j) — an applier that was taken over, or whose plan was settled meanwhile, stops with `memory.consolidation_busy` and writes nothing (it neither discards nor overwrites a book it no longer holds). A consolidation commit writes product files only (it never writes `MEMORY.md`, which is the harvest's), so the index is not asked about. Unchanged on the Read face: its verdicts and pins (but for the retired-member refusal of the #1293 b entry, which reaches the judge's policy layers too: a layer that answers with a retired `guard` is a deny there, for the attachment seat and the changed-file notice alike); one disclosure fix rides the shared body — a policy that throws is reported `threw` (it was reported `timed_out` with a wait-time sentence).
|
|
104
|
+
- **Who is hurt.** See the hard-breaking paragraph. A deployment with no rule store, no caller policy and no org lane sees no change (the control pins: the run settles). Direct callers read in the sibling checkouts (server main `7f7e6a7c`, 2026-10-12): `sema-server` calls the verb itself in its operator-consolidation boot module (`src/boot/memory-consolidation.ts:915`, with the options it resolved through `resolveMemoryConsolidationDriver` at `:176` / `:246`) and must pass the seat on pickup; sema-cli, client-core, web-client, web-admin and sdk have no direct caller (their hits are the `RunnerDeps` deployment seats, which are unchanged).
|
|
105
|
+
- **Compensation.** None needed for the stopped runs (the recommendation stays owed; remove the rule and the next task end runs it). Direction note: skipping is the tightening direction (fewer writes); the cost is that consolidation waits — the library keeps growing while a rule forbids it, and a rule that names only a product file costs one mint (the library's content goes to the consolidation model) per attempt before the apply refuses it (KL-1653-d).
|
|
106
|
+
- **Pins.** `test/backlog1653-consolidation-reads-write-judge.test.ts` (the verb's order and refusals; the leg seat; the per-file question at the apply — one product, two products in one plan, two cycles, a judge that throws, the `-2` landing name (the final spelling is what is judged); the apply as the authority — a plan left seated by a failing backend is discarded by a later deny, a rule that lands between the freeze and the apply is met at the apply (backends that declare the capability and ones that do not), a rival taking the name while the judge is awaited is asked about again, all spellings before any write; a crash between the two products of one plan, a deny on the second installed meanwhile, then the next run — the second is written by neither the recovery nor the replay and the refusal names the first (backends that declare the capability and ones that do not; a restart in between; the control lands it), the File backend's per-landing commit under a crash with the next locked writer or a fresh construction recovering; directed changes — a forget / rewrite-only plan refused at its files, a product and a forget refused whole, a seated plan's directed replay refused, the account naming a forget already applied, a removal and a rename's two files as landings; every plan-applying verb refusing coded without a gate before touching anything; the judge body's two faces; the real Runner: persisted deny / ask on the directory and on one product file, the index file and another file not binding, caller-policy ask, parity with a model's own write, an unreadable session partition beside a surviving allow, an unreadable durable store with its single notice, an org partition — healthy (the run lands its product), a snapshot naming the product file (a tool the org grammar does not carry: refused whole) and a source that throws (both refused as governance unavailable) —, the live gate's question); the driver tests re-pinned with the seat. Known limits: KL-1653-b / -c / -d / -e / -f / -h / -i / -j / -k.
|
|
107
|
+
|
|
108
|
+
### A rule store (or one partition of it) that could not be read fails closed: every call it serves is a real-approval question (a deny row last read refuses it instead) until the next successful read (#1653 r1; behavior narrowing — reverses the call lane's 7.34.0 skip; hard-breaking: one, open to veto; closed sets: +0 (existing words: the class `governance_unavailable`, the origin `rule_store_unavailable`); exports: +0; members: `AskClassFacts.ruleStoreUnreadable?` (+1 optional, engine-written; neighbour grep: zero))
|
|
109
|
+
|
|
110
|
+
**Hard-breaking (open to veto).** (1) **What changed:** when the person's rule store, or one partition of it, cannot be read, every call the rule lane serves is a real-approval question (class `governance_unavailable`, origin `rule_store_unavailable`; a deny row last read refuses it instead) for as long as the read fails — no mode releases it, `bypassPermissions` included, no allow rule or classifier clears it, a run with no approval face refuses it, `dontAsk` refuses it as `deniedBy: "mode"` (unless a hook's own ask stands in the same set: neither class floors it); in v7.49.5 the lane skipped what it could not read — a failed partition's rows, or the whole store's — and judged the call on the rest, in every mode. (2) **Who breaks:** a bypass or headless deployment that kept running while its rule store was down — each call the lane judges now stops (a person is asked; with nobody to ask, it is refused) until a read of the store succeeds; an operator who relied on `bypassPermissions` to ride out a store outage; a custom `persistedRules` lane that returns `{ unreadable: true }`. (3) **How to migrate:** attach an approver, or repair the store (the run's one `permission.rule_store_unavailable` notice names the sources whose partition could not be read); a deployment that wants no rule store wires none.
|
|
111
|
+
|
|
112
|
+
- **Who needs it.** Anyone whose standing rules live in more than one partition (the durable store and the session overlay), or whose store can fail a read: deny > ask > allow, so no answer but a deny HIT that was read (or one already unreadable) can stand (a surviving ask is no floor a blanket approval could clear) while a partition that may hold a deny or ask failed to read. Two readers needed the bit and each had a hole: the consolidation write judge (an allow beside the failure read as a yes), and every reader of the lane that only saw a surviving allow, a coverage table or nothing and took it for "no rule".
|
|
113
|
+
- **Before (v7.49.5; the same in v7.49.0).** A read of the person's rule store that failed (or a partition that threw) was SKIPPED: its rows took no part (a partition that was read was judged as read; when the whole store failed, the call was judged as if the person had no persisted rows), in every mode — `bypassPermissions` ran the call, `default` ran what the mode runs, and a standing allow from the surviving partition cleared the question. A deny row among the rows nobody could read protected nothing, and nobody was asked. The run said so once (`permission.rule_store_unavailable`).
|
|
114
|
+
- **Now.** The lane's answer carries `{ unreadable: true }` whenever a partition could not be read and the answer is not a deny hit that was read — no hit, a coverage table, a surviving allow or a surviving ask (only a DENY hit that WAS read, or an answer that is already unreadable — the call form, class `rule_unreadable` — stands beside the failure; an allow is no yes and an ask is no floor a blanket approval could clear, so both read as the store form too and become a real-approval ask). The gate turns it into a real-approval ask for every call the lane judges and no deny hit refuses: the decision (allow or ask) acquires `requiresRealApproval`, origin `rule_store_unavailable`, class `governance_unavailable` — the class an unreadable organization policy has: no allow layer, rule or classifier clears it, `bypassPermissions` keeps it for a person, a run with no approval face refuses it, `dontAsk` refuses it as `deniedBy: "mode"` (unless a hook's own ask stands in the same set: neither class floors it). **While a partition read is broken, every rule judgment that is not a refusal becomes an ask until the next successful read** (every later call reads the store again). The run still says so once. What the call's own tables hold while a partition cannot be read is the failure law of the lifetime entry above (#1293 b): the deny and ask rows of the last successful read still apply, its allow rows do not. Both faces of the synthetic judge now read it as `unanswerable` (`store_unreadable`): the write face refused before (consolidation); the read face used to say "no part", and it has two readers in this tree. The changed-file notice acts on `deny` alone (no change there). The user-attachment seat (#1311) attaches a file the engine reads for the user (a `path` entry) only on `no_objection`, so `unanswerable` is `not_attached`: the user's own path attachment is withheld while the store is down (host-held text and images are not read by the engine and are unaffected) — when the judge's own read of the store fails the frame says the engine could not confirm the file may be read, and while the run's latest read of the store stands unread the reader that resolves the target refuses first and the frame carries that refusal (`test/backlog1311a2-attachment-store-down.test.ts`). The resumed-call belt is unchanged: a parked call's approval already answered that call's question, so a store that cannot be read at resume does not veto it (only a call unreadable against the rows does). The origin word `rule_store_unavailable` now names two mechanisms again — the call could not be read against the rows (class `rule_unreadable`) and the store could not be read (class `governance_unavailable`), both bypass-immune since the next entry; a renderer that reads the origin word alone shows the call wording for both, the ask's own message and class tell them apart (the retired `ruleStoreUnreadable` wire member stays retired). The notice sentences name the new posture.
|
|
115
|
+
- **Who is hurt.** A deployment whose rule store or a partition fails to read while it runs unattended: its calls were being judged without the failed rows; they now stop for a person (headless: refused) for exactly as long as the read fails, and a user's own path attachment (#1311) is not attached for the same stretch. An operator who relied on `bypassPermissions` to ride out a store outage now sees the questions the outage hides rows behind. Custom `persistedRules` lanes that return `{ unreadable: true }` get the same treatment (it is the lane's documented "could not read" word). *Reverses part of the 7.34.0 skip (the reference implementation skips a settings source it cannot read and goes on): the decision is no longer "as if the person had no rows"; what the tables hold after a failed read is the failure law of the lifetime entry above (#1293 b), and the notice says so.*
|
|
116
|
+
- **Compensation.** None beyond restoring the read — it is the tightening direction and only on a failed read; the notice names the rules and the sources that were skipped. Operators who want the old reading can decline to wire a rule store.
|
|
117
|
+
- **Pins.** `test/backlog1653-consolidation-reads-write-judge.test.ts` (real provider, real lane, real Runner: a write × `bypassPermissions` × the session partition down × a durable allow only ⇒ asked / refused with no approval face / granted ⇒ allowed; `Read` the same; no rows at all ⇒ asked for `Read` and `Write`; both partitions readable ⇒ the old answers; `dontAsk` refuses with the class; a deny that WAS read still refuses; the store back for the second call ⇒ asked once, one notice; the consolidation run refused before any claim); re-pinned: `backlog7340rj-unread-path-rows` ①②④⑦, `backlog625-tightening-floors` ④⑪, `backlog688-contract-batch` C3, `backlog803-read-root-candidate`, `backlog1311a-read-layer`, `rev7340rm-rule-store-partitions` (notice sentence); `test/backlog1311a2-attachment-store-down.test.ts` (a real Runner with a durable partition that throws: the user's attachment is `not_attached` — from the run's start, by the reader's refusal; failing only at the attachment's own judgment, by the judge's `unanswerable`; the bytes never reach the request; the healthy control attaches).
|
|
118
|
+
|
|
119
|
+
### A rule the call could not be read against is a question no mode releases: `rule_unreadable` is bypass-immune and leaves `MODE_RELEASE_REASONS` (#1653 r2; **BREAKING**; behavior narrowing; hard-breaking: one, open to veto; closed sets: `MODE_RELEASE_REASONS` −1 (`rule_unreadable`), `ModeReleaseReason` narrows with it; exports: 0 names, 0 removed; values: `MODE_ARBITRATION.rule_unreadable.bypassImmune`, `MODE_RELEASE_REASONS`, `releasableAskClassesFor(face)` (−`rule_unreadable`))
|
|
120
|
+
|
|
121
|
+
**Hard-breaking (open to veto).** (1) **What changed:** the class `rule_unreadable` — a call the persisted-rule lane or a content-rule policy could not read against a deny / ask row that may reach it — was released by `bypassPermissions` with a disclosed `released.reason: "rule_unreadable"`; it is now bypass-immune like the store form's `governance_unavailable`: `MODE_ARBITRATION.rule_unreadable.bypassImmune` is `true`, `MODE_RELEASE_REASONS` is `["target_unresolved"]` (the exported frozen array, the `ModeReleaseReason` type and `isModeReleaseReason` lose the word), `releasableAskClassesFor("shell" | "other")` no longer lists the class, and a `released` record never carries `askClass: "rule_unreadable"` or that reason. (2) **Who breaks:** a consumer that mirrors the table or the reason set and asserts the old release — client-core's generated upstream tables and its automatic-answer verdict (a class set `[rule_unreadable]` read `may_answer` with a `releaseReason`; it now reads must-ask), sema-server's wire fixtures that build a `released` outcome over that class (`screenGateOutcome` I7 now names it not releasable), and any host that relied on bypass riding over an unreadable rule; a consumer holding the two-word reason list (the sdk's `ModeRelease.reason` union, the shell's generated release-reason mirror) keeps a superset and breaks nothing. (3) **How to migrate:** regenerate the mirrored tables from this release's exports (the row's value moves, its shape does not), drop `rule_unreadable` from released-record fixtures and from any "bypass releases" expectation, and treat the class like `governance_unavailable` — a person is asked in every mode but `dontAsk` (refused as `deniedBy: "mode"`, unless a hook's own ask stands in the same set), a run with no approval face refuses it.
|
|
122
|
+
- **Who needs it.** Anyone running unattended under `bypassPermissions`: a call nobody could read against the person's own deny / ask rows ran, with a footnote, while the same fact about the store ("the rules could not be read") now stops the call — one kind of fact (the rules did not answer), two postures, was two readings of a single thing.
|
|
123
|
+
- **Before (v7.49.5; the same in v7.49.0).** `bypassPermissions` released an ask of class `rule_unreadable` and named it on the disposition; every other mode asked (`dontAsk` refused it, as it refuses any ask).
|
|
124
|
+
- **Now.** Every mode that asks asks, `bypassPermissions` included; `dontAsk` refuses it (unless a hook's own ask stands in the same set); a run with no approval face refuses it. The release records, the once-per-run `permission.ask_released_by_mode` notice and the `reason` word are gone for this class; the call form's wording on the card is unchanged.
|
|
125
|
+
- **Who is hurt.** A bypass-mode unattended deployment whose rule lane cannot read a call against its own rows (a malformed row beside a deny, an expansion where a rule reads a word, a target no base locates): those calls stop instead of running with a disclosure, for as long as the unreadable fact stands. With #1293 a, a leg whose environment declares no home (`homeDir` undeclared — server declares none for its k8s, local-docker, ssh, adb and device lanes unless `REMOTE_EXEC_HOME_DIR` is set; DEBTS-server S-1519, 7.127.0) and a person's `~/…` deny row is the widest case: the row cannot be placed without a home, so every file-tool call it speaks for is a call the lane cannot read against its rows, and under `bypassPermissions` Read, Grep and Write each ask a real approver and are refused where nobody can answer, where v7.49.5 released the Read with a disclosure and ran Grep and Write unasked.
|
|
126
|
+
- **Compensation.** Fix or remove the row the call cannot be read against, or give the call a base; a readable deny row still refuses on its own, and the call runs again as soon as the lane can read it (per call, nothing latches).
|
|
127
|
+
- **Pins.** `backlog1111-permission-mode-arbitration` (the releasable list without the class, the immune list with it, U1 kept under bypass), `backlog1212-mode-release-reasons` (the reason set is `target_unresolved` alone; `rule_unreadable` is no reason; R5 a real lane under bypass asks), `backlog1423-wpu-by-face` (the shell face, the immune companion), `backlog1214-ask-class-on-request`, `backlog1376-relative-target-no-base` L5, `rev7340rm-rule-store-partitions` R3 ①, `backlog7340rj-unread-path-rows` ⑦, `closed-sets-frozen-exports` (a set may shrink to one word).
|
|
128
|
+
|
|
129
|
+
### `tool_end.output` keeps every block in its slot: admitted images and documents ride by block under the leg's media budget, and the 16384 cap counts text characters, cutting in place (#1668; **BREAKING** wire shape; hard-breaking: one, open to veto; closed sets: none; exports: +0 names, 0 removed; members: none public)
|
|
130
|
+
|
|
131
|
+
**Hard-breaking (open to veto).** (1) **What changed:** one rule for a blocks array: every block keeps its slot, and `truncated` is `true` exactly when `output` is not the content the tool returned. **Media:** an `image` / `document` block no longer degrades to a single `[image: image/png]`-style string when the content's serialized size passes 16384; it rides `tool_end.output` whole, in its slot, until the event's media (each block's serialized UTF-8 size on the wire) has spent what is left of ONE ceiling for the whole output. That ceiling is the leg's media budget — `RunnerDeps.mediaByteCapBytes`, the budget the request strip reads, through the same `mediaBudgetBytes` (default `AGGREGATE_MEDIA_BUDGET_BYTES` = 30 MiB decoded; `Infinity` / `0` = no budget) — spent as base64 (x 4/3: **41 943 040 bytes on the wire at the default**), after the text part's serialized size. A block past it becomes the descriptor `{ type, mimeType?, bytes, truncated: true }` in its slot (never a string; a media type over 255 characters is not echoed); descriptors spend the same budget, and when not even a descriptor fits, that block and every later media block fold into ONE `{ type: "omitted", blocks, bytes, truncated: true }` in its slot, so the media in an event is bounded at any block count. **Text:** the 16384 cap counts characters, never serialized length. The text blocks — each at its text plus whatever else it carries, at its serialized length (the contract's text block carries nothing else) — and any block that is neither text nor admitted media, at its serialized length, are one stream, with one character between consecutive blocks. Within the cap they ride as the very objects; escapes and framing no longer trip it. Over the cap, the block where the stream passes 16384 is cut IN PLACE to `{ type: "text", text }`: its first characters, then `…[+N chars truncated]`, where N counts every stream character not shown. The stream's later blocks leave. A text-only array over the cap is therefore an array cut in place, no longer a string. A tool's bare-string return reaches the projection as a one-text-block array, so this is every large text result. `totalChars` stays the sum of each block's own size. (2) **Who breaks:** a consumer that reads `truncated: true` as "`output` is a string" — a truncated result now arrives as a block array (only a body that is itself a string — a refusal or reconcile text, or the unreadable-content string — stays a string); a consumer that assumes a result carrying an image is a string once it is large; a consumer that bounds frame or replay size by the 16384 cap — one event can now carry up to the leg's media budget on the wire, 41 943 040 bytes at the default. (3) **How to migrate:** handle both shapes of `output` (it was always `string | blocks`); render a block with `truncated: true` and no `data` as "one PNG, N KB, not delivered" from its `mimeType` and `bytes`; bound a frame by the blocks' `data` lengths, not by the cap; a durable consumer may store each media block as its descriptor (`type`, `mimeType`, `bytes`) and drop `data`.
|
|
132
|
+
|
|
133
|
+
- **Before (v7.49.5).** `toolOutputFrom` measured the serialized size of the WHOLE content, base64 included: a screenshot (hundreds of KB) always tripped the 16384 gate, and the result became the string `[image: image/png]` with `truncated: true` — the SSE / replay surface never carried a real image although the model had been shown it (admission and the media byte cap already bound what reaches the model). A text-only array whose serialized size passed 16384 was re-rendered as ONE string, its blocks joined by newlines, even when its characters fitted (`truncated: false` then).
|
|
134
|
+
- **Now.** The one rule above. A string result is byte-identical to v7.49.5. A text-only array within the cap in characters rides as-is; over it, the array is cut in place. Media blocks are measured against the leg's one budget — each block's serialized UTF-8 size (payload, type, media type, any other member, escapes), first-fit in block order — and pass as the very objects the tool returned. Content that cannot be read or serialized (a throwing accessor, a cycle) still answers `"[unserializable tool output]"`, now with `truncated: true` (it is not the returned content; v7.49.5 said `false`) and no `totalChars` (its size could not be read). A media block past the budget carries `truncated: true` on its own descriptor; `bytes` is the engine's decoded-size measure and `mimeType` the block's own media type, both kept. `totalChars` (iff `truncated` and the content could be read) is each block's own size: text `.text.length` plus whatever else the block carries, media `.data.length`, any other block its serialized length — never the JSON length of the whole content. Two engine readers hand `output` to a model: the observer digest, and the sub-agent step outcome (the parent's `<recent-steps>` and the auto-mode handback review's `toolSteps`). Both render a block through the one block rendering (`renderContentBlock`), which the projection's cut block, the auto-mode classifier window and the user-attachment view also use: a text block's text, any other block its `[type: mimeType]` marker, never its payload. A child's screenshot `Read` therefore reads `Read <path> → [image: image/png]` in the parent's step tail (v7.49.5 showed this for a large image; the 7.50.0 stage had dropped it). C-R441: CC 2.1.296 drops non-text blocks from the observer digest; the marker keeps one rendering rule and is what 7.49.5 showed for a large image.
|
|
135
|
+
- **Who is hurt.** See the hard-breaking paragraph. **server**: the durable ledger persists every `tool_end` row with `output` in one SQL INSERT, and TiDB's default entry wall is 6 MiB. Server stores media blocks as descriptors (`type` / `mimeType` / `bytes`) and strips `data` before `redactDeep`; the live stream keeps whole blocks (S-1518, 7.127.0, in lockstep with this release). **cli** and **client-core**: the transcript / render path receives real image and document blocks it never saw on a large result, and a truncated text result as a one-block array. client-core reads both shapes through one text reading, `toolEndOutputText`. Its transcript shape is CC's `{type:'image', source:{type:'base64', media_type, data}}`; the descriptor keeps the engine's `mimeType` spelling for the consumer to convert at its own boundary. A read of server `57356167`, cli `68aae766` and client-core `28015df` (origin/main) found no `tool_end.output` reader that assumes a string. One downgrade on that read: client-core's block reader (`src/toolResultContent.ts` `blockOf`) passes text and four image media types only, so a `document` block, a media descriptor and the `omitted` summary fold into one trailing `[unsupported content omitted]` note where v7.49.5 showed `[document: application/pdf]`. On pickup, client-core renders a descriptor from its `mimeType` and `bytes`, and the summary from `blocks` and `bytes`. **sdk**: the `tool_end.output` media block, descriptor and summary shapes ride the 7.50.0 pickup (server K-79). **Direct callers of the media budget**: `capAggregateMediaBytes` (package root) now reads `limitBytes: undefined` as the 30 MiB default, where v7.49.5 left the messages uncapped. Only an untyped or cast call can pass `undefined`; the engine itself always passes a number, and no sibling repo calls it. A deployment whose `RunnerDeps.mediaByteCapBytes` is a non-number, `NaN` or negative is now refused at `new Runner` (`config.invalid_argument`); v7.49.5 silently read it as "no budget". `0` and `Infinity` still mean "no budget". The deps are read ONCE at `new Runner` into the plain-data copy the Runner keeps — every own seat, enumerable or not, each getter invoked once, each DEFINED on the copy as a data property so no setter ever runs (an own `__proto__` key installs nothing) — and the value doors judge that copy. A deps object that is not a plain object (a class instance: a seat its prototype would supply is never read) is refused (`config.invalid_argument`) rather than wired without that seat; v7.49.5 kept the caller's object by reference when it had nothing to add and spread it otherwise, so a non-enumerable or inherited seat worked or vanished by accident. A deployment that mutates its deps object after construction is not seen (`swapDeps` is the path). No sibling repo constructs a Runner from anything but a literal (server `src/boot/runner-deps.ts`, `src/bench/s1/live-deps.ts`).
|
|
136
|
+
- **Compensation.** None for text consumers beyond the array shape. A host that wants a small frame drops `data` from the media blocks it receives; the descriptor (`type`, `mimeType`, `bytes`) is the durable form. `output` is for observability. The engine's two model-facing readers (the observer digest, the sub-agent step outcome) render media as their markers, never their bytes; build model prompts from the session. No new knob: the budget is the deployment's existing `mediaByteCapBytes`.
|
|
137
|
+
- **Pins.** `test/backlog1668-tool-end-image-blocks.test.ts` (text cut in place × image rides; image-only / document identity; first-fit descriptor; order; `totalChars`; the block-count bound under a deployment budget; real Runner Read of a PNG; live vs durable-resume frame bytes; every cell has its own timeout); `test/backlog1668-one-rule.test.ts` (the observer digest of a screenshot is exactly `[image: image/png]`; every block kind through the one renderer; the leg's budget on the projection, the live frame and the durable-resume frame, and the four disabled forms; a caption, an image and long text keep their slots; escape-heavy text within the cap rides as-is; a text-only array is cut in place; the empty-block bound; a 2 MB member on a text block cannot ride; unreadable content is flagged with no `totalChars`; `truncated` iff the output is not the returned content; a child's screenshot step reads `[image: image/png]` on the live and the pull face and in `<recent-steps>`; the auto-mode window and the user-attachment view render the same marker; a non-number, `NaN` or negative `mediaByteCapBytes` is refused at `new Runner` while absent, a positive number, `0` and `Infinity` are accepted; the deps are read once at `new Runner` into the copy the Runner keeps — a getter that answers 1024 and then `NaN` is held at 1024); `test/streaming.test.ts` RB-210 cells re-pinned (a text-only array over the cap is an array; framing alone rides as-is). Known limits: KL-1668-b, KL-1668-c; KL-1668-a closed in this release (the projection reads the leg's budget).
|
|
138
|
+
|
|
1
139
|
## 7.49.5 — 2026-10-13
|
|
2
140
|
|
|
3
141
|
**Hard-breaking changes (#1701):** none. **Closed sets:** none. **Package exports:** +0 / −0 names (`posixModeBitsMeaningful` lives in `src/core/posix-mode-face.ts` and is not on the package index); member changes, all optional: `vetPeerRegistryDirectory(dir, { platform? })`, `judgePeerRecordFile(stat, selfUid, platform?)`, `new FileDurableRulePartitionProvider(dir, onError, { platform? })`. **Behavior:** on Windows the rule store and the peer registry stop reading the platform's `0o666` as "writable by group or others" — a loosening on exactly one platform, and the one that restores the store's purpose there (open to veto: it departs from "refuse what you cannot prove safe" for that platform); POSIX hosts are unchanged.
|
package/dist/agents/observer.js
CHANGED
|
@@ -2,6 +2,7 @@ import { Type } from "typebox";
|
|
|
2
2
|
import { defineTool } from "../core/tools.js";
|
|
3
3
|
import { createSafeNotifier } from "../core/safe-notify.js";
|
|
4
4
|
import { escapeEnvelopeTag } from "../core/untrusted-text.js";
|
|
5
|
+
import { renderContentBlock } from "../internal/llm.js";
|
|
5
6
|
import { toolFace } from "../core/tool-catalog-entries.js";
|
|
6
7
|
import { isErrorLike, thrownName, thrownText } from "../core/thrown-value.js";
|
|
7
8
|
export const OBSERVER_EVENT_TAGS = Object.freeze(["tool-call", "user-message", "tool-result", "turn-ended"]);
|
|
@@ -107,16 +108,8 @@ export function resolveObserverDeclaration(args) {
|
|
|
107
108
|
function digestTextOf(output) {
|
|
108
109
|
if (typeof output === "string")
|
|
109
110
|
return output;
|
|
110
|
-
if (Array.isArray(output))
|
|
111
|
-
|
|
112
|
-
for (const block of output) {
|
|
113
|
-
const b = block;
|
|
114
|
-
if (b?.type === "text" && typeof b.text === "string")
|
|
115
|
-
texts.push(b.text);
|
|
116
|
-
}
|
|
117
|
-
if (texts.length > 0)
|
|
118
|
-
return texts.join("\n");
|
|
119
|
-
}
|
|
111
|
+
if (Array.isArray(output))
|
|
112
|
+
return output.map(renderContentBlock).join("\n");
|
|
120
113
|
if (output === undefined || output === null)
|
|
121
114
|
return "";
|
|
122
115
|
try {
|
|
@@ -1,17 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Sub-agent residual-observability recorder (lanes A/B/D — CC harness parity)
|
|
2
|
+
* Sub-agent residual-observability recorder (lanes A/B/D — CC harness parity): a parent resuming or judging a stopped
|
|
3
|
+
* child gets a compact step tail, the child's edited files and a one-line "currently doing" string, derived from the
|
|
4
|
+
* child's live `TaskEvent` stream (the `onForwardEvent` sink the display / tick lanes tap) — a pure observer.
|
|
3
5
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* stopped at file X, step Y"). CC's harness feeds the parent a compact step tail + the child's edited
|
|
7
|
-
* files + a one-line "currently doing" progress string. This recorder derives those three from the
|
|
8
|
-
* child's live `TaskEvent` stream (the same `onForwardEvent` sink the display/tick lanes already tap),
|
|
9
|
-
* so no new engine plumbing is needed — it is a pure observer.
|
|
10
|
-
*
|
|
11
|
-
* Scope: ONE direct child. A child that itself spawns a grandchild re-threads the same forward sink,
|
|
12
|
-
* so grandchild events bubble through here too. Scoping is EXPLICIT, not first-come-first-served
|
|
13
|
-
* (CORE-2 — the old "first `sourceTaskId` seen" lock assumed direct-child content always precedes
|
|
14
|
-
* any grandchild frame, an ordering the shared sink does not guarantee):
|
|
6
|
+
* Scope: ONE direct child. A grandchild re-threads the same forward sink, and the sink does not order direct-child
|
|
7
|
+
* content before grandchild frames, so scoping is EXPLICIT:
|
|
15
8
|
* 1. the spawn site calls {@link SubagentStepRecorder.lockTo} the moment the child's taskId is known
|
|
16
9
|
* (fork lanes: the forked sessionId; retained lanes: the pre-minted child sessionId) — after that,
|
|
17
10
|
* only `sourceTaskId === lockedSource` frames are recorded;
|
|
@@ -21,8 +14,7 @@
|
|
|
21
14
|
* id (a grandchild's frames carry the child's delegation id) — so the provisional lock only seeds
|
|
22
15
|
* from a matching frame;
|
|
23
16
|
* 3. without either anchor (bare construction — tests/legacy), the first-source lock applies as before.
|
|
24
|
-
* Everything is BOUNDED (step ring ≤ {@link STEP_CAP}, fields truncated)
|
|
25
|
-
* balloon the parent's notification/report.
|
|
17
|
+
* Everything is BOUNDED (step ring ≤ {@link STEP_CAP}, fields truncated): a pathological child cannot balloon the parent's view.
|
|
26
18
|
*/
|
|
27
19
|
import type { TaskEvent } from "../core/types.js";
|
|
28
20
|
/** One completed tool step in the child's tail — enough for the parent to say "you stopped at X". */
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { stripMarkedReminderBlocks } from "../core/reminder-mint.js";
|
|
2
|
+
import { renderContentBlock } from "../internal/llm.js";
|
|
2
3
|
import { redactSecrets } from "../core/untrusted-egress.js";
|
|
3
4
|
import { isProtectedWrite } from "../core/tool-registry.js";
|
|
4
5
|
import { writeTargetPath } from "../tools/fs/safety.js";
|
|
@@ -68,12 +69,7 @@ function editTargetPath(toolName, args) {
|
|
|
68
69
|
function outputFirstLine(output) {
|
|
69
70
|
if (typeof output === "string")
|
|
70
71
|
return clip(output);
|
|
71
|
-
|
|
72
|
-
const t = output.find((b) => b && typeof b === "object" && typeof b.text === "string");
|
|
73
|
-
if (t)
|
|
74
|
-
return clip(t.text);
|
|
75
|
-
}
|
|
76
|
-
return "";
|
|
72
|
+
return Array.isArray(output) ? clip(output.map(renderContentBlock).join("\n")) : "";
|
|
77
73
|
}
|
|
78
74
|
export class SubagentStepRecorder {
|
|
79
75
|
lockedSource;
|
|
@@ -169,7 +165,7 @@ export function stepsFromMessages(messages, lastN) {
|
|
|
169
165
|
for (const m of messages) {
|
|
170
166
|
const msg = m;
|
|
171
167
|
if (msg.role === "toolResult" && typeof msg.toolCallId === "string") {
|
|
172
|
-
results.set(msg.toolCallId, { isError: msg.isError === true, body:
|
|
168
|
+
results.set(msg.toolCallId, { isError: msg.isError === true, body: outputFirstLine(msg.content), at: stampOf(msg) });
|
|
173
169
|
}
|
|
174
170
|
}
|
|
175
171
|
const steps = [];
|
|
@@ -191,13 +187,3 @@ export function stepsFromMessages(messages, lastN) {
|
|
|
191
187
|
const cap = Math.max(0, Math.min(lastN, steps.length));
|
|
192
188
|
return steps.slice(steps.length - cap);
|
|
193
189
|
}
|
|
194
|
-
function outputFirstLineFromContent(content) {
|
|
195
|
-
if (typeof content === "string")
|
|
196
|
-
return clip(content);
|
|
197
|
-
if (Array.isArray(content)) {
|
|
198
|
-
const t = content.find((b) => b && typeof b === "object" && typeof b.text === "string");
|
|
199
|
-
if (t)
|
|
200
|
-
return clip(t.text);
|
|
201
|
-
}
|
|
202
|
-
return "";
|
|
203
|
-
}
|
|
@@ -14,7 +14,7 @@ import { type AssistantMessage, type ModelOutputInvalid, type StreamDropCause }
|
|
|
14
14
|
*
|
|
15
15
|
* Internal module: not re-exported from `src/index.ts`.
|
|
16
16
|
*/
|
|
17
|
-
/** The CC
|
|
17
|
+
/** The warning (CC 2.1.220's words; gone from 2.1.245 on — 2.1.296 asks for a resume instead) stamped beside `partialFinalized` when a stream ended before its close — by the
|
|
18
18
|
* mid-stream promotion (the body threw) and by the parsers' own finalize (content arrived, no finish frame). */
|
|
19
19
|
export declare const STREAM_TORN_WARNING = "Connection closed mid-response. The response above may be incomplete.";
|
|
20
20
|
/** A provider word as a stream-end seat carries it: the word in the provider echo shape, else `?` — the fact that a
|
|
@@ -220,7 +220,7 @@ declare const SHADOW_REASON: {
|
|
|
220
220
|
readonly unverifiable_shell_text: "the text this shell will run cannot be read by the engine — it is cleared by confirming this call, never by an allow rule";
|
|
221
221
|
readonly transcript_write: "this write targets the session transcript — it is cleared by confirming this call, never by an allow rule";
|
|
222
222
|
readonly org_rule: "an organization policy rule requires approval for this call";
|
|
223
|
-
readonly governance_unavailable: "
|
|
223
|
+
readonly governance_unavailable: "a source of governance for this call cannot currently be read (this deployment's organization policy, or a partition of the person's standing rules), so whether it forbids the call is unknown";
|
|
224
224
|
readonly rule_unreadable: "this call could not be read against a deny or ask rule, so whether one forbids it is unknown";
|
|
225
225
|
readonly ask_rule: string;
|
|
226
226
|
readonly denial_limit_fallback: "the auto-mode classifier refused too many calls in a row, so a person decides";
|
package/dist/core/allow-entry.js
CHANGED
|
@@ -328,7 +328,7 @@ const SHADOW_REASON = {
|
|
|
328
328
|
unverifiable_shell_text: "the text this shell will run cannot be read by the engine — it is cleared by confirming this call, never by an allow rule",
|
|
329
329
|
transcript_write: "this write targets the session transcript — it is cleared by confirming this call, never by an allow rule",
|
|
330
330
|
org_rule: "an organization policy rule requires approval for this call",
|
|
331
|
-
governance_unavailable: "
|
|
331
|
+
governance_unavailable: "a source of governance for this call cannot currently be read (this deployment's organization policy, or a partition of the person's standing rules), so whether it forbids the call is unknown",
|
|
332
332
|
rule_unreadable: "this call could not be read against a deny or ask rule, so whether one forbids it is unknown",
|
|
333
333
|
ask_rule: mandateDisclosureOf(undefined),
|
|
334
334
|
denial_limit_fallback: "the auto-mode classifier refused too many calls in a row, so a person decides",
|
|
@@ -52,9 +52,9 @@ import type { ContainmentFace } from "../tools/fs/containment-face.js";
|
|
|
52
52
|
* their standing deny/ask rows (the tightening lexer's `unreadable` word: an expansion where a rule
|
|
53
53
|
* reads a word, an unterminated quote, a syntax error — the answer's `reason`): whether one of those
|
|
54
54
|
* rows forbids this call is unknown, so an allow tightens to an ask a person must clear and no automatic
|
|
55
|
-
* lane may answer. A store that could not be READ is
|
|
56
|
-
*
|
|
57
|
-
* `permission.rule_store_unavailable` notice
|
|
55
|
+
* lane may answer. A store (or a partition of it) that could not be READ is this word too — a row inside it
|
|
56
|
+
* may forbid the call and nobody can tell — with the class `governance_unavailable` instead of the releasable
|
|
57
|
+
* `rule_unreadable`, both bypass-immune; the run also carries one `permission.rule_store_unavailable` notice.
|
|
58
58
|
* - `hook` — a deployment's PreToolUse screening face raised or co-raised the ask; the word is
|
|
59
59
|
* engine-stamped at the fold and is the deployment's own per-call adjudication.
|
|
60
60
|
* - `ask_rule` — an EXPLICIT `ask` permission rule matched: a person's standing "ask me each time",
|
|
@@ -103,9 +103,14 @@ export interface AskOriginFacts {
|
|
|
103
103
|
readonly org: "rule" | "unavailable" | undefined;
|
|
104
104
|
/** The gate's own record of its persisted-rule lane's READ: the store was read, but THIS CALL could not be read
|
|
105
105
|
* against the person's deny/ask rows (the lane's answer carried the lexer's reason) — the origin
|
|
106
|
-
* `rule_store_unavailable
|
|
107
|
-
*
|
|
106
|
+
* `rule_store_unavailable`, the class `rule_unreadable` (bypass-immune, like the store form's). Absent: the lane read the call, or the store
|
|
107
|
+
* could not be read (see {@link ruleStoreUnreadable}). Written only by the gate. */
|
|
108
108
|
readonly ruleCallUnreadable?: boolean;
|
|
109
|
+
/** The gate's own record of the same read when a PARTITION of the store (or all of it) could not be read: a deny / ask
|
|
110
|
+
* row inside it may forbid this call and nobody can tell — the same origin `rule_store_unavailable`, the
|
|
111
|
+
* bypass-immune class `governance_unavailable` (a source of governance that cannot be read: only a person answers).
|
|
112
|
+
* Exclusive with {@link ruleCallUnreadable}. Written only by the gate. */
|
|
113
|
+
readonly ruleStoreUnreadable?: boolean;
|
|
109
114
|
/** Which of the gate's OWN post-fold stations minted the standing ask, if one did — again the gate's
|
|
110
115
|
* record of its own layer (a station replaces an `allow` wholesale, so at most one fires per pass),
|
|
111
116
|
* never a read-back of `decisionReason: "safety"` off a decision a policy could have composed. `unstated` is
|
|
@@ -569,7 +574,7 @@ export declare const MODE_ARBITRATION: {
|
|
|
569
574
|
readonly hookAskFloor: false;
|
|
570
575
|
};
|
|
571
576
|
readonly rule_unreadable: {
|
|
572
|
-
readonly bypassImmune:
|
|
577
|
+
readonly bypassImmune: true;
|
|
573
578
|
readonly classifierEligible: false;
|
|
574
579
|
readonly sandboxAdmission: false;
|
|
575
580
|
readonly ruleMayClear: false;
|
|
@@ -713,14 +718,13 @@ export declare const RRA_EXPLAINERS: ReadonlySet<AskClass>;
|
|
|
713
718
|
export declare function releasableAskClassesFor(face: AskFace): readonly AskClass[];
|
|
714
719
|
/**
|
|
715
720
|
* The closed set of `reason` words a mode release carries (`GateDisposition.released.reason`, the `released` arm of
|
|
716
|
-
* {@link Arbitration}): each is a releasable class the release record names on its own — `
|
|
717
|
-
*
|
|
718
|
-
*
|
|
719
|
-
* set holds, and none when it holds no member. {@link arbitrateByMode} and {@link isModeReleaseReason} read one copy
|
|
721
|
+
* {@link Arbitration}): each is a releasable class the release record names on its own — `target_unresolved`, a release
|
|
722
|
+
* (the shell face only, {@link RELEASED_ON_SHELL_FACE}) over a write target whose reading could not say where it leads. A
|
|
723
|
+
* release carries the first member (array order) its class set holds, and none when it holds no member. {@link arbitrateByMode} and {@link isModeReleaseReason} read one copy
|
|
720
724
|
* of this list taken at load (a caller that writes into the exported array changes neither), so a new reason is one
|
|
721
725
|
* word here.
|
|
722
726
|
*/
|
|
723
|
-
export declare const MODE_RELEASE_REASONS: readonly ["
|
|
727
|
+
export declare const MODE_RELEASE_REASONS: readonly ["target_unresolved"];
|
|
724
728
|
export type ModeReleaseReason = (typeof MODE_RELEASE_REASONS)[number];
|
|
725
729
|
/** Membership test for {@link ModeReleaseReason} — the word crosses the wire on `tool_end.gate.disposition.released.reason`. */
|
|
726
730
|
export declare function isModeReleaseReason(v: unknown): v is ModeReleaseReason;
|