@adhdev/daemon-core 0.9.82-rc.427 → 0.9.82-rc.429

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.
@@ -72,6 +72,26 @@ export interface RepoMeshNode {
72
72
  export type RepoMeshNodeHealth = 'online' | 'offline' | 'degraded' | 'dirty' | 'wrong_branch' | 'unknown';
73
73
  export type RepoMeshSessionCleanupMode = 'preserve' | 'stop' | 'delete_stopped' | 'stop_and_delete';
74
74
  export type RepoMeshSpawnedSessionVisibility = 'visible' | 'hidden';
75
+ /**
76
+ * What to do with the worker sessions a MAGI fan-out auto-launched, once the
77
+ * review's responses have been collected (terminal). MAGI dispatches each replica
78
+ * to an independent (node × provider); for a pinned target with no idle session
79
+ * the queue AUTO-LAUNCHES a fresh worker session. Those auto-launched sessions
80
+ * stay idle-LIVE after their turn (the CLI process is still running — `completed`
81
+ * means the task finished, not that the runtime exited), so repeated reviews leave
82
+ * a trail of idle live worker sessions cluttering the session list.
83
+ *
84
+ * 'stop_and_delete' (the default) force-stops AND deletes ONLY the sessions this
85
+ * fan-out auto-launched (verified by the per-session autoLaunchedForQueueTaskId
86
+ * marker — see cleanupMeshSessions). It is the only mode that covers the idle-LIVE
87
+ * case: delete_stopped skips live runtimes by contract, so it would no-op on the
88
+ * exact sessions we want gone. Reused idle sessions (no marker), the coordinator
89
+ * session, and any other node's sessions are NEVER touched.
90
+ *
91
+ * 'preserve' disables auto-cleanup entirely (leave every auto-launched worker
92
+ * session as-is for later inspection).
93
+ */
94
+ export type RepoMeshMagiSessionCleanupMode = 'preserve' | 'stop_and_delete';
75
95
  /**
76
96
  * Mesh-wide tie-break strategy for distributing untargeted queue work across
77
97
  * eligible nodes. This ONLY governs the final tie-break stage of the scheduler
@@ -220,6 +240,17 @@ export interface RepoMeshPolicy {
220
240
  * runtimes are never stopped/deleted unless the mesh owner opts in.
221
241
  */
222
242
  sessionCleanupOnNodeRemove?: RepoMeshSessionCleanupMode;
243
+ /**
244
+ * What to do with the worker sessions a MAGI fan-out auto-launched, once the
245
+ * review responses are collected (terminal). Defaults to 'stop_and_delete' so
246
+ * repeated mesh_magi_review calls don't accumulate idle-LIVE worker sessions.
247
+ * Only sessions THIS fan-out auto-launched are affected (marker-verified);
248
+ * reused/coordinator/other-node sessions are never touched. Set 'preserve' to
249
+ * leave auto-launched worker sessions for later inspection. A per-call
250
+ * auto_cleanup override on mesh_magi_review / mesh_magi_collect beats this.
251
+ * Accepts a boolean for convenience (true → stop_and_delete, false → preserve).
252
+ */
253
+ magiSessionCleanup?: RepoMeshMagiSessionCleanupMode | boolean;
223
254
  /**
224
255
  * Daemon-initiated fast-forward for idle clean nodes that are only behind
225
256
  * their tracked upstream. Defaults to enabled.
@@ -308,6 +339,36 @@ export interface RepoMeshNodePolicy {
308
339
  initSubmodulesOnClone?: boolean;
309
340
  }
310
341
  export declare const DEFAULT_MESH_POLICY: RepoMeshPolicy;
342
+ /**
343
+ * Resolve a magiSessionCleanup policy value (string mode, boolean shorthand,
344
+ * or unset) to a canonical mode. Unset → the default ('stop_and_delete', ON).
345
+ * boolean true → 'stop_and_delete', false → 'preserve'. Any unrecognized string
346
+ * falls back to the default so a typo can't silently disable auto-cleanup.
347
+ */
348
+ export declare function resolveMagiSessionCleanupMode(value: RepoMeshMagiSessionCleanupMode | boolean | undefined | null): RepoMeshMagiSessionCleanupMode;
349
+ export type MagiCleanupGateReason = 'auto_launch_marker_match' | 'auto_launch_marker_skip_coordinator_session' | 'auto_launch_marker_absent_session_not_auto_launched' | 'auto_launch_marker_mismatch';
350
+ /**
351
+ * Pure decision for the MAGI auto-cleanup marker gate. A session is eligible for
352
+ * MAGI auto-cleanup ONLY when its session-host record was auto-launched FOR the
353
+ * exact replica task the caller expects. This is the safety core: MAGI passes
354
+ * explicit session_ids (which bypass the self-coordinator / shared-daemon guards),
355
+ * so this marker check is the sole thing preventing a reused-idle, coordinator, or
356
+ * re-assigned session from being stopped/deleted.
357
+ *
358
+ * - coordinator session (meta.meshCoordinatorFor === meshId) → skip
359
+ * - no expected task id supplied for this session id → skip
360
+ * - record carries no autoLaunchedForQueueTaskId marker (reused idle) → skip
361
+ * - marker present but points at a DIFFERENT task (re-assignment) → skip
362
+ * - marker present and equals the expected task id → ALLOW
363
+ */
364
+ export declare function magiAutoLaunchedSessionCleanupDecision(args: {
365
+ recordMarker: string | undefined | null;
366
+ expectedTaskId: string | undefined | null;
367
+ isCoordinatorSession: boolean;
368
+ }): {
369
+ allow: boolean;
370
+ reason: MagiCleanupGateReason;
371
+ };
311
372
  /** Min/max bounds for the global write-task parallel cap. */
312
373
  export declare const MESH_MAX_PARALLEL_TASKS_MIN = 1;
313
374
  export declare const MESH_MAX_PARALLEL_TASKS_MAX = 8;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adhdev/daemon-core",
3
- "version": "0.9.82-rc.427",
3
+ "version": "0.9.82-rc.429",
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",
@@ -46,7 +46,7 @@
46
46
  "author": "vilmire",
47
47
  "license": "AGPL-3.0-or-later",
48
48
  "dependencies": {
49
- "@adhdev/mesh-shared": "0.9.82-rc.427",
49
+ "@adhdev/mesh-shared": "0.9.82-rc.429",
50
50
  "@adhdev/session-host-core": "*",
51
51
  "@agentclientprotocol/sdk": "^0.16.1",
52
52
  "ajv": "^8.20.0",
@@ -718,12 +718,21 @@ export class DaemonCliManager {
718
718
  // by now, so updateMeta reaches the session-host store). Best-effort; guarded.
719
719
  const launchMeshNodeId = typeof settings?.meshNodeId === 'string' ? settings.meshNodeId.trim() : '';
720
720
  const launchMeshNodeFor = typeof settings?.meshNodeFor === 'string' ? settings.meshNodeFor.trim() : '';
721
- if (launchMeshNodeId || launchMeshNodeFor) {
721
+ // Auto-launch attribution: the queue stamps the task id it auto-launched this
722
+ // worker FOR (autoLaunchedForQueueTaskId). Mirror it onto the record meta so a
723
+ // later cleanup (e.g. MAGI post-review auto-cleanup) can verify a session was
724
+ // auto-launched for a SPECIFIC replica task before stopping/deleting it — a reused
725
+ // idle session never carries this marker, so it is preserved.
726
+ const launchAutoLaunchedForQueueTaskId = typeof settings?.autoLaunchedForQueueTaskId === 'string'
727
+ ? settings.autoLaunchedForQueueTaskId.trim()
728
+ : '';
729
+ if (launchMeshNodeId || launchMeshNodeFor || launchAutoLaunchedForQueueTaskId) {
722
730
  try {
723
731
  cliInstance.getAdapter().updateRuntimeMeta?.({
724
732
  ...(launchMeshNodeId ? { meshNodeId: launchMeshNodeId } : {}),
725
733
  ...(launchMeshNodeFor ? { meshNodeFor: launchMeshNodeFor } : {}),
726
734
  ...(settings?.launchedByCoordinator === true ? { launchedByCoordinator: true } : {}),
735
+ ...(launchAutoLaunchedForQueueTaskId ? { autoLaunchedForQueueTaskId: launchAutoLaunchedForQueueTaskId } : {}),
727
736
  });
728
737
  } catch { /* best-effort — record-meta stamp is cleanup hygiene, not on the dispatch path */ }
729
738
  }
@@ -396,6 +396,15 @@ export const meshCrudHandlers: Record<string, MedFamilyHandler> = {
396
396
  const sessionIds = Array.isArray(args?.sessionIds)
397
397
  ? args.sessionIds.map((id: any) => typeof id === 'string' ? id.trim() : '').filter(Boolean)
398
398
  : undefined;
399
+ // MAGI post-review auto-cleanup routes through this same command with
400
+ // source:'magi_session_cleanup' and a per-session autoLaunchedForQueueTaskId
401
+ // map, which gates cleanup to sessions THIS fan-out actually auto-launched.
402
+ const source = args?.source === 'magi_session_cleanup' ? 'magi_session_cleanup' : 'mesh_cleanup_sessions';
403
+ const requireAutoLaunchedForTaskIds = (args?.requireAutoLaunchedForTaskIds
404
+ && typeof args.requireAutoLaunchedForTaskIds === 'object'
405
+ && !Array.isArray(args.requireAutoLaunchedForTaskIds))
406
+ ? args.requireAutoLaunchedForTaskIds as Record<string, string>
407
+ : undefined;
399
408
  const result = await ctx.cleanupMeshSessions({
400
409
  meshId,
401
410
  nodeId,
@@ -403,7 +412,8 @@ export const meshCrudHandlers: Record<string, MedFamilyHandler> = {
403
412
  mode,
404
413
  sessionIds,
405
414
  dryRun: args?.dryRun === true,
406
- source: 'mesh_cleanup_sessions',
415
+ source,
416
+ requireAutoLaunchedForTaskIds,
407
417
  });
408
418
  return result;
409
419
  } catch (e: any) {
@@ -84,7 +84,8 @@ export interface MedFamilyContext {
84
84
  mode: RepoMeshSessionCleanupMode;
85
85
  sessionIds?: string[];
86
86
  dryRun?: boolean;
87
- source?: 'mesh_cleanup_sessions' | 'mesh_remove_node';
87
+ source?: 'mesh_cleanup_sessions' | 'mesh_remove_node' | 'magi_session_cleanup';
88
+ requireAutoLaunchedForTaskIds?: Record<string, string>;
88
89
  }) => Promise<{ success: boolean; [key: string]: unknown }>;
89
90
 
90
91
  /** Bound `DaemonCommandRouter.cleanupLocalWorktreeNode`. */
@@ -35,7 +35,7 @@ import { analyzeMeshRefineNodeChangeArea, orderMeshRefineBatchNodes } from '../m
35
35
  import type { WorktreeBootstrapState } from '../mesh/worktree-bootstrap-config.js';
36
36
  import { getMeshQueueRevision } from '../mesh/mesh-work-queue.js';
37
37
  import type { RepoMeshSessionCleanupMode } from '../repo-mesh-types.js';
38
- import { DEFAULT_MESH_POLICY } from '../repo-mesh-types.js';
38
+ import { DEFAULT_MESH_POLICY, magiAutoLaunchedSessionCleanupDecision } from '../repo-mesh-types.js';
39
39
  import { resolve as pathResolve } from 'path';
40
40
  import * as fs from 'fs';
41
41
  import { execFileSync } from 'node:child_process';
@@ -1409,7 +1409,7 @@ export class DaemonCommandRouter {
1409
1409
  node: any;
1410
1410
  sessionId: string;
1411
1411
  mode: RepoMeshSessionCleanupMode;
1412
- source: 'mesh_cleanup_sessions' | 'mesh_remove_node';
1412
+ source: 'mesh_cleanup_sessions' | 'mesh_remove_node' | 'magi_session_cleanup';
1413
1413
  action: 'stop_session' | 'delete_session_force';
1414
1414
  }): Promise<void> {
1415
1415
  try {
@@ -1440,7 +1440,17 @@ export class DaemonCommandRouter {
1440
1440
  mode: RepoMeshSessionCleanupMode;
1441
1441
  sessionIds?: string[];
1442
1442
  dryRun?: boolean;
1443
- source?: 'mesh_cleanup_sessions' | 'mesh_remove_node';
1443
+ source?: 'mesh_cleanup_sessions' | 'mesh_remove_node' | 'magi_session_cleanup';
1444
+ /**
1445
+ * MAGI auto-cleanup safety gate: a map of sessionId → the queue task id that
1446
+ * session must have been AUTO-LAUNCHED for (record meta autoLaunchedForQueueTaskId).
1447
+ * When set, a matched explicit session is only acted on if its record carries that
1448
+ * exact marker. A reused idle session (no marker), the coordinator session, a
1449
+ * re-assigned session, or any session whose marker points at a DIFFERENT task is
1450
+ * skipped (reason 'auto_launch_marker_mismatch') — so MAGI never kills a session it
1451
+ * didn't itself spawn for this fan-out. Only consulted alongside explicit sessionIds.
1452
+ */
1453
+ requireAutoLaunchedForTaskIds?: Record<string, string>;
1444
1454
  }): Promise<{ success: boolean; [key: string]: unknown }> {
1445
1455
  if (args.mode === 'preserve') {
1446
1456
  return { success: true, mode: 'preserve', matchedCount: 0, stoppedSessionIds: [], deletedSessionIds: [], skippedSessionIds: [] };
@@ -1459,6 +1469,7 @@ export class DaemonCommandRouter {
1459
1469
  const skippedLiveSessionIds: string[] = [];
1460
1470
  const skippedCoordinatorSessionIds: string[] = [];
1461
1471
  const skippedLiveSessionReasons: Array<{ sessionId: string; reason: string }> = [];
1472
+ const skippedMarkerMismatchSessionIds: string[] = [];
1462
1473
  const actedLiveDelegateSessionIds: string[] = [];
1463
1474
  const deleteUnsupportedSessionIds: string[] = [];
1464
1475
  const recordsRemainSessionIds: string[] = [];
@@ -1511,6 +1522,28 @@ export class DaemonCommandRouter {
1511
1522
  skippedCoordinatorSessionIds.push(sessionId);
1512
1523
  continue;
1513
1524
  }
1525
+ // MAGI auto-cleanup marker gate. When the caller supplied a per-session
1526
+ // expected autoLaunchedForQueueTaskId, the session must (a) carry the marker
1527
+ // on its record meta AND (b) have it equal the expected replica task id.
1528
+ // This is the safety core for MAGI: explicit session_ids bypass the
1529
+ // self-coordinator / shared-daemon guards (hasExplicitSessionIds=true), so the
1530
+ // ONLY thing protecting a reused-idle / coordinator / re-assigned session here
1531
+ // is this marker check. Always-skip the coordinator session even when its id is
1532
+ // passed explicitly (it never carries an autoLaunchedForQueueTaskId marker, so
1533
+ // the mismatch branch already covers it, but be explicit for clarity).
1534
+ if (args.requireAutoLaunchedForTaskIds) {
1535
+ const decision = magiAutoLaunchedSessionCleanupDecision({
1536
+ recordMarker: readStringValue(record?.meta?.autoLaunchedForQueueTaskId),
1537
+ expectedTaskId: args.requireAutoLaunchedForTaskIds[sessionId],
1538
+ isCoordinatorSession: coordinatorSession,
1539
+ });
1540
+ if (!decision.allow) {
1541
+ skippedSessionIds.push(sessionId);
1542
+ skippedMarkerMismatchSessionIds.push(sessionId);
1543
+ skippedLiveSessionReasons.push({ sessionId, reason: decision.reason });
1544
+ continue;
1545
+ }
1546
+ }
1514
1547
  // Only the conservative shared-daemon guard for live sessions that are NOT a delegate
1515
1548
  // explicitly bound to this node. Delegate-bound live sessions fall through and are
1516
1549
  // stopped/deleted by the mode handlers below (which already record an intentional stop).
@@ -1622,6 +1655,7 @@ export class DaemonCommandRouter {
1622
1655
  skippedSessionIds,
1623
1656
  skippedLiveSessionIds,
1624
1657
  skippedCoordinatorSessionIds,
1658
+ ...(skippedMarkerMismatchSessionIds.length ? { skippedMarkerMismatchSessionIds } : {}),
1625
1659
  ...(actedLiveDelegateSessionIds.length ? { actedLiveDelegateSessionIds } : {}),
1626
1660
  ...(skippedLiveSessionReasons.length ? { skippedLiveSessionReasons } : {}),
1627
1661
  ...(deleteUnsupported ? {
@@ -22,7 +22,7 @@ import type {
22
22
  RepoMeshHostMetadata,
23
23
  RepoMeshDaemonRole,
24
24
  } from '../repo-mesh-types.js';
25
- import type { MagiPanel, MagiPanelMember } from '@adhdev/mesh-shared';
25
+ import type { MagiPanel, MagiPanelMember, MagiPanelDefaultKind } 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
 
@@ -597,6 +597,30 @@ function normalizeReplicaCount(value: unknown): number | undefined {
597
597
  return n >= 1 ? n : undefined;
598
598
  }
599
599
 
600
+ /**
601
+ * Normalize a panel `defaultKind` (the non-binding default output kind). Returns
602
+ * undefined (drop, don't throw) for any absent / unknown value so a stray field
603
+ * never blocks a panel write. 'freeform' is explicitly DROPPED with a warning: a
604
+ * panel is a cross-verification tool and freeform contributes no structured claims
605
+ * (claims:[]), so defaulting to it would silently zero out the very thing the panel
606
+ * exists for. Only the evidence-bearing kinds (claim_audit / rca / design) survive.
607
+ */
608
+ function normalizeMagiPanelDefaultKind(raw: unknown): MagiPanelDefaultKind | undefined {
609
+ if (raw == null) return undefined;
610
+ const s = typeof raw === 'string' ? raw.trim().toLowerCase() : '';
611
+ if (s === 'claim_audit' || s === 'rca' || s === 'design') return s;
612
+ if (s === 'freeform') {
613
+ // eslint-disable-next-line no-console
614
+ console.warn(
615
+ "[magi] panel defaultKind='freeform' rejected — freeform contributes no structured claims to cross-verification; dropping (use claim_audit / rca / design, or omit).",
616
+ );
617
+ return undefined;
618
+ }
619
+ // Any other value (typo / unsupported kind): drop silently — the panel still
620
+ // resolves to the claim_audit fallback at review time.
621
+ return undefined;
622
+ }
623
+
600
624
  /**
601
625
  * Validate + normalize a panel config before persisting. Mirrors the node-config
602
626
  * normalization style (mesh-config addNode/updateNode): trims strings, drops
@@ -639,10 +663,12 @@ export function normalizeMagiPanel(config: unknown): MagiPanel {
639
663
  ? raw.description.trim().slice(0, 200)
640
664
  : undefined;
641
665
  const defaultN = normalizeReplicaCount(raw.defaultN);
666
+ const defaultKind = normalizeMagiPanelDefaultKind(raw.defaultKind);
642
667
  return {
643
668
  ...(description ? { description } : {}),
644
669
  members,
645
670
  ...(defaultN !== undefined ? { defaultN } : {}),
671
+ ...(defaultKind !== undefined ? { defaultKind } : {}),
646
672
  // dedupExempt is always meaningful for a MAGI panel (intentional same-prompt
647
673
  // fan-out). Persist it true unless the caller explicitly disables it.
648
674
  dedupExempt: raw.dedupExempt === false ? false : true,
package/src/index.ts CHANGED
@@ -110,6 +110,7 @@ export type {
110
110
  RepoMeshNode,
111
111
  RepoMeshNodeHealth,
112
112
  RepoMeshPolicy,
113
+ RepoMeshMagiSessionCleanupMode,
113
114
  RepoMeshNodePolicy,
114
115
  RepoMeshRelatedRepo,
115
116
  RepoMeshNodeCapabilities,
@@ -142,6 +143,8 @@ export type {
142
143
  export {
143
144
  DEFAULT_MESH_POLICY,
144
145
  resolveDelegatedWorkerAutoApprove,
146
+ resolveMagiSessionCleanupMode,
147
+ magiAutoLaunchedSessionCleanupDecision,
145
148
  MESH_SCHEDULING_STRATEGIES,
146
149
  DEFAULT_MESH_SCHEDULING_STRATEGY,
147
150
  normalizeMeshSchedulingStrategy,
@@ -199,7 +202,7 @@ export type { CreateMeshOptions, UpdateMeshOptions, AddNodeOptions } from './con
199
202
  // leaf so the mcp-server — which depends only on @adhdev/daemon-core — can consume
200
203
  // them without taking a direct @adhdev/mesh-shared dependency).
201
204
  export type {
202
- MagiPanel, MagiPanelMember, MagiPanelMap, MagiMode,
205
+ MagiPanel, MagiPanelMember, MagiPanelMap, MagiMode, MagiTaskKind, MagiPanelDefaultKind,
203
206
  MagiClaim, MagiClaimStance, MagiAgentResponse,
204
207
  MagiResponseSource, MagiReplicaGitRef, MagiGitSkew, MagiSynthesizedResponse,
205
208
  MagiClusterCategory, MagiClusterMember, MagiClaimCluster, MagiSynthesis,
@@ -92,6 +92,27 @@ export type RepoMeshNodeHealth =
92
92
  export type RepoMeshSessionCleanupMode = 'preserve' | 'stop' | 'delete_stopped' | 'stop_and_delete';
93
93
  export type RepoMeshSpawnedSessionVisibility = 'visible' | 'hidden';
94
94
 
95
+ /**
96
+ * What to do with the worker sessions a MAGI fan-out auto-launched, once the
97
+ * review's responses have been collected (terminal). MAGI dispatches each replica
98
+ * to an independent (node × provider); for a pinned target with no idle session
99
+ * the queue AUTO-LAUNCHES a fresh worker session. Those auto-launched sessions
100
+ * stay idle-LIVE after their turn (the CLI process is still running — `completed`
101
+ * means the task finished, not that the runtime exited), so repeated reviews leave
102
+ * a trail of idle live worker sessions cluttering the session list.
103
+ *
104
+ * 'stop_and_delete' (the default) force-stops AND deletes ONLY the sessions this
105
+ * fan-out auto-launched (verified by the per-session autoLaunchedForQueueTaskId
106
+ * marker — see cleanupMeshSessions). It is the only mode that covers the idle-LIVE
107
+ * case: delete_stopped skips live runtimes by contract, so it would no-op on the
108
+ * exact sessions we want gone. Reused idle sessions (no marker), the coordinator
109
+ * session, and any other node's sessions are NEVER touched.
110
+ *
111
+ * 'preserve' disables auto-cleanup entirely (leave every auto-launched worker
112
+ * session as-is for later inspection).
113
+ */
114
+ export type RepoMeshMagiSessionCleanupMode = 'preserve' | 'stop_and_delete';
115
+
95
116
  /**
96
117
  * Mesh-wide tie-break strategy for distributing untargeted queue work across
97
118
  * eligible nodes. This ONLY governs the final tie-break stage of the scheduler
@@ -309,6 +330,17 @@ export interface RepoMeshPolicy {
309
330
  * runtimes are never stopped/deleted unless the mesh owner opts in.
310
331
  */
311
332
  sessionCleanupOnNodeRemove?: RepoMeshSessionCleanupMode;
333
+ /**
334
+ * What to do with the worker sessions a MAGI fan-out auto-launched, once the
335
+ * review responses are collected (terminal). Defaults to 'stop_and_delete' so
336
+ * repeated mesh_magi_review calls don't accumulate idle-LIVE worker sessions.
337
+ * Only sessions THIS fan-out auto-launched are affected (marker-verified);
338
+ * reused/coordinator/other-node sessions are never touched. Set 'preserve' to
339
+ * leave auto-launched worker sessions for later inspection. A per-call
340
+ * auto_cleanup override on mesh_magi_review / mesh_magi_collect beats this.
341
+ * Accepts a boolean for convenience (true → stop_and_delete, false → preserve).
342
+ */
343
+ magiSessionCleanup?: RepoMeshMagiSessionCleanupMode | boolean;
312
344
  /**
313
345
  * Daemon-initiated fast-forward for idle clean nodes that are only behind
314
346
  * their tracked upstream. Defaults to enabled.
@@ -414,6 +446,11 @@ export const DEFAULT_MESH_POLICY: RepoMeshPolicy = {
414
446
  spawnedSessionVisibility: 'hidden',
415
447
  delegatedWorkerAutoApprove: true,
416
448
  sessionCleanupOnNodeRemove: 'preserve',
449
+ // MAGI auto-launches a worker session per pinned replica target with no idle
450
+ // session; those stay idle-LIVE after their turn. Default ON (stop_and_delete)
451
+ // so repeated reviews don't pile up idle worker sessions. Only marker-verified
452
+ // auto-launched sessions are cleaned — see RepoMeshMagiSessionCleanupMode.
453
+ magiSessionCleanup: 'stop_and_delete',
417
454
  autoFastForward: { enabled: true },
418
455
  maxTaskRetries: 1,
419
456
  };
@@ -430,6 +467,56 @@ export const DEFAULT_MESH_POLICY: RepoMeshPolicy = {
430
467
  const SESSION_CLEANUP_MODES = new Set<RepoMeshSessionCleanupMode>([
431
468
  'preserve', 'stop', 'delete_stopped', 'stop_and_delete',
432
469
  ]);
470
+
471
+ /**
472
+ * Resolve a magiSessionCleanup policy value (string mode, boolean shorthand,
473
+ * or unset) to a canonical mode. Unset → the default ('stop_and_delete', ON).
474
+ * boolean true → 'stop_and_delete', false → 'preserve'. Any unrecognized string
475
+ * falls back to the default so a typo can't silently disable auto-cleanup.
476
+ */
477
+ export function resolveMagiSessionCleanupMode(
478
+ value: RepoMeshMagiSessionCleanupMode | boolean | undefined | null,
479
+ ): RepoMeshMagiSessionCleanupMode {
480
+ if (value === undefined || value === null) return DEFAULT_MESH_POLICY.magiSessionCleanup as RepoMeshMagiSessionCleanupMode;
481
+ if (typeof value === 'boolean') return value ? 'stop_and_delete' : 'preserve';
482
+ return value === 'preserve' || value === 'stop_and_delete'
483
+ ? value
484
+ : (DEFAULT_MESH_POLICY.magiSessionCleanup as RepoMeshMagiSessionCleanupMode);
485
+ }
486
+
487
+ export type MagiCleanupGateReason =
488
+ | 'auto_launch_marker_match'
489
+ | 'auto_launch_marker_skip_coordinator_session'
490
+ | 'auto_launch_marker_absent_session_not_auto_launched'
491
+ | 'auto_launch_marker_mismatch';
492
+
493
+ /**
494
+ * Pure decision for the MAGI auto-cleanup marker gate. A session is eligible for
495
+ * MAGI auto-cleanup ONLY when its session-host record was auto-launched FOR the
496
+ * exact replica task the caller expects. This is the safety core: MAGI passes
497
+ * explicit session_ids (which bypass the self-coordinator / shared-daemon guards),
498
+ * so this marker check is the sole thing preventing a reused-idle, coordinator, or
499
+ * re-assigned session from being stopped/deleted.
500
+ *
501
+ * - coordinator session (meta.meshCoordinatorFor === meshId) → skip
502
+ * - no expected task id supplied for this session id → skip
503
+ * - record carries no autoLaunchedForQueueTaskId marker (reused idle) → skip
504
+ * - marker present but points at a DIFFERENT task (re-assignment) → skip
505
+ * - marker present and equals the expected task id → ALLOW
506
+ */
507
+ export function magiAutoLaunchedSessionCleanupDecision(args: {
508
+ recordMarker: string | undefined | null;
509
+ expectedTaskId: string | undefined | null;
510
+ isCoordinatorSession: boolean;
511
+ }): { allow: boolean; reason: MagiCleanupGateReason } {
512
+ const marker = typeof args.recordMarker === 'string' ? args.recordMarker.trim() : '';
513
+ const expected = typeof args.expectedTaskId === 'string' ? args.expectedTaskId.trim() : '';
514
+ if (args.isCoordinatorSession) return { allow: false, reason: 'auto_launch_marker_skip_coordinator_session' };
515
+ if (!expected) return { allow: false, reason: 'auto_launch_marker_mismatch' };
516
+ if (!marker) return { allow: false, reason: 'auto_launch_marker_absent_session_not_auto_launched' };
517
+ if (marker !== expected) return { allow: false, reason: 'auto_launch_marker_mismatch' };
518
+ return { allow: true, reason: 'auto_launch_marker_match' };
519
+ }
433
520
  const SPAWNED_SESSION_VISIBILITY_MODES = new Set<RepoMeshSpawnedSessionVisibility>([
434
521
  'visible', 'hidden',
435
522
  ]);
@@ -526,6 +613,8 @@ export function mergeAndNormalizePolicy(
526
613
  if (!SESSION_CLEANUP_MODES.has(policy.sessionCleanupOnNodeRemove as RepoMeshSessionCleanupMode)) {
527
614
  policy.sessionCleanupOnNodeRemove = 'preserve';
528
615
  }
616
+ // Canonicalize magiSessionCleanup (accepts boolean shorthand / unset → default ON).
617
+ policy.magiSessionCleanup = resolveMagiSessionCleanupMode(policy.magiSessionCleanup);
529
618
  if (!SPAWNED_SESSION_VISIBILITY_MODES.has(policy.spawnedSessionVisibility as RepoMeshSpawnedSessionVisibility)) {
530
619
  policy.spawnedSessionVisibility = DEFAULT_MESH_POLICY.spawnedSessionVisibility;
531
620
  }