@wrongstack/core 0.310.1 → 0.313.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.
@@ -20988,7 +20988,7 @@ async function readDirectorSubagentSession(args) {
20988
20988
  }
20989
20989
 
20990
20990
  // src/types/quota-regex.ts
20991
- var QUOTA_EXHAUSTED_RE = /(?:insufficient|exhausted|depleted|exceeded|no|not enough)[-_\s]*(?:quota|credit|balance)|(?:quota|credit|balance)(?:\s+(?:has|have))?(?:\s+been)?[-_\s]*(?:exhausted|depleted|exceeded|insufficient)|billing[_\s-]*(?:hard[_\s-]*)?limit|payment required|spending limit|plan limit|usage[-_\s]*limit[-_\s]*(?:reached|exceeded)/i;
20991
+ var QUOTA_EXHAUSTED_RE = /(?:insufficient|exhausted|depleted|exceeded|no|not enough)[-_\s]*(?:quota|credit|balance)|(?:quota|credit|balance)(?:\s+(?:has|have))?(?:\s+been)?[-_\s]*(?:exhausted|depleted|exceeded|insufficient)|billing[_\s-]*(?:hard[_\s-]*)?limit|payment required|spending limit|plan limit|(?:usage|credit)[-_\s]*(?:quota|limit)(?![-_\w])|usage[-_\s]*(?:limit|quota)[-_\s]*(?:reached|exceeded|for)/i;
20992
20992
  var ROUTE_SCOPED_QUOTA_RE = /\b(?:for|on)\s+(?:(?:this|the)\s+)?(?:route|model)\b|\b(?:route|model)(?:[-_\s]+[\w.-]+)?[-_\s]*(?:quota|limit)\b|\b(?:quota|limit).{0,24}\b(?:for|on)\s+(?:(?:this|the)\s+)?(?:route|model)\b/i;
20993
20993
 
20994
20994
  // src/types/provider.ts
