@telorun/runner-core 0.8.2 → 0.10.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 (98) hide show
  1. package/README.md +33 -2
  2. package/dist/backend.d.ts +107 -23
  3. package/dist/backend.d.ts.map +1 -1
  4. package/dist/config.d.ts +56 -0
  5. package/dist/config.d.ts.map +1 -1
  6. package/dist/config.js +36 -0
  7. package/dist/config.js.map +1 -1
  8. package/dist/contract.d.ts +210 -15
  9. package/dist/contract.d.ts.map +1 -1
  10. package/dist/contract.js +9 -0
  11. package/dist/contract.js.map +1 -1
  12. package/dist/debug/ports-resolved.d.ts +26 -0
  13. package/dist/debug/ports-resolved.d.ts.map +1 -0
  14. package/dist/debug/ports-resolved.js +42 -0
  15. package/dist/debug/ports-resolved.js.map +1 -0
  16. package/dist/debug/relay.d.ts.map +1 -1
  17. package/dist/debug/relay.js +34 -6
  18. package/dist/debug/relay.js.map +1 -1
  19. package/dist/debug/run-projection.d.ts +53 -0
  20. package/dist/debug/run-projection.d.ts.map +1 -0
  21. package/dist/debug/run-projection.js +115 -0
  22. package/dist/debug/run-projection.js.map +1 -0
  23. package/dist/index.d.ts +8 -2
  24. package/dist/index.d.ts.map +1 -1
  25. package/dist/index.js +7 -1
  26. package/dist/index.js.map +1 -1
  27. package/dist/routes/apps.d.ts +1 -1
  28. package/dist/routes/apps.d.ts.map +1 -1
  29. package/dist/routes/apps.js +14 -4
  30. package/dist/routes/apps.js.map +1 -1
  31. package/dist/routes/io.d.ts.map +1 -1
  32. package/dist/routes/io.js +42 -9
  33. package/dist/routes/io.js.map +1 -1
  34. package/dist/routes/session-start.d.ts +18 -13
  35. package/dist/routes/session-start.d.ts.map +1 -1
  36. package/dist/routes/session-start.js +95 -14
  37. package/dist/routes/session-start.js.map +1 -1
  38. package/dist/routes/sessions.d.ts +11 -1
  39. package/dist/routes/sessions.d.ts.map +1 -1
  40. package/dist/routes/sessions.js +465 -5
  41. package/dist/routes/sessions.js.map +1 -1
  42. package/dist/server.d.ts.map +1 -1
  43. package/dist/server.js +21 -1
  44. package/dist/server.js.map +1 -1
  45. package/dist/session/byte-ring-buffer.d.ts +6 -1
  46. package/dist/session/byte-ring-buffer.d.ts.map +1 -1
  47. package/dist/session/byte-ring-buffer.js +2 -2
  48. package/dist/session/byte-ring-buffer.js.map +1 -1
  49. package/dist/session/registry.d.ts +144 -13
  50. package/dist/session/registry.d.ts.map +1 -1
  51. package/dist/session/registry.js +166 -25
  52. package/dist/session/registry.js.map +1 -1
  53. package/dist/session/watch-supervisor.d.ts +42 -0
  54. package/dist/session/watch-supervisor.d.ts.map +1 -0
  55. package/dist/session/watch-supervisor.js +99 -0
  56. package/dist/session/watch-supervisor.js.map +1 -0
  57. package/dist/session/workspace-app.d.ts +4 -0
  58. package/dist/session/workspace-app.d.ts.map +1 -0
  59. package/dist/session/workspace-app.js +26 -0
  60. package/dist/session/workspace-app.js.map +1 -0
  61. package/dist/session/workspace-client.d.ts +33 -0
  62. package/dist/session/workspace-client.d.ts.map +1 -0
  63. package/dist/session/workspace-client.js +62 -0
  64. package/dist/session/workspace-client.js.map +1 -0
  65. package/dist/session/workspace-marker.d.ts +25 -0
  66. package/dist/session/workspace-marker.d.ts.map +1 -0
  67. package/dist/session/workspace-marker.js +35 -0
  68. package/dist/session/workspace-marker.js.map +1 -0
  69. package/dist/sse/channel.d.ts.map +1 -1
  70. package/dist/sse/channel.js +4 -0
  71. package/dist/sse/channel.js.map +1 -1
  72. package/package.json +4 -3
  73. package/src/app-catalog.test.ts +49 -1
  74. package/src/backend.ts +111 -22
  75. package/src/config.ts +98 -0
  76. package/src/contract.ts +213 -16
  77. package/src/debug/ports-resolved.test.ts +66 -0
  78. package/src/debug/ports-resolved.ts +43 -0
  79. package/src/debug/relay.ts +32 -4
  80. package/src/debug/run-projection.test.ts +155 -0
  81. package/src/debug/run-projection.ts +122 -0
  82. package/src/index.ts +20 -1
  83. package/src/routes/apps.ts +14 -5
  84. package/src/routes/io.ts +58 -12
  85. package/src/routes/session-start.ts +115 -27
  86. package/src/routes/sessions.ts +538 -7
  87. package/src/server.ts +22 -1
  88. package/src/session/byte-ring-buffer.ts +8 -2
  89. package/src/session/registry.ts +288 -28
  90. package/src/session/ring-buffer.test.ts +4 -3
  91. package/src/session/watch-lifetime.test.ts +203 -0
  92. package/src/session/watch-supervisor.ts +112 -0
  93. package/src/session/workspace-app.ts +27 -0
  94. package/src/session/workspace-client.ts +80 -0
  95. package/src/session/workspace-marker.test.ts +35 -0
  96. package/src/session/workspace-marker.ts +39 -0
  97. package/src/sse/channel.ts +5 -0
  98. package/workspace-app/telo.yaml +228 -0
