sidekick-shared 0.26.0 → 0.26.2
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 +23 -0
- package/README.md +28 -6
- package/dist/doctor.d.ts +5 -0
- package/dist/doctor.js +69 -18
- package/dist/index.d.ts +1 -1
- package/dist/modelContext.js +8 -3
- package/dist/modelInfo.js +45 -27
- package/dist/providers/claudeCode.js +11 -2
- package/dist/providers/codex.js +17 -7
- package/dist/quotaPresentation.js +17 -5
- package/dist/quotaResolver.d.ts +6 -4
- package/dist/quotaResolver.js +36 -15
- package/dist/search/sessionSearch.d.ts +9 -5
- package/dist/search/sessionSearch.js +61 -90
- package/dist/watchers/factory.js +2 -8
- package/dist/watchers/jsonlTail.js +26 -2
- package/dist/watchers/jsonlWatcher.d.ts +5 -11
- package/dist/watchers/jsonlWatcher.js +21 -150
- package/dist/watchers/providerReaderWatcher.js +5 -3
- package/dist/watchers/sqliteWatcher.js +1 -0
- package/dist/watchers/types.d.ts +2 -0
- package/package.json +1 -1
package/dist/quotaResolver.d.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* `sidekick quota`, `quota --all`, the MCP facts server, and both dashboards
|
|
5
5
|
* used to pick between the live API, session logs, and the persisted snapshot
|
|
6
|
-
* in four different orders. `resolveQuota()`
|
|
6
|
+
* in four different orders. `resolveQuota()` defaults to this precedence:
|
|
7
7
|
*
|
|
8
8
|
* 1. a fresh persisted snapshot (younger than `QUOTA_FRESH_MAX_AGE_MS`, whatever
|
|
9
9
|
* its origin: the official status-line sample, a session log, or an earlier
|
|
@@ -14,7 +14,9 @@
|
|
|
14
14
|
* 4. an aging or stale snapshot, labelled as such, with the API failure attached;
|
|
15
15
|
* 5. an unavailable state describing why nothing could be resolved.
|
|
16
16
|
*
|
|
17
|
-
* `preferFresh: false`
|
|
17
|
+
* `preferFresh: false` asks the API first when allowed (the `--refresh` flag
|
|
18
|
+
* and one-shot Codex queries). On failure, the newer session or persisted
|
|
19
|
+
* sample wins, with ties going to the persisted sample. Every result carries
|
|
18
20
|
* `source`, `capturedSource`, `freshness`, `ageMs`, and a `resolution` tag.
|
|
19
21
|
*/
|
|
20
22
|
import type { ResolvedActiveAccount, SavedAccountProfile } from './accountRegistry';
|
|
@@ -38,9 +40,9 @@ export interface ResolveQuotaOptions<T extends QuotaResolveProviderId = QuotaRes
|
|
|
38
40
|
accountId?: string;
|
|
39
41
|
/** Workspace whose Codex rollouts are searched first for session-derived rate limits. */
|
|
40
42
|
workspacePath?: string;
|
|
41
|
-
/**
|
|
43
|
+
/** Prefer local samples (default true). False asks the API before any local sample when allowed. */
|
|
42
44
|
preferFresh?: boolean;
|
|
43
|
-
/** Allow
|
|
45
|
+
/** Allow provider API calls (default true), including when preferFresh is false. */
|
|
44
46
|
allowApi?: boolean;
|
|
45
47
|
/** Re-point a stale saved-account pointer while resolving the account (default true). */
|
|
46
48
|
selfHeal?: boolean;
|
package/dist/quotaResolver.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
*
|
|
5
5
|
* `sidekick quota`, `quota --all`, the MCP facts server, and both dashboards
|
|
6
6
|
* used to pick between the live API, session logs, and the persisted snapshot
|
|
7
|
-
* in four different orders. `resolveQuota()`
|
|
7
|
+
* in four different orders. `resolveQuota()` defaults to this precedence:
|
|
8
8
|
*
|
|
9
9
|
* 1. a fresh persisted snapshot (younger than `QUOTA_FRESH_MAX_AGE_MS`, whatever
|
|
10
10
|
* its origin: the official status-line sample, a session log, or an earlier
|
|
@@ -15,7 +15,9 @@
|
|
|
15
15
|
* 4. an aging or stale snapshot, labelled as such, with the API failure attached;
|
|
16
16
|
* 5. an unavailable state describing why nothing could be resolved.
|
|
17
17
|
*
|
|
18
|
-
* `preferFresh: false`
|
|
18
|
+
* `preferFresh: false` asks the API first when allowed (the `--refresh` flag
|
|
19
|
+
* and one-shot Codex queries). On failure, the newer session or persisted
|
|
20
|
+
* sample wins, with ties going to the persisted sample. Every result carries
|
|
19
21
|
* `source`, `capturedSource`, `freshness`, `ageMs`, and a `resolution` tag.
|
|
20
22
|
*/
|
|
21
23
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
@@ -146,6 +148,10 @@ function snapshotResolution(state) {
|
|
|
146
148
|
return 'snapshot-aging';
|
|
147
149
|
return 'snapshot-stale';
|
|
148
150
|
}
|
|
151
|
+
function captureTimeMs(state) {
|
|
152
|
+
const capturedMs = state.capturedAt ? Date.parse(state.capturedAt) : NaN;
|
|
153
|
+
return Number.isFinite(capturedMs) ? capturedMs : Number.NEGATIVE_INFINITY;
|
|
154
|
+
}
|
|
149
155
|
/**
|
|
150
156
|
* Resolve the current quota for one provider with the shared precedence
|
|
151
157
|
* described at the top of this file.
|
|
@@ -160,6 +166,7 @@ async function resolveQuota(options) {
|
|
|
160
166
|
const capturedAt = now.toISOString();
|
|
161
167
|
const preferFresh = options.preferFresh ?? true;
|
|
162
168
|
const allowApi = options.allowApi ?? true;
|
|
169
|
+
const apiFirst = !preferFresh && allowApi;
|
|
163
170
|
const readSnapshot = options.readSnapshot ?? quotaSnapshots_1.readQuotaSnapshot;
|
|
164
171
|
const writeSnapshot = options.writeSnapshot ?? quotaSnapshots_1.writeQuotaSnapshot;
|
|
165
172
|
const identity = resolveIdentity(options);
|
|
@@ -178,19 +185,27 @@ async function resolveQuota(options) {
|
|
|
178
185
|
if (preferFresh && snapshot && snapshot.freshness === 'fresh') {
|
|
179
186
|
return finish(snapshot, 'snapshot-fresh');
|
|
180
187
|
}
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
|
|
188
|
+
const readLocalCodex = () => (0, codexQuota_1.resolveCodexQuotaFromLocalSources)({
|
|
189
|
+
workspacePath: options.workspacePath,
|
|
190
|
+
provider: options.codexProvider,
|
|
191
|
+
activeAccount: identity.codexAccount,
|
|
192
|
+
codexHome: options.codexHome,
|
|
193
|
+
// Only rollout hits count as "session-derived"; the snapshot fallback is
|
|
194
|
+
// handled below so it can be labelled by age.
|
|
195
|
+
readSnapshot: () => null,
|
|
196
|
+
// Inspect candidates without overwriting the persisted fallback. Only
|
|
197
|
+
// the selected session sample is written, using the resolved account key.
|
|
198
|
+
writeSnapshot: () => { },
|
|
199
|
+
});
|
|
200
|
+
const finishLocal = (local) => {
|
|
201
|
+
if (identity.accountId)
|
|
202
|
+
writeSnapshot('codex', identity.accountId, local);
|
|
203
|
+
return finish(local, 'session');
|
|
204
|
+
};
|
|
205
|
+
if (providerId === 'codex' && !apiFirst) {
|
|
206
|
+
const local = readLocalCodex();
|
|
207
|
+
if (local?.available)
|
|
208
|
+
return finishLocal(local);
|
|
194
209
|
}
|
|
195
210
|
let apiState = null;
|
|
196
211
|
if (allowApi) {
|
|
@@ -202,6 +217,12 @@ async function resolveQuota(options) {
|
|
|
202
217
|
return finish(apiState, 'api');
|
|
203
218
|
}
|
|
204
219
|
}
|
|
220
|
+
if (providerId === 'codex' && apiFirst) {
|
|
221
|
+
const local = readLocalCodex();
|
|
222
|
+
if (local?.available && (!snapshot || captureTimeMs(local) > captureTimeMs(snapshot))) {
|
|
223
|
+
return { ...finishLocal(local), failure: (0, quotaPresentation_1.describeQuotaFailure)(apiState) };
|
|
224
|
+
}
|
|
225
|
+
}
|
|
205
226
|
if (snapshot) {
|
|
206
227
|
const fallback = finish(snapshot, snapshotResolution(snapshot));
|
|
207
228
|
return apiState ? { ...fallback, failure: (0, quotaPresentation_1.describeQuotaFailure)(apiState) } : fallback;
|
|
@@ -1,6 +1,4 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Provider-aware cross-session search.
|
|
3
|
-
*/
|
|
1
|
+
/** Provider-aware cross-session search. */
|
|
4
2
|
import type { SessionProviderBase, ProviderId } from '../providers/types';
|
|
5
3
|
export interface SearchResult {
|
|
6
4
|
providerId: ProviderId;
|
|
@@ -10,7 +8,13 @@ export interface SearchResult {
|
|
|
10
8
|
eventType: string;
|
|
11
9
|
timestamp: string;
|
|
12
10
|
}
|
|
13
|
-
export
|
|
11
|
+
export interface SessionSearchOptions {
|
|
12
|
+
/** Workspace path, resolved canonically. Takes precedence over projectSlug. */
|
|
13
|
+
projectPath?: string;
|
|
14
|
+
/** Legacy provider-encoded project identifier. */
|
|
14
15
|
projectSlug?: string;
|
|
15
16
|
maxResults?: number;
|
|
16
|
-
|
|
17
|
+
/** Stop obsolete interactive searches between session reads. */
|
|
18
|
+
signal?: AbortSignal;
|
|
19
|
+
}
|
|
20
|
+
export declare function searchSessions(provider: SessionProviderBase, query: string, options?: SessionSearchOptions): Promise<SearchResult[]>;
|
|
@@ -1,98 +1,69 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
/**
|
|
3
|
-
* Provider-aware cross-session search.
|
|
4
|
-
*/
|
|
5
|
-
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
6
|
-
if (k2 === undefined) k2 = k;
|
|
7
|
-
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
8
|
-
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
9
|
-
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
10
|
-
}
|
|
11
|
-
Object.defineProperty(o, k2, desc);
|
|
12
|
-
}) : (function(o, m, k, k2) {
|
|
13
|
-
if (k2 === undefined) k2 = k;
|
|
14
|
-
o[k2] = m[k];
|
|
15
|
-
}));
|
|
16
|
-
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
17
|
-
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
18
|
-
}) : function(o, v) {
|
|
19
|
-
o["default"] = v;
|
|
20
|
-
});
|
|
21
|
-
var __importStar = (this && this.__importStar) || (function () {
|
|
22
|
-
var ownKeys = function(o) {
|
|
23
|
-
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
24
|
-
var ar = [];
|
|
25
|
-
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
26
|
-
return ar;
|
|
27
|
-
};
|
|
28
|
-
return ownKeys(o);
|
|
29
|
-
};
|
|
30
|
-
return function (mod) {
|
|
31
|
-
if (mod && mod.__esModule) return mod;
|
|
32
|
-
var result = {};
|
|
33
|
-
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
34
|
-
__setModuleDefault(result, mod);
|
|
35
|
-
return result;
|
|
36
|
-
};
|
|
37
|
-
})();
|
|
2
|
+
/** Provider-aware cross-session search. */
|
|
38
3
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
4
|
exports.searchSessions = searchSessions;
|
|
40
|
-
const
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
5
|
+
const paths_1 = require("../paths");
|
|
6
|
+
async function enumerateSessions(provider, options) {
|
|
7
|
+
const workspace = options.projectPath
|
|
8
|
+
? (0, paths_1.resolveProjectIdentity)(options.projectPath).resolvedCwd
|
|
9
|
+
: undefined;
|
|
10
|
+
if (!options.projectSlug || workspace) {
|
|
11
|
+
if (provider.listSessionFilesAsync)
|
|
12
|
+
return provider.listSessionFilesAsync(workspace);
|
|
13
|
+
if (workspace) {
|
|
14
|
+
return provider
|
|
15
|
+
.findAllSessions(workspace)
|
|
16
|
+
.map((path) => ({ path, mtime: new Date(0), workspacePath: workspace }));
|
|
17
|
+
}
|
|
18
|
+
if (provider.listAllSessionFiles)
|
|
19
|
+
return provider.listAllSessionFiles();
|
|
20
|
+
}
|
|
21
|
+
// Older/custom providers can expose virtual project directories. Let the
|
|
22
|
+
// provider enumerate them instead of assuming a particular on-disk layout.
|
|
23
|
+
const files = [];
|
|
24
|
+
for (const folder of provider.getAllProjectFolders()) {
|
|
25
|
+
if (options.signal?.aborted)
|
|
26
|
+
break;
|
|
27
|
+
if (options.projectSlug && folder.encodedName !== options.projectSlug)
|
|
28
|
+
continue;
|
|
29
|
+
const paths = provider.id === 'codex'
|
|
30
|
+
? provider.findAllSessions(folder.name)
|
|
31
|
+
: provider.findSessionsInDirectory(folder.dir);
|
|
32
|
+
files.push(...paths.map((path) => ({ path, mtime: folder.lastModified, workspacePath: folder.name })));
|
|
33
|
+
}
|
|
34
|
+
return files;
|
|
35
|
+
}
|
|
36
|
+
async function searchSessions(provider, query, options = {}) {
|
|
37
|
+
const maxResults = options.maxResults ?? 50;
|
|
44
38
|
const results = [];
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
catch {
|
|
69
|
-
/* skip */
|
|
70
|
-
}
|
|
71
|
-
// If no files found from dir scan, try findAllSessions with the folder name
|
|
72
|
-
if (sessionFiles.length === 0) {
|
|
73
|
-
// Let canonical directory discovery handle synthetic DB-backed paths.
|
|
74
|
-
sessionFiles = provider.findSessionsInDirectory(folder.dir);
|
|
75
|
-
}
|
|
76
|
-
for (const sessionPath of sessionFiles) {
|
|
77
|
-
if (results.length >= maxResults)
|
|
78
|
-
break;
|
|
79
|
-
const remaining = maxResults - results.length;
|
|
80
|
-
const hits = provider.searchInSession(sessionPath, query, remaining);
|
|
81
|
-
for (const hit of hits) {
|
|
82
|
-
results.push({
|
|
83
|
-
providerId: provider.id,
|
|
84
|
-
projectPath: hit.projectPath || folder.name,
|
|
85
|
-
sessionPath: hit.sessionPath,
|
|
86
|
-
snippet: hit.line,
|
|
87
|
-
eventType: hit.eventType,
|
|
88
|
-
timestamp: hit.timestamp,
|
|
89
|
-
});
|
|
90
|
-
}
|
|
39
|
+
if (maxResults <= 0 || !query.trim() || options.signal?.aborted)
|
|
40
|
+
return results;
|
|
41
|
+
const files = await enumerateSessions(provider, options);
|
|
42
|
+
const seen = new Set();
|
|
43
|
+
files.sort((a, b) => b.mtime.getTime() - a.mtime.getTime());
|
|
44
|
+
for (const file of files) {
|
|
45
|
+
if (results.length >= maxResults || options.signal?.aborted)
|
|
46
|
+
break;
|
|
47
|
+
if (seen.has(file.path))
|
|
48
|
+
continue;
|
|
49
|
+
seen.add(file.path);
|
|
50
|
+
try {
|
|
51
|
+
const hits = provider.searchInSession(file.path, query, maxResults - results.length);
|
|
52
|
+
for (const hit of hits.slice(0, maxResults - results.length)) {
|
|
53
|
+
results.push({
|
|
54
|
+
providerId: provider.id,
|
|
55
|
+
projectPath: hit.projectPath || file.workspacePath || '',
|
|
56
|
+
sessionPath: hit.sessionPath,
|
|
57
|
+
snippet: hit.line,
|
|
58
|
+
eventType: hit.eventType,
|
|
59
|
+
timestamp: hit.timestamp,
|
|
60
|
+
});
|
|
91
61
|
}
|
|
92
62
|
}
|
|
63
|
+
catch {
|
|
64
|
+
// An unreadable session must not hide matches in the remaining sessions.
|
|
65
|
+
}
|
|
66
|
+
await new Promise((resolve) => setImmediate(resolve));
|
|
93
67
|
}
|
|
94
|
-
|
|
95
|
-
/* skip */
|
|
96
|
-
}
|
|
97
|
-
return results.slice(0, maxResults);
|
|
68
|
+
return results;
|
|
98
69
|
}
|
package/dist/watchers/factory.js
CHANGED
|
@@ -38,17 +38,11 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
38
38
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
39
|
exports.resolveSessionPath = resolveSessionPath;
|
|
40
40
|
exports.createWatcher = createWatcher;
|
|
41
|
-
const os = __importStar(require("os"));
|
|
42
41
|
const path = __importStar(require("path"));
|
|
42
|
+
const openCode_1 = require("../providers/openCode");
|
|
43
43
|
const jsonlWatcher_1 = require("./jsonlWatcher");
|
|
44
44
|
const providerReaderWatcher_1 = require("./providerReaderWatcher");
|
|
45
45
|
const sqliteWatcher_1 = require("./sqliteWatcher");
|
|
46
|
-
function getOpenCodeDataDir() {
|
|
47
|
-
const xdg = process.env.XDG_DATA_HOME;
|
|
48
|
-
if (xdg)
|
|
49
|
-
return path.join(xdg, 'opencode');
|
|
50
|
-
return path.join(os.homedir(), '.local', 'share', 'opencode');
|
|
51
|
-
}
|
|
52
46
|
/**
|
|
53
47
|
* Resolve the session a command should act on: an exact or unique-prefix
|
|
54
48
|
* match of `sessionId` among the workspace's sessions, or the most recent
|
|
@@ -94,7 +88,7 @@ function createWatcherForProvider(provider, sessionPath, callbacks) {
|
|
|
94
88
|
case 'codex':
|
|
95
89
|
return new providerReaderWatcher_1.ProviderReaderSessionWatcher(provider, sessionPath, callbacks);
|
|
96
90
|
case 'opencode': {
|
|
97
|
-
const dataDir = getOpenCodeDataDir();
|
|
91
|
+
const dataDir = (0, openCode_1.getOpenCodeDataDir)();
|
|
98
92
|
const dbPath = path.join(dataDir, 'opencode.db');
|
|
99
93
|
// Session ID is the basename without extension
|
|
100
94
|
const sid = path.basename(sessionPath, '.json');
|
|
@@ -42,6 +42,26 @@ const fs = __importStar(require("fs"));
|
|
|
42
42
|
const jsonl_1 = require("../parsers/jsonl");
|
|
43
43
|
const DEFAULT_DEBOUNCE_MS = 100;
|
|
44
44
|
const DEFAULT_CATCHUP_INTERVAL_MS = 30_000;
|
|
45
|
+
/** Skip old complete events while retaining an in-progress final line. */
|
|
46
|
+
function lastCompleteLineOffset(filePath) {
|
|
47
|
+
const fd = fs.openSync(filePath, 'r');
|
|
48
|
+
try {
|
|
49
|
+
let end = fs.fstatSync(fd).size;
|
|
50
|
+
const buffer = Buffer.alloc(64 * 1024);
|
|
51
|
+
while (end > 0) {
|
|
52
|
+
const start = Math.max(0, end - buffer.length);
|
|
53
|
+
const count = fs.readSync(fd, buffer, 0, end - start, start);
|
|
54
|
+
const newline = buffer.subarray(0, count).lastIndexOf(0x0a);
|
|
55
|
+
if (newline >= 0)
|
|
56
|
+
return start + newline + 1;
|
|
57
|
+
end = start;
|
|
58
|
+
}
|
|
59
|
+
return 0;
|
|
60
|
+
}
|
|
61
|
+
finally {
|
|
62
|
+
fs.closeSync(fd);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
45
65
|
function createJsonlTail(options) {
|
|
46
66
|
return new JsonlTailReader(options);
|
|
47
67
|
}
|
|
@@ -79,7 +99,7 @@ class JsonlTailReader {
|
|
|
79
99
|
this.active = true;
|
|
80
100
|
if (this.options.startAtEnd && this.options.startOffset === undefined) {
|
|
81
101
|
try {
|
|
82
|
-
this.readOffset =
|
|
102
|
+
this.readOffset = lastCompleteLineOffset(this.options.path);
|
|
83
103
|
}
|
|
84
104
|
catch {
|
|
85
105
|
this.readOffset = 0;
|
|
@@ -179,7 +199,11 @@ class JsonlTailReader {
|
|
|
179
199
|
this.fsWatcher = fs.watch(this.options.path, { persistent: false }, () => {
|
|
180
200
|
this.debouncedRead();
|
|
181
201
|
});
|
|
182
|
-
this.fsWatcher.on('error', (error) =>
|
|
202
|
+
this.fsWatcher.on('error', (error) => {
|
|
203
|
+
this.fsWatcher?.close();
|
|
204
|
+
this.fsWatcher = null;
|
|
205
|
+
this.options.onError?.(error);
|
|
206
|
+
});
|
|
183
207
|
}
|
|
184
208
|
catch {
|
|
185
209
|
// Polling still covers filesystems where fs.watch is unavailable.
|
|
@@ -5,26 +5,20 @@
|
|
|
5
5
|
import type { ProviderId } from '../providers/types';
|
|
6
6
|
import type { SessionWatcher, SessionWatcherCallbacks } from './types';
|
|
7
7
|
export declare class JsonlSessionWatcher implements SessionWatcher {
|
|
8
|
-
private _isActive;
|
|
9
|
-
private filePosition;
|
|
10
|
-
private fsWatcher;
|
|
11
|
-
private catchupTimer;
|
|
12
|
-
private debounceTimer;
|
|
13
|
-
private codexInPlanMode;
|
|
14
|
-
private readonly parser;
|
|
15
8
|
private readonly providerId;
|
|
16
9
|
private readonly sessionPath;
|
|
17
10
|
private readonly callbacks;
|
|
11
|
+
private tail;
|
|
12
|
+
private startOffset;
|
|
13
|
+
private codexInPlanMode;
|
|
18
14
|
constructor(providerId: ProviderId, sessionPath: string, callbacks: SessionWatcherCallbacks);
|
|
19
15
|
get isActive(): boolean;
|
|
20
|
-
/** Seek to a byte offset
|
|
16
|
+
/** Seek to a committed byte offset before starting replay. */
|
|
21
17
|
seekTo(position: number): void;
|
|
22
|
-
/**
|
|
18
|
+
/** Position after the last complete JSONL line. */
|
|
23
19
|
getPosition(): number;
|
|
24
20
|
start(replay: boolean): void;
|
|
25
21
|
stop(): void;
|
|
26
|
-
private debouncedRead;
|
|
27
|
-
private readNewBytes;
|
|
28
22
|
private handleRawEvent;
|
|
29
23
|
private normalizeCodexEvent;
|
|
30
24
|
}
|
|
@@ -3,47 +3,11 @@
|
|
|
3
3
|
* JSONL session watcher for Claude Code and Codex.
|
|
4
4
|
* Uses byte-offset tracking + fs.watch with debounce + catch-up polling.
|
|
5
5
|
*/
|
|
6
|
-
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
7
|
-
if (k2 === undefined) k2 = k;
|
|
8
|
-
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
9
|
-
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
10
|
-
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
11
|
-
}
|
|
12
|
-
Object.defineProperty(o, k2, desc);
|
|
13
|
-
}) : (function(o, m, k, k2) {
|
|
14
|
-
if (k2 === undefined) k2 = k;
|
|
15
|
-
o[k2] = m[k];
|
|
16
|
-
}));
|
|
17
|
-
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
18
|
-
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
19
|
-
}) : function(o, v) {
|
|
20
|
-
o["default"] = v;
|
|
21
|
-
});
|
|
22
|
-
var __importStar = (this && this.__importStar) || (function () {
|
|
23
|
-
var ownKeys = function(o) {
|
|
24
|
-
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
25
|
-
var ar = [];
|
|
26
|
-
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
27
|
-
return ar;
|
|
28
|
-
};
|
|
29
|
-
return ownKeys(o);
|
|
30
|
-
};
|
|
31
|
-
return function (mod) {
|
|
32
|
-
if (mod && mod.__esModule) return mod;
|
|
33
|
-
var result = {};
|
|
34
|
-
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
35
|
-
__setModuleDefault(result, mod);
|
|
36
|
-
return result;
|
|
37
|
-
};
|
|
38
|
-
})();
|
|
39
6
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
40
7
|
exports.JsonlSessionWatcher = void 0;
|
|
41
|
-
const
|
|
42
|
-
const jsonl_1 = require("../parsers/jsonl");
|
|
8
|
+
const jsonlTail_1 = require("./jsonlTail");
|
|
43
9
|
const codexParser_1 = require("../parsers/codexParser");
|
|
44
10
|
const toolSummary_1 = require("../formatters/toolSummary");
|
|
45
|
-
const DEBOUNCE_MS = 100;
|
|
46
|
-
const CATCHUP_INTERVAL_MS = 30_000;
|
|
47
11
|
// ── Normalizers ──
|
|
48
12
|
function normalizeClaudeCodeEvent(raw) {
|
|
49
13
|
const events = [];
|
|
@@ -220,139 +184,46 @@ function truncate(text, maxLen) {
|
|
|
220
184
|
}
|
|
221
185
|
// ── Watcher ──
|
|
222
186
|
class JsonlSessionWatcher {
|
|
223
|
-
_isActive = false;
|
|
224
|
-
filePosition = 0;
|
|
225
|
-
fsWatcher = null;
|
|
226
|
-
catchupTimer = null;
|
|
227
|
-
debounceTimer = null;
|
|
228
|
-
codexInPlanMode = false;
|
|
229
|
-
parser;
|
|
230
187
|
providerId;
|
|
231
188
|
sessionPath;
|
|
232
189
|
callbacks;
|
|
190
|
+
tail;
|
|
191
|
+
startOffset;
|
|
192
|
+
codexInPlanMode = false;
|
|
233
193
|
constructor(providerId, sessionPath, callbacks) {
|
|
234
194
|
this.providerId = providerId;
|
|
235
195
|
this.sessionPath = sessionPath;
|
|
236
196
|
this.callbacks = callbacks;
|
|
237
|
-
this.parser = new jsonl_1.JsonlParser({
|
|
238
|
-
onEvent: (event) => this.handleRawEvent(event),
|
|
239
|
-
onError: (err) => callbacks.onError?.(err),
|
|
240
|
-
});
|
|
241
197
|
}
|
|
242
198
|
get isActive() {
|
|
243
|
-
return this.
|
|
199
|
+
return this.tail?.isActive ?? false;
|
|
244
200
|
}
|
|
245
|
-
/** Seek to a byte offset
|
|
201
|
+
/** Seek to a committed byte offset before starting replay. */
|
|
246
202
|
seekTo(position) {
|
|
247
|
-
this.
|
|
248
|
-
this.
|
|
203
|
+
this.startOffset = Math.max(0, position);
|
|
204
|
+
this.tail?.seekTo(this.startOffset);
|
|
249
205
|
}
|
|
250
|
-
/**
|
|
206
|
+
/** Position after the last complete JSONL line. */
|
|
251
207
|
getPosition() {
|
|
252
|
-
return this.
|
|
208
|
+
return this.tail?.getOffset() ?? this.startOffset ?? 0;
|
|
253
209
|
}
|
|
254
210
|
start(replay) {
|
|
255
|
-
if (this.
|
|
211
|
+
if (this.isActive)
|
|
256
212
|
return;
|
|
257
|
-
this.
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
this.filePosition = stat.size;
|
|
267
|
-
}
|
|
268
|
-
catch {
|
|
269
|
-
this.filePosition = 0;
|
|
270
|
-
}
|
|
271
|
-
}
|
|
272
|
-
// Watch for changes
|
|
273
|
-
try {
|
|
274
|
-
this.fsWatcher = fs.watch(this.sessionPath, { persistent: false }, () => {
|
|
275
|
-
this.debouncedRead();
|
|
276
|
-
});
|
|
277
|
-
this.fsWatcher.on('error', (error) => {
|
|
278
|
-
// Degrade to catch-up polling without killing the live session.
|
|
279
|
-
this.fsWatcher?.close();
|
|
280
|
-
this.fsWatcher = null;
|
|
281
|
-
this.callbacks.onError?.(error);
|
|
282
|
-
});
|
|
283
|
-
}
|
|
284
|
-
catch {
|
|
285
|
-
// fs.watch not available, rely on polling only
|
|
286
|
-
}
|
|
287
|
-
// Catch-up polling
|
|
288
|
-
this.catchupTimer = setInterval(() => {
|
|
289
|
-
if (this._isActive)
|
|
290
|
-
this.readNewBytes();
|
|
291
|
-
}, CATCHUP_INTERVAL_MS);
|
|
213
|
+
this.tail = (0, jsonlTail_1.createJsonlTail)({
|
|
214
|
+
path: this.sessionPath,
|
|
215
|
+
startOffset: replay ? this.getPosition() : undefined,
|
|
216
|
+
startAtEnd: !replay,
|
|
217
|
+
onEvent: (event) => this.handleRawEvent(event),
|
|
218
|
+
onBatchComplete: () => this.callbacks.onBatchComplete?.(),
|
|
219
|
+
onError: (error) => this.callbacks.onError?.(error),
|
|
220
|
+
});
|
|
221
|
+
this.tail.start();
|
|
292
222
|
}
|
|
293
223
|
stop() {
|
|
294
|
-
|
|
295
|
-
return;
|
|
296
|
-
this._isActive = false;
|
|
297
|
-
if (this.debounceTimer) {
|
|
298
|
-
clearTimeout(this.debounceTimer);
|
|
299
|
-
this.debounceTimer = null;
|
|
300
|
-
}
|
|
301
|
-
if (this.fsWatcher) {
|
|
302
|
-
this.fsWatcher.close();
|
|
303
|
-
this.fsWatcher = null;
|
|
304
|
-
}
|
|
305
|
-
if (this.catchupTimer) {
|
|
306
|
-
clearInterval(this.catchupTimer);
|
|
307
|
-
this.catchupTimer = null;
|
|
308
|
-
}
|
|
309
|
-
this.parser.flush();
|
|
224
|
+
this.tail?.stop();
|
|
310
225
|
this.codexInPlanMode = false;
|
|
311
226
|
}
|
|
312
|
-
debouncedRead() {
|
|
313
|
-
if (this.debounceTimer)
|
|
314
|
-
clearTimeout(this.debounceTimer);
|
|
315
|
-
this.debounceTimer = setTimeout(() => {
|
|
316
|
-
this.readNewBytes();
|
|
317
|
-
}, DEBOUNCE_MS);
|
|
318
|
-
}
|
|
319
|
-
readNewBytes() {
|
|
320
|
-
if (!this._isActive)
|
|
321
|
-
return;
|
|
322
|
-
let fd = null;
|
|
323
|
-
try {
|
|
324
|
-
const stat = fs.statSync(this.sessionPath);
|
|
325
|
-
// Handle file truncation (shouldn't happen with append-only, but be safe)
|
|
326
|
-
if (stat.size < this.filePosition) {
|
|
327
|
-
this.filePosition = 0;
|
|
328
|
-
this.parser.reset();
|
|
329
|
-
}
|
|
330
|
-
if (stat.size <= this.filePosition)
|
|
331
|
-
return;
|
|
332
|
-
fd = fs.openSync(this.sessionPath, 'r');
|
|
333
|
-
const bytesToRead = stat.size - this.filePosition;
|
|
334
|
-
const buffer = Buffer.alloc(bytesToRead);
|
|
335
|
-
const bytesRead = fs.readSync(fd, buffer, 0, bytesToRead, this.filePosition);
|
|
336
|
-
fs.closeSync(fd);
|
|
337
|
-
fd = null;
|
|
338
|
-
if (bytesRead > 0) {
|
|
339
|
-
this.filePosition += bytesRead;
|
|
340
|
-
const chunk = buffer.toString('utf-8', 0, bytesRead);
|
|
341
|
-
this.parser.processChunk(chunk);
|
|
342
|
-
}
|
|
343
|
-
}
|
|
344
|
-
catch (err) {
|
|
345
|
-
if (fd !== null) {
|
|
346
|
-
try {
|
|
347
|
-
fs.closeSync(fd);
|
|
348
|
-
}
|
|
349
|
-
catch {
|
|
350
|
-
/* ignore */
|
|
351
|
-
}
|
|
352
|
-
}
|
|
353
|
-
this.callbacks.onError?.(err instanceof Error ? err : new Error(String(err)));
|
|
354
|
-
}
|
|
355
|
-
}
|
|
356
227
|
handleRawEvent(event) {
|
|
357
228
|
// For codex, raw events don't match RawSessionEvent shape exactly, but
|
|
358
229
|
// JsonlParser parses any JSON object. Cast to Record for codex normalizer.
|
|
@@ -112,12 +112,13 @@ class ProviderReaderSessionWatcher {
|
|
|
112
112
|
clearInterval(this.catchupTimer);
|
|
113
113
|
this.catchupTimer = null;
|
|
114
114
|
}
|
|
115
|
-
|
|
115
|
+
// Keep unfinished input buffered so a later start can resume losslessly.
|
|
116
116
|
}
|
|
117
117
|
seekToEnd() {
|
|
118
118
|
try {
|
|
119
|
-
|
|
120
|
-
|
|
119
|
+
// Consume existing complete records to retain parser context and any
|
|
120
|
+
// unfinished line, without emitting historical events.
|
|
121
|
+
this.reader.readNew();
|
|
121
122
|
}
|
|
122
123
|
catch {
|
|
123
124
|
this.reader.reset();
|
|
@@ -143,6 +144,7 @@ class ProviderReaderSessionWatcher {
|
|
|
143
144
|
this.callbacks.onEvent(followEvent);
|
|
144
145
|
}
|
|
145
146
|
}
|
|
147
|
+
this.callbacks.onBatchComplete?.();
|
|
146
148
|
}
|
|
147
149
|
catch (err) {
|
|
148
150
|
this.callbacks.onError?.(err instanceof Error ? err : new Error(String(err)));
|
|
@@ -312,6 +312,7 @@ class SqliteSessionWatcher {
|
|
|
312
312
|
if (part.time_updated > this.lastPartTime)
|
|
313
313
|
this.lastPartTime = part.time_updated;
|
|
314
314
|
}
|
|
315
|
+
this.callbacks.onBatchComplete?.();
|
|
315
316
|
}
|
|
316
317
|
catch (err) {
|
|
317
318
|
this.callbacks.onError?.(err instanceof Error ? err : new Error(String(err)));
|