mirror of
https://github.com/deepseek-ai/deepseek-harness.git
synced 2026-09-11 04:00:38 +00:00
# Conflicts: # .agents/notes/archived/architecture/2026-06-18-shared-persistence-write-coordinator.i18n.yaml # .agents/notes/implemented/architecture/2026-06-14-session-persistence.i18n.yaml # .agents/notes/implemented/architecture/2026-06-14-session-persistence.md # .agents/notes/implemented/architecture/2026-06-14-session-persistence.zh.md # .agents/notes/implemented/architecture/2026-07-24-project-session-directories.i18n.yaml # .agents/notes/implemented/architecture/2026-07-24-project-session-directories.md # .agents/notes/implemented/architecture/2026-07-24-project-session-directories.zh.md # .agents/notes/implemented/architecture/2026-08-05-session-preparation.i18n.yaml # .agents/notes/implemented/architecture/2026-08-05-session-preparation.md # .agents/notes/implemented/architecture/2026-08-05-session-preparation.zh.md # .agents/notes/implemented/architecture/2026-08-10-message-feedback-sidecar.i18n.yaml # .agents/notes/implemented/architecture/2026-08-10-message-feedback-sidecar.md # .agents/notes/implemented/architecture/2026-08-10-message-feedback-sidecar.zh.md # .agents/notes/implemented/architecture/2026-08-27-handle-based-session-persistence.i18n.yaml # .agents/notes/implemented/bug-fix/2026-07-28-load-pre-identity-session-messages.md # .agents/notes/implemented/bug-fix/2026-07-28-load-pre-identity-session-messages.zh.md # .agents/notes/implemented/bug-fix/2026-08-04-load-pre-react-loop-sessions.md # .agents/notes/implemented/bug-fix/2026-08-04-load-pre-react-loop-sessions.zh.md # .agents/notes/implemented/bug-fix/2026-09-01-host-goal-pause-aborts-turn.i18n.yaml # .agents/notes/implemented/feature/2026-08-10-web-session-log-export.i18n.yaml # .agents/notes/implemented/feature/2026-08-10-web-session-log-export.md # .agents/notes/implemented/feature/2026-08-10-web-session-log-export.zh.md # apps/cli/tests/profiles/headless/tests/session-format-guard.expected.e2e.ts # apps/web/tests/cold-blank-session.e2e.ts # apps/web/tests/message-actions.e2e.ts # apps/web/tests/scaffold.ts # apps/web/tests/workspace-management.e2e.ts # docs/config-catalog.i18n.yaml # docs/config-catalog.md # docs/config-catalog.zh.md # docs/event-producer-consumer.i18n.yaml # docs/event-producer-consumer.md # docs/event-producer-consumer.zh.md # docs/persistence-catalog.i18n.yaml # docs/persistence-catalog.md # docs/subsystems/feedback.i18n.yaml # docs/subsystems/feedback.md # docs/subsystems/feedback.zh.md # docs/subsystems/persistence.i18n.yaml # docs/subsystems/persistence.md # docs/subsystems/persistence.zh.md # packages/acp/acp/src/index.ts # packages/acp/acp/tests/bridge.spec.ts # packages/api/session-controller/src/list.ts # packages/api/session-controller/tests/agent.host.spec.ts # packages/api/session-controller/tests/session-cold.host.spec.ts # packages/api/session-controller/tests/session-projections.host.spec.ts # packages/api/session-controller/tests/session-search.host.spec.ts # packages/api/session-controller/tests/test-remote.ts # packages/api/session-controller/tests/transport.host.spec.ts # packages/core/agent-loop/src/index.ts # packages/core/agent-loop/tests/config-session-id.spec.ts # packages/core/agent-loop/tests/resume.spec.ts # packages/extensions/tool-cordis/src/api-catalog.ts # packages/feedback/message-feedback/src/index.ts # packages/feedback/message-feedback/tests/helpers.ts # packages/feedback/message-feedback/tests/message-feedback.spec.ts # packages/session-query/session-log-export/src/archive.ts # packages/session-query/session-log-export/tests/archive.host.spec.ts # packages/session-query/session-log-export/tests/route.host.spec.ts # packages/session-query/session-query-sqlite/README.i18n.yaml # packages/session-query/session-query-sqlite/README.md # packages/session-query/session-query-sqlite/README.zh.md # packages/session-query/session-query-sqlite/src/index.ts # packages/session-query/session-query-sqlite/tests/sqlite.spec.ts # packages/session-query/session-query/README.i18n.yaml # packages/session-query/session-query/README.md # packages/session-query/session-query/README.zh.md # packages/session-query/session-query/src/corpus.ts # packages/session-query/session-query/tests/observation.spec.ts # packages/session-query/session-query/tests/session-query.spec.ts # packages/session-query/session-query/tests/tracing.spec.ts # packages/session/session-checkpoint-policy/tests/session-checkpoint-policy.spec.ts # packages/session/session-persistence-jsonl/README.i18n.yaml # packages/session/session-persistence-jsonl/README.md # packages/session/session-persistence-jsonl/README.zh.md # packages/session/session-persistence-jsonl/src/index.ts # packages/session/session-persistence-jsonl/tests/jsonl.spec.ts # packages/session/session-persistence-jsonl/tests/zstd.spec.ts # packages/session/session-persistence/README.i18n.yaml # packages/session/session-persistence/README.md # packages/session/session-persistence/README.zh.md # packages/session/session-persistence/src/coordinator.ts # packages/session/session-persistence/src/index.ts # packages/session/session-persistence/tests/contract.ts # packages/session/session-persistence/tests/coordinator-contract.ts # packages/session/session-persistence/tests/persistence.spec.ts # packages/session/session-telemetry/package.json # packages/shell/tool-bash/tests/tools.spec.ts # packages/subagent/subagent/src/continuation.ts # packages/subagent/subagent/tests/list-children.spec.ts # packages/workspace/workspace/src/index.ts # packages/workspace/workspace/tests/workspace.spec.ts # scripts/gen-cordis-catalog.ts
152 lines
6.1 KiB
TypeScript
152 lines
6.1 KiB
TypeScript
/**
|
|
* Backend-shared storage validation: the version gate, the fail-closed event
|
|
* vocabulary, append-batch materialization, and contiguity — one place so
|
|
* every backend refuses the same inputs identically.
|
|
* @module @deepseek-ai/dsh-session-persistence/storage-contract
|
|
*/
|
|
|
|
import {
|
|
adoptSessionEvent,
|
|
KNOWN_SESSION_EVENT_TYPES,
|
|
SESSION_FORMAT_VERSION,
|
|
} from '@deepseek-ai/dsh-session'
|
|
import { snapshotJsonValue } from '@deepseek-ai/dsh-util-values'
|
|
import type { SessionEvent, SessionHeader, SessionId } from '@deepseek-ai/dsh-session'
|
|
import {
|
|
SessionFormatUnsupportedError,
|
|
SessionPersistenceCorruptionError,
|
|
sessionFormatVersionRefusal,
|
|
type SessionLocation,
|
|
} from './errors.ts'
|
|
|
|
/** Build a format refusal that points at the raw artifact when the backend has one. */
|
|
function unsupported(reason: string, location: SessionLocation | undefined): SessionFormatUnsupportedError {
|
|
return new SessionFormatUnsupportedError(
|
|
location === undefined ? reason : `${reason} (raw log: ${location.path})`,
|
|
location,
|
|
)
|
|
}
|
|
|
|
/**
|
|
* Refuse stored metadata that is not bound to the requested session id.
|
|
* @param id - the requested session id.
|
|
* @param meta - the stored header.
|
|
*/
|
|
export function assertStoredId(id: SessionId, meta: SessionHeader): void {
|
|
if (meta.id !== id) {
|
|
throw new Error(`stored session identity mismatch: requested "${id}", header contains "${meta.id}"`)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Refuse a stored header whose format version this build does not read.
|
|
* @param meta - the stored header.
|
|
* @param location - the backend's artifact location for the refusal, when one exists.
|
|
*/
|
|
export function assertVersion(
|
|
meta: { readonly id: SessionId; readonly version: number },
|
|
location?: SessionLocation,
|
|
): void {
|
|
if (meta.version !== SESSION_FORMAT_VERSION) {
|
|
throw unsupported(sessionFormatVersionRefusal(meta.id, meta.version), location)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Validate one exclusively owned stored event array in place: adopt each
|
|
* record (validating and freezing it) and refuse any event type this build
|
|
* does not know, unless its writer marked it `ignorable: true` — silently
|
|
* skipping an unknown required event could reconstruct a wrong session (the
|
|
* envelope contract on `SessionEvent.ignorable`). Both newer vocabularies and
|
|
* retired pre-release shapes refuse here; this build ships no migration.
|
|
* @param meta - the stored header the events belong to.
|
|
* @param events - exclusively owned decoded events; validated in place.
|
|
* @param location - the backend's artifact location for refusals, when one exists.
|
|
* @returns the same array, validated and frozen.
|
|
* @throws {SessionFormatUnsupportedError} for unknown event types.
|
|
* @throws {SessionPersistenceCorruptionError} for records that fail validation.
|
|
*/
|
|
export function validateStoredEvents(
|
|
meta: SessionHeader,
|
|
events: SessionEvent[],
|
|
location?: SessionLocation,
|
|
): SessionEvent[] {
|
|
for (const event of events) {
|
|
if (!KNOWN_SESSION_EVENT_TYPES.has(event.type) && event.ignorable !== true) {
|
|
throw unsupported(
|
|
`session "${meta.id}" contains event type "${event.type}" (seq ${event.seq}) unknown to this harness and not marked ignorable; refusing to interpret the log — it was likely written by a newer harness`,
|
|
location,
|
|
)
|
|
}
|
|
// The one retired shape hiding under a known type: the removed delta codec's
|
|
// full-header "fallback" reason. Everything else retired was a whole type.
|
|
if (event.type === 'request/header') {
|
|
const data: unknown = event.data
|
|
if (typeof data === 'object' && data !== null
|
|
&& (data as Record<string, unknown>)['reason'] === 'fallback') {
|
|
throw unsupported(
|
|
`session "${meta.id}" contains a request/header event (seq ${event.seq}) with the unsupported legacy reason "fallback"; refusing to interpret the log — it was written by a retired pre-release harness`,
|
|
location,
|
|
)
|
|
}
|
|
}
|
|
}
|
|
try {
|
|
for (const [index, event] of events.entries()) events[index] = adoptSessionEvent(event)
|
|
} catch (error: unknown) {
|
|
if (error instanceof SessionFormatUnsupportedError) throw error
|
|
throw new SessionPersistenceCorruptionError(
|
|
`stored session "${meta.id}" failed validation: ${String(error)}`,
|
|
{ cause: error },
|
|
)
|
|
}
|
|
return events
|
|
}
|
|
|
|
/**
|
|
* Validate and deep-snapshot a header passed to `create` in one traversal.
|
|
* @param header - the caller's header.
|
|
* @returns the detached lossless-JSON header.
|
|
* @throws {TypeError} for non-JSON metadata or an invalid `createdAt`.
|
|
*/
|
|
export function materializeCreateHeader(header: SessionHeader): SessionHeader {
|
|
const snapshot = snapshotJsonValue(header)
|
|
if (snapshot === undefined) {
|
|
throw new TypeError('session metadata must be losslessly JSON-serializable')
|
|
}
|
|
if (!Number.isSafeInteger(snapshot.createdAt) || snapshot.createdAt < 0) {
|
|
throw new TypeError('session metadata createdAt must be a non-negative safe integer')
|
|
}
|
|
return snapshot
|
|
}
|
|
|
|
/**
|
|
* Validate and deep-snapshot one append batch in a single traversal, so the
|
|
* checked value is exactly the value persisted (a check followed by a copy
|
|
* could reread accessors into a different record).
|
|
* @param events - the caller's batch.
|
|
* @returns the detached lossless-JSON batch.
|
|
* @throws {TypeError} when any event data is not losslessly JSON-serializable.
|
|
*/
|
|
export function materializeAppendBatch(events: readonly SessionEvent[]): readonly SessionEvent[] {
|
|
const batch = snapshotJsonValue(events)
|
|
if (batch === undefined) {
|
|
throw new TypeError('session event batch is not losslessly JSON-serializable because it contains non-JSON-serializable data')
|
|
}
|
|
return batch
|
|
}
|
|
|
|
/**
|
|
* Refuse a batch that does not contiguously continue the stored log.
|
|
* @param id - the session the batch belongs to.
|
|
* @param events - the batch, in seq order.
|
|
* @param cursor - the stored next-seq.
|
|
*/
|
|
export function assertContiguous(id: SessionId, events: readonly SessionEvent[], cursor: number): void {
|
|
for (const [index, event] of events.entries()) {
|
|
if (event.seq !== cursor + index) {
|
|
throw new Error(`append seq mismatch for "${id}": expected ${cursor + index} at index ${index}, got ${event.seq}`)
|
|
}
|
|
}
|
|
}
|