@byokit/herdr 0.1.1 → 0.1.6
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/CHANGELOG.md +32 -0
- package/dist/agents.d.ts +1 -0
- package/dist/agents.js +18 -6
- package/dist/kit.d.ts +1 -1
- package/dist/kit.js +21 -9
- package/dist/notices.js +2 -2
- package/dist/testing/fake-herdr/bin.js +7 -2
- package/dist/testing/fake-herdr/server.js +9 -4
- package/package.json +5 -5
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,38 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## 0.1.6 (2026-09-30)
|
|
6
|
+
|
|
7
|
+
- Depends on @byokit/relay 0.3.0.
|
|
8
|
+
|
|
9
|
+
## 0.1.5 (2026-09-30)
|
|
10
|
+
|
|
11
|
+
- Depends on @byokit/relay 0.2.2.
|
|
12
|
+
- Depends on @byokit/link 0.4.0.
|
|
13
|
+
|
|
14
|
+
- The test fake's `fake.stream` ends with one `fake.stream.done` line on the same ordered byte stream, so the
|
|
15
|
+
paused-terminal backpressure test asserts delivery causally instead of on a wall-clock wait.
|
|
16
|
+
|
|
17
|
+
## 0.1.4 (2026-09-30)
|
|
18
|
+
|
|
19
|
+
- Preserve seed-based notice opening with @byokit/seal 0.2.0.
|
|
20
|
+
|
|
21
|
+
## 0.1.3
|
|
22
|
+
|
|
23
|
+
- Depends on @byokit/reach 0.3.0.
|
|
24
|
+
|
|
25
|
+
## 0.1.2 (2026-09-29)
|
|
26
|
+
|
|
27
|
+
- FIX: `prompt` now reaches an agent that was already running when the app connected. Herdr marks only
|
|
28
|
+
the agents its own `agent.start` launched as interactive-ready, so an agent started by hand or by
|
|
29
|
+
another app was refused forever; and a snapshot read while an agent was still launching kept it
|
|
30
|
+
marked as launching, because Herdr never pushes the end of a launch. The kit no longer gates on
|
|
31
|
+
interactive-ready, and before refusing a prompt it re-reads the agent once from Herdr.
|
|
32
|
+
- FIX: when Herdr itself refuses a prompt as not ready, `prompt` now fails with `agent-not-ready`
|
|
33
|
+
like the kit's own check, not with Herdr's raw `agent_not_ready` error.
|
|
34
|
+
- The test fake leaves `launch_pending` and `interactive_ready` out when they are false, as Herdr v0.9.1 does.
|
|
35
|
+
- Depends on @byokit/relay 0.2.1.
|
|
36
|
+
|
|
5
37
|
## 0.1.1 (2026-09-29)
|
|
6
38
|
|
|
7
39
|
- SECURITY: (from @byokit/relay 0.2.0) Removing a phone now reliably stops its push notifications, even if the relay was down or restarting when you removed it. Before, `RelayClient.revoke` dropped the device's grant and sent one unsubscribe that was lost if the offline queue overflowed or the host stopped or restarted before the relay was back, so the relay kept the phone's push address. Now the unsubscribe is saved first and resent on every connection until the relay confirms it; `revoke()` resolves then. Pass a durable `store` to `RelayClient` so a pending unsubscribe survives a restart (the default is memory).
|
package/dist/agents.d.ts
CHANGED
|
@@ -4,4 +4,5 @@ export type Call = (method: string, params: Record<string, unknown>, timeoutMs?:
|
|
|
4
4
|
export declare function createAgents(ctx: {
|
|
5
5
|
call: Call;
|
|
6
6
|
snapshot(): HerdrSnapshot;
|
|
7
|
+
reread(paneId: string): Promise<void>;
|
|
7
8
|
}): Pick<HerdrKit, 'startAgent' | 'prompt' | 'sendKeys' | 'wait' | 'read' | 'agentKinds' | 'installedAgentKinds'>;
|
package/dist/agents.js
CHANGED
|
@@ -109,11 +109,18 @@ export function createAgents(ctx) {
|
|
|
109
109
|
}
|
|
110
110
|
}
|
|
111
111
|
async function prompt(target, text, o) {
|
|
112
|
-
// The readiness gate reads the kit's own tree
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
112
|
+
// The readiness gate reads the kit's own tree, re-read once before a refusal: `launch_pending`
|
|
113
|
+
// rides reads only, so a snapshot taken mid-launch goes stale. `interactive_ready` is not gated:
|
|
114
|
+
// Herdr sets it only for agents its own `agent.start` settled, and its `agent.prompt` accepts the
|
|
115
|
+
// rest. A refusal never sends `agent.prompt`.
|
|
116
|
+
const ready = () => {
|
|
117
|
+
const agent = agentOf(ctx.snapshot(), target.paneId);
|
|
118
|
+
return agent !== undefined && agent.launchPending !== true && PROMPTABLE.includes(agent.status);
|
|
119
|
+
};
|
|
120
|
+
if (!ready()) {
|
|
121
|
+
await ctx.reread(target.paneId);
|
|
122
|
+
if (!ready())
|
|
123
|
+
throw fail('agent-not-ready', 'That agent is not ready for a prompt yet.');
|
|
117
124
|
}
|
|
118
125
|
const wait = o?.wait === undefined ? undefined
|
|
119
126
|
: { timeout_ms: o.wait.timeoutMs, ...(o.wait.until === undefined ? {} : { until: o.wait.until }) };
|
|
@@ -123,9 +130,14 @@ export function createAgents(ctx) {
|
|
|
123
130
|
...(wait === undefined ? {} : { wait }) });
|
|
124
131
|
}
|
|
125
132
|
catch (error) {
|
|
126
|
-
|
|
133
|
+
const code = error?.code;
|
|
134
|
+
if (code === 'agent_blocked') {
|
|
127
135
|
throw fail('agent-blocked', 'That agent is blocked and needs an answer before a new prompt.');
|
|
128
136
|
}
|
|
137
|
+
// Herdr's own gate (unknown agent kind, launch pending, not the pane's foreground process).
|
|
138
|
+
if (code === 'agent_not_ready') {
|
|
139
|
+
throw fail('agent-not-ready', 'That agent is not ready for a prompt yet.');
|
|
140
|
+
}
|
|
129
141
|
throw error;
|
|
130
142
|
}
|
|
131
143
|
const body = isObj(receipt) ? receipt : undefined;
|
package/dist/kit.d.ts
CHANGED
|
@@ -23,7 +23,7 @@ export declare class HerdrKit {
|
|
|
23
23
|
private watchError;
|
|
24
24
|
private update;
|
|
25
25
|
private bootstrap;
|
|
26
|
-
private
|
|
26
|
+
private reread;
|
|
27
27
|
/**
|
|
28
28
|
* Connect and bootstrap. Non-fatal: a rejected `start()` (e.g. `failed/socket` while Herdr is
|
|
29
29
|
* down) leaves the kit stopped, and `start()` may be called again afterwards — the host comes up
|
package/dist/kit.js
CHANGED
|
@@ -131,7 +131,8 @@ export class HerdrKit {
|
|
|
131
131
|
constructor(o) {
|
|
132
132
|
this.o = o;
|
|
133
133
|
this.supervisor = new Supervisor(o, (s) => { this.current = s; o.onState?.(s); });
|
|
134
|
-
this.agents = createAgents({ call: this.callAny, snapshot: () => this.snapshot()
|
|
134
|
+
this.agents = createAgents({ call: this.callAny, snapshot: () => this.snapshot(),
|
|
135
|
+
reread: (paneId) => this.reread(paneId) });
|
|
135
136
|
this.blockedList = new Blocked({ call: this.callAny });
|
|
136
137
|
}
|
|
137
138
|
get state() { return this.current; }
|
|
@@ -292,16 +293,18 @@ export class HerdrKit {
|
|
|
292
293
|
this.statusStops.add(stop);
|
|
293
294
|
const ready = stop.ready;
|
|
294
295
|
if (ready)
|
|
295
|
-
statusAcks.push(ready.then((ok) => (ok === false ? undefined : this.
|
|
296
|
+
statusAcks.push(ready.then((ok) => (ok === false ? undefined : this.reread(pane.id, token))));
|
|
296
297
|
}
|
|
297
298
|
void Promise.allSettled(statusAcks).then(() => { if (token === this.generation)
|
|
298
299
|
resolveStatus(); });
|
|
299
300
|
this.current = newer ? { phase: 'needs-update', why: 'version' } : { phase: 'ready' };
|
|
300
301
|
this.o.onState?.(this.current);
|
|
301
302
|
}
|
|
302
|
-
//
|
|
303
|
-
//
|
|
304
|
-
|
|
303
|
+
// Re-read one agent and land its status, revision and launch flags: after a watch acks (a status
|
|
304
|
+
// change before the ack reached no socket) and before `prompt` refuses (`launch_pending` rides
|
|
305
|
+
// reads only, never a push). A push landing meanwhile is newer and wins status and revision (epoch
|
|
306
|
+
// guard); the launch flags still land, since no push carries them.
|
|
307
|
+
async reread(paneId, token = this.generation) {
|
|
305
308
|
const epoch = this.statusEpoch.get(paneId);
|
|
306
309
|
let made;
|
|
307
310
|
try {
|
|
@@ -311,12 +314,21 @@ export class HerdrKit {
|
|
|
311
314
|
return;
|
|
312
315
|
}
|
|
313
316
|
const agent = made?.agent;
|
|
314
|
-
if (token !== this.generation ||
|
|
315
|
-
typeof agent?.agent_status !== 'string')
|
|
317
|
+
if (token !== this.generation || typeof agent?.agent_status !== 'string')
|
|
316
318
|
return;
|
|
317
|
-
//
|
|
319
|
+
// v0.9.1 leaves both flags out when false (`skip_serializing_if = is_false`): absent means false.
|
|
320
|
+
const flags = { launch_pending: agent.launch_pending === true, interactive_ready: agent.interactive_ready === true };
|
|
321
|
+
if (this.statusEpoch.get(paneId) !== epoch) {
|
|
322
|
+
const pane = findPane(this.tree, paneId);
|
|
323
|
+
if (pane?.agent !== undefined) {
|
|
324
|
+
pane.agent = mergeAgent(pane.agent, flags);
|
|
325
|
+
this.publish();
|
|
326
|
+
}
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
// The read's other fields may be staler than a merged pane.updated.
|
|
318
330
|
this.update({ type: 'pane_agent_status_changed', pane_id: paneId, agent_status: agent.agent_status,
|
|
319
|
-
...(typeof agent.revision === 'number' ? { revision: agent.revision } : {}) });
|
|
331
|
+
...(typeof agent.revision === 'number' ? { revision: agent.revision } : {}), ...flags });
|
|
320
332
|
}
|
|
321
333
|
/**
|
|
322
334
|
* Connect and bootstrap. Non-fatal: a rejected `start()` (e.g. `failed/socket` while Herdr is
|
package/dist/notices.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// Sealed blocked-approval notices: the relay reads only a generic title, the phone opens the sealed body with its
|
|
2
2
|
// own key (docs/runtime-kits.md 7.3). Portable: no Node import anywhere in it — `./device` re-exports `openNotice`.
|
|
3
|
-
import { boxKeyPairFromSeed,
|
|
3
|
+
import { boxKeyPairFromSeed, openBoxFromSeed, sealBox } from '@byokit/seal';
|
|
4
4
|
const ABC = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_';
|
|
5
5
|
/** Base64url without padding, hand-rolled so this file stays portable (no Buffer, no atob). */
|
|
6
6
|
export function encodeB64Url(bytes) {
|
|
@@ -68,7 +68,7 @@ export function openNotice(data, seed) {
|
|
|
68
68
|
const bundle = decodeB64Url(data.sealed);
|
|
69
69
|
if (bundle === null)
|
|
70
70
|
return null;
|
|
71
|
-
const bytes =
|
|
71
|
+
const bytes = openBoxFromSeed(bundle, seed);
|
|
72
72
|
if (bytes === null)
|
|
73
73
|
return null;
|
|
74
74
|
let parsed;
|
|
@@ -88,19 +88,24 @@ async function runServer() {
|
|
|
88
88
|
// resizes the session and answers one empty frame stamped with the new size; `terminal.release`
|
|
89
89
|
// exits; `{"type":"fake.stream","count":n,"size":b,"progress":file}` streams n frames with full
|
|
90
90
|
// `<i>:` plus b bytes as fast as the pipe takes them, recording the frames-written count in
|
|
91
|
-
// `progress` after each write so a test can see how far a paused reader let the stream run
|
|
91
|
+
// `progress` after each write so a test can see how far a paused reader let the stream run, then
|
|
92
|
+
// one `{"type":"fake.stream.done","count":n}` line on the same ordered byte stream: when a test sees
|
|
93
|
+
// the marker, every stream frame is already past every frame handler, so the delivery verdict is
|
|
94
|
+
// causal instead of a wall-clock wait (a stall then means a genuinely wedged child, not a slow one).
|
|
92
95
|
function frame(paneId, full, extra = {}) {
|
|
93
96
|
return `${JSON.stringify({ type: 'terminal.frame', pane_id: paneId, data: full, full, bytes: Buffer.byteLength(full), ...extra })}\n`;
|
|
94
97
|
}
|
|
95
98
|
async function stream(paneId, o) {
|
|
99
|
+
const count = o.count ?? 0;
|
|
96
100
|
const pad = 'x'.repeat(o.size ?? 1024);
|
|
97
|
-
for (let i = 0; i <
|
|
101
|
+
for (let i = 0; i < count; i += 1) {
|
|
98
102
|
const ok = process.stdout.write(frame(paneId, `${i}:${pad}`));
|
|
99
103
|
if (o.progress)
|
|
100
104
|
writeFileSync(o.progress, String(i + 1));
|
|
101
105
|
if (!ok)
|
|
102
106
|
await new Promise((resolve) => process.stdout.once('drain', resolve));
|
|
103
107
|
}
|
|
108
|
+
process.stdout.write(`${JSON.stringify({ type: 'fake.stream.done', pane_id: paneId, count })}\n`);
|
|
104
109
|
}
|
|
105
110
|
function runTerminal(args) {
|
|
106
111
|
// `herdr terminal session <control|observe> <pane> [--takeover] --cols <n> --rows <n>`.
|
|
@@ -423,12 +423,12 @@ export async function startFakeHerdr(options) {
|
|
|
423
423
|
}
|
|
424
424
|
return { target: p.target, keys };
|
|
425
425
|
},
|
|
426
|
-
'agent.list': () => ({ agents: live.agents.map(
|
|
426
|
+
'agent.list': () => ({ agents: live.agents.map(agentWire) }),
|
|
427
427
|
'agent.get': (p) => {
|
|
428
428
|
const agent = live.agents.find((row) => row.pane_id === p.target);
|
|
429
429
|
if (agent === undefined)
|
|
430
430
|
throw fail('agent_not_found', 'agent not found');
|
|
431
|
-
return { agent };
|
|
431
|
+
return { agent: agentWire(agent) };
|
|
432
432
|
},
|
|
433
433
|
'server.agent_manifests': () => ({ manifests: kinds.map((kind) => ({ agent: kind })) }),
|
|
434
434
|
'plugin.list': (p) => {
|
|
@@ -607,10 +607,15 @@ export async function startFakeHerdr(options) {
|
|
|
607
607
|
function agentOf(pane) {
|
|
608
608
|
return live.agents.find((row) => row.pane_id === pane.pane_id);
|
|
609
609
|
}
|
|
610
|
+
// v0.9.1 serializes `launch_pending`/`interactive_ready` only when true (`skip_serializing_if = is_false`).
|
|
611
|
+
function agentWire(agent) {
|
|
612
|
+
const { launch_pending, interactive_ready, ...rest } = agent;
|
|
613
|
+
return { ...rest, ...(launch_pending ? { launch_pending } : {}), ...(interactive_ready ? { interactive_ready } : {}) };
|
|
614
|
+
}
|
|
610
615
|
function agentView(paneId) {
|
|
611
616
|
const agent = live.agents.find((row) => row.pane_id === paneId);
|
|
612
617
|
if (agent !== undefined)
|
|
613
|
-
return { agent:
|
|
618
|
+
return { agent: agentWire(agent) };
|
|
614
619
|
const pane = live.panes.find((row) => row.pane_id === paneId);
|
|
615
620
|
return { agent: { pane_id: paneId, agent_status: pane?.agent_status ?? 'unknown', interactive_ready: true } };
|
|
616
621
|
}
|
|
@@ -658,7 +663,7 @@ export async function startFakeHerdr(options) {
|
|
|
658
663
|
workspaces: live.workspaces.map(workspaceView),
|
|
659
664
|
tabs: live.tabs.map((tab) => ({ ...tab })),
|
|
660
665
|
panes: live.panes.map(paneView),
|
|
661
|
-
agents: live.agents.map(
|
|
666
|
+
agents: live.agents.map(agentWire),
|
|
662
667
|
layouts: [],
|
|
663
668
|
};
|
|
664
669
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@byokit/herdr",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.6",
|
|
4
4
|
"description": "Drive the Herdr on this computer — workspaces, tabs, panes, agents and blocked-approval answers — from an app, or hand it to a phone over @byokit/link.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
@@ -52,10 +52,10 @@
|
|
|
52
52
|
"prepack": "tsc -b && node ../../scripts/fix-words-dts.cjs"
|
|
53
53
|
},
|
|
54
54
|
"dependencies": {
|
|
55
|
-
"@byokit/link": "0.
|
|
56
|
-
"@byokit/relay": "0.
|
|
57
|
-
"@byokit/reach": "0.
|
|
58
|
-
"@byokit/seal": "0.
|
|
55
|
+
"@byokit/link": "0.4.0",
|
|
56
|
+
"@byokit/relay": "0.3.0",
|
|
57
|
+
"@byokit/reach": "0.3.0",
|
|
58
|
+
"@byokit/seal": "0.2.0",
|
|
59
59
|
"ws": "8.21.3"
|
|
60
60
|
},
|
|
61
61
|
"publishConfig": {
|