@yeaft/webchat-agent 1.0.195 → 1.0.197

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.
@@ -5,7 +5,7 @@ import { createHash, randomUUID } from 'node:crypto';
5
5
  import { normalizeEvidence } from './evidence.js';
6
6
  import { normalizeActionCheckpoint } from './action-checkpoint.js';
7
7
 
8
- const SCHEMA_VERSION = 14;
8
+ const SCHEMA_VERSION = 15;
9
9
  const OPEN_ACTION_STATUSES = "'ready','running','waiting'";
10
10
  const MAX_REUSABLE_CONTEXT_ITEMS = 12;
11
11
  const MAX_RUN_RESPONSE_CHARS = 65_536;
@@ -195,6 +195,7 @@ function mapRun(row) {
195
195
  totalTokens: Math.max(0, Number(row.total_tokens) || 0),
196
196
  progressRevision: Math.max(0, Number(row.progress_revision) || 0),
197
197
  checkpoint: normalizeActionCheckpoint(parseJson(row.checkpoint, null)),
198
+ acceptingInput: row.accepting_input !== 0,
198
199
  };
199
200
  }
200
201
 
@@ -350,7 +351,8 @@ export class WorkItemStore {
350
351
  cache_write_tokens INTEGER NOT NULL DEFAULT 0,
351
352
  total_tokens INTEGER NOT NULL DEFAULT 0,
352
353
  progress_revision INTEGER NOT NULL DEFAULT 0,
353
- checkpoint TEXT
354
+ checkpoint TEXT,
355
+ accepting_input INTEGER NOT NULL DEFAULT 1
354
356
  );
