@sema-agent/core 7.28.1 → 7.30.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 (162) hide show
  1. package/CHANGELOG.md +473 -1
  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/brain/anthropic.js +4 -2
  27. package/dist/brain/open-responses.js +4 -3
  28. package/dist/brain/openai.js +2 -1
  29. package/dist/brain/stream-engine.js +6 -0
  30. package/dist/brain/stream-shared.d.ts +8 -0
  31. package/dist/brain/stream-shared.js +8 -0
  32. package/dist/core/auto-compaction.d.ts +42 -0
  33. package/dist/core/auto-compaction.js +35 -2
  34. package/dist/core/child-clamp-seats.d.ts +11 -0
  35. package/dist/core/child-clamp-seats.js +3 -0
  36. package/dist/core/context-edit.d.ts +3 -2
  37. package/dist/core/context-edit.js +6 -5
  38. package/dist/core/context-guard.d.ts +2 -1
  39. package/dist/core/context-guard.js +3 -2
  40. package/dist/core/context-settings.d.ts +103 -0
  41. package/dist/core/context-settings.js +60 -0
  42. package/dist/core/engine-notice.d.ts +18 -1
  43. package/dist/core/frame-settlement.d.ts +97 -0
  44. package/dist/core/frame-settlement.js +36 -0
  45. package/dist/core/governance-codes.d.ts +1 -1
  46. package/dist/core/governance-codes.js +4 -0
  47. package/dist/core/hooks.d.ts +10 -10
  48. package/dist/core/mailbox-store.d.ts +41 -7
  49. package/dist/core/mailbox-store.js +9 -3
  50. package/dist/core/message-utils.d.ts +13 -0
  51. package/dist/core/message-utils.js +6 -0
  52. package/dist/core/parent-spec-seat.d.ts +14 -1
  53. package/dist/core/parent-spec-seat.js +6 -0
  54. package/dist/core/permission-rule-model.js +1 -1
  55. package/dist/core/roles.d.ts +20 -0
  56. package/dist/core/roles.js +35 -16
  57. package/dist/core/runner/assemble-result.d.ts +2 -0
  58. package/dist/core/runner/assemble-result.js +3 -1
  59. package/dist/core/runner/compaction-call-options.d.ts +26 -2
  60. package/dist/core/runner/compaction-call-options.js +9 -0
  61. package/dist/core/runner/compaction-seams.d.ts +4 -3
  62. package/dist/core/runner/compaction-seams.js +13 -6
  63. package/dist/core/runner/contracts.d.ts +61 -7
  64. package/dist/core/runner/decide-continuation.d.ts +2 -1
  65. package/dist/core/runner/decide-continuation.js +11 -10
  66. package/dist/core/runner/initial-run-state.js +2 -2
  67. package/dist/core/runner/prepare-caps-and-workflow.js +9 -1
  68. package/dist/core/runner/prepare-config-doors.js +4 -1
  69. package/dist/core/runner/prepare-context-lane.d.ts +9 -2
  70. package/dist/core/runner/prepare-context-lane.js +15 -10
  71. package/dist/core/runner/prepare-delegation-surface.d.ts +3 -2
  72. package/dist/core/runner/prepare-delegation-surface.js +9 -3
  73. package/dist/core/runner/prepare-file-history.js +10 -8
  74. package/dist/core/runner/prepare-gate-stations.d.ts +4 -0
  75. package/dist/core/runner/prepare-gate-stations.js +3 -1
  76. package/dist/core/runner/prepare-inherited-gate.js +1 -0
  77. package/dist/core/runner/prepare-prompt-inputs.d.ts +6 -0
  78. package/dist/core/runner/prepare-prompt-inputs.js +2 -1
  79. package/dist/core/runner/prepare-task.js +22 -9
  80. package/dist/core/runner/prepare-wiring-manifest.d.ts +6 -1
  81. package/dist/core/runner/prepare-wiring-manifest.js +6 -1
  82. package/dist/core/runner/response-model-seat.d.ts +16 -0
  83. package/dist/core/runner/response-model-seat.js +27 -0
  84. package/dist/core/runner/retired-runner-deps.d.ts +3 -1
  85. package/dist/core/runner/retired-runner-deps.js +1 -0
  86. package/dist/core/runner/run-compaction-machinery.d.ts +4 -2
  87. package/dist/core/runner/run-compaction-machinery.js +5 -3
  88. package/dist/core/runner/run-harness-handlers.js +9 -2
  89. package/dist/core/runner/run-identity-wiring.js +19 -1
  90. package/dist/core/runner/run-leg.js +1 -1
  91. package/dist/core/runner/run-notification-lane.js +8 -2
  92. package/dist/core/runner/run-recovery-lanes.js +9 -5
  93. package/dist/core/runner/run-settle-and-teardown.d.ts +4 -22
  94. package/dist/core/runner/run-settle-and-teardown.js +6 -10
  95. package/dist/core/runner/run-stop-and-final-verify.js +1 -0
  96. package/dist/core/runner/run-terminal-adoption.js +5 -5
  97. package/dist/core/runner/run-turn-boundary.js +32 -5
  98. package/dist/core/runner/runtask.d.ts +8 -1
  99. package/dist/core/runner/runtask.js +33 -2
  100. package/dist/core/runner-deps.d.ts +44 -13
  101. package/dist/core/side-query.d.ts +6 -5
  102. package/dist/core/side-query.js +3 -1
  103. package/dist/core/swappable-deps.d.ts +20 -4
  104. package/dist/core/swappable-deps.js +6 -1
  105. package/dist/core/task-event.d.ts +13 -0
  106. package/dist/core/task-notification.d.ts +144 -2
  107. package/dist/core/task-notification.js +154 -5
  108. package/dist/core/task-result.d.ts +27 -3
  109. package/dist/core/task-spec.d.ts +7 -0
  110. package/dist/core/tool-catalog-entries.js +2 -1
  111. package/dist/core/tool-conformance.js +1 -0
  112. package/dist/core/tool-policy.d.ts +9 -4
  113. package/dist/core/tool-spec.d.ts +14 -0
  114. package/dist/core/wiring-manifest.d.ts +12 -0
  115. package/dist/core/wiring-manifest.js +5 -1
  116. package/dist/engine/harness/agent-harness.d.ts +14 -1
  117. package/dist/engine/harness/agent-harness.js +28 -3
  118. package/dist/engine/harness/types.d.ts +3 -1
  119. package/dist/engine/llm/types.d.ts +8 -7
  120. package/dist/engine/llm/types.js +1 -1
  121. package/dist/engine/session/import-validate.js +4 -0
  122. package/dist/index.d.ts +10 -7
  123. package/dist/index.js +7 -5
  124. package/dist/orchestration/run-workflow-tool.d.ts +4 -4
  125. package/dist/orchestration/workflow.d.ts +1 -1
  126. package/dist/orchestration/workflow.js +8 -5
  127. package/dist/shell/lex.d.ts +30 -51
  128. package/dist/shell/lex.js +237 -284
  129. package/dist/stores/cc/mailbox-store.js +3 -2
  130. package/dist/stores/file/corrupt-read-sink.d.ts +28 -2
  131. package/dist/stores/file/file-history-store.d.ts +2 -4
  132. package/dist/stores/file/file-history-store.js +1 -1
  133. package/dist/stores/file/fs-atomic.d.ts +16 -11
  134. package/dist/stores/file/fs-atomic.js +17 -6
  135. package/dist/stores/file/index.d.ts +5 -6
  136. package/dist/stores/file/index.js +1 -1
  137. package/dist/stores/file/mailbox-store.d.ts +4 -9
  138. package/dist/stores/file/mailbox-store.js +29 -30
  139. package/dist/stores/file/session-policy-store.d.ts +2 -0
  140. package/dist/stores/file/session-policy-store.js +1 -1
  141. package/dist/stores/file/session-store.d.ts +11 -7
  142. package/dist/stores/file/session-store.js +23 -16
  143. package/dist/stores/file/shared-ledger.js +3 -14
  144. package/dist/stores/file/tool-result-store.d.ts +2 -0
  145. package/dist/stores/file/tool-result-store.js +5 -2
  146. package/dist/stores/file/workflow-journal-store.js +14 -9
  147. package/dist/tools/fs/bash-program-position.d.ts +15 -24
  148. package/dist/tools/fs/bash-program-position.js +11 -29
  149. package/dist/tools/fs/bash-readonly-classifier.d.ts +125 -117
  150. package/dist/tools/fs/bash-readonly-classifier.js +400 -272
  151. package/dist/tools/fs/shell-read-check.js +3 -3
  152. package/dist/tools/fs/shell-read-establish.js +1 -1
  153. package/dist/tools/fs/shell-read-plan.d.ts +11 -6
  154. package/dist/tools/fs/shell-read-plan.js +167 -183
  155. package/dist/tools/fs/shell-read-word.d.ts +50 -0
  156. package/dist/tools/fs/shell-read-word.js +59 -0
  157. package/dist/tools/fs/shell-reentry-text.d.ts +10 -0
  158. package/dist/tools/fs/shell-reentry-text.js +73 -0
  159. package/package.json +1 -1
  160. package/test/export-surface.snapshot.json +58 -8
  161. package/wire-changes/core@7.29.0.json +185 -0
  162. package/wire-changes/core@7.30.0.json +1821 -0
