@yeaft/webchat-agent 1.0.203 → 1.0.204

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.203"}
1
+ {"version":"1.0.204"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yeaft/webchat-agent",
3
- "version": "1.0.203",
3
+ "version": "1.0.204",
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",
@@ -756,6 +756,21 @@ export class WorkItemRunner {
756
756
  };
757
757
  }
758
758
  const dependencies = this.store.listActionDependencies(workItem.id, action.dependsOnStageIds || []);
759
+ if (dependencies.length > 0 && dependencies.every(dependency => (
760
+ dependency.workspaceMode === 'shared' && !dependency.workspace?.isolated
761
+ ))) {
762
+ const persistedAction = this.store.setActionWorkspaceForRun(
763
+ action.id,
764
+ run.id,
765
+ ownerBootId,
766
+ run.leaseEpoch,
767
+ action.generation,
768
+ null,
769
+ 'shared',
770
+ );
771
+ if (!persistedAction) throw integrationFenceError('Work Center integration fallback lost its Run lease');
772
+ return { ...claim, action: persistedAction };
773
+ }
759
774
  const integration = prepareActionIntegration({
760
775
  workDir: workItem.workspaceKey || workItem.workDir,
761
776
  dependencies,
@@ -933,6 +933,21 @@ export class WorkItemStore {
933
933
  const nextWorkspaceMode = workspaceMode || action.workspaceMode;
934
934
  const specChanged = nextWorkspaceMode !== action.workspaceMode;
935
935
  const nextAction = { ...action, workspaceMode: nextWorkspaceMode };
936
+ const now = this.now();
937
+ if (action.workspaceMode === 'isolated-write' && nextWorkspaceMode === 'shared') {
938
+ const workItem = this.getWorkItem(action.workItemId);
939
+ const workspaceConflict = workItem?.workspaceKey
940
+ ? this.db.prepare(`SELECT 1 FROM actions running
941
+ JOIN work_items running_item ON running_item.id = running.work_item_id
942
+ WHERE running.id != ? AND running.status = 'running'
943
+ AND running_item.workspace_key = ? LIMIT 1`).get(action.id, workItem.workspaceKey)
944
+ : null;
945
+ if (workspaceConflict) {
946
+ const error = new Error('Work Center cannot fall back to shared while the workspace has another running Action');
947
+ error.workItemPrepareDeferred = true;
948
+ throw error;
949
+ }
950
+ }
936
951
  const changed = this.db.prepare(`UPDATE actions SET workspace = ?, workspace_mode = ?,
937
952
  generation = generation + ?, spec_hash = ?, result_run_id = CASE WHEN ? = 1 THEN NULL ELSE result_run_id END,
938
953
  updated_at = ? WHERE id = ? AND status = 'running' AND current_run_id = ?
@@ -942,13 +957,90 @@ export class WorkItemStore {
942
957
  specChanged ? 1 : 0,
943
958
  specChanged ? actionSpecHash(nextAction) : action.specHash,
944
959
  specChanged ? 1 : 0,
945
- this.now(),
960
+ now,
946
961
  actionId,
947
962
  runId,
948
963
  leaseEpoch,
949
964
  expectedGeneration,
950
965
  );
951
- return Number(changed.changes) === 1 ? this.getAction(actionId) : null;
966
+ if (Number(changed.changes) !== 1) return null;
967
+
968
+ if (action.workspaceMode === 'isolated-write' && nextWorkspaceMode === 'shared') {
969
+ const pendingRows = this.db.prepare(`SELECT * FROM actions
970
+ WHERE work_item_id = ? AND id != ? AND workspace_mode IN ('isolated-write', 'integrate')
971
+ AND status = 'ready' AND current_run_id IS NULL`).all(action.workItemId, action.id);
972
+ for (const row of pendingRows) {
973
+ const pending = mapAction(row);
974
+ const fallback = { ...pending, workspaceMode: 'shared', workspace: null };
975
+ const repaired = this.db.prepare(`UPDATE actions SET workspace = NULL, workspace_mode = 'shared',
976
+ generation = generation + 1, spec_hash = ?, result_run_id = NULL, updated_at = ?
977
+ WHERE id = ? AND status = 'ready' AND current_run_id IS NULL
978
+ AND generation = ? AND workspace_mode = ?`).run(
979
+ actionSpecHash(fallback),
980
+ now,
981
+ pending.id,
982
+ pending.generation,
983
+ pending.workspaceMode,
984
+ );
985
+ if (Number(repaired.changes) !== 1) {
986
+ throw new Error('Work Center could not serialize the pending Action graph after workspace fallback');
987
+ }
988
+ }
989
+ }
990
+ return this.getAction(actionId);
991
+ });
992
+ }
993
+
994
+ #graphWorkItemState(workItemId) {
995
+ const remaining = this.db.prepare(`SELECT id, status FROM actions WHERE work_item_id = ?
996
+ AND status IN ('ready', 'running', 'waiting', 'failed') ORDER BY sequence`).all(workItemId);
997
+ const blocked = remaining.find(candidate => candidate.status === 'waiting' || candidate.status === 'failed');
998
+ const runnable = remaining.find(candidate => candidate.status === 'ready' || candidate.status === 'running');
999
+ return {
1000
+ status: blocked ? (blocked.status === 'waiting' ? 'waiting' : 'needs_attention')
1001
+ : runnable ? (remaining.some(candidate => candidate.status === 'running') ? 'running' : 'ready')
1002
+ : 'done',
1003
+ currentActionId: blocked?.id || runnable?.id || null,
1004
+ };
1005
+ }
1006
+
1007
+ deferRun(runId, ownerBootId, leaseEpoch, reason = 'Work Center resource is temporarily busy') {
1008
+ return withTransaction(this.db, () => {
1009
+ const active = this.#activeRunRow(runId, ownerBootId, leaseEpoch, true);
1010
+ if (!active) return null;
1011
+ const action = this.getAction(active.action_id);
1012
+ const workItem = this.getWorkItem(active.work_item_id);
1013
+ const now = this.now();
1014
+ const changedRun = this.db.prepare(`UPDATE runs SET status = 'interrupted', ended_at = ?, error = ?,
1015
+ failure_kind = 'resource_deferred', failure_code = 'workspace_busy'
1016
+ WHERE id = ? AND owner_boot_id = ? AND lease_epoch = ? AND status = 'running'`).run(
1017
+ now, reason, runId, ownerBootId, leaseEpoch,
1018
+ );
1019
+ if (Number(changedRun.changes) !== 1) return null;
1020
+ const changedAction = this.db.prepare(`UPDATE actions SET status = 'ready', attempt = MAX(attempt - 1, 0),
1021
+ current_run_id = NULL, updated_at = ? WHERE id = ? AND status = 'running'
1022
+ AND current_run_id = ? AND lease_epoch = ? AND generation = ?`).run(
1023
+ now, action.id, runId, leaseEpoch, action.generation,
1024
+ );
1025
+ if (Number(changedAction.changes) !== 1) {
1026
+ throw new Error('Work Center deferred Run lost the current Action fence');
1027
+ }
1028
+ const graphMode = isGraphWorkItem(workItem);
1029
+ const graphState = graphMode ? this.#graphWorkItemState(workItem.id) : null;
1030
+ const changedWorkItem = graphMode
1031
+ ? this.db.prepare(`UPDATE work_items SET status = ?, current_action_id = ?, current_run_id = NULL,
1032
+ updated_at = ? WHERE id = ? AND status IN ('ready', 'running', 'waiting', 'needs_attention')`).run(
1033
+ graphState.status, graphState.currentActionId, now, workItem.id,
1034
+ )
1035
+ : this.db.prepare(`UPDATE work_items SET status = 'ready', current_run_id = NULL, updated_at = ?
1036
+ WHERE id = ? AND status = 'running' AND current_action_id = ? AND current_run_id = ?`).run(
1037
+ now, workItem.id, action.id, runId,
1038
+ );
1039
+ if (Number(changedWorkItem.changes) !== 1) {
1040
+ throw new Error('Work Center deferred Run lost the WorkItem fence');
1041
+ }
1042
+ this.appendEvent(workItem.id, 'run.deferred', { reason }, { actionId: action.id, runId });
1043
+ return this.getWorkItemDetail(workItem.id);
952
1044
  });
953
1045
  }
954
1046
 
@@ -1444,6 +1536,19 @@ export class WorkItemStore {
1444
1536
  OR (running.work_item_id != a.work_item_id
1445
1537
  AND a.workspace_mode != 'read' AND running.workspace_mode != 'read'))
1446
1538
  )
1539
+ AND NOT EXISTS (
1540
+ SELECT 1 FROM runs deferred
1541
+ WHERE deferred.action_id = a.id
1542
+ AND deferred.failure_kind = 'resource_deferred'
1543
+ AND deferred.failure_code = 'workspace_busy'
1544
+ AND EXISTS (
1545
+ SELECT 1 FROM actions blocker
1546
+ JOIN work_items blocker_item ON blocker_item.id = blocker.work_item_id
1547
+ WHERE blocker.status = 'running' AND blocker.id != a.id
1548
+ AND blocker_item.workspace_key != ''
1549
+ AND blocker_item.workspace_key = w.workspace_key
1550
+ )
1551
+ )
1447
1552
  ORDER BY a.updated_at ASC, a.sequence ASC LIMIT 1`).get();
1448
1553
  if (!row) return null;
1449
1554
  const now = this.now();
@@ -1858,14 +1963,9 @@ export class WorkItemStore {
1858
1963
  let currentActionId = nextAction?.id ?? (transition.keepCurrentAction ? action.id : null);
1859
1964
  let changedWorkItem;
1860
1965
  if (transition.graphAdvance) {
1861
- const remaining = this.db.prepare(`SELECT id, status FROM actions WHERE work_item_id = ?
1862
- AND status IN ('ready', 'running', 'waiting', 'failed') ORDER BY sequence`).all(workItem.id);
1863
- const blocked = remaining.find(candidate => candidate.status === 'waiting' || candidate.status === 'failed');
1864
- const runnable = remaining.find(candidate => candidate.status === 'ready' || candidate.status === 'running');
1865
- workItemStatus = blocked ? (blocked.status === 'waiting' ? 'waiting' : 'needs_attention')
1866
- : runnable ? (remaining.some(candidate => candidate.status === 'running') ? 'running' : 'ready')
1867
- : 'done';
1868
- currentActionId = blocked?.id || runnable?.id || null;
1966
+ const graphState = this.#graphWorkItemState(workItem.id);
1967
+ workItemStatus = graphState.status;
1968
+ currentActionId = graphState.currentActionId;
1869
1969
  changedWorkItem = this.db.prepare(`UPDATE work_items SET status = ?, current_action_id = ?,
1870
1970
  current_run_id = NULL, ledger_revision = ledger_revision + ?, updated_at = ?
1871
1971
  WHERE id = ? AND status IN ('ready', 'running', 'waiting', 'needs_attention')
@@ -143,6 +143,17 @@ export class WorkItemWatcher {
143
143
  );
144
144
  break;
145
145
  }
146
+ if (error?.workItemPrepareDeferred) {
147
+ const detail = this.store.deferRun(
148
+ claim.run.id,
149
+ this.ownerBootId,
150
+ claim.run.leaseEpoch,
151
+ error.message,
152
+ );
153
+ if (!detail) throw new Error('Work Center deferred preparation lost its Run lease');
154
+ this.onEvent({ type: 'run.deferred', workItem: detail });
155
+ break;
156
+ }
146
157
  this.controller.submit(claim.run.id, this.ownerBootId, claim.run.leaseEpoch, {
147
158
  outcome: error?.workItemPrepareRetryable ? 'retryable' : 'failed',
148
159
  response: '', summary: '', evidence: [],
@@ -373,7 +373,7 @@ export function resolvePlanningWorkflowSnapshot(settings, requestedWorkItemType
373
373
  const typeInstruction = requestedType
374
374
  ? `The user explicitly selected workItemType "${requestedType}". Keep that exact type.`
375
375
  : 'Infer one specific workItemType from the contract.';
376
- const triageInstruction = `${normalized.actionInstructions.triage}\n\n${typeInstruction}\nReference workflow catalog:\n${catalog || '(none)'}\nUse the catalog only to understand established task categories and sequencing patterns. Always submit the smallest reliable graph of 1 to 8 task-specific Actions; never omit Actions or copy template brief text. Split independent work into separate Actions and declare dependsOnActionIds. Use workspaceMode read for analysis, isolated-write for independent Git changes, integrate for an integrate Action that combines isolated-write dependencies, and shared for serial side effects. If any Action uses isolated-write, add exactly one Action with type integrate and workspaceMode integrate; it must depend directly on every isolated-write Action, and all later Actions must depend on the integration result rather than an isolated-write Action. Non-Git or dirty workspaces are serialized automatically. Every generated Action must state objective, approach, expectedOutcome, capability, dependencies, and workspaceMode. The objective, approach, and expectedOutcome must be specific to this WorkItem and that Action: describe the concrete work, the repository-aware execution method, and the verifiable result that will guide the executor. Generic Action-type boilerplate is invalid. Add only Actions required by this task. Do not copy a generic workflow.`;
376
+ const triageInstruction = `${normalized.actionInstructions.triage}\n\n${typeInstruction}\nReference workflow catalog:\n${catalog || '(none)'}\nUse the catalog only to understand established task categories and sequencing patterns. Always submit the smallest reliable graph of 1 to 8 task-specific Actions; never omit Actions or copy template brief text. The scheduler can run up to ${normalized.maxConcurrentActions} Actions concurrently. Before submitting, compare each pair of Actions and add a dependency only when one consumes a concrete result or side effect of the other; ordering by narrative, phase name, or list position is not a dependency. Split independent analysis, verification, and repository changes into sibling Actions so the scheduler can use that concurrency. Use workspaceMode read only for Actions guaranteed not to mutate files, Git state, services, or external systems; use isolated-write for independent Git changes, integrate for an integrate Action that combines isolated-write dependencies, and shared for serial side effects. If any Action uses isolated-write, add exactly one Action with type integrate and workspaceMode integrate; it must depend directly on every isolated-write Action, and all later Actions must depend on the integration result rather than an isolated-write Action. Non-Git or dirty workspaces are serialized automatically; do not fake parallelism by marking a mutating Action as read. Every generated Action must state objective, approach, expectedOutcome, capability, dependencies, and workspaceMode. The objective, approach, and expectedOutcome must be specific to this WorkItem and that Action: describe the concrete work, the repository-aware execution method, and the verifiable result that will guide the executor. Generic Action-type boilerplate is invalid. Add only Actions required by this task. Do not copy a generic workflow.`;
377
377
  return normalizeWorkflowDefinition({
378
378
  id: 'ai-planned',
379
379
  name: 'AI planned',