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,162 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: mental-model-curator
|
|
3
|
+
description: >
|
|
4
|
+
Review the agent's OWN Hindsight memory bank with the most capable model and
|
|
5
|
+
PROPOSE well-formed mental models ("knowledge models") to the operator via the
|
|
6
|
+
approve/deny proposal card, instead of blindly creating them. Use to curate,
|
|
7
|
+
seed, or audit the standing models the agent maintains over its specialty.
|
|
8
|
+
Triggers on phrasings like: "curate mental models", "propose knowledge models",
|
|
9
|
+
"review my memory bank", "what standing models should I have", "bootstrap
|
|
10
|
+
mental models", "suggest mental models from my bank", "audit my mental models",
|
|
11
|
+
"what recurring questions do I keep re-deriving", and typos like "propose
|
|
12
|
+
knowlege models". Do NOT use for storing a single fact/preference/decision —
|
|
13
|
+
that is retain, not a mental model. Do NOT use for identity or "who is the
|
|
14
|
+
user" — that lives in profile banks, so never propose an identity model here
|
|
15
|
+
(the operator's review of the card, not any code guard, is the only gate — so
|
|
16
|
+
it's on you to honor).
|
|
17
|
+
Even when the operator directly asks you to CREATE one specific named model
|
|
18
|
+
whose shape you know, PROPOSE it through the approve/deny card — never call
|
|
19
|
+
create_mental_model directly (it is not pre-approved; the propose card is the
|
|
20
|
+
only sanctioned write path, Fix 1.2 / #2903).
|
|
21
|
+
allowed-tools: mcp__hindsight__list_banks mcp__hindsight__get_bank_stats mcp__hindsight__list_mental_models mcp__hindsight__get_mental_model mcp__hindsight__reflect mcp__hindsight__recall mcp__switchroom-telegram__mental_model_propose
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
# mental-model-curator — Propose standing knowledge models from your own bank
|
|
25
|
+
|
|
26
|
+
A **mental model** in Hindsight is a pre-computed semantic summary backed by
|
|
27
|
+
reflection over the memory bank — a standing answer to a recurring DOMAIN
|
|
28
|
+
question the agent keeps re-deriving (e.g. a coach's "current training plan,
|
|
29
|
+
recent sessions, and open adjustments"). This skill's job is to use your most
|
|
30
|
+
capable model to survey your OWN bank and PROPOSE a few well-earned models to
|
|
31
|
+
the operator through the approve/deny card — never to bulk-create noise.
|
|
32
|
+
|
|
33
|
+
You cannot self-approve. Every proposal renders a card the operator taps. Your
|
|
34
|
+
job is to make each proposal so obviously right that a glance is enough.
|
|
35
|
+
|
|
36
|
+
## Why propose instead of create
|
|
37
|
+
|
|
38
|
+
You do NOT have direct `create_mental_model` access — it is deliberately not
|
|
39
|
+
pre-approved (Fix 1.2 / #2903). Every mental-model write routes through
|
|
40
|
+
`mcp__switchroom-telegram__mental_model_propose`, because a
|
|
41
|
+
standing model is a persistent, always-in-context artifact with real cost
|
|
42
|
+
(recall/reflect spend, and — if `refresh_after_consolidation` is on — invisible
|
|
43
|
+
post-consolidation spend). A human should decide which of those the agent runs.
|
|
44
|
+
Bulk self-creation is how banks fill with overlapping, stale, or wrong-fact
|
|
45
|
+
models. Propose the few that are earned; let the operator ratify.
|
|
46
|
+
|
|
47
|
+
## Modes
|
|
48
|
+
|
|
49
|
+
- **Propose mode (default):** run the full workflow and fire proposal cards, one
|
|
50
|
+
at a time, up to the rate limit.
|
|
51
|
+
- **Dry-run / preview mode:** when the user says "preview", "dry run", "just
|
|
52
|
+
show me the candidates", "what would you propose", or you're testing against an
|
|
53
|
+
unfamiliar bank — run steps 1–4, then OUTPUT the ranked candidate list as text
|
|
54
|
+
(name + source_query + one-line reason) and STOP. Do NOT call
|
|
55
|
+
`mental_model_propose` in this mode. This is the safe way to run against any
|
|
56
|
+
bank.
|
|
57
|
+
|
|
58
|
+
## Workflow
|
|
59
|
+
|
|
60
|
+
### 1. Identify the bank and check it's worth analyzing
|
|
61
|
+
|
|
62
|
+
Resolve the session's bank (`mcp__hindsight__list_banks`, or the bank bound to
|
|
63
|
+
this agent's session). Then pull `mcp__hindsight__get_bank_stats`.
|
|
64
|
+
|
|
65
|
+
If the bank is **empty or thin**, STOP and report "not enough content to
|
|
66
|
+
synthesize models yet — the bank needs more accumulated memories before standing
|
|
67
|
+
models are meaningful." Synthesizing over an empty or thin bank produces
|
|
68
|
+
confident noise: models with no grounding that read as authoritative. A handful
|
|
69
|
+
of memories is not a domain. Do not propose against a thin bank, even in dry-run.
|
|
70
|
+
|
|
71
|
+
### 2. Read what already exists (dedupe)
|
|
72
|
+
|
|
73
|
+
Call `mcp__hindsight__list_mental_models`, and `get_mental_model` on each to read
|
|
74
|
+
its `source_query`. Build a dedupe set of the questions already covered.
|
|
75
|
+
|
|
76
|
+
The propose flow hard-rejects an EXACT-name duplicate before a card posts, but
|
|
77
|
+
that is not enough — a near-duplicate under a fresh name is still waste. You must
|
|
78
|
+
**semantically** dedupe: if a candidate's source_query asks substantially the
|
|
79
|
+
same thing as an existing model (even reworded), drop it. Only propose genuinely
|
|
80
|
+
new coverage.
|
|
81
|
+
|
|
82
|
+
### 3. Survey recurring themes
|
|
83
|
+
|
|
84
|
+
Use `mcp__hindsight__reflect` and `mcp__hindsight__recall` to find the DOMAIN
|
|
85
|
+
questions this agent keeps re-deriving — the standing state of its specialty.
|
|
86
|
+
Good prompts: "What questions do I repeatedly answer from scratch?", "What are
|
|
87
|
+
the recurring themes in this bank?", "What standing state would I want summarized
|
|
88
|
+
before every relevant turn?" Cluster the answers into candidate standing
|
|
89
|
+
questions. Each cluster must be backed by memories the bank ACTUALLY holds — if
|
|
90
|
+
you can't point to the underlying content, it's not a candidate.
|
|
91
|
+
|
|
92
|
+
### 4. Frame each candidate correctly
|
|
93
|
+
|
|
94
|
+
- **`source_query` must be a DOMAIN question**, never an identity / "who is the
|
|
95
|
+
user" question. Identity lives in dedicated profile banks; never propose an
|
|
96
|
+
identity-based model — the operator, not the platform, is the only gate, so
|
|
97
|
+
this is on you to honor (one previously caused a wrong-fact contradiction bug).
|
|
98
|
+
If a candidate is really "facts about the user", discard it.
|
|
99
|
+
- **`refresh_after_consolidation` defaults OFF.** Only set it true when the model
|
|
100
|
+
genuinely tracks fast-moving state that must be current the moment memory
|
|
101
|
+
consolidates. It adds invisible post-consolidation spend and timeout risk, so
|
|
102
|
+
the default answer is off.
|
|
103
|
+
- **Keep models tight.** recall/reflect tiers cap around ~1024 tokens; a model
|
|
104
|
+
that tries to summarize everything summarizes nothing. Set a modest `max_tokens`
|
|
105
|
+
if the default would overrun. Narrow, answerable source_queries beat broad ones.
|
|
106
|
+
- **Few models per bank.** Each must be earned by content the bank holds. Quality
|
|
107
|
+
over quantity — a bank with three sharp models beats one with ten vague ones.
|
|
108
|
+
|
|
109
|
+
### 5. Propose within limits
|
|
110
|
+
|
|
111
|
+
Rank the surviving candidates by how much re-derivation they save and how well
|
|
112
|
+
the bank supports them. Propose only the **top few**.
|
|
113
|
+
|
|
114
|
+
**HARD LIMIT: 5 proposal cards per hour per agent (sliding window).** Batch and
|
|
115
|
+
prioritize — never fire one card per cluster. If you have more than 5 worthwhile
|
|
116
|
+
candidates, propose the best 5 and mention the rest in text for a later pass.
|
|
117
|
+
|
|
118
|
+
Propose **one at a time**:
|
|
119
|
+
|
|
120
|
+
```
|
|
121
|
+
mcp__switchroom-telegram__mental_model_propose(
|
|
122
|
+
chat_id=<the current chat_id>,
|
|
123
|
+
name=<slug>,
|
|
124
|
+
source_query=<the domain question>,
|
|
125
|
+
reason=<CRISP one line — the operator sees this on the card and denies vague ones>,
|
|
126
|
+
... # refresh_after_consolidation / max_tokens only when justified above
|
|
127
|
+
)
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
The `reason` is load-bearing: it's the one line the operator reads before
|
|
131
|
+
tapping. "Tracks the athlete's current plan + open adjustments so I stop
|
|
132
|
+
rebuilding it each session" earns a tap; "useful model" gets denied.
|
|
133
|
+
|
|
134
|
+
After firing a card, **END THE TURN CLEANLY.** The flow resumes on its own via a
|
|
135
|
+
synthetic inbound (`mental_model_proposal_applied` / `mental_model_proposal_denied`)
|
|
136
|
+
when the operator taps. Do not loop, do not poll, do not fire the next card in the
|
|
137
|
+
same turn. You cannot self-approve — that's by design.
|
|
138
|
+
|
|
139
|
+
### 6. Scheduled-sweep note (future extension)
|
|
140
|
+
|
|
141
|
+
Unattended cron proposing is **NOT supported today.** `mental_model_propose`
|
|
142
|
+
needs a live turn with an operator present to tap the card — firing cards into an
|
|
143
|
+
empty topic at 3am is useless and noisy. If invoked from a scheduled sweep,
|
|
144
|
+
**STAGE** the ranked candidates (retain them, or hold them) and surface them on
|
|
145
|
+
the next interactive turn instead of firing cards unattended. Treat live-turn,
|
|
146
|
+
operator-present proposing as the only supported path for now.
|
|
147
|
+
|
|
148
|
+
## Anti-patterns
|
|
149
|
+
|
|
150
|
+
- ❌ Proposing against an empty or thin bank — synthesizes to noise.
|
|
151
|
+
- ❌ An identity / "who is the user" source_query — forbidden; profile banks own that.
|
|
152
|
+
- ❌ Ten near-duplicate models under different names — semantically dedupe first.
|
|
153
|
+
- ❌ Firing a card per cluster — respect the 5/hour limit; propose the top few.
|
|
154
|
+
- ❌ `refresh_after_consolidation: true` by default — it's an invisible cost; leave it off unless the model tracks fast-moving state.
|
|
155
|
+
- ❌ A vague `reason` — the operator denies it and you've burned a slot.
|
|
156
|
+
- ❌ Looping/polling after a proposal — end the turn; the resume inbound wakes you.
|
|
157
|
+
|
|
158
|
+
## Output
|
|
159
|
+
|
|
160
|
+
In propose mode: a short line naming what you proposed and that the operator's
|
|
161
|
+
tap will resume the flow. In dry-run mode: the ranked candidate list (name +
|
|
162
|
+
source_query + reason) and nothing fired.
|
|
@@ -447,30 +447,27 @@ function pctCells(
|
|
|
447
447
|
}
|
|
448
448
|
|
|
449
449
|
/**
|
|
450
|
-
* The
|
|
451
|
-
*
|
|
452
|
-
*
|
|
453
|
-
*
|
|
450
|
+
* The per-window reset cell (`5h resets` / `7d resets`): the tz-aware absolute
|
|
451
|
+
* reset time for THAT specific window (date shown only when not today) plus the
|
|
452
|
+
* concise relative hint in parens — e.g. `1:20 PM (in 2h)`.
|
|
453
|
+
*
|
|
454
|
+
* Degrades in lockstep with `pctCells` so the percentage cell and its reset cell
|
|
455
|
+
* always agree: no quota → probe-failed message (or `—`); thin probe → `quota
|
|
456
|
+
* unknown`; missing reset on an otherwise-present window → `—`.
|
|
454
457
|
*/
|
|
455
|
-
function
|
|
458
|
+
function windowResetCell(
|
|
459
|
+
snap: AccountSnapshot,
|
|
460
|
+
now: Date,
|
|
461
|
+
tz: string,
|
|
462
|
+
win: '5h' | '7d',
|
|
463
|
+
): string {
|
|
456
464
|
if (!snap.quota) {
|
|
457
|
-
return snap.quotaError ? `probe failed (${snap.quotaError})` : '
|
|
465
|
+
return snap.quotaError ? `probe failed (${snap.quotaError})` : '—';
|
|
458
466
|
}
|
|
459
467
|
if (isProbeThin(snap.quota)) return 'quota unknown';
|
|
460
|
-
const
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
const win = bindingWindow(q);
|
|
464
|
-
const reset = win === '5h' ? q.fiveHourResetAt : q.sevenDayResetAt;
|
|
465
|
-
if (!reset) return 'back — (reset unknown)';
|
|
466
|
-
return `back ${formatStatusTime(reset, now, tz)} (in ${formatRelative(reset, now)})`;
|
|
467
|
-
}
|
|
468
|
-
// healthy / throttling — show the soonest refill across both windows.
|
|
469
|
-
const fiveIn = q.fiveHourResetAt ? q.fiveHourResetAt.getTime() - now.getTime() : Infinity;
|
|
470
|
-
const sevenIn = q.sevenDayResetAt ? q.sevenDayResetAt.getTime() - now.getTime() : Infinity;
|
|
471
|
-
const soonest = fiveIn <= sevenIn ? q.fiveHourResetAt : q.sevenDayResetAt;
|
|
472
|
-
if (!soonest) return 'refills —';
|
|
473
|
-
return `refills ${formatStatusTime(soonest, now, tz)} (in ${formatRelative(soonest, now)})`;
|
|
468
|
+
const reset = win === '5h' ? snap.quota.fiveHourResetAt : snap.quota.sevenDayResetAt;
|
|
469
|
+
if (!reset) return '—';
|
|
470
|
+
return `${formatStatusTime(reset, now, tz)} (in ${formatRelative(reset, now)})`;
|
|
474
471
|
}
|
|
475
472
|
|
|
476
473
|
export function renderAuthSnapshotFormat2(
|
|
@@ -495,8 +492,8 @@ export function renderAuthSnapshotFormat2(
|
|
|
495
492
|
|
|
496
493
|
if (ordered.length > 0) {
|
|
497
494
|
lines.push('');
|
|
498
|
-
lines.push('| State | Account | 5h | 7d |
|
|
499
|
-
lines.push('| --- | --- | --- | --- | --- |');
|
|
495
|
+
lines.push('| State | Account | 5h | 5h resets | 7d | 7d resets |');
|
|
496
|
+
lines.push('| --- | --- | --- | --- | --- | --- |');
|
|
500
497
|
for (const s of ordered) {
|
|
501
498
|
const emoji = HEALTH_EMOJI[classifyHealth(s, now)];
|
|
502
499
|
// Account cell: FULL email, never truncated; active gets a (active) suffix.
|
|
@@ -512,9 +509,10 @@ export function renderAuthSnapshotFormat2(
|
|
|
512
509
|
// tableCell wrap around the finished span.
|
|
513
510
|
const accountCell = `\`${codeSpanSafe(s.isActive ? `${label} (active)` : label)}\``;
|
|
514
511
|
const { five, seven } = pctCells(s, now);
|
|
515
|
-
const
|
|
512
|
+
const fiveReset = windowResetCell(s, now, tz, '5h');
|
|
513
|
+
const sevenReset = windowResetCell(s, now, tz, '7d');
|
|
516
514
|
lines.push(
|
|
517
|
-
`| ${emoji} | ${tableCell(accountCell)} | ${five} | ${seven} | ${tableCell(
|
|
515
|
+
`| ${emoji} | ${tableCell(accountCell)} | ${five} | ${tableCell(fiveReset)} | ${seven} | ${tableCell(sevenReset)} |`,
|
|
518
516
|
);
|
|
519
517
|
}
|
|
520
518
|
}
|
|
@@ -30,6 +30,7 @@ import { createIpcClient, type IpcClientHandle } from './ipc-client.js'
|
|
|
30
30
|
import { buildEffectiveToolSchemas, LINEAR_ENV } from './tool-filter.js'
|
|
31
31
|
import type { InboundMessage, PermissionEvent, StatusEvent } from '../gateway/ipc-protocol.js'
|
|
32
32
|
import { matchesAllowRule } from '../permission-rule.js'
|
|
33
|
+
import { createOutstandingPermissionLedger } from './permission-ledger.js'
|
|
33
34
|
|
|
34
35
|
installPluginLogger()
|
|
35
36
|
|
|
@@ -417,6 +418,24 @@ const TOOL_SCHEMAS = [
|
|
|
417
418
|
required: ['chat_id', 'key'],
|
|
418
419
|
},
|
|
419
420
|
},
|
|
421
|
+
{
|
|
422
|
+
name: 'mental_model_propose',
|
|
423
|
+
description:
|
|
424
|
+
"Propose a Hindsight MENTAL MODEL for the operator to approve (agent-proposes → human-approves, hindsight Phase 5). Use this when — over real work — 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 — appended to your `memory.mental_models[]` in switchroom.yaml via the operator-approved config-edit path — 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 — it is the leashed, human-gated way to add a DURABLE declared model to your config. After firing this tool, END YOUR TURN cleanly — 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).",
|
|
425
|
+
inputSchema: {
|
|
426
|
+
type: 'object',
|
|
427
|
+
properties: {
|
|
428
|
+
chat_id: { type: 'string', description: 'Chat to render the approval card in (use the chat_id of the user message that triggered the workflow).' },
|
|
429
|
+
name: { type: 'string', description: 'Stable model name — 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.' },
|
|
430
|
+
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.' },
|
|
431
|
+
reason: { type: 'string', description: 'REQUIRED in practice — 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.' },
|
|
432
|
+
refresh_after_consolidation: { type: 'boolean', description: 'Refresh this model after each consolidation. Defaults OFF — refresh adds bounded background model-spend + timeout risk (RFC Phase 5). Only set true when the model genuinely needs to track fast-moving state.' },
|
|
433
|
+
max_tokens: { type: 'number', description: 'Optional cap on the synthesized model\'s token size.' },
|
|
434
|
+
message_thread_id: { type: 'string', description: 'Forum topic thread ID. Auto-applied from the last inbound message if not specified.' },
|
|
435
|
+
},
|
|
436
|
+
required: ['chat_id', 'name', 'source_query'],
|
|
437
|
+
},
|
|
438
|
+
},
|
|
420
439
|
{
|
|
421
440
|
name: 'linear_agent_activity',
|
|
422
441
|
description:
|
|
@@ -582,6 +601,40 @@ mcp.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
|
582
601
|
// added by either is honoured by all.
|
|
583
602
|
const sessionAllowRules = new Set<string>()
|
|
584
603
|
|
|
604
|
+
// #2861: outstanding permission-request ledger. Every permission_request from
|
|
605
|
+
// claude is recorded here and re-sent on each IPC (re)connect, so a request
|
|
606
|
+
// that arrives while the gateway is down (or that the gateway lost across a
|
|
607
|
+
// restart) is redelivered instead of dropped. Deleted in onPermission when a
|
|
608
|
+
// verdict is delivered. Kill switch: SWITCHROOM_PERMISSION_REARM=0 reverts to
|
|
609
|
+
// the legacy drop-when-disconnected behavior.
|
|
610
|
+
const outstandingPermissions = createOutstandingPermissionLedger()
|
|
611
|
+
const permissionRearmEnabled = process.env.SWITCHROOM_PERMISSION_REARM !== '0'
|
|
612
|
+
|
|
613
|
+
/**
|
|
614
|
+
* Re-send every outstanding permission_request to the gateway. Called on each
|
|
615
|
+
* IPC (re)connect via the ipc-client onConnect hook. The gateway dedupes /
|
|
616
|
+
* re-arms them idempotently (gateway/permission-rearm.ts), so re-sending on
|
|
617
|
+
* every reconnect is safe. Never answers a card — re-transmits the question.
|
|
618
|
+
*/
|
|
619
|
+
function flushOutstandingPermissionRequests(): void {
|
|
620
|
+
if (!permissionRearmEnabled) return
|
|
621
|
+
if (!ipc || !ipc.isConnected()) return
|
|
622
|
+
const pending = outstandingPermissions.all()
|
|
623
|
+
if (pending.length === 0) return
|
|
624
|
+
process.stderr.write(
|
|
625
|
+
`telegram bridge: re-sending ${pending.length} outstanding permission request(s) on gateway (re)connect\n`,
|
|
626
|
+
)
|
|
627
|
+
for (const p of pending) {
|
|
628
|
+
ipc.sendPermissionRequest({
|
|
629
|
+
type: 'permission_request',
|
|
630
|
+
requestId: p.request_id,
|
|
631
|
+
toolName: p.tool_name,
|
|
632
|
+
description: p.description,
|
|
633
|
+
inputPreview: p.input_preview,
|
|
634
|
+
})
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
|
|
585
638
|
mcp.setNotificationHandler(
|
|
586
639
|
z.object({
|
|
587
640
|
method: z.literal('notifications/claude/channel/permission_request'),
|
|
@@ -615,8 +668,29 @@ mcp.setNotificationHandler(
|
|
|
615
668
|
return
|
|
616
669
|
}
|
|
617
670
|
}
|
|
671
|
+
// #2861: record the request in the outstanding ledger BEFORE the connect
|
|
672
|
+
// check so it survives a disconnected gateway and is re-sent on reconnect.
|
|
673
|
+
// It's deleted in onPermission when the verdict is delivered.
|
|
674
|
+
if (permissionRearmEnabled) {
|
|
675
|
+
outstandingPermissions.add({
|
|
676
|
+
request_id: params.request_id,
|
|
677
|
+
tool_name: params.tool_name,
|
|
678
|
+
description: params.description,
|
|
679
|
+
input_preview: params.input_preview,
|
|
680
|
+
})
|
|
681
|
+
}
|
|
618
682
|
if (!ipc || !ipc.isConnected()) {
|
|
619
|
-
|
|
683
|
+
// BUFFER, don't drop (#2861 R2). The request is in the ledger; the
|
|
684
|
+
// onConnect flush re-sends it once the gateway is reachable. Falls back
|
|
685
|
+
// to the legacy drop only when re-arm is killed.
|
|
686
|
+
if (permissionRearmEnabled) {
|
|
687
|
+
process.stderr.write(
|
|
688
|
+
`telegram bridge: permission_request buffered (gateway offline), will re-send on reconnect ` +
|
|
689
|
+
`request_id=${params.request_id}\n`,
|
|
690
|
+
)
|
|
691
|
+
} else {
|
|
692
|
+
process.stderr.write('telegram bridge: permission_request received but not connected to gateway\n')
|
|
693
|
+
}
|
|
620
694
|
return
|
|
621
695
|
}
|
|
622
696
|
ipc.sendPermissionRequest({
|
|
@@ -658,6 +732,9 @@ function onPermission(msg: PermissionEvent): void {
|
|
|
658
732
|
if (msg.rule) {
|
|
659
733
|
sessionAllowRules.add(msg.rule)
|
|
660
734
|
}
|
|
735
|
+
// #2861: verdict delivered → drop the request from the outstanding ledger so
|
|
736
|
+
// a later reconnect doesn't re-send an already-resolved approval.
|
|
737
|
+
outstandingPermissions.delete(msg.requestId)
|
|
661
738
|
mcp.notification({
|
|
662
739
|
method: 'notifications/claude/channel/permission',
|
|
663
740
|
params: {
|
|
@@ -836,6 +913,8 @@ async function main(): Promise<void> {
|
|
|
836
913
|
onInbound,
|
|
837
914
|
onPermission,
|
|
838
915
|
onStatus,
|
|
916
|
+
// #2861: re-send outstanding permission requests on every (re)connect.
|
|
917
|
+
onConnect: flushOutstandingPermissionRequests,
|
|
839
918
|
log: (msg) => process.stderr.write(`telegram bridge: ipc: ${msg}\n`),
|
|
840
919
|
// #2307 Tier-1: the cron-session bridge shares the agent's STATE_DIR
|
|
841
920
|
// (access.json / history / gateway.sock) but writes its liveness file to a
|
|
@@ -48,6 +48,14 @@ export interface IpcClientOptions {
|
|
|
48
48
|
onInbound: (msg: InboundMessage) => void;
|
|
49
49
|
onPermission: (msg: PermissionEvent) => void;
|
|
50
50
|
onStatus: (msg: StatusEvent) => void;
|
|
51
|
+
/**
|
|
52
|
+
* Called every time the socket (re)connects and has registered — i.e. on
|
|
53
|
+
* the initial connect AND on each background reconnect. The bridge uses
|
|
54
|
+
* this to flush its outstanding permission-request ledger so approvals
|
|
55
|
+
* that arrived while the gateway was down are re-sent (#2861). Best-effort:
|
|
56
|
+
* a throw here is logged, never propagated into the socket open path.
|
|
57
|
+
*/
|
|
58
|
+
onConnect?: () => void;
|
|
51
59
|
log?: (msg: string) => void;
|
|
52
60
|
reconnectDelayMs?: number;
|
|
53
61
|
maxReconnectDelayMs?: number;
|
|
@@ -93,6 +101,7 @@ export function createIpcClient(options: IpcClientOptions): Promise<IpcClientHan
|
|
|
93
101
|
onInbound,
|
|
94
102
|
onPermission,
|
|
95
103
|
onStatus,
|
|
104
|
+
onConnect,
|
|
96
105
|
log = () => {},
|
|
97
106
|
reconnectDelayMs = 2000,
|
|
98
107
|
maxReconnectDelayMs = 30000,
|
|
@@ -237,6 +246,16 @@ export function createIpcClient(options: IpcClientOptions): Promise<IpcClientHan
|
|
|
237
246
|
sendRegister();
|
|
238
247
|
startHeartbeat();
|
|
239
248
|
log(`connected to ${socketPath}`);
|
|
249
|
+
// Fire the (re)connect hook AFTER register + heartbeat so the
|
|
250
|
+
// bridge's ledger flush writes onto a live, registered socket.
|
|
251
|
+
// Best-effort: never let a callback throw abort the open path.
|
|
252
|
+
if (onConnect) {
|
|
253
|
+
try {
|
|
254
|
+
onConnect();
|
|
255
|
+
} catch (err) {
|
|
256
|
+
log(`onConnect hook threw: ${err}`);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
240
259
|
resolve();
|
|
241
260
|
},
|
|
242
261
|
data(sock, data) {
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bridge-side outstanding permission-request ledger (#2861 / umbrella #2859).
|
|
3
|
+
*
|
|
4
|
+
* Problem it closes (R2): the bridge's `permission_request` handler used to
|
|
5
|
+
* DROP the request when the gateway IPC was down
|
|
6
|
+
* (`if (!ipc || !ipc.isConnected()) { log; return }`). claude stays suspended
|
|
7
|
+
* inside the MCP permission call, no card is ever posted, the turn wedges
|
|
8
|
+
* forever. There was no record of the ask to re-send once the gateway came
|
|
9
|
+
* back.
|
|
10
|
+
*
|
|
11
|
+
* Fix: every `permission_request` notification from claude is recorded here,
|
|
12
|
+
* keyed by request_id. When the IPC (re)connects, the bridge re-sends every
|
|
13
|
+
* outstanding request (the gateway dedupes / re-arms them idempotently — see
|
|
14
|
+
* `gateway/permission-rearm.ts`). An entry is deleted the moment a verdict is
|
|
15
|
+
* delivered back to claude (`onPermission`), so a resolved request is never
|
|
16
|
+
* re-sent.
|
|
17
|
+
*
|
|
18
|
+
* This never answers a card — it only re-transmits the QUESTION
|
|
19
|
+
* (no-self-escalation invariant). Pure in-memory bookkeeping: no model
|
|
20
|
+
* callsite, no I/O.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
/** The claude-side `permission_request` params the bridge forwards. */
|
|
24
|
+
export interface OutstandingPermissionParams {
|
|
25
|
+
request_id: string
|
|
26
|
+
tool_name: string
|
|
27
|
+
description: string
|
|
28
|
+
input_preview: string
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface OutstandingPermissionLedger {
|
|
32
|
+
/** Record a request. Idempotent on request_id (latest params win). */
|
|
33
|
+
add(params: OutstandingPermissionParams): void
|
|
34
|
+
/** Remove a request once its verdict has been delivered. */
|
|
35
|
+
delete(requestId: string): void
|
|
36
|
+
has(requestId: string): boolean
|
|
37
|
+
/** Snapshot of all outstanding requests (insertion order). */
|
|
38
|
+
all(): OutstandingPermissionParams[]
|
|
39
|
+
readonly size: number
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function createOutstandingPermissionLedger(): OutstandingPermissionLedger {
|
|
43
|
+
const map = new Map<string, OutstandingPermissionParams>()
|
|
44
|
+
return {
|
|
45
|
+
add(params) {
|
|
46
|
+
map.set(params.request_id, params)
|
|
47
|
+
},
|
|
48
|
+
delete(requestId) {
|
|
49
|
+
map.delete(requestId)
|
|
50
|
+
},
|
|
51
|
+
has(requestId) {
|
|
52
|
+
return map.has(requestId)
|
|
53
|
+
},
|
|
54
|
+
all() {
|
|
55
|
+
return [...map.values()]
|
|
56
|
+
},
|
|
57
|
+
get size() {
|
|
58
|
+
return map.size
|
|
59
|
+
},
|
|
60
|
+
}
|
|
61
|
+
}
|