@yeaft/webchat-agent 0.1.856 → 0.1.859

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/conversation.js CHANGED
@@ -6,6 +6,7 @@ import { query } from './sdk/index.js';
6
6
  import { loadSessionHistory } from './history.js';
7
7
  import { startClaudeQuery } from './claude.js';
8
8
  import { crewSessions, loadCrewIndex } from './crew.js';
9
+ import { getProvider, DEFAULT_PROVIDER, isValidProvider } from './providers/index.js';
9
10
 
10
11
  // 不支持的斜杠命令(真正需要交互式 CLI 的命令)
11
12
  const UNSUPPORTED_SLASH_COMMANDS = ['/help', '/bug', '/login', '/logout', '/terminal-setup', '/vim', '/config'];
@@ -293,7 +294,8 @@ export async function sendConversationList() {
293
294
  createdAt: state.createdAt,
294
295
  processing: !!state.turnActive,
295
296
  userId: state.userId,
296
- username: state.username
297
+ username: state.username,
298
+ provider: state.providerName || DEFAULT_PROVIDER
297
299
  };
298
300
  list.push(entry);
299
301
  }
@@ -356,33 +358,41 @@ export function sendError(conversationId, message) {
356
358
  export async function createConversation(msg) {
357
359
  const { conversationId, workDir, userId, username, disallowedTools } = msg;
358
360
  const effectiveWorkDir = workDir || ctx.CONFIG.workDir;
361
+ const provider = isValidProvider(msg.provider) ? msg.provider : DEFAULT_PROVIDER;
359
362
 
360
- console.log(`Creating conversation: ${conversationId} in ${effectiveWorkDir} (lazy start)`);
363
+ console.log(`Creating conversation: ${conversationId} in ${effectiveWorkDir} (lazy start, provider=${provider})`);
361
364
  if (username) console.log(` User: ${username} (${userId})`);
362
365
 
363
- // 只创建 conversation 状态,不启动 Claude 进程
364
- // Claude 进程会在用户发送第一条消息时启动 (见 handleUserInput)
365
- ctx.conversations.set(conversationId, {
366
- query: null,
367
- inputStream: null,
368
- workDir: effectiveWorkDir,
369
- claudeSessionId: null,
370
- createdAt: Date.now(),
371
- abortController: null,
372
- tools: [],
373
- slashCommands: [],
374
- model: null,
375
- userId,
376
- username,
377
- disallowedTools: disallowedTools || null, // null = 使用全局默认
378
- usage: {
379
- inputTokens: 0,
380
- outputTokens: 0,
381
- cacheRead: 0,
382
- cacheCreation: 0,
383
- totalCostUsd: 0
384
- }
385
- });
366
+ if (provider === 'claude-code') {
367
+ // Claude path: lazy-init state, real CLI boots on first message.
368
+ ctx.conversations.set(conversationId, {
369
+ query: null,
370
+ inputStream: null,
371
+ workDir: effectiveWorkDir,
372
+ claudeSessionId: null,
373
+ createdAt: Date.now(),
374
+ abortController: null,
375
+ tools: [],
376
+ slashCommands: [],
377
+ model: null,
378
+ userId,
379
+ username,
380
+ providerName: provider,
381
+ disallowedTools: disallowedTools || null,
382
+ usage: { inputTokens: 0, outputTokens: 0, cacheRead: 0, cacheCreation: 0, totalCostUsd: 0 }
383
+ });
384
+ } else {
385
+ // Non-Claude providers own their own state construction.
386
+ const driver = getProvider(provider);
387
+ const state = await driver.start({
388
+ conversationId,
389
+ workDir: effectiveWorkDir,
390
+ resumeSessionId: null,
391
+ userId,
392
+ username,
393
+ });
394
+ state.disallowedTools = disallowedTools || null;
395
+ }
386
396
 
387
397
  ctx.sendToServer({
388
398
  type: 'conversation_created',
@@ -390,6 +400,7 @@ export async function createConversation(msg) {
390
400
  workDir: effectiveWorkDir,
391
401
  userId,
392
402
  username,
403
+ provider,
393
404
  disallowedTools: disallowedTools || null
394
405
  });
395
406
 
@@ -414,13 +425,16 @@ export async function createConversation(msg) {
414
425
 
415
426
  // ★ Prestart Claude CLI in background to eagerly fetch skills/tools/model
416
427
  // Fire-and-forget: failure just degrades to lazy-start behavior
417
- prestartClaude(conversationId, effectiveWorkDir, null);
428
+ if (provider === 'claude-code') {
429
+ prestartClaude(conversationId, effectiveWorkDir, null);
430
+ }
418
431
  }
419
432
 
420
433
  // Resume 历史 conversation (延迟启动 Claude,等待用户发送第一条消息)
421
434
  export async function resumeConversation(msg) {
422
435
  const { conversationId, claudeSessionId, workDir, userId, username, disallowedTools } = msg;
423
436
  const effectiveWorkDir = workDir || ctx.CONFIG.workDir;
437
+ const provider = isValidProvider(msg.provider) ? msg.provider : DEFAULT_PROVIDER;
424
438
 
425
439
  console.log(`[Resume] conversationId: ${conversationId}`);
426
440
  console.log(`[Resume] claudeSessionId: ${claudeSessionId}`);
@@ -458,6 +472,7 @@ export async function resumeConversation(msg) {
458
472
  model: null,
459
473
  userId,
460
474
  username,
475
+ providerName: provider,
461
476
  disallowedTools: disallowedTools || null, // null = 使用全局默认
462
477
  usage: {
463
478
  inputTokens: 0,
@@ -468,6 +483,19 @@ export async function resumeConversation(msg) {
468
483
  }
469
484
  });
470
485
 
486
+ // Non-Claude providers: re-init state via driver so sessionId/providerName are set correctly.
487
+ if (provider !== 'claude-code') {
488
+ const driver = getProvider(provider);
489
+ const state = await driver.start({
490
+ conversationId,
491
+ workDir: effectiveWorkDir,
492
+ resumeSessionId: claudeSessionId || null,
493
+ userId,
494
+ username,
495
+ });
496
+ state.disallowedTools = disallowedTools || null;
497
+ }
498
+
471
499
  ctx.sendToServer({
472
500
  type: 'conversation_resumed',
473
501
  conversationId,
@@ -475,7 +503,8 @@ export async function resumeConversation(msg) {
475
503
  workDir: effectiveWorkDir,
476
504
  historyMessages,
477
505
  userId,
478
- username
506
+ username,
507
+ provider
479
508
  });
480
509
 
481
510
  // 立即发送 agent 级别的 MCP servers 列表
@@ -498,7 +527,7 @@ export async function resumeConversation(msg) {
498
527
  // ★ Prestart Claude CLI in background to eagerly fetch skills/tools/model
499
528
  // Skip if conversation already has an active query (shouldn't happen, but safety check)
500
529
  const resumeState = ctx.conversations.get(conversationId);
501
- if (!resumeState?.query) {
530
+ if (provider === 'claude-code' && !resumeState?.query) {
502
531
  prestartClaude(conversationId, effectiveWorkDir, claudeSessionId);
503
532
  }
504
533
  }
@@ -704,6 +733,41 @@ export async function handleUserInput(msg) {
704
733
 
705
734
  let state = ctx.conversations.get(conversationId);
706
735
 
736
+ // ★ Non-Claude providers: dispatch to driver and return
737
+ const providerName = state?.providerName || DEFAULT_PROVIDER;
738
+ if (providerName !== 'claude-code') {
739
+ const driver = getProvider(providerName);
740
+ const effectiveWorkDir = workDir || state?.workDir || ctx.CONFIG.workDir;
741
+ if (!state) {
742
+ state = await driver.start({
743
+ conversationId,
744
+ workDir: effectiveWorkDir,
745
+ resumeSessionId: claudeSessionId || null,
746
+ userId: msg.userId,
747
+ username: msg.username,
748
+ });
749
+ }
750
+ if (workDir) state.workDir = workDir;
751
+ sendOutput(conversationId, { type: 'user', message: { role: 'user', content: prompt } });
752
+ state.turnActive = true;
753
+ sendConversationList();
754
+ try {
755
+ await driver.sendInput(state, prompt, { conversationId, raw: msg });
756
+ } catch (err) {
757
+ sendOutput(conversationId, {
758
+ type: 'result',
759
+ subtype: 'error',
760
+ session_id: state.sessionId || null,
761
+ is_error: true,
762
+ error: `${providerName} error: ${err?.message || err}`,
763
+ });
764
+ } finally {
765
+ state.turnActive = false;
766
+ sendConversationList();
767
+ }
768
+ return;
769
+ }
770
+
707
771
  // 如果没有活跃的查询,启动新的
708
772
  if (!state || !state.query || !state.inputStream) {
709
773
  const resumeSessionId = claudeSessionId || state?.claudeSessionId || null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yeaft/webchat-agent",
3
- "version": "0.1.856",
3
+ "version": "0.1.859",
4
4
  "description": "Remote agent for Yeaft WebChat — connects worker machines to the central server",
5
5
  "main": "index.js",
6
6
  "type": "module",
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Chat provider abstraction.
3
+ *
4
+ * Wire-protocol note: `claude_output` is now a PROTOCOL name, not a
5
+ * vendor name. Every driver MUST emit events on
6
+ * `ctx.sendToServer({ type: 'claude_output', conversationId, data })`,
7
+ * where `data` follows the Claude stream-json envelope:
8
+ * - { type: 'assistant', message: { role, content: [...] } }
9
+ * - { type: 'user', message: { role, content: [...] } }
10
+ * - { type: 'result', subtype, session_id, is_error, ... }
11
+ * - { type: 'system', subtype, ... }
12
+ *
13
+ * Non-Claude drivers (e.g. Copilot) MUST translate their native event
14
+ * streams into this envelope so the existing renderer needs no changes.
15
+ *
16
+ * @typedef {Object} ChatProvider
17
+ * @property {string} name
18
+ * @property {(opts: StartOpts) => Promise<Object>} start
19
+ * @property {(state: Object, prompt: string, opts?: Object) => Promise<void>} sendInput
20
+ * @property {(state: Object) => void} abort
21
+ *
22
+ * @typedef {Object} StartOpts
23
+ * @property {string} conversationId
24
+ * @property {string} workDir
25
+ * @property {string|null} [resumeSessionId]
26
+ * @property {string} [userId]
27
+ * @property {string} [username]
28
+ */
29
+
30
+ export const PROVIDER_NAMES = Object.freeze(['claude-code', 'copilot']);
31
+ export const DEFAULT_PROVIDER = 'claude-code';
32
+ export function isValidProvider(name) {
33
+ return typeof name === 'string' && PROVIDER_NAMES.includes(name);
34
+ }
@@ -0,0 +1,34 @@
1
+ import { startClaudeQuery } from '../claude.js';
2
+
3
+ export const name = 'claude-code';
4
+
5
+ /**
6
+ * Start (or resume) a Claude Code CLI session.
7
+ * Returns the same state object that startClaudeQuery stores in ctx.conversations.
8
+ */
9
+ export async function start(opts) {
10
+ const state = await startClaudeQuery(
11
+ opts.conversationId,
12
+ opts.workDir,
13
+ opts.resumeSessionId || null
14
+ );
15
+ state.providerName = name;
16
+ return state;
17
+ }
18
+
19
+ /**
20
+ * Claude CLI handles input via the persistent stdin Stream that
21
+ * conversation.js manages directly, so this driver's sendInput is a no-op.
22
+ * conversation.js's existing branch keeps owning the Claude path.
23
+ */
24
+ export async function sendInput(_state, _prompt, _opts) {
25
+ /* handled inline by conversation.js for the Claude branch */
26
+ }
27
+
28
+ export function abort(state) {
29
+ if (state?.abortController) {
30
+ try { state.abortController.abort(); } catch { /* noop */ }
31
+ }
32
+ }
33
+
34
+ export default { name, start, sendInput, abort };
@@ -0,0 +1,278 @@
1
+ import { spawn } from 'child_process';
2
+ import { randomUUID } from 'crypto';
3
+ import ctx from '../context.js';
4
+
5
+ export const name = 'copilot';
6
+
7
+ const COPILOT_BIN = process.env.COPILOT_BIN || 'copilot';
8
+ // Opt-in only: --allow-all-tools is a destructive footgun by default in a
9
+ // multi-tenant agent. Set COPILOT_YOLO=1 (and only if you know what you're
10
+ // doing) to skip Copilot's tool prompts.
11
+ const YOLO = process.env.COPILOT_YOLO === '1';
12
+
13
+ /**
14
+ * Start (or resume) a Copilot session.
15
+ * Copilot's `-p` mode is one-shot per turn, so "start" just prepares state.
16
+ * Each sendInput() spawns one `copilot -p ...` child with the same
17
+ * --session-id for continuity.
18
+ */
19
+ export async function start(opts) {
20
+ const conversationId = opts.conversationId;
21
+ // Tear down any prior entry so we don't leak children.
22
+ const prior = ctx.conversations.get(conversationId);
23
+ if (prior?.copilotChild) {
24
+ try { prior.copilotChild.kill('SIGTERM'); } catch { /* noop */ }
25
+ }
26
+
27
+ const sessionId = opts.resumeSessionId || randomUUID();
28
+ const state = {
29
+ providerName: name,
30
+ conversationId: opts.conversationId,
31
+ query: null,
32
+ inputStream: null,
33
+ workDir: opts.workDir,
34
+ claudeSessionId: sessionId,
35
+ sessionId,
36
+ createdAt: prior?.createdAt || Date.now(),
37
+ abortController: null,
38
+ tools: [],
39
+ slashCommands: [],
40
+ model: 'copilot',
41
+ userId: opts.userId,
42
+ username: opts.username,
43
+ disallowedTools: prior?.disallowedTools || null,
44
+ copilotChild: null,
45
+ usage: { inputTokens: 0, outputTokens: 0, cacheRead: 0, cacheCreation: 0, totalCostUsd: 0 },
46
+ };
47
+ ctx.conversations.set(conversationId, state);
48
+ return state;
49
+ }
50
+
51
+ export async function sendInput(state, prompt, opts = {}) {
52
+ const conversationId = opts.conversationId || state.conversationId;
53
+ if (!conversationId) throw new Error('copilot: conversationId required');
54
+ if (!state.sessionId) state.sessionId = randomUUID();
55
+
56
+ // Abort any in-flight turn.
57
+ if (state.copilotChild) {
58
+ try { state.copilotChild.kill('SIGTERM'); } catch { /* noop */ }
59
+ state.copilotChild = null;
60
+ }
61
+ const abortController = new AbortController();
62
+ state.abortController = abortController;
63
+ state.turnActive = true;
64
+ state.turnResultReceived = false;
65
+
66
+ const args = ['-p', prompt, '--output-format', 'json', '-C', state.workDir, '--session-id', state.sessionId];
67
+ if (YOLO) args.push('--allow-all-tools');
68
+
69
+ let child;
70
+ try {
71
+ child = spawn(COPILOT_BIN, args, {
72
+ cwd: state.workDir,
73
+ env: process.env,
74
+ stdio: ['ignore', 'pipe', 'pipe'],
75
+ });
76
+ } catch (err) {
77
+ sendOutput(conversationId, {
78
+ type: 'result',
79
+ subtype: 'error',
80
+ session_id: state.sessionId,
81
+ is_error: true,
82
+ error: `copilot spawn failed: ${err?.message || err}`,
83
+ });
84
+ state.turnActive = false;
85
+ ctx.sendToServer({ type: 'turn_completed', conversationId, claudeSessionId: state.sessionId, workDir: state.workDir });
86
+ return;
87
+ }
88
+ state.copilotChild = child;
89
+
90
+ // Pre-register error handler so async ENOENT from spawn is never unhandled.
91
+ child.on('error', (err) => {
92
+ sendOutput(conversationId, {
93
+ type: 'result',
94
+ subtype: 'error',
95
+ session_id: state.sessionId,
96
+ is_error: true,
97
+ error: `copilot process error: ${err?.message || err}`,
98
+ });
99
+ });
100
+
101
+ let killTimer = null;
102
+ abortController.signal.addEventListener('abort', () => {
103
+ try { child.kill('SIGTERM'); } catch { /* noop */ }
104
+ // Escalate to SIGKILL if the child ignores SIGTERM, so the awaited
105
+ // close promise resolves and the next turn isn't blocked forever.
106
+ killTimer = setTimeout(() => {
107
+ try { child.kill('SIGKILL'); } catch { /* noop */ }
108
+ }, 5000);
109
+ });
110
+
111
+ let stderrBuf = '';
112
+ const STDERR_CAP = 64 * 1024;
113
+ let sawResult = false;
114
+
115
+ const parser = createNdjsonParser((evt) => {
116
+ const envelopes = translateCopilotEvent(evt, state);
117
+ for (const e of envelopes) {
118
+ sendOutput(conversationId, e);
119
+ if (e?.type === 'result') sawResult = true;
120
+ }
121
+ });
122
+
123
+ child.stdout.on('data', (chunk) => parser.push(chunk));
124
+ child.stderr.on('data', (chunk) => {
125
+ if (stderrBuf.length < STDERR_CAP) {
126
+ stderrBuf += chunk.toString('utf8').slice(0, STDERR_CAP - stderrBuf.length);
127
+ }
128
+ });
129
+
130
+ await new Promise((resolve) => {
131
+ child.on('close', (code) => {
132
+ if (killTimer) clearTimeout(killTimer);
133
+ parser.flush();
134
+ if (!sawResult) {
135
+ const ok = code === 0;
136
+ sendOutput(conversationId, {
137
+ type: 'result',
138
+ subtype: ok ? 'success' : 'error',
139
+ session_id: state.sessionId,
140
+ is_error: !ok,
141
+ error: ok ? undefined : (stderrBuf.trim().slice(0, 2000) || `copilot exited with code ${code}`),
142
+ });
143
+ }
144
+ state.copilotChild = null;
145
+ state.turnActive = false;
146
+ ctx.sendToServer({
147
+ type: 'turn_completed',
148
+ conversationId,
149
+ claudeSessionId: state.sessionId,
150
+ workDir: state.workDir,
151
+ });
152
+ resolve();
153
+ });
154
+ });
155
+ }
156
+
157
+ export function abort(state) {
158
+ if (state?.abortController) {
159
+ try { state.abortController.abort(); } catch { /* noop */ }
160
+ }
161
+ if (state?.copilotChild) {
162
+ try { state.copilotChild.kill('SIGTERM'); } catch { /* noop */ }
163
+ }
164
+ }
165
+
166
+ // ---------- internals ----------
167
+
168
+ function sendOutput(conversationId, data) {
169
+ ctx.sendToServer({ type: 'claude_output', conversationId, data });
170
+ }
171
+
172
+ export function createNdjsonParser(onEvent) {
173
+ let buf = '';
174
+ return {
175
+ push(chunk) {
176
+ buf += chunk.toString('utf8');
177
+ let idx;
178
+ while ((idx = buf.indexOf('\n')) >= 0) {
179
+ const line = buf.slice(0, idx).trim();
180
+ buf = buf.slice(idx + 1);
181
+ if (!line) continue;
182
+ let evt;
183
+ try { evt = JSON.parse(line); }
184
+ catch (err) {
185
+ if (ctx?.CONFIG?.debug) console.warn('[copilot] dropping unparsable line:', line.slice(0, 200));
186
+ continue;
187
+ }
188
+ try { onEvent(evt); }
189
+ catch (err) { console.warn('[copilot] event handler error:', err?.message || err); }
190
+ }
191
+ },
192
+ flush() {
193
+ const line = buf.trim();
194
+ buf = '';
195
+ if (!line) return;
196
+ try {
197
+ const evt = JSON.parse(line);
198
+ onEvent(evt);
199
+ } catch { /* discard trailing junk */ }
200
+ },
201
+ };
202
+ }
203
+
204
+ /**
205
+ * Map a Copilot NDJSON event to zero-or-more claude_output envelopes.
206
+ * Defensive: unknown shapes are logged and dropped.
207
+ *
208
+ * Recognized loose schemas (Copilot CLI JSON output is not yet stable, so
209
+ * we accept several aliases and forward only what we understand):
210
+ * - text: { type: 'text'|'text_delta'|'assistant_text', text|delta }
211
+ * - message: { type: 'message', role, content }
212
+ * - tool_call: { type: 'tool_call'|'tool_use', id, name|tool, input|arguments }
213
+ * - tool_result: { type: 'tool_result', tool_use_id|id, content|output }
214
+ * - done: { type: 'result'|'done'|'complete', session_id?, error? }
215
+ * - error: { type: 'error', message|error }
216
+ */
217
+ export function translateCopilotEvent(evt, state) {
218
+ if (!evt || typeof evt !== 'object') return [];
219
+ const t = evt.type;
220
+
221
+ if (t === 'text' || t === 'text_delta' || t === 'assistant_text') {
222
+ const text = typeof evt.text === 'string' ? evt.text : (typeof evt.delta === 'string' ? evt.delta : '');
223
+ if (!text) return [];
224
+ return [{ type: 'assistant', message: { role: 'assistant', content: [{ type: 'text', text }] } }];
225
+ }
226
+
227
+ if (t === 'message' && evt.role === 'assistant') {
228
+ const content = normalizeContent(evt.content);
229
+ return [{ type: 'assistant', message: { role: 'assistant', content } }];
230
+ }
231
+
232
+ if (t === 'tool_call' || t === 'tool_use') {
233
+ const id = evt.id || evt.call_id || randomUUID();
234
+ const toolName = evt.name || evt.tool || 'unknown';
235
+ const input = evt.input ?? evt.arguments ?? {};
236
+ return [{ type: 'assistant', message: { role: 'assistant', content: [{ type: 'tool_use', id, name: toolName, input }] } }];
237
+ }
238
+
239
+ if (t === 'tool_result') {
240
+ const tool_use_id = evt.tool_use_id || evt.id || 'unknown';
241
+ const content = typeof evt.content === 'string'
242
+ ? evt.content
243
+ : (typeof evt.output === 'string' ? evt.output : JSON.stringify(evt.content ?? evt.output ?? ''));
244
+ return [{ type: 'user', message: { role: 'user', content: [{ type: 'tool_result', tool_use_id, content }] } }];
245
+ }
246
+
247
+ if (t === 'result' || t === 'done' || t === 'complete') {
248
+ const isErr = !!evt.error || evt.is_error === true;
249
+ return [{
250
+ type: 'result',
251
+ subtype: isErr ? 'error' : 'success',
252
+ session_id: evt.session_id || state?.sessionId || null,
253
+ is_error: isErr,
254
+ error: isErr ? (evt.error || evt.message || 'copilot error') : undefined,
255
+ }];
256
+ }
257
+
258
+ if (t === 'error') {
259
+ return [{
260
+ type: 'result',
261
+ subtype: 'error',
262
+ session_id: state?.sessionId || null,
263
+ is_error: true,
264
+ error: evt.message || evt.error || 'copilot error',
265
+ }];
266
+ }
267
+
268
+ if (ctx?.CONFIG?.debug) console.warn('[copilot] dropping unknown event type:', t);
269
+ return [];
270
+ }
271
+
272
+ function normalizeContent(content) {
273
+ if (typeof content === 'string') return [{ type: 'text', text: content }];
274
+ if (Array.isArray(content)) return content;
275
+ return [{ type: 'text', text: String(content ?? '') }];
276
+ }
277
+
278
+ export default { name, start, sendInput, abort };
@@ -0,0 +1,19 @@
1
+ import { PROVIDER_NAMES, DEFAULT_PROVIDER, isValidProvider } from './base.js';
2
+ import * as claudeCode from './claude-code.js';
3
+ import * as copilot from './copilot.js';
4
+
5
+ const REGISTRY = Object.freeze({
6
+ 'claude-code': claudeCode,
7
+ 'copilot': copilot,
8
+ });
9
+
10
+ export function getProvider(nameOrUndef) {
11
+ const key = nameOrUndef || DEFAULT_PROVIDER;
12
+ const driver = REGISTRY[key];
13
+ if (!driver) {
14
+ throw new Error(`Unknown chat provider: ${nameOrUndef} (known: ${PROVIDER_NAMES.join(', ')})`);
15
+ }
16
+ return driver;
17
+ }
18
+
19
+ export { PROVIDER_NAMES, DEFAULT_PROVIDER, isValidProvider };
@@ -28,7 +28,6 @@
28
28
  */
29
29
 
30
30
  import { groupTurns, pickCoolingGroups, indicesFromGroups } from './turn-group.js';
31
- import { writeSummary } from '../memory/store-v2.js';
32
31
 
33
32
  /**
34
33
  * @typedef {{
@@ -57,7 +56,7 @@ import { writeSummary } from '../memory/store-v2.js';
57
56
  * nextMessages: object[],
58
57
  * }>}
59
58
  */
60
- export async function runCompact({ messages, keepHot = 10, taskId = null, root, hooks }) {
59
+ export async function runCompact({ messages, keepHot = 10, hooks }) {
61
60
  if (!Array.isArray(messages)) {
62
61
  throw new Error('runCompact: messages array required');
63
62
  }
@@ -104,17 +103,12 @@ export async function runCompact({ messages, keepHot = 10, taskId = null, root,
104
103
  archiveResults.push({ ...g, turnId: r?.turnId });
105
104
  }
106
105
 
107
- // Track 2 — refresh task summary if applicable.
108
- let taskSummaryRefreshed = false;
109
- if (taskId && root && typeof hooks.refreshTaskSummary === 'function') {
110
- const prior = typeof hooks.readPriorTaskSummary === 'function'
111
- ? await hooks.readPriorTaskSummary() : '';
112
- const next = await hooks.refreshTaskSummary(coolingMessages, prior);
113
- if (typeof next === 'string' && next.trim()) {
114
- await writeSummary({ kind: 'feature', id: taskId }, next, { root });
115
- taskSummaryRefreshed = true;
116
- }
117
- }
106
+ // Track 2 — task summary refresh: removed. The legacy `feature/<id>` root
107
+ // scope was dropped along with the Feature system (2026-05-13); under the
108
+ // group-isolated layout feature summaries would live at
109
+ // `group/<g>/feature/<id>/` and are written by dream, not by post-turn
110
+ // compact. Engine no longer passes `taskId`/`root` to this orchestrator.
111
+ const taskSummaryRefreshed = false;
118
112
 
119
113
  // Track 3 — memory extraction.
120
114
  let extractedCount = 0;