@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.
Files changed (108) hide show
  1. package/dist/config.d.ts +2 -0
  2. package/dist/config.d.ts.map +1 -1
  3. package/dist/config.js +16 -3
  4. package/dist/config.js.map +1 -1
  5. package/dist/conv-guard.d.ts +6 -1
  6. package/dist/conv-guard.d.ts.map +1 -1
  7. package/dist/conv-guard.js +33 -8
  8. package/dist/conv-guard.js.map +1 -1
  9. package/dist/enroll.d.ts.map +1 -1
  10. package/dist/enroll.js +12 -2
  11. package/dist/enroll.js.map +1 -1
  12. package/dist/index.d.ts +3 -1
  13. package/dist/index.d.ts.map +1 -1
  14. package/dist/index.js +819 -155
  15. package/dist/index.js.map +1 -1
  16. package/dist/launch-layout.d.ts +74 -0
  17. package/dist/launch-layout.d.ts.map +1 -0
  18. package/dist/launch-layout.js +324 -0
  19. package/dist/launch-layout.js.map +1 -0
  20. package/dist/llm-gateway-runtime/index.d.ts +6 -0
  21. package/dist/llm-gateway-runtime/index.d.ts.map +1 -1
  22. package/dist/llm-gateway-runtime/index.js +134 -60
  23. package/dist/llm-gateway-runtime/index.js.map +1 -1
  24. package/dist/llm-gateway-runtime/session-ledger.d.ts +38 -55
  25. package/dist/llm-gateway-runtime/session-ledger.d.ts.map +1 -1
  26. package/dist/llm-gateway-runtime/session-ledger.js +102 -250
  27. package/dist/llm-gateway-runtime/session-ledger.js.map +1 -1
  28. package/dist/llm-gateway-runtime/sticky.d.ts +25 -52
  29. package/dist/llm-gateway-runtime/sticky.d.ts.map +1 -1
  30. package/dist/llm-gateway-runtime/sticky.js +36 -605
  31. package/dist/llm-gateway-runtime/sticky.js.map +1 -1
  32. package/dist/llm-mesh.d.ts +19 -72
  33. package/dist/llm-mesh.d.ts.map +1 -1
  34. package/dist/llm-mesh.js +42 -399
  35. package/dist/llm-mesh.js.map +1 -1
  36. package/dist/llm-routing-config.d.ts +28 -0
  37. package/dist/llm-routing-config.d.ts.map +1 -0
  38. package/dist/llm-routing-config.js +100 -0
  39. package/dist/llm-routing-config.js.map +1 -0
  40. package/dist/native-host.d.ts +146 -0
  41. package/dist/native-host.d.ts.map +1 -0
  42. package/dist/native-host.js +456 -0
  43. package/dist/native-host.js.map +1 -0
  44. package/dist/native-terminal/host.d.ts +317 -1
  45. package/dist/native-terminal/host.d.ts.map +1 -1
  46. package/dist/native-terminal/host.js +696 -3
  47. package/dist/native-terminal/host.js.map +1 -1
  48. package/dist/native-terminal/op.d.ts +5 -0
  49. package/dist/native-terminal/op.d.ts.map +1 -0
  50. package/dist/native-terminal/op.js +408 -0
  51. package/dist/native-terminal/op.js.map +1 -0
  52. package/dist/native-terminal/process.d.ts +2 -0
  53. package/dist/native-terminal/process.d.ts.map +1 -1
  54. package/dist/native-terminal/process.js +16 -2
  55. package/dist/native-terminal/process.js.map +1 -1
  56. package/dist/native-terminal/server.d.ts.map +1 -1
  57. package/dist/native-terminal/server.js +6 -2
  58. package/dist/native-terminal/server.js.map +1 -1
  59. package/dist/native-terminal/socket-path.d.ts +8 -0
  60. package/dist/native-terminal/socket-path.d.ts.map +1 -1
  61. package/dist/native-terminal/socket-path.js +31 -1
  62. package/dist/native-terminal/socket-path.js.map +1 -1
  63. package/dist/native-terminal/supervisor.d.ts +13 -0
  64. package/dist/native-terminal/supervisor.d.ts.map +1 -1
  65. package/dist/native-terminal/supervisor.js +45 -0
  66. package/dist/native-terminal/supervisor.js.map +1 -1
  67. package/dist/pty.d.ts +23 -0
  68. package/dist/pty.d.ts.map +1 -1
  69. package/dist/pty.js +35 -0
  70. package/dist/pty.js.map +1 -1
  71. package/dist/readiness.d.ts +7 -0
  72. package/dist/readiness.d.ts.map +1 -1
  73. package/dist/readiness.js +12 -9
  74. package/dist/readiness.js.map +1 -1
  75. package/dist/registry.d.ts +324 -2
  76. package/dist/registry.d.ts.map +1 -1
  77. package/dist/registry.js +659 -20
  78. package/dist/registry.js.map +1 -1
  79. package/dist/restore.d.ts +91 -32
  80. package/dist/restore.d.ts.map +1 -1
  81. package/dist/restore.js +226 -186
  82. package/dist/restore.js.map +1 -1
  83. package/dist/status-projection.d.ts.map +1 -1
  84. package/dist/tmux.d.ts +9 -6
  85. package/dist/tmux.d.ts.map +1 -1
  86. package/dist/tmux.js +34 -8
  87. package/dist/tmux.js.map +1 -1
  88. package/package.json +3 -3
  89. package/dist/llm-gateway-runtime/accounts.d.ts +0 -62
  90. package/dist/llm-gateway-runtime/accounts.d.ts.map +0 -1
  91. package/dist/llm-gateway-runtime/accounts.js +0 -323
  92. package/dist/llm-gateway-runtime/accounts.js.map +0 -1
  93. package/dist/llm-gateway-runtime/model-catalog.d.ts +0 -44
  94. package/dist/llm-gateway-runtime/model-catalog.d.ts.map +0 -1
  95. package/dist/llm-gateway-runtime/model-catalog.js +0 -191
  96. package/dist/llm-gateway-runtime/model-catalog.js.map +0 -1
  97. package/dist/llm-gateway-runtime/proxy-anthropic.d.ts +0 -3
  98. package/dist/llm-gateway-runtime/proxy-anthropic.d.ts.map +0 -1
  99. package/dist/llm-gateway-runtime/proxy-anthropic.js +0 -318
  100. package/dist/llm-gateway-runtime/proxy-anthropic.js.map +0 -1
  101. package/dist/llm-gateway-runtime/proxy-cloud-code.d.ts +0 -11
  102. package/dist/llm-gateway-runtime/proxy-cloud-code.d.ts.map +0 -1
  103. package/dist/llm-gateway-runtime/proxy-cloud-code.js +0 -215
  104. package/dist/llm-gateway-runtime/proxy-cloud-code.js.map +0 -1
  105. package/dist/llm-gateway-runtime/proxy-openai.d.ts +0 -118
  106. package/dist/llm-gateway-runtime/proxy-openai.d.ts.map +0 -1
  107. package/dist/llm-gateway-runtime/proxy-openai.js +0 -1305
  108. 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":"AACA,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAW5B,eAAO,MAAM,GAAG,4EAAa,CAAC;AAkE9B,wBAAgB,WAAW,IAAI,IAAI,CAOlC"}
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 { acquireSession, sessionCount } from "./sticky.js";
4
- import { handleMessages } from "./proxy-anthropic.js";
5
- import { listAccountDescriptors, listRoutableModels } from "./accounts.js";
6
- import { modelCatalogResponse } from "./model-catalog.js";
7
- import { getSessionLedgerEntry, getSessionLedgerEntryForClient, listSessionLedger, } from "./session-ledger.js";
8
- export const app = new Hono();
9
- // Health — no internal state exposed
10
- app.get("/health", (c) => c.json({ ok: true }));
11
- app.get("/healthz", (c) => c.json({ ok: true }));
12
- // Session creation — called by control-plane before pod start.
13
- // Network policy enforces that pods cannot reach this endpoint.
14
- app.post("/v1/session", async (c) => {
15
- let body;
16
- try {
17
- body = await c.req.json();
18
- }
19
- catch {
20
- return c.json({ error: "invalid JSON" }, 400);
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
- try {
26
- const result = await acquireSession(body.sessionId, {
27
- ...(typeof body.provider === "string" ? { provider: body.provider } : {}),
28
- ...(typeof body.model === "string" ? { model: body.model } : {}),
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(result, 201);
40
- }
41
- catch (err) {
42
- return c.json({ error: err instanceof Error ? err.message : String(err) }, 400);
43
- }
44
- });
45
- app.get("/v1/accounts", (c) => c.json({ data: listAccountDescriptors() }));
46
- app.get("/v1/models", (c) => c.json(modelCatalogResponse(listRoutableModels())));
47
- app.get("/v1/sessions", (c) => c.json({ data: listSessionLedger() }));
48
- app.get("/v1/sessions/:id", (c) => {
49
- const entry = getSessionLedgerEntry(c.req.param("id"));
50
- if (!entry)
51
- return c.json({ error: "session not found" }, 404);
52
- return c.json(entry);
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 ?? "3001", 10);
66
- serve({ fetch: app.fetch, port }, () => {
67
- process.stdout.write(`[llm-gateway] listening on :${port} — ${sessionCount()} sessions in memory\n`);
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")