@adhdev/daemon-core 0.9.82-rc.565 → 0.9.82-rc.567
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-adapter-types.d.ts +14 -0
- package/dist/index.js +148 -20
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +148 -20
- package/dist/index.mjs.map +1 -1
- package/dist/mesh/mesh-active-work.d.ts +4 -0
- package/dist/providers/cli-provider-instance.d.ts +25 -0
- package/dist/providers/spec/cli-adapter.d.ts +1 -0
- package/dist/providers/spec/fsm-driver.d.ts +18 -0
- package/package.json +3 -3
- package/src/cli-adapter-types.d.ts +1 -0
- package/src/cli-adapter-types.ts +17 -0
- package/src/commands/chat-commands-write.ts +15 -1
- package/src/mesh/mesh-active-work.ts +22 -4
- package/src/mesh/mesh-event-forwarding.ts +41 -23
- package/src/mesh/mesh-reconcile-loop.ts +52 -0
- package/src/providers/cli-provider-instance.ts +75 -1
- package/src/providers/spec/cli-adapter.ts +37 -3
- package/src/providers/spec/fsm-driver.ts +27 -5
|
@@ -75,6 +75,10 @@ export interface BuildMeshActiveWorkOptions {
|
|
|
75
75
|
/** Include terminal direct rows (idle/failed) for handoff/recent-work surfaces. Defaults false. */
|
|
76
76
|
includeTerminalDirect?: boolean;
|
|
77
77
|
}
|
|
78
|
+
export declare function sessionStatusFromNodes(nodes: any[] | undefined, nodeId?: string, sessionId?: string): {
|
|
79
|
+
status?: MeshActiveWorkStatus;
|
|
80
|
+
staleReason?: string;
|
|
81
|
+
};
|
|
78
82
|
/**
|
|
79
83
|
* One session awaiting an approval decision — the derived row `mesh_list_pending_approvals`
|
|
80
84
|
* returns and the UI approvals inbox renders. A thin projection of the `awaiting_approval`
|
|
@@ -31,6 +31,15 @@ export declare class CliProviderInstance implements ProviderInstance {
|
|
|
31
31
|
* keystroke until the modal *content* has settled.
|
|
32
32
|
*/
|
|
33
33
|
private static readonly AUTO_APPROVE_SETTLE_MS;
|
|
34
|
+
/**
|
|
35
|
+
* APPROVAL-INBOX-BLINDSPOT (Fix A): how long after a LOCAL auto-approve fire the mesh
|
|
36
|
+
* event forwarder still treats the modal as "being resolved locally" and suppresses the
|
|
37
|
+
* coordinator notification. Chosen to comfortably cover the resolveModal → PTY absorb →
|
|
38
|
+
* status-leaves-approval round trip (incl. the win32 CR-resend loop) while staying short
|
|
39
|
+
* enough that a modal which auto-approve fired at but did NOT resolve re-surfaces to the
|
|
40
|
+
* coordinator on the next event. Aligned with the adapter's own approval cooldown scale.
|
|
41
|
+
*/
|
|
42
|
+
private static readonly APPROVAL_LOCAL_RESOLUTION_COOLDOWN_MS;
|
|
34
43
|
/**
|
|
35
44
|
* Busy-side hysteresis for the settle gate. A momentary `generating` flip
|
|
36
45
|
* while the SAME approval modal's button block is still on screen (its
|
|
@@ -200,6 +209,7 @@ export declare class CliProviderInstance implements ProviderInstance {
|
|
|
200
209
|
private autoApproveSettleTimer;
|
|
201
210
|
private autoApproveInactiveSince;
|
|
202
211
|
private autoApproveLastModalSeenAt;
|
|
212
|
+
private lastAutoApproveFiredAt;
|
|
203
213
|
private approvalStickyLastConcreteAt;
|
|
204
214
|
private approvalStickyModal;
|
|
205
215
|
private approvalStickyEntrySeq;
|
|
@@ -315,6 +325,21 @@ export declare class CliProviderInstance implements ProviderInstance {
|
|
|
315
325
|
* absent from some of them.
|
|
316
326
|
*/
|
|
317
327
|
resolveModalParkStatus(): 'waiting_choice' | 'waiting_approval' | null;
|
|
328
|
+
/**
|
|
329
|
+
* APPROVAL-INBOX-BLINDSPOT (Fix A): true when this session's approval modal was — or is
|
|
330
|
+
* being — resolved LOCALLY within the recent cooldown. Two independent positive signals:
|
|
331
|
+
* (1) auto-approve fired its resolveModal within APPROVAL_LOCAL_RESOLUTION_COOLDOWN_MS
|
|
332
|
+
* (lastAutoApproveFiredAt), or
|
|
333
|
+
* (2) the underlying adapter reports isApprovalRecentlyResolved() — its own resolve
|
|
334
|
+
* cooldown, which also covers a dashboard / mesh_approve resolution.
|
|
335
|
+
* The mesh event forwarder uses this to decide whether an agent:waiting_approval from an
|
|
336
|
+
* auto-approving worker can be safely SUPPRESSED (a local resolution is in flight) or must
|
|
337
|
+
* be FORWARDED (auto-approve is configured but has NOT actually resolved this modal, so the
|
|
338
|
+
* coordinator/inbox must be told). Keying suppression on real resolution — not just the
|
|
339
|
+
* autoApprove *intent* — is the blind-spot fix: a never-resolving worker approval is no
|
|
340
|
+
* longer silently dropped.
|
|
341
|
+
*/
|
|
342
|
+
approvalRecentlyResolvedLocally(now?: number): boolean;
|
|
318
343
|
/**
|
|
319
344
|
* NOTIF-HELD-DRAIN: true when this `waiting_approval` is a routine, transient tool-consent
|
|
320
345
|
* of an autonomously-progressing mesh session rather than a genuine human-await modal —
|
|
@@ -143,6 +143,7 @@ export declare class SpecCliAdapter implements CliAdapter {
|
|
|
143
143
|
writeRaw(data: string): void;
|
|
144
144
|
resize(cols: number, rows: number): void;
|
|
145
145
|
resolveModal(buttonIndex: number): void;
|
|
146
|
+
resolveModalMatched(buttonIndex: number): boolean;
|
|
146
147
|
resolveAction(data: unknown): Promise<void>;
|
|
147
148
|
setInteractivePromptResponse(response: InteractivePromptResponse): Promise<void>;
|
|
148
149
|
isApprovalRecentlyResolved(): boolean;
|
|
@@ -119,6 +119,13 @@ export interface ISpecDriver {
|
|
|
119
119
|
subscribe(listener: (ev: DashboardEvent) => void): () => void;
|
|
120
120
|
start(): void;
|
|
121
121
|
dispatch(cmd: DashboardCommand): void;
|
|
122
|
+
/**
|
|
123
|
+
* BUTTON-INDEX-MISMAP (Fix C.3): click a modal button by its FSM display index and report
|
|
124
|
+
* whether a button was actually matched and its confirm keys dispatched. Unlike the
|
|
125
|
+
* fire-and-forget `dispatch('click_modal_button')`, callers that need to know the click
|
|
126
|
+
* landed (mesh_approve → SpecCliAdapter.resolveModalMatched) can observe a miss.
|
|
127
|
+
*/
|
|
128
|
+
clickModalButton(index: number): boolean;
|
|
122
129
|
updateMeta(meta: Record<string, unknown>, replace?: boolean): void;
|
|
123
130
|
snapshot(): string;
|
|
124
131
|
getCursorPosition(): {
|
|
@@ -423,6 +430,17 @@ export declare class FsmDriver implements ISpecDriver {
|
|
|
423
430
|
*/
|
|
424
431
|
private scheduleWin32Submit;
|
|
425
432
|
private handleClickControl;
|
|
433
|
+
/**
|
|
434
|
+
* BUTTON-INDEX-MISMAP (Fix C.3): public modal-click entry that returns whether a button
|
|
435
|
+
* matching the requested FSM display index was actually found and its confirm keys were
|
|
436
|
+
* dispatched. The old private handleClickModalButton silently `return`ed on a miss (no
|
|
437
|
+
* modal captured, or no button whose `.index` equals the requested display index), so a
|
|
438
|
+
* mis-mapped index looked identical to a successful press. Callers that need to know
|
|
439
|
+
* whether the click landed (mesh_approve → resolveModal) can now observe the miss instead
|
|
440
|
+
* of reporting success into the void. The generic `dispatch('click_modal_button')` path
|
|
441
|
+
* keeps ignoring the return (fire-and-forget UI clicks).
|
|
442
|
+
*/
|
|
443
|
+
clickModalButton(index: number): boolean;
|
|
426
444
|
private handleClickModalButton;
|
|
427
445
|
/**
|
|
428
446
|
* Submit a modal-confirm key sequence (the choice key + its trailing CR).
|
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.567",
|
|
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",
|
|
@@ -47,8 +47,8 @@
|
|
|
47
47
|
"author": "vilmire",
|
|
48
48
|
"license": "AGPL-3.0-or-later",
|
|
49
49
|
"dependencies": {
|
|
50
|
-
"@adhdev/mesh-shared": "0.9.82-rc.
|
|
51
|
-
"@adhdev/session-host-core": "0.9.82-rc.
|
|
50
|
+
"@adhdev/mesh-shared": "0.9.82-rc.567",
|
|
51
|
+
"@adhdev/session-host-core": "0.9.82-rc.567",
|
|
52
52
|
"@agentclientprotocol/sdk": "^0.16.1",
|
|
53
53
|
"ajv": "^8.20.0",
|
|
54
54
|
"ajv-formats": "^3.0.1",
|
|
@@ -54,6 +54,7 @@ export interface CliAdapter {
|
|
|
54
54
|
clearHistory?(): void;
|
|
55
55
|
resolveAction?(data: unknown): Promise<void>;
|
|
56
56
|
resolveModal?(buttonIndex: number): void;
|
|
57
|
+
resolveModalMatched?(buttonIndex: number): boolean;
|
|
57
58
|
isApprovalRecentlyResolved?(): boolean;
|
|
58
59
|
setOnPtyData?(callback: (data: string) => void): void;
|
|
59
60
|
writeRaw?(data: string): void;
|
package/src/cli-adapter-types.ts
CHANGED
|
@@ -15,6 +15,16 @@ export interface CliAdapterStatus {
|
|
|
15
15
|
activeModal?: {
|
|
16
16
|
message: string;
|
|
17
17
|
buttons: string[];
|
|
18
|
+
/**
|
|
19
|
+
* BUTTON-INDEX-MISMAP (Fix C.1): each button's label paired with its real FSM
|
|
20
|
+
* DISPLAYED index (evaluator's Number(m[1])). `buttons` above is the label-only list
|
|
21
|
+
* every existing consumer reads (array position === pick order); `buttonMeta` preserves
|
|
22
|
+
* the index → label mapping so a partial / non-contiguous modal (display indices [1,3,4]
|
|
23
|
+
* at array positions [0,1,2]) does not lose its true indices once the modal leaves the
|
|
24
|
+
* adapter. Present only on spec/FSM adapters; absent for adapters that surface labels
|
|
25
|
+
* alone.
|
|
26
|
+
*/
|
|
27
|
+
buttonMeta?: { index: number; label: string }[];
|
|
18
28
|
/**
|
|
19
29
|
* Semantic modal class, when the adapter knows it (spec/FSM path):
|
|
20
30
|
* 'approval' = tool/command/trust consent (auto-approve may fire);
|
|
@@ -155,6 +165,13 @@ export interface CliAdapter {
|
|
|
155
165
|
resolveAction?(data: unknown): Promise<void>;
|
|
156
166
|
setInteractivePromptResponse?(response: InteractivePromptResponse): Promise<void>;
|
|
157
167
|
resolveModal?(buttonIndex: number): void;
|
|
168
|
+
// BUTTON-INDEX-MISMAP (Fix C.3): resolve a modal button by ARRAY POSITION and report
|
|
169
|
+
// whether a real button was matched (the FSM found a button for the mapped display index
|
|
170
|
+
// and dispatched its confirm keys). A `false` verdict means the requested position mapped
|
|
171
|
+
// to no button — the caller (mesh_approve) must then NOT report success. Optional so
|
|
172
|
+
// legacy adapters that only expose the void resolveModal keep working (the caller falls
|
|
173
|
+
// back to resolveModal + the resolution-cooldown check).
|
|
174
|
+
resolveModalMatched?(buttonIndex: number): boolean;
|
|
158
175
|
isApprovalRecentlyResolved?(): boolean;
|
|
159
176
|
// Raw PTY I/O (for terminal view)
|
|
160
177
|
setOnPtyData?(callback: (data: string) => void): void;
|
|
@@ -795,7 +795,21 @@ export async function handleResolveAction(h: CommandHelpers, args: any): Promise
|
|
|
795
795
|
LOG.info('Command', `[resolveAction] CLI PTY → stale_prompt (already resolved within cooldown)`);
|
|
796
796
|
return { success: true, stalePrompt: true, buttonIndex, button: buttons[buttonIndex] ?? button };
|
|
797
797
|
}
|
|
798
|
-
|
|
798
|
+
// BUTTON-INDEX-MISMAP (Fix C.3): prefer resolveModalMatched — it maps the array
|
|
799
|
+
// position to the real FSM display index AND reports whether a button was actually
|
|
800
|
+
// matched. The old path called the void resolveModal and unconditionally returned
|
|
801
|
+
// success:true, so a mis-mapped index (partial/non-contiguous modal) that pressed
|
|
802
|
+
// NOTHING still reported success and left the worker wedged at the modal. When the
|
|
803
|
+
// adapter tells us no button matched, report failure so mesh_approve / the coordinator
|
|
804
|
+
// sees the miss instead of a false success. Legacy adapters without resolveModalMatched
|
|
805
|
+
// keep the prior void-resolveModal behaviour.
|
|
806
|
+
if (typeof adapter.resolveModalMatched === 'function') {
|
|
807
|
+
const matched = adapter.resolveModalMatched(buttonIndex);
|
|
808
|
+
if (!matched) {
|
|
809
|
+
LOG.warn('Command', `[resolveAction] CLI PTY → no button matched for buttonIndex=${buttonIndex} "${buttons[buttonIndex] ?? '?'}" (modal not resolved)`);
|
|
810
|
+
return { success: false, error: 'Approval button index did not map to a visible modal button', buttonIndex, button: buttons[buttonIndex] ?? button };
|
|
811
|
+
}
|
|
812
|
+
} else if (typeof adapter.resolveModal === 'function') {
|
|
799
813
|
adapter.resolveModal(buttonIndex);
|
|
800
814
|
} else {
|
|
801
815
|
const keys = '\x1B[B'.repeat(Math.max(0, buttonIndex)) + '\r';
|
|
@@ -102,7 +102,7 @@ function elapsedSince(value: string | undefined, now: number): number {
|
|
|
102
102
|
return Number.isFinite(started) ? Math.max(0, now - started) : 0;
|
|
103
103
|
}
|
|
104
104
|
|
|
105
|
-
function sessionStatusFromNodes(nodes: any[] | undefined, nodeId?: string, sessionId?: string): { status?: MeshActiveWorkStatus; staleReason?: string } {
|
|
105
|
+
export function sessionStatusFromNodes(nodes: any[] | undefined, nodeId?: string, sessionId?: string): { status?: MeshActiveWorkStatus; staleReason?: string } {
|
|
106
106
|
if (!Array.isArray(nodes)) return {};
|
|
107
107
|
if (!nodeId) return { staleReason: 'direct task has no node id' };
|
|
108
108
|
const node = nodes.find(item => meshNodeIdMatches(item, nodeId));
|
|
@@ -365,12 +365,30 @@ export function buildMeshActiveWork(opts: BuildMeshActiveWorkOptions): { activeW
|
|
|
365
365
|
for (const task of opts.queue || []) {
|
|
366
366
|
if (task.status !== 'pending' && task.status !== 'assigned') continue;
|
|
367
367
|
const { title, summary } = summarizeMessage(task.message || '');
|
|
368
|
+
const queueNodeId = task.assignedNodeId || task.targetNodeId;
|
|
369
|
+
const queueSessionId = task.assignedSessionId || task.targetSessionId;
|
|
370
|
+
// APPROVAL-INBOX-BLINDSPOT (Fix A.2): a queue task previously reported its raw DB
|
|
371
|
+
// status (pending|assigned) verbatim, while direct dispatches consulted the live
|
|
372
|
+
// session status via sessionStatusFromNodes. That asymmetry meant a queue-dispatched
|
|
373
|
+
// worker sitting on an approval modal was recorded as 'assigned', so
|
|
374
|
+
// collectPendingApprovals (which filters status==='awaiting_approval') never counted
|
|
375
|
+
// it and mesh_list_pending_approvals returned 0. Overlay the live session status for
|
|
376
|
+
// an ASSIGNED queue task so an approval (or an active generation) on its bound session
|
|
377
|
+
// is reflected — the exact promotion the direct-dispatch path already does. A 'pending'
|
|
378
|
+
// task has no bound session yet, so it keeps its queue status.
|
|
379
|
+
const queueLive = task.status === 'assigned'
|
|
380
|
+
? sessionStatusFromNodes(opts.nodes, queueNodeId ?? undefined, queueSessionId ?? undefined)
|
|
381
|
+
: {};
|
|
382
|
+
const queueStatus: MeshActiveWorkStatus = queueLive.status === 'awaiting_approval'
|
|
383
|
+
|| queueLive.status === 'generating'
|
|
384
|
+
? queueLive.status
|
|
385
|
+
: task.status;
|
|
368
386
|
records.push({
|
|
369
387
|
taskId: task.id,
|
|
370
388
|
source: 'queue',
|
|
371
|
-
status:
|
|
372
|
-
nodeId:
|
|
373
|
-
sessionId:
|
|
389
|
+
status: queueStatus,
|
|
390
|
+
nodeId: queueNodeId,
|
|
391
|
+
sessionId: queueSessionId,
|
|
374
392
|
taskTitle: title,
|
|
375
393
|
taskSummary: summary,
|
|
376
394
|
message: task.message,
|
|
@@ -727,25 +727,39 @@ function evaluateMeshEventSuppression(
|
|
|
727
727
|
}
|
|
728
728
|
|
|
729
729
|
/**
|
|
730
|
-
*
|
|
731
|
-
*
|
|
732
|
-
*
|
|
733
|
-
*
|
|
734
|
-
*
|
|
735
|
-
*
|
|
736
|
-
* mid-MAGI-collect, HIJACKS the
|
|
737
|
-
* failure where a replica's repeated
|
|
738
|
-
*
|
|
739
|
-
*
|
|
740
|
-
*
|
|
741
|
-
*
|
|
730
|
+
* APPROVAL-INBOX-BLINDSPOT (Fix A): decide whether an agent:waiting_approval from an
|
|
731
|
+
* auto-approving worker can be SUPPRESSED (never forwarded to the coordinator).
|
|
732
|
+
*
|
|
733
|
+
* A MAGI/delegated worker launched with autoApprove:true resolves its own approval modals
|
|
734
|
+
* locally, so a forwarded agent:waiting_approval is transient noise: it injects a
|
|
735
|
+
* "[System] … is waiting for approval, use mesh_approve" turn the coordinator cannot usefully
|
|
736
|
+
* act on (the modal is already auto-resolving) and, mid-MAGI-collect, HIJACKS the
|
|
737
|
+
* coordinator's synthesis turn — the observed failure where a replica's repeated
|
|
738
|
+
* auto-approvals drowned out the completion events.
|
|
739
|
+
*
|
|
740
|
+
* BUT the OLD gate suppressed on the autoApprove *intent* alone (settings.autoApprove===true),
|
|
741
|
+
* which is the blind spot: if the worker's local resolveModal never actually fired/resolved
|
|
742
|
+
* (button-index mismatch, a modal auto-approve declined to answer, an unattended stall), the
|
|
743
|
+
* event was STILL dropped — so no task_approval_needed ledger row was created,
|
|
744
|
+
* mesh_list_pending_approvals stayed 0, the coordinator was never told, and the remote
|
|
745
|
+
* UNKNOWN-grace reclaim eventually tore the worker off its task. This tightens the gate:
|
|
746
|
+
* suppress ONLY when auto-approve is on AND we can positively confirm the modal was — or is
|
|
747
|
+
* being — resolved LOCALLY within the recent cooldown (approvalRecentlyResolvedLocally). If
|
|
748
|
+
* auto-approve is configured but has NOT actually resolved this modal, we FORWARD so the
|
|
749
|
+
* coordinator/inbox is told and can act.
|
|
742
750
|
*/
|
|
743
|
-
function
|
|
751
|
+
function shouldSuppressAutoApprovingWorkerApproval(components: DaemonComponents, sessionId: string): boolean {
|
|
744
752
|
if (!sessionId) return false;
|
|
745
753
|
try {
|
|
746
|
-
const
|
|
754
|
+
const instance = components.instanceManager?.getInstance?.(sessionId);
|
|
755
|
+
const state = instance?.getState?.();
|
|
747
756
|
const settings = (state?.settings as Record<string, unknown>) || {};
|
|
748
|
-
|
|
757
|
+
if (settings.autoApprove !== true) return false;
|
|
758
|
+
// Positive local-resolution signal required to suppress. Absent it, forward so the
|
|
759
|
+
// coordinator is told and a task_approval_needed ledger row is created.
|
|
760
|
+
const resolvedLocally = (instance as { approvalRecentlyResolvedLocally?: () => boolean } | undefined)
|
|
761
|
+
?.approvalRecentlyResolvedLocally;
|
|
762
|
+
return typeof resolvedLocally === 'function' ? resolvedLocally.call(instance) === true : false;
|
|
749
763
|
} catch {
|
|
750
764
|
return false;
|
|
751
765
|
}
|
|
@@ -1016,14 +1030,18 @@ function injectMeshSystemMessage(components: DaemonComponents, args: {
|
|
|
1016
1030
|
|
|
1017
1031
|
const eventTimestamp = readEventTimestamp(args.metadataEvent.timestamp);
|
|
1018
1032
|
|
|
1019
|
-
// Auto-approving worker:
|
|
1020
|
-
//
|
|
1021
|
-
// approval" injection
|
|
1022
|
-
// observed MAGI failure where a replica's repeated
|
|
1023
|
-
// coordinator and derailed its final synthesis.
|
|
1024
|
-
//
|
|
1025
|
-
|
|
1026
|
-
|
|
1033
|
+
// Auto-approving worker: suppress its approval prompt ONLY when the modal was — or is
|
|
1034
|
+
// being — resolved LOCALLY within the recent cooldown. The daemon resolving the modal
|
|
1035
|
+
// locally makes the "[System] … waiting for approval" injection pure noise that hijacks
|
|
1036
|
+
// the coordinator's turn (the observed MAGI failure where a replica's repeated
|
|
1037
|
+
// auto-approvals flooded the coordinator and derailed its final synthesis). But a worker
|
|
1038
|
+
// whose auto-approve is merely CONFIGURED on and did NOT actually resolve this modal
|
|
1039
|
+
// (button-index mismatch, an unattended stall, a modal auto-approve declined to answer)
|
|
1040
|
+
// must still forward — otherwise no task_approval_needed ledger row is created, the
|
|
1041
|
+
// coordinator/inbox is never told, and the remote UNKNOWN-grace reclaim eventually tears
|
|
1042
|
+
// the worker off its task (APPROVAL-INBOX-BLINDSPOT, Fix A).
|
|
1043
|
+
if (args.event === 'agent:waiting_approval' && shouldSuppressAutoApprovingWorkerApproval(components, eventSessionId)) {
|
|
1044
|
+
LOG.info('MeshEvents', `Suppressed agent:waiting_approval for auto-approving worker session ${eventSessionId || '(unknown)'} (mesh ${args.meshId}) — modal resolved locally within cooldown, coordinator not notified`);
|
|
1027
1045
|
traceMeshEventDrop('waiting_approval_auto_approving_worker', traceCtx);
|
|
1028
1046
|
return { success: true, forwarded: 0, suppressed: true, autoApprovingWorkerApproval: true };
|
|
1029
1047
|
}
|
|
@@ -81,6 +81,7 @@ import {
|
|
|
81
81
|
autoPruneStaleDirectDispatches,
|
|
82
82
|
pollAssignedTaskTerminalEvidence,
|
|
83
83
|
} from './mesh-completion-synthesis.js';
|
|
84
|
+
import { sessionStatusFromNodes } from './mesh-active-work.js';
|
|
84
85
|
|
|
85
86
|
// Re-export the extracted public API so existing importers (mesh-events.ts barrel;
|
|
86
87
|
// the reconcile-loop test suite) keep their `from './mesh-reconcile-loop.js'` paths.
|
|
@@ -726,6 +727,28 @@ export function __resetReclaimUnknownStreakForTests(): void {
|
|
|
726
727
|
deliveredUnconsumedUnknownStreak.clear();
|
|
727
728
|
}
|
|
728
729
|
|
|
730
|
+
// APPROVAL-INBOX-BLINDSPOT (Fix A.3): true when the assigned row's bound session is, per the
|
|
731
|
+
// LIVE mesh-node snapshots, sitting at an approval modal (waiting_approval). A REMOTE worker
|
|
732
|
+
// blocked on an approval reads UNKNOWN from resolveSessionBusyVerdict (it is not in THIS
|
|
733
|
+
// daemon's local instance map), so without this guard the delivered-no-turn / delivered-not-
|
|
734
|
+
// consumed UNKNOWN streak advances toward a false reclaim that tears the worker off a task it
|
|
735
|
+
// is legitimately paused on awaiting the coordinator's mesh_approve. The live status is read
|
|
736
|
+
// from the same node session snapshots mesh_status / the active-work builder use, so it is
|
|
737
|
+
// positive cross-daemon evidence (not a local-only observation). When present it HOLDS the row
|
|
738
|
+
// without accruing the streak; the reclaim resumes normally once the approval clears.
|
|
739
|
+
function assignedRowLiveStatusIsAwaitingApproval(
|
|
740
|
+
mesh: { nodes?: any[] },
|
|
741
|
+
nodeId?: string | null,
|
|
742
|
+
sessionId?: string | null,
|
|
743
|
+
): boolean {
|
|
744
|
+
if (!nodeId || !sessionId) return false;
|
|
745
|
+
try {
|
|
746
|
+
return sessionStatusFromNodes(mesh.nodes, nodeId, sessionId).status === 'awaiting_approval';
|
|
747
|
+
} catch {
|
|
748
|
+
return false;
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
|
|
729
752
|
// PHASE 2.5 — assigned-stranded dispatch watchdog (Bug B). claimNextTask atomically
|
|
730
753
|
// flips a row to 'assigned' BEFORE the fire-and-forget dispatch runs. If that dispatch
|
|
731
754
|
// neither rejects (→ no .catch requeue) nor is confirmed delivered — a relay that hangs
|
|
@@ -809,6 +832,20 @@ async function recoverStrandedAssignedDispatches(
|
|
|
809
832
|
} else {
|
|
810
833
|
if (verdict === 'IDLE_CONFIRMED') {
|
|
811
834
|
deliveredUnconsumedUnknownStreak.delete(shortStreakKey);
|
|
835
|
+
} else if (assignedRowLiveStatusIsAwaitingApproval(mesh, row.assignedNodeId, row.assignedSessionId)) {
|
|
836
|
+
// APPROVAL-INBOX-BLINDSPOT (Fix A.3): the UNKNOWN (remote) worker is live and
|
|
837
|
+
// sitting at an approval modal — it is legitimately paused awaiting the
|
|
838
|
+
// coordinator's mesh_approve, NOT a lost delivery. HOLD without advancing the
|
|
839
|
+
// streak so a genuine approval-blocked worker is never re-driven out from
|
|
840
|
+
// under its pending approval.
|
|
841
|
+
traceMeshEventDrop('short_redrive_deferred_awaiting_approval', {
|
|
842
|
+
taskId: row.id,
|
|
843
|
+
sessionId: row.assignedSessionId,
|
|
844
|
+
nodeId: row.assignedNodeId,
|
|
845
|
+
meshId,
|
|
846
|
+
event: 'agent:waiting_approval',
|
|
847
|
+
}, 'live_awaiting_approval');
|
|
848
|
+
continue;
|
|
812
849
|
} else {
|
|
813
850
|
const streak = (deliveredUnconsumedUnknownStreak.get(shortStreakKey) ?? 0) + 1;
|
|
814
851
|
deliveredUnconsumedUnknownStreak.set(shortStreakKey, streak);
|
|
@@ -899,6 +936,21 @@ async function recoverStrandedAssignedDispatches(
|
|
|
899
936
|
if (verdict === 'IDLE_CONFIRMED') {
|
|
900
937
|
deliveredNoTurnUnknownStreak.delete(streakKey);
|
|
901
938
|
reclaimReason = 'delivered_no_turn_deadline';
|
|
939
|
+
} else if (assignedRowLiveStatusIsAwaitingApproval(mesh, row.assignedNodeId, row.assignedSessionId)) {
|
|
940
|
+
// APPROVAL-INBOX-BLINDSPOT (Fix A.3): the UNKNOWN (remote) worker is live and
|
|
941
|
+
// sitting at an approval modal — legitimately paused awaiting the coordinator's
|
|
942
|
+
// mesh_approve, NOT a delivered-but-lost completion. HOLD without advancing the
|
|
943
|
+
// streak so a genuine approval-blocked worker is never reclaimed at the
|
|
944
|
+
// delivered-no-turn deadline. The reclaim resumes normally once the approval
|
|
945
|
+
// clears (the live status leaves waiting_approval).
|
|
946
|
+
traceMeshEventDrop('reclaim_deferred_awaiting_approval', {
|
|
947
|
+
taskId: row.id,
|
|
948
|
+
sessionId: row.assignedSessionId,
|
|
949
|
+
nodeId: row.assignedNodeId,
|
|
950
|
+
meshId,
|
|
951
|
+
event: 'agent:waiting_approval',
|
|
952
|
+
}, 'live_awaiting_approval');
|
|
953
|
+
continue;
|
|
902
954
|
} else {
|
|
903
955
|
// UNKNOWN — defer and accumulate the consecutive-UNKNOWN streak.
|
|
904
956
|
const streak = (deliveredNoTurnUnknownStreak.get(streakKey) ?? 0) + 1;
|
|
@@ -109,6 +109,16 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
109
109
|
*/
|
|
110
110
|
private static readonly AUTO_APPROVE_SETTLE_MS = 600;
|
|
111
111
|
|
|
112
|
+
/**
|
|
113
|
+
* APPROVAL-INBOX-BLINDSPOT (Fix A): how long after a LOCAL auto-approve fire the mesh
|
|
114
|
+
* event forwarder still treats the modal as "being resolved locally" and suppresses the
|
|
115
|
+
* coordinator notification. Chosen to comfortably cover the resolveModal → PTY absorb →
|
|
116
|
+
* status-leaves-approval round trip (incl. the win32 CR-resend loop) while staying short
|
|
117
|
+
* enough that a modal which auto-approve fired at but did NOT resolve re-surfaces to the
|
|
118
|
+
* coordinator on the next event. Aligned with the adapter's own approval cooldown scale.
|
|
119
|
+
*/
|
|
120
|
+
private static readonly APPROVAL_LOCAL_RESOLUTION_COOLDOWN_MS = 8000;
|
|
121
|
+
|
|
112
122
|
/**
|
|
113
123
|
* Busy-side hysteresis for the settle gate. A momentary `generating` flip
|
|
114
124
|
* while the SAME approval modal's button block is still on screen (its
|
|
@@ -387,6 +397,16 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
387
397
|
// signature) while a genuinely closed modal — buttons empty continuously past
|
|
388
398
|
// the continuity window — is still recognised and resets the gate.
|
|
389
399
|
private autoApproveLastModalSeenAt = 0;
|
|
400
|
+
// APPROVAL-INBOX-BLINDSPOT (Fix A): wall-clock of the last time this session actually
|
|
401
|
+
// FIRED a local auto-approve resolveModal (the settle gate passed → resolveModal
|
|
402
|
+
// dispatched). The mesh event forwarder keys its agent:waiting_approval suppression on
|
|
403
|
+
// this + a cooldown so it only drops the coordinator notification when we can positively
|
|
404
|
+
// confirm the modal was (or is being) resolved LOCALLY. If auto-approve is merely
|
|
405
|
+
// *configured* on but has NOT recently fired for this modal, the raw waiting_approval is
|
|
406
|
+
// forwarded so a task_approval_needed ledger row is created and the coordinator/inbox is
|
|
407
|
+
// told — closing the blind spot where a never-resolving worker approval was silently
|
|
408
|
+
// dropped just because settings.autoApprove===true.
|
|
409
|
+
private lastAutoApproveFiredAt = 0;
|
|
390
410
|
// AUTOAPPROVE-FLAP-INBOX-MISSING sticky-approval overlay (see APPROVAL_STICKY_FLAP_MS).
|
|
391
411
|
// The wall-clock of the last frame where the RAW adapter reported waiting_approval with
|
|
392
412
|
// a CONCRETE modal (buttons present), the cached modal to re-present across a busy blip,
|
|
@@ -1099,6 +1119,34 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
1099
1119
|
return null;
|
|
1100
1120
|
}
|
|
1101
1121
|
|
|
1122
|
+
/**
|
|
1123
|
+
* APPROVAL-INBOX-BLINDSPOT (Fix A): true when this session's approval modal was — or is
|
|
1124
|
+
* being — resolved LOCALLY within the recent cooldown. Two independent positive signals:
|
|
1125
|
+
* (1) auto-approve fired its resolveModal within APPROVAL_LOCAL_RESOLUTION_COOLDOWN_MS
|
|
1126
|
+
* (lastAutoApproveFiredAt), or
|
|
1127
|
+
* (2) the underlying adapter reports isApprovalRecentlyResolved() — its own resolve
|
|
1128
|
+
* cooldown, which also covers a dashboard / mesh_approve resolution.
|
|
1129
|
+
* The mesh event forwarder uses this to decide whether an agent:waiting_approval from an
|
|
1130
|
+
* auto-approving worker can be safely SUPPRESSED (a local resolution is in flight) or must
|
|
1131
|
+
* be FORWARDED (auto-approve is configured but has NOT actually resolved this modal, so the
|
|
1132
|
+
* coordinator/inbox must be told). Keying suppression on real resolution — not just the
|
|
1133
|
+
* autoApprove *intent* — is the blind-spot fix: a never-resolving worker approval is no
|
|
1134
|
+
* longer silently dropped.
|
|
1135
|
+
*/
|
|
1136
|
+
approvalRecentlyResolvedLocally(now = Date.now()): boolean {
|
|
1137
|
+
if (this.lastAutoApproveFiredAt
|
|
1138
|
+
&& now - this.lastAutoApproveFiredAt < CliProviderInstance.APPROVAL_LOCAL_RESOLUTION_COOLDOWN_MS) {
|
|
1139
|
+
return true;
|
|
1140
|
+
}
|
|
1141
|
+
try {
|
|
1142
|
+
const adapter = this.adapter as { isApprovalRecentlyResolved?: () => boolean };
|
|
1143
|
+
if (typeof adapter.isApprovalRecentlyResolved === 'function') {
|
|
1144
|
+
return adapter.isApprovalRecentlyResolved() === true;
|
|
1145
|
+
}
|
|
1146
|
+
} catch { /* adapter gone / transient */ }
|
|
1147
|
+
return false;
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1102
1150
|
/**
|
|
1103
1151
|
* NOTIF-HELD-DRAIN: true when this `waiting_approval` is a routine, transient tool-consent
|
|
1104
1152
|
* of an autonomously-progressing mesh session rather than a genuine human-await modal —
|
|
@@ -3139,8 +3187,34 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
3139
3187
|
this.lastAutoApprovalSignature = '';
|
|
3140
3188
|
}, 5000);
|
|
3141
3189
|
this.recordAutoApproval(modal?.message, buttonLabel, now);
|
|
3190
|
+
// APPROVAL-INBOX-BLINDSPOT (Fix A) + BUTTON-INDEX-MISMAP (Fix C): stamp the
|
|
3191
|
+
// local-resolution clock so the mesh forwarder can distinguish "auto-approve just
|
|
3192
|
+
// fired / is firing" (suppress the coordinator notification — modal is being resolved
|
|
3193
|
+
// locally) from "auto-approve is merely configured but has not fired for this modal"
|
|
3194
|
+
// (forward it so the coordinator is told and a task_approval_needed ledger row is
|
|
3195
|
+
// created). Only stamp when the click actually MATCHED a button: resolveModalMatched
|
|
3196
|
+
// maps the array position to the real FSM display index and reports whether a button
|
|
3197
|
+
// was pressed, so a mis-mapped/never-pressed modal does NOT falsely mark itself
|
|
3198
|
+
// locally-resolved (which would suppress the coordinator notification for a modal that
|
|
3199
|
+
// never got answered — the exact blind spot). Legacy adapters keep the void resolveModal.
|
|
3200
|
+
this.lastAutoApproveFiredAt = now;
|
|
3142
3201
|
setTimeout(() => {
|
|
3143
|
-
this.adapter
|
|
3202
|
+
const adapter = this.adapter as {
|
|
3203
|
+
resolveModalMatched?: (i: number) => boolean;
|
|
3204
|
+
resolveModal?: (i: number) => void;
|
|
3205
|
+
};
|
|
3206
|
+
if (typeof adapter.resolveModalMatched === 'function') {
|
|
3207
|
+
const matched = adapter.resolveModalMatched(buttonIndex);
|
|
3208
|
+
if (!matched) {
|
|
3209
|
+
// Click did not land on any button — undo the local-resolution stamp so the
|
|
3210
|
+
// next agent:waiting_approval is FORWARDED to the coordinator/inbox rather
|
|
3211
|
+
// than suppressed as "resolved locally".
|
|
3212
|
+
if (this.lastAutoApproveFiredAt === now) this.lastAutoApproveFiredAt = 0;
|
|
3213
|
+
LOG.warn('CLI', `[${this.type}] auto-approve resolveModal matched no button (index ${buttonIndex}) — surfacing approval to coordinator`);
|
|
3214
|
+
}
|
|
3215
|
+
} else {
|
|
3216
|
+
adapter.resolveModal?.(buttonIndex);
|
|
3217
|
+
}
|
|
3144
3218
|
}, 0);
|
|
3145
3219
|
return autoApproveActive;
|
|
3146
3220
|
}
|
|
@@ -204,8 +204,21 @@ export class SpecCliAdapter implements CliAdapter {
|
|
|
204
204
|
// modal this frame still stays waiting_approval (no activeModal yet).
|
|
205
205
|
// `kind` carries the semantic modal class through to the auto-approve
|
|
206
206
|
// gate so a /model picker (kind='picker') is never auto-answered.
|
|
207
|
+
// BUTTON-INDEX-MISMAP (Fix C.1): keep `buttons` as the label list every
|
|
208
|
+
// existing consumer (pickApprovalButton, mesh_approve, auto-approve) reads,
|
|
209
|
+
// but ALSO surface `buttonMeta` carrying each button's real FSM display index
|
|
210
|
+
// alongside its label. A partial/non-contiguous modal (display indices [1,3,4]
|
|
211
|
+
// at array positions [0,1,2]) then no longer loses the index → label mapping
|
|
212
|
+
// once it leaves the adapter: a consumer that has an array position can recover
|
|
213
|
+
// the true FSM index without re-parsing. resolveModal() below relies on the same
|
|
214
|
+
// ordered list to translate an array position to the correct FSM index.
|
|
207
215
|
activeModal: modal
|
|
208
|
-
? {
|
|
216
|
+
? {
|
|
217
|
+
message: modal.title ?? state.label,
|
|
218
|
+
buttons: modal.buttons.map(b => b.label),
|
|
219
|
+
buttonMeta: modal.buttons.map(b => ({ index: b.index, label: b.label })),
|
|
220
|
+
kind: modal.kind ?? null,
|
|
221
|
+
}
|
|
209
222
|
: null,
|
|
210
223
|
activeInteractivePrompt: this.activeInteractivePrompt,
|
|
211
224
|
...sessionFields,
|
|
@@ -420,8 +433,29 @@ export class SpecCliAdapter implements CliAdapter {
|
|
|
420
433
|
}
|
|
421
434
|
|
|
422
435
|
resolveModal(buttonIndex: number): void {
|
|
423
|
-
|
|
424
|
-
|
|
436
|
+
this.resolveModalMatched(buttonIndex);
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
resolveModalMatched(buttonIndex: number): boolean {
|
|
440
|
+
// BUTTON-INDEX-MISMAP (Fix C): `buttonIndex` is an ARRAY POSITION into the
|
|
441
|
+
// label list this adapter surfaced via getStatus().activeModal.buttons (the
|
|
442
|
+
// same order pickApprovalButton / mesh_approve pick from). The FSM matches a
|
|
443
|
+
// click by the button's DISPLAYED number (evaluator sets button.index =
|
|
444
|
+
// Number(m[1])), which is NOT `arrayPos + 1` for a partial / non-contiguous
|
|
445
|
+
// modal — e.g. a "1. Yes / 3. Always / 4. No" set parses to display indices
|
|
446
|
+
// [1,3,4] at array positions [0,1,2]. Blindly sending `arrayPos + 1` then
|
|
447
|
+
// targets a non-existent display index (2) and handleClickModalButton finds
|
|
448
|
+
// no button → nothing is pressed. Look up the real FSM display index from the
|
|
449
|
+
// same ordered button list instead, and fall back to the legacy +1 only when
|
|
450
|
+
// no modal is captured (defensive; the driver's own guard rejects a miss).
|
|
451
|
+
const buttons = this.latestModal?.buttons ?? [];
|
|
452
|
+
const target = (buttonIndex >= 0 && buttonIndex < buttons.length)
|
|
453
|
+
? buttons[buttonIndex].index
|
|
454
|
+
: buttonIndex + 1;
|
|
455
|
+
// clickModalButton returns whether the FSM actually found a button for `target`
|
|
456
|
+
// and dispatched its confirm keys — surfaced so mesh_approve can distinguish a
|
|
457
|
+
// real press from a silent miss (the exact false-success the mis-map produced).
|
|
458
|
+
return this.driver.clickModalButton(target);
|
|
425
459
|
}
|
|
426
460
|
|
|
427
461
|
async resolveAction(data: unknown): Promise<void> {
|
|
@@ -118,6 +118,13 @@ export interface ISpecDriver {
|
|
|
118
118
|
subscribe(listener: (ev: DashboardEvent) => void): () => void;
|
|
119
119
|
start(): void;
|
|
120
120
|
dispatch(cmd: DashboardCommand): void;
|
|
121
|
+
/**
|
|
122
|
+
* BUTTON-INDEX-MISMAP (Fix C.3): click a modal button by its FSM display index and report
|
|
123
|
+
* whether a button was actually matched and its confirm keys dispatched. Unlike the
|
|
124
|
+
* fire-and-forget `dispatch('click_modal_button')`, callers that need to know the click
|
|
125
|
+
* landed (mesh_approve → SpecCliAdapter.resolveModalMatched) can observe a miss.
|
|
126
|
+
*/
|
|
127
|
+
clickModalButton(index: number): boolean;
|
|
121
128
|
updateMeta(meta: Record<string, unknown>, replace?: boolean): void;
|
|
122
129
|
snapshot(): string;
|
|
123
130
|
getCursorPosition(): { row: number; col: number };
|
|
@@ -428,7 +435,7 @@ export class FsmDriver implements ISpecDriver {
|
|
|
428
435
|
case 'send_message': this.handleSendMessage(cmd.text); return;
|
|
429
436
|
case 'pty_write': this.adapter.send_keys(cmd.data); return;
|
|
430
437
|
case 'click_control': this.handleClickControl(cmd.control_id, cmd.payload); return;
|
|
431
|
-
case 'click_modal_button': this.
|
|
438
|
+
case 'click_modal_button': this.clickModalButton(cmd.index); return;
|
|
432
439
|
case 'attach_image': this.handleAttachImage(cmd.blob, cmd.mime); return;
|
|
433
440
|
case 'resize': this.adapter.resize(cmd.cols, cmd.rows); return;
|
|
434
441
|
case 'cancel': this.adapter.send_keys('\x03'); return;
|
|
@@ -1318,11 +1325,25 @@ export class FsmDriver implements ISpecDriver {
|
|
|
1318
1325
|
}
|
|
1319
1326
|
}
|
|
1320
1327
|
|
|
1321
|
-
|
|
1328
|
+
/**
|
|
1329
|
+
* BUTTON-INDEX-MISMAP (Fix C.3): public modal-click entry that returns whether a button
|
|
1330
|
+
* matching the requested FSM display index was actually found and its confirm keys were
|
|
1331
|
+
* dispatched. The old private handleClickModalButton silently `return`ed on a miss (no
|
|
1332
|
+
* modal captured, or no button whose `.index` equals the requested display index), so a
|
|
1333
|
+
* mis-mapped index looked identical to a successful press. Callers that need to know
|
|
1334
|
+
* whether the click landed (mesh_approve → resolveModal) can now observe the miss instead
|
|
1335
|
+
* of reporting success into the void. The generic `dispatch('click_modal_button')` path
|
|
1336
|
+
* keeps ignoring the return (fire-and-forget UI clicks).
|
|
1337
|
+
*/
|
|
1338
|
+
clickModalButton(index: number): boolean {
|
|
1339
|
+
return this.handleClickModalButton(index);
|
|
1340
|
+
}
|
|
1341
|
+
|
|
1342
|
+
private handleClickModalButton(index: number): boolean {
|
|
1322
1343
|
const m = this.currentEval?.modal;
|
|
1323
|
-
if (!m) return;
|
|
1344
|
+
if (!m) return false;
|
|
1324
1345
|
const btn = m.buttons.find(b => b.index === index);
|
|
1325
|
-
if (!btn) return;
|
|
1346
|
+
if (!btn) return false;
|
|
1326
1347
|
|
|
1327
1348
|
const rule = stateById(this.spec, this.currentStateId)?.extract?.buttons;
|
|
1328
1349
|
if (rule?.select_mode === 'arrow_keys') {
|
|
@@ -1343,9 +1364,10 @@ export class FsmDriver implements ISpecDriver {
|
|
|
1343
1364
|
const confirm = (rule.key_for_index || '\r').replace(/\{index\}/g, '') || '\r';
|
|
1344
1365
|
if (nav) this.adapter.send_keys(nav);
|
|
1345
1366
|
this.submitModalConfirm(confirm);
|
|
1346
|
-
return;
|
|
1367
|
+
return true;
|
|
1347
1368
|
}
|
|
1348
1369
|
this.submitModalConfirm(btn.key);
|
|
1370
|
+
return true;
|
|
1349
1371
|
}
|
|
1350
1372
|
|
|
1351
1373
|
/**
|