@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
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
import * as fs from 'node:fs';
|
|
22
22
|
import * as os from 'node:os';
|
|
23
23
|
import * as path from 'node:path';
|
|
24
|
-
import { TerminalAdapter, type TerminalAdapterOpts } from './adapter.js';
|
|
24
|
+
import { TerminalAdapter, type TerminalAdapterOpts, type SpecPtyEvent } from './adapter.js';
|
|
25
25
|
import { resolveCliSpawnPlanFromParts } from '../../cli-adapters/provider-cli-runtime.js';
|
|
26
26
|
import type { PtyTransportFactory } from '../../cli-adapters/pty-transport.js';
|
|
27
27
|
import { DEFAULT_SESSION_HOST_COLS, DEFAULT_SESSION_HOST_ROWS } from '@adhdev/session-host-core';
|
|
@@ -123,6 +123,7 @@ export interface ISpecDriver {
|
|
|
123
123
|
getCompletionIdleDebounceState(): { active: boolean; ageMs: number; holdMs: number; forceAfterMs: number } | null;
|
|
124
124
|
getFsmDebug?(): unknown;
|
|
125
125
|
getFsmSnapshotHistory?(): ReadonlyArray<FsmSnapshotEntry>;
|
|
126
|
+
getEventTimeline?(limit?: number): ReadonlyArray<SpecPtyEvent>;
|
|
126
127
|
}
|
|
127
128
|
|
|
128
129
|
export interface SpecDriverOpts {
|
|
@@ -162,6 +163,23 @@ const SUBMIT_DELAY_FLOOR_MS = 200;
|
|
|
162
163
|
// trimmed by the TUI on submit.
|
|
163
164
|
const WIN32_SUBMIT_RESEND_GAP_MS = 350;
|
|
164
165
|
const WIN32_SUBMIT_MAX_RESENDS = 14;
|
|
166
|
+
// Settle-gate for the win32 FIRST submit CR. Hold the CR until the PTY output has
|
|
167
|
+
// gone quiet for WIN32_SUBMIT_SETTLE_MS after the last input write — i.e. the full
|
|
168
|
+
// (possibly multi-KB / multiline) message body has finished arriving in the
|
|
169
|
+
// composer and echoing back. A long message waits until it actually lands; a short
|
|
170
|
+
// one settles almost immediately. WIN32_SUBMIT_MAX_SETTLE_WAIT_MS bounds the wait
|
|
171
|
+
// so a perpetually-noisy screen can never hang the submit. This is what stops a
|
|
172
|
+
// blind fixed-delay CR from submitting a half-arrived prompt and dropping its
|
|
173
|
+
// leading lines. The phase-2 verified-resend loop (below) is unchanged.
|
|
174
|
+
const WIN32_SUBMIT_SETTLE_MS = 500;
|
|
175
|
+
const WIN32_SUBMIT_MAX_SETTLE_WAIT_MS = 10_000;
|
|
176
|
+
const WIN32_SUBMIT_SETTLE_POLL_MS = 120;
|
|
177
|
+
// Defensive paced PTY write. A single unbounded ConPTY write can overflow the
|
|
178
|
+
// input pipe and drop leading bytes; split a large body into bounded chunks with a
|
|
179
|
+
// short inter-chunk gap so the console input buffer keeps up. Small bodies still
|
|
180
|
+
// write in one shot.
|
|
181
|
+
const WIN32_PTY_WRITE_CHUNK_CHARS = 1024;
|
|
182
|
+
const WIN32_PTY_WRITE_CHUNK_GAP_MS = 8;
|
|
165
183
|
|
|
166
184
|
export function resolveSubmitDelayMs(specBeforeSubmit: number | undefined, text: string): number {
|
|
167
185
|
const lines = countNewlines(text);
|
|
@@ -170,6 +188,27 @@ export function resolveSubmitDelayMs(specBeforeSubmit: number | undefined, text:
|
|
|
170
188
|
return Math.max(spec, SUBMIT_DELAY_FLOOR_MS + linesBonus);
|
|
171
189
|
}
|
|
172
190
|
|
|
191
|
+
/** Split `text` into chunks of at most `size` UTF-16 units without ever cutting
|
|
192
|
+
* between a high and low surrogate (which would corrupt an astral char — emoji,
|
|
193
|
+
* etc. — on the UTF-8 PTY write). */
|
|
194
|
+
export function chunkPreservingSurrogates(text: string, size: number): string[] {
|
|
195
|
+
const chunks: string[] = [];
|
|
196
|
+
let offset = 0;
|
|
197
|
+
while (offset < text.length) {
|
|
198
|
+
let end = Math.min(text.length, offset + size);
|
|
199
|
+
if (end < text.length) {
|
|
200
|
+
const code = text.charCodeAt(end - 1);
|
|
201
|
+
// Boundary lands on a high surrogate → pull back one so the pair stays
|
|
202
|
+
// together in the next chunk.
|
|
203
|
+
if (code >= 0xd800 && code <= 0xdbff) end -= 1;
|
|
204
|
+
}
|
|
205
|
+
if (end <= offset) end = Math.min(text.length, offset + size); // size 1 on a lone surrogate
|
|
206
|
+
chunks.push(text.slice(offset, end));
|
|
207
|
+
offset = end;
|
|
208
|
+
}
|
|
209
|
+
return chunks;
|
|
210
|
+
}
|
|
211
|
+
|
|
173
212
|
export function guessExt(mime: string): string {
|
|
174
213
|
if (/png/i.test(mime)) return '.png';
|
|
175
214
|
if (/jpe?g/i.test(mime)) return '.jpg';
|
|
@@ -231,6 +270,16 @@ export class FsmDriver implements ISpecDriver {
|
|
|
231
270
|
* WIN32_SUBMIT_* and scheduleWin32Submit). Re-arms itself until the FSM
|
|
232
271
|
* leaves idle (submitted) or the resend budget is spent. */
|
|
233
272
|
private win32SubmitTimer: ReturnType<typeof setTimeout> | null = null;
|
|
273
|
+
/** Wall-clock (ms) of the most recent raw PTY output chunk. Advances on every
|
|
274
|
+
* on_pty_data — including the echo of text written into the composer — so the
|
|
275
|
+
* win32 submit settle-gate can tell when input has finished landing. */
|
|
276
|
+
private lastPtyDataAt = 0;
|
|
277
|
+
/** Wall-clock (ms) of the most recent win32 message-body input write. Bridges
|
|
278
|
+
* the gap between writing a chunk and its echo so the settle-gate does not
|
|
279
|
+
* declare "quiet" mid-write. */
|
|
280
|
+
private lastWin32WriteAt = 0;
|
|
281
|
+
/** Pending paced chunk-write timer for a large win32 body (see writeWin32Body). */
|
|
282
|
+
private win32WriteTimer: ReturnType<typeof setTimeout> | null = null;
|
|
234
283
|
|
|
235
284
|
private currentEval: CurrentEval | null = null;
|
|
236
285
|
private stateHistory: HistoryEntry[] = [];
|
|
@@ -257,7 +306,7 @@ export class FsmDriver implements ISpecDriver {
|
|
|
257
306
|
this.buildAdapterOpts(),
|
|
258
307
|
{
|
|
259
308
|
init: () => this.emitInitialState(),
|
|
260
|
-
on_pty_data: (chunk) => this.emit({ kind: 'pty_data', chunk }),
|
|
309
|
+
on_pty_data: (chunk) => { this.lastPtyDataAt = Date.now(); this.emit({ kind: 'pty_data', chunk }); },
|
|
261
310
|
on_screen_changed: () => this.reevaluate(),
|
|
262
311
|
on_exit: ({ exitCode }) => this.handleExit(exitCode),
|
|
263
312
|
},
|
|
@@ -349,6 +398,7 @@ export class FsmDriver implements ISpecDriver {
|
|
|
349
398
|
if (this.wakeTimer) { clearTimeout(this.wakeTimer); this.wakeTimer = null; }
|
|
350
399
|
if (this.stallTimer) { clearTimeout(this.stallTimer); this.stallTimer = null; }
|
|
351
400
|
if (this.win32SubmitTimer) { clearTimeout(this.win32SubmitTimer); this.win32SubmitTimer = null; }
|
|
401
|
+
if (this.win32WriteTimer) { clearTimeout(this.win32WriteTimer); this.win32WriteTimer = null; }
|
|
352
402
|
this.specWatcher?.close();
|
|
353
403
|
this.adapter.kill();
|
|
354
404
|
}
|
|
@@ -385,6 +435,10 @@ export class FsmDriver implements ISpecDriver {
|
|
|
385
435
|
|
|
386
436
|
getStateHistory(): ReadonlyArray<HistoryEntry> { return this.stateHistory; }
|
|
387
437
|
getFsmSnapshotHistory(): ReadonlyArray<FsmSnapshotEntry> { return this.fsmSnapshotHistory; }
|
|
438
|
+
/** Debug-only PTY input/output/resize/cursor timeline from the adapter. */
|
|
439
|
+
getEventTimeline(limit?: number): ReadonlyArray<SpecPtyEvent> {
|
|
440
|
+
return this.adapter.getEventTimeline(limit);
|
|
441
|
+
}
|
|
388
442
|
getSections(): Array<{ id: string; text: string }> | null {
|
|
389
443
|
try {
|
|
390
444
|
const screen = this.adapter.snapshot();
|
|
@@ -867,7 +921,7 @@ export class FsmDriver implements ISpecDriver {
|
|
|
867
921
|
// typing simulation is skipped on win32; correctness of submission wins
|
|
868
922
|
// over the typing visual there.
|
|
869
923
|
if (process.platform === 'win32') {
|
|
870
|
-
this.
|
|
924
|
+
this.writeWin32Body(text);
|
|
871
925
|
this.scheduleWin32Submit(sm.submit_key, beforeSubmit);
|
|
872
926
|
return;
|
|
873
927
|
}
|
|
@@ -896,17 +950,70 @@ export class FsmDriver implements ISpecDriver {
|
|
|
896
950
|
return st ? statusForState(st) : 'idle';
|
|
897
951
|
}
|
|
898
952
|
|
|
953
|
+
/** Record a win32 body write so the settle-gate counts it as input activity
|
|
954
|
+
* even before the echo arrives. */
|
|
955
|
+
private markWin32Write(): void {
|
|
956
|
+
this.lastWin32WriteAt = Date.now();
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
/** Most recent win32 input activity — a write we issued OR a PTY output chunk
|
|
960
|
+
* (echo). The submit settle-gate waits for this to go quiet. */
|
|
961
|
+
private lastWin32InputActivityAt(): number {
|
|
962
|
+
return Math.max(this.lastPtyDataAt, this.lastWin32WriteAt);
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
/**
|
|
966
|
+
* Write the message body to the PTY for win32, paced into bounded chunks. A
|
|
967
|
+
* single unbounded ConPTY write can overflow the input pipe and drop leading
|
|
968
|
+
* bytes; splitting it with a short inter-chunk gap keeps the console input
|
|
969
|
+
* buffer from overflowing. Small bodies still go out in a single write. Each
|
|
970
|
+
* chunk advances lastWin32WriteAt so the submit settle-gate keeps waiting until
|
|
971
|
+
* the final chunk is out and echoed.
|
|
972
|
+
*/
|
|
973
|
+
private writeWin32Body(text: string): void {
|
|
974
|
+
if (this.win32WriteTimer) { clearTimeout(this.win32WriteTimer); this.win32WriteTimer = null; }
|
|
975
|
+
if (text.length <= WIN32_PTY_WRITE_CHUNK_CHARS) {
|
|
976
|
+
this.markWin32Write();
|
|
977
|
+
this.adapter.send_keys(text);
|
|
978
|
+
return;
|
|
979
|
+
}
|
|
980
|
+
const chunks = chunkPreservingSurrogates(text, WIN32_PTY_WRITE_CHUNK_CHARS);
|
|
981
|
+
let idx = 0;
|
|
982
|
+
const writeNext = (): void => {
|
|
983
|
+
this.win32WriteTimer = null;
|
|
984
|
+
if (idx >= chunks.length) return;
|
|
985
|
+
this.markWin32Write();
|
|
986
|
+
this.adapter.send_keys(chunks[idx]);
|
|
987
|
+
idx += 1;
|
|
988
|
+
if (idx < chunks.length) {
|
|
989
|
+
this.win32WriteTimer = setTimeout(writeNext, WIN32_PTY_WRITE_CHUNK_GAP_MS);
|
|
990
|
+
}
|
|
991
|
+
};
|
|
992
|
+
writeNext();
|
|
993
|
+
}
|
|
994
|
+
|
|
899
995
|
/**
|
|
900
|
-
* win32
|
|
901
|
-
*
|
|
902
|
-
*
|
|
903
|
-
*
|
|
904
|
-
*
|
|
905
|
-
*
|
|
906
|
-
*
|
|
996
|
+
* win32 submit. Two phases:
|
|
997
|
+
*
|
|
998
|
+
* Phase 1 (settle-gate): hold the first CR until the PTY output has been quiet
|
|
999
|
+
* for WIN32_SUBMIT_SETTLE_MS after the last input write — i.e. the full
|
|
1000
|
+
* (possibly multi-KB / multiline) body has finished arriving in the composer
|
|
1001
|
+
* and echoing. Honors an initial minimum delay and is bounded by
|
|
1002
|
+
* WIN32_SUBMIT_MAX_SETTLE_WAIT_MS so a noisy screen can never hang the submit.
|
|
1003
|
+
* This is what stops a long message from being submitted half-arrived (its
|
|
1004
|
+
* leading lines lost). A short message settles almost immediately.
|
|
1005
|
+
*
|
|
1006
|
+
* Phase 2 (verified resend — unchanged): send the submit key, wait a gap, and
|
|
1007
|
+
* if the FSM is still 'idle' (the CR was absorbed as a multiline-paste
|
|
1008
|
+
* newline) resend, up to WIN32_SUBMIT_MAX_RESENDS. The first CR always fires
|
|
1009
|
+
* (a stale/edge status never suppresses it); resends are gated on still being
|
|
1010
|
+
* idle and stop the instant the agent leaves idle (submitted → generating /
|
|
1011
|
+
* approval). This preserves the win32 lone-CR-swallow handling.
|
|
907
1012
|
*/
|
|
908
1013
|
private scheduleWin32Submit(submitKey: string, initialDelayMs: number): void {
|
|
909
1014
|
if (this.win32SubmitTimer) { clearTimeout(this.win32SubmitTimer); this.win32SubmitTimer = null; }
|
|
1015
|
+
const startedAt = Date.now();
|
|
1016
|
+
|
|
910
1017
|
const fire = (attempt: number): void => {
|
|
911
1018
|
this.win32SubmitTimer = null;
|
|
912
1019
|
this.adapter.send_keys(submitKey);
|
|
@@ -917,8 +1024,22 @@ export class FsmDriver implements ISpecDriver {
|
|
|
917
1024
|
fire(attempt + 1);
|
|
918
1025
|
}, WIN32_SUBMIT_RESEND_GAP_MS);
|
|
919
1026
|
};
|
|
920
|
-
|
|
921
|
-
|
|
1027
|
+
|
|
1028
|
+
const waitForSettle = (): void => {
|
|
1029
|
+
this.win32SubmitTimer = null;
|
|
1030
|
+
const now = Date.now();
|
|
1031
|
+
const quietFor = now - this.lastWin32InputActivityAt();
|
|
1032
|
+
const waited = now - startedAt;
|
|
1033
|
+
if (quietFor >= WIN32_SUBMIT_SETTLE_MS || waited >= WIN32_SUBMIT_MAX_SETTLE_WAIT_MS) {
|
|
1034
|
+
fire(0);
|
|
1035
|
+
return;
|
|
1036
|
+
}
|
|
1037
|
+
const recheckIn = Math.min(WIN32_SUBMIT_SETTLE_MS - quietFor, WIN32_SUBMIT_SETTLE_POLL_MS);
|
|
1038
|
+
this.win32SubmitTimer = setTimeout(waitForSettle, Math.max(recheckIn, 30));
|
|
1039
|
+
};
|
|
1040
|
+
|
|
1041
|
+
if (initialDelayMs > 0) this.win32SubmitTimer = setTimeout(waitForSettle, initialDelayMs);
|
|
1042
|
+
else waitForSettle();
|
|
922
1043
|
}
|
|
923
1044
|
|
|
924
1045
|
private handleClickControl(controlId: string, payload?: unknown): void {
|
|
@@ -1039,7 +1160,8 @@ function summarizeTransition(t: TransitionEval): string[] {
|
|
|
1039
1160
|
}
|
|
1040
1161
|
|
|
1041
1162
|
function flattenCond(c: import('./fsm-evaluator.js').CondResult, out: string[], depth: number): void {
|
|
1042
|
-
|
|
1163
|
+
const matched = c.matchedText ? ` matched=${JSON.stringify(c.matchedText)}` : '';
|
|
1164
|
+
out.push(`${' '.repeat(depth)}${c.kind} ${c.detail} = ${c.result}${c.remainingMs ? ` (${c.remainingMs}ms left)` : ''}${matched}`);
|
|
1043
1165
|
for (const child of c.children ?? []) flattenCond(child, out, depth + 1);
|
|
1044
1166
|
}
|
|
1045
1167
|
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
|
|
17
17
|
import type { Condition, SectionDef } from './types.js';
|
|
18
18
|
import {
|
|
19
|
-
resolveSections, evaluateCondition, type ResolvedSection, type TraceEntry,
|
|
19
|
+
resolveSections, evaluateCondition, sectionText, type ResolvedSection, type TraceEntry,
|
|
20
20
|
} from './evaluator.js';
|
|
21
21
|
import {
|
|
22
22
|
type CliSpecV4, type FsmCondition, type FsmTransition,
|
|
@@ -42,6 +42,10 @@ export interface CondResult {
|
|
|
42
42
|
/** Remaining ms until a time-based condition would flip to true. 0 if
|
|
43
43
|
* already true or not applicable. Lets the UI show a countdown. */
|
|
44
44
|
remainingMs?: number;
|
|
45
|
+
/** Debug-only: the actual substring a TRUE regex condition matched, so the
|
|
46
|
+
* snapshot shows WHAT text the rule fired on — not just which regex. Never
|
|
47
|
+
* read by the FSM; purely for the Spec Debug Snapshot. */
|
|
48
|
+
matchedText?: string;
|
|
45
49
|
children?: CondResult[];
|
|
46
50
|
}
|
|
47
51
|
|
|
@@ -164,7 +168,20 @@ function evalCond(
|
|
|
164
168
|
const detail = isRegex(cond)
|
|
165
169
|
? `${(cond as any).section ?? '*'}~/${(cond as any).matches}/`
|
|
166
170
|
: `cursor_above=${(cond as any).cursor_above} changed=${(cond as any).changed}`;
|
|
167
|
-
|
|
171
|
+
// Debug-only: when a regex condition is TRUE, also capture the substring
|
|
172
|
+
// it matched so the snapshot can show the exact text the rule fired on.
|
|
173
|
+
// This re-runs the regex (read-only) and CANNOT change `result` above —
|
|
174
|
+
// the FSM decision is still entirely owned by evaluateCondition().
|
|
175
|
+
let matchedText: string | undefined;
|
|
176
|
+
if (result && isRegex(cond)) {
|
|
177
|
+
try {
|
|
178
|
+
const hay = sectionText(sections, (cond as any).section, fullScreen);
|
|
179
|
+
const re = new RegExp((cond as any).matches, (cond as any).flags ?? 'i');
|
|
180
|
+
const m = re.exec(hay);
|
|
181
|
+
if (m && m[0]) matchedText = m[0].replace(/\s+/g, ' ').trim().slice(0, 160);
|
|
182
|
+
} catch { /* capture is best-effort; never affects result */ }
|
|
183
|
+
}
|
|
184
|
+
return matchedText ? { kind, result, detail, matchedText } : { kind, result, detail };
|
|
168
185
|
}
|
|
169
186
|
return { kind: 'all', result: false, detail: 'unknown condition' };
|
|
170
187
|
}
|
|
@@ -154,11 +154,15 @@ export interface SectionDef {
|
|
|
154
154
|
until?: string; // section id OR regex (starts with ^)
|
|
155
155
|
/**
|
|
156
156
|
* Anchor regex(es). A single string anchors on the first/last matching line
|
|
157
|
-
* (per `anchor_last`). An array is an OR-set
|
|
158
|
-
*
|
|
159
|
-
*
|
|
160
|
-
*
|
|
161
|
-
*
|
|
157
|
+
* (per `anchor_last`). An array is an OR-set of candidate shapes: each
|
|
158
|
+
* candidate resolves its OWN anchor line independently (anchor_last → that
|
|
159
|
+
* candidate's last matching line, else its first), then the TOPMOST resolved
|
|
160
|
+
* line across candidates wins — a section's anchor marks the top of its
|
|
161
|
+
* block, so the highest recognized landmark bounds the whole block. This
|
|
162
|
+
* lets one section capture two shapes — e.g. a box-divider modal AND a
|
|
163
|
+
* divider-less modal whose only stable landmark is the question line above
|
|
164
|
+
* its numbered choices — while preventing a stray LOWER landmark (e.g. an
|
|
165
|
+
* input-box `────` rule below the choices) from clipping the block.
|
|
162
166
|
*/
|
|
163
167
|
anchor?: string | string[];
|
|
164
168
|
anchor_flags?: string;
|