tryharness 0.7.22 → 0.7.25

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 (64) hide show
  1. package/README.md +48 -8
  2. package/assets/opencode-plugin.js +48 -10
  3. package/assets/pi-extension.ts +106 -26
  4. package/cli.mjs +1281 -289
  5. package/lib/activity.mjs +197 -40
  6. package/lib/adapters.mjs +337 -54
  7. package/lib/agentnode.mjs +175 -35
  8. package/lib/atlas.mjs +604 -0
  9. package/lib/bip39-english.txt +2048 -0
  10. package/lib/cards.mjs +177 -39
  11. package/lib/claudemem.mjs +74 -18
  12. package/lib/commands.mjs +13 -5
  13. package/lib/context.mjs +628 -0
  14. package/lib/control-client.mjs +5 -2
  15. package/lib/coordinator.mjs +281 -56
  16. package/lib/daemon.mjs +5590 -838
  17. package/lib/desktop.mjs +149 -35
  18. package/lib/digest.mjs +81 -0
  19. package/lib/environment.mjs +180 -0
  20. package/lib/files.mjs +246 -13
  21. package/lib/firewall.mjs +288 -0
  22. package/lib/folders.mjs +223 -57
  23. package/lib/frames.mjs +333 -0
  24. package/lib/inference.mjs +663 -0
  25. package/lib/learn.mjs +182 -0
  26. package/lib/live.mjs +286 -61
  27. package/lib/localmodels.mjs +571 -0
  28. package/lib/mcp-stdio.mjs +912 -103
  29. package/lib/media.mjs +228 -0
  30. package/lib/members.json +1 -1
  31. package/lib/members.mjs +35 -9
  32. package/lib/models.mjs +342 -80
  33. package/lib/network.mjs +201 -42
  34. package/lib/notify.mjs +49 -12
  35. package/lib/nudge.mjs +190 -32
  36. package/lib/ocr.mjs +380 -0
  37. package/lib/onboard.mjs +321 -70
  38. package/lib/parsers.mjs +132 -41
  39. package/lib/paths.mjs +33 -10
  40. package/lib/personal.mjs +77 -0
  41. package/lib/phones.mjs +398 -51
  42. package/lib/places.mjs +566 -102
  43. package/lib/provenance.mjs +19 -4
  44. package/lib/receipts.mjs +82 -0
  45. package/lib/redact.mjs +730 -0
  46. package/lib/runner.mjs +347 -55
  47. package/lib/runtime-install.mjs +59 -17
  48. package/lib/screens.mjs +212 -0
  49. package/lib/sealed.mjs +160 -28
  50. package/lib/secrets.mjs +18 -5
  51. package/lib/service.mjs +173 -35
  52. package/lib/snippets.mjs +140 -0
  53. package/lib/speech.mjs +993 -0
  54. package/lib/store.mjs +1678 -301
  55. package/lib/swarm.mjs +598 -117
  56. package/lib/t3.mjs +400 -79
  57. package/lib/trails.mjs +401 -0
  58. package/lib/ui.mjs +95 -23
  59. package/lib/vectors.mjs +5 -2
  60. package/lib/wakes.mjs +284 -68
  61. package/lib/watches.mjs +362 -0
  62. package/lib/worktrees.mjs +186 -38
  63. package/package.json +6 -4
  64. package/lib/embedder.mjs +0 -156
package/lib/service.mjs CHANGED
@@ -9,14 +9,27 @@ import { paths } from "./paths.mjs";
9
9
  const LABEL = "ai.tryharness.daemon";
10
10
 
11
11
  function xml(value) {
12
- return String(value).replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&apos;");
12
+ return String(value)
13
+ .replaceAll("&", "&amp;")
14
+ .replaceAll("<", "&lt;")
15
+ .replaceAll(">", "&gt;")
16
+ .replaceAll('"', "&quot;")
17
+ .replaceAll("'", "&apos;");
13
18
  }
14
19
 
15
20
  const WINDOWS_TASK = "tryharness-daemon";
16
21
 
