@sema-agent/core 7.0.1 → 7.1.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 (51) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/dist/agents/repair-loop.d.ts +8 -7
  3. package/dist/agents/roster-store.d.ts +7 -2
  4. package/dist/agents/subagent.js +29 -5
  5. package/dist/brain/errors.d.ts +18 -0
  6. package/dist/brain/errors.js +3 -0
  7. package/dist/brain/stream-engine.js +6 -4
  8. package/dist/core/context-edit.d.ts +3 -0
  9. package/dist/core/governance-codes.d.ts +1 -1
  10. package/dist/core/governance-codes.js +4 -0
  11. package/dist/core/hooks.d.ts +26 -6
  12. package/dist/core/hooks.js +8 -5
  13. package/dist/core/image-downsample.d.ts +4 -3
  14. package/dist/core/memory-engine/engine.d.ts +62 -5
  15. package/dist/core/memory-engine/engine.js +90 -19
  16. package/dist/core/memory-engine/index.d.ts +1 -1
  17. package/dist/core/memory-engine/layout.d.ts +11 -3
  18. package/dist/core/roles.d.ts +36 -9
  19. package/dist/core/roles.js +19 -6
  20. package/dist/core/runner/prepare-memory.js +29 -22
  21. package/dist/core/runner/prepare-task.d.ts +5 -3
  22. package/dist/core/runner/prepare-task.js +88 -47
  23. package/dist/core/runner/runtask.d.ts +12 -3
  24. package/dist/core/runner/runtask.js +32 -4
  25. package/dist/core/safety-axis-vocab.d.ts +1 -1
  26. package/dist/core/strategy-store.d.ts +4 -1
  27. package/dist/core/task-registry-shared.d.ts +7 -3
  28. package/dist/core/tool-errors.d.ts +1 -1
  29. package/dist/core/tool-policy.d.ts +40 -7
  30. package/dist/core/tool-policy.js +63 -9
  31. package/dist/core/types.d.ts +97 -11
  32. package/dist/engine/compaction/compaction.js +6 -2
  33. package/dist/engine/harness/agent-harness.d.ts +28 -6
  34. package/dist/engine/harness/agent-harness.js +34 -2
  35. package/dist/engine/harness/messages.js +4 -0
  36. package/dist/engine/harness/types.d.ts +37 -0
  37. package/dist/engine/harness/types.js +5 -0
  38. package/dist/engine/session/session.js +3 -2
  39. package/dist/index.d.ts +1 -1
  40. package/dist/internal/harness.d.ts +1 -0
  41. package/dist/internal/harness.js +1 -0
  42. package/dist/orchestration/builtin-workflows.d.ts +17 -9
  43. package/dist/orchestration/run-workflow-tool.d.ts +9 -1
  44. package/dist/orchestration/run-workflow-tool.js +18 -8
  45. package/dist/orchestration/workflow-governance.js +1 -1
  46. package/dist/orchestration/workflow-types.d.ts +1 -0
  47. package/dist/orchestration/workflow.d.ts +9 -1
  48. package/dist/orchestration/workflow.js +5 -5
  49. package/dist/stores/file/mailbox-store.d.ts +2 -1
  50. package/package.json +1 -1
  51. package/test/export-surface.snapshot.json +3 -1
