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.
Files changed (90) hide show
  1. package/README.md +139 -57
  2. package/dist/accountManager.js +10 -0
  3. package/dist/aggregation/EventAggregator.d.ts +8 -1
  4. package/dist/aggregation/EventAggregator.js +82 -136
  5. package/dist/aggregation/snapshot.d.ts +1 -1
  6. package/dist/aggregation/snapshot.js +2 -1
  7. package/dist/aggregation/types.d.ts +5 -0
  8. package/dist/autoSwitch.d.ts +1 -1
  9. package/dist/autoSwitch.js +14 -3
  10. package/dist/browser.d.ts +6 -0
  11. package/dist/browser.js +10 -1
  12. package/dist/codexQuota.d.ts +2 -0
  13. package/dist/codexQuota.js +27 -0
  14. package/dist/codexQuotaWatcher.d.ts +7 -0
  15. package/dist/codexQuotaWatcher.js +37 -19
  16. package/dist/context/sessionContext.js +43 -15
  17. package/dist/credentialIO.js +19 -5
  18. package/dist/errorHistory.d.ts +1 -0
  19. package/dist/errorHistory.js +3 -2
  20. package/dist/extractors/tokenUsage.js +1 -1
  21. package/dist/extractors/toolCall.js +2 -2
  22. package/dist/index.d.ts +14 -4
  23. package/dist/index.js +25 -7
  24. package/dist/modelInfo.d.ts +17 -7
  25. package/dist/modelInfo.js +11 -2
  26. package/dist/multiProviderQuotaService.js +13 -2
  27. package/dist/node.d.ts +4 -0
  28. package/dist/node.js +8 -1
  29. package/dist/observedSessionCollector.d.ts +68 -0
  30. package/dist/observedSessionCollector.js +178 -0
  31. package/dist/parsers/codexParser.js +24 -6
  32. package/dist/parsers/openCodeParser.js +22 -1
  33. package/dist/parsers/sessionActivityDetector.js +2 -0
  34. package/dist/parsers/sessionPathResolver.js +19 -16
  35. package/dist/parsers/subagentScanner.d.ts +1 -1
  36. package/dist/parsers/subagentScanner.js +4 -5
  37. package/dist/parsers/subagentTraceParser.js +15 -12
  38. package/dist/providerStatus.js +13 -2
  39. package/dist/providers/claudeCode.js +63 -5
  40. package/dist/providers/codex.js +56 -17
  41. package/dist/providers/detect.js +32 -3
  42. package/dist/providers/openCodeDatabase.js +12 -3
  43. package/dist/quota.js +8 -0
  44. package/dist/quotaPoller.js +17 -6
  45. package/dist/quotaSnapshots.js +80 -26
  46. package/dist/readers/decisions.js +3 -1
  47. package/dist/readers/handoff.js +1 -1
  48. package/dist/readers/notes.js +9 -4
  49. package/dist/readers/plans.js +2 -4
  50. package/dist/readers/tasks.js +3 -1
  51. package/dist/report/htmlHelpers.js +6 -1
  52. package/dist/report/openBrowser.js +3 -3
  53. package/dist/report/transcriptParser.d.ts +3 -11
  54. package/dist/report/transcriptParser.js +69 -154
  55. package/dist/schemas/index.d.ts +7 -1
  56. package/dist/schemas/index.js +18 -2
  57. package/dist/schemas/observedSessionV1.d.ts +32 -1
  58. package/dist/schemas/observedSessionV1.js +11 -1
  59. package/dist/schemas/sessionEvent.d.ts +353 -12
  60. package/dist/schemas/sessionEvent.js +76 -14
  61. package/dist/schemas/tokenEstimation.d.ts +14 -0
  62. package/dist/schemas/tokenEstimation.js +10 -0
  63. package/dist/schemas/transcript.d.ts +24 -0
  64. package/dist/schemas/transcript.js +81 -0
  65. package/dist/schemas/usageNormalization.d.ts +55 -0
  66. package/dist/schemas/usageNormalization.js +41 -0
  67. package/dist/sessionTranscripts.d.ts +16 -0
  68. package/dist/sessionTranscripts.js +47 -0
  69. package/dist/statusline/formatter.js +8 -1
  70. package/dist/terminalSync.js +16 -7
  71. package/dist/tokenEstimation.d.ts +24 -0
  72. package/dist/tokenEstimation.js +62 -0
  73. package/dist/transcript.d.ts +92 -0
  74. package/dist/transcript.js +240 -0
  75. package/dist/types/historicalData.d.ts +14 -0
  76. package/dist/types/historicalData.js +22 -0
  77. package/dist/types/observedSessionV1.d.ts +3 -1
  78. package/dist/types/observedSessionV1.js +20 -6
  79. package/dist/types/sessionEvent.d.ts +66 -7
  80. package/dist/usageNormalization.d.ts +69 -0
  81. package/dist/usageNormalization.js +100 -0
  82. package/dist/watchers/factory.js +14 -3
  83. package/dist/watchers/jsonlTail.js +34 -14
  84. package/dist/watchers/jsonlWatcher.js +5 -3
  85. package/dist/watchers/providerReaderWatcher.js +4 -2
  86. package/dist/watchers/sqliteWatcher.d.ts +2 -0
  87. package/dist/watchers/sqliteWatcher.js +27 -14
  88. package/dist/writers/atomic.js +100 -13
  89. package/dist/zaiQuotaWatcher.js +6 -0
  90. 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
- const messages = this.db.getMessagesForSession(this.sessionId);
270
- const parts = this.db.getPartsForSession(this.sessionId);
271
- if (messages.length > 0) {
272
- this.lastMessageTime = Math.max(...messages.map((m) => m.time_created));
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 (parts.length > 0) {
275
- this.lastPartTime = Math.max(...parts.map((p) => p.time_created));
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
- const messages = this.db.getMessagesForSession(this.sessionId);
283
- const parts = this.db.getPartsForSession(this.sessionId);
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((m) => m.time_created > this.lastMessageTime);
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
- if (msg.time_created > this.lastMessageTime)
291
- this.lastMessageTime = msg.time_created;
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((p) => p.time_created > this.lastPartTime);
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
- if (part.time_created > this.lastPartTime)
300
- this.lastPartTime = part.time_created;
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) {
@@ -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.writeFile(tempPath, JSON.stringify(value, null, 2), {
47
- encoding: 'utf8',
48
- mode: 0o600,
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.writeFileSync(tempPath, JSON.stringify(value, null, 2), { encoding: 'utf8', mode: 0o600 });
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
- latest = JSON.parse(await fs.promises.readFile(filePath, 'utf8'));
122
+ raw = await fs.promises.readFile(filePath, 'utf8');
123
+ }
124
+ catch (error) {
125
+ if (error.code !== 'ENOENT')
126
+ throw error;
78
127
  }
79
- catch {
80
- // Missing/malformed stores start from the schema-correct empty value.
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 lock.close().catch(() => undefined);
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
- return await fs.promises.open(lockPath, 'wx', 0o600);
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 > 10_000)
103
- await fs.promises.rm(lockPath, { force: true });
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
+ }
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sidekick-shared",
3
- "version": "0.23.0",
3
+ "version": "0.24.0",
4
4
  "description": "Shared data access layer for Sidekick — readers, types, providers, credentials, quota",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",