@telorun/runner-core 0.8.1 → 0.9.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 (97) 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 +27 -0
  5. package/dist/config.d.ts.map +1 -1
  6. package/dist/config.js +12 -0
  7. package/dist/config.js.map +1 -1
  8. package/dist/contract.d.ts +199 -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 +12 -12
  35. package/dist/routes/session-start.d.ts.map +1 -1
  36. package/dist/routes/session-start.js +86 -13
  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 +429 -4
  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 +5 -4
  73. package/src/backend.ts +111 -22
  74. package/src/config.ts +59 -0
  75. package/src/contract.ts +197 -15
  76. package/src/debug/ports-resolved.test.ts +66 -0
  77. package/src/debug/ports-resolved.ts +43 -0
  78. package/src/debug/relay.ts +32 -4
  79. package/src/debug/run-projection.test.ts +155 -0
  80. package/src/debug/run-projection.ts +122 -0
  81. package/src/index.ts +20 -1
  82. package/src/routes/apps.ts +14 -5
  83. package/src/routes/io.ts +58 -12
  84. package/src/routes/session-start.ts +106 -27
  85. package/src/routes/sessions.ts +498 -7
  86. package/src/server.ts +22 -1
  87. package/src/session/byte-ring-buffer.ts +8 -2
  88. package/src/session/registry.ts +288 -28
  89. package/src/session/ring-buffer.test.ts +4 -3
  90. package/src/session/watch-lifetime.test.ts +203 -0
  91. package/src/session/watch-supervisor.ts +112 -0
  92. package/src/session/workspace-app.ts +27 -0
  93. package/src/session/workspace-client.ts +80 -0
  94. package/src/session/workspace-marker.test.ts +35 -0
  95. package/src/session/workspace-marker.ts +39 -0
  96. package/src/sse/channel.ts +5 -0
  97. package/workspace-app/telo.yaml +228 -0
