@sema-agent/core 7.29.0 → 7.30.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (158) hide show
  1. package/CHANGELOG.md +393 -0
  2. package/dist/agents/child-model-seat.d.ts +59 -7
  3. package/dist/agents/child-model-seat.js +33 -7
  4. package/dist/agents/cross-session-judge.d.ts +27 -5
  5. package/dist/agents/cross-session-judge.js +23 -12
  6. package/dist/agents/list-agents-tool.d.ts +31 -6
  7. package/dist/agents/list-agents-tool.js +66 -14
  8. package/dist/agents/peer-admission.d.ts +20 -8
  9. package/dist/agents/peer-admission.js +21 -12
  10. package/dist/agents/peer-directory.d.ts +29 -0
  11. package/dist/agents/peer-directory.js +20 -0
  12. package/dist/agents/peer-held-queue.d.ts +31 -4
  13. package/dist/agents/peer-held-queue.js +32 -1
  14. package/dist/agents/peer-session-drain.d.ts +91 -13
  15. package/dist/agents/peer-session-drain.js +409 -102
  16. package/dist/agents/read-inbox-tool.d.ts +22 -0
  17. package/dist/agents/read-inbox-tool.js +39 -0
  18. package/dist/agents/roster-store.d.ts +2 -4
  19. package/dist/agents/roster-store.js +1 -1
  20. package/dist/agents/send-message-tool.d.ts +4 -4
  21. package/dist/agents/send-message-tool.js +22 -2
  22. package/dist/agents/subagent.js +62 -44
  23. package/dist/agents/team-role.d.ts +34 -0
  24. package/dist/agents/team-role.js +17 -0
  25. package/dist/agents/verify.js +2 -2
  26. package/dist/core/auto-compaction.d.ts +42 -0
  27. package/dist/core/auto-compaction.js +35 -2
  28. package/dist/core/child-clamp-seats.d.ts +11 -0
  29. package/dist/core/child-clamp-seats.js +3 -0
  30. package/dist/core/context-edit.d.ts +3 -2
  31. package/dist/core/context-edit.js +6 -5
  32. package/dist/core/context-guard.d.ts +2 -1
  33. package/dist/core/context-guard.js +3 -2
  34. package/dist/core/context-settings.d.ts +103 -0
  35. package/dist/core/context-settings.js +60 -0
  36. package/dist/core/engine-notice.d.ts +11 -1
  37. package/dist/core/frame-settlement.d.ts +97 -0
  38. package/dist/core/frame-settlement.js +36 -0
  39. package/dist/core/governance-codes.d.ts +1 -1
  40. package/dist/core/governance-codes.js +2 -0
  41. package/dist/core/hooks.d.ts +10 -10
  42. package/dist/core/mailbox-store.d.ts +41 -7
  43. package/dist/core/mailbox-store.js +9 -3
  44. package/dist/core/parent-spec-seat.d.ts +14 -1
  45. package/dist/core/parent-spec-seat.js +6 -0
  46. package/dist/core/permission-rule-model.js +1 -1
  47. package/dist/core/roles.d.ts +20 -0
  48. package/dist/core/roles.js +35 -16
  49. package/dist/core/runner/assemble-result.d.ts +2 -0
  50. package/dist/core/runner/assemble-result.js +1 -1
  51. package/dist/core/runner/compaction-call-options.d.ts +26 -2
  52. package/dist/core/runner/compaction-call-options.js +9 -0
  53. package/dist/core/runner/compaction-seams.d.ts +4 -3
  54. package/dist/core/runner/compaction-seams.js +13 -6
  55. package/dist/core/runner/contracts.d.ts +56 -7
  56. package/dist/core/runner/decide-continuation.d.ts +2 -1
  57. package/dist/core/runner/decide-continuation.js +11 -10
  58. package/dist/core/runner/initial-run-state.js +1 -1
  59. package/dist/core/runner/prepare-caps-and-workflow.js +9 -1
  60. package/dist/core/runner/prepare-config-doors.js +4 -1
  61. package/dist/core/runner/prepare-context-lane.d.ts +9 -2
  62. package/dist/core/runner/prepare-context-lane.js +15 -10
  63. package/dist/core/runner/prepare-delegation-surface.d.ts +3 -2
  64. package/dist/core/runner/prepare-delegation-surface.js +9 -3
  65. package/dist/core/runner/prepare-file-history.js +10 -8
  66. package/dist/core/runner/prepare-gate-stations.d.ts +4 -0
  67. package/dist/core/runner/prepare-gate-stations.js +3 -1
  68. package/dist/core/runner/prepare-inherited-gate.js +1 -0
  69. package/dist/core/runner/prepare-prompt-inputs.d.ts +6 -0
  70. package/dist/core/runner/prepare-prompt-inputs.js +2 -1
  71. package/dist/core/runner/prepare-task.js +22 -9
  72. package/dist/core/runner/prepare-wiring-manifest.d.ts +6 -1
  73. package/dist/core/runner/prepare-wiring-manifest.js +6 -1
  74. package/dist/core/runner/retired-runner-deps.d.ts +3 -1
  75. package/dist/core/runner/retired-runner-deps.js +1 -0
  76. package/dist/core/runner/run-compaction-machinery.d.ts +4 -2
  77. package/dist/core/runner/run-compaction-machinery.js +5 -3
  78. package/dist/core/runner/run-harness-handlers.js +1 -1
  79. package/dist/core/runner/run-identity-wiring.js +19 -1
  80. package/dist/core/runner/run-leg.js +1 -1
  81. package/dist/core/runner/run-notification-lane.js +8 -2
  82. package/dist/core/runner/run-recovery-lanes.js +9 -5
  83. package/dist/core/runner/run-settle-and-teardown.d.ts +4 -22
  84. package/dist/core/runner/run-settle-and-teardown.js +6 -10
  85. package/dist/core/runner/run-stop-and-final-verify.js +1 -0
  86. package/dist/core/runner/run-terminal-adoption.js +5 -5
  87. package/dist/core/runner/run-turn-boundary.js +32 -5
  88. package/dist/core/runner/runtask.d.ts +8 -1
  89. package/dist/core/runner/runtask.js +32 -1
  90. package/dist/core/runner-deps.d.ts +44 -13
  91. package/dist/core/secret-env.js +2 -1
  92. package/dist/core/swappable-deps.d.ts +20 -4
  93. package/dist/core/swappable-deps.js +6 -1
  94. package/dist/core/task-event.d.ts +5 -0
  95. package/dist/core/task-notification.d.ts +144 -2
  96. package/dist/core/task-notification.js +154 -5
  97. package/dist/core/task-result.d.ts +14 -0
  98. package/dist/core/task-spec.d.ts +7 -0
  99. package/dist/core/tool-catalog-entries.js +2 -1
  100. package/dist/core/tool-conformance.js +1 -0
  101. package/dist/core/tool-policy.d.ts +9 -4
  102. package/dist/core/tool-spec.d.ts +14 -0
  103. package/dist/core/wiring-manifest.d.ts +12 -0
  104. package/dist/core/wiring-manifest.js +5 -1
  105. package/dist/engine/harness/agent-harness.d.ts +14 -1
  106. package/dist/engine/harness/agent-harness.js +28 -3
  107. package/dist/engine/harness/types.d.ts +3 -1
  108. package/dist/engine/llm/types.d.ts +8 -7
  109. package/dist/engine/llm/types.js +1 -1
  110. package/dist/engine/lsp/node-lsp-manager.d.ts +3 -0
  111. package/dist/engine/lsp/node-lsp-manager.js +5 -3
  112. package/dist/engine/session/import-validate.js +4 -0
  113. package/dist/index.d.ts +10 -7
  114. package/dist/index.js +7 -5
  115. package/dist/orchestration/run-workflow-tool.d.ts +4 -4
  116. package/dist/orchestration/workflow.d.ts +1 -1
  117. package/dist/orchestration/workflow.js +8 -5
  118. package/dist/shell/lex.d.ts +38 -54
  119. package/dist/shell/lex.js +239 -286
  120. package/dist/stores/cc/mailbox-store.js +3 -2
  121. package/dist/stores/file/corrupt-read-sink.d.ts +28 -2
  122. package/dist/stores/file/file-history-store.d.ts +2 -4
  123. package/dist/stores/file/file-history-store.js +1 -1
  124. package/dist/stores/file/fs-atomic.d.ts +16 -11
  125. package/dist/stores/file/fs-atomic.js +17 -6
  126. package/dist/stores/file/index.d.ts +5 -6
  127. package/dist/stores/file/index.js +1 -1
  128. package/dist/stores/file/mailbox-store.d.ts +4 -9
  129. package/dist/stores/file/mailbox-store.js +29 -30
  130. package/dist/stores/file/session-policy-store.d.ts +2 -0
  131. package/dist/stores/file/session-policy-store.js +1 -1
  132. package/dist/stores/file/session-store.d.ts +11 -7
  133. package/dist/stores/file/session-store.js +23 -16
  134. package/dist/stores/file/shared-ledger.js +3 -14
  135. package/dist/stores/file/tool-result-store.d.ts +2 -0
  136. package/dist/stores/file/tool-result-store.js +5 -2
  137. package/dist/stores/file/workflow-journal-store.js +14 -9
  138. package/dist/tools/fs/bash-program-position.d.ts +15 -24
  139. package/dist/tools/fs/bash-program-position.js +11 -29
  140. package/dist/tools/fs/bash-readonly-classifier.d.ts +131 -117
  141. package/dist/tools/fs/bash-readonly-classifier.js +404 -272
  142. package/dist/tools/fs/shell-read-check.js +3 -3
  143. package/dist/tools/fs/shell-read-establish.d.ts +1 -1
  144. package/dist/tools/fs/shell-read-establish.js +5 -33
  145. package/dist/tools/fs/shell-read-identity.d.ts +13 -0
  146. package/dist/tools/fs/shell-read-identity.js +39 -0
  147. package/dist/tools/fs/shell-read-judge.d.ts +8 -0
  148. package/dist/tools/fs/shell-read-judge.js +7 -0
  149. package/dist/tools/fs/shell-read-plan.d.ts +15 -6
  150. package/dist/tools/fs/shell-read-plan.js +171 -195
  151. package/dist/tools/fs/shell-read-word.d.ts +64 -0
  152. package/dist/tools/fs/shell-read-word.js +94 -0
  153. package/dist/tools/fs/shell-reentry-text.d.ts +10 -0
  154. package/dist/tools/fs/shell-reentry-text.js +73 -0
  155. package/package.json +1 -1
  156. package/test/export-surface.snapshot.json +58 -8
  157. package/wire-changes/core@7.30.0.json +1821 -0
  158. package/wire-changes/core@7.30.1.json +154 -0
