sidekick-shared 0.22.0 → 0.23.1

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 (120) hide show
  1. package/README.md +75 -63
  2. package/dist/accountManager.js +10 -0
  3. package/dist/accountRegistry.d.ts +2 -0
  4. package/dist/accountStatus.d.ts +1 -0
  5. package/dist/accountStatus.js +6 -0
  6. package/dist/accounts.js +2 -0
  7. package/dist/aggregation/EventAggregator.d.ts +36 -2
  8. package/dist/aggregation/EventAggregator.js +259 -33
  9. package/dist/aggregation/snapshot.d.ts +1 -1
  10. package/dist/aggregation/snapshot.js +2 -1
  11. package/dist/aggregation/types.d.ts +25 -1
  12. package/dist/analytics/codeImpact.d.ts +20 -0
  13. package/dist/analytics/codeImpact.js +21 -0
  14. package/dist/analytics/compactionLedger.d.ts +11 -0
  15. package/dist/analytics/compactionLedger.js +31 -0
  16. package/dist/analytics/qualityScore.d.ts +26 -0
  17. package/dist/analytics/qualityScore.js +115 -0
  18. package/dist/autoSwitch.d.ts +1 -1
  19. package/dist/autoSwitch.js +14 -3
  20. package/dist/codexProfiles.js +2 -0
  21. package/dist/codexQuota.d.ts +2 -0
  22. package/dist/codexQuota.js +27 -0
  23. package/dist/codexQuotaWatcher.d.ts +7 -0
  24. package/dist/codexQuotaWatcher.js +37 -19
  25. package/dist/context/composer.d.ts +3 -2
  26. package/dist/context/composer.js +5 -5
  27. package/dist/context/sessionContext.js +35 -9
  28. package/dist/credentialIO.js +19 -5
  29. package/dist/doctor.d.ts +55 -0
  30. package/dist/doctor.js +244 -0
  31. package/dist/errorHistory.d.ts +33 -0
  32. package/dist/errorHistory.js +95 -0
  33. package/dist/extractors/errorTaxonomy.d.ts +6 -0
  34. package/dist/extractors/errorTaxonomy.js +47 -0
  35. package/dist/extractors/toolCall.d.ts +9 -0
  36. package/dist/extractors/toolCall.js +79 -0
  37. package/dist/handoffUrl.d.ts +7 -0
  38. package/dist/handoffUrl.js +26 -0
  39. package/dist/index.d.ts +32 -9
  40. package/dist/index.js +64 -24
  41. package/dist/modelInfo.js +6 -0
  42. package/dist/multiProviderQuotaService.js +13 -2
  43. package/dist/parsers/codexParser.js +17 -1
  44. package/dist/parsers/openCodeParser.js +13 -0
  45. package/dist/parsers/planExtractor.js +17 -6
  46. package/dist/parsers/sessionActivityDetector.js +2 -0
  47. package/dist/parsers/sessionPathResolver.js +19 -16
  48. package/dist/parsers/subagentScanner.d.ts +1 -1
  49. package/dist/parsers/subagentScanner.js +4 -5
  50. package/dist/parsers/subagentTraceParser.js +4 -7
  51. package/dist/paths.d.ts +10 -0
  52. package/dist/paths.js +18 -5
  53. package/dist/projectMigration.d.ts +23 -0
  54. package/dist/projectMigration.js +95 -0
  55. package/dist/providerStatus.js +13 -2
  56. package/dist/providers/claudeCode.js +18 -4
  57. package/dist/providers/codex.js +42 -13
  58. package/dist/providers/codexDatabase.d.ts +1 -11
  59. package/dist/providers/detect.js +32 -3
  60. package/dist/providers/openCodeDatabase.d.ts +1 -31
  61. package/dist/providers/openCodeDatabase.js +21 -11
  62. package/dist/providers/types.d.ts +1 -0
  63. package/dist/quota.js +8 -0
  64. package/dist/quotaPoller.js +17 -6
  65. package/dist/quotaSnapshots.js +80 -26
  66. package/dist/readers/decisions.d.ts +2 -1
  67. package/dist/readers/decisions.js +5 -5
  68. package/dist/readers/handoff.d.ts +2 -1
  69. package/dist/readers/handoff.js +13 -8
  70. package/dist/readers/helpers.d.ts +3 -0
  71. package/dist/readers/helpers.js +12 -0
  72. package/dist/readers/notes.d.ts +2 -1
  73. package/dist/readers/notes.js +11 -8
  74. package/dist/readers/plans.js +3 -5
  75. package/dist/readers/tasks.d.ts +2 -1
  76. package/dist/readers/tasks.js +5 -5
  77. package/dist/report/htmlHelpers.js +6 -1
  78. package/dist/report/openBrowser.js +3 -3
  79. package/dist/schemas/index.d.ts +2 -0
  80. package/dist/schemas/index.js +10 -1
  81. package/dist/schemas/observedSessionV1.d.ts +499 -0
  82. package/dist/schemas/observedSessionV1.js +69 -0
  83. package/dist/schemas/sessionEvent.d.ts +13 -0
  84. package/dist/schemas/sessionEvent.js +19 -0
  85. package/dist/search/sessionSearch.d.ts +2 -2
  86. package/dist/search/sessionSearch.js +2 -3
  87. package/dist/sessionMonitor.d.ts +29 -0
  88. package/dist/sessionMonitor.js +71 -0
  89. package/dist/statusline/BurnRateCalculator.d.ts +14 -0
  90. package/dist/statusline/BurnRateCalculator.js +45 -0
  91. package/dist/statusline/formatter.d.ts +16 -0
  92. package/dist/statusline/formatter.js +84 -0
  93. package/dist/statusline/index.d.ts +7 -0
  94. package/dist/statusline/index.js +13 -0
  95. package/dist/terminalSync.js +16 -7
  96. package/dist/types/codex.d.ts +2 -0
  97. package/dist/types/historicalData.d.ts +49 -1
  98. package/dist/types/historicalData.js +28 -2
  99. package/dist/types/observedSessionV1.d.ts +70 -0
  100. package/dist/types/observedSessionV1.js +186 -0
  101. package/dist/types/opencode.d.ts +1 -0
  102. package/dist/types/sessionEvent.d.ts +19 -1
  103. package/dist/watchers/eventBridge.js +1 -0
  104. package/dist/watchers/factory.js +14 -3
  105. package/dist/watchers/jsonlTail.js +34 -14
  106. package/dist/watchers/jsonlWatcher.js +5 -3
  107. package/dist/watchers/providerReaderWatcher.js +4 -2
  108. package/dist/watchers/sqliteWatcher.d.ts +2 -0
  109. package/dist/watchers/sqliteWatcher.js +27 -14
  110. package/dist/watchers/types.d.ts +5 -0
  111. package/dist/writers/atomic.d.ts +4 -0
  112. package/dist/writers/atomic.js +200 -0
  113. package/dist/writers/decisions.d.ts +12 -0
  114. package/dist/writers/decisions.js +88 -0
  115. package/dist/writers/notes.d.ts +8 -0
  116. package/dist/writers/notes.js +82 -0
  117. package/dist/writers/tasks.d.ts +6 -0
  118. package/dist/writers/tasks.js +100 -0
  119. package/dist/zaiQuotaWatcher.js +6 -0
  120. package/package.json +5 -1
