heyspex 0.1.0
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/README.md +157 -0
- package/bin/hs.mjs +1028 -0
- package/bin/postinstall.mjs +88 -0
- package/dev-factory/lib/fleet-readiness.mjs +274 -0
- package/dev-factory/lib/fleet-state.mjs +250 -0
- package/dev-factory/lib/sufficiency-score.mjs +441 -0
- package/dist/cli/hs-check.mjs +747 -0
- package/dist/cli/hs-claim.mjs +541 -0
- package/dist/cli/hs-connect.mjs +3436 -0
- package/dist/cli/hs-defer.mjs +8455 -0
- package/dist/cli/hs-env.mjs +36845 -0
- package/dist/cli/hs-init.mjs +1145 -0
- package/dist/cli/hs-install.mjs +1390 -0
- package/dist/cli/hs-join.mjs +38553 -0
- package/dist/cli/hs-vault-pull.mjs +7782 -0
- package/dist/cli/hs-vault.mjs +4166 -0
- package/dist/cli/hs-workspace.mjs +2898 -0
- package/dist/cli-templates/customer-vault/specs/entity-model.md +1735 -0
- package/dist/cli-templates/customer-vault/specs/protocols/canonical-frontmatter-templates.md +531 -0
- package/dist/cli-templates/customer-vault/specs/protocols/memory-protocol.md +48 -0
- package/dist/cli-templates/customer-vault/specs/protocols/spec-hierarchy.md +548 -0
- package/docs/mcp-client-config.md +200 -0
- package/package.json +124 -0
- package/scripts/check-mcp-freshness.mjs +379 -0
- package/scripts/dev-server-status.mjs +722 -0
- package/scripts/gateway-skills-lib.mjs +421 -0
- package/scripts/lib/console-url-resolver.mjs +152 -0
- package/scripts/lib/dev-mode.mjs +369 -0
- package/scripts/lib/hs-link.mjs +154 -0
- package/scripts/lib/hs-source-commit.mjs +207 -0
- package/scripts/lib/hs-source-log.mjs +366 -0
- package/scripts/lib/hs-source-preflight.mjs +402 -0
- package/scripts/lib/hs-source-push.mjs +307 -0
- package/scripts/lib/hs-source-secret-scan.mjs +263 -0
- package/scripts/lib/standalone-doctor.mjs +89 -0
- package/scripts/vault-id-lib.mjs +173 -0
- package/vendor/heyspex-mcp/alignment-gate.mjs +1347 -0
- package/vendor/heyspex-mcp/blocker-resolver.mjs +891 -0
- package/vendor/heyspex-mcp/breakdown-ladder.mjs +309 -0
- package/vendor/heyspex-mcp/breakdown-source.mjs +236 -0
- package/vendor/heyspex-mcp/breakdown-writer.mjs +524 -0
- package/vendor/heyspex-mcp/brief-observability.mjs +67 -0
- package/vendor/heyspex-mcp/canvas-inbox.mjs +130 -0
- package/vendor/heyspex-mcp/capture-active-rehydrate.mjs +76 -0
- package/vendor/heyspex-mcp/capture-buffer-patches.mjs +147 -0
- package/vendor/heyspex-mcp/capture-channel.mjs +577 -0
- package/vendor/heyspex-mcp/capture-console-client.mjs +290 -0
- package/vendor/heyspex-mcp/capture-focus.mjs +1366 -0
- package/vendor/heyspex-mcp/capture-kind-suggest.mjs +121 -0
- package/vendor/heyspex-mcp/capture-outbox.mjs +239 -0
- package/vendor/heyspex-mcp/capture-stage-gate.mjs +160 -0
- package/vendor/heyspex-mcp/cli.mjs +15754 -0
- package/vendor/heyspex-mcp/config-settings.mjs +419 -0
- package/vendor/heyspex-mcp/console-fetch.mjs +342 -0
- package/vendor/heyspex-mcp/defer-spec.mjs +527 -0
- package/vendor/heyspex-mcp/deferred-reclaim.mjs +284 -0
- package/vendor/heyspex-mcp/design-tools.mjs +449 -0
- package/vendor/heyspex-mcp/dev-factory-bridge.mjs +66 -0
- package/vendor/heyspex-mcp/diagnose-skeleton.mjs +114 -0
- package/vendor/heyspex-mcp/fleet-queue-tools.mjs +241 -0
- package/vendor/heyspex-mcp/hooks-engine.mjs +399 -0
- package/vendor/heyspex-mcp/login-handoff.mjs +438 -0
- package/vendor/heyspex-mcp/mcp-subprocess.mjs +136 -0
- package/vendor/heyspex-mcp/mvp-mode.mjs +101 -0
- package/vendor/heyspex-mcp/next-resolution.mjs +240 -0
- package/vendor/heyspex-mcp/next-review.mjs +548 -0
- package/vendor/heyspex-mcp/peer-presence.mjs +430 -0
- package/vendor/heyspex-mcp/plan-ground-conflicts.mjs +321 -0
- package/vendor/heyspex-mcp/plan-ground.mjs +2001 -0
- package/vendor/heyspex-mcp/plan-ingest.mjs +476 -0
- package/vendor/heyspex-mcp/plan-push.mjs +1826 -0
- package/vendor/heyspex-mcp/plan-review.mjs +1419 -0
- package/vendor/heyspex-mcp/preflight-gate.mjs +0 -0
- package/vendor/heyspex-mcp/procedure-tools.mjs +970 -0
- package/vendor/heyspex-mcp/promote-capture.mjs +1280 -0
- package/vendor/heyspex-mcp/promote-diagnose.mjs +187 -0
- package/vendor/heyspex-mcp/promote-research-to-diagnose.mjs +338 -0
- package/vendor/heyspex-mcp/promote-research.mjs +1553 -0
- package/vendor/heyspex-mcp/promotion-helpers.mjs +280 -0
- package/vendor/heyspex-mcp/read-path.mjs +138 -0
- package/vendor/heyspex-mcp/recipe-tools.mjs +258 -0
- package/vendor/heyspex-mcp/regen-tasks.mjs +326 -0
- package/vendor/heyspex-mcp/register-spec.mjs +417 -0
- package/vendor/heyspex-mcp/research-focus.mjs +476 -0
- package/vendor/heyspex-mcp/research-tools.mjs +109 -0
- package/vendor/heyspex-mcp/research-type-suggest.mjs +112 -0
- package/vendor/heyspex-mcp/resolution-focus.mjs +45 -0
- package/vendor/heyspex-mcp/review-focus.mjs +55 -0
- package/vendor/heyspex-mcp/review-rollup.mjs +47 -0
- package/vendor/heyspex-mcp/run-front.mjs +60 -0
- package/vendor/heyspex-mcp/scheduler.mjs +813 -0
- package/vendor/heyspex-mcp/seat-identity.mjs +281 -0
- package/vendor/heyspex-mcp/settings-tools.mjs +119 -0
- package/vendor/heyspex-mcp/spec-id-lib.mjs +790 -0
- package/vendor/heyspex-mcp/split-research.mjs +832 -0
- package/vendor/heyspex-mcp/submit-for-review.mjs +190 -0
- package/vendor/heyspex-mcp/summary-contract.mjs +162 -0
- package/vendor/heyspex-mcp/test-helpers.mjs +43 -0
- package/vendor/heyspex-mcp/validate-linkage.mjs +574 -0
- package/vendor/heyspex-mcp/vault-resolve.mjs +297 -0
- package/vendor/heyspex-mcp/vault-settings.mjs +87 -0
- package/vendor/heyspex-mcp/verify-reproduction.mjs +314 -0
- package/vendor/heyspex-mcp/why-now.mjs +64 -0
- package/vendor/heyspex-mcp/yaml-scalar.mjs +34 -0
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Console capture-session client — extracted from cli.mjs so the error
|
|
3
|
+
* classification and outbox drain logic are unit-testable without starting
|
|
4
|
+
* the full MCP server (which begins reading stdin + spawns the
|
|
5
|
+
* capture-channel watcher on import).
|
|
6
|
+
*
|
|
7
|
+
* Owning epic: [[HS-EPIC-CPMR-capture-session-patch-materialized-rows-reliability]].
|
|
8
|
+
*
|
|
9
|
+
* Design: a factory `makeCaptureConsoleClient(consoleFetch)` returns a bound
|
|
10
|
+
* client. cli.mjs creates one at module load with its real `consoleFetch`;
|
|
11
|
+
* tests create one with a stub. No module-level side effects on import.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import {
|
|
15
|
+
listOutbox,
|
|
16
|
+
outboxCount,
|
|
17
|
+
outboxEntryToPatch,
|
|
18
|
+
quarantineOutboxEntry,
|
|
19
|
+
removeOutboxEntry,
|
|
20
|
+
stashCaptureArgs,
|
|
21
|
+
} from "./capture-outbox.mjs";
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Error taxonomy for capture-session console calls ([[HS-K27H-DIAG-0G86]]).
|
|
25
|
+
*
|
|
26
|
+
* - `CONSOLE_UNREACHABLE` (status === 0): genuine TRANSPORT failure —
|
|
27
|
+
* ECONNREFUSED / DNS / timeout / abort (console-fetch stamps status 0 on
|
|
28
|
+
* all of these; an HTTP response the server returned always has status > 0).
|
|
29
|
+
* The ONLY class that touches the offline buffer: stash, the next drain
|
|
30
|
+
* replays.
|
|
31
|
+
* - `CAPTURE_CONFLICT` (4xx): the server ANSWERED and structurally refused
|
|
32
|
+
* (abandoned row, validation failure, malformed body). A client error that
|
|
33
|
+
* will fail identically on retry. NEVER stash; surface the server's
|
|
34
|
+
* error + message verbatim; quarantine on drain.
|
|
35
|
+
* - `PATCH_FAILED` (5xx or other): the server ANSWERED and errored applying
|
|
36
|
+
* the write. NEVER stash — pre-K27H this was "stashed like UNREACHABLE",
|
|
37
|
+
* which reported real server failures as connectivity blips with a false
|
|
38
|
+
* "your words are saved and will sync" promise. Surface the server's
|
|
39
|
+
* message verbatim. (Entries ALREADY in the outbox from a genuine
|
|
40
|
+
* transport failure still pause the drain on 5xx and retry later — the
|
|
41
|
+
* ban is on adding new entries for an answered request.)
|
|
42
|
+
*/
|
|
43
|
+
export const CAPTURE_ERROR_UNREACHABLE = "CONSOLE_UNREACHABLE";
|
|
44
|
+
export const CAPTURE_ERROR_CONFLICT = "CAPTURE_CONFLICT";
|
|
45
|
+
export const CAPTURE_ERROR_FAILED = "PATCH_FAILED";
|
|
46
|
+
|
|
47
|
+
/** Classify a `consoleFetch` result by HTTP status range. */
|
|
48
|
+
export function classifyConsoleFetchError(result) {
|
|
49
|
+
if (result.status === 0) return CAPTURE_ERROR_UNREACHABLE;
|
|
50
|
+
if (result.status >= 400 && result.status < 500)
|
|
51
|
+
return CAPTURE_ERROR_CONFLICT;
|
|
52
|
+
return CAPTURE_ERROR_FAILED;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Extract the console's error message from a failed `consoleFetch` result. */
|
|
56
|
+
export function extractConsoleMessage(result) {
|
|
57
|
+
return result?.body?.message || result?.body?.error || null;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Classify a failed capture write and build the honest description of what
|
|
62
|
+
* happened ([[HS-K27H-DIAG-0G86]] AC1-AC5). Pure — no fs, no HTTP.
|
|
63
|
+
*
|
|
64
|
+
* Accepts either failure shape used on the capture surface:
|
|
65
|
+
* - patchCaptureSession failures: `{ ok:false, error, status, consoleMessage, result }`
|
|
66
|
+
* - list/create helper failures: `{ ok:false, error, result }` (status on `result`)
|
|
67
|
+
*
|
|
68
|
+
* @returns {{
|
|
69
|
+
* kind: 'transport'|'client'|'server',
|
|
70
|
+
* shouldStash: boolean,
|
|
71
|
+
* error: string,
|
|
72
|
+
* http_status: number,
|
|
73
|
+
* server_error_code: string|null,
|
|
74
|
+
* server_message: string|null,
|
|
75
|
+
* message: string|null,
|
|
76
|
+
* }}
|
|
77
|
+
*/
|
|
78
|
+
export function describeCaptureWriteFailure(failure) {
|
|
79
|
+
const result = failure?.result ?? failure ?? {};
|
|
80
|
+
const status =
|
|
81
|
+
typeof failure?.status === "number"
|
|
82
|
+
? failure.status
|
|
83
|
+
: typeof result?.status === "number"
|
|
84
|
+
? result.status
|
|
85
|
+
: 0;
|
|
86
|
+
const serverCode = typeof result?.body?.error === "string" ? result.body.error : null;
|
|
87
|
+
const serverMessage = typeof result?.body?.message === "string" ? result.body.message : null;
|
|
88
|
+
|
|
89
|
+
// status 0 ⇔ genuine transport failure (ECONNREFUSED / DNS / timeout /
|
|
90
|
+
// abort — console-fetch stamps status 0 on every non-response). Anything
|
|
91
|
+
// else is an HTTP response the server actually returned.
|
|
92
|
+
if (status === 0) {
|
|
93
|
+
return {
|
|
94
|
+
kind: "transport",
|
|
95
|
+
shouldStash: true,
|
|
96
|
+
error: failure?.error || CAPTURE_ERROR_UNREACHABLE,
|
|
97
|
+
http_status: 0,
|
|
98
|
+
server_error_code: null,
|
|
99
|
+
server_message: null,
|
|
100
|
+
message: null, // the responder composes the transport message (depends on what stashed)
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const label = `HTTP ${status}${serverCode ? ` ${serverCode}` : ""}`;
|
|
105
|
+
const verbatim = serverMessage ? `: ${serverMessage}` : "";
|
|
106
|
+
if (status >= 400 && status < 500) {
|
|
107
|
+
return {
|
|
108
|
+
kind: "client",
|
|
109
|
+
shouldStash: false,
|
|
110
|
+
error: CAPTURE_ERROR_CONFLICT,
|
|
111
|
+
http_status: status,
|
|
112
|
+
server_error_code: serverCode,
|
|
113
|
+
server_message: serverMessage,
|
|
114
|
+
message:
|
|
115
|
+
`The server rejected this write (${label})${verbatim}. ` +
|
|
116
|
+
`The console answered — it is not down. This write was NOT saved and will NOT retry automatically; fix the request and send it again.`,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
return {
|
|
120
|
+
kind: "server",
|
|
121
|
+
shouldStash: false,
|
|
122
|
+
error: CAPTURE_ERROR_FAILED,
|
|
123
|
+
http_status: status,
|
|
124
|
+
server_error_code: serverCode,
|
|
125
|
+
server_message: serverMessage,
|
|
126
|
+
message:
|
|
127
|
+
`The server failed to apply this write (${label})${verbatim}. ` +
|
|
128
|
+
`The console answered — it is not down; the write errored server-side. This write was NOT saved and will NOT retry automatically; check the server logs and send it again.`,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Build the tool-facing payload for a failed capture write, performing the
|
|
134
|
+
* offline-buffer stash ONLY for genuine transport failures (AC1). This is the
|
|
135
|
+
* single buffering decision point for the capture write path — extracted here
|
|
136
|
+
* (rather than living in cli.mjs's handler) so the stash-vs-surface rule is
|
|
137
|
+
* unit-testable against a real tmp outbox.
|
|
138
|
+
*
|
|
139
|
+
* - transport → stash `args`' content fields; the "will sync automatically"
|
|
140
|
+
* promise appears ONLY when something was actually written to the outbox
|
|
141
|
+
* (AC4) — the drain path replays every OUTBOX_KINDS field in canonical
|
|
142
|
+
* structured shape.
|
|
143
|
+
* - 4xx → `status: "refused"` + the server's error/message verbatim (AC2/AC5).
|
|
144
|
+
* - 5xx → `status: "failed"` + the server's message verbatim (AC3/AC5).
|
|
145
|
+
*/
|
|
146
|
+
export function respondToCaptureWriteFailure(vaultRoot, args, failure) {
|
|
147
|
+
const d = describeCaptureWriteFailure(failure);
|
|
148
|
+
if (d.kind === "transport") {
|
|
149
|
+
const stashed = stashCaptureArgs(vaultRoot, args || {});
|
|
150
|
+
const pending = outboxCount(vaultRoot);
|
|
151
|
+
return {
|
|
152
|
+
ok: false,
|
|
153
|
+
status: "deferred",
|
|
154
|
+
error: d.error,
|
|
155
|
+
source: "offline-buffer",
|
|
156
|
+
message:
|
|
157
|
+
stashed.length > 0
|
|
158
|
+
? "HeySpex can't reach the server right now, so this capture isn't filed yet — but your exact words are saved to the offline buffer and will sync automatically on the next capture call once the connection is back. Nothing is lost."
|
|
159
|
+
: "HeySpex can't reach the server right now. Anything you've already captured is safe — please try again in a moment.",
|
|
160
|
+
stashed: stashed.map((s) => ({ kind: s.kind })),
|
|
161
|
+
pending_sync: pending,
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
return {
|
|
165
|
+
ok: false,
|
|
166
|
+
status: d.kind === "client" ? "refused" : "failed",
|
|
167
|
+
error: d.error,
|
|
168
|
+
http_status: d.http_status,
|
|
169
|
+
server_error: d.server_error_code,
|
|
170
|
+
// Back-compat key (tool description documents `console_error` as the
|
|
171
|
+
// verbatim server message).
|
|
172
|
+
console_error: d.server_message ?? d.server_error_code,
|
|
173
|
+
buffered: false,
|
|
174
|
+
message: d.message,
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* @typedef {object} ConsoleFetchResult
|
|
180
|
+
* @property {boolean} ok
|
|
181
|
+
* @property {number} status
|
|
182
|
+
* @property {*} body
|
|
183
|
+
* @property {string} [url]
|
|
184
|
+
* @property {string} [error]
|
|
185
|
+
*
|
|
186
|
+
* @callback ConsoleFetchFn
|
|
187
|
+
* @param {string} path
|
|
188
|
+
* @param {object} [options]
|
|
189
|
+
* @returns {Promise<ConsoleFetchResult>}
|
|
190
|
+
*/
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Build a capture-session console client bound to a `consoleFetch` function.
|
|
194
|
+
*
|
|
195
|
+
* @param {ConsoleFetchFn} consoleFetch
|
|
196
|
+
*/
|
|
197
|
+
export function makeCaptureConsoleClient(consoleFetch) {
|
|
198
|
+
/**
|
|
199
|
+
* PATCH a capture session's content_buffer or attach an agent.
|
|
200
|
+
*
|
|
201
|
+
* Returns:
|
|
202
|
+
* - on success: `{ ok: true, session, materialization, result }`
|
|
203
|
+
* - on failure: `{ ok: false, error: <code>, status, consoleMessage, result }`
|
|
204
|
+
* where `error` is one of CAPTURE_ERROR_*.
|
|
205
|
+
*/
|
|
206
|
+
async function patchCaptureSession(sessionId, patchBody) {
|
|
207
|
+
const result = await consoleFetch(
|
|
208
|
+
`/api/capture/sessions/${encodeURIComponent(sessionId)}`,
|
|
209
|
+
{ method: "PATCH", body: patchBody },
|
|
210
|
+
);
|
|
211
|
+
if (!result.ok) {
|
|
212
|
+
return {
|
|
213
|
+
ok: false,
|
|
214
|
+
error: classifyConsoleFetchError(result),
|
|
215
|
+
status: result.status,
|
|
216
|
+
consoleMessage: extractConsoleMessage(result),
|
|
217
|
+
result,
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
return {
|
|
221
|
+
ok: true,
|
|
222
|
+
session: result.body?.session,
|
|
223
|
+
materialization: result.body?.materialization,
|
|
224
|
+
result,
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Drain offline-buffered captures into the now-reachable session, oldest first.
|
|
230
|
+
*
|
|
231
|
+
* Per [[HS-EPIC-CPMR]]:
|
|
232
|
+
* - `CAPTURE_CONFLICT` (4xx): quarantine + KEEP DRAINING. Pre-fix this
|
|
233
|
+
* would head-of-line-block the queue forever (the offline buffer was the
|
|
234
|
+
* dead-letter graveyard for misclassified materialized-row 409s).
|
|
235
|
+
* - `CONSOLE_UNREACHABLE` / `PATCH_FAILED` (5xx): break — leave the rest
|
|
236
|
+
* for the next reachable call.
|
|
237
|
+
*
|
|
238
|
+
* @param {object} [currentBuffer] - the session's current content_buffer,
|
|
239
|
+
* threaded through `outboxEntryToPatch` so append-shaped sections
|
|
240
|
+
* (userQuotes/chatTurns) merge with what is already on the session
|
|
241
|
+
* instead of clobbering it; updated locally as entries drain so
|
|
242
|
+
* multiple stashed quotes accumulate ([[HS-K27H-DIAG-0G86]] AC4 —
|
|
243
|
+
* replay must be genuinely lossless for the sync promise to be honest).
|
|
244
|
+
* @returns {Promise<{drained: number, quarantined: number, remaining: number}>}
|
|
245
|
+
*/
|
|
246
|
+
async function drainOutbox(vaultRoot, sessionId, identity, currentBuffer = {}) {
|
|
247
|
+
const pending = listOutbox(vaultRoot);
|
|
248
|
+
if (pending.length === 0) {
|
|
249
|
+
return { drained: 0, quarantined: 0, remaining: 0 };
|
|
250
|
+
}
|
|
251
|
+
let drained = 0;
|
|
252
|
+
let quarantined = 0;
|
|
253
|
+
let localBuffer =
|
|
254
|
+
currentBuffer && typeof currentBuffer === "object" ? currentBuffer : {};
|
|
255
|
+
for (const entry of pending) {
|
|
256
|
+
const patch = outboxEntryToPatch(entry, new Date().toISOString(), localBuffer);
|
|
257
|
+
if (!patch) {
|
|
258
|
+
removeOutboxEntry(entry.path); // unmappable — discard so it can't wedge the queue
|
|
259
|
+
continue;
|
|
260
|
+
}
|
|
261
|
+
const res = await patchCaptureSession(sessionId, {
|
|
262
|
+
patches: [patch],
|
|
263
|
+
attach: { agent: identity.agent, sessionId: identity.sessionId },
|
|
264
|
+
});
|
|
265
|
+
if (res.ok) {
|
|
266
|
+
removeOutboxEntry(entry.path);
|
|
267
|
+
drained += 1;
|
|
268
|
+
// Mirror the server's last-write-wins merge locally so the NEXT
|
|
269
|
+
// entry's append-shaped patch builds on this one.
|
|
270
|
+
localBuffer = { ...localBuffer, [patch.path]: patch.value };
|
|
271
|
+
continue;
|
|
272
|
+
}
|
|
273
|
+
if (res.error === CAPTURE_ERROR_CONFLICT) {
|
|
274
|
+
quarantineOutboxEntry(vaultRoot, entry.path, {
|
|
275
|
+
reason: "console_refused",
|
|
276
|
+
status: res.status,
|
|
277
|
+
consoleMessage: res.consoleMessage,
|
|
278
|
+
});
|
|
279
|
+
quarantined += 1;
|
|
280
|
+
continue;
|
|
281
|
+
}
|
|
282
|
+
// Transient (unreachable / 5xx) — leave remainder for next call.
|
|
283
|
+
// Do not drain any more entries after encountering a transient error
|
|
284
|
+
break;
|
|
285
|
+
}
|
|
286
|
+
return { drained, quarantined, remaining: outboxCount(vaultRoot) };
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
return { patchCaptureSession, drainOutbox };
|
|
290
|
+
}
|