@hypercarrier/pi-team-durable 0.0.0-stage → 0.1.0-rc.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +209 -2
  3. package/dist/authority/anchor.d.ts +10 -0
  4. package/dist/authority/anchor.js +19 -0
  5. package/dist/authority/card-types.d.ts +152 -0
  6. package/dist/authority/card-types.js +123 -0
  7. package/dist/authority/contracts.d.ts +19 -0
  8. package/dist/authority/contracts.js +45 -0
  9. package/dist/authority/delivery.d.ts +60 -0
  10. package/dist/authority/delivery.js +158 -0
  11. package/dist/authority/graph-card.d.ts +4 -0
  12. package/dist/authority/graph-card.js +39 -0
  13. package/dist/authority/graph-control.d.ts +299 -0
  14. package/dist/authority/graph-control.js +1009 -0
  15. package/dist/authority/incremental-graph-control.d.ts +156 -0
  16. package/dist/authority/incremental-graph-control.js +1051 -0
  17. package/dist/authority/index.d.ts +15 -0
  18. package/dist/authority/index.js +15 -0
  19. package/dist/authority/interrupt.d.ts +11 -0
  20. package/dist/authority/interrupt.js +26 -0
  21. package/dist/authority/limits.d.ts +7 -0
  22. package/dist/authority/limits.js +7 -0
  23. package/dist/authority/notifier.d.ts +32 -0
  24. package/dist/authority/notifier.js +89 -0
  25. package/dist/authority/observation-store.d.ts +104 -0
  26. package/dist/authority/observation-store.js +226 -0
  27. package/dist/authority/observation.d.ts +13 -0
  28. package/dist/authority/observation.js +143 -0
  29. package/dist/authority/panel.d.ts +121 -0
  30. package/dist/authority/panel.js +44 -0
  31. package/dist/authority/projection.d.ts +5 -0
  32. package/dist/authority/projection.js +15 -0
  33. package/dist/authority/rc7-contract.json +9390 -0
  34. package/dist/authority/runtime.d.ts +106 -0
  35. package/dist/authority/runtime.js +115 -0
  36. package/dist/authority/state.d.ts +165 -0
  37. package/dist/authority/state.js +28 -0
  38. package/dist/authority/store.d.ts +144 -0
  39. package/dist/authority/store.js +675 -0
  40. package/dist/authority/sync.d.ts +13 -0
  41. package/dist/authority/sync.js +78 -0
  42. package/dist/authority/task-version-ref.d.ts +3 -0
  43. package/dist/authority/task-version-ref.js +5 -0
  44. package/dist/authority/tools.d.ts +105 -0
  45. package/dist/authority/tools.js +302 -0
  46. package/dist/testing.d.ts +11 -0
  47. package/dist/testing.js +10 -0
  48. package/package.json +63 -4
