agentlas 0.7.0 → 0.9.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/CHANGELOG.md +199 -0
  2. package/README.md +161 -18
  3. package/bin/agentlas.cjs +8 -8
  4. package/engine/agentlas-core-harness.cjs +212 -0
  5. package/engine/agentlas-desktop-loadout.cjs +527 -0
  6. package/engine/agentlas-doctor.cjs +1 -1
  7. package/engine/agentlas-experience-exchange.cjs +835 -85
  8. package/engine/agentlas-experience-intake.cjs +444 -0
  9. package/engine/agentlas-experience-mcp.cjs +580 -18
  10. package/engine/agentlas-i18n.cjs +10 -10
  11. package/engine/agentlas-input.cjs +5 -4
  12. package/engine/agentlas-mcp-env.cjs +219 -0
  13. package/engine/agentlas-mcp-wrapper.cjs +51 -0
  14. package/engine/agentlas-memory-governance.cjs +1029 -0
  15. package/engine/agentlas-native-host.cjs +129 -39
  16. package/engine/agentlas-parity.cjs +339 -154
  17. package/engine/agentlas-repl.cjs +306 -31
  18. package/engine/agentlas-workforce.cjs +2991 -0
  19. package/engine/agentlas-workload-routing.cjs +523 -0
  20. package/engine/agentlas.cjs +1619 -234
  21. package/engine/bootstrap-schema.sql +1 -1
  22. package/engine/experience-taxonomy-v1.json +49 -0
  23. package/package.json +8 -4
  24. package/scripts/gen-bootstrap-schema.sh +0 -23
  25. package/test/bootstrap-race.cjs +0 -47
  26. package/test/capture-runtime-guard.cjs +0 -122
  27. package/test/cloud-asset-restore.cjs +0 -423
  28. package/test/cloud-cas-client.cjs +0 -333
  29. package/test/cloud-owner-restore.cjs +0 -183
  30. package/test/cloud-runtime-paths.cjs +0 -40
  31. package/test/cloud-save-publish.cjs +0 -487
  32. package/test/credential-env-regression.cjs +0 -52
  33. package/test/engine-hardening-regression.cjs +0 -74
  34. package/test/experience-exchange-contract.cjs +0 -569
  35. package/test/experience-mcp-contract.cjs +0 -391
  36. package/test/fixtures/portable-experience-bundle-v1-golden.json +0 -124
  37. package/test/login-loopback-security.cjs +0 -115
  38. package/test/mcp-config-isolation.cjs +0 -36
  39. package/test/permission-mapping.cjs +0 -180
  40. package/test/route-regression.cjs +0 -357
  41. package/test/run-api-regression.cjs +0 -322
  42. package/test/runtime-env-protection.cjs +0 -89
  43. package/test/semver-precedence.cjs +0 -39
  44. package/test/smoke.sh +0 -93
  45. package/test/sqlite-driver-probe.cjs +0 -22
  46. package/test/terminal-ui-regression.cjs +0 -477
  47. package/test/timeout-regression.cjs +0 -218
  48. package/test/tool-workspace-boundary.cjs +0 -165
  49. package/test/update-safety.cjs +0 -376
