@adhdev/daemon-core 0.9.82-rc.362 → 0.9.82-rc.363
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/cli-state-engine.d.ts +1 -1
- package/dist/cli-adapters/provider-cli-shared.d.ts +10 -0
- package/dist/index.js +77 -8
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +77 -8
- package/dist/index.mjs.map +1 -1
- package/dist/mesh/mesh-events-utils.d.ts +1 -0
- package/dist/providers/cli-provider-instance.d.ts +2 -0
- package/package.json +2 -2
- package/src/cli-adapters/cli-state-engine.ts +7 -1
- package/src/cli-adapters/provider-cli-adapter.ts +1 -0
- package/src/cli-adapters/provider-cli-shared.ts +10 -0
- package/src/mesh/mesh-events-coordinator.ts +37 -0
- package/src/mesh/mesh-events-utils.ts +28 -2
- package/src/providers/cli-provider-instance.ts +46 -0
|
@@ -52,6 +52,7 @@ export declare function resolveMeshSurfacedSessionPreview(metadataEvent: Record<
|
|
|
52
52
|
role: 'assistant';
|
|
53
53
|
receivedAt: number;
|
|
54
54
|
} | undefined;
|
|
55
|
+
export declare function isWeakCompletionMetadata(metadataEvent: Record<string, unknown>): boolean;
|
|
55
56
|
export declare function buildMeshSystemMessage(args: {
|
|
56
57
|
event: string;
|
|
57
58
|
nodeLabel: string;
|
|
@@ -183,6 +183,8 @@ export declare class CliProviderInstance implements ProviderInstance {
|
|
|
183
183
|
private hasAdapterPendingResponse;
|
|
184
184
|
private shouldSuppressStaleParsedBusyStatus;
|
|
185
185
|
private getCompletedFinalizationBlock;
|
|
186
|
+
private hasApprovalResolutionEvidence;
|
|
187
|
+
private approvalResolutionFinalizationBlock;
|
|
186
188
|
private scheduleCompletedDebounceFlush;
|
|
187
189
|
private isMeshWorkerSession;
|
|
188
190
|
private meshTraceCtx;
|
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.363",
|
|
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,7 +46,7 @@
|
|
|
46
46
|
"author": "vilmire",
|
|
47
47
|
"license": "AGPL-3.0-or-later",
|
|
48
48
|
"dependencies": {
|
|
49
|
-
"@adhdev/mesh-shared": "0.9.82-rc.
|
|
49
|
+
"@adhdev/mesh-shared": "0.9.82-rc.363",
|
|
50
50
|
"@adhdev/session-host-core": "*",
|
|
51
51
|
"@agentclientprotocol/sdk": "^0.16.1",
|
|
52
52
|
"ajv": "^8.20.0",
|
|
@@ -126,7 +126,13 @@ export class CliStateEngine {
|
|
|
126
126
|
* seq) is always a real, distinct approval and must be written.
|
|
127
127
|
*/
|
|
128
128
|
approvalEntrySeq = 0;
|
|
129
|
-
|
|
129
|
+
// Records which approval entry the last resolveModal() handled. Set unconditionally on
|
|
130
|
+
// every resolveModal (auto-approve fire, dashboard/mesh_approve, dev-cli-debug), so
|
|
131
|
+
// `lastResolvedEntrySeq >= approvalEntrySeq` is positive, ADHDev-side proof that the
|
|
132
|
+
// current/latest approval entry was actually resolved. Exposed via getStatus so the
|
|
133
|
+
// completion finalization gate (FALSEIDLE-a) can require resolution evidence before
|
|
134
|
+
// confirming a waiting_approval→idle transition. Public (read-only by convention).
|
|
135
|
+
lastResolvedEntrySeq = -1;
|
|
130
136
|
/**
|
|
131
137
|
* When the engine previously held a modal but the latest parse failed
|
|
132
138
|
* to extract one, we record the timestamp here and only drop the modal
|
|
@@ -967,6 +967,7 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
967
967
|
workingDir: this.workingDir,
|
|
968
968
|
activeModal: effectiveModal,
|
|
969
969
|
approvalEntrySeq: this.engine.approvalEntrySeq,
|
|
970
|
+
lastResolvedEntrySeq: this.engine.lastResolvedEntrySeq,
|
|
970
971
|
pendingOutboundCount: this.pendingOutboundQueue.length,
|
|
971
972
|
pendingOutboundMessages: this.pendingOutboundQueue.map((message) => ({
|
|
972
973
|
id: message.id,
|
|
@@ -38,6 +38,16 @@ export interface CliSessionStatus {
|
|
|
38
38
|
* seq is the only reliable discriminator.
|
|
39
39
|
*/
|
|
40
40
|
approvalEntrySeq?: number;
|
|
41
|
+
/**
|
|
42
|
+
* The approval entry seq that the engine's last resolveModal() handled. When
|
|
43
|
+
* `lastResolvedEntrySeq >= approvalEntrySeq` (and approvalEntrySeq > 0) it is positive
|
|
44
|
+
* evidence that the current/latest approval entry was resolved through ADHDev (auto-approve
|
|
45
|
+
* fire, dashboard / mesh_approve, or dev-cli-debug — all route through resolveModal). The
|
|
46
|
+
* completion finalization gate uses this to avoid confirming a waiting_approval→idle
|
|
47
|
+
* transition for which no resolution actually occurred (a false idle: the spec's text-based
|
|
48
|
+
* approval→idle rule tripped while the modal is still on screen).
|
|
49
|
+
*/
|
|
50
|
+
lastResolvedEntrySeq?: number;
|
|
41
51
|
activeInteractivePrompt?: InteractivePrompt | null;
|
|
42
52
|
pendingOutboundCount?: number;
|
|
43
53
|
pendingOutboundMessages?: Array<{
|
|
@@ -457,6 +457,17 @@ function deliverTaskToSession(dispatchThunk: () => Promise<unknown>, ctx: Delive
|
|
|
457
457
|
});
|
|
458
458
|
}
|
|
459
459
|
|
|
460
|
+
// WTCLAIM: normalize a workspace path for base-vs-worktree comparison. Mirrors
|
|
461
|
+
// cli-manager.ts normalizeDirForCompare (fix-B) — folds separator style, trailing
|
|
462
|
+
// slashes, and case (Windows paths are case-insensitive) so a base node and a worktree
|
|
463
|
+
// clone, whose only structural difference is their distinct workspace roots, are still
|
|
464
|
+
// told apart. Kept local (the cli-manager copy is module-private) so the comparison rule
|
|
465
|
+
// stays identical to the one fix-B already uses on the worker side.
|
|
466
|
+
function normalizeMeshWorkspaceForCompare(dir?: string): string {
|
|
467
|
+
if (typeof dir !== 'string') return '';
|
|
468
|
+
return dir.trim().replace(/[\\/]+/g, '/').replace(/\/+$/, '').toLowerCase();
|
|
469
|
+
}
|
|
470
|
+
|
|
460
471
|
export function tryAssignQueueTask(
|
|
461
472
|
components: DaemonComponents,
|
|
462
473
|
meshId: string,
|
|
@@ -466,6 +477,32 @@ export function tryAssignQueueTask(
|
|
|
466
477
|
): boolean {
|
|
467
478
|
const mesh = getMeshWithCache(components, meshId);
|
|
468
479
|
const node = mesh?.nodes.find((n: any) => readMeshNodeId(n) === nodeId);
|
|
480
|
+
|
|
481
|
+
// WTCLAIM (fix-B extended to the enqueue→claim path): a base-targeted task must never be
|
|
482
|
+
// claimed by — and dispatched into — a co-located worktree-clone session, nor vice versa.
|
|
483
|
+
// The drain candidate's nodeId is derived from settings.meshNodeId || settings.nodeId
|
|
484
|
+
// (triggerMeshQueue), so a worktree session whose meshNodeId is empty/stale falls back to
|
|
485
|
+
// settings.nodeId = the BASE node id and impersonates the base node here. fix-B's worker-side
|
|
486
|
+
// workspace scope only ran for sessionless dispatch (meshScopeNodeId && !targetSessionId); the
|
|
487
|
+
// claim path ALWAYS carries a targetSessionId, so it never engaged. Apply the same scope here:
|
|
488
|
+
// for a LOCAL claiming session (adapter resolvable on this daemon), require its actual
|
|
489
|
+
// workingDir to match the target node's declared workspace. On a confirmed mismatch, refuse the
|
|
490
|
+
// claim so the task returns to pending for the correctly-scoped session/node to pull. Scoped to
|
|
491
|
+
// local sessions where the workspace is verifiable — a remote session lives on another daemon
|
|
492
|
+
// whose paths we cannot compare here (and remote candidates are already nodeId-matched from
|
|
493
|
+
// getRemoteIdleSessions). Conservative by design: when either workspace is unknown we do NOT
|
|
494
|
+
// skip, so a node with no declared workspace keeps its prior behavior and no legitimate claim
|
|
495
|
+
// is starved.
|
|
496
|
+
const localClaimAdapter = components.cliManager?.adapters?.get(sessionId) as { workingDir?: string } | undefined;
|
|
497
|
+
if (localClaimAdapter) {
|
|
498
|
+
const sessionWorkspace = normalizeMeshWorkspaceForCompare(localClaimAdapter.workingDir);
|
|
499
|
+
const nodeWorkspace = normalizeMeshWorkspaceForCompare(readNonEmptyString(node?.workspace));
|
|
500
|
+
if (sessionWorkspace && nodeWorkspace && sessionWorkspace !== nodeWorkspace) {
|
|
501
|
+
LOG.info('MeshQueue', `WTCLAIM: refusing claim for node ${nodeId} (${sessionId}) — session workspace "${sessionWorkspace}" ≠ node workspace "${nodeWorkspace}" (cross-workspace dispatch blocked)`);
|
|
502
|
+
return false;
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
|
|
469
506
|
const capabilityTags = buildMeshNodeCapabilityTags(node, providerType);
|
|
470
507
|
// Per-(node, provider) maxParallel cap (RepoMeshNodePolicy.providerRoles) layers
|
|
471
508
|
// on top of the global/taskMode caps — stricter wins. Resolved here where the
|
|
@@ -195,6 +195,23 @@ function readEventTimestampValue(value: unknown): number {
|
|
|
195
195
|
return 0;
|
|
196
196
|
}
|
|
197
197
|
|
|
198
|
+
// A completion whose evidence is WEAK: the worker FSM reached idle but the turn's
|
|
199
|
+
// final assistant message was never confirmed (missing_final_assistant / a
|
|
200
|
+
// finalAssistantPresent=false diagnostic), or the event self-declares insufficient/weak
|
|
201
|
+
// evidence. cli-provider-instance emits this on the CANON-C decoupled-immediate path and
|
|
202
|
+
// on a forced finalization timeout, and the new structural approval-resolution gate
|
|
203
|
+
// (FALSEIDLE-a) can also let an unconfirmed approval→idle through. Such a completion is NOT
|
|
204
|
+
// trustworthy terminal evidence — surface a verify hint so the coordinator confirms via
|
|
205
|
+
// mesh_read_chat / git status before declaring the task done. Mirrors
|
|
206
|
+
// isWeakCompletionMetadata in mesh-events-pending.ts.
|
|
207
|
+
export function isWeakCompletionMetadata(metadataEvent: Record<string, unknown>): boolean {
|
|
208
|
+
const evidenceLevel = readNonEmptyString(metadataEvent.evidenceLevel);
|
|
209
|
+
if (evidenceLevel === 'insufficient' || evidenceLevel === 'weak') return true;
|
|
210
|
+
if (metadataEvent.reviewRecommended === true) return true;
|
|
211
|
+
const diag = readRecord(metadataEvent.completionDiagnostic);
|
|
212
|
+
return diag?.finalAssistantPresent === false || diag?.blockReason === 'missing_final_assistant';
|
|
213
|
+
}
|
|
214
|
+
|
|
198
215
|
function formatCompletionMetadata(event: Record<string, unknown>): string {
|
|
199
216
|
const completionDiagnostic = event.completionDiagnostic && typeof event.completionDiagnostic === 'object'
|
|
200
217
|
? event.completionDiagnostic as Record<string, unknown>
|
|
@@ -229,6 +246,13 @@ export function buildMeshSystemMessage(args: {
|
|
|
229
246
|
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.`;
|
|
230
247
|
}
|
|
231
248
|
const reviewRecommended = args.metadataEvent.reviewRecommended === true;
|
|
249
|
+
// FALSEIDLE-b: a weak completion (missing final assistant / finalAssistantPresent=false /
|
|
250
|
+
// insufficient evidence) is not trustworthy terminal evidence even when reviewRecommended
|
|
251
|
+
// is not set. Append an explicit verify hint so the coordinator does not declare the task
|
|
252
|
+
// done off a false idle. Only the weak case is affected — a genuine completion (final
|
|
253
|
+
// assistant confirmed, no weak diagnostic) carries no extra note and is unchanged.
|
|
254
|
+
const weakCompletion = isWeakCompletionMetadata(args.metadataEvent);
|
|
255
|
+
const verifyTextNote = ' Completion evidence is weak — verify via mesh_read_chat or git status before declaring the task done; the worker may still be parked on an approval/modal.';
|
|
232
256
|
// Auto-surface the worker's final summary directly into the coordinator chat so it
|
|
233
257
|
// does not have to call mesh_read_chat just to see the result. The summary IS the
|
|
234
258
|
// worker's final assistant message; embedding it here replaces the previous
|
|
@@ -244,12 +268,14 @@ export function buildMeshSystemMessage(args: {
|
|
|
244
268
|
: completionSummary;
|
|
245
269
|
const verifyNote = reviewRecommended
|
|
246
270
|
? ' Completion evidence is insufficient — verify via git status or provider_session_id before assuming the task is done.'
|
|
247
|
-
: '';
|
|
271
|
+
: (weakCompletion ? verifyTextNote : '');
|
|
248
272
|
return `[System] ${args.nodeLabel} has completed its task and is now idle${metadata}. Its final summary is included below — read it directly and only call mesh_read_chat if you need the full transcript.${verifyNote}\n\n--- ${args.nodeLabel} final summary ---\n${surfaced}`;
|
|
249
273
|
}
|
|
250
274
|
const reviewNote = reviewRecommended
|
|
251
275
|
? ' 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.'
|
|
252
|
-
:
|
|
276
|
+
: (weakCompletion
|
|
277
|
+
? `${verifyTextNote} Use mesh_read_chat once if needed, but do not poll repeatedly.`
|
|
278
|
+
: ' Use mesh_read_chat once to review its final progress, but do not poll repeatedly.');
|
|
253
279
|
return `[System] ${args.nodeLabel} has completed its task and is now idle${metadata}. This completion came from the agent status event path;${reviewNote}`;
|
|
254
280
|
}
|
|
255
281
|
if (args.event === 'agent:waiting_approval') {
|
|
@@ -1401,6 +1401,12 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
1401
1401
|
}
|
|
1402
1402
|
}
|
|
1403
1403
|
|
|
1404
|
+
// (FALSEIDLE-a) Structural approval-resolution gate. Runs BEFORE the brittle
|
|
1405
|
+
// screen-text heuristic below so it also catches modals whose text does not match
|
|
1406
|
+
// looksLikeActiveApprovalPromptText (e.g. claude-cli's cd / "untrusted hooks" prompt).
|
|
1407
|
+
const approvalResolutionBlock = this.approvalResolutionFinalizationBlock(pending);
|
|
1408
|
+
if (approvalResolutionBlock) return approvalResolutionBlock;
|
|
1409
|
+
|
|
1404
1410
|
// Guard: if the screen still shows an approval/choice prompt as the last visible text,
|
|
1405
1411
|
// the turn is not complete even if the parsed status says idle and there is an assistant
|
|
1406
1412
|
// message. This catches the case where waiting_approval→idle transitions occur before
|
|
@@ -1420,6 +1426,46 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
1420
1426
|
return null;
|
|
1421
1427
|
}
|
|
1422
1428
|
|
|
1429
|
+
// (FALSEIDLE-a) Positive, structural proof that the latest approval entry was resolved
|
|
1430
|
+
// through ADHDev. resolveModal() — driven by auto-approve, dashboard/mesh_approve, and
|
|
1431
|
+
// dev-cli-debug alike — advances the engine's lastResolvedEntrySeq to the current
|
|
1432
|
+
// approvalEntrySeq. So `lastResolvedEntrySeq >= approvalEntrySeq` (with a real entry,
|
|
1433
|
+
// approvalEntrySeq > 0) means the modal we last saw was actually answered. Absence of this
|
|
1434
|
+
// evidence after a waiting_approval→idle transition means the idle is suspect: the spec's
|
|
1435
|
+
// text-based approval→idle rule false-tripped while the modal is still unresolved.
|
|
1436
|
+
// Fails OPEN (returns true) when the seq fields are unavailable, so the gate can never wedge
|
|
1437
|
+
// a session on a provider/adapter that does not surface the counters.
|
|
1438
|
+
private hasApprovalResolutionEvidence(): boolean {
|
|
1439
|
+
try {
|
|
1440
|
+
const status = this.adapter.getStatus({ allowParse: false }) as any;
|
|
1441
|
+
const entrySeq = typeof status?.approvalEntrySeq === 'number' ? status.approvalEntrySeq : 0;
|
|
1442
|
+
if (entrySeq <= 0) return true;
|
|
1443
|
+
const resolvedSeq = typeof status?.lastResolvedEntrySeq === 'number' ? status.lastResolvedEntrySeq : undefined;
|
|
1444
|
+
if (resolvedSeq === undefined) return true;
|
|
1445
|
+
return resolvedSeq >= entrySeq;
|
|
1446
|
+
} catch {
|
|
1447
|
+
return true;
|
|
1448
|
+
}
|
|
1449
|
+
}
|
|
1450
|
+
|
|
1451
|
+
// (FALSEIDLE-a) Hold a completion that is the anomalous DIRECT waiting_approval→idle
|
|
1452
|
+
// transition with no positive resolution evidence. A genuinely resolved approval routes
|
|
1453
|
+
// through resolveModal → setStatus('generating'), so its completion's previousStatus is
|
|
1454
|
+
// 'generating' (not 'waiting_approval') and this gate never fires for it. Scoped to
|
|
1455
|
+
// delegated mesh/coordinator sessions — whose only modal-resolution path is auto-approve /
|
|
1456
|
+
// mesh_approve (both advance lastResolvedEntrySeq) — so an interactive local session, where
|
|
1457
|
+
// a human may answer the PTY prompt directly and leave no resolveModal record, is untouched.
|
|
1458
|
+
// Non-terminal: the hold is bounded by COMPLETED_FINALIZATION_MAX_WAIT_MS (30s), giving a
|
|
1459
|
+
// settling auto-approve time to fire and advance the seq, and guaranteeing no permanent wedge
|
|
1460
|
+
// if resolution ever happens via a path that does not record evidence.
|
|
1461
|
+
private approvalResolutionFinalizationBlock(pending: CompletedDebouncePending): CompletedFinalizationBlock | null {
|
|
1462
|
+
if (pending.previousStatus !== 'waiting_approval') return null;
|
|
1463
|
+
const meshContext = !!(this.settings.meshNodeFor || this.settings.meshActiveTaskId || this.settings.launchedByCoordinator);
|
|
1464
|
+
if (!meshContext) return null;
|
|
1465
|
+
if (this.hasApprovalResolutionEvidence()) return null;
|
|
1466
|
+
return { reason: 'approval_resolution_unconfirmed', terminal: false };
|
|
1467
|
+
}
|
|
1468
|
+
|
|
1423
1469
|
private scheduleCompletedDebounceFlush(delayMs: number): void {
|
|
1424
1470
|
if (this.completedDebounceTimer) clearTimeout(this.completedDebounceTimer);
|
|
1425
1471
|
this.completedDebounceTimer = setTimeout(() => this.flushCompletedDebounceIfFinalized(), delayMs);
|