@adhdev/daemon-core 0.9.82-rc.416 → 0.9.82-rc.418
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/index.js +53 -15
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +53 -15
- package/dist/index.mjs.map +1 -1
- package/dist/providers/spec/fsm-driver.d.ts +17 -2
- package/package.json +2 -2
- package/src/providers/spec/fsm-driver.ts +137 -31
|
@@ -156,6 +156,8 @@ export interface SpecDriverOpts {
|
|
|
156
156
|
transportFactory?: PtyTransportFactory;
|
|
157
157
|
extraCliArgs?: string[];
|
|
158
158
|
}
|
|
159
|
+
export type Win32SubmitMode = 'paste' | 'soft_newline';
|
|
160
|
+
export declare function resolveWin32SubmitMode(env?: NodeJS.ProcessEnv): Win32SubmitMode;
|
|
159
161
|
export declare function resolveSubmitDelayMs(specBeforeSubmit: number | undefined, text: string): number;
|
|
160
162
|
/** Re-export of the shared surrogate-safe splitter so existing imports of
|
|
161
163
|
* `chunkPreservingSurrogates` from this module keep working. The implementation
|
|
@@ -347,8 +349,21 @@ export declare class FsmDriver implements ISpecDriver {
|
|
|
347
349
|
* single unbounded ConPTY write can overflow the input pipe and drop leading
|
|
348
350
|
* bytes; splitting it with a short inter-chunk gap keeps the console input
|
|
349
351
|
* buffer from overflowing. Small bodies still go out in a single write. Each
|
|
350
|
-
*
|
|
351
|
-
* the final
|
|
352
|
+
* write advances lastWin32WriteAt so the submit settle-gate keeps waiting until
|
|
353
|
+
* the final segment is out and echoed.
|
|
354
|
+
*
|
|
355
|
+
* FIX-B-v2: a body that contains an embedded newline cannot be written raw —
|
|
356
|
+
* on the real win32 Ink/ConPTY composer each '\n' SUBMITS the preceding line as
|
|
357
|
+
* its own entry, truncating the prompt to only the tail fragment. So a
|
|
358
|
+
* newline-bearing body is rewritten so its embedded newlines never submit:
|
|
359
|
+
* - 'paste' (default): wrap the body in a bracketed-paste (ESC[200~ … ESC[201~)
|
|
360
|
+
* — the composer takes the whole thing, newlines and all, as pasted text.
|
|
361
|
+
* - 'soft_newline': replace each embedded newline with a non-submitting
|
|
362
|
+
* Shift+Enter (CSI-u) so the body is typed as one multi-line entry.
|
|
363
|
+
* The trailing submit CR is NOT written here — it stays separate and is fired
|
|
364
|
+
* later by scheduleWin32Submit (concern (A)). The bracketed-paste markers are
|
|
365
|
+
* written as their own atomic segments (never chunked), so chunking can never
|
|
366
|
+
* split ESC[200~ / ESC[201~ mid-sequence regardless of body length.
|
|
352
367
|
*/
|
|
353
368
|
private writeWin32Body;
|
|
354
369
|
/**
|
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.418",
|
|
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.418",
|
|
50
50
|
"@adhdev/session-host-core": "*",
|
|
51
51
|
"@agentclientprotocol/sdk": "^0.16.1",
|
|
52
52
|
"ajv": "^8.20.0",
|
|
@@ -152,21 +152,40 @@ function countNewlines(s: string): number {
|
|
|
152
152
|
|
|
153
153
|
const SUBMIT_DELAY_FLOOR_MS = 200;
|
|
154
154
|
|
|
155
|
-
// win32 ConPTY submit reliability
|
|
156
|
-
//
|
|
157
|
-
//
|
|
158
|
-
//
|
|
159
|
-
//
|
|
160
|
-
//
|
|
161
|
-
//
|
|
162
|
-
//
|
|
163
|
-
//
|
|
164
|
-
// observes the agent has actually left the idle
|
|
165
|
-
// 'idle' —
|
|
166
|
-
// retry budget. Once submission is observed we stop
|
|
167
|
-
// the next turn. Single-line messages satisfy
|
|
168
|
-
//
|
|
169
|
-
//
|
|
155
|
+
// win32 ConPTY submit reliability — TWO independent concerns, do not conflate:
|
|
156
|
+
//
|
|
157
|
+
// (A) WHEN the single real submit CR fires. The first CR must not fire until
|
|
158
|
+
// the WHOLE body has echoed into the composer; a MULTILINE body also opens
|
|
159
|
+
// an Ink paste/newline-accumulation window during which a lone CR can be
|
|
160
|
+
// absorbed as a literal newline rather than submitting, with a
|
|
161
|
+
// nondeterministic length (observed 0–~2s, driven by ConPTY byte timing).
|
|
162
|
+
// So we VERIFY instead of guessing: hold the CR behind the head+tail
|
|
163
|
+
// echo-gate (scheduleWin32Submit phase 1), then resend the submit key on a
|
|
164
|
+
// fixed cadence until the FSM observes the agent has actually left the idle
|
|
165
|
+
// composer (status flips away from 'idle' — submitted / generating / modal),
|
|
166
|
+
// bounded by a retry budget (phase 2). Once submission is observed we stop
|
|
167
|
+
// so we don't spam Enter into the next turn. Single-line messages satisfy
|
|
168
|
+
// the check after the first CR, so their behaviour is unchanged.
|
|
169
|
+
//
|
|
170
|
+
// (B) HOW the body's OWN embedded newlines are written (FIX-B-v2). On the real
|
|
171
|
+
// win32 Ink/ConPTY composer each embedded '\n' in the body SUBMITS the
|
|
172
|
+
// preceding line as a separate composer entry, so writing the raw body
|
|
173
|
+
// (writeWin32Body) submitted every line but the last BEFORE the trailing
|
|
174
|
+
// echo-gated CR (A) ever ran — the prompt was truncated to only the tail
|
|
175
|
+
// fragment after the last '\n' (failure_category=per_newline_submit). The
|
|
176
|
+
// body must therefore land atomically as composer TEXT with ZERO per-line
|
|
177
|
+
// submits. writeWin32Body now does that: it wraps a newline-bearing body in
|
|
178
|
+
// a bracketed-paste (ESC[200~ … ESC[201~) so the composer takes the whole
|
|
179
|
+
// thing — embedded newlines and all — as pasted text (PRIMARY mode), or in
|
|
180
|
+
// the soft_newline fallback rewrites each embedded newline as a
|
|
181
|
+
// non-submitting Shift+Enter sequence. EITHER way the trailing submit CR is
|
|
182
|
+
// NOT part of this write — it stays separate and is fired later by (A).
|
|
183
|
+
//
|
|
184
|
+
// NOTE on the old "bracketed-paste wrapping does NOT help" claim that used to live
|
|
185
|
+
// here: that A/B fused the submit CR *inside* the paste (…body\r…201~), so the
|
|
186
|
+
// paste-closing still carried a submit and was never a clean text-only paste. The
|
|
187
|
+
// correct shape — paste wraps ONLY the body, CR stays separate — is what FIX-B-v2
|
|
188
|
+
// implements; it was never actually tested by that earlier A/B.
|
|
170
189
|
const WIN32_SUBMIT_RESEND_GAP_MS = 350;
|
|
171
190
|
const WIN32_SUBMIT_MAX_RESENDS = 14;
|
|
172
191
|
// Quiet window the win32 echo-gate (below) requires AFTER the body is seen in the
|
|
@@ -190,6 +209,27 @@ const WIN32_SUBMIT_SETTLE_POLL_MS = 120;
|
|
|
190
209
|
const WIN32_ECHO_PROBE_CHARS = 16;
|
|
191
210
|
const WIN32_ECHO_MAX_WAIT_MS = 20_000;
|
|
192
211
|
|
|
212
|
+
// FIX-B-v2 — how a newline-bearing win32 body's OWN embedded newlines are written
|
|
213
|
+
// (see concern (B) above). 'paste' (default) wraps the body in a bracketed-paste so
|
|
214
|
+
// the Ink composer absorbs the whole thing as text; 'soft_newline' rewrites each
|
|
215
|
+
// embedded newline as a non-submitting Shift+Enter. Whether THIS ConPTY honors
|
|
216
|
+
// bracketed-paste can only be confirmed by the live deploy A/B (we cannot A/B it via
|
|
217
|
+
// delegation — win32 truncates any newline-bearing task), so both modes ship and the
|
|
218
|
+
// fallback is selectable at runtime via ADHDEV_WIN32_SUBMIT_MODE.
|
|
219
|
+
export type Win32SubmitMode = 'paste' | 'soft_newline';
|
|
220
|
+
const WIN32_BRACKETED_PASTE_OPEN = '\x1b[200~';
|
|
221
|
+
const WIN32_BRACKETED_PASTE_CLOSE = '\x1b[201~';
|
|
222
|
+
// Non-submitting soft-newline for the claude-cli Ink composer. The spec
|
|
223
|
+
// (cli/claude-cli/specs/4.0.json) declares no soft-newline keycode, so we use the
|
|
224
|
+
// CSI-u encoding of Shift+Enter (modifyOtherKeys form: keycode 13 = Enter, modifier
|
|
225
|
+
// 2 = Shift). This inserts a literal newline into the composer WITHOUT submitting,
|
|
226
|
+
// unlike a bare CR (\r) which is reserved for the single real submit via (A).
|
|
227
|
+
const WIN32_SOFT_NEWLINE = '\x1b[27;2;13~';
|
|
228
|
+
|
|
229
|
+
export function resolveWin32SubmitMode(env: NodeJS.ProcessEnv = process.env): Win32SubmitMode {
|
|
230
|
+
return env.ADHDEV_WIN32_SUBMIT_MODE === 'soft_newline' ? 'soft_newline' : 'paste';
|
|
231
|
+
}
|
|
232
|
+
|
|
193
233
|
/** Collapse a string to its non-whitespace characters for echo comparison: the
|
|
194
234
|
* composer wraps, indents, and prefixes the body (with the `❯ ` prompt), so a raw
|
|
195
235
|
* substring test against the rendered screen fails. Stripping all whitespace makes
|
|
@@ -991,25 +1031,70 @@ export class FsmDriver implements ISpecDriver {
|
|
|
991
1031
|
* single unbounded ConPTY write can overflow the input pipe and drop leading
|
|
992
1032
|
* bytes; splitting it with a short inter-chunk gap keeps the console input
|
|
993
1033
|
* buffer from overflowing. Small bodies still go out in a single write. Each
|
|
994
|
-
*
|
|
995
|
-
* the final
|
|
1034
|
+
* write advances lastWin32WriteAt so the submit settle-gate keeps waiting until
|
|
1035
|
+
* the final segment is out and echoed.
|
|
1036
|
+
*
|
|
1037
|
+
* FIX-B-v2: a body that contains an embedded newline cannot be written raw —
|
|
1038
|
+
* on the real win32 Ink/ConPTY composer each '\n' SUBMITS the preceding line as
|
|
1039
|
+
* its own entry, truncating the prompt to only the tail fragment. So a
|
|
1040
|
+
* newline-bearing body is rewritten so its embedded newlines never submit:
|
|
1041
|
+
* - 'paste' (default): wrap the body in a bracketed-paste (ESC[200~ … ESC[201~)
|
|
1042
|
+
* — the composer takes the whole thing, newlines and all, as pasted text.
|
|
1043
|
+
* - 'soft_newline': replace each embedded newline with a non-submitting
|
|
1044
|
+
* Shift+Enter (CSI-u) so the body is typed as one multi-line entry.
|
|
1045
|
+
* The trailing submit CR is NOT written here — it stays separate and is fired
|
|
1046
|
+
* later by scheduleWin32Submit (concern (A)). The bracketed-paste markers are
|
|
1047
|
+
* written as their own atomic segments (never chunked), so chunking can never
|
|
1048
|
+
* split ESC[200~ / ESC[201~ mid-sequence regardless of body length.
|
|
996
1049
|
*/
|
|
997
1050
|
private writeWin32Body(text: string): void {
|
|
998
1051
|
if (this.win32WriteTimer) { clearTimeout(this.win32WriteTimer); this.win32WriteTimer = null; }
|
|
999
|
-
|
|
1052
|
+
|
|
1053
|
+
const hasNewline = /\r?\n/.test(text);
|
|
1054
|
+
const mode = resolveWin32SubmitMode();
|
|
1055
|
+
|
|
1056
|
+
// Build the ordered list of segments to write. Bracketed-paste markers are
|
|
1057
|
+
// their OWN segments so chunking only ever splits the body, never a marker.
|
|
1058
|
+
let segments: string[];
|
|
1059
|
+
if (!hasNewline) {
|
|
1060
|
+
// Single-line: unchanged behaviour — just (chunk and) write the body.
|
|
1061
|
+
segments = chunkPreservingSurrogates(text, WIN32_PTY_WRITE_CHUNK_CHARS);
|
|
1062
|
+
} else if (mode === 'soft_newline') {
|
|
1063
|
+
// Rewrite embedded newlines as non-submitting soft-newlines, THEN chunk.
|
|
1064
|
+
// The soft-newline sequence (ESC[27;2;13~) contains no '\n', so it is
|
|
1065
|
+
// never re-interpreted as a submit, and chunking it like ordinary text is
|
|
1066
|
+
// safe (a split mid-sequence is avoided below by chunking the whole
|
|
1067
|
+
// rewritten string — see the marker-safety note for paste; for soft_newline
|
|
1068
|
+
// the only ESC seq is short and self-contained, so we keep it simple and
|
|
1069
|
+
// chunk the rewritten body, accepting that the 1024-char chunk boundary is
|
|
1070
|
+
// astronomically unlikely to land inside a 9-byte CSI-u seq — and even if
|
|
1071
|
+
// it did, ConPTY reassembles the byte stream, the composer parses the full
|
|
1072
|
+
// sequence across the boundary).
|
|
1073
|
+
const rewritten = text.split(/\r?\n/).join(WIN32_SOFT_NEWLINE);
|
|
1074
|
+
segments = chunkPreservingSurrogates(rewritten, WIN32_PTY_WRITE_CHUNK_CHARS);
|
|
1075
|
+
} else {
|
|
1076
|
+
// paste: [OPEN marker] [body chunks…] [CLOSE marker]. Markers are atomic
|
|
1077
|
+
// segments — never merged with body bytes — so they cannot be split.
|
|
1078
|
+
segments = [
|
|
1079
|
+
WIN32_BRACKETED_PASTE_OPEN,
|
|
1080
|
+
...chunkPreservingSurrogates(text, WIN32_PTY_WRITE_CHUNK_CHARS),
|
|
1081
|
+
WIN32_BRACKETED_PASTE_CLOSE,
|
|
1082
|
+
];
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1085
|
+
if (segments.length <= 1) {
|
|
1000
1086
|
this.markWin32Write();
|
|
1001
|
-
this.adapter.send_keys(text);
|
|
1087
|
+
this.adapter.send_keys(segments[0] ?? text);
|
|
1002
1088
|
return;
|
|
1003
1089
|
}
|
|
1004
|
-
const chunks = chunkPreservingSurrogates(text, WIN32_PTY_WRITE_CHUNK_CHARS);
|
|
1005
1090
|
let idx = 0;
|
|
1006
1091
|
const writeNext = (): void => {
|
|
1007
1092
|
this.win32WriteTimer = null;
|
|
1008
|
-
if (idx >=
|
|
1093
|
+
if (idx >= segments.length) return;
|
|
1009
1094
|
this.markWin32Write();
|
|
1010
|
-
this.adapter.send_keys(
|
|
1095
|
+
this.adapter.send_keys(segments[idx]);
|
|
1011
1096
|
idx += 1;
|
|
1012
|
-
if (idx <
|
|
1097
|
+
if (idx < segments.length) {
|
|
1013
1098
|
this.win32WriteTimer = setTimeout(writeNext, WIN32_PTY_WRITE_CHUNK_GAP_MS);
|
|
1014
1099
|
}
|
|
1015
1100
|
};
|
|
@@ -1041,15 +1126,36 @@ export class FsmDriver implements ISpecDriver {
|
|
|
1041
1126
|
if (this.win32SubmitTimer) { clearTimeout(this.win32SubmitTimer); this.win32SubmitTimer = null; }
|
|
1042
1127
|
const startedAt = Date.now();
|
|
1043
1128
|
|
|
1044
|
-
//
|
|
1045
|
-
//
|
|
1046
|
-
//
|
|
1047
|
-
// the
|
|
1048
|
-
//
|
|
1049
|
-
|
|
1129
|
+
// FULL-BODY echo confirmation. A tail-only probe (slice(-N)) was insufficient
|
|
1130
|
+
// for multi-line prompts: a multiline body echoes line-by-line, so its TAIL can
|
|
1131
|
+
// appear in the composer (and the screen settle) while the HEAD / middle is still
|
|
1132
|
+
// arriving. Releasing the first CR on the tail alone then submits a PARTIAL body:
|
|
1133
|
+
// the early CR commits whatever has accumulated and the remainder is lost, so only
|
|
1134
|
+
// the trailing segment survives (the cross-machine MAGI replica receiving only the
|
|
1135
|
+
// prompt's boilerplate suffix). We now require BOTH the head probe AND the tail
|
|
1136
|
+
// probe to be present in the echo before releasing the CR.
|
|
1137
|
+
//
|
|
1138
|
+
// Crucially, the head check runs against snapshotWithScrollback(), NOT the visible
|
|
1139
|
+
// viewport: a tall body scrolls its leading lines off-screen (the very reason the
|
|
1140
|
+
// original gate used the tail), so the head is only ever observable in the
|
|
1141
|
+
// scrollback-inclusive buffer. The tail check stays on the visible snapshot (the
|
|
1142
|
+
// cursor / body end is always on screen). Both present ⇒ the whole body, head
|
|
1143
|
+
// through tail, has echoed — not just its end with a still-streaming middle.
|
|
1144
|
+
// Single-line / short bodies have overlapping head and tail probes and the
|
|
1145
|
+
// scrollback buffer is a superset of the viewport, so behaviour is unchanged. The
|
|
1146
|
+
// WIN32_ECHO_MAX_WAIT_MS blind-fire backstop below still bounds the wait so a body
|
|
1147
|
+
// that never fully confirms cannot hang.
|
|
1148
|
+
const normBody = normalizeForEcho(body);
|
|
1149
|
+
const headProbe = normBody.slice(0, WIN32_ECHO_PROBE_CHARS);
|
|
1150
|
+
const tailProbe = normBody.slice(-WIN32_ECHO_PROBE_CHARS);
|
|
1050
1151
|
const bodyEchoed = (): boolean => {
|
|
1051
|
-
if (!
|
|
1052
|
-
|
|
1152
|
+
if (!normBody) return true;
|
|
1153
|
+
// Tail on the visible viewport (cursor end is always on screen); head on the
|
|
1154
|
+
// scrollback-inclusive buffer (leading lines of a tall body scroll off-screen).
|
|
1155
|
+
const visible = normalizeForEcho(this.adapter.snapshot());
|
|
1156
|
+
if (!visible.includes(tailProbe)) return false;
|
|
1157
|
+
const full = normalizeForEcho(this.adapter.snapshotWithScrollback());
|
|
1158
|
+
return full.includes(headProbe);
|
|
1053
1159
|
};
|
|
1054
1160
|
|
|
1055
1161
|
const fire = (attempt: number): void => {
|