tickmarkr 1.76.0 → 1.78.0
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/adapters/kimi.js +25 -4
- package/dist/adapters/types.d.ts +5 -0
- package/dist/adapters/types.js +4 -1
- package/dist/drivers/herdr.d.ts +15 -1
- package/dist/drivers/herdr.js +129 -16
- package/dist/run/daemon.js +63 -3
- package/package.json +1 -1
package/dist/adapters/kimi.js
CHANGED
|
@@ -137,16 +137,37 @@ export function confirmKimiSeedBanner(banner, assignedModel) {
|
|
|
137
137
|
}
|
|
138
138
|
return { ok: true, sessionId: kimiBannerSessionId(banner) };
|
|
139
139
|
}
|
|
140
|
-
|
|
141
|
-
|
|
140
|
+
const kimiTuiLaunch = (model) => `kimi -y -m ${shq(model)}`;
|
|
141
|
+
const KIMI_ANSI_SGR_RE = /\u001B\[[0-9;]*m/g;
|
|
142
|
+
const KIMI_EDITOR_TOP_RE = /^╭─+╮$/;
|
|
143
|
+
const KIMI_EDITOR_INPUT_RE = /^│ > .*│$/;
|
|
144
|
+
const KIMI_EDITOR_EMPTY_RE = /^│ > +│$/;
|
|
145
|
+
const KIMI_EDITOR_BOTTOM_RE = /^╰─+╯$/;
|
|
146
|
+
function matchesKimiEditorBox(paneText, empty) {
|
|
147
|
+
const lines = paneText.replace(KIMI_ANSI_SGR_RE, "").split("\n");
|
|
148
|
+
const input = empty ? KIMI_EDITOR_EMPTY_RE : KIMI_EDITOR_INPUT_RE;
|
|
149
|
+
return lines.some((line, index) => input.test(line)
|
|
150
|
+
&& index > 0
|
|
151
|
+
&& index + 1 < lines.length
|
|
152
|
+
&& KIMI_EDITOR_TOP_RE.test(lines[index - 1])
|
|
153
|
+
&& KIMI_EDITOR_BOTTOM_RE.test(lines[index + 1]));
|
|
154
|
+
}
|
|
155
|
+
// v1.75 T1 / OBS-136: Kimi's actual editor is a three-line bordered box whose content row starts
|
|
156
|
+
// `│ > `. The welcome panel separately contains "Send /help..." and is only launch-liveness
|
|
157
|
+
// evidence (OBS-142), so it must never satisfy this declaration. The adapter owns the full matcher,
|
|
158
|
+
// empty-state matcher, launch distinction, and bounded cold-start window; the driver stays generic.
|
|
142
159
|
export const KIMI_INPUT_BOX = declareInputBox("kimi", {
|
|
143
|
-
fingerprint: "
|
|
160
|
+
fingerprint: "│ > ",
|
|
161
|
+
match: (paneText) => matchesKimiEditorBox(paneText, false),
|
|
162
|
+
emptyMatch: (paneText) => matchesKimiEditorBox(paneText, true),
|
|
163
|
+
launchCommand: (command) => command.startsWith("kimi -y -m "),
|
|
164
|
+
readinessTimeoutMs: 15_000,
|
|
144
165
|
});
|
|
145
166
|
// Shared launch-then-seed surface (T6) + banner confirm (T7/T2). One definition so the adapter
|
|
146
167
|
// property and the daemon's generic runInteractiveSeed path cannot drift.
|
|
147
168
|
const KIMI_SEED = {
|
|
148
169
|
// v1.76 T2 / OBS-141: 0.29.0 resolves only the full config.toml model key on the first turn.
|
|
149
|
-
launch:
|
|
170
|
+
launch: kimiTuiLaunch,
|
|
150
171
|
readinessMatch: "Send /help for help information.",
|
|
151
172
|
seedLine: (promptFile) => `Read ${promptFile} and do exactly what it says.`,
|
|
152
173
|
confirmBanner: confirmKimiSeedBanner,
|
package/dist/adapters/types.d.ts
CHANGED
|
@@ -84,10 +84,15 @@ export interface TrustDialog {
|
|
|
84
84
|
export declare function matchesTrustDialog(paneText: string, dialog: TrustDialog): boolean;
|
|
85
85
|
export interface InputBox {
|
|
86
86
|
fingerprint: string;
|
|
87
|
+
match?(paneText: string): boolean;
|
|
88
|
+
emptyMatch?(paneText: string): boolean;
|
|
89
|
+
launchCommand?(command: string): boolean;
|
|
90
|
+
readinessTimeoutMs?: number;
|
|
87
91
|
}
|
|
88
92
|
export declare function declareInputBox(adapterId: string, inputBox: InputBox): InputBox;
|
|
89
93
|
export declare function declaredInputBoxForWorkerName(workerName: string): InputBox | undefined;
|
|
90
94
|
export declare function matchesInputBox(paneText: string, inputBox: InputBox): boolean;
|
|
95
|
+
export declare function matchesEmptyInputBox(paneText: string, inputBox: InputBox): boolean;
|
|
91
96
|
export interface WorkerAdapter {
|
|
92
97
|
id: string;
|
|
93
98
|
vendor: string;
|
package/dist/adapters/types.js
CHANGED
|
@@ -40,7 +40,10 @@ export function declaredInputBoxForWorkerName(workerName) {
|
|
|
40
40
|
return adapterId === undefined ? undefined : inputBoxes.get(adapterId);
|
|
41
41
|
}
|
|
42
42
|
export function matchesInputBox(paneText, inputBox) {
|
|
43
|
-
return paneText.includes(inputBox.fingerprint);
|
|
43
|
+
return inputBox.match?.(paneText) ?? paneText.includes(inputBox.fingerprint);
|
|
44
|
+
}
|
|
45
|
+
export function matchesEmptyInputBox(paneText, inputBox) {
|
|
46
|
+
return inputBox.emptyMatch?.(paneText) === true;
|
|
44
47
|
}
|
|
45
48
|
export function channelsFromConfig(adapterId, cfg) {
|
|
46
49
|
const e = cfg.tiers[adapterId];
|
package/dist/drivers/herdr.d.ts
CHANGED
|
@@ -2,11 +2,22 @@ import { type ExecutorDriver, type NotifyOpts, type Slot, type SlotOpts } from "
|
|
|
2
2
|
export declare const TRAILER_SAFE_FLOOR_COLS = 108;
|
|
3
3
|
export declare const TRAILER_WIDTH_MARGIN = 2;
|
|
4
4
|
export declare const DELIVERY_ATTEMPTS = 3;
|
|
5
|
+
export interface HerdrTimeSource {
|
|
6
|
+
now: () => number;
|
|
7
|
+
sleep: (ms: number) => Promise<void>;
|
|
8
|
+
}
|
|
9
|
+
export declare class DeliveryReadinessError extends Error {
|
|
10
|
+
readonly waitedMs: number;
|
|
11
|
+
readonly transcript: string;
|
|
12
|
+
readonly phase = "READINESS";
|
|
13
|
+
constructor(waitedMs: number, transcript: string);
|
|
14
|
+
}
|
|
5
15
|
/** First-generation join direction from measured trailer-safe floor (43-MEASUREMENT.md). */
|
|
6
16
|
export declare function workerSplitDirection(paneCols: number | null, safeFloor?: number, margin?: number): "right" | "down";
|
|
7
17
|
export declare class HerdrDriver implements ExecutorDriver {
|
|
8
18
|
private bin;
|
|
9
19
|
private workersPerTab;
|
|
20
|
+
private time;
|
|
10
21
|
id: string;
|
|
11
22
|
interactive: boolean;
|
|
12
23
|
private groups;
|
|
@@ -18,12 +29,13 @@ export declare class HerdrDriver implements ExecutorDriver {
|
|
|
18
29
|
private ws;
|
|
19
30
|
private callerPane;
|
|
20
31
|
private watches;
|
|
21
|
-
constructor(bin?: string, workersPerTab?: number);
|
|
32
|
+
constructor(bin?: string, workersPerTab?: number, time?: HerdrTimeSource);
|
|
22
33
|
private serial;
|
|
23
34
|
private deliveryQueue;
|
|
24
35
|
private reserveDispatch;
|
|
25
36
|
private verifyPaneIdentityBinding;
|
|
26
37
|
private deliveryMatches;
|
|
38
|
+
private shellExecutionEchoed;
|
|
27
39
|
private submissionRegistered;
|
|
28
40
|
static available(): boolean;
|
|
29
41
|
private herdr;
|
|
@@ -41,8 +53,10 @@ export declare class HerdrDriver implements ExecutorDriver {
|
|
|
41
53
|
private joinGroup;
|
|
42
54
|
run(slot: Slot, cmd: string): Promise<void>;
|
|
43
55
|
private deliver;
|
|
56
|
+
private deliverPersistentShellCommand;
|
|
44
57
|
private submitVerifiedDelivery;
|
|
45
58
|
private settleDeliveryLine;
|
|
59
|
+
private awaitDeliveryReadiness;
|
|
46
60
|
private deliveryReadMatches;
|
|
47
61
|
private waitOk;
|
|
48
62
|
waitOutput(slot: Slot, pattern: string, timeoutMs: number, opts?: {
|
package/dist/drivers/herdr.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { declaredInputBoxForWorkerName, matchesInputBox, shq } from "../adapters/types.js";
|
|
1
|
+
import { declaredInputBoxForWorkerName, matchesEmptyInputBox, matchesInputBox, shq } from "../adapters/types.js";
|
|
2
2
|
import { PANE_IDENTITY_ENV, paneIdentityLine } from "../brand.js";
|
|
3
3
|
import { createWorktree, sh } from "../run/git.js";
|
|
4
4
|
import { herdrSealShellPrefix } from "./subprocess.js";
|
|
@@ -13,6 +13,25 @@ const DELIVERY_VERIFY_TIMEOUT_MS = 2000; // per attempt — a paste that hasn't
|
|
|
13
13
|
const DELIVERY_READ_LINES = 80;
|
|
14
14
|
const DELIVERY_SETTLE_READ_ATTEMPTS = 6;
|
|
15
15
|
const DELIVERY_SETTLE_POLL_MS = 100;
|
|
16
|
+
const DELIVERY_READINESS_TIMEOUT_MS = 1_000;
|
|
17
|
+
const SYSTEM_TIME = {
|
|
18
|
+
now: () => Date.now(),
|
|
19
|
+
sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
|
|
20
|
+
};
|
|
21
|
+
// OBS-142: a typed identity lets the daemon's next task distinguish cold-start variance from
|
|
22
|
+
// structural driver faults without parsing prose. The message also carries the bounded wait and
|
|
23
|
+
// final pane evidence so a terminal failure is diagnosable on its own.
|
|
24
|
+
export class DeliveryReadinessError extends Error {
|
|
25
|
+
waitedMs;
|
|
26
|
+
transcript;
|
|
27
|
+
phase = "READINESS";
|
|
28
|
+
constructor(waitedMs, transcript) {
|
|
29
|
+
super(`herdr delivery READINESS failed after ${waitedMs}ms — interface never became interactive (OBS-142); pane transcript:\n${transcript}`);
|
|
30
|
+
this.waitedMs = waitedMs;
|
|
31
|
+
this.transcript = transcript;
|
|
32
|
+
this.name = "DeliveryReadinessError";
|
|
33
|
+
}
|
|
34
|
+
}
|
|
16
35
|
/** First-generation join direction from measured trailer-safe floor (43-MEASUREMENT.md). */
|
|
17
36
|
export function workerSplitDirection(paneCols, safeFloor = TRAILER_SAFE_FLOOR_COLS, margin = TRAILER_WIDTH_MARGIN) {
|
|
18
37
|
if (paneCols == null || paneCols <= 0)
|
|
@@ -22,6 +41,7 @@ export function workerSplitDirection(paneCols, safeFloor = TRAILER_SAFE_FLOOR_CO
|
|
|
22
41
|
export class HerdrDriver {
|
|
23
42
|
bin;
|
|
24
43
|
workersPerTab;
|
|
44
|
+
time;
|
|
25
45
|
id = "herdr";
|
|
26
46
|
interactive = true;
|
|
27
47
|
groups = new Map();
|
|
@@ -40,9 +60,10 @@ export class HerdrDriver {
|
|
|
40
60
|
ws = process.env.HERDR_WORKSPACE_ID;
|
|
41
61
|
callerPane = process.env.HERDR_PANE_ID;
|
|
42
62
|
watches = new Map();
|
|
43
|
-
constructor(bin = "herdr", workersPerTab = 3) {
|
|
63
|
+
constructor(bin = "herdr", workersPerTab = 3, time = SYSTEM_TIME) {
|
|
44
64
|
this.bin = bin;
|
|
45
65
|
this.workersPerTab = workersPerTab;
|
|
66
|
+
this.time = time;
|
|
46
67
|
}
|
|
47
68
|
serial(fn) {
|
|
48
69
|
const p = this.groupSerial.then(fn, fn);
|
|
@@ -96,17 +117,38 @@ export class HerdrDriver {
|
|
|
96
117
|
const needle = norm(cmd);
|
|
97
118
|
return needle.length > 0 && hay.includes(needle);
|
|
98
119
|
}
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
120
|
+
shellExecutionEchoed(transcript, cmd) {
|
|
121
|
+
const norm = (s) => s.replace(/\u001B\[[0-9;?]*[ -/]*[@-~]/g, "").replace(/\s+/g, " ").trim();
|
|
122
|
+
const needle = norm(cmd);
|
|
123
|
+
if (!needle)
|
|
124
|
+
return false;
|
|
125
|
+
return transcript.split("\n").some((rawLine) => {
|
|
126
|
+
const line = norm(rawLine);
|
|
127
|
+
if (line === needle || !line.endsWith(needle))
|
|
128
|
+
return false;
|
|
129
|
+
const prefix = line.slice(0, -needle.length).trimEnd();
|
|
130
|
+
if (/[│┃╭╰┌└]/.test(prefix))
|
|
131
|
+
return false;
|
|
132
|
+
return /^(?:[$%#>]|[➜❯❱›»λ])(?:\s|$)/.test(prefix)
|
|
133
|
+
|| /[$%#>✗❯❱›»λ]$/.test(prefix);
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
// Submission requires positive post-Enter evidence. A prompt echoed above a fresh input target
|
|
137
|
+
// proves registration; a declared box that is visibly empty after the verified paste also proves
|
|
138
|
+
// it consumed the prompt. At the launch stage, a structurally prompt-prefixed shell execution echo
|
|
139
|
+
// is stronger evidence than waiting for first paint from the launched interface (OBS-144).
|
|
140
|
+
// Prompt absence alone is never success (OBS-142).
|
|
103
141
|
submissionRegistered(transcript, cmd, inputBox) {
|
|
104
142
|
const norm = (s) => s.replace(/\s+/g, "");
|
|
105
143
|
const hay = norm(transcript);
|
|
106
144
|
const needle = norm(cmd);
|
|
107
145
|
const promptAt = hay.lastIndexOf(needle);
|
|
108
|
-
if (needle.length === 0
|
|
146
|
+
if (needle.length === 0)
|
|
147
|
+
return false;
|
|
148
|
+
if (inputBox?.launchCommand?.(cmd) === true && this.shellExecutionEchoed(transcript, cmd))
|
|
109
149
|
return true;
|
|
150
|
+
if (promptAt < 0)
|
|
151
|
+
return inputBox !== undefined && matchesEmptyInputBox(transcript, inputBox);
|
|
110
152
|
if (inputBox && matchesInputBox(transcript, inputBox)) {
|
|
111
153
|
return hay.lastIndexOf(norm(inputBox.fingerprint)) > promptAt;
|
|
112
154
|
}
|
|
@@ -409,7 +451,8 @@ export class HerdrDriver {
|
|
|
409
451
|
this.deliveredPanes.set(slot, paneId);
|
|
410
452
|
});
|
|
411
453
|
}
|
|
412
|
-
async deliver(slot, cmd, pane) {
|
|
454
|
+
async deliver(slot, cmd, pane, verifySubmission = true) {
|
|
455
|
+
const readiness = await this.awaitDeliveryReadiness(slot, cmd, pane);
|
|
413
456
|
let transcript = "";
|
|
414
457
|
for (let attempt = 0; attempt < DELIVERY_ATTEMPTS; attempt++) {
|
|
415
458
|
if (attempt > 0) {
|
|
@@ -436,7 +479,14 @@ export class HerdrDriver {
|
|
|
436
479
|
throw new Error(`herdr pane send-text failed: ${typed.stderr || typed.stdout}`);
|
|
437
480
|
const back = await this.herdr(`pane wait-output ${shq(pane)} --match ${shq(cmd)} --timeout ${DELIVERY_VERIFY_TIMEOUT_MS}`, slot.cwd, DELIVERY_VERIFY_TIMEOUT_MS + 15_000);
|
|
438
481
|
if (this.waitOk(back.code, back.stdout) || await this.deliveryReadMatches(pane, cmd, slot.cwd)) {
|
|
439
|
-
|
|
482
|
+
if (verifySubmission) {
|
|
483
|
+
await this.submitVerifiedDelivery(slot, cmd, pane, readiness);
|
|
484
|
+
}
|
|
485
|
+
else {
|
|
486
|
+
const enter = await this.herdr(`pane send-keys ${shq(pane)} Enter`, slot.cwd);
|
|
487
|
+
if (enter.code !== 0)
|
|
488
|
+
throw new Error(`herdr pane send-keys Enter failed: ${enter.stderr || enter.stdout}`);
|
|
489
|
+
}
|
|
440
490
|
return;
|
|
441
491
|
}
|
|
442
492
|
// capture the corrupted delivery BEFORE clearing it — the OBS-85 byte-level evidence
|
|
@@ -444,9 +494,23 @@ export class HerdrDriver {
|
|
|
444
494
|
}
|
|
445
495
|
throw new Error(`herdr delivery corrupted after ${DELIVERY_ATTEMPTS} attempts — enter never pressed (OBS-85); pane transcript:\n${transcript}`);
|
|
446
496
|
}
|
|
447
|
-
|
|
497
|
+
// The narrator launches a perpetual shell watch, not an adapter-backed input interface. Keep
|
|
498
|
+
// OBS-85's readiness + paste read-back and the historical single Enter, while worker/gate runs
|
|
499
|
+
// continue through positive post-Enter evidence in submitVerifiedDelivery.
|
|
500
|
+
async deliverPersistentShellCommand(slot, cmd) {
|
|
501
|
+
return this.deliveryQueue(async () => {
|
|
502
|
+
const pane = await this.paneId(slot);
|
|
503
|
+
await this.deliver(slot, cmd, pane, false);
|
|
504
|
+
this.deliveredPanes.set(slot, pane);
|
|
505
|
+
});
|
|
506
|
+
}
|
|
507
|
+
async submitVerifiedDelivery(slot, cmd, pane, readiness) {
|
|
448
508
|
let transcript = "";
|
|
449
509
|
const inputBox = this.inputBoxes.get(slot);
|
|
510
|
+
// The base window preserves OBS-140's bounded behavior. A slow readiness observation grants
|
|
511
|
+
// the same measured time once more for submit paint, capped by the readiness bound itself.
|
|
512
|
+
const baseVerifyMs = (DELIVERY_SETTLE_READ_ATTEMPTS - 1) * DELIVERY_SETTLE_POLL_MS;
|
|
513
|
+
const verifyWindowMs = Math.min(baseVerifyMs + readiness.timeoutMs, baseVerifyMs + readiness.waitedMs);
|
|
450
514
|
for (let attempt = 0; attempt < DELIVERY_SUBMIT_ATTEMPTS; attempt++) {
|
|
451
515
|
const enter = await this.herdr(`pane send-keys ${shq(pane)} Enter`, slot.cwd);
|
|
452
516
|
if (enter.code !== 0)
|
|
@@ -454,7 +518,7 @@ export class HerdrDriver {
|
|
|
454
518
|
// Reuse the existing settle-read window. A first-read success returns before any timer; only
|
|
455
519
|
// a prompt that still occupies the delivery target spends the bounded settle window. This
|
|
456
520
|
// verification always completes before a possible re-press, so a slow submit cannot duplicate.
|
|
457
|
-
const settled = await this.settleDeliveryLine(pane, slot.cwd, transcript, inputBox, (candidate) => this.submissionRegistered(candidate, cmd, inputBox));
|
|
521
|
+
const settled = await this.settleDeliveryLine(pane, slot.cwd, transcript, inputBox, (candidate) => this.submissionRegistered(candidate, cmd, inputBox), verifyWindowMs);
|
|
458
522
|
transcript = settled.transcript;
|
|
459
523
|
if (settled.ok)
|
|
460
524
|
return;
|
|
@@ -464,9 +528,12 @@ export class HerdrDriver {
|
|
|
464
528
|
}
|
|
465
529
|
throw new Error(`herdr delivery corrupted after ${DELIVERY_SUBMIT_ATTEMPTS} submit attempts — submission never registered (OBS-140); pane transcript:\n${transcript}`);
|
|
466
530
|
}
|
|
467
|
-
async settleDeliveryLine(pane, cwd, initialTranscript, inputBox, accept) {
|
|
531
|
+
async settleDeliveryLine(pane, cwd, initialTranscript, inputBox, accept, settleWindowMs) {
|
|
468
532
|
let transcript = initialTranscript;
|
|
469
|
-
|
|
533
|
+
const readAttempts = settleWindowMs === undefined
|
|
534
|
+
? DELIVERY_SETTLE_READ_ATTEMPTS
|
|
535
|
+
: Math.max(DELIVERY_SETTLE_READ_ATTEMPTS, Math.floor(settleWindowMs / DELIVERY_SETTLE_POLL_MS) + 1);
|
|
536
|
+
for (let readAttempt = 0; readAttempt < readAttempts; readAttempt++) {
|
|
470
537
|
const read = await this.herdr(`pane read ${shq(pane)} --source recent-unwrapped --lines ${DELIVERY_READ_LINES}`, cwd);
|
|
471
538
|
if (read.code !== 0)
|
|
472
539
|
return { ok: false, transcript: read.stdout || transcript, recognizedInputBox: false, readFailed: true };
|
|
@@ -485,12 +552,58 @@ export class HerdrDriver {
|
|
|
485
552
|
};
|
|
486
553
|
}
|
|
487
554
|
transcript = read.stdout;
|
|
488
|
-
if (readAttempt <
|
|
489
|
-
await
|
|
555
|
+
if (readAttempt < readAttempts - 1) {
|
|
556
|
+
await this.time.sleep(DELIVERY_SETTLE_POLL_MS);
|
|
490
557
|
}
|
|
491
558
|
}
|
|
492
559
|
return { ok: false, transcript, recognizedInputBox: false };
|
|
493
560
|
}
|
|
561
|
+
async awaitDeliveryReadiness(slot, cmd, pane) {
|
|
562
|
+
const inputBox = this.inputBoxes.get(slot);
|
|
563
|
+
const requireInputBox = inputBox !== undefined && inputBox.launchCommand?.(cmd) !== true;
|
|
564
|
+
const timeoutMs = inputBox?.readinessTimeoutMs ?? DELIVERY_READINESS_TIMEOUT_MS;
|
|
565
|
+
const started = this.time.now();
|
|
566
|
+
let previous;
|
|
567
|
+
let transcript = "";
|
|
568
|
+
let reads = 0;
|
|
569
|
+
while (true) {
|
|
570
|
+
const elapsedBeforeRead = this.time.now() - started;
|
|
571
|
+
const remainingBeforeRead = timeoutMs - elapsedBeforeRead;
|
|
572
|
+
if (remainingBeforeRead <= 0)
|
|
573
|
+
throw new DeliveryReadinessError(elapsedBeforeRead, transcript);
|
|
574
|
+
const read = await this.herdr(`pane read ${shq(pane)} --source recent-unwrapped --lines ${DELIVERY_READ_LINES}`, slot.cwd, Math.max(1, Math.ceil(remainingBeforeRead)));
|
|
575
|
+
reads++;
|
|
576
|
+
transcript = read.stdout || transcript;
|
|
577
|
+
const waitedMs = this.time.now() - started;
|
|
578
|
+
if (read.code !== 0) {
|
|
579
|
+
// Once at least one valid frame has been observed, a read that consumes the remainder of
|
|
580
|
+
// the readiness budget is the bounded window expiring, not a new submission/protocol
|
|
581
|
+
// identity. A first-read timeout and every non-timeout read failure remain structural.
|
|
582
|
+
if (read.timedOut && previous !== undefined && waitedMs >= timeoutMs) {
|
|
583
|
+
throw new DeliveryReadinessError(waitedMs, transcript);
|
|
584
|
+
}
|
|
585
|
+
const detail = read.stderr || read.stdout || `exit ${read.code}`;
|
|
586
|
+
throw new Error(`herdr pane read failed during readiness${read.timedOut ? " (timed out)" : ""}: ${detail}`);
|
|
587
|
+
}
|
|
588
|
+
if (previous !== undefined) {
|
|
589
|
+
const stableFrame = read.stdout === previous;
|
|
590
|
+
const targetReady = stableFrame && (requireInputBox
|
|
591
|
+
? matchesInputBox(previous, inputBox) && matchesInputBox(read.stdout, inputBox)
|
|
592
|
+
: !this.deliveryMatches(read.stdout, cmd));
|
|
593
|
+
if (targetReady)
|
|
594
|
+
return { waitedMs, timeoutMs, transcript: read.stdout };
|
|
595
|
+
}
|
|
596
|
+
previous = read.stdout;
|
|
597
|
+
const remaining = timeoutMs - waitedMs;
|
|
598
|
+
if (remaining <= 0)
|
|
599
|
+
throw new DeliveryReadinessError(waitedMs, transcript);
|
|
600
|
+
// The second read is immediate: an already-painted stable target proves readiness with no
|
|
601
|
+
// added delay. Only observed change spends a poll interval, and every path remains bounded.
|
|
602
|
+
if (reads > 1) {
|
|
603
|
+
await this.time.sleep(Math.min(DELIVERY_SETTLE_POLL_MS, remaining));
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
}
|
|
494
607
|
async deliveryReadMatches(pane, cmd, cwd) {
|
|
495
608
|
const read = await this.herdr(`pane read ${shq(pane)} --source recent-unwrapped --lines ${DELIVERY_READ_LINES}`, cwd);
|
|
496
609
|
return read.code === 0 && this.deliveryMatches(read.stdout, cmd);
|
|
@@ -680,7 +793,7 @@ export class HerdrDriver {
|
|
|
680
793
|
const s = await this.watchSlot(cwd, name);
|
|
681
794
|
this.watches.set(name, s);
|
|
682
795
|
try {
|
|
683
|
-
await this.
|
|
796
|
+
await this.deliverPersistentShellCommand(s, command);
|
|
684
797
|
}
|
|
685
798
|
catch (err) {
|
|
686
799
|
this.watches.delete(name);
|
package/dist/run/daemon.js
CHANGED
|
@@ -9,6 +9,7 @@ import { allAdapters, discoverChannels, getAdapter, probeAll, readDoctor } from
|
|
|
9
9
|
import { addUsage, channelKey, matchesTrustDialog, QUOTA_RE } from "../adapters/types.js";
|
|
10
10
|
import { bannerShell, paneDispatchCommand } from "../brand.js";
|
|
11
11
|
import { globalConfigDir, loadConfigWithMode, readOverlayFile, repoOverlayPath, } from "../config/config.js";
|
|
12
|
+
import { DeliveryReadinessError } from "../drivers/herdr.js";
|
|
12
13
|
import { herdrSealShellPrefix, SubprocessDriver } from "../drivers/subprocess.js";
|
|
13
14
|
import { formatOwnedName } from "../drivers/types.js";
|
|
14
15
|
import { captureBaseline, detectGateCommands, detectVacuousOracles } from "../gates/baseline.js";
|
|
@@ -833,6 +834,38 @@ export async function runDaemon(repoRoot, opts = {}) {
|
|
|
833
834
|
let earlyLaunchDead = false;
|
|
834
835
|
let settleParsed;
|
|
835
836
|
let seedResult;
|
|
837
|
+
const handleDeliveryReadiness = async (error) => {
|
|
838
|
+
journal.append("delivery-readiness-failed", t.id, {
|
|
839
|
+
attempt,
|
|
840
|
+
waitedMs: error.waitedMs,
|
|
841
|
+
transcript: error.transcript,
|
|
842
|
+
});
|
|
843
|
+
if (keepOpen)
|
|
844
|
+
keptSlots.push(slot);
|
|
845
|
+
else
|
|
846
|
+
await closeSlot(slot);
|
|
847
|
+
feedback = `delivery readiness failed after ${error.waitedMs}ms; pane transcript:\n${error.transcript}`;
|
|
848
|
+
const step = r.ladder[Math.min(ladderIdx++, r.ladder.length - 1)];
|
|
849
|
+
journal.append("escalation", t.id, { step, attempt: attempt + 1 });
|
|
850
|
+
await driver.notify(`tickmarkr ${runId}: ${t.id} escalation: ${step}`, { tier: "attention" });
|
|
851
|
+
if (step === "retry")
|
|
852
|
+
return true;
|
|
853
|
+
if (step === "escalate") {
|
|
854
|
+
const next = failover("escalate");
|
|
855
|
+
if (next) {
|
|
856
|
+
assignment = next;
|
|
857
|
+
tried.push(channelKey(next));
|
|
858
|
+
return true;
|
|
859
|
+
}
|
|
860
|
+
// no channel left — fall through to a consult
|
|
861
|
+
}
|
|
862
|
+
if (step === "escalate" || step === "consult") {
|
|
863
|
+
const v = await runConsult("delivery-readiness", error.transcript, feedback, []);
|
|
864
|
+
return applyVerdict(v, attempt + 1, "dispatch");
|
|
865
|
+
}
|
|
866
|
+
await park(t, "escalation ladder exhausted", "ladder-exhausted", assignment, attempt + 1, startMs, gateFails, consults, tokens, metered, retryMode);
|
|
867
|
+
return false;
|
|
868
|
+
};
|
|
836
869
|
if (interactive) {
|
|
837
870
|
// v1.2 interactive: the TUI doesn't exit on completion — the trailer is the finish line.
|
|
838
871
|
// The exit wrapper still fires if the TUI dies (crash/quit): fast-fail instead of burning the timeout.
|
|
@@ -842,11 +875,29 @@ export async function runDaemon(repoRoot, opts = {}) {
|
|
|
842
875
|
// v1.69 T6: launch the real TUI without a prompt, wait for readiness, inject one seed turn,
|
|
843
876
|
// then fall through to the normal trailer harvest. A failed seed is recorded as a finished
|
|
844
877
|
// failure rather than allowed to race the trailer wait.
|
|
845
|
-
|
|
878
|
+
try {
|
|
879
|
+
seedResult = await runInteractiveSeed({ driver, slot, adapter, assignment, promptFile, taskTimeoutMinutes });
|
|
880
|
+
}
|
|
881
|
+
catch (error) {
|
|
882
|
+
if (!(error instanceof DeliveryReadinessError))
|
|
883
|
+
throw error;
|
|
884
|
+
if (await handleDeliveryReadiness(error))
|
|
885
|
+
continue attempts;
|
|
886
|
+
return;
|
|
887
|
+
}
|
|
846
888
|
output = seedResult.output;
|
|
847
889
|
}
|
|
848
890
|
else {
|
|
849
|
-
|
|
891
|
+
try {
|
|
892
|
+
await driver.run(slot, paneDispatchCommand(dispatchScript));
|
|
893
|
+
}
|
|
894
|
+
catch (error) {
|
|
895
|
+
if (!(error instanceof DeliveryReadinessError))
|
|
896
|
+
throw error;
|
|
897
|
+
if (await handleDeliveryReadiness(error))
|
|
898
|
+
continue attempts;
|
|
899
|
+
return;
|
|
900
|
+
}
|
|
850
901
|
output = await driver.read(slot, 1000);
|
|
851
902
|
}
|
|
852
903
|
if (seedResult?.seedFailed) {
|
|
@@ -976,7 +1027,16 @@ export async function runDaemon(repoRoot, opts = {}) {
|
|
|
976
1027
|
}
|
|
977
1028
|
}
|
|
978
1029
|
else {
|
|
979
|
-
|
|
1030
|
+
try {
|
|
1031
|
+
await driver.run(slot, paneDispatchCommand(dispatchScript));
|
|
1032
|
+
}
|
|
1033
|
+
catch (error) {
|
|
1034
|
+
if (!(error instanceof DeliveryReadinessError))
|
|
1035
|
+
throw error;
|
|
1036
|
+
if (await handleDeliveryReadiness(error))
|
|
1037
|
+
continue attempts;
|
|
1038
|
+
return;
|
|
1039
|
+
}
|
|
980
1040
|
// OBS-54: headless workers have the same output-inactivity budget as visible panes.
|
|
981
1041
|
// v1.76: same monotonic-progress measure as the interactive site; harvest stays raw.
|
|
982
1042
|
const stallWindowMs = taskTimeoutMinutes * 60_000;
|