@sema-agent/core 7.29.0 → 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 (149) hide show
  1. package/CHANGELOG.md +361 -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/swappable-deps.d.ts +20 -4
  92. package/dist/core/swappable-deps.js +6 -1
  93. package/dist/core/task-event.d.ts +5 -0
  94. package/dist/core/task-notification.d.ts +144 -2
  95. package/dist/core/task-notification.js +154 -5
  96. package/dist/core/task-result.d.ts +14 -0
  97. package/dist/core/task-spec.d.ts +7 -0
  98. package/dist/core/tool-catalog-entries.js +2 -1
  99. package/dist/core/tool-conformance.js +1 -0
  100. package/dist/core/tool-policy.d.ts +9 -4
  101. package/dist/core/tool-spec.d.ts +14 -0
  102. package/dist/core/wiring-manifest.d.ts +12 -0
  103. package/dist/core/wiring-manifest.js +5 -1
  104. package/dist/engine/harness/agent-harness.d.ts +14 -1
  105. package/dist/engine/harness/agent-harness.js +28 -3
  106. package/dist/engine/harness/types.d.ts +3 -1
  107. package/dist/engine/llm/types.d.ts +8 -7
  108. package/dist/engine/llm/types.js +1 -1
  109. package/dist/engine/session/import-validate.js +4 -0
  110. package/dist/index.d.ts +10 -7
  111. package/dist/index.js +7 -5
  112. package/dist/orchestration/run-workflow-tool.d.ts +4 -4
  113. package/dist/orchestration/workflow.d.ts +1 -1
  114. package/dist/orchestration/workflow.js +8 -5
  115. package/dist/shell/lex.d.ts +30 -51
  116. package/dist/shell/lex.js +237 -284
  117. package/dist/stores/cc/mailbox-store.js +3 -2
  118. package/dist/stores/file/corrupt-read-sink.d.ts +28 -2
  119. package/dist/stores/file/file-history-store.d.ts +2 -4
  120. package/dist/stores/file/file-history-store.js +1 -1
  121. package/dist/stores/file/fs-atomic.d.ts +16 -11
  122. package/dist/stores/file/fs-atomic.js +17 -6
  123. package/dist/stores/file/index.d.ts +5 -6
  124. package/dist/stores/file/index.js +1 -1
  125. package/dist/stores/file/mailbox-store.d.ts +4 -9
  126. package/dist/stores/file/mailbox-store.js +29 -30
  127. package/dist/stores/file/session-policy-store.d.ts +2 -0
  128. package/dist/stores/file/session-policy-store.js +1 -1
  129. package/dist/stores/file/session-store.d.ts +11 -7
  130. package/dist/stores/file/session-store.js +23 -16
  131. package/dist/stores/file/shared-ledger.js +3 -14
  132. package/dist/stores/file/tool-result-store.d.ts +2 -0
  133. package/dist/stores/file/tool-result-store.js +5 -2
  134. package/dist/stores/file/workflow-journal-store.js +14 -9
  135. package/dist/tools/fs/bash-program-position.d.ts +15 -24
  136. package/dist/tools/fs/bash-program-position.js +11 -29
  137. package/dist/tools/fs/bash-readonly-classifier.d.ts +125 -117
  138. package/dist/tools/fs/bash-readonly-classifier.js +400 -272
  139. package/dist/tools/fs/shell-read-check.js +3 -3
  140. package/dist/tools/fs/shell-read-establish.js +1 -1
  141. package/dist/tools/fs/shell-read-plan.d.ts +11 -6
  142. package/dist/tools/fs/shell-read-plan.js +167 -183
  143. package/dist/tools/fs/shell-read-word.d.ts +50 -0
  144. package/dist/tools/fs/shell-read-word.js +59 -0
  145. package/dist/tools/fs/shell-reentry-text.d.ts +10 -0
  146. package/dist/tools/fs/shell-reentry-text.js +73 -0
  147. package/package.json +1 -1
  148. package/test/export-surface.snapshot.json +58 -8
  149. package/wire-changes/core@7.30.0.json +1821 -0
@@ -1,53 +1,63 @@
1
1
  import { isPermissionModeClass } from "./cross-session-envelope.js";
