@yeaft/webchat-agent 1.0.218 → 1.0.220

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yeaft/webchat-agent",
3
- "version": "1.0.218",
3
+ "version": "1.0.220",
4
4
  "description": "Remote worker agent for Yeaft Web Code Agent — connects the native Yeaft engine, CLI providers, and workbench tools",
5
5
  "main": "index.js",
6
6
  "type": "module",
@@ -148,6 +148,39 @@ function sumExecutionStats(values) {
148
148
  }, emptyExecutionStats());
149
149
  }
150
150
 
151
+ function actionGeneration(value) {
152
+ return Math.max(1, count(value) || 1);
153
+ }
154
+
155
+ function runGeneration(run) {
156
+ return actionGeneration(run?.actionGeneration ?? run?.executionManifest?.actionGeneration);
157
+ }
158
+
159
+ function runSpecHash(run) {
160
+ return typeof run?.actionSpecHash === 'string' && run.actionSpecHash
161
+ ? run.actionSpecHash
162
+ : typeof run?.executionManifest?.actionSpecHash === 'string'
163
+ ? run.executionManifest.actionSpecHash
164
+ : '';
165
+ }
166
+
167
+ function threadRuns(action, runs) {
168
+ const source = (Array.isArray(runs) ? runs : []).filter(run => run?.actionId === action?.id);
169
+ const selectedSpecByGeneration = new Map((Array.isArray(action?.identityHistory) ? action.identityHistory : [])
170
+ .filter(identity => typeof identity?.specHash === 'string' && identity.specHash)
171
+ .map(identity => [actionGeneration(identity.generation), identity.specHash]));
172
+ const currentGeneration = actionGeneration(action?.generation);
173
+ if (typeof action?.specHash === 'string' && action.specHash) {
174
+ selectedSpecByGeneration.set(currentGeneration, action.specHash);
175
+ }
176
+ return source.filter(run => {
177
+ const generation = runGeneration(run);
178
+ const selectedSpec = selectedSpecByGeneration.get(generation) || '';
179
+ const spec = runSpecHash(run);
180
+ return selectedSpec ? spec === selectedSpec : generation === 1 && !spec;
181
+ });
182
+ }
183
+
151
184
  function normalizeProjectedMessage(message) {
152
185
  if (!message || typeof message !== 'object') return null;
153
186
  const text = typeof message.text === 'string'
@@ -167,13 +200,14 @@ function normalizeProjectedMessage(message) {
167
200
  ...(message.progressRevision == null ? {} : { progressRevision: count(message.progressRevision) }),
168
201
  ...(message.generation == null ? {} : { generation: Math.max(1, count(message.generation) || 1) }),
169
202
  ...(message.attempt == null ? {} : { attempt: Math.max(1, count(message.attempt) || 1) }),
203
+ ...(message.runId == null ? {} : { runId: String(message.runId) }),
170
204
  };
171
205
  }
172
206
 
