@adhdev/daemon-core 0.9.82-rc.437 → 0.9.82-rc.439

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.
@@ -4,7 +4,13 @@
4
4
  * One-shot helper that onboards an existing git project into Repo Mesh:
5
5
  * 1. suggest + write `.adhdev/refine.json` (Refinery validation config)
6
6
  * 2. suggest + write `.adhdev/worktree_bootstrap.json` (worktree bootstrap config)
7
- * 3. recommend a node providerPriority from the installed CLI providers
7
+ * 3. suggest + write `.adhdev/change-impact.json` (change-impact classification)
8
+ * 4. recommend a node providerPriority from the installed CLI providers
9
+ *
10
+ * mesh_init now covers all THREE repo-committed `.adhdev/*` config families that
11
+ * carry a suggest→validate→write engine (refine + worktree_bootstrap +
12
+ * change-impact). `.adhdev/mesh.json` (coordinator prompt + operating notes) has
13
+ * its own dedicated export/write path (export_mesh_json_config / write_mesh_json_config).
8
14
  *
9
15
  * Design contract (matches the rest of the mesh tooling):
10
16
  * - Heuristics are suggestion/scaffold only. The written files are the
@@ -34,11 +40,21 @@ import {
34
40
  validateMeshWorktreeBootstrapConfig,
35
41
  type RepoMeshWorktreeBootstrapConfig,
36
42
  } from './worktree-bootstrap-config.js';
43
+ import {
44
+ CHANGE_IMPACT_CONFIG_LOCATIONS,
45
+ loadChangeImpactConfig,
46
+ suggestChangeImpactConfig,
47
+ validateChangeImpactConfig,
48
+ type ChangeImpactConfig,
49
+ } from '../git/change-impact-config.js';
50
+ import { listMagiKindPanels } from '../config/mesh-config.js';
51
+ import type { MagiKindPanelMap } from '@adhdev/mesh-shared';
37
52
  import type { CLIInfo } from '../detection/cli-detector.js';
38
53
 
39
54
  /** Canonical write targets — the first/preferred location for each config family. */
40
55
  export const MESH_INIT_REFINE_CONFIG_PATH = MESH_REFINE_CONFIG_LOCATIONS[0];
41
56
  export const MESH_INIT_WORKTREE_BOOTSTRAP_CONFIG_PATH = MESH_WORKTREE_BOOTSTRAP_CONFIG_LOCATIONS[0];
57
+ export const MESH_INIT_CHANGE_IMPACT_CONFIG_PATH = CHANGE_IMPACT_CONFIG_LOCATIONS[0];
42
58
 
43
59
  /**
44
60
  * Default lockfiles whose change should invalidate a 'ready' bootstrap.
@@ -61,7 +77,7 @@ export interface MeshInitConfigFileResult {
61
77
  written: boolean;
62
78
  /** Why it was not written (already_exists / not_suggested / no change). */
63
79
  skippedReason?: 'already_exists' | 'no_suggestion';
64
- config?: RepoMeshRefineConfig | RepoMeshWorktreeBootstrapConfig;
80
+ config?: RepoMeshRefineConfig | RepoMeshWorktreeBootstrapConfig | ChangeImpactConfig;
65
81
  }
66
82
 
