@sanqianx/claude-ai-workbench 0.13.5 → 0.13.6
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/apps/agent-terminal/app.js +66 -19
- package/apps/agent-terminal/right-workspace-dock.css +2 -0
- package/apps/agent-terminal/right-workspace-dock.js +56 -20
- package/docs/sdk/SESSION_EVENTS.md +2 -0
- package/docs/sdk/openapi.yaml +1 -1
- package/package.json +1 -1
- package/packages/client/package.json +2 -2
- package/packages/contracts/package.json +1 -1
- package/packages/core/package.json +2 -2
- package/packages/server/lib/session-history.js +2 -1
- package/packages/server/package.json +2 -2
- package/packages/ui/package.json +2 -2
- package/src/backend/lib/claude-cli-runner.js +15 -8
- package/src/backend/lib/sdk-session-stream.js +30 -4
|
@@ -1063,6 +1063,21 @@ function renderHistoryBar() {
|
|
|
1063
1063
|
|
|
1064
1064
|
function historyEventKey(event) { return JSON.stringify([event.type, event.id || '', event.sequence || 0, event.at || '', event.text || '']); }
|
|
1065
1065
|
|
|
1066
|
+
function mergeHistoryEvents(fetched, buffered) {
|
|
1067
|
+
const keys = new Set(fetched.map(historyEventKey));
|
|
1068
|
+
const sequences = new Set(fetched.map(event => Number(event.sequence)).filter(sequence => sequence > 0));
|
|
1069
|
+
const compacted = fetched.filter(event => Number(event.compactFrom) > 0);
|
|
1070
|
+
const queued = buffered.filter(event => {
|
|
1071
|
+
if (keys.has(historyEventKey(event))) return false;
|
|
1072
|
+
const sequence = Number(event.sequence);
|
|
1073
|
+
if (!(sequence > 0)) return true;
|
|
1074
|
+
if (sequences.has(sequence)) return false;
|
|
1075
|
+
return !compacted.some(row => row.type === event.type && row.id === event.id
|
|
1076
|
+
&& row.parentToolUseId === event.parentToolUseId && sequence >= Number(row.compactFrom) && sequence <= Number(row.sequence));
|
|
1077
|
+
});
|
|
1078
|
+
return [...fetched, ...queued].sort((a, b) => (Number(a.sequence) || 0) - (Number(b.sequence) || 0));
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1066
1081
|
function rebuildHistory(view, events, prepend = true) {
|
|
1067
1082
|
const stream = $('stream');
|
|
1068
1083
|
const top = stream.scrollTop, height = stream.scrollHeight, following = state.follow, intent = state.scrollIntent;
|
|
@@ -1104,7 +1119,7 @@ async function loadEarlierHistory() {
|
|
|
1104
1119
|
view.nextCursor = page.nextCursor;
|
|
1105
1120
|
view.incomplete = page.incomplete;
|
|
1106
1121
|
view.error = false;
|
|
1107
|
-
rebuildHistory(view,
|
|
1122
|
+
rebuildHistory(view, mergeHistoryEvents(page.events, view.events));
|
|
1108
1123
|
} catch { if (historyView === view) view.error = true; }
|
|
1109
1124
|
finally { if (historyView === view && revision === view.revision) { view.loading = false; renderHistoryBar(); } }
|
|
1110
1125
|
}
|
|
@@ -1135,10 +1150,8 @@ async function reloadHistory(view) {
|
|
|
1135
1150
|
// (including before this reload even started) survive unless the fetched
|
|
1136
1151
|
// history already contains them. The fetched page must never overwrite
|
|
1137
1152
|
// the live tail into silence.
|
|
1138
|
-
const fetchedKeys = new Set(events.map(historyEventKey));
|
|
1139
|
-
const queued = buffered.filter(event => !fetchedKeys.has(historyEventKey(event)));
|
|
1140
1153
|
view.nextCursor = nextCursor; view.incomplete = page.incomplete; view.error = false;
|
|
1141
|
-
rebuildHistory(view,
|
|
1154
|
+
rebuildHistory(view, mergeHistoryEvents(events, buffered), false);
|
|
1142
1155
|
} catch { if (historyView === view) view.error = true; }
|
|
1143
1156
|
finally { if (historyView === view && revision === view.revision) { view.loading = false; renderHistoryBar(); } }
|
|
1144
1157
|
}
|
|
@@ -1180,6 +1193,8 @@ function resetStream() {
|
|
|
1180
1193
|
const col = $('stream-col');
|
|
1181
1194
|
col.textContent = '';
|
|
1182
1195
|
turn = null;
|
|
1196
|
+
pendingReplyGroups.length = 0;
|
|
1197
|
+
replyGroup = null;
|
|
1183
1198
|
state.completedReply = null;
|
|
1184
1199
|
state.follow = true;
|
|
1185
1200
|
state.scrollIntent = null;
|
|
@@ -1248,8 +1263,22 @@ function checkMark() {
|
|
|
1248
1263
|
// single collapsible block headed by the running/elapsed timer, ZCode-style
|
|
1249
1264
|
// (工作中 X 秒 → 已工作 X 秒); only the final message stays in the open stream.
|
|
1250
1265
|
let turn = null;
|
|
1266
|
+
const pendingReplyGroups = [];
|
|
1267
|
+
let replyGroup = null;
|
|
1268
|
+
|
|
1269
|
+
function newReplyGroup(turnId) {
|
|
1270
|
+
const group = { turnId: turnId || null, before: null, completedReply: null, context: null };
|
|
1271
|
+
pendingReplyGroups.push(group);
|
|
1272
|
+
return group;
|
|
1273
|
+
}
|
|
1251
1274
|
|
|
1252
|
-
function
|
|
1275
|
+
function appendReply(node, group) {
|
|
1276
|
+
const col = $('stream-col');
|
|
1277
|
+
if (group?.before?.isConnected) col.insertBefore(node, group.before);
|
|
1278
|
+
else col.append(node);
|
|
1279
|
+
}
|
|
1280
|
+
|
|
1281
|
+
function startTurn(turnId) {
|
|
1253
1282
|
endTurnVisuals();
|
|
1254
1283
|
turn = {
|
|
1255
1284
|
startedAt: Date.now(), timerId: null, proc: null, steps: null, meta: null,
|
|
@@ -1257,6 +1286,10 @@ function startTurn() {
|
|
|
1257
1286
|
lastPrompt: '', changes: new Map(), interrupted: false,
|
|
1258
1287
|
stderrNote: null, stderrLast: '', thinkHint: null, lastError: '',
|
|
1259
1288
|
};
|
|
1289
|
+
if (!replyGroup || !pendingReplyGroups.includes(replyGroup)) replyGroup = newReplyGroup(turnId);
|
|
1290
|
+
turn.replyGroup = replyGroup;
|
|
1291
|
+
replyGroup.completedReply = null;
|
|
1292
|
+
replyGroup.context = turn;
|
|
1260
1293
|
return turn;
|
|
1261
1294
|
}
|
|
1262
1295
|
|
|
@@ -1316,6 +1349,7 @@ function showThinkingHint(t) {
|
|
|
1316
1349
|
function endTurnVisuals(durationMs) {
|
|
1317
1350
|
const t = turn;
|
|
1318
1351
|
if (!t) return;
|
|
1352
|
+
if (t.liveText?.isConnected && t.replyGroup) t.replyGroup.completedReply = { node: t.liveText, text: t.liveRaw || '' };
|
|
1319
1353
|
stampThinking(t);
|
|
1320
1354
|
hideThinkingHint(t);
|
|
1321
1355
|
if (t.timerId) { clearInterval(t.timerId); t.timerId = null; }
|
|
@@ -1529,14 +1563,16 @@ function renderEvent(event) {
|
|
|
1529
1563
|
wrap.append(tray);
|
|
1530
1564
|
}
|
|
1531
1565
|
col.append(wrap);
|
|
1532
|
-
const
|
|
1566
|
+
for (const group of pendingReplyGroups) if (!group.before) group.before = wrap;
|
|
1567
|
+
replyGroup = newReplyGroup(event.turnId);
|
|
1568
|
+
const ctx = startTurn(event.turnId);
|
|
1533
1569
|
ctx.lastPrompt = event.text || '';
|
|
1534
1570
|
break;
|
|
1535
1571
|
}
|
|
1536
1572
|
case 'claude/text-start':
|
|
1537
1573
|
case 'claude/text-delta': {
|
|
1538
1574
|
stampThinking(t);
|
|
1539
|
-
const ctx = t || startTurn();
|
|
1575
|
+
const ctx = t || startTurn(event.turnId);
|
|
1540
1576
|
if (!ctx.liveText || !ctx.liveText.isConnected) {
|
|
1541
1577
|
const msg = buildConversationMessage('assistant', '');
|
|
1542
1578
|
const body = msg.querySelector('.md-body');
|
|
@@ -1557,7 +1593,7 @@ function renderEvent(event) {
|
|
|
1557
1593
|
break;
|
|
1558
1594
|
}
|
|
1559
1595
|
case 'claude/thinking-start': {
|
|
1560
|
-
const ctx = ensureProc(t || startTurn());
|
|
1596
|
+
const ctx = ensureProc(t || startTurn(event.turnId));
|
|
1561
1597
|
stampThinking(ctx);
|
|
1562
1598
|
const seg = el('div', 'think-seg');
|
|
1563
1599
|
seg._startedAt = Date.now();
|
|
@@ -1578,7 +1614,7 @@ function renderEvent(event) {
|
|
|
1578
1614
|
break;
|
|
1579
1615
|
case 'claude/tool-use': {
|
|
1580
1616
|
stampThinking(t);
|
|
1581
|
-
const ctx = ensureProc(t || startTurn());
|
|
1617
|
+
const ctx = ensureProc(t || startTurn(event.turnId));
|
|
1582
1618
|
const input = event.input || {};
|
|
1583
1619
|
ctx.steps.append(buildToolStep(event, input));
|
|
1584
1620
|
break;
|
|
@@ -1632,32 +1668,41 @@ function renderEvent(event) {
|
|
|
1632
1668
|
refreshContextUsage();
|
|
1633
1669
|
break;
|
|
1634
1670
|
case 'claude/turn-end':
|
|
1671
|
+
if (event.boundary === 'result') break; // result already finalized its own response
|
|
1672
|
+
if (event.turnId && replyGroup?.turnId && event.turnId !== replyGroup.turnId) break;
|
|
1635
1673
|
if (t?.liveText?.isConnected) state.completedReply = { node: t.liveText, text: t.liveRaw || '' };
|
|
1636
1674
|
endTurnVisuals();
|
|
1637
1675
|
setBusyState(event.exitCode ? 'failed' : 'idle');
|
|
1638
1676
|
break;
|
|
1639
1677
|
case 'claude/result': {
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
const
|
|
1643
|
-
const
|
|
1678
|
+
const index = event.turnId ? pendingReplyGroups.findIndex(group => group.turnId === event.turnId) : 0;
|
|
1679
|
+
if (event.turnId && index < 0) break; // an earlier response may be outside the currently loaded history page
|
|
1680
|
+
const group = pendingReplyGroups.splice(index, 1)[0] || replyGroup;
|
|
1681
|
+
const owner = group?.context || t;
|
|
1682
|
+
const current = !group || group === replyGroup;
|
|
1683
|
+
stampThinking(owner);
|
|
1684
|
+
const hadLiveText = Boolean(owner?.liveText?.isConnected || group?.completedReply?.node?.isConnected);
|
|
1685
|
+
const lastPrompt = owner ? owner.lastPrompt : '';
|
|
1686
|
+
const turnChanges = owner ? owner.changes : null;
|
|
1644
1687
|
const turnModel = event.model || (state.sessionSummary && state.sessionSummary.model) || '';
|
|
1645
|
-
endTurnVisuals(event.durationMs != null ? event.durationMs : event.duration_ms);
|
|
1688
|
+
if (t === owner) endTurnVisuals(event.durationMs != null ? event.durationMs : event.duration_ms);
|
|
1689
|
+
let replyNode = owner?.liveText?.isConnected ? owner.liveText : group?.completedReply?.node;
|
|
1646
1690
|
if (event.isError) {
|
|
1647
1691
|
const err = el('div', 'msg ai');
|
|
1648
1692
|
const dim = el('p', 'dim');
|
|
1649
1693
|
dim.append(icon('warn', 13), el('span', null, ` ${event.result || event.message || '执行出错'}`));
|
|
1650
1694
|
err.append(dim);
|
|
1651
|
-
|
|
1695
|
+
appendReply(err, group);
|
|
1652
1696
|
} else {
|
|
1653
1697
|
if (!hadLiveText && event.result) {
|
|
1654
|
-
|
|
1698
|
+
replyNode = buildConversationMessage('assistant', String(event.result));
|
|
1699
|
+
appendReply(replyNode, group);
|
|
1655
1700
|
}
|
|
1656
|
-
const lastMsg =
|
|
1701
|
+
const lastMsg = replyNode;
|
|
1657
1702
|
if (lastMsg) lastMsg.append(msgFooter(String(event.result || lastMsg.textContent), lastPrompt, { model: turnModel, at: event.at }));
|
|
1658
|
-
if (turnChanges && turnChanges.size)
|
|
1703
|
+
if (turnChanges && turnChanges.size) appendReply(renderTurnDiffs(turnChanges), group);
|
|
1659
1704
|
}
|
|
1660
|
-
setBusyState('idle');
|
|
1705
|
+
if (current) setBusyState('idle');
|
|
1661
1706
|
refreshContextUsage();
|
|
1662
1707
|
if (!state.replaying) refreshSessionRow();
|
|
1663
1708
|
break;
|
|
@@ -1677,6 +1722,7 @@ function renderEvent(event) {
|
|
|
1677
1722
|
case 'claude/aborted':
|
|
1678
1723
|
if (t) t.interrupted = true;
|
|
1679
1724
|
endTurnVisuals();
|
|
1725
|
+
pendingReplyGroups.length = 0; replyGroup = null;
|
|
1680
1726
|
{
|
|
1681
1727
|
const stopNote = el('div', 'turn-note');
|
|
1682
1728
|
stopNote.append(icon('stop', 12), el('span', null, ` ${abortStopLabel(event.reason)}`));
|
|
@@ -1694,6 +1740,7 @@ function renderEvent(event) {
|
|
|
1694
1740
|
case 'claude/error': {
|
|
1695
1741
|
const message = String(event.message || event.error || '执行出错');
|
|
1696
1742
|
endTurnVisuals();
|
|
1743
|
+
pendingReplyGroups.length = 0; replyGroup = null;
|
|
1697
1744
|
const err = el('div', 'msg ai error');
|
|
1698
1745
|
err.append(icon('warn', 15), el('span', 'err-text', message));
|
|
1699
1746
|
col.append(err);
|
|
@@ -116,6 +116,8 @@
|
|
|
116
116
|
|
|
117
117
|
.rd-error { padding: 4px 10px; color: var(--bad); font-size: 12px; flex: 0 0 auto; white-space: pre-wrap; }
|
|
118
118
|
.rd-error[hidden] { display: none; }
|
|
119
|
+
.rd-retry { margin-left: 10px; padding: 3px 9px; font: inherit; border: 1px solid var(--line); border-radius: 6px; background: transparent; color: var(--text); cursor: pointer; }
|
|
120
|
+
.rd-retry:disabled { opacity: .5; cursor: default; }
|
|
119
121
|
|
|
120
122
|
.rd-scroll { flex: 1 1 auto; overflow-y: auto; padding: 4px 16px 16px; min-height: 0; }
|
|
121
123
|
|
|
@@ -123,6 +123,8 @@
|
|
|
123
123
|
let listState = { loading: false, error: null, code: null, resync: false };
|
|
124
124
|
let persistTimer = null;
|
|
125
125
|
const taskIndex = new Map();
|
|
126
|
+
const taskLiveVersions = new Map();
|
|
127
|
+
let taskLiveRevision = 0, taskListRevision = 0;
|
|
126
128
|
const delegationIndex = new Map();
|
|
127
129
|
const delegationTitles = new Map();
|
|
128
130
|
let delegationState = { loading: false, error: null, code: null };
|
|
@@ -282,6 +284,7 @@
|
|
|
282
284
|
for (const [oldKey, old] of taskIndex) if (!old.taskRef && old.taskId === ref.taskId) taskIndex.delete(oldKey);
|
|
283
285
|
const entry = taskIndex.get(key) || { key, taskRef: ref, sdkSessionId: null, taskId: null, taskKind: null, taskType: null, typeConflict: false, status: null, statusSource: null, association: null, rowCount: null };
|
|
284
286
|
taskIndex.set(key, { ...entry, ...patch });
|
|
287
|
+
taskLiveVersions.set(key, ++taskLiveRevision);
|
|
285
288
|
}
|
|
286
289
|
|
|
287
290
|
function upsertFromStateEvent(ref, event) {
|
|
@@ -317,6 +320,7 @@
|
|
|
317
320
|
}
|
|
318
321
|
|
|
319
322
|
async function validateScopeTasks() {
|
|
323
|
+
const revision = ++taskListRevision;
|
|
320
324
|
if (!scope || !scope.storeScope) {
|
|
321
325
|
listState = { loading: false, error: '服务端任务身份不可用,无法读取原生任务列表。', code: 'TASK_IDENTITY_UNAVAILABLE', resync: false };
|
|
322
326
|
for (const tab of tabs) if (tab.kind !== 'delegated' && (!tab.panel || !tab.panel.loaded)) tab.invalid = { code: 'TASK_IDENTITY_UNAVAILABLE', message: '服务端任务身份不可用;本切片不会伪造任务,也不会重新派发。' };
|
|
@@ -324,20 +328,23 @@
|
|
|
324
328
|
return;
|
|
325
329
|
}
|
|
326
330
|
const epoch = scopeEpoch;
|
|
331
|
+
const liveRevision = taskLiveRevision;
|
|
327
332
|
listState = { loading: true, error: null, code: null, resync: false };
|
|
328
333
|
render();
|
|
329
334
|
let page = null, error = null;
|
|
330
335
|
try { page = await client.listNativeTasks(scope.parentSessionId, { storeScope: scope.storeScope, contextId: scope.contextId }); }
|
|
331
336
|
catch (err) { error = err; }
|
|
332
|
-
if (epoch !== scopeEpoch) return;
|
|
337
|
+
if (epoch !== scopeEpoch || revision !== taskListRevision) return;
|
|
333
338
|
if (error) {
|
|
334
339
|
const code = (error && error.code) || String((error && error.status) || '');
|
|
335
340
|
listState = { loading: false, error: `任务列表读取失败:${error.message || '未知错误'}`, code, resync: error.resync === true };
|
|
336
|
-
for (const tab of tabs) if (tab.kind
|
|
341
|
+
for (const tab of tabs) if (!tab.kind && !taskIndex.has(taskRefKey(tab.taskRef)) && (!tab.panel || !tab.panel.loaded)) tab.invalid = { code, message: listState.error, resync: listState.resync };
|
|
337
342
|
render();
|
|
338
343
|
return;
|
|
339
344
|
}
|
|
345
|
+
const newer = [...taskIndex.entries()].filter(([key]) => (taskLiveVersions.get(key) || 0) > liveRevision);
|
|
340
346
|
rebuildIndex(page && Array.isArray(page.tasks) ? page.tasks : []);
|
|
347
|
+
for (const [key, entry] of newer) taskIndex.set(key, { ...taskIndex.get(key), ...entry });
|
|
341
348
|
const incomplete = page && Array.isArray(page.incomplete) && page.incomplete.length > 0;
|
|
342
349
|
listState = { loading: false, error: null, code: null, resync: incomplete ? 'incomplete' : false };
|
|
343
350
|
// A rebuilt native index changes backgroundActivity (running counts);
|
|
@@ -383,6 +390,7 @@
|
|
|
383
390
|
return;
|
|
384
391
|
}
|
|
385
392
|
panel.events = page && Array.isArray(page.events) ? page.events : [];
|
|
393
|
+
tab.invalid = null;
|
|
386
394
|
panel.nextCursor = page && page.nextCursor || null;
|
|
387
395
|
panel.hasMore = page ? page.hasMore === true : false;
|
|
388
396
|
panel.loaded = true;
|
|
@@ -972,16 +980,28 @@
|
|
|
972
980
|
// dispatch state and diagnostics never enter customer DOM.
|
|
973
981
|
function createConversation(scroll) {
|
|
974
982
|
let turn = null;
|
|
983
|
+
let group = null;
|
|
984
|
+
let eventTurnId = null;
|
|
985
|
+
const pending = [];
|
|
975
986
|
const tools = new Map();
|
|
987
|
+
function startGroup(turnId) { group = { turnId: turnId || null, before: null, turn: null }; pending.push(group); }
|
|
976
988
|
function current() {
|
|
977
|
-
if (!turn)
|
|
989
|
+
if (!turn) {
|
|
990
|
+
if (!group || !pending.includes(group)) startGroup(eventTurnId);
|
|
991
|
+
turn = { messages: [], changes: [], process: null, live: null, thinking: null, group };
|
|
992
|
+
group.turn = turn;
|
|
993
|
+
}
|
|
978
994
|
return turn;
|
|
979
995
|
}
|
|
996
|
+
function append(node, owner) {
|
|
997
|
+
if (owner?.group?.before?.parentNode === scroll) scroll.insertBefore(node, owner.group.before);
|
|
998
|
+
else scroll.append(node);
|
|
999
|
+
}
|
|
980
1000
|
function mark(node, sequence) {
|
|
981
1001
|
if (Number.isFinite(Number(sequence))) node.dataset.seq = String(sequence);
|
|
982
1002
|
return node;
|
|
983
1003
|
}
|
|
984
|
-
function message(role, text, sequence) {
|
|
1004
|
+
function message(role, text, sequence, owner) {
|
|
985
1005
|
const node = renderMessage ? renderMessage(role, text) : el('div', role === 'user' ? 'msg user' : 'msg ai');
|
|
986
1006
|
let body = node.querySelector && node.querySelector(role === 'user' ? '.bubble' : '.md-body');
|
|
987
1007
|
if (!body) {
|
|
@@ -991,7 +1011,7 @@
|
|
|
991
1011
|
node.append(body);
|
|
992
1012
|
}
|
|
993
1013
|
mark(node, sequence);
|
|
994
|
-
|
|
1014
|
+
append(node, owner);
|
|
995
1015
|
return { node, body, text };
|
|
996
1016
|
}
|
|
997
1017
|
function process(sequence) {
|
|
@@ -1017,20 +1037,24 @@
|
|
|
1017
1037
|
return t.process;
|
|
1018
1038
|
}
|
|
1019
1039
|
function updateProcess(p) { p.meta.textContent = p.steps.children.length + ' 个步骤'; }
|
|
1020
|
-
function finish(sequence) {
|
|
1021
|
-
if (!
|
|
1022
|
-
if (renderChanges &&
|
|
1023
|
-
const changes = renderChanges(
|
|
1024
|
-
if (changes)
|
|
1040
|
+
function finish(sequence, owner = turn) {
|
|
1041
|
+
if (!owner) return;
|
|
1042
|
+
if (renderChanges && owner.changes.length) {
|
|
1043
|
+
const changes = renderChanges(owner.changes);
|
|
1044
|
+
if (changes) append(mark(changes, sequence), owner);
|
|
1045
|
+
owner.changes = [];
|
|
1025
1046
|
}
|
|
1026
|
-
turn = null;
|
|
1047
|
+
if (turn === owner) turn = null;
|
|
1027
1048
|
}
|
|
1028
1049
|
function accept(event) {
|
|
1029
1050
|
if (!event) return;
|
|
1051
|
+
eventTurnId = event.turnId || null;
|
|
1030
1052
|
const seq = event.sequence, type = event.type;
|
|
1031
1053
|
if (type === 'claude/user-prompt') {
|
|
1032
1054
|
finish(seq);
|
|
1033
|
-
|
|
1055
|
+
const user = event.text ? message('user', String(event.text), seq) : null;
|
|
1056
|
+
if (user) for (const old of pending) if (!old.before) old.before = user.node;
|
|
1057
|
+
startGroup(event.turnId);
|
|
1034
1058
|
current();
|
|
1035
1059
|
} else if (type === 'claude/text-start') {
|
|
1036
1060
|
current().live = null;
|
|
@@ -1079,15 +1103,20 @@
|
|
|
1079
1103
|
mark(p.node, seq);
|
|
1080
1104
|
updateProcess(p);
|
|
1081
1105
|
} else if (type === 'claude/result') {
|
|
1082
|
-
const
|
|
1083
|
-
if (event.
|
|
1084
|
-
|
|
1085
|
-
|
|
1106
|
+
const index = event.turnId ? pending.findIndex(group => group.turnId === event.turnId) : 0;
|
|
1107
|
+
if (event.turnId && index < 0) return;
|
|
1108
|
+
const t = pending.splice(index, 1)[0]?.turn || current();
|
|
1109
|
+
if (event.isError) append(mark(el('div', 'turn-note', '回复失败,请稍后重试。'), seq), t);
|
|
1110
|
+
else if (!t.messages.length && event.result) message('assistant', String(event.result), seq, t);
|
|
1111
|
+
finish(seq, t);
|
|
1112
|
+
} else if (type === 'claude/turn-end') {
|
|
1113
|
+
if (event.boundary !== 'result' && (!event.turnId || !group?.turnId || event.turnId === group.turnId)) finish(seq);
|
|
1086
1114
|
} else if (type === 'conversation/unavailable') {
|
|
1087
1115
|
scroll.append(mark(el('div', 'turn-note', '任务记录暂不可用,请刷新重试。'), seq));
|
|
1088
1116
|
} else if (type === 'claude/aborted' || type === 'claude/task-interrupted' || type === 'claude/error') {
|
|
1089
1117
|
scroll.append(mark(el('div', 'turn-note', type === 'claude/error' ? '回复失败,请稍后重试。' : '回复已中断'), seq));
|
|
1090
1118
|
finish(seq);
|
|
1119
|
+
pending.length = 0; group = null;
|
|
1091
1120
|
}
|
|
1092
1121
|
}
|
|
1093
1122
|
return { accept, finish };
|
|
@@ -1161,7 +1190,8 @@
|
|
|
1161
1190
|
pane.title.textContent = delegationTitle(tab.delegationId);
|
|
1162
1191
|
pane.status.textContent = delegationStatus(view).label;
|
|
1163
1192
|
pane.errorBox.hidden = !tab.invalid && !panel.error;
|
|
1164
|
-
if (tab.invalid || panel.error) pane.
|
|
1193
|
+
if (tab.invalid || panel.error) pane.errorText.textContent = '暂时无法查看此任务。';
|
|
1194
|
+
pane.retry.disabled = panel.loading === true;
|
|
1165
1195
|
pane.banner.hidden = true;
|
|
1166
1196
|
if (tab.invalid) {
|
|
1167
1197
|
pane.scroll.textContent = '';
|
|
@@ -1169,7 +1199,7 @@
|
|
|
1169
1199
|
return;
|
|
1170
1200
|
}
|
|
1171
1201
|
if (!panel.rendered) renderDelegationTimeline(tab);
|
|
1172
|
-
if (tab.error || tab.connectionError) { pane.errorBox.hidden = false; pane.
|
|
1202
|
+
if (tab.error || tab.connectionError) { pane.errorBox.hidden = false; pane.errorText.textContent = tab.error || tab.connectionError; }
|
|
1173
1203
|
if (pane.composer) { pane.composer.setBusy(childBusy(tab)); pane.composer.send.disabled = Boolean(tab.invalid || tab.sending || !view || !view.child.present); }
|
|
1174
1204
|
if (pane.composer && view) pane.composer.setLabel?.((view.child.model || '子代理') + ' · 默认防护');
|
|
1175
1205
|
renderPermissions(tab);
|
|
@@ -1267,6 +1297,7 @@
|
|
|
1267
1297
|
const previousScope = scope;
|
|
1268
1298
|
flushPersist();
|
|
1269
1299
|
scopeEpoch += 1;
|
|
1300
|
+
taskLiveVersions.clear();
|
|
1270
1301
|
scope = sanitizeScope(input);
|
|
1271
1302
|
restoreFor(scope, previousScope);
|
|
1272
1303
|
pickerOpen = false;
|
|
@@ -1711,6 +1742,10 @@
|
|
|
1711
1742
|
banner.append(bannerText, resyncBtn);
|
|
1712
1743
|
const errorBox = el('div', 'rd-error');
|
|
1713
1744
|
errorBox.hidden = true;
|
|
1745
|
+
const errorText = el('span');
|
|
1746
|
+
const retry = el('button', 'rd-retry', '重试');
|
|
1747
|
+
retry.addEventListener('click', () => { void (tab.kind === 'delegated' ? loadDelegationFirstPage(tab) : loadPanelFirstPage(tab)); });
|
|
1748
|
+
errorBox.append(errorText, retry);
|
|
1714
1749
|
const scroll = el('div', 'rd-scroll');
|
|
1715
1750
|
scroll.addEventListener('scroll', () => {
|
|
1716
1751
|
const pane = tab.pane;
|
|
@@ -1737,7 +1772,7 @@
|
|
|
1737
1772
|
});
|
|
1738
1773
|
node.append(continuation);
|
|
1739
1774
|
}
|
|
1740
|
-
tab.pane = { node, head, title, status, banner, bannerText, resyncBtn, errorBox, scroll, more,
|
|
1775
|
+
tab.pane = { node, head, title, status, banner, bannerText, resyncBtn, errorBox, errorText, retry, scroll, more,
|
|
1741
1776
|
follow: true, anchorSeq: null, anchorNote: null, pendingNew: false };
|
|
1742
1777
|
}
|
|
1743
1778
|
|
|
@@ -1867,7 +1902,8 @@
|
|
|
1867
1902
|
pane.title.textContent = taskTitle(entry);
|
|
1868
1903
|
pane.status.textContent = entry && !entry.typeConflict ? (STATUS_LABELS[entry.status] || '状态待确认') : '状态待确认';
|
|
1869
1904
|
pane.errorBox.hidden = !tab.invalid && !panel.error;
|
|
1870
|
-
if (tab.invalid || panel.error) pane.
|
|
1905
|
+
if (tab.invalid || panel.error) pane.errorText.textContent = '暂时无法查看此任务。';
|
|
1906
|
+
pane.retry.disabled = panel.loading === true;
|
|
1871
1907
|
pane.banner.hidden = !panel.resync;
|
|
1872
1908
|
if (panel.resync) pane.bannerText.textContent = '记录已更新,请刷新查看。';
|
|
1873
1909
|
if (tab.invalid) {
|
|
@@ -10,6 +10,8 @@ Session summaries include `tasks` and `backgroundTaskCount`. Task records use ac
|
|
|
10
10
|
|
|
11
11
|
`claude/turn-end` closes the foreground reply. For Claude SDK streaming input this may occur at `end_turn` before its aggregate `result`, which can wait for background tasks. The input queue and event reader remain alive while tasks run. When foreground and tasks are both idle, the query is collected after 30 seconds; the next input resumes the same native session. Changing provider/model while a task runs is rejected.
|
|
12
12
|
|
|
13
|
+
Claude SDK inputs now carry an optional opaque `turnId` on `claude/user-prompt` and the normalized response events. Match a delayed `claude/result` to that input, even when a newer prompt is already visible; a result for an input outside a loaded history page must not become the current reply. `claude/turn-end` with `boundary: "result"` is the boundary for the result already processed, rather than another active reply. Older journals without `turnId` retain their original ordered input/result pairing.
|
|
14
|
+
|
|
13
15
|
Bound native agent tasks also persist evidence rows in the same journal: `claude/native-trace` (sanitized in-process hook facts), `claude/native-task-state` (ownership/status projections) and `claude/native-task-transcript` (a snapshot of a bound agent's transcript read through the SDK's public `getSubagentMessages` at a real terminal signal, or at the bounded turn-end sweep). Transcript rows carry `source: "sdk-public-read"`, fixed content-free `read` reason codes when a read cannot happen honestly, byte-bounded `chunk` markers, and normalized redacted entries/tool steps/usage - a snapshot read, never a claim of per-token streaming. These rows are part of the single-session stream's named event vocabulary: the standalone client subscribes to `claude/native-trace`, `claude/native-task-state`, `claude/native-task-transcript` and `claude/task-interrupted` like every other chat event, so SDK consumers receive the native facts in real time. They are additionally read back through the two read-only native-task history APIs (`GET /sessions/:id/native-tasks`, `POST /sessions/:id/native-tasks/history`), which page them without dispatching input.
|
|
14
16
|
|
|
15
17
|
Set `inputOrigin: "agent-delegation"` explicitly when creating a Leader-dispatched session and on its input options. A delegated session cannot reclassify itself as human. Browser composers identify human input; unmarked legacy/API records remain unknown. This provenance follows the durable capture event key through the event bridge. No task is inferred from prose claiming that an agent is running. Drivers advertise `capabilities.backgroundTasks`; unsupported drivers show the limitation explicitly.
|
package/docs/sdk/openapi.yaml
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@claude-ai-workbench/client",
|
|
3
|
-
"version": "0.13.
|
|
3
|
+
"version": "0.13.6",
|
|
4
4
|
"type": "commonjs",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"browser": "index.js",
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
"README.md"
|
|
21
21
|
],
|
|
22
22
|
"dependencies": {
|
|
23
|
-
"@claude-ai-workbench/contracts": "0.13.
|
|
23
|
+
"@claude-ai-workbench/contracts": "0.13.6"
|
|
24
24
|
},
|
|
25
25
|
"engines": {
|
|
26
26
|
"node": ">=18"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@claude-ai-workbench/core",
|
|
3
|
-
"version": "0.13.
|
|
3
|
+
"version": "0.13.6",
|
|
4
4
|
"type": "commonjs",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"browser": "index.js",
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
"README.md"
|
|
21
21
|
],
|
|
22
22
|
"dependencies": {
|
|
23
|
-
"@claude-ai-workbench/contracts": "0.13.
|
|
23
|
+
"@claude-ai-workbench/contracts": "0.13.6"
|
|
24
24
|
},
|
|
25
25
|
"engines": {
|
|
26
26
|
"node": ">=18"
|
|
@@ -20,7 +20,8 @@ function parseLine(text) {
|
|
|
20
20
|
|
|
21
21
|
function compactInto(events, event) {
|
|
22
22
|
const previous = events[events.length - 1];
|
|
23
|
-
if (previous && DELTAS.has(event.type) && previous.type === event.type && previous.id === event.id
|
|
23
|
+
if (previous && DELTAS.has(event.type) && previous.type === event.type && previous.id === event.id
|
|
24
|
+
&& previous.parentToolUseId === event.parentToolUseId && previous.sdkSessionId === event.sdkSessionId && previous.turnId === event.turnId) {
|
|
24
25
|
previous.text = String(previous.text || '') + String(event.text || '');
|
|
25
26
|
// Provenance for later recovery: a merged row spans from its first
|
|
26
27
|
// fragment's sequence, so a stale partial buffer can be told to replace
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@claude-ai-workbench/server",
|
|
3
|
-
"version": "0.13.
|
|
3
|
+
"version": "0.13.6",
|
|
4
4
|
"type": "commonjs",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"types": "index.d.ts",
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
],
|
|
26
26
|
"dependencies": {
|
|
27
27
|
"@anthropic-ai/claude-agent-sdk": "^0.2.141",
|
|
28
|
-
"@claude-ai-workbench/contracts": "0.13.
|
|
28
|
+
"@claude-ai-workbench/contracts": "0.13.6"
|
|
29
29
|
},
|
|
30
30
|
"engines": {
|
|
31
31
|
"node": ">=18"
|
package/packages/ui/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@claude-ai-workbench/ui",
|
|
3
|
-
"version": "0.13.
|
|
3
|
+
"version": "0.13.6",
|
|
4
4
|
"type": "commonjs",
|
|
5
5
|
"main": "ai-profile-settings.js",
|
|
6
6
|
"browser": "ai-profile-settings.js",
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
"README.md"
|
|
34
34
|
],
|
|
35
35
|
"dependencies": {
|
|
36
|
-
"@claude-ai-workbench/core": "0.13.
|
|
36
|
+
"@claude-ai-workbench/core": "0.13.6"
|
|
37
37
|
},
|
|
38
38
|
"engines": {
|
|
39
39
|
"node": ">=18"
|
|
@@ -874,6 +874,10 @@ function cutSdkExecutionForNativeHistory(session, phase) {
|
|
|
874
874
|
}
|
|
875
875
|
|
|
876
876
|
function emit(session, event) {
|
|
877
|
+
if (event.type === 'claude/user-prompt') {
|
|
878
|
+
event.turnId ||= crypto.randomUUID();
|
|
879
|
+
session.inputTurnId = event.turnId;
|
|
880
|
+
} else if (session.sdkEventTurnId && !event.turnId) event.turnId = session.sdkEventTurnId;
|
|
877
881
|
if (session.sdkEventParentToolUseId && !event.parentToolUseId) event.parentToolUseId = session.sdkEventParentToolUseId;
|
|
878
882
|
// Only the SDK message currently being processed may lend its session id,
|
|
879
883
|
// and only to events that state no fact of their own: an explicit
|
|
@@ -1462,6 +1466,7 @@ async function sendInput(sessionId, text, aiProfile = null, opts = {}) {
|
|
|
1462
1466
|
}
|
|
1463
1467
|
const isFresh = !session.claudeSessionId;
|
|
1464
1468
|
emit(session, { type: 'claude/user-prompt', text, inputOrigin: opts.inputOrigin || 'unknown', attachments: Array.isArray(opts.displayAttachments) ? opts.displayAttachments : [], isFollowUp: !isFresh, isFresh });
|
|
1469
|
+
const inputTurnId = session.inputTurnId;
|
|
1465
1470
|
const sdkOverrides = buildSdkOverridesFromProfile(aiProfile);
|
|
1466
1471
|
session.permissionMode = normalizePermissionMode(opts.permissionMode || session.permissionMode || 'default');
|
|
1467
1472
|
const envModel = session.claudeEnv && session.claudeEnv.ANTHROPIC_MODEL
|
|
@@ -1484,7 +1489,7 @@ async function sendInput(sessionId, text, aiProfile = null, opts = {}) {
|
|
|
1484
1489
|
try {
|
|
1485
1490
|
if (resident.query?.setPermissionMode) await resident.query.setPermissionMode(turnOpts.permissionMode);
|
|
1486
1491
|
if (turnOpts.effort && resident.query?.setMaxThinkingTokens) await resident.query.setMaxThinkingTokens(EFFORT_THINKING_BUDGETS[normalizeEffortLevel(turnOpts.effort)]);
|
|
1487
|
-
for await (const message of createSdkPrompt(turnOpts.userPrompt, turnOpts.attachments)) resident.send(message);
|
|
1492
|
+
for await (const message of createSdkPrompt(turnOpts.userPrompt, turnOpts.attachments)) resident.send(message, inputTurnId);
|
|
1488
1493
|
session.turns++;
|
|
1489
1494
|
setState(session, 'running', { turn: session.turns, runner: 'sdk' });
|
|
1490
1495
|
} catch (error) { resident.close('input-failed'); throw error; }
|
|
@@ -1920,7 +1925,7 @@ async function runSdkTurn(session, opts, isResume, generation) {
|
|
|
1920
1925
|
const attemptToolInputBuffers = attempt === 0 ? toolInputBuffers : new Map();
|
|
1921
1926
|
try {
|
|
1922
1927
|
const { SdkSessionStream, mainReplyEnded } = require('./sdk-session-stream');
|
|
1923
|
-
const transport = new SdkSessionStream({ onMessage: message => {
|
|
1928
|
+
const transport = new SdkSessionStream({ onMessage: (message, ownership) => {
|
|
1924
1929
|
if (!isCurrent()) return;
|
|
1925
1930
|
if (!message.parent_tool_use_id && message.session_id) session.claudeSessionId = message.session_id;
|
|
1926
1931
|
session.sdkEventParentToolUseId = message.parent_tool_use_id || null;
|
|
@@ -1930,9 +1935,10 @@ async function runSdkTurn(session, opts, isResume, generation) {
|
|
|
1930
1935
|
// SDK envelope, and the stored claudeSessionId is never used to
|
|
1931
1936
|
// backfill identity.
|
|
1932
1937
|
session.sdkEventSessionId = typeof message.session_id === 'string' && message.session_id !== '' ? message.session_id : null;
|
|
1938
|
+
session.sdkEventTurnId = ownership.turnId;
|
|
1933
1939
|
let event;
|
|
1934
1940
|
try { event = handleNdjsonLine(session, JSON.stringify(message), attemptToolInputBuffers); }
|
|
1935
|
-
finally { session.sdkEventParentToolUseId = null; session.sdkEventSessionId = null; }
|
|
1941
|
+
finally { session.sdkEventParentToolUseId = null; session.sdkEventSessionId = null; session.sdkEventTurnId = null; }
|
|
1936
1942
|
// Existing normalized events were emitted above; the registry feed
|
|
1937
1943
|
// comes strictly afterwards, and background subagent messages keep
|
|
1938
1944
|
// arriving here after the main reply's end_turn.
|
|
@@ -1942,21 +1948,21 @@ async function runSdkTurn(session, opts, isResume, generation) {
|
|
|
1942
1948
|
if (session.nativeTranscriptCollector) {
|
|
1943
1949
|
try { session.nativeTranscriptCollector.notifySdkMessage(message); } catch { /* transcript reads must never break the stream */ }
|
|
1944
1950
|
}
|
|
1945
|
-
if (!message.parent_tool_use_id && event?.type === 'claude/result') {
|
|
1951
|
+
if (!message.parent_tool_use_id && event?.type === 'claude/result' && ownership.isCurrentTurn) {
|
|
1946
1952
|
if (event.isError) {
|
|
1947
1953
|
session.error = event.result || 'Claude Code returned an error result';
|
|
1948
1954
|
setState(session, 'failed', { error: session.error });
|
|
1949
1955
|
} else setState(session, 'idle', { exitCode: 0, message: 'turn complete, awaiting input' });
|
|
1950
|
-
emit(session, { type: 'claude/turn-end', exitCode: event.isError ? 1 : 0 });
|
|
1956
|
+
emit(session, { type: 'claude/turn-end', exitCode: event.isError ? 1 : 0, boundary: 'result', turnId: ownership.turnId });
|
|
1951
1957
|
// The query is ending: one bounded sweep reads every bound agent
|
|
1952
1958
|
// already in a terminal state, so a completed background task is
|
|
1953
1959
|
// never truncated by the main reply's end.
|
|
1954
1960
|
if (session.nativeTranscriptCollector) {
|
|
1955
1961
|
try { session.nativeTranscriptCollector.sweep('turn-end'); } catch { /* a sweep must never break the stream */ }
|
|
1956
1962
|
}
|
|
1957
|
-
} else if (mainReplyEnded(message) && session.state !== 'idle') {
|
|
1963
|
+
} else if (mainReplyEnded(message) && ownership.isCurrentTurn && session.state !== 'idle') {
|
|
1958
1964
|
setState(session, 'idle', { message: 'main reply complete; background notifications remain subscribed' });
|
|
1959
|
-
emit(session, { type: 'claude/turn-end', exitCode: 0, boundary: 'end_turn' });
|
|
1965
|
+
emit(session, { type: 'claude/turn-end', exitCode: 0, boundary: 'end_turn', turnId: ownership.turnId });
|
|
1960
1966
|
if (session.nativeTranscriptCollector) {
|
|
1961
1967
|
try { session.nativeTranscriptCollector.sweep('end-turn-boundary'); } catch { /* a sweep must never break the stream */ }
|
|
1962
1968
|
}
|
|
@@ -1965,7 +1971,7 @@ async function runSdkTurn(session, opts, isResume, generation) {
|
|
|
1965
1971
|
}
|
|
1966
1972
|
} });
|
|
1967
1973
|
session.sdkTransport = transport;
|
|
1968
|
-
for await (const message of createSdkPrompt(opts.userPrompt, opts.attachments)) transport.send(message);
|
|
1974
|
+
for await (const message of createSdkPrompt(opts.userPrompt, opts.attachments)) transport.send(message, opts.turnId);
|
|
1969
1975
|
queryInstance = query({
|
|
1970
1976
|
prompt: transport.input,
|
|
1971
1977
|
options: sdkOptions,
|
|
@@ -2042,6 +2048,7 @@ async function runSdkTurn(session, opts, isResume, generation) {
|
|
|
2042
2048
|
}
|
|
2043
2049
|
|
|
2044
2050
|
function startSdkTurn(session, opts, isResume) {
|
|
2051
|
+
opts.turnId ||= session.inputTurnId;
|
|
2045
2052
|
const generation = (session.sdkTurnGeneration || 0) + 1;
|
|
2046
2053
|
session.sdkTurnGeneration = generation;
|
|
2047
2054
|
session.pendingPermission = null;
|
|
@@ -10,6 +10,12 @@ class SdkSessionStream {
|
|
|
10
10
|
this.clearTimer = clearTimer;
|
|
11
11
|
this.foregroundRunning = false;
|
|
12
12
|
this.completedTurns = 0;
|
|
13
|
+
// A reply can end before its input's result arrives. Keep each input
|
|
14
|
+
// pending until that ordered result, even after accepting another input.
|
|
15
|
+
this.pendingTurns = [];
|
|
16
|
+
this.messageTurns = new Map();
|
|
17
|
+
this.currentTurn = null;
|
|
18
|
+
this.responseTurn = null;
|
|
13
19
|
this.tasks = new Map();
|
|
14
20
|
this.queue = [];
|
|
15
21
|
this.waiter = null;
|
|
@@ -31,11 +37,13 @@ class SdkSessionStream {
|
|
|
31
37
|
// unrecognized literal: still active, never a claimed terminal state.
|
|
32
38
|
get activeTaskCount() { return [...this.tasks.values()].filter(status => status === 'running' || status === 'pending' || status === UNKNOWN_TASK_STATUS).length; }
|
|
33
39
|
|
|
34
|
-
send(message) {
|
|
40
|
+
send(message, turnId = message.uuid || null) {
|
|
35
41
|
if (this.closed) throw Object.assign(new Error('session transport is closed'), { code: 'SESSION_CLOSED' });
|
|
36
42
|
if (this.foregroundRunning) throw Object.assign(new Error('session already has an active turn'), { code: 'TURN_BUSY', status: 409 });
|
|
37
43
|
this.cancelIdle();
|
|
38
44
|
this.foregroundRunning = true;
|
|
45
|
+
this.currentTurn = { ended: false, turnId };
|
|
46
|
+
this.pendingTurns.push(this.currentTurn);
|
|
39
47
|
if (this.waiter) { const resolve = this.waiter; this.waiter = null; resolve({ value: message, done: false }); }
|
|
40
48
|
else this.queue.push(message);
|
|
41
49
|
}
|
|
@@ -55,14 +63,32 @@ class SdkSessionStream {
|
|
|
55
63
|
|
|
56
64
|
observe(message) {
|
|
57
65
|
const nested = Boolean(message.parent_tool_use_id);
|
|
66
|
+
let owner = this.responseTurn || this.currentTurn;
|
|
67
|
+
if (!nested && message.type === 'stream_event' && message.event?.type === 'message_start') {
|
|
68
|
+
if (!this.pendingTurns.length) {
|
|
69
|
+
this.currentTurn = { ended: false, turnId: this.currentTurn?.turnId || null };
|
|
70
|
+
this.pendingTurns.push(this.currentTurn);
|
|
71
|
+
}
|
|
72
|
+
owner = this.pendingTurns.find(turn => !turn.ended) || this.currentTurn;
|
|
73
|
+
this.responseTurn = owner;
|
|
74
|
+
if (message.event.message?.id && owner) this.messageTurns.set(message.event.message.id, owner);
|
|
75
|
+
} else if (!nested && message.type === 'assistant' && message.message?.id) {
|
|
76
|
+
// The completed assistant envelope can trail its streamed end_turn.
|
|
77
|
+
owner = this.messageTurns.get(message.message.id) || owner;
|
|
78
|
+
} else if (!nested && message.type === 'result') {
|
|
79
|
+
owner = this.pendingTurns.shift() || owner;
|
|
80
|
+
}
|
|
81
|
+
const isCurrentTurn = !owner || owner === this.currentTurn;
|
|
58
82
|
if (message.type === 'system' && message.task_id) this._observeTask(message);
|
|
59
83
|
if (!nested && (message.type === 'result' || mainReplyEnded(message))) {
|
|
60
|
-
|
|
84
|
+
if (owner) owner.ended = true;
|
|
85
|
+
if (isCurrentTurn) this.foregroundRunning = false;
|
|
61
86
|
if (message.type === 'result') this.completedTurns++;
|
|
62
87
|
} else if (!nested && message.type === 'stream_event' && message.event?.type === 'message_start') {
|
|
63
|
-
|
|
88
|
+
if (owner) owner.ended = false;
|
|
89
|
+
if (isCurrentTurn) this.foregroundRunning = true;
|
|
64
90
|
}
|
|
65
|
-
this.onMessage(message);
|
|
91
|
+
this.onMessage(message, { isCurrentTurn, turnId: owner?.turnId || null });
|
|
66
92
|
this.cancelIdle();
|
|
67
93
|
if (!this.foregroundRunning && !this.activeTaskCount) {
|
|
68
94
|
this.idleTimer = this.setTimer(() => this.close('idle'), this.idleMs);
|