@yeaft/webchat-agent 1.0.193 → 1.0.194
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/local-runtime/version.json +1 -1
- package/local-runtime/web/app.bundle.js +27 -6
- package/local-runtime/web/app.bundle.js.gz +0 -0
- package/local-runtime/web/index.html +2 -2
- package/local-runtime/web/style.bundle.css +1 -1
- package/local-runtime/web/style.bundle.css.gz +0 -0
- package/package.json +1 -1
- package/yeaft/work-center/bridge.js +2 -2
- package/yeaft/work-center/controller.js +18 -6
- package/yeaft/work-center/mainline-projection.js +305 -0
- package/yeaft/work-center/projection.js +109 -8
- package/yeaft/work-center/runner.js +59 -7
- package/yeaft/work-center/service.js +2 -0
- package/yeaft/work-center/store.js +324 -43
- package/yeaft/work-center/watcher.js +2 -0
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { DatabaseSync } from 'node:sqlite';
|
|
2
2
|
import { mkdirSync, realpathSync } from 'node:fs';
|
|
3
3
|
import { dirname, resolve } from 'node:path';
|
|
4
|
-
import { randomUUID } from 'node:crypto';
|
|
4
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
5
5
|
import { normalizeEvidence } from './evidence.js';
|
|
6
6
|
import { normalizeActionCheckpoint } from './action-checkpoint.js';
|
|
7
7
|
|
|
8
|
-
const SCHEMA_VERSION =
|
|
8
|
+
const SCHEMA_VERSION = 14;
|
|
9
9
|
const OPEN_ACTION_STATUSES = "'ready','running','waiting'";
|
|
10
10
|
const MAX_REUSABLE_CONTEXT_ITEMS = 12;
|
|
11
11
|
const MAX_RUN_RESPONSE_CHARS = 65_536;
|
|
@@ -29,12 +29,40 @@ function stringify(value) {
|
|
|
29
29
|
return JSON.stringify(value ?? null);
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
+
function stableJson(value) {
|
|
33
|
+
if (Array.isArray(value)) return `[${value.map(stableJson).join(',')}]`;
|
|
34
|
+
if (value && typeof value === 'object') {
|
|
35
|
+
return `{${Object.keys(value).sort().map(key => `${JSON.stringify(key)}:${stableJson(value[key])}`).join(',')}}`;
|
|
36
|
+
}
|
|
37
|
+
return JSON.stringify(value);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function actionSpecHash(action) {
|
|
41
|
+
const spec = {
|
|
42
|
+
type: action.type || '',
|
|
43
|
+
stageId: action.stageId || action.type || '',
|
|
44
|
+
assignmentPolicy: action.assignmentPolicy || null,
|
|
45
|
+
modelPolicy: action.modelPolicy || null,
|
|
46
|
+
dependsOnStageIds: [...new Set(action.dependsOnStageIds || [])].sort(),
|
|
47
|
+
workspaceMode: action.workspaceMode || 'shared',
|
|
48
|
+
changesRequestedStageId: action.changesRequestedStageId || null,
|
|
49
|
+
requiredRole: action.requiredRole || '',
|
|
50
|
+
instruction: action.instruction || '',
|
|
51
|
+
brief: action.brief || null,
|
|
52
|
+
context: Array.isArray(action.context) ? action.context : [],
|
|
53
|
+
contractRevision: Number.isInteger(action.contractRevision) ? action.contractRevision : 1,
|
|
54
|
+
};
|
|
55
|
+
return createHash('sha256').update(stableJson(spec), 'utf8').digest('hex');
|
|
56
|
+
}
|
|
57
|
+
|
|
32
58
|
function mapWorkItem(row) {
|
|
33
59
|
if (!row) return null;
|
|
34
60
|
return {
|
|
35
61
|
id: row.id,
|
|
36
62
|
revision: row.revision,
|
|
37
63
|
planRevision: Math.max(0, Number(row.plan_revision) || 0),
|
|
64
|
+
executionSchemaVersion: Math.max(1, Number(row.execution_schema_version) || 1),
|
|
65
|
+
ledgerRevision: Math.max(0, Number(row.ledger_revision) || 0),
|
|
38
66
|
title: row.title,
|
|
39
67
|
goal: row.goal,
|
|
40
68
|
acceptanceCriteria: parseJson(row.acceptance_criteria, []),
|
|
@@ -65,6 +93,39 @@ function mapWorkItem(row) {
|
|
|
65
93
|
};
|
|
66
94
|
}
|
|
67
95
|
|
|
96
|
+
function isGraphWorkItem(workItem) {
|
|
97
|
+
return workItem?.workflowSnapshot?.executionMode === 'graph';
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function graphExecutionState(workItem, actions) {
|
|
101
|
+
if (!isGraphWorkItem(workItem)) return workItem;
|
|
102
|
+
const current = (Array.isArray(actions) ? actions : [])
|
|
103
|
+
.filter(action => !['superseded', 'cancelled'].includes(action.status));
|
|
104
|
+
const activeActionIds = current
|
|
105
|
+
.filter(action => ['ready', 'running'].includes(action.status))
|
|
106
|
+
.map(action => action.id);
|
|
107
|
+
const waitingIds = current.filter(action => action.status === 'waiting').map(action => action.id);
|
|
108
|
+
const failedIds = current.filter(action => action.status === 'failed').map(action => action.id);
|
|
109
|
+
const attentionActionIds = current
|
|
110
|
+
.filter(action => ['waiting', 'failed'].includes(action.status))
|
|
111
|
+
.map(action => action.id);
|
|
112
|
+
const lifecycle = workItem.status === 'cancelled' ? 'cancelled'
|
|
113
|
+
: current.length === 0 || workItem.status === 'draft' ? 'draft'
|
|
114
|
+
: current.every(action => action.status === 'completed') ? 'done'
|
|
115
|
+
: 'active';
|
|
116
|
+
const attentionState = waitingIds.length > 0 && failedIds.length > 0 ? 'mixed'
|
|
117
|
+
: waitingIds.length > 0 ? 'waiting'
|
|
118
|
+
: failedIds.length > 0 ? 'failed'
|
|
119
|
+
: 'none';
|
|
120
|
+
return {
|
|
121
|
+
...workItem,
|
|
122
|
+
lifecycle,
|
|
123
|
+
attentionState,
|
|
124
|
+
activeActionIds,
|
|
125
|
+
attentionActionIds,
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
68
129
|
function mapAction(row) {
|
|
69
130
|
if (!row) return null;
|
|
70
131
|
return {
|
|
@@ -84,6 +145,9 @@ function mapAction(row) {
|
|
|
84
145
|
brief: parseJson(row.brief, null),
|
|
85
146
|
context: parseJson(row.context, []),
|
|
86
147
|
contractRevision: row.contract_revision,
|
|
148
|
+
generation: Math.max(1, Number(row.generation) || 1),
|
|
149
|
+
specHash: row.spec_hash || '',
|
|
150
|
+
resultRunId: row.result_run_id || null,
|
|
87
151
|
status: row.status,
|
|
88
152
|
attempt: row.attempt,
|
|
89
153
|
maxAttempts: row.max_attempts,
|
|
@@ -110,11 +174,15 @@ function mapRun(row) {
|
|
|
110
174
|
vpSnapshot: parseJson(row.vp_snapshot, null),
|
|
111
175
|
modelSnapshot: parseJson(row.model_snapshot, null),
|
|
112
176
|
toolPolicySnapshot: parseJson(row.tool_policy_snapshot, null),
|
|
177
|
+
contextSnapshot: parseJson(row.context_snapshot, null),
|
|
178
|
+
executionManifest: parseJson(row.execution_manifest, null),
|
|
113
179
|
response: row.response || '',
|
|
114
180
|
summary: row.summary || '',
|
|
115
181
|
evidence: normalizeEvidence(parseJson(row.evidence, [])),
|
|
116
182
|
waitingReason: row.waiting_reason || null,
|
|
117
183
|
error: row.error || null,
|
|
184
|
+
failureKind: row.failure_kind || null,
|
|
185
|
+
failureCode: row.failure_code || null,
|
|
118
186
|
reviewDecision: row.review_decision || null,
|
|
119
187
|
contractPatch: parseJson(row.contract_patch, null),
|
|
120
188
|
loopCount: Math.max(0, Number(row.loop_count) || 0),
|
|
@@ -130,6 +198,22 @@ function mapRun(row) {
|
|
|
130
198
|
};
|
|
131
199
|
}
|
|
132
200
|
|
|
201
|
+
function mapPlanConflict(row) {
|
|
202
|
+
if (!row) return null;
|
|
203
|
+
return {
|
|
204
|
+
id: row.id,
|
|
205
|
+
workItemId: row.work_item_id,
|
|
206
|
+
actionId: row.action_id || null,
|
|
207
|
+
generation: Math.max(1, Number(row.generation) || 1),
|
|
208
|
+
kind: row.kind,
|
|
209
|
+
status: row.status,
|
|
210
|
+
details: parseJson(row.details, {}),
|
|
211
|
+
createdAt: row.created_at,
|
|
212
|
+
updatedAt: row.updated_at,
|
|
213
|
+
resolvedAt: row.resolved_at || null,
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
|
|
133
217
|
function mapEvent(row) {
|
|
134
218
|
if (!row) return null;
|
|
135
219
|
return {
|
|
@@ -183,6 +267,8 @@ export class WorkItemStore {
|
|
|
183
267
|
id TEXT PRIMARY KEY,
|
|
184
268
|
revision INTEGER NOT NULL DEFAULT 1,
|
|
185
269
|
plan_revision INTEGER NOT NULL DEFAULT 0,
|
|
270
|
+
execution_schema_version INTEGER NOT NULL DEFAULT 2,
|
|
271
|
+
ledger_revision INTEGER NOT NULL DEFAULT 0,
|
|
186
272
|
title TEXT NOT NULL,
|
|
187
273
|
goal TEXT NOT NULL,
|
|
188
274
|
acceptance_criteria TEXT NOT NULL,
|
|
@@ -218,6 +304,9 @@ export class WorkItemStore {
|
|
|
218
304
|
brief TEXT,
|
|
219
305
|
context TEXT NOT NULL DEFAULT '[]',
|
|
220
306
|
contract_revision INTEGER NOT NULL DEFAULT 1,
|
|
307
|
+
generation INTEGER NOT NULL DEFAULT 1,
|
|
308
|
+
spec_hash TEXT NOT NULL DEFAULT '',
|
|
309
|
+
result_run_id TEXT,
|
|
221
310
|
status TEXT NOT NULL,
|
|
222
311
|
attempt INTEGER NOT NULL DEFAULT 0,
|
|
223
312
|
max_attempts INTEGER NOT NULL DEFAULT 2,
|
|
@@ -241,10 +330,14 @@ export class WorkItemStore {
|
|
|
241
330
|
vp_snapshot TEXT,
|
|
242
331
|
model_snapshot TEXT,
|
|
243
332
|
tool_policy_snapshot TEXT,
|
|
333
|
+
context_snapshot TEXT,
|
|
334
|
+
execution_manifest TEXT,
|
|
244
335
|
summary TEXT,
|
|
245
336
|
evidence TEXT NOT NULL DEFAULT '[]',
|
|
246
337
|
waiting_reason TEXT,
|
|
247
338
|
error TEXT,
|
|
339
|
+
failure_kind TEXT,
|
|
340
|
+
failure_code TEXT,
|
|
248
341
|
review_decision TEXT,
|
|
249
342
|
contract_patch TEXT,
|
|
250
343
|
response TEXT NOT NULL DEFAULT '',
|
|
@@ -281,6 +374,19 @@ export class WorkItemStore {
|
|
|
281
374
|
created_at INTEGER NOT NULL,
|
|
282
375
|
UNIQUE(work_item_id, proposal_id)
|
|
283
376
|
);
|
|
377
|
+
CREATE TABLE IF NOT EXISTS plan_conflicts (
|
|
378
|
+
id TEXT PRIMARY KEY,
|
|
379
|
+
work_item_id TEXT NOT NULL REFERENCES work_items(id) ON DELETE CASCADE,
|
|
380
|
+
action_id TEXT REFERENCES actions(id) ON DELETE SET NULL,
|
|
381
|
+
generation INTEGER NOT NULL DEFAULT 1,
|
|
382
|
+
kind TEXT NOT NULL,
|
|
383
|
+
status TEXT NOT NULL DEFAULT 'open',
|
|
384
|
+
details TEXT NOT NULL DEFAULT '{}',
|
|
385
|
+
created_at INTEGER NOT NULL,
|
|
386
|
+
updated_at INTEGER NOT NULL,
|
|
387
|
+
resolved_at INTEGER
|
|
388
|
+
);
|
|
389
|
+
CREATE INDEX IF NOT EXISTS idx_plan_conflicts_work_item ON plan_conflicts(work_item_id, created_at, id);
|
|
284
390
|
CREATE INDEX IF NOT EXISTS idx_work_items_status_updated ON work_items(status, updated_at DESC);
|
|
285
391
|
CREATE INDEX IF NOT EXISTS idx_actions_ready ON actions(status, updated_at, sequence);
|
|
286
392
|
CREATE INDEX IF NOT EXISTS idx_runs_active ON runs(status, expires_at);
|
|
@@ -320,6 +426,12 @@ export class WorkItemStore {
|
|
|
320
426
|
if (!hasColumn(this.db, 'runs', 'contract_patch')) {
|
|
321
427
|
this.db.exec('ALTER TABLE runs ADD COLUMN contract_patch TEXT');
|
|
322
428
|
}
|
|
429
|
+
if (!hasColumn(this.db, 'runs', 'failure_kind')) {
|
|
430
|
+
this.db.exec('ALTER TABLE runs ADD COLUMN failure_kind TEXT');
|
|
431
|
+
}
|
|
432
|
+
if (!hasColumn(this.db, 'runs', 'failure_code')) {
|
|
433
|
+
this.db.exec('ALTER TABLE runs ADD COLUMN failure_code TEXT');
|
|
434
|
+
}
|
|
323
435
|
if (!hasColumn(this.db, 'runs', 'loop_count')) {
|
|
324
436
|
this.db.exec('ALTER TABLE runs ADD COLUMN loop_count INTEGER NOT NULL DEFAULT 0');
|
|
325
437
|
}
|
|
@@ -380,6 +492,27 @@ export class WorkItemStore {
|
|
|
380
492
|
if (!hasColumn(this.db, 'work_items', 'plan_revision')) {
|
|
381
493
|
this.db.exec('ALTER TABLE work_items ADD COLUMN plan_revision INTEGER NOT NULL DEFAULT 0');
|
|
382
494
|
}
|
|
495
|
+
if (!hasColumn(this.db, 'work_items', 'execution_schema_version')) {
|
|
496
|
+
this.db.exec('ALTER TABLE work_items ADD COLUMN execution_schema_version INTEGER NOT NULL DEFAULT 1');
|
|
497
|
+
}
|
|
498
|
+
if (!hasColumn(this.db, 'work_items', 'ledger_revision')) {
|
|
499
|
+
this.db.exec('ALTER TABLE work_items ADD COLUMN ledger_revision INTEGER NOT NULL DEFAULT 0');
|
|
500
|
+
}
|
|
501
|
+
if (!hasColumn(this.db, 'actions', 'generation')) {
|
|
502
|
+
this.db.exec('ALTER TABLE actions ADD COLUMN generation INTEGER NOT NULL DEFAULT 1');
|
|
503
|
+
}
|
|
504
|
+
if (!hasColumn(this.db, 'actions', 'spec_hash')) {
|
|
505
|
+
this.db.exec("ALTER TABLE actions ADD COLUMN spec_hash TEXT NOT NULL DEFAULT ''");
|
|
506
|
+
}
|
|
507
|
+
if (!hasColumn(this.db, 'actions', 'result_run_id')) {
|
|
508
|
+
this.db.exec('ALTER TABLE actions ADD COLUMN result_run_id TEXT');
|
|
509
|
+
}
|
|
510
|
+
if (!hasColumn(this.db, 'runs', 'context_snapshot')) {
|
|
511
|
+
this.db.exec('ALTER TABLE runs ADD COLUMN context_snapshot TEXT');
|
|
512
|
+
}
|
|
513
|
+
if (!hasColumn(this.db, 'runs', 'execution_manifest')) {
|
|
514
|
+
this.db.exec('ALTER TABLE runs ADD COLUMN execution_manifest TEXT');
|
|
515
|
+
}
|
|
383
516
|
this.db.prepare(`INSERT INTO schema_meta(key, value) VALUES('schema_version', ?)
|
|
384
517
|
ON CONFLICT(key) DO UPDATE SET value = excluded.value`).run(String(SCHEMA_VERSION));
|
|
385
518
|
}
|
|
@@ -402,16 +535,58 @@ export class WorkItemStore {
|
|
|
402
535
|
return Number(result.lastInsertRowid);
|
|
403
536
|
}
|
|
404
537
|
|
|
538
|
+
createPlanConflict(workItemId, input = {}) {
|
|
539
|
+
const id = input.id || randomUUID();
|
|
540
|
+
const now = this.now();
|
|
541
|
+
this.db.prepare(`INSERT INTO plan_conflicts
|
|
542
|
+
(id, work_item_id, action_id, generation, kind, status, details, created_at, updated_at, resolved_at)
|
|
543
|
+
VALUES (?, ?, ?, ?, ?, 'open', ?, ?, ?, NULL)`).run(
|
|
544
|
+
id,
|
|
545
|
+
workItemId,
|
|
546
|
+
input.actionId || null,
|
|
547
|
+
Number.isInteger(input.generation) && input.generation > 0 ? input.generation : 1,
|
|
548
|
+
input.kind || 'plan',
|
|
549
|
+
stringify(input.details || {}),
|
|
550
|
+
now,
|
|
551
|
+
now,
|
|
552
|
+
);
|
|
553
|
+
return this.getPlanConflict(id);
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
getPlanConflict(id) {
|
|
557
|
+
return mapPlanConflict(this.db.prepare('SELECT * FROM plan_conflicts WHERE id = ?').get(id));
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
listPlanConflicts(workItemId, options = {}) {
|
|
561
|
+
const status = typeof options.status === 'string' && options.status ? options.status : null;
|
|
562
|
+
return this.db.prepare(`SELECT * FROM plan_conflicts WHERE work_item_id = ?
|
|
563
|
+
AND (? IS NULL OR status = ?) ORDER BY created_at, id`).all(workItemId, status, status).map(mapPlanConflict);
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
resolvePlanConflict(id, details = null) {
|
|
567
|
+
const now = this.now();
|
|
568
|
+
const changed = details && typeof details === 'object'
|
|
569
|
+
? this.db.prepare(`UPDATE plan_conflicts SET status = 'resolved', details = ?,
|
|
570
|
+
updated_at = ?, resolved_at = ? WHERE id = ? AND status = 'open'`).run(stringify(details), now, now, id)
|
|
571
|
+
: this.db.prepare(`UPDATE plan_conflicts SET status = 'resolved',
|
|
572
|
+
updated_at = ?, resolved_at = ? WHERE id = ? AND status = 'open'`).run(now, now, id);
|
|
573
|
+
return Number(changed.changes) === 1 ? this.getPlanConflict(id) : null;
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
deletePlanConflict(id) {
|
|
577
|
+
return Number(this.db.prepare('DELETE FROM plan_conflicts WHERE id = ?').run(id).changes) === 1;
|
|
578
|
+
}
|
|
579
|
+
|
|
405
580
|
createWorkItem(input, firstAction) {
|
|
406
581
|
return withTransaction(this.db, () => {
|
|
407
582
|
const now = this.now();
|
|
408
583
|
const id = input.id || randomUUID();
|
|
409
584
|
const workspaceKey = canonicalWorkspaceKey(input.workDir);
|
|
410
585
|
this.db.prepare(`INSERT INTO work_items
|
|
411
|
-
(id, revision, title, goal, acceptance_criteria, workflow_template, workflow_snapshot, status,
|
|
586
|
+
(id, revision, execution_schema_version, ledger_revision, title, goal, acceptance_criteria, workflow_template, workflow_snapshot, status,
|
|
412
587
|
current_action_id, current_run_id, work_dir, workspace_key, reuse_memory, origin, linked_session_ids,
|
|
413
588
|
session_context, attachments, created_at, updated_at)
|
|
414
|
-
VALUES (?, 1, ?, ?, ?, ?, ?, ?, NULL, NULL, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(
|
|
589
|
+
VALUES (?, 1, 2, 0, ?, ?, ?, ?, ?, ?, NULL, NULL, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(
|
|
415
590
|
id,
|
|
416
591
|
input.title,
|
|
417
592
|
input.goal,
|
|
@@ -457,6 +632,9 @@ export class WorkItemStore {
|
|
|
457
632
|
brief: input.brief && typeof input.brief === 'object' ? input.brief : null,
|
|
458
633
|
context: Array.isArray(input.context) ? input.context : [],
|
|
459
634
|
contractRevision: Number.isInteger(input.contractRevision) ? input.contractRevision : 1,
|
|
635
|
+
generation: Number.isInteger(input.generation) && input.generation > 0 ? input.generation : 1,
|
|
636
|
+
specHash: '',
|
|
637
|
+
resultRunId: input.resultRunId || null,
|
|
460
638
|
status: input.status || 'ready',
|
|
461
639
|
attempt: Number.isInteger(input.attempt) ? input.attempt : 0,
|
|
462
640
|
maxAttempts: Number.isInteger(input.maxAttempts) ? input.maxAttempts : 2,
|
|
@@ -465,11 +643,12 @@ export class WorkItemStore {
|
|
|
465
643
|
createdAt: now,
|
|
466
644
|
updatedAt: now,
|
|
467
645
|
};
|
|
646
|
+
action.specHash = actionSpecHash(action);
|
|
468
647
|
this.db.prepare(`INSERT INTO actions
|
|
469
648
|
(id, work_item_id, sequence, type, required_role, stage_id, assignment_policy, model_policy,
|
|
470
649
|
depends_on_stage_ids, workspace_mode, changes_requested_stage_id, workspace, instruction, brief, context, contract_revision,
|
|
471
|
-
status, attempt, max_attempts, current_run_id, lease_epoch, created_at, updated_at)
|
|
472
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NULL, 0, ?, ?)`).run(
|
|
650
|
+
generation, spec_hash, result_run_id, status, attempt, max_attempts, current_run_id, lease_epoch, created_at, updated_at)
|
|
651
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NULL, 0, ?, ?)`).run(
|
|
473
652
|
action.id,
|
|
474
653
|
workItemId,
|
|
475
654
|
action.sequence,
|
|
@@ -486,6 +665,9 @@ export class WorkItemStore {
|
|
|
486
665
|
stringify(action.brief),
|
|
487
666
|
stringify(action.context),
|
|
488
667
|
action.contractRevision,
|
|
668
|
+
action.generation,
|
|
669
|
+
action.specHash,
|
|
670
|
+
action.resultRunId,
|
|
489
671
|
action.status,
|
|
490
672
|
action.attempt,
|
|
491
673
|
action.maxAttempts,
|
|
@@ -517,9 +699,6 @@ export class WorkItemStore {
|
|
|
517
699
|
const target = actions.find(action => action.stageId === targetStageId);
|
|
518
700
|
if (!target) throw new Error('Work Center graph reset target is missing');
|
|
519
701
|
const affected = new Set([targetStageId]);
|
|
520
|
-
for (const action of actions) {
|
|
521
|
-
if (action.status === 'running') affected.add(action.stageId);
|
|
522
|
-
}
|
|
523
702
|
let changed = true;
|
|
524
703
|
while (changed) {
|
|
525
704
|
changed = false;
|
|
@@ -548,16 +727,27 @@ export class WorkItemStore {
|
|
|
548
727
|
WHERE action_id IN (${placeholders}) AND status = 'running'`).run(now, reason, ...ids);
|
|
549
728
|
for (const action of affectedActions) {
|
|
550
729
|
const workspace = action.id === target.id ? preservedTargetWorkspace : null;
|
|
730
|
+
const nextAction = action.id === target.id && replacement
|
|
731
|
+
? { ...action, ...replacement, generation: action.generation + 1 }
|
|
732
|
+
: { ...action, generation: action.generation + 1 };
|
|
551
733
|
this.db.prepare(`UPDATE actions SET status = 'ready', attempt = 0, current_run_id = NULL,
|
|
552
|
-
lease_epoch = ?,
|
|
553
|
-
|
|
734
|
+
lease_epoch = ?, generation = generation + 1, spec_hash = ?, result_run_id = NULL,
|
|
735
|
+
workspace = ?, updated_at = ? WHERE id = ?`).run(
|
|
736
|
+
nextEpoch.get(action.id), actionSpecHash(nextAction), stringify(workspace), now, action.id,
|
|
554
737
|
);
|
|
555
738
|
}
|
|
556
739
|
}
|
|
557
740
|
if (replacement) {
|
|
741
|
+
const replacementAction = {
|
|
742
|
+
...target,
|
|
743
|
+
...replacement,
|
|
744
|
+
generation: target.generation + 1,
|
|
745
|
+
contractRevision: replacement.contractRevision ?? target.contractRevision,
|
|
746
|
+
};
|
|
747
|
+
const specHash = actionSpecHash(replacementAction);
|
|
558
748
|
this.db.prepare(`UPDATE actions SET type = ?, required_role = ?, assignment_policy = ?,
|
|
559
749
|
model_policy = ?, depends_on_stage_ids = ?, workspace_mode = ?, changes_requested_stage_id = ?,
|
|
560
|
-
instruction = ?, brief = ?, context = ?, max_attempts = ?, workspace = ?, updated_at = ?
|
|
750
|
+
instruction = ?, brief = ?, context = ?, max_attempts = ?, workspace = ?, spec_hash = ?, updated_at = ?
|
|
561
751
|
WHERE id = ?`).run(
|
|
562
752
|
replacement.type || target.type,
|
|
563
753
|
replacement.requiredRole || '',
|
|
@@ -571,6 +761,7 @@ export class WorkItemStore {
|
|
|
571
761
|
stringify(Array.isArray(replacement.context) ? replacement.context : []),
|
|
572
762
|
Number.isInteger(replacement.maxAttempts) ? replacement.maxAttempts : 2,
|
|
573
763
|
stringify(preservedTargetWorkspace),
|
|
764
|
+
specHash,
|
|
574
765
|
now,
|
|
575
766
|
target.id,
|
|
576
767
|
);
|
|
@@ -583,11 +774,62 @@ export class WorkItemStore {
|
|
|
583
774
|
}
|
|
584
775
|
|
|
585
776
|
setActionWorkspace(actionId, workspace, workspaceMode = null) {
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
777
|
+
return withTransaction(this.db, () => {
|
|
778
|
+
const action = this.getAction(actionId);
|
|
779
|
+
if (!action) return null;
|
|
780
|
+
const nextWorkspaceMode = workspaceMode || action.workspaceMode;
|
|
781
|
+
const specChanged = nextWorkspaceMode !== action.workspaceMode;
|
|
782
|
+
const nextAction = { ...action, workspaceMode: nextWorkspaceMode };
|
|
783
|
+
const changed = this.db.prepare(`UPDATE actions SET workspace = ?, workspace_mode = ?,
|
|
784
|
+
generation = generation + ?, spec_hash = ?, result_run_id = CASE WHEN ? = 1 THEN NULL ELSE result_run_id END,
|
|
785
|
+
updated_at = ? WHERE id = ? AND generation = ?`).run(
|
|
786
|
+
stringify(workspace),
|
|
787
|
+
nextWorkspaceMode,
|
|
788
|
+
specChanged ? 1 : 0,
|
|
789
|
+
specChanged ? actionSpecHash(nextAction) : action.specHash,
|
|
790
|
+
specChanged ? 1 : 0,
|
|
791
|
+
this.now(),
|
|
792
|
+
actionId,
|
|
793
|
+
action.generation,
|
|
794
|
+
);
|
|
795
|
+
return Number(changed.changes) === 1 ? this.getAction(actionId) : null;
|
|
796
|
+
});
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
setActionWorkspaceForRun(
|
|
800
|
+
actionId,
|
|
801
|
+
runId,
|
|
802
|
+
ownerBootId,
|
|
803
|
+
leaseEpoch,
|
|
804
|
+
expectedGeneration,
|
|
805
|
+
workspace,
|
|
806
|
+
workspaceMode = null,
|
|
807
|
+
) {
|
|
808
|
+
return withTransaction(this.db, () => {
|
|
809
|
+
const active = this.#activeRunRow(runId, ownerBootId, leaseEpoch, true);
|
|
810
|
+
if (!active || active.action_id !== actionId) return null;
|
|
811
|
+
const action = this.getAction(actionId);
|
|
812
|
+
if (!action || action.generation !== expectedGeneration) return null;
|
|
813
|
+
const nextWorkspaceMode = workspaceMode || action.workspaceMode;
|
|
814
|
+
const specChanged = nextWorkspaceMode !== action.workspaceMode;
|
|
815
|
+
const nextAction = { ...action, workspaceMode: nextWorkspaceMode };
|
|
816
|
+
const changed = this.db.prepare(`UPDATE actions SET workspace = ?, workspace_mode = ?,
|
|
817
|
+
generation = generation + ?, spec_hash = ?, result_run_id = CASE WHEN ? = 1 THEN NULL ELSE result_run_id END,
|
|
818
|
+
updated_at = ? WHERE id = ? AND status = 'running' AND current_run_id = ?
|
|
819
|
+
AND lease_epoch = ? AND generation = ?`).run(
|
|
820
|
+
stringify(workspace),
|
|
821
|
+
nextWorkspaceMode,
|
|
822
|
+
specChanged ? 1 : 0,
|
|
823
|
+
specChanged ? actionSpecHash(nextAction) : action.specHash,
|
|
824
|
+
specChanged ? 1 : 0,
|
|
825
|
+
this.now(),
|
|
826
|
+
actionId,
|
|
827
|
+
runId,
|
|
828
|
+
leaseEpoch,
|
|
829
|
+
expectedGeneration,
|
|
830
|
+
);
|
|
831
|
+
return Number(changed.changes) === 1 ? this.getAction(actionId) : null;
|
|
832
|
+
});
|
|
591
833
|
}
|
|
592
834
|
|
|
593
835
|
setIntegrationWorkspaceForRun(actionId, runId, ownerBootId, leaseEpoch, workspace) {
|
|
@@ -683,7 +925,11 @@ export class WorkItemStore {
|
|
|
683
925
|
}
|
|
684
926
|
|
|
685
927
|
getWorkItem(id) {
|
|
686
|
-
|
|
928
|
+
const workItem = mapWorkItem(this.db.prepare('SELECT * FROM work_items WHERE id = ?').get(id));
|
|
929
|
+
if (!isGraphWorkItem(workItem)) return workItem;
|
|
930
|
+
const actions = this.db.prepare('SELECT * FROM actions WHERE work_item_id = ? ORDER BY sequence')
|
|
931
|
+
.all(id).map(mapAction);
|
|
932
|
+
return graphExecutionState(workItem, actions);
|
|
687
933
|
}
|
|
688
934
|
|
|
689
935
|
getAction(id) {
|
|
@@ -734,18 +980,25 @@ export class WorkItemStore {
|
|
|
734
980
|
FROM work_items w LEFT JOIN runs r ON r.work_item_id = w.id
|
|
735
981
|
${where.length ? `WHERE ${where.join(' AND ')}` : ''}
|
|
736
982
|
GROUP BY w.id ORDER BY w.updated_at DESC LIMIT ?`;
|
|
737
|
-
return this.db.prepare(sql).all(...values, limit).map(mapWorkItem)
|
|
983
|
+
return this.db.prepare(sql).all(...values, limit).map(mapWorkItem).map(workItem => {
|
|
984
|
+
if (!isGraphWorkItem(workItem)) return workItem;
|
|
985
|
+
const actions = this.db.prepare('SELECT * FROM actions WHERE work_item_id = ? ORDER BY sequence')
|
|
986
|
+
.all(workItem.id).map(mapAction);
|
|
987
|
+
return graphExecutionState(workItem, actions);
|
|
988
|
+
});
|
|
738
989
|
}
|
|
739
990
|
|
|
740
991
|
getWorkItemDetail(id) {
|
|
741
992
|
const workItem = this.getWorkItem(id);
|
|
742
993
|
if (!workItem) return null;
|
|
743
|
-
|
|
994
|
+
const detail = {
|
|
744
995
|
...workItem,
|
|
745
996
|
actions: this.db.prepare('SELECT * FROM actions WHERE work_item_id = ? ORDER BY sequence').all(id).map(mapAction),
|
|
746
997
|
runs: this.db.prepare('SELECT * FROM runs WHERE work_item_id = ? ORDER BY started_at DESC').all(id).map(mapRun),
|
|
998
|
+
planConflicts: this.listPlanConflicts(id),
|
|
747
999
|
events: this.db.prepare('SELECT * FROM events WHERE work_item_id = ? ORDER BY id DESC LIMIT 500').all(id).map(mapEvent),
|
|
748
1000
|
};
|
|
1001
|
+
return graphExecutionState(detail, detail.actions);
|
|
749
1002
|
}
|
|
750
1003
|
|
|
751
1004
|
getReusableContext(workDir, excludeWorkItemId = null) {
|
|
@@ -777,14 +1030,26 @@ export class WorkItemStore {
|
|
|
777
1030
|
return withTransaction(this.db, () => {
|
|
778
1031
|
const workItem = this.getWorkItem(id);
|
|
779
1032
|
if (!workItem) return null;
|
|
780
|
-
|
|
1033
|
+
const graphMode = isGraphWorkItem(workItem);
|
|
1034
|
+
const expectedAction = this.getAction(expected.actionId);
|
|
1035
|
+
const expectedMatches = graphMode
|
|
1036
|
+
? expectedAction?.workItemId === id && expectedAction.generation === expected.generation
|
|
1037
|
+
: workItem.currentActionId === expected.actionId;
|
|
1038
|
+
if (!expectedMatches || workItem.revision !== expected.revision) {
|
|
781
1039
|
throw new Error('Action changed before guidance was applied; refresh and try again');
|
|
782
1040
|
}
|
|
783
|
-
|
|
1041
|
+
const guidanceStatuses = graphMode
|
|
1042
|
+
? ['ready', 'running', 'waiting', 'needs_attention']
|
|
1043
|
+
: ['ready', 'running'];
|
|
1044
|
+
if (!guidanceStatuses.includes(workItem.status)) {
|
|
784
1045
|
throw new Error(`WorkItem in ${workItem.status} cannot accept Action guidance`);
|
|
785
1046
|
}
|
|
786
|
-
const previous =
|
|
787
|
-
|
|
1047
|
+
const previous = graphMode ? expectedAction
|
|
1048
|
+
: (workItem.currentActionId ? this.getAction(workItem.currentActionId) : null);
|
|
1049
|
+
const actionableStatuses = graphMode
|
|
1050
|
+
? ['ready', 'running', 'waiting', 'needs_attention']
|
|
1051
|
+
: ['ready', 'running'];
|
|
1052
|
+
if (!previous || !actionableStatuses.includes(previous.status)) {
|
|
788
1053
|
throw new Error('WorkItem has no active Action for guidance');
|
|
789
1054
|
}
|
|
790
1055
|
const now = this.now();
|
|
@@ -794,7 +1059,7 @@ export class WorkItemStore {
|
|
|
794
1059
|
contractRevision: previous.contractRevision,
|
|
795
1060
|
};
|
|
796
1061
|
let action;
|
|
797
|
-
if (
|
|
1062
|
+
if (graphMode) {
|
|
798
1063
|
action = this.#resetGraphFromStage(
|
|
799
1064
|
id,
|
|
800
1065
|
previous.stageId,
|
|
@@ -954,12 +1219,13 @@ export class WorkItemStore {
|
|
|
954
1219
|
if (!['waiting', 'needs_attention'].includes(workItem.status)) {
|
|
955
1220
|
throw new Error(`WorkItem in ${workItem.status} does not need retry`);
|
|
956
1221
|
}
|
|
957
|
-
const graphMode = workItem
|
|
1222
|
+
const graphMode = isGraphWorkItem(workItem);
|
|
958
1223
|
let previous = workItem.currentActionId ? this.getAction(workItem.currentActionId) : null;
|
|
959
1224
|
if (options.expected) {
|
|
960
1225
|
const expectedAction = this.getAction(options.expected.actionId);
|
|
961
1226
|
const expectedMatches = graphMode
|
|
962
|
-
? expectedAction?.workItemId === id &&
|
|
1227
|
+
? expectedAction?.workItemId === id && expectedAction.generation === options.expected.generation
|
|
1228
|
+
&& ['waiting', 'failed'].includes(expectedAction.status)
|
|
963
1229
|
: workItem.currentActionId === options.expected.actionId;
|
|
964
1230
|
if (!expectedMatches || workItem.revision !== options.expected.revision) {
|
|
965
1231
|
throw new Error('Action changed before input was applied; refresh and try again');
|
|
@@ -1034,7 +1300,7 @@ export class WorkItemStore {
|
|
|
1034
1300
|
AND w.status = 'ready' AND w.current_action_id = a.id AND w.current_run_id IS NULL)
|
|
1035
1301
|
OR
|
|
1036
1302
|
(json_extract(w.workflow_snapshot, '$.executionMode') = 'graph'
|
|
1037
|
-
AND w.status IN ('ready', 'running')
|
|
1303
|
+
AND w.status IN ('ready', 'running', 'waiting', 'needs_attention')
|
|
1038
1304
|
AND NOT EXISTS (
|
|
1039
1305
|
SELECT 1 FROM json_each(a.depends_on_stage_ids) dependency
|
|
1040
1306
|
LEFT JOIN actions required ON required.work_item_id = a.work_item_id
|
|
@@ -1069,10 +1335,11 @@ export class WorkItemStore {
|
|
|
1069
1335
|
);
|
|
1070
1336
|
if (Number(changedAction.changes) !== 1) return null;
|
|
1071
1337
|
const workItem = this.getWorkItem(row.work_item_id);
|
|
1072
|
-
const graphMode = workItem
|
|
1338
|
+
const graphMode = isGraphWorkItem(workItem);
|
|
1073
1339
|
const changedWorkItem = graphMode
|
|
1074
1340
|
? this.db.prepare(`UPDATE work_items SET status = 'running', current_action_id = ?,
|
|
1075
|
-
current_run_id = NULL, updated_at = ? WHERE id = ?
|
|
1341
|
+
current_run_id = NULL, updated_at = ? WHERE id = ?
|
|
1342
|
+
AND status IN ('ready', 'running', 'waiting', 'needs_attention')`).run(
|
|
1076
1343
|
row.id, now, row.work_item_id,
|
|
1077
1344
|
)
|
|
1078
1345
|
: this.db.prepare(`UPDATE work_items SET status = 'running', current_run_id = ?, updated_at = ?
|
|
@@ -1103,13 +1370,13 @@ export class WorkItemStore {
|
|
|
1103
1370
|
JOIN work_items w ON w.id = r.work_item_id
|
|
1104
1371
|
WHERE r.id = ? AND r.owner_boot_id = ? AND r.lease_epoch = ? AND r.status = 'running'
|
|
1105
1372
|
AND a.status = 'running' AND a.current_run_id = r.id AND a.lease_epoch = r.lease_epoch
|
|
1106
|
-
AND w.status IN ('ready', 'running', 'waiting')
|
|
1373
|
+
AND w.status IN ('ready', 'running', 'waiting', 'needs_attention')
|
|
1107
1374
|
${requireUnexpired ? 'AND r.expires_at > ?' : ''}`).get(
|
|
1108
1375
|
runId, ownerBootId, leaseEpoch, ...(requireUnexpired ? [this.now()] : []),
|
|
1109
1376
|
);
|
|
1110
1377
|
if (!row) return null;
|
|
1111
1378
|
const workItem = this.getWorkItem(row.work_item_id);
|
|
1112
|
-
if (workItem
|
|
1379
|
+
if (isGraphWorkItem(workItem)) return row;
|
|
1113
1380
|
return workItem?.status === 'running' && workItem.currentActionId === row.action_id
|
|
1114
1381
|
&& workItem.currentRunId === row.id ? row : null;
|
|
1115
1382
|
}
|
|
@@ -1189,7 +1456,8 @@ export class WorkItemStore {
|
|
|
1189
1456
|
leaseEpoch,
|
|
1190
1457
|
);
|
|
1191
1458
|
if (Number(actionChanged.changes) !== 1) throw new Error('Run interruption lost the Action fence');
|
|
1192
|
-
const
|
|
1459
|
+
const activeWorkItem = this.getWorkItem(active.work_item_id);
|
|
1460
|
+
const graphMode = isGraphWorkItem(activeWorkItem);
|
|
1193
1461
|
const itemChanged = graphMode
|
|
1194
1462
|
? this.db.prepare(`UPDATE work_items SET status = ?, current_action_id = ?, current_run_id = NULL,
|
|
1195
1463
|
updated_at = ? WHERE id = ? AND status = 'running'`).run(
|
|
@@ -1213,13 +1481,16 @@ export class WorkItemStore {
|
|
|
1213
1481
|
return withTransaction(this.db, () => {
|
|
1214
1482
|
if (!this.#activeRunRow(runId, ownerBootId, leaseEpoch, true)) return false;
|
|
1215
1483
|
const result = this.db.prepare(`UPDATE runs SET role_snapshot = ?, vp_snapshot = ?,
|
|
1216
|
-
model_snapshot = ?, tool_policy_snapshot =
|
|
1484
|
+
model_snapshot = ?, tool_policy_snapshot = ?, context_snapshot = ?, execution_manifest = ?
|
|
1485
|
+
WHERE id = ? AND status = 'running'
|
|
1217
1486
|
AND role_snapshot IS NULL AND vp_snapshot IS NULL AND model_snapshot IS NULL
|
|
1218
|
-
AND tool_policy_snapshot IS NULL`).run(
|
|
1487
|
+
AND tool_policy_snapshot IS NULL AND context_snapshot IS NULL AND execution_manifest IS NULL`).run(
|
|
1219
1488
|
stringify(snapshots.roleSnapshot || null),
|
|
1220
1489
|
stringify(snapshots.vpSnapshot || null),
|
|
1221
1490
|
stringify(snapshots.modelSnapshot || null),
|
|
1222
1491
|
stringify(snapshots.toolPolicySnapshot || null),
|
|
1492
|
+
stringify(snapshots.contextSnapshot || null),
|
|
1493
|
+
stringify(snapshots.executionManifest || null),
|
|
1223
1494
|
runId,
|
|
1224
1495
|
);
|
|
1225
1496
|
return Number(result.changes) === 1;
|
|
@@ -1287,8 +1558,10 @@ export class WorkItemStore {
|
|
|
1287
1558
|
throw new Error('Work Center transition plan is incomplete');
|
|
1288
1559
|
}
|
|
1289
1560
|
const now = this.now();
|
|
1561
|
+
const ledgerIncrement = workItem.executionSchemaVersion === 2
|
|
1562
|
+
&& ['completed', 'failed', 'waiting'].includes(result.outcome) ? 1 : 0;
|
|
1290
1563
|
this.db.prepare(`UPDATE runs SET status = ?, ended_at = ?, response = ?, summary = ?, evidence = ?,
|
|
1291
|
-
waiting_reason = ?, error = ?, review_decision = ?, contract_patch = ?, checkpoint = ?,
|
|
1564
|
+
waiting_reason = ?, error = ?, failure_kind = ?, failure_code = ?, review_decision = ?, contract_patch = ?, checkpoint = ?,
|
|
1292
1565
|
loop_count = ?, tool_count = ?, llm_request_count = ?, input_tokens = ?, output_tokens = ?,
|
|
1293
1566
|
cache_read_tokens = ?, cache_write_tokens = ?, total_tokens = ?,
|
|
1294
1567
|
progress_revision = progress_revision + 1 WHERE id = ?`).run(
|
|
@@ -1299,6 +1572,8 @@ export class WorkItemStore {
|
|
|
1299
1572
|
stringify(normalizeEvidence(result.evidence)),
|
|
1300
1573
|
result.waitingReason || null,
|
|
1301
1574
|
result.error || null,
|
|
1575
|
+
result.failureKind || null,
|
|
1576
|
+
result.failureCode || null,
|
|
1302
1577
|
result.reviewDecision || null,
|
|
1303
1578
|
stringify(result.contractPatch || null),
|
|
1304
1579
|
stringify(normalizeActionCheckpoint(result.checkpoint)),
|
|
@@ -1314,12 +1589,17 @@ export class WorkItemStore {
|
|
|
1314
1589
|
);
|
|
1315
1590
|
this.onTransitionStep?.('after_run_update');
|
|
1316
1591
|
|
|
1317
|
-
const changedAction = this.db.prepare(`UPDATE actions SET status = ?, current_run_id = NULL,
|
|
1318
|
-
|
|
1592
|
+
const changedAction = this.db.prepare(`UPDATE actions SET status = ?, current_run_id = NULL,
|
|
1593
|
+
result_run_id = CASE WHEN ? = 'completed' THEN ? ELSE result_run_id END, updated_at = ?
|
|
1594
|
+
WHERE id = ? AND status = 'running' AND current_run_id = ? AND lease_epoch = ? AND generation = ?`).run(
|
|
1319
1595
|
transition.actionStatus,
|
|
1596
|
+
result.outcome,
|
|
1597
|
+
runId,
|
|
1320
1598
|
now,
|
|
1321
1599
|
action.id,
|
|
1322
1600
|
runId,
|
|
1601
|
+
leaseEpoch,
|
|
1602
|
+
action.generation,
|
|
1323
1603
|
);
|
|
1324
1604
|
if (Number(changedAction.changes) !== 1) {
|
|
1325
1605
|
throw new Error('Work Center terminal transition lost the current Action fence');
|
|
@@ -1446,14 +1726,15 @@ export class WorkItemStore {
|
|
|
1446
1726
|
: 'done';
|
|
1447
1727
|
currentActionId = blocked?.id || runnable?.id || null;
|
|
1448
1728
|
changedWorkItem = this.db.prepare(`UPDATE work_items SET status = ?, current_action_id = ?,
|
|
1449
|
-
current_run_id = NULL,
|
|
1450
|
-
|
|
1729
|
+
current_run_id = NULL, ledger_revision = ledger_revision + ?, updated_at = ?
|
|
1730
|
+
WHERE id = ? AND status IN ('ready', 'running', 'waiting', 'needs_attention')`).run(
|
|
1731
|
+
workItemStatus, currentActionId, ledgerIncrement, now, workItem.id,
|
|
1451
1732
|
);
|
|
1452
1733
|
} else {
|
|
1453
1734
|
changedWorkItem = this.db.prepare(`UPDATE work_items SET status = ?, current_action_id = ?,
|
|
1454
|
-
current_run_id = NULL, updated_at = ? WHERE id = ? AND current_run_id = ?
|
|
1735
|
+
current_run_id = NULL, ledger_revision = ledger_revision + ?, updated_at = ? WHERE id = ? AND current_run_id = ?
|
|
1455
1736
|
AND current_action_id = ? AND status = 'running'`).run(
|
|
1456
|
-
workItemStatus, currentActionId, now, workItem.id, runId, action.id,
|
|
1737
|
+
workItemStatus, currentActionId, ledgerIncrement, now, workItem.id, runId, action.id,
|
|
1457
1738
|
);
|
|
1458
1739
|
}
|
|
1459
1740
|
if (Number(changedWorkItem.changes) !== 1) {
|
|
@@ -1516,7 +1797,7 @@ export class WorkItemStore {
|
|
|
1516
1797
|
const action = this.getAction(row.action_id);
|
|
1517
1798
|
if (this.#hasActiveIntegrationReservation(action, now)) continue;
|
|
1518
1799
|
const workItem = this.getWorkItem(row.work_item_id);
|
|
1519
|
-
const graphMode = workItem
|
|
1800
|
+
const graphMode = isGraphWorkItem(workItem);
|
|
1520
1801
|
const isCurrent = action?.status === 'running'
|
|
1521
1802
|
&& action.currentRunId === row.id
|
|
1522
1803
|
&& action.leaseEpoch === row.lease_epoch
|