@adhdev/daemon-core 0.9.82-rc.566 → 0.9.82-rc.568
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 +194 -32
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +194 -32
- 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 +27 -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 +133 -14
- 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 —
|
|
@@ -427,6 +452,7 @@ export declare class CliProviderInstance implements ProviderInstance {
|
|
|
427
452
|
* the dashboard tail-repair cache — a display value, not a completion decision.
|
|
428
453
|
*/
|
|
429
454
|
private lastVisibleAssistantSummary;
|
|
455
|
+
private lastVisibleAssistantSummaryDetail;
|
|
430
456
|
/**
|
|
431
457
|
* NOTIF Defect-B: the final assistant summary this instance ALREADY parsed and
|
|
432
458
|
* cached for the current turn (lastCompletionSummary), if any. The evidence
|
|
@@ -442,6 +468,7 @@ export declare class CliProviderInstance implements ProviderInstance {
|
|
|
442
468
|
* next turn (see lastCompletionSummary = null on onTurnStarted).
|
|
443
469
|
*/
|
|
444
470
|
private cachedCompletionSummaryContent;
|
|
471
|
+
private cachedInTurnCompletionSummaryContent;
|
|
445
472
|
private completionFinalAssistantEvidence;
|
|
446
473
|
private completionFinalSummary;
|
|
447
474
|
private buildCompletedFinalizationDiagnostic;
|
|
@@ -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.568",
|
|
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.568",
|
|
51
|
+
"@adhdev/session-host-core": "0.9.82-rc.568",
|
|
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;
|