@fungi.computer/computer 0.0.0-stage → 0.0.1

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 (91) hide show
  1. package/Dockerfile +85 -0
  2. package/LICENSE +21 -0
  3. package/README.md +228 -2
  4. package/container/computer-init.mjs +119 -0
  5. package/container/computer-start.sh +24 -0
  6. package/container/package.json +14 -0
  7. package/container/pnpm-lock.yaml +378 -0
  8. package/container/pnpm-workspace.yaml +5 -0
  9. package/container/resource-bounds.mjs +54 -0
  10. package/container/runtime-privileges.cc +37 -0
  11. package/container/runtime-privileges.mjs +11 -0
  12. package/container/terminal-assets/README.md +35 -0
  13. package/container/terminal-assets/bashrc +49 -0
  14. package/container/terminal-assets/git-prompt.sh +586 -0
  15. package/container/terminal-protocol.mjs +58 -0
  16. package/container/terminal-runtime.mjs +10 -0
  17. package/container/terminal-server.mjs +243 -0
  18. package/dist/apply-patch/LICENSE.codex +201 -0
  19. package/dist/apply-patch/build.mjs +98 -0
  20. package/dist/apply-patch/cache.mjs +168 -0
  21. package/dist/apply-patch/compile.mjs +199 -0
  22. package/dist/apply-patch/docker/darwin.Dockerfile +15 -0
  23. package/dist/apply-patch/docker/linux-arm64.Dockerfile +38 -0
  24. package/dist/apply-patch/docker/linux-x64.Dockerfile +23 -0
  25. package/dist/apply-patch/main.rs +320 -0
  26. package/dist/apply-patch/notices.mjs +44 -0
  27. package/dist/apply-patch/pin.mjs +8 -0
  28. package/dist/apply-patch/toolchain.mjs +39 -0
  29. package/dist/computer-backends.d.ts +94 -0
  30. package/dist/computer-backends.js +103 -0
  31. package/dist/computerd.cjs +18010 -0
  32. package/dist/container-policy.d.ts +7 -0
  33. package/dist/container-policy.js +24 -0
  34. package/dist/container-system-trust.d.ts +8 -0
  35. package/dist/container-system-trust.js +66 -0
  36. package/dist/egress-policy.d.ts +89 -0
  37. package/dist/egress-policy.js +240 -0
  38. package/dist/execution-result.d.ts +3 -0
  39. package/dist/execution-result.js +28 -0
  40. package/dist/files-patch.d.ts +20 -0
  41. package/dist/files-patch.js +108 -0
  42. package/dist/files.d.ts +75 -0
  43. package/dist/files.js +136 -0
  44. package/dist/fleet/admission.d.ts +14 -0
  45. package/dist/fleet/admission.js +10 -0
  46. package/dist/fleet/backing.d.ts +52 -0
  47. package/dist/fleet/backing.js +242 -0
  48. package/dist/fleet/computers.d.ts +62 -0
  49. package/dist/fleet/computers.js +35 -0
  50. package/dist/fleet/index.d.ts +11 -0
  51. package/dist/fleet/index.js +11 -0
  52. package/dist/fleet/inventory-wire.d.ts +29 -0
  53. package/dist/fleet/inventory-wire.js +15 -0
  54. package/dist/fleet/inventory.d.ts +24 -0
  55. package/dist/fleet/inventory.js +152 -0
  56. package/dist/fleet/lifecycle.d.ts +99 -0
  57. package/dist/fleet/lifecycle.js +423 -0
  58. package/dist/fleet/runtime-usage.d.ts +50 -0
  59. package/dist/fleet/runtime-usage.js +145 -0
  60. package/dist/fleet/slots.d.ts +30 -0
  61. package/dist/fleet/slots.js +46 -0
  62. package/dist/fleet/wire.d.ts +228 -0
  63. package/dist/fleet/wire.js +127 -0
  64. package/dist/host-config.d.ts +12 -0
  65. package/dist/host-config.js +10 -0
  66. package/dist/index.d.ts +282 -0
  67. package/dist/index.js +1237 -0
  68. package/dist/light-shell.d.ts +44 -0
  69. package/dist/light-shell.js +376 -0
  70. package/dist/local-backend.d.ts +52 -0
  71. package/dist/local-backend.js +143 -0
  72. package/dist/metered-container-host.d.ts +26 -0
  73. package/dist/metered-container-host.js +48 -0
  74. package/dist/on-box-vm.d.ts +119 -0
  75. package/dist/on-box-vm.js +294 -0
  76. package/dist/operation-contract.d.ts +456 -0
  77. package/dist/operation-contract.js +452 -0
  78. package/dist/operation-record.d.ts +50 -0
  79. package/dist/operation-record.js +162 -0
  80. package/dist/preview.d.ts +55 -0
  81. package/dist/preview.js +151 -0
  82. package/dist/process-inventory.d.ts +80 -0
  83. package/dist/process-inventory.js +427 -0
  84. package/dist/pull-pending.d.ts +26 -0
  85. package/dist/pull-pending.js +35 -0
  86. package/dist/team-candidate-builder.mjs +28170 -0
  87. package/dist/transport-backend.d.ts +3 -0
  88. package/dist/transport-backend.js +99 -0
  89. package/dist/workspace-stream.d.ts +2 -0
  90. package/dist/workspace-stream.js +31 -0
  91. package/package.json +126 -3