@@ -42,11 +42,29 @@ export interface RunnerTerms {
42
42
  }
43
43
  /** HTTP header carrying the accepted terms version on `POST /v1/sessions`. */
44
44
  export declare const ACCEPTED_TERMS_HEADER = "x-telo-accepted-terms";
45
+ /**
46
+ * How an application container's byte channel is attached.
47
+ * - `tty` — one merged stream, as a terminal produces. Resize works;
48
+ * `CLICOLOR_FORCE` is injected.
49
+ * - `streams` — stdout and stderr separated at the source. Resize is rejected
50
+ * (meaningless with no PTY) and no colour is forced, so the app
51
+ * sees the environment it would see in production.
52
+ * Nothing is invented at the transport layer: docker's non-TTY attach is already
53
+ * a multiplexed stream carrying a per-frame stream id, and the kubernetes attach
54
+ * subresource without a TTY already gives separate stdout/stderr channels.
55
+ */
56
+ export type IoMode = "tty" | "streams";
45
57
  export interface RunnerFeatures {
46
- /** Runner exposes a live PTY byte channel (`/v1/sessions/:id/io`). */
47
- io: boolean;
58
+ /** Byte-channel attach modes this runner offers (`/v1/sessions/:id/io`). */
59
+ io: IoMode[];
48
60
  /** Runner can publish workload ports back to the client. */
49
61
  ports: boolean;
62
+ /** Watch sessions (`mode: "watch"`) are requestable. Server-gated: a runner
63
+ * with watch disabled advertises `false` and rejects the field. */
64
+ watch: boolean;
65
+ /** Catalog names admissible as a session's co-resident `agent`. Empty/absent
66
+ * when the operator configured none. */
67
+ agents?: string[];
50
68
  }
51
69
  /** An operator-predefined application the runner can launch by name. Only the
52
70
  * identity is advertised — the image and the operator env injected into it
@@ -90,6 +108,10 @@ export interface RunBundle {
90
108
  files: Array<{
91
109
  relativePath: string;
92
110
  contents: string;
111
+ /** `utf8` when omitted. A checkpoint re-seeds a resumed session through this
112
+ * same shape, and a workspace may hold a binary asset, so dropping anything
113
+ * that is not text would silently lose a file across a suspend. */
114
+ encoding?: "utf8" | "base64";
93
115
  }>;
94
116
  }
95
117
  export type PortProtocol = "tcp" | "udp";
@@ -112,6 +134,39 @@ export interface RunnerEndpoint {
112
134
  /** Fully-qualified URL when the endpoint is fronted by a proxy/ingress. */
113
135
  url?: string;
114
136
  }
