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.
- package/README.md +8 -14
- package/dist/activation.js +59 -1
- package/dist/agent-daemon.js +281 -4
- package/dist/agent-loop.js +332 -7
- package/dist/analytics.js +236 -13
- package/dist/attribution.js +49 -1
- package/dist/audit.js +457 -2
- package/dist/auto-capture.js +138 -3
- package/dist/auto-orchestrator.js +325 -1
- package/dist/autoconfig.d.ts +50 -0
- package/dist/autoconfig.d.ts.map +1 -1
- package/dist/autoconfig.js +1115 -39
- package/dist/autoconfig.js.map +1 -1
- package/dist/buddy-runner.js +109 -1
- package/dist/buddy.js +564 -14
- package/dist/build-flags.js +15 -1
- package/dist/capabilities.js +183 -3
- package/dist/capture.js +56 -1
- package/dist/causality.js +148 -8
- package/dist/cli.js +281 -20
- package/dist/cloud/cli.js +541 -5
- package/dist/cloud/client.js +221 -1
- package/dist/cloud/crypto.js +85 -1
- package/dist/cloud/machine-id.js +113 -2
- package/dist/cloud/recovery.js +60 -1
- package/dist/cloud/sync-engine.js +543 -7
- package/dist/cloud-backup.js +579 -5
- package/dist/cloud-profile.js +138 -1
- package/dist/cloud-sync-entrypoint.js +47 -1
- package/dist/cloud-sync.js +309 -2
- package/dist/connectors/bridge-source.d.ts +46 -0
- package/dist/connectors/bridge-source.d.ts.map +1 -0
- package/dist/connectors/bridge-source.js +77 -0
- package/dist/connectors/bridge-source.js.map +1 -0
- package/dist/connectors/index.d.ts +24 -0
- package/dist/connectors/index.d.ts.map +1 -0
- package/dist/connectors/index.js +69 -0
- package/dist/connectors/index.js.map +1 -0
- package/dist/connectors/ingest.d.ts +16 -0
- package/dist/connectors/ingest.d.ts.map +1 -0
- package/dist/connectors/ingest.js +116 -0
- package/dist/connectors/ingest.js.map +1 -0
- package/dist/connectors/types.d.ts +99 -0
- package/dist/connectors/types.d.ts.map +1 -0
- package/dist/connectors/types.js +17 -0
- package/dist/connectors/types.js.map +1 -0
- package/dist/constellation.js +168 -12
- package/dist/content-signature.js +45 -1
- package/dist/context-build-budgeted.js +144 -4
- package/dist/context-ranking.js +69 -1
- package/dist/crypto.js +179 -1
- package/dist/daemon-write-endpoint.js +290 -1
- package/dist/daemon-writer.js +406 -2
- package/dist/database.js +1278 -53
- package/dist/deprecations.js +162 -2
- package/dist/design.js +141 -13
- package/dist/event-replication.js +112 -1
- package/dist/events-sse.js +43 -7
- package/dist/events.js +238 -6
- package/dist/failure-patterns.d.ts +107 -0
- package/dist/failure-patterns.d.ts.map +1 -1
- package/dist/failure-patterns.js +924 -43
- package/dist/failure-patterns.js.map +1 -1
- package/dist/federation.js +236 -12
- package/dist/goals.js +101 -13
- package/dist/golden.js +355 -3
- package/dist/handlers/agent.js +165 -4
- package/dist/handlers/alias-adapters.js +129 -1
- package/dist/handlers/aliases.js +171 -1
- package/dist/handlers/audit.js +87 -1
- package/dist/handlers/boundary.js +221 -1
- package/dist/handlers/capture.js +1114 -73
- package/dist/handlers/causality.js +119 -9
- package/dist/handlers/cloud.js +382 -85
- package/dist/handlers/companion.js +459 -28
- package/dist/handlers/datalake.js +187 -7
- package/dist/handlers/dispatch-context.js +22 -0
- package/dist/handlers/entity.js +256 -25
- package/dist/handlers/events.js +335 -16
- package/dist/handlers/failure.d.ts.map +1 -1
- package/dist/handlers/failure.js +408 -13
- package/dist/handlers/failure.js.map +1 -1
- package/dist/handlers/goals.js +296 -4
- package/dist/handlers/intelligence.js +681 -126
- package/dist/handlers/invoicing.js +70 -1
- package/dist/handlers/mcpclient.js +137 -6
- package/dist/handlers/orchestration.js +125 -40
- package/dist/handlers/output-schemas.js +24 -1
- package/dist/handlers/presence.js +99 -3
- package/dist/handlers/project.js +182 -28
- package/dist/handlers/prompts.js +157 -6
- package/dist/handlers/quest.js +224 -4
- package/dist/handlers/recall.js +237 -13
- package/dist/handlers/registry.js +167 -1
- package/dist/handlers/resources.js +288 -1
- package/dist/handlers/review.js +74 -11
- package/dist/handlers/run.js +498 -16
- package/dist/handlers/search.js +338 -15
- package/dist/handlers/session.js +643 -31
- package/dist/handlers/share.js +184 -8
- package/dist/handlers/shims.js +464 -1
- package/dist/handlers/skill.js +449 -67
- package/dist/handlers/survivors.js +120 -1
- package/dist/handlers/symbols.js +109 -8
- package/dist/handlers/syncops.js +302 -4
- package/dist/handlers/types.js +27 -1
- package/dist/harvest.js +191 -5
- package/dist/hours.js +156 -7
- package/dist/http-auth.js +321 -3
- package/dist/http-fast.js +1302 -22
- package/dist/icons.js +47 -1
- package/dist/importers.js +268 -1
- package/dist/index.js +840 -2
- package/dist/indexer.js +145 -4
- package/dist/intelligence.js +261 -31
- package/dist/internal-dispatch.js +212 -3
- package/dist/keyset.js +110 -1
- package/dist/knowledge-graph.js +176 -12
- package/dist/license.js +441 -2
- package/dist/logger.js +199 -2
- package/dist/maintenance.js +148 -2
- package/dist/mcp-client.js +262 -6
- package/dist/memory-artifacts.js +596 -32
- package/dist/migrate-prompt.js +124 -2
- package/dist/migrations.d.ts.map +1 -1
- package/dist/migrations.js +799 -42
- package/dist/migrations.js.map +1 -1
- package/dist/performance.js +228 -1
- package/dist/presence.js +140 -11
- package/dist/priority-embed.js +164 -5
- package/dist/providers/embedding-provider.js +196 -1
- package/dist/readonly-gate.js +29 -1
- package/dist/receipt.js +43 -1
- package/dist/rehydration.js +157 -9
- package/dist/reindex.js +88 -1
- package/dist/render-target.js +544 -21
- package/dist/render.js +280 -4
- package/dist/repl-guard.js +173 -1
- package/dist/replication-daemon-entrypoint.js +31 -1
- package/dist/replication-daemon.js +262 -2
- package/dist/rerank.js +142 -1
- package/dist/resilience.js +591 -1
- package/dist/reverse-bridge.js +360 -5
- package/dist/security.js +244 -1
- package/dist/session-seen.js +51 -3
- package/dist/setup.js +260 -1
- package/dist/skill-author.js +168 -5
- package/dist/spec-kit.js +191 -1
- package/dist/sqlite-busy.js +154 -1
- package/dist/statusline.js +315 -11
- package/dist/sub-agent.js +262 -13
- package/dist/summarizer.js +139 -13
- package/dist/symbols.js +283 -7
- package/dist/sync.js +359 -5
- package/dist/tasks-dispatch.js +84 -1
- package/dist/tasks.js +282 -1
- package/dist/token-budget.js +143 -1
- package/dist/tool-analytics.js +129 -7
- package/dist/tool-annotations.js +365 -1
- package/dist/tool-manifest-v2.json +1 -1
- package/dist/tool-manifest.json +1 -1
- package/dist/tool-profiles.js +75 -1
- package/dist/trace-harvest.js +244 -6
- package/dist/types.js +30 -1
- package/dist/ui-dashboard.js +50 -41
- package/dist/ulid.js +81 -1
- package/dist/usage-tracker.js +66 -1
- package/dist/validate.js +129 -1
- package/dist/vault.js +534 -1
- package/dist/vector-init.js +67 -1
- package/dist/vectors.js +184 -3
- package/dist/version-check.js +136 -4
- package/dist/visibility.js +155 -19
- package/dist/wyrm-cli.js +2845 -101
- package/dist/wyrm-cli.js.map +1 -1
- package/dist/wyrm-guard.d.ts.map +1 -1
- package/dist/wyrm-guard.js +475 -14
- package/dist/wyrm-guard.js.map +1 -1
- package/dist/wyrm-loop.js +150 -3
- package/dist/wyrm-manifest.json +1 -1
- package/dist/wyrm-statusline-daemon.js +11 -1
- package/dist/wyrm-statusline.js +56 -4
- package/dist/wyrm-ui.js +77 -9
- package/package.json +1 -1
package/dist/spec-kit.js
CHANGED
|
@@ -1 +1,191 @@
|
|
|
1
|
-
|
|
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
|
package/dist/sqlite-busy.js
CHANGED
|
@@ -1 +1,154 @@
|
|
|
1
|
-
|
|
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
|