@@ -0,0 +1,186 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.derivePendingUserRequestV1 = derivePendingUserRequestV1;
4
+ exports.createProviderSessionAdapterV1 = createProviderSessionAdapterV1;
5
+ function evidence(provider, sessionId, sourcePath, event, eventIndex) {
6
+ return [
7
+ {
8
+ schemaVersion: 1,
9
+ provider,
10
+ sessionId,
11
+ sourcePath,
12
+ eventIndex,
13
+ eventId: event?.message?.id,
14
+ timestamp: event?.timestamp,
15
+ },
16
+ ];
17
+ }
18
+ function observed(value, provenance, confidence, refs) {
19
+ return { value, provenance, confidence, evidence: refs };
20
+ }
21
+ function derivePendingUserRequestV1(provider, sessionId, sourcePath, events) {
22
+ for (let index = events.length - 1; index >= 0; index--) {
23
+ const event = events[index];
24
+ // A user/tool-result event after a request means the human has responded.
25
+ if (event.type === 'user')
26
+ return null;
27
+ const request = waitingToolRequest(event);
28
+ if (request) {
29
+ const refs = evidence(provider, sessionId, sourcePath, event, index);
30
+ return {
31
+ schemaVersion: 1,
32
+ id: `${provider}:${sessionId}:${request.id ?? event.message?.id ?? index}`,
33
+ provider,
34
+ sessionId,
35
+ kind: observed(request.kind, 'inferred', request.kind === 'question' ? 0.95 : 0.9, refs),
36
+ requestedAt: observed(event.timestamp, 'reported', 1, refs),
37
+ prompt: observed(request.prompt, request.prompt ? 'reported' : 'inferred', request.prompt ? 0.95 : 0.4, refs),
38
+ };
39
+ }
40
+ // A later assistant turn proves any older request was already resolved.
41
+ if (event.type === 'assistant' || event.type === 'tool_use')
42
+ return null;
43
+ }
44
+ return null;
45
+ }
46
+ function waitingToolRequest(event) {
47
+ const calls = [];
48
+ const message = event.message;
49
+ if (event.type === 'tool_use') {
50
+ calls.push({ id: message?.id, name: event.tool?.name, input: event.tool?.input });
51
+ }
52
+ if (Array.isArray(message?.content)) {
53
+ for (const block of message.content) {
54
+ if (!block || typeof block !== 'object')
55
+ continue;
56
+ const value = block;
57
+ if (value.type !== 'tool_use')
58
+ continue;
59
+ calls.push({
60
+ id: typeof value.id === 'string' ? value.id : undefined,
61
+ name: typeof value.name === 'string' ? value.name : undefined,
62
+ input: value.input,
63
+ });
64
+ }
65
+ }
66
+ for (let index = calls.length - 1; index >= 0; index--) {
67
+ const call = calls[index];
68
+ if (call.name !== 'AskUserQuestion' && call.name !== 'ExitPlanMode')
69
+ continue;
70
+ return {
71
+ id: call.id,
72
+ kind: call.name === 'AskUserQuestion' ? 'question' : 'tool_approval',
73
+ prompt: waitingToolPrompt(call.input, call.name),
74
+ };
75
+ }
76
+ return null;
77
+ }
78
+ function waitingToolPrompt(input, toolName) {
79
+ if (!input || typeof input !== 'object') {
80
+ return toolName === 'ExitPlanMode' ? 'Approve the proposed plan?' : null;
81
+ }
82
+ const record = input;
83
+ if (typeof record.question === 'string')
84
+ return record.question;
85
+ if (typeof record.prompt === 'string')
86
+ return record.prompt;
87
+ if (Array.isArray(record.questions)) {
88
+ const questions = record.questions
89
+ .map((question) => question && typeof question === 'object' && 'question' in question
90
+ ? String(question.question ?? '')
91
+ : '')
92
+ .filter(Boolean);
93
+ if (questions.length > 0)
94
+ return questions.join('\n');
95
+ }
96
+ if (toolName === 'ExitPlanMode')
97
+ return 'Approve the proposed plan?';
98
+ return null;
99
+ }
100
+ function providerCapabilities(provider) {
101
+ const refs = [];
102
+ const known = (value) => observed(value, 'inferred', 1, refs);
103
+ return {
104
+ schemaVersion: 1,
105
+ provider,
106
+ resume: known(true),
107
+ forkLineage: known(provider !== 'opencode'),
108
+ quotaSource: known(provider === 'claude-code' ? 'api' : provider === 'codex' ? 'mixed' : 'none'),
109
+ assetExtraction: known(true),
110
+ };
111
+ }
112
+ function createProviderSessionAdapterV1(sessionProvider, options = {}) {
113
+ const pollIntervalMs = options.pollIntervalMs ?? 2_000;
114
+ const adapter = {
115
+ schemaVersion: 1,
116
+ provider: sessionProvider.id,
117
+ capabilities: providerCapabilities(sessionProvider.id),
118
+ async discover(cwd) {
119
+ return Promise.all(sessionProvider.findAllSessions(cwd).map((sessionPath) => adapter.read(sessionPath, cwd)));
120
+ },
121
+ async read(sessionPath, cwd = '') {
122
+ const stats = sessionProvider.readSessionStats(sessionPath);
123
+ const events = sessionProvider.createReader(sessionPath).readAll();
124
+ const refs = evidence(sessionProvider.id, stats.sessionId, sessionPath);
125
+ const model = Object.entries(stats.modelUsage).sort((left, right) => right[1].calls - left[1].calls)[0]?.[0] ?? null;
126
+ const metadata = sessionProvider.getSessionMetadata?.(sessionPath);
127
+ const modifiedAt = metadata?.mtime.getTime() ?? Date.parse(stats.endTime);
128
+ const recent = Number.isFinite(modifiedAt) && Date.now() - modifiedAt < 2 * 60_000;
129
+ return {
130
+ schemaVersion: 1,
131
+ identity: {
132
+ provider: sessionProvider.id,
133
+ sessionId: stats.sessionId,
134
+ sourcePath: sessionPath,
135
+ },
136
+ cwd: observed(cwd, 'inferred', cwd ? 0.95 : 0.4, refs),
137
+ model: observed(model, model ? 'reported' : 'inferred', model ? 1 : 0.2, refs),
138
+ activity: observed(recent ? 'active' : 'idle', 'inferred', 0.8, refs),
139
+ usage: {
140
+ inputTokens: observed(stats.tokens.input, 'reported', 1, refs),
141
+ outputTokens: observed(stats.tokens.output, 'reported', 1, refs),
142
+ cacheReadTokens: observed(stats.tokens.cacheRead, 'reported', 1, refs),
143
+ cacheWriteTokens: observed(stats.tokens.cacheWrite, 'reported', 1, refs),
144
+ costUsd: observed(stats.reportedCost, stats.reportedCost > 0 ? 'reported' : 'estimated', stats.reportedCost > 0 ? 1 : 0.5, refs),
145
+ },
146
+ pendingUserRequest: derivePendingUserRequestV1(sessionProvider.id, stats.sessionId, sessionPath, events),
147
+ observedAt: new Date().toISOString(),
148
+ };
149
+ },
150
+ watch(sessionPath, listener, cwd) {
151
+ let disposed = false;
152
+ let lastSignature = '';
153
+ const update = async () => {
154
+ if (disposed)
155
+ return;
156
+ try {
157
+ const session = await adapter.read(sessionPath, cwd);
158
+ const signature = JSON.stringify({
159
+ activity: session.activity,
160
+ model: session.model,
161
+ usage: session.usage,
162
+ pendingUserRequest: session.pendingUserRequest,
163
+ });
164
+ if (signature !== lastSignature) {
165
+ lastSignature = signature;
166
+ listener(session);
167
+ }
168
+ }
169
+ catch {
170
+ // Observed sources can disappear between polling iterations.
171
+ }
172
+ };
173
+ void update();
174
+ const timer = setInterval(update, pollIntervalMs);
175
+ timer.unref?.();
176
+ return {
177
+ dispose: () => {
178
+ disposed = true;
179
+ clearInterval(timer);
180
+ },
181
+ };
182
+ },
183
+ dispose: () => sessionProvider.dispose(),
184
+ };
185
+ return adapter;
186
+ }
@@ -234,6 +234,7 @@ export interface DbProject {
234
234
  id: string;
235
235
  worktree: string;
236
236
  name: string | null;
237
+ sandboxes: string[];
237
238
  time_created: number;
238
239
  time_updated: number;
239
240
  }
