@adhdev/daemon-core 0.9.82-rc.223 → 0.9.82-rc.225

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adhdev/daemon-core",
3
- "version": "0.9.82-rc.223",
3
+ "version": "0.9.82-rc.225",
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",
@@ -2545,9 +2545,15 @@ export async function handleReadChat(h: CommandHelpers, args: any): Promise<Comm
2545
2545
  const historyLimit = normalizeReadChatTailLimit(args);
2546
2546
  try {
2547
2547
  const agentStr = provider?.type || args?.agentType || getCurrentProviderType(h);
2548
+ const targetSid = typeof args?.targetSessionId === 'string' ? args.targetSessionId.trim() : '';
2549
+ const registrySessionWorkspace = targetSid
2550
+ ? (h.ctx?.sessionRegistry?.get?.(targetSid) as any)?.workspace
2551
+ : undefined;
2548
2552
  const workspace = typeof (h.currentSession as any)?.workspace === 'string'
2549
2553
  ? (h.currentSession as any).workspace
2550
- : undefined;
2554
+ : typeof registrySessionWorkspace === 'string'
2555
+ ? registrySessionWorkspace
2556
+ : undefined;
2551
2557
  const intendedWorkspace = typeof args?.workspace === 'string' ? args.workspace : undefined;
2552
2558
  const supportsNative = supportsCliNativeTranscript(agentStr, provider)
2553
2559
  && isNativeSourceCanonicalHistory(provider?.nativeHistory);
@@ -39,7 +39,7 @@ import { getSessionHostSurfaceKind, partitionSessionHostRecords } from '../sessi
39
39
  import { createHermesManualMeshCoordinatorSetup, resolveMeshCoordinatorSetup } from './mesh-coordinator.js';
40
40
  import { buildSessionEntries } from '../status/builders.js';
41
41
  import { registerMeshCoordinator, getCoordinatorForSession } from '../mesh/coordinator-registry.js';
42
- import { handleMeshForwardEvent, drainPendingMeshCoordinatorEvents, getPendingMeshCoordinatorEvents, queuePendingMeshCoordinatorEvent } from '../mesh/mesh-events.js';
42
+ import { handleMeshForwardEvent, drainPendingMeshCoordinatorEvents, getPendingMeshCoordinatorEvents, queuePendingMeshCoordinatorEvent, type PendingMeshCoordinatorEvent } from '../mesh/mesh-events.js';
43
43
  import { buildMeshHostRequiredFailure, normalizeMeshDaemonRole, resolveMeshHostStatus } from '../mesh/mesh-host-ownership.js';
44
44
  import { fastForwardMeshNode } from '../mesh/mesh-fast-forward.js';
45
45
  import { buildPreviewFreshness } from '../mesh/preview-freshness.js';
@@ -845,7 +845,12 @@ function applyInlineMeshBranchConvergence(mesh: any, node: any, status: Record<s
845
845
 
846
846
  function summarizeInlineMeshBranchConvergence(nodes: Array<Record<string, unknown>>): Record<string, unknown> {
847
847
  const followUps = nodes
848
- .filter(node => readObjectRecord(node.branchConvergence).needsConvergence === true)
848
+ .filter(node => {
849
+ if (readObjectRecord(node.branchConvergence).needsConvergence !== true) return false;
850
+ const workspace = typeof node.workspace === 'string' ? node.workspace : '';
851
+ if (workspace && !fs.existsSync(workspace)) return false;
852
+ return true;
853
+ })
849
854
  .map(node => {
850
855
  const convergence = readObjectRecord(node.branchConvergence);
851
856
  return {
@@ -2857,7 +2862,59 @@ export class DaemonCommandRouter {
2857
2862
  } catch (e: any) {
2858
2863
  const message = String(e?.message || e || 'worktree cleanup failed');
2859
2864
  const dirty = message.includes('dirty worktree') || message.includes('local changes');
2860
- const submoduleForceBlocked = /working trees containing submodules cannot be moved or removed/i.test(message) && !forceFallbackConvergence.allow;
2865
+ const isSubmoduleGuard = /working trees containing submodules cannot be moved or removed/i.test(message);
2866
+ const submoduleForceBlocked = isSubmoduleGuard && !forceFallbackConvergence.allow;
2867
+
2868
+ // Fallback 1: submodule guard on --force path — deinit submodules first, then retry remove
2869
+ if (isSubmoduleGuard && forceFallbackConvergence.allow) {
2870
+ const { execFile } = await import('node:child_process');
2871
+ const { promisify } = await import('node:util');
2872
+ const execFileAsync = promisify(execFile);
2873
+ const GIT_TIMEOUT_CLEANUP = 30_000;
2874
+ const GIT_MAX_BUFFER_CLEANUP = 4 * 1024 * 1024;
2875
+ try {
2876
+ await execFileAsync('git', ['-C', workspace, 'submodule', 'deinit', '--all', '-f'], {
2877
+ encoding: 'utf8', timeout: GIT_TIMEOUT_CLEANUP, maxBuffer: GIT_MAX_BUFFER_CLEANUP, windowsHide: true,
2878
+ });
2879
+ await execFileAsync('git', ['worktree', 'remove', '--force', workspace], {
2880
+ cwd: repoRoot, encoding: 'utf8', timeout: GIT_TIMEOUT_CLEANUP, maxBuffer: GIT_MAX_BUFFER_CLEANUP, windowsHide: true,
2881
+ });
2882
+ return {
2883
+ success: true,
2884
+ removedPath: workspace,
2885
+ repoRoot,
2886
+ fallback: 'git_worktree_remove_submodule_deinit' as const,
2887
+ forced: true,
2888
+ reason: 'working_trees_containing_submodules' as const,
2889
+ convergence: forceFallbackConvergence,
2890
+ };
2891
+ } catch (deinitError: any) {
2892
+ // Fallback 2: deinit+remove still failed — rmSync + prune
2893
+ try {
2894
+ fs.rmSync(workspace, { recursive: true, force: true });
2895
+ await execFileAsync('git', ['worktree', 'prune'], {
2896
+ cwd: repoRoot, encoding: 'utf8', timeout: GIT_TIMEOUT_CLEANUP, maxBuffer: GIT_MAX_BUFFER_CLEANUP, windowsHide: true,
2897
+ });
2898
+ return {
2899
+ success: true,
2900
+ removedPath: workspace,
2901
+ repoRoot,
2902
+ fallback: 'fs_rm_worktree_prune' as const,
2903
+ forced: true,
2904
+ reason: 'working_trees_containing_submodules' as const,
2905
+ convergence: forceFallbackConvergence,
2906
+ };
2907
+ } catch (rmError: any) {
2908
+ return {
2909
+ success: false,
2910
+ code: 'mesh_worktree_cleanup_failed',
2911
+ error: `All removal fallbacks exhausted. deinit+remove: ${deinitError?.message || deinitError}; rmSync+prune: ${rmError?.message || rmError}`,
2912
+ recoveryHint: 'Manually remove the worktree directory and run git worktree prune from the source repo.',
2913
+ };
2914
+ }
2915
+ }
2916
+ }
2917
+
2861
2918
  return {
2862
2919
  success: false,
2863
2920
  code: dirty
@@ -2886,10 +2943,20 @@ export class DaemonCommandRouter {
2886
2943
  const metadataStatus = typeof args.node?.branchConvergence?.status === 'string'
2887
2944
  ? args.node.branchConvergence.status
2888
2945
  : '';
2889
- if (metadataStatus === 'merged_to_main' || metadataStatus === 'cleanup_candidate') {
2946
+ if (metadataStatus === 'merged_to_main' || metadataStatus === 'cleanup_candidate' || metadataStatus === 'merged_pushed') {
2890
2947
  return { allow: true, status: metadataStatus, source: 'node_branch_convergence' };
2891
2948
  }
2892
2949
 
2950
+ // Also allow when the node's last recorded refine job reached final convergence merged_pushed
2951
+ const refinedConvergence = typeof args.node?.refineState?.finalBranchConvergenceState?.status === 'string'
2952
+ ? args.node.refineState.finalBranchConvergenceState.status
2953
+ : typeof args.node?.lastRefineResult?.finalBranchConvergenceState?.status === 'string'
2954
+ ? args.node.lastRefineResult.finalBranchConvergenceState.status
2955
+ : '';
2956
+ if (refinedConvergence === 'merged_pushed' || refinedConvergence === 'merged_to_main') {
2957
+ return { allow: true, status: refinedConvergence, source: 'node_refine_state' };
2958
+ }
2959
+
2893
2960
  const { execFile } = await import('node:child_process');
2894
2961
  const { promisify } = await import('node:util');
2895
2962
  const execFileAsync = promisify(execFile);
@@ -3455,11 +3522,31 @@ export class DaemonCommandRouter {
3455
3522
 
3456
3523
  const { stdout: baseBranchStdout } = await execFileAsync('git', ['branch', '--show-current'], { cwd: repoRoot, encoding: 'utf8' });
3457
3524
  const baseBranch = baseBranchStdout.trim();
3458
- const { stdout: baseHeadStdout } = await execFileAsync('git', ['rev-parse', 'HEAD'], { cwd: repoRoot, encoding: 'utf8' });
3525
+
3526
+ // Fetch origin so baseHead reflects the latest pushed state, not a stale local HEAD.
3527
+ // This prevents patch_equivalence failures when sequential Refines push to origin/main
3528
+ // but the local main checkout hasn't been fast-forwarded yet.
3529
+ let fetchWarning: string | undefined;
3530
+ try {
3531
+ await execFileAsync('git', ['fetch', 'origin', baseBranch], { cwd: repoRoot, encoding: 'utf8' });
3532
+ } catch (e: any) {
3533
+ fetchWarning = `git fetch origin ${baseBranch} failed (proceeding with local HEAD): ${e?.message}`;
3534
+ }
3535
+
3536
+ // Prefer origin/<baseBranch> as the authoritative base; fall back to local HEAD if fetch failed.
3537
+ let baseHeadRaw: string;
3538
+ try {
3539
+ const { stdout } = await execFileAsync('git', ['rev-parse', `origin/${baseBranch}`], { cwd: repoRoot, encoding: 'utf8' });
3540
+ baseHeadRaw = stdout.trim();
3541
+ } catch {
3542
+ const { stdout: localHead } = await execFileAsync('git', ['rev-parse', 'HEAD'], { cwd: repoRoot, encoding: 'utf8' });
3543
+ baseHeadRaw = localHead.trim();
3544
+ }
3545
+
3459
3546
  const { stdout: branchHeadStdout } = await execFileAsync('git', ['rev-parse', branch], { cwd: node.workspace, encoding: 'utf8' });
3460
- const baseHead = baseHeadStdout.trim();
3547
+ const baseHead = baseHeadRaw;
3461
3548
  let branchHead = branchHeadStdout.trim();
3462
- recordMeshRefineStage(refineStages, 'resolve_refs', 'passed', resolveStarted, { branch, baseBranch, baseHead, branchHead });
3549
+ recordMeshRefineStage(refineStages, 'resolve_refs', 'passed', resolveStarted, { branch, baseBranch, baseHead, branchHead, ...(fetchWarning ? { fetchWarning } : {}) });
3463
3550
 
3464
3551
  const validationStarted = Date.now();
3465
3552
  const validationSummary = await runMeshRefineValidationGate(mesh, node.workspace, {
@@ -3480,15 +3567,35 @@ export class DaemonCommandRouter {
3480
3567
  { validationStatus: validationSummary.status, commandsRun: validationSummary.commandsRun.length },
3481
3568
  );
3482
3569
  if (validationSummary.status === 'failed') {
3570
+ const firstFailedCmd = Array.isArray(validationSummary.commandsRun)
3571
+ ? (validationSummary.commandsRun as Array<Record<string, unknown>>).find(c => c.success === false)
3572
+ : undefined;
3573
+ const buildValidationFailedError = (): string => {
3574
+ const base = validationSummary.failureCode === 'missing_dependencies'
3575
+ ? 'Refinery validation dependencies are missing; merge/refine was not attempted. Configure validation.bootstrapCommands if Refinery should bootstrap dependencies before validation.'
3576
+ : validationSummary.failureCode === 'dependency_bootstrap_failed'
3577
+ ? 'Refinery dependency/bootstrap command failed; merge/refine was not attempted.'
3578
+ : 'Refinery validation gate failed; merge/refine was not attempted.';
3579
+ if (!firstFailedCmd) return base;
3580
+ const cmdName = typeof firstFailedCmd.displayCommand === 'string' ? firstFailedCmd.displayCommand
3581
+ : typeof firstFailedCmd.command === 'string'
3582
+ ? [firstFailedCmd.command, ...(Array.isArray(firstFailedCmd.args) ? firstFailedCmd.args : [])].join(' ').trim()
3583
+ : typeof firstFailedCmd.cmd === 'string' ? firstFailedCmd.cmd : '';
3584
+ const rawOutput = [firstFailedCmd.stdout, firstFailedCmd.stderr, firstFailedCmd.output]
3585
+ .filter(s => typeof s === 'string' && s.length > 0)
3586
+ .join('\n');
3587
+ const tail = rawOutput.length > 800 ? rawOutput.slice(-800) : rawOutput;
3588
+ return [
3589
+ base,
3590
+ cmdName ? `First failing command: ${cmdName}` : '',
3591
+ tail ? `Output (tail):\n${tail}` : '',
3592
+ ].filter(Boolean).join('\n');
3593
+ };
3483
3594
  return {
3484
3595
  success: false,
3485
3596
  code: validationSummary.failureCode || 'validation_failed',
3486
3597
  convergenceStatus: 'blocked_review',
3487
- error: validationSummary.failureCode === 'missing_dependencies'
3488
- ? 'Refinery validation dependencies are missing; merge/refine was not attempted. Configure validation.bootstrapCommands if Refinery should bootstrap dependencies before validation.'
3489
- : validationSummary.failureCode === 'dependency_bootstrap_failed'
3490
- ? 'Refinery dependency/bootstrap command failed; merge/refine was not attempted.'
3491
- : 'Refinery validation gate failed; merge/refine was not attempted.',
3598
+ error: buildValidationFailedError(),
3492
3599
  branch,
3493
3600
  into: baseBranch,
3494
3601
  validationSummary,
@@ -4184,13 +4291,64 @@ export class DaemonCommandRouter {
4184
4291
  return { success: true };
4185
4292
  }
4186
4293
 
4187
- case 'launch_cli':
4294
+ case 'launch_cli': {
4295
+ const launchResult = await this.deps.cliManager.handleCliCommand(cmd, args);
4296
+ // Bug C fix (part 1): when launching a mesh node worker session, surface
4297
+ // bootstrapPending:true if the node's worktree bootstrap is still running.
4298
+ // This is informational — the launch is NOT blocked here (blocking is done
4299
+ // upstream by getWorktreeBootstrapLaunchBlock in the MCP layer).
4300
+ const meshNodeId = readStringValue((args?.settings as any)?.meshNodeId);
4301
+ const meshId = readStringValue((args?.settings as any)?.meshNodeFor);
4302
+ if (meshNodeId && meshId && launchResult?.success !== false) {
4303
+ try {
4304
+ const { getMesh } = await import('../config/mesh-config.js');
4305
+ const meshObj = getMesh(meshId) ?? this.getCachedInlineMesh(meshId);
4306
+ const nodeObj = Array.isArray(meshObj?.nodes)
4307
+ ? meshObj.nodes.find((n: any) => n.id === meshNodeId || n.nodeId === meshNodeId)
4308
+ : undefined;
4309
+ const bootstrapStatus = readStringValue(nodeObj?.worktreeBootstrap?.status);
4310
+ if (bootstrapStatus === 'running') {
4311
+ return { success: true, ...launchResult, bootstrapPending: true };
4312
+ }
4313
+ } catch { /* best-effort — do not fail launch for bootstrap probe errors */ }
4314
+ }
4315
+ return launchResult;
4316
+ }
4188
4317
  case 'stop_cli':
4189
4318
  case 'set_cli_view_mode':
4190
- case 'record_provider_pty':
4191
- case 'agent_command': {
4319
+ case 'record_provider_pty': {
4192
4320
  return this.deps.cliManager.handleCliCommand(cmd, args);
4193
4321
  }
4322
+ case 'agent_command': {
4323
+ const agentResult = await this.deps.cliManager.handleCliCommand(cmd, args);
4324
+ // Bug C fix (part 2): when dispatching a task to a mesh node session, override
4325
+ // the dispatch acknowledgement risk reason to 'bootstrap_still_running' when
4326
+ // the target node's worktree bootstrap is still running. Informational only —
4327
+ // dispatch is NOT blocked.
4328
+ const meshCtx = args?.meshContext as Record<string, unknown> | undefined;
4329
+ const dispatchNodeId = readStringValue(meshCtx?.nodeId);
4330
+ const dispatchMeshId = readStringValue(meshCtx?.meshId);
4331
+ if (dispatchNodeId && dispatchMeshId && agentResult?.success !== false) {
4332
+ try {
4333
+ const { getMesh } = await import('../config/mesh-config.js');
4334
+ const meshObj = getMesh(dispatchMeshId) ?? this.getCachedInlineMesh(dispatchMeshId);
4335
+ const nodeObj = Array.isArray(meshObj?.nodes)
4336
+ ? meshObj.nodes.find((n: any) => n.id === dispatchNodeId || n.nodeId === dispatchNodeId)
4337
+ : undefined;
4338
+ const bootstrapStatus = readStringValue(nodeObj?.worktreeBootstrap?.status);
4339
+ if (bootstrapStatus === 'running') {
4340
+ return {
4341
+ success: true,
4342
+ ...agentResult,
4343
+ dispatchAcknowledgementRisk: true,
4344
+ dispatchAcknowledgementRiskReason: 'bootstrap_still_running',
4345
+ nextAction: 'Wait for worktree_bootstrap_complete event before dispatching work to this node.',
4346
+ };
4347
+ }
4348
+ } catch { /* best-effort */ }
4349
+ }
4350
+ return agentResult;
4351
+ }
4194
4352
 
4195
4353
  // ─── Logs ───
4196
4354
  case 'get_logs': {
@@ -5766,9 +5924,16 @@ export class DaemonCommandRouter {
5766
5924
  // keeps showing up in the dashboard graph until the cache
5767
5925
  // ages out on its own.
5768
5926
  if (removed) this.invalidateAggregateMeshStatus(meshId);
5927
+ // Node was already absent from the inline mesh (e.g. removed by a
5928
+ // prior refine cleanup). Treat as removed so caller gets removed:true.
5929
+ if (!removed && !node) removed = true;
5769
5930
  } else {
5770
5931
  const { removeNode } = await import('../config/mesh-config.js');
5771
5932
  removed = removeNode(meshId, nodeId);
5933
+ // Node already absent from config (e.g. removed by a prior refine
5934
+ // cleanup after a successful Refinery merge). Treat as removed so
5935
+ // the response is accurate.
5936
+ if (!removed && !node) removed = true;
5772
5937
  if (removed) this.invalidateAggregateMeshStatus(meshId);
5773
5938
  }
5774
5939
 
@@ -5974,17 +6139,58 @@ export class DaemonCommandRouter {
5974
6139
  new Promise<{ completed: false }>((resolve) => setTimeout(() => resolve({ completed: false }), setupWaitMs)),
5975
6140
  ]);
5976
6141
 
5977
- if (!setupResult.completed) {
5978
- setupPromise.catch((error: any) => {
5979
- const failedState: WorktreeBootstrapState = {
5980
- ...runningBootstrapState,
5981
- status: 'failed',
5982
- completedAt: new Date().toISOString(),
5983
- error: error?.message || String(error),
6142
+ const emitBootstrapEvent = (eventStatus: 'bootstrap_complete' | 'bootstrap_failed', bootstrapState: WorktreeBootstrapState, startedAtMs: number, extraPayload?: Record<string, unknown>): void => {
6143
+ try {
6144
+ const durationMs = Date.now() - startedAtMs;
6145
+ const event = `worktree_${eventStatus}` as const;
6146
+ const metadataEvent = {
6147
+ source: 'clone_mesh_node_bootstrap',
6148
+ nodeId: node.id,
6149
+ status: eventStatus,
6150
+ worktreePath: result.worktreePath,
6151
+ durationMs,
6152
+ bootstrapStatus: bootstrapState.status,
6153
+ ...(bootstrapState.error ? { error: bootstrapState.error } : {}),
6154
+ ...(bootstrapState.exitCode !== undefined ? { exitCode: bootstrapState.exitCode } : {}),
6155
+ ...(extraPayload || {}),
5984
6156
  };
5985
- void persistWorktreeSetupState(failedState);
5986
- void appendCloneLedger(false, failedState);
5987
- });
6157
+ if (typeof this.deps.instanceManager?.getByCategory === 'function') {
6158
+ const forwarded = handleMeshForwardEvent(
6159
+ { instanceManager: this.deps.instanceManager } as any,
6160
+ { event, meshId, nodeId: node.id, workspace: result.worktreePath, metadataEvent },
6161
+ );
6162
+ if (forwarded?.success === true) return;
6163
+ }
6164
+ queuePendingMeshCoordinatorEvent({
6165
+ event,
6166
+ meshId,
6167
+ nodeLabel: node.id,
6168
+ nodeId: node.id,
6169
+ workspace: result.worktreePath,
6170
+ metadataEvent,
6171
+ queuedAt: Date.now(),
6172
+ });
6173
+ } catch { /* event emission is best-effort */ }
6174
+ };
6175
+
6176
+ const bootstrapStartedMs = Date.now();
6177
+
6178
+ if (!setupResult.completed) {
6179
+ setupPromise
6180
+ .then(({ bootstrapState }) => {
6181
+ emitBootstrapEvent('bootstrap_complete', bootstrapState, bootstrapStartedMs);
6182
+ })
6183
+ .catch((error: any) => {
6184
+ const failedState: WorktreeBootstrapState = {
6185
+ ...runningBootstrapState,
6186
+ status: 'failed',
6187
+ completedAt: new Date().toISOString(),
6188
+ error: error?.message || String(error),
6189
+ };
6190
+ void persistWorktreeSetupState(failedState);
6191
+ void appendCloneLedger(false, failedState);
6192
+ emitBootstrapEvent('bootstrap_failed', failedState, bootstrapStartedMs, { error: error?.message || String(error) });
6193
+ });
5988
6194
  return {
5989
6195
  success: true,
5990
6196
  async: true,
@@ -6002,6 +6208,7 @@ export class DaemonCommandRouter {
6002
6208
  }
6003
6209
 
6004
6210
  const { submodulesInitialized, bootstrapState } = setupResult.value;
6211
+ emitBootstrapEvent('bootstrap_complete', bootstrapState, bootstrapStartedMs);
6005
6212
  return {
6006
6213
  success: true,
6007
6214
  node,
@@ -6077,7 +6284,40 @@ export class DaemonCommandRouter {
6077
6284
  const ownerFailure = await this.requireMeshHostMutationOwner(meshId, args?.inlineMesh, 'queue trigger');
6078
6285
  if (ownerFailure) return ownerFailure;
6079
6286
  try {
6080
- const { triggerMeshQueue } = await import('../mesh/mesh-events.js');
6287
+ const { triggerMeshQueue, tryAssignQueueTask } = await import('../mesh/mesh-events.js');
6288
+
6289
+ // Bug A fix: when preferredNodeId is provided, attempt to claim a pending
6290
+ // task for the preferred node's idle session first, before the general
6291
+ // round-robin trigger picks a different node.
6292
+ const preferredNodeId = typeof args?.preferredNodeId === 'string' ? args.preferredNodeId.trim() : '';
6293
+ if (preferredNodeId) {
6294
+ const cliInstances = this.deps.instanceManager.getByCategory('cli');
6295
+ // Sort: preferred node's sessions first, others after
6296
+ const sorted = [...cliInstances].sort((a, b) => {
6297
+ const aSettings = a.getState().settings as Record<string, unknown> || {};
6298
+ const bSettings = b.getState().settings as Record<string, unknown> || {};
6299
+ const aNode = readStringValue(aSettings.meshNodeId, aSettings.nodeId);
6300
+ const bNode = readStringValue(bSettings.meshNodeId, bSettings.nodeId);
6301
+ return (aNode === preferredNodeId ? -1 : 0) - (bNode === preferredNodeId ? -1 : 0);
6302
+ });
6303
+ for (const inst of sorted) {
6304
+ const state = inst.getState();
6305
+ const settings = state.settings as Record<string, unknown> || {};
6306
+ const nodeId = readStringValue(settings.meshNodeId, settings.nodeId);
6307
+ if (!nodeId || nodeId !== preferredNodeId) continue;
6308
+ const meshNodeFor = readStringValue(settings.meshNodeFor);
6309
+ if (meshNodeFor !== meshId) continue;
6310
+ const status = (readStringValue(state.status) || '').toLowerCase();
6311
+ if (status !== 'idle') continue;
6312
+ const sessionId = typeof state.instanceId === 'string' ? state.instanceId : '';
6313
+ const providerType = readStringValue(state.type, settings.providerType) || '';
6314
+ if (sessionId && providerType) {
6315
+ tryAssignQueueTask(this.deps as any, meshId, nodeId, sessionId, providerType);
6316
+ break;
6317
+ }
6318
+ }
6319
+ }
6320
+
6081
6321
  const trigger = await triggerMeshQueue(this.deps as any, meshId);
6082
6322
  return { success: true, trigger };
6083
6323
  } catch (e: any) {
@@ -870,6 +870,8 @@ const MESH_COORDINATOR_EVENTS = new Set([
870
870
  'refine:accepted',
871
871
  'refine:completed',
872
872
  'refine:failed',
873
+ 'worktree_bootstrap_complete',
874
+ 'worktree_bootstrap_failed',
873
875
  ]);
874
876
 
875
877
  const EVENT_TO_LEDGER_KIND: Record<string, MeshLedgerKind> = {
@@ -1483,9 +1483,14 @@ export class CliProviderInstance implements ProviderInstance {
1483
1483
  const rawStatus = adapterStatus.status;
1484
1484
  const autoApproveActive = this.maybeAutoApproveStatus(adapterStatus, now);
1485
1485
  const externalNativeFinal = this.getExternalNativeFinalReconciliation(undefined, adapterStatus);
1486
+ // During the autoApproveBusy window (2s after firing approval key), the PTY
1487
+ // can briefly report 'idle' before the next generating phase starts. Treat that
1488
+ // transient idle as 'generating' to suppress a spurious agent:generating_completed
1489
+ // push notification. externalNativeFinal still wins to allow hard-stop overrides.
1490
+ const autoApproveHoldIdle = this.autoApproveBusy && rawStatus === 'idle';
1486
1491
  const newStatus = externalNativeFinal && isCliGeneratingLikeStatus(rawStatus)
1487
1492
  ? 'idle'
1488
- : (autoApproveActive ? 'generating' : rawStatus);
1493
+ : (autoApproveActive || autoApproveHoldIdle ? 'generating' : rawStatus);
1489
1494
  const dirName = this.workingDir.split('/').filter(Boolean).pop() || 'session';
1490
1495
  const chatTitle = `${this.provider.name} · ${dirName}`;
1491
1496
  const partial = this.adapter.getPartialResponse();