@adhdev/mesh-shared 1.0.60-rc.70 → 1.0.60-rc.72

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,10 +1 @@
1
- /**
2
- * Pure {{key}} template interpolation shared by the mesh command paths.
3
- *
4
- * Hoisted from packages/daemon-cloud/src/mesh/mesh-interpolation.ts — the
5
- * substitution is transport-agnostic (plain string/object work), so it belongs
6
- * in the dependency-free mesh-shared leaf where standalone can reuse it too.
7
- * The substitution semantics are byte-for-byte identical to the prior cloud copy.
8
- */
9
- export declare function interpolateArgs(args: Record<string, unknown>, context: Record<string, unknown>): Record<string, unknown>;
10
1
  export declare function interpolateString(template: string, ctx: Record<string, unknown>): string;
@@ -1,32 +1,40 @@
1
1
  /**
2
- * Keyed document delta — the diff/fold pair behind the `mesh.status` lane
3
- * (the dashboard's mesh view; data-path audit 2026-09-29 P1-5).
2
+ * Keyed document delta — THE diff/fold engine behind every keyed dashboard
3
+ * lane: `daemon.metadata` (the daemon's state, sessions keyed by id) and
4
+ * `mesh.status` (the coordinator's mesh view, nodes / queue tasks / missions
5
+ * keyed) — data-path audit 2026-09-29 P1-4 (one engine, one spec per lane).
4
6
  *
5
- * A document is plain top-level fields plus named keyed COLLECTIONS (arrays of
6
- * rows identified by an id field, possibly nested one object deep, e.g.
7
- * `queue.tasks`). The daemon digests every body it would have sent, diffs it
8
- * against the digest of the last body DELIVERED to a subscription, and sends
9
- * only the difference: changed top-level fields, and per collection the rows
10
- * whose content changed (changed fields only), the fields a row lost, and the
11
- * rows that disappeared. An unchanged document diffs to `null` — zero bytes on
12
- * the wire. The dashboard folds each delta into its held snapshot; the fold of
13
- * every delta since a snapshot equals the latest snapshot (pinned by
14
- * keyed-doc-delta.test.ts).
7
+ * A document is plain top-level fields, optional SPLIT objects (an object whose
8
+ * fields are diffed one by one, e.g. daemon.metadata's `status`), and named
9
+ * keyed COLLECTIONS (arrays of rows identified by an id field, possibly nested
10
+ * one object deep, e.g. `queue.tasks` / `status.sessions`). The daemon digests
11
+ * every body it would have sent, diffs it against the digest of the last body
12
+ * DELIVERED to a subscription, and sends only the difference: changed fields,
13
+ * and per collection the rows whose content changed (changed fields only), the
14
+ * fields a row lost, and the rows that disappeared. An unchanged document
15
+ * diffs to `null` — zero bytes on the wire. The dashboard folds each delta
16
+ * into its held snapshot; the fold of every delta since a snapshot equals the
17
+ * latest snapshot apart from `ignore`d fields (pinned by keyed-doc-delta.test.ts).
15
18
  *
16
- * `volatileTopLevel` fields (a build stamp, live process counters) are not a
17
- * change by themselves; when something else changed they ride along.
19
+ * Volatile fields (a build stamp, live process counters, a row's per-build
20
+ * stamps via `stableRow`) are not a change by themselves; when something else
21
+ * changed they ride along.
18
22
  *
19
23
  * Pure leaf: plain objects in, plain objects out; no transport, no clock.
20
24
  */
