@uuaid/pillar 0.2.0-alpha.7 → 0.2.0-alpha.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/bin/pillar.mjs +30 -12
- package/package.json +1 -1
- package/src/bridges/zestro.mjs +89 -17
package/bin/pillar.mjs
CHANGED
|
@@ -323,6 +323,10 @@ async function cmdRecv(args) {
|
|
|
323
323
|
url: process.env.PILLAR_ZESTRO_URL ?? args["zestro-url"],
|
|
324
324
|
token: process.env.PILLAR_ZESTRO_TOKEN ?? args["zestro-token"],
|
|
325
325
|
sessionId: process.env.PILLAR_ZESTRO_AGENT ?? args["zestro-agent"],
|
|
326
|
+
tabId: process.env.PILLAR_ZESTRO_TAB ?? args["zestro-tab"],
|
|
327
|
+
// Opt-in: force on a busy tab spawns a SECOND agent process resuming the
|
|
328
|
+
// same transcript (handle-spawn.js has no live-process guard). Default off.
|
|
329
|
+
force: /^1|true|yes$/i.test(process.env.PILLAR_ZESTRO_FORCE ?? "") || !!args["zestro-force"],
|
|
326
330
|
});
|
|
327
331
|
await bridge.connect();
|
|
328
332
|
// Never log the full URL — it embeds the auth token.
|
|
@@ -336,24 +340,36 @@ async function cmdRecv(args) {
|
|
|
336
340
|
if (bridge && env.kind === "request") {
|
|
337
341
|
// Interaction-convention return leg: ack FIRST — pillar-ask allows 120s
|
|
338
342
|
// per phase but ZestroBridge times out at 30s, so the ack goes out before
|
|
339
|
-
// the bridged agent turn starts.
|
|
340
|
-
//
|
|
341
|
-
//
|
|
343
|
+
// the bridged agent turn starts. Reply statuses (verified against app code):
|
|
344
|
+
// "ok" = command_result received = renderer consumed / spawn initiated
|
|
345
|
+
// (NOT turn-complete — the turn's answer returns via a separate
|
|
346
|
+
// pillar send from the agent, if the prompt instructs one)
|
|
347
|
+
// "busy" = target tab busy, server rejected (fast, no 30s hang)
|
|
348
|
+
// "pending" = bridge timeout — the request was never confirmed
|
|
349
|
+
// "error" = bridge failure
|
|
342
350
|
const thread = env.payload?.thread ?? env.id;
|
|
343
351
|
const senderKey = env.transportSignature.publicKey; // open() F1: sender derives from this key
|
|
344
352
|
try {
|
|
345
353
|
await p.send({ recipient: env.sender, recipientPublicKey: senderKey, kind: "ack", payload: { inReplyTo: env.id } });
|
|
346
354
|
const result = await bridge.sendCommand({ command: JSON.stringify({ kind: env.kind, payload: env.payload, sender: env.sender }) });
|
|
347
|
-
const
|
|
355
|
+
const hasText = result && typeof result === "object" &&
|
|
356
|
+
["text", "result", "output", "response", "message"].some(k => k in result);
|
|
357
|
+
const text = hasText
|
|
358
|
+
? replyTextFromResult(result)
|
|
359
|
+
: "zestro command_result received (server accepted) — agent turn answer, if any, returns via separate pillar send";
|
|
348
360
|
await p.send({ recipient: env.sender, recipientPublicKey: senderKey, kind: "reply", payload: { thread, inReplyTo: env.id, status: "ok", text } });
|
|
349
361
|
console.log(` → zestro reply sent (${text.length} bytes)`);
|
|
350
362
|
} catch (e) {
|
|
351
|
-
//
|
|
352
|
-
//
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
363
|
+
// ZestroServerError SESSION_BUSY = server rejected: target tab busy and
|
|
364
|
+
// force off. Immediate distinct status — the sender can retry or drop.
|
|
365
|
+
if (e.code === "SESSION_BUSY") {
|
|
366
|
+
console.log(` → zestro: busy (${e.message})`);
|
|
367
|
+
await p.send({ recipient: env.sender, recipientPublicKey: senderKey, kind: "reply", payload: { thread, inReplyTo: env.id, status: "busy", reason: e.message.slice(0, 300), text: `zestro agent busy: ${e.message.slice(0, 300)}` } });
|
|
368
|
+
} else if (/timed out/.test(e.message)) {
|
|
369
|
+
// Bridge timeout = the server never confirmed delivery (renderer drop,
|
|
370
|
+
// cli-server gone, etc.) — accepted-unconfirmed, not turn-running.
|
|
371
|
+
console.log(` → zestro: unconfirmed (bridge timeout)`);
|
|
372
|
+
await p.send({ recipient: env.sender, recipientPublicKey: senderKey, kind: "reply", payload: { thread, inReplyTo: env.id, status: "pending", text: "request forwarded to zestro agent — delivery unconfirmed" } });
|
|
357
373
|
} else {
|
|
358
374
|
console.error(` → zestro bridge error: ${e.message}`);
|
|
359
375
|
await p.send({ recipient: env.sender, recipientPublicKey: senderKey, kind: "reply", payload: { thread, inReplyTo: env.id, status: "error", reason: e.message.slice(0, 300), text: `zestro bridge error: ${e.message.slice(0, 300)}` } });
|
|
@@ -364,8 +380,10 @@ async function cmdRecv(args) {
|
|
|
364
380
|
const result = await bridge.sendCommand({ command: JSON.stringify({ kind: env.kind, payload: env.payload, sender: env.sender }) });
|
|
365
381
|
console.log(` → zestro result: ${JSON.stringify(result).slice(0, 200)}`);
|
|
366
382
|
} catch (e) {
|
|
367
|
-
if (
|
|
368
|
-
console.log(` → zestro:
|
|
383
|
+
if (e.code === "SESSION_BUSY") {
|
|
384
|
+
console.log(` → zestro: busy (${e.message})`);
|
|
385
|
+
} else if (/timed out/.test(e.message)) {
|
|
386
|
+
console.log(` → zestro: unconfirmed (bridge timeout)`);
|
|
369
387
|
} else {
|
|
370
388
|
console.error(` → zestro bridge error: ${e.message}`);
|
|
371
389
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@uuaid/pillar",
|
|
3
|
-
"version": "0.2.0-alpha.
|
|
3
|
+
"version": "0.2.0-alpha.8",
|
|
4
4
|
"description": "IAASO-3050 reference implementation. Every agent runs its own PDP pillar, discovers peers via a Kademlia DHT, and exchanges cryptographically signed + Noise-encrypted messages over libp2p. One command to install and join the network.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|
package/src/bridges/zestro.mjs
CHANGED
|
@@ -1,21 +1,45 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Zestro WebSocket bridge.
|
|
3
3
|
*
|
|
4
|
-
* Wire protocol
|
|
4
|
+
* Wire protocol — verified against the OFFICIAL client contract
|
|
5
|
+
* (Zestro app.asar: dist/cli/maestro-client.ts, sourcesContent from
|
|
6
|
+
* maestro-cli.js.map) and the app's own server/renderer handlers:
|
|
5
7
|
* ws://127.0.0.1:53522/<token>/ws
|
|
6
|
-
* -> { type: "send_command",
|
|
7
|
-
*
|
|
8
|
+
* -> { type: "send_command", requestId: "req_...", sessionId: <agentId>,
|
|
9
|
+
* command: <str>, inputMode: "ai", tabId?: <uuid>, force?: true }
|
|
10
|
+
* <- { type: "command_result", success, sessionId, tabId?, requestId } // echoes requestId
|
|
11
|
+
* <- { type: "error", message, sessionId } // NO requestId echo
|
|
12
|
+
*
|
|
13
|
+
* Correlation is on `requestId`, NOT `id` — the server echoes only
|
|
14
|
+
* `requestId`, so a client correlating on `id` can never match a reply.
|
|
15
|
+
*
|
|
16
|
+
* Semantics (verified in app code, all four layers):
|
|
17
|
+
* - command_result arrives IMMEDIATELY after the renderer receives the
|
|
18
|
+
* command — it means "consumed by the renderer / spawn initiated", NOT
|
|
19
|
+
* "the agent's turn completed". The turn answer returns out-of-band.
|
|
20
|
+
* - A busy target session WITHOUT force is rejected by the server
|
|
21
|
+
* ({type:"error"}), and the renderer has TWO more busy drops of its own.
|
|
22
|
+
* Errors carry no requestId, so they must be handled single-flight.
|
|
23
|
+
* - force=true on a BUSY tab spawns a SECOND agent process resuming the
|
|
24
|
+
* same session transcript (handle-spawn.js: no live-process guard,
|
|
25
|
+
* --resume <agentSessionId>). Concurrent, not queued. Opt-in only.
|
|
8
26
|
*
|
|
9
27
|
* Token lives in ~/Library/Application Support/zestro/cli-server.json.
|
|
10
28
|
* Agent id for ZZ was verified to be 7fbce535-3a2f-4e6b-96e6-64926403efa5.
|
|
11
|
-
*
|
|
12
|
-
* Correlation on `id` so multiple concurrent sends don't race.
|
|
13
29
|
*/
|
|
14
30
|
import { WebSocket } from "ws";
|
|
15
|
-
|
|
31
|
+
|
|
32
|
+
/** Server rejected the command (busy session, bad payload, ...). */
|
|
33
|
+
export class ZestroServerError extends Error {
|
|
34
|
+
constructor(message, code = "ZESTRO_SERVER_ERROR") {
|
|
35
|
+
super(message);
|
|
36
|
+
this.name = "ZestroServerError";
|
|
37
|
+
this.code = code;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
16
40
|
|
|
17
41
|
export class ZestroBridge {
|
|
18
|
-
constructor({ url, token, sessionId, timeoutMs = 30000 }) {
|
|
42
|
+
constructor({ url, token, sessionId, tabId, timeoutMs = 30000, force = false }) {
|
|
19
43
|
if (!url || !token || !sessionId) {
|
|
20
44
|
throw new Error("ZestroBridge requires { url, token, sessionId }");
|
|
21
45
|
}
|
|
@@ -26,7 +50,9 @@ export class ZestroBridge {
|
|
|
26
50
|
}
|
|
27
51
|
this.url = url;
|
|
28
52
|
this.sessionId = sessionId;
|
|
53
|
+
this.tabId = tabId ?? null;
|
|
29
54
|
this.timeoutMs = timeoutMs;
|
|
55
|
+
this.force = force;
|
|
30
56
|
this._ws = null;
|
|
31
57
|
this._pending = new Map();
|
|
32
58
|
}
|
|
@@ -44,35 +70,81 @@ export class ZestroBridge {
|
|
|
44
70
|
ws.on("message", (buf) => {
|
|
45
71
|
let msg;
|
|
46
72
|
try { msg = JSON.parse(buf.toString()); } catch { return; }
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
73
|
+
|
|
74
|
+
// `echo` = the app did not recognize the message type (older build).
|
|
75
|
+
if (msg.type === "echo") {
|
|
76
|
+
const original = msg.data ?? {};
|
|
77
|
+
const reqId = original.requestId ?? msg.originalRequestId;
|
|
78
|
+
if (reqId && this._pending.has(reqId)) {
|
|
79
|
+
this._settle(reqId, p => p.reject(new ZestroServerError(
|
|
80
|
+
`Zestro app does not support '${original.type ?? "unknown"}' (older build?)`,
|
|
81
|
+
"ZESTRO_UNSUPPORTED"
|
|
82
|
+
)));
|
|
83
|
+
}
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Exact correlation: server echoes requestId on command_result.
|
|
88
|
+
if (msg.requestId && this._pending.has(msg.requestId)) {
|
|
89
|
+
this._settle(msg.requestId, p => p.resolve(msg));
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Single-flight: server `error` replies carry NO requestId (verified:
|
|
94
|
+
// sendError sends {type:'error', message, ...extra}). The bridge keeps
|
|
95
|
+
// at most one request in flight, so route orphan replies to it.
|
|
96
|
+
if (msg.type === "error") {
|
|
97
|
+
const message = msg.message ?? "zestro server error";
|
|
98
|
+
const code = /busy/i.test(message) ? "SESSION_BUSY" : "ZESTRO_SERVER_ERROR";
|
|
99
|
+
this._settleOldest(p => p.reject(new ZestroServerError(message, code)));
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
if (msg.type === "command_result") {
|
|
103
|
+
this._settleOldest(p => p.resolve(msg));
|
|
51
104
|
}
|
|
52
105
|
});
|
|
53
106
|
});
|
|
54
107
|
}
|
|
55
108
|
|
|
56
|
-
|
|
109
|
+
_settle(requestId, fn) {
|
|
110
|
+
const p = this._pending.get(requestId);
|
|
111
|
+
if (p) {
|
|
112
|
+
this._pending.delete(requestId);
|
|
113
|
+
fn(p);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
_settleOldest(fn) {
|
|
118
|
+
const first = this._pending.entries().next();
|
|
119
|
+
if (!first.done) {
|
|
120
|
+
const [requestId, p] = first.value;
|
|
121
|
+
this._pending.delete(requestId);
|
|
122
|
+
fn(p);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
sendCommand({ command, inputMode = "ai", force } = {}) {
|
|
57
127
|
if (!this._ws || this._ws.readyState !== WebSocket.OPEN) {
|
|
58
128
|
return Promise.reject(new Error("zestro bridge not connected"));
|
|
59
129
|
}
|
|
60
|
-
const
|
|
130
|
+
const requestId = `req_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
|
|
61
131
|
return new Promise((resolve, reject) => {
|
|
62
132
|
const timer = setTimeout(() => {
|
|
63
|
-
this._pending.delete(
|
|
64
|
-
reject(new Error(`zestro send_command ${
|
|
133
|
+
this._pending.delete(requestId);
|
|
134
|
+
reject(new Error(`zestro send_command ${requestId} timed out after ${this.timeoutMs}ms`));
|
|
65
135
|
}, this.timeoutMs);
|
|
66
|
-
this._pending.set(
|
|
136
|
+
this._pending.set(requestId, {
|
|
67
137
|
resolve: (msg) => { clearTimeout(timer); resolve(msg); },
|
|
68
138
|
reject: (e) => { clearTimeout(timer); reject(e); },
|
|
69
139
|
});
|
|
70
140
|
this._ws.send(JSON.stringify({
|
|
71
141
|
type: "send_command",
|
|
72
|
-
|
|
142
|
+
requestId,
|
|
73
143
|
sessionId: this.sessionId,
|
|
74
144
|
command,
|
|
75
145
|
inputMode,
|
|
146
|
+
...(this.tabId ? { tabId: this.tabId } : {}),
|
|
147
|
+
...((force ?? this.force) ? { force: true } : {}),
|
|
76
148
|
}));
|
|
77
149
|
});
|
|
78
150
|
}
|