355
357
  CREATE TABLE IF NOT EXISTS events (
356
358
  id INTEGER PRIMARY KEY AUTOINCREMENT,
@@ -361,6 +363,15 @@ export class WorkItemStore {
361
363
  data TEXT NOT NULL,
362
364
  created_at INTEGER NOT NULL
363
365
  );
366
+ CREATE TABLE IF NOT EXISTS pending_action_inputs (
367
+ event_id INTEGER PRIMARY KEY REFERENCES events(id) ON DELETE CASCADE,
368
+ work_item_id TEXT NOT NULL REFERENCES work_items(id) ON DELETE CASCADE,
369
+ action_id TEXT NOT NULL REFERENCES actions(id) ON DELETE CASCADE,
370
+ run_id TEXT,
371
+ text TEXT NOT NULL,
372
+ attachments TEXT NOT NULL DEFAULT '[]',
373
+ consumed_at INTEGER
374
+ );
364
375
  CREATE TABLE IF NOT EXISTS plan_audits (
365
376
  id INTEGER PRIMARY KEY AUTOINCREMENT,
366
377
  work_item_id TEXT NOT NULL REFERENCES work_items(id) ON DELETE CASCADE,
@@ -391,6 +402,8 @@ export class WorkItemStore {
391
402
  CREATE INDEX IF NOT EXISTS idx_actions_ready ON actions(status, updated_at, sequence);
392
403
  CREATE INDEX IF NOT EXISTS idx_runs_active ON runs(status, expires_at);
393
404
  CREATE INDEX IF NOT EXISTS idx_events_work_item ON events(work_item_id, id);
405
+ CREATE INDEX IF NOT EXISTS idx_pending_action_inputs_action
406
+ ON pending_action_inputs(action_id, consumed_at, event_id);
394
407
  `);
395
408
 
396
409
  // The feature shipped first as an unmerged PR, but keep the store tolerant
@@ -459,6 +472,9 @@ export class WorkItemStore {
459
472
  if (!hasColumn(this.db, 'runs', 'checkpoint')) {
460
473
  this.db.exec('ALTER TABLE runs ADD COLUMN checkpoint TEXT');
461
474
  }
475
+ if (!hasColumn(this.db, 'runs', 'accepting_input')) {
476
+ this.db.exec('ALTER TABLE runs ADD COLUMN accepting_input INTEGER NOT NULL DEFAULT 1');
477
+ }
462
478
  if (!hasColumn(this.db, 'work_items', 'workflow_snapshot')) {
463
479
  this.db.exec('ALTER TABLE work_items ADD COLUMN workflow_snapshot TEXT');
464
480
  }
@@ -535,6 +551,110 @@ export class WorkItemStore {
535
551
  return Number(result.lastInsertRowid);
536
552
  }
537
553
 
554
+ addActionInput(id, input, expected, updateReadyAction, attachments = null, addedAttachments = []) {
555
+ return withTransaction(this.db, () => {
556
+ const workItem = this.getWorkItem(id);
557
+ if (!workItem) return null;
558
+ if (!['ready', 'running'].includes(workItem.status)) {
559
+ throw new Error(`WorkItem in ${workItem.status} cannot accept Action input`);
560
+ }
561
+ const action = this.getAction(expected.actionId);
562
+ const graphMode = workItem.workflowSnapshot?.executionMode === 'graph';
563
+ const actionMatches = graphMode
564
+ ? action?.workItemId === id && ['ready', 'running'].includes(action.status)
565
+ : action?.id === workItem.currentActionId && ['ready', 'running'].includes(action?.status);
566
+ const activeRun = action?.currentRunId ? this.getRun(action.currentRunId) : null;
567
+ if (!actionMatches || activeRun?.acceptingInput === false || workItem.revision !== expected.revision) {
568
+ throw new Error('Action changed before input was applied; refresh and try again');
569
+ }
570
+ const now = this.now();
571
+ const revision = workItem.revision + 1;
572
+ const updated = updateReadyAction(workItem, action);
573
+ const changedAction = this.db.prepare(`UPDATE actions SET context = ?, instruction = ?, updated_at = ?
574
+ WHERE id = ? AND status = ? AND current_run_id IS ?`).run(
575
+ stringify(updated.context || []),
576
+ updated.instruction || action.instruction,
577
+ now,
578
+ action.id,
579
+ action.status,
580
+ action.currentRunId,
581
+ );
582
+ if (Number(changedAction.changes) !== 1) {
583
+ throw new Error('Action changed before input was applied; refresh and try again');
584
+ }
585
+ this.db.prepare(`UPDATE work_items SET attachments = ?, revision = ?, updated_at = ?
586
+ WHERE id = ?`).run(
587
+ stringify(Array.isArray(attachments) ? attachments : workItem.attachments),
588
+ revision,
589
+ now,
590
+ id,
591
+ );
592
+ const projectedAttachments = (Array.isArray(addedAttachments) ? addedAttachments : []).map(attachment => ({
593
+ id: attachment.id,
594
+ name: attachment.name,
595
+ mimeType: attachment.mimeType,
596
+ size: Math.max(0, Number(attachment.size) || 0),
597
+ isImage: attachment.isImage === true,
598
+ }));
599
+ const eventId = this.appendEvent(id, 'action.input_added', {
600
+ text: input,
601
+ attachments: projectedAttachments,
602
+ }, { actionId: action.id, runId: action.currentRunId });
603
+ if (action.status === 'running') {
604
+ this.db.prepare(`INSERT INTO pending_action_inputs
605
+ (event_id, work_item_id, action_id, run_id, text, attachments, consumed_at)
606
+ VALUES (?, ?, ?, ?, ?, ?, NULL)`).run(
607
+ eventId,
608
+ id,
609
+ action.id,
610
+ action.currentRunId,
611
+ input,
612
+ stringify(projectedAttachments),
613
+ );
614
+ }
615
+ return this.getWorkItemDetail(id);
616
+ });
617
+ }
618
+
619
+ listPendingActionInputs(actionId, runId, ownerBootId, leaseEpoch) {
620
+ const active = this.#activeRunRow(runId, ownerBootId, leaseEpoch, true);
621
+ if (!active || active.action_id !== actionId) return [];
622
+ return this.db.prepare(`SELECT * FROM pending_action_inputs
623
+ WHERE action_id = ? AND consumed_at IS NULL ORDER BY event_id`).all(actionId).map(row => ({
624
+ id: String(row.event_id),
625
+ text: row.text || '',
626
+ attachments: parseJson(row.attachments, []),
627
+ }));
628
+ }
629
+
630
+ acknowledgeActionInput(eventId, actionId, runId, ownerBootId, leaseEpoch) {
631
+ return withTransaction(this.db, () => {
632
+ const active = this.#activeRunRow(runId, ownerBootId, leaseEpoch, true);
633
+ if (!active || active.action_id !== actionId) return false;
634
+ const result = this.db.prepare(`UPDATE pending_action_inputs SET consumed_at = ?
635
+ WHERE event_id = ? AND action_id = ? AND consumed_at IS NULL`).run(
636
+ this.now(), Number(eventId), actionId,
637
+ );
638
+ return Number(result.changes) === 1;
639
+ });
640
+ }
641
+
642
+ appendRunLoop(runId, ownerBootId, leaseEpoch, loop = {}) {
643
+ return withTransaction(this.db, () => {
644
+ const active = this.#activeRunRow(runId, ownerBootId, leaseEpoch, true);
645
+ if (!active) return null;
646
+ const response = normalizeRunResponse(loop.response).trim();
647
+ if (response) {
648
+ this.appendEvent(active.work_item_id, 'run.loop_output', {
649
+ loopNumber: Math.max(0, Number(loop.loopNumber) || 0),
650
+ response,
651
+ stopReason: loop.stopReason || null,
652
+ }, { actionId: active.action_id, runId });
653
+ }
654
+ return this.getWorkItemDetail(active.work_item_id);
655
+ });
656
+ }
657
+
538
658
  createPlanConflict(workItemId, input = {}) {
539
659
  const id = input.id || randomUUID();
540
660
  const now = this.now();
@@ -1001,6 +1121,10 @@ export class WorkItemStore {
1001
1121
  return graphExecutionState(detail, detail.actions);
1002
1122
  }
1003
1123
 
1124
+ listActionEvents(actionId) {
1125
+ return this.db.prepare(`SELECT * FROM events WHERE action_id = ? ORDER BY id`).all(actionId).map(mapEvent);
1126
+ }
1127
+
1004
1128
  getReusableContext(workDir, excludeWorkItemId = null) {
1005
1129
  const workspaceKey = canonicalWorkspaceKey(workDir);
1006
1130
  if (!workspaceKey) return [];
@@ -1544,12 +1668,29 @@ export class WorkItemStore {
1544
1668
  };
1545
1669
  }
1546
1670
 
1671
+ closeRunInput(runId, ownerBootId, leaseEpoch) {
1672
+ return withTransaction(this.db, () => {
1673
+ const active = this.#activeRunRow(runId, ownerBootId, leaseEpoch, true);
1674
+ if (!active) return false;
1675
+ const pendingInput = this.db.prepare(`SELECT event_id FROM pending_action_inputs
1676
+ WHERE action_id = ? AND consumed_at IS NULL LIMIT 1`).get(active.action_id);
1677
+ if (pendingInput) return false;
1678
+ const changed = this.db.prepare(`UPDATE runs SET accepting_input = 0
1679
+ WHERE id = ? AND owner_boot_id = ? AND lease_epoch = ? AND status = 'running'
1680
+ AND accepting_input = 1`).run(runId, ownerBootId, leaseEpoch);
1681
+ return Number(changed.changes) === 1;
1682
+ });
1683
+ }
1684
+
1547
1685
  finalizeRun(runId, ownerBootId, leaseEpoch, result, makeTransition) {
1548
1686
  return withTransaction(this.db, () => {
1549
1687
  const active = this.#activeRunRow(runId, ownerBootId, leaseEpoch, true);
1550
- if (!active) return null;
1688
+ if (!active || Number(active.accepting_input) !== 0) return null;
1551
1689
  const action = this.getAction(active.action_id);
1552
1690
  const workItem = this.getWorkItem(active.work_item_id);
1691
+ const pendingInput = this.db.prepare(`SELECT event_id FROM pending_action_inputs
1692
+ WHERE action_id = ? AND consumed_at IS NULL LIMIT 1`).get(action.id);
1693
+ if (pendingInput) throw new Error('Run has unconsumed Action input and cannot finish yet');
1553
1694
  const priorRuns = this.db.prepare(`SELECT * FROM runs
1554
1695
  WHERE work_item_id = ? AND id != ? AND status != 'running'
1555
1696
  ORDER BY started_at ASC`).all(workItem.id, runId).map(mapRun);
@@ -1727,14 +1868,15 @@ export class WorkItemStore {
1727
1868
  currentActionId = blocked?.id || runnable?.id || null;
1728
1869
  changedWorkItem = this.db.prepare(`UPDATE work_items SET status = ?, current_action_id = ?,
1729
1870
  current_run_id = NULL, ledger_revision = ledger_revision + ?, updated_at = ?
1730
- WHERE id = ? AND status IN ('ready', 'running', 'waiting', 'needs_attention')`).run(
1731
- workItemStatus, currentActionId, ledgerIncrement, now, workItem.id,
1871
+ WHERE id = ? AND status IN ('ready', 'running', 'waiting', 'needs_attention')
1872
+ AND revision = ?`).run(
1873
+ workItemStatus, currentActionId, ledgerIncrement, now, workItem.id, nextWorkItem.revision,
1732
1874
  );
1733
1875
  } else {
1734
1876
  changedWorkItem = this.db.prepare(`UPDATE work_items SET status = ?, current_action_id = ?,
1735
1877
  current_run_id = NULL, ledger_revision = ledger_revision + ?, updated_at = ? WHERE id = ? AND current_run_id = ?
1736
- AND current_action_id = ? AND status = 'running'`).run(
1737
- workItemStatus, currentActionId, ledgerIncrement, now, workItem.id, runId, action.id,
1878
+ AND current_action_id = ? AND status = 'running' AND revision = ?`).run(
1879
+ workItemStatus, currentActionId, ledgerIncrement, now, workItem.id, runId, action.id, nextWorkItem.revision,
1738
1880
  );
1739
1881
  }
1740
1882
  if (Number(changedWorkItem.changes) !== 1) {
@@ -88,6 +88,13 @@ export class WorkItemWatcher {
88
88
  }
89
89
  }
90
90
 
91
+ notifyActionInput(workItemId, actionId) {
92
+ for (const entry of this.activeRuns.values()) {
93
+ if (entry.workItemId !== workItemId || entry.actionId !== actionId) continue;
94
+ try { entry.wakeForPendingUserMessage?.(); } catch {}
95
+ }
96
+ }
97
+
91
98
  #recoverExpiredRuns() {
92
99
  const recovered = this.store.recoverInterruptedRuns?.(this.ownerBootId) || 0;
93
100
  if (recovered > 0) {
@@ -172,11 +179,15 @@ export class WorkItemWatcher {
172
179
  readFinalProgress: null,
173
180
  interrupted: false,
174
181
  workItemId: claim.workItem.id,
182
+ actionId: claim.action.id,
175
183
  runId: claim.run.id,
176
184
  leaseEpoch: claim.run.leaseEpoch,
185
+ wakeForPendingUserMessage: null,
177
186
  };
178
187
  entry.promise = this.#execute(claim, abortController.signal, readProgress => {
179
188
  entry.readFinalProgress = readProgress;
189
+ }, wake => {
190
+ entry.wakeForPendingUserMessage = wake;
180
191
  }).finally(() => {
181
192
  clearInterval(renewal);
182
193
  this.activeRuns.delete(key);
@@ -186,7 +197,7 @@ export class WorkItemWatcher {
186
197
  this.onEvent({ type: 'run.started', workItem: this.store.getWorkItemDetail(claim.workItem.id) });
187
198
  }
188
199
 
189
- async #execute(claim, signal, registerProgressReader) {
200
+ async #execute(claim, signal, registerProgressReader, registerInputWake) {
190
201
  try {
191
202
  let result;
192
203
  try {
@@ -195,6 +206,7 @@ export class WorkItemWatcher {
195
206
  signal,
196
207
  ownerBootId: this.ownerBootId,
197
208
  registerProgressReader,
209
+ registerInputWake,
198
210
  onProgress: progress => {
199
211
  const detail = this.store.updateRunProgress(
200
212
  claim.run.id,