@oh-my-pi/pi-coding-agent 3.13.1337 → 3.15.0
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/CHANGELOG.md +88 -0
- package/docs/theme.md +38 -5
- package/examples/sdk/11-sessions.ts +2 -2
- package/package.json +7 -4
- package/src/cli/file-processor.ts +51 -2
- package/src/cli/plugin-cli.ts +25 -19
- package/src/cli/update-cli.ts +4 -3
- package/src/core/agent-session.ts +31 -4
- package/src/core/compaction/branch-summarization.ts +4 -32
- package/src/core/compaction/compaction.ts +6 -84
- package/src/core/compaction/utils.ts +2 -3
- package/src/core/custom-tools/types.ts +2 -0
- package/src/core/export-html/index.ts +1 -1
- package/src/core/hooks/index.ts +1 -1
- package/src/core/hooks/tool-wrapper.ts +0 -1
- package/src/core/hooks/types.ts +2 -2
- package/src/core/plugins/doctor.ts +9 -1
- package/src/core/sdk.ts +2 -1
- package/src/core/session-manager.ts +552 -41
- package/src/core/settings-manager.ts +174 -0
- package/src/core/system-prompt.ts +9 -14
- package/src/core/title-generator.ts +2 -8
- package/src/core/tools/ask.ts +19 -37
- package/src/core/tools/bash.ts +2 -37
- package/src/core/tools/edit.ts +2 -9
- package/src/core/tools/exa/render.ts +52 -48
- package/src/core/tools/find.ts +10 -8
- package/src/core/tools/grep.ts +45 -17
- package/src/core/tools/ls.ts +22 -2
- package/src/core/tools/lsp/clients/biome-client.ts +207 -0
- package/src/core/tools/lsp/clients/index.ts +49 -0
- package/src/core/tools/lsp/clients/lsp-linter-client.ts +98 -0
- package/src/core/tools/lsp/config.ts +3 -0
- package/src/core/tools/lsp/index.ts +107 -55
- package/src/core/tools/lsp/render.ts +192 -79
- package/src/core/tools/lsp/types.ts +27 -0
- package/src/core/tools/lsp/utils.ts +62 -22
- package/src/core/tools/notebook.ts +9 -1
- package/src/core/tools/output.ts +37 -14
- package/src/core/tools/read.ts +349 -34
- package/src/core/tools/renderers.ts +290 -89
- package/src/core/tools/review.ts +12 -5
- package/src/core/tools/task/agents.ts +5 -5
- package/src/core/tools/task/commands.ts +3 -3
- package/src/core/tools/task/executor.ts +33 -1
- package/src/core/tools/task/index.ts +93 -6
- package/src/core/tools/task/render.ts +147 -66
- package/src/core/tools/task/types.ts +14 -9
- package/src/core/tools/web-fetch.ts +242 -103
- package/src/core/tools/web-search/index.ts +64 -20
- package/src/core/tools/web-search/providers/exa.ts +68 -172
- package/src/core/tools/web-search/render.ts +264 -74
- package/src/core/tools/write.ts +2 -8
- package/src/main.ts +10 -6
- package/src/modes/cleanup.ts +23 -0
- package/src/modes/index.ts +9 -4
- package/src/modes/interactive/components/bash-execution.ts +6 -3
- package/src/modes/interactive/components/branch-summary-message.ts +1 -1
- package/src/modes/interactive/components/compaction-summary-message.ts +1 -1
- package/src/modes/interactive/components/dynamic-border.ts +1 -1
- package/src/modes/interactive/components/extensions/extension-dashboard.ts +4 -5
- package/src/modes/interactive/components/extensions/extension-list.ts +18 -16
- package/src/modes/interactive/components/extensions/inspector-panel.ts +8 -8
- package/src/modes/interactive/components/hook-message.ts +2 -2
- package/src/modes/interactive/components/hook-selector.ts +1 -1
- package/src/modes/interactive/components/model-selector.ts +22 -9
- package/src/modes/interactive/components/oauth-selector.ts +20 -4
- package/src/modes/interactive/components/plugin-settings.ts +4 -2
- package/src/modes/interactive/components/session-selector.ts +9 -6
- package/src/modes/interactive/components/settings-defs.ts +285 -1
- package/src/modes/interactive/components/settings-selector.ts +176 -3
- package/src/modes/interactive/components/status-line/index.ts +4 -0
- package/src/modes/interactive/components/status-line/presets.ts +94 -0
- package/src/modes/interactive/components/status-line/segments.ts +350 -0
- package/src/modes/interactive/components/status-line/separators.ts +55 -0
- package/src/modes/interactive/components/status-line/types.ts +81 -0
- package/src/modes/interactive/components/status-line-segment-editor.ts +357 -0
- package/src/modes/interactive/components/status-line.ts +169 -233
- package/src/modes/interactive/components/tool-execution.ts +446 -211
- package/src/modes/interactive/components/tree-selector.ts +17 -6
- package/src/modes/interactive/components/ttsr-notification.ts +4 -4
- package/src/modes/interactive/components/welcome.ts +27 -19
- package/src/modes/interactive/interactive-mode.ts +98 -13
- package/src/modes/interactive/theme/dark.json +3 -2
- package/src/modes/interactive/theme/defaults/dark-arctic.json +111 -0
- package/src/modes/interactive/theme/defaults/dark-catppuccin.json +106 -0
- package/src/modes/interactive/theme/defaults/dark-cyberpunk.json +109 -0
- package/src/modes/interactive/theme/defaults/dark-dracula.json +105 -0
- package/src/modes/interactive/theme/defaults/dark-forest.json +103 -0
- package/src/modes/interactive/theme/defaults/dark-github.json +112 -0
- package/src/modes/interactive/theme/defaults/dark-gruvbox.json +119 -0
- package/src/modes/interactive/theme/defaults/dark-monochrome.json +101 -0
- package/src/modes/interactive/theme/defaults/dark-monokai.json +105 -0
- package/src/modes/interactive/theme/defaults/dark-nord.json +104 -0
- package/src/modes/interactive/theme/defaults/dark-ocean.json +108 -0
- package/src/modes/interactive/theme/defaults/dark-one.json +107 -0
- package/src/modes/interactive/theme/defaults/dark-retro.json +99 -0
- package/src/modes/interactive/theme/defaults/dark-rose-pine.json +95 -0
- package/src/modes/interactive/theme/defaults/dark-solarized.json +96 -0
- package/src/modes/interactive/theme/defaults/dark-sunset.json +106 -0
- package/src/modes/interactive/theme/defaults/dark-synthwave.json +102 -0
- package/src/modes/interactive/theme/defaults/dark-tokyo-night.json +108 -0
- package/src/modes/interactive/theme/defaults/index.ts +67 -0
- package/src/modes/interactive/theme/defaults/light-arctic.json +106 -0
- package/src/modes/interactive/theme/defaults/light-catppuccin.json +105 -0
- package/src/modes/interactive/theme/defaults/light-cyberpunk.json +103 -0
- package/src/modes/interactive/theme/defaults/light-forest.json +107 -0
- package/src/modes/interactive/theme/defaults/light-github.json +114 -0
- package/src/modes/interactive/theme/defaults/light-gruvbox.json +115 -0
- package/src/modes/interactive/theme/defaults/light-monochrome.json +100 -0
- package/src/modes/interactive/theme/defaults/light-ocean.json +106 -0
- package/src/modes/interactive/theme/defaults/light-one.json +105 -0
- package/src/modes/interactive/theme/defaults/light-retro.json +105 -0
- package/src/modes/interactive/theme/defaults/light-solarized.json +101 -0
- package/src/modes/interactive/theme/defaults/light-sunset.json +106 -0
- package/src/modes/interactive/theme/defaults/light-synthwave.json +105 -0
- package/src/modes/interactive/theme/defaults/light-tokyo-night.json +118 -0
- package/src/modes/interactive/theme/light.json +3 -2
- package/src/modes/interactive/theme/theme-schema.json +120 -4
- package/src/modes/interactive/theme/theme.ts +1228 -14
- package/src/prompts/branch-summary-preamble.md +3 -0
- package/src/prompts/branch-summary.md +28 -0
- package/src/prompts/compaction-summary.md +34 -0
- package/src/prompts/compaction-turn-prefix.md +16 -0
- package/src/prompts/compaction-update-summary.md +41 -0
- package/src/prompts/init.md +30 -0
- package/src/{core/tools/task/bundled-agents → prompts}/reviewer.md +6 -0
- package/src/prompts/summarization-system.md +3 -0
- package/src/prompts/system-prompt.md +27 -0
- package/src/{core/tools/task/bundled-agents → prompts}/task.md +2 -0
- package/src/prompts/title-system.md +8 -0
- package/src/prompts/tools/ask.md +24 -0
- package/src/prompts/tools/bash.md +23 -0
- package/src/prompts/tools/edit.md +9 -0
- package/src/prompts/tools/find.md +6 -0
- package/src/prompts/tools/grep.md +12 -0
- package/src/prompts/tools/lsp.md +14 -0
- package/src/prompts/tools/output.md +23 -0
- package/src/prompts/tools/read.md +25 -0
- package/src/prompts/tools/web-fetch.md +8 -0
- package/src/prompts/tools/web-search.md +10 -0
- package/src/prompts/tools/write.md +10 -0
- package/src/commands/init.md +0 -20
- /package/src/{core/tools/task/bundled-commands → prompts}/architect-plan.md +0 -0
- /package/src/{core/tools/task/bundled-agents → prompts}/browser.md +0 -0
- /package/src/{core/tools/task/bundled-agents → prompts}/explore.md +0 -0
- /package/src/{core/tools/task/bundled-commands → prompts}/implement-with-critic.md +0 -0
- /package/src/{core/tools/task/bundled-commands → prompts}/implement.md +0 -0
- /package/src/{core/tools/task/bundled-agents → prompts}/plan.md +0 -0
|
@@ -1,19 +1,25 @@
|
|
|
1
1
|
import {
|
|
2
2
|
appendFileSync,
|
|
3
3
|
closeSync,
|
|
4
|
+
createWriteStream,
|
|
4
5
|
existsSync,
|
|
6
|
+
fsyncSync,
|
|
5
7
|
mkdirSync,
|
|
6
8
|
openSync,
|
|
7
9
|
readdirSync,
|
|
8
10
|
readFileSync,
|
|
9
11
|
readSync,
|
|
12
|
+
renameSync,
|
|
10
13
|
statSync,
|
|
14
|
+
unlinkSync,
|
|
15
|
+
type WriteStream,
|
|
11
16
|
writeFileSync,
|
|
12
17
|
} from "node:fs";
|
|
13
|
-
import { join, resolve } from "node:path";
|
|
18
|
+
import { basename, join, resolve } from "node:path";
|
|
14
19
|
import type { AgentMessage } from "@oh-my-pi/pi-agent-core";
|
|
15
|
-
import type { ImageContent, Message, TextContent } from "@oh-my-pi/pi-ai";
|
|
20
|
+
import type { ImageContent, Message, TextContent, Usage } from "@oh-my-pi/pi-ai";
|
|
16
21
|
import { nanoid } from "nanoid";
|
|
22
|
+
import sharp from "sharp";
|
|
17
23
|
import { getAgentDir as getDefaultAgentDir } from "../config";
|
|
18
24
|
import {
|
|
19
25
|
type BashExecutionMessage,
|
|
@@ -192,6 +198,7 @@ export type ReadonlySessionManager = Pick<
|
|
|
192
198
|
| "getHeader"
|
|
193
199
|
| "getEntries"
|
|
194
200
|
| "getTree"
|
|
201
|
+
| "getUsageStatistics"
|
|
195
202
|
>;
|
|
196
203
|
|
|
197
204
|
/** Generate a unique short ID (8 hex chars, collision-checked) */
|
|
@@ -283,6 +290,10 @@ export function getLatestCompactionEntry(entries: SessionEntry[]): CompactionEnt
|
|
|
283
290
|
return null;
|
|
284
291
|
}
|
|
285
292
|
|
|
293
|
+
function toError(value: unknown): Error {
|
|
294
|
+
return value instanceof Error ? value : new Error(String(value));
|
|
295
|
+
}
|
|
296
|
+
|
|
286
297
|
/**
|
|
287
298
|
* Build the session context from entries using tree traversal.
|
|
288
299
|
* If leafId is provided, walks from that entry to root.
|
|
@@ -570,6 +581,290 @@ function formatTimeAgo(date: Date): string {
|
|
|
570
581
|
return date.toLocaleDateString();
|
|
571
582
|
}
|
|
572
583
|
|
|
584
|
+
const MAX_PERSIST_CHARS = 500_000;
|
|
585
|
+
const TRUNCATION_NOTICE = "\n\n[Session persistence truncated large content]";
|
|
586
|
+
const PLACEHOLDER_IMAGE_DATA =
|
|
587
|
+
"/9j/4AAQSkZJRgABAQEASABIAAD/2wBDAP//////////////////////////////////////////////////////////////////////////////////////2wBDAf//////////////////////////////////////////////////////////////////////////////////////wAARCAAQABADASIAAhEBAxEB/8QAFQABAQAAAAAAAAAAAAAAAAAAAAf/xAAUEAEAAAAAAAAAAAAAAAAAAAAA/8QAFQEBAQAAAAAAAAAAAAAAAAAAAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwCkA//Z";
|
|
588
|
+
|
|
589
|
+
const TEXT_CONTENT_KEY = "content";
|
|
590
|
+
|
|
591
|
+
function fsyncDirSync(dir: string): void {
|
|
592
|
+
try {
|
|
593
|
+
const fd = openSync(dir, "r");
|
|
594
|
+
try {
|
|
595
|
+
fsyncSync(fd);
|
|
596
|
+
} finally {
|
|
597
|
+
closeSync(fd);
|
|
598
|
+
}
|
|
599
|
+
} catch {
|
|
600
|
+
// Best-effort: some platforms/filesystems don't support fsync on directories.
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
/**
|
|
605
|
+
* Recursively truncate large strings in an object for session persistence.
|
|
606
|
+
* - Truncates any oversized string fields (key-agnostic)
|
|
607
|
+
* - Replaces oversized image blocks with text notices
|
|
608
|
+
* - Updates lineCount when content is truncated
|
|
609
|
+
* - Returns original object if no changes needed (structural sharing)
|
|
610
|
+
*/
|
|
611
|
+
function truncateString(value: string, maxLength: number): string {
|
|
612
|
+
if (value.length <= maxLength) return value;
|
|
613
|
+
let truncated = value.slice(0, maxLength);
|
|
614
|
+
if (truncated.length > 0) {
|
|
615
|
+
const last = truncated.charCodeAt(truncated.length - 1);
|
|
616
|
+
if (last >= 0xd800 && last <= 0xdbff) {
|
|
617
|
+
truncated = truncated.slice(0, -1);
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
return truncated;
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
function isImageBlock(value: unknown): value is { type: "image"; data: string; mimeType?: string } {
|
|
624
|
+
return (
|
|
625
|
+
typeof value === "object" &&
|
|
626
|
+
value !== null &&
|
|
627
|
+
"type" in value &&
|
|
628
|
+
(value as { type?: string }).type === "image" &&
|
|
629
|
+
"data" in value &&
|
|
630
|
+
typeof (value as { data?: string }).data === "string"
|
|
631
|
+
);
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
async function compressImageForPersistence(image: ImageContent): Promise<ImageContent> {
|
|
635
|
+
try {
|
|
636
|
+
const buffer = Buffer.from(image.data, "base64");
|
|
637
|
+
const pipeline = sharp(buffer, { failOnError: false });
|
|
638
|
+
const metadata = await pipeline.metadata();
|
|
639
|
+
const width = metadata.width ?? 0;
|
|
640
|
+
const height = metadata.height ?? 0;
|
|
641
|
+
const hasDims = width > 0 && height > 0;
|
|
642
|
+
const targetWidth = hasDims && width >= height ? 512 : undefined;
|
|
643
|
+
const targetHeight = hasDims && height > width ? 512 : undefined;
|
|
644
|
+
const resized = await pipeline
|
|
645
|
+
.resize({
|
|
646
|
+
width: hasDims ? targetWidth : 512,
|
|
647
|
+
height: hasDims ? targetHeight : 512,
|
|
648
|
+
fit: "inside",
|
|
649
|
+
withoutEnlargement: true,
|
|
650
|
+
})
|
|
651
|
+
.jpeg({ quality: 70 })
|
|
652
|
+
.toBuffer();
|
|
653
|
+
const base64 = resized.toString("base64");
|
|
654
|
+
if (base64.length > MAX_PERSIST_CHARS) {
|
|
655
|
+
return { type: "image", data: PLACEHOLDER_IMAGE_DATA, mimeType: "image/jpeg" };
|
|
656
|
+
}
|
|
657
|
+
return { type: "image", data: base64, mimeType: "image/jpeg" };
|
|
658
|
+
} catch {
|
|
659
|
+
return { type: "image", data: PLACEHOLDER_IMAGE_DATA, mimeType: "image/jpeg" };
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
async function truncateForPersistence<T>(obj: T, key?: string): Promise<T> {
|
|
664
|
+
if (obj === null || obj === undefined) return obj;
|
|
665
|
+
|
|
666
|
+
if (typeof obj === "string") {
|
|
667
|
+
if (obj.length > MAX_PERSIST_CHARS) {
|
|
668
|
+
const limit = Math.max(0, MAX_PERSIST_CHARS - TRUNCATION_NOTICE.length);
|
|
669
|
+
return `${truncateString(obj, limit)}${TRUNCATION_NOTICE}` as T;
|
|
670
|
+
}
|
|
671
|
+
return obj;
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
if (Array.isArray(obj)) {
|
|
675
|
+
let changed = false;
|
|
676
|
+
const result = await Promise.all(
|
|
677
|
+
obj.map(async (item) => {
|
|
678
|
+
// Special handling: compress oversized images while preserving shape
|
|
679
|
+
if (key === TEXT_CONTENT_KEY && isImageBlock(item)) {
|
|
680
|
+
if (item.data.length > MAX_PERSIST_CHARS) {
|
|
681
|
+
changed = true;
|
|
682
|
+
return compressImageForPersistence({
|
|
683
|
+
type: "image",
|
|
684
|
+
data: item.data,
|
|
685
|
+
mimeType: item.mimeType ?? "image/jpeg",
|
|
686
|
+
});
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
const newItem = await truncateForPersistence(item, key);
|
|
690
|
+
if (newItem !== item) changed = true;
|
|
691
|
+
return newItem;
|
|
692
|
+
}),
|
|
693
|
+
);
|
|
694
|
+
return changed ? (result as T) : obj;
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
if (typeof obj === "object") {
|
|
698
|
+
let changed = false;
|
|
699
|
+
const result: Record<string, unknown> = {};
|
|
700
|
+
for (const [k, v] of Object.entries(obj as Record<string, unknown>)) {
|
|
701
|
+
const newV = await truncateForPersistence(v, k);
|
|
702
|
+
result[k] = newV;
|
|
703
|
+
if (newV !== v) changed = true;
|
|
704
|
+
}
|
|
705
|
+
// Update lineCount if content was truncated (for FileMentionFile)
|
|
706
|
+
if (changed && "lineCount" in result && "content" in result && typeof result.content === "string") {
|
|
707
|
+
result.lineCount = result.content.split("\n").length;
|
|
708
|
+
}
|
|
709
|
+
return changed ? (result as T) : obj;
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
return obj;
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
async function prepareEntryForPersistence(entry: FileEntry): Promise<FileEntry> {
|
|
716
|
+
return truncateForPersistence(entry);
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
class NdjsonFileWriter {
|
|
720
|
+
private writeStream: WriteStream;
|
|
721
|
+
private closed = false;
|
|
722
|
+
private closing = false;
|
|
723
|
+
private error: Error | undefined;
|
|
724
|
+
private pendingWrites: Promise<void> = Promise.resolve();
|
|
725
|
+
private ready: Promise<void>;
|
|
726
|
+
private fd: number | null = null;
|
|
727
|
+
private onError: ((err: Error) => void) | undefined;
|
|
728
|
+
|
|
729
|
+
constructor(path: string, options?: { flags?: string; onError?: (err: Error) => void }) {
|
|
730
|
+
this.onError = options?.onError;
|
|
731
|
+
this.writeStream = createWriteStream(path, { flags: options?.flags ?? "a" });
|
|
732
|
+
this.ready = new Promise<void>((resolve, reject) => {
|
|
733
|
+
const onOpen = (fd: number) => {
|
|
734
|
+
this.fd = fd;
|
|
735
|
+
this.writeStream.off("error", onError);
|
|
736
|
+
resolve();
|
|
737
|
+
};
|
|
738
|
+
const onError = (err: Error) => {
|
|
739
|
+
this.writeStream.off("open", onOpen);
|
|
740
|
+
reject(err);
|
|
741
|
+
};
|
|
742
|
+
this.writeStream.once("open", onOpen);
|
|
743
|
+
this.writeStream.once("error", onError);
|
|
744
|
+
});
|
|
745
|
+
this.writeStream.on("error", (err: Error) => {
|
|
746
|
+
const writeErr = toError(err);
|
|
747
|
+
if (!this.error) this.error = writeErr;
|
|
748
|
+
this.onError?.(writeErr);
|
|
749
|
+
});
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
private enqueue(task: () => Promise<void>): Promise<void> {
|
|
753
|
+
const run = async () => {
|
|
754
|
+
if (this.error) throw this.error;
|
|
755
|
+
await task();
|
|
756
|
+
};
|
|
757
|
+
const next = this.pendingWrites.then(run);
|
|
758
|
+
this.pendingWrites = next.catch((err) => {
|
|
759
|
+
if (!this.error) this.error = toError(err);
|
|
760
|
+
});
|
|
761
|
+
return next;
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
private async writeLine(line: string): Promise<void> {
|
|
765
|
+
if (this.error) throw this.error;
|
|
766
|
+
await new Promise<void>((resolve, reject) => {
|
|
767
|
+
let settled = false;
|
|
768
|
+
const onError = (err: Error) => {
|
|
769
|
+
if (settled) return;
|
|
770
|
+
settled = true;
|
|
771
|
+
const writeErr = toError(err);
|
|
772
|
+
if (!this.error) this.error = writeErr;
|
|
773
|
+
this.writeStream.off("error", onError);
|
|
774
|
+
reject(writeErr);
|
|
775
|
+
};
|
|
776
|
+
this.writeStream.once("error", onError);
|
|
777
|
+
this.writeStream.write(line, (err) => {
|
|
778
|
+
if (settled) return;
|
|
779
|
+
settled = true;
|
|
780
|
+
this.writeStream.off("error", onError);
|
|
781
|
+
if (err) {
|
|
782
|
+
const writeErr = toError(err);
|
|
783
|
+
if (!this.error) this.error = writeErr;
|
|
784
|
+
reject(writeErr);
|
|
785
|
+
} else {
|
|
786
|
+
resolve();
|
|
787
|
+
}
|
|
788
|
+
});
|
|
789
|
+
if (this.error && !settled) {
|
|
790
|
+
settled = true;
|
|
791
|
+
this.writeStream.off("error", onError);
|
|
792
|
+
reject(this.error);
|
|
793
|
+
}
|
|
794
|
+
});
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
/** Queue a write. Returns a promise so callers can await if needed. */
|
|
798
|
+
write(entry: FileEntry): Promise<void> {
|
|
799
|
+
if (this.closed || this.closing) throw new Error("Writer closed");
|
|
800
|
+
if (this.error) throw this.error;
|
|
801
|
+
const line = `${JSON.stringify(entry)}\n`;
|
|
802
|
+
return this.enqueue(() => this.writeLine(line));
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
/** Flush all buffered data to disk. Waits for all queued writes and fsync. */
|
|
806
|
+
async flush(): Promise<void> {
|
|
807
|
+
if (this.closed) return;
|
|
808
|
+
if (this.error) throw this.error;
|
|
809
|
+
|
|
810
|
+
await this.enqueue(async () => {});
|
|
811
|
+
|
|
812
|
+
if (this.error) throw this.error;
|
|
813
|
+
|
|
814
|
+
await this.ready;
|
|
815
|
+
const fd = this.fd;
|
|
816
|
+
if (typeof fd === "number") {
|
|
817
|
+
try {
|
|
818
|
+
fsyncSync(fd);
|
|
819
|
+
} catch (err) {
|
|
820
|
+
const fsyncErr = toError(err);
|
|
821
|
+
if (!this.error) this.error = fsyncErr;
|
|
822
|
+
throw fsyncErr;
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
if (this.error) throw this.error;
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
/** Close the writer, flushing all data. */
|
|
830
|
+
async close(): Promise<void> {
|
|
831
|
+
if (this.closed || this.closing) return;
|
|
832
|
+
this.closing = true;
|
|
833
|
+
|
|
834
|
+
let closeError: Error | undefined;
|
|
835
|
+
try {
|
|
836
|
+
await this.flush();
|
|
837
|
+
} catch (err) {
|
|
838
|
+
closeError = toError(err);
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
await this.pendingWrites;
|
|
842
|
+
|
|
843
|
+
await new Promise<void>((resolve, reject) => {
|
|
844
|
+
this.writeStream.end((err?: Error | null) => {
|
|
845
|
+
if (err) {
|
|
846
|
+
const endErr = toError(err);
|
|
847
|
+
if (!this.error) this.error = endErr;
|
|
848
|
+
reject(endErr);
|
|
849
|
+
} else {
|
|
850
|
+
resolve();
|
|
851
|
+
}
|
|
852
|
+
});
|
|
853
|
+
});
|
|
854
|
+
|
|
855
|
+
this.closed = true;
|
|
856
|
+
this.writeStream.removeAllListeners();
|
|
857
|
+
|
|
858
|
+
if (closeError) throw closeError;
|
|
859
|
+
if (this.error) throw this.error;
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
/** Check if there's a stored error. */
|
|
863
|
+
getError(): Error | undefined {
|
|
864
|
+
return this.error;
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
|
|
573
868
|
/** Get recent sessions for display in welcome screen */
|
|
574
869
|
export function getRecentSessions(sessionDir: string, limit = 3): RecentSessionInfo[] {
|
|
575
870
|
return getSortedSessions(sessionDir).slice(0, limit);
|
|
@@ -586,6 +881,22 @@ export function getRecentSessions(sessionDir: string, limit = 3): RecentSessionI
|
|
|
586
881
|
* Use buildSessionContext() to get the resolved message list for the LLM, which
|
|
587
882
|
* handles compaction summaries and follows the path from root to current leaf.
|
|
588
883
|
*/
|
|
884
|
+
export interface UsageStatistics {
|
|
885
|
+
input: number;
|
|
886
|
+
output: number;
|
|
887
|
+
cacheRead: number;
|
|
888
|
+
cacheWrite: number;
|
|
889
|
+
cost: number;
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
function getTaskToolUsage(details: unknown): Usage | undefined {
|
|
893
|
+
if (!details || typeof details !== "object") return undefined;
|
|
894
|
+
const record = details as Record<string, unknown>;
|
|
895
|
+
const usage = record.usage;
|
|
896
|
+
if (!usage || typeof usage !== "object") return undefined;
|
|
897
|
+
return usage as Usage;
|
|
898
|
+
}
|
|
899
|
+
|
|
589
900
|
export class SessionManager {
|
|
590
901
|
private sessionId: string = "";
|
|
591
902
|
private sessionTitle: string | undefined;
|
|
@@ -598,24 +909,38 @@ export class SessionManager {
|
|
|
598
909
|
private byId: Map<string, SessionEntry> = new Map();
|
|
599
910
|
private labelsById: Map<string, string> = new Map();
|
|
600
911
|
private leafId: string | null = null;
|
|
601
|
-
|
|
602
|
-
private
|
|
912
|
+
private usageStatistics: UsageStatistics = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0 };
|
|
913
|
+
private persistWriter: NdjsonFileWriter | undefined;
|
|
914
|
+
private persistWriterPath: string | undefined;
|
|
915
|
+
private persistChain: Promise<void> = Promise.resolve();
|
|
916
|
+
private persistError: Error | undefined;
|
|
917
|
+
private persistErrorReported = false;
|
|
918
|
+
|
|
919
|
+
private constructor(cwd: string, sessionDir: string, persist: boolean) {
|
|
603
920
|
this.cwd = cwd;
|
|
604
921
|
this.sessionDir = sessionDir;
|
|
605
922
|
this.persist = persist;
|
|
606
923
|
if (persist && sessionDir && !existsSync(sessionDir)) {
|
|
607
924
|
mkdirSync(sessionDir, { recursive: true });
|
|
608
925
|
}
|
|
926
|
+
// Note: call _initSession() or _initSessionFile() after construction
|
|
927
|
+
}
|
|
609
928
|
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
929
|
+
/** Initialize with a specific session file (used by factory methods) */
|
|
930
|
+
private async _initSessionFile(sessionFile: string): Promise<void> {
|
|
931
|
+
await this.setSessionFile(sessionFile);
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
/** Initialize with a new session (used by factory methods) */
|
|
935
|
+
private _initNewSession(): void {
|
|
936
|
+
this._newSessionSync();
|
|
615
937
|
}
|
|
616
938
|
|
|
617
939
|
/** Switch to a different session file (used for resume and branching) */
|
|
618
|
-
setSessionFile(sessionFile: string): void {
|
|
940
|
+
async setSessionFile(sessionFile: string): Promise<void> {
|
|
941
|
+
await this._closePersistWriter();
|
|
942
|
+
this.persistError = undefined;
|
|
943
|
+
this.persistErrorReported = false;
|
|
619
944
|
this.sessionFile = resolve(sessionFile);
|
|
620
945
|
if (existsSync(this.sessionFile)) {
|
|
621
946
|
this.fileEntries = loadEntriesFromFile(this.sessionFile);
|
|
@@ -624,17 +949,27 @@ export class SessionManager {
|
|
|
624
949
|
this.sessionTitle = header?.title;
|
|
625
950
|
|
|
626
951
|
if (migrateToCurrentVersion(this.fileEntries)) {
|
|
627
|
-
this._rewriteFile();
|
|
952
|
+
await this._rewriteFile();
|
|
628
953
|
}
|
|
629
954
|
|
|
630
955
|
this._buildIndex();
|
|
631
956
|
this.flushed = true;
|
|
632
957
|
} else {
|
|
633
|
-
this.
|
|
958
|
+
this._newSessionSync();
|
|
634
959
|
}
|
|
635
960
|
}
|
|
636
961
|
|
|
637
|
-
|
|
962
|
+
/** Start a new session. Closes any existing writer first. */
|
|
963
|
+
async newSession(options?: NewSessionOptions): Promise<string | undefined> {
|
|
964
|
+
await this._closePersistWriter();
|
|
965
|
+
return this._newSessionSync(options);
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
/** Sync version for initial creation (no existing writer to close) */
|
|
969
|
+
private _newSessionSync(options?: NewSessionOptions): string | undefined {
|
|
970
|
+
this.persistChain = Promise.resolve();
|
|
971
|
+
this.persistError = undefined;
|
|
972
|
+
this.persistErrorReported = false;
|
|
638
973
|
this.sessionId = nanoid();
|
|
639
974
|
const timestamp = new Date().toISOString();
|
|
640
975
|
const header: SessionHeader = {
|
|
@@ -649,6 +984,7 @@ export class SessionManager {
|
|
|
649
984
|
this.byId.clear();
|
|
650
985
|
this.leafId = null;
|
|
651
986
|
this.flushed = false;
|
|
987
|
+
this.usageStatistics = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0 };
|
|
652
988
|
|
|
653
989
|
// Only generate filename if persisting and not already set (e.g., via --session flag)
|
|
654
990
|
if (this.persist && !this.sessionFile) {
|
|
@@ -662,6 +998,7 @@ export class SessionManager {
|
|
|
662
998
|
this.byId.clear();
|
|
663
999
|
this.labelsById.clear();
|
|
664
1000
|
this.leafId = null;
|
|
1001
|
+
this.usageStatistics = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0 };
|
|
665
1002
|
for (const entry of this.fileEntries) {
|
|
666
1003
|
if (entry.type === "session") continue;
|
|
667
1004
|
this.byId.set(entry.id, entry);
|
|
@@ -673,23 +1010,135 @@ export class SessionManager {
|
|
|
673
1010
|
this.labelsById.delete(entry.targetId);
|
|
674
1011
|
}
|
|
675
1012
|
}
|
|
1013
|
+
if (entry.type === "message" && entry.message.role === "assistant") {
|
|
1014
|
+
const usage = entry.message.usage;
|
|
1015
|
+
this.usageStatistics.input += usage.input;
|
|
1016
|
+
this.usageStatistics.output += usage.output;
|
|
1017
|
+
this.usageStatistics.cacheRead += usage.cacheRead;
|
|
1018
|
+
this.usageStatistics.cacheWrite += usage.cacheWrite;
|
|
1019
|
+
this.usageStatistics.cost += usage.cost.total;
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
if (entry.type === "message" && entry.message.role === "toolResult" && entry.message.toolName === "task") {
|
|
1023
|
+
const usage = getTaskToolUsage(entry.message.details);
|
|
1024
|
+
if (usage) {
|
|
1025
|
+
this.usageStatistics.input += usage.input;
|
|
1026
|
+
this.usageStatistics.output += usage.output;
|
|
1027
|
+
this.usageStatistics.cacheRead += usage.cacheRead;
|
|
1028
|
+
this.usageStatistics.cacheWrite += usage.cacheWrite;
|
|
1029
|
+
this.usageStatistics.cost += usage.cost.total;
|
|
1030
|
+
}
|
|
1031
|
+
}
|
|
1032
|
+
}
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
private _recordPersistError(err: unknown): Error {
|
|
1036
|
+
const normalized = toError(err);
|
|
1037
|
+
if (!this.persistError) this.persistError = normalized;
|
|
1038
|
+
if (!this.persistErrorReported) {
|
|
1039
|
+
this.persistErrorReported = true;
|
|
1040
|
+
console.error("Session persistence error:", normalized);
|
|
1041
|
+
}
|
|
1042
|
+
return normalized;
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
private _queuePersistTask(task: () => Promise<void>, options?: { ignoreError?: boolean }): Promise<void> {
|
|
1046
|
+
const next = this.persistChain.then(async () => {
|
|
1047
|
+
if (this.persistError && !options?.ignoreError) throw this.persistError;
|
|
1048
|
+
await task();
|
|
1049
|
+
});
|
|
1050
|
+
this.persistChain = next.catch((err) => {
|
|
1051
|
+
this._recordPersistError(err);
|
|
1052
|
+
});
|
|
1053
|
+
return next;
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
private _ensurePersistWriter(): NdjsonFileWriter | undefined {
|
|
1057
|
+
if (!this.persist || !this.sessionFile) return undefined;
|
|
1058
|
+
if (this.persistError) throw this.persistError;
|
|
1059
|
+
if (this.persistWriter && this.persistWriterPath === this.sessionFile) return this.persistWriter;
|
|
1060
|
+
// Note: caller must await _closePersistWriter() before calling this if switching files
|
|
1061
|
+
this.persistWriter = new NdjsonFileWriter(this.sessionFile, {
|
|
1062
|
+
onError: (err) => {
|
|
1063
|
+
this._recordPersistError(err);
|
|
1064
|
+
},
|
|
1065
|
+
});
|
|
1066
|
+
this.persistWriterPath = this.sessionFile;
|
|
1067
|
+
return this.persistWriter;
|
|
1068
|
+
}
|
|
1069
|
+
|
|
1070
|
+
private async _closePersistWriterInternal(): Promise<void> {
|
|
1071
|
+
if (this.persistWriter) {
|
|
1072
|
+
await this.persistWriter.close();
|
|
1073
|
+
this.persistWriter = undefined;
|
|
1074
|
+
}
|
|
1075
|
+
this.persistWriterPath = undefined;
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
private async _closePersistWriter(): Promise<void> {
|
|
1079
|
+
await this._queuePersistTask(
|
|
1080
|
+
async () => {
|
|
1081
|
+
await this._closePersistWriterInternal();
|
|
1082
|
+
},
|
|
1083
|
+
{ ignoreError: true },
|
|
1084
|
+
);
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1087
|
+
private async _writeEntriesAtomically(entries: FileEntry[]): Promise<void> {
|
|
1088
|
+
if (!this.sessionFile) return;
|
|
1089
|
+
const dir = resolve(this.sessionFile, "..");
|
|
1090
|
+
const tempPath = join(dir, `.${basename(this.sessionFile)}.${nanoid(6)}.tmp`);
|
|
1091
|
+
const writer = new NdjsonFileWriter(tempPath, { flags: "w" });
|
|
1092
|
+
for (const entry of entries) {
|
|
1093
|
+
await writer.write(entry);
|
|
1094
|
+
}
|
|
1095
|
+
await writer.flush();
|
|
1096
|
+
await writer.close();
|
|
1097
|
+
try {
|
|
1098
|
+
renameSync(tempPath, this.sessionFile);
|
|
1099
|
+
fsyncDirSync(dir);
|
|
1100
|
+
} catch (err) {
|
|
1101
|
+
try {
|
|
1102
|
+
unlinkSync(tempPath);
|
|
1103
|
+
} catch {
|
|
1104
|
+
// Ignore cleanup errors
|
|
1105
|
+
}
|
|
1106
|
+
throw toError(err);
|
|
676
1107
|
}
|
|
677
1108
|
}
|
|
678
1109
|
|
|
679
|
-
private _rewriteFile(): void {
|
|
1110
|
+
private async _rewriteFile(): Promise<void> {
|
|
680
1111
|
if (!this.persist || !this.sessionFile) return;
|
|
681
|
-
|
|
682
|
-
|
|
1112
|
+
await this._queuePersistTask(async () => {
|
|
1113
|
+
await this._closePersistWriterInternal();
|
|
1114
|
+
const entries = await Promise.all(this.fileEntries.map((entry) => prepareEntryForPersistence(entry)));
|
|
1115
|
+
await this._writeEntriesAtomically(entries);
|
|
1116
|
+
this.flushed = true;
|
|
1117
|
+
});
|
|
683
1118
|
}
|
|
684
1119
|
|
|
685
1120
|
isPersisted(): boolean {
|
|
686
1121
|
return this.persist;
|
|
687
1122
|
}
|
|
688
1123
|
|
|
1124
|
+
/** Flush pending writes to disk. Call before switching sessions or on shutdown. */
|
|
1125
|
+
async flush(): Promise<void> {
|
|
1126
|
+
if (!this.persistWriter) return;
|
|
1127
|
+
await this._queuePersistTask(async () => {
|
|
1128
|
+
if (this.persistWriter) await this.persistWriter.flush();
|
|
1129
|
+
});
|
|
1130
|
+
if (this.persistError) throw this.persistError;
|
|
1131
|
+
}
|
|
1132
|
+
|
|
689
1133
|
getCwd(): string {
|
|
690
1134
|
return this.cwd;
|
|
691
1135
|
}
|
|
692
1136
|
|
|
1137
|
+
/** Get usage statistics across all assistant messages in the session. */
|
|
1138
|
+
getUsageStatistics(): UsageStatistics {
|
|
1139
|
+
return this.usageStatistics;
|
|
1140
|
+
}
|
|
1141
|
+
|
|
693
1142
|
getSessionDir(): string {
|
|
694
1143
|
return this.sessionDir;
|
|
695
1144
|
}
|
|
@@ -706,7 +1155,7 @@ export class SessionManager {
|
|
|
706
1155
|
return this.sessionTitle;
|
|
707
1156
|
}
|
|
708
1157
|
|
|
709
|
-
setSessionTitle(title: string): void {
|
|
1158
|
+
async setSessionTitle(title: string): Promise<void> {
|
|
710
1159
|
this.sessionTitle = title;
|
|
711
1160
|
|
|
712
1161
|
// Update the in-memory header (so first flush includes title)
|
|
@@ -716,37 +1165,71 @@ export class SessionManager {
|
|
|
716
1165
|
}
|
|
717
1166
|
|
|
718
1167
|
// Update the session file header with the title (if already flushed)
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
const
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
1168
|
+
const sessionFile = this.sessionFile;
|
|
1169
|
+
if (this.persist && sessionFile && existsSync(sessionFile)) {
|
|
1170
|
+
await this._queuePersistTask(async () => {
|
|
1171
|
+
await this._closePersistWriterInternal();
|
|
1172
|
+
try {
|
|
1173
|
+
const content = readFileSync(sessionFile, "utf-8");
|
|
1174
|
+
const lines = content.split("\n");
|
|
1175
|
+
if (lines.length > 0) {
|
|
1176
|
+
const fileHeader = JSON.parse(lines[0]) as SessionHeader;
|
|
1177
|
+
if (fileHeader.type === "session") {
|
|
1178
|
+
fileHeader.title = title;
|
|
1179
|
+
lines[0] = JSON.stringify(fileHeader);
|
|
1180
|
+
const tempPath = join(resolve(sessionFile, ".."), `.${basename(sessionFile)}.${nanoid(6)}.tmp`);
|
|
1181
|
+
writeFileSync(tempPath, lines.join("\n"));
|
|
1182
|
+
const fd = openSync(tempPath, "r");
|
|
1183
|
+
try {
|
|
1184
|
+
fsyncSync(fd);
|
|
1185
|
+
} finally {
|
|
1186
|
+
closeSync(fd);
|
|
1187
|
+
}
|
|
1188
|
+
try {
|
|
1189
|
+
renameSync(tempPath, sessionFile);
|
|
1190
|
+
fsyncDirSync(resolve(sessionFile, ".."));
|
|
1191
|
+
} catch (err) {
|
|
1192
|
+
try {
|
|
1193
|
+
unlinkSync(tempPath);
|
|
1194
|
+
} catch {
|
|
1195
|
+
// Ignore cleanup errors
|
|
1196
|
+
}
|
|
1197
|
+
throw err;
|
|
1198
|
+
}
|
|
1199
|
+
}
|
|
729
1200
|
}
|
|
1201
|
+
} catch (err) {
|
|
1202
|
+
this._recordPersistError(err);
|
|
1203
|
+
throw err;
|
|
730
1204
|
}
|
|
731
|
-
}
|
|
732
|
-
// Ignore errors updating title
|
|
733
|
-
}
|
|
1205
|
+
});
|
|
734
1206
|
}
|
|
735
1207
|
}
|
|
736
1208
|
|
|
737
1209
|
_persist(entry: SessionEntry): void {
|
|
738
1210
|
if (!this.persist || !this.sessionFile) return;
|
|
1211
|
+
if (this.persistError) throw this.persistError;
|
|
739
1212
|
|
|
740
1213
|
const hasAssistant = this.fileEntries.some((e) => e.type === "message" && e.message.role === "assistant");
|
|
741
1214
|
if (!hasAssistant) return;
|
|
742
1215
|
|
|
743
1216
|
if (!this.flushed) {
|
|
744
|
-
for (const e of this.fileEntries) {
|
|
745
|
-
appendFileSync(this.sessionFile, `${JSON.stringify(e)}\n`);
|
|
746
|
-
}
|
|
747
1217
|
this.flushed = true;
|
|
1218
|
+
void this._queuePersistTask(async () => {
|
|
1219
|
+
const writer = this._ensurePersistWriter();
|
|
1220
|
+
if (!writer) return;
|
|
1221
|
+
const entries = await Promise.all(this.fileEntries.map((e) => prepareEntryForPersistence(e)));
|
|
1222
|
+
for (const persistedEntry of entries) {
|
|
1223
|
+
await writer.write(persistedEntry);
|
|
1224
|
+
}
|
|
1225
|
+
});
|
|
748
1226
|
} else {
|
|
749
|
-
|
|
1227
|
+
void this._queuePersistTask(async () => {
|
|
1228
|
+
const writer = this._ensurePersistWriter();
|
|
1229
|
+
if (!writer) return;
|
|
1230
|
+
const persistedEntry = await prepareEntryForPersistence(entry);
|
|
1231
|
+
await writer.write(persistedEntry);
|
|
1232
|
+
});
|
|
750
1233
|
}
|
|
751
1234
|
}
|
|
752
1235
|
|
|
@@ -755,6 +1238,25 @@ export class SessionManager {
|
|
|
755
1238
|
this.byId.set(entry.id, entry);
|
|
756
1239
|
this.leafId = entry.id;
|
|
757
1240
|
this._persist(entry);
|
|
1241
|
+
if (entry.type === "message" && entry.message.role === "assistant") {
|
|
1242
|
+
const usage = entry.message.usage;
|
|
1243
|
+
this.usageStatistics.input += usage.input;
|
|
1244
|
+
this.usageStatistics.output += usage.output;
|
|
1245
|
+
this.usageStatistics.cacheRead += usage.cacheRead;
|
|
1246
|
+
this.usageStatistics.cacheWrite += usage.cacheWrite;
|
|
1247
|
+
this.usageStatistics.cost += usage.cost.total;
|
|
1248
|
+
}
|
|
1249
|
+
|
|
1250
|
+
if (entry.type === "message" && entry.message.role === "toolResult" && entry.message.toolName === "task") {
|
|
1251
|
+
const usage = getTaskToolUsage(entry.message.details);
|
|
1252
|
+
if (usage) {
|
|
1253
|
+
this.usageStatistics.input += usage.input;
|
|
1254
|
+
this.usageStatistics.output += usage.output;
|
|
1255
|
+
this.usageStatistics.cacheRead += usage.cacheRead;
|
|
1256
|
+
this.usageStatistics.cacheWrite += usage.cacheWrite;
|
|
1257
|
+
this.usageStatistics.cost += usage.cost.total;
|
|
1258
|
+
}
|
|
1259
|
+
}
|
|
758
1260
|
}
|
|
759
1261
|
|
|
760
1262
|
/** Append a message as child of current leaf, then advance leaf. Returns entry id.
|
|
@@ -1200,7 +1702,9 @@ export class SessionManager {
|
|
|
1200
1702
|
*/
|
|
1201
1703
|
static create(cwd: string, sessionDir?: string): SessionManager {
|
|
1202
1704
|
const dir = sessionDir ?? getDefaultSessionDir(cwd);
|
|
1203
|
-
|
|
1705
|
+
const manager = new SessionManager(cwd, dir, true);
|
|
1706
|
+
manager._initNewSession();
|
|
1707
|
+
return manager;
|
|
1204
1708
|
}
|
|
1205
1709
|
|
|
1206
1710
|
/**
|
|
@@ -1208,14 +1712,16 @@ export class SessionManager {
|
|
|
1208
1712
|
* @param path Path to session file
|
|
1209
1713
|
* @param sessionDir Optional session directory for /new or /branch. If omitted, derives from file's parent.
|
|
1210
1714
|
*/
|
|
1211
|
-
static open(path: string, sessionDir?: string): SessionManager {
|
|
1715
|
+
static async open(path: string, sessionDir?: string): Promise<SessionManager> {
|
|
1212
1716
|
// Extract cwd from session header if possible, otherwise use process.cwd()
|
|
1213
1717
|
const entries = loadEntriesFromFile(path);
|
|
1214
1718
|
const header = entries.find((e) => e.type === "session") as SessionHeader | undefined;
|
|
1215
1719
|
const cwd = header?.cwd ?? process.cwd();
|
|
1216
1720
|
// If no sessionDir provided, derive from file's parent directory
|
|
1217
1721
|
const dir = sessionDir ?? resolve(path, "..");
|
|
1218
|
-
|
|
1722
|
+
const manager = new SessionManager(cwd, dir, true);
|
|
1723
|
+
await manager._initSessionFile(path);
|
|
1724
|
+
return manager;
|
|
1219
1725
|
}
|
|
1220
1726
|
|
|
1221
1727
|
/**
|
|
@@ -1223,18 +1729,23 @@ export class SessionManager {
|
|
|
1223
1729
|
* @param cwd Working directory
|
|
1224
1730
|
* @param sessionDir Optional session directory. If omitted, uses default (~/.omp/agent/sessions/<encoded-cwd>/).
|
|
1225
1731
|
*/
|
|
1226
|
-
static continueRecent(cwd: string, sessionDir?: string): SessionManager {
|
|
1732
|
+
static async continueRecent(cwd: string, sessionDir?: string): Promise<SessionManager> {
|
|
1227
1733
|
const dir = sessionDir ?? getDefaultSessionDir(cwd);
|
|
1228
1734
|
const mostRecent = findMostRecentSession(dir);
|
|
1735
|
+
const manager = new SessionManager(cwd, dir, true);
|
|
1229
1736
|
if (mostRecent) {
|
|
1230
|
-
|
|
1737
|
+
await manager._initSessionFile(mostRecent);
|
|
1738
|
+
} else {
|
|
1739
|
+
manager._initNewSession();
|
|
1231
1740
|
}
|
|
1232
|
-
return
|
|
1741
|
+
return manager;
|
|
1233
1742
|
}
|
|
1234
1743
|
|
|
1235
1744
|
/** Create an in-memory session (no file persistence) */
|
|
1236
1745
|
static inMemory(cwd: string = process.cwd()): SessionManager {
|
|
1237
|
-
|
|
1746
|
+
const manager = new SessionManager(cwd, "", false);
|
|
1747
|
+
manager._initNewSession();
|
|
1748
|
+
return manager;
|
|
1238
1749
|
}
|
|
1239
1750
|
|
|
1240
1751
|
/**
|