@unson/brainbase-mcp 0.43.0 → 0.44.1
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/contracts/canonical-task/operation-schema.sql +58 -0
- package/contracts/canonical-task/search-indexes.sql +10 -0
- package/contracts/canonical-task/store-schema.sql +77 -0
- package/dist/automation-run.d.ts +213 -0
- package/dist/automation-run.js +479 -0
- package/dist/canonical-task-operations.d.ts +205 -0
- package/dist/canonical-task-operations.js +700 -0
- package/dist/canonical-task-postgres.d.ts +139 -0
- package/dist/canonical-task-postgres.js +554 -0
- package/dist/canonical-task-service.d.ts +13 -0
- package/dist/canonical-task-service.js +55 -3
- package/dist/canonical-task-store-config.d.ts +31 -0
- package/dist/canonical-task-store-config.js +53 -0
- package/dist/config-parser.d.ts +298 -0
- package/dist/config-parser.js +593 -0
- package/dist/connected-onboarding.d.ts +57 -0
- package/dist/connected-onboarding.js +37 -0
- package/dist/durable-wait-http.d.ts +3 -1
- package/dist/durable-wait-http.js +73 -6
- package/dist/durable-waits.d.ts +93 -3
- package/dist/durable-waits.js +320 -8
- package/dist/honcho.d.ts +25 -0
- package/dist/honcho.js +81 -0
- package/dist/judgment-event-adapter.d.ts +72 -0
- package/dist/judgment-event-adapter.js +114 -0
- package/dist/judgment-event-outbox.d.ts +182 -0
- package/dist/judgment-event-outbox.js +568 -0
- package/dist/judgment-execution-outcome.d.ts +46 -0
- package/dist/judgment-execution-outcome.js +149 -0
- package/dist/judgment-host.d.ts +18 -7
- package/dist/judgment-host.js +82 -34
- package/dist/judgment-lifecycle-core.d.ts +198 -0
- package/dist/judgment-lifecycle-core.js +464 -0
- package/dist/judgment-node-evidence.d.ts +57 -0
- package/dist/judgment-node-evidence.js +297 -0
- package/dist/judgment-runtime-resolution.d.ts +264 -0
- package/dist/judgment-runtime-resolution.js +1248 -0
- package/dist/judgment-runtime-resolution.public.d.ts +255 -0
- package/dist/knowledge-event.d.ts +28 -0
- package/dist/knowledge-event.js +114 -0
- package/dist/mcp-runtime.d.ts +75 -0
- package/dist/mcp-runtime.js +188 -0
- package/dist/meeting-automation-runtime.d.ts +102 -0
- package/dist/meeting-automation-runtime.js +249 -0
- package/dist/meeting-automation.d.ts +155 -0
- package/dist/meeting-automation.js +341 -0
- package/dist/meeting-minutes-context-receipt.d.ts +91 -0
- package/dist/meeting-minutes-context-receipt.js +280 -0
- package/dist/meeting-source-sync.d.ts +89 -0
- package/dist/meeting-source-sync.js +230 -0
- package/dist/mesh.d.ts +58 -0
- package/dist/mesh.js +134 -0
- package/dist/onboarding-runtime.d.ts +47 -0
- package/dist/onboarding-runtime.js +4719 -0
- package/dist/routine-cycle.d.ts +51 -0
- package/dist/routine-cycle.js +153 -0
- package/dist/routine-liveness.d.ts +49 -0
- package/dist/routine-liveness.js +227 -0
- package/dist/routine-runtime.d.ts +101 -0
- package/dist/routine-runtime.js +237 -0
- package/dist/routines.d.ts +3 -0
- package/dist/routines.js +3 -0
- package/dist/run-receipt.d.ts +40 -0
- package/dist/run-receipt.js +635 -0
- package/dist/runtime-host.d.ts +64 -0
- package/dist/runtime-host.js +70 -0
- package/dist/world-work.d.ts +16 -0
- package/package.json +115 -3
- package/ui/meeting-minutes.css +122 -0
- package/ui/meeting-minutes.js +316 -0
- package/ui/objective-editor.js +47 -11
- package/ui/world/world-district.js +18 -3
- package/ui/world/world-placement.js +13 -0
- package/ui/world/world-work-rail.js +7 -1
|
@@ -0,0 +1,568 @@
|
|
|
1
|
+
import { closeSync, linkSync, mkdirSync, openSync, readdirSync, readFileSync, renameSync, statSync, unlinkSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { basename, dirname, join } from 'node:path';
|
|
3
|
+
/** The queue format is intentionally independent of a Host, Task, or tenant. */
|
|
4
|
+
export const JUDGMENT_EVENT_OUTBOX_SCHEMA_VERSION = 'judgment_event_outbox.v1';
|
|
5
|
+
export const JUDGMENT_EVENT_DELIVERY_RECEIPT_SCHEMA_VERSION = 'knowledge_event_delivery_receipt.v1';
|
|
6
|
+
export const JUDGMENT_EVENT_READBACK_SCHEMA_VERSION = 'knowledge_cycle_receipt.v1';
|
|
7
|
+
const nodeStorage = {
|
|
8
|
+
mkdir: (path, options) => mkdirSync(path, options),
|
|
9
|
+
readFile: (path) => readFileSync(path, 'utf8'),
|
|
10
|
+
readdir: (path) => readdirSync(path),
|
|
11
|
+
rename: (from, to) => renameSync(from, to),
|
|
12
|
+
unlink: (path) => unlinkSync(path),
|
|
13
|
+
writeFile: (path, value, options) => writeFileSync(path, value, options),
|
|
14
|
+
writeExclusive: (path, value, mode) => {
|
|
15
|
+
const descriptor = openSync(path, 'wx', mode);
|
|
16
|
+
try {
|
|
17
|
+
writeFileSync(descriptor, value);
|
|
18
|
+
}
|
|
19
|
+
finally {
|
|
20
|
+
closeSync(descriptor);
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
link: (from, to) => linkSync(from, to),
|
|
24
|
+
stat: (path) => statSync(path)
|
|
25
|
+
};
|
|
26
|
+
const OUTBOX_DIRNAME = 'knowledge-event-outbox';
|
|
27
|
+
const DEAD_LETTER_DIRNAME = 'knowledge-event-dead-letter';
|
|
28
|
+
const DELIVERY_RECEIPT_DIRNAME = 'knowledge-event-delivery-receipt';
|
|
29
|
+
const DEFAULT_CHANNEL = 'codex-judgment';
|
|
30
|
+
const JSON_SUFFIX = '.json';
|
|
31
|
+
const DEFAULT_STALE_LOCK_MS = 10 * 60 * 1000;
|
|
32
|
+
/** Resolve shared state paths without importing any runtime or repository code. */
|
|
33
|
+
export function resolveJudgmentKnowledgeEventOutboxPaths(options = {}) {
|
|
34
|
+
const stateRoot = options.varDir ?? (options.root ? join(options.root, 'var') : null);
|
|
35
|
+
const outboxDir = options.outboxDir ?? (stateRoot ? join(stateRoot, OUTBOX_DIRNAME, DEFAULT_CHANNEL) : null);
|
|
36
|
+
if (!outboxDir)
|
|
37
|
+
throw new TypeError('judgment outbox requires varDir, root, or outboxDir');
|
|
38
|
+
const sharedRoot = stateRoot ?? dirname(dirname(outboxDir));
|
|
39
|
+
return {
|
|
40
|
+
outboxDir,
|
|
41
|
+
deadLetterDir: options.deadLetterDir ?? join(sharedRoot, DEAD_LETTER_DIRNAME, basename(outboxDir)),
|
|
42
|
+
deliveryReceiptDir: options.deliveryReceiptDir ?? join(sharedRoot, DELIVERY_RECEIPT_DIRNAME, basename(outboxDir)),
|
|
43
|
+
lockPath: options.lockPath ?? `${outboxDir}.sender.lock`
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
function jsonFiles(directory, storage) {
|
|
47
|
+
try {
|
|
48
|
+
return storage.readdir(directory).filter((name) => name.endsWith(JSON_SUFFIX)).sort();
|
|
49
|
+
}
|
|
50
|
+
catch (error) {
|
|
51
|
+
if (isCode(error, 'ENOENT'))
|
|
52
|
+
return [];
|
|
53
|
+
throw error;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
function isCode(error, code) {
|
|
57
|
+
return Boolean(error && typeof error === 'object' && 'code' in error && error.code === code);
|
|
58
|
+
}
|
|
59
|
+
function atomicWrite(target, value, storage) {
|
|
60
|
+
const temporary = `${target}.${process.pid}.${Date.now()}.tmp`;
|
|
61
|
+
storage.writeFile(temporary, `${JSON.stringify(value, null, 2)}\n`, { mode: 0o600 });
|
|
62
|
+
storage.rename(temporary, target);
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Publish a new queue item without replacing a file another sender created.
|
|
66
|
+
* The temporary file is complete before the hardlink makes the target visible,
|
|
67
|
+
* so readers see either the old file or the complete new file.
|
|
68
|
+
*/
|
|
69
|
+
function atomicPublishNoReplace(target, value, storage) {
|
|
70
|
+
const temporary = `${target}.${process.pid}.${Date.now()}.tmp`;
|
|
71
|
+
const serialized = `${JSON.stringify(value, null, 2)}\n`;
|
|
72
|
+
storage.writeFile(temporary, serialized, { mode: 0o600 });
|
|
73
|
+
try {
|
|
74
|
+
// Hard-link publication is required. A writeExclusive fallback can expose
|
|
75
|
+
// a partial target to a concurrent reader, so custom storage adapters must
|
|
76
|
+
// implement this port or fail closed before production use.
|
|
77
|
+
if (typeof storage.link !== 'function')
|
|
78
|
+
throw new Error('judgment_event_outbox_atomic_link_required');
|
|
79
|
+
storage.link(temporary, target);
|
|
80
|
+
}
|
|
81
|
+
finally {
|
|
82
|
+
try {
|
|
83
|
+
storage.unlink(temporary);
|
|
84
|
+
}
|
|
85
|
+
catch { /* publication may have failed */ }
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
function parseJson(path, storage) {
|
|
89
|
+
return JSON.parse(storage.readFile(path));
|
|
90
|
+
}
|
|
91
|
+
const SAFE_EVENT_ID = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,255}$/u;
|
|
92
|
+
function queuedEvent(value, expectedFile) {
|
|
93
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
94
|
+
throw new Error('judgment_event_outbox_invalid');
|
|
95
|
+
const candidate = value;
|
|
96
|
+
if (candidate.schema_version !== JUDGMENT_EVENT_OUTBOX_SCHEMA_VERSION) {
|
|
97
|
+
throw new Error('judgment_event_outbox_schema_invalid');
|
|
98
|
+
}
|
|
99
|
+
if (!candidate.event || typeof candidate.event !== 'object' || Array.isArray(candidate.event)) {
|
|
100
|
+
throw new Error('judgment_event_outbox_event_missing');
|
|
101
|
+
}
|
|
102
|
+
const event = candidate.event;
|
|
103
|
+
if (typeof event.event_id !== 'string' || !SAFE_EVENT_ID.test(event.event_id)) {
|
|
104
|
+
throw new Error('judgment_event_outbox_event_id_invalid');
|
|
105
|
+
}
|
|
106
|
+
if (expectedFile && expectedFile !== `${event.event_id}${JSON_SUFFIX}`) {
|
|
107
|
+
throw new Error('judgment_event_outbox_filename_mismatch');
|
|
108
|
+
}
|
|
109
|
+
if (!candidate.delivery || typeof candidate.delivery !== 'object' || Array.isArray(candidate.delivery)) {
|
|
110
|
+
throw new Error('judgment_event_outbox_delivery_missing');
|
|
111
|
+
}
|
|
112
|
+
return candidate;
|
|
113
|
+
}
|
|
114
|
+
/** Queue one event. Repeating the same event is idempotent; changing its body is a conflict. */
|
|
115
|
+
export function enqueueJudgmentKnowledgeEvent(event, options) {
|
|
116
|
+
if (!event || typeof event.event_id !== 'string' || !event.event_id) {
|
|
117
|
+
throw new Error('knowledge event_id is required');
|
|
118
|
+
}
|
|
119
|
+
if (!options.directory)
|
|
120
|
+
throw new Error('knowledge event outbox directory is required');
|
|
121
|
+
if (!SAFE_EVENT_ID.test(event.event_id)) {
|
|
122
|
+
throw new Error('knowledge event_id is invalid');
|
|
123
|
+
}
|
|
124
|
+
const storage = options.storage ?? nodeStorage;
|
|
125
|
+
const now = options.now ?? (() => new Date());
|
|
126
|
+
storage.mkdir(options.directory, { recursive: true, mode: 0o700 });
|
|
127
|
+
const target = join(options.directory, `${event.event_id}.json`);
|
|
128
|
+
try {
|
|
129
|
+
const existing = queuedEvent(parseJson(target, storage));
|
|
130
|
+
if (JSON.stringify(existing.event) !== JSON.stringify(event)) {
|
|
131
|
+
throw new Error('knowledge_event_outbox_conflict');
|
|
132
|
+
}
|
|
133
|
+
return { status: 'existing', path: target };
|
|
134
|
+
}
|
|
135
|
+
catch (error) {
|
|
136
|
+
if (!isCode(error, 'ENOENT'))
|
|
137
|
+
throw error;
|
|
138
|
+
}
|
|
139
|
+
const value = {
|
|
140
|
+
schema_version: JUDGMENT_EVENT_OUTBOX_SCHEMA_VERSION,
|
|
141
|
+
event,
|
|
142
|
+
delivery: { attempt: 1, queued_at: now().toISOString() }
|
|
143
|
+
};
|
|
144
|
+
try {
|
|
145
|
+
atomicPublishNoReplace(target, value, storage);
|
|
146
|
+
return { status: 'queued', path: target };
|
|
147
|
+
}
|
|
148
|
+
catch (error) {
|
|
149
|
+
if (!isCode(error, 'EEXIST'))
|
|
150
|
+
throw error;
|
|
151
|
+
// Another process won publication. Re-read its complete record and only
|
|
152
|
+
// accept it when the event body is identical; never overwrite a winner.
|
|
153
|
+
const existing = queuedEvent(parseJson(target, storage), `${event.event_id}${JSON_SUFFIX}`);
|
|
154
|
+
if (JSON.stringify(existing.event) !== JSON.stringify(event)) {
|
|
155
|
+
throw new Error('knowledge_event_outbox_conflict');
|
|
156
|
+
}
|
|
157
|
+
return { status: 'existing', path: target };
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
function normalizeErrorCode(error, fallback) {
|
|
161
|
+
const code = error && typeof error === 'object' && 'code' in error ? error.code : null;
|
|
162
|
+
return typeof code === 'string' && /^[A-Za-z0-9_.:-]{1,100}$/u.test(code) ? code : fallback;
|
|
163
|
+
}
|
|
164
|
+
function errorPhase(error, fallback) {
|
|
165
|
+
const phase = error && typeof error === 'object' && 'phase' in error ? error.phase : null;
|
|
166
|
+
return phase === 'post' || phase === 'readback' ? phase : fallback;
|
|
167
|
+
}
|
|
168
|
+
function isTransportError(error) {
|
|
169
|
+
return Boolean(error && typeof error === 'object' && 'transport' in error && error.transport === true)
|
|
170
|
+
|| normalizeErrorCode(error, '') === 'knowledge_event_delivery_transport_failed';
|
|
171
|
+
}
|
|
172
|
+
function errorStatus(error) {
|
|
173
|
+
const status = error && typeof error === 'object' && 'httpStatus' in error ? error.httpStatus : null;
|
|
174
|
+
return typeof status === 'number' && Number.isInteger(status) ? status : null;
|
|
175
|
+
}
|
|
176
|
+
function confirmedReceipt(value, eventId, candidateId, projectCode) {
|
|
177
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
178
|
+
return false;
|
|
179
|
+
const receipt = value;
|
|
180
|
+
const post = receipt.post && typeof receipt.post === 'object' ? receipt.post : null;
|
|
181
|
+
const readback = receipt.readback && typeof receipt.readback === 'object'
|
|
182
|
+
? receipt.readback
|
|
183
|
+
: null;
|
|
184
|
+
return receipt.schema_version === JUDGMENT_EVENT_DELIVERY_RECEIPT_SCHEMA_VERSION
|
|
185
|
+
&& receipt.status === 'confirmed'
|
|
186
|
+
&& receipt.event_id === eventId
|
|
187
|
+
&& receipt.candidate_id === candidateId
|
|
188
|
+
&& receipt.project_code === projectCode
|
|
189
|
+
&& post?.event_id === eventId
|
|
190
|
+
&& post.candidate_id === candidateId
|
|
191
|
+
&& readback?.schema_version === JUDGMENT_EVENT_READBACK_SCHEMA_VERSION
|
|
192
|
+
&& readback.event_id === eventId
|
|
193
|
+
&& readback.candidate_id === candidateId
|
|
194
|
+
&& readback.processing_stage === 'retrievable'
|
|
195
|
+
&& readback.semantic_state === 'active';
|
|
196
|
+
}
|
|
197
|
+
function projectCode(event) {
|
|
198
|
+
const candidate = event;
|
|
199
|
+
if (typeof candidate.project_code === 'string' && candidate.project_code)
|
|
200
|
+
return candidate.project_code;
|
|
201
|
+
const scope = candidate.applicability_scope;
|
|
202
|
+
if (scope && typeof scope === 'object' && !Array.isArray(scope)) {
|
|
203
|
+
const value = scope.project_code;
|
|
204
|
+
return typeof value === 'string' && value ? value : null;
|
|
205
|
+
}
|
|
206
|
+
return null;
|
|
207
|
+
}
|
|
208
|
+
function readConfirmedReceipt(path, eventId, candidateId, code, storage) {
|
|
209
|
+
try {
|
|
210
|
+
return confirmedReceipt(parseJson(path, storage), eventId, candidateId, code);
|
|
211
|
+
}
|
|
212
|
+
catch {
|
|
213
|
+
return false;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
function moveToDeadLetter(target, file, directory, storage) {
|
|
217
|
+
storage.mkdir(directory, { recursive: true, mode: 0o700 });
|
|
218
|
+
if (typeof storage.link !== 'function')
|
|
219
|
+
throw new Error('judgment_event_outbox_atomic_link_required');
|
|
220
|
+
const base = file.endsWith(JSON_SUFFIX) ? file.slice(0, -JSON_SUFFIX.length) : file;
|
|
221
|
+
for (let attempt = 0; attempt < 100; attempt += 1) {
|
|
222
|
+
const destinationName = attempt === 0
|
|
223
|
+
? file
|
|
224
|
+
: `${base}.${process.pid}.${Date.now()}.${attempt}${JSON_SUFFIX}`;
|
|
225
|
+
try {
|
|
226
|
+
// A hard-link followed by unlink preserves an existing dead-letter with
|
|
227
|
+
// the same source filename. EEXIST selects a fresh history filename;
|
|
228
|
+
// rename would replace the older record on POSIX.
|
|
229
|
+
storage.link(target, join(directory, destinationName));
|
|
230
|
+
storage.unlink(target);
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
catch (error) {
|
|
234
|
+
if (!isCode(error, 'EEXIST'))
|
|
235
|
+
throw error;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
throw new Error('judgment_event_dead_letter_destination_exhausted');
|
|
239
|
+
}
|
|
240
|
+
function deliverCore(options) {
|
|
241
|
+
const storage = options.storage ?? nodeStorage;
|
|
242
|
+
const now = options.now ?? (() => new Date());
|
|
243
|
+
const maxAttempts = options.maxAttempts ?? 5;
|
|
244
|
+
const limit = options.limit ?? Number.POSITIVE_INFINITY;
|
|
245
|
+
const backoff = options.backoff ?? null;
|
|
246
|
+
const allFiles = jsonFiles(options.paths.outboxDir, storage);
|
|
247
|
+
const delivery = options.delivery;
|
|
248
|
+
if (!delivery) {
|
|
249
|
+
return Promise.resolve({
|
|
250
|
+
status: 'unavailable',
|
|
251
|
+
delivered: 0,
|
|
252
|
+
failed: 0,
|
|
253
|
+
retryable: allFiles.length,
|
|
254
|
+
dead_lettered: 0,
|
|
255
|
+
pending: allFiles.length
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
let delivered = 0;
|
|
259
|
+
let failed = 0;
|
|
260
|
+
let retryable = 0;
|
|
261
|
+
let deadLettered = 0;
|
|
262
|
+
let deferred = 0;
|
|
263
|
+
let processed = 0;
|
|
264
|
+
let lastErrorCode;
|
|
265
|
+
const process = async () => {
|
|
266
|
+
for (const file of allFiles) {
|
|
267
|
+
if (processed >= limit)
|
|
268
|
+
break;
|
|
269
|
+
const target = join(options.paths.outboxDir, file);
|
|
270
|
+
let queued;
|
|
271
|
+
try {
|
|
272
|
+
queued = queuedEvent(parseJson(target, storage), file);
|
|
273
|
+
}
|
|
274
|
+
catch {
|
|
275
|
+
processed += 1;
|
|
276
|
+
failed += 1;
|
|
277
|
+
moveToDeadLetter(target, file, options.paths.deadLetterDir, storage);
|
|
278
|
+
deadLettered += 1;
|
|
279
|
+
continue;
|
|
280
|
+
}
|
|
281
|
+
const dueAt = Date.parse(queued.delivery.next_attempt_at ?? '');
|
|
282
|
+
if (backoff && Number.isFinite(dueAt) && dueAt > now().getTime()) {
|
|
283
|
+
deferred += 1;
|
|
284
|
+
continue;
|
|
285
|
+
}
|
|
286
|
+
processed += 1;
|
|
287
|
+
const eventId = queued.event.event_id;
|
|
288
|
+
const code = projectCode(queued.event);
|
|
289
|
+
const postAck = queued.delivery.post_ack;
|
|
290
|
+
const attempt = queued.delivery.attempt || 1;
|
|
291
|
+
const readbackAttempt = queued.delivery.readback_attempt || 1;
|
|
292
|
+
if (!postAck && attempt > maxAttempts) {
|
|
293
|
+
failed += 1;
|
|
294
|
+
moveToDeadLetter(target, file, options.paths.deadLetterDir, storage);
|
|
295
|
+
deadLettered += 1;
|
|
296
|
+
continue;
|
|
297
|
+
}
|
|
298
|
+
const receiptPath = join(options.paths.deliveryReceiptDir, `${eventId}.json`);
|
|
299
|
+
if (postAck && readConfirmedReceipt(receiptPath, eventId, postAck.candidate_id, code, storage)) {
|
|
300
|
+
try {
|
|
301
|
+
storage.unlink(target);
|
|
302
|
+
delivered += 1;
|
|
303
|
+
continue;
|
|
304
|
+
}
|
|
305
|
+
catch {
|
|
306
|
+
// The confirmed receipt remains authoritative; retain the queue for retry.
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
let failureKind = postAck ? 'readback' : 'post';
|
|
310
|
+
let lastStatus = null;
|
|
311
|
+
let codeForError = 'knowledge_event_delivery_unknown_error';
|
|
312
|
+
try {
|
|
313
|
+
let confirmedPost = postAck;
|
|
314
|
+
if (!confirmedPost) {
|
|
315
|
+
failureKind = 'post';
|
|
316
|
+
confirmedPost = await delivery.post(queued.event);
|
|
317
|
+
if (confirmedPost.event_id !== eventId || !confirmedPost.candidate_id) {
|
|
318
|
+
const error = new Error('knowledge_event_delivery_ack_identity_mismatch');
|
|
319
|
+
error.code = 'knowledge_event_delivery_ack_identity_mismatch';
|
|
320
|
+
error.phase = 'post';
|
|
321
|
+
throw error;
|
|
322
|
+
}
|
|
323
|
+
lastStatus = typeof confirmedPost.http_status === 'number' ? confirmedPost.http_status : null;
|
|
324
|
+
const savedAt = now().toISOString();
|
|
325
|
+
const ackQueued = {
|
|
326
|
+
...queued,
|
|
327
|
+
delivery: {
|
|
328
|
+
...queued.delivery,
|
|
329
|
+
post_ack: confirmedPost,
|
|
330
|
+
phase: 'readback_pending',
|
|
331
|
+
readback_attempt: queued.delivery.readback_attempt || 1,
|
|
332
|
+
ack_saved_at: savedAt
|
|
333
|
+
}
|
|
334
|
+
};
|
|
335
|
+
atomicWrite(target, ackQueued, storage);
|
|
336
|
+
queued = ackQueued;
|
|
337
|
+
}
|
|
338
|
+
failureKind = 'readback';
|
|
339
|
+
const readback = await delivery.readback({ event: queued.event, post: confirmedPost });
|
|
340
|
+
if (readback.schema_version !== JUDGMENT_EVENT_READBACK_SCHEMA_VERSION
|
|
341
|
+
|| readback.event_id !== eventId
|
|
342
|
+
|| readback.candidate_id !== confirmedPost.candidate_id
|
|
343
|
+
|| readback.processing_stage !== 'retrievable'
|
|
344
|
+
|| readback.semantic_state !== 'active') {
|
|
345
|
+
const error = new Error('knowledge_event_readback_identity_or_state_mismatch');
|
|
346
|
+
error.code = 'knowledge_event_readback_identity_or_state_mismatch';
|
|
347
|
+
error.phase = 'readback';
|
|
348
|
+
throw error;
|
|
349
|
+
}
|
|
350
|
+
const receipt = {
|
|
351
|
+
schema_version: JUDGMENT_EVENT_DELIVERY_RECEIPT_SCHEMA_VERSION,
|
|
352
|
+
status: 'confirmed',
|
|
353
|
+
event_id: eventId,
|
|
354
|
+
candidate_id: confirmedPost.candidate_id,
|
|
355
|
+
project_code: code,
|
|
356
|
+
post: confirmedPost,
|
|
357
|
+
readback,
|
|
358
|
+
confirmed_at: now().toISOString()
|
|
359
|
+
};
|
|
360
|
+
storage.mkdir(options.paths.deliveryReceiptDir, { recursive: true, mode: 0o700 });
|
|
361
|
+
atomicWrite(receiptPath, receipt, storage);
|
|
362
|
+
storage.unlink(target);
|
|
363
|
+
delivered += 1;
|
|
364
|
+
}
|
|
365
|
+
catch (error) {
|
|
366
|
+
failed += 1;
|
|
367
|
+
codeForError = normalizeErrorCode(error, codeForError);
|
|
368
|
+
const phase = errorPhase(error, failureKind);
|
|
369
|
+
const transport = isTransportError(error);
|
|
370
|
+
const nextAttempt = attempt + (phase === 'readback' || transport ? 0 : 1);
|
|
371
|
+
const nextReadbackAttempt = readbackAttempt + (phase === 'readback' && !transport ? 1 : 0);
|
|
372
|
+
const consecutiveFailures = (queued.delivery.consecutive_failures ?? 0) + 1;
|
|
373
|
+
const failedAt = now();
|
|
374
|
+
const next = {
|
|
375
|
+
...queued,
|
|
376
|
+
delivery: {
|
|
377
|
+
...queued.delivery,
|
|
378
|
+
attempt: nextAttempt,
|
|
379
|
+
...(phase === 'readback' ? { readback_attempt: nextReadbackAttempt } : {}),
|
|
380
|
+
last_failed_at: failedAt.toISOString(),
|
|
381
|
+
last_status: errorStatus(error) ?? lastStatus,
|
|
382
|
+
last_error_code: codeForError,
|
|
383
|
+
...(backoff ? {
|
|
384
|
+
consecutive_failures: consecutiveFailures,
|
|
385
|
+
next_attempt_at: new Date(failedAt.getTime() + Math.min(backoff.baseMs * 2 ** (consecutiveFailures - 1), backoff.maxMs)).toISOString()
|
|
386
|
+
} : {})
|
|
387
|
+
}
|
|
388
|
+
};
|
|
389
|
+
const shouldDeadLetter = (!phase || phase === 'post')
|
|
390
|
+
&& (errorStatus(error) === 409 || next.delivery.attempt > maxAttempts)
|
|
391
|
+
|| (phase === 'readback' && next.delivery.readback_attempt !== undefined
|
|
392
|
+
&& next.delivery.readback_attempt > maxAttempts);
|
|
393
|
+
atomicWrite(target, next, storage);
|
|
394
|
+
if (shouldDeadLetter) {
|
|
395
|
+
moveToDeadLetter(target, file, options.paths.deadLetterDir, storage);
|
|
396
|
+
deadLettered += 1;
|
|
397
|
+
}
|
|
398
|
+
else {
|
|
399
|
+
retryable += 1;
|
|
400
|
+
}
|
|
401
|
+
lastErrorCode = codeForError;
|
|
402
|
+
if (transport)
|
|
403
|
+
break;
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
return {
|
|
407
|
+
status: 'processed',
|
|
408
|
+
delivered,
|
|
409
|
+
failed,
|
|
410
|
+
retryable,
|
|
411
|
+
dead_lettered: deadLettered,
|
|
412
|
+
deferred,
|
|
413
|
+
pending: jsonFiles(options.paths.outboxDir, storage).length,
|
|
414
|
+
...(lastErrorCode ? { last_error_code: lastErrorCode } : {})
|
|
415
|
+
};
|
|
416
|
+
};
|
|
417
|
+
return process();
|
|
418
|
+
}
|
|
419
|
+
function acquireSenderLock(lockPath, storage, now, staleAfterMs) {
|
|
420
|
+
storage.mkdir(dirname(lockPath), { recursive: true, mode: 0o700 });
|
|
421
|
+
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
422
|
+
try {
|
|
423
|
+
storage.writeExclusive(lockPath, `${process.pid} ${now().toISOString()}\n`, 0o600);
|
|
424
|
+
return () => {
|
|
425
|
+
try {
|
|
426
|
+
storage.unlink(lockPath);
|
|
427
|
+
}
|
|
428
|
+
catch { /* another sender released it */ }
|
|
429
|
+
};
|
|
430
|
+
}
|
|
431
|
+
catch (error) {
|
|
432
|
+
if (!isCode(error, 'EEXIST'))
|
|
433
|
+
throw error;
|
|
434
|
+
let stale = true;
|
|
435
|
+
try {
|
|
436
|
+
stale = now().getTime() - storage.stat(lockPath).mtimeMs > staleAfterMs;
|
|
437
|
+
}
|
|
438
|
+
catch { /* stale */ }
|
|
439
|
+
if (!stale)
|
|
440
|
+
return null;
|
|
441
|
+
try {
|
|
442
|
+
storage.unlink(lockPath);
|
|
443
|
+
}
|
|
444
|
+
catch { /* another sender replaced it */ }
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
return null;
|
|
448
|
+
}
|
|
449
|
+
/** Execute an operation under the shared sender lock, or return busy. */
|
|
450
|
+
export async function withJudgmentOutboxSenderLock(options) {
|
|
451
|
+
const release = acquireSenderLock(options.lockPath, options.storage ?? nodeStorage, options.now ?? (() => new Date()), options.staleAfterMs ?? DEFAULT_STALE_LOCK_MS);
|
|
452
|
+
if (!release)
|
|
453
|
+
return { status: 'busy' };
|
|
454
|
+
try {
|
|
455
|
+
return await options.run();
|
|
456
|
+
}
|
|
457
|
+
finally {
|
|
458
|
+
release();
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
/** Deliver queued events while keeping retry, receipt, and lock behavior in the common core. */
|
|
462
|
+
export async function deliverJudgmentKnowledgeEventOutbox(options) {
|
|
463
|
+
const now = options.now ?? (() => new Date());
|
|
464
|
+
const run = () => deliverCore(options);
|
|
465
|
+
if (options.acquireLock === false)
|
|
466
|
+
return run();
|
|
467
|
+
const result = await withJudgmentOutboxSenderLock({
|
|
468
|
+
lockPath: options.paths.lockPath,
|
|
469
|
+
storage: options.storage,
|
|
470
|
+
now,
|
|
471
|
+
staleAfterMs: options.staleLockAfterMs,
|
|
472
|
+
run
|
|
473
|
+
});
|
|
474
|
+
if ('status' in result && result.status === 'busy') {
|
|
475
|
+
return {
|
|
476
|
+
status: 'busy',
|
|
477
|
+
delivered: 0,
|
|
478
|
+
failed: 0,
|
|
479
|
+
retryable: 0,
|
|
480
|
+
dead_lettered: 0,
|
|
481
|
+
pending: jsonFiles(options.paths.outboxDir, options.storage ?? nodeStorage).length
|
|
482
|
+
};
|
|
483
|
+
}
|
|
484
|
+
return result;
|
|
485
|
+
}
|
|
486
|
+
/** Local summary used by Host diagnostics; it never calls the network. */
|
|
487
|
+
export function summarizeJudgmentKnowledgeEventOutbox(options) {
|
|
488
|
+
const storage = options.storage ?? nodeStorage;
|
|
489
|
+
const now = options.now ?? (() => new Date());
|
|
490
|
+
const overdueAfterMs = options.overdueAfterMs ?? 15 * 60 * 1000;
|
|
491
|
+
let failing = 0;
|
|
492
|
+
let overdue = 0;
|
|
493
|
+
let latest = null;
|
|
494
|
+
const files = jsonFiles(options.paths.outboxDir, storage);
|
|
495
|
+
for (const file of files) {
|
|
496
|
+
let queued;
|
|
497
|
+
try {
|
|
498
|
+
queued = queuedEvent(parseJson(join(options.paths.outboxDir, file), storage), file);
|
|
499
|
+
}
|
|
500
|
+
catch {
|
|
501
|
+
failing += 1;
|
|
502
|
+
continue;
|
|
503
|
+
}
|
|
504
|
+
const failedAt = queued.delivery.last_failed_at;
|
|
505
|
+
if (!failedAt) {
|
|
506
|
+
const queuedAt = Date.parse(queued.delivery.queued_at);
|
|
507
|
+
if (Number.isFinite(queuedAt) && now().getTime() - queuedAt > overdueAfterMs)
|
|
508
|
+
overdue += 1;
|
|
509
|
+
continue;
|
|
510
|
+
}
|
|
511
|
+
failing += 1;
|
|
512
|
+
if (!latest || failedAt > latest.failedAt)
|
|
513
|
+
latest = { failedAt, code: queued.delivery.last_error_code ?? null };
|
|
514
|
+
}
|
|
515
|
+
return {
|
|
516
|
+
pending: files.length,
|
|
517
|
+
failing: failing + overdue,
|
|
518
|
+
dead_lettered: jsonFiles(options.paths.deadLetterDir, storage).length,
|
|
519
|
+
last_error_code: latest?.code ?? (overdue > 0 ? 'knowledge_event_delivery_overdue' : null)
|
|
520
|
+
};
|
|
521
|
+
}
|
|
522
|
+
export async function listJudgmentKnowledgeEventOutboxExceptions(options) {
|
|
523
|
+
const storage = options.storage ?? nodeStorage;
|
|
524
|
+
return jsonFiles(options.directory, storage).map((file) => {
|
|
525
|
+
try {
|
|
526
|
+
const queued = queuedEvent(parseJson(join(options.directory, file), storage), file);
|
|
527
|
+
return {
|
|
528
|
+
code: 'knowledge_event_outbox',
|
|
529
|
+
event_id: queued.event.event_id,
|
|
530
|
+
path: file,
|
|
531
|
+
created_at: queued.delivery.queued_at
|
|
532
|
+
};
|
|
533
|
+
}
|
|
534
|
+
catch {
|
|
535
|
+
return { code: 'knowledge_event_outbox_corrupt', path: file };
|
|
536
|
+
}
|
|
537
|
+
});
|
|
538
|
+
}
|
|
539
|
+
export async function listKnowledgeEventDeadLetters(options) {
|
|
540
|
+
const storage = options.storage ?? nodeStorage;
|
|
541
|
+
return jsonFiles(options.directory, storage).map((file) => {
|
|
542
|
+
let queued = null;
|
|
543
|
+
try {
|
|
544
|
+
queued = queuedEvent(parseJson(join(options.directory, file), storage), file);
|
|
545
|
+
}
|
|
546
|
+
catch { /* corrupt dead letter */ }
|
|
547
|
+
return {
|
|
548
|
+
code: 'knowledge_event_dead_letter',
|
|
549
|
+
event_id: queued?.event.event_id ?? file.replace(/\.json$/u, ''),
|
|
550
|
+
path: file,
|
|
551
|
+
created_at: queued?.delivery.last_failed_at ?? queued?.delivery.queued_at ?? new Date().toISOString()
|
|
552
|
+
};
|
|
553
|
+
});
|
|
554
|
+
}
|
|
555
|
+
/**
|
|
556
|
+
* Host adapter candidate: the Host supplies only a completed event; runtime
|
|
557
|
+
* path selection is injected here and can later be replaced without changing
|
|
558
|
+
* the Host lifecycle or public response.
|
|
559
|
+
*/
|
|
560
|
+
export function createJudgmentKnowledgeEventOutboxSink(options) {
|
|
561
|
+
return {
|
|
562
|
+
enqueue: (event) => enqueueJudgmentKnowledgeEvent(event, {
|
|
563
|
+
directory: options.paths.outboxDir,
|
|
564
|
+
storage: options.storage,
|
|
565
|
+
now: options.now
|
|
566
|
+
})
|
|
567
|
+
};
|
|
568
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Portable execution outcome contract shared by a judgment Host and its
|
|
3
|
+
* adapters. The contract deliberately carries references only; transcript,
|
|
4
|
+
* process, Task, and delivery details stay at the Host boundary.
|
|
5
|
+
*/
|
|
6
|
+
export declare const JUDGMENT_EXECUTION_OUTCOME_SCHEMA_VERSION: "judgment_execution_outcome.v1";
|
|
7
|
+
export type JudgmentExecutionHostType = 'codex' | 'claude-code';
|
|
8
|
+
export type JudgmentExecutionScope = 'host_turn' | 'external_effect';
|
|
9
|
+
export type JudgmentExecutionStatus = 'completed' | 'partial' | 'failed' | 'blocked' | 'unknown';
|
|
10
|
+
export type JudgmentExecutionStage = 'open' | 'resolve' | 'execute' | 'finalize' | 'deliver' | 'readback';
|
|
11
|
+
export type JudgmentExecutionEvidenceState = 'confirmed' | 'unconfirmed' | 'no_data';
|
|
12
|
+
export interface JudgmentExecutionHost {
|
|
13
|
+
type: JudgmentExecutionHostType;
|
|
14
|
+
adapter_id: string;
|
|
15
|
+
adapter_version: string;
|
|
16
|
+
}
|
|
17
|
+
export interface JudgmentExecutionFailure {
|
|
18
|
+
code: string;
|
|
19
|
+
summary: string;
|
|
20
|
+
upstream_code: string | null;
|
|
21
|
+
retryable: boolean;
|
|
22
|
+
}
|
|
23
|
+
export interface JudgmentExecutionEvidence {
|
|
24
|
+
state: JudgmentExecutionEvidenceState;
|
|
25
|
+
refs: string[];
|
|
26
|
+
}
|
|
27
|
+
export interface JudgmentExecutionOutcome {
|
|
28
|
+
schema_version: typeof JUDGMENT_EXECUTION_OUTCOME_SCHEMA_VERSION;
|
|
29
|
+
host: JudgmentExecutionHost;
|
|
30
|
+
execution_id: string;
|
|
31
|
+
turn_id: string;
|
|
32
|
+
scope: JudgmentExecutionScope;
|
|
33
|
+
status: JudgmentExecutionStatus;
|
|
34
|
+
stage: JudgmentExecutionStage;
|
|
35
|
+
failure: JudgmentExecutionFailure | null;
|
|
36
|
+
resume_from: string | null;
|
|
37
|
+
evidence: JudgmentExecutionEvidence;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Validate and normalize a host execution outcome.
|
|
41
|
+
*
|
|
42
|
+
* The function intentionally rejects unknown keys and unsafe filesystem
|
|
43
|
+
* references so adapters cannot accidentally turn private runtime state into
|
|
44
|
+
* a public contract.
|
|
45
|
+
*/
|
|
46
|
+
export declare function normalizeJudgmentExecutionOutcome(input: unknown): JudgmentExecutionOutcome;
|