@yeaft/webchat-agent 1.0.214 → 1.0.215
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/package.json +1 -1
- package/yeaft/work-center/action-identity.js +25 -0
- package/yeaft/work-center/mainline-projection.js +6 -11
- package/yeaft/work-center/projection.js +36 -8
- package/yeaft/work-center/service.js +5 -2
- package/yeaft/work-center/store.js +106 -11
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":"1.0.
|
|
1
|
+
{"version":"1.0.215"}
|
package/package.json
CHANGED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
function generation(value) {
|
|
2
|
+
return Math.max(1, Number(value) || 1);
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
export function eventMatchesActionGeneration(event, action) {
|
|
6
|
+
if (!event || !action) return false;
|
|
7
|
+
return generation(event.actionGeneration) === generation(action.generation);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function runMatchesActionIdentity(run, action) {
|
|
11
|
+
if (!run || !action) return false;
|
|
12
|
+
const actionGeneration = generation(action.generation);
|
|
13
|
+
const manifest = run.executionManifest;
|
|
14
|
+
const runGeneration = generation(run.actionGeneration ?? manifest?.actionGeneration);
|
|
15
|
+
if (runGeneration !== actionGeneration) return false;
|
|
16
|
+
|
|
17
|
+
const actionSpecHash = typeof action.specHash === 'string' ? action.specHash : '';
|
|
18
|
+
const runSpecHash = typeof run.actionSpecHash === 'string' && run.actionSpecHash
|
|
19
|
+
? run.actionSpecHash
|
|
20
|
+
: typeof manifest?.actionSpecHash === 'string' ? manifest.actionSpecHash : '';
|
|
21
|
+
if (!actionSpecHash || !runSpecHash) {
|
|
22
|
+
return actionGeneration === 1 && !actionSpecHash && !runSpecHash;
|
|
23
|
+
}
|
|
24
|
+
return runSpecHash === actionSpecHash;
|
|
25
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
|
+
import { eventMatchesActionGeneration, runMatchesActionIdentity } from './action-identity.js';
|
|
2
3
|
import { normalizeSessionContextSnapshot } from './session-context.js';
|
|
3
4
|
|
|
4
5
|
export const MAINLINE_CONTEXT_HARD_LIMIT_BYTES = 64 * 1024;
|
|
@@ -45,17 +46,10 @@ function stableRunOrder(left, right) {
|
|
|
45
46
|
|| String(right.id).localeCompare(String(left.id));
|
|
46
47
|
}
|
|
47
48
|
|
|
48
|
-
function runMatchesActionSpec(run, action) {
|
|
49
|
-
const manifest = run?.executionManifest;
|
|
50
|
-
return manifest?.schemaVersion === 2
|
|
51
|
-
&& manifest.actionGeneration === Math.max(1, count(action.generation) || 1)
|
|
52
|
-
&& manifest.actionSpecHash === (action.specHash || '');
|
|
53
|
-
}
|
|
54
|
-
|
|
55
49
|
function canonicalRun(action, runs) {
|
|
56
50
|
const candidates = runs.filter(run => run?.actionId === action.id
|
|
57
51
|
&& TERMINAL_RUN_STATUSES.has(run.status)
|
|
58
|
-
&&
|
|
52
|
+
&& runMatchesActionIdentity(run, action));
|
|
59
53
|
if (action.resultRunId) {
|
|
60
54
|
return candidates.find(run => run.id === action.resultRunId) || null;
|
|
61
55
|
}
|
|
@@ -83,9 +77,10 @@ function workItemMessageView(messages) {
|
|
|
83
77
|
}));
|
|
84
78
|
}
|
|
85
79
|
|
|
86
|
-
function guidanceView(events,
|
|
80
|
+
function guidanceView(events, action) {
|
|
87
81
|
return (Array.isArray(events) ? events : [])
|
|
88
|
-
.filter(event => event?.actionId ===
|
|
82
|
+
.filter(event => event?.actionId === action?.id
|
|
83
|
+
&& eventMatchesActionGeneration(event, action)
|
|
89
84
|
&& ['action.guidance_added', 'action.input_added'].includes(event.type))
|
|
90
85
|
.slice()
|
|
91
86
|
.sort((left, right) => count(left.id) - count(right.id))
|
|
@@ -265,7 +260,7 @@ export function buildMainlineContextSnapshot(detail, action, budgetInput = {}) {
|
|
|
265
260
|
};
|
|
266
261
|
const sessionContext = normalizeSessionContextSnapshot(detail.sessionContext);
|
|
267
262
|
const workItemMessages = workItemMessageView(detail.messages);
|
|
268
|
-
const guidance = guidanceView(detail.events, action
|
|
263
|
+
const guidance = guidanceView(detail.events, action);
|
|
269
264
|
const newestFirstMessages = workItemMessages.slice().reverse();
|
|
270
265
|
for (const [index, message] of newestFirstMessages.entries()) {
|
|
271
266
|
const next = {
|
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
sanitizeDebugValue,
|
|
6
6
|
sanitizeDiagnosticText,
|
|
7
7
|
} from './debug-projection.js';
|
|
8
|
+
import { eventMatchesActionGeneration, runMatchesActionIdentity } from './action-identity.js';
|
|
8
9
|
import { taskSpecificActionBrief } from './workflow.js';
|
|
9
10
|
import { buildMainlineProjection } from './mainline-projection.js';
|
|
10
11
|
|
|
@@ -164,12 +165,15 @@ function normalizeProjectedMessage(message) {
|
|
|
164
165
|
createdAt: count(message.createdAt),
|
|
165
166
|
updatedAt: count(message.updatedAt || message.createdAt),
|
|
166
167
|
...(message.progressRevision == null ? {} : { progressRevision: count(message.progressRevision) }),
|
|
168
|
+
...(message.generation == null ? {} : { generation: Math.max(1, count(message.generation) || 1) }),
|
|
169
|
+
...(message.attempt == null ? {} : { attempt: Math.max(1, count(message.attempt) || 1) }),
|
|
167
170
|
};
|
|
168
171
|
}
|
|
169
172
|
|
|
170
173
|
function actionInputMessages(action, events) {
|
|
171
174
|
return (Array.isArray(events) ? events : [])
|
|
172
175
|
.filter(event => event?.actionId === action?.id
|
|
176
|
+
&& eventMatchesActionGeneration(event, action)
|
|
173
177
|
&& ['action.guidance_added', 'action.input_added'].includes(event.type))
|
|
174
178
|
.map(event => normalizeProjectedMessage({
|
|
175
179
|
id: `event:${event.id}`,
|
|
@@ -193,12 +197,17 @@ function runResponseMessage(run) {
|
|
|
193
197
|
createdAt: count(run.startedAt),
|
|
194
198
|
updatedAt: count(run.endedAt || run.startedAt),
|
|
195
199
|
progressRevision: count(run.progressRevision),
|
|
200
|
+
generation: run.actionGeneration,
|
|
201
|
+
attempt: run.actionAttempt,
|
|
196
202
|
});
|
|
197
203
|
}
|
|
198
204
|
|
|
199
|
-
function loopOutputMessages(action, events) {
|
|
205
|
+
function loopOutputMessages(action, events, matchingRunIds) {
|
|
200
206
|
return (Array.isArray(events) ? events : [])
|
|
201
|
-
.filter(event => event?.actionId === action?.id
|
|
207
|
+
.filter(event => event?.actionId === action?.id
|
|
208
|
+
&& eventMatchesActionGeneration(event, action)
|
|
209
|
+
&& matchingRunIds.has(event.runId)
|
|
210
|
+
&& event.type === 'run.loop_output')
|
|
202
211
|
.map(event => normalizeProjectedMessage({
|
|
203
212
|
id: `event:${event.id}`,
|
|
204
213
|
role: 'assistant',
|
|
@@ -206,18 +215,24 @@ function loopOutputMessages(action, events) {
|
|
|
206
215
|
status: 'completed',
|
|
207
216
|
text: event.data?.response || '',
|
|
208
217
|
createdAt: event.createdAt,
|
|
218
|
+
generation: event.actionGeneration ?? event.data?.actionGeneration,
|
|
219
|
+
attempt: event.data?.actionAttempt,
|
|
209
220
|
}))
|
|
210
221
|
.filter(Boolean);
|
|
211
222
|
}
|
|
212
223
|
|
|
213
224
|
function actionMessages(action, runs, events) {
|
|
214
225
|
const matchingRuns = Array.isArray(runs)
|
|
215
|
-
? runs.filter(run => run?.actionId === action?.id)
|
|
226
|
+
? runs.filter(run => run?.actionId === action?.id && runMatchesActionIdentity(run, action))
|
|
216
227
|
: [];
|
|
228
|
+
const matchingRunIds = new Set(matchingRuns.map(run => run.id));
|
|
217
229
|
const runsWithLoopOutput = new Set((Array.isArray(events) ? events : [])
|
|
218
|
-
.filter(event => event?.actionId === action?.id
|
|
230
|
+
.filter(event => event?.actionId === action?.id
|
|
231
|
+
&& eventMatchesActionGeneration(event, action)
|
|
232
|
+
&& matchingRunIds.has(event.runId)
|
|
233
|
+
&& event.type === 'run.loop_output')
|
|
219
234
|
.map(event => event.runId));
|
|
220
|
-
return [...actionInputMessages(action, events), ...loopOutputMessages(action, events), ...matchingRuns
|
|
235
|
+
return [...actionInputMessages(action, events), ...loopOutputMessages(action, events, matchingRunIds), ...matchingRuns
|
|
221
236
|
.sort((left, right) => count(left.startedAt) - count(right.startedAt))
|
|
222
237
|
.filter(run => !runsWithLoopOutput.has(run.id))
|
|
223
238
|
.map(run => runResponseMessage(run))
|
|
@@ -282,11 +297,12 @@ function sanitizeFailureReason(value) {
|
|
|
282
297
|
|
|
283
298
|
function actionExecution(action, runs, events, includeBody = true) {
|
|
284
299
|
const matchingRuns = Array.isArray(runs)
|
|
285
|
-
? runs.filter(run => run?.actionId === action?.id)
|
|
300
|
+
? runs.filter(run => run?.actionId === action?.id && runMatchesActionIdentity(run, action))
|
|
286
301
|
: [];
|
|
287
302
|
if (matchingRuns.length === 0) {
|
|
288
303
|
const inputMessageCount = Array.isArray(events) ? events.filter(event => (
|
|
289
304
|
event?.actionId === action?.id
|
|
305
|
+
&& eventMatchesActionGeneration(event, action)
|
|
290
306
|
&& ['action.guidance_added', 'action.input_added'].includes(event.type)
|
|
291
307
|
)).length : 0;
|
|
292
308
|
const messages = includeBody
|
|
@@ -334,6 +350,7 @@ function actionExecution(action, runs, events, includeBody = true) {
|
|
|
334
350
|
? 0
|
|
335
351
|
: (Array.isArray(events) ? events : []).filter(event => (
|
|
336
352
|
event?.actionId === action?.id
|
|
353
|
+
&& eventMatchesActionGeneration(event, action)
|
|
337
354
|
&& ['action.guidance_added', 'action.input_added'].includes(event.type)
|
|
338
355
|
)).length;
|
|
339
356
|
const allMessages = includeBody ? actionMessages(action, matchingRuns, events) : [];
|
|
@@ -395,7 +412,9 @@ function projectAction(action, runs, events, includeBody = true) {
|
|
|
395
412
|
key,
|
|
396
413
|
truncateUtf8(value, includeBody ? MAX_CURRENT_BRIEF_BYTES : MAX_HISTORICAL_BRIEF_CHARS),
|
|
397
414
|
]));
|
|
398
|
-
const matchingRuns = Array.isArray(runs)
|
|
415
|
+
const matchingRuns = Array.isArray(runs)
|
|
416
|
+
? runs.filter(run => run?.actionId === action.id && runMatchesActionIdentity(run, action))
|
|
417
|
+
: [];
|
|
399
418
|
const latestRun = [...matchingRuns].sort((left, right) => (
|
|
400
419
|
count(right.startedAt) - count(left.startedAt) || count(right.progressRevision) - count(left.progressRevision)
|
|
401
420
|
))[0];
|
|
@@ -418,6 +437,8 @@ function projectAction(action, runs, events, includeBody = true) {
|
|
|
418
437
|
dependsOnStageIds: Array.isArray(action.dependsOnStageIds) ? action.dependsOnStageIds : [],
|
|
419
438
|
workspaceMode: action.workspaceMode || 'shared',
|
|
420
439
|
requiredRole: action.requiredRole || '',
|
|
440
|
+
generation: Math.max(1, count(action.generation) || 1),
|
|
441
|
+
replacesActionId: action.replacesActionId || null,
|
|
421
442
|
brief: projectedBrief,
|
|
422
443
|
status: action.status,
|
|
423
444
|
assignedVp,
|
|
@@ -816,6 +837,7 @@ export function projectActionMessagePage(action, runs, events, options = {}) {
|
|
|
816
837
|
const start = Math.max(0, end - limit);
|
|
817
838
|
return {
|
|
818
839
|
actionId: action.id,
|
|
840
|
+
generation: Math.max(1, count(action.generation) || 1),
|
|
819
841
|
messages: messages.slice(start, end),
|
|
820
842
|
nextCursor: start > 0 ? String(start) : null,
|
|
821
843
|
total: messages.length,
|
|
@@ -825,9 +847,14 @@ export function projectActionMessagePage(action, runs, events, options = {}) {
|
|
|
825
847
|
export function projectActionRequestIndex(action, entries) {
|
|
826
848
|
return {
|
|
827
849
|
actionId: action.id,
|
|
828
|
-
|
|
850
|
+
generation: Math.max(1, count(action.generation) || 1),
|
|
851
|
+
requests: (Array.isArray(entries) ? entries : [])
|
|
852
|
+
.filter(({ run }) => runMatchesActionIdentity(run, action))
|
|
853
|
+
.map(({ run, turn }) => ({
|
|
829
854
|
id: turn.turnId,
|
|
830
855
|
runId: run.id,
|
|
856
|
+
generation: Math.max(1, count(run.actionGeneration) || 1),
|
|
857
|
+
attempt: Math.max(1, count(run.actionAttempt) || 1),
|
|
831
858
|
status: run.status || 'running',
|
|
832
859
|
model: run.modelSnapshot?.id || null,
|
|
833
860
|
vp: run.vpSnapshot ? {
|
|
@@ -846,6 +873,7 @@ export function projectActionRequestIndex(action, entries) {
|
|
|
846
873
|
}
|
|
847
874
|
|
|
848
875
|
export function projectActionRequestDetail(action, run, history) {
|
|
876
|
+
if (!runMatchesActionIdentity(run, action)) return null;
|
|
849
877
|
const turn = Array.isArray(history?.turns) ? history.turns[0] : null;
|
|
850
878
|
if (!turn) return null;
|
|
851
879
|
const sourceLoops = Array.isArray(history?.loops) ? history.loops : [];
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { realpathSync, statSync } from 'node:fs';
|
|
2
2
|
import { join, resolve } from 'node:path';
|
|
3
3
|
import { randomUUID } from 'node:crypto';
|
|
4
|
+
import { runMatchesActionIdentity } from './action-identity.js';
|
|
4
5
|
import { WorkItemStore } from './store.js';
|
|
5
6
|
import { WorkflowController } from './controller.js';
|
|
6
7
|
import { WorkItemWatcher } from './watcher.js';
|
|
@@ -142,7 +143,8 @@ export class WorkCenterService {
|
|
|
142
143
|
const detail = this.#requiredItem(payload.id);
|
|
143
144
|
const action = this.#requiredAction(detail, payload.actionId);
|
|
144
145
|
const entries = [];
|
|
145
|
-
for (const run of detail.runs.filter(item => item.actionId === action.id
|
|
146
|
+
for (const run of detail.runs.filter(item => item.actionId === action.id
|
|
147
|
+
&& runMatchesActionIdentity(item, action))) {
|
|
146
148
|
const history = await this.#debugHistory(run, { indexOnly: true });
|
|
147
149
|
for (const turn of Array.isArray(history?.turns) ? history.turns : []) {
|
|
148
150
|
entries.push({ run, turn });
|
|
@@ -154,7 +156,8 @@ export class WorkCenterService {
|
|
|
154
156
|
const detail = this.#requiredItem(payload.id);
|
|
155
157
|
const action = this.#requiredAction(detail, payload.actionId);
|
|
156
158
|
const requestId = requiredString(payload.requestId, 'requestId');
|
|
157
|
-
const run = detail.runs.find(item => item.actionId === action.id
|
|
159
|
+
const run = detail.runs.find(item => item.actionId === action.id
|
|
160
|
+
&& item.id === payload.runId && runMatchesActionIdentity(item, action));
|
|
158
161
|
if (!run) throw new Error('Action request not found');
|
|
159
162
|
const history = await this.#debugHistory(run, { detailTurnId: requestId });
|
|
160
163
|
const projected = projectActionRequestDetail(action, run, history);
|
|
@@ -4,8 +4,9 @@ import { dirname, resolve } from 'node:path';
|
|
|
4
4
|
import { createHash, randomUUID } from 'node:crypto';
|
|
5
5
|
import { normalizeEvidence } from './evidence.js';
|
|
6
6
|
import { normalizeActionCheckpoint } from './action-checkpoint.js';
|
|
7
|
+
import { runMatchesActionIdentity } from './action-identity.js';
|
|
7
8
|
|
|
8
|
-
const SCHEMA_VERSION =
|
|
9
|
+
const SCHEMA_VERSION = 17;
|
|
9
10
|
const OPEN_ACTION_STATUSES = "'ready','running','waiting'";
|
|
10
11
|
const MAX_REUSABLE_CONTEXT_ITEMS = 12;
|
|
11
12
|
const MAX_RUN_RESPONSE_CHARS = 65_536;
|
|
@@ -163,6 +164,7 @@ function mapAction(row) {
|
|
|
163
164
|
maxAttempts: row.max_attempts,
|
|
164
165
|
currentRunId: row.current_run_id || null,
|
|
165
166
|
leaseEpoch: row.lease_epoch,
|
|
167
|
+
replacesActionId: row.replaces_action_id || null,
|
|
166
168
|
createdAt: row.created_at,
|
|
167
169
|
updatedAt: row.updated_at,
|
|
168
170
|
};
|
|
@@ -206,6 +208,9 @@ function mapRun(row) {
|
|
|
206
208
|
progressRevision: Math.max(0, Number(row.progress_revision) || 0),
|
|
207
209
|
checkpoint: normalizeActionCheckpoint(parseJson(row.checkpoint, null)),
|
|
208
210
|
acceptingInput: row.accepting_input !== 0,
|
|
211
|
+
actionGeneration: Math.max(1, Number(row.action_generation) || 1),
|
|
212
|
+
actionSpecHash: row.action_spec_hash || parseJson(row.execution_manifest, null)?.actionSpecHash || '',
|
|
213
|
+
actionAttempt: Math.max(1, Number(row.action_attempt) || 1),
|
|
209
214
|
};
|
|
210
215
|
}
|
|
211
216
|
|
|
@@ -232,6 +237,7 @@ function mapEvent(row) {
|
|
|
232
237
|
workItemId: row.work_item_id,
|
|
233
238
|
actionId: row.action_id || null,
|
|
234
239
|
runId: row.run_id || null,
|
|
240
|
+
actionGeneration: row.action_generation == null ? null : Math.max(1, Number(row.action_generation) || 1),
|
|
235
241
|
type: row.type,
|
|
236
242
|
data: parseJson(row.data, {}),
|
|
237
243
|
createdAt: row.created_at,
|
|
@@ -324,6 +330,7 @@ export class WorkItemStore {
|
|
|
324
330
|
max_attempts INTEGER NOT NULL DEFAULT 2,
|
|
325
331
|
current_run_id TEXT,
|
|
326
332
|
lease_epoch INTEGER NOT NULL DEFAULT 0,
|
|
333
|
+
replaces_action_id TEXT REFERENCES actions(id) ON DELETE SET NULL,
|
|
327
334
|
created_at INTEGER NOT NULL,
|
|
328
335
|
updated_at INTEGER NOT NULL,
|
|
329
336
|
UNIQUE(work_item_id, sequence)
|
|
@@ -363,13 +370,17 @@ export class WorkItemStore {
|
|
|
363
370
|
total_tokens INTEGER NOT NULL DEFAULT 0,
|
|
364
371
|
progress_revision INTEGER NOT NULL DEFAULT 0,
|
|
365
372
|
checkpoint TEXT,
|
|
366
|
-
accepting_input INTEGER NOT NULL DEFAULT 1
|
|
373
|
+
accepting_input INTEGER NOT NULL DEFAULT 1,
|
|
374
|
+
action_generation INTEGER NOT NULL DEFAULT 1,
|
|
375
|
+
action_spec_hash TEXT NOT NULL DEFAULT '',
|
|
376
|
+
action_attempt INTEGER NOT NULL DEFAULT 1
|
|
367
377
|
);
|
|
368
378
|
CREATE TABLE IF NOT EXISTS events (
|
|
369
379
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
370
380
|
work_item_id TEXT NOT NULL REFERENCES work_items(id) ON DELETE CASCADE,
|
|
371
381
|
action_id TEXT,
|
|
372
382
|
run_id TEXT,
|
|
383
|
+
action_generation INTEGER,
|
|
373
384
|
type TEXT NOT NULL,
|
|
374
385
|
data TEXT NOT NULL,
|
|
375
386
|
created_at INTEGER NOT NULL
|
|
@@ -417,6 +428,10 @@ export class WorkItemStore {
|
|
|
417
428
|
ON pending_action_inputs(action_id, consumed_at, event_id);
|
|
418
429
|
`);
|
|
419
430
|
|
|
431
|
+
const storedSchemaVersion = Number(
|
|
432
|
+
this.db.prepare("SELECT value FROM schema_meta WHERE key = 'schema_version'").get()?.value,
|
|
433
|
+
) || 0;
|
|
434
|
+
|
|
420
435
|
// The feature shipped first as an unmerged PR, but keep the store tolerant
|
|
421
436
|
// of databases created by review builds.
|
|
422
437
|
if (!hasColumn(this.db, 'work_items', 'workspace_key')) {
|
|
@@ -537,12 +552,48 @@ export class WorkItemStore {
|
|
|
537
552
|
if (!hasColumn(this.db, 'actions', 'result_run_id')) {
|
|
538
553
|
this.db.exec('ALTER TABLE actions ADD COLUMN result_run_id TEXT');
|
|
539
554
|
}
|
|
555
|
+
if (!hasColumn(this.db, 'actions', 'replaces_action_id')) {
|
|
556
|
+
this.db.exec('ALTER TABLE actions ADD COLUMN replaces_action_id TEXT REFERENCES actions(id) ON DELETE SET NULL');
|
|
557
|
+
}
|
|
558
|
+
if (!hasColumn(this.db, 'runs', 'action_generation')) {
|
|
559
|
+
this.db.exec('ALTER TABLE runs ADD COLUMN action_generation INTEGER NOT NULL DEFAULT 1');
|
|
560
|
+
}
|
|
561
|
+
if (!hasColumn(this.db, 'runs', 'action_spec_hash')) {
|
|
562
|
+
this.db.exec("ALTER TABLE runs ADD COLUMN action_spec_hash TEXT NOT NULL DEFAULT ''");
|
|
563
|
+
}
|
|
564
|
+
if (!hasColumn(this.db, 'runs', 'action_attempt')) {
|
|
565
|
+
this.db.exec('ALTER TABLE runs ADD COLUMN action_attempt INTEGER NOT NULL DEFAULT 1');
|
|
566
|
+
}
|
|
567
|
+
if (!hasColumn(this.db, 'events', 'action_generation')) {
|
|
568
|
+
this.db.exec('ALTER TABLE events ADD COLUMN action_generation INTEGER');
|
|
569
|
+
}
|
|
540
570
|
if (!hasColumn(this.db, 'runs', 'context_snapshot')) {
|
|
541
571
|
this.db.exec('ALTER TABLE runs ADD COLUMN context_snapshot TEXT');
|
|
542
572
|
}
|
|
543
573
|
if (!hasColumn(this.db, 'runs', 'execution_manifest')) {
|
|
544
574
|
this.db.exec('ALTER TABLE runs ADD COLUMN execution_manifest TEXT');
|
|
545
575
|
}
|
|
576
|
+
if (storedSchemaVersion < SCHEMA_VERSION) {
|
|
577
|
+
withTransaction(this.db, () => {
|
|
578
|
+
const updateIdentity = this.db.prepare(`UPDATE runs SET action_generation = ?,
|
|
579
|
+
action_spec_hash = ?, action_attempt = ? WHERE id = ?`);
|
|
580
|
+
const attempts = new Map();
|
|
581
|
+
for (const row of this.db.prepare(`SELECT id, action_id, action_generation, action_spec_hash,
|
|
582
|
+
execution_manifest, started_at FROM runs ORDER BY action_id, started_at, id`).all()) {
|
|
583
|
+
const manifest = parseJson(row.execution_manifest, null);
|
|
584
|
+
const generation = Math.max(1, Number(manifest?.actionGeneration) || Number(row.action_generation) || 1);
|
|
585
|
+
const key = `${row.action_id}\u0000${generation}`;
|
|
586
|
+
const attempt = (attempts.get(key) || 0) + 1;
|
|
587
|
+
attempts.set(key, attempt);
|
|
588
|
+
updateIdentity.run(
|
|
589
|
+
generation,
|
|
590
|
+
manifest?.actionSpecHash || row.action_spec_hash || '',
|
|
591
|
+
attempt,
|
|
592
|
+
row.id,
|
|
593
|
+
);
|
|
594
|
+
}
|
|
595
|
+
});
|
|
596
|
+
}
|
|
546
597
|
this.db.prepare(`INSERT INTO schema_meta(key, value) VALUES('schema_version', ?)
|
|
547
598
|
ON CONFLICT(key) DO UPDATE SET value = excluded.value`).run(String(SCHEMA_VERSION));
|
|
548
599
|
}
|
|
@@ -552,12 +603,16 @@ export class WorkItemStore {
|
|
|
552
603
|
}
|
|
553
604
|
|
|
554
605
|
appendEvent(workItemId, type, data = {}, refs = {}) {
|
|
606
|
+
const actionGeneration = refs.actionGeneration
|
|
607
|
+
?? (refs.actionId ? this.getAction(refs.actionId)?.generation : null)
|
|
608
|
+
?? null;
|
|
555
609
|
const result = this.db.prepare(`INSERT INTO events
|
|
556
|
-
(work_item_id, action_id, run_id, type, data, created_at)
|
|
557
|
-
VALUES (?, ?, ?, ?, ?, ?)`).run(
|
|
610
|
+
(work_item_id, action_id, run_id, action_generation, type, data, created_at)
|
|
611
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)`).run(
|
|
558
612
|
workItemId,
|
|
559
613
|
refs.actionId || null,
|
|
560
614
|
refs.runId || null,
|
|
615
|
+
actionGeneration,
|
|
561
616
|
type,
|
|
562
617
|
stringify(data),
|
|
563
618
|
this.now(),
|
|
@@ -778,6 +833,7 @@ export class WorkItemStore {
|
|
|
778
833
|
maxAttempts: Number.isInteger(input.maxAttempts) ? input.maxAttempts : 2,
|
|
779
834
|
currentRunId: null,
|
|
780
835
|
leaseEpoch: 0,
|
|
836
|
+
replacesActionId: input.replacesActionId || null,
|
|
781
837
|
createdAt: now,
|
|
782
838
|
updatedAt: now,
|
|
783
839
|
};
|
|
@@ -785,8 +841,9 @@ export class WorkItemStore {
|
|
|
785
841
|
this.db.prepare(`INSERT INTO actions
|
|
786
842
|
(id, work_item_id, sequence, type, required_role, stage_id, assignment_policy, model_policy,
|
|
787
843
|
depends_on_stage_ids, workspace_mode, changes_requested_stage_id, workspace, instruction, brief, context, contract_revision,
|
|
788
|
-
generation, spec_hash, result_run_id, status, attempt, max_attempts, current_run_id, lease_epoch,
|
|
789
|
-
|
|
844
|
+
generation, spec_hash, result_run_id, status, attempt, max_attempts, current_run_id, lease_epoch,
|
|
845
|
+
replaces_action_id, created_at, updated_at)
|
|
846
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NULL, 0, ?, ?, ?)`).run(
|
|
790
847
|
action.id,
|
|
791
848
|
workItemId,
|
|
792
849
|
action.sequence,
|
|
@@ -809,6 +866,7 @@ export class WorkItemStore {
|
|
|
809
866
|
action.status,
|
|
810
867
|
action.attempt,
|
|
811
868
|
action.maxAttempts,
|
|
869
|
+
action.replacesActionId,
|
|
812
870
|
now,
|
|
813
871
|
now,
|
|
814
872
|
);
|
|
@@ -951,6 +1009,8 @@ export class WorkItemStore {
|
|
|
951
1009
|
const nextWorkspaceMode = workspaceMode || action.workspaceMode;
|
|
952
1010
|
const specChanged = nextWorkspaceMode !== action.workspaceMode;
|
|
953
1011
|
const nextAction = { ...action, workspaceMode: nextWorkspaceMode };
|
|
1012
|
+
const nextGeneration = action.generation + (specChanged ? 1 : 0);
|
|
1013
|
+
const nextSpecHash = specChanged ? actionSpecHash(nextAction) : action.specHash;
|
|
954
1014
|
const now = this.now();
|
|
955
1015
|
if (action.workspaceMode === 'isolated-write' && nextWorkspaceMode === 'shared') {
|
|
956
1016
|
const workItem = this.getWorkItem(action.workItemId);
|
|
@@ -973,7 +1033,7 @@ export class WorkItemStore {
|
|
|
973
1033
|
stringify(workspace),
|
|
974
1034
|
nextWorkspaceMode,
|
|
975
1035
|
specChanged ? 1 : 0,
|
|
976
|
-
|
|
1036
|
+
nextSpecHash,
|
|
977
1037
|
specChanged ? 1 : 0,
|
|
978
1038
|
now,
|
|
979
1039
|
actionId,
|
|
@@ -982,6 +1042,23 @@ export class WorkItemStore {
|
|
|
982
1042
|
expectedGeneration,
|
|
983
1043
|
);
|
|
984
1044
|
if (Number(changed.changes) !== 1) return null;
|
|
1045
|
+
if (specChanged) {
|
|
1046
|
+
const rebound = this.db.prepare(`UPDATE runs SET action_generation = ?, action_spec_hash = ?
|
|
1047
|
+
WHERE id = ? AND action_id = ? AND owner_boot_id = ? AND lease_epoch = ? AND status = 'running'
|
|
1048
|
+
AND action_generation = ? AND action_spec_hash = ?`).run(
|
|
1049
|
+
nextGeneration,
|
|
1050
|
+
nextSpecHash,
|
|
1051
|
+
runId,
|
|
1052
|
+
actionId,
|
|
1053
|
+
ownerBootId,
|
|
1054
|
+
leaseEpoch,
|
|
1055
|
+
action.generation,
|
|
1056
|
+
action.specHash,
|
|
1057
|
+
);
|
|
1058
|
+
if (Number(rebound.changes) !== 1) {
|
|
1059
|
+
throw new Error('Work Center could not rebind the owned Run after workspace fallback');
|
|
1060
|
+
}
|
|
1061
|
+
}
|
|
985
1062
|
|
|
986
1063
|
if (action.workspaceMode === 'isolated-write' && nextWorkspaceMode === 'shared') {
|
|
987
1064
|
const pendingRows = this.db.prepare(`SELECT * FROM actions
|
|
@@ -1705,6 +1782,10 @@ export class WorkItemStore {
|
|
|
1705
1782
|
const priorProgress = this.db.prepare(`SELECT MAX(progress_revision) AS value FROM runs
|
|
1706
1783
|
WHERE action_id = ?`).get(row.id);
|
|
1707
1784
|
const progressRevision = Math.max(0, Number(priorProgress?.value) || 0) + 1;
|
|
1785
|
+
const actionGeneration = Math.max(1, Number(row.generation) || 1);
|
|
1786
|
+
const priorAttempt = this.db.prepare(`SELECT MAX(action_attempt) AS value FROM runs
|
|
1787
|
+
WHERE action_id = ? AND action_generation = ?`).get(row.id, actionGeneration);
|
|
1788
|
+
const actionAttempt = Math.max(0, Number(priorAttempt?.value) || 0) + 1;
|
|
1708
1789
|
const changedAction = this.db.prepare(`UPDATE actions SET status = 'running', attempt = attempt + 1,
|
|
1709
1790
|
current_run_id = ?, lease_epoch = ?, updated_at = ?
|
|
1710
1791
|
WHERE id = ? AND status = 'ready' AND current_run_id IS NULL`).run(
|
|
@@ -1726,9 +1807,19 @@ export class WorkItemStore {
|
|
|
1726
1807
|
if (Number(changedWorkItem.changes) !== 1) throw new Error('WorkItem claim lost its Action fence');
|
|
1727
1808
|
this.db.prepare(`INSERT INTO runs
|
|
1728
1809
|
(id, action_id, work_item_id, owner_boot_id, lease_epoch, status, started_at,
|
|
1729
|
-
expires_at, evidence, progress_revision)
|
|
1730
|
-
VALUES (?, ?, ?, ?, ?, 'running', ?, ?, '[]', ?)`).run(
|
|
1731
|
-
runId,
|
|
1810
|
+
expires_at, evidence, progress_revision, action_generation, action_spec_hash, action_attempt)
|
|
1811
|
+
VALUES (?, ?, ?, ?, ?, 'running', ?, ?, '[]', ?, ?, ?, ?)`).run(
|
|
1812
|
+
runId,
|
|
1813
|
+
row.id,
|
|
1814
|
+
row.work_item_id,
|
|
1815
|
+
ownerBootId,
|
|
1816
|
+
leaseEpoch,
|
|
1817
|
+
now,
|
|
1818
|
+
now + leaseMs,
|
|
1819
|
+
progressRevision,
|
|
1820
|
+
actionGeneration,
|
|
1821
|
+
row.spec_hash || '',
|
|
1822
|
+
actionAttempt,
|
|
1732
1823
|
);
|
|
1733
1824
|
this.appendEvent(row.work_item_id, 'run.claimed', { ownerBootId, leaseEpoch }, {
|
|
1734
1825
|
actionId: row.id, runId,
|
|
@@ -1903,9 +1994,13 @@ export class WorkItemStore {
|
|
|
1903
1994
|
}
|
|
1904
1995
|
|
|
1905
1996
|
getActionResumeContext(actionId, excludeRunId = null) {
|
|
1997
|
+
const action = this.getAction(actionId);
|
|
1998
|
+
if (!action) return null;
|
|
1906
1999
|
const runs = this.db.prepare(`SELECT * FROM runs
|
|
1907
2000
|
WHERE action_id = ? AND id != ? AND status IN ('interrupted', 'retryable')
|
|
1908
|
-
ORDER BY ended_at DESC, started_at DESC`).all(actionId, excludeRunId || '')
|
|
2001
|
+
ORDER BY ended_at DESC, started_at DESC`).all(actionId, excludeRunId || '')
|
|
2002
|
+
.map(mapRun)
|
|
2003
|
+
.filter(run => runMatchesActionIdentity(run, action));
|
|
1909
2004
|
if (runs.length === 0) return null;
|
|
1910
2005
|
const latest = runs[0];
|
|
1911
2006
|
const response = runs.find(run => run.response)?.response || '';
|