@yeaft/webchat-agent 0.1.570 → 0.1.573
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/web-bridge.js +156 -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/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: [{
|
|
@@ -1041,6 +1130,11 @@ export async function handleUnifyGroupChat(msg) {
|
|
|
1041
1130
|
// Per target: emit `group_message` tagged with `speakerVpId` (the VP
|
|
1042
1131
|
// being addressed — F3's GroupSelector binding consumes this) + dispatch
|
|
1043
1132
|
// through the Engine via handleUnifyChat with an `@vp-<id>` prompt prefix.
|
|
1133
|
+
//
|
|
1134
|
+
// task-fix: bracket each per-VP dispatch with `vp_typing_start` /
|
|
1135
|
+
// `vp_typing_end` events so the frontend can render a per-speaker typing
|
|
1136
|
+
// dot next to that VP's avatar (matching IM apps). Avoids the old
|
|
1137
|
+
// "one global running cat for N concurrent speakers" ambiguity.
|
|
1044
1138
|
for (const { vpId, envelope } of captured) {
|
|
1045
1139
|
try {
|
|
1046
1140
|
sendUnifyEvent({
|
|
@@ -1055,6 +1149,15 @@ export async function handleUnifyGroupChat(msg) {
|
|
|
1055
1149
|
});
|
|
1056
1150
|
} catch { /* never crash WS pipeline */ }
|
|
1057
1151
|
|
|
1152
|
+
try {
|
|
1153
|
+
sendUnifyEvent({
|
|
1154
|
+
type: 'vp_typing_start',
|
|
1155
|
+
groupId,
|
|
1156
|
+
vpId,
|
|
1157
|
+
ts: Date.now(),
|
|
1158
|
+
});
|
|
1159
|
+
} catch { /* never crash WS pipeline */ }
|
|
1160
|
+
|
|
1058
1161
|
try {
|
|
1059
1162
|
await handleUnifyChat({
|
|
1060
1163
|
...msg,
|
|
@@ -1065,6 +1168,15 @@ export async function handleUnifyGroupChat(msg) {
|
|
|
1065
1168
|
});
|
|
1066
1169
|
} catch (err) {
|
|
1067
1170
|
console.warn('[Unify] unify_group_chat: per-vp dispatch failed', vpId, err?.message || err);
|
|
1171
|
+
} finally {
|
|
1172
|
+
try {
|
|
1173
|
+
sendUnifyEvent({
|
|
1174
|
+
type: 'vp_typing_end',
|
|
1175
|
+
groupId,
|
|
1176
|
+
vpId,
|
|
1177
|
+
ts: Date.now(),
|
|
1178
|
+
});
|
|
1179
|
+
} catch { /* never crash WS pipeline */ }
|
|
1068
1180
|
}
|
|
1069
1181
|
}
|
|
1070
1182
|
}
|
|
@@ -1113,14 +1225,9 @@ export async function handleUnifyChat(msg) {
|
|
|
1113
1225
|
|
|
1114
1226
|
// Restore per-thread history from persisted conversation store.
|
|
1115
1227
|
// 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
|
-
}
|
|
1228
|
+
// task-fix: use restoreThreadHistoryFromRecent() so tool messages
|
|
1229
|
+
// and toolCalls/toolCallId survive the restore.
|
|
1230
|
+
restoreThreadHistoryFromRecent(session.conversationStore.loadRecent(50));
|
|
1124
1231
|
|
|
1125
1232
|
// Notify UI: session is ready with model info + conversationId
|
|
1126
1233
|
sendUnifyEvent({
|
|
@@ -1176,6 +1283,11 @@ export async function handleUnifyChat(msg) {
|
|
|
1176
1283
|
try {
|
|
1177
1284
|
// ─── Collect assistant response for conversation history ──
|
|
1178
1285
|
let assistantTextParts = [];
|
|
1286
|
+
// task-fix: preserve toolCalls + tool_result pairings across turns so
|
|
1287
|
+
// the next engine.query({messages}) handoff stays valid for OpenAI
|
|
1288
|
+
// chat-completions (avoids "No tool output found for function call").
|
|
1289
|
+
const toolCallsAccum = [];
|
|
1290
|
+
const toolResultsAccum = [];
|
|
1179
1291
|
|
|
1180
1292
|
// task-310: route via Dispatcher pipeline (queue → router → registry →
|
|
1181
1293
|
// EngineInstance). The input is enqueued first so the UI observes the
|
|
@@ -1203,6 +1315,8 @@ export async function handleUnifyChat(msg) {
|
|
|
1203
1315
|
const pipelineCtx = {
|
|
1204
1316
|
onEngineEvent: (event, threadId) => handleEngineEvent(event, threadId, {
|
|
1205
1317
|
assistantTextParts,
|
|
1318
|
+
toolCallsAccum,
|
|
1319
|
+
toolResultsAccum,
|
|
1206
1320
|
resetQueryTimer,
|
|
1207
1321
|
}),
|
|
1208
1322
|
onError: (err) => { throw err; },
|
|
@@ -1226,13 +1340,38 @@ export async function handleUnifyChat(msg) {
|
|
|
1226
1340
|
|
|
1227
1341
|
// ─── Query complete — accumulate messages for context continuity ──
|
|
1228
1342
|
// task-320: per-thread history (no cross-thread contamination).
|
|
1343
|
+
// task-fix: when the turn made tool calls, the assistant message MUST
|
|
1344
|
+
// carry `toolCalls` AND each paired `role:'tool'` result must be
|
|
1345
|
+
// appended — otherwise the next turn's chat-completions serializer
|
|
1346
|
+
// emits `tool_calls` without matching `tool` messages → proxy 400
|
|
1347
|
+
// "No tool output found for function call call_xxx".
|
|
1229
1348
|
const historyThread = resolvedThreadId || MAIN_THREAD_ID;
|
|
1230
1349
|
const threadMessages = getThreadMessages(historyThread);
|
|
1231
1350
|
threadMessages.push({ role: 'user', content: cleanedPrompt });
|
|
1232
1351
|
|
|
1233
1352
|
const fullText = assistantTextParts.join('');
|
|
1234
|
-
if (fullText) {
|
|
1235
|
-
|
|
1353
|
+
if (fullText || toolCallsAccum.length > 0) {
|
|
1354
|
+
const assistantMsg = { role: 'assistant', content: fullText };
|
|
1355
|
+
if (toolCallsAccum.length > 0) {
|
|
1356
|
+
assistantMsg.toolCalls = toolCallsAccum.map(tc => ({
|
|
1357
|
+
id: tc.id,
|
|
1358
|
+
name: tc.name,
|
|
1359
|
+
input: tc.input,
|
|
1360
|
+
}));
|
|
1361
|
+
}
|
|
1362
|
+
threadMessages.push(assistantMsg);
|
|
1363
|
+
|
|
1364
|
+
// Append paired tool results, in order. Chat-completions requires
|
|
1365
|
+
// one `role:'tool'` message per `tool_call_id` right after the
|
|
1366
|
+
// assistant message that emitted them.
|
|
1367
|
+
for (const tr of toolResultsAccum) {
|
|
1368
|
+
threadMessages.push({
|
|
1369
|
+
role: 'tool',
|
|
1370
|
+
toolCallId: tr.toolCallId,
|
|
1371
|
+
content: tr.content,
|
|
1372
|
+
isError: tr.isError,
|
|
1373
|
+
});
|
|
1374
|
+
}
|
|
1236
1375
|
}
|
|
1237
1376
|
|
|
1238
1377
|
// ─── Signal turn end to UI ──
|
|
@@ -1627,14 +1766,9 @@ export async function handleUnifyLoadHistory(msg) {
|
|
|
1627
1766
|
|
|
1628
1767
|
// Restore per-thread history from persisted conversation store.
|
|
1629
1768
|
// 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
|
-
}
|
|
1769
|
+
// task-fix: use restoreThreadHistoryFromRecent() so tool messages
|
|
1770
|
+
// and toolCalls/toolCallId survive the restore.
|
|
1771
|
+
restoreThreadHistoryFromRecent(session.conversationStore.loadRecent(50));
|
|
1638
1772
|
}
|
|
1639
1773
|
|
|
1640
1774
|
// task-322: replay `session_ready` + `thread_list_updated` UNCONDITIONALLY
|
|
@@ -1739,12 +1873,9 @@ export async function resetUnifySession() {
|
|
|
1739
1873
|
unifyConversationId = `unify-${Date.now()}`;
|
|
1740
1874
|
|
|
1741
1875
|
// 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
|
-
}
|
|
1876
|
+
// task-fix: use restoreThreadHistoryFromRecent() so tool messages
|
|
1877
|
+
// and toolCalls/toolCallId survive the restore.
|
|
1878
|
+
restoreThreadHistoryFromRecent(session.conversationStore.loadRecent(50));
|
|
1748
1879
|
|
|
1749
1880
|
sendUnifyEvent({
|
|
1750
1881
|
type: 'session_ready',
|