blob: 8d89a6c4374d300a7bd4fcd9684247cd504ac63d [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 { randomUUID } from 'node:crypto';
import {
publishSessionCheckpointV1,
SessionRepositoryError,
type SessionRepository,
type ImmutableObjectStore,
type CommittedSessionRevision,
} from '@maka/storage/session-repository';
import type {
SessionCheckpointBinding,
SessionCheckpointPublicationStore,
CheckpointPublicationRequest,
} from '@maka/storage/session-checkpoint-publication-store';
import type { ProductionSessionBundleArtifact } from '@maka/storage/production-session-snapshot';
import { SessionSnapshotError } from '@maka/storage/quiescent-session-snapshot';
export type HostCheckpointErrorCode =
| 'invalid_input'
| 'unsupported'
| 'busy'
| 'conflict'
| 'cancelled'
| 'interrupted'
| 'closed'
| 'publication_failed';
export class HostCheckpointError extends Error {
constructor(
readonly code: HostCheckpointErrorCode,
message: string,
options?: ErrorOptions,
) {
super(message, options);
this.name = 'HostCheckpointError';
}
}
export interface PublishHostSessionCheckpointInput {
readonly sessionId: string;
/** Stable caller identity. A new snapshot requires a new request identity. */
readonly requestId: string;
readonly confirmationGrantId?: string;
readonly signal?: AbortSignal;
readonly deadlineAt?: number;
}
export interface PublishedHostSessionCheckpoint {
readonly requestId: string;
readonly binding: SessionCheckpointBinding;
readonly committed: CommittedSessionRevision;
readonly stagingCleanup: 'released' | 'pending_recovery';
readonly bundleCleanup: 'released' | 'pending_recovery';
}
export interface HostSessionCheckpointPublication {
publish(input: PublishHostSessionCheckpointInput): Promise<PublishedHostSessionCheckpoint>;
beginDrain(): void;
close(): Promise<void>;
}
export interface HostSessionCheckpointCoordinatorDependencies {
readonly journal: SessionCheckpointPublicationStore;
readonly repository: SessionRepository;
readonly objects: ImmutableObjectStore;
/** Enforces the Host admission and selected-provider snapshot boundary. */
capture(input: {
readonly binding: SessionCheckpointBinding;
readonly commitId: string;
readonly confirmationGrantId?: string;
readonly signal: AbortSignal;
readonly deadlineAt: number;
}): Promise<ProductionSessionBundleArtifact>;
cleanupBundle(commitId: string): Promise<void>;
/** Holds the authenticated root lease and an execution residency until settlement. */
runAuthorized<T>(operation: () => Promise<T>): Promise<T>;
}
/** Explicit management API, not a model tool, automatic live write, or second Head. */
export class HostSessionCheckpointCoordinator implements HostSessionCheckpointPublication {
readonly #active = new Set<Promise<unknown>>();
readonly #drain = new AbortController();
#closed = false;
constructor(private readonly dependencies: HostSessionCheckpointCoordinatorDependencies) {}
publish(input: PublishHostSessionCheckpointInput): Promise<PublishedHostSessionCheckpoint> {
if (this.#closed)
return Promise.reject(new HostCheckpointError('closed', 'Checkpoint publication is closed'));
const accepted = { ...input };
if (
!validId(accepted.sessionId) ||
!validId(accepted.requestId) ||
(accepted.signal !== undefined && !(accepted.signal instanceof AbortSignal)) ||
(accepted.confirmationGrantId !== undefined &&
!/^[A-Za-z0-9_-]{1,128}$/u.test(accepted.confirmationGrantId)) ||
(accepted.deadlineAt !== undefined && !Number.isSafeInteger(accepted.deadlineAt))
) {
return Promise.reject(
new HostCheckpointError('invalid_input', 'Invalid checkpoint publication request'),
);
}
// Cooperative deadline for the whole attempt, not part of commit identity.
const deadlineAt = Math.min(accepted.deadlineAt ?? Infinity, Date.now() + 60_000);
if (deadlineAt <= Date.now())
return Promise.reject(
new HostCheckpointError('cancelled', 'Checkpoint deadline has expired'),
);
const timer = AbortSignal.timeout(Math.max(0, deadlineAt - Date.now()));
const signal = AbortSignal.any([
timer,
this.#drain.signal,
...(accepted.signal ? [accepted.signal] : []),
]);
const task = this.dependencies
.runAuthorized(() => this.#publish(accepted, signal, deadlineAt))
.catch((error: unknown) => {
throw normalizeError(error);
});
this.#active.add(task);
void task.finally(() => this.#active.delete(task)).catch(() => {});
return task;
}
beginDrain(): void {
this.#closed = true;
this.#drain.abort();
}
async close(): Promise<void> {
this.beginDrain();
await Promise.allSettled([...this.#active]);
}
async #publish(
input: PublishHostSessionCheckpointInput,
signal: AbortSignal,
deadlineAt: number,
): Promise<PublishedHostSessionCheckpoint> {
const deps = this.dependencies;
signal.throwIfAborted();
return deps.journal.withSession(input.sessionId, async (document, save) => {
signal.throwIfAborted();
const releaseBundle = async (position: number): Promise<'released' | 'pending_recovery'> => {
const pending = document.requests[position]!;
if (!pending.bundleCleanupPending) return 'released';
try {
await deps.cleanupBundle(pending.commitId);
const { bundleCleanupPending: _pending, ...released } = pending;
document.requests[position] = released;
try {
await save();
} catch (error) {
document.requests[position] = pending;
throw error;
}
return 'released';
} catch {
// The terminal outcome is already durable. Retain cleanup ownership
// for the next request/restart, without changing that outcome.
return 'pending_recovery';
}
};
let index = document.requests.findIndex((request) => request.requestId === input.requestId);
let request = document.requests[index];
if (request && request.confirmationGrantId !== input.confirmationGrantId) {
throw new HostCheckpointError(
'conflict',
'Checkpoint request identity was reused with different input',
);
}
// Holding this process-safe lock proves no earlier capture is still running.
// Its bytes were never durably fixed, so the identity can only be aborted.
for (let position = 0; position < document.requests.length; position++) {
const previous = document.requests[position]!;
if (previous.phase === 'capturing') {
document.requests[position] = { ...previous, phase: 'aborted' };
await save();
}
if (previous.phase !== 'prepared') await releaseBundle(position);
}
request = document.requests[index];
if (request?.phase === 'aborted') {
throw new HostCheckpointError(
'interrupted',
'Snapshot capture did not complete; use a new request identity',
);
}
if (request?.phase === 'conflicted')
throw new HostCheckpointError('conflict', 'Checkpoint publication previously conflicted');
if (!request) {
if (document.requests.some((record) => record.phase === 'prepared')) {
throw new HostCheckpointError(
'busy',
'A prepared checkpoint must be reconciled using its original request identity',
);
}
let current: CommittedSessionRevision | undefined;
try {
current = await deps.repository.checkoutCurrent(document.binding.repositorySessionId);
} catch (error) {
if (!(error instanceof SessionRepositoryError) || error.code !== 'session_not_found')
throw error;
}
if (current && current.agentId !== document.binding.agentId) {
throw new HostCheckpointError(
'conflict',
'Repository Session binding does not match this Host',
);
}
request = {
requestId: input.requestId,
commitId: randomUUID(),
...(input.confirmationGrantId ? { confirmationGrantId: input.confirmationGrantId } : {}),
expectedRevision: current?.ref.revision ?? null,
bundleCleanupPending: true,
phase: 'capturing',
};
index = document.requests.length;
document.requests.push(request);
await save();
signal.throwIfAborted();
const artifact = await deps.capture({
binding: document.binding,
commitId: request.commitId,
confirmationGrantId: input.confirmationGrantId,
signal,
deadlineAt,
});
signal.throwIfAborted();
const checkpoint = await publishSessionCheckpointV1({
objectStore: deps.objects,
compatibilityBundle: artifact,
});
request = {
...request,
phase: 'prepared',
checkpoint,
...(artifact.snapshotCleanup.state === 'pending_recovery'
? { snapshotCleanupPending: true as const }
: {}),
};
document.requests[index] = request;
// On an uncertain journal write, leave recovery to a fresh read. Never
// overwrite a possibly prepared record with a fabricated failure.
await save();
}
if (request.phase === 'committed') {
await deps.objects.assertReadable(request.checkpoint.manifest);
await deps.objects.assertReadable(request.checkpoint.value.compatibilityBundle);
return structuredClone({
requestId: request.requestId,
binding: document.binding,
committed: request.result,
stagingCleanup: request.snapshotCleanupPending ? 'pending_recovery' : 'released',
bundleCleanup: request.bundleCleanupPending ? 'pending_recovery' : 'released',
});
}
if (request.phase !== 'prepared')
throw new Error('Invalid checkpoint publication transition');
signal.throwIfAborted();
// No cancellation point between CAS and receipt persistence: after CAS
// succeeds, reporting cancellation would falsely suggest nothing committed.
let committed: CommittedSessionRevision;
try {
committed =
request.expectedRevision === null
? await deps.repository
.createSession({
sessionId: document.binding.repositorySessionId,
agentId: document.binding.agentId,
checkpoint: request.checkpoint,
})
.catch(async (error: unknown) => {
if (
!(error instanceof SessionRepositoryError) ||
error.code !== 'session_already_exists'
)
throw error;
const existing = await deps.repository.checkoutCurrent(
document.binding.repositorySessionId,
);
if (
existing.agentId !== document.binding.agentId ||
existing.forkedFrom !== undefined ||
existing.lastCommittedActivationId !== undefined ||
JSON.stringify(existing.checkpoint) !== JSON.stringify(request.checkpoint)
)
throw error;
return existing;
})
: await deps.repository.commit({
sessionId: document.binding.repositorySessionId,
expectedRevision: request.expectedRevision,
checkpoint: request.checkpoint,
commitId: request.commitId,
});
} catch (error) {
if (
error instanceof SessionRepositoryError &&
['revision_conflict', 'session_already_exists', 'idempotency_conflict'].includes(
error.code,
)
) {
document.requests[index] = { ...request, phase: 'conflicted' };
await save();
await releaseBundle(index);
}
throw error;
}
const completed: CheckpointPublicationRequest = {
...request,
phase: 'committed',
result: committed,
};
document.requests[index] = completed;
await save();
const bundleCleanup = await releaseBundle(index);
return structuredClone({
requestId: request.requestId,
binding: document.binding,
committed,
stagingCleanup: request.snapshotCleanupPending ? 'pending_recovery' : 'released',
bundleCleanup,
});
});
}
}
function validId(value: unknown): value is string {
return typeof value === 'string' && /^[A-Za-z0-9_-]{1,128}$/u.test(value);
}
function normalizeError(error: unknown): HostCheckpointError {
if (error instanceof HostCheckpointError) return error;
// The generic snapshot layer wraps Host eligibility failures. Preserve their
// bounded public classification without exposing arbitrary provider errors.
if (error instanceof SessionSnapshotError && error.cause instanceof HostCheckpointError)
return error.cause;
if (error instanceof SessionRepositoryError) {
const code = ['revision_conflict', 'session_already_exists', 'idempotency_conflict'].includes(
error.code,
)
? 'conflict'
: 'publication_failed';
return new HostCheckpointError(code, 'Checkpoint Repository rejected publication', {
cause: error,
});
}
if (error instanceof SessionSnapshotError) {
if (error.code === 'snapshot_busy' || error.code === 'session_not_quiescent') {
return new HostCheckpointError('busy', 'Session is not quiescent', { cause: error });
}
}
if (
(error instanceof Error && (error.name === 'AbortError' || error.name === 'TimeoutError')) ||
(error instanceof SessionSnapshotError && error.code === 'snapshot_cancelled')
) {
return new HostCheckpointError('cancelled', 'Checkpoint publication attempt was cancelled', {
cause: error,
});
}
return new HostCheckpointError(
'publication_failed',
'Checkpoint publication failed; retry the same request to reconcile',
{ cause: error },
);
}