@cryer/star-cli 0.3.12 → 0.3.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +17 -5
- package/README.zh-CN.md +15 -5
- package/dist/{chunk-LSYGHLRU.js → chunk-2ISBRSBA.js} +479 -296
- package/dist/chunk-2ISBRSBA.js.map +1 -0
- package/dist/{chunk-F5RWZMQY.js → chunk-4HANB3DJ.js} +458 -221
- package/dist/chunk-4HANB3DJ.js.map +1 -0
- package/dist/chunk-4IRY44TG.js +0 -0
- package/dist/{chunk-Q5FJN4LN.js → chunk-54KH4JSO.js} +213 -125
- package/dist/chunk-54KH4JSO.js.map +1 -0
- package/dist/{chunk-MYHD3SZB.js → chunk-CECMCDZG.js} +43 -30
- package/dist/chunk-CECMCDZG.js.map +1 -0
- package/dist/{chunk-MVAMPVTA.js → chunk-KILMI3S5.js} +13 -5
- package/dist/{chunk-MVAMPVTA.js.map → chunk-KILMI3S5.js.map} +1 -1
- package/dist/{chunk-2GAUAYCU.js → chunk-NRLGGA7L.js} +507 -175
- package/dist/chunk-NRLGGA7L.js.map +1 -0
- package/dist/loop-M64D5YII.js +17 -0
- package/dist/main.js +177 -131
- package/dist/main.js.map +1 -1
- package/dist/{provider-EWMUPBLH.js → provider-3Z4B7RMF.js} +2 -2
- package/dist/{repl-SSCURTAH.js → repl-N2H7JDSH.js} +617 -327
- package/dist/repl-N2H7JDSH.js.map +1 -0
- package/dist/{tools-BH55BVIS.js → tools-5RKM7ZI3.js} +3 -3
- package/package.json +16 -25
- package/dist/chunk-2GAUAYCU.js.map +0 -1
- package/dist/chunk-F5RWZMQY.js.map +0 -1
- package/dist/chunk-LSYGHLRU.js.map +0 -1
- package/dist/chunk-MYHD3SZB.js.map +0 -1
- package/dist/chunk-Q5FJN4LN.js.map +0 -1
- package/dist/loop-BYXBNFFY.js +0 -17
- package/dist/repl-SSCURTAH.js.map +0 -1
- /package/dist/{loop-BYXBNFFY.js.map → loop-M64D5YII.js.map} +0 -0
- /package/dist/{provider-EWMUPBLH.js.map → provider-3Z4B7RMF.js.map} +0 -0
- /package/dist/{tools-BH55BVIS.js.map → tools-5RKM7ZI3.js.map} +0 -0
|
@@ -1,16 +1,17 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
|
+
computeCostUsd,
|
|
3
4
|
coreMessageText,
|
|
4
5
|
pruneTreeReposExcept,
|
|
5
6
|
removeTreeRepo
|
|
6
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-CECMCDZG.js";
|
|
7
8
|
import {
|
|
8
9
|
SKIP_DIRS,
|
|
9
10
|
createIgnorePredicate,
|
|
10
11
|
downsampleImageIfNeeded,
|
|
11
12
|
isSensitivePath,
|
|
12
13
|
sessionsDir
|
|
13
|
-
} from "./chunk-
|
|
14
|
+
} from "./chunk-2ISBRSBA.js";
|
|
14
15
|
|
|
15
16
|
// src/cli/cost.ts
|
|
16
17
|
function formatDollars(cost) {
|
|
@@ -35,14 +36,6 @@ function formatTokensCompact(n) {
|
|
|
35
36
|
const scaled = thousands ? n / 1e3 : n / 1e6;
|
|
36
37
|
return `${scaled.toFixed(1).replace(/\.0$/, "")}${thousands ? "K" : "M"}`;
|
|
37
38
|
}
|
|
38
|
-
function computeCostUsd(usage, model) {
|
|
39
|
-
if (model?.promptPrice === void 0 || model.completionPrice === void 0) {
|
|
40
|
-
return null;
|
|
41
|
-
}
|
|
42
|
-
const cached = Math.min(Math.max(usage.cachedPromptTokens ?? 0, 0), usage.promptTokens);
|
|
43
|
-
const promptUsd = (usage.promptTokens - cached) * model.promptPrice + cached * (model.cacheReadPrice ?? model.promptPrice) + (usage.cacheReadInputTokens ?? 0) * (model.cacheReadPrice ?? 0);
|
|
44
|
-
return (promptUsd + usage.completionTokens * model.completionPrice) / 1e6;
|
|
45
|
-
}
|
|
46
39
|
function estimateCost(usage, modelName, model) {
|
|
47
40
|
const cost = computeCostUsd(usage, model);
|
|
48
41
|
if (cost === null) {
|
|
@@ -133,6 +126,58 @@ function toTerminalSafe(text) {
|
|
|
133
126
|
if (!UNSAFE_CHARS.test(text)) return text;
|
|
134
127
|
return text.replace(TABS, " ").replace(CONTROL_CHARS, "");
|
|
135
128
|
}
|
|
129
|
+
var WIDE_RANGES = [
|
|
130
|
+
[4352, 4447],
|
|
131
|
+
// Hangul Jamo
|
|
132
|
+
[11904, 42191],
|
|
133
|
+
// CJK radicals … Yi syllables
|
|
134
|
+
[43360, 43388],
|
|
135
|
+
// Hangul Jamo Extended-B
|
|
136
|
+
[44032, 55203],
|
|
137
|
+
// Hangul syllables
|
|
138
|
+
[63744, 64255],
|
|
139
|
+
// CJK compatibility ideographs
|
|
140
|
+
[65040, 65049],
|
|
141
|
+
// vertical forms
|
|
142
|
+
[65072, 65106],
|
|
143
|
+
// CJK compatibility forms
|
|
144
|
+
[65108, 65126],
|
|
145
|
+
// small form variants
|
|
146
|
+
[65128, 65131],
|
|
147
|
+
[65280, 65376],
|
|
148
|
+
// fullwidth forms
|
|
149
|
+
[65504, 65510],
|
|
150
|
+
// fullwidth signs
|
|
151
|
+
[127744, 128591],
|
|
152
|
+
// emoji
|
|
153
|
+
[129280, 129535],
|
|
154
|
+
// supplemental emoji
|
|
155
|
+
[131072, 262141]
|
|
156
|
+
// CJK extension planes
|
|
157
|
+
];
|
|
158
|
+
function isWideChar(ch) {
|
|
159
|
+
const cp = ch.codePointAt(0) ?? 0;
|
|
160
|
+
return WIDE_RANGES.some(([lo, hi]) => cp >= lo && cp <= hi);
|
|
161
|
+
}
|
|
162
|
+
function displayWidth(text) {
|
|
163
|
+
let width = 0;
|
|
164
|
+
for (const ch of text) width += isWideChar(ch) ? 2 : 1;
|
|
165
|
+
return width;
|
|
166
|
+
}
|
|
167
|
+
function truncateToWidth(text, maxWidth) {
|
|
168
|
+
if (maxWidth <= 0) return "";
|
|
169
|
+
if (displayWidth(text) <= maxWidth) return text;
|
|
170
|
+
const limit = Math.max(0, maxWidth - 1);
|
|
171
|
+
let width = 0;
|
|
172
|
+
let out = "";
|
|
173
|
+
for (const ch of text) {
|
|
174
|
+
const w = isWideChar(ch) ? 2 : 1;
|
|
175
|
+
if (width + w > limit) break;
|
|
176
|
+
out += ch;
|
|
177
|
+
width += w;
|
|
178
|
+
}
|
|
179
|
+
return `${out}\u2026`;
|
|
180
|
+
}
|
|
136
181
|
|
|
137
182
|
// src/cli/markdown.ts
|
|
138
183
|
var ESC = "\x1B[";
|
|
@@ -640,15 +685,153 @@ ${buf.toString("utf8")}
|
|
|
640
685
|
}
|
|
641
686
|
|
|
642
687
|
// src/session/checkpoints.ts
|
|
688
|
+
import { randomUUID as randomUUID3 } from "crypto";
|
|
689
|
+
import fs3 from "fs/promises";
|
|
690
|
+
import path4 from "path";
|
|
691
|
+
|
|
692
|
+
// src/session/store.ts
|
|
693
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
694
|
+
import { existsSync } from "fs";
|
|
643
695
|
import fs2 from "fs/promises";
|
|
644
696
|
import path3 from "path";
|
|
697
|
+
import { StringDecoder } from "string_decoder";
|
|
645
698
|
|
|
646
|
-
// src/session/
|
|
699
|
+
// src/session/lock.ts
|
|
647
700
|
import { randomUUID } from "crypto";
|
|
648
|
-
import { existsSync } from "fs";
|
|
649
701
|
import fs from "fs/promises";
|
|
650
702
|
import path2 from "path";
|
|
651
|
-
|
|
703
|
+
var LOCK_FILENAME = ".lock";
|
|
704
|
+
var RECLAIM_FILENAME = ".lock.reclaim";
|
|
705
|
+
var MALFORMED_LOCK_GRACE_MS = 5e3;
|
|
706
|
+
var RECLAIM_STALE_MS = 3e4;
|
|
707
|
+
var SessionLockedError = class extends Error {
|
|
708
|
+
constructor(sessionId) {
|
|
709
|
+
super(`Session is already open in another process: ${sessionId}`);
|
|
710
|
+
this.sessionId = sessionId;
|
|
711
|
+
this.name = "SessionLockedError";
|
|
712
|
+
}
|
|
713
|
+
sessionId;
|
|
714
|
+
code = "SESSION_LOCKED";
|
|
715
|
+
};
|
|
716
|
+
function lockPath(sessionDir) {
|
|
717
|
+
return path2.join(sessionDir, LOCK_FILENAME);
|
|
718
|
+
}
|
|
719
|
+
function processIsAlive(pid) {
|
|
720
|
+
if (!Number.isInteger(pid) || pid <= 0) return false;
|
|
721
|
+
try {
|
|
722
|
+
process.kill(pid, 0);
|
|
723
|
+
return true;
|
|
724
|
+
} catch (error) {
|
|
725
|
+
return error.code === "EPERM";
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
async function readLockRecord(file) {
|
|
729
|
+
try {
|
|
730
|
+
const parsed = JSON.parse(await fs.readFile(file, "utf8"));
|
|
731
|
+
if (typeof parsed.pid !== "number" || typeof parsed.token !== "string" || typeof parsed.createdAt !== "number") {
|
|
732
|
+
return null;
|
|
733
|
+
}
|
|
734
|
+
return { pid: parsed.pid, token: parsed.token, createdAt: parsed.createdAt };
|
|
735
|
+
} catch {
|
|
736
|
+
return null;
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
async function malformedLockIsStale(file) {
|
|
740
|
+
try {
|
|
741
|
+
const stat2 = await fs.stat(file);
|
|
742
|
+
return Date.now() - stat2.mtimeMs >= MALFORMED_LOCK_GRACE_MS;
|
|
743
|
+
} catch {
|
|
744
|
+
return false;
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
async function reclaimStaleLock(sessionDir, expected) {
|
|
748
|
+
const guard = path2.join(sessionDir, RECLAIM_FILENAME);
|
|
749
|
+
let guardHandle;
|
|
750
|
+
try {
|
|
751
|
+
guardHandle = await fs.open(guard, "wx", 384);
|
|
752
|
+
} catch (error) {
|
|
753
|
+
if (error.code !== "EEXIST") throw error;
|
|
754
|
+
try {
|
|
755
|
+
const stat2 = await fs.stat(guard);
|
|
756
|
+
if (Date.now() - stat2.mtimeMs < RECLAIM_STALE_MS) return false;
|
|
757
|
+
await fs.unlink(guard);
|
|
758
|
+
} catch {
|
|
759
|
+
return false;
|
|
760
|
+
}
|
|
761
|
+
return false;
|
|
762
|
+
}
|
|
763
|
+
const quarantine = `${lockPath(sessionDir)}.stale-${process.pid}-${randomUUID()}`;
|
|
764
|
+
try {
|
|
765
|
+
const file = lockPath(sessionDir);
|
|
766
|
+
const current = await readLockRecord(file);
|
|
767
|
+
if (expected) {
|
|
768
|
+
if (current?.token !== expected.token || processIsAlive(current.pid)) return false;
|
|
769
|
+
} else {
|
|
770
|
+
if (current) {
|
|
771
|
+
if (processIsAlive(current.pid)) return false;
|
|
772
|
+
} else if (!await malformedLockIsStale(file)) {
|
|
773
|
+
return false;
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
await fs.rename(file, quarantine);
|
|
777
|
+
await fs.unlink(quarantine).catch(() => {
|
|
778
|
+
});
|
|
779
|
+
return true;
|
|
780
|
+
} catch (error) {
|
|
781
|
+
if (error.code === "ENOENT") return true;
|
|
782
|
+
throw error;
|
|
783
|
+
} finally {
|
|
784
|
+
await guardHandle.close().catch(() => {
|
|
785
|
+
});
|
|
786
|
+
await fs.unlink(guard).catch(() => {
|
|
787
|
+
});
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
async function acquireSessionLock(sessionDir, sessionId) {
|
|
791
|
+
await fs.mkdir(sessionDir, { recursive: true, mode: 448 });
|
|
792
|
+
const file = lockPath(sessionDir);
|
|
793
|
+
const record = { pid: process.pid, token: randomUUID(), createdAt: Date.now() };
|
|
794
|
+
for (let attempt = 0; attempt < 20; attempt++) {
|
|
795
|
+
let handle;
|
|
796
|
+
try {
|
|
797
|
+
handle = await fs.open(file, "wx", 384);
|
|
798
|
+
} catch (error) {
|
|
799
|
+
if (error.code !== "EEXIST") throw error;
|
|
800
|
+
const existing = await readLockRecord(file);
|
|
801
|
+
if (existing ? processIsAlive(existing.pid) : !await malformedLockIsStale(file)) {
|
|
802
|
+
throw new SessionLockedError(sessionId);
|
|
803
|
+
}
|
|
804
|
+
if (!await reclaimStaleLock(sessionDir, existing)) {
|
|
805
|
+
const current = await readLockRecord(file);
|
|
806
|
+
if (current && processIsAlive(current.pid)) throw new SessionLockedError(sessionId);
|
|
807
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
808
|
+
}
|
|
809
|
+
continue;
|
|
810
|
+
}
|
|
811
|
+
try {
|
|
812
|
+
await handle.writeFile(JSON.stringify(record), "utf8");
|
|
813
|
+
await handle.close();
|
|
814
|
+
return {
|
|
815
|
+
release: async () => {
|
|
816
|
+
if ((await readLockRecord(file))?.token === record.token) {
|
|
817
|
+
await fs.unlink(file).catch((error) => {
|
|
818
|
+
if (error.code !== "ENOENT") throw error;
|
|
819
|
+
});
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
};
|
|
823
|
+
} catch (error) {
|
|
824
|
+
await handle.close().catch(() => {
|
|
825
|
+
});
|
|
826
|
+
await fs.unlink(file).catch(() => {
|
|
827
|
+
});
|
|
828
|
+
throw error;
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
throw new SessionLockedError(sessionId);
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
// src/session/store.ts
|
|
652
835
|
function generateId(now) {
|
|
653
836
|
const pad = (n) => String(n).padStart(2, "0");
|
|
654
837
|
const stamp = `${now.getFullYear()}${pad(now.getMonth() + 1)}${pad(now.getDate())}${pad(now.getHours())}${pad(now.getMinutes())}${pad(now.getSeconds())}`;
|
|
@@ -658,10 +841,10 @@ function generateId(now) {
|
|
|
658
841
|
var ID_COLLISION_MAX_ATTEMPTS = 5;
|
|
659
842
|
function generateUniqueId() {
|
|
660
843
|
let id = generateId(/* @__PURE__ */ new Date());
|
|
661
|
-
for (let attempt = 1; existsSync(
|
|
844
|
+
for (let attempt = 1; existsSync(path3.join(sessionsDir(), id)); attempt++) {
|
|
662
845
|
if (attempt >= ID_COLLISION_MAX_ATTEMPTS) {
|
|
663
846
|
const stamp = id.slice(0, id.lastIndexOf("-"));
|
|
664
|
-
return `${stamp}-${
|
|
847
|
+
return `${stamp}-${randomUUID2().replaceAll("-", "").slice(0, 12)}`;
|
|
665
848
|
}
|
|
666
849
|
id = generateId(/* @__PURE__ */ new Date());
|
|
667
850
|
}
|
|
@@ -669,7 +852,7 @@ function generateUniqueId() {
|
|
|
669
852
|
}
|
|
670
853
|
function normalizeCwdForCompare(cwd, platform = process.platform) {
|
|
671
854
|
if (platform !== "win32") return cwd;
|
|
672
|
-
const resolved =
|
|
855
|
+
const resolved = path3.win32.resolve(cwd);
|
|
673
856
|
const trimmed = resolved.replace(/[\\/]+$/, "");
|
|
674
857
|
return (trimmed === "" ? resolved : trimmed).toLowerCase();
|
|
675
858
|
}
|
|
@@ -738,33 +921,48 @@ var SessionStore = class _SessionStore {
|
|
|
738
921
|
dir;
|
|
739
922
|
pendingMeta = null;
|
|
740
923
|
initialized = false;
|
|
924
|
+
initialization = null;
|
|
741
925
|
cachedMeta = null;
|
|
742
|
-
|
|
743
|
-
|
|
926
|
+
lock = null;
|
|
927
|
+
closing = false;
|
|
928
|
+
closed = false;
|
|
929
|
+
messageHistory = null;
|
|
930
|
+
degraded = false;
|
|
931
|
+
messageWriteQueue = Promise.resolve();
|
|
744
932
|
metaWriteQueue = Promise.resolve();
|
|
745
933
|
metaLastFlushAt = 0;
|
|
746
934
|
metaFlushTimer = null;
|
|
747
935
|
metaDirty = false;
|
|
748
936
|
constructor(id) {
|
|
749
937
|
this.id = id;
|
|
750
|
-
this.dir =
|
|
938
|
+
this.dir = path3.join(sessionsDir(), id);
|
|
751
939
|
}
|
|
752
940
|
metaPath() {
|
|
753
|
-
return
|
|
941
|
+
return path3.join(this.dir, "meta.json");
|
|
754
942
|
}
|
|
755
943
|
messagesPath() {
|
|
756
|
-
return
|
|
944
|
+
return path3.join(this.dir, "messages.jsonl");
|
|
757
945
|
}
|
|
758
946
|
static async create(cwd, model) {
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
947
|
+
for (let attempt = 0; attempt < ID_COLLISION_MAX_ATTEMPTS; attempt++) {
|
|
948
|
+
const store = new _SessionStore(generateUniqueId());
|
|
949
|
+
try {
|
|
950
|
+
store.lock = await acquireSessionLock(store.dir, store.id);
|
|
951
|
+
} catch (error) {
|
|
952
|
+
if (error instanceof SessionLockedError) continue;
|
|
953
|
+
throw error;
|
|
954
|
+
}
|
|
955
|
+
store.pendingMeta = { cwd, model, createdAt: Date.now() };
|
|
956
|
+
store.messageHistory = [];
|
|
957
|
+
return store;
|
|
958
|
+
}
|
|
959
|
+
throw new Error("Could not allocate an exclusive session id");
|
|
762
960
|
}
|
|
763
961
|
static async open(id) {
|
|
764
962
|
const store = new _SessionStore(id);
|
|
765
963
|
let raw;
|
|
766
964
|
try {
|
|
767
|
-
raw = await
|
|
965
|
+
raw = await fs2.readFile(store.metaPath(), "utf8");
|
|
768
966
|
} catch {
|
|
769
967
|
return null;
|
|
770
968
|
}
|
|
@@ -774,32 +972,44 @@ var SessionStore = class _SessionStore {
|
|
|
774
972
|
} catch {
|
|
775
973
|
debugWarn(`session ${id}: corrupt meta.json, using defaults`);
|
|
776
974
|
}
|
|
777
|
-
await store.
|
|
778
|
-
|
|
779
|
-
|
|
975
|
+
store.lock = await acquireSessionLock(store.dir, id);
|
|
976
|
+
try {
|
|
977
|
+
await store.sweepTmpFiles();
|
|
978
|
+
store.initialized = true;
|
|
979
|
+
return store;
|
|
980
|
+
} catch (error) {
|
|
981
|
+
await store.lock.release();
|
|
982
|
+
throw error;
|
|
983
|
+
}
|
|
780
984
|
}
|
|
781
985
|
static async list(cwd) {
|
|
782
986
|
let entries;
|
|
783
987
|
try {
|
|
784
|
-
entries = await
|
|
988
|
+
entries = await fs2.readdir(sessionsDir());
|
|
785
989
|
} catch {
|
|
786
990
|
return [];
|
|
787
991
|
}
|
|
788
992
|
const wanted = cwd === void 0 ? void 0 : normalizeCwdForCompare(cwd);
|
|
789
|
-
const metas =
|
|
790
|
-
|
|
993
|
+
const metas = [];
|
|
994
|
+
let next = 0;
|
|
995
|
+
const worker = async () => {
|
|
996
|
+
while (next < entries.length) {
|
|
997
|
+
const index = next;
|
|
998
|
+
next += 1;
|
|
999
|
+
const entry = entries[index];
|
|
1000
|
+
if (!entry) continue;
|
|
791
1001
|
try {
|
|
792
|
-
const raw = await
|
|
1002
|
+
const raw = await fs2.readFile(path3.join(sessionsDir(), entry, "meta.json"), "utf8");
|
|
793
1003
|
const meta = JSON.parse(raw);
|
|
794
1004
|
if (meta.id === entry && (wanted === void 0 || normalizeCwdForCompare(meta.cwd) === wanted)) {
|
|
795
|
-
|
|
1005
|
+
metas[index] = meta;
|
|
796
1006
|
}
|
|
797
1007
|
} catch {
|
|
798
1008
|
}
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
);
|
|
802
|
-
return metas.filter((meta) => meta !== null).sort((a, b) => b.updatedAt - a.updatedAt);
|
|
1009
|
+
}
|
|
1010
|
+
};
|
|
1011
|
+
await Promise.all(Array.from({ length: Math.min(8, entries.length) }, () => worker()));
|
|
1012
|
+
return metas.filter((meta) => meta !== null && meta !== void 0).sort((a, b) => b.updatedAt - a.updatedAt);
|
|
803
1013
|
}
|
|
804
1014
|
// Meta writes are serialized per instance and land atomically (tmp file +
|
|
805
1015
|
// rename) over a cached in-memory meta: the fire-and-forget title/usage
|
|
@@ -812,15 +1022,15 @@ var SessionStore = class _SessionStore {
|
|
|
812
1022
|
// agent turn over a stale updatedAt would be the wrong trade.
|
|
813
1023
|
writeMeta(meta) {
|
|
814
1024
|
if (isFallbackMeta(meta)) return Promise.resolve();
|
|
815
|
-
const tmp = `${this.metaPath()}.tmp-${process.pid}-${
|
|
1025
|
+
const tmp = `${this.metaPath()}.tmp-${process.pid}-${randomUUID2()}`;
|
|
816
1026
|
const run = this.metaWriteQueue.then(async () => {
|
|
817
1027
|
try {
|
|
818
1028
|
await withTransientFsRetry(async () => {
|
|
819
|
-
await
|
|
820
|
-
await
|
|
1029
|
+
await fs2.writeFile(tmp, JSON.stringify(meta, null, 2));
|
|
1030
|
+
await fs2.rename(tmp, this.metaPath());
|
|
821
1031
|
});
|
|
822
1032
|
} catch (error) {
|
|
823
|
-
await
|
|
1033
|
+
await fs2.unlink(tmp).catch(() => {
|
|
824
1034
|
});
|
|
825
1035
|
debugWarn(
|
|
826
1036
|
`session ${this.id}: meta write failed (${error.code ?? String(error)}), skipping`
|
|
@@ -831,10 +1041,6 @@ var SessionStore = class _SessionStore {
|
|
|
831
1041
|
});
|
|
832
1042
|
return run;
|
|
833
1043
|
}
|
|
834
|
-
// Debounced flush for append-driven updatedAt bumps. The trailing timer is
|
|
835
|
-
// unref'd so a pending flush never keeps the process alive; close() flushes
|
|
836
|
-
// synchronously on a clean exit, and a hard exit simply drops the trailing
|
|
837
|
-
// bump — meta is bookkeeping.
|
|
838
1044
|
markMetaDirty() {
|
|
839
1045
|
this.metaDirty = true;
|
|
840
1046
|
if (this.metaFlushTimer) return;
|
|
@@ -853,9 +1059,6 @@ var SessionStore = class _SessionStore {
|
|
|
853
1059
|
if (!this.metaDirty) return;
|
|
854
1060
|
await this.flushMetaNow();
|
|
855
1061
|
}
|
|
856
|
-
// Immediate flush for substantive meta changes; cancels a pending debounced
|
|
857
|
-
// flush, which the write makes redundant either way (it serializes the live
|
|
858
|
-
// cached meta).
|
|
859
1062
|
async flushMetaNow() {
|
|
860
1063
|
if (this.metaFlushTimer) {
|
|
861
1064
|
clearTimeout(this.metaFlushTimer);
|
|
@@ -865,96 +1068,172 @@ var SessionStore = class _SessionStore {
|
|
|
865
1068
|
this.metaLastFlushAt = Date.now();
|
|
866
1069
|
if (this.cachedMeta) await this.writeMeta(this.cachedMeta);
|
|
867
1070
|
}
|
|
868
|
-
// Flushes a pending debounced meta write and waits for in-flight writes to
|
|
869
|
-
// settle; call when the store is dropped so a clean exit keeps the trailing
|
|
870
|
-
// updatedAt bump.
|
|
871
1071
|
async close() {
|
|
872
|
-
|
|
873
|
-
|
|
1072
|
+
if (this.closed || this.closing) return;
|
|
1073
|
+
this.closing = true;
|
|
1074
|
+
try {
|
|
1075
|
+
await this.messageWriteQueue;
|
|
1076
|
+
await this.flushMetaIfDirty();
|
|
1077
|
+
await this.metaWriteQueue;
|
|
1078
|
+
} finally {
|
|
1079
|
+
if (this.metaFlushTimer) clearTimeout(this.metaFlushTimer);
|
|
1080
|
+
this.metaFlushTimer = null;
|
|
1081
|
+
this.closed = true;
|
|
1082
|
+
this.closing = false;
|
|
1083
|
+
const lock = this.lock;
|
|
1084
|
+
this.lock = null;
|
|
1085
|
+
await lock?.release();
|
|
1086
|
+
}
|
|
1087
|
+
}
|
|
1088
|
+
assertCanWrite() {
|
|
1089
|
+
if (this.closed || this.closing || !this.lock) {
|
|
1090
|
+
throw new Error(`Session store is closed: ${this.id}`);
|
|
1091
|
+
}
|
|
874
1092
|
}
|
|
875
1093
|
async ensureInitialized() {
|
|
876
1094
|
if (this.initialized) return;
|
|
877
|
-
|
|
878
|
-
|
|
1095
|
+
if (this.initialization) return this.initialization;
|
|
1096
|
+
this.initialization = (async () => {
|
|
1097
|
+
const pending = this.pendingMeta;
|
|
1098
|
+
if (!pending) {
|
|
1099
|
+
this.initialized = true;
|
|
1100
|
+
return;
|
|
1101
|
+
}
|
|
1102
|
+
await this.sweepTmpFiles();
|
|
1103
|
+
const meta = {
|
|
1104
|
+
id: this.id,
|
|
1105
|
+
title: "",
|
|
1106
|
+
model: pending.model,
|
|
1107
|
+
cwd: pending.cwd,
|
|
1108
|
+
createdAt: pending.createdAt,
|
|
1109
|
+
updatedAt: pending.createdAt
|
|
1110
|
+
};
|
|
1111
|
+
await this.writeMeta(meta);
|
|
1112
|
+
this.cachedMeta = meta;
|
|
1113
|
+
this.metaLastFlushAt = Date.now();
|
|
879
1114
|
this.initialized = true;
|
|
880
|
-
|
|
1115
|
+
})();
|
|
1116
|
+
try {
|
|
1117
|
+
await this.initialization;
|
|
1118
|
+
} catch (error) {
|
|
1119
|
+
this.initialization = null;
|
|
1120
|
+
throw error;
|
|
881
1121
|
}
|
|
882
|
-
await fs.mkdir(this.dir, { recursive: true, mode: 448 });
|
|
883
|
-
await this.sweepTmpFiles();
|
|
884
|
-
const meta = {
|
|
885
|
-
id: this.id,
|
|
886
|
-
title: "",
|
|
887
|
-
model: pending.model,
|
|
888
|
-
cwd: pending.cwd,
|
|
889
|
-
createdAt: pending.createdAt,
|
|
890
|
-
updatedAt: pending.createdAt
|
|
891
|
-
};
|
|
892
|
-
await this.writeMeta(meta);
|
|
893
|
-
this.cachedMeta = meta;
|
|
894
|
-
this.metaLastFlushAt = Date.now();
|
|
895
|
-
this.initialized = true;
|
|
896
1122
|
}
|
|
897
|
-
// Removes tmp orphans left behind by crashed atomic writes (both meta.json
|
|
898
|
-
// and messages.jsonl write tmp file + rename); best-effort, the directory
|
|
899
|
-
// may not even exist yet.
|
|
900
1123
|
async sweepTmpFiles() {
|
|
901
1124
|
let entries;
|
|
902
1125
|
try {
|
|
903
|
-
entries = await
|
|
1126
|
+
entries = await fs2.readdir(this.dir);
|
|
904
1127
|
} catch {
|
|
905
1128
|
return;
|
|
906
1129
|
}
|
|
907
1130
|
await Promise.all(
|
|
908
|
-
entries.
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
1131
|
+
entries.map(async (entry) => {
|
|
1132
|
+
const match = /^(?:meta\.json|messages\.jsonl)\.tmp-(\d+)-/.exec(entry);
|
|
1133
|
+
if (!match) return;
|
|
1134
|
+
const pid = Number(match[1]);
|
|
1135
|
+
try {
|
|
1136
|
+
process.kill(pid, 0);
|
|
1137
|
+
return;
|
|
1138
|
+
} catch (error) {
|
|
1139
|
+
if (error.code === "EPERM") return;
|
|
1140
|
+
}
|
|
1141
|
+
await fs2.unlink(path3.join(this.dir, entry)).catch(() => {
|
|
1142
|
+
});
|
|
1143
|
+
})
|
|
912
1144
|
);
|
|
913
1145
|
}
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
`session ${this.id}: message append failed (${error.code ?? String(error)}), message kept in memory only`
|
|
924
|
-
);
|
|
925
|
-
}
|
|
926
|
-
const sessionMeta = await this.meta();
|
|
927
|
-
sessionMeta.updatedAt = Date.now();
|
|
928
|
-
this.markMetaDirty();
|
|
1146
|
+
enqueueMessageWrite(operation) {
|
|
1147
|
+
const run = this.messageWriteQueue.then(operation);
|
|
1148
|
+
this.messageWriteQueue = run.then(
|
|
1149
|
+
() => {
|
|
1150
|
+
},
|
|
1151
|
+
() => {
|
|
1152
|
+
}
|
|
1153
|
+
);
|
|
1154
|
+
return run;
|
|
929
1155
|
}
|
|
930
|
-
|
|
1156
|
+
get persistenceDegraded() {
|
|
1157
|
+
return this.degraded;
|
|
1158
|
+
}
|
|
1159
|
+
async ensureMessageHistory() {
|
|
1160
|
+
if (!this.messageHistory) this.messageHistory = await this.readStarMessages(true);
|
|
1161
|
+
return this.messageHistory;
|
|
1162
|
+
}
|
|
1163
|
+
async rewriteMessages(messages) {
|
|
931
1164
|
await this.ensureInitialized();
|
|
1165
|
+
this.messageHistory = [...messages];
|
|
932
1166
|
const content = messages.map(serializeMessage).join("\n");
|
|
933
|
-
const tmp = `${this.messagesPath()}.tmp-${process.pid}-${
|
|
1167
|
+
const tmp = `${this.messagesPath()}.tmp-${process.pid}-${randomUUID2()}`;
|
|
934
1168
|
try {
|
|
935
1169
|
await withTransientFsRetry(async () => {
|
|
936
|
-
await
|
|
1170
|
+
await fs2.writeFile(tmp, content ? `${content}
|
|
937
1171
|
` : "");
|
|
938
|
-
await
|
|
1172
|
+
await fs2.rename(tmp, this.messagesPath());
|
|
939
1173
|
});
|
|
940
1174
|
} catch (error) {
|
|
941
|
-
await
|
|
1175
|
+
await fs2.unlink(tmp).catch(() => {
|
|
942
1176
|
});
|
|
1177
|
+
this.degraded = true;
|
|
943
1178
|
warn(
|
|
944
1179
|
`session ${this.id}: history rewrite failed (${error.code ?? String(error)}), kept the previous messages.jsonl`
|
|
945
1180
|
);
|
|
1181
|
+
return false;
|
|
946
1182
|
}
|
|
1183
|
+
this.degraded = false;
|
|
947
1184
|
const meta = await this.meta();
|
|
948
1185
|
meta.updatedAt = Date.now();
|
|
949
1186
|
await this.flushMetaNow();
|
|
1187
|
+
return true;
|
|
1188
|
+
}
|
|
1189
|
+
async append(message, meta) {
|
|
1190
|
+
this.assertCanWrite();
|
|
1191
|
+
return this.enqueueMessageWrite(async () => {
|
|
1192
|
+
await this.ensureInitialized();
|
|
1193
|
+
const history = await this.ensureMessageHistory();
|
|
1194
|
+
const star = { message, meta };
|
|
1195
|
+
if (this.degraded) return this.rewriteMessages([...history, star]);
|
|
1196
|
+
try {
|
|
1197
|
+
await withTransientFsRetry(
|
|
1198
|
+
() => fs2.appendFile(this.messagesPath(), `${serializeMessage(star)}
|
|
1199
|
+
`)
|
|
1200
|
+
);
|
|
1201
|
+
} catch (error) {
|
|
1202
|
+
this.messageHistory = [...history, star];
|
|
1203
|
+
this.degraded = true;
|
|
1204
|
+
warn(
|
|
1205
|
+
`session ${this.id}: message append failed (${error.code ?? String(error)}), full history rewrite required`
|
|
1206
|
+
);
|
|
1207
|
+
return false;
|
|
1208
|
+
}
|
|
1209
|
+
history.push(star);
|
|
1210
|
+
const sessionMeta = await this.meta();
|
|
1211
|
+
sessionMeta.updatedAt = Date.now();
|
|
1212
|
+
this.markMetaDirty();
|
|
1213
|
+
return true;
|
|
1214
|
+
});
|
|
1215
|
+
}
|
|
1216
|
+
async replaceMessages(messages) {
|
|
1217
|
+
this.assertCanWrite();
|
|
1218
|
+
return this.enqueueMessageWrite(() => this.rewriteMessages(messages));
|
|
1219
|
+
}
|
|
1220
|
+
async recoverMessages() {
|
|
1221
|
+
this.assertCanWrite();
|
|
1222
|
+
return this.enqueueMessageWrite(async () => {
|
|
1223
|
+
if (!this.degraded) return true;
|
|
1224
|
+
return this.rewriteMessages(await this.ensureMessageHistory());
|
|
1225
|
+
});
|
|
950
1226
|
}
|
|
951
1227
|
async messages() {
|
|
952
1228
|
return (await this.starMessages()).map((star) => star.message);
|
|
953
1229
|
}
|
|
954
1230
|
async starMessages() {
|
|
1231
|
+
return this.readStarMessages(false);
|
|
1232
|
+
}
|
|
1233
|
+
async readStarMessages(strict) {
|
|
955
1234
|
let handle = null;
|
|
956
1235
|
try {
|
|
957
|
-
handle = await
|
|
1236
|
+
handle = await fs2.open(this.messagesPath(), "r");
|
|
958
1237
|
const messages = [];
|
|
959
1238
|
const decoder = new StringDecoder("utf8");
|
|
960
1239
|
const buffer = Buffer.alloc(MESSAGES_READ_CHUNK_BYTES);
|
|
@@ -979,7 +1258,8 @@ var SessionStore = class _SessionStore {
|
|
|
979
1258
|
}
|
|
980
1259
|
pushLine(tail + decoder.end());
|
|
981
1260
|
return messages;
|
|
982
|
-
} catch {
|
|
1261
|
+
} catch (error) {
|
|
1262
|
+
if (strict && error.code !== "ENOENT") throw error;
|
|
983
1263
|
return [];
|
|
984
1264
|
} finally {
|
|
985
1265
|
await handle?.close().catch(() => {
|
|
@@ -1003,7 +1283,7 @@ var SessionStore = class _SessionStore {
|
|
|
1003
1283
|
await this.ensureInitialized();
|
|
1004
1284
|
if (this.cachedMeta) return this.cachedMeta;
|
|
1005
1285
|
try {
|
|
1006
|
-
const raw = await
|
|
1286
|
+
const raw = await fs2.readFile(this.metaPath(), "utf8");
|
|
1007
1287
|
this.cachedMeta = JSON.parse(raw);
|
|
1008
1288
|
return this.cachedMeta;
|
|
1009
1289
|
} catch {
|
|
@@ -1012,70 +1292,102 @@ var SessionStore = class _SessionStore {
|
|
|
1012
1292
|
}
|
|
1013
1293
|
}
|
|
1014
1294
|
async setTitle(title) {
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1295
|
+
this.assertCanWrite();
|
|
1296
|
+
return this.enqueueMessageWrite(async () => {
|
|
1297
|
+
await this.ensureInitialized();
|
|
1298
|
+
const meta = await this.meta();
|
|
1299
|
+
meta.title = title;
|
|
1300
|
+
meta.updatedAt = Date.now();
|
|
1301
|
+
await this.flushMetaNow();
|
|
1302
|
+
});
|
|
1020
1303
|
}
|
|
1021
1304
|
// /model swaps the live loop mid-session; keep the recorded model in sync
|
|
1022
1305
|
// or the session list and /usage attribute the whole session to whichever
|
|
1023
1306
|
// model happened to be active at creation.
|
|
1024
1307
|
async setModel(model) {
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1308
|
+
this.assertCanWrite();
|
|
1309
|
+
return this.enqueueMessageWrite(async () => {
|
|
1310
|
+
await this.ensureInitialized();
|
|
1311
|
+
const meta = await this.meta();
|
|
1312
|
+
meta.model = model;
|
|
1313
|
+
meta.updatedAt = Date.now();
|
|
1314
|
+
await this.flushMetaNow();
|
|
1315
|
+
});
|
|
1030
1316
|
}
|
|
1031
1317
|
async appendCheckpoint(record, content) {
|
|
1032
|
-
|
|
1033
|
-
|
|
1318
|
+
this.assertCanWrite();
|
|
1319
|
+
return this.enqueueMessageWrite(async () => {
|
|
1320
|
+
await this.ensureInitialized();
|
|
1321
|
+
await appendCheckpointRecord(this.dir, record, content);
|
|
1322
|
+
});
|
|
1034
1323
|
}
|
|
1035
1324
|
async listCheckpoints() {
|
|
1036
1325
|
return listCheckpointRecords(this.dir);
|
|
1037
1326
|
}
|
|
1038
1327
|
async removeCheckpoints(ids) {
|
|
1039
|
-
|
|
1328
|
+
this.assertCanWrite();
|
|
1329
|
+
return this.enqueueMessageWrite(() => removeCheckpointRecords(this.dir, ids));
|
|
1040
1330
|
}
|
|
1041
|
-
async addUsage(delta) {
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1331
|
+
async addUsage(delta, attribution) {
|
|
1332
|
+
this.assertCanWrite();
|
|
1333
|
+
return this.enqueueMessageWrite(async () => {
|
|
1334
|
+
await this.ensureInitialized();
|
|
1335
|
+
const meta = await this.meta();
|
|
1336
|
+
const apply = (usage2) => {
|
|
1337
|
+
usage2.requests += 1;
|
|
1338
|
+
usage2.promptTokens += delta.promptTokens;
|
|
1339
|
+
usage2.completionTokens += delta.completionTokens;
|
|
1340
|
+
usage2.totalTokens += delta.totalTokens;
|
|
1341
|
+
if (delta.cachedPromptTokens) {
|
|
1342
|
+
usage2.cachedPromptTokens = (usage2.cachedPromptTokens ?? 0) + delta.cachedPromptTokens;
|
|
1343
|
+
}
|
|
1344
|
+
if (delta.cacheReadInputTokens) {
|
|
1345
|
+
usage2.cacheReadInputTokens = (usage2.cacheReadInputTokens ?? 0) + delta.cacheReadInputTokens;
|
|
1346
|
+
}
|
|
1347
|
+
if (attribution) {
|
|
1348
|
+
if (attribution.costUsd === null) usage2.costUnknown = true;
|
|
1349
|
+
else usage2.costUsd = (usage2.costUsd ?? 0) + attribution.costUsd;
|
|
1350
|
+
}
|
|
1351
|
+
};
|
|
1352
|
+
const usage = meta.usage ?? {
|
|
1353
|
+
requests: 0,
|
|
1354
|
+
promptTokens: 0,
|
|
1355
|
+
completionTokens: 0,
|
|
1356
|
+
totalTokens: 0
|
|
1357
|
+
};
|
|
1358
|
+
apply(usage);
|
|
1359
|
+
meta.usage = usage;
|
|
1360
|
+
if (attribution) {
|
|
1361
|
+
const byModel = meta.usageByModel ?? {};
|
|
1362
|
+
const modelUsage = byModel[attribution.modelName] ?? {
|
|
1363
|
+
requests: 0,
|
|
1364
|
+
promptTokens: 0,
|
|
1365
|
+
completionTokens: 0,
|
|
1366
|
+
totalTokens: 0
|
|
1367
|
+
};
|
|
1368
|
+
apply(modelUsage);
|
|
1369
|
+
byModel[attribution.modelName] = modelUsage;
|
|
1370
|
+
meta.usageByModel = byModel;
|
|
1371
|
+
}
|
|
1372
|
+
const day = dayKey(/* @__PURE__ */ new Date());
|
|
1373
|
+
const byDay = meta.usageByDay ?? {};
|
|
1374
|
+
const bucket = byDay[day] ?? { promptTokens: 0, completionTokens: 0, totalTokens: 0 };
|
|
1375
|
+
bucket.promptTokens += delta.promptTokens;
|
|
1376
|
+
bucket.completionTokens += delta.completionTokens;
|
|
1377
|
+
bucket.totalTokens += delta.totalTokens;
|
|
1378
|
+
byDay[day] = bucket;
|
|
1379
|
+
meta.usageByDay = byDay;
|
|
1380
|
+
await this.flushMetaNow();
|
|
1381
|
+
});
|
|
1070
1382
|
}
|
|
1071
1383
|
};
|
|
1072
1384
|
|
|
1073
1385
|
// src/session/checkpoints.ts
|
|
1074
1386
|
function checkpointsDir(sessionDir) {
|
|
1075
|
-
return
|
|
1387
|
+
return path4.join(sessionDir, "checkpoints");
|
|
1076
1388
|
}
|
|
1077
1389
|
function indexPath(sessionDir) {
|
|
1078
|
-
return
|
|
1390
|
+
return path4.join(checkpointsDir(sessionDir), "index.json");
|
|
1079
1391
|
}
|
|
1080
1392
|
var indexWriteQueues = /* @__PURE__ */ new Map();
|
|
1081
1393
|
function enqueueIndexWrite(sessionDir, op) {
|
|
@@ -1093,28 +1405,27 @@ function enqueueIndexWrite(sessionDir, op) {
|
|
|
1093
1405
|
});
|
|
1094
1406
|
return run;
|
|
1095
1407
|
}
|
|
1096
|
-
var indexTmpSeq = 0;
|
|
1097
1408
|
async function writeIndex(sessionDir, records) {
|
|
1098
1409
|
const target = indexPath(sessionDir);
|
|
1099
|
-
const tmp = `${target}.tmp-${process.pid}-${
|
|
1410
|
+
const tmp = `${target}.tmp-${process.pid}-${randomUUID3()}`;
|
|
1100
1411
|
try {
|
|
1101
1412
|
await withTransientFsRetry(async () => {
|
|
1102
|
-
await
|
|
1103
|
-
await
|
|
1413
|
+
await fs3.writeFile(tmp, JSON.stringify(records, null, 2));
|
|
1414
|
+
await fs3.rename(tmp, target);
|
|
1104
1415
|
});
|
|
1105
1416
|
} catch (error) {
|
|
1106
|
-
await
|
|
1417
|
+
await fs3.unlink(tmp).catch(() => {
|
|
1107
1418
|
});
|
|
1108
1419
|
throw error;
|
|
1109
1420
|
}
|
|
1110
1421
|
}
|
|
1111
1422
|
function checkpointContentPath(sessionDir, id) {
|
|
1112
|
-
return
|
|
1423
|
+
return path4.join(checkpointsDir(sessionDir), `${id}.snapshot`);
|
|
1113
1424
|
}
|
|
1114
1425
|
async function listCheckpointRecords(sessionDir) {
|
|
1115
1426
|
let raw;
|
|
1116
1427
|
try {
|
|
1117
|
-
raw = await
|
|
1428
|
+
raw = await fs3.readFile(indexPath(sessionDir), "utf8");
|
|
1118
1429
|
} catch {
|
|
1119
1430
|
return [];
|
|
1120
1431
|
}
|
|
@@ -1123,7 +1434,7 @@ async function listCheckpointRecords(sessionDir) {
|
|
|
1123
1434
|
return Array.isArray(parsed) ? parsed : [];
|
|
1124
1435
|
} catch {
|
|
1125
1436
|
process.stderr.write(
|
|
1126
|
-
`[star-cli] checkpoints index unreadable for session ${
|
|
1437
|
+
`[star-cli] checkpoints index unreadable for session ${path4.basename(sessionDir)}, ignoring it
|
|
1127
1438
|
`
|
|
1128
1439
|
);
|
|
1129
1440
|
return [];
|
|
@@ -1131,9 +1442,20 @@ async function listCheckpointRecords(sessionDir) {
|
|
|
1131
1442
|
}
|
|
1132
1443
|
async function appendCheckpointRecord(sessionDir, record, content) {
|
|
1133
1444
|
await enqueueIndexWrite(sessionDir, async () => {
|
|
1134
|
-
await
|
|
1445
|
+
await fs3.mkdir(checkpointsDir(sessionDir), { recursive: true, mode: 448 });
|
|
1135
1446
|
if (record.existed && content !== null) {
|
|
1136
|
-
|
|
1447
|
+
const target = checkpointContentPath(sessionDir, record.id);
|
|
1448
|
+
const tmp = `${target}.tmp-${process.pid}-${randomUUID3()}`;
|
|
1449
|
+
try {
|
|
1450
|
+
await withTransientFsRetry(async () => {
|
|
1451
|
+
await fs3.writeFile(tmp, content, "utf8");
|
|
1452
|
+
await fs3.rename(tmp, target);
|
|
1453
|
+
});
|
|
1454
|
+
} catch (error) {
|
|
1455
|
+
await fs3.unlink(tmp).catch(() => {
|
|
1456
|
+
});
|
|
1457
|
+
throw error;
|
|
1458
|
+
}
|
|
1137
1459
|
}
|
|
1138
1460
|
const records = await listCheckpointRecords(sessionDir);
|
|
1139
1461
|
records.push(record);
|
|
@@ -1147,10 +1469,10 @@ async function removeCheckpointRecords(sessionDir, ids) {
|
|
|
1147
1469
|
const records = await listCheckpointRecords(sessionDir);
|
|
1148
1470
|
const kept = records.filter((record) => !drop.has(record.id));
|
|
1149
1471
|
if (kept.length === records.length) return;
|
|
1150
|
-
await
|
|
1472
|
+
await fs3.mkdir(checkpointsDir(sessionDir), { recursive: true, mode: 448 });
|
|
1151
1473
|
await writeIndex(sessionDir, kept);
|
|
1152
1474
|
for (const id of ids) {
|
|
1153
|
-
await
|
|
1475
|
+
await fs3.rm(checkpointContentPath(sessionDir, id), { force: true });
|
|
1154
1476
|
}
|
|
1155
1477
|
});
|
|
1156
1478
|
}
|
|
@@ -1164,13 +1486,13 @@ async function loadSessionSnapshots(sessionDir) {
|
|
|
1164
1486
|
}
|
|
1165
1487
|
|
|
1166
1488
|
// src/session/list.ts
|
|
1167
|
-
import
|
|
1168
|
-
import
|
|
1489
|
+
import fs4 from "fs/promises";
|
|
1490
|
+
import path5 from "path";
|
|
1169
1491
|
var COUNT_CHUNK_BYTES = 64 * 1024;
|
|
1170
1492
|
async function countMessages(id) {
|
|
1171
1493
|
let handle = null;
|
|
1172
1494
|
try {
|
|
1173
|
-
handle = await
|
|
1495
|
+
handle = await fs4.open(path5.join(sessionsDir(), id, "messages.jsonl"), "r");
|
|
1174
1496
|
const buffer = Buffer.alloc(COUNT_CHUNK_BYTES);
|
|
1175
1497
|
let count = 0;
|
|
1176
1498
|
let lineHasContent = false;
|
|
@@ -1221,7 +1543,7 @@ async function findLatestSession(cwd) {
|
|
|
1221
1543
|
async function sessionPreview(id, maxBytes = 4096) {
|
|
1222
1544
|
let handle = null;
|
|
1223
1545
|
try {
|
|
1224
|
-
handle = await
|
|
1546
|
+
handle = await fs4.open(path5.join(sessionsDir(), id, "messages.jsonl"), "r");
|
|
1225
1547
|
const buffer = Buffer.alloc(maxBytes);
|
|
1226
1548
|
const { bytesRead } = await handle.read(buffer, 0, maxBytes, 0);
|
|
1227
1549
|
const head = buffer.toString("utf8", 0, bytesRead);
|
|
@@ -1295,11 +1617,22 @@ When you report completion, state the evidence: which checks you ran and that th
|
|
|
1295
1617
|
Never end a reply by announcing what you will do next \u2014 either do it now with tool calls, or do not mention it. Narrating future actions is not progress.`;
|
|
1296
1618
|
|
|
1297
1619
|
// src/session/clear.ts
|
|
1298
|
-
import
|
|
1299
|
-
import
|
|
1620
|
+
import fs5 from "fs/promises";
|
|
1621
|
+
import path6 from "path";
|
|
1622
|
+
async function removeIfUnlocked(id) {
|
|
1623
|
+
const dir = path6.join(sessionsDir(), id);
|
|
1624
|
+
const lock = await acquireSessionLock(dir, id).catch((error) => {
|
|
1625
|
+
if (error instanceof SessionLockedError) return null;
|
|
1626
|
+
throw error;
|
|
1627
|
+
});
|
|
1628
|
+
if (!lock) return false;
|
|
1629
|
+
await lock.release();
|
|
1630
|
+
await fs5.rm(dir, { recursive: true, force: true });
|
|
1631
|
+
return true;
|
|
1632
|
+
}
|
|
1300
1633
|
async function deleteSession(id) {
|
|
1301
1634
|
const meta = (await SessionStore.list()).find((m) => m.id === id);
|
|
1302
|
-
await
|
|
1635
|
+
if (!await removeIfUnlocked(id)) return;
|
|
1303
1636
|
if (meta && (await SessionStore.list(meta.cwd)).length === 0) {
|
|
1304
1637
|
await removeTreeRepo(meta.cwd);
|
|
1305
1638
|
}
|
|
@@ -1310,8 +1643,7 @@ async function clearSessions(cwd, excludeId) {
|
|
|
1310
1643
|
let removed2 = 0;
|
|
1311
1644
|
for (const meta of metas) {
|
|
1312
1645
|
if (meta.id === excludeId) continue;
|
|
1313
|
-
await
|
|
1314
|
-
removed2 += 1;
|
|
1646
|
+
if (await removeIfUnlocked(meta.id)) removed2 += 1;
|
|
1315
1647
|
}
|
|
1316
1648
|
if ((await SessionStore.list(cwd)).length === 0) {
|
|
1317
1649
|
await removeTreeRepo(cwd);
|
|
@@ -1320,15 +1652,14 @@ async function clearSessions(cwd, excludeId) {
|
|
|
1320
1652
|
}
|
|
1321
1653
|
let entries;
|
|
1322
1654
|
try {
|
|
1323
|
-
entries = await
|
|
1655
|
+
entries = await fs5.readdir(sessionsDir());
|
|
1324
1656
|
} catch {
|
|
1325
1657
|
return 0;
|
|
1326
1658
|
}
|
|
1327
1659
|
let removed = 0;
|
|
1328
1660
|
for (const entry of entries) {
|
|
1329
1661
|
if (entry === excludeId) continue;
|
|
1330
|
-
await
|
|
1331
|
-
removed += 1;
|
|
1662
|
+
if (await removeIfUnlocked(entry)) removed += 1;
|
|
1332
1663
|
}
|
|
1333
1664
|
const survivors = await SessionStore.list();
|
|
1334
1665
|
await pruneTreeReposExcept(survivors.map((meta) => meta.cwd));
|
|
@@ -1336,13 +1667,13 @@ async function clearSessions(cwd, excludeId) {
|
|
|
1336
1667
|
}
|
|
1337
1668
|
|
|
1338
1669
|
// src/version.ts
|
|
1339
|
-
import
|
|
1340
|
-
import
|
|
1670
|
+
import fs6 from "fs";
|
|
1671
|
+
import path7 from "path";
|
|
1341
1672
|
import { fileURLToPath } from "url";
|
|
1342
1673
|
function readPackageVersion() {
|
|
1343
1674
|
try {
|
|
1344
|
-
const pkgPath =
|
|
1345
|
-
const pkg = JSON.parse(
|
|
1675
|
+
const pkgPath = path7.join(path7.dirname(fileURLToPath(import.meta.url)), "..", "package.json");
|
|
1676
|
+
const pkg = JSON.parse(fs6.readFileSync(pkgPath, "utf8"));
|
|
1346
1677
|
if (typeof pkg === "object" && pkg !== null) {
|
|
1347
1678
|
const version = pkg.version;
|
|
1348
1679
|
if (typeof version === "string") return version;
|
|
@@ -1357,7 +1688,6 @@ export {
|
|
|
1357
1688
|
formatDollars,
|
|
1358
1689
|
formatTokens,
|
|
1359
1690
|
formatTokensCompact,
|
|
1360
|
-
computeCostUsd,
|
|
1361
1691
|
estimateCost,
|
|
1362
1692
|
resolveIconMode,
|
|
1363
1693
|
toolIcon,
|
|
@@ -1365,6 +1695,8 @@ export {
|
|
|
1365
1695
|
folderIcon,
|
|
1366
1696
|
renderMarkdown,
|
|
1367
1697
|
toTerminalSafe,
|
|
1698
|
+
displayWidth,
|
|
1699
|
+
truncateToWidth,
|
|
1368
1700
|
summarizeArgs,
|
|
1369
1701
|
previewLines,
|
|
1370
1702
|
formatByteCount,
|
|
@@ -1390,4 +1722,4 @@ export {
|
|
|
1390
1722
|
clearSessions,
|
|
1391
1723
|
VERSION
|
|
1392
1724
|
};
|
|
1393
|
-
//# sourceMappingURL=chunk-
|
|
1725
|
+
//# sourceMappingURL=chunk-NRLGGA7L.js.map
|