67
83
  /**
@@ -158,16 +174,50 @@ export interface RunMeshInitOptions {
158
174
  overwrite?: boolean;
159
175
  }
160
176
 
177
+ /**
178
+ * A snapshot of the currently-saved config for each domain, so the coordinator can
179
+ * render a current-vs-suggested diff to the user before an init/reinit overwrite.
180
+ * This is READ-ONLY echo — every field is what is on disk / machine-local right now,
181
+ * never a suggestion. Absent domains are reported as `undefined` config.
182
+ */
183
+ export interface MeshInitCurrentConfigEcho {
184
+ /** Currently-saved `.adhdev/refine.json` (repo-committed) — undefined when absent/invalid. */
185
+ refine?: RepoMeshRefineConfig;
186
+ /** Currently-saved `.adhdev/worktree_bootstrap.json` (repo-committed). */
187
+ worktreeBootstrap?: RepoMeshWorktreeBootstrapConfig;
188
+ /** Currently-saved `.adhdev/change-impact.json` (repo-committed). */
189
+ changeImpact?: ChangeImpactConfig;
190
+ /** Per-domain source type so the coordinator can tell "absent" from "invalid". */
191
+ sourceTypes: {
192
+ refine: string;
193
+ worktreeBootstrap: string;
194
+ changeImpact: string;
195
+ };
196
+ /**
197
+ * Currently-configured MAGI kind→panel slot bindings (machine-local
198
+ * ~/.adhdev/meshes.json). Empty object when none configured. This is a
199
+ * machine-local echo — the coordinator labels it as such vs. the repo files.
200
+ */
201
+ magiKindPanels: MagiKindPanelMap;
202
+ }
203
+
161
204
  export interface RunMeshInitResult {
162
205
  success: true;
163
206
  workspace: string;
164
207
  dryRun: boolean;
165
208
  refine: MeshInitConfigFileResult;
166
209
  worktreeBootstrap: MeshInitConfigFileResult;
210
+ changeImpact: MeshInitConfigFileResult;
167
211
  providers: {
168
212
  providerPriority: string[];
169
213
  installedProviders: Array<{ id: string; displayName: string; version?: string }>;
170
214
  };
215
+ /**
216
+ * Read-only snapshot of the currently-saved config per domain (repo files +
217
+ * machine-local kind panels). Lets the coordinator present a current-vs-suggested
218
+ * diff to the user before any overwrite. Always populated (dry-run and write).
219
+ */
220
+ currentConfig: MeshInitCurrentConfigEcho;
171
221
  note: string;
172
222
  }
173
223
 
@@ -210,18 +260,58 @@ export function runMeshInit(
210
260
  overwrite,
211
261
  });
212
262
 
263
+ // change-impact classification (.adhdev/change-impact.json). suggest/validate/save
264
+ // mirror refine's engine exactly, so it drops into the same applyConfigSuggestion
265
+ // gate: existing-wins unless overwrite, dry-run by default, validated before write.
266
+ const changeImpactLoaded = loadChangeImpactConfig(workspace);
267
+ const changeImpact = applyConfigSuggestion({
268
+ workspace,
269
+ relativePath: MESH_INIT_CHANGE_IMPACT_CONFIG_PATH,
270
+ existing: changeImpactLoaded.sourceType === 'repo_file' ? changeImpactLoaded.config : undefined,
271
+ suggestedConfig: suggestChangeImpactConfig(workspace).suggestedConfig,
272
+ validate: (config) => validateChangeImpactConfig(config, MESH_INIT_CHANGE_IMPACT_CONFIG_PATH).valid,
273
+ write,
274
+ overwrite,
275
+ });
276
+
213
277
  const providers = suggestNodeProviderPriority(detected);
214
278
 
