sidekick-shared 0.24.4 → 0.24.5

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 (42) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/README.md +86 -45
  3. package/dist/accountManager.d.ts +13 -0
  4. package/dist/accountManager.js +60 -32
  5. package/dist/accountRegistry.js +59 -41
  6. package/dist/accounts.js +40 -19
  7. package/dist/autoSwitch.d.ts +2 -1
  8. package/dist/autoSwitch.js +32 -7
  9. package/dist/browser.d.ts +2 -0
  10. package/dist/browser.js +3 -1
  11. package/dist/codexProfiles.d.ts +14 -0
  12. package/dist/codexProfiles.js +268 -116
  13. package/dist/context/sessionContext.js +2 -4
  14. package/dist/ensureDefaultAccounts.js +4 -4
  15. package/dist/index.d.ts +11 -4
  16. package/dist/index.js +28 -7
  17. package/dist/node.d.ts +6 -0
  18. package/dist/node.js +8 -1
  19. package/dist/observedContextWindows.d.ts +13 -0
  20. package/dist/observedContextWindows.js +13 -0
  21. package/dist/parsers/codexHistory.d.ts +30 -0
  22. package/dist/parsers/codexHistory.js +118 -0
  23. package/dist/parsers/mcpToolName.d.ts +26 -0
  24. package/dist/parsers/mcpToolName.js +31 -0
  25. package/dist/parsers/sessionPathResolver.d.ts +11 -4
  26. package/dist/parsers/sessionPathResolver.js +11 -4
  27. package/dist/paths.d.ts +7 -3
  28. package/dist/paths.js +7 -3
  29. package/dist/providers/claudeCode.d.ts +2 -1
  30. package/dist/providers/claudeCode.js +18 -0
  31. package/dist/providers/codex.d.ts +22 -1
  32. package/dist/providers/codex.js +58 -0
  33. package/dist/providers/types.d.ts +5 -0
  34. package/dist/quotaHistory.d.ts +1 -1
  35. package/dist/quotaHistory.js +2 -2
  36. package/dist/quotaSnapshots.js +8 -77
  37. package/dist/sessionPreviews.d.ts +60 -0
  38. package/dist/sessionPreviews.js +234 -0
  39. package/dist/sessionTranscripts.js +10 -4
  40. package/dist/writers/atomic.d.ts +20 -0
  41. package/dist/writers/atomic.js +190 -19
  42. package/package.json +1 -1
