switchroom 0.20.11 → 0.20.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agent-scheduler/index.js +4 -2
- package/dist/auth-broker/index.js +27 -24
- package/dist/cli/notion-write-pretool.mjs +4 -2
- package/dist/cli/switchroom.js +1608 -1068
- package/dist/host-control/main.js +28 -25
- package/dist/vault/approvals/kernel-server.js +27 -24
- package/dist/vault/broker/server.js +27 -24
- package/examples/personal-google-workspace-mcp/compose.yaml +1 -1
- package/package.json +1 -1
- package/skills/switchroom-architecture/telegram.md +0 -1
- package/skills/switchroom-cli/SKILL.md +0 -1
- package/telegram-plugin/README.md +2 -11
- package/telegram-plugin/bridge/bridge.ts +0 -12
- package/telegram-plugin/chat-lock.ts +1 -1
- package/telegram-plugin/dist/bridge/bridge.js +0 -12
- package/telegram-plugin/dist/gateway/gateway.js +54 -63
- package/telegram-plugin/dist/server.js +0 -12
- package/telegram-plugin/gateway/gateway.ts +24 -59
- package/telegram-plugin/gateway/liveness-wiring.ts +6 -1
- package/telegram-plugin/gateway/stale-pin-sweep.ts +4 -3
- package/telegram-plugin/gateway/status-pin-store.ts +10 -9
- package/telegram-plugin/gateway/stream-render.ts +4 -4
- package/telegram-plugin/gateway/turn-record-status.ts +32 -1
- package/telegram-plugin/hooks/hooks.json +13 -12
- package/telegram-plugin/hooks/narration-classify.mjs +1 -2
- package/telegram-plugin/hooks/silent-end-scan.mjs +1 -1
- package/telegram-plugin/status-pin.ts +2 -5
- package/telegram-plugin/tests/backstop-exactly-once.test.ts +8 -2
- package/telegram-plugin/tests/framework-fallback-duration-guard.test.ts +125 -0
- package/telegram-plugin/tests/pin-message-tool-retired.test.ts +64 -0
- package/telegram-plugin/tests/status-pin-boot-recovery.test.ts +38 -0
- package/telegram-plugin/tests/worker-activity-feed.test.ts +40 -1
- package/telegram-plugin/worker-activity-feed.ts +1 -1
- package/vendor/hindsight-memory/scripts/recall.py +140 -0
- package/vendor/hindsight-memory/scripts/tests/test_recall_latency_instrumentation.py +277 -0
|
@@ -22036,7 +22036,7 @@ var init_schema = __esm(() => {
|
|
|
22036
22036
|
reflect: HindsightPerOpLlmSchema.optional().describe("Per-op override for the `reflect` LLM op (synthesis / mental-model " + "refresh). Emits `HINDSIGHT_API_REFLECT_LLM_*`. Absent \u2192 uses global."),
|
|
22037
22037
|
consolidation: HindsightPerOpLlmSchema.optional().describe("Per-op override for the `consolidation` LLM op (background memory " + "merge). Emits `HINDSIGHT_API_CONSOLIDATION_LLM_*`. Absent \u2192 global.")
|
|
22038
22038
|
}).optional().describe("LLM knob for the hindsight container. The flat `provider`/`model` set " + "the global default (backward-compatible); optional `retain`/`reflect`/" + "`consolidation` blocks override individual ops. All fields optional; " + "unset fields fall back to the hard-coded defaults."),
|
|
22039
|
-
env: exports_external.record(exports_external.union([exports_external.string(), exports_external.number(), exports_external.boolean()])).optional().describe("Operator overrides for switchroom's capability-gated Hindsight " + "performance defaults. Only the keys switchroom actually manages are " + "honoured (`HINDSIGHT_PERF_ENV_KEYS` in " + "src/setup/hindsight-perf-defaults.ts: RERANKER_LOCAL_FP16, " + "RERANKER_LOCAL_BATCH_SIZE, LLM_MAX_CONCURRENT, " + "RETAIN/CONSOLIDATION_LLM_MAX_CONCURRENT, LLM_STRICT_SCHEMA, " + "LLM_MAX_RETRIES, CONSOLIDATION_LLM_PARALLELISM, " + "MAX_OBSERVATIONS_PER_SCOPE, " + "RECALL_MAX_CANDIDATES_PER_SOURCE, LINK_EXPANSION_PER_ENTITY_LIMIT, " + "LINK_EXPANSION_TIMEOUT, LLM_REASONING_EFFORT, " + "RERANKER_LOCAL_BUCKET_BATCHING, RERANKER_MAX_CANDIDATES, " + "RERANKER_LOCAL_MAX_CONCURRENT, RECALL_MAX_CONCURRENT, " + "REFLECT_WALL_TIMEOUT, WORKER_CONSOLIDATION_RESERVED_SLOTS, " + "WORKER_CONSOLIDATION_SLOT_LIMIT, " + "CONSOLIDATION_MAX_MEMORIES_PER_ROUND, GRAPH_SEED_MIN_SIMILARITY, " + "LLM_SUPPORTS_MAX_ITEMS, RECENCY_DECAY_FUNCTION, " + "RECENCY_DECAY_HALFLIFE_DAYS \u2014 switchroom defaults recall's recency " + "curve to `exponential` with a 30-day half-life so a fact retained " + "today outranks a stale one, instead of upstream's near-flat " + "linear/365-day window), the override-only keys " + "switchroom manages but ships NO default for " + "(`HINDSIGHT_PERF_OVERRIDE_ONLY_KEYS`: " + "HINDSIGHT_API_WORKER_CONSOLIDATION_BANK_PRIORITY \u2014 a per-deployment " + "`bank-pattern:priority,...` map; unset means upstream's flat " + "created_at FIFO across banks; and " + "HINDSIGHT_CE_DECISIVE_RELATIVE_GAP \u2014 the rollback knob for " + "switchroom's CE-saturation damping patch, a float; >= ~0.65 backs the " + "damping out entirely, unset means the patch's own derived gap; and " + "HINDSIGHT_API_RECENCY_DECAY_LINEAR_WINDOW_DAYS \u2014 only read when the " + "decay function is `linear`, so switchroom ships no default for it but " + "still honours an operator who flips the function back; and " + "HINDSIGHT_API_WORKER_MAX_SLOTS \u2014 the worker poller's TOTAL in-flight " + "task budget, the pool WORKER_CONSOLIDATION_RESERVED_SLOTS reserves out of; " + "unset means upstream's own default; and " + "HINDSIGHT_API_WORKER_RETAIN_RESERVED_SLOTS \u2014 the reserved slot FLOOR for the " + "retain (memory write) lane, carved from that same total; unset means " + "upstream's own 0, i.e. no floor and retain competes for the shared " + "pool; and " + "the pre-0.8.6 name HINDSIGHT_API_WORKER_<TYPE>_MAX_SLOTS is still " + "accepted for every operation type and normalised to " + "..._RESERVED_SLOTS on the way in, because setting both names for " + "one type is a hard boot failure in the engine and switchroom " + "therefore emits only the canonical name; and " + "HINDSIGHT_API_SEMANTIC_MIN_SIMILARITY \u2014 the cosine-similarity floor a " + "candidate must reach to be returned by the semantic retrieval arm at " + "all (0..1); unset means upstream's own 0.3, and where it should sit " + "depends on the bank's embedding model and phrasing diversity, so " + "switchroom ships no opinion; and " + "HINDSIGHT_MCP_RECALL_BUDGET_MODE \u2014 the rollback knob for switchroom's " + "mcp-recall-token-budget image patch; `legacy` restores upstream's " + "exact pre-patch recall returns, anything else or unset is the " + "honest-envelope mode; and " + "HINDSIGHT_API_LLM_TEMPERATURE_REFLECT \u2014 upstream's own per-op reflect " + "temperature knob, made live by switchroom's reflect-temperature image " + "patch; a float, or `none` to omit the kwarg (provider default, " + "upstream's accidental pre-patch behaviour); unset means the image's " + "baked default 0.1; and " + "HINDSIGHT_API_CONSOLIDATION_RECALL_MAX_CONCURRENT \u2014 the background " + "half of switchroom's recall-admission split (#3660): of the " + "RECALL_MAX_CONCURRENT admission slots, at most this many may be held " + "by background consolidation recalls at once, so foreground per-turn " + "recall always keeps the remainder; must be >= 1 and strictly less " + "than RECALL_MAX_CONCURRENT or the engine refuses to boot; unset " + "means the image's derived default min(2, RECALL_MAX_CONCURRENT - 1); " + "1 biases hard toward the interactive lane while a consolidation " + "backlog drains; and " + "HINDSIGHT_MM_REFRESH_MIN_INTERVAL_S \u2014 the rollback/tuning knob for " + "switchroom's MM-refresh-debounce image patch: the minimum seconds " + "between consolidation-triggered refreshes of one mental model; unset " + "means the image's baked 3600, 0 restores upstream's " + "refresh-every-round behaviour; explicit and cron-scheduled refreshes " + "are never debounced; and " + "HINDSIGHT_API_TEMPORAL_LANGUAGES \u2014 the language set dateparser is " + "restricted to during temporal query analysis, made live by switchroom's " + "temporal-language image patch (which ended a 200+-locale auto-detection " + "pass that blocked the shared asyncio loop on every recall); " + "comma-separated, unset means the image's baked `en`, set e.g. `en,es` to " + "restore i18n parsing; and " + "HINDSIGHT_API_TEMPORAL_MAX_QUERY_CHARS \u2014 the max chars of query text " + "dateparser.search_dates() is handed during temporal analysis (0 = " + "unlimited), made live by switchroom's temporal-offload image patch " + "(which moved the synchronous extraction off the shared asyncio loop onto " + "a single-worker thread and bounds its input, ending multi-second loop " + "stalls on multi-KB consolidation queries); unset means the image's baked " + "2000, set 0 to restore an unbounded full scan), plus the " + "embedded-PostgreSQL (pg0) sizing keys switchroom manages in " + "src/setup/hindsight-pg-defaults.ts (`HINDSIGHT_PG_ENV_KEYS`: " + "SWITCHROOM_HINDSIGHT_PG_EFFECTIVE_CACHE_SIZE, " + "SWITCHROOM_HINDSIGHT_PG_SHARED_BUFFERS \u2014 a postgres size string such " + "as `4GB`, or the sentinel `off` to leave pg0's own default for that " + "one knob). A value set here " + "REPLACES switchroom's default and is emitted even when the gating " + "capability is absent, so an operator can always force a knob. Other " + "`HINDSIGHT_API_*` keys are deliberately IGNORED \u2014 a blanket " + "passthrough would collide with the vars startHindsight() derives " + "itself (HINDSIGHT_API_PORT, the retain token/deadline budget).")
|
|
22039
|
+
env: exports_external.record(exports_external.union([exports_external.string(), exports_external.number(), exports_external.boolean()])).optional().describe("Operator overrides for switchroom's capability-gated Hindsight " + "performance defaults. Only the keys switchroom actually manages are " + "honoured (`HINDSIGHT_PERF_ENV_KEYS` in " + "src/setup/hindsight-perf-defaults.ts: RERANKER_LOCAL_FP16, " + "RERANKER_LOCAL_BATCH_SIZE, LLM_MAX_CONCURRENT, " + "RETAIN/CONSOLIDATION_LLM_MAX_CONCURRENT, LLM_STRICT_SCHEMA, " + "LLM_MAX_RETRIES, CONSOLIDATION_LLM_PARALLELISM, " + "MAX_OBSERVATIONS_PER_SCOPE, " + "RECALL_MAX_CANDIDATES_PER_SOURCE, LINK_EXPANSION_PER_ENTITY_LIMIT, " + "LINK_EXPANSION_TIMEOUT, LLM_REASONING_EFFORT, " + "RERANKER_LOCAL_BUCKET_BATCHING, RERANKER_MAX_CANDIDATES, " + "RERANKER_LOCAL_MAX_CONCURRENT, RECALL_MAX_CONCURRENT, " + "REFLECT_WALL_TIMEOUT, WORKER_CONSOLIDATION_RESERVED_SLOTS, " + "WORKER_CONSOLIDATION_SLOT_LIMIT, " + "CONSOLIDATION_MAX_MEMORIES_PER_ROUND, GRAPH_SEED_MIN_SIMILARITY, " + "LLM_SUPPORTS_MAX_ITEMS, RECENCY_DECAY_FUNCTION, " + "RECENCY_DECAY_HALFLIFE_DAYS \u2014 switchroom defaults recall's recency " + "curve to `exponential` with a 30-day half-life so a fact retained " + "today outranks a stale one, instead of upstream's near-flat " + "linear/365-day window), the override-only keys " + "switchroom manages but ships NO default for " + "(`HINDSIGHT_PERF_OVERRIDE_ONLY_KEYS`: " + "HINDSIGHT_API_WORKER_CONSOLIDATION_BANK_PRIORITY \u2014 a per-deployment " + "`bank-pattern:priority,...` map; unset means upstream's flat " + "created_at FIFO across banks; and " + "HINDSIGHT_CE_DECISIVE_RELATIVE_GAP \u2014 the rollback knob for " + "switchroom's CE-saturation damping patch, a float; >= ~0.65 backs the " + "damping out entirely, unset means the patch's own derived gap; and " + "HINDSIGHT_API_RECENCY_DECAY_LINEAR_WINDOW_DAYS \u2014 only read when the " + "decay function is `linear`, so switchroom ships no default for it but " + "still honours an operator who flips the function back; and " + "HINDSIGHT_API_WORKER_MAX_SLOTS \u2014 the worker poller's TOTAL in-flight " + "task budget, the pool WORKER_CONSOLIDATION_RESERVED_SLOTS reserves out of; " + "unset means upstream's own default; and " + "HINDSIGHT_API_WORKER_RETAIN_RESERVED_SLOTS \u2014 the reserved slot FLOOR for the " + "retain (memory write) lane, carved from that same total; unset means " + "upstream's own 0, i.e. no floor and retain competes for the shared " + "pool; and " + "the pre-0.8.6 name HINDSIGHT_API_WORKER_<TYPE>_MAX_SLOTS is still " + "accepted for every operation type and normalised to " + "..._RESERVED_SLOTS on the way in, because setting both names for " + "one type is a hard boot failure in the engine and switchroom " + "therefore emits only the canonical name; and " + "HINDSIGHT_API_SEMANTIC_MIN_SIMILARITY \u2014 the cosine-similarity floor a " + "candidate must reach to be returned by the semantic retrieval arm at " + "all (0..1); unset means upstream's own 0.3, and where it should sit " + "depends on the bank's embedding model and phrasing diversity, so " + "switchroom ships no opinion; and " + "HINDSIGHT_MCP_RECALL_BUDGET_MODE \u2014 the rollback knob for switchroom's " + "mcp-recall-token-budget image patch; `legacy` restores upstream's " + "exact pre-patch recall returns, anything else or unset is the " + "honest-envelope mode; and " + "HINDSIGHT_API_LLM_TEMPERATURE_REFLECT \u2014 upstream's own per-op reflect " + "temperature knob, made live by switchroom's reflect-temperature image " + "patch; a float, or `none` to omit the kwarg (provider default, " + "upstream's accidental pre-patch behaviour); unset means the image's " + "baked default 0.1; and " + "HINDSIGHT_API_CONSOLIDATION_RECALL_MAX_CONCURRENT \u2014 the background " + "half of switchroom's recall-admission split (#3660): of the " + "RECALL_MAX_CONCURRENT admission slots, at most this many may be held " + "by background consolidation recalls at once, so foreground per-turn " + "recall always keeps the remainder; must be >= 1 and strictly less " + "than RECALL_MAX_CONCURRENT or the engine refuses to boot; unset " + "means the image's derived default min(2, RECALL_MAX_CONCURRENT - 1); " + "1 biases hard toward the interactive lane while a consolidation " + "backlog drains; and " + "HINDSIGHT_MM_REFRESH_MIN_INTERVAL_S \u2014 the rollback/tuning knob for " + "switchroom's MM-refresh-debounce image patch: the minimum seconds " + "between consolidation-triggered refreshes of one mental model; unset " + "means the image's baked 3600, 0 restores upstream's " + "refresh-every-round behaviour; explicit and cron-scheduled refreshes " + "are never debounced; and " + "HINDSIGHT_API_TEMPORAL_LANGUAGES \u2014 the language set dateparser is " + "restricted to during temporal query analysis, made live by switchroom's " + "temporal-language image patch (which ended a 200+-locale auto-detection " + "pass that blocked the shared asyncio loop on every recall); " + "comma-separated, unset means the image's baked `en`, set e.g. `en,es` to " + "restore i18n parsing; and " + "HINDSIGHT_API_TEMPORAL_MAX_QUERY_CHARS \u2014 the max chars of query text " + "dateparser.search_dates() is handed during temporal analysis (0 = " + "unlimited), made live by switchroom's temporal-offload image patch " + "(which moved the synchronous extraction off the shared asyncio loop onto " + "a single-worker thread and bounds its input, ending multi-second loop " + "stalls on multi-KB consolidation queries); unset means the image's baked " + "2000, set 0 to restore an unbounded full scan); and " + "HINDSIGHT_API_TEXT_SEARCH_EXTENSION_NATIVE_LANGUAGE \u2014 the Postgres " + "text-search regconfig the native BM25 arms and the search_vector index " + "write run to_tsvector/to_tsquery against; unset means upstream's " + "always-safe `english`, set `hindsight_english` to use switchroom's " + "junk-stopping config (dict + stopword file provisioned durably by the " + "image entrypoint, recall degrading to semantic-only if it is ever " + "missing); plus the " + "embedded-PostgreSQL (pg0) sizing keys switchroom manages in " + "src/setup/hindsight-pg-defaults.ts (`HINDSIGHT_PG_ENV_KEYS`: " + "SWITCHROOM_HINDSIGHT_PG_EFFECTIVE_CACHE_SIZE, " + "SWITCHROOM_HINDSIGHT_PG_SHARED_BUFFERS \u2014 a postgres size string such " + "as `4GB`, or the sentinel `off` to leave pg0's own default for that " + "one knob). A value set here " + "REPLACES switchroom's default and is emitted even when the gating " + "capability is absent, so an operator can always force a knob. Other " + "`HINDSIGHT_API_*` keys are deliberately IGNORED \u2014 a blanket " + "passthrough would collide with the vars startHindsight() derives " + "itself (HINDSIGHT_API_PORT, the retain token/deadline budget).")
|
|
22040
22040
|
});
|
|
22041
22041
|
MicrosoftWorkspaceConfigSchema = exports_external.object({
|
|
22042
22042
|
microsoft_client_id: exports_external.string().min(1).optional().describe("Microsoft OAuth application (client) ID from Entra portal " + "(literal string or vault reference e.g. " + "'vault:microsoft-oauth-client-id'). OPTIONAL \u2014 omit it to use " + "switchroom's shipped default Microsoft app (zero-config). " + "Set it only to bring your own Entra app (BYO)."),
|
|
@@ -22489,7 +22489,9 @@ var init_schema = __esm(() => {
|
|
|
22489
22489
|
path: exports_external.string().regex(/^[a-zA-Z0-9~._\-/]+$/, "config_repo.path must not contain shell-special characters ($, `, \", ', \\, etc.)").default("~/.switchroom-config").describe("Filesystem path to the operator's private config repo. Must be a git " + "repo (else `switchroom doctor` FAILs). Tilde-expanded at read time."),
|
|
22490
22490
|
push: exports_external.boolean().default(true).describe("When true, `config-repo sync` pushes after committing (subject to the " + "`require_private` gate). When false, it commits locally only \u2014 useful " + "for offline hosts or a review-before-push workflow."),
|
|
22491
22491
|
remote: exports_external.string().min(1).default("origin").describe("Git remote name to push to. Default `origin`."),
|
|
22492
|
-
require_private: exports_external.boolean().default(true).describe("Refuse to PUSH unless the GitHub API confirms the remote repo is " + "private. On a public remote \u2014 or when the API is unreachable \u2014 the " + "push is skipped (commits still land locally) and a WARN is emitted. " + "Fail-safe against exfiltrating memory files / workspace state to a " + "repo that has been flipped public. Default true.")
|
|
22492
|
+
require_private: exports_external.boolean().default(true).describe("Refuse to PUSH unless the GitHub API confirms the remote repo is " + "private. On a public remote \u2014 or when the API is unreachable \u2014 the " + "push is skipped (commits still land locally) and a WARN is emitted. " + "Fail-safe against exfiltrating memory files / workspace state to a " + "repo that has been flipped public. Default true."),
|
|
22493
|
+
interval_minutes: exports_external.number().int().min(5).max(59).default(30).describe("Cadence (minutes) of the scheduled `config-repo sync` tick installed " + "by `switchroom config-repo --install-cron`. Rendered as a cron " + "`*/N * * * *` minute field, so it must be 5..59 (the 5-min floor " + "matches the rest of the fleet's cron floor). Default 30 (Ken's " + "approved cadence). Changing it takes effect on the next " + "`--install-cron` / `switchroom update` reconcile."),
|
|
22494
|
+
include_vault_backup: exports_external.enum(["off", "daily", "every_tick"]).default("daily").describe("Whether the scheduled tick also runs `switchroom vault backup` " + "(an encrypted vault/memory snapshot into <path>/vault-backups/) " + "before the sync. `off`: sync only. `daily`: an extra once-per-day " + "cron leg runs `vault backup && config-repo sync` (the 30-min legs " + "stay sync-only). `every_tick`: every tick runs the backup first. " + "Default `daily` \u2014 the first automated vault backup on this host.")
|
|
22493
22495
|
});
|
|
22494
22496
|
SwitchroomConfigSchema = exports_external.object({
|
|
22495
22497
|
switchroom: exports_external.object({
|
|
@@ -49357,7 +49359,7 @@ function createWorkerActivityFeed(opts) {
|
|
|
49357
49359
|
const floodWaitRemainingMs = opts.floodWaitRemainingMs ?? (() => 0);
|
|
49358
49360
|
const minEditInterval = opts.minEditIntervalMs ?? 2500;
|
|
49359
49361
|
const elapsedRefreshMs = Math.max(minEditInterval, Math.floor(opts.elapsedRefreshMs ?? 15000));
|
|
49360
|
-
const firstPaintMin = opts.firstPaintMinMs ??
|
|
49362
|
+
const firstPaintMin = opts.firstPaintMinMs ?? 4000;
|
|
49361
49363
|
const heartbeatTickMs = opts.heartbeatTickMs ?? 6000;
|
|
49362
49364
|
const maxRows = Math.max(1, Math.floor(opts.maxRows ?? 8));
|
|
49363
49365
|
const staleWorkerTtlMs = Math.max(1, Math.floor(opts.staleWorkerTtlMs ?? 50 * 60000));
|
|
@@ -79336,7 +79338,6 @@ function isStructuralNarration(text4, followedByToolUse) {
|
|
|
79336
79338
|
var EPHEMERAL_TOOLS = new Set([
|
|
79337
79339
|
"react",
|
|
79338
79340
|
"send_typing",
|
|
79339
|
-
"pin_message",
|
|
79340
79341
|
"delete_message",
|
|
79341
79342
|
"edit_message"
|
|
79342
79343
|
]);
|
|
@@ -82650,6 +82651,12 @@ function decideTurnEndGate(snapshot) {
|
|
|
82650
82651
|
}
|
|
82651
82652
|
|
|
82652
82653
|
// gateway/turn-record-status.ts
|
|
82654
|
+
function computeTurnDurationMs(startedAt, now) {
|
|
82655
|
+
if (typeof startedAt !== "number" || !Number.isFinite(startedAt) || startedAt <= 0) {
|
|
82656
|
+
return 0;
|
|
82657
|
+
}
|
|
82658
|
+
return Math.max(0, now - startedAt);
|
|
82659
|
+
}
|
|
82653
82660
|
function backstopSendOutcomeGated(args) {
|
|
82654
82661
|
if (args.threw)
|
|
82655
82662
|
return "failed";
|
|
@@ -83605,7 +83612,7 @@ function handleSessionEvent(deps, ev) {
|
|
|
83605
83612
|
chatId,
|
|
83606
83613
|
threadId,
|
|
83607
83614
|
turnId: turn.turnId,
|
|
83608
|
-
turnDurationMs: turn.startedAt
|
|
83615
|
+
turnDurationMs: computeTurnDurationMs(turn.startedAt, Date.now()),
|
|
83609
83616
|
reactionCount: reactionTransitionCounts.get(tKey) ?? 0
|
|
83610
83617
|
});
|
|
83611
83618
|
}
|
|
@@ -83623,7 +83630,7 @@ function handleSessionEvent(deps, ev) {
|
|
|
83623
83630
|
finalizeStatusReaction(chatId, threadId, "done");
|
|
83624
83631
|
{
|
|
83625
83632
|
const sKey = streamKey2(chatId, threadId);
|
|
83626
|
-
const turnDurationMs = turn.startedAt
|
|
83633
|
+
const turnDurationMs = computeTurnDurationMs(turn.startedAt, Date.now());
|
|
83627
83634
|
logStreamingEvent2({
|
|
83628
83635
|
kind: "turn_end",
|
|
83629
83636
|
chatId,
|
|
@@ -83857,7 +83864,7 @@ function handleSessionEvent(deps, ev) {
|
|
|
83857
83864
|
finalizeStatusReaction(chatId, threadId, terminalReason);
|
|
83858
83865
|
{
|
|
83859
83866
|
const sKey = streamKey2(chatId, threadId);
|
|
83860
|
-
const turnDurationMs = turn.startedAt
|
|
83867
|
+
const turnDurationMs = computeTurnDurationMs(turn.startedAt, Date.now());
|
|
83861
83868
|
logStreamingEvent2({
|
|
83862
83869
|
kind: "turn_end",
|
|
83863
83870
|
chatId,
|
|
@@ -87743,6 +87750,27 @@ init_peercred();
|
|
|
87743
87750
|
// ../src/agents/scaffold.ts
|
|
87744
87751
|
init_generation_stamp();
|
|
87745
87752
|
|
|
87753
|
+
// ../src/setup/hindsight-recall-passthrough.ts
|
|
87754
|
+
var HINDSIGHT_RECALL_TAG_WEIGHT_SEED = Object.freeze({ sidechain: 0.8 });
|
|
87755
|
+
var HINDSIGHT_RECALL_PROMPT_PREAMBLE_DEFAULT = "Relevant memories from past conversations (prioritize recent when " + "conflicting). Only use memories that are directly useful to continue " + "this conversation; ignore the rest:";
|
|
87756
|
+
var RECALL_PASSTHROUGH_DEFAULTS = Object.freeze({
|
|
87757
|
+
budget: "low",
|
|
87758
|
+
maxTokens: 1024,
|
|
87759
|
+
preferObservations: true,
|
|
87760
|
+
contextTurns: 2,
|
|
87761
|
+
roles: ["user", "assistant"],
|
|
87762
|
+
promptPreamble: HINDSIGHT_RECALL_PROMPT_PREAMBLE_DEFAULT,
|
|
87763
|
+
tags: [],
|
|
87764
|
+
tagsMatch: "any",
|
|
87765
|
+
tagGroups: {},
|
|
87766
|
+
tagWeights: HINDSIGHT_RECALL_TAG_WEIGHT_SEED,
|
|
87767
|
+
additionalBankFilters: {},
|
|
87768
|
+
transcriptFallback: true,
|
|
87769
|
+
transcriptTailBytes: 262144,
|
|
87770
|
+
maxQueryChars: 800,
|
|
87771
|
+
parallel: true
|
|
87772
|
+
});
|
|
87773
|
+
|
|
87746
87774
|
// ../src/setup/hindsight-recall-tunables.ts
|
|
87747
87775
|
var RECALL_DEADLINE_HEADROOM_SECONDS = 2;
|
|
87748
87776
|
var MIN_RECALL_HOOK_TIMEOUT_SECONDS = RECALL_DEADLINE_HEADROOM_SECONDS + 1;
|
|
@@ -88018,7 +88046,8 @@ var HINDSIGHT_PERF_OVERRIDE_ONLY_KEYS = new Set([
|
|
|
88018
88046
|
"HINDSIGHT_API_CONSOLIDATION_RECALL_MAX_CONCURRENT",
|
|
88019
88047
|
"HINDSIGHT_MM_REFRESH_MIN_INTERVAL_S",
|
|
88020
88048
|
"HINDSIGHT_API_TEMPORAL_LANGUAGES",
|
|
88021
|
-
"HINDSIGHT_API_TEMPORAL_MAX_QUERY_CHARS"
|
|
88049
|
+
"HINDSIGHT_API_TEMPORAL_MAX_QUERY_CHARS",
|
|
88050
|
+
"HINDSIGHT_API_TEXT_SEARCH_EXTENSION_NATIVE_LANGUAGE"
|
|
88022
88051
|
]);
|
|
88023
88052
|
var HINDSIGHT_WORKER_SLOT_TYPES = [
|
|
88024
88053
|
"consolidation",
|
|
@@ -88440,27 +88469,6 @@ init_vault();
|
|
|
88440
88469
|
// ../src/setup/onboarding.ts
|
|
88441
88470
|
init_paths();
|
|
88442
88471
|
|
|
88443
|
-
// ../src/setup/hindsight-recall-passthrough.ts
|
|
88444
|
-
var HINDSIGHT_RECALL_TAG_WEIGHT_SEED = Object.freeze({ sidechain: 0.8 });
|
|
88445
|
-
var HINDSIGHT_RECALL_PROMPT_PREAMBLE_DEFAULT = "Relevant memories from past conversations (prioritize recent when " + "conflicting). Only use memories that are directly useful to continue " + "this conversation; ignore the rest:";
|
|
88446
|
-
var RECALL_PASSTHROUGH_DEFAULTS = Object.freeze({
|
|
88447
|
-
budget: "low",
|
|
88448
|
-
maxTokens: 1024,
|
|
88449
|
-
preferObservations: true,
|
|
88450
|
-
contextTurns: 2,
|
|
88451
|
-
roles: ["user", "assistant"],
|
|
88452
|
-
promptPreamble: HINDSIGHT_RECALL_PROMPT_PREAMBLE_DEFAULT,
|
|
88453
|
-
tags: [],
|
|
88454
|
-
tagsMatch: "any",
|
|
88455
|
-
tagGroups: {},
|
|
88456
|
-
tagWeights: HINDSIGHT_RECALL_TAG_WEIGHT_SEED,
|
|
88457
|
-
additionalBankFilters: {},
|
|
88458
|
-
transcriptFallback: true,
|
|
88459
|
-
transcriptTailBytes: 262144,
|
|
88460
|
-
maxQueryChars: 800,
|
|
88461
|
-
parallel: true
|
|
88462
|
-
});
|
|
88463
|
-
|
|
88464
88472
|
// ../src/repos/bare-clone.ts
|
|
88465
88473
|
init_paths();
|
|
88466
88474
|
|
|
@@ -95142,6 +95150,12 @@ function maybeRotate(path3, fs3, maxBytes = TURNS_JSONL_MAX_BYTES) {
|
|
|
95142
95150
|
}
|
|
95143
95151
|
|
|
95144
95152
|
// gateway/turn-record-status.ts
|
|
95153
|
+
function computeTurnDurationMs2(startedAt, now) {
|
|
95154
|
+
if (typeof startedAt !== "number" || !Number.isFinite(startedAt) || startedAt <= 0) {
|
|
95155
|
+
return 0;
|
|
95156
|
+
}
|
|
95157
|
+
return Math.max(0, now - startedAt);
|
|
95158
|
+
}
|
|
95145
95159
|
function computeTurnStatus(turn) {
|
|
95146
95160
|
switch (turn.deliveryOutcome) {
|
|
95147
95161
|
case "failed":
|
|
@@ -95170,7 +95184,7 @@ function buildTurnRecord(turn, endedAt) {
|
|
|
95170
95184
|
return {
|
|
95171
95185
|
ts: Math.floor(endedAt / 1000),
|
|
95172
95186
|
agent: turn.agent,
|
|
95173
|
-
duration_ms: turn.startedAt
|
|
95187
|
+
duration_ms: computeTurnDurationMs2(turn.startedAt, endedAt),
|
|
95174
95188
|
tools: turn.toolCallCount ?? 0,
|
|
95175
95189
|
status: computeTurnStatus(turn),
|
|
95176
95190
|
turn_id: turn.turnId,
|
|
@@ -96933,7 +96947,7 @@ function buildSilencePokeOptions(deps) {
|
|
|
96933
96947
|
const tearsDownLiveTurn = turnMatchesFallback && wedgedTurn != null && turnLiveForItsTopic(wedgedTurn);
|
|
96934
96948
|
const turnStartedAt = activeTurnStartedAt.get(fbKey);
|
|
96935
96949
|
if (turnStartedAt != null) {
|
|
96936
|
-
const turnDurationMs = Date.now()
|
|
96950
|
+
const turnDurationMs = computeTurnDurationMs(turnStartedAt, Date.now());
|
|
96937
96951
|
const outboundMetrics = getOutboundMetrics2(fbKey);
|
|
96938
96952
|
emitRuntimeMetric2({
|
|
96939
96953
|
kind: "turn_ended",
|
|
@@ -103323,10 +103337,10 @@ function startOutboxSweep(deps) {
|
|
|
103323
103337
|
}
|
|
103324
103338
|
|
|
103325
103339
|
// ../src/build-info.ts
|
|
103326
|
-
var VERSION2 = "0.20.
|
|
103327
|
-
var COMMIT_SHA = "
|
|
103328
|
-
var COMMIT_DATE = "2026-08-
|
|
103329
|
-
var LATEST_PR =
|
|
103340
|
+
var VERSION2 = "0.20.12";
|
|
103341
|
+
var COMMIT_SHA = "298e1e15";
|
|
103342
|
+
var COMMIT_DATE = "2026-08-06T12:14:41Z";
|
|
103343
|
+
var LATEST_PR = 4468;
|
|
103330
103344
|
var COMMITS_AHEAD_OF_TAG = 0;
|
|
103331
103345
|
|
|
103332
103346
|
// gateway/boot-version.ts
|
|
@@ -108773,10 +108787,6 @@ function clearQueuedCard(key, messageId) {
|
|
|
108773
108787
|
}
|
|
108774
108788
|
var bannerPinPersistEnabled = !STATIC;
|
|
108775
108789
|
var toolPinPersistEnabled = !STATIC;
|
|
108776
|
-
var TOOL_PIN_TTL_MS = (() => {
|
|
108777
|
-
const v = Number(process.env.SWITCHROOM_TOOL_PIN_TTL_MS);
|
|
108778
|
-
return Number.isFinite(v) && v > 0 ? v : 604800000;
|
|
108779
|
-
})();
|
|
108780
108790
|
function statusPinApi() {
|
|
108781
108791
|
return createStatusPinApi(() => lockedBot, robustApiCall);
|
|
108782
108792
|
}
|
|
@@ -110377,7 +110387,6 @@ var ALLOWED_TOOLS = new Set([
|
|
|
110377
110387
|
"download_attachment",
|
|
110378
110388
|
"edit_message",
|
|
110379
110389
|
"send_typing",
|
|
110380
|
-
"pin_message",
|
|
110381
110390
|
"delete_message",
|
|
110382
110391
|
"forward_message",
|
|
110383
110392
|
"get_recent_messages",
|
|
@@ -110411,8 +110420,6 @@ async function executeToolCall(tool, args, callerAgentName = null) {
|
|
|
110411
110420
|
return executeEditMessage(args);
|
|
110412
110421
|
case "send_typing":
|
|
110413
110422
|
return executeSendTyping(args);
|
|
110414
|
-
case "pin_message":
|
|
110415
|
-
return executePinMessage(args);
|
|
110416
110423
|
case "delete_message":
|
|
110417
110424
|
return executeDeleteMessage(args);
|
|
110418
110425
|
case "forward_message":
|
|
@@ -111218,27 +111225,6 @@ async function executeSendTyping(args) {
|
|
|
111218
111225
|
}
|
|
111219
111226
|
return { content: [{ type: "text", text: `${action} indicator sent (auto-refreshes every 4s, stops after 30s or next reply)` }] };
|
|
111220
111227
|
}
|
|
111221
|
-
async function executePinMessage(args) {
|
|
111222
|
-
if (!args.chat_id)
|
|
111223
|
-
throw new Error("pin_message: chat_id is required");
|
|
111224
|
-
if (!args.message_id)
|
|
111225
|
-
throw new Error("pin_message: message_id is required");
|
|
111226
|
-
const pinChatId = String(args.chat_id ?? "");
|
|
111227
|
-
assertAllowedChat(pinChatId);
|
|
111228
|
-
const pinMsgId = Number(args.message_id);
|
|
111229
|
-
await robustApiCall(() => lockedBot.api.pinChatMessage(pinChatId, pinMsgId), { chat_id: pinChatId, verb: "pin_message" });
|
|
111230
|
-
statusPinRightsCache.clear(pinChatId);
|
|
111231
|
-
if (toolPinPersistEnabled) {
|
|
111232
|
-
const toolPinKey = `tool:${pinChatId}:${pinMsgId}`;
|
|
111233
|
-
mutateStatusPinRow(STATUS_PIN_STORE_PATH, statusPinStoreFs, toolPinKey, {
|
|
111234
|
-
pinKey: toolPinKey,
|
|
111235
|
-
chatId: pinChatId,
|
|
111236
|
-
messageId: pinMsgId,
|
|
111237
|
-
expiresAt: Date.now() + TOOL_PIN_TTL_MS
|
|
111238
|
-
});
|
|
111239
|
-
}
|
|
111240
|
-
return { content: [{ type: "text", text: `pinned message ${args.message_id}` }] };
|
|
111241
|
-
}
|
|
111242
111228
|
async function executeDeleteMessage(args) {
|
|
111243
111229
|
if (!args.chat_id)
|
|
111244
111230
|
throw new Error("delete_message: chat_id is required");
|
|
@@ -117137,6 +117123,10 @@ async function startGateway() {
|
|
|
117137
117123
|
const raw = Number(process.env.SWITCHROOM_TG_WORKER_FEED_MAX_ROWS);
|
|
117138
117124
|
return Number.isInteger(raw) && raw > 0 ? raw : undefined;
|
|
117139
117125
|
})();
|
|
117126
|
+
const workerFeedFirstPaintMs = (() => {
|
|
117127
|
+
const raw = Number(process.env.SWITCHROOM_TG_WORKER_FEED_FIRST_PAINT_MS);
|
|
117128
|
+
return Number.isInteger(raw) && raw > 0 ? raw : undefined;
|
|
117129
|
+
})();
|
|
117140
117130
|
const foregroundNestingEnabled = process.env.SWITCHROOM_FOREGROUND_SUBAGENT_NESTING !== "0";
|
|
117141
117131
|
const orphanStatusEnabled = isOrphanSubagentStatusEnabled(process.env.SWITCHROOM_ORPHAN_SUBAGENT_STATUS);
|
|
117142
117132
|
if (workerActivityFeed != null) {
|
|
@@ -117159,6 +117149,7 @@ async function startGateway() {
|
|
|
117159
117149
|
},
|
|
117160
117150
|
floodWaitRemainingMs: probeFloodWaitRemainingMs,
|
|
117161
117151
|
maxRows: workerFeedMaxRows,
|
|
117152
|
+
firstPaintMinMs: workerFeedFirstPaintMs,
|
|
117162
117153
|
staleWorkerTtlMs: resolveInflightTerminalCapMs() + WORKER_FEED_STALE_TTL_MARGIN_MS,
|
|
117163
117154
|
absoluteRowLifetimeCapMs: resolveInflightTerminalCapMs() * WORKER_FEED_ABSOLUTE_ROW_LIFETIME_CAP_MULTIPLE,
|
|
117164
117155
|
groupMessageLifetimeCapMs: WORKER_FEED_GROUP_MESSAGE_LIFETIME_CAP_MS,
|
|
@@ -24962,18 +24962,6 @@ var init_bridge = __esm(async () => {
|
|
|
24962
24962
|
required: ["chat_id"]
|
|
24963
24963
|
}
|
|
24964
24964
|
},
|
|
24965
|
-
{
|
|
24966
|
-
name: "pin_message",
|
|
24967
|
-
description: "Pin a message in a Telegram chat. Useful for important outputs the user wants to find later. Requires admin rights in groups.",
|
|
24968
|
-
inputSchema: {
|
|
24969
|
-
type: "object",
|
|
24970
|
-
properties: {
|
|
24971
|
-
chat_id: { type: "string" },
|
|
24972
|
-
message_id: { type: "string" }
|
|
24973
|
-
},
|
|
24974
|
-
required: ["chat_id", "message_id"]
|
|
24975
|
-
}
|
|
24976
|
-
},
|
|
24977
24965
|
{
|
|
24978
24966
|
name: "delete_message",
|
|
24979
24967
|
description: "Delete a message the bot previously sent. Prefer edit_message if you just want to update text \u2014 delete_message is for true removal.",
|
|
@@ -8370,8 +8370,7 @@ const statusPinClaims = new Map<string, StatusPinClaim>()
|
|
|
8370
8370
|
// Rights-aware negative cache (#3024): chats where an auto status-pin attempt
|
|
8371
8371
|
// failed with the permanent "not enough rights to manage pinned messages" 400.
|
|
8372
8372
|
// Per-process only — a restart clears it so a later-granted pin right re-enables
|
|
8373
|
-
// auto-pin.
|
|
8374
|
-
// this cache (it always attempts and surfaces the error to the agent).
|
|
8373
|
+
// auto-pin.
|
|
8375
8374
|
const statusPinRightsCache = new PinRightsCache()
|
|
8376
8375
|
|
|
8377
8376
|
// Durable snapshot of the pin claim set on the persistent per-agent volume
|
|
@@ -8466,21 +8465,16 @@ const BANNER_PIN_KEY = 'banner:owner'
|
|
|
8466
8465
|
// run → no-op). The boot-cleanup gate below is widened to cover this.
|
|
8467
8466
|
const bannerPinPersistEnabled = !STATIC
|
|
8468
8467
|
|
|
8469
|
-
// `
|
|
8470
|
-
//
|
|
8471
|
-
//
|
|
8472
|
-
//
|
|
8473
|
-
//
|
|
8474
|
-
//
|
|
8475
|
-
//
|
|
8468
|
+
// LEGACY `tool:` pin drain (#3001; the `pin_message` MCP tool was retired in
|
|
8469
|
+
// #4452). No new `tool:<chatId>:<messageId>` rows are ever written now that the
|
|
8470
|
+
// tool is gone, but rows an earlier build persisted may still sit in the shared
|
|
8471
|
+
// status-pins.json with a TTL (`expiresAt`). This flag keeps the boot cleanup /
|
|
8472
|
+
// stale-pin sweep aware of those legacy rows so they are still expired and
|
|
8473
|
+
// drained correctly on upgrade — an unexpired legacy row is preserved across
|
|
8474
|
+
// boots (never reaped as if it were a work-scoped card), then unpinned once its
|
|
8475
|
+
// TTL lapses, exactly as before. Independent of PIN_STATUS_WHILE_WORKING.
|
|
8476
|
+
// Self-clears within one TTL window since nothing writes new tool rows.
|
|
8476
8477
|
const toolPinPersistEnabled = !STATIC
|
|
8477
|
-
// 7 days: generous — an agent-pinned message the operator still cares about
|
|
8478
|
-
// after a week has usually been re-pinned or acted on; anything older is the
|
|
8479
|
-
// stale-pin long tail this issue exists to clear. Override for tuning.
|
|
8480
|
-
const TOOL_PIN_TTL_MS = (() => {
|
|
8481
|
-
const v = Number(process.env.SWITCHROOM_TOOL_PIN_TTL_MS)
|
|
8482
|
-
return Number.isFinite(v) && v > 0 ? v : 7 * 24 * 60 * 60_000
|
|
8483
|
-
})()
|
|
8484
8478
|
|
|
8485
8479
|
// Persist (or drop) the slot-banner's pin row into the shared store. Routes
|
|
8486
8480
|
// through mutateStatusPinRow: a read-modify-write for ONLY the banner:owner key,
|
|
@@ -8950,8 +8944,7 @@ async function reconcileStatusPin(
|
|
|
8950
8944
|
// "not enough rights to manage pinned messages" 400 in a supergroup took the
|
|
8951
8945
|
// whole gateway down (marko, 2026-07-01). Auto status-pin is cosmetic; it
|
|
8952
8946
|
// must NEVER be able to crash the gateway. Any throw here is logged and
|
|
8953
|
-
// absorbed.
|
|
8954
|
-
// as a normal tool-error — that path is `executePinMessage`, not this one.)
|
|
8947
|
+
// absorbed.
|
|
8955
8948
|
try {
|
|
8956
8949
|
// Serialize per pinKey (F2): reconcileStatusPinInner reads `prev` from the
|
|
8957
8950
|
// in-memory claim map at its top, so overlapping same-key reconciles must
|
|
@@ -11808,7 +11801,7 @@ if (isGatewayMain && !STATIC) {
|
|
|
11808
11801
|
* bridge from calling arbitrary functions by name. */
|
|
11809
11802
|
const ALLOWED_TOOLS = new Set([
|
|
11810
11803
|
'reply', 'progress_update', 'react', 'download_attachment',
|
|
11811
|
-
'edit_message', 'send_typing', '
|
|
11804
|
+
'edit_message', 'send_typing', 'delete_message',
|
|
11812
11805
|
'forward_message', 'get_recent_messages',
|
|
11813
11806
|
'send_checklist', 'update_checklist',
|
|
11814
11807
|
'ask_user',
|
|
@@ -11844,8 +11837,6 @@ async function executeToolCall(
|
|
|
11844
11837
|
return executeEditMessage(args)
|
|
11845
11838
|
case 'send_typing':
|
|
11846
11839
|
return executeSendTyping(args)
|
|
11847
|
-
case 'pin_message':
|
|
11848
|
-
return executePinMessage(args)
|
|
11849
11840
|
case 'delete_message':
|
|
11850
11841
|
return executeDeleteMessage(args)
|
|
11851
11842
|
case 'forward_message':
|
|
@@ -13195,44 +13186,6 @@ async function executeSendTyping(args: Record<string, unknown>): Promise<unknown
|
|
|
13195
13186
|
return { content: [{ type: 'text', text: `${action} indicator sent (auto-refreshes every 4s, stops after 30s or next reply)` }] }
|
|
13196
13187
|
}
|
|
13197
13188
|
|
|
13198
|
-
async function executePinMessage(args: Record<string, unknown>): Promise<unknown> {
|
|
13199
|
-
if (!args.chat_id) throw new Error('pin_message: chat_id is required')
|
|
13200
|
-
if (!args.message_id) throw new Error('pin_message: message_id is required')
|
|
13201
|
-
const pinChatId = String(args.chat_id ?? '')
|
|
13202
|
-
assertAllowedChat(pinChatId)
|
|
13203
|
-
// #1075: wrap through robustApiCall so flood-wait / transient network
|
|
13204
|
-
// errors are retried. THREAD_NOT_FOUND on a stale topic surfaces to the
|
|
13205
|
-
// agent as a tool-error — pinning a vanished message is genuinely a
|
|
13206
|
-
// failure the agent should see.
|
|
13207
|
-
const pinMsgId = Number(args.message_id)
|
|
13208
|
-
await robustApiCall(
|
|
13209
|
-
() => lockedBot.api.pinChatMessage(pinChatId, pinMsgId), // allow-raw-pin: MCP `pin_message` tool — an explicit, agent-requested pin of an arbitrary message, not a progress surface with a claim.
|
|
13210
|
-
{ chat_id: pinChatId, verb: 'pin_message' },
|
|
13211
|
-
)
|
|
13212
|
-
// An explicit pin succeeded here, so the bot demonstrably HAS pin rights in
|
|
13213
|
-
// this chat now — clear any auto-pin negative-cache entry (#3024) so the auto
|
|
13214
|
-
// status-pin path resumes immediately rather than waiting for a restart. The
|
|
13215
|
-
// explicit tool itself never consults the cache; a failure above still
|
|
13216
|
-
// surfaces to the agent as a normal tool error (robustApiCall rethrows).
|
|
13217
|
-
statusPinRightsCache.clear(pinChatId)
|
|
13218
|
-
// #3001: register the tool pin in the shared status-pin store under a
|
|
13219
|
-
// `tool:` key so it is no longer fire-and-forget. Unlike work-scoped
|
|
13220
|
-
// fg:/wk: rows a tool pin has no "work finished" event, so a restart does
|
|
13221
|
-
// NOT reset it — boot cleanup keeps the row until its TTL, then unpins the
|
|
13222
|
-
// (likely long-forgotten) message. Best-effort fire-and-forget: a store
|
|
13223
|
-
// failure must never fail the tool call the pin already landed for.
|
|
13224
|
-
if (toolPinPersistEnabled) {
|
|
13225
|
-
const toolPinKey = `tool:${pinChatId}:${pinMsgId}`
|
|
13226
|
-
void mutateStatusPinRow(STATUS_PIN_STORE_PATH, statusPinStoreFs, toolPinKey, { // allow-raw-pin-store: records the TTL-scoped `tool:` row for the explicit pin above so the boot sweep can expire it.
|
|
13227
|
-
pinKey: toolPinKey,
|
|
13228
|
-
chatId: pinChatId,
|
|
13229
|
-
messageId: pinMsgId,
|
|
13230
|
-
expiresAt: Date.now() + TOOL_PIN_TTL_MS,
|
|
13231
|
-
})
|
|
13232
|
-
}
|
|
13233
|
-
return { content: [{ type: 'text', text: `pinned message ${args.message_id}` }] }
|
|
13234
|
-
}
|
|
13235
|
-
|
|
13236
13189
|
async function executeDeleteMessage(args: Record<string, unknown>): Promise<unknown> {
|
|
13237
13190
|
if (!args.chat_id) throw new Error('delete_message: chat_id is required')
|
|
13238
13191
|
if (!args.message_id) throw new Error('delete_message: message_id is required')
|
|
@@ -23874,6 +23827,14 @@ async function startGateway(): Promise<void> { // #2996 P0c: the boot IIFE, now
|
|
|
23874
23827
|
const raw = Number(process.env.SWITCHROOM_TG_WORKER_FEED_MAX_ROWS)
|
|
23875
23828
|
return Number.isInteger(raw) && raw > 0 ? raw : undefined
|
|
23876
23829
|
})()
|
|
23830
|
+
// First-paint hold for a prose-silent sub-agent card. Unset /
|
|
23831
|
+
// non-positive → the feed's built-in default (4000ms). Operator
|
|
23832
|
+
// override (SWITCHROOM_TG_WORKER_FEED_FIRST_PAINT_MS) lets first
|
|
23833
|
+
// paint be retuned/reverted (e.g. back to 8000) without a redeploy.
|
|
23834
|
+
const workerFeedFirstPaintMs = (() => {
|
|
23835
|
+
const raw = Number(process.env.SWITCHROOM_TG_WORKER_FEED_FIRST_PAINT_MS)
|
|
23836
|
+
return Number.isInteger(raw) && raw > 0 ? raw : undefined
|
|
23837
|
+
})()
|
|
23877
23838
|
// Model A — foreground sub-agent nesting in the parent's live
|
|
23878
23839
|
// activity draft. ON by default; this edits the SAME activity-
|
|
23879
23840
|
// summary message the tool_label feed already owns (not the
|
|
@@ -23947,6 +23908,10 @@ async function startGateway(): Promise<void> { // #2996 P0c: the boot IIFE, now
|
|
|
23947
23908
|
// channels.telegram.worker_feed.max_rows via the config cascade
|
|
23948
23909
|
// (scaffold emits SWITCHROOM_TG_WORKER_FEED_MAX_ROWS); unset → 8.
|
|
23949
23910
|
maxRows: workerFeedMaxRows,
|
|
23911
|
+
// First-paint hold for a prose-silent worker's initial card.
|
|
23912
|
+
// Unset → the feed's built-in default (4000ms); the operator
|
|
23913
|
+
// override SWITCHROOM_TG_WORKER_FEED_FIRST_PAINT_MS retunes it.
|
|
23914
|
+
firstPaintMinMs: workerFeedFirstPaintMs,
|
|
23950
23915
|
// Backstop TTL for the feed's stale-row reaper, DERIVED in code
|
|
23951
23916
|
// from the watcher's effective in-flight terminal cap (same env /
|
|
23952
23917
|
// default the watcher itself resolves) plus a margin — NOT a
|
|
@@ -21,6 +21,7 @@ import * as silencePoke from '../silence-poke.js'
|
|
|
21
21
|
import * as signalTracker from '../turn-signal-tracker.js'
|
|
22
22
|
import * as pendingProgress from '../pending-work-progress.js'
|
|
23
23
|
import { emitRuntimeMetric } from '../runtime-metrics.js'
|
|
24
|
+
import { computeTurnDurationMs } from './turn-record-status.js'
|
|
24
25
|
import { logStreamingEvent } from '../streaming-metrics.js'
|
|
25
26
|
import { clearSilentEndState } from '../silent-end.js'
|
|
26
27
|
import { purgeStaleTurnsForChat } from './turn-state-purge.js'
|
|
@@ -377,7 +378,11 @@ export function buildSilencePokeOptions(deps: LivenessWiringDeps): Parameters<ty
|
|
|
377
378
|
turnMatchesFallback && wedgedTurn != null && turnLiveForItsTopic(wedgedTurn)
|
|
378
379
|
const turnStartedAt = activeTurnStartedAt.get(fbKey)
|
|
379
380
|
if (turnStartedAt != null) {
|
|
380
|
-
|
|
381
|
+
// Guard shared with buildTurnRecord + the stream-render turn_ended paths:
|
|
382
|
+
// a 0 / bogus start must never emit `Date.now() - 0` (an absolute epoch
|
|
383
|
+
// value) as a duration. This path previously did a bare subtraction and
|
|
384
|
+
// poisoned the turn_ended dataset when `activeTurnStartedAt` held 0.
|
|
385
|
+
const turnDurationMs = computeTurnDurationMs(turnStartedAt, Date.now())
|
|
381
386
|
const outboundMetrics = signalTracker.getOutboundMetrics(fbKey)
|
|
382
387
|
emitRuntimeMetric({
|
|
383
388
|
kind: 'turn_ended',
|
|
@@ -352,9 +352,10 @@ export function collectSweepTargets(input: {
|
|
|
352
352
|
|
|
353
353
|
/**
|
|
354
354
|
* The messageIds in `chatId` that must SURVIVE a drain because they belong to
|
|
355
|
-
* deliberately-retained store rows: unexpired time-scoped `tool:` pins (
|
|
356
|
-
* `pin_message` MCP tool, #3001
|
|
357
|
-
*
|
|
355
|
+
* deliberately-retained store rows: unexpired time-scoped `tool:` pins (legacy
|
|
356
|
+
* rows from the retired `pin_message` MCP tool, #3001 / #4452 — no new ones are
|
|
357
|
+
* written), which `runStatusPinBootCleanup` intentionally KEEPS across restarts
|
|
358
|
+
* until they expire. Work-scoped rows (no `expiresAt`) are stale by
|
|
358
359
|
* definition after a restart and are NOT re-pin candidates; expired
|
|
359
360
|
* time-scoped rows are due for sweeping.
|
|
360
361
|
*/
|
|
@@ -17,7 +17,8 @@
|
|
|
17
17
|
* — the turn it represented is over or crashed), dropping rows only after a
|
|
18
18
|
* successful unpin (failed ones are retained with an attempt counter for a
|
|
19
19
|
* next-boot retry — see runStatusPinBootCleanup). Time-scoped `tool:` rows
|
|
20
|
-
* (the `pin_message` MCP tool, #3001
|
|
20
|
+
* (legacy pins from the retired `pin_message` MCP tool, #3001 / #4452 — no new
|
|
21
|
+
* ones are written) survive boots until their `expiresAt`.
|
|
21
22
|
* It does NOT re-adopt or re-pin — it only cleans up.
|
|
22
23
|
*
|
|
23
24
|
* Shape choice — SNAPSHOT, not append-log, mirroring obligation-store.ts. The
|
|
@@ -78,10 +79,10 @@ export interface PersistedStatusPin {
|
|
|
78
79
|
/** Wall-clock ms after which this pin is stale and boot cleanup unpins it.
|
|
79
80
|
* Rows WITHOUT this field are work-scoped (fg:/wk:/banner:) — stale the
|
|
80
81
|
* moment their owning session dies, so boot cleanup unpins them
|
|
81
|
-
* unconditionally. Rows WITH it (
|
|
82
|
-
* `pin_message` MCP tool, #3001
|
|
83
|
-
* no "work finished" event: they
|
|
84
|
-
* expired. */
|
|
82
|
+
* unconditionally. Rows WITH it (legacy `tool:` pins from the retired
|
|
83
|
+
* `pin_message` MCP tool, #3001 / #4452 — no new ones are written)
|
|
84
|
+
* represented deliberate agent pins with no "work finished" event: they
|
|
85
|
+
* SURVIVE restarts and are only swept once expired. */
|
|
85
86
|
expiresAt?: number
|
|
86
87
|
/** Boot-cleanup unpin retry counter (#3001). Incremented each boot the
|
|
87
88
|
* unpin fails (flood-wait exhausted / transient 5xx); the row is retained
|
|
@@ -282,10 +283,10 @@ export function pinnedMessageIsOurs(
|
|
|
282
283
|
* record whose pin MAY have landed in Telegram, so we must treat it exactly
|
|
283
284
|
* like a confirmed one and unpin it.
|
|
284
285
|
*
|
|
285
|
-
* TIME-SCOPED rows (`tool:` pins from the `pin_message` MCP
|
|
286
|
-
* `expiresAt`) have no "work finished" event, so a
|
|
287
|
-
* them: an unexpired row is RETAINED untouched across
|
|
288
|
-
* once `now >= expiresAt`.
|
|
286
|
+
* TIME-SCOPED rows (legacy `tool:` pins from the retired `pin_message` MCP
|
|
287
|
+
* tool, #4452, carrying `expiresAt`) have no "work finished" event, so a
|
|
288
|
+
* restart does NOT reset them: an unexpired row is RETAINED untouched across
|
|
289
|
+
* boots and only unpinned once `now >= expiresAt`.
|
|
289
290
|
*
|
|
290
291
|
* RETRY-SAFETY (#3001): a row is dropped only AFTER its unpin resolves. A
|
|
291
292
|
* failing unpin (flood-wait exhausted / transient 5xx) retains the row with an
|
|
@@ -90,7 +90,7 @@ import { formatTurnLifecycle } from './status-surface-log.js'
|
|
|
90
90
|
import { removeTurnActiveMarker, touchTurnActiveMarker, writeTurnActiveMarker } from './turn-active-marker.js'
|
|
91
91
|
import { withTurnEndGateBackstop } from './turn-end-gate-backstop.js'
|
|
92
92
|
import { decideTurnEndGate } from './turn-end-gate.js'
|
|
93
|
-
import { finalizeBackstopSendGated } from './turn-record-status.js'
|
|
93
|
+
import { finalizeBackstopSendGated, computeTurnDurationMs } from './turn-record-status.js'
|
|
94
94
|
import type { SilentEndDeps } from '../silent-end.js'
|
|
95
95
|
import type { ChatKey as _ChatKey } from './inbound-delivery-machine.js'
|
|
96
96
|
import type { SessionEvent } from '../session-tail.js'
|
|
@@ -2174,7 +2174,7 @@ export function handleSessionEvent(deps: StreamRenderDeps, ev: SessionEvent): vo
|
|
|
2174
2174
|
chatId,
|
|
2175
2175
|
threadId,
|
|
2176
2176
|
turnId: turn.turnId,
|
|
2177
|
-
turnDurationMs: turn.startedAt
|
|
2177
|
+
turnDurationMs: computeTurnDurationMs(turn.startedAt, Date.now()),
|
|
2178
2178
|
reactionCount: reactionTransitionCounts.get(tKey) ?? 0,
|
|
2179
2179
|
})
|
|
2180
2180
|
}
|
|
@@ -2214,7 +2214,7 @@ export function handleSessionEvent(deps: StreamRenderDeps, ev: SessionEvent): vo
|
|
|
2214
2214
|
// still appear in turn-duration graphs.
|
|
2215
2215
|
{
|
|
2216
2216
|
const sKey = streamKey(chatId, threadId)
|
|
2217
|
-
const turnDurationMs = turn.startedAt
|
|
2217
|
+
const turnDurationMs = computeTurnDurationMs(turn.startedAt, Date.now())
|
|
2218
2218
|
logStreamingEvent({
|
|
2219
2219
|
kind: 'turn_end',
|
|
2220
2220
|
chatId,
|
|
@@ -2799,7 +2799,7 @@ export function handleSessionEvent(deps: StreamRenderDeps, ev: SessionEvent): vo
|
|
|
2799
2799
|
finalizeStatusReaction(chatId, threadId, terminalReason)
|
|
2800
2800
|
{
|
|
2801
2801
|
const sKey = streamKey(chatId, threadId)
|
|
2802
|
-
const turnDurationMs = turn.startedAt
|
|
2802
|
+
const turnDurationMs = computeTurnDurationMs(turn.startedAt, Date.now())
|
|
2803
2803
|
logStreamingEvent({
|
|
2804
2804
|
kind: 'turn_end',
|
|
2805
2805
|
chatId,
|
|
@@ -24,6 +24,37 @@
|
|
|
24
24
|
*/
|
|
25
25
|
export type DeliveryOutcome = 'delivered' | 'failed' | 'suppressed'
|
|
26
26
|
|
|
27
|
+
/**
|
|
28
|
+
* The ONE way a turn's `duration_ms` is computed, shared by every emitter
|
|
29
|
+
* (`buildTurnRecord` → turns.jsonl, and all three `turn_ended` runtime-metric
|
|
30
|
+
* sites: the two stream-render turn-end paths + the liveness framework_fallback).
|
|
31
|
+
*
|
|
32
|
+
* Why a single helper: the three `turn_ended` emitters each hand-rolled the
|
|
33
|
+
* subtraction, and one drifted. `stream-render.ts` guarded with
|
|
34
|
+
* `startedAt > 0 ? now - startedAt : 0`; the framework_fallback path in
|
|
35
|
+
* `liveness-wiring.ts` did a bare `Date.now() - turnStartedAt` with only a
|
|
36
|
+
* `!= null` presence check. When `activeTurnStartedAt` held `0` (or any
|
|
37
|
+
* non-positive / non-finite value), that path emitted `duration_ms = now - 0`,
|
|
38
|
+
* i.e. the absolute Unix-epoch-ms — a ~56,000-year "duration". This poisoned the
|
|
39
|
+
* analysed turn_ended dataset (observed: 110 rows where `duration_ms === ts`),
|
|
40
|
+
* making every latency aggregate unusable.
|
|
41
|
+
*
|
|
42
|
+
* The invariant this helper enforces: a `duration_ms` is ALWAYS a non-negative
|
|
43
|
+
* elapsed-milliseconds value derived from a positive, finite start stamp — never
|
|
44
|
+
* an absolute epoch value, never negative (clock step back), never NaN. A start
|
|
45
|
+
* that is missing / zero / bogus yields `0` (a bounded, filterable sentinel)
|
|
46
|
+
* instead of a value that destroys aggregates.
|
|
47
|
+
*/
|
|
48
|
+
export function computeTurnDurationMs(
|
|
49
|
+
startedAt: number | null | undefined,
|
|
50
|
+
now: number,
|
|
51
|
+
): number {
|
|
52
|
+
if (typeof startedAt !== 'number' || !Number.isFinite(startedAt) || startedAt <= 0) {
|
|
53
|
+
return 0
|
|
54
|
+
}
|
|
55
|
+
return Math.max(0, now - startedAt)
|
|
56
|
+
}
|
|
57
|
+
|
|
27
58
|
/** The status strings written to turns.jsonl. `send_failed` is new in PR B. */
|
|
28
59
|
export type TurnStatus = 'complete' | 'no_reply' | 'send_failed'
|
|
29
60
|
|
|
@@ -261,7 +292,7 @@ export function buildTurnRecord(
|
|
|
261
292
|
return {
|
|
262
293
|
ts: Math.floor(endedAt / 1000),
|
|
263
294
|
agent: turn.agent,
|
|
264
|
-
duration_ms: turn.startedAt
|
|
295
|
+
duration_ms: computeTurnDurationMs(turn.startedAt, endedAt),
|
|
265
296
|
tools: turn.toolCallCount ?? 0,
|
|
266
297
|
status: computeTurnStatus(turn),
|
|
267
298
|
turn_id: turn.turnId,
|