mixdog 0.9.145 → 0.9.146
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/package.json +1 -1
- package/src/session-runtime/lifecycle-api.mjs +44 -1
- package/src/session-runtime/runtime-core.mjs +1 -0
- package/src/session-runtime/session-lifecycle.test.mjs +23 -0
- package/src/tui/session/session-api-ext.mjs +6 -1
- package/src/tui/session/tool-card-results.mjs +7 -0
- package/src/tui/session/tool-result-status.mjs +31 -1
- package/src/tui/session/tool-result-status.test.mjs +30 -0
- package/src/tui/session/turn.mjs +7 -2
package/package.json
CHANGED
|
@@ -32,6 +32,32 @@ function resolveResumeCwd(session, currentCwd) {
|
|
|
32
32
|
return session?.cwd || currentCwd;
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
+
function contextNumber(value) {
|
|
36
|
+
const number = Number(value);
|
|
37
|
+
return Number.isFinite(number) && number >= 0 ? number : null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function inheritanceContextFit(status) {
|
|
41
|
+
const context = status && typeof status === 'object' ? status : {};
|
|
42
|
+
const compaction = context.compaction && typeof context.compaction === 'object'
|
|
43
|
+
? context.compaction
|
|
44
|
+
: {};
|
|
45
|
+
const used = contextNumber(
|
|
46
|
+
compaction.pressureTokens
|
|
47
|
+
?? compaction.currentEstimatedTokens
|
|
48
|
+
?? context.usedTokens
|
|
49
|
+
?? context.currentEstimatedTokens,
|
|
50
|
+
);
|
|
51
|
+
const limit = contextNumber(compaction.triggerTokens ?? context.contextWindow);
|
|
52
|
+
const known = used !== null && limit !== null && limit > 0;
|
|
53
|
+
return {
|
|
54
|
+
known,
|
|
55
|
+
fits: !known || used < limit,
|
|
56
|
+
used: used ?? 0,
|
|
57
|
+
limit: limit ?? 0,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
35
61
|
// Session lifecycle surface: teardown (close/abort), resume/new, and the
|
|
36
62
|
// resumable-session listing. Extracted verbatim from the runtime API object;
|
|
37
63
|
// stateless helpers are imported directly and the runtime injects live
|
|
@@ -51,7 +77,7 @@ export function createLifecycleApi(deps) {
|
|
|
51
77
|
withTeardownDeadline, closePatchRuntimeIfLoaded, closeNativeToolTransports,
|
|
52
78
|
stopSelfUpdateBootCheck,
|
|
53
79
|
createCurrentSession, refreshRouteEffort,
|
|
54
|
-
invalidateContextStatusCache, invalidatePreSessionToolSurface,
|
|
80
|
+
computeContextStatus, invalidateContextStatusCache, invalidatePreSessionToolSurface,
|
|
55
81
|
applyResolvedCwd, resolveRoute, applyDeferredToolSurface, getStandaloneTools,
|
|
56
82
|
beginRoutePreparation, clearRoutePreparation,
|
|
57
83
|
notificationListeners, clearRuntimeNotifications,
|
|
@@ -606,7 +632,24 @@ export function createLifecycleApi(deps) {
|
|
|
606
632
|
if (!hasUserConversationMessage(carried)) {
|
|
607
633
|
throw new Error('inheritFrom: the source session has no conversation to carry');
|
|
608
634
|
}
|
|
635
|
+
const messageStart = target.messages.length;
|
|
609
636
|
target.messages.push(...structuredClone(carried));
|
|
637
|
+
invalidateContextStatusCache();
|
|
638
|
+
try {
|
|
639
|
+
const fit = inheritanceContextFit(
|
|
640
|
+
typeof computeContextStatus === 'function' ? computeContextStatus() : null,
|
|
641
|
+
);
|
|
642
|
+
if (fit.known && !fit.fits) {
|
|
643
|
+
throw new Error(
|
|
644
|
+
`inheritFrom: the full conversation needs ${Math.ceil(fit.used)} tokens `
|
|
645
|
+
+ `but the selected model allows ${Math.floor(fit.limit)} before compaction`,
|
|
646
|
+
);
|
|
647
|
+
}
|
|
648
|
+
} catch (error) {
|
|
649
|
+
target.messages.splice(messageStart);
|
|
650
|
+
invalidateContextStatusCache();
|
|
651
|
+
throw error;
|
|
652
|
+
}
|
|
610
653
|
target.inheritedFromSessionId = source.id;
|
|
611
654
|
target.updatedAt = Date.now();
|
|
612
655
|
if (!clean(target.title) && clean(source.title)) target.title = source.title;
|
|
@@ -1458,6 +1458,7 @@ export async function createMixdogSessionRuntime({
|
|
|
1458
1458
|
stopSelfUpdateBootCheck: () => selfUpdate.stopBootCheck(),
|
|
1459
1459
|
createCurrentSession,
|
|
1460
1460
|
refreshRouteEffort,
|
|
1461
|
+
computeContextStatus,
|
|
1461
1462
|
invalidateContextStatusCache,
|
|
1462
1463
|
invalidatePreSessionToolSurface,
|
|
1463
1464
|
applyResolvedCwd,
|
|
@@ -2,6 +2,7 @@ import assert from 'node:assert/strict';
|
|
|
2
2
|
import { test } from 'node:test';
|
|
3
3
|
import { makeResolveRoute } from './config-helpers.mjs';
|
|
4
4
|
import { resolveRouteContextState, resolveRouteEffortState } from './session-lifecycle.mjs';
|
|
5
|
+
import { inheritanceContextFit } from './lifecycle-api.mjs';
|
|
5
6
|
|
|
6
7
|
test('cold route metadata preserves persisted effort and enabled Fast mode', () => {
|
|
7
8
|
assert.deepEqual(resolveRouteEffortState({
|
|
@@ -100,3 +101,25 @@ test('route config treats a cleared context percentage as model-default intent',
|
|
|
100
101
|
model: 'gpt-5.4',
|
|
101
102
|
}).contextPercent, undefined);
|
|
102
103
|
});
|
|
104
|
+
|
|
105
|
+
test('session inheritance uses the selected model compaction boundary as its fit guard', () => {
|
|
106
|
+
assert.deepEqual(inheritanceContextFit({
|
|
107
|
+
usedTokens: 80_000,
|
|
108
|
+
contextWindow: 200_000,
|
|
109
|
+
compaction: {
|
|
110
|
+
pressureTokens: 95_000,
|
|
111
|
+
triggerTokens: 100_000,
|
|
112
|
+
},
|
|
113
|
+
}), {
|
|
114
|
+
known: true,
|
|
115
|
+
fits: true,
|
|
116
|
+
used: 95_000,
|
|
117
|
+
limit: 100_000,
|
|
118
|
+
});
|
|
119
|
+
assert.equal(inheritanceContextFit({
|
|
120
|
+
compaction: {
|
|
121
|
+
pressureTokens: 100_000,
|
|
122
|
+
triggerTokens: 100_000,
|
|
123
|
+
},
|
|
124
|
+
}).fits, false);
|
|
125
|
+
});
|
|
@@ -9,7 +9,7 @@ import { flushTuiSteeringPersist } from './tui-steering-persist.mjs';
|
|
|
9
9
|
import { getVoiceStatus, toggleVoice } from '../lib/voice-setup.mjs';
|
|
10
10
|
import { createSessionOAuthFlowRegistry } from './oauth-flows.mjs';
|
|
11
11
|
import { aggregateToolCategoryEntries, aggregateDoneCategories, classifyToolCategory, formatAggregateDetail, summarizeToolResult, toolLoadingTargets } from '../../runtime/shared/tool-surface.mjs';
|
|
12
|
-
import { aggregateBucketForCategory, aggregateRawResult, failureDetailText, toolCallOutcome } from './tool-result-status.mjs';
|
|
12
|
+
import { aggregateBucketForCategory, aggregateRawResult, aggregateToolMembers, failureDetailText, toolCallOutcome } from './tool-result-status.mjs';
|
|
13
13
|
import {
|
|
14
14
|
isInternalTranscriptDisplayText,
|
|
15
15
|
isTranscriptCancelledStatusText,
|
|
@@ -142,6 +142,7 @@ function buildRestoredAggregateItem(members) {
|
|
|
142
142
|
toolName: item.name,
|
|
143
143
|
}, resultText);
|
|
144
144
|
calls.push({
|
|
145
|
+
callId: item.id,
|
|
145
146
|
name: item.name,
|
|
146
147
|
args: item.args,
|
|
147
148
|
category,
|
|
@@ -150,7 +151,10 @@ function buildRestoredAggregateItem(members) {
|
|
|
150
151
|
isExitError,
|
|
151
152
|
exitCode,
|
|
152
153
|
resultText,
|
|
154
|
+
rawResultText: String(item.rawResult ?? item.result ?? ''),
|
|
153
155
|
resolved: true,
|
|
156
|
+
startedAt: item.startedAt,
|
|
157
|
+
completedAt: item.completedAt,
|
|
154
158
|
summary: !isCallError && resultText.trim()
|
|
155
159
|
? summarizeToolResult(item.name, item.args, resultText, false)
|
|
156
160
|
: null,
|
|
@@ -194,6 +198,7 @@ function buildRestoredAggregateItem(members) {
|
|
|
194
198
|
result: displayDetail,
|
|
195
199
|
text: displayDetail,
|
|
196
200
|
rawResult: rawResult || null,
|
|
201
|
+
toolMembers: aggregateToolMembers(calls),
|
|
197
202
|
...(latestUiDiff ? { uiDiff: latestUiDiff.uiDiff } : {}),
|
|
198
203
|
expanded: false,
|
|
199
204
|
headerFinalized: true,
|
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
groupedToolResultText,
|
|
20
20
|
aggregateRawResult,
|
|
21
21
|
aggregateSummaries,
|
|
22
|
+
aggregateToolMembers,
|
|
22
23
|
assignAggregateSummaryOrder,
|
|
23
24
|
failureDetailText,
|
|
24
25
|
toolCallOutcome,
|
|
@@ -104,7 +105,9 @@ export function createToolCardResults({
|
|
|
104
105
|
callRec.isExitError = isExitError;
|
|
105
106
|
callRec.exitCode = exitCode;
|
|
106
107
|
callRec.resultText = text;
|
|
108
|
+
callRec.rawResultText = rawText;
|
|
107
109
|
callRec.resolved = true;
|
|
110
|
+
callRec.completedAt = callRec.completedAt || Date.now();
|
|
108
111
|
const allCalls = [...aggregate.calls.values()];
|
|
109
112
|
const completed = allCalls.filter((r) => r.resolved).length;
|
|
110
113
|
const errors = allCalls.filter((r) => r.isError).length;
|
|
@@ -138,6 +141,7 @@ export function createToolCardResults({
|
|
|
138
141
|
count: allCalls.length,
|
|
139
142
|
completedCount: visualCompleted,
|
|
140
143
|
doneCategories: aggregateDoneCategories(allCalls),
|
|
144
|
+
toolMembers: aggregateToolMembers(allCalls),
|
|
141
145
|
completedAt: Number(currentItem?.completedAt) || Date.now(),
|
|
142
146
|
});
|
|
143
147
|
card.done = true;
|
|
@@ -247,9 +251,11 @@ export function createToolCardResults({
|
|
|
247
251
|
for (const rec of allCalls) {
|
|
248
252
|
if (rec.resolved) continue;
|
|
249
253
|
rec.resolved = true;
|
|
254
|
+
rec.completedAt = rec.completedAt || Date.now();
|
|
250
255
|
if (!rec.completedEarly) {
|
|
251
256
|
rec.isError = false;
|
|
252
257
|
rec.resultText = rec.resultText || '';
|
|
258
|
+
rec.rawResultText = rec.rawResultText ?? rec.resultText;
|
|
253
259
|
}
|
|
254
260
|
}
|
|
255
261
|
const completed = allCalls.filter((r) => r.resolved).length;
|
|
@@ -283,6 +289,7 @@ export function createToolCardResults({
|
|
|
283
289
|
count: allCalls.length,
|
|
284
290
|
completedCount: totalCompleted,
|
|
285
291
|
doneCategories: aggregateDoneCategories(allCalls),
|
|
292
|
+
toolMembers: aggregateToolMembers(allCalls),
|
|
286
293
|
completedAt: Date.now(),
|
|
287
294
|
});
|
|
288
295
|
for (const sibling of toolCards || []) {
|
|
@@ -152,7 +152,7 @@ export function aggregateRawResult(calls) {
|
|
|
152
152
|
const chunks = [];
|
|
153
153
|
for (const rec of calls || []) {
|
|
154
154
|
if (rec?.resolved !== true) continue;
|
|
155
|
-
let text = String(rec?.resultText
|
|
155
|
+
let text = String(rec?.rawResultText ?? rec?.resultText ?? '').replace(/\s+$/, '');
|
|
156
156
|
if (!text.trim()) continue;
|
|
157
157
|
const label = String(rec?.name || rec?.category || 'tool').trim() || 'tool';
|
|
158
158
|
chunks.push(`${chunks.length + 1}. ${label}\n${text}`);
|
|
@@ -160,6 +160,36 @@ export function aggregateRawResult(calls) {
|
|
|
160
160
|
return chunks.join('\n\n');
|
|
161
161
|
}
|
|
162
162
|
|
|
163
|
+
/** Preserve the atomic calls behind a visual aggregate. Renderers can keep the
|
|
164
|
+
* aggregate as one quiet summary row while revealing the original tool names,
|
|
165
|
+
* arguments, and outputs in provider order. */
|
|
166
|
+
export function aggregateToolMembers(calls) {
|
|
167
|
+
const source = calls?.values?.() || calls || [];
|
|
168
|
+
const members = [];
|
|
169
|
+
for (const rec of source) {
|
|
170
|
+
if (!rec || typeof rec !== 'object') continue;
|
|
171
|
+
const completed = rec.resolved === true || rec.completedEarly === true;
|
|
172
|
+
members.push({
|
|
173
|
+
kind: 'tool',
|
|
174
|
+
...(rec.callId != null ? { id: rec.callId } : {}),
|
|
175
|
+
name: String(rec.name || 'tool'),
|
|
176
|
+
args: rec.args ?? {},
|
|
177
|
+
result: rec.resultText ?? null,
|
|
178
|
+
rawResult: rec.rawResultText ?? rec.resultText ?? null,
|
|
179
|
+
isError: rec.isError === true,
|
|
180
|
+
errorCount: rec.isError === true ? 1 : 0,
|
|
181
|
+
callErrorCount: rec.isCallError === true ? 1 : 0,
|
|
182
|
+
exitErrorCount: rec.isExitError === true ? 1 : 0,
|
|
183
|
+
count: 1,
|
|
184
|
+
completedCount: completed ? 1 : 0,
|
|
185
|
+
headerFinalized: completed,
|
|
186
|
+
...(Number(rec.startedAt) > 0 ? { startedAt: Number(rec.startedAt) } : {}),
|
|
187
|
+
...(Number(rec.completedAt) > 0 ? { completedAt: Number(rec.completedAt) } : {}),
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
return members;
|
|
191
|
+
}
|
|
192
|
+
|
|
163
193
|
export function aggregateBucketForCategory(category, { agentBatch = '' } = {}) {
|
|
164
194
|
// Merge consecutive tool calls of the SAME category into one aggregate card;
|
|
165
195
|
// a different category opens a fresh card (no cross-category merge). The
|
|
@@ -2,6 +2,7 @@ import assert from 'node:assert/strict';
|
|
|
2
2
|
import test from 'node:test';
|
|
3
3
|
|
|
4
4
|
import {
|
|
5
|
+
aggregateToolMembers,
|
|
5
6
|
failureDetailText,
|
|
6
7
|
shellCommandExitCode,
|
|
7
8
|
toolCallOutcome,
|
|
@@ -99,3 +100,32 @@ test('shared TUI/desktop tone keeps command failures warning and tool failures r
|
|
|
99
100
|
assert.equal(deriveToolOutcomeTone({ terminalStatus: 'exit', exitFailedCount: 1 }), 'warning');
|
|
100
101
|
assert.equal(deriveToolOutcomeTone({ terminalStatus: 'failed', callFailedCount: 1 }), 'error');
|
|
101
102
|
});
|
|
103
|
+
|
|
104
|
+
test('aggregate members preserve atomic tool identity, inputs, outputs, and order', () => {
|
|
105
|
+
const members = aggregateToolMembers([
|
|
106
|
+
{
|
|
107
|
+
callId: 'call-read',
|
|
108
|
+
name: 'read',
|
|
109
|
+
args: { file_path: 'a.ts' },
|
|
110
|
+
resultText: 'source',
|
|
111
|
+
resolved: true,
|
|
112
|
+
isError: false,
|
|
113
|
+
},
|
|
114
|
+
{
|
|
115
|
+
callId: 'call-shell',
|
|
116
|
+
name: 'shell',
|
|
117
|
+
args: { command: 'exit 2' },
|
|
118
|
+
resultText: 'boom',
|
|
119
|
+
rawResultText: '[exit code: 2]\nboom',
|
|
120
|
+
resolved: true,
|
|
121
|
+
isError: false,
|
|
122
|
+
isExitError: true,
|
|
123
|
+
},
|
|
124
|
+
]);
|
|
125
|
+
assert.deepEqual(members.map(({ id, name, args, result, rawResult, exitErrorCount }) => ({
|
|
126
|
+
id, name, args, result, rawResult, exitErrorCount,
|
|
127
|
+
})), [
|
|
128
|
+
{ id: 'call-read', name: 'read', args: { file_path: 'a.ts' }, result: 'source', rawResult: 'source', exitErrorCount: 0 },
|
|
129
|
+
{ id: 'call-shell', name: 'shell', args: { command: 'exit 2' }, result: 'boom', rawResult: '[exit code: 2]\nboom', exitErrorCount: 1 },
|
|
130
|
+
]);
|
|
131
|
+
});
|
package/src/tui/session/turn.mjs
CHANGED
|
@@ -9,7 +9,7 @@ import { isCancelLikeError } from '../../runtime/shared/err-text.mjs';
|
|
|
9
9
|
import { toolCallId, toolResultCallId, toolCallName, toolCallArgs } from './tool-call-fields.mjs';
|
|
10
10
|
import { promptDisplayText, STEERING_SUPPRESSED_DISPLAY } from './queue-helpers.mjs';
|
|
11
11
|
import { TUI_FRAME_MS, yieldToRenderer } from './render-timing.mjs';
|
|
12
|
-
import { aggregateRawResult, aggregateBucketForCategory, aggregateSummaries, assignAggregateSummaryOrder, failureDetailText, toolCallOutcome } from './tool-result-status.mjs';
|
|
12
|
+
import { aggregateRawResult, aggregateBucketForCategory, aggregateSummaries, aggregateToolMembers, assignAggregateSummaryOrder, failureDetailText, toolCallOutcome } from './tool-result-status.mjs';
|
|
13
13
|
|
|
14
14
|
export const STREAM_BATCH_INTERVAL_MS = TUI_FRAME_MS;
|
|
15
15
|
|
|
@@ -327,6 +327,7 @@ export function createRunTurn(bag) {
|
|
|
327
327
|
result: displayDetail,
|
|
328
328
|
text: displayDetail,
|
|
329
329
|
rawResult: rawResult || null,
|
|
330
|
+
toolMembers: aggregateToolMembers(allCalls),
|
|
330
331
|
isError: errors > 0,
|
|
331
332
|
errorCount: errors,
|
|
332
333
|
callErrorCount: callErrors,
|
|
@@ -403,6 +404,7 @@ export function createRunTurn(bag) {
|
|
|
403
404
|
count: aggregate.calls.size,
|
|
404
405
|
completedCount: [...aggregate.calls.values()].filter((r) => r.resolved || r.completedEarly).length,
|
|
405
406
|
categories: Object.fromEntries(aggregate.categories),
|
|
407
|
+
toolMembers: aggregateToolMembers(aggregate.calls),
|
|
406
408
|
};
|
|
407
409
|
if (aggregate.pushed) {
|
|
408
410
|
patchItem(aggregate.itemId, patch);
|
|
@@ -678,7 +680,9 @@ export function createRunTurn(bag) {
|
|
|
678
680
|
callRec.isExitError = isExitError;
|
|
679
681
|
callRec.exitCode = exitCode;
|
|
680
682
|
callRec.resultText = text;
|
|
683
|
+
callRec.rawResultText = rawText;
|
|
681
684
|
callRec.completedEarly = true;
|
|
685
|
+
callRec.completedAt = callRec.completedAt || Date.now();
|
|
682
686
|
const allCalls = [...aggregate.calls.values()];
|
|
683
687
|
const completedCount = allCalls.filter((r) => r.resolved || r.completedEarly).length;
|
|
684
688
|
const errors = allCalls.filter((r) => r.isError).length;
|
|
@@ -710,6 +714,7 @@ export function createRunTurn(bag) {
|
|
|
710
714
|
exitErrorCount: exitErrors,
|
|
711
715
|
count: allCalls.length,
|
|
712
716
|
completedCount: visualCompleted,
|
|
717
|
+
toolMembers: aggregateToolMembers(allCalls),
|
|
713
718
|
};
|
|
714
719
|
if (visualCompleted >= allCalls.length) {
|
|
715
720
|
patch.completedAt = Number(currentItem?.completedAt) || Date.now();
|
|
@@ -889,7 +894,7 @@ export function createRunTurn(bag) {
|
|
|
889
894
|
count: Number(prevCategory?.count || 0) + Number(categoryEntry.count || 1),
|
|
890
895
|
});
|
|
891
896
|
}
|
|
892
|
-
aggregateCard.calls.set(callKey, { name, args, category, summary: null, summarySeq: null, isError: false, isCallError: false, isExitError: false, exitCode: null, resultText: null, resolved: false, completedEarly: false });
|
|
897
|
+
aggregateCard.calls.set(callKey, { callId: callKey, name, args, category, summary: null, summarySeq: null, isError: false, isCallError: false, isExitError: false, exitCode: null, resultText: null, rawResultText: null, resolved: false, completedEarly: false, startedAt: Date.now(), completedAt: null });
|
|
893
898
|
touchedAggregates.add(aggregateCard);
|
|
894
899
|
const card = { itemId: aggregateCard.itemId, callId: callKey, done: false, aggregate: aggregateCard };
|
|
895
900
|
if (callId) {
|