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.
- package/dist/auth-broker/index.js +112 -6
- package/dist/cli/switchroom.js +177 -34
- package/dist/host-control/main.js +289 -27
- package/dist/vault/approvals/kernel-server.js +112 -6
- package/dist/vault/broker/server.js +112 -6
- package/package.json +1 -1
- package/telegram-plugin/dist/gateway/gateway.js +209 -61
- package/telegram-plugin/gateway/stale-pin-sweep-store.ts +14 -8
- package/telegram-plugin/gateway/stale-pin-sweep.test.ts +106 -1
- package/telegram-plugin/gateway/stale-pin-sweep.ts +156 -47
- package/telegram-plugin/gateway/status-pin-store.ts +44 -11
- package/telegram-plugin/tests/status-pin-store.test.ts +62 -6
- package/vendor/hindsight-memory/scripts/subagent_retain.py +30 -7
- package/vendor/hindsight-memory/scripts/tests/test_subagent_retain.py +100 -2
- package/vendor/hindsight-memory/settings.json +1 -1
|
@@ -44732,7 +44732,7 @@ async function handleVoiceMessage(ctx, deps) {
|
|
|
44732
44732
|
}
|
|
44733
44733
|
|
|
44734
44734
|
// gateway/status-pin-store.ts
|
|
44735
|
-
var
|
|
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 (
|
|
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:
|
|
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 =
|
|
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 =
|
|
84582
|
-
var HINDSIGHT_PG_DEFAULT_EFFECTIVE_CACHE_SIZE_MIB =
|
|
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
|
|
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
|
-
` +
|
|
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
|
|
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 (
|
|
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:
|
|
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 {
|
|
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 {
|
|
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
|
-
|
|
87219
|
-
|
|
87220
|
-
|
|
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 {
|
|
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 {
|
|
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
|
-
|
|
87312
|
-
|
|
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
|
|
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 (
|
|
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:
|
|
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.
|
|
99173
|
-
var COMMIT_SHA = "
|
|
99174
|
-
var COMMIT_DATE = "2026-07-
|
|
99175
|
-
var LATEST_PR =
|
|
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:
|
|
113
|
+
v: number
|
|
107
114
|
cursors: SweepCursor[]
|
|
108
115
|
}
|
|
109
116
|
|
|
110
|
-
|
|
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
|
-
|
|
157
|
-
|
|
158
|
-
|
|
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:
|
|
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) {
|