@sema-agent/core 7.29.0 → 7.30.1

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 (158) hide show
  1. package/CHANGELOG.md +393 -0
  2. package/dist/agents/child-model-seat.d.ts +59 -7
  3. package/dist/agents/child-model-seat.js +33 -7
  4. package/dist/agents/cross-session-judge.d.ts +27 -5
  5. package/dist/agents/cross-session-judge.js +23 -12
  6. package/dist/agents/list-agents-tool.d.ts +31 -6
  7. package/dist/agents/list-agents-tool.js +66 -14
  8. package/dist/agents/peer-admission.d.ts +20 -8
  9. package/dist/agents/peer-admission.js +21 -12
  10. package/dist/agents/peer-directory.d.ts +29 -0
  11. package/dist/agents/peer-directory.js +20 -0
  12. package/dist/agents/peer-held-queue.d.ts +31 -4
  13. package/dist/agents/peer-held-queue.js +32 -1
  14. package/dist/agents/peer-session-drain.d.ts +91 -13
  15. package/dist/agents/peer-session-drain.js +409 -102
  16. package/dist/agents/read-inbox-tool.d.ts +22 -0
  17. package/dist/agents/read-inbox-tool.js +39 -0
  18. package/dist/agents/roster-store.d.ts +2 -4
  19. package/dist/agents/roster-store.js +1 -1
  20. package/dist/agents/send-message-tool.d.ts +4 -4
  21. package/dist/agents/send-message-tool.js +22 -2
  22. package/dist/agents/subagent.js +62 -44
  23. package/dist/agents/team-role.d.ts +34 -0
  24. package/dist/agents/team-role.js +17 -0
  25. package/dist/agents/verify.js +2 -2
  26. package/dist/core/auto-compaction.d.ts +42 -0
  27. package/dist/core/auto-compaction.js +35 -2
  28. package/dist/core/child-clamp-seats.d.ts +11 -0
  29. package/dist/core/child-clamp-seats.js +3 -0
  30. package/dist/core/context-edit.d.ts +3 -2
  31. package/dist/core/context-edit.js +6 -5
  32. package/dist/core/context-guard.d.ts +2 -1
  33. package/dist/core/context-guard.js +3 -2
  34. package/dist/core/context-settings.d.ts +103 -0
  35. package/dist/core/context-settings.js +60 -0
  36. package/dist/core/engine-notice.d.ts +11 -1
  37. package/dist/core/frame-settlement.d.ts +97 -0
  38. package/dist/core/frame-settlement.js +36 -0
  39. package/dist/core/governance-codes.d.ts +1 -1
  40. package/dist/core/governance-codes.js +2 -0
  41. package/dist/core/hooks.d.ts +10 -10
  42. package/dist/core/mailbox-store.d.ts +41 -7
  43. package/dist/core/mailbox-store.js +9 -3
  44. package/dist/core/parent-spec-seat.d.ts +14 -1
  45. package/dist/core/parent-spec-seat.js +6 -0
  46. package/dist/core/permission-rule-model.js +1 -1
  47. package/dist/core/roles.d.ts +20 -0
  48. package/dist/core/roles.js +35 -16
  49. package/dist/core/runner/assemble-result.d.ts +2 -0
  50. package/dist/core/runner/assemble-result.js +1 -1
  51. package/dist/core/runner/compaction-call-options.d.ts +26 -2
  52. package/dist/core/runner/compaction-call-options.js +9 -0
  53. package/dist/core/runner/compaction-seams.d.ts +4 -3
  54. package/dist/core/runner/compaction-seams.js +13 -6
  55. package/dist/core/runner/contracts.d.ts +56 -7
  56. package/dist/core/runner/decide-continuation.d.ts +2 -1
  57. package/dist/core/runner/decide-continuation.js +11 -10
  58. package/dist/core/runner/initial-run-state.js +1 -1
  59. package/dist/core/runner/prepare-caps-and-workflow.js +9 -1
  60. package/dist/core/runner/prepare-config-doors.js +4 -1
  61. package/dist/core/runner/prepare-context-lane.d.ts +9 -2
  62. package/dist/core/runner/prepare-context-lane.js +15 -10
  63. package/dist/core/runner/prepare-delegation-surface.d.ts +3 -2
  64. package/dist/core/runner/prepare-delegation-surface.js +9 -3
  65. package/dist/core/runner/prepare-file-history.js +10 -8
  66. package/dist/core/runner/prepare-gate-stations.d.ts +4 -0
  67. package/dist/core/runner/prepare-gate-stations.js +3 -1
  68. package/dist/core/runner/prepare-inherited-gate.js +1 -0
  69. package/dist/core/runner/prepare-prompt-inputs.d.ts +6 -0
  70. package/dist/core/runner/prepare-prompt-inputs.js +2 -1
  71. package/dist/core/runner/prepare-task.js +22 -9
  72. package/dist/core/runner/prepare-wiring-manifest.d.ts +6 -1
  73. package/dist/core/runner/prepare-wiring-manifest.js +6 -1
  74. package/dist/core/runner/retired-runner-deps.d.ts +3 -1
  75. package/dist/core/runner/retired-runner-deps.js +1 -0
  76. package/dist/core/runner/run-compaction-machinery.d.ts +4 -2
  77. package/dist/core/runner/run-compaction-machinery.js +5 -3
  78. package/dist/core/runner/run-harness-handlers.js +1 -1
  79. package/dist/core/runner/run-identity-wiring.js +19 -1
  80. package/dist/core/runner/run-leg.js +1 -1
  81. package/dist/core/runner/run-notification-lane.js +8 -2
  82. package/dist/core/runner/run-recovery-lanes.js +9 -5
  83. package/dist/core/runner/run-settle-and-teardown.d.ts +4 -22
  84. package/dist/core/runner/run-settle-and-teardown.js +6 -10
  85. package/dist/core/runner/run-stop-and-final-verify.js +1 -0
  86. package/dist/core/runner/run-terminal-adoption.js +5 -5
  87. package/dist/core/runner/run-turn-boundary.js +32 -5
  88. package/dist/core/runner/runtask.d.ts +8 -1
  89. package/dist/core/runner/runtask.js +32 -1
  90. package/dist/core/runner-deps.d.ts +44 -13
  91. package/dist/core/secret-env.js +2 -1
  92. package/dist/core/swappable-deps.d.ts +20 -4
  93. package/dist/core/swappable-deps.js +6 -1
  94. package/dist/core/task-event.d.ts +5 -0
  95. package/dist/core/task-notification.d.ts +144 -2
  96. package/dist/core/task-notification.js +154 -5
  97. package/dist/core/task-result.d.ts +14 -0
  98. package/dist/core/task-spec.d.ts +7 -0
  99. package/dist/core/tool-catalog-entries.js +2 -1
  100. package/dist/core/tool-conformance.js +1 -0
  101. package/dist/core/tool-policy.d.ts +9 -4
  102. package/dist/core/tool-spec.d.ts +14 -0
  103. package/dist/core/wiring-manifest.d.ts +12 -0
  104. package/dist/core/wiring-manifest.js +5 -1
  105. package/dist/engine/harness/agent-harness.d.ts +14 -1
  106. package/dist/engine/harness/agent-harness.js +28 -3
  107. package/dist/engine/harness/types.d.ts +3 -1
  108. package/dist/engine/llm/types.d.ts +8 -7
  109. package/dist/engine/llm/types.js +1 -1
  110. package/dist/engine/lsp/node-lsp-manager.d.ts +3 -0
  111. package/dist/engine/lsp/node-lsp-manager.js +5 -3
  112. package/dist/engine/session/import-validate.js +4 -0
  113. package/dist/index.d.ts +10 -7
  114. package/dist/index.js +7 -5
  115. package/dist/orchestration/run-workflow-tool.d.ts +4 -4
  116. package/dist/orchestration/workflow.d.ts +1 -1
  117. package/dist/orchestration/workflow.js +8 -5
  118. package/dist/shell/lex.d.ts +38 -54
  119. package/dist/shell/lex.js +239 -286
  120. package/dist/stores/cc/mailbox-store.js +3 -2
  121. package/dist/stores/file/corrupt-read-sink.d.ts +28 -2
  122. package/dist/stores/file/file-history-store.d.ts +2 -4
  123. package/dist/stores/file/file-history-store.js +1 -1
  124. package/dist/stores/file/fs-atomic.d.ts +16 -11
  125. package/dist/stores/file/fs-atomic.js +17 -6
  126. package/dist/stores/file/index.d.ts +5 -6
  127. package/dist/stores/file/index.js +1 -1
  128. package/dist/stores/file/mailbox-store.d.ts +4 -9
  129. package/dist/stores/file/mailbox-store.js +29 -30
  130. package/dist/stores/file/session-policy-store.d.ts +2 -0
  131. package/dist/stores/file/session-policy-store.js +1 -1
  132. package/dist/stores/file/session-store.d.ts +11 -7
  133. package/dist/stores/file/session-store.js +23 -16
  134. package/dist/stores/file/shared-ledger.js +3 -14
  135. package/dist/stores/file/tool-result-store.d.ts +2 -0
  136. package/dist/stores/file/tool-result-store.js +5 -2
  137. package/dist/stores/file/workflow-journal-store.js +14 -9
  138. package/dist/tools/fs/bash-program-position.d.ts +15 -24
  139. package/dist/tools/fs/bash-program-position.js +11 -29
  140. package/dist/tools/fs/bash-readonly-classifier.d.ts +131 -117
  141. package/dist/tools/fs/bash-readonly-classifier.js +404 -272
  142. package/dist/tools/fs/shell-read-check.js +3 -3
  143. package/dist/tools/fs/shell-read-establish.d.ts +1 -1
  144. package/dist/tools/fs/shell-read-establish.js +5 -33
  145. package/dist/tools/fs/shell-read-identity.d.ts +13 -0
  146. package/dist/tools/fs/shell-read-identity.js +39 -0
  147. package/dist/tools/fs/shell-read-judge.d.ts +8 -0
  148. package/dist/tools/fs/shell-read-judge.js +7 -0
  149. package/dist/tools/fs/shell-read-plan.d.ts +15 -6
  150. package/dist/tools/fs/shell-read-plan.js +171 -195
  151. package/dist/tools/fs/shell-read-word.d.ts +64 -0
  152. package/dist/tools/fs/shell-read-word.js +94 -0
  153. package/dist/tools/fs/shell-reentry-text.d.ts +10 -0
  154. package/dist/tools/fs/shell-reentry-text.js +73 -0
  155. package/package.json +1 -1
  156. package/test/export-surface.snapshot.json +58 -8
  157. package/wire-changes/core@7.30.0.json +1821 -0
  158. package/wire-changes/core@7.30.1.json +154 -0
