switchroom 0.18.31 → 0.18.33
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 +21 -3
- package/dist/cli/notion-write-pretool.mjs +4 -2
- package/dist/cli/switchroom.js +1410 -852
- package/dist/host-control/main.js +22 -4
- package/dist/vault/approvals/kernel-server.js +21 -3
- package/dist/vault/broker/server.js +48 -4
- package/package.json +4 -3
- package/profiles/_base/start.sh.hbs +148 -23
- package/telegram-plugin/dist/gateway/gateway.js +62726 -58282
- package/telegram-plugin/gateway/agent-button-callback-handler.ts +237 -0
- package/telegram-plugin/gateway/ask-callback-handler.ts +92 -0
- package/telegram-plugin/gateway/attachment-message-handlers.ts +152 -0
- package/telegram-plugin/gateway/backstop-delivery.ts +223 -23
- package/telegram-plugin/gateway/boot-card.ts +169 -1
- package/telegram-plugin/gateway/bot-commands-model-effort.ts +209 -0
- package/telegram-plugin/gateway/bot-commands-start-info.ts +108 -0
- package/telegram-plugin/gateway/callback-query-handlers.ts +124 -0
- package/telegram-plugin/gateway/captured-answer-resume.ts +259 -0
- package/telegram-plugin/gateway/card-approval-keyboards.test.ts +28 -0
- package/telegram-plugin/gateway/card-tool-handlers.ts +639 -0
- package/telegram-plugin/gateway/checklist-message-handler.ts +107 -0
- package/telegram-plugin/gateway/delivery-confirm-wiring.ts +133 -0
- package/telegram-plugin/gateway/disconnect-flush.ts +6 -44
- package/telegram-plugin/gateway/gateway-import-clean.test.ts +188 -0
- package/telegram-plugin/gateway/gateway.ts +6403 -13456
- package/telegram-plugin/gateway/inbound-delivery-machine-dispatch.ts +7 -15
- package/telegram-plugin/gateway/inbound-delivery-machine-shadow.ts +35 -68
- package/telegram-plugin/gateway/inbound-interceptors.ts +1133 -0
- package/telegram-plugin/gateway/inbound-router.ts +400 -0
- package/telegram-plugin/gateway/liveness-wiring.ts +440 -0
- package/telegram-plugin/gateway/media-message-handlers.ts +256 -0
- package/telegram-plugin/gateway/mental-model-propose-card.ts +16 -0
- package/telegram-plugin/gateway/model-command.ts +23 -0
- package/telegram-plugin/gateway/narrative-lane.ts +865 -0
- package/telegram-plugin/gateway/obligation-ledger.ts +42 -0
- package/telegram-plugin/gateway/obligation-store.ts +37 -1
- package/telegram-plugin/gateway/obligation-wiring.ts +333 -0
- package/telegram-plugin/gateway/outbound-send-path.ts +2012 -0
- package/telegram-plugin/gateway/photo-message-handler.ts +80 -0
- package/telegram-plugin/gateway/pinned-message-handler.ts +86 -0
- package/telegram-plugin/gateway/secret-request-card.test.ts +46 -0
- package/telegram-plugin/gateway/secret-request-card.ts +45 -0
- package/telegram-plugin/gateway/stream-render.ts +2166 -0
- package/telegram-plugin/gateway/turn-end.ts +606 -0
- package/telegram-plugin/gateway/turn-start-surfaces.ts +298 -0
- package/telegram-plugin/gateway/vault-request-access-card.ts +16 -0
- package/telegram-plugin/gateway/vault-request-save-card.test.ts +49 -0
- package/telegram-plugin/gateway/vault-request-save-card.ts +52 -0
- package/telegram-plugin/gateway/voice-message-handler.ts +123 -0
- package/telegram-plugin/gateway/voice-ondemand-callback-handler.ts +204 -0
- package/telegram-plugin/gateway/worker-feed-dispatch.ts +40 -0
- package/telegram-plugin/narrative-dedup.ts +24 -1
- package/telegram-plugin/narrative-flush.ts +2 -2
- package/telegram-plugin/pending-user-notice.ts +59 -13
- package/telegram-plugin/render/render.ts +25 -1
- package/telegram-plugin/status-no-truncate.ts +13 -0
- package/telegram-plugin/subagent-watcher.ts +297 -31
- package/telegram-plugin/tests/activity-card-wiring.test.ts +8 -3
- package/telegram-plugin/tests/activity-ever-opened-sticky.test.ts +18 -3
- package/telegram-plugin/tests/agent-button-callback-handler.test.ts +149 -0
- package/telegram-plugin/tests/ask-callback-handler.test.ts +118 -0
- package/telegram-plugin/tests/attachment-message-handlers.test.ts +135 -0
- package/telegram-plugin/tests/backstop-delivery.test.ts +167 -0
- package/telegram-plugin/tests/backstop-readback-probe.test.ts +144 -0
- package/telegram-plugin/tests/boot-card-routing.test.ts +139 -0
- package/telegram-plugin/tests/bot-commands-model-effort.test.ts +189 -0
- package/telegram-plugin/tests/bot-commands-start-info.test.ts +240 -0
- package/telegram-plugin/tests/buffer-gate-broadened.test.ts +28 -9
- package/telegram-plugin/tests/busy-ack-wiring.test.ts +6 -1
- package/telegram-plugin/tests/button-tap-turn-gated.test.ts +21 -12
- package/telegram-plugin/tests/callback-query-handlers.test.ts +101 -0
- package/telegram-plugin/tests/captured-answer-resume.test.ts +358 -0
- package/telegram-plugin/tests/card-tool-handlers.test.ts +497 -0
- package/telegram-plugin/tests/catch-all-unhandled-message.test.ts +5 -2
- package/telegram-plugin/tests/checklist-message-handler.test.ts +160 -0
- package/telegram-plugin/tests/emission-authority-facade.test.ts +76 -29
- package/telegram-plugin/tests/emission-authority-ping-gate.test.ts +4 -1
- package/telegram-plugin/tests/emission-determinism-wiring.test.ts +45 -16
- package/telegram-plugin/tests/feed-heartbeat-liveness-open.test.ts +30 -7
- package/telegram-plugin/tests/gateway-boot-side-effect-gating.test.ts +270 -0
- package/telegram-plugin/tests/gateway-boot-smoke.test.ts +150 -0
- package/telegram-plugin/tests/gateway-bot-construction-deferral.test.ts +251 -0
- package/telegram-plugin/tests/gateway-disconnect-flush.test.ts +5 -128
- package/telegram-plugin/tests/gateway-handler-registration-wiring.test.ts +299 -0
- package/telegram-plugin/tests/gateway-loopback-paste-redact.test.ts +44 -29
- package/telegram-plugin/tests/gateway-outbound-redact.test.ts +18 -5
- package/telegram-plugin/tests/gateway-request-secret.test.ts +7 -3
- package/telegram-plugin/tests/gateway-secret-detect.test.ts +20 -10
- package/telegram-plugin/tests/gateway-session-model-relaunch.test.ts +8 -2
- package/telegram-plugin/tests/inbound-delivery-cutover-flip.test.ts +54 -150
- package/telegram-plugin/tests/inbound-delivery-cutover-gate.test.ts +10 -14
- package/telegram-plugin/tests/inbound-delivery-dispatch-equivalence.test.ts +6 -7
- package/telegram-plugin/tests/inbound-delivery-machine-dispatch.test.ts +0 -16
- package/telegram-plugin/tests/inbound-emit-after-intercepts.test.ts +18 -7
- package/telegram-plugin/tests/inbound-message-types.test.ts +52 -16
- package/telegram-plugin/tests/litellm-proxy-auth-misconfig.test.ts +69 -14
- package/telegram-plugin/tests/media-message-handlers.test.ts +276 -0
- package/telegram-plugin/tests/mental-model-propose-callback-gate.test.ts +8 -4
- package/telegram-plugin/tests/model-command.test.ts +30 -0
- package/telegram-plugin/tests/multitopic-routing-wiring.test.ts +36 -12
- package/telegram-plugin/tests/narrative-dedup.test.ts +32 -0
- package/telegram-plugin/tests/narrative-flush.test.ts +6 -2
- package/telegram-plugin/tests/narrative-lane-golden.test.ts +458 -0
- package/telegram-plugin/tests/no-reply-bounded-drain.test.ts +14 -3
- package/telegram-plugin/tests/obligation-ledger.test.ts +40 -0
- package/telegram-plugin/tests/obligation-store.test.ts +43 -0
- package/telegram-plugin/tests/pending-card-durability-wiring.test.ts +18 -8
- package/telegram-plugin/tests/per-topic-current-turn.test.ts +32 -8
- package/telegram-plugin/tests/permission-no-repeat-wiring.test.ts +9 -6
- package/telegram-plugin/tests/photo-message-handler.test.ts +114 -0
- package/telegram-plugin/tests/photo-reroute-wiring.test.ts +5 -2
- package/telegram-plugin/tests/pinned-message-handler.test.ts +108 -0
- package/telegram-plugin/tests/render/render.test.ts +42 -0
- package/telegram-plugin/tests/reply-terminal-reaction.test.ts +6 -2
- package/telegram-plugin/tests/secret-detect-delete-must-surface-failures.test.ts +8 -4
- package/telegram-plugin/tests/secret-detect-fail-closed.test.ts +38 -28
- package/telegram-plugin/tests/secret-detect-oauth-code.test.ts +31 -20
- package/telegram-plugin/tests/send-reply-golden.test.ts +571 -0
- package/telegram-plugin/tests/silence-liveness-wiring.test.ts +22 -8
- package/telegram-plugin/tests/status-pin-service-message-suppression.test.ts +42 -49
- package/telegram-plugin/tests/stop-command.test.ts +22 -12
- package/telegram-plugin/tests/stream-render-golden.test.ts +424 -0
- package/telegram-plugin/tests/subagent-watcher-boot-skip-dead.test.ts +218 -0
- package/telegram-plugin/tests/subagent-watcher-resume-reregister.test.ts +305 -0
- package/telegram-plugin/tests/subagent-watcher-resurrection.test.ts +32 -0
- package/telegram-plugin/tests/subagent-watcher.test.ts +35 -3
- package/telegram-plugin/tests/turn-end-gate-backstop.test.ts +8 -12
- package/telegram-plugin/tests/turn-flush-safety.test.ts +191 -11
- package/telegram-plugin/tests/turn-flush-suppression-wiring.test.ts +117 -0
- package/telegram-plugin/tests/vault-approval-posture.test.ts +8 -2
- package/telegram-plugin/tests/vault-grant-inbound-builders.test.ts +2 -2
- package/telegram-plugin/tests/vault-grant-union.test.ts +4 -1
- package/telegram-plugin/tests/vault-key-regex-allows-slash.test.ts +16 -5
- package/telegram-plugin/tests/vault-request-access-tool.test.ts +10 -5
- package/telegram-plugin/tests/vault-request-access-unlock-resume.test.ts +4 -1
- package/telegram-plugin/tests/vault-subcommands.test.ts +6 -1
- package/telegram-plugin/tests/voice-message-handler.test.ts +111 -0
- package/telegram-plugin/tests/voice-ondemand-callback-handler.test.ts +140 -0
- package/telegram-plugin/tests/worker-activity-feed.test.ts +86 -19
- package/telegram-plugin/tests/worker-feed-coalesce.test.ts +236 -20
- package/telegram-plugin/tests/worker-feed-resume-guard.test.ts +86 -0
- package/telegram-plugin/tool-activity-summary.ts +110 -38
- package/telegram-plugin/turn-flush-safety.ts +80 -14
- package/telegram-plugin/uat/restart-capability.ts +76 -0
- package/telegram-plugin/uat/scenarios/bg-sub-agent-dispatch-dm.test.ts +14 -4
- package/telegram-plugin/uat/scenarios/bridge-flap-resilience-dm.test.ts +11 -1
- package/telegram-plugin/uat/scenarios/cross-turn-pending-progress-dm.test.ts +19 -2
- package/telegram-plugin/uat/scenarios/jtbd-always-on-after-restart-dm.test.ts +6 -12
- package/telegram-plugin/uat/scenarios/jtbd-deliberate-restart-resumes-dm.test.ts +6 -12
- package/telegram-plugin/uat/scenarios/jtbd-interrupted-turn-resumes-dm.test.ts +6 -12
- package/telegram-plugin/uat/scenarios/jtbd-multipart-render-dm.test.ts +47 -13
- package/telegram-plugin/worker-activity-feed.ts +34 -4
- package/telegram-plugin/gateway/busy-key-reaper.ts +0 -113
- package/telegram-plugin/gateway/gate-parity-probe.ts +0 -102
- package/telegram-plugin/tests/busy-key-reaper.test.ts +0 -192
- package/telegram-plugin/tests/fixtures/cutover-killswitch-probe.ts +0 -75
- package/telegram-plugin/tests/gate-parity-probe.test.ts +0 -171
- package/telegram-plugin/tests/parallel-turns-deadlock-fix.test.ts +0 -217
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* captured-answer-resume.ts — durable captured-answer resume for a partially
|
|
3
|
+
* delivered turn-flush backstop (#3282).
|
|
4
|
+
*
|
|
5
|
+
* ── The bug this closes ──────────────────────────────────────────────────────
|
|
6
|
+
* When the turn-flush backstop (`deliverAnswer` → `runBackstopDelivery`) lands
|
|
7
|
+
* chunks 0,1 of a 3-chunk answer but chunk 2 exhausts its bounded retries, the
|
|
8
|
+
* turn-flush IIFE historically (a) cleared the per-chunk `BackstopDeliveryLedger`
|
|
9
|
+
* unconditionally and (b) left the delivery obligation OPEN. The liveness floor
|
|
10
|
+
* then re-presented the obligation as a FRESH must-answer model turn, which
|
|
11
|
+
* REGENERATED the whole answer — re-posting chunks 0,1's content the user already
|
|
12
|
+
* received. Result: the user sees the head of the answer twice after a partial
|
|
13
|
+
* failure (or after a restart mid-delivery). #3282.
|
|
14
|
+
*
|
|
15
|
+
* ── The fix (design-of-record §2.2) ─────────────────────────────────────────
|
|
16
|
+
* Make the re-present a *byte-identical captured-answer resume* of ONLY the
|
|
17
|
+
* not-yet-`landed-confirmed` tail — never a regeneration — so a chunk that
|
|
18
|
+
* already reached the chat is provably never re-posted:
|
|
19
|
+
*
|
|
20
|
+
* 1. On a partial (`!delivered`) turn-flush, persist a {@link
|
|
21
|
+
* CapturedDeliverySnapshot} (the split chunks + each landed chunk's ids and
|
|
22
|
+
* confirmed flag) onto the durable obligation record, keyed by
|
|
23
|
+
* `originTurnId`. This rides the obligation ledger's existing atomic
|
|
24
|
+
* snapshot, so it survives a gateway/container restart.
|
|
25
|
+
* 2. The obligation sweep's `represent` branch becomes source-aware: when the
|
|
26
|
+
* obligation carries a captured-delivery snapshot it drives THIS resume
|
|
27
|
+
* (re-run `runBackstopDelivery` for the SAME `turnId` and the SAME captured
|
|
28
|
+
* chunks, resuming at the first non-`landed-confirmed` index) instead of
|
|
29
|
+
* pushing a fresh-generation inbound. No snapshot ⇒ the genuine "model wrote
|
|
30
|
+
* nothing / never fired a backstop" case falls through to fresh generation
|
|
31
|
+
* unchanged.
|
|
32
|
+
* 3. The in-memory ledger is rehydrated from the snapshot AND reconciled against
|
|
33
|
+
* the durable outbound-text oracle (`hasOutboundWithText`), so a chunk that
|
|
34
|
+
* landed a durable history row is confirmed even if the snapshot's flag was
|
|
35
|
+
* not yet flushed when the process died (crash-idempotency, design §2.3/A5).
|
|
36
|
+
*
|
|
37
|
+
* Chunk identity is the chunk INDEX within `turnId`, stable across the represent
|
|
38
|
+
* boundary and across restart because the resume re-delivers the exact captured
|
|
39
|
+
* chunks (not a regenerated answer). #3278's per-chunk state machine
|
|
40
|
+
* (`unsent → pending → landed-unconfirmed → {landed-confirmed | unsent}`) binds
|
|
41
|
+
* here too: the resume re-PROBES `landed-unconfirmed` chunks (never re-sends
|
|
42
|
+
* them) and re-SENDS only `unsent` ones, so a confirmed chunk is never duplicated
|
|
43
|
+
* by either the in-fire retry OR the cross-represent resume.
|
|
44
|
+
*
|
|
45
|
+
* This module is PURE state + orchestration over injected effects — no Telegram,
|
|
46
|
+
* no SQLite, no gateway module state — so the resume decision is unit-testable
|
|
47
|
+
* (see tests/captured-answer-resume.test.ts). The gateway owns the real
|
|
48
|
+
* `deliver` (via `deliverAnswer`), the durable oracle, and the obligation I/O.
|
|
49
|
+
*/
|
|
50
|
+
|
|
51
|
+
import { hasOutboundWithText } from '../history.js'
|
|
52
|
+
import type { BackstopDeliveryLedger } from './backstop-delivery.js'
|
|
53
|
+
import type { CapturedDeliverySnapshot, Obligation } from './obligation-ledger.js'
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Build the durable {@link CapturedDeliverySnapshot} from the live per-chunk
|
|
57
|
+
* ledger after a partial `runBackstopDelivery`. Captures every LANDED chunk's
|
|
58
|
+
* message ids + confirmed flag so the resume can (a) never re-send a confirmed
|
|
59
|
+
* chunk and (b) re-PROBE a landed-unconfirmed chunk instead of blindly re-sending
|
|
60
|
+
* it. Chunks that never landed (send threw) are simply absent → the resume treats
|
|
61
|
+
* them as `unsent` and re-sends them. Pure.
|
|
62
|
+
*/
|
|
63
|
+
export function buildCapturedDeliverySnapshot(
|
|
64
|
+
ledger: BackstopDeliveryLedger,
|
|
65
|
+
turnId: string,
|
|
66
|
+
chunks: readonly string[],
|
|
67
|
+
): CapturedDeliverySnapshot {
|
|
68
|
+
const chunkStates = ledger.entries(turnId).map(({ index, messageIds }) => ({
|
|
69
|
+
index,
|
|
70
|
+
messageIds,
|
|
71
|
+
confirmed: ledger.hasConfirmedChunk(turnId, index),
|
|
72
|
+
}))
|
|
73
|
+
return { chunks: [...chunks], chunkStates }
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Rehydrate a `BackstopDeliveryLedger` for a captured-answer resume from a
|
|
78
|
+
* durable snapshot, reconciled against the durable outbound-text oracle. For each
|
|
79
|
+
* landed chunk in the snapshot:
|
|
80
|
+
* - re-record its landed message id(s) so `unsentIndices` excludes it (the
|
|
81
|
+
* resume never re-sends a chunk that already landed);
|
|
82
|
+
* - mark it `landed-confirmed` when the snapshot flag says so OR the oracle
|
|
83
|
+
* proves its text is a durable outbound row (crash-idempotency, §2.3/A5) —
|
|
84
|
+
* a confirmed chunk is neither re-probed nor re-sent.
|
|
85
|
+
* Chunks absent from the snapshot stay `unsent` (re-sent by the resume). Pure over
|
|
86
|
+
* the injected `hasLanded` oracle.
|
|
87
|
+
*/
|
|
88
|
+
export function hydrateResumeLedger(
|
|
89
|
+
ledger: BackstopDeliveryLedger,
|
|
90
|
+
turnId: string,
|
|
91
|
+
snapshot: CapturedDeliverySnapshot,
|
|
92
|
+
hasLanded: (chunkText: string, index: number) => boolean,
|
|
93
|
+
): void {
|
|
94
|
+
for (const st of snapshot.chunkStates) {
|
|
95
|
+
if (st.messageIds.length === 0) continue // never actually landed → leave unsent
|
|
96
|
+
ledger.recordChunk(turnId, st.index, [...st.messageIds])
|
|
97
|
+
if (st.confirmed || hasLanded(snapshot.chunks[st.index] ?? '', st.index)) {
|
|
98
|
+
ledger.confirmChunk(turnId, st.index)
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** The args the module hands the gateway's `deliverAnswer` to run a resume. */
|
|
104
|
+
export interface CapturedResumeArgs {
|
|
105
|
+
chatId: string
|
|
106
|
+
threadId: number | undefined
|
|
107
|
+
text: string
|
|
108
|
+
turnId: string
|
|
109
|
+
cardMessageId: number | null
|
|
110
|
+
replyToMessageId: number | null
|
|
111
|
+
resume: {
|
|
112
|
+
snapshot: CapturedDeliverySnapshot
|
|
113
|
+
hydrate: (ledger: BackstopDeliveryLedger, turnId: string) => void
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* The RAW gateway singletons the dispatcher wires. Structural (no gateway class
|
|
119
|
+
* imports) and kept minimal so the gateway injection stays a handful of lines
|
|
120
|
+
* under the gateway.ts line ratchet — the module builds every resume closure
|
|
121
|
+
* (the `deliverAnswer` resume args, the oracle-reconciled hydrate, the supersede
|
|
122
|
+
* record) itself.
|
|
123
|
+
*/
|
|
124
|
+
export interface CapturedResumePorts {
|
|
125
|
+
/** The gateway's `deliverAnswer`, invoked in RESUME mode (byte-identical tail,
|
|
126
|
+
* no card gate). Only `delivered`/`sentIds` are read. */
|
|
127
|
+
deliverAnswer: (args: CapturedResumeArgs) => Promise<{ delivered: boolean; sentIds: number[] }>
|
|
128
|
+
/** The single obligation ledger. `markRepresented` consumes one represent-budget
|
|
129
|
+
* unit + stamps the per-represent grace (bounding the resume ladder exactly
|
|
130
|
+
* like a fresh-generation represent → escalate at the cap); `close` drops the
|
|
131
|
+
* captured snapshot once fully delivered. */
|
|
132
|
+
obligationLedger: {
|
|
133
|
+
markRepresented: (originTurnId: string) => unknown
|
|
134
|
+
close: (originTurnId: string) => unknown
|
|
135
|
+
}
|
|
136
|
+
/** The per-chunk backstop ledger — GC'd for a fully delivered turn. */
|
|
137
|
+
backstopDeliveryLedger: { clear: (turnId: string) => void }
|
|
138
|
+
/** flushedTurnSupersede — feed the resume's fresh chat ids in so a late reply
|
|
139
|
+
* after the resumed tail still corrects the delivered set (design §4). */
|
|
140
|
+
flushedTurnSupersede: {
|
|
141
|
+
record: (
|
|
142
|
+
chatId: string,
|
|
143
|
+
threadId: number | undefined,
|
|
144
|
+
rec: { turnId: string; messageIds: number[]; text: string },
|
|
145
|
+
nowMs: number,
|
|
146
|
+
) => void
|
|
147
|
+
}
|
|
148
|
+
/** True when history is queryable (else the durable-text oracle reconcile that
|
|
149
|
+
* gives crash-idempotency — §2.3/A5 — is skipped). */
|
|
150
|
+
historyEnabled: boolean
|
|
151
|
+
stderr?: (s: string) => void
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export interface CapturedResumeDispatcher {
|
|
155
|
+
/** Fire a captured-answer resume for `o` (fire-and-forget; the sweep is sync).
|
|
156
|
+
* De-duped per `originTurnId` so two sweeps never launch concurrent resumes;
|
|
157
|
+
* a no-op when `o` carries no non-empty captured snapshot (the caller should
|
|
158
|
+
* then fall through to fresh generation). */
|
|
159
|
+
dispatch: (o: Obligation) => void
|
|
160
|
+
/** In-flight origin ids (test introspection). */
|
|
161
|
+
inFlight: () => string[]
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* The obligation-sweep's captured-answer resume driver. Owns the in-flight guard
|
|
166
|
+
* + the deliver→close/leave-open orchestration so the gateway sweep stays a
|
|
167
|
+
* two-line dispatch. On each dispatch it consumes one represent-budget unit
|
|
168
|
+
* (bounding the ladder → escalate), re-delivers only the non-confirmed tail, and:
|
|
169
|
+
* - fully delivered ⇒ record the supersede tail, close the obligation, GC the
|
|
170
|
+
* ledger (the represent ladder stops);
|
|
171
|
+
* - still partial / error ⇒ leave the obligation OPEN so the next eligible
|
|
172
|
+
* sweep (after the per-represent grace) retries, until the represent cap
|
|
173
|
+
* escalates it to the operator nudge — no infinite loop.
|
|
174
|
+
*/
|
|
175
|
+
export function createCapturedResumeDispatcher(ports: CapturedResumePorts): CapturedResumeDispatcher {
|
|
176
|
+
const stderr = ports.stderr ?? (() => {})
|
|
177
|
+
const inFlight = new Set<string>()
|
|
178
|
+
|
|
179
|
+
/** Re-deliver the non-confirmed tail of `o`'s captured answer, byte-identical,
|
|
180
|
+
* by rehydrating the per-chunk ledger from the snapshot reconciled against the
|
|
181
|
+
* durable text oracle. Never regenerates. */
|
|
182
|
+
function deliver(o: Obligation, snapshot: CapturedDeliverySnapshot): Promise<{ delivered: boolean; sentIds: number[] }> {
|
|
183
|
+
return ports.deliverAnswer({
|
|
184
|
+
chatId: o.chatId,
|
|
185
|
+
threadId: o.threadId,
|
|
186
|
+
text: '',
|
|
187
|
+
turnId: o.originTurnId,
|
|
188
|
+
cardMessageId: null,
|
|
189
|
+
replyToMessageId: o.messageId,
|
|
190
|
+
resume: {
|
|
191
|
+
snapshot,
|
|
192
|
+
hydrate: (ledger, turnId) =>
|
|
193
|
+
hydrateResumeLedger(ledger, turnId, snapshot, (txt) =>
|
|
194
|
+
ports.historyEnabled &&
|
|
195
|
+
hasOutboundWithText(o.chatId, txt, o.threadId ?? null, o.openedAt),
|
|
196
|
+
),
|
|
197
|
+
},
|
|
198
|
+
})
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/** Record the resume's FULL delivered id set (rehydrated-confirmed + freshly
|
|
202
|
+
* re-sent — `runBackstopDelivery` returns `ledger.sentIds`, i.e. all landed
|
|
203
|
+
* ids in index order) into the supersede lane, keyed by the turn id. This
|
|
204
|
+
* REPLACES the partial turn-flush record in place with the complete set, so a
|
|
205
|
+
* late reply for this turn corrects the WHOLE delivered answer (design §4) —
|
|
206
|
+
* no-op when nothing has landed. */
|
|
207
|
+
function supersede(o: Obligation, snapshot: CapturedDeliverySnapshot, sentIds: number[]): void {
|
|
208
|
+
if (sentIds.length === 0) return
|
|
209
|
+
ports.flushedTurnSupersede.record(
|
|
210
|
+
o.chatId,
|
|
211
|
+
o.threadId,
|
|
212
|
+
{ turnId: o.originTurnId, messageIds: sentIds, text: snapshot.chunks.join('') },
|
|
213
|
+
Date.now(),
|
|
214
|
+
)
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
async function run(o: Obligation, snapshot: CapturedDeliverySnapshot): Promise<void> {
|
|
218
|
+
try {
|
|
219
|
+
const { delivered, sentIds } = await deliver(o, snapshot)
|
|
220
|
+
supersede(o, snapshot, sentIds)
|
|
221
|
+
if (delivered) {
|
|
222
|
+
ports.obligationLedger.close(o.originTurnId)
|
|
223
|
+
ports.backstopDeliveryLedger.clear(o.originTurnId)
|
|
224
|
+
stderr(
|
|
225
|
+
`telegram gateway: captured-answer resume delivered — origin=${o.originTurnId} ` +
|
|
226
|
+
`${sentIds.length} chunk(s) confirmed; obligation closed\n`,
|
|
227
|
+
)
|
|
228
|
+
} else {
|
|
229
|
+
// Tail still not confirmed — leave the obligation OPEN for the next paced
|
|
230
|
+
// sweep / escalation (bounded by the represent cap).
|
|
231
|
+
stderr(
|
|
232
|
+
`telegram gateway: captured-answer resume partial — origin=${o.originTurnId} ` +
|
|
233
|
+
`tail still not confirmed; left OPEN for retry\n`,
|
|
234
|
+
)
|
|
235
|
+
}
|
|
236
|
+
} catch (err) {
|
|
237
|
+
stderr(
|
|
238
|
+
`telegram gateway: captured-answer resume error — origin=${o.originTurnId}: ` +
|
|
239
|
+
`${err instanceof Error ? err.message : String(err)}\n`,
|
|
240
|
+
)
|
|
241
|
+
} finally {
|
|
242
|
+
inFlight.delete(o.originTurnId)
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
return {
|
|
247
|
+
dispatch(o: Obligation): void {
|
|
248
|
+
const snapshot = o.capturedDelivery
|
|
249
|
+
if (snapshot == null || snapshot.chunks.length === 0) return
|
|
250
|
+
if (inFlight.has(o.originTurnId)) return
|
|
251
|
+
inFlight.add(o.originTurnId)
|
|
252
|
+
// Consume represent budget + arm the per-represent grace up front so the
|
|
253
|
+
// ladder is bounded and the sweep can't immediately re-dispatch mid-flight.
|
|
254
|
+
ports.obligationLedger.markRepresented(o.originTurnId)
|
|
255
|
+
void run(o, snapshot)
|
|
256
|
+
},
|
|
257
|
+
inFlight: () => [...inFlight],
|
|
258
|
+
}
|
|
259
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest'
|
|
2
|
+
import { buildVaultRequestAccessKeyboard } from './vault-request-access-card.js'
|
|
3
|
+
import { buildMentalModelProposeKeyboard } from './mental-model-propose-card.js'
|
|
4
|
+
|
|
5
|
+
// The approve/deny keyboards for the vault-access (vra:) and mental-model
|
|
6
|
+
// proposal (mmp:) cards moved out of gateway.ts in #2996 P5. Their callback_data
|
|
7
|
+
// prefixes are the contract the callback dispatch table in gateway.ts + the
|
|
8
|
+
// handlers in callback-query-handlers.ts route on — pin them so a rename here
|
|
9
|
+
// can't silently break the tap routing.
|
|
10
|
+
describe('approval-card keyboards (#2996 P5 extraction)', () => {
|
|
11
|
+
it('buildVaultRequestAccessKeyboard carries the vra:* contract', () => {
|
|
12
|
+
const flat = buildVaultRequestAccessKeyboard('abcd1234').inline_keyboard.flat()
|
|
13
|
+
expect(flat.map((b) => b.callback_data)).toEqual([
|
|
14
|
+
'vra:approve:abcd1234',
|
|
15
|
+
'vra:deny:abcd1234',
|
|
16
|
+
])
|
|
17
|
+
expect(flat.map((b) => b.text)).toEqual(['✅ Approve', '🚫 Deny'])
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
it('buildMentalModelProposeKeyboard carries the mmp:* contract', () => {
|
|
21
|
+
const flat = buildMentalModelProposeKeyboard('abcd1234').inline_keyboard.flat()
|
|
22
|
+
expect(flat.map((b) => b.callback_data)).toEqual([
|
|
23
|
+
'mmp:approve:abcd1234',
|
|
24
|
+
'mmp:deny:abcd1234',
|
|
25
|
+
])
|
|
26
|
+
expect(flat.map((b) => b.text)).toEqual(['✅ Approve', '🚫 Deny'])
|
|
27
|
+
})
|
|
28
|
+
})
|