muonroi-cli 1.8.0 → 1.8.1
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/LICENSE +21 -21
- package/README.md +122 -122
- package/dist/src/agent-harness/mock-model.js +112 -4
- package/dist/src/cli/cost-forensics.js +17 -14
- package/dist/src/council/prompts.js +20 -20
- package/dist/src/flow/compaction/index.d.ts +1 -0
- package/dist/src/flow/compaction/index.js +4 -0
- package/dist/src/generated/version.d.ts +1 -1
- package/dist/src/generated/version.js +1 -1
- package/dist/src/index.js +4 -2
- package/dist/src/lsp/manager.js +11 -3
- package/dist/src/lsp/manager.test.js +39 -0
- package/dist/src/mcp/auto-setup.js +0 -8
- package/dist/src/mcp/oauth-callback.js +2 -2
- package/dist/src/mcp/parse-headers.test.js +14 -14
- package/dist/src/mcp/self-verify-jobs.d.ts +1 -0
- package/dist/src/mcp/self-verify-runner.js +1 -1
- package/dist/src/mcp/setup-guide-text.js +74 -74
- package/dist/src/mcp/smoke.test.js +43 -43
- package/dist/src/ops/doctor.js +7 -7
- package/dist/src/orchestrator/__tests__/batch-turn-runner.test.js +1 -0
- package/dist/src/orchestrator/__tests__/message-processor.test.js +1 -0
- package/dist/src/orchestrator/__tests__/prompts.test.d.ts +1 -0
- package/dist/src/orchestrator/__tests__/prompts.test.js +88 -0
- package/dist/src/orchestrator/__tests__/sub-session-delegation.test.d.ts +1 -0
- package/dist/src/orchestrator/__tests__/sub-session-delegation.test.js +507 -0
- package/dist/src/orchestrator/auto-commit.js +9 -7
- package/dist/src/orchestrator/message-processor.d.ts +2 -0
- package/dist/src/orchestrator/message-processor.js +7 -0
- package/dist/src/orchestrator/orchestrator.d.ts +3 -0
- package/dist/src/orchestrator/orchestrator.js +362 -28
- package/dist/src/orchestrator/pending-calls.js +2 -1
- package/dist/src/orchestrator/preprocessor.js +3 -2
- package/dist/src/orchestrator/prompts.d.ts +8 -0
- package/dist/src/orchestrator/prompts.js +65 -18
- package/dist/src/orchestrator/sub-agent-cap.d.ts +2 -0
- package/dist/src/orchestrator/sub-agent-cap.js +12 -4
- package/dist/src/orchestrator/subagent-compactor.d.ts +7 -0
- package/dist/src/orchestrator/subagent-compactor.js +65 -14
- package/dist/src/orchestrator/subagent-compactor.spec.js +125 -9
- package/dist/src/orchestrator/tool-engine.d.ts +2 -0
- package/dist/src/orchestrator/tool-engine.js +80 -21
- package/dist/src/orchestrator/turn-runner-deps.d.ts +2 -0
- package/dist/src/pil/__tests__/llm-classify.test.js +79 -1
- package/dist/src/pil/layer1-intent.js +37 -37
- package/dist/src/pil/layer2_5-ponytail.js +8 -8
- package/dist/src/pil/llm-classify.d.ts +11 -0
- package/dist/src/pil/llm-classify.js +193 -0
- package/dist/src/product-loop/done-gate.js +3 -3
- package/dist/src/product-loop/loop-driver.js +18 -18
- package/dist/src/product-loop/progress-snapshot.js +4 -4
- package/dist/src/providers/mcp-vision-bridge.js +48 -48
- package/dist/src/reporter/index.js +1 -1
- package/dist/src/scaffold/bb-ecosystem-apply.js +47 -47
- package/dist/src/scaffold/bb-quality-gate.js +5 -5
- package/dist/src/scaffold/continuation-prompt.js +60 -60
- package/dist/src/scaffold/init-new.js +453 -453
- package/dist/src/self-qa/__tests__/scenario-planner.test.js +3 -3
- package/dist/src/self-qa/agentic-loop.js +11 -2
- package/dist/src/storage/__tests__/hierarchical-session.test.d.ts +1 -0
- package/dist/src/storage/__tests__/hierarchical-session.test.js +161 -0
- package/dist/src/storage/__tests__/migrations.test.js +2 -2
- package/dist/src/storage/__tests__/sweep-stale-pending.test.js +2 -2
- package/dist/src/storage/index.d.ts +1 -1
- package/dist/src/storage/index.js +1 -1
- package/dist/src/storage/interaction-log.js +5 -5
- package/dist/src/storage/migrations.js +135 -123
- package/dist/src/storage/session-experience-store.js +4 -4
- package/dist/src/storage/sessions.js +43 -43
- package/dist/src/storage/transcript-response-entry.test.js +18 -5
- package/dist/src/storage/transcript.d.ts +2 -0
- package/dist/src/storage/transcript.js +223 -97
- package/dist/src/storage/usage.js +34 -17
- package/dist/src/storage/workspaces.js +12 -12
- package/dist/src/tools/native-tools.js +10 -1
- package/dist/src/tools/registry.d.ts +1 -0
- package/dist/src/tools/registry.js +30 -2
- package/dist/src/types/index.d.ts +1 -1
- package/dist/src/ui/app.js +0 -0
- package/dist/src/ui/slash/council-inspect.js +4 -4
- package/dist/src/ui/slash/expand.js +14 -1
- package/dist/src/ui/use-app-logic.js +0 -0
- package/dist/src/utils/__tests__/compaction-caps.test.js +9 -9
- package/dist/src/utils/__tests__/llm-deadline.test.js +16 -4
- package/dist/src/utils/__tests__/logger.test.d.ts +1 -0
- package/dist/src/utils/__tests__/logger.test.js +115 -0
- package/dist/src/utils/__tests__/settings-agent-first.test.d.ts +1 -0
- package/dist/src/utils/__tests__/settings-agent-first.test.js +48 -0
- package/dist/src/utils/clipboard-image.js +23 -23
- package/dist/src/utils/install-manager.d.ts +1 -0
- package/dist/src/utils/install-manager.js +76 -7
- package/dist/src/utils/install-manager.test.js +43 -1
- package/dist/src/utils/logger.d.ts +28 -0
- package/dist/src/utils/logger.js +171 -0
- package/dist/src/utils/settings.d.ts +9 -1
- package/dist/src/utils/settings.js +59 -22
- package/dist/src/utils/side-question.js +2 -2
- package/dist/src/utils/skills.js +3 -3
- package/dist/src/utils/update-checker.test.js +5 -1
- package/dist/src/verify/__tests__/coverage-parsers.test.js +30 -30
- package/package.json +1 -1
|
@@ -43,12 +43,12 @@ export class SessionStore {
|
|
|
43
43
|
const now = new Date().toISOString();
|
|
44
44
|
const id = createSessionId();
|
|
45
45
|
const db = getDatabase();
|
|
46
|
-
db.prepare(`
|
|
47
|
-
INSERT INTO sessions (
|
|
48
|
-
id, workspace_id, title, model, mode, cwd_at_start, cwd_last, status, created_at, updated_at
|
|
49
|
-
) VALUES (
|
|
50
|
-
@id, @workspace_id, NULL, @model, @mode, @cwd_at_start, @cwd_last, 'active', @created_at, @updated_at
|
|
51
|
-
)
|
|
46
|
+
db.prepare(`
|
|
47
|
+
INSERT INTO sessions (
|
|
48
|
+
id, workspace_id, title, model, mode, cwd_at_start, cwd_last, status, created_at, updated_at
|
|
49
|
+
) VALUES (
|
|
50
|
+
@id, @workspace_id, NULL, @model, @mode, @cwd_at_start, @cwd_last, 'active', @created_at, @updated_at
|
|
51
|
+
)
|
|
52
52
|
`).run({
|
|
53
53
|
id,
|
|
54
54
|
workspace_id: this.workspace.id,
|
|
@@ -73,35 +73,35 @@ export class SessionStore {
|
|
|
73
73
|
*/
|
|
74
74
|
listRecentSessions(limit = 20) {
|
|
75
75
|
const rows = getDatabase()
|
|
76
|
-
.prepare(`
|
|
77
|
-
SELECT s.id, s.workspace_id, s.title, s.model, s.mode, s.cwd_at_start, s.cwd_last, s.status, s.created_at, s.updated_at
|
|
78
|
-
FROM sessions s
|
|
79
|
-
WHERE s.workspace_id = ?
|
|
80
|
-
AND EXISTS (SELECT 1 FROM messages m WHERE m.session_id = s.id)
|
|
81
|
-
ORDER BY s.updated_at DESC
|
|
82
|
-
LIMIT ?
|
|
76
|
+
.prepare(`
|
|
77
|
+
SELECT s.id, s.workspace_id, s.title, s.model, s.mode, s.cwd_at_start, s.cwd_last, s.status, s.created_at, s.updated_at
|
|
78
|
+
FROM sessions s
|
|
79
|
+
WHERE s.workspace_id = ?
|
|
80
|
+
AND EXISTS (SELECT 1 FROM messages m WHERE m.session_id = s.id)
|
|
81
|
+
ORDER BY s.updated_at DESC
|
|
82
|
+
LIMIT ?
|
|
83
83
|
`)
|
|
84
84
|
.all(this.workspace.id, limit);
|
|
85
85
|
return rows.map(toSessionInfo);
|
|
86
86
|
}
|
|
87
87
|
getLatestSession() {
|
|
88
88
|
const row = getDatabase()
|
|
89
|
-
.prepare(`
|
|
90
|
-
SELECT id, workspace_id, title, model, mode, cwd_at_start, cwd_last, status, created_at, updated_at
|
|
91
|
-
FROM sessions
|
|
92
|
-
WHERE workspace_id = ?
|
|
93
|
-
ORDER BY updated_at DESC
|
|
94
|
-
LIMIT 1
|
|
89
|
+
.prepare(`
|
|
90
|
+
SELECT id, workspace_id, title, model, mode, cwd_at_start, cwd_last, status, created_at, updated_at
|
|
91
|
+
FROM sessions
|
|
92
|
+
WHERE workspace_id = ?
|
|
93
|
+
ORDER BY updated_at DESC
|
|
94
|
+
LIMIT 1
|
|
95
95
|
`)
|
|
96
96
|
.get(this.workspace.id);
|
|
97
97
|
return row ? toSessionInfo(row) : null;
|
|
98
98
|
}
|
|
99
99
|
getSessionById(id) {
|
|
100
100
|
const row = getDatabase()
|
|
101
|
-
.prepare(`
|
|
102
|
-
SELECT id, workspace_id, title, model, mode, cwd_at_start, cwd_last, status, created_at, updated_at
|
|
103
|
-
FROM sessions
|
|
104
|
-
WHERE id = ?
|
|
101
|
+
.prepare(`
|
|
102
|
+
SELECT id, workspace_id, title, model, mode, cwd_at_start, cwd_last, status, created_at, updated_at
|
|
103
|
+
FROM sessions
|
|
104
|
+
WHERE id = ?
|
|
105
105
|
`)
|
|
106
106
|
.get(id);
|
|
107
107
|
return row ? toSessionInfo(row) : null;
|
|
@@ -116,49 +116,49 @@ export class SessionStore {
|
|
|
116
116
|
setStatus(id, status) {
|
|
117
117
|
const now = new Date().toISOString();
|
|
118
118
|
getDatabase()
|
|
119
|
-
.prepare(`
|
|
120
|
-
UPDATE sessions
|
|
121
|
-
SET status = ?, updated_at = ?
|
|
122
|
-
WHERE id = ?
|
|
119
|
+
.prepare(`
|
|
120
|
+
UPDATE sessions
|
|
121
|
+
SET status = ?, updated_at = ?
|
|
122
|
+
WHERE id = ?
|
|
123
123
|
`)
|
|
124
124
|
.run(status, now, id);
|
|
125
125
|
}
|
|
126
126
|
setTitle(id, title) {
|
|
127
127
|
const now = new Date().toISOString();
|
|
128
128
|
getDatabase()
|
|
129
|
-
.prepare(`
|
|
130
|
-
UPDATE sessions
|
|
131
|
-
SET title = ?, updated_at = ?
|
|
132
|
-
WHERE id = ?
|
|
129
|
+
.prepare(`
|
|
130
|
+
UPDATE sessions
|
|
131
|
+
SET title = ?, updated_at = ?
|
|
132
|
+
WHERE id = ?
|
|
133
133
|
`)
|
|
134
134
|
.run(title, now, id);
|
|
135
135
|
}
|
|
136
136
|
setModel(id, model) {
|
|
137
137
|
const now = new Date().toISOString();
|
|
138
138
|
getDatabase()
|
|
139
|
-
.prepare(`
|
|
140
|
-
UPDATE sessions
|
|
141
|
-
SET model = ?, updated_at = ?
|
|
142
|
-
WHERE id = ?
|
|
139
|
+
.prepare(`
|
|
140
|
+
UPDATE sessions
|
|
141
|
+
SET model = ?, updated_at = ?
|
|
142
|
+
WHERE id = ?
|
|
143
143
|
`)
|
|
144
144
|
.run(model, now, id);
|
|
145
145
|
}
|
|
146
146
|
setMode(id, mode) {
|
|
147
147
|
const now = new Date().toISOString();
|
|
148
148
|
getDatabase()
|
|
149
|
-
.prepare(`
|
|
150
|
-
UPDATE sessions
|
|
151
|
-
SET mode = ?, updated_at = ?
|
|
152
|
-
WHERE id = ?
|
|
149
|
+
.prepare(`
|
|
150
|
+
UPDATE sessions
|
|
151
|
+
SET mode = ?, updated_at = ?
|
|
152
|
+
WHERE id = ?
|
|
153
153
|
`)
|
|
154
154
|
.run(mode, now, id);
|
|
155
155
|
}
|
|
156
156
|
touchSession(id, cwd) {
|
|
157
157
|
getDatabase()
|
|
158
|
-
.prepare(`
|
|
159
|
-
UPDATE sessions
|
|
160
|
-
SET cwd_last = ?, updated_at = ?
|
|
161
|
-
WHERE id = ?
|
|
158
|
+
.prepare(`
|
|
159
|
+
UPDATE sessions
|
|
160
|
+
SET cwd_last = ?, updated_at = ?
|
|
161
|
+
WHERE id = ?
|
|
162
162
|
`)
|
|
163
163
|
.run(cwd, new Date().toISOString(), id);
|
|
164
164
|
}
|
|
@@ -18,11 +18,24 @@ import { afterEach, describe, expect, it, vi } from "vitest";
|
|
|
18
18
|
let MESSAGE_ROWS = [];
|
|
19
19
|
vi.mock("./db", () => ({
|
|
20
20
|
getDatabase: () => ({
|
|
21
|
-
prepare: (sql) =>
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
21
|
+
prepare: (sql) => {
|
|
22
|
+
const lower = sql.toLowerCase();
|
|
23
|
+
return {
|
|
24
|
+
all: (...params) => {
|
|
25
|
+
if (lower.includes("from messages")) {
|
|
26
|
+
return MESSAGE_ROWS;
|
|
27
|
+
}
|
|
28
|
+
if (lower.includes("from sessions")) {
|
|
29
|
+
if (lower.includes("where id in")) {
|
|
30
|
+
return params.map((id) => ({ id }));
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return [];
|
|
34
|
+
},
|
|
35
|
+
get: () => undefined,
|
|
36
|
+
run: () => undefined,
|
|
37
|
+
};
|
|
38
|
+
},
|
|
26
39
|
}),
|
|
27
40
|
withTransaction: (fn) => fn({}),
|
|
28
41
|
}));
|
|
@@ -104,5 +104,7 @@ export declare function sweepStalePendingRows(staleAfterMs?: number): {
|
|
|
104
104
|
};
|
|
105
105
|
export declare function markToolCallErrored(sessionId: string, toolCallId: string, errorMessage: string): void;
|
|
106
106
|
export declare function appendCompaction(sessionId: string, firstKeptSeq: number, summary: string, tokensBefore: number): void;
|
|
107
|
+
export declare function revertLatestCompaction(sessionId: string): void;
|
|
108
|
+
export declare function getSessionChain(sessionId: string): string[];
|
|
107
109
|
export declare function buildChatEntries(sessionId: string): ChatEntry[];
|
|
108
110
|
export declare function getLastTodoWriteArgs(sessionId: string): string | null;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { getCompactionSummaryText } from "../orchestrator/compaction.js";
|
|
2
2
|
import { getResponseTaskType, isResponseTool } from "../pil/response-tools.js";
|
|
3
|
+
import { logger } from "../utils/logger.js";
|
|
3
4
|
import { getDatabase, withTransaction } from "./db.js";
|
|
4
5
|
import { extractToolResultFromOutput, getOutputKind, isOutputSuccess } from "./tool-results.js";
|
|
5
6
|
import { buildEffectiveTranscript } from "./transcript-view.js";
|
|
@@ -327,16 +328,15 @@ export function markMessageErrored(sessionId, seq) {
|
|
|
327
328
|
*/
|
|
328
329
|
export function sweepStalePendingRows(staleAfterMs = 5 * 60 * 1000) {
|
|
329
330
|
const cutoff = new Date(Date.now() - staleAfterMs).toISOString();
|
|
330
|
-
const now = new Date().toISOString();
|
|
331
331
|
try {
|
|
332
332
|
const db = getDatabase();
|
|
333
333
|
const toolCalls = db
|
|
334
334
|
.prepare(`
|
|
335
335
|
UPDATE tool_calls
|
|
336
|
-
SET status = 'aborted', completed_at =
|
|
336
|
+
SET status = 'aborted', completed_at = started_at
|
|
337
337
|
WHERE status = 'pending' AND started_at < ?
|
|
338
338
|
`)
|
|
339
|
-
.run(
|
|
339
|
+
.run(cutoff);
|
|
340
340
|
const messages = db
|
|
341
341
|
.prepare(`
|
|
342
342
|
UPDATE messages
|
|
@@ -379,98 +379,204 @@ export function appendCompaction(sessionId, firstKeptSeq, summary, tokensBefore)
|
|
|
379
379
|
`).run(new Date().toISOString(), sessionId);
|
|
380
380
|
});
|
|
381
381
|
}
|
|
382
|
+
export function revertLatestCompaction(sessionId) {
|
|
383
|
+
withTransaction((db) => {
|
|
384
|
+
db.prepare(`
|
|
385
|
+
DELETE FROM compactions
|
|
386
|
+
WHERE session_id = ?
|
|
387
|
+
AND id = (SELECT MAX(id) FROM compactions WHERE session_id = ?)
|
|
388
|
+
`).run(sessionId, sessionId);
|
|
389
|
+
db.prepare(`
|
|
390
|
+
UPDATE sessions
|
|
391
|
+
SET updated_at = ?
|
|
392
|
+
WHERE id = ?
|
|
393
|
+
`).run(new Date().toISOString(), sessionId);
|
|
394
|
+
});
|
|
395
|
+
}
|
|
396
|
+
export function getSessionChain(sessionId) {
|
|
397
|
+
logger.debug("storage", "Resolving session chain", { sessionId });
|
|
398
|
+
const db = getDatabase();
|
|
399
|
+
// 1. Walk up to the root parent
|
|
400
|
+
let rootId = sessionId;
|
|
401
|
+
const visitedUp = new Set();
|
|
402
|
+
while (rootId && !visitedUp.has(rootId)) {
|
|
403
|
+
visitedUp.add(rootId);
|
|
404
|
+
try {
|
|
405
|
+
const row = db.prepare("SELECT parent_session_id FROM sessions WHERE id = ?").get(rootId);
|
|
406
|
+
if (row?.parent_session_id) {
|
|
407
|
+
rootId = row.parent_session_id;
|
|
408
|
+
}
|
|
409
|
+
else {
|
|
410
|
+
break;
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
catch {
|
|
414
|
+
break;
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
// 2. Find all descendants of the root session using a BFS queue to handle multiple child branches
|
|
418
|
+
const allIds = new Set([rootId]);
|
|
419
|
+
const queue = [rootId];
|
|
420
|
+
while (queue.length > 0) {
|
|
421
|
+
const currentId = queue.shift();
|
|
422
|
+
try {
|
|
423
|
+
const children = db.prepare("SELECT id FROM sessions WHERE parent_session_id = ?").all(currentId);
|
|
424
|
+
for (const child of children) {
|
|
425
|
+
if (!allIds.has(child.id)) {
|
|
426
|
+
allIds.add(child.id);
|
|
427
|
+
queue.push(child.id);
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
catch (err) {
|
|
432
|
+
logger.error("storage", `Failed to query child sessions for ${currentId} during walk`, { error: err });
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
// 3. Sort chronologically by querying the database for all matched sessions ordered by created_at
|
|
436
|
+
let chain = [];
|
|
437
|
+
try {
|
|
438
|
+
const placeholders = Array.from(allIds)
|
|
439
|
+
.map(() => "?")
|
|
440
|
+
.join(",");
|
|
441
|
+
const sortedRows = db
|
|
442
|
+
.prepare(`
|
|
443
|
+
SELECT id FROM sessions
|
|
444
|
+
WHERE id IN (${placeholders})
|
|
445
|
+
ORDER BY created_at ASC
|
|
446
|
+
`)
|
|
447
|
+
.all(...Array.from(allIds));
|
|
448
|
+
chain = sortedRows.map((r) => r.id);
|
|
449
|
+
}
|
|
450
|
+
catch (err) {
|
|
451
|
+
logger.error("storage", `Failed to sort session chain for root ${rootId}`, { error: err });
|
|
452
|
+
// Fallback to unsorted array if query fails
|
|
453
|
+
chain = Array.from(allIds);
|
|
454
|
+
}
|
|
455
|
+
logger.debug("storage", "Resolved session chain successfully", { sessionId, chainLength: chain.length, chain });
|
|
456
|
+
return chain;
|
|
457
|
+
}
|
|
382
458
|
export function buildChatEntries(sessionId) {
|
|
383
|
-
const
|
|
459
|
+
const chain = getSessionChain(sessionId);
|
|
460
|
+
logger.debug("storage", "Building chat entries from session chain", { sessionId, chainLength: chain.length });
|
|
461
|
+
const toolResults = new Map();
|
|
462
|
+
for (const sid of chain) {
|
|
463
|
+
const results = loadStoredToolResults(sid);
|
|
464
|
+
for (const [k, v] of results) {
|
|
465
|
+
toolResults.set(k, v);
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
// Load parent/child session metadata to reconcile user prompt timestamps across rotation/sub-session boundaries
|
|
469
|
+
const db = getDatabase();
|
|
470
|
+
const sessionMeta = new Map();
|
|
471
|
+
try {
|
|
472
|
+
if (chain.length > 1) {
|
|
473
|
+
const placeholders = chain.map(() => "?").join(",");
|
|
474
|
+
const rows = db
|
|
475
|
+
.prepare(`SELECT id, parent_session_id, created_at FROM sessions WHERE id IN (${placeholders})`)
|
|
476
|
+
.all(...chain);
|
|
477
|
+
for (const r of rows) {
|
|
478
|
+
sessionMeta.set(r.id, { parent_session_id: r.parent_session_id, created_at: r.created_at });
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
catch (err) {
|
|
483
|
+
logger.error("storage", "Failed to load session metadata for chain in buildChatEntries", { error: err });
|
|
484
|
+
}
|
|
485
|
+
// Map each child sub-session's initial user prompt to its creation timestamp
|
|
486
|
+
const childPrompts = new Map();
|
|
487
|
+
for (let i = 1; i < chain.length; i++) {
|
|
488
|
+
const sid = chain[i];
|
|
489
|
+
const meta = sessionMeta.get(sid);
|
|
490
|
+
if (meta && meta.parent_session_id) {
|
|
491
|
+
const records = buildEffectiveMessageRecords(sid);
|
|
492
|
+
const firstUser = records.find((r) => r.message.role === "user");
|
|
493
|
+
if (firstUser) {
|
|
494
|
+
const content = renderUserContent(firstUser.message.content);
|
|
495
|
+
if (content) {
|
|
496
|
+
childPrompts.set(content, new Date(meta.created_at));
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
}
|
|
384
501
|
const callMap = new Map();
|
|
385
502
|
const entries = [];
|
|
386
|
-
// Response-tool callIds already rendered as a structured_response from a
|
|
387
|
-
// persisted tool-RESULT row (success path). After the loop we recover any
|
|
388
|
-
// response tool that was CALLED but produced no result row — i.e. respond_*
|
|
389
|
-
// whose execution ERRORED (the AI SDK does not persist tool-error parts as
|
|
390
|
-
// tool-result rows). Its answer lives in the call args; without this it is
|
|
391
|
-
// dropped on the turn-finalize rebuild and the user sees an empty turn or
|
|
392
|
-
// only the last council debate round.
|
|
393
503
|
const renderedResponseCallIds = new Set();
|
|
394
504
|
let lastTimestamp;
|
|
395
|
-
for (
|
|
396
|
-
const
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
505
|
+
for (let i = 0; i < chain.length; i++) {
|
|
506
|
+
const sid = chain[i];
|
|
507
|
+
const records = buildEffectiveMessageRecords(sid);
|
|
508
|
+
const isChildSession = i > 0;
|
|
509
|
+
for (let j = 0; j < records.length; j++) {
|
|
510
|
+
const { message, timestamp } = records[j];
|
|
511
|
+
lastTimestamp = timestamp;
|
|
512
|
+
if (message.role === "user") {
|
|
513
|
+
const content = renderUserContent(message.content);
|
|
514
|
+
if (isChildSession && childPrompts.has(content)) {
|
|
515
|
+
// Skip B's user message because it's a duplicate of A's user message
|
|
516
|
+
continue;
|
|
517
|
+
}
|
|
518
|
+
if (content) {
|
|
519
|
+
entries.push({ type: "user", content, timestamp });
|
|
520
|
+
}
|
|
521
|
+
continue;
|
|
402
522
|
}
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
523
|
+
if (message.role === "system") {
|
|
524
|
+
const summaryText = getCompactionSummaryText(message);
|
|
525
|
+
if (isChildSession && j === 0 && summaryText !== null) {
|
|
526
|
+
// Skip the first message in a child session if it's the parent compaction summary
|
|
527
|
+
// to keep the visual timeline clean, since the user already sees the parent history.
|
|
528
|
+
continue;
|
|
529
|
+
}
|
|
530
|
+
const content = summaryText ?? (typeof message.content === "string" ? message.content.trim() : "");
|
|
531
|
+
if (content && !isInternalCouncilMarker(content)) {
|
|
532
|
+
entries.push(summaryText !== null
|
|
533
|
+
? { type: "assistant", content, timestamp, sourceLabel: "⋯ context checkpoint (auto-compacted)" }
|
|
534
|
+
: { type: "assistant", content, timestamp });
|
|
535
|
+
}
|
|
536
|
+
continue;
|
|
416
537
|
}
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
538
|
+
if (message.role === "assistant") {
|
|
539
|
+
const text = renderAssistantContent(message.content, callMap);
|
|
540
|
+
if (text) {
|
|
541
|
+
entries.push({ type: "assistant", content: text, timestamp });
|
|
542
|
+
}
|
|
543
|
+
continue;
|
|
423
544
|
}
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
const
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
: (
|
|
545
|
+
if (message.role === "tool" && Array.isArray(message.content)) {
|
|
546
|
+
for (const part of message.content) {
|
|
547
|
+
if (part.type !== "tool-result")
|
|
548
|
+
continue;
|
|
549
|
+
if (isResponseTool(part.toolName)) {
|
|
550
|
+
renderedResponseCallIds.add(part.toolCallId);
|
|
551
|
+
const rawOutput = part.output;
|
|
552
|
+
const unwrapped = rawOutput && typeof rawOutput === "object" && rawOutput.type === "json"
|
|
553
|
+
? (rawOutput.value ?? {})
|
|
554
|
+
: (rawOutput ?? {});
|
|
555
|
+
entries.push({
|
|
556
|
+
type: "structured_response",
|
|
557
|
+
content: "",
|
|
558
|
+
timestamp,
|
|
559
|
+
structuredResponse: {
|
|
560
|
+
taskType: getResponseTaskType(part.toolName) ?? part.toolName,
|
|
561
|
+
data: unwrapped,
|
|
562
|
+
},
|
|
563
|
+
});
|
|
564
|
+
continue;
|
|
565
|
+
}
|
|
566
|
+
const toolCall = callMap.get(part.toolCallId) ?? toFallbackToolCall(part.toolCallId, part.toolName);
|
|
567
|
+
const toolResult = toolResults.get(part.toolCallId) ??
|
|
568
|
+
extractToolResultFromOutput(part.output) ?? {
|
|
569
|
+
success: isOutputSuccess(part.output),
|
|
570
|
+
output: JSON.stringify(part.output),
|
|
571
|
+
};
|
|
450
572
|
entries.push({
|
|
451
|
-
type: "
|
|
452
|
-
content: "",
|
|
573
|
+
type: "tool_result",
|
|
574
|
+
content: toolResult.success ? toolResult.output || "Success" : toolResult.error || "Error",
|
|
453
575
|
timestamp,
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
data: unwrapped,
|
|
457
|
-
},
|
|
576
|
+
toolCall,
|
|
577
|
+
toolResult,
|
|
458
578
|
});
|
|
459
|
-
continue;
|
|
460
579
|
}
|
|
461
|
-
const toolCall = callMap.get(part.toolCallId) ?? toFallbackToolCall(part.toolCallId, part.toolName);
|
|
462
|
-
const toolResult = toolResults.get(part.toolCallId) ??
|
|
463
|
-
extractToolResultFromOutput(part.output) ?? {
|
|
464
|
-
success: isOutputSuccess(part.output),
|
|
465
|
-
output: JSON.stringify(part.output),
|
|
466
|
-
};
|
|
467
|
-
entries.push({
|
|
468
|
-
type: "tool_result",
|
|
469
|
-
content: toolResult.success ? toolResult.output || "Success" : toolResult.error || "Error",
|
|
470
|
-
timestamp,
|
|
471
|
-
toolCall,
|
|
472
|
-
toolResult,
|
|
473
|
-
});
|
|
474
580
|
}
|
|
475
581
|
}
|
|
476
582
|
}
|
|
@@ -501,6 +607,19 @@ export function buildChatEntries(sessionId) {
|
|
|
501
607
|
},
|
|
502
608
|
});
|
|
503
609
|
}
|
|
610
|
+
// Adjust parent user message timestamps to match their corresponding child session creation time
|
|
611
|
+
for (const entry of entries) {
|
|
612
|
+
if (entry.type === "user") {
|
|
613
|
+
const content = entry.content.trim();
|
|
614
|
+
const childTime = childPrompts.get(content);
|
|
615
|
+
if (childTime) {
|
|
616
|
+
entry.timestamp = childTime;
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
// Sort entries chronologically by timestamp (stable sort)
|
|
621
|
+
entries.sort((a, b) => a.timestamp.getTime() - b.timestamp.getTime());
|
|
622
|
+
logger.debug("storage", "Built chat entries successfully", { sessionId, count: entries.length });
|
|
504
623
|
return entries;
|
|
505
624
|
}
|
|
506
625
|
function getNextSequence(db, sessionId) {
|
|
@@ -579,20 +698,27 @@ function toFallbackToolCall(toolCallId, toolName) {
|
|
|
579
698
|
};
|
|
580
699
|
}
|
|
581
700
|
export function getLastTodoWriteArgs(sessionId) {
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
701
|
+
const chain = getSessionChain(sessionId);
|
|
702
|
+
const db = getDatabase();
|
|
703
|
+
for (let i = chain.length - 1; i >= 0; i--) {
|
|
704
|
+
try {
|
|
705
|
+
const row = db
|
|
706
|
+
.prepare(`
|
|
707
|
+
SELECT args_json
|
|
708
|
+
FROM tool_calls
|
|
709
|
+
WHERE session_id = ? AND tool_name = 'todo_write'
|
|
710
|
+
ORDER BY id DESC
|
|
711
|
+
LIMIT 1
|
|
712
|
+
`)
|
|
713
|
+
.get(chain[i]);
|
|
714
|
+
if (row?.args_json) {
|
|
715
|
+
return row.args_json;
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
catch {
|
|
719
|
+
// ignore and check parent
|
|
720
|
+
}
|
|
596
721
|
}
|
|
722
|
+
return null;
|
|
597
723
|
}
|
|
598
724
|
//# sourceMappingURL=transcript.js.map
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { getModelInfo } from "../models/registry.js";
|
|
2
|
+
import { logger } from "../utils/logger.js";
|
|
2
3
|
import { getDatabase } from "./db.js";
|
|
4
|
+
import { getSessionChain } from "./transcript.js";
|
|
3
5
|
export function recordUsageEvent(sessionId, source, model, usage, messageSeq, pilActive = false, enrichmentDelta = 0, providerOptionsShape = null) {
|
|
4
6
|
if (!usage)
|
|
5
7
|
return;
|
|
@@ -12,33 +14,48 @@ export function recordUsageEvent(sessionId, source, model, usage, messageSeq, pi
|
|
|
12
14
|
const cacheCreationTokens = usage.cacheCreationTokens ?? 0;
|
|
13
15
|
const costMicros = estimateCostMicros(model, inputTokens, outputTokens, cacheReadTokens);
|
|
14
16
|
getDatabase()
|
|
15
|
-
.prepare(`
|
|
16
|
-
INSERT INTO usage_events (
|
|
17
|
-
session_id, message_seq, source, model, input_tokens, output_tokens, total_tokens, cost_micros, created_at,
|
|
18
|
-
pil_active, enrichment_delta, cache_read_tokens, cache_creation_tokens, provider_options_shape
|
|
19
|
-
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
17
|
+
.prepare(`
|
|
18
|
+
INSERT INTO usage_events (
|
|
19
|
+
session_id, message_seq, source, model, input_tokens, output_tokens, total_tokens, cost_micros, created_at,
|
|
20
|
+
pil_active, enrichment_delta, cache_read_tokens, cache_creation_tokens, provider_options_shape
|
|
21
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
20
22
|
`)
|
|
21
23
|
.run(sessionId, messageSeq ?? null, source, model, inputTokens, outputTokens, totalTokens, costMicros, new Date().toISOString(), pilActive ? 1 : 0, enrichmentDelta, cacheReadTokens, cacheCreationTokens, providerOptionsShape);
|
|
22
24
|
}
|
|
23
25
|
export function getSessionTotalTokens(sessionId) {
|
|
26
|
+
const chain = getSessionChain(sessionId);
|
|
27
|
+
const placeholders = chain.map(() => "?").join(",");
|
|
24
28
|
const row = getDatabase()
|
|
25
|
-
.prepare(`
|
|
26
|
-
SELECT COALESCE(SUM(total_tokens), 0) AS total_tokens
|
|
27
|
-
FROM usage_events
|
|
28
|
-
WHERE session_id
|
|
29
|
+
.prepare(`
|
|
30
|
+
SELECT COALESCE(SUM(total_tokens), 0) AS total_tokens
|
|
31
|
+
FROM usage_events
|
|
32
|
+
WHERE session_id IN (${placeholders})
|
|
29
33
|
`)
|
|
30
|
-
.get(
|
|
31
|
-
|
|
34
|
+
.get(...chain);
|
|
35
|
+
const total = row?.total_tokens ?? 0;
|
|
36
|
+
logger.debug("storage", "Calculated session total tokens from chain", {
|
|
37
|
+
sessionId,
|
|
38
|
+
chainLength: chain.length,
|
|
39
|
+
totalTokens: total,
|
|
40
|
+
});
|
|
41
|
+
return total;
|
|
32
42
|
}
|
|
33
43
|
export function listSessionUsage(sessionId) {
|
|
44
|
+
const chain = getSessionChain(sessionId);
|
|
45
|
+
const placeholders = chain.map(() => "?").join(",");
|
|
34
46
|
const rows = getDatabase()
|
|
35
|
-
.prepare(`
|
|
36
|
-
SELECT id, session_id, message_seq, source, model, input_tokens, output_tokens, total_tokens, cost_micros, created_at, cache_read_tokens, cache_creation_tokens
|
|
37
|
-
FROM usage_events
|
|
38
|
-
WHERE session_id
|
|
39
|
-
ORDER BY id ASC
|
|
47
|
+
.prepare(`
|
|
48
|
+
SELECT id, session_id, message_seq, source, model, input_tokens, output_tokens, total_tokens, cost_micros, created_at, cache_read_tokens, cache_creation_tokens
|
|
49
|
+
FROM usage_events
|
|
50
|
+
WHERE session_id IN (${placeholders})
|
|
51
|
+
ORDER BY id ASC
|
|
40
52
|
`)
|
|
41
|
-
.all(
|
|
53
|
+
.all(...chain);
|
|
54
|
+
logger.debug("storage", "Retrieved session usage events from chain", {
|
|
55
|
+
sessionId,
|
|
56
|
+
chainLength: chain.length,
|
|
57
|
+
eventCount: rows.length,
|
|
58
|
+
});
|
|
42
59
|
return rows.map((row) => ({
|
|
43
60
|
id: row.id,
|
|
44
61
|
sessionId: row.session_id,
|