blob: fb1c594568c582c331a327d0ff1bfec719a87983 [file]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import type { RuntimeEvent } from './runtime-event.js';
import type { RuntimeInvocationRecord } from './runtime-invocation.js';
import type {
ContinuationClaimV1,
ImmutableRuntimePrefixV1,
RuntimeBoundaryDigest,
} from './runtime-boundary.js';
import type {
WorkspaceEpochRecordV1,
WorkspaceHeadRecordV1,
WorkspaceProjectionRebuildResult,
WorkspaceVersionRecordV1,
} from './workspace-version-authority.js';
import { WORKSPACE_VERSION_AUTHORITY_CAPABILITY_V1 } from './workspace-version-authority.js';
export const TOOL_RECOVERY_BUNDLE_CAPABILITY_V1 = 'tool_recovery_bundle_v1' as const;
export const RUNTIME_CONTINUATION_AUTHORITY_V1 = 'runtime_continuation_authority_v1' as const;
export interface RuntimeRecoveryBundleCommit {
operationId: string;
reconcileRuntimeEvent: RuntimeEvent;
outcomeRuntimeEvent?: RuntimeEvent;
decisionRuntimeEvent: RuntimeEvent;
}
/**
* An append arrived after the run's terminal fact was already written. The
* refusal is the store doing its job: once a run has said it ended, a late
* stream event is by definition not part of it. Typed so callers can tell
* this expected boundary apart from store failure (#2311): pressing stop
* seals the run ahead of the still-draining stream, and the stragglers that
* window refuses must not read as "the store is sick".
*/
export class RunSealedError extends Error {
readonly name = 'RunSealedError';
constructor(readonly runId: string) {
super(`RuntimeEvent run ${runId} is sealed by its terminal fact`);
}
}
/** A requested stable-storage barrier failed; read-back cannot upgrade it to success. */
export class DurableStoreWriteError extends Error {
readonly name = 'DurableStoreWriteError';
constructor(
message: string,
readonly storeCause: unknown,
) {
super(message);
}
}
export interface RuntimeEventStore {
/** Canonical stores fail the active run closed on every durable write error. */
readonly durability?: 'best_effort' | 'canonical';
/**
* Enumerate a Session's invocations from the canonical events.
*
* This is a query, not a table. Nothing writes it and nothing repairs it, so
* clearing any physical index and rebuilding from the events produces the
* same inventory. Reserved control-plane invocation streams have no opening
* fact and therefore never appear here.
*
* One exception, and it is a durable one: an invocation that predates the
* opening fact could not be given one without rewriting an immutable
* sequence, so a store that migrated such a Session keeps that opening
* outside the events and merges it in here. Those invocations cannot be
* rebuilt from events alone, and never will be.
*
* An invocation's `terminalEvent` is its first terminal event. Sealing makes
* that the only one for anything written through this interface; a ledger
* from before the seal can carry a straggler after it, and the ending is
* still the terminal event.
*/
listSessionInvocations(sessionId: string): Promise<RuntimeInvocationRecord[]>;
/**
* One invocation by run id, absent when no opening fact names it. A store
* that indexes openings answers this in one read; stores without the fast
* path are answered from the inventory by `readRunInvocation`.
*/
readRunInvocation?(
sessionId: string,
runId: string,
): Promise<RuntimeInvocationRecord | undefined>;
/**
* Append one event to a run.
*
* Every implementation seals: once a run holds a terminal event, appending
* any event the store does not already have must throw `RunSealedError`. An
* exact-id replay of an event already stored stays idempotent. This is what
* makes a run's ending single and final, so it is an obligation of this
* interface rather than a detail of one store — a test double that skips it
* is manufacturing a ledger no supported store can produce. Tests that need a
* corrupt ledger should build it beneath this interface, not through it.
*/
appendRuntimeEvent(
sessionId: string,
runId: string,
event: RuntimeEvent,
options?: { durable?: boolean },
): Promise<void>;
/**
* Coalesce one already-admitted mutable presentation stream into one store
* transaction. Callers must preserve provider order and flush before every
* immutable execution boundary. Stores that do not implement this optional
* fast path continue to receive one append per partial event. The seal on
* `appendRuntimeEvent` applies here too.
*/
appendRuntimePartialBatch?(
sessionId: string,
runId: string,
events: readonly RuntimeEvent[],
): Promise<void>;
/**
* Append the terminal event if absent, or re-establish its stable-storage
* barrier if present. This is the one writer the seal admits: it must commit
* the terminal event and the seal check in the same transaction, so two
* callers racing to end one run produce one terminal event and a
* `RunSealedError` for the loser.
*/
ensureTerminalRuntimeEventDurable(
sessionId: string,
runId: string,
event: RuntimeEvent,
): Promise<void>;
readRuntimeEvents(sessionId: string, runId: string): Promise<RuntimeEvent[]>;
/** Session-wide immutable append order. */
readSessionRuntimeEventEntries(
sessionId: string,
): Promise<Array<{ readonly ordinal: number; readonly event: RuntimeEvent }>>;
/** Physical append-log rows only; excludes mutable partial snapshots. */
readImmutableRuntimeEvents?(sessionId: string, runId: string): Promise<RuntimeEvent[]>;
/** Versioned physical prefix with event-seq high-water and canonical digest. */
readImmutableRuntimePrefix?(input: {
sessionId: string;
runId: string;
upToEventSeq?: number;
}): Promise<ImmutableRuntimePrefixV1>;
readSessionRuntimeEvents(sessionId: string): Promise<RuntimeEvent[]>;
/**
* Renumber a Session's event ordinals in the order its invocations opened.
*
* Ordinals are minted at append time, which is the conversation's order for
* every run this build starts. It is not the order of a run converted from
* the legacy transcript: that turn was said before runs already on the
* ledger, and it is appended after them. The transcript conversion is the
* only caller and the only writer that can know this, and it runs while the
* Session still has no ordinal reader, so these numbers are recomputed
* rather than moved out from under anyone.
*/
resequenceSessionEventOrdinals(sessionId: string): Promise<void>;
}
/** One invocation by run id, through the store's fast path when it has one. */
export async function readRunInvocation(
store: Pick<RuntimeEventStore, 'listSessionInvocations' | 'readRunInvocation'>,
sessionId: string,
runId: string,
): Promise<RuntimeInvocationRecord | undefined> {
if (store.readRunInvocation) return store.readRunInvocation(sessionId, runId);
return (await store.listSessionInvocations(sessionId)).find(
(invocation) => invocation.runId === runId,
);
}
export interface RuntimeRecoveryBundleStore extends RuntimeEventStore {
readonly recoveryBundleCapability: typeof TOOL_RECOVERY_BUNDLE_CAPABILITY_V1;
commitToolRecoveryBundle(input: RuntimeRecoveryBundleCommit): Promise<void>;
}
export type ContinuationClaimResult =
| { kind: 'acquired'; claim: ContinuationClaimV1 }
| { kind: 'existing'; claim: ContinuationClaimV1 }
| { kind: 'conflict'; claim: ContinuationClaimV1 };
export interface ContinuationClaimStateV1 {
claim: ContinuationClaimV1;
startEventId?: string;
/** Store-owned classification of the narrow command that committed event 1. */
startKind?: 'runtime_admission' | 'claim_repair';
}
export interface RuntimeContinuationAuthorityStore extends RuntimeEventStore {
readonly continuationAuthorityCapability: typeof RUNTIME_CONTINUATION_AUTHORITY_V1;
readImmutableRuntimeEvents(sessionId: string, runId: string): Promise<RuntimeEvent[]>;
readImmutableRuntimePrefix(input: {
sessionId: string;
runId: string;
upToEventSeq?: number;
}): Promise<ImmutableRuntimePrefixV1>;
claimContinuation(input: { claim: ContinuationClaimV1 }): Promise<ContinuationClaimResult>;
readContinuationClaimByBoundary(
boundaryDigest: RuntimeBoundaryDigest,
): Promise<ContinuationClaimV1 | undefined>;
readContinuationClaimStateByBoundary(
boundaryDigest: RuntimeBoundaryDigest,
): Promise<ContinuationClaimStateV1 | undefined>;
listContinuationClaimsForRecovery(sessionId: string): Promise<ContinuationClaimStateV1[]>;
commitContinuationStart(input: {
claim: ContinuationClaimV1;
event: RuntimeEvent;
}): Promise<{ created: boolean; runtimeEventSeq: number }>;
commitContinuationRepairStart(input: {
claim: ContinuationClaimV1;
event: RuntimeEvent;
}): Promise<{ created: boolean; runtimeEventSeq: number }>;
}
export interface RuntimeWorkspaceVersionAuthorityStore extends RuntimeEventStore {
readonly workspaceVersionAuthorityCapability: typeof WORKSPACE_VERSION_AUTHORITY_CAPABILITY_V1;
readWorkspaceEpoch(
workspaceId: string,
workspaceEpochId: string,
): Promise<WorkspaceEpochRecordV1 | undefined>;
readWorkspaceVersion(workspaceVersionId: string): Promise<WorkspaceVersionRecordV1 | undefined>;
readWorkspaceHead(
workspaceId: string,
workspaceEpochId: string,
): Promise<WorkspaceHeadRecordV1 | undefined>;
rebuildWorkspaceVersionProjections(): Promise<WorkspaceProjectionRebuildResult>;
}