@yeaft/webchat-agent 1.0.559 → 1.0.562
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 +207 -222
- 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 +1 -0
- package/yeaft/work-center/controller.js +21 -3
- package/yeaft/work-center/durable-model.js +16 -0
- package/yeaft/work-center/projection.js +3 -0
- package/yeaft/work-center/service.js +28 -1
- package/yeaft/work-center/store.js +131 -15
|
Binary file
|
package/package.json
CHANGED
|
@@ -254,6 +254,7 @@ export async function handleWorkCenterRequest(msg) {
|
|
|
254
254
|
op,
|
|
255
255
|
ok: false,
|
|
256
256
|
error: err?.message || String(err),
|
|
257
|
+
...(err?.code === 'WORK_CENTER_INPUT_STALE' ? { errorCode: err.code } : {}),
|
|
257
258
|
_requestUserId: msg._requestUserId || null,
|
|
258
259
|
});
|
|
259
260
|
}
|
|
@@ -164,6 +164,19 @@ export class WorkflowController {
|
|
|
164
164
|
return detail;
|
|
165
165
|
}
|
|
166
166
|
|
|
167
|
+
startScheduled(id, scheduledAt) {
|
|
168
|
+
return this.store.startWorkItemAtomic(id, workItem => {
|
|
169
|
+
const action = initialActionFor(workItem);
|
|
170
|
+
if (workItem.reuseMemory === false) return action;
|
|
171
|
+
const context = this.store.getReusableContext(workItem.workDir, workItem.id);
|
|
172
|
+
return {
|
|
173
|
+
...action,
|
|
174
|
+
context,
|
|
175
|
+
instruction: actionInstruction(action, workItem, context, renderSessionContextSnapshot(workItem.sessionContext)),
|
|
176
|
+
};
|
|
177
|
+
}, { scheduledAt });
|
|
178
|
+
}
|
|
179
|
+
|
|
167
180
|
update(id, patch) {
|
|
168
181
|
const updated = this.store.updateWorkItemAtomic(id, patch, initialActionFor);
|
|
169
182
|
if (!updated) throw new Error(`WorkItem not found: ${id}`);
|
|
@@ -251,11 +264,14 @@ export class WorkflowController {
|
|
|
251
264
|
throw new Error('actionId, revision, and generation are required for Action input');
|
|
252
265
|
}
|
|
253
266
|
const graphMode = workItem.workflowSnapshot?.executionMode === 'graph';
|
|
267
|
+
const concurrentMode = graphMode || workItem.coordinationMode === 'dynamic';
|
|
254
268
|
const targetMatches = targetAction?.workItemId === id
|
|
255
269
|
&& targetAction.generation === expectedGeneration
|
|
256
|
-
&& (
|
|
270
|
+
&& (concurrentMode || workItem.currentActionId === input.actionId);
|
|
257
271
|
if (!targetMatches || workItem.revision !== input.revision) {
|
|
258
|
-
|
|
272
|
+
const error = new Error('Action changed before input was applied; refresh and try again');
|
|
273
|
+
error.code = 'WORK_CENTER_INPUT_STALE';
|
|
274
|
+
throw error;
|
|
259
275
|
}
|
|
260
276
|
if (['ready', 'running'].includes(targetAction.status)) {
|
|
261
277
|
if (targetAction.status === 'running' && addedAttachmentCount > 0) {
|
|
@@ -269,7 +285,9 @@ export class WorkflowController {
|
|
|
269
285
|
}, input.attachments, input.addedAttachments, input.clientMessageId, quote);
|
|
270
286
|
}
|
|
271
287
|
if (!['waiting', 'failed'].includes(targetAction.status)) {
|
|
272
|
-
|
|
288
|
+
const error = new Error(`Action in ${targetAction.status} cannot accept input`);
|
|
289
|
+
error.code = 'WORK_CENTER_INPUT_STALE';
|
|
290
|
+
throw error;
|
|
273
291
|
}
|
|
274
292
|
return this.retry(id, {
|
|
275
293
|
answer: text,
|
|
@@ -20,6 +20,7 @@ const MIGRATIONS = [
|
|
|
20
20
|
['37-run-acceptance-checks', migrateRunAcceptanceChecks],
|
|
21
21
|
['38-action-closure-and-outputs', migrateActionClosureAndOutputs],
|
|
22
22
|
['39-action-creation-source', migrateActionCreationSource],
|
|
23
|
+
['40-work-item-schedules', migrateWorkItemSchedules],
|
|
23
24
|
];
|
|
24
25
|
|
|
25
26
|
const MIGRATION_ALIASES = new Map([
|
|
@@ -566,6 +567,21 @@ function migrateActionClosureAndOutputs(db) {
|
|
|
566
567
|
`);
|
|
567
568
|
}
|
|
568
569
|
|
|
570
|
+
function migrateWorkItemSchedules(db) {
|
|
571
|
+
for (const [column, definition] of [
|
|
572
|
+
['schedule_status', 'TEXT'],
|
|
573
|
+
['scheduled_for', 'INTEGER'],
|
|
574
|
+
['schedule_triggered_at', 'INTEGER'],
|
|
575
|
+
]) {
|
|
576
|
+
if (!hasColumn(db, 'work_items', column)) {
|
|
577
|
+
db.exec(`ALTER TABLE work_items ADD COLUMN ${column} ${definition}`);
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
db.exec(`CREATE INDEX IF NOT EXISTS idx_work_items_schedule_due
|
|
581
|
+
ON work_items(schedule_status, scheduled_for)
|
|
582
|
+
WHERE schedule_status = 'scheduled'`);
|
|
583
|
+
}
|
|
584
|
+
|
|
569
585
|
function migrateActionCreationSource(db) {
|
|
570
586
|
if (!hasColumn(db, 'actions', 'creation_source')) {
|
|
571
587
|
db.exec("ALTER TABLE actions ADD COLUMN creation_source TEXT NOT NULL DEFAULT 'legacy'");
|
|
@@ -1094,6 +1094,7 @@ export function projectWorkItemDetail(detail, options = {}) {
|
|
|
1094
1094
|
executionControl: detail.executionControl,
|
|
1095
1095
|
executionStats: combinedExecutionStats(detail),
|
|
1096
1096
|
reuseMemory: detail.reuseMemory !== false,
|
|
1097
|
+
schedule: detail.schedule || null,
|
|
1097
1098
|
deliveryTarget: ['response', 'workspace_files', 'pull_request', 'merge'].includes(detail.deliveryTarget)
|
|
1098
1099
|
? detail.deliveryTarget : null,
|
|
1099
1100
|
waitingReason: sanitizeDiagnosticText(waitingReason(detail), MAX_ACTION_DIAGNOSTIC_CHARS),
|
|
@@ -1204,6 +1205,7 @@ export function projectWorkItemSummary(detail) {
|
|
|
1204
1205
|
executionStats: combinedExecutionStats(detail),
|
|
1205
1206
|
executionControl: detail.executionControl,
|
|
1206
1207
|
origin: detail.origin?.sessionId ? { sessionId: detail.origin.sessionId } : null,
|
|
1208
|
+
schedule: detail.schedule || null,
|
|
1207
1209
|
linkedSessionIds: Array.isArray(detail.linkedSessionIds) ? detail.linkedSessionIds : [],
|
|
1208
1210
|
attachmentCount: Array.isArray(detail.attachments) ? detail.attachments.length : 0,
|
|
1209
1211
|
createdAt: detail.createdAt,
|
|
@@ -1244,6 +1246,7 @@ export function projectWorkItemSummary(detail) {
|
|
|
1244
1246
|
currentAction: projectCurrentActionSummary(action, projectedAction),
|
|
1245
1247
|
actionStats: projectActionStats(detail, null),
|
|
1246
1248
|
origin: detail.origin?.sessionId ? { sessionId: detail.origin.sessionId } : null,
|
|
1249
|
+
schedule: detail.schedule || null,
|
|
1247
1250
|
linkedSessionIds: Array.isArray(detail.linkedSessionIds) ? detail.linkedSessionIds : [],
|
|
1248
1251
|
attachmentCount: Array.isArray(detail.attachments) ? detail.attachments.length : 0,
|
|
1249
1252
|
createdAt: detail.createdAt,
|
|
@@ -97,6 +97,7 @@ export class WorkCenterService {
|
|
|
97
97
|
constructor(options) {
|
|
98
98
|
const yeaftDir = requiredString(options?.yeaftDir, 'yeaftDir');
|
|
99
99
|
this.yeaftDir = yeaftDir;
|
|
100
|
+
this.now = typeof options.now === 'function' ? options.now : () => Date.now();
|
|
100
101
|
this.settingsReader = options.settingsReader || readWorkCenterSettings;
|
|
101
102
|
this.settingsWriter = options.settingsWriter || writeWorkCenterSettings;
|
|
102
103
|
this.runtimeInfoProvider = typeof options.runtimeInfoProvider === 'function'
|
|
@@ -246,7 +247,13 @@ export class WorkCenterService {
|
|
|
246
247
|
root: this.attachmentRoot,
|
|
247
248
|
workItemId,
|
|
248
249
|
});
|
|
249
|
-
const
|
|
250
|
+
const scheduledFor = payload.scheduledFor == null || payload.scheduledFor === ''
|
|
251
|
+
? null : Number(payload.scheduledFor);
|
|
252
|
+
if (scheduledFor != null && (!Number.isSafeInteger(scheduledFor) || scheduledFor <= this.now())) {
|
|
253
|
+
throw new Error('scheduledFor must be in the future');
|
|
254
|
+
}
|
|
255
|
+
const shouldStart = scheduledFor == null
|
|
256
|
+
&& (payload.start === undefined ? settings.startImmediately : payload.start !== false);
|
|
250
257
|
const goal = requiredString(payload.goal, 'goal');
|
|
251
258
|
const explicitTitle = payload.titleSource !== 'coordinator_pending' && typeof payload.title === 'string'
|
|
252
259
|
? payload.title.trim() : '';
|
|
@@ -289,6 +296,10 @@ export class WorkCenterService {
|
|
|
289
296
|
? normalizeSessionContextSnapshot(payload.sessionContext)
|
|
290
297
|
: [],
|
|
291
298
|
attachments,
|
|
299
|
+
schedule: scheduledFor == null ? null : {
|
|
300
|
+
status: payload.scheduleEnabled === false ? 'paused' : 'scheduled',
|
|
301
|
+
scheduledFor,
|
|
302
|
+
},
|
|
292
303
|
start: false,
|
|
293
304
|
});
|
|
294
305
|
let detail = this.#requiredItem(workItemId);
|
|
@@ -313,6 +324,13 @@ export class WorkCenterService {
|
|
|
313
324
|
this.#emit({ type: 'work_item.updated', workItem: detail });
|
|
314
325
|
return detail;
|
|
315
326
|
}
|
|
327
|
+
case 'update_schedule': {
|
|
328
|
+
const id = requiredString(payload.id, 'id');
|
|
329
|
+
const detail = this.store.updateWorkItemSchedule(id, payload.schedule || payload);
|
|
330
|
+
if (!detail) throw new Error(`WorkItem not found: ${id}`);
|
|
331
|
+
this.#emit({ type: 'work_item.schedule_updated', workItem: detail });
|
|
332
|
+
return detail;
|
|
333
|
+
}
|
|
316
334
|
case 'start': {
|
|
317
335
|
const detail = this.controller.start(requiredString(payload.id, 'id'));
|
|
318
336
|
if (detail.coordinationMode === DYNAMIC_COORDINATION_MODE) {
|
|
@@ -715,7 +733,16 @@ export class WorkCenterService {
|
|
|
715
733
|
}
|
|
716
734
|
}
|
|
717
735
|
|
|
736
|
+
#scanSchedules() {
|
|
737
|
+
const now = this.now();
|
|
738
|
+
for (const id of this.store.listDueScheduledWorkItemIds(now)) {
|
|
739
|
+
const detail = this.controller.startScheduled(id, now);
|
|
740
|
+
if (detail) this.#emit({ type: 'work_item.schedule_triggered', workItem: detail });
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
|
|
718
744
|
#scanRecoveries() {
|
|
745
|
+
this.#scanSchedules();
|
|
719
746
|
this.#scanCoordinatorProviderRecoveries();
|
|
720
747
|
this.#scanDynamicCoordinatorWakes();
|
|
721
748
|
this.#scanFailureRecoveries();
|
|
@@ -112,6 +112,11 @@ function mapWorkItem(row) {
|
|
|
112
112
|
coordinationMode: row.coordination_mode || 'legacy',
|
|
113
113
|
finalResult: parseJson(row.final_result, null),
|
|
114
114
|
deliveryTarget: row.delivery_target || null,
|
|
115
|
+
schedule: row.schedule_status ? {
|
|
116
|
+
status: row.schedule_status,
|
|
117
|
+
scheduledFor: Number(row.scheduled_for) || null,
|
|
118
|
+
triggeredAt: Number(row.schedule_triggered_at) || null,
|
|
119
|
+
} : null,
|
|
115
120
|
title: row.title,
|
|
116
121
|
titleSource: row.title_source || 'explicit',
|
|
117
122
|
requirement: row.requirement ?? row.goal,
|
|
@@ -858,6 +863,9 @@ export class WorkItemStore {
|
|
|
858
863
|
coordination_mode TEXT NOT NULL DEFAULT 'legacy',
|
|
859
864
|
final_result TEXT,
|
|
860
865
|
delivery_target TEXT,
|
|
866
|
+
schedule_status TEXT,
|
|
867
|
+
scheduled_for INTEGER,
|
|
868
|
+
schedule_triggered_at INTEGER,
|
|
861
869
|
title TEXT NOT NULL,
|
|
862
870
|
title_source TEXT NOT NULL DEFAULT 'explicit',
|
|
863
871
|
requirement TEXT,
|
|
@@ -1022,6 +1030,16 @@ export class WorkItemStore {
|
|
|
1022
1030
|
|
|
1023
1031
|
// The feature shipped first as an unmerged PR, but keep the store tolerant
|
|
1024
1032
|
// of databases created by review builds.
|
|
1033
|
+
if (!hasColumn(this.db, 'work_items', 'schedule_status')) {
|
|
1034
|
+
this.db.exec('ALTER TABLE work_items ADD COLUMN schedule_status TEXT');
|
|
1035
|
+
}
|
|
1036
|
+
if (!hasColumn(this.db, 'work_items', 'scheduled_for')) {
|
|
1037
|
+
this.db.exec('ALTER TABLE work_items ADD COLUMN scheduled_for INTEGER');
|
|
1038
|
+
}
|
|
1039
|
+
if (!hasColumn(this.db, 'work_items', 'schedule_triggered_at')) {
|
|
1040
|
+
this.db.exec('ALTER TABLE work_items ADD COLUMN schedule_triggered_at INTEGER');
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1025
1043
|
if (!hasColumn(this.db, 'work_items', 'workspace_key')) {
|
|
1026
1044
|
withTransaction(this.db, () => {
|
|
1027
1045
|
this.db.exec("ALTER TABLE work_items ADD COLUMN workspace_key TEXT NOT NULL DEFAULT ''");
|
|
@@ -1944,7 +1962,9 @@ export class WorkItemStore {
|
|
|
1944
1962
|
? ['ready', 'running', 'waiting', 'needs_attention']
|
|
1945
1963
|
: ['ready', 'running'];
|
|
1946
1964
|
if (!inputStatuses.includes(workItem.status)) {
|
|
1947
|
-
|
|
1965
|
+
const error = new Error(`WorkItem in ${workItem.status} cannot accept Action input`);
|
|
1966
|
+
error.code = 'WORK_CENTER_INPUT_STALE';
|
|
1967
|
+
throw error;
|
|
1948
1968
|
}
|
|
1949
1969
|
const action = this.getAction(expected.actionId);
|
|
1950
1970
|
const actionMatches = action?.workItemId === id
|
|
@@ -1956,7 +1976,9 @@ export class WorkItemStore {
|
|
|
1956
1976
|
|| (activeRun?.status === 'running' && activeRun.acceptingInput !== false
|
|
1957
1977
|
&& runMatchesActionIdentity(activeRun, action));
|
|
1958
1978
|
if (!actionMatches || !runMatches || workItem.revision !== expected.revision) {
|
|
1959
|
-
|
|
1979
|
+
const error = new Error('Action changed before input was applied; refresh and try again');
|
|
1980
|
+
error.code = 'WORK_CENTER_INPUT_STALE';
|
|
1981
|
+
throw error;
|
|
1960
1982
|
}
|
|
1961
1983
|
const projectedAttachments = (Array.isArray(addedAttachments) ? addedAttachments : []).map(attachment => ({
|
|
1962
1984
|
id: attachment.id,
|
|
@@ -1998,7 +2020,7 @@ export class WorkItemStore {
|
|
|
1998
2020
|
const changedAction = this.db.prepare(`UPDATE actions SET context = ?, instruction = ?, attempt = 0,
|
|
1999
2021
|
generation = generation + 1, spec_hash = ?, identity_history = ?, result_run_id = NULL,
|
|
2000
2022
|
workspace = NULL, updated_at = ? WHERE id = ? AND status = 'ready' AND current_run_id IS NULL
|
|
2001
|
-
AND generation = ? AND spec_hash = ?`).run(
|
|
2023
|
+
AND generation = ? AND spec_hash = ? AND contract_revision = ?`).run(
|
|
2002
2024
|
stringify(context),
|
|
2003
2025
|
nextAction.instruction,
|
|
2004
2026
|
nextAction.specHash,
|
|
@@ -2007,9 +2029,12 @@ export class WorkItemStore {
|
|
|
2007
2029
|
action.id,
|
|
2008
2030
|
action.generation,
|
|
2009
2031
|
action.specHash,
|
|
2032
|
+
action.contractRevision,
|
|
2010
2033
|
);
|
|
2011
2034
|
if (Number(changedAction.changes) !== 1) {
|
|
2012
|
-
|
|
2035
|
+
const error = new Error('Action changed before input was applied; refresh and try again');
|
|
2036
|
+
error.code = 'WORK_CENTER_INPUT_STALE';
|
|
2037
|
+
throw error;
|
|
2013
2038
|
}
|
|
2014
2039
|
this.#supersedePendingActionInputs(
|
|
2015
2040
|
[action],
|
|
@@ -2400,14 +2425,16 @@ export class WorkItemStore {
|
|
|
2400
2425
|
const workspaceKey = canonicalWorkspaceKey(input.workDir);
|
|
2401
2426
|
this.db.prepare(`INSERT INTO work_items
|
|
2402
2427
|
(id, revision, execution_schema_version, ledger_revision, coordination_mode, final_result, delivery_target,
|
|
2403
|
-
title, title_source, requirement, goal, acceptance_criteria, workflow_template, workflow_snapshot, status,
|
|
2428
|
+
schedule_status, scheduled_for, schedule_triggered_at, title, title_source, requirement, goal, acceptance_criteria, workflow_template, workflow_snapshot, status,
|
|
2404
2429
|
current_action_id, current_run_id, work_dir, workspace_key, reuse_memory, origin, linked_session_ids,
|
|
2405
2430
|
session_context, attachments, created_at, updated_at)
|
|
2406
|
-
VALUES (?, 1, ?, 0, ?, NULL, ?, ?, ?, ?, ?, ?, ?, ?, ?, NULL, NULL, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(
|
|
2431
|
+
VALUES (?, 1, ?, 0, ?, NULL, ?, ?, ?, NULL, ?, ?, ?, ?, ?, ?, ?, ?, NULL, NULL, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(
|
|
2407
2432
|
id,
|
|
2408
2433
|
Number.isInteger(input.executionSchemaVersion) ? input.executionSchemaVersion : 2,
|
|
2409
2434
|
input.coordinationMode || 'legacy',
|
|
2410
2435
|
input.deliveryTarget || null,
|
|
2436
|
+
input.schedule?.status || null,
|
|
2437
|
+
input.schedule?.scheduledFor || null,
|
|
2411
2438
|
input.title,
|
|
2412
2439
|
input.titleSource === 'coordinator_pending' ? 'coordinator_pending' : 'explicit',
|
|
2413
2440
|
input.goal,
|
|
@@ -3092,6 +3119,32 @@ export class WorkItemStore {
|
|
|
3092
3119
|
}));
|
|
3093
3120
|
}
|
|
3094
3121
|
|
|
3122
|
+
listDueScheduledWorkItemIds(now = this.now()) {
|
|
3123
|
+
return this.db.prepare(`SELECT id FROM work_items
|
|
3124
|
+
WHERE schedule_status = 'scheduled' AND scheduled_for <= ? AND status = 'draft'
|
|
3125
|
+
ORDER BY scheduled_for, id`).all(now).map(row => row.id);
|
|
3126
|
+
}
|
|
3127
|
+
|
|
3128
|
+
updateWorkItemSchedule(id, input = {}) {
|
|
3129
|
+
return withTransaction(this.db, () => {
|
|
3130
|
+
const row = this.db.prepare('SELECT * FROM work_items WHERE id = ?').get(id);
|
|
3131
|
+
if (!row) return null;
|
|
3132
|
+
if (row.status !== 'draft' || !['scheduled', 'paused'].includes(row.schedule_status)) {
|
|
3133
|
+
throw new Error('Only pending scheduled WorkItems can be changed');
|
|
3134
|
+
}
|
|
3135
|
+
const scheduledFor = input.scheduledFor == null ? Number(row.scheduled_for) : Number(input.scheduledFor);
|
|
3136
|
+
if (!Number.isSafeInteger(scheduledFor) || scheduledFor <= this.now()) {
|
|
3137
|
+
throw new Error('scheduledFor must be in the future');
|
|
3138
|
+
}
|
|
3139
|
+
const status = input.enabled === false ? 'paused' : 'scheduled';
|
|
3140
|
+
const now = this.now();
|
|
3141
|
+
this.db.prepare(`UPDATE work_items SET schedule_status = ?, scheduled_for = ?,
|
|
3142
|
+
revision = revision + 1, updated_at = ? WHERE id = ?`).run(status, scheduledFor, now, id);
|
|
3143
|
+
this.appendEvent(id, 'work_item.schedule_updated', { status, scheduledFor });
|
|
3144
|
+
return this.getWorkItemDetail(id);
|
|
3145
|
+
});
|
|
3146
|
+
}
|
|
3147
|
+
|
|
3095
3148
|
listWorkItems(filters = {}) {
|
|
3096
3149
|
const where = [];
|
|
3097
3150
|
const values = [];
|
|
@@ -4485,10 +4538,14 @@ export class WorkItemStore {
|
|
|
4485
4538
|
});
|
|
4486
4539
|
}
|
|
4487
4540
|
|
|
4488
|
-
startWorkItemAtomic(id, makeInitialAction) {
|
|
4541
|
+
startWorkItemAtomic(id, makeInitialAction, options = {}) {
|
|
4489
4542
|
return withTransaction(this.db, () => {
|
|
4490
4543
|
const workItem = this.getWorkItem(id);
|
|
4491
4544
|
if (!workItem) return null;
|
|
4545
|
+
const scheduledAt = Number(options.scheduledAt);
|
|
4546
|
+
const scheduledStart = Number.isSafeInteger(scheduledAt);
|
|
4547
|
+
if (scheduledStart && (workItem.status !== 'draft' || workItem.schedule?.status !== 'scheduled'
|
|
4548
|
+
|| workItem.schedule.scheduledFor > scheduledAt)) return null;
|
|
4492
4549
|
if (['done', 'cancelled'].includes(workItem.status)) {
|
|
4493
4550
|
throw new Error(`Cannot start WorkItem in ${workItem.status}`);
|
|
4494
4551
|
}
|
|
@@ -4498,13 +4555,18 @@ export class WorkItemStore {
|
|
|
4498
4555
|
throw new Error(`WorkItem in ${workItem.status} must be resumed with retry`);
|
|
4499
4556
|
}
|
|
4500
4557
|
const now = this.now();
|
|
4558
|
+
if (scheduledStart) {
|
|
4559
|
+
this.db.prepare(`UPDATE work_items SET schedule_status = 'triggered',
|
|
4560
|
+
schedule_triggered_at = ?, revision = revision + 1, updated_at = ? WHERE id = ?`)
|
|
4561
|
+
.run(scheduledAt, now, id);
|
|
4562
|
+
}
|
|
4501
4563
|
if (isDynamicWorkItem(workItem)) {
|
|
4502
4564
|
this.db.prepare(`UPDATE work_items SET status = 'running', current_action_id = NULL,
|
|
4503
4565
|
current_run_id = NULL, updated_at = ? WHERE id = ?`).run(now, id);
|
|
4504
4566
|
this.enqueueCoordinatorMailbox(id, 'work_item_started', {
|
|
4505
4567
|
trigger: { workItemId: id },
|
|
4506
4568
|
}, `dynamic:start:${id}:${workItem.revision}`);
|
|
4507
|
-
this.appendEvent(id, 'work_item.started');
|
|
4569
|
+
this.appendEvent(id, 'work_item.started', scheduledStart ? { scheduled: true } : {});
|
|
4508
4570
|
return this.getWorkItemDetail(id);
|
|
4509
4571
|
}
|
|
4510
4572
|
const action = this.#insertAction(id, {
|
|
@@ -4513,7 +4575,7 @@ export class WorkItemStore {
|
|
|
4513
4575
|
}, this.#nextSequence(id), now);
|
|
4514
4576
|
this.db.prepare(`UPDATE work_items SET status = 'ready', current_action_id = ?,
|
|
4515
4577
|
current_run_id = NULL, updated_at = ? WHERE id = ?`).run(action.id, now, id);
|
|
4516
|
-
this.appendEvent(id, 'work_item.started', {}, { actionId: action.id });
|
|
4578
|
+
this.appendEvent(id, 'work_item.started', scheduledStart ? { scheduled: true } : {}, { actionId: action.id });
|
|
4517
4579
|
return this.getWorkItemDetail(id);
|
|
4518
4580
|
});
|
|
4519
4581
|
}
|
|
@@ -4536,12 +4598,15 @@ export class WorkItemStore {
|
|
|
4536
4598
|
}
|
|
4537
4599
|
const workItem = this.getWorkItem(id);
|
|
4538
4600
|
if (!workItem) return null;
|
|
4601
|
+
const dynamicMode = isDynamicWorkItem(workItem);
|
|
4539
4602
|
const graphMode = usesLegacyGraph(workItem);
|
|
4540
|
-
const retryableWorkItemStatuses = graphMode
|
|
4603
|
+
const retryableWorkItemStatuses = graphMode || dynamicMode
|
|
4541
4604
|
? ['ready', 'running', 'waiting', 'needs_attention']
|
|
4542
4605
|
: ['waiting', 'needs_attention'];
|
|
4543
4606
|
if (!retryableWorkItemStatuses.includes(workItem.status)) {
|
|
4544
|
-
|
|
4607
|
+
const error = new Error(`WorkItem in ${workItem.status} does not need retry`);
|
|
4608
|
+
error.code = 'WORK_CENTER_INPUT_STALE';
|
|
4609
|
+
throw error;
|
|
4545
4610
|
}
|
|
4546
4611
|
let previous = workItem.currentActionId ? this.getAction(workItem.currentActionId) : null;
|
|
4547
4612
|
if (options.expected) {
|
|
@@ -4553,10 +4618,12 @@ export class WorkItemStore {
|
|
|
4553
4618
|
const hasExpectedGeneration = Number.isInteger(expectedGeneration) && expectedGeneration > 0;
|
|
4554
4619
|
const expectedMatches = expectedAction?.workItemId === id
|
|
4555
4620
|
&& allowedExpectedStatuses.includes(expectedAction.status)
|
|
4556
|
-
&& (graphMode || workItem.currentActionId === options.expected.actionId)
|
|
4557
|
-
&& (hasExpectedGeneration ? expectedAction.generation === expectedGeneration : !graphMode);
|
|
4621
|
+
&& (graphMode || dynamicMode || workItem.currentActionId === options.expected.actionId)
|
|
4622
|
+
&& (hasExpectedGeneration ? expectedAction.generation === expectedGeneration : !(graphMode || dynamicMode));
|
|
4558
4623
|
if (!expectedMatches || workItem.revision !== options.expected.revision) {
|
|
4559
|
-
|
|
4624
|
+
const error = new Error('Action changed before input was applied; refresh and try again');
|
|
4625
|
+
error.code = 'WORK_CENTER_INPUT_STALE';
|
|
4626
|
+
throw error;
|
|
4560
4627
|
}
|
|
4561
4628
|
previous = expectedAction;
|
|
4562
4629
|
}
|
|
@@ -4565,7 +4632,7 @@ export class WorkItemStore {
|
|
|
4565
4632
|
AND status != 'running' ORDER BY ended_at DESC, started_at DESC LIMIT 1`).get(id, previous.id))
|
|
4566
4633
|
: null;
|
|
4567
4634
|
const now = this.now();
|
|
4568
|
-
const revision = options.expected ? workItem.revision + 1 : workItem.revision;
|
|
4635
|
+
const revision = options.expected && !dynamicMode ? workItem.revision + 1 : workItem.revision;
|
|
4569
4636
|
const replacement = {
|
|
4570
4637
|
...makeAction(workItem, previous, previousRun),
|
|
4571
4638
|
contractRevision: previous?.contractRevision ?? workItem.revision,
|
|
@@ -4573,6 +4640,55 @@ export class WorkItemStore {
|
|
|
4573
4640
|
const inputEvent = options.inputEvent && typeof options.inputEvent === 'object'
|
|
4574
4641
|
? options.inputEvent
|
|
4575
4642
|
: null;
|
|
4643
|
+
if (dynamicMode) {
|
|
4644
|
+
if (!previous) throw new Error('Dynamic WorkItem retry target is missing');
|
|
4645
|
+
const candidate = {
|
|
4646
|
+
...previous,
|
|
4647
|
+
...replacement,
|
|
4648
|
+
id: previous.id,
|
|
4649
|
+
generation: previous.generation + 1,
|
|
4650
|
+
attempt: 0,
|
|
4651
|
+
currentRunId: null,
|
|
4652
|
+
resultRunId: null,
|
|
4653
|
+
workspace: null,
|
|
4654
|
+
};
|
|
4655
|
+
candidate.instruction = canonicalActionInstruction(workItem, candidate, candidate.context);
|
|
4656
|
+
candidate.specHash = actionSpecHash(candidate);
|
|
4657
|
+
const changed = this.db.prepare(`UPDATE actions SET status = 'ready', attempt = 0,
|
|
4658
|
+
current_run_id = NULL, context = ?, instruction = ?, generation = generation + 1,
|
|
4659
|
+
spec_hash = ?, identity_history = ?, result_run_id = NULL, workspace = NULL, updated_at = ?
|
|
4660
|
+
WHERE id = ? AND work_item_id = ? AND status IN ('waiting', 'failed')
|
|
4661
|
+
AND current_run_id IS NULL AND generation = ? AND spec_hash = ? AND contract_revision = ?`).run(
|
|
4662
|
+
stringify(candidate.context), candidate.instruction, candidate.specHash,
|
|
4663
|
+
stringify(actionIdentityHistory(previous, candidate.generation, candidate.specHash)),
|
|
4664
|
+
now, previous.id, id, previous.generation, previous.specHash, previous.contractRevision,
|
|
4665
|
+
);
|
|
4666
|
+
if (Number(changed.changes) !== 1) {
|
|
4667
|
+
const error = new Error('Action changed before input was applied; refresh and try again');
|
|
4668
|
+
error.code = 'WORK_CENTER_INPUT_STALE';
|
|
4669
|
+
throw error;
|
|
4670
|
+
}
|
|
4671
|
+
const changedWorkItem = this.db.prepare(`UPDATE work_items SET attachments = ?, updated_at = ?
|
|
4672
|
+
WHERE id = ? AND revision = ? AND status NOT IN ('done', 'cancelled')`).run(
|
|
4673
|
+
stringify(Array.isArray(options.attachments) ? options.attachments : workItem.attachments),
|
|
4674
|
+
now, id, workItem.revision,
|
|
4675
|
+
);
|
|
4676
|
+
if (Number(changedWorkItem.changes) !== 1) {
|
|
4677
|
+
const error = new Error('Action changed before input was applied; refresh and try again');
|
|
4678
|
+
error.code = 'WORK_CENTER_INPUT_STALE';
|
|
4679
|
+
throw error;
|
|
4680
|
+
}
|
|
4681
|
+
const action = this.getAction(previous.id);
|
|
4682
|
+
if (inputEvent) {
|
|
4683
|
+
this.appendEvent(id, 'action.input_added', inputEvent, {
|
|
4684
|
+
actionId: action.id,
|
|
4685
|
+
actionGeneration: action.generation,
|
|
4686
|
+
});
|
|
4687
|
+
} else {
|
|
4688
|
+
this.appendEvent(id, 'work_item.retried', {}, { actionId: action.id });
|
|
4689
|
+
}
|
|
4690
|
+
return this.getWorkItemDetail(id);
|
|
4691
|
+
}
|
|
4576
4692
|
if (graphMode) {
|
|
4577
4693
|
if (!previous) throw new Error('WorkItem graph retry target is missing');
|
|
4578
4694
|
const action = this.#resetGraphFromStage(
|