@sema-agent/core 7.52.0 → 7.53.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.
Files changed (191) hide show
  1. package/CHANGELOG.md +194 -0
  2. package/README.md +3 -1
  3. package/dist/agents/cross-session-envelope.d.ts +8 -0
  4. package/dist/agents/cross-session-envelope.js +6 -0
  5. package/dist/agents/cross-session-judge.d.ts +21 -1
  6. package/dist/agents/cross-session-judge.js +8 -0
  7. package/dist/agents/cross-session-ref.d.ts +5 -0
  8. package/dist/agents/cross-session-ref.js +1 -0
  9. package/dist/agents/org-delivery-gate.d.ts +122 -0
  10. package/dist/agents/org-delivery-gate.js +69 -0
  11. package/dist/agents/peer-held-queue.d.ts +9 -4
  12. package/dist/agents/peer-held-queue.js +6 -5
  13. package/dist/agents/peer-session-drain.d.ts +35 -5
  14. package/dist/agents/peer-session-drain.js +271 -180
  15. package/dist/agents/read-inbox-tool.d.ts +7 -0
  16. package/dist/agents/read-inbox-tool.js +5 -1
  17. package/dist/agents/subagent.d.ts +3 -0
  18. package/dist/agents/subagent.js +99 -72
  19. package/dist/agents/tool-filter.d.ts +18 -0
  20. package/dist/agents/tool-filter.js +6 -0
  21. package/dist/brain/anthropic.d.ts +6 -1
  22. package/dist/brain/anthropic.js +4 -4
  23. package/dist/brain/model-presets.js +2 -1
  24. package/dist/brain/route-adjudicator.d.ts +6 -0
  25. package/dist/brain/routing.d.ts +24 -1
  26. package/dist/brain/routing.js +54 -19
  27. package/dist/brain/timeout.js +5 -4
  28. package/dist/core/a2a.d.ts +5 -2
  29. package/dist/core/a2a.js +30 -13
  30. package/dist/core/allow-entry.d.ts +2 -1
  31. package/dist/core/allow-entry.js +2 -2
  32. package/dist/core/auto-compaction.d.ts +57 -0
  33. package/dist/core/auto-compaction.js +28 -2
  34. package/dist/core/checkpoint-store.d.ts +29 -20
  35. package/dist/core/checkpoint-store.js +3 -3
  36. package/dist/core/compliance.js +3 -1
  37. package/dist/core/effective-path-target.d.ts +25 -6
  38. package/dist/core/effective-path-target.js +24 -2
  39. package/dist/core/engine-notice.d.ts +13 -2
  40. package/dist/core/gate-fold.js +3 -2
  41. package/dist/core/gate-lanes.js +11 -13
  42. package/dist/core/gate-outcome.d.ts +11 -6
  43. package/dist/core/governance-codes.d.ts +3 -1
  44. package/dist/core/governance-codes.js +3 -0
  45. package/dist/core/hands-band-screen.d.ts +7 -0
  46. package/dist/core/hands-band-screen.js +32 -0
  47. package/dist/core/hooks.d.ts +7 -5
  48. package/dist/core/hooks.js +5 -3
  49. package/dist/core/locked-config.d.ts +84 -32
  50. package/dist/core/locked-config.js +128 -42
  51. package/dist/core/mcp-probe.js +1 -1
  52. package/dist/core/mcp-server-spec.d.ts +88 -10
  53. package/dist/core/mcp-server-spec.js +183 -33
  54. package/dist/core/mcp.d.ts +21 -14
  55. package/dist/core/mcp.js +55 -51
  56. package/dist/core/memory-engine/engine.d.ts +14 -19
  57. package/dist/core/memory-engine/engine.js +7 -5
  58. package/dist/core/mode-policy.d.ts +3 -18
  59. package/dist/core/mode-policy.js +2 -16
  60. package/dist/core/mods/kernel/dispatch.js +13 -15
  61. package/dist/core/mods/kernel/events.d.ts +9 -6
  62. package/dist/core/mods/kernel/events.js +2 -2
  63. package/dist/core/mods/kernel/receive.d.ts +39 -10
  64. package/dist/core/mods/kernel/receive.js +119 -54
  65. package/dist/core/mods/kernel/site-rules.d.ts +80 -15
  66. package/dist/core/mods/kernel/site-rules.js +232 -40
  67. package/dist/core/mods/stations.d.ts +0 -6
  68. package/dist/core/mods/stations.js +0 -7
  69. package/dist/core/mounted-mcp.d.ts +57 -0
  70. package/dist/core/mounted-mcp.js +71 -2
  71. package/dist/core/own-property-receipt.d.ts +13 -0
  72. package/dist/core/own-property-receipt.js +3 -1
  73. package/dist/core/parent-spec-seat.d.ts +2 -2
  74. package/dist/core/permission-rule-model.d.ts +58 -9
  75. package/dist/core/permission-rule-model.js +81 -7
  76. package/dist/core/permission-rule-org.d.ts +163 -33
  77. package/dist/core/permission-rule-org.js +205 -71
  78. package/dist/core/permission-rules.d.ts +7 -1
  79. package/dist/core/permission-rules.js +37 -8
  80. package/dist/core/retention.js +7 -2
  81. package/dist/core/retired-keys.d.ts +8 -2
  82. package/dist/core/retired-keys.js +12 -1
  83. package/dist/core/roles.js +60 -10
  84. package/dist/core/rule-lane-reads.js +1 -1
  85. package/dist/core/runner/contracts.d.ts +1 -0
  86. package/dist/core/runner/gate-exit.js +4 -3
  87. package/dist/core/runner/mcp-redial.d.ts +52 -10
  88. package/dist/core/runner/mcp-redial.js +38 -9
  89. package/dist/core/runner/permission-rule-lanes.d.ts +5 -2
  90. package/dist/core/runner/permission-rule-lanes.js +22 -10
  91. package/dist/core/runner/person-path-rows.d.ts +3 -1
  92. package/dist/core/runner/person-path-rows.js +2 -2
  93. package/dist/core/runner/prepare-config-doors.d.ts +48 -2
  94. package/dist/core/runner/prepare-config-doors.js +161 -145
  95. package/dist/core/runner/prepare-defer-classify.js +4 -7
  96. package/dist/core/runner/prepare-delegation-surface.d.ts +2 -2
  97. package/dist/core/runner/prepare-delegation-surface.js +7 -5
  98. package/dist/core/runner/prepare-hands-readface.d.ts +1 -1
  99. package/dist/core/runner/prepare-hands-readface.js +2 -1
  100. package/dist/core/runner/prepare-inherited-gate.js +2 -2
  101. package/dist/core/runner/prepare-listings.d.ts +1 -1
  102. package/dist/core/runner/prepare-listings.js +4 -2
  103. package/dist/core/runner/prepare-memory-engine-session.d.ts +0 -6
  104. package/dist/core/runner/prepare-memory-engine-session.js +19 -6
  105. package/dist/core/runner/prepare-memory.d.ts +3 -2
  106. package/dist/core/runner/prepare-memory.js +6 -2
  107. package/dist/core/runner/prepare-offload-wrappers.d.ts +4 -4
  108. package/dist/core/runner/prepare-offload-wrappers.js +2 -1
  109. package/dist/core/runner/prepare-policy-chain.d.ts +1 -1
  110. package/dist/core/runner/prepare-policy-chain.js +12 -12
  111. package/dist/core/runner/prepare-project-context.d.ts +1 -1
  112. package/dist/core/runner/prepare-project-context.js +6 -4
  113. package/dist/core/runner/prepare-prompt-assembly.d.ts +1 -1
  114. package/dist/core/runner/prepare-prompt-assembly.js +20 -16
  115. package/dist/core/runner/prepare-prompt-inputs.d.ts +12 -6
  116. package/dist/core/runner/prepare-prompt-inputs.js +6 -3
  117. package/dist/core/runner/prepare-protocol-tools.d.ts +7 -5
  118. package/dist/core/runner/prepare-protocol-tools.js +19 -12
  119. package/dist/core/runner/prepare-question-face.d.ts +4 -2
  120. package/dist/core/runner/prepare-question-face.js +4 -3
  121. package/dist/core/runner/prepare-task.js +10 -10
  122. package/dist/core/runner/prepare-wiring-manifest.d.ts +8 -3
  123. package/dist/core/runner/prepare-wiring-manifest.js +8 -5
  124. package/dist/core/runner/resume-admission.d.ts +4 -1
  125. package/dist/core/runner/resume-admission.js +7 -5
  126. package/dist/core/runner/resume-internals-and-config.d.ts +3 -3
  127. package/dist/core/runner/resume-internals-and-config.js +3 -8
  128. package/dist/core/runner/retired-runner-deps.d.ts +5 -6
  129. package/dist/core/runner/retired-runner-deps.js +1 -4
  130. package/dist/core/runner/run-leg.js +42 -12
  131. package/dist/core/runner/run-recovery-lanes.js +5 -3
  132. package/dist/core/runner/run-terminal-adoption.js +6 -5
  133. package/dist/core/runner/run-turn-boundary.js +9 -3
  134. package/dist/core/runner/runtask.js +1 -1
  135. package/dist/core/runner/skills-seat.d.ts +15 -0
  136. package/dist/core/runner/skills-seat.js +57 -0
  137. package/dist/core/runner/sub-agent-deny-lists.d.ts +13 -11
  138. package/dist/core/runner/sub-agent-deny-lists.js +7 -5
  139. package/dist/core/runner/synthetic-tools.d.ts +5 -2
  140. package/dist/core/runner/synthetic-tools.js +2 -12
  141. package/dist/core/runner-deps.d.ts +29 -14
  142. package/dist/core/session-policy-store.d.ts +19 -0
  143. package/dist/core/session-policy-store.js +22 -2
  144. package/dist/core/store-contracts/mailbox-store-contract.js +13 -0
  145. package/dist/core/task-event.d.ts +38 -2
  146. package/dist/core/task-notification.js +2 -2
  147. package/dist/core/task-spec.d.ts +1 -1
  148. package/dist/core/tool-policy.d.ts +3 -0
  149. package/dist/core/tool-policy.js +10 -4
  150. package/dist/core/types.d.ts +2 -2
  151. package/dist/core/types.js +1 -1
  152. package/dist/core/untrusted-text.d.ts +16 -0
  153. package/dist/core/untrusted-text.js +7 -2
  154. package/dist/core/user-attachment.d.ts +13 -2
  155. package/dist/core/user-attachment.js +47 -41
  156. package/dist/core/wiring-manifest.d.ts +4 -2
  157. package/dist/engine/harness/agent-harness.js +2 -1
  158. package/dist/engine/harness/types.d.ts +3 -0
  159. package/dist/engine/loop/agent-loop.d.ts +12 -1
  160. package/dist/engine/loop/agent-loop.js +17 -5
  161. package/dist/engine/loop/types.d.ts +7 -1
  162. package/dist/index.d.ts +15 -9
  163. package/dist/index.js +12 -6
  164. package/dist/internal/harness.d.ts +1 -0
  165. package/dist/internal/harness.js +1 -0
  166. package/dist/orchestration/goal.d.ts +24 -1
  167. package/dist/orchestration/goal.js +3 -2
  168. package/dist/prompt-assembly/artifact-store.d.ts +44 -9
  169. package/dist/prompt-assembly/artifact-store.js +38 -12
  170. package/dist/prompt-assembly/artifact.d.ts +12 -11
  171. package/dist/prompt-assembly/artifact.js +52 -21
  172. package/dist/prompt-assembly/assemble.d.ts +2 -0
  173. package/dist/prompt-assembly/assemble.js +2 -1
  174. package/dist/prompt-assembly/catalog/index.js +2 -1
  175. package/dist/prompt-assembly/catalog/memory.d.ts +13 -0
  176. package/dist/prompt-assembly/catalog/memory.js +6 -0
  177. package/dist/prompt-assembly/catalog/schema.d.ts +16 -2
  178. package/dist/prompt-assembly/composer.d.ts +2 -0
  179. package/dist/prompt-assembly/composer.js +1 -1
  180. package/dist/prompt-assembly/overlay.d.ts +21 -11
  181. package/dist/prompt-assembly/overlay.js +55 -17
  182. package/dist/prompt-assembly/select.d.ts +21 -6
  183. package/dist/prompt-assembly/select.js +39 -2
  184. package/dist/prompts/default.d.ts +0 -11
  185. package/dist/prompts/default.js +0 -3
  186. package/dist/stores/file/permission-rule-store.d.ts +1 -1
  187. package/dist/stores/file/permission-rule-store.js +2 -1
  188. package/dist/tools/artifact/artifact-tool.js +4 -3
  189. package/package.json +1 -1
  190. package/test/export-surface.snapshot.json +65 -5
  191. package/wire-changes/core@7.53.0.json +1946 -0