@@ -6,6 +6,7 @@
6
6
  */
7
7
  import type { Model } from "../internal/llm.js";
8
8
  import type { AgentDefinition } from "./agent-definition.js";
9
+ import type { ContextSettings } from "./context-settings.js";
9
10
  import type { BackgroundChildEvent, DelegationLifecycleEvent } from "./delegation-frames.js";
10
11
  import type { EngineNotice } from "./engine-notice.js";
11
12
  import type { HandsBandOptions } from "./hands-band.js";
@@ -113,6 +114,22 @@ export interface RunnerDeps {
113
114
  brain: Brain;
114
115
  /** Catalog used to resolve string ModelRefs to Model objects. */
115
116
  models?: Record<string, Model>;
117
+ /**
118
+ * The deployment's per-model CONTEXT-WINDOW overlay, keyed by CATALOG WORD ({@link ContextSettings}):
119
+ * `contextTokens` / `autoCompactTokens` / `enabled` over what the model card declares.
120
+ *
121
+ * It is the ONE hot-swappable seat an IN-FLIGHT leg re-reads. Every other seat follows the snapshot law
122
+ * (a running task keeps the deps object it prepared under) because those seats are the routing card —
123
+ * endpoint, credentials, price, fence — and a request in flight must reach what it was priced for. The
124
+ * usable window is not the card: it is a setting, and an operator who lowers it is asking the legs
125
+ * running RIGHT NOW to compact sooner, which is exactly what they were unable to do while the number
126
+ * lived on the model object. Read at every turn boundary, every recovery pass, the end-of-task summary
127
+ * and every request build; clamped to the physical window by the one resolver.
128
+ *
129
+ * A leg that was handed a `Model` OBJECT (or resolved one through a `select` criteria) has no catalog
130
+ * word and therefore no overlay — the pin is the caller saying "this exact model, as declared".
131
+ */
132
+ contextSettings?: ContextSettings;
116
133
  /**
117
134
  * design/338 (mid-turn MCP revocation) — the HOST's revocation ledger, probed synchronously at
118
135
  * every MCP dispatch (tool call + the three resource tools) BEFORE the transport. The engine
@@ -512,9 +529,11 @@ export interface RunnerDeps {
512
529
  * {@link InMemoryFileHistoryStore}; a deployment supplies a durable backend behind the same seam
513
530
  * (backends MUST reuse the exported env-half helpers — see the contract JSDoc). Works against ANY
514
531
  * ExecutionEnv (local or remote): bytes are read/written through the env's FileSystem face and
515
- * stored host-side, so an env rebuild does not lose history.
532
+ * stored host-side, so an env rebuild does not lose history. `"disabled"` = the deployment DECIDED
533
+ * not to capture: the omitted-key semantics minus the once-per-Runner `onError` advisory an omitted
534
+ * key earns on a filesystem-capable run (`classification: "file-history-no-store"`).
516
535
  */
517
- fileHistoryStore?: import("./file-history-store.js").FileHistoryStore;
536
+ fileHistoryStore?: import("./file-history-store.js").FileHistoryStore | "disabled";
518
537
  /**
519
538
  * design/381 DV-14 (终审落锤) — what an Edit/Write/NotebookEdit does when its FIRST-TOUCH history
520
539
  * record cannot be persisted (`FileHistoryStore.trackEdit` answered `{ok:false}` = NO durable
@@ -877,12 +896,12 @@ export interface RunnerDeps {
877
896
  maxConcurrent?: number;
878
897
  maxCumulativePerSession?: number;
879
898
  };
880
- /** design/176 — deployment tuning for the always-on peer-message admission gate (SendMessage entry
881
- * judgment: rate/dedup/hop-chain/queue bounds). Per-field range-validated against the upstream
882
- * table with out-of-range values falling back to that field's default; there is NO off switch —
883
- * absent means the default table, not "guard off". Threaded into the auto-mounted SendMessage as
884
- * `SendMessageToolOptions.admission`; read per call (a value change governs the next message). */
885
- peerAdmission?: Partial<import("../agents/peer-admission.js").PeerAdmissionConfig>;
899
+ /** The peer lane's ONE knob table (`PeerLaneConfig`): the always-on admission gate (rate/dedup/hop-chain/
900
+ * queue bounds), the wake and per-pass delivery budgets, and the `ListAgents` freshness window. Per-field
901
+ * range-validated with out-of-range values falling back to that field's default, announced once; there
902
+ * is NO off switch — absent means the default table. Threaded into the auto-mounted SendMessage as
903
+ * `SendMessageToolOptions.lane` and into the session-box drain; read per call / per pass. */
904
+ peerLane?: Partial<import("../agents/peer-admission.js").PeerLaneConfig>;
886
905
  /**
887
906
  * design/385 §2.1 / §5.1 — the peer-session DIRECTORY: the discovery truth of the cross-session
888
907
  * lane (other sessions of this engine for the same user). A HOST implements the contract (cli: the
@@ -917,6 +936,16 @@ export interface RunnerDeps {
917
936
  * {@link peerDirectory}.
918
937
  */
919
938
  crossSessionDialogExpiry?: import("../agents/peer-notices.js").CrossSessionDialogExpiry | (() => import("../agents/peer-notices.js").CrossSessionDialogExpiry | undefined);
939
+ /**
940
+ * The deployment's CAPABILITY FLOOR for durable engine frames: `true` declares that EVERY process
941
+ * that drains this deployment's session boxes reads `engine_frame` records (this engine's codec or
942
+ * newer). With it, an engine-minted task-notification frame addressed to an idle session parks in
943
+ * the session's durable box (`MailboxPeerMeta.kind: "engine_frame"`) and survives a process restart;
944
+ * without it (absent / `false`) such a frame parks in process exactly as before. A deployment flips
945
+ * it only after every drainer is upgraded — a pre-codec drainer reads the record as a peer's words.
946
+ * Inert without a cross-process-safe {@link mailboxStore}.
947
+ */
948
+ sessionBoxEngineFrames?: boolean;
920
949
  /**
921
950
  * design/164 件五 — DEPLOYMENT-level usage governance: allowances that span TASKS, evaluated per
922
951
  * principal (or once for the whole deployment when a task declares none). A different axis from
@@ -1266,11 +1295,13 @@ export interface RunnerDeps {
1266
1295
  phase: "compaction" | "prompt-cache" | "prompt-constitution" | "degraded" | "config" | "memory" | "mcp" | "a2a" | "interrupt-reconcile" | "suggestions" | "rewind" | "hook";
1267
1296
  sessionId: string;
1268
1297
  /**
1269
- * Machine-readable sub-classification when the phase has one — currently the design/31
1270
- * cache-break detector's root cause (`"model-switch" | "tool-schema" | "tool-set" |
1271
- * "system-prefix" | "server-or-ttl"`). Lets the host pick its own log level: `"server-or-ttl"`
1272
- * is usually benign in agentic tasks (slow tools ⇒ 5min+ request gaps expire provider caches —
1273
- * service[64] flag②) and warrants info/warn, while the prefix-bug causes warrant attention.
1298
+ * Machine-readable sub-classification when the site has one (a kebab-case word naming the arm) —
1299
+ * e.g. the design/31 cache-break detector's root cause (`"model-switch" | "tool-schema" |
1300
+ * "tool-set" | "system-prefix" | "server-or-ttl"`), or the file-history liveness arms
1301
+ * (`"file-history-no-store"` / `"file-history-no-env"`). Lets the host pick its own log level:
1302
+ * `"server-or-ttl"` is usually benign in agentic tasks (slow tools ⇒ 5min+ request gaps expire
1303
+ * provider caches — service[64] flag②) and warrants info/warn, while the prefix-bug causes
1304
+ * warrant attention.
1274
1305
  */
1275
1306
  classification?: string;
1276
1307
  }) => void;
