@yeaft/webchat-agent 1.0.266 → 1.0.268

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=616a4bf9"></script>
19
+ <script type="module" src="app.bundle.js?v=80038488"></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.266",
3
+ "version": "1.0.268",
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",
package/yeaft/engine.js CHANGED
@@ -551,11 +551,12 @@ export class Engine {
551
551
  #externalUserWakePending = false;
552
552
 
553
553
  /**
554
- * Tasks (background bash, sub-agent spawns) that were launched DURING
555
- * the currently-running query() and have NOT terminated yet. The query
556
- * loop refuses to finalize end_turn while this set is non-empty instead
557
- * it parks on `#asyncTaskWaiters` until a terminal event or a new user
558
- * append wakes it up. Cleared at the top of each query() and again in
554
+ * Result-producing async tasks (currently sub-agent spawns) launched DURING
555
+ * the running query() and not yet terminated. Persistent shell tasks are
556
+ * deliberately status-only and never enter this set. The query loop refuses
557
+ * to finalize end_turn while this set is non-empty instead it parks on
558
+ * `#asyncTaskWaiters` until a terminal event or a new user append wakes it.
559
+ * Cleared at the top of each query() and again in
559
560
  * the finally block so a stale set never leaks across turns.
560
561
  * @type {Set<string>}
561
562
  */
@@ -1272,12 +1273,10 @@ export class Engine {
1272
1273
  // can mark "after this batch, end the turn — do NOT call adapter
1273
1274
  // again". Honored at the top of the tool-loop continuation.
1274
1275
  requestEndTurn: vpCtx?.requestEndTurn,
1275
- // Background-task ownership hook. Tools that produce a TaskManager
1276
- // task (bash background, agent spawn) call this with the new
1277
- // `task.id` so the engine keeps the current query parked at end_turn
1278
- // until the task terminates its result is then spliced into the
1279
- // next adapter loop in the SAME turn. Tools that don't produce
1280
- // async tasks ignore it.
1276
+ // Result-producing async-task ownership hook. Tools such as SpawnAgent
1277
+ // call this with the new `task.id` so the engine keeps the current query
1278
+ // parked at end_turn until the result arrives. Persistent background
1279
+ // shell tasks are status-only and intentionally do not call this hook.
1281
1280
  registerAsyncTask: (taskId, meta = {}) => {
1282
1281
  const current = typeof vpCtx?.currentToolCall === 'function' ? vpCtx.currentToolCall() : null;
1283
1282
  this.#registerAsyncTask(taskId, { ...(current || {}), ...(meta || {}) });
@@ -1305,11 +1304,9 @@ export class Engine {
1305
1304
  // to. Null when the parent has no stats wired (e.g. tests).
1306
1305
  toolStats: this.#toolStats || null,
1307
1306
  taskManager: this.#taskManager || null,
1308
- // Propagate the async-task coordinator so sub-agents launched
1309
- // from this engine register their background tasks against the
1310
- // SAME owner map the bridge uses. Without this, a sub-agent's
1311
- // background bash terminal event would not find its engine and
1312
- // would fall through to the legacy rescue path.
1307
+ // Propagate the async-task coordinator so sub-agents launched from
1308
+ // this engine register result-producing child tasks against the same
1309
+ // owner map the bridge uses.
1313
1310
  asyncTaskCoordinator: this.#asyncTaskCoordinator || null,
1314
1311
  },
1315
1312
  };
@@ -3199,9 +3196,10 @@ export class Engine {
3199
3196
  }
3200
3197
 
3201
3198
  // If no tool calls, we're done — UNLESS we still own a pending
3202
- // async task. The user-facing semantic: a turn that launched a
3203
- // background bash / sub-agent stays "live" until those tasks
3204
- // terminate (or the user appends, or abort). The model already
3199
+ // result-producing async task. Persistent shell tasks never register
3200
+ // here; they remain visible in TaskManager without holding this turn.
3201
+ // Registered tasks stay live until they terminate (or the user appends,
3202
+ // or abort). The model already
3205
3203
  // said end_turn; we just defer finalization by parking on the
3206
3204
  // wait queue, then splice the synthetic task-result message in
3207
3205
  // and run one more adapter loop. This matches the contract the
@@ -4166,8 +4164,8 @@ export class Engine {
4166
4164
  }
4167
4165
 
4168
4166
  /**
4169
- * Register a background task as belonging to the current query. Called
4170
- * from tools (bash background, agent spawn) via `toolCtx.registerAsyncTask`.
4167
+ * Register a result-producing async task as belonging to the current query.
4168
+ * Called by tools such as SpawnAgent via `toolCtx.registerAsyncTask`.
4171
4169
  * @param {string} taskId
4172
4170
  * @param {{ id?: string, name?: string, threadId?: string, toolCallId?: string, toolName?: string }} [meta]
4173
4171
  * @returns {void}
@@ -157,9 +157,9 @@ export function startSubAgent(agent, deps = {}) {
157
157
  toolStats: deps.toolStats || null,
158
158
  taskManager: deps.taskManager || null,
159
159
  });
160
- // Same-turn async-task plumbing: inherit the parent's coordinator so a
161
- // background bash launched FROM this sub-agent registers itself against
162
- // the shared owner map and its terminal event reaches the sub-engine.
160
+ // Same-turn result plumbing: inherit the parent's coordinator so any
161
+ // result-producing child task launched from this sub-agent uses the shared
162
+ // owner map. Persistent shell tasks remain status-only and do not register.
163
163
  if (deps.asyncTaskCoordinator && typeof subEngine.setAsyncTaskCoordinator === 'function') {
164
164
  subEngine.setAsyncTaskCoordinator(deps.asyncTaskCoordinator);
165
165
  }
@@ -6,7 +6,14 @@
6
6
  */
7
7
 
8
8
  import { randomUUID } from 'crypto';
9
- import { TaskStore, TASK_STATUS, isTerminalTaskStatus } from './store.js';
9
+ import {
10
+ TaskStore,
11
+ TASK_RESULT_DELIVERY,
12
+ TASK_STATUS,
13
+ isTerminalTaskStatus,
14
+ normalizeTaskResultDelivery,
15
+ taskResultDeliveryFor,
16
+ } from './store.js';
10
17
  import { startShellProcess } from './shell-runner.js';
11
18
  import { getRuntimePlatformInfo } from '../runtime-platform.js';
12
19
 
@@ -35,6 +42,7 @@ function publicSnapshot(task) {
35
42
  kind: task.kind,
36
43
  title: task.title,
37
44
  status: task.status,
45
+ resultDelivery: taskResultDeliveryFor(task),
38
46
  createdAt: task.createdAt,
39
47
  startedAt: task.startedAt,
40
48
  updatedAt: task.updatedAt,
@@ -63,20 +71,35 @@ export class TaskManager {
63
71
  this.active = new Map();
64
72
  this.processes = new Map();
65
73
  this.cancelEscalationTimers = new Map();
74
+ this.pendingStartupEvents = [];
66
75
  this.#loadPersistedRunningTasks();
67
76
  }
68
77
 
69
78
  setEventSink(onEvent) {
70
- this.onEvent = typeof onEvent === 'function' ? onEvent : null;
79
+ const sink = typeof onEvent === 'function' ? onEvent : null;
80
+ this.onEvent = sink;
81
+ if (!sink || this.pendingStartupEvents.length === 0) return;
82
+
83
+ const pending = this.pendingStartupEvents;
84
+ this.pendingStartupEvents = [];
85
+ for (const event of pending) this.#deliverEvent(event, sink);
71
86
  }
72
87
 
73
88
  #key(sessionId, taskId) {
74
89
  return `${sessionId || 'default'}::${taskId}`;
75
90
  }
76
91
 
77
- #emit(event, task, extra = {}) {
92
+ #deliverEvent(event, sink = this.onEvent) {
93
+ try { sink?.(event); } catch { /* event sinks must not break tasks */ }
94
+ }
95
+
96
+ #emit(event, task, extra = {}, { deferUntilSink = false } = {}) {
78
97
  const payload = { type: 'yeaft_task_event', event, task: publicSnapshot(task), ...extra };
79
- try { this.onEvent?.(payload); } catch { /* event sinks must not break tasks */ }
98
+ if (!this.onEvent && deferUntilSink) {
99
+ this.pendingStartupEvents.push(payload);
100
+ return;
101
+ }
102
+ this.#deliverEvent(payload);
80
103
  }
