@sema-agent/core 5.47.0 → 5.48.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 (52) hide show
  1. package/CHANGELOG.md +52 -0
  2. package/dist/agents/agent-transcript-tool.d.ts +4 -0
  3. package/dist/agents/agent-transcript-tool.js +10 -3
  4. package/dist/agents/send-message-tool.d.ts +43 -1
  5. package/dist/agents/send-message-tool.js +50 -11
  6. package/dist/agents/subagent.d.ts +18 -0
  7. package/dist/agents/subagent.js +102 -2
  8. package/dist/config/defaults.d.ts +20 -0
  9. package/dist/config/defaults.js +5 -0
  10. package/dist/core/background-agent-store.d.ts +1 -0
  11. package/dist/core/background-agent-store.js +13 -0
  12. package/dist/core/mcp.d.ts +6 -1
  13. package/dist/core/mcp.js +34 -7
  14. package/dist/core/reminder-disclosure.d.ts +90 -0
  15. package/dist/core/reminder-disclosure.js +64 -0
  16. package/dist/core/runner/prepare-acquire-reconcile.d.ts +6 -0
  17. package/dist/core/runner/prepare-acquire-reconcile.js +1 -1
  18. package/dist/core/runner/prepare-hands-readface.d.ts +4 -0
  19. package/dist/core/runner/prepare-hands-readface.js +1 -0
  20. package/dist/core/runner/prepare-task.d.ts +15 -0
  21. package/dist/core/runner/prepare-task.js +48 -30
  22. package/dist/core/runner/runtask.js +3 -1
  23. package/dist/core/session-store.d.ts +59 -1
  24. package/dist/core/session-store.js +82 -14
  25. package/dist/core/session.d.ts +83 -1
  26. package/dist/core/task-registry-agent.d.ts +28 -0
  27. package/dist/core/task-registry-agent.js +63 -2
  28. package/dist/core/task-registry.d.ts +21 -0
  29. package/dist/core/task-registry.js +4 -1
  30. package/dist/core/types.d.ts +60 -0
  31. package/dist/core/untrusted-text.d.ts +63 -0
  32. package/dist/core/untrusted-text.js +48 -0
  33. package/dist/core/wiring-manifest.d.ts +35 -0
  34. package/dist/core/wiring-manifest.js +21 -1
  35. package/dist/engine/harness/types.d.ts +36 -1
  36. package/dist/index.d.ts +5 -4
  37. package/dist/index.js +4 -3
  38. package/dist/internal/harness-types.d.ts +1 -0
  39. package/dist/stores/file/index.d.ts +19 -3
  40. package/dist/stores/file/index.js +24 -1
  41. package/dist/stores/file/session-store.d.ts +18 -4
  42. package/dist/stores/file/session-store.js +73 -12
  43. package/dist/tools/fs/fs-pdf.d.ts +12 -1
  44. package/dist/tools/fs/fs-pdf.js +17 -3
  45. package/dist/tools/fs/fs-read.d.ts +2 -1
  46. package/dist/tools/fs/fs-read.js +33 -5
  47. package/dist/tools/fs/fs-shared.d.ts +6 -2
  48. package/dist/tools/fs/index.d.ts +7 -0
  49. package/dist/tools/fs/index.js +1 -1
  50. package/dist/tools/web.js +21 -2
  51. package/package.json +3 -2
  52. package/test/export-surface.snapshot.json +15 -1
package/CHANGELOG.md CHANGED
@@ -1,5 +1,57 @@
1
1
  # Changelog
2
2
 
3
+ ## 5.48.0 — 2026-08-20
4
+
5
+ ### Added
6
+ - design/319 tickets B+C+D — the reminder-impersonation defense completes (A shipped 5.46.0):
7
+ - **Detect-and-disclose trailer** (four outlets: plain-text Read, notebook projection,
8
+ extracted PDF text, MCP success results): external bytes containing reminder-shaped text
9
+ gain a trailing disclosure block. Two wordings: bare form (reminder-shaped, markless —
10
+ throttled 60s per file / per server:tool) and marked form (carries the CURRENT session mark,
11
+ definitionally zero legitimate probability — never throttled). Scan point = the final
12
+ model-facing projection at each outlet.
13
+ - **Narrow defuse** (MCP success + web only): a block carrying the exact current mark is
14
+ defused (mark bytes broken) with a disclosure line. Read-family outlets stay byte-verbatim
15
+ (editing-loop preservation; disclosure-only there).
16
+ - Observability: `TaskResult.stats.mechanisms.reminderDisclosures`
17
+ (`Record<"outlet.form", number>`). Gate: `npm run gate:reminder-literal` (AST; mint-home
18
+ exclusivity). Public doc: `docs/REMINDER-PROVENANCE.md` (mechanism, explicit limits,
19
+ standards-evolution swap point).
20
+ - design/323 — subagent transcript persistence on the EXISTING SessionStore ("no new store"),
21
+ root-fixing revival across process restarts:
22
+ - **Placement contract** (all additive/optional): `SessionStore.placements` declaration,
23
+ placement-bearing acquire, `listPlaced`, six obligations (host-list exclusion, claim
24
+ refusal `placement_refused`, tuple immutability, mirror-direction identity check,
25
+ placed release = real row deletion with failures surfaced, partition enumeration).
26
+ `FileStorageBackend` gains `backgroundAgentStore`/`mailboxStore`/`rosterStore` (aggregation
27
+ parity with session/checkpoint/tool-result; half-wired silent-degradation shape closed) and
28
+ declares durable subagent placements.
29
+ - **Revival**: a completed background subagent now answers `SendMessage` across a process
30
+ restart (disk-replayed transcript; `status:"revived"`, cycle sequence continuous). The
31
+ anonymous-agent gate is retired (anonymous rows revive too). User-stopped agents stay
32
+ blocked on the model lane; `createAgentContinuationVerb` (module-private symbol,
33
+ `userInitiated`) is the operator door. `resolveSubagentTranscriptTier` (none/rows/full)
34
+ projects as `WiringManifest.fleet.subagentTranscripts`.
35
+ - **Caps + reaping**: `RunnerDeps.delegationEntryCaps` (defaults 20 concurrent / 200 retained;
36
+ bad values refuse via `config.delegation_entry_caps`; rejections
37
+ `delegation.concurrency_cap`/`delegation.session_cap`); the durable-agent reap gains a
38
+ placed-partition leg (`orphanPlacedReleased`) with same- and cross-process live-agent
39
+ protection; `delegation.transcript_integrity` notice. New exports: placement types,
40
+ tier/caps resolvers, continuation verb, five constants (+14, all additive).
41
+
42
+ ### Fixed
43
+ - The shipped third-party memory-backend reference template refuses cross-scope re-adds and
44
+ judges whitewash on the PRE-BATCH committed snapshot (delete-then-readd in one batch can no
45
+ longer blank an origin; `guard:"absent"` same-rev idempotent arm kept; presence-judged strip
46
+ refusal). A resident pin now runs the template through the full conformance suite on every
47
+ suite execution.
48
+
49
+ ### Notes
50
+ - Read/MCP results that contain reminder-shaped data grow a trailing block (additive, ~40
51
+ tokens); MCP/web segments carrying the exact current session mark are byte-defused (narrowing;
52
+ legitimate-content probability zero, disclosed inline). Everything else in the 319/323 batch
53
+ is additive; no BREAKING changes.
54
+
3
55
  ## 5.47.0 — 2026-08-20
