Files
vdm/extension/src/shared/protocol/events.ts
T
samiandClaude Sonnet 5 60363a7142 proto: land B4 and B2a — buffer bounds, budget knobs, effective readback (1.1.0)
Minor bump on 1.0.0, per core/docs/buffer-sizing.md.

B4 — bufferBytes bounds corrected in all four locations (DownloadSpec,
TaskDetail, download.update's patch, Settings.connection.bufferBytes): was
4 KiB-8 MiB with no stated default, now 64 KiB-16 MiB with a 1 MiB default.
64 KiB because 4 KiB is smaller than one libcurl HTTP/2 write-callback delivery;
16 MiB because throughput from write size is flat past ~1-4 MiB and past 16 MiB
there is stall-cover left to buy but no memory left to spend it on; 1 MiB
default because it is the only candidate for which docs/04's 60 MB RSS target
actually holds once buffers are counted per segment, not per download.

Two new settings keys: connection.maxTotalBufferBytes (128 MiB default) and
connection.maxActiveSegments (32 default). Without them CORE's clamp — reduce
every live segment's buffer to fit the global cap — has no wire configuration
surface, and "20 active downloads" has no meaning distinct from 160 live TLS
connections.

B2a — TaskDetail.effectiveBufferBytes: what a segment is actually using right
now, after the clamp. Placed on TaskDetail next to bufferBytes, following the
requested/effective pattern ADR 0010 already established for segments. The
download.get fixture now demonstrates a real clamp (16 MiB requested, 4 MiB
effective) rather than a case where the cap happens not to bind.

docs/04-engine-design.md §4 and §8 updated in the same change per CORE's
request and CLAUDE.md rule 5: the RSS target is now stated as conditional on
maxActiveSegments = 32, and the old 4 MiB/64 MiB/256 MiB numbers are corrected
to match the schema. ADR 0012 records the reasoning and explicitly keeps the
60 MB target over CORE's offered 120 MB alternative, with the arithmetic that
makes 60 MB achievable with margin.

Numbered 0012 rather than 0011: DAEMON is independently drafting ADR 0011
(admission control / segment budget split) in a peer session at time of
writing, so 0011 was reserved to avoid a collision.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_012fgjnqFCS5h5L7gZTZo3rV
2026-09-09 23:20:58 +04:00

111 lines
4.0 KiB
TypeScript

// ---------------------------------------------------------------------------
// GENERATED FILE — DO NOT EDIT.
//
// Source: contracts/schema/**
// Generator: contracts/codegen/gen_ts.py
// Contract: v1.1.0
//
// Hand-editing this file is a merge blocker. Fix the schema and regenerate:
// python3 contracts/codegen/gen_ts.py
// Only lane PROTO commits to contracts/.
// ---------------------------------------------------------------------------
import type {
AuthRequiredEvent,
GrabberProgressEvent,
NotifyEvent,
SettingsChangedEvent,
SpeedGlobalEvent,
TaskAddedEvent,
TaskProgressEvent,
TaskRemovedEvent,
TaskStateEvent,
} from './types.js';
/** Payload for each server-to-client notification, keyed by its wire name. */
export interface EventMap {
/**
* A server asked for credentials. The task sits in retry_wait until the client supplies
* them. Credentials travel to the Secret Service, never back through this event and never
* into a log.
*/
"event.auth.required": AuthRequiredEvent;
/**
* Crawl progress for the Site Grabber wizard. done true means the file list in
* grabber.status is final.
*/
"event.grabber.progress": GrabberProgressEvent;
/**
* Something the user should see: a completion, a failure, a queue finishing. The client
* decides between a toast, a tray balloon and a sound; the daemon does not assume a GUI is
* running.
*/
"event.notify": NotifyEvent;
/**
* Settings were written by some client. Carries only the key names; a client re-reads what
* it cares about. The extension watches for capture.* here and re-fetches capture.getRules
* so its rules never lag the daemon's.
*/
"event.settings.changed": SettingsChangedEvent;
/**
* Aggregate throughput for the status bar, the tray tooltip and the extension popup.
* Emitted at 1 Hz even when nothing is active, so a client can tell 'idle' from
* 'disconnected'.
*/
"event.speed.global": SpeedGlobalEvent;
/**
* A task entered the list. summary is always present so a client can insert the row
* without a follow-up download.get.
*/
"event.task.added": TaskAddedEvent;
/**
* Batched byte counters for every active task. Emitted at no more than 4 Hz as one array,
* never one notification per task: at twenty active downloads that is four messages a
* second instead of eighty. Clients apply a row patch and repaint the touched columns;
* rebuilding a model on this event is a bug.
*/
"event.task.progress": TaskProgressEvent;
/** A task left the list. The client deletes the row; there is nothing further to fetch. */
"event.task.removed": TaskRemovedEvent;
/**
* A task changed lifecycle state. Carries the summary so the row can be repainted in full
* without a round trip, and error whenever the new state is failed or retry_wait.
*/
"event.task.state": TaskStateEvent;
}
export type EventName = keyof EventMap;
export type EventPayload<E extends EventName> = EventMap[E];
/**
* Discriminated on `method`: narrowing an incoming notification gives the
* correctly typed params with no cast at the call site.
*/
export type ServerNotification = {
[E in EventName]: { jsonrpc: '2.0'; method: E; params: EventMap[E] };
}[EventName];
export interface EventMeta {
/** Upper bound on emission rate, where the contract sets one. */
readonly maxRateHz: number | null;
}
export const EVENTS: { readonly [E in EventName]: EventMeta } = {
"event.auth.required": { maxRateHz: null },
"event.grabber.progress": { maxRateHz: 4 },
"event.notify": { maxRateHz: null },
"event.settings.changed": { maxRateHz: null },
"event.speed.global": { maxRateHz: 1 },
"event.task.added": { maxRateHz: null },
"event.task.progress": { maxRateHz: 4 },
"event.task.removed": { maxRateHz: null },
"event.task.state": { maxRateHz: null },
} as const;
export const EVENT_NAMES = Object.keys(EVENTS) as EventName[];
export function isEventName(v: unknown): v is EventName {
return typeof v === 'string' && Object.prototype.hasOwnProperty.call(EVENTS, v);
}