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
|
@@ -23876,6 +23876,7 @@ function createIpcClient(options) {
|
|
|
23876
23876
|
onInbound,
|
|
23877
23877
|
onPermission,
|
|
23878
23878
|
onStatus,
|
|
23879
|
+
onConnect,
|
|
23879
23880
|
log = () => {},
|
|
23880
23881
|
reconnectDelayMs = 2000,
|
|
23881
23882
|
maxReconnectDelayMs = 30000,
|
|
@@ -24011,6 +24012,13 @@ function createIpcClient(options) {
|
|
|
24011
24012
|
sendRegister();
|
|
24012
24013
|
startHeartbeat();
|
|
24013
24014
|
log(`connected to ${socketPath}`);
|
|
24015
|
+
if (onConnect) {
|
|
24016
|
+
try {
|
|
24017
|
+
onConnect();
|
|
24018
|
+
} catch (err) {
|
|
24019
|
+
log(`onConnect hook threw: ${err}`);
|
|
24020
|
+
}
|
|
24021
|
+
}
|
|
24014
24022
|
resolve();
|
|
24015
24023
|
},
|
|
24016
24024
|
data(sock, data) {
|
|
@@ -24232,10 +24240,52 @@ var init_permission_rule = __esm(() => {
|
|
|
24232
24240
|
]);
|
|
24233
24241
|
});
|
|
24234
24242
|
|
|
24243
|
+
// bridge/permission-ledger.ts
|
|
24244
|
+
function createOutstandingPermissionLedger() {
|
|
24245
|
+
const map2 = new Map;
|
|
24246
|
+
return {
|
|
24247
|
+
add(params) {
|
|
24248
|
+
map2.set(params.request_id, params);
|
|
24249
|
+
},
|
|
24250
|
+
delete(requestId) {
|
|
24251
|
+
map2.delete(requestId);
|
|
24252
|
+
},
|
|
24253
|
+
has(requestId) {
|
|
24254
|
+
return map2.has(requestId);
|
|
24255
|
+
},
|
|
24256
|
+
all() {
|
|
24257
|
+
return [...map2.values()];
|
|
24258
|
+
},
|
|
24259
|
+
get size() {
|
|
24260
|
+
return map2.size;
|
|
24261
|
+
}
|
|
24262
|
+
};
|
|
24263
|
+
}
|
|
24264
|
+
|
|
24235
24265
|
// bridge/bridge.ts
|
|
24236
24266
|
var exports_bridge = {};
|
|
24237
24267
|
import { dirname as dirname3, join as join5 } from "path";
|
|
24238
24268
|
import { homedir as homedir4 } from "os";
|
|
24269
|
+
function flushOutstandingPermissionRequests() {
|
|
24270
|
+
if (!permissionRearmEnabled)
|
|
24271
|
+
return;
|
|
24272
|
+
if (!ipc || !ipc.isConnected())
|
|
24273
|
+
return;
|
|
24274
|
+
const pending = outstandingPermissions.all();
|
|
24275
|
+
if (pending.length === 0)
|
|
24276
|
+
return;
|
|
24277
|
+
process.stderr.write(`telegram bridge: re-sending ${pending.length} outstanding permission request(s) on gateway (re)connect
|
|
24278
|
+
`);
|
|
24279
|
+
for (const p of pending) {
|
|
24280
|
+
ipc.sendPermissionRequest({
|
|
24281
|
+
type: "permission_request",
|
|
24282
|
+
requestId: p.request_id,
|
|
24283
|
+
toolName: p.tool_name,
|
|
24284
|
+
description: p.description,
|
|
24285
|
+
inputPreview: p.input_preview
|
|
24286
|
+
});
|
|
24287
|
+
}
|
|
24288
|
+
}
|
|
24239
24289
|
function onInbound(msg) {
|
|
24240
24290
|
mcp.notification({
|
|
24241
24291
|
method: "notifications/claude/channel",
|
|
@@ -24252,6 +24302,7 @@ function onPermission(msg) {
|
|
|
24252
24302
|
if (msg.rule) {
|
|
24253
24303
|
sessionAllowRules.add(msg.rule);
|
|
24254
24304
|
}
|
|
24305
|
+
outstandingPermissions.delete(msg.requestId);
|
|
24255
24306
|
mcp.notification({
|
|
24256
24307
|
method: "notifications/claude/channel/permission",
|
|
24257
24308
|
params: {
|
|
@@ -24305,6 +24356,7 @@ async function main() {
|
|
|
24305
24356
|
onInbound,
|
|
24306
24357
|
onPermission,
|
|
24307
24358
|
onStatus,
|
|
24359
|
+
onConnect: flushOutstandingPermissionRequests,
|
|
24308
24360
|
log: (msg) => process.stderr.write(`telegram bridge: ipc: ${msg}
|
|
24309
24361
|
`),
|
|
24310
24362
|
livenessFilePath: process.env.SWITCHROOM_BRIDGE_ALIVE_PATH ?? join5(STATE_DIR, ".bridge-alive")
|
|
@@ -24318,7 +24370,7 @@ async function main() {
|
|
|
24318
24370
|
}
|
|
24319
24371
|
await mcp.connect(new StdioServerTransport);
|
|
24320
24372
|
}
|
|
24321
|
-
var STATE_DIR, SOCKET_PATH, TOPIC_ID, AGENT_NAME, mcp, TOOL_SCHEMAS, EFFECTIVE_TOOL_SCHEMAS, sessionAllowRules, ipc = null, sessionTailEnabled, sessionTailHandle = null, ptyTailEnabled, ptyTailHandle = null;
|
|
24373
|
+
var STATE_DIR, SOCKET_PATH, TOPIC_ID, AGENT_NAME, mcp, TOOL_SCHEMAS, EFFECTIVE_TOOL_SCHEMAS, sessionAllowRules, outstandingPermissions, permissionRearmEnabled, ipc = null, sessionTailEnabled, sessionTailHandle = null, ptyTailEnabled, ptyTailHandle = null;
|
|
24322
24374
|
var init_bridge = __esm(async () => {
|
|
24323
24375
|
init_server2();
|
|
24324
24376
|
init_stdio2();
|
|
@@ -24681,6 +24733,23 @@ var init_bridge = __esm(async () => {
|
|
|
24681
24733
|
required: ["chat_id", "key"]
|
|
24682
24734
|
}
|
|
24683
24735
|
},
|
|
24736
|
+
{
|
|
24737
|
+
name: "mental_model_propose",
|
|
24738
|
+
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).",
|
|
24739
|
+
inputSchema: {
|
|
24740
|
+
type: "object",
|
|
24741
|
+
properties: {
|
|
24742
|
+
chat_id: { type: "string", description: "Chat to render the approval card in (use the chat_id of the user message that triggered the workflow)." },
|
|
24743
|
+
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." },
|
|
24744
|
+
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.` },
|
|
24745
|
+
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.' },
|
|
24746
|
+
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." },
|
|
24747
|
+
max_tokens: { type: "number", description: "Optional cap on the synthesized model's token size." },
|
|
24748
|
+
message_thread_id: { type: "string", description: "Forum topic thread ID. Auto-applied from the last inbound message if not specified." }
|
|
24749
|
+
},
|
|
24750
|
+
required: ["chat_id", "name", "source_query"]
|
|
24751
|
+
}
|
|
24752
|
+
},
|
|
24684
24753
|
{
|
|
24685
24754
|
name: "linear_agent_activity",
|
|
24686
24755
|
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`.',
|
|
@@ -24804,6 +24873,8 @@ var init_bridge = __esm(async () => {
|
|
|
24804
24873
|
}
|
|
24805
24874
|
});
|
|
24806
24875
|
sessionAllowRules = new Set;
|
|
24876
|
+
outstandingPermissions = createOutstandingPermissionLedger();
|
|
24877
|
+
permissionRearmEnabled = process.env.SWITCHROOM_PERMISSION_REARM !== "0";
|
|
24807
24878
|
mcp.setNotificationHandler(exports_external.object({
|
|
24808
24879
|
method: exports_external.literal("notifications/claude/channel/permission_request"),
|
|
24809
24880
|
params: exports_external.object({
|
|
@@ -24825,9 +24896,22 @@ var init_bridge = __esm(async () => {
|
|
|
24825
24896
|
return;
|
|
24826
24897
|
}
|
|
24827
24898
|
}
|
|
24899
|
+
if (permissionRearmEnabled) {
|
|
24900
|
+
outstandingPermissions.add({
|
|
24901
|
+
request_id: params.request_id,
|
|
24902
|
+
tool_name: params.tool_name,
|
|
24903
|
+
description: params.description,
|
|
24904
|
+
input_preview: params.input_preview
|
|
24905
|
+
});
|
|
24906
|
+
}
|
|
24828
24907
|
if (!ipc || !ipc.isConnected()) {
|
|
24829
|
-
|
|
24908
|
+
if (permissionRearmEnabled) {
|
|
24909
|
+
process.stderr.write(`telegram bridge: permission_request buffered (gateway offline), will re-send on reconnect ` + `request_id=${params.request_id}
|
|
24910
|
+
`);
|
|
24911
|
+
} else {
|
|
24912
|
+
process.stderr.write(`telegram bridge: permission_request received but not connected to gateway
|
|
24830
24913
|
`);
|
|
24914
|
+
}
|
|
24831
24915
|
return;
|
|
24832
24916
|
}
|
|
24833
24917
|
ipc.sendPermissionRequest({
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deterministic silent-turn card climb — the framework owns "alive" and "for how
|
|
3
|
+
* long"; the model owns "what".
|
|
4
|
+
*
|
|
5
|
+
* Implements Phase 1 (the keystone) of `reference/rfcs/deterministic-turn-liveness.md`.
|
|
6
|
+
* Serves `reference/jobs/know-what-my-agent-is-doing.md` (outcome: at any moment
|
|
7
|
+
* during a turn the user can see the agent is alive and how long it has been
|
|
8
|
+
* working, without asking).
|
|
9
|
+
*
|
|
10
|
+
* ## The bug this closes
|
|
11
|
+
*
|
|
12
|
+
* On the 0-label path of `feedHeartbeatTick` (a turn that has surfaced NO tool
|
|
13
|
+
* label — a long single `Bash`, or only suppressed-by-design tools), the tick
|
|
14
|
+
* delegated to `openLivenessFeedIfDue`, whose WHEN-gate `shouldEarlyOpenLiveness`
|
|
15
|
+
* returns `false` the moment a card is already open (`activityMessageId != null`).
|
|
16
|
+
* Result: once the "Working…" card opened, the 0-label branch NEVER edited it
|
|
17
|
+
* again during a silent tool — the card froze on its opening elapsed while the
|
|
18
|
+
* agent was busy for tens of seconds. The only remaining signal was the ambient
|
|
19
|
+
* "stuck" reaction flip, which reads as *done*, not *working*.
|
|
20
|
+
*
|
|
21
|
+
* ## The fix
|
|
22
|
+
*
|
|
23
|
+
* Give the 0-label branch the SAME climbing-elapsed EDIT the labelled branch
|
|
24
|
+
* already performs: when a card is open and `mirrorLines` is empty, re-render the
|
|
25
|
+
* minimal "Working…" card with a fresh wall-clock elapsed on every heartbeat tick.
|
|
26
|
+
* The render is derived PURELY from `ageMs` (`now - turn.startedAt`), so a tool
|
|
27
|
+
* call blocked mid-flight — emitting nothing — cannot starve it. Worst-case gap
|
|
28
|
+
* between visible edits is one to two `FEED_HEARTBEAT_MIN_STALE_MS` windows
|
|
29
|
+
* (~6-12s), versus the observed ~50s freeze.
|
|
30
|
+
*
|
|
31
|
+
* This is a card EDIT, not a text send: it edits the card the chat already owns,
|
|
32
|
+
* and Telegram card edits do not push-notify — squarely the Ambient layer of
|
|
33
|
+
* `reference/rfcs/conversational-pacing.md`, crossing no invariant (no parallel
|
|
34
|
+
* surface, no mid-turn device buzz). When the model DOES narrate, its words land
|
|
35
|
+
* on the same card chronologically (`mirrorLines` becomes non-empty and the
|
|
36
|
+
* labelled-feed heartbeat takes over) — the climb fills only the gaps the model
|
|
37
|
+
* leaves and never overwrites narration.
|
|
38
|
+
*
|
|
39
|
+
* Kept as a small pure function so the guarantee can be proven at the transport
|
|
40
|
+
* boundary (Phase 4b regression test) rather than by grepping the gateway source.
|
|
41
|
+
*/
|
|
42
|
+
import {
|
|
43
|
+
renderActivityFeedWithNested,
|
|
44
|
+
formatStepSuffix,
|
|
45
|
+
type SessionActivityHeader,
|
|
46
|
+
} from './tool-activity-summary.js'
|
|
47
|
+
|
|
48
|
+
/** The minimal placeholder line the silent-turn card carries when no tool label
|
|
49
|
+
* has surfaced. Matches `openLivenessFeedIfDue`'s open render so the climb edit
|
|
50
|
+
* is a clean continuation of the placeholder the OPEN path produced. */
|
|
51
|
+
export const SILENT_TURN_PLACEHOLDER = 'Working…'
|
|
52
|
+
|
|
53
|
+
export interface SilentTurnClimbInput {
|
|
54
|
+
/** `turn.mirrorLines.length`. 0 ⇒ no tool label has surfaced this turn (pure
|
|
55
|
+
* thinking / only suppressed tools) — the case this climb owns. >0 ⇒ a real
|
|
56
|
+
* label drives the labelled-feed heartbeat, which owns the climb instead. */
|
|
57
|
+
mirrorLineCount: number
|
|
58
|
+
/** `turn.activityMessageId`. `null` ⇒ no card is open yet, so the OPEN path
|
|
59
|
+
* (`openLivenessFeedIfDue`) owns this tick; non-null ⇒ a card is open and this
|
|
60
|
+
* branch must keep it climbing (the exact freeze this fix closes). */
|
|
61
|
+
activityMessageId: number | null
|
|
62
|
+
/** `turn.labeledToolCount` — surfaced in the card header's tool count. */
|
|
63
|
+
labeledToolCount: number
|
|
64
|
+
/** `now - turn.startedAt`: wall-clock elapsed since turn start. The single
|
|
65
|
+
* signal the model can never supply while blocked inside a tool. */
|
|
66
|
+
ageMs: number
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* The deterministic 0-label climb render. Returns the card body HTML to EDIT the
|
|
71
|
+
* already-open "Working…" card with a climbing wall-clock elapsed, or `null` when
|
|
72
|
+
* this branch must NOT edit:
|
|
73
|
+
* - `mirrorLineCount > 0` — a tool label surfaced; the labelled-feed heartbeat
|
|
74
|
+
* owns the climb (its edit cleanly replaces this placeholder).
|
|
75
|
+
* - `activityMessageId == null` — no card is open yet; the OPEN path
|
|
76
|
+
* (`openLivenessFeedIfDue`) owns this tick.
|
|
77
|
+
*
|
|
78
|
+
* The header's `elapsedMs` carries the climbing clock, so each tick's edit shows
|
|
79
|
+
* a higher "Working · Ns" than the last. Model-independent by construction: it
|
|
80
|
+
* reads only wall-clock `ageMs`.
|
|
81
|
+
*/
|
|
82
|
+
export function silentTurnClimbRender(input: SilentTurnClimbInput): string | null {
|
|
83
|
+
if (input.mirrorLineCount > 0) return null
|
|
84
|
+
if (input.activityMessageId == null) return null
|
|
85
|
+
const header: SessionActivityHeader = {
|
|
86
|
+
label: 'Agent',
|
|
87
|
+
elapsedMs: input.ageMs,
|
|
88
|
+
toolCount: input.labeledToolCount,
|
|
89
|
+
state: 'running',
|
|
90
|
+
}
|
|
91
|
+
// Single "step" whose start is the turn start, so `ageMs` IS the step's own
|
|
92
|
+
// elapsed. formatStepSuffix keeps the `→` line timer-free until STEP_TIMER_MIN_MS;
|
|
93
|
+
// the header total still climbs every tick — that is the visible clock.
|
|
94
|
+
return renderActivityFeedWithNested(
|
|
95
|
+
[SILENT_TURN_PLACEHOLDER],
|
|
96
|
+
[],
|
|
97
|
+
false,
|
|
98
|
+
formatStepSuffix(input.ageMs),
|
|
99
|
+
undefined,
|
|
100
|
+
header,
|
|
101
|
+
)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* The per-turn view `runSilentTurnHeartbeatTick` reads — a structural subset of
|
|
106
|
+
* the gateway's `CurrentTurn`, kept transport-free so this module (and its test)
|
|
107
|
+
* never imports the gateway.
|
|
108
|
+
*/
|
|
109
|
+
export interface SilentTurnTickView {
|
|
110
|
+
/** `turn.mirrorLines.length` at tick time. */
|
|
111
|
+
mirrorLineCount: number
|
|
112
|
+
/** `turn.activityMessageId` at tick time (null ⇒ no card open yet). */
|
|
113
|
+
activityMessageId: number | null
|
|
114
|
+
/** `turn.labeledToolCount` — surfaced in the card header. */
|
|
115
|
+
labeledToolCount: number
|
|
116
|
+
/** `now - turn.startedAt` at tick time. */
|
|
117
|
+
ageMs: number
|
|
118
|
+
/** The labelled branch's stale floor (`FEED_HEARTBEAT_MIN_STALE_MS`). The
|
|
119
|
+
* climb applies the SAME guard for symmetry: a turn younger than one stale
|
|
120
|
+
* window is fresh (its card just opened ~1.2s in), so no climb edit yet.
|
|
121
|
+
* Without this the climb relied on the tick interval == the stale window
|
|
122
|
+
* (both 6s today) — an implicit coupling this guard makes explicit. */
|
|
123
|
+
minStaleMs: number
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* The gateway seams the tick drives, injected as thunks so THIS function — the
|
|
128
|
+
* shipped 0-label branch of `feedHeartbeatTick` — is directly executable in a
|
|
129
|
+
* test with spies at each seam. The gateway wires the real implementations
|
|
130
|
+
* (`openLivenessFeedIfDue`, `turn.activityPendingRender = …`, `cardDrainGate`,
|
|
131
|
+
* `ea.mayDrain`, `ea.openOrEditCard('liveness', …)`,
|
|
132
|
+
* `drainActivitySummary(turn, 'liveness')`); the wiring is pinned structurally
|
|
133
|
+
* in tests/feed-heartbeat-liveness-open.test.ts.
|
|
134
|
+
*/
|
|
135
|
+
export interface SilentTurnTickDeps {
|
|
136
|
+
/** The one shared OPEN path (`openLivenessFeedIfDue(turn)`). Owns the open;
|
|
137
|
+
* the climb never opens (reply-is-last gating lives on the open path). */
|
|
138
|
+
openLivenessFeedIfDue: () => void
|
|
139
|
+
/** Stage the climb render (`turn.activityPendingRender = rendered`). */
|
|
140
|
+
setPendingRender: (rendered: string) => void
|
|
141
|
+
/** The centralized chatLock-serialized card-drain gate
|
|
142
|
+
* (`cardDrainGate(turn, ea, run)`). */
|
|
143
|
+
cardDrainGate: (run: () => void) => void
|
|
144
|
+
/** The single-flight pure read (`ea.mayDrain(turn)`). */
|
|
145
|
+
mayDrain: () => boolean
|
|
146
|
+
/** The emission-authority façade routing (`ea.openOrEditCard('liveness', apply)`).
|
|
147
|
+
* With a card open this only ever EDITs — Telegram edits don't push-notify. */
|
|
148
|
+
openOrEditCard: (apply: () => void) => void
|
|
149
|
+
/** The drain kick-off (`turn.activityInFlight = drainActivitySummary(turn,
|
|
150
|
+
* 'liveness')`) — the transport writer (sendMessage / editMessageText via
|
|
151
|
+
* robustApiCall) lives behind this seam. */
|
|
152
|
+
drain: () => void
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* The REAL 0-label branch of `feedHeartbeatTick` (deterministic-turn-liveness.md
|
|
157
|
+
* Phase 1) — extracted here so the shipped decision logic is directly under
|
|
158
|
+
* outcome-based test (`tests/silent-turn-climb-transport.test.ts`); the gateway
|
|
159
|
+
* IIFE cannot be imported in-process, so the tick body lives in this module and
|
|
160
|
+
* the gateway calls it with its real deps.
|
|
161
|
+
*
|
|
162
|
+
* Returns true when the tick was handled by the 0-label path (the caller must
|
|
163
|
+
* stop — the labelled-feed heartbeat does not run), false when a tool label has
|
|
164
|
+
* surfaced (`mirrorLineCount > 0`) and the labelled branch owns the tick.
|
|
165
|
+
*
|
|
166
|
+
* Behaviour:
|
|
167
|
+
* - no card open yet → delegate to the one shared OPEN path (never opens here);
|
|
168
|
+
* - card open + turn younger than `minStaleMs` → fresh, no edit yet (the same
|
|
169
|
+
* stale-guard the labelled branch applies to its step elapsed);
|
|
170
|
+
* - card open + stale → stage the climbing "Working · Ns" render and EDIT it
|
|
171
|
+
* through the same cardDrainGate → mayDrain → openOrEditCard → drain gate
|
|
172
|
+
* sequence the labelled branch uses. Deterministic + framework-owned: reads
|
|
173
|
+
* only wall-clock `ageMs`, so a blocked tool call cannot starve it.
|
|
174
|
+
*/
|
|
175
|
+
export function runSilentTurnHeartbeatTick(
|
|
176
|
+
view: SilentTurnTickView,
|
|
177
|
+
deps: SilentTurnTickDeps,
|
|
178
|
+
): boolean {
|
|
179
|
+
if (view.mirrorLineCount !== 0) return false
|
|
180
|
+
if (view.activityMessageId == null) {
|
|
181
|
+
deps.openLivenessFeedIfDue()
|
|
182
|
+
return true
|
|
183
|
+
}
|
|
184
|
+
// Stale-guard (symmetry with the labelled branch's `elapsed <
|
|
185
|
+
// FEED_HEARTBEAT_MIN_STALE_MS` check): the 0-label "step" starts at turn
|
|
186
|
+
// start, so `ageMs` IS its elapsed. A younger turn's card just opened.
|
|
187
|
+
if (view.ageMs < view.minStaleMs) return true
|
|
188
|
+
const rendered = silentTurnClimbRender({
|
|
189
|
+
mirrorLineCount: view.mirrorLineCount,
|
|
190
|
+
activityMessageId: view.activityMessageId,
|
|
191
|
+
labeledToolCount: view.labeledToolCount,
|
|
192
|
+
ageMs: view.ageMs,
|
|
193
|
+
})
|
|
194
|
+
if (rendered == null) return true
|
|
195
|
+
deps.setPendingRender(rendered)
|
|
196
|
+
deps.cardDrainGate(() => {
|
|
197
|
+
if (deps.mayDrain()) {
|
|
198
|
+
// Maintains an already-open card (guarded above on activityMessageId !=
|
|
199
|
+
// null) → only ever EDITs. 'liveness' producer is correct.
|
|
200
|
+
deps.openOrEditCard(() => {
|
|
201
|
+
deps.drain()
|
|
202
|
+
})
|
|
203
|
+
}
|
|
204
|
+
})
|
|
205
|
+
return true
|
|
206
|
+
}
|