@yeaft/webchat-agent 1.0.366 → 1.0.367
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 +50 -50
- package/local-runtime/web/app.bundle.js.gz +0 -0
- package/local-runtime/web/index.html +1 -1
- package/package.json +1 -1
- package/yeaft/tools/create-work-item.js +8 -8
- package/yeaft/work-center/controller.js +12 -2
- package/yeaft/work-center/coordinator.js +156 -26
- package/yeaft/work-center/durable-model.js +51 -1
- package/yeaft/work-center/dynamic-coordination.js +243 -0
- package/yeaft/work-center/execution-mode.js +16 -0
- package/yeaft/work-center/mainline-projection.js +25 -13
- package/yeaft/work-center/projection.js +32 -9
- package/yeaft/work-center/runner.js +18 -13
- package/yeaft/work-center/service.js +86 -13
- package/yeaft/work-center/store.js +535 -60
|
@@ -5,6 +5,8 @@ import { createHash, randomUUID } from 'node:crypto';
|
|
|
5
5
|
import { normalizeEvidence } from './evidence.js';
|
|
6
6
|
import { normalizeActionCheckpoint } from './action-checkpoint.js';
|
|
7
7
|
import { currentActionInputEventIds, runMatchesActionIdentity } from './action-identity.js';
|
|
8
|
+
import { isDynamicWorkItem, usesLegacyGraph } from './execution-mode.js';
|
|
9
|
+
import { normalizeDynamicCompletion } from './dynamic-coordination.js';
|
|
8
10
|
import { canonicalActionInstruction, withoutActionInputContext } from './workflow.js';
|
|
9
11
|
import {
|
|
10
12
|
WORK_CENTER_SCHEMA_VERSION,
|
|
@@ -84,6 +86,7 @@ function actionSpecHash(action) {
|
|
|
84
86
|
assignmentPolicy: action.assignmentPolicy || null,
|
|
85
87
|
modelPolicy: action.modelPolicy || null,
|
|
86
88
|
dependsOnStageIds: [...new Set(action.dependsOnStageIds || [])].sort(),
|
|
89
|
+
sourceActionIds: [...new Set(action.sourceActionIds || [])].sort(),
|
|
87
90
|
workspaceMode: action.workspaceMode || 'shared',
|
|
88
91
|
changesRequestedStageId: action.changesRequestedStageId || null,
|
|
89
92
|
requiredRole: action.requiredRole || '',
|
|
@@ -103,6 +106,8 @@ function mapWorkItem(row) {
|
|
|
103
106
|
planRevision: Math.max(0, Number(row.plan_revision) || 0),
|
|
104
107
|
executionSchemaVersion: Math.max(1, Number(row.execution_schema_version) || 1),
|
|
105
108
|
ledgerRevision: Math.max(0, Number(row.ledger_revision) || 0),
|
|
109
|
+
coordinationMode: row.coordination_mode || 'legacy',
|
|
110
|
+
finalResult: parseJson(row.final_result, null),
|
|
106
111
|
title: row.title,
|
|
107
112
|
goal: row.goal,
|
|
108
113
|
acceptanceCriteria: parseJson(row.acceptance_criteria, []),
|
|
@@ -144,12 +149,29 @@ function mapWorkItem(row) {
|
|
|
144
149
|
};
|
|
145
150
|
}
|
|
146
151
|
|
|
147
|
-
function
|
|
148
|
-
|
|
152
|
+
function dynamicExecutionState(workItem, actions) {
|
|
153
|
+
if (!isDynamicWorkItem(workItem)) return workItem;
|
|
154
|
+
const current = (Array.isArray(actions) ? actions : [])
|
|
155
|
+
.filter(action => !['superseded', 'cancelled'].includes(action.status));
|
|
156
|
+
const activeActionIds = current.filter(action => ['ready', 'running'].includes(action.status))
|
|
157
|
+
.map(action => action.id);
|
|
158
|
+
const waitingIds = current.filter(action => action.status === 'waiting').map(action => action.id);
|
|
159
|
+
const failedIds = current.filter(action => action.status === 'failed').map(action => action.id);
|
|
160
|
+
return {
|
|
161
|
+
...workItem,
|
|
162
|
+
lifecycle: workItem.status === 'cancelled' ? 'cancelled'
|
|
163
|
+
: workItem.status === 'done' ? 'done'
|
|
164
|
+
: workItem.status === 'draft' ? 'draft' : 'active',
|
|
165
|
+
attentionState: waitingIds.length > 0 && failedIds.length > 0 ? 'mixed'
|
|
166
|
+
: waitingIds.length > 0 ? 'waiting'
|
|
167
|
+
: failedIds.length > 0 ? 'failed' : 'none',
|
|
168
|
+
activeActionIds,
|
|
169
|
+
attentionActionIds: [...waitingIds, ...failedIds],
|
|
170
|
+
};
|
|
149
171
|
}
|
|
150
172
|
|
|
151
173
|
function graphExecutionState(workItem, actions) {
|
|
152
|
-
if (!
|
|
174
|
+
if (!usesLegacyGraph(workItem)) return dynamicExecutionState(workItem, actions);
|
|
153
175
|
const current = (Array.isArray(actions) ? actions : [])
|
|
154
176
|
.filter(action => !['superseded', 'cancelled'].includes(action.status));
|
|
155
177
|
const activeActionIds = current
|
|
@@ -188,6 +210,7 @@ function mapAction(row) {
|
|
|
188
210
|
assignmentPolicy: parseJson(row.assignment_policy, null),
|
|
189
211
|
modelPolicy: parseJson(row.model_policy, null),
|
|
190
212
|
dependsOnStageIds: parseJson(row.depends_on_stage_ids, []),
|
|
213
|
+
sourceActionIds: parseJson(row.source_action_ids, []),
|
|
191
214
|
workspaceMode: row.workspace_mode || 'shared',
|
|
192
215
|
changesRequestedStageId: row.changes_requested_stage_id || null,
|
|
193
216
|
workspace: parseJson(row.workspace, null),
|
|
@@ -248,6 +271,7 @@ function mapRun(row) {
|
|
|
248
271
|
response: row.response || '',
|
|
249
272
|
summary: row.summary || '',
|
|
250
273
|
evidence: normalizeEvidence(parseJson(row.evidence, [])),
|
|
274
|
+
acceptanceChecks: parseJson(row.acceptance_checks, []),
|
|
251
275
|
waitingReason: row.waiting_reason || null,
|
|
252
276
|
error: row.error || null,
|
|
253
277
|
failureKind: row.failure_kind || null,
|
|
@@ -804,6 +828,8 @@ export class WorkItemStore {
|
|
|
804
828
|
plan_revision INTEGER NOT NULL DEFAULT 0,
|
|
805
829
|
execution_schema_version INTEGER NOT NULL DEFAULT 2,
|
|
806
830
|
ledger_revision INTEGER NOT NULL DEFAULT 0,
|
|
831
|
+
coordination_mode TEXT NOT NULL DEFAULT 'legacy',
|
|
832
|
+
final_result TEXT,
|
|
807
833
|
title TEXT NOT NULL,
|
|
808
834
|
goal TEXT NOT NULL,
|
|
809
835
|
acceptance_criteria TEXT NOT NULL,
|
|
@@ -834,6 +860,7 @@ export class WorkItemStore {
|
|
|
834
860
|
assignment_policy TEXT,
|
|
835
861
|
model_policy TEXT,
|
|
836
862
|
depends_on_stage_ids TEXT NOT NULL DEFAULT '[]',
|
|
863
|
+
source_action_ids TEXT NOT NULL DEFAULT '[]',
|
|
837
864
|
workspace_mode TEXT NOT NULL DEFAULT 'shared',
|
|
838
865
|
changes_requested_stage_id TEXT,
|
|
839
866
|
workspace TEXT,
|
|
@@ -873,6 +900,7 @@ export class WorkItemStore {
|
|
|
873
900
|
execution_manifest TEXT,
|
|
874
901
|
summary TEXT,
|
|
875
902
|
evidence TEXT NOT NULL DEFAULT '[]',
|
|
903
|
+
acceptance_checks TEXT NOT NULL DEFAULT '[]',
|
|
876
904
|
waiting_reason TEXT,
|
|
877
905
|
error TEXT,
|
|
878
906
|
failure_kind TEXT,
|
|
@@ -1072,6 +1100,15 @@ export class WorkItemStore {
|
|
|
1072
1100
|
if (!hasColumn(this.db, 'work_items', 'ledger_revision')) {
|
|
1073
1101
|
this.db.exec('ALTER TABLE work_items ADD COLUMN ledger_revision INTEGER NOT NULL DEFAULT 0');
|
|
1074
1102
|
}
|
|
1103
|
+
if (!hasColumn(this.db, 'work_items', 'coordination_mode')) {
|
|
1104
|
+
this.db.exec("ALTER TABLE work_items ADD COLUMN coordination_mode TEXT NOT NULL DEFAULT 'legacy'");
|
|
1105
|
+
}
|
|
1106
|
+
if (!hasColumn(this.db, 'work_items', 'final_result')) {
|
|
1107
|
+
this.db.exec('ALTER TABLE work_items ADD COLUMN final_result TEXT');
|
|
1108
|
+
}
|
|
1109
|
+
if (!hasColumn(this.db, 'actions', 'source_action_ids')) {
|
|
1110
|
+
this.db.exec("ALTER TABLE actions ADD COLUMN source_action_ids TEXT NOT NULL DEFAULT '[]'");
|
|
1111
|
+
}
|
|
1075
1112
|
if (!hasColumn(this.db, 'actions', 'generation')) {
|
|
1076
1113
|
this.db.exec('ALTER TABLE actions ADD COLUMN generation INTEGER NOT NULL DEFAULT 1');
|
|
1077
1114
|
}
|
|
@@ -1118,6 +1155,9 @@ export class WorkItemStore {
|
|
|
1118
1155
|
this.db.exec(`CREATE INDEX IF NOT EXISTS idx_pending_action_inputs_identity
|
|
1119
1156
|
ON pending_action_inputs(action_id, action_generation, action_spec_hash,
|
|
1120
1157
|
run_id, consumed_at, superseded_at, event_id)`);
|
|
1158
|
+
if (!hasColumn(this.db, 'runs', 'acceptance_checks')) {
|
|
1159
|
+
this.db.exec("ALTER TABLE runs ADD COLUMN acceptance_checks TEXT NOT NULL DEFAULT '[]'");
|
|
1160
|
+
}
|
|
1121
1161
|
if (!hasColumn(this.db, 'runs', 'context_snapshot')) {
|
|
1122
1162
|
this.db.exec('ALTER TABLE runs ADD COLUMN context_snapshot TEXT');
|
|
1123
1163
|
}
|
|
@@ -1322,6 +1362,21 @@ export class WorkItemStore {
|
|
|
1322
1362
|
});
|
|
1323
1363
|
}
|
|
1324
1364
|
|
|
1365
|
+
listPendingDynamicCoordinatorWakes() {
|
|
1366
|
+
return this.db.prepare(`SELECT c.id, c.work_item_id, c.kind, c.payload, c.source_key
|
|
1367
|
+
FROM coordinator_mailbox_entries c
|
|
1368
|
+
JOIN work_items w ON w.id = c.work_item_id
|
|
1369
|
+
WHERE w.coordination_mode = 'dynamic' AND w.status NOT IN ('done', 'cancelled')
|
|
1370
|
+
AND (c.status = 'pending' OR (c.status = 'claimed' AND c.lease_expires_at <= ?))
|
|
1371
|
+
ORDER BY c.created_at, c.sequence`).all(this.now()).map(row => ({
|
|
1372
|
+
id: row.id,
|
|
1373
|
+
workItemId: row.work_item_id,
|
|
1374
|
+
kind: row.kind,
|
|
1375
|
+
sourceKey: row.source_key,
|
|
1376
|
+
payload: parseJson(row.payload, {}),
|
|
1377
|
+
}));
|
|
1378
|
+
}
|
|
1379
|
+
|
|
1325
1380
|
claimCoordinatorMailbox(workItemId, owner, leaseMs = 60_000) {
|
|
1326
1381
|
return withTransaction(this.db, () => {
|
|
1327
1382
|
const now = this.now();
|
|
@@ -1341,6 +1396,16 @@ export class WorkItemStore {
|
|
|
1341
1396
|
});
|
|
1342
1397
|
}
|
|
1343
1398
|
|
|
1399
|
+
releaseCoordinatorMailboxClaim(id, owner, claimEpoch) {
|
|
1400
|
+
const now = this.now();
|
|
1401
|
+
const changed = this.db.prepare(`UPDATE coordinator_mailbox_entries SET status = 'pending',
|
|
1402
|
+
claim_owner = NULL, claimed_at = NULL, lease_expires_at = NULL, updated_at = ?
|
|
1403
|
+
WHERE id = ? AND status = 'claimed' AND claim_owner = ? AND claim_epoch = ?`).run(
|
|
1404
|
+
now, id, owner, claimEpoch,
|
|
1405
|
+
);
|
|
1406
|
+
return Number(changed.changes) === 1;
|
|
1407
|
+
}
|
|
1408
|
+
|
|
1344
1409
|
renewCoordinatorMailbox(id, owner, claimEpoch, leaseMs = 60_000) {
|
|
1345
1410
|
const now = this.now();
|
|
1346
1411
|
const changed = this.db.prepare(`UPDATE coordinator_mailbox_entries SET lease_expires_at = ?,
|
|
@@ -1772,8 +1837,8 @@ export class WorkItemStore {
|
|
|
1772
1837
|
if (!Number.isInteger(expectedGeneration) || expectedGeneration < 1) {
|
|
1773
1838
|
throw new Error('Action input generation must be a positive integer');
|
|
1774
1839
|
}
|
|
1775
|
-
const
|
|
1776
|
-
const inputStatuses =
|
|
1840
|
+
const concurrentMode = usesLegacyGraph(workItem) || isDynamicWorkItem(workItem);
|
|
1841
|
+
const inputStatuses = concurrentMode
|
|
1777
1842
|
? ['ready', 'running', 'waiting', 'needs_attention']
|
|
1778
1843
|
: ['ready', 'running'];
|
|
1779
1844
|
if (!inputStatuses.includes(workItem.status)) {
|
|
@@ -1783,7 +1848,7 @@ export class WorkItemStore {
|
|
|
1783
1848
|
const actionMatches = action?.workItemId === id
|
|
1784
1849
|
&& action.generation === expectedGeneration
|
|
1785
1850
|
&& ['ready', 'running'].includes(action.status)
|
|
1786
|
-
&& (
|
|
1851
|
+
&& (concurrentMode || action.id === workItem.currentActionId);
|
|
1787
1852
|
const activeRun = action?.currentRunId ? this.getRun(action.currentRunId) : null;
|
|
1788
1853
|
const runMatches = action?.status !== 'running'
|
|
1789
1854
|
|| (activeRun?.status === 'running' && activeRun.acceptingInput !== false
|
|
@@ -2229,11 +2294,14 @@ export class WorkItemStore {
|
|
|
2229
2294
|
const id = input.id || randomUUID();
|
|
2230
2295
|
const workspaceKey = canonicalWorkspaceKey(input.workDir);
|
|
2231
2296
|
this.db.prepare(`INSERT INTO work_items
|
|
2232
|
-
(id, revision, execution_schema_version, ledger_revision,
|
|
2297
|
+
(id, revision, execution_schema_version, ledger_revision, coordination_mode, final_result,
|
|
2298
|
+
title, goal, acceptance_criteria, workflow_template, workflow_snapshot, status,
|
|
2233
2299
|
current_action_id, current_run_id, work_dir, workspace_key, reuse_memory, origin, linked_session_ids,
|
|
2234
2300
|
session_context, attachments, created_at, updated_at)
|
|
2235
|
-
VALUES (?, 1,
|
|
2301
|
+
VALUES (?, 1, ?, 0, ?, NULL, ?, ?, ?, ?, ?, ?, NULL, NULL, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(
|
|
2236
2302
|
id,
|
|
2303
|
+
Number.isInteger(input.executionSchemaVersion) ? input.executionSchemaVersion : 2,
|
|
2304
|
+
input.coordinationMode || 'legacy',
|
|
2237
2305
|
input.title,
|
|
2238
2306
|
input.goal,
|
|
2239
2307
|
stringify(input.acceptanceCriteria || []),
|
|
@@ -2262,7 +2330,7 @@ export class WorkItemStore {
|
|
|
2262
2330
|
|
|
2263
2331
|
#insertAction(workItemId, input, sequence, now = this.now()) {
|
|
2264
2332
|
const stageId = input.stageId || input.type;
|
|
2265
|
-
const activeStage =
|
|
2333
|
+
const activeStage = usesLegacyGraph(this.getWorkItem(workItemId))
|
|
2266
2334
|
? this.db.prepare(`SELECT id FROM actions WHERE work_item_id = ? AND stage_id = ?
|
|
2267
2335
|
AND status NOT IN ('superseded', 'cancelled') LIMIT 1`).get(workItemId, stageId)
|
|
2268
2336
|
: null;
|
|
@@ -2278,6 +2346,7 @@ export class WorkItemStore {
|
|
|
2278
2346
|
assignmentPolicy: input.assignmentPolicy || null,
|
|
2279
2347
|
modelPolicy: input.modelPolicy || null,
|
|
2280
2348
|
dependsOnStageIds: Array.isArray(input.dependsOnStageIds) ? input.dependsOnStageIds : [],
|
|
2349
|
+
sourceActionIds: Array.isArray(input.sourceActionIds) ? input.sourceActionIds : [],
|
|
2281
2350
|
workspaceMode: input.workspaceMode || 'shared',
|
|
2282
2351
|
changesRequestedStageId: input.changesRequestedStageId || null,
|
|
2283
2352
|
workspace: input.workspace || null,
|
|
@@ -2302,10 +2371,10 @@ export class WorkItemStore {
|
|
|
2302
2371
|
action.identityHistory = actionIdentityHistory(action);
|
|
2303
2372
|
this.db.prepare(`INSERT INTO actions
|
|
2304
2373
|
(id, work_item_id, sequence, type, required_role, stage_id, assignment_policy, model_policy,
|
|
2305
|
-
depends_on_stage_ids, workspace_mode, changes_requested_stage_id, workspace,
|
|
2306
|
-
|
|
2307
|
-
replaces_action_id, created_at, updated_at)
|
|
2308
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NULL, 0, ?, ?, ?)`).run(
|
|
2374
|
+
depends_on_stage_ids, source_action_ids, workspace_mode, changes_requested_stage_id, workspace,
|
|
2375
|
+
instruction, brief, context, contract_revision, generation, spec_hash, identity_history, result_run_id,
|
|
2376
|
+
status, attempt, max_attempts, current_run_id, lease_epoch, replaces_action_id, created_at, updated_at)
|
|
2377
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NULL, 0, ?, ?, ?)`).run(
|
|
2309
2378
|
action.id,
|
|
2310
2379
|
workItemId,
|
|
2311
2380
|
action.sequence,
|
|
@@ -2315,6 +2384,7 @@ export class WorkItemStore {
|
|
|
2315
2384
|
stringify(action.assignmentPolicy),
|
|
2316
2385
|
stringify(action.modelPolicy),
|
|
2317
2386
|
stringify(action.dependsOnStageIds),
|
|
2387
|
+
stringify(action.sourceActionIds),
|
|
2318
2388
|
action.workspaceMode,
|
|
2319
2389
|
action.changesRequestedStageId,
|
|
2320
2390
|
stringify(action.workspace),
|
|
@@ -2697,12 +2767,12 @@ export class WorkItemStore {
|
|
|
2697
2767
|
const remaining = this.db.prepare(`SELECT id, status FROM actions WHERE work_item_id = ?
|
|
2698
2768
|
AND status IN ('ready', 'running', 'waiting', 'failed') ORDER BY sequence`).all(workItemId);
|
|
2699
2769
|
const blocked = remaining.find(candidate => candidate.status === 'waiting' || candidate.status === 'failed');
|
|
2700
|
-
const
|
|
2770
|
+
const running = remaining.find(candidate => candidate.status === 'running');
|
|
2771
|
+
const ready = remaining.find(candidate => candidate.status === 'ready');
|
|
2701
2772
|
return {
|
|
2702
2773
|
status: blocked ? (blocked.status === 'waiting' ? 'waiting' : 'needs_attention')
|
|
2703
|
-
:
|
|
2704
|
-
|
|
2705
|
-
currentActionId: blocked?.id || runnable?.id || null,
|
|
2774
|
+
: running ? 'running' : ready ? 'ready' : 'done',
|
|
2775
|
+
currentActionId: blocked?.id || running?.id || ready?.id || null,
|
|
2706
2776
|
};
|
|
2707
2777
|
}
|
|
2708
2778
|
|
|
@@ -2727,9 +2797,9 @@ export class WorkItemStore {
|
|
|
2727
2797
|
if (Number(changedAction.changes) !== 1) {
|
|
2728
2798
|
throw new Error('Work Center deferred Run lost the current Action fence');
|
|
2729
2799
|
}
|
|
2730
|
-
const
|
|
2731
|
-
const graphState =
|
|
2732
|
-
const changedWorkItem =
|
|
2800
|
+
const concurrentMode = usesLegacyGraph(workItem) || isDynamicWorkItem(workItem);
|
|
2801
|
+
const graphState = concurrentMode ? this.#graphWorkItemState(workItem.id) : null;
|
|
2802
|
+
const changedWorkItem = concurrentMode
|
|
2733
2803
|
? this.db.prepare(`UPDATE work_items SET status = ?, current_action_id = ?, current_run_id = NULL,
|
|
2734
2804
|
updated_at = ? WHERE id = ? AND status IN ('ready', 'running', 'waiting', 'needs_attention')`).run(
|
|
2735
2805
|
graphState.status, graphState.currentActionId, now, workItem.id,
|
|
@@ -2819,6 +2889,22 @@ export class WorkItemStore {
|
|
|
2819
2889
|
});
|
|
2820
2890
|
}
|
|
2821
2891
|
|
|
2892
|
+
listActionSources(workItemId, actionIds) {
|
|
2893
|
+
if (!Array.isArray(actionIds) || actionIds.length === 0) return [];
|
|
2894
|
+
const placeholders = actionIds.map(() => '?').join(',');
|
|
2895
|
+
return this.db.prepare(`SELECT a.*, r.summary AS dependency_summary,
|
|
2896
|
+
r.evidence AS dependency_evidence, r.vp_snapshot AS dependency_vp_snapshot
|
|
2897
|
+
FROM actions a JOIN runs r ON r.id = a.result_run_id
|
|
2898
|
+
WHERE a.work_item_id = ? AND a.id IN (${placeholders})
|
|
2899
|
+
AND a.status = 'completed' AND r.status = 'completed'
|
|
2900
|
+
ORDER BY a.sequence`).all(workItemId, ...actionIds).map(row => ({
|
|
2901
|
+
...mapAction(row),
|
|
2902
|
+
summary: row.dependency_summary || '',
|
|
2903
|
+
evidence: normalizeEvidence(parseJson(row.dependency_evidence, [])),
|
|
2904
|
+
vpId: parseJson(row.dependency_vp_snapshot, null)?.id || null,
|
|
2905
|
+
}));
|
|
2906
|
+
}
|
|
2907
|
+
|
|
2822
2908
|
listActionDependencies(workItemId, stageIds) {
|
|
2823
2909
|
if (!Array.isArray(stageIds) || stageIds.length === 0) return [];
|
|
2824
2910
|
const placeholders = stageIds.map(() => '?').join(',');
|
|
@@ -2840,7 +2926,7 @@ export class WorkItemStore {
|
|
|
2840
2926
|
|
|
2841
2927
|
getWorkItem(id) {
|
|
2842
2928
|
const workItem = mapWorkItem(this.db.prepare('SELECT * FROM work_items WHERE id = ?').get(id));
|
|
2843
|
-
if (!
|
|
2929
|
+
if (!usesLegacyGraph(workItem) && !isDynamicWorkItem(workItem)) return workItem;
|
|
2844
2930
|
const actions = this.db.prepare('SELECT * FROM actions WHERE work_item_id = ? ORDER BY sequence')
|
|
2845
2931
|
.all(id).map(mapAction);
|
|
2846
2932
|
return graphExecutionState(workItem, actions);
|
|
@@ -2874,6 +2960,7 @@ export class WorkItemStore {
|
|
|
2874
2960
|
AND recovery_event.action_generation = a.generation
|
|
2875
2961
|
AND recovery_event.type = 'coordinator.recovery_started'
|
|
2876
2962
|
WHERE a.status = 'failed' AND w.status NOT IN ('done', 'cancelled')
|
|
2963
|
+
AND COALESCE(w.coordination_mode, 'legacy') != 'dynamic'
|
|
2877
2964
|
GROUP BY a.id
|
|
2878
2965
|
ORDER BY a.updated_at, a.sequence, a.id`).all().map(row => ({
|
|
2879
2966
|
workItemId: row.work_item_id,
|
|
@@ -3064,6 +3151,7 @@ export class WorkItemStore {
|
|
|
3064
3151
|
(detail.actions || []).filter(action => !['completed', 'superseded', 'cancelled'].includes(action.status)),
|
|
3065
3152
|
),
|
|
3066
3153
|
recovery: assistant.recovery ? { ...assistant.recovery } : null,
|
|
3154
|
+
automatic: assistant.automatic === true,
|
|
3067
3155
|
claim: {
|
|
3068
3156
|
mailboxId: claim.mailboxId,
|
|
3069
3157
|
ownerBootId: claim.ownerBootId,
|
|
@@ -3073,6 +3161,71 @@ export class WorkItemStore {
|
|
|
3073
3161
|
};
|
|
3074
3162
|
}
|
|
3075
3163
|
|
|
3164
|
+
beginDynamicCoordinatorTurn(mailboxId, expected = {}) {
|
|
3165
|
+
return withTransaction(this.db, () => {
|
|
3166
|
+
const mailbox = this.db.prepare(`SELECT * FROM coordinator_mailbox_entries WHERE id = ?
|
|
3167
|
+
AND status = 'claimed' AND claim_owner = ? AND claim_epoch = ? AND lease_expires_at > ?`).get(
|
|
3168
|
+
mailboxId, expected.ownerBootId, expected.claimEpoch, this.now(),
|
|
3169
|
+
);
|
|
3170
|
+
if (!mailbox) return null;
|
|
3171
|
+
const workItem = this.getWorkItem(mailbox.work_item_id);
|
|
3172
|
+
if (!isDynamicWorkItem(workItem) || ['done', 'cancelled'].includes(workItem.status)) return null;
|
|
3173
|
+
const latest = (workItem.messages || []).at(-1);
|
|
3174
|
+
if (latest?.role === 'assistant' && latest.status === 'thinking') return null;
|
|
3175
|
+
const now = this.now();
|
|
3176
|
+
const turnId = randomUUID();
|
|
3177
|
+
const payload = parseJson(mailbox.payload, {});
|
|
3178
|
+
const assistantMessage = {
|
|
3179
|
+
id: randomUUID(), turnId, role: 'assistant', text: '', status: 'thinking',
|
|
3180
|
+
createdAt: now, updatedAt: now,
|
|
3181
|
+
automatic: true,
|
|
3182
|
+
trigger: { kind: mailbox.kind, sourceKey: mailbox.source_key, ...(payload.trigger || {}) },
|
|
3183
|
+
};
|
|
3184
|
+
const messages = [...(workItem.messages || []), assistantMessage].slice(-100);
|
|
3185
|
+
const coordinatorRevision = workItem.coordinatorRevision + 1;
|
|
3186
|
+
const changedMailbox = this.db.prepare(`UPDATE coordinator_mailbox_entries SET payload = ?,
|
|
3187
|
+
updated_at = ? WHERE id = ? AND status = 'claimed' AND claim_owner = ? AND claim_epoch = ?`).run(
|
|
3188
|
+
stringify({ ...payload, turnId }), now, mailbox.id, expected.ownerBootId, expected.claimEpoch,
|
|
3189
|
+
);
|
|
3190
|
+
if (Number(changedMailbox.changes) !== 1) return null;
|
|
3191
|
+
const changed = this.db.prepare(`UPDATE work_items SET messages = ?, coordinator_revision = ?,
|
|
3192
|
+
updated_at = ? WHERE id = ? AND revision = ? AND plan_revision = ?
|
|
3193
|
+
AND ledger_revision = ? AND coordinator_revision = ?`).run(
|
|
3194
|
+
stringify(messages), coordinatorRevision, now, workItem.id, workItem.revision,
|
|
3195
|
+
workItem.planRevision, workItem.ledgerRevision, workItem.coordinatorRevision,
|
|
3196
|
+
);
|
|
3197
|
+
if (Number(changed.changes) !== 1) throw new Error('Dynamic Coordinator turn lost its revision fence');
|
|
3198
|
+
this.#appendConversationEntry(
|
|
3199
|
+
workItem.id, assistantMessage, `coordinator:turn:${turnId}:assistant`,
|
|
3200
|
+
);
|
|
3201
|
+
this.appendEvent(workItem.id, 'coordinator.advance_started', {
|
|
3202
|
+
turnId, triggerKind: mailbox.kind, sourceKey: mailbox.source_key,
|
|
3203
|
+
});
|
|
3204
|
+
const detail = this.getWorkItemDetail(workItem.id);
|
|
3205
|
+
return {
|
|
3206
|
+
turnId,
|
|
3207
|
+
detail,
|
|
3208
|
+
fence: {
|
|
3209
|
+
workItemId: workItem.id,
|
|
3210
|
+
revision: workItem.revision,
|
|
3211
|
+
planRevision: workItem.planRevision,
|
|
3212
|
+
ledgerRevision: workItem.ledgerRevision,
|
|
3213
|
+
coordinatorRevision,
|
|
3214
|
+
status: workItem.status,
|
|
3215
|
+
actionFence: coordinatorActionFence(
|
|
3216
|
+
(detail.actions || []).filter(action => !['completed', 'superseded', 'cancelled'].includes(action.status)),
|
|
3217
|
+
),
|
|
3218
|
+
automatic: true,
|
|
3219
|
+
claim: {
|
|
3220
|
+
mailboxId: mailbox.id,
|
|
3221
|
+
ownerBootId: expected.ownerBootId,
|
|
3222
|
+
claimEpoch: Number(expected.claimEpoch),
|
|
3223
|
+
},
|
|
3224
|
+
},
|
|
3225
|
+
};
|
|
3226
|
+
});
|
|
3227
|
+
}
|
|
3228
|
+
|
|
3076
3229
|
claimStartedCoordinatorTurn(started, ownerBootId, leaseMs = 60_000) {
|
|
3077
3230
|
if (!started?.turnId || !started?.detail?.id) return null;
|
|
3078
3231
|
const claim = this.claimCoordinatorTurn(started.detail.id, started.turnId, ownerBootId, leaseMs);
|
|
@@ -3261,8 +3414,13 @@ export class WorkItemStore {
|
|
|
3261
3414
|
throw new Error('WorkItem Actions changed while the Coordinator was responding; send the message again');
|
|
3262
3415
|
}
|
|
3263
3416
|
this.#assertNoIntegrationReservation(activeActions, now);
|
|
3417
|
+
if (isDynamicWorkItem(workItem)) {
|
|
3418
|
+
return this.#completeDynamicCoordinatorTurn({
|
|
3419
|
+
turnId, result, expected, workItem, messages, assistantIndex, activeActions, now,
|
|
3420
|
+
});
|
|
3421
|
+
}
|
|
3264
3422
|
|
|
3265
|
-
const graphMode =
|
|
3423
|
+
const graphMode = usesLegacyGraph(workItem);
|
|
3266
3424
|
if (decision.kind === 'replan'
|
|
3267
3425
|
&& (!graphMode || workItem.workflowSnapshot?.planningMode !== 'ai')) {
|
|
3268
3426
|
throw new Error('Coordinator replan requires an AI-planned Action graph');
|
|
@@ -3472,6 +3630,163 @@ export class WorkItemStore {
|
|
|
3472
3630
|
});
|
|
3473
3631
|
}
|
|
3474
3632
|
|
|
3633
|
+
#completeDynamicCoordinatorTurn({
|
|
3634
|
+
turnId, result, expected, workItem, messages, assistantIndex, activeActions, now,
|
|
3635
|
+
}) {
|
|
3636
|
+
const decision = result?.decision || {};
|
|
3637
|
+
let affectedActionIds = [];
|
|
3638
|
+
let nextStatus = workItem.status;
|
|
3639
|
+
let currentActionId = workItem.currentActionId;
|
|
3640
|
+
let finalResult = null;
|
|
3641
|
+
|
|
3642
|
+
if (decision.kind === 'create_actions') {
|
|
3643
|
+
if (activeActions.some(action => action.status === 'running')) {
|
|
3644
|
+
throw new Error('Dynamic Coordinator cannot mutate Actions while a Run is active');
|
|
3645
|
+
}
|
|
3646
|
+
const mutation = result.mutation;
|
|
3647
|
+
if (!mutation || !Array.isArray(mutation.createdActions) || mutation.createdActions.length === 0) {
|
|
3648
|
+
throw new Error('Dynamic Coordinator Action creation is missing a validated mutation');
|
|
3649
|
+
}
|
|
3650
|
+
for (const actionId of mutation.supersedeActionIds || []) {
|
|
3651
|
+
const action = activeActions.find(candidate => candidate.id === actionId);
|
|
3652
|
+
if (!action || !['ready', 'waiting', 'failed'].includes(action.status)) {
|
|
3653
|
+
throw new Error('Dynamic Coordinator supersede target changed before apply');
|
|
3654
|
+
}
|
|
3655
|
+
this.#supersedePendingActionInputs([action], 'Superseded by WorkItem Coordinator', now);
|
|
3656
|
+
const changed = this.db.prepare(`UPDATE actions SET status = 'superseded', current_run_id = NULL,
|
|
3657
|
+
lease_epoch = lease_epoch + 1, updated_at = ? WHERE id = ? AND generation = ?
|
|
3658
|
+
AND status IN ('ready', 'waiting', 'failed')`).run(now, action.id, action.generation);
|
|
3659
|
+
if (Number(changed.changes) !== 1) throw new Error('Dynamic Coordinator lost an Action fence');
|
|
3660
|
+
}
|
|
3661
|
+
const patch = mutation.contractPatch || null;
|
|
3662
|
+
const title = patch?.title ?? workItem.title;
|
|
3663
|
+
const goal = patch?.goal ?? workItem.goal;
|
|
3664
|
+
const criteria = patch?.acceptanceCriteria ?? workItem.acceptanceCriteria;
|
|
3665
|
+
const contractChanged = title !== workItem.title || goal !== workItem.goal
|
|
3666
|
+
|| JSON.stringify(criteria) !== JSON.stringify(workItem.acceptanceCriteria);
|
|
3667
|
+
const snapshot = { ...workItem.workflowSnapshot, workItemType: mutation.workItemType };
|
|
3668
|
+
const changedPlan = this.db.prepare(`UPDATE work_items SET title = ?, goal = ?,
|
|
3669
|
+
acceptance_criteria = ?, workflow_snapshot = ?, revision = revision + ?,
|
|
3670
|
+
plan_revision = plan_revision + 1, updated_at = ? WHERE id = ? AND revision = ?
|
|
3671
|
+
AND plan_revision = ? AND ledger_revision = ? AND coordinator_revision = ?`).run(
|
|
3672
|
+
title, goal, stringify(criteria), stringify(snapshot), contractChanged ? 1 : 0,
|
|
3673
|
+
now, workItem.id, workItem.revision, workItem.planRevision,
|
|
3674
|
+
workItem.ledgerRevision, workItem.coordinatorRevision,
|
|
3675
|
+
);
|
|
3676
|
+
if (Number(changedPlan.changes) !== 1) throw new Error('Dynamic Coordinator lost the WorkItem plan fence');
|
|
3677
|
+
const current = this.getWorkItem(workItem.id);
|
|
3678
|
+
for (const candidate of mutation.createdActions) {
|
|
3679
|
+
const action = this.#insertAction(workItem.id, {
|
|
3680
|
+
...candidate, status: 'ready', contractRevision: current.revision,
|
|
3681
|
+
}, this.#nextSequence(workItem.id), now);
|
|
3682
|
+
affectedActionIds.push(action.id);
|
|
3683
|
+
}
|
|
3684
|
+
nextStatus = 'ready';
|
|
3685
|
+
currentActionId = affectedActionIds[0];
|
|
3686
|
+
} else if (decision.kind === 'guide_actions') {
|
|
3687
|
+
const byId = new Map(decision.guidance.map(entry => [entry.actionId, entry.instruction]));
|
|
3688
|
+
for (const action of activeActions) {
|
|
3689
|
+
const guidance = byId.get(action.id);
|
|
3690
|
+
if (!guidance) continue;
|
|
3691
|
+
if (!['ready', 'waiting', 'failed'].includes(action.status)) {
|
|
3692
|
+
throw new Error('Dynamic Coordinator can guide only non-running unfinished Actions');
|
|
3693
|
+
}
|
|
3694
|
+
const context = [...withoutActionInputContext(action.context), {
|
|
3695
|
+
type: 'coordinator-guidance', role: 'user', summary: guidance, evidence: [],
|
|
3696
|
+
}];
|
|
3697
|
+
const candidate = { ...action, context, generation: action.generation + 1 };
|
|
3698
|
+
candidate.instruction = canonicalActionInstruction(workItem, candidate, context);
|
|
3699
|
+
const specHash = actionSpecHash(candidate);
|
|
3700
|
+
const changed = this.db.prepare(`UPDATE actions SET status = 'ready', attempt = 0,
|
|
3701
|
+
current_run_id = NULL, context = ?, instruction = ?, generation = generation + 1,
|
|
3702
|
+
spec_hash = ?, identity_history = ?, result_run_id = NULL, workspace = NULL, updated_at = ?
|
|
3703
|
+
WHERE id = ? AND generation = ? AND status IN ('ready', 'waiting', 'failed')`).run(
|
|
3704
|
+
stringify(context), candidate.instruction, specHash,
|
|
3705
|
+
stringify(actionIdentityHistory(action, candidate.generation, specHash)),
|
|
3706
|
+
now, action.id, action.generation,
|
|
3707
|
+
);
|
|
3708
|
+
if (Number(changed.changes) !== 1) throw new Error('Dynamic Coordinator guidance lost its Action fence');
|
|
3709
|
+
affectedActionIds.push(action.id);
|
|
3710
|
+
}
|
|
3711
|
+
if (affectedActionIds.length !== decision.guidance.length) {
|
|
3712
|
+
throw new Error('Dynamic Coordinator guidance target changed before apply');
|
|
3713
|
+
}
|
|
3714
|
+
nextStatus = 'ready';
|
|
3715
|
+
currentActionId = affectedActionIds[0];
|
|
3716
|
+
} else if (decision.kind === 'request_human') {
|
|
3717
|
+
nextStatus = 'waiting';
|
|
3718
|
+
currentActionId = null;
|
|
3719
|
+
} else if (decision.kind === 'complete') {
|
|
3720
|
+
if (activeActions.length > 0) throw new Error('Work Center cannot complete with unfinished Actions');
|
|
3721
|
+
if (this.#hasBlockingOperation(workItem.id)) {
|
|
3722
|
+
throw new Error('WorkItem has an unsafe blocking Operation and cannot complete');
|
|
3723
|
+
}
|
|
3724
|
+
finalResult = normalizeDynamicCompletion(decision.completion, workItem.acceptanceCriteria);
|
|
3725
|
+
const canonicalRuns = new Map(this.db.prepare(`SELECT r.* FROM runs r JOIN actions a ON a.id = r.action_id
|
|
3726
|
+
WHERE r.work_item_id = ? AND r.status = 'completed' AND a.result_run_id = r.id`).all(workItem.id)
|
|
3727
|
+
.map(row => {
|
|
3728
|
+
const run = mapRun(row);
|
|
3729
|
+
return [run.id, run];
|
|
3730
|
+
}));
|
|
3731
|
+
const criteria = Array.isArray(workItem.acceptanceCriteria) ? workItem.acceptanceCriteria : [];
|
|
3732
|
+
for (const runId of finalResult.evidenceRunIds) {
|
|
3733
|
+
const run = canonicalRuns.get(runId);
|
|
3734
|
+
if (!run) throw new Error(`Completion evidence is not a canonical owned Run: ${runId}`);
|
|
3735
|
+
if (run.evidence.length === 0) {
|
|
3736
|
+
throw new Error(`Completion evidence Run has no concrete evidence: ${runId}`);
|
|
3737
|
+
}
|
|
3738
|
+
if (!Array.isArray(run.acceptanceChecks) || run.acceptanceChecks.length !== criteria.length
|
|
3739
|
+
|| run.acceptanceChecks.some((check, index) => (
|
|
3740
|
+
check?.criterion !== criteria[index] || !check?.status || !String(check?.evidence || '').trim()
|
|
3741
|
+
))) {
|
|
3742
|
+
throw new Error(`Completion evidence Run has incomplete acceptance checks: ${runId}`);
|
|
3743
|
+
}
|
|
3744
|
+
}
|
|
3745
|
+
for (const [index, acceptanceResult] of finalResult.acceptanceResults.entries()) {
|
|
3746
|
+
const provesCriterion = acceptanceResult.evidenceRunIds.some(runId => (
|
|
3747
|
+
canonicalRuns.get(runId)?.acceptanceChecks?.[index]?.criterion === criteria[index]
|
|
3748
|
+
&& canonicalRuns.get(runId)?.acceptanceChecks?.[index]?.status === 'passed'
|
|
3749
|
+
));
|
|
3750
|
+
if (!provesCriterion) {
|
|
3751
|
+
throw new Error(`Completion criterion lacks a passing canonical Run check: ${criteria[index]}`);
|
|
3752
|
+
}
|
|
3753
|
+
}
|
|
3754
|
+
nextStatus = 'done';
|
|
3755
|
+
currentActionId = null;
|
|
3756
|
+
} else if (decision.kind !== 'answer') {
|
|
3757
|
+
throw new Error(`Unsupported dynamic Coordinator decision: ${decision.kind || '(missing)'}`);
|
|
3758
|
+
}
|
|
3759
|
+
|
|
3760
|
+
messages[assistantIndex] = {
|
|
3761
|
+
...messages[assistantIndex], text: result.reply, status: 'completed', updatedAt: now,
|
|
3762
|
+
...(result.speaker ? { speaker: result.speaker } : {}),
|
|
3763
|
+
decision: { kind: decision.kind, reason: decision.reason, affectedActionIds },
|
|
3764
|
+
};
|
|
3765
|
+
this.#appendConversationEntry(
|
|
3766
|
+
workItem.id, messages[assistantIndex], `coordinator:turn:${turnId}:assistant`,
|
|
3767
|
+
);
|
|
3768
|
+
const claim = expected.claim || {};
|
|
3769
|
+
if (!this.ackCoordinatorMailbox(claim.mailboxId, claim.ownerBootId, claim.claimEpoch)) {
|
|
3770
|
+
throw new Error('Dynamic Coordinator completion lost its mailbox claim');
|
|
3771
|
+
}
|
|
3772
|
+
const current = this.getWorkItem(workItem.id);
|
|
3773
|
+
const changed = this.db.prepare(`UPDATE work_items SET messages = ?,
|
|
3774
|
+
coordinator_revision = coordinator_revision + 1, status = ?, current_action_id = ?,
|
|
3775
|
+
current_run_id = NULL, final_result = COALESCE(final_result, ?), updated_at = ?
|
|
3776
|
+
WHERE id = ? AND coordinator_revision = ? AND revision = ? AND plan_revision = ?
|
|
3777
|
+
AND ledger_revision = ? AND status NOT IN ('done', 'cancelled')`).run(
|
|
3778
|
+
stringify(messages), nextStatus, currentActionId, finalResult ? stringify(finalResult) : null,
|
|
3779
|
+
now, workItem.id, current.coordinatorRevision, current.revision,
|
|
3780
|
+
current.planRevision, current.ledgerRevision,
|
|
3781
|
+
);
|
|
3782
|
+
if (Number(changed.changes) !== 1) throw new Error('Dynamic Coordinator completion lost its turn fence');
|
|
3783
|
+
this.appendEvent(workItem.id, decision.kind === 'complete'
|
|
3784
|
+
? 'work_item.completed' : `coordinator.${decision.kind}`, {
|
|
3785
|
+
turnId, reason: decision.reason, affectedActionIds,
|
|
3786
|
+
});
|
|
3787
|
+
return this.getWorkItemDetail(workItem.id);
|
|
3788
|
+
}
|
|
3789
|
+
|
|
3475
3790
|
failCoordinatorTurn(turnId, error, expected = {}) {
|
|
3476
3791
|
return withTransaction(this.db, () => {
|
|
3477
3792
|
const claim = expected.claim || {};
|
|
@@ -3499,7 +3814,14 @@ export class WorkItemStore {
|
|
|
3499
3814
|
messages[index],
|
|
3500
3815
|
`coordinator:turn:${turnId}:assistant`,
|
|
3501
3816
|
);
|
|
3502
|
-
|
|
3817
|
+
const dynamicAutomatic = isDynamicWorkItem(workItem) && expected.automatic === true;
|
|
3818
|
+
if (dynamicAutomatic) {
|
|
3819
|
+
if (!this.releaseCoordinatorMailboxClaim(
|
|
3820
|
+
claim.mailboxId, claim.ownerBootId, claim.claimEpoch,
|
|
3821
|
+
)) return null;
|
|
3822
|
+
} else if (!this.ackCoordinatorMailbox(
|
|
3823
|
+
claim.mailboxId, claim.ownerBootId, claim.claimEpoch,
|
|
3824
|
+
)) return null;
|
|
3503
3825
|
const changed = this.db.prepare(`UPDATE work_items SET messages = ?, coordinator_revision = coordinator_revision + 1,
|
|
3504
3826
|
updated_at = ? WHERE id = ? AND coordinator_revision = ?`).run(
|
|
3505
3827
|
stringify(messages), now, workItem.id, workItem.coordinatorRevision,
|
|
@@ -3507,6 +3829,7 @@ export class WorkItemStore {
|
|
|
3507
3829
|
if (Number(changed.changes) !== 1) return null;
|
|
3508
3830
|
this.appendEvent(workItem.id, 'coordinator.turn_failed', {
|
|
3509
3831
|
turnId, error: messages[index].error,
|
|
3832
|
+
retryScheduled: dynamicAutomatic,
|
|
3510
3833
|
});
|
|
3511
3834
|
return this.getWorkItemDetail(workItem.id);
|
|
3512
3835
|
});
|
|
@@ -3541,38 +3864,71 @@ export class WorkItemStore {
|
|
|
3541
3864
|
return withTransaction(this.db, () => {
|
|
3542
3865
|
const workItem = this.getWorkItem(id);
|
|
3543
3866
|
if (!workItem) return null;
|
|
3544
|
-
const graphMode =
|
|
3867
|
+
const graphMode = usesLegacyGraph(workItem);
|
|
3868
|
+
const concurrentMode = graphMode || isDynamicWorkItem(workItem);
|
|
3545
3869
|
const expectedAction = this.getAction(expected.actionId);
|
|
3546
3870
|
const expectedGeneration = Number(expected.generation);
|
|
3547
3871
|
const hasExpectedGeneration = Number.isInteger(expectedGeneration) && expectedGeneration > 0;
|
|
3548
3872
|
const expectedMatches = expectedAction?.workItemId === id
|
|
3549
|
-
&& (
|
|
3550
|
-
&& (hasExpectedGeneration ? expectedAction.generation === expectedGeneration : !
|
|
3873
|
+
&& (concurrentMode || workItem.currentActionId === expected.actionId)
|
|
3874
|
+
&& (hasExpectedGeneration ? expectedAction.generation === expectedGeneration : !concurrentMode);
|
|
3551
3875
|
if (!expectedMatches || workItem.revision !== expected.revision) {
|
|
3552
3876
|
throw new Error('Action changed before guidance was applied; refresh and try again');
|
|
3553
3877
|
}
|
|
3554
|
-
const guidanceStatuses =
|
|
3878
|
+
const guidanceStatuses = concurrentMode
|
|
3555
3879
|
? ['ready', 'running', 'waiting', 'needs_attention']
|
|
3556
3880
|
: ['ready', 'running'];
|
|
3557
3881
|
if (!guidanceStatuses.includes(workItem.status)) {
|
|
3558
3882
|
throw new Error(`WorkItem in ${workItem.status} cannot accept Action guidance`);
|
|
3559
3883
|
}
|
|
3560
|
-
const previous =
|
|
3884
|
+
const previous = concurrentMode ? expectedAction
|
|
3561
3885
|
: (workItem.currentActionId ? this.getAction(workItem.currentActionId) : null);
|
|
3562
|
-
const actionableStatuses =
|
|
3886
|
+
const actionableStatuses = concurrentMode
|
|
3563
3887
|
? ['ready', 'running', 'waiting', 'needs_attention']
|
|
3564
3888
|
: ['ready', 'running'];
|
|
3565
3889
|
if (!previous || !actionableStatuses.includes(previous.status)) {
|
|
3566
3890
|
throw new Error('WorkItem has no active Action for guidance');
|
|
3567
3891
|
}
|
|
3568
3892
|
const now = this.now();
|
|
3569
|
-
const revision = workItem.revision + 1;
|
|
3893
|
+
const revision = workItem.revision + (isDynamicWorkItem(workItem) ? 0 : 1);
|
|
3570
3894
|
const replacement = {
|
|
3571
3895
|
...makeAction(workItem, previous),
|
|
3572
3896
|
contractRevision: previous.contractRevision,
|
|
3573
3897
|
};
|
|
3574
3898
|
let action;
|
|
3575
|
-
if (
|
|
3899
|
+
if (isDynamicWorkItem(workItem)) {
|
|
3900
|
+
this.#supersedePendingActionInputs([previous], 'Action restarted after user guidance', now);
|
|
3901
|
+
if (previous.status === 'running' && previous.currentRunId) {
|
|
3902
|
+
this.db.prepare(`UPDATE runs SET status = 'superseded', ended_at = ?, error = ?
|
|
3903
|
+
WHERE id = ? AND status = 'running'`).run(
|
|
3904
|
+
now, 'Action restarted after user guidance', previous.currentRunId,
|
|
3905
|
+
);
|
|
3906
|
+
}
|
|
3907
|
+
const candidate = {
|
|
3908
|
+
...replacement,
|
|
3909
|
+
id: previous.id,
|
|
3910
|
+
context: withoutActionInputContext(replacement.context),
|
|
3911
|
+
generation: previous.generation + 1,
|
|
3912
|
+
attempt: 0,
|
|
3913
|
+
currentRunId: null,
|
|
3914
|
+
resultRunId: null,
|
|
3915
|
+
workspace: null,
|
|
3916
|
+
};
|
|
3917
|
+
candidate.instruction = canonicalActionInstruction(workItem, candidate, candidate.context);
|
|
3918
|
+
const specHash = actionSpecHash(candidate);
|
|
3919
|
+
const changed = this.db.prepare(`UPDATE actions SET status = 'ready', attempt = 0,
|
|
3920
|
+
current_run_id = NULL, lease_epoch = lease_epoch + ?, context = ?, instruction = ?,
|
|
3921
|
+
generation = generation + 1, spec_hash = ?, identity_history = ?, result_run_id = NULL,
|
|
3922
|
+
workspace = NULL, updated_at = ? WHERE id = ? AND generation = ?
|
|
3923
|
+
AND status NOT IN ('completed', 'superseded', 'cancelled')`).run(
|
|
3924
|
+
previous.status === 'running' ? 1 : 0,
|
|
3925
|
+
stringify(candidate.context), candidate.instruction, specHash,
|
|
3926
|
+
stringify(actionIdentityHistory(previous, candidate.generation, specHash)),
|
|
3927
|
+
now, previous.id, previous.generation,
|
|
3928
|
+
);
|
|
3929
|
+
if (Number(changed.changes) !== 1) throw new Error('Dynamic Action guidance lost its generation fence');
|
|
3930
|
+
action = this.getAction(previous.id);
|
|
3931
|
+
} else if (graphMode) {
|
|
3576
3932
|
action = this.#resetGraphFromStage(
|
|
3577
3933
|
id,
|
|
3578
3934
|
previous.stageId,
|
|
@@ -3682,12 +4038,20 @@ export class WorkItemStore {
|
|
|
3682
4038
|
let action = null;
|
|
3683
4039
|
if (contractChanged) {
|
|
3684
4040
|
const updated = this.getWorkItem(id);
|
|
3685
|
-
|
|
3686
|
-
|
|
3687
|
-
|
|
3688
|
-
|
|
3689
|
-
|
|
3690
|
-
|
|
4041
|
+
if (isDynamicWorkItem(updated)) {
|
|
4042
|
+
this.db.prepare(`UPDATE work_items SET status = 'running', current_action_id = NULL,
|
|
4043
|
+
current_run_id = NULL, updated_at = ? WHERE id = ?`).run(now, id);
|
|
4044
|
+
this.enqueueCoordinatorMailbox(id, 'contract_changed', {
|
|
4045
|
+
trigger: { revision, changedFields: Object.keys(patch || {}) },
|
|
4046
|
+
}, `dynamic:contract:${id}:${revision}`);
|
|
4047
|
+
} else {
|
|
4048
|
+
action = this.#insertAction(id, {
|
|
4049
|
+
...makeInitialAction(updated),
|
|
4050
|
+
contractRevision: revision,
|
|
4051
|
+
}, this.#nextSequence(id), now);
|
|
4052
|
+
this.db.prepare(`UPDATE work_items SET status = 'ready', current_action_id = ?,
|
|
4053
|
+
current_run_id = NULL, updated_at = ? WHERE id = ?`).run(action.id, now, id);
|
|
4054
|
+
}
|
|
3691
4055
|
}
|
|
3692
4056
|
this.appendEvent(id, contractChanged ? 'workflow.retriaged' : 'work_item.updated', {
|
|
3693
4057
|
revision,
|
|
@@ -3733,6 +4097,30 @@ export class WorkItemStore {
|
|
|
3733
4097
|
const cancelledActions = this.db.prepare(`SELECT * FROM actions WHERE work_item_id = ?
|
|
3734
4098
|
AND status = 'cancelled' ORDER BY sequence`).all(id).map(mapAction);
|
|
3735
4099
|
this.#assertNoIntegrationReservation(cancelledActions, now);
|
|
4100
|
+
if (isDynamicWorkItem(workItem)) {
|
|
4101
|
+
this.#supersedePendingActionInputs(
|
|
4102
|
+
cancelledActions,
|
|
4103
|
+
'WorkItem resume returned control to the Coordinator',
|
|
4104
|
+
now,
|
|
4105
|
+
);
|
|
4106
|
+
this.db.prepare(`UPDATE actions SET status = 'superseded', current_run_id = NULL,
|
|
4107
|
+
lease_epoch = lease_epoch + 1, updated_at = ? WHERE work_item_id = ? AND status = 'cancelled'`).run(
|
|
4108
|
+
now, id,
|
|
4109
|
+
);
|
|
4110
|
+
const changed = this.db.prepare(`UPDATE work_items SET status = 'running',
|
|
4111
|
+
current_action_id = NULL, current_run_id = NULL, updated_at = ?
|
|
4112
|
+
WHERE id = ? AND status = 'cancelled' AND revision = ?`).run(now, id, expectedRevision);
|
|
4113
|
+
if (Number(changed.changes) !== 1) {
|
|
4114
|
+
throw new Error('WorkItem changed before it was resumed; refresh and try again');
|
|
4115
|
+
}
|
|
4116
|
+
this.enqueueCoordinatorMailbox(id, 'work_item_resumed', {
|
|
4117
|
+
trigger: { workItemId: id, revision: expectedRevision },
|
|
4118
|
+
}, `dynamic:resume:${id}:${expectedRevision}`);
|
|
4119
|
+
this.appendEvent(id, 'work_item.resumed', {
|
|
4120
|
+
supersededActionIds: cancelledActions.map(action => action.id),
|
|
4121
|
+
});
|
|
4122
|
+
return this.getWorkItemDetail(id);
|
|
4123
|
+
}
|
|
3736
4124
|
this.#supersedePendingActionInputs(
|
|
3737
4125
|
cancelledActions,
|
|
3738
4126
|
'WorkItem resume started a new Action generation',
|
|
@@ -3818,6 +4206,15 @@ export class WorkItemStore {
|
|
|
3818
4206
|
throw new Error(`WorkItem in ${workItem.status} must be resumed with retry`);
|
|
3819
4207
|
}
|
|
3820
4208
|
const now = this.now();
|
|
4209
|
+
if (isDynamicWorkItem(workItem)) {
|
|
4210
|
+
this.db.prepare(`UPDATE work_items SET status = 'running', current_action_id = NULL,
|
|
4211
|
+
current_run_id = NULL, updated_at = ? WHERE id = ?`).run(now, id);
|
|
4212
|
+
this.enqueueCoordinatorMailbox(id, 'work_item_started', {
|
|
4213
|
+
trigger: { workItemId: id },
|
|
4214
|
+
}, `dynamic:start:${id}:${workItem.revision}`);
|
|
4215
|
+
this.appendEvent(id, 'work_item.started');
|
|
4216
|
+
return this.getWorkItemDetail(id);
|
|
4217
|
+
}
|
|
3821
4218
|
const action = this.#insertAction(id, {
|
|
3822
4219
|
...makeInitialAction(workItem),
|
|
3823
4220
|
contractRevision: workItem.revision,
|
|
@@ -3847,7 +4244,7 @@ export class WorkItemStore {
|
|
|
3847
4244
|
}
|
|
3848
4245
|
const workItem = this.getWorkItem(id);
|
|
3849
4246
|
if (!workItem) return null;
|
|
3850
|
-
const graphMode =
|
|
4247
|
+
const graphMode = usesLegacyGraph(workItem);
|
|
3851
4248
|
const retryableWorkItemStatuses = graphMode
|
|
3852
4249
|
? ['ready', 'running', 'waiting', 'needs_attention']
|
|
3853
4250
|
: ['waiting', 'needs_attention'];
|
|
@@ -3949,6 +4346,7 @@ export class WorkItemStore {
|
|
|
3949
4346
|
SELECT 1 FROM actions failed_recovery
|
|
3950
4347
|
WHERE failed_recovery.work_item_id = a.work_item_id
|
|
3951
4348
|
AND failed_recovery.status = 'failed'
|
|
4349
|
+
AND COALESCE(w.coordination_mode, 'legacy') != 'dynamic'
|
|
3952
4350
|
)
|
|
3953
4351
|
AND NOT (
|
|
3954
4352
|
json_extract(w.messages, '$[#-1].role') = 'assistant'
|
|
@@ -3956,7 +4354,11 @@ export class WorkItemStore {
|
|
|
3956
4354
|
AND json_type(w.messages, '$[#-1].recovery') IS NOT NULL
|
|
3957
4355
|
)
|
|
3958
4356
|
AND (
|
|
3959
|
-
(COALESCE(
|
|
4357
|
+
(COALESCE(w.coordination_mode, 'legacy') = 'dynamic'
|
|
4358
|
+
AND w.status IN ('ready', 'running', 'waiting', 'needs_attention'))
|
|
4359
|
+
OR
|
|
4360
|
+
(COALESCE(w.coordination_mode, 'legacy') != 'dynamic'
|
|
4361
|
+
AND COALESCE(json_extract(w.workflow_snapshot, '$.executionMode'), 'linear') != 'graph'
|
|
3960
4362
|
AND w.status = 'ready' AND w.current_action_id = a.id AND w.current_run_id IS NULL)
|
|
3961
4363
|
OR
|
|
3962
4364
|
(json_extract(w.workflow_snapshot, '$.executionMode') = 'graph'
|
|
@@ -4042,8 +4444,8 @@ export class WorkItemStore {
|
|
|
4042
4444
|
);
|
|
4043
4445
|
if (Number(changedAction.changes) !== 1) return null;
|
|
4044
4446
|
const workItem = this.getWorkItem(action.workItemId);
|
|
4045
|
-
const
|
|
4046
|
-
const changedWorkItem =
|
|
4447
|
+
const concurrentMode = usesLegacyGraph(workItem) || isDynamicWorkItem(workItem);
|
|
4448
|
+
const changedWorkItem = concurrentMode
|
|
4047
4449
|
? this.db.prepare(`UPDATE work_items SET status = 'running', current_action_id = ?,
|
|
4048
4450
|
current_run_id = NULL, updated_at = ? WHERE id = ?
|
|
4049
4451
|
AND status IN ('ready', 'running', 'waiting', 'needs_attention')`).run(
|
|
@@ -4107,7 +4509,7 @@ export class WorkItemStore {
|
|
|
4107
4509
|
);
|
|
4108
4510
|
if (!row) return null;
|
|
4109
4511
|
const workItem = this.getWorkItem(row.work_item_id);
|
|
4110
|
-
if (
|
|
4512
|
+
if (usesLegacyGraph(workItem) || isDynamicWorkItem(workItem)) return row;
|
|
4111
4513
|
return workItem?.status === 'running' && workItem.currentActionId === row.action_id
|
|
4112
4514
|
&& workItem.currentRunId === row.id ? row : null;
|
|
4113
4515
|
}
|
|
@@ -4188,8 +4590,8 @@ export class WorkItemStore {
|
|
|
4188
4590
|
);
|
|
4189
4591
|
if (Number(actionChanged.changes) !== 1) throw new Error('Run interruption lost the Action fence');
|
|
4190
4592
|
const activeWorkItem = this.getWorkItem(active.work_item_id);
|
|
4191
|
-
const
|
|
4192
|
-
const itemChanged =
|
|
4593
|
+
const concurrentMode = usesLegacyGraph(activeWorkItem) || isDynamicWorkItem(activeWorkItem);
|
|
4594
|
+
const itemChanged = concurrentMode
|
|
4193
4595
|
? this.db.prepare(`UPDATE work_items SET status = ?, current_action_id = ?, current_run_id = NULL,
|
|
4194
4596
|
updated_at = ? WHERE id = ? AND status = 'running'`).run(
|
|
4195
4597
|
retryable ? 'ready' : 'needs_attention', action.id, now, active.work_item_id,
|
|
@@ -4311,15 +4713,31 @@ export class WorkItemStore {
|
|
|
4311
4713
|
const priorRuns = this.db.prepare(`SELECT * FROM runs
|
|
4312
4714
|
WHERE work_item_id = ? AND id != ? AND status != 'running'
|
|
4313
4715
|
ORDER BY started_at ASC`).all(workItem.id, runId).map(mapRun);
|
|
4314
|
-
const transition =
|
|
4716
|
+
const transition = isDynamicWorkItem(workItem)
|
|
4717
|
+
? {
|
|
4718
|
+
actionStatus: result.outcome === 'completed' ? 'completed'
|
|
4719
|
+
: result.outcome === 'waiting' ? 'waiting'
|
|
4720
|
+
: result.outcome === 'retryable' && action.attempt < action.maxAttempts ? 'ready' : 'failed',
|
|
4721
|
+
workItemStatus: result.outcome === 'retryable' && action.attempt < action.maxAttempts
|
|
4722
|
+
? 'ready' : 'running',
|
|
4723
|
+
keepCurrentAction: false,
|
|
4724
|
+
eventType: result.outcome === 'completed' ? 'action.completed'
|
|
4725
|
+
: result.outcome === 'waiting' ? 'action.waiting' : 'action.failed',
|
|
4726
|
+
eventData: {
|
|
4727
|
+
reviewDecision: result.reviewDecision,
|
|
4728
|
+
error: result.error,
|
|
4729
|
+
reason: result.waitingReason,
|
|
4730
|
+
},
|
|
4731
|
+
}
|
|
4732
|
+
: makeTransition({ run: mapRun(active), action, workItem, priorRuns });
|
|
4315
4733
|
if (!transition || !transition.actionStatus || !transition.workItemStatus) {
|
|
4316
4734
|
throw new Error('Work Center transition plan is incomplete');
|
|
4317
4735
|
}
|
|
4318
4736
|
const now = this.now();
|
|
4319
|
-
const ledgerIncrement = workItem.executionSchemaVersion
|
|
4737
|
+
const ledgerIncrement = workItem.executionSchemaVersion >= 2
|
|
4320
4738
|
&& ['completed', 'failed', 'waiting'].includes(result.outcome) ? 1 : 0;
|
|
4321
4739
|
this.db.prepare(`UPDATE runs SET status = ?, ended_at = ?, response = ?, summary = ?, evidence = ?,
|
|
4322
|
-
waiting_reason = ?, error = ?, failure_kind = ?, failure_code = ?, review_decision = ?, contract_patch = ?, checkpoint = ?,
|
|
4740
|
+
acceptance_checks = ?, waiting_reason = ?, error = ?, failure_kind = ?, failure_code = ?, review_decision = ?, contract_patch = ?, checkpoint = ?,
|
|
4323
4741
|
loop_count = ?, tool_count = ?, llm_request_count = ?, input_tokens = ?, output_tokens = ?,
|
|
4324
4742
|
cache_read_tokens = ?, cache_write_tokens = ?, total_tokens = ?,
|
|
4325
4743
|
progress_revision = progress_revision + 1 WHERE id = ?`).run(
|
|
@@ -4328,6 +4746,7 @@ export class WorkItemStore {
|
|
|
4328
4746
|
normalizeRunResponse(result.response),
|
|
4329
4747
|
result.summary || '',
|
|
4330
4748
|
stringify(normalizeEvidence(result.evidence)),
|
|
4749
|
+
stringify(Array.isArray(result.acceptanceChecks) ? result.acceptanceChecks : []),
|
|
4331
4750
|
result.waitingReason || null,
|
|
4332
4751
|
result.error || null,
|
|
4333
4752
|
result.failureKind || null,
|
|
@@ -4583,7 +5002,27 @@ export class WorkItemStore {
|
|
|
4583
5002
|
}
|
|
4584
5003
|
let currentActionId = nextAction?.id ?? (transition.keepCurrentAction ? action.id : null);
|
|
4585
5004
|
let changedWorkItem;
|
|
4586
|
-
if (
|
|
5005
|
+
if (isDynamicWorkItem(workItem)) {
|
|
5006
|
+
const remaining = this.db.prepare(`SELECT id, status FROM actions WHERE work_item_id = ?
|
|
5007
|
+
AND status IN ('ready', 'running', 'waiting', 'failed') ORDER BY sequence`).all(workItem.id);
|
|
5008
|
+
const running = remaining.find(candidate => candidate.status === 'running');
|
|
5009
|
+
const ready = remaining.find(candidate => candidate.status === 'ready');
|
|
5010
|
+
const settledBlocker = remaining.find(candidate => ['waiting', 'failed'].includes(candidate.status));
|
|
5011
|
+
if (running || ready) {
|
|
5012
|
+
workItemStatus = running ? 'running' : 'ready';
|
|
5013
|
+
currentActionId = running?.id || ready.id;
|
|
5014
|
+
} else {
|
|
5015
|
+
workItemStatus = settledBlocker?.status === 'waiting' ? 'waiting'
|
|
5016
|
+
: settledBlocker ? 'needs_attention' : 'running';
|
|
5017
|
+
currentActionId = settledBlocker?.id || null;
|
|
5018
|
+
}
|
|
5019
|
+
changedWorkItem = this.db.prepare(`UPDATE work_items SET status = ?, current_action_id = ?,
|
|
5020
|
+
current_run_id = NULL, ledger_revision = ledger_revision + ?, updated_at = ?
|
|
5021
|
+
WHERE id = ? AND status IN ('ready', 'running', 'waiting', 'needs_attention')
|
|
5022
|
+
AND revision = ?`).run(
|
|
5023
|
+
workItemStatus, currentActionId, ledgerIncrement, now, workItem.id, nextWorkItem.revision,
|
|
5024
|
+
);
|
|
5025
|
+
} else if (transition.planConflict) {
|
|
4587
5026
|
changedWorkItem = this.db.prepare(`UPDATE work_items SET status = ?, current_action_id = ?,
|
|
4588
5027
|
current_run_id = NULL, ledger_revision = ledger_revision + ?, updated_at = ?
|
|
4589
5028
|
WHERE id = ? AND status IN ('ready', 'running', 'waiting', 'needs_attention') AND revision = ?`).run(
|
|
@@ -4641,19 +5080,37 @@ export class WorkItemStore {
|
|
|
4641
5080
|
actionId: action.id,
|
|
4642
5081
|
runId,
|
|
4643
5082
|
});
|
|
5083
|
+
if (isDynamicWorkItem(workItem)) {
|
|
5084
|
+
this.#enqueueDynamicReconciliation(workItem.id, { runId });
|
|
5085
|
+
}
|
|
4644
5086
|
return this.getWorkItemDetail(workItem.id);
|
|
4645
5087
|
});
|
|
4646
5088
|
}
|
|
4647
5089
|
|
|
5090
|
+
#enqueueDynamicReconciliation(workItemId, trigger = {}) {
|
|
5091
|
+
const detail = this.getWorkItemDetail(workItemId);
|
|
5092
|
+
if (!isDynamicWorkItem(detail)
|
|
5093
|
+
|| (detail.actions || []).some(action => ['ready', 'running'].includes(action.status))) return null;
|
|
5094
|
+
return this.enqueueCoordinatorMailbox(workItemId, 'action_settled', {
|
|
5095
|
+
trigger: {
|
|
5096
|
+
...trigger,
|
|
5097
|
+
ledgerRevision: detail.ledgerRevision,
|
|
5098
|
+
actionIds: (detail.actions || [])
|
|
5099
|
+
.filter(action => !['completed', 'superseded', 'cancelled'].includes(action.status))
|
|
5100
|
+
.map(action => action.id),
|
|
5101
|
+
},
|
|
5102
|
+
}, `dynamic:reconcile:${workItemId}:${detail.ledgerRevision}`);
|
|
5103
|
+
}
|
|
5104
|
+
|
|
4648
5105
|
#refreshGraphWorkItem(workItemId, now) {
|
|
4649
5106
|
const remaining = this.db.prepare(`SELECT id, status FROM actions WHERE work_item_id = ?
|
|
4650
5107
|
AND status IN ('ready', 'running', 'waiting', 'failed') ORDER BY sequence`).all(workItemId);
|
|
4651
5108
|
const blocked = remaining.find(action => action.status === 'waiting' || action.status === 'failed');
|
|
4652
|
-
const
|
|
5109
|
+
const running = remaining.find(action => action.status === 'running');
|
|
5110
|
+
const ready = remaining.find(action => action.status === 'ready');
|
|
4653
5111
|
const status = blocked ? (blocked.status === 'waiting' ? 'waiting' : 'needs_attention')
|
|
4654
|
-
:
|
|
4655
|
-
|
|
4656
|
-
const currentActionId = blocked?.id || runnable?.id || null;
|
|
5112
|
+
: running ? 'running' : ready ? 'ready' : 'done';
|
|
5113
|
+
const currentActionId = blocked?.id || running?.id || ready?.id || null;
|
|
4657
5114
|
this.db.prepare(`UPDATE work_items SET status = ?, current_action_id = ?, current_run_id = NULL,
|
|
4658
5115
|
updated_at = ? WHERE id = ?`).run(status, currentActionId, now, workItemId);
|
|
4659
5116
|
}
|
|
@@ -4686,9 +5143,17 @@ export class WorkItemStore {
|
|
|
4686
5143
|
if (Number(changed.changes) !== 1) continue;
|
|
4687
5144
|
const turnId = messages[index].turnId || null;
|
|
4688
5145
|
if (turnId) {
|
|
4689
|
-
this.db.prepare(`
|
|
4690
|
-
|
|
4691
|
-
|
|
5146
|
+
const providerStatus = this.db.prepare(`SELECT status FROM coordinator_provider_turns
|
|
5147
|
+
WHERE coordinator_turn_id = ? ORDER BY attempt_number DESC LIMIT 1`).get(turnId)?.status || null;
|
|
5148
|
+
if (messages[index].automatic === true && providerStatus === null) {
|
|
5149
|
+
this.db.prepare(`UPDATE coordinator_mailbox_entries SET status = 'pending', acked_at = NULL,
|
|
5150
|
+
claim_owner = NULL, claimed_at = NULL, lease_expires_at = NULL, updated_at = ?
|
|
5151
|
+
WHERE json_extract(payload, '$.turnId') = ? AND status != 'cancelled'`).run(now, turnId);
|
|
5152
|
+
} else {
|
|
5153
|
+
this.db.prepare(`UPDATE coordinator_mailbox_entries SET status = 'acked', acked_at = ?,
|
|
5154
|
+
claim_owner = NULL, claimed_at = NULL, lease_expires_at = NULL, updated_at = ?
|
|
5155
|
+
WHERE json_extract(payload, '$.turnId') = ? AND status != 'acked'`).run(now, now, turnId);
|
|
5156
|
+
}
|
|
4692
5157
|
}
|
|
4693
5158
|
this.appendEvent(row.id, 'coordinator.turn_interrupted', {
|
|
4694
5159
|
turnId: messages[index].turnId || null,
|
|
@@ -4710,12 +5175,13 @@ export class WorkItemStore {
|
|
|
4710
5175
|
const action = this.getAction(row.action_id);
|
|
4711
5176
|
if (this.#hasActiveIntegrationReservation(action, now)) continue;
|
|
4712
5177
|
const workItem = this.getWorkItem(row.work_item_id);
|
|
4713
|
-
const graphMode =
|
|
5178
|
+
const graphMode = usesLegacyGraph(workItem);
|
|
5179
|
+
const concurrentMode = graphMode || isDynamicWorkItem(workItem);
|
|
4714
5180
|
const isCurrent = action?.status === 'running'
|
|
4715
5181
|
&& action.currentRunId === row.id
|
|
4716
5182
|
&& action.leaseEpoch === row.lease_epoch
|
|
4717
5183
|
&& workItem?.status === 'running'
|
|
4718
|
-
&& (
|
|
5184
|
+
&& (concurrentMode || (workItem.currentActionId === action.id && workItem.currentRunId === row.id));
|
|
4719
5185
|
if (!isCurrent) {
|
|
4720
5186
|
const staleStatus = workItem?.status === 'cancelled' ? 'cancelled' : 'superseded';
|
|
4721
5187
|
const stillOwnsAction = action?.status === 'running'
|
|
@@ -4739,7 +5205,11 @@ export class WorkItemStore {
|
|
|
4739
5205
|
actionId: row.action_id,
|
|
4740
5206
|
runId: row.id,
|
|
4741
5207
|
});
|
|
4742
|
-
if (
|
|
5208
|
+
if (concurrentMode && workItem) {
|
|
5209
|
+
const state = this.#graphWorkItemState(workItem.id);
|
|
5210
|
+
this.db.prepare(`UPDATE work_items SET status = ?, current_action_id = ?, current_run_id = NULL,
|
|
5211
|
+
updated_at = ? WHERE id = ?`).run(state.status, state.currentActionId, now, workItem.id);
|
|
5212
|
+
}
|
|
4743
5213
|
recovered += 1;
|
|
4744
5214
|
continue;
|
|
4745
5215
|
}
|
|
@@ -4750,8 +5220,10 @@ export class WorkItemStore {
|
|
|
4750
5220
|
const retryable = action.type !== 'deliver' && action.attempt < action.maxAttempts;
|
|
4751
5221
|
this.db.prepare(`UPDATE actions SET status = ?, current_run_id = NULL, updated_at = ?
|
|
4752
5222
|
WHERE id = ?`).run(retryable ? 'ready' : 'failed', now, action.id);
|
|
4753
|
-
if (
|
|
4754
|
-
this.#
|
|
5223
|
+
if (concurrentMode) {
|
|
5224
|
+
const state = this.#graphWorkItemState(workItem.id);
|
|
5225
|
+
this.db.prepare(`UPDATE work_items SET status = ?, current_action_id = ?, current_run_id = NULL,
|
|
5226
|
+
updated_at = ? WHERE id = ?`).run(state.status, state.currentActionId, now, workItem.id);
|
|
4755
5227
|
} else {
|
|
4756
5228
|
this.db.prepare(`UPDATE work_items SET status = ?, current_action_id = ?,
|
|
4757
5229
|
current_run_id = NULL, updated_at = ? WHERE id = ?`).run(
|
|
@@ -4765,6 +5237,9 @@ export class WorkItemStore {
|
|
|
4765
5237
|
actionId: action.id,
|
|
4766
5238
|
runId: row.id,
|
|
4767
5239
|
});
|
|
5240
|
+
if (isDynamicWorkItem(workItem) && !retryable) {
|
|
5241
|
+
this.#enqueueDynamicReconciliation(workItem.id, { runId: row.id, interrupted: true });
|
|
5242
|
+
}
|
|
4768
5243
|
recovered += 1;
|
|
4769
5244
|
}
|
|
4770
5245
|
return recovered;
|