@@ -1,9 +1,11 @@
1
1
  import { mailboxCrossProcessMountVerdict } from "../core/mailbox-store.js";
2
2
  import { decodeEngineFrame, ENGINE_FRAME_CODEC_VERSION, renderTaskNotificationXml, taskNotificationDedupKey } from "../core/task-notification.js";
3
3
  import { createFrameSettlement, toolResultReceiptKey } from "../core/frame-settlement.js";
4
+ import { PRINCIPAL_INBOX_HANDLE } from "./cross-session-ref.js";
5
+ import { isGateMintedRecord, principalInboxConsumerOf } from "./org-delivery-gate.js";
4
6
  import { deliverEngineNotice } from "../core/types.js";
5
7
  import { buildCrossSessionEnvelope, CrossSessionCodecError, encodePeerAddress } from "./cross-session-envelope.js";
6
- import { judgeCrossSessionInbound, resolveCrossSessionInboundSetting } from "./cross-session-judge.js";
8
+ import { judgeCrossSessionInbound, principalInboundLayers, resolveCrossSessionInboundSetting } from "./cross-session-judge.js";
7
9
  import { judgePeerAdmission, peerAxisToken, resolvePeerLaneConfig } from "./peer-admission.js";
8
10
  import { isPeerSessionId, PEER_SESSION_ID_GRAMMAR, peerSessionBoxHandle } from "./peer-directory.js";
9
11
  import { inlineUntrusted } from "../core/untrusted-text.js";