279
+ // Read-only echo of the currently-saved config per domain so the coordinator can
280
+ // present a current-vs-suggested diff before any overwrite (init vs reinit). This
281
+ // never suggests — it reports what is on disk / machine-local right now.
282
+ const refineLoaded = loadMeshRefineConfig(mesh, workspace);
283
+ const bootstrapLoaded = loadMeshWorktreeBootstrapConfig(mesh, workspace);
284
+ let magiKindPanels: MagiKindPanelMap = {};
285
+ try {
286
+ magiKindPanels = listMagiKindPanels();
287
+ } catch {
288
+ // Machine-local config unreadable — echo an empty binding rather than failing init.
289
+ magiKindPanels = {};
290
+ }
291
+ const currentConfig: MeshInitCurrentConfigEcho = {
292
+ refine: refineLoaded.config,
293
+ worktreeBootstrap: bootstrapLoaded.config,
294
+ changeImpact: changeImpactLoaded.sourceType === 'repo_file' ? changeImpactLoaded.config : undefined,
295
+ sourceTypes: {
296
+ refine: refineLoaded.sourceType,
297
+ worktreeBootstrap: bootstrapLoaded.sourceType,
298
+ changeImpact: changeImpactLoaded.sourceType,
299
+ },
300
+ magiKindPanels,
301
+ };
302
+
215
303
  return {
216
304
  success: true,
217
305
  workspace,
218
306
  dryRun: !write,
219
307
  refine,
220
308
  worktreeBootstrap,
309
+ changeImpact,
221
310
  providers,
311
+ currentConfig,
222
312
  note: write
223
- ? 'Configs written to disk are the execution source of truth; suggestions are scaffold and only take effect once saved. providerPriority is a recommendation — apply it to node policy via clone/policy update.'
224
- : 'Dry-run: no files written. Re-run with write=true to persist the suggested configs. Heuristic suggestions never execute until saved as repo config.',
313
+ ? 'Configs written to disk are the execution source of truth; suggestions are scaffold and only take effect once saved. providerPriority is a recommendation — apply it to node policy via clone/policy update. currentConfig echoes what was on disk before this run.'
314
+ : 'Dry-run: no files written. Re-run with write=true to persist the suggested configs. Heuristic suggestions never execute until saved as repo config. Use currentConfig to diff current-vs-suggested before overwriting.',
225
315
  };
226
316
  }
227
317
 
@@ -234,7 +324,7 @@ function applyConfigSuggestion(input: {
234
324
  workspace: string;
235
325
  relativePath: string;
236
326
  existing?: unknown;
237
- suggestedConfig?: RepoMeshRefineConfig | RepoMeshWorktreeBootstrapConfig;
327
+ suggestedConfig?: RepoMeshRefineConfig | RepoMeshWorktreeBootstrapConfig | ChangeImpactConfig;
238
328
  validate: (config: unknown) => boolean;
239
329
  write: boolean;
240
330
  overwrite: boolean;
@@ -1538,6 +1538,9 @@ async function maybeAutoLaunchOneQueueSession(components: DaemonComponents, mesh
1538
1538
  cliType: resolved.providerType,
1539
1539
  dir: node.workspace,
1540
1540
  settings: remoteSettings,
1541
+ // MAGI-KIND-PANEL model axis: forward the task's model override so the
1542
+ // remote worker session launches with it (initialModel). Best-effort.
1543
+ ...(typeof task.model === 'string' && task.model.trim() ? { initialModel: task.model.trim() } : {}),
1541
1544
  });
1542
1545
  } catch (e: any) {
1543
1546
  markAutoLaunch(meshId, task.id, { status: 'failed', reason: `remote_launch_dispatch_failed: ${e?.message || String(e)}`, nodeId, providerType: resolved.providerType });
@@ -1567,6 +1570,9 @@ async function maybeAutoLaunchOneQueueSession(components: DaemonComponents, mesh
1567
1570
  cliType: resolved.providerType,
1568
1571
  dir: node.workspace,
1569
1572
  settings: launchSettings,
1573
+ // MAGI-KIND-PANEL model axis: local launch forwards the task's model
1574
+ // override as initialModel (CLI → modelLaunchArgs; ACP → setConfigOption).
1575
+ ...(typeof task.model === 'string' && task.model.trim() ? { initialModel: task.model.trim() } : {}),
1570
1576
  });
