@adhdev/daemon-core 0.9.77-rc.49 → 0.9.77-rc.50

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.
@@ -17,6 +17,14 @@ export declare function triggerMeshQueue(components: DaemonComponents, meshId: s
17
17
  export declare function handleMeshForwardEvent(components: DaemonComponents, payload: Record<string, unknown>): {
18
18
  success: boolean;
19
19
  forwarded: number;
20
+ suppressed: boolean;
21
+ intentionalCleanupStop: boolean;
22
+ error?: undefined;
23
+ } | {
24
+ success: boolean;
25
+ forwarded: number;
26
+ suppressed?: undefined;
27
+ intentionalCleanupStop?: undefined;
20
28
  error?: undefined;
21
29
  } | {
22
30
  success: boolean;
@@ -24,6 +24,43 @@ export interface MeshLedgerEntry {
24
24
  providerType?: string;
25
25
  payload: Record<string, unknown>;
26
26
  }
27
+ export declare function isIntentionalCleanupStopEntry(entry: Pick<MeshLedgerEntry, 'kind' | 'payload'>): boolean;
28
+ export interface MeshTaskCompletionEvidence {
29
+ source: 'agent_status_event';
30
+ event: 'agent:generating_completed' | 'agent:ready';
31
+ nodeId: string;
32
+ sessionId: string;
33
+ providerType?: string;
34
+ completedAt: string;
35
+ transcriptHandle: {
36
+ kind: 'provider_session' | 'runtime_session';
37
+ sessionId: string;
38
+ providerSessionId?: string;
39
+ finalSummaryAvailable: boolean;
40
+ };
41
+ git: {
42
+ status: 'deferred';
43
+ reason: string;
44
+ };
45
+ validation: {
46
+ status: 'deferred';
47
+ commandsRun: string[];
48
+ reason: string;
49
+ };
50
+ checkpoint: {
51
+ attempted: false;
52
+ reason: 'not_attempted_for_ordinary_completion';
53
+ };
54
+ }
55
+ export interface BuildTaskCompletionEvidenceOptions {
56
+ event: MeshTaskCompletionEvidence['event'];
57
+ nodeId: string;
58
+ sessionId: string;
59
+ providerType?: string;
60
+ providerSessionId?: string;
61
+ finalSummary?: string;
62
+ completedAt?: string;
63
+ }
27
64
  export interface MeshLedgerSummary {
28
65
  meshId: string;
29
66
  totalEntries: number;
@@ -78,6 +115,7 @@ export interface AppendRemoteLedgerResult {
78
115
  }
79
116
  export declare const MAX_LEDGER_SLICE_LIMIT = 500;
80
117
  export declare function getLedgerDir(): string;
118
+ export declare function buildTaskCompletionEvidence(opts: BuildTaskCompletionEvidenceOptions): MeshTaskCompletionEvidence;
81
119
  /**
82
120
  * Append a new entry to the mesh ledger.
83
121
  * Handles file creation, rotation on size overflow, and atomic writes.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adhdev/daemon-core",
3
- "version": "0.9.77-rc.49",
3
+ "version": "0.9.77-rc.50",
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",
@@ -882,6 +882,36 @@ export class DaemonCommandRouter {
882
882
  return record?.lifecycle === 'stopped' || record?.lifecycle === 'failed' || record?.lifecycle === 'interrupted';
883
883
  }
884
884
 
885
+ private async recordIntentionalMeshSessionStop(args: {
886
+ meshId: string;
887
+ nodeId: string;
888
+ node: any;
889
+ sessionId: string;
890
+ mode: RepoMeshSessionCleanupMode;
891
+ source: 'mesh_cleanup_sessions' | 'mesh_remove_node';
892
+ action: 'stop_session' | 'delete_session_force';
893
+ }): Promise<void> {
894
+ try {
895
+ const { appendLedgerEntry } = await import('../mesh/mesh-ledger.js');
896
+ appendLedgerEntry(args.meshId, {
897
+ kind: 'session_stopped',
898
+ nodeId: args.nodeId,
899
+ sessionId: args.sessionId,
900
+ payload: {
901
+ intentional: true,
902
+ reason: 'operator_cleanup',
903
+ intentionalStopReason: 'operator_cleanup',
904
+ source: args.source,
905
+ cleanupMode: args.mode,
906
+ action: args.action,
907
+ workspace: typeof args.node?.workspace === 'string' ? args.node.workspace : undefined,
908
+ },
909
+ });
910
+ } catch (e: any) {
911
+ LOG.warn('MeshCleanup', `Failed to record intentional cleanup stop for ${args.sessionId}: ${e?.message || e}`);
912
+ }
913
+ }
914
+
885
915
  private async cleanupMeshSessions(args: {
886
916
  meshId: string;
887
917
  nodeId: string;
@@ -889,6 +919,7 @@ export class DaemonCommandRouter {
889
919
  mode: RepoMeshSessionCleanupMode;
890
920
  sessionIds?: string[];
891
921
  dryRun?: boolean;
922
+ source?: 'mesh_cleanup_sessions' | 'mesh_remove_node';
892
923
  }): Promise<{ success: boolean; [key: string]: unknown }> {
893
924
  if (args.mode === 'preserve') {
894
925
  return { success: true, mode: 'preserve', matchedCount: 0, stoppedSessionIds: [], deletedSessionIds: [], skippedSessionIds: [] };
@@ -908,6 +939,21 @@ export class DaemonCommandRouter {
908
939
  const deleteUnsupportedSessionIds: string[] = [];
909
940
  const recordsRemainSessionIds: string[] = [];
910
941
  const errors: Array<{ sessionId: string; error: string }> = [];
942
+ const cleanupSource = args.source || 'mesh_cleanup_sessions';
943
+ const markedIntentionalStopSessionIds = new Set<string>();
944
+ const markIntentionalStop = async (sessionId: string, action: 'stop_session' | 'delete_session_force') => {
945
+ if (args.dryRun || markedIntentionalStopSessionIds.has(sessionId)) return;
946
+ markedIntentionalStopSessionIds.add(sessionId);
947
+ await this.recordIntentionalMeshSessionStop({
948
+ meshId: args.meshId,
949
+ nodeId: args.nodeId,
950
+ node: args.node,
951
+ sessionId,
952
+ mode: args.mode,
953
+ source: cleanupSource,
954
+ action,
955
+ });
956
+ };
911
957
  const matchedBySurfaceKind = {
912
958
  live_runtime: 0,
913
959
  recovery_snapshot: 0,
@@ -932,7 +978,10 @@ export class DaemonCommandRouter {
932
978
  try {
933
979
  if (args.mode === 'stop') {
934
980
  if (!completed) {
935
- if (!args.dryRun) await this.deps.sessionHostControl.stopSession(sessionId);
981
+ if (!args.dryRun) {
982
+ await markIntentionalStop(sessionId, 'stop_session');
983
+ await this.deps.sessionHostControl.stopSession(sessionId);
984
+ }
936
985
  stoppedSessionIds.push(sessionId);
937
986
  } else {
938
987
  skippedSessionIds.push(sessionId);
@@ -951,6 +1000,7 @@ export class DaemonCommandRouter {
951
1000
  }
952
1001
 
953
1002
  if (args.mode === 'stop_and_delete') {
1003
+ if (!completed) await markIntentionalStop(sessionId, 'delete_session_force');
954
1004
  if (!args.dryRun) await this.deps.sessionHostControl.deleteSession(sessionId, { force: true });
955
1005
  deletedSessionIds.push(sessionId);
956
1006
  continue;
@@ -963,6 +1013,7 @@ export class DaemonCommandRouter {
963
1013
  recordsRemainSessionIds.push(sessionId);
964
1014
  if (args.mode === 'stop_and_delete' && !completed) {
965
1015
  try {
1016
+ await markIntentionalStop(sessionId, 'stop_session');
966
1017
  await this.deps.sessionHostControl.stopSession(sessionId);
967
1018
  stoppedSessionIds.push(sessionId);
968
1019
  } catch (stopError: any) {
@@ -1989,6 +2040,7 @@ export class DaemonCommandRouter {
1989
2040
  mode,
1990
2041
  sessionIds,
1991
2042
  dryRun: args?.dryRun === true,
2043
+ source: 'mesh_cleanup_sessions',
1992
2044
  });
1993
2045
  return result;
1994
2046
  } catch (e: any) {
@@ -2137,7 +2189,7 @@ export class DaemonCommandRouter {
2137
2189
  );
2138
2190
  let sessionCleanup: Record<string, unknown> | undefined;
2139
2191
  if (node && sessionCleanupMode !== 'preserve') {
2140
- sessionCleanup = await this.cleanupMeshSessions({ meshId, nodeId, node, mode: sessionCleanupMode });
2192
+ sessionCleanup = await this.cleanupMeshSessions({ meshId, nodeId, node, mode: sessionCleanupMode, source: 'mesh_remove_node' });
2141
2193
  if (sessionCleanup.success === false) return { success: false, removed: false, sessionCleanup };
2142
2194
  }
2143
2195
 
@@ -3,7 +3,7 @@ import { loadConfig } from '../config/config.js';
3
3
  import { getMesh, getMeshByRepo } from '../config/mesh-config.js';
4
4
  import { detectCLI } from '../detection/cli-detector.js';
5
5
  import { LOG } from '../logging/logger.js';
6
- import { appendLedgerEntry, getSessionRecoveryContext } from './mesh-ledger.js';
6
+ import { appendLedgerEntry, buildTaskCompletionEvidence, getSessionRecoveryContext, isIntentionalCleanupStopEntry, readLedgerEntries } from './mesh-ledger.js';
7
7
  import type { MeshLedgerKind, SessionRecoveryContext } from './mesh-ledger.js';
8
8
  import { claimNextTask, updateSessionTaskStatus, enqueueTask, updateTaskStatus, getQueue, recordTaskAutoLaunch } from './mesh-work-queue.js';
9
9
 
@@ -90,6 +90,46 @@ function getMeshWithCache(components: DaemonComponents, meshId: string): any | u
90
90
  return components.router?.getCachedInlineMesh(meshId);
91
91
  }
92
92
 
93
+ const INTENTIONAL_CLEANUP_STOP_SUPPRESSION_MS = 30 * 60 * 1000;
94
+
95
+ function isIntentionalCleanupStopMetadata(event: Record<string, unknown>): boolean {
96
+ return event.intentional === true
97
+ || event.intentionalStop === true
98
+ || event.operatorCleanup === true
99
+ || event.reason === 'operator_cleanup'
100
+ || event.stopReason === 'operator_cleanup'
101
+ || event.cleanupReason === 'operator_cleanup'
102
+ || event.source === 'mesh_cleanup_sessions'
103
+ || event.source === 'mesh_remove_node';
104
+ }
105
+
106
+ function hasRecentIntentionalCleanupStop(meshId: string, sessionId?: string, nodeId?: string): boolean {
107
+ if (!sessionId && !nodeId) return false;
108
+ const cutoff = Date.now() - INTENTIONAL_CLEANUP_STOP_SUPPRESSION_MS;
109
+ const entries = readLedgerEntries(meshId);
110
+ for (let i = entries.length - 1; i >= 0; i--) {
111
+ const entry = entries[i];
112
+ const timestamp = new Date(entry.timestamp).getTime();
113
+ if (!Number.isNaN(timestamp) && timestamp < cutoff) break;
114
+ if (!isIntentionalCleanupStopEntry(entry)) continue;
115
+ if (sessionId && entry.sessionId === sessionId) return true;
116
+ if (!sessionId && nodeId && entry.nodeId === nodeId) return true;
117
+ }
118
+ return false;
119
+ }
120
+
121
+ function shouldSuppressIntentionalCleanupStop(args: {
122
+ event: string;
123
+ meshId: string;
124
+ metadataEvent: Record<string, unknown>;
125
+ sessionId?: string;
126
+ nodeId?: string;
127
+ }): boolean {
128
+ if (args.event !== 'agent:stopped' && args.event !== 'monitor:long_generating') return false;
129
+ if (isIntentionalCleanupStopMetadata(args.metadataEvent)) return true;
130
+ return hasRecentIntentionalCleanupStop(args.meshId, args.sessionId, args.nodeId);
131
+ }
132
+
93
133
 
94
134
  export function tryAssignQueueTask(
95
135
  components: DaemonComponents,
@@ -526,6 +566,23 @@ function injectMeshSystemMessage(components: DaemonComponents, args: {
526
566
  event: string;
527
567
  metadataEvent: Record<string, unknown>;
528
568
  }) {
569
+ const eventSessionId = resolveEventSessionId(args.metadataEvent, args.sourceInstanceId);
570
+ const eventNodeId = readNonEmptyString(args.nodeId) || readNonEmptyString(args.metadataEvent.meshNodeId);
571
+ const intentionalCleanupStop = shouldSuppressIntentionalCleanupStop({
572
+ event: args.event,
573
+ meshId: args.meshId,
574
+ metadataEvent: args.metadataEvent,
575
+ sessionId: eventSessionId || undefined,
576
+ nodeId: eventNodeId || undefined,
577
+ });
578
+ if (intentionalCleanupStop) {
579
+ if (eventSessionId && eventNodeId) {
580
+ remoteIdleSessions.delete(`${eventNodeId}:${eventSessionId}`);
581
+ }
582
+ LOG.info('MeshEvents', `Suppressed ${args.event} for intentionally cleanup-stopped session ${eventSessionId || '(unknown session)'}`);
583
+ return { success: true, forwarded: 0, suppressed: true, intentionalCleanupStop: true };
584
+ }
585
+
529
586
  // ── Task Queue & Ledger ──
530
587
  let completedTaskForLedger: { id?: string } | null = null;
531
588
  if (args.event === 'agent:generating_completed') {
@@ -565,6 +622,14 @@ function injectMeshSystemMessage(components: DaemonComponents, args: {
565
622
  completedViaReady: true,
566
623
  providerSessionId: readNonEmptyString(args.metadataEvent.providerSessionId) || undefined,
567
624
  finalSummary: readNonEmptyString(args.metadataEvent.finalSummary) || undefined,
625
+ evidence: buildTaskCompletionEvidence({
626
+ event: 'agent:ready',
627
+ nodeId,
628
+ sessionId,
629
+ providerType: providerType || undefined,
630
+ providerSessionId: readNonEmptyString(args.metadataEvent.providerSessionId) || undefined,
631
+ finalSummary: readNonEmptyString(args.metadataEvent.finalSummary) || undefined,
632
+ }),
568
633
  },
569
634
  });
570
635
  } catch (e: any) {
@@ -601,17 +666,31 @@ function injectMeshSystemMessage(components: DaemonComponents, args: {
601
666
  const ledgerKind = EVENT_TO_LEDGER_KIND[args.event];
602
667
  if (ledgerKind) {
603
668
  try {
669
+ const ledgerNodeId = readNonEmptyString(args.nodeId) || readNonEmptyString(args.metadataEvent.meshNodeId) || undefined;
670
+ const ledgerSessionId = resolveEventSessionId(args.metadataEvent, args.sourceInstanceId) || undefined;
671
+ const ledgerProviderType = readNonEmptyString(args.metadataEvent.providerType) || undefined;
672
+ const completionEvidence = ledgerKind === 'task_completed' && ledgerNodeId && ledgerSessionId
673
+ ? buildTaskCompletionEvidence({
674
+ event: 'agent:generating_completed',
675
+ nodeId: ledgerNodeId,
676
+ sessionId: ledgerSessionId,
677
+ providerType: ledgerProviderType,
678
+ providerSessionId: readNonEmptyString(args.metadataEvent.providerSessionId) || undefined,
679
+ finalSummary: readNonEmptyString(args.metadataEvent.finalSummary) || undefined,
680
+ })
681
+ : undefined;
604
682
  appendLedgerEntry(args.meshId, {
605
683
  kind: ledgerKind,
606
- nodeId: readNonEmptyString(args.nodeId) || readNonEmptyString(args.metadataEvent.meshNodeId) || undefined,
607
- sessionId: resolveEventSessionId(args.metadataEvent, args.sourceInstanceId) || undefined,
608
- providerType: readNonEmptyString(args.metadataEvent.providerType) || undefined,
684
+ nodeId: ledgerNodeId,
685
+ sessionId: ledgerSessionId,
686
+ providerType: ledgerProviderType,
609
687
  payload: {
610
688
  event: args.event,
611
689
  nodeLabel: args.nodeLabel,
612
690
  taskId: completedTaskForLedger?.id || undefined,
613
691
  providerSessionId: readNonEmptyString(args.metadataEvent.providerSessionId) || undefined,
614
692
  finalSummary: readNonEmptyString(args.metadataEvent.finalSummary) || undefined,
693
+ evidence: completionEvidence,
615
694
  },
616
695
  });
617
696
  } catch (e: any) {
@@ -745,6 +824,13 @@ export function handleMeshForwardEvent(components: DaemonComponents, payload: Re
745
824
  providerType: readNonEmptyString(payload.providerType),
746
825
  providerSessionId: readNonEmptyString(payload.providerSessionId),
747
826
  finalSummary: readNonEmptyString(payload.finalSummary) || readNonEmptyString(payload.summary),
827
+ intentional: payload.intentional === true,
828
+ intentionalStop: payload.intentionalStop === true,
829
+ operatorCleanup: payload.operatorCleanup === true,
830
+ reason: readNonEmptyString(payload.reason),
831
+ stopReason: readNonEmptyString(payload.stopReason),
832
+ cleanupReason: readNonEmptyString(payload.cleanupReason),
833
+ source: readNonEmptyString(payload.source),
748
834
  },
749
835
  });
750
836
  }
@@ -49,6 +49,56 @@ export interface MeshLedgerEntry {
49
49
  payload: Record<string, unknown>;
50
50
  }
51
51
 
52
+ export function isIntentionalCleanupStopEntry(entry: Pick<MeshLedgerEntry, 'kind' | 'payload'>): boolean {
53
+ if (entry.kind !== 'session_stopped' && entry.kind !== 'task_failed' && entry.kind !== 'task_stalled') return false;
54
+ const payload = entry.payload && typeof entry.payload === 'object' && !Array.isArray(entry.payload)
55
+ ? entry.payload as Record<string, unknown>
56
+ : {};
57
+ return payload.intentional === true
58
+ && (payload.reason === 'operator_cleanup'
59
+ || payload.intentionalStopReason === 'operator_cleanup'
60
+ || payload.source === 'mesh_cleanup_sessions'
61
+ || payload.source === 'mesh_remove_node');
62
+ }
63
+
64
+ export interface MeshTaskCompletionEvidence {
65
+ source: 'agent_status_event';
66
+ event: 'agent:generating_completed' | 'agent:ready';
67
+ nodeId: string;
68
+ sessionId: string;
69
+ providerType?: string;
70
+ completedAt: string;
71
+ transcriptHandle: {
72
+ kind: 'provider_session' | 'runtime_session';
73
+ sessionId: string;
74
+ providerSessionId?: string;
75
+ finalSummaryAvailable: boolean;
76
+ };
77
+ git: {
78
+ status: 'deferred';
79
+ reason: string;
80
+ };
81
+ validation: {
82
+ status: 'deferred';
83
+ commandsRun: string[];
84
+ reason: string;
85
+ };
86
+ checkpoint: {
87
+ attempted: false;
88
+ reason: 'not_attempted_for_ordinary_completion';
89
+ };
90
+ }
91
+
92
+ export interface BuildTaskCompletionEvidenceOptions {
93
+ event: MeshTaskCompletionEvidence['event'];
94
+ nodeId: string;
95
+ sessionId: string;
96
+ providerType?: string;
97
+ providerSessionId?: string;
98
+ finalSummary?: string;
99
+ completedAt?: string;
100
+ }
101
+
52
102
  export interface MeshLedgerSummary {
53
103
  meshId: string;
54
104
  totalEntries: number;
@@ -138,6 +188,38 @@ function getRotatedPath(meshId: string, index: number): string {
138
188
 
139
189
  // ─── Core API ───────────────────────────────────
140
190
 
191
+ export function buildTaskCompletionEvidence(opts: BuildTaskCompletionEvidenceOptions): MeshTaskCompletionEvidence {
192
+ const providerSessionId = opts.providerSessionId?.trim() || undefined;
193
+ const providerType = opts.providerType?.trim() || undefined;
194
+ return {
195
+ source: 'agent_status_event',
196
+ event: opts.event,
197
+ nodeId: opts.nodeId,
198
+ sessionId: opts.sessionId,
199
+ providerType,
200
+ completedAt: opts.completedAt || new Date().toISOString(),
201
+ transcriptHandle: {
202
+ kind: providerSessionId ? 'provider_session' : 'runtime_session',
203
+ sessionId: opts.sessionId,
204
+ providerSessionId,
205
+ finalSummaryAvailable: typeof opts.finalSummary === 'string' && opts.finalSummary.trim().length > 0,
206
+ },
207
+ git: {
208
+ status: 'deferred',
209
+ reason: 'ordinary_completion_git_status_not_checked',
210
+ },
211
+ validation: {
212
+ status: 'deferred',
213
+ commandsRun: [],
214
+ reason: 'ordinary_completion_validation_not_run',
215
+ },
216
+ checkpoint: {
217
+ attempted: false,
218
+ reason: 'not_attempted_for_ordinary_completion',
219
+ },
220
+ };
221
+ }
222
+
141
223
  /**
142
224
  * Append a new entry to the mesh ledger.
143
225
  * Handles file creation, rotation on size overflow, and atomic writes.
@@ -345,13 +427,17 @@ export function getLedgerSummary(meshId: string): MeshLedgerSummary {
345
427
  case 'task_dispatched': summary.taskDispatched++; break;
346
428
  case 'task_completed': summary.taskCompleted++; break;
347
429
  case 'task_failed': {
430
+ if (isIntentionalCleanupStopEntry(entry)) break;
348
431
  summary.taskFailed++;
349
432
  if (new Date(entry.timestamp).getTime() >= recentFailureCutoff) {
350
433
  summary.recentFailures++;
351
434
  }
352
435
  break;
353
436
  }
354
- case 'task_stalled': summary.taskStalled++; break;
437
+ case 'task_stalled': {
438
+ if (!isIntentionalCleanupStopEntry(entry)) summary.taskStalled++;
439
+ break;
440
+ }
355
441
  case 'session_launched': summary.sessionLaunched++; break;
356
442
  case 'checkpoint_created': summary.checkpointCreated++; break;
357
443
  }
@@ -422,6 +508,7 @@ export function getSessionRecoveryContext(
422
508
  if (new Date(e.timestamp).getTime() < recentWindow) break;
423
509
  if (opts.nodeId && e.nodeId !== opts.nodeId) continue;
424
510
  if (e.kind === 'task_failed') {
511
+ if (isIntentionalCleanupStopEntry(e)) continue;
425
512
  consecutiveNodeFailures++;
426
513
  } else if (e.kind === 'task_completed' || e.kind === 'task_dispatched') {
427
514
  // A completion or new dispatch breaks the consecutive failure chain