@@ -0,0 +1,427 @@
1
+ var __addDisposableResource = (this && this.__addDisposableResource) || function (env, value, async) {
2
+ if (value !== null && value !== void 0) {
3
+ if (typeof value !== "object" && typeof value !== "function") throw new TypeError("Object expected.");
4
+ var dispose, inner;
5
+ if (async) {
6
+ if (!Symbol.asyncDispose) throw new TypeError("Symbol.asyncDispose is not defined.");
7
+ dispose = value[Symbol.asyncDispose];
8
+ }
9
+ if (dispose === void 0) {
10
+ if (!Symbol.dispose) throw new TypeError("Symbol.dispose is not defined.");
11
+ dispose = value[Symbol.dispose];
12
+ if (async) inner = dispose;
13
+ }
14
+ if (typeof dispose !== "function") throw new TypeError("Object not disposable.");
15
+ if (inner) dispose = function() { try { inner.call(this); } catch (e) { return Promise.reject(e); } };
16
+ env.stack.push({ value: value, dispose: dispose, async: async });
17
+ }
18
+ else if (async) {
19
+ env.stack.push({ async: true });
20
+ }
21
+ return value;
22
+ };
23
+ var __disposeResources = (this && this.__disposeResources) || (function (SuppressedError) {
24
+ return function (env) {
25
+ function fail(e) {
26
+ env.error = env.hasError ? new SuppressedError(e, env.error, "An error was suppressed during disposal.") : e;
27
+ env.hasError = true;
28
+ }
29
+ var r, s = 0;
30
+ function next() {
31
+ while (r = env.stack.pop()) {
32
+ try {
33
+ if (!r.async && s === 1) return s = 0, env.stack.push(r), Promise.resolve().then(next);
34
+ if (r.dispose) {
35
+ var result = r.dispose.call(r.value);
36
+ if (r.async) return s |= 2, Promise.resolve(result).then(next, function(e) { fail(e); return next(); });
37
+ }
38
+ else s |= 1;
39
+ }
40
+ catch (e) {
41
+ fail(e);
42
+ }
43
+ }
44
+ if (s === 1) return env.hasError ? Promise.reject(env.error) : Promise.resolve();
45
+ if (env.hasError) throw env.error;
46
+ }
47
+ return next();
48
+ };
49
+ })(typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) {
50
+ var e = new Error(message);
51
+ return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
52
+ });
53
+ import { z } from "zod";
54
+ import { computerProcessSchema, processStartInputSchema, mintComputerProcessId, ComputerProcessLimitError, ComputerPortReservedError, ComputerPortConflictError, ComputerPortLimitError, isComputerReservedPort, maximumExposedPorts, maximumProcessRecords, maximumRunningProcesses, } from "./operation-contract.js";
55
+ import { ComputerOperationConflictError } from "./operation-record.js";
56
+ function toProcess(row) {
57
+ return computerProcessSchema.parse({
58
+ id: row.id,
59
+ command: row.command,
60
+ cwd: row.cwd,
61
+ port: row.port,
62
+ state: row.state,
63
+ exitCode: row.exit_code,
64
+ sync: row.sync,
65
+ startedAt: row.started_at,
66
+ endedAt: row.ended_at,
67
+ });
68
+ }
69
+ /**
70
+ * Bump when `computer_processes`' columns change. Pre-release data is
71
+ * disposable (no migrations before launch): a mismatch resets the table
72
+ * to the fresh schema instead of a strict-table write throwing on an old
73
+ * row shape. Tracked in a dedicated row, not `PRAGMA user_version`: that
74
+ * pragma returns SQLITE_AUTH against a real Durable Object's SQLite
75
+ * storage (confirmed in workerd; node:sqlite does not enforce the same
76
+ * sandbox, so a unit test against it alone would hide this).
77
+ */
78
+ const processSchemaVersion = 2;
79
+ /** Computer-owned replay identities; computerd owns execution and exit observations. */
80
+ export class ComputerProcessInventory {
81
+ #storage;
82
+ #now;
83
+ constructor(storage, now) {
84
+ this.#storage = storage;
85
+ this.#now = now;
86
+ storage.transactionSync(() => {
87
+ storage.sql.exec("CREATE TABLE IF NOT EXISTS computer_schema_meta (id INTEGER PRIMARY KEY CHECK (id = 0), version INTEGER NOT NULL) STRICT");
88
+ const seenVersion = storage.sql
89
+ .exec("SELECT version FROM computer_schema_meta WHERE id = 0")
90
+ .toArray()[0]?.version ?? 0;
91
+ if (seenVersion !== processSchemaVersion) {
92
+ storage.sql.exec("DROP TABLE IF EXISTS computer_processes");
93
+ storage.sql.exec("DROP TABLE IF EXISTS computer_exposed_ports");
94
+ storage.sql.exec("DELETE FROM computer_schema_meta WHERE id = 0");
95
+ storage.sql.exec("INSERT INTO computer_schema_meta (id, version) VALUES (0, ?)", processSchemaVersion);
96
+ }
97
+ storage.sql.exec("CREATE TABLE IF NOT EXISTS computer_processes (id TEXT PRIMARY KEY, request_scope TEXT NOT NULL, request_id TEXT NOT NULL, input_hash TEXT NOT NULL, command TEXT NOT NULL, cwd TEXT, port INTEGER, state TEXT NOT NULL CHECK (state IN ('running','exited')), exit_code INTEGER, sync TEXT NOT NULL DEFAULT 'unknown' CHECK (sync IN ('synced','pending','unknown')), started_at INTEGER NOT NULL, ended_at INTEGER, CHECK ((state = 'running') = (ended_at IS NULL)), UNIQUE(request_scope, request_id)) STRICT");
98
+ storage.sql.exec("CREATE TABLE IF NOT EXISTS computer_exposed_ports (port INTEGER PRIMARY KEY CHECK (port BETWEEN 1 AND 65535), process_id TEXT) STRICT");
99
+ });
100
+ }
101
+ #matching(input, inputHash, accept) {
102
+ const row = this.#storage.sql
103
+ .exec("SELECT * FROM computer_processes WHERE request_scope = ? AND request_id = ?", input.requestScope, input.requestId)
104
+ .toArray()[0];
105
+ if (row === undefined)
106
+ return undefined;
107
+ accept?.();
108
+ if (row.input_hash !== inputHash)
109
+ throw new ComputerOperationConflictError();
110
+ const process = toProcess(row);
111
+ // Admission may still be dispatching: a receipt alone does not prove liveness.
112
+ return process.state === "running"
113
+ ? { ...process, state: "uncertain" }
114
+ : process;
115
+ }
116
+ /** A replay does not boot a backend or redispatch an exited command. */
117
+ replay(input, inputHash, accept) {
118
+ return this.#storage.transactionSync(() => {
119
+ return this.#matching(input, inputHash, accept);
120
+ });
121
+ }
122
+ /** Reserve the process and only a newly created exposure in one transaction. */
123
+ admit(input, inputHash, accept) {
124
+ return this.#storage.transactionSync(() => {
125
+ accept();
126
+ const existing = this.#matching(input, inputHash);
127
+ if (existing !== undefined)
128
+ return { process: existing, created: false };
129
+ const now = this.#now();
130
+ const running = this.#storage.sql
131
+ .exec("SELECT COUNT(*) AS count FROM computer_processes WHERE state = 'running' AND request_scope = ?", input.requestScope)
132
+ .toArray()[0]?.count ?? maximumRunningProcesses;
133
+ if (running >= maximumRunningProcesses)
134
+ throw new ComputerProcessLimitError();
135
+ const id = mintComputerProcessId();
136
+ if (input.port !== undefined) {
137
+ if (isComputerReservedPort(input.port))
138
+ throw new ComputerPortReservedError(input.port);
139
+ const exposure = this.#storage.sql
140
+ .exec("SELECT process_id FROM computer_exposed_ports WHERE port = ?", input.port)
141
+ .toArray()[0];
142
+ if (exposure !== undefined && exposure.process_id !== null)
143
+ throw new ComputerPortConflictError();
144
+ if (exposure === undefined) {
145
+ const ports = this.#storage.sql
146
+ .exec("SELECT COUNT(*) AS count FROM computer_exposed_ports")
147
+ .toArray()[0]?.count ?? maximumExposedPorts;
148
+ if (ports >= maximumExposedPorts)
149
+ throw new ComputerPortLimitError();
150
+ this.#storage.sql.exec("INSERT INTO computer_exposed_ports (port, process_id) VALUES (?, ?)", input.port, id);
151
+ }
152
+ }
153
+ this.#storage.sql.exec("INSERT INTO computer_processes (id, request_scope, request_id, input_hash, command, cwd, port, state, started_at) VALUES (?, ?, ?, ?, ?, ?, ?, 'running', ?)", id, input.requestScope, input.requestId, inputHash, input.command, input.cwd ?? null, input.port ?? null, now);
154
+ // Each scope retains its own newest 64 rows, including running processes.
155
+ this.#storage.sql.exec("DELETE FROM computer_processes WHERE request_scope = ? AND state = 'exited' AND id NOT IN (SELECT id FROM computer_processes WHERE request_scope = ? AND state = 'exited' ORDER BY started_at DESC, id DESC LIMIT MAX(0, ? - (SELECT COUNT(*) FROM computer_processes WHERE request_scope = ? AND state = 'running')))", input.requestScope, input.requestScope, maximumProcessRecords, input.requestScope);
156
+ return { process: this.#require(id), created: true };
157
+ });
158
+ }
159
+ exited(id, exitCode, sync = "unknown") {
160
+ return this.#storage.transactionSync(() => {
161
+ this.#storage.sql.exec("UPDATE computer_processes SET state = 'exited', exit_code = ?, sync = ?, ended_at = ? WHERE id = ? AND state = 'running'", exitCode, sync, this.#now(), id);
162
+ this.#storage.sql.exec("DELETE FROM computer_exposed_ports WHERE process_id = ?", id);
163
+ return this.#require(id);
164
+ });
165
+ }
166
+ /** Retain a failed dispatch receipt: retrying that key cannot create new work. */
167
+ release(id) {
168
+ const row = this.get(id);
169
+ if (row?.state === "running")
170
+ this.exited(id, null);
171
+ }
172
+ /**
173
+ * Upgrade an already-exited row's sync verdict once a background drain
174
+ * (one that gave up synchronously at its deadline) resolves it. A
175
+ * no-op if the row was evicted or never exited: pending rows must be
176
+ * updatable here, not only running ones.
177
+ */
178
+ resync(id, sync) {
179
+ this.#storage.transactionSync(() => {
180
+ this.#storage.sql.exec("UPDATE computer_processes SET sync = ? WHERE id = ? AND state = 'exited'", sync, id);
181
+ });
182
+ }
183
+ get(id) {
184
+ const row = this.#storage.sql
185
+ .exec("SELECT * FROM computer_processes WHERE id = ?", id)
186
+ .toArray()[0];
187
+ return row === undefined ? undefined : toProcess(row);
188
+ }
189
+ list() {
190
+ return this.#storage.sql
191
+ .exec("SELECT * FROM computer_processes ORDER BY started_at DESC, id DESC")
192
+ .toArray()
193
+ .map(toProcess);
194
+ }
195
+ #require(id) {
196
+ const found = this.get(id);
197
+ if (found === undefined)
198
+ throw new Error(`Computer process record is missing: ${id}`);
199
+ return found;
200
+ }
201
+ }
202
+ /** How long a tail read may stay silent before the process counts as live. */
203
+ export const liveProbeMs = 750;
204
+ const lostCodeSchema = z.object({ code: z.enum(["EEXEC_LOST", "ENOENT"]) });
205
+ const busyCodeSchema = z.object({ code: z.literal("EEXEC_BUSY") });
206
+ /**
207
+ * A replaced container runtime (the package's fence) or an execution computerd
208
+ * no longer holds (its retention elapsed) both prove the process is gone.
209
+ */
210
+ function lostExecution(error) {
211
+ return lostCodeSchema.safeParse(error).success;
212
+ }
213
+ /** Classify a failed attach: busy is live, lost is gone, anything else is unknown. */
214
+ function failedObservation(error) {
215
+ if (error === undefined)
216
+ return { state: "uncertain" };
217
+ // computerd admits one live subscriber; the Computer's own watcher holding
218
+ // it proves the execution is still live.
219
+ if (busyCodeSchema.safeParse(error).success)
220
+ return { state: "running" };
221
+ return lostExecution(error)
222
+ ? { state: "exited", exitCode: null, sync: "unknown" }
223
+ : { state: "uncertain" };
224
+ }
225
+ async function readExitCode(runtime, id) {
226
+ const env_1 = { stack: [], error: void 0, hasError: false };
227
+ try {
228
+ const replay = __addDisposableResource(env_1, await runtime.getExec(id, { resume: "full" }), false);
229
+ const reader = replay.getReader();
230
+ let exitCode = null;
231
+ for (;;) {
232
+ const next = await reader.read();
233
+ if (next.done)
234
+ break;
235
+ if (next.value.name === "exit")
236
+ exitCode = next.value.code;
237
+ }
238
+ return exitCode;
239
+ }
240
+ catch (e_1) {
241
+ env_1.error = e_1;
242
+ env_1.hasError = true;
243
+ }
244
+ finally {
245
+ __disposeResources(env_1);
246
+ }
247
+ }
248
+ /**
249
+ * How long a drain may keep waiting for EOF once an exit has already been
250
+ * observed, before giving up: a backgrounded child (for example
251
+ * `node server.js &`) can hold the transport open long past its own
252
+ * parent's exit, and nothing here may hang on that forever.
253
+ */
254
+ export const postExitDrainDeadlineMs = 2_000;
255
+ const drainTimedOut = Symbol("computer-process-drain-timed-out");
256
+ /**
257
+ * Keep reading past the deadline so a slow-but-real pull still lands as
258
+ * "synced" instead of leaving the row stuck at "pending" forever; an
259
+ * error gives up without claiming anything new. The caller already
260
+ * handed off disposal to whoever awaits this.
261
+ */
262
+ async function drainInBackground(reader) {
263
+ try {
264
+ for (;;) {
265
+ const next = await reader.read();
266
+ if (next.done)
267
+ return "synced";
268
+ }
269
+ }
270
+ catch (cause) {
271
+ console.error("Computer process background drain failed", cause);
272
+ return "pending";
273
+ }
274
+ }
275
+ /**
276
+ * Keep reading `reader` past an already-observed exit, bounded: the read
277
+ * that reaches EOF is the one that runs computerd's post-command pull
278
+ * (the contract in computer-execution-backends.md), so the ordinary case
279
+ * still waits for it and reports "synced". If the deadline passes
280
+ * first, hands back a `settleLater` promise that keeps draining in the
281
+ * background and resolves once that real EOF finally arrives -- the
282
+ * caller must keep the reader alive for it (not dispose it) and is
283
+ * responsible for keeping its own host alive too (a Durable Object's
284
+ * `waitUntil`), and for applying the eventual upgrade. This is also what
285
+ * keeps a backgrounded child holding the transport open (for example
286
+ * `node server.js &`) from hanging the caller synchronously.
287
+ */
288
+ async function drainPastExit(reader) {
289
+ let timer;
290
+ const expired = new Promise((resolve) => {
291
+ timer = setTimeout(() => {
292
+ resolve(drainTimedOut);
293
+ }, postExitDrainDeadlineMs);
294
+ });
295
+ try {
296
+ for (;;) {
297
+ const next = await Promise.race([reader.read(), expired]);
298
+ if (next === drainTimedOut)
299
+ return { sync: "pending", settleLater: drainInBackground(reader) };
300
+ if (next.done)
301
+ return { sync: "synced" };
302
+ }
303
+ }
304
+ finally {
305
+ clearTimeout(timer);
306
+ }
307
+ }
308
+ /**
309
+ * Observe one execution without consuming its output: a tail attach emits
310
+ * nothing for a live process, closes at once for an exited one, and fails
311
+ * for one computerd no longer holds.
312
+ */
313
+ export async function observeProcess(runtime, id) {
314
+ let tail;
315
+ try {
316
+ tail = await runtime.getExec(id, { resume: "tail" });
317
+ }
318
+ catch (error) {
319
+ return failedObservation(error instanceof Error ? error : undefined);
320
+ }
321
+ const reader = tail.getReader();
322
+ // Not a `using` disposal: the "pending" branch below hands the reader
323
+ // to a background continuation and must not dispose it out from under
324
+ // that continuation.
325
+ const dispose = () => {
326
+ reader.releaseLock();
327
+ tail[Symbol.dispose]();
328
+ };
329
+ const silent = Symbol("silent");
330
+ let timer;
331
+ let settleLater;
332
+ try {
333
+ const first = await Promise.race([
334
+ reader.read(),
335
+ new Promise((resolve) => {
336
+ timer = setTimeout(() => {
337
+ resolve(silent);
338
+ }, liveProbeMs);
339
+ }),
340
+ ]);
341
+ if (first === silent)
342
+ return { state: "running" };
343
+ if (first.done) {
344
+ try {
345
+ return {
346
+ state: "exited",
347
+ exitCode: await readExitCode(runtime, id),
348
+ sync: "unknown",
349
+ };
350
+ }
351
+ catch (cause) {
352
+ console.error("Computer process observation cleanup failed", cause);
353
+ return { state: "exited", exitCode: null, sync: "unknown" };
354
+ }
355
+ }
356
+ if (first.value.name === "exit") {
357
+ // Keep draining to EOF, bounded, before reporting exited: that read
358
+ // is the one that runs computerd's post-command Workspace pull.
359
+ // Returning immediately disposes the reader first (the `finally`
360
+ // below cancels it), which skips that pull instead of waiting for
361
+ // it; waiting unboundedly can hang on a backgrounded child that
362
+ // still holds the transport open past this execution's own exit.
363
+ const exitCode = first.value.code;
364
+ const drained = await drainPastExit(reader);
365
+ settleLater = drained.settleLater;
366
+ if (settleLater !== undefined)
367
+ return { state: "exited", exitCode, sync: drained.sync, settleLater };
368
+ return { state: "exited", exitCode, sync: drained.sync };
369
+ }
370
+ return { state: "running" };
371
+ }
372
+ catch (error) {
373
+ return failedObservation(error instanceof Error ? error : undefined);
374
+ }
375
+ finally {
376
+ clearTimeout(timer);
377
+ if (settleLater === undefined) {
378
+ await reader.cancel().catch((cause) => {
379
+ console.error("Computer process observation cleanup failed", cause);
380
+ return undefined;
381
+ });
382
+ dispose();
383
+ }
384
+ else {
385
+ void settleLater.finally(dispose);
386
+ }
387
+ }
388
+ }
389
+ /**
390
+ * Drain `run` to its exit, bounded past that exit the same way
391
+ * `observeProcess` is, and determine whether its files reached the
392
+ * Workspace. `undefined` if the stream broke or was replaced before any
393
+ * exit was observed -- there is nothing to record.
394
+ */
395
+ export async function drainProcessExit(run) {
396
+ const reader = run.getReader();
397
+ const dispose = () => {
398
+ reader.releaseLock();
399
+ run[Symbol.dispose]();
400
+ };
401
+ let exitCode;
402
+ let sync = "unknown";
403
+ let settleLater;
404
+ try {
405
+ for (;;) {
406
+ const next = await reader.read();
407
+ if (next.done)
408
+ break;
409
+ if (next.value.name === "exit") {
410
+ exitCode = next.value.code;
411
+ const drained = await drainPastExit(reader);
412
+ sync = drained.sync;
413
+ settleLater = drained.settleLater;
414
+ break;
415
+ }
416
+ }
417
+ }
418
+ finally {
419
+ if (settleLater === undefined)
420
+ dispose();
421
+ }
422
+ if (exitCode === undefined)
423
+ return undefined;
424
+ if (settleLater !== undefined)
425
+ return { exitCode, sync, settleLater: settleLater.finally(dispose) };
426
+ return { exitCode, sync };
427
+ }
@@ -0,0 +1,26 @@
1
+ import type { SyncRetryScheduler } from "@fungi.computer/cloudflare-computer";
2
+ /**
3
+ * A faithful, in-memory delegate for `@cloudflare/computer`'s own
4
+ * `retryScheduler` option: the library's documented host-owned durable
5
+ * storage boundary for a post-command pull that failed (`onPullPending`,
6
+ * from `runPostPull`'s catch branch -- "the workspace library cannot own
7
+ * a Durable Object alarm"). `get`, `schedule` and `clear` store and return
8
+ * the library's real intents, so its own dedup-by-runtimeId and
9
+ * exponential backoff (`Workspace.retryPendingSync`) stay correct for
10
+ * whoever wires an alarm to drive it. A wrapper that invents its own
11
+ * signal instead -- for example, one whose `get` always reports no
12
+ * existing intent, to make the library call `schedule` on every failure
13
+ * -- would break that dedup and backoff for an actual retry.
14
+ *
15
+ * This Computer does not yet call `retryPendingSync` from anywhere (no
16
+ * Durable Object alarm is wired to it), so a pending backend only clears
17
+ * when that retry runs and succeeds; it does not self-heal if the
18
+ * underlying problem goes away on its own. It is also in-memory only: it
19
+ * does not survive a Durable Object eviction. Both are known, bounded
20
+ * limitations, not a second compatibility path -- the real fix is wiring
21
+ * an alarm to `retryPendingSync`, a separate change.
22
+ */
23
+ export declare class PullPendingTracker {
24
+ #private;
25
+ readonly scheduler: SyncRetryScheduler;
26
+ }
@@ -0,0 +1,35 @@
1
+ /**
2
+ * A faithful, in-memory delegate for `@cloudflare/computer`'s own
3
+ * `retryScheduler` option: the library's documented host-owned durable
4
+ * storage boundary for a post-command pull that failed (`onPullPending`,
5
+ * from `runPostPull`'s catch branch -- "the workspace library cannot own
6
+ * a Durable Object alarm"). `get`, `schedule` and `clear` store and return
7
+ * the library's real intents, so its own dedup-by-runtimeId and
8
+ * exponential backoff (`Workspace.retryPendingSync`) stay correct for
9
+ * whoever wires an alarm to drive it. A wrapper that invents its own
10
+ * signal instead -- for example, one whose `get` always reports no
11
+ * existing intent, to make the library call `schedule` on every failure
12
+ * -- would break that dedup and backoff for an actual retry.
13
+ *
14
+ * This Computer does not yet call `retryPendingSync` from anywhere (no
15
+ * Durable Object alarm is wired to it), so a pending backend only clears
16
+ * when that retry runs and succeeds; it does not self-heal if the
17
+ * underlying problem goes away on its own. It is also in-memory only: it
18
+ * does not survive a Durable Object eviction. Both are known, bounded
19
+ * limitations, not a second compatibility path -- the real fix is wiring
20
+ * an alarm to `retryPendingSync`, a separate change.
21
+ */
22
+ export class PullPendingTracker {
23
+ #intents = new Map();
24
+ scheduler = {
25
+ get: (backend) => Promise.resolve(this.#intents.get(backend)),
26
+ schedule: (intent) => {
27
+ this.#intents.set(intent.backend, intent);
28
+ return Promise.resolve();
29
+ },
30
+ clear: (backend) => {
31
+ this.#intents.delete(backend);
32
+ return Promise.resolve();
33
+ },
34
+ };
35
+ }