@@ -1,6 +1,6 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import { deliverEngineNotice } from "../core/types.js";
3
- export const PEER_ADMISSION_DEFAULTS = Object.freeze({
3
+ export const PEER_LANE_DEFAULTS = Object.freeze({
4
4
  bucketCapacity: 30,
5
5
  refillPerSecond: 0.5,
6
6
  dedupWindowMs: 30_000,
@@ -9,6 +9,9 @@ export const PEER_ADMISSION_DEFAULTS = Object.freeze({
9
9
  maxTrackedSenders: 256,
10
10
  maxQueuedPeerMessages: 50,
11
11
  maxTrackedRecipients: 256,
12
+ maxWakesPerHour: 6,
13
+ maxMessagesPerRound: 5,
14
+ listingFreshWindowMs: 24 * 60 * 60_000,
12
15
  });
13
16
  const CONFIG_RANGES = Object.freeze({
14
17
  bucketCapacity: [5, 500],
@@ -19,10 +22,14 @@ const CONFIG_RANGES = Object.freeze({
19
22
  maxTrackedSenders: [16, 100_000],
20
23
  maxQueuedPeerMessages: [10, 5_000],
21
24
  maxTrackedRecipients: [16, 100_000],
25
+ maxWakesPerHour: [0, 600],
26
+ maxMessagesPerRound: [1, 500],
27
+ listingFreshWindowMs: [60_000, 30 * 24 * 60 * 60_000],
22
28
  });
29
+ const INTEGER_FIELDS = new Set(["maxWakesPerHour", "maxMessagesPerRound"]);
23
30
  let announcedOutOfRangeBySink = new WeakMap();
24
31
  const announcedOutOfRangeConsole = new Set();
25
- function peerAdmissionRangeLedger(onNotice) {
32
+ function peerLaneRangeLedger(onNotice) {
26
33
  if (typeof onNotice !== "function")
27
34
  return announcedOutOfRangeConsole;
28
35
  let lines = announcedOutOfRangeBySink.get(onNotice);
@@ -32,12 +39,12 @@ function peerAdmissionRangeLedger(onNotice) {
32
39
  }
33
40
  return lines;
34
41
  }
35
- export function __resetPeerAdmissionRangeAnnouncements() {
42
+ export function __resetPeerLaneRangeAnnouncements() {
36
43
  announcedOutOfRangeBySink = new WeakMap();
37
44
  announcedOutOfRangeConsole.clear();
38
45
  }
39
- export function resolvePeerAdmissionConfig(overrides, onNotice) {
40
- const out = { ...PEER_ADMISSION_DEFAULTS };
46
+ export function resolvePeerLaneConfig(overrides, onNotice) {
47
+ const out = { ...PEER_LANE_DEFAULTS };
41
48
  if (overrides === undefined)
42
49
  return out;
43
50
  for (const k of Object.keys(CONFIG_RANGES)) {
@@ -49,12 +56,14 @@ export function resolvePeerAdmissionConfig(overrides, onNotice) {
49
56
  ? "not_a_finite_number"
50
57
  : v < lo || v > hi
51
58
  ? "out_of_range"
52
- : undefined;
59
+ : INTEGER_FIELDS.has(k) && !Number.isInteger(v)
60
+ ? "not_an_integer"
61
+ : undefined;
53
62
  if (reason === undefined) {
54
63
  out[k] = v;
55
64
  continue;
56
65
  }
57
- announcePeerAdmissionFallback(k, v, reason, lo, hi, onNotice);
66
+ announcePeerLaneFallback(k, v, reason, lo, hi, onNotice);
58
67
  }
59
68
  return out;
60
69
  }
@@ -87,18 +96,18 @@ function configValueKey(v) {
87
96
  }
88
97
  return renderConfigValue(v);
89
98
  }
90
- function announcePeerAdmissionFallback(field, given, reason, lo, hi, onNotice) {
91
- const fallback = PEER_ADMISSION_DEFAULTS[field];
99
+ function announcePeerLaneFallback(field, given, reason, lo, hi, onNotice) {
100
+ const fallback = PEER_LANE_DEFAULTS[field];
92
101
  const shown = renderConfigValue(given);
93
102
  const key = JSON.stringify([field, typeof given, configValueKey(given)]);
94
- const ledger = peerAdmissionRangeLedger(onNotice);
103
+ const ledger = peerLaneRangeLedger(onNotice);
95
104
  if (ledger.has(key))
96
105
  return;
97
106
  ledger.add(key);
98
107
  deliverEngineNotice(onNotice, {
99
108
  code: "config.peer_admission_out_of_range",
100
- message: `Peer-admission setting ${field}=${shown} is ` +
101
- (reason === "out_of_range" ? `outside its legal range [${lo}, ${hi}]` : `not a finite number`) +
109
+ message: `Peer-lane setting ${field}=${shown} is ` +
110
+ (reason === "out_of_range" ? `outside its legal range [${lo}, ${hi}]` : reason === "not_an_integer" ? `not a whole number` : `not a finite number`) +
102
111
  `; that field falls back to its default (${fallback}). Every other field is unaffected.`,
103
112
  detail: {
104
113
  field,
@@ -6,6 +6,22 @@ export declare const PEER_SESSION_RECORD_SCHEMA_VERSION = 1;
6
6
  export declare const PEER_SESSION_RECORD_MAX_BYTES = 262144;
7
7
  export type PeerSessionLiveness = "live" | "dead" | "deleted";
8
8
  export type PeerSessionTempo = "active" | "idle" | "blocked";
9
+ /** A session's SELF-REPORTED presence: what it says it is (`agent`) and what it says it is doing
10
+ * (`intent`). Labels a peer controls — rendered bounded and prefixed `self:`, never read by any
11
+ * permission, routing or wake judgment. The same object rides `TaskSpec.peerPresence`, the wiring
12
+ * manifest and the directory row a host copies it into. */
13
+ export interface PeerPresence {
14
+ agent?: string;
15
+ intent?: string;
16
+ }
17
+ /** The read cap of one presence label; a longer value refuses the whole row (one rule, `field_invalid`). */
18
+ export declare const PEER_PRESENCE_MAX_CHARS = 120;
19
+ /** THE rule for one presence label, read by the row reader and by prepare's door on `TaskSpec.peerPresence` (a
20
+ * spec the directory would refuse is refused where it is written, not when the host files the row). */
21
+ export declare function peerPresenceLabelValid(v: unknown): v is string | undefined;
22
+ /** Prepare's door: `TaskSpec.peerPresence` is a plain object whose `agent` / `intent` each pass {@link peerPresenceLabelValid}
23
+ * — else `config.peer_presence_invalid`, naming the field. */
24
+ export declare function assertPeerPresenceValue(v: unknown): void;
9
25
  /** The registration row. Field set = CC's load-bearing subset + sema's `scope` axis + the sema-only
10
26
  * liveness/tombstone members. A host writes it; every terminal reads it through {@link readPeerSessionRecord}. */
11
27
  export interface PeerSessionRecord {
@@ -24,11 +40,24 @@ export interface PeerSessionRecord {
24
40
  procStartMs?: number;
25
41
  cwd?: string;
26
42
  startedAt: number;
43
+ /** When the row last changed (the host's `lastActivityAt` on a server row; `markDead` writes it too). */
27
44
  updatedAt: number;
45
+ /**
46
+ * @contract peer_directory.dead_is_the_hosts_word — `dead` is the HOST's word: a host marks a row dead
47
+ * when the session has no leg in flight AND `updatedAt` is older than the host's retention period, and
48
+ * stamps `diedAt` with the marking time; that retention period is at least the engine's listing
49
+ * freshness window (`listingFreshWindowMs`), so a row the engine would list as fresh is never dead.
50
+ * The engine reads `liveness` and `updatedAt`; it never rewrites `liveness` from a row's age.
51
+ */
28
52
  liveness: PeerSessionLiveness;
29
53
  /** Present iff `liveness !== "live"`: when the row was marked dead (or the tombstone was written). */
30
54
  diedAt?: number;
55
+ /** The host's activity word. A stale `tempo` is the host's to clear: a row that says `active` is
56
+ * listed as fresh regardless of its age. */
31
57
  tempo?: PeerSessionTempo;
58
+ /** Self-reported presence (see {@link PeerPresence}); a host copies it off the session's spec. */
59
+ agent?: string;
60
+ intent?: string;
32
61
  entrypoint?: string;
33
62
  peerProtocol?: number;
34
63
  /** CC capability bits (`/^[a-z0-9_]{1,32}$/`, ≤16); `sema_mailbox_v1` marks a store-lane peer. */
@@ -4,6 +4,18 @@ import { formatPeerNameRef, mintPeerRef, normalizePeerName, parsePeerNameRef, PE
4
4
  import { inlineUntrusted } from "../core/untrusted-text.js";
5
5
  export const PEER_SESSION_RECORD_SCHEMA_VERSION = 1;
6
6
  export const PEER_SESSION_RECORD_MAX_BYTES = 262_144;
7
+ export const PEER_PRESENCE_MAX_CHARS = 120;
8
+ export function peerPresenceLabelValid(v) {
9
+ return v === undefined || (typeof v === "string" && v.length <= PEER_PRESENCE_MAX_CHARS);
10
+ }
11
+ export function assertPeerPresenceValue(v) {
12
+ const bad = v === undefined ? undefined : v === null || typeof v !== "object" || Array.isArray(v) ? "peerPresence" : ["agent", "intent"].find((k) => !peerPresenceLabelValid(v[k]));
13
+ if (bad === undefined)
14
+ return;
15
+ const e = new Error(`TaskSpec.${bad === "peerPresence" ? bad : `peerPresence.${bad}`} must be ${bad === "peerPresence" ? "a plain { agent?, intent? } object" : `a string of at most ${PEER_PRESENCE_MAX_CHARS} characters`} — the peer directory refuses such a row whole, so the run would advertise a presence no listing can show`);
16
+ e.code = "config.peer_presence_invalid";
17
+ throw e;
18
+ }
7
19
  export const SESSION_BOX_PREFIX = "session.";
8
20
  const SESSION_ID_RE = /^[A-Za-z0-9_-]{1,80}$/;
9
21
  export const PEER_SESSION_ID_GRAMMAR = "^[A-Za-z0-9_-]{1,80}$";
@@ -61,6 +73,12 @@ export function readPeerSessionRecord(raw) {
61
73
  return bad("peerProtocol");
62
74
  if (!optString(r["cwd"]))
63
75
  return bad("cwd");
76
+ if (!peerPresenceLabelValid(r["agent"]))
77
+ return bad("agent");
78
+ if (!peerPresenceLabelValid(r["intent"]))
79
+ return bad("intent");
80
+ const agent = typeof r["agent"] === "string" && r["agent"].trim() !== "" ? r["agent"] : undefined;
81
+ const intent = typeof r["intent"] === "string" && r["intent"].trim() !== "" ? r["intent"] : undefined;
64
82
  if (!optString(r["entrypoint"]))
65
83
  return bad("entrypoint");
66
84
  if (!optString(r["sockPath"]))
@@ -88,6 +106,8 @@ export function readPeerSessionRecord(raw) {
88
106
  ...(r["pid"] !== undefined ? { pid: r["pid"] } : {}),
89
107
  ...(r["procStartMs"] !== undefined ? { procStartMs: r["procStartMs"] } : {}),
90
108
  ...(r["cwd"] !== undefined ? { cwd: r["cwd"] } : {}),
109
+ ...(agent !== undefined ? { agent } : {}),
110
+ ...(intent !== undefined ? { intent } : {}),
91
111
  startedAt: r["startedAt"],
92
112
  updatedAt: r["updatedAt"],
93
113
  liveness,
@@ -3,7 +3,7 @@ import type { CrossSessionHoldCause, CrossSessionInboundVerdict } from "./cross-
3
3
  import { type PeerDeliveryReceiptState } from "./peer-notices.js";
4
4
  /** The `cause` vocabulary of a `peer.inbound_disposition{disposition:"held"}` notice: the parity
5
5
  * judge's causes plus the drain-stage gate's TRANSIENT `rate_limited` (a bucket-wait, not a review
6
- * hold — it never enters the held queue; the drain stops at that seq and re-judges next round). */
6
+ * hold — it never enters the held queue; the drain stops at that seq and re-judges next pass). */
7
7
  export type PeerInboundHoldCause = CrossSessionHoldCause | "rate_limited";
8
8
  export interface PeerHeldEntry {
9
9
  /** The recipient box seq of the held record (its identity for the review face). */
@@ -26,7 +26,7 @@ export interface PeerHeldEntry {
26
26
  released?: boolean;
27
27
  }
28
28
  export type PeerHeldSettlement = "expired" | "delivered" | "refused" | "denied" | "dropped";
29
- export type PeerHeldSettleReason = "evicted" | "deadline" | "shutdown" | "rejudge" | "approved" | "denied" | "cancelled" | "inject_failed";
29
+ export type PeerHeldSettleReason = "evicted" | "deadline" | "shutdown" | "rejudge" | "approved" | "denied" | "cancelled" | "inject_failed" | "superseded";
30
30
  /** The clock/timer seam (tests inject a virtual one). Handles are opaque. */
31
31
  export interface PeerClock {
32
32
  now(): number;
@@ -41,8 +41,10 @@ export interface PeerHeldQueueSink {
41
41
  /** Re-judge an entry against the recipient's CURRENT settings and mode class (CC `M`). */
42
42
  judge: (entry: PeerHeldEntry) => CrossSessionInboundVerdict;
43
43
  /** Deliver a released entry into the live run's model context. Absent ⇒ no run is live. Throws ⇒
44
- * the lane refused the frame (settled `dropped`, receipt sent). */
45
- inject?: (entry: PeerHeldEntry) => void;
44
+ * the lane refused the frame (settled `dropped`, receipt sent). Returns `"deferred"` ⇒ the run is
45
+ * live but this pass's delivery budget is spent: the entry stays released and goes at the next
46
+ * pass's flush (the same wait a run-less approval takes). */
47
+ inject?: (entry: PeerHeldEntry) => void | "deferred";
46
48
  /** The receipt to the sender (the binding routes it into the sender's box). */
47
49
  receipt: (entry: PeerHeldEntry, state: PeerDeliveryReceiptState) => void;
48
50
  /** Announce an entry entering the queue (or its cause changing on re-judgment). */
@@ -76,6 +78,15 @@ export interface PeerHeldQueue {
76
78
  refused: number;
77
79
  held: number;
78
80
  };
81
+ /**
82
+ * #1036 — the box is this queue's durable seat, so an entry whose record is no longer IN the box was
83
+ * settled by whoever held the lease meanwhile (a takeover acked it, delivered or refused). Called by a
84
+ * drain right after it claims the lease, with the seqs the claimed snapshot holds (a claim marks EVERY
85
+ * unacked record, so absence is absence): such entries leave the queue as `expired` / `superseded`,
86
+ * with no receipt (the owner that settled them sent one) and no decision recorded (there is no record
87
+ * left to decide about). Returns how many were retired.
88
+ */
89
+ reconcile(present: ReadonlySet<number>): number;
79
90
  /** The review face (CC `l0e`): approve re-judges then delivers (or parks `released` with no live
80
91
  * run), deny/cancel drop with the matching receipt. `"gone"` when nothing is held under that seq. */
81
92
  settle(seq: number, decision: "approve" | "deny" | "cancel"): PeerHeldSettleOutcome;
@@ -83,7 +94,23 @@ export interface PeerHeldQueue {
83
94
  size(): number;
84
95
  /** CC `HBe` — a graceful shutdown settles everything still held as `expired` with receipts. */
85
96
  shutdown(): void;
97
+ /**
98
+ * #1036 — the TERMINAL DECISIONS this queue has taken about records that are still parked in
99
+ * the box, seq → the settlement word. Since a held record now stays in its durable box (the box is
100
+ * this queue's durable seat), a decision taken BETWEEN runs — a human's deny, a deadline, an
101
+ * eviction — has to survive the run whose ledger recorded it, or the session's next run re-judges
102
+ * the record from scratch and can deliver a message a person already refused. A decision is kept
103
+ * only until a drain that holds the lease acks past it ({@link PeerHeldQueue.forgetDecisions}); it
104
+ * is process-level, like the queue itself, and a crash loses it exactly as it loses the queue.
105
+ */
106
+ decisions(): ReadonlyMap<number, PeerHeldSettlement>;
107
+ /** #1036 — a lease-owning drain acked these seqs out of the box: their decisions are spent. */
108
+ forgetDecisions(seqs: Iterable<number>): void;
86
109
  }
110
+ /** #1036 — how many terminal decisions about still-parked records one box's queue remembers
111
+ * (see {@link PeerHeldQueue.decisions}). Several times the queue's own cap: the queue bounds what is
112
+ * LIVE, this bounds what has been decided and not yet observed as consumed. */
113
+ export declare const PEER_HELD_DECISION_MEMORY: number;
87
114
  export declare function createPeerHeldQueue(key: string, opts?: {
88
115
  cap?: number;
89
116
  clock?: PeerClock;
@@ -10,11 +10,13 @@ export const realPeerClock = {
10
10
  clearTimeout: (h) => clearTimeout(h),
11
11
  };
12
12
  const crashHandlers = new WeakMap();
13
+ export const PEER_HELD_DECISION_MEMORY = PEER_HELD_QUEUE_CAP * 4;
13
14
  export function createPeerHeldQueue(key, opts = {}) {
14
15
  const cap = opts.cap ?? PEER_HELD_QUEUE_CAP;
15
16
  const clock = opts.clock ?? realPeerClock;
16
17
  const entries = [];
17
18
  const deadlines = new Map();
19
+ const decisions = new Map();
18
20
  let cfg;
19
21
  const sinkOrThrow = () => {
20
22
  if (cfg === undefined)
@@ -44,6 +46,15 @@ export function createPeerHeldQueue(key, opts = {}) {
44
46
  const settleOut = (entry, settlement, reason, receipt) => {
45
47
  const sink = sinkOrThrow();
46
48
  remove(entry);
49
+ if (settlement !== "delivered") {
50
+ decisions.set(entry.seq, settlement);
51
+ while (decisions.size > PEER_HELD_DECISION_MEMORY) {
52
+ const oldest = decisions.keys().next();
53
+ if (oldest.done === true)
54
+ break;
55
+ decisions.delete(oldest.value);
56
+ }
57
+ }
47
58
  if (receipt !== undefined)
48
59
  safe(() => sink.receipt(entry, receipt));
49
60
  safe(() => sink.onSettled?.(entry, settlement, reason, entries.length));
@@ -71,7 +82,8 @@ export function createPeerHeldQueue(key, opts = {}) {
71
82
  if (sink.inject === undefined)
72
83
  return "no_leg";
73
84
  try {
74
- sink.inject(entry);
85
+ if (sink.inject(entry) === "deferred")
86
+ return "no_leg";
75
87
  }
76
88
  catch {
77
89
  settleOut(entry, "dropped", "inject_failed", "dropped");
@@ -80,6 +92,18 @@ export function createPeerHeldQueue(key, opts = {}) {
80
92
  settleOut(entry, "delivered", reason, "delivered");
81
93
  return "delivered";
82
94
  };
95
+ const reconcile = (present) => {
96
+ const sink = sinkOrThrow();
97
+ let retired = 0;
98
+ for (const entry of [...entries]) {
99
+ if (present.has(entry.seq))
100
+ continue;
101
+ remove(entry);
102
+ retired += 1;
103
+ safe(() => sink.onSettled?.(entry, "expired", "superseded", entries.length));
104
+ }
105
+ return retired;
106
+ };
83
107
  const rejudge = () => {
84
108
  const sink = sinkOrThrow();
85
109
  let released = 0;
@@ -151,6 +175,7 @@ export function createPeerHeldQueue(key, opts = {}) {
151
175
  safe(() => sink.receipt(entry, "held"));
152
176
  },
153
177
  rejudge,
178
+ reconcile,
154
179
  settle(seq, decision) {
155
180
  const sink = sinkOrThrow();
156
181
  const entry = entries.find((e) => e.seq === seq);
@@ -186,6 +211,11 @@ export function createPeerHeldQueue(key, opts = {}) {
186
211
  },
187
212
  list: () => entries.map((e) => ({ ...e, ...(e.hopChain !== undefined ? { hopChain: [...e.hopChain] } : {}), ...(e.peerMeta !== undefined ? { peerMeta: { ...e.peerMeta } } : {}) })),
188
213
  size: () => entries.length,
214
+ decisions: () => decisions,
215
+ forgetDecisions(seqs) {
216
+ for (const seq of seqs)
217
+ decisions.delete(seq);
218
+ },
189
219
  shutdown() {
190
220
  if (cfg === undefined) {
191
221
  entries.length = 0;
@@ -199,6 +229,7 @@ export function createPeerHeldQueue(key, opts = {}) {
199
229
  },
200
230
  };
201
231
  crashHandlers.set(queue, () => {
232
+ decisions.clear();
202
233
  entries.length = 0;
203
234
  for (const h of deadlines.values())
204
235
  clock.clearTimeout(h);
@@ -1,9 +1,10 @@
1
1
  import { type MailboxCrossProcessVerdict, type MailboxStore } from "../core/mailbox-store.js";
2
- import type { SessionBoxBinding, SystemInjectionPriority, TaskNotificationPayload } from "../core/task-notification.js";
2
+ import { type SessionBoxBinding, type SystemInjectionPriority, type TaskNotificationPayload } from "../core/task-notification.js";
3
+ import { type FrameSettlement } from "../core/frame-settlement.js";
3
4
  import { type EngineNotice, type RunnerDeps } from "../core/types.js";
4
5
  import { type PermissionModeClass } from "./cross-session-envelope.js";
5
6
  import { type CrossSessionInboundSettingLayers } from "./cross-session-judge.js";
6
- import { type PeerAdmissionConfig } from "./peer-admission.js";
7
+ import { type PeerLaneConfig } from "./peer-admission.js";
7
8
  import { type PeerDirectory } from "./peer-directory.js";
8
9
  import { createListAgentsTool } from "./list-agents-tool.js";
9
10
  import type { AskEffective } from "../core/wiring-manifest.js";
@@ -46,13 +47,34 @@ export declare function judgePeerLaneMount(deps: {
46
47
  * revival lane), and a drain per child on a box nobody writes would cost a lease claim per turn on
47
48
  * every background agent. `undefined` on every refusal — silently: no host asked for a box here.
48
49
  */
49
- export declare function judgeSessionBoxDrain(deps: Pick<RunnerDeps, "mailboxStore" | "onError">, leg: {
50
+ export declare function judgeSessionBoxDrain(deps: Pick<RunnerDeps, "mailboxStore" | "onError" | "sessionBoxEngineFrames">, leg: {
50
51
  sessionId: string;
51
52
  scope: string;
52
53
  isDelegatedChild: boolean;
53
54
  }): SessionBoxBinding | undefined;
54
- /** The disposition arms of a `peer.inbound_disposition` notice (closed set; `detail.disposition`). */
55
- export type PeerInboundDisposition = "refused" | "held" | "admission_refused" | "notice_unrouted" | "notice_user_only";
55
+ /** What one pull of the box handed back: the records read (already rendered frames), and what is still waiting. */
56
+ export interface PulledInbox {
57
+ delivered: TaskNotificationPayload[];
58
+ /** Records still leased in the box after this pull (the next pull, or the next boundaries, read them). */
59
+ pending: number;
60
+ /** How many of the waiting records their sender marked urgent. */
61
+ urgent: number;
62
+ }
63
+ /** The disposition arms of a `peer.inbound_disposition` notice (closed set; `detail.disposition`).
64
+ * #1036 adds `resupply_exhausted`: a record whose frame a compaction summarized away, was
65
+ * re-supplied once, and was summarized away AGAIN. It settles (leaves the box) — but as a DISCLOSED
66
+ * loss, never as "the model read it". The cap is a bound on delivery attempts; it does not
67
+ * manufacture a consumption proof. */
68
+ export type PeerInboundDisposition = "refused" | "held" | "admission_refused" | "notice_unrouted" | "notice_user_only" | "resupply_exhausted";
69
+ /**
70
+ * #1036 — how many times a record whose frame was summarized away is handed over again before the
71
+ * drain gives up and discloses. `1` = one repair, i.e. at most two occurrences of one record in one
72
+ * run. A cap exists because repair that cannot converge is a loop; it is this small because the
73
+ * second occurrence already says everything the first did. RUN-SCOPED (the ledger is in memory): a
74
+ * crash or a takeover re-serves the record at `attempt: 1`, the at-least-once posture this lane has
75
+ * always had, not a new hole.
76
+ */
77
+ export declare const PEER_RESUPPLY_CAP = 1;
56
78
  export interface PeerSessionDrainOptions {
57
79
  mailbox: MailboxStore;
58
80
  /** This session (the box owner) and this leg's cycle id (the lease owner). */
@@ -64,8 +86,8 @@ export interface PeerSessionDrainOptions {
64
86
  inject: (notification: TaskNotificationPayload, opts?: {
65
87
  priority?: SystemInjectionPriority;
66
88
  }) => void;
67
- /** Deployment tuning of the admission gate (the same seat SendMessage reads). */
68
- admission?: Partial<PeerAdmissionConfig>;
89
+ /** The peer lane's knob table (admission gate + delivery budgets + listing window; the same seat SendMessage reads). */
90
+ lane?: Partial<PeerLaneConfig>;
69
91
  /** The recipient's self-token set for the hop check, read live (the run's peer identity ref). */
70
92
  ownTokens: () => readonly string[];
71
93
  /** The recipient's `crossSessionInbound` layers, read at every drain. */
@@ -74,7 +96,7 @@ export interface PeerSessionDrainOptions {
74
96
  selfModeClass: () => PermissionModeClass | "unknown";
75
97
  /** This session's display name (its directory row's name), for receipts and idle notices it authors. */
76
98
  selfName?: () => string | undefined;
77
- /** Awaited ONCE before the first round (a directory lookup that fills `selfName`, say); a rejection is swallowed. */
99
+ /** Awaited ONCE before the first pass (a directory lookup that fills `selfName`, say); a rejection is swallowed. */
78
100
  warmup?: () => Promise<unknown>;
79
101
  /** The deployment's `dialogExpiry` (raw; resolved at bind — an out-of-vocabulary value is announced
80
102
  * once through `onError` and the default applies). Absent ⇒ the default (5 m). */
@@ -89,14 +111,55 @@ export interface PeerSessionDrainOptions {
89
111
  sessionId: string;
90
112
  classification: string;
91
113
  }) => void;
114
+ /** #1036 — the LEG's settlement seam: the receipts the harness records when a frame reaches
115
+ * the transcript and the effects the compaction commit beat records. Absent ⇒ the drain mints a
116
+ * private one, which is honest but inert: nothing fills it, so every delivered record waits for a
117
+ * receipt that never comes and stays parked (at-least-once). Every production binding supplies it. */
118
+ settlement?: FrameSettlement;
92
119
  leaseTtlMs?: number;
93
120
  /** Tests: a virtual clock for the held deadlines and idle timers (honored when this process first sees the session). */
94
121
  clock?: PeerClock;
95
122
  }
96
123
  export interface PeerSessionDrain {
97
- /** One drain round (serialized: a boundary never overlaps a still-running round). */
124
+ /**
125
+ * One drain pass (serialized: a boundary never overlaps a still-running pass).
126
+ *
127
+ * @contract cross_session.settled_means_committed_and_not_summarized — a session-box record is acked
128
+ * only after a frame carrying it has been committed to the transcript and no compaction applied up to
129
+ * that settlement beat summarized that entry away. A summarized frame is re-supplied once (a new
130
+ * occurrence of the SAME record: same `task_id`, same `seq`, `attempt: 2`, `resupplied: true`), and a
131
+ * re-supplied occurrence summarized again settles as `resupply_exhausted`, disclosed — never as seen.
132
+ * OBSERVABLE: a `compacted` frame counting a not-yet-acked delivered record
133
+ * (`summarizedCrossSessionMessages >= 1`) is followed, in the same boundary's delivery half, by a
134
+ * re-supply frame for it or by a `resupply_exhausted` disposition — never by silence; a record
135
+ * committed and never summarized is never shown twice; no compaction implies no re-supply.
136
+ */
98
137
  drain(): Promise<void>;
99
- /** Run end: ack the last batch and release the lease so the session's next cycle claims at once. */
138
+ /**
139
+ * The PULL face (the `ReadInbox` tool): one pass over the same box, same cursor, same judgments —
140
+ * but the records are RETURNED instead of injected, up to `limit` (default = the pass budget). A
141
+ * record a boundary already handed over is never returned again, and a record returned here is
142
+ * never injected later: one cursor (`settledUpTo`), two readers. Never acks (the next boundary does).
143
+ */
144
+ pull(limit?: number, toolCallId?: string, charBudget?: number): Promise<PulledInbox>;
145
+ /**
146
+ * #1036 — THE TURN BOUNDARY's pass, called by the boundary lane as its tail so it runs AFTER that
147
+ * boundary's compaction (a hook of its own would have been registration-order luck). The settlement
148
+ * half always runs; the delivery half only when `continues` — a boundary this leg will not follow
149
+ * with another request cannot produce a transcript receipt for anything handed over there.
150
+ */
151
+ boundaryRound(o: {
152
+ continues: boolean;
153
+ }): Promise<void>;
154
+ /** #1036 — "is this engine frame's record still parked in this box?" The terminal sweep's
155
+ * per-session pend must skip those: the box re-serves them itself, and a process-local copy beside
156
+ * a parked record is two sources of truth for one message. */
157
+ isBoxBacked(payload: unknown): boolean;
158
+ /** #1036 — "does this leg hold a record a landed compaction could still break?" The compaction
159
+ * commit beat asks before recording its effect, so a run that will never read one records none. */
160
+ wantsCompactionEffects(): boolean;
161
+ /** Run end: settle what reached the transcript, then release the lease so the session's next cycle
162
+ * claims at once. What did NOT reach it stays parked (at-least-once, next run re-serves it). */
100
163
  finish(): Promise<void>;
101
164
  /** The run began working (cancels a pending idle fire). */
102
165
  busy(): void;
@@ -112,6 +175,15 @@ export declare function createPeerSessionDrain(opts: PeerSessionDrainOptions): P
112
175
  * before it — both read this one cell. */
113
176
  export interface PeerLaneRefs {
114
177
  askEffective?: AskEffective;
178
+ /** The bound drain's pull face (filled by {@link bindPeerLaneDrain}); the `ReadInbox` tool reads it late. */
179
+ pull?: (limit?: number, toolCallId?: string, charBudget?: number) => Promise<PulledInbox>;
180
+ /** #1036 — the bound drain's BOUNDARY pass (filled by {@link bindPeerLaneDrain}); the leg's
181
+ * prepared seat copies it out so the turn-boundary lane can tail-call it. */
182
+ boundaryRound?: (o: {
183
+ continues: boolean;
184
+ }) => Promise<void>;
185
+ /** #1036 — the bound drain's box-backed predicate (filled by {@link bindPeerLaneDrain}). */
186
+ isBoxBacked?: (payload: unknown) => boolean;
115
187
  }
116
188
  /**
117
189
  * The mount verdict + its loud half: `true` = the lane mounts on this leg; `false` = no seat, or the
@@ -153,7 +225,10 @@ export declare function listAgentsMountable(face: {
153
225
  /** design/385 §5.1 — the ListAgents mount (alias ListPeers): read-only, concurrency-safe, on the peer
154
226
  * seat's own arm beside SendMessage; a caller-supplied tool of the same name wins (skip, not throw). */
155
227
  export declare function mountListAgents(args: {
156
- peerDirectory: PeerDirectory;
228
+ /** The directory when the peer lane is up; absent on a delegation-only mount (agent lanes list, no peer rows). */
229
+ peerDirectory?: PeerDirectory;
230
+ /** The resolved listing freshness window (the peer lane's knob table). */
231
+ freshWindowMs?: number;
157
232
  sessionId: string;
158
233
  scope: string;
159
234
  hostTaskId: string;
@@ -190,18 +265,21 @@ export interface PeerDrainHarness {
190
265
  export declare function bindPeerSessionDrain(harness: PeerDrainHarness, opts: PeerSessionDrainOptions): PeerSessionDrain;
191
266
  /**
192
267
  * The leg-side spelling of {@link bindPeerSessionDrain}: the deployment seats the drain reads
193
- * (`mailboxStore` / `peerAdmission` / `crossSessionInbound` / `crossSessionDialogExpiry` / the two
268
+ * (`mailboxStore` / `peerLane` / `crossSessionInbound` / `crossSessionDialogExpiry` / the two
194
269
  * sinks) come straight off deps, the recipient's mode class folds off the lane refs — one binding call
195
270
  * per leg, no per-seat plumbing.
196
271
  */
197
272
  export declare function bindPeerLaneDrain(harness: PeerDrainHarness, args: {
198
- deps: Pick<RunnerDeps, "mailboxStore" | "peerDirectory" | "peerAdmission" | "crossSessionInbound" | "crossSessionDialogExpiry" | "onNotice" | "onError">;
273
+ deps: Pick<RunnerDeps, "mailboxStore" | "peerDirectory" | "peerLane" | "crossSessionInbound" | "crossSessionDialogExpiry" | "onNotice" | "onError">;
199
274
  sessionId: string;
200
275
  runId: string;
201
276
  scope: string;
202
277
  inject: PeerSessionDrainOptions["inject"];
203
278
  ownTokens: () => readonly string[];
204
279
  refs: PeerLaneRefs;
280
+ /** #1036 — the leg's settlement seam (receipts + compaction effects), minted by the
281
+ * orchestrator and shared with the harness sink and the compaction commit beat. */
282
+ settlement?: FrameSettlement;
205
283
  selfName?: () => string | undefined;
206
284
  parked?: () => boolean;
207
285
  lastTurnText?: () => string | undefined;