mini-coder 0.5.12 → 0.5.14
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/BENCHMARK.md +15 -316
- package/PROGRESS.md +3 -3
- package/README.md +54 -21
- package/benchmark-baseline.sh +15 -0
- package/bun.lock +265 -90
- package/package.json +8 -7
- package/skills-lock.json +15 -0
- package/src/agent.ts +526 -13
- package/src/assistant-output.ts +73 -0
- package/src/cli.ts +2 -1
- package/src/delegation.ts +238 -0
- package/src/headless.ts +90 -44
- package/src/index.ts +267 -102
- package/src/input.ts +13 -1
- package/src/mcp.ts +609 -0
- package/src/prompt.ts +11 -13
- package/src/session-message.ts +57 -65
- package/src/session.ts +389 -42
- package/src/settings.ts +199 -7
- package/src/skills.ts +12 -3
- package/src/submit.ts +24 -2
- package/src/theme.ts +186 -3
- package/src/tool-common.ts +2 -0
- package/src/tool-delegate.ts +125 -0
- package/src/tool-shell.ts +190 -8
- package/src/tools.ts +335 -6
- package/src/ui/agent.ts +10 -0
- package/src/ui/commands.test.ts +525 -10
- package/src/ui/commands.ts +224 -8
- package/src/ui/conversation.test.ts +252 -27
- package/src/ui/conversation.ts +468 -390
- package/src/ui/help.ts +27 -11
- package/src/ui.ts +230 -75
- package/src/plugins.ts +0 -183
package/src/session.ts
CHANGED
|
@@ -10,12 +10,17 @@
|
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
12
|
import { Database } from "bun:sqlite";
|
|
13
|
-
import type {
|
|
13
|
+
import type {
|
|
14
|
+
AssistantMessage,
|
|
15
|
+
Message,
|
|
16
|
+
UserMessage,
|
|
17
|
+
} from "@mariozechner/pi-ai";
|
|
14
18
|
import {
|
|
15
19
|
getAssistantUsage,
|
|
16
20
|
isUiMessage,
|
|
17
21
|
type PersistedMessage,
|
|
18
22
|
parsePersistedMessage,
|
|
23
|
+
readAssistantUsage,
|
|
19
24
|
readFirstUserPreview,
|
|
20
25
|
type UiInfoFormat,
|
|
21
26
|
type UiInfoMessage,
|
|
@@ -86,6 +91,28 @@ export interface SessionStats {
|
|
|
86
91
|
totalCost: number;
|
|
87
92
|
}
|
|
88
93
|
|
|
94
|
+
/** One persisted synthetic summary that replaces an older compacted context prefix. */
|
|
95
|
+
export interface SessionCompaction {
|
|
96
|
+
/** Monotonic compaction row id. */
|
|
97
|
+
id: number;
|
|
98
|
+
/** Session that owns this compaction. */
|
|
99
|
+
sessionId: string;
|
|
100
|
+
/** Message-row id of the last raw message covered by the summary. */
|
|
101
|
+
messageEndId: number;
|
|
102
|
+
/** Synthetic user message injected into future model context. */
|
|
103
|
+
summaryMessage: UserMessage;
|
|
104
|
+
/** Unix timestamp in milliseconds when the compaction was created. */
|
|
105
|
+
createdAt: number;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** One uncompacted model-visible session message paired with its SQLite row id. */
|
|
109
|
+
export interface SessionModelMessageRow {
|
|
110
|
+
/** Monotonic message row id. */
|
|
111
|
+
id: number;
|
|
112
|
+
/** Persisted model-visible message. */
|
|
113
|
+
message: Message;
|
|
114
|
+
}
|
|
115
|
+
|
|
89
116
|
/** A raw submitted prompt stored for global input-history search. */
|
|
90
117
|
interface PromptHistoryEntry {
|
|
91
118
|
/** Monotonic row id. */
|
|
@@ -143,6 +170,28 @@ type SessionListRow = SessionRow & {
|
|
|
143
170
|
/** Row shape for `SELECT MAX(turn)` queries. */
|
|
144
171
|
type MaxTurnRow = { max_turn: number | null };
|
|
145
172
|
|
|
173
|
+
/** Row shape for `SELECT id, data` message queries. */
|
|
174
|
+
type StoredMessageRow = {
|
|
175
|
+
id: number;
|
|
176
|
+
data: string;
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
/** Row shape returned by `SELECT * FROM session_compactions`. */
|
|
180
|
+
type SessionCompactionRow = {
|
|
181
|
+
id: number;
|
|
182
|
+
session_id: string;
|
|
183
|
+
message_end_id: number;
|
|
184
|
+
summary_data: string;
|
|
185
|
+
usage_data: string | null;
|
|
186
|
+
created_at: number;
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
/** Row shape for `SELECT message_end_id` compaction queries. */
|
|
190
|
+
type CompactionEndRow = { message_end_id: number };
|
|
191
|
+
|
|
192
|
+
/** Row shape returned by `SELECT id` message queries. */
|
|
193
|
+
type MessageIdRow = { id: number };
|
|
194
|
+
|
|
146
195
|
/** Row shape for `SELECT data` queries. */
|
|
147
196
|
type DataRow = { data: string };
|
|
148
197
|
|
|
@@ -178,6 +227,15 @@ const SQL = {
|
|
|
178
227
|
`,
|
|
179
228
|
maxTurn: "SELECT MAX(turn) as max_turn FROM messages WHERE session_id = ?",
|
|
180
229
|
loadMessages: "SELECT data FROM messages WHERE session_id = ? ORDER BY id",
|
|
230
|
+
loadStoredMessagesAfterId:
|
|
231
|
+
"SELECT id, data FROM messages WHERE session_id = ? AND id > ? ORDER BY id",
|
|
232
|
+
listCompactions:
|
|
233
|
+
"SELECT * FROM session_compactions WHERE session_id = ? ORDER BY message_end_id, id",
|
|
234
|
+
latestCompactionEnd:
|
|
235
|
+
"SELECT message_end_id FROM session_compactions WHERE session_id = ? ORDER BY message_end_id DESC, id DESC LIMIT 1",
|
|
236
|
+
firstMessageIdForTurn:
|
|
237
|
+
"SELECT id FROM messages WHERE session_id = ? AND turn = ? ORDER BY id LIMIT 1",
|
|
238
|
+
listMessageIds: "SELECT id FROM messages WHERE session_id = ? ORDER BY id",
|
|
181
239
|
listPromptHistory:
|
|
182
240
|
"SELECT * FROM prompt_history ORDER BY created_at DESC, id DESC LIMIT ?",
|
|
183
241
|
} as const;
|
|
@@ -205,6 +263,17 @@ CREATE TABLE IF NOT EXISTS messages (
|
|
|
205
263
|
|
|
206
264
|
CREATE INDEX IF NOT EXISTS idx_messages_session ON messages(session_id, turn);
|
|
207
265
|
|
|
266
|
+
CREATE TABLE IF NOT EXISTS session_compactions (
|
|
267
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
268
|
+
session_id TEXT NOT NULL REFERENCES sessions(id) ON DELETE CASCADE,
|
|
269
|
+
message_end_id INTEGER NOT NULL,
|
|
270
|
+
summary_data TEXT NOT NULL,
|
|
271
|
+
usage_data TEXT,
|
|
272
|
+
created_at INTEGER NOT NULL
|
|
273
|
+
);
|
|
274
|
+
|
|
275
|
+
CREATE INDEX IF NOT EXISTS idx_session_compactions_session ON session_compactions(session_id, message_end_id);
|
|
276
|
+
|
|
208
277
|
CREATE TABLE IF NOT EXISTS prompt_history (
|
|
209
278
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
210
279
|
text TEXT NOT NULL,
|
|
@@ -238,12 +307,26 @@ CREATE INDEX IF NOT EXISTS idx_prompt_history_created_at ON prompt_history(creat
|
|
|
238
307
|
* db.close();
|
|
239
308
|
* ```
|
|
240
309
|
*/
|
|
310
|
+
function ensureSessionCompactionUsageColumn(db: Database): void {
|
|
311
|
+
try {
|
|
312
|
+
db.run("ALTER TABLE session_compactions ADD COLUMN usage_data TEXT");
|
|
313
|
+
} catch (error) {
|
|
314
|
+
if (
|
|
315
|
+
!(error instanceof Error) ||
|
|
316
|
+
!error.message.includes("duplicate column name: usage_data")
|
|
317
|
+
) {
|
|
318
|
+
throw error;
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
241
323
|
export function openDatabase(path: string): Database {
|
|
242
324
|
const db = new Database(path);
|
|
243
325
|
db.run("PRAGMA journal_mode = WAL");
|
|
244
326
|
db.run(`PRAGMA busy_timeout = ${SQLITE_BUSY_TIMEOUT_MS}`);
|
|
245
327
|
db.run("PRAGMA foreign_keys = ON");
|
|
246
328
|
db.exec(SCHEMA);
|
|
329
|
+
ensureSessionCompactionUsageColumn(db);
|
|
247
330
|
return db;
|
|
248
331
|
}
|
|
249
332
|
|
|
@@ -438,6 +521,175 @@ function runInImmediateTransaction<T>(db: Database, callback: () => T): T {
|
|
|
438
521
|
}
|
|
439
522
|
}
|
|
440
523
|
|
|
524
|
+
function parseCompactionSummaryMessage(data: string): UserMessage | null {
|
|
525
|
+
const message = parsePersistedMessage(data);
|
|
526
|
+
return message?.role === "user" ? message : null;
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
function parseCompactionUsage(
|
|
530
|
+
data: string | null,
|
|
531
|
+
): AssistantMessage["usage"] | null {
|
|
532
|
+
if (!data) {
|
|
533
|
+
return null;
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
try {
|
|
537
|
+
return readAssistantUsage(JSON.parse(data) as unknown);
|
|
538
|
+
} catch {
|
|
539
|
+
return null;
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
function getLatestCompactionEndId(db: Database, sessionId: string): number {
|
|
544
|
+
const row = db
|
|
545
|
+
.query<CompactionEndRow, [string]>(SQL.latestCompactionEnd)
|
|
546
|
+
.get(sessionId);
|
|
547
|
+
return row?.message_end_id ?? 0;
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
function listMessageIds(db: Database, sessionId: string): number[] {
|
|
551
|
+
return db
|
|
552
|
+
.query<MessageIdRow, [string]>(SQL.listMessageIds)
|
|
553
|
+
.all(sessionId)
|
|
554
|
+
.map((row) => row.id);
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
/**
|
|
558
|
+
* List persisted compaction summaries for a session in replacement order.
|
|
559
|
+
*
|
|
560
|
+
* @param db - Open database handle.
|
|
561
|
+
* @param sessionId - Session whose compactions should be listed.
|
|
562
|
+
* @returns Parsed compaction summaries ordered by covered message range.
|
|
563
|
+
*/
|
|
564
|
+
export function listSessionCompactions(
|
|
565
|
+
db: Database,
|
|
566
|
+
sessionId: string,
|
|
567
|
+
): SessionCompaction[] {
|
|
568
|
+
const rows = db
|
|
569
|
+
.query<SessionCompactionRow, [string]>(SQL.listCompactions)
|
|
570
|
+
.all(sessionId);
|
|
571
|
+
const compactions: SessionCompaction[] = [];
|
|
572
|
+
|
|
573
|
+
for (const row of rows) {
|
|
574
|
+
const summaryMessage = parseCompactionSummaryMessage(row.summary_data);
|
|
575
|
+
if (!summaryMessage) {
|
|
576
|
+
continue;
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
compactions.push({
|
|
580
|
+
id: row.id,
|
|
581
|
+
sessionId: row.session_id,
|
|
582
|
+
messageEndId: row.message_end_id,
|
|
583
|
+
summaryMessage,
|
|
584
|
+
createdAt: row.created_at,
|
|
585
|
+
});
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
return compactions;
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
/**
|
|
592
|
+
* Persist one synthetic compaction summary for future model-context rebuilds.
|
|
593
|
+
*
|
|
594
|
+
* @param db - Open database handle.
|
|
595
|
+
* @param sessionId - Session that owns the compaction.
|
|
596
|
+
* @param messageEndId - Last raw message-row id covered by the summary.
|
|
597
|
+
* @param summaryMessage - Synthetic user message that replaces that prefix in context.
|
|
598
|
+
* @param summaryUsage - Optional usage for the hidden summarization model call.
|
|
599
|
+
* @returns The stored {@link SessionCompaction}.
|
|
600
|
+
*/
|
|
601
|
+
export function appendSessionCompaction(
|
|
602
|
+
db: Database,
|
|
603
|
+
sessionId: string,
|
|
604
|
+
messageEndId: number,
|
|
605
|
+
summaryMessage: UserMessage,
|
|
606
|
+
summaryUsage?: AssistantMessage["usage"],
|
|
607
|
+
): SessionCompaction {
|
|
608
|
+
return runInImmediateTransaction(db, () => {
|
|
609
|
+
const now = Date.now();
|
|
610
|
+
const result = db.run(
|
|
611
|
+
"INSERT INTO session_compactions (session_id, message_end_id, summary_data, usage_data, created_at) VALUES (?, ?, ?, ?, ?)",
|
|
612
|
+
[
|
|
613
|
+
sessionId,
|
|
614
|
+
messageEndId,
|
|
615
|
+
JSON.stringify(summaryMessage),
|
|
616
|
+
summaryUsage ? JSON.stringify(summaryUsage) : null,
|
|
617
|
+
now,
|
|
618
|
+
],
|
|
619
|
+
);
|
|
620
|
+
|
|
621
|
+
return {
|
|
622
|
+
id: Number(result.lastInsertRowid),
|
|
623
|
+
sessionId,
|
|
624
|
+
messageEndId,
|
|
625
|
+
summaryMessage,
|
|
626
|
+
createdAt: now,
|
|
627
|
+
};
|
|
628
|
+
});
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
/**
|
|
632
|
+
* Load raw model-visible messages that have not yet been compacted.
|
|
633
|
+
*
|
|
634
|
+
* @param db - Open database handle.
|
|
635
|
+
* @param sessionId - Session whose uncompacted message tail should be loaded.
|
|
636
|
+
* @returns Model-visible message rows after the latest compaction boundary.
|
|
637
|
+
*/
|
|
638
|
+
export function loadUncompactedModelMessages(
|
|
639
|
+
db: Database,
|
|
640
|
+
sessionId: string,
|
|
641
|
+
): SessionModelMessageRow[] {
|
|
642
|
+
const latestCompactionEndId = getLatestCompactionEndId(db, sessionId);
|
|
643
|
+
const rows = db
|
|
644
|
+
.query<StoredMessageRow, [string, number]>(SQL.loadStoredMessagesAfterId)
|
|
645
|
+
.all(sessionId, latestCompactionEndId);
|
|
646
|
+
const messages: SessionModelMessageRow[] = [];
|
|
647
|
+
|
|
648
|
+
for (const row of rows) {
|
|
649
|
+
const message = parsePersistedMessage(row.data);
|
|
650
|
+
if (!message || isUiMessage(message)) {
|
|
651
|
+
continue;
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
messages.push({ id: row.id, message });
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
return messages;
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
/**
|
|
661
|
+
* Rebuild the model-visible session context using persisted compaction summaries.
|
|
662
|
+
*
|
|
663
|
+
* @param db - Open database handle.
|
|
664
|
+
* @param sessionId - Session whose compacted model context should be loaded.
|
|
665
|
+
* @returns Synthetic compaction summaries followed by the uncompacted message tail.
|
|
666
|
+
*/
|
|
667
|
+
export function loadCompactedModelMessages(
|
|
668
|
+
db: Database,
|
|
669
|
+
sessionId: string,
|
|
670
|
+
): Message[] {
|
|
671
|
+
return [
|
|
672
|
+
...listSessionCompactions(db, sessionId).map(
|
|
673
|
+
(compaction) => compaction.summaryMessage,
|
|
674
|
+
),
|
|
675
|
+
...loadUncompactedModelMessages(db, sessionId).map((row) => row.message),
|
|
676
|
+
];
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
/**
|
|
680
|
+
* Compute the next-request context-token estimate for a persisted session.
|
|
681
|
+
*
|
|
682
|
+
* @param db - Open database handle.
|
|
683
|
+
* @param sessionId - Session whose compacted model context should be estimated.
|
|
684
|
+
* @returns Estimated context tokens for the next request.
|
|
685
|
+
*/
|
|
686
|
+
export function computeSessionContextTokens(
|
|
687
|
+
db: Database,
|
|
688
|
+
sessionId: string,
|
|
689
|
+
): number {
|
|
690
|
+
return computeContextTokens(loadCompactedModelMessages(db, sessionId));
|
|
691
|
+
}
|
|
692
|
+
|
|
441
693
|
/**
|
|
442
694
|
* Append a UI-only message to a session's history.
|
|
443
695
|
*
|
|
@@ -661,14 +913,30 @@ export function truncatePromptHistory(db: Database, keep: number): void {
|
|
|
661
913
|
* @returns `true` if a turn was removed, `false` if the session had no messages.
|
|
662
914
|
*/
|
|
663
915
|
export function undoLastTurn(db: Database, sessionId: string): boolean {
|
|
664
|
-
|
|
665
|
-
|
|
916
|
+
return runInImmediateTransaction(db, () => {
|
|
917
|
+
const row = db.query<MaxTurnRow, [string]>(SQL.maxTurn).get(sessionId);
|
|
918
|
+
if (!row?.max_turn) {
|
|
919
|
+
return false;
|
|
920
|
+
}
|
|
666
921
|
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
922
|
+
const firstMessageRow = db
|
|
923
|
+
.query<MessageIdRow, [string, number]>(SQL.firstMessageIdForTurn)
|
|
924
|
+
.get(sessionId, row.max_turn);
|
|
925
|
+
|
|
926
|
+
db.run("DELETE FROM messages WHERE session_id = ? AND turn = ?", [
|
|
927
|
+
sessionId,
|
|
928
|
+
row.max_turn,
|
|
929
|
+
]);
|
|
930
|
+
|
|
931
|
+
if (firstMessageRow) {
|
|
932
|
+
db.run(
|
|
933
|
+
"DELETE FROM session_compactions WHERE session_id = ? AND message_end_id >= ?",
|
|
934
|
+
[sessionId, firstMessageRow.id],
|
|
935
|
+
);
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
return true;
|
|
939
|
+
});
|
|
672
940
|
}
|
|
673
941
|
|
|
674
942
|
// ---------------------------------------------------------------------------
|
|
@@ -679,8 +947,9 @@ export function undoLastTurn(db: Database, sessionId: string): boolean {
|
|
|
679
947
|
* Fork a session into a new independent copy.
|
|
680
948
|
*
|
|
681
949
|
* Creates a new session with the same `cwd`, `model`, and `effort` as the
|
|
682
|
-
* source, then copies all messages preserving their turn numbers
|
|
683
|
-
*
|
|
950
|
+
* source, then copies all messages preserving their turn numbers plus any
|
|
951
|
+
* persisted compaction summaries with remapped message-row boundaries. The
|
|
952
|
+
* new session's `forkedFrom` field points back to the source. The original
|
|
684
953
|
* session is not modified.
|
|
685
954
|
*
|
|
686
955
|
* @param db - Open database handle.
|
|
@@ -692,34 +961,83 @@ export function forkSession(db: Database, sourceId: string): Session {
|
|
|
692
961
|
const source = getSession(db, sourceId);
|
|
693
962
|
if (!source) throw new Error(`Session not found: ${sourceId}`);
|
|
694
963
|
|
|
695
|
-
|
|
696
|
-
|
|
964
|
+
return runInImmediateTransaction(db, () => {
|
|
965
|
+
const id = generateId();
|
|
966
|
+
const now = Date.now();
|
|
697
967
|
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
968
|
+
db.run(
|
|
969
|
+
"INSERT INTO sessions (id, cwd, model, effort, forked_from, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?)",
|
|
970
|
+
[id, source.cwd, source.model, source.effort, sourceId, now, now],
|
|
971
|
+
);
|
|
702
972
|
|
|
703
|
-
|
|
704
|
-
"INSERT INTO messages (session_id, turn, data, created_at) SELECT ?, turn, data, created_at FROM messages WHERE session_id = ? ORDER BY id",
|
|
705
|
-
[id, sourceId],
|
|
706
|
-
);
|
|
973
|
+
const sourceMessageIds = listMessageIds(db, sourceId);
|
|
707
974
|
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
975
|
+
db.run(
|
|
976
|
+
"INSERT INTO messages (session_id, turn, data, created_at) SELECT ?, turn, data, created_at FROM messages WHERE session_id = ? ORDER BY id",
|
|
977
|
+
[id, sourceId],
|
|
978
|
+
);
|
|
979
|
+
|
|
980
|
+
const forkedMessageIds = listMessageIds(db, id);
|
|
981
|
+
const forkedMessageIdBySourceId = new Map<number, number>();
|
|
982
|
+
for (let index = 0; index < sourceMessageIds.length; index += 1) {
|
|
983
|
+
const sourceMessageId = sourceMessageIds[index];
|
|
984
|
+
const forkedMessageId = forkedMessageIds[index];
|
|
985
|
+
if (sourceMessageId === undefined || forkedMessageId === undefined) {
|
|
986
|
+
continue;
|
|
987
|
+
}
|
|
988
|
+
forkedMessageIdBySourceId.set(sourceMessageId, forkedMessageId);
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
const sourceCompactions = db
|
|
992
|
+
.query<SessionCompactionRow, [string]>(SQL.listCompactions)
|
|
993
|
+
.all(sourceId);
|
|
994
|
+
for (const compaction of sourceCompactions) {
|
|
995
|
+
const forkedMessageEndId = forkedMessageIdBySourceId.get(
|
|
996
|
+
compaction.message_end_id,
|
|
997
|
+
);
|
|
998
|
+
if (forkedMessageEndId === undefined) {
|
|
999
|
+
continue;
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
db.run(
|
|
1003
|
+
"INSERT INTO session_compactions (session_id, message_end_id, summary_data, usage_data, created_at) VALUES (?, ?, ?, ?, ?)",
|
|
1004
|
+
[
|
|
1005
|
+
id,
|
|
1006
|
+
forkedMessageEndId,
|
|
1007
|
+
compaction.summary_data,
|
|
1008
|
+
compaction.usage_data,
|
|
1009
|
+
compaction.created_at,
|
|
1010
|
+
],
|
|
1011
|
+
);
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
return {
|
|
1015
|
+
id,
|
|
1016
|
+
cwd: source.cwd,
|
|
1017
|
+
model: source.model,
|
|
1018
|
+
effort: source.effort,
|
|
1019
|
+
forkedFrom: sourceId,
|
|
1020
|
+
createdAt: now,
|
|
1021
|
+
updatedAt: now,
|
|
1022
|
+
};
|
|
1023
|
+
});
|
|
717
1024
|
}
|
|
718
1025
|
|
|
719
1026
|
// ---------------------------------------------------------------------------
|
|
720
1027
|
// Stats
|
|
721
1028
|
// ---------------------------------------------------------------------------
|
|
722
1029
|
|
|
1030
|
+
function addUsageToStats(
|
|
1031
|
+
stats: SessionStats,
|
|
1032
|
+
usage: AssistantMessage["usage"],
|
|
1033
|
+
): SessionStats {
|
|
1034
|
+
return {
|
|
1035
|
+
totalInput: stats.totalInput + usage.input,
|
|
1036
|
+
totalOutput: stats.totalOutput + usage.output,
|
|
1037
|
+
totalCost: stats.totalCost + usage.cost.total,
|
|
1038
|
+
};
|
|
1039
|
+
}
|
|
1040
|
+
|
|
723
1041
|
/**
|
|
724
1042
|
* Add one persisted message's assistant usage to cumulative session stats.
|
|
725
1043
|
*
|
|
@@ -735,15 +1053,7 @@ export function addMessageToStats(
|
|
|
735
1053
|
message: PersistedMessage,
|
|
736
1054
|
): SessionStats {
|
|
737
1055
|
const usage = getAssistantUsage(message);
|
|
738
|
-
|
|
739
|
-
return stats;
|
|
740
|
-
}
|
|
741
|
-
|
|
742
|
-
return {
|
|
743
|
-
totalInput: stats.totalInput + usage.input,
|
|
744
|
-
totalOutput: stats.totalOutput + usage.output,
|
|
745
|
-
totalCost: stats.totalCost + usage.cost.total,
|
|
746
|
-
};
|
|
1056
|
+
return usage ? addUsageToStats(stats, usage) : stats;
|
|
747
1057
|
}
|
|
748
1058
|
|
|
749
1059
|
/** Create a zeroed cumulative session-stats object. */
|
|
@@ -773,6 +1083,32 @@ export function computeStats(
|
|
|
773
1083
|
return stats;
|
|
774
1084
|
}
|
|
775
1085
|
|
|
1086
|
+
/**
|
|
1087
|
+
* Compute cumulative session stats including persisted compaction-summary calls.
|
|
1088
|
+
*
|
|
1089
|
+
* @param db - Open database handle.
|
|
1090
|
+
* @param sessionId - Session whose cumulative stats should be loaded.
|
|
1091
|
+
* @returns Aggregated cumulative session stats for visible turns plus compactions.
|
|
1092
|
+
*/
|
|
1093
|
+
export function computeSessionStats(
|
|
1094
|
+
db: Database,
|
|
1095
|
+
sessionId: string,
|
|
1096
|
+
): SessionStats {
|
|
1097
|
+
let stats = computeStats(loadMessages(db, sessionId));
|
|
1098
|
+
|
|
1099
|
+
const compactionRows = db
|
|
1100
|
+
.query<SessionCompactionRow, [string]>(SQL.listCompactions)
|
|
1101
|
+
.all(sessionId);
|
|
1102
|
+
for (const row of compactionRows) {
|
|
1103
|
+
const usage = parseCompactionUsage(row.usage_data);
|
|
1104
|
+
if (usage) {
|
|
1105
|
+
stats = addUsageToStats(stats, usage);
|
|
1106
|
+
}
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1109
|
+
return stats;
|
|
1110
|
+
}
|
|
1111
|
+
|
|
776
1112
|
// ---------------------------------------------------------------------------
|
|
777
1113
|
// Context estimation
|
|
778
1114
|
// ---------------------------------------------------------------------------
|
|
@@ -868,21 +1204,29 @@ function estimateMessageTokens(message: Message): number {
|
|
|
868
1204
|
}
|
|
869
1205
|
}
|
|
870
1206
|
|
|
1207
|
+
interface ContextTokenOptions {
|
|
1208
|
+
/** Ignore assistant usage anchors and estimate every message directly. */
|
|
1209
|
+
ignoreAssistantUsage?: boolean;
|
|
1210
|
+
}
|
|
1211
|
+
|
|
871
1212
|
/**
|
|
872
1213
|
* Fold one persisted message into the running context estimate for the next request.
|
|
873
1214
|
*
|
|
874
1215
|
* Assistant messages with valid usage anchor the full model-visible context for
|
|
875
|
-
* that point in the transcript, so they replace the running estimate
|
|
876
|
-
*
|
|
877
|
-
*
|
|
1216
|
+
* that point in the transcript, so they replace the running estimate unless the
|
|
1217
|
+
* caller explicitly disables those anchors. All other model-visible messages are
|
|
1218
|
+
* added incrementally using the same conservative estimation logic used before
|
|
1219
|
+
* the first valid assistant usage appears.
|
|
878
1220
|
*
|
|
879
1221
|
* @param contextTokens - Running estimate before this message.
|
|
880
1222
|
* @param message - Persisted message to fold into the estimate.
|
|
1223
|
+
* @param options - Optional estimation behavior overrides.
|
|
881
1224
|
* @returns Updated context-token estimate.
|
|
882
1225
|
*/
|
|
883
1226
|
export function addMessageToContextTokens(
|
|
884
1227
|
contextTokens: number,
|
|
885
1228
|
message: PersistedMessage,
|
|
1229
|
+
options?: ContextTokenOptions,
|
|
886
1230
|
): number {
|
|
887
1231
|
if (message.role === "ui") {
|
|
888
1232
|
return contextTokens;
|
|
@@ -890,6 +1234,7 @@ export function addMessageToContextTokens(
|
|
|
890
1234
|
|
|
891
1235
|
const usage = getAssistantUsage(message);
|
|
892
1236
|
if (
|
|
1237
|
+
!options?.ignoreAssistantUsage &&
|
|
893
1238
|
message.role === "assistant" &&
|
|
894
1239
|
usage &&
|
|
895
1240
|
message.stopReason !== "aborted" &&
|
|
@@ -909,15 +1254,17 @@ export function addMessageToContextTokens(
|
|
|
909
1254
|
* message history.
|
|
910
1255
|
*
|
|
911
1256
|
* @param messages - Full persisted session history.
|
|
1257
|
+
* @param options - Optional estimation behavior overrides.
|
|
912
1258
|
* @returns Estimated context tokens visible to the next model request.
|
|
913
1259
|
*/
|
|
914
1260
|
export function computeContextTokens(
|
|
915
1261
|
messages: readonly PersistedMessage[],
|
|
1262
|
+
options?: ContextTokenOptions,
|
|
916
1263
|
): number {
|
|
917
1264
|
let contextTokens = 0;
|
|
918
1265
|
|
|
919
1266
|
for (const message of messages) {
|
|
920
|
-
contextTokens = addMessageToContextTokens(contextTokens, message);
|
|
1267
|
+
contextTokens = addMessageToContextTokens(contextTokens, message, options);
|
|
921
1268
|
}
|
|
922
1269
|
|
|
923
1270
|
return contextTokens;
|