@@ -0,0 +1,15 @@
1
+ export { createTeamRuntime, pendingStops, confirmStop, closing, stopPlan, type RuntimeOptions, type StopPlan, type StopOutcome } from './runtime.ts';
2
+ export { observe, ObservationDoc, observations, observationStats, openObservations, type Observations, type Page, type ObservationPortResolver } from './observation.ts';
3
+ export { TeamDoc, GraphDoc, JournalDoc, OutboxDoc, ReceiptDoc, open, plain, type TeamState, type GraphState, type JournalState, type OutboxState, type Team, type Worker, type ModelRole, type ModelRoleCatalog, type State } from './state.ts';
4
+ export { GraphTaskController } from './graph-control.ts';
5
+ export { projectGraphTaskCards } from './graph-card.ts';
6
+ export { snapshotResult, currentAlerts } from './projection.ts';
7
+ export { abortExpectedRun, type AbortRun, type InterruptOutcome, type InterruptTarget } from './interrupt.ts';
8
+ export { TASK_ID_MAX_LENGTH } from './limits.ts';
9
+ export { deliveryDraft, deliveryInput, SENDER_LIMIT, type DeliveryInput, type TeamNoticeData } from './delivery.ts';
10
+ export { pageDraft } from './notifier.ts';
11
+ export { updates, pageEvents, UPDATE_PAGE_EVENTS, UPDATE_SCAN_ENTRIES } from './sync.ts';
12
+ export { HeadDoc, TaskDoc, OperationDoc, ObligationDoc, AlertDoc, CallDoc, DirectCallDoc, openAuthority, taskAddress, type AuthorityHead, type TaskRecord } from './store.ts';
13
+ export { readTeamPanel, readTeamSummary } from './panel.ts';
14
+ export { readObservationCursor } from './observation-store.ts';
15
+ export { taskCards } from './store.ts';
@@ -0,0 +1,15 @@
1
+ export { createTeamRuntime, pendingStops, confirmStop, closing, stopPlan } from "./runtime.js";
2
+ export { observe, ObservationDoc, observations, observationStats, openObservations } from "./observation.js";
3
+ export { TeamDoc, GraphDoc, JournalDoc, OutboxDoc, ReceiptDoc, open, plain } from "./state.js";
4
+ export { GraphTaskController } from "./graph-control.js";
5
+ export { projectGraphTaskCards } from "./graph-card.js";
6
+ export { snapshotResult, currentAlerts } from "./projection.js";
7
+ export { abortExpectedRun } from "./interrupt.js";
8
+ export { TASK_ID_MAX_LENGTH } from "./limits.js";
9
+ export { deliveryDraft, deliveryInput, SENDER_LIMIT } from "./delivery.js";
10
+ export { pageDraft } from "./notifier.js";
11
+ export { updates, pageEvents, UPDATE_PAGE_EVENTS, UPDATE_SCAN_ENTRIES } from "./sync.js";
12
+ export { HeadDoc, TaskDoc, OperationDoc, ObligationDoc, AlertDoc, CallDoc, DirectCallDoc, openAuthority, taskAddress } from "./store.js";
13
+ export { readTeamPanel, readTeamSummary } from "./panel.js";
14
+ export { readObservationCursor } from "./observation-store.js";
15
+ export { taskCards } from "./store.js";
@@ -0,0 +1,11 @@
1
+ import type { Context } from '@earendil-works/chord/context';
2
+ import { type Harness, type TaskId, type ConversationId, type DocumentReader } from '@earendil-works/pi-durable';
3
+ export interface InterruptTarget {
4
+ key: string;
5
+ conversationId: ConversationId;
6
+ expectedRunTaskId?: TaskId;
7
+ }
8
+ export type InterruptOutcome = 'interrupted' | 'ended' | 'failed' | 'retired';
9
+ export type AbortRun = (api: Pick<DocumentReader, 'snapshot'>, target: InterruptTarget, context: Context) => Promise<InterruptOutcome>;
10
+ /** Host primitive: exact native identity, cancellable wait, and no conversation-wide abort. */
11
+ export declare function abortExpectedRun(harness: Harness, target: InterruptTarget, context: Context): Promise<InterruptOutcome>;
@@ -0,0 +1,26 @@
1
+ import { LiveDoc } from '@earendil-works/pi-durable';
2
+ import { openAuthority as open, obligation } from "./store.js";
3
+ import { eligible } from "./delivery.js";
4
+ /** Host primitive: exact native identity, cancellable wait, and no conversation-wide abort. */
5
+ export async function abortExpectedRun(harness, target, context) {
6
+ const check = await harness.commit(async (tx) => {
7
+ const s = await open(tx), o = await obligation(s, target.key);
8
+ if (!o || !await eligible(s, o) || o.conversationId !== target.conversationId || o.interruptRun !== target.expectedRunTaskId)
9
+ return 'retired';
10
+ if (target.expectedRunTaskId === undefined)
11
+ return 'ended';
12
+ const task = await tx.task(target.expectedRunTaskId), live = await tx.doc(LiveDoc, target.conversationId);
13
+ if (!task || task.conversationId !== target.conversationId)
14
+ return 'failed';
15
+ if (task.state.status === 'terminal')
16
+ return task.state.outcome.status === 'aborted' ? 'interrupted' : 'ended';
17
+ if (live.run?.taskId !== target.expectedRunTaskId)
18
+ return 'ended';
19
+ return 'abort';
20
+ }, context);
21
+ if (check !== 'abort')
22
+ return check;
23
+ await harness.abortTask(target.expectedRunTaskId, context);
24
+ const task = await harness.waitForTask(target.expectedRunTaskId, context);
25
+ return task.state.outcome.status === 'aborted' ? 'interrupted' : 'failed';
26
+ }
@@ -0,0 +1,7 @@
1
+ import { Type } from "typebox";
2
+ /** Static card type defaults. contracts.ts applies the configured runtime goal limit. */
3
+ export declare const TASK_CARD_GOAL_MAX_LENGTH = 1600;
4
+ export declare const TASK_ID_MAX_LENGTH = 128;
5
+ export declare const TASK_CARD_TITLE_MAX_LENGTH = 80;
6
+ export declare const TASK_CARD_CONTEXT_MAX_LENGTH = 2000;
7
+ export declare const TaskVersionRefSchema: Type.TString;
@@ -0,0 +1,7 @@
1
+ import { Type } from "typebox";
2
+ /** Static card type defaults. contracts.ts applies the configured runtime goal limit. */
3
+ export const TASK_CARD_GOAL_MAX_LENGTH = 1_600;
4
+ export const TASK_ID_MAX_LENGTH = 128;
5
+ export const TASK_CARD_TITLE_MAX_LENGTH = 80;
6
+ export const TASK_CARD_CONTEXT_MAX_LENGTH = 2_000;
7
+ export const TaskVersionRefSchema = Type.String({ pattern: "^v_[0-9a-f]{16}$", minLength: 18, maxLength: 18 });
@@ -0,0 +1,32 @@
1
+ import { type ConversationId, type Tx } from '@earendil-works/pi-durable';
2
+ import { type Page } from './observation-store.ts';
3
+ import { type ModelRoleCatalog } from './state.ts';
4
+ import type { TeamNoticeData } from './delivery.ts';
5
+ export declare const NOTIFY_RECHECK_MS = 1000;
6
+ export declare const NOTIFY_DELAY_MS = 30000;
7
+ /** The input a notifier page submits to the lead: a Team update that durable-team itself writes. */
8
+ export declare function pageDraft(page: Page): {
9
+ type: "input";
10
+ content: string;
11
+ whenBusy: "followUp";
12
+ requestId: string;
13
+ entry: {
14
+ kind: string;
15
+ data: TeamNoticeData;
16
+ };
17
+ };
18
+ export declare function createNotifier(rolesOf: (tx: Tx, lead: ConversationId) => Promise<ModelRoleCatalog>): import("@earendil-works/pi-durable").Task<{
19
+ epoch: string;
20
+ }, {
21
+ phase: "tick";
22
+ ordinal: number;
23
+ due?: number;
24
+ } | {
25
+ phase: "submit";
26
+ pageId: string;
27
+ ordinal: number;
28
+ } | {
29
+ phase: "settle";
30
+ pageId: string;
31
+ ordinal: number;
32
+ }, null, object>;
@@ -0,0 +1,89 @@
1
+ import { defineTask, LiveDoc } from '@earendil-works/pi-durable';
2
+ import { openObservations, reserve, pages, page as loadPage, savePage } from "./observation-store.js";
3
+ import { openAuthority as open } from "./store.js";
4
+ import { snapshotResult } from "./projection.js";
5
+ import { pageEvents, updates } from "./sync.js";
6
+ export const NOTIFY_RECHECK_MS = 1000;
7
+ export const NOTIFY_DELAY_MS = 30000;
8
+ /** The input a notifier page submits to the lead: a Team update that durable-team itself writes. */
9
+ export function pageDraft(page) {
10
+ const data = { source: 'team', pageId: page.id, epoch: page.epoch, delivery: 'update', sender: 'durable-team' };
11
+ const content = page.carrier.kind === 'input' ? page.carrier.text : page.body;
12
+ if (content === undefined)
13
+ throw new Error('PTB notifier page body is missing');
14
+ return { type: 'input', content, whenBusy: 'followUp', requestId: page.id, entry: { kind: 'team.notice', data } };
15
+ }
16
+ const draft = pageDraft;
17
+ export function createNotifier(rolesOf) {
18
+ return defineTask({
19
+ name: 'ptb.notify', version: 1, initial: () => ({ phase: 'tick', ordinal: 1 }),
20
+ phases: {
21
+ tick: async (task, runtime, context) => {
22
+ await runtime.commit(async (tx) => {
23
+ const s = await open(tx), team = s.team.current, o = await openObservations(tx);
24
+ if (!team || team.epoch !== task.input.epoch || team.lifecycle !== 'active' || team.shutdownPending)
25
+ return { status: 'terminal', outcome: { status: 'completed', result: null } };
26
+ const live = await tx.doc(LiveDoc, team.lead);
27
+ const pending = (await pages(o, 'openPages')).some(p => p.epoch === team.epoch);
28
+ const checkpoint = task.state.checkpoint, due = checkpoint.due ?? runtime.now() + NOTIFY_DELAY_MS;
29
+ if (o.head.paused || pending || live.run || s.journal.seq <= o.head.acknowledged || runtime.now() < due)
30
+ return { status: 'sleeping', until: runtime.now() + NOTIFY_RECHECK_MS, checkpoint: { ...checkpoint, due } };
31
+ const from = o.head.epoch === team.epoch ? o.head.acknowledged : 0;
32
+ const previous = o.head.scanByEpoch[team.epoch];
33
+ const change = from ? await pageEvents(tx, s, from, previous) : undefined;
34
+ const roles = change ? undefined : await rolesOf(tx, team.lead);
35
+ const result = change ? await updates(s, change.events) : await snapshotResult(s, roles.modelRoles, roles.defaultModelRole);
36
+ const id = `${team.epoch}:notify:${runtime.taskId}:${checkpoint.ordinal}`;
37
+ const text = JSON.stringify({ kind: 'team_update', page_id: id, result });
38
+ const scanThrough = change?.scanThrough ?? s.journal.events.at(-1)?.entryId;
39
+ await reserve(o, { id, epoch: team.epoch, lead: team.lead, run: 0, from, through: change ? change.events.at(-1)?.seq ?? from : s.journal.seq, carrier: { kind: 'input', text }, notifierId: runtime.taskId, ...(scanThrough ? { scanThrough } : {}) }, result);
40
+ return { status: 'running', checkpoint: { phase: 'submit', pageId: id, ordinal: checkpoint.ordinal } };
41
+ }, context);
42
+ },
43
+ submit: async (task, runtime, context) => {
44
+ let page;
45
+ await runtime.commit(async (tx) => {
46
+ const s = await open(tx), o = await openObservations(tx), p = await loadPage(o, task.state.checkpoint.pageId, true);
47
+ if (!p || s.team.current?.epoch !== p.epoch || s.team.current?.shutdownPending || s.team.current?.lifecycle !== 'active')
48
+ return { status: 'terminal', outcome: { status: 'completed', result: null } };
49
+ page = p;
50
+ return undefined;
51
+ }, context);
52
+ if (!page)
53
+ return;
54
+ const target = await runtime.conversation(page.lead, context);
55
+ if (!target)
56
+ throw new Error('PTB lead is missing');
57
+ const submission = await target.submit(draft(page), context);
58
+ await runtime.commit(async (tx) => { const o = await openObservations(tx), p = (await loadPage(o, page.id)); p.submissionId = submission.id; await savePage(o, p); return { status: 'running', checkpoint: { ...task.state.checkpoint, phase: 'settle' } }; }, context);
59
+ },
60
+ settle: async (task, runtime, context) => {
61
+ let page;
62
+ await runtime.commit(async (tx) => { page = (await loadPage(await openObservations(tx), task.state.checkpoint.pageId, true)); return undefined; }, context);
63
+ const target = await runtime.conversation(page.lead, context);
64
+ if (!target)
65
+ throw new Error('PTB lead is missing');
66
+ const submission = await target.submit(draft(page), context);
67
+ if (submission.id !== page.submissionId)
68
+ throw new Error('PTB notifier admission identity changed');
69
+ const settled = await submission.wait(context);
70
+ await runtime.commit(async (tx) => {
71
+ const o = await openObservations(tx), p = (await loadPage(o, page.id));
72
+ if (p.run === 0)
73
+ p.run = submission.id;
74
+ if (settled.status !== 'done') {
75
+ p.state = 'released';
76
+ o.head.paused = true;
77
+ }
78
+ await savePage(o, p);
79
+ return { status: 'sleeping', until: runtime.now() + NOTIFY_RECHECK_MS, checkpoint: { phase: 'tick', ordinal: task.state.checkpoint.ordinal + 1 } };
80
+ }, context);
81
+ },
82
+ },
83
+ abort: async (task, runtime, context) => runtime.commit(async (tx) => { const o = await openObservations(tx); for (const p of await pages(o, 'openPages'))
84
+ if (p.epoch === task.input.epoch && p.carrier.kind === 'input') {
85
+ p.state = 'released';
86
+ await savePage(o, p);
87
+ } return { status: 'terminal', outcome: { status: 'aborted' } }; }, context),
88
+ });
89
+ }
@@ -0,0 +1,104 @@
1
+ import { type JsonObject, type Tx, type ConversationId, type TaskId, type SubmissionId, type EntryId } from '@earendil-works/pi-durable';
2
+ import { type Result } from './state.ts';
3
+ export type Carrier = {
4
+ kind: 'tool';
5
+ callId: string;
6
+ } | {
7
+ kind: 'input';
8
+ text?: string;
9
+ };
10
+ export interface Page {
11
+ id: string;
12
+ epoch: string;
13
+ lead: ConversationId;
14
+ run: SubmissionId;
15
+ from: number;
16
+ through: number;
17
+ body?: string;
18
+ digest: string;
19
+ carrier: Carrier;
20
+ state: 'reserved' | 'presented' | 'released' | 'acknowledged' | 'replaced';
21
+ scanThrough?: EntryId;
22
+ submissionId?: SubmissionId;
23
+ notifierId?: TaskId;
24
+ pruned?: boolean;
25
+ }
26
+ export interface Proof {
27
+ generation: TaskId;
28
+ lead: ConversationId;
29
+ run: SubmissionId;
30
+ pages: string[];
31
+ responseDigest?: string;
32
+ }
33
+ export interface Observations {
34
+ pages: Record<string, Page>;
35
+ proofs: Record<string, Proof>;
36
+ acknowledged: number;
37
+ epoch?: string;
38
+ paused?: boolean;
39
+ resumedOwnerRun?: SubmissionId;
40
+ }
41
+ export declare const ObservationDoc: import("@earendil-works/pi-durable").SessionDocToken<JsonObject>;
42
+ interface Head {
43
+ initialized: boolean;
44
+ legacyFenced?: boolean;
45
+ epoch?: string;
46
+ acknowledged: number;
47
+ paused?: boolean;
48
+ resumedOwnerRun?: SubmissionId;
49
+ ordinal: number;
50
+ pageTail?: string;
51
+ openPages: Record<string, number>;
52
+ releasedPages: Record<string, number>;
53
+ waitingAck: Record<string, number>;
54
+ prunePages: Record<string, number>;
55
+ openProofs: Record<string, true>;
56
+ ackByEpoch: Record<string, number>;
57
+ scanByEpoch: Record<string, EntryId>;
58
+ acknowledgedScanThrough?: EntryId;
59
+ }
60
+ export declare const ObservationHeadDoc: import("@earendil-works/pi-durable").SessionDocToken<JsonObject>;
61
+ export declare const PageDoc: import("@earendil-works/pi-durable").SessionDocFamilyToken<JsonObject, null>, PageBodyDoc: import("@earendil-works/pi-durable").SessionDocFamilyToken<JsonObject, null>, ProofDoc: import("@earendil-works/pi-durable").SessionDocFamilyToken<JsonObject, null>;
62
+ export interface ObservationStore {
63
+ tx: Tx;
64
+ head: Head;
65
+ }
66
+ export declare const digest: (value: unknown) => string;
67
+ export declare function startObservationEpoch(s: ObservationStore, epoch: string): void;
68
+ export declare function openObservations(tx: Tx): Promise<ObservationStore>;
69
+ export declare function page(s: ObservationStore, id: string, withBody?: boolean): Promise<Page | undefined>;
70
+ export declare function pages(s: ObservationStore, index: 'openPages' | 'releasedPages' | 'waitingAck' | 'prunePages', withBody?: boolean): Promise<Page[]>;
71
+ export declare function savePage(s: ObservationStore, value: Page): Promise<void>;
72
+ export declare function reserve(s: ObservationStore, input: Omit<Page, 'body' | 'digest' | 'state' | 'pruned'>, result: Result): Promise<Page>;
73
+ export declare function reservationHead(s: ObservationStore, epoch: string, run: SubmissionId): Promise<number>;
74
+ export declare function advanceAcknowledged(s: ObservationStore): Promise<void>;
75
+ export declare function proof(s: ObservationStore, id: string): Promise<Proof | undefined>;
76
+ export declare function openProofs(s: ObservationStore): Promise<Proof[]>;
77
+ export declare function saveProof(s: ObservationStore, value: Proof): Promise<void>;
78
+ export declare function retireProof(s: ObservationStore, id: string): Promise<void>;
79
+ export declare function retireBody(s: ObservationStore, value: Page): Promise<void>;
80
+ /** Diagnostic UTF-8 JSON bytes. Excludes native record metadata and history. */
81
+ export declare function observationStats(tx: Tx): Promise<{
82
+ pages: {
83
+ count: number;
84
+ currentRecordBytes: number;
85
+ };
86
+ bodies: {
87
+ count: number;
88
+ currentRecordBytes: number;
89
+ };
90
+ openProofs: {
91
+ count: number;
92
+ currentRecordBytes: number;
93
+ };
94
+ headBytes: number;
95
+ totalBytes: number;
96
+ }>;
97
+ /** Explicit diagnostic projection. Runtime paths read the current indexes. */
98
+ export declare function observations(tx: Tx): Promise<Observations>;
99
+ /** Owner reads use only pages at or below the acknowledged cursor. This read never advances it. */
100
+ export declare function readObservationCursor(tx: Tx, epoch: string): Promise<{
101
+ acknowledged: number;
102
+ scanThrough?: EntryId;
103
+ }>;
104
+ export {};
@@ -0,0 +1,226 @@
1
+ // Observation authority separates current indexes, immutable page bodies, and proofs.
2
+ // Settled pages retain metadata. Only the observer can retire bodies and proofs.
3
+ import { defineDoc, defineDocFamily } from '@earendil-works/pi-durable';
4
+ import { createHash } from 'node:crypto';
5
+ import { fenceLegacyDoc, hasLegacyFence } from "./state.js";
6
+ export const ObservationDoc = defineDoc({ kind: 'ptb.observation', version: 2, migrate: value => value, scope: 'session', initial: () => ({ pages: {}, proofs: {}, acknowledged: 0 }), checkpointWhen: () => true });
7
+ export const ObservationHeadDoc = defineDoc({ kind: 'ptb.observation.head', version: 1, scope: 'session', initial: () => ({ initialized: false, acknowledged: 0, ordinal: 0, openPages: {}, releasedPages: {}, waitingAck: {}, prunePages: {}, openProofs: {}, ackByEpoch: {}, scanByEpoch: {} }), checkpointWhen: () => true });
8
+ const family = (kind) => defineDocFamily({ kind, version: 1, scope: 'session', family: true, initial: () => ({}), checkpointWhen: () => true });
9
+ export const PageDoc = family('ptb.observation.page'), PageBodyDoc = family('ptb.observation.body'), ProofDoc = family('ptb.observation.proof');
10
+ const transactions = new WeakMap();
11
+ export const digest = (value) => createHash('sha256').update(typeof value === 'string' ? value : JSON.stringify(value)).digest('hex');
12
+ const copyPage = (page) => ({ ...page, carrier: { ...page.carrier } });
13
+ const copyProof = (proof) => ({ ...proof, pages: [...proof.pages] });
14
+ export function startObservationEpoch(s, epoch) {
15
+ const h = s.head;
16
+ if (h.epoch === epoch)
17
+ return;
18
+ if (h.epoch)
19
+ h.ackByEpoch[h.epoch] = h.acknowledged;
20
+ h.epoch = epoch;
21
+ h.acknowledged = h.ackByEpoch[epoch] ?? 0;
22
+ delete h.acknowledgedScanThrough;
23
+ h.paused = false;
24
+ }
25
+ export async function openObservations(tx) {
26
+ const cached = transactions.get(tx);
27
+ if (cached)
28
+ return cached;
29
+ const head = await tx.doc(ObservationHeadDoc), s = { tx, head };
30
+ transactions.set(tx, s);
31
+ if (!head.initialized) {
32
+ const old = await tx.doc(ObservationDoc);
33
+ if (hasLegacyFence(old))
34
+ throw new Error('Observation index is missing after authority cutover.');
35
+ if (old.epoch) {
36
+ head.epoch = old.epoch;
37
+ head.ackByEpoch[old.epoch] = old.acknowledged;
38
+ }
39
+ head.acknowledged = old.acknowledged;
40
+ if (old.paused !== undefined)
41
+ head.paused = old.paused;
42
+ if (old.resumedOwnerRun !== undefined)
43
+ head.resumedOwnerRun = old.resumedOwnerRun;
44
+ for (const value of Object.values(old.pages)) {
45
+ const page = copyPage(value);
46
+ if (typeof page.body !== 'string')
47
+ throw new Error('Legacy observation page body is missing.');
48
+ if (page.carrier.kind === 'input') {
49
+ const prefix = `${page.epoch}:notify:`, suffix = page.id.startsWith(prefix) ? page.id.slice(prefix.length).split(':') : [];
50
+ if (suffix.length === 2 && /^\d+$/.test(suffix[0]) && /^\d+$/.test(suffix[1]))
51
+ page.notifierId = Number(suffix[0]);
52
+ }
53
+ await createPage(s, page);
54
+ }
55
+ for (const value of Object.values(old.proofs)) {
56
+ const { generation, lead, run, pages, responseDigest } = value;
57
+ if (pages.length)
58
+ await saveProof(s, { generation, lead, run, pages: [...pages], ...(responseDigest ? { responseDigest } : {}) });
59
+ }
60
+ await advanceAcknowledged(s);
61
+ if (old.epoch) {
62
+ head.ackByEpoch[old.epoch] = Math.max(head.ackByEpoch[old.epoch] ?? 0, old.acknowledged);
63
+ head.acknowledged = head.ackByEpoch[old.epoch];
64
+ }
65
+ head.initialized = true;
66
+ }
67
+ if (!head.legacyFenced) {
68
+ fenceLegacyDoc(await tx.doc(ObservationDoc));
69
+ head.legacyFenced = true;
70
+ }
71
+ return s;
72
+ }
73
+ export async function page(s, id, withBody = false) {
74
+ const record = await s.tx.doc(PageDoc, id, null);
75
+ if (!record.value)
76
+ return;
77
+ const value = copyPage(record.value);
78
+ if (withBody && !value.pruned) {
79
+ const body = await s.tx.doc(PageBodyDoc, id, null);
80
+ if (typeof body.body !== 'string' || !body.carrier)
81
+ throw new Error('Observation page body is missing.');
82
+ value.body = body.body;
83
+ value.carrier = { ...body.carrier };
84
+ }
85
+ return value;
86
+ }
87
+ export async function pages(s, index, withBody = false) {
88
+ const ids = Object.entries(s.head[index]).sort((a, b) => a[1] - b[1]).map(([id]) => id);
89
+ return (await Promise.all(ids.map(id => page(s, id, withBody)))).filter((p) => !!p);
90
+ }
91
+ export async function savePage(s, value) {
92
+ const doc = await s.tx.doc(PageDoc, value.id, null), h = s.head;
93
+ if (!doc.value) {
94
+ if (h.pageTail)
95
+ doc.previous = h.pageTail;
96
+ doc.order = ++h.ordinal;
97
+ h.pageTail = value.id;
98
+ }
99
+ const { body: _body, carrier, ...meta } = value;
100
+ doc.value = { ...meta, carrier: carrier.kind === 'input' ? { kind: 'input' } : { ...carrier } };
101
+ const order = Number(doc.order);
102
+ for (const name of ['openPages', 'releasedPages', 'waitingAck', 'prunePages'])
103
+ delete h[name][value.id];
104
+ if (value.state === 'reserved' || value.state === 'presented')
105
+ h.openPages[value.id] = order;
106
+ if (value.state === 'released')
107
+ h.releasedPages[value.id] = order;
108
+ if (value.state === 'acknowledged') {
109
+ if (value.through > (h.ackByEpoch[value.epoch] ?? 0))
110
+ h.waitingAck[value.id] = order;
111
+ if (value.scanThrough !== undefined) {
112
+ h.scanByEpoch[value.epoch] = Math.max(h.scanByEpoch[value.epoch] ?? 0, value.scanThrough);
113
+ if (value.epoch === h.epoch && value.through <= (h.ackByEpoch[value.epoch] ?? 0))
114
+ h.acknowledgedScanThrough = Math.max(h.acknowledgedScanThrough ?? 0, value.scanThrough);
115
+ }
116
+ }
117
+ if (!value.pruned && (value.state === 'acknowledged' || value.state === 'replaced'))
118
+ h.prunePages[value.id] = order;
119
+ }
120
+ async function createPage(s, value) {
121
+ const doc = await s.tx.doc(PageBodyDoc, value.id, null);
122
+ doc.body = value.body;
123
+ doc.carrier = { ...value.carrier };
124
+ await savePage(s, value);
125
+ }
126
+ export async function reserve(s, input, result) {
127
+ startObservationEpoch(s, input.epoch);
128
+ const prior = await page(s, input.id, true);
129
+ if (prior)
130
+ return prior;
131
+ const body = JSON.stringify(result), value = { ...input, body, digest: digest(body), state: 'reserved' };
132
+ await createPage(s, value);
133
+ return value;
134
+ }
135
+ export async function reservationHead(s, epoch, run) {
136
+ return Math.max(s.head.epoch === epoch ? s.head.acknowledged : 0, ...(await pages(s, 'openPages')).filter(p => p.epoch === epoch && p.run === run).map(p => p.through));
137
+ }
138
+ export async function advanceAcknowledged(s) {
139
+ const waiting = await pages(s, 'waitingAck'), h = s.head;
140
+ for (let advanced = true; advanced;) {
141
+ advanced = false;
142
+ for (const p of waiting) {
143
+ const cursor = h.ackByEpoch[p.epoch] ?? 0;
144
+ if (p.from <= cursor && p.through > cursor) {
145
+ h.ackByEpoch[p.epoch] = p.through;
146
+ advanced = true;
147
+ }
148
+ }
149
+ }
150
+ // Fold newly reachable anchors before removing their pages from the waiting index.
151
+ for (const p of waiting)
152
+ if (p.through <= (h.ackByEpoch[p.epoch] ?? 0)) {
153
+ if (p.epoch === h.epoch && p.scanThrough !== undefined)
154
+ h.acknowledgedScanThrough = Math.max(h.acknowledgedScanThrough ?? 0, p.scanThrough);
155
+ delete h.waitingAck[p.id];
156
+ }
157
+ if (h.epoch)
158
+ h.acknowledged = h.ackByEpoch[h.epoch] ?? 0;
159
+ }
160
+ export async function proof(s, id) {
161
+ if (!s.head.openProofs[id])
162
+ return;
163
+ const value = (await s.tx.doc(ProofDoc, id, null)).value;
164
+ return value ? copyProof(value) : undefined;
165
+ }
166
+ export async function openProofs(s) {
167
+ return (await Promise.all(Object.keys(s.head.openProofs).map(id => proof(s, id)))).filter((p) => !!p);
168
+ }
169
+ export async function saveProof(s, value) {
170
+ (await s.tx.doc(ProofDoc, String(value.generation), null)).value = copyProof(value);
171
+ s.head.openProofs[String(value.generation)] = true;
172
+ }
173
+ export async function retireProof(s, id) {
174
+ if (!s.head.openProofs[id])
175
+ return;
176
+ delete s.head.openProofs[id];
177
+ await s.tx.retireDoc(ProofDoc, id);
178
+ }
179
+ export async function retireBody(s, value) {
180
+ await s.tx.retireDoc(PageBodyDoc, value.id);
181
+ value.pruned = true;
182
+ delete value.body;
183
+ if (value.carrier.kind === 'input')
184
+ delete value.carrier.text;
185
+ await savePage(s, value);
186
+ }
187
+ /** Diagnostic UTF-8 JSON bytes. Excludes native record metadata and history. */
188
+ export async function observationStats(tx) {
189
+ const s = await openObservations(tx), bytes = (value) => Buffer.byteLength(JSON.stringify(value));
190
+ const result = { pages: { count: 0, currentRecordBytes: 0 }, bodies: { count: 0, currentRecordBytes: 0 }, openProofs: { count: 0, currentRecordBytes: 0 }, headBytes: bytes(s.head), totalBytes: 0 };
191
+ let id = s.head.pageTail;
192
+ while (id) {
193
+ const record = await tx.doc(PageDoc, id, null), value = record.value;
194
+ result.pages.count++;
195
+ result.pages.currentRecordBytes += bytes(record);
196
+ if (!value.pruned) {
197
+ result.bodies.count++;
198
+ result.bodies.currentRecordBytes += bytes(await tx.doc(PageBodyDoc, id, null));
199
+ }
200
+ id = record.previous;
201
+ }
202
+ for (const id of Object.keys(s.head.openProofs)) {
203
+ result.openProofs.count++;
204
+ result.openProofs.currentRecordBytes += bytes(await tx.doc(ProofDoc, id, null));
205
+ }
206
+ result.totalBytes = result.headBytes + result.pages.currentRecordBytes + result.bodies.currentRecordBytes + result.openProofs.currentRecordBytes;
207
+ return result;
208
+ }
209
+ /** Explicit diagnostic projection. Runtime paths read the current indexes. */
210
+ export async function observations(tx) {
211
+ const s = await openObservations(tx), all = [];
212
+ let id = s.head.pageTail;
213
+ while (id) {
214
+ const record = await tx.doc(PageDoc, id, null);
215
+ const value = await page(s, id, true);
216
+ if (value)
217
+ all.push(value);
218
+ id = record.previous;
219
+ }
220
+ return { pages: Object.fromEntries(all.reverse().map(p => [p.id, p])), proofs: Object.fromEntries((await openProofs(s)).map(p => [String(p.generation), p])), acknowledged: s.head.acknowledged, ...(s.head.epoch ? { epoch: s.head.epoch } : {}), ...(s.head.paused === undefined ? {} : { paused: s.head.paused }), ...(s.head.resumedOwnerRun === undefined ? {} : { resumedOwnerRun: s.head.resumedOwnerRun }) };
221
+ }
222
+ /** Owner reads use only pages at or below the acknowledged cursor. This read never advances it. */
223
+ export async function readObservationCursor(tx, epoch) {
224
+ const head = await tx.doc(ObservationHeadDoc);
225
+ return head.epoch === epoch ? { acknowledged: head.acknowledged, ...(head.acknowledgedScanThrough === undefined ? {} : { scanThrough: head.acknowledgedScanThrough }) } : { acknowledged: 0 };
226
+ }
@@ -0,0 +1,13 @@
1
+ import type { Context } from '@earendil-works/chord/context';
2
+ import type { AssistantMessage } from '@earendil-works/pi-ai';
3
+ import { type Harness, type Tx, type HookApi, type Extension } from '@earendil-works/pi-durable';
4
+ export { ObservationDoc, ObservationHeadDoc, PageDoc, PageBodyDoc, ProofDoc, observations, observationStats, openObservations, reservationHead, reserve, startObservationEpoch, digest, type Page, type Observations } from './observation-store.ts';
5
+ export interface ObservationPort {
6
+ commit<T>(fn: (tx: Tx) => Promise<T>, context: Context): Promise<T>;
7
+ }
8
+ /** Install last among message-replacing hooks. No hook advances the acknowledged cursor. */
9
+ export type ObservationPortResolver = (api: HookApi, context: Context) => Promise<ObservationPort>;
10
+ export declare function observationExtension(resolvePort: ObservationPortResolver): Extension;
11
+ export declare function recordResponse(port: ObservationPort, api: Pick<HookApi, 'taskId'>, message: AssistantMessage, context: Context): Promise<void>;
12
+ /** Native persistence and settled runs permit ACK. Proofs never scan transcript history. */
13
+ export declare function observe(harness: Harness, context: Context): Promise<void>;