@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
@@ -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":"AAWA;;+BAE+B;AAC/B,MAAM,OAAO,GAAG,KAAK,CAAC;AAEtB;;;;;;;;;;;;;;GAcG;AACH,MAAM,OAAO,eAAe;IAMG;IALrB,KAAK,GAA0B,IAAI,CAAC;IAC5C;6DACyD;IACxC,IAAI,GAAG,IAAI,GAAG,EAAU,CAAC;IAE1C,YAA6B,IAAyB;QAAzB,SAAI,GAAJ,IAAI,CAAqB;IAAG,CAAC;IAE1D,KAAK;QACH,IAAI,IAAI,CAAC,KAAK;YAAE,OAAO;QACvB,IAAI,CAAC,KAAK,GAAG,WAAW,CAAC,GAAG,EAAE,CAAC,KAAK,IAAI,CAAC,IAAI,EAAE,EAAE,OAAO,CAAC,CAAC;QAC1D,IAAI,CAAC,KAAK,CAAC,KAAK,EAAE,EAAE,CAAC;IACvB,CAAC;IAED,IAAI;QACF,IAAI,CAAC,IAAI,CAAC,KAAK;YAAE,OAAO;QACxB,aAAa,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QAC1B,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC;IACpB,CAAC;IAED,0EAA0E;IAC1E,KAAK,CAAC,IAAI;QACR,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;QACvB,KAAK,MAAM,KAAK,IAAI,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC,IAAI,EAAE,EAAE,CAAC;YAC9C,IAAI,KAAK,CAAC,IAAI,KAAK,OAAO;gBAAE,SAAS;YACrC,IAAI,KAAK,CAAC,MAAM,CAAC,IAAI,KAAK,SAAS;gBAAE,SAAS;YAC9C,IAAI,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,SAAS,CAAC;gBAAE,SAAS;YAE7C,MAAM,IAAI,GACR,KAAK,CAAC,WAAW,KAAK,CAAC;gBACvB,KAAK,CAAC,SAAS,KAAK,IAAI;gBACxB,GAAG,GAAG,KAAK,CAAC,SAAS,IAAI,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC;YAE5C,IAAI,IAAI,EAAE,CAAC;gBACT,KAAK,IAAI,CAAC,GAAG,CAAC,KAAK,EAAE,GAAG,EAAE,CAAC,IAAI,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC;YAClD,CAAC;iBAAM,IACL,KAAK,CAAC,cAAc,KAAK,IAAI;gBAC7B,GAAG,GAAG,KAAK,CAAC,cAAc,IAAI,IAAI,CAAC,IAAI,CAAC,YAAY,EACpD,CAAC;gBACD,KAAK,IAAI,CAAC,GAAG,CAAC,KAAK,EAAE,GAAG,EAAE,CAAC,IAAI,CAAC,UAAU,CAAC,KAAK,CAAC,CAAC,CAAC;YACrD,CAAC;QACH,CAAC;IACH,CAAC;IAEO,KAAK,CAAC,GAAG,CAAC,KAAmB,EAAE,GAAwB;QAC7D,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,SAAS,CAAC,CAAC;QAC/B,IAAI,CAAC;YACH,MAAM,GAAG,EAAE,CAAC;QACd,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,0EAA0E;YAC1E,sEAAsE;YACtE,wEAAwE;YACxE,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,IAAI,CACjB,EAAE,GAAG,EAAE,SAAS,EAAE,KAAK,CAAC,SAAS,EAAE,EACnC,yCAAyC,CAC1C,CAAC;QACJ,CAAC;gBAAS,CAAC;YACT,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,SAAS,CAAC,CAAC;QACpC,CAAC;IACH,CAAC;IAEO,KAAK,CAAC,UAAU,CAAC,KAAmB;QAC1C,MAAM,SAAS,GAAG,KAAK,CAAC,OAAO,EAAE,SAAS,CAAC;QAC3C,IAAI,CAAC,SAAS;YAAE,OAAO;QACvB,MAAM,KAAK,GAAG,MAAM,SAAS,CAAC,QAAQ,EAAE,CAAC;QACzC,KAAK,CAAC,UAAU,GAAG,EAAE,OAAO,EAAE,IAAI,IAAI,EAAE,EAAE,KAAK,EAAE,CAAC;QAClD,KAAK,CAAC,cAAc,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;IACpC,CAAC;IAEO,KAAK,CAAC,OAAO,CAAC,KAAmB;QACvC,MAAM,OAAO,GAAG,KAAK,CAAC,OAAO,CAAC;QAC9B,IAAI,CAAC,OAAO,EAAE,OAAO;YAAE,OAAO;QAC9B,yEAAyE;QACzE,yEAAyE;QACzE,mEAAmE;QACnE,MAAM,IAAI,CAAC,UAAU,CAAC,KAAK,CAAC,CAAC;QAC7B,MAAM,OAAO,CAAC,OAAO,EAAE,CAAC;QACxB,KAAK,CAAC,OAAO,GAAG,IAAI,CAAC;QACrB,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,KAAK,CAAC,SAAS,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,IAAI,EAAE,WAAW,EAAE,EAAE,CAAC,CAAC;IAC9F,CAAC;CACF"}
@@ -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.2",
3
+ "version": "0.10.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
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",
@@ -1,6 +1,11 @@
1
1
  import { describe, expect, it } from "vitest";
