@telora/daemon 0.18.58 → 0.18.67
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/build-info.json +4 -3
- package/dist/cli/connect.d.ts +15 -11
- package/dist/cli/connect.d.ts.map +1 -1
- package/dist/cli/connect.js +20 -24
- package/dist/cli/connect.js.map +1 -1
- package/dist/completion/review-exit-phase.d.ts.map +1 -1
- package/dist/completion/review-exit-phase.js +37 -10
- package/dist/completion/review-exit-phase.js.map +1 -1
- package/dist/config.d.ts +16 -3
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +69 -13
- package/dist/config.js.map +1 -1
- package/dist/delivery-reconciler.d.ts +21 -1
- package/dist/delivery-reconciler.d.ts.map +1 -1
- package/dist/delivery-reconciler.js +21 -3
- package/dist/delivery-reconciler.js.map +1 -1
- package/dist/discovery-poll.d.ts.map +1 -1
- package/dist/discovery-poll.js +14 -2
- package/dist/discovery-poll.js.map +1 -1
- package/dist/focus-executor.d.ts +32 -3
- package/dist/focus-executor.d.ts.map +1 -1
- package/dist/focus-executor.js +89 -2
- package/dist/focus-executor.js.map +1 -1
- package/dist/focus-lifecycle.js +1 -1
- package/dist/focus-lifecycle.js.map +1 -1
- package/dist/focus-provisioning.js +1 -1
- package/dist/focus-provisioning.js.map +1 -1
- package/dist/git-fetch.d.ts +123 -0
- package/dist/git-fetch.d.ts.map +1 -0
- package/dist/git-fetch.js +164 -0
- package/dist/git-fetch.js.map +1 -0
- package/dist/git-state-detector.d.ts.map +1 -1
- package/dist/git-state-detector.js +63 -26
- package/dist/git-state-detector.js.map +1 -1
- package/dist/host-control/capability.d.ts +31 -0
- package/dist/host-control/capability.d.ts.map +1 -0
- package/dist/host-control/capability.js +50 -0
- package/dist/host-control/capability.js.map +1 -0
- package/dist/host-control/catalog.d.ts +48 -0
- package/dist/host-control/catalog.d.ts.map +1 -0
- package/dist/host-control/catalog.js +149 -0
- package/dist/host-control/catalog.js.map +1 -0
- package/dist/host-control/client.d.ts +76 -0
- package/dist/host-control/client.d.ts.map +1 -0
- package/dist/host-control/client.js +203 -0
- package/dist/host-control/client.js.map +1 -0
- package/dist/host-control/index.d.ts +18 -0
- package/dist/host-control/index.d.ts.map +1 -0
- package/dist/host-control/index.js +14 -0
- package/dist/host-control/index.js.map +1 -0
- package/dist/host-control/load.d.ts +136 -0
- package/dist/host-control/load.d.ts.map +1 -0
- package/dist/host-control/load.js +241 -0
- package/dist/host-control/load.js.map +1 -0
- package/dist/host-control/sizing.d.ts +207 -0
- package/dist/host-control/sizing.d.ts.map +1 -0
- package/dist/host-control/sizing.js +464 -0
- package/dist/host-control/sizing.js.map +1 -0
- package/dist/host-control/types.d.ts +174 -0
- package/dist/host-control/types.d.ts.map +1 -0
- package/dist/host-control/types.js +16 -0
- package/dist/host-control/types.js.map +1 -0
- package/dist/prd-controller.d.ts +10 -12
- package/dist/prd-controller.d.ts.map +1 -1
- package/dist/prd-controller.js +24 -54
- package/dist/prd-controller.js.map +1 -1
- package/dist/prompt-sections/persona-sections.d.ts +19 -0
- package/dist/prompt-sections/persona-sections.d.ts.map +1 -1
- package/dist/prompt-sections/persona-sections.js +55 -0
- package/dist/prompt-sections/persona-sections.js.map +1 -1
- package/dist/review-defect-detector.d.ts +33 -0
- package/dist/review-defect-detector.d.ts.map +1 -1
- package/dist/review-defect-detector.js +53 -0
- package/dist/review-defect-detector.js.map +1 -1
- package/dist/team-prompt-base.d.ts +1 -1
- package/dist/team-prompt-base.d.ts.map +1 -1
- package/dist/team-prompt-base.js +1 -1
- package/dist/team-prompt-base.js.map +1 -1
- package/dist/unified-init.d.ts.map +1 -1
- package/dist/unified-init.js +0 -2
- package/dist/unified-init.js.map +1 -1
- package/dist/unified-shell.d.ts.map +1 -1
- package/dist/unified-shell.js +0 -2
- package/dist/unified-shell.js.map +1 -1
- package/dist/worktree.d.ts +5 -2
- package/dist/worktree.d.ts.map +1 -1
- package/dist/worktree.js +6 -5
- package/dist/worktree.js.map +1 -1
- package/package.json +3 -3
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Off-event-loop git fetch + circuit breaker for the per-cycle origin fetch.
|
|
3
|
+
*
|
|
4
|
+
* The poll loop's git-state detection fetches from origin every cycle. When the
|
|
5
|
+
* remote is unreachable that fetch used to run SYNCHRONOUSLY (`execFileSync`) on
|
|
6
|
+
* the daemon's single Node event loop, blocking it for the full timeout (30s)
|
|
7
|
+
* and tripping the event-loop-lag watchdog while starving unrelated
|
|
8
|
+
* fire-and-forget work (local-model discovery, telemetry). This module isolates
|
|
9
|
+
* that failure into three composable pieces:
|
|
10
|
+
*
|
|
11
|
+
* 1. `gitFetchOrigin` runs the fetch OFF the hot path via async `execFile` with
|
|
12
|
+
* an `AbortSignal` abort path, so a slow/hung fetch never blocks the loop --
|
|
13
|
+
* the `await` yields to the event loop while the child process runs.
|
|
14
|
+
* 2. The default timeout (`GIT_FETCH_DEFAULT_TIMEOUT_MS`) is bounded well below
|
|
15
|
+
* the watchdog threshold (`WATCHDOG_LAG_THRESHOLD_MS`) so a hung remote costs
|
|
16
|
+
* a few seconds per attempt, not a full 30s lag window.
|
|
17
|
+
* 3. `GitFetchCircuit` is a failure-count -> open -> backoff -> half-open ->
|
|
18
|
+
* recover state machine: after N consecutive failures it backs off instead
|
|
19
|
+
* of retrying every cycle, emitting ONE degraded-mode signal (`justOpened`)
|
|
20
|
+
* rather than a per-cycle storm, and recovering once the remote is reachable.
|
|
21
|
+
*/
|
|
22
|
+
import { execFile } from 'node:child_process';
|
|
23
|
+
// ---------------------------------------------------------------------------
|
|
24
|
+
// Timeout bounds
|
|
25
|
+
// ---------------------------------------------------------------------------
|
|
26
|
+
/**
|
|
27
|
+
* The heartbeat watchdog's event-loop-lag threshold
|
|
28
|
+
* (`EVENT_LOOP_LAG_WARNING_MS` in heartbeat.ts). The fetch timeout must stay
|
|
29
|
+
* well below this: a fetch that could run for the full threshold is exactly the
|
|
30
|
+
* window that, on the old synchronous path, tripped the watchdog. Even off-loop
|
|
31
|
+
* (where a long fetch no longer lags the loop) this caps cost-per-attempt.
|
|
32
|
+
*/
|
|
33
|
+
export const WATCHDOG_LAG_THRESHOLD_MS = 30_000;
|
|
34
|
+
/**
|
|
35
|
+
* Default origin-fetch timeout: a few seconds, well below
|
|
36
|
+
* {@link WATCHDOG_LAG_THRESHOLD_MS}. Mirrors the `TELORA_GIT_FETCH_TIMEOUT_MS`
|
|
37
|
+
* env default in `@telora/daemon-core` (operators override via that env var for
|
|
38
|
+
* high-latency remotes); used directly as the fallback when no timeout is passed.
|
|
39
|
+
*/
|
|
40
|
+
export const GIT_FETCH_DEFAULT_TIMEOUT_MS = 5_000;
|
|
41
|
+
/** Max bytes of stdout/stderr to buffer from the child (10 MiB). */
|
|
42
|
+
const MAX_FETCH_BUFFER_BYTES = 10 * 1024 * 1024;
|
|
43
|
+
/**
|
|
44
|
+
* Default async runner: `execFile('git', ...)` -- a child process whose work
|
|
45
|
+
* runs off the event loop. The `timeout` option kills the child on overrun and
|
|
46
|
+
* the optional `signal` provides an explicit abort path; either resolves to a
|
|
47
|
+
* non-fatal failure result rather than throwing.
|
|
48
|
+
*/
|
|
49
|
+
export const defaultAsyncGitRunner = (args, cwd, opts) => new Promise((resolve) => {
|
|
50
|
+
execFile('git', args, {
|
|
51
|
+
cwd,
|
|
52
|
+
encoding: 'utf-8',
|
|
53
|
+
timeout: opts.timeoutMs,
|
|
54
|
+
signal: opts.signal,
|
|
55
|
+
maxBuffer: MAX_FETCH_BUFFER_BYTES,
|
|
56
|
+
}, (err, stdout, stderr) => {
|
|
57
|
+
if (!err) {
|
|
58
|
+
resolve({ success: true, output: (stdout ?? '').trim(), error: '' });
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
const execErr = err;
|
|
62
|
+
// Timeout (Node kills the child -> killed/SIGTERM) or explicit abort.
|
|
63
|
+
if (execErr.killed ||
|
|
64
|
+
execErr.signal === 'SIGTERM' ||
|
|
65
|
+
execErr.code === 'ABORT_ERR' ||
|
|
66
|
+
execErr.name === 'AbortError') {
|
|
67
|
+
resolve({
|
|
68
|
+
success: false,
|
|
69
|
+
output: (stdout ?? '').toString(),
|
|
70
|
+
error: `Git operation timed out after ${opts.timeoutMs}ms: git ${args.join(' ')}`,
|
|
71
|
+
});
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
resolve({
|
|
75
|
+
success: false,
|
|
76
|
+
output: (stdout ?? '').toString(),
|
|
77
|
+
error: (stderr ?? '').toString() || execErr.message || 'Unknown git error',
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
/**
|
|
82
|
+
* Fetch refs from origin OFF the event-loop hot path.
|
|
83
|
+
*
|
|
84
|
+
* Equivalent to the old synchronous `runGitSync(['fetch', '--quiet', 'origin'])`
|
|
85
|
+
* but non-blocking: the returned promise resolves when the child process exits,
|
|
86
|
+
* leaving the loop free to service discovery/telemetry/timers meanwhile.
|
|
87
|
+
*/
|
|
88
|
+
export async function gitFetchOrigin(repoPath, opts = {}, runner = defaultAsyncGitRunner) {
|
|
89
|
+
const timeoutMs = opts.timeoutMs ?? GIT_FETCH_DEFAULT_TIMEOUT_MS;
|
|
90
|
+
return runner(['fetch', '--quiet', 'origin'], repoPath, {
|
|
91
|
+
timeoutMs,
|
|
92
|
+
signal: opts.signal,
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
/** Consecutive failures before the circuit opens and backs off. */
|
|
96
|
+
export const GIT_FETCH_FAILURE_THRESHOLD = 3;
|
|
97
|
+
/** How long the circuit stays open before a half-open probe (5 minutes). */
|
|
98
|
+
export const GIT_FETCH_BACKOFF_MS = 5 * 60 * 1000;
|
|
99
|
+
/**
|
|
100
|
+
* Failure-count -> open -> backoff -> half-open -> recover state machine around
|
|
101
|
+
* the origin fetch. Pure and clock-injectable; holds no I/O. The caller gates
|
|
102
|
+
* each cycle's fetch on {@link shouldAttempt} and feeds the outcome back via
|
|
103
|
+
* {@link recordSuccess} / {@link recordFailure}.
|
|
104
|
+
*/
|
|
105
|
+
export class GitFetchCircuit {
|
|
106
|
+
state = 'closed';
|
|
107
|
+
consecutiveFailures = 0;
|
|
108
|
+
openedAt = 0;
|
|
109
|
+
threshold;
|
|
110
|
+
backoffMs;
|
|
111
|
+
now;
|
|
112
|
+
constructor(opts = {}) {
|
|
113
|
+
this.threshold = opts.failureThreshold ?? GIT_FETCH_FAILURE_THRESHOLD;
|
|
114
|
+
this.backoffMs = opts.backoffMs ?? GIT_FETCH_BACKOFF_MS;
|
|
115
|
+
this.now = opts.now ?? Date.now;
|
|
116
|
+
}
|
|
117
|
+
getState() {
|
|
118
|
+
return this.state;
|
|
119
|
+
}
|
|
120
|
+
getConsecutiveFailures() {
|
|
121
|
+
return this.consecutiveFailures;
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Whether a fetch should run this cycle. When open, returns false until the
|
|
125
|
+
* backoff window elapses, at which point it transitions to half-open and
|
|
126
|
+
* allows a single probe. Closed and half-open always attempt.
|
|
127
|
+
*/
|
|
128
|
+
shouldAttempt() {
|
|
129
|
+
if (this.state === 'open') {
|
|
130
|
+
if (this.now() - this.openedAt >= this.backoffMs) {
|
|
131
|
+
this.state = 'half_open';
|
|
132
|
+
return true;
|
|
133
|
+
}
|
|
134
|
+
return false;
|
|
135
|
+
}
|
|
136
|
+
return true; // closed or half_open
|
|
137
|
+
}
|
|
138
|
+
/** Record a successful fetch -> closes the circuit; signals recovery once. */
|
|
139
|
+
recordSuccess() {
|
|
140
|
+
const recovered = this.state !== 'closed' || this.consecutiveFailures > 0;
|
|
141
|
+
this.state = 'closed';
|
|
142
|
+
this.consecutiveFailures = 0;
|
|
143
|
+
this.openedAt = 0;
|
|
144
|
+
return { state: this.state, recovered };
|
|
145
|
+
}
|
|
146
|
+
/** Record a failed fetch -> may open the circuit (once) and start backoff. */
|
|
147
|
+
recordFailure() {
|
|
148
|
+
this.consecutiveFailures += 1;
|
|
149
|
+
// A failed half-open probe drops straight back to open (restart backoff)
|
|
150
|
+
// without re-emitting the degraded signal.
|
|
151
|
+
if (this.state === 'half_open') {
|
|
152
|
+
this.state = 'open';
|
|
153
|
+
this.openedAt = this.now();
|
|
154
|
+
return { state: this.state, justOpened: false };
|
|
155
|
+
}
|
|
156
|
+
if (this.state === 'closed' && this.consecutiveFailures >= this.threshold) {
|
|
157
|
+
this.state = 'open';
|
|
158
|
+
this.openedAt = this.now();
|
|
159
|
+
return { state: this.state, justOpened: true };
|
|
160
|
+
}
|
|
161
|
+
return { state: this.state, justOpened: false };
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
//# sourceMappingURL=git-fetch.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"git-fetch.js","sourceRoot":"","sources":["../src/git-fetch.ts"],"names":[],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"git-state-detector.d.ts","sourceRoot":"","sources":["../src/git-state-detector.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAY,MAAM,YAAY,CAAC;
|
|
1
|
+
{"version":3,"file":"git-state-detector.d.ts","sourceRoot":"","sources":["../src/git-state-detector.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAY,MAAM,YAAY,CAAC;AAmFzD;;;;;;;;;;GAUG;AACH,wBAAsB,eAAe,CAAC,MAAM,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,CA0HzE"}
|
|
@@ -13,17 +13,41 @@ import { getDefaultBranch, repoHasCommits } from './git-branch.js';
|
|
|
13
13
|
import { getActiveFocuses } from './queries/deliveries.js';
|
|
14
14
|
import { getFocusDeliveries } from './queries/focuses.js';
|
|
15
15
|
import { reportGitState } from './queries/deliveries.js';
|
|
16
|
-
import { healthEvents } from '@telora/daemon-core';
|
|
17
|
-
|
|
16
|
+
import { healthEvents, loadEnvConfig } from '@telora/daemon-core';
|
|
17
|
+
import { gitFetchOrigin, GitFetchCircuit, GIT_FETCH_BACKOFF_MS, } from './git-fetch.js';
|
|
18
|
+
/** Short timeout for local branch-containment checks to avoid blocking the poll loop. */
|
|
18
19
|
const CONTAINMENT_CHECK_TIMEOUT_MS = 5_000;
|
|
20
|
+
/**
|
|
21
|
+
* Timeout for the network fetch from origin. The fetch now runs OFF the event
|
|
22
|
+
* loop (gitFetchOrigin), but the timeout is still bounded a few seconds -- well
|
|
23
|
+
* below the watchdog's event-loop-lag threshold -- so a hung remote costs
|
|
24
|
+
* seconds per attempt, not a 30s window. Configurable via
|
|
25
|
+
* TELORA_GIT_FETCH_TIMEOUT_MS (default 5s) for high-latency remotes.
|
|
26
|
+
*/
|
|
27
|
+
function getFetchTimeoutMs() {
|
|
28
|
+
return loadEnvConfig(process.env).TELORA_GIT_FETCH_TIMEOUT_MS;
|
|
29
|
+
}
|
|
19
30
|
/** Track whether origin remote exists to avoid repeated fetch failures. */
|
|
20
31
|
let originChecked = false;
|
|
21
32
|
let originExists = false;
|
|
22
33
|
let lastOriginCheckAt = 0;
|
|
23
34
|
/** Re-check origin existence every 10 minutes (remote could be added/removed). */
|
|
24
35
|
const ORIGIN_CHECK_TTL_MS = 10 * 60 * 1000;
|
|
25
|
-
/**
|
|
26
|
-
|
|
36
|
+
/**
|
|
37
|
+
* Per-repo fetch circuit breakers. A persistently-unreachable remote opens its
|
|
38
|
+
* circuit after N consecutive failures and backs off instead of retry-storming
|
|
39
|
+
* every poll cycle; keyed by repoPath so multi-product daemons don't conflate
|
|
40
|
+
* remotes.
|
|
41
|
+
*/
|
|
42
|
+
const fetchCircuits = new Map();
|
|
43
|
+
function circuitFor(repoPath) {
|
|
44
|
+
let circuit = fetchCircuits.get(repoPath);
|
|
45
|
+
if (!circuit) {
|
|
46
|
+
circuit = new GitFetchCircuit();
|
|
47
|
+
fetchCircuits.set(repoPath, circuit);
|
|
48
|
+
}
|
|
49
|
+
return circuit;
|
|
50
|
+
}
|
|
27
51
|
/**
|
|
28
52
|
* Priority ordering for git states. Higher number = more advanced.
|
|
29
53
|
* Only update a delivery's state forward, never backward.
|
|
@@ -83,30 +107,43 @@ export async function detectGitStates(config) {
|
|
|
83
107
|
console.log('[git-state-detector] Origin remote removed -- skipping fetch');
|
|
84
108
|
}
|
|
85
109
|
}
|
|
86
|
-
// Fetch refs once before iterating (ensures consistent state)
|
|
110
|
+
// Fetch refs once before iterating (ensures consistent state). The fetch runs
|
|
111
|
+
// OFF the event loop (async child process) and is gated by a per-repo circuit
|
|
112
|
+
// breaker so a persistently-unreachable remote backs off instead of blocking
|
|
113
|
+
// and retry-storming every cycle.
|
|
87
114
|
if (originExists) {
|
|
88
|
-
const
|
|
89
|
-
if (
|
|
90
|
-
|
|
91
|
-
if (!
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
115
|
+
const circuit = circuitFor(repoPath);
|
|
116
|
+
if (circuit.shouldAttempt()) {
|
|
117
|
+
const fetchResult = await gitFetchOrigin(repoPath, { timeoutMs: getFetchTimeoutMs() });
|
|
118
|
+
if (!fetchResult.success) {
|
|
119
|
+
console.debug('[git-state-detector] fetch failed (non-fatal):', fetchResult.error);
|
|
120
|
+
const { justOpened } = circuit.recordFailure();
|
|
121
|
+
if (justOpened) {
|
|
122
|
+
// Single degraded-mode signal on the closed -> open transition --
|
|
123
|
+
// not a per-cycle storm. The circuit then suppresses fetches for the
|
|
124
|
+
// backoff window.
|
|
125
|
+
const backoffMin = Math.round(GIT_FETCH_BACKOFF_MS / 60_000);
|
|
126
|
+
healthEvents.emit({
|
|
127
|
+
severity: 'warn',
|
|
128
|
+
source: 'git-state-detector',
|
|
129
|
+
code: 'git.fetch_failed',
|
|
130
|
+
message: `Git fetch to origin failed ${circuit.getConsecutiveFailures()} times in a row; ` +
|
|
131
|
+
`backing off ~${backoffMin}m before retrying. Last error: ${fetchResult.error}`,
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
else {
|
|
136
|
+
const { recovered } = circuit.recordSuccess();
|
|
137
|
+
if (recovered) {
|
|
138
|
+
healthEvents.emit({
|
|
139
|
+
severity: 'info',
|
|
140
|
+
source: 'git-state-detector',
|
|
141
|
+
code: 'git.fetch_recovered',
|
|
142
|
+
message: 'Git fetch to origin recovered',
|
|
143
|
+
});
|
|
144
|
+
healthEvents.resolveByCode('git.fetch_failed');
|
|
145
|
+
}
|
|
99
146
|
}
|
|
100
|
-
}
|
|
101
|
-
else if (lastFetchFailed) {
|
|
102
|
-
healthEvents.emit({
|
|
103
|
-
severity: 'info',
|
|
104
|
-
source: 'git-state-detector',
|
|
105
|
-
code: 'git.fetch_recovered',
|
|
106
|
-
message: 'Git fetch recovered',
|
|
107
|
-
});
|
|
108
|
-
healthEvents.resolveByCode('git.fetch_failed');
|
|
109
|
-
lastFetchFailed = false;
|
|
110
147
|
}
|
|
111
148
|
}
|
|
112
149
|
const defaultBranch = getDefaultBranch(repoPath);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"git-state-detector.js","sourceRoot":"","sources":["../src/git-state-detector.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAGH,OAAO,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;AAC5C,OAAO,EAAE,gBAAgB,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AACnE,OAAO,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;AAC3D,OAAO,EAAE,kBAAkB,EAAE,MAAM,sBAAsB,CAAC;AAC1D,OAAO,EAAE,cAAc,EAAE,MAAM,yBAAyB,CAAC;AACzD,OAAO,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;
|
|
1
|
+
{"version":3,"file":"git-state-detector.js","sourceRoot":"","sources":["../src/git-state-detector.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-endpoint host-control capability detection (Local model host control, D1).
|
|
3
|
+
*
|
|
4
|
+
* The control plane is an OPTIONAL capability detected per-endpoint, never
|
|
5
|
+
* assumed. `detectHostCapability` runs a CHEAP, FAIL-SAFE probe against the host
|
|
6
|
+
* root's control surface: a 2xx with a well-formed (non-empty, usable) catalog
|
|
7
|
+
* means controllable; ANY other outcome -- error, non-2xx, parse failure,
|
|
8
|
+
* timeout, or an empty/unusable payload -- means NOT controllable, so the caller
|
|
9
|
+
* falls back to the `/v1/models` floor. This guarantees a plain
|
|
10
|
+
* OpenAI-compatible endpoint is never broken by the probe.
|
|
11
|
+
*/
|
|
12
|
+
import type { HostCapability } from './types.js';
|
|
13
|
+
/**
|
|
14
|
+
* Pure decision: does a control-surface payload prove the host is controllable?
|
|
15
|
+
* True only when the payload normalizes to at least one usable loadable-model
|
|
16
|
+
* entry. A malformed, empty, or non-catalog payload yields false. Exported for
|
|
17
|
+
* unit testing the interpret-a-payload -> controllable? boundary in isolation.
|
|
18
|
+
*/
|
|
19
|
+
export declare function isControllableCatalogPayload(payload: unknown): boolean;
|
|
20
|
+
/**
|
|
21
|
+
* Probe an endpoint's host root for the optional control capability. Cheap (a
|
|
22
|
+
* single GET, time-boxed by the client) and FAIL-SAFE: every failure mode --
|
|
23
|
+
* SSRF rejection, network error, non-2xx, invalid JSON, timeout, or an
|
|
24
|
+
* empty/unusable catalog -- resolves to `{ controllable: false }` rather than
|
|
25
|
+
* throwing, so the caller can unconditionally proceed to the floor.
|
|
26
|
+
*
|
|
27
|
+
* `apiKey` is the bearer VALUE (resolved from the Telora secret) authorizing the
|
|
28
|
+
* probe against an auth-protected host.
|
|
29
|
+
*/
|
|
30
|
+
export declare function detectHostCapability(baseUrl: string, apiKey?: string | null): Promise<HostCapability>;
|
|
31
|
+
//# sourceMappingURL=capability.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"capability.d.ts","sourceRoot":"","sources":["../../src/host-control/capability.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAIH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAEjD;;;;;GAKG;AACH,wBAAgB,4BAA4B,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAEtE;AAED;;;;;;;;;GASG;AACH,wBAAsB,oBAAoB,CACxC,OAAO,EAAE,MAAM,EACf,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,GACrB,OAAO,CAAC,cAAc,CAAC,CAezB"}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-endpoint host-control capability detection (Local model host control, D1).
|
|
3
|
+
*
|
|
4
|
+
* The control plane is an OPTIONAL capability detected per-endpoint, never
|
|
5
|
+
* assumed. `detectHostCapability` runs a CHEAP, FAIL-SAFE probe against the host
|
|
6
|
+
* root's control surface: a 2xx with a well-formed (non-empty, usable) catalog
|
|
7
|
+
* means controllable; ANY other outcome -- error, non-2xx, parse failure,
|
|
8
|
+
* timeout, or an empty/unusable payload -- means NOT controllable, so the caller
|
|
9
|
+
* falls back to the `/v1/models` floor. This guarantees a plain
|
|
10
|
+
* OpenAI-compatible endpoint is never broken by the probe.
|
|
11
|
+
*/
|
|
12
|
+
import { hostGet, hostRootFromBaseUrl, HOST_CONTROL_FETCH_TIMEOUT_MS } from './client.js';
|
|
13
|
+
import { HOST_CONTROL_CATALOG_PATH, normalizeLocalCatalog } from './catalog.js';
|
|
14
|
+
/**
|
|
15
|
+
* Pure decision: does a control-surface payload prove the host is controllable?
|
|
16
|
+
* True only when the payload normalizes to at least one usable loadable-model
|
|
17
|
+
* entry. A malformed, empty, or non-catalog payload yields false. Exported for
|
|
18
|
+
* unit testing the interpret-a-payload -> controllable? boundary in isolation.
|
|
19
|
+
*/
|
|
20
|
+
export function isControllableCatalogPayload(payload) {
|
|
21
|
+
return normalizeLocalCatalog(payload).length > 0;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Probe an endpoint's host root for the optional control capability. Cheap (a
|
|
25
|
+
* single GET, time-boxed by the client) and FAIL-SAFE: every failure mode --
|
|
26
|
+
* SSRF rejection, network error, non-2xx, invalid JSON, timeout, or an
|
|
27
|
+
* empty/unusable catalog -- resolves to `{ controllable: false }` rather than
|
|
28
|
+
* throwing, so the caller can unconditionally proceed to the floor.
|
|
29
|
+
*
|
|
30
|
+
* `apiKey` is the bearer VALUE (resolved from the Telora secret) authorizing the
|
|
31
|
+
* probe against an auth-protected host.
|
|
32
|
+
*/
|
|
33
|
+
export async function detectHostCapability(baseUrl, apiKey) {
|
|
34
|
+
const root = hostRootFromBaseUrl(baseUrl);
|
|
35
|
+
const catalogUrl = `${root}${HOST_CONTROL_CATALOG_PATH}`;
|
|
36
|
+
try {
|
|
37
|
+
const payload = await hostGet(catalogUrl, {
|
|
38
|
+
apiKey,
|
|
39
|
+
timeoutMs: HOST_CONTROL_FETCH_TIMEOUT_MS,
|
|
40
|
+
});
|
|
41
|
+
return { controllable: isControllableCatalogPayload(payload) };
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
// Fail-safe: any error => not controllable. Detail is intentionally swallowed
|
|
45
|
+
// here (the catalog reader logs on its own fall-through); the probe must never
|
|
46
|
+
// surface a probe-oracle or break the floor path.
|
|
47
|
+
return { controllable: false };
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
//# sourceMappingURL=capability.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"capability.js","sourceRoot":"","sources":["../../src/host-control/capability.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,EAAE,OAAO,EAAE,mBAAmB,EAAE,6BAA6B,EAAE,MAAM,aAAa,CAAC;AAC1F,OAAO,EAAE,yBAAyB,EAAE,qBAAqB,EAAE,MAAM,cAAc,CAAC;AAGhF;;;;;GAKG;AACH,MAAM,UAAU,4BAA4B,CAAC,OAAgB;IAC3D,OAAO,qBAAqB,CAAC,OAAO,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC;AACnD,CAAC;AAED;;;;;;;;;GASG;AACH,MAAM,CAAC,KAAK,UAAU,oBAAoB,CACxC,OAAe,EACf,MAAsB;IAEtB,MAAM,IAAI,GAAG,mBAAmB,CAAC,OAAO,CAAC,CAAC;IAC1C,MAAM,UAAU,GAAG,GAAG,IAAI,GAAG,yBAAyB,EAAE,CAAC;IACzD,IAAI,CAAC;QACH,MAAM,OAAO,GAAG,MAAM,OAAO,CAAU,UAAU,EAAE;YACjD,MAAM;YACN,SAAS,EAAE,6BAA6B;SACzC,CAAC,CAAC;QACH,OAAO,EAAE,YAAY,EAAE,4BAA4B,CAAC,OAAO,CAAC,EAAE,CAAC;IACjE,CAAC;IAAC,MAAM,CAAC;QACP,8EAA8E;QAC9E,+EAA+E;QAC/E,kDAAkD;QAClD,OAAO,EAAE,YAAY,EAAE,KAAK,EAAE,CAAC;IACjC,CAAC;AACH,CAAC"}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Loadable-model catalog reader for the host-control plane (Local model host
|
|
3
|
+
* control, D1).
|
|
4
|
+
*
|
|
5
|
+
* A controllable host (e.g. Unsloth Studio @ :8888) exposes its FULL
|
|
6
|
+
* downloaded-but-unloaded catalog under the control surface
|
|
7
|
+
* `<root>/api/models/local`, returning every loadable model -- not just the one
|
|
8
|
+
* currently loaded that `/v1/models` reports. This module reads that catalog and
|
|
9
|
+
* degrades CLEANLY to the `/v1/models` floor (via discovery-poll's
|
|
10
|
+
* `fetchModels`) when the host is not controllable or the catalog read fails for
|
|
11
|
+
* any reason. The floor path is byte-identical to today's behavior, so a plain
|
|
12
|
+
* OpenAI-compatible endpoint is never broken.
|
|
13
|
+
*
|
|
14
|
+
* The live catalog JSON shape is host-specific (observed: 25 entries under
|
|
15
|
+
* `/api/models/local`, each carrying a load path). We do NOT have the exact live
|
|
16
|
+
* shape pinned, so `normalizeLocalCatalog` is DEFENSIVE: it tolerates a range of
|
|
17
|
+
* reasonable container and field names and is easy to extend per the runbook's
|
|
18
|
+
* live-shape reconciliation.
|
|
19
|
+
*/
|
|
20
|
+
import type { LoadableCatalog, LoadableModel } from './types.js';
|
|
21
|
+
/** The control-surface path (under the host ROOT, not `/v1`) for the full catalog. */
|
|
22
|
+
export declare const HOST_CONTROL_CATALOG_PATH = "/api/models/local";
|
|
23
|
+
/**
|
|
24
|
+
* Normalize a control-surface catalog payload into clean `LoadableModel`
|
|
25
|
+
* entries. Pure + DEFENSIVE: tolerates a bare array or a wrapped container,
|
|
26
|
+
* non-object items, missing/blank ids (dropped), and a missing model_path
|
|
27
|
+
* (captured as null). De-duplicates by id (first entry with a model_path wins
|
|
28
|
+
* its path). Exported for unit testing.
|
|
29
|
+
*/
|
|
30
|
+
export declare function normalizeLocalCatalog(payload: unknown): LoadableModel[];
|
|
31
|
+
/**
|
|
32
|
+
* Read an endpoint's loadable-model catalog, preferring the controllable host
|
|
33
|
+
* catalog and degrading cleanly to the `/v1/models` floor.
|
|
34
|
+
*
|
|
35
|
+
* Control path: GET `<root>/api/models/local`, normalize, and if it yields at
|
|
36
|
+
* least one usable entry return the full loadable list (`controllable: true`).
|
|
37
|
+
*
|
|
38
|
+
* Floor path: on ANY control-surface failure (unreachable, non-2xx, invalid
|
|
39
|
+
* JSON, timeout, or an empty/unusable catalog) fall back to `fetchModels`
|
|
40
|
+
* (`/v1/models`) so behavior is exactly today's load-then-discover. The floor
|
|
41
|
+
* fetch's own failure propagates to the caller (same contract as the existing
|
|
42
|
+
* discovery path).
|
|
43
|
+
*
|
|
44
|
+
* `apiKey` is the bearer VALUE (resolved from the Telora secret); it authorizes
|
|
45
|
+
* both the control probe and the floor fetch.
|
|
46
|
+
*/
|
|
47
|
+
export declare function readLoadableCatalog(baseUrl: string, apiKey?: string | null): Promise<LoadableCatalog>;
|
|
48
|
+
//# sourceMappingURL=catalog.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"catalog.d.ts","sourceRoot":"","sources":["../../src/host-control/catalog.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AASH,OAAO,KAAK,EAAE,eAAe,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAEjE,sFAAsF;AACtF,eAAO,MAAM,yBAAyB,sBAAsB,CAAC;AAgD7D;;;;;;GAMG;AACH,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,OAAO,GAAG,aAAa,EAAE,CAkBvE;AAYD;;;;;;;;;;;;;;;GAeG;AACH,wBAAsB,mBAAmB,CACvC,OAAO,EAAE,MAAM,EACf,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,GACrB,OAAO,CAAC,eAAe,CAAC,CAwB1B"}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Loadable-model catalog reader for the host-control plane (Local model host
|
|
3
|
+
* control, D1).
|
|
4
|
+
*
|
|
5
|
+
* A controllable host (e.g. Unsloth Studio @ :8888) exposes its FULL
|
|
6
|
+
* downloaded-but-unloaded catalog under the control surface
|
|
7
|
+
* `<root>/api/models/local`, returning every loadable model -- not just the one
|
|
8
|
+
* currently loaded that `/v1/models` reports. This module reads that catalog and
|
|
9
|
+
* degrades CLEANLY to the `/v1/models` floor (via discovery-poll's
|
|
10
|
+
* `fetchModels`) when the host is not controllable or the catalog read fails for
|
|
11
|
+
* any reason. The floor path is byte-identical to today's behavior, so a plain
|
|
12
|
+
* OpenAI-compatible endpoint is never broken.
|
|
13
|
+
*
|
|
14
|
+
* The live catalog JSON shape is host-specific (observed: 25 entries under
|
|
15
|
+
* `/api/models/local`, each carrying a load path). We do NOT have the exact live
|
|
16
|
+
* shape pinned, so `normalizeLocalCatalog` is DEFENSIVE: it tolerates a range of
|
|
17
|
+
* reasonable container and field names and is easy to extend per the runbook's
|
|
18
|
+
* live-shape reconciliation.
|
|
19
|
+
*/
|
|
20
|
+
import { fetchModels } from '../discovery-poll.js';
|
|
21
|
+
import { hostGet, hostRootFromBaseUrl, HOST_CONTROL_FETCH_TIMEOUT_MS, HostControlError, } from './client.js';
|
|
22
|
+
/** The control-surface path (under the host ROOT, not `/v1`) for the full catalog. */
|
|
23
|
+
export const HOST_CONTROL_CATALOG_PATH = '/api/models/local';
|
|
24
|
+
/**
|
|
25
|
+
* Candidate keys that may carry the array of catalog entries. The live host
|
|
26
|
+
* (Unsloth Studio) returned a bare array under `/api/models/local`; other hosts
|
|
27
|
+
* may wrap it. Defensive: we accept a top-level array OR any of these container
|
|
28
|
+
* keys. Easy to extend per the runbook.
|
|
29
|
+
*/
|
|
30
|
+
const CATALOG_ARRAY_KEYS = ['models', 'data', 'local', 'items'];
|
|
31
|
+
/**
|
|
32
|
+
* Candidate keys for a catalog entry's served model id. `id` is canonical;
|
|
33
|
+
* `model`/`name` are common alternates on local-model hosts.
|
|
34
|
+
*/
|
|
35
|
+
const ID_KEYS = ['id', 'model', 'name'];
|
|
36
|
+
/**
|
|
37
|
+
* Candidate keys for a catalog entry's host load path. `model_path` is the
|
|
38
|
+
* observed convention; the rest are reasonable alternates. Captured verbatim so
|
|
39
|
+
* Delivery 2 can resolve id -> path to load the model.
|
|
40
|
+
*/
|
|
41
|
+
const MODEL_PATH_KEYS = ['model_path', 'modelPath', 'path', 'file', 'gguf_path'];
|
|
42
|
+
/** Read the first string-valued key present on an object, trimmed; '' if none. */
|
|
43
|
+
function firstString(obj, keys) {
|
|
44
|
+
for (const key of keys) {
|
|
45
|
+
const v = obj[key];
|
|
46
|
+
if (typeof v === 'string') {
|
|
47
|
+
const trimmed = v.trim();
|
|
48
|
+
if (trimmed)
|
|
49
|
+
return trimmed;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return '';
|
|
53
|
+
}
|
|
54
|
+
/** Extract the catalog-entry array from a payload that is either a bare array or
|
|
55
|
+
* a single-keyed container. Returns [] when no array is found (defensive). */
|
|
56
|
+
function extractEntryArray(payload) {
|
|
57
|
+
if (Array.isArray(payload))
|
|
58
|
+
return payload;
|
|
59
|
+
if (!payload || typeof payload !== 'object')
|
|
60
|
+
return [];
|
|
61
|
+
const obj = payload;
|
|
62
|
+
for (const key of CATALOG_ARRAY_KEYS) {
|
|
63
|
+
const v = obj[key];
|
|
64
|
+
if (Array.isArray(v))
|
|
65
|
+
return v;
|
|
66
|
+
}
|
|
67
|
+
return [];
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Normalize a control-surface catalog payload into clean `LoadableModel`
|
|
71
|
+
* entries. Pure + DEFENSIVE: tolerates a bare array or a wrapped container,
|
|
72
|
+
* non-object items, missing/blank ids (dropped), and a missing model_path
|
|
73
|
+
* (captured as null). De-duplicates by id (first entry with a model_path wins
|
|
74
|
+
* its path). Exported for unit testing.
|
|
75
|
+
*/
|
|
76
|
+
export function normalizeLocalCatalog(payload) {
|
|
77
|
+
const entries = extractEntryArray(payload);
|
|
78
|
+
const byId = new Map();
|
|
79
|
+
for (const item of entries) {
|
|
80
|
+
if (!item || typeof item !== 'object')
|
|
81
|
+
continue;
|
|
82
|
+
const obj = item;
|
|
83
|
+
const id = firstString(obj, ID_KEYS);
|
|
84
|
+
if (!id)
|
|
85
|
+
continue; // an entry with no usable id is unselectable -- drop it.
|
|
86
|
+
const modelPath = firstString(obj, MODEL_PATH_KEYS) || null;
|
|
87
|
+
const existing = byId.get(id);
|
|
88
|
+
if (!existing) {
|
|
89
|
+
byId.set(id, { id, modelPath });
|
|
90
|
+
}
|
|
91
|
+
else if (existing.modelPath === null && modelPath !== null) {
|
|
92
|
+
// Prefer the duplicate that carries a load path over a path-less twin.
|
|
93
|
+
byId.set(id, { id, modelPath });
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return [...byId.values()];
|
|
97
|
+
}
|
|
98
|
+
/** Build the floor-path catalog from a `/v1/models` id list. `modelPath` is null
|
|
99
|
+
* on the floor -- the floor knows only the loaded id, never a load path. */
|
|
100
|
+
function floorCatalog(ids) {
|
|
101
|
+
return {
|
|
102
|
+
controllable: false,
|
|
103
|
+
ids,
|
|
104
|
+
models: ids.map((id) => ({ id, modelPath: null })),
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Read an endpoint's loadable-model catalog, preferring the controllable host
|
|
109
|
+
* catalog and degrading cleanly to the `/v1/models` floor.
|
|
110
|
+
*
|
|
111
|
+
* Control path: GET `<root>/api/models/local`, normalize, and if it yields at
|
|
112
|
+
* least one usable entry return the full loadable list (`controllable: true`).
|
|
113
|
+
*
|
|
114
|
+
* Floor path: on ANY control-surface failure (unreachable, non-2xx, invalid
|
|
115
|
+
* JSON, timeout, or an empty/unusable catalog) fall back to `fetchModels`
|
|
116
|
+
* (`/v1/models`) so behavior is exactly today's load-then-discover. The floor
|
|
117
|
+
* fetch's own failure propagates to the caller (same contract as the existing
|
|
118
|
+
* discovery path).
|
|
119
|
+
*
|
|
120
|
+
* `apiKey` is the bearer VALUE (resolved from the Telora secret); it authorizes
|
|
121
|
+
* both the control probe and the floor fetch.
|
|
122
|
+
*/
|
|
123
|
+
export async function readLoadableCatalog(baseUrl, apiKey) {
|
|
124
|
+
const root = hostRootFromBaseUrl(baseUrl);
|
|
125
|
+
const catalogUrl = `${root}${HOST_CONTROL_CATALOG_PATH}`;
|
|
126
|
+
try {
|
|
127
|
+
const payload = await hostGet(catalogUrl, {
|
|
128
|
+
apiKey,
|
|
129
|
+
timeoutMs: HOST_CONTROL_FETCH_TIMEOUT_MS,
|
|
130
|
+
});
|
|
131
|
+
const models = normalizeLocalCatalog(payload);
|
|
132
|
+
if (models.length > 0) {
|
|
133
|
+
return { controllable: true, ids: models.map((m) => m.id), models };
|
|
134
|
+
}
|
|
135
|
+
// Well-formed but empty (or no usable ids): not a usable control catalog --
|
|
136
|
+
// fall through to the floor so the picklist still offers the loaded model.
|
|
137
|
+
}
|
|
138
|
+
catch (err) {
|
|
139
|
+
// Fail-safe: any control-surface error means "not controllable here". Log the
|
|
140
|
+
// rich detail daemon-side (address-free dbMessage available for categorizing)
|
|
141
|
+
// and fall through to the floor. Never break a plain endpoint.
|
|
142
|
+
const detail = err instanceof HostControlError ? err.message : err.message;
|
|
143
|
+
console.log(`[host-control] catalog read fell back to /v1/models floor: ${detail}`);
|
|
144
|
+
}
|
|
145
|
+
// Floor: the OpenAI-compatible `/v1/models` listing (today's behavior).
|
|
146
|
+
const ids = await fetchModels(baseUrl, apiKey);
|
|
147
|
+
return floorCatalog(ids);
|
|
148
|
+
}
|
|
149
|
+
//# sourceMappingURL=catalog.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"catalog.js","sourceRoot":"","sources":["../../src/host-control/catalog.ts"],"names":[],"mappings":"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"}
|