-
Notifications
You must be signed in to change notification settings - Fork 452
feat: [ENG-2929] add brv channel core interfaces and enforce server/c… #745
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -88,4 +88,33 @@ export default [ | |
| ], | ||
| }, | ||
| }, | ||
| // Architecture boundary: src/server/core may depend only on abstractions. | ||
| // NOTE: infra is a SIBLING of core under src/server/, so a core→infra relative import | ||
| // (e.g. ../../../infra/foo.js) has NO "server" segment — the sibling-relative ../infra | ||
| // variants below are REQUIRED in addition to the **/server/infra/** form. | ||
| { | ||
| files: ['src/server/core/**/*.ts'], | ||
| rules: { | ||
| 'no-restricted-imports': [ | ||
| 'error', | ||
| { | ||
| patterns: [ | ||
| { | ||
| group: ['**/server/infra/**', '../infra/**', '../../infra/**', '../../../infra/**', '../../../../infra/**'], | ||
| message: | ||
| 'core must not import from server/infra. Depend on an interface in core/interfaces and let infra implement it (dependency inversion).', | ||
| }, | ||
|
Comment on lines
+102
to
+106
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit (defensive): The sibling-relative patterns top out at Cheap fixes (either is fine):
Not blocking — the rule does what it advertises at the present directory depth. Worth a follow-up before the channel subsystem nests deeper. |
||
| { | ||
| group: ['**/oclif/**', '../oclif/**', '../../oclif/**', '../../../oclif/**', '../../../../oclif/**'], | ||
| message: 'core must not import from oclif. Keep CLI wiring out of the domain/application core.', | ||
| }, | ||
| { | ||
| group: ['**/tui/**', '../tui/**', '../../tui/**', '../../../tui/**', '../../../../tui/**'], | ||
| message: 'core must not import from tui. Keep UI out of the domain/application core.', | ||
| }, | ||
| ], | ||
| }, | ||
| ], | ||
| }, | ||
| }, | ||
| ] | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,84 @@ | ||||||||||||||||||||||||||||||||||
| import type {ContentBlock, TurnEvent} from '../../../../shared/types/index.js' | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||
| * Payload-only `TurnEvent`: a variant's fields WITHOUT the base coordination | ||||||||||||||||||||||||||||||||||
| * metadata (`channelId`, `turnId`, `deliveryId`, `memberHandle`, `emittedAt`, | ||||||||||||||||||||||||||||||||||
| * `seq`). A driver is oblivious to channel-side coordinates — the orchestrator | ||||||||||||||||||||||||||||||||||
| * stamps the base fields (including the gap-free monotonic `seq`) as it relays | ||||||||||||||||||||||||||||||||||
| * each payload into the transcript, so correct ordering can only be assigned by | ||||||||||||||||||||||||||||||||||
| * that single writer. | ||||||||||||||||||||||||||||||||||
| * | ||||||||||||||||||||||||||||||||||
| * Derived structurally from {@link TurnEvent} via a distributive conditional so | ||||||||||||||||||||||||||||||||||
| * the two can never drift: adding a new `TurnEvent` variant updates this type | ||||||||||||||||||||||||||||||||||
| * automatically. The omitted keys MUST mirror the `TurnEvent` base shape. | ||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||
| export type TurnEventPayload = TurnEvent extends infer T | ||||||||||||||||||||||||||||||||||
| ? T extends TurnEvent | ||||||||||||||||||||||||||||||||||
| ? Omit<T, 'channelId' | 'deliveryId' | 'emittedAt' | 'memberHandle' | 'seq' | 'turnId'> | ||||||||||||||||||||||||||||||||||
| : never | ||||||||||||||||||||||||||||||||||
| : never | ||||||||||||||||||||||||||||||||||
|
Comment on lines
+3
to
+19
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. suggestion (robustness, non-blocking): The doc comment acknowledges this ("The omitted keys MUST mirror the Not blocking for this PR, but worth a follow-up before there's more than one driver implementing the interface. |
||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| /** Arguments for a single prompt/turn dispatched to a driver. */ | ||||||||||||||||||||||||||||||||||
| export type AgentDriverPromptArgs = { | ||||||||||||||||||||||||||||||||||
| /** Opaque per-turn metadata forwarded to the underlying agent (driver-defined). */ | ||||||||||||||||||||||||||||||||||
| readonly meta?: Record<string, unknown> | ||||||||||||||||||||||||||||||||||
| /** Prompt content blocks for this turn. */ | ||||||||||||||||||||||||||||||||||
| readonly prompt: ContentBlock[] | ||||||||||||||||||||||||||||||||||
| /** Channel turn this dispatch belongs to (correlation only; not echoed in payloads). */ | ||||||||||||||||||||||||||||||||||
| readonly turnId: string | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| /** Lifecycle status of a single driver instance. */ | ||||||||||||||||||||||||||||||||||
| export type AgentDriverStatus = 'errored' | 'idle' | 'stopped' | 'streaming' | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||
| * Transport-agnostic contract for driving one agent and streaming its turn — the | ||||||||||||||||||||||||||||||||||
| * single most important seam in the channel subsystem. A local ACP subprocess | ||||||||||||||||||||||||||||||||||
| * and a remote A2A peer implement THIS SAME interface, so the orchestrator never | ||||||||||||||||||||||||||||||||||
| * knows or cares whether an agent is local or networked. | ||||||||||||||||||||||||||||||||||
| * | ||||||||||||||||||||||||||||||||||
| * Deliberately free of ACP vocabulary: no protocol version, no ACP capability | ||||||||||||||||||||||||||||||||||
| * snapshot, no ACP `initialize` handshake — those belong to the concrete ACP | ||||||||||||||||||||||||||||||||||
| * implementation, not to this contract. | ||||||||||||||||||||||||||||||||||
| * | ||||||||||||||||||||||||||||||||||
| * One instance serves one channel member. Spawn / teardown is the caller's | ||||||||||||||||||||||||||||||||||
| * concern via {@link IAgentDriver.start} / {@link IAgentDriver.stop}. | ||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||
| export interface IAgentDriver { | ||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||
| * Cancels in-flight work. With a `turnId`, cancels just that turn; without, it | ||||||||||||||||||||||||||||||||||
| * cancels whatever is currently streaming. Idempotent; later prompts still work. | ||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||
| cancel(turnId?: string): Promise<void> | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| /** Stable channel-member handle this driver serves (e.g. `@claude`). */ | ||||||||||||||||||||||||||||||||||
| readonly handle: string | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||
| * Dispatches a prompt and stream the turn as it unfolds. Each yielded | ||||||||||||||||||||||||||||||||||
| * {@link TurnEventPayload} is a base-field-free slice; the orchestrator stamps | ||||||||||||||||||||||||||||||||||
| * `channelId` / `turnId` / `deliveryId` / `memberHandle` / `seq` / `emittedAt` | ||||||||||||||||||||||||||||||||||
| * before persisting and broadcasting. The iterator completes when the turn | ||||||||||||||||||||||||||||||||||
| * reaches a terminal state and may throw to signal a driver-level failure. | ||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||
| prompt(args: AgentDriverPromptArgs): AsyncIterableIterator<TurnEventPayload> | ||||||||||||||||||||||||||||||||||
|
Comment on lines
+57
to
+64
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit (style): Per the new CLAUDE.md "third-person singular present-tense" rule, the leading verb is conjugated correctly but the coordinated second verb is not:
Should be "streams" to match "dispatches".
Suggested change
|
||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||
| * Resolves a pending permission request the driver surfaced (via a | ||||||||||||||||||||||||||||||||||
| * `permission_request` payload). `response` is opaque here; the concrete driver | ||||||||||||||||||||||||||||||||||
| * interprets it. | ||||||||||||||||||||||||||||||||||
| * | ||||||||||||||||||||||||||||||||||
| * @param permissionRequestId - Id from the `permission_request` payload. | ||||||||||||||||||||||||||||||||||
| * @param response - Driver-defined decision payload. | ||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||
| respondToPermission(permissionRequestId: string, response: unknown): Promise<void> | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| /** Brings the underlying session up (spawn / connect / handshake). Idempotent. */ | ||||||||||||||||||||||||||||||||||
| start(): Promise<void> | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| /** Current lifecycle status. */ | ||||||||||||||||||||||||||||||||||
| readonly status: AgentDriverStatus | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| /** Tears the session down and release resources. Idempotent. */ | ||||||||||||||||||||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit (style): Same coordinated-verb mismatch as on
Should be "releases" to match "tears".
Suggested change
|
||||||||||||||||||||||||||||||||||
| stop(): Promise<void> | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,22 @@ | ||
| /** | ||
| * Outbound fan-out port used by the channel orchestrator. | ||
| * | ||
| * The orchestrator lives in the core layer and MUST NOT depend on the transport | ||
| * server directly — the transport may later be swapped for a cross-machine | ||
| * relay. The infra adapter binds this port to the real transport | ||
| * server, delegating to `broadcastTo('channel:<channelId>', event, data)`. | ||
| * | ||
| * Fire-and-forget: there is no awaitable delivery guarantee. Subscribers are the | ||
| * clients (TUI / webui / cli) joined to the `channel:<channelId>` room. | ||
| */ | ||
| export interface IChannelBroadcaster { | ||
| /** | ||
| * Emits `event` (with payload `data`) to every client subscribed to | ||
| * `channel:<channelId>`. | ||
| * | ||
| * @param channelId - Channel whose subscribers receive the event. | ||
| * @param event - Transport event name (e.g. `channel:turn-event`). | ||
| * @param data - Event payload; shape is event-specific. | ||
| */ | ||
| broadcastToChannel<T>(channelId: string, event: string, data: T): void | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,58 @@ | ||
| import type {Channel, ContentBlock, Turn} from '../../../../shared/types/index.js' | ||
|
|
||
| /** Cancel an in-flight turn. */ | ||
| export type CancelTurnArgs = { | ||
| readonly channelId: string | ||
| readonly turnId: string | ||
| } | ||
|
|
||
| /** Create a new channel. */ | ||
| export type CreateChannelArgs = { | ||
| readonly channelId: string | ||
| readonly title?: string | ||
| } | ||
|
|
||
| /** Post a new turn (a user or local-agent prompt) into a channel. */ | ||
| export type PostTurnArgs = { | ||
| readonly channelId: string | ||
| /** Optional client-supplied idempotency key for safe retries. */ | ||
| readonly idempotencyKey?: string | ||
| /** Handles explicitly mentioned in the prompt; drives dispatch. */ | ||
| readonly mentions?: string[] | ||
| readonly promptBlocks: ContentBlock[] | ||
| } | ||
|
|
||
| /** | ||
| * Thin application-facing coordinator for the channel subsystem. Deliberately | ||
| * smaller than the POC's god-object: it exposes only the read + lifecycle | ||
| * operations the foundation needs. The interface is EXTENDED (never replaced) as | ||
| * the subsystem grows — member management, streaming dispatch, quorum fan-out, | ||
| * and permission decisions land additively once their domain types and consumers | ||
| * exist. | ||
| * | ||
| * Implementations validate inputs against the transport request schemas before | ||
| * these methods run (the handler does this), so orchestrator methods can trust | ||
| * their arguments. | ||
| */ | ||
| export interface IChannelOrchestrator { | ||
| /** Cancels an in-flight turn and its deliveries. */ | ||
| cancelTurn(args: CancelTurnArgs): Promise<void> | ||
|
|
||
| /** Creates a new channel and returns its record. */ | ||
| createChannel(args: CreateChannelArgs): Promise<Channel> | ||
|
|
||
| /** Reads one channel, or `undefined` when it does not exist. */ | ||
| getChannel(channelId: string): Promise<Channel | undefined> | ||
|
|
||
| /** Reads one turn within a channel, or `undefined` when it does not exist. */ | ||
| getTurn(channelId: string, turnId: string): Promise<Turn | undefined> | ||
|
|
||
| /** Lists all channels. */ | ||
| listChannels(): Promise<Channel[]> | ||
|
|
||
| /** Lists the turns of a channel; ordering is defined by the adapter. */ | ||
| listTurns(channelId: string): Promise<Turn[]> | ||
|
|
||
| /** Posts a new turn into a channel and returns the created turn record. */ | ||
| postTurn(args: PostTurnArgs): Promise<Turn> | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,63 @@ | ||
| import type {Channel, ChannelMember, ChannelSettings} from '../../../../shared/types/index.js' | ||
|
|
||
| /** Add a full member record to a channel. */ | ||
| export type ChannelStoreAddMemberArgs = { | ||
| readonly channelId: string | ||
| readonly member: ChannelMember | ||
| } | ||
|
|
||
| /** Create a new channel record. */ | ||
| export type ChannelStoreCreateArgs = { | ||
| readonly channelId: string | ||
| readonly settings?: ChannelSettings | ||
| readonly title?: string | ||
| } | ||
|
|
||
| /** Remove a member from a channel by handle. */ | ||
| export type ChannelStoreRemoveMemberArgs = { | ||
| readonly channelId: string | ||
| readonly memberHandle: string | ||
| } | ||
|
|
||
| /** Apply a metadata patch to an existing channel (title / settings / archive). */ | ||
| export type ChannelStoreUpdateArgs = { | ||
| readonly archivedAt?: string | ||
| readonly channelId: string | ||
| readonly settings?: ChannelSettings | ||
| readonly title?: string | ||
| } | ||
|
|
||
| /** | ||
| * Persistence port for channel + member METADATA only. It owns the durable | ||
| * {@link Channel} record and the full {@link ChannelMember} records behind it; | ||
| * `Channel.members` remains the summarised projection the adapter derives from | ||
| * those records. | ||
| * | ||
| * It deliberately does NOT store transcripts (turns / events) — that is | ||
| * `ITranscriptStore`'s responsibility. Splitting the two lets transcript | ||
| * retention / GC evolve independently, without touching channel metadata. Member | ||
| * CRUD (`addMember` / `removeMember` / `listMembers`) is the seam invite / | ||
| * uninvite drives. | ||
| */ | ||
| export interface IChannelStore { | ||
| /** Persists a new member record under a channel. */ | ||
| addMember(args: ChannelStoreAddMemberArgs): Promise<void> | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. question (spec gap): This contract is silent on collision semantics.
Each implies a different invite/re-invite flow. Picking one and documenting it here is cheaper than discovering the divergence between two infra adapters later. Same kind of gap on |
||
|
|
||
| /** Creates and persists a new channel; rejects when `channelId` already exists. */ | ||
| createChannel(args: ChannelStoreCreateArgs): Promise<Channel> | ||
|
|
||
| /** Lists all channels (summary view). */ | ||
| listChannels(): Promise<Channel[]> | ||
|
|
||
| /** Lists the full member records of a channel. */ | ||
| listMembers(channelId: string): Promise<ChannelMember[]> | ||
|
|
||
| /** Reads one channel record, or `undefined` when it does not exist. */ | ||
| readChannel(channelId: string): Promise<Channel | undefined> | ||
|
|
||
| /** Removes a member record from a channel by handle. No-op when absent. */ | ||
| removeMember(args: ChannelStoreRemoveMemberArgs): Promise<void> | ||
|
|
||
| /** Applies a metadata patch to an existing channel; returns the updated record. */ | ||
| updateChannel(args: ChannelStoreUpdateArgs): Promise<Channel> | ||
| } | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,46 @@ | ||||||||||||||||||||||||||||||||||
| import type {IAgentDriver} from './i-agent-driver.js' | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| /** Composite key identifying one member's driver slot within a channel. */ | ||||||||||||||||||||||||||||||||||
| export type DriverPoolKey = { | ||||||||||||||||||||||||||||||||||
| readonly channelId: string | ||||||||||||||||||||||||||||||||||
| readonly memberHandle: string | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| /** Look up the driver registered for a `{channelId, memberHandle}`. */ | ||||||||||||||||||||||||||||||||||
| export type DriverPoolAcquireArgs = DriverPoolKey | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| /** Register an already-started driver under its `{channelId, memberHandle}` key. */ | ||||||||||||||||||||||||||||||||||
| export type DriverPoolRegisterArgs = DriverPoolKey & { | ||||||||||||||||||||||||||||||||||
| readonly driver: IAgentDriver | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| /** Release (stop + evict) the driver for a `{channelId, memberHandle}`. */ | ||||||||||||||||||||||||||||||||||
| export type DriverPoolReleaseArgs = DriverPoolKey | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||
| * Keyed registry of live {@link IAgentDriver} instances — one slot per | ||||||||||||||||||||||||||||||||||
| * `{channelId, memberHandle}`. The pool is pure lifecycle bookkeeping: it does | ||||||||||||||||||||||||||||||||||
| * NOT spawn drivers. The orchestrator constructs + starts a driver and hands it | ||||||||||||||||||||||||||||||||||
| * over via {@link IDriverPool.register}; `acquire` is a non-blocking lookup; the | ||||||||||||||||||||||||||||||||||
| * `release*` methods call `driver.stop()` so subprocess agents never leak. | ||||||||||||||||||||||||||||||||||
| * | ||||||||||||||||||||||||||||||||||
| * Pre-warming is intentionally absent: it has no consumer at this layer yet, and | ||||||||||||||||||||||||||||||||||
| * the pool's "never constructs drivers" invariant means warming belongs to the | ||||||||||||||||||||||||||||||||||
| * orchestrator (which owns driver construction) when a consumer needs it. | ||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||
| export interface IDriverPool { | ||||||||||||||||||||||||||||||||||
| /** Returns the registered driver for the key, or `undefined` if none. Never spawns. */ | ||||||||||||||||||||||||||||||||||
| acquire(args: DriverPoolAcquireArgs): IAgentDriver | undefined | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| /** Stores an already-started driver under its key, replacing any prior slot. */ | ||||||||||||||||||||||||||||||||||
| register(args: DriverPoolRegisterArgs): void | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| /** Stops and evict the driver for a single key. No-op when absent. */ | ||||||||||||||||||||||||||||||||||
| release(args: DriverPoolReleaseArgs): Promise<void> | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| /** Stops and evict every driver in the pool (daemon shutdown). */ | ||||||||||||||||||||||||||||||||||
| releaseAll(): Promise<void> | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| /** Stops and evict all drivers belonging to a channel (channel close / archive). */ | ||||||||||||||||||||||||||||||||||
| releaseChannel(channelId: string): Promise<void> | ||||||||||||||||||||||||||||||||||
|
Comment on lines
+38
to
+45
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit (style): Per the new CLAUDE.md rule, doc comments on functions/methods should start with a third-person singular present-tense verb. These three docs start with a third-person verb but then coordinate with a bare imperative (
Suggested change
|
||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,38 @@ | ||
| import type {TurnEvent} from '../../../../shared/types/index.js' | ||
|
|
||
| /** | ||
| * Append one fully-stamped transcript event. Location-agnostic by design: the | ||
| * caller supplies `projectRoot` + `channelId` + `turnId`; the contract says | ||
| * nothing about files, NDJSON, per-turn indexes, or whether storage is | ||
| * per-project vs global. The path / retention policy is the adapter's concern. | ||
| */ | ||
| export type AppendTurnEventArgs = { | ||
| readonly channelId: string | ||
| /** Fully-stamped event — base fields already populated by the orchestrator. */ | ||
| readonly event: TurnEvent | ||
| /** Project the channel lives under; the adapter resolves storage location from it. */ | ||
| readonly projectRoot: string | ||
| readonly turnId: string | ||
| } | ||
|
|
||
| /** Read back every persisted event for one turn, in `seq` order. */ | ||
| export type ReadTurnEventsArgs = { | ||
| readonly channelId: string | ||
| readonly projectRoot: string | ||
| readonly turnId: string | ||
| } | ||
|
|
||
| /** | ||
| * Append-and-read port for a turn's event log. Deliberately minimal: it does NOT | ||
| * model turn / channel metadata (`IChannelStore` owns that), nor does it leak any | ||
| * storage mechanism (no file handles, NDJSON, index, or GC in the contract). | ||
| * Split out from the POC's combined store so transcript retention can evolve | ||
| * without touching channel metadata. | ||
| */ | ||
| export interface ITranscriptStore { | ||
| /** Appends one stamped `TurnEvent` to a turn's log. */ | ||
| appendTurnEvent(args: AppendTurnEventArgs): Promise<void> | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. question (spec gap): The contract doesn't say what
Either is fine, but stating it here is cheap and pins down what an adapter must implement. Same kind of clarity that |
||
|
|
||
| /** Reads all persisted events for a turn, ordered by `seq` ascending. */ | ||
| readTurnEvents(args: ReadTurnEventsArgs): Promise<TurnEvent[]> | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
praise: Strongly agree with formalising the "Linear IDs / milestone labels don't belong in code" guidance — these always rot. The carve-out for
TODO(ENG-1234)against a tracked follow-up is exactly the right shape: it points at the issue tracker as the source of truth instead of trying to mirror it inside the comment.The
TODO(ENG-3034)markers added incurate-prompt-builder.tsandi-curate-executor.tsmodel the exception clause nicely; cleaning up the pre-existingM0-1/Q2/M5+violations insrc/shared/types/channel.ts(which is touched by this PR) would model the rule clause in the same change — see the comment on that file.