@@ -934,8 +934,14 @@ export interface ToolExecuteContext {
934
934
  * opted-out session captures nothing, whatever its chosen AgentDefinition says — the opt-out is
935
935
  * a floor no selection loosens (the `memoryPersistenceCapable:false` floor's exact law, on the
936
936
  * privacy axis). Trusted Runner-filled seat, never a model/tool argument.
937
+ *
938
+ * DUAL FORM (#511 件2): on a deployment whose capture record store is synchronous (the default
939
+ * file trio) this is a plain boolean, byte-identical to before; a Promise-form
940
+ * {@link RunnerDeps.memoryCaptureRecordStore} makes the live read answer a `Promise<boolean>`.
941
+ * Consumers must `await` (identity on the boolean arm) — a bare `=== true` on the Promise arm
942
+ * would coin `false`, the exact un-floored escape this seat closes.
937
943
  */
938
- memoryCaptureOptedOut?: boolean;
944
+ memoryCaptureOptedOut?: boolean | Promise<boolean>;
939
945
  /**
940
946
  * design/383 §2.5 (rescan post-6.0.0-RC) — the floor seat's THIRD state: TRUE ⇔ the spawning
941
947
  * session's capture opt-out state is INDETERMINATE at the moment a delegation tool reads this
@@ -947,8 +953,10 @@ export interface ToolExecuteContext {
947
953
  * irreversible record is ever minted off an unreadable state); a readable-and-clean answer
948
954
  * proceeds clean. Never TRUE beside {@link memoryCaptureOptedOut} — a known opt-out is
949
955
  * determinate. Trusted Runner-filled seat, never a model/tool argument.
956
+ * Dual form like its twin (#511 件2): `boolean` over a sync store, `Promise<boolean>` over a
957
+ * Promise-form store — consumers `await`.
950
958
  */
951
- memoryCaptureIndeterminate?: boolean;
959
+ memoryCaptureIndeterminate?: boolean | Promise<boolean>;
952
960
  /** design/383 §2.5 — the spawning session's write-plane control dir (the coordinate its capture
953
961
  * record is keyed under), forwarded beside the floor bit so a cross-plane child's record-query
954
962
  * leg reads the PARENT's carrier, not its own plane's. Trusted Runner-filled seat. */
@@ -3933,8 +3941,11 @@ export interface TaskEventIdentity {
3933
3941
  }
3934
3942
  /**
3935
3943
  * design/99 §E3/§E10 — a BRAIN-LAYER liveness phase, surfaced so a UI can show why a
3936
- * turn is stalled (the brain otherwise absorbs these silently in its connect/retry loop). Provider-NEUTRAL:
3937
- * carries only the graduated phase + a neutral hint, NEVER an HTTP status / provider taxonomy / stop_reason.
3944
+ * turn is stalled (the brain otherwise absorbs these silently in its connect/retry loop). The PHASE
3945
+ * itself is provider-NEUTRAL a graduated bucket this engine mints, never a provider taxonomy or a
3946
+ * `stop_reason`. The one provider-stated number that does cross this channel is the failing attempt's
3947
+ * HTTP status, and it travels on its own named seat ({@link BrainStatus.errorStatus}) in the retry
3948
+ * context only; the phase and the {@link BrainStatus.detail} hint stay free of it.
3938
3949
  * A CLOSED union (type-safe) covering the core brain layer; a deployment that surfaces its OWN states
3939
3950
  * (e.g. E25 token-refresh `authenticating`) does so on its own channel, not by widening this.
3940
3951
  */
@@ -3952,8 +3963,10 @@ export type BrainStatusPhase = "rate_limited" | "retrying" | "reconnecting" | "c
3952
3963
  * WHY a retry wait is happening, as a closed, provider-NEUTRAL bucket — the companion to
3953
3964
  * {@link BrainStatusPhase}, which says what the brain is doing about it. A consumer rendering an
3954
3965
  * unattended progress line ("no answer for three minutes") needs the reason, and until this existed the
3955
- * only carriers of it were the HTTP status and the syscall code, neither of which may cross this
3956
- * channel. Values are about the SHAPE of the failure, never its provider taxonomy:
3966
+ * only carriers of it were the HTTP status and the syscall code, neither of which crossed this channel
3967
+ * at the time. (The status has since gained its own seat, {@link BrainStatus.errorStatus}; this bucket
3968
+ * remains the reason a consumer RENDERS, and it is the only carrier for the transport classes, which
3969
+ * have no status at all.) Values are about the SHAPE of the failure, never its provider taxonomy:
3957
3970
  * - `connect_refused` — the attempt got a definite negative about the target itself (nothing accepts
3958
3971
  * at that address, or the name has no address). This is the class the SHORT retry lane serves.
3959
3972
  * - `transport` — any other transport-level failure: a connect timeout, a reset, a mid-stream
@@ -3969,7 +3982,11 @@ export type BrainRetryErrClass = "connect_refused" | "transport" | "rate_limit"
3969
3982
  /** design/99 §E3/§E10 — the payload of a {@link TaskEvent} `status` event (and the brain→runner signal). */
3970
3983
  export interface BrainStatus {
3971
3984
  phase: BrainStatusPhase;
3972
- /** Optional neutral, human-readable hint (NO provider/HTTP detail). */
3985
+ /** Optional neutral, human-readable hint. Stays free of provider/HTTP detail even now that
3986
+ * {@link errorStatus} exists: the machine-readable status has its own seat, and interpolating it
3987
+ * into the prose too would make one fact travel in two spellings a consumer has to reconcile.
3988
+ * (CC parity: its own retry banner renders a TRANSLATED sentence — "No response from API",
3989
+ * "Connection dropped" — while the raw status rides the structured key beside it.) */
3973
3990
  detail?: string;
3974
3991
  /** Seconds until the brain's next retry attempt (from the honored backoff / `Retry-After`), when known. */
3975
3992
  retryInSec?: number;
@@ -4007,6 +4024,41 @@ export interface BrainStatus {
4007
4024
  * the engine classified; absent on frames that are not a retry wait (`recovered`/`gave_up`) and on a
4008
4025
  * `circuit_open` fast-fail, which is a local verdict rather than an observed failure. */
4009
4026
  errClass?: BrainRetryErrClass;
4027
+ /**
4028
+ * #506 ㋑ — the HTTP status of the attempt that just failed, on the frame announcing the RETRY of it.
4029
+ * The one provider-stated number this channel carries; everything else about the failure still
4030
+ * crosses as the engine's own graduated buckets ({@link phase} / {@link errClass}).
4031
+ *
4032
+ * WHY IT IS HERE AT ALL, given the channel spent its life refusing it: a UI that must tell an
4033
+ * operator whether to wait or to go fix something needs the status, and the two neutral buckets
4034
+ * cannot supply it — `rate_limit` covers a 429 the provider will clear on its own AND a 429 that
4035
+ * means the account is out of quota, and `server` covers a 500 alongside a 503 behind a load
4036
+ * balancer. CC states the same number on the same occasion (`system/api_retry.error_status`, read
4037
+ * off `APIError.status`), so a serving layer forwarding this frame no longer has to choose between
4038
+ * matching CC's payload and honoring this channel's contract.
4039
+ *
4040
+ * PRESENCE — an api_retry context whose attempt got a response that NAMES the failure, and nothing
4041
+ * else. Present on the connect-ladder retry wait (`rate_limited` / `retrying` / `reconnecting` when
4042
+ * a response came back) and on the output-cap re-send (the provider's own 400). ABSENT — never
4043
+ * zeroed, never null — for:
4044
+ * · a transport failure (`connect_refused` / `transport` at the connect leg): no response existed,
4045
+ * so no status was ever stated. This is the arm CC spells as an explicit `null`; here it is
4046
+ * absence, for the same reason every other seat on this interface uses absence, and the fact is
4047
+ * still TOTAL rather than guessable — {@link errClass} names those two classes by construction.
4048
+ * · a mid-stream tear (tiers B/C): its response's own status was a SUCCESS, and a status that
4049
+ * failed nothing must not be published as the cause of a retry.
4050
+ * · a `circuit_open` fast-fail: a local verdict, nothing was sent, nobody answered.
4051
+ * · the terminal `recovered` / `gave_up` frames: they announce no attempt.
4052
+ * Same "only a status that names the failure travels" predicate as the assistant frame's
4053
+ * {@link AssistantMessage.apiErrorStatus} (one helper, both sites) — a 2xx never travels here either,
4054
+ * which is reachable on this path via a provider's own `x-should-retry` verdict on a success status.
4055
+ *
4056
+ * OPTIONAL, not CC's nullable-REQUIRED, and the difference is CC's own: CC dedicates a whole message
4057
+ * (`system/api_retry`) to the retry occasion and can therefore require the key on it, while this
4058
+ * interface is ONE shape shared by every phase — the same geometry as CC's shared
4059
+ * `control_request_progress` frame, where CC itself makes the key optional.
4060
+ */
4061
+ errorStatus?: number;
4010
4062
  }
4011
4063
  /**
4012
4064
  * design/97 CORE-8 (③): one lightweight TOOL-ACTIVITY beat surfaced from a running task, for a per-agent live
@@ -4567,7 +4619,9 @@ export type TaskEvent = ({
4567
4619
  * silently (a 429 rate-limit / a 5xx or network retry / a reconnect / an open circuit breaker). Emitted
4568
4620
  * the moment the brain decides to wait/retry, carrying the graduated {@link BrainStatusPhase} + an
4569
4621
  * optional `retryInSec` so a UI can show "rate limited, retrying in Ns". EPHEMERAL — a pure liveness
4570
- * signal, never persisted or replayed on resume. Provider-neutral (no HTTP status / stop_reason).
4622
+ * signal, never persisted or replayed on resume. The phase and the hint are provider-neutral (no
4623
+ * provider taxonomy, no `stop_reason`); the failing attempt's HTTP status rides its own seat in the
4624
+ * retry context only — see {@link BrainStatus.errorStatus}.
4571
4625
  * SCOPE: reflects THIS task's own (top-level) brain calls. A delegated sub-agent's brain liveness routes
4572
4626
  * to the SUB-AGENT's own stream (isolated, like every child event — §E2); a background internal brain
4573
4627
  * call (memory recall/consolidation) is intentionally not surfaced.
@@ -5160,9 +5214,15 @@ export interface WorkflowGovernanceBaseline {
5160
5214
  * The allowed model NAMES an LLM-authored script may pick (design/98 §2.5 新洞2). A script gives a
5161
5215
  * `modelName` string (never a `Model` object — that carries `baseUrl`/`headers` = exfil); the engine
5162
5216
  * resolves it against this list to a deploy-configured `Model` (the script never sees the object).
5163
- * **FAIL-CLOSED**: `undefined`/empty ⇒ the script CANNOT pick a model (its `modelName` is rejected the
5164
- * child falls back to the workflow's default role). NEVER fail-open to the whole `models` catalog for an
5165
- * LLM-authored workflow.
5217
+ * **FAIL-CLOSED**: `undefined`/empty ⇒ the script CANNOT pick a model a spec that names `modelName`
5218
+ * THROWS `WorkflowModelNotAllowedError` and the child is never spawned. The refusal is loud, not a
5219
+ * silent downgrade: the script asked for a model it may not have, so the agent call FAILS rather than
5220
+ * quietly running on the workflow's default role (a script that wants the default must simply OMIT
5221
+ * the field). The same throw covers a name that is off the list, and a listed name the `models`
5222
+ * catalog does not hold — so the legal set is the INTERSECTION of this list and the catalog keys
5223
+ * (note the catalog is the Runner's TIER-EXPANDED one, so on a deployment with `RunnerDeps.tiers`
5224
+ * the tier words and CC aliases are catalog keys here too, still gated by this list).
5225
+ * NEVER fail-open to the whole `models` catalog for an LLM-authored workflow.
5166
5226
  */
5167
5227
  workflowModelAllowlist?: string[];
5168
5228
  }
@@ -5741,6 +5801,32 @@ export interface EngineNotice {
5741
5801
  * implemented, so the unhonored-knob disclosure it carried has no referent; consumers must
5742
5802
  * judge ladder support by VERSION, never by that code's absence.
5743
5803
  *
5804
+ * - `"task.halt_unconsumed"` (design/384 slice 1) — the user's stop verb ({@link TaskStream.halt})
5805
+ * was ANSWERED (not refused) while the run's ending was already owned by its own abort: nothing
5806
+ * was cut and nothing was stopped by the halt, the run's own ending stands, and the result will
5807
+ * NOT carry `haltedByUser` for it (the attribution seat never signs someone else's stop). This
5808
+ * notice is the halt's only trace on that arm — without it the verb receipt (`{turnCut:false}`)
5809
+ * was the sole record and the caller could not tell "my stop did nothing because the run beat
5810
+ * me to ending" from "my stop is being honored at the next boundary". Minted once per such verb
5811
+ * call (each arrival is a distinct fact); `detail: { sessionId, runId, taskId? }` — `runId` is
5812
+ * the run whose ending the halt failed to claim. Audience `"user"` (the person who pressed
5813
+ * stop). A REFUSED halt (typed `steering.not_running` throw) mints nothing: the refusal itself
5814
+ * is the loud answer.
5815
+ *
5816
+ * - `"task.late_approval"` (design/384 slice 1) — a synchronous ask approval was NOT CONSUMED:
5817
+ * a run or turn interrupt released the wait (the `resolveAsk` race arm detached the approver's
5818
+ * promise; an unconsumed resolve is the approver releasing its wait, never a verdict), so the
5819
+ * tool did NOT run and the approval was not honored. The notice asserts non-consumption ONLY —
5820
+ * never an arrival order: an approval and an abort settling close together read as the abort
5821
+ * (the resolver's documented one-directional residual — the engine never claims a person
5822
+ * decided something it cannot show they did, and it equally never claims which came first).
5823
+ * Minted only for a value that reads as an APPROVAL — an unconsumed
5824
+ * deny/refusal is an ordinary release and leaves no trace, and an unconsumed REJECTION goes to
5825
+ * `onError(phase:"hook")` instead (a failing callback, not an answer). One notice per
5826
+ * unconsumed approval; `detail: { toolName, toolCallId, sessionId, runId, taskId? }`. Audience `"user"`
5827
+ * (whoever answered the card is the one entitled to hear the answer ran nothing); a host may
5828
+ * forward it on its own wire.
5829
+ *
5744
5830
  * - `"steering.parked_input_blocked"` (design/373 §4.3) — a PARKED steer entry was withheld when
5745
5831
  * a resume redelivered it (any resume kind that drains parked steers — wake included) by the
5746
5832
  * deployment's `userPromptSubmit` screen (block verdict, or a fail-closed non-answer/crash):
@@ -1,7 +1,7 @@
1
1
  import { resolveAgentCoreCompleteFn, } from "../loop/runtime-deps.js";
2
2
  import { asAgentMessage, convertToLlm, createCompactionSummaryMessage, createCustomMessage, } from "../harness/messages.js";
3
3
  import { buildSessionContext } from "../session/session.js";
4
- import { CompactionError, err, ok, } from "../harness/types.js";
4
+ import { CompactionError, err, isSyntheticApiErrorMessage, ok, } from "../harness/types.js";
5
5
  import { budgetInvokedSkillsRetention, computeFileLists, createFileOps, extractFileOpsFromMessage, extractInvokedSkills, extractPersistedOutputRefs, formatFileOperations, formatPersistedOutputRefs, PERSISTED_OUTPUT_REFS_MAX_ENTRIES, readElidedMessages, readPersistedOutputRefs, readRetainedInvokedSkills, readUnsummarizedMessages, replaceInvokedSkillBodiesForSummary, serializeConversation, stripFileOperationsFooter, } from "./utils.js";
6
6
  import { ENGINE_AUTHORITY_ENVELOPE_TAGS, sanitizeUntrustedText } from "../../core/untrusted-text.js";
7
7
  import { sliceHeadSafe } from "../../core/surrogate-safe-slice.js";
@@ -223,9 +223,13 @@ function findValidCutPoints(entries, startIndex, endIndex) {
223
223
  case "custom":
224
224
  case "compactionSummary":
225
225
  case "user":
226
- case "assistant":
227
226
  cutPoints.push(i);
228
227
  break;
228
+ case "assistant":
229
+ if (!isSyntheticApiErrorMessage(entry.message)) {
230
+ cutPoints.push(i);
231
+ }
232
+ break;
229
233
  case "toolResult":
230
234
  break;
231
235
  }
@@ -168,16 +168,17 @@ export declare class AgentHarness<TSkill extends Skill = Skill, TPromptTemplate
168
168
  * gated: its splice opens a NEW run (idle-park redelivery), and a halt belongs to the run that
169
169
  * minted it. */
170
170
  engineInjectionsHeld?: () => boolean;
171
- /** RB-30: shared recovery sweep — collects engine-note payloads from the given queues
172
- * in DELIVERY order (steer before followUp, each queue forward — the live loop serves steering
173
- * first, so the recovered redelivery must not present "later" frames ahead of "now/next"),
174
- * removes those entries, and hands the payloads to the runner sink. Called from BOTH terminal
175
- * paths: the natural agent_end AND abort() (which clears the queues before agent_end would see
176
- * them — the hard-abort race that round-1 shipped would have lost). */
177
171
  /** RB-30 R2 belt-and-braces: runner-callable idempotent recovery for exit paths that reach
178
172
  * neither agent_end nor abort() (a pre-prompt throw after prepare). Swept entries are removed,
179
173
  * so a double call is a no-op. */
180
174
  recoverUndrainedEngineNotes(): void;
175
+ /** RB-30: shared recovery sweep — collects engine-note payloads from the given queues
176
+ * in DELIVERY order (steer before followUp, each queue forward — the live loop serves steering
177
+ * first, so the recovered redelivery must not present "later" frames ahead of "now/next"),
178
+ * removes those entries, and hands the payloads to the runner sink. Called from BOTH terminal
179
+ * paths: the natural agent_end AND abort() — which does NOT clear the queues: #389 retired that
180
+ * clearing, so they are LEFT STANDING (the durable-park migration reads them AFTER abort()
181
+ * returns) and the abort-side pass is the belt for an exit that never reached agent_end. */
181
182
  private sweepUndrainedEngineNotes;
182
183
  /** backlog #259 — what remains in the steer/follow-up queues AFTER the engine-note sweep is USER
183
184
  * input that was accepted ("queued") and will never be consumed: the run reached agent_end first.
@@ -261,6 +262,27 @@ export declare class AgentHarness<TSkill extends Skill = Skill, TPromptTemplate
261
262
  setStopGate(gate: (() => Promise<AgentMessage[]>) | undefined): void;
262
263
  private validateToolNames;
263
264
  private flushPendingSessionWrites;
265
+ /**
266
+ * #506 ㋐ — write the transcript's record of a TERMINAL provider failure (CC 2.1.250 parity, `Oo`/`CIt`).
267
+ *
268
+ * A SEPARATE object, never the live frame: `failure` is the engine's terminal error shell and every
269
+ * other consumer of it (the run result, failover, telemetry, usage accounting) reads its real
270
+ * `model`, so re-stamping that field in place would corrupt all of them. The record is minted here
271
+ * with the reserved {@link SYNTHETIC_MESSAGE_MODEL} instead, which is what makes it recognizable — and
272
+ * therefore droppable — at the request boundary and at the derived-state walks.
273
+ *
274
+ * Two CC fields are deliberately NOT copied, because this tree already states the same facts more
275
+ * honestly: CC writes `stop_reason: "stop_sequence"` on an entry that stopped for no such reason (its
276
+ * own consumers must read `isApiErrorMessage` to know better), and CC writes a zero `usage` with
277
+ * nothing marking it synthetic. Here the stop reason stays `"error"` and `usageMissing` says the zeros
278
+ * are a shell rather than a measurement — the same discipline the live frame already carries.
279
+ *
280
+ * BEST-EFFORT: a record of a failure must never become a second failure. The append can lose the
281
+ * session's optimistic lock (another writer moved the leaf) or hit a storage fault, and the run is
282
+ * already over — swallowing that leaves the pre-#506 behavior (no record) rather than converting a
283
+ * provider outage into a harness throw on the teardown path.
284
+ */
285
+ private recordApiFailure;
264
286
  private handleAgentEvent;
265
287
  private emitRunFailure;
266
288
  private executeTurn;
@@ -4,7 +4,8 @@ import { resolveAgentCoreStreamFn } from "../loop/runtime-deps.js";
4
4
  import { normalizeEngineSegments } from "../../core/untrusted-text.js";
5
5
  import { AUTH_CARRIER_NAMES } from "../../brain/request-params.js";
6
6
  import { convertToLlm } from "./messages.js";
7
- import { AgentHarnessError, CompactionError, SessionError, toError, } from "./types.js";
7
+ import { AgentHarnessError, CompactionError, SessionError, SYNTHETIC_MESSAGE_MODEL, toError, } from "./types.js";
8
+ const API_FAILURE_RECORD_MAX_CHARS = 2000;
8
9
  function createUserMessage(text, images, provenance) {
9
10
  const content = text.length > 0 ? [{ type: "text", text }] : [];
10
11
  if (images) {
@@ -603,12 +604,13 @@ export class AgentHarness {
603
604
  },
604
605
  };
605
606
  },
606
- beforeToolCall: async ({ toolCall, args }) => {
607
+ beforeToolCall: async ({ toolCall, args }, signal) => {
607
608
  const result = await this.emitHook({
608
609
  type: "tool_call",
609
610
  toolCallId: toolCall.id,
610
611
  toolName: toolCall.name,
611
612
  input: args,
613
+ ...(signal !== undefined ? { signal } : {}),
612
614
  });
613
615
  return result;
614
616
  },
@@ -726,6 +728,33 @@ export class AgentHarness {
726
728
  this.pendingSessionWrites.shift();
727
729
  }
728
730
  }
731
+ async recordApiFailure(failure) {
732
+ const raw = failure.errorMessage?.trim();
733
+ const detail = raw !== undefined && raw.length > API_FAILURE_RECORD_MAX_CHARS
734
+ ? `${raw.slice(0, API_FAILURE_RECORD_MAX_CHARS)}… (truncated)`
735
+ : raw;
736
+ const record = {
737
+ role: "assistant",
738
+ content: [{ type: "text", text: detail ? `API Error: ${detail}` : "API Error" }],
739
+ api: failure.api,
740
+ provider: failure.provider,
741
+ model: SYNTHETIC_MESSAGE_MODEL,
742
+ isApiErrorMessage: true,
743
+ ...(failure.apiErrorStatus !== undefined ? { apiErrorStatus: failure.apiErrorStatus } : {}),
744
+ ...(failure.requestId !== undefined ? { requestId: failure.requestId } : {}),
745
+ usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, totalTokens: 0, cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 } },
746
+ usageMissing: true,
747
+ stopReason: "error",
748
+ ...(failure.errorMessage !== undefined ? { errorMessage: failure.errorMessage } : {}),
749
+ ...(failure.errorKind !== undefined ? { errorKind: failure.errorKind } : {}),
750
+ timestamp: failure.timestamp,
751
+ };
752
+ try {
753
+ await this.session.appendMessage(record);
754
+ }
755
+ catch {
756
+ }
757
+ }
729
758
  async handleAgentEvent(event, signal) {
730
759
  if (event.type === "message_end") {
731
760
  let entryId;
@@ -733,6 +762,9 @@ export class AgentHarness {
733
762
  entryId = await this.session.appendMessage(event.message);
734
763
  this.settleInjectedFrame(event.message);
735
764
  }
765
+ else if (event.message.isApiErrorMessage === true) {
766
+ await this.recordApiFailure(event.message);
767
+ }
736
768
  await this.emitAny(entryId !== undefined ? { ...event, entryId } : event, signal);
737
769
  return;
738
770
  }
@@ -1,3 +1,4 @@
1
+ import { isSyntheticApiErrorMessage } from "./types.js";
1
2
  import { parseSessionTimestampMs, requireSessionTimestampMs } from "../session/timestamps.js";
2
3
  export function asAgentMessage(message) {
3
4
  return message;
@@ -155,6 +156,9 @@ export function convertToLlm(messages) {
155
156
  case "user":
156
157
  case "assistant":
157
158
  case "toolResult":
159
+ if (isSyntheticApiErrorMessage(message)) {
160
+ return undefined;
161
+ }
158
162
  return normalizeLlmMessageContent(message);
159
163
  default:
160
164
  return undefined;
@@ -754,6 +754,37 @@ export declare function isValidModelChange(e: {
754
754
  provider?: unknown;
755
755
  modelId?: unknown;
756
756
  }): boolean;
757
+ /**
758
+ * #506 ㋐ — the reserved `model` value on a transcript entry NO MODEL PRODUCED. CC-verbatim
759
+ * (`"<synthetic>"`, CC 2.1.250's own single-occurrence constant), and single-occurrence here for the
760
+ * same reason: the mint site and the recognition predicate must read the same literal or the entry
761
+ * becomes unrecognizable the first time one of them is retyped.
762
+ *
763
+ * It is deliberately a MODEL-SHAPED string (non-empty, bounded), so an entry carrying it survives this
764
+ * session's own import door and round-trips — a self-generated entry its own validator rejected would
765
+ * be the worse failure.
766
+ *
767
+ * ACCEPTED RESIDUAL, recorded rather than defended against: nothing RESERVES this id, because model ids
768
+ * are caller-supplied and `isModelIdentifier` admits any bounded non-empty string. A deployment that
769
+ * names a real model literally `<synthetic>` AND whose Brain stamps `isApiErrorMessage` on
770
+ * content-bearing output would have that output treated as a record. Both halves are required, the
771
+ * spelling is chosen to be one no catalog would use, and CC carries the same residual for the same
772
+ * reason — a reserved-word door here would refuse ids this engine has no business refusing.
773
+ */
774
+ export declare const SYNTHETIC_MESSAGE_MODEL = "<synthetic>";
775
+ /**
776
+ * #506 ㋐ — is this transcript entry the engine's own SYNTHETIC record of a terminal API failure?
777
+ *
778
+ * Such an entry is real transcript (a session that died on a provider failure must say so when it is
779
+ * re-read) but it is NOT model output, so it is dropped wherever transcript becomes model input, and
780
+ * it must not move any state derived from real turns. This predicate is the single gate for both.
781
+ *
782
+ * THREE keys, CC-anchored (`Hje`: `type==="assistant" && isApiErrorMessage===true && model===<synthetic>`),
783
+ * and the narrowness is the whole point. `isApiErrorMessage` alone would also match an assistant message
784
+ * a foreign Brain stamped while it carried REAL output — dropping that from the request view would
785
+ * silently delete the model's own words from its context. Only the engine's own mint sets all three.
786
+ */
787
+ export declare function isSyntheticApiErrorMessage(m: unknown): boolean;
757
788
  /** RB-128: a `label` / `session_info.name` that is not a string reaches `.trim()` inside
758
789
  * `BaseSessionStorage`'s CONSTRUCTOR — and the file backend validates BEFORE constructing any storage, so
759
790
  * the contamination lands on disk and every later `open()` throws a raw `TypeError` (not even a `SessionError`).
@@ -1077,6 +1108,12 @@ export interface ToolCallEvent {
1077
1108
  toolCallId: string;
1078
1109
  toolName: string;
1079
1110
  input: Record<string, unknown>;
1111
+ /** The per-call abort signal (additive, design/384 slice 1). Present iff the loop handed its
1112
+ * turn-scoped signal to `beforeToolCall` — the run-level abort is composed into that controller,
1113
+ * so this signal fires on a turn interrupt (bare halt / steer-now) AND on a run abort. Absent on
1114
+ * emits that carry no per-call signal; a subscriber gating a wait on it must then fall back to
1115
+ * whatever run-level signal it already holds (the historical shape, byte-identical). */
1116
+ signal?: AbortSignal;
1080
1117
  }
1081
1118
  export interface ToolResultEvent {
1082
1119
  type: "tool_result";
@@ -155,6 +155,11 @@ export function isValidThinkingLevelChange(e) {
155
155
  export function isValidModelChange(e) {
156
156
  return isModelIdentifier(e.provider) && isModelIdentifier(e.modelId);
157
157
  }
158
+ export const SYNTHETIC_MESSAGE_MODEL = "<synthetic>";
159
+ export function isSyntheticApiErrorMessage(m) {
160
+ const a = m;
161
+ return a?.role === "assistant" && a.isApiErrorMessage === true && a.model === SYNTHETIC_MESSAGE_MODEL;
162
+ }
158
163
  export function isOptionalDisplayString(v, max = 4096) {
159
164
  return v === undefined || v === null || (typeof v === "string" && v.length <= max);
160
165
  }
@@ -1,5 +1,5 @@
1
1
  import { asAgentMessage, createCompactionSummaryMessage, createCustomMessage, } from "../harness/messages.js";
2
- import { SessionError, isValidModelChange, normalizeAnnouncedListing, normalizeCompactionStateCarrier, normalizeGitAnnouncement, normalizeReminderMark, normalizeWorkspaceState } from "../harness/types.js";
2
+ import { SessionError, isSyntheticApiErrorMessage, isValidModelChange, normalizeAnnouncedListing, normalizeCompactionStateCarrier, normalizeGitAnnouncement, normalizeReminderMark, normalizeWorkspaceState } from "../harness/types.js";
3
3
  import { normalizePromptEpoch } from "../../prompt-assembly/epoch.js";
4
4
  import { budgetInvokedSkillsRetention, readElidedMessages, readRetainedInvokedSkills, readUnsummarizedMessages, renderInvokedSkillsRetention, } from "../compaction/utils.js";
5
5
  const RETENTION_CLAMP_DEFAULT_CHARS_PER_TOKEN = 3;
@@ -25,7 +25,8 @@ export function buildSessionContext(pathEntries, opts) {
25
25
  model = { provider: entry.provider, modelId: entry.modelId };
26
26
  }
27
27
  else if (entry.type === "message" && entry.message.role === "assistant") {
28
- if (isValidModelChange({ provider: entry.message.provider, modelId: entry.message.model })) {
28
+ if (!isSyntheticApiErrorMessage(entry.message) &&
29
+ isValidModelChange({ provider: entry.message.provider, modelId: entry.message.model })) {
29
30
  model = { provider: entry.message.provider, modelId: entry.message.model };
30
31
  }
31
32
  }
package/dist/index.d.ts CHANGED
@@ -179,7 +179,7 @@ export { AdoptionError, assertAdoptionBootGate, readRootAdoptionFile, writeRootA
179
179
  export { adoptLocalDataRoot, ackAdoptionConfig, witnessAdoptionConfig, listAdoptionQuarantine, readAdoptionStatus, type AdoptionStatus, type AdoptLocalDataRootOptions, type AdoptLocalDataRootResult, type AdoptionCarriageLeg, type AdoptionCarriageLegContext, type AdoptionConfigWitnessReceipt, } from "./stores/file/adoption/adopt.js";
180
180
  export { formatHookFeedback, runToolGate, createHookEnvCapabilities, createPreToolUseConstraintPolicy, type PersistedRuleHit, type PersistedRuleHitRule, type PersistedRuleUnreadable, type PersistedRuleCoverage, type PersistedRuleAnswer, normalizePersistedRuleHit, type Hooks, type HookToolContext, type HookInvocationIdentity, type UserPromptSubmitContext, type PostToolBatchContext, type HookEnvCapabilities, type HookToolOutput, type PreToolUseResult, type PostToolUseResult, type UserPromptSubmitResult, type HookToolFailure, type PostToolUseFailureResult, type PostToolBatchCall, type PostToolBatchResult, type PreCompactContext, type PreCompactResult, type PostCompactContext, type StopFailureContext, type PermissionDeniedPayload, type PermissionDeniedSource, } from "./core/hooks.js";
181
181
  export { InMemoryMemoryStore, composeMemoryBlock, composeLayeredMemoryBlock, normalizeMemorySpec, type NormalizedMemorySpec, type MemorySpecInput, supportsConsolidation, supportsPeriodicConsolidation, firstSentence, expandLexicalTerms, lexicalSearchMatch, type Embedder, classifyPromotable, enforcePromotableWriteGate, guardedMemoryStore, MemoryGateError, detectSecret, enforceSecretWriteGate, enforceStructuredNoteSecretGate, type UtilityGate, type MemoryStore, type MemoryVectorMode, type ScoredMemory, type MemoryNoteHeader, type MemoryNoteRecord, type MemoryNoteType, type StructuredNoteInput, } from "./core/memory.js";
182
- export { MemoryEngine, FileMemoryEngineBackend, memoryBackendContract, assertMemoryBackendSearchEquivalence, type MemoryBackendContractHooks, buildMemoryInstruction, truncateIndex, scanEntryFiles, deriveRepoKey, deriveRepoMemoryDir, deriveControlPlaneDir, deriveRepoControlPlaneDir, resolveMemoryEngineRoot, scopeDirName, ControlPlaneCorruptError, parseEntryFile, serializeEntryFile, computeEntryRev, entryFromFile, MEMORY_ORIGIN_CAUSES, committedOriginOf, originEquals, consolidationExposedFrontmatter, ambiguousOriginRepresentation, type OriginClearanceRow, type OriginClearanceEvent, type OriginClearanceShadow, committedDistilledOf, distilledEquals, type MemoryEntryDistilled, type MemoryEntryDistilledInput, CONSOLIDATION_DEFAULTS, ConsolidationRefusedError, MEMORY_SEARCH_SUPERSEDED_TAG, consolidationTypeEligible, deriveSupersededSet, memorySupersededNote, readIntentCredentials, supersessionFuseCeiling, memoryConsolidationRecommendedNotice, memoryConsolidationCommittedNotice, memoryConsolidationConflictNotice, memoryConsolidationIncompleteNotice, memoryConsolidationRefusedNotice, memoryConsolidationWithheldNotice, type ConsolidationGateRead, type ConsolidationGateRow, type ConsolidationIntent, type ConsolidationIntentCredentialRow, type ConsolidationLeaseSeat, type ConsolidationProductProposal, type ConsolidationProposal, type MemoryConsolidationOptions, type ConsolidationCommitReceipt, type ConsolidationReconcileReport, type ConsolidationResolveReceipt, type ConsolidationPlanSummary, type ConsolidationPlanFoldEvidence, DISTILLER_DEFAULT_MAX_INPUTS_PER_PRODUCT, LLM_DISTILLER_CONTRACT, LLM_DISTILLER_CONTRACT_DL2, LLM_DISTILLER_CONTRACT_DL3, LLM_DISTILLER_CONTRACTS, MEMORY_DISTILLER_CONTRACT_V1, contractGroupingDiff, driveConsolidationToFixpoint, isAliasModelId, llmPlanDistiller, mintExposurePartitionedPlan, mintLlmConsolidationPlan, MEMORY_DISTILLER_PURITY_CONTRACT_V1, detectCleanArmVerbatimLeak, type CleanArmLeakFinding, type CleanArmLeakVerdict, type MemoryDistillerPurityContract, openAiCompatChatSeat, parseJsonAnswer, planParseRepairs, sanitizeLlmGroups, scheduleUnderFuse, type ConsolidationDistillFn, type ConsolidationDriveCycleRow, type ConsolidationDriveEngine, type ConsolidationDriveResult, type ConsolidationFoldState, type DistillerCandidate, type DistillerChatAnswer, type DistillerChatFn, type DistillerChatRequest, type FuseSchedule, type LlmConsolidationPlan, type LlmConsolidationPlanArm, type LlmConsolidationPlanProduct, type LlmDistillerContract, type MintLlmConsolidationPlanResult, type PlanParseRepairs, type SanitizedLlmGroups, CONSOLIDATION_DRIVER_PLANS_DIR, CONSOLIDATION_DRIVER_RUNS_FILE, CONSOLIDATION_RUN_STOP_REASONS, archiveDistillerPlan, readConsolidationDriverRun, runMemoryConsolidationDriver, type ConsolidationDriverEngine, type ConsolidationDriverRunRow, type ConsolidationRunReceipt, type ConsolidationRunStopReason, type RunMemoryConsolidationOptions, isInstructionEntry, type MemoryEntryOrigin, type MemoryOriginCause, MEMORY_INSTRUCTION_TEMPLATE, MEMORY_RECALL_DISCIPLINE, MEMORY_PREFERENCE_DISCIPLINE, MEMORY_READONLY_NOTICE, MEMORY_CAPTURE_OPTOUT_NOTICE, memoryCaptureOptedOutNotice, memoryCaptureOptOutUnpersistedNotice, SESSION_CAPTURE_OPTOUT_DIR, markSessionCaptureOptOut, readSessionCaptureOptOut, listSessionCaptureOptOut, fileSessionCaptureRecordStore, type SessionCaptureOptOutRecord, type SessionCaptureOptOutMarkOutcome, type SessionCaptureRecordStore, readV2HeaderHints, type V2HeaderHints, LINEAGE_FILE, CHALLENGES_FILE, CHALLENGED_HISTORY_FILE, CHALLENGE_LEDGER_MAX_EVENTS, rebuildStrictControlPlaneLedger, type ControlPlaneRebuildReceipt, type StrictControlPlaneLedger, type ChallengeAssignment, type ChallengeEvent, type ChallengedHistoryRow, type LineagePendingTxn, type LineagePromotion, MEMORY_INDEX_FILENAME, MEMORY_INDEX_MAX_LINES, MEMORY_INDEX_MAX_BYTES, MEMORY_SEARCH_TOOL_NAME, MEMORY_GET_TOOL_NAME, MEMORY_INDEX_TOOL_NAME, MEMORY_ENGINE_TOOL_NAMES, type MemorySearchDetails, type MemorySearchHit, type CleanMemorySearchHit, type ExposedMemorySearchHit, MEMORY_EXPOSURE_BANNER, MEMORY_EXPOSURE_HANDLE_TAG, memoryExposureIndexRow, type MemoryGetDetails, type MemoryIndexDetails, type MemoryIndexRow, type CleanMemoryIndexRow, type ExposedMemoryIndexRow, STUB_ARCHIVED_LINE, DEFAULT_MAX_MEMORY_FILES, DEFAULT_MAX_ENTRY_DEPTH, DEFAULT_HARVEST_DEADLINE_MS, DEFAULT_HARVEST_FILE_BUDGET, MASS_DELETION_FUSE_RATIO, scanMemoryWrite, scanMemoryFileName, scanRemediation, MEMORY_FILENAME_SEGMENT_RE, renderAnnouncements, enqueueMemoryAnnouncement, drainMemoryAnnouncements, peekMemoryAnnouncements, bumpScanFuse, scanFuseCount, clearScanFuse, ANNOUNCEMENTS_FILE, MEMORY_ANNOUNCEMENTS_MAX, SCAN_FUSE_FILE, SCAN_FUSE_THRESHOLD, type MemoryAnnouncement, type ScanFinding, type MemoryEngineOptions, type MemoryInjection, type EntryProvenanceAccount, type TransferEvidence, type CommittedBinding, type CommittedEntrySnapshot, type CommittedScopeSnapshots, type EntryCustodyReport, erasureSelectHash, type EraseMemoryEntriesInput, type ErasureSelect, type ErasedBinding, type MemoryErasureAttestation, computeMemoryBundleHash, type MemoryExportBundle, type MemoryImportReport, type MemoryExportSnapshot, type MemoryBundleImportPlan, type BundleChallengeRow, type BundleLineageRow, type BundlePollutedSession, type MemoryBackend, type MemoryEntry, type MemoryEntryFrontmatter, type MemoryEntryHeader, type MemoryScopeEnumeration, type ScoredMemoryEntry, type NotePatch, type PatchReport, type MaterializedFile, type MemorySessionHandle, type HarvestReport, type HarvestRejection, type HarvestRejectionCode, type ParsedEntryFile, type ScannedEntryFile, SCOPE_SEGMENT_MAX_ENCODED, PROJECT_MARKER_PATH, encodeScopeSegment, decodeScopeSegment, parseScopeKey, formatUserScope, formatOrgScope, formatProjScope, formatUserProjScope, isPersonalScope, assertScopeContractPlacement, formatProjectMarker, parseProjectMarker, resolveProjectId, PROJECT_ID_REGEX, type ParsedScopeKey, migrateScope, type MigrateScopeReport, materializeEntriesToFiles, harvestFilesToPatches, projectionsToWriteBack, screenInboundEntries, REMOTE_HARVEST_PER_FILE_BYTES, REMOTE_MASS_DELETION_FUSE_RATIO, type RemoteMemoryFile, type RemoteMemoryBaseline, type RemoteMaterialization, type RemoteHarvestResult, type InboundEntryFinding, deriveProjectMemoryDir, deriveProjectControlDir, recordProjectIdHint, lookupProjectIdHint, PROJECT_ID_HINTS_FILE, reconcileMemoryEntries, nextSyncBaseline, mergeRecallHits, type MemorySyncCursor, type MemorySyncConflict, type MemorySyncPlan, type RecallSource, syncMemoryScope, type MemorySyncTransport, type MemorySyncRequestBody, type MemorySyncResponseBody, type MemorySyncClientConflict, type SyncMemoryScopeOptions, type MemorySyncClientResult, } from "./core/memory-engine/index.js";
182
+ export { MemoryEngine, FileMemoryEngineBackend, memoryBackendContract, assertMemoryBackendSearchEquivalence, type MemoryBackendContractHooks, buildMemoryInstruction, truncateIndex, scanEntryFiles, deriveRepoKey, deriveRepoMemoryDir, deriveControlPlaneDir, deriveRepoControlPlaneDir, resolveMemoryEngineRoot, scopeDirName, ControlPlaneCorruptError, parseEntryFile, serializeEntryFile, computeEntryRev, entryFromFile, MEMORY_ORIGIN_CAUSES, committedOriginOf, originEquals, consolidationExposedFrontmatter, ambiguousOriginRepresentation, type OriginClearanceRow, type OriginClearanceEvent, type OriginClearanceShadow, committedDistilledOf, distilledEquals, type MemoryEntryDistilled, type MemoryEntryDistilledInput, CONSOLIDATION_DEFAULTS, ConsolidationRefusedError, MEMORY_SEARCH_SUPERSEDED_TAG, consolidationTypeEligible, deriveSupersededSet, memorySupersededNote, readIntentCredentials, supersessionFuseCeiling, memoryConsolidationRecommendedNotice, memoryConsolidationCommittedNotice, memoryConsolidationConflictNotice, memoryConsolidationIncompleteNotice, memoryConsolidationRefusedNotice, memoryConsolidationWithheldNotice, type ConsolidationGateRead, type ConsolidationGateRow, type ConsolidationIntent, type ConsolidationIntentCredentialRow, type ConsolidationLeaseSeat, type ConsolidationProductProposal, type ConsolidationProposal, type MemoryConsolidationOptions, type ConsolidationCommitReceipt, type ConsolidationReconcileReport, type ConsolidationResolveReceipt, type ConsolidationPlanSummary, type ConsolidationPlanFoldEvidence, type SessionMemoryStatus, DISTILLER_DEFAULT_MAX_INPUTS_PER_PRODUCT, LLM_DISTILLER_CONTRACT, LLM_DISTILLER_CONTRACT_DL2, LLM_DISTILLER_CONTRACT_DL3, LLM_DISTILLER_CONTRACTS, MEMORY_DISTILLER_CONTRACT_V1, contractGroupingDiff, driveConsolidationToFixpoint, isAliasModelId, llmPlanDistiller, mintExposurePartitionedPlan, mintLlmConsolidationPlan, MEMORY_DISTILLER_PURITY_CONTRACT_V1, detectCleanArmVerbatimLeak, type CleanArmLeakFinding, type CleanArmLeakVerdict, type MemoryDistillerPurityContract, openAiCompatChatSeat, parseJsonAnswer, planParseRepairs, sanitizeLlmGroups, scheduleUnderFuse, type ConsolidationDistillFn, type ConsolidationDriveCycleRow, type ConsolidationDriveEngine, type ConsolidationDriveResult, type ConsolidationFoldState, type DistillerCandidate, type DistillerChatAnswer, type DistillerChatFn, type DistillerChatRequest, type FuseSchedule, type LlmConsolidationPlan, type LlmConsolidationPlanArm, type LlmConsolidationPlanProduct, type LlmDistillerContract, type MintLlmConsolidationPlanResult, type PlanParseRepairs, type SanitizedLlmGroups, CONSOLIDATION_DRIVER_PLANS_DIR, CONSOLIDATION_DRIVER_RUNS_FILE, CONSOLIDATION_RUN_STOP_REASONS, archiveDistillerPlan, readConsolidationDriverRun, runMemoryConsolidationDriver, type ConsolidationDriverEngine, type ConsolidationDriverRunRow, type ConsolidationRunReceipt, type ConsolidationRunStopReason, type RunMemoryConsolidationOptions, isInstructionEntry, type MemoryEntryOrigin, type MemoryOriginCause, MEMORY_INSTRUCTION_TEMPLATE, MEMORY_RECALL_DISCIPLINE, MEMORY_PREFERENCE_DISCIPLINE, MEMORY_READONLY_NOTICE, MEMORY_CAPTURE_OPTOUT_NOTICE, memoryCaptureOptedOutNotice, memoryCaptureOptOutUnpersistedNotice, SESSION_CAPTURE_OPTOUT_DIR, markSessionCaptureOptOut, readSessionCaptureOptOut, listSessionCaptureOptOut, fileSessionCaptureRecordStore, type SessionCaptureOptOutRecord, type SessionCaptureOptOutMarkOutcome, type SessionCaptureRecordStore, readV2HeaderHints, type V2HeaderHints, LINEAGE_FILE, CHALLENGES_FILE, CHALLENGED_HISTORY_FILE, CHALLENGE_LEDGER_MAX_EVENTS, rebuildStrictControlPlaneLedger, type ControlPlaneRebuildReceipt, type StrictControlPlaneLedger, type ChallengeAssignment, type ChallengeEvent, type ChallengedHistoryRow, type LineagePendingTxn, type LineagePromotion, MEMORY_INDEX_FILENAME, MEMORY_INDEX_MAX_LINES, MEMORY_INDEX_MAX_BYTES, MEMORY_SEARCH_TOOL_NAME, MEMORY_GET_TOOL_NAME, MEMORY_INDEX_TOOL_NAME, MEMORY_ENGINE_TOOL_NAMES, type MemorySearchDetails, type MemorySearchHit, type CleanMemorySearchHit, type ExposedMemorySearchHit, MEMORY_EXPOSURE_BANNER, MEMORY_EXPOSURE_HANDLE_TAG, memoryExposureIndexRow, type MemoryGetDetails, type MemoryIndexDetails, type MemoryIndexRow, type CleanMemoryIndexRow, type ExposedMemoryIndexRow, STUB_ARCHIVED_LINE, DEFAULT_MAX_MEMORY_FILES, DEFAULT_MAX_ENTRY_DEPTH, DEFAULT_HARVEST_DEADLINE_MS, DEFAULT_HARVEST_FILE_BUDGET, MASS_DELETION_FUSE_RATIO, scanMemoryWrite, scanMemoryFileName, scanRemediation, MEMORY_FILENAME_SEGMENT_RE, renderAnnouncements, enqueueMemoryAnnouncement, drainMemoryAnnouncements, peekMemoryAnnouncements, bumpScanFuse, scanFuseCount, clearScanFuse, ANNOUNCEMENTS_FILE, MEMORY_ANNOUNCEMENTS_MAX, SCAN_FUSE_FILE, SCAN_FUSE_THRESHOLD, type MemoryAnnouncement, type ScanFinding, type MemoryEngineOptions, type MemoryInjection, type EntryProvenanceAccount, type TransferEvidence, type CommittedBinding, type CommittedEntrySnapshot, type CommittedScopeSnapshots, type EntryCustodyReport, erasureSelectHash, type EraseMemoryEntriesInput, type ErasureSelect, type ErasedBinding, type MemoryErasureAttestation, computeMemoryBundleHash, type MemoryExportBundle, type MemoryImportReport, type MemoryExportSnapshot, type MemoryBundleImportPlan, type BundleChallengeRow, type BundleLineageRow, type BundlePollutedSession, type MemoryBackend, type MemoryEntry, type MemoryEntryFrontmatter, type MemoryEntryHeader, type MemoryScopeEnumeration, type ScoredMemoryEntry, type NotePatch, type PatchReport, type MaterializedFile, type MemorySessionHandle, type HarvestReport, type HarvestRejection, type HarvestRejectionCode, type ParsedEntryFile, type ScannedEntryFile, SCOPE_SEGMENT_MAX_ENCODED, PROJECT_MARKER_PATH, encodeScopeSegment, decodeScopeSegment, parseScopeKey, formatUserScope, formatOrgScope, formatProjScope, formatUserProjScope, isPersonalScope, assertScopeContractPlacement, formatProjectMarker, parseProjectMarker, resolveProjectId, PROJECT_ID_REGEX, type ParsedScopeKey, migrateScope, type MigrateScopeReport, materializeEntriesToFiles, harvestFilesToPatches, projectionsToWriteBack, screenInboundEntries, REMOTE_HARVEST_PER_FILE_BYTES, REMOTE_MASS_DELETION_FUSE_RATIO, type RemoteMemoryFile, type RemoteMemoryBaseline, type RemoteMaterialization, type RemoteHarvestResult, type InboundEntryFinding, deriveProjectMemoryDir, deriveProjectControlDir, recordProjectIdHint, lookupProjectIdHint, PROJECT_ID_HINTS_FILE, reconcileMemoryEntries, nextSyncBaseline, mergeRecallHits, type MemorySyncCursor, type MemorySyncConflict, type MemorySyncPlan, type RecallSource, syncMemoryScope, type MemorySyncTransport, type MemorySyncRequestBody, type MemorySyncResponseBody, type MemorySyncClientConflict, type SyncMemoryScopeOptions, type MemorySyncClientResult, } from "./core/memory-engine/index.js";
183
183
  export { SHARED_MEMORY_READ_CAP_BYTES, SHARED_MEMORY_LIST_PAGE_SIZE, SharedMemoryStoreError, type SharedMemoryStoreProvider, type SharedMemoryStoreReader, type SharedMemoryPagedList, type SharedMemoryStoreInfo, type SharedMemoryDocumentEntry, type SharedMemorySnapshot, type SharedMemoryRequestContext, type MemoryListDetails, type MemoryReadDetails, } from "./core/shared-memory/types.js";
184
184
  export { sharedMemoryStoreContract, type SharedMemoryFixture, type SharedMemoryStoreContractHooks, } from "./core/shared-memory/contract.js";
185
185
  export { termSet, jaccardDistance, cosineDistance } from "./core/memory-vector.js";
@@ -25,3 +25,4 @@ export { uuidv7 } from "../engine/session/uuid.js";
25
25
  export { SessionError } from "../engine/harness/types.js";
26
26
  export { normalizeGitAnnouncement } from "../engine/harness/types.js";
27
27
  export { gitFrameContextVisible } from "../engine/session/session.js";
28
+ export { isSyntheticApiErrorMessage } from "../engine/harness/types.js";
@@ -14,3 +14,4 @@ export { uuidv7 } from "../engine/session/uuid.js";
14
14
  export { SessionError } from "../engine/harness/types.js";
15
15
  export { normalizeGitAnnouncement } from "../engine/harness/types.js";
16
16
  export { gitFrameContextVisible } from "../engine/session/session.js";
17
+ export { isSyntheticApiErrorMessage } from "../engine/harness/types.js";
@@ -22,12 +22,17 @@ import type { NamedWorkflowListing } from "./workflow-script-store.js";
22
22
  /** The built-in round-based discussion workflow's registered name. */
23
23
  export declare const DISCUSSION_WORKFLOW_NAME = "discussion";
24
24
  /**
25
- * The registered name this built-in carried before the C-R14 vocabulary ruling (2026-08-16), kept
26
- * resolvable for ONE minor: "team" now names the agent-teams family (persistent named teammates) only,
27
- * and a one-off multi-agent debate is a "discussion".
25
+ * The registered name this built-in carried before the C-R14 vocabulary ruling (2026-08-16): "team" now
26
+ * names the agent-teams family (persistent named teammates) only, and a one-off multi-agent debate is a
27
+ * "discussion".
28
28
  *
29
- * @deprecated Use {@link DISCUSSION_WORKFLOW_NAME}. Removed in the next minor; the retired spelling still
30
- * RESOLVES via {@link canonicalWorkflowName} until then, but the listing/card face shows the new name only.
29
+ * RETENTION, as a fact rather than a plan: the ruling's original "kept resolvable for ONE minor" window
30
+ * LAPSED. The alias has stayed on the public export surface from the 5.37.0 window through 7.x, across
31
+ * BOTH the 6.0.0 and 7.0.0 BREAKING windows. It is therefore retained until a removal is announced in a
32
+ * named BREAKING window's ship post — no minor removes it by default.
33
+ *
34
+ * @deprecated Use {@link DISCUSSION_WORKFLOW_NAME}. The retired spelling still RESOLVES via
35
+ * {@link canonicalWorkflowName}, but the listing/card face shows the new name only.
31
36
  */
32
37
  export declare const TEAM_DISCUSSION_WORKFLOW_NAME = "discussion";
33
38
  /**
@@ -35,7 +40,8 @@ export declare const TEAM_DISCUSSION_WORKFLOW_NAME = "discussion";
35
40
  * retired alias. The canonical name is the authority: a resolution reached through an alias returns the
36
41
  * canonical definition (its `meta.name`, hence every run/card projection, is the new name).
37
42
  *
38
- * @deprecated together with the alias table — this becomes an identity function next minor (C-R14).
43
+ * @deprecated together with the alias table — it becomes an identity function when that table is removed
44
+ * in an announced BREAKING window (C-R14).
39
45
  */
40
46
  export declare function canonicalWorkflowName(name: string): string;
41
47
  /**
@@ -72,7 +78,8 @@ export declare function retiredWorkflowNameAliases(canonicalName: string): strin
72
78
  * Callers must apply the deployment's `builtinWorkflows` opt-out BEFORE using this: the equivalence is a
73
79
  * fact about the built-in registry, so a deployment that removed the built-ins removed the alias with them.
74
80
  *
75
- * @deprecated together with the alias table — collapses to `[requested]` next minor (C-R14).
81
+ * @deprecated together with the alias table — collapses to `[requested]` when that table is removed in an
82
+ * announced BREAKING window (C-R14).
76
83
  */
77
84
  export declare function workflowNameProbeOrder(requested: string): string[];
78
85
  /**
@@ -82,8 +89,9 @@ export declare function workflowNameProbeOrder(requested: string): string[];
82
89
  */
83
90
  export declare const DISCUSSION_SCRIPT = "export const meta = {\n name: \"discussion\",\n description: \"Round-based discussion: configurable members debate a topic across rounds (each member sees the transcript so far), then a finalizer synthesizes a structured verdict.\",\n whenToUse: \"Use for a genuinely contested question that benefits from several perspectives arguing across rounds - design trade-offs, plan or risk reviews, adversarial critique of a proposal. Do NOT use it for a single factual question, a task with one obvious answer, or a budget-sensitive run: every round costs one agent call per member, so a discussion is never cheaper than asking once. args (all optional): { topic, members?: [{ role, prompt?, model? }], rounds?, finalizer?: { prompt?, model? } }. Hard ceilings: members is capped at 6 and rounds is capped at 5 regardless of what you pass; the run reports it via log() and a capped field on the result when a request exceeds either.\",\n phases: [\n { title: \"Discussion\" },\n { title: \"Synthesis\" },\n ],\n};\n// Zero-config runnable (design/140 \u00A76 1c): every arg has an opinionated fallback.\nconst raw = args;\nconst a = raw !== null && typeof raw === \"object\" && !Array.isArray(raw) ? raw : {};\nconst topic =\n typeof a.topic === \"string\" && a.topic.trim() !== \"\"\n ? a.topic\n : typeof raw === \"string\" && raw.trim() !== \"\"\n ? raw // ergonomic form: a bare string args IS the topic\n : \"No topic was provided. Discuss: what information should a caller supply to make a discussion like this productive, and when should they NOT convene one?\";\nconst defaultMembers = [\n { role: \"advocate\", prompt: \"Make the strongest constructive case. Propose concrete options and argue their benefits with specifics.\" },\n { role: \"skeptic\", prompt: \"Stress-test every claim made so far. Surface risks, hidden costs, failure modes, and cheaper alternatives.\" },\n];\nconst rawMembers = Array.isArray(a.members) && a.members.length > 0 ? a.members : defaultMembers;\nconst members = rawMembers.slice(0, 6).map((m, i) => {\n const mm = m !== null && typeof m === \"object\" ? m : {};\n const member = {\n role: typeof mm.role === \"string\" && mm.role.trim() !== \"\" ? mm.role : \"member-\" + (i + 1),\n prompt: typeof mm.prompt === \"string\" && mm.prompt.trim() !== \"\" ? mm.prompt : \"Contribute your own distinct perspective: be concrete, give reasons, and engage with what others said.\",\n };\n if (typeof mm.model === \"string\" && mm.model.trim() !== \"\") member.model = mm.model;\n // Slot-tools carrier (design/140 \u2461-3 + F4): a member may BE a registered agent type ({agent:\"reviewer\"}) \u2014\n // persona/tools/model then come from the deployment's AgentDefinition (role library), args stay thin.\n if (typeof mm.agent === \"string\" && mm.agent.trim() !== \"\") member.agent = mm.agent;\n return member;\n});\n// Deterministic budget truncation (design/140 \u00A71 \u9884\u7B97 row): a HARD rounds ceiling + member cap \u2014 never an\n// evaluator agent. The engine's budget/maxAgents hard stops remain the backstop.\nconst requestedRounds = Math.floor(Number(a.rounds));\nconst normalizedRounds = Number.isFinite(requestedRounds) && requestedRounds >= 1 ? requestedRounds : 2;\nconst rounds = Math.min(normalizedRounds, 5);\n// RB-380 disclosure: the member/round slices above are silent by construction (Array.prototype.slice /\n// Math.min just drop the excess) \u2014 record + surface it instead of a caller finding out only by counting\n// transcript entries. Fires only when a request actually exceeded a ceiling (never on the common path).\nconst capNotes = [];\nif (rawMembers.length > 6) capNotes.push(\"requested \" + rawMembers.length + \" members, capped at 6\");\nif (normalizedRounds > 5) capNotes.push(\"requested \" + normalizedRounds + \" rounds, capped at 5\");\nfor (const note of capNotes) log(\"discussion: \" + note);\nconst fin = a.finalizer !== null && typeof a.finalizer === \"object\" && !Array.isArray(a.finalizer) ? a.finalizer : {};\nconst finalizerPrompt = typeof fin.prompt === \"string\" && fin.prompt.trim() !== \"\"\n ? fin.prompt\n : \"You are the synthesis lead. Read the full discussion transcript and produce the final verdict: the decision/answer, the key supporting points, and the strongest unresolved dissent (if any). Do not introduce new arguments of your own.\";\nconst clip = (s) => { const t = String(s); return t.length > 4000 ? t.slice(0, 4000) + \" ...[truncated]\" : t; };\nconst isBudgetStop = (e) => e !== null && typeof e === \"object\" && e.code === \"workflow.budget_exceeded\";\n\nphase(\"Discussion\");\nconst transcript = [];\nlet truncated = null;\nfor (let r = 1; r <= rounds && truncated === null; r++) {\n // Deterministic early stop on an exhausted budget (a live read of the engine budget; the engine's\n // hard WorkflowBudgetExceededError remains the backstop if a member call itself crosses the line).\n if (budget.total !== null && budget.remaining() <= 0) { truncated = \"budget exhausted before round \" + r; break; }\n for (const m of members) {\n const history = transcript.length === 0 ? \"(none yet - you open the discussion)\" : transcript.join(\"\\n\\n\");\n const spec = {\n objective:\n \"Discussion on: \" + topic + \"\\n\\n\" +\n 'You are \"' + m.role + '\" in round ' + r + \" of \" + rounds + \".\\n\" +\n \"Your brief: \" + m.prompt + \"\\n\\n\" +\n \"Transcript so far:\\n\" + history + \"\\n\\n\" +\n \"Respond to the strongest points others made (do not repeat yourself), then advance your own position. Be concise: a few tight paragraphs at most.\",\n };\n if (m.model !== undefined) spec.modelName = m.model;\n let res;\n try {\n res = await agent(spec, m.agent !== undefined ? { label: m.role + \"-r\" + r, phase: \"Discussion\", agentType: m.agent } : { label: m.role + \"-r\" + r, phase: \"Discussion\" });\n } catch (e) {\n // The engine's budget hard stop: keep what the discussion already produced instead of failing the run.\n if (isBudgetStop(e)) { truncated = \"budget exhausted at \" + m.role + \", round \" + r; break; }\n throw e;\n }\n const text = res && res.status === \"completed\" ? clip(res.result) : \"(no contribution - agent ended \" + (res ? res.status : \"unknown\") + \")\";\n transcript.push(m.role + \" (round \" + r + \"): \" + text);\n }\n}\n\nphase(\"Synthesis\");\nconst finalSpec = {\n objective:\n finalizerPrompt + \"\\n\\nTopic: \" + topic + \"\\n\\nFull transcript:\\n\" +\n (transcript.length === 0 ? \"(the discussion produced no contributions)\" : transcript.join(\"\\n\\n\")) +\n (truncated ? \"\\n\\nNote: the discussion was cut short (\" + truncated + \").\" : \"\"),\n};\nif (typeof fin.model === \"string\" && fin.model.trim() !== \"\") finalSpec.modelName = fin.model;\nlet verdict = null;\ntry {\n verdict = await agent(finalSpec, {\n label: \"finalizer\",\n phase: \"Synthesis\",\n schema: {\n type: \"object\",\n properties: {\n decision: { type: \"string\", description: \"The final answer/decision, one paragraph.\" },\n keyPoints: { type: \"array\", items: { type: \"string\" }, description: \"The strongest supporting points from the discussion.\" },\n dissent: { type: \"string\", description: \"The strongest unresolved counter-position, if any.\" },\n },\n required: [\"decision\", \"keyPoints\"],\n },\n });\n} catch (e) {\n // Budget died before synthesis: return the transcript honestly rather than failing the whole run.\n if (!isBudgetStop(e)) throw e;\n truncated = truncated === null ? \"budget exhausted before synthesis\" : truncated;\n}\n\nreturn {\n topic,\n rounds,\n members: members.map((m) => m.role),\n ...(capNotes.length > 0 ? { capped: capNotes } : {}),\n ...(truncated ? { truncated } : {}),\n transcript,\n verdict: verdict && verdict.structuredOutput !== undefined ? verdict.structuredOutput : (verdict ? verdict.result : null),\n};\n";
84
91
  /**
85
- * @deprecated Use {@link DISCUSSION_SCRIPT}. Retired spelling kept for one minor (C-R14) so a deployment
86
- * importing the constant by its old name keeps compiling; the value is the SAME script (its `meta.name`
92
+ * @deprecated Use {@link DISCUSSION_SCRIPT}. Retired spelling retained (C-R14; the original one-minor
93
+ * window lapsed removal needs an announced BREAKING window) so a deployment importing the constant by
94
+ * its old name keeps compiling; the value is the SAME script (its `meta.name`
87
95
  * is the new `discussion`).
88
96
  */
89
97
  export declare const TEAM_DISCUSSION_SCRIPT = "export const meta = {\n name: \"discussion\",\n description: \"Round-based discussion: configurable members debate a topic across rounds (each member sees the transcript so far), then a finalizer synthesizes a structured verdict.\",\n whenToUse: \"Use for a genuinely contested question that benefits from several perspectives arguing across rounds - design trade-offs, plan or risk reviews, adversarial critique of a proposal. Do NOT use it for a single factual question, a task with one obvious answer, or a budget-sensitive run: every round costs one agent call per member, so a discussion is never cheaper than asking once. args (all optional): { topic, members?: [{ role, prompt?, model? }], rounds?, finalizer?: { prompt?, model? } }. Hard ceilings: members is capped at 6 and rounds is capped at 5 regardless of what you pass; the run reports it via log() and a capped field on the result when a request exceeds either.\",\n phases: [\n { title: \"Discussion\" },\n { title: \"Synthesis\" },\n ],\n};\n// Zero-config runnable (design/140 \u00A76 1c): every arg has an opinionated fallback.\nconst raw = args;\nconst a = raw !== null && typeof raw === \"object\" && !Array.isArray(raw) ? raw : {};\nconst topic =\n typeof a.topic === \"string\" && a.topic.trim() !== \"\"\n ? a.topic\n : typeof raw === \"string\" && raw.trim() !== \"\"\n ? raw // ergonomic form: a bare string args IS the topic\n : \"No topic was provided. Discuss: what information should a caller supply to make a discussion like this productive, and when should they NOT convene one?\";\nconst defaultMembers = [\n { role: \"advocate\", prompt: \"Make the strongest constructive case. Propose concrete options and argue their benefits with specifics.\" },\n { role: \"skeptic\", prompt: \"Stress-test every claim made so far. Surface risks, hidden costs, failure modes, and cheaper alternatives.\" },\n];\nconst rawMembers = Array.isArray(a.members) && a.members.length > 0 ? a.members : defaultMembers;\nconst members = rawMembers.slice(0, 6).map((m, i) => {\n const mm = m !== null && typeof m === \"object\" ? m : {};\n const member = {\n role: typeof mm.role === \"string\" && mm.role.trim() !== \"\" ? mm.role : \"member-\" + (i + 1),\n prompt: typeof mm.prompt === \"string\" && mm.prompt.trim() !== \"\" ? mm.prompt : \"Contribute your own distinct perspective: be concrete, give reasons, and engage with what others said.\",\n };\n if (typeof mm.model === \"string\" && mm.model.trim() !== \"\") member.model = mm.model;\n // Slot-tools carrier (design/140 \u2461-3 + F4): a member may BE a registered agent type ({agent:\"reviewer\"}) \u2014\n // persona/tools/model then come from the deployment's AgentDefinition (role library), args stay thin.\n if (typeof mm.agent === \"string\" && mm.agent.trim() !== \"\") member.agent = mm.agent;\n return member;\n});\n// Deterministic budget truncation (design/140 \u00A71 \u9884\u7B97 row): a HARD rounds ceiling + member cap \u2014 never an\n// evaluator agent. The engine's budget/maxAgents hard stops remain the backstop.\nconst requestedRounds = Math.floor(Number(a.rounds));\nconst normalizedRounds = Number.isFinite(requestedRounds) && requestedRounds >= 1 ? requestedRounds : 2;\nconst rounds = Math.min(normalizedRounds, 5);\n// RB-380 disclosure: the member/round slices above are silent by construction (Array.prototype.slice /\n// Math.min just drop the excess) \u2014 record + surface it instead of a caller finding out only by counting\n// transcript entries. Fires only when a request actually exceeded a ceiling (never on the common path).\nconst capNotes = [];\nif (rawMembers.length > 6) capNotes.push(\"requested \" + rawMembers.length + \" members, capped at 6\");\nif (normalizedRounds > 5) capNotes.push(\"requested \" + normalizedRounds + \" rounds, capped at 5\");\nfor (const note of capNotes) log(\"discussion: \" + note);\nconst fin = a.finalizer !== null && typeof a.finalizer === \"object\" && !Array.isArray(a.finalizer) ? a.finalizer : {};\nconst finalizerPrompt = typeof fin.prompt === \"string\" && fin.prompt.trim() !== \"\"\n ? fin.prompt\n : \"You are the synthesis lead. Read the full discussion transcript and produce the final verdict: the decision/answer, the key supporting points, and the strongest unresolved dissent (if any). Do not introduce new arguments of your own.\";\nconst clip = (s) => { const t = String(s); return t.length > 4000 ? t.slice(0, 4000) + \" ...[truncated]\" : t; };\nconst isBudgetStop = (e) => e !== null && typeof e === \"object\" && e.code === \"workflow.budget_exceeded\";\n\nphase(\"Discussion\");\nconst transcript = [];\nlet truncated = null;\nfor (let r = 1; r <= rounds && truncated === null; r++) {\n // Deterministic early stop on an exhausted budget (a live read of the engine budget; the engine's\n // hard WorkflowBudgetExceededError remains the backstop if a member call itself crosses the line).\n if (budget.total !== null && budget.remaining() <= 0) { truncated = \"budget exhausted before round \" + r; break; }\n for (const m of members) {\n const history = transcript.length === 0 ? \"(none yet - you open the discussion)\" : transcript.join(\"\\n\\n\");\n const spec = {\n objective:\n \"Discussion on: \" + topic + \"\\n\\n\" +\n 'You are \"' + m.role + '\" in round ' + r + \" of \" + rounds + \".\\n\" +\n \"Your brief: \" + m.prompt + \"\\n\\n\" +\n \"Transcript so far:\\n\" + history + \"\\n\\n\" +\n \"Respond to the strongest points others made (do not repeat yourself), then advance your own position. Be concise: a few tight paragraphs at most.\",\n };\n if (m.model !== undefined) spec.modelName = m.model;\n let res;\n try {\n res = await agent(spec, m.agent !== undefined ? { label: m.role + \"-r\" + r, phase: \"Discussion\", agentType: m.agent } : { label: m.role + \"-r\" + r, phase: \"Discussion\" });\n } catch (e) {\n // The engine's budget hard stop: keep what the discussion already produced instead of failing the run.\n if (isBudgetStop(e)) { truncated = \"budget exhausted at \" + m.role + \", round \" + r; break; }\n throw e;\n }\n const text = res && res.status === \"completed\" ? clip(res.result) : \"(no contribution - agent ended \" + (res ? res.status : \"unknown\") + \")\";\n transcript.push(m.role + \" (round \" + r + \"): \" + text);\n }\n}\n\nphase(\"Synthesis\");\nconst finalSpec = {\n objective:\n finalizerPrompt + \"\\n\\nTopic: \" + topic + \"\\n\\nFull transcript:\\n\" +\n (transcript.length === 0 ? \"(the discussion produced no contributions)\" : transcript.join(\"\\n\\n\")) +\n (truncated ? \"\\n\\nNote: the discussion was cut short (\" + truncated + \").\" : \"\"),\n};\nif (typeof fin.model === \"string\" && fin.model.trim() !== \"\") finalSpec.modelName = fin.model;\nlet verdict = null;\ntry {\n verdict = await agent(finalSpec, {\n label: \"finalizer\",\n phase: \"Synthesis\",\n schema: {\n type: \"object\",\n properties: {\n decision: { type: \"string\", description: \"The final answer/decision, one paragraph.\" },\n keyPoints: { type: \"array\", items: { type: \"string\" }, description: \"The strongest supporting points from the discussion.\" },\n dissent: { type: \"string\", description: \"The strongest unresolved counter-position, if any.\" },\n },\n required: [\"decision\", \"keyPoints\"],\n },\n });\n} catch (e) {\n // Budget died before synthesis: return the transcript honestly rather than failing the whole run.\n if (!isBudgetStop(e)) throw e;\n truncated = truncated === null ? \"budget exhausted before synthesis\" : truncated;\n}\n\nreturn {\n topic,\n rounds,\n members: members.map((m) => m.role),\n ...(capNotes.length > 0 ? { capped: capNotes } : {}),\n ...(truncated ? { truncated } : {}),\n transcript,\n verdict: verdict && verdict.structuredOutput !== undefined ? verdict.structuredOutput : (verdict ? verdict.result : null),\n};\n";
@@ -245,7 +245,15 @@ export interface RunWorkflowToolDeps {
245
245
  sessionId: string;
246
246
  controlDir?: string;
247
247
  }>;
248
- };
248
+ } | Promise<{
249
+ optedOut: boolean;
250
+ indeterminate: boolean;
251
+ controlDir?: string;
252
+ ancestors?: ReadonlyArray<{
253
+ sessionId: string;
254
+ controlDir?: string;
255
+ }>;
256
+ }>;
249
257
  /** TRUSTED nesting depth from the run's internals (NOT a tool param) — passed to `startWorkflow` so a
250
258
  * cross-process child workflow is rejected by the one-level guard. */
251
259
  workflowDepth?: number;