sidekick-shared 0.23.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 +24 -20
- package/dist/accountManager.js +10 -0
- package/dist/aggregation/EventAggregator.d.ts +4 -1
- package/dist/aggregation/EventAggregator.js +22 -9
- package/dist/aggregation/snapshot.d.ts +1 -1
- package/dist/aggregation/snapshot.js +2 -1
- package/dist/aggregation/types.d.ts +2 -0
- package/dist/autoSwitch.d.ts +1 -1
- package/dist/autoSwitch.js +14 -3
- 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/sessionContext.js +35 -9
- package/dist/credentialIO.js +19 -5
- package/dist/errorHistory.d.ts +1 -0
- package/dist/errorHistory.js +3 -2
- package/dist/extractors/toolCall.js +2 -2
- package/dist/index.d.ts +3 -3
- package/dist/index.js +9 -7
- package/dist/modelInfo.js +6 -0
- package/dist/multiProviderQuotaService.js +13 -2
- package/dist/parsers/codexParser.js +4 -1
- 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/providerStatus.js +13 -2
- package/dist/providers/claudeCode.js +18 -4
- package/dist/providers/codex.js +42 -13
- package/dist/providers/detect.js +32 -3
- package/dist/providers/openCodeDatabase.js +12 -3
- package/dist/quota.js +8 -0
- package/dist/quotaPoller.js +17 -6
- package/dist/quotaSnapshots.js +80 -26
- package/dist/readers/decisions.js +3 -1
- package/dist/readers/handoff.js +1 -1
- package/dist/readers/notes.js +9 -4
- package/dist/readers/plans.js +2 -4
- package/dist/readers/tasks.js +3 -1
- package/dist/report/htmlHelpers.js +6 -1
- package/dist/report/openBrowser.js +3 -3
- package/dist/statusline/formatter.js +8 -1
- package/dist/terminalSync.js +16 -7
- package/dist/types/historicalData.d.ts +14 -0
- package/dist/types/historicalData.js +22 -0
- package/dist/types/observedSessionV1.js +6 -5
- package/dist/types/sessionEvent.d.ts +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/writers/atomic.js +100 -13
- package/dist/zaiQuotaWatcher.js +6 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -110,13 +110,23 @@ await hydratePricingCatalog({ cacheDir: '~/.config/sidekick' });
|
|
|
110
110
|
### Detect the active session provider
|
|
111
111
|
|
|
112
112
|
```typescript
|
|
113
|
-
import {
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
113
|
+
import {
|
|
114
|
+
detectProvider,
|
|
115
|
+
ClaudeCodeProvider,
|
|
116
|
+
OpenCodeProvider,
|
|
117
|
+
CodexProvider,
|
|
118
|
+
} from 'sidekick-shared';
|
|
119
|
+
|
|
120
|
+
const providerId = detectProvider(); // 'claude-code' | 'opencode' | 'codex'
|
|
121
|
+
const provider =
|
|
122
|
+
providerId === 'opencode'
|
|
123
|
+
? new OpenCodeProvider()
|
|
124
|
+
: providerId === 'codex'
|
|
125
|
+
? new CodexProvider()
|
|
126
|
+
: new ClaudeCodeProvider();
|
|
127
|
+
|
|
128
|
+
console.log(`Active provider: ${provider.displayName}`);
|
|
129
|
+
const sessions = provider.findAllSessions('/path/to/project');
|
|
120
130
|
```
|
|
121
131
|
|
|
122
132
|
### Read persisted tasks
|
|
@@ -237,17 +247,15 @@ The extractor is safe for CLI and VS Code extension-host code, but not for brows
|
|
|
237
247
|
Build a provider-neutral snapshot of what an assistant has "seen" in a session — layered evidence sources, context pressure, and observed capabilities. Read it through any session provider, or build it directly from a canonical `SessionEvent[]`.
|
|
238
248
|
|
|
239
249
|
```typescript
|
|
240
|
-
import {
|
|
250
|
+
import { ClaudeCodeProvider, readSessionContextSnapshot } from 'sidekick-shared';
|
|
241
251
|
|
|
242
|
-
const provider =
|
|
243
|
-
|
|
244
|
-
const snapshot = readSessionContextSnapshot(provider, '/path/to/session.jsonl');
|
|
252
|
+
const provider = new ClaudeCodeProvider();
|
|
253
|
+
const snapshot = readSessionContextSnapshot(provider, '/path/to/session.jsonl');
|
|
245
254
|
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
}
|
|
255
|
+
console.log(snapshot.pressure); // 'low' | 'medium' | 'high'
|
|
256
|
+
console.log(snapshot.contextTokens, '/', snapshot.contextWindow);
|
|
257
|
+
console.log(snapshot.capabilities.tools, snapshot.capabilities.mcpServers);
|
|
258
|
+
console.log(snapshot.sources.length, 'evidence sources');
|
|
251
259
|
```
|
|
252
260
|
|
|
253
261
|
Use `createSessionContextProjector()` for incremental updates as new events stream in, or `calculateSessionContextPressure(contextTokens, contextWindow)` for the pressure band alone.
|
|
@@ -403,10 +411,6 @@ const totalSource = mergeCostSources(a.source, b.source); // 'unpriced' wins (le
|
|
|
403
411
|
console.log(formatCost(total), totalSource);
|
|
404
412
|
```
|
|
405
413
|
|
|
406
|
-
### Deferred Contextful adoption note
|
|
407
|
-
|
|
408
|
-
`sidekick-shared@0.18.x` already exposes the quota primitives Contextful needs: `MultiProviderQuotaService`, `ProviderQuotaMap`, `ProviderQuotaState`, and `CodexQuotaWatcher`. Contextful should keep its local integration unchanged until a newer `sidekick-shared` release is published to npm, then migrate thin wrappers to these public APIs plus `formatTokenCount()`, `formatDurationMs()`, and `createJsonlTail()`.
|
|
409
|
-
|
|
410
414
|
## Building
|
|
411
415
|
|
|
412
416
|
```bash
|
package/dist/accountManager.js
CHANGED
|
@@ -281,6 +281,7 @@ async function spawnAccountLogin(provider, label, opts = {}) {
|
|
|
281
281
|
}
|
|
282
282
|
let childExited = false;
|
|
283
283
|
let childExitCode = null;
|
|
284
|
+
const childState = {};
|
|
284
285
|
const timeoutMs = opts.timeoutMs ?? 180_000;
|
|
285
286
|
const deadline = Date.now() + timeoutMs;
|
|
286
287
|
let child;
|
|
@@ -297,6 +298,10 @@ async function spawnAccountLogin(provider, label, opts = {}) {
|
|
|
297
298
|
childExited = true;
|
|
298
299
|
childExitCode = code;
|
|
299
300
|
});
|
|
301
|
+
child.on('error', (error) => {
|
|
302
|
+
childExited = true;
|
|
303
|
+
childState.spawnError = error;
|
|
304
|
+
});
|
|
300
305
|
while (true) {
|
|
301
306
|
if (opts.signal?.aborted) {
|
|
302
307
|
child.kill();
|
|
@@ -308,6 +313,11 @@ async function spawnAccountLogin(provider, label, opts = {}) {
|
|
|
308
313
|
return finalizeAccountLogin(provider, begin.loginId, { activate: opts.activate ?? true });
|
|
309
314
|
}
|
|
310
315
|
if (childExited) {
|
|
316
|
+
if (childState.spawnError) {
|
|
317
|
+
const error = `Could not spawn account login: ${childState.spawnError.message}`;
|
|
318
|
+
emitStatus(opts, { state: 'failed', error });
|
|
319
|
+
return { success: false, error };
|
|
320
|
+
}
|
|
311
321
|
const finalStatus = emitStatus(opts, getAccountLoginStatus(provider, begin.loginId));
|
|
312
322
|
if (finalStatus.state === 'authenticated') {
|
|
313
323
|
return finalizeAccountLogin(provider, begin.loginId, { activate: opts.activate ?? true });
|
|
@@ -13,6 +13,8 @@ import type { EventAggregatorOptions, AggregatedTokens, ModelUsageStats, BurnRat
|
|
|
13
13
|
import type { SerializedFrequencyState } from './FrequencyTracker';
|
|
14
14
|
import type { SerializedHeatmapState } from './HeatmapTracker';
|
|
15
15
|
import type { SerializedPatternState } from './PatternExtractor';
|
|
16
|
+
/** Schema version for serialized snapshots. */
|
|
17
|
+
export declare const SNAPSHOT_SCHEMA_VERSION = 3;
|
|
16
18
|
/**
|
|
17
19
|
* JSON-serializable snapshot of EventAggregator state.
|
|
18
20
|
* Used by session snapshot sidecar files for fast resume.
|
|
@@ -127,6 +129,7 @@ export declare class EventAggregator {
|
|
|
127
129
|
private currentPermissionMode;
|
|
128
130
|
private permissionModeHistory;
|
|
129
131
|
private contextTimeline;
|
|
132
|
+
private readonly contextTimelineCap;
|
|
130
133
|
private contextTurnIndex;
|
|
131
134
|
private timeline;
|
|
132
135
|
private pendingUserRequest;
|
|
@@ -166,7 +169,7 @@ export declare class EventAggregator {
|
|
|
166
169
|
/** Serializes all mutable state to a JSON-safe object for snapshot persistence. */
|
|
167
170
|
serialize(): SerializedAggregatorState;
|
|
168
171
|
/** Restores mutable state from a serialized snapshot. Clears transient state (pending calls). */
|
|
169
|
-
restore(state: SerializedAggregatorState):
|
|
172
|
+
restore(state: SerializedAggregatorState): boolean;
|
|
170
173
|
private accumulateUsage;
|
|
171
174
|
private updateBurnRate;
|
|
172
175
|
private processLatency;
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* @module aggregation/EventAggregator
|
|
10
10
|
*/
|
|
11
11
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
-
exports.EventAggregator = void 0;
|
|
12
|
+
exports.EventAggregator = exports.SNAPSHOT_SCHEMA_VERSION = void 0;
|
|
13
13
|
exports.parseTodoDependencies = parseTodoDependencies;
|
|
14
14
|
const jsonl_1 = require("../parsers/jsonl");
|
|
15
15
|
const planExtractor_1 = require("../parsers/planExtractor");
|
|
@@ -24,11 +24,12 @@ const errorTaxonomy_1 = require("../extractors/errorTaxonomy");
|
|
|
24
24
|
// ── Defaults ──
|
|
25
25
|
const DEFAULT_TIMELINE_CAP = 200;
|
|
26
26
|
const DEFAULT_LATENCY_CAP = 100;
|
|
27
|
+
const DEFAULT_CONTEXT_TIMELINE_CAP = 2_000;
|
|
27
28
|
const DEFAULT_BURN_WINDOW_MS = 5 * 60_000;
|
|
28
29
|
const DEFAULT_BURN_SAMPLE_MS = 10_000;
|
|
29
30
|
const COMPACTION_DROP_THRESHOLD = 0.8; // >20% drop
|
|
30
31
|
/** Schema version for serialized snapshots. */
|
|
31
|
-
|
|
32
|
+
exports.SNAPSHOT_SCHEMA_VERSION = 3;
|
|
32
33
|
// ── Goal gate detection regex ──
|
|
33
34
|
const GOAL_GATE_REGEX = /\b(CRITICAL|MUST|blocker|required|must.?complete|goal.?gate|essential|do.?not.?skip|blocking)\b/i;
|
|
34
35
|
// ── Todo dependency parsing ──
|
|
@@ -127,6 +128,7 @@ class EventAggregator {
|
|
|
127
128
|
permissionModeHistory = [];
|
|
128
129
|
// Context timeline
|
|
129
130
|
contextTimeline = [];
|
|
131
|
+
contextTimelineCap;
|
|
130
132
|
contextTurnIndex = 0;
|
|
131
133
|
// Timeline
|
|
132
134
|
timeline = [];
|
|
@@ -150,6 +152,7 @@ class EventAggregator {
|
|
|
150
152
|
this.latencyCap = options?.latencyCap ?? DEFAULT_LATENCY_CAP;
|
|
151
153
|
this.burnWindowMs = options?.burnWindowMs ?? DEFAULT_BURN_WINDOW_MS;
|
|
152
154
|
this.burnSampleMs = options?.burnSampleMs ?? DEFAULT_BURN_SAMPLE_MS;
|
|
155
|
+
this.contextTimelineCap = options?.contextTimelineCap ?? DEFAULT_CONTEXT_TIMELINE_CAP;
|
|
153
156
|
this.computeContextSize = options?.computeContextSize ?? null;
|
|
154
157
|
this.providerId = options?.providerId ?? null;
|
|
155
158
|
this._providerId = this.providerId;
|
|
@@ -167,6 +170,10 @@ class EventAggregator {
|
|
|
167
170
|
this.lastEventTime = event.timestamp;
|
|
168
171
|
// Guard: some event types (e.g. 'summary') have no message field in the raw JSONL
|
|
169
172
|
if (!event.message) {
|
|
173
|
+
if (event.type === 'summary') {
|
|
174
|
+
this.recordExplicitCompaction(event.timestamp, event.compaction);
|
|
175
|
+
this.addTimelineFromSessionEvent(event);
|
|
176
|
+
}
|
|
170
177
|
return;
|
|
171
178
|
}
|
|
172
179
|
// 2. Track model
|
|
@@ -300,7 +307,7 @@ class EventAggregator {
|
|
|
300
307
|
priced: true,
|
|
301
308
|
};
|
|
302
309
|
acc.calls++;
|
|
303
|
-
acc.tokens += inputTok + outputTok;
|
|
310
|
+
acc.tokens += inputTok + outputTok + cacheWrite + cacheRead;
|
|
304
311
|
acc.inputTokens += inputTok;
|
|
305
312
|
acc.outputTokens += outputTok;
|
|
306
313
|
acc.cacheWriteTokens += cacheWrite;
|
|
@@ -346,7 +353,7 @@ class EventAggregator {
|
|
|
346
353
|
// 12. Gonzo/Lazyjournal analytics
|
|
347
354
|
this.feedAnalytics(event.timestamp, event.summary, event.toolName);
|
|
348
355
|
// Message count (skip system events)
|
|
349
|
-
if (event.type
|
|
356
|
+
if (event.type === 'user' || event.type === 'assistant') {
|
|
350
357
|
this.messageCount++;
|
|
351
358
|
}
|
|
352
359
|
}
|
|
@@ -543,7 +550,7 @@ class EventAggregator {
|
|
|
543
550
|
/** Serializes all mutable state to a JSON-safe object for snapshot persistence. */
|
|
544
551
|
serialize() {
|
|
545
552
|
return {
|
|
546
|
-
version: SNAPSHOT_SCHEMA_VERSION,
|
|
553
|
+
version: exports.SNAPSHOT_SCHEMA_VERSION,
|
|
547
554
|
tokens: {
|
|
548
555
|
input: this.inputTokens,
|
|
549
556
|
output: this.outputTokens,
|
|
@@ -587,8 +594,8 @@ class EventAggregator {
|
|
|
587
594
|
}
|
|
588
595
|
/** Restores mutable state from a serialized snapshot. Clears transient state (pending calls). */
|
|
589
596
|
restore(state) {
|
|
590
|
-
if (state.version !== SNAPSHOT_SCHEMA_VERSION) {
|
|
591
|
-
return;
|
|
597
|
+
if (state.version !== exports.SNAPSHOT_SCHEMA_VERSION) {
|
|
598
|
+
return false;
|
|
592
599
|
}
|
|
593
600
|
this.inputTokens = state.tokens.input;
|
|
594
601
|
this.outputTokens = state.tokens.output;
|
|
@@ -617,7 +624,9 @@ class EventAggregator {
|
|
|
617
624
|
this.permissionModeHistory = state.permissionModeHistory
|
|
618
625
|
? [...state.permissionModeHistory]
|
|
619
626
|
: [];
|
|
620
|
-
this.contextTimeline = state.contextTimeline
|
|
627
|
+
this.contextTimeline = state.contextTimeline
|
|
628
|
+
? state.contextTimeline.slice(-this.contextTimelineCap)
|
|
629
|
+
: [];
|
|
621
630
|
this.contextTurnIndex = state.contextTurnIndex ?? 0;
|
|
622
631
|
this.timeline = [...state.timeline];
|
|
623
632
|
this.messageCount = state.messageCount;
|
|
@@ -640,6 +649,7 @@ class EventAggregator {
|
|
|
640
649
|
this.pendingSubagents.clear();
|
|
641
650
|
this.pendingUserRequest = null;
|
|
642
651
|
this.planExtractor.reset();
|
|
652
|
+
return true;
|
|
643
653
|
}
|
|
644
654
|
// ═══════════════════════════════════════════════════════════════════════
|
|
645
655
|
// Private: Token & Context
|
|
@@ -716,6 +726,9 @@ class EventAggregator {
|
|
|
716
726
|
inputTokens: contextSize,
|
|
717
727
|
turnIndex: this.contextTurnIndex++,
|
|
718
728
|
});
|
|
729
|
+
if (this.contextTimeline.length > this.contextTimelineCap) {
|
|
730
|
+
this.contextTimeline.splice(0, this.contextTimeline.length - this.contextTimelineCap);
|
|
731
|
+
}
|
|
719
732
|
// Per-model usage. Accumulate reasoning tokens and an inverted-sticky
|
|
720
733
|
// `priced` flag so one unpriced event taints the aggregate (UI renders "—").
|
|
721
734
|
const perModelKey = model ?? this.currentModel ?? 'unknown';
|
|
@@ -731,7 +744,7 @@ class EventAggregator {
|
|
|
731
744
|
priced: true,
|
|
732
745
|
};
|
|
733
746
|
acc.calls++;
|
|
734
|
-
acc.tokens += inputTok + outputTok;
|
|
747
|
+
acc.tokens += inputTok + outputTok + cacheWrite + cacheRead;
|
|
735
748
|
acc.inputTokens += inputTok;
|
|
736
749
|
acc.outputTokens += outputTok;
|
|
737
750
|
acc.cacheWriteTokens += cacheWrite;
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
*
|
|
8
8
|
* @module aggregation/snapshot
|
|
9
9
|
*/
|
|
10
|
-
import type
|
|
10
|
+
import { type SerializedAggregatorState } from './EventAggregator';
|
|
11
11
|
/** Snapshot sidecar file content. */
|
|
12
12
|
export interface SessionSnapshot {
|
|
13
13
|
/** Schema version for forward-compatibility checks. */
|
|
@@ -50,8 +50,9 @@ exports.isSnapshotValid = isSnapshotValid;
|
|
|
50
50
|
const fs = __importStar(require("fs"));
|
|
51
51
|
const path = __importStar(require("path"));
|
|
52
52
|
const paths_1 = require("../paths");
|
|
53
|
+
const EventAggregator_1 = require("./EventAggregator");
|
|
53
54
|
/** Current schema version — bump when format changes. */
|
|
54
|
-
const SNAPSHOT_VERSION =
|
|
55
|
+
const SNAPSHOT_VERSION = EventAggregator_1.SNAPSHOT_SCHEMA_VERSION;
|
|
55
56
|
/** Returns the directory for snapshot files. */
|
|
56
57
|
function getSnapshotsDir() {
|
|
57
58
|
return path.join((0, paths_1.getConfigDir)(), 'snapshots');
|
|
@@ -31,6 +31,8 @@ export interface EventAggregatorOptions {
|
|
|
31
31
|
timelineCap?: number;
|
|
32
32
|
/** Maximum latency records to retain (default 100). */
|
|
33
33
|
latencyCap?: number;
|
|
34
|
+
/** Maximum context-size points to retain (default 2000). */
|
|
35
|
+
contextTimelineCap?: number;
|
|
34
36
|
/** Burn rate sliding window in ms (default 5 * 60_000). */
|
|
35
37
|
burnWindowMs?: number;
|
|
36
38
|
/** Burn rate sample interval in ms (default 10_000). */
|
package/dist/autoSwitch.d.ts
CHANGED
|
@@ -42,7 +42,7 @@ export interface AutoSwitchControllerOptions {
|
|
|
42
42
|
cooldownMs?: number;
|
|
43
43
|
now?: () => number;
|
|
44
44
|
}
|
|
45
|
-
export declare function decideAutoSwitch(_provider: AccountProviderId, active: AutoSwitchActiveAccount, candidates: AutoSwitchCandidate[], config?: AutoSwitchConfig): AutoSwitchDecision | null;
|
|
45
|
+
export declare function decideAutoSwitch(_provider: AccountProviderId, active: AutoSwitchActiveAccount, candidates: AutoSwitchCandidate[], config?: AutoSwitchConfig, nowMs?: number): AutoSwitchDecision | null;
|
|
46
46
|
export declare class AutoSwitchController implements Disposable {
|
|
47
47
|
private readonly quotaService;
|
|
48
48
|
private readonly getClaudeAccounts;
|
package/dist/autoSwitch.js
CHANGED
|
@@ -11,6 +11,7 @@ exports.DEFAULT_AUTO_SWITCH_CONFIG = {
|
|
|
11
11
|
thresholdPct: 90,
|
|
12
12
|
};
|
|
13
13
|
const MATERIAL_REMAINING_IMPROVEMENT_PCT = 5;
|
|
14
|
+
const MAX_STALE_SNAPSHOT_AGE_MS = 5 * 60 * 60 * 1000;
|
|
14
15
|
function mergedConfig(config) {
|
|
15
16
|
return {
|
|
16
17
|
...exports.DEFAULT_AUTO_SWITCH_CONFIG,
|
|
@@ -27,7 +28,17 @@ function quotaRemaining(quota) {
|
|
|
27
28
|
return 0;
|
|
28
29
|
return Math.max(0, 100 - quotaUtilization(quota));
|
|
29
30
|
}
|
|
30
|
-
function
|
|
31
|
+
function isUsableCandidateQuota(quota, nowMs) {
|
|
32
|
+
if (!quota?.available)
|
|
33
|
+
return false;
|
|
34
|
+
if (quota.stale !== true)
|
|
35
|
+
return true;
|
|
36
|
+
const capturedAt = Date.parse(quota.capturedAt ?? '');
|
|
37
|
+
if (!Number.isFinite(capturedAt) || capturedAt > nowMs)
|
|
38
|
+
return false;
|
|
39
|
+
return nowMs - capturedAt <= MAX_STALE_SNAPSHOT_AGE_MS;
|
|
40
|
+
}
|
|
41
|
+
function decideAutoSwitch(_provider, active, candidates, config, nowMs = Date.now()) {
|
|
31
42
|
const cfg = mergedConfig(config);
|
|
32
43
|
if (!cfg.enabled)
|
|
33
44
|
return null;
|
|
@@ -40,7 +51,7 @@ function decideAutoSwitch(_provider, active, candidates, config) {
|
|
|
40
51
|
const best = candidates
|
|
41
52
|
.filter((candidate) => candidate.accountId !== active.accountId &&
|
|
42
53
|
candidate.switchable !== false &&
|
|
43
|
-
candidate.quota
|
|
54
|
+
isUsableCandidateQuota(candidate.quota, nowMs))
|
|
44
55
|
.sort((a, b) => quotaRemaining(b.quota) - quotaRemaining(a.quota))[0];
|
|
45
56
|
if (!best)
|
|
46
57
|
return null;
|
|
@@ -129,7 +140,7 @@ class AutoSwitchController {
|
|
|
129
140
|
quota: accountId === activeAccountId ? activeQuota : this.readSnapshot(providerId, accountId),
|
|
130
141
|
};
|
|
131
142
|
});
|
|
132
|
-
const decision = decideAutoSwitch(providerId, { accountId: activeAccountId, quota: activeQuota }, candidates, this.config);
|
|
143
|
+
const decision = decideAutoSwitch(providerId, { accountId: activeAccountId, quota: activeQuota }, candidates, this.config, this.now());
|
|
133
144
|
if (!decision)
|
|
134
145
|
return;
|
|
135
146
|
if (this.switchedDuringCrossing.has(providerId))
|
package/dist/codexQuota.d.ts
CHANGED
|
@@ -32,6 +32,8 @@ export interface CodexQuotaApiOptions {
|
|
|
32
32
|
usageUrl?: string;
|
|
33
33
|
resetCreditsUrl?: string;
|
|
34
34
|
capturedAt?: string;
|
|
35
|
+
signal?: AbortSignal;
|
|
36
|
+
timeoutMs?: number;
|
|
35
37
|
}
|
|
36
38
|
export declare function quotaFromCodexRateLimits(rateLimits: CodexRateLimits | null | undefined, source?: 'api' | 'session' | 'cache', capturedAt?: string): QuotaState | null;
|
|
37
39
|
export declare function readLatestCodexQuotaFromRollouts(sessionPaths: string[], options?: {
|
package/dist/codexQuota.js
CHANGED
|
@@ -414,10 +414,19 @@ async function fetchCodexQuotaFromApi(options = {}) {
|
|
|
414
414
|
sevenDayLabel: 'Secondary',
|
|
415
415
|
};
|
|
416
416
|
}
|
|
417
|
+
const controller = new AbortController();
|
|
418
|
+
const onAbort = () => controller.abort();
|
|
419
|
+
if (options.signal?.aborted)
|
|
420
|
+
controller.abort();
|
|
421
|
+
else
|
|
422
|
+
options.signal?.addEventListener('abort', onAbort, { once: true });
|
|
423
|
+
const timeout = setTimeout(() => controller.abort(), options.timeoutMs ?? 10_000);
|
|
424
|
+
timeout.unref();
|
|
417
425
|
try {
|
|
418
426
|
const fetchImpl = options.fetchImpl ?? fetch;
|
|
419
427
|
const response = await fetchImpl(options.usageUrl ?? CHATGPT_USAGE_URL, {
|
|
420
428
|
method: 'GET',
|
|
429
|
+
signal: controller.signal,
|
|
421
430
|
headers: {
|
|
422
431
|
Authorization: `Bearer ${accessToken}`,
|
|
423
432
|
Accept: 'application/json',
|
|
@@ -468,6 +477,7 @@ async function fetchCodexQuotaFromApi(options = {}) {
|
|
|
468
477
|
accessToken,
|
|
469
478
|
accountId: auth?.accountId,
|
|
470
479
|
capturedAt,
|
|
480
|
+
signal: controller.signal,
|
|
471
481
|
});
|
|
472
482
|
if (resetCredits) {
|
|
473
483
|
quota.resetCredits = resetCredits;
|
|
@@ -488,6 +498,10 @@ async function fetchCodexQuotaFromApi(options = {}) {
|
|
|
488
498
|
sevenDayLabel: 'Secondary',
|
|
489
499
|
};
|
|
490
500
|
}
|
|
501
|
+
finally {
|
|
502
|
+
clearTimeout(timeout);
|
|
503
|
+
options.signal?.removeEventListener('abort', onAbort);
|
|
504
|
+
}
|
|
491
505
|
}
|
|
492
506
|
async function fetchCodexResetCreditsFromApi(options = {}) {
|
|
493
507
|
const capturedAt = options.capturedAt ?? new Date().toISOString();
|
|
@@ -497,6 +511,14 @@ async function fetchCodexResetCreditsFromApi(options = {}) {
|
|
|
497
511
|
const accessToken = auth?.accessToken;
|
|
498
512
|
if (!accessToken)
|
|
499
513
|
return undefined;
|
|
514
|
+
const controller = new AbortController();
|
|
515
|
+
const onAbort = () => controller.abort();
|
|
516
|
+
if (options.signal?.aborted)
|
|
517
|
+
controller.abort();
|
|
518
|
+
else
|
|
519
|
+
options.signal?.addEventListener('abort', onAbort, { once: true });
|
|
520
|
+
const timeout = setTimeout(() => controller.abort(), options.timeoutMs ?? 10_000);
|
|
521
|
+
timeout.unref();
|
|
500
522
|
try {
|
|
501
523
|
const fetchImpl = options.fetchImpl ?? fetch;
|
|
502
524
|
const headers = {
|
|
@@ -511,6 +533,7 @@ async function fetchCodexResetCreditsFromApi(options = {}) {
|
|
|
511
533
|
const response = await fetchImpl(options.resetCreditsUrl ?? CHATGPT_RESET_CREDITS_URL, {
|
|
512
534
|
method: 'GET',
|
|
513
535
|
headers,
|
|
536
|
+
signal: controller.signal,
|
|
514
537
|
});
|
|
515
538
|
if (!response.ok)
|
|
516
539
|
return undefined;
|
|
@@ -519,6 +542,10 @@ async function fetchCodexResetCreditsFromApi(options = {}) {
|
|
|
519
542
|
catch {
|
|
520
543
|
return undefined;
|
|
521
544
|
}
|
|
545
|
+
finally {
|
|
546
|
+
clearTimeout(timeout);
|
|
547
|
+
options.signal?.removeEventListener('abort', onAbort);
|
|
548
|
+
}
|
|
522
549
|
}
|
|
523
550
|
function readCodexAuth(codexHome) {
|
|
524
551
|
try {
|
|
@@ -15,6 +15,9 @@ export interface CodexQuotaWatcherOptions {
|
|
|
15
15
|
discoveryPollIntervalMs?: number;
|
|
16
16
|
maxTailBytes?: number;
|
|
17
17
|
maxSessionFiles?: number;
|
|
18
|
+
/** Minimum interval between expensive rollout-tail fallback scans. */
|
|
19
|
+
localScanCacheMs?: number;
|
|
20
|
+
now?: () => number;
|
|
18
21
|
providerFactory?: () => CodexProvider;
|
|
19
22
|
getActiveAccount?: CodexAccountReader;
|
|
20
23
|
readSnapshot?: SnapshotReader;
|
|
@@ -39,6 +42,8 @@ export declare class CodexQuotaWatcher implements Disposable {
|
|
|
39
42
|
private readonly watchFile;
|
|
40
43
|
private readonly maxTailBytes;
|
|
41
44
|
private readonly maxSessionFiles;
|
|
45
|
+
private readonly localScanCacheMs;
|
|
46
|
+
private readonly now;
|
|
42
47
|
private readonly workspaceId;
|
|
43
48
|
private readonly appendHistorySample;
|
|
44
49
|
private readonly listeners;
|
|
@@ -48,6 +53,8 @@ export declare class CodexQuotaWatcher implements Disposable {
|
|
|
48
53
|
private fileWatcher;
|
|
49
54
|
private sessionPath;
|
|
50
55
|
private lastEmissionKey;
|
|
56
|
+
private lastLocalScanAt;
|
|
57
|
+
private lastLocalScanState;
|
|
51
58
|
private running;
|
|
52
59
|
constructor(workspacePath: string, options?: CodexQuotaWatcherOptions);
|
|
53
60
|
start(): void;
|
|
@@ -41,6 +41,7 @@ const quotaSnapshots_1 = require("./quotaSnapshots");
|
|
|
41
41
|
const quotaHistory_1 = require("./quotaHistory");
|
|
42
42
|
const codex_1 = require("./providers/codex");
|
|
43
43
|
const DEFAULT_DISCOVERY_POLL_INTERVAL_MS = 30_000;
|
|
44
|
+
const DEFAULT_LOCAL_SCAN_CACHE_MS = 5 * 60_000;
|
|
44
45
|
function accountEmail(account) {
|
|
45
46
|
return account?.email ?? account?.metadata?.email;
|
|
46
47
|
}
|
|
@@ -81,6 +82,8 @@ class CodexQuotaWatcher {
|
|
|
81
82
|
watchFile;
|
|
82
83
|
maxTailBytes;
|
|
83
84
|
maxSessionFiles;
|
|
85
|
+
localScanCacheMs;
|
|
86
|
+
now;
|
|
84
87
|
workspaceId;
|
|
85
88
|
appendHistorySample;
|
|
86
89
|
listeners = [];
|
|
@@ -90,6 +93,8 @@ class CodexQuotaWatcher {
|
|
|
90
93
|
fileWatcher = null;
|
|
91
94
|
sessionPath = null;
|
|
92
95
|
lastEmissionKey = null;
|
|
96
|
+
lastLocalScanAt = Number.NEGATIVE_INFINITY;
|
|
97
|
+
lastLocalScanState = null;
|
|
93
98
|
running = false;
|
|
94
99
|
constructor(workspacePath, options = {}) {
|
|
95
100
|
this.workspacePath = workspacePath;
|
|
@@ -110,6 +115,8 @@ class CodexQuotaWatcher {
|
|
|
110
115
|
this.watchFile = options.watchFile ?? fs.watch;
|
|
111
116
|
this.maxTailBytes = options.maxTailBytes;
|
|
112
117
|
this.maxSessionFiles = options.maxSessionFiles;
|
|
118
|
+
this.localScanCacheMs = options.localScanCacheMs ?? DEFAULT_LOCAL_SCAN_CACHE_MS;
|
|
119
|
+
this.now = options.now ?? Date.now;
|
|
113
120
|
this.workspaceId = options.workspaceId;
|
|
114
121
|
this.appendHistorySample = options.appendHistorySample ?? quotaHistory_1.appendQuotaHistorySample;
|
|
115
122
|
}
|
|
@@ -255,28 +262,39 @@ class CodexQuotaWatcher {
|
|
|
255
262
|
}
|
|
256
263
|
emitCachedOrUnavailable() {
|
|
257
264
|
const account = this.getActiveAccount();
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
workspacePath: this.workspacePath,
|
|
263
|
-
activeAccount: account,
|
|
264
|
-
readSnapshot: this.readSnapshot,
|
|
265
|
-
writeSnapshot: this.writeSnapshot,
|
|
266
|
-
provider: localProvider,
|
|
267
|
-
maxTailBytes: this.maxTailBytes,
|
|
268
|
-
maxSessionFiles: this.maxSessionFiles,
|
|
269
|
-
});
|
|
270
|
-
if (local) {
|
|
271
|
-
this.emitState(local);
|
|
265
|
+
const scanNow = this.now();
|
|
266
|
+
if (scanNow - this.lastLocalScanAt < this.localScanCacheMs) {
|
|
267
|
+
if (this.lastLocalScanState) {
|
|
268
|
+
this.emitState(this.lastLocalScanState);
|
|
272
269
|
return;
|
|
273
270
|
}
|
|
274
271
|
}
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
272
|
+
else {
|
|
273
|
+
let localProvider = null;
|
|
274
|
+
this.lastLocalScanAt = scanNow;
|
|
275
|
+
this.lastLocalScanState = null;
|
|
276
|
+
try {
|
|
277
|
+
localProvider = this.providerFactory();
|
|
278
|
+
this.lastLocalScanState = (0, codexQuota_1.resolveCodexQuotaFromLocalSources)({
|
|
279
|
+
workspacePath: this.workspacePath,
|
|
280
|
+
activeAccount: account,
|
|
281
|
+
readSnapshot: this.readSnapshot,
|
|
282
|
+
writeSnapshot: this.writeSnapshot,
|
|
283
|
+
provider: localProvider,
|
|
284
|
+
maxTailBytes: this.maxTailBytes,
|
|
285
|
+
maxSessionFiles: this.maxSessionFiles,
|
|
286
|
+
});
|
|
287
|
+
if (this.lastLocalScanState) {
|
|
288
|
+
this.emitState(this.lastLocalScanState);
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
catch {
|
|
293
|
+
// Fall through to account-scoped cache or unavailable state.
|
|
294
|
+
}
|
|
295
|
+
finally {
|
|
296
|
+
localProvider?.dispose();
|
|
297
|
+
}
|
|
280
298
|
}
|
|
281
299
|
const cached = account ? this.readSnapshot('codex', account.id) : null;
|
|
282
300
|
if (cached) {
|
|
@@ -57,6 +57,10 @@ function buildSessionContextSnapshot(events, options = {}) {
|
|
|
57
57
|
if (event.rateLimits)
|
|
58
58
|
lastRateLimits = event.rateLimits;
|
|
59
59
|
}
|
|
60
|
+
return assembleSessionContextSnapshot(aggregator, sourceState, lastPermissionMode, lastRateLimits, options);
|
|
61
|
+
}
|
|
62
|
+
function assembleSessionContextSnapshot(aggregator, sourceState, lastPermissionMode, lastRateLimits, options) {
|
|
63
|
+
const providerId = options.providerId;
|
|
60
64
|
const metrics = aggregator.getMetrics();
|
|
61
65
|
const model = options.model ?? metrics.currentModel ?? metrics.modelStats[0]?.model;
|
|
62
66
|
const contextWindow = options.contextWindow ??
|
|
@@ -100,24 +104,46 @@ function buildSessionContextSnapshot(events, options = {}) {
|
|
|
100
104
|
};
|
|
101
105
|
}
|
|
102
106
|
function createSessionContextProjector(options = {}) {
|
|
103
|
-
let
|
|
107
|
+
let aggregator = createProjectorAggregator(options);
|
|
108
|
+
let sourceState = createSourceExtractionState(options);
|
|
109
|
+
let lastPermissionMode;
|
|
110
|
+
let lastRateLimits;
|
|
111
|
+
const process = (event) => {
|
|
112
|
+
aggregator.processEvent(event);
|
|
113
|
+
extractSourcesFromEvent(event, sourceState);
|
|
114
|
+
if (event.permissionMode)
|
|
115
|
+
lastPermissionMode = event.permissionMode;
|
|
116
|
+
if (event.rateLimits)
|
|
117
|
+
lastRateLimits = event.rateLimits;
|
|
118
|
+
};
|
|
119
|
+
const snapshot = () => assembleSessionContextSnapshot(aggregator, sourceState, lastPermissionMode, lastRateLimits, options);
|
|
104
120
|
return {
|
|
105
121
|
processEvent(event) {
|
|
106
|
-
|
|
107
|
-
return
|
|
122
|
+
process(event);
|
|
123
|
+
return snapshot();
|
|
108
124
|
},
|
|
109
125
|
processEvents(newEvents) {
|
|
110
|
-
|
|
111
|
-
|
|
126
|
+
for (const event of newEvents)
|
|
127
|
+
process(event);
|
|
128
|
+
return snapshot();
|
|
112
129
|
},
|
|
113
130
|
getSnapshot() {
|
|
114
|
-
return
|
|
131
|
+
return snapshot();
|
|
115
132
|
},
|
|
116
133
|
reset() {
|
|
117
|
-
|
|
134
|
+
aggregator = createProjectorAggregator(options);
|
|
135
|
+
sourceState = createSourceExtractionState(options);
|
|
136
|
+
lastPermissionMode = undefined;
|
|
137
|
+
lastRateLimits = undefined;
|
|
118
138
|
},
|
|
119
139
|
};
|
|
120
140
|
}
|
|
141
|
+
function createProjectorAggregator(options) {
|
|
142
|
+
return new EventAggregator_1.EventAggregator({
|
|
143
|
+
providerId: options.providerId,
|
|
144
|
+
computeContextSize: options.computeContextSize,
|
|
145
|
+
});
|
|
146
|
+
}
|
|
121
147
|
function readSessionContextSnapshot(provider, sessionPath, options = {}) {
|
|
122
148
|
const reader = provider.createReader(sessionPath);
|
|
123
149
|
const events = reader.readAll();
|
|
@@ -177,7 +203,7 @@ function extractSourcesFromEvent(event, state) {
|
|
|
177
203
|
layer: 'summary',
|
|
178
204
|
sourceType: 'summary',
|
|
179
205
|
title: 'Context summary',
|
|
180
|
-
text: extractText(event.message
|
|
206
|
+
text: extractText(event.message?.content) || 'Context compacted',
|
|
181
207
|
});
|
|
182
208
|
break;
|
|
183
209
|
}
|
|
@@ -394,7 +420,7 @@ function limitSources(sources, limit) {
|
|
|
394
420
|
}
|
|
395
421
|
function latestModel(events) {
|
|
396
422
|
for (let i = events.length - 1; i >= 0; i--) {
|
|
397
|
-
const model = events[i].message
|
|
423
|
+
const model = events[i].message?.model;
|
|
398
424
|
if (model)
|
|
399
425
|
return model;
|
|
400
426
|
}
|
package/dist/credentialIO.js
CHANGED
|
@@ -52,6 +52,12 @@ const os = __importStar(require("os"));
|
|
|
52
52
|
const child_process_1 = require("child_process");
|
|
53
53
|
const claudeProfiles_1 = require("./claudeProfiles");
|
|
54
54
|
exports.KEYCHAIN_SERVICE = 'Claude Code-credentials';
|
|
55
|
+
function quoteSecurityInteractiveArg(value) {
|
|
56
|
+
// `security -i` is not a POSIX shell: adjacent quoted segments do NOT
|
|
57
|
+
// concatenate (so the shell-style '\'' splice splits the token), and
|
|
58
|
+
// backslash escapes the next character even inside single quotes.
|
|
59
|
+
return `'${value.replace(/\\/g, '\\\\').replace(/'/g, `\\'`)}'`;
|
|
60
|
+
}
|
|
55
61
|
function getCredentialsFilePath(configDir) {
|
|
56
62
|
return path.join(configDir ?? path.join(os.homedir(), '.claude'), '.credentials.json');
|
|
57
63
|
}
|
|
@@ -93,16 +99,24 @@ function writeActiveCredentials(credentials, configDir) {
|
|
|
93
99
|
const json = JSON.stringify(credentials);
|
|
94
100
|
JSON.parse(json); // validate round-trip
|
|
95
101
|
if (process.platform === 'darwin') {
|
|
96
|
-
(0,
|
|
102
|
+
const service = (0, claudeProfiles_1.claudeKeychainService)(configDir);
|
|
103
|
+
const account = process.env.USER || 'user';
|
|
104
|
+
const command = [
|
|
97
105
|
'add-generic-password',
|
|
98
106
|
'-U',
|
|
99
107
|
'-s',
|
|
100
|
-
(
|
|
108
|
+
quoteSecurityInteractiveArg(service),
|
|
101
109
|
'-a',
|
|
102
|
-
|
|
110
|
+
quoteSecurityInteractiveArg(account),
|
|
103
111
|
'-w',
|
|
104
|
-
json,
|
|
105
|
-
]
|
|
112
|
+
quoteSecurityInteractiveArg(json),
|
|
113
|
+
].join(' ');
|
|
114
|
+
(0, child_process_1.execFileSync)('security', ['-i'], {
|
|
115
|
+
input: command + '\n',
|
|
116
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
117
|
+
timeout: 4000,
|
|
118
|
+
killSignal: 'SIGKILL',
|
|
119
|
+
});
|
|
106
120
|
return;
|
|
107
121
|
}
|
|
108
122
|
// Linux / WSL / Windows — file-based
|