17
22
  function servicePath(platform = process.platform, homeDir = os.homedir()) {
18
- if (platform === "darwin") return path.join(homeDir, "Library", "LaunchAgents", `${LABEL}.plist`);
19
- if (platform === "linux") return path.join(homeDir, ".config", "systemd", "user", "tryharness-daemon.service");
23
+ if (platform === "darwin")
24
+ return path.join(homeDir, "Library", "LaunchAgents", `${LABEL}.plist`);
25
+ if (platform === "linux")
26
+ return path.join(
27
+ homeDir,
28
+ ".config",
29
+ "systemd",
30
+ "user",
31
+ "tryharness-daemon.service"
32
+ );
20
33
  if (platform === "win32") return path.join(paths.home, `${WINDOWS_TASK}.xml`);
21
34
  return null;
22
35
  }
@@ -59,35 +72,76 @@ export function windowsLauncher({ nodePath, entry }) {
59
72
  }
60
73
 
61
74
  function windowsUser(env = process.env) {
62
- return env.USERDOMAIN && env.USERNAME ? `${env.USERDOMAIN}\\${env.USERNAME}` : env.USERNAME || os.userInfo().username;
75
+ return env.USERDOMAIN && env.USERNAME
76
+ ? `${env.USERDOMAIN}\\${env.USERNAME}`
77
+ : env.USERNAME || os.userInfo().username;
63
78
  }
64
79
 