21
25
  export interface KeyedDocSpec {
22
- /** Collection path (`nodes`, `queue.tasks`) → the id field of its rows. */
26
+ /** Collection path (`nodes`, `queue.tasks`, `status.sessions`) → the id field of its rows. */
23
27
  collections: Readonly<Record<string, string>>;
24
- /** Top-level fields that change on every build without meaning anything. */
28
+ /** Top-level objects whose own fields are diffed one by one (the rest of the object is not re-sent). */
29
+ splitObjects?: readonly string[];
30
+ /** Field paths (`daemonId`, `status.timestamp`) that are never diffed — envelope identity / clock. */
31
+ ignore?: readonly string[];
32
+ /** Field paths (top-level or inside a split object) that change on every build without meaning anything. */
25
33
  volatileTopLevel?: readonly string[];
26
34
  /**
27
35
  * The part of a collection row that decides whether it CHANGED (drop the
28
36
  * per-build stamps inside it). A row whose stable view is unchanged sends
29
- * nothing; a changed field is sent with its full current value, stamps
37
+ * nothing; a changed row sends every field whose value differs, stamps
30
38
  * included. Must not mutate `row`.
31
39
  */
32
40
  stableRow?: (collection: string, row: Record<string, unknown>) => Record<string, unknown>;
@@ -44,6 +52,7 @@ interface CollectionDigest {
44
52
  readonly order: readonly string[];
45
53
  }
46
54
  export interface KeyedDocDigest {
55
+ /** Field signatures by path (`field`, or `object.field` for a split object). */
47
56
  readonly top: ReadonlyMap<string, string>;
48
57
  readonly topValues: Readonly<Record<string, unknown>>;
49
58
  readonly collections: ReadonlyMap<string, CollectionDigest>;
@@ -62,6 +71,11 @@ export interface KeyedCollectionDelta {
62
71
  export interface KeyedDocDelta {
63
72
  set?: Record<string, unknown>;
64
73
  unset?: string[];
74
+ /** Per split object: its changed / lost fields. */
75
+ objects?: Record<string, {
76
+ set?: Record<string, unknown>;
77
+ unset?: string[];
78
+ }>;
65
79
  collections?: Record<string, KeyedCollectionDelta>;
66
80
  }
67
81
  /** Digest one document. Cheap to hold: values are referenced, not copied. */
@@ -73,6 +87,14 @@ export declare function digestKeyedDoc(doc: Record<string, unknown>, spec: Keyed
73
87
  export declare function diffKeyedDoc(prev: KeyedDocDigest, next: KeyedDocDigest, spec: KeyedDocSpec): KeyedDocDelta | null;
74
88
  /** Rebuild the full document from a held one plus a delta. Never mutates `held`. */
75
89
  export declare function foldKeyedDoc<T extends Record<string, unknown>>(held: T, delta: KeyedDocDelta, spec: KeyedDocSpec): T;
90
+ /**
91
+ * The `daemon.metadata` document (the snapshot body minus its envelope):
92
+ * `status` is split field by field, its sessions keyed by `id`. `daemonId` is
93
+ * envelope identity and `status.timestamp` the build clock — never diffed (the
94
+ * fold re-stamps them from the frame); a session's `lastUpdated` build stamp is
95
+ * not a change by itself.
96
+ */
97
+ export declare const DAEMON_METADATA_DOC_SPEC: KeyedDocSpec;
76
98
  /**
77
99
  * The `mesh.status` document: nodes keyed by `nodeId`, queue tasks and missions
78
100
  * by `id`. `refreshedAt` and `sourceOfTruth` (its aggregate-cache age /
@@ -54,11 +54,8 @@ export declare const isMeshTaskStatus: (value: unknown) => value is "failed" | "
54
54
  export declare const isMeshTerminalTaskStatus: (value: unknown) => value is "failed" | "completed" | "cancelled";
55
55
  export declare const isMeshTaskMode: (value: unknown) => value is "code_change" | "validation" | "live_debug_readonly" | "launch_app" | "convergence";
56
56
  export declare const isMeshTaskPriority: (value: unknown) => value is "low" | "high" | "normal";
57
- export declare const isMeshThinkingLevel: (value: unknown) => value is "medium" | "low" | "high";
58
57
  export declare const isMeshDeliveryMode: (value: unknown) => value is "when_idle" | "interrupt";
59
- export declare const isMeshSessionCleanupMode: (value: unknown) => value is "preserve" | "stop" | "delete_stopped" | "stop_and_delete";
60
58
  export declare const isWorkerReportOutcome: (value: unknown) => value is "failed" | "blocked" | "completed";
61
- export declare const isWorkerBranchState: (value: unknown) => value is "merged_to_main" | "pushed_feature_branch_needs_merge" | "blocked_review" | "cleanup_candidate" | "not_mergeable";
62
59
  /**
63
60
  * JSON-schema fragment for a string enum, derived from the tuple. Use this in
64
61
  * every MCP tool schema instead of spelling the member list again.
@@ -32,9 +32,9 @@ import { type CommandPayload, type Envelope, type MessageId } from './envelope';
32
32
  * time (UserSession.ts), so adding a member here without adding it there — or
33
33
  * vice versa — fails `tsc`.
34
34
  */
35
- export declare const DAEMON_STATUS_EVENT_NAMES: readonly ["agent:generating_started", "agent:waiting_approval", "agent:waiting_choice", "agent:generating_completed", "agent:stopped", "monitor:no_progress", "monitor:long_generating"];
35
+ export declare const DAEMON_STATUS_EVENT_NAMES: readonly ["agent:generating_started", "agent:waiting_approval", "agent:waiting_choice", "agent:generating_completed", "agent:stopped", "monitor:no_progress"];
36
36
  export type DaemonStatusEventName = typeof DAEMON_STATUS_EVENT_NAMES[number];
37
- export declare const isDaemonStatusEventName: (value: unknown) => value is "agent:generating_started" | "agent:waiting_approval" | "agent:waiting_choice" | "agent:generating_completed" | "agent:stopped" | "monitor:no_progress" | "monitor:long_generating";
37
+ export declare const isDaemonStatusEventName: (value: unknown) => value is "agent:generating_started" | "agent:waiting_approval" | "agent:waiting_choice" | "agent:generating_completed" | "agent:stopped" | "monitor:no_progress";
38
38
  /**
39
39
  * Server-bound status event. Content boundary: identifiers, enums, counters
40
40
  * and booleans only — plus the approved approval-modal exception
@@ -102,8 +102,6 @@ export type SeqscribeStatusWireSummary = {
102
102
  transcriptDedupedBucket?: number;
103
103
  transcriptOversizedBucket?: number;
104
104
  transcriptDroppedBucket?: number;
105
- transcriptParityRan?: boolean;
106
- transcriptParityMismatchBucket?: number;
107
105
  };
108
106
  export type CloudStatusReportWirePayload = {
109
107
  sessions: RoutingSessionWireEntry[];
@@ -9,8 +9,7 @@
9
9
  * (web-cloud `p2p.ts`, daemon-cloud `data-channel-router.ts`), the shared
10
10
  * `DashboardP2PMessageKind` listed 12 of them and was imported by nothing, and
11
11
  * the daemon's file-operation replies carried no `type` at all — the browser
12
- * matched them by `id` as a fallthrough. They are now `response` frames; the
13
- * browser decoder still accepts the typeless legacy shape from older daemons.
12
+ * matched them by `id` as a fallthrough. They are now `response` frames.
14
13
  *
15
14
  * Chunking: any request frame may be split by the sender into
16
15
  * `<kind>_chunk` frames (`id`, `chunkId`, `index`, `total`, `data`) when it
@@ -82,12 +81,16 @@ export type P2PScreenshotStopMsg = {
82
81
  type: 'screenshot_stop';
83
82
  targetSessionId?: string;
84
83
  };
85
- /** Topic subscription. `params` is topic-specific (daemon-core `SubscribeRequestMap`); `object` keeps every topic's interface assignable. */
84
+ /**
85
+ * Topic subscription. `params` is topic-specific (daemon-core `SubscribeRequestMap`); `object` keeps every topic's interface assignable.
86
+ * `wireVersion` is the dashboard wire version the page speaks (./dashboard-wire-version.ts).
87
+ */
86
88
  export type P2PSubscribeMsg = {
87
89
  type: 'subscribe';
88
90
  topic: string;
89
91
  key: string;
90
92
  params: object;
93
+ wireVersion?: number;
91
94
  };
92
95
  export type P2PUnsubscribeMsg = {
93
96
  type: 'unsubscribe';
@@ -204,13 +207,7 @@ export type P2PTranscriptTopicsAvailableMsg = {
204
207
  topics: string[];
205
208
  };
206
209
  export type DaemonToDashboardP2PMsg = P2PPingMsg | P2PPongMsg | P2PEvictedMsg | P2PStatusEventMsg | P2PTopicUpdateMsg | P2PTopicUpdateChunkMsg | P2PSessionOutputMsg | P2PSessionOutputChunkMsg | P2PSessionIoErrorMsg | P2PCommandResultMsg | P2PCommandResultChunkMsg | P2PResponseMsg | P2PTranscriptTopicsAvailableMsg;
207
- /**
208
- * Parse boundary for the dashboard (after chunk reassembly).
209
- *
210
- * A typeless `{ id, success }` frame is a file/input reply from a daemon that
211
- * predates the `response` kind; it is promoted here so the union stays
212
- * exhaustive while the fleet rolls forward.
213
- */
210
+ /** Parse boundary for the dashboard (after chunk reassembly). */
214
211
  export declare function decodeDaemonToDashboardP2P(raw: unknown): DaemonToDashboardP2PMsg | null;
215
212
  /** Both directions, for the legacy `DashboardP2PMessageKind` name and the drift gate. */
216
213
  export declare const DASHBOARD_P2P_MESSAGE_KINDS: ReadonlyArray<DashboardToDaemonP2PType | DaemonToDashboardP2PType>;
@@ -21,9 +21,9 @@ import { type DaemonStatusEventName, type P2PIceWirePayload, type P2POfferWirePa
21
21
  /** Server-originated events the dashboard receives in addition to the daemon set. */
22
22
  export declare const SERVER_STATUS_EVENT_NAMES: readonly ["daemon:disconnect", "team:session_viewed", "team:view_request", "team:view_request_approved", "team:view_request_rejected"];
23
23
  /** Mirror of daemon-core `DashboardStatusEventName`; pinned equal by the server at compile time. */
24
- export declare const DASHBOARD_STATUS_EVENT_NAMES: readonly ["agent:generating_started", "agent:waiting_approval", "agent:waiting_choice", "agent:generating_completed", "agent:stopped", "monitor:no_progress", "monitor:long_generating", "daemon:disconnect", "team:session_viewed", "team:view_request", "team:view_request_approved", "team:view_request_rejected"];
24
+ export declare const DASHBOARD_STATUS_EVENT_NAMES: readonly ["agent:generating_started", "agent:waiting_approval", "agent:waiting_choice", "agent:generating_completed", "agent:stopped", "monitor:no_progress", "daemon:disconnect", "team:session_viewed", "team:view_request", "team:view_request_approved", "team:view_request_rejected"];
25
25
  export type DashboardStatusEventName = DaemonStatusEventName | typeof SERVER_STATUS_EVENT_NAMES[number];
26
- export declare const isDashboardStatusEventName: (value: unknown) => value is "agent:generating_started" | "agent:waiting_approval" | "agent:waiting_choice" | "agent:generating_completed" | "agent:stopped" | "monitor:no_progress" | "monitor:long_generating" | "daemon:disconnect" | "team:session_viewed" | "team:view_request" | "team:view_request_approved" | "team:view_request_rejected";
26
+ export declare const isDashboardStatusEventName: (value: unknown) => value is "agent:generating_started" | "agent:waiting_approval" | "agent:waiting_choice" | "agent:generating_completed" | "agent:stopped" | "monitor:no_progress" | "daemon:disconnect" | "team:session_viewed" | "team:view_request" | "team:view_request_approved" | "team:view_request_rejected";
27
27
  /**
28
28
  * Sanitized status event as the server relays it (UserSession
29
29
  * `buildDashboardStatusEvent`). Mirror of daemon-core
@@ -0,0 +1,21 @@
1
+ /**
2
+ * protocol/dashboard-wire-version — the dashboard ↔ daemon wire version.
3
+ *
4
+ * Both dashboards (cloud over P2P, standalone over WS) state the version they
5
+ * speak on every topic subscribe; the daemon states its own on the standalone
6
+ * hello, on every keyed snapshot frame, and in the `protocol_mismatch` reply it
7
+ * sends INSTEAD of state when the two differ. There is no dual-format serving:
8
+ * a mismatched pair renders no state at all —
9
+ * - the page is older than the daemon → the page must reload (its bundle
10
+ * cannot read the daemon's frames);
11
+ * - the daemon is older than the page → the daemon must be updated.
12
+ *
13
+ * Bump this whenever a frame shape changes incompatibly; one release carries
14
+ * the same value on both ends.
15
+ * 1 — implicit (frames before versioning).
16
+ * 2 — daemon.metadata deltas on the keyed-doc engine (2026-09-29).
17
+ */
18
+ export declare const DASHBOARD_WIRE_VERSION = 2;
19
+ export type DashboardWireCompat = 'ok' | 'reload_required' | 'daemon_update_required';
20
+ /** How a page speaking `pageVersion` relates to a daemon speaking `daemonVersion` (absent = 1). */
21
+ export declare function compareDashboardWireVersion(pageVersion: number, daemonVersion: number | null | undefined): DashboardWireCompat;
@@ -13,3 +13,4 @@ export * from './envelope';
13
13
  export * from './daemon-server';
14
14
  export * from './dashboard-server';
15
15
  export * from './dashboard-daemon-p2p';
16
+ export * from './dashboard-wire-version';
@@ -59,3 +59,17 @@ export declare function isDeadStatus(raw: unknown): boolean;
59
59
  */
60
60
  export declare function isBusyStatus(raw: unknown): boolean;
61
61
  export declare function statusesOfClass(cls: Exclude<SessionStatusClass, 'unknown'>): SessionStatus[];
62
+ /**
63
+ * Mesh-record terminal spellings: a session RECORD in one of these is gone
64
+ * (the vocabulary's `dead` class covers the canonical members; records also
65
+ * carry `failed` / `terminated` / `exited` / `closed`).
66
+ */
67
+ export declare const TERMINAL_SESSION_RECORD_STATUSES: readonly ["stopped", "failed", "terminated", "exited", "closed"];
68
+ export declare function isTerminalSessionRecordStatus(raw: unknown): boolean;
69
+ /**
70
+ * THE "idle — can take a task now" predicate over a session state/record (its
71
+ * top-level `status` and chat lane). Every idle-candidate check (queue drain,
72
+ * auto-launch orphan sweep, dispatch pick) uses this one, so they cannot
73
+ * disagree about which sessions are candidates.
74
+ */
75
+ export declare function isIdleSessionState(state: unknown): boolean;
@@ -361,11 +361,9 @@ export declare const TURN_EVIDENCE_FIELD_SPECS: TurnEvidenceFieldSpecs;
361
361
  export declare const TURN_EVIDENCE_KINDS: readonly TurnEvidenceKind[];
362
362
  /** Evidence kinds that, on their own, can end a turn (the ones R18/R19/R27 reason about). */
363
363
  export declare const TERMINAL_CLASS_EVIDENCE_KINDS: readonly ["turn_end", "transcript_final", "no_progress", "worker_report", "session_error", "process_exit", "cancel", "operator_status"];
364
- export declare const isEvidenceSourceId: (value: unknown) => value is "fsm_edge" | "short_gen_inline" | "completion_flush_genuine" | "completion_flush_weak" | "monitor_final_summary" | "pre_cleanup_transcript" | "stall_transcript" | "startup_grace_collapse" | "acp_prompt_response" | "cdp_state" | "approval_gate" | "modal_button" | "mesh_stall_watchdog" | "status_monitor" | "pty_exit" | "provider_error" | "coordinator_probe" | "mcp_probe" | "worker_tool" | "dispatch" | "input_service" | "operator" | "intentional_cleanup" | "scheduler" | "git_probe" | "session_registry";
365
364
  export declare const isTurnEvidenceKind: (value: unknown) => value is "no_progress" | "delivered" | "liveness" | "turn_end" | "transcript_final" | "session_error" | "dispatch_failed" | "dispatch_accepted" | "delivery_refused" | "duplicate_dispatch_refusal" | "session_rebound" | "turn_started" | "suspension" | "suspension_resolved" | "transcript_activity" | "process_exit" | "worker_report" | "worker_progress" | "git_side_effect" | "cancel" | "operator_status" | "coordinator_ack" | "hold_expired";
366
365
  export declare const isTurnReason: (value: unknown) => value is "intentional_cleanup" | "operator_cancel" | "task_removed" | "mission_abandoned" | "superseded" | "operator_update" | "refine_terminal" | "validation_terminal" | "hard_ceiling" | "turn_end" | "weak_end_confirmed" | "transcript_final" | "worker_reported" | "finalization_timeout_no_response" | "session_error" | "provider_auth_failed" | "provider_billing_failed" | "reclaim_budget_exhausted" | "hollow_max_retries" | "migration_orphan" | "dispatch_refused_session_exited" | "dispatch_refused_no_target" | "dispatch_refused_unsupported_input" | "dispatch_failed" | "session_exit" | "session_exit_before_turn" | "session_dead" | "hollow_completion" | "assigned_stranded_dispatch_unconfirmed" | "delivered_not_consumed_redrive" | "delivered_no_turn_deadline";
367
366
  export declare const isNotifyKind: (value: unknown) => value is "stopped" | "failed" | "no_progress" | "completed" | "cancelled" | "approval" | "choice" | "approval_resolved" | "candidate" | "progress" | "late_completion" | "mesh_event";
368
- export declare const isHoldReason: (value: unknown) => value is "await_delivery" | "await_consume" | "await_turn" | "liveness" | "hard_ceiling" | "live_pending" | "transcript_quiet" | "weak_candidate" | "suspension_before_consumed" | "await_report" | "await_end";
369
367
  export declare const isTurnOutcome: (value: unknown) => value is "failed" | "completed" | "cancelled";
370
368
  export declare const isCommitStrength: (value: unknown) => value is "operator" | "genuine" | "weak" | "tool_report";
371
369
  export declare function isEvidenceIdentifier(value: unknown): value is string;
@@ -376,7 +374,6 @@ export declare function isTurnAttemptRef(value: unknown): value is TurnAttemptRe
376
374
  * and NO undeclared field (an unknown key is how free text would sneak in).
377
375
  */
378
376
  export declare function isTurnEvidence(value: unknown): value is TurnEvidence;
379
- export declare function isTerminalClassEvidenceKind(kind: TurnEvidenceKind): kind is typeof TERMINAL_CLASS_EVIDENCE_KINDS[number];
380
377
  export declare const TURN_EVIDENCE_FLAGS: readonly ["hollow", "afterFinalizationTimeout", "nativeMarker", "selfAttributing"];
381
378
  export type TurnEvidenceFlag = typeof TURN_EVIDENCE_FLAGS[number];
382
379
  /** Scalar-only payload for `mesh.record` (the existing allow-list projection's output). */
@@ -422,7 +419,6 @@ export type MeshTopicEntry = {
422
419
  taskId?: string;
423
420
  payload: ProjectedScalars;
424
421
  });
425
- export declare const isMeshTopicEntryKind: (value: unknown) => value is "turn.evidence" | "turn.committed" | "turn.notify" | "mesh.record";
426
422
  export declare function isMeshTopicEntry(value: unknown): value is MeshTopicEntry;
427
423
  /**
428
424
  * Project evidence onto its `turn.evidence` topic entry. Pure; the attempt
@@ -29,8 +29,6 @@ export type P2PSignalingWsMsg = P2PSignalType | MeshP2PSignalType;
29
29
  export type DashboardP2PMessageKind = DashboardToDaemonP2PType | DaemonToDashboardP2PType;
30
30
  export declare const DAEMON_TO_SERVER_WS_MSGS: readonly DaemonToServerWsMsg[];
31
31
  export declare const SERVER_TO_DAEMON_WS_MSGS: readonly ServerToDaemonWsMsg[];
32
- export declare const isDaemonToServerWsMsg: (value: unknown) => value is DaemonToServerWsMsg;
33
- export declare const isServerToDaemonWsMsg: (value: unknown) => value is ServerToDaemonWsMsg;
34
32
  /**
35
33
  * `auth_ok.payload.limits` — the plan-limit contract the server sends DOWN to
36
34
  * the daemon for client-side enforcement.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adhdev/mesh-shared",
3
- "version": "1.0.60-rc.70",
3
+ "version": "1.0.60-rc.72",
4
4
  "description": "ADHDev mesh-shared — pure mesh/git status normalizers shared by daemon-core and web-core",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
package/src/index.ts CHANGED
@@ -39,5 +39,4 @@ export * from './session-launch'
39
39
  export * from './turn-ipc'
40
40
  export * from './path-ownership'
41
41
  export * from './mission-brief'
42
- export * from './keyed-status-delta'
43
42
  export * from './keyed-doc-delta'
@@ -1,21 +1,3 @@
1
- /**
2
- * Pure {{key}} template interpolation shared by the mesh command paths.
3
- *
4
- * Hoisted from packages/daemon-cloud/src/mesh/mesh-interpolation.ts — the
5
- * substitution is transport-agnostic (plain string/object work), so it belongs
6
- * in the dependency-free mesh-shared leaf where standalone can reuse it too.
7
- * The substitution semantics are byte-for-byte identical to the prior cloud copy.
8
- */
9
- export function interpolateArgs(
10
- args: Record<string, unknown>,
11
- context: Record<string, unknown>,
12
- ): Record<string, unknown> {
13
- const result: Record<string, unknown> = {};
14
- for (const [k, v] of Object.entries(args)) {
15
- result[k] = typeof v === 'string' ? interpolateString(v, context) : v;
16
- }
17
- return result;
18
- }
19
1
 
20
2
  export function interpolateString(template: string, ctx: Record<string, unknown>): string {
21
3
  return template.replace(/\{\{(\w+)\}\}/g, (_, key) => {
@@ -1,33 +1,41 @@
1
1
  /**
2
- * Keyed document delta — the diff/fold pair behind the `mesh.status` lane
3
- * (the dashboard's mesh view; data-path audit 2026-09-29 P1-5).
2
+ * Keyed document delta — THE diff/fold engine behind every keyed dashboard
3
+ * lane: `daemon.metadata` (the daemon's state, sessions keyed by id) and
4
+ * `mesh.status` (the coordinator's mesh view, nodes / queue tasks / missions
5
+ * keyed) — data-path audit 2026-09-29 P1-4 (one engine, one spec per lane).
4
6
  *
5
- * A document is plain top-level fields plus named keyed COLLECTIONS (arrays of
6
- * rows identified by an id field, possibly nested one object deep, e.g.
7
- * `queue.tasks`). The daemon digests every body it would have sent, diffs it
8
- * against the digest of the last body DELIVERED to a subscription, and sends
9
- * only the difference: changed top-level fields, and per collection the rows
10
- * whose content changed (changed fields only), the fields a row lost, and the
11
- * rows that disappeared. An unchanged document diffs to `null` — zero bytes on
12
- * the wire. The dashboard folds each delta into its held snapshot; the fold of
13
- * every delta since a snapshot equals the latest snapshot (pinned by
14
- * keyed-doc-delta.test.ts).
7
+ * A document is plain top-level fields, optional SPLIT objects (an object whose
8
+ * fields are diffed one by one, e.g. daemon.metadata's `status`), and named
9
+ * keyed COLLECTIONS (arrays of rows identified by an id field, possibly nested
10
+ * one object deep, e.g. `queue.tasks` / `status.sessions`). The daemon digests
11
+ * every body it would have sent, diffs it against the digest of the last body
12
+ * DELIVERED to a subscription, and sends only the difference: changed fields,
13
+ * and per collection the rows whose content changed (changed fields only), the
14
+ * fields a row lost, and the rows that disappeared. An unchanged document
15
+ * diffs to `null` — zero bytes on the wire. The dashboard folds each delta
16
+ * into its held snapshot; the fold of every delta since a snapshot equals the
17
+ * latest snapshot apart from `ignore`d fields (pinned by keyed-doc-delta.test.ts).
15
18
  *
16
- * `volatileTopLevel` fields (a build stamp, live process counters) are not a
17
- * change by themselves; when something else changed they ride along.
19
+ * Volatile fields (a build stamp, live process counters, a row's per-build
20
+ * stamps via `stableRow`) are not a change by themselves; when something else
21
+ * changed they ride along.
18
22
  *
19
23
  * Pure leaf: plain objects in, plain objects out; no transport, no clock.
20
24
  */
21
25
 
22
26
  export interface KeyedDocSpec {
23
- /** Collection path (`nodes`, `queue.tasks`) → the id field of its rows. */
27
+ /** Collection path (`nodes`, `queue.tasks`, `status.sessions`) → the id field of its rows. */
24
28
  collections: Readonly<Record<string, string>>
25
- /** Top-level fields that change on every build without meaning anything. */
29
+ /** Top-level objects whose own fields are diffed one by one (the rest of the object is not re-sent). */
30
+ splitObjects?: readonly string[]
31
+ /** Field paths (`daemonId`, `status.timestamp`) that are never diffed — envelope identity / clock. */
32
+ ignore?: readonly string[]
33
+ /** Field paths (top-level or inside a split object) that change on every build without meaning anything. */
26
34
  volatileTopLevel?: readonly string[]
27
35
  /**
28
36
  * The part of a collection row that decides whether it CHANGED (drop the
29
37
  * per-build stamps inside it). A row whose stable view is unchanged sends
30
- * nothing; a changed field is sent with its full current value, stamps
38
+ * nothing; a changed row sends every field whose value differs, stamps
31
39
  * included. Must not mutate `row`.
32
40
  */
33
41
  stableRow?: (collection: string, row: Record<string, unknown>) => Record<string, unknown>
@@ -49,6 +57,7 @@ interface CollectionDigest {
49
57
  }
50
58
 
51
59
  export interface KeyedDocDigest {
60
+ /** Field signatures by path (`field`, or `object.field` for a split object). */
52
61
  readonly top: ReadonlyMap<string, string>
53
62
  readonly topValues: Readonly<Record<string, unknown>>
54
63
  readonly collections: ReadonlyMap<string, CollectionDigest>
@@ -69,6 +78,8 @@ export interface KeyedCollectionDelta {
69
78
  export interface KeyedDocDelta {
70
79
  set?: Record<string, unknown>
71
80
  unset?: string[]
81
+ /** Per split object: its changed / lost fields. */
82
+ objects?: Record<string, { set?: Record<string, unknown>; unset?: string[] }>
72
83
  collections?: Record<string, KeyedCollectionDelta>
73
84
  }
74
85
 
@@ -135,11 +146,21 @@ function fieldSigsOf(digest: RowDigest): Map<string, string> {
135
146
  export function digestKeyedDoc(doc: Record<string, unknown>, spec: KeyedDocSpec): KeyedDocDigest {
136
147
  const top = new Map<string, string>()
137
148
  const topValues: Record<string, unknown> = {}
138
- for (const [key, value] of Object.entries(stripCollections(doc, spec))) {
149
+ const ignore = new Set(spec.ignore ?? [])
150
+ const split = new Set(spec.splitObjects ?? [])
151
+ const add = (path: string, value: unknown) => {
152
+ if (ignore.has(path)) return
139
153
  const s = sig(value)
140
- if (s === undefined) continue
141
- top.set(key, s)
142
- topValues[key] = value
154
+ if (s === undefined) return
155
+ top.set(path, s)
156
+ topValues[path] = value
157
+ }
158
+ for (const [key, value] of Object.entries(stripCollections(doc, spec))) {
159
+ if (split.has(key) && isRecord(value)) {
160
+ for (const [field, fieldValue] of Object.entries(value)) add(`${key}.${field}`, fieldValue)
161
+ continue
162
+ }
163
+ add(key, value)
143
164
  }
144
165
  const collections = new Map<string, CollectionDigest>()
145
166
  for (const [path, idField] of Object.entries(spec.collections)) {
@@ -187,6 +208,13 @@ function diffCollection(prev: CollectionDigest | undefined, next: CollectionDige
187
208
  change[key] = nextRow.value[key]
188
209
  changed = true
189
210
  }
211
+ // A real change carries the row's volatile fields (dropped by stableRow) along.
212
+ if (changed) {
213
+ for (const [key, value] of Object.entries(nextRow.value)) {
214
+ if (key in nextRow.stable || key in change) continue
215
+ if (sig(prevRow.value[key]) !== sig(value)) change[key] = value
216
+ }
217
+ }
190
218
  const gone: string[] = []
191
219
  for (const key of prevSigs.keys()) if (!nextSigs.has(key) && !(key in nextRow.value)) gone.push(key)
192
220
  if (changed) (delta.upsert ??= []).push(change)
@@ -208,15 +236,27 @@ export function diffKeyedDoc(prev: KeyedDocDigest, next: KeyedDocDigest, spec: K
208
236
  let meaningful = false
209
237
  let set: Record<string, unknown> | undefined
210
238
  let unset: string[] | undefined
211
- for (const [key, s] of next.top) {
212
- if (prev.top.get(key) === s) continue
213
- ;(set ??= {})[key] = next.topValues[key]
214
- if (!next.volatile.has(key)) meaningful = true
239
+ const split = spec.splitObjects ?? []
240
+ const objectOf = (path: string): [string, string] | null => {
241
+ for (const object of split) {
242
+ if (path.startsWith(`${object}.`)) return [object, path.slice(object.length + 1)]
243
+ }
244
+ return null
215
245
  }
216
- for (const key of prev.top.keys()) {
217
- if (next.top.has(key)) continue
218
- ;(unset ??= []).push(key)
219
- if (!next.volatile.has(key)) meaningful = true
246
+ const objects: NonNullable<KeyedDocDelta['objects']> = {}
247
+ for (const [path, s] of next.top) {
248
+ if (prev.top.get(path) === s) continue
249
+ const inObject = objectOf(path)
250
+ if (inObject) ((objects[inObject[0]] ??= {}).set ??= {})[inObject[1]] = next.topValues[path]
251
+ else (set ??= {})[path] = next.topValues[path]
252
+ if (!next.volatile.has(path)) meaningful = true
253
+ }
254
+ for (const path of prev.top.keys()) {
255
+ if (next.top.has(path)) continue
256
+ const inObject = objectOf(path)
257
+ if (inObject) ((objects[inObject[0]] ??= {}).unset ??= []).push(inObject[1])
258
+ else (unset ??= []).push(path)
259
+ if (!next.volatile.has(path)) meaningful = true
220
260
  }
221
261
  for (const [path, idField] of Object.entries(spec.collections)) {
222
262
  const change = diffCollection(prev.collections.get(path), next.collections.get(path), idField)
@@ -227,6 +267,7 @@ export function diffKeyedDoc(prev: KeyedDocDigest, next: KeyedDocDigest, spec: K
227
267
  if (!meaningful) return null
228
268
  if (set) delta.set = set
229
269
  if (unset) delta.unset = unset
270
+ if (Object.keys(objects).length > 0) delta.objects = objects
230
271
  return delta
231
272
  }
232
273
 
@@ -274,6 +315,12 @@ export function foldKeyedDoc<T extends Record<string, unknown>>(held: T, delta:
274
315
  const next: Record<string, unknown> = stripCollections(held, spec)
275
316
  for (const key of delta.unset ?? []) delete next[key]
276
317
  Object.assign(next, delta.set ?? {})
318
+ for (const [object, change] of Object.entries(delta.objects ?? {})) {
319
+ const folded: Record<string, unknown> = isRecord(next[object]) ? { ...(next[object] as Record<string, unknown>) } : {}
320
+ for (const key of change.unset ?? []) delete folded[key]
321
+ Object.assign(folded, change.set ?? {})
322
+ next[object] = folded
323
+ }
277
324
  for (const [path, idField] of Object.entries(spec.collections)) {
278
325
  const change = delta.collections?.[path]
279
326
  const heldList = heldCollections.get(path)
@@ -296,6 +343,24 @@ export function foldKeyedDoc<T extends Record<string, unknown>>(held: T, delta:
296
343
  return next as T
297
344
  }
298
345
 
346
+ /**
347
+ * The `daemon.metadata` document (the snapshot body minus its envelope):
348
+ * `status` is split field by field, its sessions keyed by `id`. `daemonId` is
349
+ * envelope identity and `status.timestamp` the build clock — never diffed (the
350
+ * fold re-stamps them from the frame); a session's `lastUpdated` build stamp is
351
+ * not a change by itself.
352
+ */
353
+ export const DAEMON_METADATA_DOC_SPEC: KeyedDocSpec = {
354
+ collections: { 'status.sessions': 'id' },
355
+ splitObjects: ['status'],
356
+ ignore: ['daemonId', 'status.timestamp'],
357
+ stableRow: (_collection, row) => {
358
+ if (!('lastUpdated' in row)) return row
359
+ const { lastUpdated: _stamp, ...stable } = row
360
+ return stable
361
+ },
362
+ }
363
+
299
364
  /**
300
365
  * The `mesh.status` document: nodes keyed by `nodeId`, queue tasks and missions
301
366
  * by `id`. `refreshedAt` and `sourceOfTruth` (its aggregate-cache age /
@@ -76,11 +76,8 @@ export const isMeshTaskStatus = makeGuard(MESH_TASK_STATUSES)
76
76
  export const isMeshTerminalTaskStatus = makeGuard(MESH_TERMINAL_TASK_STATUSES)
77
77
  export const isMeshTaskMode = makeGuard(MESH_TASK_MODES)
78
78
  export const isMeshTaskPriority = makeGuard(MESH_TASK_PRIORITIES)
79
- export const isMeshThinkingLevel = makeGuard(MESH_THINKING_LEVELS)
80
79
  export const isMeshDeliveryMode = makeGuard(MESH_DELIVERY_MODES)
81
- export const isMeshSessionCleanupMode = makeGuard(MESH_SESSION_CLEANUP_MODES)
82
80
  export const isWorkerReportOutcome = makeGuard(WORKER_REPORT_OUTCOMES)
83
- export const isWorkerBranchState = makeGuard(WORKER_BRANCH_STATES)
84
81
 
85
82
  /**
86
83
  * JSON-schema fragment for a string enum, derived from the tuple. Use this in
@@ -52,8 +52,6 @@ export const DAEMON_STATUS_EVENT_NAMES = [
52
52
  'agent:generating_completed',
53
53
  'agent:stopped',
54
54
  'monitor:no_progress',
55
- /** Legacy alias for `monitor:no_progress`, still emitted by older daemons. */
56
- 'monitor:long_generating',
57
55
  ] as const
58
56
  export type DaemonStatusEventName = typeof DAEMON_STATUS_EVENT_NAMES[number]
59
57
  export const isDaemonStatusEventName = makeTypeGuard(DAEMON_STATUS_EVENT_NAMES)
@@ -130,8 +128,6 @@ export type SeqscribeStatusWireSummary = {
130
128
  transcriptDedupedBucket?: number
131
129
  transcriptOversizedBucket?: number
132
130
  transcriptDroppedBucket?: number
133
- transcriptParityRan?: boolean
134
- transcriptParityMismatchBucket?: number
135
131
  }
136
132
 
137
133
  export type CloudStatusReportWirePayload = {
@@ -9,8 +9,7 @@
9
9
  * (web-cloud `p2p.ts`, daemon-cloud `data-channel-router.ts`), the shared
10
10
  * `DashboardP2PMessageKind` listed 12 of them and was imported by nothing, and
11
11
  * the daemon's file-operation replies carried no `type` at all — the browser
12
- * matched them by `id` as a fallthrough. They are now `response` frames; the
13
- * browser decoder still accepts the typeless legacy shape from older daemons.
12
+ * matched them by `id` as a fallthrough. They are now `response` frames.
14
13
  *
15
14
  * Chunking: any request frame may be split by the sender into
16
15
  * `<kind>_chunk` frames (`id`, `chunkId`, `index`, `total`, `data`) when it
@@ -98,8 +97,11 @@ export type P2PPtyInputMsg = { type: 'pty_input'; sessionId?: string; targetSess
98
97
  export type P2PPtyResizeMsg = { type: 'pty_resize'; sessionId: string; cols: number; rows: number }
99
98
  export type P2PScreenshotStartMsg = { type: 'screenshot_start'; targetSessionId: string }
100
99
  export type P2PScreenshotStopMsg = { type: 'screenshot_stop'; targetSessionId?: string }
101
- /** Topic subscription. `params` is topic-specific (daemon-core `SubscribeRequestMap`); `object` keeps every topic's interface assignable. */
102
- export type P2PSubscribeMsg = { type: 'subscribe'; topic: string; key: string; params: object }
100
+ /**
101
+ * Topic subscription. `params` is topic-specific (daemon-core `SubscribeRequestMap`); `object` keeps every topic's interface assignable.
102
+ * `wireVersion` is the dashboard wire version the page speaks (./dashboard-wire-version.ts).
103
+ */
104
+ export type P2PSubscribeMsg = { type: 'subscribe'; topic: string; key: string; params: object; wireVersion?: number }
103
105
  export type P2PUnsubscribeMsg = { type: 'unsubscribe'; topic: string; key: string }
104
106
  /** Which sessions this peer wants replicated over the seqscribe lane; also the peer's responder proof. */
105
107
  export type P2PSessionInterestMsg = { type: 'seqscribe_session_interest'; sessionIds: readonly string[] }
@@ -228,18 +230,9 @@ export type DaemonToDashboardP2PMsg =
228
230
  const _daemonToDashboardNamesCoverUnion: AssertSameMembers<typeof DAEMON_TO_DASHBOARD_P2P_TYPES, DaemonToDashboardP2PMsg['type']> = true
229
231
  void _daemonToDashboardNamesCoverUnion
230
232
 
231
- /**
232
- * Parse boundary for the dashboard (after chunk reassembly).
233
- *
234
- * A typeless `{ id, success }` frame is a file/input reply from a daemon that
235
- * predates the `response` kind; it is promoted here so the union stays
236
- * exhaustive while the fleet rolls forward.
237
- */
233
+ /** Parse boundary for the dashboard (after chunk reassembly). */
238
234
  export function decodeDaemonToDashboardP2P(raw: unknown): DaemonToDashboardP2PMsg | null {
239
235
  if (!isRecord(raw)) return null
240
- if (raw.type === undefined && isNonEmptyString(raw.id) && typeof raw.success === 'boolean') {
241
- return { ...raw, type: 'response' } as P2PResponseMsg
242
- }
243
236
  if (!isDaemonToDashboardP2PType(raw.type)) return null
244
237
  switch (raw.type) {
245
238
  case 'ping':