@adhdev/daemon-core 0.9.82-rc.490 → 0.9.82-rc.492
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 +26 -0
- package/dist/cli-adapters/provider-cli-adapter.d.ts +2 -0
- package/dist/config/mesh-config.d.ts +1 -0
- package/dist/index.js +161 -11
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +161 -11
- package/dist/index.mjs.map +1 -1
- package/dist/providers/cli-provider-instance.d.ts +35 -0
- package/package.json +3 -3
- package/src/cli-adapters/cli-state-engine.ts +97 -0
- package/src/cli-adapters/provider-cli-adapter.ts +11 -0
- package/src/commands/med-family/mesh-crud.ts +49 -8
- package/src/config/mesh-config.ts +1 -1
- package/src/providers/cli-provider-instance.ts +95 -2
|
@@ -114,6 +114,28 @@ export declare class CliProviderInstance implements ProviderInstance {
|
|
|
114
114
|
* the nudge was NOT deferred) and leaked to the coordinator.
|
|
115
115
|
*/
|
|
116
116
|
private static readonly AUTO_APPROVE_MASK_STALL_MS;
|
|
117
|
+
/**
|
|
118
|
+
* FALSE-IDLE (inter-approval quiet valley): grace window after an auto-approve
|
|
119
|
+
* (or mesh_approve) RESOLVES a modal during which a subsequent generating→idle
|
|
120
|
+
* quiet valley must NOT be treated as turn completion.
|
|
121
|
+
*
|
|
122
|
+
* The RCA: auto-approve resolves a modal → the agent resumes the same turn →
|
|
123
|
+
* between resolving that approval and preparing the next tool/approval the agent
|
|
124
|
+
* falls briefly silent. The FSM sees idle + a recorded mid-turn assistant bubble
|
|
125
|
+
* and fires an early agent:generating_completed even though the turn is still in
|
|
126
|
+
* flight. Live evidence showed the same session resuming waiting_approval ~13s
|
|
127
|
+
* after a "clean" completion emit.
|
|
128
|
+
*
|
|
129
|
+
* The window must be comfortably larger than the observed resume gap (~13s) so
|
|
130
|
+
* the valley is bridged, but not so large that a turn that genuinely ended right
|
|
131
|
+
* after an approval is held for an annoying stretch. 18s clears 13s with margin
|
|
132
|
+
* while capping the worst-case extra hold on a truly-finished turn at 18s (still
|
|
133
|
+
* well under COMPLETED_FINALIZATION_MAX_WAIT_MS's 30s hard bound). The recency is
|
|
134
|
+
* measured from the engine's lastApprovalResolvedAt, which is stamped ONLY by
|
|
135
|
+
* resolveModal (auto-approve fire / dashboard / mesh_approve) — so a plain turn
|
|
136
|
+
* with no approval never carries recency and is never held (no regression).
|
|
137
|
+
*/
|
|
138
|
+
private static readonly APPROVAL_RESUME_GRACE_MS;
|
|
117
139
|
private adapter;
|
|
118
140
|
private context;
|
|
119
141
|
private events;
|
|
@@ -395,6 +417,19 @@ export declare class CliProviderInstance implements ProviderInstance {
|
|
|
395
417
|
*/
|
|
396
418
|
private approvableModalSignature;
|
|
397
419
|
private isAutonomousMeshSession;
|
|
420
|
+
/**
|
|
421
|
+
* FALSE-IDLE: are we inside the post-approval resume grace window? True when this
|
|
422
|
+
* is an autonomous auto-approving mesh session AND the engine resolved a modal
|
|
423
|
+
* (auto-approve / mesh_approve) within APPROVAL_RESUME_GRACE_MS. This is the single
|
|
424
|
+
* "auto-approve recency" judgment shared by Fix 1 (the SETTLE-VALLEY completion
|
|
425
|
+
* hold below) and Fix 2 (the FSM-level applyIdle hysteresis in cli-state-engine).
|
|
426
|
+
*
|
|
427
|
+
* Scoped to autonomous auto-approving sessions so a foreground/attended session,
|
|
428
|
+
* or a session with auto-approve off (whose approvals a human answers), is never
|
|
429
|
+
* held. The recency clock (adapter.lastApprovalResolvedAt) is 0 until the first
|
|
430
|
+
* resolveModal, so a plain turn that never saw an approval always returns false.
|
|
431
|
+
*/
|
|
432
|
+
private inApprovalResumeGrace;
|
|
398
433
|
/**
|
|
399
434
|
* ARCH-REFACTOR R1: the taskId to attribute the CURRENTLY-completing turn to.
|
|
400
435
|
* Prefers the per-turn binding (engine.currentTurnTaskId, set when the turn was
|
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.492",
|
|
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.492",
|
|
51
|
+
"@adhdev/session-host-core": "0.9.82-rc.492",
|
|
52
52
|
"@agentclientprotocol/sdk": "^0.16.1",
|
|
53
53
|
"ajv": "^8.20.0",
|
|
54
54
|
"ajv-formats": "^3.0.1",
|
|
@@ -66,6 +66,16 @@ export interface CliStateEngineCallbacks {
|
|
|
66
66
|
onStatusChange(): void;
|
|
67
67
|
onApplyParsedSession(session: ParsedSession): void;
|
|
68
68
|
onTurnCompleted(): void;
|
|
69
|
+
/**
|
|
70
|
+
* FALSE-IDLE (Fix 2): true when the owning session is inside the post-approval
|
|
71
|
+
* resume grace — an autonomous auto-approving mesh session that resolved a modal
|
|
72
|
+
* within the resume-grace window. The engine cannot decide this on its own (it has
|
|
73
|
+
* no mesh/auto-approve awareness); the instance answers using the SAME
|
|
74
|
+
* `inApprovalResumeGrace` judgment Fix 1 uses, so the two fixes stay in lockstep.
|
|
75
|
+
* Optional: absent ⇒ treated as false (no hysteresis) so any non-instance embedder
|
|
76
|
+
* keeps the pre-fix behavior.
|
|
77
|
+
*/
|
|
78
|
+
isInApprovalResumeGrace?(): boolean;
|
|
69
79
|
}
|
|
70
80
|
|
|
71
81
|
interface IdleFinishCandidate {
|
|
@@ -94,6 +104,13 @@ const FINISH_RETRY_DELAY_MS = 300;
|
|
|
94
104
|
const MAX_TRACE_ENTRIES = 250;
|
|
95
105
|
const APPROVAL_EXIT_TIMEOUT_MS = 60_000;
|
|
96
106
|
const IDLE_CONFIRMATION_GRACE_MS = 2_000;
|
|
107
|
+
// FALSE-IDLE (Fix 2): hard upper bound on how long applyIdle may keep deferring
|
|
108
|
+
// finishResponse inside the post-approval resume grace. Mirrors the instance-side
|
|
109
|
+
// APPROVAL_RESUME_GRACE_MS (18s) — kept as a local constant since the engine has no
|
|
110
|
+
// access to the instance's static. If the turn truly ended right after an approval
|
|
111
|
+
// and stays silent this long, we stop suppressing and let the normal idle-finish run,
|
|
112
|
+
// so a genuinely-finished turn can never be held past this bound (no infinite defer).
|
|
113
|
+
const APPROVAL_RESUME_IDLE_DEFER_CAP_MS = 18_000;
|
|
97
114
|
|
|
98
115
|
// ─── Engine ────────────────────────────────────────────────────────────────
|
|
99
116
|
|
|
@@ -185,6 +202,13 @@ export class CliStateEngine {
|
|
|
185
202
|
// ── Idle candidate ───────────────────────────────
|
|
186
203
|
private idleFinishCandidate: IdleFinishCandidate | null = null;
|
|
187
204
|
|
|
205
|
+
// FALSE-IDLE (Fix 2): wall-clock when the current post-approval resume-grace idle
|
|
206
|
+
// defer began, scoped to a responseEpoch. Zero when not deferring. Bounds the defer
|
|
207
|
+
// to APPROVAL_RESUME_IDLE_DEFER_CAP_MS so a turn that genuinely ended right after an
|
|
208
|
+
// approval eventually finishes. Reset whenever a new turn/response starts or completes.
|
|
209
|
+
private approvalResumeDeferSince = 0;
|
|
210
|
+
private approvalResumeDeferEpoch = -1;
|
|
211
|
+
|
|
188
212
|
// ── Idle confirmation grace ──────────────────────
|
|
189
213
|
/**
|
|
190
214
|
* `finishResponse` produces the `generating → idle` transition that
|
|
@@ -450,6 +474,8 @@ export class CliStateEngine {
|
|
|
450
474
|
this.activeModal = null;
|
|
451
475
|
this.pendingScriptStatus = null;
|
|
452
476
|
this.pendingScriptStatusSince = 0;
|
|
477
|
+
this.approvalResumeDeferSince = 0;
|
|
478
|
+
this.approvalResumeDeferEpoch = -1;
|
|
453
479
|
}
|
|
454
480
|
|
|
455
481
|
clearIdleFinishCandidate(reason: string): void {
|
|
@@ -959,6 +985,28 @@ export class CliStateEngine {
|
|
|
959
985
|
&& assistantLength >= candidate.assistantLength
|
|
960
986
|
&& (now - candidate.armedAt) >= idleFinishConfirmMs;
|
|
961
987
|
|
|
988
|
+
// FALSE-IDLE (Fix 2) FSM-level hysteresis: an autonomous auto-approving mesh
|
|
989
|
+
// worker that just auto-resolved an approval resumes the same turn and falls
|
|
990
|
+
// briefly silent (the inter-approval quiet valley) before the next tool/approval.
|
|
991
|
+
// resetActiveTurnState tearing the turn down in that valley is the root cause the
|
|
992
|
+
// downstream Fix 1 / hasAdapterPendingResponse gates then inherit as "turn closed".
|
|
993
|
+
// Suppress the idle declaration here — at the FSM level — while inside the
|
|
994
|
+
// post-approval resume grace, so the turn scope is NOT torn down mid-flight.
|
|
995
|
+
// Bounded by APPROVAL_RESUME_IDLE_DEFER_CAP_MS: if the turn genuinely ended right
|
|
996
|
+
// after an approval and stays silent past the cap, we stop deferring and let the
|
|
997
|
+
// normal idle-finish run. Re-arm the idle timeout so we re-evaluate after the
|
|
998
|
+
// quiet grows (either the worker resumes → this path releases, or the cap lapses
|
|
999
|
+
// → finish). Scoped through the instance callback so a plain/interactive turn with
|
|
1000
|
+
// no autonomous auto-approve is never affected.
|
|
1001
|
+
if (this.shouldDeferIdleForApprovalResume(now)) {
|
|
1002
|
+
this.clearIdleFinishCandidate('approval_resume_grace_defer');
|
|
1003
|
+
if (this.idleTimeout) clearTimeout(this.idleTimeout);
|
|
1004
|
+
this.idleTimeout = setTimeout(() => {
|
|
1005
|
+
if (this.isWaitingForResponse) this.evaluateSettled(this.transport.getSnapshot());
|
|
1006
|
+
}, this.timeouts.idleFinish);
|
|
1007
|
+
return;
|
|
1008
|
+
}
|
|
1009
|
+
|
|
962
1010
|
if (idleReady && candidateQuiet) {
|
|
963
1011
|
this.clearIdleFinishCandidate('finish_response');
|
|
964
1012
|
if (this.idleTimeout) clearTimeout(this.idleTimeout);
|
|
@@ -975,6 +1023,12 @@ export class CliStateEngine {
|
|
|
975
1023
|
if (this.idleTimeout) clearTimeout(this.idleTimeout);
|
|
976
1024
|
this.idleTimeout = setTimeout(() => {
|
|
977
1025
|
if (this.isWaitingForResponse && !this.hasActionableApproval()) {
|
|
1026
|
+
if (this.shouldDeferIdleForApprovalResume(Date.now())) {
|
|
1027
|
+
this.idleTimeout = setTimeout(() => {
|
|
1028
|
+
if (this.isWaitingForResponse) this.evaluateSettled(this.transport.getSnapshot());
|
|
1029
|
+
}, this.timeouts.idleFinish);
|
|
1030
|
+
return;
|
|
1031
|
+
}
|
|
978
1032
|
if (this.shouldDeferIdleTimeoutFinish()) return;
|
|
979
1033
|
const parsed = this.runParseSession(this.transport.getSnapshot());
|
|
980
1034
|
if (this.shouldDeferFinishForTranscript(parsed)) {
|
|
@@ -987,6 +1041,49 @@ export class CliStateEngine {
|
|
|
987
1041
|
}, this.timeouts.idleFinish);
|
|
988
1042
|
}
|
|
989
1043
|
|
|
1044
|
+
/**
|
|
1045
|
+
* FALSE-IDLE (Fix 2): should applyIdle suppress the idle/finish for the current
|
|
1046
|
+
* turn because we are inside the post-approval resume grace?
|
|
1047
|
+
*
|
|
1048
|
+
* True only when: (a) the owning instance reports it is an autonomous auto-approving
|
|
1049
|
+
* mesh session that resolved a modal within the resume-grace window
|
|
1050
|
+
* (isInApprovalResumeGrace callback — the SAME judgment Fix 1 uses), AND (b) the defer
|
|
1051
|
+
* for THIS response epoch has not yet exceeded APPROVAL_RESUME_IDLE_DEFER_CAP_MS.
|
|
1052
|
+
* The cap guarantees no infinite defer: a turn that genuinely ended right after an
|
|
1053
|
+
* approval and stays silent past the cap stops being suppressed and finishes normally.
|
|
1054
|
+
* The per-epoch anchor means a fresh turn/response restarts the clock.
|
|
1055
|
+
*/
|
|
1056
|
+
private shouldDeferIdleForApprovalResume(now: number): boolean {
|
|
1057
|
+
if (typeof this.callbacks.isInApprovalResumeGrace !== 'function') { this.clearApprovalResumeDefer(); return false; }
|
|
1058
|
+
let inGrace = false;
|
|
1059
|
+
try { inGrace = this.callbacks.isInApprovalResumeGrace() === true; } catch { inGrace = false; }
|
|
1060
|
+
// Probe says the session is no longer in the post-approval resume grace — reset the
|
|
1061
|
+
// cap clock so a LATER approval-resume valley in this same response starts fresh.
|
|
1062
|
+
if (!inGrace) { this.clearApprovalResumeDefer(); return false; }
|
|
1063
|
+
if (this.approvalResumeDeferEpoch !== this.responseEpoch || this.approvalResumeDeferSince === 0) {
|
|
1064
|
+
// First defer for this response — start the cap clock. Note: the clock is only
|
|
1065
|
+
// reset by clearApprovalResumeDefer (probe false, turn teardown), NOT by a
|
|
1066
|
+
// cap-release below — so once the cap trips it STAYS released for the whole
|
|
1067
|
+
// grace episode (no arm/clear cycle can restart the 18s and defer forever).
|
|
1068
|
+
this.approvalResumeDeferEpoch = this.responseEpoch;
|
|
1069
|
+
this.approvalResumeDeferSince = now;
|
|
1070
|
+
return true;
|
|
1071
|
+
}
|
|
1072
|
+
if ((now - this.approvalResumeDeferSince) >= APPROVAL_RESUME_IDLE_DEFER_CAP_MS) {
|
|
1073
|
+
// Cap reached: stop suppressing so the normal idle-finish can run. Deliberately
|
|
1074
|
+
// does NOT clear the clock — leaving deferSince set keeps this branch returning
|
|
1075
|
+
// false on every subsequent call until the probe drops (clearApprovalResumeDefer)
|
|
1076
|
+
// or the turn ends (resetActiveTurnState), preventing an infinite re-defer.
|
|
1077
|
+
return false;
|
|
1078
|
+
}
|
|
1079
|
+
return true;
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
private clearApprovalResumeDefer(): void {
|
|
1083
|
+
this.approvalResumeDeferSince = 0;
|
|
1084
|
+
this.approvalResumeDeferEpoch = -1;
|
|
1085
|
+
}
|
|
1086
|
+
|
|
990
1087
|
finishResponse(): void {
|
|
991
1088
|
if (this.submitPendingUntil > Date.now()) return;
|
|
992
1089
|
if (this.responseSettleIgnoreUntil > Date.now()) return;
|
|
@@ -169,6 +169,9 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
169
169
|
private ptyProcess: PtyRuntimeTransport | null = null;
|
|
170
170
|
private transportFactory: PtyTransportFactory;
|
|
171
171
|
private onStatusChange: (() => void) | null = null;
|
|
172
|
+
// FALSE-IDLE (Fix 2): probe the owning instance for the post-approval resume grace.
|
|
173
|
+
// Null until the instance registers it; the engine treats absence as "not in grace".
|
|
174
|
+
private inApprovalResumeGraceProbe: (() => boolean) | null = null;
|
|
172
175
|
|
|
173
176
|
// ─── State machine engine ─────────────────────────
|
|
174
177
|
readonly engine: CliStateEngine;
|
|
@@ -477,6 +480,7 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
477
480
|
onStatusChange: () => { this.onStatusChange?.(); },
|
|
478
481
|
onApplyParsedSession: (session) => { this.applyParsedSessionMetadata(session); },
|
|
479
482
|
onTurnCompleted: () => { this.responseBuffer = ''; },
|
|
483
|
+
isInApprovalResumeGrace: () => this.inApprovalResumeGraceProbe?.() === true,
|
|
480
484
|
} satisfies CliStateEngineCallbacks,
|
|
481
485
|
resolvedConfig.timeouts,
|
|
482
486
|
);
|
|
@@ -549,6 +553,13 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
549
553
|
this.onStatusChange = callback;
|
|
550
554
|
}
|
|
551
555
|
|
|
556
|
+
// FALSE-IDLE (Fix 2): the instance registers its inApprovalResumeGrace judgment so the
|
|
557
|
+
// engine's applyIdle hysteresis can scope itself to autonomous auto-approving sessions
|
|
558
|
+
// without the engine needing any mesh/auto-approve awareness of its own.
|
|
559
|
+
setInApprovalResumeGraceProbe(probe: () => boolean): void {
|
|
560
|
+
this.inApprovalResumeGraceProbe = probe;
|
|
561
|
+
}
|
|
562
|
+
|
|
552
563
|
setOnPtyData(callback: (data: string) => void): void {
|
|
553
564
|
this.onPtyDataCallback = callback;
|
|
554
565
|
}
|
|
@@ -537,7 +537,7 @@ export const meshCrudHandlers: Record<string, MedFamilyHandler> = {
|
|
|
537
537
|
const ownerFailure = await ctx.requireMeshHostMutationOwner(meshId, args?.inlineMesh, 'node update');
|
|
538
538
|
if (ownerFailure) return ownerFailure;
|
|
539
539
|
try {
|
|
540
|
-
const { updateNode } = await import('../../config/mesh-config.js');
|
|
540
|
+
const { updateNode, normalizeCapabilityTags } = await import('../../config/mesh-config.js');
|
|
541
541
|
const policy = args?.policy && typeof args.policy === 'object' && !Array.isArray(args.policy)
|
|
542
542
|
? { ...(args.policy as Record<string, unknown>) }
|
|
543
543
|
: {};
|
|
@@ -579,13 +579,54 @@ export const meshCrudHandlers: Record<string, MedFamilyHandler> = {
|
|
|
579
579
|
.filter(Boolean);
|
|
580
580
|
}
|
|
581
581
|
const node = updateNode(meshId, nodeId, patch as any);
|
|
582
|
-
if (
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
582
|
+
if (node) {
|
|
583
|
+
// Provider priority / systemPrompt changes don't touch
|
|
584
|
+
// the queue revision, so without a manual bust the
|
|
585
|
+
// cached aggregate keeps surfacing pre-update values
|
|
586
|
+
// (priority chip, coordinator prompt preview, etc.).
|
|
587
|
+
ctx.invalidateAggregateMeshStatus(meshId);
|
|
588
|
+
return { success: true, node };
|
|
589
|
+
}
|
|
590
|
+
// NODE-SLOTS-REMOTE-WRITE: updateNode reads ONLY this daemon's local
|
|
591
|
+
// meshes.json. When update_mesh_node is forwarded to a node's home-daemon
|
|
592
|
+
// that has no local config entry for a coordinator-owned mesh (a remote
|
|
593
|
+
// member daemon, or a cloud coordinator that holds the mesh solely in its
|
|
594
|
+
// inline cache), updateNode returns undefined and the write failed with
|
|
595
|
+
// "Mesh node not found" — even though the coordinator attached the mesh
|
|
596
|
+
// snapshot as inlineMesh and the read paths (get_mesh / dry-run / list)
|
|
597
|
+
// resolve it fine via getMeshForCommand's inline fallback. Mirror those
|
|
598
|
+
// read paths here: resolve the mesh from the inline cache and apply the
|
|
599
|
+
// same field semantics as updateNode, persisting to the inline cache.
|
|
600
|
+
const meshRecord = await ctx.getMeshForCommand(meshId, args?.inlineMesh, { preferInline: true });
|
|
601
|
+
const mesh = meshRecord?.mesh;
|
|
602
|
+
if (!mesh) return { success: false, error: 'Mesh not found' };
|
|
603
|
+
const inlineNode = Array.isArray(mesh.nodes)
|
|
604
|
+
? mesh.nodes.find((n: any) => meshNodeIdMatches(n, nodeId))
|
|
605
|
+
: undefined;
|
|
606
|
+
if (!inlineNode) return { success: false, error: 'Mesh node not found' };
|
|
607
|
+
// Apply the SAME field semantics updateNode uses so the inline write and a
|
|
608
|
+
// local-config write are indistinguishable: shallow-merge policy, honor an
|
|
609
|
+
// explicit systemPrompt clear, replace/normalize capability tags.
|
|
610
|
+
inlineNode.policy = {
|
|
611
|
+
...(inlineNode.policy && typeof inlineNode.policy === 'object' && !Array.isArray(inlineNode.policy)
|
|
612
|
+
? inlineNode.policy as Record<string, unknown>
|
|
613
|
+
: {}),
|
|
614
|
+
...(patch.policy as Record<string, unknown>),
|
|
615
|
+
};
|
|
616
|
+
if (Object.prototype.hasOwnProperty.call(patch, 'systemPrompt')) {
|
|
617
|
+
const sp = (patch as any).systemPrompt;
|
|
618
|
+
if (typeof sp === 'string' && sp.trim()) inlineNode.systemPrompt = sp;
|
|
619
|
+
else delete inlineNode.systemPrompt;
|
|
620
|
+
}
|
|
621
|
+
if (Object.prototype.hasOwnProperty.call(patch, 'capabilities')) {
|
|
622
|
+
const tags = normalizeCapabilityTags((patch as any).capabilities);
|
|
623
|
+
if (tags && tags.length) inlineNode.capabilities = tags;
|
|
624
|
+
else delete inlineNode.capabilities;
|
|
625
|
+
}
|
|
626
|
+
// updateInlineMeshNode canonicalizes node identity, persists the mutated
|
|
627
|
+
// mesh back to the inline cache, and busts the aggregate-status cache.
|
|
628
|
+
ctx.updateInlineMeshNode(meshId, mesh, inlineNode);
|
|
629
|
+
return { success: true, node: inlineNode };
|
|
589
630
|
} catch (e: any) {
|
|
590
631
|
return { success: false, error: e.message };
|
|
591
632
|
}
|
|
@@ -104,7 +104,7 @@ function stripDeadRoleFromProviderRoles(policy: unknown): boolean {
|
|
|
104
104
|
return changed;
|
|
105
105
|
}
|
|
106
106
|
|
|
107
|
-
function normalizeCapabilityTags(value: unknown): string[] | undefined {
|
|
107
|
+
export function normalizeCapabilityTags(value: unknown): string[] | undefined {
|
|
108
108
|
if (!Array.isArray(value)) return undefined;
|
|
109
109
|
const seen = new Set<string>();
|
|
110
110
|
const tags = value
|
|
@@ -193,6 +193,29 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
193
193
|
*/
|
|
194
194
|
private static readonly AUTO_APPROVE_MASK_STALL_MS = 10500;
|
|
195
195
|
|
|
196
|
+
/**
|
|
197
|
+
* FALSE-IDLE (inter-approval quiet valley): grace window after an auto-approve
|
|
198
|
+
* (or mesh_approve) RESOLVES a modal during which a subsequent generating→idle
|
|
199
|
+
* quiet valley must NOT be treated as turn completion.
|
|
200
|
+
*
|
|
201
|
+
* The RCA: auto-approve resolves a modal → the agent resumes the same turn →
|
|
202
|
+
* between resolving that approval and preparing the next tool/approval the agent
|
|
203
|
+
* falls briefly silent. The FSM sees idle + a recorded mid-turn assistant bubble
|
|
204
|
+
* and fires an early agent:generating_completed even though the turn is still in
|
|
205
|
+
* flight. Live evidence showed the same session resuming waiting_approval ~13s
|
|
206
|
+
* after a "clean" completion emit.
|
|
207
|
+
*
|
|
208
|
+
* The window must be comfortably larger than the observed resume gap (~13s) so
|
|
209
|
+
* the valley is bridged, but not so large that a turn that genuinely ended right
|
|
210
|
+
* after an approval is held for an annoying stretch. 18s clears 13s with margin
|
|
211
|
+
* while capping the worst-case extra hold on a truly-finished turn at 18s (still
|
|
212
|
+
* well under COMPLETED_FINALIZATION_MAX_WAIT_MS's 30s hard bound). The recency is
|
|
213
|
+
* measured from the engine's lastApprovalResolvedAt, which is stamped ONLY by
|
|
214
|
+
* resolveModal (auto-approve fire / dashboard / mesh_approve) — so a plain turn
|
|
215
|
+
* with no approval never carries recency and is never held (no regression).
|
|
216
|
+
*/
|
|
217
|
+
private static readonly APPROVAL_RESUME_GRACE_MS = 18_000;
|
|
218
|
+
|
|
196
219
|
private adapter: ProviderCliAdapter;
|
|
197
220
|
private context: InstanceContext | null = null;
|
|
198
221
|
private events: ProviderEvent[] = [];
|
|
@@ -400,6 +423,12 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
400
423
|
this.detectStatusTransition();
|
|
401
424
|
});
|
|
402
425
|
|
|
426
|
+
// FALSE-IDLE (Fix 2): let the engine's applyIdle hysteresis consult THIS instance's
|
|
427
|
+
// auto-approve/mesh-scoped resume-grace judgment (the same one Fix 1 uses).
|
|
428
|
+
if (typeof this.adapter.setInApprovalResumeGraceProbe === 'function') {
|
|
429
|
+
this.adapter.setInApprovalResumeGraceProbe(() => this.inApprovalResumeGrace());
|
|
430
|
+
}
|
|
431
|
+
|
|
403
432
|
// PTY spawn
|
|
404
433
|
await this.adapter.spawn();
|
|
405
434
|
await this.enforceFreshSessionLaunchIfNeeded();
|
|
@@ -1630,6 +1659,26 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
1630
1659
|
if (adapterAny?.currentTurnScope) return { reason: 'adapter_turn_scope_active', terminal: !approvalResolvedIdle };
|
|
1631
1660
|
if (this.hasAdapterPendingResponse()) return { reason: 'adapter_pending_response', terminal: !approvalResolvedIdle };
|
|
1632
1661
|
|
|
1662
|
+
// (FALSE-IDLE, Fix 1) SETTLE-VALLEY hold extension for the generating→idle valley.
|
|
1663
|
+
// The existing SETTLE-VALLEY hold below only covers previousStatus==='waiting_approval'.
|
|
1664
|
+
// But when auto-approve RESOLVES a modal the engine flips straight to 'generating'
|
|
1665
|
+
// (resolveModal → setStatus('generating')), so the resumed turn's brief inter-approval
|
|
1666
|
+
// quiet valley arrives with previousStatus==='generating' and a recorded mid-turn
|
|
1667
|
+
// assistant bubble — which satisfies every gate below and fires an early completion
|
|
1668
|
+
// mid-turn (RCA: same session re-enters waiting_approval ~13s later). Here we HOLD:
|
|
1669
|
+
// by this point the adapter's own pending-response evidence (above) is clean — the
|
|
1670
|
+
// engine has torn down currentTurnScope/isWaitingForResponse in the valley — so we
|
|
1671
|
+
// rely on the approval-resume recency signal instead. Non-terminal, so the retry loop
|
|
1672
|
+
// re-runs the resume guard (busy_reentry / new_pty_output / resumed_status in the
|
|
1673
|
+
// flush) and cancels the moment the turn actually resumes; and it clears on its own
|
|
1674
|
+
// once the grace window lapses (a turn that genuinely ended right after an approval
|
|
1675
|
+
// then emits normally). Bounded by COMPLETED_FINALIZATION_MAX_WAIT_MS as a hard floor
|
|
1676
|
+
// against a wedge. Scoped by inApprovalResumeGrace to autonomous auto-approving mesh
|
|
1677
|
+
// sessions with a recent resolveModal, so a plain non-approval turn is untouched.
|
|
1678
|
+
if (!approvalResolvedIdle && this.inApprovalResumeGrace()) {
|
|
1679
|
+
return { reason: 'approval_resume_grace', terminal: false };
|
|
1680
|
+
}
|
|
1681
|
+
|
|
1633
1682
|
const partial = typeof this.adapter.getPartialResponse === 'function'
|
|
1634
1683
|
? this.adapter.getPartialResponse()
|
|
1635
1684
|
: '';
|
|
@@ -1837,6 +1886,27 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
1837
1886
|
return this.isMeshWorkerSession() || !!this.settings.meshCoordinatorFor;
|
|
1838
1887
|
}
|
|
1839
1888
|
|
|
1889
|
+
/**
|
|
1890
|
+
* FALSE-IDLE: are we inside the post-approval resume grace window? True when this
|
|
1891
|
+
* is an autonomous auto-approving mesh session AND the engine resolved a modal
|
|
1892
|
+
* (auto-approve / mesh_approve) within APPROVAL_RESUME_GRACE_MS. This is the single
|
|
1893
|
+
* "auto-approve recency" judgment shared by Fix 1 (the SETTLE-VALLEY completion
|
|
1894
|
+
* hold below) and Fix 2 (the FSM-level applyIdle hysteresis in cli-state-engine).
|
|
1895
|
+
*
|
|
1896
|
+
* Scoped to autonomous auto-approving sessions so a foreground/attended session,
|
|
1897
|
+
* or a session with auto-approve off (whose approvals a human answers), is never
|
|
1898
|
+
* held. The recency clock (adapter.lastApprovalResolvedAt) is 0 until the first
|
|
1899
|
+
* resolveModal, so a plain turn that never saw an approval always returns false.
|
|
1900
|
+
*/
|
|
1901
|
+
private inApprovalResumeGrace(now = Date.now()): boolean {
|
|
1902
|
+
if (!this.isAutonomousMeshSession() || !this.shouldAutoApprove()) return false;
|
|
1903
|
+
const resolvedAt = typeof (this.adapter as any)?.lastApprovalResolvedAt === 'number'
|
|
1904
|
+
? (this.adapter as any).lastApprovalResolvedAt as number
|
|
1905
|
+
: 0;
|
|
1906
|
+
if (resolvedAt <= 0) return false;
|
|
1907
|
+
return (now - resolvedAt) < CliProviderInstance.APPROVAL_RESUME_GRACE_MS;
|
|
1908
|
+
}
|
|
1909
|
+
|
|
1840
1910
|
/**
|
|
1841
1911
|
* ARCH-REFACTOR R1: the taskId to attribute the CURRENTLY-completing turn to.
|
|
1842
1912
|
* Prefers the per-turn binding (engine.currentTurnTaskId, set when the turn was
|
|
@@ -2308,8 +2378,31 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
2308
2378
|
// Mirrors the SDK v1 detect-status approval heuristic (detect-status.ts).
|
|
2309
2379
|
const modalKind = typeof modal?.kind === 'string' ? modal.kind : 'approval';
|
|
2310
2380
|
if (modalKind !== 'approval') {
|
|
2311
|
-
//
|
|
2312
|
-
|
|
2381
|
+
// Defense-in-depth (APPROVAL-PICKER-MISROUTE): a genuine tool-consent
|
|
2382
|
+
// modal ("Do you want to proceed?" + 1. Yes / 3. No) can be MIS-routed
|
|
2383
|
+
// to modal_kind='picker' by the spec FSM when a picker matcher wins the
|
|
2384
|
+
// priority tie in the wrong state. The spec-level negative guard (Fix A)
|
|
2385
|
+
// is the root fix, but a stale/undeployed spec would leave the worker
|
|
2386
|
+
// wedged on a modal it could safely have approved. So don't bail on the
|
|
2387
|
+
// kind label alone: only bail when this modal is ALSO a genuine SELECTION
|
|
2388
|
+
// picker (/model, /mode — "Select a model/mode/option" / "Switch
|
|
2389
|
+
// between") with NO consent structure. A modal that carries approval
|
|
2390
|
+
// text or a decline/grant anchor is treated as an approval and falls
|
|
2391
|
+
// through to the structural gate below, even under kind='picker'.
|
|
2392
|
+
const modalText = `${String(modal?.title || '')}\n${String(modal?.message || '')}\n${buttons.join('\n')}`;
|
|
2393
|
+
const looksLikeSelectionPicker = /Select (?:a |an )?(?:model|mode|option)\b|Switch between/i.test(modalText);
|
|
2394
|
+
const looksLikeConsent = looksLikeActiveApprovalPromptText(modalText)
|
|
2395
|
+
|| /Do you want to (?:proceed|create|make|edit|apply|run|delete|modify|allow)\b|allow all edits\b|don'?t ask again\b/i.test(modalText)
|
|
2396
|
+
|| hasNegativeApprovalOption(buttons)
|
|
2397
|
+
|| hasReliableApprovalAffirmative(buttons);
|
|
2398
|
+
if (looksLikeSelectionPicker && !looksLikeConsent) {
|
|
2399
|
+
// Genuine /model or /mode selection picker — no safe default to
|
|
2400
|
+
// auto-pick. Leave it for the user; keep the modal surfaced.
|
|
2401
|
+
return autoApproveActive;
|
|
2402
|
+
}
|
|
2403
|
+
// Otherwise: mis-routed consent modal (or an ambiguous picker that still
|
|
2404
|
+
// carries consent structure) — fall through to the structural approval
|
|
2405
|
+
// gate below, which only fires on a real affirmative+decline/grant set.
|
|
2313
2406
|
}
|
|
2314
2407
|
const { index: buttonIndex, label: buttonLabel } = pickApprovalButton(buttons, this.provider);
|
|
2315
2408
|
// Structural decline anchor. A real approval offers BOTH an affirmative
|