@sentropic/h2a-runtime 0.91.1 → 0.93.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/config.d.ts +2 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +16 -3
- package/dist/config.js.map +1 -1
- package/dist/conv-guard.d.ts +6 -1
- package/dist/conv-guard.d.ts.map +1 -1
- package/dist/conv-guard.js +33 -8
- package/dist/conv-guard.js.map +1 -1
- package/dist/enroll.d.ts.map +1 -1
- package/dist/enroll.js +12 -2
- package/dist/enroll.js.map +1 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +819 -155
- package/dist/index.js.map +1 -1
- package/dist/launch-layout.d.ts +74 -0
- package/dist/launch-layout.d.ts.map +1 -0
- package/dist/launch-layout.js +324 -0
- package/dist/launch-layout.js.map +1 -0
- package/dist/llm-gateway-runtime/index.d.ts +6 -0
- package/dist/llm-gateway-runtime/index.d.ts.map +1 -1
- package/dist/llm-gateway-runtime/index.js +134 -60
- package/dist/llm-gateway-runtime/index.js.map +1 -1
- package/dist/llm-gateway-runtime/session-ledger.d.ts +38 -55
- package/dist/llm-gateway-runtime/session-ledger.d.ts.map +1 -1
- package/dist/llm-gateway-runtime/session-ledger.js +102 -250
- package/dist/llm-gateway-runtime/session-ledger.js.map +1 -1
- package/dist/llm-gateway-runtime/sticky.d.ts +25 -52
- package/dist/llm-gateway-runtime/sticky.d.ts.map +1 -1
- package/dist/llm-gateway-runtime/sticky.js +36 -605
- package/dist/llm-gateway-runtime/sticky.js.map +1 -1
- package/dist/llm-mesh.d.ts +19 -72
- package/dist/llm-mesh.d.ts.map +1 -1
- package/dist/llm-mesh.js +42 -399
- package/dist/llm-mesh.js.map +1 -1
- package/dist/llm-routing-config.d.ts +28 -0
- package/dist/llm-routing-config.d.ts.map +1 -0
- package/dist/llm-routing-config.js +100 -0
- package/dist/llm-routing-config.js.map +1 -0
- package/dist/native-host.d.ts +146 -0
- package/dist/native-host.d.ts.map +1 -0
- package/dist/native-host.js +456 -0
- package/dist/native-host.js.map +1 -0
- package/dist/native-terminal/host.d.ts +317 -1
- package/dist/native-terminal/host.d.ts.map +1 -1
- package/dist/native-terminal/host.js +696 -3
- package/dist/native-terminal/host.js.map +1 -1
- package/dist/native-terminal/op.d.ts +5 -0
- package/dist/native-terminal/op.d.ts.map +1 -0
- package/dist/native-terminal/op.js +408 -0
- package/dist/native-terminal/op.js.map +1 -0
- package/dist/native-terminal/process.d.ts +2 -0
- package/dist/native-terminal/process.d.ts.map +1 -1
- package/dist/native-terminal/process.js +16 -2
- package/dist/native-terminal/process.js.map +1 -1
- package/dist/native-terminal/server.d.ts.map +1 -1
- package/dist/native-terminal/server.js +6 -2
- package/dist/native-terminal/server.js.map +1 -1
- package/dist/native-terminal/socket-path.d.ts +8 -0
- package/dist/native-terminal/socket-path.d.ts.map +1 -1
- package/dist/native-terminal/socket-path.js +31 -1
- package/dist/native-terminal/socket-path.js.map +1 -1
- package/dist/native-terminal/supervisor.d.ts +13 -0
- package/dist/native-terminal/supervisor.d.ts.map +1 -1
- package/dist/native-terminal/supervisor.js +45 -0
- package/dist/native-terminal/supervisor.js.map +1 -1
- package/dist/pty.d.ts +23 -0
- package/dist/pty.d.ts.map +1 -1
- package/dist/pty.js +35 -0
- package/dist/pty.js.map +1 -1
- package/dist/readiness.d.ts +7 -0
- package/dist/readiness.d.ts.map +1 -1
- package/dist/readiness.js +12 -9
- package/dist/readiness.js.map +1 -1
- package/dist/registry.d.ts +324 -2
- package/dist/registry.d.ts.map +1 -1
- package/dist/registry.js +659 -20
- package/dist/registry.js.map +1 -1
- package/dist/restore.d.ts +91 -32
- package/dist/restore.d.ts.map +1 -1
- package/dist/restore.js +226 -186
- package/dist/restore.js.map +1 -1
- package/dist/status-projection.d.ts.map +1 -1
- package/dist/tmux.d.ts +9 -6
- package/dist/tmux.d.ts.map +1 -1
- package/dist/tmux.js +34 -8
- package/dist/tmux.js.map +1 -1
- package/package.json +3 -3
- package/dist/llm-gateway-runtime/accounts.d.ts +0 -62
- package/dist/llm-gateway-runtime/accounts.d.ts.map +0 -1
- package/dist/llm-gateway-runtime/accounts.js +0 -323
- package/dist/llm-gateway-runtime/accounts.js.map +0 -1
- package/dist/llm-gateway-runtime/model-catalog.d.ts +0 -44
- package/dist/llm-gateway-runtime/model-catalog.d.ts.map +0 -1
- package/dist/llm-gateway-runtime/model-catalog.js +0 -191
- package/dist/llm-gateway-runtime/model-catalog.js.map +0 -1
- package/dist/llm-gateway-runtime/proxy-anthropic.d.ts +0 -3
- package/dist/llm-gateway-runtime/proxy-anthropic.d.ts.map +0 -1
- package/dist/llm-gateway-runtime/proxy-anthropic.js +0 -318
- package/dist/llm-gateway-runtime/proxy-anthropic.js.map +0 -1
- package/dist/llm-gateway-runtime/proxy-cloud-code.d.ts +0 -11
- package/dist/llm-gateway-runtime/proxy-cloud-code.d.ts.map +0 -1
- package/dist/llm-gateway-runtime/proxy-cloud-code.js +0 -215
- package/dist/llm-gateway-runtime/proxy-cloud-code.js.map +0 -1
- package/dist/llm-gateway-runtime/proxy-openai.d.ts +0 -118
- package/dist/llm-gateway-runtime/proxy-openai.d.ts.map +0 -1
- package/dist/llm-gateway-runtime/proxy-openai.js +0 -1305
- package/dist/llm-gateway-runtime/proxy-openai.js.map +0 -1
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Terminal-layout rendering for `remote restore` — split out of restore.ts so
|
|
3
|
+
* the F3 invariant is a MODULE-BOUNDARY property, not a review rule:
|
|
4
|
+
*
|
|
5
|
+
* launchLayout decides every view/launch from the ONE `HostViewSnapshot`
|
|
6
|
+
* it receives AS A PARAMETER. This module must NOT import any live host
|
|
7
|
+
* reader (listNativeSessions, listLocalSessions*, nativeSession*, probe*):
|
|
8
|
+
* you can't call what you don't have, so a divergence between plan time
|
|
9
|
+
* and launch time can never be re-read here — and an error or divergence
|
|
10
|
+
* NEVER becomes permission to launch (`live -> empty` yields attach or
|
|
11
|
+
* refuse, never `h2a run` fabricating a second writer on a live
|
|
12
|
+
* conversation). Only PURE name helpers may be imported from tmux.js.
|
|
13
|
+
*/
|
|
14
|
+
import { type LocalSession } from "./tmux.js";
|
|
15
|
+
import type { LayoutTab, LayoutWindow } from "./restore.js";
|
|
16
|
+
/**
|
|
17
|
+
* The ONE host inventory snapshot a restore plan runs on: BOTH inventories
|
|
18
|
+
* read once by the caller (restore.ts owns the readers), so the plan gate and
|
|
19
|
+
* the layout render see the SAME state. A failed inventory read is carried as
|
|
20
|
+
* `known: false` — an UNKNOWN view, never an empty one.
|
|
21
|
+
*/
|
|
22
|
+
export type HostViewSnapshot = {
|
|
23
|
+
readonly tmux: {
|
|
24
|
+
readonly known: true;
|
|
25
|
+
readonly sessions: ReadonlyArray<LocalSession>;
|
|
26
|
+
} | {
|
|
27
|
+
readonly known: false;
|
|
28
|
+
readonly reason?: string;
|
|
29
|
+
};
|
|
30
|
+
readonly native: {
|
|
31
|
+
readonly known: true;
|
|
32
|
+
/** RUNNING native sessions only, with the controller-visibility bit. */
|
|
33
|
+
readonly sessions: ReadonlyArray<{
|
|
34
|
+
readonly name: string;
|
|
35
|
+
readonly controlled: boolean | undefined;
|
|
36
|
+
}>;
|
|
37
|
+
} | {
|
|
38
|
+
readonly known: false;
|
|
39
|
+
};
|
|
40
|
+
};
|
|
41
|
+
/**
|
|
42
|
+
* Per-tab command for an ABSENT local session: create it via `remote run …
|
|
43
|
+
* --resume …` (which attaches by default). Live sessions are deliberately
|
|
44
|
+
* handled by `launchLayout`, which knows their exact tmux names and whether they
|
|
45
|
+
* have a client attached; it emits `tmux attach -t <exact-name>` and never
|
|
46
|
+
* relaunches them.
|
|
47
|
+
*/
|
|
48
|
+
export declare function tabCommand(tab: LayoutTab, liveSlugs?: ReadonlySet<string>, opts?: {
|
|
49
|
+
forceGateway?: "gateway" | "direct";
|
|
50
|
+
/** Exact existing tmux name — attach directly, never replace/relaunch it. */
|
|
51
|
+
attachSession?: string;
|
|
52
|
+
}): string;
|
|
53
|
+
/** Group collision candidates before restore emits bare-slug attach/resume commands. */
|
|
54
|
+
export declare function ambiguousLiveSessionNames(sessions: readonly Pick<LocalSession, "name" | "slug">[]): Map<string, string[]>;
|
|
55
|
+
/**
|
|
56
|
+
* Launch the layout in gnome-terminal: one window per group, one tab per session.
|
|
57
|
+
*
|
|
58
|
+
* Default behaviour: a live session with a tmux client is already visible and is
|
|
59
|
+
* skipped; a live session with zero clients is orphaned, so a terminal tab is
|
|
60
|
+
* opened directly onto that exact tmux session. An absent session is launched as
|
|
61
|
+
* before. `--reattach` explicitly opens another tab even for an already-visible
|
|
62
|
+
* session. This is deliberately a view decision, not a liveness decision.
|
|
63
|
+
*
|
|
64
|
+
* Every decision reads the `hostView` PARAMETER — the plan's one snapshot —
|
|
65
|
+
* never a live host (F3: this module has no reader to call).
|
|
66
|
+
*/
|
|
67
|
+
export declare function launchLayout(windows: LayoutWindow[], hostView: HostViewSnapshot, stderr?: NodeJS.WriteStream, opts?: {
|
|
68
|
+
reattach?: boolean;
|
|
69
|
+
forceGateway?: "gateway" | "direct";
|
|
70
|
+
}): {
|
|
71
|
+
opened: number;
|
|
72
|
+
skippedLive: string[];
|
|
73
|
+
};
|
|
74
|
+
//# sourceMappingURL=launch-layout.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"launch-layout.d.ts","sourceRoot":"","sources":["../src/launch-layout.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAOH,OAAO,EAA6B,KAAK,YAAY,EAAE,MAAM,WAAW,CAAC;AACzE,OAAO,KAAK,EAAE,SAAS,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAE5D;;;;;GAKG;AACH,MAAM,MAAM,gBAAgB,GAAG;IAC7B,QAAQ,CAAC,IAAI,EACT;QAAE,QAAQ,CAAC,KAAK,EAAE,IAAI,CAAC;QAAC,QAAQ,CAAC,QAAQ,EAAE,aAAa,CAAC,YAAY,CAAC,CAAA;KAAE,GACxE;QAAE,QAAQ,CAAC,KAAK,EAAE,KAAK,CAAC;QAAC,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IACxD,QAAQ,CAAC,MAAM,EACX;QACE,QAAQ,CAAC,KAAK,EAAE,IAAI,CAAC;QACrB,wEAAwE;QACxE,QAAQ,CAAC,QAAQ,EAAE,aAAa,CAAC;YAC/B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;YACtB,QAAQ,CAAC,UAAU,EAAE,OAAO,GAAG,SAAS,CAAC;SAC1C,CAAC,CAAC;KACJ,GACD;QAAE,QAAQ,CAAC,KAAK,EAAE,KAAK,CAAA;KAAE,CAAC;CAC/B,CAAC;AAEF;;;;;;GAMG;AACH,wBAAgB,UAAU,CACxB,GAAG,EAAE,SAAS,EACd,SAAS,GAAE,WAAW,CAAC,MAAM,CAAa,EAC1C,IAAI,GAAE;IACJ,YAAY,CAAC,EAAE,SAAS,GAAG,QAAQ,CAAC;IACpC,6EAA6E;IAC7E,aAAa,CAAC,EAAE,MAAM,CAAC;CACnB,GACL,MAAM,CAgDR;AAED,wFAAwF;AACxF,wBAAgB,yBAAyB,CACvC,QAAQ,EAAE,SAAS,IAAI,CAAC,YAAY,EAAE,MAAM,GAAG,MAAM,CAAC,EAAE,GACvD,GAAG,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,CAUvB;AA4BD;;;;;;;;;;;GAWG;AACH,wBAAgB,YAAY,CAC1B,OAAO,EAAE,YAAY,EAAE,EACvB,QAAQ,EAAE,gBAAgB,EAC1B,MAAM,GAAE,MAAM,CAAC,WAA4B,EAC3C,IAAI,GAAE;IAAE,QAAQ,CAAC,EAAE,OAAO,CAAC;IAAC,YAAY,CAAC,EAAE,SAAS,GAAG,QAAQ,CAAA;CAAO,GACrE;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,WAAW,EAAE,MAAM,EAAE,CAAA;CAAE,CAgQ3C"}
|
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Terminal-layout rendering for `remote restore` — split out of restore.ts so
|
|
3
|
+
* the F3 invariant is a MODULE-BOUNDARY property, not a review rule:
|
|
4
|
+
*
|
|
5
|
+
* launchLayout decides every view/launch from the ONE `HostViewSnapshot`
|
|
6
|
+
* it receives AS A PARAMETER. This module must NOT import any live host
|
|
7
|
+
* reader (listNativeSessions, listLocalSessions*, nativeSession*, probe*):
|
|
8
|
+
* you can't call what you don't have, so a divergence between plan time
|
|
9
|
+
* and launch time can never be re-read here — and an error or divergence
|
|
10
|
+
* NEVER becomes permission to launch (`live -> empty` yields attach or
|
|
11
|
+
* refuse, never `h2a run` fabricating a second writer on a live
|
|
12
|
+
* conversation). Only PURE name helpers may be imported from tmux.js.
|
|
13
|
+
*/
|
|
14
|
+
import { spawn } from "node:child_process";
|
|
15
|
+
import { mkdirSync, writeFileSync } from "node:fs";
|
|
16
|
+
import { homedir } from "node:os";
|
|
17
|
+
import { join } from "node:path";
|
|
18
|
+
import { localSessionName, slugify } from "./tmux.js";
|
|
19
|
+
/**
|
|
20
|
+
* Per-tab command for an ABSENT local session: create it via `remote run …
|
|
21
|
+
* --resume …` (which attaches by default). Live sessions are deliberately
|
|
22
|
+
* handled by `launchLayout`, which knows their exact tmux names and whether they
|
|
23
|
+
* have a client attached; it emits `tmux attach -t <exact-name>` and never
|
|
24
|
+
* relaunches them.
|
|
25
|
+
*/
|
|
26
|
+
export function tabCommand(tab, liveSlugs = new Set(), opts = {}) {
|
|
27
|
+
const q = (s) => `'${s.replace(/'/g, "'\\''")}'`;
|
|
28
|
+
if (tab.remoteId) {
|
|
29
|
+
// SCW: attach straight into the Pod's tmux (live, copy-friendly).
|
|
30
|
+
return `h2a attach ${q(tab.remoteId)} --exec`;
|
|
31
|
+
}
|
|
32
|
+
const slug = slugify(tab.label);
|
|
33
|
+
if (tab.attachLive) {
|
|
34
|
+
// §4.1 attach-live: a live managed row whose conversation id is
|
|
35
|
+
// unresolved re-enters by EXACT managed name only. No resume id exists,
|
|
36
|
+
// so no run/replace command may ever be rendered for it.
|
|
37
|
+
return `h2a attach ${q(tab.managedName ?? localSessionName(slug))}`;
|
|
38
|
+
}
|
|
39
|
+
// Effective gateway posture: an explicit `restore --gw/--no-gw` OVERRIDES the
|
|
40
|
+
// per-instance pin; otherwise honour the pinned gatewayMode (absent = default).
|
|
41
|
+
const posture = opts.forceGateway ?? tab.gatewayMode;
|
|
42
|
+
const gwFlag = posture === "gateway" ? " --gw" : posture === "direct" ? " --no-gw" : "";
|
|
43
|
+
// Persisted-host pin — the gatewayMode pattern applied to the terminal
|
|
44
|
+
// host: a NON-LIVE session is re-created on the host its registry record
|
|
45
|
+
// names, never on the current default. A recorded native session survives
|
|
46
|
+
// the fleet-wide H2A_SESSION_HOST=tmux valve via the env pin (any other
|
|
47
|
+
// value selects native — resolveSessionHostKind), where it fails closed if
|
|
48
|
+
// the native host is unavailable rather than silently re-routing; a
|
|
49
|
+
// recorded tmux session survives the native default via --tmux. Tabs
|
|
50
|
+
// without a recorded host (scan fallback) keep following the default.
|
|
51
|
+
const nativePin = tab.hostKind === "local-native" ? "H2A_SESSION_HOST=native " : "";
|
|
52
|
+
const tmuxPin = tab.hostKind === "local-tmux" ? " --tmux" : "";
|
|
53
|
+
const runCmd = (extra) => `${nativePin}h2a run ${q(tab.tool ?? "shell")} ${q(tab.cwd)} ` +
|
|
54
|
+
(tab.sid ? `--resume ${q(tab.sid)} ` : "") +
|
|
55
|
+
`--name ${q(tab.label)}${gwFlag}${tmuxPin}${extra}`;
|
|
56
|
+
if (opts.attachSession) {
|
|
57
|
+
// `h2a attach` resolves the exact managed name and honors the session's
|
|
58
|
+
// RECORDED host (tmux attach for tmux sessions, the native bridge for
|
|
59
|
+
// native-PTY sessions) — a raw `tmux attach -t` here would strand every
|
|
60
|
+
// native session behind a "no such session" error.
|
|
61
|
+
return `h2a attach ${q(opts.attachSession)}`;
|
|
62
|
+
}
|
|
63
|
+
if (liveSlugs.has(slug)) {
|
|
64
|
+
// Compatibility for direct callers that only know a unique slug. Restore
|
|
65
|
+
// itself always supplies the exact name above, avoiding prefix collisions.
|
|
66
|
+
// Crucially, a gateway override never turns a live session into --replace:
|
|
67
|
+
// restore must preserve its process and conversation.
|
|
68
|
+
return `h2a attach ${q(slug)}`;
|
|
69
|
+
}
|
|
70
|
+
return runCmd("");
|
|
71
|
+
}
|
|
72
|
+
/** Group collision candidates before restore emits bare-slug attach/resume commands. */
|
|
73
|
+
export function ambiguousLiveSessionNames(sessions) {
|
|
74
|
+
const namesBySlug = new Map();
|
|
75
|
+
for (const session of sessions) {
|
|
76
|
+
const names = namesBySlug.get(session.slug) ?? [];
|
|
77
|
+
names.push(session.name);
|
|
78
|
+
namesBySlug.set(session.slug, names);
|
|
79
|
+
}
|
|
80
|
+
return new Map([...namesBySlug].filter(([, names]) => names.length > 1));
|
|
81
|
+
}
|
|
82
|
+
// gnome-terminal applies ONE trailing `-- command` to EVERY tab of an
|
|
83
|
+
// invocation (you cannot give each tab its own `--`). So all tabs run the same
|
|
84
|
+
// dispatcher; each claims (under flock) the first map line matching its $PWD
|
|
85
|
+
// and runs that tab's command — exactly how ~/bin/resume-dev-sessions worked.
|
|
86
|
+
const DISPATCHER = `map="$1"
|
|
87
|
+
lock="$map.lock"
|
|
88
|
+
exec 9>"$lock"; flock 9
|
|
89
|
+
line=$(awk -F'\\t' -v c="$PWD" '$1==c{print;exit}' "$map")
|
|
90
|
+
if [ -n "$line" ]; then
|
|
91
|
+
awk -F'\\t' -v c="$PWD" 'BEGIN{d=0} d==0 && $1==c {d=1; next} {print}' "$map" > "$map.tmp" && mv "$map.tmp" "$map"
|
|
92
|
+
fi
|
|
93
|
+
flock -u 9
|
|
94
|
+
cmd=$(printf '%s' "$line" | cut -f2-)
|
|
95
|
+
if [ -n "$cmd" ]; then eval "$cmd"; else echo "[h2a] rien a reprendre pour $PWD" >&2; fi
|
|
96
|
+
exec bash -l`;
|
|
97
|
+
let mapCounter = 0;
|
|
98
|
+
function runDir() {
|
|
99
|
+
const base = process.env.XDG_RUNTIME_DIR
|
|
100
|
+
? join(process.env.XDG_RUNTIME_DIR, "sentropic-remote")
|
|
101
|
+
: join(homedir(), ".config", "sentropic", "remote-cli", "run");
|
|
102
|
+
mkdirSync(base, { recursive: true });
|
|
103
|
+
return base;
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Launch the layout in gnome-terminal: one window per group, one tab per session.
|
|
107
|
+
*
|
|
108
|
+
* Default behaviour: a live session with a tmux client is already visible and is
|
|
109
|
+
* skipped; a live session with zero clients is orphaned, so a terminal tab is
|
|
110
|
+
* opened directly onto that exact tmux session. An absent session is launched as
|
|
111
|
+
* before. `--reattach` explicitly opens another tab even for an already-visible
|
|
112
|
+
* session. This is deliberately a view decision, not a liveness decision.
|
|
113
|
+
*
|
|
114
|
+
* Every decision reads the `hostView` PARAMETER — the plan's one snapshot —
|
|
115
|
+
* never a live host (F3: this module has no reader to call).
|
|
116
|
+
*/
|
|
117
|
+
export function launchLayout(windows, hostView, stderr = process.stderr, opts = {}) {
|
|
118
|
+
const tmuxKnown = hostView.tmux.known;
|
|
119
|
+
const tmuxReason = hostView.tmux.known ? undefined : hostView.tmux.reason;
|
|
120
|
+
const tmuxSessions = hostView.tmux.known
|
|
121
|
+
? hostView.tmux.sessions
|
|
122
|
+
: [];
|
|
123
|
+
// Native view by EXACT session name, carrying the controller-visibility
|
|
124
|
+
// bit. A failed inventory read is an UNKNOWN view (undefined map) — never
|
|
125
|
+
// "no sessions": launching a replacement onto an unprovable host state
|
|
126
|
+
// could create a second writer, so unknown fails closed below.
|
|
127
|
+
const nativeSessionsByName = hostView.native.known
|
|
128
|
+
? new Map(hostView.native.sessions.map((session) => [
|
|
129
|
+
session.name,
|
|
130
|
+
{ controlled: session.controlled },
|
|
131
|
+
]))
|
|
132
|
+
: undefined;
|
|
133
|
+
// Slug-union view for LEGACY tabs without a recorded host (scan fallback)
|
|
134
|
+
// ONLY — a tab that carries its persisted hostKind + exact managed name is
|
|
135
|
+
// rendered from that identity and never chooses a host from a merged slug
|
|
136
|
+
// list. A CONTROLLED native session counts as attached (already visible).
|
|
137
|
+
const nativeLive = nativeSessionsByName !== undefined
|
|
138
|
+
? [...nativeSessionsByName.entries()]
|
|
139
|
+
.filter(([name]) => name.startsWith("h2a-") && !name.endsWith(".h2a"))
|
|
140
|
+
.map(([name, view]) => ({
|
|
141
|
+
name,
|
|
142
|
+
slug: name.slice("h2a-".length),
|
|
143
|
+
attached: view.controlled === true,
|
|
144
|
+
}))
|
|
145
|
+
: [];
|
|
146
|
+
const liveSessions = [
|
|
147
|
+
...tmuxSessions,
|
|
148
|
+
...nativeLive.filter((native) => !tmuxSessions.some((s) => s.name === native.name)),
|
|
149
|
+
];
|
|
150
|
+
const ambiguousLiveNames = ambiguousLiveSessionNames(liveSessions);
|
|
151
|
+
const skippedLive = [];
|
|
152
|
+
let opened = 0;
|
|
153
|
+
// A gateway override can configure a NEW process but must never replace a live
|
|
154
|
+
// one. `--reattach` is the sole opt-in that duplicates an already-visible tab.
|
|
155
|
+
const includeAttached = opts.reattach === true;
|
|
156
|
+
const tabOpts = opts.forceGateway ? { forceGateway: opts.forceGateway } : {};
|
|
157
|
+
if (!tmuxKnown) {
|
|
158
|
+
// We cannot reliably tell "attached" from "orphaned". Prefer a harmless
|
|
159
|
+
// duplicate/failing attach over `h2a run` possibly racing a live tmux
|
|
160
|
+
// session; a restore command must never reclaim a view by killing its work.
|
|
161
|
+
stderr.write(`[h2a] tmux view state unavailable${tmuxReason ? ` (${tmuxReason})` : ""}; ` +
|
|
162
|
+
"opening attach tabs rather than relaunching local sessions\n");
|
|
163
|
+
}
|
|
164
|
+
for (const win of windows) {
|
|
165
|
+
// Remote (k8s) tabs are always included — we can't probe pod health here.
|
|
166
|
+
// Local tabs carry their finalized command because a live orphan must attach
|
|
167
|
+
// to its exact tmux name, while an absent session must be launched.
|
|
168
|
+
const activeTabs = [];
|
|
169
|
+
for (const t of win.tabs) {
|
|
170
|
+
if (t.remoteId) {
|
|
171
|
+
activeTabs.push({ tab: t, command: tabCommand(t) });
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
const slug = slugify(t.label);
|
|
175
|
+
// --- Tabs WITH a recorded host: exact-identity, single-host render. ---
|
|
176
|
+
// The host decision was made by the plan (persisted kind + exact
|
|
177
|
+
// managed name); this block only renders the view for THAT host.
|
|
178
|
+
if (t.hostKind === "local-native") {
|
|
179
|
+
const exact = t.managedName ?? `h2a-${slug}`;
|
|
180
|
+
if (nativeSessionsByName === undefined) {
|
|
181
|
+
stderr.write(`[h2a] restore: native host state UNKNOWN — "${t.label}" neither relaunched nor attached (fail closed)\n`);
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
const view = nativeSessionsByName.get(exact);
|
|
185
|
+
if (view !== undefined) {
|
|
186
|
+
if (view.controlled === true) {
|
|
187
|
+
// running && controlled → already visible. Even --reattach must
|
|
188
|
+
// not open a competing controller (single-controller model).
|
|
189
|
+
skippedLive.push(t.label);
|
|
190
|
+
if (includeAttached) {
|
|
191
|
+
stderr.write(`[h2a] restore: "${t.label}" is already controlled in a terminal; not opening a second controller\n`);
|
|
192
|
+
}
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
195
|
+
if (view.controlled === undefined) {
|
|
196
|
+
// running && view unknown (host predates the visibility bit):
|
|
197
|
+
// preserve the process, report, never relaunch.
|
|
198
|
+
stderr.write(`[h2a] restore: controller visibility UNKNOWN for running native session "${t.label}"; attach manually: h2a attach ${exact}\n`);
|
|
199
|
+
continue;
|
|
200
|
+
}
|
|
201
|
+
// running && !controlled → exactly one exact-name attach.
|
|
202
|
+
activeTabs.push({
|
|
203
|
+
tab: t,
|
|
204
|
+
command: tabCommand(t, new Set([slug]), { attachSession: exact }),
|
|
205
|
+
});
|
|
206
|
+
continue;
|
|
207
|
+
}
|
|
208
|
+
// Dead/absent on a KNOWN native view.
|
|
209
|
+
if (t.attachLive) {
|
|
210
|
+
stderr.write(`[h2a] restore: "${t.label}" was live at plan time but is gone; no resolved conversation exists to relaunch it\n`);
|
|
211
|
+
continue;
|
|
212
|
+
}
|
|
213
|
+
activeTabs.push({ tab: t, command: tabCommand(t, new Set(), tabOpts) });
|
|
214
|
+
continue;
|
|
215
|
+
}
|
|
216
|
+
if (t.hostKind === "local-tmux") {
|
|
217
|
+
const exact = t.managedName ?? `h2a-${slug}`;
|
|
218
|
+
if (!tmuxKnown) {
|
|
219
|
+
// Conservative: a harmless exact attach, never a relaunch race.
|
|
220
|
+
activeTabs.push({
|
|
221
|
+
tab: t,
|
|
222
|
+
command: tabCommand(t, new Set([slug]), { attachSession: exact }),
|
|
223
|
+
});
|
|
224
|
+
continue;
|
|
225
|
+
}
|
|
226
|
+
const liveTmux = tmuxSessions.find((session) => session.name === exact);
|
|
227
|
+
if (liveTmux) {
|
|
228
|
+
// DRAIN view: the live legacy process is attach-only — dispatched
|
|
229
|
+
// to its persisted host via `h2a attach`; never replaced, and no
|
|
230
|
+
// tmux session is ever created here.
|
|
231
|
+
if (!includeAttached && liveTmux.attached) {
|
|
232
|
+
skippedLive.push(t.label);
|
|
233
|
+
continue;
|
|
234
|
+
}
|
|
235
|
+
activeTabs.push({
|
|
236
|
+
tab: t,
|
|
237
|
+
command: tabCommand(t, new Set([slug]), { attachSession: exact }),
|
|
238
|
+
});
|
|
239
|
+
continue;
|
|
240
|
+
}
|
|
241
|
+
if (t.attachLive) {
|
|
242
|
+
stderr.write(`[h2a] restore: "${t.label}" was live at plan time but is gone; no resolved conversation exists to relaunch it\n`);
|
|
243
|
+
continue;
|
|
244
|
+
}
|
|
245
|
+
// Dead recorded tmux row: re-created on its recorded host via the
|
|
246
|
+
// --tmux pin (tabCommand). Which host a dead tmux session should be
|
|
247
|
+
// relaunched on is a REOPENED policy question — this keeps the #199
|
|
248
|
+
// behavior as-is.
|
|
249
|
+
activeTabs.push({ tab: t, command: tabCommand(t, new Set(), tabOpts) });
|
|
250
|
+
continue;
|
|
251
|
+
}
|
|
252
|
+
// --- Legacy tabs without a recorded host: slug-union view (scan). ---
|
|
253
|
+
if (!tmuxKnown) {
|
|
254
|
+
activeTabs.push({
|
|
255
|
+
tab: t,
|
|
256
|
+
command: tabCommand(t, new Set([slug]), { attachSession: `h2a-${slug}` }),
|
|
257
|
+
});
|
|
258
|
+
continue;
|
|
259
|
+
}
|
|
260
|
+
const names = ambiguousLiveNames.get(slug);
|
|
261
|
+
if (names) {
|
|
262
|
+
skippedLive.push(t.label);
|
|
263
|
+
stderr.write(`[h2a] restore skipped "${t.label}": local tmux slug is ambiguous (${names.sort().join(", ")}); ` +
|
|
264
|
+
`attach explicitly with h2a attach ${names.sort()[0]} or h2a attach ${names.sort()[1]}\n`);
|
|
265
|
+
continue;
|
|
266
|
+
}
|
|
267
|
+
const liveSession = liveSessions.find((session) => session.slug === slug);
|
|
268
|
+
if (liveSession && !includeAttached && liveSession.attached) {
|
|
269
|
+
skippedLive.push(t.label);
|
|
270
|
+
continue;
|
|
271
|
+
}
|
|
272
|
+
if (!liveSession && nativeSessionsByName === undefined) {
|
|
273
|
+
// The tab would LAUNCH — but the native host state is unknown, so a
|
|
274
|
+
// homonymous live native session cannot be ruled out. Fail closed.
|
|
275
|
+
stderr.write(`[h2a] restore: native host state UNKNOWN — "${t.label}" not launched (fail closed)\n`);
|
|
276
|
+
continue;
|
|
277
|
+
}
|
|
278
|
+
activeTabs.push({
|
|
279
|
+
tab: t,
|
|
280
|
+
command: liveSession
|
|
281
|
+
? tabCommand(t, new Set([slug]), { attachSession: liveSession.name })
|
|
282
|
+
: tabCommand(t, new Set(), tabOpts),
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
if (activeTabs.length === 0) {
|
|
286
|
+
// All sessions in this window are already active — no tab needed.
|
|
287
|
+
continue;
|
|
288
|
+
}
|
|
289
|
+
// Map keyed by per-tab working directory -> the tab's command. Tabs sharing
|
|
290
|
+
// a cwd (several sessions of one project) each claim a distinct line FIFO.
|
|
291
|
+
const slug = win.title.replace(/[^a-zA-Z0-9]+/g, "-");
|
|
292
|
+
const mapPath = join(runDir(), `restore-${process.pid}-${slug}-${mapCounter++}.map`);
|
|
293
|
+
const body = activeTabs
|
|
294
|
+
.map(({ tab, command }) => `${tab.cwd}\t${command}`)
|
|
295
|
+
.join("\n") + "\n";
|
|
296
|
+
writeFileSync(mapPath, body, "utf8");
|
|
297
|
+
const args = [];
|
|
298
|
+
activeTabs.forEach(({ tab }, i) => {
|
|
299
|
+
args.push(i === 0 ? "--window" : "--tab", `--working-directory=${tab.cwd}`, `--title=${tab.label}`);
|
|
300
|
+
});
|
|
301
|
+
// ONE shared dispatcher command for all tabs of this window.
|
|
302
|
+
args.push("--", "bash", "-lc", DISPATCHER, "remote-restore", mapPath);
|
|
303
|
+
stderr.write(`[h2a] fenêtre "${win.title}" (${activeTabs.length} onglet(s))\n`);
|
|
304
|
+
// Surface gnome-terminal errors (e.g. "Failed to get screen…") instead of
|
|
305
|
+
// silently claiming the window opened.
|
|
306
|
+
const { GNOME_TERMINAL_SCREEN: _drop, ...childEnv } = process.env;
|
|
307
|
+
const child = spawn("gnome-terminal", args, {
|
|
308
|
+
stdio: ["ignore", "ignore", "pipe"],
|
|
309
|
+
detached: true,
|
|
310
|
+
env: childEnv,
|
|
311
|
+
});
|
|
312
|
+
child.stderr?.on("data", (chunk) => {
|
|
313
|
+
stderr.write(`[h2a] gnome-terminal: ${chunk.toString().trim()}\n`);
|
|
314
|
+
});
|
|
315
|
+
child.unref();
|
|
316
|
+
opened += activeTabs.length;
|
|
317
|
+
}
|
|
318
|
+
if (skippedLive.length > 0) {
|
|
319
|
+
stderr.write(`[h2a] ${skippedLive.length} session(s) déjà visibles ignorées` +
|
|
320
|
+
` (--reattach pour les rouvrir quand même): ${skippedLive.join(", ")}\n`);
|
|
321
|
+
}
|
|
322
|
+
return { opened, skippedLive };
|
|
323
|
+
}
|
|
324
|
+
//# sourceMappingURL=launch-layout.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"launch-layout.js","sourceRoot":"","sources":["../src/launch-layout.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
import { Hono } from "hono";
|
|
2
|
+
import { type LlmMeshRoutingConfig } from "../llm-routing-config.js";
|
|
3
|
+
export interface LocalGatewayAppOptions {
|
|
4
|
+
readonly routing?: LlmMeshRoutingConfig;
|
|
5
|
+
readonly ownerScopeRef?: string;
|
|
6
|
+
}
|
|
7
|
+
export declare function createLocalGatewayApp(options?: LocalGatewayAppOptions): Hono;
|
|
2
8
|
export declare const app: Hono<import("hono/types").BlankEnv, import("hono/types").BlankSchema, "/">;
|
|
3
9
|
export declare function startServer(): void;
|
|
4
10
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/llm-gateway-runtime/index.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/llm-gateway-runtime/index.ts"],"names":[],"mappings":"AAmBA,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAM5B,OAAO,EAGL,KAAK,oBAAoB,EAC1B,MAAM,0BAA0B,CAAC;AAoDlC,MAAM,WAAW,sBAAsB;IACrC,QAAQ,CAAC,OAAO,CAAC,EAAE,oBAAoB,CAAC;IACxC,QAAQ,CAAC,aAAa,CAAC,EAAE,MAAM,CAAC;CACjC;AAED,wBAAgB,qBAAqB,CAAC,OAAO,GAAE,sBAA2B,GAAG,IAAI,CA6FhF;AAED,eAAO,MAAM,GAAG,4EAA0B,CAAC;AAE3C,wBAAgB,WAAW,IAAI,IAAI,CASlC"}
|
|
@@ -1,70 +1,144 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
1
2
|
import { serve } from "@hono/node-server";
|
|
3
|
+
import { CloudCodeRuntimeClient, CodexRuntimeClient, GeminiAdapter, OpenAIAdapter, createLlmMesh, createProviderRegistry, } from "@sentropic/llm-mesh";
|
|
4
|
+
import { createGatewayRouter, stubGatewayConfig, } from "@sentropic/llm-gateway";
|
|
2
5
|
import { Hono } from "hono";
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
return
|
|
21
|
-
}
|
|
22
|
-
if (typeof body.sessionId !== "string" || !body.sessionId) {
|
|
23
|
-
return c.json({ error: "sessionId (string) required" }, 400);
|
|
6
|
+
import { createCliLlmMeshFacade, llmMeshOwnerScopeRef, readLlmMeshConfig, } from "../llm-mesh.js";
|
|
7
|
+
import { parseLlmMeshRoutingConfig, routingProfiles, } from "../llm-routing-config.js";
|
|
8
|
+
import { getSessionLedgerEntry, getSessionLedgerEntryForClient, listSessionLedger, recordRoutePlan, recordRouteSettlement, } from "./session-ledger.js";
|
|
9
|
+
import { acquireSession, lookupSessionById, lookupToken, sessionCount, } from "./sticky.js";
|
|
10
|
+
const routingConfigFromEnvironment = () => {
|
|
11
|
+
const raw = process.env.H2A_LLM_MESH_ROUTING_JSON;
|
|
12
|
+
return raw ? parseLlmMeshRoutingConfig(raw) : undefined;
|
|
13
|
+
};
|
|
14
|
+
const runtimeMesh = () => createLlmMesh({
|
|
15
|
+
registry: createProviderRegistry([
|
|
16
|
+
new OpenAIAdapter({ client: new CodexRuntimeClient() }),
|
|
17
|
+
new GeminiAdapter({ client: new CloudCodeRuntimeClient() }),
|
|
18
|
+
]),
|
|
19
|
+
});
|
|
20
|
+
const bearerFromHeaders = (headers) => {
|
|
21
|
+
const authorization = headers.authorization;
|
|
22
|
+
if (authorization?.toLowerCase().startsWith("bearer ")) {
|
|
23
|
+
return authorization.slice(7).trim();
|
|
24
24
|
}
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
25
|
+
return headers["x-api-key"]?.trim() || undefined;
|
|
26
|
+
};
|
|
27
|
+
const observablePlanner = (delegate) => ({
|
|
28
|
+
...(delegate.listModels
|
|
29
|
+
? { listModels: (subject) => delegate.listModels(subject) }
|
|
30
|
+
: {}),
|
|
31
|
+
async plan(subject, input) {
|
|
32
|
+
const plan = await delegate.plan(subject, input);
|
|
33
|
+
const session = lookupSessionById(subject.principalRef);
|
|
34
|
+
if (session)
|
|
35
|
+
recordRoutePlan(session, input, plan);
|
|
36
|
+
return plan;
|
|
37
|
+
},
|
|
38
|
+
prepareAttempt: (...args) => delegate.prepareAttempt(...args),
|
|
39
|
+
describeAffinity: (...args) => delegate.describeAffinity(...args),
|
|
40
|
+
promoteAffinity: (...args) => delegate.promoteAffinity(...args),
|
|
41
|
+
rebindAffinity: (...args) => delegate.rebindAffinity(...args),
|
|
42
|
+
resetAffinity: (...args) => delegate.resetAffinity(...args),
|
|
43
|
+
});
|
|
44
|
+
export function createLocalGatewayApp(options = {}) {
|
|
45
|
+
const routing = options.routing ?? routingConfigFromEnvironment();
|
|
46
|
+
const currentRouting = () => {
|
|
47
|
+
if (options.routing)
|
|
48
|
+
return options.routing;
|
|
49
|
+
const publicConfig = readLlmMeshConfig();
|
|
50
|
+
// A present config with no routing field is an intentional live reset.
|
|
51
|
+
// Fall back to the startup environment only when no config exists at all.
|
|
52
|
+
return publicConfig ? publicConfig.routing : routing;
|
|
53
|
+
};
|
|
54
|
+
const ownerScopeRef = options.ownerScopeRef ?? llmMeshOwnerScopeRef();
|
|
55
|
+
const facade = createCliLlmMeshFacade();
|
|
56
|
+
const profiles = routingProfiles(routing);
|
|
57
|
+
const planner = observablePlanner(facade.createRoutePlanner(runtimeMesh(), {
|
|
58
|
+
...(routing?.council ? { council: routing.council } : {}),
|
|
59
|
+
...(profiles ? { profiles } : {}),
|
|
60
|
+
}));
|
|
61
|
+
const callerAuth = {
|
|
62
|
+
async verify(headers) {
|
|
63
|
+
const token = bearerFromHeaders(headers);
|
|
64
|
+
const session = token ? await lookupToken(token) : undefined;
|
|
65
|
+
if (!session)
|
|
66
|
+
return { ok: false, reason: "invalid local gateway bearer" };
|
|
67
|
+
return {
|
|
68
|
+
ok: true,
|
|
69
|
+
cost: {
|
|
70
|
+
tenantId: ownerScopeRef,
|
|
71
|
+
principalId: session.sessionId,
|
|
72
|
+
ownerScopeRef,
|
|
73
|
+
...(session.workspaceId ? { workspaceId: session.workspaceId } : {}),
|
|
74
|
+
source: "h2a-local-gateway",
|
|
75
|
+
correlationId: randomUUID(),
|
|
76
|
+
},
|
|
77
|
+
};
|
|
78
|
+
},
|
|
79
|
+
};
|
|
80
|
+
const routeMetering = {
|
|
81
|
+
settleRoute(settlement) {
|
|
82
|
+
recordRouteSettlement(settlement);
|
|
83
|
+
},
|
|
84
|
+
};
|
|
85
|
+
const gateway = createGatewayRouter({
|
|
86
|
+
config: { ...stubGatewayConfig, callerAuth },
|
|
87
|
+
routePlanner: planner,
|
|
88
|
+
routeMetering,
|
|
89
|
+
routeInput({ cost }) {
|
|
90
|
+
const session = lookupSessionById(cost.principalId);
|
|
91
|
+
const activeRouting = currentRouting();
|
|
92
|
+
return {
|
|
93
|
+
affinityKey: session?.clientSessionId ?? cost.principalId,
|
|
94
|
+
...(session?.workspaceId ? { workspaceId: session.workspaceId } : {}),
|
|
95
|
+
...(activeRouting?.policy ? { policyOverride: activeRouting.policy } : {}),
|
|
96
|
+
...(activeRouting?.activeProfile ? { policyProfile: activeRouting.activeProfile } : {}),
|
|
97
|
+
...(activeRouting?.explicit ? { explicit: activeRouting.explicit } : {}),
|
|
98
|
+
};
|
|
99
|
+
},
|
|
100
|
+
});
|
|
101
|
+
const app = new Hono();
|
|
102
|
+
app.get("/health", (c) => c.json({ ok: true }));
|
|
103
|
+
app.post("/v1/session", async (c) => {
|
|
104
|
+
let body;
|
|
105
|
+
try {
|
|
106
|
+
body = await c.req.json();
|
|
107
|
+
}
|
|
108
|
+
catch {
|
|
109
|
+
return c.json({ error: "invalid JSON" }, 400);
|
|
110
|
+
}
|
|
111
|
+
if (typeof body.sessionId !== "string" || !body.sessionId.trim()) {
|
|
112
|
+
return c.json({ error: "sessionId (string) required" }, 400);
|
|
113
|
+
}
|
|
114
|
+
const session = await acquireSession(body.sessionId, {
|
|
115
|
+
...(typeof body.clientSessionId === "string"
|
|
116
|
+
? { clientSessionId: body.clientSessionId }
|
|
117
|
+
: {}),
|
|
29
118
|
...(typeof body.workspaceId === "string" ? { workspaceId: body.workspaceId } : {}),
|
|
30
119
|
...(typeof body.profile === "string" ? { profile: body.profile } : {}),
|
|
31
|
-
...(typeof body.clientSessionId === "string" ? { clientSessionId: body.clientSessionId } : {}),
|
|
32
|
-
...(typeof body.reasoningEffort === "string"
|
|
33
|
-
? { reasoningEffort: body.reasoningEffort }
|
|
34
|
-
: {}),
|
|
35
|
-
...(typeof body.requiredTransport === "string"
|
|
36
|
-
? { requiredTransport: body.requiredTransport }
|
|
37
|
-
: {}),
|
|
38
120
|
});
|
|
39
|
-
return c.json(
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
app.get("/v1/
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
// The tmux surface asks for one attested client session. This is deliberately
|
|
55
|
-
// not the collection endpoint: a five-second status refresh must stay O(1).
|
|
56
|
-
app.get("/v1/status/client/:clientSessionId", (c) => {
|
|
57
|
-
const entry = getSessionLedgerEntryForClient(c.req.param("clientSessionId"));
|
|
58
|
-
if (!entry)
|
|
59
|
-
return c.json({ error: "client session not found" }, 404);
|
|
60
|
-
return c.json(entry);
|
|
61
|
-
});
|
|
62
|
-
// Anthropic Messages proxy — pods call this with Bearer gw-<hex>
|
|
63
|
-
app.post("/v1/messages", handleMessages);
|
|
121
|
+
return c.json(session, 201);
|
|
122
|
+
});
|
|
123
|
+
app.get("/v1/sessions", (c) => c.json({ data: listSessionLedger() }));
|
|
124
|
+
app.get("/v1/sessions/:id", (c) => {
|
|
125
|
+
const entry = getSessionLedgerEntry(c.req.param("id"));
|
|
126
|
+
return entry ? c.json(entry) : c.json({ error: "session not found" }, 404);
|
|
127
|
+
});
|
|
128
|
+
app.get("/v1/status/client/:clientSessionId", (c) => {
|
|
129
|
+
const entry = getSessionLedgerEntryForClient(c.req.param("clientSessionId"));
|
|
130
|
+
return entry ? c.json(entry) : c.json({ error: "client session not found" }, 404);
|
|
131
|
+
});
|
|
132
|
+
app.route("/", gateway);
|
|
133
|
+
return app;
|
|
134
|
+
}
|
|
135
|
+
export const app = createLocalGatewayApp();
|
|
64
136
|
export function startServer() {
|
|
65
|
-
const port = parseInt(process.env.PORT ?? "
|
|
66
|
-
|
|
67
|
-
|
|
137
|
+
const port = Number.parseInt(process.env.PORT ?? "3002", 10);
|
|
138
|
+
// Session minting is intentionally unauthenticated control-plane traffic.
|
|
139
|
+
// The embedded solo-dev host therefore never listens beyond loopback.
|
|
140
|
+
serve({ fetch: app.fetch, port, hostname: "127.0.0.1" }, () => {
|
|
141
|
+
process.stdout.write(`[llm-gateway] listening on :${port} — ${sessionCount()} local bearers\n`);
|
|
68
142
|
});
|
|
69
143
|
}
|
|
70
144
|
if (process.env.NODE_ENV !== "test")
|