@adhdev/daemon-core 0.9.82-rc.457 → 0.9.82-rc.459
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +1 -1
- package/dist/index.js +677 -189
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +676 -190
- package/dist/index.mjs.map +1 -1
- package/dist/logging/debug-config.d.ts +16 -0
- package/dist/mesh/mesh-queue-assignment.d.ts +28 -0
- package/dist/mesh/mesh-reconcile-loop.d.ts +1 -0
- package/dist/mesh/worktree-bootstrap-config.d.ts +45 -0
- package/dist/providers/chat-message-normalization.d.ts +26 -0
- package/dist/providers/cli-provider-instance.d.ts +7 -0
- package/dist/providers/native-history/antigravity-claim-registry.d.ts +28 -0
- package/dist/providers/native-history/antigravity-cli-transcript.d.ts +11 -0
- package/dist/providers/native-history/dispatcher.d.ts +4 -0
- package/package.json +3 -3
- 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-events-stale.ts +55 -4
- package/src/mesh/mesh-fast-forward.ts +22 -9
- package/src/mesh/mesh-queue-assignment.ts +220 -3
- package/src/mesh/mesh-reconcile-loop.ts +83 -10
- package/src/mesh/mesh-refine-gates.ts +22 -9
- package/src/mesh/worktree-bootstrap-config.ts +130 -0
- package/src/providers/chat-message-normalization.ts +44 -10
- package/src/providers/cli-provider-instance.ts +46 -0
- package/src/providers/native-history/antigravity-claim-registry.ts +131 -0
- package/src/providers/native-history/antigravity-cli-transcript.ts +154 -4
- package/src/providers/native-history/dispatcher.ts +150 -20
|
@@ -14,6 +14,22 @@ export interface DebugRuntimeConfig {
|
|
|
14
14
|
traceBufferSize: number;
|
|
15
15
|
traceCategories: string[];
|
|
16
16
|
}
|
|
17
|
+
/**
|
|
18
|
+
* ALWAYS-ON trace categories. These bypass the `collectDebugTrace` master switch
|
|
19
|
+
* (and category selection) so they are collected in production daemons where
|
|
20
|
+
* `--trace` is unset. They exist so mesh completion diagnostics — the FSM-transition
|
|
21
|
+
* and completion-gate snapshots that explain an early / missing agent:generating_completed
|
|
22
|
+
* notification — are retrievable via mesh_read_debug (chat_debug_bundle) without asking
|
|
23
|
+
* an operator to relaunch the daemon with tracing on.
|
|
24
|
+
*
|
|
25
|
+
* SAFETY: only add a category here after confirming every record() call site for it
|
|
26
|
+
* carries a content-free payload (statuses, epochs, timestamps, deltas, lengths, roles,
|
|
27
|
+
* enum-like reasons — never transcript / prompt / bubble text). Always-on collection makes
|
|
28
|
+
* such payloads unconditional, so a content-bearing field would leak into the ring buffer
|
|
29
|
+
* in production.
|
|
30
|
+
*/
|
|
31
|
+
export declare const ALWAYS_ON_TRACE_CATEGORIES: readonly string[];
|
|
32
|
+
export declare function isAlwaysOnTraceCategory(category?: string | null): boolean;
|
|
17
33
|
export declare function resolveDebugRuntimeConfig(options?: DebugRuntimeOptions): DebugRuntimeConfig;
|
|
18
34
|
export declare function setDebugRuntimeConfig(config: DebugRuntimeConfig): void;
|
|
19
35
|
export declare function getDebugRuntimeConfig(): DebugRuntimeConfig;
|
|
@@ -3,6 +3,12 @@ import type { RepoMeshSchedulingStrategy } from '../repo-mesh-types.js';
|
|
|
3
3
|
export declare function __resetIdleAutoFastForwardForTests(): void;
|
|
4
4
|
export declare function getMeshWithCache(components: DaemonComponents, meshId: string): any | undefined;
|
|
5
5
|
export declare function tryAssignQueueTask(components: DaemonComponents, meshId: string, nodeId: string, sessionId: string, providerType: string): boolean;
|
|
6
|
+
interface AwaitClaimBackoffState {
|
|
7
|
+
cycles: number;
|
|
8
|
+
nextAttemptAtMs: number;
|
|
9
|
+
}
|
|
10
|
+
export declare function __resetAutoLaunchAwaitClaimBackoffForTests(): void;
|
|
11
|
+
export declare function __seedAutoLaunchAwaitClaimBackoffForTests(meshId: string, taskId: string, state: AwaitClaimBackoffState): void;
|
|
6
12
|
/** Active assignments that hold the one-active-per-node / global-parallel invariant
|
|
7
13
|
* (everything except read-only diagnoses, which run unbounded by the write cap). */
|
|
8
14
|
export declare function activeWriteAssignedCount(meshId: string): number;
|
|
@@ -65,6 +71,28 @@ export declare function sessionHasActiveAssignment(meshId: string, sessionId: st
|
|
|
65
71
|
* dead/stale session is not generating → returns false → the requeue proceeds as before.
|
|
66
72
|
*/
|
|
67
73
|
export declare function isSessionActivelyGenerating(components: DaemonComponents, sessionId: string): boolean;
|
|
74
|
+
/**
|
|
75
|
+
* RECLAIM-FALSEPOS tri-state busy verdict for a session id.
|
|
76
|
+
*
|
|
77
|
+
* The binary isSessionActivelyGenerating() folds "absence of a positive generating
|
|
78
|
+
* signal" into a definitive NEGATIVE (returns false when the instance is absent). But a
|
|
79
|
+
* REMOTE session (never in THIS daemon's instanceManager) — or a locally-present session
|
|
80
|
+
* looked up under a skewed id form — then looks "not generating" and can be reclaimed out
|
|
81
|
+
* from under a worker that is genuinely mid-turn. This resolves an explicit three-way
|
|
82
|
+
* verdict instead:
|
|
83
|
+
* - GENERATING — a locally-present instance reports an active/streaming state.
|
|
84
|
+
* - IDLE_CONFIRMED — a locally-present instance reports a non-active (idle/terminal)
|
|
85
|
+
* state. Positive local evidence the worker is not working.
|
|
86
|
+
* - UNKNOWN — no locally-present instance matches (remote / gone / id-skew) or
|
|
87
|
+
* the observation failed. NEVER treated as IDLE_CONFIRMED.
|
|
88
|
+
*
|
|
89
|
+
* The lookup scans getByCategory('cli') with sessionIdsEquivalent (the same equivalence
|
|
90
|
+
* matching nodeHasActiveMeshWork / liveSessionCountForNode use) rather than a raw
|
|
91
|
+
* instanceManager.getInstance(id) Map.get, so an id-form-skewed but present session is
|
|
92
|
+
* found (closing the same id-form-skew hole class e245c2f9's F1 fixed elsewhere).
|
|
93
|
+
*/
|
|
94
|
+
export type SessionBusyVerdict = 'GENERATING' | 'IDLE_CONFIRMED' | 'UNKNOWN';
|
|
95
|
+
export declare function resolveSessionBusyVerdict(components: DaemonComponents, sessionId: string): SessionBusyVerdict;
|
|
68
96
|
export interface MeshQueueTriggerResult {
|
|
69
97
|
success: true;
|
|
70
98
|
meshId: string;
|
|
@@ -70,6 +70,7 @@ export declare function resolveCoordinatorDrainDeliverability(components: Pick<D
|
|
|
70
70
|
export declare function shouldHoldPendingDrainForBusyLocalCoordinator(components: Pick<DaemonComponents, 'instanceManager'> & {
|
|
71
71
|
statusInstanceId?: string;
|
|
72
72
|
}, meshId: string, requestedCoordinatorDaemonId?: string | null, callerIsSelfCoordinatorInboxRead?: boolean): boolean;
|
|
73
|
+
export declare function __resetReclaimUnknownStreakForTests(): void;
|
|
73
74
|
export declare function runMeshReconcileTick(components: DaemonComponents): Promise<void>;
|
|
74
75
|
export declare function __resetUnresolvedForwardRejectionCountsForTests(): void;
|
|
75
76
|
interface ReconcileLoopHandle {
|
|
@@ -38,6 +38,51 @@ export declare const WORKTREE_BOOTSTRAP_STALE_RUNNING_MS: number;
|
|
|
38
38
|
* lookup fails — callers must then treat any change as dirty (conservative).
|
|
39
39
|
*/
|
|
40
40
|
export declare function getRegisteredSubmodulePaths(workspace: string): Set<string>;
|
|
41
|
+
/**
|
|
42
|
+
* Read each registered submodule's configured `branch` from `.gitmodules`, keyed
|
|
43
|
+
* by the submodule's normalized path (matching {@link getRegisteredSubmodulePaths}).
|
|
44
|
+
*
|
|
45
|
+
* `.gitmodules` stores `submodule.<name>.path` and (optionally)
|
|
46
|
+
* `submodule.<name>.branch`; this joins the two on `<name>`. The special branch
|
|
47
|
+
* value `.` ("track the superproject's branch") is deliberately OMITTED so callers
|
|
48
|
+
* fall through to remote-HEAD detection instead of treating `.` as a literal branch
|
|
49
|
+
* name. Returns an empty map when there are no submodules, no `.gitmodules`, or the
|
|
50
|
+
* lookup fails (conservative — callers then detect or fall back).
|
|
51
|
+
*/
|
|
52
|
+
export declare function getSubmoduleConfiguredBranches(workspace: string): Map<string, string>;
|
|
53
|
+
/** Fallback submodule branch when no configured/detected default can be resolved. */
|
|
54
|
+
export declare const SUBMODULE_DEFAULT_BRANCH_FALLBACK = "main";
|
|
55
|
+
/**
|
|
56
|
+
* Resolve the default branch a submodule's commits are published to / checked for
|
|
57
|
+
* reachability against. Generalizes the previously hardcoded `main` so a submodule
|
|
58
|
+
* whose default branch is `master`/`trunk`/etc. is handled. Priority (each tier
|
|
59
|
+
* falls through to the next on miss/error):
|
|
60
|
+
*
|
|
61
|
+
* 1. `.gitmodules` `submodule.<name>.branch` (via {@link getSubmoduleConfiguredBranches};
|
|
62
|
+
* `.` is ignored) — an explicit, local, zero-cost declaration.
|
|
63
|
+
* 2. the submodule checkout's LOCAL remote HEAD: `git symbolic-ref --short
|
|
64
|
+
* refs/remotes/<remote>/HEAD` → strip the `<remote>/` prefix (no network).
|
|
65
|
+
* 3. the submodule remote's advertised HEAD: `git ls-remote --symref <remote> HEAD`
|
|
66
|
+
* → `ref: refs/heads/<branch>` (one network round-trip).
|
|
67
|
+
* 4. fallback {@link SUBMODULE_DEFAULT_BRANCH_FALLBACK} (`'main'`).
|
|
68
|
+
*
|
|
69
|
+
* Because the final fallback is `'main'` and every earlier tier that resolves `'main'`
|
|
70
|
+
* yields the same string, a repo whose submodules default to `main` (the common case)
|
|
71
|
+
* produces byte-identical downstream fetch/merge-base/push ref targets — only a
|
|
72
|
+
* read-only resolution probe is added.
|
|
73
|
+
*/
|
|
74
|
+
export declare function resolveSubmoduleDefaultBranch(opts: {
|
|
75
|
+
/** The submodule's local checkout — cwd for symbolic-ref / ls-remote. */
|
|
76
|
+
submoduleRepoPath: string;
|
|
77
|
+
/** The superproject workspace — for the `.gitmodules` branch lookup (tier 1). */
|
|
78
|
+
superprojectWorkspace?: string;
|
|
79
|
+
/** The submodule's path relative to the superproject (key into `.gitmodules`). */
|
|
80
|
+
submodulePath?: string;
|
|
81
|
+
/** Remote name (default `origin`). */
|
|
82
|
+
remote?: string;
|
|
83
|
+
/** Timeout for the local probe (tier 2); the network probe (tier 3) gets max(this, 30s). */
|
|
84
|
+
timeoutMs?: number;
|
|
85
|
+
}): Promise<string>;
|
|
41
86
|
export declare function isWorktreeBootstrapStaleRunning(node: {
|
|
42
87
|
worktreeBootstrap?: {
|
|
43
88
|
status?: string;
|
|
@@ -32,6 +32,32 @@ export declare function extractFinalAssistantSummaryEvidence(messages: ChatMessa
|
|
|
32
32
|
finalSummary: string;
|
|
33
33
|
transcriptMessageAt?: string;
|
|
34
34
|
};
|
|
35
|
+
/**
|
|
36
|
+
* EARLYNOTIFY-GATEBYPASS (a)/(b) — the shared turn-finality selector for the completion
|
|
37
|
+
* final-assistant judgement, so the ~duplicated "which bubble is the turn's final answer"
|
|
38
|
+
* logic is decided ONE way (UNIFY A-6).
|
|
39
|
+
*
|
|
40
|
+
* Returns the message that qualifies as the turn's FINAL assistant bubble, or null when the
|
|
41
|
+
* transcript does not (yet) prove a turn end. The rule is a NON-EMPTY LATEST user-facing
|
|
42
|
+
* assistant/model bubble:
|
|
43
|
+
* - Scanning from the end, the FIRST user-facing assistant/model bubble encountered IS the
|
|
44
|
+
* turn-end candidate. If it is EMPTY (a streaming placeholder / mid-turn narration whose
|
|
45
|
+
* text has not landed), the turn is still in flight → return null. Crucially we do NOT walk
|
|
46
|
+
* back past that empty bubble to promote an EARLIER assistant narration to "final" (the
|
|
47
|
+
* Defect-B walk-back).
|
|
48
|
+
* - Trailing activity/internal bubbles (tool/thought/status) are skipped — they are not the
|
|
49
|
+
* assistant's user-facing answer.
|
|
50
|
+
* - A trailing user-facing USER message (a freshly dispatched task with no reply yet) means the
|
|
51
|
+
* assistant did not have the last word — no earlier bubble is promoted here; callers that must
|
|
52
|
+
* still reach a prior turn's tail use the timestamp-scoped extractor instead.
|
|
53
|
+
*
|
|
54
|
+
* A bare snapshot-idle with an arbitrary non-empty tail therefore does NOT qualify as a turn end;
|
|
55
|
+
* only a genuine latest-assistant bubble does. Turn-finality signals that live OUTSIDE the
|
|
56
|
+
* transcript (a committed generating→idle FSM transition, a self-attributing final_summary_json,
|
|
57
|
+
* or a continuous-idle streak) are enforced by the callers (the CLI completion gate, the reconcile
|
|
58
|
+
* grace gate) on top of this structural check.
|
|
59
|
+
*/
|
|
60
|
+
export declare function selectFinalAssistantTurnEndMessage(messages: ChatMessage[] | null | undefined): ChatMessage | null;
|
|
35
61
|
export declare const BUILTIN_CHAT_MESSAGE_KINDS: readonly ["standard", "thought", "tool", "terminal", "system"];
|
|
36
62
|
export type BuiltinChatMessageKind = typeof BUILTIN_CHAT_MESSAGE_KINDS[number];
|
|
37
63
|
export type ChatMessageKind = BuiltinChatMessageKind | (string & {});
|
|
@@ -268,6 +268,13 @@ export declare class CliProviderInstance implements ProviderInstance {
|
|
|
268
268
|
recordAcknowledgedUserInput(input: InputEnvelope | string): void;
|
|
269
269
|
/** Drop user-input ack entries older than the dedup window so the map can't grow unbounded. */
|
|
270
270
|
private pruneRecentUserInputAcks;
|
|
271
|
+
/**
|
|
272
|
+
* Owner token for this session in the antigravity conversation-claim
|
|
273
|
+
* registry. Derived identically to the dispatcher's read-side token
|
|
274
|
+
* (workspace + spawn time) so the claims the dispatcher records under this
|
|
275
|
+
* session are the ones dispose() releases.
|
|
276
|
+
*/
|
|
277
|
+
private antigravityClaimOwner;
|
|
271
278
|
dispose(): void;
|
|
272
279
|
private completedDebounceTimer;
|
|
273
280
|
private completedDebouncePending;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/** A claim older than this with no refresh is reclaimable (owner presumed dead). */
|
|
2
|
+
export declare const CLAIM_STALE_MS: number;
|
|
3
|
+
/**
|
|
4
|
+
* Derive the per-session owner token. Both the dispatcher (from the read input:
|
|
5
|
+
* workspace + sessionStartedAtMs) and the provider instance (from its
|
|
6
|
+
* workingDir + startedAt, or its instanceId) call this with the same inputs so
|
|
7
|
+
* claims and releases line up. Returns '' when there is no stable identity to
|
|
8
|
+
* key on (e.g. a workspace-less discovery with no spawn time) — the caller then
|
|
9
|
+
* skips claiming but the exclusion checks still run against existing claims.
|
|
10
|
+
*/
|
|
11
|
+
export declare function antigravityOwnerToken(workspace: string, sessionStartedAtMs: number, instanceId?: string): string;
|
|
12
|
+
/**
|
|
13
|
+
* Claim `uuid` for `owner`. Succeeds (and refreshes) when the conversation is
|
|
14
|
+
* unclaimed, already owned by this owner, or held by a stale (abandoned) owner.
|
|
15
|
+
* Fails when a DIFFERENT live owner holds it. Returns whether `owner` holds the
|
|
16
|
+
* claim after the call.
|
|
17
|
+
*/
|
|
18
|
+
export declare function claimAntigravityConversation(uuid: string, owner: string, now?: number): boolean;
|
|
19
|
+
/** True when `uuid` is held by a live owner OTHER than `owner`. */
|
|
20
|
+
export declare function isAntigravityConversationClaimedByOther(uuid: string, owner: string, now?: number): boolean;
|
|
21
|
+
/** The live owner of `uuid`, or undefined when unclaimed or stale. */
|
|
22
|
+
export declare function antigravityConversationOwner(uuid: string, now?: number): string | undefined;
|
|
23
|
+
/** Release a single conversation, only if held by `owner` (or `owner` empty). */
|
|
24
|
+
export declare function releaseAntigravityConversation(uuid: string, owner?: string): void;
|
|
25
|
+
/** Release every conversation held by `owner` (called on session shutdown). */
|
|
26
|
+
export declare function releaseAntigravityOwner(owner: string): void;
|
|
27
|
+
/** Test-only: wipe all claims so each test starts from a clean registry. */
|
|
28
|
+
export declare function __resetAntigravityClaimRegistry(): void;
|
|
@@ -38,6 +38,17 @@
|
|
|
38
38
|
* pty parser (which only echoes the user's own input) — assistant
|
|
39
39
|
* answers appeared lost even though they were on disk.
|
|
40
40
|
*
|
|
41
|
+
* Schema-drift resilience: the exact field path (20 → 1/8 for the answer,
|
|
42
|
+
* 19 → 2/3 for the prompt) is empirically verified against real stores, but
|
|
43
|
+
* antigravity may move it in a future build. So when the known path yields
|
|
44
|
+
* no text, instead of silently dropping the turn we fall back to a UTF-8
|
|
45
|
+
* printable-run scan of the payload (recoverMessageText) that recovers the
|
|
46
|
+
* answer/prompt even if the field number drifted — while explicitly
|
|
47
|
+
* EXCLUDING the reasoning subtree (20 → 3) so internal reasoning is never
|
|
48
|
+
* surfaced as the answer. Only when even that finds nothing beyond
|
|
49
|
+
* reasoning/metadata is the step dropped, and a content-free DEBUG
|
|
50
|
+
* breadcrumb is logged so a real drift is greppable rather than invisible.
|
|
51
|
+
*
|
|
41
52
|
* This adapter provides:
|
|
42
53
|
* - Full coverage from a per-session .db (current format) — preferred.
|
|
43
54
|
* - Full coverage when a brain transcript exists (legacy authoritative source).
|
|
@@ -4,6 +4,10 @@ export interface NativeHistoryInput {
|
|
|
4
4
|
sessionId?: string;
|
|
5
5
|
providerSessionId?: string;
|
|
6
6
|
historySessionId?: string;
|
|
7
|
+
/** Daemon instance id of the reading session. Used (with workspace +
|
|
8
|
+
* sessionStartedAtMs) to derive the antigravity conversation-claim owner
|
|
9
|
+
* token so two concurrent sessions never bind to the same .db. */
|
|
10
|
+
instanceId?: string;
|
|
7
11
|
workspace?: string;
|
|
8
12
|
sessionStartedAtMs?: number;
|
|
9
13
|
format?: string;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@adhdev/daemon-core",
|
|
3
|
-
"version": "0.9.82-rc.
|
|
3
|
+
"version": "0.9.82-rc.459",
|
|
4
4
|
"description": "ADHDev daemon core — CDP, IDE detection, providers, command execution",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -46,8 +46,8 @@
|
|
|
46
46
|
"author": "vilmire",
|
|
47
47
|
"license": "AGPL-3.0-or-later",
|
|
48
48
|
"dependencies": {
|
|
49
|
-
"@adhdev/mesh-shared": "0.9.82-rc.
|
|
50
|
-
"@adhdev/session-host-core": "0.9.82-rc.
|
|
49
|
+
"@adhdev/mesh-shared": "0.9.82-rc.459",
|
|
50
|
+
"@adhdev/session-host-core": "0.9.82-rc.459",
|
|
51
51
|
"@agentclientprotocol/sdk": "^0.16.1",
|
|
52
52
|
"ajv": "^8.20.0",
|
|
53
53
|
"ajv-formats": "^3.0.1",
|
package/src/index.ts
CHANGED
|
@@ -394,6 +394,8 @@ export {
|
|
|
394
394
|
getDebugRuntimeConfig,
|
|
395
395
|
resetDebugRuntimeConfig,
|
|
396
396
|
shouldCollectTraceCategory,
|
|
397
|
+
isAlwaysOnTraceCategory,
|
|
398
|
+
ALWAYS_ON_TRACE_CATEGORIES,
|
|
397
399
|
} from './logging/debug-config.js';
|
|
398
400
|
export type { DebugRuntimeOptions, DebugRuntimeConfig } from './logging/debug-config.js';
|
|
399
401
|
export {
|
|
@@ -20,6 +20,26 @@ export interface DebugRuntimeConfig {
|
|
|
20
20
|
const NORMAL_TRACE_BUFFER_SIZE = 200
|
|
21
21
|
const DEV_TRACE_BUFFER_SIZE = 1000
|
|
22
22
|
|
|
23
|
+
/**
|
|
24
|
+
* ALWAYS-ON trace categories. These bypass the `collectDebugTrace` master switch
|
|
25
|
+
* (and category selection) so they are collected in production daemons where
|
|
26
|
+
* `--trace` is unset. They exist so mesh completion diagnostics — the FSM-transition
|
|
27
|
+
* and completion-gate snapshots that explain an early / missing agent:generating_completed
|
|
28
|
+
* notification — are retrievable via mesh_read_debug (chat_debug_bundle) without asking
|
|
29
|
+
* an operator to relaunch the daemon with tracing on.
|
|
30
|
+
*
|
|
31
|
+
* SAFETY: only add a category here after confirming every record() call site for it
|
|
32
|
+
* carries a content-free payload (statuses, epochs, timestamps, deltas, lengths, roles,
|
|
33
|
+
* enum-like reasons — never transcript / prompt / bubble text). Always-on collection makes
|
|
34
|
+
* such payloads unconditional, so a content-bearing field would leak into the ring buffer
|
|
35
|
+
* in production.
|
|
36
|
+
*/
|
|
37
|
+
export const ALWAYS_ON_TRACE_CATEGORIES: readonly string[] = ['completion-gate', 'fsm-transition']
|
|
38
|
+
|
|
39
|
+
export function isAlwaysOnTraceCategory(category?: string | null): boolean {
|
|
40
|
+
return !!category && ALWAYS_ON_TRACE_CATEGORIES.includes(category)
|
|
41
|
+
}
|
|
42
|
+
|
|
23
43
|
const DEFAULT_CONFIG: DebugRuntimeConfig = {
|
|
24
44
|
logLevel: 'info',
|
|
25
45
|
collectDebugTrace: false,
|
|
@@ -68,6 +88,11 @@ export function resetDebugRuntimeConfig(): void {
|
|
|
68
88
|
|
|
69
89
|
export function shouldCollectTraceCategory(category?: string | null): boolean {
|
|
70
90
|
const config = currentConfig
|
|
91
|
+
// Always-on categories are collected regardless of the collectDebugTrace master switch
|
|
92
|
+
// and regardless of any explicit traceCategories selection (they form a superset on top of
|
|
93
|
+
// whatever the operator requested), so an explicit --trace / --trace-categories run still
|
|
94
|
+
// includes them with its existing behavior unchanged.
|
|
95
|
+
if (isAlwaysOnTraceCategory(category)) return true
|
|
71
96
|
if (!config.collectDebugTrace) return false
|
|
72
97
|
if (!category) return true
|
|
73
98
|
if (config.traceCategories.length === 0) return true
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { getDebugRuntimeConfig, shouldCollectTraceCategory } from './debug-config.js'
|
|
1
|
+
import { getDebugRuntimeConfig, isAlwaysOnTraceCategory, shouldCollectTraceCategory } from './debug-config.js'
|
|
2
2
|
|
|
3
3
|
export type DebugTraceLevel = 'debug' | 'info' | 'warn' | 'error'
|
|
4
4
|
|
|
@@ -80,7 +80,12 @@ export function createDebugTraceStore(options: DebugTraceStoreOptions): DebugTra
|
|
|
80
80
|
|
|
81
81
|
return {
|
|
82
82
|
record(event: DebugTraceEvent): DebugTraceEntry | null {
|
|
83
|
-
|
|
83
|
+
// The store's `enabled` flag mirrors collectDebugTrace (set by configureDebugTraceStore),
|
|
84
|
+
// so it is false on a production daemon. Always-on categories must still land in the ring
|
|
85
|
+
// even then — otherwise the second gate here would swallow what shouldCollectTraceCategory
|
|
86
|
+
// just admitted. They share the same fixed-capacity buffer, so heavy always-on traffic can
|
|
87
|
+
// evict older opt-in entries; that is accepted (no separate ring).
|
|
88
|
+
if (!options.enabled && !isAlwaysOnTraceCategory(event.category)) return null
|
|
84
89
|
const entry = createEntry(event)
|
|
85
90
|
entries.push(entry)
|
|
86
91
|
if (entries.length > capacity) {
|
|
@@ -577,7 +577,7 @@ function buildRulesSection(coordinatorCliType?: string): string {
|
|
|
577
577
|
- **Verify via git, not source.** Use \`mesh_git_status\` to confirm side effects. Treat agent summaries as self-reports, not verification.
|
|
578
578
|
- **Limit parallelism.** Start with 1–2 tasks; scale only on success. Never duplicate a session because \`mesh_read_chat\` shows no final message while tool/terminal activity is ongoing. This caps *concurrent* load — it does not mean serialize independent work: when a new, independent request arrives and there is headroom under \`maxParallelTasks\`, dispatch it right away rather than waiting for an in-flight task or a user nudge (read-only diagnosis especially, since it has no merge cost).
|
|
579
579
|
- **Check history first.** Call \`mesh_task_history\` at session start to avoid duplicate work and inform recovery. On failure, read task history before retrying.
|
|
580
|
-
- **Sequence shared-base-moving merges.** Parallel dispatch is encouraged, but merging one worktree can advance another in-flight worktree's base — especially
|
|
580
|
+
- **Sequence shared-base-moving merges.** Parallel dispatch is encouraged, but merging one worktree can advance another in-flight worktree's base — especially a shared submodule pointer — turning a clean fast-forward into a diverged rebase (patch-equivalence correctly blocks this). Before merging an in-flight worktree while siblings are also in flight, land in an intentional order, re-clone long-running worktrees from the advanced base, or expect to manually rebase + ff-only the laggards; merging an independent fix mid-flight can strand siblings into a rebase.
|
|
581
581
|
- **Converge branches.** After worktree tasks: refine/fast-forward, or classify as \`pushed_feature_branch_needs_merge\` / \`blocked_review\` / \`cleanup_candidate\` / \`not_mergeable\`. Clean up with \`mesh_remove_node\`.
|
|
582
582
|
- **Refinery is config-driven.** \`mesh_refine_node\` must run validation from \`.adhdev/refine.{json,yaml,yml}\` or \`repo-mesh.refine.*\`. Heuristics are scaffolding only.
|
|
583
583
|
- **Submodule reachability = publish-needed.** \`submodule_reachability_failed\` → classify as \`blocked_review\`, request user approval to push to submodule main, then rerun \`mesh_refine_node\`.
|
|
@@ -4,8 +4,19 @@ import { updateDirectDispatchStatus, cleanupTerminalDirectDispatches } from './m
|
|
|
4
4
|
import { markSessionDeliveriesTerminal } from './mesh-delivery-policy.js';
|
|
5
5
|
import { queuePendingMeshCoordinatorEvent } from './mesh-events-pending.js';
|
|
6
6
|
import { readNonEmptyString, readRecord, resolveEventSessionId, readWorkerResultMetadata, isWeakCompletionEvidence, buildMeshSystemMessage } from './mesh-events-utils.js';
|
|
7
|
+
import { recordDebugTrace } from '../logging/debug-trace.js';
|
|
7
8
|
import { meshNodeIdMatches, sessionIdsEquivalent, type MeshNodeIdentified } from '@adhdev/mesh-shared';
|
|
8
9
|
|
|
10
|
+
// EARLYNOTIFY-GATEBYPASS (d): every completed-emit producer that bypasses the CLI-provider
|
|
11
|
+
// completion gate (transcript-reconcile synth here, no-progress reconcile below, the fast-collapse
|
|
12
|
+
// synth in cli-provider-instance) records a completion-gate trace so a synthesized "completed and
|
|
13
|
+
// idle" emit can never again be silent. Content-free by construction — keyed by taskId + source
|
|
14
|
+
// only, never worker/screen text. completion-gate is an ALWAYS_ON_TRACE_CATEGORY, so recordDebugTrace
|
|
15
|
+
// self-gates and lands in the ring even on a production daemon.
|
|
16
|
+
function recordSynthCompletionGateTrace(stage: string, payload: Record<string, unknown>): void {
|
|
17
|
+
recordDebugTrace({ category: 'completion-gate', stage, level: 'debug', payload });
|
|
18
|
+
}
|
|
19
|
+
|
|
9
20
|
// ---------------------------------------------------------------------------
|
|
10
21
|
// Stale direct-dispatch detection & transcript reconciliation
|
|
11
22
|
// ---------------------------------------------------------------------------
|
|
@@ -232,6 +243,14 @@ export function reconcileDirectDispatchCompletionFromTranscript(args: {
|
|
|
232
243
|
completedAt,
|
|
233
244
|
});
|
|
234
245
|
const workerResult = evidence.workerResult;
|
|
246
|
+
// EARLYNOTIFY-GATEBYPASS (c): a transcript-reconcile synth is TENTATIVE unless the worker's
|
|
247
|
+
// summary self-attributes to this turn — i.e. it parsed a worker-result-shaped JSON
|
|
248
|
+
// (`final_summary_json`), the same self-attribution the grace gate below exempts. A plain-text
|
|
249
|
+
// transcript tail proves neither turn-finality nor that this reconcile beat the real completion,
|
|
250
|
+
// so it is marked WEAK: buildPendingEventFingerprint then keys it `…::weak`, leaving the
|
|
251
|
+
// `…::genuine` slot free for the worker's own later agent:generating_completed to surface (the
|
|
252
|
+
// CANON-B weak→genuine supersession) instead of being dropped as a duplicate.
|
|
253
|
+
const selfAttributing = workerResult.source === 'final_summary_json';
|
|
235
254
|
const dispatchTime = dispatch?.timestamp ? new Date(dispatch.timestamp).getTime() : Number.NaN;
|
|
236
255
|
const transcriptTime = args.transcriptMessageAt ? new Date(args.transcriptMessageAt).getTime() : Number.NaN;
|
|
237
256
|
const transcriptAfterDispatch = Number.isFinite(dispatchTime) && Number.isFinite(transcriptTime) && transcriptTime >= dispatchTime;
|
|
@@ -281,7 +300,9 @@ export function reconcileDirectDispatchCompletionFromTranscript(args: {
|
|
|
281
300
|
dispatchEntryId: dispatch?.id,
|
|
282
301
|
dispatchTimestamp: dispatch?.timestamp,
|
|
283
302
|
transcriptMessageAt: readNonEmptyString(args.transcriptMessageAt),
|
|
284
|
-
|
|
303
|
+
// Honestly reflect self-attribution: a plain-text tail did NOT prove a turn-final
|
|
304
|
+
// assistant message (only a self-attributing final_summary_json did).
|
|
305
|
+
transcriptFinalAssistantPresent: selfAttributing,
|
|
285
306
|
},
|
|
286
307
|
evidence,
|
|
287
308
|
},
|
|
@@ -308,6 +329,11 @@ export function reconcileDirectDispatchCompletionFromTranscript(args: {
|
|
|
308
329
|
finalSummary,
|
|
309
330
|
taskId: args.taskId,
|
|
310
331
|
workerResult,
|
|
332
|
+
// EARLYNOTIFY-GATEBYPASS (c): mark a non-self-attributing synth WEAK so its pending
|
|
333
|
+
// fingerprint is `…::weak` (isWeakCompletionMetadata reads evidenceLevel), never claiming
|
|
334
|
+
// the genuine dedup slot. evidenceLevel:'weak' is deliberately NOT a false-idle marker
|
|
335
|
+
// (the transcript tail existed) — it keeps the completion superseable, not suppressed.
|
|
336
|
+
...(selfAttributing ? {} : { evidenceLevel: 'weak' as const }),
|
|
311
337
|
completionDiagnostic: {
|
|
312
338
|
reason: 'direct_task_transcript_reconciliation',
|
|
313
339
|
terminalLedgerKind: kind,
|
|
@@ -331,6 +357,16 @@ export function reconcileDirectDispatchCompletionFromTranscript(args: {
|
|
|
331
357
|
...(targetCoordinatorSessionId ? { targetCoordinatorSessionId } : {}),
|
|
332
358
|
});
|
|
333
359
|
|
|
360
|
+
// (d) The synth fired — record it so this gate-bypassing emit is observable.
|
|
361
|
+
recordSynthCompletionGateTrace('synth-fire', {
|
|
362
|
+
producer: 'transcript_reconcile',
|
|
363
|
+
source: args.source || 'direct_task_transcript_reconciliation',
|
|
364
|
+
taskId: args.taskId,
|
|
365
|
+
kind,
|
|
366
|
+
selfAttributing,
|
|
367
|
+
evidenceLevel: selfAttributing ? 'sufficient' : 'weak',
|
|
368
|
+
});
|
|
369
|
+
|
|
334
370
|
return { reconciled: true, kind, workerResult, ledgerEntryId: entry.id };
|
|
335
371
|
}
|
|
336
372
|
|
|
@@ -349,15 +385,29 @@ export function buildNoProgressCompletionReconciliation(args: {
|
|
|
349
385
|
const completionDiagnostic = readRecord(args.metadataEvent.completionDiagnostic);
|
|
350
386
|
const finalSummary = readNonEmptyString(args.metadataEvent.finalSummary);
|
|
351
387
|
const status = readNonEmptyString(args.metadataEvent.status).toLowerCase();
|
|
388
|
+
// EARLYNOTIFY-GATEBYPASS (c): a bare status flag (idle/ready/completed) with NO worker text is
|
|
389
|
+
// the weakest possible "done" evidence — it is exactly the false-idle the no-progress monitor
|
|
390
|
+
// fires on. Only a real assistant summary / worker result / confirmed final-assistant makes this
|
|
391
|
+
// reconcile self-attributing. When it is not, mark the synthesized completion WEAK so a later
|
|
392
|
+
// genuine completion can still supersede it (and buildMeshSystemMessage appends a verify hint).
|
|
393
|
+
const noProgressSelfAttributing = Boolean(
|
|
394
|
+
finalSummary || workerResult || completionDiagnostic?.finalAssistantPresent === true,
|
|
395
|
+
);
|
|
352
396
|
const explicitCompletionEvidence = Boolean(
|
|
353
|
-
|
|
354
|
-
|| workerResult
|
|
355
|
-
|| completionDiagnostic?.finalAssistantPresent === true
|
|
397
|
+
noProgressSelfAttributing
|
|
356
398
|
|| status === 'idle'
|
|
357
399
|
|| status === 'ready'
|
|
358
400
|
|| status === 'completed',
|
|
359
401
|
);
|
|
360
402
|
if (explicitCompletionEvidence) {
|
|
403
|
+
// (d) A no-progress→completion synth bypasses the CLI provider gate — trace it.
|
|
404
|
+
recordSynthCompletionGateTrace('synth-fire', {
|
|
405
|
+
producer: 'no_progress_reconcile',
|
|
406
|
+
source: 'no_progress_reconciliation',
|
|
407
|
+
taskId: readNonEmptyString(args.metadataEvent.taskId),
|
|
408
|
+
selfAttributing: noProgressSelfAttributing,
|
|
409
|
+
evidenceLevel: noProgressSelfAttributing ? 'sufficient' : 'weak',
|
|
410
|
+
});
|
|
361
411
|
return {
|
|
362
412
|
...args.metadataEvent,
|
|
363
413
|
targetSessionId: sessionId,
|
|
@@ -367,6 +417,7 @@ export function buildNoProgressCompletionReconciliation(args: {
|
|
|
367
417
|
source: 'no_progress_reconciliation',
|
|
368
418
|
reconciledFromEvent: 'monitor:no_progress',
|
|
369
419
|
timestamp: args.metadataEvent.timestamp ?? Date.now(),
|
|
420
|
+
...(noProgressSelfAttributing ? {} : { evidenceLevel: 'weak' as const }),
|
|
370
421
|
completionDiagnostic: {
|
|
371
422
|
...(completionDiagnostic || {}),
|
|
372
423
|
reconciliationReason: 'provider_completion_evidence',
|
|
@@ -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) });
|