@adhdev/daemon-core 0.9.82-rc.353 → 0.9.82-rc.355
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/commands/handler.d.ts +15 -0
- package/dist/index.js +703 -220
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +703 -220
- package/dist/index.mjs.map +1 -1
- package/dist/mesh/mesh-event-trace.d.ts +21 -0
- package/dist/mesh/mesh-runtime-store.d.ts +1 -1
- package/dist/mesh/mesh-work-queue.d.ts +1 -1
- package/dist/providers/acp-provider-instance.d.ts +3 -0
- package/dist/providers/cli-provider-instance.d.ts +14 -0
- package/dist/providers/manual-attendance.d.ts +63 -0
- package/dist/providers/provider-instance.d.ts +8 -0
- package/dist/providers/spec/adapter.d.ts +22 -0
- package/dist/providers/spec/fsm-driver.d.ts +49 -7
- package/dist/providers/spec/fsm-evaluator.d.ts +4 -0
- package/dist/providers/spec/types.d.ts +9 -5
- package/package.json +2 -2
- package/src/commands/cli-manager.ts +20 -2
- package/src/commands/handler.ts +32 -0
- package/src/commands/router.ts +19 -6
- package/src/git/git-diff.ts +31 -14
- package/src/mesh/mesh-event-trace.ts +67 -0
- package/src/mesh/mesh-events-coordinator.ts +117 -12
- package/src/mesh/mesh-events-pending.ts +33 -0
- package/src/mesh/mesh-events-stale.ts +3 -1
- package/src/mesh/mesh-reconcile-loop.ts +47 -0
- package/src/mesh/mesh-runtime-store.ts +18 -2
- package/src/mesh/mesh-work-queue.ts +8 -1
- package/src/providers/acp-provider-instance.ts +18 -1
- package/src/providers/cli-provider-instance.ts +123 -7
- package/src/providers/manual-attendance.ts +85 -0
- package/src/providers/provider-instance.ts +9 -0
- package/src/providers/spec/adapter.ts +67 -0
- package/src/providers/spec/cli-adapter.ts +6 -0
- package/src/providers/spec/evaluator.ts +24 -9
- package/src/providers/spec/fsm-driver.ts +135 -13
- package/src/providers/spec/fsm-evaluator.ts +19 -2
- package/src/providers/spec/types.ts +9 -5
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export interface MeshEventTraceCtx {
|
|
2
|
+
/** Primary correlation anchor — the mesh task id (meshActiveTaskId / metadataEvent.taskId). */
|
|
3
|
+
taskId?: unknown;
|
|
4
|
+
/** Optional per-event id when the producer assigns one. */
|
|
5
|
+
eventId?: unknown;
|
|
6
|
+
/** Worker session id — the fallback anchor when no task is attached. */
|
|
7
|
+
sessionId?: unknown;
|
|
8
|
+
nodeId?: unknown;
|
|
9
|
+
meshId?: unknown;
|
|
10
|
+
event?: unknown;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Stable, greppable correlation key. `task=` and `sess=` are ALWAYS rendered (as `-`
|
|
14
|
+
* when absent) so the key shape is uniform across stages and a single grep alternation
|
|
15
|
+
* (`task=<id>\|sess=<id>`) follows the event end-to-end.
|
|
16
|
+
*/
|
|
17
|
+
export declare function meshEventTraceKey(ctx: MeshEventTraceCtx): string;
|
|
18
|
+
/** Lifecycle progress (INFO). One line per stage the event clears. */
|
|
19
|
+
export declare function traceMeshEventStage(stage: string, ctx: MeshEventTraceCtx, detail?: string): void;
|
|
20
|
+
/** Event did not advance — rejected / held / skipped / deduped (WARN). */
|
|
21
|
+
export declare function traceMeshEventDrop(reason: string, ctx: MeshEventTraceCtx, detail?: string): void;
|
|
@@ -129,7 +129,7 @@ export declare class MeshRuntimeStore {
|
|
|
129
129
|
dispatchedAt: string;
|
|
130
130
|
updatedAt: string;
|
|
131
131
|
}>;
|
|
132
|
-
updateDirectDispatchStatus(meshId: string, sessionId: string, status: 'acked' | 'completed' | 'failed' | 'stale'): void;
|
|
132
|
+
updateDirectDispatchStatus(meshId: string, sessionId: string, status: 'acked' | 'completed' | 'failed' | 'stale', taskId?: string): void;
|
|
133
133
|
cleanupTerminalDirectDispatches(olderThanMs: number): void;
|
|
134
134
|
deleteDirectDispatches(meshId: string): void;
|
|
135
135
|
/**
|
|
@@ -320,7 +320,7 @@ export declare function insertDirectDispatch(meshId: string, data: {
|
|
|
320
320
|
dispatchedAt: string;
|
|
321
321
|
}): void;
|
|
322
322
|
export declare function getActiveDirectDispatches(meshId: string): DirectDispatchRecord[];
|
|
323
|
-
export declare function updateDirectDispatchStatus(meshId: string, sessionId: string, status: 'acked' | 'completed' | 'failed' | 'stale'): void;
|
|
323
|
+
export declare function updateDirectDispatchStatus(meshId: string, sessionId: string, status: 'acked' | 'completed' | 'failed' | 'stale', taskId?: string): void;
|
|
324
324
|
export declare function cleanupTerminalDirectDispatches(olderThanMs?: number): void;
|
|
325
325
|
export declare function markStaleDirectDispatches(meshId: string, olderThanMs?: number): void;
|
|
326
326
|
/**
|
|
@@ -88,6 +88,9 @@ export declare class AcpProviderInstance implements ProviderInstance {
|
|
|
88
88
|
sendPrompt(text: string, contentBlocks?: ContentBlock[]): Promise<void>;
|
|
89
89
|
private cancelSession;
|
|
90
90
|
private permissionResolvers;
|
|
91
|
+
private readonly manualAttendance;
|
|
92
|
+
/** @see ProviderInstance.noteManualInteraction */
|
|
93
|
+
noteManualInteraction(now?: number): void;
|
|
91
94
|
resolvePermission(approved: boolean): Promise<void>;
|
|
92
95
|
private handleSessionUpdate;
|
|
93
96
|
/** Handle legacy session/update formats (pre-standardization compat) */
|
|
@@ -79,6 +79,7 @@ export declare class CliProviderInstance implements ProviderInstance {
|
|
|
79
79
|
private pendingAutoApprovalSince;
|
|
80
80
|
private autoApproveSettleTimer;
|
|
81
81
|
private autoApproveInactiveSince;
|
|
82
|
+
private readonly manualAttendance;
|
|
82
83
|
private controlValues;
|
|
83
84
|
private summaryMetadata;
|
|
84
85
|
private appliedEffectKeys;
|
|
@@ -183,6 +184,8 @@ export declare class CliProviderInstance implements ProviderInstance {
|
|
|
183
184
|
private shouldSuppressStaleParsedBusyStatus;
|
|
184
185
|
private getCompletedFinalizationBlock;
|
|
185
186
|
private scheduleCompletedDebounceFlush;
|
|
187
|
+
private isMeshWorkerSession;
|
|
188
|
+
private meshTraceCtx;
|
|
186
189
|
private flushCompletedDebounceIfFinalized;
|
|
187
190
|
private maybeAutoApproveStatus;
|
|
188
191
|
/**
|
|
@@ -204,6 +207,17 @@ export declare class CliProviderInstance implements ProviderInstance {
|
|
|
204
207
|
get cliType(): string;
|
|
205
208
|
get cliName(): string;
|
|
206
209
|
private shouldAutoApprove;
|
|
210
|
+
/** @see ProviderInstance.noteManualInteraction */
|
|
211
|
+
noteManualInteraction(now?: number): void;
|
|
212
|
+
/**
|
|
213
|
+
* Whether auto-approve should be treated as active *right now* for display
|
|
214
|
+
* and firing decisions: the configured intent AND the user is not currently
|
|
215
|
+
* attending this session by hand. When a human is attending, auto-approve is
|
|
216
|
+
* held so the modal stays visible and they can drive it via the controlbar.
|
|
217
|
+
* Provider-agnostic — the attendance signal is the command set, never any
|
|
218
|
+
* CLI-specific modal text.
|
|
219
|
+
*/
|
|
220
|
+
private autoApproveEffectivelyActive;
|
|
207
221
|
private recordAutoApproval;
|
|
208
222
|
recordApprovalSelection(buttonText: string): void;
|
|
209
223
|
private formatMarkerTimestamp;
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ManualAttendanceTracker — provider-agnostic "is a human driving this session
|
|
3
|
+
* right now" signal, used to suppress auto-approve while the user is taking
|
|
4
|
+
* manual control of a session from the dashboard.
|
|
5
|
+
*
|
|
6
|
+
* Why this exists
|
|
7
|
+
* ---------------
|
|
8
|
+
* When `autoApprove` is on, an approval modal is auto-dismissed within a few
|
|
9
|
+
* hundred ms of appearing. For a background mesh worker that is exactly the
|
|
10
|
+
* desired delegated behavior. But for a session the user is actively watching
|
|
11
|
+
* and operating (a base-node / foreground session), the auto-fire closes the
|
|
12
|
+
* modal before the human can pick a button — and likewise fights their use of
|
|
13
|
+
* the controlbar. The fix is to give the human a short quiet window: while they
|
|
14
|
+
* are attending the session by hand, auto-approve holds; once they go idle it
|
|
15
|
+
* resumes.
|
|
16
|
+
*
|
|
17
|
+
* The tracker holds only a timestamp. The *signal* — which commands count as
|
|
18
|
+
* "a human attending" — is decided by the caller (the command handler), and is
|
|
19
|
+
* the same set for every provider: foreground tab selection (select_session /
|
|
20
|
+
* open_panel), controlbar use (invoke_provider_script / set_mode / change_model
|
|
21
|
+
* / set_thought_level), manual approval (resolve_action) and manual terminal
|
|
22
|
+
* input (pty_input). Notably NOT send_chat, which is also how a coordinator
|
|
23
|
+
* delegates a task to a worker — counting it would wrongly suppress the
|
|
24
|
+
* worker's delegated auto-approve.
|
|
25
|
+
*
|
|
26
|
+
* Because a background worker never receives any of those attending commands,
|
|
27
|
+
* it is never "attended", so its delegated auto-approve is unaffected. The
|
|
28
|
+
* mechanism is therefore provider-common AND preserves worker auto-approve
|
|
29
|
+
* without any per-provider branching.
|
|
30
|
+
*/
|
|
31
|
+
/**
|
|
32
|
+
* How long after the last manual interaction auto-approve stays suppressed.
|
|
33
|
+
*
|
|
34
|
+
* Trade-off: long enough that after foregrounding a session's tab the user has
|
|
35
|
+
* a realistic chance to act on an incoming approval (the auto-approve settle
|
|
36
|
+
* window is only ~600ms, so a human cannot out-race it), yet short enough that
|
|
37
|
+
* a session left unattended — e.g. a worker tab the user briefly peeked at —
|
|
38
|
+
* resumes auto-approving within about a minute. Re-armed on every attending
|
|
39
|
+
* command, so a user who keeps interacting keeps the window fresh.
|
|
40
|
+
*/
|
|
41
|
+
export declare const AUTO_APPROVE_MANUAL_ATTENDANCE_SUPPRESS_MS = 60000;
|
|
42
|
+
export declare class ManualAttendanceTracker {
|
|
43
|
+
private readonly suppressMs;
|
|
44
|
+
private lastInteractionAt;
|
|
45
|
+
constructor(suppressMs?: number);
|
|
46
|
+
/** Record that a human just drove this session by hand. */
|
|
47
|
+
note(now?: number): void;
|
|
48
|
+
/** True while a manual interaction is recent enough to suppress auto-approve. */
|
|
49
|
+
isAttended(now?: number): boolean;
|
|
50
|
+
/**
|
|
51
|
+
* Milliseconds remaining in the current suppression window, or 0 when not
|
|
52
|
+
* attended. Used to re-arm a re-check timer so auto-approve fires the moment
|
|
53
|
+
* the window lapses even if the PTY/agent has since gone silent.
|
|
54
|
+
*/
|
|
55
|
+
remainingMs(now?: number): number;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* The session-scoped commands that count as "a human is attending this session
|
|
59
|
+
* by hand". Shared so the command handler and any forward path agree on one
|
|
60
|
+
* definition. Deliberately excludes send_chat (coordinator task delegation) and
|
|
61
|
+
* pure read commands (read_chat / list_chats — passive polling, not driving).
|
|
62
|
+
*/
|
|
63
|
+
export declare const MANUAL_ATTENDANCE_COMMANDS: ReadonlySet<string>;
|
|
@@ -194,6 +194,14 @@ export interface ProviderInstance {
|
|
|
194
194
|
detachMeshAssignment?(): void;
|
|
195
195
|
/** Refresh static provider definition/scripts without restarting the live runtime. */
|
|
196
196
|
refreshProviderDefinition?(provider: ProviderModule): void;
|
|
197
|
+
/**
|
|
198
|
+
* Record that a human is actively attending this session by hand right now
|
|
199
|
+
* (foreground tab selection, controlbar use, manual approval, terminal
|
|
200
|
+
* input). Provider-common signal that suppresses auto-approve for a short
|
|
201
|
+
* window so the user can drive the session manually; background mesh worker
|
|
202
|
+
* sessions never receive it, so their delegated auto-approve is unaffected.
|
|
203
|
+
*/
|
|
204
|
+
noteManualInteraction?(now?: number): void;
|
|
197
205
|
/** cleanup */
|
|
198
206
|
dispose(): void;
|
|
199
207
|
}
|
|
@@ -33,6 +33,21 @@ export interface TerminalAdapterHandlers {
|
|
|
33
33
|
}): void;
|
|
34
34
|
tick?(): void;
|
|
35
35
|
}
|
|
36
|
+
/**
|
|
37
|
+
* One entry in the PTY input/output event timeline (debug-only). Captured at
|
|
38
|
+
* the single common point every spec@4 provider funnels through — this adapter
|
|
39
|
+
* — so the Spec Debug Snapshot can answer "what input / output preceded a status
|
|
40
|
+
* transition?". Observation only; nothing here feeds the FSM decision.
|
|
41
|
+
*/
|
|
42
|
+
export interface SpecPtyEvent {
|
|
43
|
+
/** Wall-clock ms. */
|
|
44
|
+
ts: number;
|
|
45
|
+
kind: 'spawn' | 'input' | 'output' | 'resize' | 'cursor' | 'exit';
|
|
46
|
+
/** Human-readable, control-char-escaped, length-capped preview. */
|
|
47
|
+
content: string;
|
|
48
|
+
/** Raw byte length before truncation (output/input only). */
|
|
49
|
+
bytes?: number;
|
|
50
|
+
}
|
|
36
51
|
export declare class TerminalAdapter {
|
|
37
52
|
private readonly opts;
|
|
38
53
|
private readonly handlers;
|
|
@@ -46,6 +61,9 @@ export declare class TerminalAdapter {
|
|
|
46
61
|
private screenTimer;
|
|
47
62
|
private tickTimer;
|
|
48
63
|
private lastScreen;
|
|
64
|
+
/** Debug-only ring buffer of PTY input/output/resize/cursor events. */
|
|
65
|
+
private events;
|
|
66
|
+
private lastCursorKey;
|
|
49
67
|
constructor(opts: TerminalAdapterOpts, handlers: TerminalAdapterHandlers);
|
|
50
68
|
start(): void;
|
|
51
69
|
resize(cols: number, rows: number): void;
|
|
@@ -62,6 +80,10 @@ export declare class TerminalAdapter {
|
|
|
62
80
|
col: number;
|
|
63
81
|
};
|
|
64
82
|
send_keys(text: string): void;
|
|
83
|
+
/** Debug-only: most-recent PTY input/output/resize/cursor events, oldest
|
|
84
|
+
* first. Pure observation — never consulted by the FSM. */
|
|
85
|
+
getEventTimeline(limit?: number): SpecPtyEvent[];
|
|
86
|
+
private recordEvent;
|
|
65
87
|
kill(): void;
|
|
66
88
|
private onChunk;
|
|
67
89
|
private computeScreen;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type SpecPtyEvent } from './adapter.js';
|
|
1
2
|
import type { PtyTransportFactory } from '../../cli-adapters/pty-transport.js';
|
|
2
3
|
import { type TraceEntry } from './evaluator.js';
|
|
3
4
|
import { type TransitionEval } from './fsm-evaluator.js';
|
|
@@ -139,6 +140,7 @@ export interface ISpecDriver {
|
|
|
139
140
|
} | null;
|
|
140
141
|
getFsmDebug?(): unknown;
|
|
141
142
|
getFsmSnapshotHistory?(): ReadonlyArray<FsmSnapshotEntry>;
|
|
143
|
+
getEventTimeline?(limit?: number): ReadonlyArray<SpecPtyEvent>;
|
|
142
144
|
}
|
|
143
145
|
export interface SpecDriverOpts {
|
|
144
146
|
specPath: string;
|
|
@@ -152,6 +154,10 @@ export interface SpecDriverOpts {
|
|
|
152
154
|
extraCliArgs?: string[];
|
|
153
155
|
}
|
|
154
156
|
export declare function resolveSubmitDelayMs(specBeforeSubmit: number | undefined, text: string): number;
|
|
157
|
+
/** Split `text` into chunks of at most `size` UTF-16 units without ever cutting
|
|
158
|
+
* between a high and low surrogate (which would corrupt an astral char — emoji,
|
|
159
|
+
* etc. — on the UTF-8 PTY write). */
|
|
160
|
+
export declare function chunkPreservingSurrogates(text: string, size: number): string[];
|
|
155
161
|
export declare function guessExt(mime: string): string;
|
|
156
162
|
type HistoryEntry = DriverHistoryEntry;
|
|
157
163
|
export declare class FsmDriver implements ISpecDriver {
|
|
@@ -181,6 +187,16 @@ export declare class FsmDriver implements ISpecDriver {
|
|
|
181
187
|
* WIN32_SUBMIT_* and scheduleWin32Submit). Re-arms itself until the FSM
|
|
182
188
|
* leaves idle (submitted) or the resend budget is spent. */
|
|
183
189
|
private win32SubmitTimer;
|
|
190
|
+
/** Wall-clock (ms) of the most recent raw PTY output chunk. Advances on every
|
|
191
|
+
* on_pty_data — including the echo of text written into the composer — so the
|
|
192
|
+
* win32 submit settle-gate can tell when input has finished landing. */
|
|
193
|
+
private lastPtyDataAt;
|
|
194
|
+
/** Wall-clock (ms) of the most recent win32 message-body input write. Bridges
|
|
195
|
+
* the gap between writing a chunk and its echo so the settle-gate does not
|
|
196
|
+
* declare "quiet" mid-write. */
|
|
197
|
+
private lastWin32WriteAt;
|
|
198
|
+
/** Pending paced chunk-write timer for a large win32 body (see writeWin32Body). */
|
|
199
|
+
private win32WriteTimer;
|
|
184
200
|
private currentEval;
|
|
185
201
|
private stateHistory;
|
|
186
202
|
private prevStateAt;
|
|
@@ -230,6 +246,8 @@ export declare class FsmDriver implements ISpecDriver {
|
|
|
230
246
|
};
|
|
231
247
|
getStateHistory(): ReadonlyArray<HistoryEntry>;
|
|
232
248
|
getFsmSnapshotHistory(): ReadonlyArray<FsmSnapshotEntry>;
|
|
249
|
+
/** Debug-only PTY input/output/resize/cursor timeline from the adapter. */
|
|
250
|
+
getEventTimeline(limit?: number): ReadonlyArray<SpecPtyEvent>;
|
|
233
251
|
getSections(): Array<{
|
|
234
252
|
id: string;
|
|
235
253
|
text: string;
|
|
@@ -300,14 +318,38 @@ export declare class FsmDriver implements ISpecDriver {
|
|
|
300
318
|
private actuallySendMessage;
|
|
301
319
|
/** The agent's current coarse status, derived from the FSM node we're in. */
|
|
302
320
|
private currentStatus;
|
|
321
|
+
/** Record a win32 body write so the settle-gate counts it as input activity
|
|
322
|
+
* even before the echo arrives. */
|
|
323
|
+
private markWin32Write;
|
|
324
|
+
/** Most recent win32 input activity — a write we issued OR a PTY output chunk
|
|
325
|
+
* (echo). The submit settle-gate waits for this to go quiet. */
|
|
326
|
+
private lastWin32InputActivityAt;
|
|
303
327
|
/**
|
|
304
|
-
*
|
|
305
|
-
*
|
|
306
|
-
* a
|
|
307
|
-
*
|
|
308
|
-
*
|
|
309
|
-
*
|
|
310
|
-
|
|
328
|
+
* Write the message body to the PTY for win32, paced into bounded chunks. A
|
|
329
|
+
* single unbounded ConPTY write can overflow the input pipe and drop leading
|
|
330
|
+
* bytes; splitting it with a short inter-chunk gap keeps the console input
|
|
331
|
+
* buffer from overflowing. Small bodies still go out in a single write. Each
|
|
332
|
+
* chunk advances lastWin32WriteAt so the submit settle-gate keeps waiting until
|
|
333
|
+
* the final chunk is out and echoed.
|
|
334
|
+
*/
|
|
335
|
+
private writeWin32Body;
|
|
336
|
+
/**
|
|
337
|
+
* win32 submit. Two phases:
|
|
338
|
+
*
|
|
339
|
+
* Phase 1 (settle-gate): hold the first CR until the PTY output has been quiet
|
|
340
|
+
* for WIN32_SUBMIT_SETTLE_MS after the last input write — i.e. the full
|
|
341
|
+
* (possibly multi-KB / multiline) body has finished arriving in the composer
|
|
342
|
+
* and echoing. Honors an initial minimum delay and is bounded by
|
|
343
|
+
* WIN32_SUBMIT_MAX_SETTLE_WAIT_MS so a noisy screen can never hang the submit.
|
|
344
|
+
* This is what stops a long message from being submitted half-arrived (its
|
|
345
|
+
* leading lines lost). A short message settles almost immediately.
|
|
346
|
+
*
|
|
347
|
+
* Phase 2 (verified resend — unchanged): send the submit key, wait a gap, and
|
|
348
|
+
* if the FSM is still 'idle' (the CR was absorbed as a multiline-paste
|
|
349
|
+
* newline) resend, up to WIN32_SUBMIT_MAX_RESENDS. The first CR always fires
|
|
350
|
+
* (a stale/edge status never suppresses it); resends are gated on still being
|
|
351
|
+
* idle and stop the instant the agent leaves idle (submitted → generating /
|
|
352
|
+
* approval). This preserves the win32 lone-CR-swallow handling.
|
|
311
353
|
*/
|
|
312
354
|
private scheduleWin32Submit;
|
|
313
355
|
private handleClickControl;
|
|
@@ -19,6 +19,10 @@ export interface CondResult {
|
|
|
19
19
|
/** Remaining ms until a time-based condition would flip to true. 0 if
|
|
20
20
|
* already true or not applicable. Lets the UI show a countdown. */
|
|
21
21
|
remainingMs?: number;
|
|
22
|
+
/** Debug-only: the actual substring a TRUE regex condition matched, so the
|
|
23
|
+
* snapshot shows WHAT text the rule fired on — not just which regex. Never
|
|
24
|
+
* read by the FSM; purely for the Spec Debug Snapshot. */
|
|
25
|
+
matchedText?: string;
|
|
22
26
|
children?: CondResult[];
|
|
23
27
|
}
|
|
24
28
|
/** One evaluated transition with its full reasoning. */
|
|
@@ -123,11 +123,15 @@ export interface SectionDef {
|
|
|
123
123
|
until?: string;
|
|
124
124
|
/**
|
|
125
125
|
* Anchor regex(es). A single string anchors on the first/last matching line
|
|
126
|
-
* (per `anchor_last`). An array is an OR-set
|
|
127
|
-
*
|
|
128
|
-
*
|
|
129
|
-
*
|
|
130
|
-
*
|
|
126
|
+
* (per `anchor_last`). An array is an OR-set of candidate shapes: each
|
|
127
|
+
* candidate resolves its OWN anchor line independently (anchor_last → that
|
|
128
|
+
* candidate's last matching line, else its first), then the TOPMOST resolved
|
|
129
|
+
* line across candidates wins — a section's anchor marks the top of its
|
|
130
|
+
* block, so the highest recognized landmark bounds the whole block. This
|
|
131
|
+
* lets one section capture two shapes — e.g. a box-divider modal AND a
|
|
132
|
+
* divider-less modal whose only stable landmark is the question line above
|
|
133
|
+
* its numbered choices — while preventing a stray LOWER landmark (e.g. an
|
|
134
|
+
* input-box `────` rule below the choices) from clipping the block.
|
|
131
135
|
*/
|
|
132
136
|
anchor?: string | string[];
|
|
133
137
|
anchor_flags?: string;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@adhdev/daemon-core",
|
|
3
|
-
"version": "0.9.82-rc.
|
|
3
|
+
"version": "0.9.82-rc.355",
|
|
4
4
|
"description": "ADHDev daemon core — CDP, IDE detection, providers, command execution",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -46,7 +46,7 @@
|
|
|
46
46
|
"author": "vilmire",
|
|
47
47
|
"license": "AGPL-3.0-or-later",
|
|
48
48
|
"dependencies": {
|
|
49
|
-
"@adhdev/mesh-shared": "0.9.82-rc.
|
|
49
|
+
"@adhdev/mesh-shared": "0.9.82-rc.355",
|
|
50
50
|
"@adhdev/session-host-core": "*",
|
|
51
51
|
"@agentclientprotocol/sdk": "^0.16.1",
|
|
52
52
|
"ajv": "^8.20.0",
|
|
@@ -18,7 +18,7 @@ import { loadConfig } from '../config/config.js';
|
|
|
18
18
|
import { loadState, saveState } from '../config/state-store.js';
|
|
19
19
|
import { getWorkspaceState, resolveLaunchDirectory } from '../config/workspaces.js';
|
|
20
20
|
import { appendRecentActivity } from '../config/recent-activity.js';
|
|
21
|
-
import { unregisterMeshCoordinator } from '../mesh/coordinator-registry.js';
|
|
21
|
+
import { unregisterMeshCoordinator, getCoordinatorForSession } from '../mesh/coordinator-registry.js';
|
|
22
22
|
import { upsertSavedProviderSession } from '../config/saved-sessions.js';
|
|
23
23
|
import { buildLegacyModelModeSummaryMetadata, normalizeProviderSummaryMetadata } from '../providers/summary-metadata.js';
|
|
24
24
|
import { CliProviderInstance } from '../providers/cli-provider-instance.js';
|
|
@@ -1042,6 +1042,24 @@ export class DaemonCliManager {
|
|
|
1042
1042
|
);
|
|
1043
1043
|
continue;
|
|
1044
1044
|
}
|
|
1045
|
+
// Re-establish the launch-time settings a fresh launch applies. startSession
|
|
1046
|
+
// seeds every new instance with { ...providerLoader.getSettings(type), ...override };
|
|
1047
|
+
// passing a bare {} here on restart silently dropped TWO launch settings, so a
|
|
1048
|
+
// restored session diverged from a freshly-launched one:
|
|
1049
|
+
// - autoApprove (a provider/machine setting from getSettings) → a restored
|
|
1050
|
+
// coordinator self-session lost auto-approve and re-prompted on every tool call.
|
|
1051
|
+
// - meshCoordinatorFor (the coordinator launch's settingsOverride) → the restored
|
|
1052
|
+
// session was no longer recognized as this daemon's live CLI coordinator by
|
|
1053
|
+
// findLiveCoordinators (so pending mesh events stopped draining into its PTY) nor
|
|
1054
|
+
// surfaced with the coordinator badge via settings. The persisted coordinator
|
|
1055
|
+
// registry (loaded on boot) is the source of truth to rebuild that mark.
|
|
1056
|
+
// Both restores are provider-agnostic — getSettings is keyed by provider type and the
|
|
1057
|
+
// registry mark is type-independent.
|
|
1058
|
+
const restoredSettings: Record<string, any> = { ...this.providerLoader.getSettings(normalizedType) };
|
|
1059
|
+
const coordinatorEntry = getCoordinatorForSession(record.runtimeId);
|
|
1060
|
+
if (coordinatorEntry?.meshId) {
|
|
1061
|
+
restoredSettings.meshCoordinatorFor = coordinatorEntry.meshId;
|
|
1062
|
+
}
|
|
1045
1063
|
try {
|
|
1046
1064
|
await this.registerCliInstance(
|
|
1047
1065
|
record.runtimeId,
|
|
@@ -1050,7 +1068,7 @@ export class DaemonCliManager {
|
|
|
1050
1068
|
record.workspace,
|
|
1051
1069
|
record.cliArgs,
|
|
1052
1070
|
resolvedProvider,
|
|
1053
|
-
|
|
1071
|
+
restoredSettings,
|
|
1054
1072
|
true,
|
|
1055
1073
|
{
|
|
1056
1074
|
providerSessionId: sessionBinding.providerSessionId,
|
package/src/commands/handler.ts
CHANGED
|
@@ -25,6 +25,7 @@ import type { SessionRegistry, SessionRuntimeTarget } from '../sessions/registry
|
|
|
25
25
|
import { reconcileIdeRuntimeSessions } from '../sessions/reconcile.js';
|
|
26
26
|
import { LOG } from '../logging/logger.js';
|
|
27
27
|
import { resolveLegacyProviderScript, type LegacyStringScript } from './provider-script-resolver.js';
|
|
28
|
+
import { MANUAL_ATTENDANCE_COMMANDS } from '../providers/manual-attendance.js';
|
|
28
29
|
|
|
29
30
|
// Sub-module imports
|
|
30
31
|
import * as Chat from './chat-commands.js';
|
|
@@ -375,6 +376,36 @@ export class DaemonCommandHandler implements CommandHelpers {
|
|
|
375
376
|
this._agentStream = manager;
|
|
376
377
|
}
|
|
377
378
|
|
|
379
|
+
/**
|
|
380
|
+
* When a command in the manual-attendance set arrives for a session this
|
|
381
|
+
* daemon hosts, stamp the live instance so auto-approve holds while the user
|
|
382
|
+
* drives the session by hand. Provider-common: the signal is the command
|
|
383
|
+
* (foreground select_session / open_panel, controlbar invoke_provider_script
|
|
384
|
+
* / set_mode / change_model / set_thought_level, manual resolve_action,
|
|
385
|
+
* pty_input), never any CLI-specific modal text — so it works identically for
|
|
386
|
+
* every CLI/ACP provider. send_chat is deliberately excluded because a
|
|
387
|
+
* coordinator delegating a task to a worker also uses send_chat; counting it
|
|
388
|
+
* would wrongly suppress the worker's delegated auto-approve. For a remote
|
|
389
|
+
* mesh worker session the controlbar commands are forwarded to the owning
|
|
390
|
+
* worker daemon, which runs this same hook there, so attendance is recorded
|
|
391
|
+
* on the daemon that actually hosts the instance.
|
|
392
|
+
*/
|
|
393
|
+
private noteManualAttendanceIfApplicable(cmd: string, args: any): void {
|
|
394
|
+
if (!MANUAL_ATTENDANCE_COMMANDS.has(cmd)) return;
|
|
395
|
+
const sessionId = this._currentRoute.session?.sessionId
|
|
396
|
+
|| (typeof args?.targetSessionId === 'string' ? args.targetSessionId.trim() : '');
|
|
397
|
+
if (!sessionId) return;
|
|
398
|
+
const session = this._ctx.sessionRegistry?.get(sessionId);
|
|
399
|
+
const instanceKey = session?.adapterKey || session?.instanceKey || sessionId;
|
|
400
|
+
const instance = this._ctx.instanceManager?.getInstance(instanceKey) as
|
|
401
|
+
{ noteManualInteraction?: (now?: number) => void } | undefined;
|
|
402
|
+
try {
|
|
403
|
+
instance?.noteManualInteraction?.();
|
|
404
|
+
} catch {
|
|
405
|
+
// attendance is best-effort — never block command dispatch
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
|
|
378
409
|
// ─── Command Dispatcher ──────────────────────────
|
|
379
410
|
|
|
380
411
|
async handle(cmd: string, args: any): Promise<CommandResult> {
|
|
@@ -382,6 +413,7 @@ export class DaemonCommandHandler implements CommandHelpers {
|
|
|
382
413
|
this._currentRoute = this.resolveRoute(args);
|
|
383
414
|
const startedAt = Date.now();
|
|
384
415
|
this.logCommandStart(cmd, args);
|
|
416
|
+
this.noteManualAttendanceIfApplicable(cmd, args);
|
|
385
417
|
let result: CommandResult;
|
|
386
418
|
|
|
387
419
|
if (isGitCommandName(cmd)) {
|
package/src/commands/router.ts
CHANGED
|
@@ -8110,7 +8110,10 @@ export class DaemonCommandRouter {
|
|
|
8110
8110
|
// _meshDirectDispatch prevents re-forwarding (and P2P self-dial) when the stored
|
|
8111
8111
|
// daemonId uses a legacy format that doesn't match the receiving daemon's identity.
|
|
8112
8112
|
const selfDaemonId = this.deps.statusInstanceId;
|
|
8113
|
-
|
|
8113
|
+
// daemonIdsEquivalent: a legacy-form stored daemonId that resolves to THIS
|
|
8114
|
+
// machine's core must be treated as local (not remote) so it is not forwarded /
|
|
8115
|
+
// P2P self-dialed. Equivalent → local.
|
|
8116
|
+
const isRemote = nodeDaemonId && selfDaemonId && !daemonIdsEquivalent(nodeDaemonId, selfDaemonId);
|
|
8114
8117
|
if (isRemote && this.deps.dispatchMeshCommand && !args?._meshDirectDispatch) {
|
|
8115
8118
|
const forwarded = await this.deps.dispatchMeshCommand(nodeDaemonId!, 'fast_forward_mesh_node', {
|
|
8116
8119
|
...(typeof args === 'object' && args !== null ? args as Record<string, unknown> : {}),
|
|
@@ -8154,7 +8157,9 @@ export class DaemonCommandRouter {
|
|
|
8154
8157
|
// once the call lands on the owning daemon — that daemon then reads
|
|
8155
8158
|
// its own logs even if the stored daemonId uses a legacy form.
|
|
8156
8159
|
const selfDaemonId = this.deps.statusInstanceId;
|
|
8157
|
-
|
|
8160
|
+
// daemonIdsEquivalent: a legacy-form daemonId resolving to this machine's core is
|
|
8161
|
+
// local — read locally instead of forwarding. Equivalent → local.
|
|
8162
|
+
const isRemote = nodeDaemonId && selfDaemonId && !daemonIdsEquivalent(nodeDaemonId, selfDaemonId);
|
|
8158
8163
|
if (isRemote && this.deps.dispatchMeshCommand && !args?._meshDirectDispatch) {
|
|
8159
8164
|
const forwarded = await this.deps.dispatchMeshCommand(nodeDaemonId!, 'get_mesh_node_logs', {
|
|
8160
8165
|
...(typeof args === 'object' && args !== null ? args as Record<string, unknown> : {}),
|
|
@@ -8230,7 +8235,9 @@ export class DaemonCommandRouter {
|
|
|
8230
8235
|
const forwardNode = meshRecordForForward?.mesh?.nodes?.find((n: any) => meshNodeIdMatches(n, nodeId));
|
|
8231
8236
|
const nodeDaemonId = typeof forwardNode?.daemonId === 'string' ? forwardNode.daemonId.trim() : undefined;
|
|
8232
8237
|
const selfDaemonId = this.deps.statusInstanceId;
|
|
8233
|
-
|
|
8238
|
+
// daemonIdsEquivalent: a legacy-form daemonId resolving to this machine's core
|
|
8239
|
+
// is local — execute locally instead of forwarding. Equivalent → local.
|
|
8240
|
+
const isRemote = nodeDaemonId && selfDaemonId && !daemonIdsEquivalent(nodeDaemonId, selfDaemonId);
|
|
8234
8241
|
if (isRemote && this.deps.dispatchMeshCommand && !args?._meshDirectDispatch) {
|
|
8235
8242
|
const callerCoordinatorDaemonId = typeof args?.coordinatorDaemonId === 'string' && args.coordinatorDaemonId.trim()
|
|
8236
8243
|
? args.coordinatorDaemonId.trim()
|
|
@@ -8347,7 +8354,9 @@ export class DaemonCommandRouter {
|
|
|
8347
8354
|
let worktreeCleanup: Record<string, unknown> | undefined;
|
|
8348
8355
|
if (node?.isLocalWorktree) {
|
|
8349
8356
|
const nodeDaemonId = typeof node.daemonId === 'string' ? node.daemonId.trim() : undefined;
|
|
8350
|
-
|
|
8357
|
+
// daemonIdsEquivalent: an equivalent-form daemonId is this machine —
|
|
8358
|
+
// clean up locally, do not forward. Equivalent → local.
|
|
8359
|
+
const isRemoteWorktree = nodeDaemonId && !daemonIdsEquivalent(nodeDaemonId, this.deps.statusInstanceId) && this.deps.dispatchMeshCommand
|
|
8351
8360
|
&& !args?._meshDirectDispatch;
|
|
8352
8361
|
if (isRemoteWorktree) {
|
|
8353
8362
|
// Worktree lives on a different machine — ask that daemon to clean it up.
|
|
@@ -8473,7 +8482,9 @@ export class DaemonCommandRouter {
|
|
|
8473
8482
|
// _meshDirectDispatch prevents infinite re-forwarding when the stored daemonId
|
|
8474
8483
|
// uses a legacy format that doesn't match the receiving daemon's statusInstanceId.
|
|
8475
8484
|
const sourceDaemonId = typeof sourceNode.daemonId === 'string' ? sourceNode.daemonId.trim() : undefined;
|
|
8476
|
-
|
|
8485
|
+
// daemonIdsEquivalent: an equivalent-form source daemonId is this machine —
|
|
8486
|
+
// clone locally, do not forward. Equivalent → local.
|
|
8487
|
+
if (sourceDaemonId && !daemonIdsEquivalent(sourceDaemonId, this.deps.statusInstanceId) && this.deps.dispatchMeshCommand
|
|
8477
8488
|
&& !args?._meshDirectDispatch) {
|
|
8478
8489
|
const forwarded = await this.deps.dispatchMeshCommand(sourceDaemonId, 'clone_mesh_node', {
|
|
8479
8490
|
...(typeof args === 'object' && args !== null ? args as Record<string, unknown> : {}),
|
|
@@ -8748,7 +8759,9 @@ export class DaemonCommandRouter {
|
|
|
8748
8759
|
// Bootstrap runs scripts in the worktree path — forward to the node's daemon if remote.
|
|
8749
8760
|
// _meshDirectDispatch prevents re-forwarding when stored daemonId uses legacy format.
|
|
8750
8761
|
const nodeDaemonId = typeof node.daemonId === 'string' ? node.daemonId.trim() : undefined;
|
|
8751
|
-
|
|
8762
|
+
// daemonIdsEquivalent: an equivalent-form daemonId is this machine —
|
|
8763
|
+
// bootstrap locally, do not forward. Equivalent → local.
|
|
8764
|
+
if (nodeDaemonId && !daemonIdsEquivalent(nodeDaemonId, this.deps.statusInstanceId) && this.deps.dispatchMeshCommand
|
|
8752
8765
|
&& !args?._meshDirectDispatch) {
|
|
8753
8766
|
const forwarded = await this.deps.dispatchMeshCommand(nodeDaemonId, 'retry_mesh_node_bootstrap', {
|
|
8754
8767
|
...(typeof args === 'object' && args !== null ? args as Record<string, unknown> : {}),
|
package/src/git/git-diff.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { readFile, realpath } from 'node:fs/promises';
|
|
2
2
|
import * as path from 'node:path';
|
|
3
3
|
import type { GitDiffSummary, GitFileChange, GitFileChangeStatus } from './git-types.js';
|
|
4
|
-
import { GitCommandError, isPathInside, resolveGitRepository, runGit } from './git-executor.js';
|
|
4
|
+
import { GIT_STATUS_TIMEOUT_MS, GitCommandError, isPathInside, resolveGitRepository, runGit } from './git-executor.js';
|
|
5
5
|
|
|
6
6
|
const DEFAULT_MAX_FILES = 200;
|
|
7
7
|
const DEFAULT_MAX_BYTES = 200_000;
|
|
@@ -18,6 +18,21 @@ export interface GitDiffOptions {
|
|
|
18
18
|
baseRef?: string;
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
+
/**
|
|
22
|
+
* Diff collection fans out an even larger parallel git burst than status (up to five
|
|
23
|
+
* concurrent spawns — name-status/numstat × unstaged/staged + ls-files — plus its own
|
|
24
|
+
* `rev-parse --show-toplevel`) and runs concurrently with the status path. On Windows the
|
|
25
|
+
* per-spawn cost alone can exceed the 5s `execGitRaw` default, so — exactly like
|
|
26
|
+
* getGitRepoStatus — give the collection path the larger status budget unless the caller
|
|
27
|
+
* pinned a timeout. Without this, a slow-but-healthy Windows worktree times out on the diff
|
|
28
|
+
* burst and the catch below flattens it to `repoRoot:null, isGitRepo:false`, dropping the
|
|
29
|
+
* diff while the (already-hardened) status block reports isGitRepo:true — an asymmetric
|
|
30
|
+
* failure that reads as "not a git repo" for diff only.
|
|
31
|
+
*/
|
|
32
|
+
function withCollectionTimeout(options: GitDiffOptions): GitDiffOptions {
|
|
33
|
+
return options.timeoutMs === undefined ? { ...options, timeoutMs: GIT_STATUS_TIMEOUT_MS } : options;
|
|
34
|
+
}
|
|
35
|
+
|
|
21
36
|
function validateBaseRef(ref: string): string {
|
|
22
37
|
const trimmed = ref.trim();
|
|
23
38
|
if (!trimmed || trimmed.startsWith('-') || trimmed.includes('..') || !/^[A-Za-z0-9][A-Za-z0-9._/@-]*$/.test(trimmed)) {
|
|
@@ -54,16 +69,17 @@ export async function getGitDiffSummary(
|
|
|
54
69
|
options: GitDiffOptions = {},
|
|
55
70
|
): Promise<GitDiffSummary> {
|
|
56
71
|
const lastCheckedAt = Date.now();
|
|
72
|
+
const effectiveOptions = withCollectionTimeout(options);
|
|
57
73
|
|
|
58
74
|
try {
|
|
59
|
-
const repo = await resolveGitRepository(workspace,
|
|
75
|
+
const repo = await resolveGitRepository(workspace, effectiveOptions);
|
|
60
76
|
const repoRoot = repo.repoRoot!;
|
|
61
77
|
|
|
62
78
|
if (options.baseRef) {
|
|
63
79
|
const range = `${validateBaseRef(options.baseRef)}...HEAD`;
|
|
64
80
|
const [nameStatus, numstat] = await Promise.all([
|
|
65
|
-
runGit(repo, ['diff', '--no-ext-diff', '--name-status', range, '--'], { ...
|
|
66
|
-
runGit(repo, ['diff', '--no-ext-diff', '--numstat', range, '--'], { ...
|
|
81
|
+
runGit(repo, ['diff', '--no-ext-diff', '--name-status', range, '--'], { ...effectiveOptions, cwd: repoRoot }),
|
|
82
|
+
runGit(repo, ['diff', '--no-ext-diff', '--numstat', range, '--'], { ...effectiveOptions, cwd: repoRoot }),
|
|
67
83
|
]);
|
|
68
84
|
const outputBytes = byteLength(nameStatus.stdout + numstat.stdout);
|
|
69
85
|
const changes = combineDiffEntries(nameStatus.stdout, numstat.stdout, false);
|
|
@@ -83,11 +99,11 @@ export async function getGitDiffSummary(
|
|
|
83
99
|
}
|
|
84
100
|
|
|
85
101
|
const [unstagedNameStatus, unstagedNumstat, stagedNameStatus, stagedNumstat, untracked] = await Promise.all([
|
|
86
|
-
runGit(repo, ['diff', '--no-ext-diff', '--name-status'], { ...
|
|
87
|
-
runGit(repo, ['diff', '--no-ext-diff', '--numstat'], { ...
|
|
88
|
-
runGit(repo, ['diff', '--cached', '--no-ext-diff', '--name-status'], { ...
|
|
89
|
-
runGit(repo, ['diff', '--cached', '--no-ext-diff', '--numstat'], { ...
|
|
90
|
-
runGit(repo, ['ls-files', '--others', '--exclude-standard'], { ...
|
|
102
|
+
runGit(repo, ['diff', '--no-ext-diff', '--name-status'], { ...effectiveOptions, cwd: repoRoot }),
|
|
103
|
+
runGit(repo, ['diff', '--no-ext-diff', '--numstat'], { ...effectiveOptions, cwd: repoRoot }),
|
|
104
|
+
runGit(repo, ['diff', '--cached', '--no-ext-diff', '--name-status'], { ...effectiveOptions, cwd: repoRoot }),
|
|
105
|
+
runGit(repo, ['diff', '--cached', '--no-ext-diff', '--numstat'], { ...effectiveOptions, cwd: repoRoot }),
|
|
106
|
+
runGit(repo, ['ls-files', '--others', '--exclude-standard'], { ...effectiveOptions, cwd: repoRoot }),
|
|
91
107
|
]);
|
|
92
108
|
|
|
93
109
|
const outputBytes = byteLength(
|
|
@@ -139,14 +155,15 @@ export async function getGitFileDiff(
|
|
|
139
155
|
options: GitDiffOptions = {},
|
|
140
156
|
): Promise<GitFileDiffResult> {
|
|
141
157
|
const lastCheckedAt = Date.now();
|
|
142
|
-
const
|
|
158
|
+
const effectiveOptions = withCollectionTimeout(options);
|
|
159
|
+
const repo = await resolveGitRepository(workspace, effectiveOptions);
|
|
143
160
|
const repoRoot = repo.repoRoot!;
|
|
144
161
|
const selected = await resolveRepoFilePath(repoRoot, filePath);
|
|
145
162
|
const maxBytes = normalizePositiveInteger(options.maxBytes, DEFAULT_MAX_BYTES);
|
|
146
163
|
|
|
147
164
|
if (options.baseRef) {
|
|
148
165
|
const range = `${validateBaseRef(options.baseRef)}...HEAD`;
|
|
149
|
-
const result = await runGit(repo, ['diff', '--no-ext-diff', range, '--', selected.relativePath], { ...
|
|
166
|
+
const result = await runGit(repo, ['diff', '--no-ext-diff', range, '--', selected.relativePath], { ...effectiveOptions, cwd: repoRoot });
|
|
150
167
|
const bounded = truncateText(result.stdout, maxBytes);
|
|
151
168
|
return {
|
|
152
169
|
workspace: repo.workspace,
|
|
@@ -160,15 +177,15 @@ export async function getGitFileDiff(
|
|
|
160
177
|
}
|
|
161
178
|
|
|
162
179
|
const [unstaged, staged] = await Promise.all([
|
|
163
|
-
runGit(repo, ['diff', '--no-ext-diff', '--', selected.relativePath], { ...
|
|
164
|
-
runGit(repo, ['diff', '--cached', '--no-ext-diff', '--', selected.relativePath], { ...
|
|
180
|
+
runGit(repo, ['diff', '--no-ext-diff', '--', selected.relativePath], { ...effectiveOptions, cwd: repoRoot }),
|
|
181
|
+
runGit(repo, ['diff', '--cached', '--no-ext-diff', '--', selected.relativePath], { ...effectiveOptions, cwd: repoRoot }),
|
|
165
182
|
]);
|
|
166
183
|
|
|
167
184
|
let diff = [unstaged.stdout, staged.stdout].filter((part) => part.length > 0).join('\n');
|
|
168
185
|
|
|
169
186
|
if (!diff) {
|
|
170
187
|
const untracked = await runGit(repo, ['ls-files', '--others', '--exclude-standard', '--', selected.relativePath], {
|
|
171
|
-
...
|
|
188
|
+
...effectiveOptions,
|
|
172
189
|
cwd: repoRoot,
|
|
173
190
|
});
|
|
174
191
|
const untrackedFiles = untracked.stdout.split('\n').filter(Boolean);
|