@wrongstack/core 0.306.2 → 0.306.4

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 (41) hide show
  1. package/dist/coordination/index.js +184 -86
  2. package/dist/coordination/mail-tools.d.ts +1 -1
  3. package/dist/coordination/mailbox-project-server.js +38 -16
  4. package/dist/coordination/sqlite-mailbox.d.ts +1 -0
  5. package/dist/core/conversation-state.d.ts +1 -2
  6. package/dist/core/fallback-model.d.ts +19 -1
  7. package/dist/core/fallback-profile-manager.d.ts +21 -3
  8. package/dist/core/index.d.ts +1 -1
  9. package/dist/core/index.js +152 -46
  10. package/dist/defaults/index.js +347 -139
  11. package/dist/execution/auto-compaction-middleware.d.ts +64 -0
  12. package/dist/execution/compaction-core.d.ts +4 -0
  13. package/dist/execution/index.d.ts +1 -1
  14. package/dist/execution/index.js +234 -91
  15. package/dist/execution/retry-policy.d.ts +27 -0
  16. package/dist/hq/index.js +50 -15
  17. package/dist/index.d.ts +1 -1
  18. package/dist/index.js +642 -184
  19. package/dist/infrastructure/index.js +21 -0
  20. package/dist/plugin/index.js +195 -19
  21. package/dist/security/index.js +52 -16
  22. package/dist/security/kanban-boundary.d.ts +3 -1
  23. package/dist/session-catalog/index.js +50 -15
  24. package/dist/session-catalog/project-server.js +50 -15
  25. package/dist/storage/cloud-config-sync/sanitize.d.ts +18 -0
  26. package/dist/storage/cloud-config-sync.d.ts +1 -1
  27. package/dist/storage/index.js +326 -71
  28. package/dist/storage/provider-config-watcher.d.ts +9 -0
  29. package/dist/storage/session-store/strict-empty-check.d.ts +7 -0
  30. package/dist/storage/session-store.d.ts +1 -0
  31. package/dist/tools/index.d.ts +1 -1
  32. package/dist/tools/index.js +83 -22
  33. package/dist/types/blocks.d.ts +9 -0
  34. package/dist/types/config/root.d.ts +14 -0
  35. package/dist/types/session.d.ts +7 -0
  36. package/instructions/agents/code-reviewer.md +3 -0
  37. package/instructions/coordination/subagent-baseline.md +10 -1
  38. package/instructions/system-lite.md +8 -5
  39. package/instructions/system-pro.md +26 -0
  40. package/instructions/system.md +21 -0
  41. package/package.json +3 -3
@@ -8328,6 +8328,24 @@ import { dirname as dirname5 } from "node:path";
8328
8328
  import { homedir as homedir2 } from "node:os";
8329
8329
 
8330
8330
  // src/security/secret-scrubber.ts
