wyrm-mcp 7.3.2 → 7.3.3

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 (184) hide show
  1. package/README.md +8 -14
  2. package/dist/activation.js +59 -1
  3. package/dist/agent-daemon.js +281 -4
  4. package/dist/agent-loop.js +332 -7
  5. package/dist/analytics.js +236 -13
  6. package/dist/attribution.js +49 -1
  7. package/dist/audit.js +457 -2
  8. package/dist/auto-capture.js +138 -3
  9. package/dist/auto-orchestrator.js +325 -1
  10. package/dist/autoconfig.d.ts +50 -0
  11. package/dist/autoconfig.d.ts.map +1 -1
  12. package/dist/autoconfig.js +1115 -39
  13. package/dist/autoconfig.js.map +1 -1
  14. package/dist/buddy-runner.js +109 -1
  15. package/dist/buddy.js +564 -14
  16. package/dist/build-flags.js +15 -1
  17. package/dist/capabilities.js +183 -3
  18. package/dist/capture.js +56 -1
  19. package/dist/causality.js +148 -8
  20. package/dist/cli.js +281 -20
  21. package/dist/cloud/cli.js +541 -5
  22. package/dist/cloud/client.js +221 -1
  23. package/dist/cloud/crypto.js +85 -1
  24. package/dist/cloud/machine-id.js +113 -2
  25. package/dist/cloud/recovery.js +60 -1
  26. package/dist/cloud/sync-engine.js +543 -7
  27. package/dist/cloud-backup.js +579 -5
  28. package/dist/cloud-profile.js +138 -1
  29. package/dist/cloud-sync-entrypoint.js +47 -1
  30. package/dist/cloud-sync.js +309 -2
  31. package/dist/connectors/bridge-source.d.ts +46 -0
  32. package/dist/connectors/bridge-source.d.ts.map +1 -0
  33. package/dist/connectors/bridge-source.js +77 -0
  34. package/dist/connectors/bridge-source.js.map +1 -0
  35. package/dist/connectors/index.d.ts +24 -0
  36. package/dist/connectors/index.d.ts.map +1 -0
  37. package/dist/connectors/index.js +69 -0
  38. package/dist/connectors/index.js.map +1 -0
  39. package/dist/connectors/ingest.d.ts +16 -0
  40. package/dist/connectors/ingest.d.ts.map +1 -0
  41. package/dist/connectors/ingest.js +116 -0
  42. package/dist/connectors/ingest.js.map +1 -0
  43. package/dist/connectors/types.d.ts +99 -0
  44. package/dist/connectors/types.d.ts.map +1 -0
  45. package/dist/connectors/types.js +17 -0
  46. package/dist/connectors/types.js.map +1 -0
  47. package/dist/constellation.js +168 -12
  48. package/dist/content-signature.js +45 -1
  49. package/dist/context-build-budgeted.js +144 -4
  50. package/dist/context-ranking.js +69 -1
  51. package/dist/crypto.js +179 -1
  52. package/dist/daemon-write-endpoint.js +290 -1
  53. package/dist/daemon-writer.js +406 -2
  54. package/dist/database.js +1278 -53
  55. package/dist/deprecations.js +162 -2
  56. package/dist/design.js +141 -13
  57. package/dist/event-replication.js +112 -1
  58. package/dist/events-sse.js +43 -7
  59. package/dist/events.js +238 -6
  60. package/dist/failure-patterns.d.ts +107 -0
  61. package/dist/failure-patterns.d.ts.map +1 -1
  62. package/dist/failure-patterns.js +924 -43
  63. package/dist/failure-patterns.js.map +1 -1
  64. package/dist/federation.js +236 -12
  65. package/dist/goals.js +101 -13
  66. package/dist/golden.js +355 -3
  67. package/dist/handlers/agent.js +165 -4
  68. package/dist/handlers/alias-adapters.js +129 -1
  69. package/dist/handlers/aliases.js +171 -1
  70. package/dist/handlers/audit.js +87 -1
  71. package/dist/handlers/boundary.js +221 -1
  72. package/dist/handlers/capture.js +1114 -73
  73. package/dist/handlers/causality.js +119 -9
  74. package/dist/handlers/cloud.js +382 -85
  75. package/dist/handlers/companion.js +459 -28
  76. package/dist/handlers/datalake.js +187 -7
  77. package/dist/handlers/dispatch-context.js +22 -0
  78. package/dist/handlers/entity.js +256 -25
  79. package/dist/handlers/events.js +335 -16
  80. package/dist/handlers/failure.d.ts.map +1 -1
  81. package/dist/handlers/failure.js +408 -13
  82. package/dist/handlers/failure.js.map +1 -1
  83. package/dist/handlers/goals.js +296 -4
  84. package/dist/handlers/intelligence.js +681 -126
  85. package/dist/handlers/invoicing.js +70 -1
  86. package/dist/handlers/mcpclient.js +137 -6
  87. package/dist/handlers/orchestration.js +125 -40
  88. package/dist/handlers/output-schemas.js +24 -1
  89. package/dist/handlers/presence.js +99 -3
  90. package/dist/handlers/project.js +182 -28
  91. package/dist/handlers/prompts.js +157 -6
  92. package/dist/handlers/quest.js +224 -4
  93. package/dist/handlers/recall.js +237 -13
  94. package/dist/handlers/registry.js +167 -1
  95. package/dist/handlers/resources.js +288 -1
  96. package/dist/handlers/review.js +74 -11
  97. package/dist/handlers/run.js +498 -16
  98. package/dist/handlers/search.js +338 -15
  99. package/dist/handlers/session.js +643 -31
  100. package/dist/handlers/share.js +184 -8
  101. package/dist/handlers/shims.js +464 -1
  102. package/dist/handlers/skill.js +449 -67
  103. package/dist/handlers/survivors.js +120 -1
  104. package/dist/handlers/symbols.js +109 -8
  105. package/dist/handlers/syncops.js +302 -4
  106. package/dist/handlers/types.js +27 -1
  107. package/dist/harvest.js +191 -5
  108. package/dist/hours.js +156 -7
  109. package/dist/http-auth.js +321 -3
  110. package/dist/http-fast.js +1302 -22
  111. package/dist/icons.js +47 -1
  112. package/dist/importers.js +268 -1
  113. package/dist/index.js +840 -2
  114. package/dist/indexer.js +145 -4
  115. package/dist/intelligence.js +261 -31
  116. package/dist/internal-dispatch.js +212 -3
  117. package/dist/keyset.js +110 -1
  118. package/dist/knowledge-graph.js +176 -12
  119. package/dist/license.js +441 -2
  120. package/dist/logger.js +199 -2
  121. package/dist/maintenance.js +148 -2
  122. package/dist/mcp-client.js +262 -6
  123. package/dist/memory-artifacts.js +596 -32
  124. package/dist/migrate-prompt.js +124 -2
  125. package/dist/migrations.d.ts.map +1 -1
  126. package/dist/migrations.js +799 -42
  127. package/dist/migrations.js.map +1 -1
  128. package/dist/performance.js +228 -1
  129. package/dist/presence.js +140 -11
  130. package/dist/priority-embed.js +164 -5
  131. package/dist/providers/embedding-provider.js +196 -1
  132. package/dist/readonly-gate.js +29 -1
  133. package/dist/receipt.js +43 -1
  134. package/dist/rehydration.js +157 -9
  135. package/dist/reindex.js +88 -1
  136. package/dist/render-target.js +544 -21
  137. package/dist/render.js +280 -4
  138. package/dist/repl-guard.js +173 -1
  139. package/dist/replication-daemon-entrypoint.js +31 -1
  140. package/dist/replication-daemon.js +262 -2
  141. package/dist/rerank.js +142 -1
  142. package/dist/resilience.js +591 -1
  143. package/dist/reverse-bridge.js +360 -5
  144. package/dist/security.js +244 -1
  145. package/dist/session-seen.js +51 -3
  146. package/dist/setup.js +260 -1
  147. package/dist/skill-author.js +168 -5
  148. package/dist/spec-kit.js +191 -1
  149. package/dist/sqlite-busy.js +154 -1
  150. package/dist/statusline.js +315 -11
  151. package/dist/sub-agent.js +262 -13
  152. package/dist/summarizer.js +139 -13
  153. package/dist/symbols.js +283 -7
  154. package/dist/sync.js +359 -5
  155. package/dist/tasks-dispatch.js +84 -1
  156. package/dist/tasks.js +282 -1
  157. package/dist/token-budget.js +143 -1
  158. package/dist/tool-analytics.js +129 -7
  159. package/dist/tool-annotations.js +365 -1
  160. package/dist/tool-manifest-v2.json +1 -1
  161. package/dist/tool-manifest.json +1 -1
  162. package/dist/tool-profiles.js +75 -1
  163. package/dist/trace-harvest.js +244 -6
  164. package/dist/types.js +30 -1
  165. package/dist/ui-dashboard.js +50 -41
  166. package/dist/ulid.js +81 -1
  167. package/dist/usage-tracker.js +66 -1
  168. package/dist/validate.js +129 -1
  169. package/dist/vault.js +534 -1
  170. package/dist/vector-init.js +67 -1
  171. package/dist/vectors.js +184 -3
  172. package/dist/version-check.js +136 -4
  173. package/dist/visibility.js +155 -19
  174. package/dist/wyrm-cli.js +2845 -101
  175. package/dist/wyrm-cli.js.map +1 -1
  176. package/dist/wyrm-guard.d.ts.map +1 -1
  177. package/dist/wyrm-guard.js +475 -14
  178. package/dist/wyrm-guard.js.map +1 -1
  179. package/dist/wyrm-loop.js +150 -3
  180. package/dist/wyrm-manifest.json +1 -1
  181. package/dist/wyrm-statusline-daemon.js +11 -1
  182. package/dist/wyrm-statusline.js +56 -4
  183. package/dist/wyrm-ui.js +77 -9
  184. package/package.json +1 -1
