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.
Files changed (37) hide show
  1. package/dist/agent-scheduler/index.js +2 -1
  2. package/dist/auth-broker/index.js +68 -1
  3. package/dist/cli/notion-write-pretool.mjs +2 -1
  4. package/dist/cli/switchroom.js +552 -320
  5. package/dist/host-control/main.js +69 -2
  6. package/dist/vault/approvals/kernel-server.js +71 -4
  7. package/dist/vault/broker/server.js +71 -4
  8. package/package.json +5 -4
  9. package/profiles/_base/start.sh.hbs +101 -0
  10. package/profiles/_shared/agent-self-service.md.hbs +64 -109
  11. package/profiles/_shared/delegation-golden-rule.md.hbs +5 -5
  12. package/profiles/_shared/dev-protocol.md.hbs +13 -42
  13. package/profiles/_shared/execution-discipline.md.hbs +7 -14
  14. package/profiles/coding/CLAUDE.md.hbs +0 -6
  15. package/profiles/default/CLAUDE.md.hbs +21 -50
  16. package/skills/dev-protocol/SKILL.md +90 -107
  17. package/telegram-plugin/bunfig.toml +10 -0
  18. package/telegram-plugin/dist/gateway/gateway.js +108 -16
  19. package/telegram-plugin/gateway/backstop-delivery.ts +97 -16
  20. package/telegram-plugin/gateway/captured-answer-resume.ts +46 -17
  21. package/telegram-plugin/gateway/gateway.ts +9 -7
  22. package/telegram-plugin/gateway/outbound-send-path.ts +8 -1
  23. package/telegram-plugin/gateway/stream-render.ts +6 -0
  24. package/telegram-plugin/gateway/turn-record-status.ts +19 -0
  25. package/telegram-plugin/gateway/turns-jsonl-rotate.ts +65 -0
  26. package/telegram-plugin/tests/agent-state-dir-preload.test.ts +33 -0
  27. package/telegram-plugin/tests/backstop-delivery.test.ts +204 -7
  28. package/telegram-plugin/tests/backstop-readback-probe.test.ts +12 -0
  29. package/telegram-plugin/tests/captured-answer-resume.test.ts +104 -0
  30. package/telegram-plugin/tests/turns-jsonl-rotate.test.ts +92 -1
  31. package/vendor/hindsight-memory/scripts/drain_pending.py +113 -11
  32. package/vendor/hindsight-memory/scripts/lib/pending.py +802 -65
  33. package/vendor/hindsight-memory/scripts/lib/retain_split.py +54 -7
  34. package/vendor/hindsight-memory/scripts/tests/test_pending_drops.py +1445 -11
  35. package/vendor/hindsight-memory/scripts/tests/test_retain_split.py +78 -6
  36. package/vendor/hindsight-memory/tests/test_drain_pending.py +17 -2
  37. 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 is the summary.