@@ -1,8 +1,10 @@
1
1
  import { mailboxCrossProcessMountVerdict } from "../core/mailbox-store.js";
2
+ import { decodeEngineFrame, ENGINE_FRAME_CODEC_VERSION, renderTaskNotificationXml, taskNotificationDedupKey } from "../core/task-notification.js";
3
+ import { createFrameSettlement, toolResultReceiptKey } from "../core/frame-settlement.js";
2
4
  import { deliverEngineNotice } from "../core/types.js";
3
5
  import { buildCrossSessionEnvelope, CrossSessionCodecError, encodePeerAddress } from "./cross-session-envelope.js";
4
6
  import { judgeCrossSessionInbound, resolveCrossSessionInboundSetting } from "./cross-session-judge.js";
5
- import { judgePeerAdmission, peerAxisToken, resolvePeerAdmissionConfig } from "./peer-admission.js";
7
+ import { judgePeerAdmission, peerAxisToken, resolvePeerLaneConfig } from "./peer-admission.js";
6
8
  import { isPeerSessionId, PEER_SESSION_ID_GRAMMAR, peerSessionBoxHandle } from "./peer-directory.js";
7
9
  import { inlineUntrusted } from "../core/untrusted-text.js";
8
10
  import { createListAgentsTool, LIST_AGENTS_TOOL_ALIAS, LIST_AGENTS_TOOL_NAME } from "./list-agents-tool.js";