137
+ /**
138
+ * How long a session lives relative to its runs.
139
+ * - `run` — one run. The workload exits and the session is terminal. Today's
140
+ * behaviour, and the default.
141
+ * - `watch` — a workspace that runs continuously. Each application container
142
+ * runs `telo run --watch`, an edit costs a kernel reload rather
143
+ * than a pod, and a completed one-shot run leaves the session up.
144
+ */
145
+ export type SessionMode = "run" | "watch";
146
+ /** The name a single-app session's one application takes when the request
147
+ * declares no `apps` — so every `run` / `debug` / `endpoints` event names an
148
+ * app, and no client needs two readings of the same stream. */
149
+ export declare const DEFAULT_APP_NAME = "app";
150
+ /**
151
+ * One application in a session — one container, one kernel, one watcher. Most
152
+ * workspaces have exactly one: an Application with several Services in its
153
+ * `targets:` is still one kernel. The multi-app case is two independent
154
+ * Applications, which genuinely cannot be merged (importing a `Telo.Application`
155
+ * is a hard error, so no manifest composes them).
156
+ */
157
+ export interface SessionAppSpec {
158
+ /** Unique within the session and usable as a DNS label — it appears in the
159
+ * container name and in every run event. */
160
+ name: string;
161
+ /** Bundle-relative entry manifest this app runs. */
162
+ entryRelativePath: string;
163
+ /** Ports this app declares. Unique across the WHOLE session: session hosts are
164
+ * `<port>-<sessionId>.<base-domain>`, a single label, so two apps on one port
165
+ * would collide with nothing to tell them apart. */
166
+ ports?: PortMapping[];
167
+ /** Terminal or separated streams; defaults to `tty`. */
168
+ io?: IoMode;
169
+ }
115
170
  export interface StartSessionRequest {
116
171
  bundle: RunBundle;
117
172
  env: Record<string, string>;
@@ -120,22 +175,63 @@ export interface StartSessionRequest {
120
175
  /** Request the kernel debug stream. When true the runner launches the
121
176
  * workload with `--inspect`, subscribes to the in-workload inspect endpoint
122
177
  * (reachable only by the runner — never published outward), and relays each
123
- * frame to the client as a `debug` {@link RunEvent}. */
178
+ * frame to the client as a `debug` {@link RunEvent}. A watch session always
179
+ * runs with it on — that stream is where `run` events are projected from. */
124
180
  inspect?: boolean;
181
+ /** Defaults to `run`. */
182
+ mode?: SessionMode;
183
+ /** Catalog name of a co-resident agent container. At most one per session,
184
+ * never per app: the agent's unit is the workspace, and two agents over one
185
+ * workspace would contend on the same files and split one conversation in
186
+ * half. Requires `mode: "watch"` — an agent with nothing watching its writes
187
+ * is a silent no-op. */
188
+ agent?: string;
189
+ /** The applications this session runs, one container each. Omitted, it
190
+ * defaults to a single app named `app` on the bundle's own entry with the
191
+ * request's `ports` — so a single-app session is written exactly as before. */
192
+ apps?: SessionAppSpec[];
125
193
  }
194
+ /**
195
+ * The SESSION's status — how the session itself is doing, not how any one run
196
+ * ended. The two are separate nouns on the same stream: a one-shot app finishing
197
+ * in a watch session emits `run.completed` and leaves the session `running`, so
198
+ * the next edit starts that app's next generation.
199
+ *
200
+ * `exited` belongs to a `run` session only, where the session IS the run.
201
+ */
126
202
  export type RunStatus = {
127
203
  kind: "starting";
128
204
  }
129
- /** `inspectUrl` is the kernel debug/inspection UI fronted by a proxy (set only
130
- * when the session ran with `inspect` and the runner has a public base URL);
131
- * absent when the inspect endpoint isn't externally reachable. */
205
+ /** Every application container is up. `inspectUrl` is the kernel
206
+ * debug/inspection UI fronted by a proxy (set only when the session ran with
207
+ * `inspect` and the runner has a public base URL); absent when the inspect
208
+ * endpoint isn't externally reachable.
209
+ *
210
+ * `agent` is where this session's co-resident agent answers, present only on
211
+ * a session that requested one AND that this runner could route. An ENDPOINT
212
+ * rather than a URL string because the no-proxy case is real — a docker
213
+ * runner publishing to the host knows the port and not the hostname the
214
+ * client reached it by, exactly as for an app's endpoint, so the client
215
+ * fills an empty `host` from its own base URL. Carried here and not in
216
+ * `endpoints` because the agent is session infrastructure: `endpoints` are
217
+ * the ports the APPLICATIONS declared, and merging the two would make an
218
+ * operator-run container look like part of the user's manifest. */
132
219
  | {
133
220
  kind: "running";
134
221
  endpoints?: RunnerEndpoint[];
135
222
  inspectUrl?: string;
223
+ agent?: RunnerEndpoint;
136
224
  } | {
137
225
  kind: "exited";
138
226
  code: number;
227
+ }
228
+ /** Reaped for idleness: the pod is gone, the workspace checkpoint is held, and
229
+ * `POST /v1/sessions/:id/resume` brings it back under the same session id.
230
+ * NOT terminal — nothing is evicted on it. Best-effort by design: the runner
231
+ * holds the checkpoint in memory, so a runner restart loses it and the editor
232
+ * (which holds the authoritative workspace) re-seeds from its own copy. */
233
+ | {
234
+ kind: "suspended";
139
235
  } | {
140
236
  kind: "failed";
141
237
  message: string;
@@ -152,34 +248,133 @@ export type RunPhase = "build" | "provision" | "boot";
152
248
  * runner from its own network. Drives the editor's endpoint badge
153
249
  * (spinner → ok / error) instead of an app-log line. */
154
250
  export type ReachabilityState = "checking" | "reachable" | "unreachable";
155
- export type RunEvent = {
156
- type: "stdout";
157
- chunk: string;
251
+ /** What started one generation of an application.
252
+ * - `initial` — the session came up
253
+ * - `watch` — a file changed under the app's watcher
254
+ * - `manual` — `POST /v1/sessions/:id/reload`
255
+ * - `resume` — a suspended session was brought back, or the app set changed */
256
+ export type RunTrigger = "initial" | "watch" | "manual" | "resume";
257
+ /**
258
+ * A RUN outcome — one per app per reload generation, distinct from the session
259
+ * status above. `generation` is monotonic per app and starts at 1; it is counted
260
+ * by the runner from the kernel lifecycle events already on the debug stream, so
261
+ * it asks the kernel for nothing.
262
+ */
263
+ export type RunOutcomeEvent = {
264
+ type: "run";
265
+ app: string;
266
+ generation: number;
267
+ phase: "started";
268
+ trigger: RunTrigger;
158
269
  } | {
159
- type: "stderr";
160
- chunk: string;
270
+ type: "run";
271
+ app: string;
272
+ generation: number;
273
+ phase: "completed";
274
+ code: number;
275
+ durationMs?: number;
161
276
  } | {
277
+ type: "run";
278
+ app: string;
279
+ generation: number;
280
+ phase: "failed";
281
+ reason: string;
282
+ };
283
+ /**
284
+ * Workload output does NOT travel this channel, in either mode — it goes over the
285
+ * byte channel (`/io`), which exists precisely because per-chunk events are
286
+ * wasteful for high-volume output. The `stdout` / `stderr` variants that used to
287
+ * be declared here were emitted by nothing: a contract in shape only.
288
+ */
289
+ export type RunEvent = {
162
290
  type: "status";
163
291
  status: RunStatus;
164
- } | {
292
+ }
293
+ /** `app` is absent for session-scoped provisioning (scheduling, image pull) and
294
+ * present once the message belongs to one application. */
295
+ | {
165
296
  type: "progress";
297
+ app?: string;
166
298
  phase: RunPhase;
167
299
  message: string;
168
300
  done?: boolean;
169
301
  }
170
- /** A frame relayed from the workload's kernel debug stream (kernel event or
171
- * log line). Only emitted when the session was started with `inspect`. */
302
+ /** A frame relayed from one app's kernel debug stream (kernel event or log
303
+ * line). Only emitted when the session was started with `inspect`. */
172
304
  | {
173
305
  type: "debug";
306
+ app: string;
174
307
  frame: DebugFrame;
175
308
  }
176
- /** A reachability transition for one declared port, keyed by port. */
309
+ /** A reachability transition for one declared port of one app. */
177
310
  | {
178
311
  type: "reachability";
312
+ app: string;
179
313
  port: number;
180
314
  state: ReachabilityState;
315
+ } | RunOutcomeEvent
316
+ /** An app's declared port set changed on reload and the runner re-patched the
317
+ * Service and Ingress. Without this the app binds the new port inside the pod
318
+ * and is simply unreachable: no ingress, no error, no event. */
319
+ | {
320
+ type: "endpoints";
321
+ app: string;
322
+ added?: RunnerEndpoint[];
323
+ removed?: RunnerEndpoint[];
324
+ /** A declared port that could not be routed (it collides with another app
325
+ * in this session). Reported, never dropped. */
326
+ rejected?: Array<{
327
+ port: number;
328
+ reason: string;
329
+ }>;
181
330
  };
331
+ /**
332
+ * Terminal = the session is over and the registry may schedule eviction.
333
+ * `suspended` is deliberately NOT terminal: the session record and its workspace
334
+ * checkpoint outlive the pod, and `resume` brings it back under the same id.
335
+ */
182
336
  export declare function isTerminal(status: RunStatus): boolean;
337
+ /**
338
+ * Which stream a byte-channel chunk came from. Under `io: "tty"` every chunk is
339
+ * `tty` — the tag never asserts a split that does not exist, which is exactly the
340
+ * failure the deleted `stdout` / `stderr` run events represented.
341
+ */
342
+ export type ByteStreamTag = "tty" | "stdout" | "stderr";
343
+ /** One file in a workspace snapshot: its path and the sha256 of its bytes.
344
+ * Hashing content — rather than comparing sizes or timestamps — is what makes
345
+ * two snapshots diff into an exact change set. */
346
+ export interface WorkspaceFileEntry {
347
+ path: string;
348
+ hash: string;
349
+ }
350
+ export interface WorkspaceTree {
351
+ files: WorkspaceFileEntry[];
352
+ }
353
+ /**
354
+ * An explicit write/delete list rather than a whole-tree PUT: a deletion has to
355
+ * be expressible, and a whole-tree PUT can only express it by treating absence as
356
+ * intent. A one-file save is a change set of one — there is deliberately no
357
+ * single-file write route, which would be a second set of concurrency rules.
358
+ */
359
+ export interface WorkspaceChangeSet {
360
+ write?: Array<{
361
+ path: string;
362
+ content: string;
363
+ encoding?: "utf8" | "base64";
364
+ }>;
365
+ delete?: string[];
366
+ }
367
+ export interface WorkspaceApplyResult {
368
+ written: number;
369
+ deleted: number;
370
+ }
371
+ /** One file of a checkpoint — a tree entry plus the bytes needed to re-seed a
372
+ * fresh pod. Text stays `utf8`; anything that is not valid text is carried
373
+ * `base64`, so a binary asset survives a suspend/resume round trip. */
374
+ export interface WorkspaceCheckpointFile extends WorkspaceFileEntry {
375
+ content: string;
376
+ encoding: "utf8" | "base64";
377
+ }
183
378
  /**
184
379
  * Stages a session start can fail at. The docker/k8s backends share the
185
380
  * vocabulary; not every stage applies to every backend (`pull`/`inspect` are
@@ -1 +1 @@
1
- {"version":3,"file":"contract.d.ts","sourceRoot":"","sources":["../src/contract.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"contract.d.ts","sourceRoot":"","sources":["../src/contract.ts"],"names":[],"mappings":"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"}
package/dist/contract.js CHANGED
@@ -6,6 +6,15 @@
6
6
  */
7
7
  /** HTTP header carrying the accepted terms version on `POST /v1/sessions`. */
8
8
  export const ACCEPTED_TERMS_HEADER = "x-telo-accepted-terms";
9
+ /** The name a single-app session's one application takes when the request
10
+ * declares no `apps` — so every `run` / `debug` / `endpoints` event names an
11
+ * app, and no client needs two readings of the same stream. */
12
+ export const DEFAULT_APP_NAME = "app";
13
+ /**
14
+ * Terminal = the session is over and the registry may schedule eviction.
15
+ * `suspended` is deliberately NOT terminal: the session record and its workspace
16
+ * checkpoint outlive the pod, and `resume` brings it back under the same id.
17
+ */
9
18
  export function isTerminal(status) {
10
19
  return status.kind === "exited" || status.kind === "failed" || status.kind === "stopped";
11
20
  }
@@ -1 +1 @@
1
- {"version":3,"file":"contract.js","sourceRoot":"","sources":["../src/contract.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAwCH,8EAA8E;AAC9E,MAAM,CAAC,MAAM,qBAAqB,GAAG,uBAAuB,CAAC;AAuH7D,MAAM,UAAU,UAAU,CAAC,MAAiB;IAC1C,OAAO,MAAM,CAAC,IAAI,KAAK,QAAQ,IAAI,MAAM,CAAC,IAAI,KAAK,QAAQ,IAAI,MAAM,CAAC,IAAI,KAAK,SAAS,CAAC;AAC3F,CAAC;AAeD,MAAM,OAAO,iBAAkB,SAAQ,KAAK;IAExB;IACA;IAEA;IAJlB,YACkB,IAAoC,EACpC,KAAwB,EACxC,OAAe,EACC,aAAsB;QAEtC,KAAK,CAAC,OAAO,CAAC,CAAC;QALC,SAAI,GAAJ,IAAI,CAAgC;QACpC,UAAK,GAAL,KAAK,CAAmB;QAExB,kBAAa,GAAb,aAAa,CAAS;QAGtC,IAAI,CAAC,IAAI,GAAG,mBAAmB,CAAC;IAClC,CAAC;CACF"}
1
+ {"version":3,"file":"contract.js","sourceRoot":"","sources":["../src/contract.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAwCH,8EAA8E;AAC9E,MAAM,CAAC,MAAM,qBAAqB,GAAG,uBAAuB,CAAC;AA8G7D;;gEAEgE;AAChE,MAAM,CAAC,MAAM,gBAAgB,GAAG,KAAK,CAAC;AA2JtC;;;;GAIG;AACH,MAAM,UAAU,UAAU,CAAC,MAAiB;IAC1C,OAAO,MAAM,CAAC,IAAI,KAAK,QAAQ,IAAI,MAAM,CAAC,IAAI,KAAK,QAAQ,IAAI,MAAM,CAAC,IAAI,KAAK,SAAS,CAAC;AAC3F,CAAC;AA0DD,MAAM,OAAO,iBAAkB,SAAQ,KAAK;IAExB;IACA;IAEA;IAJlB,YACkB,IAAoC,EACpC,KAAwB,EACxC,OAAe,EACC,aAAsB;QAEtC,KAAK,CAAC,OAAO,CAAC,CAAC;QALC,SAAI,GAAJ,IAAI,CAAgC;QACpC,UAAK,GAAL,KAAK,CAAmB;QAExB,kBAAa,GAAb,aAAa,CAAS;QAGtC,IAAI,CAAC,IAAI,GAAG,mBAAmB,CAAC;IAClC,CAAC;CACF"}
@@ -0,0 +1,26 @@
1
+ import { type DebugFrame } from "@telorun/debug-wire";
2
+ import type { PortMapping } from "../contract.js";
3
+ /** The kernel says this once per load: the Application's `ports:` block has
4
+ * resolved to these integers. It re-fires on every watch reload, which is what
5
+ * lets a host re-route without parsing a manifest. */
6
+ export declare const PORTS_RESOLVED_EVENT = "Kernel.PortsResolved";
7
+ /**
8
+ * The declared port set carried by a `Kernel.PortsResolved` frame, or undefined
9
+ * for any other frame.
10
+ *
11
+ * DECLARED, not bound. A runner routes what the manifest asked for; whether
12
+ * anything actually bound it is the reachability watcher's question, and it
13
+ * already answers it per port. Routing what happened to be bound instead would
14
+ * expose a port the manifest never declared, and would rest on a per-module
15
+ * convention — a transport whose kind does not emit a listening event would
16
+ * silently get no routing at all.
17
+ *
18
+ * Tolerant of a payload that is not the expected shape: a runner reads this off
19
+ * a stream produced by a kernel it does not version together with, so a frame it
20
+ * cannot make sense of is one to ignore, not to fail on.
21
+ */
22
+ export declare function portsResolvedFrom(frame: DebugFrame): PortMapping[] | undefined;
23
+ /** Identity of a port within a session: the pair, since tcp/3000 and udp/3000
24
+ * are different endpoints. */
25
+ export declare function portKey(mapping: PortMapping): string;
26
+ //# sourceMappingURL=ports-resolved.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ports-resolved.d.ts","sourceRoot":"","sources":["../../src/debug/ports-resolved.ts"],"names":[],"mappings":"AAAA,OAAO,EAAgB,KAAK,UAAU,EAAE,MAAM,qBAAqB,CAAC;AAEpE,OAAO,KAAK,EAAE,WAAW,EAAgB,MAAM,gBAAgB,CAAC;AAEhE;;uDAEuD;AACvD,eAAO,MAAM,oBAAoB,yBAAyB,CAAC;AAE3D;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,UAAU,GAAG,WAAW,EAAE,GAAG,SAAS,CAY9E;AAED;+BAC+B;AAC/B,wBAAgB,OAAO,CAAC,OAAO,EAAE,WAAW,GAAG,MAAM,CAEpD"}
@@ -0,0 +1,42 @@
1
+ import { isEventFrame } from "@telorun/debug-wire";
2
+ /** The kernel says this once per load: the Application's `ports:` block has
3
+ * resolved to these integers. It re-fires on every watch reload, which is what
4
+ * lets a host re-route without parsing a manifest. */
5
+ export const PORTS_RESOLVED_EVENT = "Kernel.PortsResolved";
6
+ /**
7
+ * The declared port set carried by a `Kernel.PortsResolved` frame, or undefined
8
+ * for any other frame.
9
+ *
10
+ * DECLARED, not bound. A runner routes what the manifest asked for; whether
11
+ * anything actually bound it is the reachability watcher's question, and it
12
+ * already answers it per port. Routing what happened to be bound instead would
13
+ * expose a port the manifest never declared, and would rest on a per-module
14
+ * convention — a transport whose kind does not emit a listening event would
15
+ * silently get no routing at all.
16
+ *
17
+ * Tolerant of a payload that is not the expected shape: a runner reads this off
18
+ * a stream produced by a kernel it does not version together with, so a frame it
19
+ * cannot make sense of is one to ignore, not to fail on.
20
+ */
21
+ export function portsResolvedFrom(frame) {
22
+ if (!isEventFrame(frame) || frame.event !== PORTS_RESOLVED_EVENT)
23
+ return undefined;
24
+ const raw = frame.payload?.ports;
25
+ if (!Array.isArray(raw))
26
+ return undefined;
27
+ const ports = [];
28
+ for (const entry of raw) {
29
+ const port = entry?.port;
30
+ const protocol = entry?.protocol;
31
+ if (typeof port !== "number" || !Number.isInteger(port))
32
+ continue;
33
+ ports.push({ port, protocol: protocol === "udp" ? "udp" : "tcp" });
34
+ }
35
+ return ports;
36
+ }
37
+ /** Identity of a port within a session: the pair, since tcp/3000 and udp/3000
38
+ * are different endpoints. */
39
+ export function portKey(mapping) {
40
+ return `${mapping.protocol}/${mapping.port}`;
41
+ }
42
+ //# sourceMappingURL=ports-resolved.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ports-resolved.js","sourceRoot":"","sources":["../../src/debug/ports-resolved.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAmB,MAAM,qBAAqB,CAAC;AAIpE;;uDAEuD;AACvD,MAAM,CAAC,MAAM,oBAAoB,GAAG,sBAAsB,CAAC;AAE3D;;;;;;;;;;;;;;GAcG;AACH,MAAM,UAAU,iBAAiB,CAAC,KAAiB;IACjD,IAAI,CAAC,YAAY,CAAC,KAAK,CAAC,IAAI,KAAK,CAAC,KAAK,KAAK,oBAAoB;QAAE,OAAO,SAAS,CAAC;IACnF,MAAM,GAAG,GAAI,KAAK,CAAC,OAA2C,EAAE,KAAK,CAAC;IACtE,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC;QAAE,OAAO,SAAS,CAAC;IAC1C,MAAM,KAAK,GAAkB,EAAE,CAAC;IAChC,KAAK,MAAM,KAAK,IAAI,GAAG,EAAE,CAAC;QACxB,MAAM,IAAI,GAAI,KAA4B,EAAE,IAAI,CAAC;QACjD,MAAM,QAAQ,GAAI,KAAgC,EAAE,QAAQ,CAAC;QAC7D,IAAI,OAAO,IAAI,KAAK,QAAQ,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,IAAI,CAAC;YAAE,SAAS;QAClE,KAAK,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,QAAQ,KAAK,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAE,KAAsB,EAAE,CAAC,CAAC;IACvF,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC;AAED;+BAC+B;AAC/B,MAAM,UAAU,OAAO,CAAC,OAAoB;IAC1C,OAAO,GAAG,OAAO,CAAC,QAAQ,IAAI,OAAO,CAAC,IAAI,EAAE,CAAC;AAC/C,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"relay.d.ts","sourceRoot":"","sources":["../../src/debug/relay.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,qBAAqB,CAAC;AAItD,MAAM,WAAW,iBAAiB;IAChC;2EACuE;IACvE,GAAG,EAAE,MAAM,CAAC;IACZ,kDAAkD;IAClD,OAAO,EAAE,CAAC,KAAK,EAAE,UAAU,KAAK,IAAI,CAAC;IACrC,wCAAwC;IACxC,MAAM,EAAE,WAAW,CAAC;IACpB;;qDAEiD;IACjD,OAAO,CAAC,EAAE,CAAC,GAAG,EAAE,KAAK,KAAK,IAAI,CAAC;CAChC;AAID;;;;;;;;GAQG;AACH,wBAAsB,gBAAgB,CAAC,IAAI,EAAE,iBAAiB,GAAG,OAAO,CAAC,IAAI,CAAC,CAmB7E"}
1
+ {"version":3,"file":"relay.d.ts","sourceRoot":"","sources":["../../src/debug/relay.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,qBAAqB,CAAC;AAItD,MAAM,WAAW,iBAAiB;IAChC;2EACuE;IACvE,GAAG,EAAE,MAAM,CAAC;IACZ,kDAAkD;IAClD,OAAO,EAAE,CAAC,KAAK,EAAE,UAAU,KAAK,IAAI,CAAC;IACrC,wCAAwC;IACxC,MAAM,EAAE,WAAW,CAAC;IACpB;;qDAEiD;IACjD,OAAO,CAAC,EAAE,CAAC,GAAG,EAAE,KAAK,KAAK,IAAI,CAAC;CAChC;AAID;;;;;;;;GAQG;AACH,wBAAsB,gBAAgB,CAAC,IAAI,EAAE,iBAAiB,GAAG,OAAO,CAAC,IAAI,CAAC,CA6B7E"}
@@ -11,17 +11,27 @@ const RECONNECT_DELAY_MS = 500;
11
11
  */
12
12
  export async function relayDebugStream(opts) {
13
13
  const { url, onFrame, signal } = opts;
14
+ // The last id delivered, carried across reconnects. Without it a drop replays
15
+ // the producer's whole buffer, and a consumer that DERIVES state from the
16
+ // stream — the run projection counting generations — re-counts every reload
17
+ // still in that buffer. In a watch session that is one pair per save.
18
+ let lastEventId = 0;
14
19
  while (!signal.aborted) {
15
20
  try {
16
21
  const res = await fetch(url, {
17
22
  signal,
18
- headers: { accept: "text/event-stream" },
23
+ headers: {
24
+ accept: "text/event-stream",
25
+ ...(lastEventId > 0 ? { "last-event-id": String(lastEventId) } : {}),
26
+ },
19
27
  });
20
28
  if (!res.ok || !res.body) {
21
29
  await abortableDelay(RECONNECT_DELAY_MS, signal);
22
30
  continue;
23
31
  }
24
- await pump(res.body, onFrame, signal);
32
+ await pump(res.body, onFrame, signal, (id) => {
33
+ lastEventId = id;
34
+ });
25
35
  }
26
36
  catch (err) {
27
37
  if (signal.aborted)
@@ -31,7 +41,7 @@ export async function relayDebugStream(opts) {
31
41
  await abortableDelay(RECONNECT_DELAY_MS, signal);
32
42
  }
33
43
  }
34
- async function pump(body, onFrame, signal) {
44
+ async function pump(body, onFrame, signal, onId) {
35
45
  const reader = body.getReader();
36
46
  const decoder = new TextDecoder();
37
47
  let buf = "";
@@ -44,10 +54,17 @@ async function pump(body, onFrame, signal) {
44
54
  let sep;
45
55
  // SSE frames are separated by a blank line.
46
56
  while ((sep = buf.indexOf("\n\n")) >= 0) {
47
- const frame = parseSseData(buf.slice(0, sep));
57
+ const block = buf.slice(0, sep);
48
58
  buf = buf.slice(sep + 2);
49
- if (frame)
50
- onFrame(frame);
59
+ const frame = parseSseData(block);
60
+ if (!frame)
61
+ continue;
62
+ onFrame(frame);
63
+ // Checkpoint AFTER delivery, so a frame the consumer never saw is not
64
+ // skipped on the next reconnect.
65
+ const id = parseSseId(block);
66
+ if (id !== null)
67
+ onId(id);
51
68
  }
52
69
  }
53
70
  }
@@ -55,6 +72,17 @@ async function pump(body, onFrame, signal) {
55
72
  reader.cancel().catch(() => undefined);
56
73
  }
57
74
  }
75
+ /** The `id:` of one SSE frame, or null when the producer sent none — an older
76
+ * kernel, whose stream simply has no resume point. */
77
+ function parseSseId(block) {
78
+ for (const line of block.split("\n")) {
79
+ if (!line.startsWith("id:"))
80
+ continue;
81
+ const parsed = Number.parseInt(line.slice(3).trim(), 10);
82
+ return Number.isFinite(parsed) ? parsed : null;
83
+ }
84
+ return null;
85
+ }
58
86
  /** Extract and parse the `data:` payload of one SSE frame. Ignores comment
59
87
  * lines (`: heartbeat`) and unparseable frames. */
60
88
  function parseSseData(block) {
@@ -1 +1 @@
1
- {"version":3,"file":"relay.js","sourceRoot":"","sources":["../../src/debug/relay.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"relay.js","sourceRoot":"","sources":["../../src/debug/relay.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,53 @@
1
+ import { type DebugFrame } from "@telorun/debug-wire";
2
+ import type { RunTrigger } from "../contract.js";
3
+ import type { SessionRegistry } from "../session/registry.js";
4
+ /**
5
+ * Projects per-app `run` events out of the kernel lifecycle events the debug
6
+ * stream already carries.
7
+ *
8
+ * The runner cannot see inside the container, and parsing the merged PTY stream
9
+ * is not a contract — so a watch session always runs with the kernel debug
10
+ * stream on and this reads it. A watch reload is a stop/start pair on ONE debug
11
+ * connection, so `generation` is counted here and the kernel is asked for
12
+ * nothing.
13
+ *
14
+ * Three event names are read, and only three:
15
+ * - `Kernel.Starting` → the generation began
16
+ * - `Kernel.Stopped` → it ended, carrying `exitCode`
17
+ * - `Kernel.RunFailed` → it never reached a running state, carrying why
18
+ *
19
+ * `Kernel.Started` is deliberately ignored: it and `Kernel.Starting` bracket the
20
+ * same transition, and one `started` per generation is the contract.
21
+ */
22
+ export declare class RunProjection {
23
+ private readonly registry;
24
+ private readonly sessionId;
25
+ /** What to attribute the NEXT generation of each app to. A reload is a watch
26
+ * reload unless something told us otherwise, so `watch` is the resting value
27
+ * and `initial` / `manual` / `resume` are set by whoever caused them. */
28
+ private readonly triggers;
29
+ constructor(registry: SessionRegistry, sessionId: string);
30
+ /** Attribute an app's next generation. Called with `initial` when the session
31
+ * comes up, `manual` from the reload route, `resume` after a pod recreate. */
32
+ expect(app: string, trigger: RunTrigger): void;
33
+ /** Attribute every app's next generation — the session-wide cases (initial,
34
+ * resume) always move all of them together. */
35
+ expectAll(trigger: RunTrigger): void;
36
+ /** Feed one frame from `app`'s debug stream. Frames that are not one of the
37
+ * three lifecycle events pass through untouched. */
38
+ frame(app: string, frame: DebugFrame): void;
39
+ /**
40
+ * Close the open generation because its WORKLOAD ended, not because the kernel
41
+ * said so. A container that goes away under `--watch` emits no `Kernel.Stopped`
42
+ * — there is nothing left to emit it — so the backend reports the ending and
43
+ * this turns it into the same run outcome the kernel path produces.
44
+ */
45
+ endGeneration(app: string, outcome: {
46
+ code?: number;
47
+ reason?: string;
48
+ }): void;
49
+ /** Open a generation unless one is already open — `Kernel.Starting` followed
50
+ * by a `Kernel.RunFailed` from the boot phase must not count twice. */
51
+ private begin;
52
+ }
53
+ //# sourceMappingURL=run-projection.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"run-projection.d.ts","sourceRoot":"","sources":["../../src/debug/run-projection.ts"],"names":[],"mappings":"AAAA,OAAO,EAAgB,KAAK,UAAU,EAAE,MAAM,qBAAqB,CAAC;AAEpE,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;AACjD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AAE9D;;;;;;;;;;;;;;;;;GAiBG;AACH,qBAAa,aAAa;IAOtB,OAAO,CAAC,QAAQ,CAAC,QAAQ;IACzB,OAAO,CAAC,QAAQ,CAAC,SAAS;IAP5B;;8EAE0E;IAC1E,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAiC;gBAGvC,QAAQ,EAAE,eAAe,EACzB,SAAS,EAAE,MAAM;IAGpC;mFAC+E;IAC/E,MAAM,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE,UAAU,GAAG,IAAI;IAI9C;oDACgD;IAChD,SAAS,CAAC,OAAO,EAAE,UAAU,GAAG,IAAI;IAKpC;yDACqD;IACrD,KAAK,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,UAAU,GAAG,IAAI;IA4B3C;;;;;OAKG;IACH,aAAa,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE;QAAE,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI;IAY7E;4EACwE;IACxE,OAAO,CAAC,KAAK;CASd"}