@yeaft/webchat-agent 0.1.503 → 0.1.504

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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/unify/web-bridge.js +133 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yeaft/webchat-agent",
3
- "version": "0.1.503",
3
+ "version": "0.1.504",
4
4
  "description": "Remote agent for Yeaft WebChat — connects worker machines to the central server",
5
5
  "main": "index.js",
6
6
  "type": "module",
@@ -243,6 +243,109 @@ const THREAD_MUTATING_TOOLS = new Set([
243
243
  'AttachThreadToTask',
244
244
  ]);
245
245
 
246
+ /**
247
+ * task-325b — Working Status event stream.
248
+ *
249
+ * Surfaces Engine lifecycle events (emitted by 325a) as a single
250
+ * `thread_status` event for the frontend Working Status panel, plus
251
+ * `thread_list_snapshot` for cold-start / reconnect.
252
+ *
253
+ * Contract (aligned with designer spec):
254
+ * thread_status → { type: 'thread_status', threadId, state,
255
+ * startedAt?, completedAt?, toolName?, reason? }
256
+ * state ∈ 'running' | 'idle' | 'aborted' | 'error'
257
+ * thread_list_snapshot → { type: 'thread_list_snapshot', threads[],
258
+ * currentThreadId, serverTime }
259
+ *
260
+ * Red lines (per PM): do NOT mutate engine state; this layer is a pure
261
+ * observer + translator. Event names match designer doc verbatim.
262
+ */
263
+
264
+ /** Map Engine event name → Working Status state string. */
265
+ function engineEventToState(engineEventType) {
266
+ switch (engineEventType) {
267
+ case 'thread_started': return 'running';
268
+ case 'thread_completed': return 'idle';
269
+ case 'thread_aborted': return 'aborted';
270
+ case 'thread_error': return 'error';
271
+ default: return null;
272
+ }
273
+ }
274
+
275
+ /**
276
+ * Build and broadcast a `thread_status` payload translated from a raw
277
+ * engine lifecycle event. The engine event shape (325a) is:
278
+ * { type, threadId, startedAt?, completedAt?, toolName?, reason? }
279
+ * Unknown fields pass through untouched so future engine additions
280
+ * (e.g. `attempt`) flow to the UI without another bridge change.
281
+ *
282
+ * @param {object} ev — engine event
283
+ * @returns {boolean} true if a thread_status was emitted
284
+ */
285
+ function emitThreadStatusFromEngineEvent(ev) {
286
+ if (!ev || typeof ev !== 'object') return false;
287
+ const state = engineEventToState(ev.type);
288
+ if (!state) return false;
289
+ const payload = { type: 'thread_status', threadId: ev.threadId, state };
290
+ if (ev.startedAt != null) payload.startedAt = ev.startedAt;
291
+ if (ev.completedAt != null) payload.completedAt = ev.completedAt;
292
+ if (ev.toolName) payload.toolName = ev.toolName;
293
+ if (ev.reason) payload.reason = ev.reason;
294
+ if (ev.error?.message) payload.error = ev.error.message;
295
+ sendUnifyEvent(payload);
296
+ return true;
297
+ }
298
+
299
+ /**
300
+ * task-325b: full-snapshot push distinct from `thread_list_updated`.
301
+ * Emits `thread_list_snapshot` — a complete state dump the client uses
302
+ * on page load / WebSocket reconnect to rebuild the Working Status panel
303
+ * without missing any in-flight thread.
304
+ *
305
+ * Snapshot includes per-thread `state` (idle / running / aborted) resolved
306
+ * from the engine registry's live inflight set. Threads the registry has
307
+ * no entry for default to 'idle'.
308
+ */
309
+ function sendThreadListSnapshot() {
310
+ try {
311
+ const store = getThreadStore();
312
+ const registry = session?.engineRegistry || null;
313
+ const inflight = new Set(
314
+ typeof registry?.inflightThreadIds === 'function'
315
+ ? registry.inflightThreadIds()
316
+ : [],
317
+ );
318
+ const threads = store.list().map(t => ({
319
+ id: t.id,
320
+ name: t.name,
321
+ goal: t.goal || '',
322
+ parentThreadId: t.parentThreadId || null,
323
+ status: t.status,
324
+ archived: !!t.archived,
325
+ messageCount: t.messageCount || 0,
326
+ lastMessageAt: t.lastMessageAt || null,
327
+ lastActivityAt: t.lastActivityAt || t.lastMessageAt || t.updatedAt || null,
328
+ unread: t.unread || 0,
329
+ preview: t.preview || '',
330
+ createdAt: t.createdAt,
331
+ updatedAt: t.updatedAt,
332
+ taskId: (typeof store.attachedTask === 'function')
333
+ ? (store.attachedTask(t.id) || null)
334
+ : null,
335
+ running: t.id === store.currentId,
336
+ state: inflight.has(t.id) ? 'running' : 'idle',
337
+ }));
338
+ sendUnifyEvent({
339
+ type: 'thread_list_snapshot',
340
+ threads,
341
+ currentThreadId: store.currentId,
342
+ serverTime: Date.now(),
343
+ });
344
+ } catch (err) {
345
+ console.warn('[Unify] sendThreadListSnapshot failed:', err?.message || err);
346
+ }
347
+ }
348
+
246
349
  /**
247
350
  * task-310: parse a leading `@thread-<id>` or `@thread-<name>` marker on
248
351
  * the user's input and return it as a dispatcher override. The marker
@@ -331,6 +434,23 @@ function forwardPipelineEvent(ev, ctx) {
331
434
  */
