sidekick-shared 0.24.5 → 0.25.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 +35 -0
- package/README.md +159 -52
- package/dist/accountChangeSignal.d.ts +3 -0
- package/dist/accountChangeSignal.js +19 -0
- package/dist/accountChanges.d.ts +21 -0
- package/dist/accountChanges.js +151 -0
- package/dist/accountRegistry.d.ts +3 -1
- package/dist/accountRegistry.js +5 -0
- package/dist/browser.d.ts +8 -3
- package/dist/browser.js +15 -1
- package/dist/codexQuotaWatcher.d.ts +6 -0
- package/dist/codexQuotaWatcher.js +101 -47
- package/dist/index.d.ts +23 -13
- package/dist/index.js +34 -10
- package/dist/modelAliases.d.ts +13 -0
- package/dist/modelAliases.js +58 -0
- package/dist/modelCatalog.d.ts +32 -0
- package/dist/modelCatalog.js +145 -0
- package/dist/modelContext.d.ts +36 -3
- package/dist/modelContext.js +109 -34
- package/dist/modelInfo.d.ts +30 -1
- package/dist/modelInfo.js +116 -26
- package/dist/multiProviderQuotaService.d.ts +8 -0
- package/dist/multiProviderQuotaService.js +64 -3
- package/dist/node.d.ts +4 -4
- package/dist/node.js +4 -1
- package/dist/observedSessionCollector.d.ts +75 -4
- package/dist/observedSessionCollector.js +452 -42
- package/dist/parsers/codexHistory.d.ts +4 -2
- package/dist/parsers/codexHistory.js +9 -2
- package/dist/parsers/sessionActivityDetector.d.ts +6 -0
- package/dist/parsers/sessionActivityDetector.js +17 -0
- package/dist/providerIds.d.ts +11 -0
- package/dist/providerIds.js +9 -0
- package/dist/providerQuota.d.ts +3 -1
- package/dist/providerQuota.js +3 -0
- package/dist/providers/claudeCode.d.ts +10 -1
- package/dist/providers/claudeCode.js +204 -39
- package/dist/providers/codex.d.ts +18 -1
- package/dist/providers/codex.js +269 -40
- package/dist/providers/codexDatabase.d.ts +9 -0
- package/dist/providers/codexDatabase.js +107 -2
- package/dist/providers/diagnostics.d.ts +13 -0
- package/dist/providers/diagnostics.js +88 -0
- package/dist/providers/factory.d.ts +14 -0
- package/dist/providers/factory.js +57 -0
- package/dist/providers/openCode.d.ts +20 -1
- package/dist/providers/openCode.js +361 -11
- package/dist/providers/openCodeDatabase.d.ts +10 -0
- package/dist/providers/openCodeDatabase.js +69 -0
- package/dist/providers/types.d.ts +52 -2
- package/dist/providers/types.js +4 -0
- package/dist/quota.d.ts +4 -1
- package/dist/quota.js +3 -7
- package/dist/quotaHistory.d.ts +2 -1
- package/dist/quotaPoller.d.ts +13 -1
- package/dist/quotaPoller.js +65 -3
- package/dist/schemas/accountManager.d.ts +2 -2
- package/dist/schemas/accountManager.js +2 -4
- package/dist/schemas/accountStatus.d.ts +3 -0
- package/dist/schemas/accountStatus.js +1 -0
- package/dist/schemas/index.d.ts +1 -0
- package/dist/schemas/index.js +7 -2
- package/dist/schemas/observedSessionV1.d.ts +27 -26
- package/dist/schemas/observedSessionV1.js +6 -4
- package/dist/schemas/quota.d.ts +2 -2
- package/dist/schemas/quota.js +3 -6
- package/dist/schemas/quotaHistory.d.ts +2 -2
- package/dist/schemas/tokenEstimation.d.ts +1 -1
- package/dist/sessionMonitor.d.ts +15 -0
- package/dist/sessionMonitor.js +142 -0
- package/dist/sessionPreviews.d.ts +26 -1
- package/dist/sessionPreviews.js +319 -4
- package/dist/types/observedSessionV1.d.ts +6 -2
- package/dist/types/observedSessionV1.js +14 -8
- package/package.json +22 -1
|
@@ -60,6 +60,7 @@ const sessionContext_1 = require("../context/sessionContext");
|
|
|
60
60
|
const openCodeParser_1 = require("../parsers/openCodeParser");
|
|
61
61
|
const openCodeDatabase_1 = require("./openCodeDatabase");
|
|
62
62
|
const zaiQuota_1 = require("../zaiQuota");
|
|
63
|
+
const diagnostics_1 = require("./diagnostics");
|
|
63
64
|
const modelContext_1 = require("../modelContext");
|
|
64
65
|
// ---------------------------------------------------------------------------
|
|
65
66
|
// Helper functions
|
|
@@ -169,6 +170,17 @@ function makeDbSessionPath(dataDir, projectId, sessionId) {
|
|
|
169
170
|
function isDbSessionPath(sessionPath) {
|
|
170
171
|
return sessionPath.includes(path.sep + DB_SESSION_PREFIX + path.sep);
|
|
171
172
|
}
|
|
173
|
+
function workspaceMatches(left, right) {
|
|
174
|
+
const a = path
|
|
175
|
+
.resolve(left)
|
|
176
|
+
.replace(/[\\/]+$/, '')
|
|
177
|
+
.toLowerCase();
|
|
178
|
+
const b = path
|
|
179
|
+
.resolve(right)
|
|
180
|
+
.replace(/[\\/]+$/, '')
|
|
181
|
+
.toLowerCase();
|
|
182
|
+
return a === b || a.startsWith(b + path.sep) || b.startsWith(a + path.sep);
|
|
183
|
+
}
|
|
172
184
|
/** Extract project ID from a synthetic DB session path. */
|
|
173
185
|
function extractProjectIdFromDbPath(sessionPath) {
|
|
174
186
|
const prefix = path.sep + DB_SESSION_PREFIX + path.sep;
|
|
@@ -650,10 +662,14 @@ class OpenCodeProvider {
|
|
|
650
662
|
db = null;
|
|
651
663
|
dbInitialized = false;
|
|
652
664
|
dbStatus = { available: false, kind: 'db_missing' };
|
|
665
|
+
diagnostics;
|
|
653
666
|
/** Cache of session metadata populated during listing */
|
|
654
667
|
sessionMetaCache = new Map();
|
|
655
668
|
/** Runtime-reported context window limit (overrides static map when set). */
|
|
656
669
|
dynamicContextWindowLimit = null;
|
|
670
|
+
constructor(options = {}) {
|
|
671
|
+
this.diagnostics = new diagnostics_1.ProviderDiagnosticTracker(this.id, options);
|
|
672
|
+
}
|
|
657
673
|
/** Lazy-initialize the database connection. */
|
|
658
674
|
ensureDb() {
|
|
659
675
|
if (this.dbInitialized)
|
|
@@ -667,12 +683,31 @@ class OpenCodeProvider {
|
|
|
667
683
|
this.dbStatus = db.getRuntimeStatus();
|
|
668
684
|
return this.db;
|
|
669
685
|
}
|
|
686
|
+
async ensureDbAsync() {
|
|
687
|
+
if (this.db)
|
|
688
|
+
return this.db;
|
|
689
|
+
return new openCodeDatabase_1.OpenCodeDatabase(getOpenCodeDataDir());
|
|
690
|
+
}
|
|
691
|
+
/**
|
|
692
|
+
* Cache an async-created database only after an operation proved it usable.
|
|
693
|
+
* Sync paths treat a cached `db` as "open() succeeded", so adopting an
|
|
694
|
+
* unproven instance would cut them off from the file-storage fallback.
|
|
695
|
+
*/
|
|
696
|
+
adoptDbIfAvailable(db) {
|
|
697
|
+
if (this.db || !db.getRuntimeStatus().available)
|
|
698
|
+
return;
|
|
699
|
+
this.db = db;
|
|
700
|
+
this.dbInitialized = true;
|
|
701
|
+
}
|
|
670
702
|
getRuntimeStatus() {
|
|
671
703
|
const db = this.ensureDb();
|
|
672
704
|
if (db)
|
|
673
705
|
return db.getRuntimeStatus();
|
|
674
706
|
return this.dbStatus;
|
|
675
707
|
}
|
|
708
|
+
getLastOperationStatus() {
|
|
709
|
+
return this.diagnostics.getLastOperationStatus();
|
|
710
|
+
}
|
|
676
711
|
canMonitorDirectory(dir) {
|
|
677
712
|
if (fs.existsSync(dir))
|
|
678
713
|
return true;
|
|
@@ -738,8 +773,10 @@ class OpenCodeProvider {
|
|
|
738
773
|
const db = this.ensureDb();
|
|
739
774
|
const dbStatus = this.getRuntimeStatus();
|
|
740
775
|
const projectId = resolveProjectId(workspacePath, db, dbStatus);
|
|
741
|
-
if (!projectId)
|
|
776
|
+
if (!projectId) {
|
|
777
|
+
this.recordDatabaseStatus('findActiveSession', dbStatus.kind === 'db_missing');
|
|
742
778
|
return null;
|
|
779
|
+
}
|
|
743
780
|
// DB primary
|
|
744
781
|
if (db) {
|
|
745
782
|
const session = db.getMostRecentSession(projectId);
|
|
@@ -749,14 +786,18 @@ class OpenCodeProvider {
|
|
|
749
786
|
title: session.title,
|
|
750
787
|
timeUpdated: session.time_updated,
|
|
751
788
|
});
|
|
789
|
+
this.recordDatabaseStatus('findActiveSession', true);
|
|
752
790
|
return syntheticPath;
|
|
753
791
|
}
|
|
754
792
|
}
|
|
755
793
|
if (dbStatus.kind !== 'db_missing') {
|
|
794
|
+
this.recordDatabaseStatus('findActiveSession', dbStatus.available);
|
|
756
795
|
return null;
|
|
757
796
|
}
|
|
758
797
|
// File fallback
|
|
759
|
-
|
|
798
|
+
const result = this.findActiveSessionFromFiles(projectId);
|
|
799
|
+
this.recordDatabaseStatus('findActiveSession', true);
|
|
800
|
+
return result;
|
|
760
801
|
}
|
|
761
802
|
findActiveSessionFromFiles(projectId) {
|
|
762
803
|
const sessionDir = path.join(getStorageDir(), 'session', projectId);
|
|
@@ -817,18 +858,64 @@ class OpenCodeProvider {
|
|
|
817
858
|
const db = this.ensureDb();
|
|
818
859
|
const dbStatus = this.getRuntimeStatus();
|
|
819
860
|
const projectId = resolveProjectId(workspacePath, db, dbStatus);
|
|
820
|
-
if (!projectId)
|
|
861
|
+
if (!projectId) {
|
|
862
|
+
this.recordDatabaseStatus('findAllSessions', dbStatus.kind === 'db_missing');
|
|
821
863
|
return [];
|
|
864
|
+
}
|
|
822
865
|
// DB primary
|
|
823
866
|
if (db) {
|
|
824
|
-
|
|
867
|
+
const sessions = this.mapDbSessions(projectId);
|
|
868
|
+
this.recordDatabaseStatus('findAllSessions', true);
|
|
869
|
+
return sessions;
|
|
825
870
|
}
|
|
826
871
|
if (dbStatus.kind !== 'db_missing') {
|
|
872
|
+
this.recordDatabaseStatus('findAllSessions', dbStatus.available);
|
|
827
873
|
return [];
|
|
828
874
|
}
|
|
829
875
|
// File fallback
|
|
830
876
|
const sessionDir = path.join(getStorageDir(), 'session', projectId);
|
|
831
|
-
|
|
877
|
+
const sessions = this.findSessionsInDirectoryFromFiles(sessionDir);
|
|
878
|
+
this.recordDatabaseStatus('findAllSessions', true);
|
|
879
|
+
return sessions;
|
|
880
|
+
}
|
|
881
|
+
findSessionById(workspacePath, sessionId) {
|
|
882
|
+
const normalizedId = sessionId.trim();
|
|
883
|
+
if (!normalizedId || path.basename(normalizedId) !== normalizedId)
|
|
884
|
+
return null;
|
|
885
|
+
try {
|
|
886
|
+
const db = this.ensureDb();
|
|
887
|
+
if (db) {
|
|
888
|
+
const session = db.getSession(normalizedId);
|
|
889
|
+
if (session && workspaceMatches(session.directory, workspacePath)) {
|
|
890
|
+
const syntheticPath = makeDbSessionPath(getOpenCodeDataDir(), session.project_id, session.id);
|
|
891
|
+
this.sessionMetaCache.set(syntheticPath, {
|
|
892
|
+
title: session.title,
|
|
893
|
+
timeUpdated: session.time_updated,
|
|
894
|
+
});
|
|
895
|
+
this.recordDatabaseStatus('findSessionById', true);
|
|
896
|
+
return syntheticPath;
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
const projectId = resolveProjectId(workspacePath, db, this.getRuntimeStatus());
|
|
900
|
+
if (projectId) {
|
|
901
|
+
const candidate = path.join(getStorageDir(), 'session', projectId, `${normalizedId}.json`);
|
|
902
|
+
try {
|
|
903
|
+
if (fs.statSync(candidate).isFile()) {
|
|
904
|
+
this.recordDatabaseStatus('findSessionById', true);
|
|
905
|
+
return candidate;
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
catch {
|
|
909
|
+
// Missing legacy session is a normal miss.
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
this.recordDatabaseStatus('findSessionById', true);
|
|
913
|
+
return null;
|
|
914
|
+
}
|
|
915
|
+
catch {
|
|
916
|
+
this.recordDatabaseStatus('findSessionById', false);
|
|
917
|
+
return null;
|
|
918
|
+
}
|
|
832
919
|
}
|
|
833
920
|
findSessionsInDirectory(dir) {
|
|
834
921
|
const db = this.ensureDb();
|
|
@@ -837,7 +924,9 @@ class OpenCodeProvider {
|
|
|
837
924
|
if (db && dir.includes(path.sep + DB_SESSION_PREFIX + path.sep)) {
|
|
838
925
|
const projectId = extractProjectIdFromDbPath(dir + path.sep + 'dummy.json');
|
|
839
926
|
if (projectId) {
|
|
840
|
-
|
|
927
|
+
const sessions = this.mapDbSessions(projectId);
|
|
928
|
+
this.recordDatabaseStatus('findSessionsInDirectory', true);
|
|
929
|
+
return sessions;
|
|
841
930
|
}
|
|
842
931
|
}
|
|
843
932
|
// For a DB-backed directory path ending with the project ID,
|
|
@@ -845,14 +934,19 @@ class OpenCodeProvider {
|
|
|
845
934
|
if (db) {
|
|
846
935
|
const dirName = path.basename(dir);
|
|
847
936
|
if (db.hasProject(dirName)) {
|
|
848
|
-
|
|
937
|
+
const sessions = this.mapDbSessions(dirName);
|
|
938
|
+
this.recordDatabaseStatus('findSessionsInDirectory', true);
|
|
939
|
+
return sessions;
|
|
849
940
|
}
|
|
850
941
|
}
|
|
851
942
|
if (dbStatus.kind !== 'db_missing') {
|
|
943
|
+
this.recordDatabaseStatus('findSessionsInDirectory', dbStatus.available);
|
|
852
944
|
return [];
|
|
853
945
|
}
|
|
854
946
|
// File fallback
|
|
855
|
-
|
|
947
|
+
const sessions = this.findSessionsInDirectoryFromFiles(dir);
|
|
948
|
+
this.recordDatabaseStatus('findSessionsInDirectory', true);
|
|
949
|
+
return sessions;
|
|
856
950
|
}
|
|
857
951
|
findSessionsInDirectoryFromFiles(dir) {
|
|
858
952
|
try {
|
|
@@ -879,6 +973,159 @@ class OpenCodeProvider {
|
|
|
879
973
|
return [];
|
|
880
974
|
}
|
|
881
975
|
}
|
|
976
|
+
listAllSessionFiles() {
|
|
977
|
+
try {
|
|
978
|
+
const db = this.ensureDb();
|
|
979
|
+
if (db) {
|
|
980
|
+
const dataDir = getOpenCodeDataDir();
|
|
981
|
+
const files = db.getAllSessions().map((session) => {
|
|
982
|
+
const syntheticPath = makeDbSessionPath(dataDir, session.project_id, session.id);
|
|
983
|
+
this.sessionMetaCache.set(syntheticPath, {
|
|
984
|
+
title: session.title,
|
|
985
|
+
timeUpdated: session.time_updated,
|
|
986
|
+
});
|
|
987
|
+
return {
|
|
988
|
+
path: syntheticPath,
|
|
989
|
+
mtime: new Date(session.time_updated),
|
|
990
|
+
label: session.title ?? undefined,
|
|
991
|
+
sizeBytes: 0,
|
|
992
|
+
sessionId: session.id,
|
|
993
|
+
workspacePath: session.directory,
|
|
994
|
+
createdAt: new Date(session.time_created),
|
|
995
|
+
};
|
|
996
|
+
});
|
|
997
|
+
this.recordDatabaseStatus('listAllSessionFiles', true);
|
|
998
|
+
return files;
|
|
999
|
+
}
|
|
1000
|
+
if (this.getRuntimeStatus().kind !== 'db_missing') {
|
|
1001
|
+
this.recordDatabaseStatus('listAllSessionFiles', true);
|
|
1002
|
+
return [];
|
|
1003
|
+
}
|
|
1004
|
+
const root = path.join(getStorageDir(), 'session');
|
|
1005
|
+
const results = [];
|
|
1006
|
+
let projectDirs = [];
|
|
1007
|
+
try {
|
|
1008
|
+
projectDirs = fs.readdirSync(root, { withFileTypes: true });
|
|
1009
|
+
}
|
|
1010
|
+
catch {
|
|
1011
|
+
this.recordDatabaseStatus('listAllSessionFiles', true);
|
|
1012
|
+
return [];
|
|
1013
|
+
}
|
|
1014
|
+
for (const entry of projectDirs) {
|
|
1015
|
+
if (!entry.isDirectory())
|
|
1016
|
+
continue;
|
|
1017
|
+
for (const sessionPath of this.findSessionsInDirectoryFromFiles(path.join(root, entry.name))) {
|
|
1018
|
+
try {
|
|
1019
|
+
const stats = fs.statSync(sessionPath);
|
|
1020
|
+
results.push({
|
|
1021
|
+
path: sessionPath,
|
|
1022
|
+
mtime: stats.mtime,
|
|
1023
|
+
sizeBytes: stats.size,
|
|
1024
|
+
sessionId: path.basename(sessionPath, '.json'),
|
|
1025
|
+
});
|
|
1026
|
+
}
|
|
1027
|
+
catch {
|
|
1028
|
+
// Session vanished between listing and stat.
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
1031
|
+
}
|
|
1032
|
+
this.recordDatabaseStatus('listAllSessionFiles', true);
|
|
1033
|
+
return results.sort((a, b) => b.mtime.getTime() - a.mtime.getTime());
|
|
1034
|
+
}
|
|
1035
|
+
catch {
|
|
1036
|
+
this.recordDatabaseStatus('listAllSessionFiles', false);
|
|
1037
|
+
return [];
|
|
1038
|
+
}
|
|
1039
|
+
}
|
|
1040
|
+
async listSessionFilesAsync(workspacePath) {
|
|
1041
|
+
const db = await this.ensureDbAsync();
|
|
1042
|
+
const sessions = await db.getAllSessionsAsync();
|
|
1043
|
+
this.dbStatus = db.getRuntimeStatus();
|
|
1044
|
+
this.adoptDbIfAvailable(db);
|
|
1045
|
+
if (this.dbStatus.available) {
|
|
1046
|
+
const dataDir = getOpenCodeDataDir();
|
|
1047
|
+
const files = [];
|
|
1048
|
+
for (const session of sessions) {
|
|
1049
|
+
if (workspacePath && !workspaceMatches(session.directory, workspacePath))
|
|
1050
|
+
continue;
|
|
1051
|
+
const syntheticPath = makeDbSessionPath(dataDir, session.project_id, session.id);
|
|
1052
|
+
this.sessionMetaCache.set(syntheticPath, {
|
|
1053
|
+
title: session.title,
|
|
1054
|
+
timeUpdated: session.time_updated,
|
|
1055
|
+
});
|
|
1056
|
+
files.push({
|
|
1057
|
+
path: syntheticPath,
|
|
1058
|
+
mtime: new Date(session.time_updated),
|
|
1059
|
+
label: session.title ?? undefined,
|
|
1060
|
+
sizeBytes: 0,
|
|
1061
|
+
sessionId: session.id,
|
|
1062
|
+
workspacePath: session.directory,
|
|
1063
|
+
createdAt: new Date(session.time_created),
|
|
1064
|
+
});
|
|
1065
|
+
}
|
|
1066
|
+
this.recordDatabaseStatus('listSessionFilesAsync', true);
|
|
1067
|
+
return files;
|
|
1068
|
+
}
|
|
1069
|
+
const root = path.join(getStorageDir(), 'session');
|
|
1070
|
+
const results = [];
|
|
1071
|
+
let projectDirs = [];
|
|
1072
|
+
try {
|
|
1073
|
+
projectDirs = await fs.promises.readdir(root, { withFileTypes: true });
|
|
1074
|
+
}
|
|
1075
|
+
catch {
|
|
1076
|
+
this.recordDatabaseStatus('listSessionFilesAsync', true);
|
|
1077
|
+
return [];
|
|
1078
|
+
}
|
|
1079
|
+
for (const project of projectDirs) {
|
|
1080
|
+
if (!project.isDirectory())
|
|
1081
|
+
continue;
|
|
1082
|
+
let entries = [];
|
|
1083
|
+
try {
|
|
1084
|
+
entries = await fs.promises.readdir(path.join(root, project.name), {
|
|
1085
|
+
withFileTypes: true,
|
|
1086
|
+
});
|
|
1087
|
+
}
|
|
1088
|
+
catch {
|
|
1089
|
+
continue;
|
|
1090
|
+
}
|
|
1091
|
+
for (const entry of entries) {
|
|
1092
|
+
if (!entry.isFile() || !entry.name.endsWith('.json'))
|
|
1093
|
+
continue;
|
|
1094
|
+
const sessionPath = path.join(root, project.name, entry.name);
|
|
1095
|
+
try {
|
|
1096
|
+
const [stat, raw] = await Promise.all([
|
|
1097
|
+
fs.promises.stat(sessionPath),
|
|
1098
|
+
workspacePath ? fs.promises.readFile(sessionPath, 'utf8') : Promise.resolve(null),
|
|
1099
|
+
]);
|
|
1100
|
+
let session = null;
|
|
1101
|
+
if (raw !== null) {
|
|
1102
|
+
try {
|
|
1103
|
+
session = JSON.parse(raw);
|
|
1104
|
+
}
|
|
1105
|
+
catch {
|
|
1106
|
+
continue;
|
|
1107
|
+
}
|
|
1108
|
+
const directory = session.directory;
|
|
1109
|
+
if (directory && !workspaceMatches(directory, workspacePath))
|
|
1110
|
+
continue;
|
|
1111
|
+
}
|
|
1112
|
+
results.push({
|
|
1113
|
+
path: sessionPath,
|
|
1114
|
+
mtime: stat.mtime,
|
|
1115
|
+
sizeBytes: stat.size,
|
|
1116
|
+
sessionId: path.basename(entry.name, '.json'),
|
|
1117
|
+
workspacePath: session?.directory,
|
|
1118
|
+
});
|
|
1119
|
+
}
|
|
1120
|
+
catch {
|
|
1121
|
+
// Skip inaccessible sessions.
|
|
1122
|
+
}
|
|
1123
|
+
await new Promise((resolve) => setImmediate(resolve));
|
|
1124
|
+
}
|
|
1125
|
+
}
|
|
1126
|
+
this.recordDatabaseStatus('listSessionFilesAsync', true);
|
|
1127
|
+
return results.sort((a, b) => b.mtime.getTime() - a.mtime.getTime());
|
|
1128
|
+
}
|
|
882
1129
|
getAllProjectFolders(workspacePath) {
|
|
883
1130
|
const db = this.ensureDb();
|
|
884
1131
|
const folders = [];
|
|
@@ -917,14 +1164,19 @@ class OpenCodeProvider {
|
|
|
917
1164
|
}
|
|
918
1165
|
return b.lastModified.getTime() - a.lastModified.getTime();
|
|
919
1166
|
});
|
|
920
|
-
if (folders.length > 0)
|
|
1167
|
+
if (folders.length > 0) {
|
|
1168
|
+
this.recordDatabaseStatus('getAllProjectFolders', true);
|
|
921
1169
|
return folders;
|
|
1170
|
+
}
|
|
922
1171
|
}
|
|
923
1172
|
if (dbStatus.kind !== 'db_missing') {
|
|
1173
|
+
this.recordDatabaseStatus('getAllProjectFolders', dbStatus.available);
|
|
924
1174
|
return [];
|
|
925
1175
|
}
|
|
926
1176
|
// File fallback
|
|
927
|
-
|
|
1177
|
+
const fallback = this.getAllProjectFoldersFromFiles(workspacePath);
|
|
1178
|
+
this.recordDatabaseStatus('getAllProjectFolders', true);
|
|
1179
|
+
return fallback;
|
|
928
1180
|
}
|
|
929
1181
|
getAllProjectFoldersFromFiles(workspacePath) {
|
|
930
1182
|
const folders = [];
|
|
@@ -1031,11 +1283,60 @@ class OpenCodeProvider {
|
|
|
1031
1283
|
if (db) {
|
|
1032
1284
|
const session = db.getSession(sessionId);
|
|
1033
1285
|
if (session?.title) {
|
|
1286
|
+
this.recordDatabaseStatus('extractSessionLabel', true);
|
|
1034
1287
|
return truncateTitle(session.title);
|
|
1035
1288
|
}
|
|
1036
1289
|
}
|
|
1037
1290
|
// File fallback
|
|
1038
|
-
|
|
1291
|
+
const label = this.extractSessionLabelFromFiles(sessionPath, sessionId);
|
|
1292
|
+
this.recordDatabaseStatus('extractSessionLabel', label !== null || !isDbSessionPath(sessionPath));
|
|
1293
|
+
return label;
|
|
1294
|
+
}
|
|
1295
|
+
async extractSessionLabelsAsync(sessionPaths) {
|
|
1296
|
+
const results = new Map();
|
|
1297
|
+
const unresolved = [];
|
|
1298
|
+
for (const sessionPath of sessionPaths) {
|
|
1299
|
+
const cached = this.sessionMetaCache.get(sessionPath);
|
|
1300
|
+
if (cached)
|
|
1301
|
+
results.set(sessionPath, cached.title ? truncateTitle(cached.title) : null);
|
|
1302
|
+
else
|
|
1303
|
+
unresolved.push(sessionPath);
|
|
1304
|
+
}
|
|
1305
|
+
if (unresolved.length > 0 && (!this.dbInitialized || this.dbStatus.available)) {
|
|
1306
|
+
const db = await this.ensureDbAsync();
|
|
1307
|
+
const byId = new Map(unresolved.map((item) => [this.getSessionId(item), item]));
|
|
1308
|
+
const sessions = await db.getSessionsByIdsAsync([...byId.keys()]);
|
|
1309
|
+
this.dbStatus = db.getRuntimeStatus();
|
|
1310
|
+
this.adoptDbIfAvailable(db);
|
|
1311
|
+
for (const session of sessions) {
|
|
1312
|
+
const sessionPath = byId.get(session.id);
|
|
1313
|
+
if (!sessionPath)
|
|
1314
|
+
continue;
|
|
1315
|
+
this.sessionMetaCache.set(sessionPath, {
|
|
1316
|
+
title: session.title,
|
|
1317
|
+
timeUpdated: session.time_updated,
|
|
1318
|
+
});
|
|
1319
|
+
results.set(sessionPath, session.title ? truncateTitle(session.title) : null);
|
|
1320
|
+
}
|
|
1321
|
+
}
|
|
1322
|
+
for (const sessionPath of unresolved) {
|
|
1323
|
+
if (results.has(sessionPath))
|
|
1324
|
+
continue;
|
|
1325
|
+
if (isDbSessionPath(sessionPath)) {
|
|
1326
|
+
results.set(sessionPath, null);
|
|
1327
|
+
continue;
|
|
1328
|
+
}
|
|
1329
|
+
try {
|
|
1330
|
+
const session = JSON.parse(await fs.promises.readFile(sessionPath, 'utf8'));
|
|
1331
|
+
results.set(sessionPath, session.title ? truncateTitle(session.title) : null);
|
|
1332
|
+
}
|
|
1333
|
+
catch {
|
|
1334
|
+
results.set(sessionPath, null);
|
|
1335
|
+
}
|
|
1336
|
+
await new Promise((resolve) => setImmediate(resolve));
|
|
1337
|
+
}
|
|
1338
|
+
this.recordDatabaseStatus('extractSessionLabelsAsync', true);
|
|
1339
|
+
return results;
|
|
1039
1340
|
}
|
|
1040
1341
|
extractSessionLabelFromFiles(sessionPath, sessionId) {
|
|
1041
1342
|
if (isDbSessionPath(sessionPath))
|
|
@@ -1343,6 +1644,9 @@ class OpenCodeProvider {
|
|
|
1343
1644
|
getProjectsBaseDir() {
|
|
1344
1645
|
return path.join(getStorageDir(), 'session');
|
|
1345
1646
|
}
|
|
1647
|
+
getWatchRoots() {
|
|
1648
|
+
return [getOpenCodeDataDir(), this.getProjectsBaseDir()];
|
|
1649
|
+
}
|
|
1346
1650
|
// --- Stats ---
|
|
1347
1651
|
readSessionStats(sessionPath) {
|
|
1348
1652
|
const sessionId = path.basename(sessionPath, '.json');
|
|
@@ -1453,6 +1757,37 @@ class OpenCodeProvider {
|
|
|
1453
1757
|
}
|
|
1454
1758
|
return null;
|
|
1455
1759
|
}
|
|
1760
|
+
async getSessionMetadataAsync(sessionPath) {
|
|
1761
|
+
if (!isDbSessionPath(sessionPath)) {
|
|
1762
|
+
try {
|
|
1763
|
+
return { mtime: (await fs.promises.stat(sessionPath)).mtime };
|
|
1764
|
+
}
|
|
1765
|
+
catch {
|
|
1766
|
+
return null;
|
|
1767
|
+
}
|
|
1768
|
+
}
|
|
1769
|
+
const cached = this.sessionMetaCache.get(sessionPath);
|
|
1770
|
+
try {
|
|
1771
|
+
const db = await this.ensureDbAsync();
|
|
1772
|
+
const sessionId = this.getSessionId(sessionPath);
|
|
1773
|
+
const session = (await db.getSessionsByIdsAsync([sessionId]))[0];
|
|
1774
|
+
this.dbStatus = db.getRuntimeStatus();
|
|
1775
|
+
this.adoptDbIfAvailable(db);
|
|
1776
|
+
if (session) {
|
|
1777
|
+
this.sessionMetaCache.set(sessionPath, {
|
|
1778
|
+
title: session.title,
|
|
1779
|
+
timeUpdated: session.time_updated,
|
|
1780
|
+
});
|
|
1781
|
+
this.recordDatabaseStatus('getSessionMetadataAsync', true);
|
|
1782
|
+
return { mtime: new Date(session.time_updated) };
|
|
1783
|
+
}
|
|
1784
|
+
this.recordDatabaseStatus('getSessionMetadataAsync', cached !== undefined);
|
|
1785
|
+
}
|
|
1786
|
+
catch {
|
|
1787
|
+
this.recordDatabaseStatus('getSessionMetadataAsync', cached !== undefined);
|
|
1788
|
+
}
|
|
1789
|
+
return cached ? { mtime: new Date(cached.timeUpdated) } : null;
|
|
1790
|
+
}
|
|
1456
1791
|
/**
|
|
1457
1792
|
* Returns the latest assistant message's token snapshot for context window sizing.
|
|
1458
1793
|
* Cumulative session totals are derived from readNew() events processed by SessionMonitor.
|
|
@@ -1602,6 +1937,21 @@ class OpenCodeProvider {
|
|
|
1602
1937
|
this.sessionMetaCache.clear();
|
|
1603
1938
|
this.dynamicContextWindowLimit = null;
|
|
1604
1939
|
}
|
|
1940
|
+
recordDatabaseStatus(operation, usable) {
|
|
1941
|
+
const status = this.db?.getRuntimeStatus() ?? this.dbStatus;
|
|
1942
|
+
const diagnostic = (0, diagnostics_1.diagnosticFromRuntimeStatus)(this.id, 'query', status);
|
|
1943
|
+
if (!diagnostic || status.kind === 'db_missing') {
|
|
1944
|
+
// A genuinely absent OpenCode DB is expected for legacy file storage.
|
|
1945
|
+
this.diagnostics.available(operation);
|
|
1946
|
+
return;
|
|
1947
|
+
}
|
|
1948
|
+
this.diagnostics.degraded(operation, status, {
|
|
1949
|
+
kind: diagnostic.kind,
|
|
1950
|
+
severity: diagnostic.severity,
|
|
1951
|
+
phase: diagnostic.phase,
|
|
1952
|
+
message: diagnostic.message,
|
|
1953
|
+
}, usable);
|
|
1954
|
+
}
|
|
1605
1955
|
// --- z.ai (GLM Coding Plan) compatibility quota derivation ---
|
|
1606
1956
|
/**
|
|
1607
1957
|
* Derives a z.ai coding-plan `QuotaState` from OpenCode assistant turns
|
|
@@ -17,12 +17,22 @@ export declare class OpenCodeDatabase {
|
|
|
17
17
|
open(): boolean;
|
|
18
18
|
close(): void;
|
|
19
19
|
private query;
|
|
20
|
+
private queryAsync;
|
|
20
21
|
private queryOne;
|
|
21
22
|
findProjectByWorktree(workspacePath: string): DbProject | null;
|
|
22
23
|
findProjectBySessionDirectory(workspacePath: string): DbProject | null;
|
|
23
24
|
getAllProjects(): DbProject[];
|
|
24
25
|
hasProject(projectId: string): boolean;
|
|
25
26
|
getSessionsForProject(projectId: string): DbSession[];
|
|
27
|
+
/** Get every root session in one query for global preview enumeration. */
|
|
28
|
+
getAllSessions(): DbSession[];
|
|
29
|
+
/** Enumerate root sessions with one non-blocking sqlite3 subprocess. */
|
|
30
|
+
getAllSessionsAsync(): Promise<DbSession[]>;
|
|
31
|
+
/**
|
|
32
|
+
* Resolve many sessions with non-blocking sqlite3 subprocesses — one per
|
|
33
|
+
* chunk of ids, so the inlined query stays clear of argv length limits.
|
|
34
|
+
*/
|
|
35
|
+
getSessionsByIdsAsync(sessionIds: readonly string[]): Promise<DbSession[]>;
|
|
26
36
|
/** Get the most recently updated session for a project (excludes subagent child sessions). */
|
|
27
37
|
getMostRecentSession(projectId: string): DbSession | null;
|
|
28
38
|
/** Get child sessions (subagents) whose parent_id matches the given session ID. */
|
|
@@ -41,6 +41,11 @@ exports.OpenCodeDatabase = void 0;
|
|
|
41
41
|
const fs = __importStar(require("fs"));
|
|
42
42
|
const path = __importStar(require("path"));
|
|
43
43
|
const child_process_1 = require("child_process");
|
|
44
|
+
/**
|
|
45
|
+
* Ids per inlined `IN (…)` query. Values are passed inside a single argv
|
|
46
|
+
* element to sqlite3, so an unbounded list would hit E2BIG on large stores.
|
|
47
|
+
*/
|
|
48
|
+
const SQL_ID_CHUNK_SIZE = 400;
|
|
44
49
|
class OpenCodeDatabase {
|
|
45
50
|
dbPath;
|
|
46
51
|
runtimeStatus = null;
|
|
@@ -127,6 +132,23 @@ class OpenCodeDatabase {
|
|
|
127
132
|
return [];
|
|
128
133
|
}
|
|
129
134
|
}
|
|
135
|
+
async queryAsync(sql, params = []) {
|
|
136
|
+
if (!this.isAvailable()) {
|
|
137
|
+
this.runtimeStatus = { available: false, kind: 'db_missing' };
|
|
138
|
+
return [];
|
|
139
|
+
}
|
|
140
|
+
const query = bindSqlParams(sql, params);
|
|
141
|
+
try {
|
|
142
|
+
const result = await execSqlite(this.dbPath, query);
|
|
143
|
+
this.runtimeStatus = { available: true, kind: 'available' };
|
|
144
|
+
const trimmed = result.trim();
|
|
145
|
+
return trimmed ? JSON.parse(trimmed) : [];
|
|
146
|
+
}
|
|
147
|
+
catch (error) {
|
|
148
|
+
this.runtimeStatus = toRuntimeStatus(error, 'query_failed');
|
|
149
|
+
return [];
|
|
150
|
+
}
|
|
151
|
+
}
|
|
130
152
|
queryOne(sql, params = []) {
|
|
131
153
|
const results = this.query(sql, params);
|
|
132
154
|
return results[0] ?? null;
|
|
@@ -181,6 +203,27 @@ class OpenCodeDatabase {
|
|
|
181
203
|
getSessionsForProject(projectId) {
|
|
182
204
|
return this.query('SELECT id, project_id, title, directory, time_created, time_updated FROM session WHERE project_id = ? AND parent_id IS NULL ORDER BY time_updated DESC', [projectId]);
|
|
183
205
|
}
|
|
206
|
+
/** Get every root session in one query for global preview enumeration. */
|
|
207
|
+
getAllSessions() {
|
|
208
|
+
return this.query('SELECT id, project_id, title, directory, time_created, time_updated FROM session WHERE parent_id IS NULL ORDER BY time_updated DESC');
|
|
209
|
+
}
|
|
210
|
+
/** Enumerate root sessions with one non-blocking sqlite3 subprocess. */
|
|
211
|
+
getAllSessionsAsync() {
|
|
212
|
+
return this.queryAsync('SELECT id, project_id, title, directory, time_created, time_updated FROM session WHERE parent_id IS NULL ORDER BY time_updated DESC');
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* Resolve many sessions with non-blocking sqlite3 subprocesses — one per
|
|
216
|
+
* chunk of ids, so the inlined query stays clear of argv length limits.
|
|
217
|
+
*/
|
|
218
|
+
async getSessionsByIdsAsync(sessionIds) {
|
|
219
|
+
const results = [];
|
|
220
|
+
for (let i = 0; i < sessionIds.length; i += SQL_ID_CHUNK_SIZE) {
|
|
221
|
+
const chunk = sessionIds.slice(i, i + SQL_ID_CHUNK_SIZE);
|
|
222
|
+
const placeholders = chunk.map(() => '?').join(', ');
|
|
223
|
+
results.push(...(await this.queryAsync(`SELECT id, project_id, title, directory, time_created, time_updated FROM session WHERE id IN (${placeholders})`, [...chunk])));
|
|
224
|
+
}
|
|
225
|
+
return results;
|
|
226
|
+
}
|
|
184
227
|
/** Get the most recently updated session for a project (excludes subagent child sessions). */
|
|
185
228
|
getMostRecentSession(projectId) {
|
|
186
229
|
return this.queryOne('SELECT id, project_id, title, directory, time_created, time_updated FROM session WHERE project_id = ? AND parent_id IS NULL ORDER BY time_updated DESC LIMIT 1', [projectId]);
|
|
@@ -315,6 +358,32 @@ class OpenCodeDatabase {
|
|
|
315
358
|
}
|
|
316
359
|
}
|
|
317
360
|
exports.OpenCodeDatabase = OpenCodeDatabase;
|
|
361
|
+
function bindSqlParams(sql, params) {
|
|
362
|
+
let paramIndex = 0;
|
|
363
|
+
return sql.replace(/\?/g, () => {
|
|
364
|
+
if (paramIndex >= params.length)
|
|
365
|
+
return '?';
|
|
366
|
+
const param = params[paramIndex++];
|
|
367
|
+
if (typeof param === 'number')
|
|
368
|
+
return Number.isFinite(param) ? String(param) : '0';
|
|
369
|
+
return `'${String(param).replace(/'/g, "''")}'`;
|
|
370
|
+
});
|
|
371
|
+
}
|
|
372
|
+
function execSqlite(dbPath, query) {
|
|
373
|
+
return new Promise((resolve, reject) => {
|
|
374
|
+
(0, child_process_1.execFile)('sqlite3', ['-json', '-readonly', dbPath, query], {
|
|
375
|
+
encoding: 'utf8',
|
|
376
|
+
timeout: 4_000,
|
|
377
|
+
killSignal: 'SIGKILL',
|
|
378
|
+
maxBuffer: 50 * 1024 * 1024,
|
|
379
|
+
}, (error, stdout) => {
|
|
380
|
+
if (error)
|
|
381
|
+
reject(error);
|
|
382
|
+
else
|
|
383
|
+
resolve(stdout);
|
|
384
|
+
});
|
|
385
|
+
});
|
|
386
|
+
}
|
|
318
387
|
function normalizePath(input) {
|
|
319
388
|
try {
|
|
320
389
|
return fs.realpathSync(input);
|