@sema-agent/core 7.28.1 → 7.30.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (162) hide show
  1. package/CHANGELOG.md +473 -1
  2. package/dist/agents/child-model-seat.d.ts +59 -7
  3. package/dist/agents/child-model-seat.js +33 -7
  4. package/dist/agents/cross-session-judge.d.ts +27 -5
  5. package/dist/agents/cross-session-judge.js +23 -12
  6. package/dist/agents/list-agents-tool.d.ts +31 -6
  7. package/dist/agents/list-agents-tool.js +66 -14
  8. package/dist/agents/peer-admission.d.ts +20 -8
  9. package/dist/agents/peer-admission.js +21 -12
  10. package/dist/agents/peer-directory.d.ts +29 -0
  11. package/dist/agents/peer-directory.js +20 -0
  12. package/dist/agents/peer-held-queue.d.ts +31 -4
  13. package/dist/agents/peer-held-queue.js +32 -1
  14. package/dist/agents/peer-session-drain.d.ts +91 -13
  15. package/dist/agents/peer-session-drain.js +409 -102
  16. package/dist/agents/read-inbox-tool.d.ts +22 -0
  17. package/dist/agents/read-inbox-tool.js +39 -0
  18. package/dist/agents/roster-store.d.ts +2 -4
  19. package/dist/agents/roster-store.js +1 -1
  20. package/dist/agents/send-message-tool.d.ts +4 -4
  21. package/dist/agents/send-message-tool.js +22 -2
  22. package/dist/agents/subagent.js +62 -44
  23. package/dist/agents/team-role.d.ts +34 -0
  24. package/dist/agents/team-role.js +17 -0
  25. package/dist/agents/verify.js +2 -2
  26. package/dist/brain/anthropic.js +4 -2
  27. package/dist/brain/open-responses.js +4 -3
  28. package/dist/brain/openai.js +2 -1
  29. package/dist/brain/stream-engine.js +6 -0
  30. package/dist/brain/stream-shared.d.ts +8 -0
  31. package/dist/brain/stream-shared.js +8 -0
  32. package/dist/core/auto-compaction.d.ts +42 -0
  33. package/dist/core/auto-compaction.js +35 -2
  34. package/dist/core/child-clamp-seats.d.ts +11 -0
  35. package/dist/core/child-clamp-seats.js +3 -0
  36. package/dist/core/context-edit.d.ts +3 -2
  37. package/dist/core/context-edit.js +6 -5
  38. package/dist/core/context-guard.d.ts +2 -1
  39. package/dist/core/context-guard.js +3 -2
  40. package/dist/core/context-settings.d.ts +103 -0
  41. package/dist/core/context-settings.js +60 -0
  42. package/dist/core/engine-notice.d.ts +18 -1
  43. package/dist/core/frame-settlement.d.ts +97 -0
  44. package/dist/core/frame-settlement.js +36 -0
  45. package/dist/core/governance-codes.d.ts +1 -1
  46. package/dist/core/governance-codes.js +4 -0
  47. package/dist/core/hooks.d.ts +10 -10
  48. package/dist/core/mailbox-store.d.ts +41 -7
  49. package/dist/core/mailbox-store.js +9 -3
  50. package/dist/core/message-utils.d.ts +13 -0
  51. package/dist/core/message-utils.js +6 -0
  52. package/dist/core/parent-spec-seat.d.ts +14 -1
  53. package/dist/core/parent-spec-seat.js +6 -0
  54. package/dist/core/permission-rule-model.js +1 -1
  55. package/dist/core/roles.d.ts +20 -0
  56. package/dist/core/roles.js +35 -16
  57. package/dist/core/runner/assemble-result.d.ts +2 -0
  58. package/dist/core/runner/assemble-result.js +3 -1
  59. package/dist/core/runner/compaction-call-options.d.ts +26 -2
  60. package/dist/core/runner/compaction-call-options.js +9 -0
  61. package/dist/core/runner/compaction-seams.d.ts +4 -3
  62. package/dist/core/runner/compaction-seams.js +13 -6
  63. package/dist/core/runner/contracts.d.ts +61 -7
  64. package/dist/core/runner/decide-continuation.d.ts +2 -1
  65. package/dist/core/runner/decide-continuation.js +11 -10
  66. package/dist/core/runner/initial-run-state.js +2 -2
  67. package/dist/core/runner/prepare-caps-and-workflow.js +9 -1
  68. package/dist/core/runner/prepare-config-doors.js +4 -1
  69. package/dist/core/runner/prepare-context-lane.d.ts +9 -2
  70. package/dist/core/runner/prepare-context-lane.js +15 -10
  71. package/dist/core/runner/prepare-delegation-surface.d.ts +3 -2
  72. package/dist/core/runner/prepare-delegation-surface.js +9 -3
  73. package/dist/core/runner/prepare-file-history.js +10 -8
  74. package/dist/core/runner/prepare-gate-stations.d.ts +4 -0
  75. package/dist/core/runner/prepare-gate-stations.js +3 -1
  76. package/dist/core/runner/prepare-inherited-gate.js +1 -0
  77. package/dist/core/runner/prepare-prompt-inputs.d.ts +6 -0
  78. package/dist/core/runner/prepare-prompt-inputs.js +2 -1
  79. package/dist/core/runner/prepare-task.js +22 -9
  80. package/dist/core/runner/prepare-wiring-manifest.d.ts +6 -1
  81. package/dist/core/runner/prepare-wiring-manifest.js +6 -1
  82. package/dist/core/runner/response-model-seat.d.ts +16 -0
  83. package/dist/core/runner/response-model-seat.js +27 -0
  84. package/dist/core/runner/retired-runner-deps.d.ts +3 -1
  85. package/dist/core/runner/retired-runner-deps.js +1 -0
  86. package/dist/core/runner/run-compaction-machinery.d.ts +4 -2
  87. package/dist/core/runner/run-compaction-machinery.js +5 -3
  88. package/dist/core/runner/run-harness-handlers.js +9 -2
  89. package/dist/core/runner/run-identity-wiring.js +19 -1
  90. package/dist/core/runner/run-leg.js +1 -1
  91. package/dist/core/runner/run-notification-lane.js +8 -2
  92. package/dist/core/runner/run-recovery-lanes.js +9 -5
  93. package/dist/core/runner/run-settle-and-teardown.d.ts +4 -22
  94. package/dist/core/runner/run-settle-and-teardown.js +6 -10
  95. package/dist/core/runner/run-stop-and-final-verify.js +1 -0
  96. package/dist/core/runner/run-terminal-adoption.js +5 -5
  97. package/dist/core/runner/run-turn-boundary.js +32 -5
  98. package/dist/core/runner/runtask.d.ts +8 -1
  99. package/dist/core/runner/runtask.js +33 -2
  100. package/dist/core/runner-deps.d.ts +44 -13
  101. package/dist/core/side-query.d.ts +6 -5
  102. package/dist/core/side-query.js +3 -1
  103. package/dist/core/swappable-deps.d.ts +20 -4
  104. package/dist/core/swappable-deps.js +6 -1
  105. package/dist/core/task-event.d.ts +13 -0
  106. package/dist/core/task-notification.d.ts +144 -2
  107. package/dist/core/task-notification.js +154 -5
  108. package/dist/core/task-result.d.ts +27 -3
  109. package/dist/core/task-spec.d.ts +7 -0
  110. package/dist/core/tool-catalog-entries.js +2 -1
  111. package/dist/core/tool-conformance.js +1 -0
  112. package/dist/core/tool-policy.d.ts +9 -4
  113. package/dist/core/tool-spec.d.ts +14 -0
  114. package/dist/core/wiring-manifest.d.ts +12 -0
  115. package/dist/core/wiring-manifest.js +5 -1
  116. package/dist/engine/harness/agent-harness.d.ts +14 -1
  117. package/dist/engine/harness/agent-harness.js +28 -3
  118. package/dist/engine/harness/types.d.ts +3 -1
  119. package/dist/engine/llm/types.d.ts +8 -7
  120. package/dist/engine/llm/types.js +1 -1
  121. package/dist/engine/session/import-validate.js +4 -0
  122. package/dist/index.d.ts +10 -7
  123. package/dist/index.js +7 -5
  124. package/dist/orchestration/run-workflow-tool.d.ts +4 -4
  125. package/dist/orchestration/workflow.d.ts +1 -1
  126. package/dist/orchestration/workflow.js +8 -5
  127. package/dist/shell/lex.d.ts +30 -51
  128. package/dist/shell/lex.js +237 -284
  129. package/dist/stores/cc/mailbox-store.js +3 -2
  130. package/dist/stores/file/corrupt-read-sink.d.ts +28 -2
  131. package/dist/stores/file/file-history-store.d.ts +2 -4
  132. package/dist/stores/file/file-history-store.js +1 -1
  133. package/dist/stores/file/fs-atomic.d.ts +16 -11
  134. package/dist/stores/file/fs-atomic.js +17 -6
  135. package/dist/stores/file/index.d.ts +5 -6
  136. package/dist/stores/file/index.js +1 -1
  137. package/dist/stores/file/mailbox-store.d.ts +4 -9
  138. package/dist/stores/file/mailbox-store.js +29 -30
  139. package/dist/stores/file/session-policy-store.d.ts +2 -0
  140. package/dist/stores/file/session-policy-store.js +1 -1
  141. package/dist/stores/file/session-store.d.ts +11 -7
  142. package/dist/stores/file/session-store.js +23 -16
  143. package/dist/stores/file/shared-ledger.js +3 -14
  144. package/dist/stores/file/tool-result-store.d.ts +2 -0
  145. package/dist/stores/file/tool-result-store.js +5 -2
  146. package/dist/stores/file/workflow-journal-store.js +14 -9
  147. package/dist/tools/fs/bash-program-position.d.ts +15 -24
  148. package/dist/tools/fs/bash-program-position.js +11 -29
  149. package/dist/tools/fs/bash-readonly-classifier.d.ts +125 -117
  150. package/dist/tools/fs/bash-readonly-classifier.js +400 -272
  151. package/dist/tools/fs/shell-read-check.js +3 -3
  152. package/dist/tools/fs/shell-read-establish.js +1 -1
  153. package/dist/tools/fs/shell-read-plan.d.ts +11 -6
  154. package/dist/tools/fs/shell-read-plan.js +167 -183
  155. package/dist/tools/fs/shell-read-word.d.ts +50 -0
  156. package/dist/tools/fs/shell-read-word.js +59 -0
  157. package/dist/tools/fs/shell-reentry-text.d.ts +10 -0
  158. package/dist/tools/fs/shell-reentry-text.js +73 -0
  159. package/package.json +1 -1
  160. package/test/export-surface.snapshot.json +58 -8
  161. package/wire-changes/core@7.29.0.json +185 -0
  162. package/wire-changes/core@7.30.0.json +1821 -0