332
435
  function handleEngineEvent(event, threadId, hctx) {
333
436
  hctx.resetQueryTimer();
437
+
438
+ // task-325b: translate Engine lifecycle events into a single
439
+ // `thread_status` event for the frontend Working Status panel. These
440
+ // events are observer-only — they never mutate bridge state. The raw
441
+ // engine event is NOT forwarded further; the switch below handles
442
+ // anything the UI still needs.
443
+ if (event && (
444
+ event.type === 'thread_started' ||
445
+ event.type === 'thread_completed' ||
446
+ event.type === 'thread_aborted' ||
447
+ event.type === 'thread_error'
448
+ )) {
449
+ // Engine events carry their own threadId; fall back to envelope id.
450
+ emitThreadStatusFromEngineEvent({ ...event, threadId: event.threadId || threadId });
451
+ return;
452
+ }
453
+
334
454
  switch (event.type) {
335
455
  case 'text_delta':
336
456
  hctx.assistantTextParts.push(event.text);
@@ -562,6 +682,10 @@ export async function handleUnifyChat(msg) {
562
682
  // task-301 Part 2: initial thread snapshot so sidebar V2 renders
563
683
  // the real 'main' thread (and any restored threads) right away.
564
684
  sendThreadListUpdate();
685
+ // task-325b: full Working Status snapshot (superset with state +
686
+ // serverTime) so a freshly-connected client can restore inflight
687
+ // status without waiting for the next engine event.
688
+ sendThreadListSnapshot();
565
689
  }
566
690
 
567
691
  // ─── Per-call AbortController (task-320) ──
@@ -945,6 +1069,12 @@ export async function handleUnifyLoadHistory(msg) {
945
1069
  tools: session.status.tools,
946
1070
  });
947
1071
  sendThreadListUpdate();
1072
+ // task-325b: after a page refresh / reconnect the frontend needs the
1073
+ // full Working Status snapshot to rebuild the panel (which thread is
1074
+ // running, idle, aborted). `thread_list_updated` is intentionally a
1075
+ // mutation-delta stream; `thread_list_snapshot` is the single
1076
+ // authoritative "everything right now" payload.
1077
+ sendThreadListSnapshot();
948
1078
 
949
1079
  const limit = msg.limit || 50;
950
1080
  const messages = session.conversationStore.loadRecent(limit);
@@ -1030,6 +1160,9 @@ export async function resetUnifySession() {
1030
1160
  });
1031
1161
  // task-301 Part 2: re-push thread snapshot after session reset.
1032
1162
  sendThreadListUpdate();
1163
+ // task-325b: also push the full Working Status snapshot so the UI
1164
+ // doesn't retain stale "running" badges from the prior session.
1165
+ sendThreadListSnapshot();
1033
1166
  } catch (err) {
1034
1167
  console.error('[Unify] Failed to re-initialize session after reset:', err.message);
1035
1168
  }