viewable-one 0.0.0-stage → 0.12.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.
- package/CHANGELOG.md +1460 -0
- package/LICENSE +21 -0
- package/README.md +755 -2
- package/SPAWN.md +2105 -0
- package/glytch.mjs +517 -0
- package/lib/acp.mjs +978 -0
- package/lib/admission.mjs +105 -0
- package/lib/agent-selection.mjs +123 -0
- package/lib/agents.mjs +913 -0
- package/lib/atomic.mjs +96 -0
- package/lib/auth.mjs +1387 -0
- package/lib/authenticatedHttp.mjs +68 -0
- package/lib/boardside.mjs +225 -0
- package/lib/child.mjs +1252 -0
- package/lib/cli-output.mjs +176 -0
- package/lib/cloudflared-release.json +25 -0
- package/lib/cloudflared.mjs +81 -0
- package/lib/codex-rpc.mjs +92 -0
- package/lib/codex.mjs +747 -0
- package/lib/codexApps.mjs +332 -0
- package/lib/codexPlanWindow.mjs +84 -0
- package/lib/codexToolInputs.mjs +139 -0
- package/lib/codingChecks.mjs +55 -0
- package/lib/compose.mjs +76 -0
- package/lib/computerApps.mjs +110 -0
- package/lib/conversationRules.mjs +45 -0
- package/lib/detect.mjs +605 -0
- package/lib/devserver.mjs +659 -0
- package/lib/digest.mjs +290 -0
- package/lib/doctor.mjs +464 -0
- package/lib/exitClass.mjs +64 -0
- package/lib/glytchFolder.mjs +105 -0
- package/lib/hosted.mjs +112 -0
- package/lib/imageAttachments.mjs +31 -0
- package/lib/init-connect.mjs +418 -0
- package/lib/kill.mjs +104 -0
- package/lib/logindaemon.mjs +976 -0
- package/lib/logs.mjs +120 -0
- package/lib/mcpjson.mjs +242 -0
- package/lib/memberPreview.mjs +165 -0
- package/lib/multiwatch.mjs +428 -0
- package/lib/pagePictures.mjs +396 -0
- package/lib/pageRoutes.mjs +67 -0
- package/lib/paths.mjs +9 -0
- package/lib/permissionAsk.mjs +418 -0
- package/lib/picker.mjs +300 -0
- package/lib/platform.mjs +60 -0
- package/lib/previewGateway.mjs +129 -0
- package/lib/previewTunnel.mjs +85 -0
- package/lib/procs.mjs +428 -0
- package/lib/project-mcp.mjs +288 -0
- package/lib/projectPaths.mjs +31 -0
- package/lib/refusal.mjs +70 -0
- package/lib/registry.mjs +204 -0
- package/lib/repomap.mjs +322 -0
- package/lib/restartPass.mjs +151 -0
- package/lib/retired-address.mjs +78 -0
- package/lib/rowsAnswers.mjs +389 -0
- package/lib/rowsEvents.mjs +86 -0
- package/lib/run-engine.mjs +16 -0
- package/lib/runner-health.mjs +57 -0
- package/lib/runner.mjs +1569 -0
- package/lib/runnerAdmission.mjs +35 -0
- package/lib/runnerApi.mjs +312 -0
- package/lib/runnerCompletion.mjs +406 -0
- package/lib/runnerLease.mjs +37 -0
- package/lib/runnerMaintenance.mjs +27 -0
- package/lib/runnerPace.mjs +283 -0
- package/lib/runnerPerformance.mjs +26 -0
- package/lib/runnerRealtime.mjs +340 -0
- package/lib/runnerRegister.mjs +96 -0
- package/lib/runnerSettings.mjs +40 -0
- package/lib/runnerSettlement.mjs +138 -0
- package/lib/runnerWorkspace.mjs +162 -0
- package/lib/serverSweep.mjs +90 -0
- package/lib/sharedPreview.mjs +292 -0
- package/lib/signIn.mjs +47 -0
- package/lib/spawn.mjs +330 -0
- package/lib/sweep.mjs +161 -0
- package/lib/sync.mjs +484 -0
- package/lib/uninstall-project.mjs +588 -0
- package/lib/uninstall.mjs +457 -0
- package/lib/update.mjs +302 -0
- package/lib/usageLimit.mjs +108 -0
- package/lib/windows-command.mjs +85 -0
- package/lib/worktree.mjs +1138 -0
- package/lib/writePolicy.mjs +45 -0
- package/package.json +58 -4
- package/spawn-runner.mjs +167 -0
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/** Admission requires durable completion delivery and a free slot. */
|
|
2
|
+
import { randomUUID } from "node:crypto";
|
|
3
|
+
import { acquireAdmission } from "./admission.mjs";
|
|
4
|
+
import { flushCompletions } from "./runnerSettlement.mjs";
|
|
5
|
+
import * as runnerApiModule from "./runnerApi.mjs";
|
|
6
|
+
|
|
7
|
+
const retryHint = (out) => (Number.isFinite(out.retryAfterMs) ? { retryAfterMs: out.retryAfterMs } : {});
|
|
8
|
+
|
|
9
|
+
export async function claimOne(root, project, settings) {
|
|
10
|
+
const runnerApi = settings.runnerApi ?? runnerApiModule;
|
|
11
|
+
const rowsOpts = { env: settings.env, fetchImpl: settings.fetchImpl };
|
|
12
|
+
const admission = await acquireAdmission(root);
|
|
13
|
+
if (!admission.ok) return { claimed: false, detail: admission.detail };
|
|
14
|
+
try {
|
|
15
|
+
// `transient` (ticket e8): the claim loop backs off on these. A saved
|
|
16
|
+
// completion that will not sync is almost always the server failing, and
|
|
17
|
+
// claiming is blocked until it lands either way.
|
|
18
|
+
if (!(await flushCompletions(project, settings))) return { claimed: false, transient: true, detail: "Saved completions are waiting to sync." };
|
|
19
|
+
// With no limit on how many run at once (the default) the count can never
|
|
20
|
+
// refuse, so it is not asked for: one call fewer per pass (#616).
|
|
21
|
+
if (Number.isFinite(settings.maxConcurrent)) {
|
|
22
|
+
const listed = await runnerApi.runs({ projectIds: [project.id], status: "running" }, rowsOpts);
|
|
23
|
+
if (!listed.ok) return { claimed: false, transient: listed.transient === true, ...retryHint(listed), detail: `Cannot verify running sessions: ${listed.error}` };
|
|
24
|
+
const running = (listed.runs ?? []).length;
|
|
25
|
+
if (running >= settings.maxConcurrent) return { claimed: false, detail: `${running} session(s) already running, and up to ${settings.maxConcurrent} run at once` };
|
|
26
|
+
}
|
|
27
|
+
const claimed = await runnerApi.claim({ projectIds: [project.id], runnerInstanceId: settings.runnerInstanceId ??= randomUUID() }, rowsOpts);
|
|
28
|
+
if (!claimed.ok) return { claimed: false, transient: claimed.transient === true, ...retryHint(claimed), detail: claimed.error };
|
|
29
|
+
if (!claimed.run) return { claimed: false, idle: true, detail: "nothing pending" };
|
|
30
|
+
return { claimed: true, run: claimed.run };
|
|
31
|
+
} finally {
|
|
32
|
+
await admission.release();
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
// THE ROWS CLIENT (brief 30, Move 2, 2026-09-11). Every `.glytch/` file the
|
|
2
|
+
// runner used to read or write becomes a row, reached over ONE Edge Function,
|
|
3
|
+
// `supabase/functions/board-runner/` - built in parallel against the same
|
|
4
|
+
// contract (brief 30, "Move 2 contract").
|
|
5
|
+
// This file is the bridge's whole half of that wire: one function per op,
|
|
6
|
+
// same bearer resolution as everything else in this package
|
|
7
|
+
// (lib/auth.mjs's hostedToken - a `glytch login` credential, or
|
|
8
|
+
// VIEWABLE_BOARD_TOKEN for a script), never PostgREST directly, because
|
|
9
|
+
// neither a `pmb_` nor a `pmo_` token is a Supabase JWT.
|
|
10
|
+
//
|
|
11
|
+
// THE FUNCTION DOES NOT EXIST YET when this file was written - another
|
|
12
|
+
// worker builds `supabase/functions/board-runner/` against the same
|
|
13
|
+
// contract, in parallel. This file is written against the CONTRACT, and its
|
|
14
|
+
// own tests stub the HTTP layer (`fetchImpl`) rather than talking to
|
|
15
|
+
// anything real, exactly the way lib/hosted.mjs's tests already do for
|
|
16
|
+
// board-mcp.
|
|
17
|
+
//
|
|
18
|
+
// URL RESOLUTION mirrors hostedConfig: `GLYTCH_RUNNER_URL` overrides
|
|
19
|
+
// everything (a local dev deploy, a staging function under a different
|
|
20
|
+
// path); otherwise it is `${appUrl}/functions/v1/board-runner`, derived from
|
|
21
|
+
// the same stored `appUrl` (or `GLYTCH_APP_URL`/the production default)
|
|
22
|
+
// hostedConfig already resolves for the MCP endpoint - one app origin, two
|
|
23
|
+
// functions under it.
|
|
24
|
+
//
|
|
25
|
+
// WHY ONE call() AND NOT ONE FETCH PER OP: every op answers the same
|
|
26
|
+
// envelope (`{ok:true, ...}` / `{ok:false, error}`), needs the same bearer,
|
|
27
|
+
// and gets the same one-retry-after-forced-refresh treatment a token aging
|
|
28
|
+
// out mid-run already gets in lib/hosted.mjs's callTool - a spawned run can
|
|
29
|
+
// sit inside this contract for up to two hours (spawn.mjs's MAX_WALL_MS), so
|
|
30
|
+
// an access token expiring partway through a run must not read as a hard
|
|
31
|
+
// failure the first time it happens.
|
|
32
|
+
|
|
33
|
+
import { DEFAULT_APP_URL, hostedConfig } from "./auth.mjs";
|
|
34
|
+
import { RETIRED_APP_URLS } from "./retired-address.mjs";
|
|
35
|
+
import { authenticatedJsonPost } from "./authenticatedHttp.mjs";
|
|
36
|
+
export { NOT_SIGNED_IN_HINT } from "./authenticatedHttp.mjs";
|
|
37
|
+
|
|
38
|
+
/** The hosted project's own address for `board-runner` (#813, report #795
|
|
39
|
+
* option a). The production app proxies `/functions/v1/` through its
|
|
40
|
+
* Worker; the runner calls the function directly instead, so its traffic
|
|
41
|
+
* never meets the Worker's rate limit. The bearer is unchanged: the function
|
|
42
|
+
* checks a token's resource against its own configured issuer, not against
|
|
43
|
+
* the host the request arrived on. */
|
|
44
|
+
export const HOSTED_RUNNER_URL = "https://edqgfvgbxmfvfahlofqd.supabase.co/functions/v1/board-runner";
|
|
45
|
+
|
|
46
|
+
/** Where the runner's own function lives: `GLYTCH_RUNNER_URL` wins outright
|
|
47
|
+
* (a local dev deploy, a staging function at a different path); the
|
|
48
|
+
* production app goes straight to the hosted project; any other app origin
|
|
49
|
+
* (a local stack) uses its own `board-runner` function, derived from the same
|
|
50
|
+
* `appUrl` the MCP endpoint's URL comes from. */
|
|
51
|
+
export function runnerApiUrlFrom(appUrl, env = process.env) {
|
|
52
|
+
const explicit = typeof (env.VIEWABLE_RUNNER_URL ?? env.GLYTCH_RUNNER_URL) === "string" ? (env.VIEWABLE_RUNNER_URL ?? env.GLYTCH_RUNNER_URL).trim() : "";
|
|
53
|
+
if (explicit !== "") return explicit;
|
|
54
|
+
if (appUrl === DEFAULT_APP_URL || RETIRED_APP_URLS.includes(appUrl)) return HOSTED_RUNNER_URL;
|
|
55
|
+
return `${appUrl}/functions/v1/board-runner`;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Turn whatever an error-shaped field carries into a string worth showing a
|
|
59
|
+
* person, never `[object Object]` (2026-09-21: seen all night in
|
|
60
|
+
* runner.log as `board-runner answered 502: [object Object]`, because a
|
|
61
|
+
* Supabase gateway error can put an OBJECT where this package always
|
|
62
|
+
* expected a string). A string passes through unchanged; an object hands
|
|
63
|
+
* back its own `.message` when that is a string (the shape a Supabase
|
|
64
|
+
* gateway error carries), else the object itself, stringified and capped at
|
|
65
|
+
* 400 characters like every other piece of raw response text this file
|
|
66
|
+
* reports. `undefined`/`null` stay `undefined`, so a caller can still fall
|
|
67
|
+
* through to the next candidate. */
|
|
68
|
+
function errorDetail(value) {
|
|
69
|
+
if (typeof value === "string") return value;
|
|
70
|
+
if (value && typeof value === "object") {
|
|
71
|
+
if (typeof value.message === "string") return value.message;
|
|
72
|
+
try {
|
|
73
|
+
return JSON.stringify(value).slice(0, 400);
|
|
74
|
+
} catch {
|
|
75
|
+
return String(value);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return undefined;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** How long a 429 or 503 asks us to wait, in ms, from its `Retry-After`
|
|
82
|
+
* header (seconds or an HTTP date). undefined when absent or unreadable.
|
|
83
|
+
* The runner sleeps at least this long before its next call (#616). */
|
|
84
|
+
export function retryAfterMs(value, now = Date.now()) {
|
|
85
|
+
if (typeof value !== "string" || value.trim() === "") return undefined;
|
|
86
|
+
const seconds = Number(value.trim());
|
|
87
|
+
if (Number.isFinite(seconds)) return seconds >= 0 ? seconds * 1000 : undefined;
|
|
88
|
+
const at = Date.parse(value);
|
|
89
|
+
return Number.isFinite(at) ? Math.max(0, at - now) : undefined;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** Cloudflare's Ray ID for a failed call: the `cf-ray` header, or the
|
|
93
|
+
* "Ray ID: ..." line on its own HTML error page. It is what Cloudflare asks
|
|
94
|
+
* for when a rate limit or block needs looking into (#616). */
|
|
95
|
+
export function cloudflareRayId(headers, text) {
|
|
96
|
+
const header = headers?.get?.("cf-ray");
|
|
97
|
+
if (typeof header === "string" && header.trim() !== "") return header.trim();
|
|
98
|
+
const match = typeof text === "string" ? text.match(/Ray ID:?\s*(?:<[^>]*>\s*)*([0-9a-f]{16})/i) : null;
|
|
99
|
+
return match ? match[1] : undefined;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const looksLikeHtml = (text) => typeof text === "string" && /^\s*<(?:!doctype|html)/i.test(text);
|
|
103
|
+
|
|
104
|
+
/** One call to the board-runner function. Never throws on a network or HTTP
|
|
105
|
+
* failure - every caller gets `{ok:false, error}` back and decides for
|
|
106
|
+
* itself whether that is worth logging, retrying on the next poll, or
|
|
107
|
+
* surfacing on a receipt. `op` and the rest of `body` are sent flat, per the
|
|
108
|
+
* contract's `{op, ...}` shape. */
|
|
109
|
+
async function call(op, body = {}, options = {}) {
|
|
110
|
+
const { env = process.env } = options;
|
|
111
|
+
const { appUrl } = await hostedConfig({ env });
|
|
112
|
+
const url = runnerApiUrlFrom(appUrl, env);
|
|
113
|
+
let result;
|
|
114
|
+
try {
|
|
115
|
+
result = await authenticatedJsonPost(url, { op, ...body }, options);
|
|
116
|
+
} catch (error) {
|
|
117
|
+
// "fetch failed" alone says nothing; undici puts the reason (ENOTFOUND,
|
|
118
|
+
// ECONNRESET, a TLS code) on `cause` (QA 2026-10-01, e8).
|
|
119
|
+
const cause = error?.cause?.code ?? error?.cause?.message;
|
|
120
|
+
// `transient` marks the failures the runner backs off on (ticket e8,
|
|
121
|
+
// lib/runnerPace.mjs): a network failure or timeout here, a 5xx or 429
|
|
122
|
+
// below. A 401 and every refusal leave it unset.
|
|
123
|
+
return { ok: false, transient: true, error: `Could not reach ${url}: ${error instanceof Error ? error.message : String(error)}${cause ? ` (${cause})` : ""}` };
|
|
124
|
+
}
|
|
125
|
+
if (result.authError) return { ok: false, error: result.authError };
|
|
126
|
+
|
|
127
|
+
const { response, text, payload } = result;
|
|
128
|
+
if (!response.ok) {
|
|
129
|
+
// A CLOUDFLARE PAGE (#616): a rate limit answers 429 with a whole HTML
|
|
130
|
+
// page, not JSON. Four hundred characters of markup told nobody
|
|
131
|
+
// anything; say what it is and keep the Ray ID, which is what Cloudflare
|
|
132
|
+
// needs to look the block up.
|
|
133
|
+
const rayId = cloudflareRayId(response.headers, text);
|
|
134
|
+
const html = !payload && looksLikeHtml(text);
|
|
135
|
+
const detail = errorDetail(payload?.error) ?? errorDetail(payload?.message)
|
|
136
|
+
?? (html ? (response.status === 429 ? "Cloudflare rate limited this computer" : "Cloudflare answered with an error page") : text.slice(0, 400));
|
|
137
|
+
// THE REQUEST ID (QA 2026-10-01, e8): ten 500s overnight read "Could not
|
|
138
|
+
// check that token" and nothing else. The id finds the call in the
|
|
139
|
+
// function's own logs, where board-runner now writes the cause.
|
|
140
|
+
const requestId = response.headers?.get?.("sb-request-id");
|
|
141
|
+
const transient = response.status >= 500 || response.status === 429;
|
|
142
|
+
const wait = transient ? retryAfterMs(response.headers?.get?.("retry-after")) : undefined;
|
|
143
|
+
const ids = [requestId ? `request ${requestId}` : "", rayId ? `Ray ID ${rayId}` : ""].filter(Boolean).join(", ");
|
|
144
|
+
return {
|
|
145
|
+
ok: false,
|
|
146
|
+
...(transient ? { transient: true } : {}),
|
|
147
|
+
...(wait !== undefined ? { retryAfterMs: wait } : {}),
|
|
148
|
+
error:
|
|
149
|
+
response.status === 401
|
|
150
|
+
? `${detail || "Unauthorized."} Sign in again with \`viewable login\`, or mint a new personal access token and set VIEWABLE_BOARD_TOKEN.`
|
|
151
|
+
: `board-runner answered ${response.status}${detail ? `: ${detail}` : ""}${ids ? ` (${ids})` : ""}${wait !== undefined ? `; retry after ${Math.ceil(wait / 1000)}s` : ""}`,
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
if (!payload || typeof payload !== "object" || Array.isArray(payload)) {
|
|
155
|
+
return { ok: false, error: `board-runner answered with unreadable JSON: ${text.slice(0, 400)}` };
|
|
156
|
+
}
|
|
157
|
+
if (payload.ok === false) {
|
|
158
|
+
return { ok: false, error: errorDetail(payload.error) ?? "board-runner refused the request." };
|
|
159
|
+
}
|
|
160
|
+
if (payload.ok !== true) return { ok: false, error: "board-runner returned an incomplete response." };
|
|
161
|
+
return payload;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// ---------- one function per op, per the Move 2 contract ----------
|
|
165
|
+
|
|
166
|
+
/** `register {runnerId, host, platform, presence, projects:[{path, name?}]}`
|
|
167
|
+
* -> `{projects:[{id, path, name}]}`. Upserts `runner_projects` on
|
|
168
|
+
* `(user, host, path)` and bumps `last_seen_at`. Called on start and every
|
|
169
|
+
* beat with EVERY folder this machine knows about (`~/.glytch/folders.json`),
|
|
170
|
+
* not only the folder a particular watch loop is polling - the upsert is
|
|
171
|
+
* idempotent, so two runner processes registering the same folder twice
|
|
172
|
+
* costs nothing. */
|
|
173
|
+
export function register({ runnerId, host, platform, presence, projects }, options = {}) {
|
|
174
|
+
return call("register", { runnerId, host, platform, presence, projects }, options);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/** `heartbeat {projectIds, presence?}` -> `{stops:[runId...], ops:[...]}`.
|
|
178
|
+
* One call per tick covers presence (bumps `last_seen_at` on those project
|
|
179
|
+
* rows), stop signals for every running run on them, and op signals
|
|
180
|
+
* (untaken `run_ops` rows) - replacing `.runner.json`'s beat, `<id>.stop`,
|
|
181
|
+
* and the merge/delete/relaunch/preview signal files all at once. */
|
|
182
|
+
export function heartbeat({ projectIds, presence, runnerInstanceId, activeRunIds } = {}, options = {}) {
|
|
183
|
+
return call("heartbeat", { projectIds, ...(presence !== undefined ? { presence } : {}), ...(runnerInstanceId !== undefined ? { runnerInstanceId } : {}), ...(activeRunIds !== undefined ? { activeRunIds } : {}) }, options);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/** `claim {projectIds}` -> `{run}` or `{run:null}`. The oldest approved,
|
|
187
|
+
* unclaimed, pending run on those projects, claimed atomically - replaces
|
|
188
|
+
* `listRequests`/`claimRequest`'s exclusive-create-on-disk race. `run.request`
|
|
189
|
+
* is the SpawnRequest a file used to carry; `run.project_id` names which
|
|
190
|
+
* registered project (and therefore which root) it belongs to. */
|
|
191
|
+
export function claim({ projectIds, runnerInstanceId }, options = {}) {
|
|
192
|
+
return call("claim", { projectIds, ...(runnerInstanceId !== undefined ? { runnerInstanceId } : {}) }, options);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/** `update {runId, patch}` -> ok, or `{ok:false, error:"result already
|
|
196
|
+
* written"}` when `patch.result` collides with the write-once rule. `patch`
|
|
197
|
+
* may carry `status`, `sidecar`, `server`, `finished_at`, `result` - this
|
|
198
|
+
* is the one call that replaces `<id>.session.json`, `<id>.server`, and
|
|
199
|
+
* `<id>.result.txt` all at once, plus the `startSession`/`endSession` ops
|
|
200
|
+
* files: the run row IS the start/end stamp now. */
|
|
201
|
+
export function update({ runId, patch }, options = {}) {
|
|
202
|
+
return call("update", { runId, patch }, options);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/** `events {runId, events:[{seq, kind, text}]}` -> insert, duplicates on
|
|
206
|
+
* `(run_id, seq)` ignored. Replaces the runner's own append-to-`<id>.log`
|
|
207
|
+
* streaming; `kind` is `"log"` or `"cut"` (an elision marker), matching
|
|
208
|
+
* `run_events.kind`'s check constraint. */
|
|
209
|
+
export function events({ runId, events }, options = {}) {
|
|
210
|
+
return call("events", { runId, events }, options);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/** `opTake {opId}` -> `{op}` or `{op:null}`. Sets `taken_at` if it was null
|
|
214
|
+
* and hands back the op row (or null if someone else already took it, or
|
|
215
|
+
* it does not exist/is not this user's) - the row equivalent of consuming a
|
|
216
|
+
* `.merge`/`.delete`/`.relaunch` signal file, or a preview request. */
|
|
217
|
+
export function opTake({ opId }, options = {}) {
|
|
218
|
+
return call("opTake", { opId }, options);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/** `opResult {opId, result}` -> ok. merge/delete/relaunch write-once;
|
|
222
|
+
* preview is updatable (its lifecycle ticks through starting/ready/offline/
|
|
223
|
+
* error on one op row rather than one receipt file per state). */
|
|
224
|
+
export function opResult({ opId, result }, options = {}) {
|
|
225
|
+
return call("opResult", { opId, result }, options);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/** `runs {projectIds, status?}` -> a list, for maintenance: the sweep's
|
|
229
|
+
* equivalent of scanning `inbox/spawn/` for finished ids, and late
|
|
230
|
+
* promotion's equivalent of finding a session's own row to resume. */
|
|
231
|
+
export function runs({ projectIds, status } = {}, options = {}) {
|
|
232
|
+
return call("runs", { projectIds, ...(status !== undefined ? { status } : {}) }, options);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/** `resume {runId, message, images?}` -> inserts an APPROVED child run row
|
|
236
|
+
* copying the parent's request with `resumeSessionId` set, and returns its
|
|
237
|
+
* id. This is Move 2's replacement for `promoteReplyToRequest`/
|
|
238
|
+
* `promoteLateReplies` filing a new request FILE that resumes a session:
|
|
239
|
+
* the runner cannot call the app's `run_request` RPC directly (it holds no
|
|
240
|
+
* Supabase JWT), so this one op does the whole thing server-side under the
|
|
241
|
+
* service role, the same way `claim` and `update` already do. Added to the
|
|
242
|
+
* contract here, dated, because a bare `promoteReplyToRequest`-shaped call
|
|
243
|
+
* would otherwise have no row-side equivalent to land on; see the brief's
|
|
244
|
+
* "Move 2 contract" section, amended 2026-09-11, for the twin of this
|
|
245
|
+
* comment on the function side.
|
|
246
|
+
*
|
|
247
|
+
* `images` (item 6, brief 30 Move 2b, same date): a late message's images,
|
|
248
|
+
* the same shape a reply file's `images` array carried, sent when the
|
|
249
|
+
* caller has any (`rowsAnswers.mjs`'s `promoteLateRows`). The op does not
|
|
250
|
+
* declare this field yet on its own side (its header says why:
|
|
251
|
+
* `session_messages` gaining an `images` column, and `collect_session`
|
|
252
|
+
* exposing it, are another worker's change, not landed on this branch) -
|
|
253
|
+
* sent anyway, because an unrecognised field in a POST body a Deno
|
|
254
|
+
* function does not destructure is simply absent from `body` there, never
|
|
255
|
+
* a hard failure; the day that worker's change lands, this call needs no
|
|
256
|
+
* edit to start actually delivering pictures. */
|
|
257
|
+
export function resume({ runId, message, images, unlessStopped }, options = {}) {
|
|
258
|
+
return call("resume", { runId, message, ...(images?.length > 0 ? { images } : {}), ...(unlessStopped ? { unlessStopped: true } : {}) }, options);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/** `presence {paths?}` -> `{projects:[{id, path, host, runner_id,
|
|
262
|
+
* last_seen_at}]}`. Read-only, added 2026-09-12 (brief 31, "doctor asks the
|
|
263
|
+
* board"): the one caller today is `lib/doctor.mjs`'s "daemon beating" row,
|
|
264
|
+
* which used to read a local mark file (`.glytch/inbox/spawn/.runner.json`)
|
|
265
|
+
* that nothing over rows has written since `register`/`heartbeat` replaced
|
|
266
|
+
* it - `runner_projects.last_seen_at` is the truth now, and this is the
|
|
267
|
+
* only op that reads it back to a caller. `paths`, when given, narrows the
|
|
268
|
+
* result to those folders instead of every project this account has ever
|
|
269
|
+
* registered on any machine.
|
|
270
|
+
*
|
|
271
|
+
* It also answers `account:{id, email?}` since 2026-09-14 - WHICH account
|
|
272
|
+
* this bearer actually reaches, as the endpoint that resolved the token
|
|
273
|
+
* sees it. A credential left behind on a previous account looks identical
|
|
274
|
+
* to one on the right account from this side of the wire, which is how two
|
|
275
|
+
* days of session reports landed on a board nobody was watching. See the
|
|
276
|
+
* block over `accountOf` in supabase/functions/board-runner/runner.mjs; the
|
|
277
|
+
* reader is `lib/doctor.mjs`'s "board sign-in" row, which now says who. */
|
|
278
|
+
export function presence({ paths } = {}, options = {}) {
|
|
279
|
+
return call("presence", { ...(paths !== undefined ? { paths } : {}) }, options);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/** `forget {projectIds}` -> `{deleted:[id...], kept:[id...]}`. THE DAEMON'S
|
|
283
|
+
* ACKNOWLEDGEMENT, added 2026-09-12 (ruling 3 on the Project panel's
|
|
284
|
+
* picker: "removing must be END TO END, not just hiding the row").
|
|
285
|
+
*
|
|
286
|
+
* A person clicks Remove in the app, which raises `forgotten_at` on the row
|
|
287
|
+
* through its own RPC; the next `heartbeat` hands this daemon a
|
|
288
|
+
* `{kind:"forget", project_id, payload:{path}}` op; the daemon drops that
|
|
289
|
+
* folder from `~/.glytch/folders.json` (lib/registry.mjs's `removeFolder`,
|
|
290
|
+
* which is also what makes multiwatch stop its watcher on the next registry
|
|
291
|
+
* read) and then calls this, and the row goes for good.
|
|
292
|
+
*
|
|
293
|
+
* It is the ONLY op here that ends something rather than recording it, and
|
|
294
|
+
* the server will not act on a project the app has not already flagged - so
|
|
295
|
+
* a daemon cannot forget a folder nobody asked it to, and this call is safe
|
|
296
|
+
* to retry: a second one simply finds nothing pending. */
|
|
297
|
+
export function forget({ projectIds }, options = {}) {
|
|
298
|
+
return call("forget", { projectIds }, options);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/** `realtime {}` -> `{url, anonKey, topic, expiresAt, renewAt}` (#811, #813).
|
|
302
|
+
* The project's public address and key, and this account's private topic,
|
|
303
|
+
* live until `expiresAt`. From `renewAt` the same ask hands out the next
|
|
304
|
+
* topic. An older function answers "Unknown op", and the runner keeps its
|
|
305
|
+
* 10 s beat (lib/runnerRealtime.mjs). */
|
|
306
|
+
export function realtime(options = {}) {
|
|
307
|
+
return call("realtime", {}, options);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/** Names-only discovery, requested by the person in Settings (#1132). */
|
|
311
|
+
export function computerAppsTake(body, options = {}) { return call("computerAppsTake", body, options); }
|
|
312
|
+
export function computerAppsResult(body, options = {}) { return call("computerAppsResult", body, options); }
|