@@ -43,9 +45,11 @@ export function judgeSessionBoxDrain(deps, leg) {
43
45
  scope: leg.scope,
44
46
  handle: peerSessionBoxHandle(leg.sessionId),
45
47
  sessionId: leg.sessionId,
48
+ ...(deps.sessionBoxEngineFrames === true ? { engineFrames: true } : {}),
46
49
  onError: (e) => deps.onError?.(e, { phase: "degraded", sessionId: leg.sessionId, classification: "session-box-park" }),
47
50
  };
48
51
  }
52
+ export const PEER_RESUPPLY_CAP = 1;
49
53
  export function createPeerSessionDrain(opts) {
50
54
  const { mailbox, sessionId, runId, scope } = opts;
51
55
  const boxHandle = peerSessionBoxHandle(sessionId);
@@ -53,28 +57,41 @@ export function createPeerSessionDrain(opts) {
53
57
  const drainOwner = `drain:${runId}`;
54
58
  const recipientKey = peerAxisToken(scope, "s", sessionId);
55
59
  const ttl = opts.leaseTtlMs ?? PEER_DRAIN_LEASE_TTL_MS;
56
- let settledUpTo;
60
+ const TERMINAL_DISPOSITIONS = new Set(["committed", "resupply_exhausted", "terminal"]);
61
+ const isTerminalDisposition = (d) => d !== undefined && TERMINAL_DISPOSITIONS.has(d.kind);
62
+ const ledger = new Map();
63
+ let ackedUpTo;
64
+ const seam = opts.settlement ?? createFrameSettlement();
57
65
  const disclosedHeld = new Set();
58
66
  let inFlight = Promise.resolve();
67
+ let noticeRound = 0;
68
+ let deprecatedSpellingAnnounced = false;
69
+ let currentRound;
59
70
  const held = peerHeldQueueFor(scope, sessionId, opts.clock !== undefined ? { clock: opts.clock } : {});
60
71
  const idleMachine = peerIdleMachineFor(scope, sessionId, opts.clock !== undefined ? { clock: opts.clock } : {});
61
72
  const disclose = (disposition, cause, m, heldNote) => deliverEngineNotice(opts.onNotice, {
62
73
  code: "peer.inbound_disposition",
63
74
  message: `cross-session message seq ${m.seq} in this session's box was ${disposition === "held"
64
- ? `HELD${heldNote !== undefined ? ` (${heldNote})` : " (it entered this session's held queue, acked from the box; it is re-judged at every boundary, released or refused with a receipt to the sender, and expires with a receipt under dialogExpiry)"}`
75
+ ? `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)"}`
65
76
  : disposition === "refused"
66
77
  ? "refused by this session's crossSessionInbound setting"
67
- : disposition === "notice_unrouted"
68
- ? "settled WITHOUT delivery (a notice-kind record the notice face cannot render)"
69
- : disposition === "notice_user_only"
70
- ? "surfaced to the user only (a notice whose sender this session's parity would hold; the model did not read it)"
71
- : "refused by the inbound admission gate"}: ${cause}`,
78
+ : disposition === "resupply_exhausted"
79
+ ? "settled WITHOUT being read: its frame was summarized away by a compaction, re-supplied once, and summarized away again \u2014 the engine has stopped handing it over. The sender was not told; reply-worthy content is GONE from this session's context"
80
+ : disposition === "notice_unrouted"
81
+ ? "settled WITHOUT delivery (a notice-kind record the notice face cannot render)"
82
+ : disposition === "notice_user_only"
83
+ ? "surfaced to the user only (a notice whose sender this session's parity would hold; the model did not read it)"
84
+ : "refused by the inbound admission gate"}: ${cause}`,
72
85
  detail: { disposition, cause, seq: m.seq, box: boxHandle, ...(m.peerMeta?.fromSession !== undefined ? { fromSession: m.peerMeta.fromSession } : {}), sessionId, runId },
73
86
  });
74
- const readFacts = () => ({
75
- setting: resolveCrossSessionInboundSetting(opts.settingLayers() ?? {}),
76
- selfModeClass: opts.selfModeClass(),
77
- });
87
+ const readFacts = () => {
88
+ const setting = resolveCrossSessionInboundSetting(opts.settingLayers() ?? {});
89
+ if (setting.deprecatedLayers !== undefined && !deprecatedSpellingAnnounced) {
90
+ deprecatedSpellingAnnounced = true;
91
+ opts.onError?.(new Error(`crossSessionInbound is spelled "accept" in the ${setting.deprecatedLayers.join(", ")} layer(s); that spelling is read as "next-turn" (deliver at the next turn boundary) — update the settings file`), { phase: "degraded", sessionId, classification: "peer-inbound-setting-spelling" });
92
+ }
93
+ return { setting, selfModeClass: opts.selfModeClass() };
94
+ };
78
95
  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() } });
