sidekick-shared 0.22.0 → 0.23.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 (86) hide show
  1. package/README.md +51 -43
  2. package/dist/accountRegistry.d.ts +2 -0
  3. package/dist/accountStatus.d.ts +1 -0
  4. package/dist/accountStatus.js +6 -0
  5. package/dist/accounts.js +2 -0
  6. package/dist/aggregation/EventAggregator.d.ts +32 -1
  7. package/dist/aggregation/EventAggregator.js +238 -25
  8. package/dist/aggregation/snapshot.js +1 -1
  9. package/dist/aggregation/types.d.ts +23 -1
  10. package/dist/analytics/codeImpact.d.ts +20 -0
  11. package/dist/analytics/codeImpact.js +21 -0
  12. package/dist/analytics/compactionLedger.d.ts +11 -0
  13. package/dist/analytics/compactionLedger.js +31 -0
  14. package/dist/analytics/qualityScore.d.ts +26 -0
  15. package/dist/analytics/qualityScore.js +115 -0
  16. package/dist/codexProfiles.js +2 -0
  17. package/dist/context/composer.d.ts +3 -2
  18. package/dist/context/composer.js +5 -5
  19. package/dist/doctor.d.ts +55 -0
  20. package/dist/doctor.js +244 -0
  21. package/dist/errorHistory.d.ts +32 -0
  22. package/dist/errorHistory.js +94 -0
  23. package/dist/extractors/errorTaxonomy.d.ts +6 -0
  24. package/dist/extractors/errorTaxonomy.js +47 -0
  25. package/dist/extractors/toolCall.d.ts +9 -0
  26. package/dist/extractors/toolCall.js +79 -0
  27. package/dist/handoffUrl.d.ts +7 -0
  28. package/dist/handoffUrl.js +26 -0
  29. package/dist/index.d.ts +32 -9
  30. package/dist/index.js +62 -24
  31. package/dist/parsers/codexParser.js +13 -0
  32. package/dist/parsers/openCodeParser.js +13 -0
  33. package/dist/parsers/planExtractor.js +17 -6
  34. package/dist/paths.d.ts +10 -0
  35. package/dist/paths.js +18 -5
  36. package/dist/projectMigration.d.ts +23 -0
  37. package/dist/projectMigration.js +95 -0
  38. package/dist/providers/codexDatabase.d.ts +1 -11
  39. package/dist/providers/openCodeDatabase.d.ts +1 -31
  40. package/dist/providers/openCodeDatabase.js +9 -8
  41. package/dist/providers/types.d.ts +1 -0
  42. package/dist/readers/decisions.d.ts +2 -1
  43. package/dist/readers/decisions.js +2 -4
  44. package/dist/readers/handoff.d.ts +2 -1
  45. package/dist/readers/handoff.js +13 -8
  46. package/dist/readers/helpers.d.ts +3 -0
  47. package/dist/readers/helpers.js +12 -0
  48. package/dist/readers/notes.d.ts +2 -1
  49. package/dist/readers/notes.js +2 -4
  50. package/dist/readers/plans.js +1 -1
  51. package/dist/readers/tasks.d.ts +2 -1
  52. package/dist/readers/tasks.js +2 -4
  53. package/dist/schemas/index.d.ts +2 -0
  54. package/dist/schemas/index.js +10 -1
  55. package/dist/schemas/observedSessionV1.d.ts +499 -0
  56. package/dist/schemas/observedSessionV1.js +69 -0
  57. package/dist/schemas/sessionEvent.d.ts +13 -0
  58. package/dist/schemas/sessionEvent.js +19 -0
  59. package/dist/search/sessionSearch.d.ts +2 -2
  60. package/dist/search/sessionSearch.js +2 -3
  61. package/dist/sessionMonitor.d.ts +29 -0
  62. package/dist/sessionMonitor.js +71 -0
  63. package/dist/statusline/BurnRateCalculator.d.ts +14 -0
  64. package/dist/statusline/BurnRateCalculator.js +45 -0
  65. package/dist/statusline/formatter.d.ts +16 -0
  66. package/dist/statusline/formatter.js +77 -0
  67. package/dist/statusline/index.d.ts +7 -0
  68. package/dist/statusline/index.js +13 -0
  69. package/dist/types/codex.d.ts +2 -0
  70. package/dist/types/historicalData.d.ts +35 -1
  71. package/dist/types/historicalData.js +6 -2
  72. package/dist/types/observedSessionV1.d.ts +70 -0
  73. package/dist/types/observedSessionV1.js +185 -0
  74. package/dist/types/opencode.d.ts +1 -0
  75. package/dist/types/sessionEvent.d.ts +18 -1
  76. package/dist/watchers/eventBridge.js +1 -0
  77. package/dist/watchers/types.d.ts +5 -0
  78. package/dist/writers/atomic.d.ts +4 -0
  79. package/dist/writers/atomic.js +113 -0
  80. package/dist/writers/decisions.d.ts +12 -0
  81. package/dist/writers/decisions.js +88 -0
  82. package/dist/writers/notes.d.ts +8 -0
  83. package/dist/writers/notes.js +82 -0
  84. package/dist/writers/tasks.d.ts +6 -0
  85. package/dist/writers/tasks.js +100 -0
  86. package/package.json +5 -1
