@sema-agent/core 7.56.0 → 7.57.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 +541 -0
- package/LICENSE +1 -1
- package/dist/agents/org-delivery-gate.js +3 -4
- package/dist/agents/peer-held-queue.d.ts +69 -49
- package/dist/agents/peer-held-queue.js +255 -106
- package/dist/agents/peer-session-drain.js +63 -43
- package/dist/agents/send-message-tool.js +3 -2
- package/dist/agents/subagent.js +8 -8
- package/dist/brain/anthropic.js +16 -8
- package/dist/brain/open-responses.js +10 -8
- package/dist/brain/openai.js +11 -9
- package/dist/brain/provider-dialect.d.ts +3 -2
- package/dist/brain/provider-dialect.js +2 -2
- package/dist/brain/request-params.d.ts +6 -0
- package/dist/brain/request-params.js +14 -1
- package/dist/brain/stream-engine.d.ts +9 -0
- package/dist/brain/stream-engine.js +24 -10
- package/dist/brain/stream-shared.d.ts +5 -4
- package/dist/brain/stream-shared.js +4 -4
- package/dist/core/allow-entry.d.ts +3 -10
- package/dist/core/allow-entry.js +17 -25
- package/dist/core/approval-object.d.ts +21 -5
- package/dist/core/approval-object.js +44 -17
- package/dist/core/ask-origin.d.ts +2 -2
- package/dist/core/auto-compaction.d.ts +71 -6
- package/dist/core/auto-compaction.js +42 -26
- package/dist/core/background-agent-store.d.ts +6 -2
- package/dist/core/background-agent-store.js +1 -1
- package/dist/core/checkpoint-requirements.d.ts +7 -1
- package/dist/core/checkpoint-requirements.js +2 -0
- package/dist/core/checkpoint-store.d.ts +17 -29
- package/dist/core/checkpoint-store.js +19 -11
- package/dist/core/cross-boundary-taint.js +4 -2
- package/dist/core/engine-notice.d.ts +3 -2
- package/dist/core/engine-notice.js +6 -0
- package/dist/core/file-history-store.d.ts +14 -8
- package/dist/core/file-history-store.js +14 -4
- package/dist/core/gate-lanes.js +2 -3
- package/dist/core/hooks.d.ts +2 -2
- package/dist/core/mailbox-store.d.ts +62 -1
- package/dist/core/mailbox-store.js +59 -0
- package/dist/core/mcp-name-judge.d.ts +46 -0
- package/dist/core/mcp-name-judge.js +61 -0
- package/dist/core/mcp-probe.d.ts +1 -1
- package/dist/core/mcp-probe.js +2 -5
- package/dist/core/mcp.js +10 -30
- package/dist/core/memory-engine/consolidation-driver.js +4 -3
- package/dist/core/memory-engine/distiller.js +26 -17
- package/dist/core/memory-engine/layout.js +5 -2
- package/dist/core/park-selfcheck.js +2 -0
- package/dist/core/permission-rule-consent.d.ts +5 -3
- package/dist/core/permission-rule-consent.js +20 -15
- package/dist/core/permission-rule-model.d.ts +43 -2
- package/dist/core/permission-rule-model.js +52 -8
- package/dist/core/permission-rule-org.js +1 -1
- package/dist/core/protocol-http-auth.d.ts +16 -2
- package/dist/core/protocol-http-auth.js +42 -11
- package/dist/core/protocol-naming.d.ts +20 -0
- package/dist/core/protocol-naming.js +20 -3
- package/dist/core/read-only-shell.d.ts +20 -5
- package/dist/core/read-only-shell.js +29 -14
- package/dist/core/remote-env.d.ts +37 -1
- package/dist/core/remote-env.js +11 -0
- package/dist/core/resume-startup-cause.d.ts +24 -2
- package/dist/core/resume-startup-cause.js +20 -1
- package/dist/core/roles.js +50 -7
- package/dist/core/runner/compact-session.d.ts +10 -0
- package/dist/core/runner/compact-session.js +202 -0
- package/dist/core/runner/compaction-call-options.d.ts +2 -2
- package/dist/core/runner/compaction-seams.d.ts +21 -1
- package/dist/core/runner/compaction-seams.js +11 -8
- package/dist/core/runner/contracts.d.ts +23 -16
- package/dist/core/runner/derived-route-fallback.d.ts +13 -0
- package/dist/core/runner/derived-route-fallback.js +6 -0
- package/dist/core/runner/env-capability-census.d.ts +3 -2
- package/dist/core/runner/env-capability-census.js +9 -3
- package/dist/core/runner/permission-rule-lanes.d.ts +7 -7
- package/dist/core/runner/permission-rule-lanes.js +17 -11
- package/dist/core/runner/prepare-acquire-reconcile.d.ts +3 -0
- package/dist/core/runner/prepare-acquire-reconcile.js +2 -1
- package/dist/core/runner/prepare-config-doors.d.ts +6 -0
- package/dist/core/runner/prepare-config-doors.js +4 -3
- package/dist/core/runner/prepare-hands-readface.d.ts +3 -1
- package/dist/core/runner/prepare-hands-readface.js +2 -0
- package/dist/core/runner/prepare-park-ask.d.ts +2 -0
- package/dist/core/runner/prepare-park-ask.js +2 -2
- package/dist/core/runner/prepare-policy-chain.js +2 -4
- package/dist/core/runner/prepare-task.d.ts +4 -1
- package/dist/core/runner/prepare-task.js +23 -27
- package/dist/core/runner/prepare-tool-disclosure-mount.d.ts +13 -0
- package/dist/core/runner/prepare-tool-disclosure-mount.js +13 -7
- package/dist/core/runner/rewind-files.d.ts +2 -5
- package/dist/core/runner/rewind-files.js +9 -51
- package/dist/core/runner/run-leg.js +10 -21
- package/dist/core/runner/run-notification-lane.d.ts +2 -0
- package/dist/core/runner/run-notification-lane.js +2 -2
- package/dist/core/runner/run-recovery-lanes.js +3 -4
- package/dist/core/runner/run-settle-and-teardown.js +5 -11
- package/dist/core/runner/run-terminal-adoption.js +3 -4
- package/dist/core/runner/run-turn-boundary.js +3 -5
- package/dist/core/runner/runtask.d.ts +8 -1
- package/dist/core/runner/runtask.js +28 -34
- package/dist/core/runner/send-message-continuation.js +1 -1
- package/dist/core/runner/session-verb-doors.d.ts +28 -0
- package/dist/core/runner/session-verb-doors.js +48 -0
- package/dist/core/runner/session-write-ledger.d.ts +30 -0
- package/dist/core/runner/session-write-ledger.js +101 -0
- package/dist/core/runner/stream-settle-backstop.js +11 -14
- package/dist/core/runner/sub-agent-deny-lists.d.ts +25 -3
- package/dist/core/runner/sub-agent-deny-lists.js +21 -10
- package/dist/core/runner/teardown-bounded.d.ts +1 -0
- package/dist/core/runner/teardown-bounded.js +1 -0
- package/dist/core/runner-deps.d.ts +2 -2
- package/dist/core/secret-env.d.ts +4 -0
- package/dist/core/secret-env.js +3 -0
- package/dist/core/session-policy-store.d.ts +7 -2
- package/dist/core/session-policy-store.js +1 -1
- package/dist/core/shell-wrapper-projection.d.ts +1 -1
- package/dist/core/store-contracts/background-agent-store-contract.js +1 -0
- package/dist/core/store-contracts/checkpoint-store-contract.js +2 -1
- package/dist/core/store-contracts/file-history-store-contract.js +9 -0
- package/dist/core/store-contracts/mailbox-store-contract.js +49 -1
- package/dist/core/task-registry-agent.d.ts +8 -4
- package/dist/core/task-registry-agent.js +14 -8
- package/dist/core/task-registry-shared.d.ts +35 -6
- package/dist/core/task-registry-shared.js +11 -0
- package/dist/core/task-registry.d.ts +1 -1
- package/dist/core/task-registry.js +1 -1
- package/dist/core/tool-catalog-entries.d.ts +6 -0
- package/dist/core/tool-catalog-entries.js +1 -0
- package/dist/core/tool-name-aliases.d.ts +32 -2
- package/dist/core/tool-name-aliases.js +23 -0
- package/dist/core/tool-name-readings.d.ts +4 -0
- package/dist/core/tool-name-readings.js +1 -0
- package/dist/core/tool-policy.d.ts +10 -6
- package/dist/core/tool-policy.js +4 -7
- package/dist/core/trace.d.ts +4 -1
- package/dist/core/trace.js +2 -1
- package/dist/core/untrusted-egress.d.ts +20 -0
- package/dist/core/untrusted-egress.js +77 -3
- package/dist/engine/harness/agent-harness.d.ts +7 -5
- package/dist/engine/harness/agent-harness.js +11 -2
- package/dist/engine/loop/agent-loop.d.ts +1 -4
- package/dist/engine/loop/agent-loop.js +26 -4
- package/dist/index.d.ts +12 -7
- package/dist/index.js +7 -4
- package/dist/internal/harness.d.ts +2 -1
- package/dist/internal/harness.js +1 -0
- package/dist/primitives/credential-ref.d.ts +89 -0
- package/dist/primitives/credential-ref.js +125 -0
- package/dist/primitives/index.d.ts +2 -0
- package/dist/primitives/index.js +1 -0
- package/dist/stores/file/checkpoint-store.d.ts +0 -4
- package/dist/stores/file/checkpoint-store.js +0 -1
- package/dist/stores/file/file-history-store.js +12 -4
- package/dist/stores/file/mailbox-store.d.ts +2 -1
- package/dist/stores/file/mailbox-store.js +26 -1
- package/dist/tools/fs/bash-readonly-classifier.d.ts +15 -19
- package/dist/tools/fs/bash-readonly-classifier.js +5 -10
- package/dist/tools/fs/fs-bash.js +10 -3
- package/dist/tools/fs/pattern-kill-guard.d.ts +3 -2
- package/dist/tools/fs/pattern-kill-guard.js +68 -36
- package/dist/tools/fs/shell-read-check.d.ts +10 -0
- package/dist/tools/fs/shell-read-check.js +7 -1
- package/dist/tools/fs/shell-read-identity.d.ts +12 -9
- package/dist/tools/fs/shell-read-identity.js +9 -4
- package/dist/tools/web.js +9 -7
- package/package.json +1 -1
- package/test/export-surface.snapshot.json +79 -3
- package/wire-changes/core-stores-sql@7.57.0.json +29 -0
- package/wire-changes/core@7.57.0.json +1487 -0
- package/wire-changes/mods-kernel@7.57.0.json +29 -0
- package/wire-changes/primitives@7.57.0.json +245 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,544 @@
|
|
|
1
|
+
## 7.57.0 — 2026-10-25
|
|
2
|
+
|
|
3
|
+
### A Model object the engine cannot read is refused by name at every seat that takes a model — `Runner.runTask` and `Runner.compactSession` read the one model-binding judgment (#2769; behavior narrowing, open to veto; hard-breaking: none; closed sets: none — the refusal is the existing `config.unknown_role` / `not_a_model_ref`; exports: none) (@server @cli)
|
|
4
|
+
|
|
5
|
+
- **Before.** The judgment of a model binding accepted any object as a Model. `{ id: "x" }` handed to `runTask` ended terminal `failed` with the bare `TypeError: Cannot read properties of undefined (reading 'startsWith')` (the prompt-assembly turn snapshot reads `api`); a Model with no `id` ran to `completed` unnamed; `compactSession` answered `compacted` on the same object and handed it to the summary call. A catalog entry a word resolved to, the `compactionModel` seat and a role seat were read the same way.
|
|
6
|
+
- **Now.** An object bound at a model seat reads as a Model when it carries `id` (a non-empty string) and `api` (a string) — the two members the engine has no designed reading for when absent. Otherwise the seat is refused by the existing code (`config.unknown_role`, `detail { field: <seat>, reason: "not_a_model_ref" }`), the sentence naming the member (``TaskSpec.model is a Model object whose `api` is absent``), before any model call and before the session's lock is taken. The same judgment reads the catalog entry a word resolves to, the entry a role's `select` criteria pick, and the catalog entry that shadows a tier word and is read as the tier's default. One reader — `assertModelObject` behind `modelOfBinding` / `assertModelBindingShape` in `roles.ts` — serves `TaskSpec.model`, `compactSession`'s `model` and `compactionModel`, role seats, tier bindings and every other `ModelRef` seat; no second judgment and no new code or reason was added. At a role seat an object is a config record only when every own key is one of `model` / `select` / `thinking` / `promptRole` / `systemPrompt` and neither `id` nor `api` can be read from it; every other object is read as a Model and judged, so a Model missing one or both identity members (or a record with a stray key) is refused instead of being read as a record that leaves the role on the default unannounced (a list or a function written there still reads as an empty record, as before). Every catalog entry a role's `select` criteria will read is judged before the selection reads any member of it. Reading is total: an accessor that throws, or a revoked Proxy, as the Model or as a member, reads as absent / unreadable and the refusal still names the member.
|
|
7
|
+
- **Not judged (stated).** The window, the endpoint and the output cap: a window the engine cannot evaluate is a disclosed state (`unevaluableWindow`), and an absent endpoint or output cap falls to the brain's own. A Model without them is accepted at both doors, as before.
|
|
8
|
+
- **Behavior narrowing (open to veto).** A Model object (or catalog entry) with no `id`, an empty `id`, or no string `api` — which ran, or crashed in the turn snapshot — is refused by name, and a partial Model written at a role seat (`roles.x = { id: "…" }`, `{ contextWindow: 1000 }`) or a role record with a key outside the five above no longer leaves that role on the default silently; a role's `select` over a catalog that holds a broken entry refuses by name (the entry's key in the sentence) instead of choosing around it. @server: a catalog entry you build must carry `id` and `api` (the type has always required both); an entry without them now refuses at first use with the sentence above instead of failing deep in a run (inferred: no server-side catalog is known to omit them). @cli: nothing to render beyond the existing `config.unknown_role` terminal.
|
|
9
|
+
- **Pins.** `test/backlog2769-model-door.test.ts` — ten bad shapes (no / empty / numeric / null / revoked-Proxy `id`, no / numeric / throwing `api`, a revoked-Proxy Model) × the two doors (refusal by name, no model call, nothing written, the lock free after), the catalog / `compactionModel` / role-seat / `select` / tier-shadow / tier-binding / bare-role-seat arms, and the controls (a whole Model, a Model without window / endpoint / output cap, a real role record, the earlier refusals keeping their words).
|
|
10
|
+
|
|
11
|
+
### `@sema-agent/core-stores-sql` is published with this release — cut with core at the same version; its public face is fixed, and this first release carries no SQL stores (#2673; no behaviour change; hard-breaking: none; closed sets: none; exports: none in the root — the package's own entry is its fourteen names, new on the registry; a new public package name and a licence-text line, open to veto) (@server @test @cli)
|
|
12
|
+
|
|
13
|
+
- **Before.** `packages/core-stores-sql` was cut in 7.56.0 as a package that never reached the registry (`private: true`): its public face — neutral factories over a caller's pool and the neutral types they speak — and the way it is built, checked and released were fixed, but nothing could install it, so a deployment that owns SQL stores had nothing to code against.
|
|
14
|
+
- **Now.** The manifest says `private: false` with `publishConfig.access: "public"`, and the release publishes it with the root — the satellite packages go out after the root, in directory-name order — from the same plain clone of the tag, at the root's version, with that version as its exact `@sema-agent/core` peer. The public face is the fourteen names it already had (`createSqlStorageBackend`, `createSqlDriver`, `baselineDdl`, `publishedColumns` and ten types); nothing is added or removed, and no store class, no dialect class, no SQL statement list, no ensure leg and no SQL predicate generator is exported. The tarball holds `package.json`, `README.md`, `LICENSE` and `dist/` — pinned by packing the package directory. The README is rewritten for a stranger: first, that this release carries no SQL stores (the registry's one-line description says it too); then install, the version pairing (each factory refuses a core it was not cut with), the fourteen names and what is not exported. The package's build now runs `tsc` twice, as the root's does (the second pass writes the declarations again with their comments), so the JSDoc on each exported name — including the sentence that a factory refuses — ships in the `.d.ts`. The root `LICENSE`'s Licensed Work parameters now name the package beside `@sema-agent/mods-kernel`, and `packages/core-stores-sql/LICENSE` and `packages/mods-kernel/LICENSE` are byte copies of it (public text, open to veto).
|
|
15
|
+
- **Release machinery.** No release script changes: each leg reads the package as it reads any non-private one: `scripts/bump-version.mjs` already moves its version cell and its exact core peer; `gate:packages` and `gate:packages:pack` already read its tsconfig, its frozen snapshot (`packages/core-stores-sql/test/export-surface.snapshot.json`, 14 names) and its smoke table (4 runtime exports), pack it and install it beside core's own tarball; `scripts/publish-release.sh` takes every non-private workspace package; the release gate and the post lint read the claim from the tag's tree, so a tag cut from here owes the registry the package and the release owes `wire-changes/core-stores-sql@<version>.json` beside the root's. That manifest is based on the previous tag like every manifest (`gate:wire -- <previous tag> <release candidate> --package core-stores-sql --version <version> --to-tag v<version>`); the package already had these fourteen names there, so the manifest reports no export moved — that the package is new on the registry is a fact of the release post, not of the manifest.
|
|
16
|
+
- **Not here yet (named).** The stores. `createSqlStorageBackend` and `createSqlDriver` read their options (every wrong value refused by name) and then refuse; `baselineDdl(dialect)` resolves to an empty list and `publishedColumns()` returns one. The stores move in with a later release, from the deployment that owns them; until then the package is a published face to code against, not a backend to run. A removed or re-kinded name will be named as breaking in the notes of the release that does it.
|
|
17
|
+
- **Consumers.** No consumer is asked to change. @server: the package can be installed from the registry from this release (`npm install @sema-agent/core @sema-agent/core-stores-sql`, same version as core); code against the fourteen names and the neutral pool shapes (`PgPoolLike`, `MysqlPoolLike`), not against a store class; do not call a factory expecting a backend yet. @test: the package is one more thing a fresh install of this release must find on the registry (`scripts/verify-fresh-install.mjs` installs every non-private package and runs its smoke table). @cli: no action. The package name is public and permanent once published (open to veto).
|
|
18
|
+
- **Pins.** `test/backlog2673-core-stores-sql-publish.test.ts` — the manifest says published (`private` false, `publishConfig.access` public, the package gate's reading clean, `files` exactly `["dist"]`, the core peer exact, the build puts the declarations' comments back, the release gate's tree reading counts the package as a promise of the root's version and the manifest leg run over the committed tree owes this package's wire manifest); the licence line in the Licensed Work parameters and the byte copy; the README (install line, the version pairing, no store class sentence, the "this release carries no SQL stores" sentence tied to both factories' own refusals and to the registry description so they go red together when the stores arrive, no status of an unpublished package, an Exports table that is the frozen surface name for name); what `npm pack` lists over the package directory; the snapshot and the smoke table as the public face (a runtime export is judged by the gate's own kind set). Re-pinned: `backlog1468-n1-core-stores-sql` (the manifest cell says published).
|
|
19
|
+
|
|
20
|
+
### A new package below the engine, `@sema-agent/primitives`, holds the credential reference — one closed set of kinds (`env` / `vault` / `handle` / `assertion`) and one reader, `readCredentialRef`, that every layer imports (#2293, slice S4 of the MCP credential seat; no behaviour change, hard-breaking: none; closed sets: none; exports: +6 root values (`readCredentialRef`, `CREDENTIAL_REF_KINDS`, `CREDENTIAL_REF_TRUST`, `CREDENTIAL_REF_TEXT_MAX`, `CREDENTIAL_REF_MAP_MAX`, `CREDENTIAL_REF_PROBLEM_REASONS`) and +6 root types, none removed; a new published package cut with core, public name open to veto)
|
|
21
|
+
|
|
22
|
+
- **Before.** The layer below the engine (the one every other layer may import) had no package: a shared definition lived in whichever layer wrote it first, and the next layer copied it. A credential reference — how a declaration names a credential without carrying its value — had no definition anywhere, so the surfaces that will have to validate one (the engine's declaration door, a deployment's acceptance gate, a settings validator, an admin form) had nothing to agree on.
|
|
23
|
+
- **Now.** `packages/primitives/` is a satellite package cut with core at the same version, compiled from `src/primitives/` (the folder the engine reads directly, as the mods kernel is), with no dependencies — not a builtin, not core — so it runs in Node and in a browser. It is the third published package (`@sema-agent/mods-kernel` and this one; `core-stores-sql`, private in 7.56.0, is published with this release as well — its own section above). Its first member is the credential reference:
|
|
24
|
+
- `CredentialRef` is a closed set of four kinds: `env` (header name → variable name for an http peer, child variable → variable name for a stdio peer; at least one), `vault` (`entry`), `handle` (`id`) and `assertion` (`audience`). `CREDENTIAL_REF_KINDS` is the set, frozen.
|
|
25
|
+
- `readCredentialRef(value)` is the one reader. It returns `{ ok: true, ref }` — a fresh frozen copy that the caller's later writes cannot reach; forward the copy, never the value read — or `{ ok: false, problem }` whose `reason` is one of `CREDENTIAL_REF_PROBLEM_REASONS`: `not_a_record`, `unknown_kind`, `missing_field`, `extra_field` or `bad_value`. The first fault in that order is reported, and a refusal is a value, never a throw: a value whose members cannot be read (a revoked or throwing Proxy) is `not_a_record`, or the `bad_value` of the map that holds it; the reader's catch clauses make no call, so a call made at the edge of the stack cannot throw out of the handler (a pin reads the source for it). With no stack left a valid value may be refused, under any reason, and an invalid one is never accepted; the call itself may throw a `RangeError` before it is entered, which no function can catch.
|
|
26
|
+
- What the reader reads. An ordinary record is an object (not an array) whose prototype is none or itself has none — the `Object.prototype` of any realm is one, so a record from another realm or from `JSON.parse` in one is read; a member is an own enumerable string-keyed data property, read off its descriptor with own-property tests, so a getter is refused and never called, nothing is read through a prototype (a polluted `Object.prototype` cannot turn a getter into a value), and an own `__proto__` key is a plain key. A key present with the value `undefined` is a bad value, not an absent member.
|
|
27
|
+
- What `bad_value` means. A text member (`entry`, `id`, `audience`) is 1 to `CREDENTIAL_REF_TEXT_MAX` (2048) UTF-16 code units, with no character that is not visible text — control (C0 and C1), format (zero-width, bidi override, soft hyphen, BOM), line and paragraph separator, default-ignorable (Hangul filler, combining grapheme joiner, variation selectors), noncharacter, private-use — no lone surrogate, and no leading or trailing white space; it is compared as written, nothing is normalised (two spellings of one visible string are two entries; visually confusable text — homoglyphs, a braille blank, stacked combining marks — is not judged; an emoji presentation selector and a zero-width joiner are refused, because these members are identifiers, not prose), and the bound counts UTF-16 units, up to 6144 UTF-8 bytes. The character classes are the host's Unicode tables: two surfaces on engines with different Unicode versions can differ for a character added recently. A name map (`headers`, `env`) holds 1 to `CREDENTIAL_REF_MAP_MAX` (64) entries; a header name is an HTTP token, a child variable and every variable name an identifier (`[A-Za-z_][A-Za-z0-9_]*`); two header names that differ only by case are one name named twice and are refused (variable names are not folded). The maps come back frozen and without a prototype, so an absent name reads `undefined`. A reference's size is bounded before it is copied, so a record of a million members costs the reader no more than listing its keys.
|
|
28
|
+
- `CREDENTIAL_REF_TRUST` says, per kind, what a request surface may carry: `vault` only for an entry that belongs to the submitting principal (`own_entry`); `env`, `handle` and `assertion` not at all (`refused` — an environment name would read any variable of the resolver's process, a handle names someone else's stored credential, an assertion is minted by the deployment). The table has a row for each kind and for nothing else (no prototype); look a kind up in it only after the reader has accepted the value. It is the shape an acceptance gate reads; it authenticates nothing.
|
|
29
|
+
- The reader judges shape and nothing else: it resolves no name, reads no environment and holds no state, and it carries no policy of any layer — which header names a deployment forbids (the protocol's own names, the principal header), which child variables a declaration may set, who answers a reference and whether this declaration may reach this destination stay with the engine's door and the deployment's seat.
|
|
30
|
+
- The root re-exports the same names from its own compiled copy of `src/primitives/` (`src/index.ts`): the root does not depend on `@sema-agent/primitives`, so a consumer that installs both packages holds two module instances compiled from the same source — plain-object values, no `instanceof`, no module identity (as with the mods kernel). A single instance is a design ticket (#2749).
|
|
31
|
+
- **Release machinery.** Nothing new is needed and each leg was run over the third package: `scripts/bump-version.mjs` moves its version cell (it has no core peer); `gate:packages` and `gate:packages:pack` read its tsconfig, snapshot (`packages/primitives/test/export-surface.snapshot.json`, 12 names) and smoke table (6 runtime exports) and pack it (built into its own `dist/`, installed offline, smoked); the release publishes it after the root and `mods-kernel` (`scripts/publish-release.sh` takes every non-private workspace package) and, from this release, owes `wire-changes/primitives@<version>.json` (`gate:wire -- <from> <to> --package primitives --version <version> --to-tag v<version>`; a package new in the range reports every export as an addition, which is the first cut). The root `LICENSE`'s Licensed Work parameters now name `@sema-agent/primitives` beside `@sema-agent/mods-kernel`, and the three satellite packages (`mods-kernel`, `primitives`, `core-stores-sql`) each carry a byte copy of it (public text, open to veto).
|
|
32
|
+
- **Not here yet (named).** Nothing in the engine reads a credential reference today: `McpServerSpec` still carries `headers` / `env` values and gains no `credentials` member in this change. The declaration door, the deployment seat that answers a reference, the retirement of the value members and the unauthorized state are the next slices of the same design (#2294 – #2296); the new package is their shared floor and ships first so the three consuming repositories can read it before the door exists.
|
|
33
|
+
- **Consumers.** No consumer is asked to change. @settings-schema / @web-admin / @server: the reference's validator you will write reads `readCredentialRef` from `@sema-agent/primitives` rather than a copy, and the acceptance gate reads `CREDENTIAL_REF_TRUST`; the package is published with this release and carries the release's version, and its name is now public and permanent (open to veto). @connectors: the shape of the reference is fixed, for your comparison table.
|
|
34
|
+
- **Pins.** `test/backlog2293-credential-ref.test.ts` — a positive control per kind and the frozen-copy law (and that a copy reads again as itself); the three named exits and the garbage exits each as their own reason, in one fixed order (unknown kind in fifteen spellings, missing, extra including an own `__proto__` key and a member in another case, not-a-record in fifteen shapes (two of them arrays wearing a record's prototype), a record of another realm accepted and an array / class instance / date of another realm refused, `kind` not text); no throw from a revoked Proxy, a Proxy whose `getPrototypeOf` / `ownKeys` / `getOwnPropertyDescriptor` trap throws; a getter never called at any depth, including under a polluted `Object.prototype`; the source's catch clauses holding no call; text members at, over and under the bound, control / format / separator / default-ignorable / noncharacter / private-use characters, lone surrogates, blank and padded text; name grammars, an empty map, a map over the bound, header names that differ only by case; a record of 50 000 members and a map of 50 000 entries each refused at once; the maps without a prototype, immune to pollution; echoed text bounded, fenced, never ill-formed; a map that cannot be read is that map's bad value; the closed sets and the trust table frozen and in step with the kinds, the table answering for its kinds only; the folder a leaf that compiles on its own with browser-and-node globals only; the root re-exporting the same values; the package gate (static arm and pack arm) over a copy of the real tree, and red for a bare import and for an export the frozen surface does not hold. Re-pinned for a third published package: `backlog1751-package-release-base` (the owed package manifests, the published packages at HEAD, the bump plan) and `backlog2104-mods-kernel-publish` (the licence copy).
|
|
35
|
+
|
|
36
|
+
### `Runner.compactSession({ sessionId, model, … })` compacts a session no run holds — the one compaction pass, under the session's lock, `trigger: "manual"`; a session a leg of this Runner holds is refused `compact.session_busy` (#2451, design/2451; behavior widening — a new verb; open to veto: the session station's `$` facade, and a pass's credential wait now ends at the pass's signal on every lane) (@server @cli @client-core)
|
|
37
|
+
|
|
38
|
+
- **Before.** The only manual compaction was `TaskStream.compact()`, which parks a request for the next turn boundary of a live run. A person typing `/compact` while their session was idle had no engine verb: hosts armed the request and fired it after the next message's run started, and when that run was shorter than the host's delay nothing was compacted (`compact.not_running`).
|
|
39
|
+
- **Now.** `runner.compactSession(request)` runs the pass every lane runs (`maybeCompact`) on an idle session: no run, no user turn, `force` with no floor, `trigger: "manual"` — the reference's `/compact` on an idle session (a local command that runs no main-loop turn; its plugin API refuses while a turn runs). The request is `{ sessionId, model, instructions?, compaction?, compactionModel?, getApiKeyAndHeaders?, signal? }`: `model` (required) is the model the session's next turn runs on and bounds the pass; `instructions` replace the standing ones for this pass as `TaskStream.compact` does; `compaction` takes the five members a pass reads (`enabled`, `reserveTokens`, `keepRecentTokens`, `instructions`, `clampTolerance`); `compactionModel` / the `summarize` role, its route pre-flight (a role-derived model whose key↔URL pairing is broken falls back to the primary, `route.fallback_to_primary`), the settings, the provider seam, the PreCompact / PostCompact stations and the `compacted` mint are the readers a run's lanes use. The answer is `{ outcome, events, usage? }`: `outcome` is `TaskStream.compact`'s closed set; `events` are the pass's frames in order (`compaction_started`, then `compacted` or `compaction_outcome`, each `trigger: "manual"`, each with its own `eventId`, no task attribution) — returned, not streamed; `usage` books the pass's model calls as a run's side calls are booked. A summarizer failure is the outcome `"failed"` (`onError` phase `compaction`), never a rejection. The ToolSearch activation snapshot on the branch is restated on the new record. A later run reads the summary — on a session no checkpoint is parked on: a session durably parked at a checkpoint has released its lock, so the verb compacts it, and the resume rewinds the branch to the checkpoint's leaf, off which the new compaction entry falls (the answer is still `compacted`; KL-2451-a, #2701).
|
|
40
|
+
- **Refusals (thrown, coded, before any model call).** `compact.invalid_request` (not a plain data object read as one snapshot — a Proxy, an inherited or accessor member, an unknown key, a wrong type, `compaction` the same; `model` missing); the model door's `config.unknown_role`; **`compact.session_busy`** — a leg of THIS Runner is running or queued on the session, or a write one of this Runner's legs issued to it has not settled (the same one reading of "busy" `Runner.rewindFiles` refuses on; otherwise the pass holds the session's lock to the end, so a leg arriving meanwhile waits and reads the summary); `resume.session_not_found`. A `signal` already aborted answers `{ outcome: "mooted", events: [] }` with nothing taken.
|
|
41
|
+
- **Open to veto — the session station's `$`.** The deployment seat's (`RunnerDeps.mods`) PreCompact / PostCompact handlers run on the pass (a block blocks it). There is no leg, so the station carries no hook identity and its facade answers the seats a session has — `$.session.id()` / `messages()` / `model()`, `$.clock.now()`, `$.model.complete()` (booked into `usage`); `$.process.run`, `$.session.cwd()` / `root()`, `$.tool.list()`, `$.agent.list()` and `$.tool.call` throw naming the station. A handler that needs one fails, and the station's one failure rule applies (a failed handler neither blocks nor adds instructions) — the rule every manual pass has; whether a PreCompact failure should block a manual pass is a separate decision for both lanes.
|
|
42
|
+
- **Open to veto — every lane.** A pass's credential callback (`getApiKeyAndHeaders` for the summary model) is now awaited against the pass's own signal, as the summary provider's consult already was: a pass cut while the callback is answering ends with the cut (`compaction_outcome{mooted}` / the abort outcome) instead of waiting on a callback that may never settle. A pass that already appended is not undone.
|
|
43
|
+
- **Not on this verb (stated).** No post-compaction file restore and no instruction-file content (there is no execution environment out of a run), no fork-form summary (no main request), and ToolSearch activations made after the newest snapshot inside the summarized range are not restated. The busy check sees this Runner's legs and the writes they issued only: a leg of another Runner on a shared durable session is not refused, and the compaction's commit checks the leaf at append time, not the branch it summarized (an existing property of the pass on every lane).
|
|
44
|
+
- **Shared readers (no behavior change).** `Runner.rewindFiles` reads the same out-of-run doors (request snapshot, idle claim, the session that must exist; its refusal texts unchanged); the turn boundary reads `manualCompactOutcomeOf`; prepare reads `resolveCompactionModel` / `compactionSummaryIsDerived` / `compactionSummaryFallback` (the pre-flight is awaited only for a role-derived summary model, as before — a run with none takes no extra suspension point); the lanes read `summaryProviderOptions`; the leg seed reads `compactionActivationSnapshot`.
|
|
45
|
+
- **Consumers.** @server: `POST /v1/sessions/:id/compact` → `runner.compactSession({ sessionId, model, instructions?, compactionModel?, compaction?, getApiKeyAndHeaders, signal })` with the per-model key resolver and the body→spec mapping runs use, and a request-scoped `signal`; refuse 409 first when the run store shows a running run on the session on any replica and hold the session against new runs while the call is in flight; map `compact.session_busy` → 409, `compact.invalid_request` / `config.unknown_role` → 400, `resume.session_not_found` → 404; relay `events` and answer `outcome` / `usage`. @cli @client-core: idle `/compact` calls the session endpoint at once; the arm-then-fire delay and the `COMPACT_NOT_RUNNING` warning retire for the idle case; a live run keeps the run-scoped verb.
|
|
46
|
+
- **Exports.** +2 types `CompactSessionRequest` / `CompactSessionResult`; `Runner` gains `compactSession`. Closed sets: +2 refusal codes from the new verb only (`compact.invalid_request`, `compact.session_busy`).
|
|
47
|
+
- **Pins.** `test/backlog2451-compact-session.test.ts` (real Runner: landing + frames + next run reads the summary; held session refused; a leg arriving mid-pass waits; the doors; withdrawn request; deployment block; the facade's seats; an unavailable seat; summarizer failure; activation snapshot restated; a credential callback that never settles; the summary model's route fallback; `disabled` / `noop`).
|
|
48
|
+
|
|
49
|
+
### The rulings-landing gate reads every ledger the way it is written — one row splitter, a `path:line` + snippet is a claim judged on the file's content, a window's note is history it never reads, a disposition addresses ONE row, one comms root per run — `scripts/rulings-disposition.mjs` drafts the post's 裁定处置 lines, and the backlog gate counts only work as a car coordinate (#2113, #2260; release tooling only; behavior change: none; hard-breaking: none; closed sets: none; exports: none; members: none) (@test @cli @server @client-core)
|
|
50
|
+
|
|
51
|
+
- **Before.** Six readings of `scripts/rulings-gate.mjs` disagreed with the books it judges. (1) `BLOCKED(on #N)` took the ticket's state from the FIFTH cell of its backlog row: 188 of 2098 ticket rows are wider than six cells (a bare `|` in a head cell), so for them the "state" was whichever cell stood fifth, and `SHIPPED-…` was never "landed". (2) An `已落` row passed when any ONE of its tokens resolved, so a row that cited a commit and a symbol the code no longer holds read as landed (on the comms ledger of 10-24: 12 `path:line` + snippet claims in 10 rows name text that is not in the file at all — `DATE_ANCHOR_CUSTOM_TYPE` moved to another file, `ENGINE_ATTESTATIONS` likewise, `judgeRead(read, boundary)` changed shape — and 128 more name a line the snippet is no longer on). (3) A ruling that owns several rows in one repo shares one key, so one line in the post disposed of all its due rows. (4) With `--ledger` named, `BLOCKED(on S-563)` was still looked up in whichever comms checkout sat beside the tree; a `#N` / `core@x` blocker in another repo's row could never resolve. (5) A window cell with a note — `7.57.0(10-24 R 对账改窗:7.56.0 未并 / 未派;原 7.56.0)` — was an unreadable window: the 33 rows re-windowed that way were neither format-clean nor due, so leg (a) never asked the post for a line about them. (6) The backlog gate's "car coordinate" was any `car-…` string anywhere in the status cell, so a car that finished, a plan that never ran and an OPEN row naming a planned car all kept a twice-re-windowed row out of the "moving without work" list (22 of the 53 such rows on the stage-7570 ledger).
|
|
52
|
+
- **Now.** (1) A ticket's state is read through the backlog gate's own row reader (`ticketStatusOf`: the second cell from the right) and the rulings gate splits its tables through the backlog gate's `cellsOf` — no second parser; a status that opens with no vocabulary word, and a DEBTS row off its seven columns, are unknown, never guessed. `SHIPPED` and `TEST-VERIFIED` count as landed. (2) A `path:line` (or `path:from-to`, or a bare `:line` after a file was named) immediately followed by a backticked snippet is a claim; when the file is in the tree and the snippet is not in it, leg (b) is red even if another token of the row resolves (a post's 兑现 line is judged the same way). A snippet in the file but not at the stated line is held and counted on one line; `--verbose` lists ruling, old coordinate and lines now. The snippet is judged literally. (3) A disposition names its row: `裁定 C-R97/core.2 改期: …` is the second `C-R97 | core` row; a bare key line is enough only while one row of the key is due, and the finding spells the selector to write. (4) `--comms <dir>` names the one comms root of a run; a named ledger brings the root that holds it, never a neighbour's debts; `--core-root <dir>` (else a sibling `sema-core`) answers `#N` / `core@x` for another repo's rows. (5) A window is `<window> [BLOCKED(on X)] [(note)]`; the note is never read, and a row is due when its first window is at or before the shipping version, whatever the note says. (6) A car coordinate is a commit sha, or a `car-…` name on an IN-PROGRESS row, in the status HEAD (the `原 …` tail is history). New `scripts/rulings-disposition.mjs` writes the post's 裁定处置 section — one 改期 line per due row, addressed by the gate's own selector, each holding `TODO(` until a person writes the reason; it never claims 兑现 or 被取代. (7) `gate:rulings` leaves EXCLUDED for the rc tier (`gate-all --rc`) and the ship lint's rulings leg judges the post: until now the one gate that reads the landing ledger ran in no mandatory run, which is how the 12 rows of 7.54.0 and the 33 re-windowed rows of 7.56.0 went unchecked (open to veto; this is the separately revertable last commit of the car).
|
|
53
|
+
- **Who is hurt.** Nobody at runtime: `src/` is untouched and nothing in the package changes. The release candidate is red until the comms ledger covers the 164 rulings that have no row and the post carries a line per due row (`gate:rulings` is in `gate-all --rc` now; in a container without a comms checkout it REFUSES, exit 2, rather than passes). The release lint reads the new gate: a post that answered a multi-row key with one bare line now needs one line per due row; a ledger row whose snippet left its file is red (12 claims today, listed by the gate); a twice-re-windowed OPEN row that names only a planned car is red from the 7.55.0 line the backlog gate already carries (22 rows on the stage-7570 ledger — dispatch the car and flip the row to IN-PROGRESS, `BLOCKED(on <票>)` it, or close it). Run the gate from another repo with `--comms` / `--core-root` instead of relying on a sibling checkout.
|
|
54
|
+
- **Pins.** `test/backlog2113-rulings-gate-defects.test.ts` (G-1 … G-6, each red on the pre-fix gate and green on this one: positional ticket state, SHIPPED, the bare-pipe row, the DEBTS shape, the second-parser tripwire; claims and their adjacency, the content verdicts over real files, the moved list, the CLI; selectors, bare-key refusal, independence of lines; `--comms` / named ledger / missing root, `--core-root`; the window grammar, history not read, the replay shape), `test/rulings-disposition.test.ts` (the round trip: what the generator writes is exactly what leg (a) asks for), `test/backlog-ledger-gate.test.ts` (`#2260 q1`–`q3`, and the older re-window pin now says IN-PROGRESS where it said OPEN). Mutations: `mutations.md` in the car receipt.
|
|
55
|
+
|
|
56
|
+
### A stored bare-name rule names a tool the catalog spells exactly — a case twin of a catalog tool is refused loudly, like a retired name, instead of being stored to reach nothing (#2654, on #1949; behavior narrowing, open to veto; hard-breaking: none; closed sets: none; exports: none) (@server @cli)
|
|
57
|
+
|
|
58
|
+
- **What changed.**
|
|
59
|
+
- A whole-tool rule's name (`Tool`, `Tool()`, `Tool(*)`) is read by the one reading the entry lists and the session-rule store ask
|
|
60
|
+
(`toolNameFaultOf`: the name judgment's stale words, then another spelling of a carried tool), over the one universe the
|
|
61
|
+
persisted store has — the engine's tool catalog, case exact. `BASH`, `webfetch`, `READ` — another spelling of `Bash`,
|
|
62
|
+
`WebFetch`, `Read` — are refused, as a retired spelling (`bash`, `read_file`) already was: a rule the engine can tell reaches
|
|
63
|
+
no tool is no longer stored. The refusal code is `unsupported.tool` for every name that reading faults (a retired, unprefixed
|
|
64
|
+
or unmintable name read `invalid.grammar` in the unreleased #1949 form) and its sentence names the spelling to write
|
|
65
|
+
(`"BASH" is another spelling of "Bash"…`; a misspelt protocol peer: `write "mcp__git_hub__get_issue"`). A set of tools
|
|
66
|
+
(`mcp__srv`, a glob) keeps `invalid.grammar`.
|
|
67
|
+
- Every door that writes a bare name reads it through that one validator: `addPersistedRule` refuses a twin (`failed`,
|
|
68
|
+
`committed: "no"`, nothing written); a whole-tool card record cannot carry one; `prepareCardApproval` mints no card for a tool
|
|
69
|
+
named by a twin and the offer factory says `lane_cannot_speak` for it; `prepareCcImport` skips a twin with the validator's
|
|
70
|
+
code and sentence (the right spelling stays a name-set item the settings layer carries).
|
|
71
|
+
- One home for the reading: `nearMissSpellingOf` and the case comparison move from `allow-entry.ts` to `tool-name-aliases.ts`
|
|
72
|
+
beside the judgment, where `toolNameFaultOf` composes them; the entry reader (`readRuleEntry`) calls it and reads every entry
|
|
73
|
+
exactly as before.
|
|
74
|
+
- **Limits, stated.** A name no catalog tool carries and that is no spelling of one stays a rule (`SendUserFile`, and the typo
|
|
75
|
+
`Bsh`): it may be a tool a deployment mounts under that name, which is the reading the session-rule store and every name list
|
|
76
|
+
already have. The persisted store has no seat to declare a deployment's own tool names, so a deployment's tool whose name is a
|
|
77
|
+
case twin of a catalog name or a retired spelling (`bash`, `read`) has no whole-tool rule and its card says it has none, and a
|
|
78
|
+
typo of a deployment's own tool (`sendUserFile` for `SendUserFile`) is stored and reaches nothing — the declaration seat is a
|
|
79
|
+
candidate ticket. A case twin of a retired spelling (`multiedit`, `TASK`) is no case twin of a catalog name and stays a rule.
|
|
80
|
+
A row stored under a name that now reads as a twin (only a build that accepted the twin wrote one) is read as no rule: the
|
|
81
|
+
person's lane fails a deny or ask of it closed and lets an allow grant nothing, while the sync join drops such a row from the
|
|
82
|
+
round and reports it (`invalid_rule_text`).
|
|
83
|
+
- **Behavior narrowing (open to veto).** What changed: a bare case twin of a catalog tool no longer stores. Who is hurt: a host
|
|
84
|
+
that wrote a rule text such as `BASH` expecting it to land (it reached nothing), or that mounts its own tool under a catalog
|
|
85
|
+
name's case twin. Way back: bare-name rules exist only in the unreleased #1949 form, so no stored row of the refused form is
|
|
86
|
+
in a released build **provided this change ships in the same version as #1949**; shipped apart, a row a build accepting the
|
|
87
|
+
twin wrote would meet the sync drop above, and the answer is a grammar-stamp bump, not this section. A veto restores the
|
|
88
|
+
retired-only judgment in one function (`parseWholeToolRule`).
|
|
89
|
+
- @server: a rule text typed by an operator can now come back `unsupported.tool` for a twin (render the sentence as given).
|
|
90
|
+
@cli: the same sentence on an import report.
|
|
91
|
+
|
|
92
|
+
### The engine's own account leaves through one throat per channel and is minted there — a notice (its sentence and every string leaf of its detail), a trace frame (every string leaf) and a memory announcement item pass the `same-principal` tier where they are delivered, not where each was worded; a host Brain's failure words, a response's reason phrase and a worktree mint's thrown text are minted where the engine receives them (#2529, design/1449 slice S2a; behavior narrowing, open to veto; hard-breaking: none; closed sets: none; exports: none) (@server @cli @client-core @test)
|
|
93
|
+
|
|
94
|
+
- **What changed.** The engine's account channels each have one delivery function, and that function now mints what crosses it,
|
|
95
|
+
so a mint site that splices in a host's, a store's or a remote's words (a thrown text, an env value, a provider's word) no longer
|
|
96
|
+
has to remember to:
|
|
97
|
+
- `deliverEngineNotice` — the one throat every `EngineNotice` crosses — mints `message` and every string leaf of `detail` through
|
|
98
|
+
the `same-principal` text pass. The detail's member names are the engine's own closed words and are kept as written (a detail
|
|
99
|
+
`key` or `tokenCount` is not a credential name); a value inside the detail that is not plain data (an error, a class instance)
|
|
100
|
+
is read the way a value from elsewhere is read (`egressValue`: every string member passed, a credential-named member whole,
|
|
101
|
+
under that value's own member bound).
|
|
102
|
+
The unwired seat's console line is the minted sentence. A host Brain's finish word in `route.finish_reason_unknown` crosses
|
|
103
|
+
the same throat.
|
|
104
|
+
- `emitTrace` — the one throat of every trace frame — mints every string leaf of the frame; its kind, numbers and member names
|
|
105
|
+
are kept, and no member bound applies to what the engine built (plain arrays and records), in a frame and in a notice's
|
|
106
|
+
detail alike: its members are carried however many there are (a function member, an unreadable accessor and a member nested
|
|
107
|
+
past the depth bound are not, as before). The member bound applies to a value from elsewhere only (an error, a class
|
|
108
|
+
instance, a proxy — whose traps can answer a fresh value at every read), one bound per such value, the bound `egressValue`
|
|
109
|
+
reads it under; an object such a value reached first answers that one reading where the engine's data reaches it again.
|
|
110
|
+
- The memory announcement queue (`enqueueMemoryAnnouncement`; the next session's first injection reads it, so it is
|
|
111
|
+
model-facing) mints each item, and a standing condition's family prefix by the same pass (so a condition whose sentence carries
|
|
112
|
+
a credential still holds one line and still clears); the queue's one read path mints the items it reads too, so a line an older
|
|
113
|
+
engine or a neighbour left in `announcements.json` reaches the next injection minted; the consolidation driver's failure items
|
|
114
|
+
mint the failure's detail before their 300-character bound.
|
|
115
|
+
- A station that bounds a store's words before the notice throat mints them before its cut: the cross-boundary record notice
|
|
116
|
+
(`config.cross_boundary_record_unwritten`) mints the store's failure text, then bounds it to 300 characters. A detail member
|
|
117
|
+
that cannot be read (a throwing getter) is left out on its own; the members around it are read once and kept; an object the
|
|
118
|
+
detail reaches twice (as data and as an error's cause) is read once.
|
|
119
|
+
- The three lanes that account for a failed compaction pass — the turn boundary, the end-of-task pass and the prompt-too-long
|
|
120
|
+
recovery — read one reason: the thrown words minted, then bounded to 512 characters. The end-of-task and recovery lanes
|
|
121
|
+
carried the raw words into `compaction_outcome{failed}.reason` (and the end-of-task lane into its trace frame) before. The
|
|
122
|
+
`task.onerror_sink_failed` frame's reason is minted before its 256-character cut.
|
|
123
|
+
- A host Brain is its own transport: the failure words of a final message it answers are read once and minted where the loop
|
|
124
|
+
receives the message (`remoteWords(…, [])` — a fixed point over the engine brains' words, which their builders minted, so those
|
|
125
|
+
messages pass unchanged; a message whose words are an accessor or a proxy's answer — whatever the first read answered — is
|
|
126
|
+
carried as a copy holding the one minted reading, built without reading the member again, so a second read can neither answer
|
|
127
|
+
differently nor throw), and a Brain that throws has its thrown text minted where the harness builds the failure from it. So
|
|
128
|
+
`TaskResult.errorMessage` / `terminal.message`, the session archive's `API Error` record and the events read minted words for
|
|
129
|
+
every brain.
|
|
130
|
+
- WebFetch reads a response's reason phrase (`statusText`) through one minting reader: the failure headline (it sits outside
|
|
131
|
+
the untrusted fence), every `details.codeText`, and the SearXNG backend's failure text.
|
|
132
|
+
- The Agent tool carries a worktree mint's and a cleanup check's thrown text minted (`details.cause`, `details.worktreeCause`,
|
|
133
|
+
the report's settle line).
|
|
134
|
+
- **Behavior narrowing (open to veto).** What changed: a notice's sentence or detail, a trace frame, a memory announcement, a
|
|
135
|
+
compaction failure reason, a host Brain's failure text, a WebFetch failure headline or `codeText`, and a worktree failure cause
|
|
136
|
+
no longer carry a credential-shaped string (the credential vocabulary's forms — vendor tokens, bearer values, `key=` / `token:`
|
|
137
|
+
label values, URL userinfo, signed URL parameters); a notice's detail and a trace frame reach the host as a new value (a data
|
|
138
|
+
snapshot of what the engine built), never the object the mint site built; a host Brain's failed final whose words the mint
|
|
139
|
+
changes is carried as a copy holding the minted words. Who is hurt: a host or a test that matches a notice's or a frame's
|
|
140
|
+
words literally where they held such a string, that keeps a reference to a detail object to compare it, or that reads a
|
|
141
|
+
credential-shaped value a notice's detail carried (a raw env-knob value that happens to have a credential's shape). How to
|
|
142
|
+
migrate: read the structured members (codes, counts, names, statuses); a credential-shaped value is not carried.
|
|
143
|
+
- **Consumers.** @server: notices, trace frames and the compaction failure reasons on runs arrive minted (core's marker
|
|
144
|
+
`[redacted]`; the server's own marker and its read-path redaction are unchanged). @cli: an engine notice's console fallback line
|
|
145
|
+
is minted; the engine's other, direct `console` lines are not yet (S2b), so `engine-<pid>.log` can still hold what a host's or a
|
|
146
|
+
store's throw wrote (KNOWN-LIMITS KL-1449-a). @client-core: notice `message` / `detail` arrive minted. @test: notice, trace and
|
|
147
|
+
announcement texts that carried credential-shaped words now read the marker.
|
|
148
|
+
- **Not in this slice (S2b).** One engine console function and a closed-set gate "no `console.*` in src outside it" (27 direct
|
|
149
|
+
console calls in 16 files); the `onError` lane, which still hands a host the thrown value itself; side calls that read a host
|
|
150
|
+
Brain's `errorMessage` outside the loop (the classifier ladder, the advisor, memory consolidation); announcement items that
|
|
151
|
+
bound foreign words before the queue (the queue mints whole words only), and stations elsewhere that cut a thrown text before
|
|
152
|
+
any mint; the provider's echoed `responseModel`.
|
|
153
|
+
- **Pins.** `test/backlog2529-account-throats.test.ts` (N1 / N1b the notice sentence, wired and unwired; N2 the detail's
|
|
154
|
+
string leaves and kept names; N3 a foreign value in the detail; N5 an unreadable detail member; N6 the cross-boundary record
|
|
155
|
+
notice's cut; N7 one walk; N4 control; T1 a trace frame; T2 the sink-failure reason minted before its cut, through a real
|
|
156
|
+
Runner; L1–L3 the three compaction lanes with a host summarizer, L2 a control (that lane minted before); H1 a host Brain's failed
|
|
157
|
+
final through a Runner and the file session archive; H2 a Brain that throws; H4 / H5 failure words behind an accessor; H3
|
|
158
|
+
control; W1 / W2 the WebFetch reason phrase; A1 the Agent tool's worktree mint; M1 / M3 / M4 / M2 the announcement queue, its
|
|
159
|
+
family prefix and its read path); `test/backlog1449-account-unbounded.test.ts` (U1 a real Runner's `prompt.assembled` frame for
|
|
160
|
+
a 300-tool mount carries the members a 2-tool mount's frame carries — `ts`, `totalChars`, `snapshot`, `lowering`, every tools
|
|
161
|
+
row; U2 a 3 000-row frame reaches the tracer as built; U3 a credential-shaped string past 2 000 members is still minted; U4 a
|
|
162
|
+
3 000-member notice detail; U5 a value from elsewhere under its own bound, the one after it whole; U6 a proxy that mints a fresh
|
|
163
|
+
proxy at every read is read under the bound, not followed level by level).
|
|
164
|
+
|
|
165
|
+
### A session write belongs to the leg that issued it, and what a resumed leg's continuation carries settles at that message's own receipt (#843 / #2080; behavior change, open to veto; hard-breaking: none — no store contract change; closed sets: none; exports: +1 root name `teardownLegBudget` (function, #2557)) (@server @client-core @cli)
|
|
166
|
+
|
|
167
|
+
- **Before.** A bounded teardown leg that wrote to the session (the date anchor, the workspace state, the announced-listing
|
|
168
|
+
snapshot, a `resumeAt` write-back) and outlived its 15 s bound kept running while the leg's session lock was released at
|
|
169
|
+
its result. The session's next leg on the same Runner woke the session and wrote beside it: when the late write landed
|
|
170
|
+
first, the next leg's own first append lost the leaf CAS — that run failed `conflict` and its objective never reached the
|
|
171
|
+
transcript; when it landed second, the late write failed with nobody told. Separately, a resumed leg's continuation was
|
|
172
|
+
marked delivered when the loop took the prompt, before the message was in the session: a failed append of it lost its
|
|
173
|
+
parked steers, the caller's wake message and the orphans' obituary with no account, and a review gate's reject carried by
|
|
174
|
+
it read as delivered, so its row was not reopened.
|
|
175
|
+
- **Now.** (1) The writes a leg issues to its session stay the session's until they settle (a Runner-level ledger beside the
|
|
176
|
+
session lock). The session's next leg starts only once they have: it waits at most the teardown bound and otherwise fails
|
|
177
|
+
`conflict` before writing anything. A write a leg issues after its leg ended is not started; a write that settles after its
|
|
178
|
+
leg ended is reported through `onError` (`phase: "degraded"`, "… it landed" / "… it failed: …"). A session with no write in
|
|
179
|
+
flight takes no new suspension point at its hold: the ledger is asked synchronously and only a busy session awaits the
|
|
180
|
+
settle (the leg's failure path keeps the microtask depth it had). `rewindFiles` answers
|
|
181
|
+
`rewind.session_busy` while such a write is in flight. (2) A resumed leg's continuation — parked steers, the wake message,
|
|
182
|
+
the approving person's words, the orphans' obituary and a review gate's decision — is settled when the continuation message
|
|
183
|
+
is in the session (the prompt's own append receipt, the step the live steer lane already settles at): a failed append
|
|
184
|
+
leaves it owed, so the frames are announced as `task.user_steer_undrained` and an undelivered reject returns its row to
|
|
185
|
+
pending. The resumed frames' `human_input` "applied" accounts go out at that step (still before the message's
|
|
186
|
+
`message_committed`).
|
|
187
|
+
- **Behavior change (open to veto).** *What changed*: a leg on a session whose previous leg on the same Runner left a write
|
|
188
|
+
unsettled waits for it (up to 15 s) and then fails `conflict` up front instead of starting beside it. *Who is hurt*: a
|
|
189
|
+
deployment running same-session tasks back to back on a store slower than 15 s per append — the second task now fails
|
|
190
|
+
before it starts (it used to fail mid-run, or drop the late write in silence). *How to migrate*: nothing to change — retry on
|
|
191
|
+
`conflict` as before; a store that slow is already reported by `TeardownLegTimeoutError`.
|
|
192
|
+
- **Limits (stated).** The ledger covers writes that reach the store; the harness's own queued writes held under an abort die
|
|
193
|
+
with a durable-suspended harness as before. The bounded wait is the drain — waiting behind a leg that is still running is
|
|
194
|
+
unbounded as before, and so is a leg wedged inside an awaited in-run append. The fence is one Runner: two Runners sharing a
|
|
195
|
+
store, or two processes, stay fenced by the leaf CAS only. A resume refused at the drain was already claimed; it takes the
|
|
196
|
+
existing reopen path (`env_failed`) — and, like any resume that fails between its claim and its leg, it drops a wake message
|
|
197
|
+
with no account.
|
|
198
|
+
- **Exports (#2557).** +1 root name `teardownLegBudget` (function, `(): number`): the bound in force for one teardown leg, in
|
|
199
|
+
milliseconds (15 000) — the same number the session's next leg waits at most for a late write. @server (S-2153): a pod
|
|
200
|
+
reclaimer's release wait reads it from the root instead of keeping a copy of the constant (`ENGINE_TEARDOWN_RELEASE_MS`).
|
|
201
|
+
- **Consumers.** @server: no store change (`PgSessionStorage` / `TiDBSessionStorage` / the window and fork-routing stores
|
|
202
|
+
unchanged); the new `conflict` arm meets the existing evict-and-retry. @client-core / @cli: `task.user_steer_undrained` rows
|
|
203
|
+
can now appear for a resumed leg's parked steers / wake message (the existing family, counted under `steer`).
|
|
204
|
+
- **Pins.** `test/backlog843-session-write-hold.test.ts` (W1 the abandoned write lands first and the next leg runs on top of it;
|
|
205
|
+
W2 it does not settle in time: the next leg is refused up front, a later one runs; W3 the ledger's clauses; W4 the rewind
|
|
206
|
+
verb's busy fact; W5 an idle session's hold takes no drain await — the hold resolves on the lock's own depth and the ledger is
|
|
207
|
+
never asked to settle; C1 / C2 a parked steer / a wake message on a failed continuation append are announced; C3 a delivered
|
|
208
|
+
continuation owes nothing and "applied" precedes the commit; C4 an undelivered plan reject reopens; C5 a refused leg's
|
|
209
|
+
stream settles at once). Re-pinned to the surviving reading: `backlog554-interrupted-continuation`,
|
|
210
|
+
`backlog554-resume-batch-settlement` (the loop's hand-off seam is retired; delivery is the prompt's `message_end`).
|
|
211
|
+
|
|
212
|
+
### A resume's claim is reversible by the store contract — `CheckpointStore.reopen` is a required verb (the `redecision` declaration retires), every refusal after the claim returns the row to pending, the result says what happened to it, and a leg is compensated once (#1914, and #1900's resume-lane and alias cells; hard-breaking, open to veto; closed sets: none; exports: `checkpointStoreExecutionUndeclared` renamed `checkpointStoreIncomplete` (it names every required member a store lacks — the `execution` declaration, the `reopen` verb — and answers `undefined` when none is missing); `CheckpointStore.reopen` becomes required; `CheckpointStore.redecision` removed) (@server @dgx)
|
|
213
|
+
|
|
214
|
+
- **Before.** The resume ladder wins the checkpoint's CAS before the resumed leg prepares; a refusal at that prepare (the doors that read the leg the claim's hand-off builds — interaction posture, the approval seat, the model gates, prepare's own stretch — and every judgment that needs the assembled roster: a near-miss own list, a re-supplied tool whose name no caller tool may carry) could return the row to `pending` only through the store's optional `reopen`. A store without it kept the row `resolved` — the approval gone — while the leg's failed result was byte-identical to the one a reopening store produced (the thrown exit added no account of the row), and three documents said the row stays pending "on a store with `reopen`" or that the engine probes for it (INTEGRATION-CORE, KNOWN-LIMITS, SERVICE-INTEGRATION-GUIDE). The returned exit answered a definitive reopen refusal with "its state is unprovable".
|
|
215
|
+
- **Now.** One contract: `reopen` is a required verb, and the verb is the contract — the store seat (`resolveCheckpointStore`, the one function every seat read goes through) refuses a store that does not carry it, beside the `execution` declaration; no declaration stands for it (`redecision` retired, and with it the sandbox-admission mode's own read of it: a resolved store is re-decidable). A claimed leg that ends before its decided action is disposed — a failed restore, a refusal at its prepare, an abort, a leg that returns failed — returns the row through it, under the failure's reason (`config_refused` for a configuration refusal). What happened to the row is ONE reading of the store's answer (`compensateUnstarted`) and one sentence per outcome on every exit (the thrown backstop, the returned settle, the post-claim abort): reopened (the leg keeps its own code; the message adds that the checkpoint was REOPENED and what to do), declined (`reopen` answered `false`: the row is no longer the resolved row this leg claimed — missing, expired, its decided action already disposed, or pending again — read it before deciding), unknown (the reopen threw: state UNKNOWN, the store's error named through the same-principal redaction on every exit; a throw is never read as a refusal, whatever code it carries). One compensation per leg: a leg whose result was already minted (a tail step threw after the run body settled it) is not reopened, unpinned or torn down a second time by the backstop. Claim and prepare keep their order: the leg's prepare rewinds the transcript, restores the paused environment and dials its peers, which only the claim's winner may do, so the screen before the claim stays the early exit and the reversal is what makes the late one safe. Reach, stated: a deployment's post-consume hook (`RunInternals.afterCheckpointResolve`) that throws, or a post-claim abort after it ran, leaves the row to the hook owner's rollback as before.
|
|
216
|
+
- **Hard-breaking (open to veto).** (1) *What changed:* a store without the `reopen` verb is refused at the first read of either store seat (`config.invalid_checkpoint_store`, the sentence naming each missing member), and the engine has no lane for one (a refusal after the claim always asks the store to reopen). *Who is hurt:* a hand-written `CheckpointStore` that omits `reopen` — TypeScript reports it at compile time, a JavaScript store (or a wrapper that does not forward the verb) at the first prepare; server's SQL and local stores already implement it. *How to migrate:* implement `reopen` (the store-contract kit's reopen cases are the reference) and forward it in every wrapper. (2) *What changed:* `CheckpointStore.redecision` is removed from the contract and read nowhere. *Who is hurt:* server's SQL and local stores and any deployment store (@dgx) that declare `redecision = { reopen: true }` — the field becomes a dead member (no compile error for a class; an object literal typed as `CheckpointStore` gets an excess-property error), and a test pinning it pins nothing the engine reads. *How to migrate:* delete the field; nothing replaces it. (3) *What changed:* the root export `checkpointStoreExecutionUndeclared` is renamed `checkpointStoreIncomplete` and returns `undefined` for a store that carries every required member. *Who is hurt:* a host wrapper that minted the engine's refusal itself. *How to migrate:* call `checkpointStoreIncomplete(seat, store)` and throw what it returns.
|
|
217
|
+
- **Behavior change (open to veto).** (1) A failed result whose claimed row was reopened now says so in its message (the code is unchanged), and a reopen the store declined says "DECLINED … read the row" on every exit instead of "terminally consumed" (thrown and abort exits) or "unprovable" (the returned exit). The codes do not separate every outcome on the returned exit (the leg's own code is kept either way) — a structured outcome member is a follow-up. (2) A widening: the sandbox-admission mode now arms over a store that carries `reopen` but never declared re-decision (before, a missing `redecision` declaration kept it off); every store's `reopen` is now contract.
|
|
218
|
+
- **Consumers.** @server: delete the two `redecision` fields (`checkpoint-store-sql.ts`, `local-checkpoint-store.ts`) and the test that pins them; a test that wraps a store without forwarding `reopen` is refused at the seat; `resume.reopen_failed_unstarted` / `checkpoint.reopen_failed` keep their codes, their sentences change. @dgx: a deployment store implements `reopen` if it lacks it and drops `redecision`.
|
|
219
|
+
- **Pins.** `test/backlog1914-claim-reversible.test.ts` (S the seat: both seats, a member that is not a function, a proxy that hides the verb, both members missing named in one refusal, the in-tree stores accepted with no declaration, the sandbox-admission mode arming over a store that declares nothing; R seven refusals after the claim — interaction posture, an unknown model, a reserved card on a caller tool, a near-miss own list, a re-supplied tool named `-deploy`, one whose second alias is `-x`, one whose alias is not a string — each: the leg's own code, the row pending under `config_refused` at `rev` + 2, the reopened sentence, the corrected resume redeeming the same token; O the three exits under a store whose `reopen` answers `false`, one that throws after committing, and one that throws an error carrying the refusal's own code (still unknown); a credential in the store's error redacted on all three exits; one compensation per leg — a minted result is not reopened again). `test/docs-contract-pins.test.ts` reads the required call, not the retired probe. The files that pinned the removed lane (a store with `reopen` deleted) re-pin onto a store whose `reopen` declines, each cell keeping its purpose (before-the-claim cells: the row pending and byte-identical; after-the-claim cells: `checkpoint.reopen_failed` with the leg's refusal as the lead).
|
|
220
|
+
|
|
221
|
+
### One judge answers "does this shell command only read" for every read-only consumer — the parallel-batch admission, the read-only shell face and the transcript gate stop judging by the program's name, and a face's program list only narrows the judge (#1438 S1; behavior change and security narrowing, open to veto; hard-breaking: one (`readOnlyShellVerdict` takes an options object); closed sets: none; exports: +1 type `ReadOnlyShellOptions`) (@server @test @cli)
|
|
222
|
+
|
|
223
|
+
- **Before.** Four functions answered the question. The read-only reader (`readOnlyShellVerdict`) served the allow layer and a compound's segments; the reversibility classifier served the full shell's probe; a name-only check served the shell tool's `isConcurrencySafe` and the read-only face's execute guard; the transcript gate had a name check of its own. For the last two a program's name on the list was enough: with `bashReadonlyAllow` widened to `sort`, `sort -o out f` ran on the read-only face, and with `readAllow` widened to `sed`, `sed -i 1d <transcript>` passed the transcript gate.
|
|
224
|
+
- **Now.** `isConcurrencySafe` = the reader, narrowed to the shared list (`BASH_READONLY_DEFAULT_ALLOW`: programs with no effect beyond reading under any arguments — no `cd`, which moves the committed working directory, no git, whose configuration can run helpers), reads the call as read-only, and the call is not `run_in_background`. Upstream reads `isConcurrencySafe = isReadOnly` with no list; this release keeps the narrower set (a divergence, open to veto). The reader no longer peels `noglob`: upstream strips it as a zsh precommand, and this engine's shell is bash, where `noglob` is a program or function looked up like any other (a divergence, stricter, open to veto). The read-only face keeps its single-simple-command admission and runs a call only when the reader, narrowed to the face's list (`readers`), reads it read-only. The transcript gate lets a command that references the transcripts through only when the reader, narrowed to `readAllow`, reads it read-only. The name-only check (`coarseReadonlyCheck`, never on the package root) is gone. A program outside a face's list is refused with that face's one sentence (`command "x" is not in the read-only allowlist`), minted in one place for the reader and the classifier. The classifier itself is unchanged in this release.
|
|
225
|
+
- **Hard-breaking (open to veto).** *What changed*: `readOnlyShellVerdict(command, shape?, cwd?, bound?)` became `readOnlyShellVerdict(command, { shape?, cwd?, bound?, readers? })`; `readers` (a `Set` of program names — the check is that `has` is callable, so a `Map` passes it too; a value without a callable `has` throws) narrows the answer to lines whose every named program — each segment's program past a `command` / `builtin` prefix, and an `xargs` segment's target — is listed. *Who is hurt*: a caller passing the old positional arguments — none found outside the engine (server's consumption ledger records the reader as not consumed; test's repro scripts pass the command alone); in-tree, two engine call sites and six test calls moved. *How to migrate*: move the positional arguments into the object (`readOnlyShellVerdict(c, s, cwd, b)` → `readOnlyShellVerdict(c, { shape: s, cwd, bound: b })`).
|
|
226
|
+
- **Behavior changes (open to veto).** *Parallel batches*: read-only pipelines and compounds of listed programs (`ls | wc -l`, `cat a | grep x`, `ls && pwd`) and a listed program behind `command` / `builtin` now join a parallel batch with other read-only calls (they ran in order); a listed program's form the reader refuses (the 13 `grep` spellings below) now stays ordered; git, `sort`, `find`, `cd`, writes, environment assignments outside the reader's names and path-prefixed programs stay ordered as before. *Allow layer*: `noglob <program>` is no longer read-only (it ran without a question). A repository's own configuration can make a git read form run a program (`diff.<driver>.textconv`, an external diff), which the allow layer runs without a question, as before and as upstream does (KL-1438-a); the upstream reader also probes the directory for bare-repository indicators and an unverifiable `.git` redirection before calling a git command read-only, and this reader does not (a named gap, now ticketed). *Read-only face*: a listed program's writing call is refused (`sort -o out f`, `uniq f out`, `gzip f`, `tar cf out.tar f` with the program listed); a listed program behind a `command` / `builtin` prefix runs (`command cat f` — it was refused by the name `command`), and so does it in the transcript gate; over the #994 dual-run corpus, 15 strings the default list used to run are now refused — 13 `grep` spellings the upstream flag walk refuses too (12 short-option clusters carrying a value: `grep -if pat f`, `grep -e/api f`, `grep -f~/x f`; and the abbreviated `--reg=pat`), and two unterminated quotes (`echo "x`). *Transcript gate*: a read-only compound over the transcripts (`cat <t> | grep x`) is allowed (it asked); a listed program's writing call asks (`sed -i` with `sed` in `readAllow`); the ask sentence carries the reader's reason.
|
|
227
|
+
- **Consumers.** @server: `createTranscriptIntegrityPolicy` with the default `readAllow` (`deployment-governance.ts`) — no change needed; a read-only compound over the deployment's transcripts no longer asks. @test: `readOnlyShellVerdict(cmd)` with the command alone is unchanged; the classifier half of the 38-command table reads as before. @cli: none (the verify scripts drive the classifier).
|
|
228
|
+
- **Comments made true.** The tightening reader is the one lexer's projection, not a lexer of its own (rule model, peel projection, root export note) — the lexer half of #1647 was closed by the one-lexer work; its walk half stays with #1536.
|
|
229
|
+
- **Pins.** `test/backlog1438-one-readonly-judge.test.ts`; re-pinned: `tool-parallelism` (`ls | wc -l` is safe), `backlog619` (`noglob ls` is not read-only), `backlog482-cc-e6s-allowlist` (the shared list narrows the reader), `backlog994-s1-dual-run` (the `coarse` column retired), `backlog619` / `backlog1116` / `backlog1685` / `backlog994-s2` (the options object).
|
|
230
|
+
|
|
231
|
+
### A remote endpoint's words reach the engine's own text through one mint — what the request was declared with goes first, then the credential vocabulary, then any cut; persistence is not an exit (#1449 slice S1, #2143 rule; design/1449 r2; behavior narrowing, open to veto; hard-breaking: none; closed sets: none; exports: none) (@server @cli @test)
|
|
232
|
+
|
|
233
|
+
- **What changed.** One function, `remoteWords(text, declared)` (`src/core/protocol-http-auth.ts`), is the form a remote's words
|
|
234
|
+
take when the engine carries them in its own words: every declared value of the request, in each spelling the request-echo
|
|
235
|
+
pass derives, then the `same-principal` credential vocabulary; where a declared value is there, the values the vocabulary finds
|
|
236
|
+
in the ORIGINAL words go with it, so removing a declared value never hides a credential from the vocabulary (a declared `Bearer`
|
|
237
|
+
and the token after it both go) and the vocabulary never leaves the rest of a declared value; words with no declared value in
|
|
238
|
+
them read exactly as the vocabulary renders them. Words are minted once, where they enter the engine's text. Its readers: the
|
|
239
|
+
MCP failure sentence (the module's private copy is gone), the model brains — where a failure is built (an HTTP error body, a
|
|
240
|
+
transport's own throw on connect or mid-stream, a request the runtime refused to build, an in-band error frame), the stream's
|
|
241
|
+
terminal shell for a value no builder minted (a transport's or a seat's own throw, a cancel; a built failure is carried as its
|
|
242
|
+
builder minted it, so the engine's own display-redacted roots keep their `«redacted»`), and the words a finalize writes without throwing (a refusal's words, the
|
|
243
|
+
provider's stop details — read once, so the sentence and the `diagnostics` record agree — a Responses terminal status and
|
|
244
|
+
reason, the malformed-frame sample, a malformed tool call's name and argument fragment, a word that arrived after the stream
|
|
245
|
+
ended) — and the memory distiller seat's failure, its transport's throw included. A brain request states what it was declared with (`SSERequest.declared`: the entry's, the
|
|
246
|
+
deployment's and the call's header bags as the route kept them, the key when it names a secret — a placeholder key (`EMPTY`, a
|
|
247
|
+
one-letter local key) is left in the words as it is left to every child — and on the Anthropic lane a configured version and
|
|
248
|
+
each entry of a declared beta list); the engine's own protocol words (`content-type`, the default version, the betas it chose)
|
|
249
|
+
stay readable. The words are minted before they are cut, so a cut leaves no piece of a value; a failed body that did not end
|
|
250
|
+
(torn, past the 64 KiB read bound) still reports, as before, but its last, unfinished word is not carried — a value's start
|
|
251
|
+
no pass can recognise is always in that word. A provider's echoed `requestId` or `apiErrorCode` rides the
|
|
252
|
+
failure only when the mint would leave it as it is; otherwise it is dropped (`brain.echo_dropped`, the existing trace).
|
|
253
|
+
The mint is a fixed point — it never rewrites one of the four marker literals a pass writes, and a marker-SHAPED word a remote
|
|
254
|
+
wrote is no marker — so a text minted twice (an MCP failure's rethrow and its status face) reads the same; before, a short
|
|
255
|
+
declared value could rewrite the marker (`[[redacted]acted]`).
|
|
256
|
+
Because the session archive's `API Error` record, `TaskResult.errorMessage` / `terminal.message`, the StopFailure hook and
|
|
257
|
+
the memory announcements read the brain's minted text, none of them carries the provider's echo of the request any more.
|
|
258
|
+
- **The rule this states (#2143).** Persistence is not an exit: whether bytes are redacted is decided by who wrote them, where
|
|
259
|
+
the engine words or carries them. Content — what a model, a tool or a user wrote: the session archive's message entries,
|
|
260
|
+
journal replay payloads and a failed row's model output, checkpoint call arguments, spilled tool results, the memory
|
|
261
|
+
distiller's plan and transcript archive — is kept verbatim by the engine's own stores (a replayed thinking block is checked
|
|
262
|
+
byte for byte by its provider; evidence rewritten is no evidence; the reference implementation keeps its transcripts
|
|
263
|
+
verbatim), in files created 0600 under directories created 0700. The engine's own words are minted already redacted. A host
|
|
264
|
+
redacts content at the exit it writes it FOR someone (`docs/INTEGRATION-CORE.md` §0 item 4, unchanged). So the reasoning a
|
|
265
|
+
server's run-event log shows redacted is in the engine's session archive as the model wrote it — two records, one rule.
|
|
266
|
+
- **Behavior narrowing (open to veto).** What changed: a model brain's or the memory distiller seat's failure text no longer
|
|
267
|
+
carries the request's declared values or credential-shaped strings, a torn or over-bound error body no longer shows its
|
|
268
|
+
last, unfinished word, an echoed `requestId` / `apiErrorCode` (or a late frame's word, now `?`) that carries either is absent,
|
|
269
|
+
and the Anthropic `stop_details` diagnostics record holds only its `category` / `explanation` strings, minted. Who is hurt: a
|
|
270
|
+
host, test or operator that matches provider error sentences literally, or keys a support ticket on a request id the
|
|
271
|
+
provider echoed a credential into. How to migrate: read the structured seats (`apiErrorStatus`, `apiErrorCode`,
|
|
272
|
+
`providerCondition`, `requestId`, `errorKind`, `errorBodyIncomplete`), not the sentence.
|
|
273
|
+
- **Differs from the reference (open to veto).** The reference turns a failed API call into a synthetic assistant message
|
|
274
|
+
holding the provider's text as it came; here the failure text is minted at the transport (stricter).
|
|
275
|
+
- **Consumers.** @server: run rows' `failed.errorMessage` now arrive minted (core marker `[redacted]`; the server's own marker
|
|
276
|
+
and read-path redaction unchanged); `requestId` may be absent on a failure that echoed a credential. @cli: KL0170 stays true
|
|
277
|
+
for model replies; the archive's `API Error` records no longer hold the request's declared values. @test: the session
|
|
278
|
+
archive's message entries differ from a redacted run-event log by design.
|
|
279
|
+
- **Not in this slice.** One mint for the engine's account lines — console lines, notices, traces, the `onError` lane, memory
|
|
280
|
+
announcements' other sources, a host Brain's failure text, WebFetch's failure headline, a sub-agent isolation failure's
|
|
281
|
+
`details.cause` (design/1449 S2); declared values written with JSON `\u` escapes (S3); permission checks on existing store
|
|
282
|
+
files (S4); A2A failure text (its own ticket, reading the same mint).
|
|
283
|
+
- **Pins.** `test/backlog1449-remote-words.test.ts` (B1 a real endpoint's 401 echo through a Runner: result, events and the
|
|
284
|
+
session archive; B2 the Anthropic lane; B3 the cut; B4 in-band frames, three lanes; B5 / B5b the distiller seat's body and
|
|
285
|
+
its transport's throw; B6 the vocabulary half; B7–B7d the mint, its fixed point, a forged marker, a declared label; B8 a torn
|
|
286
|
+
body; B9–B9c a transport's throw on connect and mid-stream, an unbuildable request, a cancel; B10 the echoed seats; B11 a
|
|
287
|
+
configured version and beta entries; B12–B12c a refusal, a malformed sample, stop details with a null explanation, a late
|
|
288
|
+
word and a Responses terminal reason; B13 a malformed tool call's fragment; C1 protocol words stay readable; C2 the archive
|
|
289
|
+
keeps reasoning verbatim).
|
|
290
|
+
|
|
291
|
+
### `importHistory` says `unsupported_format` only when the door refuses the envelope; every failure after the door — a source that cannot supply a blob (its read throws, or its bytes fail their address), a fault in staging or publishing — is `adopt_failed` (#2562; behavior change, ruled C-R857, open to veto; hard-breaking: none; closed sets: none; exports: none; members: none) (@server @test)
|
|
292
|
+
|
|
293
|
+
- **Before.** `FileHistoryError`'s contract text said a source that cannot supply a referenced blob, and a fault in the publish step, answer `adopt_failed` (the adoption family's word) and that `unsupported_format` names a bad envelope. The bundled backends answered three things differently: a source whose read THROWS while serving a blob (both backends), a blob whose bytes fail their content address (both backends), and every throw in the File backend's own staging writes and in either backend's catch-all (a disk refusing a good graph) were `unsupported_format` — so a host that reads `unsupported_format` as "the envelope is bad, do not retry" was told that about a healthy envelope, and the server's two SQL stores, which follow the contract text, answered differently from the reference for the same input (two readings of one fact).
|
|
294
|
+
- **Now.** One reading. `unsupported_format` is a word only the import DOOR says: `validateFileHistoryExport` (a foreign epoch, a wrong version, content the closed sets cannot hold — and now also a payload that cannot even be read, a throwing member or a revoked proxy, which used to THROW out of the function; the door now also reads, once, the two members a backend copies without judging their value — `backupTime` and `parentIdent` — so a throwing one is refused here and not found mid-import) and the File backend's own record-naming rule (an entry id that cannot be a file name here — a reserved device name, a leading dot or dash, over 200 characters, an unsafe character), which now runs at the door, before any blob is fetched or byte staged (it used to run mid-staging, after the fetches, and a refused envelope left fetched blobs on disk). Everything after the door is `adopt_failed`, with the caught fault's `permanence` where it has one: a source whose read throws, a source that has no such blob (unchanged), a source whose bytes do not hash to the address asked for, a fault writing the staged graph, a fault publishing it (unchanged in the File backend), and either backend's catch-all. The contract's two sentences (on `FileHistoryError.code` and on `importHistory`) are rewritten to say exactly this, and a test reads them against one table of arms so they cannot drift from the behavior again.
|
|
295
|
+
- **Behavior change (open to veto).** A host that branched on `unsupported_format` from `importHistory` to mean "refuse the payload, do not retry" now sees it only for a payload that is genuinely refused at the door; an unreachable or damaged source and a storage fault arrive as `adopt_failed` (with `permanence` when the fault code is judged). The contract kit now asks the same of a third-party backend: a source whose read throws, and a source whose bytes fail their address, must answer `adopt_failed` (a backend that answers `unsupported_format` there fails the kit's "import door refuses, never coerces" case; the server reports that its SQL stores already answer `adopt_failed` for the throw arm — the wrong-bytes arm is new to them). (@server: no change needed if both arms already answer `adopt_failed`; align the wrong-bytes arm otherwise. @test: the two-database suite reads the kit's new assertions.)
|
|
296
|
+
- **Who is hurt.** A host or test that pinned `unsupported_format` for a throwing or damaged source, or for a staging write fault, on either bundled backend; `test/backlog1468-t2-file-history-permanence.test.ts` is the one in this tree (re-pinned). A caller of `validateFileHistoryExport` that relied on it throwing for an unreadable payload (none known in this tree; the engine's own callers are the two backends).
|
|
297
|
+
- **Pins.** `test/backlog2562-import-failure-words.test.ts`: one table of arms run against both backends (door: foreign epoch / unknown self-description / unreadable payload / entry id the File backend cannot hold, none fetching a blob; adoption family: source read throws with its judged permanence, source has no blob, bytes fail their address, a staging write fault on the tracked set and on a boundary with no half-built graph left, an in-memory publish fault and an in-memory fault after the door); the door never throws over a throwing member, a revoked proxy, a throwing row, and a throwing `backupTime` / `parentIdent` (KL-2562-a records the residue: an in-process object that throws only on a second read); the contract sentences ↔ the table (each row's phrase must stand on the side of the `unsupported_format` sentence its code says; every code word the sentences name is in `FILE_HISTORY_ERROR_CODES`); the kit refuses a backend that answers the old word for either arm.
|
|
298
|
+
|
|
299
|
+
### One selector build — a leg's tool lists are read once by the strict reader, and the selector is built from what was read, before the session is acquired (#2511 / #1913; behavior narrowing, open to veto; exports unchanged)
|
|
300
|
+
|
|
301
|
+
- **Before.** A leg's tool selector (the ceiling it was spawned under, then its own `allowTools` / `excludeTools`) was compiled at the head of prepare by the lenient reader — a list that is not a list, or an entry no rule syntax reads, was read as "hides nothing" — and every early reader consulted that object: the MCP dial gate, the valve, the roster, the memory pair, the ceiling handed to children. The strict reader ran only at the end of roster assembly, on the LIVE layers a second time, and a second compile followed it. So one table had two readers whose agreement rested on their order, a live ceiling was read four times in a run that never parked (its `deny` getter, four; its `allow`, three) and once more when a leg parked — the row's `toolCeiling` stamp read the live layers again, so a getter that changed its answer was recorded differently from the selector the leg ran under — and the strict refusal arrived after the session was acquired and after the declared servers were dialed. Measured on a real Runner, previous stage head: `excludeTools: [" mcp__srv"]` (a leading blank) dialed the declared MCP server once and was then refused; the same entry in a live ceiling layer whose `deny` getter answered it first and `[]` after was never refused at all (the compile read it as nothing, the judgment read the `[]`), and the run completed; a getter that answered `[]` first and the entry after was refused only at the end of roster assembly.
|
|
302
|
+
- **Now.** `readToolSelector({ layers, where, legs })` (`sub-agent-deny-lists.ts`) reads each layer ONCE with the layer reader the run's list judgment used — which now returns what it read, a frozen layer of the entries as written, an absent allowed list staying absent — and compiles the selector from those products; the live layer is never read again. It is called right after the leg's own-list record is bound and before the session acquire, beside the other refused-before-the-acquire doors. `judgeRunToolLists` takes the built selector: its layer loop and its second compile retire, and the selector's layers — which are what the leg hands its children and its workflow agents as their ceiling, and what a parked leg's checkpoint row records as its `toolCeiling` — are the frozen products. Rule count: readers of a leg's selector lists 2 → 1; reads of a live layer per run 3–4 (one more at a park) → 1; the sentence for whose lists the own layer is has one definition (`ownListsWhereOf`), read by the build and the judgment alike.
|
|
303
|
+
- **Behavior narrowing (open to veto).** A leg whose own list or ceiling layer the strict reader refuses is refused before anything is held: the code and the sentence are the ones it always had (`config.tool_face_control`, `unknown_name` / `not_a_list` / `not_a_string`, the ceiling named by its distance, the own layer by `TaskSpec.excludeTools` or the delegation tool that wrote it), but the session store sees no `acquire` (or `forget`) for the refused run, and no declared MCP server is dialed. A live ceiling is read once, so a getter that answers differently the second time is judged and used as it first answered — in either direction (an unreadable first answer is now refused where the compile used to read it as nothing). Not changed: which list a name judgment refuses or keeps, and the order of the refusals after the build (the near-miss, protocol-denied and definition-list judgments still run over the assembled roster, as before). A task with two faults now reports the leg's list first, wherever the other refusal used to sit between the own-list record and the end of roster assembly (a refused `extraBody`, capability declaration or file-history request; a missing session under `requireExistingSession`; a roster or mount refusal).
|
|
304
|
+
- **Consumers.** @server: a refusal of the leg's lists now arrives before the session store is written (`acquire` is not called for it; a store that counted acquires or minted a row on `acquire` sees none for this refusal); the code and sentence are unchanged. Nothing else is asked of the host: `RunToolListsInput.layers` becomes `selector` on an internal function, not an exported name.
|
|
305
|
+
- **What still stands (named, not closed).** `judgeHostToolCeiling` (a workflow host's `RunWorkflowOptions.toolCeiling`) still reads its layers and then compiles the live ones (its own ticket: the refusal's attribution changes when it goes through the build); `compileToolSelector`'s lenient arms stay for its other callers (the spawn-time child selector, the single-layer helpers), which compile lists that are read strictly before any effect; the own-list door (`assertToolNameListValue`) is a third reader of the own layer's list-ness.
|
|
306
|
+
- **Pins.** `test/backlog2511-read-tool-selector.test.ts` (A the dial gate, B one read of a live ceiling, C before the acquire, D the builder, E the judgment reads no layer, F the parked row records the ceiling as read); re-pinned onto the selector: `backlog1875-one-entry-reader`, `backlog1886-r9-closure`, `backlog1910-persisted-selector-canonical`, `backlog1930-near-miss-refuses`.
|
|
307
|
+
|
|
308
|
+
### A background agent has its name from the moment its record exists — the registration writes the spawner's name, or the one derived from the handle (`agent-` + the first twelve hex digits), on the live handle and in the row's first write; `BackgroundAgentRecord.name` is required, so the card of an unnamed parked child can be decided (#2450; hard-breaking, open to veto; behavior change, open to veto; closed sets: none; exports: +1 root name `engineAgentName` (function); `BackgroundAgentRecord.name` and `BackgroundAgentRowSummary.name` go from optional to required) (@server @cli @client-core @test)
|
|
309
|
+
|
|
310
|
+
- **Before.** An Agent call that named no child started a background agent whose live handle and durable row carried no `name` — the park terms never asked for one, by decision, so such a child parked on its approval like any other. A deployment's revive door reads `row.name` and turns a parked row without one away (sema-server answers `409 decide.row_unrevivable`), so the card of an unnamed parked child could be neither approved nor denied: the child sat parked until its checkpoint expired. Two predicates — may it park, may it be revived — answered one question differently, and the engine never made them agree.
|
|
311
|
+
- **Now.** One place writes an agent's name: the background registration (`registerBackgroundAgent`). It takes the spawner's explicit name, else `engineAgentName(handle)` — `agent-` plus the first twelve hex digits of the handle (`a3fa9c2d1b7e04a15` → `agent-3fa9c2d1b7e0`; a pure function of the handle, so the same agent id reads the same name in every process and every store; twelve digits (48 bits: two agents of one caller would have to share twelve digits of their ids to collide, and the name layer resolves a name to the newest holder), not sixteen, so the result is an ordinary admissible name and not the shape of an agent id, which is refused as a name everywhere). The live handle and the durable row take it from that one value, and the row's first write carries it — it is not minted at park time. The park terms and the revive door are unchanged; they now read one fact. A revival hands the row's name back to the Agent call as `name`; when that name is the one derived from the row's handle, nobody chose it, so the call keeps the child unnamed — no spawn identity on its frames, no roster entry, no teammate role appears on its second cycle — and the registration derives the same name again. The other reader that hands a row's name to a place that reads it as an identity, SendMessage's retained-session resume (the child's session kept by this run), takes it through the same reading, so a retained resume does not publish the derived name as a spawn identity either. An explicit name is stored verbatim as before; an empty or non-string `name` handed to `registerBackgroundAgent` is refused (it used to be stored). The row a revival re-registers is healed on the way: a row an earlier version wrote without a name gets the derived one in the revived cycle's first write.
|
|
312
|
+
- **Hard-breaking (open to veto).** *What changed*: `BackgroundAgentRecord.name` and `BackgroundAgentRowSummary.name` are required `string` (they were optional); every row core writes from this version carries one. *Who is hurt*: an implementer of `BackgroundAgentStore` (sema-server's SQL stores, a host's own store) or a host that builds a record or a summary by hand — a TS host sees a compile error where it builds either without `name`; and a store that reads rows an earlier version wrote, which have none (the type is true of rows written from this version on). *How to migrate*: keep a name column (nullable is fine) and read it as `name: row.name ?? engineAgentName(row.handle)` — the derivation core uses, so an old row reads as the name a fresh registration would have given it (core's own in-memory and file stores do not rewrite old rows on read; a revival writes the name into the row, a row that is never revived stays as it is on disk). A door that turns a nameless parked row away keeps working for such rows; for rows read through the line above it never fires.
|
|
313
|
+
- **Behavior change (open to veto) — the derived name is visible.** Wherever a background agent's name is read it is now there for an unnamed agent too: the ListAgents row (it listed the description before), the name layer of SendMessage and TaskStop (`to: "agent-3fa9c2d1b7e0"` reaches the live handle; the description layers still resolve the description), the "did you mean" pool, the durable row. Not changed: the not-found footers of TaskOutput, TaskStop and SendMessage (an agent nobody named is still listed by its task id and description — an address TaskOutput accepts, which resolves exact ids only — and a chosen name is still a bare name), the launch receipt (still the task id), the fleet frames' `name` (the description-backed display label) and `spawnName` (the explicit spawn identity — a derived name is never one), the roster (only explicit names are recorded: after a restart an unnamed agent is reached by its task id, as before). What an unnamed agent is CALLED is not yet one reader's answer: ListAgents shows its derived name (`full?.name ?? t.description`, `list-agents-tool.ts:259`) where the page footer shows its description (`chosenAgentName(h.id, h.name) ?? h.description`, `task-registry-agent.ts:2110`) — #2703 (KL-2450-a).
|
|
314
|
+
- **Consumers.** @server (S-2075): with the name on every row, your revive door's `row.name === undefined` turn-away no longer fires for rows written from this version; read old rows through `row.name ?? engineAgentName(row.handle)` (above) and the unnamed parked card is decidable — the decide route's args (`name: row.name`) are accepted as they are. Your `BackgroundAgentStore` implementations take the type change; the store contract kit's row factory now carries a name. @cli / @client-core: a renderer that reads a background agent's name from ListAgents output or from the durable row now receives `agent-<12 hex>` for an unnamed agent where it used to receive the description (ListAgents) or nothing (the row); the description is still on the row. @test: an unnamed parked child's approve and deny both reach the resume (a decide answered 200 and the run continues), the row shows `agent-<12 hex>` from its first write, and the revived cycle's frames carry no `spawnName`.
|
|
315
|
+
- **Pins.** `test/backlog2450-bg-agent-name-mint.test.ts` — the derivation (form, determinism, admissibility against the spawn grammar and the reserved-name check, a non-handle refused); the registration (the handle and the row's first `put` carry the name, an explicit name verbatim, the same id the same name in another registry, the name resolves through the name layer, a legacy row healed by its revival registration); an unnamed child parked on a real Runner then claimed and driven (approve runs the action, deny does not, the revived cycle stays unnamed on every frame, an explicit name stays a spawn identity, a lookalike name is still one); ListAgents rows; SendMessage by the derived name through tier-3 revival and a revived row an earlier version wrote; a retained-session resume of an unnamed child (no `spawnName` on the resumed cycle's frames or notifications); the not-found footers keep an unnamed agent's task id (the id taken from the footer polls); the two interfaces' required `name` as compile-time anchors. Re-pinned to the new law: `agent-park-jian1-park-mint` (the anonymous child's row carries the derived name) and `agent-park-jian4-durableapproval-forwarding`; fixture rows in eleven store and registry pins carry the `name` the type now requires.
|
|
316
|
+
|
|
317
|
+
### The self-kill guard asks where an environment DECLARES its commands run, not by the remote interface — a host lane is asked again, and `capabilities.sharesProcessTable` is required (#2602; P1 fix; hard-breaking — ruled final, not open to veto (C-R840 加注 2, clay); closed sets: none; exports: none) (@server @dgx @cli @test)
|
|
318
|
+
|
|
319
|
+
- **Before (7.56.0).** The pattern-kill guard (#2348) returned before reading anything for every env that implements the remote
|
|
320
|
+
seam (`isRemoteExecutionEnv`: the interface's members and a capability block). A server's default lane
|
|
321
|
+
(`RemoteHostExecutionEnv`, `provider: "host"`) is such an env and runs its commands as processes of this machine — the
|
|
322
|
+
engine's own process table — so on the product's default lane the guard asked nothing: a black-box run on core 7.56.0
|
|
323
|
+
measured 0 of the 42 kills it refuses on a `NodeExecutionEnv` refused there, and a `pkill -f <word>` whose word was in the
|
|
324
|
+
launcher's command line ended the session that issued it (tool exit 143, the session gone).
|
|
325
|
+
- **Now.** One declared fact: `RemoteExecutionEnv.capabilities.sharesProcessTable: boolean`, required — `true` when the
|
|
326
|
+
adapter's commands run on a table that holds this process (a host lane: they are its descendants; or a PID namespace that
|
|
327
|
+
contains the engine's, such as the host of the container it runs in), `false` only when they run where this process is not
|
|
328
|
+
(another machine, a sandbox container, a pod, a device). One strict reader (only the boolean counts; an env without the
|
|
329
|
+
remote seam reads as this table, which errs toward asking). The guard reads the command first and the declaration second:
|
|
330
|
+
this table ⇒ asked exactly as on a `NodeExecutionEnv` (the same refusals, the same sentences); another table ⇒ not asked
|
|
331
|
+
(what 7.56.0 did for every remote env); absent, not a boolean, or unreadable ⇒ every pattern kill on that env is refused
|
|
332
|
+
(`finding: "unavailable"`, the sentence names `capabilities.sharesProcessTable`) — `kill -9 -1` included — while `pkill -P $$`,
|
|
333
|
+
a kill of other literal pids and every command that is no pattern kill run as before, and the run's admission census
|
|
334
|
+
announces it once (`config.execution_env_capability_invalid`, `detail: { key: "sharesProcessTable", valueType }` —
|
|
335
|
+
`"undefined"` when absent, `"unreadable"` when its getter throws: the one capability key whose absence is announced,
|
|
336
|
+
because it has no default reading; a capability block that cannot be read at all is reported under `capabilities` as
|
|
337
|
+
before).
|
|
338
|
+
- **Hard-breaking (open to veto).** *What changed:* `RemoteExecutionEnv.capabilities` has a required member
|
|
339
|
+
`sharesProcessTable`; an adapter compiled against this version without it does not compile (`implements
|
|
340
|
+
RemoteExecutionEnv`), and one that reaches the engine without it (an untyped adapter, a build against 7.56.0) gets every
|
|
341
|
+
pattern kill refused and one admission notice per run. *Who is hurt:* every remote adapter — server's seven (`adb`,
|
|
342
|
+
`device`, `e2b`, `host`, `k8s`, `local-docker`, `ssh`) and an embedder's own. *How to migrate:* one line in each capability
|
|
343
|
+
block — `sharesProcessTable: true` for the host lane, `false` for the other six; an adapter whose target can be a table
|
|
344
|
+
that holds the engine (SSH to `localhost`, a container started in the host's PID namespace, a lane that executes on the
|
|
345
|
+
host of the engine's own container) declares `true` where its configuration makes that so.
|
|
346
|
+
- **Correction of 7.56.0.** Its note "the guard asks the question of THIS process's table, so it is not asked for a remote or
|
|
347
|
+
sandboxed environment (`isRemoteExecutionEnv`)" was true of the code and untrue of the product: the server's default lane is
|
|
348
|
+
remote-shaped and local, so the guard was absent there (KL-2348-d; exit = this version). KL-2348-b now states the declared
|
|
349
|
+
reach, and what a `true` does not cover: the question is the one a `NodeExecutionEnv` gets, its limits included (it is put
|
|
350
|
+
from the engine's own working directory), and it is asked as for this process's own descendants, so commands on the
|
|
351
|
+
same table that are not its descendants (SSH to `localhost`: BSD's `pgrep` leaves its own caller's ancestors out where the
|
|
352
|
+
real kill would not) and commands in an enclosing namespace (the question sees the engine's namespace only) are answered
|
|
353
|
+
as if they were.
|
|
354
|
+
- **Reference.** The reference implementation's guard is a shell function run in the command's own shell: it asks only when the
|
|
355
|
+
CLI's own pid is visible there (`/proc/$CLAUDE_PID/comm`) and compares the selection with that one pid (cli309.js:266254).
|
|
356
|
+
This engine compares the selection with its whole ancestor chain and decides where to ask by the adapter's declaration.
|
|
357
|
+
Not chosen: asking through the env (`env.exec`) — the chain holds pid 1 whenever the engine's ancestry reaches init, and an
|
|
358
|
+
engine in a container is itself a small pid, so on a sandbox lane in another PID namespace the answer would collide with
|
|
359
|
+
them and refuse ordinary kills (inferred from the code, not run).
|
|
360
|
+
- **Limits (stated).** Unchanged by this change and open: a command whose kill words hold a NUL byte makes the question's
|
|
361
|
+
`pgrep` fail to start, which reaches the tool as an error rather than as the `unavailable` refusal — the command does not
|
|
362
|
+
run either way (KL-2348-e). The `pkill -P $$` way out is read by the option walk that builds the question since #2604, so
|
|
363
|
+
one reader decides both. The undeclared-table notice is once per RUN, not once per deployment: a host that has not declared
|
|
364
|
+
it announces for the parent run and again for every sub-agent run it starts. An environment with no remote interface cannot
|
|
365
|
+
declare at all and reads as this table (KL-2348-b).
|
|
366
|
+
- **Consumers.** @server: the seven capability blocks gain `sharesProcessTable` (`host` true, the rest false); a server that
|
|
367
|
+
has not declared it gets the refusals above with this core. @dgx: the device lane declares `false`. @cli: the product's
|
|
368
|
+
default lane is guarded once the bundled server declares `true`; the refusal is a tool error carrying the sentence. @test:
|
|
369
|
+
core756r2's host-lane arm — expected 42 / 42 refused, with the `NodeExecutionEnv` reading, on a server that declares `true`,
|
|
370
|
+
and 42 / 42 refused as `unavailable`, naming the declaration, on one that does not (this engine's own pins measure the same
|
|
371
|
+
on a host-lane fixture; the black-box arm is not re-run here).
|
|
372
|
+
- **Pins.** `test/backlog2602-self-kill-guard-process-table.test.ts` (T1 46 commands: the same refusal on a declared host lane
|
|
373
|
+
as on a NodeExecutionEnv; T2 controls; T3 / T3b undeclared, malformed and unreadable declarations; T4 a declared other table
|
|
374
|
+
is not asked; T5 the reader; T6 the census; L-H1–L-H3 a child engine whose Bash runs through a host lane). Re-aimed:
|
|
375
|
+
`backlog2348` R5 (a declared other table is not asked; the remote interface alone decides nothing), `backlog787` (its three
|
|
376
|
+
keys; the fourth's absence is pinned here).
|
|
377
|
+
|
|
378
|
+
### One judge of MCP name contention — `judgeMcpNames` answers a declared set's collisions (server names that mint one prefix; one server's tool names that mint one wire name), and the mount, the no-run probe, a listing's intake and a host that saves a set all ask it (#2568; no behaviour change; root exports +5)
|
|
379
|
+
|
|
380
|
+
- **Before.** The same fact was decided in three places, each wording it its own way: the mount minted its refusal sentence from a prefix primitive; the
|
|
381
|
+
no-run probe asked the primitive again and then borrowed the mount's throw; a listing's intake kept a private map of the wire names it had
|
|
382
|
+
minted. A host that assembles a declaration set (a console saving a server list) had no door: a collision was learned only when the next
|
|
383
|
+
run was refused.
|
|
384
|
+
- **Now.** `src/core/mcp-name-judge.ts` is the one judge. `judgeMcpNames(servers)` takes a list of `{ name, tools? }` (`McpServerSpec` fits as it is;
|
|
385
|
+
`tools` are the server's REMOTE tool names when the asker knows them) and answers `{ ok: true }` or EVERY contention as a row:
|
|
386
|
+
`server_name` (two server names minting one `mcp__<server>__` prefix — a name declared twice included) or `tool_name` (two remote tool names of one
|
|
387
|
+
server minting one wire name — one name listed twice included), each row carrying the two sources in declaration order and the name they
|
|
388
|
+
share. `describeMcpNameConflict(row)` is the sentence. The mount and the no-run probe refuse a set with a `server_name` row through one function
|
|
389
|
+
(`config.mcp_server_name_collision`, the first row's sentence, before any dial); the listing's intake asks the same claim ledger
|
|
390
|
+
(`createToolNameClaims`) and words each drop with the same sentence function (`mcpToolDropReason`). The prefix arithmetic answers both "is there a
|
|
391
|
+
collision" and "which names, all of them" (`findNamespacePrefixCollisions`; the single-answer form is its first row).
|
|
392
|
+
- **Behaviour.** No change. Every set the mount refused is refused with the same code and the same sentence; the probe answers a colliding set as the
|
|
393
|
+
mount does; every tool the intake dropped is dropped with the same sentence, the first spelling keeping the name. A battery of nine declared sets
|
|
394
|
+
(fold, a name thrice, control characters in names, a 200-character name twice, accented names, three names onto one prefix, two empty names,
|
|
395
|
+
a separator inside a name, a distinct pair) through the mount and the probe, and four listings through the intake, reads byte-identical on the
|
|
396
|
+
baseline and on this build.
|
|
397
|
+
- **Exports.** +5, none removed: `judgeMcpNames`, `describeMcpNameConflict`, `McpNameConflict`, `McpNameSetServer`, `McpNameVerdict`.
|
|
398
|
+
- **Consumers.** @server: ask `judgeMcpNames(servers)` where a declaration set is saved (`McpServerSpec[]` is accepted as it is; pass `tools` with the remote
|
|
399
|
+
names when you hold a listing) and show `describeMcpNameConflict(row)` — the sentence the run would be refused with. The engine refuses only a
|
|
400
|
+
`server_name` row; a `tool_name` row is what the listing discloses as a drop (first spelling kept), so what a host does with one is its own call.
|
|
401
|
+
A set that is not a list of `{ name: string, tools?: string[] }` is refused (`config.invalid_argument`), never read as clean. @client-core: the
|
|
402
|
+
collision answer for a whole declared set that was asked for (U5) is this call; nothing to change unless the editor wants to say it before submit.
|
|
403
|
+
- **What still stands (named, not closed).** The roster's `config.reserved_tool_name` refusal (a caller tool named like a mounted MCP tool or an engine-injected
|
|
404
|
+
resource face) compares names that exist only after a listing against the caller's own `TaskSpec.tools`; it is not a contention inside a declared MCP
|
|
405
|
+
set and stays at its site. An A2A peer set reads the same prefix arithmetic with its own code and sentence. The judge reads no `allowTools` /
|
|
406
|
+
`toolAxes` / `toolFaces` key: those are filters and declarations over names, not a list of the server's tools.
|
|
407
|
+
- **Pins.** `test/backlog2568-mcp-name-judge.test.ts` (P1–P4 the rows, N1–N3 the controls, V1 a malformed set, E1 / E2 the mount and the probe say the judge's
|
|
408
|
+
sentence and code, E2b a passing pair is not refused, E3 the intake drops exactly what the judge's tool rows name, S1 no second copy of the rule in the
|
|
409
|
+
mount / probe / intake source, S2 the root door).
|
|
410
|
+
|
|
411
|
+
### The self-kill guard's way out — `pkill -P $$` — is read by the option walk that builds the question, not by a second reading of the raw words: a line that names the shell's pid once no longer runs unasked when another parent stands beside it, the selection is negated, or a word of the line cannot be read (#2604; security fix and behavior narrowing, open to veto; hard-breaking: none; closed sets: none; exports: none — one internal function and one internal type in `src/tools/fs/pattern-kill-guard.ts`, not index-exported) (@test)
|
|
412
|
+
|
|
413
|
+
- **Before.** The guard (#2348) does not ask `pgrep` about `pkill -P $$` — the children of the shell that runs the command hold no ancestor of the agent — and it decided that from the raw words with a reading of its own: any `-P` / `--parent` followed by the shell's pid word exempted the whole line, and a negation was known only as the full spelling `--inverse` or a bundled `-v`. The reading also ignored two facts about the words it was reading: a word the guard cannot read (a stream appended by `xargs`, an expansion that is not the shell's pid) could be another `-P 1` or a `-v`, and where the platform's options end at the first operand (BSD's / macOS's `getopt`) a `-P $$` after the pattern is a pattern, not an option. Lines of all four kinds therefore ran unasked and could reach the agent. `pkill -f WORD -P $$ -P 1`: BSD's `pkill` unions repeated `-P` lists (the shell's children plus the children of pid 1), procps' keeps the last one (the children of pid 1), so with the init process as the agent's parent the line selects it. `pkill --inver -P $$ x`: procps reads any unambiguous prefix of a long option (`--inver` is `--inverse`), and a negated selection is every process the other selectors do not name — the agent's ancestors included.
|
|
414
|
+
- **Now.** The one walk that already reads every option of `pkill` / `pgrep` for the question (the table of procps' long options with prefix resolution, bundles, attached and next-word values) also records every `-P` list in the order given and whether the selection is negated, and the way out is a predicate over that record: every parent the platform's reading of the `-P` options leaves standing (procps: the last list; BSD: all of them, as a union) is the shell's own pid word (`$$`, `${$}`: the special parameters, which a command can neither assign nor unset — `$BASHPID` is not one, below), nothing is negated, and every word of the line is known — a stream of arguments, or an expansion other than the shell's own pid (it expands to digits and can be no option), lifts the way out. Where the platform's options end at the first operand (the one platform constant `killall` already read, now shared), the words after the pattern are patterns, passed on as written and read as no option. The independent exemption reader and the independent negation reader are gone (two raw-word readings → none). A line the predicate does not accept is asked as written, or — while the shell's pid word is still in it — refused as decided when the command runs, as any word the shell expands is.
|
|
415
|
+
- **Behavior narrowing (open to veto).** *What changed*: `pkill -f WORD -P $$ -P 1` and `pkill -P $$ -P 1 x` (another `-P` list beside the shell's: any on BSD, one after it on procps — `pkill -P 1 -P $$ x`, whose last list is the shell's, still runs on procps) and `pkill --inver -P $$ x`, `--inve` / `--inv` / `--in`, `-vP $$ x`, `-fv -P $$ x` (a negation in a spelling the old reading missed), `pkill -P $$ "$X"`, `pkill -P $$ -f "$X"`, `pkill -P $$ x $(…)` and `echo … | xargs pkill -P $$ …` (a word the guard cannot read beside `-P $$`: any expansion but the shell's own pid — an unquoted glob, `~` or `{a,b}` word too — and any stream) and, on macOS, `pkill -af . -P $$` and `pkill -f x -P $$ -u root` (the `-P $$` stands after the pattern, so it is a pattern) were run and are refused now. **`$BASHPID` is no longer the shell's pid word**: `pkill -P $BASHPID x`, `-P ${BASHPID}` and `--parent=${BASHPID}` (run unasked in 7.56.0; `-xP$BASHPID`, refused there as computed, stays refused instead of joining the widened bundle spelling below) are refused as decided when the command runs, and so is a `$BASHPID` word beside a `-P $$` (`BASHPID=-v; pkill -P $$ $BASHPID x` negated the selection; run unasked in 7.56.0). It is an ordinary variable in the shells commands run under: `/bin/bash` 3.2 (macOS's, and the shell the Node environment prefers on POSIX) has no such special variable, so `BASHPID=<the parent's pid>; pkill -P $BASHPID -f WORD` selects the children of the parent — the outer shell among them — and bash 5 ends its special meaning after `unset BASHPID` (measured on 3.2.57 and 5.3.15: the assignment sticks on 3.2 and after `unset` on 5); only `$$` and `${$}`, which cannot be assigned or unset, are the shell's pid. *Who is hurt*: a model that wrote a second `-P` beside `-P $$`, a negation in a spelling the old reader missed, a variable or a pipe-fed word in a `pkill -P $$` line, `-P $$` after the pattern on macOS, or `-P $BASHPID` — the refusal says so and names the way out. *How to migrate*: drop the extra `-P` or the negation; write the pattern as a literal (`pkill -P $$ -f mypattern`), put `-P $$` before the pattern, and write `$$` (or `${$}`) where `$BASHPID` stood.
|
|
416
|
+
- **Reading widened to what the killer reads (open to veto).** `pkill --par $$ x` (an unambiguous prefix of `--parent`), `pkill -P $$,$$ x` (a list of shell words) and `pkill -xP $$ x` (the `-P` inside a bundle) were refused as computed; each is the shell's children and now runs unasked. A `-v` that is no switch is no longer read as a negation: the value of another option (`pkill -F -v -P $$ x`, `pkill -P $$ -u -v x`), a pattern after `--` (`pkill -P $$ -- -v`), and on macOS a word after the pattern (`pkill -P $$ x -v`, `pkill -P $$ x -P 1`: patterns, so the shell's children still bound the selection).
|
|
417
|
+
- **Limits (stated).** The procps arm is read off its source (the option loop assigns a repeated `-P` list; `split_list` splits at commas) and runs only on Linux; the macOS arm was measured with the real `pgrep` (a union of repeated `-P` lists; a word after the first operand is a pattern: `pgrep zsh -l` prints pids only); the cells load the module as each platform does, so both arms are pinned on every host, and what procps then answers is the source's, not a measurement here. A shell's pid written as a literal number cannot be known to the guard and is asked like any other pid.
|
|
418
|
+
- **Consumers.** @test: the `selfkill` fence's guard cells (`pkill -P $$` as the way out) keep their reading; two lines are worth adding — `pkill -f <word> -P $$ -P 1` and `pkill --inver -P $$ <word>` are refused on procps and macOS alike (the agent's parent held by pid 1 is the field case).
|
|
419
|
+
- **Pins.** `test/backlog2604-parent-list-reading.test.ts`: E (every spelling of the way out reads as nothing to ask, the widened spellings and the non-negations among them), A (another parent beside the shell's: refused), N (every spelling of a negation: refused), S (a stream or an expansion other than the shell's pid lifts the way out, and the refusal names that word), O (where the options end — the same line under the two platforms' grammars), Q (another pid asked as written), G (a repeated `-P` under the two platforms' grammars), and for `$BASHPID`: B (every spelling of it bounds nothing: refused as computed under both platforms' grammars, assigned or not), U (it beside a `-P $$` is a word the guard cannot read), K (`$$` / `${$}` still bound the selection); re-pinned: the `-P $BASHPID` cells of E and of the #2348 R2 moved to `${$}` and R3 now holds the refused `$BASHPID` lines (R7 reads the same).
|
|
420
|
+
|
|
421
|
+
### A held message's decision is the record's own field in its box — one compare-and-set, read by every consumer, so a letter a person refused in one terminal is refused in all of them; `principalInboxConsumer: "terminals"` is accepted (#972b, design/972b slice S1, C-R864; hard-breaking; open to veto; closed sets: `MailboxDecision` new; exports: +13) (@server @cli @client-core @test)
|
|
422
|
+
|
|
423
|
+
- **What changed.** `MailboxMessage.decision` (seven words: `held` / `approved` / `delivered` / `denied` / `expired` / `refused`
|
|
424
|
+
/ `dropped`) is the decision about a parked record a recipient's setting held. It is never appended; it moves only through the
|
|
425
|
+
new required `MailboxStore.moveDecision(scope, handle, seq, expect, next)` — a compare-and-set, atomic against every other move
|
|
426
|
+
and against `ack`, not fenced by the lease (a person answers from whichever terminal shows the card), refusing a malformed call
|
|
427
|
+
with `mailbox.invalid_decision_move` before any side effect. The decision law: absent → `held`; `held` → `approved` /
|
|
428
|
+
`delivered` / `denied` / `expired` / `refused`; `approved` → `delivered` or a final word; `delivered` → `dropped` only; a final
|
|
429
|
+
word never moves. Every lease snapshot carries the decision, so the drain reads it from the box: a final record is disposed of
|
|
430
|
+
(and never re-supplied) and the ack prefix may pass it; a record held or approved elsewhere is shown as a card here and the card
|
|
431
|
+
— the queue is the one writer of every move out of `held` / `approved` — refuses, releases or hands it over with the receipt
|
|
432
|
+
that move owes; a `delivered` record not yet acked is handed over again (at-least-once). Every hand-over of a held record is
|
|
433
|
+
itself the move to `delivered`, taken by a pass that holds that box's lease and can inject now; an answer that lands later
|
|
434
|
+
learns it came too late. `moveMailboxDecision` is the one caller-side reading of a move that faulted after committing.
|
|
435
|
+
- **The held queue is a presentation of the decision.** `PeerHeldQueue.hold` / `rejudge` / `settle` / `shutdown` return Promises
|
|
436
|
+
(the answer is the store's); `settle` answers `decided_elsewhere` when another consumer's move won first; `PeerHeldEntry.released`
|
|
437
|
+
is replaced by `decision` (`held` / `approved` / `delivered`); `PeerHeldSettlement` gains `retired` (the card left this process,
|
|
438
|
+
no decision was taken) and `PeerHeldSettleReason` gains `decided_elsewhere`; the sink gains `decide` (the store's move),
|
|
439
|
+
`tellsSender`, `canInject` and `onFault`; `reconcile` takes the snapshot's seq → decision map. `decisions()`,
|
|
440
|
+
`forgetDecisions()` and `PEER_HELD_DECISION_MEMORY` are retired: no decision is remembered beside the box any more. A
|
|
441
|
+
queue-local event — eviction at the cap, process shutdown, a hand-over the lane refused — is written to the record only from the
|
|
442
|
+
state it ends and only when its receipt reaches the sender (the session lane); an approved card is never overridden by it; on
|
|
443
|
+
the principal inbox it only takes the card out of this process and the letter stays held for the other terminals. At the cap the
|
|
444
|
+
oldest held card that tells its sender is evicted; a queue with none keeps the new record held in the box but uncarded
|
|
445
|
+
(`peer.inbound_disposition` says so).
|
|
446
|
+
- **`"terminals"` opens.** `principalInboxConsumerOf` accepts `"terminals"`; the `terminals_unsupported` refusal word is retired
|
|
447
|
+
(KL-972-a closed). A gate minting with no declared consumer is told to declare `"terminals"` or `{ assistant }`.
|
|
448
|
+
- **Behavior on the session lane.** A held message whose sender was told `expired` at a graceful shutdown (or at the cap) is no
|
|
449
|
+
longer re-judged and delivered by the session's next process; a person's deny, a deadline expiry and a policy refusal survive a
|
|
450
|
+
process restart; an approved, not yet delivered message survives a shutdown and is delivered by the next run (it used to expire
|
|
451
|
+
with the queue). A held message re-judged by a new process now sends its sender the `delivered` / `refused` receipt it is owed
|
|
452
|
+
(it heard only `held` before).
|
|
453
|
+
- **A refusal ends a card that was handed over but not yet injected.** A card whose hand-over move was taken while the
|
|
454
|
+
`ReadInbox` page budget deferred its injection answered `held` for ever once the policy then refused it, and the ack prefix
|
|
455
|
+
stopped at it (a later refused record never left the box). A refusing policy now ends such a card, at the re-judgment a
|
|
456
|
+
lease-holding pass runs, by the decision law's one exit out of `delivered` — the record moves to `dropped` and the sender,
|
|
457
|
+
where a reply address exists, gets the `dropped` receipt. A process that does not hold the lease (an approval answer between
|
|
458
|
+
passes, a deadline timer) writes nothing for such a card: it may only have adopted a `delivered` that another consumer already
|
|
459
|
+
injected. A held or approved card still ends as `refused` wherever the verdict is taken. A person's deny / cancel of a
|
|
460
|
+
handed-over card keeps its answer (`delivered`, too late): the card is injected on the next pass, so it is not stranded.
|
|
461
|
+
- **Hard-breaking, three sentences each.**
|
|
462
|
+
- *`MailboxStore.moveDecision` is required.* Every out-of-repo `MailboxStore` (the server's SQL twin) stops compiling against
|
|
463
|
+
7.57.0 until it implements the method and persists the decision word with the record (`ack` / `reap` / `drop` remove it with
|
|
464
|
+
the record; `claimLease` returns it). Migrate: add a nullable decision column, implement the compare-and-set under the box's own
|
|
465
|
+
atomicity, refuse malformed calls with `assertMailboxDecisionMove` first, run `mailboxStoreContract` (two new cases pin the law
|
|
466
|
+
and the outcomes). @server `src/plugins/mailbox-store-sql.ts`.
|
|
467
|
+
- *The held queue face is asynchronous.* A host approval face that calls `settle` / `hold` / `rejudge` / `shutdown` and reads the
|
|
468
|
+
answer synchronously gets a Promise (no consumer found in the server / cli / client-core trees). Migrate: `await` each call,
|
|
469
|
+
read `entry.decision` instead of `entry.released`, and give a hand-built sink `decide` + `tellsSender`.
|
|
470
|
+
- *Mixed engine versions on one file box.* An engine before 7.57.0 skips `decision` events and can deliver what a newer one holds
|
|
471
|
+
or denied; its compaction erases them. Migrate: upgrade every terminal / replica that drains a box before declaring
|
|
472
|
+
`"terminals"` (deployment order @server @dgx).
|
|
473
|
+
- **Known limits written down.** KL-972-d (a long-held head blocks the ack prefix: committed records after it are delivered again
|
|
474
|
+
by the next consumer, and its deadline can keep restarting), KL-972-e (a crash between the move and its receipt leaves the
|
|
475
|
+
sender untold; a move retried after a fault can be read as won by two writers of the same word), KL-972-f (mixed versions; an
|
|
476
|
+
unknown word reads as `held` and can be overwritten), KL-972-g (`drop` restarts seq), KL-972-h (directly delivered records write
|
|
477
|
+
nothing).
|
|
478
|
+
|
|
479
|
+
### A rule with no content is the whole-tool rule (hard-breaking, open to veto) — every surface reads a bare tool name the way the reference does: a stored `SendUserFile` allow / deny / ask reaches every `SendUserFile` call, and the card of a tool with no rule grammar offers it (#1949; closed sets: `PERSISTED_RULE_MATCHES` +1; grammar stamp 5 → 6; exports: +1 `adjudicatePersistedToolRules`) (@server @cli @dgx)
|
|
480
|
+
|
|
481
|
+
- **What changed.**
|
|
482
|
+
- The one validator (`parseRuleText`) reads a rule with no content — `Tool`, `Tool()`, `Tool(*)` — as the whole-tool rule
|
|
483
|
+
(`match: "tool"`, `command: ""`, its canonical text the bare name) on every tool and under every behavior, the shell's
|
|
484
|
+
included; it refused every bare name (`invalid.grammar`) before. A name for a set of tools (a protocol peer `mcp__srv`, a
|
|
485
|
+
glob) and a name the name judgment reads as naming no tool (a retired spelling such as `read_file`) stay refused, in the
|
|
486
|
+
judgment's own words. `PERSISTED_RULE_MATCHES` gains `"tool"`.
|
|
487
|
+
- The person's rule lane reads a whole-tool row for every call that answers to its name (the call's spelling, its wire name or
|
|
488
|
+
an alias it declares): the same precedence walk (deny > ask > allow) beside the command, path, name and host walks
|
|
489
|
+
(`adjudicatePersistedToolRules`, exported beside them). A list's whole-tool entry and a stored whole-tool row answer through
|
|
490
|
+
one predicate. A kept tightening row on a tool with no rule grammar whose text no grammar reads now reaches that tool's calls
|
|
491
|
+
in the fail-closed form; it reached nothing before.
|
|
492
|
+
- The approval card. The card of a call no rule word speaks for under any name it answers to (a host's own tool, an MCP tool)
|
|
493
|
+
offers the whole-tool rule — `{ kind: "single", rule: "<Tool>", match: "tool", command: "" }`, the reference's "don't ask
|
|
494
|
+
again for <Tool>". A card the rule lane is armed for is no longer silent: a path tool's, a shell-face sibling's and a tool's
|
|
495
|
+
that answers to one of those under an alias it declares (a caller's shell wearing `Bash` — a one-click yes to its whole tool
|
|
496
|
+
would be a licence for every program) names `ruleOffersAbsence: "lane_cannot_speak"`, and a closed door's word (`mandated`,
|
|
497
|
+
`allow_outranked`, `ask_rule_matched`) appears on those cards too. A card stays silent only where no rule speaks for the call:
|
|
498
|
+
no lane wired; an anonymous task in a deployment that declares no local owner; a content-routing tool the engine mounts
|
|
499
|
+
(question, answer, blocked report — the lane does not judge them). One function (`cardRuleFormOf`) decides the card for the
|
|
500
|
+
offer factory and for the consent door; the factory reads it with the call's names (a caller's tool wearing a shell-face alias such as
|
|
501
|
+
`Bash` is offered nothing), the door holds no face and reads the wire name alone (KL-1949-a).
|
|
502
|
+
- `prepareCardApproval` mints the whole-tool card for a tool with no rule grammar when `command` is `""` (the offer's own
|
|
503
|
+
`command`: the whole tool binds no value); any other value mints nothing. A card record whose candidate is a whole-tool rule
|
|
504
|
+
is read as damage unless it is that card (no command, a tool whose card is the whole-tool one), and a card's edit to a
|
|
505
|
+
whole-tool rule is refused in so many words.
|
|
506
|
+
- `prepareCcImport` reads every settings entry through the one validator (its own spelling screen is retired): a refused entry is
|
|
507
|
+
skipped with the validator's code and sentence; a bare tool name — now the whole-tool rule — stays in the settings layer that
|
|
508
|
+
carries it, skipped as a name-set item as before.
|
|
509
|
+
- `addPersistedRule` / `removePersistedRule` write and clear a bare deny or ask. In `auto` a stored whole-tool allow is stripped
|
|
510
|
+
before matching exactly when the list entry spelling it is (the shell's, and a name the call answers to the shell under).
|
|
511
|
+
- The organization layer still reads shell command rules only: a whole-tool row (`Bash` alone) stands at its strictest reading,
|
|
512
|
+
as every row it does not read; its code is now `unsupported.tool` (`invalid.grammar` before).
|
|
513
|
+
- The rule grammar stamp (`SESSION_RULES_GRAMMAR`, on every persisted bucket, sync state and session rules row) is 6.
|
|
514
|
+
- **Hard-breaking.** What changed: `PersistedRuleMatch` (and `PERSISTED_RULE_MATCHES`) gains `"tool"`; `parseRuleText` answers a
|
|
515
|
+
rule for a bare name; cards that were silent carry an offer or `ruleOffersAbsence`; the grammar stamp is 6, so an engine of
|
|
516
|
+
grammar 5 refuses a bucket, a sync state or a session rules row this engine writes, whole. Who is hurt: a consumer with an
|
|
517
|
+
exhaustive switch over `PersistedRuleMatch`; a host UI that reads "both seats absent" as "no standing-rule option"; a store
|
|
518
|
+
backend that keeps its own match list instead of reading `PERSISTED_RULE_MATCHES`; a rolling deployment where an older engine
|
|
519
|
+
reads what a newer one wrote. Migration: add the `"tool"` arm (render `rule` as written — the whole tool); render
|
|
520
|
+
`ruleOffersAbsence` on every card that carries it; prepare the whole-tool card with `command: ""`; upgrade the readers of a
|
|
521
|
+
rule store before its writers. The mixed-version symptom: an engine of grammar 5 meeting a grammar-6 bucket refuses it whole
|
|
522
|
+
(`PERMISSION_RULES_LATER_GRAMMAR_CODE`) — its run reads that person's stored rules as unreadable and fails closed (it asks or
|
|
523
|
+
refuses more, never less) until it is upgraded. Ruling C-R866 (open to veto).
|
|
524
|
+
- **Behavior change (open to veto).** A bare-name rule — refused by every store entrance before — is now written, read and cleared
|
|
525
|
+
as the reference reads it. Ruling C-R504 (a).
|
|
526
|
+
- @server: adopt `"tool"` in your transcription of the match words (or read `PERSISTED_RULE_MATCHES`); prepare the whole-tool card
|
|
527
|
+
with `command: ""`; S-1674 can consume the whole-tool offer; deploy order for the grammar stamp. @cli: L-1238 — render the
|
|
528
|
+
whole-tool option. @dgx: deploy order for the grammar stamp.
|
|
529
|
+
|
|
530
|
+
no-notes: #2575(the multi-package release-base pin now reads a fixture repository whose history it writes instead of the checkout's own tags and packages; a pin that was red on the v7.56.0 tag tree and on a depth-1 clone is green on both; test only; no script or export change), #1030(test fixture only: the 1751 pin's fixture body now reads the RC manifest through the one git seam; the seam itself shipped in 7.54.0), #2700(ticket named by the 2604-fix1 commit, not fixed here: Monitor skipping the kill guard is a 7.58.0 car)
|
|
531
|
+
|
|
532
|
+
### A parked shell call binds where its reads landed — the resumed call whose read lands elsewhere is vetoed by the approval object's one judge, as a moved path target is (#1185, design/1185; hard-breaking, open to veto — a checkpoint store format forward change; behavior narrowing, open to veto; closed sets: requirement words +1 `approval_target_reads`; exports: +2 types `ApprovalTargetReads`, `ReadLanding`; `ApprovalObject` +`targetReads?` and `LiveApprovalObject` +`reads?`, both optional) (@server @dgx)
|
|
533
|
+
|
|
534
|
+
- **Before.** A tool with a path slot (Read, Write, a caller tool declaring one) was bound since 7.54.0: the row recorded where its target resolved when the card was asked and the execution station vetoed a move (`resume.target_moved`). A shell command was not: its row recorded nothing of where the shell read station found its reads, and the resumed leg's read belt judged the bytes on its own station, where an ordinary question on the unedited bytes is the card the person already answered. So `cat <link outside the root>` parked on its card, the link re-pointed, then approved, read the new target — the person saw one file named and the model received another; the same for an in-root link re-pointed outside while the call waited on another read's card.
|
|
535
|
+
- **Now.** The approval object's TARGETS have a second reading: per read candidate the shell read station judged for the parked bytes, where it landed (its key and its settled targets, each as a sha256 digest — a pattern lands on a set). The park mints it from the station's own judgment of the call (the one its pre-spawn admission re-establishes), beside the path slot's roles, through the one mint (`approvalTargetsOf`); the execution station reads the resumed leg's station's judgment of the bytes that will run — the belt's, a few lines earlier — through the same mint, and the one judge (`judgeApprovalObject`) compares. The member is written whenever the station judged the parked bytes — `{}` when it found no read candidate, so a read that appears by the resume is one the card did not cover. A command read differently on the resuming leg (a read the card covered is not read, or a read it did not cover is — the same read in another segment of the command is one the card did not cover, a read's key carries its segment), a read that lands elsewhere (a re-pointed link, a pattern that matches one more entry or one fewer, a read the station could not place then and places now), no judgment on the resuming leg, or a damaged member ⇒ the call is vetoed: not run, the approval spent, `resume.target_moved` on its `tool_end`, the sentence saying which (a moved read is named with where it lands now); the model re-issues it and the new card is bound to the current landing. Not judged, as before, when a person edited the call to other bytes (the belt's `rejudge` seat judges an edit); judged when the decided bytes are the parked ones handed back, and when a PreToolUse hook rewrote them (their reads are not the card's). The in-leg pre-spawn check and the row read one keying and one landing (`readLandingsOf`, `shell-read-identity.ts`).
|
|
536
|
+
- **Hard-breaking (open to veto) — store format forward change.** *What changed*: a parked shell call's `pendingAction` carries a new member `targetReads` (a record of 64-hex → 64-hex) and the reader-requirement word `approval_target_reads`. *Who is hurt*: a worker of v7.56.0 or older that meets such a row refuses it `version_newer` (the row stays pending — it is not replayed unbound); a store with a member allow-list for `pendingAction`, or a column per member. *How to migrate*: upgrade every worker that resumes parked approvals before any worker that parks them (a mixed fleet otherwise leaves shell rows pending until an upgraded worker takes them); a JSON-document store needs no migration; add `targetReads` wherever `pendingAction` members are enumerated. Rows parked before this version carry no `targetReads` and resume as before.
|
|
537
|
+
- **Behavior narrowing (open to veto).** A parked shell call whose reads moved while it waited is vetoed and asked again (once per move), where it ran. A PreToolUse hook on the resumed leg that rewrites a parked shell command so that a read the card did not cover is read — another read, or the same read in another segment (a prefix segment added) — is vetoed (its in-root rewrite ran before); a hook that is not idempotent (it adds the prefix again on the resumed leg) is vetoed at every resume (KL-1185-e, #2702). A pattern read over a directory that gains or loses an entry while the call waits is a move.
|
|
538
|
+
- **Not bound (stated; KL-1391-a, KL-1185-a … -f).** The binding stops drift between the card and the resumed run; a command the agent itself arranges to move later is the read-then-run window, which is not closed (between the pre-spawn check and the child's own open). A landing is a LOCATION: a file replaced at the same location, a hard link or a bind mount swapped in at the same path is not a move. Every read candidate is bound, the ones the station vouched for too (the in-leg check's reading): a park over a pattern in the workspace is vetoed once when the workspace gains a matching file before the approval. An engine version that plans the same command differently vetoes in-flight parked shell approvals once, worded as a re-plan. An isolated sandbox's establishment is lexical, so moves there are not seen. A station that holds more than 1024 judged-not-spawned calls on one leg forgets the oldest: a call parked past that bound carries no read member and is not judged on it. A store whose encoding changes the parked arguments (the filed bytes are not the judged ones) gets neither target member, as the path roles did before. On Linux, a read through `/proc/self/…` lands on the resolving process's own entry, so a resume in another process reads it as a move and vetoes (each time it resumes in a new process).
|
|
539
|
+
- **Consumers.** @server: add `targetReads` to the `pendingAction` keyset your trace guard enumerates; the SQL store holds the row as a JSON document (no migration); deployment order as above. @dgx: the same deployment-order sentence. @cli / @client-core: nothing new to render — the veto is today's `resume.target_moved` on the vetoed call's `tool_end`.
|
|
540
|
+
- **Pins.** `test/backlog1391-approval-object.test.ts`, block `#1185` — real Runner, zero path rules: SH-r (a link outside the root re-pointed; the re-issued call parks bound to the new landing), SH-i (an in-root link re-pointed outside while the call waited on another read's card), SH-hb (the parked command handed back), SH-g (a pattern gains an entry), SH-h (a hook rewrites to an in-root read), SH-e (a person's edit runs), SH-d (damage), SH-8 (control, a link and a pattern); SH-0 and units: the judge's read arms (an empty record, a re-plan, both readings on one row), the mint, the requirement word. SH-r / SH-i / SH-hb / SH-g / SH-h / SH-d red on v7.56.0's tree. The park self-check row carries both target members, so a store that drops either fails at wiring.
|
|
541
|
+
|
|
1
542
|
## 7.56.0 — 2026-10-23
|
|
2
543
|
|
|
3
544
|
### One writer per fact on a permission decision — a policy's or a hook's decision that states a word the gate writes is refused by name, and the integrity guards and a fold's collapsed classes ride the engine's attestation record alone (#2134 / #1617 / #1329; hard-breaking, open to veto; security narrowing; closed sets: `RETIRED_DECISION_MEMBERS` +2 (`guards`, `foldedAskClasses`); exports: `PermissionResult`'s ask arm loses `guards` and `foldedAskClasses`, `EngineAttestation` gains an optional `guards`, `AskClassFacts` (the facts `classifyAsk` takes) gains a required `call`) (@server @test @cli @client-core)
|