bareloop 0.41.0 → 0.43.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (75) hide show
  1. package/CHANGELOG.md +109 -1
  2. package/README.md +13 -21
  3. package/bareloop.context.md +97 -34
  4. package/package.json +4 -3
  5. package/src/authorflow.js +337 -29
  6. package/src/authoring.js +92 -7
  7. package/src/authorjob.js +84 -16
  8. package/src/authorreadout.js +89 -5
  9. package/src/authorrun.js +14 -5
  10. package/src/authorscout.js +108 -0
  11. package/src/declaredclose.js +25 -14
  12. package/src/facts.js +317 -0
  13. package/src/factwords.js +129 -0
  14. package/src/inputlook.js +301 -0
  15. package/src/interviewrun.js +0 -1
  16. package/src/job.js +40 -12
  17. package/src/jobblock.js +7 -2
  18. package/src/kinds.js +84 -3
  19. package/src/labelscan.js +170 -0
  20. package/src/pagefetch.js +125 -0
  21. package/src/panel/authorroutes.js +121 -34
  22. package/src/panel/authorsession.js +154 -45
  23. package/src/panel/index.html +845 -490
  24. package/src/panel/jobplan.js +34 -11
  25. package/src/panel/runroutes.js +163 -5
  26. package/src/panel/scheduler.js +156 -0
  27. package/src/panel/schedules.js +667 -0
  28. package/src/panel/schedulestart.js +243 -0
  29. package/src/panel/server.js +467 -48
  30. package/src/planrun.js +42 -9
  31. package/src/providerrows.js +3 -3
  32. package/src/reviewdoor.js +37 -0
  33. package/src/rubricstep.js +804 -0
  34. package/src/runlist.js +35 -0
  35. package/src/schedulewords.js +296 -0
  36. package/src/source.js +166 -1
  37. package/src/trigger.js +48 -0
  38. package/src/triggerclock.js +127 -0
  39. package/src/triggerfiles.js +215 -0
  40. package/src/triggerledger.js +79 -0
  41. package/src/u-readout.js +1 -1
  42. package/src/userrun.js +20 -9
  43. package/types/authorflow.d.ts +267 -13
  44. package/types/authoring.d.ts +29 -1
  45. package/types/authorjob.d.ts +7 -5
  46. package/types/authorreadout.d.ts +27 -2
  47. package/types/authorscout.d.ts +70 -0
  48. package/types/declaredclose.d.ts +5 -3
  49. package/types/facts.d.ts +71 -0
  50. package/types/factwords.d.ts +42 -0
  51. package/types/inputlook.d.ts +238 -0
  52. package/types/job.d.ts +8 -1
  53. package/types/jobblock.d.ts +8 -1
  54. package/types/labelscan.d.ts +38 -0
  55. package/types/pagefetch.d.ts +71 -0
  56. package/types/panel/authorroutes.d.ts +34 -1
  57. package/types/panel/authorsession.d.ts +55 -14
  58. package/types/panel/jobplan.d.ts +12 -4
  59. package/types/panel/runroutes.d.ts +32 -1
  60. package/types/panel/scheduler.d.ts +17 -0
  61. package/types/panel/schedules.d.ts +412 -0
  62. package/types/panel/schedulestart.d.ts +106 -0
  63. package/types/panel/server.d.ts +145 -14
  64. package/types/planrun.d.ts +2 -0
  65. package/types/providerrows.d.ts +2 -2
  66. package/types/reviewdoor.d.ts +27 -0
  67. package/types/rubricstep.d.ts +286 -0
  68. package/types/runlist.d.ts +20 -0
  69. package/types/schedulewords.d.ts +135 -0
  70. package/types/source.d.ts +94 -0
  71. package/types/trigger.d.ts +37 -0
  72. package/types/triggerclock.d.ts +52 -0
  73. package/types/triggerfiles.d.ts +93 -0
  74. package/types/triggerledger.d.ts +35 -0
  75. package/types/userrun.d.ts +11 -0
