@adhdev/daemon-core 0.9.82-rc.456 → 0.9.82-rc.458
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/commands/med-family/mesh-crud.d.ts +19 -0
- package/dist/config/config.d.ts +14 -0
- package/dist/config/registry-resolver.d.ts +54 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +410 -80
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +408 -80
- package/dist/index.mjs.map +1 -1
- package/dist/logging/debug-config.d.ts +16 -0
- package/dist/mesh/preview-freshness.d.ts +11 -1
- package/dist/mesh/worktree-bootstrap-config.d.ts +54 -0
- package/dist/providers/cli-provider-instance.d.ts +4 -0
- package/dist/providers/provider-loader.d.ts +17 -2
- package/dist/providers/spec/fsm-driver.d.ts +5 -0
- package/package.json +3 -3
- package/src/boot/daemon-lifecycle.ts +2 -0
- package/src/commands/handler.ts +3 -2
- package/src/commands/low-family/daemon-lifecycle.ts +14 -1
- package/src/commands/med-family/mesh-crud.ts +83 -49
- package/src/config/config.ts +18 -0
- package/src/config/registry-resolver.ts +100 -0
- package/src/index.ts +2 -0
- package/src/logging/debug-config.ts +25 -0
- package/src/logging/debug-trace.ts +7 -2
- package/src/mesh/coordinator-prompt.ts +1 -1
- package/src/mesh/mesh-fast-forward.ts +22 -9
- package/src/mesh/mesh-refine-gates.ts +22 -9
- package/src/mesh/preview-freshness.ts +46 -1
- package/src/mesh/worktree-bootstrap-config.ts +131 -1
- package/src/providers/cli-provider-instance.ts +159 -7
- package/src/providers/provider-loader.ts +36 -9
- package/src/providers/spec/fsm-driver.ts +49 -2
- package/src/commands/WINDOWS-UPGRADE-LOCK-FAILURE.md +0 -198
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { GitRepoStatus, GitSubmoduleStatus } from '../git/git-types.js';
|
|
2
2
|
import { getGitRepoStatus } from '../git/git-status.js';
|
|
3
3
|
import { GitCommandError, runGit } from '../git/git-executor.js';
|
|
4
|
+
import { resolveSubmoduleDefaultBranch } from './worktree-bootstrap-config.js';
|
|
4
5
|
|
|
5
6
|
export interface MeshFastForwardNodeArgs {
|
|
6
7
|
nodeId?: string;
|
|
@@ -521,31 +522,43 @@ async function resolveSubmodulePushes(
|
|
|
521
522
|
results.push({ ...base, code: 'submodule_status_incomplete' });
|
|
522
523
|
continue;
|
|
523
524
|
}
|
|
524
|
-
//
|
|
525
|
+
// Generalize the submodule's default branch (F18): '.gitmodules' branch →
|
|
526
|
+
// local remote HEAD → remote-advertised HEAD → 'main'. On a main-default
|
|
527
|
+
// submodule this resolves to 'main', keeping every ref below byte-identical.
|
|
528
|
+
const remoteBranch = await resolveSubmoduleDefaultBranch({
|
|
529
|
+
submoduleRepoPath: repoPath,
|
|
530
|
+
superprojectWorkspace: status.repoRoot ?? status.workspace,
|
|
531
|
+
submodulePath: submodule.path,
|
|
532
|
+
timeoutMs,
|
|
533
|
+
});
|
|
534
|
+
base.remoteBranch = remoteBranch;
|
|
535
|
+
const remoteRef = `refs/remotes/origin/${remoteBranch}`;
|
|
536
|
+
const fetchRefspec = `refs/heads/${remoteBranch}:${remoteRef}`;
|
|
537
|
+
// Refresh the submodule's origin/<branch>, then require it to be an ancestor of
|
|
525
538
|
// the gitlink commit (strict ff-only).
|
|
526
539
|
try {
|
|
527
|
-
await runGit(repoPath, ['-c', 'protocol.file.allow=always', 'fetch', 'origin',
|
|
540
|
+
await runGit(repoPath, ['-c', 'protocol.file.allow=always', 'fetch', 'origin', fetchRefspec], { timeoutMs: timeoutMs ?? 30_000 });
|
|
528
541
|
} catch (error) {
|
|
529
542
|
results.push({ ...base, code: 'submodule_fetch_failed', error: formatGitError(error) });
|
|
530
543
|
continue;
|
|
531
544
|
}
|
|
532
545
|
let alreadyReachable = false;
|
|
533
546
|
try {
|
|
534
|
-
await runGit(repoPath, ['merge-base', '--is-ancestor', submodule.commit,
|
|
547
|
+
await runGit(repoPath, ['merge-base', '--is-ancestor', submodule.commit, remoteRef], { timeoutMs: timeoutMs ?? 15_000 });
|
|
535
548
|
alreadyReachable = true;
|
|
536
|
-
} catch { /* not yet on origin
|
|
549
|
+
} catch { /* not yet on origin/<branch> — candidate for push */ }
|
|
537
550
|
if (alreadyReachable) {
|
|
538
551
|
results.push({ ...base, pushed: false, skipped: true, code: 'submodule_already_reachable' });
|
|
539
552
|
continue;
|
|
540
553
|
}
|
|
541
|
-
// Strict ff-only: origin
|
|
554
|
+
// Strict ff-only: origin/<branch> must be an ancestor of the commit we publish.
|
|
542
555
|
try {
|
|
543
|
-
await runGit(repoPath, ['merge-base', '--is-ancestor',
|
|
556
|
+
await runGit(repoPath, ['merge-base', '--is-ancestor', remoteRef, submodule.commit], { timeoutMs: timeoutMs ?? 15_000 });
|
|
544
557
|
} catch (error) {
|
|
545
558
|
results.push({ ...base, pushed: false, skipped: false, code: 'submodule_non_fast_forward', error: formatGitError(error) });
|
|
546
559
|
continue;
|
|
547
560
|
}
|
|
548
|
-
const refspec = `${submodule.commit}:refs/heads
|
|
561
|
+
const refspec = `${submodule.commit}:refs/heads/${remoteBranch}`;
|
|
549
562
|
if (!execute) {
|
|
550
563
|
results.push({ ...base, pushed: false, skipped: false, code: 'submodule_push_available', refspec });
|
|
551
564
|
continue;
|
|
@@ -553,8 +566,8 @@ async function resolveSubmodulePushes(
|
|
|
553
566
|
try {
|
|
554
567
|
await runGit(repoPath, ['push', 'origin', refspec], { timeoutMs: timeoutMs ?? 30_000 });
|
|
555
568
|
// Verify reachability after the push.
|
|
556
|
-
await runGit(repoPath, ['-c', 'protocol.file.allow=always', 'fetch', 'origin',
|
|
557
|
-
await runGit(repoPath, ['merge-base', '--is-ancestor', submodule.commit,
|
|
569
|
+
await runGit(repoPath, ['-c', 'protocol.file.allow=always', 'fetch', 'origin', fetchRefspec], { timeoutMs: timeoutMs ?? 30_000 });
|
|
570
|
+
await runGit(repoPath, ['merge-base', '--is-ancestor', submodule.commit, remoteRef], { timeoutMs: timeoutMs ?? 15_000 });
|
|
558
571
|
results.push({ ...base, pushed: true, skipped: false, code: 'submodule_pushed', refspec });
|
|
559
572
|
} catch (error) {
|
|
560
573
|
results.push({ ...base, pushed: false, skipped: false, code: 'submodule_push_failed', refspec, error: formatGitError(error) });
|
|
@@ -16,7 +16,7 @@ import { getGitRepoStatus } from '../git/git-status.js';
|
|
|
16
16
|
import * as yaml from 'js-yaml';
|
|
17
17
|
import { loadMeshRefineConfig, resolveMeshRefineValidationPlan } from '../mesh/refine-config.js';
|
|
18
18
|
import type { MeshRefineValidationCommandPlan } from '../mesh/refine-config.js';
|
|
19
|
-
import { evaluateWorktreeBootstrapState, loadMeshWorktreeBootstrapConfig, runMeshWorktreeBootstrap } from '../mesh/worktree-bootstrap-config.js';
|
|
19
|
+
import { evaluateWorktreeBootstrapState, loadMeshWorktreeBootstrapConfig, runMeshWorktreeBootstrap, resolveSubmoduleDefaultBranch } from '../mesh/worktree-bootstrap-config.js';
|
|
20
20
|
import type { WorktreeBootstrapState } from '../mesh/worktree-bootstrap-config.js';
|
|
21
21
|
import { basename as pathBasename, join as pathJoin, resolve as pathResolve } from 'path';
|
|
22
22
|
import * as fs from 'fs';
|
|
@@ -1295,6 +1295,10 @@ export async function runMeshRefineSubmoduleReachabilityGate(
|
|
|
1295
1295
|
commit: gitlink.commit,
|
|
1296
1296
|
reachable: false,
|
|
1297
1297
|
};
|
|
1298
|
+
// Resolved lazily once the submodule checkout/remote are confirmed; defaults
|
|
1299
|
+
// to 'main' so error messages emitted before resolution stay byte-identical
|
|
1300
|
+
// to the pre-generalization behavior on a main-default repo.
|
|
1301
|
+
let submoduleDefaultBranch = 'main';
|
|
1298
1302
|
try {
|
|
1299
1303
|
if (!fs.existsSync(submodulePath)) {
|
|
1300
1304
|
entry.error = `Submodule checkout missing at ${gitlink.path}`;
|
|
@@ -1350,9 +1354,18 @@ export async function runMeshRefineSubmoduleReachabilityGate(
|
|
|
1350
1354
|
entries.push(entry);
|
|
1351
1355
|
continue;
|
|
1352
1356
|
}
|
|
1353
|
-
|
|
1357
|
+
// Generalize the submodule's default branch (F18): '.gitmodules'
|
|
1358
|
+
// branch → local remote HEAD → remote-advertised HEAD → 'main'. On a
|
|
1359
|
+
// main-default submodule this resolves to 'main' and every ref target
|
|
1360
|
+
// below is byte-identical to the prior hardcoded path.
|
|
1361
|
+
submoduleDefaultBranch = await resolveSubmoduleDefaultBranch({
|
|
1362
|
+
submoduleRepoPath: submodulePath,
|
|
1363
|
+
superprojectWorkspace: repoRoot,
|
|
1364
|
+
submodulePath: gitlink.path,
|
|
1365
|
+
});
|
|
1366
|
+
entry.remoteMainBranch = submoduleDefaultBranch;
|
|
1354
1367
|
try {
|
|
1355
|
-
await verifyRemoteMainContainsCommit(submodulePath, gitlink.commit,
|
|
1368
|
+
await verifyRemoteMainContainsCommit(submodulePath, gitlink.commit, submoduleDefaultBranch);
|
|
1356
1369
|
entry.fetchedFromOrigin = true;
|
|
1357
1370
|
entry.remoteReachable = true;
|
|
1358
1371
|
entry.remoteMainReachable = true;
|
|
@@ -1362,17 +1375,17 @@ export async function runMeshRefineSubmoduleReachabilityGate(
|
|
|
1362
1375
|
entry.remoteMainReachable = false;
|
|
1363
1376
|
entry.publishRequired = true;
|
|
1364
1377
|
const details = truncateValidationOutput(e?.stderr || e?.message || String(e));
|
|
1365
|
-
entry.error = `Submodule remote main reachability check failed for origin
|
|
1378
|
+
entry.error = `Submodule remote main reachability check failed for origin/${submoduleDefaultBranch}: ${details}`;
|
|
1366
1379
|
if (options.allowAutoPublishSubmoduleMainCommits === true && entry.localReachable === true) {
|
|
1367
1380
|
entry.autoPublishAllowed = true;
|
|
1368
1381
|
entry.autoPublishAttempted = true;
|
|
1369
1382
|
try {
|
|
1370
|
-
const publish = await publishCommitToRemoteMain(submodulePath, gitlink.commit,
|
|
1383
|
+
const publish = await publishCommitToRemoteMain(submodulePath, gitlink.commit, submoduleDefaultBranch);
|
|
1371
1384
|
entry.autoPublishRefspec = publish.refspec;
|
|
1372
1385
|
entry.publishStdout = truncateValidationOutput(publish.stdout);
|
|
1373
1386
|
entry.publishStderr = truncateValidationOutput(publish.stderr);
|
|
1374
1387
|
entry.autoPublishSucceeded = true;
|
|
1375
|
-
await verifyRemoteMainContainsCommit(submodulePath, gitlink.commit,
|
|
1388
|
+
await verifyRemoteMainContainsCommit(submodulePath, gitlink.commit, submoduleDefaultBranch);
|
|
1376
1389
|
entry.fetchedFromOrigin = true;
|
|
1377
1390
|
entry.remoteReachable = true;
|
|
1378
1391
|
entry.remoteMainReachable = true;
|
|
@@ -1384,13 +1397,13 @@ export async function runMeshRefineSubmoduleReachabilityGate(
|
|
|
1384
1397
|
entry.autoPublishSucceeded = false;
|
|
1385
1398
|
entry.autoPublishVerified = false;
|
|
1386
1399
|
const publishDetails = truncateValidationOutput(publishError?.stderr || publishError?.message || String(publishError));
|
|
1387
|
-
entry.error = `Submodule auto-publish to origin
|
|
1400
|
+
entry.error = `Submodule auto-publish to origin/${submoduleDefaultBranch} failed or could not be verified: ${publishDetails}`;
|
|
1388
1401
|
}
|
|
1389
1402
|
} else if (options.allowAutoPublishSubmoduleMainCommits === true) {
|
|
1390
1403
|
entry.autoPublishAllowed = true;
|
|
1391
1404
|
entry.autoPublishAttempted = false;
|
|
1392
1405
|
entry.autoPublishSkippedReason = entry.autoPublishSkippedReason
|
|
1393
|
-
||
|
|
1406
|
+
|| `candidate commit is not reachable in the source checkout or worktree submodule, so Refinery cannot push it to origin/${submoduleDefaultBranch}`;
|
|
1394
1407
|
}
|
|
1395
1408
|
}
|
|
1396
1409
|
} catch (e: any) {
|
|
@@ -1398,7 +1411,7 @@ export async function runMeshRefineSubmoduleReachabilityGate(
|
|
|
1398
1411
|
entry.remoteMainReachable = false;
|
|
1399
1412
|
entry.publishRequired = true;
|
|
1400
1413
|
const details = truncateValidationOutput(e?.stderr || e?.message || String(e));
|
|
1401
|
-
entry.error = `Submodule remote main reachability check failed for origin
|
|
1414
|
+
entry.error = `Submodule remote main reachability check failed for origin/${submoduleDefaultBranch}: ${details}`;
|
|
1402
1415
|
}
|
|
1403
1416
|
} catch (e: any) {
|
|
1404
1417
|
entry.error = truncateValidationOutput(e?.message || String(e));
|
|
@@ -23,6 +23,46 @@ export interface PreviewFreshness {
|
|
|
23
23
|
|
|
24
24
|
const PREVIEW_DEPLOY_RECORD = '.adhdev/preview-deploy.json';
|
|
25
25
|
|
|
26
|
+
// Repo-relative driver scripts that indicate this repository actually ships the
|
|
27
|
+
// preview-deploy pipeline. Presence of any one of these (or the deploy record,
|
|
28
|
+
// or a `deploy:preview` npm script) means the private release-pipeline guidance
|
|
29
|
+
// carried by buildPreviewFreshness is relevant here. In every other repo the
|
|
30
|
+
// pipeline is not configured and the guidance must NOT leak (F15).
|
|
31
|
+
const PREVIEW_PIPELINE_SCRIPTS = [
|
|
32
|
+
'scripts/preview-freshness.mjs',
|
|
33
|
+
'scripts/smoke-preview-web.mjs',
|
|
34
|
+
'scripts/deploy-preview-local.mjs',
|
|
35
|
+
] as const;
|
|
36
|
+
|
|
37
|
+
function hasDeployPreviewNpmScript(repoRoot: string): boolean {
|
|
38
|
+
const pkgPath = resolve(repoRoot, 'package.json');
|
|
39
|
+
if (!existsSync(pkgPath)) return false;
|
|
40
|
+
try {
|
|
41
|
+
const pkg = JSON.parse(readFileSync(pkgPath, 'utf8')) as { scripts?: Record<string, unknown> };
|
|
42
|
+
return typeof pkg?.scripts?.['deploy:preview'] === 'string';
|
|
43
|
+
} catch {
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Gate: does this repository actually configure the preview-deploy pipeline?
|
|
50
|
+
*
|
|
51
|
+
* The preview-freshness surface embeds this project's private release-pipeline
|
|
52
|
+
* instructions (`npm run deploy:preview`, smoke preview, …). Those are only
|
|
53
|
+
* meaningful in a repo that ships the pipeline. An external repo joined to a
|
|
54
|
+
* mesh must not have that guidance leak into its coordinator prompt, so this
|
|
55
|
+
* gate keeps the surface off unless a concrete pipeline artifact is present.
|
|
56
|
+
*/
|
|
57
|
+
export function isPreviewPipelineConfigured(repoRoot: string): boolean {
|
|
58
|
+
// Strongest signal: the repo has produced a preview-deploy record before.
|
|
59
|
+
if (existsSync(resolve(repoRoot, PREVIEW_DEPLOY_RECORD))) return true;
|
|
60
|
+
// Otherwise the pipeline's own driver scripts are enough.
|
|
61
|
+
if (PREVIEW_PIPELINE_SCRIPTS.some((rel) => existsSync(resolve(repoRoot, rel)))) return true;
|
|
62
|
+
// Or the `deploy:preview` npm script that fronts the pipeline.
|
|
63
|
+
return hasDeployPreviewNpmScript(repoRoot);
|
|
64
|
+
}
|
|
65
|
+
|
|
26
66
|
function runGit(repoRoot: string, args: readonly string[]): string {
|
|
27
67
|
try {
|
|
28
68
|
return execFileSync('git', args, {
|
|
@@ -86,7 +126,12 @@ function readCurrentMainCommit(repoRoot: string): Pick<PreviewFreshness, 'curren
|
|
|
86
126
|
return { currentMainCommit: null, currentMainCommitSource: 'unknown' };
|
|
87
127
|
}
|
|
88
128
|
|
|
89
|
-
export function buildPreviewFreshness(repoRoot: string): PreviewFreshness {
|
|
129
|
+
export function buildPreviewFreshness(repoRoot: string): PreviewFreshness | null {
|
|
130
|
+
// F15 gate: only surface preview-freshness (and its private pipeline
|
|
131
|
+
// guidance) in repos that actually configure the preview-deploy pipeline.
|
|
132
|
+
// Unconfigured repos return null so the caller omits the field entirely.
|
|
133
|
+
if (!isPreviewPipelineConfigured(repoRoot)) return null;
|
|
134
|
+
|
|
90
135
|
const current = readCurrentMainCommit(repoRoot);
|
|
91
136
|
const record = readRecord(repoRoot);
|
|
92
137
|
const lastPreviewCommit = normalizeCommit(record?.lastPreviewCommit);
|
|
@@ -69,7 +69,7 @@ export const WORKTREE_BOOTSTRAP_STALE_RUNNING_MS = 10 * 60 * 1000;
|
|
|
69
69
|
* trailing slash stripped. Returns an empty set when there are no submodules or the
|
|
70
70
|
* lookup fails — callers must then treat any change as dirty (conservative).
|
|
71
71
|
*/
|
|
72
|
-
function getRegisteredSubmodulePaths(workspace: string): Set<string> {
|
|
72
|
+
export function getRegisteredSubmodulePaths(workspace: string): Set<string> {
|
|
73
73
|
const paths = new Set<string>();
|
|
74
74
|
try {
|
|
75
75
|
const out = execFileSync(
|
|
@@ -92,6 +92,136 @@ function getRegisteredSubmodulePaths(workspace: string): Set<string> {
|
|
|
92
92
|
return paths;
|
|
93
93
|
}
|
|
94
94
|
|
|
95
|
+
/**
|
|
96
|
+
* Read each registered submodule's configured `branch` from `.gitmodules`, keyed
|
|
97
|
+
* by the submodule's normalized path (matching {@link getRegisteredSubmodulePaths}).
|
|
98
|
+
*
|
|
99
|
+
* `.gitmodules` stores `submodule.<name>.path` and (optionally)
|
|
100
|
+
* `submodule.<name>.branch`; this joins the two on `<name>`. The special branch
|
|
101
|
+
* value `.` ("track the superproject's branch") is deliberately OMITTED so callers
|
|
102
|
+
* fall through to remote-HEAD detection instead of treating `.` as a literal branch
|
|
103
|
+
* name. Returns an empty map when there are no submodules, no `.gitmodules`, or the
|
|
104
|
+
* lookup fails (conservative — callers then detect or fall back).
|
|
105
|
+
*/
|
|
106
|
+
export function getSubmoduleConfiguredBranches(workspace: string): Map<string, string> {
|
|
107
|
+
const branchesByPath = new Map<string, string>();
|
|
108
|
+
try {
|
|
109
|
+
const out = execFileSync(
|
|
110
|
+
resolveWin32Executable('git'),
|
|
111
|
+
['config', '--file', '.gitmodules', '--list'],
|
|
112
|
+
{ cwd: workspace, encoding: 'utf8', timeout: 10_000, windowsHide: true },
|
|
113
|
+
);
|
|
114
|
+
// Join `submodule.<name>.path` with `submodule.<name>.branch` on <name>.
|
|
115
|
+
const pathByName = new Map<string, string>();
|
|
116
|
+
const branchByName = new Map<string, string>();
|
|
117
|
+
for (const line of String(out).split(/\r?\n/)) {
|
|
118
|
+
const trimmed = line.trim();
|
|
119
|
+
if (!trimmed) continue;
|
|
120
|
+
const eq = trimmed.indexOf('=');
|
|
121
|
+
if (eq < 0) continue;
|
|
122
|
+
const key = trimmed.slice(0, eq);
|
|
123
|
+
const value = trimmed.slice(eq + 1).trim();
|
|
124
|
+
// key: submodule.<name>.<field>; <name> may itself contain dots, so match
|
|
125
|
+
// the leading `submodule.` and trailing `.<field>` and take the middle.
|
|
126
|
+
const match = /^submodule\.(.+)\.(path|branch)$/.exec(key);
|
|
127
|
+
if (!match) continue;
|
|
128
|
+
const name = match[1];
|
|
129
|
+
if (match[2] === 'path') {
|
|
130
|
+
const norm = value.replace(/\\/g, '/').replace(/\/+$/, '');
|
|
131
|
+
if (norm) pathByName.set(name, norm);
|
|
132
|
+
} else if (value) {
|
|
133
|
+
branchByName.set(name, value);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
for (const [name, submodulePath] of pathByName) {
|
|
137
|
+
const branch = branchByName.get(name);
|
|
138
|
+
if (branch && branch !== '.') branchesByPath.set(submodulePath, branch);
|
|
139
|
+
}
|
|
140
|
+
} catch {
|
|
141
|
+
// No .gitmodules / git error → no configured branches.
|
|
142
|
+
}
|
|
143
|
+
return branchesByPath;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** Fallback submodule branch when no configured/detected default can be resolved. */
|
|
147
|
+
export const SUBMODULE_DEFAULT_BRANCH_FALLBACK = 'main';
|
|
148
|
+
|
|
149
|
+
function isPlausibleBranchName(name: unknown): name is string {
|
|
150
|
+
return typeof name === 'string' && name.length > 0 && !/\s/.test(name) && name !== 'HEAD';
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Resolve the default branch a submodule's commits are published to / checked for
|
|
155
|
+
* reachability against. Generalizes the previously hardcoded `main` so a submodule
|
|
156
|
+
* whose default branch is `master`/`trunk`/etc. is handled. Priority (each tier
|
|
157
|
+
* falls through to the next on miss/error):
|
|
158
|
+
*
|
|
159
|
+
* 1. `.gitmodules` `submodule.<name>.branch` (via {@link getSubmoduleConfiguredBranches};
|
|
160
|
+
* `.` is ignored) — an explicit, local, zero-cost declaration.
|
|
161
|
+
* 2. the submodule checkout's LOCAL remote HEAD: `git symbolic-ref --short
|
|
162
|
+
* refs/remotes/<remote>/HEAD` → strip the `<remote>/` prefix (no network).
|
|
163
|
+
* 3. the submodule remote's advertised HEAD: `git ls-remote --symref <remote> HEAD`
|
|
164
|
+
* → `ref: refs/heads/<branch>` (one network round-trip).
|
|
165
|
+
* 4. fallback {@link SUBMODULE_DEFAULT_BRANCH_FALLBACK} (`'main'`).
|
|
166
|
+
*
|
|
167
|
+
* Because the final fallback is `'main'` and every earlier tier that resolves `'main'`
|
|
168
|
+
* yields the same string, a repo whose submodules default to `main` (the common case)
|
|
169
|
+
* produces byte-identical downstream fetch/merge-base/push ref targets — only a
|
|
170
|
+
* read-only resolution probe is added.
|
|
171
|
+
*/
|
|
172
|
+
export async function resolveSubmoduleDefaultBranch(opts: {
|
|
173
|
+
/** The submodule's local checkout — cwd for symbolic-ref / ls-remote. */
|
|
174
|
+
submoduleRepoPath: string;
|
|
175
|
+
/** The superproject workspace — for the `.gitmodules` branch lookup (tier 1). */
|
|
176
|
+
superprojectWorkspace?: string;
|
|
177
|
+
/** The submodule's path relative to the superproject (key into `.gitmodules`). */
|
|
178
|
+
submodulePath?: string;
|
|
179
|
+
/** Remote name (default `origin`). */
|
|
180
|
+
remote?: string;
|
|
181
|
+
/** Timeout for the local probe (tier 2); the network probe (tier 3) gets max(this, 30s). */
|
|
182
|
+
timeoutMs?: number;
|
|
183
|
+
}): Promise<string> {
|
|
184
|
+
const remote = opts.remote?.trim() || 'origin';
|
|
185
|
+
const localTimeout = opts.timeoutMs ?? 10_000;
|
|
186
|
+
const git = resolveWin32Executable('git');
|
|
187
|
+
const execFileAsync = promisify(execFile);
|
|
188
|
+
|
|
189
|
+
// Tier 1: .gitmodules configured branch (local, zero-cost).
|
|
190
|
+
if (opts.superprojectWorkspace && opts.submodulePath) {
|
|
191
|
+
try {
|
|
192
|
+
const normalized = opts.submodulePath.replace(/\\/g, '/').replace(/\/+$/, '');
|
|
193
|
+
const configured = getSubmoduleConfiguredBranches(opts.superprojectWorkspace).get(normalized);
|
|
194
|
+
if (isPlausibleBranchName(configured)) return configured;
|
|
195
|
+
} catch { /* fall through */ }
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Tier 2: the local remote HEAD (no network) — set by clone/`git remote set-head`.
|
|
199
|
+
try {
|
|
200
|
+
const { stdout } = await execFileAsync(
|
|
201
|
+
git,
|
|
202
|
+
['symbolic-ref', '--short', `refs/remotes/${remote}/HEAD`],
|
|
203
|
+
{ cwd: opts.submoduleRepoPath, encoding: 'utf8', timeout: localTimeout, windowsHide: true },
|
|
204
|
+
);
|
|
205
|
+
const short = String(stdout || '').trim();
|
|
206
|
+
const prefix = `${remote}/`;
|
|
207
|
+
const branch = short.startsWith(prefix) ? short.slice(prefix.length) : short;
|
|
208
|
+
if (isPlausibleBranchName(branch)) return branch;
|
|
209
|
+
} catch { /* fall through */ }
|
|
210
|
+
|
|
211
|
+
// Tier 3: the remote's advertised HEAD (one network round-trip).
|
|
212
|
+
try {
|
|
213
|
+
const { stdout } = await execFileAsync(
|
|
214
|
+
git,
|
|
215
|
+
['ls-remote', '--symref', remote, 'HEAD'],
|
|
216
|
+
{ cwd: opts.submoduleRepoPath, encoding: 'utf8', timeout: Math.max(localTimeout, 30_000), windowsHide: true },
|
|
217
|
+
);
|
|
218
|
+
const match = /^ref:\s+refs\/heads\/(\S+)\s+HEAD/m.exec(String(stdout || ''));
|
|
219
|
+
if (match && isPlausibleBranchName(match[1])) return match[1];
|
|
220
|
+
} catch { /* fall through */ }
|
|
221
|
+
|
|
222
|
+
return SUBMODULE_DEFAULT_BRANCH_FALLBACK;
|
|
223
|
+
}
|
|
224
|
+
|
|
95
225
|
/**
|
|
96
226
|
* True when `git status --porcelain` output represents a worktree that is clean
|
|
97
227
|
* EXCEPT for submodule-gitlink-pointer moves. A worktree task that commits inside a
|
|
@@ -22,6 +22,8 @@ import type { PtyRuntimeMetadata, PtyTransportFactory } from '../cli-adapters/pt
|
|
|
22
22
|
import { StatusMonitor } from './status-monitor.js';
|
|
23
23
|
import { ChatHistoryWriter, isNativeSourceCanonicalHistory, materializeProviderNativeHistory, readChatHistory, readProviderChatHistory } from '../config/chat-history.js';
|
|
24
24
|
import { LOG } from '../logging/logger.js';
|
|
25
|
+
import { recordDebugTrace } from '../logging/debug-trace.js';
|
|
26
|
+
import { shouldCollectTraceCategory } from '../logging/debug-config.js';
|
|
25
27
|
import { traceMeshEventStage, traceMeshEventDrop } from '../mesh/mesh-event-trace.js';
|
|
26
28
|
import type { ChatMessage } from '../types.js';
|
|
27
29
|
import { buildPersistedProviderEffectMessage, normalizeProviderEffects } from './control-effects.js';
|
|
@@ -1526,9 +1528,23 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
1526
1528
|
}
|
|
1527
1529
|
|
|
1528
1530
|
private completionFinalAssistantEvidence(parsedMessages: unknown, turnStartedAt?: number): CompletionFinalAssistantEvidence {
|
|
1531
|
+
// (FALSEIDLE FixB) UPPER-BOUND turn-end evidence. completionHasFinalAssistantMessage is a
|
|
1532
|
+
// pure message-content check ("does the last visible bubble read as a finalized assistant
|
|
1533
|
+
// reply, post-dating the turn start?"). That LOWER bound alone treated the FIRST assistant
|
|
1534
|
+
// bubble of a turn that is STILL running — a tool call in flight between two assistant
|
|
1535
|
+
// bubbles — as proof the turn ended (RCA cases a & b). Require in ADDITION that the turn is
|
|
1536
|
+
// genuinely OVER: hasAdapterPendingResponse() folds the three upper-bound discriminators
|
|
1537
|
+
// into one — currentTurnScope closed, no in-flight tool (isProcessing false), and no partial
|
|
1538
|
+
// response buffer. So a mid-turn point-sample (short-gen / fast-collapse inline paths that
|
|
1539
|
+
// do NOT route through getCompletedFinalizationBlock) yields present=false and is held/settled
|
|
1540
|
+
// rather than fired. A genuinely-finished turn (adapter idle, no pending) is unaffected — the
|
|
1541
|
+
// gate stays open and the completion fires exactly as before. This mirrors the established
|
|
1542
|
+
// `completionHasFinalAssistantMessage(...) && !hasAdapterPendingResponse()` pairing already
|
|
1543
|
+
// used by the no-progress monitor reconcile path.
|
|
1544
|
+
const turnClosed = !this.hasAdapterPendingResponse();
|
|
1529
1545
|
if (this.completionHasFinalAssistantMessage(parsedMessages, turnStartedAt)) {
|
|
1530
1546
|
return {
|
|
1531
|
-
present:
|
|
1547
|
+
present: turnClosed,
|
|
1532
1548
|
messages: Array.isArray(parsedMessages) ? parsedMessages : [],
|
|
1533
1549
|
source: 'parsed',
|
|
1534
1550
|
};
|
|
@@ -1537,7 +1553,7 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
1537
1553
|
const externalMessages = this.readExternalCompletionMessages();
|
|
1538
1554
|
if (externalMessages) {
|
|
1539
1555
|
return {
|
|
1540
|
-
present: this.completionHasFinalAssistantMessage(externalMessages, turnStartedAt),
|
|
1556
|
+
present: turnClosed && this.completionHasFinalAssistantMessage(externalMessages, turnStartedAt),
|
|
1541
1557
|
messages: externalMessages,
|
|
1542
1558
|
source: 'external-native',
|
|
1543
1559
|
};
|
|
@@ -1690,11 +1706,21 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
1690
1706
|
|
|
1691
1707
|
const adapterAny = this.adapter as any;
|
|
1692
1708
|
const approvalResolvedIdle = pending.previousStatus === 'waiting_approval';
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1709
|
+
// (FALSEIDLE-a FixA) The adapter pending-response checks run UNCONDITIONALLY.
|
|
1710
|
+
// Previously they were SKIPPED when approvalResolvedIdle, on the assumption that
|
|
1711
|
+
// a waiting_approval→idle transition proved the approval's turn was over. But
|
|
1712
|
+
// auto-approve RESOLVES the modal and the agent RESUMES the same turn — currentTurnScope
|
|
1713
|
+
// / isWaitingForResponse stay set, or a tool runs — so skipping the guard let the FIRST
|
|
1714
|
+
// assistant bubble of the still-running turn be mistaken for the last and fired an early
|
|
1715
|
+
// completion the coordinator could never correct (RCA case a). Keep the guard live for the
|
|
1716
|
+
// approval path too: when the resumed turn genuinely ends these clear and the completion
|
|
1717
|
+
// fires. Approval-resolved holds are NON-terminal (bounded by COMPLETED_FINALIZATION_MAX_WAIT_MS)
|
|
1718
|
+
// so a provider that never closes its turn-scope still force-fires a weak completion rather
|
|
1719
|
+
// than wedging; the non-approval path keeps its terminal hold (a genuinely-busy adapter must
|
|
1720
|
+
// never force a completion out).
|
|
1721
|
+
if (adapterAny?.isWaitingForResponse === true) return { reason: 'adapter_waiting_for_response', terminal: !approvalResolvedIdle };
|
|
1722
|
+
if (adapterAny?.currentTurnScope) return { reason: 'adapter_turn_scope_active', terminal: !approvalResolvedIdle };
|
|
1723
|
+
if (this.hasAdapterPendingResponse()) return { reason: 'adapter_pending_response', terminal: !approvalResolvedIdle };
|
|
1698
1724
|
|
|
1699
1725
|
const partial = typeof this.adapter.getPartialResponse === 'function'
|
|
1700
1726
|
? this.adapter.getPartialResponse()
|
|
@@ -1891,6 +1917,40 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
1891
1917
|
};
|
|
1892
1918
|
}
|
|
1893
1919
|
|
|
1920
|
+
// COMPLETION-EARLYNOTIFY instrumentation. A session-keyed FSM-transition +
|
|
1921
|
+
// completion-gate snapshot recorded into the shared debug-trace ring buffer
|
|
1922
|
+
// (secret-safe, length/role/pattern-name only — never screen or bubble text).
|
|
1923
|
+
// Retrieved via getRecentDebugTrace (chat_debug_bundle). Both categories are a
|
|
1924
|
+
// no-op unless collectDebugTrace is on AND the category is selected, so the
|
|
1925
|
+
// hot-path guards below (completionTraceOn / fsmTraceOn) keep production cost
|
|
1926
|
+
// at a single boolean check.
|
|
1927
|
+
private completionTraceOn(): boolean {
|
|
1928
|
+
return shouldCollectTraceCategory('completion-gate');
|
|
1929
|
+
}
|
|
1930
|
+
private fsmTraceOn(): boolean {
|
|
1931
|
+
return shouldCollectTraceCategory('fsm-transition');
|
|
1932
|
+
}
|
|
1933
|
+
private recordCompletionGateTrace(stage: string, payload: Record<string, unknown>): void {
|
|
1934
|
+
recordDebugTrace({
|
|
1935
|
+
category: 'completion-gate',
|
|
1936
|
+
stage,
|
|
1937
|
+
level: 'debug',
|
|
1938
|
+
sessionId: this.instanceId,
|
|
1939
|
+
providerType: this.type,
|
|
1940
|
+
payload,
|
|
1941
|
+
});
|
|
1942
|
+
}
|
|
1943
|
+
private recordFsmTransitionTrace(payload: Record<string, unknown>): void {
|
|
1944
|
+
recordDebugTrace({
|
|
1945
|
+
category: 'fsm-transition',
|
|
1946
|
+
stage: 'transition',
|
|
1947
|
+
level: 'debug',
|
|
1948
|
+
sessionId: this.instanceId,
|
|
1949
|
+
providerType: this.type,
|
|
1950
|
+
payload,
|
|
1951
|
+
});
|
|
1952
|
+
}
|
|
1953
|
+
|
|
1894
1954
|
private flushCompletedDebounceIfFinalized(): void {
|
|
1895
1955
|
const pending = this.completedDebouncePending;
|
|
1896
1956
|
if (!pending) {
|
|
@@ -1904,6 +1964,13 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
1904
1964
|
LOG.debug('CLI', `[${this.type}] flush attempt: adapterStatus=${latestStatus.status} latestVisible=${latestVisibleStatus} generatingStartedAt=${this.generatingStartedAt} isWaitingForResponse=${!!(this.adapter as any)?.isWaitingForResponse} hasPartial=${!!this.adapter.getPartialResponse?.()}`);
|
|
1905
1965
|
if (latestVisibleStatus !== 'idle') {
|
|
1906
1966
|
LOG.info('CLI', `[${this.type}] cancelled pending completed (resumed ${latestVisibleStatus})`);
|
|
1967
|
+
if (this.completionTraceOn()) this.recordCompletionGateTrace('cancel', {
|
|
1968
|
+
blockReason: 'resumed_status',
|
|
1969
|
+
latestVisibleStatus,
|
|
1970
|
+
previousStatus: pending.previousStatus,
|
|
1971
|
+
busyEpochAtArm: pending.busyEpochAtArm,
|
|
1972
|
+
busyEpoch: this.busyEpoch,
|
|
1973
|
+
});
|
|
1907
1974
|
this.completedDebouncePending = null;
|
|
1908
1975
|
this.completedDebounceTimer = null;
|
|
1909
1976
|
return;
|
|
@@ -1921,6 +1988,14 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
1921
1988
|
// so shared behaviour for claude/codex/antigravity is strictly stricter, never looser.
|
|
1922
1989
|
if (typeof pending.busyEpochAtArm === 'number' && this.busyEpoch !== pending.busyEpochAtArm) {
|
|
1923
1990
|
LOG.info('CLI', `[${this.type}] cancelled pending completed (busy re-entry during settle: epoch ${pending.busyEpochAtArm}→${this.busyEpoch})`);
|
|
1991
|
+
if (this.completionTraceOn()) this.recordCompletionGateTrace('cancel', {
|
|
1992
|
+
blockReason: 'busy_reentry',
|
|
1993
|
+
latestVisibleStatus,
|
|
1994
|
+
previousStatus: pending.previousStatus,
|
|
1995
|
+
busyEpochAtArm: pending.busyEpochAtArm,
|
|
1996
|
+
busyEpoch: this.busyEpoch,
|
|
1997
|
+
busyEpochDelta: this.busyEpoch - pending.busyEpochAtArm,
|
|
1998
|
+
});
|
|
1924
1999
|
this.completedDebouncePending = null;
|
|
1925
2000
|
this.completedDebounceTimer = null;
|
|
1926
2001
|
return;
|
|
@@ -1930,6 +2005,14 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
1930
2005
|
&& typeof latestOutputAt === 'number'
|
|
1931
2006
|
&& latestOutputAt > pending.lastOutputAtArm) {
|
|
1932
2007
|
LOG.info('CLI', `[${this.type}] cancelled pending completed (new PTY output during settle: ${pending.lastOutputAtArm}→${latestOutputAt})`);
|
|
2008
|
+
if (this.completionTraceOn()) this.recordCompletionGateTrace('cancel', {
|
|
2009
|
+
blockReason: 'new_pty_output',
|
|
2010
|
+
latestVisibleStatus,
|
|
2011
|
+
previousStatus: pending.previousStatus,
|
|
2012
|
+
lastOutputAtArm: pending.lastOutputAtArm,
|
|
2013
|
+
lastOutputAt: latestOutputAt,
|
|
2014
|
+
lastOutputAtDelta: latestOutputAt - pending.lastOutputAtArm,
|
|
2015
|
+
});
|
|
1933
2016
|
this.completedDebouncePending = null;
|
|
1934
2017
|
this.completedDebounceTimer = null;
|
|
1935
2018
|
return;
|
|
@@ -1975,6 +2058,17 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
1975
2058
|
if (this.isMeshWorkerSession()) {
|
|
1976
2059
|
traceMeshEventDrop('completion_gate_hold', this.meshTraceCtx(), `${blockReason} waited=${waitedMs}ms`);
|
|
1977
2060
|
}
|
|
2061
|
+
// COMPLETION-EARLYNOTIFY: a hold is the CORRECT outcome when the turn is not
|
|
2062
|
+
// yet proven done (the FixA/FixB gates route here); trace it so an early-notify
|
|
2063
|
+
// investigation can see the gate holding rather than firing.
|
|
2064
|
+
if (this.completionTraceOn()) this.recordCompletionGateTrace('hold', {
|
|
2065
|
+
blockReason,
|
|
2066
|
+
latestVisibleStatus,
|
|
2067
|
+
terminal: block.terminal === true,
|
|
2068
|
+
holdForTranscript: block.holdForTranscript === true,
|
|
2069
|
+
approvalResolvedIdle: pending.previousStatus === 'waiting_approval',
|
|
2070
|
+
waitedMs,
|
|
2071
|
+
});
|
|
1978
2072
|
pending.loggedBlockReason = blockReason;
|
|
1979
2073
|
}
|
|
1980
2074
|
this.scheduleCompletedDebounceFlush(COMPLETED_FINALIZATION_RETRY_MS);
|
|
@@ -1997,6 +2091,19 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
1997
2091
|
if (this.isMeshWorkerSession()) {
|
|
1998
2092
|
traceMeshEventStage('fired', this.meshTraceCtx(), `forced after ${waitedMs}ms (${blockReason})`);
|
|
1999
2093
|
}
|
|
2094
|
+
if (this.completionTraceOn()) this.recordCompletionGateTrace('fire', {
|
|
2095
|
+
path: isTranscriptEvidenceGate && !emittedAfterFinalizationTimeout ? 'canon_c_decoupled' : 'forced_timeout',
|
|
2096
|
+
blockReason,
|
|
2097
|
+
latestVisibleStatus,
|
|
2098
|
+
approvalResolvedIdle: pending.previousStatus === 'waiting_approval',
|
|
2099
|
+
finalAssistantPresent: (completionDiagnostic as any).finalAssistantPresent === true,
|
|
2100
|
+
evidenceSource: (completionDiagnostic as any).finalAssistantEvidenceSource ?? null,
|
|
2101
|
+
lastVisibleRole: (completionDiagnostic as any).lastVisibleRole ?? null,
|
|
2102
|
+
lastVisibleContentLen: (completionDiagnostic as any).lastVisibleContentLength ?? null,
|
|
2103
|
+
emittedAfterFinalizationTimeout,
|
|
2104
|
+
waitedMs,
|
|
2105
|
+
busyEpoch: this.busyEpoch,
|
|
2106
|
+
});
|
|
2000
2107
|
this.pushEvent({
|
|
2001
2108
|
event: 'agent:generating_completed',
|
|
2002
2109
|
chatTitle: pending.chatTitle,
|
|
@@ -2028,6 +2135,14 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
2028
2135
|
if (this.isMeshWorkerSession()) {
|
|
2029
2136
|
traceMeshEventStage('fired', this.meshTraceCtx(), `duration=${pending.duration}s`);
|
|
2030
2137
|
}
|
|
2138
|
+
if (this.completionTraceOn()) this.recordCompletionGateTrace('fire', {
|
|
2139
|
+
path: 'clean',
|
|
2140
|
+
latestVisibleStatus,
|
|
2141
|
+
approvalResolvedIdle: pending.previousStatus === 'waiting_approval',
|
|
2142
|
+
finalAssistantPresent: true,
|
|
2143
|
+
duration: pending.duration,
|
|
2144
|
+
busyEpoch: this.busyEpoch,
|
|
2145
|
+
});
|
|
2031
2146
|
this.pushEvent({
|
|
2032
2147
|
event: 'agent:generating_completed',
|
|
2033
2148
|
chatTitle: pending.chatTitle,
|
|
@@ -2424,6 +2539,22 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
2424
2539
|
const previousStatus = this.lastStatus;
|
|
2425
2540
|
if (newStatus !== this.lastStatus) {
|
|
2426
2541
|
LOG.info('CLI', `[${this.type}] status: ${this.lastStatus} → ${newStatus}`);
|
|
2542
|
+
// COMPLETION-EARLYNOTIFY: snapshot every FSM status transition (the arm/fire/cancel
|
|
2543
|
+
// decisions downstream all hang off these edges). Guarded so production pays only a
|
|
2544
|
+
// boolean check; payload carries the visibility/auto-approve flags and the continuity
|
|
2545
|
+
// clocks (busyEpoch / lastOutputAt / lastScreenChangeAt) that the completion gate reads.
|
|
2546
|
+
if (this.fsmTraceOn()) this.recordFsmTransitionTrace({
|
|
2547
|
+
from: this.lastStatus,
|
|
2548
|
+
to: newStatus,
|
|
2549
|
+
rawStatus,
|
|
2550
|
+
autoApproveActive,
|
|
2551
|
+
autoApproveHoldIdle,
|
|
2552
|
+
autoApproveBusy: this.autoApproveBusy,
|
|
2553
|
+
hasPending: this.hasAdapterPendingResponse(),
|
|
2554
|
+
busyEpoch: this.busyEpoch,
|
|
2555
|
+
lastOutputAt: typeof adapterStatus?.lastOutputAt === 'number' ? adapterStatus.lastOutputAt : null,
|
|
2556
|
+
lastScreenChangeAt: typeof adapterStatus?.lastScreenChangeAt === 'number' ? adapterStatus.lastScreenChangeAt : null,
|
|
2557
|
+
});
|
|
2427
2558
|
// GENERATING-MISSING (win32 fresh-worktree first-turn): a freshly-launched session
|
|
2428
2559
|
// is in 'starting' until its startup-grace settles to idle. When the FIRST inject
|
|
2429
2560
|
// lands inside that grace window, the adapter can report status DIRECTLY
|
|
@@ -2661,6 +2792,17 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
2661
2792
|
if (this.isMeshWorkerSession()) {
|
|
2662
2793
|
traceMeshEventStage('arm', this.meshTraceCtx(), `short-generating settle-arm (source=${shortEvidenceSource}, missingEvidence=${missingEvidence})`);
|
|
2663
2794
|
}
|
|
2795
|
+
if (this.completionTraceOn()) this.recordCompletionGateTrace('arm', {
|
|
2796
|
+
branch: 'short_generating',
|
|
2797
|
+
previousStatus: this.lastStatus,
|
|
2798
|
+
turnStartedAt: shortTurnStartedAt || null,
|
|
2799
|
+
busyEpochAtArm: this.busyEpoch,
|
|
2800
|
+
lastOutputAtArm: typeof adapterStatus?.lastOutputAt === 'number' ? adapterStatus.lastOutputAt : null,
|
|
2801
|
+
flushDelay: NATIVE_HISTORY_MESH_IDLE_SETTLE_MS,
|
|
2802
|
+
evidenceSource: shortEvidenceSource,
|
|
2803
|
+
missingEvidence,
|
|
2804
|
+
hasFinalSummary: !!shortFinalSummary,
|
|
2805
|
+
});
|
|
2664
2806
|
this.scheduleCompletedDebounceFlush(NATIVE_HISTORY_MESH_IDLE_SETTLE_MS);
|
|
2665
2807
|
} else if (missingEvidence) {
|
|
2666
2808
|
// NON-MESH, missing evidence: suppress the completion event entirely (the
|
|
@@ -2749,6 +2891,16 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
2749
2891
|
? (meshSettleSession ? NATIVE_HISTORY_MESH_IDLE_SETTLE_MS : 0)
|
|
2750
2892
|
: 3000;
|
|
2751
2893
|
LOG.debug('CLI', `[${this.type}] set completedDebouncePending duration=${duration}s ownsExternalHistory=${ownsExternalHistory} meshSettle=${meshSettleSession} flushDelay=${flushDelay}ms generatingStartedAt=${this.generatingStartedAt}`);
|
|
2894
|
+
if (this.completionTraceOn()) this.recordCompletionGateTrace('arm', {
|
|
2895
|
+
branch: 'normal',
|
|
2896
|
+
previousStatus: this.completedDebouncePending.previousStatus,
|
|
2897
|
+
turnStartedAt: this.completedDebouncePending.turnStartedAt ?? null,
|
|
2898
|
+
busyEpochAtArm: this.completedDebouncePending.busyEpochAtArm ?? null,
|
|
2899
|
+
lastOutputAtArm: this.completedDebouncePending.lastOutputAtArm ?? null,
|
|
2900
|
+
flushDelay,
|
|
2901
|
+
ownsExternalHistory,
|
|
2902
|
+
meshSettle: meshSettleSession,
|
|
2903
|
+
});
|
|
2752
2904
|
this.scheduleCompletedDebounceFlush(flushDelay);
|
|
2753
2905
|
}
|
|
2754
2906
|
} else if (newStatus === 'idle' && this.lastStatus === 'starting') {
|