@ours.network/fleet 0.9.7 → 0.9.8
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/monitor.js +82 -5
- package/package.json +1 -1
package/dist/monitor.js
CHANGED
|
@@ -234,11 +234,73 @@ export function looksRunning(pane) {
|
|
|
234
234
|
return true; // "(12s · … tokens)" elapsed meter
|
|
235
235
|
return false;
|
|
236
236
|
}
|
|
237
|
-
|
|
237
|
+
const COMPOSER_TOP = /^[^\S\n]*[╭┌][─━]/;
|
|
238
|
+
const COMPOSER_BOTTOM = /^[^\S\n]*[╰└][─━]/;
|
|
239
|
+
const COMPOSER_PROMPT = /^[^\S\n]*(?:[│┃|][^\S\n]*)?[❯›>][^\S\n]*$/;
|
|
240
|
+
/**
|
|
241
|
+
* Remove wrapping-only whitespace and box chrome from composer rows. Notification
|
|
242
|
+
* lines contain no meaningful whitespace distinction, so this lets a fragment
|
|
243
|
+
* cross an arbitrary terminal wrap without matching unrelated transcript text.
|
|
244
|
+
*/
|
|
245
|
+
function normalizeComposerRows(rows) {
|
|
246
|
+
return rows.map((raw, i) => {
|
|
247
|
+
let row = raw;
|
|
248
|
+
if (i > 0)
|
|
249
|
+
row = row.replace(/^[^\S\n]*(?:[│┃|][^\S\n]*)?/, '');
|
|
250
|
+
row = row.replace(/[^\S\n]*(?:[│┃|])?[^\S\n]*$/, '');
|
|
251
|
+
return row;
|
|
252
|
+
}).join('').replace(/\s+/g, '');
|
|
253
|
+
}
|
|
254
|
+
/**
|
|
255
|
+
* Is the injected line still sitting unsubmitted in the composer?
|
|
256
|
+
*
|
|
257
|
+
* The footer has variable height and the line may wrap across any number of
|
|
258
|
+
* rows, so a fixed tail window cannot identify the composer. Prefer the final
|
|
259
|
+
* bordered composer region; for a borderless/truncated capture, require the
|
|
260
|
+
* notification prefix to follow a composer prompt. This keeps old submitted
|
|
261
|
+
* wake lines in the transcript from causing stray Enters.
|
|
262
|
+
*/
|
|
238
263
|
function stillInComposer(pane, line) {
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
264
|
+
if (!pane)
|
|
265
|
+
return false; // dead pane: do not waste Enters
|
|
266
|
+
const lines = pane.split('\n');
|
|
267
|
+
let bottom = -1;
|
|
268
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
269
|
+
if (COMPOSER_BOTTOM.test(lines[i])) {
|
|
270
|
+
bottom = i;
|
|
271
|
+
break;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
let top = -1;
|
|
275
|
+
if (bottom >= 0) {
|
|
276
|
+
for (let i = bottom - 1; i >= 0; i--) {
|
|
277
|
+
if (COMPOSER_TOP.test(lines[i])) {
|
|
278
|
+
top = i;
|
|
279
|
+
break;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
const from = top >= 0 ? top + 1 : 0;
|
|
284
|
+
const to = bottom >= 0 ? bottom : lines.length;
|
|
285
|
+
let wakeRow = -1;
|
|
286
|
+
let wakeColumn = -1;
|
|
287
|
+
for (let i = to - 1; i >= from; i--) {
|
|
288
|
+
const column = lines[i].lastIndexOf(PREFIX);
|
|
289
|
+
if (column >= 0) {
|
|
290
|
+
wakeRow = i;
|
|
291
|
+
wakeColumn = column;
|
|
292
|
+
break;
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
if (wakeRow < 0)
|
|
296
|
+
return false;
|
|
297
|
+
// Without both box boundaries, only trust text visibly in a composer prompt.
|
|
298
|
+
// This is the safe fallback for borderless TUIs and truncated captures.
|
|
299
|
+
if (top < 0 && !COMPOSER_PROMPT.test(lines[wakeRow].slice(0, wakeColumn)))
|
|
300
|
+
return false;
|
|
301
|
+
const rows = lines.slice(wakeRow, to);
|
|
302
|
+
rows[0] = rows[0].slice(wakeColumn);
|
|
303
|
+
return normalizeComposerRows(rows).includes(line.replace(/\s+/g, ''));
|
|
242
304
|
}
|
|
243
305
|
export class Monitor {
|
|
244
306
|
name;
|
|
@@ -412,18 +474,33 @@ export class Monitor {
|
|
|
412
474
|
// Verify submission for THIS line even if stop() arrives mid-flight: the text
|
|
413
475
|
// is already in the composer and we want it submitted (at-least-once). A truly
|
|
414
476
|
// dead pane makes safeCapture return '' ⇒ not-in-composer ⇒ breaks, no wasted Enter.
|
|
415
|
-
for (let i = 0; i < MAX_ENTER_RETRIES;
|
|
477
|
+
for (let i = 0; i < MAX_ENTER_RETRIES;) {
|
|
416
478
|
await this.deps.sleep(POST_VERIFY_MS);
|
|
417
479
|
const capture = await safeCapture(this.deps.tmux, this.name);
|
|
418
480
|
if (!capture.ok) {
|
|
419
481
|
this.degrade('delivery', 'capture failed during injection verification');
|
|
420
482
|
return false;
|
|
421
483
|
}
|
|
484
|
+
// A dialog can appear after the initial send. Never let a verification
|
|
485
|
+
// retry confirm it. Wait under the same bounded modal policy as initial
|
|
486
|
+
// injection, then re-capture immediately before considering Enter.
|
|
487
|
+
if (looksModal(capture.pane)) {
|
|
488
|
+
const state = await this.awaitInjectable(pid);
|
|
489
|
+
if (state === 'ready')
|
|
490
|
+
continue;
|
|
491
|
+
if (state === 'offline')
|
|
492
|
+
this.degrade('offline', 'offline during injection verification');
|
|
493
|
+
else if (state === 'modal')
|
|
494
|
+
this.degrade('modal', `modal wedge during injection verification — ` +
|
|
495
|
+
`no Enter sent for ${MODAL_GIVE_UP_MS / 1000}s`);
|
|
496
|
+
return false;
|
|
497
|
+
}
|
|
422
498
|
if (!stillInComposer(capture.pane, line)) {
|
|
423
499
|
delivered = true;
|
|
424
500
|
break;
|
|
425
501
|
}
|
|
426
502
|
await this.deps.tmux.sendKey(this.name, 'Enter');
|
|
503
|
+
i++;
|
|
427
504
|
}
|
|
428
505
|
if (!delivered) {
|
|
429
506
|
this.degrade('delivery', 'injection unverified');
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ours.network/fleet",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.8",
|
|
4
4
|
"description": "Harness-agnostic fleet of persistent, identity-bound AI agents. Declarative fleet.yaml, tmux or ACP sessions, supervision, and ours.network messaging.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "FSL-1.1-Apache-2.0",
|