switchroom 0.19.1 → 0.19.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/dist/agent-scheduler/index.js +31 -1
- package/dist/auth-broker/index.js +565 -48
- package/dist/cli/autoaccept-poll.js +31 -1
- package/dist/cli/drive-write-pretool.mjs +32 -2
- package/dist/cli/ms-365-write-pretool.mjs +32 -2
- package/dist/cli/switchroom.js +1148 -274
- package/dist/host-control/main.js +3 -3
- package/dist/vault/approvals/kernel-server.js +2 -2
- package/dist/vault/broker/server.js +2 -2
- package/package.json +3 -2
- package/profiles/_base/start.sh.hbs +1 -0
- package/profiles/default/CLAUDE.md.hbs +8 -0
- package/skills/mental-model-curator/SKILL.md +68 -2
- package/skills/switchroom-cli/SKILL.md +25 -0
- package/telegram-plugin/auth-snapshot-format.ts +143 -12
- package/telegram-plugin/dist/bridge/bridge.js +8 -2
- package/telegram-plugin/dist/gateway/gateway.js +1427 -689
- package/telegram-plugin/dist/server.js +8 -2
- package/telegram-plugin/external-spend.ts +135 -0
- package/telegram-plugin/flushed-turn-supersede.ts +117 -13
- package/telegram-plugin/gateway/auth-add-flow.ts +215 -6
- package/telegram-plugin/gateway/auth-command.ts +138 -5
- package/telegram-plugin/gateway/gateway.ts +141 -158
- package/telegram-plugin/gateway/inbound-interceptors.ts +13 -3
- package/telegram-plugin/gateway/model-command.ts +309 -1
- package/telegram-plugin/gateway/narrative-lane.ts +23 -9
- package/telegram-plugin/gateway/outbound-send-path.ts +68 -15
- package/telegram-plugin/gateway/session-model-source.ts +90 -10
- package/telegram-plugin/gateway/status-pin-store.ts +64 -4
- package/telegram-plugin/gateway/stream-render.ts +22 -5
- package/telegram-plugin/gateway/usage-mask.ts +29 -0
- package/telegram-plugin/hooks/subagent-tracker-pretool.mjs +19 -2
- package/telegram-plugin/quota-bar-format.ts +78 -12
- package/telegram-plugin/quota-check.ts +17 -2
- package/telegram-plugin/reply-owner-resolve.ts +76 -11
- package/telegram-plugin/session-tail.ts +27 -3
- package/telegram-plugin/tests/activity-card-wiring.test.ts +47 -0
- package/telegram-plugin/tests/auth-add-flow.test.ts +367 -5
- package/telegram-plugin/tests/auth-snapshot-format.test.ts +41 -0
- package/telegram-plugin/tests/external-spend.test.ts +168 -0
- package/telegram-plugin/tests/flushed-turn-supersede.test.ts +117 -0
- package/telegram-plugin/tests/gateway-session-model-relaunch.test.ts +219 -29
- package/telegram-plugin/tests/model-command.test.ts +220 -0
- package/telegram-plugin/tests/quota-bar-format.test.ts +43 -0
- package/telegram-plugin/tests/quota-check.test.ts +57 -0
- package/telegram-plugin/tests/reply-owner-resolve.test.ts +257 -13
- package/telegram-plugin/tests/send-reply-golden.test.ts +154 -0
- package/telegram-plugin/tests/session-model-source.test.ts +142 -0
- package/telegram-plugin/tests/session-tail-first-attach.test.ts +115 -2
- package/telegram-plugin/tests/status-pin-store.test.ts +198 -0
- package/telegram-plugin/tests/subagent-tracker-hooks.test.ts +50 -0
- package/telegram-plugin/tests/usage-footer-freshness.test.ts +141 -0
- package/telegram-plugin/tests/usage-mask.test.ts +35 -0
- package/telegram-plugin/tests/worker-feed-dispatch.test.ts +27 -0
- package/telegram-plugin/tests/worker-feed-pin-persistence.test.ts +131 -1
- package/vendor/hindsight-memory/CHANGELOG.md +102 -0
- package/vendor/hindsight-memory/README.md +2 -1
- package/vendor/hindsight-memory/hooks/hooks.json +12 -0
- package/vendor/hindsight-memory/scripts/directive_verify.py +100 -3
- package/vendor/hindsight-memory/scripts/lib/config.py +150 -1
- package/vendor/hindsight-memory/scripts/lib/content.py +55 -5
- package/vendor/hindsight-memory/scripts/lib/directives.py +152 -15
- package/vendor/hindsight-memory/scripts/lib/parallel_recall.py +142 -0
- package/vendor/hindsight-memory/scripts/lib/state.py +31 -0
- package/vendor/hindsight-memory/scripts/recall.py +789 -143
- package/vendor/hindsight-memory/scripts/reconcile_tail.py +22 -1
- package/vendor/hindsight-memory/scripts/retain.py +71 -2
- package/vendor/hindsight-memory/scripts/subagent_retain.py +501 -0
- package/vendor/hindsight-memory/scripts/tests/test_directive_verify.py +169 -0
- package/vendor/hindsight-memory/scripts/tests/test_directives.py +177 -0
- package/vendor/hindsight-memory/scripts/tests/test_lesson_tagging.py +200 -0
- package/vendor/hindsight-memory/scripts/tests/test_recall_context_turns_default.py +200 -0
- package/vendor/hindsight-memory/scripts/tests/test_recall_envelope_strip_telemetry.py +477 -0
- package/vendor/hindsight-memory/scripts/tests/test_recall_integration.py +51 -0
- package/vendor/hindsight-memory/scripts/tests/test_recall_parallel_deadline.py +409 -0
- package/vendor/hindsight-memory/scripts/tests/test_recall_tag_weights.py +96 -0
- package/vendor/hindsight-memory/scripts/tests/test_recall_transcript_fallback.py +413 -0
- package/vendor/hindsight-memory/scripts/tests/test_reconcile_durability.py +49 -0
- package/vendor/hindsight-memory/scripts/tests/test_subagent_retain.py +439 -0
- package/vendor/hindsight-memory/settings.json +3 -1
|
@@ -42,6 +42,7 @@ import {
|
|
|
42
42
|
resolveSafeBoundaryEnabled,
|
|
43
43
|
} from './interrupt-defer.js'
|
|
44
44
|
import { parseStopKeyword, buildStopReply } from './stop-command.js'
|
|
45
|
+
import { shouldMaskUsageLabels } from './usage-mask.js'
|
|
45
46
|
import { shouldPostBusyAck, formatBusyAckText, BUSY_ACK_STEP_AGE_THRESHOLD_MS } from './busy-ack.js'
|
|
46
47
|
import {
|
|
47
48
|
resolveStickerSendArgs,
|
|
@@ -365,10 +366,10 @@ import { createFleetFallbackResumeGate } from '../fleet-fallback-resume.js'
|
|
|
365
366
|
import { resolveExhaustUntil } from './exhaust-until.js'
|
|
366
367
|
import {
|
|
367
368
|
pendingAuthAddFlows,
|
|
368
|
-
startAccountAuthSession,
|
|
369
369
|
submitAccountAuthCode,
|
|
370
370
|
cancelAccountAuthSession,
|
|
371
371
|
cleanScratchDir as cleanAuthAddScratchDir,
|
|
372
|
+
handleAuthAddOrCancel,
|
|
372
373
|
} from './auth-add-flow.js'
|
|
373
374
|
import {
|
|
374
375
|
pendingLoopbackFlows,
|
|
@@ -482,6 +483,7 @@ import {
|
|
|
482
483
|
} from '../turn-flush-safety.js'
|
|
483
484
|
import {
|
|
484
485
|
resolveReplyOwnerTurnId,
|
|
486
|
+
type AnswerDeliveredLatch,
|
|
485
487
|
} from '../reply-owner-resolve.js'
|
|
486
488
|
// PR A — deterministic answer-ready quiescence flush (late-delivery fix).
|
|
487
489
|
import {
|
|
@@ -537,6 +539,11 @@ import {
|
|
|
537
539
|
modelCommandReceiptLine,
|
|
538
540
|
handleModelCommand,
|
|
539
541
|
classifyModelSwitchConfirmation,
|
|
542
|
+
formatModelRelaunchDiagLog,
|
|
543
|
+
servedModelMatchesRequested,
|
|
544
|
+
buildServedModelDivergenceHandler,
|
|
545
|
+
deliverModelSwitchBootNotice,
|
|
546
|
+
type ModelBootCardDeps,
|
|
540
547
|
buildModelMenu,
|
|
541
548
|
handleModelMenuCallback,
|
|
542
549
|
isValidModelArg,
|
|
@@ -651,6 +658,7 @@ import {
|
|
|
651
658
|
mutateStatusPinRow,
|
|
652
659
|
reconcileAndPersistStatusPin,
|
|
653
660
|
runStatusPinBootCleanup,
|
|
661
|
+
withPinReconcileLock,
|
|
654
662
|
type PersistedStatusPin,
|
|
655
663
|
type StatusPinPersistOp,
|
|
656
664
|
} from './status-pin-store.js'
|
|
@@ -877,7 +885,7 @@ import {
|
|
|
877
885
|
decideAnnouncementDelivery,
|
|
878
886
|
foldAnnouncementIntoCard,
|
|
879
887
|
} from '../fallback-card-collapse.js'
|
|
880
|
-
import { buildSnapshotsFromState, buildSnapshotsFromCachedState, zipProbeResults } from '../auth-snapshot-format.js'
|
|
888
|
+
import { buildSnapshotsFromState, buildSnapshotsFromCachedState, zipProbeResults, deriveUsageFooterFreshness } from '../auth-snapshot-format.js'
|
|
881
889
|
import { maskUsername } from '../demo-mask.js'
|
|
882
890
|
import {
|
|
883
891
|
writeTurnActiveMarker,
|
|
@@ -3407,18 +3415,33 @@ export type CurrentTurn = {
|
|
|
3407
3415
|
// false ONLY at turn start, mirroring `activityEverOpened`'s sticky-true
|
|
3408
3416
|
// contract.
|
|
3409
3417
|
finalAnswerEverDelivered: boolean
|
|
3410
|
-
// 2026-07 double-reply-on-DM fix (Part 2 — race backstop)
|
|
3411
|
-
// SYNCHRONOUSLY at turn-flush FIRE time (before
|
|
3412
|
-
// before `flushedTurnSupersede.record`)
|
|
3413
|
-
//
|
|
3414
|
-
// substantive `reply` sends. It persists on the ended turn in
|
|
3418
|
+
// 2026-07 double-reply-on-DM fix (Part 2 — race backstop), SOURCE-TAGGED
|
|
3419
|
+
// since #3426. Set to 'flush' SYNCHRONOUSLY at turn-flush FIRE time (before
|
|
3420
|
+
// the ~500 ms async send and before `flushedTurnSupersede.record`) and at
|
|
3421
|
+
// supersede-record consumption (the resurrection window); set to 'reply'
|
|
3422
|
+
// when a substantive `reply` sends. It persists on the ended turn in
|
|
3415
3423
|
// `recentTurnsById`, so a LATE reply landing in the flush's post-fire
|
|
3416
3424
|
// pre-record race window (where `flushedTurnSupersede` finds no record to
|
|
3417
3425
|
// delete yet) resolves this turn via the unified owner resolver, sees the
|
|
3418
|
-
// latch already set, and suppresses itself — closing the residual
|
|
3419
|
-
// 1's supersede cannot reach.
|
|
3420
|
-
//
|
|
3421
|
-
|
|
3426
|
+
// 'flush' latch already set, and suppresses itself — closing the residual
|
|
3427
|
+
// window Part 1's supersede cannot reach. The 'reply' tag deliberately does
|
|
3428
|
+
// NOT suppress a late reply (#3426): a substantive interim ack followed by
|
|
3429
|
+
// an async sub-agent handback (which lands with NO live gateway turn and
|
|
3430
|
+
// resolves this ended turn as owner via the latest-ended tier) must deliver,
|
|
3431
|
+
// not silently drop. Scoped to the substantive floor so an interim sub-floor
|
|
3432
|
+
// ack NEITHER sets nor trips it. Reset false at turn start.
|
|
3433
|
+
answerDelivered: AnswerDeliveredLatch
|
|
3434
|
+
// #3429 — the text the turn-flush backstop delivered (or is mid-delivering)
|
|
3435
|
+
// as this turn's answer. Stamped SYNCHRONOUSLY alongside the 'flush' latch
|
|
3436
|
+
// arm (stream-render fire site; outbound-send-path supersede-consumption
|
|
3437
|
+
// resurrection site) and cleared wherever that latch is reset. Lets the
|
|
3438
|
+
// late-reply suppression discriminate BY CONTENT between the flushed answer
|
|
3439
|
+
// landing again (suppress — the flush race) and a genuinely new async
|
|
3440
|
+
// handback attributed to this flush-delivered ended turn (deliver fresh —
|
|
3441
|
+
// suppressing/editing it is the #3429 silent client-side drop), including in
|
|
3442
|
+
// the post-fire pre-record window where no supersede record exists yet.
|
|
3443
|
+
// Null when no flush armed this turn. Reset null at turn start.
|
|
3444
|
+
flushedAnswerText: string | null
|
|
3422
3445
|
// 2026-07 double-reply-on-DM fix (F2 — recency bound). Wall-clock ms the turn
|
|
3423
3446
|
// ENDED (stamped once by `endCurrentTurnAtomic`), or null while still live.
|
|
3424
3447
|
// The `findLatestEndedTurnForChat` supersede tier carries DESTRUCTIVE
|
|
@@ -3668,8 +3691,9 @@ const currentTurnMap = new CurrentTurnMap<CurrentTurn>()
|
|
|
3668
3691
|
// is seq-stamped and `resolve()` prefers the NEWER observation, so neither
|
|
3669
3692
|
// source can go stale behind the other (session-model-source.ts, pinned by
|
|
3670
3693
|
// tests/session-model-source.test.ts). buildAgentMetadata reads resolve();
|
|
3671
|
-
// the /model command paths write via setOverride.
|
|
3672
|
-
|
|
3694
|
+
// the /model command paths write via setOverride. The comparator arms the
|
|
3695
|
+
// #3427 requested-vs-served tripwire (handler registered at boot rehydration).
|
|
3696
|
+
const sessionModelSource = createSessionModelSource({ servedMatchesRequested: servedModelMatchesRequested })
|
|
3673
3697
|
// Captures the most-recently-started turn's sessionChatId. Unlike currentTurn,
|
|
3674
3698
|
// this is NOT cleared by the silence poke (firePoke/clearTurnStarted). It lets
|
|
3675
3699
|
// the Bug B fallback in executeReply route to the correct chat even when the
|
|
@@ -8440,6 +8464,10 @@ const PIN_STATUS_WHILE_WORKING = (() => {
|
|
|
8440
8464
|
// handlers unconditionally unpinning on every send) and runs NO polling
|
|
8441
8465
|
// watchdog / getChat().pinned_message reconciler.
|
|
8442
8466
|
const statusPinState = new Map<string, PinState>()
|
|
8467
|
+
// F2 serialization: same-pinKey reconciles run one-at-a-time via
|
|
8468
|
+
// `withPinReconcileLock` (status-pin-store.ts — kept there so it's
|
|
8469
|
+
// unit-testable) so each reads a fresh `prev`; see its doc for the stale-`prev`
|
|
8470
|
+
// race it closes. Adds zero Telegram API calls (a serialized noop still no-ops).
|
|
8443
8471
|
// Companion registry: pinKey → chatId, so the pre-restart sweep can unpin
|
|
8444
8472
|
// owned pins without threading the chat id through every call site. Written on
|
|
8445
8473
|
// every desired-pinned reconcile, cleared alongside the state on unpin.
|
|
@@ -9023,7 +9051,10 @@ async function reconcileStatusPin(
|
|
|
9023
9051
|
// absorbed. (The `pin_message` MCP tool still surfaces failures to the agent
|
|
9024
9052
|
// as a normal tool-error — that path is `executePinMessage`, not this one.)
|
|
9025
9053
|
try {
|
|
9026
|
-
|
|
9054
|
+
// Serialize per pinKey (F2): reconcileStatusPinInner reads `prev` from the
|
|
9055
|
+
// in-memory claim map at its top, so overlapping same-key reconciles must
|
|
9056
|
+
// run one-at-a-time or a stale `prev` clears the disk row under a live pin.
|
|
9057
|
+
await withPinReconcileLock(pinKey, () => reconcileStatusPinInner(pinKey, chatId, desired))
|
|
9027
9058
|
} catch (err) {
|
|
9028
9059
|
const msg = err instanceof Error ? err.message : String(err)
|
|
9029
9060
|
process.stderr.write(
|
|
@@ -9040,14 +9071,11 @@ async function reconcileStatusPinInner(
|
|
|
9040
9071
|
): Promise<void> {
|
|
9041
9072
|
if (!PIN_STATUS_WHILE_WORKING) return
|
|
9042
9073
|
if (chatId.length === 0) return
|
|
9043
|
-
//
|
|
9044
|
-
//
|
|
9045
|
-
//
|
|
9046
|
-
//
|
|
9047
|
-
//
|
|
9048
|
-
// Telegram, and the first reconcile to set the claim makes every subsequent
|
|
9049
|
-
// edit a no-op — it converges in one round, never a storm. A per-key mutex
|
|
9050
|
-
// would remove the duplicate pin but adds lock complexity for zero UX gain.
|
|
9074
|
+
// Serialized per pinKey by `withPinReconcileLock` at the caller (F2), so this
|
|
9075
|
+
// `prev` snapshot is taken only after any prior same-key reconcile fully
|
|
9076
|
+
// settled — always the true current claim. Closes the stale-`prev` race (a
|
|
9077
|
+
// turn-end clear dropping the disk row under a flood-delayed open-pin) and the
|
|
9078
|
+
// older duplicate-pin concern (two edits both reading prev=null).
|
|
9051
9079
|
const prev = statusPinState.get(pinKey) ?? null
|
|
9052
9080
|
|
|
9053
9081
|
const runReconcile = () =>
|
|
@@ -14333,6 +14361,26 @@ function isAuthorizedSender(ctx: Context): boolean {
|
|
|
14333
14361
|
return false
|
|
14334
14362
|
}
|
|
14335
14363
|
|
|
14364
|
+
// Adversarial-review F4 — a group configured with an EMPTY `allowFrom`
|
|
14365
|
+
// authorizes every member (isAuthorizedSender returns true for any sender in
|
|
14366
|
+
// that group). That's an intentional "whole-group" access mode, but it means
|
|
14367
|
+
// `/usage` would expose per-account email labels + quota headroom to every
|
|
14368
|
+
// member of a broadly-shared group. Harden minimally: for the quota-bearing
|
|
14369
|
+
// card in a non-private chat, mask account labels (reusing the demo-mask
|
|
14370
|
+
// machinery) UNLESS the group pinned a non-empty `allowFrom` — i.e. an
|
|
14371
|
+
// explicit operator-curated member list is treated as trusted enough to see
|
|
14372
|
+
// the real labels. Private (operator DM) chats are never masked. This changes
|
|
14373
|
+
// only what /usage REVEALS, not who may run it. The pure decision lives in
|
|
14374
|
+
// ./usage-mask.ts (shouldMaskUsageLabels) so it is unit-testable without
|
|
14375
|
+
// importing the whole gateway module.
|
|
14376
|
+
function shouldMaskAccountLabels(ctx: Context): boolean {
|
|
14377
|
+
const groupAllowFrom =
|
|
14378
|
+
ctx.chat?.type === 'group' || ctx.chat?.type === 'supergroup'
|
|
14379
|
+
? loadAccess().groups[String(ctx.chat.id)]?.allowFrom
|
|
14380
|
+
: undefined
|
|
14381
|
+
return shouldMaskUsageLabels(ctx.chat?.type, groupAllowFrom)
|
|
14382
|
+
}
|
|
14383
|
+
|
|
14336
14384
|
// safeName moved to ./media-message-handlers.ts (switchroom#2996 P6 cluster A);
|
|
14337
14385
|
// imported above and shared with the attachment handlers still inline here.
|
|
14338
14386
|
|
|
@@ -20830,68 +20878,17 @@ bot.command("auth", async ctx => {
|
|
|
20830
20878
|
// handleAuthCommand which only needs the narrow broker surface.
|
|
20831
20879
|
const chatId = String(ctx.chat?.id ?? '')
|
|
20832
20880
|
if (parsed.kind === 'add' || parsed.kind === 'cancel') {
|
|
20833
|
-
|
|
20834
|
-
|
|
20835
|
-
|
|
20836
|
-
|
|
20837
|
-
|
|
20838
|
-
|
|
20839
|
-
|
|
20840
|
-
|
|
20841
|
-
)
|
|
20842
|
-
|
|
20843
|
-
}
|
|
20844
|
-
// PR3 supergroup-mode: key auth-add flows by (chat, thread) so
|
|
20845
|
-
// separate flows in two topics of one supergroup can't collide.
|
|
20846
|
-
// In DM chats message_thread_id is undefined → key collapses to
|
|
20847
|
-
// `chatId:_`, identical to today's behavior.
|
|
20848
|
-
const authAddKey = chatKey(chatId, ctx.message?.message_thread_id ?? null) as string
|
|
20849
|
-
if (parsed.kind === 'cancel') {
|
|
20850
|
-
const existing = pendingAuthAddFlows.get(authAddKey)
|
|
20851
|
-
if (!existing) {
|
|
20852
|
-
await switchroomReply(ctx, "_No pending \`/auth add\` flow in this chat._", { html: true })
|
|
20853
|
-
return
|
|
20854
|
-
}
|
|
20855
|
-
cancelAccountAuthSession(existing)
|
|
20856
|
-
pendingAuthAddFlows.delete(authAddKey)
|
|
20857
|
-
await switchroomReply(ctx, "Cancelled.", { html: true })
|
|
20858
|
-
return
|
|
20859
|
-
}
|
|
20860
|
-
// parsed.kind === 'add'
|
|
20861
|
-
if (pendingAuthAddFlows.has(authAddKey)) {
|
|
20862
|
-
await switchroomReply(
|
|
20863
|
-
ctx,
|
|
20864
|
-
"_An \`/auth add\` flow is already in progress for this chat. " +
|
|
20865
|
-
"Finish the paste, or send \`/auth cancel\` to abort._",
|
|
20866
|
-
{ html: true },
|
|
20867
|
-
)
|
|
20868
|
-
return
|
|
20869
|
-
}
|
|
20870
|
-
try {
|
|
20871
|
-
const { loginUrl, scratchDir, tmuxSocket, tmuxSession } = await startAccountAuthSession(parsed.label)
|
|
20872
|
-
pendingAuthAddFlows.set(authAddKey, {
|
|
20873
|
-
label: parsed.label,
|
|
20874
|
-
scratchDir,
|
|
20875
|
-
tmuxSocket,
|
|
20876
|
-
tmuxSession,
|
|
20877
|
-
startedAt: Date.now(),
|
|
20878
|
-
})
|
|
20879
|
-
await switchroomReply(
|
|
20880
|
-
ctx,
|
|
20881
|
-
`**Adding account** \`${parsed.label}\`\n\n` +
|
|
20882
|
-
`1. Open this URL on your phone:\n${loginUrl}\n\n` +
|
|
20883
|
-
`2. Log into Anthropic, copy the code Claude shows.\n` +
|
|
20884
|
-
`3. Paste it back here.\n\n` +
|
|
20885
|
-
`Send \`/auth cancel\` to abort.`,
|
|
20886
|
-
{ html: true },
|
|
20887
|
-
)
|
|
20888
|
-
} catch (err) {
|
|
20889
|
-
await switchroomReply(
|
|
20890
|
-
ctx,
|
|
20891
|
-
`**/auth add failed:** ${escapeHtmlForTg((err as Error)?.message ?? String(err))}`,
|
|
20892
|
-
{ html: true },
|
|
20893
|
-
)
|
|
20894
|
-
}
|
|
20881
|
+
// Gateway-routed `/auth add|readd|cancel` — extracted to
|
|
20882
|
+
// handleAuthAddOrCancel in auth-add-flow.ts (switchroom#2996 ratchet).
|
|
20883
|
+
await handleAuthAddOrCancel({
|
|
20884
|
+
parsed,
|
|
20885
|
+
isAdmin,
|
|
20886
|
+
currentAgent,
|
|
20887
|
+
chatId,
|
|
20888
|
+
threadId: ctx.message?.message_thread_id ?? null,
|
|
20889
|
+
reply: (text: string) => switchroomReply(ctx, text, { html: true }),
|
|
20890
|
+
escapeHtml: escapeHtmlForTg,
|
|
20891
|
+
})
|
|
20895
20892
|
return
|
|
20896
20893
|
}
|
|
20897
20894
|
|
|
@@ -21444,7 +21441,12 @@ bot.command('issues', async ctx => {
|
|
|
21444
21441
|
})
|
|
21445
21442
|
bot.command('usage', async ctx => {
|
|
21446
21443
|
if (!isAuthorizedSender(ctx)) return
|
|
21447
|
-
|
|
21444
|
+
// `demo` is the explicit `/usage demo` opt-in mask. F4 additionally masks
|
|
21445
|
+
// account labels for a non-private chat whose group has no pinned
|
|
21446
|
+
// `allowFrom` (open-membership group) — see shouldMaskAccountLabels. The
|
|
21447
|
+
// effective mask feeds the label-rendering paths (renderUsageCard,
|
|
21448
|
+
// buildSnapshotKeyboard) exactly as `demo` did.
|
|
21449
|
+
const demo = hasDemoFlag(getCommandArgs(ctx)) || shouldMaskAccountLabels(ctx)
|
|
21448
21450
|
// Format 2 path: enumerate every account in the broker's known set,
|
|
21449
21451
|
// probe live quota in parallel, render the health-grouped snapshot.
|
|
21450
21452
|
// Falls back to the legacy single-agent shape when the broker is
|
|
@@ -21484,26 +21486,34 @@ bot.command('usage', async ctx => {
|
|
|
21484
21486
|
// segment, just relative instead of absolute — no info lost; the
|
|
21485
21487
|
// recommendation + cached/live footer the table used to carry are
|
|
21486
21488
|
// preserved by renderUsageCard.
|
|
21489
|
+
// External OpenRouter/$ block (layout B) — best-effort; omitted when
|
|
21490
|
+
// LiteLLM admin key is unavailable or the spend endpoint fails.
|
|
21491
|
+
const { fetchExternalSpendSummary } = await import('../external-spend.js')
|
|
21492
|
+
const externalSpend = await fetchExternalSpendSummary(renderNow).catch(() => null)
|
|
21487
21493
|
const exhaustedByLabel = new Map<string, boolean>(
|
|
21488
21494
|
state.accounts.map((a) => [a.label, a.exhausted]),
|
|
21489
21495
|
)
|
|
21490
21496
|
const text = renderUsageCard(snapshots, exhaustedByLabel, {
|
|
21491
21497
|
now: renderNow,
|
|
21492
21498
|
demo,
|
|
21499
|
+
externalSpend,
|
|
21493
21500
|
// #2495 Change 2 — a TTL-hit / failed-probe fallback is tagged
|
|
21494
21501
|
// served:"cache"; surface it as `⚠ cached Nm ago` instead of a
|
|
21495
21502
|
// false live stamp. Otherwise stamp the live refresh time.
|
|
21496
21503
|
// Honesty backstop: a TOTAL probe failure (the .catch above
|
|
21497
21504
|
// returned `{results: []}` and nothing was served from cache)
|
|
21498
21505
|
// must render an explicit "probe failed" marker, NOT a false
|
|
21499
|
-
// "Live" stamp next to "⚠️ no data" rows
|
|
21500
|
-
//
|
|
21501
|
-
//
|
|
21502
|
-
|
|
21503
|
-
|
|
21504
|
-
|
|
21505
|
-
|
|
21506
|
-
|
|
21506
|
+
// "Live" stamp next to "⚠️ no data" rows (#2959 review finding).
|
|
21507
|
+
// Honesty invariant (adversarial-review F1): the live stamp is
|
|
21508
|
+
// derived from whether ANY row carries usable data, NOT from the
|
|
21509
|
+
// array length — a failed live probe against an empty cache returns
|
|
21510
|
+
// a non-empty results array of all-`ok:false` rows. Full rationale
|
|
21511
|
+
// in deriveUsageFooterFreshness (auth-snapshot-format.ts).
|
|
21512
|
+
...deriveUsageFooterFreshness(
|
|
21513
|
+
probeResp.results,
|
|
21514
|
+
staleCachedAtMs,
|
|
21515
|
+
renderNow.getTime(),
|
|
21516
|
+
),
|
|
21507
21517
|
})
|
|
21508
21518
|
// Preserve the Switch/Refresh/usage/Add inline keyboard on the
|
|
21509
21519
|
// rich-message render — the table card carries the same actions the
|
|
@@ -23867,75 +23877,48 @@ async function startGateway(): Promise<void> { // #2996 P0c: the boot IIFE, now
|
|
|
23867
23877
|
// real post-boot signal (`.active-session-model`), never from a
|
|
23868
23878
|
// scraped pane or an optimistic record.
|
|
23869
23879
|
const isApplyBoot = launched.length > 0 && launched !== configured
|
|
23870
|
-
|
|
23871
|
-
//
|
|
23872
|
-
//
|
|
23873
|
-
|
|
23874
|
-
//
|
|
23875
|
-
//
|
|
23876
|
-
//
|
|
23877
|
-
|
|
23878
|
-
|
|
23879
|
-
|
|
23880
|
-
|
|
23881
|
-
|
|
23882
|
-
|
|
23880
|
+
// { verify: true } (#3427 item 4 / H1): ONLY this site arms the
|
|
23881
|
+
// requested-vs-served tripwire — `launched` IS the token of the
|
|
23882
|
+
// session now serving. Command-time setOverride never arms.
|
|
23883
|
+
sessionModelSource.setOverride(isApplyBoot ? launched : null, { verify: true })
|
|
23884
|
+
// Boot /model cards (#3427): the divergence tripwire warn and
|
|
23885
|
+
// the switch confirmation share one deps surface; the card
|
|
23886
|
+
// logic lives in model-command.ts (#2996 ratchet).
|
|
23887
|
+
const modelBootCardDeps: ModelBootCardDeps = {
|
|
23888
|
+
agent: getMyAgentName(),
|
|
23889
|
+
chat: modelSwitchMarkerChat,
|
|
23890
|
+
log: (line) => process.stderr.write(line),
|
|
23891
|
+
// allow-raw-bot-api: one-shot boot /model cards (divergence warn + switch confirmation), same shape as the session-model alert relay below
|
|
23892
|
+
sendCard: (chatId, body, opts) => lockedBot.api.sendMessage(chatId, body, opts),
|
|
23893
|
+
}
|
|
23894
|
+
if (isApplyBoot) {
|
|
23895
|
+
sessionModelSource.setDivergenceHandler(buildServedModelDivergenceHandler(modelBootCardDeps))
|
|
23896
|
+
}
|
|
23897
|
+
// F1/N4: classify + log + one confirmation card. Formatters live
|
|
23898
|
+
// in model-command.ts so this file does not inflate (#2996 ratchet).
|
|
23899
|
+
const confirmation = modelSwitchReason != null
|
|
23900
|
+
? classifyModelSwitchConfirmation({
|
|
23901
|
+
reason: modelSwitchReason,
|
|
23902
|
+
launched,
|
|
23903
|
+
configured,
|
|
23904
|
+
})
|
|
23905
|
+
: null
|
|
23883
23906
|
process.stderr.write(
|
|
23884
|
-
|
|
23885
|
-
|
|
23886
|
-
// Switch-confirmation (F1 / PLAN §4 step 2): on a /model apply-boot
|
|
23887
|
-
// with a known initiating chat, send ONE confirmation built from the
|
|
23888
|
-
// ACTUAL launched model — never optimistic. Keyed on the DETERMINISTIC
|
|
23889
|
-
// /model switch reason (from the clean-shutdown marker), not on
|
|
23890
|
-
// `launched !== configured`, so it ALSO fires when a switch landed on
|
|
23891
|
-
// the configured default (`/model default`, or `/model <configured>`)
|
|
23892
|
-
// — N4. The generic boot card is suppressed for this boot (N3), so
|
|
23893
|
-
// this is the single card the operator sees for the switch.
|
|
23894
|
-
if (modelSwitchReason != null && modelSwitchMarkerChat) {
|
|
23895
|
-
const chat = modelSwitchMarkerChat
|
|
23896
|
-
// Derive the confirmation from the DETERMINISTIC post-boot
|
|
23897
|
-
// signals. A non-default switch that reverted to the configured
|
|
23898
|
-
// default (a wedged/consumed apply-boot — the silent-revert bug)
|
|
23899
|
-
// must WARN, not print a misleading green "✅ Now running
|
|
23900
|
-
// <default>" card. `applied` / `default` keep the honest green
|
|
23901
|
-
// card (N4: the default/revert case still confirms).
|
|
23902
|
-
const confirmation = classifyModelSwitchConfirmation({
|
|
23903
|
-
reason: modelSwitchReason,
|
|
23907
|
+
formatModelRelaunchDiagLog({
|
|
23908
|
+
agent: getMyAgentName(),
|
|
23904
23909
|
launched,
|
|
23905
23910
|
configured,
|
|
23911
|
+
confirmation,
|
|
23912
|
+
isApplyBoot,
|
|
23913
|
+
}),
|
|
23914
|
+
)
|
|
23915
|
+
if (confirmation != null) {
|
|
23916
|
+
// #3427 item 2: card-vs-suppress decision is pure + behaviorally tested.
|
|
23917
|
+
deliverModelSwitchBootNotice({
|
|
23918
|
+
...modelBootCardDeps,
|
|
23919
|
+
confirmation,
|
|
23920
|
+
hasSessionModelAlert: existsSync(join(smAgentDir, '.session-model-alert')),
|
|
23906
23921
|
})
|
|
23907
|
-
// LOW-2 dedup: the config-default-changed / proxy-down revert
|
|
23908
|
-
// paths in start.sh write a TAILORED `.session-model-alert`
|
|
23909
|
-
// (relayed to operators below) that already explains why the
|
|
23910
|
-
// switch didn't apply and how to re-issue it. Suppress the
|
|
23911
|
-
// generic not-applied card when such an alert is present for
|
|
23912
|
-
// this boot so the operator isn't double-warned — the alert is
|
|
23913
|
-
// the more specific message. The not-applied card still fires
|
|
23914
|
-
// for the plain wedge/revert case (no alert on disk).
|
|
23915
|
-
const hasSessionModelAlert = existsSync(join(smAgentDir, '.session-model-alert'))
|
|
23916
|
-
if (confirmation.kind === 'not-applied' && hasSessionModelAlert) {
|
|
23917
|
-
process.stderr.write(
|
|
23918
|
-
`telegram gateway: gw /model relaunch applied — suppressing not-applied confirmation (a .session-model-alert is present and will be relayed) agent=${getMyAgentName()} target=${confirmation.target}\n`,
|
|
23919
|
-
)
|
|
23920
|
-
} else {
|
|
23921
|
-
const body =
|
|
23922
|
-
confirmation.kind === 'applied'
|
|
23923
|
-
? `✅ Now running \`${confirmation.launched}\` — session-only, reverts to the configured model on the next restart. Fresh session; memory and the handoff briefing carry the context.`
|
|
23924
|
-
: confirmation.kind === 'not-applied'
|
|
23925
|
-
? `⚠️ Your switch to \`${confirmation.target}\` didn't apply — the agent reverted to \`${confirmation.revertedTo}\` (the apply-boot didn't complete). Re-issue \`/model ${confirmation.target}\` to try again.`
|
|
23926
|
-
: `✅ Now running \`${confirmation.launched}\` (the configured default) — fresh session; memory and the handoff briefing carry the context.`
|
|
23927
|
-
// allow-raw-bot-api: one-shot boot confirmation, same shape as the session-model alert relay below
|
|
23928
|
-
void lockedBot.api
|
|
23929
|
-
.sendMessage(chat.chatId, body, {
|
|
23930
|
-
parse_mode: 'Markdown',
|
|
23931
|
-
...(chat.threadId != null ? { message_thread_id: chat.threadId } : {}),
|
|
23932
|
-
})
|
|
23933
|
-
.catch((err: unknown) =>
|
|
23934
|
-
process.stderr.write(
|
|
23935
|
-
`telegram gateway: model-switch confirmation send failed: ${(err as Error)?.message ?? String(err)}\n`,
|
|
23936
|
-
),
|
|
23937
|
-
)
|
|
23938
|
-
}
|
|
23939
23922
|
}
|
|
23940
23923
|
} catch { /* leave override as-is on a bad read */ }
|
|
23941
23924
|
}
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
import type { Context } from 'grammy'
|
|
19
19
|
import type { ReactionTypeEmoji } from 'grammy/types'
|
|
20
20
|
import { parseStopKeyword, buildStopReply } from './stop-command.js'
|
|
21
|
+
import { formatGrantedScopesReply } from './auth-command.js'
|
|
21
22
|
import { decideInterruptTiming, resolveSafeBoundaryEnabled } from './interrupt-defer.js'
|
|
22
23
|
import { naturalAction } from '../permission-title.js'
|
|
23
24
|
import { richMessage } from '../rich-send.js'
|
|
@@ -463,15 +464,24 @@ export async function interceptAuthAdd(
|
|
|
463
464
|
deps.pendingAuthAddFlows.delete(p.interceptKey)
|
|
464
465
|
try {
|
|
465
466
|
const credentials = await deps.submitAccountAuthCode(pendingAdd, p.text.trim())
|
|
467
|
+
const replace = pendingAdd.replace === true
|
|
466
468
|
try {
|
|
467
|
-
await deps.addAccountViaBroker(pendingAdd.label, credentials, { replace
|
|
469
|
+
await deps.addAccountViaBroker(pendingAdd.label, credentials, { replace })
|
|
468
470
|
// success — wipe scratch dir now that the broker owns the creds
|
|
469
471
|
deps.cleanAuthAddScratchDir(pendingAdd.scratchDir)
|
|
472
|
+
// Read the minted scopes STRUCTURALLY (never scraped) and surface
|
|
473
|
+
// them so a scope regression (missing user:profile) is caught at add
|
|
474
|
+
// time, not at /usage time.
|
|
475
|
+
const scopeReply = formatGrantedScopesReply(
|
|
476
|
+
(credentials as { claudeAiOauth?: { scopes?: string[] } }).claudeAiOauth?.scopes,
|
|
477
|
+
)
|
|
478
|
+
const verb = replace ? 're-authenticated' : 'added'
|
|
470
479
|
await deps.switchroomReply(
|
|
471
480
|
p.ctx,
|
|
472
|
-
`✓ Account \`${pendingAdd.label}\`
|
|
481
|
+
`✓ Account \`${pendingAdd.label}\` ${verb}.\n` +
|
|
473
482
|
`The fleet's active account hasn't changed. Send ` +
|
|
474
|
-
`\`/auth use ${deps.escapeHtmlForTg(pendingAdd.label)}\` to switch to it
|
|
483
|
+
`\`/auth use ${deps.escapeHtmlForTg(pendingAdd.label)}\` to switch to it.` +
|
|
484
|
+
scopeReply.text,
|
|
475
485
|
{ html: true },
|
|
476
486
|
)
|
|
477
487
|
} catch (brokerErr) {
|