switchroom 0.19.19 → 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 +121 -1
- package/dist/cli/notion-write-pretool.mjs +2 -1
- package/dist/cli/switchroom.js +2995 -1583
- package/dist/host-control/main.js +122 -2
- package/dist/vault/approvals/kernel-server.js +124 -4
- package/dist/vault/broker/server.js +124 -4
- package/package.json +7 -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/skills/switchroom-release/SKILL.md +103 -20
- package/telegram-plugin/bunfig.toml +10 -0
- package/telegram-plugin/card-format.ts +92 -3
- package/telegram-plugin/dist/gateway/gateway.js +873 -184
- package/telegram-plugin/edit-flood-fuse.ts +477 -0
- package/telegram-plugin/format.ts +19 -7
- package/telegram-plugin/gateway/backstop-delivery.ts +97 -16
- package/telegram-plugin/gateway/boot-sweep-gate.ts +164 -0
- package/telegram-plugin/gateway/callback-query-handlers.ts +454 -81
- package/telegram-plugin/gateway/captured-answer-resume.ts +46 -17
- package/telegram-plugin/gateway/gateway.ts +75 -63
- package/telegram-plugin/gateway/inbound-interceptors.ts +27 -4
- package/telegram-plugin/gateway/narrative-lane.ts +49 -3
- package/telegram-plugin/gateway/outbound-send-path.ts +8 -1
- package/telegram-plugin/gateway/status-pin-api.ts +145 -0
- 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/hooks/subagent-tracker-posttool.mjs +325 -45
- package/telegram-plugin/retry-api-call.ts +15 -2
- package/telegram-plugin/send-gate.ts +1 -1
- package/telegram-plugin/status-no-truncate.ts +64 -1
- package/telegram-plugin/status-pin-driver.ts +50 -27
- package/telegram-plugin/status-pin.ts +43 -5
- package/telegram-plugin/tests/activity-card-send-gate.test.ts +275 -0
- package/telegram-plugin/tests/activity-card-wiring.test.ts +16 -7
- 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/boot-pin-sweep-wiring.test.ts +101 -0
- package/telegram-plugin/tests/boot-sweep-gate.test.ts +293 -0
- package/telegram-plugin/tests/boot-version-string.test.ts +0 -0
- package/telegram-plugin/tests/captured-answer-resume.test.ts +104 -0
- package/telegram-plugin/tests/edit-flood-fuse.test.ts +431 -0
- package/telegram-plugin/tests/pinned-card-collapse.test.ts +356 -0
- package/telegram-plugin/tests/status-pin-api.test.ts +178 -0
- package/telegram-plugin/tests/status-pin-boot-recovery.test.ts +94 -11
- package/telegram-plugin/tests/status-pin.test.ts +106 -5
- package/telegram-plugin/tests/subagent-tracker-hooks.test.ts +631 -1
- package/telegram-plugin/tests/tool-activity-summary.test.ts +19 -10
- package/telegram-plugin/tests/turns-jsonl-rotate.test.ts +92 -1
- package/telegram-plugin/tests/vault-approval-posture.test.ts +6 -1
- package/telegram-plugin/tests/vault-passphrase-retry.test.ts +666 -0
- package/telegram-plugin/tests/vault-request-access-unlock-resume.test.ts +42 -21
- package/telegram-plugin/tests/worker-feed-coalesce.test.ts +233 -1
- package/telegram-plugin/tool-activity-summary.ts +85 -13
- package/telegram-plugin/worker-activity-feed.ts +5 -1
- package/vendor/hindsight-memory/scripts/drain_pending.py +304 -34
- package/vendor/hindsight-memory/scripts/lib/pending.py +886 -70
- package/vendor/hindsight-memory/scripts/lib/retain_split.py +71 -13
- package/vendor/hindsight-memory/scripts/recall.py +74 -5
- package/vendor/hindsight-memory/scripts/tests/test_pending_drops.py +1602 -14
- package/vendor/hindsight-memory/scripts/tests/test_pending_failure_class.py +105 -0
- package/vendor/hindsight-memory/scripts/tests/test_pending_wedge.py +300 -0
- package/vendor/hindsight-memory/scripts/tests/test_recall_degraded_notice.py +365 -0
- package/vendor/hindsight-memory/scripts/tests/test_recall_envelope_strip_telemetry.py +12 -4
- package/vendor/hindsight-memory/scripts/tests/test_recall_transcript_fallback.py +27 -2
- package/vendor/hindsight-memory/scripts/tests/test_retain_split.py +93 -13
- package/vendor/hindsight-memory/tests/test_drain_pending.py +44 -3
- package/vendor/hindsight-memory/tests/test_pending.py +12 -4
|
@@ -68,7 +68,8 @@ from typing import Optional
|
|
|
68
68
|
# The bound — derived, not chosen
|
|
69
69
|
# ---------------------------------------------------------------------------
|
|
70
70
|
#
|
|
71
|
-
# max_content_chars = retain_chunk_size
|
|
71
|
+
# max_content_chars = retain_chunk_size
|
|
72
|
+
# × floor(client_deadline × deadline_safety / chunk_latency)
|
|
72
73
|
#
|
|
73
74
|
# Each input is a measured or read property of the deployment, not a taste
|
|
74
75
|
# call, and each is env-overridable so the bound tracks the deployment:
|
|
@@ -83,7 +84,7 @@ from typing import Optional
|
|
|
83
84
|
# completion-token bucket; the runaway bucket is
|
|
84
85
|
# a separate defect, fixed by capping
|
|
85
86
|
# HINDSIGHT_API_RETAIN_MAX_COMPLETION_TOKENS).
|
|
86
|
-
# client_deadline
|
|
87
|
+
# client_deadline 310 s — the deadline of the DURABILITY path (the
|
|
87
88
|
# out-of-hook backlog drain, #3599), deliberately
|
|
88
89
|
# not the live Stop hook's 15s. The live path is
|
|
89
90
|
# allowed to miss its deadline: it enqueues to
|
|
@@ -96,20 +97,60 @@ from typing import Optional
|
|
|
96
97
|
# literal, and `src/setup/hindsight.ts`
|
|
97
98
|
# (`HINDSIGHT_RETAIN_CLIENT_DEADLINE_S`, #3611)
|
|
98
99
|
# mirrors it as the client half of that PR's
|
|
99
|
-
# `
|
|
100
|
-
# assertion
|
|
101
|
-
#
|
|
102
|
-
#
|
|
100
|
+
# `hindsight per-call timeout < client deadline`
|
|
101
|
+
# assertion. A test in `tests/setup/hindsight.test.ts`
|
|
102
|
+
# enforces that the two stay equal, so the mirror
|
|
103
|
+
# is checked, not merely asserted in prose.
|
|
104
|
+
# WAS 280.0. Raised to 310 when the retain
|
|
105
|
+
# per-call timeout became derived from the litellm
|
|
106
|
+
# routing chain (`local 200 + fallback 90 +
|
|
107
|
+
# margin 10 = 300`) instead of from the token
|
|
108
|
+
# budget alone: #3611's 204s could not cover that
|
|
109
|
+
# chain, so the retain OpenRouter fallback hop had
|
|
110
|
+
# 4s of headroom and could never complete. 310 is
|
|
111
|
+
# that 300 plus the same one margin, so the plugin
|
|
112
|
+
# always outlives hindsight by construction. It is
|
|
113
|
+
# NOT a hand-set number on either side — change
|
|
114
|
+
# `src/litellm/timeout-budget.ts` and both move.
|
|
103
115
|
#
|
|
104
|
-
#
|
|
116
|
+
# deadline_safety 0.7 — the FRACTION of the deadline a maximally-sized
|
|
117
|
+
# part is allowed to consume. See below; this
|
|
118
|
+
# input did not exist until #3693 and was
|
|
119
|
+
# effectively 1.0.
|
|
105
120
|
#
|
|
106
|
-
#
|
|
107
|
-
#
|
|
108
|
-
#
|
|
109
|
-
#
|
|
121
|
+
# floor(310 × 0.7 / 18.4) = 11 chunks → 11 × 3000 = 33,000 chars
|
|
122
|
+
#
|
|
123
|
+
# WHY THERE IS A SAFETY FRACTION AT ALL (#3693). Without it the bound was
|
|
124
|
+
# `floor(deadline / latency)`, which sizes a maximally-sized part to consume
|
|
125
|
+
# ~100% of the deadline BY CONSTRUCTION: 16 × 18.4 = 294.4s against a 310s
|
|
126
|
+
# deadline is 15.6s — 5% — of headroom for the POST/response, server queueing,
|
|
127
|
+
# and any chunk slower than the n=752 MEAN the latency input is. Half the
|
|
128
|
+
# extraction calls are slower than the mean; a part sized to the mean therefore
|
|
129
|
+
# misses the deadline roughly half the time. `drain_pending._backlog_timeout()`
|
|
130
|
+
# defaults to the SAME deadline, so the drainer inherits the same zero margin,
|
|
131
|
+
# times the entry out, bumps its attempt count, and after MAX_ATTEMPTS ages it
|
|
132
|
+
# to `.dead`. That is not a theory: on this fleet every `.dead` marker measured
|
|
133
|
+
# on 2026-07-26 held content of 16,076–44,568 chars — every one of them UNDER
|
|
134
|
+
# the then-current 45,000 bound. The bound was manufacturing dead memories.
|
|
135
|
+
#
|
|
136
|
+
# 0.7 is not a taste call either: it is the utilisation at which the observed
|
|
137
|
+
# per-chunk latency distribution fits, and it independently matches the 30,000
|
|
138
|
+
# char limit the host-side stopgap converged on empirically before this landed.
|
|
139
|
+
# At 0.7 a maximally-sized part is 11 × 18.4 = 202.4s against 310s, leaving
|
|
140
|
+
# 107.6s — enough for a chunk distribution ~50% worse than its own mean.
|
|
141
|
+
#
|
|
142
|
+
# The trade is more parts per logical memory (33,000 rather than 48,000 chars
|
|
143
|
+
# each). That costs nothing in total LLM work — the chunk count across the
|
|
144
|
+
# whole memory is unchanged, only its grouping — and each part now finishes.
|
|
145
|
+
# A part that finishes is worth strictly more than a larger part that does not.
|
|
110
146
|
DEFAULT_RETAIN_CHUNK_SIZE = 3000
|
|
111
147
|
DEFAULT_RETAIN_CHUNK_LATENCY_S = 18.4
|
|
112
|
-
DEFAULT_RETAIN_CLIENT_DEADLINE_S =
|
|
148
|
+
DEFAULT_RETAIN_CLIENT_DEADLINE_S = 310.0
|
|
149
|
+
|
|
150
|
+
# Fraction of the client deadline a maximally-sized part may consume.
|
|
151
|
+
# Clamped to (0, 1]: a value above 1.0 would size parts to overrun the
|
|
152
|
+
# deadline outright, which is the defect this input exists to prevent.
|
|
153
|
+
DEFAULT_RETAIN_DEADLINE_SAFETY = 0.7
|
|
113
154
|
|
|
114
155
|
# Absolute floor: one chunk. A bound below one chunk would split every
|
|
115
156
|
# transcript into extraction-sized confetti and is never the right answer.
|
|
@@ -140,6 +181,18 @@ def retain_client_deadline() -> float:
|
|
|
140
181
|
return _env_number("HINDSIGHT_RETAIN_CLIENT_DEADLINE_S", DEFAULT_RETAIN_CLIENT_DEADLINE_S)
|
|
141
182
|
|
|
142
183
|
|
|
184
|
+
def retain_deadline_safety() -> float:
|
|
185
|
+
"""Fraction of the client deadline a maximally-sized part may consume.
|
|
186
|
+
|
|
187
|
+
Clamped to ``(0, 1]``. A value above 1.0 is not honoured: it would size
|
|
188
|
+
parts to overrun the deadline by construction, which is exactly the defect
|
|
189
|
+
(#3693) this input exists to prevent, so it is treated as "no margin at
|
|
190
|
+
all" — 1.0 — rather than as a licence to go further.
|
|
191
|
+
"""
|
|
192
|
+
value = _env_number("HINDSIGHT_RETAIN_DEADLINE_SAFETY", DEFAULT_RETAIN_DEADLINE_SAFETY)
|
|
193
|
+
return min(1.0, value)
|
|
194
|
+
|
|
195
|
+
|
|
143
196
|
def retain_content_limit() -> int:
|
|
144
197
|
"""Max chars of retain content that can complete inside the client deadline.
|
|
145
198
|
|
|
@@ -159,7 +212,12 @@ def retain_content_limit() -> int:
|
|
|
159
212
|
latency = _env_number("HINDSIGHT_RETAIN_CHUNK_LATENCY_S", DEFAULT_RETAIN_CHUNK_LATENCY_S)
|
|
160
213
|
deadline = retain_client_deadline()
|
|
161
214
|
|
|
162
|
-
|
|
215
|
+
# A FRACTION of the deadline, not all of it (#3693). Sizing to the whole
|
|
216
|
+
# deadline leaves a maximally-sized part no headroom for the POST itself,
|
|
217
|
+
# server queueing, or a chunk slower than the MEAN `latency` is measured
|
|
218
|
+
# as — so the part times out, the drain bumps its attempt count, and it
|
|
219
|
+
# ages to `.dead`. See the derivation block at the top of this module.
|
|
220
|
+
chunks = int((deadline * retain_deadline_safety()) // latency)
|
|
163
221
|
if chunks < 1:
|
|
164
222
|
chunks = 1
|
|
165
223
|
return max(MIN_RETAIN_CONTENT_CHARS, chunk_size * chunks)
|
|
@@ -1296,6 +1296,56 @@ def _combine_context(base, nudge) -> str:
|
|
|
1296
1296
|
return "\n\n".join(parts)
|
|
1297
1297
|
|
|
1298
1298
|
|
|
1299
|
+
def degraded_recall_notice(bank_id, bank_timings) -> str:
|
|
1300
|
+
"""Switchroom #3619 — return the degraded-recall disclosure for this turn,
|
|
1301
|
+
or "" when the agent's own bank answered.
|
|
1302
|
+
|
|
1303
|
+
Until now a recall whose own bank timed out was indistinguishable, from the
|
|
1304
|
+
agent's side, from a bank that genuinely held nothing relevant: both
|
|
1305
|
+
produced an empty block and silence. That ambiguity is what let a measured
|
|
1306
|
+
~90% own-bank timeout rate run for weeks unnoticed while every agent's
|
|
1307
|
+
CLAUDE.md asserted recall "auto-fires on every inbound message" — the agent
|
|
1308
|
+
had no way to know it was answering from an empty context, so it never said
|
|
1309
|
+
so and the operator never saw it.
|
|
1310
|
+
|
|
1311
|
+
Only the agent's OWN bank warrants the notice: additional banks (a shared
|
|
1312
|
+
profile bank, say) are supplementary, and a side-bank timeout does not mean
|
|
1313
|
+
the agent lost its own memory. Matching is by `bank_id`, never by position
|
|
1314
|
+
in `bank_timings` — the fan-out order is not stable.
|
|
1315
|
+
|
|
1316
|
+
Kept to a single short line on purpose: this fires on an already-degraded
|
|
1317
|
+
turn, and a verbose block would spend the very budget the degradation is
|
|
1318
|
+
starving. The caller must keep it OUT of the cached context (see
|
|
1319
|
+
`_combine_context`) — it is per-turn state and would otherwise replay on a
|
|
1320
|
+
later healthy cache hit.
|
|
1321
|
+
"""
|
|
1322
|
+
if not bank_id or not bank_timings:
|
|
1323
|
+
return ""
|
|
1324
|
+
own = next(
|
|
1325
|
+
(
|
|
1326
|
+
bt
|
|
1327
|
+
for bt in bank_timings
|
|
1328
|
+
if isinstance(bt, dict) and bt.get("bank_id") == bank_id
|
|
1329
|
+
),
|
|
1330
|
+
None,
|
|
1331
|
+
)
|
|
1332
|
+
if not own:
|
|
1333
|
+
return ""
|
|
1334
|
+
if own.get("timed_out"):
|
|
1335
|
+
reason = "timed out"
|
|
1336
|
+
elif own.get("errored"):
|
|
1337
|
+
reason = "was unreachable"
|
|
1338
|
+
else:
|
|
1339
|
+
return ""
|
|
1340
|
+
return (
|
|
1341
|
+
f"[Hindsight] Memory recall was DEGRADED this turn: your own bank "
|
|
1342
|
+
f"('{bank_id}') {reason}, so the memories below (if any) are "
|
|
1343
|
+
f"incomplete and may be missing entirely. Treat an absence of "
|
|
1344
|
+
f"relevant memory as UNKNOWN, not as 'nothing was remembered' — "
|
|
1345
|
+
f"say so rather than asserting there is no prior context."
|
|
1346
|
+
)
|
|
1347
|
+
|
|
1348
|
+
|
|
1299
1349
|
def main():
|
|
1300
1350
|
config = load_config()
|
|
1301
1351
|
|
|
@@ -2129,13 +2179,22 @@ def main():
|
|
|
2129
2179
|
"transcript_fallback_truncated": transcript_fallback_telemetry["truncated"],
|
|
2130
2180
|
})
|
|
2131
2181
|
|
|
2132
|
-
#
|
|
2182
|
+
# Switchroom #3619 — DEGRADED-RECALL DISCLOSURE. See
|
|
2183
|
+
# `degraded_recall_notice` for why this exists and why only the agent's
|
|
2184
|
+
# OWN bank counts.
|
|
2185
|
+
degraded_block = degraded_recall_notice(bank_id, bank_timings)
|
|
2186
|
+
|
|
2187
|
+
# If no block has content, there's nothing to inject — exit
|
|
2133
2188
|
# silently to avoid emitting an empty hookSpecificOutput. #2848: unless
|
|
2134
2189
|
# the directive-capture nudge fired, in which case emit the nudge alone
|
|
2135
2190
|
# (a correction with no memories/directives still needs the reminder).
|
|
2191
|
+
# #3619: a degraded own-bank read is likewise worth emitting alone — that
|
|
2192
|
+
# is precisely the turn on which the agent must not assume it remembers.
|
|
2136
2193
|
if not directives_block and not memories_block and not transcript_fallback_block:
|
|
2137
|
-
if nudge_block:
|
|
2138
|
-
_emit_cached_context(
|
|
2194
|
+
if degraded_block or nudge_block:
|
|
2195
|
+
_emit_cached_context(
|
|
2196
|
+
"\n\n".join([b for b in (degraded_block, nudge_block) if b])
|
|
2197
|
+
)
|
|
2139
2198
|
return
|
|
2140
2199
|
|
|
2141
2200
|
# Compose final context. Directives block goes ABOVE memories so the
|
|
@@ -2143,6 +2202,12 @@ def main():
|
|
|
2143
2202
|
# transcript fallback (#3369) goes LAST — it is the lowest-confidence
|
|
2144
2203
|
# signal (raw transcript, not synthesized fact) and only present when
|
|
2145
2204
|
# memories_block is empty by construction.
|
|
2205
|
+
#
|
|
2206
|
+
# #3619's degraded notice is deliberately NOT part of context_message: like
|
|
2207
|
+
# the #2848 nudge it is per-turn state, and this string is what gets cached
|
|
2208
|
+
# and written to LAST_RECALL_STATE. Caching it would replay "recall was
|
|
2209
|
+
# DEGRADED" on later healthy cache hits; it is prepended at emit time
|
|
2210
|
+
# instead, so a cache hit re-derives the turn's real condition.
|
|
2146
2211
|
parts = []
|
|
2147
2212
|
if directives_block:
|
|
2148
2213
|
parts.append(directives_block)
|
|
@@ -2177,11 +2242,15 @@ def main():
|
|
|
2177
2242
|
|
|
2178
2243
|
# Output JSON for Claude Code hook system. #2848: append the
|
|
2179
2244
|
# directive-capture nudge (if it fired) at emit time — it's kept out of
|
|
2180
|
-
# the cached / last-recall context above so it can't go stale.
|
|
2245
|
+
# the cached / last-recall context above so it can't go stale. #3619: the
|
|
2246
|
+
# degraded-recall notice is prepended for the same reason, and goes FIRST
|
|
2247
|
+
# because it changes how everything after it should be read.
|
|
2181
2248
|
output = {
|
|
2182
2249
|
"hookSpecificOutput": {
|
|
2183
2250
|
"hookEventName": "UserPromptSubmit",
|
|
2184
|
-
"additionalContext": _combine_context(
|
|
2251
|
+
"additionalContext": _combine_context(
|
|
2252
|
+
_combine_context(degraded_block, context_message), nudge_block
|
|
2253
|
+
),
|
|
2185
2254
|
}
|
|
2186
2255
|
}
|
|
2187
2256
|
json.dump(output, sys.stdout)
|