@@ -16,6 +18,10 @@ import { routePeerDeliveryReceipt, routePeerDroppedReceipt, routePeerIdleNotice
16
18
  import { thrownText } from "../core/thrown-value.js";
17
19
  export const PEER_DRAIN_LEASE_TTL_MS = 5 * 60_000;
18
20
  export const PEER_SESSION_ID_UNGRAMMATICAL_CODE = "peer.session_id_ungrammatical";
21
+ export const PRINCIPAL_INBOX_UNMINTED_CAUSE = "not_minted_by_delivery_gate";
22
+ export function terminalsDrainPrincipalInbox(deps) {
23
+ return principalInboxConsumerOf(deps).kind === "terminals";
24
+ }
19
25
  export function judgePeerLaneMount(deps, sessionId) {
20
26
  if (deps.peerDirectory === undefined)
21
27
  return undefined;
@@ -52,26 +58,37 @@ export function judgeSessionBoxDrain(deps, leg) {
52
58
  export const PEER_RESUPPLY_CAP = 1;
53
59
  export function createPeerSessionDrain(opts) {
54
60
  const { mailbox, sessionId, runId, scope } = opts;
55
- const boxHandle = peerSessionBoxHandle(sessionId);
56
61
  const storedSessionAddress = (fromSession) => fromSession !== undefined && isPeerSessionId(fromSession) ? peerSessionBoxHandle(fromSession) : undefined;
57
62
  const drainOwner = `drain:${runId}`;
58
63
  const recipientKey = peerAxisToken(scope, "s", sessionId);
59
64
  const ttl = opts.leaseTtlMs ?? PEER_DRAIN_LEASE_TTL_MS;
60
65
  const TERMINAL_DISPOSITIONS = new Set(["committed", "resupply_exhausted", "terminal"]);
61
66
  const isTerminalDisposition = (d) => d !== undefined && TERMINAL_DISPOSITIONS.has(d.kind);
62
- const ledger = new Map();
63
- let ackedUpTo;
67
+ const clockOpt = opts.clock !== undefined ? { clock: opts.clock } : {};
68
+ const mintLane = (kind) => {
69
+ const boxHandle = kind === "session" ? peerSessionBoxHandle(sessionId) : PRINCIPAL_INBOX_HANDLE;
70
+ return {
71
+ kind,
72
+ boxHandle,
73
+ label: kind === "session" ? "this session's box" : "this principal's inbox",
74
+ ledger: new Map(),
75
+ ackedUpTo: undefined,
76
+ disclosedHeld: new Set(),
77
+ held: peerHeldQueueFor(scope, sessionId, kind === "session" ? clockOpt : { ...clockOpt, box: boxHandle }),
78
+ };
79
+ };
80
+ const lanes = opts.principalInbox === true ? [mintLane("session"), mintLane("principal")] : [mintLane("session")];
81
+ const sessionLane = lanes[0];
82
+ const boxHandle = sessionLane.boxHandle;
64
83
  const seam = opts.settlement ?? createFrameSettlement();
65
- const disclosedHeld = new Set();
66
84
  let inFlight = Promise.resolve();
67
85
  let noticeRound = 0;
68
86
  let currentRound;
69
- const held = peerHeldQueueFor(scope, sessionId, opts.clock !== undefined ? { clock: opts.clock } : {});
70
- const idleMachine = peerIdleMachineFor(scope, sessionId, opts.clock !== undefined ? { clock: opts.clock } : {});
71
- const disclose = (disposition, cause, m, heldNote) => deliverEngineNotice(opts.onNotice, {
87
+ const idleMachine = peerIdleMachineFor(scope, sessionId, clockOpt);
88
+ const disclose = (lane, disposition, cause, m, heldNote) => deliverEngineNotice(opts.onNotice, {
72
89
  code: "peer.inbound_disposition",
73
- message: `cross-session message seq ${m.seq} in this session's box was ${disposition === "held"
74
- ? `HELD${heldNote !== undefined ? ` (${heldNote})` : " (it entered this session's held queue and STAYS IN THE BOX \u2014 the box is that queue's durable seat, so it is not acked until it settles; it is re-judged at every boundary, released or refused with a receipt to the sender, and expires with a receipt under dialogExpiry)"}`
90
+ message: `cross-session message seq ${m.seq} in ${lane.label} was ${disposition === "held"
91
+ ? `HELD${heldNote !== undefined ? ` (${heldNote})` : lane.kind === "session" ? " (it entered this session's held queue and STAYS IN THE BOX \u2014 the box is that queue's durable seat, so it is not acked until it settles; it is re-judged at every boundary, released or refused with a receipt to the sender, and expires with a receipt under dialogExpiry)" : " (it entered this session's held queue for the principal inbox and STAYS IN THE BOX until it settles; it is re-judged at every boundary. The sender is another principal: no receipt reaches it from here \u2014 the delivery gate's receipt is its record)"}`
75
92
  : disposition === "refused"
76
93
  ? "refused by this session's crossSessionInbound setting"
77
94
  : disposition === "resupply_exhausted"
@@ -81,20 +98,23 @@ export function createPeerSessionDrain(opts) {
81
98
  : disposition === "notice_user_only"
82
99
  ? "surfaced to the user only (a notice whose sender this session's parity would hold; the model did not read it)"
83
100
  : "refused by the inbound admission gate"}: ${cause}`,
84
- detail: { disposition, cause, seq: m.seq, box: boxHandle, ...(m.peerMeta?.fromSession !== undefined ? { fromSession: m.peerMeta.fromSession } : {}), sessionId, runId },
101
+ detail: { disposition, cause, seq: m.seq, box: lane.boxHandle, ...(m.peerMeta?.fromSession !== undefined ? { fromSession: m.peerMeta.fromSession } : {}), sessionId, runId },
85
102
  });
86
103
  const readFacts = () => {
87
- const setting = resolveCrossSessionInboundSetting(opts.settingLayers() ?? {});
88
- return { setting, selfModeClass: opts.selfModeClass() };
104
+ const layers = opts.settingLayers() ?? {};
105
+ return { setting: resolveCrossSessionInboundSetting(layers), principalSetting: resolveCrossSessionInboundSetting(principalInboundLayers(layers)), selfModeClass: opts.selfModeClass() };
89
106
  };
90
- const judgeMeta = (facts, meta) => judgeCrossSessionInbound({ setting: facts.setting, selfModeClass: facts.selfModeClass, sender: { ...(meta?.fromMode !== undefined ? { fromMode: meta.fromMode } : {}), selfSent: meta?.fromSession !== undefined && meta.fromSession.toLowerCase() === sessionId.toLowerCase() } });
107
+ const judgeMeta = (facts, meta) => meta?.fromScope !== undefined
108
+ ? judgeCrossSessionInbound({ setting: facts.principalSetting, selfModeClass: facts.selfModeClass, sender: { fromScope: meta.fromScope } })
109
+ : judgeCrossSessionInbound({ setting: facts.setting, selfModeClass: facts.selfModeClass, sender: { ...(meta?.fromMode !== undefined ? { fromMode: meta.fromMode } : {}), selfSent: meta?.fromSession !== undefined && meta.fromSession.toLowerCase() === sessionId.toLowerCase() } });
91
110
  const route = () => ({
92
111
  mailbox,
93
112
  scope,
94
113
  self: { sessionId, ...(opts.selfName?.() !== undefined ? { name: opts.selfName() } : {}), modeClass: opts.selfModeClass },
95
114
  ...(opts.onError !== undefined ? { onError: opts.onError } : {}),
96
115
  });
97
- const buildPeerMessagePayload = (m) => {
116
+ const buildPeerMessagePayload = (lane, m) => {
117
+ const { boxHandle } = lane;
98
118
  const meta = m.peerMeta;
99
119
  const fields = {
100
120
  from: encodePeerAddress(storedSessionAddress(meta?.fromSession) ?? (m.from ?? "unknown")),
@@ -126,8 +146,9 @@ export function createPeerSessionDrain(opts) {
126
146
  crossSessionNotice: { kind, text },
127
147
  _sema_provenance: { kind: "cross_session_notice", from: encodePeerAddress(storedSessionAddress(m.peerMeta?.fromSession) ?? (m.from ?? "unknown")), taskId: boxHandle, seq: m.seq, ...(m.peerMeta !== undefined ? { peerMeta: { ...m.peerMeta } } : {}) },
128
148
  });
129
- const receipt = (entry, state) => {
130
- const to = entry.peerMeta?.fromSession;
149
+ const receiptAddress = (lane, entry) => (lane.kind === "session" ? entry.peerMeta?.fromSession : undefined);
150
+ const receipt = (lane) => (entry, state) => {
151
+ const to = receiptAddress(lane, entry);
131
152
  if (to === undefined)
132
153
  return;
133
154
  if (state === "dropped")
@@ -135,41 +156,42 @@ export function createPeerSessionDrain(opts) {
135
156
  else
136
157
  void routePeerDeliveryReceipt(route(), to, state, entry.seq);
137
158
  };
138
- const heldSink = (live) => ({
159
+ const heldSink = (lane, live) => ({
139
160
  judge: (entry) => judgeMeta(readFacts(), entry.peerMeta),
140
161
  ...(live
141
162
  ? {
142
163
  inject: (entry) => {
143
164
  if (currentRound === undefined)
144
165
  return "deferred";
145
- const payload = buildPeerMessagePayload(entry);
166
+ const payload = buildPeerMessagePayload(lane, entry);
146
167
  if (currentRound.deliver(payload, "next") === "deferred")
147
168
  return "deferred";
148
- noteDelivered(entry.seq, 1, payload, currentRound.carrier);
169
+ noteDelivered(lane, entry.seq, 1, payload, currentRound.carrier);
149
170
  return undefined;
150
171
  },
151
172
  }
152
173
  : {}),
153
- receipt,
174
+ receipt: receipt(lane),
154
175
  onHeld: (entry, count, cause, reannounce) => {
155
- if (!reannounce && disclosedHeld.has(entry.seq))
176
+ if (!reannounce && lane.disclosedHeld.has(entry.seq))
156
177
  return;
157
- disclosedHeld.add(entry.seq);
158
- disclose("held", cause, entry, reannounce ? `still held; its cause moved to ${cause} (${count} held)` : undefined);
178
+ lane.disclosedHeld.add(entry.seq);
179
+ disclose(lane, "held", cause, entry, reannounce ? `still held; its cause moved to ${cause} (${count} held)` : undefined);
159
180
  },
160
181
  onSettled: (entry, settlement, reason, count) => {
161
- if (entry.box === boxHandle && settlement !== "delivered")
162
- ledger.set(entry.seq, { kind: "terminal", why: `held_${settlement}` });
182
+ if (entry.box === lane.boxHandle && settlement !== "delivered")
183
+ lane.ledger.set(entry.seq, { kind: "terminal", why: `held_${settlement}` });
184
+ const replyTo = receiptAddress(lane, entry);
163
185
  deliverEngineNotice(opts.onNotice, {
164
186
  code: "peer.held_settled",
165
- message: `held cross-session message seq ${entry.seq} left this session's held queue: ${settlement} (${reason}); ${count} still held${settlement === "expired" && reason === "evicted" ? " — the held queue was full, so the oldest entry was expired to make room" : ""}. The sender ${entry.peerMeta?.fromSession !== undefined ? "was sent a receipt" : "has no reply address and was not told"}.`,
166
- detail: { settlement, reason, seq: entry.seq, box: boxHandle, ...(entry.peerMeta?.fromSession !== undefined ? { fromSession: entry.peerMeta.fromSession } : {}), heldCount: count, sessionId, runId },
187
+ message: `held cross-session message seq ${entry.seq} left this session's held queue: ${settlement} (${reason}); ${count} still held${settlement === "expired" && reason === "evicted" ? " — the held queue was full, so the oldest entry was expired to make room" : ""}. The sender ${replyTo !== undefined ? "was sent a receipt" : "has no reply address and was not told"}.`,
188
+ detail: { settlement, reason, seq: entry.seq, box: lane.boxHandle, ...(entry.peerMeta?.fromSession !== undefined ? { fromSession: entry.peerMeta.fromSession } : {}), heldCount: count, sessionId, runId },
167
189
  });
168
190
  },
169
191
  });
170
192
  let dialogExpiryMs = resolveCrossSessionDialogExpiry(undefined).ms;
171
193
  let dialogExpiryAnnounced = false;
172
- const bindHeld = (live) => {
194
+ const bindHeld = (lane, live) => {
173
195
  try {
174
196
  const resolved = resolveCrossSessionDialogExpiry(opts.dialogExpiry?.());
175
197
  dialogExpiryMs = resolved.ms;
@@ -181,67 +203,84 @@ export function createPeerSessionDrain(opts) {
181
203
  catch (e) {
182
204
  opts.onError?.(e, { phase: "degraded", sessionId, classification: "peer-dialog-expiry" });
183
205
  }
184
- held.configure({ sink: heldSink(live), dialogExpiryMs });
206
+ lane.held.configure({ sink: heldSink(lane, live), dialogExpiryMs });
185
207
  };
186
- bindHeld(true);
208
+ for (const lane of lanes)
209
+ bindHeld(lane, true);
187
210
  idleMachine.target.configure({
188
211
  notify: (sub, notice) => void routePeerIdleNotice(route(), sub.requester, notice),
189
212
  judge: (sub) => judgeMeta(readFacts(), { ...(sub.fromMode !== undefined ? { fromMode: sub.fromMode } : {}), ...(sub.selfSent ? { fromSession: sessionId } : {}) }),
190
- heldCount: () => held.size(),
213
+ heldCount: () => lanes.reduce((n, lane) => n + lane.held.size(), 0),
191
214
  ...(opts.lastTurnText !== undefined ? { lastTurnText: opts.lastTurnText } : {}),
192
215
  });
193
216
  idleMachine.requester.configure({
194
217
  expired: (o) => void routePeerIdleNotice(route(), sessionId, { kind: "expired", label: o.label }),
195
218
  });
196
- const adoptHeldDecisions = () => {
197
- const decided = held.decisions();
219
+ const adoptHeldDecisions = (lane) => {
220
+ const decided = lane.held.decisions();
198
221
  if (decided.size === 0)
199
222
  return;
200
223
  for (const [seq, settlement] of decided) {
201
- if (ackedUpTo !== undefined && seq <= ackedUpTo)
224
+ if (lane.ackedUpTo !== undefined && seq <= lane.ackedUpTo)
202
225
  continue;
203
- const mine = ledger.get(seq);
226
+ const mine = lane.ledger.get(seq);
204
227
  if (mine !== undefined && mine.kind !== "held")
205
228
  continue;
206
- ledger.set(seq, { kind: "terminal", why: `held_${settlement}` });
229
+ lane.ledger.set(seq, { kind: "terminal", why: `held_${settlement}` });
207
230
  }
208
231
  };
209
- const reconcileHeld = (lease) => {
210
- held.reconcile(new Set(lease.messages.map((m) => m.seq)));
232
+ const reconcileHeld = (lane, lease) => {
233
+ lane.held.reconcile(new Set(lease.messages.map((m) => m.seq)));
211
234
  let queued;
212
- for (const [seq, d] of ledger) {
235
+ for (const [seq, d] of lane.ledger) {
213
236
  if (d.kind !== "held")
214
237
  continue;
215
- queued ??= new Set(held.list().filter((e) => e.box === boxHandle).map((e) => e.seq));
238
+ queued ??= new Set(lane.held.list().filter((e) => e.box === lane.boxHandle).map((e) => e.seq));
216
239
  if (!queued.has(seq))
217
- ledger.delete(seq);
240
+ lane.ledger.delete(seq);
218
241
  }
219
242
  };
220
243
  const wantsCompactionEffects = () => {
221
- for (const d of ledger.values())
222
- if (d.kind === "delivered" || d.kind === "committed")
223
- return true;
244
+ for (const lane of lanes)
245
+ for (const d of lane.ledger.values())
246
+ if (d.kind === "delivered" || d.kind === "committed")
247
+ return true;
224
248
  return false;
225
249
  };
226
- const noteDelivered = (seq, attempt, payload, carrier) => {
250
+ const noteDelivered = (lane, seq, attempt, payload, carrier) => {
227
251
  const own = taskNotificationDedupKey(payload);
228
252
  const key = carrier === undefined ? own : `${carrier}|${own}`;
229
- ledger.set(seq, { kind: "delivered", attempt, key, payload });
253
+ lane.ledger.set(seq, { kind: "delivered", attempt, key, payload });
254
+ currentRound?.tally(lane, attempt);
255
+ if (carrier === undefined && payload._sema_provenance?.peerMeta?.fromScope !== undefined) {
256
+ try {
257
+ opts.markExternalContent?.(`a letter from another principal (cross-principal message seq ${seq} in ${lane.boxHandle}) was delivered into this session`);
258
+ }
259
+ catch (e) {
260
+ opts.onError?.(e, { phase: "degraded", sessionId, classification: "peer-external-mark" });
261
+ }
262
+ }
230
263
  seam.awaiting.set(key, carrier === undefined ? { carrier: own } : { carrier, frameText: renderTaskNotificationXml(payload) });
231
264
  };
232
- const deriveAckPrefix = (messages) => {
265
+ const deriveAckPrefix = (lane, messages) => {
233
266
  let prefix;
234
267
  for (const m of messages) {
235
- if (ackedUpTo !== undefined && m.seq <= ackedUpTo)
268
+ if (lane.ackedUpTo !== undefined && m.seq <= lane.ackedUpTo)
236
269
  continue;
237
- if (!isTerminalDisposition(ledger.get(m.seq)))
270
+ if (!isTerminalDisposition(lane.ledger.get(m.seq)))
238
271
  break;
239
272
  prefix = m.seq;
240
273
  }
241
274
  return prefix;
242
275
  };
243
276
  const settleLocally = () => {
244
- adoptHeldDecisions();
277
+ const landed = seam.compactions.splice(0);
278
+ for (const lane of lanes)
279
+ settleLaneLocally(lane, landed);
280
+ };
281
+ const settleLaneLocally = (lane, landed) => {
282
+ const { ledger, boxHandle } = lane;
283
+ adoptHeldDecisions(lane);
245
284
  const hostWithheldSeqs = [];
246
285
  if (seam.receipts.size > 0 || seam.withheld.size > 0 || seam.hostWithheld.size > 0) {
247
286
  for (const [seq, d] of ledger) {
@@ -269,7 +308,7 @@ export function createPeerSessionDrain(opts) {
269
308
  if (hostWithheldSeqs.length > 0) {
270
309
  opts.onError?.(new Error(`${hostWithheldSeqs.length} cross-session message frame(s) (seq ${hostWithheldSeqs.join(", ")}) could not be found in this session's ReadInbox result as a postToolUse hook rewrote it (removed, truncated or reshaped beyond whitespace) — settled as the host's decision: they leave ${boxHandle} and are not handed over again`), { phase: "degraded", sessionId, classification: "peer-inbox-host-withheld" });
271
310
  }
272
- for (const effect of seam.compactions.splice(0)) {
311
+ for (const effect of landed) {
273
312
  for (const [seq, d] of ledger) {
274
313
  if (d.kind !== "committed")
275
314
  continue;
@@ -281,23 +320,24 @@ export function createPeerSessionDrain(opts) {
281
320
  for (const [seq, d] of ledger) {
282
321
  if (d.kind === "invalidated" && d.attempt > PEER_RESUPPLY_CAP) {
283
322
  const meta = d.payload._sema_provenance?.peerMeta;
284
- disclose("resupply_exhausted", `attempt ${d.attempt} of ${PEER_RESUPPLY_CAP + 1} was summarized away too`, { seq, ...(meta !== undefined ? { peerMeta: meta } : {}) });
323
+ disclose(lane, "resupply_exhausted", `attempt ${d.attempt} of ${PEER_RESUPPLY_CAP + 1} was summarized away too`, { seq, ...(meta !== undefined ? { peerMeta: meta } : {}) });
285
324
  ledger.set(seq, { kind: "resupply_exhausted" });
286
325
  }
287
326
  }
288
327
  };
289
- const ackSettledPrefix = async (lease) => {
328
+ const ackSettledPrefix = async (lane, lease) => {
329
+ const { held, ledger, boxHandle } = lane;
290
330
  const decided = held.decisions();
291
331
  if (decided.size > 0) {
292
332
  const live = new Set(lease.messages.map((m) => m.seq));
293
333
  held.forgetDecisions([...decided.keys()].filter((seq) => !live.has(seq)));
294
334
  }
295
- const prefix = deriveAckPrefix(lease.messages);
296
- if (prefix !== undefined && (ackedUpTo === undefined || prefix > ackedUpTo)) {
335
+ const prefix = deriveAckPrefix(lane, lease.messages);
336
+ if (prefix !== undefined && (lane.ackedUpTo === undefined || prefix > lane.ackedUpTo)) {
297
337
  try {
298
338
  const result = await mailbox.ack(scope, boxHandle, drainOwner, prefix);
299
339
  if (result.acked) {
300
- ackedUpTo = prefix;
340
+ lane.ackedUpTo = prefix;
301
341
  const gone = [];
302
342
  for (const seq of [...ledger.keys()]) {
303
343
  if (seq > prefix)
@@ -308,7 +348,7 @@ export function createPeerSessionDrain(opts) {
308
348
  held.forgetDecisions(gone);
309
349
  }
310
350
  else {
311
- opts.onError?.(new Error(`the session box ${boxHandle} refused this leg's ack up to seq ${prefix}: the drain lease is no longer held by ${drainOwner} (it expired and another cycle claimed it). The records stay parked and the lease holder re-serves them.`), { phase: "degraded", sessionId, classification: "peer-box-ack" });
351
+ opts.onError?.(new Error(`the ${lane.kind === "session" ? "session box" : "principal inbox"} ${boxHandle} refused this leg's ack up to seq ${prefix}: the drain lease is no longer held by ${drainOwner} (it expired and another cycle claimed it). The records stay parked and the lease holder re-serves them.`), { phase: "degraded", sessionId, classification: "peer-box-ack" });
312
352
  }
313
353
  }
314
354
  catch (e) {
@@ -321,8 +361,8 @@ export function createPeerSessionDrain(opts) {
321
361
  const kind = meta.kind;
322
362
  const stateFits = notice !== undefined && (kind === "idle_notice" ? isPeerIdleNoticeKind(notice.state) : isPeerDeliveryReceiptState(notice.state));
323
363
  if (!stateFits) {
324
- disclose("notice_unrouted", `kind=${kind}${notice === undefined ? " without a typed notice" : ` with state=${inlineUntrusted(notice.state, 32)}`}`, m);
325
- currentRound.dispose(m.seq, `notice_unrouted:${kind}`);
364
+ disclose(sessionLane, "notice_unrouted", `kind=${kind}${notice === undefined ? " without a typed notice" : ` with state=${inlineUntrusted(notice.state, 32)}`}`, m);
365
+ currentRound.dispose(sessionLane, m.seq, `notice_unrouted:${kind}`);
326
366
  return;
327
367
  }
328
368
  let text;
@@ -341,22 +381,22 @@ export function createPeerSessionDrain(opts) {
341
381
  }
342
382
  const judged = judgeMeta(facts, meta);
343
383
  if (judged.verdict === "refuse") {
344
- disclose("refused", `${judged.cause} (a ${kind})`, m);
384
+ disclose(sessionLane, "refused", `${judged.cause} (a ${kind})`, m);
345
385
  }
346
386
  else if (judged.verdict === "hold") {
347
- disclose("notice_user_only", `${judged.cause}: ${text}`, m);
387
+ disclose(sessionLane, "notice_user_only", `${judged.cause}: ${text}`, m);
348
388
  }
349
389
  else {
350
- currentRound.batch.push({ payload: buildNoticePayload(m, kind, text), priority: "next", urgent: false, seq: m.seq, attempt: 1 });
390
+ currentRound.batch.push({ lane: sessionLane, payload: buildNoticePayload(m, kind, text), priority: "next", urgent: false, seq: m.seq, attempt: 1 });
351
391
  return;
352
392
  }
353
- currentRound.dispose(m.seq, judged.verdict === "refuse" ? "refused" : "notice_user_only");
393
+ currentRound.dispose(sessionLane, m.seq, judged.verdict === "refuse" ? "refused" : "notice_user_only");
354
394
  };
355
395
  const settleSubscription = (facts, m, meta) => {
356
396
  const requester = meta.fromSession;
357
397
  if (requester === undefined) {
358
- disclose("notice_unrouted", "kind=idle_subscription without a reply address (fromSession)", m);
359
- currentRound.dispose(m.seq, "subscription_unaddressed");
398
+ disclose(sessionLane, "notice_unrouted", "kind=idle_subscription without a reply address (fromSession)", m);
399
+ currentRound.dispose(sessionLane, m.seq, "subscription_unaddressed");
360
400
  return;
361
401
  }
362
402
  const sub = { requester, ...(m.from !== undefined ? { requesterName: m.from } : {}), ...(meta.fromMode !== undefined ? { fromMode: meta.fromMode } : {}), selfSent: requester.toLowerCase() === sessionId.toLowerCase(), requestedAt: m.sentAt };
@@ -368,22 +408,22 @@ export function createPeerSessionDrain(opts) {
368
408
  if (facts.setting.value === "refuse") {
369
409
  void routePeerIdleNotice(route(), requester, { kind: "unavailable" });
370
410
  announce("refused", idleMachine.target.size());
371
- currentRound.dispose(m.seq, "subscription_refused");
411
+ currentRound.dispose(sessionLane, m.seq, "subscription_refused");
372
412
  return;
373
413
  }
374
414
  const outcome = idleMachine.target.subscribe(sub);
375
415
  if (outcome === "full")
376
416
  void routePeerIdleNotice(route(), requester, { kind: "unavailable" });
377
417
  announce(outcome, idleMachine.target.size());
378
- currentRound.dispose(m.seq, `subscription_${outcome}`);
418
+ currentRound.dispose(sessionLane, m.seq, `subscription_${outcome}`);
379
419
  };
380
- const countWaiting = (lease) => {
420
+ const countWaiting = (lane, lease) => {
381
421
  if (lease === null)
382
422
  return { pending: 0, urgent: 0 };
383
423
  const waiting = lease.messages.filter((m) => {
384
- if (ackedUpTo !== undefined && m.seq <= ackedUpTo)
424
+ if (lane.ackedUpTo !== undefined && m.seq <= lane.ackedUpTo)
385
425
  return false;
386
- const d = ledger.get(m.seq);
426
+ const d = lane.ledger.get(m.seq);
387
427
  return d === undefined || d.kind === "invalidated";
388
428
  });
389
429
  return { pending: waiting.length, urgent: waiting.filter((m) => m.peerMeta?.urgent === true).length };
@@ -395,9 +435,20 @@ export function createPeerSessionDrain(opts) {
395
435
  carrier;
396
436
  charBudget;
397
437
  injected = 0;
398
- resupplied = 0;
399
- dispose(seq, why) {
400
- ledger.set(seq, { kind: "terminal", why });
438
+ tallies = new Map();
439
+ tally(lane, attempt) {
440
+ const t = this.tallies.get(lane) ?? { fresh: 0, resupplied: 0 };
441
+ if (attempt > 1)
442
+ t.resupplied += 1;
443
+ else
444
+ t.fresh += 1;
445
+ this.tallies.set(lane, t);
446
+ }
447
+ deliveredIn(lane) {
448
+ return this.tallies.get(lane) ?? { fresh: 0, resupplied: 0 };
449
+ }
450
+ dispose(lane, seq, why) {
451
+ lane.ledger.set(seq, { kind: "terminal", why });
401
452
  }
402
453
  batch = [];
403
454
  chars = 0;
@@ -437,20 +488,29 @@ export function createPeerSessionDrain(opts) {
437
488
  catch {
438
489
  }
439
490
  }
440
- let lease;
441
- try {
442
- lease = await mailbox.claimLease(scope, boxHandle, drainOwner, ttl);
491
+ const claims = [];
492
+ for (const lane of lanes) {
493
+ try {
494
+ claims.push({ lane, lease: await mailbox.claimLease(scope, lane.boxHandle, drainOwner, ttl) });
495
+ }
496
+ catch (e) {
497
+ opts.onError?.(e, { phase: "degraded", sessionId, classification: "peer-box-claim" });
498
+ }
443
499
  }
444
- catch (e) {
445
- opts.onError?.(e, { phase: "degraded", sessionId, classification: "peer-box-claim" });
500
+ if (claims.length === 0)
446
501
  return { pending: 0, urgent: 0 };
447
- }
502
+ const waitingAcross = () => claims.reduce((sum, { lane, lease }) => {
503
+ const c = countWaiting(lane, lease);
504
+ return { pending: sum.pending + c.pending, urgent: sum.urgent + c.urgent };
505
+ }, { pending: 0, urgent: 0 });
448
506
  if (pass.settle)
449
507
  settleLocally();
450
- if (pass.settle && lease !== null)
451
- await ackSettledPrefix(lease);
508
+ if (pass.settle)
509
+ for (const { lane, lease } of claims)
510
+ if (lease !== null)
511
+ await ackSettledPrefix(lane, lease);
452
512
  if (!pass.deliver)
453
- return countWaiting(lease);
513
+ return waitingAcross();
454
514
  let facts;
455
515
  let admissionConfig;
456
516
  try {
@@ -464,92 +524,104 @@ export function createPeerSessionDrain(opts) {
464
524
  currentRound = round;
465
525
  try {
466
526
  idleMachine.target.sweep();
467
- if (lease === null)
527
+ const leased = claims.filter((c) => c.lease !== null);
528
+ if (leased.length === 0)
468
529
  return { pending: 0, urgent: 0 };
469
- bindHeld(true);
470
- reconcileHeld(lease);
471
- held.rejudge();
530
+ for (const { lane, lease } of leased) {
531
+ bindHeld(lane, true);
532
+ reconcileHeld(lane, lease);
533
+ lane.held.rejudge();
534
+ }
472
535
  try {
473
- for (const m of lease.messages) {
474
- const prior = ledger.get(m.seq);
475
- if (prior === undefined || prior.kind !== "invalidated")
476
- continue;
477
- if (round.spent)
478
- break;
479
- const attempt = prior.attempt + 1;
480
- round.batch.push({ payload: { ...prior.payload, attempt, resupplied: true }, priority: "next", urgent: false, seq: m.seq, attempt });
481
- }
482
- adoptHeldDecisions();
483
- for (const m of lease.messages) {
484
- if (ackedUpTo !== undefined && m.seq <= ackedUpTo)
485
- continue;
486
- if (ledger.has(m.seq))
487
- continue;
488
- if (round.spent)
489
- break;
490
- const meta = m.peerMeta;
491
- if (m.hopChain !== undefined) {
492
- const verdict = judgePeerAdmission(scope, recipientKey, { senderKey: meta?.senderKey, body: m.content, prospectiveChain: m.hopChain, ownTokens: opts.ownTokens(), seq: m.seq }, admissionConfig, undefined, "drain");
493
- if (!verdict.ok) {
494
- if (verdict.reason === "rate_limited") {
495
- if (!disclosedHeld.has(m.seq)) {
496
- disclosedHeld.add(m.seq);
497
- disclose("held", verdict.reason, m, "it stays in the box; the inbound admission bucket is empty and it is re-judged at a later boundary once the bucket refills");
498
- }
499
- break;
500
- }
501
- disclose("admission_refused", verdict.reason, m);
502
- round.dispose(m.seq, `admission_refused:${verdict.reason}`);
536
+ for (const { lane, lease } of leased) {
537
+ const { ledger, disclosedHeld, held, boxHandle } = lane;
538
+ const ackedUpTo = lane.ackedUpTo;
539
+ for (const m of lease.messages) {
540
+ const prior = ledger.get(m.seq);
541
+ if (prior === undefined || prior.kind !== "invalidated")
503
542
  continue;
504
- }
505
- }
506
- if (meta?.kind === "idle_subscription") {
507
- settleSubscription(facts, m, meta);
508
- continue;
509
- }
510
- if (meta?.kind === "idle_notice" || meta?.kind === "delivery_notice") {
511
- settleNoticeRecord(facts, m, meta);
512
- continue;
543
+ if (round.spent)
544
+ break;
545
+ const attempt = prior.attempt + 1;
546
+ round.batch.push({ lane, payload: { ...prior.payload, attempt, resupplied: true }, priority: "next", urgent: false, seq: m.seq, attempt });
513
547
  }
514
- if (meta?.kind === "engine_frame") {
515
- const decoded = decodeEngineFrame(m.content);
516
- if (!decoded.ok && decoded.reason === "newer") {
517
- opts.onError?.(new Error(`engine_frame record seq ${m.seq} in ${boxHandle} carries codec version ${decoded.codecVersion}; this engine reads up to ${ENGINE_FRAME_CODEC_VERSION} — the box stops at that record until an engine that reads it drains the session`), { phase: "degraded", sessionId, classification: "peer-box-engine-frame" });
548
+ adoptHeldDecisions(lane);
549
+ for (const m of lease.messages) {
550
+ if (ackedUpTo !== undefined && m.seq <= ackedUpTo)
551
+ continue;
552
+ if (ledger.has(m.seq))
553
+ continue;
554
+ if (round.spent)
518
555
  break;
556
+ const meta = m.peerMeta;
557
+ if (lane.kind === "principal" && !isGateMintedRecord(meta)) {
558
+ disclose(lane, "admission_refused", PRINCIPAL_INBOX_UNMINTED_CAUSE, m);
559
+ round.dispose(lane, m.seq, `admission_refused:${PRINCIPAL_INBOX_UNMINTED_CAUSE}`);
560
+ continue;
519
561
  }
520
- if (!decoded.ok) {
521
- disclose("notice_unrouted", `kind=engine_frame (${decoded.detail})`, m);
522
- round.dispose(m.seq, `engine_frame_malformed:${decoded.detail}`);
562
+ if (m.hopChain !== undefined) {
563
+ const verdict = judgePeerAdmission(scope, recipientKey, { senderKey: meta?.senderKey, body: m.content, prospectiveChain: m.hopChain, ownTokens: opts.ownTokens(), seq: m.seq }, admissionConfig, undefined, "drain");
564
+ if (!verdict.ok) {
565
+ if (verdict.reason === "rate_limited") {
566
+ if (!disclosedHeld.has(m.seq)) {
567
+ disclosedHeld.add(m.seq);
568
+ disclose(lane, "held", verdict.reason, m, "it stays in the box; the inbound admission bucket is empty and it is re-judged at a later boundary once the bucket refills");
569
+ }
570
+ break;
571
+ }
572
+ disclose(lane, "admission_refused", verdict.reason, m);
573
+ round.dispose(lane, m.seq, `admission_refused:${verdict.reason}`);
574
+ continue;
575
+ }
576
+ }
577
+ if (meta?.kind === "idle_subscription") {
578
+ settleSubscription(facts, m, meta);
523
579
  continue;
524
580
  }
525
- round.batch.push({ payload: decoded.payload, priority: decoded.priority, urgent: false, seq: m.seq, attempt: 1 });
526
- continue;
527
- }
528
- const sameTree = meta?.fromRoot !== undefined && meta.fromRoot.toLowerCase() === sessionId.toLowerCase();
529
- if (!sameTree) {
530
- const judged = judgeMeta(facts, meta);
531
- if (judged.verdict === "refuse") {
532
- disclose("refused", judged.cause, m);
533
- round.dispose(m.seq, `refused:${judged.cause}`);
581
+ if (meta?.kind === "idle_notice" || meta?.kind === "delivery_notice") {
582
+ settleNoticeRecord(facts, m, meta);
534
583
  continue;
535
584
  }
536
- if (judged.verdict === "hold") {
537
- held.hold({ seq: m.seq, box: boxHandle, content: m.content, ...(m.from !== undefined ? { from: m.from } : {}), sentAt: m.sentAt, ...(m.hopChain !== undefined ? { hopChain: [...m.hopChain] } : {}), ...(meta !== undefined ? { peerMeta: { ...meta } } : {}), cause: judged.cause });
538
- if (!ledger.has(m.seq))
539
- ledger.set(m.seq, { kind: "held" });
585
+ if (meta?.kind === "engine_frame") {
586
+ const decoded = decodeEngineFrame(m.content);
587
+ if (!decoded.ok && decoded.reason === "newer") {
588
+ opts.onError?.(new Error(`engine_frame record seq ${m.seq} in ${boxHandle} carries codec version ${decoded.codecVersion}; this engine reads up to ${ENGINE_FRAME_CODEC_VERSION} — the box stops at that record until an engine that reads it drains the session`), { phase: "degraded", sessionId, classification: "peer-box-engine-frame" });
589
+ break;
590
+ }
591
+ if (!decoded.ok) {
592
+ disclose(lane, "notice_unrouted", `kind=engine_frame (${decoded.detail})`, m);
593
+ round.dispose(lane, m.seq, `engine_frame_malformed:${decoded.detail}`);
594
+ continue;
595
+ }
596
+ round.batch.push({ lane, payload: decoded.payload, priority: decoded.priority, urgent: false, seq: m.seq, attempt: 1 });
540
597
  continue;
541
598
  }
599
+ const sameTree = meta?.fromRoot !== undefined && meta.fromRoot.toLowerCase() === sessionId.toLowerCase();
600
+ if (!sameTree) {
601
+ const judged = judgeMeta(facts, meta);
602
+ if (judged.verdict === "refuse") {
603
+ disclose(lane, "refused", judged.cause, m);
604
+ round.dispose(lane, m.seq, `refused:${judged.cause}`);
605
+ continue;
606
+ }
607
+ if (judged.verdict === "hold") {
608
+ held.hold({ seq: m.seq, box: boxHandle, content: m.content, ...(m.from !== undefined ? { from: m.from } : {}), sentAt: m.sentAt, ...(m.hopChain !== undefined ? { hopChain: [...m.hopChain] } : {}), ...(meta !== undefined ? { peerMeta: { ...meta } } : {}), cause: judged.cause });
609
+ if (!ledger.has(m.seq))
610
+ ledger.set(m.seq, { kind: "held" });
611
+ continue;
612
+ }
613
+ }
614
+ let payload;
615
+ try {
616
+ payload = buildPeerMessagePayload(lane, m);
617
+ }
618
+ catch (e) {
619
+ disclose(lane, "refused", `record fields cannot be rendered canonically (${e instanceof CrossSessionCodecError ? e.code : thrownText(e)})`, m);
620
+ round.dispose(lane, m.seq, "uncanonical_fields");
621
+ continue;
622
+ }
623
+ round.batch.push({ lane, payload, priority: "next", urgent: meta?.urgent === true, seq: m.seq, attempt: 1 });
542
624
  }
543
- let payload;
544
- try {
545
- payload = buildPeerMessagePayload(m);
546
- }
547
- catch (e) {
548
- disclose("refused", `record fields cannot be rendered canonically (${e instanceof CrossSessionCodecError ? e.code : thrownText(e)})`, m);
549
- round.dispose(m.seq, "uncanonical_fields");
550
- continue;
551
- }
552
- round.batch.push({ payload, priority: "next", urgent: meta?.urgent === true, seq: m.seq, attempt: 1 });
553
625
  }
554
626
  }
555
627
  finally {
@@ -563,13 +635,15 @@ export function createPeerSessionDrain(opts) {
563
635
  opts.onError?.(e, { phase: "degraded", sessionId, classification: "peer-box-inject" });
564
636
  break;
565
637
  }
566
- noteDelivered(item.seq, item.attempt, item.payload, round.carrier);
567
- if (item.attempt > 1)
568
- round.resupplied += 1;
638
+ noteDelivered(item.lane, item.seq, item.attempt, item.payload, round.carrier);
569
639
  }
570
640
  }
571
- const counts = countWaiting(lease);
572
- if (counts.pending > 0 && round.out === undefined) {
641
+ for (const { lane, lease } of leased) {
642
+ const counts = countWaiting(lane, lease);
643
+ if (counts.pending === 0 || round.out !== undefined)
644
+ continue;
645
+ const { boxHandle } = lane;
646
+ const delivered = round.deliveredIn(lane);
573
647
  noticeRound += 1;
574
648
  try {
575
649
  opts.inject({
@@ -578,7 +652,7 @@ export function createPeerSessionDrain(opts) {
578
652
  status: "event",
579
653
  seq: noticeRound,
580
654
  summary: `inbox notice: ${counts.pending} cross-session message(s) waiting`,
581
- inboxNotice: { pending: counts.pending, delivered: round.injected - round.resupplied, ...(round.resupplied > 0 ? { resupplied: round.resupplied } : {}), ...(counts.urgent > 0 ? { urgent: counts.urgent } : {}), box: boxHandle },
655
+ inboxNotice: { pending: counts.pending, delivered: delivered.fresh, ...(delivered.resupplied > 0 ? { resupplied: delivered.resupplied } : {}), ...(counts.urgent > 0 ? { urgent: counts.urgent } : {}), box: boxHandle },
582
656
  _sema_provenance: { kind: "inbox_notice", from: boxHandle, taskId: `inbox:${boxHandle}`, seq: noticeRound },
583
657
  }, { priority: "next" });
584
658
  }
@@ -586,7 +660,7 @@ export function createPeerSessionDrain(opts) {
586
660
  opts.onError?.(e, { phase: "degraded", sessionId, classification: "peer-box-inject" });
587
661
  }
588
662
  }
589
- return counts;
663
+ return waitingAcross();
590
664
  }
591
665
  finally {
592
666
  currentRound = undefined;
@@ -613,20 +687,33 @@ export function createPeerSessionDrain(opts) {
613
687
  return settled.then(() => ({ delivered: out, pending: counts.pending, urgent: counts.urgent }));
614
688
  };
615
689
  const finishOnce = async () => {
616
- try {
617
- const lease = await mailbox.claimLease(scope, boxHandle, drainOwner, ttl);
618
- settleLocally();
619
- if (lease !== null)
620
- await ackSettledPrefix(lease);
621
- }
622
- catch (e) {
623
- opts.onError?.(e, { phase: "degraded", sessionId, classification: "peer-box-claim" });
690
+ const claimed = [];
691
+ for (const lane of lanes) {
692
+ try {
693
+ claimed.push({ lane, lease: await mailbox.claimLease(scope, lane.boxHandle, drainOwner, ttl) });
694
+ }
695
+ catch (e) {
696
+ opts.onError?.(e, { phase: "degraded", sessionId, classification: "peer-box-claim" });
697
+ }
624
698
  }
625
- held.detachLeg();
626
- try {
627
- await mailbox.releaseLease(scope, boxHandle, drainOwner);
699
+ if (claimed.length > 0) {
700
+ try {
701
+ settleLocally();
702
+ for (const { lane, lease } of claimed)
703
+ if (lease !== null)
704
+ await ackSettledPrefix(lane, lease);
705
+ }
706
+ catch (e) {
707
+ opts.onError?.(e, { phase: "degraded", sessionId, classification: "peer-box-claim" });
708
+ }
628
709
  }
629
- catch {
710
+ for (const lane of lanes) {
711
+ lane.held.detachLeg();
712
+ try {
713
+ await mailbox.releaseLease(scope, lane.boxHandle, drainOwner);
714
+ }
715
+ catch {
716
+ }
630
717
  }
631
718
  };
632
719
  const finish = () => {
@@ -642,16 +729,18 @@ export function createPeerSessionDrain(opts) {
642
729
  if (p === null || typeof p !== "object" || typeof p.task_id !== "string" || typeof p.status !== "string")
643
730
  return false;
644
731
  const key = taskNotificationDedupKey(p);
645
- for (const d of ledger.values())
646
- if (d.kind === "delivered" && d.key === key)
647
- return true;
732
+ for (const lane of lanes)
733
+ for (const d of lane.ledger.values())
734
+ if (d.kind === "delivered" && d.key === key)
735
+ return true;
648
736
  return false;
649
737
  },
650
738
  finish,
651
739
  wantsCompactionEffects,
652
740
  busy: () => idleMachine.target.busy(),
653
741
  idle: (finishedAt) => idleMachine.target.idle(finishedAt),
654
- held,
742
+ held: sessionLane.held,
743
+ ...(lanes[1] !== undefined ? { principalHeld: lanes[1].held } : {}),
655
744
  idleMachine,
656
745
  };
657
746
  }
@@ -684,7 +773,7 @@ export function peerLaneSendMessageSeats(args) {
684
773
  };
685
774
  }
686
775
  export function listAgentsMountable(face) {
687
- return face.selector.admits({ name: LIST_AGENTS_TOOL_NAME, aliases: [LIST_AGENTS_TOOL_ALIAS] }) && listAgentsNameFree(face.specTools);
776
+ return face.leaves({ name: LIST_AGENTS_TOOL_NAME, aliases: [LIST_AGENTS_TOOL_ALIAS] }) && listAgentsNameFree(face.specTools);
688
777
  }
689
778
  function listAgentsNameFree(specTools) {
690
779
  return !specTools.some((t) => t.name === LIST_AGENTS_TOOL_NAME || t.name === LIST_AGENTS_TOOL_ALIAS || t.aliases?.includes(LIST_AGENTS_TOOL_NAME) || t.aliases?.includes(LIST_AGENTS_TOOL_ALIAS));
@@ -754,6 +843,7 @@ export function bindPeerLaneDrain(harness, args) {
754
843
  sessionId: args.sessionId,
755
844
  runId: args.runId,
756
845
  scope: args.scope,
846
+ ...(terminalsDrainPrincipalInbox(deps) ? { principalInbox: true } : {}),
757
847
  inject: args.inject,
758
848
  ...(deps.peerLane !== undefined ? { lane: deps.peerLane } : {}),
759
849
  ownTokens: args.ownTokens,
@@ -765,6 +855,7 @@ export function bindPeerLaneDrain(harness, args) {
765
855
  ...(args.settlement !== undefined ? { settlement: args.settlement } : {}),
766
856
  ...(args.parked !== undefined ? { parked: args.parked } : {}),
767
857
  ...(args.lastTurnText !== undefined ? { lastTurnText: args.lastTurnText } : {}),
858
+ ...(args.markExternalContent !== undefined ? { markExternalContent: args.markExternalContent } : {}),
768
859
  ...(deps.onNotice !== undefined ? { onNotice: deps.onNotice } : {}),
769
860
  ...(deps.onError !== undefined ? { onError: deps.onError } : {}),
770
861
  });