@yeaft/webchat-agent 1.0.560 → 1.0.564

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yeaft/webchat-agent",
3
- "version": "1.0.560",
3
+ "version": "1.0.564",
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",
@@ -17,6 +17,7 @@
17
17
 
18
18
  import { delimiter, join } from 'node:path';
19
19
  import { COLLAB_TOOL_POLICY } from './tools/registry.js';
20
+ import { createFullRegistry } from './tools/index.js';
20
21
  import { existsSync, lstatSync } from 'node:fs';
21
22
  import { randomUUID } from 'node:crypto';
22
23
  import { DEFAULT_YEAFT_DIR } from './init.js';
@@ -3360,9 +3361,15 @@ function resolvePluginCatalogRuntime(workDir = '') {
3360
3361
  const active = runtimeBelongsToOwner(runtime, owner)
3361
3362
  ? runtime
3362
3363
  : (baseRuntime && runtimeBelongsToOwner(baseRuntime, owner) ? baseRuntime : null);
3364
+ const yeaftDir = ctx.CONFIG?.yeaftDir || session?.yeaftDir || DEFAULT_YEAFT_DIR;
3365
+ // Inventory belongs to the Agent, not an initialized inference runtime.
3366
+ // Read Skills afresh for the requested scope (empty means Agent-only), so a
3367
+ // cold/unvisited project never inherits another Session's Skills. Do not boot
3368
+ // a Session, load a provider, or connect MCP just to inspect capabilities.
3363
3369
  return {
3364
- toolRegistry: session?.toolRegistry || null,
3365
- skillManager: active?.skillManager || session?.skillManager || null,
3370
+ toolRegistry: createFullRegistry(),
3371
+ skillManager: createSkillManager(yeaftDir, normalizedWorkDir),
3372
+ mcpConfig: loadPluginCatalogMcpConfig(yeaftDir),
3366
3373
  mcpManager: active?.mcpManager || session?.mcpManager || null,
3367
3374
  };
3368
3375
  }
@@ -3439,11 +3446,8 @@ function reloadManagedSkillRuntime(scope, workDir = '') {
3439
3446
  if (scope === 'user') reloadActiveSkills(owner);
3440
3447
  }
3441
3448
 
