@adhdev/daemon-core 0.9.82-rc.481 → 0.9.82-rc.482

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.
@@ -172,11 +172,14 @@ export declare function assertPendingMeshCoordinatorEventV2(raw: unknown, path?:
172
172
  export declare function shouldDeliverPendingEventToCoordinator(event: PendingMeshCoordinatorEventV2, drainer: CoordinatorIdentity): boolean;
173
173
  /**
174
174
  * Default the v2 scope for an event by its producer event name (design decision
175
- * §3). Terminal task events → unicast (routed to the originating coordinator).
176
- * Ledger-consistency / dispatch-plane events → system. Everything else — node
177
- * lifecycle and progress signals — → broadcast, which also matches v1's
178
- * implicit "deliver to any coordinator" behaviour, so an unstamped v1 event and
179
- * a v2-stamped-as-broadcast event route identically during rollout.
175
+ * §3). Terminal task events and coordinator-addressed alerts → unicast (routed
176
+ * to the originating coordinator). Everything else — node lifecycle and
177
+ * progress signals — → broadcast, which also matches v1's implicit "deliver to
178
+ * any coordinator" behaviour, so an unstamped v1 event and a v2-stamped-as-
179
+ * broadcast event route identically during rollout. No event currently defaults
180
+ * to 'system'; the scope remains in MESH_EVENT_SCOPES for wire compatibility
181
+ * (an already-queued or version-skewed 'system' event still routes away from
182
+ * coordinators).
180
183
  */
181
184
  export declare function defaultScopeForEvent(eventName: string): MeshEventScope;
182
185
  /**
@@ -72,6 +72,10 @@ export declare function shouldHoldPendingDrainForBusyLocalCoordinator(components
72
72
  statusInstanceId?: string;
73
73
  }, meshId: string, requestedCoordinatorDaemonId?: string | null, callerIsSelfCoordinatorInboxRead?: boolean): boolean;
74
74
  export declare function __resetReclaimUnknownStreakForTests(): void;
75
+ export declare function reconcileZombieAssignedTasks(components: DaemonComponents, mesh: {
76
+ id: string;
77
+ nodes?: unknown[];
78
+ }, selfIds: string[]): void;
75
79
  export declare function runMeshReconcileTick(components: DaemonComponents): Promise<void>;
76
80
  export declare function __resetUnresolvedForwardRejectionCountsForTests(): void;
77
81
  interface ReconcileLoopHandle {
@@ -262,9 +262,39 @@ export declare class MeshRuntimeStore {
262
262
  };
263
263
  /**
264
264
  * Prune tool call log entries older than the given age in ms.
265
- * Exposed for testing.
265
+ * Returns the number of rows deleted. Also used by the periodic retention
266
+ * sweep (pruneMeshRuntimeRetention) — the in-write sweep in recordMeshToolCall
267
+ * only fires every 200 calls and only covers the rate-limit window, so a
268
+ * quiet mesh otherwise accumulates rows indefinitely.
266
269
  */
267
- pruneToolCallLog(olderThanMs: number): void;
270
+ pruneToolCallLog(olderThanMs: number): number;
271
+ /**
272
+ * Retention prune for mesh_event_ledger (SoT 1-11 (b)). The ledger is append-only
273
+ * with NO lifecycle GC of its own, so lifecycle events accumulate without bound
274
+ * (the dominant mesh-runtime.db growth). Every production reader is bounded to a
275
+ * recent window (readLedgerEntries tail/limit ≤ a few hundred; task-stats /
276
+ * terminal-evidence scans look at recent tasks), so rows past a generous age only
277
+ * cost space. Excluded from deletion — retained forever:
278
+ * - coordinator_operating_note / _tombstone: runtime-accumulated lessons whose
279
+ * whole point is surviving restarts; a tombstone must also outlive the notes
280
+ * it retracts.
281
+ * Timestamps are ISO-8601 TEXT, so the lexicographic `<` cutoff is a correct time
282
+ * comparison; a malformed timestamp compares greater than any ISO date and is
283
+ * conservatively retained. Returns rows deleted.
284
+ */
285
+ pruneEventLedger(olderThanMs: number): number;
286
+ /**
287
+ * Retention prune for TERMINAL (completed/cancelled/failed) mesh_queue rows
288
+ * (SoT 1-11 (b)). Terminal rows are kept as recent history (mesh_task_history,
289
+ * completion-dedup taskId lookups) but nothing ever deletes them, so the queue
290
+ * table grows monotonically. Rows past the retention window serve no reader —
291
+ * every dedup/attribution path operates on recent tasks — EXCEPT as a dependency
292
+ * anchor: taskDependenciesSatisfied resolves dependsOn by id and treats a MISSING
293
+ * row as not-completed, so deleting a completed row that a still-live
294
+ * (pending/assigned) row depends on would permanently strand the dependent.
295
+ * Those ids are collected first and excluded. Returns rows deleted.
296
+ */
297
+ pruneTerminalQueueEntries(olderThanMs: number): number;
268
298
  appendLedgerEntry(entry: {
269
299
  id: string;
270
300
  meshId: string;
@@ -506,3 +536,18 @@ export declare class MeshRuntimeStore {
506
536
  undrainedOlderThanMs: number;
507
537
  }): number;
508
538
  }
539
+ export declare const MESH_EVENT_LEDGER_RETENTION_MS: number;
540
+ export declare const MESH_TOOL_CALL_LOG_RETENTION_MS: number;
541
+ export declare const MESH_TERMINAL_QUEUE_RETENTION_MS: number;
542
+ /**
543
+ * Periodic retention sweep for the mesh-runtime.db tables that previously had no
544
+ * lifecycle GC (event ledger, tool-call log, terminal queue rows). Runs on the SAME
545
+ * cadence as the pending-events retention prune (the hourly mesh-event maintenance
546
+ * sweep in mesh-event-forwarding.ts). Best-effort and idempotent: a store failure
547
+ * degrades to a no-op with one warn; an empty table costs three cheap DELETEs.
548
+ */
549
+ export declare function pruneMeshRuntimeRetention(): {
550
+ ledger: number;
551
+ toolCalls: number;
552
+ terminalQueue: number;
553
+ };
@@ -9,6 +9,10 @@ export interface UnresolvedForwardEntry {
9
9
  /** When the entry was first enqueued (epoch ms) — used for age-based expiry. */
10
10
  queuedAt: number;
11
11
  }
12
+ /** Register (or clear, with undefined) the PHASE 0 retry nudge handler. */
13
+ export declare function registerUnresolvedForwardRetryNudge(handler?: () => void): void;
14
+ /** Fire-and-forget nudge: ask the reconcile loop to run the outbox retry soon. */
15
+ export declare function nudgeUnresolvedForwardRetry(): void;
12
16
  /**
13
17
  * Durably enqueue an unresolved-delegate forward for a coordinator daemon. The
14
18
  * `forwardPayload` is the flat shape handleMeshForwardEvent reads on the coordinator.
@@ -13,7 +13,7 @@
13
13
  import type { GitRepoStatus, GitCompactSummary } from './git/git-types.js';
14
14
  import type { MeshMissionSummary, MeshMissionSlimSummary } from './mesh/mesh-missions.js';
15
15
  import type { MeshMagiActivitySummary } from './mesh/mesh-magi-status.js';
16
- import type { MagiPanelMap, MagiKindPanelMap } from '@adhdev/mesh-shared';
16
+ import type { MagiKindPanelMap } from '@adhdev/mesh-shared';
17
17
  export interface RepoMesh {
18
18
  id: string;
19
19
  name: string;
@@ -547,18 +547,12 @@ export interface RepoMeshCoordinatorConfig {
547
547
  export interface LocalMeshConfig {
548
548
  meshes: LocalMeshEntry[];
549
549
  /**
550
- * MAGI cross-verification panels (machine-local). Keyed by panel name; each
551
- * binds concrete `(node × provider)` members machine-dependent facts — so
552
- * panels live here in meshes.json, never in the repo-shared .adhdev/mesh.json.
553
- * Optional: absent on configs written before MAGI existed.
554
- */
555
- magiPanels?: MagiPanelMap;
556
- /**
557
- * MAGI-KIND-PANEL: per-task_kind panel bindings (machine-local). Keyed by
558
- * task_kind (rca / design / claim_audit / freeform); each maps to ≥1
559
- * `(node × provider × model?)` slot. A `mesh_magi_review` invoked with a bare
560
- * `task_kind` resolves its panel from here — an unconfigured kind is a hard
561
- * error, never a synthesized fallback. Optional; absent on pre-feature configs.
550
+ * MAGI-KIND-PANEL: per-task_kind panel bindings (machine-local), the SOLE MAGI
551
+ * panel-resolution surface (the former named-panel `magiPanels` map was removed).
552
+ * Keyed by task_kind (rca / design / claim_audit / freeform); each maps to ≥1
553
+ * `(node × provider × model?)` slot. A `mesh_magi_review` resolves its panel from
554
+ * here — an unconfigured kind is a hard error, never a synthesized fallback.
555
+ * Optional; absent on pre-feature configs.
562
556
  */
563
557
  magiKindPanels?: MagiKindPanelMap;
564
558
  }