@@ -35,11 +35,42 @@ var __importStar = (this && this.__importStar) || (function () {
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.atomicWriteJson = atomicWriteJson;
37
37
  exports.atomicWriteJsonSync = atomicWriteJsonSync;
38
+ exports.atomicWriteFileSync = atomicWriteFileSync;
38
39
  exports.updateJsonStoreAtomic = updateJsonStoreAtomic;
40
+ exports.withFileLockSync = withFileLockSync;
41
+ exports.updateJsonStoreAtomicSync = updateJsonStoreAtomicSync;
39
42
  const crypto = __importStar(require("crypto"));
40
43
  const fs = __importStar(require("fs"));
41
44
  const path = __importStar(require("path"));
42
45
  const LOCK_STALE_MS = 10_000;
46
+ /**
47
+ * How long to wait for a lock that is making no progress.
48
+ *
49
+ * This budget detects a wedged holder, not a long queue. It is measured from the
50
+ * last time the lock changed hands rather than from the first attempt, because a
51
+ * writer queued behind other healthy writers is waiting, not failing — see
52
+ * {@link acquireLock}.
53
+ */
54
+ const LOCK_NO_PROGRESS_MS = 3_000;
55
+ /**
56
+ * A live async holder heartbeats every LOCK_STALE_MS / 3; an mtime frozen this
57
+ * long means the heartbeat timer is gone — the owner is dead (possibly with its
58
+ * PID recycled by the OS) or wedged beyond recovery. Reclaim regardless of the
59
+ * PID probe, which cannot tell a recycled PID from a live owner. Sync holders
60
+ * have no heartbeat; they usually hold locks for milliseconds, and even the
61
+ * slowest (a keychain read through `security` behind the auth-swap lock, up to
62
+ * a few seconds) stays far under this ceiling.
63
+ */
64
+ const LOCK_ABANDONED_MS = 120_000;
65
+ /**
66
+ * Hard ceiling on how long a synchronous waiter blocks, regardless of queue
67
+ * progress. Progress resets are correct for async waiters, but a sync waiter
68
+ * blocks its process's event loop while queued, so healthy-queue churn must not
69
+ * extend the block indefinitely.
70
+ */
71
+ const SYNC_LOCK_MAX_WAIT_MS = 15_000;
72
+ const LOCK_WAIT_INTERVAL_MS = 15;
73
+ const lockSleepView = new Int32Array(new SharedArrayBuffer(4));
43
74
  async function syncDirectory(directory) {
44
75
  let handle;
45
76
  try {
@@ -91,12 +122,16 @@ async function atomicWriteJson(filePath, value) {
91
122
  }
92
123
  }
93
124
  function atomicWriteJsonSync(filePath, value) {
125
+ atomicWriteFileSync(filePath, JSON.stringify(value, null, 2));
126
+ }
127
+ /** Raw-string sibling of {@link atomicWriteJsonSync} for non-JSON payloads. */
128
+ function atomicWriteFileSync(filePath, content, mode = 0o600) {
94
129
  fs.mkdirSync(path.dirname(filePath), { recursive: true, mode: 0o700 });
95
130
  const tempPath = `${filePath}.${process.pid}.${crypto.randomBytes(8).toString('hex')}.tmp`;
96
131
  let descriptor;
97
132
  try {
98
- descriptor = fs.openSync(tempPath, 'wx', 0o600);
99
- fs.writeFileSync(descriptor, JSON.stringify(value, null, 2), { encoding: 'utf8' });
133
+ descriptor = fs.openSync(tempPath, 'wx', mode);
134
+ fs.writeFileSync(descriptor, content, { encoding: 'utf8' });
100
135
  fs.fsyncSync(descriptor);
101
136
  fs.closeSync(descriptor);
102
137
  descriptor = undefined;
@@ -142,8 +177,22 @@ async function updateJsonStoreAtomic(filePath, createEmpty, update) {
142
177
  await releaseLock(lockPath, lock);
143
178
  }
144
179
  }
180
+ /**
181
+ * Waits for exclusive ownership of `lockPath`.
182
+ *
183
+ * The wait budget is reset every time the lock changes hands. A flat budget from
184
+ * the first attempt conflated two unrelated conditions — a wedged holder and a
185
+ * queue of healthy writers — so under real contention the writers at the back of
186
+ * the queue failed even though every lock was being acquired and released
187
+ * correctly. Each cycle costs three fsyncs (lock file, temp file, directory), so
188
+ * on slow storage a burst of concurrent captures drained slower than the budget.
189
+ *
190
+ * A stuck holder keeps the same token, so it makes no progress and still trips
191
+ * the timeout; the stale-reclaim path below still handles an owner that died.
192
+ */
145
193
  async function acquireLock(lockPath) {
146
- const deadline = Date.now() + 3000;
194
+ let deadline = Date.now() + LOCK_NO_PROGRESS_MS;
195
+ let observedOwner = null;
147
196
  while (true) {
148
197
  try {
149
198
  const handle = await fs.promises.open(lockPath, 'wx', 0o600);
@@ -159,29 +208,27 @@ async function acquireLock(lockPath) {
159
208
  catch (error) {
160
209
  if (error.code !== 'EEXIST')
161
210
  throw error;
211
+ // The token identifies the current holder. mtime cannot stand in for it:
212
+ // the heartbeat keeps touching the file, so even a wedged holder looks
213
+ // fresh. A different token means the queue advanced.
214
+ const currentOwner = await fs.promises.readFile(lockPath, 'utf8').catch(() => null);
215
+ if (currentOwner !== null && currentOwner !== observedOwner) {
216
+ observedOwner = currentOwner;
217
+ deadline = Date.now() + LOCK_NO_PROGRESS_MS;
218
+ }
162
219
  try {
163
220
  const stat = await fs.promises.stat(lockPath);
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 });
221
+ const frozenMs = Date.now() - stat.mtimeMs;
222
+ if (frozenMs > LOCK_STALE_MS && !lockOwnerAlive(currentOwner, frozenMs)) {
223
+ await fs.promises.rm(lockPath, { force: true });
178
224
  }
179
225
  }
180
226
  catch {
181
227
  // The other writer released between checks.
182
228
  }
183
- if (Date.now() >= deadline)
184
- throw new Error(`Timed out waiting for store lock: ${lockPath}`);
229
+ if (Date.now() >= deadline) {
230
+ throw new Error(`Timed out waiting for store lock: ${lockPath} (no progress for ${LOCK_NO_PROGRESS_MS}ms)`);
231
+ }
185
232
  await new Promise((resolve) => setTimeout(resolve, 15));
186
233
  }
187
234
  }
@@ -198,3 +245,127 @@ async function releaseLock(lockPath, lock) {
198
245
  // The lock was already released or replaced; never delete another owner's lock.
199
246
  }
200
247
  }
248
+ /**
249
+ * Whether the recorded owner of a stale lock should still be treated as alive.
250
+ * An mtime frozen past {@link LOCK_ABANDONED_MS} overrides the PID probe — see
251
+ * the constant for why the probe alone cannot be trusted at that age.
252
+ */
253
+ function lockOwnerAlive(owner, frozenMs) {
254
+ if (frozenMs > LOCK_ABANDONED_MS)
255
+ return false;
256
+ const ownerPid = Number.parseInt((owner ?? '').split(':', 1)[0], 10);
257
+ if (!Number.isInteger(ownerPid) || ownerPid <= 0)
258
+ return false;
259
+ try {
260
+ process.kill(ownerPid, 0);
261
+ return true;
262
+ }
263
+ catch (error) {
264
+ return error.code === 'EPERM';
265
+ }
266
+ }
267
+ /**
268
+ * Synchronous counterpart of {@link acquireLock}, sharing its lock-file
269
+ * protocol so sync and async waiters exclude each other across processes.
270
+ *
271
+ * Two timeout rules apply instead of one: the same progress-based budget as the
272
+ * async path (a token that never changes trips it in LOCK_NO_PROGRESS_MS), plus
273
+ * an absolute {@link SYNC_LOCK_MAX_WAIT_MS} ceiling, because a sync waiter
274
+ * blocks its event loop while queued. Sync holders take no heartbeat — their
275
+ * critical sections must stay well under LOCK_STALE_MS (in-repo users are
276
+ * local-filesystem read-modify-writes, plus keychain reads behind the
277
+ * auth-swap locks that can take a few seconds).
278
+ *
279
+ * The caller must ensure `dirname(lockPath)` exists — a missing parent
280
+ * surfaces as a raw ENOENT, not a timeout.
281
+ */
282
+ function withFileLockSync(lockPath, operation) {
283
+ const token = acquireLockSync(lockPath);
284
+ try {
285
+ return operation();
286
+ }
287
+ finally {
288
+ try {
289
+ if (fs.readFileSync(lockPath, 'utf8') === token)
290
+ fs.rmSync(lockPath, { force: true });
291
+ }
292
+ catch {
293
+ // The lock was already released or replaced; never delete another owner's lock.
294
+ }
295
+ }
296
+ }
297
+ function acquireLockSync(lockPath) {
298
+ const start = Date.now();
299
+ let deadline = start + LOCK_NO_PROGRESS_MS;
300
+ let observedOwner = null;
301
+ while (true) {
302
+ try {
303
+ const descriptor = fs.openSync(lockPath, 'wx', 0o600);
304
+ const token = `${process.pid}:${crypto.randomBytes(16).toString('hex')}`;
305
+ fs.writeFileSync(descriptor, token, 'utf8');
306
+ fs.fsyncSync(descriptor);
307
+ fs.closeSync(descriptor);
308
+ return token;
309
+ }
310
+ catch (error) {
311
+ if (error.code !== 'EEXIST')
312
+ throw error;
313
+ let currentOwner = null;
314
+ try {
315
+ currentOwner = fs.readFileSync(lockPath, 'utf8');
316
+ }
317
+ catch {
318
+ // The other writer released between checks.
319
+ }
320
+ if (currentOwner !== null && currentOwner !== observedOwner) {
321
+ observedOwner = currentOwner;
322
+ deadline = Date.now() + LOCK_NO_PROGRESS_MS;
323
+ }
324
+ try {
325
+ const stat = fs.statSync(lockPath);
326
+ const frozenMs = Date.now() - stat.mtimeMs;
327
+ if (frozenMs > LOCK_STALE_MS && !lockOwnerAlive(currentOwner, frozenMs)) {
328
+ fs.rmSync(lockPath, { force: true });
329
+ }
330
+ }
331
+ catch {
332
+ // The other writer released between checks.
333
+ }
334
+ const now = Date.now();
335
+ if (now >= deadline) {
336
+ throw new Error(`Timed out waiting for store lock: ${lockPath} (no progress for ${LOCK_NO_PROGRESS_MS}ms)`);
337
+ }
338
+ if (now - start >= SYNC_LOCK_MAX_WAIT_MS) {
339
+ throw new Error(`Timed out waiting for store lock: ${lockPath} (blocked synchronously for ${SYNC_LOCK_MAX_WAIT_MS}ms)`);
340
+ }
341
+ Atomics.wait(lockSleepView, 0, 0, LOCK_WAIT_INTERVAL_MS);
342
+ }
343
+ }
344
+ }
345
+ /** Synchronous mirror of {@link updateJsonStoreAtomic}. */
346
+ function updateJsonStoreAtomicSync(filePath, createEmpty, update) {
347
+ fs.mkdirSync(path.dirname(filePath), { recursive: true, mode: 0o700 });
348
+ return withFileLockSync(`${filePath}.lock`, () => {
349
+ let latest = createEmpty();
350
+ let raw;
351
+ try {
352
+ raw = fs.readFileSync(filePath, 'utf8');
353
+ }
354
+ catch (error) {
355
+ if (error.code !== 'ENOENT')
356
+ throw error;
357
+ }
358
+ if (raw !== undefined) {
359
+ try {
360
+ latest = JSON.parse(raw);
361
+ }
362
+ catch {
363
+ const corruptPath = `${filePath}.corrupt-${Date.now()}-${crypto.randomBytes(4).toString('hex')}`;
364
+ fs.renameSync(filePath, corruptPath);
365
+ }
366
+ }
367
+ const next = update(latest);
368
+ atomicWriteJsonSync(filePath, next);
369
+ return next;
370
+ });
371
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sidekick-shared",
3
- "version": "0.24.4",
3
+ "version": "0.24.5",
4
4
  "description": "Shared data access layer for Sidekick — readers, types, providers, credentials, quota",
5
5
  "keywords": [
6
6
  "claude-code",