switchroom 0.17.6 → 0.18.3
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/bin/workspace-dynamic-hook.sh +12 -13
- package/dist/agent-scheduler/index.js +65 -5
- package/dist/auth-broker/index.js +6623 -514
- package/dist/cli/notion-write-pretool.mjs +64 -4
- package/dist/cli/switchroom.js +1888 -1162
- package/dist/host-control/main.js +6306 -162
- package/dist/vault/approvals/kernel-server.js +6014 -202
- package/dist/vault/broker/server.js +6741 -940
- package/package.json +1 -1
- package/profiles/_base/settings.json.hbs +2 -2
- package/profiles/_base/start.sh.hbs +218 -25
- package/profiles/coding/CLAUDE.md.hbs +1 -1
- package/profiles/default/CLAUDE.md +116 -0
- package/profiles/default/CLAUDE.md.hbs +2 -2
- package/profiles/executive-assistant/CLAUDE.md.hbs +1 -1
- package/profiles/health-coach/CLAUDE.md.hbs +1 -1
- package/skills/mental-model-curator/SKILL.md +162 -0
- package/telegram-plugin/auth-snapshot-format.ts +22 -24
- package/telegram-plugin/bridge/bridge.ts +80 -1
- package/telegram-plugin/bridge/ipc-client.ts +19 -0
- package/telegram-plugin/bridge/permission-ledger.ts +61 -0
- package/telegram-plugin/consolidation-legibility.ts +279 -0
- package/telegram-plugin/context-exhaustion.ts +124 -0
- package/telegram-plugin/dist/bridge/bridge.js +85 -1
- package/telegram-plugin/dist/gateway/gateway.js +25802 -8488
- package/telegram-plugin/dist/server.js +86 -2
- package/telegram-plugin/feed-heartbeat-climb.ts +206 -0
- package/telegram-plugin/gateway/activity-card-store.ts +369 -0
- package/telegram-plugin/gateway/gateway.ts +1861 -172
- package/telegram-plugin/gateway/inbound-delivery-gate.ts +26 -0
- package/telegram-plugin/gateway/inbound-spool.ts +22 -0
- package/telegram-plugin/gateway/mental-model-propose-card.ts +69 -0
- package/telegram-plugin/gateway/mental-model-propose-diff.ts +171 -0
- package/telegram-plugin/gateway/mental-model-propose-inbound-builders.ts +147 -0
- package/telegram-plugin/gateway/mental-model-propose-resolve.ts +201 -0
- package/telegram-plugin/gateway/missed-approvals-card.ts +161 -0
- package/telegram-plugin/gateway/missed-approvals-store.ts +167 -0
- package/telegram-plugin/gateway/model-command.ts +70 -10
- package/telegram-plugin/gateway/permission-rearm.ts +115 -0
- package/telegram-plugin/gateway/scoped-grant-store.ts +89 -0
- package/telegram-plugin/memory-legibility.ts +217 -0
- package/telegram-plugin/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -0
- package/telegram-plugin/package.json +6 -0
- package/telegram-plugin/quota-watch.ts +4 -6
- package/telegram-plugin/registry/turns-schema.test.ts +97 -0
- package/telegram-plugin/registry/turns-schema.ts +78 -0
- package/telegram-plugin/render/ir.ts +209 -0
- package/telegram-plugin/render/parse.ts +363 -0
- package/telegram-plugin/render/render.ts +440 -0
- package/telegram-plugin/render/rich-render.ts +72 -0
- package/telegram-plugin/scoped-approval.ts +59 -0
- package/telegram-plugin/silent-end.ts +78 -0
- package/telegram-plugin/stream-controller.ts +14 -3
- package/telegram-plugin/subagent-watcher.ts +60 -6
- package/telegram-plugin/tests/activity-card-store.test.ts +530 -0
- package/telegram-plugin/tests/activity-card-wiring.test.ts +88 -0
- package/telegram-plugin/tests/auth-command-format2.test.ts +1 -1
- package/telegram-plugin/tests/auth-snapshot-format.test.ts +30 -16
- package/telegram-plugin/tests/claude-code-event-contract.test.ts +48 -0
- package/telegram-plugin/tests/consolidation-legibility.test.ts +224 -0
- package/telegram-plugin/tests/emission-authority-facade.test.ts +25 -10
- package/telegram-plugin/tests/feed-heartbeat-liveness-open.test.ts +44 -9
- package/telegram-plugin/tests/feed-survival.test.ts +39 -0
- package/telegram-plugin/tests/gateway-boot-marker-clear.test.ts +3 -3
- package/telegram-plugin/tests/gateway-session-model-relaunch.test.ts +81 -0
- package/telegram-plugin/tests/inbound-emit-after-intercepts.test.ts +82 -0
- package/telegram-plugin/tests/inbound-spool.test.ts +105 -0
- package/telegram-plugin/tests/liveness-tracker.test.ts +228 -0
- package/telegram-plugin/tests/memory-legibility.test.ts +216 -0
- package/telegram-plugin/tests/mental-model-propose-callback-gate.test.ts +67 -0
- package/telegram-plugin/tests/mental-model-propose-card.test.ts +56 -0
- package/telegram-plugin/tests/mental-model-propose-diff.test.ts +201 -0
- package/telegram-plugin/tests/mental-model-propose-inbound-builders.test.ts +68 -0
- package/telegram-plugin/tests/mental-model-propose-resolve.test.ts +157 -0
- package/telegram-plugin/tests/missed-approvals-card.test.ts +145 -0
- package/telegram-plugin/tests/missed-approvals-store.test.ts +147 -0
- package/telegram-plugin/tests/missed-approvals-wiring.test.ts +89 -0
- package/telegram-plugin/tests/model-command.test.ts +193 -16
- package/telegram-plugin/tests/narrative-render.test.ts +125 -0
- package/telegram-plugin/tests/orphaned-reply-rearm.test.ts +123 -163
- package/telegram-plugin/tests/permission-ledger.test.ts +166 -0
- package/telegram-plugin/tests/permission-no-repeat-wiring.test.ts +1 -1
- package/telegram-plugin/tests/permission-rearm-wiring.test.ts +175 -0
- package/telegram-plugin/tests/permission-rearm.test.ts +126 -0
- package/telegram-plugin/tests/quota-watch.test.ts +1 -4
- package/telegram-plugin/tests/rapid-fire-delivery-ordering.test.ts +149 -0
- package/telegram-plugin/tests/render/parse-torture.test.ts +136 -0
- package/telegram-plugin/tests/render/parse.test.ts +393 -0
- package/telegram-plugin/tests/render/render.test.ts +436 -0
- package/telegram-plugin/tests/render/rich-render.test.ts +85 -0
- package/telegram-plugin/tests/scoped-grant-persist.test.ts +223 -0
- package/telegram-plugin/tests/silent-end-transport.test.ts +290 -0
- package/telegram-plugin/tests/silent-turn-climb-transport.test.ts +337 -0
- package/telegram-plugin/tests/subagent-watcher.test.ts +139 -0
- package/telegram-plugin/tests/telegram-activity-visibility-integration.test.ts +155 -1
- package/telegram-plugin/tests/worktree-watch-cwds.test.ts +198 -0
- package/telegram-plugin/turn-liveness-floor.ts +35 -1
- package/telegram-plugin/uat/assertions.ts +88 -4
- package/telegram-plugin/uat/feed-matcher.test.ts +69 -0
- package/telegram-plugin/uat/scenarios/fuzz-liveness-climb-dm.test.ts +155 -0
- package/telegram-plugin/uat/scenarios/jtbd-directive-capture-nudge-dm.test.ts +185 -0
- package/telegram-plugin/uat/scenarios/jtbd-liveness-climb-channel.test.ts +192 -0
- package/telegram-plugin/uat/scenarios/jtbd-liveness-climb-dm.test.ts +220 -0
- package/telegram-plugin/uat/scenarios/jtbd-liveness-narration-channel.test.ts +137 -0
- package/telegram-plugin/uat/scenarios/jtbd-liveness-narration-dm.test.ts +148 -0
- package/telegram-plugin/uat/scenarios/jtbd-memory-legibility-channel.test.ts +66 -0
- package/telegram-plugin/uat/scenarios/jtbd-memory-legibility-dm.test.ts +61 -0
- package/telegram-plugin/uat/scenarios/jtbd-rich-formatting-render-dm.test.ts +99 -7
- package/telegram-plugin/uat/scenarios/silent-end-recovery-channel.test.ts +136 -0
- package/telegram-plugin/uat/scenarios/silent-end-recovery-dm.test.ts +24 -2
- package/telegram-plugin/worktree-watch-cwds.ts +135 -0
- package/vendor/hindsight-memory/hooks/hooks.json +9 -0
- package/vendor/hindsight-memory/scripts/__pycache__/directive_verify.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/scripts/__pycache__/drain_pending.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/scripts/__pycache__/recall.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/scripts/__pycache__/retain.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/scripts/__pycache__/session_end.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/scripts/directive_verify.py +445 -0
- package/vendor/hindsight-memory/scripts/lib/__pycache__/__init__.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/scripts/lib/__pycache__/bank.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/scripts/lib/__pycache__/client.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/scripts/lib/__pycache__/config.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/scripts/lib/__pycache__/content.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/scripts/lib/__pycache__/daemon.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/scripts/lib/__pycache__/directives.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/scripts/lib/__pycache__/gateway_ipc.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/scripts/lib/__pycache__/llm.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/scripts/lib/__pycache__/pending.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/scripts/lib/__pycache__/state.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/scripts/lib/__pycache__/switchroom_envelope.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/scripts/lib/client.py +11 -1
- package/vendor/hindsight-memory/scripts/lib/config.py +46 -2
- package/vendor/hindsight-memory/scripts/lib/directives.py +88 -0
- package/vendor/hindsight-memory/scripts/lib/switchroom_envelope.py +77 -0
- package/vendor/hindsight-memory/scripts/recall.py +217 -10
- package/vendor/hindsight-memory/scripts/retain.py +17 -0
- package/vendor/hindsight-memory/scripts/setup_hooks.py +9 -0
- package/vendor/hindsight-memory/scripts/tests/__pycache__/__init__.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/scripts/tests/__pycache__/test_config_client_casts.cpython-313-pytest-9.1.1.pyc +0 -0
- package/vendor/hindsight-memory/scripts/tests/__pycache__/test_config_client_casts.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/scripts/tests/__pycache__/test_directive_capture_nudge.cpython-313-pytest-9.1.1.pyc +0 -0
- package/vendor/hindsight-memory/scripts/tests/__pycache__/test_directive_capture_nudge.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/scripts/tests/__pycache__/test_directive_verify.cpython-313-pytest-9.1.1.pyc +0 -0
- package/vendor/hindsight-memory/scripts/tests/__pycache__/test_directive_verify.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/scripts/tests/__pycache__/test_directives.cpython-313-pytest-9.1.1.pyc +0 -0
- package/vendor/hindsight-memory/scripts/tests/__pycache__/test_directives.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/scripts/tests/__pycache__/test_gateway_ipc.cpython-313-pytest-9.1.1.pyc +0 -0
- package/vendor/hindsight-memory/scripts/tests/__pycache__/test_gateway_ipc.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/scripts/tests/__pycache__/test_recall_context_slice.cpython-313-pytest-9.1.1.pyc +0 -0
- package/vendor/hindsight-memory/scripts/tests/__pycache__/test_recall_context_slice.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/scripts/tests/__pycache__/test_recall_integration.cpython-313-pytest-9.1.1.pyc +0 -0
- package/vendor/hindsight-memory/scripts/tests/__pycache__/test_recall_integration.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/scripts/tests/__pycache__/test_recall_tag_filters.cpython-313-pytest-9.1.1.pyc +0 -0
- package/vendor/hindsight-memory/scripts/tests/__pycache__/test_recall_tag_filters.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/scripts/tests/__pycache__/test_recall_topic_filter.cpython-313-pytest-9.1.1.pyc +0 -0
- package/vendor/hindsight-memory/scripts/tests/__pycache__/test_recall_topic_filter.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/scripts/tests/__pycache__/test_recall_trivial_skip.cpython-313-pytest-9.1.1.pyc +0 -0
- package/vendor/hindsight-memory/scripts/tests/__pycache__/test_recall_trivial_skip.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/scripts/tests/__pycache__/test_retain_window.cpython-313-pytest-9.1.1.pyc +0 -0
- package/vendor/hindsight-memory/scripts/tests/__pycache__/test_retain_window.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/scripts/tests/__pycache__/test_sender_routing.cpython-313-pytest-9.1.1.pyc +0 -0
- package/vendor/hindsight-memory/scripts/tests/__pycache__/test_sender_routing.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/scripts/tests/__pycache__/test_switchroom_envelope.cpython-313-pytest-9.1.1.pyc +0 -0
- package/vendor/hindsight-memory/scripts/tests/test_directive_capture_nudge.py +185 -0
- package/vendor/hindsight-memory/scripts/tests/test_directive_verify.py +516 -0
- package/vendor/hindsight-memory/scripts/tests/test_directives.py +49 -0
- package/vendor/hindsight-memory/scripts/tests/test_recall_integration.py +1 -0
- package/vendor/hindsight-memory/scripts/tests/test_retain_window.py +66 -1
- package/vendor/hindsight-memory/scripts/tests/test_switchroom_envelope.py +69 -0
- package/vendor/hindsight-memory/tests/__pycache__/conftest.cpython-313-pytest-9.0.3.pyc +0 -0
- package/vendor/hindsight-memory/tests/__pycache__/conftest.cpython-313-pytest-9.1.1.pyc +0 -0
- package/vendor/hindsight-memory/tests/__pycache__/test_bank.cpython-313-pytest-9.1.1.pyc +0 -0
- package/vendor/hindsight-memory/tests/__pycache__/test_bank.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/tests/__pycache__/test_client.cpython-313-pytest-9.1.1.pyc +0 -0
- package/vendor/hindsight-memory/tests/__pycache__/test_client.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/tests/__pycache__/test_config.cpython-313-pytest-9.0.3.pyc +0 -0
- package/vendor/hindsight-memory/tests/__pycache__/test_config.cpython-313-pytest-9.1.1.pyc +0 -0
- package/vendor/hindsight-memory/tests/__pycache__/test_config.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/tests/__pycache__/test_content.cpython-313-pytest-9.1.1.pyc +0 -0
- package/vendor/hindsight-memory/tests/__pycache__/test_content.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/tests/__pycache__/test_drain_pending.cpython-313-pytest-9.1.1.pyc +0 -0
- package/vendor/hindsight-memory/tests/__pycache__/test_drain_pending.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/tests/__pycache__/test_hooks.cpython-313-pytest-9.1.1.pyc +0 -0
- package/vendor/hindsight-memory/tests/__pycache__/test_hooks.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/tests/__pycache__/test_manifest.cpython-313-pytest-9.1.1.pyc +0 -0
- package/vendor/hindsight-memory/tests/__pycache__/test_manifest.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/tests/__pycache__/test_pending.cpython-313-pytest-9.1.1.pyc +0 -0
- package/vendor/hindsight-memory/tests/__pycache__/test_pending.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/tests/__pycache__/test_recall_exit_codes.cpython-313-pytest-9.1.1.pyc +0 -0
- package/vendor/hindsight-memory/tests/__pycache__/test_recall_exit_codes.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/tests/__pycache__/test_session_end_pending.cpython-313-pytest-9.1.1.pyc +0 -0
- package/vendor/hindsight-memory/tests/__pycache__/test_session_end_pending.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/tests/__pycache__/test_state.cpython-313-pytest-9.1.1.pyc +0 -0
- package/vendor/hindsight-memory/tests/__pycache__/test_state.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/tests/test_client.py +43 -0
- package/vendor/hindsight-memory/tests/test_recall_exit_codes.py +49 -2
- package/vendor/hindsight-memory/tests/test_recall_precision.py +114 -0
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Consolidation-driven chat-legible memory surface — hindsight Phase 4,
|
|
3
|
+
* the "updated what I know about Y" side (RFC
|
|
4
|
+
* `reference/rfcs/hindsight-synthesis-layers.md`, Phase 4).
|
|
5
|
+
*
|
|
6
|
+
* #2858 shipped the STORE/CORRECT side driven by deterministic tool-call
|
|
7
|
+
* observation of the interactive session (`create_directive` →
|
|
8
|
+
* "📌 remembered", `invalidate_memory` / demote → "✂️ forgot"). That path
|
|
9
|
+
* sees only what the *foreground* agent explicitly does with memory tools.
|
|
10
|
+
*
|
|
11
|
+
* The RFC also names the poll-free UPDATE side: when the *background*
|
|
12
|
+
* consolidation engine actually distils new durable observations (or
|
|
13
|
+
* supersedes stale ones) it emits a `consolidation.completed` webhook, and
|
|
14
|
+
* that is the honest signal for a terse "🧠 updated what I know about Y"
|
|
15
|
+
* line. This module is the consumer for that webhook.
|
|
16
|
+
*
|
|
17
|
+
* Two hard constraints from the RFC and the `remember-across-sessions` job
|
|
18
|
+
* shape every choice here:
|
|
19
|
+
*
|
|
20
|
+
* 1. **Sparse + material-only.** Consolidation fires on *every* retain
|
|
21
|
+
* (`retainEveryNTurns=1`), so the raw webhook stream is per-turn. A
|
|
22
|
+
* line per fire would BE the "regurgitating old facts unprompted just
|
|
23
|
+
* to prove it remembered" anti-pattern the job forbids. So we surface
|
|
24
|
+
* a line ONLY when a consolidation genuinely *stored* or *corrected* a
|
|
25
|
+
* durable memory (`detectConsolidationEvent` returns null otherwise),
|
|
26
|
+
* AND we rate-limit hard (`ConsolidationRateLimiter`) so bursts of
|
|
27
|
+
* material consolidations collapse to at most one line per interval.
|
|
28
|
+
*
|
|
29
|
+
* 2. **OFF by default.** Unlike the #2858 tool-observation path (default
|
|
30
|
+
* ON — it fires on rare, unambiguous, user-initiated tool calls), this
|
|
31
|
+
* path is driven by an unbounded background engine and by a webhook the
|
|
32
|
+
* pinned hindsight image does not yet emit. It stays OFF until an
|
|
33
|
+
* operator opts in (`SWITCHROOM_CONSOLIDATION_LEGIBILITY=1`), matching
|
|
34
|
+
* the RFC's "sparse, not per-turn … clearly gated" language.
|
|
35
|
+
*
|
|
36
|
+
* No model call, no polling, no `claude -p` — this is a pure consumer of a
|
|
37
|
+
* webhook the receiver already verified + forwarded (claude-native clean).
|
|
38
|
+
*/
|
|
39
|
+
|
|
40
|
+
import { stripMarkdown, truncate } from './card-format.js'
|
|
41
|
+
|
|
42
|
+
/** The hindsight webhook source + event this module consumes. */
|
|
43
|
+
export const HINDSIGHT_WEBHOOK_SOURCE = 'hindsight'
|
|
44
|
+
export const CONSOLIDATION_COMPLETED_EVENT = 'consolidation.completed'
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* OPT-IN — the operator must set SWITCHROOM_CONSOLIDATION_LEGIBILITY to a
|
|
48
|
+
* truthy value ('1' / 'true' / 'on' / 'yes') to enable. Default OFF: the
|
|
49
|
+
* literal opposite of the #2858 tool-observation path's default-ON switch,
|
|
50
|
+
* deliberately, because this side is driven by an unbounded background
|
|
51
|
+
* engine (RFC: "sparse, not per-turn"; "clearly gated").
|
|
52
|
+
*/
|
|
53
|
+
export function isConsolidationLegibilityEnabled(envVal: string | undefined): boolean {
|
|
54
|
+
if (envVal == null) return false
|
|
55
|
+
const v = envVal.trim().toLowerCase()
|
|
56
|
+
return v === '1' || v === 'true' || v === 'on' || v === 'yes'
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export type ConsolidationLegibilityKind = 'updated' | 'revised'
|
|
60
|
+
|
|
61
|
+
export interface ConsolidationLegibilityEvent {
|
|
62
|
+
/** `updated` — new durable observation(s) distilled; `revised` — an
|
|
63
|
+
* existing memory was superseded/invalidated by consolidation. `revised`
|
|
64
|
+
* is the higher-signal framing and wins when both happened. */
|
|
65
|
+
kind: ConsolidationLegibilityKind
|
|
66
|
+
/** Best-effort human subject ("your deploy preferences"). May be empty —
|
|
67
|
+
* the render then falls back to a bare "updated what I know about you".
|
|
68
|
+
* Raw (unescaped); `renderConsolidationLine` cleans it. */
|
|
69
|
+
topic: string
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function asString(v: unknown): string {
|
|
73
|
+
return typeof v === 'string' ? v : ''
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Coerce a numeric-ish field (number, or a numeric string) to a count ≥ 0. */
|
|
77
|
+
function asCount(v: unknown): number {
|
|
78
|
+
if (typeof v === 'number' && Number.isFinite(v)) return v > 0 ? Math.floor(v) : 0
|
|
79
|
+
if (typeof v === 'string') {
|
|
80
|
+
const n = Number(v)
|
|
81
|
+
return Number.isFinite(n) && n > 0 ? Math.floor(n) : 0
|
|
82
|
+
}
|
|
83
|
+
// An array field (e.g. `observations: [...]`) counts by length.
|
|
84
|
+
if (Array.isArray(v)) return v.length
|
|
85
|
+
return 0
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** First non-empty count across a set of tolerant field aliases. */
|
|
89
|
+
function firstCount(payload: Record<string, unknown>, keys: string[]): number {
|
|
90
|
+
for (const k of keys) {
|
|
91
|
+
const c = asCount(payload[k])
|
|
92
|
+
if (c > 0) return c
|
|
93
|
+
}
|
|
94
|
+
return 0
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Best-effort human subject of the consolidation. The webhook is RFC-only
|
|
99
|
+
* (the pinned image does not emit it yet), so tolerate a range of shapes:
|
|
100
|
+
* an explicit `subject` / `topic`, the first entry of `subjects`/`entities`,
|
|
101
|
+
* or the `subject`/`statement` of the first observation. Anything opaque
|
|
102
|
+
* (a bank_id, a uuid) is left to the empty fallback.
|
|
103
|
+
*/
|
|
104
|
+
function extractTopic(payload: Record<string, unknown>): string {
|
|
105
|
+
const direct = asString(payload.subject).trim() || asString(payload.topic).trim()
|
|
106
|
+
if (direct) return direct
|
|
107
|
+
|
|
108
|
+
for (const key of ['subjects', 'entities', 'topics']) {
|
|
109
|
+
const arr = payload[key]
|
|
110
|
+
if (Array.isArray(arr)) {
|
|
111
|
+
for (const el of arr) {
|
|
112
|
+
if (typeof el === 'string' && el.trim()) return el.trim()
|
|
113
|
+
if (el && typeof el === 'object') {
|
|
114
|
+
const name = asString((el as Record<string, unknown>).name).trim()
|
|
115
|
+
if (name) return name
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const obs = payload.observations
|
|
122
|
+
if (Array.isArray(obs) && obs.length > 0 && obs[0] && typeof obs[0] === 'object') {
|
|
123
|
+
const o = obs[0] as Record<string, unknown>
|
|
124
|
+
const sub = asString(o.subject).trim()
|
|
125
|
+
if (sub) return sub
|
|
126
|
+
const stmt = asString(o.statement).trim()
|
|
127
|
+
if (stmt) return stmt
|
|
128
|
+
}
|
|
129
|
+
return ''
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Decide whether a `consolidation.completed` payload represents a MATERIAL
|
|
134
|
+
* durable change — a genuine store or correct — or null if it is a routine
|
|
135
|
+
* no-op consolidation (the common case, which must surface NO line).
|
|
136
|
+
*
|
|
137
|
+
* Pure, no I/O. Tolerant of the RFC-only payload's exact field names.
|
|
138
|
+
*/
|
|
139
|
+
export function detectConsolidationEvent(
|
|
140
|
+
payload: Record<string, unknown> | undefined,
|
|
141
|
+
): ConsolidationLegibilityEvent | null {
|
|
142
|
+
if (payload == null || typeof payload !== 'object') return null
|
|
143
|
+
|
|
144
|
+
const stored = firstCount(payload, [
|
|
145
|
+
'stored',
|
|
146
|
+
'created',
|
|
147
|
+
'added',
|
|
148
|
+
'new_observations',
|
|
149
|
+
'observations_created',
|
|
150
|
+
'observations',
|
|
151
|
+
])
|
|
152
|
+
const corrected = firstCount(payload, [
|
|
153
|
+
'corrected',
|
|
154
|
+
'invalidated',
|
|
155
|
+
'superseded',
|
|
156
|
+
'removed',
|
|
157
|
+
'demoted',
|
|
158
|
+
])
|
|
159
|
+
|
|
160
|
+
// Immaterial: nothing durable stored, nothing corrected. This is the
|
|
161
|
+
// overwhelming majority of consolidation fires — surface nothing.
|
|
162
|
+
if (stored === 0 && corrected === 0) return null
|
|
163
|
+
|
|
164
|
+
// A correction ("what I believed changed") is higher-signal than another
|
|
165
|
+
// stored fact, so it wins the framing when both happened.
|
|
166
|
+
const kind: ConsolidationLegibilityKind = corrected > 0 ? 'revised' : 'updated'
|
|
167
|
+
return { kind, topic: extractTopic(payload) }
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** HTML-escape for parse_mode:'HTML' (escape the 3 entity-significant chars). */
|
|
171
|
+
function escapeHtml(s: string): string {
|
|
172
|
+
return s.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/** Max chars of subject shown on the one-liner. */
|
|
176
|
+
const TOPIC_MAX = 120
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Render the terse one-line surface (Telegram HTML). Sent as a real
|
|
180
|
+
* `sendMessage` with notifications suppressed — a status surface, never a
|
|
181
|
+
* device ping.
|
|
182
|
+
*
|
|
183
|
+
* 🧠 <i>updated what I know</i> about "your deploy preferences"
|
|
184
|
+
* 🧠 <i>revised what I know</i> about "the old runbook"
|
|
185
|
+
* 🧠 <i>updated what I know about you.</i> (no legible subject)
|
|
186
|
+
*/
|
|
187
|
+
export function renderConsolidationLine(ev: ConsolidationLegibilityEvent): string {
|
|
188
|
+
const verb = ev.kind === 'revised' ? 'revised' : 'updated'
|
|
189
|
+
const clean = truncate(stripMarkdown(ev.topic).replace(/\s+/g, ' ').trim(), TOPIC_MAX)
|
|
190
|
+
if (clean.length === 0) return `🧠 <i>${verb} what I know about you.</i>`
|
|
191
|
+
return `🧠 <i>${verb} what I know</i> about "${escapeHtml(clean)}"`
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
export interface ConsolidationRateLimiterOptions {
|
|
195
|
+
/** Minimum gap between ANY two surfaced lines for one agent. Collapses a
|
|
196
|
+
* burst of material consolidations to at most one line per window.
|
|
197
|
+
* Default 10 min. */
|
|
198
|
+
minIntervalMs?: number
|
|
199
|
+
/** Suppress an IDENTICAL line (same kind+topic) for this long even if the
|
|
200
|
+
* min-interval has passed — stops "updated about X" repeating as the
|
|
201
|
+
* engine re-derives the same observation. Default 1 hour. */
|
|
202
|
+
dedupWindowMs?: number
|
|
203
|
+
/** Clock override (tests). */
|
|
204
|
+
now?: () => number
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const DEFAULT_MIN_INTERVAL_MS = 10 * 60 * 1000
|
|
208
|
+
const DEFAULT_DEDUP_WINDOW_MS = 60 * 60 * 1000
|
|
209
|
+
/** Cap on retained per-signature timestamps so the map can't grow unbounded
|
|
210
|
+
* under a long-lived gateway. */
|
|
211
|
+
const MAX_TRACKED_SIGNATURES = 512
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Per-agent rate limiter for the consolidation surface. Two independent
|
|
215
|
+
* gates, both must pass:
|
|
216
|
+
*
|
|
217
|
+
* - **min-interval** — at most one line per agent per `minIntervalMs`,
|
|
218
|
+
* regardless of subject. This is the anti-spam floor.
|
|
219
|
+
* - **dedup** — the same (kind+topic) signature is suppressed for
|
|
220
|
+
* `dedupWindowMs` even across the interval boundary.
|
|
221
|
+
*
|
|
222
|
+
* Stateful + long-lived: the gateway constructs ONE instance and reuses it
|
|
223
|
+
* across events. Deterministic under an injected clock (tests).
|
|
224
|
+
*/
|
|
225
|
+
export class ConsolidationRateLimiter {
|
|
226
|
+
private readonly minIntervalMs: number
|
|
227
|
+
private readonly dedupWindowMs: number
|
|
228
|
+
private readonly clock: () => number
|
|
229
|
+
private readonly lastEmit = new Map<string, number>()
|
|
230
|
+
private readonly recentSig = new Map<string, number>()
|
|
231
|
+
|
|
232
|
+
constructor(opts: ConsolidationRateLimiterOptions = {}) {
|
|
233
|
+
this.minIntervalMs = opts.minIntervalMs ?? DEFAULT_MIN_INTERVAL_MS
|
|
234
|
+
this.dedupWindowMs = opts.dedupWindowMs ?? DEFAULT_DEDUP_WINDOW_MS
|
|
235
|
+
this.clock = opts.now ?? Date.now
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* True when a line for (`agent`, `signature`) may be surfaced now, and
|
|
240
|
+
* records the emit. False (and records nothing) when either gate blocks.
|
|
241
|
+
* `signature` should be stable for identical lines (e.g. `kind:topic`).
|
|
242
|
+
*/
|
|
243
|
+
allow(agent: string, signature: string, now?: number): boolean {
|
|
244
|
+
const t = now ?? this.clock()
|
|
245
|
+
|
|
246
|
+
const last = this.lastEmit.get(agent)
|
|
247
|
+
if (last !== undefined && t - last < this.minIntervalMs) return false
|
|
248
|
+
|
|
249
|
+
const sigKey = `${agent}\0${signature}`
|
|
250
|
+
const seen = this.recentSig.get(sigKey)
|
|
251
|
+
if (seen !== undefined && t - seen < this.dedupWindowMs) return false
|
|
252
|
+
|
|
253
|
+
this.lastEmit.set(agent, t)
|
|
254
|
+
this.recentSig.set(sigKey, t)
|
|
255
|
+
this.pruneSignatures(t)
|
|
256
|
+
return true
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/** Drop signature entries older than the dedup window; hard-cap the map. */
|
|
260
|
+
private pruneSignatures(now: number): void {
|
|
261
|
+
for (const [k, ts] of this.recentSig) {
|
|
262
|
+
if (now - ts >= this.dedupWindowMs) this.recentSig.delete(k)
|
|
263
|
+
}
|
|
264
|
+
if (this.recentSig.size > MAX_TRACKED_SIGNATURES) {
|
|
265
|
+
// Evict oldest until back under the cap (insertion order ≈ age).
|
|
266
|
+
const excess = this.recentSig.size - MAX_TRACKED_SIGNATURES
|
|
267
|
+
let i = 0
|
|
268
|
+
for (const k of this.recentSig.keys()) {
|
|
269
|
+
if (i++ >= excess) break
|
|
270
|
+
this.recentSig.delete(k)
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/** Stable dedup signature for an event (kind + normalised topic). */
|
|
277
|
+
export function consolidationSignature(ev: ConsolidationLegibilityEvent): string {
|
|
278
|
+
return `${ev.kind}:${ev.topic.replace(/\s+/g, ' ').trim().toLowerCase()}`
|
|
279
|
+
}
|
|
@@ -48,3 +48,127 @@ export function shouldArmOrphanedReplyTimeout(params: {
|
|
|
48
48
|
!params.progressCardActive
|
|
49
49
|
)
|
|
50
50
|
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Default "recently streaming" window (ms) for the orphaned-reply liveness
|
|
54
|
+
* stamp. The gateway overrides this from
|
|
55
|
+
* SWITCHROOM_ORPHANED_REPLY_STREAM_WINDOW_MS; this constant is the fallback and
|
|
56
|
+
* the value the LivenessTracker tests pin against.
|
|
57
|
+
*/
|
|
58
|
+
export const ORPHANED_REPLY_STREAM_WINDOW_MS = 120_000
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Result of a fuse-expiry decision.
|
|
62
|
+
*
|
|
63
|
+
* - `rearm` — re-arm the fuse (keep the turn alive) instead of
|
|
64
|
+
* firing the synthetic turn_end backstop.
|
|
65
|
+
* - `countsAgainstCap`— this rearm (or the fire it turned into at the cap) was
|
|
66
|
+
* a working/recently-streaming rearm, i.e. subject to the
|
|
67
|
+
* ORPHANED_REPLY_MAX_REARMS cap. Human-wait rearms are
|
|
68
|
+
* uncapped and report `false` here.
|
|
69
|
+
*/
|
|
70
|
+
export interface OrphanedReplyExpiryDecision {
|
|
71
|
+
rearm: boolean
|
|
72
|
+
countsAgainstCap: boolean
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Per-turn liveness tracker for the orphaned-reply backstop.
|
|
77
|
+
*
|
|
78
|
+
* WHY THIS EXISTS (the thinking-pause fix):
|
|
79
|
+
* The orphaned-reply fuse (ORPHANED_REPLY_TIMEOUT_MS = 30 s) used to be reset
|
|
80
|
+
* ONLY by `tool_label` and `text` stream events. During a long model
|
|
81
|
+
* reasoning pause the gateway sees NO such events (thinking events carry no
|
|
82
|
+
* text), so the fuse ran down and force-ended a genuinely-live turn mid-work
|
|
83
|
+
* ("orphaned-reply timeout (30000ms) — forcing backstop" ~1 ms before
|
|
84
|
+
* turn_end, with minutes of real work still to come).
|
|
85
|
+
*
|
|
86
|
+
* THE FIX:
|
|
87
|
+
* Stamp `lastStreamEventAt` on ANY genuine stream event (via onStreamEvent,
|
|
88
|
+
* called from the gateway dispatcher entry). If a genuine event arrived
|
|
89
|
+
* within `windowMs` (default 120 s) the turn is "recently streaming" and the
|
|
90
|
+
* fuse re-arms instead of firing — a reasoning pause is survivable while a
|
|
91
|
+
* genuine multi-minute hang (no events at all) still fires.
|
|
92
|
+
*
|
|
93
|
+
* This is a small, pure, side-effect-free seam so the decision logic is unit
|
|
94
|
+
* testable without the full gateway. The gateway holds one instance per
|
|
95
|
+
* CurrentTurn (so per-turn identity / supersession semantics are unchanged) and
|
|
96
|
+
* delegates: the dispatcher calls onStreamEvent; the fire callback calls
|
|
97
|
+
* decideOnExpiry; the defensive turn_end guard reads recentlyStreaming.
|
|
98
|
+
*/
|
|
99
|
+
export class LivenessTracker {
|
|
100
|
+
/** Wall-clock (ms) of the last genuine stream event for this turn. */
|
|
101
|
+
lastStreamEventAt: number
|
|
102
|
+
/**
|
|
103
|
+
* How many times the fuse re-armed on a working / recently-streaming
|
|
104
|
+
* expiry. Bounded by ORPHANED_REPLY_MAX_REARMS. Zeroed by onStreamEvent
|
|
105
|
+
* whenever a genuine stream event lands, so the cap only bites CONSECUTIVE
|
|
106
|
+
* silent expiries (a genuinely wedged single tool with no stream at all).
|
|
107
|
+
*/
|
|
108
|
+
orphanedReplyRearmCount = 0
|
|
109
|
+
|
|
110
|
+
constructor(startedAt: number) {
|
|
111
|
+
this.lastStreamEventAt = startedAt
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Stamp liveness for a genuine stream event and reset the rearm counter.
|
|
116
|
+
*
|
|
117
|
+
* F4 predicate: everything EXCEPT the synthetic `turn_end` re-entry
|
|
118
|
+
* (durationMs === -1, the fire callback's own re-dispatch) counts as a
|
|
119
|
+
* genuine stream event. Stamping a REAL turn_end (durationMs >= 0) is
|
|
120
|
+
* harmless — the turn is ending anyway.
|
|
121
|
+
*
|
|
122
|
+
* The counter reset lives HERE (driven by the dispatcher), NOT in the fuse
|
|
123
|
+
* re-arm path — otherwise every rearm would zero its own counter and nullify
|
|
124
|
+
* the 20-cap.
|
|
125
|
+
*/
|
|
126
|
+
onStreamEvent(kind: string, durationMs: number | undefined, now: number): void {
|
|
127
|
+
if (kind === 'turn_end' && durationMs === -1) return
|
|
128
|
+
this.lastStreamEventAt = now
|
|
129
|
+
this.orphanedReplyRearmCount = 0
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** True iff a genuine stream event landed within `windowMs` of `now`. */
|
|
133
|
+
recentlyStreaming(now: number, windowMs: number): boolean {
|
|
134
|
+
return now - this.lastStreamEventAt < windowMs
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Decide what to do when the orphaned-reply fuse expires.
|
|
139
|
+
*
|
|
140
|
+
* Rearm when the turn is working OR recently streaming OR a human is being
|
|
141
|
+
* waited on. Working / recently-streaming rearms count against `maxRearms`
|
|
142
|
+
* (so a genuinely wedged single tool that never streams still surfaces after
|
|
143
|
+
* the cap). Human-wait rearms are uncapped (the human simply hasn't tapped
|
|
144
|
+
* yet). Once the cap is hit with nothing else keeping the turn alive, fire
|
|
145
|
+
* (rearm:false) — matching the existing fail-safe.
|
|
146
|
+
*
|
|
147
|
+
* Mutates `orphanedReplyRearmCount` on each rearm.
|
|
148
|
+
*/
|
|
149
|
+
decideOnExpiry(opts: {
|
|
150
|
+
working: boolean
|
|
151
|
+
humanWaiting: boolean
|
|
152
|
+
now: number
|
|
153
|
+
windowMs: number
|
|
154
|
+
maxRearms: number
|
|
155
|
+
}): OrphanedReplyExpiryDecision {
|
|
156
|
+
// Human-wait rearms are uncapped (unchanged from prior behaviour).
|
|
157
|
+
if (opts.humanWaiting) {
|
|
158
|
+
this.orphanedReplyRearmCount++
|
|
159
|
+
return { rearm: true, countsAgainstCap: false }
|
|
160
|
+
}
|
|
161
|
+
const recently = this.recentlyStreaming(opts.now, opts.windowMs)
|
|
162
|
+
if (opts.working || recently) {
|
|
163
|
+
if (this.orphanedReplyRearmCount < opts.maxRearms) {
|
|
164
|
+
this.orphanedReplyRearmCount++
|
|
165
|
+
return { rearm: true, countsAgainstCap: true }
|
|
166
|
+
}
|
|
167
|
+
// Cap reached with only working/recently-streaming keeping it alive:
|
|
168
|
+
// fire to surface the wedge.
|
|
169
|
+
return { rearm: false, countsAgainstCap: true }
|
|
170
|
+
}
|
|
171
|
+
// Nothing keeping the turn alive → fire (fail-safe).
|
|
172
|
+
return { rearm: false, countsAgainstCap: false }
|
|
173
|
+
}
|
|
174
|
+
}
|
|
@@ -24279,6 +24279,7 @@ function createIpcClient(options) {
|
|
|
24279
24279
|
onInbound,
|
|
24280
24280
|
onPermission,
|
|
24281
24281
|
onStatus,
|
|
24282
|
+
onConnect,
|
|
24282
24283
|
log = () => {},
|
|
24283
24284
|
reconnectDelayMs = 2000,
|
|
24284
24285
|
maxReconnectDelayMs = 30000,
|
|
@@ -24414,6 +24415,13 @@ function createIpcClient(options) {
|
|
|
24414
24415
|
sendRegister();
|
|
24415
24416
|
startHeartbeat();
|
|
24416
24417
|
log(`connected to ${socketPath}`);
|
|
24418
|
+
if (onConnect) {
|
|
24419
|
+
try {
|
|
24420
|
+
onConnect();
|
|
24421
|
+
} catch (err) {
|
|
24422
|
+
log(`onConnect hook threw: ${err}`);
|
|
24423
|
+
}
|
|
24424
|
+
}
|
|
24417
24425
|
resolve();
|
|
24418
24426
|
},
|
|
24419
24427
|
data(sock, data) {
|
|
@@ -24629,6 +24637,28 @@ function matchesAllowRule(rule, toolName, inputPreview) {
|
|
|
24629
24637
|
return rule === toolName;
|
|
24630
24638
|
}
|
|
24631
24639
|
|
|
24640
|
+
// bridge/permission-ledger.ts
|
|
24641
|
+
function createOutstandingPermissionLedger() {
|
|
24642
|
+
const map2 = new Map;
|
|
24643
|
+
return {
|
|
24644
|
+
add(params) {
|
|
24645
|
+
map2.set(params.request_id, params);
|
|
24646
|
+
},
|
|
24647
|
+
delete(requestId) {
|
|
24648
|
+
map2.delete(requestId);
|
|
24649
|
+
},
|
|
24650
|
+
has(requestId) {
|
|
24651
|
+
return map2.has(requestId);
|
|
24652
|
+
},
|
|
24653
|
+
all() {
|
|
24654
|
+
return [...map2.values()];
|
|
24655
|
+
},
|
|
24656
|
+
get size() {
|
|
24657
|
+
return map2.size;
|
|
24658
|
+
}
|
|
24659
|
+
};
|
|
24660
|
+
}
|
|
24661
|
+
|
|
24632
24662
|
// bridge/bridge.ts
|
|
24633
24663
|
installPluginLogger();
|
|
24634
24664
|
var STATE_DIR = process.env.TELEGRAM_STATE_DIR ?? join4(homedir3(), ".claude", "channels", "telegram");
|
|
@@ -24981,6 +25011,23 @@ var TOOL_SCHEMAS = [
|
|
|
24981
25011
|
required: ["chat_id", "key"]
|
|
24982
25012
|
}
|
|
24983
25013
|
},
|
|
25014
|
+
{
|
|
25015
|
+
name: "mental_model_propose",
|
|
25016
|
+
description: "Propose a Hindsight MENTAL MODEL for the operator to approve (agent-proposes \u2192 human-approves, hindsight Phase 5). Use this when \u2014 over real work \u2014 you notice a recurring, domain-specific question worth maintaining a standing, semantically-refreshed answer to from YOUR bank (e.g. a coach's `training-plan-state`, a lawyer's `open-matters`). You may PROPOSE but can NEVER self-approve: this renders a Telegram [Approve]/[Deny] card to the operator. On Approve the model is DECLARED \u2014 appended to your `memory.mental_models[]` in switchroom.yaml via the operator-approved config-edit path \u2014 and ensured in your bank (it then refreshes from your bank content). On Deny nothing is written. This is NOT for identity/'who is the user' (dedicated profile banks own that) and NOT a substitute for `retain` (store a fact) or `create_mental_model` where you already have direct Hindsight tools \u2014 it is the leashed, human-gated way to add a DURABLE declared model to your config. After firing this tool, END YOUR TURN cleanly \u2014 a fresh inbound arrives (`<channel source=\"mental_model_proposal_applied\">` / `mental_model_proposal_denied`) once the operator decides. Do NOT propose a model whose name is already declared (it is rejected), and do NOT spam (one card per proposal; the operator sees every one).",
|
|
25017
|
+
inputSchema: {
|
|
25018
|
+
type: "object",
|
|
25019
|
+
properties: {
|
|
25020
|
+
chat_id: { type: "string", description: "Chat to render the approval card in (use the chat_id of the user message that triggered the workflow)." },
|
|
25021
|
+
name: { type: "string", description: "Stable model name \u2014 the idempotent-ensure identity key (lowercase kebab/snake, e.g. `training-plan-state`). Must be UNIQUE among your already-declared models or the proposal is rejected." },
|
|
25022
|
+
source_query: { type: "string", description: `The reflection query the model answers, semantically refreshed from your bank content (e.g. "What is the athlete's current training plan, recent sessions, and open adjustments?"). Frame it as a DOMAIN question, never an identity question.` },
|
|
25023
|
+
reason: { type: "string", description: 'REQUIRED in practice \u2014 one-line rationale rendered on the card (e.g. "I keep re-deriving the plan state every session; a standing model would save the lookup"). Omitting it renders "why: not provided" and the operator will usually Deny.' },
|
|
25024
|
+
refresh_after_consolidation: { type: "boolean", description: "Refresh this model after each consolidation. Defaults OFF \u2014 refresh adds bounded background model-spend + timeout risk (RFC Phase 5). Only set true when the model genuinely needs to track fast-moving state." },
|
|
25025
|
+
max_tokens: { type: "number", description: "Optional cap on the synthesized model's token size." },
|
|
25026
|
+
message_thread_id: { type: "string", description: "Forum topic thread ID. Auto-applied from the last inbound message if not specified." }
|
|
25027
|
+
},
|
|
25028
|
+
required: ["chat_id", "name", "source_query"]
|
|
25029
|
+
}
|
|
25030
|
+
},
|
|
24984
25031
|
{
|
|
24985
25032
|
name: "linear_agent_activity",
|
|
24986
25033
|
description: 'Emit a structured Linear AgentActivity against an agent session (#2298). Use this ONLY inside a turn that was woken by a Linear agent session (the inbound carries meta.source="linear" and meta.agent_session_id) \u2014 pass that agent_session_id back here. Linear renders activities as status chips + a timeline on the issue, so the human sees acknowledge \u2192 work \u2192 result. Emit a `thought` within ~10s of being woken so the session does not look dead, then `message`(s) as you make progress, and finally exactly one terminal `complete` (work done) or `error` (you could not proceed). body is required for thought/message/error and optional for complete. Resolves the agent\'s Linear app token from the vault; on VAULT-BROKER-DENIED it returns an error instructing you to vault_request_access for `linear/<agent>/token`.',
|
|
@@ -25104,6 +25151,28 @@ mcp.setRequestHandler(CallToolRequestSchema2, async (req) => {
|
|
|
25104
25151
|
}
|
|
25105
25152
|
});
|
|
25106
25153
|
var sessionAllowRules = new Set;
|
|
25154
|
+
var outstandingPermissions = createOutstandingPermissionLedger();
|
|
25155
|
+
var permissionRearmEnabled = process.env.SWITCHROOM_PERMISSION_REARM !== "0";
|
|
25156
|
+
function flushOutstandingPermissionRequests() {
|
|
25157
|
+
if (!permissionRearmEnabled)
|
|
25158
|
+
return;
|
|
25159
|
+
if (!ipc || !ipc.isConnected())
|
|
25160
|
+
return;
|
|
25161
|
+
const pending = outstandingPermissions.all();
|
|
25162
|
+
if (pending.length === 0)
|
|
25163
|
+
return;
|
|
25164
|
+
process.stderr.write(`telegram bridge: re-sending ${pending.length} outstanding permission request(s) on gateway (re)connect
|
|
25165
|
+
`);
|
|
25166
|
+
for (const p of pending) {
|
|
25167
|
+
ipc.sendPermissionRequest({
|
|
25168
|
+
type: "permission_request",
|
|
25169
|
+
requestId: p.request_id,
|
|
25170
|
+
toolName: p.tool_name,
|
|
25171
|
+
description: p.description,
|
|
25172
|
+
inputPreview: p.input_preview
|
|
25173
|
+
});
|
|
25174
|
+
}
|
|
25175
|
+
}
|
|
25107
25176
|
mcp.setNotificationHandler(exports_external.object({
|
|
25108
25177
|
method: exports_external.literal("notifications/claude/channel/permission_request"),
|
|
25109
25178
|
params: exports_external.object({
|
|
@@ -25125,9 +25194,22 @@ mcp.setNotificationHandler(exports_external.object({
|
|
|
25125
25194
|
return;
|
|
25126
25195
|
}
|
|
25127
25196
|
}
|
|
25197
|
+
if (permissionRearmEnabled) {
|
|
25198
|
+
outstandingPermissions.add({
|
|
25199
|
+
request_id: params.request_id,
|
|
25200
|
+
tool_name: params.tool_name,
|
|
25201
|
+
description: params.description,
|
|
25202
|
+
input_preview: params.input_preview
|
|
25203
|
+
});
|
|
25204
|
+
}
|
|
25128
25205
|
if (!ipc || !ipc.isConnected()) {
|
|
25129
|
-
|
|
25206
|
+
if (permissionRearmEnabled) {
|
|
25207
|
+
process.stderr.write(`telegram bridge: permission_request buffered (gateway offline), will re-send on reconnect ` + `request_id=${params.request_id}
|
|
25208
|
+
`);
|
|
25209
|
+
} else {
|
|
25210
|
+
process.stderr.write(`telegram bridge: permission_request received but not connected to gateway
|
|
25130
25211
|
`);
|
|
25212
|
+
}
|
|
25131
25213
|
return;
|
|
25132
25214
|
}
|
|
25133
25215
|
ipc.sendPermissionRequest({
|
|
@@ -25155,6 +25237,7 @@ function onPermission(msg) {
|
|
|
25155
25237
|
if (msg.rule) {
|
|
25156
25238
|
sessionAllowRules.add(msg.rule);
|
|
25157
25239
|
}
|
|
25240
|
+
outstandingPermissions.delete(msg.requestId);
|
|
25158
25241
|
mcp.notification({
|
|
25159
25242
|
method: "notifications/claude/channel/permission",
|
|
25160
25243
|
params: {
|
|
@@ -25287,6 +25370,7 @@ async function main() {
|
|
|
25287
25370
|
onInbound,
|
|
25288
25371
|
onPermission,
|
|
25289
25372
|
onStatus,
|
|
25373
|
+
onConnect: flushOutstandingPermissionRequests,
|
|
25290
25374
|
log: (msg) => process.stderr.write(`telegram bridge: ipc: ${msg}
|
|
25291
25375
|
`),
|
|
25292
25376
|
livenessFilePath: process.env.SWITCHROOM_BRIDGE_ALIVE_PATH ?? join4(STATE_DIR, ".bridge-alive")
|