@sema-agent/core 7.3.1 → 7.4.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 (56) hide show
  1. package/CHANGELOG.md +35 -0
  2. package/dist/agents/peer-admission.d.ts +18 -3
  3. package/dist/agents/peer-admission.js +79 -4
  4. package/dist/agents/peer-held-queue.d.ts +101 -0
  5. package/dist/agents/peer-held-queue.js +229 -0
  6. package/dist/agents/peer-idle.d.ts +109 -0
  7. package/dist/agents/peer-idle.js +240 -0
  8. package/dist/agents/peer-notice-route.d.ts +33 -0
  9. package/dist/agents/peer-notice-route.js +46 -0
  10. package/dist/agents/peer-notices.d.ts +103 -0
  11. package/dist/agents/peer-notices.js +206 -0
  12. package/dist/agents/peer-session-drain.d.ts +39 -4
  13. package/dist/agents/peer-session-drain.js +248 -42
  14. package/dist/agents/send-message-tool.d.ts +8 -1
  15. package/dist/agents/send-message-tool.js +96 -30
  16. package/dist/agents/subagent.js +1 -0
  17. package/dist/core/auto-mode-defaults.d.ts +11 -0
  18. package/dist/core/auto-mode-defaults.js +2 -0
  19. package/dist/core/auto-mode.d.ts +59 -0
  20. package/dist/core/auto-mode.js +57 -1
  21. package/dist/core/checkpoint-store.js +2 -2
  22. package/dist/core/governance-codes.d.ts +1 -1
  23. package/dist/core/governance-codes.js +8 -0
  24. package/dist/core/hooks.d.ts +30 -0
  25. package/dist/core/hooks.js +43 -8
  26. package/dist/core/mailbox-store.d.ts +33 -1
  27. package/dist/core/mailbox-store.js +42 -2
  28. package/dist/core/runner/assemble-result.d.ts +5 -0
  29. package/dist/core/runner/assemble-result.js +1 -1
  30. package/dist/core/runner/denial-limit-arms.d.ts +149 -0
  31. package/dist/core/runner/denial-limit-arms.js +91 -0
  32. package/dist/core/runner/edited-files-ledger.d.ts +33 -0
  33. package/dist/core/runner/edited-files-ledger.js +14 -0
  34. package/dist/core/runner/prepare-hands-readface.d.ts +5 -0
  35. package/dist/core/runner/prepare-hands-readface.js +1 -0
  36. package/dist/core/runner/prepare-task.d.ts +62 -1
  37. package/dist/core/runner/prepare-task.js +120 -89
  38. package/dist/core/runner/runtask.js +10 -0
  39. package/dist/core/sensitive-path-policy.d.ts +27 -6
  40. package/dist/core/sensitive-path-policy.js +57 -2
  41. package/dist/core/task-notification.d.ts +24 -2
  42. package/dist/core/task-notification.js +6 -1
  43. package/dist/core/tool-policy.d.ts +55 -4
  44. package/dist/core/tool-policy.js +28 -5
  45. package/dist/core/types.d.ts +207 -10
  46. package/dist/index.d.ts +10 -5
  47. package/dist/index.js +8 -3
  48. package/dist/orchestration/workflow.js +7 -3
  49. package/dist/tools/fs/fs-write.d.ts +4 -4
  50. package/dist/tools/fs/fs-write.js +30 -11
  51. package/dist/tools/fs/index.d.ts +7 -1
  52. package/dist/tools/fs/index.js +1 -1
  53. package/dist/tools/fs/safety.d.ts +29 -8
  54. package/dist/tools/fs/safety.js +11 -1
  55. package/package.json +1 -1
  56. package/test/export-surface.snapshot.json +169 -1
@@ -8,6 +8,8 @@ import { type PeerDirectory } from "./peer-directory.js";
8
8
  import { createListAgentsTool } from "./list-agents-tool.js";
9
9
  import type { AskEffective } from "../core/wiring-manifest.js";
10
10
  import type { SendMessageToolOptions } from "./send-message-tool.js";
11
+ import { type PeerClock, type PeerHeldQueue } from "./peer-held-queue.js";
12
+ import { type PeerIdleMachine } from "./peer-idle.js";
11
13
  /** design/385 §1.2④ — the drain lease TTL. Same sizing law as the tier-3 revive lease: well under the
12
14
  * store's stale-row floor, long enough to cover one turn (a longer turn renews on the same owner at
13
15
  * its next boundary; an expired lease re-serves the batch — at-least-once). */
