@sema-agent/core 7.6.1 → 7.6.3

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,23 @@
1
1
  # Changelog
2
2
 
3
+ ## 7.6.3 — 2026-09-08
4
+
5
+ Maintenance patch on the 7.6.x line: one correction to 7.6.2's forwarding drain.
6
+
7
+ ### Fixed
8
+ - **forwardSubagentEvents drain gate** — 7.6.2 gated the drain inside an `async` helper and awaited it unconditionally, so a run that did NOT opt in still yielded one extra microtask before its terminal push (the black-box round measured parent `done` at hop 356 vs 355 on 7.6.1). The gate is now a synchronous predicate at both terminal pushes and the helper is a plain Promise factory; the non-forwarding terminal depth is pinned (ratchet 58, the unconditional-await form reds it at 59). No change for opted-in runs.
9
+
10
+ ## 7.6.2 — 2026-09-08
11
+
12
+ Backport patch on the 7.6.x line (two field bugs the server 7.64.0 chain found; no store-contract change — that is 7.7.0).
13
+
14
+ ### Fixed
15
+ - **R12 (`forwardSubagentEvents`)** — a run that opted into forwarding did not wait for the frames its BACKGROUND children had already handed to the forward channel before saying `done`; the gap is one microtask ladder deep and moved when #594 split the tool gate into three async stations (a child's gated call went from 24 to 27 microtask hops between `tool_start` and `tool_end`), so a consumer that read the parent to `done` and then inspected the sink lost the child's `tool_end`. The parent now takes one macrotask turn right before its terminal push, only on opted-in runs, on both the success and the failure path. Pin: `test/forward-subagent-events-drain.test.ts`.
16
+ - **R13 (checkpoint)** — the pre-CAS refusal of a pending approval row that carries no `origin` word (a row minted before the ask's origin was persisted on the park) now carries `reason: "origin_missing"` like its three `unsupported_version` siblings, so a host upgrading with parked cards can classify the orphaned row. Pin: `test/backlog-r13-origin-missing-reason.test.ts`.
17
+
18
+ ### Changed
19
+ - `CheckpointError.detail.reason` closed set gains `"origin_missing"` (terminal for the row's bytes; the host decides the orphaned card).
20
+
3
21
  ## 7.6.1 — 2026-09-07
4
22
 
5
23
  Patch: three bug fixes from the 7.6.0 field and black-box rounds (#608 P1, #609, #610) and the first refactor wave (design/393 S0, #594, #598), all byte-invariant on the runtime except where named. One public d.ts narrowing is called out below.
@@ -2093,11 +2093,13 @@ export declare class CheckpointError extends Error {
2093
2093
  * additive and optional like `field` — `code` remains the only REQUIRED discriminant.
2094
2094
  * · `unsupported_version` arms: `"version_newer"` (retryable on a newer worker),
2095
2095
  * `"env_factory_missing"` (retryable on a factory-wired worker), `"governed_unwired"`
2096
- * (retryable on an org-wired worker).
2096
+ * (retryable on an org-wired worker), `"origin_missing"` (a pending approval row minted
2097
+ * before the ask's origin was persisted on the park — terminal for the row's bytes; the host
2098
+ * decides the orphaned card itself, no worker version redeems it).
2097
2099
  * · `invalid_outcome` pre-CAS row-integrity arms: `"real_approval_damaged"`,
2098
2100
  * `"real_approval_forged"`, `"constraint_chain_missing"` (all terminal for the row's
2099
2101
  * current bytes — no worker version redeems a damaged row). */
2100
- reason?: "version_newer" | "env_factory_missing" | "governed_unwired" | "real_approval_damaged" | "real_approval_forged" | "read_face_damaged" | "read_face_forged" | "constraint_chain_missing";
2102
+ reason?: "version_newer" | "env_factory_missing" | "governed_unwired" | "origin_missing" | "real_approval_damaged" | "real_approval_forged" | "read_face_damaged" | "read_face_forged" | "constraint_chain_missing";
2101
2103
  } | undefined;
2102
2104
  constructor(code: "checkpoint.already_exists" | "checkpoint.already_resolved" | "checkpoint.not_found"
2103
2105
  /** `runner.resume` was handed an {@link ResumeOutcome} whose `gate` arm does not match the
@@ -2275,11 +2277,13 @@ export declare class CheckpointError extends Error {
2275
2277
  * additive and optional like `field` — `code` remains the only REQUIRED discriminant.
2276
2278
  * · `unsupported_version` arms: `"version_newer"` (retryable on a newer worker),
2277
2279
  * `"env_factory_missing"` (retryable on a factory-wired worker), `"governed_unwired"`
2278
- * (retryable on an org-wired worker).
2280
+ * (retryable on an org-wired worker), `"origin_missing"` (a pending approval row minted
2281
+ * before the ask's origin was persisted on the park — terminal for the row's bytes; the host
2282
+ * decides the orphaned card itself, no worker version redeems it).
2279
2283
  * · `invalid_outcome` pre-CAS row-integrity arms: `"real_approval_damaged"`,
2280
2284
  * `"real_approval_forged"`, `"constraint_chain_missing"` (all terminal for the row's
2281
2285
  * current bytes — no worker version redeems a damaged row). */
2282
- reason?: "version_newer" | "env_factory_missing" | "governed_unwired" | "real_approval_damaged" | "real_approval_forged" | "read_face_damaged" | "read_face_forged" | "constraint_chain_missing";
2286
+ reason?: "version_newer" | "env_factory_missing" | "governed_unwired" | "origin_missing" | "real_approval_damaged" | "real_approval_forged" | "read_face_damaged" | "read_face_forged" | "constraint_chain_missing";
2283
2287
  } | undefined);
2284
2288
  }
2285
2289
  /**
@@ -86,4 +86,20 @@ export interface PrepareRunRefsResult {
86
86
  worktreeIsolation: SubagentWorktreeIsolation | undefined;
87
87
  }
88
88
  /** The M3a phase body — prepareTask's run-refs stretch, verbatim (see the module header). */
89
+ /**
90
+ * A run that opted into `forwardSubagentEvents` may have BACKGROUND children whose frames are forwarded to the
91
+ * deployment's sink from the child's own run. Those forwards are scheduled on later microtasks than the parent's
92
+ * turn, so "the parent stream ended" and "the child's already-emitted frames reached the sink" are two different
93
+ * moments — the gap is one microtask ladder deep and moves whenever the gate's station structure changes (design/393
94
+ * / #594: the three-layer gate added three async frames to a child's gated call, and a consumer that read the parent
95
+ * to `done` then inspected the sink lost the child's `tool_end`). The rule is not a tick count: **before the parent
96
+ * says `done`, every frame a child had already handed to the forward channel is delivered.** One macrotask turn is
97
+ * the smallest boundary that strictly orders after all pending microtasks; it is taken only on the opted-in run,
98
+ * only once, right before `done`.
99
+ */
100
+ export declare function forwardsSubagentEvents(spec: Pick<TaskSpec, "forwardSubagentEvents">): boolean;
101
+ /** The drain itself — one macrotask turn. Callers gate it with {@link forwardsSubagentEvents} SYNCHRONOUSLY so a
102
+ * non-forwarding run's terminal push is not one microtask later than it was (an `await` of an early-returning
103
+ * async function still yields once; the black-box round measured that tick). */
104
+ export declare function drainForwardedFramesBeforeDone(): Promise<void>;
89
105
  export declare function prepareRunRefs(input: PrepareRunRefsInput): PrepareRunRefsResult;
@@ -2,6 +2,12 @@ import { createSubagentWorktreeHelper } from "../../agents/subagent.js";
2
2
  import { SubagentRetainLedger } from "../../agents/retain-ledger.js";
3
3
  import { ActiveSkillScope } from "./active-skill-scope.js";
4
4
  import { createEditedFilesLedger } from "./edited-files-ledger.js";
5
+ export function forwardsSubagentEvents(spec) {
6
+ return spec.forwardSubagentEvents === true;
7
+ }
8
+ export function drainForwardedFramesBeforeDone() {
9
+ return new Promise((resolve) => setImmediate(resolve));
10
+ }
5
11
  export function prepareRunRefs(input) {
6
12
  const { spec, internals, executionEnv, taskRootFinal } = input;
7
13
  const { note: noteFileEdited, snapshot: editedFilesSnapshot } = createEditedFilesLedger();
@@ -39,6 +39,7 @@ import { amendTerminal, terminalProjection } from "./terminal-projection.js";
39
39
  import { ATTACHMENT_BYTE_CAP, CHANGED_FILES_MAX, TOTAL_TOKENS_REMINDER_DEFAULT_MODE, AGENT_LISTING_REMOVED_HEADER, SKILLS_LISTING_DELTA_HEADER, SKILLS_LISTING_REMOVED_HEADER, advanceCadenceClock, agentListingDeltaHeader, attachmentEnvelopeTags, agentListingInitialHeader, replayAnnouncedListing, replayAnnouncedModels, clipToBytes, collectDateChange, collectDueAttachments, collectInstructionsChange, commitAgentListing, commitInstructionsChange, commitSkillsListing, createAttachmentState, rebaseCadenceWindows, reduceToolEnd, renderAgentListingDelta, renderMcpDroppedTools, renderMcpInstructionsDelta, renderOrphanedBackgroundTasks, selectMcpDroppedBatch, renderSkillsListingDelta, renderToolsDelta, stampWriteAnchor } from "./turn-attachments.js";
40
40
  import { buildWorkingFileAttachments, centerAdoptionOption, contextInstructionFilesOption, emitInputTruncated, forkContextOption } from "./compaction-call-options.js";
41
41
  import { effectiveDelegationFacts, prepareTask } from "./prepare-task.js";
42
+ import { drainForwardedFramesBeforeDone, forwardsSubagentEvents } from "./prepare-run-refs.js";
42
43
  import { gatedCallIdOf } from "./park-commit.js";
43
44
  import { placementValueOrAbsent } from "./checkpoint-scope.js";
44
45
  import { SessionReadFileStates } from "./prepare-hands-readface.js";
@@ -1837,6 +1838,8 @@ export class Runner {
1837
1838
  onError: (f) => console.warn(`[sema-core] ${f.site}: delegation-lifecycle observer threw (contained; further failures counted, not re-disclosed): ${f.error.message}`),
1838
1839
  }), "runtask.onDelegationLifecycle");
