@yeaft/webchat-agent 0.1.569 → 0.1.572
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/connection/message-router.js +6 -1
- package/package.json +1 -1
- package/unify/conversation/persist.js +22 -1
- package/unify/stop-hooks.js +43 -9
- package/unify/vp/vp-bridge.js +10 -0
- package/unify/web-bridge.js +133 -25
|
@@ -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 } from '../unify/config-api.js';
|
|
39
|
-
import { handleUnifyChat, handleUnifyGroupChat, handleUnifyModeSwitch, handleUnifyModelSwitch, resetUnifySession, handleUnifyLoadHistory, handleUnifyMergeThread, handleUnifyForkThread, handleUnifyAbortThread, handleUnifyAbortAll, handleUnifyVpSubscribe, handleUnifyVpCreate, handleUnifyVpUpdate, handleUnifyVpDelete, handleUnifyVpRead, handleUnifyTaskMessage, handleUnifyUserMemoryWrite, handleUnifyUserMemoryRemove, handleUnifyListGroups, handleUnifyCreateGroup, handleUnifyRenameGroup, handleUnifyArchiveGroup, handleUnifyAddMember, handleUnifyRemoveMember, handleUnifySetDefaultVp, handleUnifyDreamTrigger } from '../unify/web-bridge.js';
|
|
39
|
+
import { handleUnifyChat, handleUnifyGroupChat, handleUnifyModeSwitch, handleUnifyModelSwitch, resetUnifySession, handleUnifyLoadHistory, handleUnifyMergeThread, handleUnifyForkThread, handleUnifyAbortThread, handleUnifyAbortAll, handleUnifyVpSubscribe, handleUnifyVpCreate, handleUnifyVpUpdate, handleUnifyVpDelete, handleUnifyVpRead, handleUnifyTaskMessage, handleUnifyUserMemoryWrite, handleUnifyUserMemoryRemove, handleUnifyMemoryScopeList, handleUnifyListGroups, handleUnifyCreateGroup, handleUnifyRenameGroup, handleUnifyArchiveGroup, handleUnifyAddMember, handleUnifyRemoveMember, handleUnifySetDefaultVp, handleUnifyDreamTrigger } from '../unify/web-bridge.js';
|
|
40
40
|
|
|
41
41
|
export async function handleMessage(msg) {
|
|
42
42
|
switch (msg.type) {
|
|
@@ -440,6 +440,11 @@ export async function handleMessage(msg) {
|
|
|
440
440
|
case 'unify_user_memory_remove':
|
|
441
441
|
handleUnifyUserMemoryRemove(msg);
|
|
442
442
|
break;
|
|
443
|
+
// task-fix: list MemoryStore entries (scope-tree folder view) for the
|
|
444
|
+
// Unify "User Memory" page. Replies with `memory_scope_snapshot`.
|
|
445
|
+
case 'unify_memory_scope_list':
|
|
446
|
+
handleUnifyMemoryScopeList(msg);
|
|
447
|
+
break;
|
|
443
448
|
|
|
444
449
|
// task-334m: Group CRUD + D1 seed wiring (§Δ10 334m + R6 §Δ31.2).
|
|
445
450
|
// All handlers reply via `group_crud_result`; mutating ops additionally
|
package/package.json
CHANGED
|
@@ -71,11 +71,24 @@ function serializeMessage(msg) {
|
|
|
71
71
|
fm.push(`tokens_est: ${tokensEst}`);
|
|
72
72
|
|
|
73
73
|
// Tool calls as YAML array (simplified)
|
|
74
|
+
// task-fix: persist `input` as base64-encoded JSON so multi-line tool
|
|
75
|
+
// arguments round-trip safely (YAML string escaping is brittle for
|
|
76
|
+
// JSON blobs with newlines / quotes). Paired with the parser below.
|
|
74
77
|
if (msg.toolCalls && msg.toolCalls.length > 0) {
|
|
75
78
|
fm.push(`toolCalls:`);
|
|
76
79
|
for (const tc of msg.toolCalls) {
|
|
77
80
|
fm.push(` - id: ${tc.id}`);
|
|
78
81
|
fm.push(` name: ${tc.name}`);
|
|
82
|
+
if (tc.input !== undefined) {
|
|
83
|
+
try {
|
|
84
|
+
const b64 = Buffer.from(JSON.stringify(tc.input)).toString('base64');
|
|
85
|
+
fm.push(` inputB64: ${b64}`);
|
|
86
|
+
} catch {
|
|
87
|
+
// best-effort: if input isn't JSON-serializable, skip it;
|
|
88
|
+
// restoring a tool_call without input is still better than
|
|
89
|
+
// dropping the whole record.
|
|
90
|
+
}
|
|
91
|
+
}
|
|
79
92
|
}
|
|
80
93
|
}
|
|
81
94
|
|
|
@@ -138,13 +151,21 @@ export function parseMessage(raw) {
|
|
|
138
151
|
for (const entry of entries) {
|
|
139
152
|
const tc = {};
|
|
140
153
|
for (const line of entry.split('\n')) {
|
|
141
|
-
|
|
154
|
+
// task-fix: the split regex only strips `\n - ` between
|
|
155
|
+
// entries, leaving a leading `- ` on the first line of the
|
|
156
|
+
// first entry. Strip it here so `- id: xxx` parses as `id`.
|
|
157
|
+
const trimmed = line.trim().replace(/^-\s+/, '');
|
|
142
158
|
const ci = trimmed.indexOf(':');
|
|
143
159
|
if (ci === -1) continue;
|
|
144
160
|
const k = trimmed.slice(0, ci).trim();
|
|
145
161
|
const v = trimmed.slice(ci + 1).trim();
|
|
146
162
|
if (k === 'id') tc.id = v;
|
|
147
163
|
if (k === 'name') tc.name = v;
|
|
164
|
+
if (k === 'inputB64') {
|
|
165
|
+
try {
|
|
166
|
+
tc.input = JSON.parse(Buffer.from(v, 'base64').toString('utf8'));
|
|
167
|
+
} catch { /* best-effort: leave input undefined */ }
|
|
168
|
+
}
|
|
148
169
|
}
|
|
149
170
|
if (tc.id && tc.name) toolCalls.push(tc);
|
|
150
171
|
}
|
package/unify/stop-hooks.js
CHANGED
|
@@ -65,19 +65,53 @@ export async function runStopHooks(context) {
|
|
|
65
65
|
}
|
|
66
66
|
|
|
67
67
|
// 1. Persist latest messages
|
|
68
|
+
//
|
|
69
|
+
// task-fix: we must persist the complete new turn — including the
|
|
70
|
+
// assistant's `toolCalls` and each paired `role:'tool'` result —
|
|
71
|
+
// otherwise restoring history on session reload drops the pairing
|
|
72
|
+
// and causes "No tool output found for function call" 400s on the
|
|
73
|
+
// next chat-completions request. We walk back from the end of
|
|
74
|
+
// `messages` to find the first `role:'user'` that marks the start
|
|
75
|
+
// of the current turn, then persist everything from there forward.
|
|
68
76
|
try {
|
|
69
77
|
if (conversationStore && messages.length > 0) {
|
|
70
|
-
|
|
78
|
+
// Find the start of the latest turn — the last `role:'user'`
|
|
79
|
+
// message in the array. Everything from that index onward is
|
|
80
|
+
// new this turn (user + assistant[+toolCalls] + tool results).
|
|
81
|
+
let turnStart = messages.length - 1;
|
|
82
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
83
|
+
if (messages[i] && messages[i].role === 'user') {
|
|
84
|
+
turnStart = i;
|
|
85
|
+
break;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
const recentMessages = messages.slice(turnStart);
|
|
71
89
|
for (const msg of recentMessages) {
|
|
72
|
-
if (msg
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
90
|
+
if (!msg || !msg.role) continue;
|
|
91
|
+
// Allow empty assistant content when toolCalls are present;
|
|
92
|
+
// tool messages have content by construction.
|
|
93
|
+
const hasContent =
|
|
94
|
+
(typeof msg.content === 'string' && msg.content.length > 0) ||
|
|
95
|
+
(msg.content && typeof msg.content !== 'string') ||
|
|
96
|
+
(Array.isArray(msg.toolCalls) && msg.toolCalls.length > 0) ||
|
|
97
|
+
msg.role === 'tool';
|
|
98
|
+
if (!hasContent) continue;
|
|
99
|
+
|
|
100
|
+
const record = {
|
|
101
|
+
role: msg.role,
|
|
102
|
+
content: typeof msg.content === 'string'
|
|
103
|
+
? msg.content
|
|
104
|
+
: JSON.stringify(msg.content ?? ''),
|
|
105
|
+
mode,
|
|
106
|
+
model: persistModel,
|
|
107
|
+
};
|
|
108
|
+
if (msg.toolCallId) record.toolCallId = msg.toolCallId;
|
|
109
|
+
if (Array.isArray(msg.toolCalls) && msg.toolCalls.length > 0) {
|
|
110
|
+
record.toolCalls = msg.toolCalls;
|
|
80
111
|
}
|
|
112
|
+
if (msg.isError) record.isError = true;
|
|
113
|
+
conversationStore.append(record);
|
|
114
|
+
result.messagesPersisted++;
|
|
81
115
|
}
|
|
82
116
|
}
|
|
83
117
|
} catch (err) {
|
package/unify/vp/vp-bridge.js
CHANGED
|
@@ -147,6 +147,16 @@ function _fanout(evt) {
|
|
|
147
147
|
function ensureLoader(registry = defaultRegistry) {
|
|
148
148
|
if (_loaderStarted) return { loader: _loader, fresh: false };
|
|
149
149
|
_loaderStarted = true;
|
|
150
|
+
// For NON-default registries (unit tests seeding VPs manually) we MUST NOT
|
|
151
|
+
// start VpLoader — its .start() scans DEFAULT_VP_LIB_DIR and push-imports
|
|
152
|
+
// every on-disk VP into the test registry, overwriting/augmenting the
|
|
153
|
+
// fixture. Tests don't need hot-reload anyway; `_broadcastChangeForTest`
|
|
154
|
+
// drives the diff path directly.
|
|
155
|
+
if (registry !== defaultRegistry) {
|
|
156
|
+
_loader = null;
|
|
157
|
+
captureState(registry);
|
|
158
|
+
return { loader: null, fresh: true };
|
|
159
|
+
}
|
|
150
160
|
try {
|
|
151
161
|
_loader = new VpLoader({
|
|
152
162
|
registry,
|
package/unify/web-bridge.js
CHANGED
|
@@ -86,6 +86,39 @@ function getThreadMessages(threadId) {
|
|
|
86
86
|
return arr;
|
|
87
87
|
}
|
|
88
88
|
|
|
89
|
+
/**
|
|
90
|
+
* Restore per-thread message history from persisted conversation store.
|
|
91
|
+
*
|
|
92
|
+
* task-fix: accept `role:'tool'` messages AND preserve `toolCalls` /
|
|
93
|
+
* `toolCallId` fields. Without this, chat-completions serialization
|
|
94
|
+
* emits `tool_calls` without paired `role:'tool'` results, causing
|
|
95
|
+
* "No tool output found for function call" 400s after the first tool
|
|
96
|
+
* use across any restart / session-ready / model-switch event.
|
|
97
|
+
*
|
|
98
|
+
* @param {Array<object>} recent — output of conversationStore.loadRecent()
|
|
99
|
+
*/
|
|
100
|
+
function restoreThreadHistoryFromRecent(recent) {
|
|
101
|
+
messagesByThread.clear();
|
|
102
|
+
for (const m of recent) {
|
|
103
|
+
// Keep user, assistant, AND tool messages. Tool messages are required
|
|
104
|
+
// for the chat-completions `tool_call_id` pairing.
|
|
105
|
+
if (m.role !== 'user' && m.role !== 'assistant' && m.role !== 'tool') continue;
|
|
106
|
+
const tid = m.threadId || MAIN_THREAD_ID;
|
|
107
|
+
const bucket = getThreadMessages(tid);
|
|
108
|
+
const entry = { role: m.role, content: m.content };
|
|
109
|
+
if (m.toolCallId) entry.toolCallId = m.toolCallId;
|
|
110
|
+
if (Array.isArray(m.toolCalls) && m.toolCalls.length > 0) {
|
|
111
|
+
entry.toolCalls = m.toolCalls.map(tc => ({
|
|
112
|
+
id: tc.id,
|
|
113
|
+
name: tc.name,
|
|
114
|
+
input: tc.input,
|
|
115
|
+
}));
|
|
116
|
+
}
|
|
117
|
+
if (m.isError) entry.isError = true;
|
|
118
|
+
bucket.push(entry);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
89
122
|
/** Whether we've already sent a permission warning to the UI */
|
|
90
123
|
let _permissionDiagnosticSent = false;
|
|
91
124
|
|
|
@@ -257,6 +290,41 @@ export function handleUnifyUserMemoryRemove(msg) {
|
|
|
257
290
|
_handleUnifyUserMemoryRemove(msg, sendUnifyEvent);
|
|
258
291
|
}
|
|
259
292
|
|
|
293
|
+
/**
|
|
294
|
+
* task-fix: list MemoryStore entries as a scope-tree for the
|
|
295
|
+
* UserMemoryPage "folder view". Entries come from MemoryStore.listEntries()
|
|
296
|
+
* and are grouped client-side by their `scope` (a `/`-separated path like
|
|
297
|
+
* `work/claude-web-chat/auth`).
|
|
298
|
+
*
|
|
299
|
+
* Request shape: { type: 'unify_memory_scope_list', requestId? }
|
|
300
|
+
* Reply shape: { type: 'memory_scope_snapshot',
|
|
301
|
+
* entries: Array<{name,scope,kind,tags,importance,
|
|
302
|
+
* frequency,created_at,updated_at,
|
|
303
|
+
* content}>,
|
|
304
|
+
* requestId? }
|
|
305
|
+
*/
|
|
306
|
+
export function handleUnifyMemoryScopeList(msg) {
|
|
307
|
+
const requestId = msg && typeof msg.requestId === 'string' ? msg.requestId : undefined;
|
|
308
|
+
try {
|
|
309
|
+
const store = session && session.memoryStore;
|
|
310
|
+
const entries = store && typeof store.listEntries === 'function'
|
|
311
|
+
? store.listEntries()
|
|
312
|
+
: [];
|
|
313
|
+
sendUnifyEvent({
|
|
314
|
+
type: 'memory_scope_snapshot',
|
|
315
|
+
entries,
|
|
316
|
+
...(requestId ? { requestId } : {}),
|
|
317
|
+
});
|
|
318
|
+
} catch (err) {
|
|
319
|
+
sendUnifyEvent({
|
|
320
|
+
type: 'memory_scope_snapshot',
|
|
321
|
+
entries: [],
|
|
322
|
+
error: String(err && err.message || err),
|
|
323
|
+
...(requestId ? { requestId } : {}),
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
260
328
|
export function handleUnifyVpRead(msg) {
|
|
261
329
|
const requestId = msg && msg.requestId;
|
|
262
330
|
const vpId = msg && msg.vpId;
|
|
@@ -754,7 +822,7 @@ function forwardPipelineEvent(ev, ctx) {
|
|
|
754
822
|
*
|
|
755
823
|
* @param {object} event — engine event (text_delta / tool_call / …)
|
|
756
824
|
* @param {string} threadId — owning thread id (from envelope)
|
|
757
|
-
* @param {{assistantTextParts:string[], resetQueryTimer:Function}} hctx
|
|
825
|
+
* @param {{assistantTextParts:string[], toolCallsAccum:Array, toolResultsAccum:Array, resetQueryTimer:Function}} hctx
|
|
758
826
|
*/
|
|
759
827
|
function handleEngineEvent(event, threadId, hctx) {
|
|
760
828
|
hctx.resetQueryTimer();
|
|
@@ -790,6 +858,16 @@ function handleEngineEvent(event, threadId, hctx) {
|
|
|
790
858
|
break;
|
|
791
859
|
|
|
792
860
|
case 'tool_call':
|
|
861
|
+
// Capture tool_call for the assistant message's toolCalls array so the
|
|
862
|
+
// next turn's history correctly pairs `tool_calls` with `role:'tool'`
|
|
863
|
+
// results (fixes "No tool output found for function call" 400s).
|
|
864
|
+
if (hctx.toolCallsAccum) {
|
|
865
|
+
hctx.toolCallsAccum.push({
|
|
866
|
+
id: event.id,
|
|
867
|
+
name: event.name,
|
|
868
|
+
input: event.input,
|
|
869
|
+
});
|
|
870
|
+
}
|
|
793
871
|
// Finish any in-progress text streaming so UI shows typing dots
|
|
794
872
|
sendUnifyOutput({
|
|
795
873
|
type: 'assistant',
|
|
@@ -820,6 +898,17 @@ function handleEngineEvent(event, threadId, hctx) {
|
|
|
820
898
|
break;
|
|
821
899
|
|
|
822
900
|
case 'tool_end':
|
|
901
|
+
// Capture tool result for the next-turn history so the paired
|
|
902
|
+
// `role:'tool'` message is included when we hand `messages` back to
|
|
903
|
+
// engine.query() (chat-completions requires tool_call_id pairing).
|
|
904
|
+
if (hctx.toolResultsAccum) {
|
|
905
|
+
hctx.toolResultsAccum.push({
|
|
906
|
+
role: 'tool',
|
|
907
|
+
toolCallId: event.id,
|
|
908
|
+
content: typeof event.output === 'string' ? event.output : JSON.stringify(event.output ?? ''),
|
|
909
|
+
isError: !!event.isError,
|
|
910
|
+
});
|
|
911
|
+
}
|
|
823
912
|
sendUnifyOutput({
|
|
824
913
|
type: 'user',
|
|
825
914
|
tool_use_result: [{
|
|
@@ -1113,14 +1202,9 @@ export async function handleUnifyChat(msg) {
|
|
|
1113
1202
|
|
|
1114
1203
|
// Restore per-thread history from persisted conversation store.
|
|
1115
1204
|
// task-320: bucket by threadId so each thread keeps its own context.
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
if (m.role !== 'user' && m.role !== 'assistant') continue;
|
|
1120
|
-
const tid = m.threadId || MAIN_THREAD_ID;
|
|
1121
|
-
const bucket = getThreadMessages(tid);
|
|
1122
|
-
bucket.push({ role: m.role, content: m.content });
|
|
1123
|
-
}
|
|
1205
|
+
// task-fix: use restoreThreadHistoryFromRecent() so tool messages
|
|
1206
|
+
// and toolCalls/toolCallId survive the restore.
|
|
1207
|
+
restoreThreadHistoryFromRecent(session.conversationStore.loadRecent(50));
|
|
1124
1208
|
|
|
1125
1209
|
// Notify UI: session is ready with model info + conversationId
|
|
1126
1210
|
sendUnifyEvent({
|
|
@@ -1176,6 +1260,11 @@ export async function handleUnifyChat(msg) {
|
|
|
1176
1260
|
try {
|
|
1177
1261
|
// ─── Collect assistant response for conversation history ──
|
|
1178
1262
|
let assistantTextParts = [];
|
|
1263
|
+
// task-fix: preserve toolCalls + tool_result pairings across turns so
|
|
1264
|
+
// the next engine.query({messages}) handoff stays valid for OpenAI
|
|
1265
|
+
// chat-completions (avoids "No tool output found for function call").
|
|
1266
|
+
const toolCallsAccum = [];
|
|
1267
|
+
const toolResultsAccum = [];
|
|
1179
1268
|
|
|
1180
1269
|
// task-310: route via Dispatcher pipeline (queue → router → registry →
|
|
1181
1270
|
// EngineInstance). The input is enqueued first so the UI observes the
|
|
@@ -1203,6 +1292,8 @@ export async function handleUnifyChat(msg) {
|
|
|
1203
1292
|
const pipelineCtx = {
|
|
1204
1293
|
onEngineEvent: (event, threadId) => handleEngineEvent(event, threadId, {
|
|
1205
1294
|
assistantTextParts,
|
|
1295
|
+
toolCallsAccum,
|
|
1296
|
+
toolResultsAccum,
|
|
1206
1297
|
resetQueryTimer,
|
|
1207
1298
|
}),
|
|
1208
1299
|
onError: (err) => { throw err; },
|
|
@@ -1226,13 +1317,38 @@ export async function handleUnifyChat(msg) {
|
|
|
1226
1317
|
|
|
1227
1318
|
// ─── Query complete — accumulate messages for context continuity ──
|
|
1228
1319
|
// task-320: per-thread history (no cross-thread contamination).
|
|
1320
|
+
// task-fix: when the turn made tool calls, the assistant message MUST
|
|
1321
|
+
// carry `toolCalls` AND each paired `role:'tool'` result must be
|
|
1322
|
+
// appended — otherwise the next turn's chat-completions serializer
|
|
1323
|
+
// emits `tool_calls` without matching `tool` messages → proxy 400
|
|
1324
|
+
// "No tool output found for function call call_xxx".
|
|
1229
1325
|
const historyThread = resolvedThreadId || MAIN_THREAD_ID;
|
|
1230
1326
|
const threadMessages = getThreadMessages(historyThread);
|
|
1231
1327
|
threadMessages.push({ role: 'user', content: cleanedPrompt });
|
|
1232
1328
|
|
|
1233
1329
|
const fullText = assistantTextParts.join('');
|
|
1234
|
-
if (fullText) {
|
|
1235
|
-
|
|
1330
|
+
if (fullText || toolCallsAccum.length > 0) {
|
|
1331
|
+
const assistantMsg = { role: 'assistant', content: fullText };
|
|
1332
|
+
if (toolCallsAccum.length > 0) {
|
|
1333
|
+
assistantMsg.toolCalls = toolCallsAccum.map(tc => ({
|
|
1334
|
+
id: tc.id,
|
|
1335
|
+
name: tc.name,
|
|
1336
|
+
input: tc.input,
|
|
1337
|
+
}));
|
|
1338
|
+
}
|
|
1339
|
+
threadMessages.push(assistantMsg);
|
|
1340
|
+
|
|
1341
|
+
// Append paired tool results, in order. Chat-completions requires
|
|
1342
|
+
// one `role:'tool'` message per `tool_call_id` right after the
|
|
1343
|
+
// assistant message that emitted them.
|
|
1344
|
+
for (const tr of toolResultsAccum) {
|
|
1345
|
+
threadMessages.push({
|
|
1346
|
+
role: 'tool',
|
|
1347
|
+
toolCallId: tr.toolCallId,
|
|
1348
|
+
content: tr.content,
|
|
1349
|
+
isError: tr.isError,
|
|
1350
|
+
});
|
|
1351
|
+
}
|
|
1236
1352
|
}
|
|
1237
1353
|
|
|
1238
1354
|
// ─── Signal turn end to UI ──
|
|
@@ -1627,14 +1743,9 @@ export async function handleUnifyLoadHistory(msg) {
|
|
|
1627
1743
|
|
|
1628
1744
|
// Restore per-thread history from persisted conversation store.
|
|
1629
1745
|
// task-320: bucket by threadId so each thread keeps its own context.
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
if (m.role !== 'user' && m.role !== 'assistant') continue;
|
|
1634
|
-
const tid = m.threadId || MAIN_THREAD_ID;
|
|
1635
|
-
const bucket = getThreadMessages(tid);
|
|
1636
|
-
bucket.push({ role: m.role, content: m.content });
|
|
1637
|
-
}
|
|
1746
|
+
// task-fix: use restoreThreadHistoryFromRecent() so tool messages
|
|
1747
|
+
// and toolCalls/toolCallId survive the restore.
|
|
1748
|
+
restoreThreadHistoryFromRecent(session.conversationStore.loadRecent(50));
|
|
1638
1749
|
}
|
|
1639
1750
|
|
|
1640
1751
|
// task-322: replay `session_ready` + `thread_list_updated` UNCONDITIONALLY
|
|
@@ -1739,12 +1850,9 @@ export async function resetUnifySession() {
|
|
|
1739
1850
|
unifyConversationId = `unify-${Date.now()}`;
|
|
1740
1851
|
|
|
1741
1852
|
// Restore per-thread history for LLM context (task-320).
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
const tid = m.threadId || MAIN_THREAD_ID;
|
|
1746
|
-
getThreadMessages(tid).push({ role: m.role, content: m.content });
|
|
1747
|
-
}
|
|
1853
|
+
// task-fix: use restoreThreadHistoryFromRecent() so tool messages
|
|
1854
|
+
// and toolCalls/toolCallId survive the restore.
|
|
1855
|
+
restoreThreadHistoryFromRecent(session.conversationStore.loadRecent(50));
|
|
1748
1856
|
|
|
1749
1857
|
sendUnifyEvent({
|
|
1750
1858
|
type: 'session_ready',
|