81
104
 
82
105
  #loadPersistedRunningTasks() {
@@ -93,13 +116,17 @@ export class TaskManager {
93
116
  };
94
117
  this.store.writeTask(orphaned);
95
118
  this.store.appendEvent(orphaned.sessionId, { event: 'orphaned', taskId: orphaned.id });
96
- this.#emit('completed', orphaned);
119
+ this.#emit('completed', orphaned, {}, { deferUntilSink: true });
97
120
  }
98
121
  }
99
122
 
100
- startTask({ sessionId, ownerVpId = null, kind = 'tool', title = '', runtime = {}, source = {}, logPath = null } = {}) {
123
+ startTask({ sessionId, ownerVpId = null, kind = 'tool', title = '', runtime = {}, source = {}, logPath = null, resultDelivery = TASK_RESULT_DELIVERY.STATUS_ONLY } = {}) {
101
124
  const taskId = makeTaskId();
102
125
  const resolvedSessionId = sessionId || 'default';
126
+ const normalizedResultDelivery = normalizeTaskResultDelivery(resultDelivery);
127
+ if (normalizedResultDelivery !== resultDelivery) {
128
+ console.warn(`[Yeaft] Invalid task resultDelivery ${String(resultDelivery)}; using ${TASK_RESULT_DELIVERY.STATUS_ONLY}.`);
129
+ }
103
130
  const task = {
104
131
  id: taskId,
105
132
  sessionId: resolvedSessionId,
@@ -107,6 +134,7 @@ export class TaskManager {
107
134
  kind,
108
135
  title: title || kind,
109
136
  status: TASK_STATUS.RUNNING,
137
+ resultDelivery: normalizedResultDelivery,
110
138
  createdAt: nowIso(),
111
139
  startedAt: nowIso(),
112
140
  updatedAt: nowIso(),
@@ -140,6 +168,7 @@ export class TaskManager {
140
168
  kind: 'shell',
141
169
  title: title || command.slice(0, 120),
142
170
  status: TASK_STATUS.RUNNING,
171
+ resultDelivery: TASK_RESULT_DELIVERY.STATUS_ONLY,
143
172
  createdAt: nowIso(),
144
173
  startedAt: nowIso(),
145
174
  updatedAt: nowIso(),
@@ -27,6 +27,35 @@ export const TASK_STATUS = Object.freeze({
27
27
  ORPHANED: 'orphaned',
28
28
  });
29
29
 
30
+ export const TASK_RESULT_DELIVERY = Object.freeze({
31
+ MODEL_REENTRY: 'model_reentry',
32
+ STATUS_ONLY: 'status_only',
33
+ });
34
+
35
+ export function normalizeTaskResultDelivery(value) {
36
+ if (value === TASK_RESULT_DELIVERY.MODEL_REENTRY || value === TASK_RESULT_DELIVERY.STATUS_ONLY) {
37
+ return value;
38
+ }
39
+ return TASK_RESULT_DELIVERY.STATUS_ONLY;
40
+ }
41
+
42
+ export function taskResultDeliveryFor(task) {
43
+ return normalizeTaskResultDelivery(task?.resultDelivery);
44
+ }
45
+
46
+ function normalizePersistedTask(task) {
47
+ if (!task || typeof task !== 'object') return task;
48
+ if (Object.prototype.hasOwnProperty.call(task, 'resultDelivery')) {
49
+ return { ...task, resultDelivery: normalizeTaskResultDelivery(task.resultDelivery) };
50
+ }
51
+ return {
52
+ ...task,
53
+ resultDelivery: task.kind === 'sub_agent'
54
+ ? TASK_RESULT_DELIVERY.MODEL_REENTRY
55
+ : TASK_RESULT_DELIVERY.STATUS_ONLY,
56
+ };
57
+ }
58
+
30
59
  const TERMINAL = new Set([
31
60
  TASK_STATUS.SUCCEEDED,
32
61
  TASK_STATUS.FAILED,
@@ -133,7 +162,7 @@ export class TaskStore {
133
162
  readTask(sessionId, taskId) {
134
163
  const path = this.taskPath(sessionId, taskId);
135
164
  if (!existsSync(path)) return null;
136
- return JSON.parse(readFileSync(path, 'utf8'));
165
+ return normalizePersistedTask(JSON.parse(readFileSync(path, 'utf8')));
137
166
  }
138
167
 
139
168
  loadActiveTasks() {
@@ -145,7 +174,7 @@ export class TaskStore {
145
174
  for (const entry of readdirSync(sessionDir, { withFileTypes: true })) {
146
175
  if (!entry.isFile() || !entry.name.endsWith('.json')) continue;
147
176
  try {
148
- const task = JSON.parse(readFileSync(join(sessionDir, entry.name), 'utf8'));
177
+ const task = normalizePersistedTask(JSON.parse(readFileSync(join(sessionDir, entry.name), 'utf8')));
149
178
  if (task && task.status === TASK_STATUS.RUNNING) out.push(task);
150
179
  } catch {
151
180
  // Ignore corrupt task metadata; one bad task must not block boot.
@@ -26,6 +26,7 @@ import { getPersona, listPersonaIds } from '../personas.js';
26
26
  import { startSubAgent } from '../sub-agent/runner.js';
27
27
  import { STATUS, isTerminalAgentStatus } from '../sub-agent/status.js';
28
28
  import { diagnoseAgentLiveness, makeLiveness } from '../sub-agent/liveness.js';
29
+ import { TASK_RESULT_DELIVERY } from '../tasks/store.js';
29
30
 
30
31
  /** In-memory sub-agent registry. */
31
32
  const agents = new Map();
@@ -424,6 +425,7 @@ use it as the default workflow or call it repeatedly in a loop.`,
424
425
  ownerVpId: callerScope.parentVpId || ctx?.currentVpId || null,
425
426
  kind: 'sub_agent',
426
427
  title: spec.mission || spec.task || name,
428
+ resultDelivery: TASK_RESULT_DELIVERY.MODEL_REENTRY,
427
429
  runtime: { subAgentId: agentId, name, cwd: agent.cwd },
428
430
  source: { threadId: callerScope.parentThreadId || ctx?.threadId || 'main' },
429
431
  });
@@ -255,14 +255,7 @@ Guidelines:
255
255
  threadId: ctx.threadId || 'main',
256
256
  },
257
257
  });
258
- // Same-turn parking: tell the engine "this turn has an async
259
- // task in flight". The engine refuses to finalize end_turn
260
- // while the set is non-empty and will splice the task result
261
- // into the next adapter loop when it terminates. No-op when
262
- // the engine didn't wire the hook (legacy callers / tests).
263
- const currentToolCall = typeof ctx.currentToolCall === 'function' ? ctx.currentToolCall() : null;
264
- try { ctx.registerAsyncTask?.(task.id, currentToolCall || {}); } catch { /* never block tool return on coord errors */ }
265
- return `Started background task ${task.id}.\nStatus: ${task.status}\nLog: ${task.log?.path || ''}\nUse ListTasks, ReadTaskLog, or CancelTask to inspect or control it.`;
258
+ return `Started background task ${task.id}.\nStatus: ${task.status}\nLog: ${task.log?.path || ''}\nThe task is detached from this turn. Use ListTasks, ReadTaskLog, or CancelTask to inspect or control it.`;
266
259
  } catch (err) {
267
260
  throw new Error(err?.message || String(err));
268
261
  }
@@ -76,6 +76,7 @@ import { getAgentRegistry, agentBelongsToScope } from './tools/agent.js';
76
76
  import { isPromptableAgentStatus } from './sub-agent/status.js';
77
77
  import { perfNowMs, recordAgentPerfTrace } from './perf-trace.js';
78
78
  import { recordAgentSessionCreated, recordAgentTurn } from '../metrics.js';
79
+ import { TASK_RESULT_DELIVERY, taskResultDeliveryFor } from './tasks/store.js';
79
80
 
80
81
  const LEGACY_SKILL_COMMAND_PREFIX = 'skill:';
81
82
  const YEAFT_SKILL_COMMAND_PREFIX = 'yeaft-skills:';
@@ -1706,10 +1707,9 @@ function getOrCreateVpEngine(sessionId, vpId, threadId = 'main') {
1706
1707
  sessionId,
1707
1708
  vpId,
1708
1709
  });
1709
- // Install the async-task coordinator so background tasks launched
1710
- // from this engine register against the shared owner map and
1711
- // `scheduleTaskResultReentry` can deliver terminal events back into
1712
- // the same query() instead of opening a fresh turn.
1710
+ // Install the async-task coordinator so result-producing child tasks
1711
+ // register against the shared owner map. Persistent shell tasks are
1712
+ // status-only and never enter this ownership path.
1713
1713
  try {
1714
1714
  if (typeof eng.setAsyncTaskCoordinator === 'function') {
1715
1715
  eng.setAsyncTaskCoordinator(buildAsyncTaskCoordinator());
@@ -1852,6 +1852,7 @@ function scheduleTaskResultRescue({ taskId, sessionId, vpId, threadId = 'main',
1852
1852
  function scheduleTaskResultReentry(event) {
1853
1853
  if (!event || event.event !== 'completed' || !event.task) return;
1854
1854
  const task = event.task;
1855
+ if (taskResultDeliveryFor(task) !== TASK_RESULT_DELIVERY.MODEL_REENTRY) return;
1855
1856
  const sessionId = task.sessionId || event.sessionId || null;
1856
1857
  const vpId = task.ownerVpId || null;
1857
1858
  if (!sessionId || !vpId) return;
@@ -3990,9 +3991,10 @@ function handleEngineEvent(event, hctx) {
3990
3991
  }, envelope);
3991
3992
  break;
3992
3993
 
3993
- // Same-turn async-task wait. Engine parks at end_turn while a
3994
- // background bash / sub-agent is still running and re-enters the
3995
- // same turn when the terminal event arrives (see engine.js
3994
+ // Same-turn result-producing task wait. Engine parks at end_turn while a
3995
+ // registered child task is still running and re-enters the same turn when
3996
+ // the terminal event arrives. Persistent shell tasks are status-only and
3997
+ // never emit this wait edge (see engine.js
3996
3998
  // `#runQuery` wait block). Bridge forwards both edges so the debug
3997
3999
  // panel (and any other in-process subscriber) can render the park
3998
4000
  // window with the live list of pending taskIds. Wire types stay