@fungi.computer/trellis 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.
@@ -0,0 +1,141 @@
1
+ import { workflowLibraryGill } from "./gills.js";
2
+ import * as z from "zod";
3
+ import { memberInboxItemSchema } from "@fungi.computer/agar/inbox";
4
+ import { catalogReplySchema as catalogReply } from "./catalog.js";
5
+ import { scheduleMethods } from "./schedule-methods.js";
6
+ import { toolFamilyMethods } from "./tool-family.js";
7
+ import { libraryMethods } from "./library-methods.js";
8
+ import * as Schema from "effect/Schema";
9
+ import { reviewAnswerSchema } from "@fungi.computer/braid";
10
+ import { defineAppBackend, defineAppMethod, } from "@fungi.computer/agar/backend-definition";
11
+ import { chatsReplySchema, answerReviewReply, cancelReplySchema, catalogInputSchema, runListItemSchema, runsReplySchema, statusReply, viewerReplySchema, } from "@fungi.computer/workflows-wire";
12
+ import { workflowIdSchema } from "@fungi.computer/braid";
13
+ const identity = z.string().min(1).max(256);
14
+ const inboxPage = z.object({ items: z.array(z.json()).max(200) });
15
+ const granted = (context) => {
16
+ if (!context.env.WORKFLOWS)
17
+ throw new Error("Workflow access is not granted");
18
+ return context.env.WORKFLOWS;
19
+ };
20
+ export const appBackend = defineAppBackend({
21
+ sync: { method: "continuePublication" },
22
+ gills: [workflowLibraryGill],
23
+ toolFamily: { name: "flows", list: "listWorkflowTools", call: "runWorkflow" },
24
+ methods: {
25
+ ...libraryMethods,
26
+ ...toolFamilyMethods,
27
+ listRuns: defineAppMethod({
28
+ description: "List up to fifty runs from this installation, newest first.",
29
+ effect: "read",
30
+ requires: ["workflow.run"],
31
+ input: z.strictObject({ workflowId: workflowIdSchema.optional() }),
32
+ output: z.array(runListItemSchema).max(50),
33
+ handler: async (input, context) => {
34
+ const indexes = context.indexes;
35
+ if (!indexes)
36
+ throw new Error("Workflow indexes unavailable");
37
+ const reply = runsReplySchema.parse(await granted(context).runs(input.workflowId === undefined
38
+ ? {}
39
+ : { workflowId: input.workflowId }));
40
+ if ("error" in reply)
41
+ throw new Error("Workflow run list unavailable");
42
+ return reply.runs.map((row) => {
43
+ const copied = indexes.publications.read(`${row.workflowId}:${row.commit}`);
44
+ const published = copied.kind === "ready" &&
45
+ copied.head === row.commit &&
46
+ copied.value.commit === row.commit
47
+ ? copied.value
48
+ : null;
49
+ return {
50
+ ...row,
51
+ title: published?.definition.title ?? null,
52
+ version: published?.version ?? null,
53
+ };
54
+ });
55
+ },
56
+ }),
57
+ viewerId: defineAppMethod({
58
+ description: "The calling member's own id, so the Runs UI can tell its own runs from a teammate's.",
59
+ effect: "read",
60
+ requires: [],
61
+ input: z.strictObject({}),
62
+ output: viewerReplySchema,
63
+ handler: (_input, _context, caller) => caller.actor.kind === "member"
64
+ ? { memberId: caller.actor.memberId }
65
+ : { error: "not_granted" },
66
+ }),
67
+ ...scheduleMethods,
68
+ listChats: defineAppMethod({
69
+ description: "List your existing chats (Agent Sessions) that a run or trigger may report its result to.",
70
+ effect: "read",
71
+ requires: ["agent.read"],
72
+ input: z.strictObject({}),
73
+ output: chatsReplySchema,
74
+ handler: async (_input, context) => {
75
+ if (!context.env.INBOX)
76
+ return { error: "not_granted" };
77
+ const page = inboxPage.parse(await context.env.INBOX.page({ after: null }));
78
+ return {
79
+ chats: page.items.flatMap((raw) => {
80
+ const item = Schema.decodeUnknownSync(memberInboxItemSchema)(raw);
81
+ return "kind" in item || !("title" in item)
82
+ ? []
83
+ : [
84
+ {
85
+ agentId: item.agentId,
86
+ sessionId: item.sessionId,
87
+ title: item.title,
88
+ },
89
+ ];
90
+ }),
91
+ };
92
+ },
93
+ }),
94
+ catalog: defineAppMethod({
95
+ description: "List your installed Apps' methods: a palette index with no port schemas, or one installation's full ports when `installationId` is given.",
96
+ effect: "read",
97
+ requires: ["workflow.run"],
98
+ input: catalogInputSchema,
99
+ output: catalogReply,
100
+ handler: async (input, context) => catalogReply.parse(await granted(context).catalog(input)),
101
+ }),
102
+ runStatus: defineAppMethod({
103
+ description: "Read one of your Workflow runs: per-node events, open reviews and the engine's status.",
104
+ effect: "read",
105
+ requires: ["workflow.run"],
106
+ input: z.strictObject({ runId: identity }),
107
+ output: statusReply,
108
+ handler: async (input, context) => statusReply.parse(await granted(context).graphStatus(input)),
109
+ }),
110
+ cancel: defineAppMethod({
111
+ description: "Cancel a run you started or administer as the current Team owner. Completed runs remain terminal.",
112
+ effect: "write",
113
+ requires: ["workflow.run"],
114
+ input: z.strictObject({ runId: identity }),
115
+ output: cancelReplySchema,
116
+ handler: async (input, context) => cancelReplySchema.parse(await granted(context).cancel(input)),
117
+ }),
118
+ answerReview: defineAppMethod({
119
+ description: "Answer one of your Workflow runs' reviews: approve or reject, or pick one of the review's named choices. Only a person can answer; Agents cannot.",
120
+ effect: "write",
121
+ requires: ["workflow.run"],
122
+ input: z.strictObject({
123
+ runId: identity,
124
+ nodeId: identity,
125
+ iteration: identity,
126
+ answer: z.fromJSONSchema(Schema.toJsonSchemaDocument(reviewAnswerSchema).schema),
127
+ }),
128
+ output: answerReviewReply,
129
+ handler: async (input, context, caller) => {
130
+ if (caller.actor.kind !== "member")
131
+ return { error: "not_granted" };
132
+ return answerReviewReply.parse(await granted(context).answerReview({
133
+ ...input,
134
+ answer: Schema.decodeUnknownSync(reviewAnswerSchema)(input.answer, {
135
+ onExcessProperty: "error",
136
+ }),
137
+ }));
138
+ },
139
+ }),
140
+ },
141
+ });
@@ -0,0 +1,451 @@
1
+ import * as z from "zod";
2
+ import * as S from "effect/Schema";
3
+ import { Watchdog, watchdogSqliteRowSchema, } from "@fungi.computer/watchdog";
4
+ import { begin, GitStepMaterialError, runStep, gitStepSchema, sqliteGitStepMaterial, dropMaterial, } from "@fungi.computer/agar/git-steps";
5
+ import { countingGit } from "@fungi.computer/agar/read-copy";
6
+ import { advertisedRefs } from "@fungi.computer/agar/git-wire";
7
+ import { contentDigest, validate, workflowDefinitionSchema, } from "@fungi.computer/braid";
8
+ import { actorLane, backgroundOriginSchema, backgroundSyncRefusalSchema, } from "@fungi.computer/puffball/background-replay";
9
+ import { sha256DigestSchema } from "@fungi.computer/puffball/effect";
10
+ import { draftDocument, publicationIds, publishedDocument, publicationFailure, publicationComplete, publicationFailed, workflowIdSchema, } from "@fungi.computer/workflows-wire";
11
+ import { versionRefs } from "@fungi.computer/workflows-wire";
12
+ import { serializedDraft, retainedDraft } from "./library.js";
13
+ import { definitionAppIds } from "./catalog.js";
14
+ const cargo = z.strictObject({
15
+ publicationId: publicationIds.publication.schema,
16
+ step: z.number().int().nonnegative(),
17
+ });
18
+ const rowSchema = z.strictObject({
19
+ id: publicationIds.publication.schema,
20
+ workflow_id: workflowIdSchema,
21
+ revision: z.number().int().positive(),
22
+ lane: z.string(),
23
+ origin: z.string(),
24
+ body: z.string().nullable(),
25
+ epoch: z.number().int().nonnegative(),
26
+ step: z.number().int().nonnegative(),
27
+ attempts: z.number().int().min(0).max(2),
28
+ phase: z.enum(["inspect", "create", "prepare", "git"]),
29
+ checkpoint: z.string().nullable(),
30
+ version: z.number().int().positive().nullable(),
31
+ status: z.enum(["publishing", "published", "failed"]),
32
+ reason: publicationFailure.nullable(),
33
+ commit_oid: z.string().nullable(),
34
+ request_key: z.string().nullable(),
35
+ digest: z.string().refine(S.is(sha256DigestSchema)),
36
+ });
37
+ const queue = (id) => `publication:${id}`;
38
+ /** StoredPublication cargo and status share the facet's existing SQLite owner with Watchdog. */
39
+ export function createPublicationJobs(storage, store) {
40
+ const sql = {
41
+ exec(statement, ...bindings) {
42
+ const cursor = storage.sql.exec(statement, ...bindings);
43
+ return {
44
+ toArray: () => cursor
45
+ .toArray()
46
+ .map((row) => S.decodeUnknownSync(watchdogSqliteRowSchema)(row)),
47
+ };
48
+ },
49
+ };
50
+ const owner = {
51
+ sql,
52
+ transactionSync: (operation) => storage.transactionSync(operation),
53
+ };
54
+ const material = sqliteGitStepMaterial(sql);
55
+ sql.exec("CREATE TABLE IF NOT EXISTS workflow_publication_jobs (id TEXT PRIMARY KEY, workflow_id TEXT NOT NULL, revision INTEGER NOT NULL, lane TEXT NOT NULL, origin TEXT NOT NULL, body TEXT, epoch INTEGER NOT NULL, step INTEGER NOT NULL, attempts INTEGER NOT NULL, phase TEXT NOT NULL, checkpoint TEXT, version INTEGER, status TEXT NOT NULL, reason TEXT, commit_oid TEXT, request_key TEXT, digest TEXT NOT NULL)");
56
+ sql.exec("CREATE INDEX IF NOT EXISTS workflow_publication_pending ON workflow_publication_jobs(lane,status)");
57
+ const live = new Set();
58
+ function read(id) {
59
+ const raw = sql
60
+ .exec("SELECT * FROM workflow_publication_jobs WHERE id = ?", id)
61
+ .toArray()[0];
62
+ return raw === undefined ? null : rowSchema.parse(raw);
63
+ }
64
+ function status(row) {
65
+ if (row.status === "published")
66
+ return publicationComplete.parse({
67
+ kind: "published",
68
+ commit: row.commit_oid,
69
+ version: row.version,
70
+ });
71
+ if (row.status === "failed")
72
+ return publicationFailed.parse({ kind: "failed", reason: row.reason });
73
+ return { kind: "publishing", publicationId: row.id };
74
+ }
75
+ function forDraft(workflowId, revision) {
76
+ const raw = sql
77
+ .exec("SELECT * FROM workflow_publication_jobs WHERE workflow_id = ? AND revision = ? ORDER BY rowid DESC LIMIT 1", workflowId, revision)
78
+ .toArray()[0];
79
+ return raw === undefined ? null : rowSchema.parse(raw);
80
+ }
81
+ function existing(workflowId, revision, caller) {
82
+ const row = forDraft(workflowId, revision);
83
+ if (row === null)
84
+ return null;
85
+ const lane = caller?.actor.kind === "member" ? actorLane(caller.actor) : null;
86
+ if (row.status === "publishing" && row.lane !== lane)
87
+ return { kind: "publishing_elsewhere" };
88
+ if (row.status !== "failed" ||
89
+ (row.lane === lane &&
90
+ caller?.idempotencyKey !== undefined &&
91
+ row.request_key === caller.idempotencyKey))
92
+ return status(row);
93
+ return null;
94
+ }
95
+ const queued = (row) => ({
96
+ jobId: `${row.id}:${String(row.step)}`,
97
+ queue: queue(row.id),
98
+ lane: row.lane,
99
+ priority: 0,
100
+ state: "queued",
101
+ payload: { publicationId: row.id, step: row.step },
102
+ recovery: { maxRecoveries: 3 },
103
+ });
104
+ function storedOrigin(value) {
105
+ try {
106
+ const decoded = backgroundOriginSchema.safeParse(JSON.parse(value));
107
+ return decoded.success ? decoded.data : null;
108
+ }
109
+ catch (cause) {
110
+ if (cause instanceof SyntaxError)
111
+ return null;
112
+ throw cause;
113
+ }
114
+ }
115
+ const frameClosed = (signal) => signal.aborted;
116
+ async function runtime(lane, ports) {
117
+ const progress = new Map();
118
+ const terminal = new Set();
119
+ const claimed = new Set();
120
+ const retry = (jobId, row, interrupted = false) => {
121
+ if (!interrupted && row.attempts === 2)
122
+ return { kind: "failed", reason: "publication_failed" };
123
+ progress.set(jobId, {
124
+ checkpoint: row.checkpoint === null
125
+ ? null
126
+ : gitStepSchema.parse(JSON.parse(row.checkpoint)),
127
+ version: row.version,
128
+ attempts: interrupted ? row.attempts : row.attempts + 1,
129
+ });
130
+ return { kind: "completed" };
131
+ };
132
+ const runner = await Watchdog.make({
133
+ owner,
134
+ concurrentLane: lane,
135
+ isAlive: (job) => Promise.resolve(live.has(job.jobId)),
136
+ wake: { recompute: () => Promise.resolve() },
137
+ execute: async (job, signal) => {
138
+ const input = cargo.parse(job.payload);
139
+ const row = read(input.publicationId);
140
+ if (row?.status !== "publishing" ||
141
+ row.lane !== lane ||
142
+ job.lane !== lane ||
143
+ row.step !== input.step)
144
+ return { kind: "failed", reason: "publication_changed" };
145
+ if (!ports)
146
+ throw new Error("StoredPublication transport unavailable");
147
+ live.add(job.jobId);
148
+ claimed.add(job.jobId);
149
+ try {
150
+ if (frameClosed(signal))
151
+ return retry(job.jobId, row, true);
152
+ const origin = storedOrigin(row.origin);
153
+ if (origin === null || !ports.background)
154
+ return { kind: "failed", reason: "access_changed" };
155
+ const admission = await ports.background.admit(origin);
156
+ if (admission.kind === "refused")
157
+ return { kind: "failed", reason: admission.code };
158
+ const git = countingGit(ports.git, { limit: 1, signal }).git;
159
+ if (row.body === null)
160
+ return { kind: "failed", reason: "publication_changed" };
161
+ const draft = draftDocument.parse(JSON.parse(row.body));
162
+ const definition = S.decodeUnknownSync(workflowDefinitionSchema)(draft.definition);
163
+ const catalog = await ports.catalog(definitionAppIds(definition));
164
+ if (validate(definition, catalog).length) {
165
+ progress.set(job.jobId, {
166
+ checkpoint: null,
167
+ version: row.version,
168
+ result: { kind: "failed", reason: "publication_failed" },
169
+ });
170
+ return { kind: "completed" };
171
+ }
172
+ if (row.checkpoint === null) {
173
+ if (frameClosed(signal))
174
+ return retry(job.jobId, row, true);
175
+ if (row.phase === "inspect") {
176
+ const exists = await git.info("trellis");
177
+ progress.set(job.jobId, {
178
+ checkpoint: null,
179
+ version: null,
180
+ phase: exists ? "prepare" : "create",
181
+ });
182
+ return { kind: "completed" };
183
+ }
184
+ if (row.phase === "create") {
185
+ await git.create("trellis", { defaultBranch: "main" });
186
+ progress.set(job.jobId, {
187
+ checkpoint: null,
188
+ version: null,
189
+ phase: "prepare",
190
+ });
191
+ return { kind: "completed" };
192
+ }
193
+ const refs = await git.refs("trellis");
194
+ if (frameClosed(signal))
195
+ return retry(job.jobId, row, true);
196
+ const version = (versionRefs(row.workflow_id, refs)[0]?.version ?? 0) + 1;
197
+ const checkpoint = await begin({
198
+ kind: "commit",
199
+ requestKey: row.id,
200
+ request: {
201
+ repository: "trellis",
202
+ branch: "main",
203
+ expectedHead: refs.find((ref) => ref.name === "refs/heads/main")?.hash ??
204
+ null,
205
+ changes: [
206
+ {
207
+ path: `workflows/${row.workflow_id}.json`,
208
+ content: new TextEncoder().encode(serializedDraft(retainedDraft({ ...draft, definition }))),
209
+ mode: "100644",
210
+ },
211
+ ],
212
+ message: `Publish ${row.workflow_id}`,
213
+ author: ports.author,
214
+ },
215
+ tags: [
216
+ {
217
+ ref: `refs/tags/${row.workflow_id}@v${String(version)}`,
218
+ expected: null,
219
+ },
220
+ ],
221
+ }, material);
222
+ progress.set(job.jobId, { checkpoint, version, phase: "git" });
223
+ }
224
+ else {
225
+ const checkpoint = gitStepSchema.parse(JSON.parse(row.checkpoint));
226
+ let witnessedRefs;
227
+ const witnessedGit = {
228
+ exchange: async (repository, request) => {
229
+ const response = await git.exchange(repository, request);
230
+ if (request.service === "upload-pack" &&
231
+ request.phase === "advertise" &&
232
+ response.status === 200)
233
+ witnessedRefs = [...advertisedRefs(response.body)].map(([name, hash]) => ({ name, hash }));
234
+ return response;
235
+ },
236
+ };
237
+ const result = await runStep(checkpoint, { git: witnessedGit, material }, signal);
238
+ if ("done" in result) {
239
+ const done = result.done;
240
+ // Reuse the helper's fresh confirmation witness. An extra read
241
+ // after the tag lands could fail or observe a different main.
242
+ const refs = done.kind === "committed" ? witnessedRefs : undefined;
243
+ const tag = `refs/tags/${row.workflow_id}@v${String(row.version)}`;
244
+ const final = done.kind === "committed" &&
245
+ refs?.some((ref) => ref.name === tag && ref.hash === done.commit)
246
+ ? publicationComplete.parse({
247
+ kind: "published",
248
+ commit: done.commit,
249
+ version: row.version,
250
+ })
251
+ : {
252
+ kind: "failed",
253
+ reason: done.kind === "too_large"
254
+ ? "too_large"
255
+ : "git_conflict",
256
+ };
257
+ const completed = {
258
+ checkpoint,
259
+ version: row.version,
260
+ result: final,
261
+ };
262
+ if (refs === undefined)
263
+ progress.set(job.jobId, completed);
264
+ else
265
+ progress.set(job.jobId, { ...completed, refs });
266
+ }
267
+ else
268
+ progress.set(job.jobId, {
269
+ checkpoint: "next" in result ? result.next : checkpoint,
270
+ version: row.version,
271
+ });
272
+ }
273
+ if (frameClosed(signal))
274
+ return retry(job.jobId, row, true);
275
+ return { kind: "completed" };
276
+ }
277
+ catch (cause) {
278
+ if (frameClosed(signal) ||
279
+ (cause instanceof Error && cause.name === "AbortError"))
280
+ return retry(job.jobId, row, true);
281
+ if (cause instanceof GitStepMaterialError)
282
+ return { kind: "failed", reason: "publication_failed" };
283
+ return retry(job.jobId, row);
284
+ }
285
+ },
286
+ project: (transition) => {
287
+ const settled = transition.settled;
288
+ if (!settled)
289
+ return;
290
+ const input = cargo.parse(settled.payload);
291
+ const row = read(input.publicationId);
292
+ if (row?.status !== "publishing" || row.step !== input.step)
293
+ return;
294
+ let next = settled.outcome === "completed"
295
+ ? progress.get(settled.jobId)
296
+ : undefined;
297
+ if (!next &&
298
+ (settled.outcome === "cancelled" ||
299
+ settled.failureReason === "execution_interrupted")) {
300
+ retry(settled.jobId, row, true);
301
+ next = progress.get(settled.jobId);
302
+ }
303
+ if (!next || next.result?.kind === "failed") {
304
+ const reason = next?.result?.kind === "failed"
305
+ ? next.result.reason
306
+ : settled.failureReason === "access_changed"
307
+ ? "access_changed"
308
+ : "publication_failed";
309
+ sql.exec("UPDATE workflow_publication_jobs SET status = 'failed', reason = ?, body = NULL WHERE id = ?", reason, row.id);
310
+ terminal.add(row.id);
311
+ return;
312
+ }
313
+ if (next.result?.kind === "published") {
314
+ if (row.body === null)
315
+ throw new Error("StoredPublication draft missing");
316
+ const { revision, ...draft } = draftDocument.parse(JSON.parse(row.body));
317
+ if (revision !== row.revision)
318
+ throw new Error("StoredPublication revision mismatch");
319
+ if (!next.refs)
320
+ throw new Error("Confirmed publication refs missing");
321
+ store.publish(publishedDocument.parse({
322
+ ...draft,
323
+ workflowId: row.workflow_id,
324
+ commit: next.result.commit,
325
+ version: next.result.version,
326
+ digest: row.digest,
327
+ }), row.epoch, next.refs);
328
+ sql.exec("UPDATE workflow_publication_jobs SET status = 'published', commit_oid = ?, version = ?, body = NULL WHERE id = ?", next.result.commit, next.result.version, row.id);
329
+ terminal.add(row.id);
330
+ return;
331
+ }
332
+ const successor = {
333
+ ...row,
334
+ step: row.step + 1,
335
+ attempts: next.attempts ?? 0,
336
+ phase: next.phase ?? row.phase,
337
+ version: next.version,
338
+ checkpoint: next.checkpoint === null ? null : JSON.stringify(next.checkpoint),
339
+ };
340
+ sql.exec("UPDATE workflow_publication_jobs SET step = ?, attempts = ?, phase = ?, checkpoint = ?, version = ? WHERE id = ?", successor.step, successor.attempts, successor.phase, successor.checkpoint, successor.version, row.id);
341
+ runner.transactional.enqueueAcceptedJob(queued(successor));
342
+ },
343
+ });
344
+ return {
345
+ runner,
346
+ release: () => {
347
+ for (const id of claimed)
348
+ live.delete(id);
349
+ },
350
+ cleanup: async () => {
351
+ for (const id of terminal) {
352
+ const row = read(id);
353
+ if (row && row.status !== "publishing" && row.checkpoint !== null)
354
+ await dropMaterial(gitStepSchema.parse(JSON.parse(row.checkpoint)), material);
355
+ }
356
+ terminal.clear();
357
+ },
358
+ };
359
+ }
360
+ return {
361
+ existing,
362
+ async enqueue(workflowId, revision, caller) {
363
+ const lane = actorLane(caller.actor);
364
+ const draft = store.readDraft(workflowId);
365
+ if (draft?.revision !== revision)
366
+ return { kind: "conflict", head: null };
367
+ const { runner } = await runtime(lane);
368
+ const digest = S.decodeUnknownSync(sha256DigestSchema)(await contentDigest(S.decodeUnknownSync(S.Json)(draft.definition)));
369
+ return storage.transactionSync(() => {
370
+ if (store.readDraft(workflowId)?.revision !== revision)
371
+ return { kind: "conflict", head: null };
372
+ const retained = existing(workflowId, revision, caller);
373
+ if (retained !== null)
374
+ return retained;
375
+ const row = {
376
+ id: publicationIds.publication.generate(),
377
+ workflow_id: workflowId,
378
+ revision,
379
+ lane,
380
+ origin: JSON.stringify(backgroundOriginSchema.parse(caller.backgroundOrigin)),
381
+ body: JSON.stringify(draft),
382
+ epoch: store.revision(),
383
+ step: 0,
384
+ attempts: 0,
385
+ phase: "inspect",
386
+ checkpoint: null,
387
+ version: null,
388
+ status: "publishing",
389
+ reason: null,
390
+ commit_oid: null,
391
+ request_key: caller.idempotencyKey ?? null,
392
+ digest,
393
+ };
394
+ sql.exec("INSERT INTO workflow_publication_jobs(id,workflow_id,revision,lane,origin,body,epoch,step,attempts,phase,checkpoint,version,status,reason,commit_oid,request_key,digest) VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)", row.id, workflowId, revision, lane, row.origin, row.body, row.epoch, row.step, row.attempts, row.phase, row.checkpoint, row.version, row.status, row.reason, row.commit_oid, row.request_key, row.digest);
395
+ runner.transactional.enqueueAcceptedJob(queued(row));
396
+ return status(row);
397
+ });
398
+ },
399
+ async continue(caller, ports) {
400
+ if (caller.actor.kind !== "member" || caller.workflow !== undefined)
401
+ return { more: false };
402
+ const lane = actorLane(caller.actor);
403
+ if (caller.backgroundLane !== undefined && caller.backgroundLane !== lane)
404
+ return { more: false };
405
+ // R0 strips enqueue markers on readmission. A sealed foreground write
406
+ // is still foreground, even if its host exposes a refusing binding.
407
+ if (caller.backgroundOrigin !== undefined ||
408
+ caller.backgroundLane !== undefined ||
409
+ !ports.background)
410
+ return {
411
+ more: sql
412
+ .exec("SELECT id FROM workflow_publication_jobs WHERE lane = ? AND status = 'publishing' LIMIT 1", lane)
413
+ .toArray().length !== 0,
414
+ };
415
+ const { runner, cleanup, release } = await runtime(lane, ports);
416
+ try {
417
+ await runner.tickConcurrent();
418
+ }
419
+ finally {
420
+ release();
421
+ }
422
+ await cleanup();
423
+ return {
424
+ more: sql
425
+ .exec("SELECT id FROM workflow_publication_jobs WHERE lane = ? AND status = 'publishing' LIMIT 1", lane)
426
+ .toArray().length !== 0,
427
+ };
428
+ },
429
+ status(workflowId, publicationId, caller) {
430
+ const row = read(publicationId);
431
+ return caller.actor.kind === "member" &&
432
+ row?.workflow_id === workflowId &&
433
+ row.lane === actorLane(caller.actor)
434
+ ? status(row)
435
+ : { kind: "not_found" };
436
+ },
437
+ async refused(input) {
438
+ const refusal = backgroundSyncRefusalSchema.parse(input);
439
+ const { runner, cleanup } = await runtime(refusal.lane);
440
+ let more;
441
+ do {
442
+ const result = await runner.failLane({
443
+ lane: refusal.lane,
444
+ reason: refusal.code,
445
+ });
446
+ more = result.more;
447
+ await cleanup();
448
+ } while (more);
449
+ },
450
+ };
451
+ }