mdkg 0.1.9 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +40 -1
- package/README.md +15 -11
- package/dist/cli.js +147 -6
- package/dist/commands/db.js +185 -1
- package/dist/core/project_db_materializer.js +462 -0
- package/dist/core/project_db_migrations.js +24 -0
- package/dist/core/project_db_queue.js +186 -0
- package/dist/core/project_db_snapshot.js +28 -3
- package/dist/init/AGENT_START.md +10 -8
- package/dist/init/CLI_COMMAND_MATRIX.md +13 -8
- package/dist/init/README.md +11 -9
- package/dist/init/init-manifest.json +60 -5
- package/dist/init/skills/default/verify-close-and-checkpoint/SKILL.md +8 -7
- package/dist/init/templates/skills/base.SKILL.md +66 -0
- package/dist/init/templates/specs/agent.SPEC.md +39 -0
- package/dist/init/templates/specs/api.SPEC.md +32 -0
- package/dist/init/templates/specs/base.SPEC.md +87 -0
- package/dist/init/templates/specs/capability.SPEC.md +32 -0
- package/dist/init/templates/specs/integration.SPEC.md +24 -0
- package/dist/init/templates/specs/model.SPEC.md +20 -0
- package/dist/init/templates/specs/omniruntime-agent.SPEC.md +39 -0
- package/dist/init/templates/specs/project.SPEC.md +26 -0
- package/dist/init/templates/specs/runtime-image.SPEC.md +20 -0
- package/dist/init/templates/specs/tool.SPEC.md +24 -0
- package/package.json +4 -2
|
@@ -0,0 +1,462 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.PROJECT_DB_MATERIALIZER_SCHEMA_VERSION = exports.PROJECT_DB_MATERIALIZER_KIND = exports.PROJECT_DB_MATERIALIZER_QUEUE = void 0;
|
|
7
|
+
exports.enqueueProjectDbMaterialization = enqueueProjectDbMaterialization;
|
|
8
|
+
exports.runNextProjectDbMaterializer = runNextProjectDbMaterializer;
|
|
9
|
+
exports.readProjectDbMaterializerStats = readProjectDbMaterializerStats;
|
|
10
|
+
const crypto_1 = __importDefault(require("crypto"));
|
|
11
|
+
const fs_1 = __importDefault(require("fs"));
|
|
12
|
+
const path_1 = __importDefault(require("path"));
|
|
13
|
+
const config_1 = require("./config");
|
|
14
|
+
const project_db_queue_1 = require("./project_db_queue");
|
|
15
|
+
const project_db_events_1 = require("./project_db_events");
|
|
16
|
+
const project_db_snapshot_1 = require("./project_db_snapshot");
|
|
17
|
+
exports.PROJECT_DB_MATERIALIZER_QUEUE = "project-db.materialize";
|
|
18
|
+
exports.PROJECT_DB_MATERIALIZER_KIND = "project-db.materialize";
|
|
19
|
+
exports.PROJECT_DB_MATERIALIZER_SCHEMA_VERSION = 1;
|
|
20
|
+
function loadDatabaseCtor() {
|
|
21
|
+
try {
|
|
22
|
+
const loaded = require("node:sqlite");
|
|
23
|
+
if (!loaded.DatabaseSync) {
|
|
24
|
+
throw new Error("node:sqlite DatabaseSync is unavailable");
|
|
25
|
+
}
|
|
26
|
+
return loaded.DatabaseSync;
|
|
27
|
+
}
|
|
28
|
+
catch (err) {
|
|
29
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
30
|
+
throw new Error(`node:sqlite is required for mdkg project DB materializers: ${message}`);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
function nowMs(input) {
|
|
34
|
+
if (input !== undefined) {
|
|
35
|
+
assertInteger(input, "now_ms");
|
|
36
|
+
return input;
|
|
37
|
+
}
|
|
38
|
+
return Date.now();
|
|
39
|
+
}
|
|
40
|
+
function assertNonEmpty(value, field) {
|
|
41
|
+
if (typeof value !== "string" || value.trim() === "") {
|
|
42
|
+
throw new Error(`${field} must be a non-empty string`);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
function assertInteger(value, field) {
|
|
46
|
+
if (!Number.isInteger(value)) {
|
|
47
|
+
throw new Error(`${field} must be an integer`);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
function assertPositiveInteger(value, field) {
|
|
51
|
+
assertInteger(value, field);
|
|
52
|
+
if (value <= 0) {
|
|
53
|
+
throw new Error(`${field} must be greater than 0`);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
function sha256File(filePath) {
|
|
57
|
+
return `sha256:${crypto_1.default.createHash("sha256").update(fs_1.default.readFileSync(filePath)).digest("hex")}`;
|
|
58
|
+
}
|
|
59
|
+
function safeSegment(value) {
|
|
60
|
+
return value.replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/^-+|-+$/g, "") || "materializer";
|
|
61
|
+
}
|
|
62
|
+
function isObject(value) {
|
|
63
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
64
|
+
}
|
|
65
|
+
function stringField(value, field) {
|
|
66
|
+
const raw = value[field];
|
|
67
|
+
if (typeof raw !== "string" || raw.trim() === "") {
|
|
68
|
+
throw new Error(`materializer payload ${field} must be a non-empty string`);
|
|
69
|
+
}
|
|
70
|
+
return raw;
|
|
71
|
+
}
|
|
72
|
+
function parsePayload(payloadJson) {
|
|
73
|
+
let parsed;
|
|
74
|
+
try {
|
|
75
|
+
parsed = JSON.parse(payloadJson);
|
|
76
|
+
}
|
|
77
|
+
catch (err) {
|
|
78
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
79
|
+
throw new Error(`materializer payload must be valid JSON: ${message}`);
|
|
80
|
+
}
|
|
81
|
+
if (!isObject(parsed)) {
|
|
82
|
+
throw new Error("materializer payload must be an object");
|
|
83
|
+
}
|
|
84
|
+
if (parsed.kind !== exports.PROJECT_DB_MATERIALIZER_KIND) {
|
|
85
|
+
throw new Error(`materializer payload kind must be ${exports.PROJECT_DB_MATERIALIZER_KIND}`);
|
|
86
|
+
}
|
|
87
|
+
if (parsed.schema_version !== exports.PROJECT_DB_MATERIALIZER_SCHEMA_VERSION) {
|
|
88
|
+
throw new Error(`materializer payload schema_version must be ${exports.PROJECT_DB_MATERIALIZER_SCHEMA_VERSION}`);
|
|
89
|
+
}
|
|
90
|
+
const reducerName = stringField(parsed, "reducer_name");
|
|
91
|
+
if (reducerName !== "project_meta.set") {
|
|
92
|
+
throw new Error(`unsupported materializer reducer_name: ${reducerName}`);
|
|
93
|
+
}
|
|
94
|
+
const reducerVersion = stringField(parsed, "reducer_version");
|
|
95
|
+
if (reducerVersion !== "v1") {
|
|
96
|
+
throw new Error(`unsupported materializer reducer_version: ${reducerVersion}`);
|
|
97
|
+
}
|
|
98
|
+
return {
|
|
99
|
+
kind: exports.PROJECT_DB_MATERIALIZER_KIND,
|
|
100
|
+
schema_version: exports.PROJECT_DB_MATERIALIZER_SCHEMA_VERSION,
|
|
101
|
+
project_id: stringField(parsed, "project_id"),
|
|
102
|
+
branch_id: stringField(parsed, "branch_id"),
|
|
103
|
+
event_id: stringField(parsed, "event_id"),
|
|
104
|
+
reducer_name: reducerName,
|
|
105
|
+
reducer_version: reducerVersion,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
function partialPayloadIds(payloadJson) {
|
|
109
|
+
try {
|
|
110
|
+
const parsed = JSON.parse(payloadJson);
|
|
111
|
+
if (isObject(parsed)) {
|
|
112
|
+
return {
|
|
113
|
+
project_id: typeof parsed.project_id === "string" && parsed.project_id.trim() !== "" ? parsed.project_id : "unknown",
|
|
114
|
+
branch_id: typeof parsed.branch_id === "string" && parsed.branch_id.trim() !== "" ? parsed.branch_id : "unknown",
|
|
115
|
+
event_id: typeof parsed.event_id === "string" && parsed.event_id.trim() !== "" ? parsed.event_id : null,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
catch {
|
|
120
|
+
// fall through to unknown identifiers for rejected payload receipts
|
|
121
|
+
}
|
|
122
|
+
return { project_id: "unknown", branch_id: "unknown", event_id: null };
|
|
123
|
+
}
|
|
124
|
+
function findRepoRoot(databasePath, explicitRoot) {
|
|
125
|
+
if (explicitRoot) {
|
|
126
|
+
return path_1.default.resolve(explicitRoot);
|
|
127
|
+
}
|
|
128
|
+
let current = path_1.default.dirname(path_1.default.resolve(databasePath));
|
|
129
|
+
while (true) {
|
|
130
|
+
if (fs_1.default.existsSync(path_1.default.join(current, ".mdkg", "config.json"))) {
|
|
131
|
+
return current;
|
|
132
|
+
}
|
|
133
|
+
const parent = path_1.default.dirname(current);
|
|
134
|
+
if (parent === current) {
|
|
135
|
+
throw new Error("repo_root is required when databasePath is not inside an mdkg repo");
|
|
136
|
+
}
|
|
137
|
+
current = parent;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
function withDb(databasePath, fn) {
|
|
141
|
+
const DatabaseSync = loadDatabaseCtor();
|
|
142
|
+
const db = new DatabaseSync(databasePath);
|
|
143
|
+
try {
|
|
144
|
+
db.exec("PRAGMA foreign_keys = ON;");
|
|
145
|
+
return fn(db);
|
|
146
|
+
}
|
|
147
|
+
finally {
|
|
148
|
+
db.close();
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
function readBranchSnapshotHash(databasePath, projectId, branchId) {
|
|
152
|
+
return withDb(databasePath, (db) => {
|
|
153
|
+
const row = db
|
|
154
|
+
.prepare("SELECT current_snapshot_hash FROM project_branch_state WHERE project_id = ? AND branch_id = ?")
|
|
155
|
+
.get(projectId, branchId);
|
|
156
|
+
return row?.current_snapshot_hash === undefined || row.current_snapshot_hash === null
|
|
157
|
+
? null
|
|
158
|
+
: String(row.current_snapshot_hash);
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
function queueName(input) {
|
|
162
|
+
if (input !== undefined) {
|
|
163
|
+
assertNonEmpty(input, "queue_name");
|
|
164
|
+
return input;
|
|
165
|
+
}
|
|
166
|
+
return exports.PROJECT_DB_MATERIALIZER_QUEUE;
|
|
167
|
+
}
|
|
168
|
+
function result(input) {
|
|
169
|
+
return {
|
|
170
|
+
status: input.status,
|
|
171
|
+
queue_name: input.queue_name,
|
|
172
|
+
queue_message: input.queue_message,
|
|
173
|
+
payload: input.payload ?? null,
|
|
174
|
+
reducer: input.reducer ?? null,
|
|
175
|
+
lease: input.lease ?? null,
|
|
176
|
+
receipt: input.receipt ?? null,
|
|
177
|
+
snapshot: input.snapshot ?? null,
|
|
178
|
+
error: input.error ?? null,
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
function failStatus(message, fallback) {
|
|
182
|
+
return message.status === "dead_letter" ? "dead_letter" : fallback;
|
|
183
|
+
}
|
|
184
|
+
function writeMaterializerReceipt(databasePath, input) {
|
|
185
|
+
return (0, project_db_events_1.writeProjectDbReceipt)(databasePath, {
|
|
186
|
+
project_id: input.payload.project_id,
|
|
187
|
+
branch_id: input.payload.branch_id,
|
|
188
|
+
kind: input.kind,
|
|
189
|
+
status: input.status,
|
|
190
|
+
event_id: input.payload.event_id,
|
|
191
|
+
actor: input.actor,
|
|
192
|
+
details: {
|
|
193
|
+
message_id: input.message.message_id,
|
|
194
|
+
queue_name: input.message.queue_name,
|
|
195
|
+
error: input.error,
|
|
196
|
+
},
|
|
197
|
+
now_ms: input.now_ms,
|
|
198
|
+
receipts_path: input.receipts_path,
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
function tryReleaseWriterLease(databasePath, payload, leaseId, leaseOwner, now, receiptsPath) {
|
|
202
|
+
try {
|
|
203
|
+
(0, project_db_events_1.releaseProjectWriterLease)(databasePath, {
|
|
204
|
+
project_id: payload.project_id,
|
|
205
|
+
branch_id: payload.branch_id,
|
|
206
|
+
lease_id: leaseId,
|
|
207
|
+
lease_owner: leaseOwner,
|
|
208
|
+
now_ms: now,
|
|
209
|
+
receipts_path: receiptsPath,
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
catch {
|
|
213
|
+
// The lease may already be committed, conflicted, released, or absent.
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
function enqueueProjectDbMaterialization(databasePath, input) {
|
|
217
|
+
assertNonEmpty(input.message_id, "message_id");
|
|
218
|
+
assertNonEmpty(input.project_id, "project_id");
|
|
219
|
+
assertNonEmpty(input.branch_id, "branch_id");
|
|
220
|
+
assertNonEmpty(input.event_id, "event_id");
|
|
221
|
+
const selectedQueue = queueName(input.queue_name);
|
|
222
|
+
const payload = {
|
|
223
|
+
kind: exports.PROJECT_DB_MATERIALIZER_KIND,
|
|
224
|
+
schema_version: exports.PROJECT_DB_MATERIALIZER_SCHEMA_VERSION,
|
|
225
|
+
project_id: input.project_id,
|
|
226
|
+
branch_id: input.branch_id,
|
|
227
|
+
event_id: input.event_id,
|
|
228
|
+
reducer_name: input.reducer_name,
|
|
229
|
+
reducer_version: input.reducer_version,
|
|
230
|
+
};
|
|
231
|
+
return (0, project_db_queue_1.enqueueProjectQueueMessage)(databasePath, {
|
|
232
|
+
queue_name: selectedQueue,
|
|
233
|
+
message_id: input.message_id,
|
|
234
|
+
dedupe_key: input.dedupe_key === undefined
|
|
235
|
+
? `${input.project_id}:${input.branch_id}:${input.event_id}:${input.reducer_name}:${input.reducer_version}`
|
|
236
|
+
: input.dedupe_key,
|
|
237
|
+
payload,
|
|
238
|
+
available_at_ms: input.available_at_ms,
|
|
239
|
+
max_attempts: input.max_attempts,
|
|
240
|
+
now_ms: input.now_ms,
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
function runNextProjectDbMaterializer(databasePath, input) {
|
|
244
|
+
assertNonEmpty(input.lease_owner, "lease_owner");
|
|
245
|
+
assertPositiveInteger(input.lease_ms, "lease_ms");
|
|
246
|
+
const selectedQueue = queueName(input.queue_name);
|
|
247
|
+
const currentNow = nowMs(input.now_ms);
|
|
248
|
+
const retryAfter = input.retry_after_ms ?? 0;
|
|
249
|
+
assertInteger(retryAfter, "retry_after_ms");
|
|
250
|
+
if (retryAfter < 0) {
|
|
251
|
+
throw new Error("retry_after_ms must be greater than or equal to 0");
|
|
252
|
+
}
|
|
253
|
+
(0, project_db_queue_1.releaseExpiredProjectQueueLeases)(databasePath, {
|
|
254
|
+
queue_name: selectedQueue,
|
|
255
|
+
now_ms: currentNow,
|
|
256
|
+
});
|
|
257
|
+
const claimed = (0, project_db_queue_1.claimProjectQueueMessage)(databasePath, {
|
|
258
|
+
queue_name: selectedQueue,
|
|
259
|
+
lease_owner: input.lease_owner,
|
|
260
|
+
lease_ms: input.lease_ms,
|
|
261
|
+
now_ms: currentNow,
|
|
262
|
+
});
|
|
263
|
+
if (!claimed) {
|
|
264
|
+
return result({ status: "idle", queue_name: selectedQueue, queue_message: null });
|
|
265
|
+
}
|
|
266
|
+
let payload;
|
|
267
|
+
try {
|
|
268
|
+
payload = parsePayload(claimed.payload_json);
|
|
269
|
+
}
|
|
270
|
+
catch (err) {
|
|
271
|
+
const error = err instanceof Error ? err.message : String(err);
|
|
272
|
+
const partial = partialPayloadIds(claimed.payload_json);
|
|
273
|
+
const receipt = writeMaterializerReceipt(databasePath, {
|
|
274
|
+
payload: partial,
|
|
275
|
+
kind: "materializer-invalid-payload",
|
|
276
|
+
status: "rejected",
|
|
277
|
+
actor: input.lease_owner,
|
|
278
|
+
message: claimed,
|
|
279
|
+
error,
|
|
280
|
+
now_ms: currentNow,
|
|
281
|
+
receipts_path: input.receipts_path,
|
|
282
|
+
});
|
|
283
|
+
const dead = (0, project_db_queue_1.deadLetterProjectQueueMessage)(databasePath, {
|
|
284
|
+
queue_name: selectedQueue,
|
|
285
|
+
message_id: claimed.message_id,
|
|
286
|
+
lease_owner: input.lease_owner,
|
|
287
|
+
error,
|
|
288
|
+
now_ms: currentNow,
|
|
289
|
+
});
|
|
290
|
+
return result({
|
|
291
|
+
status: "dead_letter",
|
|
292
|
+
queue_name: selectedQueue,
|
|
293
|
+
queue_message: dead,
|
|
294
|
+
receipt,
|
|
295
|
+
error,
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
const leaseId = `materializer-${safeSegment(claimed.message_id)}-${claimed.attempt_count}`;
|
|
299
|
+
const currentBranchHash = readBranchSnapshotHash(databasePath, payload.project_id, payload.branch_id);
|
|
300
|
+
const baseHash = input.base_snapshot_hash ?? currentBranchHash ?? sha256File(databasePath);
|
|
301
|
+
let lease = null;
|
|
302
|
+
try {
|
|
303
|
+
lease = (0, project_db_events_1.acquireProjectWriterLease)(databasePath, {
|
|
304
|
+
project_id: payload.project_id,
|
|
305
|
+
branch_id: payload.branch_id,
|
|
306
|
+
lease_id: leaseId,
|
|
307
|
+
lease_owner: input.lease_owner,
|
|
308
|
+
base_snapshot_hash: baseHash,
|
|
309
|
+
lease_ms: input.lease_ms,
|
|
310
|
+
now_ms: currentNow,
|
|
311
|
+
});
|
|
312
|
+
if (currentBranchHash && currentBranchHash !== baseHash) {
|
|
313
|
+
const conflict = (0, project_db_events_1.commitProjectWriterLease)(databasePath, {
|
|
314
|
+
project_id: payload.project_id,
|
|
315
|
+
branch_id: payload.branch_id,
|
|
316
|
+
lease_id: leaseId,
|
|
317
|
+
lease_owner: input.lease_owner,
|
|
318
|
+
result_snapshot_hash: sha256File(databasePath),
|
|
319
|
+
now_ms: currentNow,
|
|
320
|
+
receipts_path: input.receipts_path,
|
|
321
|
+
});
|
|
322
|
+
const failed = (0, project_db_queue_1.failProjectQueueMessage)(databasePath, {
|
|
323
|
+
queue_name: selectedQueue,
|
|
324
|
+
message_id: claimed.message_id,
|
|
325
|
+
lease_owner: input.lease_owner,
|
|
326
|
+
error: `snapshot hash mismatch: current ${currentBranchHash}, base ${baseHash}`,
|
|
327
|
+
retry_after_ms: retryAfter,
|
|
328
|
+
now_ms: currentNow,
|
|
329
|
+
});
|
|
330
|
+
return result({
|
|
331
|
+
status: failStatus(failed, "conflict"),
|
|
332
|
+
queue_name: selectedQueue,
|
|
333
|
+
queue_message: failed,
|
|
334
|
+
payload,
|
|
335
|
+
lease: conflict.lease,
|
|
336
|
+
receipt: conflict.receipt,
|
|
337
|
+
error: failed.last_error,
|
|
338
|
+
});
|
|
339
|
+
}
|
|
340
|
+
const reducer = (0, project_db_events_1.applyProjectDbReducer)(databasePath, {
|
|
341
|
+
event_id: payload.event_id,
|
|
342
|
+
reducer_name: payload.reducer_name,
|
|
343
|
+
reducer_version: payload.reducer_version,
|
|
344
|
+
actor: input.lease_owner,
|
|
345
|
+
now_ms: currentNow,
|
|
346
|
+
receipts_path: input.receipts_path,
|
|
347
|
+
});
|
|
348
|
+
if (!reducer.applied && reducer.receipt.status === "rejected") {
|
|
349
|
+
tryReleaseWriterLease(databasePath, payload, leaseId, input.lease_owner, currentNow, input.receipts_path);
|
|
350
|
+
const failed = (0, project_db_queue_1.failProjectQueueMessage)(databasePath, {
|
|
351
|
+
queue_name: selectedQueue,
|
|
352
|
+
message_id: claimed.message_id,
|
|
353
|
+
lease_owner: input.lease_owner,
|
|
354
|
+
error: reducer.receipt.details_json,
|
|
355
|
+
retry_after_ms: retryAfter,
|
|
356
|
+
now_ms: currentNow,
|
|
357
|
+
});
|
|
358
|
+
return result({
|
|
359
|
+
status: failStatus(failed, "rejected"),
|
|
360
|
+
queue_name: selectedQueue,
|
|
361
|
+
queue_message: failed,
|
|
362
|
+
payload,
|
|
363
|
+
reducer,
|
|
364
|
+
lease,
|
|
365
|
+
receipt: reducer.receipt,
|
|
366
|
+
error: failed.last_error,
|
|
367
|
+
});
|
|
368
|
+
}
|
|
369
|
+
const commit = (0, project_db_events_1.commitProjectWriterLease)(databasePath, {
|
|
370
|
+
project_id: payload.project_id,
|
|
371
|
+
branch_id: payload.branch_id,
|
|
372
|
+
lease_id: leaseId,
|
|
373
|
+
lease_owner: input.lease_owner,
|
|
374
|
+
result_snapshot_hash: sha256File(databasePath),
|
|
375
|
+
now_ms: currentNow,
|
|
376
|
+
receipts_path: input.receipts_path,
|
|
377
|
+
});
|
|
378
|
+
if (!commit.committed) {
|
|
379
|
+
const failed = (0, project_db_queue_1.failProjectQueueMessage)(databasePath, {
|
|
380
|
+
queue_name: selectedQueue,
|
|
381
|
+
message_id: claimed.message_id,
|
|
382
|
+
lease_owner: input.lease_owner,
|
|
383
|
+
error: commit.receipt.details_json,
|
|
384
|
+
retry_after_ms: retryAfter,
|
|
385
|
+
now_ms: currentNow,
|
|
386
|
+
});
|
|
387
|
+
return result({
|
|
388
|
+
status: failStatus(failed, "conflict"),
|
|
389
|
+
queue_name: selectedQueue,
|
|
390
|
+
queue_message: failed,
|
|
391
|
+
payload,
|
|
392
|
+
reducer,
|
|
393
|
+
lease: commit.lease,
|
|
394
|
+
receipt: commit.receipt,
|
|
395
|
+
error: failed.last_error,
|
|
396
|
+
});
|
|
397
|
+
}
|
|
398
|
+
const acked = (0, project_db_queue_1.ackProjectQueueMessage)(databasePath, {
|
|
399
|
+
queue_name: selectedQueue,
|
|
400
|
+
message_id: claimed.message_id,
|
|
401
|
+
lease_owner: input.lease_owner,
|
|
402
|
+
now_ms: currentNow,
|
|
403
|
+
});
|
|
404
|
+
const repoRoot = findRepoRoot(databasePath, input.repo_root);
|
|
405
|
+
const config = (0, config_1.loadConfig)(repoRoot);
|
|
406
|
+
const snapshot = (0, project_db_snapshot_1.sealProjectDbSnapshot)(repoRoot, config);
|
|
407
|
+
return result({
|
|
408
|
+
status: reducer.applied ? "applied" : "duplicate",
|
|
409
|
+
queue_name: selectedQueue,
|
|
410
|
+
queue_message: acked,
|
|
411
|
+
payload,
|
|
412
|
+
reducer,
|
|
413
|
+
lease: commit.lease,
|
|
414
|
+
receipt: reducer.receipt,
|
|
415
|
+
snapshot,
|
|
416
|
+
});
|
|
417
|
+
}
|
|
418
|
+
catch (err) {
|
|
419
|
+
const error = err instanceof Error ? err.message : String(err);
|
|
420
|
+
if (lease) {
|
|
421
|
+
tryReleaseWriterLease(databasePath, payload, leaseId, input.lease_owner, currentNow, input.receipts_path);
|
|
422
|
+
}
|
|
423
|
+
const receipt = writeMaterializerReceipt(databasePath, {
|
|
424
|
+
payload,
|
|
425
|
+
kind: "materializer-error",
|
|
426
|
+
status: "rejected",
|
|
427
|
+
actor: input.lease_owner,
|
|
428
|
+
message: claimed,
|
|
429
|
+
error,
|
|
430
|
+
now_ms: currentNow,
|
|
431
|
+
receipts_path: input.receipts_path,
|
|
432
|
+
});
|
|
433
|
+
const failed = (0, project_db_queue_1.failProjectQueueMessage)(databasePath, {
|
|
434
|
+
queue_name: selectedQueue,
|
|
435
|
+
message_id: claimed.message_id,
|
|
436
|
+
lease_owner: input.lease_owner,
|
|
437
|
+
error,
|
|
438
|
+
retry_after_ms: retryAfter,
|
|
439
|
+
now_ms: currentNow,
|
|
440
|
+
});
|
|
441
|
+
return result({
|
|
442
|
+
status: failStatus(failed, "retry"),
|
|
443
|
+
queue_name: selectedQueue,
|
|
444
|
+
queue_message: failed,
|
|
445
|
+
payload,
|
|
446
|
+
lease,
|
|
447
|
+
receipt,
|
|
448
|
+
error,
|
|
449
|
+
});
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
function readProjectDbMaterializerStats(databasePath, input = {}) {
|
|
453
|
+
const selectedQueue = queueName(input.queue_name);
|
|
454
|
+
return {
|
|
455
|
+
queue_name: selectedQueue,
|
|
456
|
+
queue: (0, project_db_queue_1.readProjectQueueStats)(databasePath, {
|
|
457
|
+
queue_name: selectedQueue,
|
|
458
|
+
now_ms: input.now_ms,
|
|
459
|
+
}),
|
|
460
|
+
writer_leases: (0, project_db_events_1.readProjectWriterLeaseStats)(databasePath, { now_ms: input.now_ms }),
|
|
461
|
+
};
|
|
462
|
+
}
|
|
@@ -53,6 +53,24 @@ ON project_queue_message(queue_name, status, available_at_ms, created_at_ms, mes
|
|
|
53
53
|
CREATE INDEX IF NOT EXISTS project_queue_message_lease_idx
|
|
54
54
|
ON project_queue_message(queue_name, status, lease_deadline_ms, created_at_ms, message_id);
|
|
55
55
|
`;
|
|
56
|
+
const QUEUE_CONTROL_MIGRATION_SQL = `
|
|
57
|
+
CREATE TABLE IF NOT EXISTS project_queue (
|
|
58
|
+
queue_name TEXT PRIMARY KEY,
|
|
59
|
+
status TEXT NOT NULL CHECK(status IN ('active', 'paused')),
|
|
60
|
+
paused_reason TEXT,
|
|
61
|
+
created_at_ms INTEGER NOT NULL,
|
|
62
|
+
updated_at_ms INTEGER NOT NULL
|
|
63
|
+
) STRICT;
|
|
64
|
+
|
|
65
|
+
INSERT OR IGNORE INTO project_queue (queue_name, status, paused_reason, created_at_ms, updated_at_ms)
|
|
66
|
+
SELECT DISTINCT
|
|
67
|
+
queue_name,
|
|
68
|
+
'active',
|
|
69
|
+
NULL,
|
|
70
|
+
CAST((julianday('now') - 2440587.5) * 86400000 AS INTEGER),
|
|
71
|
+
CAST((julianday('now') - 2440587.5) * 86400000 AS INTEGER)
|
|
72
|
+
FROM project_queue_message;
|
|
73
|
+
`;
|
|
56
74
|
const EVENTS_RECEIPTS_MIGRATION_SQL = `
|
|
57
75
|
CREATE TABLE IF NOT EXISTS project_event (
|
|
58
76
|
event_id TEXT PRIMARY KEY,
|
|
@@ -162,6 +180,12 @@ const BUILTIN_MIGRATIONS = [
|
|
|
162
180
|
filename: "004_mdkg_project_db_writer_leases.sql",
|
|
163
181
|
sql: WRITER_LEASES_MIGRATION_SQL.trim(),
|
|
164
182
|
},
|
|
183
|
+
{
|
|
184
|
+
ordinal: 5,
|
|
185
|
+
key: "mdkg.project_db.queue_control.v1",
|
|
186
|
+
filename: "005_mdkg_project_db_queue_control.sql",
|
|
187
|
+
sql: QUEUE_CONTROL_MIGRATION_SQL.trim(),
|
|
188
|
+
},
|
|
165
189
|
];
|
|
166
190
|
function loadDatabaseCtor() {
|
|
167
191
|
try {
|