wyrm-mcp 7.2.0 → 7.2.2
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/LICENSE +26 -667
- package/NOTICE +14 -33
- package/dist/activation.d.ts.map +1 -1
- package/dist/activation.js +1 -44
- package/dist/activation.js.map +1 -1
- package/dist/agent-daemon.js +4 -281
- package/dist/agent-loop.js +7 -332
- package/dist/analytics.js +13 -236
- package/dist/attribution.js +1 -49
- package/dist/audit.js +2 -457
- package/dist/auto-capture.js +3 -138
- package/dist/auto-orchestrator.js +1 -325
- package/dist/autoconfig.js +39 -840
- package/dist/buddy-runner.js +1 -109
- package/dist/buddy.js +14 -564
- package/dist/build-flags.js +1 -17
- package/dist/capabilities.js +3 -183
- package/dist/capture.js +1 -56
- package/dist/causality.js +6 -107
- package/dist/cli.js +20 -281
- package/dist/cloud/cli.js +5 -541
- package/dist/cloud/client.js +1 -221
- package/dist/cloud/crypto.js +1 -85
- package/dist/cloud/machine-id.js +2 -113
- package/dist/cloud/recovery.js +1 -60
- package/dist/cloud/sync-engine.js +7 -543
- package/dist/cloud-backup.js +5 -579
- package/dist/cloud-profile.js +1 -138
- package/dist/cloud-sync-entrypoint.js +1 -47
- package/dist/cloud-sync.js +2 -309
- package/dist/constellation.js +12 -168
- package/dist/context-build-budgeted.js +4 -144
- package/dist/context-ranking.js +1 -69
- package/dist/crypto.js +1 -179
- package/dist/daemon-write-endpoint.js +1 -290
- package/dist/daemon-writer.js +2 -406
- package/dist/database.js +43 -1110
- package/dist/deprecations.js +2 -162
- package/dist/design.js +13 -141
- package/dist/event-replication.js +1 -112
- package/dist/events-sse.js +7 -43
- package/dist/events.js +6 -238
- package/dist/failure-patterns.js +42 -659
- package/dist/federation.js +12 -236
- package/dist/goals.js +13 -101
- package/dist/golden.js +3 -355
- package/dist/handlers/agent.js +4 -165
- package/dist/handlers/alias-adapters.js +1 -129
- package/dist/handlers/aliases.js +1 -171
- package/dist/handlers/audit.js +1 -87
- package/dist/handlers/boundary.js +1 -221
- package/dist/handlers/capture.js +73 -1109
- package/dist/handlers/causality.js +7 -114
- package/dist/handlers/cloud.js +85 -382
- package/dist/handlers/companion.js +28 -459
- package/dist/handlers/datalake.js +7 -187
- package/dist/handlers/dispatch-context.js +0 -22
- package/dist/handlers/entity.js +25 -256
- package/dist/handlers/events.js +16 -335
- package/dist/handlers/failure.js +13 -340
- package/dist/handlers/goals.js +4 -296
- package/dist/handlers/intelligence.js +126 -674
- package/dist/handlers/invoicing.js +1 -70
- package/dist/handlers/mcpclient.js +6 -137
- package/dist/handlers/orchestration.js +40 -125
- package/dist/handlers/output-schemas.js +1 -24
- package/dist/handlers/presence.js +3 -99
- package/dist/handlers/project.js +28 -182
- package/dist/handlers/prompts.js +6 -157
- package/dist/handlers/quest.js +4 -224
- package/dist/handlers/recall.js +11 -218
- package/dist/handlers/registry.js +1 -167
- package/dist/handlers/resources.js +1 -288
- package/dist/handlers/review.js +11 -74
- package/dist/handlers/run.js +17 -487
- package/dist/handlers/search.js +15 -326
- package/dist/handlers/session.js +28 -615
- package/dist/handlers/share.js +8 -184
- package/dist/handlers/shims.js +1 -464
- package/dist/handlers/skill.js +67 -449
- package/dist/handlers/survivors.js +1 -120
- package/dist/handlers/symbols.js +8 -109
- package/dist/handlers/syncops.js +4 -302
- package/dist/handlers/types.js +1 -27
- package/dist/harvest.js +5 -191
- package/dist/hours.js +7 -156
- package/dist/http-auth.js +3 -321
- package/dist/http-fast.js +21 -1137
- package/dist/icons.js +1 -47
- package/dist/index.js +2 -924
- package/dist/indexer.js +4 -145
- package/dist/intelligence.js +31 -261
- package/dist/internal-dispatch.js +3 -212
- package/dist/keyset.js +1 -110
- package/dist/knowledge-graph.js +12 -176
- package/dist/license.d.ts +11 -0
- package/dist/license.d.ts.map +1 -1
- package/dist/license.js +2 -414
- package/dist/license.js.map +1 -1
- package/dist/logger.js +2 -199
- package/dist/maintenance.js +2 -148
- package/dist/mcp-client.js +6 -262
- package/dist/memory-artifacts.js +30 -449
- package/dist/migrate-prompt.js +2 -124
- package/dist/migrations.js +40 -655
- package/dist/performance.js +1 -228
- package/dist/presence.js +11 -140
- package/dist/priority-embed.js +5 -164
- package/dist/providers/embedding-provider.js +1 -196
- package/dist/readonly-gate.js +1 -29
- package/dist/rehydration.js +9 -157
- package/dist/reindex.js +1 -88
- package/dist/render-target.js +21 -514
- package/dist/render.js +4 -280
- package/dist/repl-guard.js +1 -173
- package/dist/replication-daemon-entrypoint.js +1 -31
- package/dist/replication-daemon.js +2 -262
- package/dist/resilience.js +1 -591
- package/dist/reverse-bridge.js +5 -360
- package/dist/security.js +1 -244
- package/dist/session-seen.js +3 -51
- package/dist/setup.js +1 -260
- package/dist/skill-author.js +5 -168
- package/dist/spec-kit.js +1 -191
- package/dist/sqlite-busy.js +1 -154
- package/dist/statusline.js +11 -315
- package/dist/sub-agent.js +13 -262
- package/dist/summarizer.js +13 -139
- package/dist/symbols.js +7 -283
- package/dist/sync.js +5 -359
- package/dist/tasks-dispatch.js +1 -84
- package/dist/tasks.js +1 -282
- package/dist/token-budget.js +1 -143
- package/dist/tool-analytics.js +7 -129
- package/dist/tool-annotations.js +1 -365
- package/dist/tool-manifest-v2.json +1 -1
- package/dist/tool-manifest.json +1 -1
- package/dist/tool-profiles.js +1 -75
- package/dist/trace-harvest.js +6 -244
- package/dist/types.js +1 -30
- package/dist/ui-dashboard.js +41 -50
- package/dist/ulid.js +1 -81
- package/dist/validate.js +1 -129
- package/dist/vault.js +1 -534
- package/dist/vectors.js +3 -184
- package/dist/version-check.js +4 -136
- package/dist/visibility.js +19 -155
- package/dist/wyrm-cli.js +98 -2451
- package/dist/wyrm-cli.js.map +1 -1
- package/dist/wyrm-guard.js +14 -424
- package/dist/wyrm-loop.js +3 -150
- package/dist/wyrm-manifest.json +1 -1
- package/dist/wyrm-statusline-daemon.js +1 -11
- package/dist/wyrm-statusline.js +4 -56
- package/dist/wyrm-ui.js +9 -77
- package/package.json +4 -2
package/dist/daemon-writer.js
CHANGED
|
@@ -1,406 +1,2 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
*
|
|
4
|
-
* Article I — the stdio direct path stays FIRST-CLASS and is the default:
|
|
5
|
-
* with the env var unset (the default), nothing here ever touches the
|
|
6
|
-
* network; every write runs the same local synchronous better-sqlite3 path
|
|
7
|
-
* it always has. When opted in, the canonical write paths (data-lake insert,
|
|
8
|
-
* failure_record, truth_set, capture) route their WRITES over loopback HTTP
|
|
9
|
-
* to the running daemon's `POST /write` (daemon-write-endpoint.ts), funneling
|
|
10
|
-
* a multi-process fleet's writes through ONE writer process so the others
|
|
11
|
-
* stop contending for the SQLite write lock. READS always stay local.
|
|
12
|
-
*
|
|
13
|
-
* FAIL-DIRECT (the documented Article I choice, not fail-loud) — but ONLY on
|
|
14
|
-
* outcomes that prove the daemon did NOT commit (v7 F2 review fix): the
|
|
15
|
-
* daemon absent (connection refused — the request was never delivered),
|
|
16
|
-
* unauthorized (401), op-rejected ({e}), or the daemon's own structured BUSY
|
|
17
|
-
* report (503 — the write threw before committing). Those writes fall back to
|
|
18
|
-
* the local direct path with a ONE-TIME stderr warning naming the reason (the
|
|
19
|
-
* warnStderr discipline: stdout is the MCP wire). Rationale: Wyrm is memory;
|
|
20
|
-
* an unreachable daemon must never cost a memory write, and the direct path
|
|
21
|
-
* is guaranteed to work offline (Article I).
|
|
22
|
-
*
|
|
23
|
-
* AMBIGUOUS outcomes are NOT fail-direct (v7 F2 review fix — Article VI data
|
|
24
|
-
* safety): a timed-out request or a lost response may have been delivered and
|
|
25
|
-
* COMMITTED daemon-side (the daemon's busy_timeout is 5000ms — longer than
|
|
26
|
-
* this client's 2000ms first budget — and a client-side abort cannot cancel a
|
|
27
|
-
* synchronous better-sqlite3 write already executing). Silently writing
|
|
28
|
-
* direct on top of that double-writes canonical memory: duplicate artifact/
|
|
29
|
-
* quest/data rows, a spurious truth supersession, double-counted failure
|
|
30
|
-
* occurrences. Instead:
|
|
31
|
-
* 1. Every forwarded write carries a client-minted ULID `write_id`
|
|
32
|
-
* (idempotency key). The daemon replays — never re-executes — a
|
|
33
|
-
* write_id it already committed (daemon-write-endpoint.ts).
|
|
34
|
-
* 2. An ambiguous first attempt is RE-SENT with the SAME write_id under an
|
|
35
|
-
* extended budget that outlives one full daemon busy_timeout window
|
|
36
|
-
* (DAEMON_WRITE_RETRY_TIMEOUT_MS) — a live daemon answers it
|
|
37
|
-
* definitively (replayed commit, fresh execution, or a rejection that
|
|
38
|
-
* proves no commit happened).
|
|
39
|
-
* 3. Still ambiguous after the re-send → throw a structured
|
|
40
|
-
* WYRM_DAEMON_AMBIGUOUS error (the dispatcher maps it to the WYRM_BUSY-
|
|
41
|
-
* style retryable body). The pending write_id AND the exact body bytes
|
|
42
|
-
* first sent are remembered, keyed by write content (or by the caller's
|
|
43
|
-
* idempotencyMaterial — see below) within a bounded TTL
|
|
44
|
-
* (PENDING_WRITE_TTL_MS), so the caller's instructed retry re-enters
|
|
45
|
-
* with the same key, re-sends the ORIGINAL bytes, and the daemon still
|
|
46
|
-
* cannot apply it twice. Entries expire on read: a breadcrumb older
|
|
47
|
-
* than the TTL is never adopted — a genuinely new identical-content
|
|
48
|
-
* write hours later mints fresh instead of silently replaying a stale
|
|
49
|
-
* committed row. Deletes are GUARDED by write_id, so a concurrent
|
|
50
|
-
* identical-content call's definitive outcome cannot destroy another
|
|
51
|
-
* call's breadcrumb.
|
|
52
|
-
* 4. Bounded retry, deterministic errors surface: a daemon-side
|
|
53
|
-
* SQLITE_CONSTRAINT throw (provably rolled back — every op is one
|
|
54
|
-
* statement or one IMMEDIATE transaction) arrives as the rejected {e}
|
|
55
|
-
* shape and FAILS DIRECT, so the real domain error (a CHECK-violating
|
|
56
|
-
* enum, a stale FK) surfaces through the local path exactly as in
|
|
57
|
-
* non-daemon mode. Any write that stays ambiguous for
|
|
58
|
-
* DAEMON_AMBIGUOUS_CYCLE_CAP consecutive instructed-retry cycles throws
|
|
59
|
-
* a NON-retryable error naming the last daemon response — a persistent
|
|
60
|
-
* environmental 5xx (SQLITE_FULL, SQLITE_READONLY) becomes a loud
|
|
61
|
-
* error, never an unbounded retry livelock.
|
|
62
|
-
* Idempotency identity vs encryption: data_insert under WYRM_ENCRYPTION_KEY
|
|
63
|
-
* produces a FRESH ciphertext per call (random salt + IV), so the wire
|
|
64
|
-
* input can never key the retry contract. The caller passes the
|
|
65
|
-
* PRE-encryption tuple as idempotencyMaterial; the pending entry's stored
|
|
66
|
-
* body bytes guarantee the retry re-sends the original ciphertext, keeping
|
|
67
|
-
* the daemon-side row identical across retries.
|
|
68
|
-
* Residual at-least-once windows (documented):
|
|
69
|
-
* - the daemon crashing after commit but before any response — its
|
|
70
|
-
* in-memory dedup ledger dies with it, and a later fail-direct
|
|
71
|
-
* (connection refused) can then land a second row;
|
|
72
|
-
* - a caller-level retry arriving after the pending breadcrumb's TTL or
|
|
73
|
-
* the daemon ledger's retention horizon (daemon-write-endpoint.ts
|
|
74
|
-
* DEDUP_TTL_MS) — the write executes fresh and VISIBLY (a possible
|
|
75
|
-
* duplicate row), never a silent stale replay;
|
|
76
|
-
* - within the pending TTL, a genuinely NEW logical write whose content
|
|
77
|
-
* is byte-identical to a still-pending ambiguous one shares its retry
|
|
78
|
-
* contract (they are indistinguishable on the wire) — the TTL bounds
|
|
79
|
-
* this window to seconds, not hours.
|
|
80
|
-
*
|
|
81
|
-
* Article VII — minimal env, token required, loopback only:
|
|
82
|
-
* - Target host is HARDCODED to 127.0.0.1 (the 6.14.1 bind default). There is
|
|
83
|
-
* deliberately NO host env var: writes-over-network beyond loopback is not
|
|
84
|
-
* a thing this module can be configured into.
|
|
85
|
-
* - Bearer token comes from WYRM_TOKEN — the SAME env contract the hook
|
|
86
|
-
* clients (scripts/hooks/wyrm-push.py) already use for the http-auth token.
|
|
87
|
-
* No token → we never even attempt the daemon (no unauthenticated attempts,
|
|
88
|
-
* and the dashboard's X-Wyrm-Origin:ui trust path is NOT used here).
|
|
89
|
-
* - Env surface is exactly: WYRM_DAEMON_WRITES, WYRM_TOKEN, WYRM_PORT.
|
|
90
|
-
*
|
|
91
|
-
* Attribution (v7 F2 review fix): the caller's ambient actor envelope (T009)
|
|
92
|
-
* rides IN THE REQUEST BODY as `actor: {agent_id, run_id}` — always, even
|
|
93
|
-
* when both are null — so a run-only envelope (run_id without agent_id, which
|
|
94
|
-
* the `Wyrm-Actor: agent_id[;run_id]` header grammar cannot express) survives
|
|
95
|
-
* the hop, and the daemon NEVER substitutes its own process env for a
|
|
96
|
-
* forwarded write (an absent caller identity stays NULL/legacy instead of
|
|
97
|
-
* being stamped with the daemon's WYRM_RUN_ID, which would silently flip the
|
|
98
|
-
* T014 quarantine tier). The header is still sent when agent_id is known, for
|
|
99
|
-
* older daemons that predate the body envelope.
|
|
100
|
-
*
|
|
101
|
-
* @copyright 2026 Ghost Protocol (Pvt) Ltd.
|
|
102
|
-
* @license AGPL-3.0-or-later — dual-licensed; commercial terms: ghosts.lk@proton.me. See LICENSE.
|
|
103
|
-
*/
|
|
104
|
-
import { getActor } from './handlers/boundary.js';
|
|
105
|
-
import { ulid } from './ulid.js';
|
|
106
|
-
import { WYRM_DAEMON_AMBIGUOUS_CODE } from './sqlite-busy.js';
|
|
107
|
-
/**
|
|
108
|
-
* Hard per-write budget for the FIRST attempt. Loopback HTTP to a healthy
|
|
109
|
-
* daemon is single-digit milliseconds; 2s only matters when the daemon itself
|
|
110
|
-
* is stuck mid busy_timeout — past that the attempt is AMBIGUOUS (the daemon
|
|
111
|
-
* may still commit) and the re-send protocol below takes over.
|
|
112
|
-
*/
|
|
113
|
-
export const DAEMON_WRITE_TIMEOUT_MS = 2000;
|
|
114
|
-
/**
|
|
115
|
-
* Budget for the same-write_id RE-SEND after an ambiguous first attempt.
|
|
116
|
-
* Deliberately longer than the daemon's busy_timeout=5000 (database.ts): a
|
|
117
|
-
* daemon stuck in one full busy window — the realistic ambiguity — finishes
|
|
118
|
-
* inside this budget and answers the re-send definitively (replay or reject),
|
|
119
|
-
* turning commit-after-abort from a silent double-write into an observed
|
|
120
|
-
* outcome.
|
|
121
|
-
*/
|
|
122
|
-
export const DAEMON_WRITE_RETRY_TIMEOUT_MS = 6000;
|
|
123
|
-
/**
|
|
124
|
-
* How long an AMBIGUOUS write's breadcrumb (write_id + first-sent bytes)
|
|
125
|
-
* stays adoptable. It only needs to span the instructed-retry window (the
|
|
126
|
-
* structured body advises retry_after_ms=1000 plus an agent turn), and every
|
|
127
|
-
* further ambiguous cycle refreshes it. Past the TTL a byte-identical call is
|
|
128
|
-
* a NEW logical write and mints fresh — the alternative (no expiry) let a
|
|
129
|
-
* stale breadcrumb silently replay an hours-old committed row for a
|
|
130
|
-
* genuinely new identical-content write (failure accrual is the norm there).
|
|
131
|
-
*/
|
|
132
|
-
export const PENDING_WRITE_TTL_MS = 5 * DAEMON_WRITE_RETRY_TIMEOUT_MS;
|
|
133
|
-
/**
|
|
134
|
-
* Consecutive ambiguous instructed-retry cycles allowed per pending write
|
|
135
|
-
* before the structured retryable error gives way to a NON-retryable one. A
|
|
136
|
-
* transient daemon stall resolves in one or two cycles; past the cap the
|
|
137
|
-
* cause is persistent (an environmental daemon-side error: SQLITE_FULL,
|
|
138
|
-
* SQLITE_READONLY, a wedged event loop) and instructing further retries is a
|
|
139
|
-
* livelock, not a recovery path.
|
|
140
|
-
*/
|
|
141
|
-
export const DAEMON_AMBIGUOUS_CYCLE_CAP = 3;
|
|
142
|
-
/** The non-retryable terminal code for a write that exhausted its ambiguous
|
|
143
|
-
* cycles. Deliberately NOT classified by retryableWriteCause: the dispatcher
|
|
144
|
-
* must surface it loudly through the generic error tail, not re-instruct a
|
|
145
|
-
* retry. */
|
|
146
|
-
export const WYRM_DAEMON_RETRY_EXHAUSTED_CODE = 'WYRM_DAEMON_RETRY_EXHAUSTED';
|
|
147
|
-
export function daemonWritesEnabled(env = process.env) {
|
|
148
|
-
return env.WYRM_DAEMON_WRITES === '1';
|
|
149
|
-
}
|
|
150
|
-
// One-time-per-process fallback warning (the deprecations.ts discipline):
|
|
151
|
-
// the first fallback names its reason on stderr, later ones stay silent so a
|
|
152
|
-
// daemon-less opt-in session doesn't spam every single write.
|
|
153
|
-
let warnedFallback = false;
|
|
154
|
-
const pendingWriteIds = new Map();
|
|
155
|
-
const PENDING_WRITE_CAP = 256;
|
|
156
|
-
/** Test hook: reset the one-time warning + pending-key state. */
|
|
157
|
-
export function _resetDaemonWriterForTests() {
|
|
158
|
-
warnedFallback = false;
|
|
159
|
-
pendingWriteIds.clear();
|
|
160
|
-
}
|
|
161
|
-
/** Test hook: age every pending breadcrumb by `ms` (drives TTL expiry without a clock). */
|
|
162
|
-
export function _agePendingWritesForTests(ms) {
|
|
163
|
-
for (const entry of pendingWriteIds.values())
|
|
164
|
-
entry.expiresAt -= ms;
|
|
165
|
-
}
|
|
166
|
-
function warnFallbackOnce(reason) {
|
|
167
|
-
if (warnedFallback)
|
|
168
|
-
return;
|
|
169
|
-
warnedFallback = true;
|
|
170
|
-
try {
|
|
171
|
-
process.stderr.write(`[wyrm] WYRM_DAEMON_WRITES=1 but the daemon write path was not used (${reason}); ` +
|
|
172
|
-
`falling back to the local direct write path (Article I). Further fallbacks this process are silent.\n`);
|
|
173
|
-
}
|
|
174
|
-
catch { /* warnings must never break a write */ }
|
|
175
|
-
}
|
|
176
|
-
function rememberPending(key, entry) {
|
|
177
|
-
if (!pendingWriteIds.has(key) && pendingWriteIds.size >= PENDING_WRITE_CAP) {
|
|
178
|
-
const oldest = pendingWriteIds.keys().next().value;
|
|
179
|
-
if (oldest !== undefined)
|
|
180
|
-
pendingWriteIds.delete(oldest);
|
|
181
|
-
}
|
|
182
|
-
pendingWriteIds.set(key, entry);
|
|
183
|
-
}
|
|
184
|
-
/** Adopt a pending breadcrumb only while it is fresh; a lapsed one is deleted
|
|
185
|
-
* so the caller mints a NEW write_id (a visible fresh execution — never the
|
|
186
|
-
* silent stale replay an unexpiring breadcrumb produced). */
|
|
187
|
-
function freshPending(key) {
|
|
188
|
-
const entry = pendingWriteIds.get(key);
|
|
189
|
-
if (entry === undefined)
|
|
190
|
-
return null;
|
|
191
|
-
if (Date.now() >= entry.expiresAt) {
|
|
192
|
-
pendingWriteIds.delete(key);
|
|
193
|
-
return null;
|
|
194
|
-
}
|
|
195
|
-
return entry;
|
|
196
|
-
}
|
|
197
|
-
/** Delete the breadcrumb only if it still belongs to THIS call's write_id —
|
|
198
|
-
* a concurrent identical-content call that minted its own id must not have
|
|
199
|
-
* its definitive outcome destroy another call's retry contract. */
|
|
200
|
-
function clearPendingIfOwn(key, writeId) {
|
|
201
|
-
if (pendingWriteIds.get(key)?.writeId === writeId)
|
|
202
|
-
pendingWriteIds.delete(key);
|
|
203
|
-
}
|
|
204
|
-
/** True when the socket never connected — the request was provably not
|
|
205
|
-
* delivered, so the daemon cannot have committed anything. fetch()/undici
|
|
206
|
-
* wraps the socket error in `.cause` (sometimes an AggregateError). */
|
|
207
|
-
function isConnectionNeverEstablished(err) {
|
|
208
|
-
const codes = new Set(['ECONNREFUSED', 'ENOTFOUND', 'EAI_AGAIN']);
|
|
209
|
-
const seen = new Set();
|
|
210
|
-
const stack = [err];
|
|
211
|
-
while (stack.length > 0) {
|
|
212
|
-
const e = stack.pop();
|
|
213
|
-
if (e === null || e === undefined || typeof e !== 'object' || seen.has(e))
|
|
214
|
-
continue;
|
|
215
|
-
seen.add(e);
|
|
216
|
-
const code = e.code;
|
|
217
|
-
if (typeof code === 'string' && codes.has(code))
|
|
218
|
-
return true;
|
|
219
|
-
const cause = e.cause;
|
|
220
|
-
if (cause)
|
|
221
|
-
stack.push(cause);
|
|
222
|
-
const errors = e.errors;
|
|
223
|
-
if (Array.isArray(errors))
|
|
224
|
-
stack.push(...errors);
|
|
225
|
-
}
|
|
226
|
-
return false;
|
|
227
|
-
}
|
|
228
|
-
async function attemptWrite(port, headers, body, timeoutMs) {
|
|
229
|
-
const ctrl = new AbortController();
|
|
230
|
-
const timer = setTimeout(() => ctrl.abort(), timeoutMs);
|
|
231
|
-
try {
|
|
232
|
-
const res = await fetch(`http://127.0.0.1:${port}/write`, {
|
|
233
|
-
method: 'POST',
|
|
234
|
-
headers,
|
|
235
|
-
body,
|
|
236
|
-
signal: ctrl.signal,
|
|
237
|
-
});
|
|
238
|
-
let parsed = null;
|
|
239
|
-
try {
|
|
240
|
-
parsed = await res.json();
|
|
241
|
-
}
|
|
242
|
-
catch (err) {
|
|
243
|
-
// The response started but its body was lost/garbled. The STATUS still
|
|
244
|
-
// classifies: a 2xx means the daemon committed BEFORE responding, and a
|
|
245
|
-
// non-503 5xx proves nothing about the commit — both are ambiguous (the
|
|
246
|
-
// same rule the parseable-body branch below applies; an unreadable body
|
|
247
|
-
// must not flip the commit-state conclusion). 4xx/503 prove no commit.
|
|
248
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
249
|
-
return res.ok || (res.status >= 500 && res.status !== 503)
|
|
250
|
-
? { kind: 'ambiguous', reason: res.ok ? `daemon 2xx response body unreadable: ${msg}` : `daemon answered HTTP ${res.status}` }
|
|
251
|
-
: { kind: 'rejected', reason: `daemon answered HTTP ${res.status}` };
|
|
252
|
-
}
|
|
253
|
-
if (!res.ok) {
|
|
254
|
-
// 4xx (auth/validation, pre-execution) and 503 (the daemon's own
|
|
255
|
-
// structured BUSY report — the write threw before committing) prove no
|
|
256
|
-
// commit. Any other 5xx leaves the commit state unknown.
|
|
257
|
-
return res.status >= 500 && res.status !== 503
|
|
258
|
-
? { kind: 'ambiguous', reason: `daemon answered HTTP ${res.status}` }
|
|
259
|
-
: { kind: 'rejected', reason: `daemon answered HTTP ${res.status}` };
|
|
260
|
-
}
|
|
261
|
-
if (!parsed || parsed.ok !== true || parsed.row == null) {
|
|
262
|
-
return { kind: 'rejected', reason: `daemon rejected the write: ${parsed?.e ?? 'malformed response'}` };
|
|
263
|
-
}
|
|
264
|
-
return { kind: 'ok', row: parsed.row };
|
|
265
|
-
}
|
|
266
|
-
catch (err) {
|
|
267
|
-
const reason = err instanceof Error ? err.message : String(err);
|
|
268
|
-
if (isConnectionNeverEstablished(err)) {
|
|
269
|
-
return { kind: 'rejected', reason: `daemon unreachable: ${reason}`, neverSent: true };
|
|
270
|
-
}
|
|
271
|
-
// Abort (our timeout), ECONNRESET, socket hang-up, …: the request may
|
|
272
|
-
// have been delivered and may still commit.
|
|
273
|
-
return { kind: 'ambiguous', reason: `no response from the daemon: ${reason}` };
|
|
274
|
-
}
|
|
275
|
-
finally {
|
|
276
|
-
clearTimeout(timer);
|
|
277
|
-
}
|
|
278
|
-
}
|
|
279
|
-
/**
|
|
280
|
-
* Forward one canonical write to the daemon.
|
|
281
|
-
* - the written row → the daemon committed it (exactly once);
|
|
282
|
-
* - null → take the direct path (PROVEN no daemon commit:
|
|
283
|
-
* disabled, no token, never connected, 401/4xx/503,
|
|
284
|
-
* op rejected — including a daemon-side
|
|
285
|
-
* SQLITE_CONSTRAINT rollback riding the {e} shape);
|
|
286
|
-
* - throws WYRM_DAEMON_AMBIGUOUS → outcome unknown even after the same-key
|
|
287
|
-
* re-send; the caller must NOT write directly (Article VI — possible
|
|
288
|
-
* double-write). The dispatcher maps it to the structured retryable body;
|
|
289
|
-
* the instructed retry re-enters with the SAME idempotency key and the
|
|
290
|
-
* SAME first-sent bytes (within PENDING_WRITE_TTL_MS);
|
|
291
|
-
* - throws WYRM_DAEMON_RETRY_EXHAUSTED → still ambiguous after
|
|
292
|
-
* DAEMON_AMBIGUOUS_CYCLE_CAP instructed-retry cycles; NON-retryable, names
|
|
293
|
-
* the last daemon response. The breadcrumb is kept (within its TTL) so an
|
|
294
|
-
* insistent caller still cannot land the write twice.
|
|
295
|
-
*
|
|
296
|
-
* `opts.idempotencyMaterial`: the PRODUCTION identity seam for ops whose wire
|
|
297
|
-
* input is non-deterministic (data_insert's per-call ciphertext) — the
|
|
298
|
-
* pending key derives from this material instead of `input`, and the stored
|
|
299
|
-
* body bytes make the retry re-send the original ciphertext.
|
|
300
|
-
*/
|
|
301
|
-
export async function forwardWrite(op, projectId, input, env = process.env, opts) {
|
|
302
|
-
if (!daemonWritesEnabled(env))
|
|
303
|
-
return null;
|
|
304
|
-
const token = env.WYRM_TOKEN;
|
|
305
|
-
if (!token) {
|
|
306
|
-
warnFallbackOnce('WYRM_TOKEN is not set — the http-auth Bearer token is required');
|
|
307
|
-
return null;
|
|
308
|
-
}
|
|
309
|
-
const port = parseInt(env.WYRM_PORT || '3333', 10) || 3333;
|
|
310
|
-
const headers = {
|
|
311
|
-
'Content-Type': 'application/json',
|
|
312
|
-
'Authorization': `Bearer ${token}`,
|
|
313
|
-
};
|
|
314
|
-
// Attribution rides in the body (see the module header); the legacy header
|
|
315
|
-
// form is kept for daemons that predate the body envelope (it cannot carry
|
|
316
|
-
// a run-only envelope — that is exactly what the body fixes).
|
|
317
|
-
const actor = getActor();
|
|
318
|
-
if (actor.agent_id) {
|
|
319
|
-
headers['Wyrm-Actor'] = actor.run_id ? `${actor.agent_id};${actor.run_id}` : actor.agent_id;
|
|
320
|
-
}
|
|
321
|
-
// Idempotency key: ONE ULID per logical write. A write whose earlier
|
|
322
|
-
// attempt ended AMBIGUOUS keeps its key AND its exact first-sent bytes
|
|
323
|
-
// (pendingWriteIds, TTL-bounded), so the caller-level retry the structured
|
|
324
|
-
// error instructs cannot land twice — even when a field of `input` is
|
|
325
|
-
// non-deterministic per call (the pending key derives from
|
|
326
|
-
// idempotencyMaterial when given, and the stored body is what gets
|
|
327
|
-
// re-sent, never a re-serialization).
|
|
328
|
-
const pendingKey = JSON.stringify([
|
|
329
|
-
op, projectId, opts?.idempotencyMaterial ?? input, actor.agent_id, actor.run_id,
|
|
330
|
-
]);
|
|
331
|
-
const pending = freshPending(pendingKey);
|
|
332
|
-
const writeId = pending?.writeId ?? ulid();
|
|
333
|
-
const body = pending?.body ?? JSON.stringify({
|
|
334
|
-
op,
|
|
335
|
-
project_id: projectId,
|
|
336
|
-
input,
|
|
337
|
-
write_id: writeId,
|
|
338
|
-
actor: { agent_id: actor.agent_id, run_id: actor.run_id },
|
|
339
|
-
});
|
|
340
|
-
let attempt = await attemptWrite(port, headers, body, opts?.timeoutMs ?? DAEMON_WRITE_TIMEOUT_MS);
|
|
341
|
-
if (attempt.kind === 'ambiguous') {
|
|
342
|
-
// The request may have been delivered and may still commit. Re-send the
|
|
343
|
-
// SAME write_id under a budget that outlives one full daemon busy window:
|
|
344
|
-
// a live daemon answers definitively (replayed commit / fresh execution /
|
|
345
|
-
// provable rejection) — exactly once either way.
|
|
346
|
-
const resend = await attemptWrite(port, headers, body, opts?.resendTimeoutMs ?? DAEMON_WRITE_RETRY_TIMEOUT_MS);
|
|
347
|
-
attempt = resend.kind === 'rejected' && resend.neverSent
|
|
348
|
-
// The daemon vanished BETWEEN an ambiguous attempt and the re-send —
|
|
349
|
-
// the first attempt's commit state is unknowable, so this is NOT the
|
|
350
|
-
// safe fail-direct refusal, it stays ambiguous.
|
|
351
|
-
? { kind: 'ambiguous', reason: `daemon vanished after an ambiguous write attempt (${resend.reason})` }
|
|
352
|
-
: resend;
|
|
353
|
-
}
|
|
354
|
-
if (attempt.kind === 'ok') {
|
|
355
|
-
clearPendingIfOwn(pendingKey, writeId);
|
|
356
|
-
return attempt.row;
|
|
357
|
-
}
|
|
358
|
-
if (attempt.kind === 'rejected') {
|
|
359
|
-
clearPendingIfOwn(pendingKey, writeId);
|
|
360
|
-
warnFallbackOnce(attempt.reason);
|
|
361
|
-
return null;
|
|
362
|
-
}
|
|
363
|
-
// Still ambiguous: refuse the silent local fallback (it could double-write
|
|
364
|
-
// canonical memory). Refresh the breadcrumb (TTL + cycle count) and surface
|
|
365
|
-
// the structured retryable error — until the cycle cap, past which the
|
|
366
|
-
// outcome is a loud NON-retryable terminal error (the cause is persistent;
|
|
367
|
-
// instructing more identical retries is a livelock, not recovery).
|
|
368
|
-
const ambiguousCycles = (pending?.ambiguousCycles ?? 0) + 1;
|
|
369
|
-
rememberPending(pendingKey, {
|
|
370
|
-
writeId, body, ambiguousCycles,
|
|
371
|
-
expiresAt: Date.now() + PENDING_WRITE_TTL_MS,
|
|
372
|
-
});
|
|
373
|
-
if (ambiguousCycles >= DAEMON_AMBIGUOUS_CYCLE_CAP) {
|
|
374
|
-
throw Object.assign(new Error(`daemon write for ${op} stayed ambiguous after ${ambiguousCycles} retry cycles ` +
|
|
375
|
-
`(last daemon response: ${attempt.reason}); giving up on the retry protocol — ` +
|
|
376
|
-
`the daemon is persistently failing this write (check the daemon's logs/disk), ` +
|
|
377
|
-
`and the direct fallback stays withheld to avoid a possible duplicate`), { code: WYRM_DAEMON_RETRY_EXHAUSTED_CODE });
|
|
378
|
-
}
|
|
379
|
-
throw Object.assign(new Error(`daemon write outcome unknown for ${op} (${attempt.reason}); ` +
|
|
380
|
-
`refusing the direct fallback to avoid a possible duplicate — retry the same call`), { code: WYRM_DAEMON_AMBIGUOUS_CODE });
|
|
381
|
-
}
|
|
382
|
-
/**
|
|
383
|
-
* The one-line seam the canonical write sites use:
|
|
384
|
-
*
|
|
385
|
-
* const row = await daemonOr('artifact_add', proj.id, input, () => memory.add(proj.id, input));
|
|
386
|
-
*
|
|
387
|
-
* Daemon mode returns the daemon-written row (same shape — the endpoint calls
|
|
388
|
-
* the same domain method); a PROVEN-no-commit daemon failure runs the local
|
|
389
|
-
* `direct()` thunk (Article I — the direct path is the default and the
|
|
390
|
-
* fallback); an AMBIGUOUS daemon outcome propagates as a structured
|
|
391
|
-
* WYRM_DAEMON_AMBIGUOUS error so the caller retries instead of double-writing
|
|
392
|
-
* (Article VI — see the module header for the exactly-once protocol and its
|
|
393
|
-
* residual windows).
|
|
394
|
-
*
|
|
395
|
-
* `idempotencyMaterial`: pass the STABLE pre-transformation identity when
|
|
396
|
-
* `input` carries a non-deterministic field (data_insert's ciphertext is
|
|
397
|
-
* fresh per call) — it keys the caller-level retry contract so the retry
|
|
398
|
-
* resumes the same write_id and re-sends the same bytes.
|
|
399
|
-
*/
|
|
400
|
-
export async function daemonOr(op, projectId, input, direct, idempotencyMaterial) {
|
|
401
|
-
const row = await forwardWrite(op, projectId, input, process.env, idempotencyMaterial ? { idempotencyMaterial } : undefined);
|
|
402
|
-
if (row != null)
|
|
403
|
-
return row;
|
|
404
|
-
return direct();
|
|
405
|
-
}
|
|
406
|
-
//# sourceMappingURL=daemon-writer.js.map
|
|
1
|
+
import{getActor as k}from"./handlers/boundary.js";import{ulid as y}from"./ulid.js";import{WYRM_DAEMON_AMBIGUOUS_CODE as O}from"./sqlite-busy.js";const A=2e3,h=6e3,M=5*h,N=3,R="WYRM_DAEMON_RETRY_EXHAUSTED";function W(e=process.env){return e.WYRM_DAEMON_WRITES==="1"}let g=!1;const c=new Map,I=256;function Y(){g=!1,c.clear()}function v(e){for(const n of c.values())n.expiresAt-=e}function E(e){if(!g){g=!0;try{process.stderr.write(`[wyrm] WYRM_DAEMON_WRITES=1 but the daemon write path was not used (${e}); falling back to the local direct write path (Article I). Further fallbacks this process are silent.
|
|
2
|
+
`)}catch{}}}function D(e,n){if(!c.has(e)&&c.size>=I){const i=c.keys().next().value;i!==void 0&&c.delete(i)}c.set(e,n)}function S(e){const n=c.get(e);return n===void 0?null:Date.now()>=n.expiresAt?(c.delete(e),null):n}function T(e,n){c.get(e)?.writeId===n&&c.delete(e)}function $(e){const n=new Set(["ECONNREFUSED","ENOTFOUND","EAI_AGAIN"]),i=new Set,a=[e];for(;a.length>0;){const r=a.pop();if(r==null||typeof r!="object"||i.has(r))continue;i.add(r);const d=r.code;if(typeof d=="string"&&n.has(d))return!0;const t=r.cause;t&&a.push(t);const o=r.errors;Array.isArray(o)&&a.push(...o)}return!1}async function b(e,n,i,a){const r=new AbortController,d=setTimeout(()=>r.abort(),a);try{const t=await fetch(`http://127.0.0.1:${e}/write`,{method:"POST",headers:n,body:i,signal:r.signal});let o=null;try{o=await t.json()}catch(s){const l=s instanceof Error?s.message:String(s);return t.ok||t.status>=500&&t.status!==503?{kind:"ambiguous",reason:t.ok?`daemon 2xx response body unreadable: ${l}`:`daemon answered HTTP ${t.status}`}:{kind:"rejected",reason:`daemon answered HTTP ${t.status}`}}return t.ok?!o||o.ok!==!0||o.row==null?{kind:"rejected",reason:`daemon rejected the write: ${o?.e??"malformed response"}`}:{kind:"ok",row:o.row}:t.status>=500&&t.status!==503?{kind:"ambiguous",reason:`daemon answered HTTP ${t.status}`}:{kind:"rejected",reason:`daemon answered HTTP ${t.status}`}}catch(t){const o=t instanceof Error?t.message:String(t);return $(t)?{kind:"rejected",reason:`daemon unreachable: ${o}`,neverSent:!0}:{kind:"ambiguous",reason:`no response from the daemon: ${o}`}}finally{clearTimeout(d)}}async function x(e,n,i,a=process.env,r){if(!W(a))return null;const d=a.WYRM_TOKEN;if(!d)return E("WYRM_TOKEN is not set \u2014 the http-auth Bearer token is required"),null;const t=parseInt(a.WYRM_PORT||"3333",10)||3333,o={"Content-Type":"application/json",Authorization:`Bearer ${d}`},s=k();s.agent_id&&(o["Wyrm-Actor"]=s.run_id?`${s.agent_id};${s.run_id}`:s.agent_id);const l=JSON.stringify([e,n,r?.idempotencyMaterial??i,s.agent_id,s.run_id]),p=S(l),f=p?.writeId??y(),_=p?.body??JSON.stringify({op:e,project_id:n,input:i,write_id:f,actor:{agent_id:s.agent_id,run_id:s.run_id}});let u=await b(t,o,_,r?.timeoutMs??A);if(u.kind==="ambiguous"){const m=await b(t,o,_,r?.resendTimeoutMs??h);u=m.kind==="rejected"&&m.neverSent?{kind:"ambiguous",reason:`daemon vanished after an ambiguous write attempt (${m.reason})`}:m}if(u.kind==="ok")return T(l,f),u.row;if(u.kind==="rejected")return T(l,f),E(u.reason),null;const w=(p?.ambiguousCycles??0)+1;throw D(l,{writeId:f,body:_,ambiguousCycles:w,expiresAt:Date.now()+M}),w>=N?Object.assign(new Error(`daemon write for ${e} stayed ambiguous after ${w} retry cycles (last daemon response: ${u.reason}); giving up on the retry protocol \u2014 the daemon is persistently failing this write (check the daemon's logs/disk), and the direct fallback stays withheld to avoid a possible duplicate`),{code:R}):Object.assign(new Error(`daemon write outcome unknown for ${e} (${u.reason}); refusing the direct fallback to avoid a possible duplicate \u2014 retry the same call`),{code:O})}async function U(e,n,i,a,r){const d=await x(e,n,i,process.env,r?{idempotencyMaterial:r}:void 0);return d??a()}export{N as DAEMON_AMBIGUOUS_CYCLE_CAP,h as DAEMON_WRITE_RETRY_TIMEOUT_MS,A as DAEMON_WRITE_TIMEOUT_MS,M as PENDING_WRITE_TTL_MS,R as WYRM_DAEMON_RETRY_EXHAUSTED_CODE,v as _agePendingWritesForTests,Y as _resetDaemonWriterForTests,U as daemonOr,W as daemonWritesEnabled,x as forwardWrite};
|