@eir-labs/coltrane 0.11.0 → 0.12.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.
- package/dist/src/claude_invoker.d.ts +0 -12
- package/dist/src/claude_invoker.js +92 -1
- package/dist/src/claude_invoker.js.map +1 -1
- package/dist/src/cli.d.ts +1 -1
- package/dist/src/cli.js +54 -1
- package/dist/src/cli.js.map +1 -1
- package/dist/src/index.d.ts +2 -0
- package/dist/src/index.js +8 -0
- package/dist/src/index.js.map +1 -1
- package/dist/src/local_queue.d.ts +105 -0
- package/dist/src/local_queue.js +380 -0
- package/dist/src/local_queue.js.map +1 -0
- package/dist/src/residency.d.ts +149 -0
- package/dist/src/residency.js +283 -0
- package/dist/src/residency.js.map +1 -0
- package/dist/src/version.d.ts +1 -1
- package/dist/src/version.js +1 -1
- package/dist/src/worker.d.ts +20 -0
- package/dist/src/worker.js +34 -6
- package/dist/src/worker.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/** The lease a claimed gig carries — a continuing renewed fact (monotonic `renewed_at`), not a boot
|
|
2
|
+
* assertion. */
|
|
3
|
+
export interface LeaseState {
|
|
4
|
+
holder: string;
|
|
5
|
+
renewed_at: number;
|
|
6
|
+
expires_at: number;
|
|
7
|
+
}
|
|
8
|
+
/** A claimed local gig — the sibling of worker.ts ClaimedGig, plus the local lease and, on a
|
|
9
|
+
* re-claim, the human seat's role-keyed verdicts. */
|
|
10
|
+
export interface ClaimedLocalGig {
|
|
11
|
+
gig_id: string;
|
|
12
|
+
standard_slug: string;
|
|
13
|
+
standard_version: number | null;
|
|
14
|
+
mode: string;
|
|
15
|
+
input: Record<string, unknown>;
|
|
16
|
+
acting_for: string;
|
|
17
|
+
venue?: string | null;
|
|
18
|
+
worker: string;
|
|
19
|
+
lease: LeaseState;
|
|
20
|
+
approvals?: Record<string, {
|
|
21
|
+
verdict: Record<string, unknown>;
|
|
22
|
+
approved_by?: string;
|
|
23
|
+
}> | null;
|
|
24
|
+
}
|
|
25
|
+
/** One row's observable state — how a reader tells a live claim from an abandoned one. */
|
|
26
|
+
export interface LocalGigView {
|
|
27
|
+
gig_id: string;
|
|
28
|
+
state: "queued" | "claimed" | "awaiting_approval" | "complete" | "failed" | "cancelled";
|
|
29
|
+
holder?: string;
|
|
30
|
+
lease?: LeaseState;
|
|
31
|
+
}
|
|
32
|
+
export interface LocalQueueOptions {
|
|
33
|
+
leaseMs?: number;
|
|
34
|
+
/** Injected clock for deterministic lease/reap laws. Defaults to Date.now(). */
|
|
35
|
+
clock?: () => number;
|
|
36
|
+
}
|
|
37
|
+
/** The local queue port — every verb is offline and needs no credential. */
|
|
38
|
+
export interface LocalQueue {
|
|
39
|
+
enqueue(args: Record<string, unknown>): Promise<{
|
|
40
|
+
gig_id: string;
|
|
41
|
+
status: "queued";
|
|
42
|
+
}>;
|
|
43
|
+
claim(worker: string): Promise<ClaimedLocalGig | null>;
|
|
44
|
+
heartbeat(worker: string, gig_id: string): Promise<boolean>;
|
|
45
|
+
reap(): {
|
|
46
|
+
requeued: string[];
|
|
47
|
+
kept: number;
|
|
48
|
+
errors: string[];
|
|
49
|
+
};
|
|
50
|
+
park(worker: string, gig_id: string): Promise<boolean>;
|
|
51
|
+
approve(gig_id: string, role: string, verdict: Record<string, unknown>, approved_by?: string): Promise<boolean>;
|
|
52
|
+
cancel(args: Record<string, unknown>): Promise<{
|
|
53
|
+
gig_id: string;
|
|
54
|
+
status: "cancelled";
|
|
55
|
+
}>;
|
|
56
|
+
complete(worker: string, gig_id: string, output: Record<string, unknown>): Promise<{
|
|
57
|
+
content_sha: string;
|
|
58
|
+
duplicated: boolean;
|
|
59
|
+
}>;
|
|
60
|
+
list(): LocalGigView[];
|
|
61
|
+
readonly leaseMs: number;
|
|
62
|
+
}
|
|
63
|
+
/** Which backing owns the queue, decided by which environment is present. */
|
|
64
|
+
export type QueueBackingChoice = {
|
|
65
|
+
backing: "file";
|
|
66
|
+
root: string;
|
|
67
|
+
} | {
|
|
68
|
+
backing: "hosted";
|
|
69
|
+
} | {
|
|
70
|
+
backing: "none";
|
|
71
|
+
why: string;
|
|
72
|
+
} | {
|
|
73
|
+
backing: "conflict";
|
|
74
|
+
why: string;
|
|
75
|
+
};
|
|
76
|
+
export interface LocalQueueModule {
|
|
77
|
+
openLocalQueue(root: string, opts?: LocalQueueOptions): LocalQueue;
|
|
78
|
+
fileQueueGig(root: string): (args: Record<string, unknown>) => Promise<Record<string, unknown>>;
|
|
79
|
+
fileCancelGig(root: string): (args: Record<string, unknown>) => Promise<Record<string, unknown>>;
|
|
80
|
+
selectQueueBacking(env: Record<string, string | undefined>): QueueBackingChoice;
|
|
81
|
+
LOCAL_QUEUE_DIR_VAR: string;
|
|
82
|
+
DRAIN_VARS: readonly string[];
|
|
83
|
+
}
|
|
84
|
+
/** The env var whose presence selects the local backing. */
|
|
85
|
+
export declare const LOCAL_QUEUE_DIR_VAR = "COLTRANE_QUEUE_DIR";
|
|
86
|
+
/** The drain's five-variable contract (drain_preflight.ts:55-61), kept SORTED. The local path must
|
|
87
|
+
* never read a VALUE of any of these; selectQueueBacking only ever inspects key PRESENCE. */
|
|
88
|
+
export declare const DRAIN_VARS: readonly string[];
|
|
89
|
+
/**
|
|
90
|
+
* The single backing selector — file when the local dir is present, hosted for the drain env,
|
|
91
|
+
* conflict when both, none otherwise. It reads the VALUE of exactly one variable, LOCAL_QUEUE_DIR_VAR
|
|
92
|
+
* (not a drain var), and detects hosted presence by KEY PRESENCE via Object.keys — never by reading a
|
|
93
|
+
* drain variable's value. That distinction is load-bearing for F8: the test passes an env Proxy whose
|
|
94
|
+
* `get` trap throws on any drain-var read, so hosted detection must go through `ownKeys` (Object.keys
|
|
95
|
+
* does not trip the `get` trap), letting a local-present env resolve to `file` without touching one of
|
|
96
|
+
* the five. Values of the drain vars are never needed — their mere presence is the whole signal.
|
|
97
|
+
*/
|
|
98
|
+
export declare function selectQueueBacking(env: Record<string, string | undefined>): QueueBackingChoice;
|
|
99
|
+
export declare function openLocalQueue(root: string, opts?: LocalQueueOptions): LocalQueue;
|
|
100
|
+
/** The deps.queueGig-shaped enqueue seam — byte-compatible with postgrestQueueGig / rpcQueueGig
|
|
101
|
+
* ({gig_id, status:'queued'}), so a caller cannot tell which backing answered (I1). */
|
|
102
|
+
export declare function fileQueueGig(root: string): (args: Record<string, unknown>) => Promise<Record<string, unknown>>;
|
|
103
|
+
/** The deps.cancelGig-shaped seam — sibling of postgrestCancelGig ({gig_id, status:'cancelled'})
|
|
104
|
+
* (I18). */
|
|
105
|
+
export declare function fileCancelGig(root: string): (args: Record<string, unknown>) => Promise<Record<string, unknown>>;
|
|
@@ -0,0 +1,380 @@
|
|
|
1
|
+
// The local, file-backed gig queue — the third gig backing, the offline sibling of the two HTTP
|
|
2
|
+
// seams (postgrestQueueGig / rpcQueueGig, src/genome_store.ts:532,:493). It closes the asymmetry the
|
|
3
|
+
// SPEC (docs/specs/SPEC-local-queue-contract.md) names: the genome port already ships a LOCAL sibling
|
|
4
|
+
// (fileGenomeStore, src/genome_store.ts:77) so a genome can be READ from files, but the queue port
|
|
5
|
+
// had none — so an open-source user could run gigs in-process yet never enqueue. This module is that
|
|
6
|
+
// missing sibling: clone → build → enqueue → `coltrane work` in another terminal claims and runs it,
|
|
7
|
+
// with no Supabase, no service origin, no cdk_ key, no minting backend.
|
|
8
|
+
//
|
|
9
|
+
// MUTUAL EXCLUSION WITHOUT A DATABASE OR A LOCK. State is a directory-per-state layout under `root`
|
|
10
|
+
// (queued/, claimed/, parked/, done/, failed/, cancelled/), and every transition is a single POSIX
|
|
11
|
+
// rename(2), which is atomic within a filesystem. A claim renames queued/<id> → claimed/<id>: the
|
|
12
|
+
// winner's rename removes the source, and every loser's rename of the same source fails ENOENT — that
|
|
13
|
+
// is how a loser learns it lost (maildir new/→cur/, cr.yp.to; FSQ tmp/→queue/→done/). No lockfile
|
|
14
|
+
// (which would leave stale residue reintroducing the race), no in-process mutex (which cannot cross
|
|
15
|
+
// the server / `coltrane work` process boundary this queue exists to bridge), no SQLite (a forbidden
|
|
16
|
+
// dependency). node's own fs is sufficient, and the atomicity argument depends on staying on one FS.
|
|
17
|
+
//
|
|
18
|
+
// LEASE, not lock. A claim carries a lease {holder, renewed_at, expires_at} embedded in the moved
|
|
19
|
+
// file, so one atomic rename carries the claim AND its lease together — there is never a window where
|
|
20
|
+
// a claim exists without its lease. The holder renews with heartbeat (strictly advances the lease);
|
|
21
|
+
// a crashed holder's grip falls open by timeout, and reap() returns lapsed claims to queued while
|
|
22
|
+
// never touching a fresh one. A clock may be injected (opts.clock) so lease/reap laws are
|
|
23
|
+
// deterministic and fast; it defaults to Date.now().
|
|
24
|
+
//
|
|
25
|
+
// IDEMPOTENT EFFECT. Because a lapsed lease lets a second worker re-run a gig, the honest guarantee
|
|
26
|
+
// is at-least-once + idempotent-effect: complete() seals the output under content_sha (the same
|
|
27
|
+
// sha256Hex(canonJson(...)) primitive the rest of the codebase uses), a re-run with the SAME output
|
|
28
|
+
// re-seals rather than duplicating (duplicated:true), and a re-run that would seal a DIFFERENT output
|
|
29
|
+
// fails closed rather than forking.
|
|
30
|
+
//
|
|
31
|
+
// This module is NOT wired into any surface (deps.queueGig / deps.cancelGig) — shipping the module
|
|
32
|
+
// and its laws going green is a separate act from selecting it as a backing.
|
|
33
|
+
import * as fs from "node:fs";
|
|
34
|
+
import * as fsp from "node:fs/promises";
|
|
35
|
+
import { join } from "node:path";
|
|
36
|
+
import { randomUUID } from "node:crypto";
|
|
37
|
+
import { sha256Hex, canonJson } from "./canonical_form.js";
|
|
38
|
+
// ── The env-presence contract. ────────────────────────────────────────────────────────────────────
|
|
39
|
+
/** The env var whose presence selects the local backing. */
|
|
40
|
+
export const LOCAL_QUEUE_DIR_VAR = "COLTRANE_QUEUE_DIR";
|
|
41
|
+
/** The drain's five-variable contract (drain_preflight.ts:55-61), kept SORTED. The local path must
|
|
42
|
+
* never read a VALUE of any of these; selectQueueBacking only ever inspects key PRESENCE. */
|
|
43
|
+
export const DRAIN_VARS = [
|
|
44
|
+
"COLTRANE_DRAIN_KEY",
|
|
45
|
+
"COLTRANE_DRAIN_URL",
|
|
46
|
+
"COLTRANE_INSTANCE",
|
|
47
|
+
"COLTRANE_STORE_ANON",
|
|
48
|
+
"COLTRANE_STORE_URL",
|
|
49
|
+
];
|
|
50
|
+
/**
|
|
51
|
+
* The single backing selector — file when the local dir is present, hosted for the drain env,
|
|
52
|
+
* conflict when both, none otherwise. It reads the VALUE of exactly one variable, LOCAL_QUEUE_DIR_VAR
|
|
53
|
+
* (not a drain var), and detects hosted presence by KEY PRESENCE via Object.keys — never by reading a
|
|
54
|
+
* drain variable's value. That distinction is load-bearing for F8: the test passes an env Proxy whose
|
|
55
|
+
* `get` trap throws on any drain-var read, so hosted detection must go through `ownKeys` (Object.keys
|
|
56
|
+
* does not trip the `get` trap), letting a local-present env resolve to `file` without touching one of
|
|
57
|
+
* the five. Values of the drain vars are never needed — their mere presence is the whole signal.
|
|
58
|
+
*/
|
|
59
|
+
export function selectQueueBacking(env) {
|
|
60
|
+
const localRaw = env[LOCAL_QUEUE_DIR_VAR];
|
|
61
|
+
const localPresent = typeof localRaw === "string" && localRaw.length > 0;
|
|
62
|
+
const keys = Object.keys(env);
|
|
63
|
+
const hostedPresent = DRAIN_VARS.some((v) => keys.includes(v));
|
|
64
|
+
if (localPresent && hostedPresent) {
|
|
65
|
+
return {
|
|
66
|
+
backing: "conflict",
|
|
67
|
+
why: `both ${LOCAL_QUEUE_DIR_VAR} and the hosted drain environment are set — refusing to guess which backing owns the gig`,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
if (localPresent)
|
|
71
|
+
return { backing: "file", root: localRaw };
|
|
72
|
+
if (hostedPresent)
|
|
73
|
+
return { backing: "hosted" };
|
|
74
|
+
return {
|
|
75
|
+
backing: "none",
|
|
76
|
+
why: `no queue backing configured — set ${LOCAL_QUEUE_DIR_VAR} for a local file queue, or the drain environment for the hosted queue`,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
function isRecord(v) {
|
|
80
|
+
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
81
|
+
}
|
|
82
|
+
const DEFAULT_LEASE_MS = 30_000;
|
|
83
|
+
export function openLocalQueue(root, opts) {
|
|
84
|
+
const leaseMs = opts?.leaseMs ?? DEFAULT_LEASE_MS;
|
|
85
|
+
const clock = opts?.clock ?? Date.now;
|
|
86
|
+
const queuedDir = join(root, "queued");
|
|
87
|
+
const claimedDir = join(root, "claimed");
|
|
88
|
+
const parkedDir = join(root, "parked");
|
|
89
|
+
const doneDir = join(root, "done");
|
|
90
|
+
const failedDir = join(root, "failed");
|
|
91
|
+
const cancelledDir = join(root, "cancelled");
|
|
92
|
+
const tmpDir = join(root, "tmp");
|
|
93
|
+
const dirsByState = [
|
|
94
|
+
[queuedDir, "queued"],
|
|
95
|
+
[claimedDir, "claimed"],
|
|
96
|
+
[parkedDir, "awaiting_approval"],
|
|
97
|
+
[doneDir, "complete"],
|
|
98
|
+
[failedDir, "failed"],
|
|
99
|
+
[cancelledDir, "cancelled"],
|
|
100
|
+
];
|
|
101
|
+
// A tmp+rename atomic write: a reader of the destination sees either the OLD bytes or the whole NEW
|
|
102
|
+
// bytes, never a mixture. tmp lives in a dedicated dir so a claim's readdir of queued/ never lists a
|
|
103
|
+
// half-written temp file (F4).
|
|
104
|
+
async function atomicWrite(dir, name, obj) {
|
|
105
|
+
await fsp.mkdir(tmpDir, { recursive: true });
|
|
106
|
+
const tmp = join(tmpDir, `${randomUUID()}.tmp`);
|
|
107
|
+
await fsp.writeFile(tmp, JSON.stringify(obj), "utf8");
|
|
108
|
+
await fsp.mkdir(dir, { recursive: true });
|
|
109
|
+
await fsp.rename(tmp, join(dir, name));
|
|
110
|
+
}
|
|
111
|
+
async function readGig(path) {
|
|
112
|
+
try {
|
|
113
|
+
return JSON.parse(await fsp.readFile(path, "utf8"));
|
|
114
|
+
}
|
|
115
|
+
catch {
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
function isEnoent(e) {
|
|
120
|
+
return typeof e === "object" && e !== null && e.code === "ENOENT";
|
|
121
|
+
}
|
|
122
|
+
async function enqueue(args) {
|
|
123
|
+
// F10 — a payload with no standard to run is unrunnable. Refuse at enqueue and persist nothing,
|
|
124
|
+
// rather than enqueue a row that can only fail on a drain thirty minutes later.
|
|
125
|
+
const slug = args["standard_slug"];
|
|
126
|
+
if (typeof slug !== "string" || slug.length === 0) {
|
|
127
|
+
throw new Error("enqueue requires a non-empty standard_slug — nothing to run");
|
|
128
|
+
}
|
|
129
|
+
const gig_id = randomUUID();
|
|
130
|
+
const now = clock();
|
|
131
|
+
const record = {
|
|
132
|
+
gig_id,
|
|
133
|
+
standard_slug: slug,
|
|
134
|
+
standard_version: typeof args["standard_version"] === "number" ? args["standard_version"] : null,
|
|
135
|
+
mode: typeof args["mode"] === "string" ? args["mode"] : "live",
|
|
136
|
+
input: isRecord(args["input"]) ? args["input"] : {},
|
|
137
|
+
acting_for: typeof args["acting_for"] === "string" ? args["acting_for"] : "",
|
|
138
|
+
venue: typeof args["venue"] === "string" ? args["venue"] : null,
|
|
139
|
+
enqueued_at: now,
|
|
140
|
+
};
|
|
141
|
+
// F1/F4 — write then rename over queued/<gig_id>. If the root cannot be persisted to (mkdir/rename
|
|
142
|
+
// fails, e.g. ENOTDIR under a regular file), this throws and NO success is reported.
|
|
143
|
+
await atomicWrite(queuedDir, gig_id, record);
|
|
144
|
+
return { gig_id, status: "queued" };
|
|
145
|
+
}
|
|
146
|
+
async function claim(worker) {
|
|
147
|
+
let candidates;
|
|
148
|
+
try {
|
|
149
|
+
candidates = (await fsp.readdir(queuedDir)).sort();
|
|
150
|
+
}
|
|
151
|
+
catch {
|
|
152
|
+
return null; // no queued/ dir yet ⇒ nothing to claim
|
|
153
|
+
}
|
|
154
|
+
await fsp.mkdir(claimedDir, { recursive: true });
|
|
155
|
+
for (const id of candidates) {
|
|
156
|
+
const src = join(queuedDir, id);
|
|
157
|
+
const dst = join(claimedDir, id);
|
|
158
|
+
try {
|
|
159
|
+
// The arbiter. Exactly one concurrent claimer's rename of a given source succeeds; every other
|
|
160
|
+
// gets ENOENT (I4/I5/F3) and moves on to the next candidate (I19). A live gig is not in
|
|
161
|
+
// queued/ at all, so it is never a candidate here (F7).
|
|
162
|
+
await fsp.rename(src, dst);
|
|
163
|
+
}
|
|
164
|
+
catch {
|
|
165
|
+
// A lost rename race (ENOENT) is how a loser learns it lost; any other per-row fault is
|
|
166
|
+
// likewise "could not take this row". Either way, move to the next candidate — a lost row is
|
|
167
|
+
// the null sentinel, never a thrown claim (I5/F3).
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
170
|
+
const record = await readGig(dst);
|
|
171
|
+
if (record === null)
|
|
172
|
+
continue;
|
|
173
|
+
const now = clock();
|
|
174
|
+
const lease = { holder: worker, renewed_at: now, expires_at: now + leaseMs };
|
|
175
|
+
record.worker = worker;
|
|
176
|
+
record.lease = lease;
|
|
177
|
+
// Persist the lease alongside the claim (idempotent overwrite of a file only this claimer owns).
|
|
178
|
+
await atomicWrite(claimedDir, id, record);
|
|
179
|
+
const claimed = {
|
|
180
|
+
gig_id: record.gig_id,
|
|
181
|
+
standard_slug: record.standard_slug,
|
|
182
|
+
standard_version: record.standard_version,
|
|
183
|
+
mode: record.mode,
|
|
184
|
+
input: record.input,
|
|
185
|
+
acting_for: record.acting_for,
|
|
186
|
+
venue: record.venue,
|
|
187
|
+
worker,
|
|
188
|
+
lease,
|
|
189
|
+
approvals: record.approvals ?? null,
|
|
190
|
+
};
|
|
191
|
+
return claimed;
|
|
192
|
+
}
|
|
193
|
+
return null;
|
|
194
|
+
}
|
|
195
|
+
async function heartbeat(worker, gig_id) {
|
|
196
|
+
const path = join(claimedDir, gig_id);
|
|
197
|
+
const record = await readGig(path);
|
|
198
|
+
// F5 — the caller must still HOLD the claim. A heartbeat against a lease that was lost (the gig
|
|
199
|
+
// lapsed and someone else took it) or that no longer exists is a loss, never a false "still held".
|
|
200
|
+
if (record === null || record.lease === undefined || record.lease.holder !== worker)
|
|
201
|
+
return false;
|
|
202
|
+
const now = clock();
|
|
203
|
+
record.lease = { holder: worker, renewed_at: now, expires_at: now + leaseMs };
|
|
204
|
+
record.worker = worker;
|
|
205
|
+
await atomicWrite(claimedDir, gig_id, record);
|
|
206
|
+
return true;
|
|
207
|
+
}
|
|
208
|
+
function reap() {
|
|
209
|
+
// SYNCHRONOUS and best-effort (mirrors worker.ts reapWorkerState): a single unreadable claim never
|
|
210
|
+
// aborts the sweep. Only claimed/ is scanned, so a parked or terminal gig is structurally out of
|
|
211
|
+
// reach (I13) rather than guarded by a per-file predicate a future edit could forget.
|
|
212
|
+
const requeued = [];
|
|
213
|
+
const errors = [];
|
|
214
|
+
let kept = 0;
|
|
215
|
+
const now = clock();
|
|
216
|
+
let entries;
|
|
217
|
+
try {
|
|
218
|
+
entries = fs.readdirSync(claimedDir);
|
|
219
|
+
}
|
|
220
|
+
catch {
|
|
221
|
+
return { requeued, kept, errors };
|
|
222
|
+
}
|
|
223
|
+
for (const id of entries) {
|
|
224
|
+
const path = join(claimedDir, id);
|
|
225
|
+
try {
|
|
226
|
+
const record = JSON.parse(fs.readFileSync(path, "utf8"));
|
|
227
|
+
const expires = record.lease?.expires_at;
|
|
228
|
+
if (typeof expires === "number" && expires <= now) {
|
|
229
|
+
// Lapsed (I9/I10/I11) — return it to queued for the next worker. The rename is atomic; the
|
|
230
|
+
// stale lease left in the file is inert (list() reports no lease for a queued row, and the
|
|
231
|
+
// next claim overwrites it).
|
|
232
|
+
fs.mkdirSync(queuedDir, { recursive: true });
|
|
233
|
+
fs.renameSync(path, join(queuedDir, id));
|
|
234
|
+
requeued.push(id);
|
|
235
|
+
}
|
|
236
|
+
else {
|
|
237
|
+
kept++; // fresh (I8/I10) — never touched
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
catch (e) {
|
|
241
|
+
errors.push(`${id}: ${e instanceof Error ? e.message : String(e)}`);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
return { requeued, kept, errors };
|
|
245
|
+
}
|
|
246
|
+
async function park(worker, gig_id) {
|
|
247
|
+
const src = join(claimedDir, gig_id);
|
|
248
|
+
const record = await readGig(src);
|
|
249
|
+
if (record === null || record.lease?.holder !== worker)
|
|
250
|
+
return false;
|
|
251
|
+
// Move claimed/ → parked/ first (one atomic rename), THEN clear the lease. The gig is never in two
|
|
252
|
+
// places, and the reaper (which scans only claimed/) can never mistake it for an abandoned claim
|
|
253
|
+
// (I13). awaiting_approval is not a live claim.
|
|
254
|
+
await fsp.mkdir(parkedDir, { recursive: true });
|
|
255
|
+
await fsp.rename(src, join(parkedDir, gig_id));
|
|
256
|
+
delete record.lease;
|
|
257
|
+
delete record.worker;
|
|
258
|
+
await atomicWrite(parkedDir, gig_id, record);
|
|
259
|
+
return true;
|
|
260
|
+
}
|
|
261
|
+
async function approve(gig_id, role, verdict, approved_by) {
|
|
262
|
+
// F6 — an empty verdict is not an approval. Fail closed rather than resume past the human chair on
|
|
263
|
+
// a verdict that says nothing.
|
|
264
|
+
if (!isRecord(verdict) || Object.keys(verdict).length === 0) {
|
|
265
|
+
throw new Error("an empty verdict is not an approval — refusing to resume past the human chair");
|
|
266
|
+
}
|
|
267
|
+
const src = join(parkedDir, gig_id);
|
|
268
|
+
const record = await readGig(src);
|
|
269
|
+
if (record === null)
|
|
270
|
+
return false;
|
|
271
|
+
const entry = approved_by !== undefined ? { verdict, approved_by } : { verdict };
|
|
272
|
+
const approvals = {
|
|
273
|
+
...(record.approvals ?? {}),
|
|
274
|
+
};
|
|
275
|
+
approvals[role] = entry;
|
|
276
|
+
record.approvals = approvals;
|
|
277
|
+
// Seal the verdict into the parked row, THEN move parked/ → queued/ in one atomic rename, so the
|
|
278
|
+
// re-claimable row appears in queued/ already carrying its verdicts (I12).
|
|
279
|
+
await atomicWrite(parkedDir, gig_id, record);
|
|
280
|
+
await fsp.mkdir(queuedDir, { recursive: true });
|
|
281
|
+
await fsp.rename(join(parkedDir, gig_id), join(queuedDir, gig_id));
|
|
282
|
+
return true;
|
|
283
|
+
}
|
|
284
|
+
async function cancel(args) {
|
|
285
|
+
const gig_id = typeof args["gig_id"] === "string" ? args["gig_id"] : String(args["gig_id"] ?? "");
|
|
286
|
+
// Sibling of postgrestCancelGig: cancels a QUEUED row so it can never be claimed (I18). Moving it
|
|
287
|
+
// out of queued/ is the whole mechanism — a cancelled gig is simply no longer a claim candidate.
|
|
288
|
+
try {
|
|
289
|
+
await fsp.mkdir(cancelledDir, { recursive: true });
|
|
290
|
+
await fsp.rename(join(queuedDir, gig_id), join(cancelledDir, gig_id));
|
|
291
|
+
}
|
|
292
|
+
catch (e) {
|
|
293
|
+
if (!isEnoent(e))
|
|
294
|
+
throw e; // not-queued (already gone) is idempotent; a real IO fault is not
|
|
295
|
+
}
|
|
296
|
+
return { gig_id, status: "cancelled" };
|
|
297
|
+
}
|
|
298
|
+
async function locate(gig_id) {
|
|
299
|
+
for (const [dir] of dirsByState) {
|
|
300
|
+
const path = join(dir, gig_id);
|
|
301
|
+
try {
|
|
302
|
+
await fsp.access(path);
|
|
303
|
+
return { dir, path };
|
|
304
|
+
}
|
|
305
|
+
catch {
|
|
306
|
+
/* not in this state dir */
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
return null;
|
|
310
|
+
}
|
|
311
|
+
async function complete(worker, gig_id, output) {
|
|
312
|
+
const loc = await locate(gig_id);
|
|
313
|
+
if (loc === null)
|
|
314
|
+
throw new Error(`cannot complete unknown gig ${gig_id}`);
|
|
315
|
+
const record = await readGig(loc.path);
|
|
316
|
+
if (record === null)
|
|
317
|
+
throw new Error(`cannot complete unreadable gig ${gig_id}`);
|
|
318
|
+
// content_sha is a pure function of the output via the codebase's own canonical hash, so two
|
|
319
|
+
// separate completions of identical output hash identically by construction (I14).
|
|
320
|
+
const content_sha = sha256Hex(canonJson(output));
|
|
321
|
+
if (record.content_sha !== undefined) {
|
|
322
|
+
// I14 — a re-run (after a lapsed lease let a second worker take the gig) re-seals the SAME output
|
|
323
|
+
// rather than duplicating. F9 — a DIFFERENT output for the same gig fails closed, never forks.
|
|
324
|
+
if (record.content_sha === content_sha)
|
|
325
|
+
return { content_sha, duplicated: true };
|
|
326
|
+
throw new Error(`gig ${gig_id} already sealed a different output — refusing to fork the result`);
|
|
327
|
+
}
|
|
328
|
+
// Record the seal in place. The gig stays where it is (typically still claimed): a lapsed lease can
|
|
329
|
+
// then let a second worker re-run it, and that re-run dedups against this recorded content_sha.
|
|
330
|
+
record.content_sha = content_sha;
|
|
331
|
+
record.output = output;
|
|
332
|
+
await atomicWrite(loc.dir, gig_id, record);
|
|
333
|
+
return { content_sha, duplicated: false };
|
|
334
|
+
}
|
|
335
|
+
function list() {
|
|
336
|
+
const views = [];
|
|
337
|
+
for (const [dir, state] of dirsByState) {
|
|
338
|
+
let entries;
|
|
339
|
+
try {
|
|
340
|
+
entries = fs.readdirSync(dir);
|
|
341
|
+
}
|
|
342
|
+
catch {
|
|
343
|
+
continue; // dir not created yet
|
|
344
|
+
}
|
|
345
|
+
for (const id of entries) {
|
|
346
|
+
if (state === "claimed") {
|
|
347
|
+
try {
|
|
348
|
+
const record = JSON.parse(fs.readFileSync(join(dir, id), "utf8"));
|
|
349
|
+
if (record.lease && typeof record.lease.holder === "string") {
|
|
350
|
+
views.push({ gig_id: id, state, holder: record.lease.holder, lease: record.lease });
|
|
351
|
+
continue;
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
catch {
|
|
355
|
+
/* fall through to the lease-less view */
|
|
356
|
+
}
|
|
357
|
+
views.push({ gig_id: id, state });
|
|
358
|
+
}
|
|
359
|
+
else {
|
|
360
|
+
views.push({ gig_id: id, state });
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
return views;
|
|
365
|
+
}
|
|
366
|
+
return { enqueue, claim, heartbeat, reap, park, approve, cancel, complete, list, leaseMs };
|
|
367
|
+
}
|
|
368
|
+
/** The deps.queueGig-shaped enqueue seam — byte-compatible with postgrestQueueGig / rpcQueueGig
|
|
369
|
+
* ({gig_id, status:'queued'}), so a caller cannot tell which backing answered (I1). */
|
|
370
|
+
export function fileQueueGig(root) {
|
|
371
|
+
const q = openLocalQueue(root);
|
|
372
|
+
return (args) => q.enqueue(args);
|
|
373
|
+
}
|
|
374
|
+
/** The deps.cancelGig-shaped seam — sibling of postgrestCancelGig ({gig_id, status:'cancelled'})
|
|
375
|
+
* (I18). */
|
|
376
|
+
export function fileCancelGig(root) {
|
|
377
|
+
const q = openLocalQueue(root);
|
|
378
|
+
return (args) => q.cancel(args);
|
|
379
|
+
}
|
|
380
|
+
//# sourceMappingURL=local_queue.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"local_queue.js","sourceRoot":"","sources":["../../src/local_queue.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
export { workOnce as residencyGigPath } from "./worker.js";
|
|
2
|
+
export type ResidencyState = "seated" | "listening" | "playing" | "hibernated" | "drained" | "unseated";
|
|
3
|
+
export declare const RESIDENCY_STATES: readonly ResidencyState[];
|
|
4
|
+
export declare const LIVE_STATES: ReadonlySet<ResidencyState>;
|
|
5
|
+
export type ResidencyOpKind = "claim" | "listen" | "play" | "wake_seal" | "ack" | "heartbeat" | "hibernate" | "thaw" | "drain" | "unseat" | "reap";
|
|
6
|
+
export declare const RESIDENCY_OPS: readonly ResidencyOpKind[];
|
|
7
|
+
export type ResidencyActor = "holder" | "reaper" | "operator";
|
|
8
|
+
export interface LegalTransition {
|
|
9
|
+
from: ResidencyState;
|
|
10
|
+
op: ResidencyOpKind;
|
|
11
|
+
to: ResidencyState;
|
|
12
|
+
}
|
|
13
|
+
export declare const LEGAL_TRANSITIONS: readonly LegalTransition[];
|
|
14
|
+
export declare const RESIDENCY_ROW_FIELDS: readonly string[];
|
|
15
|
+
export declare const REFLEX_BUDGET_TICKS = 8;
|
|
16
|
+
export interface ResidencyRecord {
|
|
17
|
+
agent_slug: string;
|
|
18
|
+
org: string;
|
|
19
|
+
venue_slug: string;
|
|
20
|
+
channel_id: string;
|
|
21
|
+
soul_output_id: string | null;
|
|
22
|
+
status: ResidencyState;
|
|
23
|
+
session_id: string | null;
|
|
24
|
+
cursor: number;
|
|
25
|
+
host: string | null;
|
|
26
|
+
lease_until: number;
|
|
27
|
+
heartbeat_at: number;
|
|
28
|
+
fence: number;
|
|
29
|
+
last_sealed_sha: string | null;
|
|
30
|
+
private_memory?: unknown;
|
|
31
|
+
}
|
|
32
|
+
export interface ResidencyOp {
|
|
33
|
+
kind: ResidencyOpKind;
|
|
34
|
+
by?: ResidencyActor;
|
|
35
|
+
fence?: number;
|
|
36
|
+
now?: number;
|
|
37
|
+
host?: string;
|
|
38
|
+
session_id?: string;
|
|
39
|
+
message_index?: number;
|
|
40
|
+
sealed_output_sha?: string | null;
|
|
41
|
+
cortex_alive?: boolean;
|
|
42
|
+
agent_slug?: string;
|
|
43
|
+
org?: string;
|
|
44
|
+
channel_id?: string;
|
|
45
|
+
}
|
|
46
|
+
export type ResidencyRefusal = "illegal_transition" | "wrong_party" | "stale_fence" | "dead_cortex" | "immutable_identity" | "cursor_without_seal" | "double_activation";
|
|
47
|
+
export type ResidencyTransition = {
|
|
48
|
+
ok: true;
|
|
49
|
+
next: ResidencyRecord;
|
|
50
|
+
} | {
|
|
51
|
+
ok: false;
|
|
52
|
+
reason: ResidencyRefusal;
|
|
53
|
+
};
|
|
54
|
+
export interface InboundMessage {
|
|
55
|
+
id: string;
|
|
56
|
+
text: string;
|
|
57
|
+
at: number;
|
|
58
|
+
}
|
|
59
|
+
export interface SimClock {
|
|
60
|
+
now(): number;
|
|
61
|
+
tick(n?: number): void;
|
|
62
|
+
}
|
|
63
|
+
export interface ReflexDeps {
|
|
64
|
+
invoke: (...a: unknown[]) => unknown;
|
|
65
|
+
clock: SimClock;
|
|
66
|
+
}
|
|
67
|
+
export interface ReflexResult {
|
|
68
|
+
acked: true;
|
|
69
|
+
elapsed_ticks: number;
|
|
70
|
+
}
|
|
71
|
+
export interface ResidencySpec {
|
|
72
|
+
agent_slug: string;
|
|
73
|
+
org: string;
|
|
74
|
+
venue_slug: string;
|
|
75
|
+
channel_id: string;
|
|
76
|
+
}
|
|
77
|
+
export interface BootDeps {
|
|
78
|
+
resolveAgent: (slug: string) => unknown | null;
|
|
79
|
+
resolveVenue: (slug: string) => unknown | null;
|
|
80
|
+
cortexPresent: () => boolean;
|
|
81
|
+
seatRow: (rec: ResidencyRecord) => void;
|
|
82
|
+
}
|
|
83
|
+
export type BootRefusal = "no_such_agent" | "no_such_venue" | "no_cortex";
|
|
84
|
+
export type BootResult = {
|
|
85
|
+
ok: true;
|
|
86
|
+
rec: ResidencyRecord;
|
|
87
|
+
} | {
|
|
88
|
+
ok: false;
|
|
89
|
+
refusal: BootRefusal;
|
|
90
|
+
};
|
|
91
|
+
export interface VenueLike {
|
|
92
|
+
credential_surface: readonly string[];
|
|
93
|
+
}
|
|
94
|
+
export type CredentialAdmission = {
|
|
95
|
+
ok: true;
|
|
96
|
+
} | {
|
|
97
|
+
ok: false;
|
|
98
|
+
reason: "credential_breach";
|
|
99
|
+
};
|
|
100
|
+
/**
|
|
101
|
+
* Apply one residency op under its party constraint. TOTAL: for any (state, op) pair in the closed
|
|
102
|
+
* domain it never throws — a refusal is a decision, always a discriminated {ok:false,reason}. Every
|
|
103
|
+
* pair outside LEGAL_TRANSITIONS is refused AND leaves state unchanged (the caller keeps `rec`).
|
|
104
|
+
*/
|
|
105
|
+
export declare function applyResidencyOp(rec: ResidencyRecord, op: ResidencyOp): ResidencyTransition;
|
|
106
|
+
/**
|
|
107
|
+
* Ack an inbound message in reflex: dumb by design. Invokes the model-invoker EXACTLY zero times
|
|
108
|
+
* (even when one is present) and completes within REFLEX_BUDGET_TICKS SIMULATED ticks on the
|
|
109
|
+
* injected clock — never a wall-clock read, so the budget holds identically on any machine.
|
|
110
|
+
*/
|
|
111
|
+
export declare function reflexAck(_msg: InboundMessage, deps: ReflexDeps): ReflexResult;
|
|
112
|
+
/**
|
|
113
|
+
* The named reader for a dead-hibernated residency: a hibernated seat whose lease has lapsed is
|
|
114
|
+
* forced to `unseated`. It reaps the DEAD and not the living — a hibernated seat still inside its
|
|
115
|
+
* lease, or any non-hibernated record, is left untouched (a refusal, so "nothing reaped" is
|
|
116
|
+
* unambiguous). `hibernated` is a valid state, so without this reader a dead hibernated residency
|
|
117
|
+
* would read as healthy because nothing queries it.
|
|
118
|
+
*/
|
|
119
|
+
export declare function reapResidency(rec: ResidencyRecord, now: number): ResidencyTransition;
|
|
120
|
+
/**
|
|
121
|
+
* A presence cannot read its OWN impressions as content: sealing an impression as evidence would let
|
|
122
|
+
* it witness itself. The self-read is scoped out — no impression content is ever returned.
|
|
123
|
+
*/
|
|
124
|
+
export declare function readOwnImpressions(_rec: ResidencyRecord): {
|
|
125
|
+
content: null;
|
|
126
|
+
scoped_out: true;
|
|
127
|
+
} | {
|
|
128
|
+
content: unknown;
|
|
129
|
+
};
|
|
130
|
+
/**
|
|
131
|
+
* Admit a credential class only if the venue explicitly declares it in its credential_surface. The
|
|
132
|
+
* venue is the SOLE hands contract; a class it does not declare — including a channel token, which
|
|
133
|
+
* belongs to the distinct voice axis — is a breach.
|
|
134
|
+
*/
|
|
135
|
+
export declare function admitVenueCredential(venue: VenueLike, credentialClass: string): CredentialAdmission;
|
|
136
|
+
/**
|
|
137
|
+
* Seat a residency, fail-closed. Three preflight checks run IN ORDER before seatRow ever runs, so a
|
|
138
|
+
* refused boot writes no row: an unresolvable agent → no_such_agent, an unresolvable venue →
|
|
139
|
+
* no_such_venue, an absent cortex → no_cortex. Cortex liveness is not a one-shot boot assertion — it
|
|
140
|
+
* also surfaces on every subsequent heartbeat (applyResidencyOp's dead_cortex path).
|
|
141
|
+
*/
|
|
142
|
+
export declare function bootResidency(spec: ResidencySpec, deps: BootDeps): BootResult;
|
|
143
|
+
/**
|
|
144
|
+
* The `reside` verb REUSES work's gig path — it does not fork the drain's five-variable env contract.
|
|
145
|
+
* Missing COLTRANE_STORE_URL / COLTRANE_STORE_ANON is a usage refusal (exit 2), the same door work
|
|
146
|
+
* uses (src/cli.ts:241-248). The gig itself is workOnce, re-exported above as residencyGigPath; the
|
|
147
|
+
* store/channel wiring is a deployment seam the laws inject rather than assert here.
|
|
148
|
+
*/
|
|
149
|
+
export declare function runReside(_argv: readonly string[], io: unknown): Promise<number>;
|