@miadi/connector 0.1.19 → 0.1.20
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/README.md +3 -1
- package/dist/approve.d.ts +13 -2
- package/dist/approve.js +176 -10
- package/dist/mission-status.js +3 -1
- package/dist/model-preflight.d.ts +9 -2
- package/dist/model-preflight.js +23 -1
- package/dist/provider-policy.d.ts +94 -0
- package/dist/provider-policy.js +150 -0
- package/dist/providers.d.ts +7 -3
- package/dist/providers.js +13 -7
- package/dist/queue-state.js +12 -1
- package/dist/reading/adapters.d.ts +18 -0
- package/dist/reading/adapters.js +90 -0
- package/dist/reading/contract.d.ts +153 -0
- package/dist/reading/contract.js +79 -0
- package/dist/reading/markdown.d.ts +47 -0
- package/dist/reading/markdown.js +52 -0
- package/dist/reading/privacy.d.ts +7 -0
- package/dist/reading/privacy.js +14 -0
- package/dist/reading/workflow.d.ts +93 -0
- package/dist/reading/workflow.js +325 -0
- package/dist/records.d.ts +1 -1
- package/dist/records.js +4 -0
- package/dist/request-state.js +3 -0
- package/dist/server.js +2 -1
- package/dist/store.d.ts +2 -0
- package/dist/store.js +18 -1
- package/dist/tools.js +162 -10
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { block, hash } from "./contract.js";
|
|
2
|
+
/** UTF-16 string offsets outside code fences, retaining CRLF. No Markdown reserialization. */
|
|
3
|
+
export function headings(markdown) {
|
|
4
|
+
let offset = 0, fence = null, fenceLength = 0;
|
|
5
|
+
const out = [];
|
|
6
|
+
for (const line of markdown.match(/[^\n]*\n|[^\n]+$/g) ?? []) {
|
|
7
|
+
const f = line.match(/^ {0,3}(`{3,}|~{3,})/);
|
|
8
|
+
if (f) {
|
|
9
|
+
if (!fence) {
|
|
10
|
+
fence = f[1][0];
|
|
11
|
+
fenceLength = f[1].length;
|
|
12
|
+
}
|
|
13
|
+
else if (fence === f[1][0] && f[1].length >= fenceLength)
|
|
14
|
+
fence = null;
|
|
15
|
+
}
|
|
16
|
+
else if (!fence) {
|
|
17
|
+
const h = line.match(/^## ([^\r\n]+?)\s*#*\s*$/);
|
|
18
|
+
if (h)
|
|
19
|
+
out.push({ name: h[1].trim(), at: offset });
|
|
20
|
+
}
|
|
21
|
+
offset += line.length;
|
|
22
|
+
}
|
|
23
|
+
if (fence)
|
|
24
|
+
block("blocked_citations", "The base contains an unclosed Markdown fence. Ask the author to approve its repair before enrichment.");
|
|
25
|
+
return out;
|
|
26
|
+
}
|
|
27
|
+
export function insert(base, at, addition) {
|
|
28
|
+
const markdown = base.slice(0, at) + addition + base.slice(at);
|
|
29
|
+
return { markdown, diff: { operation: "insert", at, addition, base_sha256: hash(base), sha256: hash(markdown), bytes: Buffer.byteLength(markdown), retained: true } };
|
|
30
|
+
}
|
|
31
|
+
export function academic(base, s) {
|
|
32
|
+
const hs = headings(base), summary = hs.findIndex(h => h.name.toLowerCase() === "summary");
|
|
33
|
+
if (summary < 0 || hs.filter(h => h.name.toLowerCase() === "summary").length !== 1 || hs.some(h => /^(academic fields|academic field survey)$/i.test(h.name)))
|
|
34
|
+
block("blocked_citations", "A unique Summary and no existing Academic Fields are required; resolve the base with the author.");
|
|
35
|
+
const at = hs[summary + 1]?.at ?? base.length;
|
|
36
|
+
const fields = "\n\n## Academic Fields\n\n" + s.fields.map(f => `### ${f.name}\n\n${f.body}\n\nSource anchor: ${f.at_ms} ms (transcript). External literature: ${f.source_ids.map(id => `[${id}]`).join(", ")}.\n\nOpen question: ${f.question}\n\nLimit: ${f.limitation}\n`).join("\n") + "\n";
|
|
37
|
+
const first = insert(base, at, fields);
|
|
38
|
+
const sources = "\n\n## Reading Sources\n\n" + s.sources.map(x => `- [${x.id}] ${x.title}. ${x.url}\n Retrieved excerpt checked; relevance to the inquiry is an agent interpretation. This receipt does not claim a full reading.\n`).join("\n");
|
|
39
|
+
const second = insert(first.markdown, first.markdown.length, sources);
|
|
40
|
+
return { markdown: second.markdown, diffs: [first.diff, second.diff] };
|
|
41
|
+
}
|
|
42
|
+
export function reflective(base, r, companion) {
|
|
43
|
+
let added = `\n\n## Reflective Methodology — AI-generated Ava–Mia Dialogue\n\nGenerated by ${r.model}; fictional agent voices reflecting on this survey. These words were not spoken in the recording or supplied by the author.\n\n`;
|
|
44
|
+
added += r.dialogue.map(d => `**${d.voice} (AI-generated):** ${d.text}\n`).join("\n");
|
|
45
|
+
added += `\n### Cross-field synthesis (interpretation)\n\n${r.synthesis}\n\n### Proposed methods and possible evidence\n\n`;
|
|
46
|
+
added += r.methods.map(m => `- Question: ${m.question}\n Method (proposed): ${m.method}\n Possible evidence: ${m.evidence}\n Contradiction to test: ${m.contradiction}\n`).join("\n");
|
|
47
|
+
added += `\nHypothesis (untested): ${r.hypothesis}\n\n`;
|
|
48
|
+
if (companion)
|
|
49
|
+
added += `## Author-supplied Companion Dialogue\n\nSeparate contribution; supplied by ${companion.author}, originating tool ${companion.tool}, ${companion.supplied_at}. Alignment with video unknown. Exact supplied text follows.\n\n${companion.text}\n\n`;
|
|
50
|
+
const at = headings(base).find(h => h.name === "Reading Sources")?.at ?? base.length;
|
|
51
|
+
return insert(base, at, added);
|
|
52
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { ConnectorRecord } from "../records.js";
|
|
2
|
+
/** Processing approval never grants other readers access to drafts or completion text. */
|
|
3
|
+
export declare function readingOwner(record: ConnectorRecord, records: ConnectorRecord[]): string | undefined;
|
|
4
|
+
export declare function canReadReading(record: ConnectorRecord, records: ConnectorRecord[], viewer: {
|
|
5
|
+
id: string;
|
|
6
|
+
api_tier?: string | null;
|
|
7
|
+
}): boolean;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/** Processing approval never grants other readers access to drafts or completion text. */
|
|
2
|
+
export function readingOwner(record, records) {
|
|
3
|
+
if (record.body.reading || record.body.reading_import)
|
|
4
|
+
return record.actor.person_id;
|
|
5
|
+
if (record.kind === "session_completion_proposal") {
|
|
6
|
+
const parent = records.find(r => r.kind === "run_request" && (r.body.reading || r.body.reading_import) && (r.id === record.body.mission || record.refs.includes(r.id)));
|
|
7
|
+
return parent?.actor.person_id;
|
|
8
|
+
}
|
|
9
|
+
return undefined;
|
|
10
|
+
}
|
|
11
|
+
export function canReadReading(record, records, viewer) {
|
|
12
|
+
const owner = readingOwner(record, records);
|
|
13
|
+
return !owner || viewer.api_tier === "write" || owner === viewer.id;
|
|
14
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { type Actor, type ConnectorRecord } from "../records.js";
|
|
2
|
+
import { Ledger } from "../store.js";
|
|
3
|
+
import { type Provider, type ReadingScope, type SourcePart, type BaseReview, type Survey, type Reflection } from "./contract.js";
|
|
4
|
+
export interface ReviewBase {
|
|
5
|
+
id: string;
|
|
6
|
+
version: number;
|
|
7
|
+
markdown: string;
|
|
8
|
+
video_id: string;
|
|
9
|
+
}
|
|
10
|
+
export interface StageContext {
|
|
11
|
+
workflow_id: string;
|
|
12
|
+
video_id: string;
|
|
13
|
+
scope: ReadingScope;
|
|
14
|
+
idempotency_key: string;
|
|
15
|
+
transcript: SourcePart;
|
|
16
|
+
source_part_id: string;
|
|
17
|
+
base?: BaseReview;
|
|
18
|
+
survey?: Survey;
|
|
19
|
+
}
|
|
20
|
+
export interface ReadingAdapters {
|
|
21
|
+
/** Provider execution belongs to the runtime/shim. Every operation must replay a key without charging twice. */
|
|
22
|
+
providers: Record<"transcript" | "v1" | "academic" | "reflective", Provider & {
|
|
23
|
+
idempotent: true;
|
|
24
|
+
}>;
|
|
25
|
+
lookup(video: string): Promise<ReviewBase | null>;
|
|
26
|
+
acquire(context: Omit<StageContext, "transcript" | "source_part_id">): Promise<SourcePart | null>;
|
|
27
|
+
generateV1(context: StageContext): Promise<BaseReview>;
|
|
28
|
+
generateSurvey(context: StageContext): Promise<Survey>;
|
|
29
|
+
generateReflection(context: StageContext): Promise<Reflection>;
|
|
30
|
+
/** Read primary sources through a host-controlled allowlist; no source-directed credential or media forwarding. */
|
|
31
|
+
verifySource(source: Survey["sources"][number]): Promise<{
|
|
32
|
+
url: string;
|
|
33
|
+
text: string;
|
|
34
|
+
fetched_at: string;
|
|
35
|
+
}>;
|
|
36
|
+
/** True only after a real capability handshake for atomic compare-and-swap + durable idempotency. */
|
|
37
|
+
atomicImport: boolean;
|
|
38
|
+
importExact(input: {
|
|
39
|
+
video_id: string;
|
|
40
|
+
review_id: string | null;
|
|
41
|
+
expected_version: number;
|
|
42
|
+
expected_sha256: string;
|
|
43
|
+
markdown: string;
|
|
44
|
+
sha256: string;
|
|
45
|
+
idempotency_key: string;
|
|
46
|
+
}): Promise<{
|
|
47
|
+
id: string;
|
|
48
|
+
version: number;
|
|
49
|
+
}>;
|
|
50
|
+
readExact(id: string, version: number): Promise<ReviewBase>;
|
|
51
|
+
}
|
|
52
|
+
type Receipt = {
|
|
53
|
+
stage: string;
|
|
54
|
+
status: string;
|
|
55
|
+
attempt: number;
|
|
56
|
+
started_at: string;
|
|
57
|
+
completed_at: string;
|
|
58
|
+
workflow_id: string;
|
|
59
|
+
request_id: string;
|
|
60
|
+
video_id: string;
|
|
61
|
+
actor: Actor;
|
|
62
|
+
consent_scope: ReadingScope;
|
|
63
|
+
approval_state: string;
|
|
64
|
+
artifact_refs: string[];
|
|
65
|
+
sha256: string;
|
|
66
|
+
previous_sha256: string | null;
|
|
67
|
+
artifact: any;
|
|
68
|
+
next_action: string;
|
|
69
|
+
};
|
|
70
|
+
export type ReadingResult = {
|
|
71
|
+
workflow_id: string;
|
|
72
|
+
status: string;
|
|
73
|
+
next_action: string;
|
|
74
|
+
receipts: Receipt[];
|
|
75
|
+
};
|
|
76
|
+
/** One workflow per person and video; two concurrent releases cannot spend twice. */
|
|
77
|
+
export declare function workflowId(actor: string, video: string, revisionOf?: string): string;
|
|
78
|
+
export declare class ReadingWorkflow {
|
|
79
|
+
readonly stateDir: string;
|
|
80
|
+
readonly adapters: ReadingAdapters;
|
|
81
|
+
readonly ledger: Ledger<ConnectorRecord>;
|
|
82
|
+
constructor(stateDir: string, adapters: ReadingAdapters);
|
|
83
|
+
receipts(id: string): Receipt[];
|
|
84
|
+
private approved;
|
|
85
|
+
private source;
|
|
86
|
+
private locked;
|
|
87
|
+
private record;
|
|
88
|
+
private done;
|
|
89
|
+
private provider;
|
|
90
|
+
run(requestId: string, companion?: unknown): Promise<ReadingResult>;
|
|
91
|
+
importApproved(requestId: string): Promise<ReadingResult>;
|
|
92
|
+
}
|
|
93
|
+
export {};
|
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
import { closeSync, mkdirSync, openSync, readFileSync, renameSync, rmSync, writeFileSync, fsyncSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { randomUUID } from "node:crypto";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
import { current } from "../records.js";
|
|
6
|
+
import { Ledger } from "../store.js";
|
|
7
|
+
import { contentHash } from "../revisions.js";
|
|
8
|
+
import { academic, reflective, insert } from "./markdown.js";
|
|
9
|
+
import { baseReview, block, canonicalVideo, hash, ReadingBlocked, readingScope, sourcePart, suppliedDialogue, survey, reflection } from "./contract.js";
|
|
10
|
+
/** One workflow per person and video; two concurrent releases cannot spend twice. */
|
|
11
|
+
export function workflowId(actor, video, revisionOf) { return `reading:${hash(`${actor}:${video}${revisionOf ? `:${revisionOf}` : ""}`).slice(0, 32)}`; }
|
|
12
|
+
export class ReadingWorkflow {
|
|
13
|
+
stateDir;
|
|
14
|
+
adapters;
|
|
15
|
+
ledger;
|
|
16
|
+
constructor(stateDir, adapters) {
|
|
17
|
+
this.stateDir = stateDir;
|
|
18
|
+
this.adapters = adapters;
|
|
19
|
+
mkdirSync(stateDir, { recursive: true, mode: 0o700 });
|
|
20
|
+
this.ledger = new Ledger(join(stateDir, "ledger.jsonl"));
|
|
21
|
+
}
|
|
22
|
+
receipts(id) {
|
|
23
|
+
return this.ledger.all().filter(r => r.kind === "observation" && r.body.reading && r.body.reading.workflow_id === id).map(r => r.body.reading);
|
|
24
|
+
}
|
|
25
|
+
approved(requestId) {
|
|
26
|
+
const r = current(this.ledger.all()).find(x => x.id === requestId);
|
|
27
|
+
if (!r || r.kind !== "run_request" || r.approval_state !== "approved")
|
|
28
|
+
block("blocked_permission", "Approve the bounded reading request through the connector decision door.");
|
|
29
|
+
if (r.completion_receipt?.instructions)
|
|
30
|
+
block("blocked_permission", "Approval carries changes: reconcile them in a revised typed scope before execution.");
|
|
31
|
+
return r;
|
|
32
|
+
}
|
|
33
|
+
source(request) {
|
|
34
|
+
const scope = readingScope.parse(request.body.reading);
|
|
35
|
+
if (scope.max_cost_usd > 0 && !scope.budget_approval_ref)
|
|
36
|
+
block("blocked_permission", "A nonzero provider budget needs a separate budget approval reference.");
|
|
37
|
+
if (scope.max_cost_usd > 0) {
|
|
38
|
+
const b = this.approved(scope.budget_approval_ref);
|
|
39
|
+
const parsed = z.object({ source_intent: z.string(), max_cost_usd: z.number().finite().min(0), providers: z.array(z.string()).min(1) }).strict().safeParse(b.body.reading_budget);
|
|
40
|
+
if (!parsed.success || parsed.data.source_intent !== scope.source_intent || parsed.data.max_cost_usd < scope.max_cost_usd || scope.providers.some(p => !parsed.data.providers.includes(p)) || b.actor.person_id !== request.actor.person_id)
|
|
41
|
+
block("blocked_permission", "The separate approved budget must name this source, person, providers and finite ceiling.");
|
|
42
|
+
}
|
|
43
|
+
const source = current(this.ledger.all()).find(r => r.id === scope.source_intent);
|
|
44
|
+
if (!source || source.kind !== "source_intent" || source.actor.person_id !== request.actor.person_id || source.state !== "ready" || source.body.readiness !== "released_for_review")
|
|
45
|
+
block("withheld", "The source owner must explicitly release this source; registration or uploading does not start work.");
|
|
46
|
+
const video = canonicalVideo(String(source.body.video_id));
|
|
47
|
+
if (scope.revision_of) {
|
|
48
|
+
const previous = this.done(scope.revision_of, "registration");
|
|
49
|
+
const priorRequest = current(this.ledger.all()).find(r => r.kind === "run_request" && r.body.workflow_id === scope.revision_of && r.actor.person_id === request.actor.person_id && r.approval_state === "approved" && r.body.reading?.source_intent === scope.source_intent);
|
|
50
|
+
if ((!previous && !priorRequest) || (previous && (previous.actor.person_id !== request.actor.person_id || previous.video_id !== video)) || this.done(scope.revision_of, "import"))
|
|
51
|
+
block("blocked_permission", "A revision must name this author's unimported prior workflow for the same video.");
|
|
52
|
+
}
|
|
53
|
+
return { scope, source, video, id: workflowId(request.actor.person_id, video, scope.revision_of) };
|
|
54
|
+
}
|
|
55
|
+
async locked(id, fn) {
|
|
56
|
+
const root = join(this.stateDir, "reading-locks");
|
|
57
|
+
mkdirSync(root, { recursive: true, mode: 0o700 });
|
|
58
|
+
const file = join(root, hash(id));
|
|
59
|
+
// Serialize every acquisition/recovery, including the dead-owner check and replacement.
|
|
60
|
+
// A crash in this tiny critical section fails closed; it is never stolen on a timer.
|
|
61
|
+
const acquiring = `${file}.acquiring`;
|
|
62
|
+
try {
|
|
63
|
+
mkdirSync(acquiring, { mode: 0o700 });
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
return block("failed_retryable", `Acquisition is in progress. If its worker crashed, inspect and recover ${acquiring} before retrying.`);
|
|
67
|
+
}
|
|
68
|
+
try {
|
|
69
|
+
try {
|
|
70
|
+
mkdirSync(file, { mode: 0o700 });
|
|
71
|
+
}
|
|
72
|
+
catch (e) {
|
|
73
|
+
if (e.code !== "EEXIST")
|
|
74
|
+
throw e;
|
|
75
|
+
// No time-based stealing: a slow provider must never become a second writer.
|
|
76
|
+
let pid;
|
|
77
|
+
try {
|
|
78
|
+
pid = Number(readFileSync(join(file, "pid"), "utf8"));
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
return block("failed_retryable", "Workflow lock is initializing; retry, or inspect its owner after a crash.");
|
|
82
|
+
}
|
|
83
|
+
if (!Number.isSafeInteger(pid) || pid <= 0)
|
|
84
|
+
block("failed_retryable", "Inspect the invalid workflow lock owner.");
|
|
85
|
+
try {
|
|
86
|
+
process.kill(pid, 0);
|
|
87
|
+
return block("failed_retryable", "Another worker holds this video; read its receipts.");
|
|
88
|
+
}
|
|
89
|
+
catch (e) {
|
|
90
|
+
if (e.code !== "ESRCH")
|
|
91
|
+
throw e;
|
|
92
|
+
}
|
|
93
|
+
// The acquisition mutex prevents two recoverers from moving a new active lock.
|
|
94
|
+
const stale = `${file}.dead-${randomUUID()}`;
|
|
95
|
+
try {
|
|
96
|
+
renameSync(file, stale);
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
return block("failed_retryable", "Another worker recovered this lock; retry.");
|
|
100
|
+
}
|
|
101
|
+
rmSync(stale, { recursive: true });
|
|
102
|
+
mkdirSync(file, { mode: 0o700 });
|
|
103
|
+
}
|
|
104
|
+
writeFileSync(join(file, "pid"), String(process.pid), { mode: 0o600 });
|
|
105
|
+
}
|
|
106
|
+
finally {
|
|
107
|
+
rmSync(acquiring, { recursive: true });
|
|
108
|
+
}
|
|
109
|
+
try {
|
|
110
|
+
return await fn();
|
|
111
|
+
}
|
|
112
|
+
finally {
|
|
113
|
+
rmSync(file, { recursive: true });
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
record(request, scope, id, video, stage, status, artifact, next_action = "") {
|
|
117
|
+
const previous = this.receipts(id), at = new Date().toISOString();
|
|
118
|
+
const starts = previous.filter(r => r.stage === stage && r.status === "started");
|
|
119
|
+
const attempt = status === "started" ? starts.length + 1 : starts.length || 1;
|
|
120
|
+
const started = status === "started" ? at : starts.at(-1)?.started_at ?? at;
|
|
121
|
+
const receipt = { stage, status, attempt, started_at: started, completed_at: at, workflow_id: id, request_id: request.id, video_id: video, actor: request.actor, consent_scope: scope, approval_state: "approved_bounded_processing", artifact_refs: [], sha256: hash(JSON.stringify(artifact)), previous_sha256: previous.at(-1)?.sha256 ?? null, artifact, next_action };
|
|
122
|
+
const record = { id: `observation:${randomUUID()}`, kind: "observation", actor: request.actor, created_at: at, circle: "continuity", claim_category: "observed", refs: [request.id, `https://youtu.be/${video}`], evidence_refs: [], body: { reading: receipt } };
|
|
123
|
+
receipt.artifact_refs = [record.id];
|
|
124
|
+
this.ledger.appendDurable(record);
|
|
125
|
+
// Persist a new ledger directory entry too, before a downstream call.
|
|
126
|
+
const fd = openSync(this.stateDir, "r");
|
|
127
|
+
try {
|
|
128
|
+
fsyncSync(fd);
|
|
129
|
+
}
|
|
130
|
+
finally {
|
|
131
|
+
closeSync(fd);
|
|
132
|
+
}
|
|
133
|
+
return receipt;
|
|
134
|
+
}
|
|
135
|
+
done(id, stage) { return this.receipts(id).find(r => r.stage === stage && r.status === "completed"); }
|
|
136
|
+
provider(scope, id, stage) {
|
|
137
|
+
const p = this.adapters.providers[stage];
|
|
138
|
+
if (!p || !p.available)
|
|
139
|
+
block(stage === "transcript" ? "blocked_transcript" : "blocked_permission", `Configure and verify an available ${stage} provider; no fallback was called.`);
|
|
140
|
+
if (!p.idempotent || !scope.providers.includes(p.id) || (p.external && !scope.allow_external_processing) || (p.media && !scope.allow_media_access))
|
|
141
|
+
block("blocked_permission", `Authorize provider ${p.id}, its media/external access and durable retry contract first.`);
|
|
142
|
+
if (!Number.isFinite(p.max_cost_usd) || p.max_cost_usd < 0)
|
|
143
|
+
block("blocked_permission", "Provider must declare a finite maximum cost.");
|
|
144
|
+
const reservations = this.receipts(id).filter(r => r.status === "reserved");
|
|
145
|
+
const spent = reservations.reduce((s, r) => s + r.artifact.max_cost_usd, 0);
|
|
146
|
+
if (!reservations.some(r => r.stage === stage) && spent + p.max_cost_usd > scope.max_cost_usd)
|
|
147
|
+
block("blocked_permission", `The ${stage} provider exceeds the approved total budget; request a separate budget.`);
|
|
148
|
+
const old = reservations.find(r => r.stage === stage);
|
|
149
|
+
if (old && (old.artifact.id !== p.id || old.artifact.method !== p.method || old.artifact.max_cost_usd !== p.max_cost_usd))
|
|
150
|
+
block("blocked_permission", "Provider settings changed after reservation; reconcile the prior operation before retry.");
|
|
151
|
+
return p;
|
|
152
|
+
}
|
|
153
|
+
async run(requestId, companion) {
|
|
154
|
+
const request = this.approved(requestId), { scope, video, id } = this.source(request);
|
|
155
|
+
const approvedCompanion = request.body.reading_companion;
|
|
156
|
+
if (companion !== undefined && hash(JSON.stringify(companion)) !== hash(JSON.stringify(approvedCompanion ?? null)))
|
|
157
|
+
block("blocked_permission", "The companion text must be part of the author's approved reading request.");
|
|
158
|
+
companion = approvedCompanion;
|
|
159
|
+
return this.locked(workflowId(request.actor.person_id, video), async () => {
|
|
160
|
+
let stage = "registration";
|
|
161
|
+
const result = (status, next_action) => ({ workflow_id: id, status, next_action, receipts: this.receipts(id) });
|
|
162
|
+
try {
|
|
163
|
+
const registration = this.done(id, "registration");
|
|
164
|
+
if (registration && registration.artifact.request_id !== requestId)
|
|
165
|
+
return result("duplicate_video", "This video already has a bounded workflow. Resume its original request.");
|
|
166
|
+
if (registration && registration.artifact.request_hash !== contentHash(request))
|
|
167
|
+
return result(registration.artifact.request_id === requestId ? "blocked_permission" : "duplicate_video", "This video already has a bounded workflow. Resume its original request; a changed grant cannot overwrite its receipts.");
|
|
168
|
+
if (!registration)
|
|
169
|
+
this.record(request, scope, id, video, stage, "completed", { request_hash: contentHash(request), request_id: requestId, video_id: video, kind: scope.kind });
|
|
170
|
+
let context = { workflow_id: id, video_id: video, scope, idempotency_key: "", transcript: undefined, source_part_id: "" };
|
|
171
|
+
const execute = async (name, fn, validate) => {
|
|
172
|
+
stage = name;
|
|
173
|
+
const done = this.done(id, name);
|
|
174
|
+
if (done)
|
|
175
|
+
return done.artifact;
|
|
176
|
+
// Source release is rechecked at every new stage.
|
|
177
|
+
this.source(this.approved(requestId));
|
|
178
|
+
const p = this.provider(scope, id, name);
|
|
179
|
+
if (!this.receipts(id).some(r => r.stage === name && r.status === "reserved"))
|
|
180
|
+
this.record(request, scope, id, video, name, "reserved", p);
|
|
181
|
+
this.record(request, scope, id, video, name, "started", { provider: p.id, method: p.method });
|
|
182
|
+
const output = validate(await fn({ ...context, idempotency_key: `${id}:${name}` }));
|
|
183
|
+
return this.record(request, scope, id, video, name, "completed", output).artifact;
|
|
184
|
+
};
|
|
185
|
+
const oldTranscript = scope.revision_of ? this.done(scope.revision_of, "transcript") : undefined;
|
|
186
|
+
if (!this.done(id, "transcript") && oldTranscript && oldTranscript.artifact.language === scope.language && oldTranscript.artifact.rights === scope.rights && scope.providers.includes(oldTranscript.artifact.provider)) {
|
|
187
|
+
this.record(request, scope, id, video, "transcript", "completed", { ...oldTranscript.artifact, reused_from: oldTranscript.artifact_refs });
|
|
188
|
+
}
|
|
189
|
+
const transcript = await execute("transcript", c => this.adapters.acquire(c), x => {
|
|
190
|
+
if (x === null)
|
|
191
|
+
block("blocked_transcript", "No authorized captions or working local transcript. Supply exact transcript/caption bytes with provenance and retry; no paid fallback ran.");
|
|
192
|
+
const part = sourcePart.parse(x);
|
|
193
|
+
if (!part.text.trim() || part.video_id !== video || part.channel !== "video" || part.language !== scope.language || part.provider !== this.adapters.providers.transcript.id || part.rights !== scope.rights || /\b(summary|key points)\b/i.test(part.kind))
|
|
194
|
+
block("blocked_transcript", "Video transcript provenance, language, rights or channel does not match this grant.");
|
|
195
|
+
if (part.segments.some((s, i) => s.end_ms < s.start_ms || (i > 0 && s.start_ms < part.segments[i - 1].start_ms)))
|
|
196
|
+
block("blocked_transcript", "Transcript has invalid or unordered timecodes.");
|
|
197
|
+
if (part.timestamp_granularity !== "none" && !part.segments.length)
|
|
198
|
+
block("blocked_transcript", "Claimed timestamp granularity needs actual segments.");
|
|
199
|
+
return { ...part, sha256: hash(part.text), source_part_id: `source_part:${hash(`${video}:${part.provider}:${scope.language}:${hash(part.text)}`)}`, stored_at: new Date().toISOString(), normalized_sha256: part.normalized_text ? hash(part.normalized_text) : null };
|
|
200
|
+
});
|
|
201
|
+
context = { ...context, transcript, source_part_id: transcript.source_part_id };
|
|
202
|
+
stage = "lookup";
|
|
203
|
+
if (!this.done(id, "lookup")) {
|
|
204
|
+
const base = await this.adapters.lookup(video);
|
|
205
|
+
if (base && (base.video_id !== video || !Number.isSafeInteger(base.version) || base.version < 1 || !base.markdown))
|
|
206
|
+
block("blocked_permission", "Review lookup returned an invalid or ambiguous base; resolve it before generating.");
|
|
207
|
+
this.record(request, scope, id, video, "lookup", "completed", { base });
|
|
208
|
+
}
|
|
209
|
+
const base = this.done(id, "lookup").artifact.base;
|
|
210
|
+
const validateBase = (x) => {
|
|
211
|
+
const b = baseReview.parse(x);
|
|
212
|
+
for (const c of b.claims) {
|
|
213
|
+
if (c.category === "transcript_supported" && (c.at_ms === undefined || !transcript.segments.some((s) => c.at_ms >= s.start_ms && c.at_ms <= s.end_ms)))
|
|
214
|
+
block("blocked_transcript", "A source claim lacks an archived transcript timecode.");
|
|
215
|
+
if (c.category === "visually_evidenced" && !c.visual_receipt)
|
|
216
|
+
block("blocked_transcript", "A visual claim requires a separately inspected visual evidence receipt.");
|
|
217
|
+
}
|
|
218
|
+
return b;
|
|
219
|
+
};
|
|
220
|
+
const priorV1 = scope.revision_of && !base && transcript.reused_from ? this.done(scope.revision_of, "v1") : undefined;
|
|
221
|
+
if (priorV1 && !this.done(id, "v1"))
|
|
222
|
+
this.record(request, scope, id, video, "v1", "completed", { ...priorV1.artifact, reused_from: priorV1.artifact_refs });
|
|
223
|
+
const v1 = base ? { markdown: base.markdown, claims: [{ category: "inference", claim: "Existing review reused verbatim; its claims have not been independently verified." }], model: "existing_review" } : await execute("v1", c => this.adapters.generateV1(c), validateBase);
|
|
224
|
+
if (base && !this.done(id, "v1"))
|
|
225
|
+
this.record(request, scope, id, video, "v1", "completed", { ...v1, reused: true, review_id: base.id, version: base.version });
|
|
226
|
+
context.base = v1;
|
|
227
|
+
const academicResult = await execute("academic", c => this.adapters.generateSurvey(c), asyncIdentity);
|
|
228
|
+
// Verification is its own resumable stage, before any V2 is accepted.
|
|
229
|
+
stage = "citations";
|
|
230
|
+
this.source(this.approved(requestId));
|
|
231
|
+
let checked = this.done(id, "citations")?.artifact;
|
|
232
|
+
if (!checked) {
|
|
233
|
+
const parsed = survey.parse(academicResult);
|
|
234
|
+
if (parsed.fields.length > scope.max_fields || new Set(parsed.sources.map(s => s.id)).size !== parsed.sources.length)
|
|
235
|
+
block("blocked_citations", "Field limit exceeded or duplicate source identifiers.");
|
|
236
|
+
const verified = [];
|
|
237
|
+
const normalize = (s) => s.replace(/\s+/g, " ").trim().toLowerCase();
|
|
238
|
+
for (const source of parsed.sources) {
|
|
239
|
+
const proof = await this.adapters.verifySource(source);
|
|
240
|
+
if (proof.url !== source.url || !Number.isFinite(Date.parse(proof.fetched_at)) || !normalize(proof.text) || !normalize(source.excerpt) || !normalize(proof.text).includes(normalize(source.excerpt)))
|
|
241
|
+
block("blocked_citations", `Source ${source.id} could not be verified against retrieved text. Correct or remove it, then request a new bounded candidate.`);
|
|
242
|
+
verified.push({ id: source.id, url: source.url, fetched_at: proof.fetched_at, sha256: hash(proof.text), excerpt_sha256: hash(source.excerpt), claim: source.claim, status: "retrieved_excerpt", full_reading: false });
|
|
243
|
+
}
|
|
244
|
+
for (const field of parsed.fields) {
|
|
245
|
+
if (field.body.trim().split(/\s+/).length > scope.max_field_words || !transcript.segments.some((s) => field.at_ms >= s.start_ms && field.at_ms <= s.end_ms) || field.source_ids.some(id => !verified.some(v => v.id === id)))
|
|
246
|
+
block("blocked_citations", "Field length, timestamp or citation does not meet the authorized survey contract.");
|
|
247
|
+
}
|
|
248
|
+
checked = this.record(request, scope, id, video, stage, "completed", { survey: parsed, verified }).artifact;
|
|
249
|
+
}
|
|
250
|
+
context.survey = checked.survey;
|
|
251
|
+
stage = "v2";
|
|
252
|
+
this.source(this.approved(requestId));
|
|
253
|
+
const v2 = this.done(id, "v2")?.artifact ?? this.record(request, scope, id, video, stage, "completed", academic(v1.markdown, checked.survey)).artifact;
|
|
254
|
+
const reflectionResult = await execute("reflective", c => this.adapters.generateReflection(c), x => {
|
|
255
|
+
const r = reflection.parse(x);
|
|
256
|
+
if (new Set(r.dialogue.map(d => d.voice)).size !== 2)
|
|
257
|
+
block("blocked_citations", "Reflective stage must contain both labelled generated voices.");
|
|
258
|
+
return r;
|
|
259
|
+
});
|
|
260
|
+
stage = "v3";
|
|
261
|
+
this.source(this.approved(requestId));
|
|
262
|
+
const supplied = companion === undefined ? undefined : suppliedDialogue.parse(companion);
|
|
263
|
+
const prior = this.done(id, "v3");
|
|
264
|
+
if (prior && hash(JSON.stringify(supplied ?? null)) !== prior.artifact.companion_sha256)
|
|
265
|
+
block("blocked_permission", "A later companion is a new attributed contribution; the immutable candidate cannot be overwritten.");
|
|
266
|
+
if (!prior) {
|
|
267
|
+
const v3 = reflective(v2.markdown, reflectionResult, supplied);
|
|
268
|
+
const manifest = { workflow_id: id, video_id: video, source_part_id: transcript.source_part_id, transcript_sha256: transcript.sha256, transcript_receipt: this.done(id, "transcript").artifact_refs, language: transcript.language, rights: transcript.rights, locator: transcript.locator, provider: transcript.provider, fetched_at: transcript.fetched_at, timestamp_granularity: transcript.timestamp_granularity, alignment_status: transcript.alignment_status, speaker_status: transcript.speaker_status, claim_categories: v1.claims, sources: checked.verified, base, companion: supplied ?? null, gates: ["editorial_import", "publication", "deep_diver", "chronicle", "cultural_reuse"] };
|
|
269
|
+
const manifestText = `\n\n## Processing Provenance\n\nTranscript archived before generation. Source transcript supports spoken content; visual evidence is separate. Unknown speakers and channel synchronization remain uncertain.\n\n\`\`\`json\n${JSON.stringify(manifest, null, 2)}\n\`\`\`\n`;
|
|
270
|
+
const final = insert(v3.markdown, v3.markdown.length, manifestText);
|
|
271
|
+
this.record(request, scope, id, video, stage, "completed", { markdown: final.markdown, sha256: hash(final.markdown), bytes: Buffer.byteLength(final.markdown), diffs: [...v2.diffs, v3.diff, final.diff], manifest, companion_sha256: hash(JSON.stringify(supplied ?? null)), expected_version: base?.version ?? 0, expected_sha256: hash(base?.markdown ?? ""), review_id: base?.id ?? null });
|
|
272
|
+
}
|
|
273
|
+
const imported = this.done(id, "import");
|
|
274
|
+
return imported ? result("imported", "Public review import was read back exactly. Deep Diver, Chronicle and further publication/reuse remain separate gates.") : result("awaiting_editorial_decision", "Inspect V1/V2/V3, the insertion diff and manifest. Approve the exact candidate hash and base in a separate editorial request; import is a public release.");
|
|
275
|
+
}
|
|
276
|
+
catch (e) {
|
|
277
|
+
const blocked = e instanceof ReadingBlocked ? e : new ReadingBlocked(stage === "transcript" ? "blocked_transcript" : stage === "academic" || stage === "citations" ? "blocked_citations" : "failed_retryable", "Stage failed or returned malformed data. Inspect the adapter; retry the same request and idempotency key after repair.");
|
|
278
|
+
this.record(request, scope, id, video, stage, blocked.status, { error_type: e instanceof Error ? e.name : "unknown" }, blocked.next_action);
|
|
279
|
+
return result(blocked.status, blocked.next_action);
|
|
280
|
+
}
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
async importApproved(requestId) {
|
|
284
|
+
const request = this.approved(requestId), approval = request.body.reading_import;
|
|
285
|
+
if (!approval || typeof approval.workflow_id !== "string" || !/^reading:[a-f\d]{32}$/.test(approval.workflow_id))
|
|
286
|
+
block("blocked_permission", "An editorial request must name one staged workflow and exact candidate/base hashes.");
|
|
287
|
+
const id = approval.workflow_id;
|
|
288
|
+
const stagedOwner = this.done(id, "registration");
|
|
289
|
+
if (!stagedOwner)
|
|
290
|
+
block("blocked_permission", "Workflow is unavailable.");
|
|
291
|
+
return this.locked(workflowId(stagedOwner.actor.person_id, stagedOwner.video_id), async () => {
|
|
292
|
+
const staged = this.done(id, "v3"), registration = this.done(id, "registration");
|
|
293
|
+
if (!staged || !registration || staged.actor.person_id !== request.actor.person_id)
|
|
294
|
+
block("blocked_permission", "Only the source owner can request this candidate's editorial import.");
|
|
295
|
+
const candidate = staged.artifact, scope = staged.consent_scope, video = staged.video_id;
|
|
296
|
+
// Recheck the original bounded grant and current source release before sharing anything.
|
|
297
|
+
this.source(this.approved(registration.artifact.request_id));
|
|
298
|
+
if (approval.sha256 !== candidate.sha256 || approval.expected_version !== candidate.expected_version || approval.expected_sha256 !== candidate.expected_sha256 || approval.public_release !== true)
|
|
299
|
+
block("blocked_permission", "Approve the exact candidate and base, explicitly including public release.");
|
|
300
|
+
if (this.done(id, "import"))
|
|
301
|
+
return { workflow_id: id, status: "imported", next_action: "Already imported and verified; nothing was written again.", receipts: this.receipts(id) };
|
|
302
|
+
if (!this.adapters.atomicImport)
|
|
303
|
+
block("blocked_permission", "Review service lacks verified atomic conditional import and idempotency. Apply the v0 owner prompt; never fall back to the legacy import API.");
|
|
304
|
+
const intent = this.done(id, "import_intent");
|
|
305
|
+
// Persist the approved exact write ahead of the remote call; a lost response replays the same key.
|
|
306
|
+
if (!intent)
|
|
307
|
+
this.record(request, scope, id, video, "import_intent", "completed", { sha256: candidate.sha256, approval: request.id });
|
|
308
|
+
this.record(request, scope, id, video, "import", "started", { candidate_sha256: candidate.sha256 });
|
|
309
|
+
try {
|
|
310
|
+
const receipt = await this.adapters.importExact({ video_id: video, review_id: candidate.review_id, expected_version: candidate.expected_version, expected_sha256: candidate.expected_sha256, markdown: candidate.markdown, sha256: candidate.sha256, idempotency_key: `${id}:import:${candidate.sha256}` });
|
|
311
|
+
const back = await this.adapters.readExact(receipt.id, receipt.version);
|
|
312
|
+
if (back.id !== receipt.id || (candidate.review_id && back.id !== candidate.review_id) || back.video_id !== video || back.version !== candidate.expected_version + 1 || hash(back.markdown) !== candidate.sha256)
|
|
313
|
+
block("failed_retryable", "Import read-back mismatch; reconcile the idempotency receipt before any further write.");
|
|
314
|
+
this.record(request, scope, id, video, "import", "completed", { ...receipt, sha256: hash(back.markdown), bytes: Buffer.byteLength(back.markdown), editorial_approval: request.id, publication: "explicitly_approved", deep_diver: "not_requested", chronicle: "not_requested" });
|
|
315
|
+
}
|
|
316
|
+
catch (e) {
|
|
317
|
+
const b = e instanceof ReadingBlocked ? e : new ReadingBlocked("failed_retryable", "Remote import outcome is uncertain. Replay the same durable operation key; never send a legacy write.");
|
|
318
|
+
this.record(request, scope, id, video, "import", b.status, { candidate_sha256: candidate.sha256 }, b.next_action);
|
|
319
|
+
throw b;
|
|
320
|
+
}
|
|
321
|
+
return { workflow_id: id, status: "imported", next_action: "Exact public import verified. Deep Diver and Chronicle require separate author decisions.", receipts: this.receipts(id) };
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
const asyncIdentity = (x) => survey.parse(x);
|
package/dist/records.d.ts
CHANGED
|
@@ -36,7 +36,7 @@ export declare const DOMAINS: readonly ["creative", "ceremonial", "engineering"]
|
|
|
36
36
|
/** #815 "claim category": what kind of statement a record makes. */
|
|
37
37
|
export declare const CLAIM_CATEGORIES: readonly ["observed", "hypothesis", "desired_future", "fiction", "verified_implementation"];
|
|
38
38
|
export type ClaimCategory = (typeof CLAIM_CATEGORIES)[number];
|
|
39
|
-
export declare const RECORD_KINDS: readonly ["observation", "inquiry", "link", "work_proposal", "run_request", "handoff", "source_intent", "review_operation_request", "deep_diver_request", "review_episode_link_request", "calendar_event_draft", "session_completion_proposal", "research_artifact", "inquiry_episode_request", "session_steer_request"];
|
|
39
|
+
export declare const RECORD_KINDS: readonly ["observation", "inquiry", "link", "work_proposal", "run_request", "handoff", "source_intent", "review_operation_request", "deep_diver_request", "review_episode_link_request", "calendar_event_draft", "session_completion_proposal", "research_artifact", "inquiry_episode_request", "session_steer_request", "provider_choice", "provider_policy"];
|
|
40
40
|
export type RecordKind = (typeof RECORD_KINDS)[number];
|
|
41
41
|
/** #816 handoff states. */
|
|
42
42
|
export declare const HANDOFF_STATES: readonly ["pending", "blocked", "ready", "in_progress", "completed", "withdrawn"];
|
package/dist/records.js
CHANGED
|
@@ -61,6 +61,9 @@ export const RECORD_KINDS = [
|
|
|
61
61
|
"research_artifact",
|
|
62
62
|
"inquiry_episode_request",
|
|
63
63
|
"session_steer_request",
|
|
64
|
+
// A requester's own per-mission provider choice and provider policy, made on the connector's pages (jgwill/Miadi#926).
|
|
65
|
+
"provider_choice",
|
|
66
|
+
"provider_policy",
|
|
64
67
|
];
|
|
65
68
|
/** #816 handoff states. */
|
|
66
69
|
export const HANDOFF_STATES = ["pending", "blocked", "ready", "in_progress", "completed", "withdrawn"];
|
|
@@ -98,6 +101,7 @@ export function current(records) {
|
|
|
98
101
|
approval_state: r.approval_state ?? base.approval_state,
|
|
99
102
|
next_action: r.next_action ?? base.next_action,
|
|
100
103
|
completion_receipt: r.completion_receipt ?? base.completion_receipt,
|
|
104
|
+
...(base.kind === "source_intent" && r.kind === "source_intent" && r.body.source_readiness_transition === true ? { body: { ...base.body, readiness: r.body.readiness } } : {}),
|
|
101
105
|
history: [...(base.history ?? []), r.id],
|
|
102
106
|
});
|
|
103
107
|
continue;
|
package/dist/request-state.js
CHANGED
|
@@ -154,6 +154,9 @@ function projectCore(record, ctx = {}) {
|
|
|
154
154
|
if (ctx.queue?.state === "cancelled" || ctx.queue?.state === "stale") {
|
|
155
155
|
outstanding.push(`The factory intake set the brief aside (${ctx.queue.state}), so nothing is running for it.`);
|
|
156
156
|
}
|
|
157
|
+
if (ctx.queue?.state === "refused") {
|
|
158
|
+
outstanding.push(`${ctx.queue.note ?? "The factory runner refused to launch it."} Nothing is running for it.`);
|
|
159
|
+
}
|
|
157
160
|
const lines = [...decisionLines(decision), outcomeWord ? `Outcome: ${outcomeWord}.` : "Outcome: queued for the factory intake."];
|
|
158
161
|
if (delivered)
|
|
159
162
|
lines.push("Delivered to an agent session.");
|
package/dist/server.js
CHANGED
|
@@ -16,7 +16,7 @@ import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/
|
|
|
16
16
|
import { mcpAuthRouter, createOAuthMetadata, getOAuthProtectedResourceMetadataUrl } from "@modelcontextprotocol/sdk/server/auth/router.js";
|
|
17
17
|
import { requireBearerAuth } from "@modelcontextprotocol/sdk/server/auth/middleware/bearerAuth.js";
|
|
18
18
|
import { MiadiOAuthProvider, SCOPES, SCOPE_CONTRIBUTE, SCOPE_READ, emptyOAuthState } from "./oauth.js";
|
|
19
|
-
import { reconcileInterrupted, registerApprovalRoutes } from "./approve.js";
|
|
19
|
+
import { reconcileInterrupted, registerApprovalRoutes, registerProviderPolicyRoutes } from "./approve.js";
|
|
20
20
|
import { DecisionTokens } from "./decisions.js";
|
|
21
21
|
import { JsonFile, Ledger, ensureStateDir, statePaths } from "./store.js";
|
|
22
22
|
import { registerTools } from "./tools.js";
|
|
@@ -92,6 +92,7 @@ export function createConnectorApp(config, deps) {
|
|
|
92
92
|
// A run the connector was recording when it last stopped is failed now, so a person may run it again (#924).
|
|
93
93
|
reconcileInterrupted(approvalDeps);
|
|
94
94
|
registerApprovalRoutes(app, approvalDeps);
|
|
95
|
+
registerProviderPolicyRoutes(app, approvalDeps);
|
|
95
96
|
const decisionTokens = new DecisionTokens(config.stateDir, config.decisionTokenTtl);
|
|
96
97
|
const bearer = requireBearerAuth({
|
|
97
98
|
verifier: provider,
|
package/dist/store.d.ts
CHANGED
|
@@ -17,6 +17,8 @@ export declare class Ledger<R extends {
|
|
|
17
17
|
private cache;
|
|
18
18
|
constructor(path: string);
|
|
19
19
|
append(record: R): R;
|
|
20
|
+
/** A processing receipt is committed to disk before its dependent stage starts. */
|
|
21
|
+
appendDurable(record: R): R;
|
|
20
22
|
all(): R[];
|
|
21
23
|
}
|
|
22
24
|
export declare function statePaths(dir: string): {
|
package/dist/store.js
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* (observations, handoffs, proposals). Append-only: a record
|
|
11
11
|
* is never edited, a later record supersedes it.
|
|
12
12
|
*/
|
|
13
|
-
import { appendFileSync, chmodSync, existsSync, mkdirSync, readFileSync, statSync } from "node:fs";
|
|
13
|
+
import { appendFileSync, chmodSync, closeSync, existsSync, fsyncSync, mkdirSync, openSync, readFileSync, statSync, writeSync } from "node:fs";
|
|
14
14
|
import { createHash, randomBytes } from "node:crypto";
|
|
15
15
|
import { join } from "node:path";
|
|
16
16
|
import { readJsonSafe, writeJsonAtomic } from "@miadi/node-service-kit/json";
|
|
@@ -66,6 +66,23 @@ export class Ledger {
|
|
|
66
66
|
appendFileSync(this.path, JSON.stringify(record) + "\n", { mode: 0o600 });
|
|
67
67
|
return record;
|
|
68
68
|
}
|
|
69
|
+
/** A processing receipt is committed to disk before its dependent stage starts. */
|
|
70
|
+
appendDurable(record) {
|
|
71
|
+
const fd = openSync(this.path, "a", 0o600);
|
|
72
|
+
try {
|
|
73
|
+
// Keep a torn crash tail separate from this record, as all() skips invalid lines.
|
|
74
|
+
const old = readFileSync(this.path);
|
|
75
|
+
const line = Buffer.from((old.length && old[old.length - 1] !== 10 ? "\n" : "") + JSON.stringify(record) + "\n");
|
|
76
|
+
let written = 0;
|
|
77
|
+
while (written < line.length)
|
|
78
|
+
written += writeSync(fd, line, written, line.length - written);
|
|
79
|
+
fsyncSync(fd);
|
|
80
|
+
}
|
|
81
|
+
finally {
|
|
82
|
+
closeSync(fd);
|
|
83
|
+
}
|
|
84
|
+
return record;
|
|
85
|
+
}
|
|
69
86
|
all() {
|
|
70
87
|
if (!existsSync(this.path))
|
|
71
88
|
return [];
|