1839
1840
  }
1841
+ if (forwardsSubagentEvents(spec))
1842
+ await drainForwardedFramesBeforeDone();
1840
1843
  queue.push({ type: "done", result: resultValue });
1841
1844
  queue.close();
1842
1845
  });
@@ -4276,6 +4279,8 @@ export class Runner {
4276
4279
  drainManualCompact("mooted");
4277
4280
  }
4278
4281
  manualCompactRef.emitMooted = undefined;
4282
+ if (forwardsSubagentEvents(spec))
4283
+ await drainForwardedFramesBeforeDone();
4279
4284
  queue.push({ type: "done", result });
4280
4285
  queue.close();
4281
4286
  await prepared.fileHistoryBoundary?.settle();
@@ -4983,7 +4988,7 @@ export class Runner {
4983
4988
  if (plainPolicyOutcome !== undefined) {
4984
4989
  const parkedOrigin = cp.pendingAction.kind === "tool_approval" ? cp.pendingAction.origin : undefined;
4985
4990
  if (!isAskOrigin(parkedOrigin)) {
4986
- throw new CheckpointError("checkpoint.unsupported_version", "this checkpoint's pending approval carries no origin word (a row minted before the ask's origin was persisted on the park) — its settlement cannot be recorded; the checkpoint stays pending");
4991
+ throw new CheckpointError("checkpoint.unsupported_version", "this checkpoint's pending approval carries no origin word (a row minted before the ask's origin was persisted on the park) — its settlement cannot be recorded; the checkpoint stays pending", { reason: "origin_missing" });
4987
4992
  }
4988
4993
  const facts = plainPolicyOutcome.hostDecision;
4989
4994
  const approverCell = facts.approver !== undefined ? { approver: facts.approver } : {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sema-agent/core",
3
- "version": "7.6.1",
3
+ "version": "7.6.3",
4
4
  "description": "Stateless, task-oriented AI agent core",
5
5
  "type": "module",
6
6
  "license": "BUSL-1.1",