@@ -76,6 +76,21 @@ export interface SessionEvent {
76
76
  resetsAt: number;
77
77
  };
78
78
  };
79
+ /** Exact context compaction counts when reported by the provider. */
80
+ compaction?: {
81
+ tokensBefore: number;
82
+ tokensAfter: number;
83
+ };
84
+ /** Provider-specific signals normalized for shared analytics. */
85
+ providerMetadata?: {
86
+ retryAttempts?: number[];
87
+ finishReasons?: string[];
88
+ providerError?: {
89
+ type?: string;
90
+ message?: string;
91
+ code?: string | number;
92
+ };
93
+ };
79
94
  /** Tool use details (when type is 'tool_use') */
80
95
  tool?: {
81
96
  name: string;
@@ -156,7 +171,7 @@ export interface ToolCall {
156
171
  /** Truncated error message if the call failed */
157
172
  errorMessage?: string;
158
173
  /** Error category (permission, not_found, timeout, syntax, exit_code, tool_error, other) */
159
- errorCategory?: string;
174
+ errorCategory?: import('../extractors/errorTaxonomy').ErrorCategory;
160
175
  /** Tool use ID for correlation with tool_result */
161
176
  toolUseId?: string;
162
177
  /** Truncated tool output/result content (max 5000 chars) */
@@ -206,6 +221,7 @@ export interface TimelineEvent {
206
221
  metadata?: {
207
222
  model?: string;
208
223
  toolName?: string;
224
+ toolUseId?: string;
209
225
  isError?: boolean;
210
226
  tokenCount?: number;
211
227
  fullText?: string;
@@ -506,6 +522,8 @@ export interface CompactionEvent {
506
522
  contextAfter: number;
507
523
  /** Tokens reclaimed (contextBefore - contextAfter) */
508
524
  tokensReclaimed: number;
525
+ /** Whether the provider reported exact counts or Sidekick inferred them. */
526
+ source: 'reported' | 'heuristic';
509
527
  }
510
528
  /**
511
529
  * Record of a tool output truncation event.
@@ -141,6 +141,7 @@ function toFollowEvents(event, providerId) {
141
141
  type: 'summary',
142
142
  timestamp: ts,
143
143
  summary: 'Context compacted',
144
+ compaction: event.compaction,
144
145
  raw: event,
145
146
  });
146
147
  break;
@@ -56,14 +56,25 @@ function createWatcher(options) {
56
56
  }
57
57
  let sessionPath;
58
58
  if (sessionId) {
59
- const match = sessions.find((s) => s.includes(sessionId));
60
- if (!match) {
59
+ const matches = sessions.map((sessionPath) => ({
60
+ sessionPath,
61
+ id: provider.getSessionId(sessionPath),
62
+ }));
63
+ const exact = matches.filter((candidate) => candidate.id === sessionId);
64
+ const candidates = exact.length > 0 ? exact : matches.filter((candidate) => candidate.id.startsWith(sessionId));
65
+ if (candidates.length === 0) {
61
66
  throw new Error(`Session ${sessionId} not found. Available: ${sessions
62
67
  .slice(0, 5)
63
68
  .map((s) => path.basename(s))
64
69
  .join(', ')}`);
65
70
  }
66
- sessionPath = match;
71
+ if (candidates.length > 1) {
72
+ throw new Error(`Session ${sessionId} is ambiguous. Matches: ${candidates
73
+ .slice(0, 5)
74
+ .map((candidate) => candidate.id)
75
+ .join(', ')}`);
76
+ }
77
+ sessionPath = candidates[0].sessionPath;
67
78
  }
68
79
  else {
69
80
  sessionPath = sessions[0]; // most recent
@@ -48,7 +48,11 @@ function createJsonlTail(options) {
48
48
  class JsonlTailReader {
49
49
  options;
50
50
  active = false;
51
- offset;
51
+ /** Physical byte position already read from the file. */
52
+ readOffset;
53
+ /** Resume-safe byte position ending immediately after a newline. */
54
+ committedOffset;
55
+ pendingBytes = Buffer.alloc(0);
52
56
  fsWatcher = null;
53
57
  debounceTimer = null;
54
58
  catchupTimer = null;
@@ -56,7 +60,8 @@ class JsonlTailReader {
56
60
  parser;
57
61
  constructor(options) {
58
62
  this.options = options;
59
- this.offset = options.startOffset ?? 0;
63
+ this.readOffset = options.startOffset ?? 0;
64
+ this.committedOffset = this.readOffset;
60
65
  this.parser = new jsonl_1.JsonlParser({
61
66
  onEvent: (event) => {
62
67
  this.eventsInCurrentBatch += 1;
@@ -74,11 +79,12 @@ class JsonlTailReader {
74
79
  this.active = true;
75
80
  if (this.options.startAtEnd && this.options.startOffset === undefined) {
76
81
  try {
77
- this.offset = fs.statSync(this.options.path).size;
82
+ this.readOffset = fs.statSync(this.options.path).size;
78
83
  }
79
84
  catch {
80
- this.offset = 0;
85
+ this.readOffset = 0;
81
86
  }
87
+ this.committedOffset = this.readOffset;
82
88
  }
83
89
  this.readNow();
84
90
  this.watchFile();
@@ -112,27 +118,39 @@ class JsonlTailReader {
112
118
  let fd = null;
113
119
  try {
114
120
  const stat = fs.statSync(this.options.path);
115
- if (stat.size < this.offset) {
116
- this.offset = 0;
121
+ if (stat.size < this.readOffset) {
122
+ this.readOffset = 0;
123
+ this.committedOffset = 0;
124
+ this.pendingBytes = Buffer.alloc(0);
117
125
  this.parser.reset();
118
126
  }
119
- if (stat.size <= this.offset)
127
+ if (stat.size <= this.readOffset)
120
128
  return;
121
- const bytesToRead = stat.size - this.offset;
129
+ const bytesToRead = stat.size - this.readOffset;
122
130
  const buffer = Buffer.alloc(bytesToRead);
123
131
  fd = fs.openSync(this.options.path, 'r');
124
- const bytesRead = fs.readSync(fd, buffer, 0, bytesToRead, this.offset);
132
+ const bytesRead = fs.readSync(fd, buffer, 0, bytesToRead, this.readOffset);
125
133
  fs.closeSync(fd);
126
134
  fd = null;
127
135
  if (bytesRead <= 0)
128
136
  return;
129
- this.offset += bytesRead;
137
+ this.readOffset += bytesRead;
130
138
  this.eventsInCurrentBatch = 0;
131
- this.parser.processChunk(buffer.toString('utf-8', 0, bytesRead));
139
+ const combined = Buffer.concat([this.pendingBytes, buffer.subarray(0, bytesRead)]);
140
+ const lastNewline = combined.lastIndexOf(0x0a);
141
+ if (lastNewline >= 0) {
142
+ const completeLines = combined.subarray(0, lastNewline + 1);
143
+ this.pendingBytes = Buffer.from(combined.subarray(lastNewline + 1));
144
+ this.parser.processChunk(completeLines.toString('utf8'));
145
+ }
146
+ else {
147
+ this.pendingBytes = combined;
148
+ }
149
+ this.committedOffset = this.readOffset - this.pendingBytes.length;
132
150
  this.options.onBatchComplete?.({
133
151
  bytesRead,
134
152
  eventsRead: this.eventsInCurrentBatch,
135
- offset: this.offset,
153
+ offset: this.committedOffset,
136
154
  });
137
155
  }
138
156
  catch (error) {
@@ -148,10 +166,12 @@ class JsonlTailReader {
148
166
  }
149
167
  }
150
168
  getOffset() {
151
- return this.offset;
169
+ return this.committedOffset;
152
170
  }
153
171
  seekTo(offset) {
154
- this.offset = Math.max(0, offset);
172
+ this.readOffset = Math.max(0, offset);
173
+ this.committedOffset = this.readOffset;
174
+ this.pendingBytes = Buffer.alloc(0);
155
175
  this.parser.reset();
156
176
  }
157
177
  watchFile() {
@@ -274,9 +274,11 @@ class JsonlSessionWatcher {
274
274
  this.fsWatcher = fs.watch(this.sessionPath, { persistent: false }, () => {
275
275
  this.debouncedRead();
276
276
  });
277
- this.fsWatcher.on('error', () => {
278
- // File may have been deleted; stop gracefully
279
- this.stop();
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);
280
282
  });
281
283
  }
282
284
  catch {
@@ -82,8 +82,10 @@ class ProviderReaderSessionWatcher {
82
82
  this.fsWatcher = fs.watch(this.sessionPath, { persistent: false }, () => {
83
83
  this.debouncedRead();
84
84
  });
85
- this.fsWatcher.on('error', () => {
86
- this.stop();
85
+ this.fsWatcher.on('error', (error) => {
86
+ this.fsWatcher?.close();
87
+ this.fsWatcher = null;
88
+ this.callbacks.onError?.(error);
87
89
  });
88
90
  }
89
91
  catch {
@@ -7,6 +7,8 @@ export declare class SqliteSessionWatcher implements SessionWatcher {
7
7
  private _isActive;
8
8
  private lastMessageTime;
9
9
  private lastPartTime;
10
+ private seenMessageVersions;
11
+ private seenPartVersions;
10
12
  private fsWatcher;
11
13
  private walWatcher;
12
14
  private pollTimer;
@@ -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) {
@@ -36,6 +36,11 @@ export interface FollowEvent {
36
36
  resetsAt: number;
37
37
  };
38
38
  };
39
+ /** Exact compaction counts when supplied by the provider. */
40
+ compaction?: {
41
+ tokensBefore: number;
42
+ tokensAfter: number;
43
+ };
39
44
  /** Original raw event for JSON output. */
40
45
  raw?: unknown;
41
46
  }
@@ -0,0 +1,4 @@
1
+ export declare function atomicWriteJson(filePath: string, value: unknown): Promise<void>;
2
+ export declare function atomicWriteJsonSync(filePath: string, value: unknown): void;
3
+ /** Serializes read-modify-write cycles across Sidekick processes. */
4
+ export declare function updateJsonStoreAtomic<T>(filePath: string, createEmpty: () => T, update: (latest: T) => T | Promise<T>): Promise<T>;