sidekick-shared 0.23.0 → 0.24.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/README.md +139 -57
- package/dist/accountManager.js +10 -0
- package/dist/aggregation/EventAggregator.d.ts +8 -1
- package/dist/aggregation/EventAggregator.js +82 -136
- package/dist/aggregation/snapshot.d.ts +1 -1
- package/dist/aggregation/snapshot.js +2 -1
- package/dist/aggregation/types.d.ts +5 -0
- package/dist/autoSwitch.d.ts +1 -1
- package/dist/autoSwitch.js +14 -3
- package/dist/browser.d.ts +6 -0
- package/dist/browser.js +10 -1
- package/dist/codexQuota.d.ts +2 -0
- package/dist/codexQuota.js +27 -0
- package/dist/codexQuotaWatcher.d.ts +7 -0
- package/dist/codexQuotaWatcher.js +37 -19
- package/dist/context/sessionContext.js +43 -15
- package/dist/credentialIO.js +19 -5
- package/dist/errorHistory.d.ts +1 -0
- package/dist/errorHistory.js +3 -2
- package/dist/extractors/tokenUsage.js +1 -1
- package/dist/extractors/toolCall.js +2 -2
- package/dist/index.d.ts +14 -4
- package/dist/index.js +25 -7
- package/dist/modelInfo.d.ts +17 -7
- package/dist/modelInfo.js +11 -2
- package/dist/multiProviderQuotaService.js +13 -2
- package/dist/node.d.ts +4 -0
- package/dist/node.js +8 -1
- package/dist/observedSessionCollector.d.ts +68 -0
- package/dist/observedSessionCollector.js +178 -0
- package/dist/parsers/codexParser.js +24 -6
- package/dist/parsers/openCodeParser.js +22 -1
- package/dist/parsers/sessionActivityDetector.js +2 -0
- package/dist/parsers/sessionPathResolver.js +19 -16
- package/dist/parsers/subagentScanner.d.ts +1 -1
- package/dist/parsers/subagentScanner.js +4 -5
- package/dist/parsers/subagentTraceParser.js +15 -12
- package/dist/providerStatus.js +13 -2
- package/dist/providers/claudeCode.js +63 -5
- package/dist/providers/codex.js +56 -17
- package/dist/providers/detect.js +32 -3
- package/dist/providers/openCodeDatabase.js +12 -3
- package/dist/quota.js +8 -0
- package/dist/quotaPoller.js +17 -6
- package/dist/quotaSnapshots.js +80 -26
- package/dist/readers/decisions.js +3 -1
- package/dist/readers/handoff.js +1 -1
- package/dist/readers/notes.js +9 -4
- package/dist/readers/plans.js +2 -4
- package/dist/readers/tasks.js +3 -1
- package/dist/report/htmlHelpers.js +6 -1
- package/dist/report/openBrowser.js +3 -3
- package/dist/report/transcriptParser.d.ts +3 -11
- package/dist/report/transcriptParser.js +69 -154
- package/dist/schemas/index.d.ts +7 -1
- package/dist/schemas/index.js +18 -2
- package/dist/schemas/observedSessionV1.d.ts +32 -1
- package/dist/schemas/observedSessionV1.js +11 -1
- package/dist/schemas/sessionEvent.d.ts +353 -12
- package/dist/schemas/sessionEvent.js +76 -14
- package/dist/schemas/tokenEstimation.d.ts +14 -0
- package/dist/schemas/tokenEstimation.js +10 -0
- package/dist/schemas/transcript.d.ts +24 -0
- package/dist/schemas/transcript.js +81 -0
- package/dist/schemas/usageNormalization.d.ts +55 -0
- package/dist/schemas/usageNormalization.js +41 -0
- package/dist/sessionTranscripts.d.ts +16 -0
- package/dist/sessionTranscripts.js +47 -0
- package/dist/statusline/formatter.js +8 -1
- package/dist/terminalSync.js +16 -7
- package/dist/tokenEstimation.d.ts +24 -0
- package/dist/tokenEstimation.js +62 -0
- package/dist/transcript.d.ts +92 -0
- package/dist/transcript.js +240 -0
- package/dist/types/historicalData.d.ts +14 -0
- package/dist/types/historicalData.js +22 -0
- package/dist/types/observedSessionV1.d.ts +3 -1
- package/dist/types/observedSessionV1.js +20 -6
- package/dist/types/sessionEvent.d.ts +66 -7
- package/dist/usageNormalization.d.ts +69 -0
- package/dist/usageNormalization.js +100 -0
- package/dist/watchers/factory.js +14 -3
- package/dist/watchers/jsonlTail.js +34 -14
- package/dist/watchers/jsonlWatcher.js +5 -3
- package/dist/watchers/providerReaderWatcher.js +4 -2
- package/dist/watchers/sqliteWatcher.d.ts +2 -0
- package/dist/watchers/sqliteWatcher.js +27 -14
- package/dist/writers/atomic.js +100 -13
- package/dist/zaiQuotaWatcher.js +6 -0
- package/package.json +1 -1
|
@@ -164,6 +164,8 @@ class SqliteSessionWatcher {
|
|
|
164
164
|
_isActive = false;
|
|
165
165
|
lastMessageTime = 0;
|
|
166
166
|
lastPartTime = 0;
|
|
167
|
+
seenMessageVersions = new Map();
|
|
168
|
+
seenPartVersions = new Map();
|
|
167
169
|
fsWatcher = null;
|
|
168
170
|
walWatcher = null;
|
|
169
171
|
pollTimer = null;
|
|
@@ -185,6 +187,8 @@ class SqliteSessionWatcher {
|
|
|
185
187
|
seekTo(position) {
|
|
186
188
|
this.lastMessageTime = position;
|
|
187
189
|
this.lastPartTime = position;
|
|
190
|
+
this.seenMessageVersions.clear();
|
|
191
|
+
this.seenPartVersions.clear();
|
|
188
192
|
}
|
|
189
193
|
/** Get the current cursor position (max timestamp seen). */
|
|
190
194
|
getPosition() {
|
|
@@ -266,38 +270,47 @@ class SqliteSessionWatcher {
|
|
|
266
270
|
}, DEBOUNCE_MS);
|
|
267
271
|
}
|
|
268
272
|
skipToEnd() {
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
if (
|
|
272
|
-
|
|
273
|
+
this.lastMessageTime = this.db.getLatestMessageTimeUpdated(this.sessionId);
|
|
274
|
+
this.lastPartTime = this.db.getLatestPartTimeUpdated(this.sessionId);
|
|
275
|
+
if (this.lastMessageTime > 0) {
|
|
276
|
+
for (const message of this.db.getMessagesNewerThan(this.sessionId, this.lastMessageTime - 1)) {
|
|
277
|
+
this.seenMessageVersions.set(message.id, `${message.time_updated}:${message.data}`);
|
|
278
|
+
}
|
|
273
279
|
}
|
|
274
|
-
if (
|
|
275
|
-
|
|
280
|
+
if (this.lastPartTime > 0) {
|
|
281
|
+
for (const part of this.db.getPartsNewerThan(this.sessionId, this.lastPartTime - 1)) {
|
|
282
|
+
this.seenPartVersions.set(part.id, `${part.time_updated}:${part.data}`);
|
|
283
|
+
}
|
|
276
284
|
}
|
|
277
285
|
}
|
|
278
286
|
pollNewData() {
|
|
279
287
|
if (!this._isActive)
|
|
280
288
|
return;
|
|
281
289
|
try {
|
|
282
|
-
|
|
283
|
-
|
|
290
|
+
// Query incrementally by update time. Subtracting one millisecond lets us
|
|
291
|
+
// see distinct rows committed at the cursor timestamp; version maps
|
|
292
|
+
// suppress rows whose id/data have already been emitted.
|
|
293
|
+
const messages = this.db.getMessagesNewerThan(this.sessionId, Math.max(-1, this.lastMessageTime - 1));
|
|
294
|
+
const parts = this.db.getPartsNewerThan(this.sessionId, Math.max(-1, this.lastPartTime - 1));
|
|
284
295
|
// Emit new messages
|
|
285
|
-
const newMessages = messages.filter((
|
|
296
|
+
const newMessages = messages.filter((message) => this.seenMessageVersions.get(message.id) !== `${message.time_updated}:${message.data}`);
|
|
286
297
|
for (const msg of newMessages) {
|
|
287
298
|
const events = normalizeMessage(msg);
|
|
288
299
|
for (const e of events)
|
|
289
300
|
this.callbacks.onEvent(e);
|
|
290
|
-
|
|
291
|
-
|
|
301
|
+
this.seenMessageVersions.set(msg.id, `${msg.time_updated}:${msg.data}`);
|
|
302
|
+
if (msg.time_updated > this.lastMessageTime)
|
|
303
|
+
this.lastMessageTime = msg.time_updated;
|
|
292
304
|
}
|
|
293
305
|
// Emit new parts
|
|
294
|
-
const newParts = parts.filter((
|
|
306
|
+
const newParts = parts.filter((part) => this.seenPartVersions.get(part.id) !== `${part.time_updated}:${part.data}`);
|
|
295
307
|
for (const part of newParts) {
|
|
296
308
|
const events = normalizePart(part);
|
|
297
309
|
for (const e of events)
|
|
298
310
|
this.callbacks.onEvent(e);
|
|
299
|
-
|
|
300
|
-
|
|
311
|
+
this.seenPartVersions.set(part.id, `${part.time_updated}:${part.data}`);
|
|
312
|
+
if (part.time_updated > this.lastPartTime)
|
|
313
|
+
this.lastPartTime = part.time_updated;
|
|
301
314
|
}
|
|
302
315
|
}
|
|
303
316
|
catch (err) {
|
package/dist/writers/atomic.js
CHANGED
|
@@ -39,17 +39,53 @@ exports.updateJsonStoreAtomic = updateJsonStoreAtomic;
|
|
|
39
39
|
const crypto = __importStar(require("crypto"));
|
|
40
40
|
const fs = __importStar(require("fs"));
|
|
41
41
|
const path = __importStar(require("path"));
|
|
42
|
+
const LOCK_STALE_MS = 10_000;
|
|
43
|
+
async function syncDirectory(directory) {
|
|
44
|
+
let handle;
|
|
45
|
+
try {
|
|
46
|
+
handle = await fs.promises.open(directory, 'r');
|
|
47
|
+
await handle.sync();
|
|
48
|
+
}
|
|
49
|
+
catch (error) {
|
|
50
|
+
const code = error.code;
|
|
51
|
+
if (!['EISDIR', 'EINVAL', 'ENOSYS', 'EPERM', 'EACCES'].includes(code ?? ''))
|
|
52
|
+
throw error;
|
|
53
|
+
}
|
|
54
|
+
finally {
|
|
55
|
+
await handle?.close().catch(() => undefined);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
function syncDirectorySync(directory) {
|
|
59
|
+
let descriptor;
|
|
60
|
+
try {
|
|
61
|
+
descriptor = fs.openSync(directory, 'r');
|
|
62
|
+
fs.fsyncSync(descriptor);
|
|
63
|
+
}
|
|
64
|
+
catch (error) {
|
|
65
|
+
const code = error.code;
|
|
66
|
+
if (!['EISDIR', 'EINVAL', 'ENOSYS', 'EPERM', 'EACCES'].includes(code ?? ''))
|
|
67
|
+
throw error;
|
|
68
|
+
}
|
|
69
|
+
finally {
|
|
70
|
+
if (descriptor !== undefined)
|
|
71
|
+
fs.closeSync(descriptor);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
42
74
|
async function atomicWriteJson(filePath, value) {
|
|
43
75
|
await fs.promises.mkdir(path.dirname(filePath), { recursive: true, mode: 0o700 });
|
|
44
76
|
const tempPath = `${filePath}.${process.pid}.${crypto.randomBytes(8).toString('hex')}.tmp`;
|
|
77
|
+
let handle;
|
|
45
78
|
try {
|
|
46
|
-
await fs.promises.
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
79
|
+
handle = await fs.promises.open(tempPath, 'wx', 0o600);
|
|
80
|
+
await handle.writeFile(JSON.stringify(value, null, 2), 'utf8');
|
|
81
|
+
await handle.sync();
|
|
82
|
+
await handle.close();
|
|
83
|
+
handle = undefined;
|
|
50
84
|
await fs.promises.rename(tempPath, filePath);
|
|
85
|
+
await syncDirectory(path.dirname(filePath));
|
|
51
86
|
}
|
|
52
87
|
catch (error) {
|
|
88
|
+
await handle?.close().catch(() => undefined);
|
|
53
89
|
await fs.promises.rm(tempPath, { force: true }).catch(() => undefined);
|
|
54
90
|
throw error;
|
|
55
91
|
}
|
|
@@ -57,11 +93,19 @@ async function atomicWriteJson(filePath, value) {
|
|
|
57
93
|
function atomicWriteJsonSync(filePath, value) {
|
|
58
94
|
fs.mkdirSync(path.dirname(filePath), { recursive: true, mode: 0o700 });
|
|
59
95
|
const tempPath = `${filePath}.${process.pid}.${crypto.randomBytes(8).toString('hex')}.tmp`;
|
|
96
|
+
let descriptor;
|
|
60
97
|
try {
|
|
61
|
-
fs.
|
|
98
|
+
descriptor = fs.openSync(tempPath, 'wx', 0o600);
|
|
99
|
+
fs.writeFileSync(descriptor, JSON.stringify(value, null, 2), { encoding: 'utf8' });
|
|
100
|
+
fs.fsyncSync(descriptor);
|
|
101
|
+
fs.closeSync(descriptor);
|
|
102
|
+
descriptor = undefined;
|
|
62
103
|
fs.renameSync(tempPath, filePath);
|
|
104
|
+
syncDirectorySync(path.dirname(filePath));
|
|
63
105
|
}
|
|
64
106
|
catch (error) {
|
|
107
|
+
if (descriptor !== undefined)
|
|
108
|
+
fs.closeSync(descriptor);
|
|
65
109
|
fs.rmSync(tempPath, { force: true });
|
|
66
110
|
throw error;
|
|
67
111
|
}
|
|
@@ -73,34 +117,65 @@ async function updateJsonStoreAtomic(filePath, createEmpty, update) {
|
|
|
73
117
|
const lock = await acquireLock(lockPath);
|
|
74
118
|
try {
|
|
75
119
|
let latest = createEmpty();
|
|
120
|
+
let raw;
|
|
76
121
|
try {
|
|
77
|
-
|
|
122
|
+
raw = await fs.promises.readFile(filePath, 'utf8');
|
|
123
|
+
}
|
|
124
|
+
catch (error) {
|
|
125
|
+
if (error.code !== 'ENOENT')
|
|
126
|
+
throw error;
|
|
78
127
|
}
|
|
79
|
-
|
|
80
|
-
|
|
128
|
+
if (raw !== undefined) {
|
|
129
|
+
try {
|
|
130
|
+
latest = JSON.parse(raw);
|
|
131
|
+
}
|
|
132
|
+
catch {
|
|
133
|
+
const corruptPath = `${filePath}.corrupt-${Date.now()}-${crypto.randomBytes(4).toString('hex')}`;
|
|
134
|
+
await fs.promises.rename(filePath, corruptPath);
|
|
135
|
+
}
|
|
81
136
|
}
|
|
82
137
|
const next = await update(latest);
|
|
83
138
|
await atomicWriteJson(filePath, next);
|
|
84
139
|
return next;
|
|
85
140
|
}
|
|
86
141
|
finally {
|
|
87
|
-
await
|
|
88
|
-
await fs.promises.rm(lockPath, { force: true }).catch(() => undefined);
|
|
142
|
+
await releaseLock(lockPath, lock);
|
|
89
143
|
}
|
|
90
144
|
}
|
|
91
145
|
async function acquireLock(lockPath) {
|
|
92
146
|
const deadline = Date.now() + 3000;
|
|
93
147
|
while (true) {
|
|
94
148
|
try {
|
|
95
|
-
|
|
149
|
+
const handle = await fs.promises.open(lockPath, 'wx', 0o600);
|
|
150
|
+
const token = `${process.pid}:${crypto.randomBytes(16).toString('hex')}`;
|
|
151
|
+
await handle.writeFile(token, 'utf8');
|
|
152
|
+
await handle.sync();
|
|
153
|
+
const heartbeat = setInterval(() => {
|
|
154
|
+
fs.promises.utimes(lockPath, new Date(), new Date()).catch(() => undefined);
|
|
155
|
+
}, Math.floor(LOCK_STALE_MS / 3));
|
|
156
|
+
heartbeat.unref();
|
|
157
|
+
return { handle, token, heartbeat };
|
|
96
158
|
}
|
|
97
159
|
catch (error) {
|
|
98
160
|
if (error.code !== 'EEXIST')
|
|
99
161
|
throw error;
|
|
100
162
|
try {
|
|
101
163
|
const stat = await fs.promises.stat(lockPath);
|
|
102
|
-
if (Date.now() - stat.mtimeMs >
|
|
103
|
-
await fs.promises.
|
|
164
|
+
if (Date.now() - stat.mtimeMs > LOCK_STALE_MS) {
|
|
165
|
+
const owner = await fs.promises.readFile(lockPath, 'utf8').catch(() => '');
|
|
166
|
+
const ownerPid = Number.parseInt(owner.split(':', 1)[0], 10);
|
|
167
|
+
let ownerAlive = Number.isInteger(ownerPid) && ownerPid > 0;
|
|
168
|
+
if (ownerAlive) {
|
|
169
|
+
try {
|
|
170
|
+
process.kill(ownerPid, 0);
|
|
171
|
+
}
|
|
172
|
+
catch (ownerError) {
|
|
173
|
+
ownerAlive = ownerError.code === 'EPERM';
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
if (!ownerAlive)
|
|
177
|
+
await fs.promises.rm(lockPath, { force: true });
|
|
178
|
+
}
|
|
104
179
|
}
|
|
105
180
|
catch {
|
|
106
181
|
// The other writer released between checks.
|
|
@@ -111,3 +186,15 @@ async function acquireLock(lockPath) {
|
|
|
111
186
|
}
|
|
112
187
|
}
|
|
113
188
|
}
|
|
189
|
+
async function releaseLock(lockPath, lock) {
|
|
190
|
+
clearInterval(lock.heartbeat);
|
|
191
|
+
await lock.handle.close().catch(() => undefined);
|
|
192
|
+
try {
|
|
193
|
+
const currentToken = await fs.promises.readFile(lockPath, 'utf8');
|
|
194
|
+
if (currentToken === lock.token)
|
|
195
|
+
await fs.promises.rm(lockPath, { force: true });
|
|
196
|
+
}
|
|
197
|
+
catch {
|
|
198
|
+
// The lock was already released or replaced; never delete another owner's lock.
|
|
199
|
+
}
|
|
200
|
+
}
|
package/dist/zaiQuotaWatcher.js
CHANGED
|
@@ -169,6 +169,12 @@ class ZaiQuotaWatcher {
|
|
|
169
169
|
}
|
|
170
170
|
recomputeAndEmit() {
|
|
171
171
|
const nowMs = this.now();
|
|
172
|
+
if (this.lastError?.resetsAt) {
|
|
173
|
+
const resetsAt = Date.parse(this.lastError.resetsAt);
|
|
174
|
+
if (Number.isFinite(resetsAt) && resetsAt <= nowMs) {
|
|
175
|
+
this.lastError = null;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
172
178
|
const accumulated = (0, zaiQuota_1.accumulateZaiUsage)(this.turns, nowMs);
|
|
173
179
|
const tier = this.resolveTier(this.tier, accumulated);
|
|
174
180
|
const state = accumulated.fiveHourTurns > 0 || accumulated.weeklyTurns > 0
|
package/package.json
CHANGED