@yeaft/webchat-agent 1.0.366 → 1.0.368

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.
Binary file
@@ -16,6 +16,6 @@
16
16
  </head>
17
17
  <body>
18
18
  <div id="app"></div>
19
- <script type="module" src="app.bundle.js?v=86aebbdf"></script>
19
+ <script type="module" src="app.bundle.js?v=76df89f4"></script>
20
20
  </body>
21
21
  </html>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yeaft/webchat-agent",
3
- "version": "1.0.366",
3
+ "version": "1.0.368",
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",
@@ -11,10 +11,10 @@ export default defineTool({
11
11
  description: {
12
12
  en: `Create a persistent Agent-level Work Center item from the current Session.
13
13
 
14
- Use this when work must continue beyond the current turn, needs role handoffs, review, waiting, retry, or durable tracking. This creates only the goal contract; Work Center triage chooses the task type, Actions, and executors. The current Session is always stamped as the origin and cannot be overridden by model input.`,
14
+ Use this when work must continue beyond the current turn, needs role handoffs, review, waiting, retry, or durable tracking. This creates only the goal contract; the Work Center Coordinator dynamically chooses the next Actions and executors until the acceptance criteria are verified. The current Session is always stamped as the origin and cannot be overridden by model input.`,
15
15
  zh: `从当前 Session 创建一个持久化的 Agent 级工作项。
16
16
 
17
- 当工作需要跨 turn 继续、需要角色接力、评审、等待、重试或长期跟踪时使用。该工具只创建目标契约,任务类型、Action 和执行者由 Work Center triage 决定,不在当前 turn 内执行。来源 Session 由运行时强制写入,模型输入不能覆盖。`,
17
+ 当工作需要跨 turn 继续、需要角色接力、评审、等待、重试或长期跟踪时使用。该工具只创建目标契约;Work Center Coordinator 会动态选择下一批 Action 和执行者,直到验收条件得到验证。来源 Session 由运行时强制写入,模型输入不能覆盖。`,
18
18
  },
19
19
  parameters: {
20
20
  type: 'object',
@@ -42,15 +42,15 @@ Use this when work must continue beyond the current turn, needs role handoffs, r
42
42
  },
43
43
  start: {
44
44
  type: 'boolean',
45
- description: { en: 'Start triage immediately (default true)', zh: '是否立即开始 triage(默认 true)' },
45
+ description: { en: 'Start Coordinator execution immediately (default true)', zh: '是否立即启动 Coordinator 执行(默认 true)' },
46
46
  },
47
47
  },
48
48
  required: ['title', 'goal'],
49
49
  },
50
50
  isConcurrencySafe: () => false,
51
51
  isReadOnly: () => false,
52
- // The Work Center watcher can advance from triage into a writable Action
53
- // after creation returns. A paused item has no watcher-owned execution.
52
+ // The persistent Coordinator can create a writable Action after creation
53
+ // returns. A paused item has no Coordinator- or watcher-owned execution.
54
54
  mayMutateWorkspaceAfterReturn: input => input?.start !== false,
