@sema-agent/core 7.50.1 → 7.50.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,3 +1,15 @@
1
+ ## 7.50.2 — 2026-10-14
2
+
3
+ **Hard-breaking changes:** none. **Closed sets:** none. **Exports:** +0 / −0 (`releasePolicyDefects` is module-level, not on the package root).
4
+
5
+ ### The gate-outcome screen reads the shape of a mode release, not today's release policy (#1730; fix; truthfulness of persisted records; hard-breaking: none) (@server @test)
6
+
7
+ - **Before (v7.50.0 / v7.50.1).** `screenGateOutcome` I7 asked whether `released.askClass` is releasable under TODAY's table (`releasableAskClassesFor`). Release policy moves between versions (7.50.0 made `rule_unreadable` bypass-immune), so a record a 7.49.5 engine minted lawfully — a `rule_unreadable` ask released under `bypassPermissions` — screened as `I7: released.askClass "rule_unreadable" is not a releasable class` on 7.50.x, and a reader of persisted records (server's event-ledger replay, S-1546) recorded lawful history as a defective producer.
8
+ - **Now.** I7 is a shape invariant: the class word is a member of the closed set (`isAskClass`), the release carries no settlement, the mode is `bypassPermissions`, `classes` is a non-empty list of ask classes, `origin` / `reason` are members of their sets. Whether a class MAY be released is the producer's law at mint time: `releasePolicyDefects` (module-level) is read beside the shape screen at the gate's exit (`gate-exit.ts`) and at the station screen (`prepare-gate-stations.ts`), so this engine still refuses to mint a release of an immune class today, while a reader of persisted records asks only the shape.
9
+ - **Who is hurt.** A consumer that used `screenGateOutcome` as a producer check on records it mints itself (none known; the engine's two producer sites now read both halves) no longer gets the policy defect from the screen alone. Nobody else: a record this engine minted screens exactly as before; a lawful historical release stops reading as defective.
10
+ - **Compensation.** None needed for readers of persisted records. A host that mints gate records and wants today's policy judged calls both functions, or waits for 7.51.0's `screenGateOutcome(g, { reading: "producer" | "archive" })` with the engine's retired-vocabulary table (so 7.50.x records naming `governance_unavailable` read clean after its 7.51.0 retirement).
11
+ - **Pins.** `test/backlog1730-gate-screen-shape-not-policy.test.ts` P1–P5 (P1 red on v7.50.1); `test/backlog1423-wpu-by-face.test.ts` re-pinned: the policy defect is the producer's, the shape screen accepts the record.
12
+
1
13
  ## 7.50.1 — 2026-10-14
2
14
 
3
15
  **Hard-breaking changes:** none. **Closed sets:** none — the read station's details code stays `read_unestablished` on both faces (whether a write finding gets its own code is a closed-set question, left to 7.51.0). **Package exports:** +0 / −0 names (`npm run handoff:diff v7.50.0 HEAD`); no type, signature or closed-set change (`wire-changes` preview: 0 / 0 / 0). **Behavior:** a tool policy that throws is denied in its own words with one redacted line and reported once on `onError`, never as a timeout; a write the read station could not place is carded as a write. Both defects were present in 7.49.5 and 7.50.0.
@@ -401,9 +401,11 @@ export interface ModeRefusal {
401
401
  * {@link import("./auto-mode.js").AUTO_MODE_FALLBACK_CAUSES} — a record whose `to` and `modelUsed`
402
402
  * disagree would let a reader answer "which model decided this call?" two ways;
403
403
  * I7 `disposition.released` present ⇒ `kind === "allowed"`, no settlement, `mode` is `bypassPermissions`,
404
- * `askClass` is a releasable class, every member of `classes` is an ask class, `origin` / `reason`, when
404
+ * `askClass` is an ask class (a member of the closed set — SHAPE; whether the class was releasable is the mint's
405
+ * law at mint time, read by the gate's mode-release arm, never re-judged here against today's table: a persisted
406
+ * record outlives the policy of its day), every member of `classes` is an ask class, `origin` / `reason`, when
405
407
  * present, are members of their closed sets, and `reason` names a member of `classes` (the mint takes it from
406
- * that set) — a mode releases nothing a person answered and nothing the table calls immune;
408
+ * that set) — a mode releases nothing a person answered;
407
409
  * I8 `deniedBy === "mode"` ⇔ `mode` present (`bypassPermissions` | `dontAsk`) with `askClass` and `classes`.
408
410
  * Nothing here is read out of an ABSENCE: an ordinary allow with no ask is `{ disposition: { kind: "allowed" } }`,
409
411
  * a direct policy deny is `{ disposition: { kind: "denied", deniedBy: "policy" } }`, and neither carries a
@@ -422,4 +424,12 @@ export interface GateOutcome {
422
424
  * clean record); the caller decides the fail-loud form (report + withhold, or refuse), never silently
423
425
  * repairs the record.
424
426
  */
427
+ /**
428
+ * The PRODUCER's half of I7, read beside {@link screenGateOutcome} wherever this engine composes or screens a record it is
429
+ * minting today: a mode release names a class this engine's table lets a mode release (`releasableAskClassesFor` on some
430
+ * face). Policy, not shape — it moves between versions (7.50.0 made `rule_unreadable` immune), so a reader of PERSISTED
431
+ * records (a replay, an archive) does not read it: a release lawful when minted is not a defect under a later table.
432
+ * Module-level only; not part of the package root.
433
+ */
434
+ export declare function releasePolicyDefects(g: GateOutcome): string[];
425
435
  export declare function screenGateOutcome(g: GateOutcome): string[];
@@ -108,6 +108,15 @@ export const DENIED_BY_MAY_CARRY_CAUSE = {
108
108
  read_boundary: false,
109
109
  mode: false,
110
110
  };
111
+ export function releasePolicyDefects(g) {
112
+ const d = g.disposition;
113
+ if (d.kind !== "allowed" || d.released === undefined)
114
+ return [];
115
+ const askClass = d.released.askClass;
116
+ if (!isAskClass(askClass))
117
+ return [];
118
+ return ASK_FACES.some((face) => releasableAskClassesFor(face).includes(askClass)) ? [] : [`I7: released.askClass "${askClass}" is not a releasable class under this engine's table`];
119
+ }
111
120
  export function screenGateOutcome(g) {
112
121
  const defects = [];
113
122
  const d = g.disposition;
@@ -144,8 +153,8 @@ export function screenGateOutcome(g) {
144
153
  defects.push("I7: a mode release carries no settlement — nobody answered the ask it released");
145
154
  if (r.mode !== "bypassPermissions")
146
155
  defects.push(`I7: a mode release names mode "${String(r.mode)}" — only bypassPermissions releases`);
147
- if (!isAskClass(r.askClass) || !ASK_FACES.some((face) => releasableAskClassesFor(face).includes(r.askClass)))
148
- defects.push(`I7: released.askClass "${String(r.askClass)}" is not a releasable class`);
156
+ if (!isAskClass(r.askClass))
157
+ defects.push(`I7: released.askClass "${String(r.askClass)}" is outside the closed set`);
149
158
  if (!Array.isArray(r.classes) || r.classes.length === 0 || !r.classes.every((c) => isAskClass(c)))
150
159
  defects.push("I7: released.classes must be a non-empty list of ask classes");
151
160
  if (r.origin !== undefined && !isAskOrigin(r.origin))
@@ -1,4 +1,4 @@
1
- import { screenGateOutcome, SETTLEMENT_IS_REFUSAL } from "../gate-outcome.js";
1
+ import { screenGateOutcome, SETTLEMENT_IS_REFUSAL, releasePolicyDefects } from "../gate-outcome.js";
2
2
  import { askFaceOf, classifyAsk, headlessApprovalRemedy } from "../ask-origin.js";
3
3
  import { wireOf } from "../tool-name-readings.js";
4
4
  import { isProtectedWrite } from "../tool-registry.js";
@@ -23,7 +23,7 @@ export function mintGateOutcome(facts) {
23
23
  : { kind: "allowed", ...(attribution !== undefined ? { classifier: attribution } : {}), ...(facts.released !== undefined ? { released: Object.freeze({ ...facts.released, classes: Object.freeze([...facts.released.classes]) }) } : {}) }),
24
24
  ...(facts.settled !== undefined ? { settlement: settlement, origin: facts.settled.origin } : {}),
25
25
  });
26
- const defects = screenGateOutcome(outcome);
26
+ const defects = [...screenGateOutcome(outcome), ...releasePolicyDefects(outcome)];
27
27
  if (defects.length > 0)
28
28
  throw new Error(`the tool gate composed an outcome that violates its own record: ${defects.join("; ")}`);
29
29
  return outcome;
@@ -2,7 +2,7 @@ import { SEND_MESSAGE_TOOL_NAME } from "../../agents/send-message-tool.js";
2
2
  import { mountedToolRequiresUserInteraction } from "../mode-policy.js";
3
3
  import { complianceCallDenial } from "../compliance.js";
4
4
  import { cloneObserverInput, formatHookFeedback, runToolGate } from "../hooks.js";
5
- import { screenGateOutcome } from "../gate-outcome.js";
5
+ import { screenGateOutcome, releasePolicyDefects } from "../gate-outcome.js";
6
6
  import { directDeny, frozenInputSnapshot } from "./gate-exit.js";
7
7
  import { emitTrace } from "../trace.js";
8
8
  import { deliverEngineNotice } from "../engine-notice.js";
@@ -20,7 +20,7 @@ function screenGateResult(result) {
20
20
  const gate = result.gate;
21
21
  if (gate === undefined)
22
22
  return { defects: ["a tool-gate result carried no outcome record on a terminal exit — every terminal exit mints one; the frame carries none"] };
23
- const defects = screenGateOutcome(gate);
23
+ const defects = [...screenGateOutcome(gate), ...releasePolicyDefects(gate)];
24
24
  const blocked = result.block === true;
25
25
  if ((gate.disposition.kind === "denied") !== blocked) {
26
26
  defects.push(`a tool-gate result's outcome reads "${gate.disposition.kind}" on ${blocked ? "a blocked" : "an executing"} call — the two faces of one verdict disagree; the frame carries no outcome`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sema-agent/core",
3
- "version": "7.50.1",
3
+ "version": "7.50.2",
4
4
  "description": "Stateless, task-oriented AI agent core",
5
5
  "type": "module",
6
6
  "license": "BUSL-1.1",
@@ -0,0 +1,42 @@
1
+ {
2
+ "repo": "core",
3
+ "version": "7.50.2",
4
+ "from": "c9b91932c9c442efbedc38a9fd43629d6da3b64c",
5
+ "to": "6420369270dad7ad426f39d338defa4eeee9594b",
6
+ "added": [],
7
+ "removed": [],
8
+ "changed": [],
9
+ "narrowed": [],
10
+ "closedSetMembers": [],
11
+ "closedSetsUnreadable": [],
12
+ "typeKeys": [],
13
+ "typeKeysUnreadableArms": [],
14
+ "typeKeysFlattenedCount": 229,
15
+ "signatures": [],
16
+ "declarationMoves": [],
17
+ "mintMoves": [],
18
+ "criteriaObserves": [
19
+ {
20
+ "kind": "taskEventField",
21
+ "name": "tool_end.gate",
22
+ "assertions": [
23
+ "G1.b"
24
+ ]
25
+ },
26
+ {
27
+ "kind": "typeMember",
28
+ "name": "GateOutcome.disposition",
29
+ "assertions": [
30
+ "G1.a"
31
+ ]
32
+ }
33
+ ],
34
+ "consumersHint": {
35
+ "cli": [],
36
+ "server": [],
37
+ "sdk": [],
38
+ "note": "scanned cli@37f37540 server@9ce3abaf sdk@fe943ec — a name listed is MENTIONED there today, which is evidence of consumption, not proof of it"
39
+ },
40
+ "breakingCount": 0,
41
+ "breakingItems": []
42
+ }