2
- export const CROSS_SESSION_INBOUND_SETTINGS = Object.freeze(["accept", "hold", "refuse"]);
2
+ export const CROSS_SESSION_INBOUND_SETTINGS = Object.freeze(["wake", "next-turn", "hold", "refuse"]);
3
3
  export function isCrossSessionInboundSetting(value) {
4
4
  return typeof value === "string" && CROSS_SESSION_INBOUND_SETTINGS.includes(value);
5
5
  }
6
- const RANK = { accept: 0, hold: 1, refuse: 2 };
6
+ export const CROSS_SESSION_INBOUND_DEPRECATED_SPELLINGS = Object.freeze({ accept: "next-turn" });
7
+ const RANK = { wake: 0, "next-turn": 1, hold: 2, refuse: 3 };
8
+ const UNSET_BASELINE = "next-turn";
7
9
  function readLayer(v) {
8
10
  if (v === undefined || v === null || v === "default")
9
- return undefined;
10
- return isCrossSessionInboundSetting(v) ? v : "invalid";
11
+ return { value: undefined };
12
+ if (isCrossSessionInboundSetting(v))
13
+ return { value: v };
14
+ const migrated = typeof v === "string" && Object.hasOwn(CROSS_SESSION_INBOUND_DEPRECATED_SPELLINGS, v) ? CROSS_SESSION_INBOUND_DEPRECATED_SPELLINGS[v] : undefined;
15
+ return migrated !== undefined ? { value: migrated, deprecated: true } : { value: "invalid" };
11
16
  }
