@sema-agent/core 5.33.0 → 5.34.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 CHANGED
@@ -1,5 +1,57 @@
1
1
  # Changelog
2
2
 
3
+ ## 5.34.0 — 2026-08-15
4
+
5
+ No BREAKING changes. Two behavior-surface additions (both additive seats), one packaging addition,
6
+ new test/gate infrastructure; zero resolution-result changes.
7
+
8
+ ### Added
9
+
10
+ - `RunInternals.onStatusEvent` (#253): the run's own top-level `status` TaskEvent stream (brain
11
+ liveness — rate-limit/retry/reconnect/circuit-open) offered to the internals holder beside the
12
+ queue. A composition entry (verify/cascade/`runDeveloperTask`) drains its inner legs' queues
13
+ itself, so an inner leg's retry disclosure previously died inside the gate. The sink receives the
14
+ SAME (frozen) frame object the stream carries, gated on queue acceptance (a late detached emission
15
+ cannot reach the sink after the stream refused it), contained per the host-callback isolation
16
+ primitive: a throwing OR async-rejecting sink never faults the leg, first failure per site is
17
+ disclosed via `console.warn`, later ones are counted.
18
+ - Status frames on the task stream are now frozen (`Object.freeze`, flat object): a mutating host
19
+ callback can no longer rewrite the queued wire event. `PushQueue.push` returns whether the value
20
+ was accepted (compatible signature widening, `void` callers unaffected).
21
+ - The export-surface snapshot (`test/export-surface.snapshot.json`) ships in the npm package —
22
+ downstream consumption gates can anchor the in-package file instead of a vendored copy.
23
+ - `examples/` (source tree, excluded from the build): a runnable end-to-end governed-agent arc
24
+ (`src/examples/governed-agent/`) — org rules, narrowing, durable approval park, cross-worker
25
+ resume with non-widening enforcement — 29 self-checking steps, offline and key-less, with a
26
+ smoke test on the real loading path. Not part of the published artifact.
27
+ - Request-prefix/caching test infrastructure: a prefix-stability baseline (adjacent requests as
28
+ append-extensions, three known breaks pinned as current-state), a live provider cache-hit leg
29
+ (same-session second turn must be served from the prefix cache; measured ≈97.5% on deepseek), and
30
+ a byte-equal request-reconstruction pin across durable resume and session fork.
31
+ - ship-post-lint registry legs: cited F-nnn/A-nnn ledger files must exist and carry the shipping
32
+ version; zero citations require an explicit `FEATS: none` / `AUDITS: none`.
33
+
34
+ ### Fixed
35
+
36
+ - 5.34 pre-release rescan (two confirmed): an `async` sink at any run-internals seat now has its
37
+ rejection contained through the same notifier/site as a synchronous throw
38
+ (`observeThenableRejection`, one source for status + both activity sites — previously a rejected
39
+ async `onActivity` escaped as an unhandled rejection and could terminate the process); and
40
+ ship-post-lint's published-output delta and `--tree-fingerprint` now fold packaged non-dist files
41
+ (`pkg:` prefix), so a change to the shipped snapshot can no longer report as byte-identical output.
42
+
43
+ - #248: the run-internals activity sink (`onActivity`) joins the host-callback isolation primitive —
44
+ a throwing sink previously failed the leg (misclassified `provider.error`) and, inside a cascade,
45
+ escalated a rung whose non-idempotent write had already run (measured: the write ran twice). Now
46
+ contained: swallow + first-failure disclosure + per-site counts.
47
+
48
+ ### Docs
49
+
50
+ - The blackbox gate leaves the release tiers (its leg is owned by the independent black-box
51
+ acceptance institution; the script remains a dev-time self-check).
52
+ - Six live standing-leg env-gated skips join the skip baseline (historical omission; union merge,
53
+ zero removals).
54
+
3
55
  ## 5.33.0 — 2026-08-14
4
56
 
5
57
  ### BREAKING (operational)
@@ -3,7 +3,10 @@ export declare class PushQueue<T> implements AsyncIterable<T> {
3
3
  private buffer;
4
4
  private waiters;
5
5
  private closed;
6
- push(value: T): void;
6
+ /** Returns whether the value was ACCEPTED (`false` after close — the push is a silent no-op then).
7
+ * Callers that mirror a pushed value to a second reader gate on this so both sides stay in parity
8
+ * (#253: a status frame must not reach a side sink the stream itself already refused). */
9
+ push(value: T): boolean;
7
10
  close(): void;
8
11
  [Symbol.asyncIterator](): AsyncIterator<T>;
9
12
  }
