@cotal-ai/core 0.23.0 → 0.25.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/checkpoint-answer.d.ts +46 -0
- package/dist/checkpoint-answer.d.ts.map +1 -0
- package/dist/checkpoint-answer.js +112 -0
- package/dist/checkpoint-answer.js.map +1 -0
- package/dist/connector-config.d.ts +23 -1
- package/dist/connector-config.d.ts.map +1 -1
- package/dist/connector-config.js +13 -1
- package/dist/connector-config.js.map +1 -1
- package/dist/connector.d.ts +20 -0
- package/dist/connector.d.ts.map +1 -1
- package/dist/endpoint-action.d.ts +2 -0
- package/dist/endpoint-action.d.ts.map +1 -1
- package/dist/endpoint-action.js +13 -2
- package/dist/endpoint-action.js.map +1 -1
- package/dist/endpoint-binding.d.ts +61 -0
- package/dist/endpoint-binding.d.ts.map +1 -1
- package/dist/endpoint-binding.js +109 -0
- package/dist/endpoint-binding.js.map +1 -1
- package/dist/endpoint-checkpoint.d.ts +21 -0
- package/dist/endpoint-checkpoint.d.ts.map +1 -1
- package/dist/endpoint-checkpoint.js +41 -3
- package/dist/endpoint-checkpoint.js.map +1 -1
- package/dist/endpoint-envelope.d.ts.map +1 -1
- package/dist/endpoint-envelope.js +13 -2
- package/dist/endpoint-envelope.js.map +1 -1
- package/dist/endpoint-invoke.d.ts.map +1 -1
- package/dist/endpoint-invoke.js +40 -4
- package/dist/endpoint-invoke.js.map +1 -1
- package/dist/endpoint-records.d.ts.map +1 -1
- package/dist/endpoint-records.js +86 -0
- package/dist/endpoint-records.js.map +1 -1
- package/dist/endpoint.d.ts.map +1 -1
- package/dist/endpoint.js +21 -0
- package/dist/endpoint.js.map +1 -1
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -0
- package/dist/index.js.map +1 -1
- package/dist/launch-material.d.ts +86 -0
- package/dist/launch-material.d.ts.map +1 -0
- package/dist/launch-material.js +217 -0
- package/dist/launch-material.js.map +1 -0
- package/dist/provision.d.ts.map +1 -1
- package/dist/provision.js +54 -11
- package/dist/provision.js.map +1 -1
- package/dist/run-journal.d.ts +337 -0
- package/dist/run-journal.d.ts.map +1 -0
- package/dist/run-journal.js +703 -0
- package/dist/run-journal.js.map +1 -0
- package/dist/run-migration.d.ts +85 -0
- package/dist/run-migration.d.ts.map +1 -0
- package/dist/run-migration.js +201 -0
- package/dist/run-migration.js.map +1 -0
- package/dist/run-notice.d.ts +84 -0
- package/dist/run-notice.d.ts.map +1 -0
- package/dist/run-notice.js +212 -0
- package/dist/run-notice.js.map +1 -0
- package/dist/run-record.d.ts +104 -0
- package/dist/run-record.d.ts.map +1 -0
- package/dist/run-record.js +58 -0
- package/dist/run-record.js.map +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The RUN NOTICE record: a bounded decision a workflow told an agent about, written onto the run.
|
|
3
|
+
*
|
|
4
|
+
* **A record and not a channel post.** `notify` is the only primitive that moves program-authored
|
|
5
|
+
* bytes toward an agent's context, and a post would put the program into the conversation as a
|
|
6
|
+
* participant — the one thing this plane's first non-negotiable forbids. So a notice is filed here,
|
|
7
|
+
* addressed to one agent, and rendered as a fixed key→value table ahead of that agent's next turn.
|
|
8
|
+
*
|
|
9
|
+
* **Keyed by a DERIVED addressee id.** An agent's name is dotted and a dot is the records-key
|
|
10
|
+
* separator, so a raw name in the key would silently re-tokenize it into a key of another shape —
|
|
11
|
+
* and a name is not an id token in the first place. The id is a digest of the name; a reader holding
|
|
12
|
+
* the handle re-derives it, so enumerating one agent's notices is still a single prefix scan.
|
|
13
|
+
*
|
|
14
|
+
* **The spec is create-only; the status is the consumption.** A notice is something the program
|
|
15
|
+
* decided, so its content is immutable — a retry after a crash re-derives the same id and lands on
|
|
16
|
+
* its own record rather than filing a second one. Whether it has been CONSUMED is a fact somebody
|
|
17
|
+
* else establishes later, which is why it lives in the status half: the migrate rule refuses to
|
|
18
|
+
* move a run whose notice has not yet been consumed by its addressee's next turn, and that rule
|
|
19
|
+
* needs something to read.
|
|
20
|
+
*/
|
|
21
|
+
import { createHash } from "node:crypto";
|
|
22
|
+
import { EpEnvelopeError } from "./endpoint-envelope.js";
|
|
23
|
+
import { assertIdToken } from "./endpoint-subjects.js";
|
|
24
|
+
import { RECORD_KINDS, createRecordEntry, readRecord, recordSpecKey, recordStatusKey, assertStatusValue, } from "./endpoint-records.js";
|
|
25
|
+
import { canonicalJson } from "./canonical.js";
|
|
26
|
+
const SPEC_KEYS = ["v", "run", "step", "addressee", "fact", "at"];
|
|
27
|
+
const STATUS_KEYS = ["v", "consumedAt", "by", "observedSpecRevision"];
|
|
28
|
+
function qualifiers(endpoint, runId, addresseeId, noticeId) {
|
|
29
|
+
return [endpoint, runId, addresseeId, noticeId];
|
|
30
|
+
}
|
|
31
|
+
/** base64url of the sha256, truncated to 43 characters — the same shape and alphabet as a run's
|
|
32
|
+
* request ids and a checkpoint answer id, so a derived id is an `<id-token>` by construction. */
|
|
33
|
+
function digestId(canonical) {
|
|
34
|
+
return createHash("sha256").update(canonical, "utf8").digest("base64url").slice(0, 43);
|
|
35
|
+
}
|
|
36
|
+
/** The key token standing for an agent. Derived from the name, never the name itself. */
|
|
37
|
+
export function noticeAddresseeId(agent) {
|
|
38
|
+
if (agent.length === 0)
|
|
39
|
+
throw new EpEnvelopeError("failed-precondition", `a notice addressee is an agent name; the empty string names nobody`);
|
|
40
|
+
return digestId(canonicalJson({ agent }));
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* The id one notice is filed under.
|
|
44
|
+
*
|
|
45
|
+
* Derived from the step's own request id and the addressee, so the same `notify` call re-run after
|
|
46
|
+
* a crash re-derives exactly the same ids and its create-only writes land on their own records. One
|
|
47
|
+
* call to N agents writes N notices, and the addressee is what separates them.
|
|
48
|
+
*/
|
|
49
|
+
export function runNoticeId(requestId, addressee) {
|
|
50
|
+
return digestId(canonicalJson({ requestId, addressee }));
|
|
51
|
+
}
|
|
52
|
+
function parseSpec(raw, key) {
|
|
53
|
+
if (raw === null || typeof raw !== "object" || Array.isArray(raw))
|
|
54
|
+
throw new EpEnvelopeError("internal", `notice ${key} is not an object; garbled state never authorizes`);
|
|
55
|
+
const o = raw;
|
|
56
|
+
for (const k of Object.keys(o))
|
|
57
|
+
if (!SPEC_KEYS.includes(k))
|
|
58
|
+
throw new EpEnvelopeError("internal", `notice ${key} carries the unknown field "${k}"; record schemas are closed`);
|
|
59
|
+
if (o.v !== 1 || typeof o.run !== "string" || typeof o.step !== "string"
|
|
60
|
+
|| typeof o.addressee !== "string" || o.addressee.length === 0
|
|
61
|
+
|| typeof o.at !== "number" || !Number.isSafeInteger(o.at) || o.at < 0)
|
|
62
|
+
throw new EpEnvelopeError("internal", `notice ${key} is malformed; garbled state never authorizes`);
|
|
63
|
+
const fact = o.fact;
|
|
64
|
+
if (fact === null || fact === undefined || typeof fact !== "object" || Array.isArray(fact)
|
|
65
|
+
|| typeof fact.decision !== "string" || typeof fact.outcome !== "string")
|
|
66
|
+
throw new EpEnvelopeError("internal", `notice ${key} carries no decision fact; garbled state never authorizes`);
|
|
67
|
+
return {
|
|
68
|
+
v: 1,
|
|
69
|
+
run: o.run,
|
|
70
|
+
step: o.step,
|
|
71
|
+
addressee: o.addressee,
|
|
72
|
+
fact: fact,
|
|
73
|
+
at: o.at,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
function parseStatus(raw, key) {
|
|
77
|
+
if (raw === null || typeof raw !== "object" || Array.isArray(raw))
|
|
78
|
+
throw new EpEnvelopeError("internal", `notice status ${key} is not an object; garbled state never authorizes`);
|
|
79
|
+
const o = raw;
|
|
80
|
+
for (const k of Object.keys(o))
|
|
81
|
+
if (!STATUS_KEYS.includes(k))
|
|
82
|
+
throw new EpEnvelopeError("internal", `notice status ${key} carries the unknown field "${k}"; record schemas are closed`);
|
|
83
|
+
if (o.v !== 1 || typeof o.consumedAt !== "number" || !Number.isSafeInteger(o.consumedAt) || o.consumedAt < 0
|
|
84
|
+
|| typeof o.by !== "string" || o.by.length === 0)
|
|
85
|
+
throw new EpEnvelopeError("internal", `notice status ${key} is malformed; garbled state never authorizes`);
|
|
86
|
+
return {
|
|
87
|
+
v: 1,
|
|
88
|
+
consumedAt: o.consumedAt,
|
|
89
|
+
by: o.by,
|
|
90
|
+
observedSpecRevision: o.observedSpecRevision,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* File one notice, create-only.
|
|
95
|
+
*
|
|
96
|
+
* A retry of the same notice is not a conflict: the id is derived from the step's request id and
|
|
97
|
+
* the addressee, so an existing record with identical bytes is this run's own earlier attempt.
|
|
98
|
+
* Different bytes under the same id would mean two different decisions claiming one identity, which
|
|
99
|
+
* is refused rather than overwritten — a notice that could be rewritten would be a message.
|
|
100
|
+
*/
|
|
101
|
+
export async function writeRunNotice(kv, endpoint, noticeId, value) {
|
|
102
|
+
assertIdToken(noticeId, "noticeId");
|
|
103
|
+
const addresseeId = noticeAddresseeId(value.addressee);
|
|
104
|
+
const key = recordSpecKey(RECORD_KINDS.notice, qualifiers(endpoint, value.run, addresseeId, noticeId));
|
|
105
|
+
try {
|
|
106
|
+
await createRecordEntry(kv, key, value);
|
|
107
|
+
return { key, created: true };
|
|
108
|
+
}
|
|
109
|
+
catch (e) {
|
|
110
|
+
if (!(e instanceof EpEnvelopeError && e.code === "conflict"))
|
|
111
|
+
throw e;
|
|
112
|
+
const existing = await kv.get(key);
|
|
113
|
+
if (!existing || existing.operation !== "PUT")
|
|
114
|
+
throw new EpEnvelopeError("internal", `notice ${key} lost its create CAS but is not readable; reconcile the store`);
|
|
115
|
+
const parsed = parseSpec(JSON.parse(new TextDecoder().decode(existing.value)), key);
|
|
116
|
+
if (canonicalJson(parsed) !== canonicalJson(value))
|
|
117
|
+
throw new EpEnvelopeError("conflict", `notice ${key} already exists with a different decision; a notice id is derived from the step that decided it, so a differing one under the same id is never overwritten`);
|
|
118
|
+
return { key, created: false };
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
/** Read one notice with its consumption, or `undefined` when nothing was filed under this id. */
|
|
122
|
+
export async function readRunNotice(kv, endpoint, runId, addressee, noticeId) {
|
|
123
|
+
return await readByQualifiers(kv, qualifiers(endpoint, runId, noticeAddresseeId(addressee), noticeId), noticeId);
|
|
124
|
+
}
|
|
125
|
+
/** The same read from the KEY's own tokens, for a caller enumerating a run rather than an agent. */
|
|
126
|
+
async function readByQualifiers(kv, q, noticeId) {
|
|
127
|
+
const merged = await readRecord(kv, RECORD_KINDS.notice, q);
|
|
128
|
+
if (merged === undefined)
|
|
129
|
+
return undefined;
|
|
130
|
+
const spec = parseSpec(merged.spec.value, recordSpecKey(RECORD_KINDS.notice, q));
|
|
131
|
+
const status = merged.status === undefined
|
|
132
|
+
? undefined
|
|
133
|
+
: parseStatus(merged.status.value, recordStatusKey(RECORD_KINDS.notice, q));
|
|
134
|
+
return {
|
|
135
|
+
noticeId,
|
|
136
|
+
spec,
|
|
137
|
+
specRevision: merged.spec.revision,
|
|
138
|
+
...(status !== undefined ? { consumed: status } : {}),
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Every notice addressed to one agent on one run, oldest first.
|
|
143
|
+
*
|
|
144
|
+
* ORDER IS PART OF THE CONTRACT, because the render is a table a person or an agent reads top to
|
|
145
|
+
* bottom: `at` first, then the notice id, so two notices filed in the same millisecond still come
|
|
146
|
+
* back in one fixed order rather than in whatever order the store enumerated its keys.
|
|
147
|
+
*/
|
|
148
|
+
export async function listRunNotices(kv, endpoint, runId, addressee) {
|
|
149
|
+
const addresseeId = noticeAddresseeId(addressee);
|
|
150
|
+
const prefix = recordSpecKey(RECORD_KINDS.notice, qualifiers(endpoint, runId, addresseeId, "x")).slice(0, -"x.spec".length);
|
|
151
|
+
// ONE wildcard token, because the addressee is fixed and only the notice id varies.
|
|
152
|
+
return await scan(kv, `${prefix}*.spec`);
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Every notice filed on one RUN, whoever it was addressed to, oldest first.
|
|
156
|
+
*
|
|
157
|
+
* The migrate rule asks a question the journal cannot answer on its own: a `notify` entry
|
|
158
|
+
* records an input HASH, not the agents it addressed, so "has this orphaned notify been consumed?"
|
|
159
|
+
* cannot be asked per addressee. It is asked per RUN, and each notice's own `step` says which entry
|
|
160
|
+
* it came from — which is why the step is in the value rather than only in the key.
|
|
161
|
+
*/
|
|
162
|
+
export async function listRunNoticesForRun(kv, endpoint, runId) {
|
|
163
|
+
const prefix = recordSpecKey(RECORD_KINDS.notice, qualifiers(endpoint, runId, "a", "n")).slice(0, -"a.n.spec".length);
|
|
164
|
+
// TWO, because both the addressee and the notice id vary — a KV filter's `*` is one token, and a
|
|
165
|
+
// single wildcard here would match a key shape that does not exist and return nothing at all.
|
|
166
|
+
return await scan(kv, `${prefix}*.*.spec`);
|
|
167
|
+
}
|
|
168
|
+
/** Read every notice a KV key filter matches. The filter is the caller's: `*` is one token. */
|
|
169
|
+
async function scan(kv, filter) {
|
|
170
|
+
const found = [];
|
|
171
|
+
const seen = await kv.keys(filter);
|
|
172
|
+
const qs = [];
|
|
173
|
+
for await (const key of seen) {
|
|
174
|
+
// The key is `notice.<endpoint>.<runId>.<addresseeId>.<noticeId>.spec` and every qualifier is
|
|
175
|
+
// an id token, so the four the read wants are the four before `spec`. Taken from the KEY and
|
|
176
|
+
// not re-derived: an enumeration holds the addressee's digest, never the name it came from.
|
|
177
|
+
const parts = key.split(".");
|
|
178
|
+
qs.push(parts.slice(parts.length - 5, parts.length - 1));
|
|
179
|
+
}
|
|
180
|
+
for (const q of qs) {
|
|
181
|
+
const one = await readByQualifiers(kv, q, q[q.length - 1]);
|
|
182
|
+
if (one !== undefined)
|
|
183
|
+
found.push(one);
|
|
184
|
+
}
|
|
185
|
+
found.sort((a, b) => (a.spec.at - b.spec.at) || (a.noticeId < b.noticeId ? -1 : a.noticeId > b.noticeId ? 1 : 0));
|
|
186
|
+
return found;
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* Record that a turn carried this notice.
|
|
190
|
+
*
|
|
191
|
+
* **The create-only CAS is the arbiter, and it is the only one.** Two turns racing to claim one
|
|
192
|
+
* notice both read no status and both write; the store decides, and the loser gets a loud conflict.
|
|
193
|
+
* A "has it been consumed already?" check before the write would be a fast path that lies under
|
|
194
|
+
* exactly the race it looks like it is preventing, so there is one gate rather than two — which
|
|
195
|
+
* turn delivered a notice is a fact about something that happened once.
|
|
196
|
+
*/
|
|
197
|
+
export async function markRunNoticeConsumed(kv, endpoint, runId, addressee, noticeId, by, at) {
|
|
198
|
+
const q = qualifiers(endpoint, runId, noticeAddresseeId(addressee), noticeId);
|
|
199
|
+
const specKey = recordSpecKey(RECORD_KINDS.notice, q);
|
|
200
|
+
const statusKey = recordStatusKey(RECORD_KINDS.notice, q);
|
|
201
|
+
const merged = await readRecord(kv, RECORD_KINDS.notice, q);
|
|
202
|
+
if (merged === undefined)
|
|
203
|
+
throw new EpEnvelopeError("failed-precondition", `no notice is filed at ${specKey}; a consumption names the notice it consumed`);
|
|
204
|
+
const value = assertStatusValue({
|
|
205
|
+
v: 1,
|
|
206
|
+
consumedAt: at,
|
|
207
|
+
by,
|
|
208
|
+
observedSpecRevision: merged.spec.revision,
|
|
209
|
+
});
|
|
210
|
+
await createRecordEntry(kv, statusKey, value);
|
|
211
|
+
}
|
|
212
|
+
//# sourceMappingURL=run-notice.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"run-notice.js","sourceRoot":"","sources":["../src/run-notice.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The run record: what a workflow run IS, beside the journal that says what it DID.
|
|
3
|
+
*
|
|
4
|
+
* The journal on WFJ is append-only and authoritative about events. This is last-value-wins and
|
|
5
|
+
* authoritative about state — who holds the run, what state it is in, and the PIN SET resolved once
|
|
6
|
+
* at start. Two runs of one program under different pins are two different runs, so a resume reads
|
|
7
|
+
* the pins back rather than re-deriving them: "the default" is a property of the interpreter, and
|
|
8
|
+
* the interpreter is the thing that may have changed between attempts.
|
|
9
|
+
*
|
|
10
|
+
* It also carries the one fact the journal cannot hold about itself: **how far the journal has
|
|
11
|
+
* got**. A record deleted from the TAIL of a run's subject leaves the survivors contiguous — the
|
|
12
|
+
* ordinal chain sees nothing missing, because the missing part is the part it would have compared
|
|
13
|
+
* against — and the per-subject head is recalculated backwards, so the fence accepts an append at a
|
|
14
|
+
* head the run already moved past. Measured end to end: the run replays SHORT and intact, a
|
|
15
|
+
* successor activates on it, and it resumes from a prefix that is missing work the run really did.
|
|
16
|
+
* No anchor inside the journal can detect that. `journalHigh` is the anchor OUTSIDE it.
|
|
17
|
+
*
|
|
18
|
+
* What that anchor does and does not cover, stated plainly because a guard believed to be wider
|
|
19
|
+
* than it is is worse than none: it is written at each ACTIVATION, so it detects truncation back
|
|
20
|
+
* past the last takeover. Steps appended since that activation are not covered — writing the record
|
|
21
|
+
* per append would double every step's cost, and the journal's own ordinal chain already covers
|
|
22
|
+
* every interior loss.
|
|
23
|
+
*/
|
|
24
|
+
import type { KV } from "@nats-io/kv";
|
|
25
|
+
import { type MergedRecord } from "./endpoint-records.js";
|
|
26
|
+
/**
|
|
27
|
+
* The pins, resolved. Structurally the language's `RunPins` — `@cotal-ai/core` does not depend on
|
|
28
|
+
* `@cotal-ai/lang`, and the wire shape is the contract between them rather than a shared type.
|
|
29
|
+
*/
|
|
30
|
+
export interface RunPinsValue {
|
|
31
|
+
readonly seed: string;
|
|
32
|
+
/** The run's logical epoch. `now()` derives from THIS, never from the resuming host's clock. */
|
|
33
|
+
readonly startedAt: number;
|
|
34
|
+
readonly yieldEvery: number;
|
|
35
|
+
readonly stepBudget: number;
|
|
36
|
+
readonly effectCeiling: number;
|
|
37
|
+
readonly languageVersion: string;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* The immutable half: what this run IS, decided once and never re-decided.
|
|
41
|
+
*
|
|
42
|
+
* Deliberately NOT carrying a program hash yet. The interpreter reports one, but only after a run,
|
|
43
|
+
* and a hash computed here from the source would be a different function's answer wearing the same
|
|
44
|
+
* name — the divergence check that matters is per-step and the language already makes it from the
|
|
45
|
+
* recorded input hashes. A source-identity pin is its own item, not a field filled with a guess.
|
|
46
|
+
*/
|
|
47
|
+
export interface RunSpecValue {
|
|
48
|
+
readonly v: 1;
|
|
49
|
+
readonly run: string;
|
|
50
|
+
readonly pins: RunPinsValue;
|
|
51
|
+
readonly createdAt: number;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* What a run is doing now.
|
|
55
|
+
*
|
|
56
|
+
* `released` and `failed` are not the same thing and the distinction is the point: a program that
|
|
57
|
+
* failed has a result and the journal has it, while a released run has no result at all — its
|
|
58
|
+
* driver stopped holding it. Recording one as the other would write down a conclusion about work
|
|
59
|
+
* nobody observed.
|
|
60
|
+
*/
|
|
61
|
+
export type RunState = "running" | "released" | "completed" | "failed";
|
|
62
|
+
export interface RunStatusValue {
|
|
63
|
+
readonly v: 1;
|
|
64
|
+
readonly observedSpecRevision: number;
|
|
65
|
+
readonly state: RunState;
|
|
66
|
+
readonly holder: string;
|
|
67
|
+
readonly epoch: number;
|
|
68
|
+
readonly fencingToken: number;
|
|
69
|
+
/**
|
|
70
|
+
* The highest journal ordinal this run is KNOWN to have reached, written at each activation.
|
|
71
|
+
* A replay whose last ordinal is BELOW this has lost records from its tail.
|
|
72
|
+
*/
|
|
73
|
+
readonly journalHigh: number;
|
|
74
|
+
readonly at: number;
|
|
75
|
+
}
|
|
76
|
+
/** A run's journal is missing records from its END — which nothing inside the journal can see. */
|
|
77
|
+
export declare class RunJournalTailTruncated extends Error {
|
|
78
|
+
readonly run: string;
|
|
79
|
+
readonly recordedHigh: number;
|
|
80
|
+
readonly replayedHigh: number;
|
|
81
|
+
constructor(run: string, recordedHigh: number, replayedHigh: number);
|
|
82
|
+
}
|
|
83
|
+
/** Read a run's record. `undefined` = this run has never been started. */
|
|
84
|
+
export declare function readRunRecord(kv: KV, endpoint: string, runId: string): Promise<MergedRecord<RunSpecValue, RunStatusValue> | undefined>;
|
|
85
|
+
/**
|
|
86
|
+
* Create a run's spec. Create-only: a spec that already exists means this run was started before,
|
|
87
|
+
* and starting it again under a fresh set of pins would be a different run wearing the same id.
|
|
88
|
+
*/
|
|
89
|
+
export declare function createRunSpec(kv: KV, endpoint: string, runId: string, value: Omit<RunSpecValue, "v" | "run">): Promise<number>;
|
|
90
|
+
/**
|
|
91
|
+
* Write a run's status. `expectedRevision` is `undefined` for the first write and the last read
|
|
92
|
+
* revision thereafter — the CAS is what keeps two drivers from taking turns overwriting each
|
|
93
|
+
* other's view of who holds the run.
|
|
94
|
+
*/
|
|
95
|
+
export declare function writeRunStatus(kv: KV, endpoint: string, runId: string, value: Omit<RunStatusValue, "v">, expectedRevision?: number): Promise<number>;
|
|
96
|
+
/**
|
|
97
|
+
* The tail check, run BETWEEN the replay and the activation.
|
|
98
|
+
*
|
|
99
|
+
* Before, and not after, because an activation appended over a rolled-back head is itself a record
|
|
100
|
+
* written into a journal we already know is wrong — and it would be written at an ordinal the
|
|
101
|
+
* deleted records already used, which is a hole the chain would then blame on someone else.
|
|
102
|
+
*/
|
|
103
|
+
export declare function assertJournalTailIntact(runId: string, recorded: RunStatusValue | undefined, replayedHigh: number): void;
|
|
104
|
+
//# sourceMappingURL=run-record.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"run-record.d.ts","sourceRoot":"","sources":["../src/run-record.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,OAAO,KAAK,EAAE,EAAE,EAAE,MAAM,aAAa,CAAC;AACtC,OAAO,EAQL,KAAK,YAAY,EAClB,MAAM,uBAAuB,CAAC;AAE/B;;;GAGG;AACH,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,gGAAgG;IAChG,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;IAC/B,QAAQ,CAAC,eAAe,EAAE,MAAM,CAAC;CAClC;AAED;;;;;;;GAOG;AACH,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,CAAC,EAAE,CAAC,CAAC;IACd,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,IAAI,EAAE,YAAY,CAAC;IAC5B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;CAC5B;AAED;;;;;;;GAOG;AACH,MAAM,MAAM,QAAQ,GAAG,SAAS,GAAG,UAAU,GAAG,WAAW,GAAG,QAAQ,CAAC;AAEvE,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,CAAC,EAAE,CAAC,CAAC;IACd,QAAQ,CAAC,oBAAoB,EAAE,MAAM,CAAC;IACtC,QAAQ,CAAC,KAAK,EAAE,QAAQ,CAAC;IACzB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B;;;OAGG;IACH,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;CACrB;AAED,kGAAkG;AAClG,qBAAa,uBAAwB,SAAQ,KAAK;IAE9C,QAAQ,CAAC,GAAG,EAAE,MAAM;IACpB,QAAQ,CAAC,YAAY,EAAE,MAAM;IAC7B,QAAQ,CAAC,YAAY,EAAE,MAAM;gBAFpB,GAAG,EAAE,MAAM,EACX,YAAY,EAAE,MAAM,EACpB,YAAY,EAAE,MAAM;CAUhC;AAED,0EAA0E;AAC1E,wBAAsB,aAAa,CACjC,EAAE,EAAE,EAAE,EACN,QAAQ,EAAE,MAAM,EAChB,KAAK,EAAE,MAAM,GACZ,OAAO,CAAC,YAAY,CAAC,YAAY,EAAE,cAAc,CAAC,GAAG,SAAS,CAAC,CAEjE;AAED;;;GAGG;AACH,wBAAsB,aAAa,CACjC,EAAE,EAAE,EAAE,EACN,QAAQ,EAAE,MAAM,EAChB,KAAK,EAAE,MAAM,EACb,KAAK,EAAE,IAAI,CAAC,YAAY,EAAE,GAAG,GAAG,KAAK,CAAC,GACrC,OAAO,CAAC,MAAM,CAAC,CAGjB;AAED;;;;GAIG;AACH,wBAAsB,cAAc,CAClC,EAAE,EAAE,EAAE,EACN,QAAQ,EAAE,MAAM,EAChB,KAAK,EAAE,MAAM,EACb,KAAK,EAAE,IAAI,CAAC,cAAc,EAAE,GAAG,CAAC,EAChC,gBAAgB,CAAC,EAAE,MAAM,GACxB,OAAO,CAAC,MAAM,CAAC,CAQjB;AAED;;;;;;GAMG;AACH,wBAAgB,uBAAuB,CACrC,KAAK,EAAE,MAAM,EACb,QAAQ,EAAE,cAAc,GAAG,SAAS,EACpC,YAAY,EAAE,MAAM,GACnB,IAAI,CAKN"}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { RECORD_KINDS, createRecordEntry, updateRecordEntry, readRecord, recordSpecKey, recordStatusKey, assertStatusValue, } from "./endpoint-records.js";
|
|
2
|
+
/** A run's journal is missing records from its END — which nothing inside the journal can see. */
|
|
3
|
+
export class RunJournalTailTruncated extends Error {
|
|
4
|
+
run;
|
|
5
|
+
recordedHigh;
|
|
6
|
+
replayedHigh;
|
|
7
|
+
constructor(run, recordedHigh, replayedHigh) {
|
|
8
|
+
super(`run ${run}: the journal replayed to ordinal ${replayedHigh}, but this run is recorded as having reached ${recordedHigh}. ` +
|
|
9
|
+
`Records are missing from the END of the subject: the surviving prefix is contiguous, so the ordinal chain cannot see it, ` +
|
|
10
|
+
`and the subject's head has rolled back with them. Resuming here would repeat work the run already did. ` +
|
|
11
|
+
`Recover the missing records or retire the run; do not drive it.`);
|
|
12
|
+
this.run = run;
|
|
13
|
+
this.recordedHigh = recordedHigh;
|
|
14
|
+
this.replayedHigh = replayedHigh;
|
|
15
|
+
this.name = "RunJournalTailTruncated";
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
/** Read a run's record. `undefined` = this run has never been started. */
|
|
19
|
+
export async function readRunRecord(kv, endpoint, runId) {
|
|
20
|
+
return await readRecord(kv, RECORD_KINDS.run, [endpoint, runId]);
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Create a run's spec. Create-only: a spec that already exists means this run was started before,
|
|
24
|
+
* and starting it again under a fresh set of pins would be a different run wearing the same id.
|
|
25
|
+
*/
|
|
26
|
+
export async function createRunSpec(kv, endpoint, runId, value) {
|
|
27
|
+
const spec = { v: 1, run: runId, ...value };
|
|
28
|
+
return await createRecordEntry(kv, recordSpecKey(RECORD_KINDS.run, [endpoint, runId]), spec);
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Write a run's status. `expectedRevision` is `undefined` for the first write and the last read
|
|
32
|
+
* revision thereafter — the CAS is what keeps two drivers from taking turns overwriting each
|
|
33
|
+
* other's view of who holds the run.
|
|
34
|
+
*/
|
|
35
|
+
export async function writeRunStatus(kv, endpoint, runId, value, expectedRevision) {
|
|
36
|
+
const status = assertStatusValue({ v: 1, ...value });
|
|
37
|
+
// Through the key BUILDER, never a join of our own: it is what asserts the endpoint and the run
|
|
38
|
+
// id are grammatical tokens, and a run id that tokenized loose would write into another run's key.
|
|
39
|
+
const key = recordStatusKey(RECORD_KINDS.run, [endpoint, runId]);
|
|
40
|
+
return expectedRevision === undefined
|
|
41
|
+
? await createRecordEntry(kv, key, status)
|
|
42
|
+
: await updateRecordEntry(kv, key, status, expectedRevision);
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* The tail check, run BETWEEN the replay and the activation.
|
|
46
|
+
*
|
|
47
|
+
* Before, and not after, because an activation appended over a rolled-back head is itself a record
|
|
48
|
+
* written into a journal we already know is wrong — and it would be written at an ordinal the
|
|
49
|
+
* deleted records already used, which is a hole the chain would then blame on someone else.
|
|
50
|
+
*/
|
|
51
|
+
export function assertJournalTailIntact(runId, recorded, replayedHigh) {
|
|
52
|
+
if (recorded === undefined)
|
|
53
|
+
return;
|
|
54
|
+
if (replayedHigh < recorded.journalHigh) {
|
|
55
|
+
throw new RunJournalTailTruncated(runId, recorded.journalHigh, replayedHigh);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
//# sourceMappingURL=run-record.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"run-record.js","sourceRoot":"","sources":["../src/run-record.ts"],"names":[],"mappings":"AAwBA,OAAO,EACL,YAAY,EACZ,iBAAiB,EACjB,iBAAiB,EACjB,UAAU,EACV,aAAa,EACb,eAAe,EACf,iBAAiB,GAElB,MAAM,uBAAuB,CAAC;AAwD/B,kGAAkG;AAClG,MAAM,OAAO,uBAAwB,SAAQ,KAAK;IAErC;IACA;IACA;IAHX,YACW,GAAW,EACX,YAAoB,EACpB,YAAoB;QAE7B,KAAK,CACH,OAAO,GAAG,qCAAqC,YAAY,gDAAgD,YAAY,IAAI;YACzH,2HAA2H;YAC3H,yGAAyG;YACzG,iEAAiE,CACpE,CAAC;QATO,QAAG,GAAH,GAAG,CAAQ;QACX,iBAAY,GAAZ,YAAY,CAAQ;QACpB,iBAAY,GAAZ,YAAY,CAAQ;QAQ7B,IAAI,CAAC,IAAI,GAAG,yBAAyB,CAAC;IACxC,CAAC;CACF;AAED,0EAA0E;AAC1E,MAAM,CAAC,KAAK,UAAU,aAAa,CACjC,EAAM,EACN,QAAgB,EAChB,KAAa;IAEb,OAAO,MAAM,UAAU,CAA+B,EAAE,EAAE,YAAY,CAAC,GAAG,EAAE,CAAC,QAAQ,EAAE,KAAK,CAAC,CAAC,CAAC;AACjG,CAAC;AAED;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,aAAa,CACjC,EAAM,EACN,QAAgB,EAChB,KAAa,EACb,KAAsC;IAEtC,MAAM,IAAI,GAAiB,EAAE,CAAC,EAAE,CAAC,EAAE,GAAG,EAAE,KAAK,EAAE,GAAG,KAAK,EAAE,CAAC;IAC1D,OAAO,MAAM,iBAAiB,CAAC,EAAE,EAAE,aAAa,CAAC,YAAY,CAAC,GAAG,EAAE,CAAC,QAAQ,EAAE,KAAK,CAAC,CAAC,EAAE,IAAI,CAAC,CAAC;AAC/F,CAAC;AAED;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,cAAc,CAClC,EAAM,EACN,QAAgB,EAChB,KAAa,EACb,KAAgC,EAChC,gBAAyB;IAEzB,MAAM,MAAM,GAAG,iBAAiB,CAAC,EAAE,CAAC,EAAE,CAAU,EAAE,GAAG,KAAK,EAAE,CAAC,CAAC;IAC9D,gGAAgG;IAChG,mGAAmG;IACnG,MAAM,GAAG,GAAG,eAAe,CAAC,YAAY,CAAC,GAAG,EAAE,CAAC,QAAQ,EAAE,KAAK,CAAC,CAAC,CAAC;IACjE,OAAO,gBAAgB,KAAK,SAAS;QACnC,CAAC,CAAC,MAAM,iBAAiB,CAAC,EAAE,EAAE,GAAG,EAAE,MAAM,CAAC;QAC1C,CAAC,CAAC,MAAM,iBAAiB,CAAC,EAAE,EAAE,GAAG,EAAE,MAAM,EAAE,gBAAgB,CAAC,CAAC;AACjE,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,uBAAuB,CACrC,KAAa,EACb,QAAoC,EACpC,YAAoB;IAEpB,IAAI,QAAQ,KAAK,SAAS;QAAE,OAAO;IACnC,IAAI,YAAY,GAAG,QAAQ,CAAC,WAAW,EAAE,CAAC;QACxC,MAAM,IAAI,uBAAuB,CAAC,KAAK,EAAE,QAAQ,CAAC,WAAW,EAAE,YAAY,CAAC,CAAC;IAC/E,CAAC;AACH,CAAC"}
|
package/package.json
CHANGED