switchroom 0.18.30 → 0.18.32
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 +4 -2
- package/dist/cli/notion-write-pretool.mjs +4 -2
- package/dist/cli/switchroom.js +708 -255
- package/dist/host-control/main.js +5 -3
- package/dist/vault/approvals/kernel-server.js +4 -2
- package/dist/vault/broker/server.js +4 -2
- package/package.json +3 -2
- package/profiles/_base/start.sh.hbs +142 -7
- package/telegram-plugin/dist/gateway/gateway.js +25870 -25007
- package/telegram-plugin/gateway/backstop-delivery.ts +223 -23
- package/telegram-plugin/gateway/captured-answer-resume.ts +259 -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 +5479 -7069
- 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/obligation-ledger.ts +42 -0
- package/telegram-plugin/gateway/obligation-store.ts +37 -1
- package/telegram-plugin/gateway/outbound-send-path.ts +2012 -0
- package/telegram-plugin/gateway/turn-flush-suppression.ts +82 -0
- package/telegram-plugin/pending-user-notice.ts +59 -13
- package/telegram-plugin/subagent-watcher.ts +111 -28
- 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/buffer-gate-broadened.test.ts +16 -6
- package/telegram-plugin/tests/button-tap-turn-gated.test.ts +3 -3
- package/telegram-plugin/tests/captured-answer-resume.test.ts +358 -0
- package/telegram-plugin/tests/emission-authority-facade.test.ts +29 -19
- package/telegram-plugin/tests/emission-authority-ping-gate.test.ts +4 -1
- package/telegram-plugin/tests/emission-determinism-wiring.test.ts +18 -7
- package/telegram-plugin/tests/gateway-boot-side-effect-gating.test.ts +249 -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 +303 -0
- package/telegram-plugin/tests/gateway-outbound-redact.test.ts +10 -3
- 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 +4 -4
- package/telegram-plugin/tests/litellm-proxy-auth-misconfig.test.ts +69 -14
- package/telegram-plugin/tests/multitopic-routing-wiring.test.ts +9 -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 +2 -1
- package/telegram-plugin/tests/permission-no-repeat-wiring.test.ts +9 -6
- package/telegram-plugin/tests/photo-reroute-wiring.test.ts +5 -2
- package/telegram-plugin/tests/reply-terminal-reaction.test.ts +6 -2
- package/telegram-plugin/tests/secret-detect-oauth-code.test.ts +3 -2
- package/telegram-plugin/tests/send-reply-golden.test.ts +571 -0
- package/telegram-plugin/tests/subagent-watcher-resume-reregister.test.ts +291 -0
- package/telegram-plugin/tests/subagent-watcher-resurrection.test.ts +32 -0
- package/telegram-plugin/tests/turn-end-gate-backstop.test.ts +8 -12
- package/telegram-plugin/tests/turn-flush-safety.test.ts +8 -6
- package/telegram-plugin/tests/turn-flush-suppression-wiring.test.ts +112 -0
- package/telegram-plugin/tests/turn-flush-suppression.test.ts +90 -0
- package/telegram-plugin/tests/vault-grant-inbound-builders.test.ts +2 -2
- package/telegram-plugin/tests/worker-feed-coalesce.test.ts +126 -0
- package/telegram-plugin/tool-activity-summary.ts +27 -3
- package/telegram-plugin/worker-activity-feed.ts +24 -0
- 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
|
@@ -38,14 +38,13 @@
|
|
|
38
38
|
*
|
|
39
39
|
* The gateway-internal turn/poke effects (setTurnStarted,
|
|
40
40
|
* clearTurnStarted, noteOutbound, firePoke) map to gateway-scope state
|
|
41
|
-
* (
|
|
41
|
+
* (the silence-poke ladder, turn markers) that this decoupled module
|
|
42
42
|
* cannot reach directly, so they are dispatched through OPTIONAL
|
|
43
43
|
* callbacks on the ctx. When a callback is absent the effect logs an
|
|
44
44
|
* `unwired` trace rather than silently no-opping — the trace is the gate.
|
|
45
45
|
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
* Default is ON — this PR is the cutover.
|
|
46
|
+
* The `SWITCHROOM_DELIVERY_MACHINE_CUTOVER` kill switch was removed in
|
|
47
|
+
* #2996 P1 after the cutover baked — the dispatcher always executes.
|
|
49
48
|
*/
|
|
50
49
|
|
|
51
50
|
import type {
|
|
@@ -80,10 +79,10 @@ export interface DispatchCtx {
|
|
|
80
79
|
*/
|
|
81
80
|
readonly onUserInboundDelivered?: (merged: InboundMessage) => void
|
|
82
81
|
// ── Gateway-internal turn/poke effect callbacks ──────────────────
|
|
83
|
-
// These effects map to gateway-scope state (
|
|
84
|
-
//
|
|
85
|
-
// gateway supplies them
|
|
86
|
-
//
|
|
82
|
+
// These effects map to gateway-scope state (the silence-poke ladder,
|
|
83
|
+
// turn markers) that this decoupled module cannot reach. The
|
|
84
|
+
// gateway supplies them at the live inbound call site. Absent → the
|
|
85
|
+
// effect logs an `unwired` trace.
|
|
87
86
|
readonly onSetTurnStarted?: (key: ChatKey, at: number) => void
|
|
88
87
|
/**
|
|
89
88
|
* Optional: observe the outcome of a `deliverToBridge` effect. The
|
|
@@ -101,12 +100,6 @@ export interface DispatchCtx {
|
|
|
101
100
|
readonly onFirePoke?: (key: ChatKey, level: 'soft' | 'firm' | 'fallback') => void
|
|
102
101
|
}
|
|
103
102
|
|
|
104
|
-
const enabled = process.env.SWITCHROOM_DELIVERY_MACHINE_CUTOVER !== '0'
|
|
105
|
-
|
|
106
|
-
export function isDispatchEnabled(): boolean {
|
|
107
|
-
return enabled
|
|
108
|
-
}
|
|
109
|
-
|
|
110
103
|
/**
|
|
111
104
|
* Execute the effects returned by `transition()`. Pure imperative
|
|
112
105
|
* driver: side-effects only, no machine state held here.
|
|
@@ -117,7 +110,6 @@ export function isDispatchEnabled(): boolean {
|
|
|
117
110
|
* already up).
|
|
118
111
|
*/
|
|
119
112
|
export function dispatchEffects(effects: readonly Effect[], ctx: DispatchCtx): void {
|
|
120
|
-
if (!enabled) return
|
|
121
113
|
for (const effect of effects) {
|
|
122
114
|
dispatchOne(effect, ctx)
|
|
123
115
|
}
|
|
@@ -1,32 +1,30 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* InboundDeliveryStateMachine —
|
|
2
|
+
* InboundDeliveryStateMachine — live machine state + event driver.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
4
|
+
* Historically the "shadow mode" wiring (RFC
|
|
5
|
+
* `reference/rfcs/inbound-delivery-state-machine.md` Phase 2b PR 2): the
|
|
6
|
+
* machine ran ALONGSIDE the imperative gateway code, predicting effects
|
|
7
|
+
* to a structured trace. The #3012 cutover made this state AUTHORITATIVE
|
|
8
|
+
* for the turn-in-flight gate and the deliver-vs-buffer inbound routing,
|
|
9
|
+
* and #2996 P1 removed the kill switches (`SWITCHROOM_DELIVERY_MACHINE_SHADOW`,
|
|
10
|
+
* `SWITCHROOM_DELIVERY_MACHINE_CUTOVER`) — the machine is now the sole
|
|
11
|
+
* delivery authority. (The `shadowEmit` name is kept for its ~15 gateway
|
|
12
|
+
* call sites; the `gw-trace shadow` trace prefix is kept for log-parser
|
|
13
|
+
* continuity.) This module's job:
|
|
9
14
|
*
|
|
10
15
|
* 1. Own the module-scope machine state.
|
|
11
|
-
* 2. Expose `shadowEmit(event)` that runs `transition()
|
|
12
|
-
*
|
|
13
|
-
*
|
|
16
|
+
* 2. Expose `shadowEmit(event)` that runs `transition()`, advances the
|
|
17
|
+
* state, logs via `gw-trace shadow ...` stderr lines, and RETURNS
|
|
18
|
+
* the effects for the gateway to execute (`dispatchEffects`).
|
|
19
|
+
* 3. Expose `isMachineInTurn()` — the authoritative gate read.
|
|
20
|
+
* 4. Provide test hooks for resetting + inspecting state.
|
|
14
21
|
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
* imperative paths get deleted in PR 4.
|
|
18
|
-
*
|
|
19
|
-
* Telemetry approach: each `shadowEmit` writes a single stderr line
|
|
20
|
-
* with the event kind + emitted effect kinds. Operators can then:
|
|
22
|
+
* Telemetry: each `shadowEmit` writes a single stderr line with the
|
|
23
|
+
* event kind + emitted effect kinds. Operators can then:
|
|
21
24
|
*
|
|
22
25
|
* docker exec switchroom-<agent> sh -lc 'grep "gw-trace shadow" \
|
|
23
26
|
* /var/log/switchroom/gateway-supervisor.log | tail -50'
|
|
24
27
|
*
|
|
25
|
-
* to see what the state machine PREDICTS the gateway should do for
|
|
26
|
-
* each event. Comparing against the actual log lines (`pending-inbound-
|
|
27
|
-
* buffer: agent=X buffered ...` etc.) is the validation that the
|
|
28
|
-
* machine is bit-identical with reality.
|
|
29
|
-
*
|
|
30
28
|
* Trace verbosity (#3025): the TTL `tick` event fires every ~30s and on a
|
|
31
29
|
* healthy idle agent emits a zero-signal `event=tick effects=[]
|
|
32
30
|
* global=bridge_alive_idle` line. Those no-op ticks are SUPPRESSED by
|
|
@@ -36,10 +34,6 @@
|
|
|
36
34
|
* in `../shared/gw-trace-gate.ts` and also governs the `tg-post` poll
|
|
37
35
|
* heartbeats. Real events, turn-expiring ticks, and in-turn ticks always
|
|
38
36
|
* log regardless of the flag.
|
|
39
|
-
*
|
|
40
|
-
* Toggle off via `SWITCHROOM_DELIVERY_MACHINE_SHADOW=0` — a kill
|
|
41
|
-
* switch for the case where the shadow emits prove problematic
|
|
42
|
-
* (e.g., trace volume too high). The default is ON.
|
|
43
37
|
*/
|
|
44
38
|
|
|
45
39
|
import {
|
|
@@ -52,58 +46,36 @@ import {
|
|
|
52
46
|
import { shouldEmitShadowTrace } from '../shared/gw-trace-gate.js'
|
|
53
47
|
|
|
54
48
|
let state: State = initialState()
|
|
55
|
-
const enabled = process.env.SWITCHROOM_DELIVERY_MACHINE_SHADOW !== '0'
|
|
56
|
-
|
|
57
|
-
// Phase 2b PR 3 — STAGED CUTOVER. When enabled, the gateway's
|
|
58
|
-
// "is a turn in flight?" gate reads this machine's global state
|
|
59
|
-
// instead of the PR3b `claudeBusyKeys` set. The machine tracks ONE
|
|
60
|
-
// `activeTurn` (single bridge) plus TTL `tick` expiry, so — unlike a
|
|
61
|
-
// per-delivery key set — it cannot accumulate orphan keys and wedge
|
|
62
|
-
// the gate "in-flight forever" (the gymbro/clerk 5-min dangle of
|
|
63
|
-
// 2026-05-28). Scope is the turn-in-flight GATE only; the poke ladder
|
|
64
|
-
// and perm-verdict effects stay imperative for a follow-up PR.
|
|
65
|
-
//
|
|
66
|
-
// Kill switch: `SWITCHROOM_DELIVERY_MACHINE_CUTOVER=0` reverts every
|
|
67
|
-
// gate to the legacy claudeBusyKeys read (zero behaviour change).
|
|
68
|
-
// Requires shadow mode ON — with shadow off the machine state is
|
|
69
|
-
// frozen and must NOT be read as authoritative.
|
|
70
|
-
const cutoverEnabled = enabled && process.env.SWITCHROOM_DELIVERY_MACHINE_CUTOVER !== '0'
|
|
71
|
-
|
|
72
|
-
/**
|
|
73
|
-
* True when the kill-switch leaves the delivery machine authoritative
|
|
74
|
-
* for the turn-in-flight gate. Gateway gate sites branch on this.
|
|
75
|
-
*/
|
|
76
|
-
export function isDeliveryCutoverEnabled(): boolean {
|
|
77
|
-
return cutoverEnabled
|
|
78
|
-
}
|
|
79
49
|
|
|
80
50
|
/**
|
|
81
51
|
* Authoritative "is a turn currently in flight?" read for the gate.
|
|
82
|
-
*
|
|
83
|
-
*
|
|
84
|
-
*
|
|
52
|
+
* The machine tracks ONE `activeTurn` (single bridge) plus TTL `tick`
|
|
53
|
+
* expiry, so — unlike the deleted per-delivery `claudeBusyKeys` set —
|
|
54
|
+
* it cannot accumulate orphan keys and wedge the gate "in-flight
|
|
55
|
+
* forever" (the gymbro/clerk 5-min dangle of 2026-05-28). `bridge_dead`
|
|
56
|
+
* and `bridge_alive_idle` are both "not in flight".
|
|
85
57
|
*/
|
|
86
58
|
export function isMachineInTurn(): boolean {
|
|
87
59
|
return state.global.kind === 'bridge_alive_in_turn'
|
|
88
60
|
}
|
|
89
61
|
|
|
90
62
|
/**
|
|
91
|
-
* Run an event through the state machine
|
|
92
|
-
*
|
|
93
|
-
*
|
|
94
|
-
*
|
|
95
|
-
*
|
|
96
|
-
*
|
|
63
|
+
* Run an event through the state machine: the machine state advances,
|
|
64
|
+
* the effects are logged and RETURNED for the caller to execute
|
|
65
|
+
* (`dispatchEffects` at the live inbound/bridgeUp call sites; the
|
|
66
|
+
* lifecycle emits — turnStart/turnEnd/modelOutbound/tick — advance state
|
|
67
|
+
* only and ignore the return, their real-world work still being
|
|
68
|
+
* imperative per the RFC's open PR3b checklist).
|
|
97
69
|
*/
|
|
98
70
|
export function shadowEmit(event: Event): readonly Effect[] {
|
|
99
|
-
|
|
100
|
-
//
|
|
101
|
-
// pure (no I/O, no async) and property-tested over 5000 schedules,
|
|
71
|
+
// The machine driver MUST NEVER break the gateway. The state machine
|
|
72
|
+
// is pure (no I/O, no async) and property-tested over 5000 schedules,
|
|
102
73
|
// so transition() won't throw on well-formed input. But event
|
|
103
74
|
// construction at the call site could mis-shape inputs; the
|
|
104
|
-
// try/catch is belt-and-braces so a
|
|
105
|
-
// turn. The catch logs and bails — the
|
|
106
|
-
//
|
|
75
|
+
// try/catch is belt-and-braces so a driver bug never wedges a real
|
|
76
|
+
// turn. The catch logs and bails — the gateway code below the emit
|
|
77
|
+
// point still runs (falling back to the buffer-everything semantics
|
|
78
|
+
// of an un-advanced machine).
|
|
107
79
|
try {
|
|
108
80
|
const result = transition(state, event)
|
|
109
81
|
state = result.state
|
|
@@ -164,8 +136,3 @@ export function __shadowResetForTests(): void {
|
|
|
164
136
|
export function __shadowGetStateForTests(): State {
|
|
165
137
|
return state
|
|
166
138
|
}
|
|
167
|
-
|
|
168
|
-
/** Test hook: check if shadow mode is enabled (mirrors the env-var). */
|
|
169
|
-
export function __shadowEnabledForTests(): boolean {
|
|
170
|
-
return enabled
|
|
171
|
-
}
|
|
@@ -26,6 +26,28 @@
|
|
|
26
26
|
|
|
27
27
|
import type { InboundMessage } from './ipc-protocol.js'
|
|
28
28
|
|
|
29
|
+
/**
|
|
30
|
+
* #3282 — the durable per-chunk snapshot of a PARTIALLY delivered backstop
|
|
31
|
+
* answer, attached to the obligation so the represent can resume the exact
|
|
32
|
+
* captured answer's non-`landed-confirmed` tail (byte-identical) instead of
|
|
33
|
+
* regenerating it (which re-posts chunks that already landed). Rides the
|
|
34
|
+
* obligation ledger's atomic snapshot, so it survives a gateway/container
|
|
35
|
+
* restart. See captured-answer-resume.ts for the resume mechanism.
|
|
36
|
+
*/
|
|
37
|
+
export interface CapturedDeliverySnapshot {
|
|
38
|
+
/** The byte-identical captured answer, split into chunks (the SAME split the
|
|
39
|
+
* original backstop delivery used). Re-delivered verbatim on resume. */
|
|
40
|
+
readonly chunks: string[]
|
|
41
|
+
/** Per-LANDED-chunk state: the landed message id(s) + whether a read-back
|
|
42
|
+
* confirmed the message exists. A chunk absent here never landed → the resume
|
|
43
|
+
* treats it as `unsent` and re-sends it. */
|
|
44
|
+
readonly chunkStates: Array<{
|
|
45
|
+
readonly index: number
|
|
46
|
+
readonly messageIds: number[]
|
|
47
|
+
readonly confirmed: boolean
|
|
48
|
+
}>
|
|
49
|
+
}
|
|
50
|
+
|
|
29
51
|
export interface Obligation {
|
|
30
52
|
/** deriveTurnId(chat, thread, messageId) — the stable identity. */
|
|
31
53
|
readonly originTurnId: string
|
|
@@ -61,6 +83,12 @@ export interface Obligation {
|
|
|
61
83
|
* re-present/escalate when the sweep fires < 5s later. Durable (part of the
|
|
62
84
|
* snapshot) so the grace window survives a restart. */
|
|
63
85
|
lastRepresentedAt?: number
|
|
86
|
+
/** #3282 — durable captured-answer snapshot for a PARTIALLY delivered backstop
|
|
87
|
+
* answer. Present ⇒ the represent branch drives a byte-identical captured-answer
|
|
88
|
+
* RESUME of the non-confirmed tail (never a regeneration); absent ⇒ the genuine
|
|
89
|
+
* "model wrote nothing" represent falls through to fresh generation. Dropped
|
|
90
|
+
* with the obligation on close/escalate (bounded lifetime, design A6). */
|
|
91
|
+
capturedDelivery?: CapturedDeliverySnapshot
|
|
64
92
|
}
|
|
65
93
|
|
|
66
94
|
/** What the gateway should do for the oldest open obligation at an idle boundary. */
|
|
@@ -316,6 +344,20 @@ export class ObligationLedger {
|
|
|
316
344
|
return null
|
|
317
345
|
}
|
|
318
346
|
|
|
347
|
+
/**
|
|
348
|
+
* #3282 — attach (or refresh) the durable captured-answer snapshot for a
|
|
349
|
+
* PARTIALLY delivered backstop answer. No-op if the obligation isn't open (a
|
|
350
|
+
* fully delivered turn closes its obligation, so there is nothing to resume).
|
|
351
|
+
* Persists, so the snapshot survives a restart and the represent resumes the
|
|
352
|
+
* missing tail instead of regenerating the whole answer.
|
|
353
|
+
*/
|
|
354
|
+
noteCapturedDelivery(originTurnId: string, snapshot: CapturedDeliverySnapshot): void {
|
|
355
|
+
const o = this.open.get(originTurnId)
|
|
356
|
+
if (o === undefined || snapshot.chunks.length === 0) return
|
|
357
|
+
o.capturedDelivery = snapshot
|
|
358
|
+
this.persist()
|
|
359
|
+
}
|
|
360
|
+
|
|
319
361
|
/** Record that an obligation was just re-presented (bumps representCount, stamps
|
|
320
362
|
* lastRepresentedAt for the per-represent grace window). */
|
|
321
363
|
markRepresented(originTurnId: string, now = Date.now()): number {
|
|
@@ -55,6 +55,42 @@ function isObligationRow(x: unknown): x is Obligation {
|
|
|
55
55
|
)
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
+
/**
|
|
59
|
+
* #3282 — true iff the optional captured-answer snapshot is structurally sound.
|
|
60
|
+
* A malformed blob (partial write, forward-incompatible shape) must NOT crash the
|
|
61
|
+
* resume: `sanitizeCapturedDelivery` strips it, so the obligation degrades to a
|
|
62
|
+
* fresh-generation represent (today's behaviour) rather than throwing.
|
|
63
|
+
*/
|
|
64
|
+
function isValidCapturedDelivery(x: unknown): boolean {
|
|
65
|
+
if (x == null || typeof x !== 'object') return false
|
|
66
|
+
const c = x as Record<string, unknown>
|
|
67
|
+
// Empty chunks ⇒ nothing to resume: treat as malformed so it is STRIPPED and
|
|
68
|
+
// the obligation degrades to a (bounded) fresh-generation represent rather than
|
|
69
|
+
// sitting non-null-but-empty and no-op'ing the sweep forever.
|
|
70
|
+
if (!Array.isArray(c.chunks) || c.chunks.length === 0 || !c.chunks.every((t) => typeof t === 'string')) return false
|
|
71
|
+
if (!Array.isArray(c.chunkStates)) return false
|
|
72
|
+
return c.chunkStates.every((s) => {
|
|
73
|
+
if (s == null || typeof s !== 'object') return false
|
|
74
|
+
const st = s as Record<string, unknown>
|
|
75
|
+
return (
|
|
76
|
+
typeof st.index === 'number' &&
|
|
77
|
+
typeof st.confirmed === 'boolean' &&
|
|
78
|
+
Array.isArray(st.messageIds) &&
|
|
79
|
+
st.messageIds.every((m) => typeof m === 'number')
|
|
80
|
+
)
|
|
81
|
+
})
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** Drop a malformed `capturedDelivery` in place so a corrupt snapshot degrades to
|
|
85
|
+
* fresh-generation represent instead of crashing the resume (fail-open). */
|
|
86
|
+
function sanitizeCapturedDelivery(o: Obligation): Obligation {
|
|
87
|
+
if (o.capturedDelivery != null && !isValidCapturedDelivery(o.capturedDelivery)) {
|
|
88
|
+
const { capturedDelivery: _drop, ...rest } = o
|
|
89
|
+
return rest
|
|
90
|
+
}
|
|
91
|
+
return o
|
|
92
|
+
}
|
|
93
|
+
|
|
58
94
|
/**
|
|
59
95
|
* Load the persisted open set. Returns [] on a missing, unreadable, or
|
|
60
96
|
* malformed file (fail-open to empty: a corrupt snapshot must never crash boot;
|
|
@@ -78,7 +114,7 @@ export function loadObligations(path: string, fs: ObligationStoreFsSeam): Obliga
|
|
|
78
114
|
if (parsed == null || typeof parsed !== 'object') return []
|
|
79
115
|
const env = parsed as Record<string, unknown>
|
|
80
116
|
if (env.v !== 1 || !Array.isArray(env.obligations)) return []
|
|
81
|
-
return env.obligations.filter(isObligationRow)
|
|
117
|
+
return env.obligations.filter(isObligationRow).map(sanitizeCapturedDelivery)
|
|
82
118
|
}
|
|
83
119
|
|
|
84
120
|
/**
|