@principles/core 1.138.0 → 1.140.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/dist/runtime-v2/__tests__/internalization-integrity-remediation.test.js +108 -9
- package/dist/runtime-v2/__tests__/internalization-integrity-remediation.test.js.map +1 -1
- package/dist/runtime-v2/__tests__/mainline-product-path.test.js +126 -45
- package/dist/runtime-v2/__tests__/mainline-product-path.test.js.map +1 -1
- package/dist/runtime-v2/internalization-chain-integrity-read-model.d.ts +4 -0
- package/dist/runtime-v2/internalization-chain-integrity-read-model.d.ts.map +1 -1
- package/dist/runtime-v2/internalization-chain-integrity-read-model.js +18 -0
- package/dist/runtime-v2/internalization-chain-integrity-read-model.js.map +1 -1
- package/dist/runtime-v2/internalization-integrity-remediation.d.ts +23 -0
- package/dist/runtime-v2/internalization-integrity-remediation.d.ts.map +1 -1
- package/dist/runtime-v2/internalization-integrity-remediation.js +475 -76
- package/dist/runtime-v2/internalization-integrity-remediation.js.map +1 -1
- package/package.json +1 -1
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
import * as fs from 'fs';
|
|
2
2
|
import * as path from 'path';
|
|
3
|
+
import * as crypto from 'crypto';
|
|
3
4
|
import Database from 'better-sqlite3';
|
|
5
|
+
import { Value } from '@sinclair/typebox/value';
|
|
4
6
|
import { extractPIMetadata } from './internalization-chain-integrity-read-model.js';
|
|
5
7
|
import { createRemediationResult, remediationAction } from './remediation-contract.js';
|
|
6
8
|
import { SqliteRunStore } from './store/run/sqlite-run-store.js';
|
|
9
|
+
import { RunRecordSchema, RuntimeKindSchema } from './runtime-protocol.js';
|
|
7
10
|
export class InternalizationIntegrityRemediation {
|
|
8
11
|
dbPath;
|
|
9
12
|
constructor(opts) {
|
|
@@ -202,21 +205,41 @@ export class InternalizationIntegrityRemediation {
|
|
|
202
205
|
skippedCount++;
|
|
203
206
|
continue;
|
|
204
207
|
}
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
208
|
+
try {
|
|
209
|
+
this.forceExpireAndMarkRunFailed(sl.taskId);
|
|
210
|
+
repairedCount++;
|
|
211
|
+
actions.push(remediationAction({
|
|
212
|
+
action: 'force_expire_lease',
|
|
213
|
+
targetId: sl.taskId,
|
|
214
|
+
taskId: sl.taskId,
|
|
215
|
+
type: 'lease_stuck',
|
|
216
|
+
severity: 'warning',
|
|
217
|
+
previousState: 'leased',
|
|
218
|
+
nextState: 'pending',
|
|
219
|
+
previousStatus: 'leased',
|
|
220
|
+
newStatus: 'pending',
|
|
221
|
+
recommendedAction: 'force_expire_lease',
|
|
222
|
+
reason: `Task ${sl.taskId} had expired lease (owner: ${sl.leaseOwner ?? 'unknown'}) — force-expired to pending and latest run marked as failed`,
|
|
223
|
+
}));
|
|
224
|
+
}
|
|
225
|
+
catch (err) {
|
|
226
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
227
|
+
actions.push(remediationAction({
|
|
228
|
+
action: 'refuse_repair',
|
|
229
|
+
targetId: sl.taskId,
|
|
230
|
+
taskId: sl.taskId,
|
|
231
|
+
type: 'lease_stuck',
|
|
232
|
+
severity: 'error',
|
|
233
|
+
previousState: 'leased',
|
|
234
|
+
nextState: 'leased',
|
|
235
|
+
previousStatus: 'leased',
|
|
236
|
+
newStatus: 'leased',
|
|
237
|
+
recommendedAction: 'manual_intervention',
|
|
238
|
+
reason: `Cannot safely force-expire lease for task ${sl.taskId}: ${msg}. nextAction: Manually update/delete the task/run in state.db`,
|
|
239
|
+
}));
|
|
240
|
+
warnings.push(`Cannot safely force-expire lease for task ${sl.taskId}: ${msg}`);
|
|
241
|
+
skippedCount++;
|
|
242
|
+
}
|
|
220
243
|
}
|
|
221
244
|
}
|
|
222
245
|
// ── 3. Fix orphaned running runs ───────────────────────────────────
|
|
@@ -255,21 +278,41 @@ export class InternalizationIntegrityRemediation {
|
|
|
255
278
|
skippedCount++;
|
|
256
279
|
continue;
|
|
257
280
|
}
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
281
|
+
try {
|
|
282
|
+
this.markOrphanedRunFailed(or.runId, or.taskId);
|
|
283
|
+
repairedCount++;
|
|
284
|
+
actions.push(remediationAction({
|
|
285
|
+
action: 'mark_run_failed',
|
|
286
|
+
targetId: or.runId,
|
|
287
|
+
taskId: or.taskId,
|
|
288
|
+
type: 'running_run_stuck',
|
|
289
|
+
severity: 'error',
|
|
290
|
+
previousState: 'running',
|
|
291
|
+
nextState: 'failed',
|
|
292
|
+
previousStatus: 'running',
|
|
293
|
+
newStatus: 'failed',
|
|
294
|
+
recommendedAction: 'mark_run_failed',
|
|
295
|
+
reason: `Run ${or.runId} for task ${or.taskId} was 'running' with task status ${or.taskStatus ?? 'none'} — marked as failed (recovery repair)`,
|
|
296
|
+
}));
|
|
297
|
+
}
|
|
298
|
+
catch (err) {
|
|
299
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
300
|
+
actions.push(remediationAction({
|
|
301
|
+
action: 'refuse_repair',
|
|
302
|
+
targetId: or.runId,
|
|
303
|
+
taskId: or.taskId,
|
|
304
|
+
type: 'running_run_stuck',
|
|
305
|
+
severity: 'error',
|
|
306
|
+
previousState: 'running',
|
|
307
|
+
nextState: 'running',
|
|
308
|
+
previousStatus: 'running',
|
|
309
|
+
newStatus: 'running',
|
|
310
|
+
recommendedAction: 'manual_intervention',
|
|
311
|
+
reason: `Cannot safely mark stuck run as failed: ${msg}. nextAction: Manually update/delete the run row in state.db`,
|
|
312
|
+
}));
|
|
313
|
+
warnings.push(`Cannot safely mark stuck run ${or.runId} as failed: ${msg}`);
|
|
314
|
+
skippedCount++;
|
|
315
|
+
}
|
|
273
316
|
}
|
|
274
317
|
}
|
|
275
318
|
// ── 4. Quarantine malformed run rows ────────────────────────────────
|
|
@@ -314,22 +357,109 @@ export class InternalizationIntegrityRemediation {
|
|
|
314
357
|
}));
|
|
315
358
|
}
|
|
316
359
|
else {
|
|
317
|
-
|
|
318
|
-
|
|
360
|
+
try {
|
|
361
|
+
this.quarantineMalformedRun(mr.runId, mr.error);
|
|
362
|
+
repairedCount++;
|
|
363
|
+
actions.push(remediationAction({
|
|
364
|
+
action: 'quarantine_malformed_run',
|
|
365
|
+
targetId: mr.runId,
|
|
366
|
+
taskId: mr.taskId,
|
|
367
|
+
type: 'malformed_run_row',
|
|
368
|
+
severity: 'warning',
|
|
369
|
+
previousState: mr.currentStatus ?? 'unknown',
|
|
370
|
+
nextState: 'failed',
|
|
371
|
+
previousStatus: mr.currentStatus ?? 'unknown',
|
|
372
|
+
newStatus: 'failed',
|
|
373
|
+
recommendedAction: 'quarantine_malformed_run',
|
|
374
|
+
reason: `Run ${mr.runId} (task ${mr.taskId}) failed schema validation — quarantined as failed + storage_unavailable. Error: ${mr.error}`,
|
|
375
|
+
}));
|
|
376
|
+
}
|
|
377
|
+
catch (err) {
|
|
378
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
379
|
+
actions.push(remediationAction({
|
|
380
|
+
action: 'refuse_repair',
|
|
381
|
+
targetId: mr.runId,
|
|
382
|
+
taskId: mr.taskId,
|
|
383
|
+
type: 'malformed_run_row',
|
|
384
|
+
severity: 'error',
|
|
385
|
+
previousState: mr.currentStatus ?? 'unknown',
|
|
386
|
+
nextState: mr.currentStatus ?? 'unknown',
|
|
387
|
+
previousStatus: mr.currentStatus ?? 'unknown',
|
|
388
|
+
newStatus: mr.currentStatus ?? 'unknown',
|
|
389
|
+
recommendedAction: 'manual_intervention',
|
|
390
|
+
reason: `Cannot safely quarantine malformed run ${mr.runId}: ${msg}. nextAction: Manually delete or update the run row in state.db`,
|
|
391
|
+
}));
|
|
392
|
+
warnings.push(`Cannot safely quarantine malformed run ${mr.runId}: ${msg}`);
|
|
393
|
+
skippedCount++;
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
// ── 5. Supplement succeeded runs for task_succeeded_no_succeeded_run ──
|
|
398
|
+
const succeededTasksNoRun = this.detectSucceededTasksWithoutSucceededRun();
|
|
399
|
+
for (const st of succeededTasksNoRun) {
|
|
400
|
+
if (params.dryRun) {
|
|
319
401
|
actions.push(remediationAction({
|
|
320
|
-
action: '
|
|
321
|
-
targetId:
|
|
322
|
-
taskId:
|
|
323
|
-
type: '
|
|
324
|
-
severity: '
|
|
325
|
-
previousState:
|
|
326
|
-
nextState: '
|
|
327
|
-
previousStatus:
|
|
328
|
-
newStatus: '
|
|
329
|
-
recommendedAction: '
|
|
330
|
-
reason: `
|
|
402
|
+
action: 'supplement_succeeded_run',
|
|
403
|
+
targetId: st.taskId,
|
|
404
|
+
taskId: st.taskId,
|
|
405
|
+
type: 'task_succeeded_no_succeeded_run',
|
|
406
|
+
severity: 'error',
|
|
407
|
+
previousState: 'missing',
|
|
408
|
+
nextState: 'succeeded',
|
|
409
|
+
previousStatus: 'missing',
|
|
410
|
+
newStatus: 'succeeded',
|
|
411
|
+
recommendedAction: 'supplement_succeeded_run',
|
|
412
|
+
reason: `Task ${st.taskId} is succeeded but has no succeeded run — would supplement a canonical succeeded run`,
|
|
331
413
|
}));
|
|
332
414
|
}
|
|
415
|
+
else {
|
|
416
|
+
try {
|
|
417
|
+
this.supplementSucceededRun(st.taskId, st.resultRef);
|
|
418
|
+
repairedCount++;
|
|
419
|
+
actions.push(remediationAction({
|
|
420
|
+
action: 'supplement_succeeded_run',
|
|
421
|
+
targetId: st.taskId,
|
|
422
|
+
taskId: st.taskId,
|
|
423
|
+
type: 'task_succeeded_no_succeeded_run',
|
|
424
|
+
severity: 'error',
|
|
425
|
+
previousState: 'missing',
|
|
426
|
+
nextState: 'succeeded',
|
|
427
|
+
previousStatus: 'missing',
|
|
428
|
+
newStatus: 'succeeded',
|
|
429
|
+
recommendedAction: 'supplement_succeeded_run',
|
|
430
|
+
reason: `Task ${st.taskId} is succeeded but had no succeeded run — supplemented a canonical succeeded run`,
|
|
431
|
+
}));
|
|
432
|
+
}
|
|
433
|
+
catch (err) {
|
|
434
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
435
|
+
actions.push(remediationAction({
|
|
436
|
+
action: 'refuse_repair',
|
|
437
|
+
targetId: st.taskId,
|
|
438
|
+
taskId: st.taskId,
|
|
439
|
+
type: 'task_succeeded_no_succeeded_run',
|
|
440
|
+
severity: 'error',
|
|
441
|
+
previousState: 'missing',
|
|
442
|
+
nextState: 'missing',
|
|
443
|
+
previousStatus: 'missing',
|
|
444
|
+
newStatus: 'missing',
|
|
445
|
+
recommendedAction: 'manual_intervention',
|
|
446
|
+
reason: `Cannot safely supplement run for ${st.taskId}: ${msg}. nextAction: Manually create a run row in state.db`,
|
|
447
|
+
}));
|
|
448
|
+
skippedCount++;
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
if (!params.dryRun) {
|
|
453
|
+
// Perform a post-repair verification check
|
|
454
|
+
const postBrokenDreamers = this.detectBrokenDreamers();
|
|
455
|
+
const postStuckLeases = this.detectStuckLeases();
|
|
456
|
+
const postOrphanedRuns = this.detectOrphanedRuns();
|
|
457
|
+
const postMalformedRuns = this.detectMalformedRuns();
|
|
458
|
+
const postSucceededTasksNoRun = this.detectSucceededTasksWithoutSucceededRun();
|
|
459
|
+
const totalUnresolved = postBrokenDreamers.length + postStuckLeases.length + postOrphanedRuns.length + postMalformedRuns.length + postSucceededTasksNoRun.length;
|
|
460
|
+
if (totalUnresolved > 0) {
|
|
461
|
+
console.warn(`[Integrity Check Post-Repair] ${totalUnresolved} unresolved issue(s) remaining.`);
|
|
462
|
+
}
|
|
333
463
|
}
|
|
334
464
|
return createRemediationResult({
|
|
335
465
|
mode: params.dryRun ? 'dry_run' : 'confirm',
|
|
@@ -490,19 +620,29 @@ export class InternalizationIntegrityRemediation {
|
|
|
490
620
|
const rows = db.prepare('SELECT * FROM runs').all();
|
|
491
621
|
const results = [];
|
|
492
622
|
for (const row of rows) {
|
|
623
|
+
let isMalformed = false;
|
|
624
|
+
let errMsg = '';
|
|
493
625
|
try {
|
|
494
626
|
SqliteRunStore.rowToRecord(row);
|
|
627
|
+
// If it is schema-valid but has been quarantined by integrity-repair
|
|
628
|
+
const reason = typeof row.reason === 'string' ? row.reason : '';
|
|
629
|
+
if (row.execution_status === 'failed' && row.error_category === 'storage_unavailable' && reason.includes('quarantined')) {
|
|
630
|
+
isMalformed = true;
|
|
631
|
+
errMsg = reason;
|
|
632
|
+
}
|
|
495
633
|
}
|
|
496
634
|
catch (err) {
|
|
497
|
-
|
|
635
|
+
isMalformed = true;
|
|
636
|
+
errMsg = err instanceof Error ? err.message : String(err);
|
|
637
|
+
}
|
|
638
|
+
if (isMalformed) {
|
|
498
639
|
const runId = typeof row.run_id === 'string' ? row.run_id : String(row.run_id ?? 'unknown');
|
|
499
640
|
const taskId = typeof row.task_id === 'string' ? row.task_id : String(row.task_id ?? 'unknown');
|
|
500
641
|
const executionStatus = typeof row.execution_status === 'string' ? row.execution_status : null;
|
|
501
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
502
642
|
results.push({
|
|
503
643
|
runId,
|
|
504
644
|
taskId,
|
|
505
|
-
error:
|
|
645
|
+
error: errMsg,
|
|
506
646
|
currentStatus: executionStatus,
|
|
507
647
|
});
|
|
508
648
|
}
|
|
@@ -563,12 +703,11 @@ export class InternalizationIntegrityRemediation {
|
|
|
563
703
|
// Mark the latest run as failed
|
|
564
704
|
const latestRun = db.prepare(`SELECT run_id FROM runs WHERE task_id = ? AND execution_status = 'running' ORDER BY started_at DESC LIMIT 1`).get(taskId);
|
|
565
705
|
if (latestRun) {
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
`).run(now, 'Lease expired — force-expired by integrity-repair', 'lease_expired', latestRun.run_id);
|
|
706
|
+
this.safeUpdateRunRow(latestRun.run_id, {
|
|
707
|
+
executionStatus: 'failed',
|
|
708
|
+
reason: 'Lease expired — force-expired by integrity-repair',
|
|
709
|
+
errorCategory: 'lease_expired',
|
|
710
|
+
}, db);
|
|
572
711
|
}
|
|
573
712
|
});
|
|
574
713
|
tx();
|
|
@@ -583,18 +722,11 @@ export class InternalizationIntegrityRemediation {
|
|
|
583
722
|
* reflect the terminal outcome.
|
|
584
723
|
*/
|
|
585
724
|
markOrphanedRunFailed(runId, _taskId) {
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
SET execution_status = 'failed', ended_at = ?, reason = ?, error_category = ?
|
|
592
|
-
WHERE run_id = ?
|
|
593
|
-
`).run(now, `Orphaned run — recovered by integrity-repair (task not leased)`, 'recovery_sweep', runId);
|
|
594
|
-
}
|
|
595
|
-
finally {
|
|
596
|
-
db.close();
|
|
597
|
-
}
|
|
725
|
+
this.safeUpdateRunRow(runId, {
|
|
726
|
+
executionStatus: 'failed',
|
|
727
|
+
reason: 'Orphaned run — recovered by integrity-repair (task not leased)',
|
|
728
|
+
errorCategory: 'execution_failed',
|
|
729
|
+
});
|
|
598
730
|
}
|
|
599
731
|
/**
|
|
600
732
|
* Quarantine a schema-malformed run row: mark it failed + storage_unavailable.
|
|
@@ -605,18 +737,11 @@ export class InternalizationIntegrityRemediation {
|
|
|
605
737
|
* - The reason records the validation error so operators can trace root cause
|
|
606
738
|
*/
|
|
607
739
|
quarantineMalformedRun(runId, validationError) {
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
SET execution_status = 'failed', ended_at = ?, reason = ?, error_category = ?
|
|
614
|
-
WHERE run_id = ?
|
|
615
|
-
`).run(now, `Malformed run row quarantined by integrity-repair. Validation error: ${validationError}`, 'storage_unavailable', runId);
|
|
616
|
-
}
|
|
617
|
-
finally {
|
|
618
|
-
db.close();
|
|
619
|
-
}
|
|
740
|
+
this.safeUpdateRunRow(runId, {
|
|
741
|
+
executionStatus: 'failed',
|
|
742
|
+
reason: `Malformed run row quarantined by integrity-repair. Validation error: ${validationError}`,
|
|
743
|
+
errorCategory: 'storage_unavailable',
|
|
744
|
+
});
|
|
620
745
|
}
|
|
621
746
|
// ── Successor helpers (unchanged) ──────────────────────────────────────
|
|
622
747
|
findExistingSuccessor(dreamerTaskId) {
|
|
@@ -668,5 +793,279 @@ export class InternalizationIntegrityRemediation {
|
|
|
668
793
|
db.close();
|
|
669
794
|
}
|
|
670
795
|
}
|
|
796
|
+
/**
|
|
797
|
+
* Helper to check if input_payload and output_payload exist in the database.
|
|
798
|
+
*/
|
|
799
|
+
static checkRunsSchema(db) {
|
|
800
|
+
try {
|
|
801
|
+
const columns = db.pragma("table_info(runs)");
|
|
802
|
+
return {
|
|
803
|
+
hasInputPayload: columns.some(c => c.name === 'input_payload'),
|
|
804
|
+
hasOutputPayload: columns.some(c => c.name === 'output_payload'),
|
|
805
|
+
};
|
|
806
|
+
}
|
|
807
|
+
catch {
|
|
808
|
+
return { hasInputPayload: false, hasOutputPayload: false };
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
/**
|
|
812
|
+
* Helper to update a run row under a database transaction with pre-write
|
|
813
|
+
* and post-write schema validation checks. Rollback is triggered on error.
|
|
814
|
+
*/
|
|
815
|
+
safeUpdateRunRow(runId, patch, existingDb) {
|
|
816
|
+
const db = existingDb ?? new Database(this.dbPath);
|
|
817
|
+
try {
|
|
818
|
+
const tx = db.transaction(() => {
|
|
819
|
+
// 1. Read existing row
|
|
820
|
+
const rawRow = db.prepare('SELECT * FROM runs WHERE run_id = ?').get(runId);
|
|
821
|
+
if (!rawRow) {
|
|
822
|
+
throw new Error(`Run not found: ${runId}`);
|
|
823
|
+
}
|
|
824
|
+
// Validate required fields from the raw DB row before patching
|
|
825
|
+
if (typeof rawRow.task_id !== 'string' || rawRow.task_id.length === 0) {
|
|
826
|
+
throw new Error(`Run row is missing required task_id`);
|
|
827
|
+
}
|
|
828
|
+
if (typeof rawRow.runtime_kind !== 'string' || !Value.Check(RuntimeKindSchema, rawRow.runtime_kind)) {
|
|
829
|
+
throw new Error(`Run row has invalid runtime_kind: '${rawRow.runtime_kind}'`);
|
|
830
|
+
}
|
|
831
|
+
if (typeof rawRow.started_at !== 'string' || rawRow.started_at.length === 0) {
|
|
832
|
+
throw new Error(`Run row is missing required started_at`);
|
|
833
|
+
}
|
|
834
|
+
if (typeof rawRow.created_at !== 'string' || rawRow.created_at.length === 0) {
|
|
835
|
+
throw new Error(`Run row is missing required created_at`);
|
|
836
|
+
}
|
|
837
|
+
if (typeof rawRow.attempt_number !== 'number' || !Number.isInteger(rawRow.attempt_number) || rawRow.attempt_number < 0) {
|
|
838
|
+
throw new Error(`Run row has invalid attempt_number: '${rawRow.attempt_number}'`);
|
|
839
|
+
}
|
|
840
|
+
const now = new Date().toISOString();
|
|
841
|
+
const schema = InternalizationIntegrityRemediation.checkRunsSchema(db);
|
|
842
|
+
// 2. Build candidate patched record
|
|
843
|
+
const candidateRecord = {
|
|
844
|
+
runId: typeof rawRow.run_id === 'string' && rawRow.run_id.length > 0 ? rawRow.run_id : runId,
|
|
845
|
+
taskId: rawRow.task_id,
|
|
846
|
+
runtimeKind: rawRow.runtime_kind,
|
|
847
|
+
executionStatus: patch.executionStatus,
|
|
848
|
+
startedAt: rawRow.started_at,
|
|
849
|
+
attemptNumber: rawRow.attempt_number,
|
|
850
|
+
createdAt: rawRow.created_at,
|
|
851
|
+
updatedAt: now,
|
|
852
|
+
endedAt: now,
|
|
853
|
+
reason: patch.reason,
|
|
854
|
+
errorCategory: patch.errorCategory,
|
|
855
|
+
outputRef: typeof rawRow.output_ref === 'string' ? rawRow.output_ref : undefined,
|
|
856
|
+
inputPayload: schema.hasInputPayload && typeof rawRow.input_payload === 'string' ? rawRow.input_payload : undefined,
|
|
857
|
+
outputPayload: schema.hasOutputPayload && typeof rawRow.output_payload === 'string' ? rawRow.output_payload : undefined,
|
|
858
|
+
};
|
|
859
|
+
// 写前校验
|
|
860
|
+
if (!Value.Check(RunRecordSchema, candidateRecord)) {
|
|
861
|
+
const errors = [...Value.Errors(RunRecordSchema, candidateRecord)];
|
|
862
|
+
const details = errors.map(e => `${e.path}: ${e.message}`).join(', ');
|
|
863
|
+
throw new Error(`Run ${runId} failed pre-write schema check: ${details}`);
|
|
864
|
+
}
|
|
865
|
+
// 3. Update DB dynamically based on available columns
|
|
866
|
+
const sets = [
|
|
867
|
+
'task_id = ?',
|
|
868
|
+
'runtime_kind = ?',
|
|
869
|
+
'execution_status = ?',
|
|
870
|
+
'started_at = ?',
|
|
871
|
+
'attempt_number = ?',
|
|
872
|
+
'created_at = ?',
|
|
873
|
+
'updated_at = ?',
|
|
874
|
+
'ended_at = ?',
|
|
875
|
+
'reason = ?',
|
|
876
|
+
'error_category = ?',
|
|
877
|
+
'output_ref = ?',
|
|
878
|
+
];
|
|
879
|
+
const params = [
|
|
880
|
+
candidateRecord.taskId,
|
|
881
|
+
candidateRecord.runtimeKind,
|
|
882
|
+
candidateRecord.executionStatus,
|
|
883
|
+
candidateRecord.startedAt,
|
|
884
|
+
candidateRecord.attemptNumber,
|
|
885
|
+
candidateRecord.createdAt,
|
|
886
|
+
candidateRecord.updatedAt,
|
|
887
|
+
candidateRecord.endedAt,
|
|
888
|
+
candidateRecord.reason,
|
|
889
|
+
candidateRecord.errorCategory ?? null,
|
|
890
|
+
candidateRecord.outputRef ?? null,
|
|
891
|
+
];
|
|
892
|
+
if (schema.hasInputPayload) {
|
|
893
|
+
sets.push('input_payload = ?');
|
|
894
|
+
params.push(candidateRecord.inputPayload ?? null);
|
|
895
|
+
}
|
|
896
|
+
if (schema.hasOutputPayload) {
|
|
897
|
+
sets.push('output_payload = ?');
|
|
898
|
+
params.push(candidateRecord.outputPayload ?? null);
|
|
899
|
+
}
|
|
900
|
+
params.push(candidateRecord.runId);
|
|
901
|
+
db.prepare(`
|
|
902
|
+
UPDATE runs
|
|
903
|
+
SET ${sets.join(', ')}
|
|
904
|
+
WHERE run_id = ?
|
|
905
|
+
`).run(...params);
|
|
906
|
+
// 写后复校
|
|
907
|
+
const updatedRow = db.prepare('SELECT * FROM runs WHERE run_id = ?').get(runId);
|
|
908
|
+
SqliteRunStore.rowToRecord(updatedRow);
|
|
909
|
+
});
|
|
910
|
+
tx();
|
|
911
|
+
}
|
|
912
|
+
finally {
|
|
913
|
+
if (!existingDb) {
|
|
914
|
+
db.close();
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
/**
|
|
919
|
+
* Helper to insert a run row under a database transaction with pre-write
|
|
920
|
+
* and post-write schema validation checks. Rollback is triggered on error.
|
|
921
|
+
*/
|
|
922
|
+
safeInsertRunRow(record) {
|
|
923
|
+
const db = new Database(this.dbPath);
|
|
924
|
+
try {
|
|
925
|
+
const tx = db.transaction(() => {
|
|
926
|
+
// 写前校验
|
|
927
|
+
if (!Value.Check(RunRecordSchema, record)) {
|
|
928
|
+
const errors = [...Value.Errors(RunRecordSchema, record)];
|
|
929
|
+
const details = errors.map(e => `${e.path}: ${e.message}`).join(', ');
|
|
930
|
+
throw new Error(`Run ${record.runId} failed pre-write schema check: ${details}`);
|
|
931
|
+
}
|
|
932
|
+
const schema = InternalizationIntegrityRemediation.checkRunsSchema(db);
|
|
933
|
+
const columns = [
|
|
934
|
+
'run_id',
|
|
935
|
+
'task_id',
|
|
936
|
+
'runtime_kind',
|
|
937
|
+
'execution_status',
|
|
938
|
+
'started_at',
|
|
939
|
+
'ended_at',
|
|
940
|
+
'reason',
|
|
941
|
+
'output_ref',
|
|
942
|
+
'attempt_number',
|
|
943
|
+
'created_at',
|
|
944
|
+
'updated_at',
|
|
945
|
+
'error_category'
|
|
946
|
+
];
|
|
947
|
+
const placeholders = ['?', '?', '?', '?', '?', '?', '?', '?', '?', '?', '?', '?'];
|
|
948
|
+
const params = [
|
|
949
|
+
record.runId,
|
|
950
|
+
record.taskId,
|
|
951
|
+
record.runtimeKind,
|
|
952
|
+
record.executionStatus,
|
|
953
|
+
record.startedAt,
|
|
954
|
+
record.endedAt ?? null,
|
|
955
|
+
record.reason ?? null,
|
|
956
|
+
record.outputRef ?? null,
|
|
957
|
+
record.attemptNumber,
|
|
958
|
+
record.createdAt,
|
|
959
|
+
record.updatedAt,
|
|
960
|
+
record.errorCategory ?? null,
|
|
961
|
+
];
|
|
962
|
+
if (schema.hasInputPayload) {
|
|
963
|
+
columns.push('input_payload');
|
|
964
|
+
placeholders.push('?');
|
|
965
|
+
params.push(record.inputPayload ?? null);
|
|
966
|
+
}
|
|
967
|
+
if (schema.hasOutputPayload) {
|
|
968
|
+
columns.push('output_payload');
|
|
969
|
+
placeholders.push('?');
|
|
970
|
+
params.push(record.outputPayload ?? null);
|
|
971
|
+
}
|
|
972
|
+
db.prepare(`
|
|
973
|
+
INSERT INTO runs (${columns.join(', ')})
|
|
974
|
+
VALUES (${placeholders.join(', ')})
|
|
975
|
+
`).run(...params);
|
|
976
|
+
// 写后复校
|
|
977
|
+
const insertedRow = db.prepare('SELECT * FROM runs WHERE run_id = ?').get(record.runId);
|
|
978
|
+
SqliteRunStore.rowToRecord(insertedRow);
|
|
979
|
+
});
|
|
980
|
+
tx();
|
|
981
|
+
}
|
|
982
|
+
finally {
|
|
983
|
+
db.close();
|
|
984
|
+
}
|
|
985
|
+
}
|
|
986
|
+
/**
|
|
987
|
+
* Detect tasks that succeeded but have no succeeded run record.
|
|
988
|
+
*/
|
|
989
|
+
detectSucceededTasksWithoutSucceededRun() {
|
|
990
|
+
const db = new Database(this.dbPath, { readonly: true });
|
|
991
|
+
try {
|
|
992
|
+
const succeededTasks = db.prepare("SELECT task_id, result_ref FROM tasks WHERE status = 'succeeded'").all();
|
|
993
|
+
const results = [];
|
|
994
|
+
for (const t of succeededTasks) {
|
|
995
|
+
const runs = db.prepare("SELECT * FROM runs WHERE task_id = ? AND execution_status = 'succeeded'").all(t.task_id);
|
|
996
|
+
let hasValidSucceededRun = false;
|
|
997
|
+
for (const run of runs) {
|
|
998
|
+
try {
|
|
999
|
+
SqliteRunStore.rowToRecord(run);
|
|
1000
|
+
hasValidSucceededRun = true;
|
|
1001
|
+
break;
|
|
1002
|
+
}
|
|
1003
|
+
catch {
|
|
1004
|
+
// Malformed run row — ignore it as a valid succeeded run
|
|
1005
|
+
}
|
|
1006
|
+
}
|
|
1007
|
+
if (!hasValidSucceededRun) {
|
|
1008
|
+
results.push({
|
|
1009
|
+
taskId: t.task_id,
|
|
1010
|
+
resultRef: t.result_ref,
|
|
1011
|
+
});
|
|
1012
|
+
}
|
|
1013
|
+
}
|
|
1014
|
+
return results;
|
|
1015
|
+
}
|
|
1016
|
+
finally {
|
|
1017
|
+
db.close();
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
1020
|
+
/**
|
|
1021
|
+
* Supplements a canonical succeeded run for a succeeded task.
|
|
1022
|
+
* Resolves runtime kind from task's existing runs or defaults to 'openclaw'.
|
|
1023
|
+
*/
|
|
1024
|
+
supplementSucceededRun(taskId, resultRef) {
|
|
1025
|
+
const db = new Database(this.dbPath, { readonly: true });
|
|
1026
|
+
let runtimeKind = 'openclaw';
|
|
1027
|
+
let attemptNumber = 1;
|
|
1028
|
+
let startedAt = new Date().toISOString();
|
|
1029
|
+
let createdAt = startedAt;
|
|
1030
|
+
try {
|
|
1031
|
+
// Resolve runtimeKind from any existing run of this task
|
|
1032
|
+
const existingRun = db.prepare('SELECT runtime_kind, attempt_number, started_at, created_at FROM runs WHERE task_id = ? ORDER BY started_at DESC LIMIT 1').get(taskId);
|
|
1033
|
+
if (existingRun) {
|
|
1034
|
+
if (Value.Check(RuntimeKindSchema, existingRun.runtime_kind)) {
|
|
1035
|
+
runtimeKind = existingRun.runtime_kind;
|
|
1036
|
+
}
|
|
1037
|
+
attemptNumber = existingRun.attempt_number;
|
|
1038
|
+
startedAt = existingRun.started_at;
|
|
1039
|
+
createdAt = existingRun.created_at;
|
|
1040
|
+
}
|
|
1041
|
+
else {
|
|
1042
|
+
// Query task attributes
|
|
1043
|
+
const taskRow = db.prepare('SELECT attempt_count, created_at, updated_at FROM tasks WHERE task_id = ?').get(taskId);
|
|
1044
|
+
if (taskRow) {
|
|
1045
|
+
attemptNumber = Math.max(1, taskRow.attempt_count);
|
|
1046
|
+
startedAt = taskRow.created_at;
|
|
1047
|
+
createdAt = taskRow.created_at;
|
|
1048
|
+
}
|
|
1049
|
+
}
|
|
1050
|
+
}
|
|
1051
|
+
finally {
|
|
1052
|
+
db.close();
|
|
1053
|
+
}
|
|
1054
|
+
const now = new Date().toISOString();
|
|
1055
|
+
const runRecord = {
|
|
1056
|
+
runId: crypto.randomUUID(),
|
|
1057
|
+
taskId,
|
|
1058
|
+
runtimeKind,
|
|
1059
|
+
executionStatus: 'succeeded',
|
|
1060
|
+
startedAt,
|
|
1061
|
+
endedAt: now,
|
|
1062
|
+
attemptNumber,
|
|
1063
|
+
createdAt,
|
|
1064
|
+
updatedAt: now,
|
|
1065
|
+
reason: 'Supplemented succeeded run for succeeded task by integrity-repair',
|
|
1066
|
+
outputRef: resultRef ?? undefined,
|
|
1067
|
+
};
|
|
1068
|
+
this.safeInsertRunRow(runRecord);
|
|
1069
|
+
}
|
|
671
1070
|
}
|
|
672
1071
|
//# sourceMappingURL=internalization-integrity-remediation.js.map
|