@@ -4,7 +4,7 @@ export class PushQueue {
4
4
  closed = false;
5
5
  push(value) {
6
6
  if (this.closed) {
7
- return;
7
+ return false;
8
8
  }
9
9
  const waiter = this.waiters.shift();
10
10
  if (waiter) {
@@ -13,6 +13,7 @@ export class PushQueue {
13
13
  else {
14
14
  this.buffer.push(value);
15
15
  }
16
+ return true;
16
17
  }
17
18
  close() {
18
19
  this.closed = true;
@@ -1285,6 +1285,19 @@ export interface RunInternals {
1285
1285
  * MODEL context (this is purely a render channel). Absent unless the deployment opted in.
1286
1286
  */
1287
1287
  onForwardEvent?: (event: TaskEvent) => void;
1288
+ /**
1289
+ * #253 — the run's OWN top-level `status` TaskEvent stream (brain liveness: rate-limit/retry/
1290
+ * reconnect/circuit-open), offered to the internals holder beside the queue. The queue alone was
1291
+ * enough for a direct `runTask` caller (the TaskStream carries these frames), but a COMPOSITION
1292
+ * entry (verify/cascade) drains its inner legs' queues itself — without this seat, an inner leg's
1293
+ * retry disclosure died inside the gate and the wire showed a silent stall. Fed the SAME frame
1294
+ * object the queue receives, at the same moment; contained by the run's safe notifier (#248 form:
1295
+ * a throwing sink is swallowed, first failure per site disclosed, never faults the leg). Subagent
1296
+ * frames still ride {@link onForwardEvent} — this seat is ONLY the run's own status type.
1297
+ */
1298
+ onStatusEvent?: (event: Extract<TaskEvent, {
1299
+ type: "status";
1300
+ }>) => void;
1288
1301
  /**
1289
1302
  * design/115 P2 core slice — trusted run-local system-injection sink. `Runner.runLocked` wires this to the
1290
1303
  * live TaskStream queue plus the current harness follow-up lane; it is not a public TaskSpec field.
@@ -1,5 +1,5 @@
1
1
  import { persistedReadDenyEntryProblem } from "../../tools/fs/read-deny.js";
2
- import { createSafeNotifier } from "../safe-notify.js";
2
+ import { createSafeNotifier, observeThenableRejection } from "../safe-notify.js";
3
3
  import { AgentHarness, DEFAULT_COMPACTION_SETTINGS, uuidv7 } from "../../internal/harness.js";
4
4
  import { snapshotActorAssertion } from "../../internal/llm.js";
5
5
  import { CheckpointError, BINDING_CHECKPOINT_VERSION, checkpointVersionOf, F012_CHECKPOINT_VERSION, MAX_SUPPORTED_CHECKPOINT_VERSION, REAL_APPROVAL_CHECKPOINT_VERSION, FACE_CHECKPOINT_VERSION, remainingBudgetMicroUsd, readPendingSteerQueue, remainingTokens, validatePendingSteer, winnerFromOutcome, } from "../checkpoint-store.js";
@@ -1154,7 +1154,7 @@ function makeHarnessHandlers(prepared, stats, rs, deps) {
1154
1154
  rs.counters.groundingSignalPostR9 = true;
1155
1155
  }
1156
1156
  const activityArg = primaryActivityArg(event.args);
1157
- internalsNotifier.notify(() => internals?.onActivity?.({ phase: "start", toolCallId: event.toolCallId, toolName: event.toolName, at: startAt, ...(activityArg !== undefined ? { arg: activityArg } : {}) }), "runtask.onActivity.start");
1157
+ internalsNotifier.notify(() => observeThenableRejection(internals?.onActivity?.({ phase: "start", toolCallId: event.toolCallId, toolName: event.toolName, at: startAt, ...(activityArg !== undefined ? { arg: activityArg } : {}) }), internalsNotifier, "runtask.onActivity.start"), "runtask.onActivity.start");
1158
1158
  pushContent({
1159
1159
  type: "tool_start",
1160
1160
  toolCallId: event.toolCallId,
@@ -1203,7 +1203,7 @@ function makeHarnessHandlers(prepared, stats, rs, deps) {
1203
1203
  ok: !event.isError,
1204
1204
  ts: toolNow,
1205
1205
  }));
1206
- internalsNotifier.notify(() => internals?.onActivity?.({ phase: "end", toolCallId: event.toolCallId, toolName: event.toolName, isError: event.isError, at: toolNow }), "runtask.onActivity.end");
1206
+ internalsNotifier.notify(() => observeThenableRejection(internals?.onActivity?.({ phase: "end", toolCallId: event.toolCallId, toolName: event.toolName, isError: event.isError, at: toolNow }), internalsNotifier, "runtask.onActivity.end"), "runtask.onActivity.end");
1207
1207
  if (prepared.lspDiagnostics && !event.isError) {
1208
1208
  const name = event.toolName;
1209
1209
  if (name === "Edit" || name === "Write" || name === "NotebookEdit") {
@@ -2329,8 +2329,11 @@ export class Runner {
2329
2329
  queue.push({ type: "message_committed", entryId, role, ...(toolCallId !== undefined ? { toolCallId } : {}), ...ident() });
2330
2330
  };
2331
2331
  const subagentName = parentToolCallId !== undefined && internals?.agentName !== undefined ? inlineUntrusted(internals.agentName.slice(0, 320), 80) : undefined;
2332
+ const statusSinkNotifier = createSafeNotifier({
2333
+ onError: (f) => console.warn(`[sema-core] ${f.site}: run-internals status sink threw (contained; further failures counted, not re-disclosed): ${f.error.message}`),
2334
+ });
2332
2335
  const statusEmit = (s) => {
2333
- queue.push({
2336
+ const frame = {
2334
2337
  type: "status",
2335
2338
  phase: s.phase,
2336
2339
  ...(s.detail !== undefined ? { detail: s.detail } : {}),
@@ -2340,7 +2343,12 @@ export class Runner {
2340
2343
  ...(s.maxRetries !== undefined ? { maxRetries: s.maxRetries } : {}),
2341
2344
  ...(s.errClass !== undefined ? { errClass: s.errClass } : {}),
2342
2345
  ...ident(),
2343
- });
2346
+ };
2347
+ Object.freeze(frame);
2348
+ const accepted = queue.push(frame);
2349
+ if (accepted && internals?.onStatusEvent !== undefined) {
2350
+ statusSinkNotifier.notify(() => observeThenableRejection(internals.onStatusEvent?.(frame), statusSinkNotifier, "runtask.onStatusEvent"), "runtask.onStatusEvent");
2351
+ }
2344
2352
  };
2345
2353
  const telemetryEmit = (t) => {
2346
2354
  emitTrace(rs.telemetry.tracer, () => t.kind === "failover"
@@ -79,3 +79,12 @@ export interface SafeNotifier {
79
79
  * notifier whose callbacks never throw costs one object and nothing per call beyond the `try`.
80
80
  */
81
81
  export declare function createSafeNotifier(opts?: SafeNotifierOptions): SafeNotifier;
82
+ /**
83
+ * Observe a `=> void` host callback's RETURN value for the async dialect: TypeScript accepts an
84
+ * `async` function at a void seat, and a rejected one would escape {@link SafeNotifier.notify}
85
+ * (which deliberately ignores return values — see the scope note above) as an unhandled rejection.
86
+ * Call this with the callback's return value INSIDE the notify thunk: a thenable's rejection is
87
+ * routed back through the same notifier/site, so async and sync failures share one count and one
88
+ * bounded disclosure, and neither can fault the engine's control flow.
89
+ */
90
+ export declare function observeThenableRejection(r: unknown, notifier: SafeNotifier, site: string): void;
@@ -57,3 +57,12 @@ export function createSafeNotifier(opts) {
57
57
  },
58
58
  };
59
59
  }
60
+ export function observeThenableRejection(r, notifier, site) {
61
+ if (typeof r?.then === "function") {
62
+ r.then(undefined, (err) => {
63
+ notifier.notify(() => {
64
+ throw err instanceof Error ? err : new Error(String(err));
65
+ }, site);
66
+ });
67
+ }
68
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sema-agent/core",
3
- "version": "5.33.0",
3
+ "version": "5.34.0",
4
4
  "description": "Stateless, task-oriented AI agent core",
5
5
  "type": "module",
6
6
  "license": "BUSL-1.1",
@@ -36,6 +36,7 @@
36
36
  },
37
37
  "files": [
38
38
  "dist",
39
+ "test/export-surface.snapshot.json",
39
40
  "README.md",
40
41
  "NOTICE.md",
41
42
  "CHANGELOG.md",