@@ -21001,7 +21001,8 @@ function classifyProviderError(status, body, message) {
21001
21001
  if (status === 0) return "network";
21002
21002
  if (status === 408) return "timeout";
21003
21003
  if (status === 599) return "stream_hang";
21004
- if (status === 402 || QUOTA_EXHAUSTED_RE.test(text)) return "quota_exhausted";
21004
+ if (QUOTA_EXHAUSTED_RE.test(text)) return "quota_exhausted";
21005
+ if (status === 402) return "quota_exhausted";
21005
21006
  if (status === 429 && body?.message && body.type !== "rate_limit_exceeded" && RATE_LIMIT_EXCEEDED_RE.test(body.message)) {
21006
21007
  return "quota_exhausted";
21007
21008
  }
@@ -67,8 +67,8 @@ var EventBus = class {
67
67
  *
68
68
  * Returns an unsubscribe function.
69
69
  */
70
- onAny(fn) {
71
- return this.onPattern("*", fn);
70
+ onAny(fn, owner) {
71
+ return this.onPattern("*", fn, owner);
72
72
  }
73
73
  /**
74
74
  * Subscribe to all events whose name matches a glob-style prefix.
@@ -81,16 +81,16 @@ var EventBus = class {
81
81
  *
82
82
  * Returns an unsubscribe function.
83
83
  */
84
- onPattern(pattern, fn) {
84
+ onPattern(pattern, fn, owner) {
85
85
  if (this.wildcards.length >= MAX_WILDCARDS) {
86
86
  this.logger?.error(
87
- `EventBus wildcard limit (${MAX_WILDCARDS}) reached \u2014 rejecting onPattern("${pattern}"). Callers must dispose their wildcard listeners to prevent unbounded growth.`
87
+ `EventBus wildcard limit (${MAX_WILDCARDS}) reached \u2014 rejecting onPattern("${pattern}")` + (owner ? ` (owner: ${owner})` : "") + ". Callers must dispose their wildcard listeners to prevent unbounded growth."
88
88
  );
89
89
  return () => {
90
90
  };
91
91
  }
92
92
  const match = makePatternMatcher(pattern);
93
- const entry = { match, fn };
93
+ const entry = { match, fn, owner };
94
94
  this.wildcards.push(entry);
95
95
  this.wildcardSnapshotCache = null;
96
96
  return () => {
@@ -108,15 +108,15 @@ var EventBus = class {
108
108
  *
109
109
  * Returns an unsubscribe function.
110
110
  */
111
- onRegex(regex, fn) {
111
+ onRegex(regex, fn, owner) {
112
112
  if (this.wildcards.length >= MAX_WILDCARDS) {
113
113
  this.logger?.error(
114
- `EventBus wildcard limit (${MAX_WILDCARDS}) reached \u2014 rejecting onRegex(${regex}). Callers must dispose their wildcard listeners to prevent unbounded growth.`
114
+ `EventBus wildcard limit (${MAX_WILDCARDS}) reached \u2014 rejecting onRegex(${regex})` + (owner ? ` (owner: ${owner})` : "") + ". Callers must dispose their wildcard listeners to prevent unbounded growth."
115
115
  );
116
116
  return () => {
117
117
  };
118
118
  }
119
- const entry = { match: (e) => regex.test(e), fn };
119
+ const entry = { match: (e) => regex.test(e), fn, owner };
120
120
  this.wildcards.push(entry);
121
121
  this.wildcardSnapshotCache = null;
122
122
  return () => {
@@ -1416,6 +1416,38 @@ function isMailboxReceiptRecordV2(value) {
1416
1416
  return true;
1417
1417
  }
1418
1418
 
1419
+ // src/coordination/mailbox-session-sync.ts
1420
+ function isAffectedBySessionAffinity(message) {
1421
+ return message.sessionAffinity !== void 0;
1422
+ }
1423
+ async function acceptMailboxMessageForSession(message, currentSessionId, ctx) {
1424
+ if (!isAffectedBySessionAffinity(message)) return true;
1425
+ const affinity = message.sessionAffinity;
1426
+ if (affinity === null || typeof affinity !== "object" || Array.isArray(affinity)) {
1427
+ return false;
1428
+ }
1429
+ if (affinity.sessionId !== void 0 && typeof affinity.sessionId !== "string" || affinity.reportId !== void 0 && typeof affinity.reportId !== "string") {
1430
+ return false;
1431
+ }
1432
+ if (!currentSessionId) {
1433
+ return ctx?.allowUnscoped === true;
1434
+ }
1435
+ if (typeof affinity.sessionId === "string" && affinity.sessionId.length > 0) {
1436
+ if (affinity.sessionId !== currentSessionId) return false;
1437
+ return true;
1438
+ }
1439
+ if (affinity.reportId && ctx?.resolveChimeraReportSessionId) {
1440
+ try {
1441
+ const resolved = await ctx.resolveChimeraReportSessionId(affinity.reportId);
1442
+ if (resolved === currentSessionId) return true;
1443
+ if (resolved !== void 0) return false;
1444
+ } catch {
1445
+ }
1446
+ }
1447
+ if (ctx?.allowUnscoped === true) return true;
1448
+ return false;
1449
+ }
1450
+
1419
1451
  // src/coordination/mailbox-message-codec.ts
1420
1452
  var MESSAGE_TYPES = /* @__PURE__ */ new Set([
1421
1453
  "note",
@@ -2772,7 +2804,7 @@ var SqliteMailbox = class {
2772
2804
  }
2773
2805
  const rows = this.stmt(sql).all(...params);
2774
2806
  const idFilter = query.ids === void 0 ? void 0 : new Set(query.ids);
2775
- const messages = this.materializeMessageRows(rows).filter((message) => {
2807
+ const filtered = this.materializeMessageRows(rows).filter((message) => {
2776
2808
  if (idFilter !== void 0 && !idFilter.has(message.id)) return false;
2777
2809
  if (query.to !== void 0 && message.to !== query.to && message.to !== "*") return false;
2778
2810
  if (query.from !== void 0 && message.from !== query.from) return false;
@@ -2791,6 +2823,20 @@ var SqliteMailbox = class {
2791
2823
  if (query.replyTo !== void 0 && message.replyTo !== query.replyTo) return false;
2792
2824
  return true;
2793
2825
  });
2826
+ let messages = filtered;
2827
+ if (query.currentSessionId !== void 0 || query.sessionAffinityCtx !== void 0) {
2828
+ const kept = [];
2829
+ for (const message of filtered) {
2830
+ if (await acceptMailboxMessageForSession(
2831
+ message,
2832
+ query.currentSessionId,
2833
+ query.sessionAffinityCtx
2834
+ )) {
2835
+ kept.push(message);
2836
+ }
2837
+ }
2838
+ messages = kept;
2839
+ }
2794
2840
  messages.sort((left, right) => right.timestamp.localeCompare(left.timestamp));
2795
2841
  return messages.slice(0, query.limit ?? 50).map((message) => {
2796
2842
  const copy = {
@@ -2903,7 +2949,7 @@ var SqliteMailbox = class {
2903
2949
  * identity is `leader`. This call path carries no role, matching the
2904
2950
  * `isMailboxMessageVisibleTo(message, forAgentId)` it replaces.
2905
2951
  */
2906
- async unreadCount(forAgentId, sessionId) {
2952
+ async unreadCount(forAgentId, sessionId, ctx) {
2907
2953
  const sessionAddress = sessionId === void 0 ? void 0 : sessionRecipient(sessionId);
2908
2954
  const where = [];
2909
2955
  const params = [];
@@ -2944,10 +2990,20 @@ var SqliteMailbox = class {
2944
2990
  WHERE any_receipt.message_id = messages.id
2945
2991
  )
2946
2992
  )`);
2947
- const row = this.stmt(
2948
- `SELECT COUNT(*) AS total FROM messages WHERE ${where.join(" AND ")}`
2949
- ).get(...params);
2950
- return Number(row?.total ?? 0);
2993
+ if (sessionId === void 0 && ctx === void 0) {
2994
+ const row = this.stmt(
2995
+ `SELECT COUNT(*) AS total FROM messages WHERE ${where.join(" AND ")}`
2996
+ ).get(...params);
2997
+ return Number(row?.total ?? 0);
2998
+ }
2999
+ const rows = this.stmt(
3000
+ `SELECT id, data, legacy_global_completion FROM messages WHERE ${where.join(" AND ")}`
3001
+ ).all(...params);
3002
+ let total = 0;
3003
+ for (const message of this.materializeMessageRows(rows)) {
3004
+ if (await acceptMailboxMessageForSession(message, sessionId, ctx)) total += 1;
3005
+ }
3006
+ return total;
2951
3007
  }
2952
3008
  async softDelete(mailId, by) {
2953
3009
  const message = this.findMessage(mailId);
@@ -3,6 +3,7 @@ import type { MailboxAudience, MailboxMessage, MailboxSessionAffinity, MailboxTa
3
3
  export { MAILBOX_TYPE_PROPERTIES, type MailboxMessageType, type MailboxTypeCategory, } from './mailbox-type-properties.js';
4
4
  export { expandMailboxCapabilities, hasMailboxCapability, MAILBOX_CAPABILITY_IMPLICATIONS, type MailboxActorContext, type MailboxAuthMode, type MailboxCapability, type MailboxPrincipalKind, } from './mailbox-auth-types.js';
5
5
  export { isActionRequiredForActor, isMailboxLeader, isMailboxMessageVisibleTo, isMailboxReceiptRecordV2, isMailboxSenderInFamily, mailboxIdentityBase, normalizeRecipient, SESSION_RECIPIENT_PREFIX, sessionRecipient, validateSendType, } from './mailbox-predicates.js';
6
+ import type { MailboxSessionAffinityContext } from './mailbox-session-sync.js';
6
7
  export { acceptMailboxMessageForSession, acceptMailboxMessageForSessionSync, type MailboxSessionAffinityContext, } from './mailbox-session-sync.js';
7
8
  export { type ActorMailboxMessage, type MailboxAudience, type MailboxLegacyReportSessionAffinity, type MailboxMessage, type MailboxReceiptRecordV2, type MailboxScopedSessionAffinity, type MailboxSessionAffinity, type MailboxTaskContext, type ReadReceipts, } from './mailbox-message-types.js';
8
9
  export interface RegisteredAgent {
@@ -48,7 +49,18 @@ export interface MailboxQuery {
48
49
  minPriority?: 'low' | 'normal' | 'high' | undefined;
49
50
  limit?: number | undefined;
50
51
  since?: string | undefined;
52
+ /** Filters by the message's SENDER session id (origin), not the reader's session. */
51
53
  sessionId?: string | undefined;
54
+ /**
55
+ * Reader's current session id. When present, messages carrying a
56
+ * session-affinity token for a DIFFERENT session are excluded, matching the
57
+ * inbox checker's receive-side filter (mailbox-attach applySessionAffinityFilter).
58
+ * Kept distinct from `sessionId` (sender origin) so a reader can filter by
59
+ * both simultaneously.
60
+ */
61
+ currentSessionId?: string | undefined;
62
+ /** Resolver + unscoped policy for session-affinity tokens (see acceptMailboxMessageForSession). */
63
+ sessionAffinityCtx?: MailboxSessionAffinityContext | undefined;
52
64
  includeDeleted?: boolean | undefined;
53
65
  replyTo?: string | undefined;
54
66
  }
@@ -172,7 +184,7 @@ export interface Mailbox {
172
184
  registerAgent(input: AgentRegistrationInput): Promise<void>;
173
185
  deregisterAgent(agentId: string): Promise<void>;
174
186
  heartbeat(input: AgentHeartbeatInput): Promise<void>;
175
- unreadCount(forAgentId: string, sessionId?: string): Promise<number>;
187
+ unreadCount(forAgentId: string, sessionId?: string, ctx?: MailboxSessionAffinityContext): Promise<number>;
176
188
  close(): Promise<void>;
177
189
  clearAll(): Promise<void>;
178
190
  purgeStale(opts?: PurgeOptions): Promise<PurgeResult>;
@@ -2,6 +2,7 @@ import type { EventBus } from '../kernel/events.js';
2
2
  import type { CredentialValidation, IssueCredentialOptions, MailboxCredential } from './mailbox-credential-store.js';
3
3
  import type { MailboxEventEmitter } from './mailbox-events.js';
4
4
  import type { AgentHeartbeatInput, AgentRegistrationInput, AutoCompactOptions, AutoCompactResult, ClientHeartbeatInput, ClientRegistrationInput, ClientStatus, Mailbox, MailboxAckBatchInput, MailboxAckInput, MailboxAgentStatus, MailboxMessage, MailboxQuery, MailboxSendInput, PurgeOptions, PurgeResult } from './mailbox-types.js';
5
+ import { type MailboxSessionAffinityContext } from './mailbox-types.js';
5
6
  export declare const SQLITE_MAILBOX_FILE = "_mailbox.sqlite";
6
7
  export declare class SqliteMailbox implements Mailbox {
7
8
  readonly projectDir: string;
@@ -59,7 +60,7 @@ export declare class SqliteMailbox implements Mailbox {
59
60
  * identity is `leader`. This call path carries no role, matching the
60
61
  * `isMailboxMessageVisibleTo(message, forAgentId)` it replaces.
61
62
  */
62
- unreadCount(forAgentId: string, sessionId?: string): Promise<number>;
63
+ unreadCount(forAgentId: string, sessionId?: string, ctx?: MailboxSessionAffinityContext): Promise<number>;
63
64
  softDelete(mailId: string, by: string): Promise<MailboxMessage | null>;
64
65
  restore(mailId: string): Promise<MailboxMessage | null>;
65
66
  private persistAgent;
@@ -3947,7 +3947,7 @@ function truncate(s, max) {
3947
3947
  }
3948
3948
 
3949
3949
  // src/types/quota-regex.ts
3950
- var QUOTA_EXHAUSTED_RE = /(?:insufficient|exhausted|depleted|exceeded|no|not enough)[-_\s]*(?:quota|credit|balance)|(?:quota|credit|balance)(?:\s+(?:has|have))?(?:\s+been)?[-_\s]*(?:exhausted|depleted|exceeded|insufficient)|billing[_\s-]*(?:hard[_\s-]*)?limit|payment required|spending limit|plan limit|usage[-_\s]*limit[-_\s]*(?:reached|exceeded)/i;
3950
+ var QUOTA_EXHAUSTED_RE = /(?:insufficient|exhausted|depleted|exceeded|no|not enough)[-_\s]*(?:quota|credit|balance)|(?:quota|credit|balance)(?:\s+(?:has|have))?(?:\s+been)?[-_\s]*(?:exhausted|depleted|exceeded|insufficient)|billing[_\s-]*(?:hard[_\s-]*)?limit|payment required|spending limit|plan limit|(?:usage|credit)[-_\s]*(?:quota|limit)(?![-_\w])|usage[-_\s]*(?:limit|quota)[-_\s]*(?:reached|exceeded|for)/i;
3951
3951
 
3952
3952
  // src/types/provider.ts
3953
3953
  function effectiveInputTokens(usage) {
@@ -3962,7 +3962,8 @@ function classifyProviderError(status, body, message) {
3962
3962
  if (status === 0) return "network";
3963
3963
  if (status === 408) return "timeout";
3964
3964
  if (status === 599) return "stream_hang";
3965
- if (status === 402 || QUOTA_EXHAUSTED_RE.test(text)) return "quota_exhausted";
3965
+ if (QUOTA_EXHAUSTED_RE.test(text)) return "quota_exhausted";
3966
+ if (status === 402) return "quota_exhausted";
3966
3967
  if (status === 429 && body?.message && body.type !== "rate_limit_exceeded" && RATE_LIMIT_EXCEEDED_RE.test(body.message)) {
3967
3968
  return "quota_exhausted";
3968
3969
  }
@@ -4854,7 +4854,7 @@ function truncate(s, max) {
4854
4854
  }
4855
4855
 
4856
4856
  // src/types/quota-regex.ts
4857
- var QUOTA_EXHAUSTED_RE = /(?:insufficient|exhausted|depleted|exceeded|no|not enough)[-_\s]*(?:quota|credit|balance)|(?:quota|credit|balance)(?:\s+(?:has|have))?(?:\s+been)?[-_\s]*(?:exhausted|depleted|exceeded|insufficient)|billing[_\s-]*(?:hard[_\s-]*)?limit|payment required|spending limit|plan limit|usage[-_\s]*limit[-_\s]*(?:reached|exceeded)/i;
4857
+ var QUOTA_EXHAUSTED_RE = /(?:insufficient|exhausted|depleted|exceeded|no|not enough)[-_\s]*(?:quota|credit|balance)|(?:quota|credit|balance)(?:\s+(?:has|have))?(?:\s+been)?[-_\s]*(?:exhausted|depleted|exceeded|insufficient)|billing[_\s-]*(?:hard[_\s-]*)?limit|payment required|spending limit|plan limit|(?:usage|credit)[-_\s]*(?:quota|limit)(?![-_\w])|usage[-_\s]*(?:limit|quota)[-_\s]*(?:reached|exceeded|for)/i;
4858
4858
 
4859
4859
  // src/types/provider.ts
4860
4860
  var CONTEXT_OVERFLOW_RE = /context.length|context.window|maximum context|max.*tokens?.*exceeded|(?:prompt|request|input|messages?).{0,12}too (?:large|long)|exceeds the context|\btokens\b.*exceed|too many tokens|reduce the length|resulted in \d+ tokens|context_length_exceeded/i;
@@ -4866,7 +4866,8 @@ function classifyProviderError(status, body, message) {
4866
4866
  if (status === 0) return "network";
4867
4867
  if (status === 408) return "timeout";
4868
4868
  if (status === 599) return "stream_hang";
4869
- if (status === 402 || QUOTA_EXHAUSTED_RE.test(text)) return "quota_exhausted";
4869
+ if (QUOTA_EXHAUSTED_RE.test(text)) return "quota_exhausted";
4870
+ if (status === 402) return "quota_exhausted";
4870
4871
  if (status === 429 && body?.message && body.type !== "rate_limit_exceeded" && RATE_LIMIT_EXCEEDED_RE.test(body.message)) {
4871
4872
  return "quota_exhausted";
4872
4873
  }
package/dist/index.js CHANGED
@@ -38421,7 +38421,7 @@ import { randomUUID as randomUUID18 } from "node:crypto";
38421
38421
  init_errors();
38422
38422
 
38423
38423
  // src/types/quota-regex.ts
38424
- var QUOTA_EXHAUSTED_RE = /(?:insufficient|exhausted|depleted|exceeded|no|not enough)[-_\s]*(?:quota|credit|balance)|(?:quota|credit|balance)(?:\s+(?:has|have))?(?:\s+been)?[-_\s]*(?:exhausted|depleted|exceeded|insufficient)|billing[_\s-]*(?:hard[_\s-]*)?limit|payment required|spending limit|plan limit|usage[-_\s]*limit[-_\s]*(?:reached|exceeded)/i;
38424
+ var QUOTA_EXHAUSTED_RE = /(?:insufficient|exhausted|depleted|exceeded|no|not enough)[-_\s]*(?:quota|credit|balance)|(?:quota|credit|balance)(?:\s+(?:has|have))?(?:\s+been)?[-_\s]*(?:exhausted|depleted|exceeded|insufficient)|billing[_\s-]*(?:hard[_\s-]*)?limit|payment required|spending limit|plan limit|(?:usage|credit)[-_\s]*(?:quota|limit)(?![-_\w])|usage[-_\s]*(?:limit|quota)[-_\s]*(?:reached|exceeded|for)/i;
38425
38425
  var ROUTE_SCOPED_QUOTA_RE = /\b(?:for|on)\s+(?:(?:this|the)\s+)?(?:route|model)\b|\b(?:route|model)(?:[-_\s]+[\w.-]+)?[-_\s]*(?:quota|limit)\b|\b(?:quota|limit).{0,24}\b(?:for|on)\s+(?:(?:this|the)\s+)?(?:route|model)\b/i;
38426
38426
 
38427
38427
  // src/types/provider.ts
@@ -38458,7 +38458,8 @@ function classifyProviderError(status, body, message) {
38458
38458
  if (status === 0) return "network";
38459
38459
  if (status === 408) return "timeout";
38460
38460
  if (status === 599) return "stream_hang";
38461
- if (status === 402 || QUOTA_EXHAUSTED_RE.test(text2)) return "quota_exhausted";
38461
+ if (QUOTA_EXHAUSTED_RE.test(text2)) return "quota_exhausted";
38462
+ if (status === 402) return "quota_exhausted";
38462
38463
  if (status === 429 && body?.message && body.type !== "rate_limit_exceeded" && RATE_LIMIT_EXCEEDED_RE.test(body.message)) {
38463
38464
  return "quota_exhausted";
38464
38465
  }
@@ -73361,8 +73362,8 @@ var EventBus = class {
73361
73362
  *
73362
73363
  * Returns an unsubscribe function.
73363
73364
  */
73364
- onAny(fn) {
73365
- return this.onPattern("*", fn);
73365
+ onAny(fn, owner) {
73366
+ return this.onPattern("*", fn, owner);
73366
73367
  }
73367
73368
  /**
73368
73369
  * Subscribe to all events whose name matches a glob-style prefix.
@@ -73375,16 +73376,16 @@ var EventBus = class {
73375
73376
  *
73376
73377
  * Returns an unsubscribe function.
73377
73378
  */
73378
- onPattern(pattern, fn) {
73379
+ onPattern(pattern, fn, owner) {
73379
73380
  if (this.wildcards.length >= MAX_WILDCARDS) {
73380
73381
  this.logger?.error(
73381
- `EventBus wildcard limit (${MAX_WILDCARDS}) reached \u2014 rejecting onPattern("${pattern}"). Callers must dispose their wildcard listeners to prevent unbounded growth.`
73382
+ `EventBus wildcard limit (${MAX_WILDCARDS}) reached \u2014 rejecting onPattern("${pattern}")` + (owner ? ` (owner: ${owner})` : "") + ". Callers must dispose their wildcard listeners to prevent unbounded growth."
73382
73383
  );
73383
73384
  return () => {
73384
73385
  };
73385
73386
  }
73386
73387
  const match = makePatternMatcher(pattern);
73387
- const entry = { match, fn };
73388
+ const entry = { match, fn, owner };
73388
73389
  this.wildcards.push(entry);
73389
73390
  this.wildcardSnapshotCache = null;
73390
73391
  return () => {
@@ -73402,15 +73403,15 @@ var EventBus = class {
73402
73403
  *
73403
73404
  * Returns an unsubscribe function.
73404
73405
  */
73405
- onRegex(regex, fn) {
73406
+ onRegex(regex, fn, owner) {
73406
73407
  if (this.wildcards.length >= MAX_WILDCARDS) {
73407
73408
  this.logger?.error(
73408
- `EventBus wildcard limit (${MAX_WILDCARDS}) reached \u2014 rejecting onRegex(${regex}). Callers must dispose their wildcard listeners to prevent unbounded growth.`
73409
+ `EventBus wildcard limit (${MAX_WILDCARDS}) reached \u2014 rejecting onRegex(${regex})` + (owner ? ` (owner: ${owner})` : "") + ". Callers must dispose their wildcard listeners to prevent unbounded growth."
73409
73410
  );
73410
73411
  return () => {
73411
73412
  };
73412
73413
  }
73413
- const entry = { match: (e) => regex.test(e), fn };
73414
+ const entry = { match: (e) => regex.test(e), fn, owner };
73414
73415
  this.wildcards.push(entry);
73415
73416
  this.wildcardSnapshotCache = null;
73416
73417
  return () => {
@@ -73588,16 +73589,16 @@ var ScopedEventBus = class extends EventBus {
73588
73589
  * Subscribe to all events. Alias for `onPattern('*')` — the listener is
73589
73590
  * tracked so that `teardown()` will remove it automatically.
73590
73591
  */
73591
- onAny(fn) {
73592
+ onAny(fn, owner) {
73592
73593
  if (this.wildcards.length >= MAX_WILDCARDS) {
73593
73594
  this.logger?.error(
73594
- `EventBus wildcard limit (${MAX_WILDCARDS}) reached \u2014 rejecting onAny(). Callers must dispose their wildcard listeners to prevent unbounded growth.`
73595
+ `EventBus wildcard limit (${MAX_WILDCARDS}) reached \u2014 rejecting onAny()` + (owner ? ` (owner: ${owner})` : "") + ". Callers must dispose their wildcard listeners to prevent unbounded growth."
73595
73596
  );
73596
73597
  return () => {
73597
73598
  };
73598
73599
  }
73599
73600
  const key = this.nextKey++;
73600
- const unsub = EventBus.prototype.onPattern.call(this, "*", fn);
73601
+ const unsub = EventBus.prototype.onPattern.call(this, "*", fn, owner);
73601
73602
  this.registrations.set(key, unsub);
73602
73603
  return () => {
73603
73604
  this.registrations.delete(key);
@@ -73608,16 +73609,16 @@ var ScopedEventBus = class extends EventBus {
73608
73609
  * Identical to `EventBus.onPattern` but the listener is tracked so that
73609
73610
  * `teardown()` will remove it automatically.
73610
73611
  */
73611
- onPattern(pattern, fn) {
73612
+ onPattern(pattern, fn, owner) {
73612
73613
  if (this.wildcards.length >= MAX_WILDCARDS) {
73613
73614
  this.logger?.error(
73614
- `EventBus wildcard limit (${MAX_WILDCARDS}) reached \u2014 rejecting onPattern("${pattern}"). Callers must dispose their wildcard listeners to prevent unbounded growth.`
73615
+ `EventBus wildcard limit (${MAX_WILDCARDS}) reached \u2014 rejecting onPattern("${pattern}")` + (owner ? ` (owner: ${owner})` : "") + ". Callers must dispose their wildcard listeners to prevent unbounded growth."
73615
73616
  );
73616
73617
  return () => {
73617
73618
  };
73618
73619
  }
73619
73620
  const key = this.nextKey++;
73620
- const unsub = super.onPattern(pattern, fn);
73621
+ const unsub = super.onPattern(pattern, fn, owner);
73621
73622
  this.registrations.set(key, unsub);
73622
73623
  return () => {
73623
73624
  this.registrations.delete(key);
@@ -73628,16 +73629,16 @@ var ScopedEventBus = class extends EventBus {
73628
73629
  * Identical to `EventBus.onRegex` but the listener is tracked so that
73629
73630
  * `teardown()` will remove it automatically.
73630
73631
  */
73631
- onRegex(regex, fn) {
73632
+ onRegex(regex, fn, owner) {
73632
73633
  if (this.wildcards.length >= MAX_WILDCARDS) {
73633
73634
  this.logger?.error(
73634
- `EventBus wildcard limit (${MAX_WILDCARDS}) reached \u2014 rejecting onRegex(${regex}). Callers must dispose their wildcard listeners to prevent unbounded growth.`
73635
+ `EventBus wildcard limit (${MAX_WILDCARDS}) reached \u2014 rejecting onRegex(${regex})` + (owner ? ` (owner: ${owner})` : "") + ". Callers must dispose their wildcard listeners to prevent unbounded growth."
73635
73636
  );
73636
73637
  return () => {
73637
73638
  };
73638
73639
  }
73639
73640
  const key = this.nextKey++;
73640
- const unsub = super.onRegex(regex, fn);
73641
+ const unsub = super.onRegex(regex, fn, owner);
73641
73642
  this.registrations.set(key, unsub);
73642
73643
  return () => {
73643
73644
  this.registrations.delete(key);
@@ -0,0 +1,26 @@
1
+ /**
2
+ * WrongTrace gate-decision events.
3
+ *
4
+ * Emitted by the WrongTrace lock-gate hooks (see
5
+ * `@wrongstack/wrongtrace` hooks.ts) through each host's EventBus:
6
+ * every mutating tool call's gate decision is observable — denied edits,
7
+ * fragile-file nudges, lock acquisition / race-loss / release.
8
+ *
9
+ * Kept structurally matching the adapter's `WrongTraceGateDecisionEvent`
10
+ * union so core never imports the adapter (direction: wrongtrace is the
11
+ * leaf; core must not depend on it).
12
+ */
13
+ export interface WrongTraceEventMap {
14
+ 'wrongtrace.gate.decision': {
15
+ kind: 'deny' | 'allow-fragile' | 'lock-acquired' | 'lock-conflict-race' | 'lock-released';
16
+ /** Target file path the gate decided on. */
17
+ path: string;
18
+ /** `deny` — human-readable owner/expiry reason shown to the model. */
19
+ reason?: string | undefined;
20
+ /** `allow-fragile` — why the file is considered fragile. */
21
+ reasons?: readonly string[] | undefined;
22
+ /** `lock-acquired` — lock owner identity (`wrongstack:<sessionId>`). */
23
+ owner?: string | undefined;
24
+ };
25
+ }
26
+ //# sourceMappingURL=wrongtrace-events.d.ts.map
@@ -14,6 +14,7 @@ import type { SddEventMap } from './events/sdd-events.js';
14
14
  import type { SessionEventMap } from './events/session-events.js';
15
15
  import type { ToolEventMap } from './events/tool-events.js';
16
16
  import type { WorktreeEventMap } from './events/worktree-events.js';
17
+ import type { WrongTraceEventMap } from './events/wrongtrace-events.js';
17
18
  /** Distress signals the BrainMonitor watches. See `coordination/brain-monitor.ts`. */
18
19
  export type BrainInterventionKind = 'tool_failure_streak' | 'error_storm' | 'agent_stall' | 'file_churn';
19
20
  /**
@@ -48,7 +49,7 @@ export interface TrackedAgentSnapshot {
48
49
  latestPromptAt?: number | undefined;
49
50
  lastActivityAt: string;
50
51
  }
51
- export interface EventMap extends AgentEventMap, BrainEventMap, SessionEventMap, ProviderEventMap, ProcessEventMap, NetworkEventMap, FileEventMap, ToolEventMap, MemoryEventMap, SddEventMap, WorktreeEventMap, FleetEventMap {
52
+ export interface EventMap extends AgentEventMap, BrainEventMap, SessionEventMap, ProviderEventMap, ProcessEventMap, NetworkEventMap, FileEventMap, ToolEventMap, MemoryEventMap, SddEventMap, WorktreeEventMap, FleetEventMap, WrongTraceEventMap {
52
53
  }
53
54
  export type EventName = keyof EventMap;
54
55
  export type Listener<E extends EventName> = (payload: EventMap[E]) => void;
@@ -60,6 +61,8 @@ export declare class EventBus {
60
61
  protected readonly wildcards: Array<{
61
62
  match: (event: string) => boolean;
62
63
  fn: (event: string, payload: unknown) => void;
64
+ /** Optional registration-site label included in cap-rejection warnings. */
65
+ owner?: string | undefined;
63
66
  }>;
64
67
  protected logger?: EventLogger | undefined;
65
68
  /**
@@ -82,7 +85,9 @@ export declare class EventBus {
82
85
  *
83
86
  * Returns an unsubscribe function.
84
87
  */
85
- onAny(fn: (event: string, payload: unknown) => void): () => void;
88
+ onAny(fn: (event: string, payload: unknown) => void,
89
+ /** Optional registration-site label included in cap-rejection warnings. */
90
+ owner?: string): () => void;
86
91
  /**
87
92
  * Subscribe to all events whose name matches a glob-style prefix.
88
93
  * `'tool.*'` matches `tool.started`, `tool.executed`, `tool.progress`, etc.
@@ -94,7 +99,9 @@ export declare class EventBus {
94
99
  *
95
100
  * Returns an unsubscribe function.
96
101
  */
97
- onPattern(pattern: string, fn: (event: string, payload: unknown) => void): () => void;
102
+ onPattern(pattern: string, fn: (event: string, payload: unknown) => void,
103
+ /** Optional registration-site label included in cap-rejection warnings. */
104
+ owner?: string): () => void;
98
105
  /**
99
106
  * Subscribe to all events whose name matches a RegExp.
100
107
  * More flexible than `onPattern` — use when you need regex features
@@ -102,7 +109,9 @@ export declare class EventBus {
102
109
  *
103
110
  * Returns an unsubscribe function.
104
111
  */
105
- onRegex(regex: RegExp, fn: (event: string, payload: unknown) => void): () => void;
112
+ onRegex(regex: RegExp, fn: (event: string, payload: unknown) => void,
113
+ /** Optional registration-site label included in cap-rejection warnings. */
114
+ owner?: string): () => void;
106
115
  emit<E extends EventName>(event: E, payload: EventMap[E]): void;
107
116
  /**
108
117
  * Dispatch array for one event name, or `undefined` when nothing is
@@ -208,17 +217,17 @@ export declare class ScopedEventBus extends EventBus {
208
217
  * Subscribe to all events. Alias for `onPattern('*')` — the listener is
209
218
  * tracked so that `teardown()` will remove it automatically.
210
219
  */
211
- onAny(fn: (event: string, payload: unknown) => void): () => void;
220
+ onAny(fn: (event: string, payload: unknown) => void, owner?: string): () => void;
212
221
  /**
213
222
  * Identical to `EventBus.onPattern` but the listener is tracked so that
214
223
  * `teardown()` will remove it automatically.
215
224
  */
216
- onPattern(pattern: string, fn: (event: string, payload: unknown) => void): () => void;
225
+ onPattern(pattern: string, fn: (event: string, payload: unknown) => void, owner?: string): () => void;
217
226
  /**
218
227
  * Identical to `EventBus.onRegex` but the listener is tracked so that
219
228
  * `teardown()` will remove it automatically.
220
229
  */
221
- onRegex(regex: RegExp, fn: (event: string, payload: unknown) => void): () => void;
230
+ onRegex(regex: RegExp, fn: (event: string, payload: unknown) => void, owner?: string): () => void;
222
231
  /**
223
232
  * Remove every listener that was registered through this scoped bus.
224
233
  * Idempotent — calling it multiple times is safe.
@@ -499,8 +499,8 @@ var EventBus = class {
499
499
  *
500
500
  * Returns an unsubscribe function.
501
501
  */
502
- onAny(fn) {
503
- return this.onPattern("*", fn);
502
+ onAny(fn, owner) {
503
+ return this.onPattern("*", fn, owner);
504
504
  }
505
505
  /**
506
506
  * Subscribe to all events whose name matches a glob-style prefix.
@@ -513,16 +513,16 @@ var EventBus = class {
513
513
  *
514
514
  * Returns an unsubscribe function.
515
515
  */
516
- onPattern(pattern, fn) {
516
+ onPattern(pattern, fn, owner) {
517
517
  if (this.wildcards.length >= MAX_WILDCARDS) {
518
518
  this.logger?.error(
519
- `EventBus wildcard limit (${MAX_WILDCARDS}) reached \u2014 rejecting onPattern("${pattern}"). Callers must dispose their wildcard listeners to prevent unbounded growth.`
519
+ `EventBus wildcard limit (${MAX_WILDCARDS}) reached \u2014 rejecting onPattern("${pattern}")` + (owner ? ` (owner: ${owner})` : "") + ". Callers must dispose their wildcard listeners to prevent unbounded growth."
520
520
  );
521
521
  return () => {
522
522
  };
523
523
  }
524
524
  const match = makePatternMatcher(pattern);
525
- const entry = { match, fn };
525
+ const entry = { match, fn, owner };
526
526
  this.wildcards.push(entry);
527
527
  this.wildcardSnapshotCache = null;
528
528
  return () => {
@@ -540,15 +540,15 @@ var EventBus = class {
540
540
  *
541
541
  * Returns an unsubscribe function.
542
542
  */
543
- onRegex(regex, fn) {
543
+ onRegex(regex, fn, owner) {
544
544
  if (this.wildcards.length >= MAX_WILDCARDS) {
545
545
  this.logger?.error(
546
- `EventBus wildcard limit (${MAX_WILDCARDS}) reached \u2014 rejecting onRegex(${regex}). Callers must dispose their wildcard listeners to prevent unbounded growth.`
546
+ `EventBus wildcard limit (${MAX_WILDCARDS}) reached \u2014 rejecting onRegex(${regex})` + (owner ? ` (owner: ${owner})` : "") + ". Callers must dispose their wildcard listeners to prevent unbounded growth."
547
547
  );
548
548
  return () => {
549
549
  };
550
550
  }
551
- const entry = { match: (e) => regex.test(e), fn };
551
+ const entry = { match: (e) => regex.test(e), fn, owner };
552
552
  this.wildcards.push(entry);
553
553
  this.wildcardSnapshotCache = null;
554
554
  return () => {
@@ -726,16 +726,16 @@ var ScopedEventBus = class extends EventBus {
726
726
  * Subscribe to all events. Alias for `onPattern('*')` — the listener is
727
727
  * tracked so that `teardown()` will remove it automatically.
728
728
  */
729
- onAny(fn) {
729
+ onAny(fn, owner) {
730
730
  if (this.wildcards.length >= MAX_WILDCARDS) {
731
731
  this.logger?.error(
732
- `EventBus wildcard limit (${MAX_WILDCARDS}) reached \u2014 rejecting onAny(). Callers must dispose their wildcard listeners to prevent unbounded growth.`
732
+ `EventBus wildcard limit (${MAX_WILDCARDS}) reached \u2014 rejecting onAny()` + (owner ? ` (owner: ${owner})` : "") + ". Callers must dispose their wildcard listeners to prevent unbounded growth."
733
733
  );
734
734
  return () => {
735
735
  };
736
736
  }
737
737
  const key = this.nextKey++;
738
- const unsub = EventBus.prototype.onPattern.call(this, "*", fn);
738
+ const unsub = EventBus.prototype.onPattern.call(this, "*", fn, owner);
739
739
  this.registrations.set(key, unsub);
740
740
  return () => {
741
741
  this.registrations.delete(key);
@@ -746,16 +746,16 @@ var ScopedEventBus = class extends EventBus {
746
746
  * Identical to `EventBus.onPattern` but the listener is tracked so that
747
747
  * `teardown()` will remove it automatically.
748
748
  */
749
- onPattern(pattern, fn) {
749
+ onPattern(pattern, fn, owner) {
750
750
  if (this.wildcards.length >= MAX_WILDCARDS) {
751
751
  this.logger?.error(
752
- `EventBus wildcard limit (${MAX_WILDCARDS}) reached \u2014 rejecting onPattern("${pattern}"). Callers must dispose their wildcard listeners to prevent unbounded growth.`
752
+ `EventBus wildcard limit (${MAX_WILDCARDS}) reached \u2014 rejecting onPattern("${pattern}")` + (owner ? ` (owner: ${owner})` : "") + ". Callers must dispose their wildcard listeners to prevent unbounded growth."
753
753
  );
754
754
  return () => {
755
755
  };
756
756
  }
757
757
  const key = this.nextKey++;
758
- const unsub = super.onPattern(pattern, fn);
758
+ const unsub = super.onPattern(pattern, fn, owner);
759
759
  this.registrations.set(key, unsub);
760
760
  return () => {
761
761
  this.registrations.delete(key);
@@ -766,16 +766,16 @@ var ScopedEventBus = class extends EventBus {
766
766
  * Identical to `EventBus.onRegex` but the listener is tracked so that
767
767
  * `teardown()` will remove it automatically.
768
768
  */
769
- onRegex(regex, fn) {
769
+ onRegex(regex, fn, owner) {
770
770
  if (this.wildcards.length >= MAX_WILDCARDS) {
771
771
  this.logger?.error(
772
- `EventBus wildcard limit (${MAX_WILDCARDS}) reached \u2014 rejecting onRegex(${regex}). Callers must dispose their wildcard listeners to prevent unbounded growth.`
772
+ `EventBus wildcard limit (${MAX_WILDCARDS}) reached \u2014 rejecting onRegex(${regex})` + (owner ? ` (owner: ${owner})` : "") + ". Callers must dispose their wildcard listeners to prevent unbounded growth."
773
773
  );
774
774
  return () => {
775
775
  };
776
776
  }
777
777
  const key = this.nextKey++;
778
- const unsub = super.onRegex(regex, fn);
778
+ const unsub = super.onRegex(regex, fn, owner);
779
779
  this.registrations.set(key, unsub);
780
780
  return () => {
781
781
  this.registrations.delete(key);
@@ -1551,7 +1551,7 @@ var FsError = class extends WrongStackError {
1551
1551
  };
1552
1552
 
1553
1553
  // src/types/quota-regex.ts
1554
- var QUOTA_EXHAUSTED_RE = /(?:insufficient|exhausted|depleted|exceeded|no|not enough)[-_\s]*(?:quota|credit|balance)|(?:quota|credit|balance)(?:\s+(?:has|have))?(?:\s+been)?[-_\s]*(?:exhausted|depleted|exceeded|insufficient)|billing[_\s-]*(?:hard[_\s-]*)?limit|payment required|spending limit|plan limit|usage[-_\s]*limit[-_\s]*(?:reached|exceeded)/i;
1554
+ var QUOTA_EXHAUSTED_RE = /(?:insufficient|exhausted|depleted|exceeded|no|not enough)[-_\s]*(?:quota|credit|balance)|(?:quota|credit|balance)(?:\s+(?:has|have))?(?:\s+been)?[-_\s]*(?:exhausted|depleted|exceeded|insufficient)|billing[_\s-]*(?:hard[_\s-]*)?limit|payment required|spending limit|plan limit|(?:usage|credit)[-_\s]*(?:quota|limit)(?![-_\w])|usage[-_\s]*(?:limit|quota)[-_\s]*(?:reached|exceeded|for)/i;
1555
1555
 
1556
1556
  // src/types/provider.ts
1557
1557
  var CONTEXT_OVERFLOW_RE = /context.length|context.window|maximum context|max.*tokens?.*exceeded|(?:prompt|request|input|messages?).{0,12}too (?:large|long)|exceeds the context|\btokens\b.*exceed|too many tokens|reduce the length|resulted in \d+ tokens|context_length_exceeded/i;
@@ -1563,7 +1563,8 @@ function classifyProviderError(status, body, message) {
1563
1563
  if (status === 0) return "network";
1564
1564
  if (status === 408) return "timeout";
1565
1565
  if (status === 599) return "stream_hang";
1566
- if (status === 402 || QUOTA_EXHAUSTED_RE.test(text)) return "quota_exhausted";
1566
+ if (QUOTA_EXHAUSTED_RE.test(text)) return "quota_exhausted";
1567
+ if (status === 402) return "quota_exhausted";
1567
1568
  if (status === 429 && body?.message && body.type !== "rate_limit_exceeded" && RATE_LIMIT_EXCEEDED_RE.test(body.message)) {
1568
1569
  return "quota_exhausted";
1569
1570
  }
@@ -111,7 +111,7 @@ export interface ToolsConfig {
111
111
  nextsteps?: NextStepsToolConfig | undefined;
112
112
  /**
113
113
  * WrongProxy / WrongTrace: automatic base-URL rerouting through a
114
- * local proxy daemon (default `http://localhost:8000`). When
114
+ * local proxy daemon (default `http://localhost:3444`). When
115
115
  * `enabled` is true AND the daemon at `url` is reachable, every
116
116
  * provider's base URL is rewritten through
117
117
  * `${url}/proxy/<host><path>`. openai-codex is excluded by spec.
@@ -134,7 +134,7 @@ export interface WrongProxyToolConfig {
134
134
  enabled?: boolean | undefined;
135
135
  /**
136
136
  * Where the local proxy daemon listens. Default
137
- * `http://localhost:8000`. The CLI's periodic probe targets
137
+ * `http://localhost:3444`. The CLI's periodic probe targets
138
138
  * `<url>/api/health`; a 2xx response flips the runtime's
139
139
  * `active` flag.
140
140
  */
@@ -912,7 +912,7 @@ function truncate(s, max) {
912
912
  }
913
913
 
914
914
  // src/types/quota-regex.ts
915
- var QUOTA_EXHAUSTED_RE = /(?:insufficient|exhausted|depleted|exceeded|no|not enough)[-_\s]*(?:quota|credit|balance)|(?:quota|credit|balance)(?:\s+(?:has|have))?(?:\s+been)?[-_\s]*(?:exhausted|depleted|exceeded|insufficient)|billing[_\s-]*(?:hard[_\s-]*)?limit|payment required|spending limit|plan limit|usage[-_\s]*limit[-_\s]*(?:reached|exceeded)/i;
915
+ var QUOTA_EXHAUSTED_RE = /(?:insufficient|exhausted|depleted|exceeded|no|not enough)[-_\s]*(?:quota|credit|balance)|(?:quota|credit|balance)(?:\s+(?:has|have))?(?:\s+been)?[-_\s]*(?:exhausted|depleted|exceeded|insufficient)|billing[_\s-]*(?:hard[_\s-]*)?limit|payment required|spending limit|plan limit|(?:usage|credit)[-_\s]*(?:quota|limit)(?![-_\w])|usage[-_\s]*(?:limit|quota)[-_\s]*(?:reached|exceeded|for)/i;
916
916
 
917
917
  // src/types/provider.ts
918
918
  var REASONING_EFFORT_LEVELS = [
@@ -948,7 +948,8 @@ function classifyProviderError(status, body, message) {
948
948
  if (status === 0) return "network";
949
949
  if (status === 408) return "timeout";
950
950
  if (status === 599) return "stream_hang";
951
- if (status === 402 || QUOTA_EXHAUSTED_RE.test(text)) return "quota_exhausted";
951
+ if (QUOTA_EXHAUSTED_RE.test(text)) return "quota_exhausted";
952
+ if (status === 402) return "quota_exhausted";
952
953
  if (status === 429 && body?.message && body.type !== "rate_limit_exceeded" && RATE_LIMIT_EXCEEDED_RE.test(body.message)) {
953
954
  return "quota_exhausted";
954
955
  }
@@ -8,8 +8,8 @@
8
8
  *
9
9
  * Example:
10
10
  * original = "https://api.openai.com/v1"
11
- * proxyUrl = "http://localhost:8000"
12
- * output = "http://localhost:8000/proxy/api.openai.com/v1"
11
+ * proxyUrl = "http://localhost:3444"
12
+ * output = "http://localhost:3444/proxy/api.openai.com/v1"
13
13
  *
14
14
  * The host appears *without* a scheme in the path; the proxy terminates
15
15
  * TLS (or speaks plain HTTP for localhost) and forwards the original
@@ -56,10 +56,25 @@ export interface ProxyConfig {
56
56
  }
57
57
  /** Read the current proxy configuration. Safe to call from any layer. */
58
58
  export declare function getProxyConfig(): ProxyConfig;
59
+ /**
60
+ * Listener notified by `applyProxyConfig` when the resulting config
61
+ * MATERIALLY changed (`enabled` / `url` / `active`). The periodic probe
62
+ * re-writes the same healthy values every tick, so a value-identical
63
+ * write must NOT notify — subscribers (e.g. the instant-apply provider
64
+ * rebuilder) would otherwise run every probe interval.
65
+ */
66
+ export type ProxyConfigListener = (next: ProxyConfig, previous: ProxyConfig) => void;
67
+ /**
68
+ * Subscribe to material proxy-config changes. Returns an unsubscribe
69
+ * function. A throwing listener is isolated: it cannot break the probe
70
+ * loop or starve other subscribers.
71
+ */
72
+ export declare function subscribeToProxyConfig(listener: ProxyConfigListener): () => void;
59
73
  /**
60
74
  * Apply a new proxy configuration. Returns the previous config so callers
61
75
  * (notably the probe) can decide whether the change requires an immediate
62
- * probe vs. waiting for the next tick.
76
+ * probe vs. waiting for the next tick. Notifies subscribers only when the
77
+ * merged result actually differs from the previous state.
63
78
  */
64
79
  export declare function applyProxyConfig(next: Partial<ProxyConfig>): ProxyConfig;
65
80
  /**
@@ -70,7 +85,62 @@ export declare function applyProxyConfig(next: Partial<ProxyConfig>): ProxyConfi
70
85
  export declare function shouldRewriteFor(providerId: string): boolean;
71
86
  /**
72
87
  * Reset to defaults. Intended for tests; do NOT call from production code
73
- * (the singleton lives for the lifetime of the process).
88
+ * (the singleton lives for the lifetime of the process). Also drops all
89
+ * change listeners so tests never observe each other's notifications.
74
90
  */
75
91
  export declare function __resetProxyConfigForTests(): void;
92
+ /** Minimal structural logger — keeps this module dependency-free. */
93
+ export interface ProxyInstantApplyLogger {
94
+ info: (message: string) => void;
95
+ warn: (message: string) => void;
96
+ }
97
+ export interface ProxyInstantApplyDeps {
98
+ /** Current active provider id — the provider whose URL is baked in. */
99
+ getActiveProviderId: () => string;
100
+ /**
101
+ * Raw (pre-rewrite) base URL for a provider id, exactly as
102
+ * `resolveProviderCfg` reads it: `savedCfg?.baseUrl ?? config.baseUrl`.
103
+ * The singleton holds no provider configs, so the caller — who owns the
104
+ * live Config — must inject this reader.
105
+ */
106
+ getRawBaseUrl: (providerId: string) => string | undefined;
107
+ /**
108
+ * Rebuild the live provider for `providerId` and swap it into the live
109
+ * context. Callers inject their existing path — e.g.
110
+ * `buildProviderForModel(providerId, model)` — which re-resolves
111
+ * proxy-aware config via `shouldRewriteFor` on every build. Should
112
+ * serialize the swap through the host's model-transition gate and
113
+ * re-check the live provider id inside it (superseded guard).
114
+ */
115
+ rebuildProvider: (providerId: string) => Promise<void>;
116
+ /** Structured logger for the rebuild decisions. */
117
+ logger: ProxyInstantApplyLogger;
118
+ }
119
+ export interface ProxyInstantApplyHandle {
120
+ /** Detach the subscription. Safe to call more than once. */
121
+ dispose: () => void;
122
+ }
123
+ /**
124
+ * Subscribe to material proxy-config changes and rebuild the live
125
+ * provider when the routing verdict for the ACTIVE provider changes.
126
+ *
127
+ * Comparison is on the effective base URL, so:
128
+ * - probe ticks that rewrite identical values never rebuild;
129
+ * - a proxy URL change while enabled+active rebuilds (new target);
130
+ * - deactivation rebuilds (proxy-rewritten → raw);
131
+ * - toggling off while already direct does NOT rebuild;
132
+ * - a provider/model switch between notifications re-baselines against
133
+ * the PREVIOUS config — the config the switch actually built under —
134
+ * and then falls through to the verdict comparison. Seeding from the
135
+ * new config instead would swallow a needed rebuild: a provider that
136
+ * switched in while the proxy was on (built rewritten) must rebuild
137
+ * direct when the very next change deactivates the proxy.
138
+ *
139
+ * Rebuilds are SERIALIZED: a toggle-off immediately followed by a probe
140
+ * verdict must not race two async rebuilds against each other — the
141
+ * slower, stale build could overwrite the fresh provider swap. Each
142
+ * rebuild re-reads proxy state at build time, so the chain converges on
143
+ * the latest verdict.
144
+ */
145
+ export declare function createProxyInstantApply(deps: ProxyInstantApplyDeps): ProxyInstantApplyHandle;
76
146
  //# sourceMappingURL=proxy-rewrite.d.ts.map
@@ -41,6 +41,13 @@ var currentConfig = { ...DEFAULT_PROXY_CONFIG };
41
41
  function getProxyConfig() {
42
42
  return currentConfig;
43
43
  }
44
+ var listeners = /* @__PURE__ */ new Set();
45
+ function subscribeToProxyConfig(listener) {
46
+ listeners.add(listener);
47
+ return () => {
48
+ listeners.delete(listener);
49
+ };
50
+ }
44
51
  function applyProxyConfig(next) {
45
52
  const previous = currentConfig;
46
53
  currentConfig = {
@@ -48,6 +55,15 @@ function applyProxyConfig(next) {
48
55
  url: next.url ?? previous.url,
49
56
  active: next.active ?? previous.active
50
57
  };
58
+ const materialChange = currentConfig.enabled !== previous.enabled || currentConfig.url !== previous.url || currentConfig.active !== previous.active;
59
+ if (materialChange) {
60
+ for (const listener of listeners) {
61
+ try {
62
+ listener(currentConfig, previous);
63
+ } catch {
64
+ }
65
+ }
66
+ }
51
67
  return previous;
52
68
  }
53
69
  function shouldRewriteFor(providerId) {
@@ -57,14 +73,60 @@ function shouldRewriteFor(providerId) {
57
73
  }
58
74
  function __resetProxyConfigForTests() {
59
75
  currentConfig = { ...DEFAULT_PROXY_CONFIG };
76
+ listeners.clear();
77
+ }
78
+ function effectiveBaseUrlFor(cfg, providerId, rawBaseUrl) {
79
+ if (!rawBaseUrl) return void 0;
80
+ const active = cfg.enabled && cfg.active && !!cfg.url && isProxyEligible(providerId);
81
+ return active ? rewriteBaseUrl(rawBaseUrl, cfg.url) : rawBaseUrl;
82
+ }
83
+ function effectiveBaseUrl(providerId, rawBaseUrl) {
84
+ return effectiveBaseUrlFor(currentConfig, providerId, rawBaseUrl);
85
+ }
86
+ function createProxyInstantApply(deps) {
87
+ const { getActiveProviderId, getRawBaseUrl, rebuildProvider, logger } = deps;
88
+ let lastProviderId = getActiveProviderId();
89
+ let lastEffectiveUrl = effectiveBaseUrl(lastProviderId, getRawBaseUrl(lastProviderId));
90
+ let disposed = false;
91
+ let rebuildChain = Promise.resolve();
92
+ const unsubscribe = subscribeToProxyConfig((next, previous) => {
93
+ if (disposed) return;
94
+ const providerId = getActiveProviderId();
95
+ const raw = getRawBaseUrl(providerId);
96
+ const nextUrl = effectiveBaseUrlFor(next, providerId, raw);
97
+ const baselineUrl = providerId === lastProviderId ? lastEffectiveUrl : effectiveBaseUrlFor(previous, providerId, raw);
98
+ lastProviderId = providerId;
99
+ lastEffectiveUrl = nextUrl;
100
+ if (nextUrl === baselineUrl) return;
101
+ rebuildChain = rebuildChain.then(() => rebuildProvider(providerId)).then(() => {
102
+ if (!disposed) {
103
+ logger.info(`WrongProxy routing changed \u2014 live provider rebuilt (${providerId})`);
104
+ }
105
+ }).catch((err) => {
106
+ if (!disposed) {
107
+ logger.warn(
108
+ `WrongProxy instant-apply: provider rebuild failed for ${providerId}: ${err instanceof Error ? err.message : String(err)}`
109
+ );
110
+ }
111
+ });
112
+ });
113
+ return {
114
+ dispose() {
115
+ if (disposed) return;
116
+ disposed = true;
117
+ unsubscribe();
118
+ }
119
+ };
60
120
  }
61
121
  export {
62
122
  PROXY_EXCLUDED_PROVIDERS,
63
123
  __resetProxyConfigForTests,
64
124
  applyProxyConfig,
125
+ createProxyInstantApply,
65
126
  getProxyConfig,
66
127
  isProxyEligible,
67
128
  rewriteBaseUrl,
68
- shouldRewriteFor
129
+ shouldRewriteFor,
130
+ subscribeToProxyConfig
69
131
  };
70
132
  //# sourceMappingURL=proxy-rewrite.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wrongstack/core",
3
- "version": "0.310.1",
3
+ "version": "0.313.0",
4
4
  "license": "MIT",
5
5
  "description": "WrongStack core: kernel, types, and shared utilities for the WrongStack CLI agent.",
6
6
  "repository": {
@@ -182,9 +182,9 @@
182
182
  "wrongstackApiVersion": "0.1.10",
183
183
  "dependencies": {
184
184
  "zod": "4.4.3",
185
- "@wrongstack/primitives": "0.310.1",
186
- "@wrongstack/kanban": "0.310.1",
187
- "@wrongstack/persistence": "0.310.1"
185
+ "@wrongstack/kanban": "0.313.0",
186
+ "@wrongstack/primitives": "0.313.0",
187
+ "@wrongstack/persistence": "0.313.0"
188
188
  },
189
189
  "devDependencies": {
190
190
  "@types/node": "^22.19.0",