Files
goodbuddy/src/agent-daemon/event-journal.ts
T
mesalogo 607bb33249 feat: add managed SSH execution spaces
Add managed SSH Hosts and projects with signed side-by-side GoodBuddy Agent and OpenCode Runtime installation, Ask and Execute workspace access, a Main-owned model bridge, detached Agent lifecycle, durable ACP output recovery, and project activation UI.

Use protocol v2 with bounded FIFO frames over SSH instead of custom credit, priority, batching, or model-message chunking. Mismatched Host Agents now upgrade automatically, an exactly verified Node runtime is reused on-host, and local Portable builds can embed strictly verified Agent artifacts.

Release note: 新增托管 SSH 远程项目,可自动安装或升级签名 Agent/OpenCode Runtime,并在 SSH 断线后继续 Agent 工作和同步未确认输出。
2026-08-24 23:02:18 +08:00

1739 lines
55 KiB
TypeScript

import type { DatabaseSync, StatementSync } from 'node:sqlite'
import {
AGENT_PROTOCOL_LIMITS,
acpFrameDirectionSchema,
agentIdentifierSchema,
agentSequenceSchema,
eventIdentitySchema,
positiveAgentSequenceSchema,
type AcpFrameDirection,
type EventIdentity
} from '../shared/agent-protocol/contracts'
import type { AcpJournalCursor } from '../shared/remote-agent-contracts'
import { openPrivateSqliteDatabase } from '../shared/node/private-sqlite-database'
export type JournalEventClass = 'data' | 'control' | 'terminal'
export type JournalEvent = {
identity: EventIdentity
eventClass: JournalEventClass
payload: Uint8Array
createdAt: number
}
export type EventStreamLimits = {
maximumBytes: number
maximumEvents: number
terminalReserveBytes: number
terminalReserveEvents: number
maximumDurationMs: number
}
export type EventJournalOptions = {
defaultStreamLimits?: Partial<EventStreamLimits>
maximumControllerBytes?: number
maximumDaemonBytes?: number
terminalReserveBytes?: number
minimumUnacknowledgedTerminalRetentionMs?: number
maximumPayloadBytes?: number
maximumAcpChannelBytes?: number
maximumAcpChannelFrames?: number
maximumAcpControllerFrames?: number
maximumAcpDaemonFrames?: number
now?: () => number
}
export class EventJournalCapacityError extends Error {
constructor(
message: string,
readonly level: 'stream' | 'controller' | 'daemon',
readonly terminalWriteAllowed: boolean
) {
super(message)
this.name = 'EventJournalCapacityError'
}
}
export class EventJournalSequenceError extends Error {
constructor(message: string) {
super(message)
this.name = 'EventJournalSequenceError'
}
}
type StreamRow = {
controller_id: string
stream_id: string
stream_epoch: number
last_sequence: number
ack_sequence: number
created_at: number
maximum_bytes: number
maximum_events: number
terminal_reserve_bytes: number
terminal_reserve_events: number
maximum_duration_ms: number
stored_bytes: number
stored_events: number
terminal_sequence: number
}
type EventRow = {
controller_id: string
stream_id: string
stream_epoch: number
sequence: number
event_class: JournalEventClass
payload: Uint8Array
byte_length: number
created_at: number
}
type AcpChannelRow = {
binding_id: string
channel_epoch: number
direction: AcpFrameDirection
journaled_sequence: number
delivered_sequence: number
main_ack_sequence: number
controller_id: string
stored_bytes: number
stored_frames: number
}
type AcpFrameRow = {
sequence: number
payload: Uint8Array
created_at: number
}
const defaultStreamLimits: EventStreamLimits = {
maximumBytes: AGENT_PROTOCOL_LIMITS.runEventJournalBytes,
maximumEvents: AGENT_PROTOCOL_LIMITS.runPendingEvents,
terminalReserveBytes:
AGENT_PROTOCOL_LIMITS.runTerminalReserveBytes,
terminalReserveEvents: 4,
maximumDurationMs: 24 * 60 * 60 * 1000
}
export class EventJournal {
readonly #database: DatabaseSync
readonly #streamLimits: EventStreamLimits
readonly #maximumControllerBytes: number
readonly #maximumDaemonBytes: number
readonly #terminalReserveBytes: number
readonly #minimumTerminalRetentionMs: number
readonly #maximumPayloadBytes: number
readonly #maximumAcpChannelBytes: number
readonly #maximumAcpChannelFrames: number
readonly #maximumAcpControllerFrames: number
readonly #maximumAcpDaemonFrames: number
readonly #now: () => number
readonly #statements: {
getStream: StatementSync
getEvent: StatementSync
insertEvent: StatementSync
updateEventSequence: StatementSync
replayEvents: StatementSync
updateEventAck: StatementSync
streamsToPrune: StatementSync
pruneEventRows: StatementSync
archiveEventStream: StatementSync
retireEventStream: StatementSync
getControllerUsage: StatementSync
getDaemonUsage: StatementSync
getAcpControllerFrames: StatementSync
getAcpDaemonFrames: StatementSync
insertStream: StatementSync
getAcpChannel: StatementSync
getAcpFrame: StatementSync
insertAcpFrame: StatementSync
updateAcpJournaled: StatementSync
replayAcpFrames: StatementSync
updateAcpDelivered: StatementSync
updateAcpMainAck: StatementSync
pruneAcpInbound: StatementSync
pruneAcpOutbound: StatementSync
deleteAcpBindingFrames: StatementSync
archiveAcpInbound: StatementSync
archiveAcpOutbound: StatementSync
retireAcpInbound: StatementSync
retireAcpOutbound: StatementSync
getRetiredStream: StatementSync
getRetiredAcpChannel: StatementSync
getAcpEpochOwner: StatementSync
insertAcpChannel: StatementSync
}
constructor(path: string, options: EventJournalOptions = {}) {
this.#streamLimits = {
...defaultStreamLimits,
...options.defaultStreamLimits
}
this.#maximumControllerBytes =
options.maximumControllerBytes ??
AGENT_PROTOCOL_LIMITS.controllerJournalBytes
this.#maximumDaemonBytes =
options.maximumDaemonBytes ??
AGENT_PROTOCOL_LIMITS.daemonJournalBytes
this.#terminalReserveBytes =
options.terminalReserveBytes ??
Math.min(
AGENT_PROTOCOL_LIMITS.runTerminalReserveBytes,
this.#maximumControllerBytes,
this.#maximumDaemonBytes
)
this.#minimumTerminalRetentionMs =
options.minimumUnacknowledgedTerminalRetentionMs ??
24 * 60 * 60 * 1000
this.#maximumPayloadBytes =
options.maximumPayloadBytes ??
AGENT_PROTOCOL_LIMITS.maximumEventPayloadBytes
this.#maximumAcpChannelBytes =
options.maximumAcpChannelBytes ??
AGENT_PROTOCOL_LIMITS.runEventJournalBytes
this.#maximumAcpChannelFrames =
options.maximumAcpChannelFrames ??
AGENT_PROTOCOL_LIMITS.maximumAcpFramesPerChannel
this.#maximumAcpControllerFrames =
options.maximumAcpControllerFrames ??
AGENT_PROTOCOL_LIMITS.maximumAcpFramesPerController
this.#maximumAcpDaemonFrames =
options.maximumAcpDaemonFrames ??
AGENT_PROTOCOL_LIMITS.maximumAcpFramesPerDaemon
this.#now = options.now ?? Date.now
validateLimits(
this.#streamLimits,
this.#maximumControllerBytes,
this.#maximumDaemonBytes,
this.#terminalReserveBytes
)
if (
!Number.isSafeInteger(this.#minimumTerminalRetentionMs) ||
this.#minimumTerminalRetentionMs < 0 ||
!Number.isSafeInteger(this.#maximumPayloadBytes) ||
this.#maximumPayloadBytes < 0 ||
!Number.isSafeInteger(this.#maximumAcpChannelBytes) ||
this.#maximumAcpChannelBytes < 1 ||
!Number.isSafeInteger(this.#maximumAcpChannelFrames) ||
this.#maximumAcpChannelFrames < 1 ||
!Number.isSafeInteger(this.#maximumAcpControllerFrames) ||
this.#maximumAcpControllerFrames < this.#maximumAcpChannelFrames ||
!Number.isSafeInteger(this.#maximumAcpDaemonFrames) ||
this.#maximumAcpDaemonFrames < this.#maximumAcpControllerFrames
) {
throw new RangeError('Invalid event journal limits')
}
this.#database = openPrivateSqliteDatabase(path)
this.#database.exec(`
PRAGMA journal_mode = WAL;
PRAGMA synchronous = FULL;
PRAGMA busy_timeout = 5000;
PRAGMA foreign_keys = ON;
CREATE TABLE IF NOT EXISTS event_streams (
controller_id TEXT NOT NULL,
stream_id TEXT NOT NULL,
stream_epoch INTEGER NOT NULL CHECK (stream_epoch > 0),
last_sequence INTEGER NOT NULL DEFAULT 0,
ack_sequence INTEGER NOT NULL DEFAULT 0,
created_at INTEGER NOT NULL,
maximum_bytes INTEGER NOT NULL,
maximum_events INTEGER NOT NULL,
terminal_reserve_bytes INTEGER NOT NULL,
terminal_reserve_events INTEGER NOT NULL,
maximum_duration_ms INTEGER NOT NULL,
stored_bytes INTEGER NOT NULL DEFAULT 0
CHECK (stored_bytes >= 0),
stored_events INTEGER NOT NULL DEFAULT 0
CHECK (stored_events >= 0),
terminal_sequence INTEGER NOT NULL DEFAULT 0
CHECK (terminal_sequence >= 0),
PRIMARY KEY (controller_id, stream_id, stream_epoch)
) STRICT;
CREATE TABLE IF NOT EXISTS journal_events (
controller_id TEXT NOT NULL,
stream_id TEXT NOT NULL,
stream_epoch INTEGER NOT NULL,
sequence INTEGER NOT NULL CHECK (sequence > 0),
event_class TEXT NOT NULL CHECK (
event_class IN ('data', 'control', 'terminal')
),
payload BLOB NOT NULL,
byte_length INTEGER NOT NULL CHECK (byte_length >= 0),
created_at INTEGER NOT NULL,
PRIMARY KEY (
controller_id, stream_id, stream_epoch, sequence
),
FOREIGN KEY (controller_id, stream_id, stream_epoch)
REFERENCES event_streams(
controller_id, stream_id, stream_epoch
) ON DELETE CASCADE
) STRICT;
CREATE INDEX IF NOT EXISTS journal_events_replay
ON journal_events(
controller_id, stream_id, stream_epoch, sequence
);
CREATE TABLE IF NOT EXISTS acp_channels (
binding_id TEXT NOT NULL,
channel_epoch INTEGER NOT NULL CHECK (channel_epoch > 0),
direction TEXT NOT NULL CHECK (
direction IN ('main-to-runtime', 'runtime-to-main')
),
controller_id TEXT NOT NULL,
journaled_sequence INTEGER NOT NULL DEFAULT 0,
delivered_sequence INTEGER NOT NULL DEFAULT 0,
main_ack_sequence INTEGER NOT NULL DEFAULT 0,
stored_bytes INTEGER NOT NULL DEFAULT 0
CHECK (stored_bytes >= 0),
stored_frames INTEGER NOT NULL DEFAULT 0
CHECK (stored_frames >= 0),
PRIMARY KEY (binding_id, channel_epoch, direction)
) STRICT;
CREATE TABLE IF NOT EXISTS acp_frames (
binding_id TEXT NOT NULL,
channel_epoch INTEGER NOT NULL,
direction TEXT NOT NULL,
sequence INTEGER NOT NULL CHECK (sequence > 0),
payload BLOB NOT NULL,
byte_length INTEGER NOT NULL CHECK (byte_length >= 0),
created_at INTEGER NOT NULL,
PRIMARY KEY (
binding_id, channel_epoch, direction, sequence
),
FOREIGN KEY (binding_id, channel_epoch, direction)
REFERENCES acp_channels(binding_id, channel_epoch, direction)
ON DELETE CASCADE
) STRICT;
CREATE TABLE IF NOT EXISTS retired_event_streams (
controller_id TEXT NOT NULL,
stream_id TEXT NOT NULL,
stream_epoch INTEGER NOT NULL,
last_sequence INTEGER NOT NULL,
PRIMARY KEY (controller_id, stream_id, stream_epoch)
) STRICT;
CREATE TABLE IF NOT EXISTS retired_acp_channels (
binding_id TEXT NOT NULL,
channel_epoch INTEGER NOT NULL,
direction TEXT NOT NULL,
controller_id TEXT NOT NULL,
journaled_sequence INTEGER NOT NULL,
PRIMARY KEY (binding_id, channel_epoch, direction)
) STRICT;
`)
this.#migrateAndRepairUsage()
this.#statements = {
getStream: this.#database.prepare(
`SELECT * FROM event_streams
WHERE controller_id = ? AND stream_id = ?
AND stream_epoch = ?`
),
getEvent: this.#database.prepare(
`SELECT * FROM journal_events
WHERE controller_id = ? AND stream_id = ?
AND stream_epoch = ? AND sequence = ?`
),
insertEvent: this.#database.prepare(
`INSERT INTO journal_events (
controller_id, stream_id, stream_epoch, sequence,
event_class, payload, byte_length, created_at
) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`
),
updateEventSequence: this.#database.prepare(
`UPDATE event_streams
SET last_sequence = ?,
terminal_sequence = CASE
WHEN ? = 'terminal' THEN ? ELSE terminal_sequence
END
WHERE controller_id = ? AND stream_id = ?
AND stream_epoch = ?`
),
replayEvents: this.#database.prepare(
`SELECT * FROM journal_events
WHERE controller_id = ? AND stream_id = ?
AND stream_epoch = ? AND sequence > ?
ORDER BY sequence ASC LIMIT ?`
),
updateEventAck: this.#database.prepare(
`UPDATE event_streams SET ack_sequence = ?
WHERE controller_id = ? AND stream_id = ?
AND stream_epoch = ?`
),
streamsToPrune: this.#database.prepare(
'SELECT * FROM event_streams WHERE ack_sequence > 0'
),
pruneEventRows: this.#database.prepare(
`DELETE FROM journal_events
WHERE controller_id = ? AND stream_id = ?
AND stream_epoch = ? AND sequence <= ?
AND (event_class != 'terminal' OR created_at <= ?)`
),
archiveEventStream: this.#database.prepare(
`INSERT OR IGNORE INTO retired_event_streams (
controller_id, stream_id, stream_epoch, last_sequence
)
SELECT controller_id, stream_id, stream_epoch, last_sequence
FROM event_streams
WHERE controller_id = ? AND stream_id = ?
AND stream_epoch = ?
AND terminal_sequence > 0
AND ack_sequence >= last_sequence
AND stored_events = 0`
),
retireEventStream: this.#database.prepare(
`DELETE FROM event_streams
WHERE controller_id = ? AND stream_id = ?
AND stream_epoch = ?
AND terminal_sequence > 0
AND ack_sequence >= last_sequence
AND stored_events = 0`
),
getControllerUsage: this.#database.prepare(
`SELECT total_bytes FROM event_controller_usage
WHERE controller_id = ?`
),
getDaemonUsage: this.#database.prepare(
'SELECT total_bytes FROM event_journal_usage WHERE id = 1'
),
getAcpControllerFrames: this.#database.prepare(
`SELECT COALESCE(SUM(stored_frames), 0) AS total_frames
FROM acp_channels WHERE controller_id = ?`
),
getAcpDaemonFrames: this.#database.prepare(
`SELECT COALESCE(SUM(stored_frames), 0) AS total_frames
FROM acp_channels`
),
insertStream: this.#database.prepare(
`INSERT INTO event_streams (
controller_id, stream_id, stream_epoch, created_at,
maximum_bytes, maximum_events, terminal_reserve_bytes,
terminal_reserve_events, maximum_duration_ms
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`
),
getAcpChannel: this.#database.prepare(
`SELECT * FROM acp_channels
WHERE binding_id = ? AND channel_epoch = ? AND direction = ?`
),
getAcpFrame: this.#database.prepare(
`SELECT * FROM acp_frames
WHERE binding_id = ? AND channel_epoch = ?
AND direction = ? AND sequence = ?`
),
insertAcpFrame: this.#database.prepare(
`INSERT INTO acp_frames (
binding_id, channel_epoch, direction, sequence,
payload, byte_length, created_at
) VALUES (?, ?, ?, ?, ?, ?, ?)`
),
updateAcpJournaled: this.#database.prepare(
`UPDATE acp_channels SET journaled_sequence = ?
WHERE binding_id = ? AND channel_epoch = ? AND direction = ?`
),
replayAcpFrames: this.#database.prepare(
`SELECT sequence, payload, created_at FROM acp_frames
WHERE binding_id = ? AND channel_epoch = ?
AND direction = ? AND sequence > ?
ORDER BY sequence ASC LIMIT ?`
),
updateAcpDelivered: this.#database.prepare(
`UPDATE acp_channels SET delivered_sequence = ?
WHERE binding_id = ? AND channel_epoch = ? AND direction = ?`
),
updateAcpMainAck: this.#database.prepare(
`UPDATE acp_channels SET main_ack_sequence = ?
WHERE binding_id = ? AND channel_epoch = ? AND direction = ?`
),
pruneAcpInbound: this.#database.prepare(
`DELETE FROM acp_frames
WHERE binding_id = ? AND channel_epoch = ?
AND direction = 'runtime-to-main' AND sequence <= ?`
),
pruneAcpOutbound: this.#database.prepare(
`DELETE FROM acp_frames
WHERE binding_id = ? AND channel_epoch = ?
AND direction = 'main-to-runtime' AND sequence <= ?`
),
deleteAcpBindingFrames: this.#database.prepare(
`DELETE FROM acp_frames
WHERE binding_id = ? AND channel_epoch = ?`
),
archiveAcpInbound: this.#database.prepare(
`INSERT OR IGNORE INTO retired_acp_channels (
binding_id, channel_epoch, direction, controller_id,
journaled_sequence
)
SELECT binding_id, channel_epoch, direction, controller_id,
journaled_sequence
FROM acp_channels
WHERE binding_id = ? AND channel_epoch = ?
AND direction = 'runtime-to-main'`
),
archiveAcpOutbound: this.#database.prepare(
`INSERT OR IGNORE INTO retired_acp_channels (
binding_id, channel_epoch, direction, controller_id,
journaled_sequence
)
SELECT binding_id, channel_epoch, direction, controller_id,
journaled_sequence
FROM acp_channels
WHERE binding_id = ? AND channel_epoch = ?
AND direction = 'main-to-runtime'`
),
retireAcpInbound: this.#database.prepare(
`DELETE FROM acp_channels
WHERE binding_id = ? AND channel_epoch = ?
AND direction = 'runtime-to-main'`
),
retireAcpOutbound: this.#database.prepare(
`DELETE FROM acp_channels
WHERE binding_id = ? AND channel_epoch = ?
AND direction = 'main-to-runtime'`
),
getRetiredStream: this.#database.prepare(
`SELECT last_sequence FROM retired_event_streams
WHERE controller_id = ? AND stream_id = ?
AND stream_epoch = ?`
),
getRetiredAcpChannel: this.#database.prepare(
`SELECT controller_id, journaled_sequence
FROM retired_acp_channels
WHERE binding_id = ? AND channel_epoch = ? AND direction = ?`
),
getAcpEpochOwner: this.#database.prepare(
`SELECT controller_id, channel_epoch
FROM (
SELECT controller_id, channel_epoch, binding_id
FROM acp_channels
UNION ALL
SELECT controller_id, channel_epoch, binding_id
FROM retired_acp_channels
)
WHERE binding_id = ?
ORDER BY channel_epoch DESC LIMIT 1`
),
insertAcpChannel: this.#database.prepare(
`INSERT INTO acp_channels (
binding_id, channel_epoch, direction, controller_id
) VALUES (?, ?, ?, ?)`
)
}
}
appendEvent(input: {
identity: EventIdentity
eventClass: JournalEventClass
payload: Uint8Array
limits?: Partial<EventStreamLimits>
}): { created: boolean; event: JournalEvent } {
const identity = eventIdentitySchema.parse(input.identity)
validateEventClass(input.eventClass)
this.#assertPayload(input.payload)
return this.#transaction(() => {
const stream = this.#ensureStream(identity, input.limits)
const sequence = sequenceNumber(identity.sequence)
if (sequence <= stream.last_sequence) {
const existing = this.#getEvent(identity)
if (
existing === undefined ||
existing.event_class !== input.eventClass ||
!equalBytes(existing.payload, input.payload)
) {
throw new EventJournalSequenceError(
'Event sequence is already bound to different content'
)
}
return { created: false, event: eventRowToEvent(existing) }
}
if (sequence !== stream.last_sequence + 1) {
throw new EventJournalSequenceError(
'Event sequence must be contiguous'
)
}
const terminal = input.eventClass === 'terminal'
this.#assertEventCapacity(
stream,
input.payload.byteLength,
terminal
)
const now = this.#now()
this.#statements.insertEvent.run(
identity.controllerId,
identity.streamId,
sequenceNumber(identity.streamEpoch),
sequence,
input.eventClass,
input.payload,
input.payload.byteLength,
now
)
this.#statements.updateEventSequence.run(
sequence,
input.eventClass,
sequence,
identity.controllerId,
identity.streamId,
sequenceNumber(identity.streamEpoch)
)
return {
created: true,
event: {
identity,
eventClass: input.eventClass,
payload: input.payload.slice(),
createdAt: now
}
}
})
}
replayEvents(input: {
controllerId: string
streamId: string
streamEpoch: string
afterSequence: string
limit?: number
}): JournalEvent[] {
agentIdentifierSchema.parse(input.controllerId)
agentIdentifierSchema.parse(input.streamId)
positiveAgentSequenceSchema.parse(input.streamEpoch)
agentSequenceSchema.parse(input.afterSequence)
const limit = input.limit ?? AGENT_PROTOCOL_LIMITS.runPendingEvents
if (!Number.isSafeInteger(limit) || limit < 1 || limit > 10_000) {
throw new RangeError('Invalid replay limit')
}
return (
this.#statements.replayEvents.all(
input.controllerId,
input.streamId,
sequenceNumber(input.streamEpoch),
sequenceNumber(input.afterSequence),
limit
) as EventRow[]
).map(eventRowToEvent)
}
acknowledgeEvents(input: {
controllerId: string
streamId: string
streamEpoch: string
sequence: string
}): void {
eventIdentitySchema.parse({
...input,
sequence: input.sequence
})
const changes = this.#transaction(() => {
const stream = this.#getStream(
input.controllerId,
input.streamId,
sequenceNumber(input.streamEpoch)
)
if (stream === undefined) {
const retired = this.#statements.getRetiredStream.get(
input.controllerId,
input.streamId,
sequenceNumber(input.streamEpoch)
) as { last_sequence: number } | undefined
if (
retired !== undefined &&
sequenceNumber(input.sequence) === retired.last_sequence
) {
return 0
}
throw new EventJournalSequenceError('Event stream does not exist')
}
const sequence = sequenceNumber(input.sequence)
if (sequence < stream.ack_sequence) {
throw new EventJournalSequenceError('Event ACK moved backwards')
}
if (sequence > stream.last_sequence) {
throw new EventJournalSequenceError(
'Event ACK exceeds journaled sequence'
)
}
this.#statements.updateEventAck.run(
sequence,
input.controllerId,
input.streamId,
sequenceNumber(input.streamEpoch)
)
return this.#pruneAckedForStream(
input.controllerId,
input.streamId,
sequenceNumber(input.streamEpoch),
sequence
)
})
this.#checkpointAfterSubstantialPrune(changes)
}
pruneAcknowledged(): number {
let changes = 0
this.#transaction(() => {
const streams = this.#statements.streamsToPrune.all() as StreamRow[]
for (const stream of streams) {
changes += this.#pruneAckedForStream(
stream.controller_id,
stream.stream_id,
stream.stream_epoch,
stream.ack_sequence
)
}
})
this.#checkpointAfterSubstantialPrune(changes)
return changes
}
appendAcpFrame(input: {
controllerId: string
bindingId: string
channelEpoch: string
direction: AcpFrameDirection
sequence: string
payload: Uint8Array
}): { created: boolean } {
agentIdentifierSchema.parse(input.controllerId)
agentIdentifierSchema.parse(input.bindingId)
positiveAgentSequenceSchema.parse(input.channelEpoch)
acpFrameDirectionSchema.parse(input.direction)
agentSequenceSchema.parse(input.sequence)
this.#assertPayload(input.payload)
return this.#transaction(() => {
const channel = this.#ensureAcpChannel(input)
const sequence = sequenceNumber(input.sequence)
if (sequence <= channel.journaled_sequence) {
const existing = this.#statements.getAcpFrame.get(
input.bindingId,
sequenceNumber(input.channelEpoch),
input.direction,
sequence
) as AcpFrameRow | undefined
if (
existing === undefined ||
!equalBytes(existing.payload, input.payload)
) {
throw new EventJournalSequenceError(
'ACP sequence is already bound to different content'
)
}
return { created: false }
}
if (sequence !== channel.journaled_sequence + 1) {
throw new EventJournalSequenceError(
'ACP frame sequence must be contiguous'
)
}
this.#assertGlobalCapacity(
input.controllerId,
input.payload.byteLength,
false
)
if (
channel.stored_bytes + input.payload.byteLength >
this.#maximumAcpChannelBytes
) {
throw new EventJournalCapacityError(
'ACP channel journal quota reached',
'stream',
false
)
}
this.#assertAcpFrameCapacity(channel)
this.#statements.insertAcpFrame.run(
input.bindingId,
sequenceNumber(input.channelEpoch),
input.direction,
sequence,
input.payload,
input.payload.byteLength,
this.#now()
)
this.#statements.updateAcpJournaled.run(
sequence,
input.bindingId,
sequenceNumber(input.channelEpoch),
input.direction
)
return { created: true }
})
}
markAcpDelivered(input: {
bindingId: string
channelEpoch: string
sequence: string
}): void {
const changes = this.#advanceAcpCursor({
...input,
direction: 'main-to-runtime',
column: 'delivered_sequence'
})
this.#checkpointAfterSubstantialPrune(changes)
}
acknowledgeAcpFromMain(input: {
bindingId: string
channelEpoch: string
sequence: string
}): void {
agentIdentifierSchema.parse(input.bindingId)
positiveAgentSequenceSchema.parse(input.channelEpoch)
agentSequenceSchema.parse(input.sequence)
const changes = this.#transaction(() => {
const epoch = sequenceNumber(input.channelEpoch)
const channel = this.#getAcpChannel(
input.bindingId,
epoch,
'runtime-to-main'
)
if (channel === undefined) {
const retired = this.#statements.getRetiredAcpChannel.get(
input.bindingId,
epoch,
'runtime-to-main'
) as
| { controller_id: string; journaled_sequence: number }
| undefined
if (
retired !== undefined &&
sequenceNumber(input.sequence) ===
retired.journaled_sequence
) {
return 0
}
throw new EventJournalSequenceError('ACP channel does not exist')
}
const next = sequenceNumber(input.sequence)
this.#assertAcpCursor(channel, next, 'main_ack_sequence')
if (next === channel.main_ack_sequence) {
return 0
}
this.#statements.updateAcpMainAck.run(
next,
input.bindingId,
epoch,
'runtime-to-main'
)
const pruned = Number(
this.#statements.pruneAcpInbound.run(
input.bindingId,
epoch,
next
).changes
)
return pruned
})
this.#checkpointAfterSubstantialPrune(changes)
}
retireAcpBinding(input: {
controllerId: string
bindingId: string
channelEpoch: string
}): number {
agentIdentifierSchema.parse(input.controllerId)
agentIdentifierSchema.parse(input.bindingId)
positiveAgentSequenceSchema.parse(input.channelEpoch)
const changes = this.#transaction(() => {
const epoch = sequenceNumber(input.channelEpoch)
const inbound = this.#ensureAcpRetirementChannel({
...input,
channelEpoch: epoch,
direction: 'runtime-to-main'
})
const outbound = this.#ensureAcpRetirementChannel({
...input,
channelEpoch: epoch,
direction: 'main-to-runtime'
})
const storedFrames =
(inbound?.stored_frames ?? 0) +
(outbound?.stored_frames ?? 0)
this.#statements.archiveAcpInbound.run(input.bindingId, epoch)
this.#statements.archiveAcpOutbound.run(input.bindingId, epoch)
this.#statements.deleteAcpBindingFrames.run(
input.bindingId,
epoch
)
this.#statements.retireAcpInbound.run(input.bindingId, epoch)
this.#statements.retireAcpOutbound.run(input.bindingId, epoch)
return storedFrames
})
this.#checkpointAfterSubstantialPrune(changes)
return changes
}
pruneReconciledAcpOutbound(input: {
bindingId: string
channelEpoch: string
throughSequence: string
}): number {
agentIdentifierSchema.parse(input.bindingId)
positiveAgentSequenceSchema.parse(input.channelEpoch)
agentSequenceSchema.parse(input.throughSequence)
const changes = this.#transaction(() => {
const epoch = sequenceNumber(input.channelEpoch)
const through = sequenceNumber(input.throughSequence)
const channel = this.#getAcpChannel(
input.bindingId,
epoch,
'main-to-runtime'
)
if (
channel === undefined
) {
const retired = this.#statements.getRetiredAcpChannel.get(
input.bindingId,
epoch,
'main-to-runtime'
) as
| { controller_id: string; journaled_sequence: number }
| undefined
if (
retired !== undefined &&
through <= retired.journaled_sequence
) {
return 0
}
throw new EventJournalSequenceError(
'Cannot prune outbound ACP before delivery reconciliation'
)
}
if (through > channel.delivered_sequence) {
throw new EventJournalSequenceError(
'Cannot prune outbound ACP before delivery reconciliation'
)
}
const pruned = Number(
this.#statements.pruneAcpOutbound.run(
input.bindingId,
epoch,
through
).changes
)
return pruned
})
this.#checkpointAfterSubstantialPrune(changes)
return changes
}
replayAcpFrames(input: {
bindingId: string
channelEpoch: string
direction: AcpFrameDirection
afterSequence: string
limit?: number
}): Array<{ sequence: string; payload: Uint8Array; createdAt: number }> {
agentIdentifierSchema.parse(input.bindingId)
positiveAgentSequenceSchema.parse(input.channelEpoch)
acpFrameDirectionSchema.parse(input.direction)
agentSequenceSchema.parse(input.afterSequence)
const limit = input.limit ?? AGENT_PROTOCOL_LIMITS.runPendingEvents
if (!Number.isSafeInteger(limit) || limit < 1 || limit > 10_000) {
throw new RangeError('Invalid replay limit')
}
return (
this.#statements.replayAcpFrames.all(
input.bindingId,
sequenceNumber(input.channelEpoch),
input.direction,
sequenceNumber(input.afterSequence),
limit
) as AcpFrameRow[]
).map((row) => ({
sequence: String(row.sequence),
payload: new Uint8Array(row.payload),
createdAt: row.created_at
}))
}
getAcpCursor(
bindingId: string,
channelEpoch: string,
direction: AcpFrameDirection
): AcpJournalCursor | undefined {
agentIdentifierSchema.parse(bindingId)
positiveAgentSequenceSchema.parse(channelEpoch)
acpFrameDirectionSchema.parse(direction)
const row = this.#getAcpChannel(
bindingId,
sequenceNumber(channelEpoch),
direction
)
if (row === undefined) {
return undefined
}
return {
bindingId,
channelEpoch,
direction,
journaledSequence: String(row.journaled_sequence),
deliveredSequence: String(row.delivered_sequence),
mainAckSequence: String(row.main_ack_sequence)
}
}
close(): void {
this.#database.close()
}
#ensureStream(
identity: EventIdentity,
overrides: Partial<EventStreamLimits> | undefined
): StreamRow {
const epoch = sequenceNumber(identity.streamEpoch)
const existing = this.#getStream(
identity.controllerId,
identity.streamId,
epoch
)
if (existing !== undefined) {
return existing
}
if (
this.#statements.getRetiredStream.get(
identity.controllerId,
identity.streamId,
epoch
) !== undefined
) {
throw new EventJournalSequenceError(
'Event stream has already been terminally acknowledged'
)
}
const limits = { ...this.#streamLimits, ...overrides }
validateStreamLimits(limits)
const now = this.#now()
this.#statements.insertStream.run(
identity.controllerId,
identity.streamId,
epoch,
now,
limits.maximumBytes,
limits.maximumEvents,
limits.terminalReserveBytes,
limits.terminalReserveEvents,
limits.maximumDurationMs
)
return this.#getStream(
identity.controllerId,
identity.streamId,
epoch
)!
}
#getStream(
controllerId: string,
streamId: string,
streamEpoch: number
): StreamRow | undefined {
return this.#statements.getStream.get(
controllerId,
streamId,
streamEpoch
) as
| StreamRow
| undefined
}
#getEvent(identity: EventIdentity): EventRow | undefined {
return this.#statements.getEvent.get(
identity.controllerId,
identity.streamId,
sequenceNumber(identity.streamEpoch),
sequenceNumber(identity.sequence)
) as EventRow | undefined
}
#assertEventCapacity(
stream: StreamRow,
addedBytes: number,
terminal: boolean
): void {
const streamByteLimit = terminal
? stream.maximum_bytes
: stream.maximum_bytes - stream.terminal_reserve_bytes
const streamEventLimit = terminal
? stream.maximum_events
: stream.maximum_events - stream.terminal_reserve_events
if (
stream.stored_events + 1 > streamEventLimit ||
stream.stored_bytes + addedBytes > streamByteLimit ||
(!terminal &&
this.#now() - stream.created_at > stream.maximum_duration_ms)
) {
throw new EventJournalCapacityError(
'Event stream quota reached',
'stream',
stream.stored_bytes + addedBytes <= stream.maximum_bytes &&
stream.stored_events + 1 <= stream.maximum_events
)
}
this.#assertGlobalCapacity(
stream.controller_id,
addedBytes,
terminal
)
}
#assertGlobalCapacity(
controllerId: string,
addedBytes: number,
terminal: boolean
): void {
const controllerBytes = this.#controllerBytes(controllerId)
const daemonBytes = this.#daemonBytes()
const controllerLimit = terminal
? this.#maximumControllerBytes
: this.#maximumControllerBytes - this.#terminalReserveBytes
const daemonLimit = terminal
? this.#maximumDaemonBytes
: this.#maximumDaemonBytes - this.#terminalReserveBytes
if (controllerBytes + addedBytes > controllerLimit) {
throw new EventJournalCapacityError(
'Controller journal quota reached',
'controller',
controllerBytes + addedBytes <= this.#maximumControllerBytes
)
}
if (daemonBytes + addedBytes > daemonLimit) {
throw new EventJournalCapacityError(
'Daemon journal quota reached',
'daemon',
daemonBytes + addedBytes <= this.#maximumDaemonBytes
)
}
}
#assertAcpFrameCapacity(channel: AcpChannelRow): void {
if (channel.stored_frames + 1 > this.#maximumAcpChannelFrames) {
throw new EventJournalCapacityError(
'ACP channel frame quota reached',
'stream',
false
)
}
const controller = this.#statements.getAcpControllerFrames.get(
channel.controller_id
) as { total_frames: number } | undefined
const daemon = this.#statements.getAcpDaemonFrames.get() as
| { total_frames: number }
| undefined
if (
controller === undefined ||
!Number.isSafeInteger(controller.total_frames) ||
controller.total_frames < 0 ||
controller.total_frames + 1 > this.#maximumAcpControllerFrames
) {
throw new EventJournalCapacityError(
'ACP controller frame quota reached',
'controller',
false
)
}
if (
daemon === undefined ||
!Number.isSafeInteger(daemon.total_frames) ||
daemon.total_frames < 0 ||
daemon.total_frames + 1 > this.#maximumAcpDaemonFrames
) {
throw new EventJournalCapacityError(
'ACP daemon frame quota reached',
'daemon',
false
)
}
}
#controllerBytes(controllerId: string): number {
const usage = this.#statements.getControllerUsage.get(
controllerId
) as { total_bytes: number } | undefined
if (
usage === undefined ||
!Number.isSafeInteger(usage.total_bytes) ||
usage.total_bytes < 0
) {
throw new EventJournalCapacityError(
'Controller journal usage is unavailable',
'controller',
false
)
}
return usage.total_bytes
}
#daemonBytes(): number {
const usage = this.#statements.getDaemonUsage.get() as
| { total_bytes: number }
| undefined
if (
usage === undefined ||
!Number.isSafeInteger(usage.total_bytes) ||
usage.total_bytes < 0
) {
throw new EventJournalCapacityError(
'Daemon journal usage is unavailable',
'daemon',
false
)
}
return usage.total_bytes
}
#ensureAcpChannel(input: {
controllerId: string
bindingId: string
channelEpoch: string
direction: AcpFrameDirection
}): AcpChannelRow {
const epoch = sequenceNumber(input.channelEpoch)
const existing = this.#getAcpChannel(
input.bindingId,
epoch,
input.direction
)
if (existing !== undefined) {
if (existing.controller_id !== input.controllerId) {
throw new EventJournalSequenceError(
'ACP channel belongs to another controller'
)
}
return existing
}
if (
this.#statements.getRetiredAcpChannel.get(
input.bindingId,
epoch,
input.direction
) !== undefined
) {
throw new EventJournalSequenceError(
'ACP channel has already been reconciled'
)
}
const epochOwner = this.#statements.getAcpEpochOwner.get(
input.bindingId
) as
| { controller_id: string; channel_epoch: number }
| undefined
if (
epochOwner !== undefined &&
(epoch < epochOwner.channel_epoch ||
epochOwner.controller_id !== input.controllerId)
) {
throw new EventJournalSequenceError(
'ACP channel epoch is obsolete or belongs to another controller'
)
}
this.#statements.insertAcpChannel.run(
input.bindingId,
epoch,
input.direction,
input.controllerId
)
return this.#getAcpChannel(
input.bindingId,
epoch,
input.direction
)!
}
#getAcpChannel(
bindingId: string,
channelEpoch: number,
direction: AcpFrameDirection
): AcpChannelRow | undefined {
return this.#statements.getAcpChannel.get(
bindingId,
channelEpoch,
direction
) as
| AcpChannelRow
| undefined
}
#ensureAcpRetirementChannel(input: {
controllerId: string
bindingId: string
channelEpoch: number
direction: AcpFrameDirection
}): AcpChannelRow | undefined {
let channel = this.#getAcpChannel(
input.bindingId,
input.channelEpoch,
input.direction
)
if (channel === undefined) {
const retired = this.#statements.getRetiredAcpChannel.get(
input.bindingId,
input.channelEpoch,
input.direction
) as
| { controller_id: string; journaled_sequence: number }
| undefined
if (retired !== undefined) {
if (retired.controller_id !== input.controllerId) {
throw new EventJournalSequenceError(
'ACP channel belongs to another controller'
)
}
return undefined
}
channel = this.#ensureAcpChannel({
controllerId: input.controllerId,
bindingId: input.bindingId,
channelEpoch: input.channelEpoch.toString(),
direction: input.direction
})
} else if (channel.controller_id !== input.controllerId) {
throw new EventJournalSequenceError(
'ACP channel belongs to another controller'
)
}
return channel
}
#advanceAcpCursor(input: {
bindingId: string
channelEpoch: string
sequence: string
direction: AcpFrameDirection
column: 'delivered_sequence' | 'main_ack_sequence'
}): number {
agentIdentifierSchema.parse(input.bindingId)
positiveAgentSequenceSchema.parse(input.channelEpoch)
agentSequenceSchema.parse(input.sequence)
return this.#transaction(() => {
const epoch = sequenceNumber(input.channelEpoch)
const channel = this.#getAcpChannel(
input.bindingId,
epoch,
input.direction
)
if (channel === undefined) {
const retired = this.#statements.getRetiredAcpChannel.get(
input.bindingId,
epoch,
input.direction
) as
| { controller_id: string; journaled_sequence: number }
| undefined
if (
retired !== undefined &&
sequenceNumber(input.sequence) ===
retired.journaled_sequence
) {
return 0
}
throw new EventJournalSequenceError('ACP channel does not exist')
}
const next = sequenceNumber(input.sequence)
this.#assertAcpCursor(channel, next, input.column)
if (next !== channel[input.column]) {
const statement =
input.column === 'delivered_sequence'
? this.#statements.updateAcpDelivered
: this.#statements.updateAcpMainAck
statement.run(
next,
input.bindingId,
epoch,
input.direction
)
}
return Number(
this.#statements.pruneAcpOutbound.run(
input.bindingId,
epoch,
next
).changes
)
})
}
#assertAcpCursor(
channel: AcpChannelRow,
next: number,
column: 'delivered_sequence' | 'main_ack_sequence'
): void {
const current = channel[column]
if (next < current) {
throw new EventJournalSequenceError('ACP cursor moved backwards')
}
if (next > channel.journaled_sequence) {
throw new EventJournalSequenceError(
'ACP cursor exceeds journaled sequence'
)
}
if (column === 'delivered_sequence' && next > current + 1) {
throw new EventJournalSequenceError(
'ACP delivery cursor must be contiguous'
)
}
}
#pruneAckedForStream(
controllerId: string,
streamId: string,
streamEpoch: number,
sequence: number
): number {
const cutoff = this.#now() - this.#minimumTerminalRetentionMs
const result = this.#statements.pruneEventRows.run(
controllerId,
streamId,
streamEpoch,
sequence,
cutoff
)
this.#statements.archiveEventStream.run(
controllerId,
streamId,
streamEpoch
)
this.#statements.retireEventStream.run(
controllerId,
streamId,
streamEpoch
)
return Number(result.changes)
}
#migrateAndRepairUsage(): void {
this.#database.exec('BEGIN IMMEDIATE')
try {
const streamColumns = new Set(
(
this.#database
.prepare('PRAGMA table_info(event_streams)')
.all() as Array<{ name: string }>
).map((column) => column.name)
)
if (!streamColumns.has('stored_bytes')) {
this.#database.exec(
`ALTER TABLE event_streams
ADD COLUMN stored_bytes INTEGER NOT NULL DEFAULT 0
CHECK (stored_bytes >= 0)`
)
}
if (!streamColumns.has('stored_events')) {
this.#database.exec(
`ALTER TABLE event_streams
ADD COLUMN stored_events INTEGER NOT NULL DEFAULT 0
CHECK (stored_events >= 0)`
)
}
if (!streamColumns.has('terminal_sequence')) {
this.#database.exec(
`ALTER TABLE event_streams
ADD COLUMN terminal_sequence INTEGER NOT NULL DEFAULT 0
CHECK (terminal_sequence >= 0)`
)
}
const channelColumns = new Set(
(
this.#database
.prepare('PRAGMA table_info(acp_channels)')
.all() as Array<{ name: string }>
).map((column) => column.name)
)
if (!channelColumns.has('stored_bytes')) {
this.#database.exec(
`ALTER TABLE acp_channels
ADD COLUMN stored_bytes INTEGER NOT NULL DEFAULT 0
CHECK (stored_bytes >= 0)`
)
}
if (!channelColumns.has('stored_frames')) {
this.#database.exec(
`ALTER TABLE acp_channels
ADD COLUMN stored_frames INTEGER NOT NULL DEFAULT 0
CHECK (stored_frames >= 0)`
)
}
this.#database.exec(`
CREATE TABLE IF NOT EXISTS event_journal_usage (
id INTEGER PRIMARY KEY CHECK (id = 1),
total_bytes INTEGER NOT NULL CHECK (total_bytes >= 0)
) STRICT;
CREATE TABLE IF NOT EXISTS event_controller_usage (
controller_id TEXT PRIMARY KEY,
total_bytes INTEGER NOT NULL CHECK (total_bytes >= 0)
) STRICT;
INSERT OR IGNORE INTO event_journal_usage (id, total_bytes)
VALUES (1, 0);
DROP TRIGGER IF EXISTS journal_events_usage_insert;
DROP TRIGGER IF EXISTS journal_events_usage_delete;
DROP TRIGGER IF EXISTS acp_frames_usage_insert;
DROP TRIGGER IF EXISTS acp_frames_usage_delete;
DROP TRIGGER IF EXISTS event_stream_controller_usage_insert;
DROP TRIGGER IF EXISTS acp_channel_controller_usage_insert;
CREATE TRIGGER event_stream_controller_usage_insert
AFTER INSERT ON event_streams
BEGIN
INSERT OR IGNORE INTO event_controller_usage (
controller_id, total_bytes
) VALUES (NEW.controller_id, 0);
END;
CREATE TRIGGER acp_channel_controller_usage_insert
AFTER INSERT ON acp_channels
BEGIN
INSERT OR IGNORE INTO event_controller_usage (
controller_id, total_bytes
) VALUES (NEW.controller_id, 0);
END;
CREATE TRIGGER journal_events_usage_insert
AFTER INSERT ON journal_events
BEGIN
UPDATE event_streams
SET stored_bytes = stored_bytes + NEW.byte_length,
stored_events = stored_events + 1
WHERE controller_id = NEW.controller_id
AND stream_id = NEW.stream_id
AND stream_epoch = NEW.stream_epoch;
UPDATE event_journal_usage
SET total_bytes = total_bytes + NEW.byte_length
WHERE id = 1;
INSERT INTO event_controller_usage (
controller_id, total_bytes
) VALUES (NEW.controller_id, NEW.byte_length)
ON CONFLICT(controller_id) DO UPDATE
SET total_bytes = total_bytes + NEW.byte_length;
END;
CREATE TRIGGER journal_events_usage_delete
AFTER DELETE ON journal_events
BEGIN
UPDATE event_streams
SET stored_bytes = stored_bytes - OLD.byte_length,
stored_events = stored_events - 1
WHERE controller_id = OLD.controller_id
AND stream_id = OLD.stream_id
AND stream_epoch = OLD.stream_epoch;
UPDATE event_journal_usage
SET total_bytes = total_bytes - OLD.byte_length
WHERE id = 1;
UPDATE event_controller_usage
SET total_bytes = total_bytes - OLD.byte_length
WHERE controller_id = OLD.controller_id;
END;
CREATE TRIGGER acp_frames_usage_insert
AFTER INSERT ON acp_frames
BEGIN
UPDATE acp_channels
SET stored_bytes = stored_bytes + NEW.byte_length,
stored_frames = stored_frames + 1
WHERE binding_id = NEW.binding_id
AND channel_epoch = NEW.channel_epoch
AND direction = NEW.direction;
UPDATE event_journal_usage
SET total_bytes = total_bytes + NEW.byte_length
WHERE id = 1;
INSERT INTO event_controller_usage (
controller_id, total_bytes
)
SELECT controller_id, NEW.byte_length
FROM acp_channels
WHERE binding_id = NEW.binding_id
AND channel_epoch = NEW.channel_epoch
AND direction = NEW.direction
ON CONFLICT(controller_id) DO UPDATE
SET total_bytes = total_bytes + NEW.byte_length;
END;
CREATE TRIGGER acp_frames_usage_delete
AFTER DELETE ON acp_frames
BEGIN
UPDATE event_journal_usage
SET total_bytes = total_bytes - OLD.byte_length
WHERE id = 1;
UPDATE event_controller_usage
SET total_bytes = total_bytes - OLD.byte_length
WHERE controller_id = (
SELECT controller_id FROM acp_channels
WHERE binding_id = OLD.binding_id
AND channel_epoch = OLD.channel_epoch
AND direction = OLD.direction
);
UPDATE acp_channels
SET stored_bytes = stored_bytes - OLD.byte_length,
stored_frames = stored_frames - 1
WHERE binding_id = OLD.binding_id
AND channel_epoch = OLD.channel_epoch
AND direction = OLD.direction;
END;
UPDATE event_streams
SET stored_bytes = (
SELECT COALESCE(SUM(byte_length), 0)
FROM journal_events
WHERE controller_id = event_streams.controller_id
AND stream_id = event_streams.stream_id
AND stream_epoch = event_streams.stream_epoch
),
stored_events = (
SELECT COUNT(*)
FROM journal_events
WHERE controller_id = event_streams.controller_id
AND stream_id = event_streams.stream_id
AND stream_epoch = event_streams.stream_epoch
),
terminal_sequence = COALESCE((
SELECT MAX(sequence)
FROM journal_events
WHERE controller_id = event_streams.controller_id
AND stream_id = event_streams.stream_id
AND stream_epoch = event_streams.stream_epoch
AND event_class = 'terminal'
), terminal_sequence);
UPDATE acp_channels
SET stored_bytes = (
SELECT COALESCE(SUM(byte_length), 0)
FROM acp_frames
WHERE binding_id = acp_channels.binding_id
AND channel_epoch = acp_channels.channel_epoch
AND direction = acp_channels.direction
),
stored_frames = (
SELECT COUNT(*)
FROM acp_frames
WHERE binding_id = acp_channels.binding_id
AND channel_epoch = acp_channels.channel_epoch
AND direction = acp_channels.direction
);
DELETE FROM event_controller_usage;
INSERT INTO event_controller_usage (controller_id, total_bytes)
SELECT controller_id, SUM(bytes)
FROM (
SELECT controller_id, SUM(byte_length) AS bytes
FROM journal_events GROUP BY controller_id
UNION ALL
SELECT channel.controller_id, SUM(frame.byte_length) AS bytes
FROM acp_frames frame
JOIN acp_channels channel
ON channel.binding_id = frame.binding_id
AND channel.channel_epoch = frame.channel_epoch
AND channel.direction = frame.direction
GROUP BY channel.controller_id
)
GROUP BY controller_id;
INSERT OR IGNORE INTO event_controller_usage (
controller_id, total_bytes
)
SELECT controller_id, 0 FROM event_streams
UNION
SELECT controller_id, 0 FROM acp_channels;
UPDATE event_journal_usage
SET total_bytes =
(SELECT COALESCE(SUM(total_bytes), 0)
FROM event_controller_usage)
WHERE id = 1;
`)
this.#database.exec('COMMIT')
} catch (error) {
this.#database.exec('ROLLBACK')
throw error
}
}
#checkpointAfterSubstantialPrune(changes: number): void {
if (changes >= 100) {
this.#database.exec('PRAGMA wal_checkpoint(PASSIVE)')
}
}
#assertPayload(payload: Uint8Array): void {
if (payload.byteLength > this.#maximumPayloadBytes) {
throw new EventJournalCapacityError(
'Journal payload is oversized',
'stream',
false
)
}
}
#transaction<T>(action: () => T): T {
this.#database.exec('BEGIN IMMEDIATE')
try {
const result = action()
this.#database.exec('COMMIT')
return result
} catch (error) {
this.#database.exec('ROLLBACK')
throw error
}
}
}
function eventRowToEvent(row: EventRow): JournalEvent {
return {
identity: {
controllerId: row.controller_id,
streamId: row.stream_id,
streamEpoch: String(row.stream_epoch),
sequence: String(row.sequence)
},
eventClass: row.event_class,
payload: new Uint8Array(row.payload),
createdAt: row.created_at
}
}
function sequenceNumber(sequence: string): number {
return Number(agentSequenceSchema.parse(sequence))
}
function equalBytes(left: Uint8Array, right: Uint8Array): boolean {
if (left.byteLength !== right.byteLength) {
return false
}
return left.every((byte, index) => byte === right[index])
}
function validateEventClass(
value: string
): asserts value is JournalEventClass {
if (value !== 'data' && value !== 'control' && value !== 'terminal') {
throw new TypeError('Invalid journal event class')
}
}
function validateLimits(
stream: EventStreamLimits,
controllerBytes: number,
daemonBytes: number,
terminalReserveBytes: number
): void {
validateStreamLimits(stream)
if (
!Number.isSafeInteger(controllerBytes) ||
!Number.isSafeInteger(daemonBytes) ||
controllerBytes < 1 ||
daemonBytes < controllerBytes ||
!Number.isSafeInteger(terminalReserveBytes) ||
terminalReserveBytes < 0 ||
terminalReserveBytes > controllerBytes
) {
throw new RangeError('Invalid event journal limits')
}
}
function validateStreamLimits(limits: EventStreamLimits): void {
if (
!Number.isSafeInteger(limits.maximumBytes) ||
limits.maximumBytes < 1 ||
!Number.isSafeInteger(limits.maximumEvents) ||
limits.maximumEvents < 1 ||
!Number.isSafeInteger(limits.terminalReserveBytes) ||
limits.terminalReserveBytes < 0 ||
limits.terminalReserveBytes > limits.maximumBytes ||
!Number.isSafeInteger(limits.terminalReserveEvents) ||
limits.terminalReserveEvents < 1 ||
limits.terminalReserveEvents > limits.maximumEvents ||
!Number.isSafeInteger(limits.maximumDurationMs) ||
limits.maximumDurationMs < 1
) {
throw new RangeError('Invalid event stream limits')
}
}