65
- export function installWindowsTask({ runtimeRoot, nodePath = process.execPath, execute = run, env = process.env }) {
80
+ export function installWindowsTask({
81
+ runtimeRoot,
82
+ nodePath = process.execPath,
83
+ execute = run,
84
+ env = process.env,
85
+ }) {
66
86
  ensureDir(paths.home);
67
87
  const entry = path.join(runtimeRoot, "cli.mjs");
68
88
  const launcher = path.join(paths.home, `${WINDOWS_TASK}.vbs`);
69
89
  const definition = path.join(paths.home, `${WINDOWS_TASK}.xml`);
70
90
  fs.writeFileSync(launcher, windowsLauncher({ nodePath, entry }));
71
91
  // schtasks reads task XML as UTF-16 with a byte order mark.
72
- fs.writeFileSync(definition, "\ufeff" + windowsTaskDefinition({ nodePath, entry, user: windowsUser(env), launcher }), "utf16le");
73
- const created = execute("schtasks", ["/Create", "/TN", WINDOWS_TASK, "/XML", definition, "/F"]);
74
- if (!created.ok) throw new Error(`could not register the Harness daemon with Task Scheduler: ${created.stderr || created.stdout || created.error}`);
92
+ fs.writeFileSync(
93
+ definition,
94
+ "\ufeff" +
95
+ windowsTaskDefinition({
96
+ nodePath,
97
+ entry,
98
+ user: windowsUser(env),
99
+ launcher,
100
+ }),
101
+ "utf16le"
102
+ );
103
+ const created = execute("schtasks", [
104
+ "/Create",
105
+ "/TN",
106
+ WINDOWS_TASK,
107
+ "/XML",
108
+ definition,
109
+ "/F",
110
+ ]);
111
+ if (!created.ok)
112
+ throw new Error(
113
+ `could not register the Harness daemon with Task Scheduler: ${created.stderr || created.stdout || created.error}`
114
+ );
75
115
  execute("schtasks", ["/End", "/TN", WINDOWS_TASK]);
76
116
  const started = execute("schtasks", ["/Run", "/TN", WINDOWS_TASK]);
77
- if (!started.ok) throw new Error(`the Harness daemon task was registered but did not start: ${started.stderr || started.stdout || started.error}`);
117
+ if (!started.ok)
118
+ throw new Error(
119
+ `the Harness daemon task was registered but did not start: ${started.stderr || started.stdout || started.error}`
120
+ );
78
121
  return { manager: "task-scheduler", file: definition };
79
122
  }
80
123
 
81
124
  export function uninstallWindowsTask({ execute = run } = {}) {
82
125
  execute("schtasks", ["/End", "/TN", WINDOWS_TASK]);
83
126
  execute("schtasks", ["/Delete", "/TN", WINDOWS_TASK, "/F"]);
84
- for (const file of [path.join(paths.home, `${WINDOWS_TASK}.xml`), path.join(paths.home, `${WINDOWS_TASK}.vbs`)]) {
85
- try { fs.unlinkSync(file); } catch {}
127
+ for (const file of [
128
+ path.join(paths.home, `${WINDOWS_TASK}.xml`),
129
+ path.join(paths.home, `${WINDOWS_TASK}.vbs`),
130
+ ]) {
131
+ try {
132
+ fs.unlinkSync(file);
133
+ } catch {}
86
134
  }
87
135
  }
88
136
 
89
137
  function agentPath() {
90
- const dirs = new Set([path.dirname(process.execPath), "/opt/homebrew/bin", "/usr/local/bin", "/usr/bin", "/bin"]);
138
+ const dirs = new Set([
139
+ path.dirname(process.execPath),
140
+ "/opt/homebrew/bin",
141
+ "/usr/local/bin",
142
+ "/usr/bin",
143
+ "/bin",
144
+ ]);
91
145
  for (const command of ["claude", "codex", "pi", "opencode"]) {
92
146
  const found = resolveCommand(command);
93
147
  if (found) dirs.add(path.dirname(found));
@@ -97,78 +151,162 @@ function agentPath() {
97
151
 
98
152
  function retireLegacyBridge() {
99
153
  if (process.platform === "darwin") {
100
- const legacy = path.join(os.homedir(), "Library", "LaunchAgents", "ai.tryharness.bridge.plist");
154
+ const legacy = path.join(
155
+ os.homedir(),
156
+ "Library",
157
+ "LaunchAgents",
158
+ "ai.tryharness.bridge.plist"
159
+ );
101
160
  if (!fs.existsSync(legacy)) return;
102
161
  run("launchctl", ["unload", legacy]);
103
- try { fs.renameSync(legacy, legacy + ".retired"); } catch {}
162
+ try {
163
+ fs.renameSync(legacy, legacy + ".retired");
164
+ } catch {}
104
165
  } else if (process.platform === "linux") {
105
- const legacy = path.join(os.homedir(), ".config", "systemd", "user", "tryharness-bridge.service");
166
+ const legacy = path.join(
167
+ os.homedir(),
168
+ ".config",
169
+ "systemd",
170
+ "user",
171
+ "tryharness-bridge.service"
172
+ );
106
173
  if (!fs.existsSync(legacy)) return;
107
- run("systemctl", ["--user", "disable", "--now", "tryharness-bridge.service"]);
108
- try { fs.renameSync(legacy, legacy + ".retired"); } catch {}
174
+ run("systemctl", [
175
+ "--user",
176
+ "disable",
177
+ "--now",
178
+ "tryharness-bridge.service",
179
+ ]);
180
+ try {
181
+ fs.renameSync(legacy, legacy + ".retired");
182
+ } catch {}
109
183
  }
110
184
  }
111
185
 
112
- export function installService({ runtimeRoot, nodePath = process.execPath, platform = process.platform, execute = run }) {
186
+ export function installService({
187
+ runtimeRoot,
188
+ nodePath = process.execPath,
189
+ platform = process.platform,
190
+ execute = run,
191
+ }) {
113
192
  ensureDir(paths.home);
114
193
  retireLegacyBridge();
115
194
  const entry = path.join(runtimeRoot, "cli.mjs");
116
195
  const envPath = agentPath();
117
- if (platform === "win32") return installWindowsTask({ runtimeRoot, nodePath, execute });
196
+ if (platform === "win32")
197
+ return installWindowsTask({ runtimeRoot, nodePath, execute });
118
198
  if (platform === "darwin") {
119
199
  const plist = servicePath();
120
200
  ensureDir(path.dirname(plist));
121
- fs.writeFileSync(plist, `<?xml version="1.0" encoding="UTF-8"?>
201
+ // MallocLargeCache=0: macOS otherwise keeps the large blocks ONNX Runtime frees (a screenshot's OCR held 1.6 GB after
202
+ // it finished; 255 MB with it off, image-redaction eval).
203
+ fs.writeFileSync(
204
+ plist,
205
+ `<?xml version="1.0" encoding="UTF-8"?>
122
206
  <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
123
207
  <plist version="1.0"><dict>
124
208
  <key>Label</key><string>${LABEL}</string>
125
209
  <key>ProgramArguments</key><array><string>${xml(nodePath)}</string><string>${xml(entry)}</string><string>daemon</string></array>
126
- <key>EnvironmentVariables</key><dict><key>PATH</key><string>${xml(envPath)}</string></dict>
210
+ <key>EnvironmentVariables</key><dict><key>PATH</key><string>${xml(envPath)}</string><key>MallocLargeCache</key><string>0</string></dict>
127
211
  <key>RunAtLoad</key><true/><key>KeepAlive</key><true/><key>ThrottleInterval</key><integer>5</integer>
128
212
  <key>StandardOutPath</key><string>${xml(paths.log)}</string><key>StandardErrorPath</key><string>${xml(paths.log)}</string>
129
- </dict></plist>\n`, { mode: 0o600 });
213
+ </dict></plist>\n`,
214
+ { mode: 0o600 }
215
+ );
130
216
  const domain = `gui/${process.getuid()}`;
131
217
  run("launchctl", ["bootout", domain, plist]);
132
218
  const started = run("launchctl", ["bootstrap", domain, plist]);
133
- if (!started.ok) throw new Error(`could not start Harness daemon with launchd: ${started.stderr || started.error}`);
219
+ if (!started.ok)
220
+ throw new Error(
221
+ `could not start Harness daemon with launchd: ${started.stderr || started.error}`
222
+ );
134
223
  run("launchctl", ["kickstart", "-k", `${domain}/${LABEL}`]);
135
224
  return { manager: "launchd", file: plist };
136
225
  }
137
226
  if (platform === "linux") {
138
227
  const unit = servicePath();
139
228
  ensureDir(path.dirname(unit));
140
- fs.writeFileSync(unit, `[Unit]\nDescription=Harness local daemon\nAfter=network-online.target\n\n[Service]\nExecStart=${JSON.stringify(nodePath)} ${JSON.stringify(entry)} daemon\nEnvironment=PATH=${envPath}\nRestart=always\nRestartSec=5\nStandardOutput=append:${paths.log}\nStandardError=append:${paths.log}\n\n[Install]\nWantedBy=default.target\n`, { mode: 0o600 });
229
+ fs.writeFileSync(
230
+ unit,
231
+ `[Unit]\nDescription=Harness local daemon\nAfter=network-online.target\n\n[Service]\nExecStart=${JSON.stringify(nodePath)} ${JSON.stringify(entry)} daemon\nEnvironment=PATH=${envPath}\nRestart=always\nRestartSec=5\nStandardOutput=append:${paths.log}\nStandardError=append:${paths.log}\n\n[Install]\nWantedBy=default.target\n`,
232
+ { mode: 0o600 }
233
+ );
141
234
  const reload = run("systemctl", ["--user", "daemon-reload"]);
142
- const started = run("systemctl", ["--user", "enable", "--now", "tryharness-daemon.service"]);
143
- if (!reload.ok || !started.ok) throw new Error(`could not start Harness daemon with systemd: ${started.stderr || reload.stderr}`);
235
+ const started = run("systemctl", [
236
+ "--user",
237
+ "enable",
238
+ "--now",
239
+ "tryharness-daemon.service",
240
+ ]);
241
+ if (!reload.ok || !started.ok)
242
+ throw new Error(
243
+ `could not start Harness daemon with systemd: ${started.stderr || reload.stderr}`
244
+ );
144
245
  return { manager: "systemd", file: unit };
145
246
  }
146
247
  // Other Node platforms get a ready daemon for this login only. `doctor` reports that
147
248
  // login persistence is unavailable rather than pretending it was set.
148
- const child = spawn(nodePath, [entry, "daemon"], { detached: true, stdio: "ignore", env: { ...process.env, PATH: envPath } });
249
+ const child = spawn(nodePath, [entry, "daemon"], {
250
+ detached: true,
251
+ stdio: "ignore",
252
+ env: {
253
+ ...process.env,
254
+ PATH: envPath,
255
+ ...(process.platform === "darwin" ? { MallocLargeCache: "0" } : {}),
256
+ },
257
+ });
149
258
  child.unref();
150
259
  return { manager: "detached", file: null };
151
260
  }
152
261
 
153
- export function uninstallService({ platform = process.platform, execute = run } = {}) {
262
+ export function uninstallService({
263
+ platform = process.platform,
264
+ execute = run,
265
+ } = {}) {
154
266
  const file = servicePath(platform);
155
- if (platform === "win32") { uninstallWindowsTask({ execute }); return; }
267
+ if (platform === "win32") {
268
+ uninstallWindowsTask({ execute });
269
+ return;
270
+ }
156
271
  if (platform === "darwin" && file) {
157
272
  run("launchctl", ["bootout", `gui/${process.getuid()}`, file]);
158
- try { fs.unlinkSync(file); } catch {}
273
+ try {
274
+ fs.unlinkSync(file);
275
+ } catch {}
159
276
  } else if (platform === "linux" && file) {
160
- run("systemctl", ["--user", "disable", "--now", "tryharness-daemon.service"]);
161
- try { fs.unlinkSync(file); } catch {}
277
+ run("systemctl", [
278
+ "--user",
279
+ "disable",
280
+ "--now",
281
+ "tryharness-daemon.service",
282
+ ]);
283
+ try {
284
+ fs.unlinkSync(file);
285
+ } catch {}
162
286
  run("systemctl", ["--user", "daemon-reload"]);
163
287
  }
164
288
  }
165
289
 
166
- export function serviceInfo({ platform = process.platform, execute = run } = {}) {
290
+ export function serviceInfo({
291
+ platform = process.platform,
292
+ execute = run,
293
+ } = {}) {
167
294
  const file = servicePath(platform);
168
295
  if (platform === "win32") {
169
296
  // Installed means the task is registered, not merely that its files exist.
170
- const registered = Boolean(file && fs.existsSync(file)) && execute("schtasks", ["/Query", "/TN", WINDOWS_TASK]).ok;
297
+ const registered =
298
+ Boolean(file && fs.existsSync(file)) &&
299
+ execute("schtasks", ["/Query", "/TN", WINDOWS_TASK]).ok;
171
300
  return { manager: "task-scheduler", installed: registered, file };
172
301
  }
173
- return { manager: platform === "darwin" ? "launchd" : platform === "linux" ? "systemd" : "detached", installed: Boolean(file && fs.existsSync(file)), file };
302
+ return {
303
+ manager:
304
+ platform === "darwin"
305
+ ? "launchd"
306
+ : platform === "linux"
307
+ ? "systemd"
308
+ : "detached",
309
+ installed: Boolean(file && fs.existsSync(file)),
310
+ file,
311
+ };
174
312
  }
@@ -0,0 +1,140 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { randomUUID } from "node:crypto";
4
+ import { nameOf } from "./members.mjs";
5
+ import { repositoryOf } from "./worktrees.mjs";
6
+
7
+ // Code for an agent that does not run on this computer (Grok Bot, Muse) leaves it only with the person's OK. The agent
8
+ // says what it needs to see (request_snippet); the relay finds it in the thread's folder with its local search, takes
9
+ // the lines around it, masks any secret in them, and holds them. The person shares or keeps them (the buddy, or
10
+ // `tryharness board snippet <id> share|keep`); shared lines are posted in the thread for that agent, kept ones never leave.
11
+
12
+ const BEFORE = 5;
13
+ const LINES = 40;
14
+
15
+ export function createSnippets({
16
+ store,
17
+ search,
18
+ scrub,
19
+ wake = () => {},
20
+ notify = () => {},
21
+ log = () => {},
22
+ }) {
23
+ const read = (id) => {
24
+ try {
25
+ return JSON.parse(store.meta(`snippet:${id}`) || "null");
26
+ } catch {
27
+ return null;
28
+ }
29
+ };
30
+ const save = (snippet) => {
31
+ store.meta(`snippet:${snippet.id}`, JSON.stringify(snippet));
32
+ const ids = JSON.parse(store.meta("snippets") || "[]");
33
+ if (!ids.includes(snippet.id))
34
+ store.meta("snippets", JSON.stringify([...ids, snippet.id].slice(-200)));
35
+ };
36
+
37
+ /** A cloud agent's request: { ok, snippet } held for the person, or why there is nothing to hold. */
38
+ const request = async ({ threadId, agent, about }) => {
39
+ const thread = store.getTask(threadId);
40
+ const folder = thread?.kind === "goal" ? thread.goal.workspace : null;
41
+ if (!folder)
42
+ return {
43
+ ok: false,
44
+ error:
45
+ "this thread has no folder on the person's computer to show code from",
46
+ };
47
+ const words = String(about || "")
48
+ .trim()
49
+ .slice(0, 500);
50
+ if (!words) return { ok: false, error: "say what you need to see" };
51
+ const found = await search({ query: words, folder, limit: 1 });
52
+ const file = found?.ok ? found.files[0] : null;
53
+ if (!file)
54
+ return {
55
+ ok: false,
56
+ error: found?.error || "nothing in this thread's folder matched that",
57
+ };
58
+ const root = repositoryOf(folder)?.root ?? folder;
59
+ let lines;
60
+ try {
61
+ lines = fs.readFileSync(path.join(root, file.path), "utf8").split("\n");
62
+ } catch (error) {
63
+ return {
64
+ ok: false,
65
+ error: `${file.path} could not be read (${error.code || error.message})`,
66
+ };
67
+ }
68
+ const from = Math.max(1, file.line - BEFORE),
69
+ to = Math.min(lines.length, from + LINES - 1);
70
+ // A secret in the lines stays a placeholder for good: it is never filled back in for an agent elsewhere.
71
+ const { text, masked } = await scrub(lines.slice(from - 1, to).join("\n"));
72
+ const snippet = {
73
+ id: randomUUID(),
74
+ thread: threadId,
75
+ agent,
76
+ about: words,
77
+ path: file.path,
78
+ from,
79
+ to,
80
+ text,
81
+ masked,
82
+ state: "waiting",
83
+ at: new Date().toISOString(),
84
+ };
85
+ save(snippet);
86
+ store.roomPost({
87
+ goalId: threadId,
88
+ author: "harness",
89
+ kind: "note",
90
+ text: `${nameOf(agent)} asks to see lines ${from} to ${to} of ${file.path} ("${words.slice(0, 120)}"). They stay on this computer until you share them.`,
91
+ });
92
+ notify(
93
+ `${nameOf(agent)} asks to see ${to - from + 1} lines of ${path.basename(file.path)}. Share or keep them in Harness.`,
94
+ snippet
95
+ );
96
+ return {
97
+ ok: true,
98
+ snippet: { id: snippet.id, path: file.path, from, to, state: "waiting" },
99
+ note: "The person decides whether to share these lines; you are told in the thread.",
100
+ };
101
+ };
102
+
103
+ /** The person shares or keeps a waiting snippet. */
104
+ const decide = (id, share) => {
105
+ const snippet = read(id);
106
+ if (!snippet) return { ok: false, error: "no such request" };
107
+ if (snippet.state !== "waiting")
108
+ return { ok: false, error: `already ${snippet.state}` };
109
+ const done = {
110
+ ...snippet,
111
+ state: share ? "shared" : "kept",
112
+ decidedAt: new Date().toISOString(),
113
+ };
114
+ save(done);
115
+ if (share) {
116
+ const post = store.roomPost({
117
+ goalId: snippet.thread,
118
+ author: "harness",
119
+ kind: "post",
120
+ mentions: [snippet.agent],
121
+ text: `@${snippet.agent} the person shared lines ${snippet.from} to ${snippet.to} of ${snippet.path}${snippet.masked ? ` (${snippet.masked} secret${snippet.masked === 1 ? "" : "s"} in them stay masked)` : ""}:\n\n\`\`\`\n${snippet.text}\n\`\`\``,
122
+ });
123
+ wake(snippet.thread, post, [snippet.agent]);
124
+ } else
125
+ store.roomPost({
126
+ goalId: snippet.thread,
127
+ author: "harness",
128
+ kind: "note",
129
+ text: `The person kept lines ${snippet.from} to ${snippet.to} of ${snippet.path} on this computer.`,
130
+ });
131
+ log(`snippet ${id}: ${done.state}`);
132
+ return { ok: true, snippet: { id, state: done.state } };
133
+ };
134
+
135
+ const list = ({ all = false } = {}) =>
136
+ JSON.parse(store.meta("snippets") || "[]")
137
+ .map(read)
138
+ .filter((snippet) => snippet && (all || snippet.state === "waiting"));
139
+ return { request, decide, list };
140
+ }