4
56
 
5
57
  ### Added
@@ -28,6 +28,10 @@ export interface AgentTranscriptToolOptions {
28
28
  * design/157 A7 — behind). Absent ⇒ unchanged behavior: the `opts` axes alone supply the identity.
29
29
  */
30
30
  enrichCtx?: import("../core/tools.js").ToolCtxEnricher;
31
+ /** Subagent transcript persistence — the declared-durability integrity-gap disclosure sink (see
32
+ * the SendMessage twin): a durable row with a bound sessionId whose session store attests
33
+ * not_found. Announced once per (handle, process); absent ⇒ the honest per-call degrade alone. */
34
+ onTranscriptIntegrityGap?: (handle: string) => void;
31
35
  }
32
36
  /**
33
37
  * Residual observability (lane C) — build the MODEL-VISIBLE **AgentTranscript** tool: read the last N tool steps of a
@@ -1,6 +1,7 @@
1
1
  import { Type } from "typebox";
2
2
  import { defineTool } from "../core/tools.js";
3
- import { canAccessAgentRecord } from "../core/background-agent-store.js";
3
+ import { announceTranscriptIntegrityGapOnce, canAccessAgentRecord } from "../core/background-agent-store.js";
4
+ import { DURABLE_AGENT_HANDLE_RE } from "../core/task-registry.js";
4
5
  import { inlineUntrusted } from "../core/untrusted-text.js";
5
6
  import { stepsFromMessages } from "./subagent-steps.js";
6
7
  import { REPORT_FIELD_MAX } from "./subagent.js";
@@ -61,7 +62,10 @@ export function createAgentTranscriptTool(opts) {
61
62
  details: { type: "agent-transcript", id, steps },
62
63
  };
63
64
  }
64
- catch {
65
+ catch (e) {
66
+ if (e?.code === "not_found") {
67
+ announceTranscriptIntegrityGapOnce(id, opts.onTranscriptIntegrityGap);
68
+ }
65
69
  return undefined;
66
70
  }
67
71
  };
@@ -77,7 +81,10 @@ export function createAgentTranscriptTool(opts) {
77
81
  const durable = await readDurableSteps();
78
82
  if (durable)
79
83
  return durable;
80
- return { content: `No transcript: no background agent task "${id}" (unknown id, not yours, or expired).`, details: { error: "not_found", id }, isError: true };
84
+ if (DURABLE_AGENT_HANDLE_RE.test(id)) {
85
+ return { content: `No transcript found for agent ${id}.`, details: { error: "not_found", id }, isError: true };
86
+ }
87
+ return { content: `No transcript: no background agent task "${id}" (unknown id, or not yours).`, details: { error: "not_found", id }, isError: true };
81
88
  }
82
89
  const runLedger = ctx.subagentRetain ?? undefined;
83
90
  const atLedgerSessionId = ctx.sessionId ?? opts.sessionId;
@@ -2,6 +2,7 @@ import { Type } from "typebox";
2
2
  import type { Runner } from "../core/runner/runtask.js";
3
3
  import type { TaskNotificationPayload } from "../core/task-notification.js";
4
4
  import { type ToolCtxEnricher } from "../core/tools.js";
5
+ import { type TaskAccess } from "../core/task-registry.js";
5
6
  import { SubagentRetainLedger } from "./retain-ledger.js";
6
7
  import { type SubagentSteerHandle } from "./subagent.js";
7
8
  import { type PeerAdmissionConfig, type PeerInboundChainRef, type PeerSelfRef } from "./peer-admission.js";
@@ -125,6 +126,14 @@ export interface SendMessageToolOptions {
125
126
  * A direct mount that wires `uplink` without this leaves L1 ungated (recorded honest boundary:
126
127
  * the library does not fabricate an identity on the host's behalf). */
127
128
  uplinkRecipient?: PeerSelfRef;
