@yeaft/webchat-agent 1.0.252 → 1.0.253
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 +2 -2
- 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/work-center/coordinator.js +522 -136
- package/yeaft/work-center/projection.js +1 -1
- package/yeaft/work-center/service.js +102 -0
- package/yeaft/work-center/store.js +159 -21
|
@@ -894,7 +894,7 @@ export function projectWorkItemDetail(detail, options = {}) {
|
|
|
894
894
|
status: ['thinking', 'completed', 'failed'].includes(message.status) ? message.status : 'completed',
|
|
895
895
|
error: truncateUtf8(message.error || '', MAX_ACTION_DIAGNOSTIC_CHARS) || null,
|
|
896
896
|
decision: message.decision && typeof message.decision === 'object' ? {
|
|
897
|
-
kind: ['answer', 'guide_actions', 'replan'].includes(message.decision.kind)
|
|
897
|
+
kind: ['answer', 'guide_actions', 'replan', 'request_human'].includes(message.decision.kind)
|
|
898
898
|
? message.decision.kind : null,
|
|
899
899
|
reason: truncateUtf8(message.decision.reason || '', MAX_ACTION_DIAGNOSTIC_CHARS),
|
|
900
900
|
changedContract: message.decision.changedContract === true,
|
|
@@ -107,6 +107,13 @@ export class WorkCenterService {
|
|
|
107
107
|
});
|
|
108
108
|
this.coordinator = options.coordinator || null;
|
|
109
109
|
this.onEvent = typeof options.onEvent === 'function' ? options.onEvent : () => {};
|
|
110
|
+
this.recoveryTasks = new Map();
|
|
111
|
+
this.recoveryQueue = new Map();
|
|
112
|
+
this.recoveryPollIntervalMs = Number(options.pollIntervalMs) > 0
|
|
113
|
+
? Number(options.pollIntervalMs)
|
|
114
|
+
: 2_000;
|
|
115
|
+
this.recoveryTimer = null;
|
|
116
|
+
this.shuttingDown = false;
|
|
110
117
|
this.watcher = new WorkItemWatcher({
|
|
111
118
|
store: this.store,
|
|
112
119
|
controller: this.controller,
|
|
@@ -485,14 +492,109 @@ export class WorkCenterService {
|
|
|
485
492
|
|
|
486
493
|
#emit(event) {
|
|
487
494
|
try { this.onEvent(event); } catch {}
|
|
495
|
+
if (['run.finished', 'coordinator.turn_completed'].includes(event?.type)) {
|
|
496
|
+
for (const action of event.workItem?.actions || []) {
|
|
497
|
+
if (action.status !== 'failed') continue;
|
|
498
|
+
this.#enqueueFailureRecovery({
|
|
499
|
+
workItemId: event.workItem.id,
|
|
500
|
+
actionId: action.id,
|
|
501
|
+
actionGeneration: action.generation,
|
|
502
|
+
});
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
#recoveryKey(entry) {
|
|
508
|
+
return `${entry.workItemId}:${entry.actionId}:${entry.actionGeneration}`;
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
#enqueueFailureRecovery(entry) {
|
|
512
|
+
if (this.shuttingDown || !this.coordinator || !entry?.workItemId || !entry.actionId) return;
|
|
513
|
+
const key = this.#recoveryKey(entry);
|
|
514
|
+
this.recoveryQueue.set(key, entry);
|
|
515
|
+
this.#drainFailureRecoveryQueue();
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
#scanFailureRecoveries() {
|
|
519
|
+
if (this.shuttingDown || !this.coordinator) return;
|
|
520
|
+
const now = Date.now();
|
|
521
|
+
for (const entry of this.store.listFailedActionRecoveries()) {
|
|
522
|
+
const delay = entry.recoveryAttempts > 0
|
|
523
|
+
? Math.min(1_000 * (2 ** Math.min(entry.recoveryAttempts - 1, 9)), 300_000)
|
|
524
|
+
: 0;
|
|
525
|
+
if (entry.lastRecoveryAt > 0 && now < entry.lastRecoveryAt + delay) continue;
|
|
526
|
+
this.recoveryQueue.set(this.#recoveryKey(entry), entry);
|
|
527
|
+
}
|
|
528
|
+
this.#drainFailureRecoveryQueue();
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
#drainFailureRecoveryQueue() {
|
|
532
|
+
if (this.shuttingDown || !this.coordinator || this.recoveryTasks.size > 0) return;
|
|
533
|
+
let next = null;
|
|
534
|
+
for (const [key, entry] of this.recoveryQueue) {
|
|
535
|
+
const detail = this.store.getWorkItemDetail(entry.workItemId);
|
|
536
|
+
const action = detail?.actions?.find(candidate => candidate.id === entry.actionId);
|
|
537
|
+
if (!detail || ['done', 'cancelled'].includes(detail.status)
|
|
538
|
+
|| action?.status !== 'failed'
|
|
539
|
+
|| action.generation !== entry.actionGeneration) {
|
|
540
|
+
this.recoveryQueue.delete(key);
|
|
541
|
+
continue;
|
|
542
|
+
}
|
|
543
|
+
if (detail.actions.some(candidate => candidate.status === 'running')) continue;
|
|
544
|
+
next = [key, entry];
|
|
545
|
+
break;
|
|
546
|
+
}
|
|
547
|
+
if (!next) return;
|
|
548
|
+
const [key, entry] = next;
|
|
549
|
+
this.recoveryQueue.delete(key);
|
|
550
|
+
let turn;
|
|
551
|
+
try {
|
|
552
|
+
turn = this.coordinator.recover(entry.workItemId, {
|
|
553
|
+
actionId: entry.actionId,
|
|
554
|
+
actionGeneration: entry.actionGeneration,
|
|
555
|
+
onUpdate: (type, workItem) => {
|
|
556
|
+
this.watcher.abortInvalidWorkItemRuns(entry.workItemId);
|
|
557
|
+
this.#emit({ type, actionId: entry.actionId, workItem });
|
|
558
|
+
},
|
|
559
|
+
});
|
|
560
|
+
} catch (error) {
|
|
561
|
+
this.#emit({
|
|
562
|
+
type: 'coordinator.recovery_schedule_failed',
|
|
563
|
+
actionId: entry.actionId,
|
|
564
|
+
workItem: this.store.getWorkItemDetail(entry.workItemId),
|
|
565
|
+
error: error?.message || String(error),
|
|
566
|
+
});
|
|
567
|
+
return null;
|
|
568
|
+
}
|
|
569
|
+
if (!turn) return null;
|
|
570
|
+
const task = turn.task.finally(() => {
|
|
571
|
+
this.recoveryTasks.delete(key);
|
|
572
|
+
});
|
|
573
|
+
this.recoveryTasks.set(key, task);
|
|
574
|
+
task.catch(() => {});
|
|
575
|
+
return task;
|
|
488
576
|
}
|
|
489
577
|
|
|
490
578
|
start() {
|
|
579
|
+
this.#scanFailureRecoveries();
|
|
580
|
+
if (!this.recoveryTimer) {
|
|
581
|
+
this.recoveryTimer = setInterval(
|
|
582
|
+
() => this.#scanFailureRecoveries(),
|
|
583
|
+
this.recoveryPollIntervalMs,
|
|
584
|
+
);
|
|
585
|
+
this.recoveryTimer.unref?.();
|
|
586
|
+
}
|
|
491
587
|
this.watcher.start();
|
|
492
588
|
}
|
|
493
589
|
|
|
494
590
|
async shutdown() {
|
|
591
|
+
this.shuttingDown = true;
|
|
592
|
+
if (this.recoveryTimer) clearInterval(this.recoveryTimer);
|
|
593
|
+
this.recoveryTimer = null;
|
|
495
594
|
await this.coordinator?.shutdown?.();
|
|
595
|
+
await Promise.allSettled([...this.recoveryTasks.values()]);
|
|
596
|
+
this.recoveryTasks.clear();
|
|
597
|
+
this.recoveryQueue.clear();
|
|
496
598
|
await this.watcher.stop();
|
|
497
599
|
try { await this.watcher.runner?.shutdown?.(); } catch {}
|
|
498
600
|
try { await this.watcher.runner?.trace?.close?.(); } catch {}
|
|
@@ -50,6 +50,28 @@ function coordinatorActionFence(actions) {
|
|
|
50
50
|
})).sort((left, right) => left.id.localeCompare(right.id))), 'utf8').digest('hex');
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
+
function coordinatorRecoveryIdentity(value) {
|
|
54
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) return null;
|
|
55
|
+
const actionId = typeof value.actionId === 'string' ? value.actionId : '';
|
|
56
|
+
const stageId = typeof value.stageId === 'string' ? value.stageId : '';
|
|
57
|
+
const actionGeneration = Number(value.actionGeneration);
|
|
58
|
+
if (!actionId || !stageId || !Number.isInteger(actionGeneration) || actionGeneration < 1) return null;
|
|
59
|
+
return { actionId, actionGeneration, stageId };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function sameCoordinatorRecoveryIdentity(persisted, expected) {
|
|
63
|
+
const persistedPresent = persisted != null;
|
|
64
|
+
const expectedPresent = expected != null;
|
|
65
|
+
if (!persistedPresent && !expectedPresent) return true;
|
|
66
|
+
if (persistedPresent !== expectedPresent) return false;
|
|
67
|
+
const persistedIdentity = coordinatorRecoveryIdentity(persisted);
|
|
68
|
+
const expectedIdentity = coordinatorRecoveryIdentity(expected);
|
|
69
|
+
return !!persistedIdentity && !!expectedIdentity
|
|
70
|
+
&& persistedIdentity.actionId === expectedIdentity.actionId
|
|
71
|
+
&& persistedIdentity.actionGeneration === expectedIdentity.actionGeneration
|
|
72
|
+
&& persistedIdentity.stageId === expectedIdentity.stageId;
|
|
73
|
+
}
|
|
74
|
+
|
|
53
75
|
function actionSpecHash(action) {
|
|
54
76
|
const spec = {
|
|
55
77
|
type: action.type || '',
|
|
@@ -1991,6 +2013,26 @@ export class WorkItemStore {
|
|
|
1991
2013
|
}));
|
|
1992
2014
|
}
|
|
1993
2015
|
|
|
2016
|
+
listFailedActionRecoveries() {
|
|
2017
|
+
return this.db.prepare(`SELECT a.work_item_id, a.id AS action_id, a.generation,
|
|
2018
|
+
COUNT(recovery_event.id) AS recovery_attempts,
|
|
2019
|
+
MAX(recovery_event.created_at) AS last_recovery_at
|
|
2020
|
+
FROM actions a JOIN work_items w ON w.id = a.work_item_id
|
|
2021
|
+
LEFT JOIN events recovery_event ON recovery_event.work_item_id = a.work_item_id
|
|
2022
|
+
AND recovery_event.action_id = a.id
|
|
2023
|
+
AND recovery_event.action_generation = a.generation
|
|
2024
|
+
AND recovery_event.type = 'coordinator.recovery_started'
|
|
2025
|
+
WHERE a.status = 'failed' AND w.status NOT IN ('done', 'cancelled')
|
|
2026
|
+
GROUP BY a.id
|
|
2027
|
+
ORDER BY a.updated_at, a.sequence, a.id`).all().map(row => ({
|
|
2028
|
+
workItemId: row.work_item_id,
|
|
2029
|
+
actionId: row.action_id,
|
|
2030
|
+
actionGeneration: Math.max(1, Number(row.generation) || 1),
|
|
2031
|
+
recoveryAttempts: Math.max(0, Number(row.recovery_attempts) || 0),
|
|
2032
|
+
lastRecoveryAt: Math.max(0, Number(row.last_recovery_at) || 0),
|
|
2033
|
+
}));
|
|
2034
|
+
}
|
|
2035
|
+
|
|
1994
2036
|
listWorkItems(filters = {}) {
|
|
1995
2037
|
const where = [];
|
|
1996
2038
|
const values = [];
|
|
@@ -2133,7 +2175,7 @@ export class WorkItemStore {
|
|
|
2133
2175
|
return this.db.prepare(`SELECT * FROM events WHERE action_id = ? ORDER BY id`).all(actionId).map(mapEvent);
|
|
2134
2176
|
}
|
|
2135
2177
|
|
|
2136
|
-
beginCoordinatorTurn(id, text, expected = {},
|
|
2178
|
+
beginCoordinatorTurn(id, text, expected = {}, options = {}) {
|
|
2137
2179
|
return withTransaction(this.db, () => {
|
|
2138
2180
|
const workItem = this.getWorkItem(id);
|
|
2139
2181
|
if (!workItem) return null;
|
|
@@ -2151,32 +2193,56 @@ export class WorkItemStore {
|
|
|
2151
2193
|
throw new Error('WorkItem Coordinator is already responding');
|
|
2152
2194
|
}
|
|
2153
2195
|
const now = this.now();
|
|
2154
|
-
const projectedAttachments = (Array.isArray(addedAttachments) ? addedAttachments : [])
|
|
2155
|
-
|
|
2156
|
-
|
|
2157
|
-
|
|
2158
|
-
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
throw new Error('WorkItem Coordinator message or attachments are required');
|
|
2163
|
-
}
|
|
2196
|
+
const projectedAttachments = (Array.isArray(options.addedAttachments) ? options.addedAttachments : [])
|
|
2197
|
+
.map(attachment => ({
|
|
2198
|
+
id: attachment.id,
|
|
2199
|
+
name: attachment.name,
|
|
2200
|
+
mimeType: attachment.mimeType,
|
|
2201
|
+
size: Math.max(0, Number(attachment.size) || 0),
|
|
2202
|
+
isImage: attachment.isImage === true,
|
|
2203
|
+
}));
|
|
2164
2204
|
const activeActions = this.db.prepare(`SELECT * FROM actions WHERE work_item_id = ?
|
|
2165
2205
|
AND status NOT IN ('completed', 'superseded', 'cancelled') ORDER BY sequence`).all(id).map(mapAction);
|
|
2166
2206
|
this.#assertNoIntegrationReservation(activeActions, now);
|
|
2207
|
+
let recovery = options.recovery && typeof options.recovery === 'object'
|
|
2208
|
+
? { ...options.recovery } : null;
|
|
2209
|
+
if (recovery) {
|
|
2210
|
+
const failedAction = activeActions.find(action => action.id === recovery.actionId);
|
|
2211
|
+
if (['done', 'cancelled'].includes(workItem.status)
|
|
2212
|
+
|| failedAction?.status !== 'failed'
|
|
2213
|
+
|| failedAction.generation !== recovery.actionGeneration
|
|
2214
|
+
|| failedAction.stageId !== recovery.stageId) {
|
|
2215
|
+
throw new Error('WorkItem failure changed before Coordinator recovery started');
|
|
2216
|
+
}
|
|
2217
|
+
const priorAttempts = Number(this.db.prepare(`SELECT COUNT(*) AS count FROM events
|
|
2218
|
+
WHERE work_item_id = ? AND type = 'coordinator.recovery_started'
|
|
2219
|
+
AND action_id = ? AND action_generation = ?`).get(
|
|
2220
|
+
id,
|
|
2221
|
+
failedAction.id,
|
|
2222
|
+
failedAction.generation,
|
|
2223
|
+
)?.count) || 0;
|
|
2224
|
+
recovery = { ...recovery, attempt: priorAttempts + 1 };
|
|
2225
|
+
}
|
|
2226
|
+
if (!recovery && !String(text || '').trim() && projectedAttachments.length === 0) {
|
|
2227
|
+
throw new Error('WorkItem Coordinator message or attachments are required');
|
|
2228
|
+
}
|
|
2167
2229
|
const turnId = randomUUID();
|
|
2168
|
-
const userMessage = {
|
|
2230
|
+
const userMessage = recovery ? null : {
|
|
2169
2231
|
id: randomUUID(), turnId, role: 'user', text, attachments: projectedAttachments,
|
|
2170
2232
|
status: 'completed', createdAt: now,
|
|
2171
2233
|
};
|
|
2172
2234
|
const assistantMessage = {
|
|
2173
2235
|
id: randomUUID(), turnId, role: 'assistant', text: '', status: 'thinking',
|
|
2174
2236
|
createdAt: now, updatedAt: now, decision: null,
|
|
2237
|
+
...(recovery ? { recovery: { ...recovery } } : {}),
|
|
2175
2238
|
};
|
|
2176
|
-
const messages = [...(workItem.messages || []), userMessage, assistantMessage]
|
|
2239
|
+
const messages = [...(workItem.messages || []), ...(userMessage ? [userMessage] : []), assistantMessage]
|
|
2240
|
+
.slice(-100);
|
|
2177
2241
|
const coordinatorRevision = workItem.coordinatorRevision + 1;
|
|
2178
2242
|
const revision = workItem.revision + (projectedAttachments.length > 0 ? 1 : 0);
|
|
2179
|
-
const nextAttachments = Array.isArray(attachments)
|
|
2243
|
+
const nextAttachments = Array.isArray(options.attachments)
|
|
2244
|
+
? options.attachments
|
|
2245
|
+
: workItem.attachments;
|
|
2180
2246
|
const changed = this.db.prepare(`UPDATE work_items SET messages = ?, attachments = ?, revision = ?, coordinator_revision = ?, updated_at = ?
|
|
2181
2247
|
WHERE id = ? AND coordinator_revision = ? AND revision = ? AND plan_revision = ?
|
|
2182
2248
|
AND ledger_revision = ?`).run(
|
|
@@ -2184,9 +2250,15 @@ export class WorkItemStore {
|
|
|
2184
2250
|
id, workItem.coordinatorRevision, workItem.revision, workItem.planRevision, workItem.ledgerRevision,
|
|
2185
2251
|
);
|
|
2186
2252
|
if (Number(changed.changes) !== 1) throw new Error('Coordinator turn lost its revision fence');
|
|
2187
|
-
this.appendEvent(id, 'coordinator.turn_started', {
|
|
2188
|
-
turnId,
|
|
2189
|
-
|
|
2253
|
+
this.appendEvent(id, recovery ? 'coordinator.recovery_started' : 'coordinator.turn_started', {
|
|
2254
|
+
turnId,
|
|
2255
|
+
status: 'thinking',
|
|
2256
|
+
coordinatorRevision,
|
|
2257
|
+
addedAttachmentCount: projectedAttachments.length,
|
|
2258
|
+
}, recovery ? {
|
|
2259
|
+
actionId: recovery.actionId,
|
|
2260
|
+
actionGeneration: recovery.actionGeneration,
|
|
2261
|
+
} : {});
|
|
2190
2262
|
const detail = this.getWorkItemDetail(id);
|
|
2191
2263
|
return {
|
|
2192
2264
|
turnId,
|
|
@@ -2199,6 +2271,7 @@ export class WorkItemStore {
|
|
|
2199
2271
|
coordinatorRevision,
|
|
2200
2272
|
status: workItem.status,
|
|
2201
2273
|
actionFence: coordinatorActionFence(activeActions),
|
|
2274
|
+
recovery: recovery ? { ...recovery } : null,
|
|
2202
2275
|
},
|
|
2203
2276
|
};
|
|
2204
2277
|
});
|
|
@@ -2220,6 +2293,11 @@ export class WorkItemStore {
|
|
|
2220
2293
|
message?.turnId === turnId && message.role === 'assistant' && message.status === 'thinking'
|
|
2221
2294
|
));
|
|
2222
2295
|
if (assistantIndex < 0) return null;
|
|
2296
|
+
const persistedRecovery = messages[assistantIndex]?.recovery ?? null;
|
|
2297
|
+
if (!sameCoordinatorRecoveryIdentity(persistedRecovery, expected.recovery ?? null)) {
|
|
2298
|
+
throw new Error('Coordinator recovery fence does not match the persisted turn identity');
|
|
2299
|
+
}
|
|
2300
|
+
const recovery = coordinatorRecoveryIdentity(persistedRecovery);
|
|
2223
2301
|
const decision = result?.decision || {};
|
|
2224
2302
|
const now = this.now();
|
|
2225
2303
|
const activeActions = this.db.prepare(`SELECT * FROM actions WHERE work_item_id = ?
|
|
@@ -2230,13 +2308,63 @@ export class WorkItemStore {
|
|
|
2230
2308
|
this.#assertNoIntegrationReservation(activeActions, now);
|
|
2231
2309
|
|
|
2232
2310
|
const graphMode = isGraphWorkItem(workItem);
|
|
2233
|
-
if (decision.kind
|
|
2311
|
+
if (decision.kind === 'replan'
|
|
2234
2312
|
&& (!graphMode || workItem.workflowSnapshot?.planningMode !== 'ai')) {
|
|
2235
|
-
throw new Error('Coordinator
|
|
2313
|
+
throw new Error('Coordinator replan requires an AI-planned Action graph');
|
|
2314
|
+
}
|
|
2315
|
+
if (recovery && decision.kind === 'guide_actions') {
|
|
2316
|
+
const failedAction = activeActions.find(action => (
|
|
2317
|
+
action.id === recovery.actionId
|
|
2318
|
+
&& action.generation === recovery.actionGeneration
|
|
2319
|
+
&& action.stageId === recovery.stageId
|
|
2320
|
+
&& action.status === 'failed'
|
|
2321
|
+
));
|
|
2322
|
+
if (!failedAction
|
|
2323
|
+
|| !Array.isArray(decision.guidance)
|
|
2324
|
+
|| decision.guidance.length !== 1
|
|
2325
|
+
|| decision.guidance[0]?.stageId !== failedAction.stageId) {
|
|
2326
|
+
throw new Error('Coordinator recovery guidance must target only the failed Action identity');
|
|
2327
|
+
}
|
|
2236
2328
|
}
|
|
2237
2329
|
let nextWorkItem = workItem;
|
|
2238
2330
|
let affectedActionIds = [];
|
|
2239
|
-
if (decision.kind === '
|
|
2331
|
+
if (decision.kind === 'request_human') {
|
|
2332
|
+
const action = recovery ? activeActions.find(candidate => (
|
|
2333
|
+
candidate.id === recovery.actionId
|
|
2334
|
+
&& candidate.generation === recovery.actionGeneration
|
|
2335
|
+
)) : null;
|
|
2336
|
+
if (!action || action.status !== 'failed') {
|
|
2337
|
+
throw new Error('Coordinator human request lost the failed Action fence');
|
|
2338
|
+
}
|
|
2339
|
+
const question = typeof decision.question === 'string' ? decision.question.trim().slice(0, 8_000) : '';
|
|
2340
|
+
if (!question) throw new Error('Coordinator human request requires a question');
|
|
2341
|
+
const changedAction = this.db.prepare(`UPDATE actions SET status = 'waiting', updated_at = ?
|
|
2342
|
+
WHERE id = ? AND status = 'failed' AND generation = ? AND current_run_id IS NULL`).run(
|
|
2343
|
+
now, action.id, action.generation,
|
|
2344
|
+
);
|
|
2345
|
+
if (Number(changedAction.changes) !== 1) {
|
|
2346
|
+
throw new Error('Coordinator human request lost the failed Action generation fence');
|
|
2347
|
+
}
|
|
2348
|
+
const resultRunId = action.resultRunId
|
|
2349
|
+
|| this.db.prepare(`SELECT id FROM runs WHERE action_id = ? AND status = 'failed'
|
|
2350
|
+
ORDER BY ended_at DESC, started_at DESC LIMIT 1`).get(action.id)?.id;
|
|
2351
|
+
if (!resultRunId) throw new Error('Coordinator human request requires a failed Run');
|
|
2352
|
+
const changedRun = this.db.prepare(`UPDATE runs SET waiting_reason = ?
|
|
2353
|
+
WHERE id = ? AND action_id = ? AND status = 'failed'`).run(question, resultRunId, action.id);
|
|
2354
|
+
if (Number(changedRun.changes) !== 1) {
|
|
2355
|
+
throw new Error('Coordinator human request lost the failed Run fence');
|
|
2356
|
+
}
|
|
2357
|
+
affectedActionIds = [action.id];
|
|
2358
|
+
this.appendEvent(workItem.id, 'action.waiting', {
|
|
2359
|
+
reason: question,
|
|
2360
|
+
source: 'coordinator',
|
|
2361
|
+
turnId,
|
|
2362
|
+
}, {
|
|
2363
|
+
actionId: action.id,
|
|
2364
|
+
runId: resultRunId,
|
|
2365
|
+
actionGeneration: action.generation,
|
|
2366
|
+
});
|
|
2367
|
+
} else if (decision.kind === 'guide_actions') {
|
|
2240
2368
|
const guidanceByStage = new Map(decision.guidance.map(entry => [entry.stageId, entry.instruction]));
|
|
2241
2369
|
for (const action of activeActions) {
|
|
2242
2370
|
const instruction = guidanceByStage.get(action.stageId);
|
|
@@ -2344,7 +2472,7 @@ export class WorkItemStore {
|
|
|
2344
2472
|
);
|
|
2345
2473
|
}
|
|
2346
2474
|
|
|
2347
|
-
const graphState = decision.kind
|
|
2475
|
+
const graphState = ['answer'].includes(decision.kind) ? null : this.#graphWorkItemState(workItem.id);
|
|
2348
2476
|
messages[assistantIndex] = {
|
|
2349
2477
|
...messages[assistantIndex], text: result.reply, status: 'completed', updatedAt: now,
|
|
2350
2478
|
decision: {
|
|
@@ -2724,6 +2852,16 @@ export class WorkItemStore {
|
|
|
2724
2852
|
const row = this.db.prepare(`SELECT a.* FROM actions a
|
|
2725
2853
|
JOIN work_items w ON w.id = a.work_item_id
|
|
2726
2854
|
WHERE a.status = 'ready' AND a.current_run_id IS NULL
|
|
2855
|
+
AND NOT EXISTS (
|
|
2856
|
+
SELECT 1 FROM actions failed_recovery
|
|
2857
|
+
WHERE failed_recovery.work_item_id = a.work_item_id
|
|
2858
|
+
AND failed_recovery.status = 'failed'
|
|
2859
|
+
)
|
|
2860
|
+
AND NOT (
|
|
2861
|
+
json_extract(w.messages, '$[#-1].role') = 'assistant'
|
|
2862
|
+
AND json_extract(w.messages, '$[#-1].status') = 'thinking'
|
|
2863
|
+
AND json_type(w.messages, '$[#-1].recovery') IS NOT NULL
|
|
2864
|
+
)
|
|
2727
2865
|
AND (
|
|
2728
2866
|
(COALESCE(json_extract(w.workflow_snapshot, '$.executionMode'), 'linear') != 'graph'
|
|
2729
2867
|
AND w.status = 'ready' AND w.current_action_id = a.id AND w.current_run_id IS NULL)
|