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.
- package/README.md +75 -63
- package/dist/accountManager.js +10 -0
- package/dist/accountRegistry.d.ts +2 -0
- package/dist/accountStatus.d.ts +1 -0
- package/dist/accountStatus.js +6 -0
- package/dist/accounts.js +2 -0
- package/dist/aggregation/EventAggregator.d.ts +36 -2
- package/dist/aggregation/EventAggregator.js +259 -33
- package/dist/aggregation/snapshot.d.ts +1 -1
- package/dist/aggregation/snapshot.js +2 -1
- package/dist/aggregation/types.d.ts +25 -1
- package/dist/analytics/codeImpact.d.ts +20 -0
- package/dist/analytics/codeImpact.js +21 -0
- package/dist/analytics/compactionLedger.d.ts +11 -0
- package/dist/analytics/compactionLedger.js +31 -0
- package/dist/analytics/qualityScore.d.ts +26 -0
- package/dist/analytics/qualityScore.js +115 -0
- package/dist/autoSwitch.d.ts +1 -1
- package/dist/autoSwitch.js +14 -3
- package/dist/codexProfiles.js +2 -0
- 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/composer.d.ts +3 -2
- package/dist/context/composer.js +5 -5
- package/dist/context/sessionContext.js +35 -9
- package/dist/credentialIO.js +19 -5
- package/dist/doctor.d.ts +55 -0
- package/dist/doctor.js +244 -0
- package/dist/errorHistory.d.ts +33 -0
- package/dist/errorHistory.js +95 -0
- package/dist/extractors/errorTaxonomy.d.ts +6 -0
- package/dist/extractors/errorTaxonomy.js +47 -0
- package/dist/extractors/toolCall.d.ts +9 -0
- package/dist/extractors/toolCall.js +79 -0
- package/dist/handoffUrl.d.ts +7 -0
- package/dist/handoffUrl.js +26 -0
- package/dist/index.d.ts +32 -9
- package/dist/index.js +64 -24
- package/dist/modelInfo.js +6 -0
- package/dist/multiProviderQuotaService.js +13 -2
- package/dist/parsers/codexParser.js +17 -1
- package/dist/parsers/openCodeParser.js +13 -0
- package/dist/parsers/planExtractor.js +17 -6
- 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 +4 -7
- package/dist/paths.d.ts +10 -0
- package/dist/paths.js +18 -5
- package/dist/projectMigration.d.ts +23 -0
- package/dist/projectMigration.js +95 -0
- package/dist/providerStatus.js +13 -2
- package/dist/providers/claudeCode.js +18 -4
- package/dist/providers/codex.js +42 -13
- package/dist/providers/codexDatabase.d.ts +1 -11
- package/dist/providers/detect.js +32 -3
- package/dist/providers/openCodeDatabase.d.ts +1 -31
- package/dist/providers/openCodeDatabase.js +21 -11
- package/dist/providers/types.d.ts +1 -0
- package/dist/quota.js +8 -0
- package/dist/quotaPoller.js +17 -6
- package/dist/quotaSnapshots.js +80 -26
- package/dist/readers/decisions.d.ts +2 -1
- package/dist/readers/decisions.js +5 -5
- package/dist/readers/handoff.d.ts +2 -1
- package/dist/readers/handoff.js +13 -8
- package/dist/readers/helpers.d.ts +3 -0
- package/dist/readers/helpers.js +12 -0
- package/dist/readers/notes.d.ts +2 -1
- package/dist/readers/notes.js +11 -8
- package/dist/readers/plans.js +3 -5
- package/dist/readers/tasks.d.ts +2 -1
- package/dist/readers/tasks.js +5 -5
- package/dist/report/htmlHelpers.js +6 -1
- package/dist/report/openBrowser.js +3 -3
- package/dist/schemas/index.d.ts +2 -0
- package/dist/schemas/index.js +10 -1
- package/dist/schemas/observedSessionV1.d.ts +499 -0
- package/dist/schemas/observedSessionV1.js +69 -0
- package/dist/schemas/sessionEvent.d.ts +13 -0
- package/dist/schemas/sessionEvent.js +19 -0
- package/dist/search/sessionSearch.d.ts +2 -2
- package/dist/search/sessionSearch.js +2 -3
- package/dist/sessionMonitor.d.ts +29 -0
- package/dist/sessionMonitor.js +71 -0
- package/dist/statusline/BurnRateCalculator.d.ts +14 -0
- package/dist/statusline/BurnRateCalculator.js +45 -0
- package/dist/statusline/formatter.d.ts +16 -0
- package/dist/statusline/formatter.js +84 -0
- package/dist/statusline/index.d.ts +7 -0
- package/dist/statusline/index.js +13 -0
- package/dist/terminalSync.js +16 -7
- package/dist/types/codex.d.ts +2 -0
- package/dist/types/historicalData.d.ts +49 -1
- package/dist/types/historicalData.js +28 -2
- package/dist/types/observedSessionV1.d.ts +70 -0
- package/dist/types/observedSessionV1.js +186 -0
- package/dist/types/opencode.d.ts +1 -0
- package/dist/types/sessionEvent.d.ts +19 -1
- package/dist/watchers/eventBridge.js +1 -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/watchers/types.d.ts +5 -0
- package/dist/writers/atomic.d.ts +4 -0
- package/dist/writers/atomic.js +200 -0
- package/dist/writers/decisions.d.ts +12 -0
- package/dist/writers/decisions.js +88 -0
- package/dist/writers/notes.d.ts +8 -0
- package/dist/writers/notes.js +82 -0
- package/dist/writers/tasks.d.ts +6 -0
- package/dist/writers/tasks.js +100 -0
- package/dist/zaiQuotaWatcher.js +6 -0
- 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
|
+
}
|
package/dist/types/opencode.d.ts
CHANGED
|
@@ -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?:
|
|
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.
|
package/dist/watchers/factory.js
CHANGED
|
@@ -56,14 +56,25 @@ function createWatcher(options) {
|
|
|
56
56
|
}
|
|
57
57
|
let sessionPath;
|
|
58
58
|
if (sessionId) {
|
|
59
|
-
const
|
|
60
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
82
|
+
this.readOffset = fs.statSync(this.options.path).size;
|
|
78
83
|
}
|
|
79
84
|
catch {
|
|
80
|
-
this.
|
|
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.
|
|
116
|
-
this.
|
|
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.
|
|
127
|
+
if (stat.size <= this.readOffset)
|
|
120
128
|
return;
|
|
121
|
-
const bytesToRead = stat.size - this.
|
|
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.
|
|
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.
|
|
137
|
+
this.readOffset += bytesRead;
|
|
130
138
|
this.eventsInCurrentBatch = 0;
|
|
131
|
-
this.
|
|
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.
|
|
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.
|
|
169
|
+
return this.committedOffset;
|
|
152
170
|
}
|
|
153
171
|
seekTo(offset) {
|
|
154
|
-
this.
|
|
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
|
-
//
|
|
279
|
-
this.
|
|
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.
|
|
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
|
-
|
|
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/watchers/types.d.ts
CHANGED
|
@@ -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>;
|