@yeaft/webchat-agent 1.0.217 → 1.0.218

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.
@@ -1 +1 @@
1
- {"version":"1.0.217"}
1
+ {"version":"1.0.218"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yeaft/webchat-agent",
3
- "version": "1.0.217",
3
+ "version": "1.0.218",
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",
@@ -8,7 +8,7 @@ import {
8
8
  } from './workflow.js';
9
9
  import { renderSessionContextSnapshot } from './session-context.js';
10
10
  import { normalizeEvidence } from './evidence.js';
11
- import { applyAdditivePlanProposal } from './plan-mutation.js';
11
+ import { applyAdditivePlanProposal, applyReplanMutation } from './plan-mutation.js';
12
12
  import { normalizeContractPatch, validateCompletedResult } from './completion-contract.js';
13
13
 
14
14
  function normalizeTerminalResult(result, action) {
@@ -46,6 +46,8 @@ function normalizeTerminalResult(result, action) {
46
46
  && !Array.isArray(result.planProposal) ? result.planProposal : null,
47
47
  replanRequest: result.replanRequest && typeof result.replanRequest === 'object'
48
48
  && !Array.isArray(result.replanRequest) ? result.replanRequest : null,
49
+ replanMutation: result.replanMutation && typeof result.replanMutation === 'object'
50
+ && !Array.isArray(result.replanMutation) ? result.replanMutation : null,
49
51
  };
50
52
  if (normalized.outcome === 'waiting' && !normalized.waitingReason) {
51
53
  throw new Error('waiting outcome requires waitingReason');
@@ -58,12 +60,14 @@ function normalizeTerminalResult(result, action) {
58
60
  if (normalized.outcome !== 'completed') {
59
61
  normalized.planProposal = null;
60
62
  normalized.replanRequest = null;
63
+ normalized.replanMutation = null;
61
64
  }
62
- if (normalized.planProposal && normalized.replanRequest) {
65
+ if ([normalized.planProposal, normalized.replanRequest, normalized.replanMutation].filter(Boolean).length > 1) {
63
66
  normalized.outcome = 'failed';
64
- normalized.error = 'An Action cannot expand and replan the WorkItem in the same completion';
67
+ normalized.error = 'An Action cannot submit more than one WorkItem plan mutation';
65
68
  normalized.planProposal = null;
66
69
  normalized.replanRequest = null;
70
+ normalized.replanMutation = null;
67
71
  }
68
72
  if (action.type === 'review' && normalized.outcome === 'completed' && !normalized.reviewDecision) {
69
73
  normalized.outcome = 'failed';
@@ -323,10 +327,17 @@ export class WorkflowController {
323
327
  throw new Error('Run has unconsumed Action input and cannot finish yet');
324
328
  }
325
329
  const result = normalizeTerminalResult(rawResult, activeAction);
330
+ if (result.outcome === 'completed'
331
+ && activeAction.stageId?.startsWith('replan-')
332
+ && !result.replanMutation) {
333
+ result.outcome = 'failed';
334
+ result.error = 'Work Center replan triage must submit SubmitWorkItemReplan';
335
+ }
326
336
  validateCompletedResult(result, activeAction, activeWorkItem);
327
337
  let validatedGeneratedWorkflow = null;
328
338
  if (result.outcome === 'completed'
329
339
  && activeAction.type === 'triage'
340
+ && !activeAction.stageId?.startsWith('replan-')
330
341
  && activeRun
331
342
  && this.store.getWorkItem(activeRun.workItemId)?.workflowSnapshot?.planningMode === 'ai') {
332
343
  const current = this.store.getWorkItem(activeRun.workItemId);
@@ -364,6 +375,27 @@ export class WorkflowController {
364
375
  result.error = error?.message || String(error);
365
376
  }
366
377
  }
378
+ let validatedReplanMutation = null;
379
+ let staleReplanMutation = null;
380
+ if (result.outcome === 'completed' && result.replanMutation) {
381
+ const currentWorkItem = this.store.getWorkItem(activeWorkItem.id);
382
+ if (Number(result.replanMutation.basePlanRevision) !== currentWorkItem.planRevision) {
383
+ staleReplanMutation = result.replanMutation;
384
+ } else {
385
+ try {
386
+ validatedReplanMutation = applyReplanMutation({
387
+ workItem: currentWorkItem,
388
+ action: activeAction,
389
+ actions: this.store.getWorkItemDetail(activeWorkItem.id).actions,
390
+ proposal: result.replanMutation,
391
+ availableVpIds: this.listAvailableVpIds?.(),
392
+ });
393
+ } catch (error) {
394
+ result.outcome = 'failed';
395
+ result.error = error?.message || String(error);
396
+ }
397
+ }
398
+ }
367
399
  if (result.outcome === 'completed' && result.replanRequest) {
368
400
  const basePlanRevision = Number(result.replanRequest.basePlanRevision);
369
401
  const proposalId = typeof result.replanRequest.proposalId === 'string'
@@ -451,6 +483,36 @@ export class WorkflowController {
451
483
  : effectiveWorkItem;
452
484
  const context = [...(action.context || []), contextEntry(action, result, activeRun)];
453
485
  if (plannedWorkItem.workflowSnapshot?.executionMode === 'graph') {
486
+ if (staleReplanMutation) {
487
+ return {
488
+ actionStatus: 'completed', workItemStatus: 'needs_attention', graphAdvance: false,
489
+ keepCurrentAction: true,
490
+ planConflict: {
491
+ kind: 'plan_revision',
492
+ proposalId: staleReplanMutation.proposalId,
493
+ expectedPlanRevision: staleReplanMutation.basePlanRevision,
494
+ actualPlanRevision: workItem.planRevision,
495
+ },
496
+ eventType: 'workflow.plan_conflict',
497
+ eventData: { proposalId: staleReplanMutation.proposalId },
498
+ };
499
+ }
500
+ if (validatedReplanMutation) {
501
+ return {
502
+ actionStatus: 'completed', workItemStatus: 'ready', graphAdvance: true,
503
+ workflowSnapshot: validatedReplanMutation.workflowSnapshot,
504
+ expectedPlanRevision: validatedReplanMutation.basePlanRevision,
505
+ proposalId: validatedReplanMutation.proposalId,
506
+ replanMutation: validatedReplanMutation,
507
+ eventType: 'workflow.replanned',
508
+ eventData: {
509
+ retainedActionCount: validatedReplanMutation.retain.length,
510
+ replacedActionCount: validatedReplanMutation.replace.length,
511
+ removedActionCount: validatedReplanMutation.remove.length,
512
+ addedActionCount: validatedReplanMutation.add.length,
513
+ },
514
+ };
515
+ }
454
516
  if (result.replanRequest) {
455
517
  const replanStage = {
456
518
  ...plannedWorkItem.workflowSnapshot.stages[0],
@@ -12,6 +12,11 @@ function cleanProposalId(value) {
12
12
  return id;
13
13
  }
14
14
 
15
+ function replanBarrierFrom(action) {
16
+ return (Array.isArray(action?.context) ? action.context : [])
17
+ .find(entry => entry?.type === 'replan-barrier') || null;
18
+ }
19
+
15
20
  function planActionFromStage(stage) {
16
21
  return {
17
22
  id: stage.id,
@@ -183,3 +188,118 @@ export function applyAdditivePlanProposal({ workItem, actions, proposal, availab
183
188
  dependencyPatches,
184
189
  };
185
190
  }
191
+
192
+ export function applyReplanMutation({ workItem, action, actions, proposal, availableVpIds = null }) {
193
+ if (workItem.workflowSnapshot?.executionMode !== 'graph'
194
+ || action?.type !== 'triage'
195
+ || !action?.stageId?.startsWith('replan-')) {
196
+ throw new Error('Work Center replan mutation requires a replan triage Action');
197
+ }
198
+ if (!proposal || typeof proposal !== 'object' || Array.isArray(proposal)) {
199
+ throw new Error('Work Center replan mutation must be an object');
200
+ }
201
+ const proposalId = cleanProposalId(proposal.proposalId);
202
+ const basePlanRevision = Number(proposal.basePlanRevision);
203
+ if (!Number.isInteger(basePlanRevision) || basePlanRevision !== workItem.planRevision) {
204
+ throw new Error('Work Center replan mutation has a stale basePlanRevision');
205
+ }
206
+ const barrier = replanBarrierFrom(action);
207
+ if (!barrier || !Array.isArray(barrier.candidateActionIds)) {
208
+ throw new Error('Work Center replan Action is missing its frozen candidate set');
209
+ }
210
+ const candidateIds = barrier.candidateActionIds;
211
+ const actionById = new Map(actions.map(candidate => [candidate.id, candidate]));
212
+ const candidates = new Map(candidateIds.map(id => [id, actionById.get(id)]));
213
+ for (const [id, candidate] of candidates) {
214
+ if (!candidate || candidate.status !== 'superseded') {
215
+ throw new Error(`Work Center replan candidate is missing or no longer superseded: ${id}`);
216
+ }
217
+ }
218
+
219
+ const classified = new Set();
220
+ const classify = (actionId, kind) => {
221
+ const id = typeof actionId === 'string' ? actionId.trim() : '';
222
+ if (!candidates.has(id)) throw new Error(`Work Center replan ${kind} references a non-candidate Action: ${id || '(missing)'}`);
223
+ if (classified.has(id)) throw new Error(`Work Center replan candidate is classified more than once: ${id}`);
224
+ classified.add(id);
225
+ return candidates.get(id);
226
+ };
227
+ const retained = (Array.isArray(proposal.retain) ? proposal.retain : []).map(entry => ({
228
+ action: classify(entry?.actionId, 'retain'), input: entry?.action,
229
+ }));
230
+ const replaced = (Array.isArray(proposal.replace) ? proposal.replace : []).map(entry => ({
231
+ action: classify(entry?.actionId, 'replace'), input: entry?.action,
232
+ }));
233
+ const removed = (Array.isArray(proposal.remove) ? proposal.remove : []).map(id => classify(id, 'remove'));
234
+ const missing = candidateIds.filter(id => !classified.has(id));
235
+ if (missing.length > 0) throw new Error(`Work Center replan must classify every frozen candidate: ${missing.join(', ')}`);
236
+
237
+ const completed = actions.filter(candidate => candidate.status === 'completed' && candidate.type !== 'triage');
238
+ const currentStages = new Map((workItem.workflowSnapshot.stages || []).map(stage => [stage.id, stage]));
239
+ const historicalStageIds = new Set(actions.map(candidate => candidate.stageId));
240
+ const futureIds = new Set();
241
+ const canonicalFuture = (raw, expectedId = null) => {
242
+ if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {
243
+ throw new Error('Work Center replan classification requires a full Action specification');
244
+ }
245
+ const id = canonicalActionId(raw.id);
246
+ if (!id || (expectedId && id !== expectedId)) {
247
+ throw new Error(`Work Center retained Action must keep stage identity: ${expectedId || '(missing)'}`);
248
+ }
249
+ if (futureIds.has(id)) throw new Error(`Work Center replan Action id is duplicated: ${id}`);
250
+ futureIds.add(id);
251
+ return {
252
+ ...raw,
253
+ id,
254
+ dependsOnActionIds: canonicalExplicitActionIds(raw.dependsOnActionIds, `Action "${id}" dependencies`),
255
+ changesRequestedActionId: Object.hasOwn(raw, 'changesRequestedActionId')
256
+ ? canonicalExplicitActionId(raw.changesRequestedActionId, `Action "${id}" review target`)
257
+ : undefined,
258
+ };
259
+ };
260
+ const retainedInputs = retained.map(entry => canonicalFuture(entry.input, entry.action.stageId));
261
+ const replacementInputs = replaced.map(entry => {
262
+ const input = canonicalFuture(entry.input);
263
+ if (historicalStageIds.has(input.id)) throw new Error(`Work Center replacement Action reuses historical stage identity: ${input.id}`);
264
+ return input;
265
+ });
266
+ const addedInputs = (Array.isArray(proposal.add) ? proposal.add : []).map(raw => {
267
+ const input = canonicalFuture(raw);
268
+ if (historicalStageIds.has(input.id)) throw new Error(`Work Center added Action reuses historical stage identity: ${input.id}`);
269
+ return input;
270
+ });
271
+ const completedInputs = completed.map(candidate => {
272
+ const stage = currentStages.get(candidate.stageId);
273
+ if (!stage) throw new Error(`Work Center completed Action is missing from the frozen workflow: ${candidate.stageId}`);
274
+ return planActionFromStage(stage);
275
+ });
276
+ const synthetic = {
277
+ ...workItem,
278
+ workflowSnapshot: { ...workItem.workflowSnapshot, actionTemplates: [], stages: [workItem.workflowSnapshot.stages[0]] },
279
+ };
280
+ const workflowSnapshot = applyGeneratedPlan(synthetic, {
281
+ workItemType: workItem.workflowSnapshot.workItemType,
282
+ actions: stableTopologicalActions([...completedInputs, ...retainedInputs, ...replacementInputs, ...addedInputs]),
283
+ }, { availableVpIds });
284
+ const stageById = new Map(workflowSnapshot.stages.map(stage => [stage.id, stage]));
285
+ const context = (Array.isArray(action.context) ? action.context : [])
286
+ .filter(entry => entry?.type !== 'replan-barrier');
287
+ return {
288
+ proposalId,
289
+ basePlanRevision,
290
+ workflowSnapshot,
291
+ retain: retained.map(entry => ({
292
+ action: entry.action,
293
+ nextAction: actionForStage(stageById.get(entry.action.stageId), { ...workItem, workflowSnapshot }, context),
294
+ })),
295
+ replace: replaced.map((entry, index) => ({
296
+ action: entry.action,
297
+ nextAction: {
298
+ ...actionForStage(stageById.get(replacementInputs[index].id), { ...workItem, workflowSnapshot }, context),
299
+ replacesActionId: entry.action.id,
300
+ },
301
+ })),
302
+ add: addedInputs.map(input => actionForStage(stageById.get(input.id), { ...workItem, workflowSnapshot }, context)),
303
+ remove: removed.map(candidate => candidate.id),
304
+ };
305
+ }
@@ -437,6 +437,52 @@ export function createRequestWorkItemReplanTool({ workItem, collector, isRunActi
437
437
  });
438
438
  }
439
439
 
440
+ export function createSubmitWorkItemReplanTool({ vps, workItem, action, actions, collector, isRunActive }) {
441
+ const vpCatalog = planningVpCatalog(vps);
442
+ const vpIds = vpCatalog.map(vp => vp.id);
443
+ const barrier = (Array.isArray(action.context) ? action.context : [])
444
+ .find(entry => entry?.type === 'replan-barrier');
445
+ const candidateIds = Array.isArray(barrier?.candidateActionIds) ? barrier.candidateActionIds : [];
446
+ const actionById = new Map(actions.map(candidate => [candidate.id, candidate]));
447
+ const candidateSummary = candidateIds.map(id => {
448
+ const candidate = actionById.get(id);
449
+ return `${id}/${candidate?.stageId || 'missing'} (${candidate?.type || 'unknown'})`;
450
+ }).join('; ');
451
+ const candidateIdSchema = candidateIds.length > 0
452
+ ? { type: 'string', enum: candidateIds }
453
+ : { type: 'string' };
454
+ const candidateLimit = Math.min(8, candidateIds.length);
455
+ const classification = { type: 'object', additionalProperties: false,
456
+ required: ['actionId', 'action'], properties: {
457
+ actionId: candidateIdSchema,
458
+ action: plannedActionSchema(vpIds),
459
+ } };
460
+ return defineTool({
461
+ name: 'SubmitWorkItemReplan',
462
+ description: `Submit the complete replacement topology after a replan barrier. Classify every frozen candidate exactly once as retain, replace, or remove. Retain keeps its database Action identity and stage id but requires the complete updated specification. Replace creates a new Action linked to the old database Action. Add is only for new work. Frozen candidates: ${candidateSummary}. Available VPs: ${vpCatalog.map(vp => vp.id).join(', ')}.`,
463
+ parameters: { type: 'object', additionalProperties: false,
464
+ required: ['summary', 'evidence', 'acceptanceChecks', 'proposalId', 'basePlanRevision', 'retain', 'replace', 'remove', 'add'],
465
+ properties: {
466
+ ...terminalPlanningFields(),
467
+ proposalId: { type: 'string', minLength: 1, maxLength: 128 },
468
+ basePlanRevision: { type: 'integer', const: workItem.planRevision },
469
+ retain: { type: 'array', maxItems: candidateLimit, items: classification },
470
+ replace: { type: 'array', maxItems: candidateLimit, items: classification },
471
+ remove: { type: 'array', maxItems: candidateLimit, uniqueItems: true, items: candidateIdSchema },
472
+ add: { type: 'array', maxItems: 8, items: plannedActionSchema(vpIds) },
473
+ } },
474
+ async execute(input, ctx = {}) {
475
+ if (!isRunActive()) throw new Error('Work Center Run is no longer active');
476
+ if (collector.value) throw new Error('A WorkItem plan was already submitted for this Run');
477
+ collector.value = structuredClone(input);
478
+ ctx.requestEndTurn?.({ kind: 'work_item_replan_submitted', proposalId: input.proposalId });
479
+ return JSON.stringify({ submitted: true, proposalId: input.proposalId });
480
+ },
481
+ isConcurrencySafe: () => false,
482
+ isReadOnly: () => false,
483
+ });
484
+ }
485
+
440
486
  export function createWorkItemToolRegistry({ workDir, attachmentFiles = [], isRunActive, mcpTools = [], runTools = [] }) {
441
487
  const canonicalDir = canonicalWorkDir(path.resolve(workDir));
442
488
  const canonicalAttachmentFiles = attachmentFiles.map(file => ({
@@ -504,11 +550,15 @@ function completionContract(action, workItem) {
504
550
  const triageField = action.type === 'triage'
505
551
  ? ',\n "contractPatch": { "goal": "optional refined goal", "acceptanceCriteria": ["optional refined criterion"] }'
506
552
  : '';
507
- const planField = action.type === 'triage' && workItem?.workflowSnapshot?.planningMode === 'ai'
553
+ const planField = action.type === 'triage'
554
+ && !action.stageId?.startsWith('replan-')
555
+ && workItem?.workflowSnapshot?.planningMode === 'ai'
508
556
  ? ',\n "plan": { "workItemType": "specific-lowercase-slug", "actions": [{ "id": "stable-id", "name": "User-facing name", "type": "extensible-lowercase-slug (built-ins include research|design|diagnose|implement|migrate|test|review|document|operate|deliver|integrate|write|custom)", "capability": "specific executor capability", "objective": "task-specific concrete work this Action must do", "approach": "task-specific repository-aware method the executor must follow", "expectedOutcome": "task-specific verifiable result this Action must produce", "dependsOnActionIds": ["earlier Action id; [] means concurrent root"], "workspaceMode": "read|isolated-write|integrate|shared", "separateFromActionTypes": ["optional prior Action type"], "changesRequestedActionId": "for review: optional earlier editable Action id; omit to use nearest", "maxAttempts": 2 }] }'
509
557
  : '';
510
- const toolSubmission = action.type === 'triage' && workItem?.workflowSnapshot?.planningMode === 'ai'
511
- ? '\nSubmit the initial plan with SubmitWorkItemPlan. The legacy terminal JSON plan below exists only for compatibility; do not use it when the tool is available.'
558
+ const toolSubmission = action.type === 'triage' && action.stageId?.startsWith('replan-')
559
+ ? '\nSubmit the replan only with SubmitWorkItemReplan. Classify every frozen candidate exactly once; do not emit terminal JSON after calling it.'
560
+ : action.type === 'triage' && workItem?.workflowSnapshot?.planningMode === 'ai'
561
+ ? '\nSubmit the initial plan with SubmitWorkItemPlan. The legacy terminal JSON plan below exists only for compatibility; do not use it when the tool is available.'
512
562
  : workItem?.workflowSnapshot?.executionMode === 'graph'
513
563
  ? '\nIf execution discovered strictly additive work, use ProposeWorkItemActions. If the contract or existing unfinished topology must change, use RequestWorkItemReplan. Both tools submit the completed Action and end the turn; do not emit terminal JSON after calling one.'
514
564
  : '';
@@ -901,19 +951,29 @@ export class WorkItemRunner {
901
951
  const mcpToolNames = workspaceRuntime.mcpTools.map(tool => tool.name);
902
952
  const planCollector = { value: null };
903
953
  const mutationCollector = { value: null };
954
+ const replanToolEnabled = executionAction.type === 'triage'
955
+ && executionAction.stageId?.startsWith('replan-');
904
956
  const planToolEnabled = executionAction.type === 'triage'
905
- && workItem?.workflowSnapshot?.planningMode === 'ai';
957
+ && workItem?.workflowSnapshot?.planningMode === 'ai'
958
+ && !replanToolEnabled;
906
959
  const runTools = [];
907
960
  if (planToolEnabled) runTools.push(createSubmitWorkItemPlanTool({
908
961
  vps: this.registry.listVps(),
909
962
  workItem,
910
963
  collector: planCollector,
911
964
  isRunActive,
912
- reservedStageIds: executionAction.stageId?.startsWith('replan-')
913
- ? this.store.getWorkItemDetail(workItem.id).actions.map(item => item.stageId)
914
- : [],
965
+ reservedStageIds: [],
966
+ }));
967
+ if (replanToolEnabled) runTools.push(createSubmitWorkItemReplanTool({
968
+ vps: this.registry.listVps(),
969
+ workItem,
970
+ action: executionAction,
971
+ actions: this.store.getWorkItemDetail(workItem.id).actions,
972
+ collector: planCollector,
973
+ isRunActive,
915
974
  }));
916
- if (!planToolEnabled && workItem?.workflowSnapshot?.executionMode === 'graph') {
975
+ if (!planToolEnabled && !replanToolEnabled
976
+ && workItem?.workflowSnapshot?.executionMode === 'graph') {
917
977
  runTools.push(createProposeWorkItemActionsTool({
918
978
  vps: this.registry.listVps(), workItem,
919
979
  actions: this.store.getWorkItemDetail(workItem.id).actions,
@@ -1125,7 +1185,8 @@ export class WorkItemRunner {
1125
1185
  }
1126
1186
  const response = publicWorkItemResponse(text);
1127
1187
  reportProgress(true);
1128
- const submittedPlan = planCollector.value;
1188
+ const submittedPlan = !replanToolEnabled ? planCollector.value : null;
1189
+ const submittedReplanMutation = replanToolEnabled ? planCollector.value : null;
1129
1190
  const submittedExpansion = mutationCollector.value?.kind === 'expand'
1130
1191
  ? mutationCollector.value.input : null;
1131
1192
  const submittedReplan = mutationCollector.value?.kind === 'replan'
@@ -1137,6 +1198,19 @@ export class WorkItemRunner {
1137
1198
  contractPatch: submittedPlan.contractPatch || null,
1138
1199
  plan: { workItemType: submittedPlan.workItemType, actions: submittedPlan.actions },
1139
1200
  acceptanceChecks: submittedPlan.acceptanceChecks,
1201
+ } : submittedReplanMutation ? {
1202
+ outcome: 'completed',
1203
+ summary: submittedReplanMutation.summary,
1204
+ evidence: submittedReplanMutation.evidence,
1205
+ acceptanceChecks: submittedReplanMutation.acceptanceChecks,
1206
+ replanMutation: {
1207
+ proposalId: submittedReplanMutation.proposalId,
1208
+ basePlanRevision: submittedReplanMutation.basePlanRevision,
1209
+ retain: submittedReplanMutation.retain,
1210
+ replace: submittedReplanMutation.replace,
1211
+ remove: submittedReplanMutation.remove,
1212
+ add: submittedReplanMutation.add,
1213
+ },
1140
1214
  } : submittedExpansion ? {
1141
1215
  outcome: 'completed', summary: submittedExpansion.summary,
1142
1216
  evidence: submittedExpansion.evidence, acceptanceChecks: submittedExpansion.acceptanceChecks,
@@ -2094,6 +2094,15 @@ export class WorkItemStore {
2094
2094
  throw new Error('Work Center terminal transition lost the current Action fence');
2095
2095
  }
2096
2096
 
2097
+ if (transition.planConflict) {
2098
+ this.db.prepare(`INSERT INTO plan_conflicts
2099
+ (id, work_item_id, action_id, generation, kind, status, details, created_at, updated_at, resolved_at)
2100
+ VALUES (?, ?, ?, ?, ?, 'open', ?, ?, ?, NULL)`).run(
2101
+ randomUUID(), workItem.id, action.id, action.generation,
2102
+ transition.planConflict.kind || 'plan', stringify(transition.planConflict), now, now,
2103
+ );
2104
+ }
2105
+
2097
2106
  let nextWorkItem = workItem;
2098
2107
  if (transition.contractPatch) {
2099
2108
  const patch = transition.contractPatch;
@@ -2175,10 +2184,66 @@ export class WorkItemStore {
2175
2184
  nextWorkItem = this.getWorkItem(workItem.id);
2176
2185
  nextAction = this.#insertAction(workItem.id, {
2177
2186
  ...barrier.action,
2187
+ context: [
2188
+ ...(Array.isArray(barrier.action.context) ? barrier.action.context : []),
2189
+ {
2190
+ type: 'replan-barrier',
2191
+ proposalId: barrier.proposalId,
2192
+ basePlanRevision: nextWorkItem.planRevision,
2193
+ candidateActionIds: unfinished.map(candidate => candidate.id),
2194
+ },
2195
+ ],
2178
2196
  contractRevision: nextWorkItem.revision,
2179
2197
  status: 'ready',
2180
2198
  }, this.#nextSequence(workItem.id), now);
2181
2199
  }
2200
+ if (transition.replanMutation) {
2201
+ for (const retained of transition.replanMutation.retain) {
2202
+ const prior = retained.action;
2203
+ const candidate = {
2204
+ ...prior,
2205
+ ...retained.nextAction,
2206
+ status: 'ready',
2207
+ generation: prior.generation + 1,
2208
+ attempt: 0,
2209
+ currentRunId: null,
2210
+ resultRunId: null,
2211
+ contractRevision: nextWorkItem.revision,
2212
+ };
2213
+ const changed = this.db.prepare(`UPDATE actions SET type = ?, required_role = ?, stage_id = ?,
2214
+ assignment_policy = ?, model_policy = ?, depends_on_stage_ids = ?, workspace_mode = ?,
2215
+ changes_requested_stage_id = ?, workspace = NULL, instruction = ?, brief = ?, context = ?,
2216
+ contract_revision = ?, generation = ?, spec_hash = ?, result_run_id = NULL, status = 'ready',
2217
+ attempt = 0, max_attempts = ?, current_run_id = NULL, lease_epoch = lease_epoch + 1,
2218
+ updated_at = ? WHERE id = ? AND work_item_id = ? AND status = 'superseded' AND generation = ?`).run(
2219
+ candidate.type, candidate.requiredRole || '', candidate.stageId,
2220
+ stringify(candidate.assignmentPolicy || null), stringify(candidate.modelPolicy || null),
2221
+ stringify(candidate.dependsOnStageIds || []), candidate.workspaceMode || 'shared',
2222
+ candidate.changesRequestedStageId || null, candidate.instruction || '', stringify(candidate.brief || null),
2223
+ stringify(candidate.context || []), candidate.contractRevision, candidate.generation,
2224
+ actionSpecHash(candidate), candidate.maxAttempts || 2, now,
2225
+ prior.id, workItem.id, prior.generation,
2226
+ );
2227
+ if (Number(changed.changes) !== 1) throw new Error('Work Center retained Action lost its superseded identity fence');
2228
+ if (!nextAction) nextAction = this.getAction(prior.id);
2229
+ }
2230
+ for (const replacement of transition.replanMutation.replace) {
2231
+ const inserted = this.#insertAction(workItem.id, {
2232
+ ...replacement.nextAction,
2233
+ contractRevision: nextWorkItem.revision,
2234
+ status: 'ready',
2235
+ }, this.#nextSequence(workItem.id), now);
2236
+ if (!nextAction) nextAction = inserted;
2237
+ }
2238
+ for (const added of transition.replanMutation.add) {
2239
+ const inserted = this.#insertAction(workItem.id, {
2240
+ ...added,
2241
+ contractRevision: nextWorkItem.revision,
2242
+ status: 'ready',
2243
+ }, this.#nextSequence(workItem.id), now);
2244
+ if (!nextAction) nextAction = inserted;
2245
+ }
2246
+ }
2182
2247
  if (transition.graphResetStageId) {
2183
2248
  nextAction = this.#resetGraphFromStage(
2184
2249
  workItem.id,
@@ -2205,7 +2270,13 @@ export class WorkItemStore {
2205
2270
  let workItemStatus = transition.workItemStatus;
2206
2271
  let currentActionId = nextAction?.id ?? (transition.keepCurrentAction ? action.id : null);
2207
2272
  let changedWorkItem;
2208
- if (transition.graphAdvance) {
2273
+ if (transition.planConflict) {
2274
+ changedWorkItem = this.db.prepare(`UPDATE work_items SET status = ?, current_action_id = ?,
2275
+ current_run_id = NULL, ledger_revision = ledger_revision + ?, updated_at = ?
2276
+ WHERE id = ? AND status IN ('ready', 'running', 'waiting', 'needs_attention') AND revision = ?`).run(
2277
+ workItemStatus, currentActionId, ledgerIncrement, now, workItem.id, nextWorkItem.revision,
2278
+ );
2279
+ } else if (transition.graphAdvance) {
2209
2280
  const graphState = this.#graphWorkItemState(workItem.id);
2210
2281
  workItemStatus = graphState.status;
2211
2282
  currentActionId = graphState.currentActionId;
@@ -2233,7 +2304,9 @@ export class WorkItemStore {
2233
2304
  (work_item_id, proposal_id, base_plan_revision, plan_revision, kind, action_id, run_id, data, created_at)
2234
2305
  VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(
2235
2306
  workItem.id, proposalId, workItem.planRevision, nextWorkItem.planRevision,
2236
- transition.replanBarrier ? 'replan' : (workItem.planRevision === 0 ? 'initial' : 'expand'),
2307
+ (transition.replanBarrier || transition.replanMutation)
2308
+ ? 'replan'
2309
+ : (workItem.planRevision === 0 ? 'initial' : 'expand'),
2237
2310
  action.id, runId, stringify(transition.eventData || {}), now,
2238
2311
  );
2239
2312
  } catch (error) {