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.
Files changed (156) hide show
  1. package/LICENSE +26 -667
  2. package/NOTICE +14 -33
  3. package/dist/activation.d.ts.map +1 -1
  4. package/dist/activation.js +1 -44
  5. package/dist/activation.js.map +1 -1
  6. package/dist/agent-daemon.js +4 -281
  7. package/dist/agent-loop.js +7 -332
  8. package/dist/analytics.js +13 -236
  9. package/dist/attribution.js +1 -49
  10. package/dist/audit.js +2 -457
  11. package/dist/auto-capture.js +3 -138
  12. package/dist/auto-orchestrator.js +1 -325
  13. package/dist/autoconfig.js +39 -840
  14. package/dist/buddy-runner.js +1 -109
  15. package/dist/buddy.js +14 -564
  16. package/dist/build-flags.js +1 -17
  17. package/dist/capabilities.js +3 -183
  18. package/dist/capture.js +1 -56
  19. package/dist/causality.js +6 -107
  20. package/dist/cli.js +20 -281
  21. package/dist/cloud/cli.js +5 -541
  22. package/dist/cloud/client.js +1 -221
  23. package/dist/cloud/crypto.js +1 -85
  24. package/dist/cloud/machine-id.js +2 -113
  25. package/dist/cloud/recovery.js +1 -60
  26. package/dist/cloud/sync-engine.js +7 -543
  27. package/dist/cloud-backup.js +5 -579
  28. package/dist/cloud-profile.js +1 -138
  29. package/dist/cloud-sync-entrypoint.js +1 -47
  30. package/dist/cloud-sync.js +2 -309
  31. package/dist/constellation.js +12 -168
  32. package/dist/context-build-budgeted.js +4 -144
  33. package/dist/context-ranking.js +1 -69
  34. package/dist/crypto.js +1 -179
  35. package/dist/daemon-write-endpoint.js +1 -290
  36. package/dist/daemon-writer.js +2 -406
  37. package/dist/database.js +43 -1110
  38. package/dist/deprecations.js +2 -162
  39. package/dist/design.js +13 -141
  40. package/dist/event-replication.js +1 -112
  41. package/dist/events-sse.js +7 -43
  42. package/dist/events.js +6 -238
  43. package/dist/failure-patterns.js +42 -659
  44. package/dist/federation.js +12 -236
  45. package/dist/goals.js +13 -101
  46. package/dist/golden.js +3 -355
  47. package/dist/handlers/agent.js +4 -165
  48. package/dist/handlers/alias-adapters.js +1 -129
  49. package/dist/handlers/aliases.js +1 -171
  50. package/dist/handlers/audit.js +1 -87
  51. package/dist/handlers/boundary.js +1 -221
  52. package/dist/handlers/capture.js +73 -1109
  53. package/dist/handlers/causality.js +7 -114
  54. package/dist/handlers/cloud.js +85 -382
  55. package/dist/handlers/companion.js +28 -459
  56. package/dist/handlers/datalake.js +7 -187
  57. package/dist/handlers/dispatch-context.js +0 -22
  58. package/dist/handlers/entity.js +25 -256
  59. package/dist/handlers/events.js +16 -335
  60. package/dist/handlers/failure.js +13 -340
  61. package/dist/handlers/goals.js +4 -296
  62. package/dist/handlers/intelligence.js +126 -674
  63. package/dist/handlers/invoicing.js +1 -70
  64. package/dist/handlers/mcpclient.js +6 -137
  65. package/dist/handlers/orchestration.js +40 -125
  66. package/dist/handlers/output-schemas.js +1 -24
  67. package/dist/handlers/presence.js +3 -99
  68. package/dist/handlers/project.js +28 -182
  69. package/dist/handlers/prompts.js +6 -157
  70. package/dist/handlers/quest.js +4 -224
  71. package/dist/handlers/recall.js +11 -218
  72. package/dist/handlers/registry.js +1 -167
  73. package/dist/handlers/resources.js +1 -288
  74. package/dist/handlers/review.js +11 -74
  75. package/dist/handlers/run.js +17 -487
  76. package/dist/handlers/search.js +15 -326
  77. package/dist/handlers/session.js +28 -615
  78. package/dist/handlers/share.js +8 -184
  79. package/dist/handlers/shims.js +1 -464
  80. package/dist/handlers/skill.js +67 -449
  81. package/dist/handlers/survivors.js +1 -120
  82. package/dist/handlers/symbols.js +8 -109
  83. package/dist/handlers/syncops.js +4 -302
  84. package/dist/handlers/types.js +1 -27
  85. package/dist/harvest.js +5 -191
  86. package/dist/hours.js +7 -156
  87. package/dist/http-auth.js +3 -321
  88. package/dist/http-fast.js +21 -1137
  89. package/dist/icons.js +1 -47
  90. package/dist/index.js +2 -924
  91. package/dist/indexer.js +4 -145
  92. package/dist/intelligence.js +31 -261
  93. package/dist/internal-dispatch.js +3 -212
  94. package/dist/keyset.js +1 -110
  95. package/dist/knowledge-graph.js +12 -176
  96. package/dist/license.d.ts +11 -0
  97. package/dist/license.d.ts.map +1 -1
  98. package/dist/license.js +2 -414
  99. package/dist/license.js.map +1 -1
  100. package/dist/logger.js +2 -199
  101. package/dist/maintenance.js +2 -148
  102. package/dist/mcp-client.js +6 -262
  103. package/dist/memory-artifacts.js +30 -449
  104. package/dist/migrate-prompt.js +2 -124
  105. package/dist/migrations.js +40 -655
  106. package/dist/performance.js +1 -228
  107. package/dist/presence.js +11 -140
  108. package/dist/priority-embed.js +5 -164
  109. package/dist/providers/embedding-provider.js +1 -196
  110. package/dist/readonly-gate.js +1 -29
  111. package/dist/rehydration.js +9 -157
  112. package/dist/reindex.js +1 -88
  113. package/dist/render-target.js +21 -514
  114. package/dist/render.js +4 -280
  115. package/dist/repl-guard.js +1 -173
  116. package/dist/replication-daemon-entrypoint.js +1 -31
  117. package/dist/replication-daemon.js +2 -262
  118. package/dist/resilience.js +1 -591
  119. package/dist/reverse-bridge.js +5 -360
  120. package/dist/security.js +1 -244
  121. package/dist/session-seen.js +3 -51
  122. package/dist/setup.js +1 -260
  123. package/dist/skill-author.js +5 -168
  124. package/dist/spec-kit.js +1 -191
  125. package/dist/sqlite-busy.js +1 -154
  126. package/dist/statusline.js +11 -315
  127. package/dist/sub-agent.js +13 -262
  128. package/dist/summarizer.js +13 -139
  129. package/dist/symbols.js +7 -283
  130. package/dist/sync.js +5 -359
  131. package/dist/tasks-dispatch.js +1 -84
  132. package/dist/tasks.js +1 -282
  133. package/dist/token-budget.js +1 -143
  134. package/dist/tool-analytics.js +7 -129
  135. package/dist/tool-annotations.js +1 -365
  136. package/dist/tool-manifest-v2.json +1 -1
  137. package/dist/tool-manifest.json +1 -1
  138. package/dist/tool-profiles.js +1 -75
  139. package/dist/trace-harvest.js +6 -244
  140. package/dist/types.js +1 -30
  141. package/dist/ui-dashboard.js +41 -50
  142. package/dist/ulid.js +1 -81
  143. package/dist/validate.js +1 -129
  144. package/dist/vault.js +1 -534
  145. package/dist/vectors.js +3 -184
  146. package/dist/version-check.js +4 -136
  147. package/dist/visibility.js +19 -155
  148. package/dist/wyrm-cli.js +98 -2451
  149. package/dist/wyrm-cli.js.map +1 -1
  150. package/dist/wyrm-guard.js +14 -424
  151. package/dist/wyrm-loop.js +3 -150
  152. package/dist/wyrm-manifest.json +1 -1
  153. package/dist/wyrm-statusline-daemon.js +1 -11
  154. package/dist/wyrm-statusline.js +4 -56
  155. package/dist/wyrm-ui.js +9 -77
  156. package/package.json +4 -2
@@ -1,406 +1,2 @@
1
- /**
2
- * Daemon-as-writer CLIENT (v7 F2, T012) OPT-IN via WYRM_DAEMON_WRITES=1.
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};