55
55
  async execute(input, ctx = {}) {
56
56
  const sessionId = typeof ctx.sessionId === 'string' ? ctx.sessionId.trim() : '';
@@ -69,9 +69,9 @@ Use this when work must continue beyond the current turn, needs role handoffs, r
69
69
  acceptanceCriteria: cleanCriteria(input.acceptanceCriteria),
70
70
  workItemType: typeof input.workItemType === 'string' ? input.workItemType.trim() : 'auto',
71
71
  workDir: typeof input.workDir === 'string' ? input.workDir.trim() : (ctx.cwd || ''),
72
- // The Agent-local Work Center settings choose the default workflow and
73
- // freeze its policy snapshot. Tool callers create the contract; they do
74
- // not get to smuggle a different dispatch policy into it.
72
+ // Agent-local Work Center settings freeze the Action-template and model
73
+ // policy snapshot. Tool callers create the contract; they cannot smuggle
74
+ // a different dispatch policy into it.
75
75
  origin: {
76
76
  sessionId,
77
77
  messageId: ctx.inboundEnvelope?.msgId || null,
@@ -12,6 +12,7 @@ import {
12
12
  import { renderSessionContextSnapshot } from './session-context.js';
13
13
  import { normalizeSessionMessageQuote } from '../session-message-quote.js';
14
14
  import { normalizeEvidence } from './evidence.js';
15
+ import { isDynamicWorkItem } from './execution-mode.js';
15
16
  import { applyAdditivePlanProposal, applyReplanMutation } from './plan-mutation.js';
16
17
  import { normalizeContractPatch, validateCompletedResult } from './completion-contract.js';
17
18
 
@@ -129,7 +130,7 @@ export class WorkflowController {
129
130
  acceptanceCriteria: Array.isArray(input.acceptanceCriteria) ? input.acceptanceCriteria : [],
130
131
  attachments: Array.isArray(input.attachments) ? input.attachments : [],
131
132
  };
132
- let firstAction = input.start !== false ? initialActionFor(draft) : null;
133
+ let firstAction = input.start !== false && !isDynamicWorkItem(draft) ? initialActionFor(draft) : null;
133
134
  if (firstAction) {
134
135
  firstAction = {
135
136
  ...firstAction,
@@ -301,7 +302,7 @@ export class WorkflowController {
301
302
  quote: input.inputEvent?.quote || null,
302
303
  });
303
304
  }
304
- if (Number(workItem.executionSchemaVersion) === 2 && input.inputEvent?.inputId) {
305
+ if (Number(workItem.executionSchemaVersion) >= 2 && input.inputEvent?.inputId) {
305
306
  context.push({
306
307
  type: 'input',
307
308
  role: 'user',
@@ -337,6 +338,15 @@ export class WorkflowController {
337
338
  throw new Error('Run has unconsumed Action input and cannot finish yet');
338
339
  }
339
340
  const result = normalizeTerminalResult(rawResult, activeAction);
341
+ if (isDynamicWorkItem(activeWorkItem)) {
342
+ result.contractPatch = null;
343
+ result.plan = null;
344
+ result.planProposal = null;
345
+ result.replanRequest = null;
346
+ result.replanMutation = null;
347
+ result.nextActions = [];
348
+ result.expandPlan = null;
349
+ }
340
350
  if (result.outcome === 'completed'
341
351
  && activeAction.stageId?.startsWith('replan-')
342
352
  && !result.replanMutation) {
@@ -9,6 +9,8 @@ import {
9
9
  } from '../llm/adapter.js';
10
10
  import { resolveWorkItemModel, selectWorkItemVp } from './assignment.js';
11
11
  import { normalizeContractPatch } from './completion-contract.js';
12
+ import { prepareDynamicActionMutation } from './dynamic-coordination.js';
13
+ import { isDynamicWorkItem } from './execution-mode.js';
12
14
  import { applyCoordinatorReplan } from './plan-mutation.js';
13
15
  import { buildWorkItemAttachmentContext } from './attachments.js';
14
16
  import { sanitizeDiagnosticText } from './debug-projection.js';
@@ -124,17 +126,25 @@ function coordinatorStageReferences(detail) {
124
126
  };
125
127
  }
126
128
 
127
- function boundedAction(action, result, stageReferences, compact = false) {
129
+ function boundedAction(action, result, stageReferences, compact = false, dynamic = false) {
128
130
  const brief = action?.brief && typeof action.brief === 'object' ? action.brief : null;
129
131
  return {
130
- stageId: stageReferences.project(action?.stageId),
132
+ ...(dynamic
133
+ ? {
134
+ actionId: truncateUtf8(action?.id, 256),
135
+ sourceActionIds: (Array.isArray(action?.sourceActionIds) ? action.sourceActionIds : [])
136
+ .slice(0, 8).map(value => truncateUtf8(value, 256)).filter(Boolean),
137
+ }
138
+ : { stageId: stageReferences.project(action?.stageId) }),
131
139
  type: truncateUtf8(action?.type, 64),
132
140
  status: truncateUtf8(action?.status, 64),
133
141
  generation: Math.max(1, Number(action?.generation) || 1),
134
- dependencies: (Array.isArray(action?.dependsOnStageIds) ? action.dependsOnStageIds : [])
135
- .slice(0, 8)
136
- .map(value => stageReferences.project(value))
137
- .filter(Boolean),
142
+ ...(dynamic ? {} : {
143
+ dependencies: (Array.isArray(action?.dependsOnStageIds) ? action.dependsOnStageIds : [])
144
+ .slice(0, 8)
145
+ .map(value => stageReferences.project(value))
146
+ .filter(Boolean),
147
+ }),
138
148
  workspaceMode: truncateUtf8(action?.workspaceMode, 64),
139
149
  ...(!compact && brief ? {
140
150
  brief: {
@@ -146,7 +156,15 @@ function boundedAction(action, result, stageReferences, compact = false) {
146
156
  result: result ? {
147
157
  status: truncateUtf8(result.status, 64),
148
158
  summary: truncateUtf8(result.summary, compact ? 256 : 768),
149
- ...(!compact ? { evidence: boundedEvidence(result.evidence) } : {}),
159
+ ...(!compact ? {
160
+ evidence: boundedEvidence(result.evidence),
161
+ acceptanceChecks: (Array.isArray(result.acceptanceChecks) ? result.acceptanceChecks : [])
162
+ .slice(0, 24).map(check => ({
163
+ criterion: truncateUtf8(check?.criterion, 512),
164
+ status: truncateUtf8(check?.status, 64),
165
+ evidence: truncateUtf8(check?.evidence, 1_000),
166
+ })),
167
+ } : {}),
150
168
  waitingReason: truncateUtf8(result.waitingReason, 384) || null,
151
169
  error: truncateUtf8(result.error, 384) || null,
152
170
  reviewDecision: truncateUtf8(result.reviewDecision, 64) || null,
@@ -188,11 +206,42 @@ Decision rules:
188
206
  - Stage ids in the snapshot may be bounded aliases. Echo them exactly; the runtime resolves them to durable identities.
189
207
  - Never return destructive cancellation. Tell the user to use the explicit cancel control instead.`;
190
208
 
191
- function coordinatorSystemPrompt(language) {
209
+ const DYNAMIC_COORDINATOR_SYSTEM_PROMPT = `You are the persistent Work Center Coordinator. You own progress toward one durable WorkItem.
210
+
211
+ Observe the current contract, Action journal, canonical Run evidence, and conversation. Decide only the next justified step; never predict or emit a complete workflow graph.
212
+
213
+ Return exactly one JSON object and no surrounding prose:
214
+ {
215
+ "reply": "natural user-facing response",
216
+ "decision": {
217
+ "kind": "answer|create_actions|guide_actions|request_human|complete",
218
+ "reason": "short audit reason",
219
+ "question": null,
220
+ "workItemType": null,
221
+ "contractPatch": null,
222
+ "supersedeActionIds": [],
223
+ "guidance": [],
224
+ "actions": [],
225
+ "completion": null
226
+ }
227
+ }
228
+
229
+ Rules:
230
+ - answer: explain state only. Never use it for an automatic advance trigger.
231
+ - create_actions: create 1..8 currently runnable Actions. Every Action needs type, objective, approach, expectedOutcome, capability, candidateVpIds, assignmentReason, sourceActionIds, workspaceMode, and optional maxAttempts/separateFromActionTypes. sourceActionIds are context/audit references, never scheduling dependencies. Do not include dependsOnActionIds, dependsOnStageIds, stages, or a graph.
232
+ - guide_actions: target 1..8 unfinished non-running Actions by durable actionId.
233
+ - request_human: only when external information or a user decision is genuinely required.
234
+ - complete: only when every acceptance criterion has canonical completed Run evidence and there are no unfinished Actions. Include summary, ordered acceptanceResults with evidenceRunIds, evidenceRunIds, and residualRisks.
235
+ - Preserve completed Action history. Never claim tests, review, merge, release, or external effects without canonical Run evidence.
236
+ - Action templates are reusable capabilities, not a prescribed workflow. Create the smallest useful Action boundary, not tool-call-sized work.
237
+ - Never return destructive cancellation. The user owns the explicit cancel control.`;
238
+
239
+ function coordinatorSystemPrompt(language, detail) {
192
240
  const userLanguage = coordinatorLanguage(language) === 'zh'
193
241
  ? 'Simplified Chinese (zh-CN)'
194
242
  : 'English';
195
- return `${COORDINATOR_SYSTEM_PROMPT}
243
+ const base = isDynamicWorkItem(detail) ? DYNAMIC_COORDINATOR_SYSTEM_PROMPT : COORDINATOR_SYSTEM_PROMPT;
244
+ return `${base}
196
245
 
197
246
  User-facing language: ${userLanguage}. Write reply and question in that language. Keep JSON property names and decision enum values in English. Never expose deterministic validator errors as the user-facing reply; explain the underlying issue plainly.`;
198
247
  }
@@ -327,19 +376,22 @@ function normalizeGuidance(value, detail) {
327
376
  if (!Array.isArray(value) || value.length < 1 || value.length > 8) {
328
377
  throw new Error('Work Center Coordinator guidance requires between 1 and 8 targets');
329
378
  }
330
- const activeByStage = new Map((detail.actions || [])
331
- .filter(action => !['completed', 'superseded', 'cancelled'].includes(action.status))
332
- .map(action => [action.stageId, action]));
379
+ const dynamic = isDynamicWorkItem(detail);
380
+ const active = (detail.actions || [])
381
+ .filter(action => !['completed', 'superseded', 'cancelled'].includes(action.status));
382
+ const activeByReference = new Map(active.map(action => [dynamic ? action.id : action.stageId, action]));
333
383
  const stageReferences = coordinatorStageReferences(detail);
334
384
  const seen = new Set();
335
385
  return value.map(entry => {
336
- const stageId = stageReferences.resolve(entry?.stageId);
337
- if (!stageId || seen.has(stageId) || !activeByStage.has(stageId)) {
338
- throw new Error(`Work Center Coordinator guidance references an invalid unfinished Action: ${stageId || '(missing)'}`);
386
+ const reference = dynamic
387
+ ? (typeof entry?.actionId === 'string' ? entry.actionId.trim() : '')
388
+ : stageReferences.resolve(entry?.stageId);
389
+ if (!reference || seen.has(reference) || !activeByReference.has(reference)) {
390
+ throw new Error(`Work Center Coordinator guidance references an invalid unfinished Action: ${reference || '(missing)'}`);
339
391
  }
340
- seen.add(stageId);
392
+ seen.add(reference);
341
393
  return {
342
- stageId,
394
+ ...(dynamic ? { actionId: reference } : { stageId: reference }),
343
395
  instruction: cleanText(entry?.instruction, COORDINATOR_MAX_INSTRUCTION_CHARS, 'guidance instruction'),
344
396
  };
345
397
  });
@@ -368,9 +420,14 @@ export function normalizeCoordinatorResponse(value, detail, options = {}) {
368
420
  const source = parsed?.decision && typeof parsed.decision === 'object' && !Array.isArray(parsed.decision)
369
421
  ? parsed.decision
370
422
  : {};
371
- const allowedKinds = options.recovery === true
372
- ? ['guide_actions', 'replan', 'request_human']
373
- : ['answer', 'guide_actions', 'replan'];
423
+ const dynamic = isDynamicWorkItem(detail);
424
+ const allowedKinds = dynamic
425
+ ? (options.automatic === true
426
+ ? ['create_actions', 'guide_actions', 'request_human', 'complete']
427
+ : ['answer', 'create_actions', 'guide_actions', 'request_human', 'complete'])
428
+ : (options.recovery === true
429
+ ? ['guide_actions', 'replan', 'request_human']
430
+ : ['answer', 'guide_actions', 'replan']);
374
431
  const kind = allowedKinds.includes(source.kind) ? source.kind : '';
375
432
  if (!kind) throw new Error('Work Center Coordinator decision kind is invalid');
376
433
  const reason = cleanText(source.reason, 2_000, 'decision reason');
@@ -411,6 +468,41 @@ export function normalizeCoordinatorResponse(value, detail, options = {}) {
411
468
  },
412
469
  };
413
470
  }
471
+ if (dynamic && kind === 'complete') {
472
+ return {
473
+ reply,
474
+ decision: {
475
+ kind,
476
+ reason,
477
+ completion: source.completion,
478
+ contractPatch: null,
479
+ guidance: [],
480
+ actions: [],
481
+ },
482
+ };
483
+ }
484
+ if (dynamic && kind === 'create_actions') {
485
+ const contractPatch = normalizeContractPatch(source.contractPatch);
486
+ const decision = {
487
+ kind,
488
+ reason,
489
+ workItemType: source.workItemType,
490
+ contractPatch,
491
+ supersedeActionIds: source.supersedeActionIds,
492
+ guidance: [],
493
+ actions: source.actions,
494
+ };
495
+ return {
496
+ reply,
497
+ decision,
498
+ mutation: prepareDynamicActionMutation({
499
+ workItem: detail,
500
+ actions: detail.actions || [],
501
+ decision,
502
+ availableVpIds: options.availableVpIds,
503
+ }),
504
+ };
505
+ }
414
506
  const contractPatch = normalizeContractPatch(source.contractPatch);
415
507
  if (!Array.isArray(source.actions)) {
416
508
  throw new Error('Work Center Coordinator replan requires the complete unfinished Action graph');
@@ -496,6 +588,7 @@ function coordinatorSnapshot(detail) {
496
588
  throw new Error('WorkItem contract cannot be represented within the Coordinator snapshot budget');
497
589
  }
498
590
 
591
+ const dynamic = isDynamicWorkItem(detail);
499
592
  const currentActions = (Array.isArray(detail.actions) ? detail.actions : [])
500
593
  .filter(action => !['superseded', 'cancelled'].includes(action.status));
501
594
  const stageReferences = coordinatorStageReferences(detail);
@@ -510,20 +603,24 @@ function coordinatorSnapshot(detail) {
510
603
  canonicalRunByAction.get(action.id),
511
604
  stageReferences,
512
605
  action.status === 'completed',
606
+ dynamic,
513
607
  ));
514
608
  let actions = boundedJsonArray(projectedActions, COORDINATOR_MAX_ACTIONS_BYTES);
515
- let includedActionIdentities = new Set(actions.map(action => `${action.stageId}:${action.generation}`));
516
- if (unfinished.some(action => !includedActionIdentities.has(`${stageReferences.project(action.stageId)}:${action.generation}`))) {
609
+ const identity = action => `${dynamic ? action.actionId : action.stageId}:${action.generation}`;
610
+ const expectedIdentity = action => `${dynamic ? action.id : stageReferences.project(action.stageId)}:${action.generation}`;
611
+ let includedActionIdentities = new Set(actions.map(identity));
612
+ if (unfinished.some(action => !includedActionIdentities.has(expectedIdentity(action)))) {
517
613
  projectedActions = selected.map(action => boundedAction(
518
614
  action,
519
615
  canonicalRunByAction.get(action.id),
520
616
  stageReferences,
521
617
  true,
618
+ dynamic,
522
619
  ));
523
620
  actions = boundedJsonArray(projectedActions, COORDINATOR_MAX_ACTIONS_BYTES);
524
- includedActionIdentities = new Set(actions.map(action => `${action.stageId}:${action.generation}`));
621
+ includedActionIdentities = new Set(actions.map(identity));
525
622
  }
526
- if (unfinished.some(action => !includedActionIdentities.has(`${stageReferences.project(action.stageId)}:${action.generation}`))) {
623
+ if (unfinished.some(action => !includedActionIdentities.has(expectedIdentity(action)))) {
527
624
  throw new Error('Active Actions cannot be represented within the Coordinator snapshot budget');
528
625
  }
529
626
 
@@ -604,6 +701,34 @@ export class WorkItemCoordinator {
604
701
  });
605
702
  }
606
703
 
704
+ advance(mailboxId, options = {}) {
705
+ if (this.shuttingDown) throw new Error('Work Center Coordinator is shutting down');
706
+ const claim = this.store.claimCoordinatorMailbox(options.workItemId, this.ownerBootId, this.claimLeaseMs);
707
+ if (!claim) return null;
708
+ if (claim.id !== mailboxId) {
709
+ this.store.releaseCoordinatorMailboxClaim(
710
+ claim.id, this.ownerBootId, Number(claim.claim_epoch),
711
+ );
712
+ return null;
713
+ }
714
+ const started = this.store.beginDynamicCoordinatorTurn(mailboxId, {
715
+ ownerBootId: this.ownerBootId,
716
+ claimEpoch: Number(claim.claim_epoch),
717
+ });
718
+ if (!started) {
719
+ this.store.releaseCoordinatorMailboxClaim(
720
+ claim.id, this.ownerBootId, Number(claim.claim_epoch),
721
+ );
722
+ return null;
723
+ }
724
+ options.onUpdate?.('coordinator.advance_started', started.detail);
725
+ const trigger = started.detail.messages?.find(message => message.turnId === started.turnId)?.trigger;
726
+ const text = `Automatic WorkItem advance trigger: ${JSON.stringify(trigger || { kind: claim.kind })}. `
727
+ + 'Observe the current durable state and choose the next justified Actions, a genuine human request, '
728
+ + 'or evidence-backed completion. Do not stop merely because the previous Action ended.';
729
+ return this.#scheduleTurn(started, { text, recovery: false, options });
730
+ }
731
+
607
732
  recover(id, options = {}) {
608
733
  if (this.shuttingDown) throw new Error('Work Center Coordinator is shutting down');
609
734
  const detail = this.store.getWorkItemDetail(id);
@@ -746,7 +871,7 @@ export class WorkItemCoordinator {
746
871
  : latestMessage;
747
872
  const requestBody = {
748
873
  model: resolved.model,
749
- system: coordinatorSystemPrompt(language),
874
+ system: coordinatorSystemPrompt(language, started.detail),
750
875
  messages: [{ role: 'user', content }],
751
876
  maxTokens: Math.min(
752
877
  resolveMaxOutputTokens(resolved.model, runtime.config),
@@ -799,8 +924,11 @@ export class WorkItemCoordinator {
799
924
  }
800
925
  normalized = normalizeCoordinatorResponse(result?.text, started.detail, {
801
926
  recovery,
927
+ automatic: started.fence.automatic === true,
802
928
  recoveryActionId: started.fence.recovery?.actionId || null,
929
+ availableVpIds: vps.map(vp => vp.id),
803
930
  });
931
+ mutation = normalized.mutation || null;
804
932
  if (normalized.decision.kind === 'replan') {
805
933
  finalizedCriteria(started.detail, normalized.decision.contractPatch);
806
934
  mutation = applyCoordinatorReplan({
@@ -873,7 +1001,9 @@ export class WorkItemCoordinator {
873
1001
  attemptCount,
874
1002
  }, started.fence);
875
1003
  if (!detail) return this.store.getWorkItemDetail(started.detail.id);
876
- options.onUpdate?.(recovery ? 'coordinator.recovery_completed' : 'coordinator.turn_completed', detail);
1004
+ options.onUpdate?.(started.fence.automatic === true
1005
+ ? 'coordinator.advance_completed'
1006
+ : recovery ? 'coordinator.recovery_completed' : 'coordinator.turn_completed', detail);
877
1007
  return detail;
878
1008
  } catch (error) {
879
1009
  if (providerTurn?.status === 'responded') {
@@ -1,6 +1,6 @@
1
1
  import { createHash, randomUUID } from 'node:crypto';
2
2
 
3
- export const WORK_CENTER_SCHEMA_VERSION = 35;
3
+ export const WORK_CENTER_SCHEMA_VERSION = 37;
4
4
 
5
5
  const MIGRATIONS = [
6
6
  ['23-conversation-stream', migrateConversationStream],
@@ -16,6 +16,8 @@ const MIGRATIONS = [
16
16
  ['33-coordinator-provider-turns', migrateCoordinatorProviderTurns],
17
17
  ['34-engine-turn-status-repair', repairEngineTurnStatusContract],
18
18
  ['35-coordinator-provider-claims', migrateCoordinatorProviderClaims],
19
+ ['36-dynamic-coordination', migrateDynamicCoordination],
20
+ ['37-run-acceptance-checks', migrateRunAcceptanceChecks],
19
21
  ];
20
22
 
21
23
  const MIGRATION_ALIASES = new Map([
@@ -478,6 +480,54 @@ function migrateCoordinatorProviderClaims(db) {
478
480
  `);
479
481
  }
480
482
 
483
+ function migrateDynamicCoordination(db) {
484
+ if (!hasColumn(db, 'work_items', 'coordination_mode')) {
485
+ db.exec("ALTER TABLE work_items ADD COLUMN coordination_mode TEXT NOT NULL DEFAULT 'legacy'");
486
+ }
487
+ if (!hasColumn(db, 'work_items', 'final_result')) {
488
+ db.exec('ALTER TABLE work_items ADD COLUMN final_result TEXT');
489
+ }
490
+ if (!hasColumn(db, 'actions', 'source_action_ids')) {
491
+ db.exec("ALTER TABLE actions ADD COLUMN source_action_ids TEXT NOT NULL DEFAULT '[]'");
492
+ }
493
+ db.exec(`
494
+ CREATE INDEX IF NOT EXISTS idx_work_items_dynamic_status
495
+ ON work_items(coordination_mode, status, updated_at);
496
+ CREATE TRIGGER IF NOT EXISTS trg_work_item_final_result_immutable
497
+ BEFORE UPDATE OF final_result ON work_items
498
+ WHEN OLD.final_result IS NOT NULL AND NEW.final_result IS NOT OLD.final_result
499
+ BEGIN
500
+ SELECT RAISE(ABORT, 'WorkItem final result is immutable');
501
+ END;
502
+ `);
503
+ }
504
+
505
+ function migrateRunAcceptanceChecks(db) {
506
+ if (!hasColumn(db, 'runs', 'acceptance_checks')) {
507
+ db.exec("ALTER TABLE runs ADD COLUMN acceptance_checks TEXT NOT NULL DEFAULT '[]'");
508
+ }
509
+ db.exec(`
510
+ DROP TRIGGER IF EXISTS trg_runs_terminal_identity_immutable;
511
+ CREATE TRIGGER IF NOT EXISTS trg_runs_terminal_identity_immutable
512
+ BEFORE UPDATE ON runs
513
+ WHEN OLD.terminal_status IS NOT NULL AND (
514
+ NEW.action_id IS NOT OLD.action_id OR NEW.work_item_id IS NOT OLD.work_item_id OR
515
+ NEW.owner_boot_id IS NOT OLD.owner_boot_id OR NEW.lease_epoch IS NOT OLD.lease_epoch OR
516
+ NEW.ordinal IS NOT OLD.ordinal OR NEW.started_at IS NOT OLD.started_at OR
517
+ NEW.status IS NOT OLD.status OR NEW.ended_at IS NOT OLD.ended_at OR
518
+ NEW.terminal_status IS NOT OLD.terminal_status OR NEW.terminal_at IS NOT OLD.terminal_at OR
519
+ NEW.response IS NOT OLD.response OR NEW.summary IS NOT OLD.summary OR
520
+ NEW.evidence IS NOT OLD.evidence OR NEW.acceptance_checks IS NOT OLD.acceptance_checks OR
521
+ NEW.waiting_reason IS NOT OLD.waiting_reason OR NEW.error IS NOT OLD.error OR
522
+ NEW.failure_kind IS NOT OLD.failure_kind OR NEW.failure_code IS NOT OLD.failure_code OR
523
+ NEW.review_decision IS NOT OLD.review_decision OR NEW.contract_patch IS NOT OLD.contract_patch OR
524
+ NEW.checkpoint IS NOT OLD.checkpoint)
525
+ BEGIN
526
+ SELECT RAISE(ABORT, 'terminal Run result is immutable');
527
+ END;
528
+ `);
529
+ }
530
+
481
531
  function migrateReliabilityGuards(db) {
482
532
  for (const [column, definition] of [
483
533
  ['dispatch_capability', "TEXT NOT NULL DEFAULT 'unknown'"],