sidekick-shared 0.25.0 → 0.26.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/CHANGELOG.md +65 -0
- package/README.md +50 -3
- package/dist/accountChanges.js +22 -3
- package/dist/accountRegistry.d.ts +22 -1
- package/dist/accountRegistry.js +37 -14
- package/dist/accountStatus.d.ts +9 -1
- package/dist/accountStatus.js +3 -3
- package/dist/accounts.d.ts +3 -1
- package/dist/accounts.js +84 -64
- package/dist/aggregation/EventAggregator.d.ts +15 -2
- package/dist/aggregation/EventAggregator.js +54 -9
- package/dist/aggregation/costProvenance.d.ts +14 -0
- package/dist/aggregation/costProvenance.js +35 -0
- package/dist/aggregation/snapshot.d.ts +13 -2
- package/dist/aggregation/snapshot.js +57 -6
- package/dist/aggregation/types.d.ts +30 -1
- package/dist/analytics/compactionLedger.js +15 -5
- package/dist/analytics/qualityScore.js +2 -1
- package/dist/browser.d.ts +11 -1
- package/dist/browser.js +29 -1
- package/dist/codexProfiles.d.ts +3 -1
- package/dist/codexProfiles.js +4 -2
- package/dist/codexQuota.d.ts +2 -0
- package/dist/codexQuota.js +11 -66
- package/dist/failingTools.d.ts +37 -0
- package/dist/failingTools.js +70 -0
- package/dist/formatters/sessionDump.js +13 -7
- package/dist/formatting.d.ts +13 -0
- package/dist/formatting.js +35 -0
- package/dist/historicalStore.d.ts +47 -0
- package/dist/historicalStore.js +312 -0
- package/dist/index.d.ts +46 -12
- package/dist/index.js +112 -24
- package/dist/modelContext.js +8 -3
- package/dist/modelInfo.js +45 -27
- package/dist/node.d.ts +19 -0
- package/dist/node.js +47 -1
- package/dist/observedContextWindows.d.ts +7 -0
- package/dist/observedContextWindows.js +17 -2
- package/dist/observedSessionCollector.d.ts +3 -8
- package/dist/observedSessionCollector.js +10 -27
- package/dist/parsers/sessionActivityDetector.d.ts +24 -2
- package/dist/parsers/sessionActivityDetector.js +95 -17
- package/dist/parsers/sessionPathResolver.d.ts +11 -0
- package/dist/parsers/sessionPathResolver.js +13 -10
- package/dist/peakHours.d.ts +34 -3
- package/dist/peakHours.js +92 -7
- package/dist/pricingCatalog.d.ts +7 -0
- package/dist/pricingCatalog.js +8 -5
- package/dist/providers/claudeCode.js +11 -96
- package/dist/providers/codex.d.ts +2 -0
- package/dist/providers/codex.js +94 -259
- package/dist/providers/detect.d.ts +2 -0
- package/dist/providers/detect.js +29 -24
- package/dist/providers/factory.d.ts +6 -0
- package/dist/providers/openCode.d.ts +2 -0
- package/dist/providers/openCode.js +105 -121
- package/dist/providers/rolloutWalker.d.ts +53 -0
- package/dist/providers/rolloutWalker.js +209 -0
- package/dist/providers/types.d.ts +30 -0
- package/dist/quota.d.ts +30 -3
- package/dist/quota.js +35 -3
- package/dist/quotaHistory.d.ts +1 -1
- package/dist/quotaHistory.js +36 -35
- package/dist/quotaPresentation.js +17 -5
- package/dist/quotaResolver.d.ts +83 -0
- package/dist/quotaResolver.js +234 -0
- package/dist/quotaSnapshots.d.ts +15 -2
- package/dist/quotaSnapshots.js +25 -3
- package/dist/quotaThresholds.d.ts +60 -0
- package/dist/quotaThresholds.js +90 -0
- package/dist/report/htmlReportGenerator.js +10 -4
- package/dist/report/sessionReportInputs.d.ts +23 -0
- package/dist/report/sessionReportInputs.js +34 -0
- package/dist/schemas/index.d.ts +2 -0
- package/dist/schemas/index.js +10 -1
- package/dist/schemas/quota.d.ts +139 -0
- package/dist/schemas/quota.js +17 -2
- package/dist/schemas/quotaHistory.d.ts +1 -0
- package/dist/schemas/quotaHistory.js +1 -1
- package/dist/schemas/sessionEvent.d.ts +6 -0
- package/dist/schemas/sessionEvent.js +1 -0
- package/dist/schemas/stateFile.d.ts +198 -0
- package/dist/schemas/stateFile.js +77 -0
- package/dist/schemas/transcript.js +2 -0
- package/dist/sessionFingerprint.d.ts +25 -0
- package/dist/sessionFingerprint.js +46 -0
- package/dist/sessionPreviews.d.ts +7 -1
- package/dist/sessionPreviews.js +15 -3
- package/dist/sessionStats.d.ts +51 -0
- package/dist/sessionStats.js +166 -0
- package/dist/stateFile.d.ts +113 -0
- package/dist/stateFile.js +160 -0
- package/dist/statusline/claudeStatuslinePayload.d.ts +89 -0
- package/dist/statusline/claudeStatuslinePayload.js +152 -0
- package/dist/statusline/formatter.d.ts +13 -0
- package/dist/statusline/formatter.js +48 -6
- package/dist/statusline/index.d.ts +10 -2
- package/dist/statusline/index.js +19 -1
- package/dist/tokenSummary.d.ts +62 -0
- package/dist/tokenSummary.js +72 -0
- package/dist/transcript.d.ts +5 -0
- package/dist/transcript.js +2 -0
- package/dist/types/observedSessionV1.d.ts +2 -0
- package/dist/types/observedSessionV1.js +75 -18
- package/dist/types/sessionEvent.d.ts +2 -0
- package/dist/usage/billingBlocks.d.ts +85 -0
- package/dist/usage/billingBlocks.js +153 -0
- package/dist/usage/importSessionHistory.d.ts +53 -0
- package/dist/usage/importSessionHistory.js +88 -0
- package/dist/usage/usageEvents.d.ts +92 -0
- package/dist/usage/usageEvents.js +322 -0
- package/dist/usage/usageReports.d.ts +76 -0
- package/dist/usage/usageReports.js +208 -0
- package/dist/watchers/factory.d.ts +6 -0
- package/dist/watchers/factory.js +30 -25
- package/dist/zaiQuotaApi.d.ts +2 -0
- package/dist/zaiQuotaApi.js +2 -0
- package/package.json +1 -1
|
@@ -64,6 +64,7 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
64
64
|
};
|
|
65
65
|
})();
|
|
66
66
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
67
|
+
exports.OBSERVED_CONTEXT_WINDOW_TTL_MS = void 0;
|
|
67
68
|
exports.getObservedContextWindowPath = getObservedContextWindowPath;
|
|
68
69
|
exports._clearObservedContextWindowWriteCache = _clearObservedContextWindowWriteCache;
|
|
69
70
|
exports.loadObservedContextWindows = loadObservedContextWindows;
|
|
@@ -76,6 +77,13 @@ const atomic_1 = require("./writers/atomic");
|
|
|
76
77
|
// ── Constants ──
|
|
77
78
|
const STORE_FILE_NAME = 'observed-context-windows.json';
|
|
78
79
|
const STORE_VERSION = 1;
|
|
80
|
+
/**
|
|
81
|
+
* Observations older than this are ignored on load. A provider reports its
|
|
82
|
+
* context window on every session, so a live model refreshes its entry
|
|
83
|
+
* constantly; an entry that has gone quiet this long belongs to a tier or
|
|
84
|
+
* model the user no longer runs and must not outrank the catalog forever.
|
|
85
|
+
*/
|
|
86
|
+
exports.OBSERVED_CONTEXT_WINDOW_TTL_MS = 30 * 86_400_000;
|
|
79
87
|
/** Resolve the on-disk store path. */
|
|
80
88
|
function getObservedContextWindowPath(cacheDir) {
|
|
81
89
|
return path.join(cacheDir ?? (0, paths_1.getConfigDir)(), STORE_FILE_NAME);
|
|
@@ -98,13 +106,20 @@ const lastPersistedByStore = new Map();
|
|
|
98
106
|
function _clearObservedContextWindowWriteCache() {
|
|
99
107
|
lastPersistedByStore.clear();
|
|
100
108
|
}
|
|
101
|
-
/**
|
|
102
|
-
|
|
109
|
+
/**
|
|
110
|
+
* Keep only well-formed, recent entries; a hand-edited or partly-corrupt file
|
|
111
|
+
* shouldn't poison lookups, and a stale observation shouldn't outrank the
|
|
112
|
+
* catalog (see {@link OBSERVED_CONTEXT_WINDOW_TTL_MS}).
|
|
113
|
+
*/
|
|
114
|
+
function toOverrideMap(store, now = Date.now()) {
|
|
103
115
|
const out = {};
|
|
104
116
|
for (const [modelId, entry] of Object.entries(store.models ?? {})) {
|
|
105
117
|
const window = entry?.contextWindow;
|
|
106
118
|
if (typeof window !== 'number' || !Number.isFinite(window) || window <= 0)
|
|
107
119
|
continue;
|
|
120
|
+
const observedAt = typeof entry.observedAt === 'string' ? Date.parse(entry.observedAt) : NaN;
|
|
121
|
+
if (Number.isFinite(observedAt) && now - observedAt > exports.OBSERVED_CONTEXT_WINDOW_TTL_MS)
|
|
122
|
+
continue;
|
|
108
123
|
out[modelId] = window;
|
|
109
124
|
}
|
|
110
125
|
return out;
|
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
import type { ObservedAgentSessionV1, ProviderCapabilitiesV1, ProviderSessionAdapterV1Options } from './types/observedSessionV1';
|
|
2
2
|
import type { SessionProviderBase } from './providers/types';
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
}
|
|
3
|
+
import type { ObservedSessionFingerprintParts } from './sessionFingerprint';
|
|
4
|
+
export { fileFingerprint, fileFingerprintParts } from './sessionFingerprint';
|
|
5
|
+
export type { ObservedSessionFingerprintParts } from './sessionFingerprint';
|
|
7
6
|
export interface ObservedSessionReference {
|
|
8
7
|
sessionId: string;
|
|
9
8
|
/** Opaque source locator used only by the source implementation. Never emitted in diagnostics. */
|
|
@@ -136,7 +135,3 @@ export declare class ObservedSessionCollector<T = unknown> {
|
|
|
136
135
|
* one malformed session cannot reject discovery of its healthy siblings.
|
|
137
136
|
*/
|
|
138
137
|
export declare function observedSessionSourceFromProvider(provider: SessionProviderBase, cwd: string, options?: ProviderSessionAdapterV1Options): ProviderObservedSessionCollectionSource;
|
|
139
|
-
/** Structured size/mtime fingerprint; returns null for inaccessible or synthetic paths. */
|
|
140
|
-
export declare function fileFingerprintParts(sourcePath: string): ObservedSessionFingerprintParts | null;
|
|
141
|
-
/** Size/mtime fingerprint suitable for retry bypass; returns null for synthetic DB locators. */
|
|
142
|
-
export declare function fileFingerprint(sourcePath: string): string | null;
|
|
@@ -33,14 +33,15 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
33
33
|
};
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
-
exports.ObservedSessionCollector = void 0;
|
|
36
|
+
exports.ObservedSessionCollector = exports.fileFingerprintParts = exports.fileFingerprint = void 0;
|
|
37
37
|
exports.observedSessionSourceFromProvider = observedSessionSourceFromProvider;
|
|
38
|
-
exports.fileFingerprintParts = fileFingerprintParts;
|
|
39
|
-
exports.fileFingerprint = fileFingerprint;
|
|
40
38
|
const fs = __importStar(require("node:fs"));
|
|
41
|
-
const node_fs_1 = require("node:fs");
|
|
42
39
|
const observedSessionV1_1 = require("./types/observedSessionV1");
|
|
43
40
|
const sessionActivityDetector_1 = require("./parsers/sessionActivityDetector");
|
|
41
|
+
const sessionFingerprint_1 = require("./sessionFingerprint");
|
|
42
|
+
var sessionFingerprint_2 = require("./sessionFingerprint");
|
|
43
|
+
Object.defineProperty(exports, "fileFingerprint", { enumerable: true, get: function () { return sessionFingerprint_2.fileFingerprint; } });
|
|
44
|
+
Object.defineProperty(exports, "fileFingerprintParts", { enumerable: true, get: function () { return sessionFingerprint_2.fileFingerprintParts; } });
|
|
44
45
|
const DEFAULT_MAX_CONCURRENT_READS = 4;
|
|
45
46
|
const MAX_CONCURRENT_READS = 16;
|
|
46
47
|
const DEFAULT_DEBOUNCE_MS = 100;
|
|
@@ -444,12 +445,12 @@ function observedSessionSourceFromProvider(provider, cwd, options = {}) {
|
|
|
444
445
|
const referenceFromPath = (sessionPath, sessionId = provider.getSessionId(sessionPath), suppliedParts) => {
|
|
445
446
|
const metadata = suppliedParts ? null : provider.getSessionMetadata?.(sessionPath);
|
|
446
447
|
const parts = suppliedParts ??
|
|
447
|
-
fileFingerprintParts(sessionPath) ??
|
|
448
|
+
(0, sessionFingerprint_1.fileFingerprintParts)(sessionPath) ??
|
|
448
449
|
(metadata ? { sizeBytes: 0, mtimeMs: metadata.mtime.getTime() } : null);
|
|
449
450
|
return {
|
|
450
451
|
sessionId,
|
|
451
452
|
sourceKey: sessionPath,
|
|
452
|
-
fingerprintHint: parts ? fingerprintString(parts) : undefined,
|
|
453
|
+
fingerprintHint: parts ? (0, sessionFingerprint_1.fingerprintString)(parts) : undefined,
|
|
453
454
|
fingerprintParts: parts ?? undefined,
|
|
454
455
|
};
|
|
455
456
|
};
|
|
@@ -472,12 +473,12 @@ function observedSessionSourceFromProvider(provider, cwd, options = {}) {
|
|
|
472
473
|
}
|
|
473
474
|
return provider.findAllSessions(cwd).map((sessionPath) => {
|
|
474
475
|
const metadata = provider.getSessionMetadata?.(sessionPath);
|
|
475
|
-
const parts = fileFingerprintParts(sessionPath) ??
|
|
476
|
+
const parts = (0, sessionFingerprint_1.fileFingerprintParts)(sessionPath) ??
|
|
476
477
|
(metadata ? { sizeBytes: 0, mtimeMs: metadata.mtime.getTime() } : null);
|
|
477
478
|
return {
|
|
478
479
|
sessionId: provider.getSessionId(sessionPath),
|
|
479
480
|
sourceKey: sessionPath,
|
|
480
|
-
fingerprintHint: parts ? fingerprintString(parts) : undefined,
|
|
481
|
+
fingerprintHint: parts ? (0, sessionFingerprint_1.fingerprintString)(parts) : undefined,
|
|
481
482
|
fingerprintParts: parts ?? undefined,
|
|
482
483
|
};
|
|
483
484
|
});
|
|
@@ -493,7 +494,7 @@ function observedSessionSourceFromProvider(provider, cwd, options = {}) {
|
|
|
493
494
|
},
|
|
494
495
|
refreshCached(reference, cached, observedAt) {
|
|
495
496
|
const mtimeMs = reference.fingerprintParts?.mtimeMs ?? Date.parse(cached.observedAt);
|
|
496
|
-
const activity = (0, sessionActivityDetector_1.refreshSessionActivityState)(cached.activity.value, mtimeMs);
|
|
497
|
+
const activity = (0, sessionActivityDetector_1.refreshSessionActivityState)(cached.activity.value, mtimeMs, Date.now(), (0, observedSessionV1_1.getObservedActivityReason)(cached));
|
|
497
498
|
return {
|
|
498
499
|
...cached,
|
|
499
500
|
activity: { ...cached.activity, value: activity },
|
|
@@ -506,24 +507,6 @@ function observedSessionSourceFromProvider(provider, cwd, options = {}) {
|
|
|
506
507
|
dispose: () => adapter.dispose(),
|
|
507
508
|
};
|
|
508
509
|
}
|
|
509
|
-
/** Structured size/mtime fingerprint; returns null for inaccessible or synthetic paths. */
|
|
510
|
-
function fileFingerprintParts(sourcePath) {
|
|
511
|
-
try {
|
|
512
|
-
const stat = (0, node_fs_1.statSync)(sourcePath);
|
|
513
|
-
return { sizeBytes: stat.size, mtimeMs: stat.mtimeMs };
|
|
514
|
-
}
|
|
515
|
-
catch {
|
|
516
|
-
return null;
|
|
517
|
-
}
|
|
518
|
-
}
|
|
519
|
-
/** Size/mtime fingerprint suitable for retry bypass; returns null for synthetic DB locators. */
|
|
520
|
-
function fileFingerprint(sourcePath) {
|
|
521
|
-
const parts = fileFingerprintParts(sourcePath);
|
|
522
|
-
return parts ? fingerprintString(parts) : null;
|
|
523
|
-
}
|
|
524
|
-
function fingerprintString(parts) {
|
|
525
|
-
return `${parts.sizeBytes}:${parts.mtimeMs}`;
|
|
526
|
-
}
|
|
527
510
|
function parseFingerprintParts(value) {
|
|
528
511
|
if (!value)
|
|
529
512
|
return null;
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
*
|
|
10
10
|
* @module parsers/sessionActivityDetector
|
|
11
11
|
*/
|
|
12
|
+
import type { SessionEvent } from '../types/sessionEvent';
|
|
12
13
|
export type SessionActivityState = 'ongoing' | 'ended' | 'stale';
|
|
13
14
|
export interface SessionActivityResult {
|
|
14
15
|
state: SessionActivityState;
|
|
@@ -17,6 +18,10 @@ export interface SessionActivityResult {
|
|
|
17
18
|
/** Reason for the classification */
|
|
18
19
|
reason: string;
|
|
19
20
|
}
|
|
21
|
+
/** Grace period to prevent spinner flicker (ms) */
|
|
22
|
+
export declare const SESSION_ACTIVITY_GRACE_PERIOD_MS = 5000;
|
|
23
|
+
/** If mtime is older than this, session is stale regardless of content */
|
|
24
|
+
export declare const SESSION_ACTIVITY_STALENESS_MS: number;
|
|
20
25
|
/**
|
|
21
26
|
* Determines whether a session is ongoing, ended, or stale.
|
|
22
27
|
*
|
|
@@ -29,9 +34,26 @@ export interface SessionActivityResult {
|
|
|
29
34
|
* @returns Activity state with classification reason
|
|
30
35
|
*/
|
|
31
36
|
export declare function detectSessionActivity(sessionPath: string): SessionActivityResult;
|
|
37
|
+
export interface ClassifySessionActivityOptions {
|
|
38
|
+
/** Canonical events already read for the session; only the tail is inspected. */
|
|
39
|
+
events: readonly SessionEvent[];
|
|
40
|
+
/** Source mtime; the last event's timestamp is used when unknown. */
|
|
41
|
+
mtimeMs: number | null | undefined;
|
|
42
|
+
/** Clock (default `Date.now()`). */
|
|
43
|
+
now?: number;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Classify a session from events already in memory, with the same states,
|
|
47
|
+
* reasons, grace period, and staleness rule as {@link detectSessionActivity},
|
|
48
|
+
* but without opening the file again. Works for every provider (the raw
|
|
49
|
+
* tail patterns only ever matched Claude Code JSONL) and for database-backed
|
|
50
|
+
* sessions that have no file to tail.
|
|
51
|
+
*/
|
|
52
|
+
export declare function classifySessionActivity(options: ClassifySessionActivityOptions): SessionActivityResult;
|
|
32
53
|
/**
|
|
33
54
|
* Refresh a cached classification from its unchanged fingerprint without
|
|
34
55
|
* re-reading the transcript. An active file remains active until it crosses
|
|
35
|
-
* the same five-minute staleness boundary used by detectSessionActivity
|
|
56
|
+
* the same five-minute staleness boundary used by detectSessionActivity,
|
|
57
|
+
* unless it was active only by grace period (`previousReason`).
|
|
36
58
|
*/
|
|
37
|
-
export declare function refreshSessionActivityState(previous: 'active' | 'idle' | 'ended' | 'unknown', mtimeMs: number, nowMs?: number): 'active' | 'idle' | 'ended' | 'unknown';
|
|
59
|
+
export declare function refreshSessionActivityState(previous: 'active' | 'idle' | 'ended' | 'unknown', mtimeMs: number, nowMs?: number, previousReason?: string): 'active' | 'idle' | 'ended' | 'unknown';
|
|
@@ -44,16 +44,22 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
44
44
|
};
|
|
45
45
|
})();
|
|
46
46
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
47
|
+
exports.SESSION_ACTIVITY_STALENESS_MS = exports.SESSION_ACTIVITY_GRACE_PERIOD_MS = void 0;
|
|
47
48
|
exports.detectSessionActivity = detectSessionActivity;
|
|
49
|
+
exports.classifySessionActivity = classifySessionActivity;
|
|
48
50
|
exports.refreshSessionActivityState = refreshSessionActivityState;
|
|
49
51
|
const fs = __importStar(require("fs"));
|
|
50
52
|
// ── Constants ──
|
|
51
53
|
/** How many bytes to read from the end of the file for analysis */
|
|
52
54
|
const TAIL_BYTES = 32 * 1024;
|
|
53
55
|
/** Grace period to prevent spinner flicker (ms) */
|
|
54
|
-
|
|
56
|
+
exports.SESSION_ACTIVITY_GRACE_PERIOD_MS = 5_000;
|
|
57
|
+
const GRACE_PERIOD_MS = exports.SESSION_ACTIVITY_GRACE_PERIOD_MS;
|
|
55
58
|
/** If mtime is older than this, session is stale regardless of content */
|
|
56
|
-
|
|
59
|
+
exports.SESSION_ACTIVITY_STALENESS_MS = 5 * 60 * 1000;
|
|
60
|
+
const STALENESS_THRESHOLD_MS = exports.SESSION_ACTIVITY_STALENESS_MS;
|
|
61
|
+
/** How many trailing events the in-memory classifier inspects (about a 32 KiB tail). */
|
|
62
|
+
const TAIL_EVENTS = 200;
|
|
57
63
|
// ── Event Classification Patterns ──
|
|
58
64
|
/** Patterns indicating the AI is actively working */
|
|
59
65
|
const AI_ACTIVITY_PATTERNS = [
|
|
@@ -106,7 +112,9 @@ function detectSessionActivity(sessionPath) {
|
|
|
106
112
|
if (lines.length === 0) {
|
|
107
113
|
return { state: 'ended', lastActivityTime: new Date(stat.mtimeMs), reason: 'no-events' };
|
|
108
114
|
}
|
|
109
|
-
|
|
115
|
+
return classifyFromSignals(signalsFromLines(lines), stat.mtimeMs, mtimeAge);
|
|
116
|
+
}
|
|
117
|
+
function signalsFromLines(lines) {
|
|
110
118
|
let lastAiActivityIndex = -1;
|
|
111
119
|
let lastEndingIndex = -1;
|
|
112
120
|
let hasTerminal = false;
|
|
@@ -136,38 +144,101 @@ function detectSessionActivity(sessionPath) {
|
|
|
136
144
|
}
|
|
137
145
|
}
|
|
138
146
|
}
|
|
147
|
+
return { lastAiActivityIndex, lastEndingIndex, hasTerminal };
|
|
148
|
+
}
|
|
149
|
+
function hasToolResultBlock(event) {
|
|
150
|
+
const content = event.message?.content;
|
|
151
|
+
return (Array.isArray(content) &&
|
|
152
|
+
content.some((block) => typeof block === 'object' &&
|
|
153
|
+
block !== null &&
|
|
154
|
+
block.type === 'tool_result'));
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* The same signals the tail scanner derives from raw JSONL lines, read from
|
|
158
|
+
* canonical events instead: assistant, tool-use, and tool-result events (and
|
|
159
|
+
* user events carrying tool results) are AI activity; an assistant turn that
|
|
160
|
+
* stopped with `end_turn` or a user prompt is an ending event.
|
|
161
|
+
*/
|
|
162
|
+
function signalsFromEvents(events) {
|
|
163
|
+
let lastAiActivityIndex = -1;
|
|
164
|
+
let lastEndingIndex = -1;
|
|
165
|
+
for (let i = 0; i < events.length; i++) {
|
|
166
|
+
const event = events[i];
|
|
167
|
+
const carriesToolResult = event.type === 'user' && hasToolResultBlock(event);
|
|
168
|
+
if (event.type === 'assistant' ||
|
|
169
|
+
event.type === 'tool_use' ||
|
|
170
|
+
event.type === 'tool_result' ||
|
|
171
|
+
carriesToolResult) {
|
|
172
|
+
lastAiActivityIndex = i;
|
|
173
|
+
}
|
|
174
|
+
if ((event.type === 'assistant' && event.message?.stop_reason === 'end_turn') ||
|
|
175
|
+
(event.type === 'user' && !carriesToolResult)) {
|
|
176
|
+
lastEndingIndex = i;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
return { lastAiActivityIndex, lastEndingIndex, hasTerminal: false };
|
|
180
|
+
}
|
|
181
|
+
function classifyFromSignals(signals, mtimeMs, mtimeAge) {
|
|
182
|
+
const lastActivityTime = new Date(mtimeMs);
|
|
183
|
+
const { lastAiActivityIndex, lastEndingIndex, hasTerminal } = signals;
|
|
139
184
|
// Terminal patterns mean the session is definitely done
|
|
140
185
|
if (hasTerminal) {
|
|
141
|
-
return { state: 'ended', lastActivityTime
|
|
186
|
+
return { state: 'ended', lastActivityTime, reason: 'terminal-event' };
|
|
142
187
|
}
|
|
143
|
-
// AI activity after the last ending event
|
|
188
|
+
// AI activity after the last ending event -> ongoing
|
|
144
189
|
if (lastAiActivityIndex > lastEndingIndex) {
|
|
145
|
-
return {
|
|
146
|
-
state: 'ongoing',
|
|
147
|
-
lastActivityTime: new Date(stat.mtimeMs),
|
|
148
|
-
reason: 'ai-activity-after-ending',
|
|
149
|
-
};
|
|
190
|
+
return { state: 'ongoing', lastActivityTime, reason: 'ai-activity-after-ending' };
|
|
150
191
|
}
|
|
151
192
|
// Ending event with no subsequent AI activity
|
|
152
193
|
if (lastEndingIndex >= 0) {
|
|
153
194
|
// Apply grace period to prevent flicker
|
|
154
195
|
if (mtimeAge < GRACE_PERIOD_MS) {
|
|
155
|
-
return { state: 'ongoing', lastActivityTime
|
|
196
|
+
return { state: 'ongoing', lastActivityTime, reason: 'grace-period' };
|
|
156
197
|
}
|
|
157
|
-
return { state: 'ended', lastActivityTime
|
|
198
|
+
return { state: 'ended', lastActivityTime, reason: 'ending-event' };
|
|
158
199
|
}
|
|
159
|
-
// No clear signal
|
|
200
|
+
// No clear signal: if recently modified, assume ongoing
|
|
160
201
|
if (mtimeAge < GRACE_PERIOD_MS) {
|
|
161
|
-
return { state: 'ongoing', lastActivityTime
|
|
202
|
+
return { state: 'ongoing', lastActivityTime, reason: 'recent-mtime' };
|
|
203
|
+
}
|
|
204
|
+
return { state: 'ended', lastActivityTime, reason: 'no-activity-signal' };
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* Classify a session from events already in memory, with the same states,
|
|
208
|
+
* reasons, grace period, and staleness rule as {@link detectSessionActivity},
|
|
209
|
+
* but without opening the file again. Works for every provider (the raw
|
|
210
|
+
* tail patterns only ever matched Claude Code JSONL) and for database-backed
|
|
211
|
+
* sessions that have no file to tail.
|
|
212
|
+
*/
|
|
213
|
+
function classifySessionActivity(options) {
|
|
214
|
+
const now = options.now ?? Date.now();
|
|
215
|
+
const events = options.events;
|
|
216
|
+
const lastEventMs = events.length > 0 ? Date.parse(events[events.length - 1].timestamp) : NaN;
|
|
217
|
+
const mtimeMs = typeof options.mtimeMs === 'number' && Number.isFinite(options.mtimeMs)
|
|
218
|
+
? options.mtimeMs
|
|
219
|
+
: Number.isFinite(lastEventMs)
|
|
220
|
+
? lastEventMs
|
|
221
|
+
: null;
|
|
222
|
+
if (mtimeMs === null) {
|
|
223
|
+
return { state: 'ended', lastActivityTime: null, reason: 'no-events' };
|
|
162
224
|
}
|
|
163
|
-
|
|
225
|
+
const mtimeAge = now - mtimeMs;
|
|
226
|
+
if (mtimeAge > STALENESS_THRESHOLD_MS) {
|
|
227
|
+
return { state: 'stale', lastActivityTime: new Date(mtimeMs), reason: 'mtime-stale' };
|
|
228
|
+
}
|
|
229
|
+
if (events.length === 0) {
|
|
230
|
+
return { state: 'ended', lastActivityTime: new Date(mtimeMs), reason: 'no-events' };
|
|
231
|
+
}
|
|
232
|
+
const tail = events.length > TAIL_EVENTS ? events.slice(-TAIL_EVENTS) : events;
|
|
233
|
+
return classifyFromSignals(signalsFromEvents(tail), mtimeMs, mtimeAge);
|
|
164
234
|
}
|
|
165
235
|
/**
|
|
166
236
|
* Refresh a cached classification from its unchanged fingerprint without
|
|
167
237
|
* re-reading the transcript. An active file remains active until it crosses
|
|
168
|
-
* the same five-minute staleness boundary used by detectSessionActivity
|
|
238
|
+
* the same five-minute staleness boundary used by detectSessionActivity,
|
|
239
|
+
* unless it was active only by grace period (`previousReason`).
|
|
169
240
|
*/
|
|
170
|
-
function refreshSessionActivityState(previous, mtimeMs, nowMs = Date.now()) {
|
|
241
|
+
function refreshSessionActivityState(previous, mtimeMs, nowMs = Date.now(), previousReason) {
|
|
171
242
|
if (!Number.isFinite(mtimeMs))
|
|
172
243
|
return previous;
|
|
173
244
|
if (nowMs - mtimeMs > STALENESS_THRESHOLD_MS)
|
|
@@ -176,6 +247,13 @@ function refreshSessionActivityState(previous, mtimeMs, nowMs = Date.now()) {
|
|
|
176
247
|
return 'ended';
|
|
177
248
|
if (previous === 'idle')
|
|
178
249
|
return 'idle';
|
|
250
|
+
// A session that was only active thanks to the grace period ends once the
|
|
251
|
+
// grace period lapses, exactly as a fresh classification would say.
|
|
252
|
+
if (previous === 'active' &&
|
|
253
|
+
(previousReason === 'grace-period' || previousReason === 'recent-mtime') &&
|
|
254
|
+
nowMs - mtimeMs >= GRACE_PERIOD_MS) {
|
|
255
|
+
return 'ended';
|
|
256
|
+
}
|
|
179
257
|
return previous === 'unknown' ? 'unknown' : 'active';
|
|
180
258
|
}
|
|
181
259
|
// ── Helpers ──
|
|
@@ -205,6 +205,17 @@ export declare function getAllProjectFolders(workspacePath?: string): ProjectFol
|
|
|
205
205
|
* ```
|
|
206
206
|
*/
|
|
207
207
|
export declare function findSessionsInDirectory(sessionDir: string): string[];
|
|
208
|
+
export interface SessionFileStat {
|
|
209
|
+
path: string;
|
|
210
|
+
mtime: Date;
|
|
211
|
+
sizeBytes: number;
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* The non-empty `.jsonl` files in a session directory with the stat each one
|
|
215
|
+
* already cost, newest first, so callers that need the mtime do not stat the
|
|
216
|
+
* same files a second time.
|
|
217
|
+
*/
|
|
218
|
+
export declare function findSessionFilesWithStats(sessionDir: string): SessionFileStat[];
|
|
208
219
|
/**
|
|
209
220
|
* Detects if a workspace is a git worktree and resolves the main repo path.
|
|
210
221
|
*
|
|
@@ -54,6 +54,7 @@ exports.findAllSessions = findAllSessions;
|
|
|
54
54
|
exports.decodeEncodedPath = decodeEncodedPath;
|
|
55
55
|
exports.getAllProjectFolders = getAllProjectFolders;
|
|
56
56
|
exports.findSessionsInDirectory = findSessionsInDirectory;
|
|
57
|
+
exports.findSessionFilesWithStats = findSessionFilesWithStats;
|
|
57
58
|
exports.resolveWorktreeMainRepo = resolveWorktreeMainRepo;
|
|
58
59
|
exports.discoverWorktreeSiblings = discoverWorktreeSiblings;
|
|
59
60
|
exports.findAllSessionsWithWorktrees = findAllSessionsWithWorktrees;
|
|
@@ -590,31 +591,33 @@ function getAllProjectFolders(workspacePath) {
|
|
|
590
591
|
* ```
|
|
591
592
|
*/
|
|
592
593
|
function findSessionsInDirectory(sessionDir) {
|
|
594
|
+
return findSessionFilesWithStats(sessionDir).map((file) => file.path);
|
|
595
|
+
}
|
|
596
|
+
/**
|
|
597
|
+
* The non-empty `.jsonl` files in a session directory with the stat each one
|
|
598
|
+
* already cost, newest first, so callers that need the mtime do not stat the
|
|
599
|
+
* same files a second time.
|
|
600
|
+
*/
|
|
601
|
+
function findSessionFilesWithStats(sessionDir) {
|
|
593
602
|
try {
|
|
594
603
|
if (!fs.existsSync(sessionDir)) {
|
|
595
604
|
return [];
|
|
596
605
|
}
|
|
597
|
-
|
|
606
|
+
return fs
|
|
598
607
|
.readdirSync(sessionDir)
|
|
599
608
|
.filter((file) => file.endsWith('.jsonl'))
|
|
600
609
|
.map((file) => {
|
|
601
610
|
const fullPath = path.join(sessionDir, file);
|
|
602
611
|
try {
|
|
603
612
|
const stats = fs.statSync(fullPath);
|
|
604
|
-
return {
|
|
605
|
-
path: fullPath,
|
|
606
|
-
mtime: stats.mtime.getTime(),
|
|
607
|
-
size: stats.size,
|
|
608
|
-
};
|
|
613
|
+
return { path: fullPath, mtime: stats.mtime, sizeBytes: stats.size };
|
|
609
614
|
}
|
|
610
615
|
catch {
|
|
611
616
|
return null;
|
|
612
617
|
}
|
|
613
618
|
})
|
|
614
|
-
.filter((f) => f !== null && f.
|
|
615
|
-
.sort((a, b) => b.mtime - a.mtime)
|
|
616
|
-
.map((f) => f.path);
|
|
617
|
-
return files;
|
|
619
|
+
.filter((f) => f !== null && f.sizeBytes > 0)
|
|
620
|
+
.sort((a, b) => b.mtime.getTime() - a.mtime.getTime());
|
|
618
621
|
}
|
|
619
622
|
catch {
|
|
620
623
|
return [];
|
package/dist/peakHours.d.ts
CHANGED
|
@@ -22,9 +22,34 @@ export interface PeakHoursState {
|
|
|
22
22
|
updatedAt: string;
|
|
23
23
|
unavailable: boolean;
|
|
24
24
|
notApplicable?: boolean;
|
|
25
|
+
/**
|
|
26
|
+
* `promoclock` when the state came from promoclock.co, `schedule` when it
|
|
27
|
+
* was computed locally from the published weekday window. Absent on the
|
|
28
|
+
* not-applicable and unavailable placeholder states.
|
|
29
|
+
*/
|
|
30
|
+
source?: 'promoclock' | 'schedule';
|
|
25
31
|
}
|
|
26
|
-
/**
|
|
27
|
-
|
|
32
|
+
/**
|
|
33
|
+
* Default budget for the promoclock.co request, in milliseconds. The schedule
|
|
34
|
+
* fallback answers the question anyway, so a slow third-party host should not
|
|
35
|
+
* hold up a `sidekick status` or `sidekick quota` run for long.
|
|
36
|
+
*/
|
|
37
|
+
export declare const DEFAULT_PEAK_HOURS_TIMEOUT_MS = 4000;
|
|
38
|
+
/** A promoclock.co answer is reused for this long before the host is asked again. */
|
|
39
|
+
export declare const PEAK_HOURS_CACHE_MS: number;
|
|
40
|
+
/** Published peak window: weekdays 13:00–19:00 UTC. */
|
|
41
|
+
export declare const PEAK_WINDOW_START_UTC_HOUR = 13;
|
|
42
|
+
export declare const PEAK_WINDOW_END_UTC_HOUR = 19;
|
|
43
|
+
export declare const PEAK_HOURS_DESCRIPTION = "Weekdays 13:00\u201319:00 UTC";
|
|
44
|
+
/** Forget the cached promoclock.co answer. Test-only. */
|
|
45
|
+
export declare function _resetPeakHoursCache(): void;
|
|
46
|
+
/**
|
|
47
|
+
* Peak-hours state computed from the published schedule alone, with no
|
|
48
|
+
* network access. Used directly by cache-only commands (`sidekick today`) and
|
|
49
|
+
* as the fallback when promoclock.co cannot be reached, so every surface
|
|
50
|
+
* agrees on whether it is currently peak.
|
|
51
|
+
*/
|
|
52
|
+
export declare function getScheduledPeakHoursState(now?: Date): PeakHoursState;
|
|
28
53
|
/** Options for {@link fetchPeakHoursStatus}. */
|
|
29
54
|
export interface FetchPeakHoursOptions {
|
|
30
55
|
/**
|
|
@@ -32,6 +57,8 @@ export interface FetchPeakHoursOptions {
|
|
|
32
57
|
* Defaults to {@link DEFAULT_PEAK_HOURS_TIMEOUT_MS}.
|
|
33
58
|
*/
|
|
34
59
|
timeoutMs?: number;
|
|
60
|
+
/** Bypass the in-process cache and ask promoclock.co again. */
|
|
61
|
+
force?: boolean;
|
|
35
62
|
}
|
|
36
63
|
export declare function isClaudeCodeSessionProvider(providerId: ProviderId): boolean;
|
|
37
64
|
export declare function createPeakHoursNotApplicableState(providerId: ProviderId): PeakHoursState;
|
|
@@ -40,7 +67,11 @@ export declare function scopePeakHoursToSessionProvider(providerId: ProviderId,
|
|
|
40
67
|
* Fetch current Claude peak-hours state from promoclock.co.
|
|
41
68
|
*
|
|
42
69
|
* Single-shot — caller wraps in polling loop, EventEmitter, or interval.
|
|
43
|
-
*
|
|
70
|
+
* A successful answer is cached in-process for {@link PEAK_HOURS_CACHE_MS}.
|
|
71
|
+
* On any network, timeout, or parse error the state is computed from the
|
|
72
|
+
* published schedule instead (`source: 'schedule'`), so callers always get a
|
|
73
|
+
* usable answer and every surface agrees on it; {@link unavailableState} is
|
|
74
|
+
* only returned when the schedule itself cannot be evaluated.
|
|
44
75
|
*
|
|
45
76
|
* promoclock.co is a third-party host, so the request is always bounded by
|
|
46
77
|
* an abort timeout. Without one a hung host would stall the caller — and
|
package/dist/peakHours.js
CHANGED
|
@@ -11,14 +11,74 @@
|
|
|
11
11
|
* Polling / eventing is the caller's responsibility.
|
|
12
12
|
*/
|
|
13
13
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
14
|
-
exports.DEFAULT_PEAK_HOURS_TIMEOUT_MS = void 0;
|
|
14
|
+
exports.PEAK_HOURS_DESCRIPTION = exports.PEAK_WINDOW_END_UTC_HOUR = exports.PEAK_WINDOW_START_UTC_HOUR = exports.PEAK_HOURS_CACHE_MS = exports.DEFAULT_PEAK_HOURS_TIMEOUT_MS = void 0;
|
|
15
|
+
exports._resetPeakHoursCache = _resetPeakHoursCache;
|
|
16
|
+
exports.getScheduledPeakHoursState = getScheduledPeakHoursState;
|
|
15
17
|
exports.isClaudeCodeSessionProvider = isClaudeCodeSessionProvider;
|
|
16
18
|
exports.createPeakHoursNotApplicableState = createPeakHoursNotApplicableState;
|
|
17
19
|
exports.scopePeakHoursToSessionProvider = scopePeakHoursToSessionProvider;
|
|
18
20
|
exports.fetchPeakHoursStatus = fetchPeakHoursStatus;
|
|
19
21
|
const PROMOCLOCK_ENDPOINT = 'https://promoclock.co/api/status';
|
|
20
|
-
/**
|
|
21
|
-
|
|
22
|
+
/**
|
|
23
|
+
* Default budget for the promoclock.co request, in milliseconds. The schedule
|
|
24
|
+
* fallback answers the question anyway, so a slow third-party host should not
|
|
25
|
+
* hold up a `sidekick status` or `sidekick quota` run for long.
|
|
26
|
+
*/
|
|
27
|
+
exports.DEFAULT_PEAK_HOURS_TIMEOUT_MS = 4_000;
|
|
28
|
+
/** A promoclock.co answer is reused for this long before the host is asked again. */
|
|
29
|
+
exports.PEAK_HOURS_CACHE_MS = 10 * 60_000;
|
|
30
|
+
/** Published peak window: weekdays 13:00–19:00 UTC. */
|
|
31
|
+
exports.PEAK_WINDOW_START_UTC_HOUR = 13;
|
|
32
|
+
exports.PEAK_WINDOW_END_UTC_HOUR = 19;
|
|
33
|
+
exports.PEAK_HOURS_DESCRIPTION = 'Weekdays 13:00–19:00 UTC';
|
|
34
|
+
let cachedNetworkState = null;
|
|
35
|
+
/** Forget the cached promoclock.co answer. Test-only. */
|
|
36
|
+
function _resetPeakHoursCache() {
|
|
37
|
+
cachedNetworkState = null;
|
|
38
|
+
}
|
|
39
|
+
function isWeekdayUtc(date) {
|
|
40
|
+
const day = date.getUTCDay();
|
|
41
|
+
return day >= 1 && day <= 5;
|
|
42
|
+
}
|
|
43
|
+
function utcDateAtHour(date, hour, dayOffset = 0) {
|
|
44
|
+
return new Date(Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate() + dayOffset, hour));
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Peak-hours state computed from the published schedule alone, with no
|
|
48
|
+
* network access. Used directly by cache-only commands (`sidekick today`) and
|
|
49
|
+
* as the fallback when promoclock.co cannot be reached, so every surface
|
|
50
|
+
* agrees on whether it is currently peak.
|
|
51
|
+
*/
|
|
52
|
+
function getScheduledPeakHoursState(now = new Date()) {
|
|
53
|
+
const hour = now.getUTCHours() + now.getUTCMinutes() / 60;
|
|
54
|
+
const isPeak = isWeekdayUtc(now) && hour >= exports.PEAK_WINDOW_START_UTC_HOUR && hour < exports.PEAK_WINDOW_END_UTC_HOUR;
|
|
55
|
+
let nextChange;
|
|
56
|
+
if (isPeak) {
|
|
57
|
+
nextChange = utcDateAtHour(now, exports.PEAK_WINDOW_END_UTC_HOUR);
|
|
58
|
+
}
|
|
59
|
+
else if (isWeekdayUtc(now) && hour < exports.PEAK_WINDOW_START_UTC_HOUR) {
|
|
60
|
+
nextChange = utcDateAtHour(now, exports.PEAK_WINDOW_START_UTC_HOUR);
|
|
61
|
+
}
|
|
62
|
+
else {
|
|
63
|
+
let offset = 1;
|
|
64
|
+
while (!isWeekdayUtc(utcDateAtHour(now, 0, offset)))
|
|
65
|
+
offset += 1;
|
|
66
|
+
nextChange = utcDateAtHour(now, exports.PEAK_WINDOW_START_UTC_HOUR, offset);
|
|
67
|
+
}
|
|
68
|
+
return {
|
|
69
|
+
status: isPeak ? 'peak' : 'off_peak',
|
|
70
|
+
isPeak,
|
|
71
|
+
sessionLimitSpeed: isPeak ? 'faster' : 'normal',
|
|
72
|
+
label: isPeak ? 'Peak Hours' : 'Off-Peak',
|
|
73
|
+
peakHoursDescription: exports.PEAK_HOURS_DESCRIPTION,
|
|
74
|
+
nextChange: nextChange.toISOString(),
|
|
75
|
+
minutesUntilChange: Math.max(0, Math.round((nextChange.getTime() - now.getTime()) / 60_000)),
|
|
76
|
+
note: 'Computed from the published weekday schedule.',
|
|
77
|
+
updatedAt: now.toISOString(),
|
|
78
|
+
unavailable: false,
|
|
79
|
+
source: 'schedule',
|
|
80
|
+
};
|
|
81
|
+
}
|
|
22
82
|
function unavailableState() {
|
|
23
83
|
return {
|
|
24
84
|
status: 'unknown',
|
|
@@ -76,13 +136,23 @@ function normalizeSpeed(raw) {
|
|
|
76
136
|
* Fetch current Claude peak-hours state from promoclock.co.
|
|
77
137
|
*
|
|
78
138
|
* Single-shot — caller wraps in polling loop, EventEmitter, or interval.
|
|
79
|
-
*
|
|
139
|
+
* A successful answer is cached in-process for {@link PEAK_HOURS_CACHE_MS}.
|
|
140
|
+
* On any network, timeout, or parse error the state is computed from the
|
|
141
|
+
* published schedule instead (`source: 'schedule'`), so callers always get a
|
|
142
|
+
* usable answer and every surface agrees on it; {@link unavailableState} is
|
|
143
|
+
* only returned when the schedule itself cannot be evaluated.
|
|
80
144
|
*
|
|
81
145
|
* promoclock.co is a third-party host, so the request is always bounded by
|
|
82
146
|
* an abort timeout. Without one a hung host would stall the caller — and
|
|
83
147
|
* this is polled, so the stall would repeat.
|
|
84
148
|
*/
|
|
85
149
|
async function fetchPeakHoursStatus(options = {}) {
|
|
150
|
+
const now = Date.now();
|
|
151
|
+
if (!options.force &&
|
|
152
|
+
cachedNetworkState &&
|
|
153
|
+
now - cachedNetworkState.fetchedAt < exports.PEAK_HOURS_CACHE_MS) {
|
|
154
|
+
return cachedNetworkState.state;
|
|
155
|
+
}
|
|
86
156
|
const timeoutMs = options.timeoutMs ?? exports.DEFAULT_PEAK_HOURS_TIMEOUT_MS;
|
|
87
157
|
const controller = new AbortController();
|
|
88
158
|
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
@@ -90,12 +160,12 @@ async function fetchPeakHoursStatus(options = {}) {
|
|
|
90
160
|
try {
|
|
91
161
|
const res = await fetch(PROMOCLOCK_ENDPOINT, { signal: controller.signal });
|
|
92
162
|
if (!res.ok)
|
|
93
|
-
return
|
|
163
|
+
return scheduleFallback();
|
|
94
164
|
const data = await res.json();
|
|
95
165
|
const status = normalizeStatus(data.status);
|
|
96
166
|
const sessionLimitSpeed = normalizeSpeed(data.sessionLimitSpeed);
|
|
97
167
|
const isPeak = typeof data.isPeak === 'boolean' ? data.isPeak : status === 'peak';
|
|
98
|
-
|
|
168
|
+
const state = {
|
|
99
169
|
status,
|
|
100
170
|
isPeak,
|
|
101
171
|
sessionLimitSpeed,
|
|
@@ -106,13 +176,28 @@ async function fetchPeakHoursStatus(options = {}) {
|
|
|
106
176
|
note: data.note ?? '',
|
|
107
177
|
updatedAt: data.timestamp ?? new Date().toISOString(),
|
|
108
178
|
unavailable: false,
|
|
179
|
+
source: 'promoclock',
|
|
109
180
|
};
|
|
181
|
+
cachedNetworkState = { state, fetchedAt: now };
|
|
182
|
+
return state;
|
|
110
183
|
}
|
|
111
184
|
catch {
|
|
112
185
|
// Covers AbortError from the timeout above alongside network/parse failures.
|
|
113
|
-
return
|
|
186
|
+
return scheduleFallback();
|
|
114
187
|
}
|
|
115
188
|
finally {
|
|
116
189
|
clearTimeout(timeout);
|
|
117
190
|
}
|
|
118
191
|
}
|
|
192
|
+
function scheduleFallback() {
|
|
193
|
+
try {
|
|
194
|
+
const state = getScheduledPeakHoursState();
|
|
195
|
+
return {
|
|
196
|
+
...state,
|
|
197
|
+
note: 'promoclock.co unreachable; computed from the published weekday schedule.',
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
catch {
|
|
201
|
+
return unavailableState();
|
|
202
|
+
}
|
|
203
|
+
}
|
package/dist/pricingCatalog.d.ts
CHANGED
|
@@ -34,6 +34,13 @@ export interface HydrateOptions {
|
|
|
34
34
|
url?: string;
|
|
35
35
|
/** Optional logger (useful for diagnostics without pulling a logger dep). */
|
|
36
36
|
logger?: (msg: string) => void;
|
|
37
|
+
/**
|
|
38
|
+
* Never touch the network: apply the on-disk cache regardless of its age, or
|
|
39
|
+
* stay on the static tables when there is none. Callers that must produce
|
|
40
|
+
* the same figures on every run (scripted reports, tests, air-gapped hosts)
|
|
41
|
+
* set this; `SIDEKICK_OFFLINE=1` does the same for the CLI.
|
|
42
|
+
*/
|
|
43
|
+
offline?: boolean;
|
|
37
44
|
}
|
|
38
45
|
export interface HydrateResult {
|
|
39
46
|
/** Where the loaded pricing came from. */
|