@adhdev/mesh-shared 1.0.60-rc.70 → 1.0.60-rc.71
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.
- package/dist/index.d.ts +0 -1
- package/dist/index.js +96 -217
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +91 -205
- package/dist/index.mjs.map +1 -1
- package/dist/interpolation.d.ts +0 -9
- package/dist/keyed-doc-delta.d.ts +39 -17
- package/dist/mesh-vocabulary.d.ts +0 -3
- package/dist/protocol/daemon-server.d.ts +2 -4
- package/dist/protocol/dashboard-daemon-p2p.d.ts +7 -10
- package/dist/protocol/dashboard-server.d.ts +2 -2
- package/dist/protocol/dashboard-wire-version.d.ts +21 -0
- package/dist/protocol/index.d.ts +1 -0
- package/dist/session-status.d.ts +14 -0
- package/dist/turn-evidence.d.ts +0 -4
- package/dist/ws-protocol.d.ts +0 -2
- package/package.json +1 -1
- package/src/index.ts +0 -1
- package/src/interpolation.ts +0 -18
- package/src/keyed-doc-delta.ts +94 -29
- package/src/mesh-vocabulary.ts +0 -3
- package/src/protocol/daemon-server.ts +0 -4
- package/src/protocol/dashboard-daemon-p2p.ts +7 -14
- package/src/protocol/dashboard-wire-version.ts +27 -0
- package/src/protocol/index.ts +1 -0
- package/src/session-status.ts +24 -0
- package/src/turn-evidence.ts +0 -6
- package/src/turn-ipc.ts +1 -2
- package/src/ws-protocol.ts +0 -4
- package/dist/keyed-status-delta.d.ts +0 -76
- package/src/keyed-status-delta.ts +0 -239
package/dist/interpolation.d.ts
CHANGED
|
@@ -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 —
|
|
3
|
-
* (the
|
|
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
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
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
|
-
*
|
|
17
|
-
* change by themselves; when something else
|
|
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
|
|
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
|
|
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"
|
|
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"
|
|
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
|
|
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
|
-
/**
|
|
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", "
|
|
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" | "
|
|
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;
|
package/dist/protocol/index.d.ts
CHANGED
package/dist/session-status.d.ts
CHANGED
|
@@ -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;
|
package/dist/turn-evidence.d.ts
CHANGED
|
@@ -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
|
package/dist/ws-protocol.d.ts
CHANGED
|
@@ -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
package/src/index.ts
CHANGED
package/src/interpolation.ts
CHANGED
|
@@ -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) => {
|
package/src/keyed-doc-delta.ts
CHANGED
|
@@ -1,33 +1,41 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Keyed document delta —
|
|
3
|
-
* (the
|
|
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
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
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
|
-
*
|
|
17
|
-
* change by themselves; when something else
|
|
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
|
|
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
|
|
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
|
-
|
|
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)
|
|
141
|
-
top.set(
|
|
142
|
-
topValues[
|
|
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
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
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
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
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 /
|
package/src/mesh-vocabulary.ts
CHANGED
|
@@ -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
|
|
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
|
-
/**
|
|
102
|
-
|
|
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':
|