12
17
  export function resolveCrossSessionInboundSetting(layers) {
13
18
  const invalidLayers = [];
19
+ const deprecatedLayers = [];
14
20
  let value;
15
21
  let decidedBy;
16
22
  for (const [name, raw] of [
17
23
  ["managed", layers.managed],
18
24
  ["user", layers.user],
19
25
  ]) {
20
- const v = readLayer(raw);
26
+ const { value: v, deprecated } = readLayer(raw);
21
27
  if (v === "invalid") {
22
28
  invalidLayers.push(name);
23
29
  continue;
24
30
  }
31
+ if (deprecated === true)
32
+ deprecatedLayers.push(name);
25
33
  if (v !== undefined && value === undefined) {
26
34
  value = v;
27
35
  decidedBy = name;
28
36
  }
29
37
  }
30
38
  for (const raw of layers.repo ?? []) {
31
- const v = readLayer(raw);
39
+ const { value: v, deprecated } = readLayer(raw);
32
40
  if (v === "invalid") {
33
41
  invalidLayers.push("repo");
34
42
  continue;
35
43
  }
44
+ if (deprecated === true)
45
+ deprecatedLayers.push("repo");
36
46
  if (v === undefined)
37
47
  continue;
38
- if (RANK[v] > RANK[value ?? "accept"]) {
48
+ if (RANK[v] > RANK[value ?? UNSET_BASELINE]) {
39
49
  value = v;
40
50
  decidedBy = "repo";
41
51
  }
42
- else if (v !== "accept" && value !== undefined && RANK[v] === RANK[value] && decidedBy !== "managed") {
52
+ else if (v !== UNSET_BASELINE && value !== undefined && RANK[v] === RANK[value] && decidedBy !== "managed") {
43
53
  decidedBy = "repo";
44
54
  }
45
55
  }
46
- if (invalidLayers.length > 0 && RANK[value ?? "accept"] < RANK.hold) {
56
+ if (invalidLayers.length > 0 && RANK[value ?? UNSET_BASELINE] < RANK.hold) {
47
57
  value = "hold";
48
58
  decidedBy = "invalid";
49
59
  }
50
- return { ...(value !== undefined ? { value } : {}), ...(decidedBy !== undefined ? { decidedBy } : {}), invalidLayers };
60
+ return { ...(value !== undefined ? { value } : {}), ...(decidedBy !== undefined ? { decidedBy } : {}), invalidLayers, ...(deprecatedLayers.length > 0 ? { deprecatedLayers } : {}) };
51
61
  }
52
62
  export const CROSS_SESSION_HOLD_CAUSES = Object.freeze([
53
63
  "mode-mismatch",
@@ -71,7 +81,7 @@ export function describeCrossSessionHoldCause(cause) {
71
81
  case "managed-setting":
72
82
  return `Your organization's managed settings set "crossSessionInbound" to "hold".`;
73
83
  case "repo-setting":
74
- return `This repository's settings set "crossSessionInbound" to "hold" (your own "accept" cannot override a repo tightening).`;
84
+ return `This repository's settings set "crossSessionInbound" to "hold" (your own "next-turn" cannot override a repo tightening).`;
75
85
  case "invalid-setting":
76
86
  return 'A settings file has an unrecognized "crossSessionInbound" value (see the settings warning), so messages are held while it is present.';
77
87
  case "bypass-default":
@@ -121,7 +131,8 @@ export function judgeCrossSessionInbound(input) {
121
131
  const { setting, selfModeClass, sender } = input;
122
132
  if (setting.value !== undefined) {
123
133
  switch (setting.value) {
124
- case "accept":
134
+ case "wake":
135
+ case "next-turn":
125
136
  return { verdict: "deliver" };
126
137
  case "refuse":
127
138
  return { verdict: "refuse", cause: "opt-out" };
@@ -1,14 +1,27 @@
1
1
  import { Type } from "typebox";
2
2
  import type { TaskAccess } from "../core/task-registry.js";
3
3
  import { type RosterEntry } from "./roster-store.js";
4
- import { type PeerDirectory } from "./peer-directory.js";
4
+ import { type PeerDirectory, type PeerPresence, type PeerSessionRecord } from "./peer-directory.js";
5
5
  export declare const LIST_AGENTS_TOOL_NAME = "ListAgents";
6
6
  export declare const LIST_AGENTS_TOOL_ALIAS = "ListPeers";
7
7
  /** CC `maxResultSizeChars` for this tool. */
8
8
  export declare const LIST_AGENTS_MAX_RESULT_CHARS = 10000;
9
+ /** The default freshness window (24 h) — one source, the peer lane's knob table. */
10
+ export declare const PEER_LISTING_FRESH_WINDOW_MS: number;
11
+ /**
12
+ * THE freshness predicate: a peer row is listed by default iff it is live AND (its host says it is
13
+ * working — `tempo` active/blocked — OR it changed within the window). One rule over rendering
14
+ * visibility only: refs are minted and names resolve over the FULL set, so a row hidden here is still
15
+ * addressable by name. Dead and stale rows are what `includeAll` adds.
16
+ */
17
+ export declare function isFreshPeerRow(r: Pick<PeerSessionRecord, "liveness" | "tempo" | "updatedAt">, now: number, windowMs?: number): boolean;
18
+ /** `Ns` / `Nm` / `Nh` / `Nd` — a bounded age word for a listing row; a future stamp reads as `0s`. */
19
+ export declare function formatAgo(ms: number): string;
20
+ /** The last two path segments of a working directory (what a person recognizes a project by). */
21
+ export declare function cwdTail(cwd: string): string;
9
22
  export interface ListAgentsToolOptions {
10
- /** The peer-session directory (the mount reason; without it the tool is not mounted). */
11
- peerDirectory: PeerDirectory;
23
+ /** The peer-session directory. Absent ⇒ no peer rows (the two agent lanes still list). */
24
+ peerDirectory?: PeerDirectory;
12
25
  /** This session's own identity — excluded from the peer listing, included in ref disambiguation. */
13
26
  self: {
14
27
  sessionId: string;
@@ -40,8 +53,10 @@ export interface ListAgentsToolOptions {
40
53
  roster?: {
41
54
  list(): RosterEntry[] | Promise<RosterEntry[]>;
42
55
  };
43
- /** Include dead (offline) peer sessions in the listing. Default false (CC observable form). */
44
- includeOffline?: boolean;
56
+ /** The resolved freshness window ({@link isFreshPeerRow}); absent ⇒ {@link PEER_LISTING_FRESH_WINDOW_MS}. */
57
+ freshWindowMs?: number;
58
+ /** The clock (tests). */
59
+ now?: () => number;
45
60
  }
46
61
  /** One rendered row. `kind` names the lane; `address` is what SendMessage accepts. */
47
62
  export interface ListAgentsRow {
@@ -49,6 +64,14 @@ export interface ListAgentsRow {
49
64
  name: string;
50
65
  /** Peer rows only: the listing-minted ref. */
51
66
  ref?: string;
67
+ /** Peer rows, live: when the row last changed (rendered `active <N> ago`). */
68
+ lastActiveAt?: number;
69
+ /** Peer rows, dead: when the host marked it (rendered `offline <N> ago`); a dead row with no stamp renders `offline`. */
70
+ offlineAt?: number;
71
+ /** Peer rows: the last two segments of the session's working directory, when the row carries one. */
72
+ cwd?: string;
73
+ /** Peer rows: the session's self-reported presence, when it reported any. */
74
+ presence?: PeerPresence;
52
75
  /** What SendMessage accepts: on both AGENT lanes the registry `task_id` VERBATIM (no prefixing or
53
76
  * re-minting), never the child's session id — a different domain, tabled in INTEGRATION-CORE §1.6a.
54
77
  * Peer rows carry the `session.<id>` box handle instead. */
@@ -59,4 +82,6 @@ export interface ListAgentsRow {
59
82
  * COMPLETED and nothing else: a model applies the continuable rule to that word. */
60
83
  state: "busy" | "idle" | "offline" | "running" | "parked" | "finished" | "failed" | "killed" | "cancelled" | "unknown";
61
84
  }
62
- export declare function createListAgentsTool(opts: ListAgentsToolOptions): import("../internal/harness-types.js").AgentTool<Type.TObject<{}>, unknown>;
85
+ export declare function createListAgentsTool(opts: ListAgentsToolOptions): import("../internal/harness-types.js").AgentTool<Type.TObject<{
86
+ includeAll: Type.TOptional<Type.TBoolean>;
87
+ }>, unknown>;
@@ -3,12 +3,38 @@ import { defineTool } from "../core/tools.js";
3
3
  import { inlineUntrusted } from "../core/untrusted-text.js";
4
4
  import { formatPeerNameRef } from "./cross-session-ref.js";
5
5
  import { entryAccessible } from "./roster-store.js";
6
- import { mintPeerSessionCandidates, peerSessionBoxHandle } from "./peer-directory.js";
6
+ import { mintPeerSessionCandidates, PEER_PRESENCE_MAX_CHARS, peerSessionBoxHandle } from "./peer-directory.js";
7
+ import { PEER_LANE_DEFAULTS } from "./peer-admission.js";
7
8
  import { toolFace } from "../core/tool-catalog-entries.js";
8
9
  import { thrownText } from "../core/thrown-value.js";
10
+ import { assertNoRetiredKeys } from "../core/retired-keys.js";
9
11
  export const LIST_AGENTS_TOOL_NAME = "ListAgents";
10
12
  export const LIST_AGENTS_TOOL_ALIAS = "ListPeers";
11
13
  export const LIST_AGENTS_MAX_RESULT_CHARS = 10_000;
14
+ export const PEER_LISTING_FRESH_WINDOW_MS = PEER_LANE_DEFAULTS.listingFreshWindowMs;
15
+ export function isFreshPeerRow(r, now, windowMs = PEER_LISTING_FRESH_WINDOW_MS) {
16
+ if (r.liveness !== "live")
17
+ return false;
18
+ if (r.tempo === "active" || r.tempo === "blocked")
19
+ return true;
20
+ return now - r.updatedAt <= windowMs;
21
+ }
22
+ export function formatAgo(ms) {
23
+ const sec = Math.max(0, Math.floor(ms / 1000));
24
+ if (sec < 60)
25
+ return `${sec}s`;
26
+ const min = Math.floor(sec / 60);
27
+ if (min < 60)
28
+ return `${min}m`;
29
+ const h = Math.floor(min / 60);
30
+ if (h < 48)
31
+ return `${h}h`;
32
+ return `${Math.floor(h / 24)}d`;
33
+ }
34
+ export function cwdTail(cwd) {
35
+ const parts = cwd.split(/[\\/]+/).filter((p) => p !== "");
36
+ return parts.slice(-2).join("/");
37
+ }
12
38
  const AGENT_ROW_STATE = {
13
39
  pending: "running",
14
40
  running: "running",
@@ -32,26 +58,46 @@ function peerRowState(r) {
32
58
  const tempo = r.tempo;
33
59
  return tempo !== undefined && Object.hasOwn(PEER_ROW_STATE, tempo) ? PEER_ROW_STATE[tempo] : "unknown";
34
60
  }
61
+ function renderPresence(p) {
62
+ const parts = [...(p.agent !== undefined ? [`agent=${inlineUntrusted(p.agent, PEER_PRESENCE_MAX_CHARS)}`] : []), ...(p.intent !== undefined ? [`intent=${inlineUntrusted(p.intent, PEER_PRESENCE_MAX_CHARS)}`] : [])];
63
+ return `self: ${parts.join(", ")}`;
64
+ }
65
+ const RETIRED_LIST_AGENTS_OPTION_KEYS = {
66
+ includeOffline: "the model asks for stale and offline rows per call with the `includeAll` tool parameter (a superset: stale + offline)",
67
+ };
35
68
  export function createListAgentsTool(opts) {
69
+ assertNoRetiredKeys(opts, RETIRED_LIST_AGENTS_OPTION_KEYS, "ListAgentsToolOptions");
70
+ const peers = opts.peerDirectory !== undefined;
71
+ const windowMs = opts.freshWindowMs ?? PEER_LISTING_FRESH_WINDOW_MS;
36
72
  return defineTool({
37
73
  name: LIST_AGENTS_TOOL_NAME,
38
74
  ...toolFace("list-agents"),
39
75
  isConcurrencySafe: () => true,
40
- description: `List the agents and peer sessions you can message with SendMessage. Peer sessions are other sessions of this ` +
41
- `engine running for the same user on this machine, shown as \`name [ref]\`; send to one with its name (or \`name [ref]\` ` +
42
- `when a name is ambiguous, or its \`session.<id>\` address). A peer that is offline still accepts messages — they are parked ` +
43
- `durably in its session box and delivered when it next runs. Background agents you spawned are listed with their task_id. ` +
76
+ description: `List the agents${peers ? " and peer sessions" : ""} you can message with SendMessage. ` +
77
+ (peers
78
+ ? `Peer sessions are other sessions of this engine running for the same user, shown as \`name [ref] · state · active <N> ago · <directory> · self: agent=…, intent=…\`; send to one with its name (or \`name [ref]\` ` +
79
+ `when a name is ambiguous, or its \`session.<id>\` address). By default only FRESH peers are listed (working now, or active within the last ${formatAgo(windowMs)}); ` +
80
+ `pass includeAll: true to also list stale and offline ones. A peer that is offline or unlisted still accepts messages — they are parked ` +
81
+ `durably in its session box and delivered when it next runs. `
82
+ : `Peer sessions of this engine are not addressable on this mount (no session directory). `) +
83
+ `Background agents you spawned are listed with their task_id. ` +
44
84
  `A ref is only valid against the listing (or disambiguation error) you read it from — do not invent one.`,
45
- parameters: Type.Object({}),
46
- execute: async () => {
85
+ parameters: Type.Object({
86
+ includeAll: Type.Optional(Type.Boolean({ description: "Also list stale (no activity within the freshness window) and offline peer sessions. Default false." })),
87
+ }),
88
+ execute: async (args) => {
89
+ const includeAll = args.includeAll === true;
90
+ const now = opts.now?.() ?? Date.now();
47
91
  const rows = [];
48
92
  let peerRows = [];
49
93
  let directoryFault;
50
- try {
51
- peerRows = await opts.peerDirectory.listPeerSessions({ scope: opts.self.scope });
52
- }
53
- catch (e) {
54
- directoryFault = thrownText(e);
94
+ if (opts.peerDirectory !== undefined) {
95
+ try {
96
+ peerRows = await opts.peerDirectory.listPeerSessions({ scope: opts.self.scope });
97
+ }
98
+ catch (e) {
99
+ directoryFault = thrownText(e);
100
+ }
55
101
  }
56
102
  for (const c of mintPeerSessionCandidates(peerRows, { sessionId: opts.self.sessionId })) {
57
103
  const r = c.record;
@@ -59,14 +105,18 @@ export function createListAgentsTool(opts) {
59
105
  continue;
60
106
  if (r.liveness === "deleted")
61
107
  continue;
62
- if (r.liveness === "dead" && opts.includeOffline !== true)
108
+ if (!includeAll && !isFreshPeerRow(r, now, windowMs))
63
109
  continue;
110
+ const presence = { ...(r.agent !== undefined ? { agent: r.agent } : {}), ...(r.intent !== undefined ? { intent: r.intent } : {}) };
64
111
  rows.push({
65
112
  kind: "peer_session",
66
113
  name: r.name,
67
114
  ref: c.ref,
68
115
  address: peerSessionBoxHandle(r.sessionId),
69
116
  state: peerRowState(r),
117
+ ...(r.liveness === "live" ? { lastActiveAt: r.updatedAt } : r.diedAt !== undefined ? { offlineAt: r.diedAt } : {}),
118
+ ...(r.cwd !== undefined ? { cwd: cwdTail(r.cwd) } : {}),
119
+ ...(presence.agent !== undefined || presence.intent !== undefined ? { presence } : {}),
70
120
  });
71
121
  }
72
122
  let registryCapped = false;
@@ -101,7 +151,9 @@ export function createListAgentsTool(opts) {
101
151
  }
102
152
  const lines = rows.map((r) => {
103
153
  const label = r.kind === "peer_session" ? formatPeerNameRef(inlineUntrusted(r.name, 64), r.ref) : inlineUntrusted(r.name, 64);
104
- return `${label} · ${r.state} · ${r.kind === "peer_session" ? "peer session" : r.kind === "roster" ? "agent (durable record)" : "agent"} · ${r.address}`;
154
+ const age = r.kind !== "peer_session" ? undefined : r.lastActiveAt !== undefined ? `active ${formatAgo(now - r.lastActiveAt)} ago` : r.offlineAt !== undefined ? `offline ${formatAgo(now - r.offlineAt)} ago` : "offline";
155
+ const cols = [label, r.state, age, r.cwd !== undefined ? inlineUntrusted(r.cwd, 64) : undefined, r.presence !== undefined ? renderPresence(r.presence) : undefined, r.kind === "peer_session" ? "peer session" : r.kind === "roster" ? "agent (durable record)" : "agent", r.address];
156
+ return cols.filter((c) => c !== undefined).join(" · ");
105
157
  });
106
158
  const finishedCount = rows.filter((r) => r.state === "finished").length;
107
159
  const header = rows.length === 0 ? "No agents or peer sessions are listed right now." : `${rows.length} listed:`;
@@ -7,7 +7,7 @@ export type PeerAxisTag = "h" | "s" | "t";
7
7
  export type PeerRefusalCode = "rate_limited" | "duplicate" | "hop_loop" | "hop_runaway" | "queue_full";
8
8
  /** Gate verdict subset of {@link PeerRefusalCode} (what `admit` itself can refuse with). */
9
9
  export type PeerAdmissionRefusal = Exclude<PeerRefusalCode, "queue_full">;
10
- export interface PeerAdmissionConfig {
10
+ export interface PeerLaneConfig {
11
11
  /** Per-sender burst budget (bucket size). */
12
12
  bucketCapacity: number;
13
13
  /** Per-sender steady-state refill rate (tokens per second). */
@@ -27,12 +27,24 @@ export interface PeerAdmissionConfig {
27
27
  * senders restart with full buckets — no correctness debt (the gate holds no state that must
28
28
  * survive across messages). */
29
29
  maxTrackedRecipients: number;
30
+ /** Peer WAKE budget, HOST-enforced: how many times per hour a host may start a turn on a session
31
+ * whose `crossSessionInbound` resolves to `wake` because its box grew. The engine spells the
32
+ * number and the contract; the host that starts turns is the executor. Integer. */
33
+ maxWakesPerHour: number;
34
+ /** Peer delivery budget, ENGINE-enforced: how many box RECORDS one drain pass injects (held
35
+ * releases, notice lines, peer messages and engine frames count alike — one counter over every
36
+ * record injection); the rest stay leased for the next boundary, and the pass's own `inbox_notice`
37
+ * line — which is not a record — says how many. Integer. */
38
+ maxMessagesPerRound: number;
39
+ /** The `ListAgents` freshness window: a live row with no activity word is listed by default only
40
+ * while `now − updatedAt` is within it (`includeAll` lists the rest). Milliseconds. */
41
+ listingFreshWindowMs: number;
30
42
  }
31
43
  /** Upstream defaults, adopted verbatim (anchored; deviation would need evidence sema does not have). */
32
- export declare const PEER_ADMISSION_DEFAULTS: Readonly<PeerAdmissionConfig>;
44
+ export declare const PEER_LANE_DEFAULTS: Readonly<PeerLaneConfig>;
33
45
  /** Test seam (mirrors `__resetMaterializeEnvAnnouncements`); never called by production code.
34
46
  * Re-arms BOTH arms (a WeakMap has no clear — it is re-minted). */
35
- export declare function __resetPeerAdmissionRangeAnnouncements(): void;
47
+ export declare function __resetPeerLaneRangeAnnouncements(): void;
36
48
  /**
37
49
  * Resolve a caller's partial config against the defaults, per-field range-clamped-to-default.
38
50
  * There is no off switch: the guard is always on; the ranges ARE the host's tuning space.
@@ -46,7 +58,7 @@ export declare function __resetPeerAdmissionRangeAnnouncements(): void;
46
58
  * the deployment's sink; absent, the announcement still reaches `console.warn` through the shared
47
59
  * delivery throat (an unwired host is not a silent one).
48
60
  */
49
- export declare function resolvePeerAdmissionConfig(overrides?: Partial<PeerAdmissionConfig>, onNotice?: (notice: EngineNotice) => void): PeerAdmissionConfig;
61
+ export declare function resolvePeerLaneConfig(overrides?: Partial<PeerLaneConfig>, onNotice?: (notice: EngineNotice) => void): PeerLaneConfig;
50
62
  /** Hop-chain carrier sliding window: a chain keeps its most recent entries (upstream `Xbr`=32).
51
63
  * The chain grows ONLY at send time (the sender appends its own token); delivery/replay never
52
64
  * appends — a message-driven revival is not a hop. */
@@ -135,10 +147,10 @@ export interface PeerAdmission {
135
147
  * records the body IN THIS CALL (no rollback protocol — a downstream delivery failure does not
136
148
  * refund; the bucket refills on its own). Refusal has zero side effects. Config is read
137
149
  * per call (state is long-lived, values are current — upstream form). */
138
- admit(req: PeerAdmissionRequest, config: PeerAdmissionConfig): PeerAdmissionVerdict;
150
+ admit(req: PeerAdmissionRequest, config: PeerLaneConfig): PeerAdmissionVerdict;
139
151
  /** Hop-only check (the identity-less-sender arm, and any caller that needs the chain verdict
140
152
  * without touching rate/dedup state). */
141
- checkHopChain(prospectiveChain: readonly string[], ownTokens: readonly string[], config: PeerAdmissionConfig): PeerAdmissionVerdict;
153
+ checkHopChain(prospectiveChain: readonly string[], ownTokens: readonly string[], config: PeerLaneConfig): PeerAdmissionVerdict;
142
154
  /** Observation face: senders currently tracked by this recipient instance. */
143
155
  trackedSenderCount(): number;
144
156
  /** Observation face: refusals by reason since this instance was created. */
@@ -149,7 +161,7 @@ export interface PeerAdmissionOptions {
149
161
  now?: () => number;
150
162
  }
151
163
  export declare function createPeerAdmission(options?: PeerAdmissionOptions): PeerAdmission;
152
- export declare function peerAdmissionFor(scope: string | undefined, recipientKey: string, config: PeerAdmissionConfig, options?: PeerAdmissionOptions): PeerAdmission;
164
+ export declare function peerAdmissionFor(scope: string | undefined, recipientKey: string, config: PeerLaneConfig, options?: PeerAdmissionOptions): PeerAdmission;
153
165
  /**
154
166
  * The delivery legs' ONE admission entry: registry seat-commit follows the SAME
155
167
  * refusal-is-side-effect-free rule as the sender table inside `admit` — the instance is looked up
@@ -166,7 +178,7 @@ export declare function peerAdmissionFor(scope: string | undefined, recipientKey
166
178
  * sent would read the send-point body record and refuse its own message as `duplicate`. Two gate
167
179
  * instances, one per stage; the stage-less key is the pre-385 spelling and stays byte-identical. */
168
180
  export type PeerAdmissionStage = "send" | "drain";
169
- export declare function judgePeerAdmission(scope: string | undefined, recipientKey: string, req: PeerAdmissionRequest, config: PeerAdmissionConfig, options?: PeerAdmissionOptions, stage?: PeerAdmissionStage): PeerAdmissionVerdict;
181
+ export declare function judgePeerAdmission(scope: string | undefined, recipientKey: string, req: PeerAdmissionRequest, config: PeerLaneConfig, options?: PeerAdmissionOptions, stage?: PeerAdmissionStage): PeerAdmissionVerdict;
170
182
  /** Test seam ONLY (module-level registry hygiene between test files — same posture as the lane map,
171
183
  * which drains itself; this one is long-lived by design so tests reset it explicitly). */
172
184
  export declare function resetPeerAdmissionRegistryForTests(): void;
@@ -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;