79
96
  const route = () => ({
80
97
  mailbox,
@@ -125,7 +142,19 @@ export function createPeerSessionDrain(opts) {
125
142
  };
126
143
  const heldSink = (live) => ({
127
144
  judge: (entry) => judgeMeta(readFacts(), entry.peerMeta),
128
- ...(live ? { inject: (entry) => opts.inject(buildPeerMessagePayload(entry), { priority: "next" }) } : {}),
145
+ ...(live
146
+ ? {
147
+ inject: (entry) => {
148
+ if (currentRound === undefined)
149
+ return "deferred";
150
+ const payload = buildPeerMessagePayload(entry);
151
+ if (currentRound.deliver(payload, "next") === "deferred")
152
+ return "deferred";
153
+ noteDelivered(entry.seq, 1, payload, currentRound.carrier);
154
+ return undefined;
155
+ },
156
+ }
157
+ : {}),
129
158
  receipt,
130
159
  onHeld: (entry, count, cause, reannounce) => {
131
160
  if (!reannounce && disclosedHeld.has(entry.seq))
@@ -133,11 +162,15 @@ export function createPeerSessionDrain(opts) {
133
162
  disclosedHeld.add(entry.seq);
134
163
  disclose("held", cause, entry, reannounce ? `still held; its cause moved to ${cause} (${count} held)` : undefined);
135
164
  },
136
- onSettled: (entry, settlement, reason, count) => deliverEngineNotice(opts.onNotice, {
137
- code: "peer.held_settled",
138
- 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"}.`,
139
- detail: { settlement, reason, seq: entry.seq, box: boxHandle, ...(entry.peerMeta?.fromSession !== undefined ? { fromSession: entry.peerMeta.fromSession } : {}), heldCount: count, sessionId, runId },
140
- }),
165
+ onSettled: (entry, settlement, reason, count) => {
166
+ if (entry.box === boxHandle && settlement !== "delivered")
167
+ ledger.set(entry.seq, { kind: "terminal", why: `held_${settlement}` });
168
+ deliverEngineNotice(opts.onNotice, {
169
+ code: "peer.held_settled",
170
+ 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"}.`,
171
+ detail: { settlement, reason, seq: entry.seq, box: boxHandle, ...(entry.peerMeta?.fromSession !== undefined ? { fromSession: entry.peerMeta.fromSession } : {}), heldCount: count, sessionId, runId },
172
+ });
173
+ },
141
174
  });
142
175
  let dialogExpiryMs = resolveCrossSessionDialogExpiry(undefined).ms;
143
176
  let dialogExpiryAnnounced = false;
@@ -165,18 +198,127 @@ export function createPeerSessionDrain(opts) {
165
198
  idleMachine.requester.configure({
166
199
  expired: (o) => void routePeerIdleNotice(route(), sessionId, { kind: "expired", label: o.label }),
167
200
  });
168
- const ackSettled = async () => {
169
- if (settledUpTo === undefined)
201
+ const adoptHeldDecisions = () => {
202
+ const decided = held.decisions();
203
+ if (decided.size === 0)
170
204
  return;
171
- const upTo = settledUpTo;
172
- settledUpTo = undefined;
173
- try {
174
- await mailbox.ack(scope, boxHandle, drainOwner, upTo);
205
+ for (const [seq, settlement] of decided) {
206
+ if (ackedUpTo !== undefined && seq <= ackedUpTo)
207
+ continue;
208
+ const mine = ledger.get(seq);
209
+ if (mine !== undefined && mine.kind !== "held")
210
+ continue;
211
+ ledger.set(seq, { kind: "terminal", why: `held_${settlement}` });
175
212
  }
176
- catch (e) {
177
- if (settledUpTo === undefined || settledUpTo < upTo)
178
- settledUpTo = upTo;
179
- opts.onError?.(e, { phase: "degraded", sessionId, classification: "peer-box-ack" });
213
+ };
214
+ const reconcileHeld = (lease) => {
215
+ held.reconcile(new Set(lease.messages.map((m) => m.seq)));
216
+ let queued;
217
+ for (const [seq, d] of ledger) {
218
+ if (d.kind !== "held")
219
+ continue;
220
+ queued ??= new Set(held.list().filter((e) => e.box === boxHandle).map((e) => e.seq));
221
+ if (!queued.has(seq))
222
+ ledger.delete(seq);
223
+ }
224
+ };
225
+ const wantsCompactionEffects = () => {
226
+ for (const d of ledger.values())
227
+ if (d.kind === "delivered" || d.kind === "committed")
228
+ return true;
229
+ return false;
230
+ };
231
+ const noteDelivered = (seq, attempt, payload, carrier) => {
232
+ const own = taskNotificationDedupKey(payload);
233
+ const key = carrier === undefined ? own : `${carrier}|${own}`;
234
+ ledger.set(seq, { kind: "delivered", attempt, key, payload });
235
+ seam.awaiting.set(key, carrier === undefined ? { carrier: own } : { carrier, frameText: renderTaskNotificationXml(payload) });
236
+ };
237
+ const deriveAckPrefix = (messages) => {
238
+ let prefix;
239
+ for (const m of messages) {
240
+ if (ackedUpTo !== undefined && m.seq <= ackedUpTo)
241
+ continue;
242
+ if (!isTerminalDisposition(ledger.get(m.seq)))
243
+ break;
244
+ prefix = m.seq;
245
+ }
246
+ return prefix;
247
+ };
248
+ const settleLocally = () => {
249
+ adoptHeldDecisions();
250
+ const hostWithheldSeqs = [];
251
+ if (seam.receipts.size > 0 || seam.withheld.size > 0 || seam.hostWithheld.size > 0) {
252
+ for (const [seq, d] of ledger) {
253
+ if (d.kind !== "delivered")
254
+ continue;
255
+ if (seam.hostWithheld.delete(d.key)) {
256
+ ledger.set(seq, { kind: "terminal", why: "host_withheld" });
257
+ hostWithheldSeqs.push(seq);
258
+ continue;
259
+ }
260
+ if (seam.withheld.delete(d.key)) {
261
+ if (d.attempt > 1)
262
+ ledger.set(seq, { kind: "invalidated", attempt: d.attempt - 1, payload: d.payload });
263
+ else
264
+ ledger.delete(seq);
265
+ continue;
266
+ }
267
+ const entryId = seam.receipts.get(d.key);
268
+ if (entryId === undefined)
269
+ continue;
270
+ seam.receipts.delete(d.key);
271
+ ledger.set(seq, { kind: "committed", attempt: d.attempt, key: d.key, entryId, payload: d.payload });
272
+ }
273
+ }
274
+ if (hostWithheldSeqs.length > 0) {
275
+ 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" });
276
+ }
277
+ for (const effect of seam.compactions.splice(0)) {
278
+ for (const [seq, d] of ledger) {
279
+ if (d.kind !== "committed")
280
+ continue;
281
+ if (effect.summarizedEntryIds === "unknown" || effect.summarizedEntryIds.has(d.entryId)) {
282
+ ledger.set(seq, { kind: "invalidated", attempt: d.attempt, payload: d.payload });
283
+ }
284
+ }
285
+ }
286
+ for (const [seq, d] of ledger) {
287
+ if (d.kind === "invalidated" && d.attempt > PEER_RESUPPLY_CAP) {
288
+ const meta = d.payload._sema_provenance?.peerMeta;
289
+ disclose("resupply_exhausted", `attempt ${d.attempt} of ${PEER_RESUPPLY_CAP + 1} was summarized away too`, { seq, ...(meta !== undefined ? { peerMeta: meta } : {}) });
290
+ ledger.set(seq, { kind: "resupply_exhausted" });
291
+ }
292
+ }
293
+ };
294
+ const ackSettledPrefix = async (lease) => {
295
+ const decided = held.decisions();
296
+ if (decided.size > 0) {
297
+ const live = new Set(lease.messages.map((m) => m.seq));
298
+ held.forgetDecisions([...decided.keys()].filter((seq) => !live.has(seq)));
299
+ }
300
+ const prefix = deriveAckPrefix(lease.messages);
301
+ if (prefix !== undefined && (ackedUpTo === undefined || prefix > ackedUpTo)) {
302
+ try {
303
+ const result = await mailbox.ack(scope, boxHandle, drainOwner, prefix);
304
+ if (result.acked) {
305
+ ackedUpTo = prefix;
306
+ const gone = [];
307
+ for (const seq of [...ledger.keys()]) {
308
+ if (seq > prefix)
309
+ continue;
310
+ gone.push(seq);
311
+ ledger.delete(seq);
312
+ }
313
+ held.forgetDecisions(gone);
314
+ }
315
+ else {
316
+ 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" });
317
+ }
318
+ }
319
+ catch (e) {
320
+ opts.onError?.(e, { phase: "degraded", sessionId, classification: "peer-box-ack" });
321
+ }
180
322
  }
181
323
  };
182
324
  const settleNoticeRecord = (facts, m, meta) => {
@@ -185,7 +327,7 @@ export function createPeerSessionDrain(opts) {
185
327
  const stateFits = notice !== undefined && (kind === "idle_notice" ? isPeerIdleNoticeKind(notice.state) : isPeerDeliveryReceiptState(notice.state));
186
328
  if (!stateFits) {
187
329
  disclose("notice_unrouted", `kind=${kind}${notice === undefined ? " without a typed notice" : ` with state=${inlineUntrusted(notice.state, 32)}`}`, m);
188
- settledUpTo = m.seq;
330
+ currentRound.dispose(m.seq, `notice_unrouted:${kind}`);
189
331
  return;
190
332
  }
191
333
  let text;
@@ -210,15 +352,16 @@ export function createPeerSessionDrain(opts) {
210
352
  disclose("notice_user_only", `${judged.cause}: ${text}`, m);
211
353
  }
212
354
  else {
213
- opts.inject(buildNoticePayload(m, kind, text), { priority: "next" });
355
+ currentRound.batch.push({ payload: buildNoticePayload(m, kind, text), priority: "next", urgent: false, seq: m.seq, attempt: 1 });
356
+ return;
214
357
  }
215
- settledUpTo = m.seq;
358
+ currentRound.dispose(m.seq, judged.verdict === "refuse" ? "refused" : "notice_user_only");
216
359
  };
217
360
  const settleSubscription = (facts, m, meta) => {
218
361
  const requester = meta.fromSession;
219
362
  if (requester === undefined) {
220
363
  disclose("notice_unrouted", "kind=idle_subscription without a reply address (fromSession)", m);
221
- settledUpTo = m.seq;
364
+ currentRound.dispose(m.seq, "subscription_unaddressed");
222
365
  return;
223
366
  }
224
367
  const sub = { requester, ...(m.from !== undefined ? { requesterName: m.from } : {}), ...(meta.fromMode !== undefined ? { fromMode: meta.fromMode } : {}), selfSent: requester.toLowerCase() === sessionId.toLowerCase(), requestedAt: m.sentAt };
@@ -230,17 +373,67 @@ export function createPeerSessionDrain(opts) {
230
373
  if (facts.setting.value === "refuse") {
231
374
  void routePeerIdleNotice(route(), requester, { kind: "unavailable" });
232
375
  announce("refused", idleMachine.target.size());
233
- settledUpTo = m.seq;
376
+ currentRound.dispose(m.seq, "subscription_refused");
234
377
  return;
235
378
  }
236
379
  const outcome = idleMachine.target.subscribe(sub);
237
380
  if (outcome === "full")
238
381
  void routePeerIdleNotice(route(), requester, { kind: "unavailable" });
239
382
  announce(outcome, idleMachine.target.size());
240
- settledUpTo = m.seq;
383
+ currentRound.dispose(m.seq, `subscription_${outcome}`);
384
+ };
385
+ const countWaiting = (lease) => {
386
+ if (lease === null)
387
+ return { pending: 0, urgent: 0 };
388
+ const waiting = lease.messages.filter((m) => {
389
+ if (ackedUpTo !== undefined && m.seq <= ackedUpTo)
390
+ return false;
391
+ const d = ledger.get(m.seq);
392
+ return d === undefined || d.kind === "invalidated";
393
+ });
394
+ return { pending: waiting.length, urgent: waiting.filter((m) => m.peerMeta?.urgent === true).length };
241
395
  };
242
396
  let warmedUp = opts.warmup === undefined;
243
- const drainOnce = async () => {
397
+ class DrainRound {
398
+ limit;
399
+ out;
400
+ carrier;
401
+ charBudget;
402
+ injected = 0;
403
+ resupplied = 0;
404
+ dispose(seq, why) {
405
+ ledger.set(seq, { kind: "terminal", why });
406
+ }
407
+ batch = [];
408
+ chars = 0;
409
+ constructor(limit, out, carrier, charBudget) {
410
+ this.limit = limit;
411
+ this.out = out;
412
+ this.carrier = carrier;
413
+ this.charBudget = charBudget;
414
+ }
415
+ get spent() {
416
+ return this.injected + this.batch.length >= this.limit;
417
+ }
418
+ deliver(payload, priority) {
419
+ if (this.spent)
420
+ return "deferred";
421
+ if (this.out !== undefined) {
422
+ if (this.charBudget !== undefined) {
423
+ const size = renderTaskNotificationXml(payload).length;
424
+ if (this.out.length > 0 && this.chars + size > this.charBudget)
425
+ return "deferred";
426
+ this.chars += size;
427
+ }
428
+ this.out.push(payload);
429
+ }
430
+ else
431
+ opts.inject(payload, { priority });
432
+ this.injected += 1;
433
+ return undefined;
434
+ }
435
+ }
436
+ const drainRound = async (round, pass) => {
244
437
  if (!warmedUp) {
245
438
  warmedUp = true;
246
439
  try {
@@ -255,91 +448,185 @@ export function createPeerSessionDrain(opts) {
255
448
  }
256
449
  catch (e) {
257
450
  opts.onError?.(e, { phase: "degraded", sessionId, classification: "peer-box-claim" });
258
- return;
451
+ return { pending: 0, urgent: 0 };
259
452
  }
260
- const previouslySettled = settledUpTo;
261
- await ackSettled();
453
+ if (pass.settle)
454
+ settleLocally();
455
+ if (pass.settle && lease !== null)
456
+ await ackSettledPrefix(lease);
457
+ if (!pass.deliver)
458
+ return countWaiting(lease);
262
459
  let facts;
263
460
  let admissionConfig;
264
461
  try {
265
462
  facts = readFacts();
266
- admissionConfig = resolvePeerAdmissionConfig(opts.admission, opts.onNotice);
463
+ admissionConfig = resolvePeerLaneConfig(opts.lane, opts.onNotice);
267
464
  }
268
465
  catch (e) {
269
466
  opts.onError?.(e, { phase: "degraded", sessionId, classification: "peer-box-settings" });
270
- return;
467
+ return { pending: 0, urgent: 0 };
271
468
  }
272
- bindHeld(true);
273
- idleMachine.target.sweep();
274
- held.rejudge();
275
- if (lease === null)
276
- return;
277
- for (const m of lease.messages) {
278
- if (previouslySettled !== undefined && m.seq <= previouslySettled)
279
- continue;
280
- const meta = m.peerMeta;
281
- if (m.hopChain !== undefined) {
282
- const verdict = judgePeerAdmission(scope, recipientKey, { senderKey: meta?.senderKey, body: m.content, prospectiveChain: m.hopChain, ownTokens: opts.ownTokens(), seq: m.seq }, admissionConfig, undefined, "drain");
283
- if (!verdict.ok) {
284
- if (verdict.reason === "rate_limited") {
285
- if (!disclosedHeld.has(m.seq)) {
286
- disclosedHeld.add(m.seq);
287
- 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");
288
- }
469
+ currentRound = round;
470
+ try {
471
+ idleMachine.target.sweep();
472
+ if (lease === null)
473
+ return { pending: 0, urgent: 0 };
474
+ bindHeld(true);
475
+ reconcileHeld(lease);
476
+ held.rejudge();
477
+ try {
478
+ for (const m of lease.messages) {
479
+ const prior = ledger.get(m.seq);
480
+ if (prior === undefined || prior.kind !== "invalidated")
481
+ continue;
482
+ if (round.spent)
483
+ break;
484
+ const attempt = prior.attempt + 1;
485
+ round.batch.push({ payload: { ...prior.payload, attempt, resupplied: true }, priority: "next", urgent: false, seq: m.seq, attempt });
486
+ }
487
+ adoptHeldDecisions();
488
+ for (const m of lease.messages) {
489
+ if (ackedUpTo !== undefined && m.seq <= ackedUpTo)
490
+ continue;
491
+ if (ledger.has(m.seq))
492
+ continue;
493
+ if (round.spent)
289
494
  break;
495
+ const meta = m.peerMeta;
496
+ if (m.hopChain !== undefined) {
497
+ const verdict = judgePeerAdmission(scope, recipientKey, { senderKey: meta?.senderKey, body: m.content, prospectiveChain: m.hopChain, ownTokens: opts.ownTokens(), seq: m.seq }, admissionConfig, undefined, "drain");
498
+ if (!verdict.ok) {
499
+ if (verdict.reason === "rate_limited") {
500
+ if (!disclosedHeld.has(m.seq)) {
501
+ disclosedHeld.add(m.seq);
502
+ 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");
503
+ }
504
+ break;
505
+ }
506
+ disclose("admission_refused", verdict.reason, m);
507
+ round.dispose(m.seq, `admission_refused:${verdict.reason}`);
508
+ continue;
509
+ }
290
510
  }
291
- disclose("admission_refused", verdict.reason, m);
292
- settledUpTo = m.seq;
293
- continue;
511
+ if (meta?.kind === "idle_subscription") {
512
+ settleSubscription(facts, m, meta);
513
+ continue;
514
+ }
515
+ if (meta?.kind === "idle_notice" || meta?.kind === "delivery_notice") {
516
+ settleNoticeRecord(facts, m, meta);
517
+ continue;
518
+ }
519
+ if (meta?.kind === "engine_frame") {
520
+ const decoded = decodeEngineFrame(m.content);
521
+ if (!decoded.ok && decoded.reason === "newer") {
522
+ 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" });
523
+ break;
524
+ }
525
+ if (!decoded.ok) {
526
+ disclose("notice_unrouted", `kind=engine_frame (${decoded.detail})`, m);
527
+ round.dispose(m.seq, `engine_frame_malformed:${decoded.detail}`);
528
+ continue;
529
+ }
530
+ round.batch.push({ payload: decoded.payload, priority: decoded.priority, urgent: false, seq: m.seq, attempt: 1 });
531
+ continue;
532
+ }
533
+ const sameTree = meta?.fromRoot !== undefined && meta.fromRoot.toLowerCase() === sessionId.toLowerCase();
534
+ if (!sameTree) {
535
+ const judged = judgeMeta(facts, meta);
536
+ if (judged.verdict === "refuse") {
537
+ disclose("refused", judged.cause, m);
538
+ round.dispose(m.seq, `refused:${judged.cause}`);
539
+ continue;
540
+ }
541
+ if (judged.verdict === "hold") {
542
+ 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 });
543
+ if (!ledger.has(m.seq))
544
+ ledger.set(m.seq, { kind: "held" });
545
+ continue;
546
+ }
547
+ }
548
+ let payload;
549
+ try {
550
+ payload = buildPeerMessagePayload(m);
551
+ }
552
+ catch (e) {
553
+ disclose("refused", `record fields cannot be rendered canonically (${e instanceof CrossSessionCodecError ? e.code : thrownText(e)})`, m);
554
+ round.dispose(m.seq, "uncanonical_fields");
555
+ continue;
556
+ }
557
+ round.batch.push({ payload, priority: "next", urgent: meta?.urgent === true, seq: m.seq, attempt: 1 });
294
558
  }
295
559
  }
296
- if (meta?.kind === "idle_subscription") {
297
- settleSubscription(facts, m, meta);
298
- continue;
299
- }
300
- if (meta?.kind === "idle_notice" || meta?.kind === "delivery_notice") {
301
- settleNoticeRecord(facts, m, meta);
302
- continue;
560
+ finally {
561
+ const ordered = round.batch.splice(0).sort((a, b) => Number(b.urgent) - Number(a.urgent));
562
+ for (const item of ordered) {
563
+ try {
564
+ if (round.deliver(item.payload, item.priority) === "deferred")
565
+ continue;
566
+ }
567
+ catch (e) {
568
+ opts.onError?.(e, { phase: "degraded", sessionId, classification: "peer-box-inject" });
569
+ break;
570
+ }
571
+ noteDelivered(item.seq, item.attempt, item.payload, round.carrier);
572
+ if (item.attempt > 1)
573
+ round.resupplied += 1;
574
+ }
303
575
  }
304
- const sameTree = meta?.fromRoot !== undefined && meta.fromRoot.toLowerCase() === sessionId.toLowerCase();
305
- if (!sameTree) {
306
- const judged = judgeMeta(facts, meta);
307
- if (judged.verdict === "refuse") {
308
- disclose("refused", judged.cause, m);
309
- settledUpTo = m.seq;
310
- continue;
576
+ const counts = countWaiting(lease);
577
+ if (counts.pending > 0 && round.out === undefined) {
578
+ noticeRound += 1;
579
+ try {
580
+ opts.inject({
581
+ task_id: `inbox:${boxHandle}`,
582
+ task_type: "background_agent",
583
+ status: "event",
584
+ seq: noticeRound,
585
+ summary: `inbox notice: ${counts.pending} cross-session message(s) waiting`,
586
+ inboxNotice: { pending: counts.pending, delivered: round.injected - round.resupplied, ...(round.resupplied > 0 ? { resupplied: round.resupplied } : {}), ...(counts.urgent > 0 ? { urgent: counts.urgent } : {}), box: boxHandle },
587
+ _sema_provenance: { kind: "inbox_notice", from: boxHandle, taskId: `inbox:${boxHandle}`, seq: noticeRound },
588
+ }, { priority: "next" });
311
589
  }
312
- if (judged.verdict === "hold") {
313
- 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 });
314
- settledUpTo = m.seq;
315
- continue;
590
+ catch (e) {
591
+ opts.onError?.(e, { phase: "degraded", sessionId, classification: "peer-box-inject" });
316
592
  }
317
593
  }
318
- let payload;
319
- try {
320
- payload = buildPeerMessagePayload(m);
321
- }
322
- catch (e) {
323
- disclose("refused", `record fields cannot be rendered canonically (${e instanceof CrossSessionCodecError ? e.code : thrownText(e)})`, m);
324
- settledUpTo = m.seq;
325
- continue;
326
- }
327
- try {
328
- opts.inject(payload, { priority: "next" });
329
- }
330
- catch (e) {
331
- opts.onError?.(e, { phase: "degraded", sessionId, classification: "peer-box-inject" });
332
- break;
333
- }
334
- settledUpTo = m.seq;
594
+ return counts;
595
+ }
596
+ finally {
597
+ currentRound = undefined;
335
598
  }
336
599
  };
337
- const drain = () => {
338
- inFlight = inFlight.then(drainOnce, drainOnce);
600
+ const boundPass = (pass) => async () => {
601
+ await drainRound(new DrainRound(resolvePeerLaneConfig(opts.lane, opts.onNotice).maxMessagesPerRound, undefined), pass);
602
+ };
603
+ const serialize = (once) => {
604
+ inFlight = inFlight.then(once, once);
339
605
  return inFlight;
340
606
  };
607
+ const drain = () => serialize(boundPass({ settle: true, deliver: true }));
608
+ const boundaryRound = (o) => serialize(boundPass({ settle: true, deliver: o.continues }));
609
+ const pull = (limit, toolCallId, charBudget) => {
610
+ const out = [];
611
+ const bound = limit !== undefined && Number.isInteger(limit) && limit > 0 ? limit : resolvePeerLaneConfig(opts.lane, opts.onNotice).maxMessagesPerRound;
612
+ let counts = { pending: 0, urgent: 0 };
613
+ const once = async () => {
614
+ counts = await drainRound(new DrainRound(bound, out, toolCallId !== undefined ? toolResultReceiptKey(toolCallId) : undefined, charBudget), { settle: false, deliver: true });
615
+ };
616
+ const settled = inFlight.then(once, once);
617
+ inFlight = settled;
618
+ return settled.then(() => ({ delivered: out, pending: counts.pending, urgent: counts.urgent }));
619
+ };
341
620
  const finishOnce = async () => {
342
- await ackSettled();
621
+ try {
622
+ const lease = await mailbox.claimLease(scope, boxHandle, drainOwner, ttl);
623
+ settleLocally();
624
+ if (lease !== null)
625
+ await ackSettledPrefix(lease);
626
+ }
627
+ catch (e) {
628
+ opts.onError?.(e, { phase: "degraded", sessionId, classification: "peer-box-claim" });
629
+ }
343
630
  held.detachLeg();
344
631
  try {
345
632
  await mailbox.releaseLease(scope, boxHandle, drainOwner);
@@ -353,7 +640,20 @@ export function createPeerSessionDrain(opts) {
353
640
  };
354
641
  return {
355
642
  drain,
643
+ boundaryRound,
644
+ pull,
645
+ isBoxBacked: (payload) => {
646
+ const p = payload;
647
+ if (p === null || typeof p !== "object" || typeof p.task_id !== "string" || typeof p.status !== "string")
648
+ return false;
649
+ const key = taskNotificationDedupKey(p);
650
+ for (const d of ledger.values())
651
+ if (d.kind === "delivered" && d.key === key)
652
+ return true;
653
+ return false;
654
+ },
356
655
  finish,
656
+ wantsCompactionEffects,
357
657
  busy: () => idleMachine.target.busy(),
358
658
  idle: (finishedAt) => idleMachine.target.idle(finishedAt),
359
659
  held,
@@ -396,7 +696,8 @@ export function mountListAgents(args) {
396
696
  args.toolEffects.set(LIST_AGENTS_TOOL_NAME, "read");
397
697
  args.toolEffects.set(LIST_AGENTS_TOOL_ALIAS, "read");
398
698
  return createListAgentsTool({
399
- peerDirectory: args.peerDirectory,
699
+ ...(args.peerDirectory !== undefined ? { peerDirectory: args.peerDirectory } : {}),
700
+ ...(args.freshWindowMs !== undefined ? { freshWindowMs: args.freshWindowMs } : {}),
400
701
  self: { sessionId: args.sessionId, scope: args.scope },
401
702
  registry: args.registry,
402
703
  access: { owner: args.hostTaskId, scope: args.scope, sessionId: args.sessionId },
@@ -414,7 +715,6 @@ export function bindPeerSessionDrain(harness, opts) {
414
715
  drain.busy();
415
716
  return round();
416
717
  });
417
- harness.on("turn_boundary", round);
418
718
  harness.subscribe((event) => {
419
719
  if (event.type === "agent_end")
420
720
  return drain.finish().catch(() => undefined);
@@ -448,22 +748,29 @@ export function bindPeerLaneDrain(harness, args) {
448
748
  rowName = undefined;
449
749
  }
450
750
  };
451
- return bindPeerSessionDrain(harness, {
751
+ const drain = bindPeerSessionDrain(harness, {
452
752
  mailbox: deps.mailboxStore,
453
753
  sessionId: args.sessionId,
454
754
  runId: args.runId,
455
755
  scope: args.scope,
456
756
  inject: args.inject,
457
- ...(deps.peerAdmission !== undefined ? { admission: deps.peerAdmission } : {}),
757
+ ...(deps.peerLane !== undefined ? { lane: deps.peerLane } : {}),
458
758
  ownTokens: args.ownTokens,
459
759
  settingLayers: () => (typeof deps.crossSessionInbound === "function" ? deps.crossSessionInbound() : deps.crossSessionInbound),
460
760
  selfModeClass: () => (args.refs.askEffective !== undefined ? foldPermissionModeClass(args.refs.askEffective) : "unknown"),
461
761
  dialogExpiry: () => (typeof deps.crossSessionDialogExpiry === "function" ? deps.crossSessionDialogExpiry() : deps.crossSessionDialogExpiry),
462
762
  selfName: () => rowName ?? args.selfName?.(),
463
763
  warmup: lookupRowName,
764
+ ...(args.settlement !== undefined ? { settlement: args.settlement } : {}),
464
765
  ...(args.parked !== undefined ? { parked: args.parked } : {}),
465
766
  ...(args.lastTurnText !== undefined ? { lastTurnText: args.lastTurnText } : {}),
466
767
  ...(deps.onNotice !== undefined ? { onNotice: deps.onNotice } : {}),
467
768
  ...(deps.onError !== undefined ? { onError: deps.onError } : {}),
468
769
  });
770
+ args.refs.pull = (limit, toolCallId, charBudget) => drain.pull(limit, toolCallId, charBudget);
771
+ args.refs.boundaryRound = (o) => drain.boundaryRound(o);
772
+ args.refs.isBoxBacked = (payload) => drain.isBoxBacked(payload);
773
+ if (args.settlement !== undefined)
774
+ args.settlement.wantsEffects = () => drain.wantsCompactionEffects();
775
+ return drain;
469
776
  }