@paigy/harness 0.3.28 → 0.3.30
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 +16 -5
- package/dist/agy-acp.js +44 -8
- package/dist/cli.js +666 -296
- package/dist/main.js +4917 -4563
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
# paigy-harness
|
|
2
2
|
|
|
3
|
+
Phone launches persist their selected harness and Paigy session identity with the local slot.
|
|
4
|
+
Recovery uses that runtime, never a default Claude process. Older slots need a known named
|
|
5
|
+
runtime or a Claude transcript; otherwise the host logs a missing-runtime blocker.
|
|
6
|
+
|
|
7
|
+
The Antigravity adapter reconnects once after its specific `subscriber fell behind updates`
|
|
8
|
+
failure, using the conversation ID reported by `agy`. It asks the resumed agent to inspect
|
|
9
|
+
existing results before continuing, rather than replaying the original task. Missing IDs,
|
|
10
|
+
other errors, and a failed recovery remain visible failures. Diagnostics name the conversation,
|
|
11
|
+
recovery attempt, and outcome. This mitigates the vendor stream failure; it does not repair
|
|
12
|
+
Antigravity's internal broadcaster. Keep verbose command output in files and read bounded summaries.
|
|
13
|
+
|
|
3
14
|
Runs Claude Code and Codex on your machine, mirrors every turn to your Paigy inbox, and
|
|
4
15
|
— when you ask it to — **rings your phone when an agent is blocked** (#804).
|
|
5
16
|
|
|
@@ -23,13 +34,13 @@ The point isn't notifications, it's a conversation with an agent that happens to
|
|
|
23
34
|
running on your laptop. Both directions close (`paigy/conversation.ts`):
|
|
24
35
|
|
|
25
36
|
- **Answering.** The agent asks through Paigy's `contact` — its only channel to you; the
|
|
26
|
-
harness never relays its terminal output (`companion.md` Part 4, one channel) → your
|
|
37
|
+
harness never relays its terminal output (`docs/clients/desktop/companion.md` Part 4, one channel) → your
|
|
27
38
|
reply lands on its Goal and goes straight back into stdin as the next turn. Without this
|
|
28
39
|
the loop is one-directional and dies at the first idle.
|
|
29
40
|
- **Initiating.** You say something unprompted from your phone → it arrives on the agent's
|
|
30
41
|
Goal → same stdin. You never have to wait to be asked. A Claude agent in the middle of a task
|
|
31
42
|
sees your words right after its next tool call, and gets them again as its next turn
|
|
32
|
-
(`companion.md` Part 4, words reach a running turn).
|
|
43
|
+
(`docs/clients/desktop/companion.md` Part 4, words reach a running turn).
|
|
33
44
|
|
|
34
45
|
Answers are read out of whatever shape they arrive in (`spokenText`) — a tapped option,
|
|
35
46
|
a spoken sentence, a whole call log — because the words are what the agent needs, not
|
|
@@ -253,7 +264,7 @@ node apps/desktop/dist/cli.js --harness codex --cwd ~/repo "fix the flaky test"
|
|
|
253
264
|
node apps/desktop/dist/cli.js host --grant ~/projects # standby, launchable from the phone
|
|
254
265
|
```
|
|
255
266
|
|
|
256
|
-
The window deliberately has NO run form — launching moved to the phone (companion.md).
|
|
267
|
+
The window deliberately has NO run form — launching moved to the phone (docs/clients/desktop/companion.md).
|
|
257
268
|
It shows pairing, the party, the broker's card and queue, the doctor, the workspace
|
|
258
269
|
allow-list, a Stop button, and the log.
|
|
259
270
|
|
|
@@ -293,7 +304,7 @@ LAST, after the hooks are gone.
|
|
|
293
304
|
frame is skipped, never fatal. If mirroring goes quiet after a CLI update, re-verify
|
|
294
305
|
the frame names in the driver doc comments first. An adapter can also leave a prompt
|
|
295
306
|
unanswered for good (claude-agent-acp#1145); the driver cancels one after a long quiet
|
|
296
|
-
with no tool running (`companion.md` Part 4, one way in).
|
|
307
|
+
with no tool running (`docs/clients/desktop/companion.md` Part 4, one way in).
|
|
297
308
|
- **Auth probes are vendor commands** (`claude auth status`, `codex login status`) and
|
|
298
309
|
can drift with CLI releases; a probe failure reads as "needs login", so drift shows
|
|
299
310
|
up as a nagging hint, not a broken start.
|
|
@@ -306,4 +317,4 @@ LAST, after the hooks are gone.
|
|
|
306
317
|
- **The host service IS the daemon** (`paigy-harness service`, launchd, KeepAlive) —
|
|
307
318
|
it claims phone launches and runs sessions. The WAKE channel for terminal-agent
|
|
308
319
|
replies still belongs to `paigy-listen` + `PAIGY_ON_WAKE` (#486); spawn-on-wake
|
|
309
|
-
needs the slot-lease design in companion.md before it auto-wires.
|
|
320
|
+
needs the slot-lease design in docs/clients/desktop/companion.md before it auto-wires.
|
package/dist/agy-acp.js
CHANGED
|
@@ -22,6 +22,13 @@ var AgyAcp = class {
|
|
|
22
22
|
bypass = false;
|
|
23
23
|
started = false;
|
|
24
24
|
promptId;
|
|
25
|
+
conversation;
|
|
26
|
+
dropped = false;
|
|
27
|
+
retried = false;
|
|
28
|
+
/** The vendor can fail on stderr without a result frame. Never retry other failures. */
|
|
29
|
+
diagnostic(text) {
|
|
30
|
+
if (text.includes("subscriber fell behind updates")) this.dropped = true;
|
|
31
|
+
}
|
|
25
32
|
/** A frame from the harness. */
|
|
26
33
|
fromClient(msg) {
|
|
27
34
|
const out = { client: [], agy: [] };
|
|
@@ -50,6 +57,8 @@ var AgyAcp = class {
|
|
|
50
57
|
out.start = { cwd: this.cwd, args: agyArgs(this.bypass) };
|
|
51
58
|
}
|
|
52
59
|
this.promptId = msg.id;
|
|
60
|
+
this.dropped = false;
|
|
61
|
+
this.retried = false;
|
|
53
62
|
out.agy.push(JSON.stringify({ event: "user", message: { role: "user", content } }));
|
|
54
63
|
return out;
|
|
55
64
|
}
|
|
@@ -68,6 +77,10 @@ var AgyAcp = class {
|
|
|
68
77
|
return out;
|
|
69
78
|
}
|
|
70
79
|
const update = (body) => out.client.push(frame({ method: "session/update", params: { sessionId: this.sessionId, update: body } }));
|
|
80
|
+
if (ev.event === "init" && typeof ev.conversation_id === "string" && /^[0-9a-f-]{36}$/i.test(ev.conversation_id)) {
|
|
81
|
+
this.conversation = ev.conversation_id;
|
|
82
|
+
out.diagnostic = `agy conversation=${this.conversation} recovery=${this.retried ? 1 : 0} connected`;
|
|
83
|
+
}
|
|
71
84
|
if (ev.event === "step_update") {
|
|
72
85
|
const s = ev.step_update ?? {};
|
|
73
86
|
if (s.step_type === "agent_response" && typeof s.text_delta === "string" && s.text_delta) {
|
|
@@ -79,6 +92,9 @@ var AgyAcp = class {
|
|
|
79
92
|
}
|
|
80
93
|
if (ev.event === "result" && this.promptId !== void 0) {
|
|
81
94
|
const r = ev.result ?? {};
|
|
95
|
+
if (r.status !== "SUCCESS") this.diagnostic(String(r.error ?? ""));
|
|
96
|
+
if (r.status !== "SUCCESS" && this.dropped && this.conversation && !this.retried) return out;
|
|
97
|
+
if (this.retried) out.diagnostic = `agy conversation=${this.conversation} recovery=1 outcome=${r.status === "SUCCESS" ? "success" : "failed"}`;
|
|
82
98
|
const denied = Array.isArray(r.denied_actions) ? r.denied_actions : [];
|
|
83
99
|
if (denied.length) {
|
|
84
100
|
const names = denied.map((d) => d.display_name ?? d.action ?? "a tool").join(", ");
|
|
@@ -96,10 +112,20 @@ Antigravity cannot ask for permission when it runs in the background, so it was
|
|
|
96
112
|
return out;
|
|
97
113
|
}
|
|
98
114
|
/** `agy` exited: a turn still open fails by name rather than hanging the harness. */
|
|
99
|
-
exited(code) {
|
|
115
|
+
exited(code, recover = true) {
|
|
100
116
|
const out = { client: [], agy: [] };
|
|
117
|
+
if (recover && this.promptId !== void 0 && this.dropped && this.conversation && !this.retried) {
|
|
118
|
+
this.retried = true;
|
|
119
|
+
this.dropped = false;
|
|
120
|
+
out.start = { cwd: this.cwd, args: [...agyArgs(this.bypass), "--conversation", this.conversation] };
|
|
121
|
+
out.agy.push(JSON.stringify({ event: "user", message: { role: "user", content: "The update stream disconnected. Inspect this conversation and existing command results before continuing the unfinished work. Do not repeat completed actions or blindly rerun the previous command. Redirect verbose command output to a file and read only a bounded summary. If the outcome of an action is uncertain, verify it before retrying; report a blocker if you cannot do so safely." } }));
|
|
122
|
+
out.diagnostic = `agy conversation=${this.conversation} recovery=1 reason=subscriber_lag exit=${code} reconnecting`;
|
|
123
|
+
return out;
|
|
124
|
+
}
|
|
101
125
|
if (this.promptId !== void 0) {
|
|
102
|
-
|
|
126
|
+
const message = this.retried ? `Antigravity stream recovery failed for conversation ${this.conversation}; inspect its results before retrying` : `agy exited ${code ?? "by signal"}`;
|
|
127
|
+
out.client.push(frame({ id: this.promptId, error: { code: -32e3, message } }));
|
|
128
|
+
out.diagnostic = `agy conversation=${this.conversation ?? "unknown"} recovery=${this.retried ? 1 : 0} outcome=failed exit=${code}`;
|
|
103
129
|
this.promptId = void 0;
|
|
104
130
|
}
|
|
105
131
|
return out;
|
|
@@ -108,22 +134,31 @@ Antigravity cannot ask for permission when it runs in the background, so it was
|
|
|
108
134
|
function main() {
|
|
109
135
|
const adapter = new AgyAcp();
|
|
110
136
|
let agy;
|
|
137
|
+
let closed = false;
|
|
111
138
|
const apply = (out) => {
|
|
139
|
+
if (out.diagnostic) process.stderr.write(`${out.diagnostic}
|
|
140
|
+
`);
|
|
112
141
|
for (const f of out.client) process.stdout.write(`${f}
|
|
113
142
|
`);
|
|
114
143
|
if (out.start) {
|
|
115
144
|
agy = spawn("agy", out.start.args, { cwd: out.start.cwd, stdio: ["pipe", "pipe", "pipe"], env: process.env });
|
|
145
|
+
agy.stdin.on("error", () => {
|
|
146
|
+
});
|
|
116
147
|
createInterface({ input: agy.stdout }).on("line", (l) => apply(adapter.fromAgy(l)));
|
|
117
|
-
|
|
118
|
-
agy.on("
|
|
119
|
-
|
|
120
|
-
|
|
148
|
+
let diagnosticTail = "";
|
|
149
|
+
agy.stderr.on("data", (chunk) => {
|
|
150
|
+
adapter.diagnostic(diagnosticTail + String(chunk));
|
|
151
|
+
diagnosticTail = (diagnosticTail + String(chunk)).slice(-512);
|
|
152
|
+
process.stderr.write(chunk);
|
|
153
|
+
});
|
|
154
|
+
agy.on("close", (code) => {
|
|
155
|
+
const next = adapter.exited(code, !closed);
|
|
156
|
+
apply(next);
|
|
157
|
+
if (!next.start) process.stdout.write("", () => process.exit(code ?? 1));
|
|
121
158
|
});
|
|
122
159
|
agy.on("error", (e) => {
|
|
123
160
|
process.stderr.write(`agy: ${e.message}
|
|
124
161
|
`);
|
|
125
|
-
apply(adapter.exited(null));
|
|
126
|
-
process.exit(1);
|
|
127
162
|
});
|
|
128
163
|
}
|
|
129
164
|
for (const l of out.agy) agy?.stdin?.write(`${l}
|
|
@@ -135,6 +170,7 @@ function main() {
|
|
|
135
170
|
} catch {
|
|
136
171
|
}
|
|
137
172
|
}).on("close", () => {
|
|
173
|
+
closed = true;
|
|
138
174
|
agy?.stdin?.end();
|
|
139
175
|
if (!agy) process.exit(0);
|
|
140
176
|
});
|