@wrongstack/core 0.298.1 → 0.298.3

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.
@@ -13220,8 +13220,16 @@ function resolveSubagentModelTarget(config, role, opts = {}) {
13220
13220
  const resolution = resolveModelMatrixResolution(config.modelMatrix, role);
13221
13221
  const matrixTarget = resolveModelTargetFromEntry(config, resolution?.entry);
13222
13222
  const implementationTarget = opts.implementationTarget ?? resolveImplementationModelTarget(config);
13223
+ const isAvailable = (providerId, model) => {
13224
+ if (!opts.statusTracker) return true;
13225
+ if (!providerId || !model) return true;
13226
+ return opts.statusTracker.isAvailable(providerId, model);
13227
+ };
13228
+ const matrixEffective = materializeTarget(config, matrixTarget);
13229
+ const matrixIsAvailable = !matrixEffective || isAvailable(matrixEffective.provider, matrixEffective.model);
13223
13230
  if (!roleNeedsIndependentReviewModel(role)) {
13224
13231
  if (!matrixTarget) return void 0;
13232
+ if (!matrixIsAvailable) return void 0;
13225
13233
  return {
13226
13234
  ...matrixTarget,
13227
13235
  source: "matrix",
@@ -13230,16 +13238,27 @@ function resolveSubagentModelTarget(config, role, opts = {}) {
13230
13238
  }
13231
13239
  const matrixRef = materializeTarget(config, matrixTarget);
13232
13240
  if (resolution?.source === "role" || resolution?.source === "phase") {
13233
- return matrixTarget ? { ...matrixTarget, source: "matrix", matrixSource: resolution.source } : void 0;
13241
+ if (!matrixTarget) return void 0;
13242
+ if (!matrixIsAvailable) return void 0;
13243
+ return {
13244
+ ...matrixTarget,
13245
+ source: "matrix",
13246
+ matrixSource: resolution.source
13247
+ };
13234
13248
  }
13235
13249
  if (matrixRef && !sameModelReference(matrixRef, implementationTarget)) {
13250
+ if (!matrixIsAvailable) return void 0;
13236
13251
  return {
13237
13252
  ...matrixTarget ?? {},
13238
13253
  source: "matrix",
13239
13254
  matrixSource: resolution?.source
13240
13255
  };
13241
13256
  }
13242
- const diverse = chooseDiverseModelTarget(config, implementationTarget);
13257
+ const diverse = chooseDiverseModelTargetWithTracker(
13258
+ config,
13259
+ implementationTarget,
13260
+ opts.statusTracker
13261
+ );
13243
13262
  if (diverse) {
13244
13263
  return {
13245
13264
  provider: diverse.provider,
@@ -13253,6 +13272,7 @@ function resolveSubagentModelTarget(config, role, opts = {}) {
13253
13272
  };
13254
13273
  }
13255
13274
  if (!matrixTarget) return void 0;
13275
+ if (!matrixIsAvailable) return void 0;
13256
13276
  return {
13257
13277
  ...matrixTarget,
13258
13278
  source: "matrix",
@@ -13289,6 +13309,14 @@ function chooseDiverseModelTarget(config, avoid) {
13289
13309
  candidates.sort((a, b) => modelDiversityScore(b, avoid) - modelDiversityScore(a, avoid));
13290
13310
  return candidates[0];
13291
13311
  }
13312
+ function chooseDiverseModelTargetWithTracker(config, avoid, tracker) {
13313
+ if (!tracker) return chooseDiverseModelTarget(config, avoid);
13314
+ const ranked = collectConfiguredModelTargets(config).filter((candidate) => !sameModelReference(candidate, avoid)).sort((a, b) => modelDiversityScore(b, avoid) - modelDiversityScore(a, avoid));
13315
+ for (const candidate of ranked) {
13316
+ if (tracker.isAvailable(candidate.provider, candidate.model)) return candidate;
13317
+ }
13318
+ return void 0;
13319
+ }
13292
13320
  function collectConfiguredModelTargets(config) {
13293
13321
  const seen = /* @__PURE__ */ new Set();
13294
13322
  const out = [];
@@ -20402,6 +20430,36 @@ function isMailboxLeader(agentId, role) {
20402
20430
  function isMailboxMessageVisibleTo(message, agentId, role) {
20403
20431
  return message.audience !== "leaders" || isMailboxLeader(agentId, role);
20404
20432
  }
20433
+ function isAffectedBySessionAffinity(message) {
20434
+ return message.sessionAffinity !== void 0;
20435
+ }
20436
+ async function acceptMailboxMessageForSession(message, currentSessionId, ctx) {
20437
+ if (!isAffectedBySessionAffinity(message)) return true;
20438
+ const affinity = message.sessionAffinity;
20439
+ if (affinity === null || typeof affinity !== "object" || Array.isArray(affinity)) {
20440
+ return false;
20441
+ }
20442
+ if (affinity.sessionId !== void 0 && typeof affinity.sessionId !== "string" || affinity.reportId !== void 0 && typeof affinity.reportId !== "string") {
20443
+ return false;
20444
+ }
20445
+ if (!currentSessionId) {
20446
+ return ctx?.allowUnscoped === true;
20447
+ }
20448
+ if (typeof affinity.sessionId === "string" && affinity.sessionId.length > 0) {
20449
+ if (affinity.sessionId !== currentSessionId) return false;
20450
+ return true;
20451
+ }
20452
+ if (affinity.reportId && ctx?.resolveChimeraReportSessionId) {
20453
+ try {
20454
+ const resolved = await ctx.resolveChimeraReportSessionId(affinity.reportId);
20455
+ if (resolved === currentSessionId) return true;
20456
+ if (resolved !== void 0) return false;
20457
+ } catch {
20458
+ }
20459
+ }
20460
+ if (ctx?.allowUnscoped === true) return true;
20461
+ return false;
20462
+ }
20405
20463
  function validateSendType(type, to) {
20406
20464
  if (type === "control") {
20407
20465
  throw new TypeError(
@@ -21025,7 +21083,7 @@ function makeMailboxTool(opts = {}) {
21025
21083
  case "ack":
21026
21084
  return executeAck(mb, callerId, i);
21027
21085
  case "query":
21028
- return executeQuery(mb, callerId, identity.role, i);
21086
+ return executeQuery(mb, callerId, callerSessionId, identity.role, i);
21029
21087
  case "status":
21030
21088
  return executeStatus(mb);
21031
21089
  case "online":
@@ -21054,12 +21112,18 @@ async function executeCheck(mb, agentId, sessionId, aliases, role, i) {
21054
21112
  )
21055
21113
  );
21056
21114
  const seen = /* @__PURE__ */ new Set();
21057
- const messages = batches.flat().filter((m) => {
21115
+ const candidates = batches.flat().filter((m) => {
21058
21116
  if (seen.has(m.id)) return false;
21059
21117
  if (!isMailboxMessageVisibleTo(m, agentId, role)) return false;
21060
21118
  seen.add(m.id);
21061
21119
  return true;
21062
21120
  });
21121
+ const messages = [];
21122
+ for (const m of candidates) {
21123
+ if (await acceptMailboxMessageForSession(m, sessionId)) {
21124
+ messages.push(m);
21125
+ }
21126
+ }
21063
21127
  const acked = markRead || completed ? await mb.ackMany({
21064
21128
  acks: messages.map((m) => ({
21065
21129
  messageId: m.id,
@@ -21140,7 +21204,7 @@ async function executeAck(mb, agentId, i) {
21140
21204
  summary: `Message ${messageId} acknowledged. Read by ${Object.keys(updated.readBy).length} agent(s), Completed: ${updated.completed}.`
21141
21205
  };
21142
21206
  }
21143
- async function executeQuery(mb, agentId, role, i) {
21207
+ async function executeQuery(mb, agentId, sessionId, role, i) {
21144
21208
  const limit = i.limit ?? 50;
21145
21209
  const messages = await mb.query({
21146
21210
  to: i.to,
@@ -21153,7 +21217,12 @@ async function executeQuery(mb, agentId, role, i) {
21153
21217
  sessionId: i.sessionId,
21154
21218
  limit
21155
21219
  });
21156
- const visible = messages.filter((message) => isMailboxMessageVisibleTo(message, agentId, role));
21220
+ const visible = [];
21221
+ for (const message of messages) {
21222
+ if (isMailboxMessageVisibleTo(message, agentId, role) && await acceptMailboxMessageForSession(message, sessionId)) {
21223
+ visible.push(message);
21224
+ }
21225
+ }
21157
21226
  return { ok: true, count: visible.length, messages: visible, summary: `${visible.length} message(s).` };
21158
21227
  }
21159
21228
  async function executeStatus(mb) {
@@ -21944,6 +22013,14 @@ var SEND_ALLOWED_FIELDS = /* @__PURE__ */ new Set([
21944
22013
  "senderSessionId",
21945
22014
  "ttlMs",
21946
22015
  "taskContext"
22016
+ // 'sessionAffinity' is NOT in the allow-list. The trust-line contract:
22017
+ // session-affinity tokens are stamped ONLY by trusted internal callers
22018
+ // (chimera/auto-review pipelines) that call `mailbox.send()` directly
22019
+ // and never go through this boundary codec. Allowing the field at the
22020
+ // boundary would let any actor with `mail.send.informational` stamp
22021
+ // another session's id and bypass the receiver-side filter. The
22022
+ // receiver trusts the sender-asserted `sessionId`; the boundary must
22023
+ // refuse the field entirely.
21947
22024
  ]);
21948
22025
  var ACK_ALLOWED_FIELDS = /* @__PURE__ */ new Set([
21949
22026
  "messageId",
@@ -21980,7 +22057,15 @@ function parseMailboxSendInput(payload, actor) {
21980
22057
  const priority = validatePriority(rawPriority);
21981
22058
  const audience = validateAudience(payload["audience"]);
21982
22059
  const replyTo = optionalString2(payload, "replyTo", "send");
21983
- return { to, type: typeResult.type, subject, body, priority, audience, replyTo };
22060
+ return {
22061
+ to,
22062
+ type: typeResult.type,
22063
+ subject,
22064
+ body,
22065
+ priority,
22066
+ audience,
22067
+ replyTo
22068
+ };
21984
22069
  }
21985
22070
  function parseMailboxQueryInput(payload, actor) {
21986
22071
  assertCapability(actor, "mail.read.self", "query");
@@ -75,12 +75,12 @@ export declare function makeMailSendTool(opts?: MailToolsOptions): {
75
75
  required: string[];
76
76
  };
77
77
  execute(input: unknown, ctx: Context): Promise<{
78
- messageId?: never;
79
- to?: never;
80
- summary?: never;
81
78
  ok: boolean;
82
79
  error: string;
80
+ messageId?: never;
83
81
  from?: never;
82
+ to?: never;
83
+ summary?: never;
84
84
  } | {
85
85
  error?: never;
86
86
  ok: boolean;
@@ -56,7 +56,7 @@ export declare function parseMailboxSendInput(payload: Record<string, unknown>,
56
56
  * Queries tolerate unknown fields (forward compatibility for read-only clients).
57
57
  * Actor-derived recipient forms are NOT overridden by body-supplied `readerRole`.
58
58
  *
59
- * @throws {MailboxValidationError} on validation failure.
59
+ * @throws {MailboxValidationError} on any validation failure.
60
60
  */
61
61
  export declare function parseMailboxQueryInput(payload: Record<string, unknown>, actor: MailboxActorContext): MailboxQuery;
62
62
  /**
@@ -1520,6 +1520,34 @@ function parseTaskContext(value) {
1520
1520
  ...status === void 0 ? {} : { status }
1521
1521
  };
1522
1522
  }
1523
+ function parseSessionAffinity(value) {
1524
+ if (value === void 0) return void 0;
1525
+ if (!isRecord2(value)) {
1526
+ throw new TypeError('mailbox message field "sessionAffinity" must be an object');
1527
+ }
1528
+ const sessionId = value["sessionId"];
1529
+ const reportId = value["reportId"];
1530
+ const kind = optionalString(value, "kind");
1531
+ if (sessionId !== void 0) {
1532
+ if (typeof sessionId !== "string" || sessionId.length === 0) {
1533
+ throw new TypeError("mailbox message sessionAffinity.sessionId must be a non-empty string");
1534
+ }
1535
+ return {
1536
+ sessionId,
1537
+ ...optionalString(value, "reportId"),
1538
+ ...kind
1539
+ };
1540
+ }
1541
+ if (typeof reportId !== "string" || reportId.length === 0) {
1542
+ throw new TypeError(
1543
+ "mailbox message sessionAffinity.reportId must be a non-empty string when sessionId is absent"
1544
+ );
1545
+ }
1546
+ return {
1547
+ reportId,
1548
+ ...kind
1549
+ };
1550
+ }
1523
1551
  function parseMailboxMessage(value) {
1524
1552
  if (!isRecord2(value)) throw new TypeError("mailbox message must be an object");
1525
1553
  const to = value["to"] === void 0 ? "" : requiredString(value, "to");
@@ -1529,6 +1557,7 @@ function parseMailboxMessage(value) {
1529
1557
  }
1530
1558
  const taskContext = parseTaskContext(value["taskContext"]);
1531
1559
  const audience = parseAudience(value["audience"]);
1560
+ const sessionAffinity = parseSessionAffinity(value["sessionAffinity"]);
1532
1561
  return {
1533
1562
  id: requiredString(value, "id"),
1534
1563
  from: requiredString(value, "from"),
@@ -1549,7 +1578,8 @@ function parseMailboxMessage(value) {
1549
1578
  ...optionalString(value, "replyTo"),
1550
1579
  ...optionalString(value, "senderSessionId"),
1551
1580
  ...optionalString(value, "expiresAt"),
1552
- ...taskContext === void 0 ? {} : { taskContext }
1581
+ ...taskContext === void 0 ? {} : { taskContext },
1582
+ ...sessionAffinity === void 0 ? {} : { sessionAffinity }
1553
1583
  };
1554
1584
  }
1555
1585
  function isAckRecord(value) {
@@ -2620,6 +2650,7 @@ var SqliteMailbox = class {
2620
2650
  ...input.replyTo !== void 0 ? { replyTo: input.replyTo } : {},
2621
2651
  ...input.taskContext !== void 0 ? { taskContext: input.taskContext } : {},
2622
2652
  ...input.senderSessionId !== void 0 ? { senderSessionId: input.senderSessionId } : {},
2653
+ ...input.sessionAffinity !== void 0 ? { sessionAffinity: input.sessionAffinity } : {},
2623
2654
  ...input.ttlMs !== void 0 ? { expiresAt: new Date(Date.now() + input.ttlMs).toISOString() } : {}
2624
2655
  };
2625
2656
  this.persistMessage(message);
@@ -88,7 +88,7 @@ export { MAILBOX_TYPE_PROPERTIES, type MailboxMessageType, type MailboxTypeCateg
88
88
  * The mailbox system enforces these dispatch rules:
89
89
  *
90
90
  * 1. **Send-side**: `mail_send` auto-defaults the type: `broadcast` when
91
- * `to` is `"*"` or `"@session::..."`, otherwise `note`.
91
+ * `to` is `"*"` or `"@session:..."`, otherwise `note`.
92
92
  * 2. **Send-side validation**: `assign` always requires a specific `to`
93
93
  * (not `"*"`). `control` is reserved for runtime use — agents passing it
94
94
  * via the tool surface will be rejected.
@@ -98,8 +98,8 @@ export { MAILBOX_TYPE_PROPERTIES, type MailboxMessageType, type MailboxTypeCateg
98
98
  * 4. **Control isolation**: `control`-type messages are filtered by
99
99
  * `injectPendingMailboxMessages()` and NEVER enter the folded
100
100
  * conversation block — they are out-of-band signals only.
101
- * 5. **Background routing**: in `background` delivery mode, only
102
- * `ACTIONABLE_BACKGROUND_TYPES` (`steer`, `ask`, `assign`, `result`,
101
+ * 5. **Background routing**: in `background` delivery mode, only the
102
+ * actionable message types (`steer`, `ask`, `assign`, `result`,
103
103
  * `review`) are escalated; `note`, `btw`, `status`, and `broadcast`
104
104
  * are suppressed to minimise disruption during tool work.
105
105
  * 6. **Awareness polling**: `btw` messages intercepted by background
@@ -134,6 +134,100 @@ export declare function mailboxIdentityBase(agentId: string): string;
134
134
  export declare function isMailboxLeader(agentId: string, role?: string): boolean;
135
135
  /** Whether a message may be consumed by the supplied agent identity. */
136
136
  export declare function isMailboxMessageVisibleTo(message: Pick<MailboxMessage, 'audience'>, agentId: string, role?: string): boolean;
137
+ /**
138
+ * Shape of the optional secondary lookup the leader filter performs when a
139
+ * message arrives without an affinity token (legacy sender / older build).
140
+ *
141
+ * `resolveChimeraReportSessionId(reportId)` returns the persisted
142
+ * `ReviewReport.sessionId` for that report id, or `undefined` when the
143
+ * report is unknown. May be sync or async — the filter awaits the result.
144
+ */
145
+ export interface MailboxSessionAffinityContext {
146
+ /**
147
+ * Look up the originating session id of a chimera report by its report id.
148
+ * Return `undefined` for unknown reports. Sync or async.
149
+ */
150
+ resolveChimeraReportSessionId?: ((reportId: string) => string | undefined | Promise<string | undefined>) | undefined;
151
+ /**
152
+ * Legacy / one-shot opt-in. The flag has TWO consultation points inside
153
+ * {@link acceptMailboxMessageForSession}:
154
+ *
155
+ * 1. When the recipient does not pass a `currentSessionId` (rule 2),
156
+ * a token-bearing message is accepted iff this is `true`.
157
+ * 2. When a token-bearing message lacks a resolvable `sessionId` AND
158
+ * the persisted-report resolver returns no match (rule 5), the
159
+ * message is accepted iff this is `true`.
160
+ *
161
+ * Token-LESS messages are NOT gated by this flag — they are accepted
162
+ * unconditionally by rule 1 as the discriminator for legitimate
163
+ * subagent `result` / `review` traffic that does not stamp an affinity
164
+ * token (task-auctioneer, cascade handlers, legacy senders).
165
+ *
166
+ * This flag does NOT override an explicit mismatch: a message that
167
+ * carries `sessionAffinity.sessionId !== currentSessionId` is dropped
168
+ * regardless of this flag, because the sender explicitly claimed a
169
+ * different session — honoring it would re-open the leak.
170
+ *
171
+ * Default `false`.
172
+ */
173
+ allowUnscoped?: boolean | undefined;
174
+ }
175
+ /**
176
+ * Decide whether a mailbox message belongs to the recipient's current session
177
+ * and should therefore be delivered to the leader's inbox.
178
+ *
179
+ * The actual code path is:
180
+ *
181
+ * 1. {@link isAffectedBySessionAffinity} check. If the message has no
182
+ * affinity token, the filter does NOT apply and the message is accepted.
183
+ * This is the **discriminator** that protects legitimate subagent
184
+ * `result` / `review` traffic (task-auctioneer, cascade handlers) from
185
+ * collateral drops — they do not stamp an affinity token, so they fall
186
+ * through here. When a token is present, every token is enforced regardless
187
+ * of its optional `kind`; malformed or unrecognized kinds must not bypass
188
+ * a mismatched `sessionAffinity.sessionId`.
189
+ * 2. No-session-id branch. If the recipient did not pass a `currentSessionId`,
190
+ * accept only when `ctx.allowUnscoped === true`. Default: **fail-closed**
191
+ * (drop) — the safe behavior, not a silent fail-open.
192
+ * 3. Explicit `sessionAffinity.sessionId` match. If the sender stamped a
193
+ * session id and it does not match the recipient's current session id,
194
+ * drop immediately. The sender claimed a different session; honoring it
195
+ * would re-open the cross-session leak. This drop happens even when
196
+ * `allowUnscoped` is set — a wrong session is misconfiguration, not a
197
+ * legacy case.
198
+ * 4. Persisted-report fallback. If the token lacks a `sessionId` but carries
199
+ * a `reportId`, await `ctx.resolveChimeraReportSessionId(reportId)` and
200
+ * accept if the persisted `ReviewReport.sessionId` matches. Accepts
201
+ * sync or async resolvers.
202
+ * 5. `allowUnscoped` opt-in. If the previous steps did not accept and
203
+ * `ctx.allowUnscoped === true`, accept (legacy / one-shot tool
204
+ * compatibility).
205
+ * 6. Otherwise drop (`false`).
206
+ *
207
+ * Trust note: the filter accepts the sender-asserted `sessionAffinity.sessionId`
208
+ * without store cross-check (rule 3). The persisted-report fallback (rule 4)
209
+ * is the only server-side check. A sender that fabricates both an affinity
210
+ * token and a persisted report could impersonate another session — guard
211
+ * chimera's send path so only the actual originating session can stamp the
212
+ * token, and do not relax rule 3.
213
+ *
214
+ * The helper is side-effect-free except for awaiting the optional resolver.
215
+ * It lives next to {@link isMailboxMessageVisibleTo} so mailbox consumers
216
+ * can compose both filters without taking a dependency on the persistence
217
+ * layer.
218
+ */
219
+ export declare function acceptMailboxMessageForSession(message: Pick<MailboxMessage, 'type' | 'sessionAffinity'>, currentSessionId: string | undefined, ctx?: MailboxSessionAffinityContext): Promise<boolean>;
220
+ /**
221
+ * Synchronous wrapper for callers that cannot await the resolver
222
+ * (e.g. legacy event handlers). If `ctx.resolveChimeraReportSessionId` is
223
+ * async, it is called without awaiting and the result is ignored — the
224
+ * rule degrades to "match affinity.sessionId, otherwise drop unless
225
+ * allowUnscoped".
226
+ *
227
+ * Prefer {@link acceptMailboxMessageForSession} whenever the caller can
228
+ * await.
229
+ */
230
+ export declare function acceptMailboxMessageForSessionSync(message: Pick<MailboxMessage, 'type' | 'sessionAffinity'>, currentSessionId: string | undefined, ctx?: MailboxSessionAffinityContext): boolean;
137
231
  /** Category + expectsReply are provided by MAILBOX_TYPE_PROPERTIES directly. */
138
232
  /**
139
233
  * Validate that a given (type, to) pair is internally consistent.
@@ -196,6 +290,13 @@ export interface MailboxMessage {
196
290
  taskContext?: MailboxTaskContext | undefined;
197
291
  /** Session id of the sender. Enables cross-session communication. */
198
292
  senderSessionId?: string | undefined;
293
+ /**
294
+ * Session-affinity token persisted on the message. When set, the
295
+ * recipient's leader filter uses it to drop the message if the
296
+ * recipient's current session id does not match. See
297
+ * {@link MailboxSessionAffinity} for the contract.
298
+ */
299
+ sessionAffinity?: MailboxSessionAffinity | undefined;
199
300
  /**
200
301
  * ISO8601 — when the message expires. Set at send time from `ttlMs`
201
302
  * (default: 24h via AUTO_COMPACT_DEFAULT_TTL_MS). The auto-compaction
@@ -352,6 +453,25 @@ export interface MailboxSendInput {
352
453
  taskContext?: MailboxTaskContext | undefined;
353
454
  /** Sender session id. Required when `to` is the `"@session"` alias. */
354
455
  senderSessionId?: string | undefined;
456
+ /**
457
+ * Session-affinity token. When set, the recipient's mailbox filter uses it
458
+ * to drop the message if the recipient's current session id does not match.
459
+ *
460
+ * Why two fields instead of one: a chimera report is generated by a reviewer
461
+ * subagent that runs inside the originating session (so `senderSessionId`
462
+ * is correct), but a mailbox envelope may also be relayed by an outer
463
+ * pipeline (auto-review cascade, broadcast wrapper) where the recipient
464
+ * needs to know which session the report *belongs to*, not who *physically
465
+ * called send()*. `sessionId` carries the originating session;
466
+ * `reportId` carries the chimera report id so the leader's filter
467
+ * can also look the report up in the persisted review-report store.
468
+ *
469
+ * A session-affinity token is **required** for any `chimera.*` review
470
+ * delivery wrapper — this is the trust boundary that prevents a leader
471
+ * from acting on reports emitted by another session, even when those
472
+ * reports land in its mailbox.
473
+ */
474
+ sessionAffinity?: MailboxSessionAffinity | undefined;
355
475
  /**
356
476
  * Time-to-live in milliseconds. When set, the message's `expiresAt` is
357
477
  * computed as `now + ttlMs` at send time. The auto-compaction sweep
@@ -359,6 +479,41 @@ export interface MailboxSendInput {
359
479
  */
360
480
  ttlMs?: number | undefined;
361
481
  }
482
+ /**
483
+ * Session-affinity token carried on a mailbox message.
484
+ *
485
+ * The token is the *only* signal a recipient's leader filter uses to decide
486
+ * whether a chimera report belongs to the recipient's own session. Without
487
+ * it, any leader that polls the project-wide mailbox would see — and feel
488
+ * compelled to act on — chimera reports emitted by every other session in
489
+ * the project.
490
+ */
491
+ export type MailboxSessionAffinity = MailboxScopedSessionAffinity | MailboxLegacyReportSessionAffinity;
492
+ export interface MailboxScopedSessionAffinity {
493
+ /**
494
+ * The originating session id. The recipient's leader filter compares this
495
+ * against its own current session id and drops the message on mismatch.
496
+ */
497
+ sessionId: string;
498
+ /**
499
+ * Originating chimera report id (UUID). Used as a secondary lookup only
500
+ * when the message is a legacy report token without `sessionId`.
501
+ */
502
+ reportId?: string | undefined;
503
+ /** Free-form tag for filtering / metrics, e.g. `'chimera.review'`. */
504
+ kind?: string | undefined;
505
+ }
506
+ export interface MailboxLegacyReportSessionAffinity {
507
+ /**
508
+ * Legacy report-only tokens deliberately omit `sessionId`; any present
509
+ * session id is authoritative and must be checked before report lookup.
510
+ */
511
+ sessionId?: undefined;
512
+ /** Originating chimera report id (UUID) used for persisted-session lookup. */
513
+ reportId: string;
514
+ /** Free-form tag for filtering / metrics, e.g. `'chimera.review'`. */
515
+ kind?: string | undefined;
516
+ }
362
517
  /**
363
518
  * Append-only ack record stored in the JSONL alongside messages.
364
519
  *
@@ -17,6 +17,7 @@
17
17
  * truth both that command and the spawn path use to validate + resolve keys.
18
18
  */
19
19
  import type { Config, ModelMatrixEntry } from '../types/config.js';
20
+ import type { ProviderModelStatusTracker } from './provider-status-tracker.js';
20
21
  /** Either a static matrix or a live getter (re-read on every spawn). */
21
22
  export type ModelMatrixSource = Record<string, ModelMatrixEntry> | (() => Record<string, ModelMatrixEntry> | undefined);
22
23
  /** All valid phase keys, in catalog order. */
@@ -81,9 +82,24 @@ export declare function roleNeedsIndependentReviewModel(role: string | undefined
81
82
  * implementation model.
82
83
  * 3. A different configured provider/model for review roles.
83
84
  * 4. The matrix/leader fallback.
85
+ *
86
+ * When a {@link ProviderModelStatusTracker} is supplied, resolved candidates
87
+ * currently in the waiting room (`state: 'blocked'`) are dropped so the
88
+ * subagent never spawns on a model the leader just 429-stricken. The
89
+ * fallback extension inside the subagent will also re-check `isAvailable`
90
+ * before invoking the provider, but filtering here saves the spawn
91
+ * round-trip on doomed picks.
92
+ *
93
+ * The tracker check uses the EFFECTIVE (provider, model) pair — i.e. the
94
+ * pair that {@link materializeTarget} would render — not the raw
95
+ * `matrixTarget.provider` field. A model-only matrix entry falls back to
96
+ * `config.provider` for the wire call, so the waiting-room entry sits on
97
+ * the same effective identity. Checking the raw field would silently
98
+ * bypass the quarantine and respawn on a 429-stricken model.
84
99
  */
85
100
  export declare function resolveSubagentModelTarget(config: Config, role: string | undefined, opts?: {
86
101
  implementationTarget?: ModelReference | undefined;
102
+ statusTracker?: ProviderModelStatusTracker | undefined;
87
103
  }): ResolvedSubagentModelTarget | undefined;
88
104
  /**
89
105
  * Resolve the default implementation lane. The generic Executor is the closest