8331
+ var JSON_CREDENTIAL_KEY_ANCHORS = [
8332
+ 'Key"',
8333
+ 'key"',
8334
+ 'KEY"',
8335
+ 'token"',
8336
+ 'Token"',
8337
+ 'TOKEN"',
8338
+ 'secret"',
8339
+ 'Secret"',
8340
+ 'SECRET"',
8341
+ 'password"',
8342
+ 'Password"',
8343
+ 'PASSWORD"',
8344
+ 'authorization"',
8345
+ 'Authorization"',
8346
+ 'bearer"',
8347
+ 'Bearer"'
8348
+ ];
8331
8349
  var PATTERNS = [
8332
8350
  // Anchored at the start where possible so partial matches inside larger
8333
8351
  // strings don't trigger false positives.
@@ -8430,6 +8448,30 @@ var PATTERNS = [
8430
8448
  regex: /(^|\s)([A-Z_]{4,}(?:KEY|TOKEN|SECRET|PASSWORD|PWD))\s*[:=]\s*['"]?([A-Za-z0-9_/+=-]{20,512})['"]?(?=\s|$)/g,
8431
8449
  anchor: ["KEY", "TOKEN", "SECRET", "PASSWORD", "PWD"]
8432
8450
  },
8451
+ {
8452
+ type: "json_credential_key",
8453
+ // The JSON counterpart to `high_entropy_env`, and the pattern that the
8454
+ // now-deleted `JSON_KEY_ANCHORS` list was written for. Without it those
8455
+ // anchors only widened the cheap pre-scan — `hasCredentialAnchors` said
8456
+ // "this text may hold a secret", every pattern then declined to match, and
8457
+ // the value went out verbatim. `high_entropy_env` cannot cover these: it
8458
+ // requires an UPPERCASE unquoted key (`API_KEY=…`), so `{"apiKey":"…"}`
8459
+ // never matched.
8460
+ //
8461
+ // Tool results are routinely serialised as JSON, and a credential with no
8462
+ // recognisable prefix (Azure, self-hosted gateways, Anthropic/Codex OAuth)
8463
+ // has no other pattern that can catch it — this is the only thing standing
8464
+ // between such a value and the session JSONL, chronicle, HQ broadcast and
8465
+ // the model's own context.
8466
+ //
8467
+ // The key may carry a prefix (`"anthropicApiKey"`), but the credential word
8468
+ // must END the key: `"tokenCount"` and `"maxTokens"` do not match, because
8469
+ // the closing quote has to follow the word immediately.
8470
+ // Value floor of 8 chars keeps enum-ish values (`"authorization":"none"`)
8471
+ // out. Capture groups: 1=key + punctuation, 2=value, 3=closing quote.
8472
+ regex: /("[A-Za-z0-9_]*(?:apiKey|api_key|token|secret|password|authorization|bearer|private_key|access_token|refresh_token|client_secret)"\s*:\s*")([^"\\]{8,512})(")/gi,
8473
+ anchor: JSON_CREDENTIAL_KEY_ANCHORS
8474
+ },
8433
8475
  // ── Ported from packages/plugins credential-patterns.ts (WS-034) ─────────
8434
8476
  // The plugin runtime carried 37 patterns while this scrubber — the one that
8435
8477
  // guards session JSONL, chronicle, HQ broadcast, WebUI events and the auth
@@ -8512,9 +8554,12 @@ var PATTERNS = [
8512
8554
  anchor: "GOCSPX-"
8513
8555
  }
8514
8556
  ];
8515
- var SIMPLE_PATTERNS = PATTERNS.filter((p) => p.type !== "high_entropy_env");
8557
+ var SIMPLE_PATTERNS = PATTERNS.filter(
8558
+ (p) => p.type !== "high_entropy_env" && p.type !== "json_credential_key"
8559
+ );
8516
8560
  var COMBINED_REGEX = new RegExp(SIMPLE_PATTERNS.map((p) => `(${p.regex.source})`).join("|"), "g");
8517
8561
  var HIGH_ENTROPY_REGEX = PATTERNS.find((p) => p.type === "high_entropy_env").regex;
8562
+ var JSON_CREDENTIAL_REGEX = PATTERNS.find((p) => p.type === "json_credential_key").regex;
8518
8563
  var COMBINED_REPLACEMENTS = SIMPLE_PATTERNS.map((p) => `[REDACTED:${p.type}]`);
8519
8564
  var SCRUB_CHUNK_BYTES = 64 * 1024;
8520
8565
  var SCRUB_OVERLAP_BYTES = 1024;
@@ -8525,20 +8570,7 @@ var PATTERN_ANCHORS = [
8525
8570
  )
8526
8571
  )
8527
8572
  ];
