@adhdev/daemon-core 0.9.82-rc.456 → 0.9.82-rc.457
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.js +288 -62
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +288 -62
- package/dist/index.mjs.map +1 -1
- package/dist/mesh/preview-freshness.d.ts +11 -1
- package/dist/mesh/worktree-bootstrap-config.d.ts +9 -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/mesh/preview-freshness.ts +46 -1
- package/src/mesh/worktree-bootstrap-config.ts +1 -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
|
@@ -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') {
|
|
@@ -37,6 +37,11 @@ import {
|
|
|
37
37
|
resolveActiveSource,
|
|
38
38
|
} from './external-sources.js';
|
|
39
39
|
import type { ProviderSourceMode } from '../config/config.js';
|
|
40
|
+
import {
|
|
41
|
+
resolveRegistryBaseUrl,
|
|
42
|
+
resolveProviderTarballUrl,
|
|
43
|
+
resolveProviderTarballTarget,
|
|
44
|
+
} from '../config/registry-resolver.js';
|
|
40
45
|
import type { ProviderSourceConfigSnapshot, ProviderUserDirSource } from '../config/provider-source-config.js';
|
|
41
46
|
import { executeNativeHistory } from './spec/native-history-executor.js';
|
|
42
47
|
import { createNativeHistoryDispatcher, type ReaderId } from './native-history/dispatcher.js';
|
|
@@ -186,13 +191,19 @@ export class ProviderLoader {
|
|
|
186
191
|
private versionArchive: VersionArchive | null = null;
|
|
187
192
|
private scriptsCache = new Map<string, Partial<ProviderScripts>>();
|
|
188
193
|
|
|
194
|
+
/**
|
|
195
|
+
* Resolved registry base URL and provider tarball URL. Resolution order:
|
|
196
|
+
* explicit config field (constructor option) → env var → vendor default.
|
|
197
|
+
* See `config/registry-resolver.ts`.
|
|
198
|
+
*/
|
|
199
|
+
private readonly registryBaseUrl: string;
|
|
200
|
+
private readonly providerTarballUrl: string;
|
|
201
|
+
|
|
189
202
|
/** Inject VersionArchive so resolve() can auto-detect installed versions */
|
|
190
203
|
setVersionArchive(archive: VersionArchive): void {
|
|
191
204
|
this.versionArchive = archive;
|
|
192
205
|
}
|
|
193
206
|
|
|
194
|
-
private static readonly GITHUB_TARBALL_URL = 'https://github.com/vilmire/adhdev-providers/archive/refs/heads/main.tar.gz';
|
|
195
|
-
private static readonly REGISTRY_BASE_URL = 'https://api.adhf.dev/api/v1/registry';
|
|
196
207
|
private static readonly META_FILE = '.meta.json';
|
|
197
208
|
private static readonly REGISTRY_META_FILE = '.registry-meta.json';
|
|
198
209
|
private static readonly REPO_PROVIDER_DIRNAME = 'adhdev-providers';
|
|
@@ -278,9 +289,21 @@ export class ProviderLoader {
|
|
|
278
289
|
* probing; production code should leave this unset.
|
|
279
290
|
*/
|
|
280
291
|
probeStarts?: string[];
|
|
292
|
+
/**
|
|
293
|
+
* Explicit provider registry base URL override (config.registryUrl).
|
|
294
|
+
* Highest-priority resolver source, ahead of ADHDEV_REGISTRY_URL + default.
|
|
295
|
+
*/
|
|
296
|
+
registryUrl?: string;
|
|
297
|
+
/**
|
|
298
|
+
* Explicit provider tarball URL override (config.providerTarballUrl).
|
|
299
|
+
* Highest-priority resolver source, ahead of ADHDEV_PROVIDER_TARBALL_URL + default.
|
|
300
|
+
*/
|
|
301
|
+
providerTarballUrl?: string;
|
|
281
302
|
}) {
|
|
282
303
|
this.logFn = options?.logFn || LOG.forComponent('Provider').asLogFn();
|
|
283
304
|
this.probeStarts = options?.probeStarts ?? [process.cwd(), __dirname];
|
|
305
|
+
this.registryBaseUrl = resolveRegistryBaseUrl(options?.registryUrl);
|
|
306
|
+
this.providerTarballUrl = resolveProviderTarballUrl(options?.providerTarballUrl);
|
|
284
307
|
|
|
285
308
|
// Default directory for auto-downloads
|
|
286
309
|
this.defaultProvidersDir = path.join(os.homedir(), '.adhdev', 'providers');
|
|
@@ -1554,7 +1577,7 @@ export class ProviderLoader {
|
|
|
1554
1577
|
this.log('Registry sync skipped (sourceMode=no-upstream)');
|
|
1555
1578
|
return { updated: false };
|
|
1556
1579
|
}
|
|
1557
|
-
this.log(`Registry sync starting (${
|
|
1580
|
+
this.log(`Registry sync starting (${this.registryBaseUrl})...`);
|
|
1558
1581
|
|
|
1559
1582
|
const https = require('https') as typeof import('https');
|
|
1560
1583
|
const regMetaPath = path.join(this.upstreamDir, ProviderLoader.REGISTRY_META_FILE);
|
|
@@ -1569,7 +1592,7 @@ export class ProviderLoader {
|
|
|
1569
1592
|
|
|
1570
1593
|
try {
|
|
1571
1594
|
// 1. Fetch provider list
|
|
1572
|
-
const listUrl = `${
|
|
1595
|
+
const listUrl = `${this.registryBaseUrl}/providers`;
|
|
1573
1596
|
const listBody = await new Promise<string>((resolve, reject) => {
|
|
1574
1597
|
const req = https.get(listUrl, { headers: { 'User-Agent': 'adhdev-daemon', 'Accept': 'application/json' }, timeout: 10000 }, (res) => {
|
|
1575
1598
|
if (res.statusCode !== 200) { reject(new Error(`registry list HTTP ${res.statusCode}`)); return; }
|
|
@@ -1592,7 +1615,7 @@ export class ProviderLoader {
|
|
|
1592
1615
|
if (cachedChecksums[cacheKey] === checksum) continue; // already current
|
|
1593
1616
|
|
|
1594
1617
|
// Download this provider's manifest
|
|
1595
|
-
const dlUrl = `${
|
|
1618
|
+
const dlUrl = `${this.registryBaseUrl}/providers/${type}/${version}/download`;
|
|
1596
1619
|
const manifestBody = await new Promise<string>((resolve, reject) => {
|
|
1597
1620
|
const req = https.get(dlUrl, { headers: { 'User-Agent': 'adhdev-daemon', 'Accept': 'application/json' }, timeout: 30000 }, (res) => {
|
|
1598
1621
|
if (res.statusCode !== 200) { reject(new Error(`registry download HTTP ${res.statusCode} for ${type}@${version}`)); return; }
|
|
@@ -1667,13 +1690,17 @@ export class ProviderLoader {
|
|
|
1667
1690
|
return { updated: false };
|
|
1668
1691
|
}
|
|
1669
1692
|
|
|
1693
|
+
// Resolve the tarball target (config → env → vendor default) once so the
|
|
1694
|
+
// HEAD probe and the download below hit the same (possibly self-hosted) URL.
|
|
1695
|
+
const tarballTarget = resolveProviderTarballTarget(this.providerTarballUrl);
|
|
1696
|
+
|
|
1670
1697
|
try {
|
|
1671
1698
|
// Step 1: HEAD request to check ETag
|
|
1672
1699
|
const etag = await new Promise<string>((resolve, reject) => {
|
|
1673
1700
|
const options = {
|
|
1674
1701
|
method: 'HEAD',
|
|
1675
|
-
hostname:
|
|
1676
|
-
path:
|
|
1702
|
+
hostname: tarballTarget.hostname,
|
|
1703
|
+
path: tarballTarget.path,
|
|
1677
1704
|
headers: { 'User-Agent': 'adhdev-launcher' },
|
|
1678
1705
|
timeout: 10000,
|
|
1679
1706
|
};
|
|
@@ -1718,7 +1745,7 @@ export class ProviderLoader {
|
|
|
1718
1745
|
const tmpExtract = path.join(os.tmpdir(), `adhdev-providers-extract-${Date.now()}`);
|
|
1719
1746
|
|
|
1720
1747
|
// Download tarball
|
|
1721
|
-
await this.downloadFile(
|
|
1748
|
+
await this.downloadFile(tarballTarget.url, tmpTar);
|
|
1722
1749
|
|
|
1723
1750
|
// Extract
|
|
1724
1751
|
fs.mkdirSync(tmpExtract, { recursive: true });
|
|
@@ -1825,7 +1852,7 @@ export class ProviderLoader {
|
|
|
1825
1852
|
etag,
|
|
1826
1853
|
timestamp,
|
|
1827
1854
|
lastCheck: new Date(timestamp).toISOString(),
|
|
1828
|
-
source:
|
|
1855
|
+
source: this.providerTarballUrl,
|
|
1829
1856
|
}, null, 2));
|
|
1830
1857
|
} catch { }
|
|
1831
1858
|
}
|
|
@@ -38,6 +38,8 @@ import { loadFsmSpec } from './fsm-loader.js';
|
|
|
38
38
|
import { applyPreLaunchTrust } from './pre-launch-trust.js';
|
|
39
39
|
import type { Control, DelegateTrigger } from './types.js';
|
|
40
40
|
import { LOG } from '../../logging/logger.js';
|
|
41
|
+
import { recordDebugTrace } from '../../logging/debug-trace.js';
|
|
42
|
+
import { shouldCollectTraceCategory } from '../../logging/debug-config.js';
|
|
41
43
|
import {
|
|
42
44
|
WIN32_PTY_WRITE_CHUNK_CHARS,
|
|
43
45
|
WIN32_PTY_WRITE_CHUNK_GAP_MS,
|
|
@@ -300,6 +302,11 @@ export class FsmDriver implements ISpecDriver {
|
|
|
300
302
|
* (−1 = whole screen), or a `section:<id>` / `<region>#ignore:<pat>` string
|
|
301
303
|
* when the clause scopes to a section or declares an ignore_lines filter. */
|
|
302
304
|
private regionLastChangedAt = new Map<number | string, number>();
|
|
305
|
+
/** COMPLETION-EARLYNOTIFY stable-eval trace: last stable/not-stable verdict
|
|
306
|
+
* recorded per stable region, so the trace fires only when the verdict FLIPS
|
|
307
|
+
* (not every quiet frame). Cleared on every transition alongside
|
|
308
|
+
* regionLastChangedAt. Diagnostic-only — never consulted by the FSM. */
|
|
309
|
+
private stableVerdictCache = new Map<number | string, boolean>();
|
|
303
310
|
/** Timer that re-runs evaluate() when a time-condition would flip true
|
|
304
311
|
* with no PTY frame to trigger it. */
|
|
305
312
|
private wakeTimer: ReturnType<typeof setTimeout> | null = null;
|
|
@@ -684,6 +691,7 @@ export class FsmDriver implements ISpecDriver {
|
|
|
684
691
|
// Region change timestamps are relative to the previous state's
|
|
685
692
|
// activity; reset so stable_ms in the new state measures from entry.
|
|
686
693
|
this.regionLastChangedAt.clear();
|
|
694
|
+
this.stableVerdictCache.clear();
|
|
687
695
|
this.pushHistory(fired.to, stateById(this.spec, fired.to)?.label ?? fired.to, {
|
|
688
696
|
reason: 'transition',
|
|
689
697
|
via: `${from}→${fired.to}`,
|
|
@@ -818,6 +826,13 @@ export class FsmDriver implements ISpecDriver {
|
|
|
818
826
|
private trackRegionChanges(currentLines: string[], cursor: { row: number; col: number }, now: number): void {
|
|
819
827
|
if (this.prevScreenLines.length === 0) return;
|
|
820
828
|
const descs = this.stableRegionDescriptors();
|
|
829
|
+
// COMPLETION-EARLYNOTIFY hook 4: record the stable/not-stable verdict for each
|
|
830
|
+
// tracked region, but ONLY when the verdict flips (see stableVerdictCache) so a
|
|
831
|
+
// quiet screen does not spam the ring buffer. This is the case-b diagnostic — an
|
|
832
|
+
// ignore_lines-scoped stable clause declaring a tool-execution screen "stable-idle"
|
|
833
|
+
// shows up here as verdict:true with a short fingerprint. Payload carries lengths
|
|
834
|
+
// and the pattern SOURCE only — never screen text.
|
|
835
|
+
const stableTraceOn = shouldCollectTraceCategory('fsm-transition');
|
|
821
836
|
// Section ranges depend on screen content, so resolve per-frame for both
|
|
822
837
|
// frames — but only when some tracked region is actually section-scoped.
|
|
823
838
|
const needsSections = descs.some(d => !!d.section);
|
|
@@ -839,6 +854,28 @@ export class FsmDriver implements ISpecDriver {
|
|
|
839
854
|
const cur = filterIgnoredLines(curLines, d.ignoreRe).join('\n');
|
|
840
855
|
const prev = filterIgnoredLines(prevLines, d.ignoreRe).join('\n');
|
|
841
856
|
if (cur !== prev) this.regionLastChangedAt.set(d.key, now);
|
|
857
|
+
if (stableTraceOn && typeof d.holdMs === 'number') {
|
|
858
|
+
const lastChanged = this.regionLastChangedAt.get(d.key) ?? this.stateEnteredAt;
|
|
859
|
+
const ageMs = now - lastChanged;
|
|
860
|
+
const verdict = ageMs >= d.holdMs;
|
|
861
|
+
if (this.stableVerdictCache.get(d.key) !== verdict) {
|
|
862
|
+
this.stableVerdictCache.set(d.key, verdict);
|
|
863
|
+
recordDebugTrace({
|
|
864
|
+
category: 'fsm-transition',
|
|
865
|
+
stage: 'stable-eval',
|
|
866
|
+
level: 'debug',
|
|
867
|
+
payload: {
|
|
868
|
+
state: this.currentStateId,
|
|
869
|
+
regionKey: String(d.key),
|
|
870
|
+
ignorePattern: d.ignoreRe?.source ?? null,
|
|
871
|
+
fingerprintLen: cur.length,
|
|
872
|
+
ageMs,
|
|
873
|
+
holdMs: d.holdMs,
|
|
874
|
+
verdict,
|
|
875
|
+
},
|
|
876
|
+
});
|
|
877
|
+
}
|
|
878
|
+
}
|
|
842
879
|
}
|
|
843
880
|
}
|
|
844
881
|
|
|
@@ -1440,6 +1477,11 @@ interface StableRegionDescriptor {
|
|
|
1440
1477
|
section?: string;
|
|
1441
1478
|
cursor_above?: number;
|
|
1442
1479
|
ignoreRe?: RegExp;
|
|
1480
|
+
/** The stable_ms threshold the FIRST clause on this region declares. Used
|
|
1481
|
+
* only by the COMPLETION-EARLYNOTIFY stable-eval trace to report the
|
|
1482
|
+
* stable/not-stable verdict; the FSM decision itself is owned by the
|
|
1483
|
+
* evaluator against the live clause. */
|
|
1484
|
+
holdMs?: number;
|
|
1443
1485
|
}
|
|
1444
1486
|
|
|
1445
1487
|
function collectStableDescriptors(when: FsmTransition['when'], byKey: Map<number | string, StableRegionDescriptor>): void {
|
|
@@ -1447,14 +1489,19 @@ function collectStableDescriptors(when: FsmTransition['when'], byKey: Map<number
|
|
|
1447
1489
|
const w = when as any;
|
|
1448
1490
|
if ('stable_ms' in w) {
|
|
1449
1491
|
const key = stableRegionKey(w);
|
|
1450
|
-
|
|
1492
|
+
const existing = byKey.get(key);
|
|
1493
|
+
if (!existing) {
|
|
1451
1494
|
let ignoreRe: RegExp | undefined;
|
|
1452
1495
|
if (w.ignore_lines) {
|
|
1453
1496
|
// Compile once here; a bad pattern is validated at load time, so
|
|
1454
1497
|
// this is best-effort and simply skips the filter if it throws.
|
|
1455
1498
|
try { ignoreRe = new RegExp(w.ignore_lines, 'm'); } catch { /* validated at load */ }
|
|
1456
1499
|
}
|
|
1457
|
-
byKey.set(key, { key, section: w.section, cursor_above: w.cursor_above, ignoreRe });
|
|
1500
|
+
byKey.set(key, { key, section: w.section, cursor_above: w.cursor_above, ignoreRe, holdMs: typeof w.stable_ms === 'number' ? w.stable_ms : undefined });
|
|
1501
|
+
} else if (existing.holdMs === undefined && typeof w.stable_ms === 'number') {
|
|
1502
|
+
// Enrich the -1 whole-screen seed (or an earlier clause) with a threshold
|
|
1503
|
+
// so its verdict can be traced. Geometry/ignoreRe from the first set win.
|
|
1504
|
+
existing.holdMs = w.stable_ms;
|
|
1458
1505
|
}
|
|
1459
1506
|
return;
|
|
1460
1507
|
}
|