package/dist/spec-kit.js CHANGED
@@ -1 +1,191 @@
1
- import{readFileSync as h,existsSync as g}from"fs";import{join as k}from"path";function b(a){const s=a.split(/\r?\n/);let c,r;for(const t of s){const e=t.match(/^#\s+(.+?)\s*$/);if(e){c=e[1].trim();break}const i=t.match(/^\s*(?:title|feature|name)\s*[:=]\s*(.+?)\s*$/i);if(i){c=i[1].trim();break}}const l=s.findIndex(t=>/^#{1,6}\s+(summary|overview|description)\b/i.test(t));if(l>=0){const t=[];for(let e=l+1;e<s.length;e++){const i=s[e];if(/^#{1,6}\s+/.test(i)||i.trim()===""&&t.length)break;i.trim()!==""&&t.push(i.trim())}t.length&&(r=t.join(" ").trim())}if(!r){const t=[];for(const e of s){if(/^#{1,6}\s+/.test(e)){if(t.length)break;continue}if(e.trim()===""){if(t.length)break;continue}if(/^\s*[-*]\s*\[/.test(e)){if(t.length)break;continue}t.push(e.trim())}t.length&&(r=t.join(" ").trim())}return{title:c,summary:r}}function $(a){const s=[],c=a.split(/\r?\n/);let r=!1,l=0;for(const t of c){const i=t.replace(/\s+$/,"").trim();if(/^```/.test(i)){r=!r;continue}if(r||i===""||/^#{1,6}\s+/.test(i))continue;let n="",d=!1,f=!1;const u=i.match(/^[-*+]\s*\[([ xX])\]\s*(.+)$/);if(u&&(d=u[1].toLowerCase()==="x",n=u[2].trim(),f=!0),!f){const o=i.match(/^\d+[.)]\s+(.+)$/);o&&(n=o[1].trim(),f=!0)}if(!f){const o=i.match(/^(T\d{1,4})[\s:.)-]+(.+)$/i);o&&(n=`${o[1].toUpperCase()} ${o[2].trim()}`,f=!0)}if(!f){const o=i.match(/^[-*+]\s+(.+)$/);o&&/[a-zA-Z]/.test(o[1])&&(n=o[1].trim(),f=!0)}if(!f||n==="")continue;l++;let m;const p=n.match(/^(T\d{1,4})\b[\s:.)-]*/i);p?(m=p[1].toUpperCase(),n=n.slice(p[0].length).trim()):m=`L${String(l).padStart(3,"0")}`,n=n.replace(/^\[[A-Za-z0-9]{1,3}\]\s*/,"").trim(),n===""&&(n=m),s.push({id:m,title:n,done:d,ordinal:l})}return s}function y(a){const s=k(a,"spec.md"),c=k(a,"tasks.md");let r,l;if(g(s)){const e=b(h(s,"utf-8"));r=e.title,l=e.summary}let t=[];return g(c)&&(t=$(h(c,"utf-8"))),{title:r,summary:l,tasks:t}}function S(a,s){return`spec:${(a.replace(/[\\/]+$/,"").split(/[\\/]/).pop()||a).toLowerCase().replace(/[^a-z0-9]+/g,"-").replace(/^-+|-+$/g,"")}:${s.toLowerCase()}`}export{b as parseSpecMarkdown,$ as parseTasksMarkdown,y as readSpecDir,S as specTaskSignature};
1
+ /**
2
+ * Wyrm spec-kit registry — GHOSTMESH spec-kit specs → Wyrm-native quests.
3
+ *
4
+ * Parses the spec-kit document layout (spec.md / tasks.md) into structured data
5
+ * the wyrm_spec_register tool turns into idempotent quests. Pure functions +
6
+ * filesystem reads only — no DB coupling, so it's trivially unit-testable.
7
+ *
8
+ * @copyright 2026 Ghost Protocol (Pvt) Ltd.
9
+ * @license Proprietary — (c) 2026 Ghost Protocol (Pvt) Ltd. All rights reserved. See LICENSE.
10
+ */
11
+ import { readFileSync, existsSync } from 'fs';
12
+ import { join } from 'path';
13
+ /**
14
+ * Extract a title + summary from a spec.md body.
15
+ * - title: first markdown H1 (`# ...`) or, failing that, a `Title:`/`Feature:` field.
16
+ * - summary: first `## Summary`/`## Overview` paragraph, else the first non-heading
17
+ * prose paragraph.
18
+ */
19
+ export function parseSpecMarkdown(md) {
20
+ const lines = md.split(/\r?\n/);
21
+ let title;
22
+ let summary;
23
+ for (const line of lines) {
24
+ const h1 = line.match(/^#\s+(.+?)\s*$/);
25
+ if (h1) {
26
+ title = h1[1].trim();
27
+ break;
28
+ }
29
+ const field = line.match(/^\s*(?:title|feature|name)\s*[:=]\s*(.+?)\s*$/i);
30
+ if (field) {
31
+ title = field[1].trim();
32
+ break;
33
+ }
34
+ }
35
+ // Summary: prefer the paragraph under a Summary/Overview/Description heading.
36
+ const headingIdx = lines.findIndex((l) => /^#{1,6}\s+(summary|overview|description)\b/i.test(l));
37
+ if (headingIdx >= 0) {
38
+ const para = [];
39
+ for (let i = headingIdx + 1; i < lines.length; i++) {
40
+ const l = lines[i];
41
+ if (/^#{1,6}\s+/.test(l))
42
+ break; // next heading ends the section
43
+ if (l.trim() === '' && para.length)
44
+ break; // blank line ends the first paragraph
45
+ if (l.trim() !== '')
46
+ para.push(l.trim());
47
+ }
48
+ if (para.length)
49
+ summary = para.join(' ').trim();
50
+ }
51
+ // Fallback: first non-heading, non-blank prose paragraph.
52
+ if (!summary) {
53
+ const para = [];
54
+ for (const l of lines) {
55
+ if (/^#{1,6}\s+/.test(l)) {
56
+ if (para.length)
57
+ break;
58
+ else
59
+ continue;
60
+ }
61
+ if (l.trim() === '') {
62
+ if (para.length)
63
+ break;
64
+ else
65
+ continue;
66
+ }
67
+ if (/^\s*[-*]\s*\[/.test(l)) {
68
+ if (para.length)
69
+ break;
70
+ else
71
+ continue;
72
+ } // skip task lines
73
+ para.push(l.trim());
74
+ }
75
+ if (para.length)
76
+ summary = para.join(' ').trim();
77
+ }
78
+ return { title, summary };
79
+ }
80
+ /**
81
+ * Parse a tasks.md into a flat ordered task list. Recognizes the common
82
+ * spec-kit / markdown task encodings:
83
+ * - `T001 Do the thing` (id-prefixed)
84
+ * - `- [ ] Do the thing` (gfm checkbox, open)
85
+ * - `- [x] Do the thing` (gfm checkbox, done)
86
+ * - `- [ ] T012 [P] Do the thing` (checkbox + id + parallel marker)
87
+ * - `1. Do the thing` (numbered list)
88
+ * - `- Do the thing` (plain bullet)
89
+ * Headings, blank lines, code fences, and non-task prose are ignored.
90
+ */
91
+ export function parseTasksMarkdown(md) {
92
+ const tasks = [];
93
+ const lines = md.split(/\r?\n/);
94
+ let inFence = false;
95
+ let ordinal = 0;
96
+ for (const raw of lines) {
97
+ const line = raw.replace(/\s+$/, '');
98
+ const trimmed = line.trim();
99
+ if (/^```/.test(trimmed)) {
100
+ inFence = !inFence;
101
+ continue;
102
+ }
103
+ if (inFence)
104
+ continue;
105
+ if (trimmed === '')
106
+ continue;
107
+ if (/^#{1,6}\s+/.test(trimmed))
108
+ continue; // headings are sections, not tasks
109
+ let body = '';
110
+ let done = false;
111
+ let matched = false;
112
+ // 1) GFM checkbox bullet: - [ ] / - [x] / * [X]
113
+ const cb = trimmed.match(/^[-*+]\s*\[([ xX])\]\s*(.+)$/);
114
+ if (cb) {
115
+ done = cb[1].toLowerCase() === 'x';
116
+ body = cb[2].trim();
117
+ matched = true;
118
+ }
119
+ // 2) Numbered list: 1. text or 1) text
120
+ if (!matched) {
121
+ const num = trimmed.match(/^\d+[.)]\s+(.+)$/);
122
+ if (num) {
123
+ body = num[1].trim();
124
+ matched = true;
125
+ }
126
+ }
127
+ // 3) Bare id-prefixed task: T001 text (also catches "T001: text", "T001. text")
128
+ if (!matched) {
129
+ const idline = trimmed.match(/^(T\d{1,4})[\s:.)-]+(.+)$/i);
130
+ if (idline) {
131
+ body = `${idline[1].toUpperCase()} ${idline[2].trim()}`;
132
+ matched = true;
133
+ }
134
+ }
135
+ // 4) Plain bullet: - text (only when it looks like a task, i.e. has letters)
136
+ if (!matched) {
137
+ const bullet = trimmed.match(/^[-*+]\s+(.+)$/);
138
+ if (bullet && /[a-zA-Z]/.test(bullet[1])) {
139
+ body = bullet[1].trim();
140
+ matched = true;
141
+ }
142
+ }
143
+ if (!matched || body === '')
144
+ continue;
145
+ ordinal++;
146
+ // Pull out a leading task id if present (e.g. "T001 [P] do x" → id T001).
147
+ let id;
148
+ const idMatch = body.match(/^(T\d{1,4})\b[\s:.)-]*/i);
149
+ if (idMatch) {
150
+ id = idMatch[1].toUpperCase();
151
+ body = body.slice(idMatch[0].length).trim();
152
+ }
153
+ else {
154
+ id = `L${String(ordinal).padStart(3, '0')}`;
155
+ }
156
+ // Strip a leading [P]/[X] parallel/phase marker that spec-kit sometimes uses.
157
+ body = body.replace(/^\[[A-Za-z0-9]{1,3}\]\s*/, '').trim();
158
+ if (body === '')
159
+ body = id;
160
+ tasks.push({ id, title: body, done, ordinal });
161
+ }
162
+ return tasks;
163
+ }
164
+ /** Read + parse a spec directory. Tolerant of missing spec.md (title/summary become undefined). */
165
+ export function readSpecDir(specDir) {
166
+ const specPath = join(specDir, 'spec.md');
167
+ const tasksPath = join(specDir, 'tasks.md');
168
+ let title;
169
+ let summary;
170
+ if (existsSync(specPath)) {
171
+ const meta = parseSpecMarkdown(readFileSync(specPath, 'utf-8'));
172
+ title = meta.title;
173
+ summary = meta.summary;
174
+ }
175
+ let tasks = [];
176
+ if (existsSync(tasksPath)) {
177
+ tasks = parseTasksMarkdown(readFileSync(tasksPath, 'utf-8'));
178
+ }
179
+ return { title, summary, tasks };
180
+ }
181
+ /**
182
+ * Deterministic dedupe signature for a spec task. Combines a slug of the spec
183
+ * directory (basename) with the task id so re-running spec_register matches the
184
+ * same quest by tag instead of creating a duplicate.
185
+ */
186
+ export function specTaskSignature(specDir, taskId) {
187
+ const base = specDir.replace(/[\\/]+$/, '').split(/[\\/]/).pop() || specDir;
188
+ const slug = base.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '');
189
+ return `spec:${slug}:${taskId.toLowerCase()}`;
190
+ }
191
+ //# sourceMappingURL=spec-kit.js.map
@@ -1 +1,154 @@
1
- import{renderErrorResponse as o}from"./render.js";const n="WYRM_BUSY",i="WYRM_CIRCUIT_OPEN",s="WYRM_DAEMON_AMBIGUOUS",a=1e3,c=3e4;function u(e){if(e===null||typeof e!="object")return!1;const t=e.code;return typeof t=="string"&&t.startsWith("SQLITE_BUSY")}function p(e){if(e===null||typeof e!="object")return!1;const t=e.code;return typeof t=="string"&&t.startsWith("SQLITE_CONSTRAINT")}function _(e){if(u(e))return"lock";if(e===null||typeof e!="object")return null;const t=e.code;return t===i?"circuit":t===s?"daemon":null}function d(e,t="lock"){const r=t==="circuit"?`${e}: the Wyrm write path's circuit breaker is open after repeated write failures (WYRM_CIRCUIT_OPEN); writes are refused until it half-opens (~30s).`:t==="daemon"?`${e}: the daemon write outcome is unknown (WYRM_DAEMON_WRITES=1 and the daemon stopped responding mid-write); the local fallback was withheld to avoid a duplicate row.`:`${e}: the Wyrm database is briefly write-locked by another process (SQLITE_BUSY \u2014 the cross-process write lock outlived the busy_timeout grace window, or a read-then-write transaction lost a snapshot upgrade race).`;return{error:{code:n,message:r,retryable:!0,retry_after_ms:t==="circuit"?c:a},expected:`Retry the SAME ${e} call with the SAME arguments after retry_after_ms; no arguments need to change. Retrying is safe: writes are transactional or coalesce by key (a retried failure_record may count one extra occurrence by design`+(t==="daemon"?"; forwarded writes reuse their idempotency key inside the retry window, so the daemon cannot apply the write twice":"")+")."}}function f(e,t="lock"){return o(d(e,t))}export{a as BUSY_RETRY_AFTER_MS,c as CIRCUIT_RETRY_AFTER_MS,n as WYRM_BUSY_CODE,i as WYRM_CIRCUIT_OPEN_CODE,s as WYRM_DAEMON_AMBIGUOUS_CODE,d as busyErrorBody,f as busyErrorResponse,u as isSqliteBusyError,p as isSqliteConstraintError,_ as retryableWriteCause};
1
+ /**
2
+ * Cross-process write story (v7 F2, T011) — the SQLITE_BUSY boundary.
3
+ *
4
+ * WHAT THIS IS: when two OS PROCESSES (an MCP server + the HTTP daemon, or two
5
+ * fleet subagent processes) share one wyrm.db, SQLite's WAL mode allows many
6
+ * readers but exactly ONE writer at a time. A contended writer spins inside
7
+ * SQLite for `busy_timeout` (5000ms, set + documented in database.ts) and only
8
+ * then surfaces SQLITE_BUSY. Before T011 that escaped the dispatcher as an
9
+ * opaque `Error: SqliteError: database is locked` string — a structured-output
10
+ * subagent reading that has no machine-readable signal that the write is
11
+ * RETRYABLE, so fleets abandoned writes that one retry would have landed.
12
+ *
13
+ * This module owns the classification + the structured SEP-1303-style error
14
+ * body (`isError: true` + `{error, expected}`, the same framing spec FR-3
15
+ * standardizes for validation errors) so subagents self-correct: parse
16
+ * `error.code === 'WYRM_BUSY'`, wait `retry_after_ms`, retry the SAME call.
17
+ *
18
+ * WHY THERE IS NO IN-PROCESS WRITE QUEUE: better-sqlite3 is fully synchronous —
19
+ * two writes issued by the SAME process can never interleave by construction
20
+ * (each `run()` completes before the next statement starts), so an in-process
21
+ * queue would serialize something that is already serial. The only real
22
+ * contention is CROSS-process, and that is handled by (a) busy_timeout giving
23
+ * the writer a 5s grace window, (b) WyrmDB.batchWrites() — the transaction
24
+ * helper the batched insert path commits through (WyrmDB.insertDataBatch,
25
+ * behind wyrm_data_batch_insert) — taking the write lock once per batch
26
+ * instead of once per row, and (c) this structured BUSY/RETRY body when the
27
+ * window is exceeded.
28
+ *
29
+ * Article III: zero LLM involvement — classification is a string check on the
30
+ * better-sqlite3 error code; the retry hint is a fixed documented constant.
31
+ *
32
+ * @copyright 2026 Ghost Protocol (Pvt) Ltd.
33
+ * @license Proprietary — (c) 2026 Ghost Protocol (Pvt) Ltd. All rights reserved. See LICENSE.
34
+ */
35
+ import { renderErrorResponse } from './render.js';
36
+ /** Machine-readable error code subagents key their retry logic on. */
37
+ export const WYRM_BUSY_CODE = 'WYRM_BUSY';
38
+ /** v7 F2 review fix: ResilienceManager's circuit breaker refuses writes while
39
+ * open. The error it returns now carries this code so the dispatcher can map
40
+ * it to the SAME structured retryable body instead of an opaque plain Error
41
+ * (previously the breaker, once tripped, made the WYRM_BUSY body unreachable
42
+ * for its whole 30s window — exactly during sustained contention). */
43
+ export const WYRM_CIRCUIT_OPEN_CODE = 'WYRM_CIRCUIT_OPEN';
44
+ /** v7 F2 review fix: a daemon-forwarded write (WYRM_DAEMON_WRITES=1) whose
45
+ * outcome is UNKNOWN — the request may have been delivered and committed but
46
+ * no response arrived. daemon-writer.ts throws this instead of silently
47
+ * falling back to a local direct write (which could double-write canonical
48
+ * memory — Article VI). Retrying the same call is safe: the forwarded write
49
+ * reuses its idempotency key (within the pending-breadcrumb TTL), so the
50
+ * daemon cannot apply it twice. Bounded: after DAEMON_AMBIGUOUS_CYCLE_CAP
51
+ * consecutive ambiguous cycles daemon-writer.ts throws a NON-retryable
52
+ * WYRM_DAEMON_RETRY_EXHAUSTED instead — deliberately unclassified by
53
+ * retryableWriteCause, so a persistently failing daemon surfaces loudly
54
+ * rather than holding callers in an instructed-retry loop. */
55
+ export const WYRM_DAEMON_AMBIGUOUS_CODE = 'WYRM_DAEMON_AMBIGUOUS';
56
+ /**
57
+ * Retry spacing advice (ms). Rationale: by the time SQLITE_BUSY surfaces the
58
+ * caller has ALREADY waited the full 5000ms busy_timeout inside SQLite, so the
59
+ * lock holder is a genuinely long-running writer (bulk import, vacuum,
60
+ * checkpoint), not a sub-millisecond row write. 1s spacing keeps a retrying
61
+ * fleet responsive without a thundering herd of immediate re-attempts that
62
+ * would each burn another 5s busy_timeout window. This is advice to the
63
+ * caller, not a measured performance claim (Article VIII).
64
+ */
65
+ export const BUSY_RETRY_AFTER_MS = 1000;
66
+ /** Circuit-open retry spacing: the breaker half-opens after its 30s timeout
67
+ * (resilience.ts DEFAULT_CIRCUIT_CONFIG.timeout) — advising a 1s retry would
68
+ * just bounce off the still-open breaker 29 more times. */
69
+ export const CIRCUIT_RETRY_AFTER_MS = 30_000;
70
+ /**
71
+ * True when `err` is a better-sqlite3 SQLITE_BUSY family error
72
+ * (SQLITE_BUSY, SQLITE_BUSY_SNAPSHOT, SQLITE_BUSY_RECOVERY, ...).
73
+ * Deliberately NOT matching SQLITE_LOCKED: that is same-connection /
74
+ * shared-cache contention — a programming error, not a retryable
75
+ * cross-process race — and must stay loud.
76
+ */
77
+ export function isSqliteBusyError(err) {
78
+ if (err === null || typeof err !== 'object')
79
+ return false;
80
+ const code = err.code;
81
+ return typeof code === 'string' && code.startsWith('SQLITE_BUSY');
82
+ }
83
+ /**
84
+ * True when `err` is a better-sqlite3 SQLITE_CONSTRAINT family error
85
+ * (SQLITE_CONSTRAINT_CHECK, _FOREIGNKEY, _UNIQUE, _NOTNULL, ...). These are
86
+ * DETERMINISTIC domain rejections: every daemon write op is a single
87
+ * statement or one IMMEDIATE transaction, so a constraint throw PROVES the
88
+ * write rolled back without committing. The daemon write endpoint maps this
89
+ * family to the rejected `{e}` wire shape — the client fails direct and the
90
+ * real constraint message surfaces through the local path — instead of a
91
+ * generic 500, which the client must treat as commit-state-unknown and would
92
+ * otherwise retry forever (the same inputs deterministically re-violate).
93
+ * Deliberately NOT widened to all non-BUSY errors: a post-commit throw also
94
+ * 5xxs on a COMMITTED row, and claiming "no commit" for it double-writes.
95
+ */
96
+ export function isSqliteConstraintError(err) {
97
+ if (err === null || typeof err !== 'object')
98
+ return false;
99
+ const code = err.code;
100
+ return typeof code === 'string' && code.startsWith('SQLITE_CONSTRAINT');
101
+ }
102
+ export function retryableWriteCause(err) {
103
+ if (isSqliteBusyError(err))
104
+ return 'lock';
105
+ if (err === null || typeof err !== 'object')
106
+ return null;
107
+ const code = err.code;
108
+ if (code === WYRM_CIRCUIT_OPEN_CODE)
109
+ return 'circuit';
110
+ if (code === WYRM_DAEMON_AMBIGUOUS_CODE)
111
+ return 'daemon';
112
+ return null;
113
+ }
114
+ /**
115
+ * v7 F2 review fix — the `expected` contract no longer overclaims "Nothing was
116
+ * written": multi-statement handlers exist (failure_record's record-then-
117
+ * promote sequence), and an ambiguous daemon outcome may have committed
118
+ * remotely. The honest, still machine-actionable contract: retrying the SAME
119
+ * call is SAFE — statement groups are transactional (failures.record() and
120
+ * audit appends run in one IMMEDIATE transaction), coalescing writes coalesce
121
+ * by key (a re-recorded failure may count one extra occurrence, the documented
122
+ * accrual-by-design), and daemon-forwarded writes reuse their idempotency key.
123
+ */
124
+ export function busyErrorBody(tool, cause = 'lock') {
125
+ const message = cause === 'circuit'
126
+ ? `${tool}: the Wyrm write path's circuit breaker is open after repeated write failures (WYRM_CIRCUIT_OPEN); writes are refused until it half-opens (~30s).`
127
+ : cause === 'daemon'
128
+ ? `${tool}: the daemon write outcome is unknown (WYRM_DAEMON_WRITES=1 and the daemon stopped responding mid-write); the local fallback was withheld to avoid a duplicate row.`
129
+ : `${tool}: the Wyrm database is briefly write-locked by another process (SQLITE_BUSY — the cross-process write lock outlived the busy_timeout grace window, or a read-then-write transaction lost a snapshot upgrade race).`;
130
+ return {
131
+ error: {
132
+ code: WYRM_BUSY_CODE,
133
+ message,
134
+ retryable: true,
135
+ retry_after_ms: cause === 'circuit' ? CIRCUIT_RETRY_AFTER_MS : BUSY_RETRY_AFTER_MS,
136
+ },
137
+ expected: `Retry the SAME ${tool} call with the SAME arguments after retry_after_ms; no arguments need to change. ` +
138
+ `Retrying is safe: writes are transactional or coalesce by key (a retried failure_record may count one extra occurrence by design` +
139
+ (cause === 'daemon' ? '; forwarded writes reuse their idempotency key inside the retry window, so the daemon cannot apply the write twice' : '') +
140
+ `).`,
141
+ };
142
+ }
143
+ /**
144
+ * The full MCP tool response for a BUSY write: `content[0].text` is the JSON
145
+ * serialization of the SAME body that rides in `structuredContent`, so
146
+ * text-parsing and structured clients see one truth that cannot drift.
147
+ * v7 F3 (T019): the dual emit goes through the single renderer (render.ts) —
148
+ * byte-identical to the hand-rolled pre-T019 output, now derived in exactly
149
+ * one place.
150
+ */
151
+ export function busyErrorResponse(tool, cause = 'lock') {
152
+ return renderErrorResponse(busyErrorBody(tool, cause));
153
+ }
154
+ //# sourceMappingURL=sqlite-busy.js.map