@yeaft/webchat-agent 0.1.615 → 0.1.617
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/upgrade.js +102 -0
- package/package.json +1 -1
- package/unify/compact/orchestrator.js +1 -1
- package/unify/{task-message.js → feature-message.js} +38 -38
- package/unify/features/store.js +583 -0
- package/unify/{tasks → features}/summary.js +54 -85
- package/unify/memory/dream-shard.js +15 -15
- package/unify/memory/recall-r6.js +2 -2
- package/unify/memory/schema.js +7 -7
- package/unify/memory/scope-tree.js +6 -6
- package/unify/pipeline/dispatcher.js +7 -7
- package/unify/router/intent-classifier.js +25 -25
- package/unify/session.js +3 -3
- package/unify/threads/store.js +18 -18
- package/unify/tools/{task-tools.js → feature-tools.js} +180 -175
- package/unify/tools/index.js +21 -21
- package/unify/tools/thread-tools.js +29 -108
- package/unify/web-bridge.js +61 -61
- package/unify/memory/migrate-r5-to-r6.js +0 -611
- package/unify/migration/detect.js +0 -176
- package/unify/migration/map-fields.js +0 -293
- package/unify/migration/v0-to-v1.js +0 -588
- package/unify/tasks/store.js +0 -652
package/unify/tools/index.js
CHANGED
|
@@ -48,25 +48,25 @@ import listAgents from './list-agents.js';
|
|
|
48
48
|
// --- P1 Routing tools (task-334d) ---
|
|
49
49
|
import routeForward from './route-forward.js';
|
|
50
50
|
|
|
51
|
-
// --- P1
|
|
51
|
+
// --- P1 Feature tools ---
|
|
52
52
|
import {
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
53
|
+
featureCreate,
|
|
54
|
+
featureUpdate,
|
|
55
|
+
featureList,
|
|
56
|
+
featureGet,
|
|
57
|
+
featureProgress,
|
|
58
|
+
featureMemory,
|
|
59
|
+
followupFeature,
|
|
60
60
|
updatePlan,
|
|
61
|
-
|
|
61
|
+
featureSummaryPost,
|
|
62
|
+
} from './feature-tools.js';
|
|
62
63
|
|
|
63
64
|
// --- P1 Thread tools (task-299 Phase 1) ---
|
|
64
65
|
import {
|
|
65
66
|
spawnThread,
|
|
66
67
|
switchThread,
|
|
67
68
|
listThreads,
|
|
68
|
-
|
|
69
|
-
spawnTask,
|
|
69
|
+
attachThreadToFeature,
|
|
70
70
|
readThreadSummary,
|
|
71
71
|
readThreadRecent,
|
|
72
72
|
} from './thread-tools.js';
|
|
@@ -126,22 +126,22 @@ export const allTools = [
|
|
|
126
126
|
// P1 Routing (task-334d)
|
|
127
127
|
routeForward,
|
|
128
128
|
|
|
129
|
-
// P1
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
129
|
+
// P1 Feature
|
|
130
|
+
featureCreate,
|
|
131
|
+
featureUpdate,
|
|
132
|
+
featureList,
|
|
133
|
+
featureGet,
|
|
134
|
+
featureProgress,
|
|
135
|
+
featureMemory,
|
|
136
|
+
followupFeature,
|
|
137
137
|
updatePlan,
|
|
138
|
+
featureSummaryPost,
|
|
138
139
|
|
|
139
140
|
// P1 Thread (task-299 Phase 1)
|
|
140
141
|
spawnThread,
|
|
141
142
|
switchThread,
|
|
142
143
|
listThreads,
|
|
143
|
-
|
|
144
|
-
spawnTask,
|
|
144
|
+
attachThreadToFeature,
|
|
145
145
|
readThreadSummary,
|
|
146
146
|
readThreadRecent,
|
|
147
147
|
|
|
@@ -2,14 +2,13 @@
|
|
|
2
2
|
* thread-tools.js — Thread-spawning tools for Unify Engine (Phase 1).
|
|
3
3
|
*
|
|
4
4
|
* Phase 1 scope (task-299 rework):
|
|
5
|
-
* - SpawnThread
|
|
6
|
-
* - SwitchThread
|
|
7
|
-
* - ListThreads
|
|
8
|
-
* -
|
|
9
|
-
* -
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
* - ReadThreadRecent — cross-reference: last N messages of a thread
|
|
5
|
+
* - SpawnThread — create a new thread
|
|
6
|
+
* - SwitchThread — set the engine's currentThreadId marker
|
|
7
|
+
* - ListThreads — list threads + current marker + cached stats
|
|
8
|
+
* - AttachThreadToFeature — bind a thread to an existing feature
|
|
9
|
+
* - ReadThreadSummary — cross-reference: summary of a thread (id/name/
|
|
10
|
+
* status/messageCount/lastMessageAt/feature)
|
|
11
|
+
* - ReadThreadRecent — cross-reference: last N messages of a thread
|
|
13
12
|
*
|
|
14
13
|
* Phase 1 uses the in-memory ThreadStore (agent/unify/threads/store.js)
|
|
15
14
|
* and the existing ConversationStore for messages. When task-298 merges,
|
|
@@ -18,9 +17,8 @@
|
|
|
18
17
|
*/
|
|
19
18
|
|
|
20
19
|
import { defineTool } from './types.js';
|
|
21
|
-
import { randomUUID } from 'crypto';
|
|
22
20
|
import { getThreadStore, MAIN_THREAD_ID } from '../threads/store.js';
|
|
23
|
-
import {
|
|
21
|
+
import { getFeatureStore } from './feature-tools.js';
|
|
24
22
|
|
|
25
23
|
// ─── SpawnThread ─────────────────────────────────────────
|
|
26
24
|
|
|
@@ -134,7 +132,7 @@ fields — does not scan messages.`,
|
|
|
134
132
|
archived: t.archived,
|
|
135
133
|
unread: t.unread || 0,
|
|
136
134
|
preview: t.preview || '',
|
|
137
|
-
|
|
135
|
+
attachedFeatureId: store.attachedFeature(t.id),
|
|
138
136
|
}));
|
|
139
137
|
return JSON.stringify(
|
|
140
138
|
{
|
|
@@ -148,44 +146,44 @@ fields — does not scan messages.`,
|
|
|
148
146
|
},
|
|
149
147
|
});
|
|
150
148
|
|
|
151
|
-
// ───
|
|
149
|
+
// ─── AttachThreadToFeature ──────────────────────────────────
|
|
152
150
|
|
|
153
|
-
export const
|
|
154
|
-
name: '
|
|
155
|
-
description: `Link an existing thread to an existing
|
|
151
|
+
export const attachThreadToFeature = defineTool({
|
|
152
|
+
name: 'AttachThreadToFeature',
|
|
153
|
+
description: `Link an existing thread to an existing feature.
|
|
156
154
|
|
|
157
|
-
Use to record which thread is responsible for which
|
|
158
|
-
the
|
|
155
|
+
Use to record which thread is responsible for which feature. The thread and
|
|
156
|
+
the feature must both already exist. Overwrites any previous attachment for
|
|
159
157
|
the same thread.`,
|
|
160
158
|
parameters: {
|
|
161
159
|
type: 'object',
|
|
162
160
|
properties: {
|
|
163
161
|
thread_id: { type: 'string' },
|
|
164
|
-
|
|
162
|
+
feature_id: { type: 'string' },
|
|
165
163
|
},
|
|
166
|
-
required: ['thread_id', '
|
|
164
|
+
required: ['thread_id', 'feature_id'],
|
|
167
165
|
},
|
|
168
166
|
isConcurrencySafe: () => false,
|
|
169
167
|
isReadOnly: () => false,
|
|
170
168
|
async execute(input) {
|
|
171
|
-
const { thread_id,
|
|
169
|
+
const { thread_id, feature_id } = input || {};
|
|
172
170
|
if (!thread_id) return JSON.stringify({ error: 'thread_id is required' });
|
|
173
|
-
if (!
|
|
171
|
+
if (!feature_id) return JSON.stringify({ error: 'feature_id is required' });
|
|
174
172
|
|
|
175
|
-
const
|
|
176
|
-
if (
|
|
177
|
-
const
|
|
178
|
-
if (!
|
|
173
|
+
const featureStore = getFeatureStore();
|
|
174
|
+
if (featureStore) {
|
|
175
|
+
const feature = featureStore.get(feature_id);
|
|
176
|
+
if (!feature) return JSON.stringify({ error: `Feature not found: ${feature_id}` });
|
|
179
177
|
}
|
|
180
178
|
|
|
181
179
|
try {
|
|
182
180
|
const store = getThreadStore();
|
|
183
|
-
store.
|
|
181
|
+
store.attachFeature(thread_id, feature_id);
|
|
184
182
|
return JSON.stringify({
|
|
185
183
|
success: true,
|
|
186
184
|
threadId: thread_id,
|
|
187
|
-
|
|
188
|
-
message: `Thread ${thread_id} attached to
|
|
185
|
+
featureId: feature_id,
|
|
186
|
+
message: `Thread ${thread_id} attached to feature ${feature_id}`,
|
|
189
187
|
});
|
|
190
188
|
} catch (err) {
|
|
191
189
|
return JSON.stringify({ error: err.message || String(err) });
|
|
@@ -193,91 +191,14 @@ the same thread.`,
|
|
|
193
191
|
},
|
|
194
192
|
});
|
|
195
193
|
|
|
196
|
-
// ─── SpawnTask (
|
|
197
|
-
//
|
|
198
|
-
// task-333b folded SpawnTask into TaskCreate (see task-tools.js). Kept
|
|
199
|
-
// registered for one release as a deprecated alias per PM constraint:
|
|
200
|
-
// LLM calls still resolve, but a one-time console.warn nudges migration.
|
|
201
|
-
// Prefer TaskCreate with `parent_task_id` for all new call sites.
|
|
202
|
-
|
|
203
|
-
const _spawnTaskWarned = { v: false };
|
|
204
|
-
function warnSpawnTaskDeprecated() {
|
|
205
|
-
if (_spawnTaskWarned.v) return;
|
|
206
|
-
_spawnTaskWarned.v = true;
|
|
207
|
-
// eslint-disable-next-line no-console
|
|
208
|
-
console.warn('[deprecated] SpawnTask → TaskCreate. Pass parent_task_id to TaskCreate for subtasks.');
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
export const spawnTask = defineTool({
|
|
212
|
-
name: 'SpawnTask',
|
|
213
|
-
description: `DEPRECATED — use TaskCreate with parent_id instead. Retained as a thin alias for backwards compatibility; delegates to the same task store. When parent_task_id is omitted this behaves like TaskCreate; when provided it creates a subtask under that parent (parent must exist). Removal target: v0.2.0.`,
|
|
214
|
-
parameters: {
|
|
215
|
-
type: 'object',
|
|
216
|
-
properties: {
|
|
217
|
-
title: { type: 'string' },
|
|
218
|
-
description: { type: 'string' },
|
|
219
|
-
priority: {
|
|
220
|
-
type: 'string',
|
|
221
|
-
enum: ['low', 'medium', 'high', 'critical'],
|
|
222
|
-
},
|
|
223
|
-
parent_task_id: {
|
|
224
|
-
type: 'string',
|
|
225
|
-
description: 'Optional parent task id; when present, a subtask is created',
|
|
226
|
-
},
|
|
227
|
-
},
|
|
228
|
-
required: ['title'],
|
|
229
|
-
},
|
|
230
|
-
isConcurrencySafe: () => false,
|
|
231
|
-
isReadOnly: () => false,
|
|
232
|
-
async execute(input) {
|
|
233
|
-
warnSpawnTaskDeprecated();
|
|
234
|
-
const store = getTaskStore();
|
|
235
|
-
if (!store) {
|
|
236
|
-
return JSON.stringify({ error: 'Task store not initialized. Session may still be loading.' });
|
|
237
|
-
}
|
|
238
|
-
const { title, description = '', priority = 'medium', parent_task_id } = input || {};
|
|
239
|
-
if (!title) return JSON.stringify({ error: 'title is required' });
|
|
240
|
-
|
|
241
|
-
if (parent_task_id) {
|
|
242
|
-
const parent = store.get(parent_task_id);
|
|
243
|
-
if (!parent) return JSON.stringify({ error: `Parent task not found: ${parent_task_id}` });
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
const id = `task-${randomUUID().slice(0, 8)}`;
|
|
247
|
-
const now = Date.now();
|
|
248
|
-
store.create({
|
|
249
|
-
id,
|
|
250
|
-
title,
|
|
251
|
-
description,
|
|
252
|
-
priority,
|
|
253
|
-
status: 'pending',
|
|
254
|
-
parentId: parent_task_id || null,
|
|
255
|
-
parentTaskId: parent_task_id || null, // design §5 canonical field; kept in sync with parentId
|
|
256
|
-
createdAt: now,
|
|
257
|
-
updatedAt: now,
|
|
258
|
-
});
|
|
259
|
-
return JSON.stringify({
|
|
260
|
-
success: true,
|
|
261
|
-
task: {
|
|
262
|
-
id,
|
|
263
|
-
title,
|
|
264
|
-
priority,
|
|
265
|
-
status: 'pending',
|
|
266
|
-
parentTaskId: parent_task_id || null,
|
|
267
|
-
},
|
|
268
|
-
message: parent_task_id
|
|
269
|
-
? `Subtask spawned: ${title} (${id}) under ${parent_task_id}`
|
|
270
|
-
: `Task spawned: ${title} (${id})`,
|
|
271
|
-
});
|
|
272
|
-
},
|
|
273
|
-
});
|
|
194
|
+
// ─── SpawnTask removed (task-333b) — use FeatureCreate with parent_feature_id
|
|
274
195
|
|
|
275
196
|
// ─── ReadThreadSummary (cross-reference, design §6 Q5) ──
|
|
276
197
|
|
|
277
198
|
export const readThreadSummary = defineTool({
|
|
278
199
|
name: 'ReadThreadSummary',
|
|
279
200
|
description: `Return a one-shot summary of a thread: id, name, goal, status,
|
|
280
|
-
messageCount, lastMessageAt, parentThreadId,
|
|
201
|
+
messageCount, lastMessageAt, parentThreadId, attachedFeatureId.
|
|
281
202
|
|
|
282
203
|
Use this to cross-reference work on another thread without switching.`,
|
|
283
204
|
parameters: {
|
|
@@ -305,7 +226,7 @@ Use this to cross-reference work on another thread without switching.`,
|
|
|
305
226
|
messageCount: t.messageCount,
|
|
306
227
|
lastMessageAt: t.lastMessageAt,
|
|
307
228
|
parentThreadId: t.parentThreadId,
|
|
308
|
-
|
|
229
|
+
attachedFeatureId: store.attachedFeature(t.id),
|
|
309
230
|
createdAt: t.createdAt,
|
|
310
231
|
updatedAt: t.updatedAt,
|
|
311
232
|
},
|
package/unify/web-bridge.js
CHANGED
|
@@ -30,7 +30,7 @@ import { handleVpSubscribe } from './vp/vp-bridge.js';
|
|
|
30
30
|
import { createVp, updateVp, deleteVp, readVp, VpCrudError } from './vp/vp-crud.js';
|
|
31
31
|
import { scanVpLibrary } from './vp/vp-store.js';
|
|
32
32
|
import { createRouter } from './routing/router.js';
|
|
33
|
-
import {
|
|
33
|
+
import { handleUnifyFeatureMessage as _handleUnifyFeatureMessage } from './feature-message.js';
|
|
34
34
|
import {
|
|
35
35
|
handleUnifyUserMemoryWrite as _handleUnifyUserMemoryWrite,
|
|
36
36
|
handleUnifyUserMemoryRemove as _handleUnifyUserMemoryRemove,
|
|
@@ -278,8 +278,8 @@ export function handleUnifyVpDelete(msg) {
|
|
|
278
278
|
*
|
|
279
279
|
* @param {any} msg
|
|
280
280
|
*/
|
|
281
|
-
export function
|
|
282
|
-
|
|
281
|
+
export function handleUnifyFeatureMessage(msg) {
|
|
282
|
+
_handleUnifyFeatureMessage(msg, sendUnifyEvent);
|
|
283
283
|
}
|
|
284
284
|
|
|
285
285
|
/**
|
|
@@ -639,11 +639,11 @@ function sendThreadListUpdate() {
|
|
|
639
639
|
preview: t.preview || '',
|
|
640
640
|
createdAt: t.createdAt,
|
|
641
641
|
updatedAt: t.updatedAt,
|
|
642
|
-
// task-315: attached
|
|
643
|
-
// messages belonging to a
|
|
644
|
-
// the thread has no attached
|
|
645
|
-
|
|
646
|
-
? (store.
|
|
642
|
+
// task-315: attached featureId (if any) so the UI can aggregate all
|
|
643
|
+
// messages belonging to a feature across multiple threads. null when
|
|
644
|
+
// the thread has no attached feature.
|
|
645
|
+
featureId: (typeof store.attachedFeature === 'function')
|
|
646
|
+
? (store.attachedFeature(t.id) || null)
|
|
647
647
|
: null,
|
|
648
648
|
// `running` — the thread whose id equals the store's currentId is
|
|
649
649
|
// considered the active/running track. The UI uses this for the
|
|
@@ -662,7 +662,7 @@ const THREAD_MUTATING_TOOLS = new Set([
|
|
|
662
662
|
'SpawnThread',
|
|
663
663
|
'SwitchThread',
|
|
664
664
|
'ArchiveThread',
|
|
665
|
-
'
|
|
665
|
+
'AttachThreadToFeature',
|
|
666
666
|
]);
|
|
667
667
|
|
|
668
668
|
/**
|
|
@@ -751,8 +751,8 @@ function sendThreadListSnapshot() {
|
|
|
751
751
|
preview: t.preview || '',
|
|
752
752
|
createdAt: t.createdAt,
|
|
753
753
|
updatedAt: t.updatedAt,
|
|
754
|
-
|
|
755
|
-
? (store.
|
|
754
|
+
featureId: (typeof store.attachedFeature === 'function')
|
|
755
|
+
? (store.attachedFeature(t.id) || null)
|
|
756
756
|
: null,
|
|
757
757
|
running: t.id === store.currentId,
|
|
758
758
|
state: inflight.has(t.id) ? 'running' : 'idle',
|
|
@@ -1809,37 +1809,37 @@ export function handleUnifyModeSwitch(_msg) {
|
|
|
1809
1809
|
}
|
|
1810
1810
|
|
|
1811
1811
|
/**
|
|
1812
|
-
* R6 G2 — VP/
|
|
1812
|
+
* R6 G2 — VP/Feature memory browser query.
|
|
1813
1813
|
*
|
|
1814
1814
|
* Reads from session.memoryShardStore (R6 shard-based memory) and replies
|
|
1815
|
-
* with a time-sorted list of entries scoped to the requested vpId /
|
|
1815
|
+
* with a time-sorted list of entries scoped to the requested vpId / featureId.
|
|
1816
1816
|
* The web UI's MemoryCard / MemoryTraceModal consume the reply.
|
|
1817
1817
|
*
|
|
1818
1818
|
* Request shape:
|
|
1819
1819
|
* { type: 'unify_memory_query',
|
|
1820
|
-
* vpId?: string,
|
|
1820
|
+
* vpId?: string, featureId?: string,
|
|
1821
1821
|
* limit?: number, requestId?: string }
|
|
1822
1822
|
*
|
|
1823
1823
|
* Reply shape:
|
|
1824
1824
|
* { type: 'unify_memory_query_result',
|
|
1825
|
-
* scope: { vpId,
|
|
1825
|
+
* scope: { vpId, featureId },
|
|
1826
1826
|
* entries: Array<thinEntry>, // shape from shard-store mapRecordToThinEntry
|
|
1827
1827
|
* requestId? }
|
|
1828
1828
|
*
|
|
1829
1829
|
* Per D2 the query is scoped — the LLM owns memory recall via the
|
|
1830
1830
|
* memory_query tool; this surface is purely UI browsing (read-only).
|
|
1831
1831
|
*
|
|
1832
|
-
* @param {{ vpId?: string,
|
|
1832
|
+
* @param {{ vpId?: string, featureId?: string, limit?: number, requestId?: string }} msg
|
|
1833
1833
|
*/
|
|
1834
1834
|
export function handleUnifyMemoryQuery(msg = {}) {
|
|
1835
1835
|
const requestId = typeof msg.requestId === 'string' ? msg.requestId : undefined;
|
|
1836
1836
|
const vpId = typeof msg.vpId === 'string' ? msg.vpId : null;
|
|
1837
|
-
const
|
|
1837
|
+
const featureId = typeof msg.featureId === 'string' ? msg.featureId : null;
|
|
1838
1838
|
const limit = Number.isFinite(msg.limit) ? Math.max(1, Math.min(200, msg.limit)) : 50;
|
|
1839
1839
|
|
|
1840
1840
|
const reply = (extra = {}) => sendUnifyEvent({
|
|
1841
1841
|
type: 'unify_memory_query_result',
|
|
1842
|
-
scope: { vpId,
|
|
1842
|
+
scope: { vpId, featureId },
|
|
1843
1843
|
...extra,
|
|
1844
1844
|
...(requestId ? { requestId } : {}),
|
|
1845
1845
|
});
|
|
@@ -1852,7 +1852,7 @@ export function handleUnifyMemoryQuery(msg = {}) {
|
|
|
1852
1852
|
try {
|
|
1853
1853
|
const filter = {};
|
|
1854
1854
|
if (vpId) filter.vp = vpId;
|
|
1855
|
-
if (
|
|
1855
|
+
if (featureId) filter.feature = featureId;
|
|
1856
1856
|
const res = session.memoryShardStore.query(filter);
|
|
1857
1857
|
const list = Array.isArray(res?.results) ? res.results : [];
|
|
1858
1858
|
// Time-sorted desc on updatedAt / createdAt.
|
|
@@ -1912,41 +1912,41 @@ export function handleUnifyMemoryTrace(msg = {}) {
|
|
|
1912
1912
|
}
|
|
1913
1913
|
|
|
1914
1914
|
/**
|
|
1915
|
-
* R6 G1a — Fetch a
|
|
1915
|
+
* R6 G1a — Fetch a feature's summary history (revision chain).
|
|
1916
1916
|
*
|
|
1917
|
-
* Streams the group log filtered by `
|
|
1917
|
+
* Streams the group log filtered by `featureId` and `meta.kind === 'summary'`,
|
|
1918
1918
|
* separates `current` (≤10 most recent non-superseded) from `archived` rows
|
|
1919
1919
|
* per §Δ31.5. Default `includeArchived: false` keeps the wire payload small;
|
|
1920
1920
|
* the UI's "Show archived" button re-issues with the flag set.
|
|
1921
1921
|
*
|
|
1922
1922
|
* Request shape:
|
|
1923
|
-
* { type: 'unify_fetch_summary_history',
|
|
1923
|
+
* { type: 'unify_fetch_summary_history', featureId, includeArchived?: bool }
|
|
1924
1924
|
*
|
|
1925
1925
|
* Reply shape:
|
|
1926
|
-
* { type: 'unify_summary_history',
|
|
1926
|
+
* { type: 'unify_summary_history', featureId, revisions: [...],
|
|
1927
1927
|
* archived: [...]|null, error?: string, requestId? }
|
|
1928
1928
|
*/
|
|
1929
1929
|
export async function handleUnifyFetchSummaryHistory(msg = {}) {
|
|
1930
1930
|
const requestId = typeof msg.requestId === 'string' ? msg.requestId : undefined;
|
|
1931
|
-
const
|
|
1931
|
+
const featureId = typeof msg.featureId === 'string' ? msg.featureId : null;
|
|
1932
1932
|
const includeArchived = !!msg.includeArchived;
|
|
1933
1933
|
|
|
1934
1934
|
const reply = (extra = {}) => sendUnifyEvent({
|
|
1935
1935
|
type: 'unify_summary_history',
|
|
1936
|
-
|
|
1936
|
+
featureId,
|
|
1937
1937
|
...extra,
|
|
1938
1938
|
...(requestId ? { requestId } : {}),
|
|
1939
1939
|
});
|
|
1940
1940
|
|
|
1941
|
-
if (!
|
|
1941
|
+
if (!featureId) { reply({ revisions: [], archived: null, error: 'missing_feature_id' }); return; }
|
|
1942
1942
|
|
|
1943
1943
|
try {
|
|
1944
|
-
const {
|
|
1945
|
-
const
|
|
1946
|
-
const
|
|
1947
|
-
if (!
|
|
1948
|
-
const groupId =
|
|
1949
|
-
if (!groupId) { reply({ revisions: [], archived: null, error: '
|
|
1944
|
+
const { getFeatureStore } = await import('./tools/feature-tools.js');
|
|
1945
|
+
const featureStore = getFeatureStore();
|
|
1946
|
+
const feature = featureStore?.get(featureId);
|
|
1947
|
+
if (!feature) { reply({ revisions: [], archived: null, error: 'feature_not_found' }); return; }
|
|
1948
|
+
const groupId = feature.groupId;
|
|
1949
|
+
if (!groupId) { reply({ revisions: [], archived: null, error: 'feature_has_no_group' }); return; }
|
|
1950
1950
|
|
|
1951
1951
|
const yeaftDir = ctx.CONFIG?.yeaftDir;
|
|
1952
1952
|
if (!yeaftDir) { reply({ revisions: [], archived: null, error: 'no_yeaft_dir' }); return; }
|
|
@@ -1963,7 +1963,7 @@ export async function handleUnifyFetchSummaryHistory(msg = {}) {
|
|
|
1963
1963
|
const groupHandle = openGroup(root, groupId);
|
|
1964
1964
|
const summaries = [];
|
|
1965
1965
|
for (const m of groupHandle.streamMessages()) {
|
|
1966
|
-
if (!m || m.
|
|
1966
|
+
if (!m || m.featureId !== featureId) continue;
|
|
1967
1967
|
const meta = m.meta || {};
|
|
1968
1968
|
if (meta.kind === 'summary' || meta.type === 'summary') summaries.push(m);
|
|
1969
1969
|
}
|
|
@@ -2002,74 +2002,74 @@ export async function handleUnifyFetchSummaryHistory(msg = {}) {
|
|
|
2002
2002
|
}
|
|
2003
2003
|
|
|
2004
2004
|
/**
|
|
2005
|
-
* R6 G1a —
|
|
2005
|
+
* R6 G1a — Feature affiliation CRUD (relate / unrelate / kick_vp / abort_vp).
|
|
2006
2006
|
*
|
|
2007
2007
|
* Single envelope so the UI doesn't fan out four separate WS message types
|
|
2008
|
-
* for housekeeping verbs. Replies with `
|
|
2008
|
+
* for housekeeping verbs. Replies with `unify_feature_crud_result`.
|
|
2009
2009
|
*
|
|
2010
|
-
* - relate {
|
|
2011
|
-
* - unrelate {
|
|
2012
|
-
* - kick_vp {
|
|
2013
|
-
* - abort_vp {
|
|
2010
|
+
* - relate { featureId, relatedFeatureId } — bidirectional Δ27 link
|
|
2011
|
+
* - unrelate { featureId, relatedFeatureId } — drop both directions
|
|
2012
|
+
* - kick_vp { featureId, vpId } — featureStore.removeMember
|
|
2013
|
+
* - abort_vp { featureId, vpId } — abort that VP's in-flight engine inside the feature
|
|
2014
2014
|
*/
|
|
2015
|
-
export async function
|
|
2015
|
+
export async function handleUnifyFeatureCrud(msg = {}) {
|
|
2016
2016
|
const requestId = typeof msg.requestId === 'string' ? msg.requestId : undefined;
|
|
2017
2017
|
const op = typeof msg.op === 'string' ? msg.op : null;
|
|
2018
|
-
const
|
|
2018
|
+
const featureId = typeof msg.featureId === 'string' ? msg.featureId : null;
|
|
2019
2019
|
const vpId = typeof msg.vpId === 'string' ? msg.vpId : null;
|
|
2020
|
-
const
|
|
2020
|
+
const relatedFeatureId = typeof msg.relatedFeatureId === 'string' ? msg.relatedFeatureId : null;
|
|
2021
2021
|
|
|
2022
2022
|
const reply = (extra = {}) => sendUnifyEvent({
|
|
2023
|
-
type: '
|
|
2023
|
+
type: 'unify_feature_crud_result',
|
|
2024
2024
|
op,
|
|
2025
|
-
|
|
2025
|
+
featureId,
|
|
2026
2026
|
...(vpId ? { vpId } : {}),
|
|
2027
2027
|
...extra,
|
|
2028
2028
|
...(requestId ? { requestId } : {}),
|
|
2029
2029
|
});
|
|
2030
2030
|
|
|
2031
2031
|
if (!op) { reply({ ok: false, error: 'missing_op' }); return; }
|
|
2032
|
-
if (!
|
|
2032
|
+
if (!featureId) { reply({ ok: false, error: 'missing_feature_id' }); return; }
|
|
2033
2033
|
|
|
2034
2034
|
try {
|
|
2035
|
-
const {
|
|
2036
|
-
const
|
|
2037
|
-
const
|
|
2038
|
-
if (!
|
|
2035
|
+
const { getFeatureStore } = await import('./tools/feature-tools.js');
|
|
2036
|
+
const featureStore = getFeatureStore();
|
|
2037
|
+
const feature = featureStore?.get(featureId);
|
|
2038
|
+
if (!feature) { reply({ ok: false, error: 'feature_not_found' }); return; }
|
|
2039
2039
|
|
|
2040
2040
|
if (op === 'relate' || op === 'unrelate') {
|
|
2041
|
-
if (!
|
|
2042
|
-
const other =
|
|
2043
|
-
if (!other) { reply({ ok: false, error: '
|
|
2041
|
+
if (!relatedFeatureId) { reply({ ok: false, error: 'missing_related_feature_id' }); return; }
|
|
2042
|
+
const other = featureStore.get(relatedFeatureId);
|
|
2043
|
+
if (!other) { reply({ ok: false, error: 'related_feature_not_found' }); return; }
|
|
2044
2044
|
const apply = (t, otherId, add) => {
|
|
2045
|
-
const cur = Array.isArray(t.
|
|
2045
|
+
const cur = Array.isArray(t.relatedFeatureIds) ? t.relatedFeatureIds.slice() : [];
|
|
2046
2046
|
const idx = cur.indexOf(otherId);
|
|
2047
2047
|
if (add && idx === -1) cur.push(otherId);
|
|
2048
2048
|
if (!add && idx !== -1) cur.splice(idx, 1);
|
|
2049
|
-
|
|
2049
|
+
featureStore.update(t.id, { relatedFeatureIds: cur });
|
|
2050
2050
|
};
|
|
2051
|
-
apply(
|
|
2052
|
-
apply(other,
|
|
2053
|
-
reply({ ok: true,
|
|
2051
|
+
apply(feature, relatedFeatureId, op === 'relate');
|
|
2052
|
+
apply(other, featureId, op === 'relate');
|
|
2053
|
+
reply({ ok: true, relatedFeatureId });
|
|
2054
2054
|
return;
|
|
2055
2055
|
}
|
|
2056
2056
|
|
|
2057
2057
|
if (op === 'kick_vp') {
|
|
2058
2058
|
if (!vpId) { reply({ ok: false, error: 'missing_vp_id' }); return; }
|
|
2059
|
-
|
|
2059
|
+
featureStore.removeMember(featureId, vpId);
|
|
2060
2060
|
reply({ ok: true });
|
|
2061
2061
|
return;
|
|
2062
2062
|
}
|
|
2063
2063
|
|
|
2064
2064
|
if (op === 'abort_vp') {
|
|
2065
2065
|
if (!vpId) { reply({ ok: false, error: 'missing_vp_id' }); return; }
|
|
2066
|
-
// Reuse per-thread abort registry — keyed by (
|
|
2066
|
+
// Reuse per-thread abort registry — keyed by (featureId,vpId) tuple if
|
|
2067
2067
|
// the engine instance is registered there. For v1 we surface success
|
|
2068
2068
|
// and let the engine settle; full per-VP cancellation is owned by
|
|
2069
2069
|
// the engine registry in 334o follow-up.
|
|
2070
2070
|
const reg = session?.engineRegistry;
|
|
2071
|
-
if (reg && typeof reg.
|
|
2072
|
-
reg.
|
|
2071
|
+
if (reg && typeof reg.abortVpInFeature === 'function') {
|
|
2072
|
+
reg.abortVpInFeature(featureId, vpId);
|
|
2073
2073
|
}
|
|
2074
2074
|
reply({ ok: true });
|
|
2075
2075
|
return;
|