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

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.
@@ -0,0 +1,239 @@
1
+ /**
2
+ * Keyed status delta — the diff/fold pair behind the `daemon.metadata` lane
3
+ * (the ONE daemon→dashboard state lane; data-path audit 2026-09-29 P0-3).
4
+ *
5
+ * The daemon digests every body it would have sent, diffs it against the
6
+ * digest of the last body DELIVERED to a subscription, and sends only the
7
+ * difference: changed top-level fields, changed daemon-level `status` fields,
8
+ * and per-session changed fields keyed by session id, with removals explicit.
9
+ * An unchanged state diffs to `null` — zero bytes on the wire. The dashboard
10
+ * folds each delta into its held snapshot; the fold of every delta since a
11
+ * snapshot equals the latest snapshot (pinned by keyed-status-delta.test.ts).
12
+ *
13
+ * Pure leaf: plain objects in, plain objects out; no transport, no clock.
14
+ */
15
+
16
+ /** A session row: identified by `id`, everything else is a field. */
17
+ export interface KeyedStatusSession {
18
+ id: string
19
+ [field: string]: unknown
20
+ }
21
+
22
+ /** The snapshot body being diffed: top-level fields plus a `status` with a keyed session list. */
23
+ export interface KeyedStatusBody {
24
+ status: { sessions: KeyedStatusSession[]; [field: string]: unknown }
25
+ [field: string]: unknown
26
+ }
27
+
28
+ export interface KeyedStatusDigestOptions {
29
+ /** Top-level keys that are never diffed (envelope identity: daemonId, …). */
30
+ ignoreTopLevel?: readonly string[]
31
+ /** `status` keys that are never diffed (in addition to `sessions`). */
32
+ ignoreStatus?: readonly string[]
33
+ /**
34
+ * Session fields that change on every build without meaning anything
35
+ * (a build-time `lastUpdated` stamp). A change to ONLY these fields is not
36
+ * a change; when something else changed they ride along.
37
+ */
38
+ volatileSessionFields?: readonly string[]
39
+ }
40
+
41
+ interface SessionDigest {
42
+ readonly value: KeyedStatusSession
43
+ /** Signature of every non-volatile field (fast unchanged check). */
44
+ readonly sig: string
45
+ /** Per-field signatures, computed lazily — only for sessions that changed. */
46
+ fieldSigs?: Map<string, string>
47
+ }
48
+
49
+ export interface KeyedStatusDigest {
50
+ readonly top: ReadonlyMap<string, string>
51
+ readonly topValues: Readonly<Record<string, unknown>>
52
+ readonly status: ReadonlyMap<string, string>
53
+ readonly statusValues: Readonly<Record<string, unknown>>
54
+ readonly sessions: ReadonlyMap<string, SessionDigest>
55
+ readonly order: readonly string[]
56
+ readonly volatile: ReadonlySet<string>
57
+ }
58
+
59
+ export interface KeyedStatusDelta {
60
+ set?: Record<string, unknown>
61
+ unset?: string[]
62
+ statusSet?: Record<string, unknown>
63
+ statusUnset?: string[]
64
+ sessions?: KeyedStatusSession[]
65
+ sessionUnset?: Record<string, string[]>
66
+ removedSessionIds?: string[]
67
+ sessionOrder?: string[]
68
+ }
69
+
70
+ function sig(value: unknown): string | undefined {
71
+ if (value === undefined) return undefined
72
+ try {
73
+ return JSON.stringify(value)
74
+ } catch {
75
+ return undefined
76
+ }
77
+ }
78
+
79
+ function digestFields(
80
+ source: Record<string, unknown>,
81
+ ignore: ReadonlySet<string>,
82
+ ): { sigs: Map<string, string>; values: Record<string, unknown> } {
83
+ const sigs = new Map<string, string>()
84
+ const values: Record<string, unknown> = {}
85
+ for (const [key, value] of Object.entries(source)) {
86
+ if (ignore.has(key)) continue
87
+ const s = sig(value)
88
+ if (s === undefined) continue
89
+ sigs.set(key, s)
90
+ values[key] = value
91
+ }
92
+ return { sigs, values }
93
+ }
94
+
95
+ function sessionSignature(session: KeyedStatusSession, volatile: ReadonlySet<string>): string {
96
+ if (volatile.size === 0) return sig(session) ?? ''
97
+ const stable: Record<string, unknown> = {}
98
+ for (const [key, value] of Object.entries(session)) {
99
+ if (!volatile.has(key)) stable[key] = value
100
+ }
101
+ return sig(stable) ?? ''
102
+ }
103
+
104
+ function fieldSigsOf(digest: SessionDigest): Map<string, string> {
105
+ if (!digest.fieldSigs) {
106
+ const sigs = new Map<string, string>()
107
+ for (const [key, value] of Object.entries(digest.value)) {
108
+ const s = sig(value)
109
+ if (s !== undefined) sigs.set(key, s)
110
+ }
111
+ digest.fieldSigs = sigs
112
+ }
113
+ return digest.fieldSigs
114
+ }
115
+
116
+ /** Digest one body. Cheap to hold: values are referenced, not copied. */
117
+ export function digestKeyedStatus(body: KeyedStatusBody, options: KeyedStatusDigestOptions = {}): KeyedStatusDigest {
118
+ const volatile = new Set(options.volatileSessionFields ?? [])
119
+ const top = digestFields(body as Record<string, unknown>, new Set(['status', ...(options.ignoreTopLevel ?? [])]))
120
+ const status = digestFields(body.status, new Set(['sessions', ...(options.ignoreStatus ?? [])]))
121
+ const sessions = new Map<string, SessionDigest>()
122
+ const order: string[] = []
123
+ for (const session of Array.isArray(body.status?.sessions) ? body.status.sessions : []) {
124
+ if (!session || typeof session.id !== 'string' || !session.id) continue
125
+ if (sessions.has(session.id)) continue
126
+ sessions.set(session.id, { value: session, sig: sessionSignature(session, volatile) })
127
+ order.push(session.id)
128
+ }
129
+ return { top: top.sigs, topValues: top.values, status: status.sigs, statusValues: status.values, sessions, order, volatile }
130
+ }
131
+
132
+ function diffFieldMaps(
133
+ prev: ReadonlyMap<string, string>,
134
+ next: ReadonlyMap<string, string>,
135
+ nextValues: Readonly<Record<string, unknown>>,
136
+ ): { set?: Record<string, unknown>; unset?: string[] } {
137
+ let set: Record<string, unknown> | undefined
138
+ let unset: string[] | undefined
139
+ for (const [key, s] of next) {
140
+ if (prev.get(key) === s) continue
141
+ ;(set ??= {})[key] = nextValues[key]
142
+ }
143
+ for (const key of prev.keys()) {
144
+ if (!next.has(key)) (unset ??= []).push(key)
145
+ }
146
+ return { ...(set ? { set } : {}), ...(unset ? { unset } : {}) }
147
+ }
148
+
149
+ function sameOrder(a: readonly string[], b: readonly string[]): boolean {
150
+ if (a.length !== b.length) return false
151
+ for (let i = 0; i < a.length; i += 1) if (a[i] !== b[i]) return false
152
+ return true
153
+ }
154
+
155
+ /**
156
+ * The difference `prev → next`, or `null` when nothing a consumer can observe
157
+ * changed. `prev === null` is not accepted here — a subscription with no
158
+ * delivered baseline gets a snapshot, not a delta.
159
+ */
160
+ export function diffKeyedStatus(prev: KeyedStatusDigest, next: KeyedStatusDigest): KeyedStatusDelta | null {
161
+ const delta: KeyedStatusDelta = {}
162
+ const top = diffFieldMaps(prev.top, next.top, next.topValues)
163
+ if (top.set) delta.set = top.set
164
+ if (top.unset) delta.unset = top.unset
165
+ const status = diffFieldMaps(prev.status, next.status, next.statusValues)
166
+ if (status.set) delta.statusSet = status.set
167
+ if (status.unset) delta.statusUnset = status.unset
168
+
169
+ for (const [id, nextSession] of next.sessions) {
170
+ const prevSession = prev.sessions.get(id)
171
+ if (!prevSession) {
172
+ ;(delta.sessions ??= []).push(nextSession.value)
173
+ continue
174
+ }
175
+ if (prevSession.sig === nextSession.sig) continue
176
+ const prevSigs = fieldSigsOf(prevSession)
177
+ const nextSigs = fieldSigsOf(nextSession)
178
+ const change: KeyedStatusSession = { id }
179
+ let changed = false
180
+ for (const [key, s] of nextSigs) {
181
+ if (key === 'id' || prevSigs.get(key) === s) continue
182
+ change[key] = nextSession.value[key]
183
+ changed = true
184
+ }
185
+ const gone: string[] = []
186
+ for (const key of prevSigs.keys()) {
187
+ if (!nextSigs.has(key)) gone.push(key)
188
+ }
189
+ if (changed) (delta.sessions ??= []).push(change)
190
+ if (gone.length > 0) (delta.sessionUnset ??= {})[id] = gone
191
+ }
192
+ for (const id of prev.sessions.keys()) {
193
+ if (!next.sessions.has(id)) (delta.removedSessionIds ??= []).push(id)
194
+ }
195
+ if (!sameOrder(prev.order, next.order)) delta.sessionOrder = [...next.order]
196
+
197
+ return Object.keys(delta).length > 0 ? delta : null
198
+ }
199
+
200
+ /** Rebuild the full body from a held one plus a delta. Never mutates `held`. */
201
+ export function foldKeyedStatus<T extends KeyedStatusBody>(held: T, delta: KeyedStatusDelta): T {
202
+ const next: Record<string, unknown> = { ...held }
203
+ for (const key of delta.unset ?? []) delete next[key]
204
+ Object.assign(next, delta.set ?? {})
205
+
206
+ const status: Record<string, unknown> = { ...held.status }
207
+ for (const key of delta.statusUnset ?? []) delete status[key]
208
+ Object.assign(status, delta.statusSet ?? {})
209
+
210
+ const byId = new Map<string, KeyedStatusSession>()
211
+ for (const session of held.status.sessions ?? []) byId.set(session.id, session)
212
+ for (const id of delta.removedSessionIds ?? []) byId.delete(id)
213
+ for (const change of delta.sessions ?? []) {
214
+ const base = byId.get(change.id)
215
+ byId.set(change.id, base ? { ...base, ...change } : { ...change })
216
+ }
217
+ for (const [id, keys] of Object.entries(delta.sessionUnset ?? {})) {
218
+ const base = byId.get(id)
219
+ if (!base) continue
220
+ const trimmed = { ...base }
221
+ for (const key of keys) delete trimmed[key]
222
+ byId.set(id, trimmed)
223
+ }
224
+ const order = delta.sessionOrder ?? (held.status.sessions ?? []).map((session) => session.id)
225
+ const sessions: KeyedStatusSession[] = []
226
+ const placed = new Set<string>()
227
+ for (const id of order) {
228
+ const session = byId.get(id)
229
+ if (!session || placed.has(id)) continue
230
+ sessions.push(session)
231
+ placed.add(id)
232
+ }
233
+ for (const [id, session] of byId) {
234
+ if (!placed.has(id)) sessions.push(session)
235
+ }
236
+ status.sessions = sessions
237
+ next.status = status
238
+ return next as T
239
+ }
@@ -124,15 +124,6 @@ export type SeqscribeStatusWireSummary = {
124
124
  fgenAgeBucket: number
125
125
  quarantined: boolean
126
126
  authority: boolean
127
- dualWrite?: boolean
128
- dualWriteFailedBucket?: number
129
- dualWriteDroppedBucket?: number
130
- dualWriteBackfilledBucket?: number
131
- parityMismatchBucket?: number
132
- parityRan?: boolean
133
- parityMissingInShadowBucket?: number
134
- parityExtraInShadowBucket?: number
135
- parityFieldMismatchBucket?: number
136
127
  transcriptPublish?: boolean
137
128
  transcriptPublishedBucket?: number
138
129
  transcriptPublishFailedBucket?: number
@@ -286,7 +277,8 @@ export type P2PIceWirePayload = { candidate: string; mid: string; peerId: string
286
277
 
287
278
  /**
288
279
  * A dashboard-originated signal as relayed to the daemon. `peerId` is stamped
289
- * by UserSession/SharedSession, `sharePermission` only by SharedSession (server
280
+ * by UserSession/SharedSession, `sharePermission` and `shareTargetSessionId`
281
+ * (the one session a share link may see) only by SharedSession (server
290
282
  * authority, never the client). Field names follow the browser/node-datachannel
291
283
  * spellings both ends already read.
292
284
  */
@@ -294,6 +286,7 @@ export type RelayedP2PSignalWirePayload = {
294
286
  peerId?: string
295
287
  daemonId?: string
296
288
  sharePermission?: string
289
+ shareTargetSessionId?: string
297
290
  sdp?: string
298
291
  type?: string
299
292
  candidate?: string
@@ -154,7 +154,6 @@ export const DAEMON_TO_DASHBOARD_P2P_TYPES = [
154
154
  'ping',
155
155
  'pong',
156
156
  'p2p_evicted',
157
- 'status_report',
158
157
  'status_event',
159
158
  'topic_update',
160
159
  'topic_update_chunk',
@@ -164,13 +163,11 @@ export const DAEMON_TO_DASHBOARD_P2P_TYPES = [
164
163
  'command_result',
165
164
  'command_result_chunk',
166
165
  'response',
166
+ 'transcript_topics_available',
167
167
  ] as const
168
168
  export type DaemonToDashboardP2PType = typeof DAEMON_TO_DASHBOARD_P2P_TYPES[number]
169
169
  export const isDaemonToDashboardP2PType = makeTypeGuard(DAEMON_TO_DASHBOARD_P2P_TYPES)
170
170
 
171
- /** Rich P2P status snapshot (daemon-core `StatusReportPayload`, or a `_delta` partial of it). Dynamic by design on this plane. */
172
- export type P2PStatusReportWirePayload = Record<string, unknown>
173
-
174
171
  /** The server-bound event plus the P2P-only structured prompt (daemon-core `P2PStatusEventPayload`). */
175
172
  export type P2PStatusEventWirePayload = DaemonStatusEventWirePayload & {
176
173
  interactivePrompt?: unknown
@@ -184,7 +181,6 @@ export type TopicUpdateWireEnvelope = { topic: string; key: string }
184
181
  export type P2PFileEntry = { name: string; type: string; size?: number }
185
182
 
186
183
  export type P2PEvictedMsg = { type: 'p2p_evicted'; reason: string; maxconnections: number }
187
- export type P2PStatusReportMsg = { type: 'status_report'; payload: P2PStatusReportWirePayload; timestamp: number }
188
184
  export type P2PStatusEventMsg = { type: 'status_event'; payload: P2PStatusEventWirePayload; timestamp: number }
189
185
  export type P2PTopicUpdateMsg = { type: 'topic_update'; update: TopicUpdateWireEnvelope }
190
186
  export type P2PTopicUpdateChunkMsg = ChunkFrame<'topic_update_chunk'>
@@ -205,11 +201,19 @@ export type P2PResponseMsg = {
205
201
  entries?: P2PFileEntry[]
206
202
  }
207
203
 
204
+ /**
205
+ * `session.<id>.chat` topics that just became SUB-able for THIS peer (the
206
+ * topic was defined, or the peer's declared session interest widened to it).
207
+ * Sent only after the grant is in place, so a seqscribe SUB issued on receipt
208
+ * is accepted; without it the browser can only guess when to re-SUB a refused
209
+ * subscription. Topic names only — the P2P plane already carries the ids.
210
+ */
211
+ export type P2PTranscriptTopicsAvailableMsg = { type: 'transcript_topics_available'; topics: string[] }
212
+
208
213
  export type DaemonToDashboardP2PMsg =
209
214
  | P2PPingMsg
210
215
  | P2PPongMsg
211
216
  | P2PEvictedMsg
212
- | P2PStatusReportMsg
213
217
  | P2PStatusEventMsg
214
218
  | P2PTopicUpdateMsg
215
219
  | P2PTopicUpdateChunkMsg
@@ -219,6 +223,7 @@ export type DaemonToDashboardP2PMsg =
219
223
  | P2PCommandResultMsg
220
224
  | P2PCommandResultChunkMsg
221
225
  | P2PResponseMsg
226
+ | P2PTranscriptTopicsAvailableMsg
222
227
 
223
228
  const _daemonToDashboardNamesCoverUnion: AssertSameMembers<typeof DAEMON_TO_DASHBOARD_P2P_TYPES, DaemonToDashboardP2PMsg['type']> = true
224
229
  void _daemonToDashboardNamesCoverUnion
@@ -241,7 +246,6 @@ export function decodeDaemonToDashboardP2P(raw: unknown): DaemonToDashboardP2PMs
241
246
  case 'pong':
242
247
  case 'p2p_evicted':
243
248
  return raw as DaemonToDashboardP2PMsg
244
- case 'status_report':
245
249
  case 'status_event':
246
250
  return isRecord(raw.payload) ? (raw as DaemonToDashboardP2PMsg) : null
247
251
  case 'topic_update':
@@ -263,6 +267,10 @@ export function decodeDaemonToDashboardP2P(raw: unknown): DaemonToDashboardP2PMs
263
267
  case 'command_result':
264
268
  case 'response':
265
269
  return isNonEmptyString(raw.id) ? (raw as DaemonToDashboardP2PMsg) : null
270
+ case 'transcript_topics_available':
271
+ return Array.isArray(raw.topics) && raw.topics.every((topic) => typeof topic === 'string')
272
+ ? { type: raw.type, topics: raw.topics as string[] }
273
+ : null
266
274
  }
267
275
  }
268
276