129
+ /**
130
+ * Subagent transcript persistence — the "declared durability contradicted by reality" disclosure
131
+ * sink: called (at most once per handle per process, {@link announceTranscriptIntegrityGapOnce})
132
+ * when a durable row with a bound sessionId meets a session store that attests `not_found`. The
133
+ * first-party mount wires an EngineNotice (`delegation.transcript_integrity`); absent ⇒ the
134
+ * per-call honest refusal is the only loudness (unchanged behavior).
135
+ */
136
+ onTranscriptIntegrityGap?: (handle: string) => void;
128
137
  }
129
138
  /** RB-382 — max chars of the `summary` arg (the short recap label, distinct from `message`'s much
130
139
  * larger {@link UPLINK_RESULT_MAX}) before ITS OWN truncation. Exported for direct unit-testing only
@@ -152,8 +161,41 @@ export declare function clipSendMessageSummary(raw: string): string;
152
161
  * second revive). The auto-mode peer-referral tighten keys on the WIRE NAME and applies to a direct
153
162
  * mount all the same.
154
163
  */
155
- export declare function createSendMessageTool(opts: SendMessageToolOptions): import("../core/types.js").AgentTool<Type.TObject<{
164
+ export declare function createSendMessageTool(opts: SendMessageToolOptions): import("../internal/harness.js").AgentTool<Type.TObject<{
156
165
  to: Type.TString;
157
166
  message: Type.TString;
158
167
  summary: Type.TOptional<Type.TString>;
159
168
  }>, unknown>;
169
+ /** The receipt {@link createAgentContinuationVerb} resolves with — the SendMessage tool's own
170
+ * receipt family, flattened to one text (same content, same `details.error` code vocabulary). */
171
+ export interface AgentContinuationReceipt {
172
+ content: string;
173
+ details?: unknown;
174
+ isError?: boolean;
175
+ }
176
+ /**
177
+ * Subagent transcript persistence — the OPERATOR continuation verb: the SendMessage delivery ladder
178
+ * as a reusable, non-model face for a deployment's own continuation endpoint. SINGLE-SOURCE by
179
+ * construction: the verb routes THROUGH `createSendMessageTool`'s product (status arms, admission
180
+ * gates, preflight order, claim CAS + rollback, parked/running refusals and the whole refusal-code
181
+ * family are the tool's own, byte-shared — never a twin implementation). The one fork is the
182
+ * `userInitiated` mode bit ({@link OPERATOR_CONTINUATION_CTX}): `true` waives BOTH user-stop gates —
183
+ * the durable row's `stoppedBy === "user"` arm and the live registry's stop-attribution arm — so an
184
+ * explicit human "continue it" overrides an earlier human "stop it" (the winning claim's
185
+ * clear-terminal-payload erases the durable mark; the model-facing tool NEVER softens).
186
+ *
187
+ * Authorization is three-layered and this verb owns only the first: ① the same
188
+ * `canAccessAgentRecord` row predicate the tool runs (an out-of-scope handle reads not_found —
189
+ * non-leaking); ② "this really is an explicit human instruction" is the DEPLOYMENT's obligation —
190
+ * the same trust seat that stamps `stopSource:"user"` on the stop side (a server exposes this only
191
+ * through a verified interactive-user principal endpoint, never to unattended machine clients); ③ the model
192
+ * tool face has no such parameter (the bit travels on a module-private symbol — zero forgeable
193
+ * surface from arguments).
194
+ *
195
+ * Store-conditional like every continuation face: with no durable stores wired the live-gate waiver
196
+ * still works for same-process handles, and the durable leg refuses honestly.
197
+ */
198
+ export declare function createAgentContinuationVerb(opts: SendMessageToolOptions): (handle: string, content: string, access: TaskAccess, o?: {
199
+ userInitiated?: true;
200
+ summary?: string;
201
+ }) => Promise<AgentContinuationReceipt>;
@@ -1,7 +1,8 @@
1
1
  import { Type } from "typebox";
2
2
  import { defineTool } from "../core/tools.js";
3
3
  import { normalizeAgentName, DURABLE_AGENT_HANDLE_RE, DURABLE_AGENT_HEARTBEAT_MS } from "../core/task-registry.js";
4
- import { canAccessAgentRecord, clearRevivedRowTerminalPayload } from "../core/background-agent-store.js";
4
+ import { announceTranscriptIntegrityGapOnce, canAccessAgentRecord, clearRevivedRowTerminalPayload } from "../core/background-agent-store.js";
5
+ import { uuidv7 } from "../internal/harness.js";
5
6
  import { MAILBOX_TOMBSTONED_RECIPIENT_CODE } from "../core/mailbox-store.js";
6
7
  import { escapeAttributeValue, escapeEnvelopeTag, isObserverTaskId, OBSERVER_SENDMESSAGE_SENDER_REFUSAL, OBSERVER_SENDMESSAGE_TARGET_REFUSAL, } from "./observer.js";
7
8
  import { SUBAGENT_RESUME_CAP, SubagentRetainLedger, getSessionRetainLedger } from "./retain-ledger.js";
@@ -41,6 +42,7 @@ function withTargetLane(key, fn) {
41
42
  function targetLaneKey(scope, targetId) {
42
43
  return JSON.stringify([scope ?? "", targetId]);
43
44
  }
45
+ const OPERATOR_CONTINUATION_CTX = Symbol("sema.operator_continuation");
44
46
  export function createSendMessageTool(opts) {
45
47
  const tier3Capable = opts.agentStore !== undefined && opts.mailbox !== undefined && opts.reviveSpawn !== undefined;
46
48
  return defineTool({
@@ -56,9 +58,9 @@ export function createSendMessageTool(opts) {
56
58
  `address an older one. You will be notified automatically when it completes — prefer ending your turn; do not ` +
57
59
  `poll. ` +
58
60
  (tier3Capable
59
- ? `Continuing a finished agent works for NAMED agents (revived from their durable record, even across restarts) and for runs that retain sub-agent sessions; when neither covers it you get an honest error and should launch a new agent with the needed context instead.`
61
+ ? `Continuing a finished agent works for agents with a durable record — by name or task_id, even across restarts and for runs that retain sub-agent sessions; when neither covers it you get an honest error and should launch a new agent with the needed context instead.`
60
62
  : `Continuing a finished agent requires the run to retain sub-agent sessions; when the session was not ` +
61
- `retained (or has expired) you get an honest error and should launch a new agent with the needed context instead.`),
63
+ `retained (or is no longer held) you get an honest error and should launch a new agent with the needed context instead.`),
62
64
  parameters: Type.Object({
63
65
  to: Type.String({ description: 'Recipient: the agent\'s name, or its task_id (a…) returned by the Agent tool with run_in_background. "main" is reserved for the spawning conversation.' }),
64
66
  message: Type.String({ description: "The follow-up request. The agent continues from its full prior context." }),
@@ -66,6 +68,7 @@ export function createSendMessageTool(opts) {
66
68
  }),
67
69
  execute: async (args, ctx) => {
68
70
  const a = args;
71
+ const operatorUserInitiated = ctx[OPERATOR_CONTINUATION_CTX] === true;
69
72
  const to = String(a.to ?? "").trim();
70
73
  const message = String(a.message ?? "").trim();
71
74
  if (!to)
@@ -201,10 +204,10 @@ export function createSendMessageTool(opts) {
201
204
  if (!canAccessAgentRecord(row, parentAccess))
202
205
  return undefined;
203
206
  }
204
- if (row.name === undefined || row.agentType === "fork")
207
+ if (row.agentType === "fork")
205
208
  return undefined;
206
209
  const whoT3 = handle === to ? `agent ${handle}` : `agent "${to}" (${handle})`;
207
- if (row.status === "killed" && row.stoppedBy === "user") {
210
+ if (row.status === "killed" && row.stoppedBy === "user" && !operatorUserInitiated) {
208
211
  return {
209
212
  content: `Message not sent: ${whoT3} was stopped by the user and was not resumed. Treat its work as cancelled; only start a new agent for it if the user explicitly asks.`,
210
213
  details: { error: "killed", to, stoppedBy: row.stoppedBy },
@@ -289,7 +292,8 @@ export function createSendMessageTool(opts) {
289
292
  catch (e) {
290
293
  await rollbackDrain();
291
294
  if (e?.code === "not_found") {
292
- return queueFullReceipt(`Its transcript session no longer exists (evicted or reaped), so a revival cannot drain the backlog.`);
295
+ announceTranscriptIntegrityGapOnce(handle, opts.onTranscriptIntegrityGap);
296
+ return queueFullReceipt(`No transcript found for agent ${handle}, so a revival cannot drain the backlog.`);
293
297
  }
294
298
  return queueFullReceipt(`Its transcript store did not answer (${e instanceof Error ? e.message : String(e)}) — this send did not start a backlog drain; a later resend will try again.`);
295
299
  }
@@ -387,8 +391,9 @@ export function createSendMessageTool(opts) {
387
391
  catch (e) {
388
392
  await rollback();
389
393
  if (e?.code === "not_found") {
394
+ announceTranscriptIntegrityGapOnce(handle, opts.onTranscriptIntegrityGap);
390
395
  return {
391
- content: `Message not sent: ${whoT3}'s transcript session no longer exists (evicted or reaped) — it cannot be revived. Launch a new agent with the needed context instead.`,
396
+ content: `Message not sent: no transcript found for agent ${handle} — it cannot be revived. Launch a new agent with the needed context instead.`,
392
397
  details: { error: "resume.session_not_found", to },
393
398
  isError: true,
394
399
  };
@@ -533,8 +538,15 @@ export function createSendMessageTool(opts) {
533
538
  const labels = parentAccess !== undefined
534
539
  ? [...ownLabels, ...opts.registry.runningBackgroundAgentLabels(parentAccess).filter((l) => !ownLabels.includes(l))]
535
540
  : ownLabels;
541
+ if (DURABLE_AGENT_HANDLE_RE.test(to)) {
542
+ return {
543
+ content: `Message not sent: no transcript found for agent ${to} — launch a new agent with the needed context instead.`,
544
+ details: { error: "not_found", to },
545
+ isError: true,
546
+ };
547
+ }
536
548
  return {
537
- content: `Message not sent: no agent matches "${to}" (unknown id or name, not yours, or expired).` +
549
+ content: `Message not sent: no agent matches "${to}" (unknown id or name, or not yours).` +
538
550
  (suggestion !== undefined ? ` Did you mean: ${suggestion}?` : "") +
539
551
  (labels.length > 0 ? ` Running background agents: ${labels.join(", ")}.` : "") +
540
552
  ` Note: a completed foreground agent is not resumable (its transcript is not retained) — spawn with run_in_background to keep an agent addressable, or launch a new agent.`,
@@ -617,7 +629,7 @@ export function createSendMessageTool(opts) {
617
629
  }
618
630
  if (row.status === "killed") {
619
631
  const by = opts.registry.getStopAttribution(targetId);
620
- if (by === "user") {
632
+ if (by === "user" && !operatorUserInitiated) {
621
633
  return {
622
634
  content: `Message not sent: ${who} was stopped by the user and was not resumed. Treat its work as cancelled; only start a new agent for it if the user explicitly asks.`,
623
635
  details: { error: "killed", to, stoppedBy: by },
@@ -712,9 +724,10 @@ export function createSendMessageTool(opts) {
712
724
  const text = code === "resume.retain_off"
713
725
  ? `${who}'s session was not retained (retainSubagentSessions is off) — relaunch a new agent instead.`
714
726
  : code === "resume.evicted"
715
- ? `${who}'s retained session was evicted (retain TTL / capacity / parent run ended) — relaunch a new agent instead.`
727
+ ?
728
+ `${who} is no longer held in this run's fast lane (retain window or capacity passed, or the parent run ended) — relaunch a new agent with the needed context instead.`
716
729
  : code === "resume.session_not_found"
717
- ? `${who}'s session no longer exists — relaunch a new agent instead.`
730
+ ? `no transcript found for ${who} — relaunch a new agent with the needed context instead.`
718
731
  : code === "resume.cap"
719
732
  ? `${who} reached its resume cap (${SUBAGENT_RESUME_CAP} follow-ups per agent) — relaunch a new agent instead.`
720
733
  : code === "steering.still_running"
@@ -732,3 +745,29 @@ export function createSendMessageTool(opts) {
732
745
  },
733
746
  }, opts.enrichCtx !== undefined ? { enrichCtx: opts.enrichCtx } : {});
734
747
  }
748
+ export function createAgentContinuationVerb(opts) {
749
+ return async (handle, content, access, o) => {
750
+ const tool = createSendMessageTool({
751
+ ...opts,
752
+ enrichCtx: (base) => {
753
+ const enriched = {
754
+ ...base,
755
+ ...(access.owner !== undefined ? { taskId: access.owner } : {}),
756
+ ...(access.scope !== undefined ? { principal: access.scope } : {}),
757
+ ...(access.sessionId !== undefined ? { sessionId: access.sessionId } : {}),
758
+ };
759
+ if (o?.userInitiated === true)
760
+ enriched[OPERATOR_CONTINUATION_CTX] = true;
761
+ return enriched;
762
+ },
763
+ });
764
+ const summaryRaw = (o?.summary ?? content).replace(/\s+/g, " ").trim().slice(0, 200);
765
+ const summary = summaryRaw === "" ? "operator follow-up" : summaryRaw;
766
+ const out = await tool.execute(`op-${uuidv7()}`, { to: handle, message: content, summary });
767
+ const text = out.content
768
+ .filter((c) => c.type === "text")
769
+ .map((c) => c.text)
770
+ .join("\n");
771
+ return { content: text, ...(out.details !== undefined ? { details: out.details } : {}), ...(out.isError === true ? { isError: true } : {}) };
772
+ };
773
+ }
@@ -466,6 +466,24 @@ export declare function createSubagentResume(deps: {
466
466
  * isolation scope (first failure per site; later ones counted only). */
467
467
  onNotifyError?: (failure: import("../core/safe-notify.js").SafeNotifyFailure) => void;
468
468
  }): (content: string) => Promise<string>;
469
+ /** Subagent transcript persistence — the RESOLVED delegation entry-caps pair (both members present;
470
+ * see {@link resolveDelegationEntryCaps}). */
471
+ export interface ResolvedDelegationEntryCaps {
472
+ maxConcurrent: number;
473
+ maxCumulativePerSession: number;
474
+ }
475
+ /**
476
+ * Subagent transcript persistence — resolve + LOUDLY validate `RunnerDeps.delegationEntryCaps`
477
+ * (#123 bad-value law: a garbage knob refuses with a coded error, it is never silently folded to
478
+ * the defaults). Refusal shapes: a member that is not a positive integer (NaN, 0, negative,
479
+ * fractional, non-number), and a RESOLVED pair where `maxConcurrent > maxCumulativePerSession`
480
+ * (you cannot run more agents at once than the tree may ever create — a contradiction, not a
481
+ * preference). Absent members take the exported CC-parity defaults.
482
+ */
483
+ export declare function resolveDelegationEntryCaps(caps: {
484
+ maxConcurrent?: number;
485
+ maxCumulativePerSession?: number;
486
+ } | undefined): ResolvedDelegationEntryCaps;
469
487
  /**
470
488
  * Options for {@link createSubagentTool}.
471
489
  *
@@ -24,7 +24,7 @@ import { BG_AGENT_REAP_STOP_ERROR } from "../core/task-registry.js";
24
24
  import { readDurableOrgAdmission } from "../core/memory-admission.js";
25
25
  import { extractErrorCode } from "../brain/errors.js";
26
26
  import { governanceRetryClass } from "../core/governance-codes.js";
27
- import { RETAIN_DEFAULT_TTL_MS, RETAIN_DEFAULT_MAX, RUNNING_AGENT_OBSERVE_EVERY_BEATS } from "../config/defaults.js";
27
+ import { RETAIN_DEFAULT_TTL_MS, RETAIN_DEFAULT_MAX, RUNNING_AGENT_OBSERVE_EVERY_BEATS, DELEGATION_MAX_CONCURRENT_DEFAULT, DELEGATION_MAX_PER_SESSION_DEFAULT } from "../config/defaults.js";
28
28
  function rollupDelegatedCost(stats, nested) {
29
29
  if (stats.costMicroUsd === undefined)
30
30
  return undefined;
@@ -875,6 +875,43 @@ function parkCompletionNotify(deps) {
875
875
  };
876
876
  entry.deferredNotify = { ...(payload.seq !== undefined ? { seq: payload.seq } : {}), cancel, flush: () => deliver(false) };
877
877
  }
878
+ export function resolveDelegationEntryCaps(caps) {
879
+ const bad = (member, value) => {
880
+ const e = new Error(`RunnerDeps.delegationEntryCaps.${member} holds ${typeof value === "number" ? String(value) : value === null ? "null" : typeof value} — must be a positive integer (or absent for the default). A bad cap refuses loudly; it is never silently folded to the default.`);
881
+ e.code = "config.delegation_entry_caps";
882
+ throw e;
883
+ };
884
+ const member = (name, fallback) => {
885
+ const v = caps?.[name];
886
+ if (v === undefined)
887
+ return fallback;
888
+ if (typeof v !== "number" || !Number.isInteger(v) || v <= 0)
889
+ bad(name, v);
890
+ return v;
891
+ };
892
+ const maxConcurrent = member("maxConcurrent", DELEGATION_MAX_CONCURRENT_DEFAULT);
893
+ const maxCumulativePerSession = member("maxCumulativePerSession", DELEGATION_MAX_PER_SESSION_DEFAULT);
894
+ if (maxConcurrent > maxCumulativePerSession) {
895
+ const e = new Error(`RunnerDeps.delegationEntryCaps resolves to maxConcurrent ${maxConcurrent} > maxCumulativePerSession ${maxCumulativePerSession} — a tree cannot run more agents at once than it may ever create. Fix the pair; a contradictory configuration refuses loudly.`);
896
+ e.code = "config.delegation_entry_caps";
897
+ throw e;
898
+ }
899
+ return { maxConcurrent, maxCumulativePerSession };
900
+ }
901
+ const delegationEntryLedgers = new WeakMap();
902
+ function delegationEntryLedger(registry, key) {
903
+ let byKey = delegationEntryLedgers.get(registry);
904
+ if (byKey === undefined) {
905
+ byKey = new Map();
906
+ delegationEntryLedgers.set(registry, byKey);
907
+ }
908
+ let set = byKey.get(key);
909
+ if (set === undefined) {
910
+ set = new Set();
911
+ byKey.set(key, set);
912
+ }
913
+ return set;
914
+ }
878
915
  export function normalizeSubagentType(value) {
879
916
  return value.normalize("NFKC").toLowerCase().replace(/[\p{White_Space}\p{Pd}_]+/gu, "");
880
917
  }
@@ -2432,6 +2469,53 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
2432
2469
  const shortDesc = reviveRow?.description ?? String(a.description ?? "sub-agent").slice(0, 200);
2433
2470
  const bgOwner = reviveRow !== undefined ? reviveRow.owner : sessionScopedBg ? ctx.sessionId : ctx.taskId ?? bg.owner;
2434
2471
  const bgScope = treeScope;
2472
+ const capScope = bgScope;
2473
+ const capRoot = reviveRow !== undefined
2474
+ ? (reviveRow.rootSessionId ?? reviveRow.parentSessionId ?? (reviveRow.sessionScoped ? reviveRow.owner : undefined))
2475
+ : (ctx.rootSessionId ?? ctx.sessionId);
2476
+ const entryCaps = ctx.delegationEntryCaps ?? { maxConcurrent: DELEGATION_MAX_CONCURRENT_DEFAULT, maxCumulativePerSession: DELEGATION_MAX_PER_SESSION_DEFAULT };
2477
+ const capLedgerKey = capScope !== undefined && capRoot !== undefined ? JSON.stringify([capScope, capRoot]) : undefined;
2478
+ if (capScope !== undefined && capRoot !== undefined) {
2479
+ let storedHandles = [];
2480
+ let storedEnumerationOk = false;
2481
+ if (bg.agentStore !== undefined && reviveRow === undefined) {
2482
+ try {
2483
+ storedHandles = (await bg.agentStore.listBySession(capScope, capRoot)).map((r) => r.handle);
2484
+ storedEnumerationOk = true;
2485
+ }
2486
+ catch {
2487
+ storedHandles = [];
2488
+ }
2489
+ }
2490
+ const capRefusal = async (code, text) => {
2491
+ dropHostAbortListener();
2492
+ await cancelObserver();
2493
+ const wt = await finishWorktree();
2494
+ return { isError: true, content: `Sub-agent not started in background: ${text}${wt ? `\n${wt}` : ""}`, details: { error: code } };
2495
+ };
2496
+ const activeFace = bg.registry.activeDelegationHandles;
2497
+ const active = typeof activeFace === "function" ? activeFace.call(bg.registry, capScope, capRoot) : [];
2498
+ if (typeof activeFace === "function" && active.length >= entryCaps.maxConcurrent) {
2499
+ return await capRefusal("delegation.concurrency_cap", `this session tree already has ${active.length} background agents running — the concurrency cap (${entryCaps.maxConcurrent}; RunnerDeps.delegationEntryCaps.maxConcurrent) refuses another. Wait for one to complete (you will be notified) or stop one, then relaunch.`);
2500
+ }
2501
+ if (reviveRow === undefined) {
2502
+ const ledger = delegationEntryLedger(bg.registry, capLedgerKey);
2503
+ let cumulative;
2504
+ if (bg.agentStore !== undefined && storedEnumerationOk) {
2505
+ const keep = new Set([...storedHandles, ...active]);
2506
+ for (const h of [...ledger])
2507
+ if (!keep.has(h))
2508
+ ledger.delete(h);
2509
+ cumulative = new Set([...storedHandles, ...ledger]).size;
2510
+ }
2511
+ else {
2512
+ cumulative = ledger.size;
2513
+ }
2514
+ if (cumulative >= entryCaps.maxCumulativePerSession) {
2515
+ return await capRefusal("delegation.session_cap", `this session tree has already launched ${cumulative} background agents in its retained window — the cumulative cap (${entryCaps.maxCumulativePerSession}; RunnerDeps.delegationEntryCaps.maxCumulativePerSession) refuses more. Continue an existing agent (SendMessage) instead of launching new ones.`);
2516
+ }
2517
+ }
2518
+ }
2435
2519
  const settlementSeat = ctx.delegationSettlement?.();
2436
2520
  const bgSettleId = settlementSeat !== undefined ? `bg-${uuidv7()}` : undefined;
2437
2521
  if (settlementSeat !== undefined && bgSettleId !== undefined) {
@@ -2554,6 +2638,8 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
2554
2638
  }
2555
2639
  return { isError: true, content: `Sub-agent not started in background: ${e instanceof Error ? e.message : String(e)}${wt ? `\n${wt}` : ""}`, details: { error: "register_failed" } };
2556
2640
  }
2641
+ if (capLedgerKey !== undefined && reviveRow === undefined)
2642
+ delegationEntryLedger(bg.registry, capLedgerKey).add(taskId);
2557
2643
  childInternals.peerSelfRef?.addAxis("h", taskId);
2558
2644
  if (agentName !== undefined) {
2559
2645
  recordRosterSpawn(ctx.roster, { name: agentName, agentId: taskId, toolUseId: ctx.toolCallId, owner: bgOwner, scope: bgScope, ...((typeof childModel === "string" ? resolveModelDisplayLabel(childModel) : childModel?.id) !== undefined ? { model: typeof childModel === "string" ? childModel : childModel?.id } : {}), ...(reviveRow !== undefined ? ((reviveRow.rootSessionId ?? reviveRow.parentSessionId) !== undefined ? { rootSessionId: reviveRow.rootSessionId ?? reviveRow.parentSessionId } : {}) : (ctx.rootSessionId ?? ctx.sessionId) !== undefined ? { rootSessionId: ctx.rootSessionId ?? ctx.sessionId } : {}), ...(sessionScopedBg ? { sessionScoped: true } : {}), createdAt: reviveRow?.spawnedAt ?? Date.now() }, (err) => opts.onObserverError?.(err, { site: "roster.recordSpawn" }));
@@ -2691,10 +2777,22 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
2691
2777
  usage: { toolUses: toolStarts },
2692
2778
  });
2693
2779
  }, () => bg.registry.noteBackgroundAgentActivity(taskId));
2780
+ const bgPlacementRoot = reviveRow !== undefined ? (reviveRow.rootSessionId ?? reviveRow.parentSessionId) : (ctx.rootSessionId ?? ctx.sessionId);
2781
+ const bgPlacementParent = reviveRow !== undefined ? reviveRow.parentSessionId : ctx.sessionId;
2782
+ const bgPlacement = bg.agentStore !== undefined && opts.runner.sessions.placements?.subagent !== undefined
2783
+ ? {
2784
+ kind: "subagent",
2785
+ ...(bgScope !== undefined ? { scope: bgScope } : {}),
2786
+ ...(bgPlacementParent !== undefined ? { parentSessionId: bgPlacementParent } : {}),
2787
+ ...(bgPlacementRoot !== undefined ? { rootSessionId: bgPlacementRoot } : {}),
2788
+ handle: taskId,
2789
+ }
2790
+ : undefined;
2694
2791
  const bgInternals = bgSink
2695
2792
  ? {
2696
2793
  ...childInternals,
2697
2794
  ...(bgQuestionStrip !== undefined ? bgQuestionStrip.internalsFlag : {}),
2795
+ ...(bgPlacement !== undefined ? { sessionPlacement: bgPlacement } : {}),
2698
2796
  onNotifyInjectorReady: s2NotifyReady,
2699
2797
  delegationTaskType: "background_agent",
2700
2798
  ...(bgCycleSeq !== undefined ? { cycleSeq: bgCycleSeq } : {}),
@@ -2730,6 +2828,7 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
2730
2828
  : {
2731
2829
  ...childInternals,
2732
2830
  ...(bgQuestionStrip !== undefined ? bgQuestionStrip.internalsFlag : {}),
2831
+ ...(bgPlacement !== undefined ? { sessionPlacement: bgPlacement } : {}),
2733
2832
  onNotifyInjectorReady: s2NotifyReady,
2734
2833
  delegationTaskType: "background_agent",
2735
2834
  ...(bgCycleSeq !== undefined ? { cycleSeq: bgCycleSeq } : {}),
@@ -2760,7 +2859,8 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
2760
2859
  };
2761
2860
  try {
2762
2861
  await opts.runner.sessions.acquire(bgChildSessionId, { requireExisting: true });
2763
- await opts.runner.sessions.release(bgChildSessionId);
2862
+ if (opts.runner.sessions.forget)
2863
+ await opts.runner.sessions.forget(bgChildSessionId);
2764
2864
  }
2765
2865
  catch (e) {
2766
2866
  const code = e.code;
@@ -21,6 +21,26 @@
21
21
  */
22
22
  export declare const RETAIN_DEFAULT_TTL_MS: number;
23
23
  export declare const RETAIN_DEFAULT_MAX = 16;
24
+ /** Subagent transcript persistence — the delegation ENTRY caps (CC values: 20 concurrent /
25
+ * 200 per session tree). The INLET bound family (`RunnerDeps.delegationEntryCaps`), as opposed to
26
+ * the retain pair above, which is a pure in-memory fast-lane handle bound (outlet — armed
27
+ * deployments lose nothing when it evicts). Concurrent = running/pending a* handles under one
28
+ * (scope, rootSessionId) key in THIS process's registry; cumulative = retained-window count under
29
+ * the same key (see the enforcement site for the exact retained-window semantics). */
30
+ export declare const DELEGATION_MAX_CONCURRENT_DEFAULT = 20;
31
+ export declare const DELEGATION_MAX_PER_SESSION_DEFAULT = 200;
32
+ /** Subagent transcript persistence — orphan adoption (CC parity: after a restart a deployment
33
+ * auto-adopts at most this many stale rows whose transcript mtime is inside the window; older ones
34
+ * stay MANUALLY continuable for the whole retention period — never deleted by the window). The
35
+ * TRIGGER is deployment-owned (core has no daemon and never revives runs nobody asked for); these
36
+ * constants are the shared vocabulary so every deployment adopts by the same numbers. */
37
+ export declare const ORPHAN_ADOPT_WINDOW_MS_DEFAULT: number;
38
+ export declare const ORPHAN_ADOPT_MAX_DEFAULT = 20;
39
+ /** Subagent transcript persistence — the default transcript retention period (CC
40
+ * cleanupPeriodDays parity). Core ships NO sweeper (design/151 ruling 2): the blessed path is the
41
+ * deployment calling `TaskRegistry.reapDurableAgents` with `maxAgeMs` derived from this (boot +
42
+ * every 24h is the reference cadence). */
43
+ export declare const SUBAGENT_TRANSCRIPT_RETENTION_DAYS_DEFAULT = 30;
24
44
  /** Default idle TTL (days) before a cached session is evicted (config-catalog `session.idleTtlDays`). */
25
45
  export declare const SESSION_DEFAULT_TTL_DAYS = 7;
26
46
  /**
@@ -1,4 +1,9 @@
1
1
  export const RETAIN_DEFAULT_TTL_MS = 30 * 60 * 1000;
2
2
  export const RETAIN_DEFAULT_MAX = 16;
3
+ export const DELEGATION_MAX_CONCURRENT_DEFAULT = 20;
4
+ export const DELEGATION_MAX_PER_SESSION_DEFAULT = 200;
5
+ export const ORPHAN_ADOPT_WINDOW_MS_DEFAULT = 48 * 60 * 60 * 1000;
6
+ export const ORPHAN_ADOPT_MAX_DEFAULT = 20;
7
+ export const SUBAGENT_TRANSCRIPT_RETENTION_DAYS_DEFAULT = 30;
3
8
  export const SESSION_DEFAULT_TTL_DAYS = 7;
4
9
  export const RUNNING_AGENT_OBSERVE_EVERY_BEATS = 4;
@@ -170,6 +170,7 @@ export interface BackgroundAgentRecord {
170
170
  export declare const REVIVED_ROW_CLEARED_FIELDS: readonly ["settledAt", "stoppedBy", "completionId", "finalOutput", "finalOutputFull", "error", "errorCode", "errorRetryable", "errorKind", "errorRetryAfterMs", "resultIsPartial", "summary", "recentSteps", "editedFiles", "usage"];
171
171
  /** Erase {@link REVIVED_ROW_CLEARED_FIELDS} from a record a revival is about to write back. */
172
172
  export declare function clearRevivedRowTerminalPayload(record: BackgroundAgentRecord): void;
173
+ export declare function announceTranscriptIntegrityGapOnce(handle: string, sink: ((handle: string) => void) | undefined): void;
173
174
  /** Content-free projection for list reads (design/151 HIGH-1: `summary`/`finalOutput`/`recentSteps`
174
175
  * and friends NEVER ride a list — content is get-by-handle only, behind the full predicate). */
175
176
  export interface BackgroundAgentRowSummary {
@@ -21,6 +21,19 @@ export function clearRevivedRowTerminalPayload(record) {
21
21
  for (const field of REVIVED_ROW_CLEARED_FIELDS)
22
22
  delete record[field];
23
23
  }
24
+ const transcriptIntegrityAnnounced = new Set();
25
+ export function announceTranscriptIntegrityGapOnce(handle, sink) {
26
+ if (sink === undefined)
27
+ return;
28
+ if (transcriptIntegrityAnnounced.has(handle))
29
+ return;
30
+ transcriptIntegrityAnnounced.add(handle);
31
+ try {
32
+ sink(handle);
33
+ }
34
+ catch {
35
+ }
36
+ }
24
37
  export class BackgroundAgentStoreError extends Error {
25
38
  code;
26
39
  constructor(code, message) {
@@ -36,6 +36,7 @@ import type { AgentTool } from "../internal/harness-types.js";
36
36
  import type { ImageContent, TextContent } from "../internal/llm.js";
37
37
  import { type McpImageResizer } from "./image-downsample.js";
38
38
  import type { McpServerSpec, OnElicit, ToolEffect } from "./types.js";
39
+ import { type ReminderDisclosureCounts } from "./reminder-disclosure.js";
39
40
  /**
40
41
  * The safety axes (design/77 §4 irreversibility, design/70 egress) derived from one materialized MCP
41
42
  * tool's server-advertised `annotations`. These ride alongside the {@link AgentTool} (which is vendored
@@ -462,7 +463,11 @@ export declare function normalizeMcpToolSchema(schema: unknown): McpSchemaNormal
462
463
  * combinator's own structure is legal JSON Schema ⇒ passes validateJsonSchemaShape too).
463
464
  */
464
465
  export declare function mcpToolSchemaProblem(schema: unknown): string | undefined;
465
- export declare function materializeMcpTools(specs: McpServerSpec[], principal?: string, onElicit?: OnElicit, imageResizer?: McpImageResizer): Promise<MaterializedMcp>;
466
+ export declare function materializeMcpTools(specs: McpServerSpec[], principal?: string, onElicit?: OnElicit, imageResizer?: McpImageResizer, // design/116 CONFIRM-1 seam: deployment-injected; default = auto-detected sharp
467
+ reminderDisclosure?: {
468
+ reminderMark?: string;
469
+ counts?: ReminderDisclosureCounts;
470
+ }): Promise<MaterializedMcp>;
466
471
  /**
467
472
  * Fold the caller's AUTHORITATIVE per-tool override (design F: caller = trust root) over the server-hint axis.
468
473
  * Unlike server hints, the caller may RAISE or LOWER any axis: `effect` sets the repeat-safety class (lower to