@@ -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);
@@ -198,10 +198,20 @@ export interface TaskResult {
198
198
  * {@link import("./runner/terminal-projection.js").terminalProjection} — the one cause→word derivation.
199
199
  */
200
200
  terminal: TerminalCause;
201
- /** design/99 MF-25: the EFFECTIVE model id that served this task — the RESOLVED `Model.id`, not the requested
202
- * `TaskSpec.model` ref (which may be a role / name / `@mention`). Lets a UI echo "served by X" instead of the
203
- * requested ref. A mid-run degradation is observed separately (see the degraded-model fields). */
201
+ /** design/99 MF-25: the EFFECTIVE model id this task ran on — the ENGINE-SELECTED, RESOLVED `Model.id`, not
202
+ * the requested `TaskSpec.model` ref (a role / name / `@mention`). A mid-run degradation is observed
203
+ * separately (the degraded-model fields); what the PROVIDER called it rides {@link responseModel}. */
204
204
  model?: string;
205
+ /** The PROVIDER'S OWN name for the model that answered this run's FINAL turn, when it DIFFERS from the
206
+ * id the request was sent with — the result-face twin of `turn_end.responseModel`. {@link model} above
207
+ * stays the ENGINE-SELECTED id a resume re-seeds and a catalog resolves; a UI echoing "served by X"
208
+ * prints this one when it is here and {@link model} when it is not. **In-presence condition**: the final
209
+ * assistant message carried one. ABSENT ⇒ the provider named the requested id or named nothing (one
210
+ * reading, not two), there was no final assistant message, or the final turn AGREED while an earlier one
211
+ * did not — the per-turn frames are the complete record, this is the terminal one. Never present on the
212
+ * engine's synthetic messages, which carry no provider report; a mismatch is announced either way as
213
+ * `route.response_model_mismatch`. */
214
+ responseModel?: string;
205
215
  /**
206
216
  * The leg's effective REASONING resolution: the result-face twin of {@link model} for the thinking
207
217
  * knob. How the requested tier resolved against the serving model's real capability
@@ -264,6 +274,20 @@ export interface TaskResult {
264
274
  * Undefined on every other terminal and when there was nothing to salvage.
265
275
  */
266
276
  salvagedOutput?: string;
277
+ /**
278
+ * Present iff this run compacted its context at least once at a turn boundary or a prompt-too-long
279
+ * recovery, or opened its compaction breaker: `passes` = landed compactions, `kind` = the last landed
280
+ * pass's trigger (`reactive` is the wire's `forced`), `breakerOpen` = compaction was disabled for the
281
+ * rest of the run (later context was trimmed away, not summarized). The end-of-task settle pass is
282
+ * NOT counted (ruled): it summarizes AFTER the work, for the next run, and says nothing about what this
283
+ * run's model saw. Projected onto a delegated child's terminal frame so
284
+ * the parent can tell that a child forgot.
285
+ */
286
+ compaction?: {
287
+ passes: number;
288
+ breakerOpen: boolean;
289
+ kind?: import("./task-notification.js").CompactionKind;
290
+ };
267
291
  /**
268
292
  * The remote-workspace lifecycle failures this run hit, as DATA. The seam declares eleven
269
293
  * distinct `RemoteExecutionErrorCode`s, but every one of them used to reach the caller as the same
@@ -554,6 +554,13 @@ export interface TaskSpec {
554
554
  * interception, prompt gating, stop pushback, compaction taps, permission-denied observation; each
555
555
  * member's contract is on the interface). Overrides `RunnerDeps.hooks`. */
556
556
  hooks?: import("./hooks.js").Hooks;
557
+ /**
558
+ * The session's SELF-REPORTED presence for the peer directory (`agent` = what it is, `intent` = what
559
+ * it is doing; each ≤ 120 chars). The engine never writes a directory row: the same object goes out
560
+ * on the leg's `wiring_manifest` so a host copies it into the row it writes, and `ListAgents` renders
561
+ * it on other sessions' listings prefixed `self:`. Labels only — never read by any judgment.
562
+ */
563
+ peerPresence?: import("../agents/peer-directory.js").PeerPresence;
557
564
  /**
558
565
  * design/45 **durable suspend-on-approval** (F4). When set AND a `CheckpointStore` is wired
559
566
  * (`checkpointStore` here or on `RunnerDeps`), a tool-policy `ask` **can suspend durably**: when no LIVE
@@ -111,7 +111,8 @@ export const TOOL_CATALOG_ENTRIES = [
111
111
  { id: "monitor", name: "Monitor", source: "builtin", definedIn: "src/tools/monitor.ts", mountedBy: ["backgroundTasks"], cards: ["monitor-start", "readonly_out_of_root"], face: { family: "other", effect: "write", contract: c("core.monitor@1"), offloadThresholdChars: 10_000 } },
112
112
  { id: "send-message", name: "SendMessage", source: "builtin", definedIn: "src/agents/send-message-tool.ts", mountedBy: ["delegation", "hostInternals", "peerLane"], cards: ["send-message"], face: { family: "protocol", effect: "write", contract: c("core.send_message@1") } },
113
113
  { id: "agent-transcript", name: "AgentTranscript", source: "builtin", definedIn: "src/agents/agent-transcript-tool.ts", mountedBy: ["delegation"], cards: ["agent-transcript"], face: { family: "delegate", effect: "read", contract: c("core.agent_transcript@1") } },
114
- { id: "list-agents", name: "ListAgents", source: "builtin", definedIn: "src/agents/list-agents-tool.ts", mountedBy: ["peerLane"], cards: ["list-agents"], face: { family: "protocol", effect: "read", aliases: ["ListPeers"], contract: c("core.list_agents@1") } },
114
+ { id: "list-agents", name: "ListAgents", source: "builtin", definedIn: "src/agents/list-agents-tool.ts", mountedBy: ["delegation", "peerLane"], cards: ["list-agents"], face: { family: "protocol", effect: "read", aliases: ["ListPeers"], contract: c("core.list_agents@1") } },
115
+ { id: "read-inbox", name: "ReadInbox", source: "builtin", definedIn: "src/agents/read-inbox-tool.ts", mountedBy: ["peerLane"], cards: ["read-inbox"], face: { family: "protocol", effect: "read", contract: c("core.read_inbox@1"), offload: false, budgetExempt: true } },
115
116
  { id: "workflow", name: "Workflow", source: "builtin", definedIn: "src/orchestration/run-workflow-tool.ts", mountedBy: ["workflow"], cards: ["workflow-run"], face: { family: "delegate", effect: "write", contract: c("core.workflow@1") } },
116
117
  { id: "report-blocked", name: "ReportBlocked", source: "builtin", definedIn: "src/core/runner/synthetic-tools.ts", mountedBy: ["always"], cards: [], face: { family: "scaffold", effect: "read", contract: c("core.report_blocked@1") } },
117
118
  { id: "report-findings", name: "ReportFindings", source: "builtin", definedIn: "src/core/runner/synthetic-tools.ts", mountedBy: ["always"], cards: ["report-findings"], face: { family: "scaffold", effect: "read", contract: c("core.report_findings@1") } },
@@ -13,6 +13,7 @@ export const TOOL_CONFORMANCE_VECTORS = [
13
13
  vector("hands-full", ["always", "hands", "backgroundTasks", "delegation"], [
14
14
  { name: "SendMessage", why: "needs the runner self seat (a direct prepare has none)" },
15
15
  { name: "AgentTranscript", why: "needs the runner self seat (a direct prepare has none)" },
16
+ { name: "ListAgents", why: "mounts with the delegation face only under a runner self seat (a direct prepare has none)" },
16
17
  ], [OFFLOAD_READER]),
17
18
  vector("hands-readonly", ["always", "hands"], [
18
19
  { name: "Edit", why: "the read-only band mounts the read half only" },
@@ -321,8 +321,12 @@ export type PermissionResult = {
321
321
  * in the one direction the stamp cannot reach (a caller composing its own ask) the claim is
322
322
  * tightening-only: it can make an ask outlive a rule, never turn one into an allow. */
323
323
  probeMandated?: true;
324
- /** This ask is MANDATED — no persisted allow rule and no standing posture may retire it, only an
325
- * answer to THIS call. ENGINE-STAMPED at the gate's single origin stamp from the ONE derivation
324
+ /** This ask is MANDATED — no persisted allow rule and no STANDING ANSWER (a release remembered from an
325
+ * earlier call, at any scope, a session included) may retire it; only an answer produced for THIS call
326
+ * clears it — a person, a hook, a deployment's blanket `onAsk: "allow"` seat, or the live deciders the
327
+ * mandate tables admit ({@link import("./ask-origin.js").classifierMayAnswerMandate} /
328
+ * `sandboxMayAdmitMandate`); {@link requiresRealApproval} is the stronger demand that also refuses the
329
+ * blanket seat. ENGINE-STAMPED at the gate's single origin stamp from the ONE derivation
326
330
  * the approval card's rule-offer word also reads ({@link import("./ask-origin.js").ruleClearanceOf},
327
331
  * which lists the doors). TIGHTENING-ONLY: a layer below the mint may not clear one a layer above
328
332
  * set, and a policy self-declaring it only opts its own ask OUT of automatic answering. Carried
@@ -1259,8 +1263,9 @@ export interface AskRequest {
1259
1263
  * caller/worker-settable field. */
1260
1264
  readonly requiresRealApproval?: boolean;
1261
1265
  /**
1262
- * PRESENCE ONLY, and a FACT ABOUT THIS ASK: no persisted allow rule and no standing posture can
1263
- * retire this question. ABSENT is not the inverse claim: a person's own explicit `ask` rule and a
1266
+ * PRESENCE ONLY, and a FACT ABOUT THIS ASK: no persisted allow rule and no STANDING ANSWER — a release
1267
+ * remembered from an earlier call, at any scope, a session included — can retire this question; it is
1268
+ * answered only for THIS call, by an answerer (see the ask-arm member of the same name for who may). ABSENT is not the inverse claim: a person's own explicit `ask` rule and a
1264
1269
  * matching allow rule the lane already refused both leave it absent and still block rule clearance
1265
1270
  * ({@link ruleOffersAbsence} words those `shadowed`). The wire twin of the
1266
1271
  * {@link PermissionResult} ask-arm member of the same name, minted once by the engine at the gate's
@@ -14,6 +14,7 @@ import type { AgentDefinition } from "./agent-definition.js";
14
14
  import type { TaskEvent } from "./task-event.js";
15
15
  import type { TaskSpec } from "./task-spec.js";
16
16
  import type { BackgroundChildEvent } from "./delegation-frames.js";
17
+ import type { ModelRef } from "./model-seat.js";
17
18
  import type { ToolFamily, ToolPathTarget, ToolRenderHints, ToolRuleFace } from "./tool-face.js";
18
19
  /**
19
20
  * Side-effect class of a tool. Used by wake/resume reconciliation: when a call was interrupted
@@ -819,6 +820,19 @@ export interface ToolExecuteContext {
819
820
  * the tool runs outside a Runner task.
820
821
  */
821
822
  model?: Model;
823
+ /**
824
+ * WHICH REF chose {@link model} — the catalog word the parent leg is running under, or the `Model`
825
+ * object itself when no word exists (an object-pinned spec, a `select`-criteria role). Snapshotted at
826
+ * the same call-time instant as {@link model}, and its companion in the same sense {@link thinkingLevel}
827
+ * is: one read, one moment.
828
+ *
829
+ * A delegation tool inheriting the caller's model reads THIS, not {@link model}: a word resolves against
830
+ * the catalog generation in force at the CHILD's own spawn, so a child spawned after a catalog hot-swap
831
+ * lands on the current generation instead of on the object its parent has been holding since its own
832
+ * prepare (and a word the swap retired refuses loudly, exactly as a caller-written retired word does).
833
+ * Undefined when the tool runs outside a Runner task — the same absence {@link model} has there.
834
+ */
835
+ modelRef?: ModelRef;
822
836
  /**
823
837
  * The PARENT task's current thinking level, snapshotted at call time — the {@link model} companion.
824
838
  * A delegation tool threads it into the child so a sub-agent inherits the caller's thinking depth by
@@ -158,6 +158,14 @@ export interface WiringManifest {
158
158
  leg?: {
159
159
  kind: WiringLegKind;
160
160
  };
161
+ /** Effective half only — the leg's team role ({@link import("../agents/team-role.js").teamRoleOf}):
162
+ * `lead` for a top-level leg, `teammate` for a named delegation, `none` for an unnamed one. Leg-local
163
+ * (excluded from the fingerprint). */
164
+ teamRole?: import("../agents/team-role.js").TeamRole;
165
+ /** Effective half only, present iff the spec reported one — the session's self-reported presence,
166
+ * the very object `TaskSpec.peerPresence` carried, for a host to copy into the directory row it
167
+ * writes without parsing the spec. Leg-local (excluded from the fingerprint). */
168
+ peerPresence?: import("../agents/peer-directory.js").PeerPresence;
161
169
  ask: {
162
170
  form: AskSeamForm;
163
171
  /** Absent iff `form === "absent"` (no seat supplied a value — a fabricated provenance would be
@@ -413,6 +421,10 @@ export interface WiringFacts {
413
421
  half: "static" | "effective";
414
422
  /** Required on the effective half; must be absent on the static half. */
415
423
  leg?: WiringLegKind;
424
+ /** Effective half only — see {@link WiringManifest.teamRole}. */
425
+ teamRole?: import("../agents/team-role.js").TeamRole;
426
+ /** Effective half only — see {@link WiringManifest.peerPresence}; copied, never aliased. */
427
+ peerPresence?: import("../agents/peer-directory.js").PeerPresence;
416
428
  askForm: AskSeamForm;
417
429
  askProvenance?: SeamProvenance;
418
430
  questionWired: boolean;
@@ -146,9 +146,13 @@ export function deriveWiringManifest(facts) {
146
146
  ? { readDeny: { builtinTiers: [...facts.readDenyBuiltinTiersActive] } }
147
147
  : {}),
148
148
  ...(facts.half === "effective" && facts.autoConsolidationOnRecommendation === true ? { autoConsolidation: { onRecommendation: true } } : {}),
149
+ ...(facts.half === "effective" && facts.teamRole !== undefined ? { teamRole: facts.teamRole } : {}),
150
+ ...(facts.half === "effective" && facts.peerPresence !== undefined && (facts.peerPresence.agent !== undefined || facts.peerPresence.intent !== undefined)
151
+ ? { peerPresence: { ...(facts.peerPresence.agent !== undefined ? { agent: facts.peerPresence.agent } : {}), ...(facts.peerPresence.intent !== undefined ? { intent: facts.peerPresence.intent } : {}) } }
152
+ : {}),
149
153
  };
150
154
  if (facts.half === "effective") {
151
- const { leg: _leg, mcp: _mcp, tools: _tools, lsp: _lsp, hooks: _hooks, ...assembly } = manifest;
155
+ const { leg: _leg, mcp: _mcp, tools: _tools, lsp: _lsp, hooks: _hooks, teamRole: _teamRole, peerPresence: _peerPresence, ...assembly } = manifest;
152
156
  const { provenance: _askSeat, ...askForHash } = assembly.ask;
153
157
  const { provenance: _questionSeat, ...questionForHash } = assembly.question;
154
158
  manifest.configFingerprint = createHash("sha256")