17
- This skill is the long-form procedure. Work through the five phases in order;
18
- they are checkpoints, not vibes.
19
-
20
- ## 1. Orient — ground before you build
21
-
22
- Before forming any theory or plan:
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 phase 4.
71
-
72
- For larger tasks:
73
-
74
- 1. **Design report before code.** Send the user an evidence-grounded report:
75
- what exists today (with citations), what will change, the chosen approach
76
- and its rejected alternatives, and the PR staging plan. Get alignment
77
- before implementation.
78
- 2. **Red-team your own plan adversarially.** Review the design item by item.
79
- Each item gets a verdict `SOUND`, `RISK`, or `WRONG` backed by
80
- evidence (a file you read, a test you ran, a documented behavior), not
81
- intuition. Fix every `WRONG` and address every `RISK` before starting.
82
- 3. **Stage delivery as focused single-concern PRs.** One concern per PR:
83
- reviewable in one sitting, revertable in one command. Never bundle a
84
- refactor with a behavior change.
85
-
86
- ## 4. Pipeline — how a change ships
87
-
88
- 1. **Branch off fresh main.** `git fetch && git checkout -b <branch> origin/main`.
89
- 2. **Implement with durable fixes.** Fix root causes. A workaround is
90
- acceptable only with an explicit reason stated and a follow-up filed.
91
- Prefer deterministic mechanisms (a check, a hook, a schema, a lint gate)
92
- over model-dependent behavior if code can enforce the guarantee, don't
93
- leave it to prompt discipline.
94
- 3. **Tests assert outcomes.** Every test must fail if the bug it guards
95
- returns. A test that merely exercises the code path without asserting the
96
- observable outcome is not a test.
97
- 4. **Scoped tests + lint locally.** Run the test files covering what you
98
- touched, plus the repo's lint gate. Local runs are a fast filter; **CI is
99
- the full-suite authority** — never claim done off a local run alone.
100
- 5. **Adversarial review of the diff.** Dispatch a reviewer (sub-agent or
101
- fresh pass) with this structure:
102
- - Input: the full diff, the task statement, and the design report if one
103
- exists.
104
- - Charge: *find reasons this change is wrong* — correctness, missed edge
105
- cases, untested behavior, inconsistency with surrounding code, docs
106
- drift, security/data-loss risk.
107
- - Output: a findings list, each with severity (high/medium/low), the
108
- evidence (`file:line`), and a concrete fix.
109
- 6. **Fix ALL findings — including lows.** A low you skip is a bug you
110
- shipped. If a finding is genuinely invalid, rebut it with evidence in
111
- writing; silence is not a rebuttal.
112
- 7. **Re-review the fix.** The re-review verdict must contain, per original
113
- finding: the finding ID, what changed (`file:line` of the fix), whether it
114
- fully resolves the finding (`RESOLVED` / `PARTIAL` / `REBUTTED` with
115
- evidence), and whether the fix introduced anything new. A bare "fixed" is
116
- not a verdict.
117
- 8. **Merge only on CI green.** No exceptions. A red or flaky CI run is a
118
- blocker to investigate, not to override.
119
-
120
- ## 5. Communicate while you work
121
-
122
- - **Consolidated messages.** Batch related findings and results into one
123
- substantive update; never send five fragments where one message serves.
124
- - **Always-visible progress.** Long-running work surfaces status the user can
125
- see (progress card, interim edit, explicit "still running: X"). Never go
126
- dark mid-task.
127
- - **No foreground watches over 30 seconds.** Anything longer — builds, CI
128
- waits, deploys runs in the background with a notification on completion.
129
- Don't block a turn polling.
130
- - **Max 15 parallel sub-agents.** Fan out for genuinely parallel work
131
- (independent reviews, independent modules), but cap the swarm at 15.
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
- return w.isShed(r) ? "ambiguous" : "exists";
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} chunk(s) confirmed; obligation closed
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 confirmed; left OPEN for retry
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 delivered = ledger.allConfirmed(turnId, chunkCount) && backstopReceiptIds(ledger.confirmedIds(turnId), cardMessageId).length > 0;
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.22";
96335
- var COMMIT_SHA = "50ef9fb5";
96336
- var COMMIT_DATE = "2026-07-26T06:22:32Z";
96337
- var LATEST_PR = 3696;
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, process.env.SWITCHROOM_AGENT_STATE_DIR ?? "/state/agent");
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 = process.env.SWITCHROOM_AGENT_STATE_DIR ?? "/state/agent";
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 = "/state/agent/turns.jsonl";
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. Only a confirmed
118
- * chunk counts toward delivery / `complete`.
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 set
168
- * the delivery predicate counts (fresh non-card ids that are proven-present). */
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 ⇒ leave
230
- * `landed-unconfirmed`; NEVER re-send (a re-send would risk a
231
- * duplicate, and duplicate-risk beats missing-risk here).
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
- /** True IFF every chunk landed at least one fresh non-card id. */
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). `delivered` now requires every chunk `landed-confirmed`,
349
- * so an API-ack'd-but-silently-dropped send (fresh id, absent on read-back) is
350
- * reported `delivered:false` and the caller leaves the obligation OPEN.
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
- // #3278 — delivered IFF every chunk is `landed-confirmed` AND at least one
453
- // confirmed id is a fresh non-card chat id (the receipt gate, guard 7).
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
- ledger.allConfirmed(turnId, chunkCount) &&
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
  }