switchroom 0.18.3 → 0.18.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agent-scheduler/index.js +2 -1
- package/dist/auth-broker/index.js +2 -1
- package/dist/cli/notion-write-pretool.mjs +2 -1
- package/dist/cli/switchroom.js +119 -74
- package/dist/host-control/main.js +3 -2
- package/dist/vault/approvals/kernel-server.js +2 -1
- package/dist/vault/broker/server.js +37 -8
- package/package.json +1 -1
- package/skills/switchroom-release/SKILL.md +78 -0
- package/telegram-plugin/auth-snapshot-format.ts +15 -1
- package/telegram-plugin/dist/gateway/gateway.js +922 -459
- package/telegram-plugin/gateway/gateway.ts +260 -21
- package/telegram-plugin/gateway/resolve-person.ts +304 -0
- package/telegram-plugin/gateway/unhandled-rejection-policy.ts +21 -1
- package/telegram-plugin/hooks/silent-end-scan.mjs +164 -40
- package/telegram-plugin/operator-events.ts +21 -0
- package/telegram-plugin/pending-work-progress.ts +42 -7
- package/telegram-plugin/quota-bar-format.ts +360 -0
- package/telegram-plugin/subagent-watcher.ts +27 -9
- package/telegram-plugin/tests/auth-snapshot-format.test.ts +21 -0
- package/telegram-plugin/tests/gateway-boot-marker-clear.test.ts +3 -3
- package/telegram-plugin/tests/operator-events.test.ts +16 -0
- package/telegram-plugin/tests/pending-work-progress.test.ts +116 -3
- package/telegram-plugin/tests/quota-bar-format.test.ts +444 -0
- package/telegram-plugin/tests/resolve-person.test.ts +290 -0
- package/telegram-plugin/tests/silent-end-interrupt-stop-integration.test.ts +53 -0
- package/telegram-plugin/tests/silent-end-interrupt-stop-scan.test.ts +138 -0
- package/telegram-plugin/tests/subagent-watcher.test.ts +61 -0
- package/telegram-plugin/tests/unhandled-rejection-policy.test.ts +19 -0
- package/telegram-plugin/tests/worker-activity-feed.test.ts +97 -0
- package/telegram-plugin/worker-activity-feed.ts +220 -15
- package/profiles/default/CLAUDE.md +0 -116
- package/telegram-plugin/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +0 -1
- 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/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/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/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
|
@@ -185,6 +185,12 @@ import {
|
|
|
185
185
|
import { clearStaleTelegramPollingState } from '../startup-reset.js'
|
|
186
186
|
import { gatewayStartupRetry } from './startup-network-retry.js'
|
|
187
187
|
import { writeQuarantineMarker } from './quarantine.js'
|
|
188
|
+
import {
|
|
189
|
+
runPersonDirectoryBootCheck,
|
|
190
|
+
safeResolvePersonName,
|
|
191
|
+
type PersonDirectory,
|
|
192
|
+
type RawPersonEntry,
|
|
193
|
+
} from './resolve-person.js'
|
|
188
194
|
// RFC H §7.3: auth-dashboard + auth-slot-parser deleted. Three chat
|
|
189
195
|
// verbs (/auth show | use | rotate) talk to switchroom-auth-broker
|
|
190
196
|
// via the thin client in src/auth/broker/client.ts.
|
|
@@ -721,6 +727,12 @@ const ACCESS_FILE = join(STATE_DIR, 'access.json')
|
|
|
721
727
|
const APPROVED_DIR = join(STATE_DIR, 'approved')
|
|
722
728
|
const ENV_FILE = join(STATE_DIR, '.env')
|
|
723
729
|
const INBOX_DIR = join(STATE_DIR, 'inbox')
|
|
730
|
+
// person_id name resolution (docs/configuration.md). Sibling to
|
|
731
|
+
// access.json but a COMPLETELY SEPARATE, purely config-derived file —
|
|
732
|
+
// scaffold regenerates it on every reconcile via writeFileSyncIfChanged,
|
|
733
|
+
// the gateway never mutates it, and (unlike access.json) it is read
|
|
734
|
+
// exactly ONCE at boot into PERSON_DIRECTORY below — no hot-reload.
|
|
735
|
+
const PEOPLE_FILE = join(STATE_DIR, 'people.json')
|
|
724
736
|
|
|
725
737
|
/**
|
|
726
738
|
* Trigger a restart of the agent + gateway pair.
|
|
@@ -1224,6 +1236,33 @@ function loadAccess(): Access {
|
|
|
1224
1236
|
return BOOT_ACCESS ?? readAccessFile()
|
|
1225
1237
|
}
|
|
1226
1238
|
|
|
1239
|
+
/**
|
|
1240
|
+
* Read `people.json` (the scaffold's plain projection of `users:` entries
|
|
1241
|
+
* that carry a `person_id`). Fail-open: ENOENT or corrupt/malformed JSON
|
|
1242
|
+
* returns an empty array rather than throwing — this feature must never
|
|
1243
|
+
* block startup. Unlike `access.json` this file is never gateway-mutated,
|
|
1244
|
+
* so there's no "move corrupt file aside" concern; the scaffold owns and
|
|
1245
|
+
* regenerates it on every reconcile.
|
|
1246
|
+
*/
|
|
1247
|
+
function readPeopleFile(): RawPersonEntry[] {
|
|
1248
|
+
try {
|
|
1249
|
+
const raw = readFileSync(PEOPLE_FILE, 'utf8')
|
|
1250
|
+
const parsed = JSON.parse(raw) as { entries?: unknown }
|
|
1251
|
+
if (!Array.isArray(parsed.entries)) return []
|
|
1252
|
+
return parsed.entries as RawPersonEntry[]
|
|
1253
|
+
} catch {
|
|
1254
|
+
return []
|
|
1255
|
+
}
|
|
1256
|
+
}
|
|
1257
|
+
|
|
1258
|
+
/**
|
|
1259
|
+
* Boot-time-only person-name directory (see resolve-person.ts module doc).
|
|
1260
|
+
* Populated exactly once by the boot-validation block in the startup IIFE
|
|
1261
|
+
* below, then never reassigned or re-read from disk again for the life of
|
|
1262
|
+
* this process — a config change requires an agent restart.
|
|
1263
|
+
*/
|
|
1264
|
+
let PERSON_DIRECTORY: PersonDirectory = { byTelegramKey: {} }
|
|
1265
|
+
|
|
1227
1266
|
function assertAllowedChat(chat_id: string | number): void {
|
|
1228
1267
|
const id = String(chat_id)
|
|
1229
1268
|
const access = loadAccess()
|
|
@@ -1488,6 +1527,64 @@ function resolveSubagentOriginChat(
|
|
|
1488
1527
|
}
|
|
1489
1528
|
}
|
|
1490
1529
|
|
|
1530
|
+
/**
|
|
1531
|
+
* Tracks worker-feed agents whose owner-DM fallback has already been
|
|
1532
|
+
* logged, so `resolveWorkerFeedChat` emits the routing-decision line once
|
|
1533
|
+
* per agent instead of every watcher tick (~1/s). Capped FIFO at 256 — a
|
|
1534
|
+
* late duplicate log is harmless (one extra stderr line), and the cap keeps
|
|
1535
|
+
* the set bounded across a long gateway lifetime even if origin resolution
|
|
1536
|
+
* is persistently broken fleet-wide (history disabled → every nested worker
|
|
1537
|
+
* hits the fallback). A gateway restart clears it.
|
|
1538
|
+
*/
|
|
1539
|
+
const WORKER_FEED_FALLBACK_LOG_CAP = 256
|
|
1540
|
+
const workerFeedOwnerDmFallbackLogged = new Set<string>()
|
|
1541
|
+
|
|
1542
|
+
/**
|
|
1543
|
+
* Resolve a worker-feed destination chat with a guaranteed last resort.
|
|
1544
|
+
*
|
|
1545
|
+
* The universal-liveness contract is "any active work has a card, at any
|
|
1546
|
+
* nesting depth, with or without a parent turn open." Origin resolution
|
|
1547
|
+
* (`resolveSubagentOriginChat`) only succeeds when the ancestor turn row is
|
|
1548
|
+
* in `turnsDb` and history is enabled; a depth-2+ worker whose chain can't
|
|
1549
|
+
* be walked (history disabled, row reaped, ancestor never stamped) would
|
|
1550
|
+
* otherwise fall through `fleetChatId || allowFrom[0]` and, if those are
|
|
1551
|
+
* empty too, hit the `chatId.length === 0` skip in `workerActivityFeed.update`
|
|
1552
|
+
* — painting NOTHING, silently. That is the one path most likely to violate
|
|
1553
|
+
* "any and all active work has a card."
|
|
1554
|
+
*
|
|
1555
|
+
* This never returns `''`: the precedence is origin chat → fleet chat →
|
|
1556
|
+
* first allowed chat (the owner DM). The owner DM is the durable floor —
|
|
1557
|
+
* "wrong chat" beats "no card." A one-line routing-decision log flags the
|
|
1558
|
+
* fallback so an operator can see the misroute without it reading as an
|
|
1559
|
+
* error (it isn't one — the card surfaced).
|
|
1560
|
+
*/
|
|
1561
|
+
function resolveWorkerFeedChat(
|
|
1562
|
+
agentId: string,
|
|
1563
|
+
fleetChatId: string,
|
|
1564
|
+
): { chatId: string; threadId?: number } {
|
|
1565
|
+
const origin = resolveSubagentOriginChat(agentId)
|
|
1566
|
+
if (origin != null && origin.chatId.length > 0) return origin
|
|
1567
|
+
if (fleetChatId.length > 0) return { chatId: fleetChatId }
|
|
1568
|
+
const ownerDm = loadAccess().allowFrom[0] ?? ''
|
|
1569
|
+
if (origin == null && fleetChatId.length === 0 && ownerDm.length > 0) {
|
|
1570
|
+
// Routing decision, not a warning: origin resolution failed and no
|
|
1571
|
+
// fleet chat is configured, so the nested worker's card lands in the
|
|
1572
|
+
// owner DM. Logged ONCE per agent so the misroute is auditable
|
|
1573
|
+
// without spamming every tick (the watcher drives onProgress ~1/s).
|
|
1574
|
+
if (!workerFeedOwnerDmFallbackLogged.has(agentId)) {
|
|
1575
|
+
workerFeedOwnerDmFallbackLogged.add(agentId)
|
|
1576
|
+
if (workerFeedOwnerDmFallbackLogged.size > WORKER_FEED_FALLBACK_LOG_CAP) {
|
|
1577
|
+
const oldest = workerFeedOwnerDmFallbackLogged.values().next().value
|
|
1578
|
+
if (oldest != null) workerFeedOwnerDmFallbackLogged.delete(oldest)
|
|
1579
|
+
}
|
|
1580
|
+
process.stderr.write(
|
|
1581
|
+
`telegram gateway: worker-feed origin unresolved agent=${agentId} — routing card to owner DM\n`,
|
|
1582
|
+
)
|
|
1583
|
+
}
|
|
1584
|
+
}
|
|
1585
|
+
return { chatId: ownerDm, threadId: origin?.threadId }
|
|
1586
|
+
}
|
|
1587
|
+
|
|
1491
1588
|
// ─── Periodic history reaper (#1073) ──────────────────────────────────────
|
|
1492
1589
|
// The init-time prune in history.ts only touched the `messages` table.
|
|
1493
1590
|
// `subagents` and `turns` in registry.db grew unbounded — every Agent()
|
|
@@ -4070,9 +4167,26 @@ async function resolveCompactCard(
|
|
|
4070
4167
|
{ chat_id: card.chatId, verb: `proactiveCompact.${kind}` },
|
|
4071
4168
|
);
|
|
4072
4169
|
} catch (err) {
|
|
4170
|
+
// Best-effort status-card edit — a transport hiccup (429 / "not
|
|
4171
|
+
// modified" / message gone) is not a liveness-logic error and must
|
|
4172
|
+
// not log a "card edit failed" warning (the warning-as-excuse-for-a-
|
|
4173
|
+
// stale-card anti-pattern). Only a genuine unexpected error warrants
|
|
4174
|
+
// a line; transport classes are silent.
|
|
4175
|
+
const desc = err instanceof Error ? err.message : String(err);
|
|
4176
|
+
const low = desc.toLowerCase();
|
|
4177
|
+
if (
|
|
4178
|
+
low.includes('not modified') ||
|
|
4179
|
+
low.includes('not found') ||
|
|
4180
|
+
low.includes("can't be edited") ||
|
|
4181
|
+
low.includes('cannot be edited') ||
|
|
4182
|
+
low.includes('not enough rights') ||
|
|
4183
|
+
low.includes('429') ||
|
|
4184
|
+
low.includes('retry after')
|
|
4185
|
+
) {
|
|
4186
|
+
return;
|
|
4187
|
+
}
|
|
4073
4188
|
process.stderr.write(
|
|
4074
|
-
`telegram gateway: proactive-compact ${kind} card edit failed:
|
|
4075
|
-
`${err instanceof Error ? err.message : String(err)}\n`,
|
|
4189
|
+
`telegram gateway: proactive-compact ${kind} card edit failed: ${desc}\n`,
|
|
4076
4190
|
);
|
|
4077
4191
|
}
|
|
4078
4192
|
}
|
|
@@ -12896,7 +13010,22 @@ async function drainActivitySummary(
|
|
|
12896
13010
|
turn.activityLastSentRender = target
|
|
12897
13011
|
} catch (err) {
|
|
12898
13012
|
const msg = err instanceof Error ? err.message : String(err)
|
|
12899
|
-
|
|
13013
|
+
const low = msg.toLowerCase()
|
|
13014
|
+
// Transport-class failures (429, message gone, "not modified") must
|
|
13015
|
+
// NOT inflate `activityDrainFailures` — that counter flags a turn as
|
|
13016
|
+
// DEGRADED on turn-end, and a transient rate-limit or an
|
|
13017
|
+
// already-deleted message is not a logic defect. Counting them would
|
|
13018
|
+
// false-flag a healthy turn. "not modified" is success; gone/429 are
|
|
13019
|
+
// retried or harmless. Only a genuine send/open failure counts.
|
|
13020
|
+
const isTransport =
|
|
13021
|
+
low.includes('not modified') ||
|
|
13022
|
+
low.includes('not found') ||
|
|
13023
|
+
low.includes("can't be edited") ||
|
|
13024
|
+
low.includes('cannot be edited') ||
|
|
13025
|
+
low.includes('not enough rights') ||
|
|
13026
|
+
low.includes('429') ||
|
|
13027
|
+
low.includes('retry after')
|
|
13028
|
+
if (!isTransport) {
|
|
12900
13029
|
turn.activityDrainFailures += 1
|
|
12901
13030
|
// Surface the failing anchor + topic: the resume-400 bug fed a
|
|
12902
13031
|
// fabricated 13-digit message_id as the reply anchor here, so every
|
|
@@ -13246,7 +13375,23 @@ function clearActivitySummary(turn: CurrentTurn, finalHtmlOverride?: string | nu
|
|
|
13246
13375
|
{ chat_id: chat, ...(thread != null ? { threadId: thread } : {}), verb: 'activity-summary.delete' },
|
|
13247
13376
|
)
|
|
13248
13377
|
} catch (err) {
|
|
13249
|
-
|
|
13378
|
+
// Best-effort teardown of a status card. A transport-class failure
|
|
13379
|
+
// (message already deleted, chat gone, 429) is not a liveness-logic
|
|
13380
|
+
// error — "message to delete not found" is the desired end state
|
|
13381
|
+
// here, and a 429 on a teardown delete is retried by robustApiCall.
|
|
13382
|
+
// Stay silent on those; warn only on a genuinely unexpected error.
|
|
13383
|
+
const msg = err instanceof Error ? err.message : String(err)
|
|
13384
|
+
const low = msg.toLowerCase()
|
|
13385
|
+
if (
|
|
13386
|
+
low.includes('not modified') ||
|
|
13387
|
+
low.includes('not found') ||
|
|
13388
|
+
low.includes('not enough rights') ||
|
|
13389
|
+
low.includes('429') ||
|
|
13390
|
+
low.includes('retry after')
|
|
13391
|
+
) {
|
|
13392
|
+
return
|
|
13393
|
+
}
|
|
13394
|
+
process.stderr.write(`telegram gateway: activity-summary delete failed: ${msg}\n`)
|
|
13250
13395
|
}
|
|
13251
13396
|
return
|
|
13252
13397
|
}
|
|
@@ -13272,10 +13417,26 @@ function clearActivitySummary(turn: CurrentTurn, finalHtmlOverride?: string | nu
|
|
|
13272
13417
|
{ chat_id: chat, ...(thread != null ? { threadId: thread } : {}), verb: 'activity-summary.finalize' },
|
|
13273
13418
|
)
|
|
13274
13419
|
} catch (err) {
|
|
13420
|
+
// Same transport-class discipline as the delete path: the card
|
|
13421
|
+
// finalize is a best-effort liveness edit. "not modified" = the card
|
|
13422
|
+
// already shows the finalized body (success); "not found" / "not
|
|
13423
|
+
// enough rights" = the message is gone (nothing to finalize); 429 is
|
|
13424
|
+
// retried by robustApiCall. None of those are liveness-logic errors
|
|
13425
|
+
// and none warrant a stderr warning. Warn only on the unexpected.
|
|
13275
13426
|
const msg = err instanceof Error ? err.message : String(err)
|
|
13276
|
-
|
|
13277
|
-
|
|
13427
|
+
const low = msg.toLowerCase()
|
|
13428
|
+
if (
|
|
13429
|
+
low.includes('not modified') ||
|
|
13430
|
+
low.includes('not found') ||
|
|
13431
|
+
low.includes("can't be edited") ||
|
|
13432
|
+
low.includes('cannot be edited') ||
|
|
13433
|
+
low.includes('not enough rights') ||
|
|
13434
|
+
low.includes('429') ||
|
|
13435
|
+
low.includes('retry after')
|
|
13436
|
+
) {
|
|
13437
|
+
return
|
|
13278
13438
|
}
|
|
13439
|
+
process.stderr.write(`telegram gateway: activity-summary finalize failed: ${msg}\n`)
|
|
13279
13440
|
}
|
|
13280
13441
|
})
|
|
13281
13442
|
}
|
|
@@ -16650,12 +16811,28 @@ async function handleInbound(
|
|
|
16650
16811
|
TOPIC_FRAMING_ENABLED && messageThreadId != null
|
|
16651
16812
|
? 'This message belongs to the current topic only — answer ONLY this question, in this topic. Do not also answer a pending message from another topic.'
|
|
16652
16813
|
: undefined
|
|
16814
|
+
// person_id name resolution (docs/configuration.md, resolve-person.ts):
|
|
16815
|
+
// chat-scoped, boot-time-static lookup — falls back to today's raw
|
|
16816
|
+
// id/username behavior (fail-open) whenever unresolved or when group
|
|
16817
|
+
// membership can't be positively confirmed from access.json's allowFrom.
|
|
16818
|
+
// Never touches access.json itself and never blocks/denies anything.
|
|
16819
|
+
const rawUser = from.username ?? String(from.id)
|
|
16820
|
+
const displayUser = safeResolvePersonName(
|
|
16821
|
+
PERSON_DIRECTORY,
|
|
16822
|
+
{
|
|
16823
|
+
telegramId: String(from.id),
|
|
16824
|
+
username: from.username,
|
|
16825
|
+
isDm: isDmChatId(chat_id),
|
|
16826
|
+
groupAllowFrom: access.groups[chat_id]?.allowFrom,
|
|
16827
|
+
},
|
|
16828
|
+
rawUser,
|
|
16829
|
+
)
|
|
16653
16830
|
const inboundMsg: InboundMessage = {
|
|
16654
16831
|
type: 'inbound',
|
|
16655
16832
|
chatId: chat_id,
|
|
16656
16833
|
...(messageThreadId != null ? { threadId: messageThreadId } : {}),
|
|
16657
16834
|
messageId: msgId ?? 0,
|
|
16658
|
-
user:
|
|
16835
|
+
user: displayUser,
|
|
16659
16836
|
userId: from.id,
|
|
16660
16837
|
ts: ctx.message?.date ?? Math.floor(Date.now() / 1000),
|
|
16661
16838
|
text: effectiveText,
|
|
@@ -16670,7 +16847,7 @@ async function handleInbound(
|
|
|
16670
16847
|
meta: {
|
|
16671
16848
|
chat_id,
|
|
16672
16849
|
...(msgId != null ? { message_id: String(msgId) } : {}),
|
|
16673
|
-
user:
|
|
16850
|
+
user: displayUser,
|
|
16674
16851
|
user_id: String(from.id),
|
|
16675
16852
|
ts: new Date((ctx.message?.date ?? 0) * 1000).toISOString(),
|
|
16676
16853
|
...(messageThreadId != null ? { message_thread_id: String(messageThreadId) } : {}),
|
|
@@ -22841,7 +23018,15 @@ async function handleAuthDashboardCallback(ctx: Context): Promise<void> {
|
|
|
22841
23018
|
tz,
|
|
22842
23019
|
now: renderNow,
|
|
22843
23020
|
demo: refreshDemo,
|
|
22844
|
-
|
|
23021
|
+
// Honesty backstop (same as /usage): a TOTAL probe failure (zero
|
|
23022
|
+
// result rows, nothing served from cache) renders an explicit
|
|
23023
|
+
// "probe failed" marker instead of a false "Live" footer next to
|
|
23024
|
+
// no-data rows.
|
|
23025
|
+
...(staleCachedAtMs != null
|
|
23026
|
+
? { staleCachedAtMs }
|
|
23027
|
+
: probeResp.results.length > 0
|
|
23028
|
+
? { liveProbedAtMs: renderNow.getTime() }
|
|
23029
|
+
: { probeFailed: true }),
|
|
22845
23030
|
})
|
|
22846
23031
|
const kbRows = buildSnapshotKeyboard(snapshots, { now: renderNow, demo: refreshDemo })
|
|
22847
23032
|
const inline_keyboard = kbRows.map((row) =>
|
|
@@ -23307,21 +23492,49 @@ bot.command('usage', async ctx => {
|
|
|
23307
23492
|
// flight; a TTL-hit or failed-probe fallback is tagged served:"cache",
|
|
23308
23493
|
// which we surface as a "⚠ cached Nm ago" footer instead of a false
|
|
23309
23494
|
// live stamp.
|
|
23495
|
+
const renderNow = new Date()
|
|
23310
23496
|
const probeResp = await client.probeQuota(state.accounts.map((a) => a.label)).catch(() => ({ results: [] }))
|
|
23311
23497
|
const { quotas, staleCachedAtMs } = zipProbeResults(
|
|
23312
23498
|
state.accounts.map((a) => a.label),
|
|
23313
23499
|
probeResp.results,
|
|
23314
23500
|
)
|
|
23315
|
-
const {
|
|
23501
|
+
const { buildSnapshotsFromState, buildSnapshotKeyboard } = await import(
|
|
23316
23502
|
'../auth-snapshot-format.js'
|
|
23317
23503
|
)
|
|
23318
|
-
const
|
|
23504
|
+
const { renderUsageCard } = await import('../quota-bar-format.js')
|
|
23319
23505
|
const snapshots = buildSnapshotsFromState(state, quotas)
|
|
23320
|
-
|
|
23321
|
-
|
|
23322
|
-
|
|
23506
|
+
// /usage renders the compact quota-bar block (at-a-glance headroom +
|
|
23507
|
+
// pace tick per window) plus a two-line footer — the actionable
|
|
23508
|
+
// cross-account recommendation verdict and the freshness marker (`⚠
|
|
23509
|
+
// cached Nm ago` when the probe was served stale from cache, else
|
|
23510
|
+
// `Live · refreshed Nm ago`). The old Format 2 table
|
|
23511
|
+
// (renderAuthSnapshotFormat2) is no longer appended here (operator
|
|
23512
|
+
// call, 2026-07-10): the two views were redundant and the table
|
|
23513
|
+
// doubled the message length. Every per-window reset the table
|
|
23514
|
+
// carried has an equivalent in the bar rows' `pct% / <time-left>`
|
|
23515
|
+
// segment, just relative instead of absolute — no info lost; the
|
|
23516
|
+
// recommendation + cached/live footer the table used to carry are
|
|
23517
|
+
// preserved by renderUsageCard.
|
|
23518
|
+
const exhaustedByLabel = new Map<string, boolean>(
|
|
23519
|
+
state.accounts.map((a) => [a.label, a.exhausted]),
|
|
23520
|
+
)
|
|
23521
|
+
const text = renderUsageCard(snapshots, exhaustedByLabel, {
|
|
23522
|
+
now: renderNow,
|
|
23323
23523
|
demo,
|
|
23324
|
-
|
|
23524
|
+
// #2495 Change 2 — a TTL-hit / failed-probe fallback is tagged
|
|
23525
|
+
// served:"cache"; surface it as `⚠ cached Nm ago` instead of a
|
|
23526
|
+
// false live stamp. Otherwise stamp the live refresh time.
|
|
23527
|
+
// Honesty backstop: a TOTAL probe failure (the .catch above
|
|
23528
|
+
// returned `{results: []}` and nothing was served from cache)
|
|
23529
|
+
// must render an explicit "probe failed" marker, NOT a false
|
|
23530
|
+
// "Live" stamp next to "⚠️ no data" rows. Without this the
|
|
23531
|
+
// footer claimed "Live · refreshed 0s ago" while every account
|
|
23532
|
+
// row said "no data — probe failed" (#2959 review finding).
|
|
23533
|
+
...(staleCachedAtMs != null
|
|
23534
|
+
? { staleCachedAtMs }
|
|
23535
|
+
: probeResp.results.length > 0
|
|
23536
|
+
? { liveProbedAtMs: renderNow.getTime() }
|
|
23537
|
+
: { probeFailed: true }),
|
|
23325
23538
|
})
|
|
23326
23539
|
// Preserve the Switch/Refresh/usage/Add inline keyboard on the
|
|
23327
23540
|
// rich-message render — the table card carries the same actions the
|
|
@@ -26380,6 +26593,32 @@ void (async () => {
|
|
|
26380
26593
|
|
|
26381
26594
|
if (!didOneTimeSetup) {
|
|
26382
26595
|
didOneTimeSetup = true
|
|
26596
|
+
|
|
26597
|
+
// person_id name resolution (docs/configuration.md, resolve-person.ts):
|
|
26598
|
+
// ONE-TIME boot validation, NOT periodic. `PERSON_DIRECTORY` is
|
|
26599
|
+
// built exactly once, here, from the static in-memory `people.json`
|
|
26600
|
+
// the scaffold projected from `switchroom.yaml`'s `users:` block —
|
|
26601
|
+
// a config change requires an agent restart, this never re-reads.
|
|
26602
|
+
// The orchestration (per-entry validation + dead-man's-switch) is
|
|
26603
|
+
// in the pure, unit-tested `runPersonDirectoryBootCheck` — this
|
|
26604
|
+
// block only performs the resulting I/O (stderr log + the EXISTING
|
|
26605
|
+
// fleet-alert path at low/config severity, kind: 'config-warning',
|
|
26606
|
+
// which must never page like a real outage).
|
|
26607
|
+
{
|
|
26608
|
+
const bootCheck = runPersonDirectoryBootCheck(readPeopleFile)
|
|
26609
|
+
PERSON_DIRECTORY = bootCheck.directory
|
|
26610
|
+
process.stderr.write(bootCheck.logLine + '\n')
|
|
26611
|
+
if (bootCheck.alertDetail != null) {
|
|
26612
|
+
emitGatewayOperatorEvent({
|
|
26613
|
+
kind: 'config-warning',
|
|
26614
|
+
agent: process.env.SWITCHROOM_AGENT_NAME ?? '-',
|
|
26615
|
+
detail: bootCheck.alertDetail,
|
|
26616
|
+
suggestedActions: [],
|
|
26617
|
+
firstSeenAt: new Date(),
|
|
26618
|
+
})
|
|
26619
|
+
}
|
|
26620
|
+
}
|
|
26621
|
+
|
|
26383
26622
|
void registerSwitchroomBotCommands().catch(() => {})
|
|
26384
26623
|
|
|
26385
26624
|
// #613 fix: pre-warm the chatAvailableReactions cache for every
|
|
@@ -27334,8 +27573,8 @@ void (async () => {
|
|
|
27334
27573
|
orphanStatusEnabled,
|
|
27335
27574
|
})
|
|
27336
27575
|
if (surface === 'worker-feed') {
|
|
27337
|
-
const
|
|
27338
|
-
const wkChat =
|
|
27576
|
+
const wk = resolveWorkerFeedChat(agentId, fleetChatId)
|
|
27577
|
+
const wkChat = wk.chatId
|
|
27339
27578
|
void workerActivityFeed?.update(
|
|
27340
27579
|
agentId,
|
|
27341
27580
|
wkChat,
|
|
@@ -27347,7 +27586,7 @@ void (async () => {
|
|
|
27347
27586
|
elapsedMs,
|
|
27348
27587
|
state: 'running',
|
|
27349
27588
|
},
|
|
27350
|
-
|
|
27589
|
+
wk.threadId,
|
|
27351
27590
|
)?.then(() => reconcileWorkerPin(agentId, wkChat, true))
|
|
27352
27591
|
return
|
|
27353
27592
|
}
|
|
@@ -27472,8 +27711,8 @@ void (async () => {
|
|
|
27472
27711
|
// resolved (the pinned-card fleet that used to carry the chat
|
|
27473
27712
|
// is gone — see resolveSubagentOriginChat).
|
|
27474
27713
|
if (workerFeedEnabled) {
|
|
27475
|
-
const
|
|
27476
|
-
const wkChat =
|
|
27714
|
+
const wk = resolveWorkerFeedChat(agentId, fleetChatId)
|
|
27715
|
+
const wkChat = wk.chatId
|
|
27477
27716
|
void workerActivityFeed?.update(
|
|
27478
27717
|
agentId,
|
|
27479
27718
|
wkChat,
|
|
@@ -27488,7 +27727,7 @@ void (async () => {
|
|
|
27488
27727
|
elapsedMs,
|
|
27489
27728
|
state: 'running',
|
|
27490
27729
|
},
|
|
27491
|
-
|
|
27730
|
+
wk.threadId,
|
|
27492
27731
|
)?.then(() => reconcileWorkerPin(agentId, wkChat, true))
|
|
27493
27732
|
return
|
|
27494
27733
|
}
|