@@ -0,0 +1,99 @@
1
+ /** How often the supervisor wakes. Both of its jobs are coarse — a checkpoint
2
+ * cadence and an idle window, both measured in minutes — so a fine tick would
3
+ * buy nothing but wakeups. */
4
+ const TICK_MS = 5_000;
5
+ /**
6
+ * The two background jobs that make a watch session affordable, and they only
7
+ * work as a pair: a periodic workspace checkpoint, and an idle reap that deletes
8
+ * the pod while keeping the session record and that checkpoint.
9
+ *
10
+ * Aggressive reaping is what makes per-visitor watch sessions affordable, and
11
+ * the checkpoint is what makes aggressive reaping safe to do.
12
+ *
13
+ * The checkpoint is a CACHE, never the only copy. The runner is a single replica
14
+ * with an in-memory registry, so a redeploy or crash drops every suspended
15
+ * session — the editor holds the authoritative workspace, already diffs its own
16
+ * files against `GET /workspace`, and on a `404` at resume creates a new session
17
+ * and re-seeds from its own copy in one change set. What the checkpoint saves is
18
+ * that upload.
19
+ */
20
+ export class WatchSupervisor {
21
+ deps;
22
+ timer = null;
23
+ /** Sessions with a checkpoint or suspend in flight, so a slow snapshot cannot
24
+ * have a second one started on top of it every tick. */
25
+ busy = new Set();
26
+ constructor(deps) {
27
+ this.deps = deps;
28
+ }
29
+ start() {
30
+ if (this.timer)
31
+ return;
32
+ this.timer = setInterval(() => void this.tick(), TICK_MS);
33
+ this.timer.unref?.();
34
+ }
35
+ stop() {
36
+ if (!this.timer)
37
+ return;
38
+ clearInterval(this.timer);
39
+ this.timer = null;
40
+ }
41
+ /** Exposed for tests, which drive the clock rather than waiting on it. */
42
+ async tick() {
43
+ const now = Date.now();
44
+ for (const entry of this.deps.registry.list()) {
45
+ if (entry.mode !== "watch")
46
+ continue;
47
+ if (entry.status.kind !== "running")
48
+ continue;
49
+ if (this.busy.has(entry.sessionId))
50
+ continue;
51
+ const idle = entry.subscribers === 0 &&
52
+ entry.idleSince !== null &&
53
+ now - entry.idleSince >= this.deps.idleMs;
54
+ if (idle) {
55
+ void this.run(entry, () => this.suspend(entry));
56
+ }
57
+ else if (entry.checkpointedAt === null ||
58
+ now - entry.checkpointedAt >= this.deps.checkpointMs) {
59
+ void this.run(entry, () => this.checkpoint(entry));
60
+ }
61
+ }
62
+ }
63
+ async run(entry, job) {
64
+ this.busy.add(entry.sessionId);
65
+ try {
66
+ await job();
67
+ }
68
+ catch (err) {
69
+ // A failed checkpoint is not a failed session: the editor still holds the
70
+ // authoritative copy, and the next tick retries. Reported rather than
71
+ // swallowed so an operator sees a workspace that has stopped answering.
72
+ this.deps.log?.warn({ err, sessionId: entry.sessionId }, "watch session checkpoint/suspend failed");
73
+ }
74
+ finally {
75
+ this.busy.delete(entry.sessionId);
76
+ }
77
+ }
78
+ async checkpoint(entry) {
79
+ const workspace = entry.session?.workspace;
80
+ if (!workspace)
81
+ return;
82
+ const files = await workspace.snapshot();
83
+ entry.checkpoint = { takenAt: new Date(), files };
84
+ entry.checkpointedAt = Date.now();
85
+ }
86
+ async suspend(entry) {
87
+ const session = entry.session;
88
+ if (!session?.suspend)
89
+ return;
90
+ // Snapshot BEFORE the pod goes: the volume dies with it. A snapshot that
91
+ // fails aborts the suspend rather than taking the pod down with an older
92
+ // checkpoint — the session stays up and the next tick tries again.
93
+ await this.checkpoint(entry);
94
+ await session.suspend();
95
+ entry.session = null;
96
+ this.deps.registry.emit(entry.sessionId, { type: "status", status: { kind: "suspended" } });
97
+ }
98
+ }
99
+ //# sourceMappingURL=watch-supervisor.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"watch-supervisor.js","sourceRoot":"","sources":["../../src/session/watch-supervisor.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,4 @@
1
+ /** The name the manifest is mounted under, in every backend. */
2
+ export declare const WORKSPACE_APP_FILENAME = "telo.yaml";
3
+ export declare function workspaceAppManifest(): string;
4
+ //# sourceMappingURL=workspace-app.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"workspace-app.d.ts","sourceRoot":"","sources":["../../src/session/workspace-app.ts"],"names":[],"mappings":"AAIA,gEAAgE;AAChE,eAAO,MAAM,sBAAsB,cAAc,CAAC;AAclD,wBAAgB,oBAAoB,IAAI,MAAM,CAO7C"}
@@ -0,0 +1,26 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { dirname, join } from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ /** The name the manifest is mounted under, in every backend. */
5
+ export const WORKSPACE_APP_FILENAME = "telo.yaml";
6
+ /**
7
+ * The workspace application's manifest, read from the package rather than
8
+ * inlined as a string: it is a Telo manifest, so it must stay a `.yaml` file the
9
+ * repo's own `telo check` and formatter see. It lives in runner-core because the
10
+ * workspace surface is part of the `/v1` session contract — both backends mount
11
+ * the same bytes, and a copy per backend would be two manifests to hold in
12
+ * agreement.
13
+ *
14
+ * Read once and memoized: it never changes within a process.
15
+ */
16
+ let cached;
17
+ export function workspaceAppManifest() {
18
+ if (cached !== undefined)
19
+ return cached;
20
+ // `dist/session/` at runtime, `src/session/` under a source run — three levels
21
+ // up is the package root either way.
22
+ const here = dirname(fileURLToPath(import.meta.url));
23
+ cached = readFileSync(join(here, "..", "..", "workspace-app", WORKSPACE_APP_FILENAME), "utf8");
24
+ return cached;
25
+ }
26
+ //# sourceMappingURL=workspace-app.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"workspace-app.js","sourceRoot":"","sources":["../../src/session/workspace-app.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AACvC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAC1C,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAEzC,gEAAgE;AAChE,MAAM,CAAC,MAAM,sBAAsB,GAAG,WAAW,CAAC;AAElD;;;;;;;;;GASG;AACH,IAAI,MAA0B,CAAC;AAE/B,MAAM,UAAU,oBAAoB;IAClC,IAAI,MAAM,KAAK,SAAS;QAAE,OAAO,MAAM,CAAC;IACxC,+EAA+E;IAC/E,qCAAqC;IACrC,MAAM,IAAI,GAAG,OAAO,CAAC,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;IACrD,MAAM,GAAG,YAAY,CAAC,IAAI,CAAC,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,eAAe,EAAE,sBAAsB,CAAC,EAAE,MAAM,CAAC,CAAC;IAC/F,OAAO,MAAM,CAAC;AAChB,CAAC"}
@@ -0,0 +1,33 @@
1
+ import type { WorkspaceAccess } from "../backend.js";
2
+ import type { WorkspaceChangeSet, WorkspaceCheckpointFile, WorkspaceTree } from "../contract.js";
3
+ /**
4
+ * HTTP client for the `workspace` container's surface, shared by both backends:
5
+ * the container is the same image running the same manifest whether it is a
6
+ * sibling container on a docker network or a container in a kubernetes pod, so
7
+ * only the base URL differs.
8
+ *
9
+ * The runner reaches it over its own network (a pod IP, a container address) and
10
+ * proxies it outward. It is never published.
11
+ */
12
+ export declare class WorkspaceClient implements WorkspaceAccess {
13
+ private readonly baseUrl;
14
+ private readonly timeoutMs;
15
+ constructor(baseUrl: string, timeoutMs?: number);
16
+ tree(): Promise<WorkspaceTree>;
17
+ readFile(path: string): Promise<{
18
+ content: string;
19
+ size: number;
20
+ }>;
21
+ apply(changes: WorkspaceChangeSet): Promise<{
22
+ written: number;
23
+ deleted: number;
24
+ }>;
25
+ snapshot(): Promise<WorkspaceCheckpointFile[]>;
26
+ /** Re-run one app with no file change: rewrite its entry manifest with the
27
+ * bytes it already holds, through the same write path everything else takes.
28
+ * That is what makes `reload` need no signalling into the container, no
29
+ * shared PID namespace and no `exec` — RBAC is unchanged. */
30
+ touch(path: string): Promise<void>;
31
+ private request;
32
+ }
33
+ //# sourceMappingURL=workspace-client.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"workspace-client.d.ts","sourceRoot":"","sources":["../../src/session/workspace-client.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AACrD,OAAO,KAAK,EACV,kBAAkB,EAClB,uBAAuB,EACvB,aAAa,EACd,MAAM,gBAAgB,CAAC;AAExB;;;;;;;;GAQG;AACH,qBAAa,eAAgB,YAAW,eAAe;IAEnD,OAAO,CAAC,QAAQ,CAAC,OAAO;IACxB,OAAO,CAAC,QAAQ,CAAC,SAAS;gBADT,OAAO,EAAE,MAAM,EACf,SAAS,SAAS;IAG/B,IAAI,IAAI,OAAO,CAAC,aAAa,CAAC;IAI9B,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC;IAOlE,KAAK,CAAC,OAAO,EAAE,kBAAkB,GAAG,OAAO,CAAC;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC;IAIjF,QAAQ,IAAI,OAAO,CAAC,uBAAuB,EAAE,CAAC;IAQpD;;;kEAG8D;IACxD,KAAK,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;YAQ1B,OAAO;CAsBtB"}
@@ -0,0 +1,62 @@
1
+ /**
2
+ * HTTP client for the `workspace` container's surface, shared by both backends:
3
+ * the container is the same image running the same manifest whether it is a
4
+ * sibling container on a docker network or a container in a kubernetes pod, so
5
+ * only the base URL differs.
6
+ *
7
+ * The runner reaches it over its own network (a pod IP, a container address) and
8
+ * proxies it outward. It is never published.
9
+ */
10
+ export class WorkspaceClient {
11
+ baseUrl;
12
+ timeoutMs;
13
+ constructor(baseUrl, timeoutMs = 30_000) {
14
+ this.baseUrl = baseUrl;
15
+ this.timeoutMs = timeoutMs;
16
+ }
17
+ async tree() {
18
+ return this.request("GET", "/workspace");
19
+ }
20
+ async readFile(path) {
21
+ return this.request("GET", `/workspace/file?path=${encodeURIComponent(path)}`);
22
+ }
23
+ async apply(changes) {
24
+ return this.request("POST", "/workspace", changes);
25
+ }
26
+ async snapshot() {
27
+ const body = await this.request("GET", "/workspace/snapshot");
28
+ return body.files;
29
+ }
30
+ /** Re-run one app with no file change: rewrite its entry manifest with the
31
+ * bytes it already holds, through the same write path everything else takes.
32
+ * That is what makes `reload` need no signalling into the container, no
33
+ * shared PID namespace and no `exec` — RBAC is unchanged. */
34
+ async touch(path) {
35
+ const file = await this.request("GET", `/workspace/file?path=${encodeURIComponent(path)}&encoding=base64`);
36
+ await this.apply({ write: [{ path, content: file.content, encoding: "base64" }] });
37
+ }
38
+ async request(method, path, body) {
39
+ const controller = new AbortController();
40
+ const timer = setTimeout(() => controller.abort(), this.timeoutMs);
41
+ try {
42
+ const response = await fetch(`${this.baseUrl}${path}`, {
43
+ method,
44
+ signal: controller.signal,
45
+ ...(body === undefined
46
+ ? {}
47
+ : { headers: { "content-type": "application/json" }, body: JSON.stringify(body) }),
48
+ });
49
+ const text = await response.text();
50
+ if (!response.ok) {
51
+ // Surfaced, never swallowed: a workspace that has stopped answering is
52
+ // the one failure that makes every later edit silently do nothing.
53
+ throw new Error(`workspace ${method} ${path} failed: ${response.status} ${text.slice(0, 500)}`);
54
+ }
55
+ return JSON.parse(text);
56
+ }
57
+ finally {
58
+ clearTimeout(timer);
59
+ }
60
+ }
61
+ }
62
+ //# sourceMappingURL=workspace-client.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"workspace-client.js","sourceRoot":"","sources":["../../src/session/workspace-client.ts"],"names":[],"mappings":"AAOA;;;;;;;;GAQG;AACH,MAAM,OAAO,eAAe;IAEP;IACA;IAFnB,YACmB,OAAe,EACf,YAAY,MAAM;QADlB,YAAO,GAAP,OAAO,CAAQ;QACf,cAAS,GAAT,SAAS,CAAS;IAClC,CAAC;IAEJ,KAAK,CAAC,IAAI;QACR,OAAO,IAAI,CAAC,OAAO,CAAgB,KAAK,EAAE,YAAY,CAAC,CAAC;IAC1D,CAAC;IAED,KAAK,CAAC,QAAQ,CAAC,IAAY;QACzB,OAAO,IAAI,CAAC,OAAO,CACjB,KAAK,EACL,wBAAwB,kBAAkB,CAAC,IAAI,CAAC,EAAE,CACnD,CAAC;IACJ,CAAC;IAED,KAAK,CAAC,KAAK,CAAC,OAA2B;QACrC,OAAO,IAAI,CAAC,OAAO,CAAuC,MAAM,EAAE,YAAY,EAAE,OAAO,CAAC,CAAC;IAC3F,CAAC;IAED,KAAK,CAAC,QAAQ;QACZ,MAAM,IAAI,GAAG,MAAM,IAAI,CAAC,OAAO,CAC7B,KAAK,EACL,qBAAqB,CACtB,CAAC;QACF,OAAO,IAAI,CAAC,KAAK,CAAC;IACpB,CAAC;IAED;;;kEAG8D;IAC9D,KAAK,CAAC,KAAK,CAAC,IAAY;QACtB,MAAM,IAAI,GAAG,MAAM,IAAI,CAAC,OAAO,CAC7B,KAAK,EACL,wBAAwB,kBAAkB,CAAC,IAAI,CAAC,kBAAkB,CACnE,CAAC;QACF,MAAM,IAAI,CAAC,KAAK,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,CAAC,OAAO,EAAE,QAAQ,EAAE,QAAQ,EAAE,CAAC,EAAE,CAAC,CAAC;IACrF,CAAC;IAEO,KAAK,CAAC,OAAO,CAAI,MAAc,EAAE,IAAY,EAAE,IAAc;QACnE,MAAM,UAAU,GAAG,IAAI,eAAe,EAAE,CAAC;QACzC,MAAM,KAAK,GAAG,UAAU,CAAC,GAAG,EAAE,CAAC,UAAU,CAAC,KAAK,EAAE,EAAE,IAAI,CAAC,SAAS,CAAC,CAAC;QACnE,IAAI,CAAC;YACH,MAAM,QAAQ,GAAG,MAAM,KAAK,CAAC,GAAG,IAAI,CAAC,OAAO,GAAG,IAAI,EAAE,EAAE;gBACrD,MAAM;gBACN,MAAM,EAAE,UAAU,CAAC,MAAM;gBACzB,GAAG,CAAC,IAAI,KAAK,SAAS;oBACpB,CAAC,CAAC,EAAE;oBACJ,CAAC,CAAC,EAAE,OAAO,EAAE,EAAE,cAAc,EAAE,kBAAkB,EAAE,EAAE,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,EAAE,CAAC;aACrF,CAAC,CAAC;YACH,MAAM,IAAI,GAAG,MAAM,QAAQ,CAAC,IAAI,EAAE,CAAC;YACnC,IAAI,CAAC,QAAQ,CAAC,EAAE,EAAE,CAAC;gBACjB,uEAAuE;gBACvE,mEAAmE;gBACnE,MAAM,IAAI,KAAK,CAAC,aAAa,MAAM,IAAI,IAAI,YAAY,QAAQ,CAAC,MAAM,IAAI,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,EAAE,CAAC,CAAC;YAClG,CAAC;YACD,OAAO,IAAI,CAAC,KAAK,CAAC,IAAI,CAAM,CAAC;QAC/B,CAAC;gBAAS,CAAC;YACT,YAAY,CAAC,KAAK,CAAC,CAAC;QACtB,CAAC;IACH,CAAC;CACF"}
@@ -0,0 +1,25 @@
1
+ import type { RunBundle, WorkspaceChangeSet } from "../contract.js";
2
+ /** The kernel anchors its `.telo` cache at the directory holding this file. */
3
+ export declare const WORKSPACE_MARKER_FILENAME = "telo-workspace.yaml";
4
+ /**
5
+ * The marker a session's workspace root carries.
6
+ *
7
+ * Its LOCATION is what matters here: the kernel walks up from an app's entry
8
+ * manifest looking for it and anchors the `.telo` cache at the directory that
9
+ * holds it. Without one, each app anchors on its OWN entry directory, so two
10
+ * apps in one workspace resolve the same module twice into two caches — and an
11
+ * app in a subdirectory gets a third.
12
+ *
13
+ * `modules:` is release scope and is read by `telo release` alone, which never
14
+ * runs in a session. It is written anyway because an empty list is a hard error
15
+ * in the parser, and a file this repo's own tooling would reject is not one to
16
+ * seed into a user's workspace.
17
+ */
18
+ export declare const WORKSPACE_MARKER_CONTENTS = "# Marks the root of this session's workspace.\n#\n# The Telo kernel anchors its module cache (.telo) at the directory holding this\n# file, so every application in this workspace resolves its imports once into one\n# cache instead of once per app.\nmodules: [\"*\"]\n";
19
+ /**
20
+ * The marker, unless the workspace already brings its own. A user whose project
21
+ * really is a Telo workspace has a marker with a real \`modules:\` list, and
22
+ * overwriting it would silently change what \`telo release\` discovers.
23
+ */
24
+ export declare function workspaceMarkerWrite(bundle: RunBundle): NonNullable<WorkspaceChangeSet["write"]>;
25
+ //# sourceMappingURL=workspace-marker.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"workspace-marker.d.ts","sourceRoot":"","sources":["../../src/session/workspace-marker.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,kBAAkB,EAAE,MAAM,gBAAgB,CAAC;AAEpE,+EAA+E;AAC/E,eAAO,MAAM,yBAAyB,wBAAwB,CAAC;AAE/D;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,yBAAyB,gRAMrC,CAAC;AAEF;;;;GAIG;AACH,wBAAgB,oBAAoB,CAClC,MAAM,EAAE,SAAS,GAChB,WAAW,CAAC,kBAAkB,CAAC,OAAO,CAAC,CAAC,CAI1C"}
@@ -0,0 +1,35 @@
1
+ /** The kernel anchors its `.telo` cache at the directory holding this file. */
2
+ export const WORKSPACE_MARKER_FILENAME = "telo-workspace.yaml";
3
+ /**
4
+ * The marker a session's workspace root carries.
5
+ *
6
+ * Its LOCATION is what matters here: the kernel walks up from an app's entry
7
+ * manifest looking for it and anchors the `.telo` cache at the directory that
8
+ * holds it. Without one, each app anchors on its OWN entry directory, so two
9
+ * apps in one workspace resolve the same module twice into two caches — and an
10
+ * app in a subdirectory gets a third.
11
+ *
12
+ * `modules:` is release scope and is read by `telo release` alone, which never
13
+ * runs in a session. It is written anyway because an empty list is a hard error
14
+ * in the parser, and a file this repo's own tooling would reject is not one to
15
+ * seed into a user's workspace.
16
+ */
17
+ export const WORKSPACE_MARKER_CONTENTS = `# Marks the root of this session's workspace.
18
+ #
19
+ # The Telo kernel anchors its module cache (.telo) at the directory holding this
20
+ # file, so every application in this workspace resolves its imports once into one
21
+ # cache instead of once per app.
22
+ modules: ["*"]
23
+ `;
24
+ /**
25
+ * The marker, unless the workspace already brings its own. A user whose project
26
+ * really is a Telo workspace has a marker with a real \`modules:\` list, and
27
+ * overwriting it would silently change what \`telo release\` discovers.
28
+ */
29
+ export function workspaceMarkerWrite(bundle) {
30
+ const provided = bundle.files.some((f) => f.relativePath === WORKSPACE_MARKER_FILENAME);
31
+ if (provided)
32
+ return [];
33
+ return [{ path: WORKSPACE_MARKER_FILENAME, content: WORKSPACE_MARKER_CONTENTS }];
34
+ }
35
+ //# sourceMappingURL=workspace-marker.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"workspace-marker.js","sourceRoot":"","sources":["../../src/session/workspace-marker.ts"],"names":[],"mappings":"AAEA,+EAA+E;AAC/E,MAAM,CAAC,MAAM,yBAAyB,GAAG,qBAAqB,CAAC;AAE/D;;;;;;;;;;;;;GAaG;AACH,MAAM,CAAC,MAAM,yBAAyB,GAAG;;;;;;CAMxC,CAAC;AAEF;;;;GAIG;AACH,MAAM,UAAU,oBAAoB,CAClC,MAAiB;IAEjB,MAAM,QAAQ,GAAG,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,YAAY,KAAK,yBAAyB,CAAC,CAAC;IACxF,IAAI,QAAQ;QAAE,OAAO,EAAE,CAAC;IACxB,OAAO,CAAC,EAAE,IAAI,EAAE,yBAAyB,EAAE,OAAO,EAAE,yBAAyB,EAAE,CAAC,CAAC;AACnF,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"channel.d.ts","sourceRoot":"","sources":["../../src/sse/channel.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AAG5D,OAAO,EAAuB,KAAK,eAAe,EAAE,MAAM,wBAAwB,CAAC;AAKnF,MAAM,WAAW,aAAa;IAC5B,QAAQ,EAAE,eAAe,CAAC;IAC1B,GAAG,EAAE,cAAc,CAAC;IACpB,KAAK,EAAE,YAAY,CAAC;IACpB,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,EAAE,MAAM,EAAE,GAAG,GAAG,CAAC;CAC7B;AAED,wBAAsB,mBAAmB,CAAC,IAAI,EAAE,aAAa,GAAG,OAAO,CAAC,IAAI,CAAC,CAuF5E"}
1
+ {"version":3,"file":"channel.d.ts","sourceRoot":"","sources":["../../src/sse/channel.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AAG5D,OAAO,EAAuB,KAAK,eAAe,EAAE,MAAM,wBAAwB,CAAC;AAKnF,MAAM,WAAW,aAAa;IAC5B,QAAQ,EAAE,eAAe,CAAC;IAC1B,GAAG,EAAE,cAAc,CAAC;IACpB,KAAK,EAAE,YAAY,CAAC;IACpB,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,EAAE,MAAM,EAAE,GAAG,GAAG,CAAC;CAC7B;AAED,wBAAsB,mBAAmB,CAAC,IAAI,EAAE,aAAa,GAAG,OAAO,CAAC,IAAI,CAAC,CA4F5E"}
@@ -44,10 +44,14 @@ export async function streamSessionEvents(args) {
44
44
  let unsubscribe = null;
45
45
  let heartbeat = null;
46
46
  let closed = false;
47
+ // An attached event stream is a live client. Idleness — no subscriber on
48
+ // either channel for the configured window — is what suspends a watch session.
49
+ const releaseSubscriber = registry.addSubscriber(sessionId);
47
50
  const cleanup = () => {
48
51
  if (closed)
49
52
  return;
50
53
  closed = true;
54
+ releaseSubscriber();
51
55
  if (heartbeat)
52
56
  clearInterval(heartbeat);
53
57
  if (unsubscribe)
@@ -1 +1 @@
1
- {"version":3,"file":"channel.js","sourceRoot":"","sources":["../../src/sse/channel.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"channel.js","sourceRoot":"","sources":["../../src/sse/channel.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@telorun/runner-core",
3
- "version": "0.8.1",
3
+ "version": "0.9.0",
4
4
  "description": "Backend-neutral core for Telo runners: the /v1 session contract, the RunnerBackend interface, the session registry, SSE/byte ring buffers, and bundle handling. Shared by docker-runner and k8s-runner.",
5
5
  "keywords": [
6
6
  "telo",
@@ -24,14 +24,15 @@
24
24
  },
25
25
  "files": [
26
26
  "dist/**",
27
- "src/**"
27
+ "src/**",
28
+ "workspace-app/**"
28
29
  ],
29
30
  "dependencies": {
30
31
  "@fastify/cors": "^11.0.1",
31
32
  "@fastify/websocket": "^11.0.1",
32
- "fastify": "^5.7.2",
33
+ "fastify": "^5.12.1",
33
34
  "yaml": "^2.8.3",
34
- "@telorun/debug-wire": "0.4.0"
35
+ "@telorun/debug-wire": "0.4.1"
35
36
  },
36
37
  "devDependencies": {
37
38
  "@types/node": "^20.0.0",
package/src/backend.ts CHANGED
@@ -1,14 +1,22 @@
1
1
  import type { DebugFrame } from "@telorun/debug-wire";
2
2
 
3
+ import type { ResolvedRunnerApp } from "./config.js";
3
4
  import type {
4
5
  AvailabilityReport,
6
+ ByteStreamTag,
7
+ IoMode,
5
8
  PortMapping,
6
9
  ProbeConfig,
7
10
  ReachabilityState,
8
11
  RunBundle,
12
+ RunnerEndpoint,
9
13
  RunPhase,
10
14
  RunStatus,
11
15
  SessionConfig,
16
+ SessionMode,
17
+ WorkspaceChangeSet,
18
+ WorkspaceCheckpointFile,
19
+ WorkspaceTree,
12
20
  } from "./contract.js";
13
21
 
14
22
  /**
@@ -40,59 +48,140 @@ export interface RunnerBackend {
40
48
  reapOrphans?(): Promise<void>;
41
49
  }
42
50
 
43
- export interface BackendStartSpec {
44
- sessionId: string;
45
- bundle: RunBundle;
46
- /** Bundle-relative entry path, already traversal-normalized by core. */
51
+ /** One application container the backend must stand up. */
52
+ export interface BackendAppSpec {
53
+ name: string;
54
+ /** Bundle-relative entry path, already traversal-normalized by core. Empty for
55
+ * a self-contained app session, where the image's own entrypoint runs. */
47
56
  entryRelativePath: string;
48
- env: Record<string, string>;
49
57
  ports: PortMapping[];
58
+ io: IoMode;
59
+ }
60
+
61
+ /**
62
+ * The DATA half of a workload launch — everything a start needs that is not a
63
+ * callback. Retained on the session entry so `resume` can build a fresh pod
64
+ * under the same session id from the same description, rather than a second,
65
+ * drifting copy of it.
66
+ */
67
+ export interface WorkloadLaunch {
68
+ bundle: RunBundle;
69
+ env: Record<string, string>;
50
70
  config: SessionConfig;
51
71
  /** True for an operator-predefined app session (`POST /v1/apps/:name/sessions`):
52
72
  * `config.image` is self-contained (app + controllers baked in), so the
53
73
  * backend runs the image's own entrypoint and stages no bundle — `bundle`
54
74
  * is an empty placeholder. */
55
75
  selfContained: boolean;
56
- /** When true, launch the workload with `--inspect` and relay its kernel debug
76
+ /** When true, launch each app with `--inspect` and relay its kernel debug
57
77
  * stream via `onDebug`. The inspect endpoint stays reachable only by the
58
- * runner — never published outward. */
78
+ * runner — never published outward. Always true for a watch session: that
79
+ * stream is where `run` events are projected from. */
59
80
  inspect: boolean;
81
+ /** `run` (one pod, terminal on exit, today's shape) or `watch` (a workspace
82
+ * volume, `telo run --watch` per app, the pod outliving its runs). */
83
+ mode: SessionMode;
84
+ /** The applications to run, one container each. Never empty — core defaults a
85
+ * request with no `apps` to a single entry. */
86
+ apps: BackendAppSpec[];
87
+ /** The resolved catalog entry for a co-resident agent container, when one was
88
+ * requested. Its operator env goes on THAT container and nowhere else — the
89
+ * credential boundary used to be structural (two pods) and is now a code
90
+ * invariant (containers in one pod). */
91
+ agent?: ResolvedRunnerApp;
92
+ }
93
+
94
+ export interface BackendStartSpec extends WorkloadLaunch {
95
+ sessionId: string;
60
96
 
61
97
  /** Emit a lifecycle status. The backend drives `starting` → `running` →
62
- * terminal (`exited`/`failed`/`stopped`). */
98
+ * terminal (`exited`/`failed`/`stopped`), or `suspended` on an idle reap. */
63
99
  onStatus(status: RunStatus): void;
64
100
  /** Emit a progress message for a coming-up phase (build / provision / boot).
65
101
  * Additive to status — surfaces what's happening while the session is still
66
- * `starting`, driving the editor's spinner + step feed. */
67
- onProgress(phase: RunPhase, message: string, done?: boolean): void;
68
- /** Raw merged stdout/stderr (PTY) bytes from the workload. */
69
- onOutput(chunk: Buffer): void;
70
- /** A frame relayed from the workload's kernel debug stream. Only called when
71
- * `inspect` is true and the backend has connected to the inspect endpoint. */
72
- onDebug(frame: DebugFrame): void;
102
+ * `starting`. `app` is omitted for session-scoped provisioning. */
103
+ onProgress(phase: RunPhase, message: string, done?: boolean, app?: string): void;
104
+ /** Bytes from one app's terminal. `stream` is `tty` under a terminal attach
105
+ * and `stdout`/`stderr` only where the transport genuinely separated them. */
106
+ onOutput(app: string, chunk: Buffer, stream: ByteStreamTag): void;
107
+ /** A frame relayed from one app's kernel debug stream. Only called when
108
+ * `inspect` is true and the backend has connected to that app's endpoint. */
109
+ onDebug(app: string, frame: DebugFrame): void;
73
110
  /** Report a declared port's reachability from the runner network — `checking`
74
111
  * while the workload comes up, then `reachable`, or `unreachable` after a
75
112
  * timeout. Surfaced on the editor's endpoint badge, not the log stream. */
76
- onReachability(port: number, state: ReachabilityState): void;
113
+ onReachability(app: string, port: number, state: ReachabilityState): void;
114
+ /** One app's workload ended on its own. A run session's workload ending IS the
115
+ * session ending, so this is a watch-session concern: under `--watch` a
116
+ * finished run leaves the container up, and a container that goes away has
117
+ * died. Reported through the contract rather than by synthesizing a kernel
118
+ * frame — that stream's contract is "frames relayed from the workload", and a
119
+ * backend writing into it puts an event on the wire no kernel emitted. */
120
+ onRunEnded(app: string, outcome: { code?: number; reason?: string }): void;
121
+ /** An app's routable endpoint set changed after a reload re-read its declared
122
+ * ports. */
123
+ onEndpoints(
124
+ app: string,
125
+ change: {
126
+ added?: RunnerEndpoint[];
127
+ removed?: RunnerEndpoint[];
128
+ rejected?: Array<{ port: number; reason: string }>;
129
+ },
130
+ ): void;
77
131
  /** True once a user stop / shutdown has been requested — lets the backend
78
132
  * classify a kill as `stopped` rather than `failed`. */
79
133
  isUserStopped(): boolean;
80
134
  }
81
135
 
82
136
  export interface BackendSession {
83
- /** Write bytes to the workload's stdin (PTY). A no-op once the workload has
84
- * terminated, so callers need not track liveness. */
85
- writeStdin(bytes: Uint8Array): void;
137
+ /** Write bytes to one app's stdin. A no-op once that workload has terminated,
138
+ * so callers need not track liveness. */
139
+ writeStdin(app: string, bytes: Uint8Array): void;
86
140
 
87
- /** Resize the workload's PTY. */
88
- resize(cols: number, rows: number): void;
141
+ /** Resize one app's PTY. A no-op for an app running under `io: "streams"` —
142
+ * the route rejects such a resize before it reaches here. */
143
+ resize(app: string, cols: number, rows: number): void;
89
144
 
90
145
  /** Resolves after the workload terminates and its terminal status has been
91
146
  * emitted via `onStatus`. Never rejects — terminal failures surface as a
92
- * `failed` status. */
147
+ * `failed` status. A watch session's workload outlives its runs, so this
148
+ * settles only on stop, suspend, or the pod deadline. */
93
149
  readonly done: Promise<void>;
94
150
 
95
151
  /** Force-stop the workload. Idempotent and safe to call after natural exit
96
152
  * (a backend should treat an already-gone workload as a no-op). */
97
153
  stop(): Promise<void>;
154
+
155
+ /** The workspace surface, present only on a watch session. Everything outside
156
+ * the pod writes through here; the agent (inside it) writes the volume with
157
+ * its own filesystem tools. */
158
+ readonly workspace?: WorkspaceAccess;
159
+
160
+ /** Re-run one app with no file change (`POST /v1/sessions/:id/reload`), by
161
+ * touching its entry manifest through the same path every other write takes.
162
+ * No signalling into the container, no shared PID namespace, no `exec` — RBAC
163
+ * is unchanged. Watch sessions only. */
164
+ reload?(app: string): Promise<void>;
165
+
166
+ /** Replace the running app set. A pod's container list is fixed at creation,
167
+ * so this checkpoints, deletes the pod and creates one with the new set —
168
+ * the suspend/resume path, reused because it has to be. */
169
+ setApps?(apps: BackendAppSpec[]): Promise<void>;
170
+
171
+ /** Delete the workload, keeping nothing but what the caller already
172
+ * checkpointed. `resume` builds a fresh one from that checkpoint. */
173
+ suspend?(): Promise<void>;
174
+ }
175
+
176
+ /** The `workspace` container's routes, proxied by the runner. Reading and
177
+ * applying a change set are the only two shapes: a single-file write route
178
+ * would be a second set of concurrency rules over the same directory. */
179
+ export interface WorkspaceAccess {
180
+ tree(): Promise<WorkspaceTree>;
181
+ readFile(path: string): Promise<{ content: string; size: number }>;
182
+ apply(changes: WorkspaceChangeSet): Promise<{ written: number; deleted: number }>;
183
+ /** Whole-tree pull for the checkpoint timer and for suspend. Whole tree rather
184
+ * than a delta log: a manifest workspace is small, and one shape is easier to
185
+ * reason about than a replay that has to be correct. */
186
+ snapshot(): Promise<WorkspaceCheckpointFile[]>;
98
187
  }