@@ -1,4 +1,5 @@
1
- const SECRET_ENV_RE = /(^|_)(KEY|TOKEN|SECRET|SECRETS|PASSWORD|PASSWD|CREDENTIAL|CREDENTIALS|PRIVATEKEY|APIKEY|ACCESS_KEY|SESSION_TOKEN)(?:_(?:\d+|ID))?$/i;
1
+ const TOKEN_COUNT_WORDS = ["MAX", "OUTPUT", "INPUT", "THINKING", "NUM", "TOTAL"];
2
+ const SECRET_ENV_RE = new RegExp(`(^|_)(KEY|KEYS|TOKEN|(?<!(?:^|_)(?:${TOKEN_COUNT_WORDS.join("|")})_)TOKENS|SECRET|SECRETS|PASSWORD|PASSWORDS|PASSWD|CREDENTIAL|CREDENTIALS|PRIVATEKEY|APIKEY|ACCESS_KEY|SESSION_TOKEN)(?:_(?:\\d+|ID))?$`, "i");
2
3
  const SECRET_ENV_EXACT_NAMES = new Set([
3
4
  "DATABASE_URL", "CONNECTION_STRING", "DSN", "NPM_AUTH",
4
5
  "PGPASSWORD", "MYSQL_PWD", "SSHPASS", "GPG_PASSPHRASE", "GH_PAT", "GITHUB_PAT",
@@ -12,11 +12,23 @@
12
12
  * the door refuses a key outside the set at run time (never a silent ignore — a hot-config driver
13
13
  * handing `{ brain }` must learn that the brain is not a swappable seat, not watch nothing happen).
14
14
  *
15
- * WHAT A SWAP MEANS — the natural snapshot, one sentence for every seat: a running task reads the deps
16
- * object it was handed at its prepare and holds that reference; a swap replaces the Runner's object, so
15
+ * WHAT A SWAP MEANS — the natural snapshot, one sentence for every ROUTING seat: a running task reads the
16
+ * deps object it was handed at its prepare and holds that reference; a swap replaces the Runner's object, so
17
17
  * every LATER prepare reads the new seat and every in-flight leg keeps the value it prepared under.
18
18
  * Nothing is re-routed, re-priced or re-fenced mid-run; the divergence window is exactly the in-flight
19
- * legs' lifetime, by design (the `swapModels` precedent, now the rule for the whole set).
19
+ * legs' lifetime, by design (the `swapModels` precedent).
20
+ *
21
+ * …WITH ONE SEAT OUTSIDE IT, and the boundary is the point rather than the exception: `contextSettings` is
22
+ * read LIVE by in-flight legs. The snapshot law protects the routing CARD — a request already in flight must
23
+ * reach the endpoint it was priced, credentialed and fenced for, and re-reading any of that mid-run buys
24
+ * nothing and breaks the request. A leg's usable WINDOW is not on that card: it is a setting about how much
25
+ * conversation may accumulate before the leg summarizes, the reference implementation keeps it off the model
26
+ * object for exactly this reason and re-reads it on every request (clamped to the model's physical window),
27
+ * and an operator who lowers it is asking the legs running RIGHT NOW to compact sooner — which is the whole
28
+ * request. So the window declarations live on their own seat (`context-settings.ts`), keyed by catalog word,
29
+ * and the legs read them at every boundary, every recovery pass, the end-of-task summary and every request
30
+ * build. Routing stays snapshotted; geometry follows the operator. The two are not the same question, and
31
+ * one law over both could only be wrong about one of them.
20
32
  *
21
33
  * KEY PRESENCE IS THE INSTRUCTION: a key present on the candidate is REPLACED (an explicit `undefined`
22
34
  * CLEARS an optional seat back to its engine default — `readFace: undefined` ⇒ the deployment declares
@@ -44,8 +56,11 @@ import type { RunnerDeps } from "./runner-deps.js";
44
56
  * deployment seat that resolution reads.
45
57
  * - `writeFace` — the deployment's write-face declaration ({@link RunnerDeps.writeFace}); the read seat's
46
58
  * twin (same value gate, same in-flight snapshot rule, its own notice) and independent of it.
59
+ * - `contextSettings` — the per-model context-window overlay ({@link RunnerDeps.contextSettings}); screened
60
+ * by the same value gate construction applies (`assertContextSettingsValue`). THE ONE SEAT
61
+ * IN-FLIGHT LEGS RE-READ — see the header: it carries geometry, not route.
47
62
  */
48
- export declare const SWAPPABLE_DEP_SEATS: readonly ["models", "tiers", "readFace", "writeFace"];
63
+ export declare const SWAPPABLE_DEP_SEATS: readonly ["models", "tiers", "readFace", "writeFace", "contextSettings"];
49
64
  export type SwappableDepSeat = (typeof SWAPPABLE_DEP_SEATS)[number];
50
65
  /** The candidate a swap takes: the swappable seats of {@link RunnerDeps}, each optional (presence = instruction). */
51
66
  export type SwappableDeps = Pick<RunnerDeps, SwappableDepSeat>;
@@ -61,6 +76,7 @@ export declare const SWAP_SEAT_NOTICE: {
61
76
  readonly tiers: "config.models_swapped";
62
77
  readonly readFace: "config.read_face_swapped";
63
78
  readonly writeFace: "config.write_face_swapped";
79
+ readonly contextSettings: "config.context_settings_swapped";
64
80
  };
65
81
  /** The fence over the notice table: `never` while every seat has a row. */
66
82
  export type SwapNoticeTableCoversEverySeat = AssertAllKeysHandled<Exclude<SwappableDepSeat, keyof typeof SWAP_SEAT_NOTICE>>;
@@ -1,5 +1,6 @@
1
1
  import { assertContainmentFaceValue } from "../tools/fs/containment-face.js";
2
- export const SWAPPABLE_DEP_SEATS = ["models", "tiers", "readFace", "writeFace"];
2
+ import { assertContextSettingsValue } from "./context-settings.js";
3
+ export const SWAPPABLE_DEP_SEATS = ["models", "tiers", "readFace", "writeFace", "contextSettings"];
3
4
  const SWAPPABLE_DEP_SEAT_SET = new Set(SWAPPABLE_DEP_SEATS);
4
5
  export function isSwappableDepSeat(k) {
5
6
  return SWAPPABLE_DEP_SEAT_SET.has(k);
@@ -9,6 +10,7 @@ export const SWAP_SEAT_NOTICE = {
9
10
  tiers: "config.models_swapped",
10
11
  readFace: "config.read_face_swapped",
11
12
  writeFace: "config.write_face_swapped",
13
+ contextSettings: "config.context_settings_swapped",
12
14
  };
13
15
  export function screenSwappableDeps(next) {
14
16
  if (next === null || typeof next !== "object" || Array.isArray(next)) {
@@ -49,6 +51,9 @@ export function screenSwappableDeps(next) {
49
51
  case "writeFace":
50
52
  candidate.writeFace = assertContainmentFaceValue(v, "write", "deps");
51
53
  break;
54
+ case "contextSettings":
55
+ candidate.contextSettings = assertContextSettingsValue(v, "swapDeps");
56
+ break;
52
57
  default: {
53
58
  const _exhaustive = seat;
54
59
  throw new Error(`swapDeps: unhandled seat ${String(_exhaustive)}`);
@@ -629,6 +629,11 @@ export type TaskEvent = ({
629
629
  preserved_segment?: {
630
630
  firstKeptEntryId: string;
631
631
  };
632
+ /** Cross-session message frames that sat in the SUMMARIZED prefix of this pass — delivered
633
+ * earlier and visible to the model for the turns since. A record still in the session's box is
634
+ * handed over again by the engine (once; a second loss is disclosed `resupply_exhausted`), so
635
+ * this counts what the summary swallowed, not what is gone. Present iff at least one. */
636
+ summarizedCrossSessionMessages?: number;
632
637
  /** CC post-compact file-restore parity (2026-07-03): the working files the engine
633
638
  * re-read into the summary (attachWorkingFiles), attachment order. A shell renders CC's
634
639
  * `Read {path} (…)` cards under the compaction divider from this. Absent when the feature is
@@ -107,6 +107,54 @@ export interface TaskNotificationPayload {
107
107
  * semantics as the poll face's `partial_result` flag (one meaning across both surfaces). Absent on
108
108
  * every other notification (a completed result is never flagged). */
109
109
  partial?: boolean;
110
+ /** Delegation lanes, terminal frames: the child's EXPLICIT `Agent({name})` spawn key — the same word
111
+ * its `task_progress.spawnName`, roster row and hook identity carry, rendered as `<agent-name>` so
112
+ * the parent can `SendMessage({to})` it by name. Minted from the identity the frame's producer holds
113
+ * (never from the description); ABSENT = an unnamed delegation. */
114
+ spawnName?: string;
115
+ /** Delegation lanes, terminal frames — how the child's report reached this frame under the
116
+ * hand-back contract: `send` = a report was delivered; `flagged` = delivered under the hand-back
117
+ * screening's SECURITY WARNING; `withheld` = nothing was delivered (no result text, no partial text).
118
+ * Absent on non-terminal frames and on the non-delegation lanes. The model face renders a
119
+ * `<handback>` line only for `flagged` / `withheld` — the parent is told when a report did not
120
+ * arrive plainly, and nothing for the ordinary case. */
121
+ handback?: HandbackState;
122
+ /** Delegation lanes, terminal frames, present iff the child's leg compacted its context at least once
123
+ * or opened its compaction breaker: `passes` = landed compactions, `breakerOpen` = compaction was
124
+ * disabled for the rest of the leg (later context was trimmed, not summarized), `kind` = the last
125
+ * landed pass's trigger. The parent otherwise cannot tell that a child forgot. */
126
+ compaction?: {
127
+ passes: number;
128
+ breakerOpen: boolean;
129
+ kind?: CompactionKind;
130
+ };
131
+ /**
132
+ * The INBOX NOTICE carrier: one drain pass of this session's box stopped with records still waiting
133
+ * (the pass budget, an admission stop, a lane refusal). `pending` = records still leased and not yet
134
+ * injected when the pass ended; `delivered` = records injected this pass; `urgent` = how many of the
135
+ * waiting ones their sender marked urgent (absent = 0); `box` = the session box. Its PRESENCE is the
136
+ * render discriminator: the model reads ONE engine line (`[Inbox notice] …`), never a shell. Minted
137
+ * ONLY by the engine's session-box drain, keyed `task_id: "inbox:<box>"` with the pass counter as
138
+ * `seq` so it never collides with a message frame's key; absent when the pass emptied the box.
139
+ */
140
+ inboxNotice?: {
141
+ pending: number;
142
+ delivered: number;
143
+ urgent?: number;
144
+ resupplied?: number;
145
+ box: string;
146
+ };
147
+ /** #1036 — THIS occurrence is a RE-SUPPLY of a record the model was already shown once and
148
+ * whose frame a compaction then summarized away. Same record, same `task_id`, same `seq`, a new
149
+ * occurrence: `attempt` says which one. ABSENT = not a re-supply (a first delivery); there is no
150
+ * `false` form. Only the session-box drain mints it. */
151
+ resupplied?: true;
152
+ /** #1036 — which DELIVERY ATTEMPT of this record's frame this occurrence is: 1 = the first
153
+ * delivery, 2 = the one re-supply a summarized record gets. ABSENT = 1 (there is no `0`, and a
154
+ * consumer must not read absence as "unknown"). It folds into the injection queue's dedup key
155
+ * ({@link taskNotificationDedupKey}), which is what keeps a re-supply from collapsing into the
156
+ * first occurrence's entry. Only the session-box drain mints it. */
157
+ attempt?: number;
110
158
  output_file?: string;
111
159
  /** Whatever the settling lane knows about its own spend, JSON-rendered verbatim into one `<usage>`
112
160
  * tag. Deliberately untyped — the lanes report different quantities and nothing here should force
@@ -223,7 +271,7 @@ export interface SemaProvenance {
223
271
  /** `agent_message` = the same-process uplink (§1.4 d1); `cross_session_message` = the session-box
224
272
  * drain (§4.1) — `from` is then the sender's ADDRESS, `taskId` the recipient's own box handle,
225
273
  * `seq` the box seq, and the typed peer record rides in `peerMeta`. */
226
- kind: "agent_message" | "cross_session_message" | "cross_session_notice";
274
+ kind: "agent_message" | "cross_session_message" | "cross_session_notice" | "inbox_notice";
227
275
  from: string;
228
276
  taskId: string;
229
277
  seq: number;
@@ -297,7 +345,7 @@ export interface SystemInjection<TPayload = unknown> {
297
345
  * entry. A producer that wants a NEW occurrence delivered under a different tier mints a fresh
298
346
  * `seq` (the documented repeat discipline); silently migrating a queued frame between lanes on a
299
347
  * repeat, or refusing the repeat typed, would each turn a dedup fold into a delivery mutation. */
300
- export declare function taskNotificationDedupKey(n: Pick<TaskNotificationPayload, "task_id" | "task_type" | "status" | "seq">): string;
348
+ export declare function taskNotificationDedupKey(n: Pick<TaskNotificationPayload, "task_id" | "task_type" | "status" | "seq" | "attempt">): string;
301
349
  /**
302
350
  * The lane-scoped identity of a task, extracted from {@link taskNotificationDedupKey} so every
303
351
  * place that treats "same task" as one bucket applies the SAME lane rule.
@@ -349,7 +397,17 @@ export declare function renderCrossSessionMessageFrame(frame: NonNullable<TaskNo
349
397
  * defusal runs once more here so no carrier can smuggle a system-reminder into the model face.
350
398
  */
351
399
  export declare function renderCrossSessionNoticeLine(notice: NonNullable<TaskNotificationPayload["crossSessionNotice"]>): string;
400
+ /** #1036 — the ONE preamble a re-supplied occurrence carries, ahead of whatever carrier it is
401
+ * riding. It says the true thing (you read this once; the summary that replaced it did not keep it)
402
+ * without asking the model to reconcile two copies: the first occurrence is gone from the context. */
403
+ export declare const RESUPPLIED_FRAME_PREAMBLE = "[Re-supplied] You were shown this message once before; the compaction that replaced older context with a summary did not keep it, so the engine is handing it over again. It is the SAME message, not a new one \u2014 act on it once.";
352
404
  export declare function renderTaskNotificationXml(n: TaskNotificationPayload): string;
405
+ /** The one sentence a parent reads about a child that compacted: how often, on what trigger, and
406
+ * whether compaction gave up (after which older context was trimmed away rather than summarized). */
407
+ export declare function renderCompactionDisclosure(c: NonNullable<TaskNotificationPayload["compaction"]>): string;
408
+ /** The `[Inbox notice]` line — counts only, engine-authored; the records themselves arrive on their
409
+ * own frames (this pass's) or the next passs', and the pull tool reads them on demand. */
410
+ export declare function renderInboxNoticeLine(n: NonNullable<TaskNotificationPayload["inboxNotice"]>): string;
353
411
  /**
354
412
  * The BETWEEN-TURNS pending lane. A task notification born while the session has NO live lane at all
355
413
  * (every run of it is over, or the one that is live rejected the injection because its agent was
@@ -398,9 +456,75 @@ export interface SessionBoxBinding {
398
456
  handle: string;
399
457
  /** The session id the box belongs to — the `fromRoot` a durably parked child frame records. */
400
458
  sessionId: string;
459
+ /** The deployment's engine-frame floor (`RunnerDeps.sessionBoxEngineFrames`): `true` ⇒ an engine
460
+ * frame parks in this box as an `engine_frame` record; otherwise it parks in process. */
461
+ engineFrames?: boolean;
401
462
  /** Where an append fault is disclosed (the frame then takes the in-process park — never lost). */
402
463
  onError?: (e: unknown) => void;
403
464
  }
465
+ /** The hand-back contract's three words (see {@link TaskNotificationPayload.handback}). */
466
+ export declare const HANDBACK_STATES: readonly ["send", "flagged", "withheld"];
467
+ export type HandbackState = (typeof HANDBACK_STATES)[number];
468
+ /** THE hand-back projection, one rule every terminal frame producer calls: a warning ⇒ `flagged`; any
469
+ * report or partial text ⇒ `send`; neither ⇒ `withheld`. */
470
+ export declare function handbackStateOf(facts: {
471
+ result?: string | undefined;
472
+ partialText?: string | undefined;
473
+ warning?: string | undefined;
474
+ }): HandbackState;
475
+ /** The three-way compaction trigger word a terminal frame carries (the wire's `forced` is `reactive` here). */
476
+ export declare const COMPACTION_KINDS: readonly ["manual", "auto", "reactive"];
477
+ export type CompactionKind = (typeof COMPACTION_KINDS)[number];
478
+ /** The wire trigger word → the frame's kind word (`forced` = a reactive pass the run could not skip). */
479
+ export declare function compactionKindOf(trigger: "auto" | "manual" | "forced"): CompactionKind;
480
+ /**
481
+ * THE projection every delegation terminal frame calls for its identity + hand-back + context facets:
482
+ * the spawn key from the identity the producer holds (never the description), the hand-back state from
483
+ * the delivered text and the screening's warning, and the child's compaction record verbatim. Absent
484
+ * inputs stay absent; `handback` is always minted (a terminal always says how the report reached it).
485
+ */
486
+ export declare function terminalFrameFacets(facts: {
487
+ spawnName?: string | undefined;
488
+ result?: string | undefined;
489
+ partialText?: string | undefined;
490
+ warning?: string | undefined;
491
+ compaction?: TaskNotificationPayload["compaction"] | undefined;
492
+ }): Pick<TaskNotificationPayload, "spawnName" | "handback" | "compaction">;
493
+ /** The codec version this engine writes into an `engine_frame` record and the highest it reads. */
494
+ export declare const ENGINE_FRAME_CODEC_VERSION = 1;
495
+ /** The engine's own lanes — the only frames the durable park may encode (an `external` event never is). */
496
+ export declare const ENGINE_FRAME_TASK_TYPES: readonly ["background_agent", "monitor", "workflow"];
497
+ /** May this frame take the durable engine-frame park? Its lane must be one of the engine's own and it
498
+ * must wear no shell-less carrier. */
499
+ export declare function isEngineFrameParkable(payload: TaskNotificationPayload): boolean;
500
+ /** The record text of a durably parked engine frame: the codec version, the injection tier it was
501
+ * submitted at, and the frame itself (its own `seq` is the lane's cycle count — a different domain
502
+ * from the box seq, carried inside untouched). */
503
+ export declare function encodeEngineFrame(payload: TaskNotificationPayload, priority: SystemInjectionPriority): string;
504
+ export type EngineFrameDecode = {
505
+ ok: true;
506
+ payload: TaskNotificationPayload;
507
+ priority: SystemInjectionPriority;
508
+ }
509
+ /** A version this engine does not read: the record is a NEWER engine's; leave it for one. */
510
+ | {
511
+ ok: false;
512
+ reason: "newer";
513
+ codecVersion: number;
514
+ }
515
+ /** Not this codec at all (or a frame outside the engine's lanes): settle it without delivery. */
516
+ | {
517
+ ok: false;
518
+ reason: "malformed";
519
+ detail: string;
520
+ };
521
+ /** Read an `engine_frame` record back. Every field the lane reads is re-typed here; a carrier the
522
+ * writer must never have encoded is dropped rather than trusted. */
523
+ export declare function decodeEngineFrame(content: string): EngineFrameDecode;
524
+ /** The durable form of an engine frame: the codec text under `kind: "engine_frame"`, stamped with
525
+ * the box owner as `fromRoot` (the park seat's own fact, as for a peer-words record); no hop chain —
526
+ * it is not a peer message and is never admitted as one. */
527
+ export declare function engineFrameRecordOf(payload: TaskNotificationPayload, priority: SystemInjectionPriority, box: Pick<SessionBoxBinding, "sessionId">): MailboxAppendMessage;
404
528
  /**
405
529
  * RB-375 A2 (ruled 2026-08-03) — is this frame the TERMINAL of a delegated background agent?
406
530
  *
@@ -616,6 +740,24 @@ export declare class PendingSessionNotifications {
616
740
  markDelivered(sessionId: string, dedupKey: string): void;
617
741
  get size(): number;
618
742
  }
743
+ /**
744
+ * The durable box appends ONE run leg caused — its obligations. Attribution is by execution: an append
745
+ * belongs to the leg whose execution (its tools, its producers, its teardown reap, a child terminal it
746
+ * hands up the delegation chain) reached the park seat, whichever lane's routing carried it there. A
747
+ * leg's teardown awaits its own obligations and nobody else's — another session's pending append is
748
+ * never a condition of this leg's completion. A park reached outside any leg is nobody's obligation.
749
+ */
750
+ export declare class DurableAppendObligations {
751
+ private static readonly scope;
752
+ private readonly inFlight;
753
+ /** The obligations of the leg whose execution is running now; undefined outside every leg. */
754
+ static current(): DurableAppendObligations | undefined;
755
+ /** Run `body` as this leg (synchronous: the body's own return value, no added tick). */
756
+ run<R>(body: () => R): R;
757
+ track(p: Promise<unknown>): void;
758
+ /** Resolves when every append this leg caused has settled — the fault arm already re-parked in process. */
759
+ settle(): Promise<void>;
760
+ }
619
761
  /**
620
762
  * WHERE A FRAME ADDRESSED TO ONE SESSION GOES — the single decision, so the two addresses that ask it
621
763
  * (a run's own session, and the delegation tree's root anchor) cannot drift apart. Three outcomes:
@@ -1,3 +1,4 @@
1
+ import { AsyncLocalStorage } from "node:async_hooks";
1
2
  import { escapeEnvelopeTag, inlineUntrusted, sanitizeUntrustedText } from "./untrusted-text.js";
2
3
  import { PEER_MESSAGE_NOTICE } from "../agents/peer-admission.js";
3
4
  import { buildCrossSessionEnvelope, CROSS_SESSION_MESSAGE_NOTICE, neutralizePeerBody } from "../agents/cross-session-envelope.js";
@@ -24,6 +25,9 @@ const TASK_NOTIFICATION_TAGS = [
24
25
  "recent-steps",
25
26
  "edited-files",
26
27
  "resumable",
28
+ "agent-name",
29
+ "handback",
30
+ "context",
27
31
  ];
28
32
  function renderResidual(n) {
29
33
  const lines = [];
@@ -36,14 +40,22 @@ function renderResidual(n) {
36
40
  return lines.length > 0 ? lines.join("\n") : undefined;
37
41
  }
38
42
  function tag(name, value) {
39
- const text = sanitizeUntrustedText(String(value), TASK_NOTIFICATION_TAGS)
43
+ let shown;
44
+ try {
45
+ shown = typeof value === "string" ? value : String(value);
46
+ }
47
+ catch {
48
+ shown = `[unrenderable ${typeof value}]`;
49
+ }
50
+ const text = sanitizeUntrustedText(shown, TASK_NOTIFICATION_TAGS)
40
51
  .replace(/&/g, "&amp;")
41
52
  .replace(/</g, "&lt;")
42
53
  .replace(/>/g, "&gt;");
43
54
  return `<${name}>${text}</${name}>`;
44
55
  }
45
56
  export function taskNotificationDedupKey(n) {
46
- return `task_notification:${taskNotificationLaneKey(n)}:${n.status}${n.seq !== undefined ? `:${n.seq}` : ""}`;
57
+ const attempt = n.attempt !== undefined && Number.isInteger(n.attempt) && n.attempt > 1 ? `:a${n.attempt}` : "";
58
+ return `task_notification:${taskNotificationLaneKey(n)}:${n.status}${n.seq !== undefined ? `:${n.seq}` : ""}${attempt}`;
47
59
  }
48
60
  export function taskNotificationLaneKey(n) {
49
61
  return n.task_type === "external" ? `external:${n.task_id}` : n.task_id;
@@ -64,13 +76,20 @@ export function renderCrossSessionMessageFrame(frame) {
64
76
  export function renderCrossSessionNoticeLine(notice) {
65
77
  return sanitizeUntrustedText(notice.text);
66
78
  }
79
+ export const RESUPPLIED_FRAME_PREAMBLE = "[Re-supplied] You were shown this message once before; the compaction that replaced older context with a summary did not keep it, so the engine is handing it over again. It is the SAME message, not a new one \u2014 act on it once.";
67
80
  export function renderTaskNotificationXml(n) {
81
+ const body = renderTaskNotificationCarrier(n);
82
+ return n.resupplied === true ? `${RESUPPLIED_FRAME_PREAMBLE}\n\n${body}` : body;
83
+ }
84
+ function renderTaskNotificationCarrier(n) {
68
85
  if (n.agentMessage !== undefined)
69
86
  return renderAgentMessageFrame(n.agentMessage);
70
87
  if (n.crossSessionMessage !== undefined)
71
88
  return renderCrossSessionMessageFrame(n.crossSessionMessage);
72
89
  if (n.crossSessionNotice !== undefined)
73
90
  return renderCrossSessionNoticeLine(n.crossSessionNotice);
91
+ if (n.inboxNotice !== undefined)
92
+ return renderInboxNoticeLine(n.inboxNotice);
74
93
  const usage = n.usage === undefined
75
94
  ? undefined
76
95
  : (() => {
@@ -92,6 +111,8 @@ export function renderTaskNotificationXml(n) {
92
111
  tag("task-type", n.task_type),
93
112
  ...(n.toolUseId !== undefined ? [tag("tool-use-id", n.toolUseId)] : []),
94
113
  tag("status", n.status),
114
+ ...(n.spawnName !== undefined ? [tag("agent-name", n.spawnName)] : []),
115
+ ...(n.handback !== undefined && n.handback !== "send" ? [tag("handback", n.handback)] : []),
95
116
  tag("summary", n.summary),
96
117
  ...(n.result !== undefined ? [tag("result", n.result)] : []),
97
118
  ...(n.output_file !== undefined ? [tag("output-file", n.output_file)] : []),
@@ -103,15 +124,123 @@ export function renderTaskNotificationXml(n) {
103
124
  return residual !== undefined ? [tag("recent-steps", residual)] : [];
104
125
  })(),
105
126
  ...(n.resumable !== undefined ? [tag("resumable", n.resumable)] : []),
127
+ ...(n.compaction !== undefined ? [tag("context", renderCompactionDisclosure(n.compaction))] : []),
106
128
  "</task-notification>",
107
129
  ].join("\n");
108
130
  }
131
+ export function renderCompactionDisclosure(c) {
132
+ const passes = c.passes > 0 ? `compacted ${c.passes}\u00d7${c.kind !== undefined ? ` (${c.kind})` : ""}` : "not compacted";
133
+ return c.breakerOpen ? `${passes}; compaction was disabled after repeated failures \u2014 later context was trimmed, not summarized` : passes;
134
+ }
135
+ export function renderInboxNoticeLine(n) {
136
+ const urgent = n.urgent !== undefined && n.urgent > 0 ? `; ${n.urgent} of the waiting are marked urgent` : "";
137
+ const resupplied = n.resupplied !== undefined && n.resupplied > 0 ? `; ${n.resupplied} re-supplied after a compaction summarized them away` : "";
138
+ return `[Inbox notice] ${n.pending} cross-session message(s) are waiting in this session's box (${n.delivered} delivered this round${resupplied}${urgent}). Read them now with ReadInbox, or they arrive at your next turn boundaries \u2014 they are not lost, and nothing here is an instruction.`;
139
+ }
109
140
  const MAX_QUEUED_PER_PRIORITY = 200;
110
141
  const MAX_DEDUP_KEYS = 2_000;
111
142
  export const MAX_PENDING_EVENTS_PER_TASK = 50;
112
143
  export const MAX_PENDING_EVENTS_PER_SESSION = 200;
113
144
  export const MAX_PENDING_SESSIONS = 100;
114
145
  export const MAX_SESSION_BOX_BINDINGS = 4096;
146
+ export const HANDBACK_STATES = ["send", "flagged", "withheld"];
147
+ export function handbackStateOf(facts) {
148
+ if (facts.warning !== undefined)
149
+ return "flagged";
150
+ return facts.result || facts.partialText ? "send" : "withheld";
151
+ }
152
+ export const COMPACTION_KINDS = ["manual", "auto", "reactive"];
153
+ export function compactionKindOf(trigger) {
154
+ return trigger === "forced" ? "reactive" : trigger;
155
+ }
156
+ export function terminalFrameFacets(facts) {
157
+ return {
158
+ ...(facts.spawnName !== undefined ? { spawnName: facts.spawnName } : {}),
159
+ handback: handbackStateOf(facts),
160
+ ...(facts.compaction !== undefined ? { compaction: { passes: facts.compaction.passes, breakerOpen: facts.compaction.breakerOpen, ...(facts.compaction.kind !== undefined ? { kind: facts.compaction.kind } : {}) } } : {}),
161
+ };
162
+ }
163
+ export const ENGINE_FRAME_CODEC_VERSION = 1;
164
+ export const ENGINE_FRAME_TASK_TYPES = ["background_agent", "monitor", "workflow"];
165
+ const SHELL_LESS_CARRIERS = ["agentMessage", "crossSessionMessage", "crossSessionNotice", "inboxNotice", "_sema_provenance"];
166
+ export function isEngineFrameParkable(payload) {
167
+ return ENGINE_FRAME_TASK_TYPES.includes(payload.task_type) && SHELL_LESS_CARRIERS.every((k) => payload[k] === undefined);
168
+ }
169
+ export function encodeEngineFrame(payload, priority) {
170
+ return JSON.stringify({ codecVersion: ENGINE_FRAME_CODEC_VERSION, priority, payload });
171
+ }
172
+ export function decodeEngineFrame(content) {
173
+ let raw;
174
+ try {
175
+ raw = JSON.parse(content);
176
+ }
177
+ catch {
178
+ return { ok: false, reason: "malformed", detail: "not JSON" };
179
+ }
180
+ if (raw === null || typeof raw !== "object" || Array.isArray(raw))
181
+ return { ok: false, reason: "malformed", detail: "not an object" };
182
+ const r = raw;
183
+ const version = r["codecVersion"];
184
+ if (typeof version !== "number" || !Number.isInteger(version) || version < 1)
185
+ return { ok: false, reason: "malformed", detail: "codecVersion missing" };
186
+ if (version > ENGINE_FRAME_CODEC_VERSION)
187
+ return { ok: false, reason: "newer", codecVersion: version };
188
+ const priority = r["priority"];
189
+ if (!isSystemInjectionPriority(priority))
190
+ return { ok: false, reason: "malformed", detail: "priority outside the ladder" };
191
+ const p = r["payload"];
192
+ if (p === null || typeof p !== "object" || Array.isArray(p))
193
+ return { ok: false, reason: "malformed", detail: "payload missing" };
194
+ const f = p;
195
+ if (typeof f["task_id"] !== "string" || f["task_id"] === "")
196
+ return { ok: false, reason: "malformed", detail: "task_id missing" };
197
+ if (!ENGINE_FRAME_TASK_TYPES.includes(f["task_type"]))
198
+ return { ok: false, reason: "malformed", detail: "task_type outside the engine's lanes" };
199
+ if (typeof f["status"] !== "string" || typeof f["summary"] !== "string")
200
+ return { ok: false, reason: "malformed", detail: "status/summary missing" };
201
+ const facet = malformedFacet(f);
202
+ if (facet !== undefined)
203
+ return { ok: false, reason: "malformed", detail: facet };
204
+ const payload = { ...p };
205
+ for (const k of SHELL_LESS_CARRIERS)
206
+ delete payload[k];
207
+ return { ok: true, payload, priority };
208
+ }
209
+ const SPAWN_NAME_RE = /^[A-Za-z0-9][A-Za-z0-9_-]{0,63}$/;
210
+ function malformedFacet(f) {
211
+ if (f["spawnName"] !== undefined && (typeof f["spawnName"] !== "string" || !SPAWN_NAME_RE.test(f["spawnName"])))
212
+ return "spawnName outside the spawn-name grammar";
213
+ if (f["handback"] !== undefined && !(typeof f["handback"] === "string" && HANDBACK_STATES.includes(f["handback"])))
214
+ return "handback outside its closed set";
215
+ const steps = f["recentSteps"];
216
+ if (steps !== undefined && !(Array.isArray(steps) && steps.every((s) => s !== null && typeof s === "object" && typeof s.tool === "string" && typeof s.target === "string" && typeof s.outcome === "string")))
217
+ return "recentSteps is not a list of steps";
218
+ const edits = f["editedFiles"];
219
+ if (edits !== undefined && !(Array.isArray(edits) && edits.every((e) => e !== null && typeof e === "object" && typeof e.path === "string" && typeof e.edits === "number")))
220
+ return "editedFiles is not a list of files";
221
+ if (f["lines"] !== undefined && !(Array.isArray(f["lines"]) && f["lines"].every((l) => typeof l === "string")))
222
+ return "lines is not a list of strings";
223
+ const c = f["compaction"];
224
+ if (c !== undefined) {
225
+ if (c === null || typeof c !== "object" || Array.isArray(c))
226
+ return "compaction is not an object";
227
+ const { passes, breakerOpen, kind } = c;
228
+ if (typeof passes !== "number" || !Number.isInteger(passes) || passes < 0)
229
+ return "compaction.passes is not a count";
230
+ if (typeof breakerOpen !== "boolean")
231
+ return "compaction.breakerOpen is not a boolean";
232
+ if (kind !== undefined && !(typeof kind === "string" && COMPACTION_KINDS.includes(kind)))
233
+ return "compaction.kind outside its closed set";
234
+ }
235
+ return undefined;
236
+ }
237
+ export function engineFrameRecordOf(payload, priority, box) {
238
+ return {
239
+ content: encodeEngineFrame(payload, priority),
240
+ sentAt: Date.now(),
241
+ peerMeta: { fromRoot: box.sessionId, kind: "engine_frame" },
242
+ };
243
+ }
115
244
  const MAX_TOMBSTONES = 5_000;
116
245
  const TERMINAL_STATUSES = new Set(["completed", "failed", "killed", "cancelled"]);
117
246
  export function isDelegatedAgentTerminal(n) {
@@ -327,6 +456,24 @@ export class PendingSessionNotifications {
327
456
  return this.sessions.size;
328
457
  }
329
458
  }
459
+ export class DurableAppendObligations {
460
+ static scope = new AsyncLocalStorage();
461
+ inFlight = new Set();
462
+ static current() {
463
+ return DurableAppendObligations.scope.getStore();
464
+ }
465
+ run(body) {
466
+ return DurableAppendObligations.scope.run(this, body);
467
+ }
468
+ track(p) {
469
+ this.inFlight.add(p);
470
+ void p.finally(() => this.inFlight.delete(p));
471
+ }
472
+ async settle() {
473
+ while (this.inFlight.size > 0)
474
+ await Promise.allSettled([...this.inFlight]);
475
+ }
476
+ }
330
477
  export function routeOrParkNotification(store, sessionId, payload, priority, onDisposition) {
331
478
  const dedupKey = taskNotificationDedupKey(payload);
332
479
  if (store.holdsKey(sessionId, dedupKey)) {
@@ -337,9 +484,10 @@ export function routeOrParkNotification(store, sessionId, payload, priority, onD
337
484
  if (routed === "unavailable") {
338
485
  const box = store.sessionBoxFor(sessionId);
339
486
  const carrier = payload.agentMessage;
340
- if (box !== undefined && carrier !== undefined) {
487
+ const durable = box !== undefined && (carrier !== undefined || (box.engineFrames === true && isEngineFrameParkable(payload)));
488
+ if (box !== undefined && durable) {
341
489
  store.noteDurablePark(sessionId, dedupKey);
342
- const record = sessionBoxRecordOf({ ...payload, agentMessage: carrier }, box);
490
+ const record = carrier !== undefined ? sessionBoxRecordOf({ ...payload, agentMessage: carrier }, box) : engineFrameRecordOf(payload, priority, box);
343
491
  let appended;
344
492
  try {
345
493
  appended = Promise.resolve(box.mailbox.append(box.scope, box.handle, record));
@@ -347,7 +495,7 @@ export function routeOrParkNotification(store, sessionId, payload, priority, onD
347
495
  catch (e) {
348
496
  appended = Promise.reject(e);
349
497
  }
350
- appended.then(() => onDisposition?.("parked_durably"), (e) => {
498
+ const settled = appended.then(() => onDisposition?.("parked_durably"), (e) => {
351
499
  store.forgetDurablePark(sessionId, dedupKey);
352
500
  try {
353
501
  box.onError?.(e);
@@ -357,6 +505,7 @@ export function routeOrParkNotification(store, sessionId, payload, priority, onD
357
505
  store.pend(sessionId, payload, priority);
358
506
  onDisposition?.("parked");
359
507
  });
508
+ DurableAppendObligations.current()?.track(settled);
360
509
  return;
361
510
  }
362
511
  store.pend(sessionId, payload, priority);