@yeaft/webchat-agent 0.1.671 → 0.1.672
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 +2 -7
- package/package.json +1 -1
- package/unify/vp/registry.js +2 -1
- package/unify/web-bridge.js +165 -148
|
@@ -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 {
|
|
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';
|
|
40
40
|
|
|
41
41
|
export async function handleMessage(msg) {
|
|
42
42
|
switch (msg.type) {
|
|
@@ -378,12 +378,7 @@ export async function handleMessage(msg) {
|
|
|
378
378
|
break;
|
|
379
379
|
}
|
|
380
380
|
|
|
381
|
-
// Unify —
|
|
382
|
-
case 'unify_chat':
|
|
383
|
-
await handleUnifyChat(msg);
|
|
384
|
-
break;
|
|
385
|
-
|
|
386
|
-
// task-338-F4: Unify group-chat dispatch via GroupCoordinator.
|
|
381
|
+
// Unify — single conversation backed by the default group.
|
|
387
382
|
case 'unify_group_chat':
|
|
388
383
|
await handleUnifyGroupChat(msg);
|
|
389
384
|
break;
|
package/package.json
CHANGED
package/unify/vp/registry.js
CHANGED
|
@@ -7,7 +7,8 @@
|
|
|
7
7
|
*
|
|
8
8
|
* The previous RoleInstance map (per (vpId, groupId) pair, with LRU
|
|
9
9
|
* eviction) was removed in GC.2 — production fans out per-VP via
|
|
10
|
-
* `
|
|
10
|
+
* `handleUnifyGroupChat` -> `runVpTurn` directly and never instantiated
|
|
11
|
+
* RoleInstance.
|
|
11
12
|
*/
|
|
12
13
|
|
|
13
14
|
export class Registry {
|
package/unify/web-bridge.js
CHANGED
|
@@ -18,6 +18,8 @@
|
|
|
18
18
|
* ranges removed.
|
|
19
19
|
*/
|
|
20
20
|
|
|
21
|
+
import { join } from 'node:path';
|
|
22
|
+
import { existsSync } from 'node:fs';
|
|
21
23
|
import { loadSession } from './session.js';
|
|
22
24
|
import { sendToServer } from '../connection/buffer.js';
|
|
23
25
|
import ctx from '../context.js';
|
|
@@ -38,6 +40,9 @@ import {
|
|
|
38
40
|
setGroupDefaultVp,
|
|
39
41
|
snapshotGroups,
|
|
40
42
|
} from './groups/group-crud.js';
|
|
43
|
+
import { openGroup, loadGroupMeta } from './groups/group-store.js';
|
|
44
|
+
import { createCoordinator } from './groups/coordinator.js';
|
|
45
|
+
import { seedDefaultGroup } from './groups/seed-default.js';
|
|
41
46
|
|
|
42
47
|
/** @type {import('./session.js').Session | null} */
|
|
43
48
|
let session = null;
|
|
@@ -578,50 +583,101 @@ function handleEngineEvent(event, hctx) {
|
|
|
578
583
|
}
|
|
579
584
|
|
|
580
585
|
/**
|
|
581
|
-
* Handle a unify_group_chat message from the web UI
|
|
582
|
-
*
|
|
586
|
+
* Handle a unify_group_chat message from the web UI — the SOLE Unify
|
|
587
|
+
* conversation entry point.
|
|
588
|
+
*
|
|
589
|
+
* Contract (post-consolidation, was previously split between handleUnifyChat
|
|
590
|
+
* and handleUnifyGroupChat):
|
|
591
|
+
* - Frontend ALWAYS sends `unify_group_chat`. There is no `unify_chat`.
|
|
592
|
+
* - `groupId` defaults to `'grp_default'` if missing — Unify is a single
|
|
593
|
+
* conversation backed by the default group; the user is never "outside"
|
|
594
|
+
* a group.
|
|
595
|
+
* - If the group dir doesn't exist and the resolved id is `'grp_default'`,
|
|
596
|
+
* it is seeded on the fly. Any other unknown groupId surfaces an error.
|
|
597
|
+
* - Coordinator is MANDATORY (this is what guarantees ctx.router is wired
|
|
598
|
+
* so the `route_forward` tool can never trip `router_unavailable`).
|
|
599
|
+
* - No legacy "no-group" fallback paths — they were the source of the
|
|
600
|
+
* router_unavailable bug fixed in v0.1.671.
|
|
583
601
|
*/
|
|
584
602
|
export async function handleUnifyGroupChat(msg) {
|
|
585
603
|
if (!msg || typeof msg !== 'object') return;
|
|
586
|
-
const {
|
|
604
|
+
const { text } = msg;
|
|
587
605
|
if (!text?.trim()) return;
|
|
588
606
|
const mentions = Array.isArray(msg.mentions) ? msg.mentions : [];
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
607
|
+
const groupId = (typeof msg.groupId === 'string' && msg.groupId.trim())
|
|
608
|
+
? msg.groupId.trim()
|
|
609
|
+
: 'grp_default';
|
|
610
|
+
|
|
611
|
+
// yeaftDir is a hard prerequisite for both session boot and group seeding;
|
|
612
|
+
// validate BEFORE booting so a misconfigured agent doesn't leave a zombie
|
|
613
|
+
// session lying around.
|
|
614
|
+
const yeaftDir = ctx.CONFIG?.yeaftDir;
|
|
615
|
+
if (!yeaftDir) {
|
|
616
|
+
sendUnifyOutput({
|
|
617
|
+
type: 'assistant',
|
|
618
|
+
message: { content: [{ type: 'text', text: '⚠️ Unify session error: no yeaft directory configured.' }] },
|
|
619
|
+
}, groupId);
|
|
620
|
+
sendUnifyOutput({ type: 'result', result_text: '' }, groupId);
|
|
592
621
|
return;
|
|
593
622
|
}
|
|
594
623
|
|
|
624
|
+
await ensureSessionLoaded();
|
|
625
|
+
|
|
626
|
+
// Cancel any prior in-flight dispatch BEFORE we fan out. One dispatch =
|
|
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).
|
|
631
|
+
if (currentAbortCtrl && !currentAbortCtrl.signal.aborted) {
|
|
632
|
+
try { currentAbortCtrl.abort(); } catch { /* best-effort */ }
|
|
633
|
+
}
|
|
634
|
+
const dispatchAbortCtrl = new AbortController();
|
|
635
|
+
currentAbortCtrl = dispatchAbortCtrl;
|
|
636
|
+
|
|
637
|
+
// Open the group; seed grp_default on the fly if absent. Track
|
|
638
|
+
// seedFailed separately so a seed crash surfaces a different message
|
|
639
|
+
// than a genuinely-missing group.
|
|
595
640
|
let groupHandle = null;
|
|
641
|
+
let seedFailed = false;
|
|
596
642
|
try {
|
|
597
|
-
const
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
643
|
+
const root = join(yeaftDir, 'groups');
|
|
644
|
+
const dir = join(root, groupId);
|
|
645
|
+
if (existsSync(dir) && loadGroupMeta(dir)) {
|
|
646
|
+
groupHandle = openGroup(root, groupId);
|
|
647
|
+
} else if (groupId === 'grp_default') {
|
|
648
|
+
try {
|
|
649
|
+
const seeded = seedDefaultGroup(yeaftDir, {});
|
|
650
|
+
groupHandle = seeded.group;
|
|
651
|
+
} catch (seedErr) {
|
|
652
|
+
seedFailed = true;
|
|
653
|
+
console.warn('[Unify] unify_group_chat: seedDefaultGroup failed', seedErr?.message || seedErr);
|
|
606
654
|
}
|
|
655
|
+
} else {
|
|
656
|
+
console.warn('[Unify] unify_group_chat: groupId %s not found', groupId);
|
|
607
657
|
}
|
|
608
658
|
} catch (err) {
|
|
609
659
|
console.warn('[Unify] unify_group_chat: group open failed', err?.message || err);
|
|
610
660
|
}
|
|
611
661
|
|
|
612
662
|
if (!groupHandle) {
|
|
613
|
-
|
|
663
|
+
const errText = seedFailed
|
|
664
|
+
? `⚠️ Failed to seed default group ${groupId} — check ~/.yeaft/ permissions.`
|
|
665
|
+
: `⚠️ Group ${groupId} not found.`;
|
|
666
|
+
sendUnifyOutput({
|
|
667
|
+
type: 'assistant',
|
|
668
|
+
message: { content: [{ type: 'text', text: errText }] },
|
|
669
|
+
}, groupId);
|
|
670
|
+
sendUnifyOutput({ type: 'result', result_text: '' }, groupId);
|
|
614
671
|
return;
|
|
615
672
|
}
|
|
616
673
|
|
|
617
674
|
// Auto-add @-mentioned VPs from the library, heal missing defaultVpId.
|
|
618
675
|
try {
|
|
619
676
|
const meta = groupHandle.getMeta();
|
|
620
|
-
const yeaftDir = ctx.CONFIG?.yeaftDir;
|
|
621
677
|
const wantsAdd = mentions.filter(
|
|
622
678
|
(m) => m && m !== 'all' && !meta.roster.includes(m)
|
|
623
679
|
);
|
|
624
|
-
if (wantsAdd.length
|
|
680
|
+
if (wantsAdd.length) {
|
|
625
681
|
let mutated = false;
|
|
626
682
|
for (const vpId of wantsAdd) {
|
|
627
683
|
try {
|
|
@@ -633,19 +689,15 @@ export async function handleUnifyGroupChat(msg) {
|
|
|
633
689
|
}
|
|
634
690
|
if (mutated) {
|
|
635
691
|
try { groupHandle.close && groupHandle.close(); } catch { /* best-effort */ }
|
|
636
|
-
const { openGroup } = await import('./groups/group-store.js');
|
|
637
|
-
const { join } = await import('node:path');
|
|
638
692
|
groupHandle = openGroup(join(yeaftDir, 'groups'), groupId);
|
|
639
693
|
sendGroupRosterChanged(groupHandle.getMeta());
|
|
640
694
|
}
|
|
641
695
|
}
|
|
642
696
|
const meta2 = groupHandle.getMeta();
|
|
643
|
-
if (!meta2.defaultVpId && meta2.roster.length
|
|
697
|
+
if (!meta2.defaultVpId && meta2.roster.length) {
|
|
644
698
|
try {
|
|
645
699
|
setGroupDefaultVp(yeaftDir, groupId, meta2.roster[0]);
|
|
646
700
|
try { groupHandle.close && groupHandle.close(); } catch { /* best-effort */ }
|
|
647
|
-
const { openGroup } = await import('./groups/group-store.js');
|
|
648
|
-
const { join } = await import('node:path');
|
|
649
701
|
groupHandle = openGroup(join(yeaftDir, 'groups'), groupId);
|
|
650
702
|
sendGroupRosterChanged(groupHandle.getMeta());
|
|
651
703
|
} catch { /* best-effort */ }
|
|
@@ -654,7 +706,6 @@ export async function handleUnifyGroupChat(msg) {
|
|
|
654
706
|
console.warn('[Unify] unify_group_chat: auto-roster heal failed', err?.message || err);
|
|
655
707
|
}
|
|
656
708
|
|
|
657
|
-
const { createCoordinator } = await import('./groups/coordinator.js');
|
|
658
709
|
const captured = [];
|
|
659
710
|
const coord = createCoordinator(groupHandle, {
|
|
660
711
|
deliver: (vpId, envelope) => { captured.push({ vpId, envelope }); },
|
|
@@ -670,13 +721,25 @@ export async function handleUnifyGroupChat(msg) {
|
|
|
670
721
|
});
|
|
671
722
|
} catch (err) {
|
|
672
723
|
console.warn('[Unify] unify_group_chat: coord.ingest failed', err?.message || err);
|
|
673
|
-
|
|
724
|
+
sendUnifyOutput({
|
|
725
|
+
type: 'assistant',
|
|
726
|
+
message: { content: [{ type: 'text', text: `⚠️ Group dispatch error: ${err?.message || err}` }] },
|
|
727
|
+
}, groupId);
|
|
728
|
+
sendUnifyOutput({ type: 'result', result_text: '' }, groupId);
|
|
674
729
|
return;
|
|
675
730
|
}
|
|
676
731
|
|
|
677
732
|
const dispatchedIds = Array.isArray(report?.dispatched) ? report.dispatched : [];
|
|
678
733
|
if (dispatchedIds.length === 0 && !report?.fallback) {
|
|
679
|
-
|
|
734
|
+
// Coordinator chose nobody and provided no fallback — should not happen
|
|
735
|
+
// with a healthy roster. Surface the failure explicitly rather than
|
|
736
|
+
// silently retrying as a single-VP turn (the legacy fallback masked
|
|
737
|
+
// group-roster bugs).
|
|
738
|
+
sendUnifyOutput({
|
|
739
|
+
type: 'assistant',
|
|
740
|
+
message: { content: [{ type: 'text', text: '⚠️ No VP available to respond — check the group roster.' }] },
|
|
741
|
+
}, groupId);
|
|
742
|
+
sendUnifyOutput({ type: 'result', result_text: '' }, groupId);
|
|
680
743
|
return;
|
|
681
744
|
}
|
|
682
745
|
|
|
@@ -706,7 +769,9 @@ export async function handleUnifyGroupChat(msg) {
|
|
|
706
769
|
|
|
707
770
|
// GC.1 Commit C: VP-level parallelism. Each selected VP runs its
|
|
708
771
|
// turn concurrently via Promise.all. Intra-VP loops (LLM → tool →
|
|
709
|
-
// LLM) stay serial inside each
|
|
772
|
+
// LLM) stay serial inside each runVpTurn call. All siblings share
|
|
773
|
+
// the dispatch-level AbortSignal so a single user-initiated abort
|
|
774
|
+
// (or a timeout in any one VP) cancels the whole fan-out.
|
|
710
775
|
//
|
|
711
776
|
// Side-effect: VP-B's transcript no longer contains VP-A's reply
|
|
712
777
|
// (they're concurrent). Cross-VP visibility moves to the explicit
|
|
@@ -714,13 +779,12 @@ export async function handleUnifyGroupChat(msg) {
|
|
|
714
779
|
// truth for full-fidelity replay.
|
|
715
780
|
await Promise.all(captured.map(async ({ vpId }) => {
|
|
716
781
|
try {
|
|
717
|
-
await
|
|
718
|
-
...msg,
|
|
782
|
+
await runVpTurn({
|
|
719
783
|
prompt: `@vp-${vpId} ${text}`,
|
|
720
784
|
groupId,
|
|
721
785
|
vpId,
|
|
722
|
-
|
|
723
|
-
|
|
786
|
+
groupCoordinator: coord,
|
|
787
|
+
abortCtrl: dispatchAbortCtrl,
|
|
724
788
|
});
|
|
725
789
|
} catch (err) {
|
|
726
790
|
console.warn('[Unify] unify_group_chat: per-vp dispatch failed', vpId, err?.message || err);
|
|
@@ -794,94 +858,96 @@ export function buildVpQueryOpts({ vpId, groupCoordinator, groupId }) {
|
|
|
794
858
|
}
|
|
795
859
|
|
|
796
860
|
/**
|
|
797
|
-
*
|
|
798
|
-
*
|
|
799
|
-
*
|
|
800
|
-
* conversation, not per-thread). The history accumulator is a flat array.
|
|
801
|
-
*
|
|
802
|
-
* @param {{ prompt: string, mode?: string, userId?: string, username?: string }} msg
|
|
861
|
+
* Lazy session boot. Idempotent: subsequent calls are no-ops once `session`
|
|
862
|
+
* is set. Emits `session_ready` on first init so the frontend can finalize
|
|
863
|
+
* its handshake.
|
|
803
864
|
*/
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
const
|
|
808
|
-
|
|
809
|
-
|
|
865
|
+
async function ensureSessionLoaded() {
|
|
866
|
+
if (session) return;
|
|
867
|
+
|
|
868
|
+
const yeaftDir = ctx.CONFIG?.yeaftDir;
|
|
869
|
+
session = await loadSession({
|
|
870
|
+
...(yeaftDir && { dir: yeaftDir }),
|
|
871
|
+
skipMCP: false,
|
|
872
|
+
skipSkills: false,
|
|
873
|
+
});
|
|
810
874
|
|
|
811
|
-
|
|
812
|
-
console.warn('[Unify] unify_chat.mode is deprecated and ignored — Unify now runs in a single unified mode.');
|
|
813
|
-
}
|
|
875
|
+
installUnifyRuntimeBridge(session);
|
|
814
876
|
|
|
815
877
|
try {
|
|
816
|
-
if (
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
skipSkills: false,
|
|
878
|
+
if (session.engine && typeof session.engine.setSubAgentEventSink === 'function') {
|
|
879
|
+
session.engine.setSubAgentEventSink((agentId, evt) => {
|
|
880
|
+
try {
|
|
881
|
+
sendUnifyEvent({ type: 'sub_agent_event', agentId, payload: evt });
|
|
882
|
+
} catch { /* ignore */ }
|
|
822
883
|
});
|
|
884
|
+
}
|
|
885
|
+
} catch (err) {
|
|
886
|
+
console.warn('[Unify] setSubAgentEventSink wiring failed:', err?.message || err);
|
|
887
|
+
}
|
|
823
888
|
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
sendUnifyEvent({ type: 'sub_agent_event', agentId, payload: evt });
|
|
831
|
-
} catch { /* ignore */ }
|
|
832
|
-
});
|
|
833
|
-
}
|
|
834
|
-
} catch (err) {
|
|
835
|
-
console.warn('[Unify] setSubAgentEventSink wiring failed:', err?.message || err);
|
|
889
|
+
// Bug 8: clean up legacy `.archived-*` group dirs at boot.
|
|
890
|
+
try {
|
|
891
|
+
if (yeaftDir) {
|
|
892
|
+
const removed = purgeArchivedGroups(yeaftDir);
|
|
893
|
+
if (removed && removed.length > 0) {
|
|
894
|
+
console.log(`[Unify] purged ${removed.length} legacy .archived group dir(s)`);
|
|
836
895
|
}
|
|
896
|
+
}
|
|
897
|
+
} catch (err) {
|
|
898
|
+
console.warn('[Unify] purgeArchivedGroups failed:', err?.message || err);
|
|
899
|
+
}
|
|
837
900
|
|
|
838
|
-
|
|
839
|
-
try {
|
|
840
|
-
const yeaftDir = ctx.CONFIG?.yeaftDir;
|
|
841
|
-
if (yeaftDir) {
|
|
842
|
-
const removed = purgeArchivedGroups(yeaftDir);
|
|
843
|
-
if (removed && removed.length > 0) {
|
|
844
|
-
console.log(`[Unify] purged ${removed.length} legacy .archived group dir(s)`);
|
|
845
|
-
}
|
|
846
|
-
}
|
|
847
|
-
} catch (err) {
|
|
848
|
-
console.warn('[Unify] purgeArchivedGroups failed:', err?.message || err);
|
|
849
|
-
}
|
|
901
|
+
unifyConversationId = `unify-${Date.now()}`;
|
|
850
902
|
|
|
851
|
-
|
|
903
|
+
restoreHistoryFromRecent(session.conversationStore.loadRecent(50));
|
|
852
904
|
|
|
853
|
-
|
|
905
|
+
sendUnifyEvent({
|
|
906
|
+
type: 'session_ready',
|
|
907
|
+
conversationId: unifyConversationId,
|
|
908
|
+
model: session.config.model,
|
|
909
|
+
availableModels: session.config.availableModels || [],
|
|
910
|
+
skills: session.status.skills,
|
|
911
|
+
mcpServers: session.status.mcpServers,
|
|
912
|
+
tools: session.status.tools,
|
|
913
|
+
});
|
|
914
|
+
sendGroupSnapshotBroadcast();
|
|
915
|
+
}
|
|
854
916
|
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
917
|
+
/**
|
|
918
|
+
* Run a single VP's turn: call engine.query() with the supplied prompt and
|
|
919
|
+
* coordinator-bound router, stream events to the frontend, and append the
|
|
920
|
+
* result to the flat conversation history.
|
|
921
|
+
*
|
|
922
|
+
* Private — only `handleUnifyGroupChat` calls this. Coordinator and groupId
|
|
923
|
+
* are mandatory; callers MUST resolve a default group before invoking. The
|
|
924
|
+
* caller also owns the AbortController; we receive the whole controller
|
|
925
|
+
* (not just its signal) so the per-VP query-timeout can abort exactly the
|
|
926
|
+
* dispatch it belongs to — and only if that controller is still the
|
|
927
|
+
* module-active one. This avoids a stale-timer-from-an-aborted-dispatch
|
|
928
|
+
* killing the next dispatch.
|
|
929
|
+
*
|
|
930
|
+
* @param {{ prompt: string, groupId: string, vpId: string|null, groupCoordinator: object, abortCtrl: AbortController }} args
|
|
931
|
+
*/
|
|
932
|
+
async function runVpTurn({ prompt, groupId, vpId, groupCoordinator, abortCtrl }) {
|
|
933
|
+
if (!prompt?.trim()) return;
|
|
866
934
|
|
|
935
|
+
try {
|
|
867
936
|
if (session?.dreamScheduler) {
|
|
868
937
|
session.dreamScheduler.noteUserMessage();
|
|
869
938
|
}
|
|
870
939
|
|
|
871
|
-
// Cancel any prior in-flight round before starting this one.
|
|
872
|
-
if (currentAbortCtrl && !currentAbortCtrl.signal.aborted) {
|
|
873
|
-
try { currentAbortCtrl.abort(); } catch { /* best-effort */ }
|
|
874
|
-
}
|
|
875
|
-
const abortCtrl = new AbortController();
|
|
876
|
-
currentAbortCtrl = abortCtrl;
|
|
877
|
-
|
|
878
940
|
let queryTimer = null;
|
|
879
941
|
const resetQueryTimer = () => {
|
|
880
942
|
if (queryTimer) clearTimeout(queryTimer);
|
|
881
943
|
queryTimer = setTimeout(() => {
|
|
882
|
-
if
|
|
944
|
+
// Abort the captured dispatch controller — but only if it is
|
|
945
|
+
// still the module-active one. If a later dispatch already
|
|
946
|
+
// replaced it, our timer is stale and must NOT abort whatever
|
|
947
|
+
// the new dispatch installed (that was the original race).
|
|
948
|
+
if (abortCtrl === currentAbortCtrl && !abortCtrl.signal.aborted) {
|
|
883
949
|
console.error(`[Unify] query timeout after ${QUERY_TIMEOUT_MS / 1000}s of silence — aborting`);
|
|
884
|
-
abortCtrl.abort();
|
|
950
|
+
try { abortCtrl.abort(); } catch { /* best-effort */ }
|
|
885
951
|
}
|
|
886
952
|
}, QUERY_TIMEOUT_MS);
|
|
887
953
|
};
|
|
@@ -892,48 +958,6 @@ export async function handleUnifyChat(msg) {
|
|
|
892
958
|
const toolCallsAccum = [];
|
|
893
959
|
const toolResultsAccum = [];
|
|
894
960
|
|
|
895
|
-
// Backend safety net: if no coordinator was supplied (legacy
|
|
896
|
-
// `unify_chat` path, or test harness) but we have a yeaft dir, build
|
|
897
|
-
// an ephemeral coordinator over `groupId || grp_default`. This keeps
|
|
898
|
-
// ctx.router always wired so `route_forward` never bombs out with
|
|
899
|
-
// `router_unavailable`. The coordinator is local to this turn — no
|
|
900
|
-
// fan-out, no extra dispatch — it only exists so the per-VP Engine
|
|
901
|
-
// query can hand it to createRouter().
|
|
902
|
-
if (!groupCoordinator) {
|
|
903
|
-
try {
|
|
904
|
-
const yeaftDir = ctx.CONFIG?.yeaftDir;
|
|
905
|
-
if (yeaftDir) {
|
|
906
|
-
const resolvedGroupId = groupId || 'grp_default';
|
|
907
|
-
const { openGroup, loadGroupMeta } = await import('./groups/group-store.js');
|
|
908
|
-
const { join } = await import('node:path');
|
|
909
|
-
const { existsSync } = await import('node:fs');
|
|
910
|
-
const root = join(yeaftDir, 'groups');
|
|
911
|
-
const dir = join(root, resolvedGroupId);
|
|
912
|
-
let groupHandle = null;
|
|
913
|
-
if (existsSync(dir) && loadGroupMeta(dir)) {
|
|
914
|
-
groupHandle = openGroup(root, resolvedGroupId);
|
|
915
|
-
} else if (resolvedGroupId === 'grp_default') {
|
|
916
|
-
const { seedDefaultGroup } = await import('./groups/seed-default.js');
|
|
917
|
-
const seeded = seedDefaultGroup(yeaftDir, {});
|
|
918
|
-
groupHandle = seeded.group;
|
|
919
|
-
} else {
|
|
920
|
-
console.warn('[Unify] handleUnifyChat: groupId %s not found; no router will be wired for this turn', resolvedGroupId);
|
|
921
|
-
}
|
|
922
|
-
if (groupHandle) {
|
|
923
|
-
const { createCoordinator } = await import('./groups/coordinator.js');
|
|
924
|
-
groupCoordinator = createCoordinator(groupHandle, {
|
|
925
|
-
deliver: () => { /* no-op: 1:1 path, no fan-out */ },
|
|
926
|
-
});
|
|
927
|
-
if (!groupId) groupId = resolvedGroupId;
|
|
928
|
-
}
|
|
929
|
-
}
|
|
930
|
-
} catch (err) {
|
|
931
|
-
console.warn('[Unify] handleUnifyChat: ephemeral coordinator build failed', err?.message || err);
|
|
932
|
-
// Non-fatal — buildVpQueryOpts will return without router and
|
|
933
|
-
// route_forward will surface `router_unavailable` for that turn.
|
|
934
|
-
}
|
|
935
|
-
}
|
|
936
|
-
|
|
937
961
|
// H2.f.5: dispatcher + InputQueue retired. Call engine.query() directly,
|
|
938
962
|
// passing the flat conversation history as `messages` for context continuity.
|
|
939
963
|
const queryOpts = buildVpQueryOpts({ vpId, groupCoordinator, groupId });
|
|
@@ -1000,7 +1024,7 @@ export async function handleUnifyChat(msg) {
|
|
|
1000
1024
|
return;
|
|
1001
1025
|
}
|
|
1002
1026
|
|
|
1003
|
-
console.error('[Unify] query error:', err
|
|
1027
|
+
console.error('[Unify] query error:', err);
|
|
1004
1028
|
|
|
1005
1029
|
if (isPermissionErrorMsg(err.message)) {
|
|
1006
1030
|
if (!_permissionDiagnosticSent) {
|
|
@@ -1030,10 +1054,6 @@ export async function handleUnifyChat(msg) {
|
|
|
1030
1054
|
type: 'result',
|
|
1031
1055
|
result_text: '',
|
|
1032
1056
|
}, groupId);
|
|
1033
|
-
} finally {
|
|
1034
|
-
if (currentAbortCtrl && currentAbortCtrl.signal.aborted) {
|
|
1035
|
-
// Aborted controllers stay where they are; a new query will replace.
|
|
1036
|
-
}
|
|
1037
1057
|
}
|
|
1038
1058
|
}
|
|
1039
1059
|
|
|
@@ -1164,9 +1184,6 @@ export async function handleUnifyFetchSummaryHistory(msg = {}) {
|
|
|
1164
1184
|
const yeaftDir = ctx.CONFIG?.yeaftDir;
|
|
1165
1185
|
if (!yeaftDir) { reply({ revisions: [], archived: null, error: 'no_yeaft_dir' }); return; }
|
|
1166
1186
|
|
|
1167
|
-
const { openGroup, loadGroupMeta } = await import('./groups/group-store.js');
|
|
1168
|
-
const { join } = await import('node:path');
|
|
1169
|
-
const { existsSync } = await import('node:fs');
|
|
1170
1187
|
const root = join(yeaftDir, 'groups');
|
|
1171
1188
|
const dir = join(root, groupId);
|
|
1172
1189
|
if (!existsSync(dir) || !loadGroupMeta(dir)) {
|