@yeaft/webchat-agent 1.0.365 → 1.0.367

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,4 +1,5 @@
1
1
  import { createHash } from 'node:crypto';
2
+ import { isDynamicWorkItem } from './execution-mode.js';
2
3
  import {
3
4
  currentActionInputEventIds,
4
5
  eventMatchesActionGeneration,
@@ -281,6 +282,7 @@ export function buildMainlineProjection(detail) {
281
282
  const actions = (Array.isArray(detail.actions) ? detail.actions : []).slice().sort(stableActionOrder);
282
283
  const runs = Array.isArray(detail.runs) ? detail.runs : [];
283
284
  const activeActions = actions.filter(action => action.status !== 'superseded');
285
+ const dynamic = isDynamicWorkItem(detail);
284
286
  const completedStageIds = new Set(activeActions
285
287
  .filter(action => action.status === 'completed')
286
288
  .map(action => action.stageId));
@@ -292,10 +294,11 @@ export function buildMainlineProjection(detail) {
292
294
  generation: Math.max(1, count(action.generation) || 1),
293
295
  specHash: action.specHash || '',
294
296
  status: action.status,
295
- dependsOnStageIds: [...new Set(action.dependsOnStageIds || [])].sort(),
297
+ dependsOnStageIds: dynamic ? [] : [...new Set(action.dependsOnStageIds || [])].sort(),
298
+ sourceActionIds: dynamic ? [...new Set(action.sourceActionIds || [])].sort() : [],
296
299
  }));
297
300
  const frontier = nodes.filter(node => !CLOSED_ACTION_STATUSES.has(node.status)
298
- && node.dependsOnStageIds.every(stageId => completedStageIds.has(stageId)))
301
+ && (dynamic || node.dependsOnStageIds.every(stageId => completedStageIds.has(stageId))))
299
302
  .map(node => node.id);
300
303
  const canonicalActionResults = {};
