@oh-my-pi/pi-coding-agent 11.8.2 → 11.8.3
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/docs/tui.md +9 -9
- package/package.json +7 -7
- package/src/cli/file-processor.ts +8 -13
- package/src/cli/oclif-help.ts +1 -1
- package/src/cli.ts +14 -0
- package/src/commit/git/index.ts +16 -16
- package/src/config/keybindings.ts +11 -11
- package/src/config/model-registry.ts +31 -66
- package/src/config/settings.ts +88 -95
- package/src/config.ts +2 -2
- package/src/cursor.ts +4 -4
- package/src/debug/index.ts +28 -28
- package/src/discovery/codex.ts +5 -13
- package/src/discovery/cursor.ts +2 -7
- package/src/exa/mcp-client.ts +2 -2
- package/src/exa/websets.ts +2 -2
- package/src/export/html/index.ts +3 -3
- package/src/export/ttsr.ts +27 -27
- package/src/extensibility/custom-tools/loader.ts +9 -9
- package/src/extensibility/extensions/runner.ts +64 -64
- package/src/extensibility/hooks/runner.ts +46 -46
- package/src/extensibility/plugins/manager.ts +49 -49
- package/src/index.ts +0 -1
- package/src/internal-urls/router.ts +5 -5
- package/src/ipy/kernel.ts +61 -57
- package/src/lsp/client.ts +1 -1
- package/src/lsp/clients/biome-client.ts +2 -2
- package/src/lsp/clients/lsp-linter-client.ts +7 -7
- package/src/lsp/index.ts +9 -9
- package/src/mcp/manager.ts +47 -47
- package/src/mcp/tool-bridge.ts +12 -12
- package/src/mcp/transports/http.ts +34 -34
- package/src/mcp/transports/stdio.ts +47 -47
- package/src/modes/components/assistant-message.ts +25 -25
- package/src/modes/components/bash-execution.ts +51 -51
- package/src/modes/components/bordered-loader.ts +7 -7
- package/src/modes/components/branch-summary-message.ts +7 -7
- package/src/modes/components/compaction-summary-message.ts +7 -7
- package/src/modes/components/countdown-timer.ts +15 -15
- package/src/modes/components/custom-editor.ts +22 -22
- package/src/modes/components/custom-message.ts +21 -21
- package/src/modes/components/dynamic-border.ts +3 -3
- package/src/modes/components/extensions/extension-dashboard.ts +72 -72
- package/src/modes/components/extensions/extension-list.ts +99 -97
- package/src/modes/components/extensions/inspector-panel.ts +26 -26
- package/src/modes/components/footer.ts +36 -36
- package/src/modes/components/history-search.ts +52 -52
- package/src/modes/components/hook-editor.ts +20 -20
- package/src/modes/components/hook-input.ts +20 -20
- package/src/modes/components/hook-message.ts +22 -22
- package/src/modes/components/hook-selector.ts +52 -52
- package/src/modes/components/index.ts +0 -1
- package/src/modes/components/login-dialog.ts +57 -57
- package/src/modes/components/model-selector.ts +173 -173
- package/src/modes/components/oauth-selector.ts +45 -45
- package/src/modes/components/plugin-settings.ts +52 -52
- package/src/modes/components/python-execution.ts +53 -53
- package/src/modes/components/queue-mode-selector.ts +7 -7
- package/src/modes/components/read-tool-group.ts +23 -23
- package/src/modes/components/session-selector.ts +40 -37
- package/src/modes/components/settings-selector.ts +80 -80
- package/src/modes/components/show-images-selector.ts +7 -7
- package/src/modes/components/skill-message.ts +27 -27
- package/src/modes/components/status-line-segment-editor.ts +81 -81
- package/src/modes/components/status-line.ts +73 -73
- package/src/modes/components/theme-selector.ts +11 -11
- package/src/modes/components/thinking-selector.ts +7 -7
- package/src/modes/components/todo-display.ts +19 -19
- package/src/modes/components/todo-reminder.ts +9 -9
- package/src/modes/components/tool-execution.ts +204 -196
- package/src/modes/components/tree-selector.ts +144 -144
- package/src/modes/components/ttsr-notification.ts +17 -17
- package/src/modes/components/user-message-selector.ts +18 -18
- package/src/modes/components/welcome.ts +10 -10
- package/src/modes/controllers/command-controller.ts +0 -7
- package/src/modes/controllers/event-controller.ts +23 -23
- package/src/modes/controllers/extension-ui-controller.ts +13 -13
- package/src/modes/controllers/input-controller.ts +4 -9
- package/src/modes/interactive-mode.ts +234 -241
- package/src/modes/rpc/rpc-client.ts +77 -77
- package/src/modes/rpc/rpc-mode.ts +5 -5
- package/src/modes/theme/theme.ts +113 -113
- package/src/modes/types.ts +0 -1
- package/src/patch/index.ts +45 -45
- package/src/prompts/tools/task.md +22 -2
- package/src/session/agent-session.ts +463 -476
- package/src/session/agent-storage.ts +72 -75
- package/src/session/auth-storage.ts +186 -252
- package/src/session/history-storage.ts +36 -38
- package/src/session/session-manager.ts +300 -299
- package/src/session/session-storage.ts +65 -90
- package/src/ssh/connection-manager.ts +9 -9
- package/src/task/agents.ts +1 -1
- package/src/task/executor.ts +2 -2
- package/src/task/index.ts +13 -12
- package/src/task/subprocess-tool-registry.ts +5 -5
- package/src/tools/ask.ts +7 -7
- package/src/tools/bash.ts +8 -7
- package/src/tools/browser.ts +123 -123
- package/src/tools/calculator.ts +46 -46
- package/src/tools/context.ts +9 -9
- package/src/tools/exit-plan-mode.ts +5 -5
- package/src/tools/fetch.ts +5 -5
- package/src/tools/find.ts +16 -16
- package/src/tools/grep.ts +10 -10
- package/src/tools/notebook.ts +6 -6
- package/src/tools/output-meta.ts +10 -2
- package/src/tools/python.ts +12 -11
- package/src/tools/read.ts +17 -17
- package/src/tools/ssh.ts +9 -9
- package/src/tools/submit-result.ts +13 -13
- package/src/tools/todo-write.ts +6 -6
- package/src/tools/write.ts +10 -10
- package/src/tui/output-block.ts +6 -6
- package/src/tui/utils.ts +9 -9
- package/src/utils/event-bus.ts +10 -10
- package/src/utils/frontmatter.ts +1 -1
- package/src/utils/ignore-files.ts +1 -1
- package/src/web/search/index.ts +5 -5
- package/src/web/search/providers/anthropic.ts +7 -2
- package/examples/hooks/snake.ts +0 -342
- package/src/modes/components/armin.ts +0 -379
|
@@ -846,89 +846,89 @@ async function prepareEntryForPersistence(entry: FileEntry, blobStore: BlobStore
|
|
|
846
846
|
}
|
|
847
847
|
|
|
848
848
|
class NdjsonFileWriter {
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
849
|
+
#writer: SessionStorageWriter;
|
|
850
|
+
#closed = false;
|
|
851
|
+
#closing = false;
|
|
852
|
+
#error: Error | undefined;
|
|
853
|
+
#pendingWrites: Promise<void> = Promise.resolve();
|
|
854
|
+
#onError: ((err: Error) => void) | undefined;
|
|
855
855
|
|
|
856
856
|
constructor(storage: SessionStorage, path: string, options?: { flags?: "a" | "w"; onError?: (err: Error) => void }) {
|
|
857
|
-
this
|
|
858
|
-
this
|
|
857
|
+
this.#onError = options?.onError;
|
|
858
|
+
this.#writer = storage.openWriter(path, {
|
|
859
859
|
flags: options?.flags ?? "a",
|
|
860
|
-
onError: (err: Error) => this
|
|
860
|
+
onError: (err: Error) => this.#recordError(err),
|
|
861
861
|
});
|
|
862
862
|
}
|
|
863
863
|
|
|
864
|
-
|
|
864
|
+
#recordError(err: unknown): Error {
|
|
865
865
|
const writeErr = toError(err);
|
|
866
|
-
if (!this
|
|
867
|
-
this
|
|
866
|
+
if (!this.#error) this.#error = writeErr;
|
|
867
|
+
this.#onError?.(writeErr);
|
|
868
868
|
return writeErr;
|
|
869
869
|
}
|
|
870
870
|
|
|
871
|
-
|
|
871
|
+
#enqueue(task: () => Promise<void>): Promise<void> {
|
|
872
872
|
const run = async () => {
|
|
873
|
-
if (this
|
|
873
|
+
if (this.#error) throw this.#error;
|
|
874
874
|
await task();
|
|
875
875
|
};
|
|
876
|
-
const next = this
|
|
876
|
+
const next = this.#pendingWrites.then(run);
|
|
877
877
|
void next.catch((err: unknown) => {
|
|
878
|
-
if (!this
|
|
878
|
+
if (!this.#error) this.#error = toError(err);
|
|
879
879
|
});
|
|
880
|
-
this
|
|
880
|
+
this.#pendingWrites = next;
|
|
881
881
|
return next;
|
|
882
882
|
}
|
|
883
883
|
|
|
884
|
-
|
|
885
|
-
if (this
|
|
884
|
+
async #writeLine(line: string): Promise<void> {
|
|
885
|
+
if (this.#error) throw this.#error;
|
|
886
886
|
try {
|
|
887
|
-
await this
|
|
887
|
+
await this.#writer.writeLine(line);
|
|
888
888
|
} catch (err) {
|
|
889
|
-
throw this
|
|
889
|
+
throw this.#recordError(err);
|
|
890
890
|
}
|
|
891
891
|
}
|
|
892
892
|
|
|
893
893
|
/** Queue a write. Returns a promise so callers can await if needed. */
|
|
894
894
|
write(entry: FileEntry): Promise<void> {
|
|
895
|
-
if (this
|
|
896
|
-
if (this
|
|
895
|
+
if (this.#closed || this.#closing) throw new Error("Writer closed");
|
|
896
|
+
if (this.#error) throw this.#error;
|
|
897
897
|
const line = `${JSON.stringify(entry)}\n`;
|
|
898
|
-
return this
|
|
898
|
+
return this.#enqueue(() => this.#writeLine(line));
|
|
899
899
|
}
|
|
900
900
|
|
|
901
901
|
/** Flush all buffered data to disk. Waits for all queued writes. */
|
|
902
902
|
async flush(): Promise<void> {
|
|
903
|
-
if (this
|
|
904
|
-
if (this
|
|
903
|
+
if (this.#closed) return;
|
|
904
|
+
if (this.#error) throw this.#error;
|
|
905
905
|
|
|
906
|
-
await this
|
|
906
|
+
await this.#enqueue(async () => {});
|
|
907
907
|
|
|
908
|
-
if (this
|
|
908
|
+
if (this.#error) throw this.#error;
|
|
909
909
|
|
|
910
910
|
try {
|
|
911
|
-
await this
|
|
911
|
+
await this.#writer.flush();
|
|
912
912
|
} catch (err) {
|
|
913
|
-
throw this
|
|
913
|
+
throw this.#recordError(err);
|
|
914
914
|
}
|
|
915
915
|
}
|
|
916
916
|
|
|
917
917
|
/** Sync data to persistent storage. */
|
|
918
918
|
async fsync(): Promise<void> {
|
|
919
|
-
if (this
|
|
920
|
-
if (this
|
|
919
|
+
if (this.#closed) return;
|
|
920
|
+
if (this.#error) throw this.#error;
|
|
921
921
|
try {
|
|
922
|
-
await this
|
|
922
|
+
await this.#writer.fsync();
|
|
923
923
|
} catch (err) {
|
|
924
|
-
throw this
|
|
924
|
+
throw this.#recordError(err);
|
|
925
925
|
}
|
|
926
926
|
}
|
|
927
927
|
|
|
928
928
|
/** Close the writer, flushing all data. */
|
|
929
929
|
async close(): Promise<void> {
|
|
930
|
-
if (this
|
|
931
|
-
this
|
|
930
|
+
if (this.#closed || this.#closing) return;
|
|
931
|
+
this.#closing = true;
|
|
932
932
|
|
|
933
933
|
let closeError: Error | undefined;
|
|
934
934
|
try {
|
|
@@ -938,27 +938,27 @@ class NdjsonFileWriter {
|
|
|
938
938
|
}
|
|
939
939
|
|
|
940
940
|
try {
|
|
941
|
-
await this
|
|
941
|
+
await this.#pendingWrites;
|
|
942
942
|
} catch (err) {
|
|
943
943
|
if (!closeError) closeError = toError(err);
|
|
944
944
|
}
|
|
945
945
|
|
|
946
946
|
try {
|
|
947
|
-
await this
|
|
947
|
+
await this.#writer.close();
|
|
948
948
|
} catch (err) {
|
|
949
|
-
const endErr = this
|
|
949
|
+
const endErr = this.#recordError(err);
|
|
950
950
|
if (!closeError) closeError = endErr;
|
|
951
951
|
}
|
|
952
952
|
|
|
953
|
-
this
|
|
953
|
+
this.#closed = true;
|
|
954
954
|
|
|
955
|
-
if (!closeError && this
|
|
955
|
+
if (!closeError && this.#error) closeError = this.#error;
|
|
956
956
|
if (closeError) throw closeError;
|
|
957
957
|
}
|
|
958
958
|
|
|
959
959
|
/** Check if there's a stored error. */
|
|
960
960
|
getError(): Error | undefined {
|
|
961
|
-
return this
|
|
961
|
+
return this.#error;
|
|
962
962
|
}
|
|
963
963
|
}
|
|
964
964
|
|
|
@@ -1076,21 +1076,21 @@ async function collectSessionsFromFiles(files: string[], storage: SessionStorage
|
|
|
1076
1076
|
}
|
|
1077
1077
|
|
|
1078
1078
|
export class SessionManager {
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1079
|
+
#sessionId: string = "";
|
|
1080
|
+
#sessionName: string | undefined;
|
|
1081
|
+
#sessionFile: string | undefined;
|
|
1082
|
+
#flushed: boolean = false;
|
|
1083
|
+
#fileEntries: FileEntry[] = [];
|
|
1084
|
+
#byId: Map<string, SessionEntry> = new Map();
|
|
1085
|
+
#labelsById: Map<string, string> = new Map();
|
|
1086
|
+
#leafId: string | null = null;
|
|
1087
|
+
#usageStatistics: UsageStatistics = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0 };
|
|
1088
|
+
#persistWriter: NdjsonFileWriter | undefined;
|
|
1089
|
+
#persistWriterPath: string | undefined;
|
|
1090
|
+
#persistChain: Promise<void> = Promise.resolve();
|
|
1091
|
+
#persistError: Error | undefined;
|
|
1092
|
+
#persistErrorReported = false;
|
|
1093
|
+
readonly #blobStore: BlobStore;
|
|
1094
1094
|
|
|
1095
1095
|
private constructor(
|
|
1096
1096
|
private readonly cwd: string,
|
|
@@ -1098,7 +1098,7 @@ export class SessionManager {
|
|
|
1098
1098
|
private readonly persist: boolean,
|
|
1099
1099
|
private readonly storage: SessionStorage,
|
|
1100
1100
|
) {
|
|
1101
|
-
this
|
|
1101
|
+
this.#blobStore = new BlobStore(getBlobsDir());
|
|
1102
1102
|
if (persist && sessionDir) {
|
|
1103
1103
|
this.storage.ensureDirSync(sessionDir);
|
|
1104
1104
|
}
|
|
@@ -1107,54 +1107,54 @@ export class SessionManager {
|
|
|
1107
1107
|
|
|
1108
1108
|
/** Puts a binary blob into the blob store and returns the blob reference */
|
|
1109
1109
|
async putBlob(data: Buffer): Promise<BlobPutResult> {
|
|
1110
|
-
return this
|
|
1110
|
+
return this.#blobStore.put(data);
|
|
1111
1111
|
}
|
|
1112
1112
|
|
|
1113
1113
|
/** Initialize with a specific session file (used by factory methods) */
|
|
1114
|
-
|
|
1114
|
+
async #initSessionFile(sessionFile: string): Promise<void> {
|
|
1115
1115
|
await this.setSessionFile(sessionFile);
|
|
1116
1116
|
}
|
|
1117
1117
|
|
|
1118
1118
|
/** Initialize with a new session (used by factory methods) */
|
|
1119
|
-
|
|
1120
|
-
this
|
|
1119
|
+
#initNewSession(): void {
|
|
1120
|
+
this.#newSessionSync();
|
|
1121
1121
|
}
|
|
1122
1122
|
|
|
1123
1123
|
/** Switch to a different session file (used for resume and branching) */
|
|
1124
1124
|
async setSessionFile(sessionFile: string): Promise<void> {
|
|
1125
|
-
await this
|
|
1126
|
-
this
|
|
1127
|
-
this
|
|
1128
|
-
this
|
|
1129
|
-
writeTerminalBreadcrumb(this.cwd, this
|
|
1130
|
-
this
|
|
1131
|
-
if (this
|
|
1132
|
-
const header = this
|
|
1133
|
-
this
|
|
1134
|
-
this
|
|
1135
|
-
|
|
1136
|
-
if (migrateToCurrentVersion(this
|
|
1137
|
-
await this
|
|
1125
|
+
await this.#closePersistWriter();
|
|
1126
|
+
this.#persistError = undefined;
|
|
1127
|
+
this.#persistErrorReported = false;
|
|
1128
|
+
this.#sessionFile = path.resolve(sessionFile);
|
|
1129
|
+
writeTerminalBreadcrumb(this.cwd, this.#sessionFile);
|
|
1130
|
+
this.#fileEntries = await loadEntriesFromFile(this.#sessionFile, this.storage);
|
|
1131
|
+
if (this.#fileEntries.length > 0) {
|
|
1132
|
+
const header = this.#fileEntries.find(e => e.type === "session") as SessionHeader | undefined;
|
|
1133
|
+
this.#sessionId = header?.id ?? Snowflake.next();
|
|
1134
|
+
this.#sessionName = header?.title;
|
|
1135
|
+
|
|
1136
|
+
if (migrateToCurrentVersion(this.#fileEntries)) {
|
|
1137
|
+
await this.#rewriteFile();
|
|
1138
1138
|
}
|
|
1139
1139
|
|
|
1140
|
-
await resolveBlobRefsInEntries(this
|
|
1140
|
+
await resolveBlobRefsInEntries(this.#fileEntries, this.#blobStore);
|
|
1141
1141
|
|
|
1142
|
-
this
|
|
1143
|
-
this
|
|
1142
|
+
this.#buildIndex();
|
|
1143
|
+
this.#flushed = true;
|
|
1144
1144
|
} else {
|
|
1145
|
-
const explicitPath = this
|
|
1146
|
-
this
|
|
1147
|
-
this
|
|
1148
|
-
await this
|
|
1149
|
-
this
|
|
1145
|
+
const explicitPath = this.#sessionFile;
|
|
1146
|
+
this.#newSessionSync();
|
|
1147
|
+
this.#sessionFile = explicitPath; // preserve explicit path from --session flag
|
|
1148
|
+
await this.#rewriteFile();
|
|
1149
|
+
this.#flushed = true;
|
|
1150
1150
|
return;
|
|
1151
1151
|
}
|
|
1152
1152
|
}
|
|
1153
1153
|
|
|
1154
1154
|
/** Start a new session. Closes any existing writer first. */
|
|
1155
1155
|
async newSession(options?: NewSessionOptions): Promise<string | undefined> {
|
|
1156
|
-
await this
|
|
1157
|
-
return this
|
|
1156
|
+
await this.#closePersistWriter();
|
|
1157
|
+
return this.#newSessionSync(options);
|
|
1158
1158
|
}
|
|
1159
1159
|
|
|
1160
1160
|
/**
|
|
@@ -1163,125 +1163,125 @@ export class SessionManager {
|
|
|
1163
1163
|
* @returns { oldSessionFile, newSessionFile } or undefined if not persisting
|
|
1164
1164
|
*/
|
|
1165
1165
|
async fork(): Promise<{ oldSessionFile: string; newSessionFile: string } | undefined> {
|
|
1166
|
-
if (!this.persist || !this
|
|
1166
|
+
if (!this.persist || !this.#sessionFile) {
|
|
1167
1167
|
return undefined;
|
|
1168
1168
|
}
|
|
1169
1169
|
|
|
1170
|
-
const oldSessionFile = this
|
|
1171
|
-
const oldSessionId = this
|
|
1170
|
+
const oldSessionFile = this.#sessionFile;
|
|
1171
|
+
const oldSessionId = this.#sessionId;
|
|
1172
1172
|
|
|
1173
1173
|
// Close the current writer
|
|
1174
|
-
await this
|
|
1175
|
-
this
|
|
1176
|
-
this
|
|
1177
|
-
this
|
|
1174
|
+
await this.#closePersistWriter();
|
|
1175
|
+
this.#persistChain = Promise.resolve();
|
|
1176
|
+
this.#persistError = undefined;
|
|
1177
|
+
this.#persistErrorReported = false;
|
|
1178
1178
|
|
|
1179
1179
|
// Create new session ID and header
|
|
1180
|
-
this
|
|
1180
|
+
this.#sessionId = Snowflake.next();
|
|
1181
1181
|
const timestamp = new Date().toISOString();
|
|
1182
1182
|
const fileTimestamp = timestamp.replace(/[:.]/g, "-");
|
|
1183
|
-
this
|
|
1183
|
+
this.#sessionFile = path.join(this.getSessionDir(), `${fileTimestamp}_${this.#sessionId}.jsonl`);
|
|
1184
1184
|
|
|
1185
1185
|
// Update the header with new ID but keep all entries
|
|
1186
|
-
const oldHeader = this
|
|
1186
|
+
const oldHeader = this.#fileEntries.find(e => e.type === "session") as SessionHeader | undefined;
|
|
1187
1187
|
const newHeader: SessionHeader = {
|
|
1188
1188
|
type: "session",
|
|
1189
1189
|
version: CURRENT_SESSION_VERSION,
|
|
1190
|
-
id: this
|
|
1191
|
-
title: oldHeader?.title ?? this
|
|
1190
|
+
id: this.#sessionId,
|
|
1191
|
+
title: oldHeader?.title ?? this.#sessionName,
|
|
1192
1192
|
timestamp,
|
|
1193
1193
|
cwd: this.cwd,
|
|
1194
1194
|
parentSession: oldSessionId,
|
|
1195
1195
|
};
|
|
1196
|
-
this
|
|
1196
|
+
this.#sessionName = newHeader.title;
|
|
1197
1197
|
|
|
1198
1198
|
// Replace the header in fileEntries
|
|
1199
|
-
const entries = this
|
|
1200
|
-
this
|
|
1199
|
+
const entries = this.#fileEntries.filter(e => e.type !== "session") as SessionEntry[];
|
|
1200
|
+
this.#fileEntries = [newHeader, ...entries];
|
|
1201
1201
|
|
|
1202
1202
|
// Write the new session file
|
|
1203
|
-
this
|
|
1204
|
-
await this
|
|
1203
|
+
this.#flushed = false;
|
|
1204
|
+
await this.#rewriteFile();
|
|
1205
1205
|
|
|
1206
|
-
return { oldSessionFile, newSessionFile: this
|
|
1206
|
+
return { oldSessionFile, newSessionFile: this.#sessionFile };
|
|
1207
1207
|
}
|
|
1208
1208
|
|
|
1209
1209
|
/** Sync version for initial creation (no existing writer to close) */
|
|
1210
|
-
|
|
1211
|
-
this
|
|
1212
|
-
this
|
|
1213
|
-
this
|
|
1214
|
-
this
|
|
1215
|
-
this
|
|
1210
|
+
#newSessionSync(options?: NewSessionOptions): string | undefined {
|
|
1211
|
+
this.#persistChain = Promise.resolve();
|
|
1212
|
+
this.#persistError = undefined;
|
|
1213
|
+
this.#persistErrorReported = false;
|
|
1214
|
+
this.#sessionId = Snowflake.next();
|
|
1215
|
+
this.#sessionName = undefined;
|
|
1216
1216
|
const timestamp = new Date().toISOString();
|
|
1217
1217
|
const header: SessionHeader = {
|
|
1218
1218
|
type: "session",
|
|
1219
1219
|
version: CURRENT_SESSION_VERSION,
|
|
1220
|
-
id: this
|
|
1220
|
+
id: this.#sessionId,
|
|
1221
1221
|
timestamp,
|
|
1222
1222
|
cwd: this.cwd,
|
|
1223
1223
|
parentSession: options?.parentSession,
|
|
1224
1224
|
};
|
|
1225
|
-
this
|
|
1226
|
-
this
|
|
1227
|
-
this
|
|
1228
|
-
this
|
|
1229
|
-
this
|
|
1230
|
-
this
|
|
1225
|
+
this.#fileEntries = [header];
|
|
1226
|
+
this.#byId.clear();
|
|
1227
|
+
this.#labelsById.clear();
|
|
1228
|
+
this.#leafId = null;
|
|
1229
|
+
this.#flushed = false;
|
|
1230
|
+
this.#usageStatistics = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0 };
|
|
1231
1231
|
|
|
1232
1232
|
if (this.persist) {
|
|
1233
1233
|
const fileTimestamp = timestamp.replace(/[:.]/g, "-");
|
|
1234
|
-
this
|
|
1235
|
-
writeTerminalBreadcrumb(this.cwd, this
|
|
1234
|
+
this.#sessionFile = path.join(this.getSessionDir(), `${fileTimestamp}_${this.#sessionId}.jsonl`);
|
|
1235
|
+
writeTerminalBreadcrumb(this.cwd, this.#sessionFile);
|
|
1236
1236
|
}
|
|
1237
|
-
return this
|
|
1237
|
+
return this.#sessionFile;
|
|
1238
1238
|
}
|
|
1239
1239
|
|
|
1240
|
-
|
|
1241
|
-
this
|
|
1242
|
-
this
|
|
1243
|
-
this
|
|
1244
|
-
this
|
|
1245
|
-
for (const entry of this
|
|
1240
|
+
#buildIndex(): void {
|
|
1241
|
+
this.#byId.clear();
|
|
1242
|
+
this.#labelsById.clear();
|
|
1243
|
+
this.#leafId = null;
|
|
1244
|
+
this.#usageStatistics = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0 };
|
|
1245
|
+
for (const entry of this.#fileEntries) {
|
|
1246
1246
|
if (entry.type === "session") continue;
|
|
1247
|
-
this
|
|
1248
|
-
this
|
|
1247
|
+
this.#byId.set(entry.id, entry);
|
|
1248
|
+
this.#leafId = entry.id;
|
|
1249
1249
|
if (entry.type === "label") {
|
|
1250
1250
|
if (entry.label) {
|
|
1251
|
-
this
|
|
1251
|
+
this.#labelsById.set(entry.targetId, entry.label);
|
|
1252
1252
|
} else {
|
|
1253
|
-
this
|
|
1253
|
+
this.#labelsById.delete(entry.targetId);
|
|
1254
1254
|
}
|
|
1255
1255
|
}
|
|
1256
1256
|
if (entry.type === "message" && entry.message.role === "assistant") {
|
|
1257
1257
|
const usage = entry.message.usage;
|
|
1258
|
-
this
|
|
1259
|
-
this
|
|
1260
|
-
this
|
|
1261
|
-
this
|
|
1262
|
-
this
|
|
1258
|
+
this.#usageStatistics.input += usage.input;
|
|
1259
|
+
this.#usageStatistics.output += usage.output;
|
|
1260
|
+
this.#usageStatistics.cacheRead += usage.cacheRead;
|
|
1261
|
+
this.#usageStatistics.cacheWrite += usage.cacheWrite;
|
|
1262
|
+
this.#usageStatistics.cost += usage.cost.total;
|
|
1263
1263
|
}
|
|
1264
1264
|
|
|
1265
1265
|
if (entry.type === "message" && entry.message.role === "toolResult" && entry.message.toolName === "task") {
|
|
1266
1266
|
const usage = getTaskToolUsage(entry.message.details);
|
|
1267
1267
|
if (usage) {
|
|
1268
|
-
this
|
|
1269
|
-
this
|
|
1270
|
-
this
|
|
1271
|
-
this
|
|
1272
|
-
this
|
|
1268
|
+
this.#usageStatistics.input += usage.input;
|
|
1269
|
+
this.#usageStatistics.output += usage.output;
|
|
1270
|
+
this.#usageStatistics.cacheRead += usage.cacheRead;
|
|
1271
|
+
this.#usageStatistics.cacheWrite += usage.cacheWrite;
|
|
1272
|
+
this.#usageStatistics.cost += usage.cost.total;
|
|
1273
1273
|
}
|
|
1274
1274
|
}
|
|
1275
1275
|
}
|
|
1276
1276
|
}
|
|
1277
1277
|
|
|
1278
|
-
|
|
1278
|
+
#recordPersistError(err: unknown): Error {
|
|
1279
1279
|
const normalized = toError(err);
|
|
1280
|
-
if (!this
|
|
1281
|
-
if (!this
|
|
1282
|
-
this
|
|
1280
|
+
if (!this.#persistError) this.#persistError = normalized;
|
|
1281
|
+
if (!this.#persistErrorReported) {
|
|
1282
|
+
this.#persistErrorReported = true;
|
|
1283
1283
|
logger.error("Session persistence error.", {
|
|
1284
|
-
sessionFile: this
|
|
1284
|
+
sessionFile: this.#sessionFile,
|
|
1285
1285
|
error: normalized.message,
|
|
1286
1286
|
stack: normalized.stack,
|
|
1287
1287
|
});
|
|
@@ -1289,52 +1289,52 @@ export class SessionManager {
|
|
|
1289
1289
|
return normalized;
|
|
1290
1290
|
}
|
|
1291
1291
|
|
|
1292
|
-
|
|
1293
|
-
const next = this
|
|
1294
|
-
if (this
|
|
1292
|
+
#queuePersistTask(task: () => Promise<void>, options?: { ignoreError?: boolean }): Promise<void> {
|
|
1293
|
+
const next = this.#persistChain.then(async () => {
|
|
1294
|
+
if (this.#persistError && !options?.ignoreError) throw this.#persistError;
|
|
1295
1295
|
await task();
|
|
1296
1296
|
});
|
|
1297
|
-
this
|
|
1298
|
-
this
|
|
1297
|
+
this.#persistChain = next.catch(err => {
|
|
1298
|
+
this.#recordPersistError(err);
|
|
1299
1299
|
});
|
|
1300
1300
|
return next;
|
|
1301
1301
|
}
|
|
1302
1302
|
|
|
1303
|
-
|
|
1304
|
-
if (!this.persist || !this
|
|
1305
|
-
if (this
|
|
1306
|
-
if (this
|
|
1303
|
+
#ensurePersistWriter(): NdjsonFileWriter | undefined {
|
|
1304
|
+
if (!this.persist || !this.#sessionFile) return undefined;
|
|
1305
|
+
if (this.#persistError) throw this.#persistError;
|
|
1306
|
+
if (this.#persistWriter && this.#persistWriterPath === this.#sessionFile) return this.#persistWriter;
|
|
1307
1307
|
// Note: caller must await _closePersistWriter() before calling this if switching files
|
|
1308
|
-
this
|
|
1308
|
+
this.#persistWriter = new NdjsonFileWriter(this.storage, this.#sessionFile, {
|
|
1309
1309
|
onError: err => {
|
|
1310
|
-
this
|
|
1310
|
+
this.#recordPersistError(err);
|
|
1311
1311
|
},
|
|
1312
1312
|
});
|
|
1313
|
-
this
|
|
1314
|
-
return this
|
|
1313
|
+
this.#persistWriterPath = this.#sessionFile;
|
|
1314
|
+
return this.#persistWriter;
|
|
1315
1315
|
}
|
|
1316
1316
|
|
|
1317
|
-
|
|
1318
|
-
if (this
|
|
1319
|
-
await this
|
|
1320
|
-
this
|
|
1317
|
+
async #closePersistWriterInternal(): Promise<void> {
|
|
1318
|
+
if (this.#persistWriter) {
|
|
1319
|
+
await this.#persistWriter.close();
|
|
1320
|
+
this.#persistWriter = undefined;
|
|
1321
1321
|
}
|
|
1322
|
-
this
|
|
1322
|
+
this.#persistWriterPath = undefined;
|
|
1323
1323
|
}
|
|
1324
1324
|
|
|
1325
|
-
|
|
1326
|
-
await this
|
|
1325
|
+
async #closePersistWriter(): Promise<void> {
|
|
1326
|
+
await this.#queuePersistTask(
|
|
1327
1327
|
async () => {
|
|
1328
|
-
await this
|
|
1328
|
+
await this.#closePersistWriterInternal();
|
|
1329
1329
|
},
|
|
1330
1330
|
{ ignoreError: true },
|
|
1331
1331
|
);
|
|
1332
1332
|
}
|
|
1333
1333
|
|
|
1334
|
-
|
|
1335
|
-
if (!this
|
|
1336
|
-
const dir = path.resolve(this
|
|
1337
|
-
const tempPath = path.join(dir, `.${path.basename(this
|
|
1334
|
+
async #writeEntriesAtomically(entries: FileEntry[]): Promise<void> {
|
|
1335
|
+
if (!this.#sessionFile) return;
|
|
1336
|
+
const dir = path.resolve(this.#sessionFile, "..");
|
|
1337
|
+
const tempPath = path.join(dir, `.${path.basename(this.#sessionFile)}.${Snowflake.next()}.tmp`);
|
|
1338
1338
|
const writer = new NdjsonFileWriter(this.storage, tempPath, { flags: "w" });
|
|
1339
1339
|
try {
|
|
1340
1340
|
for (const entry of entries) {
|
|
@@ -1343,8 +1343,7 @@ export class SessionManager {
|
|
|
1343
1343
|
await writer.flush();
|
|
1344
1344
|
await writer.fsync();
|
|
1345
1345
|
await writer.close();
|
|
1346
|
-
await this.storage.rename(tempPath, this
|
|
1347
|
-
this.storage.fsyncDirSync(dir);
|
|
1346
|
+
await this.storage.rename(tempPath, this.#sessionFile);
|
|
1348
1347
|
} catch (err) {
|
|
1349
1348
|
try {
|
|
1350
1349
|
await writer.close();
|
|
@@ -1360,15 +1359,15 @@ export class SessionManager {
|
|
|
1360
1359
|
}
|
|
1361
1360
|
}
|
|
1362
1361
|
|
|
1363
|
-
|
|
1364
|
-
if (!this.persist || !this
|
|
1365
|
-
await this
|
|
1366
|
-
await this
|
|
1362
|
+
async #rewriteFile(): Promise<void> {
|
|
1363
|
+
if (!this.persist || !this.#sessionFile) return;
|
|
1364
|
+
await this.#queuePersistTask(async () => {
|
|
1365
|
+
await this.#closePersistWriterInternal();
|
|
1367
1366
|
const entries = await Promise.all(
|
|
1368
|
-
this
|
|
1367
|
+
this.#fileEntries.map(entry => prepareEntryForPersistence(entry, this.#blobStore)),
|
|
1369
1368
|
);
|
|
1370
|
-
await this
|
|
1371
|
-
this
|
|
1369
|
+
await this.#writeEntriesAtomically(entries);
|
|
1370
|
+
this.#flushed = true;
|
|
1372
1371
|
});
|
|
1373
1372
|
}
|
|
1374
1373
|
|
|
@@ -1378,14 +1377,14 @@ export class SessionManager {
|
|
|
1378
1377
|
|
|
1379
1378
|
/** Flush pending writes to disk. Call before switching sessions or on shutdown. */
|
|
1380
1379
|
async flush(): Promise<void> {
|
|
1381
|
-
if (!this
|
|
1382
|
-
await this
|
|
1383
|
-
if (this
|
|
1384
|
-
await this
|
|
1385
|
-
await this
|
|
1380
|
+
if (!this.#persistWriter) return;
|
|
1381
|
+
await this.#queuePersistTask(async () => {
|
|
1382
|
+
if (this.#persistWriter) {
|
|
1383
|
+
await this.#persistWriter.flush();
|
|
1384
|
+
await this.#persistWriter.fsync();
|
|
1386
1385
|
}
|
|
1387
1386
|
});
|
|
1388
|
-
if (this
|
|
1387
|
+
if (this.#persistError) throw this.#persistError;
|
|
1389
1388
|
}
|
|
1390
1389
|
|
|
1391
1390
|
getCwd(): string {
|
|
@@ -1394,7 +1393,7 @@ export class SessionManager {
|
|
|
1394
1393
|
|
|
1395
1394
|
/** Get usage statistics across all assistant messages in the session. */
|
|
1396
1395
|
getUsageStatistics(): UsageStatistics {
|
|
1397
|
-
return this
|
|
1396
|
+
return this.#usageStatistics;
|
|
1398
1397
|
}
|
|
1399
1398
|
|
|
1400
1399
|
getSessionDir(): string {
|
|
@@ -1402,86 +1401,88 @@ export class SessionManager {
|
|
|
1402
1401
|
}
|
|
1403
1402
|
|
|
1404
1403
|
getSessionId(): string {
|
|
1405
|
-
return this
|
|
1404
|
+
return this.#sessionId;
|
|
1406
1405
|
}
|
|
1407
1406
|
|
|
1408
1407
|
getSessionFile(): string | undefined {
|
|
1409
|
-
return this
|
|
1408
|
+
return this.#sessionFile;
|
|
1410
1409
|
}
|
|
1411
1410
|
|
|
1412
1411
|
getSessionName(): string | undefined {
|
|
1413
|
-
return this
|
|
1412
|
+
return this.#sessionName;
|
|
1414
1413
|
}
|
|
1415
1414
|
|
|
1416
1415
|
async setSessionName(name: string): Promise<void> {
|
|
1417
|
-
this
|
|
1416
|
+
this.#sessionName = name;
|
|
1418
1417
|
|
|
1419
1418
|
// Update the in-memory header (so first flush includes title)
|
|
1420
|
-
const header = this
|
|
1419
|
+
const header = this.#fileEntries.find(e => e.type === "session") as SessionHeader | undefined;
|
|
1421
1420
|
if (header) {
|
|
1422
1421
|
header.title = name;
|
|
1423
1422
|
}
|
|
1424
1423
|
|
|
1425
1424
|
// Update the session file header with the title (if already flushed)
|
|
1426
|
-
const sessionFile = this
|
|
1425
|
+
const sessionFile = this.#sessionFile;
|
|
1427
1426
|
if (this.persist && sessionFile && this.storage.existsSync(sessionFile)) {
|
|
1428
|
-
await this
|
|
1427
|
+
await this.#rewriteFile();
|
|
1429
1428
|
}
|
|
1430
1429
|
}
|
|
1431
1430
|
|
|
1432
1431
|
_persist(entry: SessionEntry): void {
|
|
1433
|
-
if (!this.persist || !this
|
|
1434
|
-
if (this
|
|
1432
|
+
if (!this.persist || !this.#sessionFile) return;
|
|
1433
|
+
if (this.#persistError) throw this.#persistError;
|
|
1435
1434
|
|
|
1436
|
-
const hasAssistant = this
|
|
1435
|
+
const hasAssistant = this.#fileEntries.some(e => e.type === "message" && e.message.role === "assistant");
|
|
1437
1436
|
if (!hasAssistant) {
|
|
1438
1437
|
// Mark as not flushed so when assistant arrives, all entries get written
|
|
1439
|
-
this
|
|
1438
|
+
this.#flushed = false;
|
|
1440
1439
|
return;
|
|
1441
1440
|
}
|
|
1442
1441
|
|
|
1443
|
-
if (!this
|
|
1444
|
-
this
|
|
1445
|
-
void this
|
|
1446
|
-
const writer = this
|
|
1442
|
+
if (!this.#flushed) {
|
|
1443
|
+
this.#flushed = true;
|
|
1444
|
+
void this.#queuePersistTask(async () => {
|
|
1445
|
+
const writer = this.#ensurePersistWriter();
|
|
1447
1446
|
if (!writer) return;
|
|
1448
|
-
const entries = await Promise.all(
|
|
1447
|
+
const entries = await Promise.all(
|
|
1448
|
+
this.#fileEntries.map(e => prepareEntryForPersistence(e, this.#blobStore)),
|
|
1449
|
+
);
|
|
1449
1450
|
for (const persistedEntry of entries) {
|
|
1450
1451
|
await writer.write(persistedEntry);
|
|
1451
1452
|
}
|
|
1452
1453
|
});
|
|
1453
1454
|
} else {
|
|
1454
|
-
void this
|
|
1455
|
-
const writer = this
|
|
1455
|
+
void this.#queuePersistTask(async () => {
|
|
1456
|
+
const writer = this.#ensurePersistWriter();
|
|
1456
1457
|
if (!writer) return;
|
|
1457
|
-
const persistedEntry = await prepareEntryForPersistence(entry, this
|
|
1458
|
+
const persistedEntry = await prepareEntryForPersistence(entry, this.#blobStore);
|
|
1458
1459
|
await writer.write(persistedEntry);
|
|
1459
1460
|
});
|
|
1460
1461
|
}
|
|
1461
1462
|
}
|
|
1462
1463
|
|
|
1463
|
-
|
|
1464
|
-
this
|
|
1465
|
-
this
|
|
1466
|
-
this
|
|
1464
|
+
#appendEntry(entry: SessionEntry): void {
|
|
1465
|
+
this.#fileEntries.push(entry);
|
|
1466
|
+
this.#byId.set(entry.id, entry);
|
|
1467
|
+
this.#leafId = entry.id;
|
|
1467
1468
|
this._persist(entry);
|
|
1468
1469
|
if (entry.type === "message" && entry.message.role === "assistant") {
|
|
1469
1470
|
const usage = entry.message.usage;
|
|
1470
|
-
this
|
|
1471
|
-
this
|
|
1472
|
-
this
|
|
1473
|
-
this
|
|
1474
|
-
this
|
|
1471
|
+
this.#usageStatistics.input += usage.input;
|
|
1472
|
+
this.#usageStatistics.output += usage.output;
|
|
1473
|
+
this.#usageStatistics.cacheRead += usage.cacheRead;
|
|
1474
|
+
this.#usageStatistics.cacheWrite += usage.cacheWrite;
|
|
1475
|
+
this.#usageStatistics.cost += usage.cost.total;
|
|
1475
1476
|
}
|
|
1476
1477
|
|
|
1477
1478
|
if (entry.type === "message" && entry.message.role === "toolResult" && entry.message.toolName === "task") {
|
|
1478
1479
|
const usage = getTaskToolUsage(entry.message.details);
|
|
1479
1480
|
if (usage) {
|
|
1480
|
-
this
|
|
1481
|
-
this
|
|
1482
|
-
this
|
|
1483
|
-
this
|
|
1484
|
-
this
|
|
1481
|
+
this.#usageStatistics.input += usage.input;
|
|
1482
|
+
this.#usageStatistics.output += usage.output;
|
|
1483
|
+
this.#usageStatistics.cacheRead += usage.cacheRead;
|
|
1484
|
+
this.#usageStatistics.cacheWrite += usage.cacheWrite;
|
|
1485
|
+
this.#usageStatistics.cost += usage.cost.total;
|
|
1485
1486
|
}
|
|
1486
1487
|
}
|
|
1487
1488
|
}
|
|
@@ -1503,12 +1504,12 @@ export class SessionManager {
|
|
|
1503
1504
|
): string {
|
|
1504
1505
|
const entry: SessionMessageEntry = {
|
|
1505
1506
|
type: "message",
|
|
1506
|
-
id: generateId(this
|
|
1507
|
-
parentId: this
|
|
1507
|
+
id: generateId(this.#byId),
|
|
1508
|
+
parentId: this.#leafId,
|
|
1508
1509
|
timestamp: new Date().toISOString(),
|
|
1509
1510
|
message,
|
|
1510
1511
|
};
|
|
1511
|
-
this
|
|
1512
|
+
this.#appendEntry(entry);
|
|
1512
1513
|
return entry.id;
|
|
1513
1514
|
}
|
|
1514
1515
|
|
|
@@ -1516,12 +1517,12 @@ export class SessionManager {
|
|
|
1516
1517
|
appendThinkingLevelChange(thinkingLevel: string): string {
|
|
1517
1518
|
const entry: ThinkingLevelChangeEntry = {
|
|
1518
1519
|
type: "thinking_level_change",
|
|
1519
|
-
id: generateId(this
|
|
1520
|
-
parentId: this
|
|
1520
|
+
id: generateId(this.#byId),
|
|
1521
|
+
parentId: this.#leafId,
|
|
1521
1522
|
timestamp: new Date().toISOString(),
|
|
1522
1523
|
thinkingLevel,
|
|
1523
1524
|
};
|
|
1524
|
-
this
|
|
1525
|
+
this.#appendEntry(entry);
|
|
1525
1526
|
return entry.id;
|
|
1526
1527
|
}
|
|
1527
1528
|
|
|
@@ -1529,13 +1530,13 @@ export class SessionManager {
|
|
|
1529
1530
|
appendModeChange(mode: string, data?: Record<string, unknown>): string {
|
|
1530
1531
|
const entry: ModeChangeEntry = {
|
|
1531
1532
|
type: "mode_change",
|
|
1532
|
-
id: generateId(this
|
|
1533
|
-
parentId: this
|
|
1533
|
+
id: generateId(this.#byId),
|
|
1534
|
+
parentId: this.#leafId,
|
|
1534
1535
|
timestamp: new Date().toISOString(),
|
|
1535
1536
|
mode,
|
|
1536
1537
|
data,
|
|
1537
1538
|
};
|
|
1538
|
-
this
|
|
1539
|
+
this.#appendEntry(entry);
|
|
1539
1540
|
return entry.id;
|
|
1540
1541
|
}
|
|
1541
1542
|
|
|
@@ -1547,13 +1548,13 @@ export class SessionManager {
|
|
|
1547
1548
|
appendModelChange(model: string, role?: string): string {
|
|
1548
1549
|
const entry: ModelChangeEntry = {
|
|
1549
1550
|
type: "model_change",
|
|
1550
|
-
id: generateId(this
|
|
1551
|
-
parentId: this
|
|
1551
|
+
id: generateId(this.#byId),
|
|
1552
|
+
parentId: this.#leafId,
|
|
1552
1553
|
timestamp: new Date().toISOString(),
|
|
1553
1554
|
model,
|
|
1554
1555
|
role,
|
|
1555
1556
|
};
|
|
1556
|
-
this
|
|
1557
|
+
this.#appendEntry(entry);
|
|
1557
1558
|
return entry.id;
|
|
1558
1559
|
}
|
|
1559
1560
|
|
|
@@ -1561,12 +1562,12 @@ export class SessionManager {
|
|
|
1561
1562
|
appendSessionInit(init: { systemPrompt: string; task: string; tools: string[]; outputSchema?: unknown }): string {
|
|
1562
1563
|
const entry: SessionInitEntry = {
|
|
1563
1564
|
type: "session_init",
|
|
1564
|
-
id: generateId(this
|
|
1565
|
-
parentId: this
|
|
1565
|
+
id: generateId(this.#byId),
|
|
1566
|
+
parentId: this.#leafId,
|
|
1566
1567
|
timestamp: new Date().toISOString(),
|
|
1567
1568
|
...init,
|
|
1568
1569
|
};
|
|
1569
|
-
this
|
|
1570
|
+
this.#appendEntry(entry);
|
|
1570
1571
|
return entry.id;
|
|
1571
1572
|
}
|
|
1572
1573
|
|
|
@@ -1582,8 +1583,8 @@ export class SessionManager {
|
|
|
1582
1583
|
): string {
|
|
1583
1584
|
const entry: CompactionEntry<T> = {
|
|
1584
1585
|
type: "compaction",
|
|
1585
|
-
id: generateId(this
|
|
1586
|
-
parentId: this
|
|
1586
|
+
id: generateId(this.#byId),
|
|
1587
|
+
parentId: this.#leafId,
|
|
1587
1588
|
timestamp: new Date().toISOString(),
|
|
1588
1589
|
summary,
|
|
1589
1590
|
shortSummary,
|
|
@@ -1593,7 +1594,7 @@ export class SessionManager {
|
|
|
1593
1594
|
fromExtension,
|
|
1594
1595
|
preserveData,
|
|
1595
1596
|
};
|
|
1596
|
-
this
|
|
1597
|
+
this.#appendEntry(entry);
|
|
1597
1598
|
return entry.id;
|
|
1598
1599
|
}
|
|
1599
1600
|
|
|
@@ -1603,11 +1604,11 @@ export class SessionManager {
|
|
|
1603
1604
|
type: "custom",
|
|
1604
1605
|
customType,
|
|
1605
1606
|
data,
|
|
1606
|
-
id: generateId(this
|
|
1607
|
-
parentId: this
|
|
1607
|
+
id: generateId(this.#byId),
|
|
1608
|
+
parentId: this.#leafId,
|
|
1608
1609
|
timestamp: new Date().toISOString(),
|
|
1609
1610
|
};
|
|
1610
|
-
this
|
|
1611
|
+
this.#appendEntry(entry);
|
|
1611
1612
|
return entry.id;
|
|
1612
1613
|
}
|
|
1613
1614
|
|
|
@@ -1616,8 +1617,8 @@ export class SessionManager {
|
|
|
1616
1617
|
* Use sparingly (e.g., pruning old tool outputs).
|
|
1617
1618
|
*/
|
|
1618
1619
|
async rewriteEntries(): Promise<void> {
|
|
1619
|
-
if (!this.persist || !this
|
|
1620
|
-
await this
|
|
1620
|
+
if (!this.persist || !this.#sessionFile) return;
|
|
1621
|
+
await this.#rewriteFile();
|
|
1621
1622
|
}
|
|
1622
1623
|
|
|
1623
1624
|
/**
|
|
@@ -1626,8 +1627,8 @@ export class SessionManager {
|
|
|
1626
1627
|
*/
|
|
1627
1628
|
async rewriteAssistantToolCallArgs(toolCallId: string, args: Record<string, unknown>): Promise<boolean> {
|
|
1628
1629
|
let updated = false;
|
|
1629
|
-
for (let i = this
|
|
1630
|
-
const entry = this
|
|
1630
|
+
for (let i = this.#fileEntries.length - 1; i >= 0; i--) {
|
|
1631
|
+
const entry = this.#fileEntries[i];
|
|
1631
1632
|
if (entry.type !== "message" || entry.message.role !== "assistant") continue;
|
|
1632
1633
|
const message = entry.message as { content?: unknown };
|
|
1633
1634
|
if (!Array.isArray(message.content)) continue;
|
|
@@ -1644,8 +1645,8 @@ export class SessionManager {
|
|
|
1644
1645
|
if (updated) break;
|
|
1645
1646
|
}
|
|
1646
1647
|
|
|
1647
|
-
if (updated && this.persist && this
|
|
1648
|
-
await this
|
|
1648
|
+
if (updated && this.persist && this.#sessionFile) {
|
|
1649
|
+
await this.#rewriteFile();
|
|
1649
1650
|
}
|
|
1650
1651
|
return updated;
|
|
1651
1652
|
}
|
|
@@ -1670,11 +1671,11 @@ export class SessionManager {
|
|
|
1670
1671
|
content,
|
|
1671
1672
|
display,
|
|
1672
1673
|
details,
|
|
1673
|
-
id: generateId(this
|
|
1674
|
-
parentId: this
|
|
1674
|
+
id: generateId(this.#byId),
|
|
1675
|
+
parentId: this.#leafId,
|
|
1675
1676
|
timestamp: new Date().toISOString(),
|
|
1676
1677
|
};
|
|
1677
|
-
this
|
|
1678
|
+
this.#appendEntry(entry);
|
|
1678
1679
|
return entry.id;
|
|
1679
1680
|
}
|
|
1680
1681
|
|
|
@@ -1690,12 +1691,12 @@ export class SessionManager {
|
|
|
1690
1691
|
appendTtsrInjection(ruleNames: string[]): string {
|
|
1691
1692
|
const entry: TtsrInjectionEntry = {
|
|
1692
1693
|
type: "ttsr_injection",
|
|
1693
|
-
id: generateId(this
|
|
1694
|
-
parentId: this
|
|
1694
|
+
id: generateId(this.#byId),
|
|
1695
|
+
parentId: this.#leafId,
|
|
1695
1696
|
timestamp: new Date().toISOString(),
|
|
1696
1697
|
injectedRules: ruleNames,
|
|
1697
1698
|
};
|
|
1698
|
-
this
|
|
1699
|
+
this.#appendEntry(entry);
|
|
1699
1700
|
return entry.id;
|
|
1700
1701
|
}
|
|
1701
1702
|
|
|
@@ -1721,11 +1722,11 @@ export class SessionManager {
|
|
|
1721
1722
|
// =========================================================================
|
|
1722
1723
|
|
|
1723
1724
|
getLeafId(): string | null {
|
|
1724
|
-
return this
|
|
1725
|
+
return this.#leafId;
|
|
1725
1726
|
}
|
|
1726
1727
|
|
|
1727
1728
|
getLeafEntry(): SessionEntry | undefined {
|
|
1728
|
-
return this
|
|
1729
|
+
return this.#leafId ? this.#byId.get(this.#leafId) : undefined;
|
|
1729
1730
|
}
|
|
1730
1731
|
|
|
1731
1732
|
/**
|
|
@@ -1738,13 +1739,13 @@ export class SessionManager {
|
|
|
1738
1739
|
if (current.type === "model_change") {
|
|
1739
1740
|
return current.role ?? "default";
|
|
1740
1741
|
}
|
|
1741
|
-
current = current.parentId ? this
|
|
1742
|
+
current = current.parentId ? this.#byId.get(current.parentId) : undefined;
|
|
1742
1743
|
}
|
|
1743
1744
|
return undefined;
|
|
1744
1745
|
}
|
|
1745
1746
|
|
|
1746
1747
|
getEntry(id: string): SessionEntry | undefined {
|
|
1747
|
-
return this
|
|
1748
|
+
return this.#byId.get(id);
|
|
1748
1749
|
}
|
|
1749
1750
|
|
|
1750
1751
|
/**
|
|
@@ -1752,7 +1753,7 @@ export class SessionManager {
|
|
|
1752
1753
|
*/
|
|
1753
1754
|
getChildren(parentId: string): SessionEntry[] {
|
|
1754
1755
|
const children: SessionEntry[] = [];
|
|
1755
|
-
for (const entry of this
|
|
1756
|
+
for (const entry of this.#byId.values()) {
|
|
1756
1757
|
if (entry.parentId === parentId) {
|
|
1757
1758
|
children.push(entry);
|
|
1758
1759
|
}
|
|
@@ -1764,7 +1765,7 @@ export class SessionManager {
|
|
|
1764
1765
|
* Get the label for an entry, if any.
|
|
1765
1766
|
*/
|
|
1766
1767
|
getLabel(id: string): string | undefined {
|
|
1767
|
-
return this
|
|
1768
|
+
return this.#labelsById.get(id);
|
|
1768
1769
|
}
|
|
1769
1770
|
|
|
1770
1771
|
/**
|
|
@@ -1773,22 +1774,22 @@ export class SessionManager {
|
|
|
1773
1774
|
* Pass undefined or empty string to clear the label.
|
|
1774
1775
|
*/
|
|
1775
1776
|
appendLabelChange(targetId: string, label: string | undefined): string {
|
|
1776
|
-
if (!this
|
|
1777
|
+
if (!this.#byId.has(targetId)) {
|
|
1777
1778
|
throw new Error(`Entry ${targetId} not found`);
|
|
1778
1779
|
}
|
|
1779
1780
|
const entry: LabelEntry = {
|
|
1780
1781
|
type: "label",
|
|
1781
|
-
id: generateId(this
|
|
1782
|
-
parentId: this
|
|
1782
|
+
id: generateId(this.#byId),
|
|
1783
|
+
parentId: this.#leafId,
|
|
1783
1784
|
timestamp: new Date().toISOString(),
|
|
1784
1785
|
targetId,
|
|
1785
1786
|
label,
|
|
1786
1787
|
};
|
|
1787
|
-
this
|
|
1788
|
+
this.#appendEntry(entry);
|
|
1788
1789
|
if (label) {
|
|
1789
|
-
this
|
|
1790
|
+
this.#labelsById.set(targetId, label);
|
|
1790
1791
|
} else {
|
|
1791
|
-
this
|
|
1792
|
+
this.#labelsById.delete(targetId);
|
|
1792
1793
|
}
|
|
1793
1794
|
return entry.id;
|
|
1794
1795
|
}
|
|
@@ -1800,11 +1801,11 @@ export class SessionManager {
|
|
|
1800
1801
|
*/
|
|
1801
1802
|
getBranch(fromId?: string): SessionEntry[] {
|
|
1802
1803
|
const path: SessionEntry[] = [];
|
|
1803
|
-
const startId = fromId ?? this
|
|
1804
|
-
let current = startId ? this
|
|
1804
|
+
const startId = fromId ?? this.#leafId;
|
|
1805
|
+
let current = startId ? this.#byId.get(startId) : undefined;
|
|
1805
1806
|
while (current) {
|
|
1806
1807
|
path.unshift(current);
|
|
1807
|
-
current = current.parentId ? this
|
|
1808
|
+
current = current.parentId ? this.#byId.get(current.parentId) : undefined;
|
|
1808
1809
|
}
|
|
1809
1810
|
return path;
|
|
1810
1811
|
}
|
|
@@ -1814,14 +1815,14 @@ export class SessionManager {
|
|
|
1814
1815
|
* Uses tree traversal from current leaf.
|
|
1815
1816
|
*/
|
|
1816
1817
|
buildSessionContext(): SessionContext {
|
|
1817
|
-
return buildSessionContext(this.getEntries(), this
|
|
1818
|
+
return buildSessionContext(this.getEntries(), this.#leafId, this.#byId);
|
|
1818
1819
|
}
|
|
1819
1820
|
|
|
1820
1821
|
/**
|
|
1821
1822
|
* Get session header.
|
|
1822
1823
|
*/
|
|
1823
1824
|
getHeader(): SessionHeader | null {
|
|
1824
|
-
const h = this
|
|
1825
|
+
const h = this.#fileEntries.find(e => e.type === "session");
|
|
1825
1826
|
return h ? (h as SessionHeader) : null;
|
|
1826
1827
|
}
|
|
1827
1828
|
|
|
@@ -1831,7 +1832,7 @@ export class SessionManager {
|
|
|
1831
1832
|
* change the leaf pointer. Entries cannot be modified or deleted.
|
|
1832
1833
|
*/
|
|
1833
1834
|
getEntries(): SessionEntry[] {
|
|
1834
|
-
return this
|
|
1835
|
+
return this.#fileEntries.filter((e): e is SessionEntry => e.type !== "session");
|
|
1835
1836
|
}
|
|
1836
1837
|
|
|
1837
1838
|
/**
|
|
@@ -1846,7 +1847,7 @@ export class SessionManager {
|
|
|
1846
1847
|
|
|
1847
1848
|
// Create nodes with resolved labels
|
|
1848
1849
|
for (const entry of entries) {
|
|
1849
|
-
const label = this
|
|
1850
|
+
const label = this.#labelsById.get(entry.id);
|
|
1850
1851
|
nodeMap.set(entry.id, { entry, children: [], label });
|
|
1851
1852
|
}
|
|
1852
1853
|
|
|
@@ -1889,10 +1890,10 @@ export class SessionManager {
|
|
|
1889
1890
|
* are not modified or deleted.
|
|
1890
1891
|
*/
|
|
1891
1892
|
branch(branchFromId: string): void {
|
|
1892
|
-
if (!this
|
|
1893
|
+
if (!this.#byId.has(branchFromId)) {
|
|
1893
1894
|
throw new Error(`Entry ${branchFromId} not found`);
|
|
1894
1895
|
}
|
|
1895
|
-
this
|
|
1896
|
+
this.#leafId = branchFromId;
|
|
1896
1897
|
}
|
|
1897
1898
|
|
|
1898
1899
|
/**
|
|
@@ -1901,7 +1902,7 @@ export class SessionManager {
|
|
|
1901
1902
|
* Use this when navigating to re-edit the first user message.
|
|
1902
1903
|
*/
|
|
1903
1904
|
resetLeaf(): void {
|
|
1904
|
-
this
|
|
1905
|
+
this.#leafId = null;
|
|
1905
1906
|
}
|
|
1906
1907
|
|
|
1907
1908
|
/**
|
|
@@ -1910,13 +1911,13 @@ export class SessionManager {
|
|
|
1910
1911
|
* context from the abandoned conversation path.
|
|
1911
1912
|
*/
|
|
1912
1913
|
branchWithSummary(branchFromId: string | null, summary: string, details?: unknown, fromExtension?: boolean): string {
|
|
1913
|
-
if (branchFromId !== null && !this
|
|
1914
|
+
if (branchFromId !== null && !this.#byId.has(branchFromId)) {
|
|
1914
1915
|
throw new Error(`Entry ${branchFromId} not found`);
|
|
1915
1916
|
}
|
|
1916
|
-
this
|
|
1917
|
+
this.#leafId = branchFromId;
|
|
1917
1918
|
const entry: BranchSummaryEntry = {
|
|
1918
1919
|
type: "branch_summary",
|
|
1919
|
-
id: generateId(this
|
|
1920
|
+
id: generateId(this.#byId),
|
|
1920
1921
|
parentId: branchFromId,
|
|
1921
1922
|
timestamp: new Date().toISOString(),
|
|
1922
1923
|
fromId: branchFromId ?? "root",
|
|
@@ -1924,7 +1925,7 @@ export class SessionManager {
|
|
|
1924
1925
|
details,
|
|
1925
1926
|
fromExtension,
|
|
1926
1927
|
};
|
|
1927
|
-
this
|
|
1928
|
+
this.#appendEntry(entry);
|
|
1928
1929
|
return entry.id;
|
|
1929
1930
|
}
|
|
1930
1931
|
|
|
@@ -1934,7 +1935,7 @@ export class SessionManager {
|
|
|
1934
1935
|
* Returns the new session file path, or undefined if not persisting.
|
|
1935
1936
|
*/
|
|
1936
1937
|
createBranchedSession(leafId: string): string | undefined {
|
|
1937
|
-
const previousSessionFile = this
|
|
1938
|
+
const previousSessionFile = this.#sessionFile;
|
|
1938
1939
|
const branchPath = this.getBranch(leafId);
|
|
1939
1940
|
if (branchPath.length === 0) {
|
|
1940
1941
|
throw new Error(`Entry ${leafId} not found`);
|
|
@@ -1960,7 +1961,7 @@ export class SessionManager {
|
|
|
1960
1961
|
// Collect labels for entries in the path
|
|
1961
1962
|
const pathEntryIds = new Set(pathWithoutLabels.map(e => e.id));
|
|
1962
1963
|
const labelsToWrite: Array<{ targetId: string; label: string }> = [];
|
|
1963
|
-
for (const [targetId, label] of this
|
|
1964
|
+
for (const [targetId, label] of this.#labelsById) {
|
|
1964
1965
|
if (pathEntryIds.has(targetId)) {
|
|
1965
1966
|
labelsToWrite.push({ targetId, label });
|
|
1966
1967
|
}
|
|
@@ -1991,11 +1992,11 @@ export class SessionManager {
|
|
|
1991
1992
|
parentId = labelEntry.id;
|
|
1992
1993
|
}
|
|
1993
1994
|
this.storage.writeTextSync(newSessionFile, `${lines.join("\n")}\n`);
|
|
1994
|
-
this
|
|
1995
|
-
this
|
|
1996
|
-
this
|
|
1997
|
-
this
|
|
1998
|
-
this
|
|
1995
|
+
this.#fileEntries = [header, ...pathWithoutLabels, ...labelEntries];
|
|
1996
|
+
this.#sessionId = newSessionId;
|
|
1997
|
+
this.#sessionFile = newSessionFile;
|
|
1998
|
+
this.#flushed = true;
|
|
1999
|
+
this.#buildIndex();
|
|
1999
2000
|
return newSessionFile;
|
|
2000
2001
|
}
|
|
2001
2002
|
|
|
@@ -2014,9 +2015,9 @@ export class SessionManager {
|
|
|
2014
2015
|
labelEntries.push(labelEntry);
|
|
2015
2016
|
parentId = labelEntry.id;
|
|
2016
2017
|
}
|
|
2017
|
-
this
|
|
2018
|
-
this
|
|
2019
|
-
this
|
|
2018
|
+
this.#fileEntries = [header, ...pathWithoutLabels, ...labelEntries];
|
|
2019
|
+
this.#sessionId = newSessionId;
|
|
2020
|
+
this.#buildIndex();
|
|
2020
2021
|
return undefined;
|
|
2021
2022
|
}
|
|
2022
2023
|
|
|
@@ -2028,7 +2029,7 @@ export class SessionManager {
|
|
|
2028
2029
|
static create(cwd: string, sessionDir?: string, storage: SessionStorage = new FileSessionStorage()): SessionManager {
|
|
2029
2030
|
const dir = sessionDir ?? getDefaultSessionDir(cwd, storage);
|
|
2030
2031
|
const manager = new SessionManager(cwd, dir, true, storage);
|
|
2031
|
-
manager
|
|
2032
|
+
manager.#initNewSession();
|
|
2032
2033
|
return manager;
|
|
2033
2034
|
}
|
|
2034
2035
|
|
|
@@ -2046,16 +2047,16 @@ export class SessionManager {
|
|
|
2046
2047
|
const manager = new SessionManager(cwd, dir, true, storage);
|
|
2047
2048
|
const forkEntries = structuredClone(await loadEntriesFromFile(sourcePath, storage)) as FileEntry[];
|
|
2048
2049
|
migrateToCurrentVersion(forkEntries);
|
|
2049
|
-
await resolveBlobRefsInEntries(forkEntries, manager
|
|
2050
|
+
await resolveBlobRefsInEntries(forkEntries, manager.#blobStore);
|
|
2050
2051
|
const sourceHeader = forkEntries.find(e => e.type === "session") as SessionHeader | undefined;
|
|
2051
2052
|
const historyEntries = forkEntries.filter(entry => entry.type !== "session") as SessionEntry[];
|
|
2052
|
-
manager
|
|
2053
|
-
const newHeader = manager
|
|
2053
|
+
manager.#newSessionSync({ parentSession: sourceHeader?.id });
|
|
2054
|
+
const newHeader = manager.#fileEntries[0] as SessionHeader;
|
|
2054
2055
|
newHeader.title = sourceHeader?.title;
|
|
2055
|
-
manager
|
|
2056
|
-
manager
|
|
2057
|
-
manager
|
|
2058
|
-
await manager
|
|
2056
|
+
manager.#fileEntries = [newHeader, ...historyEntries];
|
|
2057
|
+
manager.#sessionName = newHeader.title;
|
|
2058
|
+
manager.#buildIndex();
|
|
2059
|
+
await manager.#rewriteFile();
|
|
2059
2060
|
return manager;
|
|
2060
2061
|
}
|
|
2061
2062
|
|
|
@@ -2076,7 +2077,7 @@ export class SessionManager {
|
|
|
2076
2077
|
// If no sessionDir provided, derive from file's parent directory
|
|
2077
2078
|
const dir = sessionDir ?? path.resolve(filePath, "..");
|
|
2078
2079
|
const manager = new SessionManager(cwd, dir, true, storage);
|
|
2079
|
-
await manager
|
|
2080
|
+
await manager.#initSessionFile(filePath);
|
|
2080
2081
|
return manager;
|
|
2081
2082
|
}
|
|
2082
2083
|
|
|
@@ -2096,9 +2097,9 @@ export class SessionManager {
|
|
|
2096
2097
|
const mostRecent = terminalSession ?? (await findMostRecentSession(dir, storage));
|
|
2097
2098
|
const manager = new SessionManager(cwd, dir, true, storage);
|
|
2098
2099
|
if (mostRecent) {
|
|
2099
|
-
await manager
|
|
2100
|
+
await manager.#initSessionFile(mostRecent);
|
|
2100
2101
|
} else {
|
|
2101
|
-
manager
|
|
2102
|
+
manager.#initNewSession();
|
|
2102
2103
|
}
|
|
2103
2104
|
return manager;
|
|
2104
2105
|
}
|
|
@@ -2106,7 +2107,7 @@ export class SessionManager {
|
|
|
2106
2107
|
/** Create an in-memory session (no file persistence) */
|
|
2107
2108
|
static inMemory(cwd: string = process.cwd(), storage: SessionStorage = new MemorySessionStorage()): SessionManager {
|
|
2108
2109
|
const manager = new SessionManager(cwd, "", false, storage);
|
|
2109
|
-
manager
|
|
2110
|
+
manager.#initNewSession();
|
|
2110
2111
|
return manager;
|
|
2111
2112
|
}
|
|
2112
2113
|
|