chat-recall 0.5.5 → 0.5.7
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 +332 -0
- package/dist/cli.js +11473 -8310
- package/dist/cli.js.map +1 -1
- package/dist/code-index-cursor.d.ts +18 -0
- package/dist/code-index-cursor.d.ts.map +1 -0
- package/dist/code-index-cursor.js +89 -0
- package/dist/code-index-cursor.js.map +1 -0
- package/dist/daemon-log.d.ts +44 -0
- package/dist/daemon-log.d.ts.map +1 -0
- package/dist/daemon-log.js +74 -0
- package/dist/daemon-log.js.map +1 -0
- package/dist/derived.d.ts +20 -0
- package/dist/derived.d.ts.map +1 -0
- package/dist/derived.js +106 -0
- package/dist/derived.js.map +1 -0
- package/dist/install-skills.d.ts.map +1 -1
- package/dist/install-skills.js +6 -1
- package/dist/install-skills.js.map +1 -1
- package/dist/log-rotate.d.ts +16 -0
- package/dist/log-rotate.d.ts.map +1 -0
- package/dist/log-rotate.js +50 -0
- package/dist/log-rotate.js.map +1 -0
- package/dist/mcp-diagnostics.d.ts.map +1 -1
- package/dist/mcp-diagnostics.js +10 -8
- package/dist/mcp-diagnostics.js.map +1 -1
- package/dist/mcp.d.ts.map +1 -1
- package/dist/mcp.js +5763 -3045
- package/dist/mcp.js.map +1 -1
- package/dist/project-tasks.d.ts.map +1 -1
- package/dist/project-tasks.js +13 -8
- package/dist/project-tasks.js.map +1 -1
- package/dist/redact-deep.d.ts +4 -0
- package/dist/redact-deep.d.ts.map +1 -0
- package/dist/redact-deep.js +26 -0
- package/dist/redact-deep.js.map +1 -0
- package/dist/scan-pool.d.ts +56 -0
- package/dist/scan-pool.d.ts.map +1 -0
- package/dist/scan-pool.js +245 -0
- package/dist/scan-pool.js.map +1 -0
- package/dist/scan-worker.d.ts +78 -0
- package/dist/scan-worker.d.ts.map +1 -0
- package/dist/scan-worker.js +11175 -0
- package/dist/scan-worker.js.map +1 -0
- package/dist/self-restart.d.ts +20 -2
- package/dist/self-restart.d.ts.map +1 -1
- package/dist/self-restart.js +16 -6
- package/dist/self-restart.js.map +1 -1
- package/dist/server-probe.d.ts +76 -0
- package/dist/server-probe.d.ts.map +1 -0
- package/dist/server-probe.js +106 -0
- package/dist/server-probe.js.map +1 -0
- package/dist/service-installer.d.ts +1 -1
- package/dist/service-installer.d.ts.map +1 -1
- package/dist/service-installer.js +20 -6
- package/dist/service-installer.js.map +1 -1
- package/dist/sync-client.d.ts +25 -0
- package/dist/sync-client.d.ts.map +1 -1
- package/dist/sync-client.js +590 -176
- package/dist/sync-client.js.map +1 -1
- package/dist/sync-ledger.d.ts +22 -0
- package/dist/sync-ledger.d.ts.map +1 -1
- package/dist/sync-ledger.js +108 -1
- package/dist/sync-ledger.js.map +1 -1
- package/dist/target-breaker.d.ts +61 -0
- package/dist/target-breaker.d.ts.map +1 -0
- package/dist/target-breaker.js +85 -0
- package/dist/target-breaker.js.map +1 -0
- package/dist/telemetry-consent.d.ts +31 -0
- package/dist/telemetry-consent.d.ts.map +1 -0
- package/dist/telemetry-consent.js +115 -0
- package/dist/telemetry-consent.js.map +1 -0
- package/dist/telemetry.d.ts +22 -0
- package/dist/telemetry.d.ts.map +1 -0
- package/dist/telemetry.js +125 -0
- package/dist/telemetry.js.map +1 -0
- package/dist/tick-queue.d.ts +67 -0
- package/dist/tick-queue.d.ts.map +1 -0
- package/dist/tick-queue.js +104 -0
- package/dist/tick-queue.js.map +1 -0
- package/dist/transport-safety.d.ts +43 -0
- package/dist/transport-safety.d.ts.map +1 -0
- package/dist/transport-safety.js +100 -0
- package/dist/transport-safety.js.map +1 -0
- package/dist/verify-repair.js +1 -1
- package/dist/verify-repair.js.map +1 -1
- package/dist/watch.js +8887 -6028
- package/hooks/chat_recall_escalate_hook.sh +28 -2
- package/hooks/chat_recall_save_hook.sh +17 -11
- package/hooks/chat_recall_wakeup_hook.sh +27 -1
- package/package.json +27 -4
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stop hammering a server that is not answering.
|
|
3
|
+
*
|
|
4
|
+
* ── The behaviour this replaces ───────────────────────────────────────────
|
|
5
|
+
* Every walk tried every configured target, in full, forever. A target that was
|
|
6
|
+
* down did not merely fail — it failed SLOWLY, once per session, inside the same
|
|
7
|
+
* serial walk as the healthy one: connect timeout, three retries with 2s/8s/30s
|
|
8
|
+
* backoff, then the next session. One measured log carried 1,772 `fetch failed`
|
|
9
|
+
* and 3,526 HTTP 429 and was still producing them at 26–38 per hour, days later.
|
|
10
|
+
*
|
|
11
|
+
* The cost lands on the wrong target. Because targets are walked in sequence, a
|
|
12
|
+
* dead LAN box delays every ship to the healthy SaaS host behind it, and the
|
|
13
|
+
* whole walk's completion — which is what the ledger, the health file and the
|
|
14
|
+
* progress report all wait on.
|
|
15
|
+
*
|
|
16
|
+
* ── The rule ─────────────────────────────────────────────────────────────
|
|
17
|
+
* Consecutive whole-walk failures trip the breaker. It stays open for a
|
|
18
|
+
* doubling cooldown, capped, and one success closes it completely.
|
|
19
|
+
*
|
|
20
|
+
* Deliberately counted per WALK, not per request. A single walk already retries
|
|
21
|
+
* internally and obeys Retry-After; failing individual requests are normal
|
|
22
|
+
* operation and must not trip anything. What "the target is down" actually looks
|
|
23
|
+
* like is a whole walk failing, repeatedly.
|
|
24
|
+
*
|
|
25
|
+
* Process-lifetime only, and that is the right lifetime: a restart is exactly
|
|
26
|
+
* when you want to retry a target you gave up on. Nothing is persisted.
|
|
27
|
+
*
|
|
28
|
+
* A tripped breaker is never silent. It is reported to the health file so the
|
|
29
|
+
* user hears "this server is being skipped" instead of watching a target quietly
|
|
30
|
+
* never sync.
|
|
31
|
+
*/
|
|
32
|
+
/** Consecutive failed walks before a target is skipped. */
|
|
33
|
+
export declare const TRIP_AFTER_FAILURES: number;
|
|
34
|
+
/** First cooldown, doubled per subsequent failure. */
|
|
35
|
+
export declare const BASE_COOLDOWN_MS: number;
|
|
36
|
+
/** Ceiling, so a target is always retried eventually. */
|
|
37
|
+
export declare const MAX_COOLDOWN_MS: number;
|
|
38
|
+
/** Test seam. */
|
|
39
|
+
export declare function _resetBreakers(): void;
|
|
40
|
+
/** Cooldown for the Nth consecutive failure, doubling and capped. */
|
|
41
|
+
export declare function cooldownFor(failures: number): number;
|
|
42
|
+
export interface BreakerVerdict {
|
|
43
|
+
/** True when this target should be skipped right now. */
|
|
44
|
+
open: boolean;
|
|
45
|
+
/** Milliseconds until the next attempt. 0 when closed. */
|
|
46
|
+
retryInMs: number;
|
|
47
|
+
failures: number;
|
|
48
|
+
lastError: string;
|
|
49
|
+
}
|
|
50
|
+
/** Should this target be skipped? */
|
|
51
|
+
export declare function breakerState(serverUrl: string, now?: number): BreakerVerdict;
|
|
52
|
+
/** A walk against this target completed. Clear everything. */
|
|
53
|
+
export declare function noteTargetSuccess(serverUrl: string): void;
|
|
54
|
+
/**
|
|
55
|
+
* A walk against this target failed. Returns the verdict for the NEXT attempt,
|
|
56
|
+
* so the caller can report "skipping for 4m" in the same breath as the failure.
|
|
57
|
+
*/
|
|
58
|
+
export declare function noteTargetFailure(serverUrl: string, error: string, now?: number): BreakerVerdict;
|
|
59
|
+
/** One line for the log, or null when there is nothing to say. */
|
|
60
|
+
export declare function breakerNotice(serverUrl: string, v: BreakerVerdict): string | null;
|
|
61
|
+
//# sourceMappingURL=target-breaker.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"target-breaker.d.ts","sourceRoot":"","sources":["../src/target-breaker.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AAEH,2DAA2D;AAC3D,eAAO,MAAM,mBAAmB,QAAoD,CAAC;AACrF,sDAAsD;AACtD,eAAO,MAAM,gBAAgB,QAAgE,CAAC;AAC9F,yDAAyD;AACzD,eAAO,MAAM,eAAe,QAAgE,CAAC;AAM7F,iBAAiB;AACjB,wBAAgB,cAAc,IAAI,IAAI,CAAmB;AAIzD,qEAAqE;AACrE,wBAAgB,WAAW,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAGpD;AAED,MAAM,WAAW,cAAc;IAC7B,yDAAyD;IACzD,IAAI,EAAE,OAAO,CAAC;IACd,0DAA0D;IAC1D,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,qCAAqC;AACrC,wBAAgB,YAAY,CAAC,SAAS,EAAE,MAAM,EAAE,GAAG,SAAa,GAAG,cAAc,CAMhF;AAED,8DAA8D;AAC9D,wBAAgB,iBAAiB,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,CAEzD;AAED;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,SAAa,GAAG,cAAc,CAapG;AAED,kEAAkE;AAClE,wBAAgB,aAAa,CAAC,SAAS,EAAE,MAAM,EAAE,CAAC,EAAE,cAAc,GAAG,MAAM,GAAG,IAAI,CAKjF"}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stop hammering a server that is not answering.
|
|
3
|
+
*
|
|
4
|
+
* ── The behaviour this replaces ───────────────────────────────────────────
|
|
5
|
+
* Every walk tried every configured target, in full, forever. A target that was
|
|
6
|
+
* down did not merely fail — it failed SLOWLY, once per session, inside the same
|
|
7
|
+
* serial walk as the healthy one: connect timeout, three retries with 2s/8s/30s
|
|
8
|
+
* backoff, then the next session. One measured log carried 1,772 `fetch failed`
|
|
9
|
+
* and 3,526 HTTP 429 and was still producing them at 26–38 per hour, days later.
|
|
10
|
+
*
|
|
11
|
+
* The cost lands on the wrong target. Because targets are walked in sequence, a
|
|
12
|
+
* dead LAN box delays every ship to the healthy SaaS host behind it, and the
|
|
13
|
+
* whole walk's completion — which is what the ledger, the health file and the
|
|
14
|
+
* progress report all wait on.
|
|
15
|
+
*
|
|
16
|
+
* ── The rule ─────────────────────────────────────────────────────────────
|
|
17
|
+
* Consecutive whole-walk failures trip the breaker. It stays open for a
|
|
18
|
+
* doubling cooldown, capped, and one success closes it completely.
|
|
19
|
+
*
|
|
20
|
+
* Deliberately counted per WALK, not per request. A single walk already retries
|
|
21
|
+
* internally and obeys Retry-After; failing individual requests are normal
|
|
22
|
+
* operation and must not trip anything. What "the target is down" actually looks
|
|
23
|
+
* like is a whole walk failing, repeatedly.
|
|
24
|
+
*
|
|
25
|
+
* Process-lifetime only, and that is the right lifetime: a restart is exactly
|
|
26
|
+
* when you want to retry a target you gave up on. Nothing is persisted.
|
|
27
|
+
*
|
|
28
|
+
* A tripped breaker is never silent. It is reported to the health file so the
|
|
29
|
+
* user hears "this server is being skipped" instead of watching a target quietly
|
|
30
|
+
* never sync.
|
|
31
|
+
*/
|
|
32
|
+
/** Consecutive failed walks before a target is skipped. */
|
|
33
|
+
export const TRIP_AFTER_FAILURES = Number(process.env.CHAT_RECALL_BREAKER_TRIP) || 3;
|
|
34
|
+
/** First cooldown, doubled per subsequent failure. */
|
|
35
|
+
export const BASE_COOLDOWN_MS = Number(process.env.CHAT_RECALL_BREAKER_COOLDOWN_MS) || 60_000;
|
|
36
|
+
/** Ceiling, so a target is always retried eventually. */
|
|
37
|
+
export const MAX_COOLDOWN_MS = Number(process.env.CHAT_RECALL_BREAKER_MAX_MS) || 15 * 60_000;
|
|
38
|
+
const state = new Map();
|
|
39
|
+
/** Test seam. */
|
|
40
|
+
export function _resetBreakers() { state.clear(); }
|
|
41
|
+
const key = (serverUrl) => serverUrl.replace(/\/+$/, '');
|
|
42
|
+
/** Cooldown for the Nth consecutive failure, doubling and capped. */
|
|
43
|
+
export function cooldownFor(failures) {
|
|
44
|
+
const over = Math.max(0, failures - TRIP_AFTER_FAILURES);
|
|
45
|
+
return Math.min(MAX_COOLDOWN_MS, BASE_COOLDOWN_MS * 2 ** over);
|
|
46
|
+
}
|
|
47
|
+
/** Should this target be skipped? */
|
|
48
|
+
export function breakerState(serverUrl, now = Date.now()) {
|
|
49
|
+
const s = state.get(key(serverUrl));
|
|
50
|
+
if (!s || s.openUntil <= now) {
|
|
51
|
+
return { open: false, retryInMs: 0, failures: s?.failures ?? 0, lastError: s?.lastError ?? '' };
|
|
52
|
+
}
|
|
53
|
+
return { open: true, retryInMs: s.openUntil - now, failures: s.failures, lastError: s.lastError };
|
|
54
|
+
}
|
|
55
|
+
/** A walk against this target completed. Clear everything. */
|
|
56
|
+
export function noteTargetSuccess(serverUrl) {
|
|
57
|
+
state.delete(key(serverUrl));
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* A walk against this target failed. Returns the verdict for the NEXT attempt,
|
|
61
|
+
* so the caller can report "skipping for 4m" in the same breath as the failure.
|
|
62
|
+
*/
|
|
63
|
+
export function noteTargetFailure(serverUrl, error, now = Date.now()) {
|
|
64
|
+
const k = key(serverUrl);
|
|
65
|
+
const prior = state.get(k);
|
|
66
|
+
const failures = (prior?.failures ?? 0) + 1;
|
|
67
|
+
const lastError = error.slice(0, 200);
|
|
68
|
+
if (failures < TRIP_AFTER_FAILURES) {
|
|
69
|
+
// Not yet a pattern — a single failed walk is ordinary.
|
|
70
|
+
state.set(k, { failures, openUntil: 0, lastError });
|
|
71
|
+
return { open: false, retryInMs: 0, failures, lastError };
|
|
72
|
+
}
|
|
73
|
+
const cooldown = cooldownFor(failures);
|
|
74
|
+
state.set(k, { failures, openUntil: now + cooldown, lastError });
|
|
75
|
+
return { open: true, retryInMs: cooldown, failures, lastError };
|
|
76
|
+
}
|
|
77
|
+
/** One line for the log, or null when there is nothing to say. */
|
|
78
|
+
export function breakerNotice(serverUrl, v) {
|
|
79
|
+
if (!v.open)
|
|
80
|
+
return null;
|
|
81
|
+
const mins = Math.max(1, Math.round(v.retryInMs / 60_000));
|
|
82
|
+
return `skipping ${serverUrl} — ${v.failures} consecutive failed sync(s), next attempt in ~${mins}m`
|
|
83
|
+
+ (v.lastError ? ` (last error: ${v.lastError})` : '');
|
|
84
|
+
}
|
|
85
|
+
//# sourceMappingURL=target-breaker.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"target-breaker.js","sourceRoot":"","sources":["../src/target-breaker.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Has the user opted out?
|
|
3
|
+
*
|
|
4
|
+
* Default is ON for the operational facts above — an opt-OUT, which is what the
|
|
5
|
+
* product intends and what the docs must say plainly. Anything that touches user
|
|
6
|
+
* CONTENT is not covered by this and is not collected at all, at any setting.
|
|
7
|
+
*/
|
|
8
|
+
export declare function userConsents(): boolean;
|
|
9
|
+
/** Record what the server said about this tenant's plan. */
|
|
10
|
+
export declare function setTelemetryEligible(serverUrl: string, allowed: boolean): void;
|
|
11
|
+
/** Default NO: a server that never said yes never gets telemetry. */
|
|
12
|
+
export declare function serverAllowsTelemetry(serverUrl: string): boolean;
|
|
13
|
+
/** Test seam. */
|
|
14
|
+
export declare function _resetTelemetryEligibility(): void;
|
|
15
|
+
/** Both gates, for one target. */
|
|
16
|
+
export declare function mayReport(serverUrl: string): boolean;
|
|
17
|
+
export interface SensitiveKeyViolation {
|
|
18
|
+
key: string;
|
|
19
|
+
where: string;
|
|
20
|
+
}
|
|
21
|
+
/** Every forbidden key in `obj`, deeply. Empty means safe to send. */
|
|
22
|
+
export declare function findSensitiveKeys(obj: unknown, where?: string): SensitiveKeyViolation[];
|
|
23
|
+
/**
|
|
24
|
+
* Throw when a payload carries a forbidden key.
|
|
25
|
+
*
|
|
26
|
+
* Deliberately loud: a telemetry event that leaks a file path is a privacy
|
|
27
|
+
* incident, and the cheapest place to stop it is the type of failure a developer
|
|
28
|
+
* cannot ignore.
|
|
29
|
+
*/
|
|
30
|
+
export declare function assertNoSensitiveKeys(payload: unknown): void;
|
|
31
|
+
//# sourceMappingURL=telemetry-consent.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"telemetry-consent.d.ts","sourceRoot":"","sources":["../src/telemetry-consent.ts"],"names":[],"mappings":"AAwCA;;;;;;GAMG;AACH,wBAAgB,YAAY,IAAI,OAAO,CAStC;AAKD,4DAA4D;AAC5D,wBAAgB,oBAAoB,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,GAAG,IAAI,CAE9E;AAED,qEAAqE;AACrE,wBAAgB,qBAAqB,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAEhE;AAED,iBAAiB;AACjB,wBAAgB,0BAA0B,IAAI,IAAI,CAAsB;AAExE,kCAAkC;AAClC,wBAAgB,SAAS,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAEpD;AAWD,MAAM,WAAW,qBAAqB;IAAG,GAAG,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE;AAErE,sEAAsE;AACtE,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,OAAO,EAAE,KAAK,SAAK,GAAG,qBAAqB,EAAE,CAYnF;AAED;;;;;;GAMG;AACH,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAQ5D"}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Whether this machine may report operational telemetry, and what counts as it.
|
|
3
|
+
*
|
|
4
|
+
* TWO INDEPENDENT GATES, both of which must open. They are separate on purpose:
|
|
5
|
+
* one answers "is the user willing", the other "is the operator entitled", and
|
|
6
|
+
* conflating them is how a product ends up collecting from someone who said no.
|
|
7
|
+
*
|
|
8
|
+
* 1. CONSENT — the user's setting, checked on this machine. It wins over
|
|
9
|
+
* everything. No server response can override it, because a server should
|
|
10
|
+
* not be able to talk a client into sending data its owner declined.
|
|
11
|
+
*
|
|
12
|
+
* 2. ELIGIBILITY — the server says whether this tenant's plan includes it.
|
|
13
|
+
* The client cannot know its own plan (the entitlement lives in the control
|
|
14
|
+
* plane), so it is told, and it defaults to NOT eligible: an older server
|
|
15
|
+
* that says nothing gets nothing.
|
|
16
|
+
*
|
|
17
|
+
* The server enforces (2) again at ingest. This gate is an optimisation — it
|
|
18
|
+
* stops the data leaving a machine that shouldn't send it — not the boundary.
|
|
19
|
+
*
|
|
20
|
+
* ── What may be reported, and what may never be ──────────────────────────
|
|
21
|
+
* Operational facts about the COLLECTOR, never about the user's work:
|
|
22
|
+
*
|
|
23
|
+
* allowed walk duration, sessions considered/shipped, bytes uploaded,
|
|
24
|
+
* 429/failure counts, breaker trips, RSS peak, CLI version, OS,
|
|
25
|
+
* error CLASS plus a redacted message
|
|
26
|
+
* forbidden transcript content, prompts, file paths, project names, session
|
|
27
|
+
* ids, repository names, anything derived from what the user wrote
|
|
28
|
+
*
|
|
29
|
+
* `assertNoSensitiveKeys` enforces the second list structurally rather than by
|
|
30
|
+
* review, because "no paths" is exactly the promise that erodes one convenient
|
|
31
|
+
* field at a time.
|
|
32
|
+
*/
|
|
33
|
+
import { loadSettings } from '@chat-recall/engine/core/settings.js';
|
|
34
|
+
/** Env kill-switch, for a machine where editing settings is not practical. */
|
|
35
|
+
const ENV_OFF = () => {
|
|
36
|
+
const v = (process.env.CHAT_RECALL_TELEMETRY || '').trim().toLowerCase();
|
|
37
|
+
return v === '0' || v === 'off' || v === 'false' || v === 'no';
|
|
38
|
+
};
|
|
39
|
+
/**
|
|
40
|
+
* Has the user opted out?
|
|
41
|
+
*
|
|
42
|
+
* Default is ON for the operational facts above — an opt-OUT, which is what the
|
|
43
|
+
* product intends and what the docs must say plainly. Anything that touches user
|
|
44
|
+
* CONTENT is not covered by this and is not collected at all, at any setting.
|
|
45
|
+
*/
|
|
46
|
+
export function userConsents() {
|
|
47
|
+
if (ENV_OFF())
|
|
48
|
+
return false;
|
|
49
|
+
try {
|
|
50
|
+
const s = loadSettings();
|
|
51
|
+
return s.privacy?.telemetry !== false;
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
// Unreadable settings must not be read as consent.
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
/** Eligibility as last reported by a server, keyed by server URL. */
|
|
59
|
+
const eligible = new Map();
|
|
60
|
+
/** Record what the server said about this tenant's plan. */
|
|
61
|
+
export function setTelemetryEligible(serverUrl, allowed) {
|
|
62
|
+
eligible.set(serverUrl.replace(/\/+$/, ''), allowed);
|
|
63
|
+
}
|
|
64
|
+
/** Default NO: a server that never said yes never gets telemetry. */
|
|
65
|
+
export function serverAllowsTelemetry(serverUrl) {
|
|
66
|
+
return eligible.get(serverUrl.replace(/\/+$/, '')) === true;
|
|
67
|
+
}
|
|
68
|
+
/** Test seam. */
|
|
69
|
+
export function _resetTelemetryEligibility() { eligible.clear(); }
|
|
70
|
+
/** Both gates, for one target. */
|
|
71
|
+
export function mayReport(serverUrl) {
|
|
72
|
+
return userConsents() && serverAllowsTelemetry(serverUrl);
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Keys that must never appear in a telemetry payload.
|
|
76
|
+
*
|
|
77
|
+
* Checked structurally so the promise survives the next person adding "just the
|
|
78
|
+
* project name for context". A violation throws in development and drops the
|
|
79
|
+
* event in production — never sends it anyway.
|
|
80
|
+
*/
|
|
81
|
+
const FORBIDDEN_KEY = /(path|dir|file|project|repo|session|prompt|content|text|title|branch|url|email|user|home|cwd)/i;
|
|
82
|
+
/** Every forbidden key in `obj`, deeply. Empty means safe to send. */
|
|
83
|
+
export function findSensitiveKeys(obj, where = '') {
|
|
84
|
+
const out = [];
|
|
85
|
+
const walk = (v, at) => {
|
|
86
|
+
if (!v || typeof v !== 'object')
|
|
87
|
+
return;
|
|
88
|
+
if (Array.isArray(v)) {
|
|
89
|
+
v.forEach((e, i) => walk(e, `${at}[${i}]`));
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
for (const [k, val] of Object.entries(v)) {
|
|
93
|
+
if (FORBIDDEN_KEY.test(k))
|
|
94
|
+
out.push({ key: k, where: at || '(root)' });
|
|
95
|
+
walk(val, at ? `${at}.${k}` : k);
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
walk(obj, where);
|
|
99
|
+
return out;
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Throw when a payload carries a forbidden key.
|
|
103
|
+
*
|
|
104
|
+
* Deliberately loud: a telemetry event that leaks a file path is a privacy
|
|
105
|
+
* incident, and the cheapest place to stop it is the type of failure a developer
|
|
106
|
+
* cannot ignore.
|
|
107
|
+
*/
|
|
108
|
+
export function assertNoSensitiveKeys(payload) {
|
|
109
|
+
const bad = findSensitiveKeys(payload);
|
|
110
|
+
if (bad.length > 0) {
|
|
111
|
+
throw new Error(`telemetry payload carries forbidden key(s): ${bad.map((b) => `${b.where}.${b.key}`).join(', ')}. `
|
|
112
|
+
+ 'Operational telemetry must describe the COLLECTOR, never the user\'s work.');
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
//# sourceMappingURL=telemetry-consent.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"telemetry-consent.js","sourceRoot":"","sources":["../src/telemetry-consent.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/** One reportable fact about a collector run. `kind` is the event class. */
|
|
2
|
+
export interface TelemetryEvent {
|
|
3
|
+
kind: string;
|
|
4
|
+
/** Numbers only, plus the few enumerated strings below. No free text. */
|
|
5
|
+
[k: string]: string | number | boolean | undefined;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Record an event. Cheap and synchronous — it never blocks the caller and never
|
|
9
|
+
* throws, because a measurement must not be able to break the thing it measures.
|
|
10
|
+
*/
|
|
11
|
+
export declare function record(event: TelemetryEvent): void;
|
|
12
|
+
/** Pending events, for tests and `doctor`. */
|
|
13
|
+
export declare function pending(): readonly TelemetryEvent[];
|
|
14
|
+
export declare function _clearForTests(): void;
|
|
15
|
+
/**
|
|
16
|
+
* Ship whatever is queued to every eligible target, then clear.
|
|
17
|
+
*
|
|
18
|
+
* Called after a sync completes — the connection is warm, the numbers are final,
|
|
19
|
+
* and there is no separate schedule to get wrong.
|
|
20
|
+
*/
|
|
21
|
+
export declare function flush(): Promise<void>;
|
|
22
|
+
//# sourceMappingURL=telemetry.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"telemetry.d.ts","sourceRoot":"","sources":["../src/telemetry.ts"],"names":[],"mappings":"AAqCA,4EAA4E;AAC5E,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,yEAAyE;IACzE,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,MAAM,GAAG,OAAO,GAAG,SAAS,CAAC;CACpD;AAKD;;;GAGG;AACH,wBAAgB,MAAM,CAAC,KAAK,EAAE,cAAc,GAAG,IAAI,CAWlD;AAED,8CAA8C;AAC9C,wBAAgB,OAAO,IAAI,SAAS,cAAc,EAAE,CAAkB;AACtE,wBAAgB,cAAc,IAAI,IAAI,CAAsB;AAE5D;;;;;GAKG;AACH,wBAAsB,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAsC3C"}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Report how the COLLECTOR is doing, to the user's own server.
|
|
3
|
+
*
|
|
4
|
+
* ── Why this exists ──────────────────────────────────────────────────────
|
|
5
|
+
* Every performance fact known about this product came from one developer
|
|
6
|
+
* machine. "How long does a first sync take for a customer with 5,000 sessions?"
|
|
7
|
+
* had no answer, because nothing measured a customer. The collector already
|
|
8
|
+
* computes everything needed — walk duration, sessions considered, bytes shipped,
|
|
9
|
+
* 429 counts, breaker trips, RSS peak — and then throws it away.
|
|
10
|
+
*
|
|
11
|
+
* ── What it is not ───────────────────────────────────────────────────────
|
|
12
|
+
* Not analytics, not usage tracking, not anything about what the user is doing.
|
|
13
|
+
* `assertNoSensitiveKeys` refuses a payload carrying a path, project, session id,
|
|
14
|
+
* prompt or content key, so the promise is enforced by the type of failure a
|
|
15
|
+
* developer cannot ignore rather than by review.
|
|
16
|
+
*
|
|
17
|
+
* ── Gates ────────────────────────────────────────────────────────────────
|
|
18
|
+
* Both must open (see telemetry-consent.ts): the user has not opted out, AND the
|
|
19
|
+
* server says this tenant's plan includes it. The server checks the plan again at
|
|
20
|
+
* ingest, so this gate exists to stop the data LEAVING a machine that should not
|
|
21
|
+
* send it — the client is never the boundary.
|
|
22
|
+
*
|
|
23
|
+
* ── Failure policy ───────────────────────────────────────────────────────
|
|
24
|
+
* Fire-and-forget, and silent. Telemetry that logs its own failures produces
|
|
25
|
+
* noise about the thing that was supposed to reduce noise, and a collector must
|
|
26
|
+
* never fail a sync because it could not report on the sync.
|
|
27
|
+
*/
|
|
28
|
+
import { hostname, platform } from 'os';
|
|
29
|
+
import { mayReport, assertNoSensitiveKeys } from './telemetry-consent.js';
|
|
30
|
+
import { fetchWithTimeout } from './http.js';
|
|
31
|
+
import { loadAllCredentials } from './sync-client.js';
|
|
32
|
+
const cliVersion = () => {
|
|
33
|
+
try {
|
|
34
|
+
return typeof __CLI_VERSION__ === 'string' ? __CLI_VERSION__ : 'dev';
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
return 'dev';
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
const MAX_QUEUE = 50;
|
|
41
|
+
const queue = [];
|
|
42
|
+
/**
|
|
43
|
+
* Record an event. Cheap and synchronous — it never blocks the caller and never
|
|
44
|
+
* throws, because a measurement must not be able to break the thing it measures.
|
|
45
|
+
*/
|
|
46
|
+
export function record(event) {
|
|
47
|
+
try {
|
|
48
|
+
assertNoSensitiveKeys(event);
|
|
49
|
+
}
|
|
50
|
+
catch (err) {
|
|
51
|
+
// A forbidden key is a developer error, not a runtime condition. Loud in
|
|
52
|
+
// development, dropped in production — never sent either way.
|
|
53
|
+
if (process.env.NODE_ENV !== 'production')
|
|
54
|
+
throw err;
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
if (queue.length >= MAX_QUEUE)
|
|
58
|
+
queue.shift(); // newest matter most
|
|
59
|
+
queue.push(event);
|
|
60
|
+
}
|
|
61
|
+
/** Pending events, for tests and `doctor`. */
|
|
62
|
+
export function pending() { return queue; }
|
|
63
|
+
export function _clearForTests() { queue.length = 0; }
|
|
64
|
+
/**
|
|
65
|
+
* Ship whatever is queued to every eligible target, then clear.
|
|
66
|
+
*
|
|
67
|
+
* Called after a sync completes — the connection is warm, the numbers are final,
|
|
68
|
+
* and there is no separate schedule to get wrong.
|
|
69
|
+
*/
|
|
70
|
+
export async function flush() {
|
|
71
|
+
if (queue.length === 0)
|
|
72
|
+
return;
|
|
73
|
+
let targets = [];
|
|
74
|
+
try {
|
|
75
|
+
targets = loadAllCredentials();
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
const eligible = targets.filter((t) => mayReport(t.serverUrl));
|
|
81
|
+
if (eligible.length === 0) {
|
|
82
|
+
// Nothing may receive these. Drop them rather than growing a queue that will
|
|
83
|
+
// never drain — a user who opted out must not accumulate a backlog either.
|
|
84
|
+
queue.length = 0;
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
const events = queue.map((e) => ({
|
|
88
|
+
...e,
|
|
89
|
+
ts: Date.now(),
|
|
90
|
+
cliVersion: cliVersion(),
|
|
91
|
+
os: platform(),
|
|
92
|
+
// A STABLE, NON-IDENTIFYING machine marker. The server overwrites device_id
|
|
93
|
+
// from the authenticated token anyway (it is the only side that can resolve
|
|
94
|
+
// it), so this is a fallback for older servers and is deliberately a hash,
|
|
95
|
+
// never the hostname itself.
|
|
96
|
+
deviceId: hashHost(),
|
|
97
|
+
}));
|
|
98
|
+
queue.length = 0;
|
|
99
|
+
await Promise.allSettled(eligible.map(async (t) => {
|
|
100
|
+
try {
|
|
101
|
+
await fetchWithTimeout(`${t.serverUrl.replace(/\/+$/, '')}/api/client-events`, {
|
|
102
|
+
method: 'POST',
|
|
103
|
+
headers: {
|
|
104
|
+
'content-type': 'application/json',
|
|
105
|
+
...(t.token ? { authorization: `Bearer ${t.token}` } : {}),
|
|
106
|
+
},
|
|
107
|
+
body: JSON.stringify({ events }),
|
|
108
|
+
}, 10_000);
|
|
109
|
+
}
|
|
110
|
+
catch { /* fire and forget — see the header */ }
|
|
111
|
+
}));
|
|
112
|
+
}
|
|
113
|
+
/** sha256 of the hostname, first 12 — stable per machine, not reversible. */
|
|
114
|
+
function hashHost() {
|
|
115
|
+
try {
|
|
116
|
+
// Lazy so the crypto import is not paid by callers that never report.
|
|
117
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
118
|
+
const { createHash } = require('node:crypto');
|
|
119
|
+
return createHash('sha256').update(hostname()).digest('hex').slice(0, 12);
|
|
120
|
+
}
|
|
121
|
+
catch {
|
|
122
|
+
return '';
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
//# sourceMappingURL=telemetry.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"telemetry.js","sourceRoot":"","sources":["../src/telemetry.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One background task at a time, in priority order, with same-kind coalescing.
|
|
3
|
+
*
|
|
4
|
+
* ── What it replaces ─────────────────────────────────────────────────────
|
|
5
|
+
* Three independent in-flight booleans — `syncInFlight`, `intentDrainInFlight`,
|
|
6
|
+
* `codeIndexInFlight`. Each correctly stopped its OWN tick from overlapping
|
|
7
|
+
* itself, and none of them knew about the others, so a code-intelligence sweep
|
|
8
|
+
* could run at the same time as a full sync walk and an intent drain. All three
|
|
9
|
+
* are heavy: the sweep spawns codeindex and holds its output, the walk holds a
|
|
10
|
+
* session container, the drain copies files. Measured with a sweep and a walk
|
|
11
|
+
* overlapping, the main thread sat at 80–95% and RSS more than doubled.
|
|
12
|
+
*
|
|
13
|
+
* They also could not express what the daemon actually wants: shipping to the
|
|
14
|
+
* server matters more than refreshing code intelligence, so if both are due, the
|
|
15
|
+
* sweep should wait.
|
|
16
|
+
*
|
|
17
|
+
* ── Semantics ────────────────────────────────────────────────────────────
|
|
18
|
+
* SERIAL — exactly one task runs at a time.
|
|
19
|
+
*
|
|
20
|
+
* COALESCED PER KIND — if a kind is already waiting, a new request for that kind
|
|
21
|
+
* is dropped rather than queued twice. Both would do the same work against the
|
|
22
|
+
* same state, and the second would find nothing to do. This preserves what the
|
|
23
|
+
* booleans got right (no self-overlap) without the "silently skipped" behaviour:
|
|
24
|
+
* a request that arrives while its kind is RUNNING sets a re-run flag, so a file
|
|
25
|
+
* change during a walk still gets a walk afterwards instead of being lost.
|
|
26
|
+
*
|
|
27
|
+
* PRIORITISED — lower number first, and only among tasks already waiting. A
|
|
28
|
+
* running task is never preempted; a long sweep still finishes.
|
|
29
|
+
*/
|
|
30
|
+
export type TickKind = string;
|
|
31
|
+
export interface TickQueueOpts {
|
|
32
|
+
/** Reports every state change; the daemon logs through it. */
|
|
33
|
+
onError?: (kind: TickKind, err: unknown) => void;
|
|
34
|
+
}
|
|
35
|
+
export declare class TickQueue {
|
|
36
|
+
private readonly opts;
|
|
37
|
+
private waiting;
|
|
38
|
+
private running;
|
|
39
|
+
/** Kinds that asked to run while they were already running. */
|
|
40
|
+
private rerun;
|
|
41
|
+
constructor(opts?: TickQueueOpts);
|
|
42
|
+
/** The kind currently executing, or null when idle. Diagnostics only. */
|
|
43
|
+
get active(): TickKind | null;
|
|
44
|
+
get depth(): number;
|
|
45
|
+
/**
|
|
46
|
+
* Ask for `kind` to run. Returns immediately — this is a scheduler, not a
|
|
47
|
+
* `await`-able task.
|
|
48
|
+
*/
|
|
49
|
+
request(kind: TickKind, priority: number, run: () => Promise<void>): void;
|
|
50
|
+
private pump;
|
|
51
|
+
/** Tests: settle the queue. */
|
|
52
|
+
drain(): Promise<void>;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Priorities for the daemon's ticks. Lower runs first.
|
|
56
|
+
*
|
|
57
|
+
* Shipping to the server is the product; everything else is enrichment. Code
|
|
58
|
+
* intelligence is last because it is the heaviest and the least urgent — it
|
|
59
|
+
* refreshes on a 180-minute cadence, so waiting out a walk costs nothing.
|
|
60
|
+
*/
|
|
61
|
+
export declare const TICK_PRIORITY: {
|
|
62
|
+
readonly sync: 10;
|
|
63
|
+
readonly intents: 20;
|
|
64
|
+
readonly codeIndex: 30;
|
|
65
|
+
readonly housekeeping: 40;
|
|
66
|
+
};
|
|
67
|
+
//# sourceMappingURL=tick-queue.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tick-queue.d.ts","sourceRoot":"","sources":["../src/tick-queue.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AAEH,MAAM,MAAM,QAAQ,GAAG,MAAM,CAAC;AAQ9B,MAAM,WAAW,aAAa;IAC5B,8DAA8D;IAC9D,OAAO,CAAC,EAAE,CAAC,IAAI,EAAE,QAAQ,EAAE,GAAG,EAAE,OAAO,KAAK,IAAI,CAAC;CAClD;AAED,qBAAa,SAAS;IAMR,OAAO,CAAC,QAAQ,CAAC,IAAI;IALjC,OAAO,CAAC,OAAO,CAAiB;IAChC,OAAO,CAAC,OAAO,CAAyB;IACxC,+DAA+D;IAC/D,OAAO,CAAC,KAAK,CAAuB;gBAEP,IAAI,GAAE,aAAkB;IAErD,yEAAyE;IACzE,IAAI,MAAM,IAAI,QAAQ,GAAG,IAAI,CAAyB;IACtD,IAAI,KAAK,IAAI,MAAM,CAAgC;IAEnD;;;OAGG;IACH,OAAO,CAAC,IAAI,EAAE,QAAQ,EAAE,QAAQ,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI;YAY3D,IAAI;IAuBlB,+BAA+B;IACzB,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;CAK7B;AAED;;;;;;GAMG;AACH,eAAO,MAAM,aAAa;;;;;CAKhB,CAAC"}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One background task at a time, in priority order, with same-kind coalescing.
|
|
3
|
+
*
|
|
4
|
+
* ── What it replaces ─────────────────────────────────────────────────────
|
|
5
|
+
* Three independent in-flight booleans — `syncInFlight`, `intentDrainInFlight`,
|
|
6
|
+
* `codeIndexInFlight`. Each correctly stopped its OWN tick from overlapping
|
|
7
|
+
* itself, and none of them knew about the others, so a code-intelligence sweep
|
|
8
|
+
* could run at the same time as a full sync walk and an intent drain. All three
|
|
9
|
+
* are heavy: the sweep spawns codeindex and holds its output, the walk holds a
|
|
10
|
+
* session container, the drain copies files. Measured with a sweep and a walk
|
|
11
|
+
* overlapping, the main thread sat at 80–95% and RSS more than doubled.
|
|
12
|
+
*
|
|
13
|
+
* They also could not express what the daemon actually wants: shipping to the
|
|
14
|
+
* server matters more than refreshing code intelligence, so if both are due, the
|
|
15
|
+
* sweep should wait.
|
|
16
|
+
*
|
|
17
|
+
* ── Semantics ────────────────────────────────────────────────────────────
|
|
18
|
+
* SERIAL — exactly one task runs at a time.
|
|
19
|
+
*
|
|
20
|
+
* COALESCED PER KIND — if a kind is already waiting, a new request for that kind
|
|
21
|
+
* is dropped rather than queued twice. Both would do the same work against the
|
|
22
|
+
* same state, and the second would find nothing to do. This preserves what the
|
|
23
|
+
* booleans got right (no self-overlap) without the "silently skipped" behaviour:
|
|
24
|
+
* a request that arrives while its kind is RUNNING sets a re-run flag, so a file
|
|
25
|
+
* change during a walk still gets a walk afterwards instead of being lost.
|
|
26
|
+
*
|
|
27
|
+
* PRIORITISED — lower number first, and only among tasks already waiting. A
|
|
28
|
+
* running task is never preempted; a long sweep still finishes.
|
|
29
|
+
*/
|
|
30
|
+
export class TickQueue {
|
|
31
|
+
opts;
|
|
32
|
+
waiting = [];
|
|
33
|
+
running = null;
|
|
34
|
+
/** Kinds that asked to run while they were already running. */
|
|
35
|
+
rerun = new Set();
|
|
36
|
+
constructor(opts = {}) {
|
|
37
|
+
this.opts = opts;
|
|
38
|
+
}
|
|
39
|
+
/** The kind currently executing, or null when idle. Diagnostics only. */
|
|
40
|
+
get active() { return this.running; }
|
|
41
|
+
get depth() { return this.waiting.length; }
|
|
42
|
+
/**
|
|
43
|
+
* Ask for `kind` to run. Returns immediately — this is a scheduler, not a
|
|
44
|
+
* `await`-able task.
|
|
45
|
+
*/
|
|
46
|
+
request(kind, priority, run) {
|
|
47
|
+
if (this.running === kind) {
|
|
48
|
+
// Its state changed underneath it; run once more when it finishes rather
|
|
49
|
+
// than dropping the request (a file change during a walk must not be lost).
|
|
50
|
+
this.rerun.add(kind);
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
if (this.waiting.some((w) => w.kind === kind))
|
|
54
|
+
return; // coalesced
|
|
55
|
+
this.waiting.push({ kind, priority, run });
|
|
56
|
+
void this.pump();
|
|
57
|
+
}
|
|
58
|
+
async pump() {
|
|
59
|
+
if (this.running !== null)
|
|
60
|
+
return;
|
|
61
|
+
const next = this.waiting
|
|
62
|
+
.map((w, i) => ({ w, i }))
|
|
63
|
+
.sort((a, b) => (a.w.priority - b.w.priority) || (a.i - b.i))[0];
|
|
64
|
+
if (!next)
|
|
65
|
+
return;
|
|
66
|
+
this.waiting.splice(next.i, 1);
|
|
67
|
+
const { kind, priority, run } = next.w;
|
|
68
|
+
this.running = kind;
|
|
69
|
+
try {
|
|
70
|
+
await run();
|
|
71
|
+
}
|
|
72
|
+
catch (err) {
|
|
73
|
+
this.opts.onError?.(kind, err);
|
|
74
|
+
}
|
|
75
|
+
finally {
|
|
76
|
+
this.running = null;
|
|
77
|
+
if (this.rerun.delete(kind)) {
|
|
78
|
+
// Re-queue at its own priority; it takes its turn like anything else.
|
|
79
|
+
this.waiting.push({ kind, priority, run });
|
|
80
|
+
}
|
|
81
|
+
void this.pump();
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
/** Tests: settle the queue. */
|
|
85
|
+
async drain() {
|
|
86
|
+
while (this.running !== null || this.waiting.length > 0) {
|
|
87
|
+
await new Promise((r) => setImmediate(r));
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Priorities for the daemon's ticks. Lower runs first.
|
|
93
|
+
*
|
|
94
|
+
* Shipping to the server is the product; everything else is enrichment. Code
|
|
95
|
+
* intelligence is last because it is the heaviest and the least urgent — it
|
|
96
|
+
* refreshes on a 180-minute cadence, so waiting out a walk costs nothing.
|
|
97
|
+
*/
|
|
98
|
+
export const TICK_PRIORITY = {
|
|
99
|
+
sync: 10,
|
|
100
|
+
intents: 20,
|
|
101
|
+
codeIndex: 30,
|
|
102
|
+
housekeeping: 40,
|
|
103
|
+
};
|
|
104
|
+
//# sourceMappingURL=tick-queue.js.map
|