package/dist/types.d.ts CHANGED
@@ -5,7 +5,7 @@
5
5
  * When modifying this file, also update interface contracts in AGENT_PROTOCOL.md.
6
6
  */
7
7
  import type { StatusReportPayload, AvailableProviderInfo } from './shared-types.js';
8
- import type { ChatMessageKind } from './providers/chat-message-normalization.js';
8
+ import type { ChatMessageKind, ChatMessageVisibility, ChatMessageTranscriptVisibility, ChatMessageAudience, ChatMessageSource } from './providers/chat-message-normalization.js';
9
9
  /** Full status response from /api/v1/status and WS events */
10
10
  export interface StatusResponse extends StatusReportPayload {
11
11
  /** For standalone API compat */
@@ -47,11 +47,20 @@ export interface ChatMessage {
47
47
  /** Optional: fiber metadata */
48
48
  _type?: string;
49
49
  _sub?: string;
50
- /** Transcript visibility/audience contract for separating chat-visible content from internal/debug runtime rows. */
51
- visibility?: 'visible' | 'user' | 'chat' | 'hidden' | 'debug' | 'internal' | (string & {});
52
- transcriptVisibility?: 'visible' | 'user' | 'chat' | 'hidden' | 'debug' | 'internal' | (string & {});
53
- audience?: 'chat' | 'debug' | 'trace' | 'internal' | (string & {});
54
- source?: 'assistant_text' | 'tool_call' | 'terminal_command' | 'runtime_activity' | 'runtime_status' | 'provider_chrome' | 'control' | (string & {});
50
+ /**
51
+ * Transcript visibility/audience contract for separating chat-visible content
52
+ * from internal/debug runtime rows. These reference the canonical named unions
53
+ * declared alongside the classifier (chat-message-normalization.ts) so the known
54
+ * values have one source of truth instead of a hand-inlined copy that drifts.
55
+ * Each alias keeps the `| (string & {})` escape hatch: the read-chat contract
56
+ * (read-chat-contract.ts) preserves ANY producer-supplied string verbatim, so
57
+ * the type must stay open — it documents the known values without forbidding
58
+ * provider-specific extensions.
59
+ */
60
+ visibility?: ChatMessageVisibility;
61
+ transcriptVisibility?: ChatMessageTranscriptVisibility;
62
+ audience?: ChatMessageAudience;
63
+ source?: ChatMessageSource;
55
64
  userFacing?: boolean;
56
65
  internal?: boolean;
57
66
  isInternal?: boolean;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adhdev/daemon-core",
3
- "version": "0.9.82-rc.481",
3
+ "version": "0.9.82-rc.482",
4
4
  "description": "ADHDev daemon core — CDP, IDE detection, providers, command execution",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -47,8 +47,8 @@
47
47
  "author": "vilmire",
48
48
  "license": "AGPL-3.0-or-later",
49
49
  "dependencies": {
50
- "@adhdev/mesh-shared": "0.9.82-rc.481",
51
- "@adhdev/session-host-core": "0.9.82-rc.481",
50
+ "@adhdev/mesh-shared": "0.9.82-rc.482",
51
+ "@adhdev/session-host-core": "0.9.82-rc.482",
52
52
  "@agentclientprotocol/sdk": "^0.16.1",
53
53
  "ajv": "^8.20.0",
54
54
  "ajv-formats": "^3.0.1",
@@ -195,6 +195,57 @@ function isRuntimeFallbackHistorySessionId(
195
195
  const candidate = typeof candidateHistorySessionId === 'string' ? candidateHistorySessionId.trim() : '';
196
196
  return candidate === target;
197
197
  }
198
+
199
+ interface ResolvedNativeHistoryReadSession {
200
+ /**
201
+ * True when the candidate history id is the daemon runtime session id (==
202
+ * targetSessionId) standing in for a real provider-native conv uuid — reached
203
+ * either via getHistorySessionId's internal fallback (empty args) or because
204
+ * the browser explicitly echoed targetSessionId back as historySessionId (the
205
+ * poisoned agy-coordinator read). See isRuntimeFallbackHistorySessionId.
206
+ */
207
+ isRuntimeFallback: boolean;
208
+ /** Owner-confirmed pin recorded by a prior bound read for this mesh session, if any. */
209
+ pinnedProviderSessionId: string | undefined;
210
+ /**
211
+ * The id to key the native read on: the pin (or undefined) when the candidate
212
+ * is a runtime fallback so pin / workspace-latest resolution engages, else the
213
+ * candidate unchanged (a real DISTINCT provider uuid still exact-binds).
214
+ */
215
+ effectiveHistorySessionId: string | undefined;
216
+ }
217
+
218
+ /**
219
+ * Resolve the runtime-fallback → pin substitution shared by every native-history
220
+ * read path (handleChatHistory, the CLI-adapter main read, and the history-only
221
+ * read). Each site previously inlined this same four-step computation verbatim:
222
+ * detect the runtime fallback (candidate === targetSessionId AND no distinct
223
+ * explicit id), look up the owner-confirmed pin, and drop the runtime id in favor
224
+ * of the pin (or undefined) so readCliProviderNativeHistory's pin / workspace-
225
+ * latest paths can engage instead of fail-closing to pty-parser. Extracted to a
226
+ * single helper so the D9 historySessionId-poison guard has one definition.
227
+ * Behavior is identical to the inlined blocks — same target (args.targetSessionId),
228
+ * same explicit-id source, same pin key (getBoundProviderSessionIdPin trims).
229
+ */
230
+ function resolveNativeHistoryReadSession(
231
+ args: any,
232
+ candidateHistorySessionId: string | undefined,
233
+ ): ResolvedNativeHistoryReadSession {
234
+ const targetSid = typeof args?.targetSessionId === 'string' ? args.targetSessionId.trim() : '';
235
+ const explicitHistorySessionId = getExplicitHistorySessionId(args);
236
+ const isRuntimeFallback = Boolean(
237
+ targetSid
238
+ && isRuntimeFallbackHistorySessionId(candidateHistorySessionId, targetSid)
239
+ && (!explicitHistorySessionId
240
+ || isRuntimeFallbackHistorySessionId(explicitHistorySessionId, targetSid)),
241
+ );
242
+ const pinnedProviderSessionId = getBoundProviderSessionIdPin(args?.targetSessionId);
243
+ const effectiveHistorySessionId = isRuntimeFallback
244
+ ? (pinnedProviderSessionId || undefined)
245
+ : candidateHistorySessionId;
246
+ return { isRuntimeFallback, pinnedProviderSessionId, effectiveHistorySessionId };
247
+ }
248
+
198
249
  function getHistorySessionId(h: CommandHelpers, args: any): string | undefined {
199
250
  const explicit = getExplicitHistorySessionId(args);
200
251
  if (explicit) return explicit;
@@ -1653,24 +1704,18 @@ export async function handleChatHistory(h: CommandHelpers, args: any): Promise<C
1653
1704
  ? (h.currentSession as any).workspace
1654
1705
  : undefined;
1655
1706
  // Same runtime-fallback poison guard as the subscribe / history-only
1656
- // paths: getHistorySessionId falls back to targetSessionId (the ADHDev
1657
- // id) for an agy coordinator, and the browser may also send that id back
1658
- // explicitly. Reading native history keyed on it can never exact-bind
1659
- // (it is not the on-disk conv uuid). Drop it here too so the pin /
1660
- // workspace-latest / owner-confirmed resolution engages instead of
1661
- // fail-closing to pty-parser. A real DISTINCT provider uuid is preserved.
1662
- const targetSidForHistory = typeof args?.targetSessionId === 'string' ? args.targetSessionId.trim() : '';
1663
- const explicitHistorySessionIdForHistory = getExplicitHistorySessionId(args);
1664
- const historySessionIdIsRuntimeFallback = Boolean(
1665
- targetSidForHistory
1666
- && isRuntimeFallbackHistorySessionId(historySessionId, targetSidForHistory)
1667
- && (!explicitHistorySessionIdForHistory
1668
- || isRuntimeFallbackHistorySessionId(explicitHistorySessionIdForHistory, targetSidForHistory)),
1669
- );
1670
- const pinnedProviderSessionIdForHistory = getBoundProviderSessionIdPin(args?.targetSessionId);
1671
- const effectiveHistorySessionId = historySessionIdIsRuntimeFallback
1672
- ? (pinnedProviderSessionIdForHistory || undefined)
1673
- : historySessionId;
1707
+ // paths (see resolveNativeHistoryReadSession): getHistorySessionId falls
1708
+ // back to targetSessionId (the ADHDev id) for an agy coordinator, and the
1709
+ // browser may also send that id back explicitly. Reading native history
1710
+ // keyed on it can never exact-bind (it is not the on-disk conv uuid). Drop
1711
+ // it here too so the pin / workspace-latest / owner-confirmed resolution
1712
+ // engages instead of fail-closing to pty-parser. A real DISTINCT provider
1713
+ // uuid is preserved.
1714
+ const {
1715
+ isRuntimeFallback: historySessionIdIsRuntimeFallback,
1716
+ pinnedProviderSessionId: pinnedProviderSessionIdForHistory,
1717
+ effectiveHistorySessionId,
1718
+ } = resolveNativeHistoryReadSession(args, historySessionId);
1674
1719
  const exactNativeHistoryScope = Boolean(
1675
1720
  (typeof args?.targetSessionId === 'string' && args.targetSessionId.trim())
1676
1721
  || (typeof args?.historySessionId === 'string' && args.historySessionId.trim() && !historySessionIdIsRuntimeFallback)
@@ -1921,7 +1966,8 @@ export async function handleReadChat(h: CommandHelpers, args: any): Promise<Comm
1921
1966
  let nativeHistory: any | null = null;
1922
1967
  let nativeHistoryError: unknown | undefined;
1923
1968
  if (supportsNative) {
1924
- // Runtime-fallback → pin substitution: nativeHistoryReadSessionId is
1969
+ // Runtime-fallback → pin substitution (see
1970
+ // resolveNativeHistoryReadSession): nativeHistoryReadSessionId is
1925
1971
  // the bare runtime/session id when no explicit provider handle was
1926
1972
  // supplied and none was parsed (antigravity takes no --session-id, so
1927
1973
  // its this.providerSessionId stays empty and getHistorySessionId falls
@@ -1933,23 +1979,10 @@ export async function handleReadChat(h: CommandHelpers, args: any): Promise<Comm
1933
1979
  // persisted across restart) over the runtime id, else drop the runtime
1934
1980
  // id so readCliProviderNativeHistory's pin / workspace-latest paths can
1935
1981
  // engage. Mirrors the handleChatHistory path's established handling.
1936
- const pinnedProviderSessionIdForRead = getBoundProviderSessionIdPin(targetSessionId);
1937
- // Runtime fallback whether the runtime id was reached via
1938
- // getHistorySessionId's internal fallback (empty args) OR the
1939
- // browser explicitly sent historySessionId === targetSessionId
1940
- // (the poisoned agy-coordinator read). Both must drop the
1941
- // runtime id so pin / live-bind resolution engages; only a real
1942
- // DISTINCT provider uuid stays as an exact-bind id.
1943
- const explicitHistorySessionIdForRead = getExplicitHistorySessionId(args);
1944
- const nativeReadSessionIdIsRuntimeFallback = Boolean(
1945
- targetSessionId
1946
- && isRuntimeFallbackHistorySessionId(nativeHistoryReadSessionId, targetSessionId)
1947
- && (!explicitHistorySessionIdForRead
1948
- || isRuntimeFallbackHistorySessionId(explicitHistorySessionIdForRead, targetSessionId)),
1949
- );
1950
- const effectiveNativeReadSessionId = nativeReadSessionIdIsRuntimeFallback
1951
- ? (pinnedProviderSessionIdForRead || undefined)
1952
- : nativeHistoryReadSessionId;
1982
+ const {
1983
+ pinnedProviderSessionId: pinnedProviderSessionIdForRead,
1984
+ effectiveHistorySessionId: effectiveNativeReadSessionId,
1985
+ } = resolveNativeHistoryReadSession(args, nativeHistoryReadSessionId);
1953
1986
  try {
1954
1987
  nativeHistory = readCliProviderNativeHistory(agentStr, {
1955
1988
  canonicalHistory: provider?.nativeHistory,
@@ -2353,30 +2386,18 @@ export async function handleReadChat(h: CommandHelpers, args: any): Promise<Comm
2353
2386
  // zero rows) even though the transcript is present in state.db. When
2354
2387
  // this is that runtime fallback (historySessionId === targetSid and no
2355
2388
  // explicit id was passed) and we hold a pin from an earlier bound
2356
- // read, prefer the pin so the query hits the real session.
2357
- const pinnedProviderSessionIdForHistory = getBoundProviderSessionIdPin(targetSid);
2358
- // Runtime fallback whether historySessionId reached targetSid via
2359
- // getHistorySessionId's internal fallback (empty args) OR the browser
2360
- // explicitly sent historySessionId === targetSid (the poisoned
2361
- // agy-coordinator subscription / D8 refreshAuthoritativeTail read).
2362
- // In both cases the runtime id is NOT a real provider conv uuid, so
2363
- // drop it and let pin / workspace-latest / owner-confirmed resolution
2364
- // run. A real DISTINCT provider uuid still exact-binds unchanged.
2365
- const explicitHistorySessionId = getExplicitHistorySessionId(args);
2366
- const historySessionIdIsRuntimeFallback = Boolean(
2367
- targetSid
2368
- && isRuntimeFallbackHistorySessionId(historySessionId, targetSid)
2369
- && (!explicitHistorySessionId
2370
- || isRuntimeFallbackHistorySessionId(explicitHistorySessionId, targetSid)),
2371
- );
2372
- // When this is the runtime fallback (not a real provider id): prefer
2373
- // the pin if we have one, else drop the runtime id entirely so the
2374
- // pin-reuse / workspace-latest logic inside readCliProviderNativeHistory
2375
- // can engage (passing the runtime id as historySessionId would pin the
2376
- // query to a non-existent session and never reach those paths).
2377
- const effectiveHistorySessionIdForRead = historySessionIdIsRuntimeFallback
2378
- ? (pinnedProviderSessionIdForHistory || undefined)
2379
- : historySessionId;
2389
+ // read, prefer the pin so the query hits the real session. Detects the
2390
+ // fallback whether historySessionId reached targetSid via
2391
+ // getHistorySessionId's internal fallback (empty args) or the browser
2392
+ // explicitly echoed targetSid back (poisoned agy-coordinator
2393
+ // subscription / D8 refreshAuthoritativeTail read); a real DISTINCT
2394
+ // provider uuid still exact-binds unchanged. See
2395
+ // resolveNativeHistoryReadSession.
2396
+ const {
2397
+ isRuntimeFallback: historySessionIdIsRuntimeFallback,
2398
+ pinnedProviderSessionId: pinnedProviderSessionIdForHistory,
2399
+ effectiveHistorySessionId: effectiveHistorySessionIdForRead,
2400
+ } = resolveNativeHistoryReadSession(args, historySessionId);
2380
2401
  const history = supportsNative
2381
2402
  ? readCliProviderNativeHistory(agentStr, {
2382
2403
  canonicalHistory: provider?.nativeHistory,
@@ -411,69 +411,15 @@ export const meshCrudHandlers: Record<string, MedFamilyHandler> = {
411
411
  }
412
412
  },
413
413
 
414
- // ─── MAGI panels (machine-local config, sibling to meshes) ───────────────
415
- // Panels live in ~/.adhdev/meshes.json `magiPanels` and are pure local config
416
- // (no mesh ownership). These three handlers mirror list_meshes/create_mesh/
417
- // update_mesh: dynamic-import the already-exported mesh-config accessors and
418
- // surface normalizeMagiPanel's structured error codes (invalid_magi_panel,
419
- // magi_panel_exists) verbatim so the dashboard can render them.
420
- //
421
- // Permission: magi_panel_set / magi_panel_remove are WRITE commands. They are
422
- // intentionally NOT listed in canPeerUsePrivilegedShareCommand (daemon-cloud
423
- // data-channel-router), so a peer holding ANY share permission hits its
424
- // `default → false` branch — identical owner-only gating to create_mesh /
425
- // update_mesh / list_meshes (none of which are listed there either). A trusted
426
- // peer (no permission = the owner) passes the top `!permission → true` guard.
427
- // Mirror, don't invent: do not add a new policy tier here.
428
- //
429
- // Resolvability (coupling / stale / available) is deliberately NOT computed
430
- // here: buildMagiFanoutPlan lives in mcp-server, unreachable from daemon-core.
431
- // magi_panel_list returns the raw definitions only; the dashboard derives
432
- // member resolvability client-side (web-core MagiPanelManager, reusing the
433
- // MagiGroupRow coupling logic) against live mesh_status.
434
- magi_panel_list: async (_ctx: MedFamilyContext, _args: any) => {
435
- try {
436
- const { listMagiPanels } = await import('../../config/mesh-config.js');
437
- return { success: true, panels: listMagiPanels() };
438
- } catch (e: any) {
439
- return { success: false, error: e.message };
440
- }
441
- },
442
-
443
- magi_panel_set: async (_ctx: MedFamilyContext, args: any) => {
444
- const name = typeof args?.name === 'string' ? args.name.trim() : '';
445
- if (!name) return { success: false, error: 'invalid_magi_panel: panel name is required' };
446
- try {
447
- const { upsertMagiPanel } = await import('../../config/mesh-config.js');
448
- // normalizeMagiPanel (invoked inside upsertMagiPanel) validates members,
449
- // enforces MAX_MAGI_PANEL_MEMBERS, and clamps replica counts. Its
450
- // invalid_magi_panel / magi_panel_exists messages flow back as `error`.
451
- const panel = upsertMagiPanel(name, args?.panel, { overwrite: args?.overwrite === true });
452
- return { success: true, name, panel };
453
- } catch (e: any) {
454
- // Surface the structured code (invalid_magi_panel: … / magi_panel_exists: …)
455
- // verbatim so the editor can map it to a field-level message.
456
- return { success: false, error: e.message };
457
- }
458
- },
459
-
460
- magi_panel_remove: async (_ctx: MedFamilyContext, args: any) => {
461
- const name = typeof args?.name === 'string' ? args.name.trim() : '';
462
- if (!name) return { success: false, error: 'invalid_magi_panel: panel name is required' };
463
- try {
464
- const { removeMagiPanel } = await import('../../config/mesh-config.js');
465
- const removed = removeMagiPanel(name);
466
- return { success: true, removed };
467
- } catch (e: any) {
468
- return { success: false, error: e.message };
469
- }
470
- },
471
-
472
414
  // ─── MAGI kind → panel bindings (MAGI-KIND-PANEL, machine-local config) ───
473
- // Per-task_kind slot lists in ~/.adhdev/meshes.json `magiKindPanels`. Same
474
- // owner-only gating and structured-error precedent as the magi_panel_* handlers
475
- // above (not listed in canPeerUsePrivilegedShareCommand owner-only). set/remove
476
- // are WRITE commands; list is read-only. normalizeMagiSlots (inside setMagiKindPanel)
415
+ // Per-task_kind slot lists in ~/.adhdev/meshes.json `magiKindPanels` — the SOLE
416
+ // MAGI panel-resolution surface (the former named-panel magi_panel_* handlers were
417
+ // removed). Owner-only gating: intentionally NOT listed in
418
+ // canPeerUsePrivilegedShareCommand (daemon-cloud data-channel-router), so a peer
419
+ // holding ANY share permission hits its `default → false` branch — identical
420
+ // owner-only gating to create_mesh / update_mesh / list_meshes. A trusted peer (no
421
+ // permission = the owner) passes the top `!permission → true` guard. set/remove are
422
+ // WRITE commands; list is read-only. normalizeMagiSlots (inside setMagiKindPanel)
477
423
  // surfaces invalid_magi_kind_panel: … messages verbatim for the editor.
478
424
  magi_kind_panel_list: async (_ctx: MedFamilyContext, _args: any) => {
479
425
  try {
@@ -22,7 +22,7 @@ import type {
22
22
  RepoMeshHostMetadata,
23
23
  RepoMeshDaemonRole,
24
24
  } from '../repo-mesh-types.js';
25
- import type { MagiPanel, MagiPanelMember, MagiPanelDefaultKind, MagiKindPanelMap, MagiSlot, MagiTaskKind } from '@adhdev/mesh-shared';
25
+ import type { MagiKindPanelMap, MagiSlot, MagiTaskKind } from '@adhdev/mesh-shared';
26
26
  import { mergeAndNormalizePolicy } from '../repo-mesh-types.js';
27
27
  import { createDefaultMeshHostMetadata } from '../mesh/mesh-host-ownership.js';
28
28
 
@@ -626,8 +626,10 @@ export function updateNode(
626
626
 
627
627
  // ─── MAGI Panels (machine-local cross-verification quorums) ──
628
628
 
629
- /** Hard cap on members per panel a sanity bound, not the per-invocation replica cap. */
630
- const MAX_MAGI_PANEL_MEMBERS = 24;
629
+ // NOTE: the named-panel model (normalizeMagiPanel / list / get / upsert / remove,
630
+ // stored under meshes.json `magiPanels`) was REMOVED. MAGI now resolves its fan-out
631
+ // slots SOLELY from the per-task_kind `magiKindPanels` binding below. `normalizeMagiSlots`
632
+ // is the sole slot normalizer.
631
633
 
632
634
  function normalizeReplicaCount(value: unknown): number | undefined {
633
635
  if (typeof value !== 'number' || !Number.isFinite(value)) return undefined;
@@ -635,138 +637,6 @@ function normalizeReplicaCount(value: unknown): number | undefined {
635
637
  return n >= 1 ? n : undefined;
636
638
  }
637
639
 
638
- /**
639
- * Normalize a panel `defaultKind` (the non-binding default output kind). Returns
640
- * undefined (drop, don't throw) for any absent / unknown value so a stray field
641
- * never blocks a panel write. 'freeform' is explicitly DROPPED with a warning: a
642
- * panel is a cross-verification tool and freeform contributes no structured claims
643
- * (claims:[]), so defaulting to it would silently zero out the very thing the panel
644
- * exists for. Only the evidence-bearing kinds (claim_audit / rca / design) survive.
645
- */
646
- function normalizeMagiPanelDefaultKind(raw: unknown): MagiPanelDefaultKind | undefined {
647
- if (raw == null) return undefined;
648
- const s = typeof raw === 'string' ? raw.trim().toLowerCase() : '';
649
- if (s === 'claim_audit' || s === 'rca' || s === 'design') return s;
650
- if (s === 'freeform') {
651
- // eslint-disable-next-line no-console
652
- console.warn(
653
- "[magi] panel defaultKind='freeform' rejected — freeform contributes no structured claims to cross-verification; dropping (use claim_audit / rca / design, or omit).",
654
- );
655
- return undefined;
656
- }
657
- // Any other value (typo / unsupported kind): drop silently — the panel still
658
- // resolves to the claim_audit fallback at review time.
659
- return undefined;
660
- }
661
-
662
- /**
663
- * Validate + normalize a panel config before persisting. Mirrors the node-config
664
- * normalization style (mesh-config addNode/updateNode): trims strings, drops
665
- * empties, requires a provider per member, clamps replica counts. Throws on
666
- * structurally invalid input so the calling tool returns a clear error rather than
667
- * writing a malformed panel.
668
- */
669
- export function normalizeMagiPanel(config: unknown): MagiPanel {
670
- if (!config || typeof config !== 'object' || Array.isArray(config)) {
671
- throw new Error('invalid_magi_panel: config must be an object');
672
- }
673
- const raw = config as Record<string, unknown>;
674
- const rawMembers = raw.members;
675
- if (!Array.isArray(rawMembers) || rawMembers.length === 0) {
676
- throw new Error('invalid_magi_panel: members must be a non-empty array');
677
- }
678
- if (rawMembers.length > MAX_MAGI_PANEL_MEMBERS) {
679
- throw new Error(`invalid_magi_panel: too many members (max ${MAX_MAGI_PANEL_MEMBERS})`);
680
- }
681
- const members: MagiPanelMember[] = rawMembers.map((entry, idx) => {
682
- if (!entry || typeof entry !== 'object' || Array.isArray(entry)) {
683
- throw new Error(`invalid_magi_panel: member[${idx}] must be an object`);
684
- }
685
- const m = entry as Record<string, unknown>;
686
- const provider = typeof m.provider === 'string' ? m.provider.trim() : '';
687
- if (!provider) {
688
- throw new Error(`invalid_magi_panel: member[${idx}].provider is required`);
689
- }
690
- const nodeId = typeof m.nodeId === 'string' && m.nodeId.trim() ? m.nodeId.trim() : undefined;
691
- const model = typeof m.model === 'string' && m.model.trim() ? m.model.trim() : undefined;
692
- const capabilityTags = normalizeCapabilityTags(m.capabilityTags);
693
- const n = normalizeReplicaCount(m.n);
694
- return {
695
- provider,
696
- ...(nodeId ? { nodeId } : {}),
697
- ...(model ? { model } : {}),
698
- ...(capabilityTags ? { capabilityTags } : {}),
699
- ...(n !== undefined ? { n } : {}),
700
- };
701
- });
702
- const description = typeof raw.description === 'string' && raw.description.trim()
703
- ? raw.description.trim().slice(0, 200)
704
- : undefined;
705
- const defaultN = normalizeReplicaCount(raw.defaultN);
706
- const defaultKind = normalizeMagiPanelDefaultKind(raw.defaultKind);
707
- return {
708
- ...(description ? { description } : {}),
709
- members,
710
- ...(defaultN !== undefined ? { defaultN } : {}),
711
- ...(defaultKind !== undefined ? { defaultKind } : {}),
712
- // dedupExempt is always meaningful for a MAGI panel (intentional same-prompt
713
- // fan-out). Persist it true unless the caller explicitly disables it.
714
- dedupExempt: raw.dedupExempt === false ? false : true,
715
- };
716
- }
717
-
718
- function normalizePanelName(name: unknown): string {
719
- const trimmed = typeof name === 'string' ? name.trim() : '';
720
- if (!trimmed) throw new Error('invalid_magi_panel: panel name is required');
721
- return trimmed.slice(0, 100);
722
- }
723
-
724
- /** All configured MAGI panels (machine-local), keyed by name. Empty when none. */
725
- export function listMagiPanels(): Record<string, MagiPanel> {
726
- return loadMeshConfig().magiPanels ?? {};
727
- }
728
-
729
- /** A single panel by name, or undefined when not configured. */
730
- export function getMagiPanel(name: string): MagiPanel | undefined {
731
- const key = typeof name === 'string' ? name.trim() : '';
732
- if (!key) return undefined;
733
- return loadMeshConfig().magiPanels?.[key];
734
- }
735
-
736
- /**
737
- * Upsert a named panel into meshes.json. Defaults to refusing to clobber an
738
- * existing panel (overwrite=false) — mirrors the mesh_init write/overwrite
739
- * precedent. Returns the normalized, persisted panel.
740
- */
741
- export function upsertMagiPanel(
742
- name: string,
743
- config: unknown,
744
- opts: { overwrite?: boolean } = {},
745
- ): MagiPanel {
746
- const key = normalizePanelName(name);
747
- const panel = normalizeMagiPanel(config);
748
- const stored = loadMeshConfig();
749
- const panels = stored.magiPanels ?? {};
750
- if (panels[key] && opts.overwrite !== true) {
751
- throw new Error(`magi_panel_exists: panel '${key}' already exists — pass overwrite=true to replace it`);
752
- }
753
- panels[key] = panel;
754
- stored.magiPanels = panels;
755
- saveMeshConfig(stored);
756
- return panel;
757
- }
758
-
759
- /** Remove a named panel. Returns true when a panel was removed. */
760
- export function removeMagiPanel(name: string): boolean {
761
- const key = typeof name === 'string' ? name.trim() : '';
762
- if (!key) return false;
763
- const stored = loadMeshConfig();
764
- if (!stored.magiPanels || !stored.magiPanels[key]) return false;
765
- delete stored.magiPanels[key];
766
- saveMeshConfig(stored);
767
- return true;
768
- }
769
-
770
640
  // ─── MAGI kind → panel bindings (MAGI-KIND-PANEL) ─────────
771
641
  //
772
642
  // Per-task_kind slot lists (machine-local, meshes.json `magiKindPanels`). A bare
@@ -788,11 +658,10 @@ function normalizeMagiTaskKindKey(raw: unknown): MagiTaskKind {
788
658
  }
789
659
 
790
660
  /**
791
- * Validate + normalize a kind-panel's slots. Mirrors normalizeMagiPanel's member
792
- * normalization: provider required per slot, trims strings, drops empties, clamps
793
- * replica counts, and additionally carries an optional per-slot `model`. Throws on
794
- * structurally invalid input (empty list / no provider) so the write returns a clear
795
- * error. Returns the normalized slot array.
661
+ * Validate + normalize a kind-panel's slots (the SOLE MAGI slot normalizer): provider
662
+ * required per slot, trims strings, drops empties, clamps replica counts, and carries
663
+ * an optional per-slot `model`. Throws on structurally invalid input (empty list / no
664
+ * provider) so the write returns a clear error. Returns the normalized slot array.
796
665
  */
797
666
  export function normalizeMagiSlots(slots: unknown): MagiSlot[] {
798
667
  if (!Array.isArray(slots) || slots.length === 0) {