173
- function actionInputMessages(action, events) {
207
+ function actionInputMessages(action, events, generation = actionGeneration(action?.generation), includeThreadIdentity = false) {
174
208
  return (Array.isArray(events) ? events : [])
175
209
  .filter(event => event?.actionId === action?.id
176
- && eventMatchesActionGeneration(event, action)
210
+ && actionGeneration(event.actionGeneration) === generation
177
211
  && ['action.guidance_added', 'action.input_added'].includes(event.type))
178
212
  .map(event => normalizeProjectedMessage({
179
213
  id: `event:${event.id}`,
@@ -183,11 +217,12 @@ function actionInputMessages(action, events) {
183
217
  text: event.data?.text || event.data?.guidance || '',
184
218
  attachments: event.data?.attachments,
185
219
  createdAt: event.createdAt,
220
+ ...(includeThreadIdentity ? { generation } : {}),
186
221
  }))
187
222
  .filter(Boolean);
188
223
  }
189
224
 
190
- function runResponseMessage(run) {
225
+ function runResponseMessage(run, includeThreadIdentity = false) {
191
226
  return normalizeProjectedMessage({
192
227
  id: `run:${run.id}`,
193
228
  role: 'assistant',
@@ -197,15 +232,18 @@ function runResponseMessage(run) {
197
232
  createdAt: count(run.startedAt),
198
233
  updatedAt: count(run.endedAt || run.startedAt),
199
234
  progressRevision: count(run.progressRevision),
200
- generation: run.actionGeneration,
201
- attempt: run.actionAttempt,
235
+ ...(includeThreadIdentity ? {
236
+ generation: runGeneration(run),
237
+ attempt: run.actionAttempt,
238
+ runId: run.id,
239
+ } : {}),
202
240
  });
203
241
  }
204
242
 
205
- function loopOutputMessages(action, events, matchingRunIds) {
243
+ function loopOutputMessages(action, events, matchingRunIds, generation = actionGeneration(action?.generation), includeThreadIdentity = false) {
206
244
  return (Array.isArray(events) ? events : [])
207
245
  .filter(event => event?.actionId === action?.id
208
- && eventMatchesActionGeneration(event, action)
246
+ && actionGeneration(event.actionGeneration ?? event.data?.actionGeneration) === generation
209
247
  && matchingRunIds.has(event.runId)
210
248
  && event.type === 'run.loop_output')
211
249
  .map(event => normalizeProjectedMessage({
@@ -215,33 +253,73 @@ function loopOutputMessages(action, events, matchingRunIds) {
215
253
  status: 'completed',
216
254
  text: event.data?.response || '',
217
255
  createdAt: event.createdAt,
218
- generation: event.actionGeneration ?? event.data?.actionGeneration,
219
- attempt: event.data?.actionAttempt,
256
+ ...(includeThreadIdentity ? {
257
+ generation: event.actionGeneration ?? event.data?.actionGeneration,
258
+ attempt: event.data?.actionAttempt,
259
+ runId: event.runId,
260
+ } : {}),
220
261
  }))
221
262
  .filter(Boolean);
222
263
  }
223
264
 
224
- function actionMessages(action, runs, events) {
225
- const matchingRuns = Array.isArray(runs)
226
- ? runs.filter(run => run?.actionId === action?.id && runMatchesActionIdentity(run, action))
227
- : [];
265
+ function messagesForGeneration(action, runs, events, generation, includeThreadIdentity = false) {
266
+ const matchingRuns = threadRuns(action, runs).filter(run => runGeneration(run) === generation);
228
267
  const matchingRunIds = new Set(matchingRuns.map(run => run.id));
229
268
  const runsWithLoopOutput = new Set((Array.isArray(events) ? events : [])
230
269
  .filter(event => event?.actionId === action?.id
231
- && eventMatchesActionGeneration(event, action)
270
+ && actionGeneration(event.actionGeneration ?? event.data?.actionGeneration) === generation
232
271
  && matchingRunIds.has(event.runId)
233
272
  && event.type === 'run.loop_output')
234
273
  .map(event => event.runId));
235
- return [...actionInputMessages(action, events), ...loopOutputMessages(action, events, matchingRunIds), ...matchingRuns
274
+ return [
275
+ ...actionInputMessages(action, events, generation, includeThreadIdentity),
276
+ ...loopOutputMessages(action, events, matchingRunIds, generation, includeThreadIdentity),
277
+ ...matchingRuns
236
278
  .sort((left, right) => count(left.startedAt) - count(right.startedAt))
237
279
  .filter(run => !runsWithLoopOutput.has(run.id))
238
- .map(run => runResponseMessage(run))
280
+ .map(run => runResponseMessage(run, includeThreadIdentity))
239
281
  .filter(Boolean)]
240
282
  .sort((left, right) => left.createdAt - right.createdAt
241
283
  || (left.role === 'user' ? -1 : 1)
242
284
  || left.id.localeCompare(right.id));
243
285
  }
244
286
 
287
+ function actionMessages(action, runs, events) {
288
+ return messagesForGeneration(action, runs, events, actionGeneration(action?.generation));
289
+ }
290
+
291
+ function projectActionThread(action, runs, events) {
292
+ const allRuns = threadRuns(action, runs);
293
+ const generations = new Set([
294
+ actionGeneration(action?.generation),
295
+ ...allRuns.map(runGeneration),
296
+ ...(Array.isArray(events) ? events : [])
297
+ .filter(event => event?.actionId === action?.id
298
+ && ['action.guidance_added', 'action.input_added', 'run.loop_output'].includes(event.type))
299
+ .map(event => actionGeneration(event.actionGeneration ?? event.data?.actionGeneration)),
300
+ ]);
301
+ return [...generations].sort((left, right) => left - right).map(generation => {
302
+ const canonical = generation === actionGeneration(action?.generation);
303
+ return {
304
+ generation,
305
+ canonical,
306
+ messages: canonical ? [] : messagesForGeneration(action, allRuns, events, generation, true).slice(-MAX_ACTION_MESSAGES),
307
+ runs: allRuns.filter(run => runGeneration(run) === generation)
308
+ .sort((left, right) => count(left.startedAt) - count(right.startedAt) || String(left.id).localeCompare(String(right.id)))
309
+ .map(run => ({
310
+ id: run.id,
311
+ attempt: Math.max(1, count(run.actionAttempt) || 1),
312
+ status: run.status || 'running',
313
+ startedAt: count(run.startedAt),
314
+ endedAt: count(run.endedAt),
315
+ progressRevision: count(run.progressRevision),
316
+ loopCount: count(run.loopCount),
317
+ toolCount: count(run.toolCount),
318
+ })),
319
+ };
320
+ }).filter(entry => entry.messages.length > 0 || entry.runs.length > 0 || entry.canonical);
321
+ }
322
+
245
323
 
246
324
 
247
325
  const MAX_FAILURE_REASON_LENGTH = 2_000;
@@ -457,6 +535,7 @@ function projectAction(action, runs, events, includeBody = true) {
457
535
  response: execution.response,
458
536
  failure: execution.failure,
459
537
  messages: execution.messages,
538
+ thread: Array.isArray(runs) ? projectActionThread(action, runs, events) : (action.thread || []),
460
539
  liveMessage: execution.liveMessage,
461
540
  } : {}),
462
541
  };
@@ -478,6 +557,7 @@ function stripActionBody(action, keepFailure = false) {
478
557
  const projected = { ...action };
479
558
  delete projected.response;
480
559
  delete projected.messages;
560
+ delete projected.thread;
481
561
  delete projected.liveMessage;
482
562
  if (!keepFailure) delete projected.failure;
483
563
  if (projected.brief) {
@@ -539,6 +619,7 @@ function enforceWorkItemBrowserDtoBudget(value, options = {}) {
539
619
  if (keep) {
540
620
  delete keep.response;
541
621
  delete keep.messages;
622
+ delete keep.thread;
542
623
  delete keep.liveMessage;
543
624
  }
544
625
  if (jsonByteLength(dto) <= MAX_WORK_ITEM_BROWSER_DTO_BYTES) return dto;
@@ -844,12 +925,18 @@ export function projectActionMessagePage(action, runs, events, options = {}) {
844
925
  };
845
926
  }
846
927
 
928
+ export function actionThreadIncludesRun(action, runs, runId) {
929
+ return threadRuns(action, runs).some(run => run.id === runId);
930
+ }
931
+
847
932
  export function projectActionRequestIndex(action, entries) {
933
+ const source = Array.isArray(entries) ? entries : [];
934
+ const allowedRunIds = new Set(threadRuns(action, source.map(({ run }) => run)).map(run => run.id));
848
935
  return {
849
936
  actionId: action.id,
850
937
  generation: Math.max(1, count(action.generation) || 1),
851
- requests: (Array.isArray(entries) ? entries : [])
852
- .filter(({ run }) => runMatchesActionIdentity(run, action))
938
+ requests: source
939
+ .filter(({ run }) => allowedRunIds.has(run?.id))
853
940
  .map(({ run, turn }) => ({
854
941
  id: turn.turnId,
855
942
  runId: run.id,
@@ -872,8 +959,8 @@ export function projectActionRequestIndex(action, entries) {
872
959
  };
873
960
  }
874
961
 
875
- export function projectActionRequestDetail(action, run, history) {
876
- if (!runMatchesActionIdentity(run, action)) return null;
962
+ export function projectActionRequestDetail(action, run, history, runs = [run]) {
963
+ if (!actionThreadIncludesRun(action, runs, run?.id)) return null;
877
964
  const turn = Array.isArray(history?.turns) ? history.turns[0] : null;
878
965
  if (!turn) return null;
879
966
  const sourceLoops = Array.isArray(history?.loops) ? history.loops : [];
@@ -1,7 +1,6 @@
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';
5
4
  import { WorkItemStore } from './store.js';
6
5
  import { WorkflowController } from './controller.js';
7
6
  import { WorkItemWatcher } from './watcher.js';
@@ -143,8 +142,7 @@ export class WorkCenterService {
143
142
  const detail = this.#requiredItem(payload.id);
144
143
  const action = this.#requiredAction(detail, payload.actionId);
145
144
  const entries = [];
146
- for (const run of detail.runs.filter(item => item.actionId === action.id
147
- && runMatchesActionIdentity(item, action))) {
145
+ for (const run of detail.runs.filter(item => item.actionId === action.id)) {
148
146
  const history = await this.#debugHistory(run, { indexOnly: true });
149
147
  for (const turn of Array.isArray(history?.turns) ? history.turns : []) {
150
148
  entries.push({ run, turn });
@@ -156,11 +154,10 @@ export class WorkCenterService {
156
154
  const detail = this.#requiredItem(payload.id);
157
155
  const action = this.#requiredAction(detail, payload.actionId);
158
156
  const requestId = requiredString(payload.requestId, 'requestId');
159
- const run = detail.runs.find(item => item.actionId === action.id
160
- && item.id === payload.runId && runMatchesActionIdentity(item, action));
157
+ const run = detail.runs.find(item => item.actionId === action.id && item.id === payload.runId);
161
158
  if (!run) throw new Error('Action request not found');
162
159
  const history = await this.#debugHistory(run, { detailTurnId: requestId });
163
- const projected = projectActionRequestDetail(action, run, history);
160
+ const projected = projectActionRequestDetail(action, run, history, detail.runs);
164
161
  if (!projected) throw new Error('Action request detail is no longer available');
165
162
  return projected;
166
163
  }
@@ -158,6 +158,7 @@ function mapAction(row) {
158
158
  contractRevision: row.contract_revision,
159
159
  generation: Math.max(1, Number(row.generation) || 1),
160
160
  specHash: row.spec_hash || '',
161
+ identityHistory: parseJson(row.identity_history, []),
161
162
  resultRunId: row.result_run_id || null,
162
163
  status: row.status,
163
164
  attempt: row.attempt,
@@ -170,6 +171,19 @@ function mapAction(row) {
170
171
  };
171
172
  }
172
173
 
174
+ function actionIdentityHistory(action, generation = action?.generation, specHash = action?.specHash) {
175
+ const byGeneration = new Map();
176
+ for (const identity of Array.isArray(action?.identityHistory) ? action.identityHistory : []) {
177
+ const value = Math.max(1, Number(identity?.generation) || 1);
178
+ if (typeof identity?.specHash === 'string' && identity.specHash) {
179
+ byGeneration.set(value, { generation: value, specHash: identity.specHash });
180
+ }
181
+ }
182
+ const value = Math.max(1, Number(generation) || 1);
183
+ if (typeof specHash === 'string' && specHash) byGeneration.set(value, { generation: value, specHash });
184
+ return [...byGeneration.values()].sort((left, right) => left.generation - right.generation).slice(-100);
185
+ }
186
+
173
187
  function mapRun(row) {
174
188
  if (!row) return null;
175
189
  return {
@@ -324,6 +338,7 @@ export class WorkItemStore {
324
338
  contract_revision INTEGER NOT NULL DEFAULT 1,
325
339
  generation INTEGER NOT NULL DEFAULT 1,
326
340
  spec_hash TEXT NOT NULL DEFAULT '',
341
+ identity_history TEXT NOT NULL DEFAULT '[]',
327
342
  result_run_id TEXT,
328
343
  status TEXT NOT NULL,
329
344
  attempt INTEGER NOT NULL DEFAULT 0,
@@ -549,6 +564,16 @@ export class WorkItemStore {
549
564
  if (!hasColumn(this.db, 'actions', 'spec_hash')) {
550
565
  this.db.exec("ALTER TABLE actions ADD COLUMN spec_hash TEXT NOT NULL DEFAULT ''");
551
566
  }
567
+ if (!hasColumn(this.db, 'actions', 'identity_history')) {
568
+ this.db.exec("ALTER TABLE actions ADD COLUMN identity_history TEXT NOT NULL DEFAULT '[]'");
569
+ const seedIdentity = this.db.prepare('UPDATE actions SET identity_history = ? WHERE id = ?');
570
+ for (const row of this.db.prepare('SELECT id, generation, spec_hash FROM actions').all()) {
571
+ seedIdentity.run(stringify(actionIdentityHistory({
572
+ generation: row.generation,
573
+ specHash: row.spec_hash,
574
+ })), row.id);
575
+ }
576
+ }
552
577
  if (!hasColumn(this.db, 'actions', 'result_run_id')) {
553
578
  this.db.exec('ALTER TABLE actions ADD COLUMN result_run_id TEXT');
554
579
  }
@@ -838,12 +863,13 @@ export class WorkItemStore {
838
863
  updatedAt: now,
839
864
  };
840
865
  action.specHash = actionSpecHash(action);
866
+ action.identityHistory = actionIdentityHistory(action);
841
867
  this.db.prepare(`INSERT INTO actions
842
868
  (id, work_item_id, sequence, type, required_role, stage_id, assignment_policy, model_policy,
843
869
  depends_on_stage_ids, workspace_mode, changes_requested_stage_id, workspace, instruction, brief, context, contract_revision,
844
- generation, spec_hash, result_run_id, status, attempt, max_attempts, current_run_id, lease_epoch,
870
+ generation, spec_hash, identity_history, result_run_id, status, attempt, max_attempts, current_run_id, lease_epoch,
845
871
  replaces_action_id, created_at, updated_at)
846
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NULL, 0, ?, ?, ?)`).run(
872
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NULL, 0, ?, ?, ?)`).run(
847
873
  action.id,
848
874
  workItemId,
849
875
  action.sequence,
@@ -862,6 +888,7 @@ export class WorkItemStore {
862
888
  action.contractRevision,
863
889
  action.generation,
864
890
  action.specHash,
891
+ stringify(action.identityHistory),
865
892
  action.resultRunId,
866
893
  action.status,
867
894
  action.attempt,
@@ -926,10 +953,13 @@ export class WorkItemStore {
926
953
  const nextAction = action.id === target.id && replacement
927
954
  ? { ...action, ...replacement, generation: action.generation + 1 }
928
955
  : { ...action, generation: action.generation + 1 };
956
+ const nextSpecHash = actionSpecHash(nextAction);
929
957
  this.db.prepare(`UPDATE actions SET status = 'ready', attempt = 0, current_run_id = NULL,
930
- lease_epoch = ?, generation = generation + 1, spec_hash = ?, result_run_id = NULL,
958
+ lease_epoch = ?, generation = generation + 1, spec_hash = ?, identity_history = ?, result_run_id = NULL,
931
959
  workspace = ?, updated_at = ? WHERE id = ?`).run(
932
- nextEpoch.get(action.id), actionSpecHash(nextAction), stringify(workspace), now, action.id,
960
+ nextEpoch.get(action.id), nextSpecHash,
961
+ stringify(actionIdentityHistory(action, nextAction.generation, nextSpecHash)),
962
+ stringify(workspace), now, action.id,
933
963
  );
934
964
  }
935
965
  }
@@ -976,13 +1006,18 @@ export class WorkItemStore {
976
1006
  const nextWorkspaceMode = workspaceMode || action.workspaceMode;
977
1007
  const specChanged = nextWorkspaceMode !== action.workspaceMode;
978
1008
  const nextAction = { ...action, workspaceMode: nextWorkspaceMode };
1009
+ const nextSpecHash = specChanged ? actionSpecHash(nextAction) : action.specHash;
979
1010
  const changed = this.db.prepare(`UPDATE actions SET workspace = ?, workspace_mode = ?,
980
- generation = generation + ?, spec_hash = ?, result_run_id = CASE WHEN ? = 1 THEN NULL ELSE result_run_id END,
1011
+ generation = generation + ?, spec_hash = ?, identity_history = ?,
1012
+ result_run_id = CASE WHEN ? = 1 THEN NULL ELSE result_run_id END,
981
1013
  updated_at = ? WHERE id = ? AND generation = ?`).run(
982
1014
  stringify(workspace),
983
1015
  nextWorkspaceMode,
984
1016
  specChanged ? 1 : 0,
985
- specChanged ? actionSpecHash(nextAction) : action.specHash,
1017
+ nextSpecHash,
1018
+ stringify(specChanged
1019
+ ? actionIdentityHistory(action, action.generation + 1, nextSpecHash)
1020
+ : action.identityHistory),
986
1021
  specChanged ? 1 : 0,
987
1022
  this.now(),
988
1023
  actionId,
@@ -1027,13 +1062,17 @@ export class WorkItemStore {
1027
1062
  }
1028
1063
  }
1029
1064
  const changed = this.db.prepare(`UPDATE actions SET workspace = ?, workspace_mode = ?,
1030
- generation = generation + ?, spec_hash = ?, result_run_id = CASE WHEN ? = 1 THEN NULL ELSE result_run_id END,
1065
+ generation = generation + ?, spec_hash = ?, identity_history = ?,
1066
+ result_run_id = CASE WHEN ? = 1 THEN NULL ELSE result_run_id END,
1031
1067
  updated_at = ? WHERE id = ? AND status = 'running' AND current_run_id = ?
1032
1068
  AND lease_epoch = ? AND generation = ?`).run(
1033
1069
  stringify(workspace),
1034
1070
  nextWorkspaceMode,
1035
1071
  specChanged ? 1 : 0,
1036
1072
  nextSpecHash,
1073
+ stringify(specChanged
1074
+ ? actionIdentityHistory(action, nextGeneration, nextSpecHash)
1075
+ : action.identityHistory),
1037
1076
  specChanged ? 1 : 0,
1038
1077
  now,
1039
1078
  actionId,
@@ -1067,11 +1106,13 @@ export class WorkItemStore {
1067
1106
  for (const row of pendingRows) {
1068
1107
  const pending = mapAction(row);
1069
1108
  const fallback = { ...pending, workspaceMode: 'shared', workspace: null };
1109
+ const fallbackSpecHash = actionSpecHash(fallback);
1070
1110
  const repaired = this.db.prepare(`UPDATE actions SET workspace = NULL, workspace_mode = 'shared',
1071
- generation = generation + 1, spec_hash = ?, result_run_id = NULL, updated_at = ?
1111
+ generation = generation + 1, spec_hash = ?, identity_history = ?, result_run_id = NULL, updated_at = ?
1072
1112
  WHERE id = ? AND status = 'ready' AND current_run_id IS NULL
1073
1113
  AND generation = ? AND workspace_mode = ?`).run(
1074
- actionSpecHash(fallback),
1114
+ fallbackSpecHash,
1115
+ stringify(actionIdentityHistory(pending, pending.generation + 1, fallbackSpecHash)),
1075
1116
  now,
1076
1117
  pending.id,
1077
1118
  pending.generation,
@@ -1406,12 +1447,18 @@ export class WorkItemStore {
1406
1447
  for (const action of openActions) {
1407
1448
  if (action.status === 'ready') {
1408
1449
  const instruction = updateActionInstruction(updatedWorkItem, action);
1409
- this.db.prepare(`UPDATE actions SET instruction = ?, spec_hash = ?, updated_at = ?
1410
- WHERE id = ? AND status = 'ready'`).run(
1450
+ const nextGeneration = action.generation + 1;
1451
+ const nextSpecHash = actionSpecHash({ ...action, instruction, generation: nextGeneration });
1452
+ this.db.prepare(`UPDATE actions SET instruction = ?, generation = ?, spec_hash = ?,
1453
+ identity_history = ?, result_run_id = NULL, updated_at = ?
1454
+ WHERE id = ? AND status = 'ready' AND generation = ?`).run(
1411
1455
  instruction,
1412
- actionSpecHash({ ...action, instruction }),
1456
+ nextGeneration,
1457
+ nextSpecHash,
1458
+ stringify(actionIdentityHistory(action, nextGeneration, nextSpecHash)),
1413
1459
  now,
1414
1460
  action.id,
1461
+ action.generation,
1415
1462
  );
1416
1463
  continue;
1417
1464
  }
@@ -2210,10 +2257,11 @@ export class WorkItemStore {
2210
2257
  resultRunId: null,
2211
2258
  contractRevision: nextWorkItem.revision,
2212
2259
  };
2260
+ const candidateSpecHash = actionSpecHash(candidate);
2213
2261
  const changed = this.db.prepare(`UPDATE actions SET type = ?, required_role = ?, stage_id = ?,
2214
2262
  assignment_policy = ?, model_policy = ?, depends_on_stage_ids = ?, workspace_mode = ?,
2215
2263
  changes_requested_stage_id = ?, workspace = NULL, instruction = ?, brief = ?, context = ?,
2216
- contract_revision = ?, generation = ?, spec_hash = ?, result_run_id = NULL, status = 'ready',
2264
+ contract_revision = ?, generation = ?, spec_hash = ?, identity_history = ?, result_run_id = NULL, status = 'ready',
2217
2265
  attempt = 0, max_attempts = ?, current_run_id = NULL, lease_epoch = lease_epoch + 1,
2218
2266
  updated_at = ? WHERE id = ? AND work_item_id = ? AND status = 'superseded' AND generation = ?`).run(
2219
2267
  candidate.type, candidate.requiredRole || '', candidate.stageId,
@@ -2221,7 +2269,8 @@ export class WorkItemStore {
2221
2269
  stringify(candidate.dependsOnStageIds || []), candidate.workspaceMode || 'shared',
2222
2270
  candidate.changesRequestedStageId || null, candidate.instruction || '', stringify(candidate.brief || null),
2223
2271
  stringify(candidate.context || []), candidate.contractRevision, candidate.generation,
2224
- actionSpecHash(candidate), candidate.maxAttempts || 2, now,
2272
+ candidateSpecHash, stringify(actionIdentityHistory(prior, candidate.generation, candidateSpecHash)),
2273
+ candidate.maxAttempts || 2, now,
2225
2274
  prior.id, workItem.id, prior.generation,
2226
2275
  );
2227
2276
  if (Number(changed.changes) !== 1) throw new Error('Work Center retained Action lost its superseded identity fence');