@@ -34,6 +36,8 @@ export declare function judgePeerLaneMount(deps: {
34
36
  peerDirectory?: PeerDirectory;
35
37
  mailboxStore?: MailboxStore;
36
38
  }, sessionId?: string): PeerLaneMountVerdict | undefined;
39
+ /** The disposition arms of a `peer.inbound_disposition` notice (closed set; `detail.disposition`). */
40
+ export type PeerInboundDisposition = "refused" | "held" | "admission_refused" | "notice_unrouted" | "notice_user_only";
37
41
  export interface PeerSessionDrainOptions {
38
42
  mailbox: MailboxStore;
39
43
  /** This session (the box owner) and this leg's cycle id (the lease owner). */
@@ -53,6 +57,17 @@ export interface PeerSessionDrainOptions {
53
57
  settingLayers: () => CrossSessionInboundSettingLayers | undefined;
54
58
  /** The recipient's own permission-mode class, read at every drain (the manifest's ask fold). */
55
59
  selfModeClass: () => PermissionModeClass | "unknown";
60
+ /** This session's display name (its directory row's name), for receipts and idle notices it authors. */
61
+ selfName?: () => string | undefined;
62
+ /** Awaited ONCE before the first round (a directory lookup that fills `selfName`, say); a rejection is swallowed. */
63
+ warmup?: () => Promise<unknown>;
64
+ /** The deployment's `dialogExpiry` (raw; resolved at bind — an out-of-vocabulary value is announced
65
+ * once through `onError` and the default applies). Absent ⇒ the default (5 m). */
66
+ dialogExpiry?: () => unknown;
67
+ /** The last turn's text, for the idle notice's one-line detail (CC `getLastTurnText`). */
68
+ lastTurnText?: () => string | undefined;
69
+ /** Whether the run is parked on a pending ask / review at its end (a parked run is `blocked`, not idle). */
70
+ parked?: () => boolean;
56
71
  onNotice?: (notice: EngineNotice) => void;
57
72
  onError?: (error: unknown, ctx: {
58
73
  phase: "degraded";
@@ -60,12 +75,22 @@ export interface PeerSessionDrainOptions {
60
75
  classification: string;
61
76
  }) => void;
62
77
  leaseTtlMs?: number;
78
+ /** Tests: a virtual clock for the held deadlines and idle timers (honored when this process first sees the session). */
79
+ clock?: PeerClock;
63
80
  }
64
81
  export interface PeerSessionDrain {
65
82
  /** One drain round (serialized: a boundary never overlaps a still-running round). */
66
83
  drain(): Promise<void>;
67
84
  /** Run end: ack the last batch and release the lease so the session's next cycle claims at once. */
68
85
  finish(): Promise<void>;
86
+ /** The run began working (cancels a pending idle fire). */
87
+ busy(): void;
88
+ /** The run ended with nothing queued and no pending ask: the one-shot idle fire is scheduled (§5.2). */
89
+ idle(finishedAt: number): void;
90
+ /** This session's held queue (process-level; the host review face reads/settles through it). */
91
+ readonly held: PeerHeldQueue;
92
+ /** This session's idle machine (process-level). */
93
+ readonly idleMachine: PeerIdleMachine;
69
94
  }
70
95
  export declare function createPeerSessionDrain(opts: PeerSessionDrainOptions): PeerSessionDrain;
71
96
  /** The lane's late-bound facts: the ask fold resolves after the manifest derivation, the mount runs
@@ -134,26 +159,36 @@ export interface PeerDrainHarness {
134
159
  on(type: "turn_boundary" | "before_agent_start", handler: () => Promise<undefined>): unknown;
135
160
  subscribe(listener: (event: {
136
161
  type: string;
162
+ nextTurnCount?: number;
163
+ undrainedSteerCount?: number;
164
+ undrainedFollowUpCount?: number;
137
165
  }) => void | Promise<void>): unknown;
138
166
  }
139
167
  /**
140
168
  * Bind the drain to a leg: the run's own injector at the `next` tier, run open (the loop's first
141
169
  * steering drain follows the `before_agent_start` hook) + every turn boundary, and the run-end
142
- * ack/release on `agent_end`.
170
+ * ack/release on `agent_end`. Slice 4: the harness's `settled` account (the frame after `agent_end`)
171
+ * is the idle judgment's seat — "finished its turn with nothing queued" = no next-turn frames, no
172
+ * undrained user inputs, and (through `opts.parked`) no pending ask/review; the held queue's own
173
+ * emptiness is the machine's to check.
143
174
  */
144
175
  export declare function bindPeerSessionDrain(harness: PeerDrainHarness, opts: PeerSessionDrainOptions): PeerSessionDrain;
145
176
  /**
146
177
  * The leg-side spelling of {@link bindPeerSessionDrain}: the deployment seats the drain reads
147
- * (`mailboxStore` / `peerAdmission` / `crossSessionInbound` / the two sinks) come straight off deps, the
148
- * recipient's mode class folds off the lane refs — one binding call per leg, no per-seat plumbing.
178
+ * (`mailboxStore` / `peerAdmission` / `crossSessionInbound` / `crossSessionDialogExpiry` / the two
179
+ * sinks) come straight off deps, the recipient's mode class folds off the lane refs — one binding call
180
+ * per leg, no per-seat plumbing.
149
181
  */
150
182
  export declare function bindPeerLaneDrain(harness: PeerDrainHarness, args: {
151
- deps: Pick<RunnerDeps, "mailboxStore" | "peerAdmission" | "crossSessionInbound" | "onNotice" | "onError">;
183
+ deps: Pick<RunnerDeps, "mailboxStore" | "peerDirectory" | "peerAdmission" | "crossSessionInbound" | "crossSessionDialogExpiry" | "onNotice" | "onError">;
152
184
  sessionId: string;
153
185
  runId: string;
154
186
  scope: string;
155
187
  inject: PeerSessionDrainOptions["inject"];
156
188
  ownTokens: () => readonly string[];
157
189
  refs: PeerLaneRefs;
190
+ selfName?: () => string | undefined;
191
+ parked?: () => boolean;
192
+ lastTurnText?: () => string | undefined;
158
193
  }): PeerSessionDrain;
159
194
  export {};
@@ -7,6 +7,10 @@ import { isPeerSessionId, PEER_SESSION_ID_GRAMMAR, peerSessionBoxHandle } from "
7
7
  import { inlineUntrusted } from "../core/untrusted-text.js";
8
8
  import { createListAgentsTool, LIST_AGENTS_TOOL_ALIAS, LIST_AGENTS_TOOL_NAME } from "./list-agents-tool.js";
9
9
  import { foldPermissionModeClass } from "./cross-session-judge.js";
10
+ import { peerHeldQueueFor } from "./peer-held-queue.js";
11
+ import { peerIdleMachineFor } from "./peer-idle.js";
12
+ import { describePeerIdleSubscription, isPeerDeliveryReceiptState, isPeerIdleNoticeKind, peerIdleNoticeLabel, renderCrossSessionDeliveryNotice, renderCrossSessionDroppedNotice, renderCrossSessionIdleNotice, resolveCrossSessionDialogExpiry } from "./peer-notices.js";
13
+ import { routePeerDeliveryReceipt, routePeerDroppedReceipt, routePeerIdleNotice } from "./peer-notice-route.js";
10
14
  export const PEER_DRAIN_LEASE_TTL_MS = 5 * 60_000;
11
15
  export const PEER_SESSION_ID_UNGRAMMATICAL_CODE = "peer.session_id_ungrammatical";
12
16
  export function judgePeerLaneMount(deps, sessionId) {
@@ -30,17 +34,122 @@ export function judgePeerLaneMount(deps, sessionId) {
30
34
  export function createPeerSessionDrain(opts) {
31
35
  const { mailbox, sessionId, runId, scope } = opts;
32
36
  const boxHandle = peerSessionBoxHandle(sessionId);
37
+ const storedSessionAddress = (fromSession) => fromSession !== undefined && isPeerSessionId(fromSession) ? peerSessionBoxHandle(fromSession) : undefined;
33
38
  const drainOwner = `drain:${runId}`;
34
39
  const recipientKey = peerAxisToken(scope, "s", sessionId);
35
40
  const ttl = opts.leaseTtlMs ?? PEER_DRAIN_LEASE_TTL_MS;
36
41
  let settledUpTo;
37
42
  const disclosedHeld = new Set();
38
43
  let inFlight = Promise.resolve();
39
- const disclose = (disposition, cause, m, heldNote = "the held-message review face is not mounted in this build") => deliverEngineNotice(opts.onNotice, {
44
+ const held = peerHeldQueueFor(scope, sessionId, opts.clock !== undefined ? { clock: opts.clock } : {});
45
+ const idleMachine = peerIdleMachineFor(scope, sessionId, opts.clock !== undefined ? { clock: opts.clock } : {});
46
+ const disclose = (disposition, cause, m, heldNote) => deliverEngineNotice(opts.onNotice, {
40
47
  code: "peer.inbound_disposition",
41
- message: `cross-session message seq ${m.seq} in this session's box was ${disposition === "held" ? `HELD (it stays parked; ${heldNote})` : disposition === "refused" ? "refused by this session's crossSessionInbound setting" : disposition === "notice_unrouted" ? "settled WITHOUT delivery (a notice-kind record; the notice face is not mounted in this build)" : "refused by the inbound admission gate"}: ${cause}`,
48
+ message: `cross-session message seq ${m.seq} in this session's box was ${disposition === "held"
49
+ ? `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)"}`
50
+ : disposition === "refused"
51
+ ? "refused by this session's crossSessionInbound setting"
52
+ : disposition === "notice_unrouted"
53
+ ? "settled WITHOUT delivery (a notice-kind record the notice face cannot render)"
54
+ : disposition === "notice_user_only"
55
+ ? "surfaced to the user only (a notice whose sender this session's parity would hold; the model did not read it)"
56
+ : "refused by the inbound admission gate"}: ${cause}`,
42
57
  detail: { disposition, cause, seq: m.seq, box: boxHandle, ...(m.peerMeta?.fromSession !== undefined ? { fromSession: m.peerMeta.fromSession } : {}), sessionId, runId },
43
58
  });
59
+ const readFacts = () => ({
60
+ setting: resolveCrossSessionInboundSetting(opts.settingLayers() ?? {}),
61
+ selfModeClass: opts.selfModeClass(),
62
+ });
63
+ 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() } });
64
+ const route = () => ({
65
+ mailbox,
66
+ scope,
67
+ self: { sessionId, ...(opts.selfName?.() !== undefined ? { name: opts.selfName() } : {}), modeClass: opts.selfModeClass },
68
+ ...(opts.onError !== undefined ? { onError: opts.onError } : {}),
69
+ });
70
+ const buildPeerMessagePayload = (m) => {
71
+ const meta = m.peerMeta;
72
+ const fields = {
73
+ from: encodePeerAddress(storedSessionAddress(meta?.fromSession) ?? (m.from ?? "unknown")),
74
+ ...(meta?.fromSession !== undefined ? { fromSession: meta.fromSession } : {}),
75
+ ...(m.from !== undefined ? { fromName: m.from } : {}),
76
+ ...(meta?.fromMode !== undefined ? { fromMode: meta.fromMode } : {}),
77
+ ...(meta?.fromScope !== undefined ? { fromScope: meta.fromScope } : {}),
78
+ };
79
+ buildCrossSessionEnvelope(fields, m.content);
80
+ return {
81
+ task_id: boxHandle,
82
+ task_type: "background_agent",
83
+ status: "event",
84
+ summary: `cross-session message from ${m.from ?? fields.from}`,
85
+ result: m.content,
86
+ seq: m.seq,
87
+ peer: { hopChain: m.hopChain !== undefined ? [...m.hopChain] : [] },
88
+ crossSessionMessage: { ...fields, body: m.content },
89
+ _sema_provenance: { kind: "cross_session_message", from: fields.from, taskId: boxHandle, seq: m.seq, ...(meta !== undefined ? { peerMeta: { ...meta } } : {}) },
90
+ };
91
+ };
92
+ const buildNoticePayload = (m, kind, text) => ({
93
+ task_id: boxHandle,
94
+ task_type: "background_agent",
95
+ status: "event",
96
+ summary: kind === "delivery_notice" ? "cross-session delivery notice" : "cross-session idle notice",
97
+ result: text,
98
+ seq: m.seq,
99
+ crossSessionNotice: { kind, text },
100
+ _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 } } : {}) },
101
+ });
102
+ const receipt = (entry, state) => {
103
+ const to = entry.peerMeta?.fromSession;
104
+ if (to === undefined)
105
+ return;
106
+ if (state === "dropped")
107
+ void routePeerDroppedReceipt(route(), to, entry.seq);
108
+ else
109
+ void routePeerDeliveryReceipt(route(), to, state, entry.seq);
110
+ };
111
+ const heldSink = (live) => ({
112
+ judge: (entry) => judgeMeta(readFacts(), entry.peerMeta),
113
+ ...(live ? { inject: (entry) => opts.inject(buildPeerMessagePayload(entry), { priority: "next" }) } : {}),
114
+ receipt,
115
+ onHeld: (entry, count, cause, reannounce) => {
116
+ if (!reannounce && disclosedHeld.has(entry.seq))
117
+ return;
118
+ disclosedHeld.add(entry.seq);
119
+ disclose("held", cause, entry, reannounce ? `still held; its cause moved to ${cause} (${count} held)` : undefined);
120
+ },
121
+ onSettled: (entry, settlement, reason, count) => deliverEngineNotice(opts.onNotice, {
122
+ code: "peer.held_settled",
123
+ 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"}.`,
124
+ detail: { settlement, reason, seq: entry.seq, box: boxHandle, ...(entry.peerMeta?.fromSession !== undefined ? { fromSession: entry.peerMeta.fromSession } : {}), heldCount: count, sessionId, runId },
125
+ }),
126
+ });
127
+ let dialogExpiryMs = resolveCrossSessionDialogExpiry(undefined).ms;
128
+ let dialogExpiryAnnounced = false;
129
+ const bindHeld = (live) => {
130
+ try {
131
+ const resolved = resolveCrossSessionDialogExpiry(opts.dialogExpiry?.());
132
+ dialogExpiryMs = resolved.ms;
133
+ if (resolved.invalid && !dialogExpiryAnnounced) {
134
+ dialogExpiryAnnounced = true;
135
+ opts.onError?.(new Error(`crossSessionDialogExpiry holds a value outside its vocabulary (60s|5m|10m|never); the default ${resolved.value} is in force for held cross-session messages`), { phase: "degraded", sessionId, classification: "peer-dialog-expiry" });
136
+ }
137
+ }
138
+ catch (e) {
139
+ opts.onError?.(e, { phase: "degraded", sessionId, classification: "peer-dialog-expiry" });
140
+ }
141
+ held.configure({ sink: heldSink(live), dialogExpiryMs });
142
+ };
143
+ bindHeld(true);
144
+ idleMachine.target.configure({
145
+ notify: (sub, notice) => void routePeerIdleNotice(route(), sub.requester, notice),
146
+ judge: (sub) => judgeMeta(readFacts(), { ...(sub.fromMode !== undefined ? { fromMode: sub.fromMode } : {}), ...(sub.selfSent ? { fromSession: sessionId } : {}) }),
147
+ heldCount: () => held.size(),
148
+ ...(opts.lastTurnText !== undefined ? { lastTurnText: opts.lastTurnText } : {}),
149
+ });
150
+ idleMachine.requester.configure({
151
+ expired: (o) => void routePeerIdleNotice(route(), sessionId, { kind: "expired", label: o.label }),
152
+ });
44
153
  const ackSettled = async () => {
45
154
  if (settledUpTo === undefined)
46
155
  return;
@@ -53,7 +162,76 @@ export function createPeerSessionDrain(opts) {
53
162
  opts.onError?.(e, { phase: "degraded", sessionId, classification: "peer-box-ack" });
54
163
  }
55
164
  };
165
+ const settleNoticeRecord = (facts, m, meta) => {
166
+ const notice = meta.notice;
167
+ const kind = meta.kind;
168
+ const stateFits = notice !== undefined && (kind === "idle_notice" ? isPeerIdleNoticeKind(notice.state) : isPeerDeliveryReceiptState(notice.state));
169
+ if (!stateFits) {
170
+ disclose("notice_unrouted", `kind=${kind}${notice === undefined ? " without a typed notice" : ` with state=${inlineUntrusted(notice.state, 32)}`}`, m);
171
+ settledUpTo = m.seq;
172
+ return;
173
+ }
174
+ let text;
175
+ if (kind === "idle_notice") {
176
+ const idleKind = notice.state;
177
+ const from = meta.fromSession;
178
+ const selfAuthored = from !== undefined && from.toLowerCase() === sessionId.toLowerCase();
179
+ const settled = from !== undefined && !selfAuthored ? idleMachine.requester.settle(from, idleKind) : { cleared: 0 };
180
+ const label = (notice.target !== undefined ? peerIdleNoticeLabel(notice.target) : undefined) ?? settled.label ?? peerIdleNoticeLabel(m.from ?? storedSessionAddress(from) ?? "") ?? "(unnamed session)";
181
+ text = renderCrossSessionIdleNotice({ kind: idleKind, label, ...(notice.finishedAt !== undefined ? { finishedAt: notice.finishedAt } : {}), ...(notice.detail !== undefined ? { detail: peerIdleNoticeLabel(notice.detail) ?? "" } : {}) });
182
+ }
183
+ else {
184
+ const state = notice.state;
185
+ const recipients = [notice.recipient ?? m.from ?? ""];
186
+ text = state === "dropped" ? renderCrossSessionDroppedNotice(1, recipients, [notice.dropReason]) : renderCrossSessionDeliveryNotice(state, 1, recipients);
187
+ }
188
+ const judged = judgeMeta(facts, meta);
189
+ if (judged.verdict === "refuse") {
190
+ disclose("refused", `${judged.cause} (a ${kind})`, m);
191
+ }
192
+ else if (judged.verdict === "hold") {
193
+ disclose("notice_user_only", `${judged.cause}: ${text}`, m);
194
+ }
195
+ else {
196
+ opts.inject(buildNoticePayload(m, kind, text), { priority: "next" });
197
+ }
198
+ settledUpTo = m.seq;
199
+ };
200
+ const settleSubscription = (facts, m, meta) => {
201
+ const requester = meta.fromSession;
202
+ if (requester === undefined) {
203
+ disclose("notice_unrouted", "kind=idle_subscription without a reply address (fromSession)", m);
204
+ settledUpTo = m.seq;
205
+ return;
206
+ }
207
+ const sub = { requester, ...(m.from !== undefined ? { requesterName: m.from } : {}), ...(meta.fromMode !== undefined ? { fromMode: meta.fromMode } : {}), selfSent: requester.toLowerCase() === sessionId.toLowerCase(), requestedAt: m.sentAt };
208
+ const announce = (outcome, live) => deliverEngineNotice(opts.onNotice, {
209
+ code: "peer.idle_subscription",
210
+ message: outcome === "recorded" || outcome === "refreshed" ? `${describePeerIdleSubscription(encodePeerAddress(peerSessionBoxHandle(requester)))} (${outcome}; ${live} live)` : `A peer session at ${encodePeerAddress(peerSessionBoxHandle(requester))} asked to be told when this session is next idle and was answered "unavailable": ${outcome === "full" ? "this session's subscription table is full" : `this session's crossSessionInbound setting is "refuse"`}.`,
211
+ detail: { outcome, fromSession: requester, live, seq: m.seq, box: boxHandle, sessionId, runId },
212
+ });
213
+ if (facts.setting.value === "refuse") {
214
+ void routePeerIdleNotice(route(), requester, { kind: "unavailable" });
215
+ announce("refused", idleMachine.target.size());
216
+ settledUpTo = m.seq;
217
+ return;
218
+ }
219
+ const outcome = idleMachine.target.subscribe(sub);
220
+ if (outcome === "full")
221
+ void routePeerIdleNotice(route(), requester, { kind: "unavailable" });
222
+ announce(outcome, idleMachine.target.size());
223
+ settledUpTo = m.seq;
224
+ };
225
+ let warmedUp = opts.warmup === undefined;
56
226
  const drainOnce = async () => {
227
+ if (!warmedUp) {
228
+ warmedUp = true;
229
+ try {
230
+ await opts.warmup();
231
+ }
232
+ catch {
233
+ }
234
+ }
57
235
  let lease;
58
236
  try {
59
237
  lease = await mailbox.claimLease(scope, boxHandle, drainOwner, ttl);
@@ -64,20 +242,21 @@ export function createPeerSessionDrain(opts) {
64
242
  }
65
243
  const previouslySettled = settledUpTo;
66
244
  await ackSettled();
67
- if (lease === null)
68
- return;
69
- let setting;
245
+ let facts;
70
246
  let admissionConfig;
71
- let selfModeClass;
72
247
  try {
73
- setting = resolveCrossSessionInboundSetting(opts.settingLayers() ?? {});
74
- admissionConfig = resolvePeerAdmissionConfig(opts.admission);
75
- selfModeClass = opts.selfModeClass();
248
+ facts = readFacts();
249
+ admissionConfig = resolvePeerAdmissionConfig(opts.admission, opts.onNotice);
76
250
  }
77
251
  catch (e) {
78
252
  opts.onError?.(e, { phase: "degraded", sessionId, classification: "peer-box-settings" });
79
253
  return;
80
254
  }
255
+ bindHeld(true);
256
+ idleMachine.target.sweep();
257
+ held.rejudge();
258
+ if (lease === null)
259
+ return;
81
260
  for (const m of lease.messages) {
82
261
  if (previouslySettled !== undefined && m.seq <= previouslySettled)
83
262
  continue;
@@ -88,7 +267,7 @@ export function createPeerSessionDrain(opts) {
88
267
  if (verdict.reason === "rate_limited") {
89
268
  if (!disclosedHeld.has(m.seq)) {
90
269
  disclosedHeld.add(m.seq);
91
- disclose("held", verdict.reason, m, "the inbound admission bucket is empty; it is re-judged at a later boundary once the bucket refills");
270
+ 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");
92
271
  }
93
272
  break;
94
273
  }
@@ -97,53 +276,36 @@ export function createPeerSessionDrain(opts) {
97
276
  continue;
98
277
  }
99
278
  }
100
- if (meta?.kind !== undefined && meta.kind !== "peer_message") {
101
- disclose("notice_unrouted", `kind=${meta.kind}`, m);
102
- settledUpTo = m.seq;
279
+ if (meta?.kind === "idle_subscription") {
280
+ settleSubscription(facts, m, meta);
281
+ continue;
282
+ }
283
+ if (meta?.kind === "idle_notice" || meta?.kind === "delivery_notice") {
284
+ settleNoticeRecord(facts, m, meta);
103
285
  continue;
104
286
  }
105
287
  {
106
- const judged = judgeCrossSessionInbound({ setting, selfModeClass, sender: { ...(meta?.fromMode !== undefined ? { fromMode: meta.fromMode } : {}), selfSent: meta?.fromSession !== undefined && meta.fromSession.toLowerCase() === sessionId.toLowerCase() } });
288
+ const judged = judgeMeta(facts, meta);
107
289
  if (judged.verdict === "refuse") {
108
290
  disclose("refused", judged.cause, m);
109
291
  settledUpTo = m.seq;
110
292
  continue;
111
293
  }
112
294
  if (judged.verdict === "hold") {
113
- if (!disclosedHeld.has(m.seq)) {
114
- disclosedHeld.add(m.seq);
115
- disclose("held", `${judged.cause}: ${judged.message}${judged.warning !== undefined ? ` (${judged.warning})` : ""}`, m);
116
- }
117
- break;
295
+ 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 });
296
+ settledUpTo = m.seq;
297
+ continue;
118
298
  }
119
299
  }
120
- let fields;
300
+ let payload;
121
301
  try {
122
- fields = {
123
- from: encodePeerAddress(meta?.fromSession !== undefined ? peerSessionBoxHandle(meta.fromSession) : (m.from ?? "unknown")),
124
- ...(meta?.fromSession !== undefined ? { fromSession: meta.fromSession } : {}),
125
- ...(m.from !== undefined ? { fromName: m.from } : {}),
126
- ...(meta?.fromMode !== undefined ? { fromMode: meta.fromMode } : {}),
127
- ...(meta?.fromScope !== undefined ? { fromScope: meta.fromScope } : {}),
128
- };
129
- buildCrossSessionEnvelope(fields, m.content);
302
+ payload = buildPeerMessagePayload(m);
130
303
  }
131
304
  catch (e) {
132
305
  disclose("refused", `record fields cannot be rendered canonically (${e instanceof CrossSessionCodecError ? e.code : e instanceof Error ? e.message : String(e)})`, m);
133
306
  settledUpTo = m.seq;
134
307
  continue;
135
308
  }
136
- const payload = {
137
- task_id: boxHandle,
138
- task_type: "background_agent",
139
- status: "event",
140
- summary: `cross-session message from ${m.from ?? fields.from}`,
141
- result: m.content,
142
- seq: m.seq,
143
- peer: { hopChain: m.hopChain !== undefined ? [...m.hopChain] : [] },
144
- crossSessionMessage: { ...fields, body: m.content },
145
- _sema_provenance: { kind: "cross_session_message", from: fields.from, taskId: boxHandle, seq: m.seq, ...(meta !== undefined ? { peerMeta: { ...meta } } : {}) },
146
- };
147
309
  try {
148
310
  opts.inject(payload, { priority: "next" });
149
311
  }
@@ -160,6 +322,7 @@ export function createPeerSessionDrain(opts) {
160
322
  };
161
323
  const finishOnce = async () => {
162
324
  await ackSettled();
325
+ held.detachLeg();
163
326
  try {
164
327
  await mailbox.releaseLease(scope, boxHandle, drainOwner);
165
328
  }
@@ -170,7 +333,14 @@ export function createPeerSessionDrain(opts) {
170
333
  inFlight = inFlight.then(finishOnce, finishOnce);
171
334
  return inFlight;
172
335
  };
173
- return { drain, finish };
336
+ return {
337
+ drain,
338
+ finish,
339
+ busy: () => idleMachine.target.busy(),
340
+ idle: (finishedAt) => idleMachine.target.idle(finishedAt),
341
+ held,
342
+ idleMachine,
343
+ };
174
344
  }
175
345
  export function announcePeerLaneMount(deps, leg) {
176
346
  const verdict = judgePeerLaneMount(deps, leg.sessionId);
@@ -222,13 +392,44 @@ export function bindPeerSessionDrain(harness, opts) {
222
392
  await drain.drain();
223
393
  return undefined;
224
394
  };
225
- harness.on("before_agent_start", round);
395
+ harness.on("before_agent_start", async () => {
396
+ drain.busy();
397
+ return round();
398
+ });
226
399
  harness.on("turn_boundary", round);
227
- harness.subscribe((event) => (event.type === "agent_end" ? drain.finish().catch(() => undefined) : undefined));
400
+ harness.subscribe((event) => {
401
+ if (event.type === "agent_end")
402
+ return drain.finish().catch(() => undefined);
403
+ if (event.type === "settled") {
404
+ const nothingQueued = (event.nextTurnCount ?? 0) === 0 && event.undrainedSteerCount === undefined && event.undrainedFollowUpCount === undefined;
405
+ let parked = false;
406
+ try {
407
+ parked = opts.parked?.() ?? false;
408
+ }
409
+ catch {
410
+ parked = true;
411
+ }
412
+ if (nothingQueued && !parked)
413
+ drain.idle(Date.now());
414
+ }
415
+ return undefined;
416
+ });
228
417
  return drain;
229
418
  }
230
419
  export function bindPeerLaneDrain(harness, args) {
231
420
  const { deps } = args;
421
+ let rowName;
422
+ const lookupRowName = async () => {
423
+ if (deps.peerDirectory === undefined)
424
+ return;
425
+ try {
426
+ const rows = await deps.peerDirectory.listPeerSessions({ scope: args.scope });
427
+ rowName = rows.find((r) => r.sessionId.toLowerCase() === args.sessionId.toLowerCase())?.name;
428
+ }
429
+ catch {
430
+ rowName = undefined;
431
+ }
432
+ };
232
433
  return bindPeerSessionDrain(harness, {
233
434
  mailbox: deps.mailboxStore,
234
435
  sessionId: args.sessionId,
@@ -239,6 +440,11 @@ export function bindPeerLaneDrain(harness, args) {
239
440
  ownTokens: args.ownTokens,
240
441
  settingLayers: () => (typeof deps.crossSessionInbound === "function" ? deps.crossSessionInbound() : deps.crossSessionInbound),
241
442
  selfModeClass: () => (args.refs.askEffective !== undefined ? foldPermissionModeClass(args.refs.askEffective) : "unknown"),
443
+ dialogExpiry: () => (typeof deps.crossSessionDialogExpiry === "function" ? deps.crossSessionDialogExpiry() : deps.crossSessionDialogExpiry),
444
+ selfName: () => rowName ?? args.selfName?.(),
445
+ warmup: lookupRowName,
446
+ ...(args.parked !== undefined ? { parked: args.parked } : {}),
447
+ ...(args.lastTurnText !== undefined ? { lastTurnText: args.lastTurnText } : {}),
242
448
  ...(deps.onNotice !== undefined ? { onNotice: deps.onNotice } : {}),
243
449
  ...(deps.onError !== undefined ? { onError: deps.onError } : {}),
244
450
  });
@@ -137,10 +137,17 @@ export interface SendMessageToolOptions {
137
137
  */
138
138
  enrichCtx?: ToolCtxEnricher;
139
139
  /** design/176 — caller tuning for the always-on peer-message admission gate (per-field
140
- * range-validated; out-of-range values fall back to that field's default; there is no off
140
+ * range-validated; a value outside its range or one that is not a finite number — falls back to
141
+ * that field's default and is ANNOUNCED once per process through {@link onNotice}; there is no off
141
142
  * switch). The Runner mount threads {@link import("../core/types.js").RunnerDeps.peerAdmission}
142
143
  * here. Read per call — a changed value governs the next message. */
143
144
  admission?: Partial<PeerAdmissionConfig>;
145
+ /** #551 — the deployment's structured notice sink, used by this face for ONE fact: a
146
+ * {@link admission} field the resolver could not use falls back to its default and announces
147
+ * `config.peer_admission_out_of_range` through here. The Runner mount threads
148
+ * {@link import("../core/types.js").RunnerDeps.onNotice}; a direct mount that leaves it absent
149
+ * still gets the line on `console.warn` (the shared delivery throat's unwired arm). */
150
+ onNotice?: (notice: import("../core/types.js").EngineNotice) => void;
144
151
  /** design/176 — the SENDING run's peer-identity carrier (trusted internals chain; ctx twin
145
152
  * {@link import("../core/types.js").ToolExecuteContext.peerSelfRef} wins when present). Supplies
146
153
  * the sender's bucket key and the hop token appended to every outbound chain. A direct mount