talon-agent 1.49.0 → 1.50.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 (46) hide show
  1. package/README.md +2 -0
  2. package/package.json +1 -1
  3. package/src/backend/codex/factory.ts +2 -0
  4. package/src/backend/codex/handler/message.ts +10 -0
  5. package/src/backend/kilo/factory.ts +2 -0
  6. package/src/backend/kilo/handler/message.ts +10 -0
  7. package/src/backend/openai-agents/factory.ts +2 -0
  8. package/src/backend/openai-agents/handler/message.ts +10 -0
  9. package/src/backend/opencode/factory.ts +2 -0
  10. package/src/backend/opencode/handler/message.ts +10 -0
  11. package/src/backend/shared/index.ts +4 -0
  12. package/src/backend/shared/turn-interrupt.ts +61 -0
  13. package/src/bootstrap.ts +5 -0
  14. package/src/cli/events.ts +47 -12
  15. package/src/cli/fs.ts +138 -0
  16. package/src/cli/index.ts +31 -7
  17. package/src/cli/tasks.ts +85 -20
  18. package/src/core/engine/gateway-actions/index.ts +3 -0
  19. package/src/core/engine/gateway-actions/native.ts +151 -24
  20. package/src/core/engine/gateway-actions/vfs.ts +69 -0
  21. package/src/core/engine/gateway.ts +28 -0
  22. package/src/core/tasks/table.ts +1 -1
  23. package/src/core/tasks/types.ts +6 -2
  24. package/src/core/tools/index.ts +2 -0
  25. package/src/core/tools/native.ts +19 -6
  26. package/src/core/tools/types.ts +2 -1
  27. package/src/core/tools/vfs.ts +61 -0
  28. package/src/core/vfs/index.ts +113 -0
  29. package/src/core/vfs/mounts/files.ts +154 -0
  30. package/src/core/vfs/mounts/plugins.ts +72 -0
  31. package/src/core/vfs/mounts/proc.ts +125 -0
  32. package/src/core/vfs/types.ts +103 -0
  33. package/src/core/vfs/vfs.ts +237 -0
  34. package/src/core/weaver/weaver.ts +40 -3
  35. package/src/frontend/native/discovery.ts +3 -0
  36. package/src/frontend/native/index.ts +10 -1
  37. package/src/frontend/native/server.ts +65 -6
  38. package/src/frontend/native/tls.ts +313 -0
  39. package/src/storage/journal.ts +91 -0
  40. package/src/storage/repositories/journal-repo.ts +38 -0
  41. package/src/storage/sql/journal.sql +16 -0
  42. package/src/storage/sql/schema.sql +15 -0
  43. package/src/storage/sql/statements.generated.ts +24 -1
  44. package/src/util/config.ts +9 -0
  45. package/src/util/log.ts +1 -0
  46. package/src/util/paths.ts +2 -0
