@zswarm/core 0.2.7 → 0.2.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/harness.d.ts +13 -4
- package/dist/harness.js +29 -9
- package/dist/host/cli.js +16 -3
- package/dist/host/relay.d.ts +35 -0
- package/dist/host/relay.js +51 -5
- package/dist/ops/delivery.d.ts +30 -6
- package/dist/ops/delivery.js +71 -21
- package/dist/ops/dispatch.d.ts +1 -1
- package/dist/ops/dispatch.js +3 -3
- package/dist/ops/restart.d.ts +2 -0
- package/dist/ops/restart.js +36 -3
- package/dist/schema.js +8 -0
- package/package.json +1 -1
package/dist/harness.d.ts
CHANGED
|
@@ -37,15 +37,23 @@ export type HarnessProfile = {
|
|
|
37
37
|
/** Trailing lines showing the harness finished starting and is ready for input. */
|
|
38
38
|
ready: readonly RegExp[];
|
|
39
39
|
};
|
|
40
|
+
type HarnessName = Exclude<HarnessProfile["name"], "unknown">;
|
|
40
41
|
/**
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
|
|
42
|
+
* The stable, ordered list of known harness names. `--harness` validates
|
|
43
|
+
* against this and the schema help names it, so both stay in step with PROFILES.
|
|
44
|
+
*/
|
|
45
|
+
export declare const HARNESS_NAMES: readonly HarnessName[];
|
|
46
|
+
/**
|
|
47
|
+
* Identify the harness, case-insensitively. An explicit `name` is the override
|
|
48
|
+
* for wrapper-launched panes whose command/title hides the real harness: a
|
|
49
|
+
* non-empty name must be one of `HARNESS_NAMES` (else `bad_arg`). Without an
|
|
50
|
+
* override the pane's command wins (the executable that spawned it), then the
|
|
51
|
+
* title for panes launched from a generic shell, then `unknown`.
|
|
44
52
|
*/
|
|
45
53
|
export declare function resolveHarness(pane: {
|
|
46
54
|
command?: string | null;
|
|
47
55
|
title?: string | null;
|
|
48
|
-
}): HarnessProfile;
|
|
56
|
+
}, name?: string | null): HarnessProfile;
|
|
49
57
|
/**
|
|
50
58
|
* Does this screen's trailing lines show the harness's queued-message hint?
|
|
51
59
|
* Only the last few lines count: a quoted marker in scrollback is not a queue.
|
|
@@ -64,3 +72,4 @@ export declare function busyPrompt(screen: string, profile: HarnessProfile): boo
|
|
|
64
72
|
* must not make the restart op wait forever.
|
|
65
73
|
*/
|
|
66
74
|
export declare function readyPrompt(screen: string, profile: HarnessProfile): boolean;
|
|
75
|
+
export {};
|
package/dist/harness.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { ZellijError } from "./errors.js";
|
|
1
2
|
/** Command and title needles observed on real panes in one live session. */
|
|
2
3
|
const PROFILES = [
|
|
3
4
|
{ name: "codex", command: ["codex"], title: ["agent-codex"] },
|
|
@@ -7,6 +8,11 @@ const PROFILES = [
|
|
|
7
8
|
{ name: "pi", command: ["pi"], title: ["agent-pi"] },
|
|
8
9
|
{ name: "claude", command: ["claude"], title: ["agent-claude"] },
|
|
9
10
|
];
|
|
11
|
+
/**
|
|
12
|
+
* The stable, ordered list of known harness names. `--harness` validates
|
|
13
|
+
* against this and the schema help names it, so both stay in step with PROFILES.
|
|
14
|
+
*/
|
|
15
|
+
export const HARNESS_NAMES = PROFILES.map((p) => p.name);
|
|
10
16
|
/**
|
|
11
17
|
* Only codex needs a second Enter to submit: its composer keeps the pasted
|
|
12
18
|
* message (`› [zswarm from=...]`) until an extra Enter lands — verified live
|
|
@@ -170,15 +176,8 @@ function matchTitle(title) {
|
|
|
170
176
|
}
|
|
171
177
|
return null;
|
|
172
178
|
}
|
|
173
|
-
/**
|
|
174
|
-
|
|
175
|
-
* The command is the executable that spawned the pane, so it wins; the title
|
|
176
|
-
* is the fallback for panes launched from a generic shell. Never throws.
|
|
177
|
-
*/
|
|
178
|
-
export function resolveHarness(pane) {
|
|
179
|
-
const name = matchCommand(pane.command) ?? matchTitle(pane.title) ?? "unknown";
|
|
180
|
-
if (name === "unknown")
|
|
181
|
-
return UNKNOWN;
|
|
179
|
+
/** Assemble one known profile from the per-name tables, the one assembly path. */
|
|
180
|
+
function profileFor(name) {
|
|
182
181
|
return {
|
|
183
182
|
name,
|
|
184
183
|
submit: SUBMIT[name],
|
|
@@ -190,6 +189,27 @@ export function resolveHarness(pane) {
|
|
|
190
189
|
ready: READY[name],
|
|
191
190
|
};
|
|
192
191
|
}
|
|
192
|
+
/**
|
|
193
|
+
* Identify the harness, case-insensitively. An explicit `name` is the override
|
|
194
|
+
* for wrapper-launched panes whose command/title hides the real harness: a
|
|
195
|
+
* non-empty name must be one of `HARNESS_NAMES` (else `bad_arg`). Without an
|
|
196
|
+
* override the pane's command wins (the executable that spawned it), then the
|
|
197
|
+
* title for panes launched from a generic shell, then `unknown`.
|
|
198
|
+
*/
|
|
199
|
+
export function resolveHarness(pane, name) {
|
|
200
|
+
const override = name?.trim().toLowerCase();
|
|
201
|
+
if (override) {
|
|
202
|
+
const known = HARNESS_NAMES.find((candidate) => candidate === override);
|
|
203
|
+
if (!known) {
|
|
204
|
+
throw new ZellijError("bad_arg", `unknown harness "${name.trim()}"; known harnesses: ${HARNESS_NAMES.join(", ")}`);
|
|
205
|
+
}
|
|
206
|
+
return profileFor(known);
|
|
207
|
+
}
|
|
208
|
+
const inferred = matchCommand(pane.command) ?? matchTitle(pane.title) ?? "unknown";
|
|
209
|
+
if (inferred === "unknown")
|
|
210
|
+
return UNKNOWN;
|
|
211
|
+
return profileFor(inferred);
|
|
212
|
+
}
|
|
193
213
|
/**
|
|
194
214
|
* Does this screen's trailing lines show the harness's queued-message hint?
|
|
195
215
|
* Only the last few lines count: a quoted marker in scrollback is not a queue.
|
package/dist/host/cli.js
CHANGED
|
@@ -84,12 +84,23 @@ async function crew(argv) {
|
|
|
84
84
|
return 2;
|
|
85
85
|
}
|
|
86
86
|
}
|
|
87
|
+
/** The DELIVERY cell: the recorded verdict, a reply failure, or `-`. */
|
|
88
|
+
function deliveryLabel(delivery) {
|
|
89
|
+
if (!delivery)
|
|
90
|
+
return "-";
|
|
91
|
+
if (delivery.submitted)
|
|
92
|
+
return delivery.submitted;
|
|
93
|
+
if (delivery.note)
|
|
94
|
+
return `failed:${delivery.note}`;
|
|
95
|
+
return "-";
|
|
96
|
+
}
|
|
87
97
|
/** A plain-text table for `relay --list`, one row per relay. */
|
|
88
98
|
function relayTable(rows) {
|
|
89
|
-
const header = ["ID", "STATE", "TO", "SESSION", "AGE", "LAST CHECK", "JOB", "LEASE"];
|
|
99
|
+
const header = ["ID", "STATE", "DELIVERY", "TO", "SESSION", "AGE", "LAST CHECK", "JOB", "LEASE"];
|
|
90
100
|
const body = rows.map((r) => [
|
|
91
101
|
r.id,
|
|
92
102
|
r.state,
|
|
103
|
+
deliveryLabel(r.delivery),
|
|
93
104
|
r.to,
|
|
94
105
|
r.session ?? "-",
|
|
95
106
|
`${r.age}s`,
|
|
@@ -172,6 +183,7 @@ async function relay(argv) {
|
|
|
172
183
|
to: str(flags["--to"]) ?? "",
|
|
173
184
|
bodyFile: str(flags["--body-file"]),
|
|
174
185
|
from: str(flags["--from"]),
|
|
186
|
+
harness: str(flags["--harness"]),
|
|
175
187
|
done: str(flags["--done"]) ?? "",
|
|
176
188
|
replyTo: str(flags["--reply-to"]) ?? "",
|
|
177
189
|
replySession: str(flags["--reply-session"]),
|
|
@@ -245,16 +257,17 @@ const COMMANDS = {
|
|
|
245
257
|
relay: {
|
|
246
258
|
usage: [
|
|
247
259
|
"zswarm relay --to PANE --done REGEX --reply-to PANE [--serve TARGET] [--session S] [--body-file F]",
|
|
248
|
-
" [--reply-session S] [--from NAME] [--check SCRIPT] [--message TEMPLATE] [--id ID]",
|
|
260
|
+
" [--reply-session S] [--from NAME] [--harness NAME] [--check SCRIPT] [--message TEMPLATE] [--id ID]",
|
|
249
261
|
" [--timeout-min 240] [--max-age MIN] [--queue] [--force] [--foreground] [--if-idle]",
|
|
250
262
|
" send the body to PANE over serve (ZSWARM_SERVE / ZSWARM_SERVE_TOKEN), then a detached relay",
|
|
251
263
|
" waits for REGEX (and SCRIPT exit 0) and pastes a message into the local --reply-to pane;",
|
|
264
|
+
" --harness names the target harness and is passed on every send so delivery matches it;",
|
|
252
265
|
" --max-age (default --timeout-min + 60) is a hard end that always yields a final status",
|
|
253
266
|
" it leases its target pane until it ends; a second relay to a leased pane fails pane_leased",
|
|
254
267
|
" unless --queue waits for the lease to free or --force shares it",
|
|
255
268
|
" --if-idle applies send's busy check before the lease: a harness that shows it is working fails pane_busy",
|
|
256
269
|
" without sending; --queue then waits for the pane to go idle as well as for the lease, and --force skips the check",
|
|
257
|
-
"zswarm relay --list [--json] id, state, target, session, age, last check, job and lease per relay dir",
|
|
270
|
+
"zswarm relay --list [--json] id, state, delivery, target, session, age, last check, job and lease per relay dir",
|
|
258
271
|
"zswarm relay --leases [--json] live pane leases and the relay that holds each",
|
|
259
272
|
"zswarm relay --cancel ID | --cancel-all --to PANE stop the job, mark it CANCELLED, paste nothing",
|
|
260
273
|
"zswarm relay --wait ID [--timeout-min 240] block until relay ID has its message, print it (pull path)",
|
package/dist/host/relay.d.ts
CHANGED
|
@@ -25,6 +25,19 @@ export type RelayJob = {
|
|
|
25
25
|
unit?: string;
|
|
26
26
|
pid?: number;
|
|
27
27
|
};
|
|
28
|
+
/**
|
|
29
|
+
* The last delivery verdict, persisted so `relay --list` can show whether the
|
|
30
|
+
* body (and later the reply) actually landed. A verdict is not a failure: the
|
|
31
|
+
* relay keeps waiting for the done line after recording one.
|
|
32
|
+
*/
|
|
33
|
+
export type RelayDelivery = {
|
|
34
|
+
/** The op's `submitted` value: "true", "false", "unverified", "queued", "not-delivered". */
|
|
35
|
+
submitted?: string;
|
|
36
|
+
/** Epoch milliseconds the verdict was recorded. */
|
|
37
|
+
at?: number;
|
|
38
|
+
/** A reply's hard failure code, or a short note about a soft-but-not-ok send. */
|
|
39
|
+
note?: string;
|
|
40
|
+
};
|
|
28
41
|
export type RelayConfig = {
|
|
29
42
|
id: string;
|
|
30
43
|
serve: string;
|
|
@@ -35,6 +48,8 @@ export type RelayConfig = {
|
|
|
35
48
|
replyTo: string;
|
|
36
49
|
replySession?: string;
|
|
37
50
|
from: string;
|
|
51
|
+
/** The target harness (`--harness`); sent with the body and the reply. */
|
|
52
|
+
harness?: string;
|
|
38
53
|
timeoutMin: number;
|
|
39
54
|
/** Hard cap on the relay's age; defaults to `timeoutMin` plus 60 minutes. */
|
|
40
55
|
maxAgeMin?: number;
|
|
@@ -49,6 +64,8 @@ export type RelayConfig = {
|
|
|
49
64
|
job?: RelayJob;
|
|
50
65
|
/** Final status once the loop ends; absent while the relay runs. */
|
|
51
66
|
status?: RelayStatus;
|
|
67
|
+
/** The last delivery verdict (the initial send, then the reply). */
|
|
68
|
+
delivery?: RelayDelivery;
|
|
52
69
|
/** Set by `--cancel`; a late loop must not overwrite it. */
|
|
53
70
|
cancelled?: boolean;
|
|
54
71
|
cancelledAt?: number;
|
|
@@ -81,6 +98,20 @@ export declare function doneLines(screen: string, done: string): string[];
|
|
|
81
98
|
export declare function freshDoneLine(screen: string, done: string, seen?: readonly string[]): string | null;
|
|
82
99
|
/** Fill {id} {status} {note} {match} {output} {pane} in a message template. */
|
|
83
100
|
export declare function fillMessage(template: string, fields: Record<string, string>): string;
|
|
101
|
+
/**
|
|
102
|
+
* True when a send result is only a delivery *verdict*, not a hard failure.
|
|
103
|
+
*
|
|
104
|
+
* The op layer reports `ok:false, error.code="not_delivered"` when the pane was
|
|
105
|
+
* unchanged apart from the composer. That is the op saying "I could not
|
|
106
|
+
* confirm", not "the send failed": a slow-redrawing pane can still have taken
|
|
107
|
+
* the body, and a false not-delivered must never kill the relay. `submitted`
|
|
108
|
+
* values other than `true` (false / "unverified" / "queued") are the same kind
|
|
109
|
+
* of verdict. Only a hard error is worth aborting on: the pane is gone
|
|
110
|
+
* (peer_not_found / pane_exited / not_found) or serve/the tunnel is down
|
|
111
|
+
* (serve_* / connection failures), or the call itself threw. The relay records
|
|
112
|
+
* the verdict and keeps waiting — the done line is the real proof of delivery.
|
|
113
|
+
*/
|
|
114
|
+
export declare function softSendVerdict(res: OpsResult): boolean;
|
|
84
115
|
/** Paste `body` into the reply pane, retrying with backoff; false leaves it in undelivered/. */
|
|
85
116
|
export declare function deliverMessage(cfg: RelayConfig, body: string, dir: string, deps?: RelayDeps): Promise<boolean>;
|
|
86
117
|
/** The waiting loop; returns the final status and the message it delivered or left behind. */
|
|
@@ -95,6 +126,8 @@ export type RelayStartInput = {
|
|
|
95
126
|
to: string;
|
|
96
127
|
bodyFile?: string;
|
|
97
128
|
from?: string;
|
|
129
|
+
/** Target harness name; sent with the body and the reply (omitted when absent). */
|
|
130
|
+
harness?: string;
|
|
98
131
|
done: string;
|
|
99
132
|
replyTo: string;
|
|
100
133
|
replySession?: string;
|
|
@@ -171,6 +204,8 @@ export type RelayRow = {
|
|
|
171
204
|
age: number;
|
|
172
205
|
state: string;
|
|
173
206
|
lastCheck?: number;
|
|
207
|
+
/** The last delivery verdict recorded in config.json, for the DELIVERY column. */
|
|
208
|
+
delivery?: RelayDelivery;
|
|
174
209
|
/** The launchd label / systemd unit / pid, when the job is still loaded. */
|
|
175
210
|
job?: string;
|
|
176
211
|
/** The live pane lease held for this relay's target, if any. */
|
package/dist/host/relay.js
CHANGED
|
@@ -92,14 +92,35 @@ const NOTES = {
|
|
|
92
92
|
SERVE_DOWN: "serve was unreachable for three checks in a row; run zswarm doctor against it.",
|
|
93
93
|
UNCONFIRMED: "the pane printed the done line but the check never passed; dump the pane once for the error.",
|
|
94
94
|
};
|
|
95
|
+
/**
|
|
96
|
+
* True when a send result is only a delivery *verdict*, not a hard failure.
|
|
97
|
+
*
|
|
98
|
+
* The op layer reports `ok:false, error.code="not_delivered"` when the pane was
|
|
99
|
+
* unchanged apart from the composer. That is the op saying "I could not
|
|
100
|
+
* confirm", not "the send failed": a slow-redrawing pane can still have taken
|
|
101
|
+
* the body, and a false not-delivered must never kill the relay. `submitted`
|
|
102
|
+
* values other than `true` (false / "unverified" / "queued") are the same kind
|
|
103
|
+
* of verdict. Only a hard error is worth aborting on: the pane is gone
|
|
104
|
+
* (peer_not_found / pane_exited / not_found) or serve/the tunnel is down
|
|
105
|
+
* (serve_* / connection failures), or the call itself threw. The relay records
|
|
106
|
+
* the verdict and keeps waiting — the done line is the real proof of delivery.
|
|
107
|
+
*/
|
|
108
|
+
export function softSendVerdict(res) {
|
|
109
|
+
if (!res.ok)
|
|
110
|
+
return res.error.code === "not_delivered";
|
|
111
|
+
const submitted = res.data?.submitted;
|
|
112
|
+
return submitted !== true;
|
|
113
|
+
}
|
|
95
114
|
/** Paste `body` into the reply pane, retrying with backoff; false leaves it in undelivered/. */
|
|
96
115
|
export async function deliverMessage(cfg, body, dir, deps = {}) {
|
|
97
116
|
const env = deps.env ?? process.env;
|
|
98
117
|
const call = deps.call ?? ((args, e) => dispatchZswarm(args, undefined, { env: e }));
|
|
99
118
|
const sleep = deps.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
|
|
119
|
+
const now = deps.now ?? Date.now;
|
|
100
120
|
const log = deps.log ?? (() => { });
|
|
101
121
|
writeFileSync(join(dir, "message.txt"), body);
|
|
102
122
|
const delays = delaysFrom(env, deps.delays);
|
|
123
|
+
let failureCode = "failed";
|
|
103
124
|
for (const [i, delay] of delays.entries()) {
|
|
104
125
|
await sleep(delay * 1000);
|
|
105
126
|
// Generous Zellij budgets: this runs exactly when the machine is busiest.
|
|
@@ -108,16 +129,23 @@ export async function deliverMessage(cfg, body, dir, deps = {}) {
|
|
|
108
129
|
};
|
|
109
130
|
if (cfg.replySession)
|
|
110
131
|
args.session = cfg.replySession;
|
|
132
|
+
if (cfg.harness)
|
|
133
|
+
args.harness = cfg.harness;
|
|
111
134
|
const res = await call(args, localEnv(env)).catch((err) => ({ ok: false, error: { code: "failed", message: String(err) } }));
|
|
112
135
|
if (res.ok) {
|
|
113
|
-
|
|
136
|
+
const submitted = String(res.data?.submitted ?? "unknown");
|
|
137
|
+
recordDelivery(dir, submitted, now());
|
|
138
|
+
log(`message attempt ${i + 1}: delivered, submitted=${submitted}`);
|
|
114
139
|
return true;
|
|
115
140
|
}
|
|
141
|
+
failureCode = res.error.code;
|
|
116
142
|
log(`message attempt ${i + 1} failed: ${res.error.code}: ${res.error.message.slice(0, 200)}`);
|
|
117
143
|
}
|
|
118
144
|
const fallback = join(relaysDir(env), "undelivered");
|
|
119
145
|
mkdirSync(fallback, { recursive: true, mode: 0o700 });
|
|
120
146
|
writeFileSync(join(fallback, `${cfg.id}.txt`), body);
|
|
147
|
+
// The reply never landed: record the last failure code so --list shows failed:<code>.
|
|
148
|
+
recordDelivery(dir, undefined, now(), failureCode);
|
|
121
149
|
log(`message not delivered after ${delays.length} attempts; left in ${join(fallback, `${cfg.id}.txt`)}`);
|
|
122
150
|
return false;
|
|
123
151
|
}
|
|
@@ -304,6 +332,11 @@ function updateConfig(dir, patch) {
|
|
|
304
332
|
const cfg = { ...JSON.parse(readFileSync(path, "utf8")), ...patch };
|
|
305
333
|
writePrivate(path, `${JSON.stringify(cfg, null, 1)}\n`);
|
|
306
334
|
}
|
|
335
|
+
/** Record the last delivery verdict in config.json; `note` carries a hard failure code. */
|
|
336
|
+
function recordDelivery(dir, submitted, at, note) {
|
|
337
|
+
const delivery = { ...(submitted ? { submitted } : {}), at, ...(note ? { note } : {}) };
|
|
338
|
+
updateConfig(dir, { delivery });
|
|
339
|
+
}
|
|
307
340
|
/** Persist the final status unless `--cancel` already marked the relay. */
|
|
308
341
|
function writeFinalStatus(dir, status) {
|
|
309
342
|
try {
|
|
@@ -391,6 +424,7 @@ export async function startRelay(input) {
|
|
|
391
424
|
// The detached job gets a minimal env, so pin the caller's session now.
|
|
392
425
|
replySession: input.replySession?.trim() || env.ZSWARM_SESSION?.trim() || env.ZELLIJ_SESSION_NAME?.trim() || undefined,
|
|
393
426
|
from: input.from?.trim() || "zswarm-relay",
|
|
427
|
+
harness: input.harness?.trim() || undefined,
|
|
394
428
|
timeoutMin: input.timeoutMin ?? 240,
|
|
395
429
|
maxAgeMin: input.maxAgeMin ?? (input.timeoutMin ?? 240) + DEFAULT_MAX_AGE_EXTRA_MIN,
|
|
396
430
|
};
|
|
@@ -405,6 +439,7 @@ export async function startRelay(input) {
|
|
|
405
439
|
const leaseNow = input.deps?.now ?? Date.now;
|
|
406
440
|
const leaseSleep = input.deps?.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
|
|
407
441
|
const call = input.deps?.call ?? ((args, e) => dispatchZswarm(args, undefined, { env: e }));
|
|
442
|
+
const log = input.deps?.log ?? ((line) => appendFileSync(join(dir, "relay.log"), `${new Date().toISOString()} ${line}\n`));
|
|
408
443
|
const remote = serveEnv(cfg, env);
|
|
409
444
|
const target = (args) => cfg.session ? { ...args, session: cfg.session } : args;
|
|
410
445
|
const take = () => takeLease(store, { relayId: id, from: cfg.from, at: leaseNow(), dir, target: leaseTarget, force: input.force });
|
|
@@ -430,7 +465,7 @@ export async function startRelay(input) {
|
|
|
430
465
|
const res = await call(target({ op: "dump", to: cfg.to, max: 400 }), remote).catch((err) => ({ ok: false, error: { code: "failed", message: String(err) } }));
|
|
431
466
|
if (!res.ok)
|
|
432
467
|
return null; // cannot see the screen: the lease is the only guard
|
|
433
|
-
return paneBusyMarker(String(res.data?.text ?? ""), idlePane ?? { title: cfg.to });
|
|
468
|
+
return paneBusyMarker(String(res.data?.text ?? ""), idlePane ?? { title: cfg.to }, cfg.harness);
|
|
434
469
|
};
|
|
435
470
|
const deadline = leaseNow() + cfg.timeoutMin * 60_000;
|
|
436
471
|
let busy = null;
|
|
@@ -479,11 +514,21 @@ export async function startRelay(input) {
|
|
|
479
514
|
}
|
|
480
515
|
try {
|
|
481
516
|
if (body !== undefined) {
|
|
482
|
-
const
|
|
483
|
-
if (
|
|
517
|
+
const sendArgs = { op: "send", to: cfg.to, body, from: cfg.from, settleMs: 800 };
|
|
518
|
+
if (cfg.harness)
|
|
519
|
+
sendArgs.harness = cfg.harness;
|
|
520
|
+
const res = await call(target(sendArgs), remote);
|
|
521
|
+
// A non-ok send is only fatal when it is a hard error. `not_delivered` is a
|
|
522
|
+
// verdict ("could not confirm"), so the relay records it and keeps waiting.
|
|
523
|
+
if (!res.ok && !softSendVerdict(res)) {
|
|
484
524
|
throw new ZellijError(res.error.code, `send failed: ${res.error.message}`, res.error.details);
|
|
485
|
-
|
|
525
|
+
}
|
|
526
|
+
const rawSubmitted = res.ok ? res.data?.submitted : undefined;
|
|
527
|
+
sent = rawSubmitted === true ? "submitted" : "unverified";
|
|
528
|
+
const submittedText = rawSubmitted === undefined ? (res.ok ? "unverified" : "not-delivered") : String(rawSubmitted);
|
|
529
|
+
recordDelivery(dir, submittedText, leaseNow(), res.ok ? undefined : res.error.code);
|
|
486
530
|
if (sent !== "submitted") {
|
|
531
|
+
log(`send verdict submitted=${submittedText}; waiting for the done line`);
|
|
487
532
|
// One dump; one Enter if the body still sits in the composer.
|
|
488
533
|
const dump = await call(target({ op: "dump", to: cfg.to, max: 2000 }), remote);
|
|
489
534
|
const screen = dump.ok ? String(dump.data.text ?? "") : "";
|
|
@@ -720,6 +765,7 @@ export function listRelays(deps = {}) {
|
|
|
720
765
|
age: Math.max(0, Math.floor((now() - run.started) / 1000)),
|
|
721
766
|
state: relayState(run.dir, cfg),
|
|
722
767
|
lastCheck: lastCheckResult(run.dir),
|
|
768
|
+
...(cfg.delivery ? { delivery: cfg.delivery } : {}),
|
|
723
769
|
...(loaded && cfg.job ? { job: jobLabelText(cfg.job) } : {}),
|
|
724
770
|
...(held ? { lease: { relayId: held.relayId, from: held.from, at: held.at } } : {}),
|
|
725
771
|
});
|
package/dist/ops/delivery.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { HarnessProfile } from "../harness.js";
|
|
1
2
|
import type { StateStore } from "../state.js";
|
|
2
3
|
import type { ZellijClient } from "../zellij/client.js";
|
|
3
4
|
import type { ZellijPane } from "../zellij/panes.js";
|
|
@@ -20,19 +21,26 @@ export declare const SEND_BUDGETS: {
|
|
|
20
21
|
readonly settleMs: 300;
|
|
21
22
|
/** Pane-lookup retry window for an absent pane. */
|
|
22
23
|
readonly observeMs: 1000;
|
|
24
|
+
/**
|
|
25
|
+
* Base window that polls the pane for positive evidence before any negative
|
|
26
|
+
* verdict; scaled up for slow panes and capped by `confirmMaxMs`.
|
|
27
|
+
*/
|
|
28
|
+
readonly confirmMs: 1000;
|
|
23
29
|
/** The scaled window never exceeds this multiple of the default. */
|
|
24
30
|
readonly maxScale: 5;
|
|
25
31
|
/** A first Zellij call at or under this many ms is not "slow". */
|
|
26
32
|
readonly slowMs: 1000;
|
|
27
33
|
readonly settleMaxMs: 5000;
|
|
28
34
|
readonly observeMaxMs: 30000;
|
|
35
|
+
readonly confirmMaxMs: 15000;
|
|
29
36
|
};
|
|
30
37
|
/**
|
|
31
|
-
* Scale the fixed settle/observe windows by how slow the send's first
|
|
32
|
-
* call was. A call under `slowMs` keeps the defaults; five seconds or
|
|
33
|
-
* the 5x cap. Explicit `--settle-ms` / `--observe-ms` are returned as
|
|
34
|
-
* and a scaled value is capped by the call's own timeout so it cannot
|
|
35
|
-
* the observation budget.
|
|
38
|
+
* Scale the fixed settle/observe/confirm windows by how slow the send's first
|
|
39
|
+
* Zellij call was. A call under `slowMs` keeps the defaults; five seconds or
|
|
40
|
+
* more pins the 5x cap. Explicit `--settle-ms` / `--observe-ms` are returned as
|
|
41
|
+
* given, and a scaled value is capped by the call's own timeout so it cannot
|
|
42
|
+
* outlast the observation budget. `confirmMs` is always scaled (no explicit
|
|
43
|
+
* flag) and capped by both `confirmMaxMs` and the call timeout.
|
|
36
44
|
*/
|
|
37
45
|
export declare function scaleBudgets(input: {
|
|
38
46
|
firstCallMs: number;
|
|
@@ -42,6 +50,7 @@ export declare function scaleBudgets(input: {
|
|
|
42
50
|
}): {
|
|
43
51
|
settleMs: number;
|
|
44
52
|
observeMs: number;
|
|
53
|
+
confirmMs: number;
|
|
45
54
|
scale: number;
|
|
46
55
|
};
|
|
47
56
|
export type SenderSource = "arg" | "env" | "title" | "default";
|
|
@@ -78,7 +87,8 @@ export declare function attachKnownSender(args: Record<string, unknown>, env: No
|
|
|
78
87
|
export declare function bodyText(client: ZellijClient, args: Record<string, unknown>, body: string): string;
|
|
79
88
|
/**
|
|
80
89
|
* Caller `submit=` always wins. With none given, the pane's harness picks
|
|
81
|
-
* auto vs double-enter (codex needs two Enters; the rest submit on one).
|
|
90
|
+
* auto vs double-enter (codex needs two Enters; the rest submit on one). An
|
|
91
|
+
* explicit `--harness` overrides the pane-inferred profile.
|
|
82
92
|
*/
|
|
83
93
|
export declare function resolveSubmitMode(args: Record<string, unknown>, pane: {
|
|
84
94
|
command?: string | null;
|
|
@@ -107,6 +117,20 @@ export declare function composerHolds(screen: string, body: string): boolean;
|
|
|
107
117
|
export declare function classifySubmit(before: string, after: string, body: string, opts?: {
|
|
108
118
|
queued?: (screen: string) => boolean;
|
|
109
119
|
}): Submitted;
|
|
120
|
+
/**
|
|
121
|
+
* Press Enter once and report whether the body submitted, without pasting.
|
|
122
|
+
* The restart op uses this to re-ask a still-held composer; a failed dump is
|
|
123
|
+
* unverified, never a negative verdict.
|
|
124
|
+
*/
|
|
125
|
+
export declare function submitAfterEnter(client: ZellijClient, input: {
|
|
126
|
+
session: string;
|
|
127
|
+
paneId: string;
|
|
128
|
+
body: string;
|
|
129
|
+
before: string;
|
|
130
|
+
clock: Clock;
|
|
131
|
+
settleMs: number;
|
|
132
|
+
profile: HarnessProfile;
|
|
133
|
+
}): Promise<Submitted>;
|
|
110
134
|
/** Paste a body into one pane and record the attempt. */
|
|
111
135
|
export declare function deliverTo(client: ZellijClient, state: StateStore, args: Record<string, unknown>, input: {
|
|
112
136
|
session: string;
|
package/dist/ops/delivery.js
CHANGED
|
@@ -1,24 +1,33 @@
|
|
|
1
|
-
import { queuedPrompt, resolveHarness } from "../harness.js";
|
|
2
|
-
import { isTrue, numberArg } from "./util.js";
|
|
1
|
+
import { busyMarker, queuedPrompt, resolveHarness } from "../harness.js";
|
|
2
|
+
import { isTrue, numberArg, optionalString } from "./util.js";
|
|
3
3
|
/** Fixed send windows before adaptive scaling; explicit flags always win. */
|
|
4
4
|
export const SEND_BUDGETS = {
|
|
5
5
|
/** Pause before checking the paste landed. */
|
|
6
6
|
settleMs: 300,
|
|
7
7
|
/** Pane-lookup retry window for an absent pane. */
|
|
8
8
|
observeMs: 1_000,
|
|
9
|
+
/**
|
|
10
|
+
* Base window that polls the pane for positive evidence before any negative
|
|
11
|
+
* verdict; scaled up for slow panes and capped by `confirmMaxMs`.
|
|
12
|
+
*/
|
|
13
|
+
confirmMs: 1_000,
|
|
9
14
|
/** The scaled window never exceeds this multiple of the default. */
|
|
10
15
|
maxScale: 5,
|
|
11
16
|
/** A first Zellij call at or under this many ms is not "slow". */
|
|
12
17
|
slowMs: 1_000,
|
|
13
18
|
settleMaxMs: 5_000,
|
|
14
19
|
observeMaxMs: 30_000,
|
|
20
|
+
confirmMaxMs: 15_000,
|
|
15
21
|
};
|
|
22
|
+
/** Upper bound on confirm polls, so a pinned deadline cannot spin forever. */
|
|
23
|
+
const MAX_CONFIRM_POLLS = 40;
|
|
16
24
|
/**
|
|
17
|
-
* Scale the fixed settle/observe windows by how slow the send's first
|
|
18
|
-
* call was. A call under `slowMs` keeps the defaults; five seconds or
|
|
19
|
-
* the 5x cap. Explicit `--settle-ms` / `--observe-ms` are returned as
|
|
20
|
-
* and a scaled value is capped by the call's own timeout so it cannot
|
|
21
|
-
* the observation budget.
|
|
25
|
+
* Scale the fixed settle/observe/confirm windows by how slow the send's first
|
|
26
|
+
* Zellij call was. A call under `slowMs` keeps the defaults; five seconds or
|
|
27
|
+
* more pins the 5x cap. Explicit `--settle-ms` / `--observe-ms` are returned as
|
|
28
|
+
* given, and a scaled value is capped by the call's own timeout so it cannot
|
|
29
|
+
* outlast the observation budget. `confirmMs` is always scaled (no explicit
|
|
30
|
+
* flag) and capped by both `confirmMaxMs` and the call timeout.
|
|
22
31
|
*/
|
|
23
32
|
export function scaleBudgets(input) {
|
|
24
33
|
const raw = input.firstCallMs > SEND_BUDGETS.slowMs ? input.firstCallMs / SEND_BUDGETS.slowMs : 1;
|
|
@@ -27,6 +36,7 @@ export function scaleBudgets(input) {
|
|
|
27
36
|
return {
|
|
28
37
|
settleMs: input.settleMs !== undefined ? Math.min(input.settleMs, SEND_BUDGETS.settleMaxMs) : scaled(SEND_BUDGETS.settleMs, SEND_BUDGETS.settleMaxMs),
|
|
29
38
|
observeMs: input.observeMs !== undefined ? Math.min(input.observeMs, SEND_BUDGETS.observeMaxMs) : scaled(SEND_BUDGETS.observeMs, SEND_BUDGETS.observeMaxMs),
|
|
39
|
+
confirmMs: scaled(SEND_BUDGETS.confirmMs, SEND_BUDGETS.confirmMaxMs),
|
|
30
40
|
scale,
|
|
31
41
|
};
|
|
32
42
|
}
|
|
@@ -91,7 +101,8 @@ export function bodyText(client, args, body) {
|
|
|
91
101
|
}
|
|
92
102
|
/**
|
|
93
103
|
* Caller `submit=` always wins. With none given, the pane's harness picks
|
|
94
|
-
* auto vs double-enter (codex needs two Enters; the rest submit on one).
|
|
104
|
+
* auto vs double-enter (codex needs two Enters; the rest submit on one). An
|
|
105
|
+
* explicit `--harness` overrides the pane-inferred profile.
|
|
95
106
|
*/
|
|
96
107
|
export function resolveSubmitMode(args, pane) {
|
|
97
108
|
const raw = args.submit;
|
|
@@ -99,7 +110,7 @@ export function resolveSubmitMode(args, pane) {
|
|
|
99
110
|
return raw;
|
|
100
111
|
if (raw !== undefined && raw !== null && raw !== "")
|
|
101
112
|
return "auto";
|
|
102
|
-
return resolveHarness(pane).submit;
|
|
113
|
+
return resolveHarness(pane, optionalString(args.harness)).submit;
|
|
103
114
|
}
|
|
104
115
|
function nonEmptyLines(screen) {
|
|
105
116
|
return screen
|
|
@@ -205,7 +216,7 @@ async function dumpOrNull(client, session, paneId) {
|
|
|
205
216
|
}
|
|
206
217
|
}
|
|
207
218
|
async function verifySubmit(client, input) {
|
|
208
|
-
const { session, paneId, body, mode, settleMs, clock, before, queued } = input;
|
|
219
|
+
const { session, paneId, body, mode, settleMs, confirmMs, clock, before, profile, queued } = input;
|
|
209
220
|
const classify = (base, screen) => classifySubmit(base, screen, body, { queued });
|
|
210
221
|
if (mode === "none")
|
|
211
222
|
return "unverified";
|
|
@@ -232,24 +243,58 @@ async function verifySubmit(client, input) {
|
|
|
232
243
|
verdict = classify(before, rescued);
|
|
233
244
|
if (verdict === true || verdict === "queued")
|
|
234
245
|
return verdict;
|
|
235
|
-
if (verdict === false)
|
|
236
|
-
return false;
|
|
237
246
|
}
|
|
238
|
-
//
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
247
|
+
// Nothing conclusive yet: a freshly restarted agent can redraw slowly, so poll
|
|
248
|
+
// for the whole scaled confirm window before any negative verdict. Positive
|
|
249
|
+
// evidence is new output, a queue hint, the harness's busy marker, or the body
|
|
250
|
+
// leaving a composer it was sitting in.
|
|
251
|
+
const pollMs = Math.min(Math.max(settleMs * 2, 500), 1_000);
|
|
252
|
+
const deadline = clock.now() + confirmMs;
|
|
253
|
+
for (let polls = 0; polls < MAX_CONFIRM_POLLS && clock.now() < deadline; polls++) {
|
|
254
|
+
await clock.sleep(Math.min(pollMs, Math.max(0, deadline - clock.now())));
|
|
255
|
+
const later = await dumpOrNull(client, session, paneId);
|
|
256
|
+
if (later === null)
|
|
257
|
+
continue; // unreadable pane: keep polling
|
|
258
|
+
const seen = classify(before, later);
|
|
259
|
+
if (seen === true || seen === "queued")
|
|
260
|
+
return seen;
|
|
261
|
+
if (busyMarker(later, profile))
|
|
262
|
+
return true;
|
|
263
|
+
if (composerHolds(after, body) && !composerHolds(later, body))
|
|
264
|
+
return true;
|
|
265
|
+
verdict = seen;
|
|
266
|
+
}
|
|
267
|
+
// An unknown harness has no reliable busy marker, so an unchanged screen is
|
|
268
|
+
// not proof the body never arrived; a false not-delivered would make the
|
|
269
|
+
// caller resend the whole message. A known harness's not-delivered is now
|
|
270
|
+
// backed by the whole window.
|
|
271
|
+
if (verdict === "not-delivered" && profile.name === "unknown")
|
|
272
|
+
return "unverified";
|
|
273
|
+
return verdict;
|
|
274
|
+
}
|
|
275
|
+
/**
|
|
276
|
+
* Press Enter once and report whether the body submitted, without pasting.
|
|
277
|
+
* The restart op uses this to re-ask a still-held composer; a failed dump is
|
|
278
|
+
* unverified, never a negative verdict.
|
|
279
|
+
*/
|
|
280
|
+
export async function submitAfterEnter(client, input) {
|
|
281
|
+
const { session, paneId, body, before, clock, settleMs, profile } = input;
|
|
282
|
+
await client.sendKeys({ session, paneId, keys: ["Enter"] });
|
|
283
|
+
await clock.sleep(settleMs);
|
|
284
|
+
const screen = await dumpOrNull(client, session, paneId);
|
|
285
|
+
if (screen === null)
|
|
286
|
+
return "unverified";
|
|
287
|
+
return classifySubmit(before, screen, body, {
|
|
288
|
+
queued: (s) => queuedPrompt(s, profile),
|
|
289
|
+
});
|
|
246
290
|
}
|
|
247
291
|
/** Paste a body into one pane and record the attempt. */
|
|
248
292
|
export async function deliverTo(client, state, args, input) {
|
|
249
293
|
const { session, pane, body, op, at, clock, firstCallMs = 0, timeoutMs = 30_000 } = input;
|
|
250
294
|
const from = senderLabel(args);
|
|
295
|
+
const override = optionalString(args.harness);
|
|
251
296
|
const mode = resolveSubmitMode(args, pane);
|
|
252
|
-
const profile = resolveHarness(pane);
|
|
297
|
+
const profile = resolveHarness(pane, override);
|
|
253
298
|
const queued = (screen) => queuedPrompt(screen, profile);
|
|
254
299
|
const explicitSettle = args.settleMs !== undefined && args.settleMs !== null && args.settleMs !== "";
|
|
255
300
|
const budgets = scaleBudgets({
|
|
@@ -258,6 +303,7 @@ export async function deliverTo(client, state, args, input) {
|
|
|
258
303
|
settleMs: explicitSettle ? numberArg(args, "settleMs", SEND_BUDGETS.settleMs, { min: 50, max: SEND_BUDGETS.settleMaxMs }) : undefined,
|
|
259
304
|
});
|
|
260
305
|
const settleMs = budgets.settleMs;
|
|
306
|
+
const confirmMs = budgets.confirmMs;
|
|
261
307
|
const confirmOnce = isTrue(args.confirm);
|
|
262
308
|
try {
|
|
263
309
|
let before = null;
|
|
@@ -276,8 +322,10 @@ export async function deliverTo(client, state, args, input) {
|
|
|
276
322
|
body,
|
|
277
323
|
mode,
|
|
278
324
|
settleMs,
|
|
325
|
+
confirmMs,
|
|
279
326
|
clock,
|
|
280
327
|
before,
|
|
328
|
+
profile,
|
|
281
329
|
queued,
|
|
282
330
|
});
|
|
283
331
|
// Opt-in confirm. A second paste is only safe on positive evidence the
|
|
@@ -293,8 +341,10 @@ export async function deliverTo(client, state, args, input) {
|
|
|
293
341
|
body,
|
|
294
342
|
mode,
|
|
295
343
|
settleMs,
|
|
344
|
+
confirmMs,
|
|
296
345
|
clock,
|
|
297
346
|
before: retryBefore,
|
|
347
|
+
profile,
|
|
298
348
|
queued,
|
|
299
349
|
});
|
|
300
350
|
}
|
package/dist/ops/dispatch.d.ts
CHANGED
|
@@ -8,7 +8,7 @@ import type { DispatchDeps, OpsResult } from "./types.js";
|
|
|
8
8
|
export declare function paneBusyMarker(screen: string, pane: {
|
|
9
9
|
command?: string | null;
|
|
10
10
|
title?: string | null;
|
|
11
|
-
}): string | null;
|
|
11
|
+
}, harness?: string | null): string | null;
|
|
12
12
|
/**
|
|
13
13
|
* Per-invocation routing from `--local` / `--ssh`. `--local` clears both SSH
|
|
14
14
|
* and serve (and remote IPC). `--ssh` sets the destination for this call and
|
package/dist/ops/dispatch.js
CHANGED
|
@@ -36,8 +36,8 @@ import { installServeService, uninstallServeService } from "../host/serve-servic
|
|
|
36
36
|
* busy check behind `send --if-idle` and `relay --if-idle`; the marker names
|
|
37
37
|
* itself (its regex source) in the `pane_busy` error.
|
|
38
38
|
*/
|
|
39
|
-
export function paneBusyMarker(screen, pane) {
|
|
40
|
-
return busyMarker(screen, resolveHarness(pane))?.source ?? null;
|
|
39
|
+
export function paneBusyMarker(screen, pane, harness) {
|
|
40
|
+
return busyMarker(screen, resolveHarness(pane, harness))?.source ?? null;
|
|
41
41
|
}
|
|
42
42
|
/**
|
|
43
43
|
* Per-invocation routing from `--local` / `--ssh`. `--local` clears both SSH
|
|
@@ -412,7 +412,7 @@ async function dispatchOperation(args, injected, deps, context, env) {
|
|
|
412
412
|
}
|
|
413
413
|
const screen = await client.dumpPane({ session, paneId: pane.id }).then((res) => res.text, () => null);
|
|
414
414
|
if (screen !== null) {
|
|
415
|
-
const marker = paneBusyMarker(screen, pane);
|
|
415
|
+
const marker = paneBusyMarker(screen, pane, optionalString(args.harness));
|
|
416
416
|
if (marker) {
|
|
417
417
|
throw new ZellijError("pane_busy", `pane ${to} is busy with a task (marker: ${marker}); retry when it is idle`);
|
|
418
418
|
}
|
package/dist/ops/restart.d.ts
CHANGED
|
@@ -27,6 +27,8 @@ export declare const READY_SETTLE_MS = 500;
|
|
|
27
27
|
export declare const READY_STABLE_GAP_MS = 1000;
|
|
28
28
|
/** Screen poll interval while a step waits. */
|
|
29
29
|
export declare const RESTART_POLL_MS = 250;
|
|
30
|
+
/** Settle after the rescue Enter before re-reading the composer, matching the relay. */
|
|
31
|
+
export declare const SUBMIT_ENTER_SETTLE_MS = 800;
|
|
30
32
|
export type RestartPaneKind = "command" | "shell";
|
|
31
33
|
export type RestartStep = {
|
|
32
34
|
step: string;
|
package/dist/ops/restart.js
CHANGED
|
@@ -5,7 +5,7 @@ import { ZellijError } from "../errors.js";
|
|
|
5
5
|
import { readyPrompt, resolveHarness } from "../harness.js";
|
|
6
6
|
import { findLease } from "../host/lease.js";
|
|
7
7
|
import { tokenizeCommand } from "../keys.js";
|
|
8
|
-
import { deliverTo } from "./delivery.js";
|
|
8
|
+
import { deliverTo, submitAfterEnter } from "./delivery.js";
|
|
9
9
|
import { assertNotPlugin, assertNotSelf } from "./guards.js";
|
|
10
10
|
import { isTrue, numberArg, optionalString, throwIfAborted } from "./util.js";
|
|
11
11
|
/**
|
|
@@ -32,6 +32,8 @@ export const READY_SETTLE_MS = 500;
|
|
|
32
32
|
export const READY_STABLE_GAP_MS = 1_000;
|
|
33
33
|
/** Screen poll interval while a step waits. */
|
|
34
34
|
export const RESTART_POLL_MS = 250;
|
|
35
|
+
/** Settle after the rescue Enter before re-reading the composer, matching the relay. */
|
|
36
|
+
export const SUBMIT_ENTER_SETTLE_MS = 800;
|
|
35
37
|
/**
|
|
36
38
|
* Zellij keeps an exited command pane on screen with a re-run prompt. The exact
|
|
37
39
|
* wording has changed between releases, so match the stable phrases.
|
|
@@ -394,8 +396,39 @@ export async function restartPane(client, state, args, target, clock, opts = {})
|
|
|
394
396
|
failStep("deliver", `handoff not delivered (${delivered.error?.message ?? "unknown"})`, steps);
|
|
395
397
|
}
|
|
396
398
|
const source = handoff.pointer ? `pointer to ${handoffPath}` : `${handoff.body.length} chars`;
|
|
397
|
-
|
|
398
|
-
|
|
399
|
+
let submitted = delivered.submitted;
|
|
400
|
+
// `false` means the paste is still sitting in the harness's composer: the
|
|
401
|
+
// body was never sent. Reporting that as success loses the handoff when the
|
|
402
|
+
// pane is restarted again, so press Enter once and re-verify before trusting
|
|
403
|
+
// it. A screen read that fails here only turns the verdict into `unverified`,
|
|
404
|
+
// which still fails the step below rather than claiming success.
|
|
405
|
+
if (submitted === false) {
|
|
406
|
+
let before = "";
|
|
407
|
+
try {
|
|
408
|
+
before = (await client.dumpPane({ session, paneId: pane.id })).text;
|
|
409
|
+
}
|
|
410
|
+
catch {
|
|
411
|
+
// Unreadable pane: submitAfterEnter reports unverified, never a pass.
|
|
412
|
+
}
|
|
413
|
+
submitted = await submitAfterEnter(client, {
|
|
414
|
+
session,
|
|
415
|
+
paneId: pane.id,
|
|
416
|
+
body: handoff.body,
|
|
417
|
+
before,
|
|
418
|
+
clock,
|
|
419
|
+
settleMs: Math.min(SUBMIT_ENTER_SETTLE_MS, Math.max(1, remaining())),
|
|
420
|
+
profile,
|
|
421
|
+
});
|
|
422
|
+
if (submitted !== true && submitted !== "queued") {
|
|
423
|
+
record("deliver", false, `composer still holds the handoff; submitted=${submitted} after one Enter`);
|
|
424
|
+
failStep("deliver", `handoff not submitted (composer still holds it; submitted=${submitted} after one Enter)`, steps);
|
|
425
|
+
}
|
|
426
|
+
record("deliver", true, `${source} ${resent ? "resent" : "sent"}; submitted=${submitted} after one Enter`);
|
|
427
|
+
}
|
|
428
|
+
else {
|
|
429
|
+
const verdict = submitted === undefined ? "unknown" : String(submitted);
|
|
430
|
+
record("deliver", true, `${source} ${resent ? "resent" : "sent"}; submitted=${verdict}`);
|
|
431
|
+
}
|
|
399
432
|
}
|
|
400
433
|
return {
|
|
401
434
|
ok: true,
|
package/dist/schema.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { ZellijError } from "./errors.js";
|
|
2
|
+
import { HARNESS_NAMES } from "./harness.js";
|
|
2
3
|
/**
|
|
3
4
|
* One description of the zswarm surface. The MCP tool schema, the CLI flags,
|
|
4
5
|
* and the CLI help text are all generated from it, so they cannot drift apart.
|
|
@@ -474,6 +475,13 @@ export const PARAMS = [
|
|
|
474
475
|
ops: ["send", "broadcast"],
|
|
475
476
|
description: 'send/broadcast: after the send, dump once more and report what it sees; paste again only on "not-delivered" (positive evidence nothing landed), press Enter when the body is still in the composer, and never paste the body twice',
|
|
476
477
|
},
|
|
478
|
+
{
|
|
479
|
+
name: "harness",
|
|
480
|
+
type: "string",
|
|
481
|
+
flags: ["--harness"],
|
|
482
|
+
ops: ["send", "restart"],
|
|
483
|
+
description: `send/restart: override the harness inferred from the pane's command/title for wrapper-launched panes (known: ${HARNESS_NAMES.join(", ")})`,
|
|
484
|
+
},
|
|
477
485
|
{
|
|
478
486
|
name: "ifIdle",
|
|
479
487
|
type: "boolean",
|