3442
- function managedSkillCatalog(yeaftDir, workDir = '') {
3443
- const normalizedWorkDir = normalizeSessionWorkDir(workDir);
3444
- const runtime = resolvePluginCatalogRuntime(normalizedWorkDir);
3445
- const manager = createSkillManager(yeaftDir, normalizedWorkDir || process.cwd());
3446
- return buildPluginCatalog({ ...runtime, skillManager: manager });
3449
+ function managedSkillCatalog(workDir = '') {
3450
+ return buildPluginCatalog(resolvePluginCatalogRuntime(workDir));
3447
3451
  }
3448
3452
 
3449
3453
  /**
@@ -3479,7 +3483,7 @@ export function handleYeaftManagedSkill(msg = {}) {
3479
3483
  ? createManagedSkill(join(yeaftDir, 'skills'), msg.skill || {})
3480
3484
  : removeManagedSkill(join(yeaftDir, 'skills'), msg.name));
3481
3485
  reloadManagedSkillRuntime(scope, managedSession?.workDir || '');
3482
- const catalog = managedSkillCatalog(yeaftDir, managedSession?.workDir || '');
3486
+ const catalog = managedSkillCatalog(managedSession?.workDir || '');
3483
3487
  respond({ scope, sessionId: managedSession?.sessionId || null, result, catalog, error: null });
3484
3488
  } catch (err) {
3485
3489
  respond({ scope, sessionId: null, catalog: { tools: [], skills: [], skillSources: [], mcpServers: [] }, error: err?.message || String(err) });
@@ -3495,10 +3499,7 @@ export function handleYeaftPluginCatalog(msg = {}) {
3495
3499
  const requestId = msg.requestId || null;
3496
3500
  const requestedWorkDir = normalizeSessionWorkDir(msg.workDir);
3497
3501
  try {
3498
- const runtime = resolvePluginCatalogRuntime(requestedWorkDir);
3499
- const yeaftDir = ctx.CONFIG?.yeaftDir || session?.yeaftDir || DEFAULT_YEAFT_DIR;
3500
- runtime.mcpConfig = loadPluginCatalogMcpConfig(yeaftDir);
3501
- const catalog = buildPluginCatalog(runtime);
3502
+ const catalog = buildPluginCatalog(resolvePluginCatalogRuntime(requestedWorkDir));
3502
3503
  sendToServer({
3503
3504
  type: 'yeaft_plugin_catalog_result',
3504
3505
  requestId,
@@ -164,6 +164,19 @@ export class WorkflowController {
164
164
  return detail;
165
165
  }
166
166
 
167
+ startScheduled(id, scheduledAt) {
168
+ return this.store.startWorkItemAtomic(id, workItem => {
169
+ const action = initialActionFor(workItem);
170
+ if (workItem.reuseMemory === false) return action;
171
+ const context = this.store.getReusableContext(workItem.workDir, workItem.id);
172
+ return {
173
+ ...action,
174
+ context,
175
+ instruction: actionInstruction(action, workItem, context, renderSessionContextSnapshot(workItem.sessionContext)),
176
+ };
177
+ }, { scheduledAt });
178
+ }
179
+
167
180
  update(id, patch) {
168
181
  const updated = this.store.updateWorkItemAtomic(id, patch, initialActionFor);
169
182
  if (!updated) throw new Error(`WorkItem not found: ${id}`);
@@ -20,6 +20,7 @@ const MIGRATIONS = [
20
20
  ['37-run-acceptance-checks', migrateRunAcceptanceChecks],
21
21
  ['38-action-closure-and-outputs', migrateActionClosureAndOutputs],
22
22
  ['39-action-creation-source', migrateActionCreationSource],
23
+ ['40-work-item-schedules', migrateWorkItemSchedules],
23
24
  ];
24
25
 
25
26
  const MIGRATION_ALIASES = new Map([
@@ -566,6 +567,21 @@ function migrateActionClosureAndOutputs(db) {
566
567
  `);
567
568
  }
568
569
 
570
+ function migrateWorkItemSchedules(db) {
571
+ for (const [column, definition] of [
572
+ ['schedule_status', 'TEXT'],
573
+ ['scheduled_for', 'INTEGER'],
574
+ ['schedule_triggered_at', 'INTEGER'],
575
+ ]) {
576
+ if (!hasColumn(db, 'work_items', column)) {
577
+ db.exec(`ALTER TABLE work_items ADD COLUMN ${column} ${definition}`);
578
+ }
579
+ }
580
+ db.exec(`CREATE INDEX IF NOT EXISTS idx_work_items_schedule_due
581
+ ON work_items(schedule_status, scheduled_for)
582
+ WHERE schedule_status = 'scheduled'`);
583
+ }
584
+
569
585
  function migrateActionCreationSource(db) {
570
586
  if (!hasColumn(db, 'actions', 'creation_source')) {
571
587
  db.exec("ALTER TABLE actions ADD COLUMN creation_source TEXT NOT NULL DEFAULT 'legacy'");
@@ -1094,6 +1094,7 @@ export function projectWorkItemDetail(detail, options = {}) {
1094
1094
  executionControl: detail.executionControl,
1095
1095
  executionStats: combinedExecutionStats(detail),
1096
1096
  reuseMemory: detail.reuseMemory !== false,
1097
+ schedule: detail.schedule || null,
1097
1098
  deliveryTarget: ['response', 'workspace_files', 'pull_request', 'merge'].includes(detail.deliveryTarget)
1098
1099
  ? detail.deliveryTarget : null,
1099
1100
  waitingReason: sanitizeDiagnosticText(waitingReason(detail), MAX_ACTION_DIAGNOSTIC_CHARS),
@@ -1204,6 +1205,7 @@ export function projectWorkItemSummary(detail) {
1204
1205
  executionStats: combinedExecutionStats(detail),
1205
1206
  executionControl: detail.executionControl,
1206
1207
  origin: detail.origin?.sessionId ? { sessionId: detail.origin.sessionId } : null,
1208
+ schedule: detail.schedule || null,
1207
1209
  linkedSessionIds: Array.isArray(detail.linkedSessionIds) ? detail.linkedSessionIds : [],
1208
1210
  attachmentCount: Array.isArray(detail.attachments) ? detail.attachments.length : 0,
1209
1211
  createdAt: detail.createdAt,
@@ -1244,6 +1246,7 @@ export function projectWorkItemSummary(detail) {
1244
1246
  currentAction: projectCurrentActionSummary(action, projectedAction),
1245
1247
  actionStats: projectActionStats(detail, null),
1246
1248
  origin: detail.origin?.sessionId ? { sessionId: detail.origin.sessionId } : null,
1249
+ schedule: detail.schedule || null,
1247
1250
  linkedSessionIds: Array.isArray(detail.linkedSessionIds) ? detail.linkedSessionIds : [],
1248
1251
  attachmentCount: Array.isArray(detail.attachments) ? detail.attachments.length : 0,
1249
1252
  createdAt: detail.createdAt,
@@ -97,6 +97,7 @@ export class WorkCenterService {
97
97
  constructor(options) {
98
98
  const yeaftDir = requiredString(options?.yeaftDir, 'yeaftDir');
99
99
  this.yeaftDir = yeaftDir;
100
+ this.now = typeof options.now === 'function' ? options.now : () => Date.now();
100
101
  this.settingsReader = options.settingsReader || readWorkCenterSettings;
101
102
  this.settingsWriter = options.settingsWriter || writeWorkCenterSettings;
102
103
  this.runtimeInfoProvider = typeof options.runtimeInfoProvider === 'function'
@@ -246,7 +247,13 @@ export class WorkCenterService {
246
247
  root: this.attachmentRoot,
247
248
  workItemId,
248
249
  });
249
- const shouldStart = payload.start === undefined ? settings.startImmediately : payload.start !== false;
250
+ const scheduledFor = payload.scheduledFor == null || payload.scheduledFor === ''
251
+ ? null : Number(payload.scheduledFor);
252
+ if (scheduledFor != null && (!Number.isSafeInteger(scheduledFor) || scheduledFor <= this.now())) {
253
+ throw new Error('scheduledFor must be in the future');
254
+ }
255
+ const shouldStart = scheduledFor == null
256
+ && (payload.start === undefined ? settings.startImmediately : payload.start !== false);
250
257
  const goal = requiredString(payload.goal, 'goal');
251
258
  const explicitTitle = payload.titleSource !== 'coordinator_pending' && typeof payload.title === 'string'
252
259
  ? payload.title.trim() : '';
@@ -289,6 +296,10 @@ export class WorkCenterService {
289
296
  ? normalizeSessionContextSnapshot(payload.sessionContext)
290
297
  : [],
291
298
  attachments,
299
+ schedule: scheduledFor == null ? null : {
300
+ status: payload.scheduleEnabled === false ? 'paused' : 'scheduled',
301
+ scheduledFor,
302
+ },
292
303
  start: false,
293
304
  });
294
305
  let detail = this.#requiredItem(workItemId);
@@ -313,6 +324,13 @@ export class WorkCenterService {
313
324
  this.#emit({ type: 'work_item.updated', workItem: detail });
314
325
  return detail;
315
326
  }
327
+ case 'update_schedule': {
328
+ const id = requiredString(payload.id, 'id');
329
+ const detail = this.store.updateWorkItemSchedule(id, payload.schedule || payload);
330
+ if (!detail) throw new Error(`WorkItem not found: ${id}`);
331
+ this.#emit({ type: 'work_item.schedule_updated', workItem: detail });
332
+ return detail;
333
+ }
316
334
  case 'start': {
317
335
  const detail = this.controller.start(requiredString(payload.id, 'id'));
318
336
  if (detail.coordinationMode === DYNAMIC_COORDINATION_MODE) {
@@ -715,7 +733,16 @@ export class WorkCenterService {
715
733
  }
716
734
  }
717
735
 
736
+ #scanSchedules() {
737
+ const now = this.now();
738
+ for (const id of this.store.listDueScheduledWorkItemIds(now)) {
739
+ const detail = this.controller.startScheduled(id, now);
740
+ if (detail) this.#emit({ type: 'work_item.schedule_triggered', workItem: detail });
741
+ }
742
+ }
743
+
718
744
  #scanRecoveries() {
745
+ this.#scanSchedules();
719
746
  this.#scanCoordinatorProviderRecoveries();
720
747
  this.#scanDynamicCoordinatorWakes();
721
748
  this.#scanFailureRecoveries();
@@ -112,6 +112,11 @@ function mapWorkItem(row) {
112
112
  coordinationMode: row.coordination_mode || 'legacy',
113
113
  finalResult: parseJson(row.final_result, null),
114
114
  deliveryTarget: row.delivery_target || null,
115
+ schedule: row.schedule_status ? {
116
+ status: row.schedule_status,
117
+ scheduledFor: Number(row.scheduled_for) || null,
118
+ triggeredAt: Number(row.schedule_triggered_at) || null,
119
+ } : null,
115
120
  title: row.title,
116
121
  titleSource: row.title_source || 'explicit',
117
122
  requirement: row.requirement ?? row.goal,
@@ -858,6 +863,9 @@ export class WorkItemStore {
858
863
  coordination_mode TEXT NOT NULL DEFAULT 'legacy',
859
864
  final_result TEXT,
860
865
  delivery_target TEXT,
866
+ schedule_status TEXT,
867
+ scheduled_for INTEGER,
868
+ schedule_triggered_at INTEGER,
861
869
  title TEXT NOT NULL,
862
870
  title_source TEXT NOT NULL DEFAULT 'explicit',
863
871
  requirement TEXT,
@@ -1022,6 +1030,16 @@ export class WorkItemStore {
1022
1030
 
1023
1031
  // The feature shipped first as an unmerged PR, but keep the store tolerant
1024
1032
  // of databases created by review builds.
1033
+ if (!hasColumn(this.db, 'work_items', 'schedule_status')) {
1034
+ this.db.exec('ALTER TABLE work_items ADD COLUMN schedule_status TEXT');
1035
+ }
1036
+ if (!hasColumn(this.db, 'work_items', 'scheduled_for')) {
1037
+ this.db.exec('ALTER TABLE work_items ADD COLUMN scheduled_for INTEGER');
1038
+ }
1039
+ if (!hasColumn(this.db, 'work_items', 'schedule_triggered_at')) {
1040
+ this.db.exec('ALTER TABLE work_items ADD COLUMN schedule_triggered_at INTEGER');
1041
+ }
1042
+
1025
1043
  if (!hasColumn(this.db, 'work_items', 'workspace_key')) {
1026
1044
  withTransaction(this.db, () => {
1027
1045
  this.db.exec("ALTER TABLE work_items ADD COLUMN workspace_key TEXT NOT NULL DEFAULT ''");
@@ -2407,14 +2425,16 @@ export class WorkItemStore {
2407
2425
  const workspaceKey = canonicalWorkspaceKey(input.workDir);
2408
2426
  this.db.prepare(`INSERT INTO work_items
2409
2427
  (id, revision, execution_schema_version, ledger_revision, coordination_mode, final_result, delivery_target,
2410
- title, title_source, requirement, goal, acceptance_criteria, workflow_template, workflow_snapshot, status,
2428
+ schedule_status, scheduled_for, schedule_triggered_at, title, title_source, requirement, goal, acceptance_criteria, workflow_template, workflow_snapshot, status,
2411
2429
  current_action_id, current_run_id, work_dir, workspace_key, reuse_memory, origin, linked_session_ids,
2412
2430
  session_context, attachments, created_at, updated_at)
2413
- VALUES (?, 1, ?, 0, ?, NULL, ?, ?, ?, ?, ?, ?, ?, ?, ?, NULL, NULL, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(
2431
+ VALUES (?, 1, ?, 0, ?, NULL, ?, ?, ?, NULL, ?, ?, ?, ?, ?, ?, ?, ?, NULL, NULL, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(
2414
2432
  id,
2415
2433
  Number.isInteger(input.executionSchemaVersion) ? input.executionSchemaVersion : 2,
2416
2434
  input.coordinationMode || 'legacy',
2417
2435
  input.deliveryTarget || null,
2436
+ input.schedule?.status || null,
2437
+ input.schedule?.scheduledFor || null,
2418
2438
  input.title,
2419
2439
  input.titleSource === 'coordinator_pending' ? 'coordinator_pending' : 'explicit',
2420
2440
  input.goal,
@@ -3099,6 +3119,32 @@ export class WorkItemStore {
3099
3119
  }));
3100
3120
  }
3101
3121
 
3122
+ listDueScheduledWorkItemIds(now = this.now()) {
3123
+ return this.db.prepare(`SELECT id FROM work_items
3124
+ WHERE schedule_status = 'scheduled' AND scheduled_for <= ? AND status = 'draft'
3125
+ ORDER BY scheduled_for, id`).all(now).map(row => row.id);
3126
+ }
3127
+
3128
+ updateWorkItemSchedule(id, input = {}) {
3129
+ return withTransaction(this.db, () => {
3130
+ const row = this.db.prepare('SELECT * FROM work_items WHERE id = ?').get(id);
3131
+ if (!row) return null;
3132
+ if (row.status !== 'draft' || !['scheduled', 'paused'].includes(row.schedule_status)) {
3133
+ throw new Error('Only pending scheduled WorkItems can be changed');
3134
+ }
3135
+ const scheduledFor = input.scheduledFor == null ? Number(row.scheduled_for) : Number(input.scheduledFor);
3136
+ if (!Number.isSafeInteger(scheduledFor) || scheduledFor <= this.now()) {
3137
+ throw new Error('scheduledFor must be in the future');
3138
+ }
3139
+ const status = input.enabled === false ? 'paused' : 'scheduled';
3140
+ const now = this.now();
3141
+ this.db.prepare(`UPDATE work_items SET schedule_status = ?, scheduled_for = ?,
3142
+ revision = revision + 1, updated_at = ? WHERE id = ?`).run(status, scheduledFor, now, id);
3143
+ this.appendEvent(id, 'work_item.schedule_updated', { status, scheduledFor });
3144
+ return this.getWorkItemDetail(id);
3145
+ });
3146
+ }
3147
+
3102
3148
  listWorkItems(filters = {}) {
3103
3149
  const where = [];
3104
3150
  const values = [];
@@ -4492,10 +4538,14 @@ export class WorkItemStore {
4492
4538
  });
4493
4539
  }
4494
4540
 
4495
- startWorkItemAtomic(id, makeInitialAction) {
4541
+ startWorkItemAtomic(id, makeInitialAction, options = {}) {
4496
4542
  return withTransaction(this.db, () => {
4497
4543
  const workItem = this.getWorkItem(id);
4498
4544
  if (!workItem) return null;
4545
+ const scheduledAt = Number(options.scheduledAt);
4546
+ const scheduledStart = Number.isSafeInteger(scheduledAt);
4547
+ if (scheduledStart && (workItem.status !== 'draft' || workItem.schedule?.status !== 'scheduled'
4548
+ || workItem.schedule.scheduledFor > scheduledAt)) return null;
4499
4549
  if (['done', 'cancelled'].includes(workItem.status)) {
4500
4550
  throw new Error(`Cannot start WorkItem in ${workItem.status}`);
4501
4551
  }
@@ -4505,13 +4555,18 @@ export class WorkItemStore {
4505
4555
  throw new Error(`WorkItem in ${workItem.status} must be resumed with retry`);
4506
4556
  }
4507
4557
  const now = this.now();
4558
+ if (scheduledStart) {
4559
+ this.db.prepare(`UPDATE work_items SET schedule_status = 'triggered',
4560
+ schedule_triggered_at = ?, revision = revision + 1, updated_at = ? WHERE id = ?`)
4561
+ .run(scheduledAt, now, id);
4562
+ }
4508
4563
  if (isDynamicWorkItem(workItem)) {
4509
4564
  this.db.prepare(`UPDATE work_items SET status = 'running', current_action_id = NULL,
4510
4565
  current_run_id = NULL, updated_at = ? WHERE id = ?`).run(now, id);
4511
4566
  this.enqueueCoordinatorMailbox(id, 'work_item_started', {
4512
4567
  trigger: { workItemId: id },
4513
4568
  }, `dynamic:start:${id}:${workItem.revision}`);
4514
- this.appendEvent(id, 'work_item.started');
4569
+ this.appendEvent(id, 'work_item.started', scheduledStart ? { scheduled: true } : {});
4515
4570
  return this.getWorkItemDetail(id);
4516
4571
  }
4517
4572
  const action = this.#insertAction(id, {
@@ -4520,7 +4575,7 @@ export class WorkItemStore {
4520
4575
  }, this.#nextSequence(id), now);
4521
4576
  this.db.prepare(`UPDATE work_items SET status = 'ready', current_action_id = ?,
4522
4577
  current_run_id = NULL, updated_at = ? WHERE id = ?`).run(action.id, now, id);
4523
- this.appendEvent(id, 'work_item.started', {}, { actionId: action.id });
4578
+ this.appendEvent(id, 'work_item.started', scheduledStart ? { scheduled: true } : {}, { actionId: action.id });
4524
4579
  return this.getWorkItemDetail(id);
4525
4580
  });
4526
4581
  }