@@ -0,0 +1,71 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.SessionMonitor = void 0;
4
+ const EventAggregator_1 = require("./aggregation/EventAggregator");
5
+ /**
6
+ * UI-independent session monitor for library consumers. It owns one provider
7
+ * reader and the canonical aggregator; hosts decide how often to call `poll()`.
8
+ */
9
+ class SessionMonitor {
10
+ provider;
11
+ aggregator;
12
+ reader = null;
13
+ sessionPath = null;
14
+ listeners = new Set();
15
+ replayOnAttach;
16
+ constructor(provider, options = {}) {
17
+ this.provider = provider;
18
+ this.replayOnAttach = options.replayOnAttach ?? true;
19
+ this.aggregator = new EventAggregator_1.EventAggregator({ ...options, providerId: provider.id });
20
+ }
21
+ attach(sessionPath, replay = this.replayOnAttach) {
22
+ this.aggregator.reset();
23
+ this.reader = this.provider.createReader(sessionPath);
24
+ this.sessionPath = sessionPath;
25
+ if (replay)
26
+ this.consume(this.reader.readAll());
27
+ return this.getMetrics();
28
+ }
29
+ attachLatest(workspacePath, replay = this.replayOnAttach) {
30
+ const sessionPath = this.provider.findActiveSession(workspacePath);
31
+ return sessionPath ? this.attach(sessionPath, replay) : null;
32
+ }
33
+ poll() {
34
+ if (!this.reader)
35
+ return [];
36
+ if (this.reader.wasTruncated())
37
+ this.aggregator.reset();
38
+ const events = this.reader.readNew();
39
+ this.consume(events);
40
+ return events;
41
+ }
42
+ process(event) {
43
+ this.aggregator.processEvent(event);
44
+ for (const listener of this.listeners)
45
+ listener(event);
46
+ }
47
+ onEvent(listener) {
48
+ this.listeners.add(listener);
49
+ return () => this.listeners.delete(listener);
50
+ }
51
+ getMetrics() {
52
+ return this.aggregator.getMetrics();
53
+ }
54
+ getSessionPath() {
55
+ return this.sessionPath;
56
+ }
57
+ reset() {
58
+ this.reader?.reset();
59
+ this.aggregator.reset();
60
+ }
61
+ dispose() {
62
+ this.listeners.clear();
63
+ this.reader = null;
64
+ this.provider.dispose();
65
+ }
66
+ consume(events) {
67
+ for (const event of events)
68
+ this.process(event);
69
+ }
70
+ }
71
+ exports.SessionMonitor = SessionMonitor;
@@ -0,0 +1,14 @@
1
+ /** Sliding-window burn-rate calculator shared by every Sidekick surface. */
2
+ export declare class BurnRateCalculator {
3
+ private events;
4
+ private readonly windowMs;
5
+ constructor(windowMinutes?: number);
6
+ addEvent(tokens: number, timestamp?: Date): void;
7
+ calculateBurnRate(now?: Date): number;
8
+ estimateTimeToQuota(currentTokens: number, quotaLimit: number, now?: Date): number | null;
9
+ getEventCount(): number;
10
+ reset(): void;
11
+ private pruneOldEvents;
12
+ }
13
+ /** Pure ETA helper for cached or externally computed burn rates. */
14
+ export declare function estimateTimeToQuota(currentUsage: number, quotaLimit: number, burnRatePerMinute: number): number | null;
@@ -0,0 +1,45 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.BurnRateCalculator = void 0;
4
+ exports.estimateTimeToQuota = estimateTimeToQuota;
5
+ /** Sliding-window burn-rate calculator shared by every Sidekick surface. */
6
+ class BurnRateCalculator {
7
+ events = [];
8
+ windowMs;
9
+ constructor(windowMinutes = 5) {
10
+ this.windowMs = windowMinutes * 60_000;
11
+ }
12
+ addEvent(tokens, timestamp = new Date()) {
13
+ this.events.push({ tokens, timestamp });
14
+ this.pruneOldEvents(timestamp);
15
+ }
16
+ calculateBurnRate(now = new Date()) {
17
+ this.pruneOldEvents(now);
18
+ if (this.events.length === 0)
19
+ return 0;
20
+ const totalTokens = this.events.reduce((sum, event) => sum + event.tokens, 0);
21
+ const elapsedMinutes = Math.max((now.getTime() - this.events[0].timestamp.getTime()) / 60_000, 1);
22
+ return totalTokens / elapsedMinutes;
23
+ }
24
+ estimateTimeToQuota(currentTokens, quotaLimit, now = new Date()) {
25
+ return estimateTimeToQuota(currentTokens, quotaLimit, this.calculateBurnRate(now));
26
+ }
27
+ getEventCount() {
28
+ return this.events.length;
29
+ }
30
+ reset() {
31
+ this.events = [];
32
+ }
33
+ pruneOldEvents(now) {
34
+ const cutoff = now.getTime() - this.windowMs;
35
+ this.events = this.events.filter((event) => event.timestamp.getTime() >= cutoff);
36
+ }
37
+ }
38
+ exports.BurnRateCalculator = BurnRateCalculator;
39
+ /** Pure ETA helper for cached or externally computed burn rates. */
40
+ function estimateTimeToQuota(currentUsage, quotaLimit, burnRatePerMinute) {
41
+ if (burnRatePerMinute <= 0)
42
+ return null;
43
+ const remaining = quotaLimit - currentUsage;
44
+ return remaining <= 0 ? 0 : remaining / burnRatePerMinute;
45
+ }
@@ -0,0 +1,16 @@
1
+ import type { ActiveAccountStatus } from '../accountStatus';
2
+ import type { QuotaState } from '../quota';
3
+ export interface StatuslineInput {
4
+ accounts: ActiveAccountStatus;
5
+ claudeQuota?: QuotaState | null;
6
+ codexQuota?: QuotaState | null;
7
+ now?: Date;
8
+ }
9
+ export interface StatuslineSelection {
10
+ providerId: 'claude-code' | 'codex';
11
+ accountId?: string;
12
+ accountLabel: string;
13
+ quota: QuotaState | null;
14
+ }
15
+ export declare function selectStatuslineAccount(input: StatuslineInput): StatuslineSelection | null;
16
+ export declare function formatStatusline(input: StatuslineInput): string;
@@ -0,0 +1,77 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.selectStatuslineAccount = selectStatuslineAccount;
4
+ exports.formatStatusline = formatStatusline;
5
+ const quota_1 = require("../quota");
6
+ const BurnRateCalculator_1 = require("./BurnRateCalculator");
7
+ function selectStatuslineAccount(input) {
8
+ if (input.accounts.claude.present) {
9
+ return {
10
+ providerId: 'claude-code',
11
+ accountId: input.accounts.claude.accountId,
12
+ accountLabel: input.accounts.claude.label ?? input.accounts.claude.email ?? 'claude',
13
+ quota: input.claudeQuota ?? null,
14
+ };
15
+ }
16
+ if (input.accounts.codex.present) {
17
+ return {
18
+ providerId: 'codex',
19
+ accountId: input.accounts.codex.accountId,
20
+ accountLabel: input.accounts.codex.label ?? input.accounts.codex.email ?? 'codex',
21
+ quota: input.codexQuota ?? null,
22
+ };
23
+ }
24
+ return null;
25
+ }
26
+ function formatStatusline(input) {
27
+ const selected = selectStatuslineAccount(input);
28
+ if (!selected)
29
+ return 'acct:none · quota unavailable';
30
+ const account = compactLabel(selected.accountLabel);
31
+ const quota = selected.quota;
32
+ if (!quota?.available)
33
+ return `acct:${account} · quota unavailable`;
34
+ const now = input.now ?? new Date();
35
+ const utilization = clampPercent(quota.fiveHour.utilization);
36
+ const reset = formatReset(quota.fiveHour.resetsAt, now);
37
+ const eta = estimateWindowEta(quota, now);
38
+ const etaText = eta == null ? '' : ` · ~${formatMinutes(eta)} left`;
39
+ return `acct:${account} · 5h ${Math.round(utilization)}% resets ${reset}${etaText}`;
40
+ }
41
+ function estimateWindowEta(quota, now) {
42
+ const resetAt = Date.parse(quota.fiveHour.resetsAt);
43
+ if (!Number.isFinite(resetAt))
44
+ return null;
45
+ const capturedAt = quota.capturedAt ? Date.parse(quota.capturedAt) : now.getTime();
46
+ const sampleAt = Number.isFinite(capturedAt) ? capturedAt : now.getTime();
47
+ const elapsedMinutes = (sampleAt - (resetAt - quota_1.FIVE_HOUR_WINDOW_MS)) / 60_000;
48
+ if (elapsedMinutes <= 0 || quota.fiveHour.utilization <= 0)
49
+ return null;
50
+ const burnRate = quota.fiveHour.utilization / elapsedMinutes;
51
+ return (0, BurnRateCalculator_1.estimateTimeToQuota)(quota.fiveHour.utilization, 100, burnRate);
52
+ }
53
+ function compactLabel(value) {
54
+ const trimmed = value.trim();
55
+ const localPart = trimmed.includes('@') ? trimmed.split('@')[0] : trimmed;
56
+ return localPart.replace(/\s+/g, '-').slice(0, 24) || 'unknown';
57
+ }
58
+ function clampPercent(value) {
59
+ return Number.isFinite(value) ? Math.min(100, Math.max(0, value)) : 0;
60
+ }
61
+ function formatReset(value, fallback) {
62
+ const reset = new Date(value);
63
+ const date = Number.isNaN(reset.getTime()) ? fallback : reset;
64
+ return new Intl.DateTimeFormat(undefined, {
65
+ hour: '2-digit',
66
+ minute: '2-digit',
67
+ hour12: false,
68
+ }).format(date);
69
+ }
70
+ function formatMinutes(value) {
71
+ const minutes = Math.max(0, Math.round(value));
72
+ if (minutes < 60)
73
+ return `${minutes}min`;
74
+ const hours = Math.floor(minutes / 60);
75
+ const remainder = minutes % 60;
76
+ return remainder ? `${hours}h${remainder}m` : `${hours}h`;
77
+ }
@@ -0,0 +1,7 @@
1
+ export { getActiveAccountStatus } from '../accountStatus';
2
+ export type { ActiveAccountStatus, ActiveProviderAccountStatus } from '../accountStatus';
3
+ export { readQuotaSnapshot } from '../quotaSnapshots';
4
+ export type { QuotaSnapshotProviderId } from '../quotaSnapshots';
5
+ export { BurnRateCalculator, estimateTimeToQuota } from './BurnRateCalculator';
6
+ export { formatStatusline, selectStatuslineAccount } from './formatter';
7
+ export type { StatuslineInput, StatuslineSelection } from './formatter';
@@ -0,0 +1,13 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.selectStatuslineAccount = exports.formatStatusline = exports.estimateTimeToQuota = exports.BurnRateCalculator = exports.readQuotaSnapshot = exports.getActiveAccountStatus = void 0;
4
+ var accountStatus_1 = require("../accountStatus");
5
+ Object.defineProperty(exports, "getActiveAccountStatus", { enumerable: true, get: function () { return accountStatus_1.getActiveAccountStatus; } });
6
+ var quotaSnapshots_1 = require("../quotaSnapshots");
7
+ Object.defineProperty(exports, "readQuotaSnapshot", { enumerable: true, get: function () { return quotaSnapshots_1.readQuotaSnapshot; } });
8
+ var BurnRateCalculator_1 = require("./BurnRateCalculator");
9
+ Object.defineProperty(exports, "BurnRateCalculator", { enumerable: true, get: function () { return BurnRateCalculator_1.BurnRateCalculator; } });
10
+ Object.defineProperty(exports, "estimateTimeToQuota", { enumerable: true, get: function () { return BurnRateCalculator_1.estimateTimeToQuota; } });
11
+ var formatter_1 = require("./formatter");
12
+ Object.defineProperty(exports, "formatStatusline", { enumerable: true, get: function () { return formatter_1.formatStatusline; } });
13
+ Object.defineProperty(exports, "selectStatuslineAccount", { enumerable: true, get: function () { return formatter_1.selectStatuslineAccount; } });
@@ -124,6 +124,8 @@ export interface CodexCustomToolCallOutputItem {
124
124
  /** Context compaction marker. */
125
125
  export interface CodexCompacted {
126
126
  summary?: string;
127
+ tokens_before?: number;
128
+ tokens_after?: number;
127
129
  }
128
130
  /** Turn context providing model and policy info. */
129
131
  export interface CodexTurnContext {
@@ -10,8 +10,12 @@
10
10
  * $0 because pricing was unknown — surfaced in the dashboard footer.
11
11
  * Legacy records (schemaVersion 1 or missing) have neither field and should
12
12
  * be treated as fully priced.
13
+ *
14
+ * Schema version 3 (2026-07-18) adds a capped per-session record list with
15
+ * provider/project dimensions, beta quality factors, churn, and impact cost.
13
16
  */
14
- export declare const HISTORICAL_DATA_SCHEMA_VERSION = 2;
17
+ export declare const HISTORICAL_DATA_SCHEMA_VERSION = 3;
18
+ export declare const HISTORICAL_SESSION_RETENTION_LIMIT = 500;
15
19
  export interface TokenTotals {
16
20
  inputTokens: number;
17
21
  outputTokens: number;
@@ -74,9 +78,32 @@ export interface HistoricalDataStore {
74
78
  monthly: Record<string, MonthlyData>;
75
79
  allTime: AllTimeStats;
76
80
  lastSaved: string;
81
+ /** Capped durable per-session records, newest last (schema v3). */
82
+ sessions?: SessionHistoryRecord[];
77
83
  importedFiles?: string[];
78
84
  lastImportTimestamp?: string;
79
85
  }
86
+ export interface SessionHistoryRecord {
87
+ sessionId: string;
88
+ provider: string;
89
+ project: string;
90
+ startTime: string;
91
+ endTime: string;
92
+ tokens: TokenTotals;
93
+ totalCost: number;
94
+ messageCount: number;
95
+ qualityScore: number;
96
+ qualityFactors: Array<{
97
+ id: string;
98
+ label: string;
99
+ contribution: number;
100
+ maximum: number;
101
+ detail: string;
102
+ }>;
103
+ additions: number;
104
+ deletions: number;
105
+ costPerChangedLine: number | null;
106
+ }
80
107
  export interface SessionSummary {
81
108
  sessionId: string;
82
109
  startTime: string;
@@ -96,5 +123,12 @@ export interface SessionSummary {
96
123
  * Omitted on v1 records or when every model was priced.
97
124
  */
98
125
  unpricedModelIds?: string[];
126
+ provider?: string;
127
+ project?: string;
128
+ qualityScore?: number;
129
+ qualityFactors?: SessionHistoryRecord['qualityFactors'];
130
+ additions?: number;
131
+ deletions?: number;
132
+ costPerChangedLine?: number | null;
99
133
  }
100
134
  export declare function createEmptyTokenTotals(): TokenTotals;
@@ -11,11 +11,15 @@
11
11
  * $0 because pricing was unknown — surfaced in the dashboard footer.
12
12
  * Legacy records (schemaVersion 1 or missing) have neither field and should
13
13
  * be treated as fully priced.
14
+ *
15
+ * Schema version 3 (2026-07-18) adds a capped per-session record list with
16
+ * provider/project dimensions, beta quality factors, churn, and impact cost.
14
17
  */
15
18
  Object.defineProperty(exports, "__esModule", { value: true });
16
- exports.HISTORICAL_DATA_SCHEMA_VERSION = void 0;
19
+ exports.HISTORICAL_SESSION_RETENTION_LIMIT = exports.HISTORICAL_DATA_SCHEMA_VERSION = void 0;
17
20
  exports.createEmptyTokenTotals = createEmptyTokenTotals;
18
- exports.HISTORICAL_DATA_SCHEMA_VERSION = 2;
21
+ exports.HISTORICAL_DATA_SCHEMA_VERSION = 3;
22
+ exports.HISTORICAL_SESSION_RETENTION_LIMIT = 500;
19
23
  function createEmptyTokenTotals() {
20
24
  return {
21
25
  inputTokens: 0,
@@ -0,0 +1,70 @@
1
+ import type { SessionEvent } from './sessionEvent';
2
+ import type { ProviderId, SessionProviderBase } from '../providers/types';
3
+ export type ObservationProvenanceV1 = 'reported' | 'estimated' | 'inferred';
4
+ export interface SessionEvidenceRefV1 {
5
+ schemaVersion: 1;
6
+ provider: ProviderId;
7
+ sessionId: string;
8
+ sourcePath?: string;
9
+ eventIndex?: number;
10
+ eventId?: string;
11
+ timestamp?: string;
12
+ }
13
+ export interface ObservedValueV1<T> {
14
+ value: T;
15
+ provenance: ObservationProvenanceV1;
16
+ confidence: number;
17
+ evidence: SessionEvidenceRefV1[];
18
+ }
19
+ export interface PendingUserRequestV1 {
20
+ schemaVersion: 1;
21
+ id: string;
22
+ provider: ProviderId;
23
+ sessionId: string;
24
+ kind: ObservedValueV1<'prompt_response' | 'tool_approval' | 'question'>;
25
+ requestedAt: ObservedValueV1<string>;
26
+ prompt: ObservedValueV1<string | null>;
27
+ }
28
+ export interface ObservedAgentSessionV1 {
29
+ schemaVersion: 1;
30
+ identity: {
31
+ provider: ProviderId;
32
+ sessionId: string;
33
+ sourcePath: string;
34
+ };
35
+ cwd: ObservedValueV1<string>;
36
+ model: ObservedValueV1<string | null>;
37
+ activity: ObservedValueV1<'active' | 'idle' | 'ended' | 'unknown'>;
38
+ usage: {
39
+ inputTokens: ObservedValueV1<number>;
40
+ outputTokens: ObservedValueV1<number>;
41
+ cacheReadTokens: ObservedValueV1<number>;
42
+ cacheWriteTokens: ObservedValueV1<number>;
43
+ costUsd: ObservedValueV1<number>;
44
+ };
45
+ pendingUserRequest: PendingUserRequestV1 | null;
46
+ observedAt: string;
47
+ }
48
+ export interface ProviderCapabilitiesV1 {
49
+ schemaVersion: 1;
50
+ provider: ProviderId;
51
+ resume: ObservedValueV1<boolean>;
52
+ forkLineage: ObservedValueV1<boolean>;
53
+ quotaSource: ObservedValueV1<'api' | 'session' | 'mixed' | 'none'>;
54
+ assetExtraction: ObservedValueV1<boolean>;
55
+ }
56
+ export interface ProviderSessionAdapterV1 {
57
+ readonly schemaVersion: 1;
58
+ readonly provider: ProviderId;
59
+ readonly capabilities: ProviderCapabilitiesV1;
60
+ discover(cwd: string): Promise<ObservedAgentSessionV1[]>;
61
+ read(sessionPath: string, cwd?: string): Promise<ObservedAgentSessionV1>;
62
+ watch(sessionPath: string, listener: (session: ObservedAgentSessionV1) => void, cwd?: string): {
63
+ dispose(): void;
64
+ };
65
+ dispose(): void;
66
+ }
67
+ export declare function derivePendingUserRequestV1(provider: ProviderId, sessionId: string, sourcePath: string, events: SessionEvent[]): PendingUserRequestV1 | null;
68
+ export declare function createProviderSessionAdapterV1(sessionProvider: SessionProviderBase, options?: {
69
+ pollIntervalMs?: number;
70
+ }): ProviderSessionAdapterV1;
@@ -0,0 +1,185 @@
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
+ if (event.type === 'tool_use') {
49
+ calls.push({ id: event.message.id, name: event.tool?.name, input: event.tool?.input });
50
+ }
51
+ if (Array.isArray(event.message.content)) {
52
+ for (const block of event.message.content) {
53
+ if (!block || typeof block !== 'object')
54
+ continue;
55
+ const value = block;
56
+ if (value.type !== 'tool_use')
57
+ continue;
58
+ calls.push({
59
+ id: typeof value.id === 'string' ? value.id : undefined,
60
+ name: typeof value.name === 'string' ? value.name : undefined,
61
+ input: value.input,
62
+ });
63
+ }
64
+ }
65
+ for (let index = calls.length - 1; index >= 0; index--) {
66
+ const call = calls[index];
67
+ if (call.name !== 'AskUserQuestion' && call.name !== 'ExitPlanMode')
68
+ continue;
69
+ return {
70
+ id: call.id,
71
+ kind: call.name === 'AskUserQuestion' ? 'question' : 'tool_approval',
72
+ prompt: waitingToolPrompt(call.input, call.name),
73
+ };
74
+ }
75
+ return null;
76
+ }
77
+ function waitingToolPrompt(input, toolName) {
78
+ if (!input || typeof input !== 'object') {
79
+ return toolName === 'ExitPlanMode' ? 'Approve the proposed plan?' : null;
80
+ }
81
+ const record = input;
82
+ if (typeof record.question === 'string')
83
+ return record.question;
84
+ if (typeof record.prompt === 'string')
85
+ return record.prompt;
86
+ if (Array.isArray(record.questions)) {
87
+ const questions = record.questions
88
+ .map((question) => question && typeof question === 'object' && 'question' in question
89
+ ? String(question.question ?? '')
90
+ : '')
91
+ .filter(Boolean);
92
+ if (questions.length > 0)
93
+ return questions.join('\n');
94
+ }
95
+ if (toolName === 'ExitPlanMode')
96
+ return 'Approve the proposed plan?';
97
+ return null;
98
+ }
99
+ function providerCapabilities(provider) {
100
+ const refs = [];
101
+ const known = (value) => observed(value, 'inferred', 1, refs);
102
+ return {
103
+ schemaVersion: 1,
104
+ provider,
105
+ resume: known(true),
106
+ forkLineage: known(provider !== 'opencode'),
107
+ quotaSource: known(provider === 'claude-code' ? 'api' : provider === 'codex' ? 'mixed' : 'none'),
108
+ assetExtraction: known(true),
109
+ };
110
+ }
111
+ function createProviderSessionAdapterV1(sessionProvider, options = {}) {
112
+ const pollIntervalMs = options.pollIntervalMs ?? 2_000;
113
+ const adapter = {
114
+ schemaVersion: 1,
115
+ provider: sessionProvider.id,
116
+ capabilities: providerCapabilities(sessionProvider.id),
117
+ async discover(cwd) {
118
+ return Promise.all(sessionProvider.findAllSessions(cwd).map((sessionPath) => adapter.read(sessionPath, cwd)));
119
+ },
120
+ async read(sessionPath, cwd = '') {
121
+ const stats = sessionProvider.readSessionStats(sessionPath);
122
+ const events = sessionProvider.createReader(sessionPath).readAll();
123
+ const refs = evidence(sessionProvider.id, stats.sessionId, sessionPath);
124
+ const model = Object.entries(stats.modelUsage).sort((left, right) => right[1].calls - left[1].calls)[0]?.[0] ?? null;
125
+ const metadata = sessionProvider.getSessionMetadata?.(sessionPath);
126
+ const modifiedAt = metadata?.mtime.getTime() ?? Date.parse(stats.endTime);
127
+ const recent = Number.isFinite(modifiedAt) && Date.now() - modifiedAt < 2 * 60_000;
128
+ return {
129
+ schemaVersion: 1,
130
+ identity: {
131
+ provider: sessionProvider.id,
132
+ sessionId: stats.sessionId,
133
+ sourcePath: sessionPath,
134
+ },
135
+ cwd: observed(cwd, 'inferred', cwd ? 0.95 : 0.4, refs),
136
+ model: observed(model, model ? 'reported' : 'inferred', model ? 1 : 0.2, refs),
137
+ activity: observed(recent ? 'active' : 'idle', 'inferred', 0.8, refs),
138
+ usage: {
139
+ inputTokens: observed(stats.tokens.input, 'reported', 1, refs),
140
+ outputTokens: observed(stats.tokens.output, 'reported', 1, refs),
141
+ cacheReadTokens: observed(stats.tokens.cacheRead, 'reported', 1, refs),
142
+ cacheWriteTokens: observed(stats.tokens.cacheWrite, 'reported', 1, refs),
143
+ costUsd: observed(stats.reportedCost, stats.reportedCost > 0 ? 'reported' : 'estimated', stats.reportedCost > 0 ? 1 : 0.5, refs),
144
+ },
145
+ pendingUserRequest: derivePendingUserRequestV1(sessionProvider.id, stats.sessionId, sessionPath, events),
146
+ observedAt: new Date().toISOString(),
147
+ };
148
+ },
149
+ watch(sessionPath, listener, cwd) {
150
+ let disposed = false;
151
+ let lastSignature = '';
152
+ const update = async () => {
153
+ if (disposed)
154
+ return;
155
+ try {
156
+ const session = await adapter.read(sessionPath, cwd);
157
+ const signature = JSON.stringify({
158
+ activity: session.activity,
159
+ model: session.model,
160
+ usage: session.usage,
161
+ pendingUserRequest: session.pendingUserRequest,
162
+ });
163
+ if (signature !== lastSignature) {
164
+ lastSignature = signature;
165
+ listener(session);
166
+ }
167
+ }
168
+ catch {
169
+ // Observed sources can disappear between polling iterations.
170
+ }
171
+ };
172
+ void update();
173
+ const timer = setInterval(update, pollIntervalMs);
174
+ timer.unref?.();
175
+ return {
176
+ dispose: () => {
177
+ disposed = true;
178
+ clearInterval(timer);
179
+ },
180
+ };
181
+ },
182
+ dispose: () => sessionProvider.dispose(),
183
+ };
184
+ return adapter;
185
+ }
@@ -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) */
@@ -506,6 +521,8 @@ export interface CompactionEvent {
506
521
  contextAfter: number;
507
522
  /** Tokens reclaimed (contextBefore - contextAfter) */
508
523
  tokensReclaimed: number;
524
+ /** Whether the provider reported exact counts or Sidekick inferred them. */
525
+ source: 'reported' | 'heuristic';
509
526
  }
510
527
  /**
511
528
  * 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;
@@ -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>;