8528
- var JSON_KEY_ANCHORS = [
8529
- '"apiKey"',
8530
- '"api_key"',
8531
- '"token"',
8532
- '"secret"',
8533
- '"password"',
8534
- '"authorization"',
8535
- '"bearer"',
8536
- '"private_key"',
8537
- '"access_token"',
8538
- '"refresh_token"',
8539
- '"client_secret"'
8540
- ];
8541
- var ALL_ANCHORS = [...PATTERN_ANCHORS, ...JSON_KEY_ANCHORS];
8573
+ var ALL_ANCHORS = PATTERN_ANCHORS;
8542
8574
  function hasCredentialAnchors(text) {
8543
8575
  for (const anchor of ALL_ANCHORS) {
8544
8576
  if (text.includes(anchor)) return true;
@@ -8587,6 +8619,9 @@ var DefaultSecretScrubber = class {
8587
8619
  out = out.replace(HIGH_ENTROPY_REGEX, (_match, lead, key, _value) => {
8588
8620
  return `${lead}${key}=[REDACTED:high_entropy_env]`;
8589
8621
  });
8622
+ out = out.replace(JSON_CREDENTIAL_REGEX, (_match, keyPrefix, _value, closingQuote) => {
8623
+ return `${keyPrefix}[REDACTED:json_credential_key]${closingQuote}`;
8624
+ });
8590
8625
  return out;
8591
8626
  }
8592
8627
  /**
@@ -14584,6 +14619,44 @@ function userInputTitle(content) {
14584
14619
  return sessionContentPreview(content, 60);
14585
14620
  }
14586
14621
 
14622
+ // src/storage/session-writer-scrubber.ts
14623
+ function scrubSessionWriterEvent(event, secretScrubber) {
14624
+ const persistMessage = (message) => {
14625
+ const { _estTokens: _ignored, ...persisted } = message;
14626
+ return {
14627
+ ...persisted,
14628
+ content: typeof persisted.content === "string" ? secretScrubber?.scrub(persisted.content) ?? persisted.content : secretScrubber?.scrubObject(persisted.content) ?? persisted.content
14629
+ };
14630
+ };
14631
+ if (event.type === "context_snapshot" || event.type === "messages_replaced") {
14632
+ return { ...event, messages: event.messages.map(persistMessage) };
14633
+ }
14634
+ if (event.type === "message_appended" || event.type === "message_updated") {
14635
+ return { ...event, message: persistMessage(event.message) };
14636
+ }
14637
+ if (!secretScrubber) return event;
14638
+ if (event.type === "user_input") {
14639
+ return {
14640
+ ...event,
14641
+ content: typeof event.content === "string" ? secretScrubber.scrub(event.content) : secretScrubber.scrubObject(event.content)
14642
+ };
14643
+ }
14644
+ if (event.type === "llm_response") {
14645
+ return { ...event, content: secretScrubber.scrubObject(event.content) };
14646
+ }
14647
+ if (event.type === "file_snapshot") {
14648
+ return {
14649
+ ...event,
14650
+ files: event.files.map((f) => ({
14651
+ ...f,
14652
+ before: f.before !== null ? secretScrubber.scrub(f.before) : null,
14653
+ after: f.after !== null ? secretScrubber.scrub(f.after) : null
14654
+ }))
14655
+ };
14656
+ }
14657
+ return event;
14658
+ }
14659
+
14587
14660
  // src/storage/session-writer-truncate.ts
14588
14661
  import * as fsp6 from "node:fs/promises";
14589
14662
  var CHUNK_SIZE = 65536;
@@ -14728,45 +14801,11 @@ async function rewriteSessionToCheckpoint(filePath, checkpointByteOffset) {
14728
14801
  }
14729
14802
  }
14730
14803
 
14731
- // src/storage/session-writer-scrubber.ts
14732
- function scrubSessionWriterEvent(event, secretScrubber) {
14733
- const persistMessage = (message) => {
14734
- const { _estTokens: _ignored, ...persisted } = message;
14735
- return {
14736
- ...persisted,
14737
- content: typeof persisted.content === "string" ? secretScrubber?.scrub(persisted.content) ?? persisted.content : secretScrubber?.scrubObject(persisted.content) ?? persisted.content
14738
- };
14739
- };
14740
- if (event.type === "context_snapshot" || event.type === "messages_replaced") {
14741
- return { ...event, messages: event.messages.map(persistMessage) };
14742
- }
14743
- if (event.type === "message_appended" || event.type === "message_updated") {
14744
- return { ...event, message: persistMessage(event.message) };
14745
- }
14746
- if (!secretScrubber) return event;
14747
- if (event.type === "user_input") {
14748
- return {
14749
- ...event,
14750
- content: typeof event.content === "string" ? secretScrubber.scrub(event.content) : secretScrubber.scrubObject(event.content)
14751
- };
14752
- }
14753
- if (event.type === "llm_response") {
14754
- return { ...event, content: secretScrubber.scrubObject(event.content) };
14755
- }
14756
- if (event.type === "file_snapshot") {
14757
- return {
14758
- ...event,
14759
- files: event.files.map((f) => ({
14760
- ...f,
14761
- before: f.before !== null ? secretScrubber.scrub(f.before) : null,
14762
- after: f.after !== null ? secretScrubber.scrub(f.after) : null
14763
- }))
14764
- };
14765
- }
14766
- return event;
14767
- }
14768
-
14769
14804
  // src/storage/file-session-writer.ts
14805
+ function isClosedHandleError(err) {
14806
+ const code = err?.code;
14807
+ return code === "EBADF" || code === "ERR_CLOSED_RESOURCE" || code === "ERR_INVALID_HANDLE";
14808
+ }
14770
14809
  var FileSessionWriter = class _FileSessionWriter {
14771
14810
  constructor(id, handle, startedAt, meta, events, opts = {}, traceId) {
14772
14811
  this.id = id;
@@ -14938,8 +14977,7 @@ var FileSessionWriter = class _FileSessionWriter {
14938
14977
  try {
14939
14978
  return await this.handle.appendFile(data, "utf8");
14940
14979
  } catch (err) {
14941
- const nodeErr = err;
14942
- if (nodeErr?.code === "EBADF") {
14980
+ if (isClosedHandleError(err)) {
14943
14981
  this.handle = await fsp7.open(this.filePath, "a", 384);
14944
14982
  return await this.handle.appendFile(data, "utf8");
14945
14983
  }
@@ -14979,8 +15017,8 @@ var FileSessionWriter = class _FileSessionWriter {
14979
15017
  bufferSynchronousEvent(event) {
14980
15018
  if (this.closed) return;
14981
15019
  void this.ensureInit();
14982
- this.observeForSummary(event);
14983
- const appendEvent = event.type === "file_snapshot" ? scrubSessionWriterEvent(event, this.secretScrubber) : event;
15020
+ const appendEvent = scrubSessionWriterEvent(event, this.secretScrubber);
15021
+ this.observeForSummary(appendEvent);
14984
15022
  try {
14985
15023
  this._onAppend?.(appendEvent);
14986
15024
  } catch {
@@ -15133,8 +15171,7 @@ var FileSessionWriter = class _FileSessionWriter {
15133
15171
  try {
15134
15172
  await this.handle.datasync();
15135
15173
  } catch (err) {
15136
- const nodeErr = err;
15137
- if (nodeErr?.code === "EBADF") {
15174
+ if (isClosedHandleError(err)) {
15138
15175
  this.handle = await fsp7.open(this.filePath, "a", 384);
15139
15176
  return;
15140
15177
  }
@@ -15371,6 +15408,7 @@ var FileSessionWriter = class _FileSessionWriter {
15371
15408
  return this.closePromise;
15372
15409
  }
15373
15410
  async doClose() {
15411
+ await this.ensureInit();
15374
15412
  if (this.pendingFileSnapshots.length > 0) {
15375
15413
  await this.writeFileSnapshot(this.activePromptIndex ?? 0, [...this.pendingFileSnapshots]);
15376
15414
  this.pendingFileSnapshots = [];
@@ -15386,8 +15424,7 @@ var FileSessionWriter = class _FileSessionWriter {
15386
15424
  try {
15387
15425
  await this.handle.datasync();
15388
15426
  } catch (err) {
15389
- const nodeErr = err;
15390
- if (nodeErr?.code !== "EBADF") throw err;
15427
+ if (!isClosedHandleError(err)) throw err;
15391
15428
  }
15392
15429
  const endedAt = (/* @__PURE__ */ new Date()).toISOString();
15393
15430
  const observedActivityMs = Date.parse(this.lastActivityAt);
@@ -17100,9 +17137,40 @@ async function readOrBuildShardManifestEntry(opts) {
17100
17137
  return entry;
17101
17138
  }
17102
17139
 
17103
- // src/storage/session-store/summary-builder.ts
17140
+ // src/storage/session-store/strict-empty-check.ts
17104
17141
  import { createReadStream as createReadStream4 } from "node:fs";
17105
17142
  import { createInterface as createInterface4 } from "node:readline";
17143
+ var EMPTY_SESSION_EVENT_TYPES = /* @__PURE__ */ new Set(["session_start", "session_resumed", "session_end"]);
17144
+ async function isStrictlyEmptySessionFile(file) {
17145
+ const input = createReadStream4(file, { encoding: "utf8" });
17146
+ const lines = createInterface4({ input, crlfDelay: Infinity });
17147
+ let sawSessionStart = false;
17148
+ try {
17149
+ for await (const line of lines) {
17150
+ if (!line.trim()) continue;
17151
+ let event;
17152
+ try {
17153
+ event = JSON.parse(line);
17154
+ } catch {
17155
+ return false;
17156
+ }
17157
+ if (event === null || typeof event !== "object" || Array.isArray(event)) return false;
17158
+ const type = event.type;
17159
+ if (typeof type !== "string" || !EMPTY_SESSION_EVENT_TYPES.has(type)) return false;
17160
+ if (type === "session_start") sawSessionStart = true;
17161
+ }
17162
+ } catch {
17163
+ return false;
17164
+ } finally {
17165
+ lines.close();
17166
+ input.destroy();
17167
+ }
17168
+ return sawSessionStart;
17169
+ }
17170
+
17171
+ // src/storage/session-store/summary-builder.ts
17172
+ import { createReadStream as createReadStream5 } from "node:fs";
17173
+ import { createInterface as createInterface5 } from "node:readline";
17106
17174
  async function summarizeSessionFile(opts) {
17107
17175
  return summarizeSessionEventSequence({
17108
17176
  id: opts.id,
@@ -17219,8 +17287,8 @@ async function summarizeSessionEventSequence(opts) {
17219
17287
  }
17220
17288
  }
17221
17289
  async function* iterateSessionEvents(file, secretScrubber) {
17222
- const stream = createReadStream4(file, { encoding: "utf8" });
17223
- const lines = createInterface4({ input: stream, crlfDelay: Infinity });
17290
+ const stream = createReadStream5(file, { encoding: "utf8" });
17291
+ const lines = createInterface5({ input: stream, crlfDelay: Infinity });
17224
17292
  try {
17225
17293
  for await (const line of lines) {
17226
17294
  if (!line.trim()) continue;
@@ -18116,6 +18184,10 @@ var DefaultSessionStore = class _DefaultSessionStore {
18116
18184
  await deleteSessionArtifacts({ rootDir: this.dir, id, jsonlPath });
18117
18185
  await this.writeTombstone(id);
18118
18186
  }
18187
+ async isEmpty(id) {
18188
+ const canonicalId = await this.resolveId(id);
18189
+ return isStrictlyEmptySessionFile(this.sessionPath(canonicalId, ".jsonl"));
18190
+ }
18119
18191
  async delete(id) {
18120
18192
  if (this.catalogClient) {
18121
18193
  const canonical = await this.resolveId(id);
@@ -18679,6 +18751,12 @@ function parseModelRef(ref) {
18679
18751
  function hasText(value) {
18680
18752
  return typeof value === "string" && value.trim().length > 0;
18681
18753
  }
18754
+ function asRefList(value) {
18755
+ return Array.isArray(value) ? value : void 0;
18756
+ }
18757
+ function asProfileName(value) {
18758
+ return hasText(value) ? value : void 0;
18759
+ }
18682
18760
  function providerHasKey(entry) {
18683
18761
  if (!entry) return false;
18684
18762
  if (hasText(entry.apiKey)) return true;
@@ -18689,7 +18767,7 @@ function providerHasKey(entry) {
18689
18767
  }
18690
18768
  function visibleProviderModels(config, providerId, providerModels) {
18691
18769
  const entry = config.providers?.[providerId];
18692
- return entry?.models !== void 0 ? [...entry.models] : providerModels;
18770
+ return Array.isArray(entry?.models) ? [...entry.models] : providerModels;
18693
18771
  }
18694
18772
  function buildProfiles(config) {
18695
18773
  const entries = /* @__PURE__ */ new Map();
@@ -18726,13 +18804,34 @@ var FallbackProfileManager = class {
18726
18804
  listProfiles() {
18727
18805
  return Object.freeze([...this.profiles.keys()]);
18728
18806
  }
18807
+ /**
18808
+ * The profile the session has selected (`config.fallbackProfile`, set by
18809
+ * `/fallback profile use <name>`), or undefined when none is selected or the
18810
+ * name no longer resolves to a defined profile.
18811
+ *
18812
+ * Consulted by every resolution entry point when the caller does not name a
18813
+ * profile itself. Without this the leader — which passes no profile — could
18814
+ * never use a named profile at all: `config.fallbackProfiles` was reachable
18815
+ * only by copying a chain into `fallbackModels`.
18816
+ */
18817
+ activeProfileName() {
18818
+ const name = asProfileName(this.config.fallbackProfile);
18819
+ return name && this.profiles.has(name) ? name : void 0;
18820
+ }
18729
18821
  // ── Resolution ─────────────────────────────────────────────────────────
18730
18822
  /**
18731
18823
  * Resolve a named fallback profile to a validated, provider-filtered chain.
18732
18824
  *
18733
- * Returns an empty chain when:
18734
- * - The profile doesn't exist.
18735
- * - Every entry's provider is missing, has no key, or has no matching model.
18825
+ * Returns an empty chain when the profile doesn't exist, or when every entry
18826
+ * is excluded, quarantined, or blacked out.
18827
+ *
18828
+ * Filtering is intentionally identical to {@link resolveRefs} (the explicit
18829
+ * `fallbackModels` path): the self-exclusion, the runtime status tracker,
18830
+ * and the availability calendar — nothing else. Anything a named profile
18831
+ * drops, an explicit chain drops too, and vice versa. Profiles used to apply
18832
+ * two extra filters (provider "usability" and the `providers[].models`
18833
+ * snapshot) that the explicit path did not, which silently rerouted roles
18834
+ * pinned to a profile onto a different model than the one configured.
18736
18835
  *
18737
18836
  * @param name - Profile name from config.fallbackProfiles.
18738
18837
  * @param defaultProvider - Used when an entry has no explicit provider.
@@ -18753,13 +18852,9 @@ var FallbackProfileManager = class {
18753
18852
  if (seen.has(key)) continue;
18754
18853
  seen.add(key);
18755
18854
  if (excludeKey && key === excludeKey) continue;
18756
- const health = this.checkProvider(providerId);
18757
- if (!health.usable) continue;
18758
18855
  if (this.statusTracker && !this.statusTracker.isAvailable(providerId, parsed.model)) continue;
18759
18856
  if (!evaluateModelCalendar(this.config.modelAvailabilitySchedule, providerId, parsed.model).allowed)
18760
18857
  continue;
18761
- const allowedModels = this.config.providers?.[providerId]?.models;
18762
- if (allowedModels && !allowedModels.includes(parsed.model)) continue;
18763
18858
  resolved.push({
18764
18859
  providerId,
18765
18860
  model: parsed.model,
@@ -18777,12 +18872,14 @@ var FallbackProfileManager = class {
18777
18872
  resolveEffective(opts = {}) {
18778
18873
  const bridge = this.resolveBridge(opts.exclude);
18779
18874
  let selected = FREEZER_EMPTY;
18780
- if (opts.fallbackModels && opts.fallbackModels.length > 0) {
18781
- const resolved = this.resolveRefs(opts.fallbackModels, opts.exclude);
18875
+ const explicitRefs = asRefList(opts.fallbackModels);
18876
+ const profileName = asProfileName(opts.fallbackProfile) ?? this.activeProfileName();
18877
+ if (explicitRefs && explicitRefs.length > 0) {
18878
+ const resolved = this.resolveRefs(explicitRefs, opts.exclude);
18782
18879
  if (resolved.length > 0) selected = resolved;
18783
18880
  }
18784
- if (selected.length === 0 && opts.fallbackProfile) {
18785
- const resolved = this.resolve(opts.fallbackProfile, { exclude: opts.exclude });
18881
+ if (selected.length === 0 && profileName) {
18882
+ const resolved = this.resolve(profileName, { exclude: opts.exclude });
18786
18883
  if (resolved.length > 0) selected = resolved;
18787
18884
  }
18788
18885
  if (selected.length === 0 && opts.fallbackAuto !== false) {
@@ -18858,13 +18955,14 @@ var FallbackProfileManager = class {
18858
18955
  };
18859
18956
  const configFallbackAuto = this.config.fallbackAuto;
18860
18957
  const effectiveFallbackAuto = configFallbackAuto !== void 0 && configFallbackAuto !== null ? configFallbackAuto : !opts.closedWorld;
18861
- const explicitRefs = opts.fallbackModels ?? this.config.fallbackModels;
18958
+ const explicitRefs = asRefList(opts.fallbackModels) ?? asRefList(this.config.fallbackModels);
18959
+ const profileName = asProfileName(opts.fallbackProfile) ?? this.activeProfileName();
18862
18960
  const explicitUsable = explicitRefs !== void 0 && explicitRefs.length > 0 && this.resolveRefs(explicitRefs, current).length > 0;
18863
- const profileUsable = opts.fallbackProfile !== void 0 && this.hasProfile(opts.fallbackProfile) && this.resolve(opts.fallbackProfile, { exclude: current }).length > 0;
18961
+ const profileUsable = profileName !== void 0 && this.hasProfile(profileName) && this.resolve(profileName, { exclude: current }).length > 0;
18864
18962
  const fromExplicitSource = explicitUsable || profileUsable;
18865
- const selectedChain = opts.closedWorld ? explicitRefs && explicitRefs.length > 0 ? this.resolveRefs(explicitRefs, current) : opts.fallbackProfile ? this.resolve(opts.fallbackProfile, { exclude: current }) : FREEZER_EMPTY : this.resolveEffective({
18963
+ const selectedChain = opts.closedWorld ? explicitRefs && explicitRefs.length > 0 ? this.resolveRefs(explicitRefs, current) : profileName ? this.resolve(profileName, { exclude: current }) : FREEZER_EMPTY : this.resolveEffective({
18866
18964
  fallbackModels: explicitRefs,
18867
- fallbackProfile: opts.fallbackProfile,
18965
+ fallbackProfile: profileName,
18868
18966
  fallbackAuto: effectiveFallbackAuto,
18869
18967
  exclude: current
18870
18968
  });
@@ -18881,7 +18979,7 @@ var FallbackProfileManager = class {
18881
18979
  });
18882
18980
  }
18883
18981
  candidates.push(...selectedChain);
18884
- if (!fromExplicitSource && effectiveFallbackAuto && opts.fallbackProfile !== "default") {
18982
+ if (!fromExplicitSource && effectiveFallbackAuto && profileName !== "default") {
18885
18983
  candidates.push(...this.resolve("default", { exclude: current }));
18886
18984
  }
18887
18985
  if (!fromExplicitSource && effectiveFallbackAuto) {
@@ -18959,7 +19057,7 @@ var FallbackProfileManager = class {
18959
19057
  const leaderModel = this.config.model;
18960
19058
  const providers = this.config.providers ?? {};
18961
19059
  const favoriteSet = new Set(
18962
- (this.config.favoriteModels ?? []).map((ref) => {
19060
+ (asRefList(this.config.favoriteModels) ?? []).map((ref) => {
18963
19061
  const p = parseModelRef(ref);
18964
19062
  return `${p.provider ?? leaderProvider}/${p.model}`;
18965
19063
  })
@@ -27718,10 +27816,10 @@ var AdaptiveConcurrencyController = class {
27718
27816
  };
27719
27817
 
27720
27818
  // src/coordination/agent-monitor.ts
27721
- import { createReadStream as createReadStream5 } from "node:fs";
27819
+ import { createReadStream as createReadStream6 } from "node:fs";
27722
27820
  import * as fs6 from "node:fs/promises";
27723
27821
  import * as path38 from "node:path";
27724
- import { createInterface as createInterface5 } from "node:readline";
27822
+ import { createInterface as createInterface6 } from "node:readline";
27725
27823
  var AgentMonitorService = class _AgentMonitorService {
27726
27824
  _fleetBus;
27727
27825
  _events;
@@ -27828,8 +27926,8 @@ var AgentMonitorService = class _AgentMonitorService {
27828
27926
  const accessible = await fs6.access(file).then(() => true).catch(() => false);
27829
27927
  if (!accessible) return [];
27830
27928
  const out = [];
27831
- const input = createReadStream5(file, { encoding: "utf8" });
27832
- const lines = createInterface5({ input, crlfDelay: Number.POSITIVE_INFINITY });
27929
+ const input = createReadStream6(file, { encoding: "utf8" });
27930
+ const lines = createInterface6({ input, crlfDelay: Number.POSITIVE_INFINITY });
27833
27931
  try {
27834
27932
  for await (const line of lines) {
27835
27933
  const trimmed = line.trim();
@@ -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
+ summary?: never;
78
79
  ok: boolean;
79
80
  error: string;
80
81
  messageId?: never;
81
82
  from?: never;
82
83
  to?: never;
83
- summary?: never;
84
84
  } | {
85
85
  error?: never;
86
86
  ok: boolean;
@@ -1972,16 +1972,18 @@ function persistReceipt(db, messageId, state) {
1972
1972
  }
1973
1973
  function materializeMessageRows(db, rows) {
1974
1974
  if (rows.length === 0) return [];
1975
- const useTargetedReceipts = rows.length <= 500;
1976
- const receiptSql = useTargetedReceipts ? `
1977
- SELECT message_id, actor_id, read_at, completed_at, completed_by, outcome
1978
- FROM message_receipts
1979
- WHERE message_id IN (${rows.map(() => "?").join(", ")})
1980
- ` : `
1975
+ const receiptRows = [];
1976
+ for (const ids of chunk(
1977
+ rows.map((row) => row.id),
1978
+ 400
1979
+ )) {
1980
+ const receiptSql = `
1981
1981
  SELECT message_id, actor_id, read_at, completed_at, completed_by, outcome
1982
1982
  FROM message_receipts
1983
+ WHERE message_id IN (${ids.map(() => "?").join(", ")})
1983
1984
  `;
1984
- const receiptRows = db.prepare(receiptSql).all(...useTargetedReceipts ? rows.map((row) => row.id) : []);
1985
+ receiptRows.push(...db.prepare(receiptSql).all(...ids));
1986
+ }
1985
1987
  const receiptState = /* @__PURE__ */ new Map();
1986
1988
  for (const row of receiptRows) {
1987
1989
  const states = receiptState.get(row.message_id) ?? {};
@@ -2010,8 +2012,16 @@ function materializeMessageRows(db, rows) {
2010
2012
  });
2011
2013
  }
2012
2014
  function deleteMessages(db, ids) {
2013
- const statement = db.prepare("DELETE FROM messages WHERE id = ?");
2014
- for (const id of ids) statement.run(id);
2015
+ for (const chunkIds of chunk(ids, 400)) {
2016
+ db.prepare(`DELETE FROM messages WHERE id IN (${chunkIds.map(() => "?").join(", ")})`).run(
2017
+ ...chunkIds
2018
+ );
2019
+ }
2020
+ }
2021
+ function* chunk(values, size) {
2022
+ for (let start = 0; start < values.length; start += size) {
2023
+ yield values.slice(start, start + size);
2024
+ }
2015
2025
  }
2016
2026
  function persistAgent(db, agent) {
2017
2027
  db.prepare(`
@@ -2264,11 +2274,11 @@ function createMailboxParseState(raw) {
2264
2274
  ingestMailboxChunk(state, raw);
2265
2275
  return state;
2266
2276
  }
2267
- function ingestMailboxChunk(state, chunk) {
2277
+ function ingestMailboxChunk(state, chunk2) {
2268
2278
  const firstNewIndex = state.messages.length;
2269
2279
  const ackRecords = [];
2270
2280
  const staleExisting = /* @__PURE__ */ new Set();
2271
- for (const line of chunk.split(LINE_SEPARATOR)) {
2281
+ for (const line of chunk2.split(LINE_SEPARATOR)) {
2272
2282
  if (line.trim().length === 0) continue;
2273
2283
  let parsed;
2274
2284
  try {
@@ -2580,6 +2590,7 @@ var SqliteMailbox = class {
2580
2590
  lastHeartbeat = /* @__PURE__ */ new Map();
2581
2591
  lastClientHeartbeat = /* @__PURE__ */ new Map();
2582
2592
  autoCompactTimer = null;
2593
+ autoCompactInFlight;
2583
2594
  closed = false;
2584
2595
  stmt(sql) {
2585
2596
  return this.db.prepare(sql);
@@ -3023,10 +3034,14 @@ var SqliteMailbox = class {
3023
3034
  * heartbeat from that id simply is not throttled and writes once more.
3024
3035
  */
3025
3036
  pruneHeartbeats(map, nowMs) {
3026
- if (map.size <= HEARTBEAT_TRACKING_MAX_ENTRIES) return;
3027
3037
  for (const [id, at] of map) {
3028
3038
  if (nowMs - at > HEARTBEAT_TRACKING_TTL_MS) map.delete(id);
3029
3039
  }
3040
+ while (map.size > HEARTBEAT_TRACKING_MAX_ENTRIES) {
3041
+ const oldest = map.keys().next().value;
3042
+ if (oldest === void 0) break;
3043
+ map.delete(oldest);
3044
+ }
3030
3045
  }
3031
3046
  async deregisterAgent(agentId) {
3032
3047
  this.stmt("DELETE FROM agents WHERE agent_id = ?").run(agentId);
@@ -3130,7 +3145,14 @@ var SqliteMailbox = class {
3130
3145
  return purgeStale(this.compactionCtx(), options);
3131
3146
  }
3132
3147
  async autoCompact(options) {
3133
- return autoCompact(this.compactionCtx(), options);
3148
+ if (this.autoCompactInFlight !== void 0) return this.autoCompactInFlight;
3149
+ const inFlight = autoCompact(this.compactionCtx(), options);
3150
+ this.autoCompactInFlight = inFlight;
3151
+ try {
3152
+ return await inFlight;
3153
+ } finally {
3154
+ if (this.autoCompactInFlight === inFlight) this.autoCompactInFlight = void 0;
3155
+ }
3134
3156
  }
3135
3157
  /** Bundle of store operations the retention sweeps drive. */
3136
3158
  compactionCtx() {
@@ -3446,9 +3468,9 @@ function handleMessage(state, message) {
3446
3468
  scheduleIdleStop();
3447
3469
  });
3448
3470
  }
3449
- function onData(state, chunk) {
3471
+ function onData(state, chunk2) {
3450
3472
  state.lastSeenAt = Date.now();
3451
- state.buffer += chunk;
3473
+ state.buffer += chunk2;
3452
3474
  while (true) {
3453
3475
  const newline = state.buffer.indexOf("\n");
3454
3476
  if (newline < 0) {
@@ -3544,7 +3566,7 @@ var server = net.createServer((socket) => {
3544
3566
  void metadataWritten.then(() => {
3545
3567
  if (!socket.destroyed) send(state, { type: "hello", ...serverInfo });
3546
3568
  });
3547
- socket.on("data", (chunk) => onData(state, chunk));
3569
+ socket.on("data", (chunk2) => onData(state, chunk2));
3548
3570
  socket.on("error", () => {
3549
3571
  });
3550
3572
  socket.on("close", () => {
@@ -14,6 +14,7 @@ export declare class SqliteMailbox implements Mailbox {
14
14
  private readonly lastHeartbeat;
15
15
  private readonly lastClientHeartbeat;
16
16
  private autoCompactTimer;
17
+ private autoCompactInFlight;
17
18
  private closed;
18
19
  constructor(projectDir: string, events?: EventBus, eventEmitter?: MailboxEventEmitter);
19
20
  private stmt;
@@ -1,7 +1,6 @@
1
1
  import type { ContentBlock } from '../types/blocks.js';
2
2
  import type { Message } from '../types/messages.js';
3
- import type { TodoItem } from './context.js';
4
- import { Context } from './context.js';
3
+ import type { Context, TodoItem } from './context.js';
5
4
  /**
6
5
  * Observable wrapper for mutable conversation state. Production code should
7
6
  * mutate messages, todos, and meta through this API so subscribers see a
@@ -143,9 +143,27 @@ export declare function fallbackProfileChain(config: Config, profileName: string
143
143
  export declare function smartDefaultFallbackChain(config: Config): string[];
144
144
  /**
145
145
  * The effective fallback chain for a turn: the explicit `fallbackModels` list
146
- * when non-empty, otherwise the smart default (unless `fallbackAuto` is off).
146
+ * when non-empty, otherwise the selected profile, otherwise the smart default
147
+ * (unless `fallbackAuto` is off).
148
+ *
149
+ * NOTE: this is the SELECTED chain, not the full runtime order — it omits the
150
+ * bridge, the primary re-insertion, the extra `default`-profile depth and the
151
+ * last-resort sweep that {@link runtimeFallbackChain} adds. Use
152
+ * `runtimeFallbackChain` for anything shown to a user as "what will be tried".
147
153
  */
148
154
  export declare function effectiveFallbackChain(config: Config): string[];
155
+ /**
156
+ * The chain the agent loop will ACTUALLY rotate through, in order, if the
157
+ * current primary fails right now — the same `resolveCandidates` call the
158
+ * fallback extension makes, including bridge, primary re-insertion, the
159
+ * `default`-profile depth and the last-resort sweep.
160
+ *
161
+ * `/fallback` used to render `effectiveFallbackChain` instead, so the
162
+ * displayed chain could be four entries while the runtime rotated through
163
+ * seventeen — the view was structurally unable to match the behavior it
164
+ * claimed to describe.
165
+ */
166
+ export declare function runtimeFallbackChain(config: Config): string[];
149
167
  /**
150
168
  * Build the cross-provider fallback extension. Always returns an extension —
151
169
  * the effective chain (`effectiveFallbackChain`) is recomputed every turn from
@@ -59,12 +59,30 @@ export declare class FallbackProfileManager {
59
59
  setStatusTracker(tracker: ProviderModelStatusTracker | undefined): void;
60
60
  hasProfile(name: string): boolean;
61
61
  listProfiles(): readonly string[];
62
+ /**
63
+ * The profile the session has selected (`config.fallbackProfile`, set by
64
+ * `/fallback profile use <name>`), or undefined when none is selected or the
65
+ * name no longer resolves to a defined profile.
66
+ *
67
+ * Consulted by every resolution entry point when the caller does not name a
68
+ * profile itself. Without this the leader — which passes no profile — could
69
+ * never use a named profile at all: `config.fallbackProfiles` was reachable
70
+ * only by copying a chain into `fallbackModels`.
71
+ */
72
+ activeProfileName(): string | undefined;
62
73
  /**
63
74
  * Resolve a named fallback profile to a validated, provider-filtered chain.
64
75
  *
65
- * Returns an empty chain when:
66
- * - The profile doesn't exist.
67
- * - Every entry's provider is missing, has no key, or has no matching model.
76
+ * Returns an empty chain when the profile doesn't exist, or when every entry
77
+ * is excluded, quarantined, or blacked out.
78
+ *
79
+ * Filtering is intentionally identical to {@link resolveRefs} (the explicit
80
+ * `fallbackModels` path): the self-exclusion, the runtime status tracker,
81
+ * and the availability calendar — nothing else. Anything a named profile
82
+ * drops, an explicit chain drops too, and vice versa. Profiles used to apply
83
+ * two extra filters (provider "usability" and the `providers[].models`
84
+ * snapshot) that the explicit path did not, which silently rerouted roles
85
+ * pinned to a profile onto a different model than the one configured.
68
86
  *
69
87
  * @param name - Profile name from config.fallbackProfiles.
70
88
  * @param defaultProvider - Used when an entry has no explicit provider.