1571
1577
  if (!launchResult?.success) {
1572
1578
  const reason = launchResult?.error || 'launch_cli_failed';
@@ -502,6 +502,14 @@ export interface MeshWorkQueueEntry {
502
502
  * replicas. Absent on ordinary tasks. Rides in the payload JSON (no column).
503
503
  */
504
504
  consensusGroupId?: string;
505
+ /**
506
+ * MAGI-KIND-PANEL (model axis): model override for the session that executes this
507
+ * task. When the task auto-launches a session, this is passed to launch_cli as
508
+ * `initialModel` (ACP → setConfigOption; CLI → modelLaunchArgs template). Absent on
509
+ * ordinary tasks. Rides in the payload JSON (no column). Best-effort — a provider
510
+ * that cannot honor the model still runs the task (never a fatal launch error).
511
+ */
512
+ model?: string;
505
513
  /**
506
514
  * M1: why this task is held back (e.g. "dependency_failed:<taskId>").
507
515
  * Only set by the system on dependency failure under the 'block' policy;
@@ -772,6 +780,8 @@ export function enqueueTask(
772
780
  missionId?: string;
773
781
  /** MAGI: consensus group id shared by every replica of a mesh_magi_review fan-out. */
774
782
  consensusGroupId?: string;
783
+ /** MAGI-KIND-PANEL: model override forwarded to the executing session's launch (initialModel). */
784
+ model?: string;
775
785
  /** Explicit task id for batch/template flows (M5). Random UUID when omitted. */
776
786
  id?: string;
777
787
  /** (3) Originating coordinator session id (for session-anchored completion routing). */
@@ -816,6 +826,7 @@ export function enqueueTask(
816
826
  ...(dependsOn.length > 0 ? { dependsOn } : {}),
817
827
  ...(typeof opts?.missionId === 'string' && opts.missionId.trim() ? { missionId: opts.missionId.trim() } : {}),
818
828
  ...(typeof opts?.consensusGroupId === 'string' && opts.consensusGroupId.trim() ? { consensusGroupId: opts.consensusGroupId.trim() } : {}),
829
+ ...(typeof opts?.model === 'string' && opts.model.trim() ? { model: opts.model.trim() } : {}),
819
830
  ...(typeof opts?.sourceCoordinatorSessionId === 'string' && opts.sourceCoordinatorSessionId.trim()
820
831
  ? { sourceCoordinatorSessionId: opts.sourceCoordinatorSessionId.trim() }
821
832
  : {}),
@@ -2813,7 +2813,13 @@ export class CliProviderInstance implements ProviderInstance {
2813
2813
  typeof data.providerSessionId === 'string' ? data.providerSessionId : '',
2814
2814
  );
2815
2815
  if (patchedProviderSessionId) {
2816
- this.promoteProviderSessionId(patchedProviderSessionId);
2816
+ // A provider-response id is authoritative when it carries an
2817
+ // explicit `new_session` marker (the CLI genuinely started a new
2818
+ // conversation). Without that marker it's just an observed id and
2819
+ // must not hijack an existing binding (see promoteProviderSessionId).
2820
+ this.promoteProviderSessionId(patchedProviderSessionId, {
2821
+ authoritative: data.sessionEvent === 'new_session',
2822
+ });
2817
2823
  }
2818
2824
 
2819
2825
  if (data.sessionEvent === 'new_session') {
@@ -3167,10 +3173,26 @@ export class CliProviderInstance implements ProviderInstance {
3167
3173
  return lines.join('\n');
3168
3174
  }
3169
3175
 
3170
- private promoteProviderSessionId(sessionId: string): void {
3176
+ private promoteProviderSessionId(sessionId: string, opts: { authoritative?: boolean } = {}): void {
3171
3177
  const nextSessionId = String(sessionId || '').trim();
3172
3178
  if (!nextSessionId || nextSessionId === this.providerSessionId) return;
3173
3179
 
3180
+ // Sticky binding: once this instance is bound to a provider session,
3181
+ // an *observed* id (one discovered from a status parse or the native
3182
+ // history reader) must NOT hijack the live binding. hermes ≥0.14
3183
+ // spawns a fresh `sessions` row per internal sub-session, so a
3184
+ // newest-wins native read surfaces a different id mid-turn on every
3185
+ // poll; accepting it would re-bind the instance, re-hydrate unbounded
3186
+ // history (daemon saturation) and reset completion detection so the
3187
+ // turn never finalizes. Only an *authoritative* change — the first
3188
+ // bind (no id yet) or an explicit provider `new_session`/resume — may
3189
+ // replace an existing binding. Legitimate resume/new-session paths
3190
+ // pass authoritative:true and are unaffected.
3191
+ if (this.providerSessionId && !opts.authoritative) {
3192
+ LOG.debug('CLI', `[${this.type}] ignoring non-authoritative session id ${nextSessionId} (bound to ${this.providerSessionId})`);
3193
+ return;
3194
+ }
3195
+
3174
3196
  const previousHistorySessionId = this.providerSessionId || this.instanceId;
3175
3197
  const previousProviderSessionId = this.providerSessionId;
3176
3198
  this.providerSessionId = nextSessionId;
@@ -594,6 +594,16 @@ export interface ProviderModule {
594
594
  /** Auto-implement spawn config — controls how this provider is invoked for autonomous script generation */
595
595
  autoImpl?: ProviderAutoImplSpawnConfig;
596
596
  };
597
+ /**
598
+ * MAGI-KIND-PANEL (model axis): template for expanding an `initialModel` selection
599
+ * into launch args for a CLI provider. `{{model}}` is substituted with the model
600
+ * string; e.g. `['--model', '{{model}}']` for claude-cli → `--model opus`. Applied
601
+ * at session launch when `initialModel` is passed AND this provider is a plain CLI
602
+ * (ACP providers instead route the model through setConfigOption). A CLI provider
603
+ * with no template silently ignores `initialModel` at launch (best-effort; a model
604
+ * request never fails a launch). Absent → no launch-time model selection for CLI.
605
+ */
606
+ modelLaunchArgs?: string[];
597
607
  /** Delay before submitting typed CLI input (provider-specific TUI tuning) */
598
608
  sendDelayMs?: number;
599
609
  /** Submit key used after typing into CLI PTY (default: carriage return) */
@@ -66,7 +66,7 @@ export function createNativeHistoryDispatcher(reader: ReaderId): (input: NativeH
66
66
  try { fs.statSync(sourcePath); } catch { /* best-effort metadata refresh */ }
67
67
  }
68
68
 
69
- const session = readByReader(reader, sourcePath, sessionId, workspace);
69
+ const session = readByReader(reader, sourcePath, sessionId, workspace, requestedProviderSid);
70
70
  if (!session) return null;
71
71
 
72
72
  if (requestedProviderSid && session.providerSessionId && session.providerSessionId !== requestedProviderSid) {
@@ -265,12 +265,18 @@ function readByReader(
265
265
  sourcePath: string,
266
266
  sessionId: string,
267
267
  workspace: string,
268
+ requestedProviderSid: string,
268
269
  ): any | null {
269
270
  switch (reader) {
270
271
  case 'claude-cli': return readClaudeCliSession(sourcePath);
271
272
  case 'codex-cli': return readCodexCliSession(sourcePath);
272
273
  case 'antigravity-cli': return readAntigravityCliSession(sourcePath, sessionId || undefined, workspace || undefined);
273
- case 'hermes-cli': return readHermesCliSession(sourcePath);
274
+ // hermes reads a *shared* state.db and would otherwise pick the newest
275
+ // source='cli' session, which drifts every read (hermes ≥0.14 writes a
276
+ // fresh row per internal sub-session). Pass the bound id so it reads
277
+ // THAT session directly instead of newest-wins. claude/codex resolve a
278
+ // per-session file upstream, so they need no equivalent pin here.
279
+ case 'hermes-cli': return readHermesCliSession(sourcePath, requestedProviderSid || undefined);
274
280
  }
275
281
  }
276
282
 
@@ -93,26 +93,42 @@ function loadMessagesForSession(db: any, sessionId: string): NativeHistoryMessag
93
93
  return out;
94
94
  }
95
95
 
96
- export function readSession(sessionPath: string): NativeHistorySession | null {
96
+ export function readSession(sessionPath: string, requestedSessionId?: string): NativeHistorySession | null {
97
97
  if (!sessionPath) return null;
98
98
 
99
- // Path mode A: SQLite — sourcePath is the state.db path. Pick the
100
- // newest source='cli' session that has at least one persisted message.
99
+ // Path mode A: SQLite — sourcePath is the state.db path.
101
100
  if (sessionPath === HERMES_STATE_DB) {
102
101
  const db = openDb();
103
102
  if (!db) return null;
104
103
  try {
105
- const row: any = db.prepare(
106
- `SELECT id, started_at FROM sessions
107
- WHERE source = 'cli' AND message_count > 0
108
- ORDER BY started_at DESC LIMIT 1`,
109
- ).get();
110
- if (!row) return null;
111
- const messages = loadMessagesForSession(db, row.id);
104
+ const pinned = String(requestedSessionId || '').trim();
105
+ let sessionId: string;
106
+ if (pinned) {
107
+ // Session pin: the caller is already bound to a specific
108
+ // provider session, so read THAT session directly instead of
109
+ // the newest-wins pick below. hermes ≥0.14 writes a fresh
110
+ // `sessions` row per internal sub-session, so an unpinned
111
+ // `ORDER BY started_at DESC LIMIT 1` drifts to a different id
112
+ // on every read → re-bind churn + reading completion evidence
113
+ // from the wrong session. loadMessagesForSession returning
114
+ // rows validates the id exists.
115
+ sessionId = pinned;
116
+ } else {
117
+ // No bound id yet (discovery): pick the newest source='cli'
118
+ // session that has at least one persisted message.
119
+ const row: any = db.prepare(
120
+ `SELECT id, started_at FROM sessions
121
+ WHERE source = 'cli' AND message_count > 0
122
+ ORDER BY started_at DESC LIMIT 1`,
123
+ ).get();
124
+ if (!row) return null;
125
+ sessionId = String(row.id);
126
+ }
127
+ const messages = loadMessagesForSession(db, sessionId);
112
128
  if (messages.length === 0) return null;
113
129
  return {
114
130
  messages,
115
- providerSessionId: String(row.id),
131
+ providerSessionId: sessionId,
116
132
  source: 'provider-native',
117
133
  sourcePath: sessionPath,
118
134
  sourceMtimeMs: statMtimeMs(sessionPath),
@@ -124,6 +124,11 @@
124
124
  "minimum": 0,
125
125
  "description": "Delay between pasting prompt text and pressing Enter."
126
126
  },
127
+ "modelLaunchArgs": {
128
+ "type": "array",
129
+ "items": { "type": "string" },
130
+ "description": "Template for expanding an initialModel selection into launch args. '{{model}}' is substituted with the model string (e.g. ['--model', '{{model}}'] → --model opus). Applied at launch when a model is requested for this CLI provider (MAGI kind-panel model axis). Absent → no launch-time model selection."
131
+ },
127
132
  "scriptCallBudgetMs": {
128
133
  "type": "integer",
129
134
  "minimum": 1,
@@ -263,35 +263,50 @@ function executeSqlite(src: NativeHistorySqliteSource, input: NativeHistoryInput
263
263
  catch { return null; }
264
264
 
265
265
  try {
266
- let sessionRow: any;
267
- try {
268
- // session_query may reference `?` to receive the session's
269
- // start-time floor in seconds (e.g. WHERE started_at >= ?).
270
- // That gives spec authors a robust way to keep prior-session
271
- // rows out of a fresh dashboard view without inventing their
272
- // own time arithmetic in SQL. When the caller didn't pass a
273
- // session floor (i.e. no live session is associated with the
274
- // call), we use 0 so spec queries that bind `?` still produce
275
- // a sane result rather than choking the whole executor.
276
- const sessionFloorSeconds = typeof input.sessionStartedAtMs === 'number'
277
- ? Math.floor(input.sessionStartedAtMs / 1000)
278
- : 0;
279
- const stmt = db.prepare(src.session_query);
266
+ const requested = input.providerSessionId || '';
267
+ let sessionId: string;
268
+ if (requested) {
269
+ // Session pin: the caller already bound this instance to a
270
+ // specific provider session, so read THAT session directly and
271
+ // skip the newest-wins `session_query` entirely. hermes ≥0.14
272
+ // spawns a fresh `sessions` row per internal sub-session, so the
273
+ // `ORDER BY started_at DESC LIMIT 1` pick drifts to a different
274
+ // id on every read. Left unpinned that churns the bound session
275
+ // (each re-bind re-hydrates unbounded history daemon
276
+ // saturation) and reads completion evidence from the wrong
277
+ // session (turn never finalizes). Binding straight to the
278
+ // requested id fixes both. Existence is validated below by the
279
+ // spec's own `message_query` returning rows for this id, so we
280
+ // don't hardcode any schema here.
281
+ sessionId = requested;
282
+ } else {
283
+ let sessionRow: any;
280
284
  try {
281
- sessionRow = stmt.get(sessionFloorSeconds);
282
- } catch {
283
- sessionRow = stmt.get();
284
- }
285
- } catch { return null; }
286
- if (!sessionRow) return null;
287
- // First column of the first row is the session id.
288
- const sessionIdRaw = Object.values(sessionRow)[0];
289
- const sessionId = sessionIdRaw == null ? '' : String(sessionIdRaw);
285
+ // session_query may reference `?` to receive the session's
286
+ // start-time floor in seconds (e.g. WHERE started_at >= ?).
287
+ // That gives spec authors a robust way to keep prior-session
288
+ // rows out of a fresh dashboard view without inventing their
289
+ // own time arithmetic in SQL. When the caller didn't pass a
290
+ // session floor (i.e. no live session is associated with the
291
+ // call), we use 0 so spec queries that bind `?` still produce
292
+ // a sane result rather than choking the whole executor.
293
+ const sessionFloorSeconds = typeof input.sessionStartedAtMs === 'number'
294
+ ? Math.floor(input.sessionStartedAtMs / 1000)
295
+ : 0;
296
+ const stmt = db.prepare(src.session_query);
297
+ try {
298
+ sessionRow = stmt.get(sessionFloorSeconds);
299
+ } catch {
300
+ sessionRow = stmt.get();
301
+ }
302
+ } catch { return null; }
303
+ if (!sessionRow) return null;
304
+ // First column of the first row is the session id.
305
+ const sessionIdRaw = Object.values(sessionRow)[0];
306
+ sessionId = sessionIdRaw == null ? '' : String(sessionIdRaw);
307
+ }
290
308
  if (!sessionId) return null;
291
309
 
292
- const requested = input.providerSessionId || '';
293
- if (requested && sessionId !== requested) return null;
294
-
295
310
  const messageRows: any[] = db.prepare(src.message_query).all(sessionId);
296
311
  if (!messageRows || messageRows.length === 0) return null;
297
312
 
@@ -14,7 +14,7 @@
14
14
  import type { GitRepoStatus, GitCompactSummary } from './git/git-types.js';
15
15
  import type { MeshMissionSummary, MeshMissionSlimSummary } from './mesh/mesh-missions.js';
16
16
  import type { MeshMagiActivitySummary } from './mesh/mesh-magi-status.js';
17
- import type { MagiPanelMap } from '@adhdev/mesh-shared';
17
+ import type { MagiPanelMap, MagiKindPanelMap } from '@adhdev/mesh-shared';
18
18
 
19
19
  // ─── Core Mesh Types ────────────────────────────
20
20
 
@@ -803,6 +803,14 @@ export interface LocalMeshConfig {
803
803
  * Optional: absent on configs written before MAGI existed.
804
804
  */
805
805
  magiPanels?: MagiPanelMap;
806
+ /**
807
+ * MAGI-KIND-PANEL: per-task_kind panel bindings (machine-local). Keyed by
808
+ * task_kind (rca / design / claim_audit / freeform); each maps to ≥1
809
+ * `(node × provider × model?)` slot. A `mesh_magi_review` invoked with a bare
810
+ * `task_kind` resolves its panel from here — an unconfigured kind is a hard
811
+ * error, never a synthesized fallback. Optional; absent on pre-feature configs.
812
+ */
813
+ magiKindPanels?: MagiKindPanelMap;
806
814
  }
807
815
 
808
816
  export interface LocalMeshEntry {