@zswarm/core 0.1.5 → 0.1.7
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/README.md +4 -1
- package/dist/exec.d.ts +47 -2
- package/dist/exec.js +249 -20
- package/dist/harness.js +12 -7
- package/dist/index.d.ts +8 -6
- package/dist/index.js +5 -4
- package/dist/ops/broadcast.js +1 -1
- package/dist/ops/bus.d.ts +6 -3
- package/dist/ops/bus.js +85 -35
- package/dist/ops/dispatch.d.ts +7 -1
- package/dist/ops/dispatch.js +235 -70
- package/dist/ops/guards.d.ts +2 -0
- package/dist/ops/guards.js +11 -1
- package/dist/ops/observation.d.ts +8 -0
- package/dist/ops/observation.js +26 -0
- package/dist/ops/panes.js +4 -4
- package/dist/ops/routing.d.ts +13 -0
- package/dist/ops/routing.js +21 -0
- package/dist/ops/spawn.d.ts +3 -3
- package/dist/ops/spawn.js +130 -64
- package/dist/ops/status.d.ts +30 -5
- package/dist/ops/status.js +245 -96
- package/dist/ops/types.d.ts +5 -1
- package/dist/ops/util.d.ts +3 -0
- package/dist/ops/util.js +2 -1
- package/dist/ops/worktree.js +2 -2
- package/dist/schema.d.ts +2 -0
- package/dist/schema.js +60 -9
- package/dist/zellij/args.d.ts +2 -0
- package/dist/zellij/binary.d.ts +47 -0
- package/dist/zellij/binary.js +201 -8
- package/dist/zellij/bus.d.ts +2 -0
- package/dist/zellij/bus.js +3 -2
- package/dist/zellij/cache.d.ts +24 -0
- package/dist/zellij/cache.js +90 -0
- package/dist/zellij/client.d.ts +37 -6
- package/dist/zellij/client.js +181 -42
- package/dist/zellij/panes.js +3 -0
- package/dist/zellij/session.d.ts +19 -3
- package/dist/zellij/session.js +39 -9
- package/package.json +3 -3
package/dist/ops/bus.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { existsSync } from "node:fs";
|
|
2
2
|
import { ZellijError } from "../errors.js";
|
|
3
3
|
import { loadPolicy } from "../policy.js";
|
|
4
|
-
import { DEFAULT_BUS_KEY, busPluginUrl, isBusPluginPane, parseBusReply, parseChangedReply, parseScrollbackReply, parseWaitReply, resolveBusPlugin, scrollbackToScreen, } from "../zellij/bus.js";
|
|
5
|
-
import {
|
|
4
|
+
import { DEFAULT_BUS_KEY, DEFAULT_BUS_TIMEOUT_MS, busPluginUrl, isBusPluginPane, parseBusReply, parseChangedReply, parseScrollbackReply, parseWaitReply, resolveBusPlugin, scrollbackToScreen, } from "../zellij/bus.js";
|
|
5
|
+
import { DEFAULT_TIMEOUT_MS } from "../zellij/binary.js";
|
|
6
|
+
import { isTrue, throwIfAborted } from "./util.js";
|
|
6
7
|
/**
|
|
7
8
|
* Whether to ask the event-bus plugin, and what to do when it does not answer.
|
|
8
9
|
*
|
|
@@ -18,15 +19,18 @@ const COLD_RETRY_MS = 200;
|
|
|
18
19
|
* copies.
|
|
19
20
|
*/
|
|
20
21
|
const INSTALL_IN_FLIGHT_MS = 10_000;
|
|
21
|
-
/**
|
|
22
|
-
|
|
22
|
+
/** Back off failed pipes briefly, isolated to their transport/session/plugin. */
|
|
23
|
+
const busFailures = new Map();
|
|
23
24
|
/** The key that actually answered, per session, for the life of this process. */
|
|
24
25
|
const answeredWith = new Map();
|
|
25
26
|
/** Tests share a module instance; this drops what a previous case cached. */
|
|
26
27
|
export function resetBusCache() {
|
|
27
|
-
|
|
28
|
+
busFailures.clear();
|
|
28
29
|
answeredWith.clear();
|
|
29
30
|
}
|
|
31
|
+
function busScope(client, session, plan) {
|
|
32
|
+
return JSON.stringify([client.contextKey(), session, plan.url, plan.configKey]);
|
|
33
|
+
}
|
|
30
34
|
function affirmative(raw) {
|
|
31
35
|
const v = (raw ?? "").trim().toLowerCase();
|
|
32
36
|
return v === "1" || v === "true" || v === "yes" || v === "on";
|
|
@@ -61,8 +65,12 @@ export function planBus(client, state, env = process.env, session = "") {
|
|
|
61
65
|
reason: "no plugin wasm found; run pnpm build:plugin or set ZSWARM_BUS_PLUGIN",
|
|
62
66
|
};
|
|
63
67
|
}
|
|
64
|
-
|
|
65
|
-
|
|
68
|
+
const scope = busScope(client, session, base);
|
|
69
|
+
const failed = busFailures.get(scope);
|
|
70
|
+
if (failed && Date.now() < failed.until)
|
|
71
|
+
return { ...base, reason: failed.reason };
|
|
72
|
+
if (failed)
|
|
73
|
+
busFailures.delete(scope);
|
|
66
74
|
if (affirmative(env.ZSWARM_BUS)) {
|
|
67
75
|
return { ...base, enabled: true, reason: "ZSWARM_BUS=1" };
|
|
68
76
|
}
|
|
@@ -70,8 +78,10 @@ export function planBus(client, state, env = process.env, session = "") {
|
|
|
70
78
|
return { ...base, enabled: true, reason: "installed" };
|
|
71
79
|
return { ...base, reason: 'not installed; run zswarm({op:"bus", install:true})' };
|
|
72
80
|
}
|
|
73
|
-
async function askOnce(client, session, url, configKey, payload) {
|
|
74
|
-
const result = await client.pipePlugin({
|
|
81
|
+
async function askOnce(client, session, url, configKey, payload, timeoutMs) {
|
|
82
|
+
const result = await client.pipePlugin({
|
|
83
|
+
session, url, configKey, payload, timeoutMs,
|
|
84
|
+
});
|
|
75
85
|
return parseBusReply(result.stdout);
|
|
76
86
|
}
|
|
77
87
|
/**
|
|
@@ -80,14 +90,25 @@ async function askOnce(client, session, url, configKey, payload) {
|
|
|
80
90
|
* blind (ready:false, paneUpdates:0) until something moves. Renaming a pane
|
|
81
91
|
* to the title it already has is an invisible change that forces the push.
|
|
82
92
|
*/
|
|
83
|
-
async function nudgeManifest(client, session, env) {
|
|
93
|
+
async function nudgeManifest(client, session, env, remaining) {
|
|
84
94
|
if (loadPolicy(env).readOnly)
|
|
85
95
|
return;
|
|
86
|
-
const
|
|
96
|
+
const listBudget = Math.min(DEFAULT_TIMEOUT_MS, remaining());
|
|
97
|
+
if (listBudget <= 0)
|
|
98
|
+
return;
|
|
99
|
+
const panes = await client.listPanes(session, listBudget, { fresh: true });
|
|
87
100
|
const pane = panes.find((p) => !p.isPlugin && !p.exited && p.title.trim());
|
|
88
101
|
if (!pane)
|
|
89
102
|
return;
|
|
90
|
-
|
|
103
|
+
const renameBudget = Math.min(DEFAULT_TIMEOUT_MS, remaining());
|
|
104
|
+
if (renameBudget <= 0)
|
|
105
|
+
return;
|
|
106
|
+
await client.renamePane({
|
|
107
|
+
session,
|
|
108
|
+
paneId: pane.id,
|
|
109
|
+
name: pane.title,
|
|
110
|
+
timeoutMs: renameBudget,
|
|
111
|
+
});
|
|
91
112
|
}
|
|
92
113
|
/**
|
|
93
114
|
* One pipe, one answer — or null, and the caller polls instead.
|
|
@@ -99,35 +120,61 @@ async function nudgeManifest(client, session, env) {
|
|
|
99
120
|
* --plugin` loads a new copy for each key — that is how a dead bus became
|
|
100
121
|
* dozens of WASM panes. One remembered key, then polling.
|
|
101
122
|
*/
|
|
102
|
-
export async function busSnapshot(client, state, session, clock, env = process.env, payload = "status") {
|
|
123
|
+
export async function busSnapshot(client, state, session, clock, env = process.env, payload = "status", opts = {}) {
|
|
124
|
+
const remaining = () => {
|
|
125
|
+
throwIfAborted(opts.signal);
|
|
126
|
+
return opts.deadlineAt === undefined
|
|
127
|
+
? Infinity
|
|
128
|
+
: Math.max(0, opts.deadlineAt - clock.now());
|
|
129
|
+
};
|
|
130
|
+
if (remaining() <= 0)
|
|
131
|
+
return null;
|
|
103
132
|
const plan = planBus(client, state, env, session);
|
|
104
133
|
const pluginPath = plan.plugin;
|
|
105
|
-
|
|
134
|
+
const url = plan.url;
|
|
135
|
+
if (!plan.enabled || !url || !pluginPath)
|
|
106
136
|
return null;
|
|
107
|
-
const
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
137
|
+
const scope = busScope(client, session, plan);
|
|
138
|
+
const configKey = answeredWith.get(scope) ?? plan.configKey;
|
|
139
|
+
const ask = async () => {
|
|
140
|
+
const budget = Math.min(DEFAULT_BUS_TIMEOUT_MS, remaining());
|
|
141
|
+
if (budget <= 0)
|
|
142
|
+
return null;
|
|
143
|
+
const snapshot = await askOnce(client, session, url, configKey, payload, budget);
|
|
144
|
+
throwIfAborted(opts.signal);
|
|
145
|
+
return snapshot;
|
|
146
|
+
};
|
|
147
|
+
let snapshot = await ask();
|
|
148
|
+
if (snapshot && !snapshot.ready && remaining() > 0) {
|
|
149
|
+
await clock.sleep(Math.min(COLD_RETRY_MS, remaining()));
|
|
150
|
+
snapshot = (await ask()) ?? snapshot;
|
|
114
151
|
}
|
|
115
|
-
if (snapshot && !snapshot.ready) {
|
|
152
|
+
if (snapshot && !snapshot.ready && remaining() > 0) {
|
|
116
153
|
try {
|
|
117
|
-
await nudgeManifest(client, session, env);
|
|
118
|
-
snapshot =
|
|
119
|
-
(await askOnce(client, session, plan.url, configKey, payload)) ??
|
|
120
|
-
snapshot;
|
|
154
|
+
await nudgeManifest(client, session, env, remaining);
|
|
155
|
+
snapshot = (await ask()) ?? snapshot;
|
|
121
156
|
}
|
|
122
157
|
catch {
|
|
158
|
+
throwIfAborted(opts.signal);
|
|
123
159
|
// A failed nudge must not fail the op — the caller polls instead.
|
|
124
160
|
}
|
|
125
161
|
}
|
|
162
|
+
// An expired request says nothing about bus health or an empty cold manifest.
|
|
163
|
+
if (remaining() <= 0 && !snapshot?.ready)
|
|
164
|
+
return null;
|
|
126
165
|
if (snapshot) {
|
|
127
|
-
answeredWith.set(
|
|
166
|
+
answeredWith.set(scope, configKey);
|
|
167
|
+
if (answeredWith.size > 256)
|
|
168
|
+
answeredWith.delete(answeredWith.keys().next().value);
|
|
169
|
+
if (snapshot.ready)
|
|
170
|
+
client.observeManifest(session, JSON.stringify([
|
|
171
|
+
scope, snapshot.paneUpdates, snapshot.tabUpdates, snapshot.panes, snapshot.tabs, snapshot.tabIds,
|
|
172
|
+
]));
|
|
128
173
|
return { snapshot, configKey };
|
|
129
174
|
}
|
|
130
|
-
|
|
175
|
+
busFailures.set(scope, { reason: "plugin did not answer; using zellij polling for 5s", until: Date.now() + 5000 });
|
|
176
|
+
if (busFailures.size > 256)
|
|
177
|
+
busFailures.delete(busFailures.keys().next().value);
|
|
131
178
|
return null;
|
|
132
179
|
}
|
|
133
180
|
/**
|
|
@@ -141,18 +188,19 @@ export async function busSnapshot(client, state, session, clock, env = process.e
|
|
|
141
188
|
* `dump-screen` returns them ragged. `normalizeScreen` erases exactly that
|
|
142
189
|
* difference, and every caller here already normalizes before comparing.
|
|
143
190
|
*/
|
|
144
|
-
export async function busScreens(client, state, session, paneIds, clock, env = process.env) {
|
|
191
|
+
export async function busScreens(client, state, session, paneIds, clock, env = process.env, timeoutMs) {
|
|
145
192
|
if (paneIds.length < 2)
|
|
146
193
|
return null;
|
|
147
194
|
const plan = planBus(client, state, env, session);
|
|
148
195
|
if (!plan.enabled || !plan.url)
|
|
149
196
|
return null;
|
|
150
|
-
const configKey = answeredWith.get(session) ?? plan.configKey;
|
|
197
|
+
const configKey = answeredWith.get(busScope(client, session, plan)) ?? plan.configKey;
|
|
151
198
|
const reply = await client.scrollbackPlugin({
|
|
152
199
|
session,
|
|
153
200
|
url: plan.url,
|
|
154
201
|
configKey,
|
|
155
202
|
panes: paneIds,
|
|
203
|
+
timeoutMs,
|
|
156
204
|
});
|
|
157
205
|
const parsed = parseScrollbackReply(reply.stdout);
|
|
158
206
|
if (!parsed || !parsed.ready)
|
|
@@ -177,7 +225,7 @@ export async function busWait(client, state, session, request, env = process.env
|
|
|
177
225
|
const plan = planBus(client, state, env, session);
|
|
178
226
|
if (!plan.enabled || !plan.url)
|
|
179
227
|
return null;
|
|
180
|
-
const configKey = answeredWith.get(session) ?? plan.configKey;
|
|
228
|
+
const configKey = answeredWith.get(busScope(client, session, plan)) ?? plan.configKey;
|
|
181
229
|
const reply = await client.waitPlugin({
|
|
182
230
|
session,
|
|
183
231
|
url: plan.url,
|
|
@@ -194,23 +242,25 @@ export async function busWait(client, state, session, request, env = process.env
|
|
|
194
242
|
* `status` answers by sampling twice, without the gap between samples — but
|
|
195
243
|
* relative to the previous call rather than a fixed window.
|
|
196
244
|
*/
|
|
197
|
-
export async function busChanged(client, state, session, paneIds, env = process.env) {
|
|
245
|
+
export async function busChanged(client, state, session, paneIds, env = process.env, timeoutMs) {
|
|
198
246
|
if (paneIds.length === 0)
|
|
199
247
|
return null;
|
|
200
248
|
const plan = planBus(client, state, env, session);
|
|
201
249
|
if (!plan.enabled || !plan.url)
|
|
202
250
|
return null;
|
|
203
|
-
const configKey = answeredWith.get(session) ?? plan.configKey;
|
|
251
|
+
const configKey = answeredWith.get(busScope(client, session, plan)) ?? plan.configKey;
|
|
204
252
|
const reply = await client.changedPlugin({
|
|
205
253
|
session,
|
|
206
254
|
url: plan.url,
|
|
207
255
|
configKey,
|
|
208
256
|
panes: paneIds,
|
|
257
|
+
timeoutMs,
|
|
209
258
|
});
|
|
210
259
|
const parsed = parseChangedReply(reply.stdout);
|
|
211
260
|
if (!parsed || !parsed.ready)
|
|
212
261
|
return null;
|
|
213
|
-
|
|
262
|
+
const received = new Set([...parsed.panes.map((pane) => pane.id), ...parsed.missing]);
|
|
263
|
+
return paneIds.every((id) => received.has(id)) ? parsed : null;
|
|
214
264
|
}
|
|
215
265
|
function installedReply(session, plugin, configKey, ready, panes, extra = {}) {
|
|
216
266
|
return {
|
|
@@ -228,7 +278,7 @@ function installedReply(session, plugin, configKey, ready, panes, extra = {}) {
|
|
|
228
278
|
};
|
|
229
279
|
}
|
|
230
280
|
async function listBusPluginPanes(client, session, pluginPath) {
|
|
231
|
-
const panes = await client.listPanes(session);
|
|
281
|
+
const panes = await client.listPanes(session, undefined, { fresh: true });
|
|
232
282
|
return panes.filter((pane) => isBusPluginPane(pane, pluginPath));
|
|
233
283
|
}
|
|
234
284
|
/** Close every zswarm-bus plugin pane in this session. Best-effort per pane. */
|
package/dist/ops/dispatch.d.ts
CHANGED
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
import { type ZellijClient } from "../zellij/client.js";
|
|
2
2
|
import type { DispatchDeps, OpsResult } from "./types.js";
|
|
3
|
-
/**
|
|
3
|
+
/**
|
|
4
|
+
* Per-invocation routing from `--local` / `--ssh`. `--local` clears both SSH
|
|
5
|
+
* and serve (and remote IPC). `--ssh` sets the destination for this call and
|
|
6
|
+
* clears serve so the flag wins over a sticky ZSWARM_SERVE.
|
|
7
|
+
*/
|
|
8
|
+
export declare function resolveInvocationEnv(args: Record<string, unknown>, base: NodeJS.ProcessEnv): NodeJS.ProcessEnv;
|
|
9
|
+
/** Shared MCP/CLI dispatch, with invocation routing on successes and failures. */
|
|
4
10
|
export declare function dispatchZswarm(args: Record<string, unknown>, injected?: ZellijClient, deps?: DispatchDeps): Promise<OpsResult>;
|