@@ -0,0 +1,412 @@
1
+ /**
2
+ * When a process started, in epoch ms, from `/proc/<pid>/stat` field 22 (clock ticks since boot) and `/proc/stat` btime
3
+ * (whole seconds, so good to about a second). `null` where /proc cannot be read: the caller then trusts `isLiveRunner` alone.
4
+ * @param {number} pid
5
+ * @param {(path: string) => string} [read]
6
+ * @returns {number|null}
7
+ */
8
+ export function procStartMs(pid: number, read?: (path: string) => string): number | null;
9
+ /**
10
+ * Is this listed run's runner alive? `runIsAlive` (the pid is alive AND a bareloop runner) plus the PID REUSE GUARD: a
11
+ * pid whose process started MORE than 5 s after the row was written is a recycled pid, not that run. A resumed run's row
12
+ * keeps its first leg's time while its pid is the latest leg's, so the guard is skipped for it (the cmdline check stands).
13
+ * @param {any} row @param {number} nowMs
14
+ */
15
+ export function runnerAlive(row: any, nowMs: number): boolean;
16
+ /**
17
+ * @typedef {{runid: string, outcome: string|null, spec: any, records: any[]}} JobRun
18
+ * @typedef {{job: string, trigger: import('../trigger.js').Trigger, spec: any, specHash: string, workflowKey: string,
19
+ * originRunid: string, inputsText: string, destination?: string, signedBy: string, signedAt: string}} Schedule
20
+ */
21
+ /**
22
+ * @param {{
23
+ * home?: string,
24
+ * sessionsRoot?: string,
25
+ * now?: () => number,
26
+ * jobRuns: (job: string) => JobRun[],
27
+ * startFrom: (runid: string) => any,
28
+ * jobOfRun: (runid: string) => string|null,
29
+ * rowsFor: () => readonly import('../providerrows.js').KeyRow[],
30
+ * runRows?: () => any[],
31
+ * installRetrySteps?: readonly number[], installGiveUpMs?: number,
32
+ * userHome?: string,
33
+ * }} deps `userHome` = the folder shown as `~` in words (default the OS home). `installRetrySteps` (ms) and `installGiveUpMs` = the install-needed back-off (tests inject them). `runRows` = a test seam for the one run-list read the busy check does. `jobRuns(job)` = the job's listed runs, newest first, each with its signed spec when on disk and its terminal
34
+ * outcome off the spine; `startFrom(runid)` = the panel's `getStartFrom`; `jobOfRun` = the listed run's job name.
35
+ */
36
+ export function createScheduleDoor(deps: {
37
+ home?: string;
38
+ sessionsRoot?: string;
39
+ now?: () => number;
40
+ jobRuns: (job: string) => JobRun[];
41
+ startFrom: (runid: string) => any;
42
+ jobOfRun: (runid: string) => string | null;
43
+ rowsFor: () => readonly import("../providerrows.js").KeyRow[];
44
+ runRows?: () => any[];
45
+ installRetrySteps?: readonly number[];
46
+ installGiveUpMs?: number;
47
+ userHome?: string;
48
+ }): {
49
+ /** The starter is built after the author routes (which need this door), so it is attached here. @param {ReturnType<typeof import('./schedulestart.js').createStarter>} s */
50
+ attachStarter(s: ReturnType<typeof import("./schedulestart.js").createStarter>): void;
51
+ busy: (job: string) => boolean;
52
+ paused: (job: string) => boolean;
53
+ /**
54
+ * RULED 2026-10-15 (v1.104): the fires that started NOTHING (busy, month used up, refused) of ALL signings of the job's schedule
55
+ * (the job's ledger is the job's, so every signing's rows are in it; cancelled-and-set-again rows stay). Newest first. Lines are
56
+ * `MM/DD HH:MM <words>`. The caller shows the first 7 and counts them all.
57
+ * @param {string} job
58
+ * @returns {{at: number, line: string}[]}
59
+ */
60
+ skippedFor(job: string): {
61
+ at: number;
62
+ line: string;
63
+ }[];
64
+ /**
65
+ * One fire, in this order (T15): busy, then monthly, then wouldAccept, then the starter (which calls `beforeSpawn`, the
66
+ * ONE consuming row, only after every "no" is behind it). Each outcome is ONE row in ledger.jsonl, told once per
67
+ * `key|outcome|why` (read back from disk, so a restart does not repeat it). RULED 2026-10-14: a busy, monthly or refused
68
+ * fire consumes NOTHING, so the next tick tries again; a spawn error after `beforeSpawn` writes a `released` row (bounded).
69
+ * @param {string} job
70
+ * @param {{kind: 'clock', slot: number, catchUp: boolean, missed: number} | {kind: 'file', path: string, sha: string, size?: number}} cause
71
+ * @param {{beforeSpawn?: () => void, busy?: boolean}} [hooks] `busy` = the scheduler's one read of the run list this tick
72
+ * @returns {Promise<{outcome: 'busy'|'monthly-exhausted'|'refused'|'started', why?: string, told: boolean, sessionId?: string}>}
73
+ */
74
+ fire(job: string, cause: {
75
+ kind: "clock";
76
+ slot: number;
77
+ catchUp: boolean;
78
+ missed: number;
79
+ } | {
80
+ kind: "file";
81
+ path: string;
82
+ sha: string;
83
+ size?: number;
84
+ }, hooks?: {
85
+ beforeSpawn?: () => void;
86
+ busy?: boolean;
87
+ }): Promise<{
88
+ outcome: "busy" | "monthly-exhausted" | "refused" | "started";
89
+ why?: string;
90
+ told: boolean;
91
+ sessionId?: string;
92
+ }>;
93
+ greenFor: (job: string) => {
94
+ ok: true;
95
+ run: JobRun;
96
+ key: string;
97
+ } | {
98
+ ok: false;
99
+ say: string;
100
+ };
101
+ armFor: (job: string, schedule: Schedule, o?: {
102
+ baseline?: boolean;
103
+ }) => {
104
+ armed: boolean;
105
+ already: boolean;
106
+ };
107
+ readSchedule: (job: string) => Schedule | null;
108
+ ledgerOf: (job: string) => string;
109
+ /**
110
+ * A failure the scheduler met on a job (a folder that went away, a file refused), written as ONE `refused` row per
111
+ * `tag`, read back from disk so a restart does not repeat it (T13). Returns whether it wrote.
112
+ * @param {string} job @param {string} sig @param {string} tag @param {string} why
113
+ */
114
+ tellOnce(job: string, sig: string, tag: string, why: string): boolean;
115
+ /** Click 1. All $0 checks; signs and writes nothing; returns `{words, hash, monthlyNote}`. @param {any} body */
116
+ prepare(body: any): {
117
+ status: number;
118
+ body: {
119
+ ok: boolean;
120
+ };
121
+ } | {
122
+ status: number;
123
+ body: {
124
+ ok: boolean;
125
+ job: any;
126
+ words: string | null;
127
+ hash: string;
128
+ monthlyNote: string | null;
129
+ cancel: boolean;
130
+ };
131
+ };
132
+ /** Click 2, carrying the hash it was shown. $0: no drafter, no run. @param {any} body */
133
+ set(body: any): {
134
+ status: number;
135
+ body: {
136
+ ok: boolean;
137
+ };
138
+ } | {
139
+ status: number;
140
+ body: {
141
+ ok: boolean;
142
+ refused: string;
143
+ say: "What you were shown is not what is on the card now. Nothing was signed. Look again, then sign.";
144
+ job?: undefined;
145
+ cancelled?: undefined;
146
+ words?: undefined;
147
+ };
148
+ } | {
149
+ status: number;
150
+ body: {
151
+ ok: boolean;
152
+ job: any;
153
+ cancelled: boolean;
154
+ words: "Cancel schedule";
155
+ refused?: undefined;
156
+ say?: undefined;
157
+ };
158
+ } | {
159
+ status: number;
160
+ body: {
161
+ say?: string | undefined;
162
+ ok: boolean;
163
+ job: any;
164
+ words: string | null;
165
+ hash: string;
166
+ armed: boolean;
167
+ refused?: undefined;
168
+ cancelled?: undefined;
169
+ };
170
+ };
171
+ /** Cancel a job's schedule, never refused. @param {string} job @returns {boolean} there was one */
172
+ cancel(job: string): boolean;
173
+ /** Every job with a schedule.json: `{job, dir, schedule}`. One readdir; nothing else read when there is none. */
174
+ scan(): {
175
+ job: string;
176
+ dir: string;
177
+ schedule: Schedule;
178
+ }[];
179
+ /**
180
+ * What a card shows of a job's schedule, as DATA (no words): the trigger, whether it is armed under the CURRENT
181
+ * signing, the next slot (clock kinds), the last outcome (rows under the current signing only, never a wall-clock
182
+ * compare, T19), whether the job has a green run, and the refusal sentence when it has none.
183
+ * @param {string} job
184
+ */
185
+ read(job: string): {
186
+ schedule: null;
187
+ trigger: null;
188
+ armed: boolean;
189
+ armedAt: null;
190
+ nextSlotMs: null;
191
+ lastOutcome: null;
192
+ retryInMs: null;
193
+ gaveUp: null;
194
+ job: string;
195
+ hasGreen: boolean;
196
+ refusal: string | null;
197
+ } | {
198
+ schedule: Schedule;
199
+ trigger: import("../trigger.js").Trigger;
200
+ armed: boolean;
201
+ armedAt: number | null | undefined;
202
+ nextSlotMs: number | null;
203
+ lastOutcome: import("../triggerledger.js").LedgerRow | null;
204
+ retryInMs: number | null;
205
+ gaveUp: import("../triggerledger.js").LedgerRow | null;
206
+ job: string;
207
+ hasGreen: boolean;
208
+ refusal: string | null;
209
+ } | null;
210
+ stateOf: (job: string) => {
211
+ schedule: null;
212
+ trigger: null;
213
+ armed: boolean;
214
+ armedAt: null;
215
+ nextSlotMs: null;
216
+ lastOutcome: null;
217
+ retryInMs: null;
218
+ gaveUp: null;
219
+ } | {
220
+ schedule: Schedule;
221
+ trigger: import("../trigger.js").Trigger;
222
+ armed: boolean;
223
+ armedAt: number | null | undefined;
224
+ nextSlotMs: number | null;
225
+ lastOutcome: import("../triggerledger.js").LedgerRow | null;
226
+ retryInMs: number | null;
227
+ gaveUp: import("../triggerledger.js").LedgerRow | null;
228
+ };
229
+ userHome: string;
230
+ /**
231
+ * What the Run tab shows of a job's schedule (A.12: one read of the schedule files and the ledger; the green gate is
232
+ * memoised per run-list state, so a poll never re-parses every run). The SERVER decides: `scheduled` shows Cancel /
233
+ * modify, else `canSchedule` shows Schedule run, else no button. `fromRunid` = the green run the card opens from. `waitingRunNo` = the job's newest run when it waits
234
+ * for the person's Accept (the next scheduled run does NOT wait for it, T7).
235
+ * @param {string} job @param {{waitingRunNo?: number|null}} [o]
236
+ */
237
+ view(job: string, o?: {
238
+ waitingRunNo?: number | null;
239
+ }): {
240
+ fromRunid?: string | undefined;
241
+ scheduled: boolean;
242
+ canSchedule: boolean;
243
+ words: Readonly<{
244
+ byHand: "By hand";
245
+ cancelSchedule: "Cancel schedule";
246
+ cancelWords: "Cancel schedule";
247
+ scheduleRun: "Schedule run";
248
+ cancelModify: "Cancel / modify schedule";
249
+ signSchedule: "Sign schedule";
250
+ nothingChanged: "Nothing changed yet. This card only sets or adjusts the schedule; it never runs the job.";
251
+ confirmPrefix: "Confirm: ";
252
+ whenLabel: "When";
253
+ nowPrefix: "Now: ";
254
+ scheduledLabel: "SCHEDULE";
255
+ nextLabel: "NEXT";
256
+ options: Readonly<{
257
+ hand: "By hand";
258
+ daily: "Every day at HH:MM";
259
+ weekdays: "Weekdays at HH:MM";
260
+ weekly: "Every <day> at HH:MM";
261
+ 'file-updated': "When <file> is updated";
262
+ 'new-file': "When a new file arrives in <folder>";
263
+ }>;
264
+ gaveUpPill: "Check";
265
+ problemNext: "next: fix it in Cancel / modify schedule";
266
+ monthNext: "next: raise the limit in Settings, or it starts on the 1st";
267
+ acceptWaiting: "waiting for your Accept (the next run does not wait)";
268
+ scheduleGroup: "schedule";
269
+ notGreen: "This job has not had a good run yet. Run it by hand and get it right first; then you can set when it runs. A rubric job also needs your Accept once. Nothing was changed.";
270
+ stale: "What you were shown is not what is on the card now. Nothing was signed. Look again, then sign.";
271
+ }>;
272
+ text?: undefined;
273
+ next?: undefined;
274
+ nextRed?: undefined;
275
+ lastRow?: undefined;
276
+ problem?: undefined;
277
+ nextWords?: undefined;
278
+ accept?: undefined;
279
+ } | {
280
+ scheduled: boolean;
281
+ canSchedule: boolean;
282
+ words: Readonly<{
283
+ byHand: "By hand";
284
+ cancelSchedule: "Cancel schedule";
285
+ cancelWords: "Cancel schedule";
286
+ scheduleRun: "Schedule run";
287
+ cancelModify: "Cancel / modify schedule";
288
+ signSchedule: "Sign schedule";
289
+ nothingChanged: "Nothing changed yet. This card only sets or adjusts the schedule; it never runs the job.";
290
+ confirmPrefix: "Confirm: ";
291
+ whenLabel: "When";
292
+ nowPrefix: "Now: ";
293
+ scheduledLabel: "SCHEDULE";
294
+ nextLabel: "NEXT";
295
+ options: Readonly<{
296
+ hand: "By hand";
297
+ daily: "Every day at HH:MM";
298
+ weekdays: "Weekdays at HH:MM";
299
+ weekly: "Every <day> at HH:MM";
300
+ 'file-updated': "When <file> is updated";
301
+ 'new-file': "When a new file arrives in <folder>";
302
+ }>;
303
+ gaveUpPill: "Check";
304
+ problemNext: "next: fix it in Cancel / modify schedule";
305
+ monthNext: "next: raise the limit in Settings, or it starts on the 1st";
306
+ acceptWaiting: "waiting for your Accept (the next run does not wait)";
307
+ scheduleGroup: "schedule";
308
+ notGreen: "This job has not had a good run yet. Run it by hand and get it right first; then you can set when it runs. A rubric job also needs your Accept once. Nothing was changed.";
309
+ stale: "What you were shown is not what is on the card now. Nothing was signed. Look again, then sign.";
310
+ }>;
311
+ fromRunid: string;
312
+ text: string | null;
313
+ next: string;
314
+ nextRed: boolean;
315
+ lastRow: import("../triggerledger.js").LedgerRow | null;
316
+ problem: boolean;
317
+ nextWords: string;
318
+ accept: boolean;
319
+ } | null;
320
+ /**
321
+ * The schedule card's data for `start-from?mode=schedule`: the current schedule in the When field's own terms, the
322
+ * words, the refusal that disables the field (a job with no good run and no schedule), and the edit note.
323
+ * Cancel is never refused, so a scheduled job is never disabled here.
324
+ * @param {string} job
325
+ */
326
+ cardFor(job: string): {
327
+ scheduled: boolean;
328
+ trigger: import("../trigger.js").Trigger | null;
329
+ now: string | null;
330
+ refusal: string | null;
331
+ words: Readonly<{
332
+ byHand: "By hand";
333
+ cancelSchedule: "Cancel schedule";
334
+ cancelWords: "Cancel schedule";
335
+ scheduleRun: "Schedule run";
336
+ cancelModify: "Cancel / modify schedule";
337
+ signSchedule: "Sign schedule";
338
+ nothingChanged: "Nothing changed yet. This card only sets or adjusts the schedule; it never runs the job.";
339
+ confirmPrefix: "Confirm: ";
340
+ whenLabel: "When";
341
+ nowPrefix: "Now: ";
342
+ scheduledLabel: "SCHEDULE";
343
+ nextLabel: "NEXT";
344
+ options: Readonly<{
345
+ hand: "By hand";
346
+ daily: "Every day at HH:MM";
347
+ weekdays: "Weekdays at HH:MM";
348
+ weekly: "Every <day> at HH:MM";
349
+ 'file-updated': "When <file> is updated";
350
+ 'new-file': "When a new file arrives in <folder>";
351
+ }>;
352
+ gaveUpPill: "Check";
353
+ problemNext: "next: fix it in Cancel / modify schedule";
354
+ monthNext: "next: raise the limit in Settings, or it starts on the 1st";
355
+ acceptWaiting: "waiting for your Accept (the next run does not wait)";
356
+ scheduleGroup: "schedule";
357
+ notGreen: "This job has not had a good run yet. Run it by hand and get it right first; then you can set when it runs. A rubric job also needs your Accept once. Nothing was changed.";
358
+ stale: "What you were shown is not what is on the card now. Nothing was signed. Look again, then sign.";
359
+ }>;
360
+ };
361
+ /**
362
+ * RULED 2026-10-15: the jobs whose schedule gave up, for the runs list's Needs you section (the SERVER decides; the page works out nothing):
363
+ * `[{job, pill: 'Check', words, at: 'MM/DD HH:MM', runId}]`, newest give-up first. `runId` = the job's HEAD run by time. Never ages out.
364
+ * @param {Map<string, string|null>|Record<string, string|null>} heads
365
+ */
366
+ listGaveUp(heads: Map<string, string | null> | Record<string, string | null>): {
367
+ job: string;
368
+ pill: "Check";
369
+ words: string;
370
+ at: string;
371
+ runId: string | null;
372
+ }[];
373
+ /**
374
+ * The Scheduled section of the runs list: `[{job, at, in, words, runId}]`, soonest first; a file trigger (no clock
375
+ * time) after those that have one, by name. `heads` = job -> its HEAD run id by time (summarizeJobs), so the click
376
+ * opens the job's newest run, never "the first in server order". A job that cannot be read is absent.
377
+ * @param {Map<string, string|null>|Record<string, string|null>} heads
378
+ */
379
+ listScheduled(heads: Map<string, string | null> | Record<string, string | null>): {
380
+ job: string;
381
+ at: string | null;
382
+ in: string | null;
383
+ words: string | null;
384
+ runId: string | null;
385
+ }[];
386
+ };
387
+ /** a spawn error after the consuming row releases it at most this many times; then it stays consumed (bounded rows) */
388
+ export const MAX_RELEASES: 3;
389
+ /** a live pid whose process started more than this after its run row was written is a RECYCLED pid, not that run */
390
+ export const PID_REUSE_SLACK_MS: 5000;
391
+ /** RULED 2026-10-15: waits (ms) between consecutive `install-needed` refusals of a job: Fibonacci 1, 2, 3, 5, 8, 13, 21 minutes. A test injects its own via `deps.installRetrySteps`. */
392
+ export const INSTALL_RETRY_STEPS_MS: readonly number[];
393
+ /** RULED 2026-10-15: the total wait a slot or file gets. A wait that would pass it is not taken: the schedule gives up and pauses (the job waits in Needs you). */
394
+ export const INSTALL_GIVE_UP_MS: number;
395
+ export type JobRun = {
396
+ runid: string;
397
+ outcome: string | null;
398
+ spec: any;
399
+ records: any[];
400
+ };
401
+ export type Schedule = {
402
+ job: string;
403
+ trigger: import("../trigger.js").Trigger;
404
+ spec: any;
405
+ specHash: string;
406
+ workflowKey: string;
407
+ originRunid: string;
408
+ inputsText: string;
409
+ destination?: string;
410
+ signedBy: string;
411
+ signedAt: string;
412
+ };
@@ -0,0 +1,106 @@
1
+ /**
2
+ * The spec one fire runs: the signed schedule's spec; for a file cause, Inputs line 1 is the arriving file. Every other
3
+ * field must be deep-equal and the workflow key unchanged (F.4): the person signed the SCHEDULE, each fire's hash is
4
+ * derived by code. A clock fire's spec equals the signed spec byte for byte (same hash).
5
+ * @param {{spec: any, specHash: string, workflowKey: string}} schedule
6
+ * @param {{kind: 'clock'}|{kind: 'file', path: string}} cause
7
+ * @returns {{ok: true, spec: any, hash: string}|{ok: false, why: string}}
8
+ */
9
+ export function perFireSpec(schedule: {
10
+ spec: any;
11
+ specHash: string;
12
+ workflowKey: string;
13
+ }, cause: {
14
+ kind: "clock";
15
+ } | {
16
+ kind: "file";
17
+ path: string;
18
+ }): {
19
+ ok: true;
20
+ spec: any;
21
+ hash: string;
22
+ } | {
23
+ ok: false;
24
+ why: string;
25
+ };
26
+ /**
27
+ * @param {{
28
+ * door: ReturnType<typeof import('./schedules.js').createScheduleDoor>,
29
+ * startSession: ReturnType<typeof import('./authorroutes.js').createAuthorRoutes>['startSession'],
30
+ * envNow: () => Record<string, string|undefined>,
31
+ * spawnFn: Function, bareloopBin: string, sessionsRoot?: string, home?: string,
32
+ * rowsFor: () => readonly import('../providerrows.js').KeyRow[],
33
+ * startFrom: (runid: string) => any,
34
+ * now?: () => number, prepareTimeoutMs?: number, pollMs?: number, childGraceMs?: number,
35
+ * forgetSession?: (id: string) => void, ranSince?: (job: string, sinceMs: number) => boolean,
36
+ * }} deps
37
+ */
38
+ export function createStarter(deps: {
39
+ door: ReturnType<typeof import("./schedules.js").createScheduleDoor>;
40
+ startSession: ReturnType<typeof import("./authorroutes.js").createAuthorRoutes>["startSession"];
41
+ envNow: () => Record<string, string | undefined>;
42
+ spawnFn: Function;
43
+ bareloopBin: string;
44
+ sessionsRoot?: string;
45
+ home?: string;
46
+ rowsFor: () => readonly import("../providerrows.js").KeyRow[];
47
+ startFrom: (runid: string) => any;
48
+ now?: () => number;
49
+ prepareTimeoutMs?: number;
50
+ pollMs?: number;
51
+ childGraceMs?: number;
52
+ forgetSession?: (id: string) => void;
53
+ ranSince?: (job: string, sinceMs: number) => boolean;
54
+ }): {
55
+ /**
56
+ * Cheap, $0, no copy, no session. `{ok: true}` or `{ok: false, why}` (words).
57
+ * @param {string} job
58
+ * @param {{kind: 'clock'} | {kind: 'file', path: string, sha: string}} cause
59
+ * @returns {{ok: true}|{ok: false, why: string}}
60
+ */
61
+ wouldAccept(job: string, cause: {
62
+ kind: "clock";
63
+ } | {
64
+ kind: "file";
65
+ path: string;
66
+ sha: string;
67
+ }): {
68
+ ok: true;
69
+ } | {
70
+ ok: false;
71
+ why: string;
72
+ };
73
+ /**
74
+ * Start the run. Never throws. `consumed` says whether `beforeSpawn` ran, so the caller knows whether a `released` row is owed.
75
+ * @param {string} job
76
+ * @param {{kind: 'clock', slot: number, catchUp: boolean, missed: number} | {kind: 'file', path: string, sha: string}} cause
77
+ * @param {{beforeSpawn?: () => void}} [hooks]
78
+ * @returns {Promise<{ok: true, sessionId: string, specHash: string, consumed: true}|{ok: false, why: string, consumed: boolean, installNeeded?: true}>}
79
+ */
80
+ start(job: string, cause: {
81
+ kind: "clock";
82
+ slot: number;
83
+ catchUp: boolean;
84
+ missed: number;
85
+ } | {
86
+ kind: "file";
87
+ path: string;
88
+ sha: string;
89
+ }, hooks?: {
90
+ beforeSpawn?: () => void;
91
+ }): Promise<{
92
+ ok: true;
93
+ sessionId: string;
94
+ specHash: string;
95
+ consumed: true;
96
+ } | {
97
+ ok: false;
98
+ why: string;
99
+ consumed: boolean;
100
+ installNeeded?: true;
101
+ }>;
102
+ };
103
+ /** how long a headless session may take to reach `prepared` (copy, install, signing gates) */
104
+ export const PREPARE_TIMEOUT_MS: number;
105
+ /** how long the starter listens for a child that refuses at once (run-u checks its spec and approve hash first thing) */
106
+ export const CHILD_GRACE_MS: 1500;