@@ -0,0 +1,444 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * Successful-run -> private Operational Experience candidate bridge.
5
+ *
6
+ * Storage boundary:
7
+ * - Run/evidence and value-free intake decisions use the existing shared
8
+ * `run_events` ledger.
9
+ * - Candidates use the existing Portable Experience exchange store.
10
+ * - Preferences never enter Operational Experience; they remain local Taste
11
+ * observations referencing curated Memory, with no copied preference text.
12
+ */
13
+ const crypto = require("node:crypto");
14
+ const exchange = require("./agentlas-experience-exchange.cjs");
15
+
16
+ const RUN_RECEIPT_SCHEMA = "agentlas.run-receipt.v1";
17
+ const INTAKE_POLICY_VERSION = "agentlas-terminal-operational-intake.v1";
18
+ const ID_RE = /^[A-Za-z0-9][A-Za-z0-9._:/@-]{2,255}$/;
19
+ const HASH_RE = /^sha256:[0-9a-f]{64}$/;
20
+ const OPERATIONAL_KINDS = new Set(["procedure", "decision", "risk"]);
21
+
22
+ function canonicalHash(value) {
23
+ return exchange.canonicalHash(value);
24
+ }
25
+
26
+ function digestHex(...parts) {
27
+ const hash = crypto.createHash("sha256");
28
+ for (const part of parts) hash.update(String(part ?? "")).update("\0");
29
+ return hash.digest("hex");
30
+ }
31
+
32
+ function opaqueId(prefix, ...parts) {
33
+ return `${prefix}:${digestHex(...parts).slice(0, 32)}`;
34
+ }
35
+
36
+ function cleanId(value, fallback) {
37
+ const text = String(value || "").trim();
38
+ return ID_RE.test(text) ? text : fallback;
39
+ }
40
+
41
+ function codePointSlice(value, max) {
42
+ return Array.from(String(value || "").normalize("NFC").trim()).slice(0, max).join("");
43
+ }
44
+
45
+ function runtimeEnvironment(input = {}) {
46
+ const platform = String(input.os || process.platform).toLowerCase();
47
+ const arch = String(input.arch || process.arch).toLowerCase();
48
+ const runtime = String(input.runtime || "terminal").toLowerCase().replace(/[^a-z0-9._-]/g, "-").slice(0, 64);
49
+ const osName = platform === "darwin" || platform === "macos"
50
+ ? "macos"
51
+ : platform === "win32" || platform === "windows"
52
+ ? "windows"
53
+ : platform === "linux" ? "linux" : "unknown";
54
+ const archName = arch === "arm64" || arch === "aarch64" ? "arm64" : arch === "x64" || arch === "x86_64" ? "x64" : "unknown";
55
+ if (osName === "unknown" || archName === "unknown" || !/^[a-z0-9][a-z0-9._-]{1,63}$/.test(runtime)) return null;
56
+ const constraints = [
57
+ `agentlas.env.v1/os/${osName}`,
58
+ `agentlas.env.v1/arch/${archName}`,
59
+ `agentlas.env.v1/runtime/${runtime}`,
60
+ ];
61
+ return {
62
+ runtime,
63
+ os: osName,
64
+ arch: archName,
65
+ constraints,
66
+ fingerprintHash: canonicalHash({ runtime, os: osName, arch: archName }),
67
+ };
68
+ }
69
+
70
+ function createRunReceipt(input) {
71
+ const environment = runtimeEnvironment(input.environment);
72
+ if (!environment) throw new Error("RunReceipt requires a canonical runtime environment.");
73
+ const taskSignature = exchange.canonicalSourceTaskId(input.taskSignature);
74
+ if (!taskSignature) throw new Error("RunReceipt requires one canonical task signature.");
75
+ if (!input.exactBase || !ID_RE.test(String(input.exactBase.agentReleaseId || ""))) {
76
+ throw new Error("RunReceipt requires an exact agent definition release.");
77
+ }
78
+ const runId = cleanId(input.runId, opaqueId("run", input.exactBase.agentReleaseId, taskSignature, input.createdAt));
79
+ const idempotencyKey = cleanId(input.idempotencyKey, opaqueId("run-key", runId));
80
+ const createdAt = input.createdAt || new Date().toISOString();
81
+ const provider = cleanId(input.model?.provider, "terminal-runtime");
82
+ const modelId = cleanId(input.model?.modelId, provider);
83
+ const outcome = ["succeeded", "partial", "failed", "cancelled"].includes(input.outcome?.status)
84
+ ? input.outcome.status
85
+ : "failed";
86
+ const metrics = input.metrics || {};
87
+ const promptTokens = Math.max(0, Math.trunc(Number(metrics.promptTokens) || 0));
88
+ const completionTokens = Math.max(0, Math.trunc(Number(metrics.completionTokens) || 0));
89
+ const totalTokens = Math.max(
90
+ promptTokens + completionTokens,
91
+ Math.max(0, Math.trunc(Number(metrics.totalTokens) || 0)),
92
+ );
93
+ const draft = {
94
+ schemaVersion: RUN_RECEIPT_SCHEMA,
95
+ kind: "agentlas-run-receipt",
96
+ receiptId: "pending:run-receipt",
97
+ idempotencyKey,
98
+ runId,
99
+ agentDefinitionReleaseId: input.exactBase.agentReleaseId,
100
+ experiencePackReleaseId: input.experiencePackReleaseId && ID_RE.test(input.experiencePackReleaseId) ? input.experiencePackReleaseId : null,
101
+ variantId: null,
102
+ taskSignature: {
103
+ kind: taskSignature,
104
+ hash: canonicalHash({ kind: taskSignature, locale: input.locale || "und" }),
105
+ locale: String(input.locale || "und").slice(0, 20),
106
+ },
107
+ environment: {
108
+ runtime: environment.runtime,
109
+ os: environment.os,
110
+ arch: environment.arch,
111
+ fingerprintHash: environment.fingerprintHash,
112
+ },
113
+ resources: {
114
+ mcp: (input.mcp || []).slice(0, 64).flatMap((item) => {
115
+ const catalogId = cleanId(item?.catalogId, null);
116
+ if (!catalogId) return [];
117
+ const status = ["recommended", "approved", "connected", "skipped", "missing-key", "failed", "degraded"].includes(item.status)
118
+ ? item.status
119
+ : "approved";
120
+ return [{ catalogId, status, resolvedVersion: item.resolvedVersion || null, fallbackFor: item.fallbackFor || null }];
121
+ }),
122
+ skills: [],
123
+ model: { provider, modelId },
124
+ },
125
+ outcome: { status: outcome, failureCode: input.outcome?.failureCode ? codePointSlice(input.outcome.failureCode, 120) : null },
126
+ verification: { verdict: "unverified", method: "none", verifierRef: null, evidenceRefs: [] },
127
+ metricsEligible: false,
128
+ metrics: {
129
+ promptTokens,
130
+ completionTokens,
131
+ totalTokens,
132
+ durationMs: Math.max(0, Math.trunc(Number(metrics.durationMs) || 0)),
133
+ retryCount: Math.max(0, Math.trunc(Number(metrics.retryCount) || 0)),
134
+ },
135
+ sideEffects: { occurred: input.sideEffects?.occurred === true, adverse: input.sideEffects?.adverse === true, evidenceRefs: [] },
136
+ privacy: { rawPromptIncluded: false, rawTranscriptIncluded: false, rawLocalPathsIncluded: false, credentialValuesIncluded: false },
137
+ createdAt,
138
+ signature: null,
139
+ };
140
+ const receiptId = opaqueId("run-receipt", idempotencyKey, input.exactBase.agentReleaseId, taskSignature);
141
+ // Core public RunReceipt hashes the canonical payload without receiptHash or
142
+ // the optional transport signature. Keeping signature:null out of the hash
143
+ // preserves Web/Desktop verification parity.
144
+ const { signature: _signature, ...hashPayload } = { ...draft, receiptId };
145
+ const receiptHash = canonicalHash(hashPayload);
146
+ return { ...draft, receiptId, receiptHash };
147
+ }
148
+
149
+ function validateRunReceipt(receipt) {
150
+ const required = [
151
+ "schemaVersion", "kind", "receiptId", "idempotencyKey", "receiptHash", "runId",
152
+ "agentDefinitionReleaseId", "experiencePackReleaseId", "variantId", "taskSignature",
153
+ "environment", "resources", "outcome", "verification", "metricsEligible", "metrics",
154
+ "sideEffects", "privacy", "createdAt", "signature",
155
+ ];
156
+ if (!receipt || Object.keys(receipt).some((key) => !required.includes(key)) || required.some((key) => !(key in receipt))) {
157
+ throw new Error("Terminal RunReceipt shape drifted from Core v1.");
158
+ }
159
+ if (receipt.schemaVersion !== RUN_RECEIPT_SCHEMA || receipt.kind !== "agentlas-run-receipt") throw new Error("Terminal RunReceipt schema is invalid.");
160
+ for (const key of ["receiptId", "idempotencyKey", "runId", "agentDefinitionReleaseId"]) if (!ID_RE.test(String(receipt[key] || ""))) throw new Error(`RunReceipt ${key} is invalid.`);
161
+ if (!HASH_RE.test(receipt.receiptHash) || !HASH_RE.test(receipt.taskSignature?.hash) || !HASH_RE.test(receipt.environment?.fingerprintHash)) throw new Error("RunReceipt hash is invalid.");
162
+ const { receiptHash: claimedHash, signature: _signature, ...hashPayload } = receipt;
163
+ if (claimedHash !== canonicalHash(hashPayload)) throw new Error("RunReceipt hash does not match the Core canonical payload.");
164
+ if (receipt.privacy?.rawPromptIncluded !== false || receipt.privacy?.rawTranscriptIncluded !== false || receipt.privacy?.rawLocalPathsIncluded !== false || receipt.privacy?.credentialValuesIncluded !== false) {
165
+ throw new Error("RunReceipt privacy flags must all be false.");
166
+ }
167
+ if (receipt.signature !== null) throw new Error("Portable RunReceipt signature must be null.");
168
+ return receipt;
169
+ }
170
+
171
+ function tableExists(db, name) {
172
+ try { return Boolean(db.prepare("SELECT 1 FROM sqlite_master WHERE type='table' AND name=?").get(name)); }
173
+ catch { return false; }
174
+ }
175
+
176
+ function appendRunEvent(db, input) {
177
+ if (!db || !tableExists(db, "run_events")) return false;
178
+ const id = input.id;
179
+ if (db.prepare("SELECT 1 FROM run_events WHERE id=?").get(id)) return false;
180
+ const seqRow = db.prepare("SELECT COALESCE(MAX(seq), -1) + 1 AS seq FROM run_events WHERE run_id=?").get(input.runId);
181
+ db.prepare(
182
+ "INSERT OR IGNORE INTO run_events (id,run_id,seq,ts,kind,chat_id,automation_id,node_id,agent_id,payload_json) VALUES (?,?,?,?,?,NULL,NULL,NULL,?,?)",
183
+ ).run(id, input.runId, Number(seqRow?.seq || 0), input.ts, input.kind, input.agentId || null, JSON.stringify(input.payload));
184
+ return true;
185
+ }
186
+
187
+ function persistRunReceipt(db, receipt, agentId) {
188
+ validateRunReceipt(receipt);
189
+ appendRunEvent(db, {
190
+ id: opaqueId("event", "experience-run-receipt", receipt.receiptId),
191
+ runId: receipt.runId,
192
+ ts: receipt.createdAt,
193
+ kind: "experience-run-receipt",
194
+ agentId,
195
+ payload: receipt,
196
+ });
197
+ return receipt;
198
+ }
199
+
200
+ function recordIntakeDecision(db, input) {
201
+ const sourceHash = digestHex(INTAKE_POLICY_VERSION, input.agentId, input.memoryId || "none", input.exactBase?.agentReleaseId || "none", input.environmentKey || "none");
202
+ appendRunEvent(db, {
203
+ id: opaqueId("event", "experience-intake", sourceHash),
204
+ runId: input.runId,
205
+ ts: input.ts,
206
+ kind: input.kind || "experience-intake-decision",
207
+ agentId: input.agentId,
208
+ payload: {
209
+ schemaVersion: "agentlas.terminal-experience-intake-receipt.v1",
210
+ sourceMemoryRefHash: `sha256:${sourceHash}`,
211
+ status: input.status,
212
+ reasonCodes: [...new Set(input.reasonCodes || [])].sort(),
213
+ candidateId: input.candidateId || null,
214
+ bundleId: input.bundleId || null,
215
+ exactAgentReleaseId: input.exactBase?.agentReleaseId || null,
216
+ environmentKey: input.environmentKey || null,
217
+ privacy: { sourceContentIncluded: false, rawPromptIncluded: false, rawTranscriptIncluded: false },
218
+ networkUsed: false,
219
+ published: false,
220
+ attached: false,
221
+ promoted: false,
222
+ },
223
+ });
224
+ }
225
+
226
+ function existingCandidate(userDataDir, itemId) {
227
+ const state = exchange.loadExchangeState(userDataDir);
228
+ for (const row of state.bundles) {
229
+ try {
230
+ const validation = exchange.readStoredBundle(userDataDir, row.bundleId);
231
+ if (validation.bundle.items.some((item) => item.experienceItemId === itemId)) return { row, validation };
232
+ } catch { /* corrupted unrelated rows are surfaced by list/inspect; intake continues fail-closed */ }
233
+ }
234
+ return null;
235
+ }
236
+
237
+ function candidateType(memoryKind) {
238
+ return memoryKind === "risk" ? "warning" : "procedure";
239
+ }
240
+
241
+ function buildCandidateBundle(input) {
242
+ const summary = codePointSlice(input.memory.content, 320);
243
+ const scopeHash = exchange.projectScopeHash(input.cwd);
244
+ const packKey = digestHex(INTAKE_POLICY_VERSION, input.agentId, input.exactBase.agentDefinitionId, input.exactBase.agentReleaseId, scopeHash, ...input.environment.constraints);
245
+ const candidateKey = digestHex(packKey, input.memory.id, summary, ...input.taskSignatures);
246
+ const experiencePackId = `exp:${packKey.slice(0, 32)}`;
247
+ const releaseId = `experience-release:${candidateKey.slice(0, 32)}`;
248
+ const itemId = `experience-item:${candidateKey.slice(0, 32)}`;
249
+ const createdAt = input.receipt.createdAt;
250
+ const item = {
251
+ schemaVersion: "agentlas.experience-item.v1",
252
+ kind: "agentlas-experience-item",
253
+ experienceItemId: itemId,
254
+ experiencePackId,
255
+ experiencePackReleaseId: releaseId,
256
+ type: candidateType(input.memory.kind),
257
+ summary,
258
+ instructions: [summary],
259
+ taskSignatures: [...new Set(input.taskSignatures)].sort(),
260
+ environmentConstraints: [...input.environment.constraints],
261
+ evidenceReceiptIds: [input.receipt.receiptId],
262
+ supersedesItemIds: [],
263
+ confidence: input.memory.confidence === "high" ? 0.85 : input.memory.confidence === "low" ? 0.4 : 0.65,
264
+ status: "candidate",
265
+ privacyScope: "private",
266
+ createdAt,
267
+ };
268
+ const bundle = {
269
+ schemaVersion: exchange.BUNDLE_SCHEMA,
270
+ kind: "agentlas-experience-bundle",
271
+ bundleId: "exb_" + "0".repeat(48),
272
+ bundleHash: `sha256:${"0".repeat(64)}`,
273
+ requestedVisibility: "private",
274
+ pack: {
275
+ schemaVersion: "agentlas.experience-pack.v1",
276
+ kind: "agentlas-experience-pack",
277
+ experiencePackId,
278
+ releaseId,
279
+ ownerRef: "owner:local-terminal",
280
+ version: "0.0.1",
281
+ baseCompatibility: {
282
+ agentDefinitionId: input.exactBase.agentDefinitionId,
283
+ compatibleBaseReleaseIds: [input.exactBase.agentReleaseId],
284
+ },
285
+ itemIds: [itemId],
286
+ evidenceReceiptIds: [input.receipt.receiptId],
287
+ mcpRequirements: [],
288
+ containsBasePackageMaterial: false,
289
+ contentHash: `sha256:${"0".repeat(64)}`,
290
+ visibility: "private",
291
+ status: "draft",
292
+ createdAt,
293
+ },
294
+ items: [item],
295
+ sourceAttestations: [],
296
+ privacy: {
297
+ basePackageMaterialIncluded: false,
298
+ rawPromptIncluded: false,
299
+ rawTranscriptIncluded: false,
300
+ rawLocalPathsIncluded: false,
301
+ credentialValuesIncluded: false,
302
+ },
303
+ };
304
+ bundle.pack.contentHash = exchange.experiencePackContentHash(bundle);
305
+ bundle.bundleHash = exchange.experienceBundleHash(bundle);
306
+ bundle.bundleId = exchange.experienceBundleId(bundle);
307
+ return { validation: exchange.validateExperienceBundle(bundle), itemId };
308
+ }
309
+
310
+ function captureOperationalCandidate(input) {
311
+ const issues = exchange.portableExperienceSafetyIssues(input.memory.content);
312
+ const sensitivity = String(input.memory.sensitivity || "internal").trim().toLowerCase();
313
+ if (!["internal", "public"].includes(sensitivity)) issues.push("sensitive-memory");
314
+ if (input.memory.scope === "user_identity") issues.push("user-specific-memory-scope");
315
+ if (issues.length) return { status: "blocked", reasonCodes: [...new Set(issues)].sort() };
316
+ const tasks = exchange.deriveCanonicalTaskClasses(input.taskHint, {
317
+ declaredTaskClasses: input.taskSignatures,
318
+ }).taskIds;
319
+ if (!tasks.length) return { status: "skipped", reasonCodes: ["task-taxonomy-unavailable"] };
320
+ const { validation, itemId } = buildCandidateBundle({ ...input, taskSignatures: tasks });
321
+ const existing = existingCandidate(input.userDataDir, itemId);
322
+ if (existing) return {
323
+ status: "existing",
324
+ reasonCodes: ["idempotent-existing-candidate"],
325
+ candidateId: itemId,
326
+ bundleId: existing.row.bundleId,
327
+ row: existing.row,
328
+ };
329
+ const row = exchange.saveLocalBundle(input.userDataDir, validation, { cwd: input.cwd });
330
+ return { status: "candidate-created", reasonCodes: [], candidateId: itemId, bundleId: row.bundleId, row };
331
+ }
332
+
333
+ function finalizeAgentExecution(input) {
334
+ const now = input.createdAt || new Date().toISOString();
335
+ const result = {
336
+ receipt: null,
337
+ candidates: [],
338
+ blocked: 0,
339
+ skipped: 0,
340
+ tasteObservations: 0,
341
+ networkUsed: false,
342
+ published: false,
343
+ attached: false,
344
+ promoted: false,
345
+ };
346
+ if (!input.agent?.id || !input.exactBase?.agentDefinitionId || !input.exactBase?.agentReleaseId) return result;
347
+ const environment = runtimeEnvironment(input.environment);
348
+ const taskResolution = exchange.deriveCanonicalTaskClasses(input.taskHint, { declaredTaskClasses: input.taskSignatures });
349
+ if (!environment || !taskResolution.taskIds.length) return result;
350
+ const receipt = createRunReceipt({
351
+ runId: input.runId,
352
+ idempotencyKey: input.idempotencyKey,
353
+ exactBase: input.exactBase,
354
+ experiencePackReleaseId: input.experiencePackReleaseId,
355
+ taskSignature: taskResolution.taskIds[0],
356
+ environment,
357
+ model: input.model,
358
+ mcp: input.mcp,
359
+ outcome: input.outcome,
360
+ metrics: input.metrics,
361
+ sideEffects: input.sideEffects,
362
+ locale: input.locale,
363
+ createdAt: now,
364
+ });
365
+ persistRunReceipt(input.db, receipt, input.agent.id);
366
+ result.receipt = receipt;
367
+ if (receipt.outcome.status !== "succeeded") return result;
368
+
369
+ const memories = Array.isArray(input.curatedMemories) ? input.curatedMemories : [];
370
+ for (const memory of memories) {
371
+ if (!memory?.id || !memory.kind) continue;
372
+ if (memory.kind === "preference") {
373
+ recordIntakeDecision(input.db, {
374
+ runId: receipt.runId,
375
+ ts: now,
376
+ kind: "taste-draft-observation",
377
+ agentId: input.agent.id,
378
+ memoryId: memory.id,
379
+ exactBase: input.exactBase,
380
+ environmentKey: environment.fingerprintHash,
381
+ status: "local-observation",
382
+ reasonCodes: ["preference-private-taste-only", "pairwise-evidence-required"],
383
+ });
384
+ result.tasteObservations += 1;
385
+ continue;
386
+ }
387
+ if (!OPERATIONAL_KINDS.has(memory.kind)) {
388
+ recordIntakeDecision(input.db, {
389
+ runId: receipt.runId, ts: now, agentId: input.agent.id, memoryId: memory.id,
390
+ exactBase: input.exactBase, environmentKey: environment.fingerprintHash,
391
+ status: "skipped", reasonCodes: ["non-operational-memory-kind"],
392
+ });
393
+ result.skipped += 1;
394
+ continue;
395
+ }
396
+ const captured = captureOperationalCandidate({
397
+ userDataDir: input.userDataDir,
398
+ cwd: input.cwd,
399
+ agentId: input.agent.id,
400
+ exactBase: input.exactBase,
401
+ environment,
402
+ memory,
403
+ receipt,
404
+ taskHint: input.taskHint,
405
+ taskSignatures: taskResolution.taskIds,
406
+ });
407
+ recordIntakeDecision(input.db, {
408
+ runId: receipt.runId,
409
+ ts: now,
410
+ agentId: input.agent.id,
411
+ memoryId: memory.id,
412
+ exactBase: input.exactBase,
413
+ environmentKey: environment.fingerprintHash,
414
+ status: captured.status,
415
+ reasonCodes: captured.reasonCodes,
416
+ candidateId: captured.candidateId,
417
+ bundleId: captured.bundleId,
418
+ });
419
+ if (captured.status === "blocked") result.blocked += 1;
420
+ else if (captured.status === "skipped") result.skipped += 1;
421
+ else result.candidates.push(captured);
422
+ }
423
+ return result;
424
+ }
425
+
426
+ function listRunEvents(db, kind, agentId = null) {
427
+ if (!db || !tableExists(db, "run_events")) return [];
428
+ const rows = agentId
429
+ ? db.prepare("SELECT payload_json FROM run_events WHERE kind=? AND agent_id=? ORDER BY ts DESC,id ASC").all(kind, agentId)
430
+ : db.prepare("SELECT payload_json FROM run_events WHERE kind=? ORDER BY ts DESC,id ASC").all(kind);
431
+ return rows.flatMap((row) => { try { return [JSON.parse(row.payload_json)]; } catch { return []; } });
432
+ }
433
+
434
+ module.exports = {
435
+ RUN_RECEIPT_SCHEMA,
436
+ INTAKE_POLICY_VERSION,
437
+ runtimeEnvironment,
438
+ createRunReceipt,
439
+ validateRunReceipt,
440
+ persistRunReceipt,
441
+ finalizeAgentExecution,
442
+ captureOperationalCandidate,
443
+ listRunEvents,
444
+ };