@cortexkit/opencode-magic-context 0.29.0 → 0.30.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/features/magic-context/dreamer/retrospective-raw-provider.d.ts +0 -1
- package/dist/features/magic-context/dreamer/retrospective-raw-provider.d.ts.map +1 -1
- package/dist/features/magic-context/dreamer/storage-task-schedule.d.ts +10 -0
- package/dist/features/magic-context/dreamer/storage-task-schedule.d.ts.map +1 -1
- package/dist/features/magic-context/dreamer/task-executor.d.ts +0 -3
- package/dist/features/magic-context/dreamer/task-executor.d.ts.map +1 -1
- package/dist/features/magic-context/dreamer/task-registry.d.ts +0 -1
- package/dist/features/magic-context/dreamer/task-registry.d.ts.map +1 -1
- package/dist/features/magic-context/memory/embedding-local.d.ts +0 -3
- package/dist/features/magic-context/memory/embedding-local.d.ts.map +1 -1
- package/dist/features/magic-context/storage-db.d.ts +2 -21
- package/dist/features/magic-context/storage-db.d.ts.map +1 -1
- package/dist/features/magic-context/storage-schema-helpers.d.ts +30 -0
- package/dist/features/magic-context/storage-schema-helpers.d.ts.map +1 -0
- package/dist/hooks/magic-context/channel2-delivery.d.ts +9 -5
- package/dist/hooks/magic-context/channel2-delivery.d.ts.map +1 -1
- package/dist/hooks/magic-context/event-handler.d.ts +1 -4
- package/dist/hooks/magic-context/event-handler.d.ts.map +1 -1
- package/dist/hooks/magic-context/hook.d.ts +0 -2
- package/dist/hooks/magic-context/hook.d.ts.map +1 -1
- package/dist/hooks/magic-context/transform.d.ts.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3118 -5026
- package/dist/plugin/hooks/create-session-hooks.d.ts.map +1 -1
- package/dist/plugin/rpc-handlers.d.ts.map +1 -1
- package/dist/shared/announcement.d.ts +1 -1
- package/dist/shared/announcement.d.ts.map +1 -1
- package/dist/shared/exit-abort-registry.d.ts +25 -0
- package/dist/shared/exit-abort-registry.d.ts.map +1 -0
- package/dist/shared/harness-provider-map.d.ts +30 -0
- package/dist/shared/harness-provider-map.d.ts.map +1 -0
- package/dist/tools/ctx-note/tools.d.ts.map +1 -1
- package/dist/tui/badge-contrast.d.ts +37 -22
- package/dist/tui/badge-contrast.d.ts.map +1 -1
- package/package.json +78 -78
- package/src/shared/announcement.ts +4 -3
- package/src/shared/exit-abort-registry.test.ts +50 -0
- package/src/shared/exit-abort-registry.ts +46 -0
- package/src/shared/harness-provider-map.test.ts +63 -0
- package/src/shared/harness-provider-map.ts +56 -0
- package/src/tui/badge-contrast.test.ts +39 -1
- package/src/tui/badge-contrast.ts +63 -25
- package/src/tui/slots/sidebar-content.tsx +2 -2
- package/dist/hooks/is-anthropic-provider.d.ts +0 -2
- package/dist/hooks/is-anthropic-provider.d.ts.map +0 -1
- package/dist/shared/live-server-client.d.ts +0 -50
- package/dist/shared/live-server-client.d.ts.map +0 -1
- package/src/shared/live-server-client.ts +0 -152
|
@@ -1,39 +1,52 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Pick
|
|
3
|
-
* background
|
|
2
|
+
* Pick the text color for the sidebar header badge (a bold label drawn on a
|
|
3
|
+
* `theme.accent` background).
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
* always opaque, so deriving the text color from it is transparency-proof.
|
|
5
|
+
* Primary rule (matches AFT's sidebar by construction): paint the theme's own
|
|
6
|
+
* `background` color as the label, the inverse-of-panel look. Because it is a
|
|
7
|
+
* fixed theme token rather than an accent-derived computation, MC's badge and
|
|
8
|
+
* AFT's badge agree on EVERY accent automatically, so the same theme can never
|
|
9
|
+
* make one badge black and the other white (issue #198).
|
|
11
10
|
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
11
|
+
* Fallback rule (the reason a luminance pick exists at all): `theme.background`
|
|
12
|
+
* can be unusable as a label color in two degenerate cases, where it would
|
|
13
|
+
* render the label invisible on the accent:
|
|
14
|
+
* 1. Transparent background. Themes that respect terminal transparency set
|
|
15
|
+
* `background: "none"`, which resolves to `RGBA(0,0,0,0)`; drawing it as
|
|
16
|
+
* text renders fully transparent and the label disappears (issue #186).
|
|
17
|
+
* 2. Background ~= accent. If the theme's background and accent are nearly the
|
|
18
|
+
* same color, background-on-accent text has no contrast.
|
|
19
|
+
* In either case we fall back to a black/white pick that is guaranteed visible
|
|
20
|
+
* on the always-opaque accent.
|
|
21
21
|
*
|
|
22
|
-
* `RGBA` channels from @opentui/core are normalized 0..1 floats
|
|
23
|
-
* minimal `{ r, g, b }` shape so this stays a pure, trivially
|
|
24
|
-
* independent of the native color class
|
|
22
|
+
* `RGBA` channels from @opentui/core are normalized 0..1 floats (alpha included).
|
|
23
|
+
* We accept the minimal `{ r, g, b, a? }` shape so this stays a pure, trivially
|
|
24
|
+
* testable function independent of the native color class, and we return the
|
|
25
|
+
* passed-in `background` object unchanged on the primary path so it stays the
|
|
26
|
+
* exact same theme token AFT uses.
|
|
25
27
|
*/
|
|
26
28
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
//
|
|
29
|
+
type Color = { r: number; g: number; b: number; a?: number };
|
|
30
|
+
|
|
31
|
+
// Below this alpha the theme background is too transparent to read as a label on
|
|
32
|
+
// the accent (issue #186: background:"none" resolves to alpha 0).
|
|
33
|
+
const MIN_OPAQUE_ALPHA = 0.5;
|
|
34
|
+
|
|
35
|
+
// If the theme background and accent are within this per-channel distance they
|
|
36
|
+
// are effectively the same color, so background-on-accent text is unreadable.
|
|
37
|
+
const MIN_CHANNEL_DISTANCE = 0.06;
|
|
38
|
+
|
|
39
|
+
// Luminance midpoint for the fallback pick: accents below this keep white text,
|
|
40
|
+
// accents at/above it (light/pastel/near-white) get black. White-biased relative
|
|
41
|
+
// to the strict equal-contrast crossover (~0.179) so saturated mid-tone accents
|
|
42
|
+
// stay white. Only consulted on the degenerate fallback path.
|
|
30
43
|
const LIGHT_ACCENT_LUMINANCE = 0.5;
|
|
31
44
|
|
|
32
45
|
function srgbChannelToLinear(c: number): number {
|
|
33
46
|
return c <= 0.03928 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4;
|
|
34
47
|
}
|
|
35
48
|
|
|
36
|
-
function relativeLuminance(bg:
|
|
49
|
+
function relativeLuminance(bg: Color): number {
|
|
37
50
|
return (
|
|
38
51
|
0.2126 * srgbChannelToLinear(bg.r) +
|
|
39
52
|
0.7152 * srgbChannelToLinear(bg.g) +
|
|
@@ -41,6 +54,31 @@ function relativeLuminance(bg: { r: number; g: number; b: number }): number {
|
|
|
41
54
|
);
|
|
42
55
|
}
|
|
43
56
|
|
|
44
|
-
|
|
57
|
+
function nearlyEqual(a: Color, b: Color): boolean {
|
|
58
|
+
return (
|
|
59
|
+
Math.abs(a.r - b.r) < MIN_CHANNEL_DISTANCE &&
|
|
60
|
+
Math.abs(a.g - b.g) < MIN_CHANNEL_DISTANCE &&
|
|
61
|
+
Math.abs(a.b - b.b) < MIN_CHANNEL_DISTANCE
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Pure black/white pick by accent luminance. Used as the badge fallback and kept
|
|
67
|
+
* exported for callers that only have the accent.
|
|
68
|
+
*/
|
|
69
|
+
export function readableTextColorOn(bg: Color): string {
|
|
45
70
|
return relativeLuminance(bg) < LIGHT_ACCENT_LUMINANCE ? "#ffffff" : "#000000";
|
|
46
71
|
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Badge label color on the accent: the theme background (AFT parity) when it is
|
|
75
|
+
* usable, else a guaranteed-visible black/white fallback. Returns the passed-in
|
|
76
|
+
* `background` reference unchanged on the primary path.
|
|
77
|
+
*/
|
|
78
|
+
export function badgeTextColor<T extends Color>(accent: T, background: T): T | string {
|
|
79
|
+
const alpha = background.a ?? 1;
|
|
80
|
+
if (alpha >= MIN_OPAQUE_ALPHA && !nearlyEqual(accent, background)) {
|
|
81
|
+
return background;
|
|
82
|
+
}
|
|
83
|
+
return readableTextColorOn(accent);
|
|
84
|
+
}
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { Show, createEffect, createMemo, createSignal, on, onCleanup } from "solid-js"
|
|
3
3
|
import type { TuiSlotPlugin, TuiPluginApi, TuiThemeCurrent } from "@opencode-ai/plugin/tui"
|
|
4
4
|
import packageJson from "../../../package.json"
|
|
5
|
-
import {
|
|
5
|
+
import { badgeTextColor } from '../badge-contrast';
|
|
6
6
|
import { loadSidebarSnapshot, type SidebarSnapshot } from "../data/context-db"
|
|
7
7
|
import { formatThresholdPercent } from "../../shared/format-threshold"
|
|
8
8
|
import {
|
|
@@ -695,7 +695,7 @@ const SidebarContent = (props: {
|
|
|
695
695
|
onMouseDown={() => props.controller.toggleCollapsed()}
|
|
696
696
|
>
|
|
697
697
|
<box paddingLeft={1} paddingRight={1} backgroundColor={props.theme.accent}>
|
|
698
|
-
<text fg={
|
|
698
|
+
<text fg={badgeTextColor(props.theme.accent, props.theme.background)}>
|
|
699
699
|
<b>{collapsed() ? "▶ " : "▼ "}{headerLabel()}</b>
|
|
700
700
|
</text>
|
|
701
701
|
</box>
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"is-anthropic-provider.d.ts","sourceRoot":"","sources":["../../src/hooks/is-anthropic-provider.ts"],"names":[],"mappings":"AAAA,wBAAgB,mBAAmB,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAE/D"}
|
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Live-server client for the Channel 2 ctx_reduce ceiling nudge (a synthetic
|
|
3
|
-
* user `<system-reminder>` delivered via `promptAsync`).
|
|
4
|
-
*
|
|
5
|
-
* WHY a separate client instead of the plugin-provided `input.client`:
|
|
6
|
-
* OpenCode's plugin `input.client` routes through `Server.Default().app.fetch`,
|
|
7
|
-
* which uses a SEPARATE Effect `memoMap` from the live HTTP listener the UI
|
|
8
|
-
* uses. `SessionRunState` lives per-memoMap, so a plugin-origin `promptAsync`
|
|
9
|
-
* observes an "idle" runner while the live turn is still running, `ensureRunning`
|
|
10
|
-
* fails to coalesce, and OpenCode persists duplicate assistant children
|
|
11
|
-
* (upstream bug anomalyco/opencode#28202). Building a `createOpencodeClient`
|
|
12
|
-
* aimed at `input.serverUrl` via `globalThis.fetch` enters the SAME live
|
|
13
|
-
* listener, so `ensureRunning` sees the real run and coalesces — the synthetic
|
|
14
|
-
* message lands at the tail after the current assistant step.
|
|
15
|
-
*
|
|
16
|
-
* The live listener is only reachable on OpenCode Desktop (Electron+Node) and
|
|
17
|
-
* TUI launched with `--port 0`; plain TUI binds an internal listener that 404s
|
|
18
|
-
* `/session/*`. We probe once at init and cache per `serverUrl`. When
|
|
19
|
-
* unreachable, Channel 2 is DISABLED (Channel 1 + 85% force-materialization
|
|
20
|
-
* remain the backstop) — MC deliberately does NOT fall back to the in-process
|
|
21
|
-
* client because that would knowingly trigger #28202.
|
|
22
|
-
*/
|
|
23
|
-
import { createOpencodeClient } from "@opencode-ai/sdk";
|
|
24
|
-
export type LiveServerClient = ReturnType<typeof createOpencodeClient>;
|
|
25
|
-
/**
|
|
26
|
-
* Cached `createOpencodeClient` aimed at the live HTTP listener for the given
|
|
27
|
-
* `(serverUrl, directory)`. One client is reused across deliveries.
|
|
28
|
-
*/
|
|
29
|
-
export declare function getLiveServerClient(serverUrl: string, directory: string): LiveServerClient;
|
|
30
|
-
/**
|
|
31
|
-
* Probe whether `serverUrl` serves OpenCode's HTTP API within `timeoutMs`.
|
|
32
|
-
* `true` only when `/session` proves the API is usable: any 2xx, or 401/403
|
|
33
|
-
* (auth-protected listener still exists). `false` for 404 (plain TUI internal
|
|
34
|
-
* listener), 5xx, connection refused, DNS failure, timeout, or malformed URL.
|
|
35
|
-
* Records the result + timestamp in the per-serverUrl cache.
|
|
36
|
-
*/
|
|
37
|
-
export declare function probeServerReachable(serverUrl: string | undefined, timeoutMs?: number): Promise<boolean>;
|
|
38
|
-
/** Record a probe result directly (test helper / explicit override). */
|
|
39
|
-
export declare function setLiveServerWakeAvailable(serverUrl: string | undefined, available: boolean): void;
|
|
40
|
-
/**
|
|
41
|
-
* Should Channel 2 deliver through the live-server client for `serverUrl`?
|
|
42
|
-
* Returns false when never probed or the last probe failed. A stale decision
|
|
43
|
-
* (older than the TTL) returns false so the caller re-probes before delivering.
|
|
44
|
-
*/
|
|
45
|
-
export declare function useLiveServerWake(serverUrl?: string): boolean;
|
|
46
|
-
/** True when a usable (non-stale) probe decision exists, regardless of outcome. */
|
|
47
|
-
export declare function hasFreshProbe(serverUrl?: string): boolean;
|
|
48
|
-
/** Test helper — reset both caches between cases. */
|
|
49
|
-
export declare function __resetLiveServerClientForTests(): void;
|
|
50
|
-
//# sourceMappingURL=live-server-client.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"live-server-client.d.ts","sourceRoot":"","sources":["../../src/shared/live-server-client.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEH,OAAO,EAAE,oBAAoB,EAAE,MAAM,kBAAkB,CAAC;AAExD,MAAM,MAAM,gBAAgB,GAAG,UAAU,CAAC,OAAO,oBAAoB,CAAC,CAAC;AA0BvE;;;GAGG;AACH,wBAAgB,mBAAmB,CAAC,SAAS,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,gBAAgB,CAY1F;AAcD;;;;;;GAMG;AACH,wBAAsB,oBAAoB,CACtC,SAAS,EAAE,MAAM,GAAG,SAAS,EAC7B,SAAS,SAAO,GACjB,OAAO,CAAC,OAAO,CAAC,CAqBlB;AAED,wEAAwE;AACxE,wBAAgB,0BAA0B,CACtC,SAAS,EAAE,MAAM,GAAG,SAAS,EAC7B,SAAS,EAAE,OAAO,GACnB,IAAI,CAMN;AAED;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,SAAS,CAAC,EAAE,MAAM,GAAG,OAAO,CAM7D;AAED,mFAAmF;AACnF,wBAAgB,aAAa,CAAC,SAAS,CAAC,EAAE,MAAM,GAAG,OAAO,CAKzD;AAED,qDAAqD;AACrD,wBAAgB,+BAA+B,IAAI,IAAI,CAGtD"}
|
|
@@ -1,152 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Live-server client for the Channel 2 ctx_reduce ceiling nudge (a synthetic
|
|
3
|
-
* user `<system-reminder>` delivered via `promptAsync`).
|
|
4
|
-
*
|
|
5
|
-
* WHY a separate client instead of the plugin-provided `input.client`:
|
|
6
|
-
* OpenCode's plugin `input.client` routes through `Server.Default().app.fetch`,
|
|
7
|
-
* which uses a SEPARATE Effect `memoMap` from the live HTTP listener the UI
|
|
8
|
-
* uses. `SessionRunState` lives per-memoMap, so a plugin-origin `promptAsync`
|
|
9
|
-
* observes an "idle" runner while the live turn is still running, `ensureRunning`
|
|
10
|
-
* fails to coalesce, and OpenCode persists duplicate assistant children
|
|
11
|
-
* (upstream bug anomalyco/opencode#28202). Building a `createOpencodeClient`
|
|
12
|
-
* aimed at `input.serverUrl` via `globalThis.fetch` enters the SAME live
|
|
13
|
-
* listener, so `ensureRunning` sees the real run and coalesces — the synthetic
|
|
14
|
-
* message lands at the tail after the current assistant step.
|
|
15
|
-
*
|
|
16
|
-
* The live listener is only reachable on OpenCode Desktop (Electron+Node) and
|
|
17
|
-
* TUI launched with `--port 0`; plain TUI binds an internal listener that 404s
|
|
18
|
-
* `/session/*`. We probe once at init and cache per `serverUrl`. When
|
|
19
|
-
* unreachable, Channel 2 is DISABLED (Channel 1 + 85% force-materialization
|
|
20
|
-
* remain the backstop) — MC deliberately does NOT fall back to the in-process
|
|
21
|
-
* client because that would knowingly trigger #28202.
|
|
22
|
-
*/
|
|
23
|
-
|
|
24
|
-
import { createOpencodeClient } from "@opencode-ai/sdk";
|
|
25
|
-
|
|
26
|
-
export type LiveServerClient = ReturnType<typeof createOpencodeClient>;
|
|
27
|
-
|
|
28
|
-
const clientCache = new Map<string, LiveServerClient>();
|
|
29
|
-
|
|
30
|
-
function cacheKey(serverUrl: string, directory: string): string {
|
|
31
|
-
return `${serverUrl}|${directory}`;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
function normalizeServerUrl(serverUrl: string): string {
|
|
35
|
-
try {
|
|
36
|
-
return new URL(serverUrl).toString();
|
|
37
|
-
} catch {
|
|
38
|
-
return serverUrl;
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
/** Basic-auth header OpenCode expects when `OPENCODE_SERVER_PASSWORD` is set. */
|
|
43
|
-
function serverAuthHeaders(): Record<string, string> | undefined {
|
|
44
|
-
const password = process.env.OPENCODE_SERVER_PASSWORD;
|
|
45
|
-
if (!password) return undefined;
|
|
46
|
-
const username = process.env.OPENCODE_SERVER_USERNAME ?? "opencode";
|
|
47
|
-
return {
|
|
48
|
-
Authorization: `Basic ${Buffer.from(`${username}:${password}`).toString("base64")}`,
|
|
49
|
-
};
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
/**
|
|
53
|
-
* Cached `createOpencodeClient` aimed at the live HTTP listener for the given
|
|
54
|
-
* `(serverUrl, directory)`. One client is reused across deliveries.
|
|
55
|
-
*/
|
|
56
|
-
export function getLiveServerClient(serverUrl: string, directory: string): LiveServerClient {
|
|
57
|
-
const key = cacheKey(serverUrl, directory);
|
|
58
|
-
const cached = clientCache.get(key);
|
|
59
|
-
if (cached) return cached;
|
|
60
|
-
const client = createOpencodeClient({
|
|
61
|
-
baseUrl: serverUrl,
|
|
62
|
-
directory,
|
|
63
|
-
headers: serverAuthHeaders(),
|
|
64
|
-
fetch: globalThis.fetch,
|
|
65
|
-
});
|
|
66
|
-
clientCache.set(key, client);
|
|
67
|
-
return client;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
// Per-serverUrl wake decision + probe TTL. One plugin process can host multiple
|
|
71
|
-
// OpenCode windows with different listener URLs, so the decision must be keyed.
|
|
72
|
-
interface ProbeDecision {
|
|
73
|
-
reachable: boolean;
|
|
74
|
-
probedAt: number;
|
|
75
|
-
}
|
|
76
|
-
const wakeDecisionByServerUrl = new Map<string, ProbeDecision>();
|
|
77
|
-
|
|
78
|
-
// Re-probe window: a transient 404/timeout shouldn't permanently disable
|
|
79
|
-
// Channel 2 for the whole session lifetime (per council-r3).
|
|
80
|
-
const PROBE_TTL_MS = 10 * 60_000;
|
|
81
|
-
|
|
82
|
-
/**
|
|
83
|
-
* Probe whether `serverUrl` serves OpenCode's HTTP API within `timeoutMs`.
|
|
84
|
-
* `true` only when `/session` proves the API is usable: any 2xx, or 401/403
|
|
85
|
-
* (auth-protected listener still exists). `false` for 404 (plain TUI internal
|
|
86
|
-
* listener), 5xx, connection refused, DNS failure, timeout, or malformed URL.
|
|
87
|
-
* Records the result + timestamp in the per-serverUrl cache.
|
|
88
|
-
*/
|
|
89
|
-
export async function probeServerReachable(
|
|
90
|
-
serverUrl: string | undefined,
|
|
91
|
-
timeoutMs = 1500,
|
|
92
|
-
): Promise<boolean> {
|
|
93
|
-
if (!serverUrl) return false;
|
|
94
|
-
const normalized = normalizeServerUrl(serverUrl);
|
|
95
|
-
const controller = new AbortController();
|
|
96
|
-
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
97
|
-
let reachable = false;
|
|
98
|
-
try {
|
|
99
|
-
const probeUrl = new URL("/session", serverUrl).toString();
|
|
100
|
-
const res = await globalThis.fetch(probeUrl, {
|
|
101
|
-
method: "GET",
|
|
102
|
-
headers: serverAuthHeaders(),
|
|
103
|
-
signal: controller.signal,
|
|
104
|
-
});
|
|
105
|
-
reachable = res.ok || res.status === 401 || res.status === 403;
|
|
106
|
-
} catch {
|
|
107
|
-
reachable = false;
|
|
108
|
-
} finally {
|
|
109
|
-
clearTimeout(timer);
|
|
110
|
-
wakeDecisionByServerUrl.set(normalized, { reachable, probedAt: Date.now() });
|
|
111
|
-
}
|
|
112
|
-
return reachable;
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
/** Record a probe result directly (test helper / explicit override). */
|
|
116
|
-
export function setLiveServerWakeAvailable(
|
|
117
|
-
serverUrl: string | undefined,
|
|
118
|
-
available: boolean,
|
|
119
|
-
): void {
|
|
120
|
-
if (!serverUrl) return;
|
|
121
|
-
wakeDecisionByServerUrl.set(normalizeServerUrl(serverUrl), {
|
|
122
|
-
reachable: available,
|
|
123
|
-
probedAt: Date.now(),
|
|
124
|
-
});
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
/**
|
|
128
|
-
* Should Channel 2 deliver through the live-server client for `serverUrl`?
|
|
129
|
-
* Returns false when never probed or the last probe failed. A stale decision
|
|
130
|
-
* (older than the TTL) returns false so the caller re-probes before delivering.
|
|
131
|
-
*/
|
|
132
|
-
export function useLiveServerWake(serverUrl?: string): boolean {
|
|
133
|
-
if (!serverUrl) return false;
|
|
134
|
-
const decision = wakeDecisionByServerUrl.get(normalizeServerUrl(serverUrl));
|
|
135
|
-
if (!decision) return false;
|
|
136
|
-
if (Date.now() - decision.probedAt > PROBE_TTL_MS) return false;
|
|
137
|
-
return decision.reachable;
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
/** True when a usable (non-stale) probe decision exists, regardless of outcome. */
|
|
141
|
-
export function hasFreshProbe(serverUrl?: string): boolean {
|
|
142
|
-
if (!serverUrl) return false;
|
|
143
|
-
const decision = wakeDecisionByServerUrl.get(normalizeServerUrl(serverUrl));
|
|
144
|
-
if (!decision) return false;
|
|
145
|
-
return Date.now() - decision.probedAt <= PROBE_TTL_MS;
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
/** Test helper — reset both caches between cases. */
|
|
149
|
-
export function __resetLiveServerClientForTests(): void {
|
|
150
|
-
clientCache.clear();
|
|
151
|
-
wakeDecisionByServerUrl.clear();
|
|
152
|
-
}
|