@adhdev/daemon-core 0.9.82-rc.328 → 0.9.82-rc.329
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/cli-adapters/provider-cli-shared.d.ts +1 -1
- package/dist/git/change-impact-config.d.ts +159 -0
- package/dist/git/git-status.d.ts +14 -0
- package/dist/git/index.d.ts +2 -0
- package/dist/index.js +647 -281
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +598 -238
- package/dist/index.mjs.map +1 -1
- package/dist/mesh/mesh-events-stale.d.ts +1 -1
- package/dist/providers/status-monitor.d.ts +7 -7
- package/dist/shared-types.d.ts +1 -1
- package/package.json +2 -2
- package/src/agent-stream/provider-adapter.ts +1 -1
- package/src/cli-adapters/provider-cli-adapter.ts +2 -2
- package/src/cli-adapters/provider-cli-shared.ts +1 -1
- package/src/commands/chat-commands.ts +1 -1
- package/src/commands/cli-manager.ts +1 -1
- package/src/commands/router.ts +46 -0
- package/src/git/change-impact-config.ts +354 -0
- package/src/git/git-status.ts +182 -16
- package/src/git/index.ts +16 -0
- package/src/mesh/mesh-events-coordinator.ts +13 -12
- package/src/mesh/mesh-events-stale.ts +4 -4
- package/src/mesh/mesh-events-utils.ts +3 -3
- package/src/providers/acp-provider-instance.ts +6 -5
- package/src/providers/cli-provider-instance.ts +14 -10
- package/src/providers/extension-provider-instance.ts +7 -6
- package/src/providers/ide-provider-instance.ts +10 -6
- package/src/providers/read-chat-contract.ts +1 -1
- package/src/providers/status-monitor.d.ts +7 -7
- package/src/providers/status-monitor.ts +37 -22
- package/src/shared-types.ts +3 -0
- package/src/status/reporter.ts +1 -1
package/src/git/git-status.ts
CHANGED
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
import type { DaemonBuildBehind, GitRepoStatus, GitSubmoduleStatus, GitUpstreamFreshness } from './git-types.js';
|
|
2
2
|
import { GIT_STATUS_TIMEOUT_MS, GitCommandError, resolveGitRepository, runGit } from './git-executor.js';
|
|
3
3
|
import { getDaemonBuildInfo, type DaemonBuildInfo } from '../build-info.js';
|
|
4
|
+
import {
|
|
5
|
+
type ChangeImpactConfig,
|
|
6
|
+
type ChangeImpactKind,
|
|
7
|
+
type ChangeImpactTarget,
|
|
8
|
+
globToRegExp,
|
|
9
|
+
loadChangeImpactConfig,
|
|
10
|
+
} from './change-impact-config.js';
|
|
4
11
|
|
|
5
12
|
type ResolvedGitRepo = { workspace: string; repoRoot: string | null; isGitRepo: boolean };
|
|
6
13
|
|
|
@@ -12,9 +19,42 @@ type ResolvedGitRepo = { workspace: string; repoRoot: string | null; isGitRepo:
|
|
|
12
19
|
*/
|
|
13
20
|
const lastKnownGoodStatus = new Map<string, GitRepoStatus>();
|
|
14
21
|
|
|
22
|
+
/**
|
|
23
|
+
* Memoized Change Impact evaluation, keyed by the inputs that can change the
|
|
24
|
+
* verdict: the scope repo path, the buildCommit..HEAD pair, and the resolved
|
|
25
|
+
* config source. The daemonBuildBehind probe runs on every mesh_status /
|
|
26
|
+
* mesh_git_status / fast_forward / git-monitor hit; without this, each hit
|
|
27
|
+
* re-shells `git diff` for the identical commit range. The cache stays correct
|
|
28
|
+
* because HEAD or a config edit perturbs the key, forcing re-evaluation.
|
|
29
|
+
*/
|
|
30
|
+
interface ChangeImpactEvalEntry {
|
|
31
|
+
isDaemonAffecting: boolean;
|
|
32
|
+
affectedPackages: string[];
|
|
33
|
+
}
|
|
34
|
+
const changeImpactEvalCache = new Map<string, ChangeImpactEvalEntry>();
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Best-effort cache of the loaded Change Impact config per repo root, keyed by the
|
|
38
|
+
* config sourceKey (path+mtime). A new sourceKey (config edited/added/removed)
|
|
39
|
+
* supersedes the entry. Avoids re-reading + re-parsing the config file on every
|
|
40
|
+
* status probe while still honoring on-disk edits.
|
|
41
|
+
*/
|
|
42
|
+
interface ChangeImpactConfigCacheEntry {
|
|
43
|
+
sourceKey: string;
|
|
44
|
+
config: ChangeImpactConfig | null;
|
|
45
|
+
}
|
|
46
|
+
const changeImpactConfigCache = new Map<string, ChangeImpactConfigCacheEntry>();
|
|
47
|
+
|
|
15
48
|
/** Test seam: clear the last-known-good status cache between cases. */
|
|
16
49
|
export function __resetGitStatusCacheForTests(): void {
|
|
17
50
|
lastKnownGoodStatus.clear();
|
|
51
|
+
changeImpactEvalCache.clear();
|
|
52
|
+
changeImpactConfigCache.clear();
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Test-only introspection: number of memoized Change Impact evaluations. */
|
|
56
|
+
export function __changeImpactEvalCacheSizeForTests(): number {
|
|
57
|
+
return changeImpactEvalCache.size;
|
|
18
58
|
}
|
|
19
59
|
|
|
20
60
|
/**
|
|
@@ -43,6 +83,17 @@ export interface GitStatusOptions {
|
|
|
43
83
|
* (getDaemonBuildInfo) is used.
|
|
44
84
|
*/
|
|
45
85
|
daemonBuildInfo?: DaemonBuildInfo;
|
|
86
|
+
/**
|
|
87
|
+
* Change Impact policy override. When provided, the daemonBuildBehind
|
|
88
|
+
* classifier uses this declarative config instead of auto-loading the repo's
|
|
89
|
+
* `.adhdev/change-impact.*` file. When omitted, getGitRepoStatus auto-loads the
|
|
90
|
+
* repo config (cached); when neither is present the built-in ADHDev default
|
|
91
|
+
* policy applies, preserving the legacy behavior exactly.
|
|
92
|
+
*
|
|
93
|
+
* Pass `null` to force the built-in default policy and skip auto-loading (used
|
|
94
|
+
* to assert legacy parity in tests).
|
|
95
|
+
*/
|
|
96
|
+
changeImpactConfig?: ChangeImpactConfig | null;
|
|
46
97
|
}
|
|
47
98
|
|
|
48
99
|
interface GitUpstreamProbe {
|
|
@@ -177,7 +228,7 @@ async function collectGitRepoStatus(
|
|
|
177
228
|
* same process surface as the daemon tooling, so it is classified as
|
|
178
229
|
* daemon-affecting (conservative). Unknown package → daemon-affecting.
|
|
179
230
|
*/
|
|
180
|
-
const
|
|
231
|
+
const DEFAULT_DAEMON_RUNTIME_PACKAGES = [
|
|
181
232
|
'daemon-core',
|
|
182
233
|
'daemon-standalone',
|
|
183
234
|
'session-host-core',
|
|
@@ -187,14 +238,66 @@ const DAEMON_RUNTIME_PACKAGES = new Set([
|
|
|
187
238
|
'terminal-mux-cli',
|
|
188
239
|
'ghostty-vt-node',
|
|
189
240
|
'mcp-server',
|
|
190
|
-
]
|
|
241
|
+
];
|
|
191
242
|
|
|
192
|
-
const
|
|
243
|
+
const DEFAULT_WEB_ONLY_PACKAGES = [
|
|
193
244
|
'web-core',
|
|
194
245
|
'web-standalone',
|
|
195
246
|
'web-devconsole',
|
|
196
247
|
'terminal-render-web',
|
|
197
|
-
]
|
|
248
|
+
];
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Built-in recommended action/command per impact classification. A change-impact
|
|
252
|
+
* config may override any of these via `impactTargets`; missing keys fall back here.
|
|
253
|
+
*/
|
|
254
|
+
const DEFAULT_IMPACT_TARGETS: Record<ChangeImpactKind, ChangeImpactTarget> = {
|
|
255
|
+
daemon: {
|
|
256
|
+
recommendedCommand: 'Redeploy + restart the daemon (a local dist rebuild alone does not update a cloud daemon).',
|
|
257
|
+
},
|
|
258
|
+
web: {
|
|
259
|
+
recommendedCommand: 'Redeploy the web app (no daemon restart required).',
|
|
260
|
+
},
|
|
261
|
+
none: {
|
|
262
|
+
recommendedCommand: 'No action required.',
|
|
263
|
+
},
|
|
264
|
+
};
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* The resolved Change Impact policy: the built-in ADHDev defaults merged with any
|
|
268
|
+
* config override. This is what the classifier consults — git-status only knows the
|
|
269
|
+
* facts (changed files/packages), policy is data.
|
|
270
|
+
*/
|
|
271
|
+
interface ResolvedChangeImpactPolicy {
|
|
272
|
+
daemonRuntimePackages: Set<string>;
|
|
273
|
+
webOnlyPackages: Set<string>;
|
|
274
|
+
/** Compiled config globs for additional non-runtime root files (on top of built-ins). */
|
|
275
|
+
nonRuntimeRootFilePatterns: RegExp[];
|
|
276
|
+
impactTargets: Record<ChangeImpactKind, ChangeImpactTarget>;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function resolveChangeImpactPolicy(config: ChangeImpactConfig | null | undefined): ResolvedChangeImpactPolicy {
|
|
280
|
+
// A field provided in config REPLACES the built-in default for that field; an
|
|
281
|
+
// omitted field falls back to the ADHDev default, so a repo with no config (or a
|
|
282
|
+
// partial config) behaves exactly as before.
|
|
283
|
+
const daemonRuntimePackages = new Set(
|
|
284
|
+
config?.daemonRuntimePackages && config.daemonRuntimePackages.length
|
|
285
|
+
? config.daemonRuntimePackages
|
|
286
|
+
: DEFAULT_DAEMON_RUNTIME_PACKAGES,
|
|
287
|
+
);
|
|
288
|
+
const webOnlyPackages = new Set(
|
|
289
|
+
config?.webOnlyPackages && config.webOnlyPackages.length
|
|
290
|
+
? config.webOnlyPackages
|
|
291
|
+
: DEFAULT_WEB_ONLY_PACKAGES,
|
|
292
|
+
);
|
|
293
|
+
const nonRuntimeRootFilePatterns = (config?.nonRuntimeRootFilePatterns || []).map(globToRegExp);
|
|
294
|
+
const impactTargets: Record<ChangeImpactKind, ChangeImpactTarget> = {
|
|
295
|
+
daemon: config?.impactTargets?.daemon ?? DEFAULT_IMPACT_TARGETS.daemon,
|
|
296
|
+
web: config?.impactTargets?.web ?? DEFAULT_IMPACT_TARGETS.web,
|
|
297
|
+
none: config?.impactTargets?.none ?? DEFAULT_IMPACT_TARGETS.none,
|
|
298
|
+
};
|
|
299
|
+
return { daemonRuntimePackages, webOnlyPackages, nonRuntimeRootFilePatterns, impactTargets };
|
|
300
|
+
}
|
|
198
301
|
|
|
199
302
|
/**
|
|
200
303
|
* Root-level (non-package) files that demonstrably cannot change what the daemon
|
|
@@ -210,8 +313,11 @@ const WEB_ONLY_PACKAGES = new Set([
|
|
|
210
313
|
* real-world case (e.g. `.verify-patch-equiv-rc292`), so they are matched broadly;
|
|
211
314
|
* docs are matched by the `docs/` prefix or a markdown/text-doc extension at a
|
|
212
315
|
* filename we recognize as documentation (README/CHANGELOG/LICENSE/NOTICE).
|
|
316
|
+
*
|
|
317
|
+
* Config-supplied `nonRuntimeRootFilePatterns` are matched in ADDITION to these
|
|
318
|
+
* built-ins, so a repo can extend (never narrow) the benign set declaratively.
|
|
213
319
|
*/
|
|
214
|
-
function isNonRuntimeRootFile(file: string): boolean {
|
|
320
|
+
function isNonRuntimeRootFile(file: string, policy: ResolvedChangeImpactPolicy): boolean {
|
|
215
321
|
const base = file.slice(file.lastIndexOf('/') + 1);
|
|
216
322
|
// Verify/convergence markers: dotfiles whose name signals a transient marker.
|
|
217
323
|
if (/^\.(?:verify|marker|converge|ff-verify|patch-equiv|live-verify)\b/i.test(base)) return true;
|
|
@@ -221,19 +327,24 @@ function isNonRuntimeRootFile(file: string): boolean {
|
|
|
221
327
|
if (/^(?:README|CHANGELOG|LICENSE|NOTICE|AUTHORS|CONTRIBUTING|CODEOWNERS)(?:\.[A-Za-z0-9]+)?$/i.test(base)) {
|
|
222
328
|
return true;
|
|
223
329
|
}
|
|
330
|
+
// Config-declared additional non-runtime root globs.
|
|
331
|
+
for (const re of policy.nonRuntimeRootFilePatterns) {
|
|
332
|
+
if (re.test(file)) return true;
|
|
333
|
+
}
|
|
224
334
|
return false;
|
|
225
335
|
}
|
|
226
336
|
|
|
227
337
|
/**
|
|
228
338
|
* Determine whether the changes between buildCommit..HEAD touch any daemon-runtime
|
|
229
|
-
* package. Returns isDaemonAffecting:true conservatively
|
|
230
|
-
* can't be obtained or any changed path is outside the
|
|
231
|
-
* AND is not a recognized non-runtime root file
|
|
339
|
+
* package, per the resolved policy. Returns isDaemonAffecting:true conservatively
|
|
340
|
+
* when the changed-file set can't be obtained or any changed path is outside the
|
|
341
|
+
* known web-only package set AND is not a recognized non-runtime root file.
|
|
232
342
|
*/
|
|
233
343
|
async function classifyDaemonBuildChange(
|
|
234
344
|
repoPath: string,
|
|
235
345
|
buildCommit: string,
|
|
236
346
|
options: GitStatusOptions,
|
|
347
|
+
policy: ResolvedChangeImpactPolicy,
|
|
237
348
|
): Promise<{ isDaemonAffecting: boolean; affectedPackages: string[] }> {
|
|
238
349
|
try {
|
|
239
350
|
const diff = await runGit(repoPath, ['diff', '--name-only', `${buildCommit}..HEAD`], options);
|
|
@@ -254,7 +365,7 @@ async function classifyDaemonBuildChange(
|
|
|
254
365
|
for (const file of files) {
|
|
255
366
|
const match = file.match(/(?:^|\/)packages\/([^/]+)\//);
|
|
256
367
|
if (!match) {
|
|
257
|
-
if (!isNonRuntimeRootFile(file)) sawRuntimeAmbiguousNonPackage = true;
|
|
368
|
+
if (!isNonRuntimeRootFile(file, policy)) sawRuntimeAmbiguousNonPackage = true;
|
|
258
369
|
continue;
|
|
259
370
|
}
|
|
260
371
|
pkgs.add(match[1]);
|
|
@@ -267,7 +378,7 @@ async function classifyDaemonBuildChange(
|
|
|
267
378
|
// file changed) — i.e. nothing runtime-ambiguous remains.
|
|
268
379
|
const allBenign =
|
|
269
380
|
!sawRuntimeAmbiguousNonPackage &&
|
|
270
|
-
affectedPackages.every((p) =>
|
|
381
|
+
affectedPackages.every((p) => policy.webOnlyPackages.has(p) && !policy.daemonRuntimePackages.has(p));
|
|
271
382
|
return { isDaemonAffecting: !allBenign, affectedPackages };
|
|
272
383
|
} catch {
|
|
273
384
|
// diff probe failed → can't prove web-only; stay conservative.
|
|
@@ -275,6 +386,46 @@ async function classifyDaemonBuildChange(
|
|
|
275
386
|
}
|
|
276
387
|
}
|
|
277
388
|
|
|
389
|
+
/**
|
|
390
|
+
* Resolve the Change Impact config to apply for this status read. Priority:
|
|
391
|
+
* - options.changeImpactConfig === null → force built-in default policy (no load).
|
|
392
|
+
* - options.changeImpactConfig provided → use it verbatim (override seam).
|
|
393
|
+
* - otherwise → auto-load the repo's `.adhdev/change-impact.*` (cached by sourceKey).
|
|
394
|
+
* Returns the (possibly null) config plus the sourceKey used for cache invalidation.
|
|
395
|
+
*/
|
|
396
|
+
function resolveChangeImpactConfigForRepo(
|
|
397
|
+
repoRoot: string | null,
|
|
398
|
+
options: GitStatusOptions,
|
|
399
|
+
): { config: ChangeImpactConfig | null; sourceKey: string } {
|
|
400
|
+
if (options.changeImpactConfig === null) {
|
|
401
|
+
return { config: null, sourceKey: 'forced-default' };
|
|
402
|
+
}
|
|
403
|
+
if (options.changeImpactConfig !== undefined) {
|
|
404
|
+
// Injected override — key it to its content so a different injected config
|
|
405
|
+
// re-evaluates rather than reusing a prior verdict.
|
|
406
|
+
let key = 'injected';
|
|
407
|
+
try {
|
|
408
|
+
key = `injected:${JSON.stringify(options.changeImpactConfig)}`;
|
|
409
|
+
} catch {
|
|
410
|
+
// Non-serializable override (shouldn't happen) — fall back to a constant key.
|
|
411
|
+
}
|
|
412
|
+
return { config: options.changeImpactConfig, sourceKey: key };
|
|
413
|
+
}
|
|
414
|
+
if (!repoRoot) {
|
|
415
|
+
return { config: null, sourceKey: 'no-repo-root' };
|
|
416
|
+
}
|
|
417
|
+
const loaded = loadChangeImpactConfig(repoRoot);
|
|
418
|
+
const cached = changeImpactConfigCache.get(repoRoot);
|
|
419
|
+
if (cached && cached.sourceKey === loaded.sourceKey) {
|
|
420
|
+
return { config: cached.config, sourceKey: loaded.sourceKey };
|
|
421
|
+
}
|
|
422
|
+
// An invalid config is conservatively ignored (built-in default policy applies),
|
|
423
|
+
// mirroring the "fail safe" rule — never let a malformed config silence warnings.
|
|
424
|
+
const config = loaded.sourceType === 'repo_file' ? loaded.config ?? null : null;
|
|
425
|
+
changeImpactConfigCache.set(repoRoot, { sourceKey: loaded.sourceKey, config });
|
|
426
|
+
return { config, sourceKey: loaded.sourceKey };
|
|
427
|
+
}
|
|
428
|
+
|
|
278
429
|
async function detectDaemonBuildBehind(
|
|
279
430
|
repo: ResolvedGitRepo,
|
|
280
431
|
submodules: GitSubmoduleStatus[] | undefined,
|
|
@@ -283,6 +434,9 @@ async function detectDaemonBuildBehind(
|
|
|
283
434
|
const build = options.daemonBuildInfo ?? getDaemonBuildInfo();
|
|
284
435
|
if (!build.commit || build.commit === 'unknown') return undefined;
|
|
285
436
|
|
|
437
|
+
const { config, sourceKey: configKey } = resolveChangeImpactConfigForRepo(repo.repoRoot, options);
|
|
438
|
+
const policy = resolveChangeImpactPolicy(config);
|
|
439
|
+
|
|
286
440
|
// Check the root repo first, then each submodule. The daemon build commit is
|
|
287
441
|
// baked from the daemon-core (oss submodule) HEAD, so on an adhdev
|
|
288
442
|
// superproject worktree the match is expected on the `oss` submodule, not the
|
|
@@ -307,12 +461,22 @@ async function detectDaemonBuildBehind(
|
|
|
307
461
|
// Inspect WHICH packages changed in buildCommit..HEAD. A daemon rebuild/restart
|
|
308
462
|
// is only actually required when a daemon-runtime package changed; if only web /
|
|
309
463
|
// render packages changed, the daemon is unaffected and just the web deploy is
|
|
310
|
-
// pending. Conservative: any probe failure → treat as daemon-affecting.
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
)
|
|
464
|
+
// pending. Conservative: any probe failure → treat as daemon-affecting. The
|
|
465
|
+
// verdict is memoized on (repoPath, buildCommit, head, config) to suppress
|
|
466
|
+
// re-evaluation on the hot status path.
|
|
467
|
+
const evalKey = `${repoPath}${build.commit}${head}${configKey}`;
|
|
468
|
+
let evaluated = changeImpactEvalCache.get(evalKey);
|
|
469
|
+
if (!evaluated) {
|
|
470
|
+
evaluated = await classifyDaemonBuildChange(repoPath, build.commit, options, policy);
|
|
471
|
+
changeImpactEvalCache.set(evalKey, evaluated);
|
|
472
|
+
}
|
|
473
|
+
const { isDaemonAffecting, affectedPackages } = evaluated;
|
|
474
|
+
const kind: ChangeImpactKind = isDaemonAffecting
|
|
475
|
+
? 'daemon'
|
|
476
|
+
: affectedPackages.length > 0
|
|
477
|
+
? 'web'
|
|
478
|
+
: 'none';
|
|
479
|
+
const target = policy.impactTargets[kind];
|
|
316
480
|
const scopeLabel = scope === 'root' ? 'workspace' : scope;
|
|
317
481
|
const benignDetail = affectedPackages.length > 0
|
|
318
482
|
? `only web packages changed (${affectedPackages.join(', ')})`
|
|
@@ -330,6 +494,8 @@ async function detectDaemonBuildBehind(
|
|
|
330
494
|
scope,
|
|
331
495
|
isDaemonAffecting,
|
|
332
496
|
...(affectedPackages && affectedPackages.length > 0 ? { affectedPackages } : {}),
|
|
497
|
+
recommendedAction: kind,
|
|
498
|
+
recommendedCommand: target.recommendedCommand,
|
|
333
499
|
warning,
|
|
334
500
|
};
|
|
335
501
|
} catch {
|
package/src/git/index.ts
CHANGED
|
@@ -26,6 +26,22 @@ export type { GitCommandResult as GitExecutorCommandResult, GitExecutorOptions,
|
|
|
26
26
|
export { getGitRepoStatus, parsePorcelainV2Status } from './git-status.js';
|
|
27
27
|
export type { GitStatusOptions } from './git-status.js';
|
|
28
28
|
|
|
29
|
+
export {
|
|
30
|
+
CHANGE_IMPACT_CONFIG_LOCATIONS,
|
|
31
|
+
CHANGE_IMPACT_CONFIG_SCHEMA,
|
|
32
|
+
globToRegExp,
|
|
33
|
+
loadChangeImpactConfig,
|
|
34
|
+
suggestChangeImpactConfig,
|
|
35
|
+
validateChangeImpactConfig,
|
|
36
|
+
} from './change-impact-config.js';
|
|
37
|
+
export type {
|
|
38
|
+
ChangeImpactConfig,
|
|
39
|
+
ChangeImpactConfigLoadResult,
|
|
40
|
+
ChangeImpactConfigSuggestion,
|
|
41
|
+
ChangeImpactKind,
|
|
42
|
+
ChangeImpactTarget,
|
|
43
|
+
} from './change-impact-config.js';
|
|
44
|
+
|
|
29
45
|
export { getGitDiffSummary, getGitFileDiff } from './git-diff.js';
|
|
30
46
|
export type { GitDiffOptions, GitFileDiffResult } from './git-diff.js';
|
|
31
47
|
|
|
@@ -21,7 +21,7 @@ import {
|
|
|
21
21
|
findRecentTerminalLedgerEvidence,
|
|
22
22
|
hasDispatchAfterTerminal,
|
|
23
23
|
hasUnterminalDirectDispatchLedgerEntry,
|
|
24
|
-
|
|
24
|
+
buildNoProgressCompletionReconciliation,
|
|
25
25
|
} from './mesh-events-stale.js';
|
|
26
26
|
import {
|
|
27
27
|
buildMeshSystemMessage,
|
|
@@ -125,7 +125,7 @@ function shouldSuppressIntentionalCleanupStop(args: {
|
|
|
125
125
|
sessionId?: string;
|
|
126
126
|
nodeId?: string;
|
|
127
127
|
}): boolean {
|
|
128
|
-
if (args.event !== 'agent:stopped' && args.event !== 'monitor:
|
|
128
|
+
if (args.event !== 'agent:stopped' && args.event !== 'monitor:no_progress') return false;
|
|
129
129
|
if (isIntentionalCleanupStopMetadata(args.metadataEvent)) return true;
|
|
130
130
|
return hasRecentIntentionalCleanupStop(args.meshId, args.sessionId, args.nodeId);
|
|
131
131
|
}
|
|
@@ -453,7 +453,8 @@ function resolveAutoFastForwardPolicy(mesh: any): { enabled: boolean; maxBehind?
|
|
|
453
453
|
function sessionStateLooksActive(state: any): boolean {
|
|
454
454
|
const status = readNonEmptyString(state?.status).toLowerCase();
|
|
455
455
|
const chatStatus = readNonEmptyString(state?.activeChat?.status).toLowerCase();
|
|
456
|
-
|
|
456
|
+
// 'long_generating' is retained as a legacy alias for the renamed 'no_progress' busy status.
|
|
457
|
+
const active = new Set(['generating', 'streaming', 'no_progress', 'long_generating', 'working', 'starting', 'waiting_approval']);
|
|
457
458
|
return active.has(status) || active.has(chatStatus);
|
|
458
459
|
}
|
|
459
460
|
|
|
@@ -1329,7 +1330,7 @@ const MESH_COORDINATOR_EVENTS = new Set([
|
|
|
1329
1330
|
'agent:waiting_approval',
|
|
1330
1331
|
'agent:stopped',
|
|
1331
1332
|
'agent:ready',
|
|
1332
|
-
'monitor:
|
|
1333
|
+
'monitor:no_progress',
|
|
1333
1334
|
'refine:accepted',
|
|
1334
1335
|
'refine:completed',
|
|
1335
1336
|
'refine:failed',
|
|
@@ -1341,7 +1342,7 @@ const EVENT_TO_LEDGER_KIND: Record<string, MeshLedgerKind> = {
|
|
|
1341
1342
|
'agent:generating_completed': 'task_completed',
|
|
1342
1343
|
'agent:waiting_approval': 'task_approval_needed',
|
|
1343
1344
|
'agent:stopped': 'task_failed',
|
|
1344
|
-
'monitor:
|
|
1345
|
+
'monitor:no_progress': 'task_stalled',
|
|
1345
1346
|
};
|
|
1346
1347
|
|
|
1347
1348
|
export function isMeshCoordinatorEvent(eventName: unknown): eventName is string {
|
|
@@ -1419,24 +1420,24 @@ function injectMeshSystemMessage(components: DaemonComponents, args: {
|
|
|
1419
1420
|
return { success: true, forwarded: 0, suppressed: true, intentionalCleanupStop: true };
|
|
1420
1421
|
}
|
|
1421
1422
|
|
|
1422
|
-
if (args.event === 'monitor:
|
|
1423
|
-
const reconciledCompletion =
|
|
1423
|
+
if (args.event === 'monitor:no_progress') {
|
|
1424
|
+
const reconciledCompletion = buildNoProgressCompletionReconciliation({
|
|
1424
1425
|
meshId: args.meshId,
|
|
1425
1426
|
nodeId: args.nodeId,
|
|
1426
1427
|
nodeLabel: args.nodeLabel,
|
|
1427
1428
|
metadataEvent: args.metadataEvent,
|
|
1428
1429
|
sourceInstanceId: args.sourceInstanceId,
|
|
1429
1430
|
});
|
|
1430
|
-
if (reconciledCompletion?.source === '
|
|
1431
|
-
LOG.info('MeshEvents', `Reconciled
|
|
1431
|
+
if (reconciledCompletion?.source === 'no_progress_reconciliation') {
|
|
1432
|
+
LOG.info('MeshEvents', `Reconciled no-progress monitor to completion for session ${eventSessionId || '(unknown session)'}`);
|
|
1432
1433
|
return injectMeshSystemMessage(components, {
|
|
1433
1434
|
...args,
|
|
1434
1435
|
event: 'agent:generating_completed',
|
|
1435
1436
|
metadataEvent: reconciledCompletion,
|
|
1436
1437
|
});
|
|
1437
1438
|
}
|
|
1438
|
-
if (reconciledCompletion?.source === '
|
|
1439
|
-
LOG.info('MeshEvents', `Suppressed
|
|
1439
|
+
if (reconciledCompletion?.source === 'no_progress_terminal_ledger_suppression') {
|
|
1440
|
+
LOG.info('MeshEvents', `Suppressed no-progress monitor because terminal ledger evidence already exists for session ${eventSessionId || '(unknown session)'}`);
|
|
1440
1441
|
return {
|
|
1441
1442
|
success: true,
|
|
1442
1443
|
forwarded: 0,
|
|
@@ -1483,7 +1484,7 @@ function injectMeshSystemMessage(components: DaemonComponents, args: {
|
|
|
1483
1484
|
if (
|
|
1484
1485
|
(terminalProviderSessionId && terminalProviderSessionId === eventProviderSessionId)
|
|
1485
1486
|
|| (terminalFinalSummary && terminalFinalSummary === eventFinalSummary)
|
|
1486
|
-
|| args.metadataEvent.source === '
|
|
1487
|
+
|| args.metadataEvent.source === 'no_progress_reconciliation'
|
|
1487
1488
|
) {
|
|
1488
1489
|
LOG.info('MeshEvents', `Suppressed duplicate completion with existing terminal ledger evidence for mesh ${args.meshId} session ${eventSessionId}`);
|
|
1489
1490
|
return { success: true, forwarded: 0, suppressed: true, duplicateCompletion: true, terminalLedgerEvidence: true };
|
|
@@ -235,7 +235,7 @@ export function reconcileDirectDispatchCompletionFromTranscript(args: {
|
|
|
235
235
|
return { reconciled: true, kind, workerResult, ledgerEntryId: entry.id };
|
|
236
236
|
}
|
|
237
237
|
|
|
238
|
-
export function
|
|
238
|
+
export function buildNoProgressCompletionReconciliation(args: {
|
|
239
239
|
meshId: string;
|
|
240
240
|
nodeId?: string;
|
|
241
241
|
nodeLabel: string;
|
|
@@ -265,8 +265,8 @@ export function buildLongGeneratingCompletionReconciliation(args: {
|
|
|
265
265
|
providerType,
|
|
266
266
|
providerSessionId,
|
|
267
267
|
finalSummary,
|
|
268
|
-
source: '
|
|
269
|
-
reconciledFromEvent: 'monitor:
|
|
268
|
+
source: 'no_progress_reconciliation',
|
|
269
|
+
reconciledFromEvent: 'monitor:no_progress',
|
|
270
270
|
timestamp: args.metadataEvent.timestamp ?? Date.now(),
|
|
271
271
|
completionDiagnostic: {
|
|
272
272
|
...(completionDiagnostic || {}),
|
|
@@ -283,7 +283,7 @@ export function buildLongGeneratingCompletionReconciliation(args: {
|
|
|
283
283
|
if (!terminal) return null;
|
|
284
284
|
return {
|
|
285
285
|
...args.metadataEvent,
|
|
286
|
-
source: '
|
|
286
|
+
source: 'no_progress_terminal_ledger_suppression',
|
|
287
287
|
terminalLedgerKind: terminal.kind,
|
|
288
288
|
terminalLedgerAt: terminal.timestamp,
|
|
289
289
|
};
|
|
@@ -137,8 +137,8 @@ export function buildMeshSystemMessage(args: {
|
|
|
137
137
|
}): string {
|
|
138
138
|
const metadata = formatCompletionMetadata(args.metadataEvent);
|
|
139
139
|
if (args.event === 'agent:generating_completed') {
|
|
140
|
-
if (args.metadataEvent.source === '
|
|
141
|
-
return `[System] ${args.nodeLabel} already has completion evidence${metadata}. The
|
|
140
|
+
if (args.metadataEvent.source === 'no_progress_reconciliation') {
|
|
141
|
+
return `[System] ${args.nodeLabel} already has completion evidence${metadata}. The no-progress monitor reconciled the terminal handoff and marked the session complete; wait for the queued completion event/status refresh before doing any manual transcript check.`;
|
|
142
142
|
}
|
|
143
143
|
const reviewNote = args.metadataEvent.reviewRecommended === true
|
|
144
144
|
? ' Completion evidence is insufficient — verify via git status or provider_session_id before assuming the task is done. Use mesh_read_chat once if needed, but do not poll repeatedly.'
|
|
@@ -173,7 +173,7 @@ export function buildMeshSystemMessage(args: {
|
|
|
173
173
|
}
|
|
174
174
|
return `[System] ${args.nodeLabel} has stopped${metadata}. Use mesh_read_chat once if you need to inspect its last output.`;
|
|
175
175
|
}
|
|
176
|
-
if (args.event === 'monitor:
|
|
176
|
+
if (args.event === 'monitor:no_progress') {
|
|
177
177
|
return `[System] ${args.nodeLabel} is still reported as generating after a long interval${metadata}. Wait for pendingCoordinatorEvents or a completion/status event; if the user explicitly asks for status, make one bounded status check and then wait again.`;
|
|
178
178
|
}
|
|
179
179
|
if (args.event === 'worktree_bootstrap_complete') {
|
|
@@ -329,8 +329,8 @@ export class AcpProviderInstance implements ProviderInstance {
|
|
|
329
329
|
this.settings = context.settings || {};
|
|
330
330
|
this.monitor.updateConfig({
|
|
331
331
|
approvalAlert: this.settings.approvalAlert !== false,
|
|
332
|
-
|
|
333
|
-
|
|
332
|
+
noProgressAlert: (this.settings.noProgressAlert ?? this.settings.longGeneratingAlert) !== false,
|
|
333
|
+
noProgressThresholdSec: this.settings.noProgressThresholdSec ?? this.settings.longGeneratingThresholdSec ?? 180,
|
|
334
334
|
});
|
|
335
335
|
|
|
336
336
|
await this.spawnAgent();
|
|
@@ -674,8 +674,8 @@ export class AcpProviderInstance implements ProviderInstance {
|
|
|
674
674
|
this.settings = { ...this.settings, ...newSettings };
|
|
675
675
|
this.monitor.updateConfig({
|
|
676
676
|
approvalAlert: this.settings.approvalAlert !== false,
|
|
677
|
-
|
|
678
|
-
|
|
677
|
+
noProgressAlert: (this.settings.noProgressAlert ?? this.settings.longGeneratingAlert) !== false,
|
|
678
|
+
noProgressThresholdSec: this.settings.noProgressThresholdSec ?? this.settings.longGeneratingThresholdSec ?? 180,
|
|
679
679
|
});
|
|
680
680
|
this.log.info(`[${this.type}] Settings updated: ${Object.keys(newSettings).join(', ')}`);
|
|
681
681
|
}
|
|
@@ -1533,7 +1533,8 @@ export class AcpProviderInstance implements ProviderInstance {
|
|
|
1533
1533
|
|
|
1534
1534
|
// Monitor check
|
|
1535
1535
|
const agentKey = `${this.type}:acp`;
|
|
1536
|
-
const
|
|
1536
|
+
const approvalPending = newStatus === 'waiting_approval';
|
|
1537
|
+
const monitorEvents = this.monitor.check(agentKey, newStatus, now, progressFingerprint, approvalPending);
|
|
1537
1538
|
for (const me of monitorEvents) {
|
|
1538
1539
|
this.pushEvent({ event: me.type, agentKey: me.agentKey, message: me.message, elapsedSec: me.elapsedSec, timestamp: me.timestamp });
|
|
1539
1540
|
}
|
|
@@ -183,7 +183,7 @@ function hasNonEmptyCliModalButtons(activeModal: unknown): boolean {
|
|
|
183
183
|
}
|
|
184
184
|
|
|
185
185
|
function isCliGeneratingLikeStatus(status: unknown): boolean {
|
|
186
|
-
return status === 'generating' || status === 'streaming' || status === 'long_generating' || status === 'starting';
|
|
186
|
+
return status === 'generating' || status === 'streaming' || status === 'no_progress' || status === 'long_generating' || status === 'starting';
|
|
187
187
|
}
|
|
188
188
|
|
|
189
189
|
export function buildCliStructuredInputPrompt(
|
|
@@ -479,8 +479,8 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
479
479
|
this.adapter.updateRuntimeSettings?.(this.settings);
|
|
480
480
|
this.monitor.updateConfig({
|
|
481
481
|
approvalAlert: this.settings.approvalAlert !== false,
|
|
482
|
-
|
|
483
|
-
|
|
482
|
+
noProgressAlert: (this.settings.noProgressAlert ?? this.settings.longGeneratingAlert) !== false,
|
|
483
|
+
noProgressThresholdSec: this.settings.noProgressThresholdSec ?? this.settings.longGeneratingThresholdSec ?? 180,
|
|
484
484
|
});
|
|
485
485
|
|
|
486
486
|
// Server connection
|
|
@@ -698,7 +698,7 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
698
698
|
&& adapterStatus.status === 'idle'
|
|
699
699
|
&& parsedStatus?.status === 'idle';
|
|
700
700
|
let messagesToSave = parsedMessages;
|
|
701
|
-
if (!suppressStaleParsedBusyStatus && (parsedChatStatus === 'generating' || parsedChatStatus === 'long_generating')) {
|
|
701
|
+
if (!suppressStaleParsedBusyStatus && (parsedChatStatus === 'generating' || parsedChatStatus === 'no_progress' || parsedChatStatus === 'long_generating')) {
|
|
702
702
|
const lastIdx = messagesToSave.length - 1;
|
|
703
703
|
if (lastIdx >= 0 && messagesToSave[lastIdx]?.role === 'assistant') {
|
|
704
704
|
messagesToSave = messagesToSave.slice(0, lastIdx);
|
|
@@ -866,8 +866,8 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
866
866
|
this.adapter.updateRuntimeSettings?.(this.settings);
|
|
867
867
|
this.monitor.updateConfig({
|
|
868
868
|
approvalAlert: this.settings.approvalAlert !== false,
|
|
869
|
-
|
|
870
|
-
|
|
869
|
+
noProgressAlert: (this.settings.noProgressAlert ?? this.settings.longGeneratingAlert) !== false,
|
|
870
|
+
noProgressThresholdSec: this.settings.noProgressThresholdSec ?? this.settings.longGeneratingThresholdSec ?? 180,
|
|
871
871
|
});
|
|
872
872
|
}
|
|
873
873
|
|
|
@@ -1545,7 +1545,7 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
1545
1545
|
const dirName = workingDirBasename(this.workingDir);
|
|
1546
1546
|
const chatTitle = `${this.provider.name} · ${dirName}`;
|
|
1547
1547
|
const partial = this.adapter.getPartialResponse();
|
|
1548
|
-
// Liveness fingerprint for the
|
|
1548
|
+
// Liveness fingerprint for the no-progress watchdog. The parsed
|
|
1549
1549
|
// assistant buffer (`partial`) alone goes static while a tool/build runs
|
|
1550
1550
|
// — the assistant emits no tokens even though the PTY is actively
|
|
1551
1551
|
// printing tool output — which made the watchdog false-fire a "stuck"
|
|
@@ -1764,11 +1764,15 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
1764
1764
|
|
|
1765
1765
|
// Monitor check (cooldown based notification, IDE/CLI common)
|
|
1766
1766
|
const agentKey = `${this.type}:cli`;
|
|
1767
|
-
|
|
1767
|
+
// Approval pending is detected from the raw adapter status, not `newStatus`:
|
|
1768
|
+
// auto-approve synthesizes `waiting_approval` → 'generating', which would
|
|
1769
|
+
// otherwise let the no-progress watchdog accumulate the approval wait.
|
|
1770
|
+
const approvalPending = rawStatus === 'waiting_approval';
|
|
1771
|
+
const monitorEvents = this.monitor.check(agentKey, newStatus, now, progressFingerprint, approvalPending);
|
|
1768
1772
|
const monitorParsedStatus: any = parsedStatus;
|
|
1769
1773
|
for (const me of monitorEvents) {
|
|
1770
1774
|
if (
|
|
1771
|
-
me.type === 'monitor:
|
|
1775
|
+
me.type === 'monitor:no_progress'
|
|
1772
1776
|
&& this.completionHasFinalAssistantMessage(monitorParsedStatus?.messages)
|
|
1773
1777
|
&& !this.hasAdapterPendingResponse()
|
|
1774
1778
|
&& !hasNonEmptyCliModalButtons(monitorParsedStatus?.activeModal ?? monitorParsedStatus?.modal)
|
|
@@ -1783,7 +1787,7 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
1783
1787
|
providerType: this.type,
|
|
1784
1788
|
sessionId: this.instanceId,
|
|
1785
1789
|
providerSessionId: this.providerSessionId || null,
|
|
1786
|
-
reconciliationReason: '
|
|
1790
|
+
reconciliationReason: 'no_progress_monitor_final_summary',
|
|
1787
1791
|
finalAssistantPresent: true,
|
|
1788
1792
|
},
|
|
1789
1793
|
});
|
|
@@ -63,8 +63,8 @@ export class ExtensionProviderInstance implements ProviderInstance {
|
|
|
63
63
|
this.settings = context.settings || {};
|
|
64
64
|
this.monitor.updateConfig({
|
|
65
65
|
approvalAlert: this.settings.approvalAlert !== false,
|
|
66
|
-
|
|
67
|
-
|
|
66
|
+
noProgressAlert: (this.settings.noProgressAlert ?? this.settings.longGeneratingAlert) !== false,
|
|
67
|
+
noProgressThresholdSec: this.settings.noProgressThresholdSec ?? this.settings.longGeneratingThresholdSec ?? 180,
|
|
68
68
|
});
|
|
69
69
|
}
|
|
70
70
|
|
|
@@ -178,8 +178,8 @@ export class ExtensionProviderInstance implements ProviderInstance {
|
|
|
178
178
|
this.settings = { ...this.settings, ...newSettings };
|
|
179
179
|
this.monitor.updateConfig({
|
|
180
180
|
approvalAlert: this.settings.approvalAlert !== false,
|
|
181
|
-
|
|
182
|
-
|
|
181
|
+
noProgressAlert: (this.settings.noProgressAlert ?? this.settings.longGeneratingAlert) !== false,
|
|
182
|
+
noProgressThresholdSec: this.settings.noProgressThresholdSec ?? this.settings.longGeneratingThresholdSec ?? 180,
|
|
183
183
|
});
|
|
184
184
|
}
|
|
185
185
|
|
|
@@ -251,9 +251,10 @@ export class ExtensionProviderInstance implements ProviderInstance {
|
|
|
251
251
|
: 'immediate',
|
|
252
252
|
});
|
|
253
253
|
|
|
254
|
-
// Monitor check (cooldown based notification) — keep monitor events (
|
|
254
|
+
// Monitor check (cooldown based notification) — keep monitor events (no_progress etc)
|
|
255
255
|
const agentKey = `${this.type}:ext`;
|
|
256
|
-
const
|
|
256
|
+
const approvalPending = agentStatus === 'waiting_approval';
|
|
257
|
+
const monitorEvents = this.monitor.check(agentKey, agentStatus, now, progressFingerprint, approvalPending);
|
|
257
258
|
for (const me of monitorEvents) {
|
|
258
259
|
this.pushEvent({ event: me.type, agentKey: me.agentKey, message: me.message, elapsedSec: me.elapsedSec, timestamp: me.timestamp });
|
|
259
260
|
}
|
|
@@ -105,8 +105,8 @@ export class IdeProviderInstance implements ProviderInstance {
|
|
|
105
105
|
// Sync Monitor config
|
|
106
106
|
this.monitor.updateConfig({
|
|
107
107
|
approvalAlert: this.settings.approvalAlert !== false,
|
|
108
|
-
|
|
109
|
-
|
|
108
|
+
noProgressAlert: (this.settings.noProgressAlert ?? this.settings.longGeneratingAlert) !== false,
|
|
109
|
+
noProgressThresholdSec: this.settings.noProgressThresholdSec ?? this.settings.longGeneratingThresholdSec ?? 180,
|
|
110
110
|
});
|
|
111
111
|
}
|
|
112
112
|
|
|
@@ -267,8 +267,8 @@ export class IdeProviderInstance implements ProviderInstance {
|
|
|
267
267
|
this.settings = { ...this.settings, ...newSettings };
|
|
268
268
|
this.monitor.updateConfig({
|
|
269
269
|
approvalAlert: this.settings.approvalAlert !== false,
|
|
270
|
-
|
|
271
|
-
|
|
270
|
+
noProgressAlert: (this.settings.noProgressAlert ?? this.settings.longGeneratingAlert) !== false,
|
|
271
|
+
noProgressThresholdSec: this.settings.noProgressThresholdSec ?? this.settings.longGeneratingThresholdSec ?? 180,
|
|
272
272
|
});
|
|
273
273
|
}
|
|
274
274
|
|
|
@@ -417,7 +417,7 @@ export class IdeProviderInstance implements ProviderInstance {
|
|
|
417
417
|
const persistedMessages = chat.messages || messages;
|
|
418
418
|
if (persistedMessages.length > 0) {
|
|
419
419
|
let toSave = persistedMessages;
|
|
420
|
-
if (chat.status === 'generating' || chat.status === 'long_generating') {
|
|
420
|
+
if (chat.status === 'generating' || chat.status === 'no_progress' || chat.status === 'long_generating') {
|
|
421
421
|
// Find and exclude last assistant message
|
|
422
422
|
const lastIdx = toSave.length - 1;
|
|
423
423
|
if (lastIdx >= 0 && toSave[lastIdx].role === 'assistant') {
|
|
@@ -508,7 +508,11 @@ export class IdeProviderInstance implements ProviderInstance {
|
|
|
508
508
|
}
|
|
509
509
|
|
|
510
510
|
// Monitor check (cooldown based notification)
|
|
511
|
-
|
|
511
|
+
// Approval pending is detected from the raw status: auto-approve synthesizes
|
|
512
|
+
// `waiting_approval` → 'generating', so the no-progress watchdog must be told
|
|
513
|
+
// to hold its timer during the wait rather than count it as a stall.
|
|
514
|
+
const approvalPending = rawAgentStatus === 'waiting_approval';
|
|
515
|
+
const monitorEvents = this.monitor.check(agentKey, agentStatus, now, progressFingerprint, approvalPending);
|
|
512
516
|
for (const me of monitorEvents) {
|
|
513
517
|
this.pushEvent({ event: me.type, agentKey: me.agentKey, message: me.message, elapsedSec: me.elapsedSec, timestamp: me.timestamp });
|
|
514
518
|
}
|