switchroom 0.19.32 → 0.19.34
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/agent-scheduler/index.js +1 -0
- package/dist/auth-broker/index.js +11173 -10974
- package/dist/cli/notion-write-pretool.mjs +1 -0
- package/dist/cli/switchroom.js +10663 -10226
- package/dist/host-control/main.js +9781 -9423
- package/dist/vault/approvals/kernel-server.js +559 -428
- package/dist/vault/broker/server.js +1688 -1566
- package/package.json +1 -1
- package/telegram-plugin/dist/gateway/gateway.js +212 -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
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "switchroom",
|
|
3
3
|
"//version": "NOT the release version — source of truth is the git tag, resolved by scripts/build.mjs:resolveVersion() (see CLAUDE.md > Standard release process). This field is stale by design and only the Layer-4 dev/non-tag fallback for build.mjs + src/cli/resolve-version.ts; do NOT bump it expecting a release to pick it up. npm-pack tarball naming needs a real version — do that as an UNCOMMITTED pack-time bump (see release step 6), never a committed one.",
|
|
4
|
-
"version": "0.19.
|
|
4
|
+
"version": "0.19.34",
|
|
5
5
|
"description": "Run Claude Code 24/7 on your Claude Pro/Max subscription over Telegram. Open-source alternative to OpenClaw and NanoClaw — no API keys.",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"bin": {
|
|
@@ -21972,6 +21972,7 @@ var init_schema = __esm(() => {
|
|
|
21972
21972
|
}).describe("Per-operation LLM override. Every field optional; an unset field (or " + "an omitted op block) inherits the global `hindsight.llm.*`, which is " + "already the engine's fallback \u2014 switchroom emits only the vars set.");
|
|
21973
21973
|
HindsightConfigSchema = exports_external.object({
|
|
21974
21974
|
gpu: exports_external.boolean().optional().describe("Force GPU passthrough for the hindsight container on (`true`) or off " + "(`false`), overriding host autodetection in BOTH directions. Absent " + "(the default) \u2192 autodetect from the persisted host-capabilities " + "verdict (`~/.switchroom/host-capabilities.json`), which enables " + "`--gpus all` only when that file proves BOTH a GPU and the nvidia " + "container toolkit. Set `true` when that verdict is wrong or unreadable " + "and you know the host has a working toolkit \u2014 switchroom cannot verify " + "it for you, and `docker run --gpus all` hard-fails container create on " + "a host without one. Set `false` to pin the container to CPU on a GPU " + "host. This is also the declarative opt-out for the recreate-time GPU " + "drop guard (`switchroom memory setup --recreate` refuses to silently " + "turn a GPU container into a CPU one). `--gpu`/`--no-gpu` on `memory " + "setup` override this for a single run."),
|
|
21975
|
+
cp_access_key: exports_external.string().min(1).optional().describe("Access key for the hindsight control-plane DASHBOARD (upstream " + "`HINDSIGHT_CP_ACCESS_KEY`, port 9999). Literal or \u2014 strongly preferred " + "\u2014 a `vault:` reference such as `vault:hindsight_cp_access_key`, read " + "through the broker at container-launch time. This is the ONLY thing " + "that arms the dashboard's login: upstream's middleware short-circuits " + "to `next()` when the var is unset, so an unset key means the dashboard " + "has no authentication at all, not weak authentication. Because of that, " + "leaving it unset is FAIL-CLOSED: switchroom pins " + "`HINDSIGHT_CP_HOSTNAME=127.0.0.1` so the loginless dashboard is " + "reachable only from the host, and warns. Set this to serve the " + "dashboard on the LAN/tailnet."),
|
|
21975
21976
|
llm: exports_external.object({
|
|
21976
21977
|
provider: exports_external.string().min(1).optional().describe("Hindsight LLM provider (upstream `HINDSIGHT_API_LLM_PROVIDER`). " + "Defaults to `claude-code` (subscription-honest, broker-fed OAuth). " + "Any litellm-routable provider the upstream image supports is valid. " + "Serves as the GLOBAL default for every op absent a per-op override."),
|
|
21977
21978
|
model: exports_external.string().min(1).optional().describe("Hindsight LLM model (upstream `HINDSIGHT_API_LLM_MODEL`). Defaults " + "to HINDSIGHT_DEFAULT_MODEL. Any model your LiteLLM proxy can route " + "is valid, e.g. `openrouter/z-ai/glm-5.2` when routing through the " + "fleet proxy. With provider=claude-code this value is ALSO exported " + "as `ANTHROPIC_MODEL` to the claude subprocess. Serves as the GLOBAL " + "default for every op absent a per-op override."),
|
|
@@ -44732,7 +44733,7 @@ async function handleVoiceMessage(ctx, deps) {
|
|
|
44732
44733
|
}
|
|
44733
44734
|
|
|
44734
44735
|
// gateway/status-pin-store.ts
|
|
44735
|
-
var
|
|
44736
|
+
var SNAPSHOT_VERSION = 4;
|
|
44736
44737
|
function isPinRow(x) {
|
|
44737
44738
|
if (x == null || typeof x !== "object")
|
|
44738
44739
|
return false;
|
|
@@ -44757,12 +44758,12 @@ function loadStatusPins(path, fs2) {
|
|
|
44757
44758
|
if (parsed == null || typeof parsed !== "object")
|
|
44758
44759
|
return [];
|
|
44759
44760
|
const env = parsed;
|
|
44760
|
-
if (
|
|
44761
|
+
if (!Number.isInteger(env.v) || env.v < 1 || !Array.isArray(env.pins))
|
|
44761
44762
|
return [];
|
|
44762
44763
|
return env.pins.filter(isPinRow);
|
|
44763
44764
|
}
|
|
44764
44765
|
function persistStatusPins(path, fs2, snapshot, log = (l) => process.stderr.write(l)) {
|
|
44765
|
-
const env = { v:
|
|
44766
|
+
const env = { v: SNAPSHOT_VERSION, pins: [...snapshot] };
|
|
44766
44767
|
const tmp = path + ".tmp";
|
|
44767
44768
|
try {
|
|
44768
44769
|
fs2.writeFileSync(tmp, JSON.stringify(env));
|
|
@@ -84574,12 +84575,12 @@ init_paths();
|
|
|
84574
84575
|
var _warnedReads2 = new Set;
|
|
84575
84576
|
|
|
84576
84577
|
// ../src/setup/hindsight-pg-defaults.ts
|
|
84577
|
-
var HINDSIGHT_PG_MEM_LIMIT_MIB_FOR_DERIVATION =
|
|
84578
|
+
var HINDSIGHT_PG_MEM_LIMIT_MIB_FOR_DERIVATION = 16 * 1024;
|
|
84578
84579
|
var HINDSIGHT_PG_APP_ANON_MIB = 2560;
|
|
84579
84580
|
var HINDSIGHT_PG_PAGE_CACHE_FLOOR_MIB = 2048;
|
|
84580
84581
|
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 =
|
|
84582
|
+
var HINDSIGHT_PG_DEFAULT_SHARED_BUFFERS_MIB = 3072;
|
|
84583
|
+
var HINDSIGHT_PG_DEFAULT_EFFECTIVE_CACHE_SIZE_MIB = 7168;
|
|
84583
84584
|
function pgMib(mib) {
|
|
84584
84585
|
return `${mib}MB`;
|
|
84585
84586
|
}
|
|
@@ -84752,9 +84753,21 @@ var HINDSIGHT_RETAIN_CLIENT_DEADLINE_S = hindsightRetainClientTimeoutSeconds() +
|
|
|
84752
84753
|
var HINDSIGHT_HEALTHCHECK_PY = 'import urllib.request,sys; sys.exit(0 if urllib.request.urlopen("http://localhost:8888/health",timeout=4).getcode()==200 else 1)';
|
|
84753
84754
|
var HINDSIGHT_HEALTHCHECK_CMD = `python3 -c '${HINDSIGHT_HEALTHCHECK_PY}'`;
|
|
84754
84755
|
var DOCKER_PROBE_TIMEOUT_MS = 60 * 1000;
|
|
84756
|
+
var HINDSIGHT_CP_UNAUTHENTICATED_BIND_ADDR = "127.0.0.1";
|
|
84757
|
+
var HINDSIGHT_CP_NO_ACCESS_KEY_WARNING = "hindsight: no `hindsight.cp_access_key` configured \u2014 the control-plane " + "dashboard has NO login (the access-key middleware is inert when " + "HINDSIGHT_CP_ACCESS_KEY is unset), so it is pinned to " + `${HINDSIGHT_CP_UNAUTHENTICATED_BIND_ADDR} and will NOT be reachable from ` + "the LAN or tailnet. Set `hindsight.cp_access_key: vault:<key>` in " + "switchroom.yaml to arm the login and open it up.";
|
|
84755
84758
|
|
|
84756
84759
|
// ../src/memory/hindsight.ts
|
|
84757
|
-
var DEFAULT_RETAIN_MISSION = `Extract durable facts that will still be true and useful weeks from now
|
|
84760
|
+
var DEFAULT_RETAIN_MISSION = `Extract durable facts that will still be true and useful weeks from now, once this session is forgotten.
|
|
84761
|
+
` + `
|
|
84762
|
+
` + `DURABILITY GATE. Emit a candidate ONLY if it is one of these five classes:
|
|
84763
|
+
` + `- PREFERENCE \u2014 what the user likes, wants, or always does; a standing rule or correction.
|
|
84764
|
+
` + `- 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.
|
|
84765
|
+
` + `- FINDING \u2014 a root cause, a measurement, or verified behaviour of a system. Include the number.
|
|
84766
|
+
` + `- OUTCOME \u2014 a completed result that changed the world: what shipped, what a thing turned out to be. Not the act of shipping it.
|
|
84767
|
+
` + `- RELATIONSHIP \u2014 who a person is, what a project or tool is, and how they connect.
|
|
84768
|
+
` + `If a candidate fits none of the five, it is not a memory. Drop it; do not reword it into one.
|
|
84769
|
+
` + `
|
|
84770
|
+
` + `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
84771
|
` + `
|
|
84759
84772
|
` + `A TOOL RESULT IS NOT A FACT. Before extracting, ask: is the subject of this
|
|
84760
84773
|
` + `candidate a file path, a command/process/agent/session id, a temp directory, or
|
|
@@ -84771,6 +84784,9 @@ var DEFAULT_RETAIN_MISSION = `Extract durable facts that will still be true and
|
|
|
84771
84784
|
` + `- Anything mentioning a path under /tmp, a scratchpad directory, or a .tmp file.
|
|
84772
84785
|
` + `- Agent tool-use traces or narration of what the assistant did (e.g. "the
|
|
84773
84786
|
` + ` assistant used X to query Y", "ran a search", "sent the message").
|
|
84787
|
+
` + `- The act of delegating, dispatching, spawning, launching, steering or merging
|
|
84788
|
+
` + ` work \u2014 including when it succeeded. "X was dispatched and completed" is the
|
|
84789
|
+
` + ` session describing itself. Record only what the work LEARNED or CHANGED.
|
|
84774
84790
|
` + `- In-flight workflow/process narration (a sub-task started, paused, or is still
|
|
84775
84791
|
` + ` running) \u2014 retain the outcome only once the task completes or a decision is made.
|
|
84776
84792
|
` + `- Operation, request, batch, agent, command or session IDs, UUIDs, hashes, or error codes.
|
|
@@ -84793,8 +84809,12 @@ var DEFAULT_RETAIN_MISSION = `Extract durable facts that will still be true and
|
|
|
84793
84809
|
` + ` the fact is explicitly dated, in which case record it as a dated observation.
|
|
84794
84810
|
` + `- Greetings, acknowledgements, and routine operational chatter.
|
|
84795
84811
|
` + `
|
|
84812
|
+
` + `Write each fact so it stands alone: name the thing, the number, and the date. A
|
|
84813
|
+
` + `sentence that only makes sense while reading this transcript is not durable.
|
|
84814
|
+
` + `
|
|
84796
84815
|
` + `If a candidate fact matches an exclusion, drop it rather than rewording it. If
|
|
84797
|
-
` +
|
|
84816
|
+
` + `nothing durable remains, return an empty facts list.
|
|
84817
|
+
`;
|
|
84798
84818
|
var SUPERSEDED_RETAIN_MISSIONS = [
|
|
84799
84819
|
"Extract technical decisions, architectural choices, user preferences, project context, and people/tool relationships. Ignore routine greetings and transient operational details.",
|
|
84800
84820
|
"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 +84861,96 @@ var SUPERSEDED_RETAIN_MISSIONS = [
|
|
|
84841
84861
|
` + `- Greetings, acknowledgements, and routine operational chatter.
|
|
84842
84862
|
` + `
|
|
84843
84863
|
` + `If a candidate fact matches an exclusion, drop it rather than rewording it. If
|
|
84844
|
-
` + "nothing durable remains, return an empty facts list."
|
|
84864
|
+
` + "nothing durable remains, return an empty facts list.",
|
|
84865
|
+
`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.
|
|
84866
|
+
` + `
|
|
84867
|
+
` + `A TOOL RESULT IS NOT A FACT. Before extracting, ask: is the subject of this
|
|
84868
|
+
` + `candidate a file path, a command/process/agent/session id, a temp directory, or
|
|
84869
|
+
` + `the location where some output was written? If yes, drop it \u2014 it is transcript
|
|
84870
|
+
` + `exhaust, not memory.
|
|
84871
|
+
` + `
|
|
84872
|
+
` + `NEVER extract:
|
|
84873
|
+
` + `- Tool results verbatim or paraphrased. Concretely, never produce a fact whose
|
|
84874
|
+
` + ` text resembles any of these: "File created successfully at /path/to/file",
|
|
84875
|
+
` + ` "A background command with ID bctz4yskm is running, and its output will be
|
|
84876
|
+
` + ` written to /tmp/...", "Async agent a745598ba84e71df1 was launched successfully
|
|
84877
|
+
` + ` and is running in the background", "User executed a Bash command to sleep for
|
|
84878
|
+
` + ` 200 seconds", "The assistant used grep to locate 'truncateSync' in src/foo.ts".
|
|
84879
|
+
` + `- Anything mentioning a path under /tmp, a scratchpad directory, or a .tmp file.
|
|
84880
|
+
` + `- Agent tool-use traces or narration of what the assistant did (e.g. "the
|
|
84881
|
+
` + ` assistant used X to query Y", "ran a search", "sent the message").
|
|
84882
|
+
` + `- In-flight workflow/process narration (a sub-task started, paused, or is still
|
|
84883
|
+
` + ` running) \u2014 retain the outcome only once the task completes or a decision is made.
|
|
84884
|
+
` + `- Operation, request, batch, agent, command or session IDs, UUIDs, hashes, or error codes.
|
|
84885
|
+
` + `- Slash commands the user typed and their effects (e.g. "User issued /clear to
|
|
84886
|
+
` + ` reset assistant state").
|
|
84887
|
+
` + `- Hindsight's own errors, retries, backlogs, or internal state \u2014 the memory
|
|
84888
|
+
` + ` system's self-reports are not memories.
|
|
84889
|
+
` + `- Restatements of the user's current request or the task in progress.
|
|
84890
|
+
` + `- Volatile state written as a timeless assertion. A version, count, size,
|
|
84891
|
+
` + ` backlog, status, or any "X is running Y" / "X is at Y" / "X is currently Y"
|
|
84892
|
+
` + ` claim is true only at the instant it was said. Concretely, never produce a
|
|
84893
|
+
` + ` fact whose text resembles any of these: "Switchroom fleet is running image
|
|
84894
|
+
` + ` version v0.18.19", "The switchroom repo is at /path/to/fleet, version
|
|
84895
|
+
` + ` v0.19.5", "Bank overlord has 43155 pending consolidations", "The build is
|
|
84896
|
+
` + ` currently green". If the claim is worth keeping, put the date INSIDE the
|
|
84897
|
+
` + ` fact text ("As of 2026-07-19 the fleet was running v0.18.19"); if you
|
|
84898
|
+
` + ` cannot date it, drop it. An undated one is recalled forever as though it
|
|
84899
|
+
` + ` were still true, which is worse than not remembering it at all.
|
|
84900
|
+
` + `- Transient state (unread counts, build status, what is running right now) unless
|
|
84901
|
+
` + ` the fact is explicitly dated, in which case record it as a dated observation.
|
|
84902
|
+
` + `- Greetings, acknowledgements, and routine operational chatter.
|
|
84903
|
+
` + `
|
|
84904
|
+
` + `If a candidate fact matches an exclusion, drop it rather than rewording it. If
|
|
84905
|
+
` + "nothing durable remains, return an empty facts list.",
|
|
84906
|
+
`Extract durable facts that will still be true and useful weeks from now, once this session is forgotten.
|
|
84907
|
+
` + `
|
|
84908
|
+
` + `DURABILITY GATE. Emit a candidate ONLY if it is one of these five classes:
|
|
84909
|
+
` + `- PREFERENCE \u2014 what the user likes, wants, or always does; a standing rule or correction.
|
|
84910
|
+
` + `- 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.
|
|
84911
|
+
` + `- FINDING \u2014 a root cause, a measurement, or verified behaviour of a system. Include the number.
|
|
84912
|
+
` + `- OUTCOME \u2014 a completed result that changed the world: what shipped, what a thing turned out to be. Not the act of shipping it.
|
|
84913
|
+
` + `- RELATIONSHIP \u2014 who a person is, what a project or tool is, and how they connect.
|
|
84914
|
+
` + `If a candidate fits none of the five, it is not a memory. Drop it; do not reword it into one.
|
|
84915
|
+
` + `
|
|
84916
|
+
` + `A preference revealed by a request is durable \u2014 record the preference (what the user likes, wants, or always does), not the request itself.
|
|
84917
|
+
` + `
|
|
84918
|
+
` + `A TOOL RESULT IS NOT A FACT. Before extracting, ask: is the subject of this
|
|
84919
|
+
` + `candidate a file path, a command/process/agent/session id, a temp directory, or
|
|
84920
|
+
` + `the location where some output was written? If yes, drop it \u2014 it is transcript
|
|
84921
|
+
` + `exhaust, not memory.
|
|
84922
|
+
` + `
|
|
84923
|
+
` + `NEVER extract:
|
|
84924
|
+
` + `- Tool results verbatim or paraphrased. Concretely, never produce a fact whose
|
|
84925
|
+
` + ` text resembles any of these: "File created successfully at /path/to/file",
|
|
84926
|
+
` + ` "A background command with ID bctz4yskm is running, and its output will be
|
|
84927
|
+
` + ` written to /tmp/...", "Async agent a745598ba84e71df1 was launched successfully
|
|
84928
|
+
` + ` and is running in the background", "User executed a Bash command to sleep for
|
|
84929
|
+
` + ` 200 seconds", "The assistant used grep to locate 'truncateSync' in src/foo.ts".
|
|
84930
|
+
` + `- Anything mentioning a path under /tmp, a scratchpad directory, or a .tmp file.
|
|
84931
|
+
` + `- Agent tool-use traces or narration of what the assistant did (e.g. "the
|
|
84932
|
+
` + ` assistant used X to query Y", "ran a search", "sent the message").
|
|
84933
|
+
` + `- The act of delegating, dispatching, spawning, launching, steering or merging
|
|
84934
|
+
` + ` work \u2014 including when it succeeded. "X was dispatched and completed" is the
|
|
84935
|
+
` + ` session describing itself. Record only what the work LEARNED or CHANGED.
|
|
84936
|
+
` + `- In-flight workflow/process narration (a sub-task started, paused, or is still
|
|
84937
|
+
` + ` running) \u2014 retain the outcome only once the task completes or a decision is made.
|
|
84938
|
+
` + `- Operation, request, batch, agent, command or session IDs, UUIDs, hashes, or error codes.
|
|
84939
|
+
` + `- Slash commands the user typed and their effects (e.g. "User issued /clear to
|
|
84940
|
+
` + ` reset assistant state").
|
|
84941
|
+
` + `- Hindsight's own errors, retries, backlogs, or internal state \u2014 the memory
|
|
84942
|
+
` + ` system's self-reports are not memories.
|
|
84943
|
+
` + `- Restatements of the user's current request or the task in progress.
|
|
84944
|
+
` + `- Transient state (unread counts, build status, what is running right now) unless
|
|
84945
|
+
` + ` the fact is explicitly dated, in which case record it as a dated observation.
|
|
84946
|
+
` + `- Greetings, acknowledgements, and routine operational chatter.
|
|
84947
|
+
` + `
|
|
84948
|
+
` + `Write each fact so it stands alone: name the thing, the number, and the date. A
|
|
84949
|
+
` + `sentence that only makes sense while reading this transcript is not durable.
|
|
84950
|
+
` + `
|
|
84951
|
+
` + `If a candidate fact matches an exclusion, drop it rather than rewording it. If
|
|
84952
|
+
` + `nothing durable remains, return an empty facts list.
|
|
84953
|
+
`
|
|
84845
84954
|
];
|
|
84846
84955
|
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
84956
|
` + `
|
|
@@ -86523,7 +86632,7 @@ function shouldSweepChatAtBoot(chatId) {
|
|
|
86523
86632
|
|
|
86524
86633
|
// gateway/stale-pin-sweep-store.ts
|
|
86525
86634
|
var SWEEP_MAX_ATTEMPTS = 8;
|
|
86526
|
-
var
|
|
86635
|
+
var SWEEP_ENVELOPE_VERSION = 1;
|
|
86527
86636
|
function sweepTargetKey(chatId, threadId) {
|
|
86528
86637
|
return `${chatId}:${threadId ?? "-"}`;
|
|
86529
86638
|
}
|
|
@@ -86551,13 +86660,13 @@ function loadSweepCursors(path2, fs2) {
|
|
|
86551
86660
|
if (parsed == null || typeof parsed !== "object")
|
|
86552
86661
|
return [];
|
|
86553
86662
|
const env = parsed;
|
|
86554
|
-
if (
|
|
86663
|
+
if (!Number.isInteger(env.v) || env.v < 1 || !Array.isArray(env.cursors)) {
|
|
86555
86664
|
return [];
|
|
86556
86665
|
}
|
|
86557
86666
|
return env.cursors.filter(isCursorRow);
|
|
86558
86667
|
}
|
|
86559
86668
|
function persistSweepCursors(path2, fs2, cursors, log = (l) => process.stderr.write(l)) {
|
|
86560
|
-
const env = { v:
|
|
86669
|
+
const env = { v: SWEEP_ENVELOPE_VERSION, cursors: [...cursors] };
|
|
86561
86670
|
try {
|
|
86562
86671
|
fs2.writeFileSync(path2, JSON.stringify(env));
|
|
86563
86672
|
} catch (err) {
|
|
@@ -86978,11 +87087,13 @@ function createStalePinSweeper(deps) {
|
|
|
86978
87087
|
const repinUnpinLoop = async (target, kind, cursor) => {
|
|
86979
87088
|
const gate = gateFor(kind);
|
|
86980
87089
|
let popped = 0;
|
|
87090
|
+
let issued = 0;
|
|
86981
87091
|
let consecutiveFloods = 0;
|
|
86982
87092
|
let lastRetryAfter = null;
|
|
86983
87093
|
let noProgressAtSameRetryAfter = 0;
|
|
86984
87094
|
let lastObservedTop = null;
|
|
86985
87095
|
let unverifiedPop = false;
|
|
87096
|
+
const out = (r) => ({ ...r, issued });
|
|
86986
87097
|
const creditPop = () => {
|
|
86987
87098
|
popped++;
|
|
86988
87099
|
cursor.popped++;
|
|
@@ -86991,29 +87102,29 @@ function createStalePinSweeper(deps) {
|
|
|
86991
87102
|
};
|
|
86992
87103
|
for (let i = 0;i < gate.maxPopsPerChatPerSweep; i++) {
|
|
86993
87104
|
if (circuitOpen) {
|
|
86994
|
-
return { status: "aborted-circuit-breaker", popped, detail: "breaker open" };
|
|
87105
|
+
return out({ status: "aborted-circuit-breaker", popped, detail: "breaker open" });
|
|
86995
87106
|
}
|
|
86996
87107
|
const observed = await readTopVerified(target.chatId);
|
|
86997
87108
|
if (observed == null) {
|
|
86998
|
-
return { status: "incomplete", popped, detail: "pin-stack read unverifiable" };
|
|
87109
|
+
return out({ status: "incomplete", popped, detail: "pin-stack read unverifiable" });
|
|
86999
87110
|
}
|
|
87000
87111
|
if (unverifiedPop) {
|
|
87001
87112
|
if (observed.top != null && observed.top === lastObservedTop) {
|
|
87002
|
-
return {
|
|
87113
|
+
return out({
|
|
87003
87114
|
status: "incomplete",
|
|
87004
87115
|
popped,
|
|
87005
87116
|
detail: `no progress: message ${observed.top} still on top after a repin+unpin`
|
|
87006
|
-
};
|
|
87117
|
+
});
|
|
87007
87118
|
}
|
|
87008
87119
|
creditPop();
|
|
87009
87120
|
unverifiedPop = false;
|
|
87010
87121
|
}
|
|
87011
87122
|
if (observed.top == null) {
|
|
87012
|
-
return { status: "drained", popped };
|
|
87123
|
+
return out({ status: "drained", popped });
|
|
87013
87124
|
}
|
|
87014
87125
|
lastObservedTop = observed.top;
|
|
87015
87126
|
if (!await awaitWriteSlot(kind)) {
|
|
87016
|
-
return { status: "deferred-budget", popped, detail: "per-minute pin-op budget spent" };
|
|
87127
|
+
return out({ status: "deferred-budget", popped, detail: "per-minute pin-op budget spent" });
|
|
87017
87128
|
}
|
|
87018
87129
|
try {
|
|
87019
87130
|
await deps.pinSilent(target.chatId, observed.top);
|
|
@@ -87021,10 +87132,10 @@ function createStalePinSweeper(deps) {
|
|
|
87021
87132
|
const d = classify(err);
|
|
87022
87133
|
if (d.kind === "breaker") {
|
|
87023
87134
|
circuitOpen = true;
|
|
87024
|
-
return { status: "aborted-circuit-breaker", popped, detail: d.detail };
|
|
87135
|
+
return out({ status: "aborted-circuit-breaker", popped, detail: d.detail });
|
|
87025
87136
|
}
|
|
87026
87137
|
if (d.kind === "rights")
|
|
87027
|
-
return { status: "skipped-no-rights", popped };
|
|
87138
|
+
return out({ status: "skipped-no-rights", popped });
|
|
87028
87139
|
if (d.kind === "flood") {
|
|
87029
87140
|
consecutiveFloods++;
|
|
87030
87141
|
if (lastRetryAfter === d.seconds)
|
|
@@ -87033,19 +87144,19 @@ function createStalePinSweeper(deps) {
|
|
|
87033
87144
|
noProgressAtSameRetryAfter = 1;
|
|
87034
87145
|
lastRetryAfter = d.seconds;
|
|
87035
87146
|
if (consecutiveFloods >= MAX_CONSECUTIVE_FLOOD_WAITS || noProgressAtSameRetryAfter >= MAX_IDENTICAL_RETRY_AFTER_NO_PROGRESS) {
|
|
87036
|
-
return {
|
|
87147
|
+
return out({
|
|
87037
87148
|
status: "deferred-flood",
|
|
87038
87149
|
popped,
|
|
87039
87150
|
detail: `flood: ${consecutiveFloods} consecutive 429s, retry_after=${d.seconds}s`
|
|
87040
|
-
};
|
|
87151
|
+
});
|
|
87041
87152
|
}
|
|
87042
87153
|
await deps.sleep(d.seconds * 1000 * Math.pow(FLOOD_BACKOFF_FACTOR, consecutiveFloods - 1));
|
|
87043
87154
|
continue;
|
|
87044
87155
|
}
|
|
87045
|
-
return { status: "error", popped, detail: d.detail };
|
|
87156
|
+
return out({ status: "error", popped, detail: d.detail });
|
|
87046
87157
|
}
|
|
87047
87158
|
if (!await awaitWriteSlot(kind)) {
|
|
87048
|
-
return { status: "deferred-budget", popped, detail: "per-minute pin-op budget spent" };
|
|
87159
|
+
return out({ status: "deferred-budget", popped, detail: "per-minute pin-op budget spent" });
|
|
87049
87160
|
}
|
|
87050
87161
|
try {
|
|
87051
87162
|
await deps.unpin(target.chatId, observed.top);
|
|
@@ -87053,12 +87164,12 @@ function createStalePinSweeper(deps) {
|
|
|
87053
87164
|
const d = classify(err);
|
|
87054
87165
|
if (d.kind === "breaker") {
|
|
87055
87166
|
circuitOpen = true;
|
|
87056
|
-
return { status: "aborted-circuit-breaker", popped, detail: d.detail };
|
|
87167
|
+
return out({ status: "aborted-circuit-breaker", popped, detail: d.detail });
|
|
87057
87168
|
}
|
|
87058
87169
|
if (d.kind === "rights")
|
|
87059
|
-
return { status: "skipped-no-rights", popped };
|
|
87170
|
+
return out({ status: "skipped-no-rights", popped });
|
|
87060
87171
|
if (isNothingToUnpinError(err))
|
|
87061
|
-
return { status: "drained", popped };
|
|
87172
|
+
return out({ status: "drained", popped });
|
|
87062
87173
|
if (d.kind === "flood") {
|
|
87063
87174
|
consecutiveFloods++;
|
|
87064
87175
|
if (lastRetryAfter === d.seconds)
|
|
@@ -87067,30 +87178,31 @@ function createStalePinSweeper(deps) {
|
|
|
87067
87178
|
noProgressAtSameRetryAfter = 1;
|
|
87068
87179
|
lastRetryAfter = d.seconds;
|
|
87069
87180
|
if (consecutiveFloods >= MAX_CONSECUTIVE_FLOOD_WAITS || noProgressAtSameRetryAfter >= MAX_IDENTICAL_RETRY_AFTER_NO_PROGRESS) {
|
|
87070
|
-
return {
|
|
87181
|
+
return out({
|
|
87071
87182
|
status: "deferred-flood",
|
|
87072
87183
|
popped,
|
|
87073
87184
|
detail: `flood: ${consecutiveFloods} consecutive 429s, retry_after=${d.seconds}s`
|
|
87074
|
-
};
|
|
87185
|
+
});
|
|
87075
87186
|
}
|
|
87076
87187
|
await deps.sleep(d.seconds * 1000 * Math.pow(FLOOD_BACKOFF_FACTOR, consecutiveFloods - 1));
|
|
87077
87188
|
continue;
|
|
87078
87189
|
}
|
|
87079
|
-
return { status: "error", popped, detail: d.detail };
|
|
87190
|
+
return out({ status: "error", popped, detail: d.detail });
|
|
87080
87191
|
}
|
|
87081
87192
|
consecutiveFloods = 0;
|
|
87193
|
+
issued++;
|
|
87082
87194
|
unverifiedPop = true;
|
|
87083
87195
|
}
|
|
87084
87196
|
const finalRead = await readTopVerified(target.chatId);
|
|
87085
87197
|
if (unverifiedPop && finalRead != null && finalRead.top !== lastObservedTop)
|
|
87086
87198
|
creditPop();
|
|
87087
87199
|
if (finalRead != null && finalRead.top == null)
|
|
87088
|
-
return { status: "drained", popped };
|
|
87089
|
-
return {
|
|
87200
|
+
return out({ status: "drained", popped });
|
|
87201
|
+
return out({
|
|
87090
87202
|
status: "deferred-budget",
|
|
87091
87203
|
popped,
|
|
87092
87204
|
detail: `hit the ${gate.maxPopsPerChatPerSweep}-pop cap for this chat; yielding to next boot`
|
|
87093
|
-
};
|
|
87205
|
+
});
|
|
87094
87206
|
};
|
|
87095
87207
|
const targetedUnpinDrain = async (target, kind, cursor, ids) => {
|
|
87096
87208
|
const alreadyDone = new Set(cursor.doneIds ?? []);
|
|
@@ -87187,10 +87299,21 @@ function createStalePinSweeper(deps) {
|
|
|
87187
87299
|
const forumTopicDrain = async (target, cursor) => {
|
|
87188
87300
|
const threadId = target.threadId;
|
|
87189
87301
|
if (threadId == null) {
|
|
87190
|
-
return {
|
|
87302
|
+
return {
|
|
87303
|
+
status: "skipped-nothing-recorded",
|
|
87304
|
+
popped: 0,
|
|
87305
|
+
issued: 0,
|
|
87306
|
+
detail: "no message_thread_id"
|
|
87307
|
+
};
|
|
87191
87308
|
}
|
|
87309
|
+
const before = await readTopVerified(target.chatId);
|
|
87192
87310
|
if (!await awaitWriteSlot("forum-topic")) {
|
|
87193
|
-
return {
|
|
87311
|
+
return {
|
|
87312
|
+
status: "deferred-budget",
|
|
87313
|
+
popped: 0,
|
|
87314
|
+
issued: 0,
|
|
87315
|
+
detail: "per-minute pin-op budget spent"
|
|
87316
|
+
};
|
|
87194
87317
|
}
|
|
87195
87318
|
try {
|
|
87196
87319
|
await deps.unpinAllForumTopicMessages(target.chatId, threadId);
|
|
@@ -87198,39 +87321,59 @@ function createStalePinSweeper(deps) {
|
|
|
87198
87321
|
const d = classify(err);
|
|
87199
87322
|
if (d.kind === "breaker") {
|
|
87200
87323
|
circuitOpen = true;
|
|
87201
|
-
return { status: "aborted-circuit-breaker", popped: 0, detail: d.detail };
|
|
87324
|
+
return { status: "aborted-circuit-breaker", popped: 0, issued: 0, detail: d.detail };
|
|
87202
87325
|
}
|
|
87203
87326
|
if (d.kind === "rights")
|
|
87204
|
-
return { status: "skipped-no-rights", popped: 0 };
|
|
87327
|
+
return { status: "skipped-no-rights", popped: 0, issued: 0 };
|
|
87205
87328
|
if (d.kind === "flood") {
|
|
87206
87329
|
return {
|
|
87207
87330
|
status: "deferred-flood",
|
|
87208
87331
|
popped: 0,
|
|
87332
|
+
issued: 0,
|
|
87209
87333
|
detail: `unpinAllForumTopicMessages pegged at retry_after=${d.seconds}s; not looping`
|
|
87210
87334
|
};
|
|
87211
87335
|
}
|
|
87212
|
-
return { status: "error", popped: 0, detail: d.detail };
|
|
87336
|
+
return { status: "error", popped: 0, issued: 0, detail: d.detail };
|
|
87213
87337
|
}
|
|
87214
|
-
cursor.popped++;
|
|
87215
|
-
cursor.updatedAt = deps.now();
|
|
87216
|
-
commit(cursor);
|
|
87217
87338
|
const observed = await readTopVerified(target.chatId);
|
|
87218
|
-
|
|
87219
|
-
|
|
87220
|
-
|
|
87339
|
+
const topMoved = before != null && observed != null && observed.top !== before.top ? 1 : 0;
|
|
87340
|
+
if (topMoved > 0) {
|
|
87341
|
+
cursor.popped += topMoved;
|
|
87342
|
+
cursor.updatedAt = deps.now();
|
|
87343
|
+
commit(cursor);
|
|
87344
|
+
}
|
|
87345
|
+
if (observed != null && observed.top == null) {
|
|
87346
|
+
return { status: "drained", popped: topMoved, issued: 1 };
|
|
87347
|
+
}
|
|
87348
|
+
return {
|
|
87349
|
+
status: "drained",
|
|
87350
|
+
popped: topMoved,
|
|
87351
|
+
issued: 1,
|
|
87352
|
+
detail: "topic drained; chat-wide stack may hold other topics"
|
|
87353
|
+
};
|
|
87221
87354
|
};
|
|
87222
87355
|
const sweepOnce = async (target) => {
|
|
87223
87356
|
if (!deps.eligible())
|
|
87224
|
-
return { status: "skipped-not-eligible", popped: 0 };
|
|
87357
|
+
return { status: "skipped-not-eligible", popped: 0, issued: 0 };
|
|
87225
87358
|
if (circuitOpen) {
|
|
87226
|
-
return {
|
|
87359
|
+
return {
|
|
87360
|
+
status: "aborted-circuit-breaker",
|
|
87361
|
+
popped: 0,
|
|
87362
|
+
issued: 0,
|
|
87363
|
+
detail: "breaker already open"
|
|
87364
|
+
};
|
|
87227
87365
|
}
|
|
87228
87366
|
const kind = classifyChatForSweep(target);
|
|
87229
87367
|
const cursor = loadCursor(target, kind);
|
|
87230
87368
|
if (cursor.done)
|
|
87231
|
-
return { status: "already-drained", popped: 0 };
|
|
87369
|
+
return { status: "already-drained", popped: 0, issued: 0 };
|
|
87232
87370
|
if (cursor.attempts >= SWEEP_MAX_ATTEMPTS) {
|
|
87233
|
-
return {
|
|
87371
|
+
return {
|
|
87372
|
+
status: "forfeited",
|
|
87373
|
+
popped: 0,
|
|
87374
|
+
issued: 0,
|
|
87375
|
+
detail: `${cursor.attempts} attempts exhausted`
|
|
87376
|
+
};
|
|
87234
87377
|
}
|
|
87235
87378
|
if (kind !== "dm") {
|
|
87236
87379
|
let allowed = false;
|
|
@@ -87244,7 +87387,7 @@ function createStalePinSweeper(deps) {
|
|
|
87244
87387
|
cursor.lastStatus = "skipped-no-rights";
|
|
87245
87388
|
cursor.updatedAt = deps.now();
|
|
87246
87389
|
commit(cursor);
|
|
87247
|
-
return { status: "skipped-no-rights", popped: 0 };
|
|
87390
|
+
return { status: "skipped-no-rights", popped: 0, issued: 0 };
|
|
87248
87391
|
}
|
|
87249
87392
|
}
|
|
87250
87393
|
cursor.attempts++;
|
|
@@ -87273,7 +87416,7 @@ function createStalePinSweeper(deps) {
|
|
|
87273
87416
|
} : await targetedUnpinDrain(target, kind, cursor, ids);
|
|
87274
87417
|
}
|
|
87275
87418
|
} catch (err) {
|
|
87276
|
-
result = { status: "error", popped: 0, detail: err.message };
|
|
87419
|
+
result = { status: "error", popped: 0, issued: 0, detail: err.message };
|
|
87277
87420
|
}
|
|
87278
87421
|
if (result.status === "drained" || result.status === "skipped-nothing-recorded") {
|
|
87279
87422
|
cursor.done = true;
|
|
@@ -87306,10 +87449,12 @@ function createStalePinSweeper(deps) {
|
|
|
87306
87449
|
const running = inFlight.get(key);
|
|
87307
87450
|
if (running != null)
|
|
87308
87451
|
return running;
|
|
87309
|
-
if (!deps.eligible())
|
|
87310
|
-
return Promise.resolve({ status: "skipped-not-eligible", popped: 0 });
|
|
87311
|
-
|
|
87312
|
-
|
|
87452
|
+
if (!deps.eligible()) {
|
|
87453
|
+
return Promise.resolve({ status: "skipped-not-eligible", popped: 0, issued: 0 });
|
|
87454
|
+
}
|
|
87455
|
+
if (attempted.has(key)) {
|
|
87456
|
+
return Promise.resolve({ status: "already-attempted", popped: 0, issued: 0 });
|
|
87457
|
+
}
|
|
87313
87458
|
attempted.add(key);
|
|
87314
87459
|
const p = sweepOnce(target).finally(() => inFlight.delete(key));
|
|
87315
87460
|
inFlight.set(key, p);
|
|
@@ -90214,7 +90359,7 @@ function createCapturedResumeDispatcher(ports) {
|
|
|
90214
90359
|
|
|
90215
90360
|
// gateway/status-pin-store.ts
|
|
90216
90361
|
var BOOT_UNPIN_MAX_ATTEMPTS = 5;
|
|
90217
|
-
var
|
|
90362
|
+
var SNAPSHOT_VERSION2 = 4;
|
|
90218
90363
|
function isPinRow2(x) {
|
|
90219
90364
|
if (x == null || typeof x !== "object")
|
|
90220
90365
|
return false;
|
|
@@ -90239,12 +90384,12 @@ function loadStatusPins2(path3, fs3) {
|
|
|
90239
90384
|
if (parsed == null || typeof parsed !== "object")
|
|
90240
90385
|
return [];
|
|
90241
90386
|
const env = parsed;
|
|
90242
|
-
if (
|
|
90387
|
+
if (!Number.isInteger(env.v) || env.v < 1 || !Array.isArray(env.pins))
|
|
90243
90388
|
return [];
|
|
90244
90389
|
return env.pins.filter(isPinRow2);
|
|
90245
90390
|
}
|
|
90246
90391
|
function persistStatusPins2(path3, fs3, snapshot, log = (l) => process.stderr.write(l)) {
|
|
90247
|
-
const env = { v:
|
|
90392
|
+
const env = { v: SNAPSHOT_VERSION2, pins: [...snapshot] };
|
|
90248
90393
|
const tmp = path3 + ".tmp";
|
|
90249
90394
|
try {
|
|
90250
90395
|
fs3.writeFileSync(tmp, JSON.stringify(env));
|
|
@@ -96545,6 +96690,12 @@ function parseAuditLine(line) {
|
|
|
96545
96690
|
entry.m = o.m;
|
|
96546
96691
|
if (typeof o.prior_pin === "string")
|
|
96547
96692
|
entry.prior_pin = o.prior_pin;
|
|
96693
|
+
if (Array.isArray(o.prior_pin_observed) && o.prior_pin_observed.every((v) => typeof v === "string")) {
|
|
96694
|
+
entry.prior_pin_observed = o.prior_pin_observed;
|
|
96695
|
+
}
|
|
96696
|
+
if (typeof o.prior_pin_source === "string") {
|
|
96697
|
+
entry.prior_pin_source = o.prior_pin_source;
|
|
96698
|
+
}
|
|
96548
96699
|
return entry;
|
|
96549
96700
|
}
|
|
96550
96701
|
function filterEntries(entries, filters) {
|
|
@@ -99169,10 +99320,10 @@ function startOutboxSweep(deps) {
|
|
|
99169
99320
|
}
|
|
99170
99321
|
|
|
99171
99322
|
// ../src/build-info.ts
|
|
99172
|
-
var VERSION2 = "0.19.
|
|
99173
|
-
var COMMIT_SHA = "
|
|
99174
|
-
var COMMIT_DATE = "2026-07-
|
|
99175
|
-
var LATEST_PR =
|
|
99323
|
+
var VERSION2 = "0.19.34";
|
|
99324
|
+
var COMMIT_SHA = "73a1b218";
|
|
99325
|
+
var COMMIT_DATE = "2026-07-29T05:33:45Z";
|
|
99326
|
+
var LATEST_PR = 3981;
|
|
99176
99327
|
var COMMITS_AHEAD_OF_TAG = 0;
|
|
99177
99328
|
|
|
99178
99329
|
// 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) {
|