@yeaft/webchat-agent 0.1.762 → 0.1.766
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/bin/yeaft-stats.js +165 -0
- package/connection/message-router.js +6 -9
- package/package.json +3 -2
- package/unify/cli.js +16 -0
- package/unify/dream-v2/prompts/index.js +4 -2
- package/unify/dream-v2/session-wiring.js +5 -4
- package/unify/dream-v2/triage.js +7 -10
- package/unify/engine.js +42 -88
- package/unify/groups/pre-flow.js +5 -5
- package/unify/prompts.js +35 -202
- package/unify/session.js +18 -3
- package/unify/stats/format.js +31 -0
- package/unify/stats/tool-usage.js +324 -0
- package/unify/sub-agent/runner.js +4 -19
- package/unify/templates/plan-instruction.md +42 -0
- package/unify/templates/tool-guidance.md +41 -0
- package/unify/tools/index.js +13 -22
- package/unify/tools/start-plan.js +133 -0
- package/unify/tools/todo-write.js +125 -0
- package/unify/vp/vp-crud.js +1 -0
- package/unify/vp/vp-loader.js +2 -1
- package/unify/vp/vp-store.js +8 -0
- package/unify/web-bridge.js +93 -145
- package/unify/features/store.js +0 -583
- package/unify/features/summary.js +0 -250
- package/unify/tools/feature-tools.js +0 -713
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* todo-write.js — TodoWrite tool: per-VP multi-step task tracking.
|
|
3
|
+
*
|
|
4
|
+
* Mirrors Claude Code's `TodoWrite` tool 1:1 in shape (name + `todos[]`
|
|
5
|
+
* with `content` / `status` / `activeForm`) so the existing frontend
|
|
6
|
+
* rendering pipeline (`MessageList.js:691` → `AssistantTurn.js:53-63`
|
|
7
|
+
* → `ToolLine.js:150`) renders a checkmark-style list automatically
|
|
8
|
+
* without any new UI code. The frontend reads the *input* of the
|
|
9
|
+
* `tool_use` event — not the result — so this tool's persistence story
|
|
10
|
+
* is "stamp into the LLM event stream and cache on ctx for replay."
|
|
11
|
+
*
|
|
12
|
+
* Per-VP isolation: each VP keeps its own current todo list. The
|
|
13
|
+
* web-bridge injects `ctx.getCurrentTodos()` / `ctx.setCurrentTodos()`
|
|
14
|
+
* pointing at a per-(groupId,vpId) slot so two VPs in the same group
|
|
15
|
+
* can independently track their own multi-step tasks without
|
|
16
|
+
* stepping on each other.
|
|
17
|
+
*
|
|
18
|
+
* Reference: plan §2 (2026-05-13 — Feature system retired, TodoWrite
|
|
19
|
+
* added as the actual progress-tracking surface for the LLM).
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import { defineTool } from './types.js';
|
|
23
|
+
|
|
24
|
+
const VALID_STATUS = new Set(['pending', 'in_progress', 'completed']);
|
|
25
|
+
|
|
26
|
+
export default defineTool({
|
|
27
|
+
name: 'TodoWrite',
|
|
28
|
+
description: `Track multi-step task progress with a checklist that the user can see ticked off in real time.
|
|
29
|
+
|
|
30
|
+
WHEN TO USE:
|
|
31
|
+
- The task has 3+ meaningful steps, or
|
|
32
|
+
- The user gave you a list of things to do (numbered/comma-separated), or
|
|
33
|
+
- You're about to start a non-trivial, multi-file change.
|
|
34
|
+
|
|
35
|
+
HOW TO USE:
|
|
36
|
+
- First call: enumerate all the todos with status "pending", set exactly one to "in_progress".
|
|
37
|
+
- Each subsequent call: rewrite the FULL list — mark the just-finished item "completed", mark the next item "in_progress".
|
|
38
|
+
- AT MOST one item may be "in_progress" at any time.
|
|
39
|
+
- \`content\` is the imperative form ("Run tests"); \`activeForm\` is the present-continuous shown during execution ("Running tests").
|
|
40
|
+
|
|
41
|
+
WHEN NOT TO USE:
|
|
42
|
+
- Single trivial change, single command run, pure conversation/question.`,
|
|
43
|
+
parameters: {
|
|
44
|
+
type: 'object',
|
|
45
|
+
properties: {
|
|
46
|
+
todos: {
|
|
47
|
+
type: 'array',
|
|
48
|
+
description: 'The full current todo list. Always send the entire list, not a diff.',
|
|
49
|
+
items: {
|
|
50
|
+
type: 'object',
|
|
51
|
+
properties: {
|
|
52
|
+
content: {
|
|
53
|
+
type: 'string',
|
|
54
|
+
description: 'Imperative description of the step (e.g. "Run tests").',
|
|
55
|
+
},
|
|
56
|
+
status: {
|
|
57
|
+
type: 'string',
|
|
58
|
+
enum: ['pending', 'in_progress', 'completed'],
|
|
59
|
+
description: 'Current state. At most one item may be "in_progress".',
|
|
60
|
+
},
|
|
61
|
+
activeForm: {
|
|
62
|
+
type: 'string',
|
|
63
|
+
description: 'Present-continuous form shown while executing (e.g. "Running tests").',
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
required: ['content', 'status', 'activeForm'],
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
required: ['todos'],
|
|
71
|
+
},
|
|
72
|
+
isConcurrencySafe: () => false,
|
|
73
|
+
isReadOnly: () => true,
|
|
74
|
+
async execute(input, ctx) {
|
|
75
|
+
const todos = input && Array.isArray(input.todos) ? input.todos : null;
|
|
76
|
+
if (!todos || todos.length === 0) {
|
|
77
|
+
return JSON.stringify({ error: 'todos must be a non-empty array' });
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
let inProgressCount = 0;
|
|
81
|
+
for (let i = 0; i < todos.length; i++) {
|
|
82
|
+
const t = todos[i];
|
|
83
|
+
if (!t || typeof t !== 'object') {
|
|
84
|
+
return JSON.stringify({ error: `todos[${i}] must be an object` });
|
|
85
|
+
}
|
|
86
|
+
if (typeof t.content !== 'string' || !t.content.trim()) {
|
|
87
|
+
return JSON.stringify({ error: `todos[${i}].content must be a non-empty string` });
|
|
88
|
+
}
|
|
89
|
+
if (typeof t.activeForm !== 'string' || !t.activeForm.trim()) {
|
|
90
|
+
return JSON.stringify({ error: `todos[${i}].activeForm must be a non-empty string` });
|
|
91
|
+
}
|
|
92
|
+
if (!VALID_STATUS.has(t.status)) {
|
|
93
|
+
return JSON.stringify({
|
|
94
|
+
error: `todos[${i}].status must be one of: pending, in_progress, completed`,
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
if (t.status === 'in_progress') inProgressCount += 1;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (inProgressCount > 1) {
|
|
101
|
+
return JSON.stringify({
|
|
102
|
+
error: `at most one todo may be in_progress at a time (found ${inProgressCount})`,
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Cache the current todo list onto the per-VP slot if the web-bridge
|
|
107
|
+
// provided one. Best-effort: this is the cache the frontend may pull
|
|
108
|
+
// on reconnect / VP-switch. Tools should not crash if the slot is
|
|
109
|
+
// missing — sub-agent ctx or test ctx may lack it.
|
|
110
|
+
if (ctx && typeof ctx.setCurrentTodos === 'function') {
|
|
111
|
+
try { ctx.setCurrentTodos(todos.slice()); } catch { /* swallow */ }
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const counts = { pending: 0, in_progress: 0, completed: 0 };
|
|
115
|
+
for (const t of todos) counts[t.status] += 1;
|
|
116
|
+
|
|
117
|
+
return JSON.stringify({
|
|
118
|
+
success: true,
|
|
119
|
+
count: todos.length,
|
|
120
|
+
pending: counts.pending,
|
|
121
|
+
in_progress: counts.in_progress,
|
|
122
|
+
completed: counts.completed,
|
|
123
|
+
});
|
|
124
|
+
},
|
|
125
|
+
});
|
package/unify/vp/vp-crud.js
CHANGED
|
@@ -297,5 +297,6 @@ export function readVp(vpId, options = {}) {
|
|
|
297
297
|
traits: Array.isArray(meta.traits) ? meta.traits.map(String) : [],
|
|
298
298
|
modelHint,
|
|
299
299
|
persona: body,
|
|
300
|
+
planInstruction: typeof meta.planInstruction === 'string' ? String(meta.planInstruction) : '',
|
|
300
301
|
};
|
|
301
302
|
}
|
package/unify/vp/vp-loader.js
CHANGED
|
@@ -140,7 +140,8 @@ export class VpLoader {
|
|
|
140
140
|
prev.mtimeMs !== next.mtimeMs ||
|
|
141
141
|
prev.persona !== next.persona ||
|
|
142
142
|
prev.name !== next.name ||
|
|
143
|
-
prev.role !== next.role
|
|
143
|
+
prev.role !== next.role ||
|
|
144
|
+
prev.planInstruction !== next.planInstruction
|
|
144
145
|
) {
|
|
145
146
|
// In-place update preserves VP identity → RoleInstance.runtimeState
|
|
146
147
|
// is untouched; persona swap propagates on next read of vp.persona.
|
package/unify/vp/vp-store.js
CHANGED
|
@@ -37,6 +37,9 @@ import { createHash } from 'crypto';
|
|
|
37
37
|
* @property {'fast'|'primary'|undefined} modelHint
|
|
38
38
|
* @property {string} persona — markdown body (persona / system prompt seed)
|
|
39
39
|
* @property {string} personaHash — sha256(persona).slice(0,8); changes when persona body changes
|
|
40
|
+
* @property {string} planInstruction — optional per-VP planning style (used by `StartPlan`
|
|
41
|
+
* tool); '' means "fall back to the default template".
|
|
42
|
+
* Frontmatter scalar key `planInstruction`.
|
|
40
43
|
* @property {string} dir — absolute path to VP dir
|
|
41
44
|
* @property {string} memoryDir — absolute path to VP memory dir
|
|
42
45
|
* @property {number} mtimeMs — role.md mtime (for hot-reload)
|
|
@@ -163,6 +166,11 @@ export function loadVpFromDir(dir) {
|
|
|
163
166
|
modelHint,
|
|
164
167
|
persona: body,
|
|
165
168
|
personaHash: personaHashValue,
|
|
169
|
+
// Optional per-VP planning style for the `StartPlan` tool. Stored as a
|
|
170
|
+
// raw scalar string in role.md frontmatter (`planInstruction: "..."`).
|
|
171
|
+
// Empty / missing → the tool falls back to the default template. Kept
|
|
172
|
+
// verbatim — the tool itself is responsible for any framing.
|
|
173
|
+
planInstruction: typeof meta.planInstruction === 'string' ? String(meta.planInstruction) : '',
|
|
166
174
|
dir,
|
|
167
175
|
memoryDir,
|
|
168
176
|
mtimeMs: st.mtimeMs,
|
package/unify/web-bridge.js
CHANGED
|
@@ -107,6 +107,24 @@ const vpDrivers = new Map();
|
|
|
107
107
|
const vpEngines = new Map();
|
|
108
108
|
/** @type {Map<string, AbortController>} */
|
|
109
109
|
const vpAborts = new Map();
|
|
110
|
+
/**
|
|
111
|
+
* Per-(groupId, vpId) current TodoWrite list. Each VP in a group keeps
|
|
112
|
+
* its own todo state so two VPs in the same group can independently
|
|
113
|
+
* track multi-step tasks without overwriting each other. Threaded into
|
|
114
|
+
* the engine's tool ctx via buildVpQueryOpts → getCurrentTodos /
|
|
115
|
+
* setCurrentTodos closures. Best-effort in-memory cache only — todos
|
|
116
|
+
* are also stamped into the LLM event stream (the frontend reads from
|
|
117
|
+
* the tool_use input, not from this map), so a server restart simply
|
|
118
|
+
* loses the "what was the most recent list?" peek without breaking the
|
|
119
|
+
* UI replay.
|
|
120
|
+
*
|
|
121
|
+
* Key: `${groupId}::${vpId}` (matches vpEngines/vpAborts convention).
|
|
122
|
+
* Value: `Array<{content, status, activeForm}>` — the last full list
|
|
123
|
+
* the VP wrote with TodoWrite.
|
|
124
|
+
*
|
|
125
|
+
* @type {Map<string, Array<{content: string, status: string, activeForm: string}>>}
|
|
126
|
+
*/
|
|
127
|
+
const vpCurrentTodos = new Map();
|
|
110
128
|
/**
|
|
111
129
|
* Per-group cached coordinator + router. Created on first
|
|
112
130
|
* `handleUnifyGroupChat` for a given groupId; reused across user messages
|
|
@@ -157,6 +175,11 @@ function invalidateGroupContext(groupId) {
|
|
|
157
175
|
if (!k.startsWith(prefix)) continue;
|
|
158
176
|
if (Array.isArray(inbox)) inbox.length = 0;
|
|
159
177
|
}
|
|
178
|
+
// Reap per-(group,vp) TodoWrite snapshots for this group so a
|
|
179
|
+
// deleted/renamed group doesn't pin a stale checklist forever.
|
|
180
|
+
for (const k of vpCurrentTodos.keys()) {
|
|
181
|
+
if (k.startsWith(prefix)) vpCurrentTodos.delete(k);
|
|
182
|
+
}
|
|
160
183
|
// Engines are NOT torn down here on purpose. They hold subordinate
|
|
161
184
|
// state (AMS adjustments) that should survive a meta change and a
|
|
162
185
|
// closed groupHandle — they don't reach the on-disk group meta
|
|
@@ -739,6 +762,7 @@ export async function __testResetVpState() {
|
|
|
739
762
|
vpEngines.clear();
|
|
740
763
|
vpAborts.clear();
|
|
741
764
|
groupContexts.clear();
|
|
765
|
+
vpCurrentTodos.clear();
|
|
742
766
|
// Per-group compact in-flight + pending state lives on the session's
|
|
743
767
|
// Compactor. Clear it so a follow-on test doesn't see ghost in-flight
|
|
744
768
|
// promises from a prior run.
|
|
@@ -753,27 +777,25 @@ export async function __testResetVpState() {
|
|
|
753
777
|
* Envelope fields: conversationId, groupId, vpId, turnId — the last two
|
|
754
778
|
* let the frontend route incremental deltas to the correct per-VP message block.
|
|
755
779
|
*/
|
|
756
|
-
function sendUnifyOutput(data, { groupId, vpId, turnId
|
|
780
|
+
function sendUnifyOutput(data, { groupId, vpId, turnId } = {}) {
|
|
757
781
|
sendToServer({
|
|
758
782
|
type: 'unify_output',
|
|
759
783
|
conversationId: unifyConversationId,
|
|
760
784
|
...(groupId ? { groupId } : {}),
|
|
761
785
|
...(vpId ? { vpId } : {}),
|
|
762
786
|
...(turnId ? { turnId } : {}),
|
|
763
|
-
...(featureId ? { featureId } : {}),
|
|
764
787
|
data,
|
|
765
788
|
});
|
|
766
789
|
}
|
|
767
790
|
|
|
768
791
|
/** Send a unify_output event (non-claude_output metadata). */
|
|
769
|
-
function sendUnifyEvent(event, { groupId, vpId, turnId
|
|
792
|
+
function sendUnifyEvent(event, { groupId, vpId, turnId } = {}) {
|
|
770
793
|
sendToServer({
|
|
771
794
|
type: 'unify_output',
|
|
772
795
|
conversationId: unifyConversationId,
|
|
773
796
|
...(groupId ? { groupId } : {}),
|
|
774
797
|
...(vpId ? { vpId } : {}),
|
|
775
798
|
...(turnId ? { turnId } : {}),
|
|
776
|
-
...(featureId ? { featureId } : {}),
|
|
777
799
|
event,
|
|
778
800
|
});
|
|
779
801
|
}
|
|
@@ -1152,18 +1174,10 @@ export function installUnifyRuntimeBridge(s) {
|
|
|
1152
1174
|
*/
|
|
1153
1175
|
function handleEngineEvent(event, hctx) {
|
|
1154
1176
|
hctx.resetQueryTimer();
|
|
1155
|
-
// Sub-agent events may carry their own `featureId` (stamped by the
|
|
1156
|
-
// sub-agent runner from the parent's inbound feature scope). Plain
|
|
1157
|
-
// VP-turn events have no featureId — auto-feature creation was
|
|
1158
|
-
// removed when Track-A / FeatureArc was deleted (2026-05-08).
|
|
1159
|
-
const eventFeatureId = typeof event === 'object' && event && typeof event.featureId === 'string'
|
|
1160
|
-
? event.featureId
|
|
1161
|
-
: null;
|
|
1162
1177
|
const envelope = {
|
|
1163
1178
|
groupId: hctx.groupId,
|
|
1164
1179
|
vpId: hctx.vpId,
|
|
1165
1180
|
turnId: hctx.turnId,
|
|
1166
|
-
...(eventFeatureId ? { featureId: eventFeatureId } : {}),
|
|
1167
1181
|
};
|
|
1168
1182
|
|
|
1169
1183
|
switch (event.type) {
|
|
@@ -1854,6 +1868,10 @@ export function buildVpQueryOpts({ vpId, groupCoordinator, groupId, envelope })
|
|
|
1854
1868
|
role: vp.role || '',
|
|
1855
1869
|
roleZh: vp.roleZh || '',
|
|
1856
1870
|
persona: vp.persona || '',
|
|
1871
|
+
// Optional per-VP planning style for the `StartPlan` tool. Empty
|
|
1872
|
+
// string means "fall back to the default template" — the tool
|
|
1873
|
+
// handles the lookup so callers stay ignorant of the default.
|
|
1874
|
+
planInstruction: typeof vp.planInstruction === 'string' ? vp.planInstruction : '',
|
|
1857
1875
|
};
|
|
1858
1876
|
}
|
|
1859
1877
|
} catch { /* persona load is best-effort */ }
|
|
@@ -1872,6 +1890,20 @@ export function buildVpQueryOpts({ vpId, groupCoordinator, groupId, envelope })
|
|
|
1872
1890
|
if (envelope && typeof envelope === 'object') {
|
|
1873
1891
|
out.inboundEnvelope = envelope;
|
|
1874
1892
|
}
|
|
1893
|
+
// TodoWrite per-VP isolation. Bind closures that read/write a slot
|
|
1894
|
+
// keyed by `${groupId}::${vpId}` so two VPs in the same group don't
|
|
1895
|
+
// overwrite each other's lists, and the TodoWrite tool can stay
|
|
1896
|
+
// ignorant of routing details (it just calls ctx.setCurrentTodos).
|
|
1897
|
+
const todosKey = `${out.groupId || ''}::${resolvedVpId}`;
|
|
1898
|
+
out.getCurrentTodos = () => {
|
|
1899
|
+
const cached = vpCurrentTodos.get(todosKey);
|
|
1900
|
+
return Array.isArray(cached) ? cached.slice() : null;
|
|
1901
|
+
};
|
|
1902
|
+
out.setCurrentTodos = (todos) => {
|
|
1903
|
+
if (Array.isArray(todos)) {
|
|
1904
|
+
vpCurrentTodos.set(todosKey, todos.slice());
|
|
1905
|
+
}
|
|
1906
|
+
};
|
|
1875
1907
|
return out;
|
|
1876
1908
|
}
|
|
1877
1909
|
|
|
@@ -2529,149 +2561,64 @@ export async function handleUnifyDreamTrigger(msg = {}) {
|
|
|
2529
2561
|
}
|
|
2530
2562
|
}
|
|
2531
2563
|
|
|
2532
|
-
/**
|
|
2533
|
-
|
|
2534
|
-
|
|
2535
|
-
|
|
2536
|
-
|
|
2537
|
-
|
|
2538
|
-
|
|
2539
|
-
|
|
2540
|
-
|
|
2541
|
-
|
|
2542
|
-
|
|
2543
|
-
|
|
2544
|
-
|
|
2545
|
-
|
|
2546
|
-
|
|
2547
|
-
|
|
2548
|
-
|
|
2549
|
-
|
|
2550
|
-
if (!featureId) { reply({ revisions: [], archived: null, error: 'missing_feature_id' }); return; }
|
|
2551
|
-
|
|
2564
|
+
/**
|
|
2565
|
+
* 2026-05-13: serve the Unify debug drawer's "Tool Stats" panel.
|
|
2566
|
+
*
|
|
2567
|
+
* Replies with `{type: 'unify_tool_stats', snapshot, registered,
|
|
2568
|
+
* unused}`. `snapshot` is the `ToolUsageStats.snapshot()` keyed by
|
|
2569
|
+
* tool name (callCount, errorCount, p50Ms, p95Ms, avgMs, lastCalledAt,
|
|
2570
|
+
* lastError, errorRate). `registered` is the static list of built-in
|
|
2571
|
+
* tool names so the frontend can render the "(defined but never
|
|
2572
|
+
* called)" subview without spinning up its own registry mirror.
|
|
2573
|
+
*
|
|
2574
|
+
* Best-effort: if the session hasn't booted yet or toolStats is
|
|
2575
|
+
* missing, we still reply with an empty snapshot so the UI can render
|
|
2576
|
+
* a placeholder rather than spin forever.
|
|
2577
|
+
*/
|
|
2578
|
+
export async function handleUnifyFetchToolStats(_msg = {}) {
|
|
2579
|
+
let snapshot = {};
|
|
2580
|
+
let registered = [];
|
|
2581
|
+
let unused = [];
|
|
2552
2582
|
try {
|
|
2553
|
-
|
|
2554
|
-
|
|
2555
|
-
const feature = featureStore?.get(featureId);
|
|
2556
|
-
if (!feature) { reply({ revisions: [], archived: null, error: 'feature_not_found' }); return; }
|
|
2557
|
-
const groupId = feature.groupId;
|
|
2558
|
-
if (!groupId) { reply({ revisions: [], archived: null, error: 'feature_has_no_group' }); return; }
|
|
2559
|
-
|
|
2560
|
-
const yeaftDir = ctx.CONFIG?.yeaftDir;
|
|
2561
|
-
if (!yeaftDir) { reply({ revisions: [], archived: null, error: 'no_yeaft_dir' }); return; }
|
|
2562
|
-
|
|
2563
|
-
const root = join(yeaftDir, 'groups');
|
|
2564
|
-
const dir = join(root, groupId);
|
|
2565
|
-
if (!existsSync(dir) || !loadGroupMeta(dir)) {
|
|
2566
|
-
reply({ revisions: [], archived: null, error: 'group_not_found' });
|
|
2567
|
-
return;
|
|
2568
|
-
}
|
|
2569
|
-
const groupHandle = openGroup(root, groupId);
|
|
2570
|
-
const summaries = [];
|
|
2571
|
-
for (const m of groupHandle.streamMessages()) {
|
|
2572
|
-
if (!m || m.featureId !== featureId) continue;
|
|
2573
|
-
const meta = m.meta || {};
|
|
2574
|
-
if (meta.kind === 'summary' || meta.type === 'summary') summaries.push(m);
|
|
2583
|
+
if (session?.toolStats && typeof session.toolStats.snapshot === 'function') {
|
|
2584
|
+
snapshot = session.toolStats.snapshot();
|
|
2575
2585
|
}
|
|
2576
|
-
|
|
2577
|
-
|
|
2578
|
-
|
|
2579
|
-
|
|
2580
|
-
|
|
2581
|
-
|
|
2582
|
-
|
|
2583
|
-
|
|
2584
|
-
if (Array.isArray(arr)) for (const id of arr) supersededIds.add(id);
|
|
2586
|
+
// Pull the static built-in tool list. MCP/skill tools aren't in
|
|
2587
|
+
// here — that's fine: the "unused" view is meant to flag stale
|
|
2588
|
+
// built-in tools, not user-installed ones.
|
|
2589
|
+
const { allTools } = await import('./tools/index.js');
|
|
2590
|
+
if (Array.isArray(allTools)) {
|
|
2591
|
+
registered = allTools
|
|
2592
|
+
.filter(t => t && typeof t.name === 'string' && t.name)
|
|
2593
|
+
.map(t => t.name);
|
|
2585
2594
|
}
|
|
2586
|
-
|
|
2587
|
-
|
|
2588
|
-
for (const s of summaries) {
|
|
2589
|
-
if (supersededIds.has(s.id)) archived.push(s);
|
|
2590
|
-
else current.push(s);
|
|
2595
|
+
if (session?.toolStats && typeof session.toolStats.getRegisteredButUncalled === 'function') {
|
|
2596
|
+
unused = session.toolStats.getRegisteredButUncalled(registered);
|
|
2591
2597
|
}
|
|
2592
|
-
const overflow = current.slice(10);
|
|
2593
|
-
const trimmedCurrent = current.slice(0, 10);
|
|
2594
|
-
if (overflow.length) archived.push(...overflow);
|
|
2595
|
-
archived.sort((a, b) => {
|
|
2596
|
-
const at = Date.parse(a.ts || '') || 0;
|
|
2597
|
-
const bt = Date.parse(b.ts || '') || 0;
|
|
2598
|
-
return bt - at;
|
|
2599
|
-
});
|
|
2600
|
-
reply({
|
|
2601
|
-
revisions: trimmedCurrent,
|
|
2602
|
-
archived: includeArchived ? archived : null,
|
|
2603
|
-
});
|
|
2604
2598
|
} catch (err) {
|
|
2605
|
-
|
|
2599
|
+
sendToServer({
|
|
2600
|
+
type: 'unify_tool_stats',
|
|
2601
|
+
snapshot: {},
|
|
2602
|
+
registered: [],
|
|
2603
|
+
unused: [],
|
|
2604
|
+
error: err && err.message ? err.message : String(err),
|
|
2605
|
+
});
|
|
2606
|
+
return;
|
|
2606
2607
|
}
|
|
2607
|
-
|
|
2608
|
-
|
|
2609
|
-
|
|
2610
|
-
|
|
2611
|
-
|
|
2612
|
-
const op = typeof msg.op === 'string' ? msg.op : null;
|
|
2613
|
-
const featureId = typeof msg.featureId === 'string' ? msg.featureId : null;
|
|
2614
|
-
const vpId = typeof msg.vpId === 'string' ? msg.vpId : null;
|
|
2615
|
-
const relatedFeatureId = typeof msg.relatedFeatureId === 'string' ? msg.relatedFeatureId : null;
|
|
2616
|
-
|
|
2617
|
-
const reply = (extra = {}) => sendUnifyEvent({
|
|
2618
|
-
type: 'unify_feature_crud_result',
|
|
2619
|
-
op,
|
|
2620
|
-
featureId,
|
|
2621
|
-
...(vpId ? { vpId } : {}),
|
|
2622
|
-
...extra,
|
|
2623
|
-
...(requestId ? { requestId } : {}),
|
|
2608
|
+
sendToServer({
|
|
2609
|
+
type: 'unify_tool_stats',
|
|
2610
|
+
snapshot,
|
|
2611
|
+
registered,
|
|
2612
|
+
unused,
|
|
2624
2613
|
});
|
|
2614
|
+
}
|
|
2625
2615
|
|
|
2626
|
-
|
|
2627
|
-
|
|
2628
|
-
|
|
2629
|
-
try {
|
|
2630
|
-
const { getFeatureStore } = await import('./tools/feature-tools.js');
|
|
2631
|
-
const featureStore = getFeatureStore();
|
|
2632
|
-
const feature = featureStore?.get(featureId);
|
|
2633
|
-
if (!feature) { reply({ ok: false, error: 'feature_not_found' }); return; }
|
|
2634
|
-
|
|
2635
|
-
if (op === 'relate' || op === 'unrelate') {
|
|
2636
|
-
if (!relatedFeatureId) { reply({ ok: false, error: 'missing_related_feature_id' }); return; }
|
|
2637
|
-
const other = featureStore.get(relatedFeatureId);
|
|
2638
|
-
if (!other) { reply({ ok: false, error: 'related_feature_not_found' }); return; }
|
|
2639
|
-
const apply = (t, otherId, add) => {
|
|
2640
|
-
const cur = Array.isArray(t.relatedFeatureIds) ? t.relatedFeatureIds.slice() : [];
|
|
2641
|
-
const idx = cur.indexOf(otherId);
|
|
2642
|
-
if (add && idx === -1) cur.push(otherId);
|
|
2643
|
-
if (!add && idx !== -1) cur.splice(idx, 1);
|
|
2644
|
-
featureStore.update(t.id, { relatedFeatureIds: cur });
|
|
2645
|
-
};
|
|
2646
|
-
apply(feature, relatedFeatureId, op === 'relate');
|
|
2647
|
-
apply(other, featureId, op === 'relate');
|
|
2648
|
-
reply({ ok: true, relatedFeatureId });
|
|
2649
|
-
return;
|
|
2650
|
-
}
|
|
2651
|
-
|
|
2652
|
-
if (op === 'kick_vp') {
|
|
2653
|
-
if (!vpId) { reply({ ok: false, error: 'missing_vp_id' }); return; }
|
|
2654
|
-
featureStore.removeMember(featureId, vpId);
|
|
2655
|
-
reply({ ok: true });
|
|
2656
|
-
return;
|
|
2657
|
-
}
|
|
2658
|
-
|
|
2659
|
-
if (op === 'abort_vp') {
|
|
2660
|
-
if (!vpId) { reply({ ok: false, error: 'missing_vp_id' }); return; }
|
|
2661
|
-
// H2.f.2: per-VP abort no longer routed through engineRegistry — the
|
|
2662
|
-
// single engine handles its own abort via currentAbortCtrl. Reply
|
|
2663
|
-
// ok:true so the UI surface still works; deeper per-VP cancel is a
|
|
2664
|
-
// separate task.
|
|
2665
|
-
reply({ ok: true });
|
|
2666
|
-
return;
|
|
2667
|
-
}
|
|
2668
|
-
|
|
2669
|
-
reply({ ok: false, error: 'unknown_op' });
|
|
2670
|
-
} catch (err) {
|
|
2671
|
-
reply({ ok: false, error: String(err?.message || err) });
|
|
2672
|
-
}
|
|
2616
|
+
/** Deprecated mode switch — Unify is single-mode. */
|
|
2617
|
+
export function handleUnifyModeSwitch(_msg) {
|
|
2618
|
+
console.warn('[Unify] unify_mode_switch is deprecated and ignored — Unify now runs in a single unified mode.');
|
|
2673
2619
|
}
|
|
2674
2620
|
|
|
2621
|
+
|
|
2675
2622
|
/** Handle model switch from the web UI. */
|
|
2676
2623
|
export function handleUnifyModelSwitch(msg) {
|
|
2677
2624
|
if (!session || !msg.model) return;
|
|
@@ -2914,6 +2861,7 @@ export async function resetUnifySession() {
|
|
|
2914
2861
|
vpDrivers.clear();
|
|
2915
2862
|
vpEngines.clear();
|
|
2916
2863
|
groupContexts.clear();
|
|
2864
|
+
vpCurrentTodos.clear();
|
|
2917
2865
|
// History-dedup cache is keyed by per-session coordinator msg ids;
|
|
2918
2866
|
// a fresh session resets the id space, so clear the cache too.
|
|
2919
2867
|
_persistedUserMsgIds.clear();
|