switchroom 0.19.32 → 0.19.33

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.
@@ -44732,7 +44732,7 @@ async function handleVoiceMessage(ctx, deps) {
44732
44732
  }
44733
44733
 
44734
44734
  // gateway/status-pin-store.ts
44735
- var SNAPSHOT_VERSIONS = new Set([1, 2, 3, 4]);
44735
+ var SNAPSHOT_VERSION = 4;
44736
44736
  function isPinRow(x) {
44737
44737
  if (x == null || typeof x !== "object")
44738
44738
  return false;
@@ -44757,12 +44757,12 @@ function loadStatusPins(path, fs2) {
44757
44757
  if (parsed == null || typeof parsed !== "object")
44758
44758
  return [];
44759
44759
  const env = parsed;
44760
- if (typeof env.v !== "number" || !SNAPSHOT_VERSIONS.has(env.v) || !Array.isArray(env.pins))
44760
+ if (!Number.isInteger(env.v) || env.v < 1 || !Array.isArray(env.pins))
44761
44761
  return [];
44762
44762
  return env.pins.filter(isPinRow);
44763
44763
  }
44764
44764
  function persistStatusPins(path, fs2, snapshot, log = (l) => process.stderr.write(l)) {
44765
- const env = { v: 4, pins: [...snapshot] };
44765
+ const env = { v: SNAPSHOT_VERSION, pins: [...snapshot] };
44766
44766
  const tmp = path + ".tmp";
44767
44767
  try {
44768
44768
  fs2.writeFileSync(tmp, JSON.stringify(env));
@@ -84574,12 +84574,12 @@ init_paths();
84574
84574
  var _warnedReads2 = new Set;
84575
84575
 
84576
84576
  // ../src/setup/hindsight-pg-defaults.ts
84577
- var HINDSIGHT_PG_MEM_LIMIT_MIB_FOR_DERIVATION = 8 * 1024;
84577
+ var HINDSIGHT_PG_MEM_LIMIT_MIB_FOR_DERIVATION = 16 * 1024;
84578
84578
  var HINDSIGHT_PG_APP_ANON_MIB = 2560;
84579
84579
  var HINDSIGHT_PG_PAGE_CACHE_FLOOR_MIB = 2048;
84580
84580
  var HINDSIGHT_PG_SHARED_BUFFERS_BUDGET_MIB = HINDSIGHT_PG_MEM_LIMIT_MIB_FOR_DERIVATION - HINDSIGHT_PG_APP_ANON_MIB - HINDSIGHT_PG_PAGE_CACHE_FLOOR_MIB;
84581
- var HINDSIGHT_PG_DEFAULT_SHARED_BUFFERS_MIB = 1536;
84582
- var HINDSIGHT_PG_DEFAULT_EFFECTIVE_CACHE_SIZE_MIB = 4096;
84581
+ var HINDSIGHT_PG_DEFAULT_SHARED_BUFFERS_MIB = 3072;
84582
+ var HINDSIGHT_PG_DEFAULT_EFFECTIVE_CACHE_SIZE_MIB = 7168;
84583
84583
  function pgMib(mib) {
84584
84584
  return `${mib}MB`;
84585
84585
  }
@@ -84754,7 +84754,17 @@ var HINDSIGHT_HEALTHCHECK_CMD = `python3 -c '${HINDSIGHT_HEALTHCHECK_PY}'`;
84754
84754
  var DOCKER_PROBE_TIMEOUT_MS = 60 * 1000;
84755
84755
 
84756
84756
  // ../src/memory/hindsight.ts
84757
- var DEFAULT_RETAIN_MISSION = `Extract durable facts that will still be true and useful weeks from now: user preferences and standing rules, ongoing projects and recurring commitments, technical and architectural decisions with their rationale, and people/tool relationships. A preference revealed by a request is durable \u2014 record the preference (what the user likes, wants, or always does), not the request itself.
84757
+ var DEFAULT_RETAIN_MISSION = `Extract durable facts that will still be true and useful weeks from now, once this session is forgotten.
84758
+ ` + `
84759
+ ` + `DURABILITY GATE. Emit a candidate ONLY if it is one of these five classes:
84760
+ ` + `- PREFERENCE \u2014 what the user likes, wants, or always does; a standing rule or correction.
84761
+ ` + `- DECISION \u2014 a settled choice that changes how future work is done, including a choice NOT to do something. A decision about the mechanics of the CURRENT task (which worker to dispatch, which branch to rebase, which PR to merge now, what to do next) is process narration, not a durable decision \u2014 drop it unless it establishes a standing rule or permanently changes a system.
84762
+ ` + `- FINDING \u2014 a root cause, a measurement, or verified behaviour of a system. Include the number.
84763
+ ` + `- OUTCOME \u2014 a completed result that changed the world: what shipped, what a thing turned out to be. Not the act of shipping it.
84764
+ ` + `- RELATIONSHIP \u2014 who a person is, what a project or tool is, and how they connect.
84765
+ ` + `If a candidate fits none of the five, it is not a memory. Drop it; do not reword it into one.
84766
+ ` + `
84767
+ ` + `A preference revealed by a request is durable \u2014 record the preference (what the user likes, wants, or always does), not the request itself.
84758
84768
  ` + `
84759
84769
  ` + `A TOOL RESULT IS NOT A FACT. Before extracting, ask: is the subject of this
84760
84770
  ` + `candidate a file path, a command/process/agent/session id, a temp directory, or
@@ -84771,6 +84781,9 @@ var DEFAULT_RETAIN_MISSION = `Extract durable facts that will still be true and
84771
84781
  ` + `- Anything mentioning a path under /tmp, a scratchpad directory, or a .tmp file.
84772
84782
  ` + `- Agent tool-use traces or narration of what the assistant did (e.g. "the
84773
84783
  ` + ` assistant used X to query Y", "ran a search", "sent the message").
84784
+ ` + `- The act of delegating, dispatching, spawning, launching, steering or merging
84785
+ ` + ` work \u2014 including when it succeeded. "X was dispatched and completed" is the
84786
+ ` + ` session describing itself. Record only what the work LEARNED or CHANGED.
84774
84787
  ` + `- In-flight workflow/process narration (a sub-task started, paused, or is still
84775
84788
  ` + ` running) \u2014 retain the outcome only once the task completes or a decision is made.
84776
84789
  ` + `- Operation, request, batch, agent, command or session IDs, UUIDs, hashes, or error codes.
@@ -84793,8 +84806,12 @@ var DEFAULT_RETAIN_MISSION = `Extract durable facts that will still be true and
84793
84806
  ` + ` the fact is explicitly dated, in which case record it as a dated observation.
84794
84807
  ` + `- Greetings, acknowledgements, and routine operational chatter.
84795
84808
  ` + `
84809
+ ` + `Write each fact so it stands alone: name the thing, the number, and the date. A
84810
+ ` + `sentence that only makes sense while reading this transcript is not durable.
84811
+ ` + `
84796
84812
  ` + `If a candidate fact matches an exclusion, drop it rather than rewording it. If
84797
- ` + "nothing durable remains, return an empty facts list.";
84813
+ ` + `nothing durable remains, return an empty facts list.
84814
+ `;
84798
84815
  var SUPERSEDED_RETAIN_MISSIONS = [
84799
84816
  "Extract technical decisions, architectural choices, user preferences, project context, and people/tool relationships. Ignore routine greetings and transient operational details.",
84800
84817
  "Extract user preferences, ongoing projects, recurring commitments, " + "important context, and durable facts that should help across future " + "conversations. Skip one-off chatter and temporary task noise.",
@@ -84841,7 +84858,96 @@ var SUPERSEDED_RETAIN_MISSIONS = [
84841
84858
  ` + `- Greetings, acknowledgements, and routine operational chatter.
84842
84859
  ` + `
84843
84860
  ` + `If a candidate fact matches an exclusion, drop it rather than rewording it. If
84844
- ` + "nothing durable remains, return an empty facts list."
84861
+ ` + "nothing durable remains, return an empty facts list.",
84862
+ `Extract durable facts that will still be true and useful weeks from now: user preferences and standing rules, ongoing projects and recurring commitments, technical and architectural decisions with their rationale, and people/tool relationships. A preference revealed by a request is durable \u2014 record the preference (what the user likes, wants, or always does), not the request itself.
84863
+ ` + `
84864
+ ` + `A TOOL RESULT IS NOT A FACT. Before extracting, ask: is the subject of this
84865
+ ` + `candidate a file path, a command/process/agent/session id, a temp directory, or
84866
+ ` + `the location where some output was written? If yes, drop it \u2014 it is transcript
84867
+ ` + `exhaust, not memory.
84868
+ ` + `
84869
+ ` + `NEVER extract:
84870
+ ` + `- Tool results verbatim or paraphrased. Concretely, never produce a fact whose
84871
+ ` + ` text resembles any of these: "File created successfully at /path/to/file",
84872
+ ` + ` "A background command with ID bctz4yskm is running, and its output will be
84873
+ ` + ` written to /tmp/...", "Async agent a745598ba84e71df1 was launched successfully
84874
+ ` + ` and is running in the background", "User executed a Bash command to sleep for
84875
+ ` + ` 200 seconds", "The assistant used grep to locate 'truncateSync' in src/foo.ts".
84876
+ ` + `- Anything mentioning a path under /tmp, a scratchpad directory, or a .tmp file.
84877
+ ` + `- Agent tool-use traces or narration of what the assistant did (e.g. "the
84878
+ ` + ` assistant used X to query Y", "ran a search", "sent the message").
84879
+ ` + `- In-flight workflow/process narration (a sub-task started, paused, or is still
84880
+ ` + ` running) \u2014 retain the outcome only once the task completes or a decision is made.
84881
+ ` + `- Operation, request, batch, agent, command or session IDs, UUIDs, hashes, or error codes.
84882
+ ` + `- Slash commands the user typed and their effects (e.g. "User issued /clear to
84883
+ ` + ` reset assistant state").
84884
+ ` + `- Hindsight's own errors, retries, backlogs, or internal state \u2014 the memory
84885
+ ` + ` system's self-reports are not memories.
84886
+ ` + `- Restatements of the user's current request or the task in progress.
84887
+ ` + `- Volatile state written as a timeless assertion. A version, count, size,
84888
+ ` + ` backlog, status, or any "X is running Y" / "X is at Y" / "X is currently Y"
84889
+ ` + ` claim is true only at the instant it was said. Concretely, never produce a
84890
+ ` + ` fact whose text resembles any of these: "Switchroom fleet is running image
84891
+ ` + ` version v0.18.19", "The switchroom repo is at /path/to/fleet, version
84892
+ ` + ` v0.19.5", "Bank overlord has 43155 pending consolidations", "The build is
84893
+ ` + ` currently green". If the claim is worth keeping, put the date INSIDE the
84894
+ ` + ` fact text ("As of 2026-07-19 the fleet was running v0.18.19"); if you
84895
+ ` + ` cannot date it, drop it. An undated one is recalled forever as though it
84896
+ ` + ` were still true, which is worse than not remembering it at all.
84897
+ ` + `- Transient state (unread counts, build status, what is running right now) unless
84898
+ ` + ` the fact is explicitly dated, in which case record it as a dated observation.
84899
+ ` + `- Greetings, acknowledgements, and routine operational chatter.
84900
+ ` + `
84901
+ ` + `If a candidate fact matches an exclusion, drop it rather than rewording it. If
84902
+ ` + "nothing durable remains, return an empty facts list.",
84903
+ `Extract durable facts that will still be true and useful weeks from now, once this session is forgotten.
84904
+ ` + `
84905
+ ` + `DURABILITY GATE. Emit a candidate ONLY if it is one of these five classes:
84906
+ ` + `- PREFERENCE \u2014 what the user likes, wants, or always does; a standing rule or correction.
84907
+ ` + `- DECISION \u2014 a settled choice that changes how future work is done, including a choice NOT to do something. A decision about the mechanics of the CURRENT task (which worker to dispatch, which branch to rebase, which PR to merge now, what to do next) is process narration, not a durable decision \u2014 drop it unless it establishes a standing rule or permanently changes a system.
84908
+ ` + `- FINDING \u2014 a root cause, a measurement, or verified behaviour of a system. Include the number.
84909
+ ` + `- OUTCOME \u2014 a completed result that changed the world: what shipped, what a thing turned out to be. Not the act of shipping it.
84910
+ ` + `- RELATIONSHIP \u2014 who a person is, what a project or tool is, and how they connect.
84911
+ ` + `If a candidate fits none of the five, it is not a memory. Drop it; do not reword it into one.
84912
+ ` + `
84913
+ ` + `A preference revealed by a request is durable \u2014 record the preference (what the user likes, wants, or always does), not the request itself.
84914
+ ` + `
84915
+ ` + `A TOOL RESULT IS NOT A FACT. Before extracting, ask: is the subject of this
84916
+ ` + `candidate a file path, a command/process/agent/session id, a temp directory, or
84917
+ ` + `the location where some output was written? If yes, drop it \u2014 it is transcript
84918
+ ` + `exhaust, not memory.
84919
+ ` + `
84920
+ ` + `NEVER extract:
84921
+ ` + `- Tool results verbatim or paraphrased. Concretely, never produce a fact whose
84922
+ ` + ` text resembles any of these: "File created successfully at /path/to/file",
84923
+ ` + ` "A background command with ID bctz4yskm is running, and its output will be
84924
+ ` + ` written to /tmp/...", "Async agent a745598ba84e71df1 was launched successfully
84925
+ ` + ` and is running in the background", "User executed a Bash command to sleep for
84926
+ ` + ` 200 seconds", "The assistant used grep to locate 'truncateSync' in src/foo.ts".
84927
+ ` + `- Anything mentioning a path under /tmp, a scratchpad directory, or a .tmp file.
84928
+ ` + `- Agent tool-use traces or narration of what the assistant did (e.g. "the
84929
+ ` + ` assistant used X to query Y", "ran a search", "sent the message").
84930
+ ` + `- The act of delegating, dispatching, spawning, launching, steering or merging
84931
+ ` + ` work \u2014 including when it succeeded. "X was dispatched and completed" is the
84932
+ ` + ` session describing itself. Record only what the work LEARNED or CHANGED.
84933
+ ` + `- In-flight workflow/process narration (a sub-task started, paused, or is still
84934
+ ` + ` running) \u2014 retain the outcome only once the task completes or a decision is made.
84935
+ ` + `- Operation, request, batch, agent, command or session IDs, UUIDs, hashes, or error codes.
84936
+ ` + `- Slash commands the user typed and their effects (e.g. "User issued /clear to
84937
+ ` + ` reset assistant state").
84938
+ ` + `- Hindsight's own errors, retries, backlogs, or internal state \u2014 the memory
84939
+ ` + ` system's self-reports are not memories.
84940
+ ` + `- Restatements of the user's current request or the task in progress.
84941
+ ` + `- Transient state (unread counts, build status, what is running right now) unless
84942
+ ` + ` the fact is explicitly dated, in which case record it as a dated observation.
84943
+ ` + `- Greetings, acknowledgements, and routine operational chatter.
84944
+ ` + `
84945
+ ` + `Write each fact so it stands alone: name the thing, the number, and the date. A
84946
+ ` + `sentence that only makes sense while reading this transcript is not durable.
84947
+ ` + `
84948
+ ` + `If a candidate fact matches an exclusion, drop it rather than rewording it. If
84949
+ ` + `nothing durable remains, return an empty facts list.
84950
+ `
84845
84951
  ];
84846
84952
  var DEFAULT_OBSERVATIONS_MISSION = `Synthesise durable, standing knowledge about the people, projects, and systems this agent works with: preferences and standing rules, roles and relationships, skills and recurring patterns, technical and operational decisions with their rationale, and the state of long-running work once it lands.
84847
84953
  ` + `
@@ -86523,7 +86629,7 @@ function shouldSweepChatAtBoot(chatId) {
86523
86629
 
86524
86630
  // gateway/stale-pin-sweep-store.ts
86525
86631
  var SWEEP_MAX_ATTEMPTS = 8;
86526
- var SWEEP_ENVELOPE_VERSIONS = new Set([1]);
86632
+ var SWEEP_ENVELOPE_VERSION = 1;
86527
86633
  function sweepTargetKey(chatId, threadId) {
86528
86634
  return `${chatId}:${threadId ?? "-"}`;
86529
86635
  }
@@ -86551,13 +86657,13 @@ function loadSweepCursors(path2, fs2) {
86551
86657
  if (parsed == null || typeof parsed !== "object")
86552
86658
  return [];
86553
86659
  const env = parsed;
86554
- if (typeof env.v !== "number" || !SWEEP_ENVELOPE_VERSIONS.has(env.v) || !Array.isArray(env.cursors)) {
86660
+ if (!Number.isInteger(env.v) || env.v < 1 || !Array.isArray(env.cursors)) {
86555
86661
  return [];
86556
86662
  }
86557
86663
  return env.cursors.filter(isCursorRow);
86558
86664
  }
86559
86665
  function persistSweepCursors(path2, fs2, cursors, log = (l) => process.stderr.write(l)) {
86560
- const env = { v: 1, cursors: [...cursors] };
86666
+ const env = { v: SWEEP_ENVELOPE_VERSION, cursors: [...cursors] };
86561
86667
  try {
86562
86668
  fs2.writeFileSync(path2, JSON.stringify(env));
86563
86669
  } catch (err) {
@@ -86978,11 +87084,13 @@ function createStalePinSweeper(deps) {
86978
87084
  const repinUnpinLoop = async (target, kind, cursor) => {
86979
87085
  const gate = gateFor(kind);
86980
87086
  let popped = 0;
87087
+ let issued = 0;
86981
87088
  let consecutiveFloods = 0;
86982
87089
  let lastRetryAfter = null;
86983
87090
  let noProgressAtSameRetryAfter = 0;
86984
87091
  let lastObservedTop = null;
86985
87092
  let unverifiedPop = false;
87093
+ const out = (r) => ({ ...r, issued });
86986
87094
  const creditPop = () => {
86987
87095
  popped++;
86988
87096
  cursor.popped++;
@@ -86991,29 +87099,29 @@ function createStalePinSweeper(deps) {
86991
87099
  };
86992
87100
  for (let i = 0;i < gate.maxPopsPerChatPerSweep; i++) {
86993
87101
  if (circuitOpen) {
86994
- return { status: "aborted-circuit-breaker", popped, detail: "breaker open" };
87102
+ return out({ status: "aborted-circuit-breaker", popped, detail: "breaker open" });
86995
87103
  }
86996
87104
  const observed = await readTopVerified(target.chatId);
86997
87105
  if (observed == null) {
86998
- return { status: "incomplete", popped, detail: "pin-stack read unverifiable" };
87106
+ return out({ status: "incomplete", popped, detail: "pin-stack read unverifiable" });
86999
87107
  }
87000
87108
  if (unverifiedPop) {
87001
87109
  if (observed.top != null && observed.top === lastObservedTop) {
87002
- return {
87110
+ return out({
87003
87111
  status: "incomplete",
87004
87112
  popped,
87005
87113
  detail: `no progress: message ${observed.top} still on top after a repin+unpin`
87006
- };
87114
+ });
87007
87115
  }
87008
87116
  creditPop();
87009
87117
  unverifiedPop = false;
87010
87118
  }
87011
87119
  if (observed.top == null) {
87012
- return { status: "drained", popped };
87120
+ return out({ status: "drained", popped });
87013
87121
  }
87014
87122
  lastObservedTop = observed.top;
87015
87123
  if (!await awaitWriteSlot(kind)) {
87016
- return { status: "deferred-budget", popped, detail: "per-minute pin-op budget spent" };
87124
+ return out({ status: "deferred-budget", popped, detail: "per-minute pin-op budget spent" });
87017
87125
  }
87018
87126
  try {
87019
87127
  await deps.pinSilent(target.chatId, observed.top);
@@ -87021,10 +87129,10 @@ function createStalePinSweeper(deps) {
87021
87129
  const d = classify(err);
87022
87130
  if (d.kind === "breaker") {
87023
87131
  circuitOpen = true;
87024
- return { status: "aborted-circuit-breaker", popped, detail: d.detail };
87132
+ return out({ status: "aborted-circuit-breaker", popped, detail: d.detail });
87025
87133
  }
87026
87134
  if (d.kind === "rights")
87027
- return { status: "skipped-no-rights", popped };
87135
+ return out({ status: "skipped-no-rights", popped });
87028
87136
  if (d.kind === "flood") {
87029
87137
  consecutiveFloods++;
87030
87138
  if (lastRetryAfter === d.seconds)
@@ -87033,19 +87141,19 @@ function createStalePinSweeper(deps) {
87033
87141
  noProgressAtSameRetryAfter = 1;
87034
87142
  lastRetryAfter = d.seconds;
87035
87143
  if (consecutiveFloods >= MAX_CONSECUTIVE_FLOOD_WAITS || noProgressAtSameRetryAfter >= MAX_IDENTICAL_RETRY_AFTER_NO_PROGRESS) {
87036
- return {
87144
+ return out({
87037
87145
  status: "deferred-flood",
87038
87146
  popped,
87039
87147
  detail: `flood: ${consecutiveFloods} consecutive 429s, retry_after=${d.seconds}s`
87040
- };
87148
+ });
87041
87149
  }
87042
87150
  await deps.sleep(d.seconds * 1000 * Math.pow(FLOOD_BACKOFF_FACTOR, consecutiveFloods - 1));
87043
87151
  continue;
87044
87152
  }
87045
- return { status: "error", popped, detail: d.detail };
87153
+ return out({ status: "error", popped, detail: d.detail });
87046
87154
  }
87047
87155
  if (!await awaitWriteSlot(kind)) {
87048
- return { status: "deferred-budget", popped, detail: "per-minute pin-op budget spent" };
87156
+ return out({ status: "deferred-budget", popped, detail: "per-minute pin-op budget spent" });
87049
87157
  }
87050
87158
  try {
87051
87159
  await deps.unpin(target.chatId, observed.top);
@@ -87053,12 +87161,12 @@ function createStalePinSweeper(deps) {
87053
87161
  const d = classify(err);
87054
87162
  if (d.kind === "breaker") {
87055
87163
  circuitOpen = true;
87056
- return { status: "aborted-circuit-breaker", popped, detail: d.detail };
87164
+ return out({ status: "aborted-circuit-breaker", popped, detail: d.detail });
87057
87165
  }
87058
87166
  if (d.kind === "rights")
87059
- return { status: "skipped-no-rights", popped };
87167
+ return out({ status: "skipped-no-rights", popped });
87060
87168
  if (isNothingToUnpinError(err))
87061
- return { status: "drained", popped };
87169
+ return out({ status: "drained", popped });
87062
87170
  if (d.kind === "flood") {
87063
87171
  consecutiveFloods++;
87064
87172
  if (lastRetryAfter === d.seconds)
@@ -87067,30 +87175,31 @@ function createStalePinSweeper(deps) {
87067
87175
  noProgressAtSameRetryAfter = 1;
87068
87176
  lastRetryAfter = d.seconds;
87069
87177
  if (consecutiveFloods >= MAX_CONSECUTIVE_FLOOD_WAITS || noProgressAtSameRetryAfter >= MAX_IDENTICAL_RETRY_AFTER_NO_PROGRESS) {
87070
- return {
87178
+ return out({
87071
87179
  status: "deferred-flood",
87072
87180
  popped,
87073
87181
  detail: `flood: ${consecutiveFloods} consecutive 429s, retry_after=${d.seconds}s`
87074
- };
87182
+ });
87075
87183
  }
87076
87184
  await deps.sleep(d.seconds * 1000 * Math.pow(FLOOD_BACKOFF_FACTOR, consecutiveFloods - 1));
87077
87185
  continue;
87078
87186
  }
87079
- return { status: "error", popped, detail: d.detail };
87187
+ return out({ status: "error", popped, detail: d.detail });
87080
87188
  }
87081
87189
  consecutiveFloods = 0;
87190
+ issued++;
87082
87191
  unverifiedPop = true;
87083
87192
  }
87084
87193
  const finalRead = await readTopVerified(target.chatId);
87085
87194
  if (unverifiedPop && finalRead != null && finalRead.top !== lastObservedTop)
87086
87195
  creditPop();
87087
87196
  if (finalRead != null && finalRead.top == null)
87088
- return { status: "drained", popped };
87089
- return {
87197
+ return out({ status: "drained", popped });
87198
+ return out({
87090
87199
  status: "deferred-budget",
87091
87200
  popped,
87092
87201
  detail: `hit the ${gate.maxPopsPerChatPerSweep}-pop cap for this chat; yielding to next boot`
87093
- };
87202
+ });
87094
87203
  };
87095
87204
  const targetedUnpinDrain = async (target, kind, cursor, ids) => {
87096
87205
  const alreadyDone = new Set(cursor.doneIds ?? []);
@@ -87187,10 +87296,21 @@ function createStalePinSweeper(deps) {
87187
87296
  const forumTopicDrain = async (target, cursor) => {
87188
87297
  const threadId = target.threadId;
87189
87298
  if (threadId == null) {
87190
- return { status: "skipped-nothing-recorded", popped: 0, detail: "no message_thread_id" };
87299
+ return {
87300
+ status: "skipped-nothing-recorded",
87301
+ popped: 0,
87302
+ issued: 0,
87303
+ detail: "no message_thread_id"
87304
+ };
87191
87305
  }
87306
+ const before = await readTopVerified(target.chatId);
87192
87307
  if (!await awaitWriteSlot("forum-topic")) {
87193
- return { status: "deferred-budget", popped: 0, detail: "per-minute pin-op budget spent" };
87308
+ return {
87309
+ status: "deferred-budget",
87310
+ popped: 0,
87311
+ issued: 0,
87312
+ detail: "per-minute pin-op budget spent"
87313
+ };
87194
87314
  }
87195
87315
  try {
87196
87316
  await deps.unpinAllForumTopicMessages(target.chatId, threadId);
@@ -87198,39 +87318,59 @@ function createStalePinSweeper(deps) {
87198
87318
  const d = classify(err);
87199
87319
  if (d.kind === "breaker") {
87200
87320
  circuitOpen = true;
87201
- return { status: "aborted-circuit-breaker", popped: 0, detail: d.detail };
87321
+ return { status: "aborted-circuit-breaker", popped: 0, issued: 0, detail: d.detail };
87202
87322
  }
87203
87323
  if (d.kind === "rights")
87204
- return { status: "skipped-no-rights", popped: 0 };
87324
+ return { status: "skipped-no-rights", popped: 0, issued: 0 };
87205
87325
  if (d.kind === "flood") {
87206
87326
  return {
87207
87327
  status: "deferred-flood",
87208
87328
  popped: 0,
87329
+ issued: 0,
87209
87330
  detail: `unpinAllForumTopicMessages pegged at retry_after=${d.seconds}s; not looping`
87210
87331
  };
87211
87332
  }
87212
- return { status: "error", popped: 0, detail: d.detail };
87333
+ return { status: "error", popped: 0, issued: 0, detail: d.detail };
87213
87334
  }
87214
- cursor.popped++;
87215
- cursor.updatedAt = deps.now();
87216
- commit(cursor);
87217
87335
  const observed = await readTopVerified(target.chatId);
87218
- if (observed != null && observed.top == null)
87219
- return { status: "drained", popped: 1 };
87220
- return { status: "drained", popped: 1, detail: "topic drained; chat-wide stack may hold other topics" };
87336
+ const topMoved = before != null && observed != null && observed.top !== before.top ? 1 : 0;
87337
+ if (topMoved > 0) {
87338
+ cursor.popped += topMoved;
87339
+ cursor.updatedAt = deps.now();
87340
+ commit(cursor);
87341
+ }
87342
+ if (observed != null && observed.top == null) {
87343
+ return { status: "drained", popped: topMoved, issued: 1 };
87344
+ }
87345
+ return {
87346
+ status: "drained",
87347
+ popped: topMoved,
87348
+ issued: 1,
87349
+ detail: "topic drained; chat-wide stack may hold other topics"
87350
+ };
87221
87351
  };
87222
87352
  const sweepOnce = async (target) => {
87223
87353
  if (!deps.eligible())
87224
- return { status: "skipped-not-eligible", popped: 0 };
87354
+ return { status: "skipped-not-eligible", popped: 0, issued: 0 };
87225
87355
  if (circuitOpen) {
87226
- return { status: "aborted-circuit-breaker", popped: 0, detail: "breaker already open" };
87356
+ return {
87357
+ status: "aborted-circuit-breaker",
87358
+ popped: 0,
87359
+ issued: 0,
87360
+ detail: "breaker already open"
87361
+ };
87227
87362
  }
87228
87363
  const kind = classifyChatForSweep(target);
87229
87364
  const cursor = loadCursor(target, kind);
87230
87365
  if (cursor.done)
87231
- return { status: "already-drained", popped: 0 };
87366
+ return { status: "already-drained", popped: 0, issued: 0 };
87232
87367
  if (cursor.attempts >= SWEEP_MAX_ATTEMPTS) {
87233
- return { status: "forfeited", popped: 0, detail: `${cursor.attempts} attempts exhausted` };
87368
+ return {
87369
+ status: "forfeited",
87370
+ popped: 0,
87371
+ issued: 0,
87372
+ detail: `${cursor.attempts} attempts exhausted`
87373
+ };
87234
87374
  }
87235
87375
  if (kind !== "dm") {
87236
87376
  let allowed = false;
@@ -87244,7 +87384,7 @@ function createStalePinSweeper(deps) {
87244
87384
  cursor.lastStatus = "skipped-no-rights";
87245
87385
  cursor.updatedAt = deps.now();
87246
87386
  commit(cursor);
87247
- return { status: "skipped-no-rights", popped: 0 };
87387
+ return { status: "skipped-no-rights", popped: 0, issued: 0 };
87248
87388
  }
87249
87389
  }
87250
87390
  cursor.attempts++;
@@ -87273,7 +87413,7 @@ function createStalePinSweeper(deps) {
87273
87413
  } : await targetedUnpinDrain(target, kind, cursor, ids);
87274
87414
  }
87275
87415
  } catch (err) {
87276
- result = { status: "error", popped: 0, detail: err.message };
87416
+ result = { status: "error", popped: 0, issued: 0, detail: err.message };
87277
87417
  }
87278
87418
  if (result.status === "drained" || result.status === "skipped-nothing-recorded") {
87279
87419
  cursor.done = true;
@@ -87306,10 +87446,12 @@ function createStalePinSweeper(deps) {
87306
87446
  const running = inFlight.get(key);
87307
87447
  if (running != null)
87308
87448
  return running;
87309
- if (!deps.eligible())
87310
- return Promise.resolve({ status: "skipped-not-eligible", popped: 0 });
87311
- if (attempted.has(key))
87312
- return Promise.resolve({ status: "already-attempted", popped: 0 });
87449
+ if (!deps.eligible()) {
87450
+ return Promise.resolve({ status: "skipped-not-eligible", popped: 0, issued: 0 });
87451
+ }
87452
+ if (attempted.has(key)) {
87453
+ return Promise.resolve({ status: "already-attempted", popped: 0, issued: 0 });
87454
+ }
87313
87455
  attempted.add(key);
87314
87456
  const p = sweepOnce(target).finally(() => inFlight.delete(key));
87315
87457
  inFlight.set(key, p);
@@ -90214,7 +90356,7 @@ function createCapturedResumeDispatcher(ports) {
90214
90356
 
90215
90357
  // gateway/status-pin-store.ts
90216
90358
  var BOOT_UNPIN_MAX_ATTEMPTS = 5;
90217
- var SNAPSHOT_VERSIONS2 = new Set([1, 2, 3, 4]);
90359
+ var SNAPSHOT_VERSION2 = 4;
90218
90360
  function isPinRow2(x) {
90219
90361
  if (x == null || typeof x !== "object")
90220
90362
  return false;
@@ -90239,12 +90381,12 @@ function loadStatusPins2(path3, fs3) {
90239
90381
  if (parsed == null || typeof parsed !== "object")
90240
90382
  return [];
90241
90383
  const env = parsed;
90242
- if (typeof env.v !== "number" || !SNAPSHOT_VERSIONS2.has(env.v) || !Array.isArray(env.pins))
90384
+ if (!Number.isInteger(env.v) || env.v < 1 || !Array.isArray(env.pins))
90243
90385
  return [];
90244
90386
  return env.pins.filter(isPinRow2);
90245
90387
  }
90246
90388
  function persistStatusPins2(path3, fs3, snapshot, log = (l) => process.stderr.write(l)) {
90247
- const env = { v: 4, pins: [...snapshot] };
90389
+ const env = { v: SNAPSHOT_VERSION2, pins: [...snapshot] };
90248
90390
  const tmp = path3 + ".tmp";
90249
90391
  try {
90250
90392
  fs3.writeFileSync(tmp, JSON.stringify(env));
@@ -96545,6 +96687,12 @@ function parseAuditLine(line) {
96545
96687
  entry.m = o.m;
96546
96688
  if (typeof o.prior_pin === "string")
96547
96689
  entry.prior_pin = o.prior_pin;
96690
+ if (Array.isArray(o.prior_pin_observed) && o.prior_pin_observed.every((v) => typeof v === "string")) {
96691
+ entry.prior_pin_observed = o.prior_pin_observed;
96692
+ }
96693
+ if (typeof o.prior_pin_source === "string") {
96694
+ entry.prior_pin_source = o.prior_pin_source;
96695
+ }
96548
96696
  return entry;
96549
96697
  }
96550
96698
  function filterEntries(entries, filters) {
@@ -99169,10 +99317,10 @@ function startOutboxSweep(deps) {
99169
99317
  }
99170
99318
 
99171
99319
  // ../src/build-info.ts
99172
- var VERSION2 = "0.19.32";
99173
- var COMMIT_SHA = "5ccce0a7";
99174
- var COMMIT_DATE = "2026-07-28T23:48:52Z";
99175
- var LATEST_PR = 3962;
99320
+ var VERSION2 = "0.19.33";
99321
+ var COMMIT_SHA = "4ccbde6b";
99322
+ var COMMIT_DATE = "2026-07-29T02:39:39Z";
99323
+ var LATEST_PR = 3973;
99176
99324
  var COMMITS_AHEAD_OF_TAG = 0;
99177
99325
 
99178
99326
  // gateway/boot-version.ts
@@ -102,12 +102,20 @@ export interface SweepStoreFsSeam {
102
102
  existsSync: (path: string) => boolean
103
103
  }
104
104
 
105
+ /**
106
+ * Envelope. Same bump rule as `status-pin-store.ts` (#3957): every field added
107
+ * after v1 is OPTIONAL, and the reader is version-TOLERANT so a row written by
108
+ * a newer build survives a downgrade rather than being discarded. Discarding is
109
+ * not "do nothing" for an obligation ledger — it forgets an obligation, which
110
+ * is precisely the orphaned-pin failure this module exists to close.
111
+ */
105
112
  interface SweepEnvelope {
106
- v: 1
113
+ v: number
107
114
  cursors: SweepCursor[]
108
115
  }
109
116
 
110
- const SWEEP_ENVELOPE_VERSIONS = new Set([1])
117
+ /** The version THIS build writes. Only the writer is pinned; see above. */
118
+ const SWEEP_ENVELOPE_VERSION = 1
111
119
 
112
120
  /** Stable identity of a sweep target: chat plus topic (topic-less = `-`). */
113
121
  export function sweepTargetKey(chatId: string, threadId?: number): string {
@@ -153,11 +161,9 @@ export function loadSweepCursors(path: string, fs: SweepStoreFsSeam): SweepCurso
153
161
  }
154
162
  if (parsed == null || typeof parsed !== 'object') return []
155
163
  const env = parsed as Record<string, unknown>
156
- if (
157
- typeof env.v !== 'number' ||
158
- !SWEEP_ENVELOPE_VERSIONS.has(env.v) ||
159
- !Array.isArray(env.cursors)
160
- ) {
164
+ // Version-TOLERANT, structurally strict — see {@link SweepEnvelope}. An
165
+ // unknown `v` still yields the cursor rows that validate.
166
+ if (!Number.isInteger(env.v) || (env.v as number) < 1 || !Array.isArray(env.cursors)) {
161
167
  return []
162
168
  }
163
169
  return env.cursors.filter(isCursorRow)
@@ -174,7 +180,7 @@ export function persistSweepCursors(
174
180
  cursors: readonly SweepCursor[],
175
181
  log: (line: string) => void = (l) => process.stderr.write(l),
176
182
  ): void {
177
- const env: SweepEnvelope = { v: 1, cursors: [...cursors] }
183
+ const env: SweepEnvelope = { v: SWEEP_ENVELOPE_VERSION, cursors: [...cursors] }
178
184
  try {
179
185
  fs.writeFileSync(path, JSON.stringify(env))
180
186
  } catch (err) {