@yeaft/webchat-agent 0.1.674 → 0.1.675
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/connection/message-router.js +6 -1
- package/package.json +1 -1
- package/unify/web-bridge.js +167 -101
|
@@ -36,7 +36,7 @@ import { sendToServer, flushMessageBuffer } from './buffer.js';
|
|
|
36
36
|
import { handleRestartAgent, handleUpgradeAgent } from './upgrade.js';
|
|
37
37
|
import { loadMcpServers, updateMcpConfig } from '../mcp.js';
|
|
38
38
|
import { getLlmConfig, updateLlmConfig, getUnifySettings, updateUnifySettings, getSearchSettings, updateSearchSettings, fetchTavilyUsage } from '../unify/config-api.js';
|
|
39
|
-
import { handleUnifyGroupChat, handleUnifyModeSwitch, handleUnifyModelSwitch, resetUnifySession, handleUnifyLoadHistory, handleUnifyAbortThread, handleUnifyAbortAll, handleUnifyVpSubscribe, handleUnifyVpCreate, handleUnifyVpUpdate, handleUnifyVpDelete, handleUnifyVpRead, handleUnifyFeatureMessage, handleUnifyFetchSummaryHistory, handleUnifyFeatureCrud, handleUnifyListGroups, handleUnifyCreateGroup, handleUnifyRenameGroup, handleUnifyArchiveGroup, handleUnifyDeleteGroup, handleUnifyAddMember, handleUnifyRemoveMember, handleUnifySetDefaultVp, handleUnifyDreamTrigger } from '../unify/web-bridge.js';
|
|
39
|
+
import { handleUnifyGroupChat, handleUnifyModeSwitch, handleUnifyModelSwitch, resetUnifySession, handleUnifyLoadHistory, handleUnifyAbortThread, handleUnifyAbortAll, handleUnifyAbortTurn, handleUnifyVpSubscribe, handleUnifyVpCreate, handleUnifyVpUpdate, handleUnifyVpDelete, handleUnifyVpRead, handleUnifyFeatureMessage, handleUnifyFetchSummaryHistory, handleUnifyFeatureCrud, handleUnifyListGroups, handleUnifyCreateGroup, handleUnifyRenameGroup, handleUnifyArchiveGroup, handleUnifyDeleteGroup, handleUnifyAddMember, handleUnifyRemoveMember, handleUnifySetDefaultVp, handleUnifyDreamTrigger } from '../unify/web-bridge.js';
|
|
40
40
|
|
|
41
41
|
export async function handleMessage(msg) {
|
|
42
42
|
switch (msg.type) {
|
|
@@ -412,6 +412,11 @@ export async function handleMessage(msg) {
|
|
|
412
412
|
handleUnifyAbortAll();
|
|
413
413
|
break;
|
|
414
414
|
|
|
415
|
+
case 'unify_abort_turn':
|
|
416
|
+
// Per-VP stop: abort a single VP turn by turnId.
|
|
417
|
+
handleUnifyAbortTurn(msg);
|
|
418
|
+
break;
|
|
419
|
+
|
|
415
420
|
// task-334-ui-a: VP library subscribe — replies with one-shot
|
|
416
421
|
// vp_snapshot event. Live diff (vp_updated/vp_removed) deferred to 334h.
|
|
417
422
|
case 'unify_vp_subscribe':
|
package/package.json
CHANGED
package/unify/web-bridge.js
CHANGED
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
|
|
21
21
|
import { join } from 'node:path';
|
|
22
22
|
import { existsSync } from 'node:fs';
|
|
23
|
+
import { randomUUID } from 'node:crypto';
|
|
23
24
|
import { loadSession } from './session.js';
|
|
24
25
|
import { sendToServer } from '../connection/buffer.js';
|
|
25
26
|
import ctx from '../context.js';
|
|
@@ -54,6 +55,15 @@ let session = null;
|
|
|
54
55
|
*/
|
|
55
56
|
let currentAbortCtrl = null;
|
|
56
57
|
|
|
58
|
+
/**
|
|
59
|
+
* Per-VP-turn AbortControllers. Maps `turnId` → `AbortController`.
|
|
60
|
+
* Each VP-turn in a fan-out gets its own controller so it can be stopped
|
|
61
|
+
* independently (per-VP Stop button). `handleUnifyAbortTurn` looks up by
|
|
62
|
+
* turnId to abort a single VP. `handleUnifyAbortAll` iterates and aborts all.
|
|
63
|
+
* @type {Map<string, AbortController>}
|
|
64
|
+
*/
|
|
65
|
+
const turnAbortCtrls = new Map();
|
|
66
|
+
|
|
57
67
|
/** Query timeout in ms — abort if LLM doesn't respond within this window */
|
|
58
68
|
const QUERY_TIMEOUT_MS = 120_000;
|
|
59
69
|
|
|
@@ -108,25 +118,28 @@ function isPermissionErrorMsg(msg) {
|
|
|
108
118
|
|
|
109
119
|
/**
|
|
110
120
|
* Send a unify_output message carrying claude_output-format data.
|
|
111
|
-
*
|
|
112
|
-
* the
|
|
113
|
-
* the SEND-context group.
|
|
121
|
+
* Envelope fields: conversationId, groupId, vpId, turnId — the last two
|
|
122
|
+
* let the frontend route incremental deltas to the correct per-VP message block.
|
|
114
123
|
*/
|
|
115
|
-
function sendUnifyOutput(data, groupId) {
|
|
124
|
+
function sendUnifyOutput(data, { groupId, vpId, turnId } = {}) {
|
|
116
125
|
sendToServer({
|
|
117
126
|
type: 'unify_output',
|
|
118
127
|
conversationId: unifyConversationId,
|
|
119
128
|
...(groupId ? { groupId } : {}),
|
|
129
|
+
...(vpId ? { vpId } : {}),
|
|
130
|
+
...(turnId ? { turnId } : {}),
|
|
120
131
|
data,
|
|
121
132
|
});
|
|
122
133
|
}
|
|
123
134
|
|
|
124
135
|
/** Send a unify_output event (non-claude_output metadata). */
|
|
125
|
-
function sendUnifyEvent(event, groupId) {
|
|
136
|
+
function sendUnifyEvent(event, { groupId, vpId, turnId } = {}) {
|
|
126
137
|
sendToServer({
|
|
127
138
|
type: 'unify_output',
|
|
128
139
|
conversationId: unifyConversationId,
|
|
129
140
|
...(groupId ? { groupId } : {}),
|
|
141
|
+
...(vpId ? { vpId } : {}),
|
|
142
|
+
...(turnId ? { turnId } : {}),
|
|
130
143
|
event,
|
|
131
144
|
});
|
|
132
145
|
}
|
|
@@ -402,11 +415,11 @@ export function installUnifyRuntimeBridge(s) {
|
|
|
402
415
|
* H2.f.2: no longer stamps a threadId on outgoing claude_output frames.
|
|
403
416
|
*
|
|
404
417
|
* @param {object} event — engine event (text_delta / tool_call / …)
|
|
405
|
-
* @param {{assistantTextParts:string[], toolCallsAccum:Array, toolResultsAccum:Array, resetQueryTimer:Function, groupId?:string}} hctx
|
|
418
|
+
* @param {{assistantTextParts:string[], toolCallsAccum:Array, toolResultsAccum:Array, resetQueryTimer:Function, groupId?:string, vpId?:string, turnId?:string}} hctx
|
|
406
419
|
*/
|
|
407
420
|
function handleEngineEvent(event, hctx) {
|
|
408
421
|
hctx.resetQueryTimer();
|
|
409
|
-
const
|
|
422
|
+
const envelope = { groupId: hctx.groupId, vpId: hctx.vpId, turnId: hctx.turnId };
|
|
410
423
|
|
|
411
424
|
switch (event.type) {
|
|
412
425
|
case 'text_delta':
|
|
@@ -414,11 +427,11 @@ function handleEngineEvent(event, hctx) {
|
|
|
414
427
|
sendUnifyOutput({
|
|
415
428
|
type: 'assistant',
|
|
416
429
|
message: { content: [{ type: 'text', text: event.text }] },
|
|
417
|
-
},
|
|
430
|
+
}, envelope);
|
|
418
431
|
break;
|
|
419
432
|
|
|
420
433
|
case 'thinking_delta':
|
|
421
|
-
sendUnifyEvent({ type: 'thinking_delta', text: event.text },
|
|
434
|
+
sendUnifyEvent({ type: 'thinking_delta', text: event.text }, envelope);
|
|
422
435
|
break;
|
|
423
436
|
|
|
424
437
|
case 'tool_call':
|
|
@@ -436,7 +449,7 @@ function handleEngineEvent(event, hctx) {
|
|
|
436
449
|
sendUnifyOutput({
|
|
437
450
|
type: 'assistant',
|
|
438
451
|
message: { content: [] },
|
|
439
|
-
},
|
|
452
|
+
}, envelope);
|
|
440
453
|
sendUnifyOutput({
|
|
441
454
|
type: 'assistant',
|
|
442
455
|
message: {
|
|
@@ -447,7 +460,7 @@ function handleEngineEvent(event, hctx) {
|
|
|
447
460
|
input: event.input,
|
|
448
461
|
}],
|
|
449
462
|
},
|
|
450
|
-
},
|
|
463
|
+
}, envelope);
|
|
451
464
|
break;
|
|
452
465
|
|
|
453
466
|
case 'tool_start':
|
|
@@ -455,7 +468,7 @@ function handleEngineEvent(event, hctx) {
|
|
|
455
468
|
type: 'tool_start',
|
|
456
469
|
id: event.id,
|
|
457
470
|
name: event.name,
|
|
458
|
-
},
|
|
471
|
+
}, envelope);
|
|
459
472
|
break;
|
|
460
473
|
|
|
461
474
|
case 'tool_end':
|
|
@@ -475,7 +488,7 @@ function handleEngineEvent(event, hctx) {
|
|
|
475
488
|
content: event.output || '',
|
|
476
489
|
is_error: event.isError || false,
|
|
477
490
|
}],
|
|
478
|
-
},
|
|
491
|
+
}, envelope);
|
|
479
492
|
break;
|
|
480
493
|
|
|
481
494
|
case 'turn_start':
|
|
@@ -489,7 +502,7 @@ function handleEngineEvent(event, hctx) {
|
|
|
489
502
|
type: 'context_usage',
|
|
490
503
|
inputTokens: event.inputTokens,
|
|
491
504
|
outputTokens: event.outputTokens,
|
|
492
|
-
},
|
|
505
|
+
}, envelope);
|
|
493
506
|
break;
|
|
494
507
|
|
|
495
508
|
case 'recall':
|
|
@@ -497,7 +510,7 @@ function handleEngineEvent(event, hctx) {
|
|
|
497
510
|
type: 'recall',
|
|
498
511
|
entryCount: event.entryCount,
|
|
499
512
|
cached: event.cached,
|
|
500
|
-
},
|
|
513
|
+
}, envelope);
|
|
501
514
|
break;
|
|
502
515
|
|
|
503
516
|
case 'consolidate':
|
|
@@ -507,7 +520,7 @@ function handleEngineEvent(event, hctx) {
|
|
|
507
520
|
type: 'consolidate',
|
|
508
521
|
archivedCount: event.archivedCount,
|
|
509
522
|
extractedCount: event.extractedCount,
|
|
510
|
-
},
|
|
523
|
+
}, envelope);
|
|
511
524
|
break;
|
|
512
525
|
|
|
513
526
|
case 'fallback':
|
|
@@ -516,7 +529,7 @@ function handleEngineEvent(event, hctx) {
|
|
|
516
529
|
from: event.from,
|
|
517
530
|
to: event.to,
|
|
518
531
|
reason: event.reason,
|
|
519
|
-
},
|
|
532
|
+
}, envelope);
|
|
520
533
|
break;
|
|
521
534
|
|
|
522
535
|
case 'reflection':
|
|
@@ -529,7 +542,7 @@ function handleEngineEvent(event, hctx) {
|
|
|
529
542
|
content: event.content,
|
|
530
543
|
durationMs: event.durationMs,
|
|
531
544
|
error: event.error,
|
|
532
|
-
},
|
|
545
|
+
}, envelope);
|
|
533
546
|
break;
|
|
534
547
|
|
|
535
548
|
case 'debug_turn':
|
|
@@ -547,7 +560,7 @@ function handleEngineEvent(event, hctx) {
|
|
|
547
560
|
stopReason: event.stopReason,
|
|
548
561
|
rawRequest: event.rawRequest,
|
|
549
562
|
rawResponse: event.rawResponse,
|
|
550
|
-
},
|
|
563
|
+
}, envelope);
|
|
551
564
|
break;
|
|
552
565
|
|
|
553
566
|
case 'error': {
|
|
@@ -563,7 +576,7 @@ function handleEngineEvent(event, hctx) {
|
|
|
563
576
|
text: '⚠️ Cannot write to ~/.yeaft/ directory — some features (memory, history) are unavailable. Please check directory permissions: `chmod -R u+rw ~/.yeaft/`',
|
|
564
577
|
}],
|
|
565
578
|
},
|
|
566
|
-
},
|
|
579
|
+
}, envelope);
|
|
567
580
|
}
|
|
568
581
|
} else {
|
|
569
582
|
sendUnifyOutput({
|
|
@@ -571,7 +584,7 @@ function handleEngineEvent(event, hctx) {
|
|
|
571
584
|
message: {
|
|
572
585
|
content: [{ type: 'text', text: `⚠️ Error: ${errMsg}` }],
|
|
573
586
|
},
|
|
574
|
-
},
|
|
587
|
+
}, envelope);
|
|
575
588
|
}
|
|
576
589
|
break;
|
|
577
590
|
}
|
|
@@ -616,21 +629,23 @@ export async function handleUnifyGroupChat(msg) {
|
|
|
616
629
|
sendUnifyOutput({
|
|
617
630
|
type: 'assistant',
|
|
618
631
|
message: { content: [{ type: 'text', text: '⚠️ Unify session error: no yeaft directory configured.' }] },
|
|
619
|
-
}, groupId);
|
|
620
|
-
sendUnifyOutput({ type: 'result', result_text: '' }, groupId);
|
|
632
|
+
}, { groupId });
|
|
633
|
+
sendUnifyOutput({ type: 'result', result_text: '' }, { groupId });
|
|
621
634
|
return;
|
|
622
635
|
}
|
|
623
636
|
|
|
624
637
|
await ensureSessionLoaded();
|
|
625
638
|
|
|
626
|
-
// Cancel any prior in-flight dispatch BEFORE we fan out.
|
|
627
|
-
// one cancellation domain. Each per-VP runVpTurn shares this signal, so
|
|
628
|
-
// siblings within the same dispatch never abort each other (the bug
|
|
629
|
-
// before this fix: each runVpTurn replaced currentAbortCtrl, causing
|
|
630
|
-
// VP-B's start to silently kill VP-A's in-flight LLM call).
|
|
639
|
+
// Cancel any prior in-flight dispatch BEFORE we fan out.
|
|
631
640
|
if (currentAbortCtrl && !currentAbortCtrl.signal.aborted) {
|
|
632
641
|
try { currentAbortCtrl.abort(); } catch { /* best-effort */ }
|
|
633
642
|
}
|
|
643
|
+
// Also abort any lingering per-VP controllers from the prior dispatch.
|
|
644
|
+
for (const ctrl of turnAbortCtrls.values()) {
|
|
645
|
+
try { if (!ctrl.signal.aborted) ctrl.abort(); } catch { /* best-effort */ }
|
|
646
|
+
}
|
|
647
|
+
turnAbortCtrls.clear();
|
|
648
|
+
|
|
634
649
|
const dispatchAbortCtrl = new AbortController();
|
|
635
650
|
currentAbortCtrl = dispatchAbortCtrl;
|
|
636
651
|
|
|
@@ -666,8 +681,8 @@ export async function handleUnifyGroupChat(msg) {
|
|
|
666
681
|
sendUnifyOutput({
|
|
667
682
|
type: 'assistant',
|
|
668
683
|
message: { content: [{ type: 'text', text: errText }] },
|
|
669
|
-
}, groupId);
|
|
670
|
-
sendUnifyOutput({ type: 'result', result_text: '' }, groupId);
|
|
684
|
+
}, { groupId });
|
|
685
|
+
sendUnifyOutput({ type: 'result', result_text: '' }, { groupId });
|
|
671
686
|
return;
|
|
672
687
|
}
|
|
673
688
|
|
|
@@ -724,8 +739,8 @@ export async function handleUnifyGroupChat(msg) {
|
|
|
724
739
|
sendUnifyOutput({
|
|
725
740
|
type: 'assistant',
|
|
726
741
|
message: { content: [{ type: 'text', text: `⚠️ Group dispatch error: ${err?.message || err}` }] },
|
|
727
|
-
}, groupId);
|
|
728
|
-
sendUnifyOutput({ type: 'result', result_text: '' }, groupId);
|
|
742
|
+
}, { groupId });
|
|
743
|
+
sendUnifyOutput({ type: 'result', result_text: '' }, { groupId });
|
|
729
744
|
return;
|
|
730
745
|
}
|
|
731
746
|
|
|
@@ -738,12 +753,20 @@ export async function handleUnifyGroupChat(msg) {
|
|
|
738
753
|
sendUnifyOutput({
|
|
739
754
|
type: 'assistant',
|
|
740
755
|
message: { content: [{ type: 'text', text: '⚠️ No VP available to respond — check the group roster.' }] },
|
|
741
|
-
}, groupId);
|
|
742
|
-
sendUnifyOutput({ type: 'result', result_text: '' }, groupId);
|
|
756
|
+
}, { groupId });
|
|
757
|
+
sendUnifyOutput({ type: 'result', result_text: '' }, { groupId });
|
|
743
758
|
return;
|
|
744
759
|
}
|
|
745
760
|
|
|
761
|
+
// Mint a dispatch ID for this fan-out — each VP gets a unique turnId.
|
|
762
|
+
const dispatchId = randomUUID().slice(0, 8);
|
|
763
|
+
|
|
764
|
+
// Snapshot conversation history BEFORE fan-out starts. Each VP reads
|
|
765
|
+
// from this consistent point; no VP sees another VP's in-flight output.
|
|
766
|
+
const baseSnapshot = [...conversationMessages];
|
|
767
|
+
|
|
746
768
|
for (const { vpId, envelope } of captured) {
|
|
769
|
+
const turnId = `${dispatchId}:${vpId}`;
|
|
747
770
|
try {
|
|
748
771
|
sendUnifyEvent({
|
|
749
772
|
type: 'group_message',
|
|
@@ -754,7 +777,7 @@ export async function handleUnifyGroupChat(msg) {
|
|
|
754
777
|
mentions,
|
|
755
778
|
trigger: envelope?.trigger || 'fallback',
|
|
756
779
|
ts: Date.now(),
|
|
757
|
-
});
|
|
780
|
+
}, { groupId, vpId, turnId });
|
|
758
781
|
} catch { /* never crash WS pipeline */ }
|
|
759
782
|
|
|
760
783
|
try {
|
|
@@ -762,40 +785,43 @@ export async function handleUnifyGroupChat(msg) {
|
|
|
762
785
|
type: 'vp_typing_start',
|
|
763
786
|
groupId,
|
|
764
787
|
vpId,
|
|
788
|
+
turnId,
|
|
765
789
|
ts: Date.now(),
|
|
766
|
-
});
|
|
790
|
+
}, { groupId, vpId, turnId });
|
|
767
791
|
} catch { /* never crash WS pipeline */ }
|
|
768
792
|
}
|
|
769
793
|
|
|
770
|
-
//
|
|
771
|
-
//
|
|
772
|
-
//
|
|
773
|
-
//
|
|
774
|
-
// (or a timeout in any one VP) cancels the whole fan-out.
|
|
775
|
-
//
|
|
776
|
-
// Side-effect: VP-B's transcript no longer contains VP-A's reply
|
|
777
|
-
// (they're concurrent). Cross-VP visibility moves to the explicit
|
|
778
|
-
// route_forward tool. The group jsonl log remains the source of
|
|
779
|
-
// truth for full-fidelity replay.
|
|
794
|
+
// Per-VP parallel fan-out. Each VP gets its own AbortController
|
|
795
|
+
// (stoppable individually via per-VP Stop button) and reads from
|
|
796
|
+
// the shared baseSnapshot. On completion (or abort), results are
|
|
797
|
+
// atomically appended to conversationMessages.
|
|
780
798
|
await Promise.all(captured.map(async ({ vpId }) => {
|
|
799
|
+
const turnId = `${dispatchId}:${vpId}`;
|
|
800
|
+
const vpAbort = new AbortController();
|
|
801
|
+
turnAbortCtrls.set(turnId, vpAbort);
|
|
802
|
+
|
|
781
803
|
try {
|
|
782
804
|
await runVpTurn({
|
|
783
805
|
prompt: `@vp-${vpId} ${text}`,
|
|
784
806
|
groupId,
|
|
785
807
|
vpId,
|
|
808
|
+
turnId,
|
|
786
809
|
groupCoordinator: coord,
|
|
787
|
-
|
|
810
|
+
vpAbort,
|
|
811
|
+
baseSnapshot,
|
|
788
812
|
});
|
|
789
813
|
} catch (err) {
|
|
790
814
|
console.warn('[Unify] unify_group_chat: per-vp dispatch failed', vpId, err?.message || err);
|
|
791
815
|
} finally {
|
|
816
|
+
turnAbortCtrls.delete(turnId);
|
|
792
817
|
try {
|
|
793
818
|
sendUnifyEvent({
|
|
794
819
|
type: 'vp_typing_end',
|
|
795
820
|
groupId,
|
|
796
821
|
vpId,
|
|
822
|
+
turnId,
|
|
797
823
|
ts: Date.now(),
|
|
798
|
-
});
|
|
824
|
+
}, { groupId, vpId, turnId });
|
|
799
825
|
} catch { /* never crash WS pipeline */ }
|
|
800
826
|
}
|
|
801
827
|
}));
|
|
@@ -919,19 +945,19 @@ async function ensureSessionLoaded() {
|
|
|
919
945
|
* coordinator-bound router, stream events to the frontend, and append the
|
|
920
946
|
* result to the flat conversation history.
|
|
921
947
|
*
|
|
922
|
-
* Private — only `handleUnifyGroupChat` calls this.
|
|
923
|
-
*
|
|
924
|
-
*
|
|
925
|
-
*
|
|
926
|
-
*
|
|
927
|
-
* module-active one. This avoids a stale-timer-from-an-aborted-dispatch
|
|
928
|
-
* killing the next dispatch.
|
|
948
|
+
* Private — only `handleUnifyGroupChat` calls this. Each VP-turn gets its
|
|
949
|
+
* own AbortController (`vpAbort`) so it can be stopped individually. The
|
|
950
|
+
* shared `baseSnapshot` is the conversation history at fan-out start — no
|
|
951
|
+
* VP sees another VP's in-flight output. After the turn finishes (or is
|
|
952
|
+
* aborted), the VP's output is atomically appended to `conversationMessages`.
|
|
929
953
|
*
|
|
930
|
-
* @param {{ prompt: string, groupId: string, vpId: string
|
|
954
|
+
* @param {{ prompt: string, groupId: string, vpId: string, turnId: string, groupCoordinator: object, vpAbort: AbortController, baseSnapshot: Array }} args
|
|
931
955
|
*/
|
|
932
|
-
async function runVpTurn({ prompt, groupId, vpId, groupCoordinator,
|
|
956
|
+
async function runVpTurn({ prompt, groupId, vpId, turnId, groupCoordinator, vpAbort, baseSnapshot }) {
|
|
933
957
|
if (!prompt?.trim()) return;
|
|
934
958
|
|
|
959
|
+
const envelope = { groupId, vpId, turnId };
|
|
960
|
+
|
|
935
961
|
try {
|
|
936
962
|
if (session?.dreamScheduler) {
|
|
937
963
|
session.dreamScheduler.noteUserMessage();
|
|
@@ -941,25 +967,22 @@ async function runVpTurn({ prompt, groupId, vpId, groupCoordinator, abortCtrl })
|
|
|
941
967
|
const resetQueryTimer = () => {
|
|
942
968
|
if (queryTimer) clearTimeout(queryTimer);
|
|
943
969
|
queryTimer = setTimeout(() => {
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
// the new dispatch installed (that was the original race).
|
|
948
|
-
if (abortCtrl === currentAbortCtrl && !abortCtrl.signal.aborted) {
|
|
949
|
-
console.error(`[Unify] query timeout after ${QUERY_TIMEOUT_MS / 1000}s of silence — aborting`);
|
|
950
|
-
try { abortCtrl.abort(); } catch { /* best-effort */ }
|
|
970
|
+
if (!vpAbort.signal.aborted) {
|
|
971
|
+
console.error(`[Unify] query timeout after ${QUERY_TIMEOUT_MS / 1000}s of silence — aborting VP ${vpId}`);
|
|
972
|
+
try { vpAbort.abort(); } catch { /* best-effort */ }
|
|
951
973
|
}
|
|
952
974
|
}, QUERY_TIMEOUT_MS);
|
|
953
975
|
};
|
|
954
976
|
resetQueryTimer();
|
|
955
977
|
|
|
978
|
+
// Emit turn_start so frontend can create the message block.
|
|
979
|
+
sendUnifyEvent({ type: 'vp_turn_start', vpId, turnId, groupId }, envelope);
|
|
980
|
+
|
|
956
981
|
try {
|
|
957
982
|
const assistantTextParts = [];
|
|
958
983
|
const toolCallsAccum = [];
|
|
959
984
|
const toolResultsAccum = [];
|
|
960
985
|
|
|
961
|
-
// H2.f.5: dispatcher + InputQueue retired. Call engine.query() directly,
|
|
962
|
-
// passing the flat conversation history as `messages` for context continuity.
|
|
963
986
|
const queryOpts = buildVpQueryOpts({ vpId, groupCoordinator, groupId });
|
|
964
987
|
const handlerCtx = {
|
|
965
988
|
assistantTextParts,
|
|
@@ -967,50 +990,30 @@ async function runVpTurn({ prompt, groupId, vpId, groupCoordinator, abortCtrl })
|
|
|
967
990
|
toolResultsAccum,
|
|
968
991
|
resetQueryTimer,
|
|
969
992
|
groupId,
|
|
993
|
+
vpId,
|
|
994
|
+
turnId,
|
|
970
995
|
};
|
|
971
996
|
for await (const event of session.engine.query({
|
|
972
997
|
prompt,
|
|
973
|
-
messages:
|
|
974
|
-
signal:
|
|
998
|
+
messages: baseSnapshot,
|
|
999
|
+
signal: vpAbort.signal,
|
|
975
1000
|
...queryOpts,
|
|
976
1001
|
})) {
|
|
977
1002
|
resetQueryTimer();
|
|
978
1003
|
handleEngineEvent(event, handlerCtx);
|
|
979
1004
|
}
|
|
980
1005
|
|
|
981
|
-
//
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
const fullText = assistantTextParts.join('');
|
|
985
|
-
if (fullText || toolCallsAccum.length > 0) {
|
|
986
|
-
const assistantMsg = { role: 'assistant', content: fullText };
|
|
987
|
-
if (toolCallsAccum.length > 0) {
|
|
988
|
-
assistantMsg.toolCalls = toolCallsAccum.map(tc => ({
|
|
989
|
-
id: tc.id,
|
|
990
|
-
name: tc.name,
|
|
991
|
-
input: tc.input,
|
|
992
|
-
}));
|
|
993
|
-
}
|
|
994
|
-
conversationMessages.push(assistantMsg);
|
|
995
|
-
|
|
996
|
-
for (const tr of toolResultsAccum) {
|
|
997
|
-
conversationMessages.push({
|
|
998
|
-
role: 'tool',
|
|
999
|
-
toolCallId: tr.toolCallId,
|
|
1000
|
-
content: tr.content,
|
|
1001
|
-
isError: tr.isError,
|
|
1002
|
-
});
|
|
1003
|
-
}
|
|
1004
|
-
}
|
|
1006
|
+
// Turn completed — atomically append this VP's output to shared history.
|
|
1007
|
+
appendTurnToHistory(prompt, assistantTextParts, toolCallsAccum, toolResultsAccum);
|
|
1005
1008
|
|
|
1006
1009
|
sendUnifyOutput({
|
|
1007
1010
|
type: 'assistant',
|
|
1008
1011
|
message: { content: [] },
|
|
1009
|
-
},
|
|
1012
|
+
}, envelope);
|
|
1010
1013
|
sendUnifyOutput({
|
|
1011
1014
|
type: 'result',
|
|
1012
1015
|
result_text: '',
|
|
1013
|
-
},
|
|
1016
|
+
}, envelope);
|
|
1014
1017
|
} finally {
|
|
1015
1018
|
if (queryTimer) clearTimeout(queryTimer);
|
|
1016
1019
|
}
|
|
@@ -1020,7 +1023,8 @@ async function runVpTurn({ prompt, groupId, vpId, groupCoordinator, abortCtrl })
|
|
|
1020
1023
|
sendUnifyOutput({
|
|
1021
1024
|
type: 'result',
|
|
1022
1025
|
result_text: '',
|
|
1023
|
-
|
|
1026
|
+
stopped: true,
|
|
1027
|
+
}, envelope);
|
|
1024
1028
|
return;
|
|
1025
1029
|
}
|
|
1026
1030
|
|
|
@@ -1037,7 +1041,7 @@ async function runVpTurn({ prompt, groupId, vpId, groupCoordinator, abortCtrl })
|
|
|
1037
1041
|
text: '⚠️ Cannot write to ~/.yeaft/ directory — some features (memory, history) are unavailable. Please check directory permissions: `chmod -R u+rw ~/.yeaft/`',
|
|
1038
1042
|
}],
|
|
1039
1043
|
},
|
|
1040
|
-
},
|
|
1044
|
+
}, envelope);
|
|
1041
1045
|
}
|
|
1042
1046
|
} else {
|
|
1043
1047
|
sendUnifyOutput({
|
|
@@ -1048,12 +1052,42 @@ async function runVpTurn({ prompt, groupId, vpId, groupCoordinator, abortCtrl })
|
|
|
1048
1052
|
text: `⚠️ Session error: ${err.message}`,
|
|
1049
1053
|
}],
|
|
1050
1054
|
},
|
|
1051
|
-
},
|
|
1055
|
+
}, envelope);
|
|
1052
1056
|
}
|
|
1053
1057
|
sendUnifyOutput({
|
|
1054
1058
|
type: 'result',
|
|
1055
1059
|
result_text: '',
|
|
1056
|
-
},
|
|
1060
|
+
}, envelope);
|
|
1061
|
+
}
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
/**
|
|
1065
|
+
* Atomically append a completed VP-turn's messages to the shared
|
|
1066
|
+
* conversation history. Called once at turn end (not during streaming).
|
|
1067
|
+
*/
|
|
1068
|
+
function appendTurnToHistory(prompt, assistantTextParts, toolCallsAccum, toolResultsAccum) {
|
|
1069
|
+
conversationMessages.push({ role: 'user', content: prompt });
|
|
1070
|
+
|
|
1071
|
+
const fullText = assistantTextParts.join('');
|
|
1072
|
+
if (fullText || toolCallsAccum.length > 0) {
|
|
1073
|
+
const assistantMsg = { role: 'assistant', content: fullText };
|
|
1074
|
+
if (toolCallsAccum.length > 0) {
|
|
1075
|
+
assistantMsg.toolCalls = toolCallsAccum.map(tc => ({
|
|
1076
|
+
id: tc.id,
|
|
1077
|
+
name: tc.name,
|
|
1078
|
+
input: tc.input,
|
|
1079
|
+
}));
|
|
1080
|
+
}
|
|
1081
|
+
conversationMessages.push(assistantMsg);
|
|
1082
|
+
|
|
1083
|
+
for (const tr of toolResultsAccum) {
|
|
1084
|
+
conversationMessages.push({
|
|
1085
|
+
role: 'tool',
|
|
1086
|
+
toolCallId: tr.toolCallId,
|
|
1087
|
+
content: tr.content,
|
|
1088
|
+
isError: tr.isError,
|
|
1089
|
+
});
|
|
1090
|
+
}
|
|
1057
1091
|
}
|
|
1058
1092
|
}
|
|
1059
1093
|
|
|
@@ -1072,6 +1106,11 @@ export function handleUnifyAbortThread(_msg = {}) {
|
|
|
1072
1106
|
try { currentAbortCtrl.abort(); aborted.push('main'); } catch { /* best-effort */ }
|
|
1073
1107
|
}
|
|
1074
1108
|
currentAbortCtrl = null;
|
|
1109
|
+
// Also abort all per-VP turn controllers.
|
|
1110
|
+
for (const [turnId, ctrl] of turnAbortCtrls) {
|
|
1111
|
+
try { if (!ctrl.signal.aborted) { ctrl.abort(); aborted.push(turnId); } } catch { /* best-effort */ }
|
|
1112
|
+
}
|
|
1113
|
+
turnAbortCtrls.clear();
|
|
1075
1114
|
sendUnifyEvent({ type: 'unify_aborted', aborted, all: false });
|
|
1076
1115
|
return { aborted, all: false };
|
|
1077
1116
|
}
|
|
@@ -1086,10 +1125,37 @@ export function handleUnifyAbortAll() {
|
|
|
1086
1125
|
try { currentAbortCtrl.abort(); aborted.push('main'); } catch { /* best-effort */ }
|
|
1087
1126
|
}
|
|
1088
1127
|
currentAbortCtrl = null;
|
|
1128
|
+
// Also abort all per-VP turn controllers.
|
|
1129
|
+
for (const [turnId, ctrl] of turnAbortCtrls) {
|
|
1130
|
+
try { if (!ctrl.signal.aborted) { ctrl.abort(); aborted.push(turnId); } } catch { /* best-effort */ }
|
|
1131
|
+
}
|
|
1132
|
+
turnAbortCtrls.clear();
|
|
1089
1133
|
sendUnifyEvent({ type: 'unify_aborted', aborted, all: true });
|
|
1090
1134
|
return { aborted, all: true };
|
|
1091
1135
|
}
|
|
1092
1136
|
|
|
1137
|
+
/**
|
|
1138
|
+
* Per-VP abort: stops a single VP turn by turnId without affecting siblings.
|
|
1139
|
+
* Frontend sends `{ type: 'unify_abort_turn', turnId }`.
|
|
1140
|
+
* @param {{ turnId?: string }} msg
|
|
1141
|
+
*/
|
|
1142
|
+
export function handleUnifyAbortTurn(msg = {}) {
|
|
1143
|
+
const { turnId } = msg;
|
|
1144
|
+
if (!turnId) {
|
|
1145
|
+
sendUnifyEvent({ type: 'unify_turn_aborted', turnId: null, success: false });
|
|
1146
|
+
return;
|
|
1147
|
+
}
|
|
1148
|
+
const ctrl = turnAbortCtrls.get(turnId);
|
|
1149
|
+
if (ctrl && !ctrl.signal.aborted) {
|
|
1150
|
+
try { ctrl.abort(); } catch { /* best-effort */ }
|
|
1151
|
+
turnAbortCtrls.delete(turnId);
|
|
1152
|
+
sendUnifyEvent({ type: 'unify_turn_aborted', turnId, success: true });
|
|
1153
|
+
} else {
|
|
1154
|
+
turnAbortCtrls.delete(turnId);
|
|
1155
|
+
sendUnifyEvent({ type: 'unify_turn_aborted', turnId, success: false });
|
|
1156
|
+
}
|
|
1157
|
+
}
|
|
1158
|
+
|
|
1093
1159
|
/**
|
|
1094
1160
|
* Unified abort entry: routes by payload shape.
|
|
1095
1161
|
* @param {{ threadId?: string, all?: boolean }} [opts]
|
|
@@ -1352,13 +1418,13 @@ export async function handleUnifyLoadHistory(msg) {
|
|
|
1352
1418
|
|
|
1353
1419
|
for (const m of messages) {
|
|
1354
1420
|
if (m.role === 'user') {
|
|
1355
|
-
sendUnifyOutput({ type: 'user', message: { content: m.content } }, m.groupId || null);
|
|
1421
|
+
sendUnifyOutput({ type: 'user', message: { content: m.content } }, { groupId: m.groupId || null });
|
|
1356
1422
|
} else if (m.role === 'assistant') {
|
|
1357
1423
|
sendUnifyOutput({
|
|
1358
1424
|
type: 'assistant',
|
|
1359
1425
|
message: { content: [{ type: 'text', text: m.content }] },
|
|
1360
|
-
}, m.groupId || null);
|
|
1361
|
-
sendUnifyOutput({ type: 'result', result_text: '' }, m.groupId || null);
|
|
1426
|
+
}, { groupId: m.groupId || null });
|
|
1427
|
+
sendUnifyOutput({ type: 'result', result_text: '' }, { groupId: m.groupId || null });
|
|
1362
1428
|
}
|
|
1363
1429
|
}
|
|
1364
1430
|
|