switchroom 0.19.22 → 0.19.23
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 +2 -1
- package/dist/auth-broker/index.js +68 -1
- package/dist/cli/notion-write-pretool.mjs +2 -1
- package/dist/cli/switchroom.js +552 -320
- package/dist/host-control/main.js +69 -2
- package/dist/vault/approvals/kernel-server.js +71 -4
- package/dist/vault/broker/server.js +71 -4
- package/package.json +5 -4
- package/profiles/_base/start.sh.hbs +101 -0
- package/profiles/_shared/agent-self-service.md.hbs +64 -109
- package/profiles/_shared/delegation-golden-rule.md.hbs +5 -5
- package/profiles/_shared/dev-protocol.md.hbs +13 -42
- package/profiles/_shared/execution-discipline.md.hbs +7 -14
- package/profiles/coding/CLAUDE.md.hbs +0 -6
- package/profiles/default/CLAUDE.md.hbs +21 -50
- package/skills/dev-protocol/SKILL.md +90 -107
- package/telegram-plugin/bunfig.toml +10 -0
- package/telegram-plugin/dist/gateway/gateway.js +108 -16
- package/telegram-plugin/gateway/backstop-delivery.ts +97 -16
- package/telegram-plugin/gateway/captured-answer-resume.ts +46 -17
- package/telegram-plugin/gateway/gateway.ts +9 -7
- package/telegram-plugin/gateway/outbound-send-path.ts +8 -1
- package/telegram-plugin/gateway/stream-render.ts +6 -0
- package/telegram-plugin/gateway/turn-record-status.ts +19 -0
- package/telegram-plugin/gateway/turns-jsonl-rotate.ts +65 -0
- package/telegram-plugin/tests/agent-state-dir-preload.test.ts +33 -0
- package/telegram-plugin/tests/backstop-delivery.test.ts +204 -7
- package/telegram-plugin/tests/backstop-readback-probe.test.ts +12 -0
- package/telegram-plugin/tests/captured-answer-resume.test.ts +104 -0
- package/telegram-plugin/tests/turns-jsonl-rotate.test.ts +92 -1
- package/vendor/hindsight-memory/scripts/drain_pending.py +113 -11
- package/vendor/hindsight-memory/scripts/lib/pending.py +802 -65
- package/vendor/hindsight-memory/scripts/lib/retain_split.py +54 -7
- package/vendor/hindsight-memory/scripts/tests/test_pending_drops.py +1445 -11
- package/vendor/hindsight-memory/scripts/tests/test_retain_split.py +78 -6
- package/vendor/hindsight-memory/tests/test_drain_pending.py +17 -2
- package/vendor/hindsight-memory/tests/test_pending.py +12 -4
|
@@ -13,51 +13,14 @@ description: >
|
|
|
13
13
|
|
|
14
14
|
# Development Protocol — the playbook
|
|
15
15
|
|
|
16
|
-
The always-loaded CLAUDE.md "Development Protocol" section
|
|
17
|
-
This skill
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
- **Read the real source.** The repo's source files at the current HEAD — not
|
|
25
|
-
build artifacts (`dist/`, generated files, caches), not your memory of the
|
|
26
|
-
code, not the task description's paraphrase. If a claim matters, open the
|
|
27
|
-
file.
|
|
28
|
-
- **Verify the root cause, not the first plausible cause.** Reproduce or trace
|
|
29
|
-
the failure to a specific mechanism before fixing. "This line looks wrong"
|
|
30
|
-
is a lead; the fix ships only when you can say *why* it produced the
|
|
31
|
-
observed symptom.
|
|
32
|
-
- **Report contradicting evidence.** If what you find contradicts the task
|
|
33
|
-
description, the ticket, or your own working theory — say so explicitly and
|
|
34
|
-
stop to re-plan. Never force-fit evidence to the plan you already had.
|
|
35
|
-
- **Cite everything.** Claims about the codebase carry `file:line`, commit
|
|
36
|
-
hashes, or PR numbers. "The scaffold appends fragments at
|
|
37
|
-
`src/agents/scaffold.ts:4113`" is a claim; "the scaffold appends fragments
|
|
38
|
-
somewhere" is not.
|
|
39
|
-
|
|
40
|
-
## 2. Clarify vs proceed
|
|
41
|
-
|
|
42
|
-
- **Infer first.** Most questions are answerable from the codebase, git
|
|
43
|
-
history, existing tests, or docs. Exhaust those before asking.
|
|
44
|
-
- **One question at a time.** If genuinely unsure after inferring, ask the
|
|
45
|
-
single question whose answer unblocks the most work. Phrase it as a
|
|
46
|
-
decision with a default: state what you found, the 2–3 viable options, which
|
|
47
|
-
you'd pick and why, and ask for confirmation — e.g. *"The config loader
|
|
48
|
-
supports both YAML and JSON overlays; the task says 'config file' without
|
|
49
|
-
specifying. I'd extend the YAML path since all existing overlays are YAML
|
|
50
|
-
(src/config/merge.ts:88) — confirm, or should JSON be covered too?"* Never
|
|
51
|
-
send a questionnaire.
|
|
52
|
-
- **Phase discipline.** Clarify during *planning*. Once the plan is agreed,
|
|
53
|
-
execute autonomously: make the reasonable call on small ambiguities, record
|
|
54
|
-
the assumption in your report, and keep moving. Mid-execution questions are
|
|
55
|
-
reserved for discoveries that invalidate the plan.
|
|
56
|
-
|
|
57
|
-
## 3. Design-align on larger tasks
|
|
58
|
-
|
|
59
|
-
**Classify the task first.** Treat it as "larger" (design-align before
|
|
60
|
-
implementing) when ANY of these hold:
|
|
16
|
+
The always-loaded CLAUDE.md "Development Protocol" section carries the
|
|
17
|
+
judgement criteria. This skill carries the parts that are *this fleet's
|
|
18
|
+
specific opinion* — the ones you would get wrong by defaulting to generic
|
|
19
|
+
good practice, because our answer differs from the obvious one.
|
|
20
|
+
|
|
21
|
+
## 1. Is this a "larger" task? (decides whether you design-align first)
|
|
22
|
+
|
|
23
|
+
Treat it as larger — design report before implementing — when ANY of these hold:
|
|
61
24
|
|
|
62
25
|
- It changes a public interface, schema, config shape, or on-disk format.
|
|
63
26
|
- It cuts across 3+ modules or touches a load-bearing invariant.
|
|
@@ -67,65 +30,85 @@ implementing) when ANY of these hold:
|
|
|
67
30
|
("add flag Y").
|
|
68
31
|
- It will land as more than one PR.
|
|
69
32
|
|
|
70
|
-
Small, single-concern, obvious-approach changes skip straight to
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
- **
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
33
|
+
Small, single-concern, obvious-approach changes skip straight to the pipeline.
|
|
34
|
+
Design-aligning a one-liner is its own failure mode.
|
|
35
|
+
|
|
36
|
+
## 2. Design report + red-team (larger tasks only)
|
|
37
|
+
|
|
38
|
+
The report states what exists today **with citations**, what will change, the
|
|
39
|
+
chosen approach, the alternatives you rejected and why, and the PR staging
|
|
40
|
+
plan. Get alignment before implementing.
|
|
41
|
+
|
|
42
|
+
Then red-team your own plan item by item. Each item gets a verdict —
|
|
43
|
+
`SOUND`, `RISK`, or `WRONG` — backed by evidence you can point at (a file you
|
|
44
|
+
read, a test you ran, documented behaviour), not intuition. Fix every `WRONG`
|
|
45
|
+
and address every `RISK` before starting. A red-team that returns all-`SOUND`
|
|
46
|
+
on a non-trivial plan is a red-team you didn't actually do.
|
|
47
|
+
|
|
48
|
+
## 3. Ask one question, as a decision with a default
|
|
49
|
+
|
|
50
|
+
If you're genuinely blocked after inferring from code and history, don't send
|
|
51
|
+
a questionnaire. State what you found, the 2–3 viable options, which you'd
|
|
52
|
+
pick and why, and ask for confirmation:
|
|
53
|
+
|
|
54
|
+
> "The config loader supports both YAML and JSON overlays; the task says
|
|
55
|
+
> 'config file' without specifying. I'd extend the YAML path since all
|
|
56
|
+
> existing overlays are YAML (`src/config/merge.ts:88`) — confirm, or should
|
|
57
|
+
> JSON be covered too?"
|
|
58
|
+
|
|
59
|
+
Clarify during *planning*. Once the plan is agreed, execute autonomously:
|
|
60
|
+
make the reasonable call on small ambiguities, record the assumption in your
|
|
61
|
+
report, keep moving. Mid-execution questions are reserved for discoveries
|
|
62
|
+
that invalidate the plan.
|
|
63
|
+
|
|
64
|
+
## 4. Adversarial review — bounded on purpose
|
|
65
|
+
|
|
66
|
+
Dispatch the review to a **fresh** pass or sub-agent; the coder cannot review
|
|
67
|
+
its own work in-context. Structure it:
|
|
68
|
+
|
|
69
|
+
- **Input:** the full diff, the task statement, and the design report if one exists.
|
|
70
|
+
- **Charge:** *find reasons this change is wrong* — correctness, missed edge
|
|
71
|
+
cases, untested behaviour, inconsistency with surrounding code, docs drift,
|
|
72
|
+
security or data-loss risk.
|
|
73
|
+
- **Output:** a findings list, each with a severity, the evidence (`file:line`),
|
|
74
|
+
and a concrete fix.
|
|
75
|
+
|
|
76
|
+
**The severity gate — this is the fleet-specific part.** The old rule was
|
|
77
|
+
"fix ALL findings including lows, then re-review". That is a loop generator by
|
|
78
|
+
construction: an adversarial reviewer always surfaces lows (that is its job),
|
|
79
|
+
fixing lows produces a new diff, and a new diff earned another re-review. It
|
|
80
|
+
produced PRs going four rounds where the last round's only finding was an
|
|
81
|
+
inaccurate doc comment. So:
|
|
82
|
+
|
|
83
|
+
- **Blockers and mediums block the merge.** Fix them.
|
|
84
|
+
- **Lows do NOT block.** Fix a low inline only if it's a genuine one-liner;
|
|
85
|
+
otherwise **file a follow-up issue and merge**. Filing is mandatory — an
|
|
86
|
+
unfiled low is a dropped bug, and there is no human team to catch it later.
|
|
87
|
+
- **A re-review is earned only by a behavioural fix.** A docs-, comment-,
|
|
88
|
+
log-, or test-only fix commit does not earn another pass.
|
|
89
|
+
- **Two rounds is the cap.** Prefix every commit answering a review round
|
|
90
|
+
`review-fix:` — `scripts/check-review-rounds.mjs` counts them and the
|
|
91
|
+
`review-rounds` check fails past two, unless a `review-cap-override` label
|
|
92
|
+
is on the PR.
|
|
93
|
+
|
|
94
|
+
If a finding is genuinely invalid, rebut it with evidence in writing; silence
|
|
95
|
+
is not a rebuttal.
|
|
96
|
+
|
|
97
|
+
When you do re-review, the verdict states per original finding: the finding
|
|
98
|
+
ID, what changed (`file:line` of the fix), whether it fully resolves the
|
|
99
|
+
finding (`RESOLVED` / `PARTIAL` / `REBUTTED` with evidence), and whether the
|
|
100
|
+
fix introduced anything new. A bare "fixed" is not a verdict.
|
|
101
|
+
|
|
102
|
+
## 5. Non-obvious pipeline rules
|
|
103
|
+
|
|
104
|
+
- **CI is the full-suite authority.** Local scoped tests are a fast filter,
|
|
105
|
+
never the merge evidence. Never claim done off a local run alone.
|
|
106
|
+
- **A test that wouldn't fail on the bug it guards is not a test.** Assert the
|
|
107
|
+
observable outcome, not that the code path executed.
|
|
108
|
+
- **Prefer a deterministic mechanism over prompt discipline.** If a check, a
|
|
109
|
+
hook, a schema, or a lint gate can enforce the guarantee, write that instead
|
|
110
|
+
of a convention — conventions demonstrably fail here.
|
|
111
|
+
- **Never go dark, and never foreground-watch over 30 seconds.** Builds, CI
|
|
112
|
+
waits, and deploys run in the background with a notification. Batch related
|
|
113
|
+
findings into one substantive update rather than five fragments. Cap
|
|
114
|
+
parallel sub-agents at 15.
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# bun test configuration for runs whose CWD is telegram-plugin/ — CI's
|
|
2
|
+
# `bun-test-run` job (scripts/bun-test-ci.sh) and `bun run --cwd telegram-plugin`.
|
|
3
|
+
#
|
|
4
|
+
# bun reads the bunfig.toml in its CWD only, so this file exists purely to load
|
|
5
|
+
# the same state-dir hermeticity preload as the repo-root bunfig.toml. Keep the
|
|
6
|
+
# two in sync; `npm run lint:agent-state-dir-hermeticity` fails if either stops
|
|
7
|
+
# loading the guard. Rationale for the guard itself lives in
|
|
8
|
+
# tests/vitest-setup/agent-state-dir-guard.mjs.
|
|
9
|
+
[test]
|
|
10
|
+
preload = ["../tests/vitest-setup/agent-state-dir-guard.mjs"]
|
|
@@ -21051,7 +21051,8 @@ var init_schema = __esm(() => {
|
|
|
21051
21051
|
retain: HindsightPerOpLlmSchema.optional().describe("Per-op override for the `retain` LLM op (memory ingestion). Emits " + "`HINDSIGHT_API_RETAIN_LLM_*`. Absent \u2192 uses the global model/provider."),
|
|
21052
21052
|
reflect: HindsightPerOpLlmSchema.optional().describe("Per-op override for the `reflect` LLM op (synthesis / mental-model " + "refresh). Emits `HINDSIGHT_API_REFLECT_LLM_*`. Absent \u2192 uses global."),
|
|
21053
21053
|
consolidation: HindsightPerOpLlmSchema.optional().describe("Per-op override for the `consolidation` LLM op (background memory " + "merge). Emits `HINDSIGHT_API_CONSOLIDATION_LLM_*`. Absent \u2192 global.")
|
|
21054
|
-
}).optional().describe("LLM knob for the hindsight container. The flat `provider`/`model` set " + "the global default (backward-compatible); optional `retain`/`reflect`/" + "`consolidation` blocks override individual ops. All fields optional; " + "unset fields fall back to the hard-coded defaults.")
|
|
21054
|
+
}).optional().describe("LLM knob for the hindsight container. The flat `provider`/`model` set " + "the global default (backward-compatible); optional `retain`/`reflect`/" + "`consolidation` blocks override individual ops. All fields optional; " + "unset fields fall back to the hard-coded defaults."),
|
|
21055
|
+
env: exports_external.record(exports_external.union([exports_external.string(), exports_external.number(), exports_external.boolean()])).optional().describe("Operator overrides for switchroom's capability-gated Hindsight " + "performance defaults. Only the keys switchroom actually manages are " + "honoured (`HINDSIGHT_PERF_ENV_KEYS` in " + "src/setup/hindsight-perf-defaults.ts: RERANKER_LOCAL_FP16, " + "LLM_MAX_CONCURRENT, RETAIN/CONSOLIDATION_LLM_MAX_CONCURRENT, " + "RECALL_MAX_CANDIDATES_PER_SOURCE, LINK_EXPANSION_PER_ENTITY_LIMIT, " + "LINK_EXPANSION_TIMEOUT, LLM_REASONING_EFFORT), plus the " + "embedded-PostgreSQL (pg0) sizing keys switchroom manages in " + "src/setup/hindsight-pg-defaults.ts (`HINDSIGHT_PG_ENV_KEYS`: " + "SWITCHROOM_HINDSIGHT_PG_EFFECTIVE_CACHE_SIZE, " + "SWITCHROOM_HINDSIGHT_PG_SHARED_BUFFERS \u2014 a postgres size string such " + "as `4GB`, or the sentinel `off` to leave pg0's own default for that " + "one knob). A value set here " + "REPLACES switchroom's default and is emitted even when the gating " + "capability is absent, so an operator can always force a knob. Other " + "`HINDSIGHT_API_*` keys are deliberately IGNORED \u2014 a blanket " + "passthrough would collide with the vars startHindsight() derives " + "itself (HINDSIGHT_API_PORT, the retain token/deadline budget).")
|
|
21055
21056
|
});
|
|
21056
21057
|
MicrosoftWorkspaceConfigSchema = exports_external.object({
|
|
21057
21058
|
microsoft_client_id: exports_external.string().min(1).optional().describe("Microsoft OAuth application (client) ID from Entra portal " + "(literal string or vault reference e.g. " + "'vault:microsoft-oauth-client-id'). OPTIONAL \u2014 omit it to use " + "switchroom's shipped default Microsoft app (zero-config). " + "Set it only to bring your own Entra app (BYO)."),
|
|
@@ -76376,7 +76377,9 @@ function createBackstopReadBack(w) {
|
|
|
76376
76377
|
};
|
|
76377
76378
|
try {
|
|
76378
76379
|
const r = await w.gate(() => w.editMessageText(messageId, body, editApiOpts), gateOpts);
|
|
76379
|
-
|
|
76380
|
+
if (w.isShed(r) || r === undefined)
|
|
76381
|
+
return "ambiguous";
|
|
76382
|
+
return "exists";
|
|
76380
76383
|
} catch (err) {
|
|
76381
76384
|
return classifyReadBackError(err);
|
|
76382
76385
|
}
|
|
@@ -79124,6 +79127,7 @@ function handleSessionEvent(deps, ev) {
|
|
|
79124
79127
|
sentIds = delivery.sentIds;
|
|
79125
79128
|
chunkCount = delivery.chunkCount;
|
|
79126
79129
|
delivered = delivery.delivered;
|
|
79130
|
+
turn.landedUnconfirmed = delivery.landedUnconfirmed;
|
|
79127
79131
|
outboundDedup.record(backstopChatId, backstopThreadId, capturedText, Date.now(), getCurrentTurn()?.registryKey ?? null);
|
|
79128
79132
|
if (sentIds.length > 0) {
|
|
79129
79133
|
flushedTurnSupersede.record(backstopChatId, backstopThreadId, { turnId: turn.turnId, messageIds: sentIds, text: capturedText }, Date.now());
|
|
@@ -83021,6 +83025,75 @@ function assertPositive(value, label) {
|
|
|
83021
83025
|
}
|
|
83022
83026
|
}
|
|
83023
83027
|
|
|
83028
|
+
// ../src/setup/host-capabilities.ts
|
|
83029
|
+
init_paths();
|
|
83030
|
+
|
|
83031
|
+
// ../src/setup/hindsight-perf-defaults.ts
|
|
83032
|
+
var HINDSIGHT_RERANKER_MAX_CANDIDATES_FOR_DERIVATION = 150;
|
|
83033
|
+
var HINDSIGHT_DEFAULT_RECALL_MAX_CANDIDATES_PER_SOURCE = Math.ceil(HINDSIGHT_RERANKER_MAX_CANDIDATES_FOR_DERIVATION * 0.4);
|
|
83034
|
+
var HINDSIGHT_DEFAULT_LINK_EXPANSION_PER_ENTITY_LIMIT = 50;
|
|
83035
|
+
var HINDSIGHT_DEFAULT_LINK_EXPANSION_TIMEOUT_S = 2;
|
|
83036
|
+
var HINDSIGHT_DEFAULT_LLM_REASONING_EFFORT = "low";
|
|
83037
|
+
var HINDSIGHT_DEFAULT_LLM_MAX_CONCURRENT = 4;
|
|
83038
|
+
var HINDSIGHT_DEFAULT_RETAIN_LLM_MAX_CONCURRENT = 1;
|
|
83039
|
+
var HINDSIGHT_DEFAULT_CONSOLIDATION_LLM_MAX_CONCURRENT = 1;
|
|
83040
|
+
var HINDSIGHT_DEFAULT_RERANKER_LOCAL_FP16 = "true";
|
|
83041
|
+
var HINDSIGHT_PERF_DEFAULTS_UNGATED = [
|
|
83042
|
+
[
|
|
83043
|
+
"HINDSIGHT_API_RECALL_MAX_CANDIDATES_PER_SOURCE",
|
|
83044
|
+
String(HINDSIGHT_DEFAULT_RECALL_MAX_CANDIDATES_PER_SOURCE)
|
|
83045
|
+
],
|
|
83046
|
+
[
|
|
83047
|
+
"HINDSIGHT_API_LINK_EXPANSION_PER_ENTITY_LIMIT",
|
|
83048
|
+
String(HINDSIGHT_DEFAULT_LINK_EXPANSION_PER_ENTITY_LIMIT)
|
|
83049
|
+
],
|
|
83050
|
+
[
|
|
83051
|
+
"HINDSIGHT_API_LINK_EXPANSION_TIMEOUT",
|
|
83052
|
+
String(HINDSIGHT_DEFAULT_LINK_EXPANSION_TIMEOUT_S)
|
|
83053
|
+
],
|
|
83054
|
+
["HINDSIGHT_API_LLM_REASONING_EFFORT", HINDSIGHT_DEFAULT_LLM_REASONING_EFFORT]
|
|
83055
|
+
];
|
|
83056
|
+
var HINDSIGHT_PERF_DEFAULTS_GPU = [
|
|
83057
|
+
["HINDSIGHT_API_RERANKER_LOCAL_FP16", HINDSIGHT_DEFAULT_RERANKER_LOCAL_FP16]
|
|
83058
|
+
];
|
|
83059
|
+
var HINDSIGHT_PERF_DEFAULTS_LOCAL_LLM = [
|
|
83060
|
+
["HINDSIGHT_API_LLM_MAX_CONCURRENT", String(HINDSIGHT_DEFAULT_LLM_MAX_CONCURRENT)],
|
|
83061
|
+
[
|
|
83062
|
+
"HINDSIGHT_API_RETAIN_LLM_MAX_CONCURRENT",
|
|
83063
|
+
String(HINDSIGHT_DEFAULT_RETAIN_LLM_MAX_CONCURRENT)
|
|
83064
|
+
],
|
|
83065
|
+
[
|
|
83066
|
+
"HINDSIGHT_API_CONSOLIDATION_LLM_MAX_CONCURRENT",
|
|
83067
|
+
String(HINDSIGHT_DEFAULT_CONSOLIDATION_LLM_MAX_CONCURRENT)
|
|
83068
|
+
]
|
|
83069
|
+
];
|
|
83070
|
+
var HINDSIGHT_PERF_ENV_KEYS = new Set([
|
|
83071
|
+
...HINDSIGHT_PERF_DEFAULTS_UNGATED,
|
|
83072
|
+
...HINDSIGHT_PERF_DEFAULTS_GPU,
|
|
83073
|
+
...HINDSIGHT_PERF_DEFAULTS_LOCAL_LLM
|
|
83074
|
+
].map(([k]) => k));
|
|
83075
|
+
|
|
83076
|
+
// ../src/setup/hindsight-pg-defaults.ts
|
|
83077
|
+
var HINDSIGHT_PG_MEM_LIMIT_MIB_FOR_DERIVATION = 8 * 1024;
|
|
83078
|
+
var HINDSIGHT_PG_APP_ANON_MIB = 2560;
|
|
83079
|
+
var HINDSIGHT_PG_PAGE_CACHE_FLOOR_MIB = 2048;
|
|
83080
|
+
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;
|
|
83081
|
+
var HINDSIGHT_PG_DEFAULT_SHARED_BUFFERS_MIB = 1536;
|
|
83082
|
+
var HINDSIGHT_PG_DEFAULT_EFFECTIVE_CACHE_SIZE_MIB = 4096;
|
|
83083
|
+
function pgMib(mib) {
|
|
83084
|
+
return `${mib}MB`;
|
|
83085
|
+
}
|
|
83086
|
+
var HINDSIGHT_PG_EFFECTIVE_CACHE_SIZE_ENV = "SWITCHROOM_HINDSIGHT_PG_EFFECTIVE_CACHE_SIZE";
|
|
83087
|
+
var HINDSIGHT_PG_SHARED_BUFFERS_ENV = "SWITCHROOM_HINDSIGHT_PG_SHARED_BUFFERS";
|
|
83088
|
+
var HINDSIGHT_PG_DEFAULTS = [
|
|
83089
|
+
[
|
|
83090
|
+
HINDSIGHT_PG_EFFECTIVE_CACHE_SIZE_ENV,
|
|
83091
|
+
pgMib(HINDSIGHT_PG_DEFAULT_EFFECTIVE_CACHE_SIZE_MIB)
|
|
83092
|
+
],
|
|
83093
|
+
[HINDSIGHT_PG_SHARED_BUFFERS_ENV, pgMib(HINDSIGHT_PG_DEFAULT_SHARED_BUFFERS_MIB)]
|
|
83094
|
+
];
|
|
83095
|
+
var HINDSIGHT_PG_ENV_KEYS = new Set(HINDSIGHT_PG_DEFAULTS.map(([k]) => k));
|
|
83096
|
+
|
|
83024
83097
|
// ../src/setup/hindsight.ts
|
|
83025
83098
|
var HINDSIGHT_DEFAULT_API_PORT = 18888;
|
|
83026
83099
|
var HINDSIGHT_DEFAULT_MCP_URL = `http://127.0.0.1:${HINDSIGHT_DEFAULT_API_PORT}/mcp/`;
|
|
@@ -87593,10 +87666,10 @@ function createCapturedResumeDispatcher(ports) {
|
|
|
87593
87666
|
if (delivered) {
|
|
87594
87667
|
ports.obligationLedger.close(o.originTurnId);
|
|
87595
87668
|
ports.backstopDeliveryLedger.clear(o.originTurnId);
|
|
87596
|
-
stderr(`telegram gateway: captured-answer resume delivered \u2014 origin=${o.originTurnId} ` + `${sentIds.length}
|
|
87669
|
+
stderr(`telegram gateway: captured-answer resume delivered \u2014 origin=${o.originTurnId} ` + `${sentIds.length} message id(s) landed; obligation closed
|
|
87597
87670
|
`);
|
|
87598
87671
|
} else {
|
|
87599
|
-
stderr(`telegram gateway: captured-answer resume partial \u2014 origin=${o.originTurnId} ` + `tail still not
|
|
87672
|
+
stderr(`telegram gateway: captured-answer resume partial \u2014 origin=${o.originTurnId} ` + `tail still not landed; left OPEN for retry
|
|
87600
87673
|
`);
|
|
87601
87674
|
}
|
|
87602
87675
|
} catch (err) {
|
|
@@ -88469,6 +88542,14 @@ function resolveAnswerThreadId(input) {
|
|
|
88469
88542
|
|
|
88470
88543
|
// gateway/turns-jsonl-rotate.ts
|
|
88471
88544
|
var TURNS_JSONL_MAX_BYTES = 5 * 1024 * 1024;
|
|
88545
|
+
var DEFAULT_AGENT_STATE_DIR = "/state/agent";
|
|
88546
|
+
function resolveAgentStateDir(env = process.env) {
|
|
88547
|
+
const dir = env.SWITCHROOM_AGENT_STATE_DIR?.trim();
|
|
88548
|
+
return dir != null && dir !== "" ? dir.replace(/\/+$/, "") : DEFAULT_AGENT_STATE_DIR;
|
|
88549
|
+
}
|
|
88550
|
+
function resolveTurnsJsonlPath(env = process.env) {
|
|
88551
|
+
return `${resolveAgentStateDir(env)}/turns.jsonl`;
|
|
88552
|
+
}
|
|
88472
88553
|
function maybeRotate(path3, fs3, maxBytes = TURNS_JSONL_MAX_BYTES) {
|
|
88473
88554
|
const size = fs3.statSize(path3);
|
|
88474
88555
|
if (size == null || size < maxBytes)
|
|
@@ -88497,7 +88578,8 @@ function buildTurnRecord(turn, endedAt) {
|
|
|
88497
88578
|
duration_ms: turn.startedAt > 0 ? endedAt - turn.startedAt : 0,
|
|
88498
88579
|
tools: turn.toolCallCount ?? 0,
|
|
88499
88580
|
status: computeTurnStatus(turn),
|
|
88500
|
-
turn_id: turn.turnId
|
|
88581
|
+
turn_id: turn.turnId,
|
|
88582
|
+
...turn.landedUnconfirmed != null && turn.landedUnconfirmed > 0 ? { landed_unconfirmed: turn.landedUnconfirmed } : {}
|
|
88501
88583
|
};
|
|
88502
88584
|
}
|
|
88503
88585
|
|
|
@@ -88679,8 +88761,16 @@ async function runBackstopDelivery(ledger, turnId, chunks, cardMessageId, deps,
|
|
|
88679
88761
|
break;
|
|
88680
88762
|
}
|
|
88681
88763
|
const sentIds = ledger.sentIds(turnId);
|
|
88682
|
-
const
|
|
88764
|
+
const confirmed = ledger.allConfirmed(turnId, chunkCount);
|
|
88765
|
+
const allLanded = chunkCount > 0 && ledger.unsentIndices(turnId, chunkCount).length === 0;
|
|
88766
|
+
const delivered = allLanded && backstopReceiptIds(sentIds, cardMessageId).length > 0;
|
|
88683
88767
|
const exhausted = !delivered;
|
|
88768
|
+
const confirmedSet = new Set(ledger.confirmedIds(turnId));
|
|
88769
|
+
const landedUnconfirmedIds = sentIds.filter((id) => !confirmedSet.has(id));
|
|
88770
|
+
if (delivered && !confirmed) {
|
|
88771
|
+
stderr(`telegram gateway: backstop delivery landed-unconfirmed for turn ${turnId} \u2014 ` + `every chunk returned a fresh message id but the read-back probe was ` + `inconclusive for ${landedUnconfirmedIds.length} of ${sentIds.length} landed ` + `id(s); counting it delivered (an ambiguous probe is not a failure)
|
|
88772
|
+
`);
|
|
88773
|
+
}
|
|
88684
88774
|
if (deps.recordOutbound && sentIds.length > 0) {
|
|
88685
88775
|
const texts = [];
|
|
88686
88776
|
const ids = [];
|
|
@@ -88692,7 +88782,7 @@ async function runBackstopDelivery(ledger, turnId, chunks, cardMessageId, deps,
|
|
|
88692
88782
|
}
|
|
88693
88783
|
deps.recordOutbound(ids, texts);
|
|
88694
88784
|
}
|
|
88695
|
-
return { sentIds, chunkCount, delivered, attempts, exhausted };
|
|
88785
|
+
return { sentIds, chunkCount, delivered, confirmed, landedUnconfirmedIds, attempts, exhausted };
|
|
88696
88786
|
}
|
|
88697
88787
|
|
|
88698
88788
|
// gateway/inbound-delivery-confirm.ts
|
|
@@ -96331,10 +96421,10 @@ function startOutboxSweep(deps) {
|
|
|
96331
96421
|
}
|
|
96332
96422
|
|
|
96333
96423
|
// ../src/build-info.ts
|
|
96334
|
-
var VERSION2 = "0.19.
|
|
96335
|
-
var COMMIT_SHA = "
|
|
96336
|
-
var COMMIT_DATE = "2026-07-
|
|
96337
|
-
var LATEST_PR =
|
|
96424
|
+
var VERSION2 = "0.19.23";
|
|
96425
|
+
var COMMIT_SHA = "56e3c01e";
|
|
96426
|
+
var COMMIT_DATE = "2026-07-26T13:24:39Z";
|
|
96427
|
+
var LATEST_PR = 3711;
|
|
96338
96428
|
var COMMITS_AHEAD_OF_TAG = 0;
|
|
96339
96429
|
|
|
96340
96430
|
// gateway/boot-version.ts
|
|
@@ -98194,7 +98284,7 @@ var STATE_DIR = process.env.TELEGRAM_STATE_DIR ?? join65(homedir19(), ".claude",
|
|
|
98194
98284
|
var permCardStore = createPermissionCardStore(STATE_DIR);
|
|
98195
98285
|
var BLOCKED_APPROVALS_DIR = process.env.SWITCHROOM_BLOCKED_APPROVALS_DIR ?? "/state/blocked-approvals";
|
|
98196
98286
|
var AGENT_NAME = process.env.SWITCHROOM_AGENT_NAME ?? "agent";
|
|
98197
|
-
var blockedApprovalStore = createBlockedApprovalStore(BLOCKED_APPROVALS_DIR, AGENT_NAME,
|
|
98287
|
+
var blockedApprovalStore = createBlockedApprovalStore(BLOCKED_APPROVALS_DIR, AGENT_NAME, resolveAgentStateDir());
|
|
98198
98288
|
function reconcileBlockedApprovals() {
|
|
98199
98289
|
const oldest = selectOldestHeld(pendingPermissions);
|
|
98200
98290
|
if (oldest == null) {
|
|
@@ -98975,7 +99065,8 @@ async function deliverAnswer(args) {
|
|
|
98975
99065
|
sentIds: result.sentIds,
|
|
98976
99066
|
chunkCount: result.chunkCount,
|
|
98977
99067
|
delivered: result.delivered,
|
|
98978
|
-
exhausted: result.exhausted
|
|
99068
|
+
exhausted: result.exhausted,
|
|
99069
|
+
landedUnconfirmed: result.landedUnconfirmedIds.length
|
|
98979
99070
|
};
|
|
98980
99071
|
}
|
|
98981
99072
|
var chatAvailableReactions = new Map;
|
|
@@ -99802,7 +99893,7 @@ function snapshotContextOccupancy() {
|
|
|
99802
99893
|
} catch {
|
|
99803
99894
|
cap = null;
|
|
99804
99895
|
}
|
|
99805
|
-
const stateDir =
|
|
99896
|
+
const stateDir = resolveAgentStateDir();
|
|
99806
99897
|
writeContextOccupancySnapshot(stateDir, buildContextOccupancy(occupancy, cap, Date.now()));
|
|
99807
99898
|
} catch {}
|
|
99808
99899
|
}
|
|
@@ -99814,10 +99905,11 @@ function emitTurnRecord(turn, endedAt) {
|
|
|
99814
99905
|
toolCallCount: turn.toolCallCount ?? 0,
|
|
99815
99906
|
turnId: turn.turnId,
|
|
99816
99907
|
finalAnswerDelivered: turn.finalAnswerDelivered,
|
|
99817
|
-
deliveryOutcome: turn.deliveryOutcome
|
|
99908
|
+
deliveryOutcome: turn.deliveryOutcome,
|
|
99909
|
+
landedUnconfirmed: turn.landedUnconfirmed
|
|
99818
99910
|
}, endedAt)) + `
|
|
99819
99911
|
`;
|
|
99820
|
-
const turnsPath =
|
|
99912
|
+
const turnsPath = resolveTurnsJsonlPath();
|
|
99821
99913
|
maybeRotate(turnsPath, {
|
|
99822
99914
|
statSize: (p) => {
|
|
99823
99915
|
try {
|
|
@@ -114,8 +114,10 @@ export class BackstopDeliveryLedger {
|
|
|
114
114
|
|
|
115
115
|
/**
|
|
116
116
|
* #3278 — transition a landed-unconfirmed chunk to `landed-confirmed` after a
|
|
117
|
-
* read-back probe proved the message exists in the chat.
|
|
118
|
-
*
|
|
117
|
+
* read-back probe proved the message exists in the chat. Confirmation is the
|
|
118
|
+
* STRONGER of the two delivery states; the weaker `landed-unconfirmed` also
|
|
119
|
+
* counts as delivered (an inconclusive probe is not a failure — see
|
|
120
|
+
* `runBackstopDelivery`). Only a POSITIVE absence (`demoteChunk`) un-delivers.
|
|
119
121
|
*/
|
|
120
122
|
confirmChunk(turnId: string, index: number): void {
|
|
121
123
|
let set = this.confirmed.get(turnId)
|
|
@@ -164,8 +166,14 @@ export class BackstopDeliveryLedger {
|
|
|
164
166
|
return true
|
|
165
167
|
}
|
|
166
168
|
|
|
167
|
-
/** Landed message ids of CONFIRMED chunks only, in chunk-index order — the
|
|
168
|
-
*
|
|
169
|
+
/** Landed message ids of CONFIRMED chunks only, in chunk-index order — the
|
|
170
|
+
* read-back view of the ledger (ids proven present in the chat). The delivery
|
|
171
|
+
* predicate counts LANDED ids (`sentIds`), not these; this is the stronger
|
|
172
|
+
* proven-present subset. Its production consumer is
|
|
173
|
+
* {@link BackstopDeliveryResult.landedUnconfirmedIds} — `sentIds` minus these
|
|
174
|
+
* is the landed-but-uncorroborated set that reaches `turns.jsonl` as
|
|
175
|
+
* `landed_unconfirmed`, the counter that measures whether this module's
|
|
176
|
+
* optimism about an inconclusive probe is ever wrong. */
|
|
169
177
|
confirmedIds(turnId: string): number[] {
|
|
170
178
|
const m = this.chunks.get(turnId)
|
|
171
179
|
const set = this.confirmed.get(turnId)
|
|
@@ -226,9 +234,11 @@ export class BackstopDeliveryLedger {
|
|
|
226
234
|
* `landed-confirmed` (counts toward delivery).
|
|
227
235
|
* - `absent` — Telegram `400 message to edit not found` ⇒ positive absence
|
|
228
236
|
* ⇒ demote to `unsent` (safe to re-send — it never landed).
|
|
229
|
-
* - `ambiguous` — 429 / 5xx / network / gate-shed / anything else
|
|
230
|
-
* `landed-unconfirmed`; NEVER re-send (a re-send would
|
|
231
|
-
* duplicate, and duplicate-risk beats missing-risk
|
|
237
|
+
* - `ambiguous` — 429 / 5xx / network / gate-shed / gate no-op / anything else
|
|
238
|
+
* ⇒ leave `landed-unconfirmed`; NEVER re-send (a re-send would
|
|
239
|
+
* risk a duplicate, and duplicate-risk beats missing-risk
|
|
240
|
+
* here) and NEVER count it as a delivery failure either — the
|
|
241
|
+
* probe established nothing, so the landed-id evidence stands.
|
|
232
242
|
*
|
|
233
243
|
* NOTE (honest limitation, #3278 §1.4): a passing probe proves only that the
|
|
234
244
|
* message EXISTS at that chat_id. It does NOT prove the human's client rendered
|
|
@@ -322,8 +332,36 @@ export interface BackstopDeliveryResult {
|
|
|
322
332
|
sentIds: number[]
|
|
323
333
|
/** Number of input chunks the answer was split into. */
|
|
324
334
|
chunkCount: number
|
|
325
|
-
/**
|
|
335
|
+
/**
|
|
336
|
+
* True IFF every chunk landed at least one message id AND at least one of
|
|
337
|
+
* them is a fresh non-card chat id. This is the DELIVERY verdict the turn
|
|
338
|
+
* record, the obligation ledger and the status reaction key on.
|
|
339
|
+
*
|
|
340
|
+
* A read-back probe can only ever LOWER it, and only on POSITIVE absence: an
|
|
341
|
+
* `absent` verdict demotes the chunk back to `unsent`, so it stops counting as
|
|
342
|
+
* landed. An `ambiguous` probe carries no information and therefore does not
|
|
343
|
+
* move this flag — see {@link confirmed}.
|
|
344
|
+
*/
|
|
326
345
|
delivered: boolean
|
|
346
|
+
/**
|
|
347
|
+
* True IFF every chunk was read-back CONFIRMED (`exists`). Strictly stronger
|
|
348
|
+
* than {@link delivered} and purely observational — nothing keys a failure on
|
|
349
|
+
* it. `delivered && !confirmed` is the `landed-unconfirmed` state: the Bot API
|
|
350
|
+
* returned fresh ids for every chunk but the probe could not corroborate them.
|
|
351
|
+
*/
|
|
352
|
+
confirmed: boolean
|
|
353
|
+
/**
|
|
354
|
+
* The landed message ids NO read-back corroborated — `sentIds` minus the
|
|
355
|
+
* confirmed subset, in chunk-index order. Empty when {@link confirmed}.
|
|
356
|
+
*
|
|
357
|
+
* This is the MEASURABLE form of this module's central bet: a landed id whose
|
|
358
|
+
* probe came back inconclusive is counted as delivered. Its count is stamped
|
|
359
|
+
* onto the turn record as `landed_unconfirmed` (see `buildTurnRecord`) so the
|
|
360
|
+
* fleet can tell how often that bet is being made — and, if a
|
|
361
|
+
* `landed_unconfirmed` turn is ever followed by a "you never answered me",
|
|
362
|
+
* that the bet was wrong. Purely observational: nothing keys a failure on it.
|
|
363
|
+
*/
|
|
364
|
+
landedUnconfirmedIds: number[]
|
|
327
365
|
/** How many attempts ran (1..maxAttempts). */
|
|
328
366
|
attempts: number
|
|
329
367
|
/** True when retries were exhausted without full delivery (terminal fail). */
|
|
@@ -345,9 +383,12 @@ export interface BackstopDeliveryResult {
|
|
|
345
383
|
* is read-back-probed via `deps.readBack` (when provided): `exists` confirms it,
|
|
346
384
|
* `absent` demotes it to `unsent` so the NEXT attempt re-sends only that chunk,
|
|
347
385
|
* and `ambiguous` leaves it `landed-unconfirmed` — never re-sent (duplicate-risk
|
|
348
|
-
* beats missing-risk).
|
|
349
|
-
*
|
|
350
|
-
*
|
|
386
|
+
* beats missing-risk). An API-ack'd-but-silently-dropped send (fresh id, absent
|
|
387
|
+
* on read-back) is therefore reported `delivered:false` and the caller leaves
|
|
388
|
+
* the obligation OPEN — a correct mechanism that is INERT UNTIL #3703, because
|
|
389
|
+
* the probe is 100% shed in production and never resolves `absent`. An
|
|
390
|
+
* INCONCLUSIVE probe is not a failure: it leaves `delivered` alone and only
|
|
391
|
+
* clears `confirmed` (see `BackstopDeliveryResult`).
|
|
351
392
|
*
|
|
352
393
|
* `recordOutbound` (when provided) fires ONCE at the end with the full landed
|
|
353
394
|
* set and a `texts` array ALIGNED to the actual sent ids (via `ledger.entries`).
|
|
@@ -449,12 +490,52 @@ export async function runBackstopDelivery(
|
|
|
449
490
|
}
|
|
450
491
|
|
|
451
492
|
const sentIds = ledger.sentIds(turnId)
|
|
452
|
-
|
|
453
|
-
//
|
|
493
|
+
const confirmed = ledger.allConfirmed(turnId, chunkCount)
|
|
494
|
+
// The delivery verdict is EVIDENCE-BASED, not confirmation-gated.
|
|
495
|
+
//
|
|
496
|
+
// #3278 originally required every chunk to be `landed-confirmed`, so an
|
|
497
|
+
// `ambiguous` probe — which by definition establishes nothing — produced
|
|
498
|
+
// `delivered:false`. That inverted the guard it was meant to be: the turn was
|
|
499
|
+
// recorded `send_failed`, the status reaction painted error, and the delivery
|
|
500
|
+
// obligation was left OPEN for a re-present, all for an answer the user had
|
|
501
|
+
// demonstrably received. In production the probe is ambiguous essentially
|
|
502
|
+
// always (it is issued at cosmetic priority in the same millisecond as the
|
|
503
|
+
// send it probes, so the per-chat token bucket sheds it), so this turned a
|
|
504
|
+
// successful backstop delivery into a logged failure ~146 times in two weeks.
|
|
505
|
+
//
|
|
506
|
+
// Absence of evidence is not evidence of absence. The verdict is therefore:
|
|
507
|
+
// every chunk LANDED (guard 6) and at least one landed id is a fresh non-card
|
|
508
|
+
// chat id (the receipt gate, guard 7). A probe can still lower it — an
|
|
509
|
+
// `absent` verdict demotes the chunk to `unsent` above, so it is no longer
|
|
510
|
+
// landed — which keeps #3278's real contribution (a positive absence is
|
|
511
|
+
// caught and re-sent) while an inconclusive probe changes nothing.
|
|
512
|
+
//
|
|
513
|
+
// Honesty about what that contribution is worth TODAY: it is INERT UNTIL
|
|
514
|
+
// #3703. The probe is issued at cosmetic priority in the same millisecond as
|
|
515
|
+
// the send it probes, so the per-chat token bucket sheds it 100% of the time
|
|
516
|
+
// (146 `ambiguous` / 0 `absent` across the live fleet) and the `absent` branch
|
|
517
|
+
// is structurally unreachable in production. The demote-and-re-send path below
|
|
518
|
+
// is a correct mechanism with no live trigger; #3703 is the change that wakes
|
|
519
|
+
// the probe (and must first fix its resplit-overwrite hazard). Do not read
|
|
520
|
+
// this block as "absence is still caught in production" — it is not, yet.
|
|
521
|
+
const allLanded = chunkCount > 0 && ledger.unsentIndices(turnId, chunkCount).length === 0
|
|
454
522
|
const delivered =
|
|
455
|
-
|
|
456
|
-
backstopReceiptIds(ledger.confirmedIds(turnId), cardMessageId).length > 0
|
|
523
|
+
allLanded && backstopReceiptIds(sentIds, cardMessageId).length > 0
|
|
457
524
|
const exhausted = !delivered
|
|
525
|
+
// The landed-but-uncorroborated set (L2/L5): `sentIds` minus the read-back
|
|
526
|
+
// confirmed subset. Surfaced on the result so the caller can stamp
|
|
527
|
+
// `landed_unconfirmed` on the turn record — the only way to measure whether
|
|
528
|
+
// counting an inconclusive probe as delivered is ever wrong.
|
|
529
|
+
const confirmedSet = new Set(ledger.confirmedIds(turnId))
|
|
530
|
+
const landedUnconfirmedIds = sentIds.filter(id => !confirmedSet.has(id))
|
|
531
|
+
if (delivered && !confirmed) {
|
|
532
|
+
stderr(
|
|
533
|
+
`telegram gateway: backstop delivery landed-unconfirmed for turn ${turnId} — ` +
|
|
534
|
+
`every chunk returned a fresh message id but the read-back probe was ` +
|
|
535
|
+
`inconclusive for ${landedUnconfirmedIds.length} of ${sentIds.length} landed ` +
|
|
536
|
+
`id(s); counting it delivered (an ambiguous probe is not a failure)\n`,
|
|
537
|
+
)
|
|
538
|
+
}
|
|
458
539
|
|
|
459
540
|
if (deps.recordOutbound && sentIds.length > 0) {
|
|
460
541
|
const texts: string[] = []
|
|
@@ -468,5 +549,5 @@ export async function runBackstopDelivery(
|
|
|
468
549
|
deps.recordOutbound(ids, texts)
|
|
469
550
|
}
|
|
470
551
|
|
|
471
|
-
return { sentIds, chunkCount, delivered, attempts, exhausted }
|
|
552
|
+
return { sentIds, chunkCount, delivered, confirmed, landedUnconfirmedIds, attempts, exhausted }
|
|
472
553
|
}
|