@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/dist/index.js +224 -15
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +224 -15
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/commands/chat-commands.ts +7 -1
- package/src/commands/router.ts +266 -26
- package/src/mesh/mesh-events-coordinator.ts +2 -0
- package/src/providers/cli-provider-instance.ts +6 -1
package/package.json
CHANGED
|
@@ -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
|
-
:
|
|
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);
|
package/src/commands/router.ts
CHANGED
|
@@ -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 =>
|
|
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
|
|
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
|
-
|
|
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 =
|
|
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:
|
|
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
|
-
|
|
5978
|
-
|
|
5979
|
-
const
|
|
5980
|
-
|
|
5981
|
-
|
|
5982
|
-
|
|
5983
|
-
|
|
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
|
-
|
|
5986
|
-
|
|
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();
|