2
2
 
3
- import { loadAppsFromEnv, loadResolvedApps, RunnerConfigError } from "./config.js";
3
+ import {
4
+ coResidentAgentNames,
5
+ loadAppsFromEnv,
6
+ loadResolvedApps,
7
+ RunnerConfigError,
8
+ } from "./config.js";
4
9
 
5
10
  describe("loadAppsFromEnv", () => {
6
11
  it("returns undefined when RUNNER_APPS is unset (no apps offered)", () => {
@@ -31,6 +36,18 @@ describe("loadAppsFromEnv", () => {
31
36
  loadAppsFromEnv({ RUNNER_APPS: '{"x":{"image":"i","pullPolicy":"sometimes"}}' }),
32
37
  ).toThrow(/pullPolicy/);
33
38
  });
39
+
40
+ it("accepts a port and rejects one that is not a usable tcp port", () => {
41
+ const catalog = loadAppsFromEnv({
42
+ RUNNER_APPS: '{"agent":{"image":"i","port":8080}}',
43
+ })!;
44
+ expect(catalog.agent.port).toBe(8080);
45
+ for (const bad of ["0", "65536", '"8080"', "8080.5"]) {
46
+ expect(() =>
47
+ loadAppsFromEnv({ RUNNER_APPS: `{"agent":{"image":"i","port":${bad}}}` }),
48
+ ).toThrow(/invalid 'port'/);
49
+ }
50
+ });
34
51
  });
35
52
 
36
53
  describe("loadResolvedApps", () => {
@@ -47,8 +64,39 @@ describe("loadResolvedApps", () => {
47
64
  image: "acme/tool:1",
48
65
  env: {},
49
66
  pullPolicy: "missing",
67
+ port: undefined,
50
68
  title: undefined,
51
69
  description: undefined,
52
70
  });
53
71
  });
72
+
73
+ // No default: the catalog is operator configuration and the runner knows
74
+ // nothing about any specific app, so a port it did not declare is absent
75
+ // rather than guessed — which is what makes `agent_port_undeclared` possible.
76
+ it("leaves an undeclared port undefined and carries a declared one", () => {
77
+ const resolved = loadResolvedApps({
78
+ RUNNER_APPS: '{"plain":{"image":"a"},"agent":{"image":"b","port":8080}}',
79
+ });
80
+ expect(resolved.plain.port).toBeUndefined();
81
+ expect(resolved.agent.port).toBe(8080);
82
+ });
83
+ });
84
+
85
+ describe("coResidentAgentNames", () => {
86
+ it("names only the entries a session would accept as an agent", () => {
87
+ // The advertised set IS the acceptance condition. Deriving it here rather
88
+ // than in each runner is what stops the two drifting the next time the
89
+ // condition grows — which is exactly how a port-less catalog came to be
90
+ // advertised and then refused on every run.
91
+ const apps = loadResolvedApps({
92
+ RUNNER_APPS: '{"agent":{"image":"a","port":8080},"tool":{"image":"b"}}',
93
+ });
94
+ expect(coResidentAgentNames(apps)).toEqual(["agent"]);
95
+ });
96
+
97
+ it("is empty for a catalog predating the port field", () => {
98
+ expect(coResidentAgentNames(loadResolvedApps({ RUNNER_APPS: '{"tool":{"image":"b"}}' }))).toEqual(
99
+ [],
100
+ );
101
+ });
54
102
  });