@adhdev/daemon-core 0.9.77-rc.4 → 0.9.77-rc.41

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.
@@ -8,17 +8,12 @@ export interface PendingMeshCoordinatorEvent {
8
8
  }
9
9
  /** Drain and return all pending coordinator events, clearing the queue. */
10
10
  export declare function drainPendingMeshCoordinatorEvents(): PendingMeshCoordinatorEvent[];
11
- export declare function tryAssignQueueTask(components: {
12
- cliManager: any;
13
- }, meshId: string, nodeId: string, sessionId: string, providerType: string): boolean;
11
+ export declare function tryAssignQueueTask(components: DaemonComponents, meshId: string, nodeId: string, sessionId: string, providerType: string): boolean;
14
12
  /**
15
13
  * Triggers a queue check for all nodes in the mesh.
16
14
  * Called when a new task is enqueued, in case nodes are already idle.
17
15
  */
18
- export declare function triggerMeshQueue(components: {
19
- instanceManager: any;
20
- cliManager: any;
21
- }, meshId: string): void;
16
+ export declare function triggerMeshQueue(components: DaemonComponents, meshId: string): void;
22
17
  export declare function handleMeshForwardEvent(components: DaemonComponents, payload: Record<string, unknown>): {
23
18
  success: boolean;
24
19
  forwarded: number;
@@ -1,4 +1,4 @@
1
- export type MeshTaskStatus = 'pending' | 'assigned' | 'completed' | 'failed';
1
+ export type MeshTaskStatus = 'pending' | 'assigned' | 'completed' | 'failed' | 'cancelled';
2
2
  export interface MeshWorkQueueEntry {
3
3
  id: string;
4
4
  meshId: string;
@@ -6,10 +6,19 @@ export interface MeshWorkQueueEntry {
6
6
  status: MeshTaskStatus;
7
7
  /** If specified, only this node can claim the task (used by legacy mesh_send_task) */
8
8
  targetNodeId?: string;
9
+ /** If specified, only this runtime session can claim the task */
10
+ targetSessionId?: string;
9
11
  /** The node that actually claimed and is executing the task */
10
12
  assignedNodeId?: string;
11
13
  /** The session currently executing the task */
12
14
  assignedSessionId?: string;
15
+ /** Human/operator reason for terminal cancellation. */
16
+ cancelReason?: string;
17
+ cancelledAt?: string;
18
+ /** Human/operator reason for manually requeueing a task. */
19
+ requeueReason?: string;
20
+ requeuedAt?: string;
21
+ requeueCount?: number;
13
22
  createdAt: string;
14
23
  updatedAt: string;
15
24
  }
@@ -18,6 +27,7 @@ export interface MeshWorkQueueEntry {
18
27
  */
19
28
  export declare function enqueueTask(meshId: string, message: string, opts?: {
20
29
  targetNodeId?: string;
30
+ targetSessionId?: string;
21
31
  }): MeshWorkQueueEntry;
22
32
  /**
23
33
  * Get all tasks in the queue, optionally filtered by status.
@@ -34,6 +44,23 @@ export declare function claimNextTask(meshId: string, nodeId: string, sessionId:
34
44
  * Used when a session completes, fails, or stalls.
35
45
  */
36
46
  export declare function updateTaskStatus(meshId: string, taskId: string, status: MeshTaskStatus): MeshWorkQueueEntry | null;
47
+ /**
48
+ * Mark a queue task as manually cancelled without deleting audit history.
49
+ */
50
+ export declare function cancelTask(meshId: string, taskId: string, opts?: {
51
+ reason?: string;
52
+ }): MeshWorkQueueEntry | null;
53
+ /**
54
+ * Return a queue task to pending for retry. By default, dead session targeting
55
+ * and assigned ownership are cleared so stale assignments do not strand again.
56
+ */
57
+ export declare function requeueTask(meshId: string, taskId: string, opts?: {
58
+ reason?: string;
59
+ targetNodeId?: string;
60
+ targetSessionId?: string;
61
+ clearTargetNode?: boolean;
62
+ clearTargetSession?: boolean;
63
+ }): MeshWorkQueueEntry | null;
37
64
  /**
38
65
  * Update the status of the task currently assigned to a specific session.
39
66
  */
@@ -43,6 +70,13 @@ export interface MeshWorkQueueStats {
43
70
  assigned: number;
44
71
  completed: number;
45
72
  failed: number;
73
+ cancelled: number;
74
+ activeAssignments: Array<{
75
+ id: string;
76
+ nodeId?: string;
77
+ sessionId?: string;
78
+ message: string;
79
+ }>;
46
80
  }
47
81
  /**
48
82
  * Return aggregate queue statistics for the given mesh.
@@ -297,6 +297,13 @@ export interface SessionEntry {
297
297
  assigned: number;
298
298
  completed: number;
299
299
  failed: number;
300
+ cancelled?: number;
301
+ activeAssignments?: Array<{
302
+ id: string;
303
+ nodeId?: string;
304
+ sessionId?: string;
305
+ message: string;
306
+ }>;
300
307
  };
301
308
  }
302
309
  /**
@@ -341,6 +348,13 @@ export interface CompactSessionEntry {
341
348
  assigned: number;
342
349
  completed: number;
343
350
  failed: number;
351
+ cancelled?: number;
352
+ activeAssignments?: Array<{
353
+ id: string;
354
+ nodeId?: string;
355
+ sessionId?: string;
356
+ message: string;
357
+ }>;
344
358
  };
345
359
  }
346
360
  export type VersionUpdateReason = 'force_update_below' | 'major_minor_mismatch' | 'patch_mismatch' | 'daemon_ahead';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adhdev/daemon-core",
3
- "version": "0.9.77-rc.4",
3
+ "version": "0.9.77-rc.41",
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",
@@ -83,6 +83,9 @@ export interface DaemonInitConfig {
83
83
 
84
84
  /** Fired before send_chat is dispatched — used for turn snapshot hooks */
85
85
  onBeforeSendChat?: (params: { workspace: string; sessionId: string }) => void;
86
+
87
+ /** Relays a command to a remote mesh node daemon */
88
+ dispatchMeshCommand?: (daemonId: string, command: string, args: Record<string, unknown>) => Promise<any>;
86
89
  }
87
90
 
88
91
  // ─── Result ───
@@ -100,6 +103,7 @@ export interface DaemonComponents {
100
103
  sessionRegistry: SessionRegistry;
101
104
  detectedIdes: { value: IDEInfo[] };
102
105
  refreshProviderAvailability: (providerType?: string) => Promise<void>;
106
+ dispatchMeshCommand?: (daemonId: string, command: string, args: Record<string, unknown>) => Promise<any>;
103
107
  }
104
108
 
105
109
  export interface DaemonDevSupportOptions {
@@ -331,6 +335,7 @@ export async function initDaemonComponents(config: DaemonInitConfig): Promise<Da
331
335
  sessionRegistry,
332
336
  detectedIdes: detectedIdesRef,
333
337
  refreshProviderAvailability,
338
+ dispatchMeshCommand: config.dispatchMeshCommand,
334
339
  };
335
340
 
336
341
  // 11. Setup Mesh Event Forwarding
@@ -195,6 +195,8 @@ export class ProviderCliAdapter implements CliAdapter {
195
195
 
196
196
  // ─── CLI Scripts (script-based parsing) ───
197
197
  private cliScripts: CliScripts;
198
+ /** Per-session opaque state object created by cliScripts.createState(), reset on stop. */
199
+ private scriptState: unknown = null;
198
200
  private runtimeSettings: Record<string, any> = {};
199
201
  /** Full accumulated rendered PTY transcript for parser/readback use */
200
202
  private accumulatedBuffer: string = '';
@@ -223,6 +225,7 @@ export class ProviderCliAdapter implements CliAdapter {
223
225
  currentTurnScope: TurnParseScope | null;
224
226
  recentOutputBuffer: string;
225
227
  accumulatedBuffer: string;
228
+ accumulatedRawBufferKey: string;
226
229
  screenText: string;
227
230
  currentStatus: CliSessionStatus['status'];
228
231
  activeModal: { message: string; buttons: string[] } | null;
@@ -297,14 +300,23 @@ export class ProviderCliAdapter implements CliAdapter {
297
300
  this.lastScreenSnapshotReadAt = Number.NEGATIVE_INFINITY;
298
301
  }
299
302
 
303
+ private getAccumulatedRawBufferCacheKey(): string {
304
+ return this.accumulatedRawBuffer
305
+ .replace(/\x1B\][^\x07]*(?:\x07|\x1B\\)/g, '')
306
+ .replace(/\x1B[P^_X][\s\S]*?(?:\x07|\x1B\\)/g, '')
307
+ .replace(/\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])/g, '');
308
+ }
309
+
300
310
  private getFreshParsedStatusCache(): any | null {
301
311
  const cached = this.parsedStatusCache;
312
+ const accumulatedRawBufferKey = this.getAccumulatedRawBufferCacheKey();
302
313
  if (
303
314
  cached
304
315
  && cached.responseBuffer === this.responseBuffer
305
316
  && cached.currentTurnScope === this.currentTurnScope
306
317
  && cached.recentOutputBuffer === this.recentOutputBuffer
307
318
  && cached.accumulatedBuffer === this.accumulatedBuffer
319
+ && cached.accumulatedRawBufferKey === accumulatedRawBufferKey
308
320
  && cached.screenText === this.lastScreenText
309
321
  && cached.currentStatus === this.currentStatus
310
322
  && cached.activeModal === this.activeModal
@@ -477,6 +489,9 @@ export class ProviderCliAdapter implements CliAdapter {
477
489
  this.cliScripts = scripts;
478
490
  this.parsedStatusCache = null;
479
491
  this.parseErrorMessage = null;
492
+ // Initialize per-session state: createState() is called once here and on script reload.
493
+ // The returned object lives until the PTY exits (scriptState = null on exit).
494
+ this.scriptState = typeof scripts.createState === 'function' ? scripts.createState() : null;
480
495
  const scriptNames = listCliScriptNames(scripts);
481
496
  LOG.info('CLI', `[${this.cliType}] CLI scripts injected: [${scriptNames.join(', ')}]`);
482
497
  }
@@ -610,6 +625,7 @@ export class ProviderCliAdapter implements CliAdapter {
610
625
  this.ready = false;
611
626
  this.startupParseGate = false;
612
627
  this.spawnAt = 0;
628
+ this.scriptState = null;
613
629
  this.onStatusChange?.();
614
630
  });
615
631
 
@@ -1450,6 +1466,14 @@ export class ProviderCliAdapter implements CliAdapter {
1450
1466
 
1451
1467
  // ─── Script Execution ──────────────────────────
1452
1468
 
1469
+ private invokeCliScript<T>(script: Function, input: any): T {
1470
+ const hasStateFactory = typeof this.cliScripts?.createState === 'function';
1471
+ const expectsStateArgument = hasStateFactory || this.scriptState !== null || script.length >= 2;
1472
+ return expectsStateArgument
1473
+ ? script(this.scriptState, input)
1474
+ : script(input);
1475
+ }
1476
+
1453
1477
  private runParseSession(): ParsedSession | null {
1454
1478
  if (typeof this.cliScripts?.parseSession !== 'function') {
1455
1479
  this.parseErrorMessage = `${this.cliType} parseSession unavailable`;
@@ -1470,7 +1494,10 @@ export class ProviderCliAdapter implements CliAdapter {
1470
1494
  scope: this.currentTurnScope,
1471
1495
  runtimeSettings: this.runtimeSettings,
1472
1496
  });
1473
- const session = this.cliScripts.parseSession({ ...input, tail, tailScreen: buildCliScreenSnapshot(tail) });
1497
+ const session = this.invokeCliScript<ParsedSession | null>(
1498
+ this.cliScripts.parseSession,
1499
+ { ...input, tail, tailScreen: buildCliScreenSnapshot(tail) },
1500
+ );
1474
1501
  this.parseErrorMessage = null;
1475
1502
  return session && typeof session === 'object' ? session : null;
1476
1503
  } catch (e: any) {
@@ -1485,7 +1512,7 @@ export class ProviderCliAdapter implements CliAdapter {
1485
1512
  if (!this.cliScripts?.detectStatus) return null;
1486
1513
  try {
1487
1514
  const screenText = this.terminalScreen.getText();
1488
- const status = this.cliScripts.detectStatus({
1515
+ const status = this.invokeCliScript<string | null>(this.cliScripts.detectStatus, {
1489
1516
  tail: text.slice(-500),
1490
1517
  screenText,
1491
1518
  rawBuffer: this.accumulatedRawBuffer,
@@ -1505,7 +1532,7 @@ export class ProviderCliAdapter implements CliAdapter {
1505
1532
  try {
1506
1533
  const screenText = this.terminalScreen.getText();
1507
1534
  const buffer = screenText || this.accumulatedBuffer;
1508
- return this.cliScripts.parseApproval({
1535
+ return this.invokeCliScript<{ message: string; buttons: string[] } | null>(this.cliScripts.parseApproval, {
1509
1536
  buffer,
1510
1537
  screenText,
1511
1538
  rawBuffer: this.accumulatedRawBuffer,
@@ -1570,12 +1597,14 @@ export class ProviderCliAdapter implements CliAdapter {
1570
1597
  const screenText = this.readTerminalScreenText();
1571
1598
  const parseScreenText = this.getParseScreenText(screenText);
1572
1599
  const cached = this.parsedStatusCache;
1600
+ const accumulatedRawBufferKey = this.getAccumulatedRawBufferCacheKey();
1573
1601
  if (
1574
1602
  cached
1575
1603
  && cached.responseBuffer === this.responseBuffer
1576
1604
  && cached.currentTurnScope === this.currentTurnScope
1577
1605
  && cached.recentOutputBuffer === this.recentOutputBuffer
1578
1606
  && cached.accumulatedBuffer === this.accumulatedBuffer
1607
+ && cached.accumulatedRawBufferKey === accumulatedRawBufferKey
1579
1608
  && cached.screenText === parseScreenText
1580
1609
  && cached.currentStatus === this.currentStatus
1581
1610
  && cached.activeModal === this.activeModal
@@ -1615,6 +1644,7 @@ export class ProviderCliAdapter implements CliAdapter {
1615
1644
  currentTurnScope: this.currentTurnScope,
1616
1645
  recentOutputBuffer: this.recentOutputBuffer,
1617
1646
  accumulatedBuffer: this.accumulatedBuffer,
1647
+ accumulatedRawBufferKey,
1618
1648
  screenText: parseScreenText,
1619
1649
  currentStatus: this.currentStatus,
1620
1650
  activeModal: this.activeModal,
@@ -1640,7 +1670,7 @@ export class ProviderCliAdapter implements CliAdapter {
1640
1670
  scope: this.currentTurnScope,
1641
1671
  runtimeSettings: this.runtimeSettings,
1642
1672
  });
1643
- return await Promise.resolve(fn({
1673
+ return await Promise.resolve(fn(this.scriptState, {
1644
1674
  ...input,
1645
1675
  args: args && typeof args === 'object' ? { ...args } : {},
1646
1676
  }));
@@ -48,11 +48,21 @@ export interface ParsedSession {
48
48
  }
49
49
 
50
50
  export interface CliScripts {
51
- parseSession?: (input: CliScriptInput & { tail?: string; tailScreen?: CliScreenSnapshot }) => ParsedSession | null;
52
- detectStatus?: (input: CliStatusInput) => string | null;
53
- parseApproval?: (input: CliApprovalInput) => { message: string; buttons: string[] } | null;
51
+ /**
52
+ * Optional state factory. Called once per CLI session start (or script reload).
53
+ * The returned object is passed as the first argument to detectStatus, parseApproval,
54
+ * and parseSession on every invocation, allowing scripts to maintain per-session state
55
+ * (e.g. last-seen status, approval fingerprints, stability counters).
56
+ *
57
+ * Scripts that don't define createState() receive null as the state argument,
58
+ * making this change fully backward compatible.
59
+ */
60
+ createState?: () => unknown;
61
+ parseSession?: (state: unknown, input: CliScriptInput & { tail?: string; tailScreen?: CliScreenSnapshot }) => ParsedSession | null;
62
+ detectStatus?: (state: unknown, input: CliStatusInput) => string | null;
63
+ parseApproval?: (state: unknown, input: CliApprovalInput) => { message: string; buttons: string[] } | null;
54
64
  resolveAction?: (data: any) => string;
55
- [name: string]: ((input: any) => any) | undefined;
65
+ [name: string]: ((state: unknown, input: any) => any) | ((data: any) => any) | (() => unknown) | undefined;
56
66
  }
57
67
 
58
68
  export interface CliScreenLine {
@@ -177,10 +177,6 @@ export function buildCoordinatorDelegatedCliLaunchOptions(
177
177
  const cliArgs = Array.isArray(input.cliArgs) ? [...input.cliArgs] : [];
178
178
  const env: Record<string, string> = { ...(input.env || {}), ...COORDINATOR_DELEGATED_ENV_UNSETS };
179
179
 
180
- if (cliType === 'hermes-cli' && !hasCliArg(cliArgs, '--ignore-user-config')) {
181
- cliArgs.unshift('--ignore-user-config');
182
- }
183
-
184
180
  if (cliType === 'claude-cli' && !hasCliArg(cliArgs, '--mcp-config')) {
185
181
  cliArgs.unshift('--mcp-config', ensureEmptyDelegatedMcpConfig(input.workspace));
186
182
  }
@@ -28,6 +28,15 @@ export type MeshCoordinatorSetup =
28
28
  instructions: string
29
29
  template: string
30
30
  }
31
+ | {
32
+ /** Provider registers MCP via its own CLI command (e.g. `codex mcp add` / `gemini mcp add`). */
33
+ kind: 'cli_command'
34
+ serverName: string
35
+ /** The rendered shell command to execute before launching the coordinator session. */
36
+ command: string
37
+ requiresRestart: boolean
38
+ instructions: string
39
+ }
31
40
  | {
32
41
  kind: 'unsupported'
33
42
  reason: string
@@ -41,6 +50,8 @@ export interface ResolveMeshCoordinatorSetupOptions {
41
50
  adhdevMcpCommand?: string
42
51
  adhdevMcpEntryPath?: string
43
52
  nodeExecutable?: string
53
+ adhdevMcpTransport?: 'local' | 'ipc'
54
+ adhdevMcpPort?: number
44
55
  }
45
56
 
46
57
  const DEFAULT_SERVER_NAME = 'adhdev-mesh'
@@ -58,6 +69,8 @@ function resolveHermesMeshCoordinatorSetup(options: ResolveMeshCoordinatorSetupO
58
69
  meshId: options.meshId,
59
70
  nodeExecutable: options.nodeExecutable,
60
71
  adhdevMcpEntryPath: options.adhdevMcpEntryPath,
72
+ adhdevMcpTransport: options.adhdevMcpTransport,
73
+ adhdevMcpPort: options.adhdevMcpPort,
61
74
  })
62
75
  if (!mcpServer) {
63
76
  return {
@@ -130,6 +143,8 @@ export function resolveMeshCoordinatorSetup(options: ResolveMeshCoordinatorSetup
130
143
  meshId,
131
144
  nodeExecutable: options.nodeExecutable,
132
145
  adhdevMcpEntryPath: options.adhdevMcpEntryPath,
146
+ adhdevMcpTransport: options.adhdevMcpTransport,
147
+ adhdevMcpPort: options.adhdevMcpPort,
133
148
  })
134
149
  if (!mcpServer) {
135
150
  return {
@@ -152,6 +167,24 @@ export function resolveMeshCoordinatorSetup(options: ResolveMeshCoordinatorSetup
152
167
  if (!instructions || !template?.trim()) {
153
168
  return { kind: 'unsupported', reason: 'Provider manual MCP setup is missing instructions or template' }
154
169
  }
170
+ const renderedTemplate = renderMeshCoordinatorTemplate(template, {
171
+ meshId,
172
+ workspace,
173
+ serverName,
174
+ adhdevMcpCommand: options.adhdevMcpCommand || DEFAULT_ADHDEV_MCP_COMMAND,
175
+ })
176
+ // Detect if the template is a runnable CLI command (single line, no YAML/JSON structure).
177
+ // If so, use cli_command kind so the daemon can execute it automatically.
178
+ const isCliCommand = !renderedTemplate.trim().includes('\n') && !renderedTemplate.trim().startsWith('{')
179
+ if (isCliCommand) {
180
+ return {
181
+ kind: 'cli_command',
182
+ serverName,
183
+ command: renderedTemplate.trim(),
184
+ requiresRestart: mcpConfig.requiresRestart === true,
185
+ instructions: instructions,
186
+ }
187
+ }
155
188
  return {
156
189
  kind: 'manual',
157
190
  serverName,
@@ -159,12 +192,7 @@ export function resolveMeshCoordinatorSetup(options: ResolveMeshCoordinatorSetup
159
192
  configPathCommand: mcpConfig.configPathCommand,
160
193
  requiresRestart: mcpConfig.requiresRestart === true,
161
194
  instructions,
162
- template: renderMeshCoordinatorTemplate(template, {
163
- meshId,
164
- workspace,
165
- serverName,
166
- adhdevMcpCommand: options.adhdevMcpCommand || DEFAULT_ADHDEV_MCP_COMMAND,
167
- }),
195
+ template: renderedTemplate,
168
196
  }
169
197
  }
170
198
 
@@ -196,17 +224,37 @@ function resolveAdhdevMcpServerLaunch(options: {
196
224
  meshId: string
197
225
  nodeExecutable?: string
198
226
  adhdevMcpEntryPath?: string
227
+ adhdevMcpTransport?: 'local' | 'ipc'
228
+ adhdevMcpPort?: number
199
229
  }): MeshCoordinatorMcpServerLaunch | null {
200
230
  const entryPath = resolveAdhdevMcpEntryPath(options.adhdevMcpEntryPath)
201
231
  if (!entryPath) return null
202
232
  const nodeExecutable = resolveMcpNodeExecutable(options.nodeExecutable)
203
233
  if (!nodeExecutable) return null
234
+ const transport = resolveMcpTransport(options.adhdevMcpTransport)
235
+ const args = [entryPath, '--mode', transport, '--repo-mesh', options.meshId]
236
+ const port = resolveMcpPort(options.adhdevMcpPort)
237
+ if (port !== undefined) args.push('--port', String(port))
204
238
  return {
205
239
  command: nodeExecutable,
206
- args: [entryPath, '--mode', 'ipc', '--repo-mesh', options.meshId],
240
+ args,
207
241
  }
208
242
  }
209
243
 
244
+ function resolveMcpTransport(explicitTransport?: 'local' | 'ipc'): 'local' | 'ipc' {
245
+ if (explicitTransport === 'local' || explicitTransport === 'ipc') return explicitTransport
246
+ const envTransport = process.env.ADHDEV_COORDINATOR_MCP_TRANSPORT?.trim()
247
+ return envTransport === 'local' ? 'local' : 'ipc'
248
+ }
249
+
250
+ function resolveMcpPort(explicitPort?: number): number | undefined {
251
+ if (typeof explicitPort === 'number' && Number.isInteger(explicitPort) && explicitPort > 0) return explicitPort
252
+ const raw = process.env.ADHDEV_COORDINATOR_MCP_PORT?.trim()
253
+ if (!raw) return undefined
254
+ const parsed = Number(raw)
255
+ return Number.isInteger(parsed) && parsed > 0 ? parsed : undefined
256
+ }
257
+
210
258
  function resolveMcpNodeExecutable(explicitExecutable?: string): string | null {
211
259
  const explicit = explicitExecutable?.trim()
212
260
  if (explicit) return explicit