@yeaft/webchat-agent 1.0.175 → 1.0.177
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/package.json +1 -1
- package/yeaft/dream/state.js +14 -8
- package/yeaft/engine.js +13 -8
- package/yeaft/memory/ams.js +57 -25
- package/yeaft/memory/prompt-cleanup.js +87 -0
- package/yeaft/memory/segment-store.js +2 -1
- package/yeaft/work-center/controller.js +10 -2
- package/yeaft/work-center/store.js +57 -22
package/package.json
CHANGED
package/yeaft/dream/state.js
CHANGED
|
@@ -248,20 +248,26 @@ export async function readScopeDreamMarker(memoryMdAbsPath) {
|
|
|
248
248
|
*/
|
|
249
249
|
export function withDreamMarker(memoryMd, fields) {
|
|
250
250
|
const block = renderDreamBlock(fields);
|
|
251
|
-
const body =
|
|
252
|
-
if (body.includes(DREAM_BLOCK_OPEN) && body.includes(DREAM_BLOCK_CLOSE)) {
|
|
253
|
-
// Replace existing block.
|
|
254
|
-
return body.replace(
|
|
255
|
-
new RegExp(`${escapeRe(DREAM_BLOCK_OPEN)}[\\s\\S]*?${escapeRe(DREAM_BLOCK_CLOSE)}`),
|
|
256
|
-
block,
|
|
257
|
-
);
|
|
258
|
-
}
|
|
251
|
+
const body = stripDreamMarker(memoryMd);
|
|
259
252
|
// Append. Ensure exactly one newline before the block.
|
|
260
253
|
const trimmed = body.replace(/\s+$/, '');
|
|
261
254
|
const sep = trimmed.length === 0 ? '' : '\n\n';
|
|
262
255
|
return `${trimmed}${sep}${block}\n`;
|
|
263
256
|
}
|
|
264
257
|
|
|
258
|
+
/**
|
|
259
|
+
* Remove all per-scope dream-state blocks from a markdown blob.
|
|
260
|
+
*
|
|
261
|
+
* @param {string} body
|
|
262
|
+
* @returns {string}
|
|
263
|
+
*/
|
|
264
|
+
export function stripDreamMarker(body) {
|
|
265
|
+
return String(body || '').replace(
|
|
266
|
+
new RegExp(`${escapeRe(DREAM_BLOCK_OPEN)}[\\s\\S]*?${escapeRe(DREAM_BLOCK_CLOSE)}`, 'g'),
|
|
267
|
+
'',
|
|
268
|
+
).trim();
|
|
269
|
+
}
|
|
270
|
+
|
|
265
271
|
/**
|
|
266
272
|
* Extract the contents of the dream-state block (between the two HTML
|
|
267
273
|
* comments). Returns null if the block isn't present.
|
package/yeaft/engine.js
CHANGED
|
@@ -32,6 +32,7 @@ import { archiveTurn } from './archive/turn-archive.js';
|
|
|
32
32
|
import { archiveToolResults } from './archive/tool-results.js';
|
|
33
33
|
import { readSummary as readScopeSummary } from './memory/store.js';
|
|
34
34
|
import { runAdjust } from './memory/adjust.js';
|
|
35
|
+
import { cleanMemoryPromptText } from './memory/prompt-cleanup.js';
|
|
35
36
|
import { isVpSeedBackfillStub } from './memory/seed-backfill.js';
|
|
36
37
|
import { runStopHooks } from './stop-hooks.js';
|
|
37
38
|
import { perfNowMs, recordAgentPerfTrace } from './perf-trace.js';
|
|
@@ -389,13 +390,17 @@ export function shouldAllowGroupReflection({
|
|
|
389
390
|
export function buildResidentEntries(args) {
|
|
390
391
|
const summaries = (args && args.summaries) || {};
|
|
391
392
|
const out = [];
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
393
|
+
const userSummary = cleanMemoryPromptText(summaries.user);
|
|
394
|
+
const sessionSummary = cleanMemoryPromptText(summaries.session);
|
|
395
|
+
const vpSummary = cleanMemoryPromptText(summaries.vp);
|
|
396
|
+
if (userSummary) out.push({ scope: 'user', summary: userSummary });
|
|
397
|
+
if (args.sessionId && sessionSummary) {
|
|
398
|
+
out.push({ scope: `sessions/${args.sessionId}`, summary: sessionSummary });
|
|
395
399
|
}
|
|
396
400
|
if (args.sessionId && Array.isArray(summaries.topics)) {
|
|
397
401
|
for (const topic of summaries.topics) {
|
|
398
|
-
|
|
402
|
+
const summary = cleanMemoryPromptText(topic?.summary);
|
|
403
|
+
if (topic?.scope && summary) out.push({ scope: topic.scope, summary });
|
|
399
404
|
}
|
|
400
405
|
}
|
|
401
406
|
// VP per-session isolation (2026-06-09): the VP summary scope MUST be
|
|
@@ -407,8 +412,8 @@ export function buildResidentEntries(args) {
|
|
|
407
412
|
// by id rather than by full scope path. The session-qualified form
|
|
408
413
|
// makes the per-session boundary explicit and matches the on-disk
|
|
409
414
|
// layout 1:1.
|
|
410
|
-
if (args.sessionId && args.ownVpId &&
|
|
411
|
-
out.push({ scope: `sessions/${args.sessionId}/vp/${args.ownVpId}`, summary:
|
|
415
|
+
if (args.sessionId && args.ownVpId && vpSummary && !isVpSeedBackfillStub(vpSummary)) {
|
|
416
|
+
out.push({ scope: `sessions/${args.sessionId}/vp/${args.ownVpId}`, summary: vpSummary });
|
|
412
417
|
}
|
|
413
418
|
return out;
|
|
414
419
|
}
|
|
@@ -1940,8 +1945,8 @@ export class Engine {
|
|
|
1940
1945
|
))
|
|
1941
1946
|
.map(e => ({
|
|
1942
1947
|
scope: e.scope,
|
|
1943
|
-
summary: String(e.summary)
|
|
1944
|
-
truncated:
|
|
1948
|
+
summary: String(e.summary),
|
|
1949
|
+
truncated: false,
|
|
1945
1950
|
source: 'resident-summary',
|
|
1946
1951
|
}))
|
|
1947
1952
|
: [];
|
package/yeaft/memory/ams.js
CHANGED
|
@@ -16,7 +16,8 @@
|
|
|
16
16
|
* vpId at construction.
|
|
17
17
|
*/
|
|
18
18
|
|
|
19
|
-
import { approxTokens
|
|
19
|
+
import { approxTokens } from './budget.js';
|
|
20
|
+
import { cleanMemoryPromptText, isDuplicateMemoryText, rememberMemoryText } from './prompt-cleanup.js';
|
|
20
21
|
import { isVpForeign } from './store.js';
|
|
21
22
|
|
|
22
23
|
const RECENT_DEFAULT_CAPACITY = 64;
|
|
@@ -69,8 +70,9 @@ export class ActiveMemorySet {
|
|
|
69
70
|
this._resident.clear();
|
|
70
71
|
for (const e of entries) {
|
|
71
72
|
if (this._isForeignVp(e.scope)) continue;
|
|
72
|
-
|
|
73
|
-
|
|
73
|
+
const summary = cleanMemoryPromptText(e.summary);
|
|
74
|
+
if (!summary) continue;
|
|
75
|
+
this._resident.set(e.scope, summary);
|
|
74
76
|
}
|
|
75
77
|
}
|
|
76
78
|
|
|
@@ -85,8 +87,10 @@ export class ActiveMemorySet {
|
|
|
85
87
|
touchRecent(seg) {
|
|
86
88
|
if (!seg || !seg.id) return;
|
|
87
89
|
if (this._isForeignVp(seg.scope)) return;
|
|
90
|
+
const body = cleanMemoryPromptText(seg.body);
|
|
91
|
+
if (!body) return;
|
|
88
92
|
if (this._recent.has(seg.id)) this._recent.delete(seg.id);
|
|
89
|
-
this._recent.set(seg.id, { seg, ts: Date.now() });
|
|
93
|
+
this._recent.set(seg.id, { seg: { ...seg, body }, ts: Date.now() });
|
|
90
94
|
while (this._recent.size > this.recentCapacity) {
|
|
91
95
|
const firstKey = this._recent.keys().next().value;
|
|
92
96
|
this._recent.delete(firstKey);
|
|
@@ -104,7 +108,9 @@ export class ActiveMemorySet {
|
|
|
104
108
|
this._onDemand.clear();
|
|
105
109
|
for (const seg of segments) {
|
|
106
110
|
if (this._isForeignVp(seg.scope)) continue;
|
|
107
|
-
|
|
111
|
+
const body = cleanMemoryPromptText(seg.body);
|
|
112
|
+
if (!body) continue;
|
|
113
|
+
this._onDemand.set(seg.id, { ...seg, body });
|
|
108
114
|
}
|
|
109
115
|
}
|
|
110
116
|
|
|
@@ -116,7 +122,9 @@ export class ActiveMemorySet {
|
|
|
116
122
|
addOnDemand(segments) {
|
|
117
123
|
for (const seg of segments) {
|
|
118
124
|
if (this._isForeignVp(seg.scope)) continue;
|
|
119
|
-
|
|
125
|
+
const body = cleanMemoryPromptText(seg.body);
|
|
126
|
+
if (!body) continue;
|
|
127
|
+
this._onDemand.set(seg.id, { ...seg, body });
|
|
120
128
|
}
|
|
121
129
|
}
|
|
122
130
|
|
|
@@ -139,31 +147,40 @@ export class ActiveMemorySet {
|
|
|
139
147
|
* @returns {AmsSnapshot}
|
|
140
148
|
*/
|
|
141
149
|
snapshot() {
|
|
150
|
+
const seenPromptText = new Set();
|
|
151
|
+
|
|
142
152
|
// Resident: pack scopes by priority order (caller provides via insert
|
|
143
153
|
// order — current group's own vp first, then user, etc.).
|
|
144
|
-
const
|
|
145
|
-
scope, summary
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
154
|
+
const { picked: resPicked, cost: resCost } = pickMemoryItems({
|
|
155
|
+
items: [...this._resident.entries()].map(([scope, summary]) => ({
|
|
156
|
+
scope, summary: cleanMemoryPromptText(summary),
|
|
157
|
+
})),
|
|
158
|
+
budget: this.budget.resident,
|
|
159
|
+
seen: seenPromptText,
|
|
160
|
+
textOf: e => e.summary,
|
|
161
|
+
costOf: e => approxTokens(e.summary),
|
|
162
|
+
});
|
|
151
163
|
|
|
152
164
|
// Recent: insertion order is oldest-first; we want newest first.
|
|
153
|
-
const
|
|
154
|
-
.
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
165
|
+
const { picked: recPicked, cost: recCost } = pickMemoryItems({
|
|
166
|
+
items: [...this._recent.values()]
|
|
167
|
+
.reverse()
|
|
168
|
+
.map(e => ({ ...e.seg, body: cleanMemoryPromptText(e.seg?.body) })),
|
|
169
|
+
budget: this.budget.recent,
|
|
170
|
+
seen: seenPromptText,
|
|
171
|
+
textOf: seg => seg.body,
|
|
172
|
+
costOf: seg => approxTokens(seg.body),
|
|
173
|
+
});
|
|
160
174
|
|
|
161
175
|
// OnDemand: insertion order from caller (already FTS-ranked).
|
|
162
|
-
const
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
176
|
+
const { picked: odPicked, cost: odCost } = pickMemoryItems({
|
|
177
|
+
items: [...this._onDemand.values()]
|
|
178
|
+
.map(seg => ({ ...seg, body: cleanMemoryPromptText(seg?.body) })),
|
|
179
|
+
budget: this.budget.onDemand,
|
|
180
|
+
seen: seenPromptText,
|
|
181
|
+
textOf: seg => seg.body,
|
|
182
|
+
costOf: seg => approxTokens(seg.body),
|
|
183
|
+
});
|
|
167
184
|
|
|
168
185
|
return {
|
|
169
186
|
resident: resPicked,
|
|
@@ -193,3 +210,18 @@ export class ActiveMemorySet {
|
|
|
193
210
|
return isVpForeign(scope, this.ownVpId);
|
|
194
211
|
}
|
|
195
212
|
}
|
|
213
|
+
|
|
214
|
+
function pickMemoryItems({ items, budget, seen, textOf, costOf }) {
|
|
215
|
+
const picked = [];
|
|
216
|
+
let cost = 0;
|
|
217
|
+
for (const item of items) {
|
|
218
|
+
const text = textOf(item);
|
|
219
|
+
if (!text || isDuplicateMemoryText(text, seen)) continue;
|
|
220
|
+
const itemCost = costOf(item);
|
|
221
|
+
if (cost + itemCost > budget) continue;
|
|
222
|
+
picked.push(item);
|
|
223
|
+
cost += itemCost;
|
|
224
|
+
rememberMemoryText(text, seen);
|
|
225
|
+
}
|
|
226
|
+
return { picked, cost };
|
|
227
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* memory/prompt-cleanup.js — prompt-facing memory text hygiene.
|
|
3
|
+
*
|
|
4
|
+
* Disk memory can carry operational metadata such as the per-scope Dream
|
|
5
|
+
* marker. That metadata is useful for schedulers, not for the model. Keep the
|
|
6
|
+
* storage schema compatible and clean only at read / prompt boundaries.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const DREAM_STATE_BLOCK_RE = /<!--\s*dream-state\s*-->[\s\S]*?<!--\s*\/dream-state\s*-->/gi;
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Remove Dream scheduler metadata blocks from memory text.
|
|
13
|
+
*
|
|
14
|
+
* @param {string} text
|
|
15
|
+
* @returns {string}
|
|
16
|
+
*/
|
|
17
|
+
export function stripDreamStateBlocks(text) {
|
|
18
|
+
return String(text || '').replace(DREAM_STATE_BLOCK_RE, '').trim();
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Clean text before it is eligible for prompt injection.
|
|
23
|
+
*
|
|
24
|
+
* @param {string} text
|
|
25
|
+
* @returns {string}
|
|
26
|
+
*/
|
|
27
|
+
export function cleanMemoryPromptText(text) {
|
|
28
|
+
return stripDreamStateBlocks(text)
|
|
29
|
+
.replace(/[ \t]+\n/g, '\n')
|
|
30
|
+
.replace(/\n{3,}/g, '\n\n')
|
|
31
|
+
.trim();
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Normalized key for conservative prompt dedupe. This is intentionally simple:
|
|
36
|
+
* exact semantic clustering belongs in Dream; prompt assembly only removes
|
|
37
|
+
* obvious repeats and near-contained copies.
|
|
38
|
+
*
|
|
39
|
+
* @param {string} text
|
|
40
|
+
* @returns {string}
|
|
41
|
+
*/
|
|
42
|
+
export function memoryDedupeKey(text) {
|
|
43
|
+
return cleanMemoryPromptText(text)
|
|
44
|
+
.toLowerCase()
|
|
45
|
+
.replace(/[`*_>#\-\[\]().,,。::;;!!??"'“”‘’]/g, ' ')
|
|
46
|
+
.replace(/\s+/g, ' ')
|
|
47
|
+
.trim();
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Whether a candidate is redundant with text already emitted to the prompt.
|
|
52
|
+
* Does not mutate `seen`; call `rememberMemoryText` only after the candidate
|
|
53
|
+
* is actually selected into the snapshot.
|
|
54
|
+
*
|
|
55
|
+
* @param {string} candidate
|
|
56
|
+
* @param {Set<string>} seen
|
|
57
|
+
* @returns {boolean}
|
|
58
|
+
*/
|
|
59
|
+
export function isDuplicateMemoryText(candidate, seen) {
|
|
60
|
+
const key = memoryDedupeKey(candidate);
|
|
61
|
+
if (!key) return true;
|
|
62
|
+
if (seen.has(key)) return true;
|
|
63
|
+
|
|
64
|
+
// Avoid dropping tiny generic snippets via containment. A candidate is
|
|
65
|
+
// redundant only when already-selected text covers it. If the candidate is
|
|
66
|
+
// longer and contains the selected text, keep it: actual memory often carries
|
|
67
|
+
// details that a resident summary compressed away.
|
|
68
|
+
if (key.length >= 80) {
|
|
69
|
+
for (const existing of seen) {
|
|
70
|
+
if (existing.length >= 80 && existing.includes(key)) {
|
|
71
|
+
return true;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Mark text as emitted to the prompt for later dedupe checks.
|
|
80
|
+
*
|
|
81
|
+
* @param {string} text
|
|
82
|
+
* @param {Set<string>} seen
|
|
83
|
+
*/
|
|
84
|
+
export function rememberMemoryText(text, seen) {
|
|
85
|
+
const key = memoryDedupeKey(text);
|
|
86
|
+
if (key) seen.add(key);
|
|
87
|
+
}
|
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
readdirSync, statSync, renameSync,
|
|
20
20
|
} from 'node:fs';
|
|
21
21
|
import { join, dirname, relative, sep } from 'node:path';
|
|
22
|
+
import { stripDreamStateBlocks } from './prompt-cleanup.js';
|
|
22
23
|
import { parseSegments, serializeSegments } from './segment.js';
|
|
23
24
|
|
|
24
25
|
/**
|
|
@@ -32,7 +33,7 @@ export function readScope(memoryRoot, scope) {
|
|
|
32
33
|
const path = scopeFilePath(memoryRoot, scope);
|
|
33
34
|
if (!existsSync(path)) return [];
|
|
34
35
|
const text = readFileSync(path, 'utf8');
|
|
35
|
-
return parseSegments(text, { defaultScope: scope });
|
|
36
|
+
return parseSegments(stripDreamStateBlocks(text), { defaultScope: scope });
|
|
36
37
|
}
|
|
37
38
|
|
|
38
39
|
/**
|
|
@@ -202,6 +202,9 @@ export class WorkflowController {
|
|
|
202
202
|
assignmentPolicy: previous.assignmentPolicy,
|
|
203
203
|
modelPolicy: previous.modelPolicy,
|
|
204
204
|
requiredRole: previous.requiredRole,
|
|
205
|
+
dependsOnStageIds: previous.dependsOnStageIds,
|
|
206
|
+
workspaceMode: previous.workspaceMode,
|
|
207
|
+
changesRequestedStageId: previous.changesRequestedStageId,
|
|
205
208
|
brief: previous.brief,
|
|
206
209
|
};
|
|
207
210
|
return {
|
|
@@ -237,7 +240,12 @@ export class WorkflowController {
|
|
|
237
240
|
if (!['waiting', 'needs_attention'].includes(workItem.status)) {
|
|
238
241
|
throw new Error(`WorkItem in ${workItem.status} cannot accept Action input`);
|
|
239
242
|
}
|
|
240
|
-
|
|
243
|
+
const targetAction = this.store.getAction(input.actionId);
|
|
244
|
+
const graphMode = workItem.workflowSnapshot?.executionMode === 'graph';
|
|
245
|
+
const targetMatches = graphMode
|
|
246
|
+
? targetAction?.workItemId === id && ['waiting', 'failed'].includes(targetAction.status)
|
|
247
|
+
: workItem.currentActionId === input.actionId;
|
|
248
|
+
if (!targetMatches || workItem.revision !== input.revision) {
|
|
241
249
|
throw new Error('Action changed before input was applied; refresh and try again');
|
|
242
250
|
}
|
|
243
251
|
return this.retry(id, {
|
|
@@ -337,7 +345,7 @@ export class WorkflowController {
|
|
|
337
345
|
({ action, workItem }) => {
|
|
338
346
|
if (result.outcome === 'waiting') {
|
|
339
347
|
return {
|
|
340
|
-
actionStatus: '
|
|
348
|
+
actionStatus: 'waiting',
|
|
341
349
|
workItemStatus: 'waiting',
|
|
342
350
|
keepCurrentAction: true,
|
|
343
351
|
eventType: 'action.waiting',
|
|
@@ -770,21 +770,35 @@ export class WorkItemStore {
|
|
|
770
770
|
throw new Error('WorkItem has no active Action for guidance');
|
|
771
771
|
}
|
|
772
772
|
const now = this.now();
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
'superseded',
|
|
776
|
-
'superseded',
|
|
777
|
-
'Action restarted after user guidance',
|
|
778
|
-
now,
|
|
779
|
-
);
|
|
780
|
-
const action = this.#insertAction(id, {
|
|
773
|
+
const revision = workItem.revision + 1;
|
|
774
|
+
const replacement = {
|
|
781
775
|
...makeAction(workItem, previous),
|
|
782
|
-
contractRevision:
|
|
783
|
-
}
|
|
776
|
+
contractRevision: previous.contractRevision,
|
|
777
|
+
};
|
|
778
|
+
let action;
|
|
779
|
+
if (workItem.workflowSnapshot?.executionMode === 'graph') {
|
|
780
|
+
action = this.#resetGraphFromStage(
|
|
781
|
+
id,
|
|
782
|
+
previous.stageId,
|
|
783
|
+
replacement,
|
|
784
|
+
'Action restarted after user guidance',
|
|
785
|
+
now,
|
|
786
|
+
);
|
|
787
|
+
} else {
|
|
788
|
+
this.#invalidateExecution(
|
|
789
|
+
workItem,
|
|
790
|
+
'superseded',
|
|
791
|
+
'superseded',
|
|
792
|
+
'Action restarted after user guidance',
|
|
793
|
+
now,
|
|
794
|
+
);
|
|
795
|
+
action = this.#insertAction(id, replacement, this.#nextSequence(id), now);
|
|
796
|
+
}
|
|
784
797
|
this.db.prepare(`UPDATE work_items SET status = 'ready', current_action_id = ?,
|
|
785
|
-
current_run_id = NULL, attachments = ?, updated_at = ? WHERE id = ?`).run(
|
|
798
|
+
current_run_id = NULL, attachments = ?, revision = ?, updated_at = ? WHERE id = ?`).run(
|
|
786
799
|
action.id,
|
|
787
800
|
stringify(Array.isArray(attachments) ? attachments : workItem.attachments),
|
|
801
|
+
revision,
|
|
788
802
|
now,
|
|
789
803
|
id,
|
|
790
804
|
);
|
|
@@ -922,19 +936,29 @@ export class WorkItemStore {
|
|
|
922
936
|
if (!['waiting', 'needs_attention'].includes(workItem.status)) {
|
|
923
937
|
throw new Error(`WorkItem in ${workItem.status} does not need retry`);
|
|
924
938
|
}
|
|
939
|
+
const graphMode = workItem.workflowSnapshot?.executionMode === 'graph';
|
|
940
|
+
let previous = workItem.currentActionId ? this.getAction(workItem.currentActionId) : null;
|
|
925
941
|
if (options.expected) {
|
|
926
|
-
|
|
942
|
+
const expectedAction = this.getAction(options.expected.actionId);
|
|
943
|
+
const expectedMatches = graphMode
|
|
944
|
+
? expectedAction?.workItemId === id && ['waiting', 'failed'].includes(expectedAction.status)
|
|
945
|
+
: workItem.currentActionId === options.expected.actionId;
|
|
946
|
+
if (!expectedMatches || workItem.revision !== options.expected.revision) {
|
|
927
947
|
throw new Error('Action changed before input was applied; refresh and try again');
|
|
928
948
|
}
|
|
949
|
+
previous = expectedAction;
|
|
929
950
|
}
|
|
930
|
-
const previous = workItem.currentActionId ? this.getAction(workItem.currentActionId) : null;
|
|
931
951
|
const previousRun = previous
|
|
932
952
|
? mapRun(this.db.prepare(`SELECT * FROM runs WHERE work_item_id = ? AND action_id = ?
|
|
933
953
|
AND status != 'running' ORDER BY ended_at DESC, started_at DESC LIMIT 1`).get(id, previous.id))
|
|
934
954
|
: null;
|
|
935
955
|
const now = this.now();
|
|
936
|
-
const
|
|
937
|
-
|
|
956
|
+
const revision = options.expected ? workItem.revision + 1 : workItem.revision;
|
|
957
|
+
const replacement = {
|
|
958
|
+
...makeAction(workItem, previous, previousRun),
|
|
959
|
+
contractRevision: previous?.contractRevision ?? workItem.revision,
|
|
960
|
+
};
|
|
961
|
+
if (graphMode) {
|
|
938
962
|
if (!previous) throw new Error('WorkItem graph retry target is missing');
|
|
939
963
|
const action = this.#resetGraphFromStage(
|
|
940
964
|
id,
|
|
@@ -944,18 +968,29 @@ export class WorkItemStore {
|
|
|
944
968
|
now,
|
|
945
969
|
);
|
|
946
970
|
this.db.prepare(`UPDATE work_items SET status = 'ready', current_action_id = ?,
|
|
947
|
-
current_run_id = NULL, updated_at = ? WHERE id = ?`).run(
|
|
948
|
-
|
|
971
|
+
current_run_id = NULL, attachments = ?, revision = ?, updated_at = ? WHERE id = ?`).run(
|
|
972
|
+
action.id,
|
|
973
|
+
stringify(Array.isArray(options.attachments) ? options.attachments : workItem.attachments),
|
|
974
|
+
revision,
|
|
975
|
+
now,
|
|
976
|
+
id,
|
|
977
|
+
);
|
|
978
|
+
const inputEvent = options.inputEvent && typeof options.inputEvent === 'object'
|
|
979
|
+
? options.inputEvent
|
|
980
|
+
: null;
|
|
981
|
+
if (inputEvent) {
|
|
982
|
+
this.appendEvent(id, 'action.input_added', inputEvent, { actionId: action.id });
|
|
983
|
+
} else {
|
|
984
|
+
this.appendEvent(id, 'work_item.retried', { targetStageId: action.stageId }, { actionId: action.id });
|
|
985
|
+
}
|
|
949
986
|
return this.getWorkItemDetail(id);
|
|
950
987
|
}
|
|
951
|
-
const action = this.#insertAction(id,
|
|
952
|
-
...replacement,
|
|
953
|
-
contractRevision: workItem.revision,
|
|
954
|
-
}, this.#nextSequence(id), now);
|
|
988
|
+
const action = this.#insertAction(id, replacement, this.#nextSequence(id), now);
|
|
955
989
|
this.db.prepare(`UPDATE work_items SET status = 'ready', current_action_id = ?,
|
|
956
|
-
current_run_id = NULL, attachments = ?, updated_at = ? WHERE id = ?`).run(
|
|
990
|
+
current_run_id = NULL, attachments = ?, revision = ?, updated_at = ? WHERE id = ?`).run(
|
|
957
991
|
action.id,
|
|
958
992
|
stringify(Array.isArray(options.attachments) ? options.attachments : workItem.attachments),
|
|
993
|
+
revision,
|
|
959
994
|
now,
|
|
960
995
|
id,
|
|
961
996
|
);
|