@@ -39,7 +39,8 @@ export const nativeTools: ToolDefinition[] = [
39
39
  name: "bash",
40
40
  description:
41
41
  "Run a shell command. Runs on the daemon host, or ON the active teleport device if one is engaged. On a teleported device, `cd` persists across calls (a real working-directory session). " +
42
- "Foreground runs are killed at timeout_sec — for streaming/never-ending commands (adb logcat, tail -f, dev servers, watchers) set background:true instead: the command is launched detached in its own process group with stdout+stderr captured to a log file, and the tool returns immediately with the pid + log path so you can poll the log and kill it when done.",
42
+ "Foreground runs are killed at timeout_sec — for streaming/never-ending commands (adb logcat, tail -f, dev servers, watchers) set background:true instead: the command is launched detached in its own process group with stdout+stderr captured to a log file, and the tool returns immediately with the pid + log path so you can poll the log and kill it when done. " +
43
+ "talon:// addresses are not OS paths — the shell needs the disk locations the namespace root listing shows (vfs_list).",
43
44
  schema: {
44
45
  command: z.string().describe("The shell command to run."),
45
46
  cwd: z
@@ -69,7 +70,11 @@ export const nativeTools: ToolDefinition[] = [
69
70
  description:
70
71
  "Read a file (with line numbers). Runs on the daemon host or the active teleport device. Supports offset/limit for large files.",
71
72
  schema: {
72
- path: z.string().describe("Absolute file path."),
73
+ path: z
74
+ .string()
75
+ .describe(
76
+ "Absolute file path, or a talon:// namespace address (resolved on the daemon host).",
77
+ ),
73
78
  offset: z.number().optional().describe("0-based line to start from."),
74
79
  limit: z
75
80
  .number()
@@ -84,7 +89,11 @@ export const nativeTools: ToolDefinition[] = [
84
89
  description:
85
90
  "Write (create/overwrite) a file with the given content. Runs on the daemon host or the active teleport device.",
86
91
  schema: {
87
- path: z.string().describe("Absolute file path."),
92
+ path: z
93
+ .string()
94
+ .describe(
95
+ "Absolute file path, or a talon:// namespace address (resolved on the daemon host).",
96
+ ),
88
97
  content: z.string().describe("Full file content to write."),
89
98
  },
90
99
  execute: (params, bridge) => bridge("native_write", params),
@@ -95,7 +104,11 @@ export const nativeTools: ToolDefinition[] = [
95
104
  description:
96
105
  "Exact-string replacement in a file. old_string must be unique unless replace_all is set. Runs on the daemon host or the active teleport device.",
97
106
  schema: {
98
- path: z.string().describe("Absolute file path."),
107
+ path: z
108
+ .string()
109
+ .describe(
110
+ "Absolute file path, or a talon:// namespace address (resolved on the daemon host).",
111
+ ),
99
112
  old_string: z.string().describe("Exact text to replace."),
100
113
  new_string: z.string().describe("Replacement text."),
101
114
  replace_all: z
@@ -115,7 +128,7 @@ export const nativeTools: ToolDefinition[] = [
115
128
  path: z
116
129
  .string()
117
130
  .optional()
118
- .describe("Root directory to search (default cwd)."),
131
+ .describe("Root directory to search (default cwd; talon:// accepted)."),
119
132
  },
120
133
  execute: (params, bridge) => bridge("native_glob", params),
121
134
  tag: "native",
@@ -129,7 +142,7 @@ export const nativeTools: ToolDefinition[] = [
129
142
  path: z
130
143
  .string()
131
144
  .optional()
132
- .describe("Root directory or file (default cwd)."),
145
+ .describe("Root directory or file (default cwd; talon:// accepted)."),
133
146
  glob: z
134
147
  .string()
135
148
  .optional()
@@ -28,7 +28,8 @@ export type ToolTag =
28
28
  | "admin"
29
29
  | "models"
30
30
  | "mesh"
31
- | "native";
31
+ | "native"
32
+ | "vfs";
32
33
 
33
34
  /** The bridge caller signature — injected into execute(). */
34
35
  export type BridgeFunction = (
@@ -0,0 +1,61 @@
1
+ /**
2
+ * VFS tools — the unified `talon://` namespace.
3
+ *
4
+ * One browsable tree over everything the daemon owns: real stores (the
5
+ * workspace, skills, scripts, logs) and live synthetic state (`proc/` for
6
+ * the task table and event bus, `plugins/` for the registry) answer the
7
+ * same three tools. The synthetic mounts are the point — this is the only
8
+ * way to read live daemon state as plain files.
9
+ */
10
+
11
+ import { z } from "zod";
12
+ import type { ToolDefinition } from "./types.js";
13
+
14
+ const NAMESPACE = `Namespace roots: home/ (workspace), skills/, scripts/, logs/, proc/ (live task table under proc/tasks/<id> and the event ring at proc/events), plugins/ (registry view). List "" to see the root and where each mount lives on disk. Absolute OS paths inside a mounted directory are accepted and translated.`;
15
+
16
+ export const vfsTools: ToolDefinition[] = [
17
+ {
18
+ name: "vfs_list",
19
+ description: `List a directory in the talon:// namespace — the unified view over Talon's own state. ${NAMESPACE} Entries show kind (d/-), writability, size, and name.`,
20
+ schema: {
21
+ path: z
22
+ .string()
23
+ .describe(
24
+ 'Namespace path, e.g. "" (root), "proc/tasks", "skills/review-pr". Forward slashes on every platform; "talon://" prefix optional.',
25
+ ),
26
+ },
27
+ execute: (params, bridge) => bridge("vfs_list", params),
28
+ tag: "vfs",
29
+ },
30
+
31
+ {
32
+ name: "vfs_read",
33
+ description: `Read a file from the talon:// namespace (UTF-8 text, 256KB cap). ${NAMESPACE} Reading proc/tasks/<id> returns that task as JSON; proc/events returns the event ring as JSON Lines.`,
34
+ schema: {
35
+ path: z
36
+ .string()
37
+ .describe(
38
+ 'Namespace file path, e.g. "proc/events" or "logs/2026-07-16.md"',
39
+ ),
40
+ },
41
+ execute: (params, bridge) => bridge("vfs_read", params),
42
+ tag: "vfs",
43
+ },
44
+
45
+ {
46
+ name: "vfs_write",
47
+ description: `Write a UTF-8 file into a writable part of the talon:// namespace (home/, skills/, scripts/). Parent directories are created; synthetic mounts (proc/, plugins/) and logs/ are read-only views.`,
48
+ schema: {
49
+ path: z
50
+ .string()
51
+ .describe(
52
+ 'Namespace file path under a writable mount, e.g. "home/notes/ideas.md"',
53
+ ),
54
+ content: z
55
+ .string()
56
+ .describe("Full file content (replaces any existing content)"),
57
+ },
58
+ execute: (params, bridge) => bridge("vfs_write", params),
59
+ tag: "vfs",
60
+ },
61
+ ];
@@ -0,0 +1,113 @@
1
+ /**
2
+ * VFS — public surface and default namespace.
3
+ *
4
+ * `getVfs()` serves the process-wide namespace, built on first use rather
5
+ * than at module load: the file mounts capture `dirs.*` when constructed,
6
+ * and eager construction would freeze those paths into whichever process
7
+ * (or test mock) happened to import this barrel first. Mounts project
8
+ * live singletons, so there is nothing to inject at bootstrap:
9
+ *
10
+ * talon://
11
+ * home/ workspace (writable)
12
+ * skills/ SKILL.md bundles (writable)
13
+ * scripts/ agent script bodies (writable)
14
+ * logs/ daily logs (read-only — the daemon writes these)
15
+ * proc/ task table + event bus ring (synthetic, read-only)
16
+ * plugins/ plugin registry view (synthetic, read-only)
17
+ *
18
+ * Read surfaces: gateway `GET /vfs/list` / `GET /vfs/read` and the
19
+ * vfs_* MCP tools (via gateway actions); CLI `talon ls` / `talon cat`.
20
+ */
21
+
22
+ import { dirs } from "../../util/paths.js";
23
+ import { bus } from "../bus/index.js";
24
+ import { registry } from "../plugin/registry.js";
25
+ import { taskTable } from "../tasks/index.js";
26
+ import { createFileMount } from "./mounts/files.js";
27
+ import { createPluginsMount, type PluginView } from "./mounts/plugins.js";
28
+ import { createProcMount } from "./mounts/proc.js";
29
+ import { Vfs } from "./vfs.js";
30
+
31
+ export { Vfs } from "./vfs.js";
32
+ export { createFileMount } from "./mounts/files.js";
33
+ export { createProcMount, type ProcMountDeps } from "./mounts/proc.js";
34
+ export { createPluginsMount, type PluginView } from "./mounts/plugins.js";
35
+ export type {
36
+ VfsErrorCode,
37
+ VfsMount,
38
+ VfsNodeKind,
39
+ VfsResult,
40
+ VfsStat,
41
+ } from "./types.js";
42
+ export { VFS_MAX_READ_BYTES, vfsError, vfsOk } from "./types.js";
43
+
44
+ function pluginViews(): PluginView[] {
45
+ const modules: PluginView[] = registry.all.map(({ plugin, path }) => ({
46
+ name: plugin.name,
47
+ kind: "module",
48
+ ...(plugin.description !== undefined
49
+ ? { description: plugin.description }
50
+ : {}),
51
+ ...(plugin.version !== undefined ? { version: plugin.version } : {}),
52
+ source: path,
53
+ }));
54
+ const mcp: PluginView[] = registry.mcpEntries.map((entry) => ({
55
+ name: entry.name,
56
+ kind: "mcp",
57
+ source: `${entry.command}${entry.args ? ` ${entry.args.join(" ")}` : ""}`,
58
+ }));
59
+ return [...modules, ...mcp];
60
+ }
61
+
62
+ function buildDefaultNamespace(): Vfs {
63
+ const vfs = new Vfs();
64
+ vfs.mount(
65
+ "home",
66
+ createFileMount({
67
+ root: dirs.workspace,
68
+ description: "The workspace — the agent's home directory",
69
+ writable: true,
70
+ }),
71
+ );
72
+ vfs.mount(
73
+ "skills",
74
+ createFileMount({
75
+ root: dirs.skills,
76
+ description: "SKILL.md workflow bundles",
77
+ writable: true,
78
+ }),
79
+ );
80
+ vfs.mount(
81
+ "scripts",
82
+ createFileMount({
83
+ root: dirs.scripts,
84
+ description: "Agent-authored script bodies",
85
+ writable: true,
86
+ }),
87
+ );
88
+ vfs.mount(
89
+ "logs",
90
+ createFileMount({
91
+ root: dirs.logs,
92
+ description: "Daily interaction logs (daemon-written)",
93
+ writable: false,
94
+ }),
95
+ );
96
+ vfs.mount(
97
+ "proc",
98
+ createProcMount({
99
+ tasks: () => taskTable.list(),
100
+ events: () => bus.recent(0),
101
+ }),
102
+ );
103
+ vfs.mount("plugins", createPluginsMount(pluginViews));
104
+ return vfs;
105
+ }
106
+
107
+ let defaultVfs: Vfs | undefined;
108
+
109
+ /** The process-wide namespace, built on first use. */
110
+ export function getVfs(): Vfs {
111
+ defaultVfs ??= buildDefaultNamespace();
112
+ return defaultVfs;
113
+ }
@@ -0,0 +1,154 @@
1
+ /**
2
+ * File mount — a real directory exposed into the namespace.
3
+ *
4
+ * Backs the workspace-shaped mounts (home, skills, scripts, logs). The
5
+ * resolver has already rejected traversal segments; the containment check
6
+ * here is belt-and-braces so no future caller can reach outside the root.
7
+ * A missing root reads as an empty directory — mounts exist from boot,
8
+ * the workspace dirs appear lazily.
9
+ */
10
+
11
+ import {
12
+ existsSync,
13
+ mkdirSync,
14
+ readFileSync,
15
+ readdirSync,
16
+ statSync,
17
+ writeFileSync,
18
+ } from "node:fs";
19
+ import { dirname, isAbsolute, relative, resolve, sep } from "node:path";
20
+ import type { VfsMount, VfsResult, VfsStat } from "../types.js";
21
+ import { VFS_MAX_READ_BYTES, vfsError, vfsOk } from "../types.js";
22
+
23
+ export function createFileMount(options: {
24
+ root: string;
25
+ description: string;
26
+ writable: boolean;
27
+ }): VfsMount {
28
+ const root = resolve(options.root);
29
+
30
+ /** Absolute path for `rel`, or undefined when it would escape the root. */
31
+ function realPath(rel: string): string | undefined {
32
+ const abs = resolve(root, ...rel.split("/"));
33
+ const offset = relative(root, abs);
34
+ if (
35
+ offset.startsWith(`..${sep}`) ||
36
+ offset === ".." ||
37
+ isAbsolute(offset)
38
+ ) {
39
+ return undefined;
40
+ }
41
+ return abs;
42
+ }
43
+
44
+ function statAt(rel: string, abs: string): VfsResult<VfsStat> {
45
+ try {
46
+ const stats = statSync(abs);
47
+ const name = rel === "" ? "" : rel.split("/").at(-1)!;
48
+ return vfsOk({
49
+ path: rel,
50
+ name,
51
+ kind: stats.isDirectory() ? ("dir" as const) : ("file" as const),
52
+ ...(stats.isFile() ? { size: stats.size } : {}),
53
+ modifiedAt: Math.floor(stats.mtimeMs),
54
+ writable: options.writable,
55
+ osPath: abs,
56
+ });
57
+ } catch {
58
+ return vfsError("not-found");
59
+ }
60
+ }
61
+
62
+ return {
63
+ description: options.description,
64
+ writable: options.writable,
65
+ osRoot: root,
66
+
67
+ stat(rel) {
68
+ const abs = realPath(rel);
69
+ if (!abs) return vfsError("invalid-path");
70
+ if (rel === "" && !existsSync(abs)) {
71
+ return vfsOk({
72
+ path: "",
73
+ name: "",
74
+ kind: "dir",
75
+ writable: options.writable,
76
+ osPath: abs,
77
+ });
78
+ }
79
+ return statAt(rel, abs);
80
+ },
81
+
82
+ list(rel) {
83
+ const abs = realPath(rel);
84
+ if (!abs) return vfsError("invalid-path");
85
+ if (rel === "" && !existsSync(abs)) return vfsOk([]);
86
+ let stats;
87
+ try {
88
+ stats = statSync(abs);
89
+ } catch {
90
+ return vfsError("not-found");
91
+ }
92
+ if (!stats.isDirectory()) return vfsError("not-a-directory");
93
+ const entries = readdirSync(abs, { withFileTypes: true })
94
+ .map((entry) => {
95
+ const childRel = rel === "" ? entry.name : `${rel}/${entry.name}`;
96
+ const child = statAt(childRel, resolve(abs, entry.name));
97
+ return child.ok ? child.value : undefined;
98
+ })
99
+ .filter((entry): entry is VfsStat => entry !== undefined)
100
+ .sort(
101
+ (a, b) =>
102
+ Number(a.kind === "file") - Number(b.kind === "file") ||
103
+ a.name.localeCompare(b.name),
104
+ );
105
+ return vfsOk(entries);
106
+ },
107
+
108
+ read(rel) {
109
+ const abs = realPath(rel);
110
+ if (!abs) return vfsError("invalid-path");
111
+ let stats;
112
+ try {
113
+ stats = statSync(abs);
114
+ } catch {
115
+ return vfsError("not-found");
116
+ }
117
+ if (stats.isDirectory()) return vfsError("is-a-directory");
118
+ if (stats.size > VFS_MAX_READ_BYTES) {
119
+ return vfsError(
120
+ "too-large",
121
+ `${stats.size} bytes (cap ${VFS_MAX_READ_BYTES}) — on disk at ${abs}`,
122
+ );
123
+ }
124
+ const buffer = readFileSync(abs);
125
+ if (buffer.includes(0)) {
126
+ return vfsError("binary-file", "Content contains null bytes");
127
+ }
128
+ return vfsOk(buffer.toString("utf-8"));
129
+ },
130
+
131
+ ...(options.writable
132
+ ? {
133
+ write(rel: string, content: string): VfsResult<VfsStat> {
134
+ if (rel === "") return vfsError("is-a-directory");
135
+ const abs = realPath(rel);
136
+ if (!abs) return vfsError("invalid-path");
137
+ if (existsSync(abs) && statSync(abs).isDirectory()) {
138
+ return vfsError("is-a-directory");
139
+ }
140
+ try {
141
+ mkdirSync(dirname(abs), { recursive: true });
142
+ writeFileSync(abs, content, "utf-8");
143
+ } catch (err) {
144
+ return vfsError(
145
+ "io-error",
146
+ err instanceof Error ? err.message : String(err),
147
+ );
148
+ }
149
+ return statAt(rel, abs);
150
+ },
151
+ }
152
+ : {}),
153
+ };
154
+ }
@@ -0,0 +1,72 @@
1
+ /**
2
+ * Plugins mount — the plugin registry as a synthetic read-only directory:
3
+ * one file per loaded plugin (or registered standalone MCP entry), whose
4
+ * content is the registry's view of it as pretty JSON. Provider-injected
5
+ * like proc — the index wires the live registry, tests wire fixtures.
6
+ */
7
+
8
+ import type { VfsMount, VfsResult, VfsStat } from "../types.js";
9
+ import { vfsError, vfsOk } from "../types.js";
10
+
11
+ /** The registry facts one plugin file exposes. */
12
+ export interface PluginView {
13
+ readonly name: string;
14
+ readonly kind: "module" | "mcp";
15
+ readonly description?: string;
16
+ readonly version?: string;
17
+ /** Module path, or the MCP entry's command line. */
18
+ readonly source: string;
19
+ }
20
+
21
+ function render(view: PluginView): string {
22
+ return `${JSON.stringify(view, null, 2)}\n`;
23
+ }
24
+
25
+ function fileStat(view: PluginView): VfsStat {
26
+ return {
27
+ path: view.name,
28
+ name: view.name,
29
+ kind: "file",
30
+ size: Buffer.byteLength(render(view), "utf-8"),
31
+ writable: false,
32
+ };
33
+ }
34
+
35
+ export function createPluginsMount(
36
+ plugins: () => readonly PluginView[],
37
+ ): VfsMount {
38
+ function byName(rel: string): PluginView | undefined {
39
+ return plugins().find((view) => view.name === rel);
40
+ }
41
+
42
+ return {
43
+ description: "Loaded plugins and registered MCP servers (registry view)",
44
+ writable: false,
45
+
46
+ stat(rel): VfsResult<VfsStat> {
47
+ if (rel === "")
48
+ return vfsOk({ path: "", name: "", kind: "dir", writable: false });
49
+ const view = byName(rel);
50
+ return view ? vfsOk(fileStat(view)) : vfsError("not-found");
51
+ },
52
+
53
+ list(rel) {
54
+ if (rel !== "") {
55
+ return byName(rel)
56
+ ? vfsError("not-a-directory")
57
+ : vfsError("not-found");
58
+ }
59
+ return vfsOk(
60
+ plugins()
61
+ .map(fileStat)
62
+ .sort((a, b) => a.name.localeCompare(b.name)),
63
+ );
64
+ },
65
+
66
+ read(rel) {
67
+ if (rel === "") return vfsError("is-a-directory");
68
+ const view = byName(rel);
69
+ return view ? vfsOk(render(view)) : vfsError("not-found");
70
+ },
71
+ };
72
+ }
@@ -0,0 +1,125 @@
1
+ /**
2
+ * Proc mount — live daemon state as a synthetic read-only tree, in the
3
+ * /proc tradition:
4
+ *
5
+ * proc/
6
+ * tasks/<id> one task table record, pretty JSON
7
+ * events the event bus ring, JSON Lines (newest last)
8
+ *
9
+ * Providers are injected so the mount is a pure projection — the index
10
+ * wires the real task table and bus, tests wire fixtures. Sizes and
11
+ * timestamps are computed from the rendered content on demand; this is a
12
+ * view of live state, nothing is cached.
13
+ */
14
+
15
+ import type { PublishedEvent } from "../../bus/index.js";
16
+ import type { TaskRecord } from "../../tasks/index.js";
17
+ import type { VfsMount, VfsResult, VfsStat } from "../types.js";
18
+ import { vfsError, vfsOk } from "../types.js";
19
+
20
+ export interface ProcMountDeps {
21
+ tasks: () => readonly TaskRecord[];
22
+ events: () => readonly PublishedEvent[];
23
+ }
24
+
25
+ const DIR = (path: string, name: string): VfsStat => ({
26
+ path,
27
+ name,
28
+ kind: "dir",
29
+ writable: false,
30
+ });
31
+
32
+ function fileStat(path: string, content: string, modifiedAt?: number): VfsStat {
33
+ return {
34
+ path,
35
+ name: path.split("/").at(-1)!,
36
+ kind: "file",
37
+ size: Buffer.byteLength(content, "utf-8"),
38
+ ...(modifiedAt !== undefined ? { modifiedAt } : {}),
39
+ writable: false,
40
+ };
41
+ }
42
+
43
+ function renderTask(task: TaskRecord): string {
44
+ return `${JSON.stringify(task, null, 2)}\n`;
45
+ }
46
+
47
+ function taskModifiedAt(task: TaskRecord): number {
48
+ return task.endedAt ?? task.startedAt ?? task.queuedAt;
49
+ }
50
+
51
+ function renderEvents(events: readonly PublishedEvent[]): string {
52
+ if (events.length === 0) return "";
53
+ return events.map((event) => JSON.stringify(event)).join("\n") + "\n";
54
+ }
55
+
56
+ export function createProcMount(deps: ProcMountDeps): VfsMount {
57
+ function taskById(id: string): TaskRecord | undefined {
58
+ if (!/^\d+$/.test(id)) return undefined;
59
+ return deps.tasks().find((task) => task.id === Number(id));
60
+ }
61
+
62
+ function resolveNode(rel: string): VfsResult<VfsStat> {
63
+ if (rel === "") return vfsOk(DIR("", ""));
64
+ if (rel === "tasks") return vfsOk(DIR("tasks", "tasks"));
65
+ if (rel === "events") {
66
+ const events = deps.events();
67
+ return vfsOk(fileStat("events", renderEvents(events), events.at(-1)?.at));
68
+ }
69
+ const taskId = rel.startsWith("tasks/") ? rel.slice("tasks/".length) : null;
70
+ if (taskId !== null && !taskId.includes("/")) {
71
+ const task = taskById(taskId);
72
+ if (!task) return vfsError("not-found");
73
+ return vfsOk(
74
+ fileStat(`tasks/${task.id}`, renderTask(task), taskModifiedAt(task)),
75
+ );
76
+ }
77
+ return vfsError("not-found");
78
+ }
79
+
80
+ return {
81
+ description: "Live daemon state: task table and event bus ring",
82
+ writable: false,
83
+
84
+ stat: resolveNode,
85
+
86
+ list(rel) {
87
+ if (rel === "") {
88
+ const events = deps.events();
89
+ return vfsOk([
90
+ DIR("tasks", "tasks"),
91
+ fileStat("events", renderEvents(events), events.at(-1)?.at),
92
+ ]);
93
+ }
94
+ if (rel === "tasks") {
95
+ return vfsOk(
96
+ deps
97
+ .tasks()
98
+ .map((task) =>
99
+ fileStat(
100
+ `tasks/${task.id}`,
101
+ renderTask(task),
102
+ taskModifiedAt(task),
103
+ ),
104
+ )
105
+ .sort((a, b) => Number(a.name) - Number(b.name)),
106
+ );
107
+ }
108
+ const node = resolveNode(rel);
109
+ if (!node.ok) return node;
110
+ return vfsError("not-a-directory");
111
+ },
112
+
113
+ read(rel) {
114
+ if (rel === "events") return vfsOk(renderEvents(deps.events()));
115
+ if (rel.startsWith("tasks/")) {
116
+ const task = taskById(rel.slice("tasks/".length));
117
+ if (!task) return vfsError("not-found");
118
+ return vfsOk(renderTask(task));
119
+ }
120
+ const node = resolveNode(rel);
121
+ if (!node.ok) return node;
122
+ return vfsError("is-a-directory");
123
+ },
124
+ };
125
+ }