301
304
  for (const action of activeActions) {
@@ -321,7 +324,9 @@ export function buildMainlineProjection(detail) {
321
324
  goal: detail.goal || '',
322
325
  acceptanceCriteria: Array.isArray(detail.acceptanceCriteria) ? detail.acceptanceCriteria : [],
323
326
  },
324
- graph: { planRevision: count(detail.planRevision), nodes, frontier },
327
+ ...(dynamic
328
+ ? { actionJournal: { revision: count(detail.planRevision), entries: nodes, runnableActionIds: frontier } }
329
+ : { graph: { planRevision: count(detail.planRevision), nodes, frontier } }),
325
330
  canonicalActionResults,
326
331
  planConflicts: (Array.isArray(detail.planConflicts) ? detail.planConflicts : [])
327
332
  .slice()
@@ -343,14 +348,17 @@ export function buildMainlineContextSnapshot(detail, action, budgetInput = {}) {
343
348
  throw mainlineContextBlocked(`Mainline fixed prompt content exceeds 64 KiB (${reservedBytes} rendered UTF-8 bytes)`);
344
349
  }
345
350
  const projection = buildMainlineProjection(detail);
346
- const dependencyIds = new Set(action.dependsOnStageIds || []);
347
- const actionByStage = new Map((detail.actions || []).filter(candidate => candidate.status !== 'superseded')
348
- .map(candidate => [candidate.stageId, candidate]));
349
- const dependencies = [...dependencyIds].sort().map(stageId => {
350
- const dependency = actionByStage.get(stageId);
351
- if (!dependency) return { stageId, actionId: null, result: null };
351
+ const dynamic = isDynamicWorkItem(detail);
352
+ const dependencyIds = new Set(dynamic ? action.sourceActionIds || [] : action.dependsOnStageIds || []);
353
+ const actionByReference = new Map((detail.actions || []).filter(candidate => candidate.status !== 'superseded')
354
+ .map(candidate => [dynamic ? candidate.id : candidate.stageId, candidate]));
355
+ const dependencies = [...dependencyIds].sort().map(reference => {
356
+ const dependency = actionByReference.get(reference);
357
+ if (!dependency) return dynamic
358
+ ? { sourceActionId: reference, actionId: null, result: null }
359
+ : { stageId: reference, actionId: null, result: null };
352
360
  return {
353
- stageId,
361
+ ...(dynamic ? { sourceActionId: reference } : { stageId: reference }),
354
362
  actionId: dependency.id,
355
363
  generation: dependency.generation || 1,
356
364
  specHash: dependency.specHash || '',
@@ -377,14 +385,18 @@ export function buildMainlineContextSnapshot(detail, action, budgetInput = {}) {
377
385
  policyInstruction: detail.workflowSnapshot?.actionInstructions?.[action.type]
378
386
  || detail.workflowSnapshot?.actionInstructions?.custom
379
387
  || '',
380
- dependsOnStageIds: [...dependencyIds].sort(),
388
+ ...(dynamic
389
+ ? { sourceActionIds: [...dependencyIds].sort() }
390
+ : { dependsOnStageIds: [...dependencyIds].sort() }),
381
391
  workspaceMode: action.workspaceMode || 'shared',
382
392
  changesRequestedStageId: action.changesRequestedStageId || null,
383
393
  },
384
394
  },
385
- graph: projection.graph,
395
+ ...(dynamic
396
+ ? { actionJournal: projection.actionJournal }
397
+ : { graph: projection.graph }),
386
398
  canonicalCompletedResultsIndex: resultIndex,
387
- directDependencies: dependencies,
399
+ ...(dynamic ? { sourceResults: dependencies } : { directDependencies: dependencies }),
388
400
  userContext: {
389
401
  sessionContext: [],
390
402
  workItemMessages: [],
@@ -605,6 +605,7 @@ function projectAction(action, runs, events, includeBody = true) {
605
605
  ? (action.assignmentPolicy || null)
606
606
  : projectAssignmentPolicy(action.assignmentPolicy),
607
607
  dependsOnStageIds: Array.isArray(action.dependsOnStageIds) ? action.dependsOnStageIds : [],
608
+ sourceActionIds: Array.isArray(action.sourceActionIds) ? action.sourceActionIds : [],
608
609
  workspaceMode: action.workspaceMode || 'shared',
609
610
  requiredRole: action.requiredRole || '',
610
611
  generation: Math.max(1, count(action.generation) || 1),
@@ -797,17 +798,20 @@ function projectCanonicalEvidence(value) {
797
798
  }
798
799
 
799
800
  function projectMainlineBrowser(detail) {
800
- if (!detail?.id || detail.executionSchemaVersion !== 2) return null;
801
+ if (!detail?.id || Number(detail.executionSchemaVersion) < 2) return null;
801
802
  const mainline = buildMainlineProjection(detail);
803
+ const actionSet = mainline.actionJournal || mainline.graph;
804
+ const nodes = actionSet.entries || actionSet.nodes || [];
805
+ const frontier = actionSet.runnableActionIds || actionSet.frontier || [];
802
806
  const actionById = new Map((detail.actions || []).map(action => [action.id, action]));
803
807
  const activeActionIds = Array.isArray(detail.activeActionIds)
804
808
  ? detail.activeActionIds
805
- : mainline.graph.nodes.filter(node => ['ready', 'running'].includes(node.status)).map(node => node.id);
809
+ : nodes.filter(node => ['ready', 'running'].includes(node.status)).map(node => node.id);
806
810
  const attentionActionIds = Array.isArray(detail.attentionActionIds)
807
811
  ? detail.attentionActionIds
808
- : mainline.graph.nodes.filter(node => ['waiting', 'failed'].includes(node.status)).map(node => node.id);
812
+ : nodes.filter(node => ['waiting', 'failed'].includes(node.status)).map(node => node.id);
809
813
  const counts = Object.fromEntries(['completed', 'running', 'ready', 'waiting', 'failed']
810
- .map(status => [status, mainline.graph.nodes.filter(node => node.status === status).length]));
814
+ .map(status => [status, nodes.filter(node => node.status === status).length]));
811
815
  return {
812
816
  contract: {
813
817
  title: truncateUtf8(mainline.contract.title, 8_000),
@@ -816,15 +820,15 @@ function projectMainlineBrowser(detail) {
816
820
  .map(criterion => truncateUtf8(criterion, 4_000)),
817
821
  },
818
822
  progress: {
819
- lifecycle: detail.lifecycle || (counts.completed === mainline.graph.nodes.length ? 'done' : 'active'),
823
+ lifecycle: detail.lifecycle || (counts.completed === nodes.length ? 'done' : 'active'),
820
824
  attentionState: detail.attentionState || (counts.waiting && counts.failed ? 'mixed'
821
825
  : counts.waiting ? 'waiting' : counts.failed ? 'failed' : 'none'),
822
826
  activeActionIds: [...activeActionIds],
823
827
  attentionActionIds: [...attentionActionIds],
824
- frontierActionIds: [...mainline.graph.frontier],
828
+ frontierActionIds: [...frontier],
825
829
  counts,
826
830
  },
827
- actions: mainline.graph.nodes.map(node => {
831
+ actions: nodes.map(node => {
828
832
  const action = actionById.get(node.id) || {};
829
833
  const result = mainline.canonicalActionResults[node.id];
830
834
  return {
@@ -839,7 +843,7 @@ function projectMainlineBrowser(detail) {
839
843
  truncateUtf8(value, MAX_CURRENT_BRIEF_BYTES),
840
844
  ]))
841
845
  : null,
842
- dependencies: [...node.dependsOnStageIds],
846
+ dependencies: [...(node.sourceActionIds?.length ? node.sourceActionIds : node.dependsOnStageIds || [])],
843
847
  canonicalResult: result ? {
844
848
  status: result.status,
845
849
  summary: sanitizeMainlineDiagnostic(result.summary, MAX_ACTION_DIAGNOSTIC_CHARS),
@@ -895,6 +899,22 @@ export function projectWorkItemDetail(detail, options = {}) {
895
899
  planRevision: count(detail.planRevision),
896
900
  ledgerRevision: count(detail.ledgerRevision),
897
901
  coordinatorRevision: count(detail.coordinatorRevision),
902
+ coordinationMode: detail.coordinationMode || 'legacy',
903
+ finalResult: detail.finalResult && typeof detail.finalResult === 'object' ? {
904
+ summary: truncateUtf8(detail.finalResult.summary || '', MAX_ACTION_MESSAGE_CHARS),
905
+ acceptanceResults: Array.isArray(detail.finalResult.acceptanceResults)
906
+ ? detail.finalResult.acceptanceResults.slice(0, 24).map(result => ({
907
+ criterion: truncateUtf8(result?.criterion || '', MAX_ACTION_MESSAGE_CHARS),
908
+ status: result?.status === 'passed' ? 'passed' : null,
909
+ evidenceRunIds: Array.isArray(result?.evidenceRunIds)
910
+ ? result.evidenceRunIds.map(String).slice(0, 24) : [],
911
+ })) : [],
912
+ evidenceRunIds: Array.isArray(detail.finalResult.evidenceRunIds)
913
+ ? detail.finalResult.evidenceRunIds.map(String).slice(0, 64) : [],
914
+ residualRisks: Array.isArray(detail.finalResult.residualRisks)
915
+ ? detail.finalResult.residualRisks
916
+ .map(risk => truncateUtf8(risk, MAX_ACTION_MESSAGE_CHARS)).slice(0, 24) : [],
917
+ } : null,
898
918
  title: detail.title,
899
919
  goal: detail.goal,
900
920
  acceptanceCriteria: Array.isArray(detail.acceptanceCriteria) ? detail.acceptanceCriteria : [],
@@ -931,7 +951,8 @@ export function projectWorkItemDetail(detail, options = {}) {
931
951
  status: ['thinking', 'completed', 'failed'].includes(message.status) ? message.status : 'completed',
932
952
  error: truncateUtf8(message.error || '', MAX_ACTION_DIAGNOSTIC_CHARS) || null,
933
953
  decision: message.decision && typeof message.decision === 'object' ? {
934
- kind: ['answer', 'guide_actions', 'replan', 'request_human'].includes(message.decision.kind)
954
+ kind: ['answer', 'create_actions', 'guide_actions', 'replan', 'request_human', 'complete']
955
+ .includes(message.decision.kind)
935
956
  ? message.decision.kind : null,
936
957
  reason: truncateUtf8(message.decision.reason || '', MAX_ACTION_DIAGNOSTIC_CHARS),
937
958
  changedContract: message.decision.changedContract === true,
@@ -981,6 +1002,7 @@ export function projectWorkItemSummary(detail) {
981
1002
  planRevision: count(detail.planRevision),
982
1003
  ledgerRevision: count(detail.ledgerRevision),
983
1004
  coordinatorRevision: count(detail.coordinatorRevision),
1005
+ coordinationMode: detail.coordinationMode || 'legacy',
984
1006
  title: detail.title,
985
1007
  goal: detail.goal,
986
1008
  workItemType: detail.workflowSnapshot?.workItemType || detail.workItemType || null,
@@ -1017,6 +1039,7 @@ export function projectWorkItemSummary(detail) {
1017
1039
  planRevision: count(detail.planRevision),
1018
1040
  ledgerRevision: count(detail.ledgerRevision),
1019
1041
  coordinatorRevision: count(detail.coordinatorRevision),
1042
+ coordinationMode: detail.coordinationMode || 'legacy',
1020
1043
  title: detail.title,
1021
1044
  goal: detail.goal,
1022
1045
  workItemType: detail.workflowSnapshot?.workItemType || detail.workItemType || null,
@@ -34,6 +34,7 @@ import { MCPManager } from '../mcp.js';
34
34
  import { buildMcpFlattenedTools } from '../tools/mcp-tools.js';
35
35
  import { recallWorkspaceSessionContext } from './workspace-context.js';
36
36
  import { applyGeneratedPlan, BUILT_IN_ACTION_TYPES } from './workflow.js';
37
+ import { isDynamicWorkItem, usesMainlineContext } from './execution-mode.js';
37
38
  import {
38
39
  applyAdditivePlanProposal,
39
40
  applyReplanMutation,
@@ -946,7 +947,9 @@ export class WorkItemRunner {
946
947
  action: finalizeOwnedIntegration(this.store, action, run, ownerBootId),
947
948
  };
948
949
  }
949
- const dependencies = this.store.listActionDependencies(workItem.id, action.dependsOnStageIds || []);
950
+ const dependencies = isDynamicWorkItem(workItem)
951
+ ? this.store.listActionSources(workItem.id, action.sourceActionIds || [])
952
+ : this.store.listActionDependencies(workItem.id, action.dependsOnStageIds || []);
950
953
  if (dependencies.length > 0 && dependencies.every(dependency => (
951
954
  dependency.workspaceMode === 'shared' && !dependency.workspace?.isolated
952
955
  ))) {
@@ -1021,9 +1024,8 @@ export class WorkItemRunner {
1021
1024
 
1022
1025
  async run({ workItem, action, run, signal, ownerBootId, onProgress, registerProgressReader, registerInputWake }) {
1023
1026
  const runtime = await this.runtimeProvider();
1024
- const currentSettings = workItem?.workflowSnapshot?.planningMode === 'ai' && this.policyProvider
1025
- ? await this.policyProvider()
1026
- : null;
1027
+ const currentSettings = ['ai', 'coordinator'].includes(workItem?.workflowSnapshot?.planningMode)
1028
+ && this.policyProvider ? await this.policyProvider() : null;
1027
1029
  const currentModelPolicy = currentSettings?.actionModelPolicies?.[action.type]
1028
1030
  || currentSettings?.actionModelPolicies?.custom
1029
1031
  || currentSettings?.modelPolicy
@@ -1036,15 +1038,16 @@ export class WorkItemRunner {
1036
1038
  ? resolveWorkItemWorkDir({ workspaceKey: action.workspace.path }, runtime.defaultWorkDir)
1037
1039
  : workspaceDir;
1038
1040
  const priorRuns = this.store.listCompletedRuns(workItem.id);
1039
- const v2Execution = Number(workItem.executionSchemaVersion) === 2;
1040
- const dependencyContext = v2Execution
1041
- ? []
1042
- : this.store.listActionDependencies?.(workItem.id, action.dependsOnStageIds || []) || [];
1041
+ const mainlineExecution = usesMainlineContext(workItem);
1042
+ const dependencyContext = isDynamicWorkItem(workItem)
1043
+ ? this.store.listActionSources?.(workItem.id, action.sourceActionIds || []) || []
1044
+ : mainlineExecution ? []
1045
+ : this.store.listActionDependencies?.(workItem.id, action.dependsOnStageIds || []) || [];
1043
1046
  const dependencyBlock = dependencyContext.length === 0 ? '' : `\n\nCompleted dependency results:\n${dependencyContext.map(dependency => {
1044
1047
  const evidence = dependency.evidence?.length
1045
1048
  ? `\nEvidence: ${dependency.evidence.map(item => item.label).join('; ')}`
1046
1049
  : '';
1047
- return `### ${dependency.stageId} (${dependency.vpId || 'unknown VP'})\n${dependency.summary || '(no summary)'}${evidence}`;
1050
+ return `### ${isDynamicWorkItem(workItem) ? dependency.id : dependency.stageId} (${dependency.vpId || 'unknown VP'})\n${dependency.summary || '(no summary)'}${evidence}`;
1048
1051
  }).join('\n\n')}`;
1049
1052
  const resumeBlock = renderActionResumeBlock(this.store.getActionResumeContext?.(action.id, run.id));
1050
1053
  const assignment = executionAction.assignmentPolicy
@@ -1084,7 +1087,7 @@ export class WorkItemRunner {
1084
1087
  const attachmentFileById = new Map(attachmentContext.files.map(file => [file.id, file]));
1085
1088
  const fixedPromptSuffix = `${resumeBlock}${attachmentContext.promptBlock}${completionContract(executionAction, workItem)}`;
1086
1089
  const reservedPromptBytes = Buffer.byteLength(fixedPromptSuffix, 'utf8');
1087
- const mainline = v2Execution
1090
+ const mainline = mainlineExecution
1088
1091
  ? buildMainlineContextSnapshot(
1089
1092
  this.store.getWorkItemDetail(workItem.id),
1090
1093
  executionAction,
@@ -1212,7 +1215,9 @@ export class WorkItemRunner {
1212
1215
  executionManifest: mainline ? {
1213
1216
  schemaVersion: 2,
1214
1217
  ledgerRevision: mainline.contextSnapshot.ledgerRevision,
1215
- planRevision: mainline.contextSnapshot.graph.planRevision,
1218
+ planRevision: isDynamicWorkItem(workItem)
1219
+ ? mainline.contextSnapshot.actionJournal.revision
1220
+ : mainline.contextSnapshot.graph.planRevision,
1216
1221
  contractRevision: mainline.contextSnapshot.contract.revision,
1217
1222
  actionGeneration: mainline.contextSnapshot.action.generation,
1218
1223
  actionSpecHash: mainline.contextSnapshot.action.specHash,
@@ -1336,11 +1341,11 @@ export class WorkItemRunner {
1336
1341
  }
1337
1342
  };
1338
1343
  try {
1339
- const prompt = v2Execution
1344
+ const prompt = mainlineExecution
1340
1345
  ? `${renderMainlineContextSnapshot(mainline.contextSnapshot)}${fixedPromptSuffix}`
1341
1346
  : `${executionAction.instruction}${dependencyBlock}${resumeBlock}${attachmentContext.promptBlock}${workspaceSessionBlock}${memoryBlock}${completionContract(executionAction, workItem)}`;
1342
1347
  const promptBytes = Buffer.byteLength(prompt, 'utf8');
1343
- if (v2Execution && promptBytes > MAINLINE_CONTEXT_HARD_LIMIT_BYTES) {
1348
+ if (mainlineExecution && promptBytes > MAINLINE_CONTEXT_HARD_LIMIT_BYTES) {
1344
1349
  throw new Error(`Work Center Mainline prompt exceeds 64 KiB (${promptBytes} rendered UTF-8 bytes)`);
1345
1350
  }
1346
1351
  const promptParts = attachmentContext.promptParts.length > 0
@@ -23,9 +23,12 @@ import { readWorkCenterSettings, writeWorkCenterSettings } from './settings.js';
23
23
  import {
24
24
  defaultWorkCenterStageInstructions,
25
25
  listWorkItemTypeTemplates,
26
- resolvePlanningWorkflowSnapshot,
27
- resolveWorkflowSnapshot,
28
26
  } from './workflow.js';
27
+ import {
28
+ DYNAMIC_COORDINATION_MODE,
29
+ DYNAMIC_EXECUTION_SCHEMA_VERSION,
30
+ resolveDynamicActionPolicySnapshot,
31
+ } from './dynamic-coordination.js';
29
32
 
30
33
  function requiredString(value, name) {
31
34
  if (typeof value !== 'string' || !value.trim()) throw new Error(`${name} is required`);
@@ -122,6 +125,7 @@ export class WorkCenterService {
122
125
  ? Number(options.pollIntervalMs)
123
126
  : 2_000;
124
127
  this.recoveryTimer = null;
128
+ this.dynamicCoordinatorTasks = new Map();
125
129
  this.shuttingDown = false;
126
130
  this.watcher = new WorkItemWatcher({
127
131
  store: this.store,
@@ -227,14 +231,8 @@ export class WorkCenterService {
227
231
  }
228
232
  case 'create': {
229
233
  const settings = this.settingsReader(this.yeaftDir);
230
- const explicitWorkflow = requestContext.trustedProducer === true
231
- && typeof payload.workflowTemplate === 'string' && payload.workflowTemplate.trim()
232
- ? payload.workflowTemplate.trim()
233
- : null;
234
- const workflowTemplate = explicitWorkflow || 'ai-planned';
235
- const workflowSnapshot = explicitWorkflow
236
- ? resolveWorkflowSnapshot(settings, explicitWorkflow, payload.stageOverrides)
237
- : resolvePlanningWorkflowSnapshot(settings, payload.workItemType);
234
+ const workflowTemplate = 'coordinator-driven';
235
+ const workflowSnapshot = resolveDynamicActionPolicySnapshot(settings, payload.workItemType);
238
236
  const runtime = !Object.hasOwn(payload, 'workDir') && !settings.defaultWorkDir
239
237
  ? await this.runtimeInfo()
240
238
  : null;
@@ -248,6 +246,7 @@ export class WorkCenterService {
248
246
  root: this.attachmentRoot,
249
247
  workItemId,
250
248
  });
249
+ const shouldStart = payload.start === undefined ? settings.startImmediately : payload.start !== false;
251
250
  this.controller.create({
252
251
  id: workItemId,
253
252
  title: requiredString(payload.title, 'title'),
@@ -257,6 +256,8 @@ export class WorkCenterService {
257
256
  : [],
258
257
  workflowTemplate,
259
258
  workflowSnapshot,
259
+ coordinationMode: DYNAMIC_COORDINATION_MODE,
260
+ executionSchemaVersion: DYNAMIC_EXECUTION_SCHEMA_VERSION,
260
261
  workDir,
261
262
  reuseMemory: payload.reuseMemory !== false,
262
263
  origin: payload.origin && typeof payload.origin === 'object'
@@ -274,9 +275,13 @@ export class WorkCenterService {
274
275
  ? normalizeSessionContextSnapshot(payload.sessionContext)
275
276
  : [],
276
277
  attachments,
277
- start: payload.start === undefined ? settings.startImmediately : payload.start !== false,
278
+ start: false,
278
279
  });
279
- const detail = this.#requiredItem(workItemId);
280
+ let detail = this.#requiredItem(workItemId);
281
+ if (shouldStart) {
282
+ detail = this.controller.start(workItemId);
283
+ this.#queueDynamicCoordinatorWake(workItemId);
284
+ }
280
285
  this.#emit({ type: 'work_item.created', workItem: detail });
281
286
  return detail;
282
287
  } catch (error) {
@@ -288,11 +293,17 @@ export class WorkCenterService {
288
293
  const id = requiredString(payload.id, 'id');
289
294
  const detail = this.controller.update(id, payload.patch || {});
290
295
  this.watcher.abortInvalidWorkItemRuns(id);
296
+ if (detail.coordinationMode === DYNAMIC_COORDINATION_MODE) {
297
+ this.#queueDynamicCoordinatorWake(id);
298
+ }
291
299
  this.#emit({ type: 'work_item.updated', workItem: detail });
292
300
  return detail;
293
301
  }
294
302
  case 'start': {
295
303
  const detail = this.controller.start(requiredString(payload.id, 'id'));
304
+ if (detail.coordinationMode === DYNAMIC_COORDINATION_MODE) {
305
+ this.#queueDynamicCoordinatorWake(detail.id);
306
+ }
296
307
  this.#emit({ type: 'work_item.started', workItem: detail });
297
308
  return detail;
298
309
  }
@@ -307,6 +318,9 @@ export class WorkCenterService {
307
318
  const id = requiredString(payload.id, 'id');
308
319
  const detail = this.controller.resume(id, { revision: payload.revision });
309
320
  this.watcher.abortInvalidWorkItemRuns(id);
321
+ if (detail.coordinationMode === DYNAMIC_COORDINATION_MODE) {
322
+ this.#queueDynamicCoordinatorWake(id);
323
+ }
310
324
  this.#emit({ type: 'work_item.resumed', workItem: detail });
311
325
  return detail;
312
326
  }
@@ -571,6 +585,10 @@ export class WorkCenterService {
571
585
 
572
586
  #emit(event) {
573
587
  try { this.onEvent(event); } catch {}
588
+ if (event?.workItem?.coordinationMode === DYNAMIC_COORDINATION_MODE) {
589
+ this.#queueDynamicCoordinatorWake(event.workItem.id);
590
+ return;
591
+ }
574
592
  if (['run.finished', 'coordinator.turn_completed'].includes(event?.type)) {
575
593
  for (const action of event.workItem?.actions || []) {
576
594
  if (action.status !== 'failed') continue;
@@ -583,6 +601,56 @@ export class WorkCenterService {
583
601
  }
584
602
  }
585
603
 
604
+ #queueDynamicCoordinatorWake(workItemId) {
605
+ if (this.shuttingDown || !this.coordinator || !workItemId
606
+ || this.dynamicCoordinatorTasks.has(workItemId)) return;
607
+ this.dynamicCoordinatorTasks.set(workItemId, null);
608
+ queueMicrotask(() => {
609
+ if (this.dynamicCoordinatorTasks.get(workItemId) === null) {
610
+ this.dynamicCoordinatorTasks.delete(workItemId);
611
+ this.#drainDynamicCoordinatorWakes(workItemId);
612
+ }
613
+ });
614
+ }
615
+
616
+ #scanDynamicCoordinatorWakes() {
617
+ if (this.shuttingDown || !this.coordinator) return;
618
+ for (const entry of this.store.listPendingDynamicCoordinatorWakes()) {
619
+ this.#queueDynamicCoordinatorWake(entry.workItemId);
620
+ }
621
+ }
622
+
623
+ #drainDynamicCoordinatorWakes(workItemId) {
624
+ if (this.shuttingDown || !this.coordinator || this.dynamicCoordinatorTasks.has(workItemId)) return null;
625
+ const entries = this.store.listPendingDynamicCoordinatorWakes()
626
+ .filter(candidate => candidate.workItemId === workItemId);
627
+ const entry = entries.find(candidate => candidate.payload?.turnId) || entries[0];
628
+ if (!entry) return null;
629
+ const detail = this.store.getWorkItemDetail(workItemId);
630
+ if (detail?.actions?.some(action => action.status === 'running')) return null;
631
+ let turn;
632
+ try {
633
+ turn = this.coordinator.advance(entry.id, {
634
+ workItemId,
635
+ onUpdate: (type, workItem) => this.#emit({ type, workItem }),
636
+ });
637
+ } catch (error) {
638
+ this.#emit({
639
+ type: 'coordinator.advance_schedule_failed',
640
+ workItem: this.store.getWorkItemDetail(workItemId),
641
+ error: error?.message || String(error),
642
+ });
643
+ return null;
644
+ }
645
+ if (!turn) return null;
646
+ const task = turn.task.finally(() => {
647
+ this.dynamicCoordinatorTasks.delete(workItemId);
648
+ });
649
+ this.dynamicCoordinatorTasks.set(workItemId, task);
650
+ task.catch(() => {});
651
+ return task;
652
+ }
653
+
586
654
  #recoveryKey(entry) {
587
655
  return `${entry.workItemId}:${entry.actionId}:${entry.actionGeneration}`;
588
656
  }
@@ -621,6 +689,7 @@ export class WorkCenterService {
621
689
 
622
690
  #scanRecoveries() {
623
691
  this.#scanCoordinatorProviderRecoveries();
692
+ this.#scanDynamicCoordinatorWakes();
624
693
  this.#scanFailureRecoveries();
625
694
  }
626
695
 
@@ -701,8 +770,12 @@ export class WorkCenterService {
701
770
  if (this.recoveryTimer) clearInterval(this.recoveryTimer);
702
771
  this.recoveryTimer = null;
703
772
  await this.coordinator?.shutdown?.();
704
- await Promise.allSettled([...this.recoveryTasks.values()]);
773
+ await Promise.allSettled([
774
+ ...this.recoveryTasks.values(),
775
+ ...[...this.dynamicCoordinatorTasks.values()].filter(Boolean),
776
+ ]);
705
777
  this.recoveryTasks.clear();
778
+ this.dynamicCoordinatorTasks.clear();
706
779
  this.recoveryQueue.clear();
707
780
  await this.watcher.stop();
708
781
  try { await this.watcher.runner?.shutdown?.(); } catch {}