patchcord 0.6.46 → 0.6.48
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/.claude-plugin/plugin.json +1 -1
- package/agent-plugin/.codex-plugin/plugin.json +1 -1
- package/agent-plugin/plugin.json +1 -1
- package/agent-plugin/skills/subscribe/SKILL.md +49 -23
- package/package.json +1 -1
- package/per-project-skills/jcode/subscribe/SKILL.md +15 -6
- package/scripts/lib/stall-signal.mjs +15 -2
- package/scripts/subscribe.mjs +27 -5
- package/skills/subscribe/SKILL.md +49 -23
package/agent-plugin/plugin.json
CHANGED
|
@@ -44,33 +44,59 @@ WAKES YOU: the pipe fills with messages you are never told about, and you
|
|
|
44
44
|
discover them only when the user asks why you did not answer. That has happened
|
|
45
45
|
in production, on jcode, and is the reason this section exists.
|
|
46
46
|
|
|
47
|
-
So, if you have no `Monitor
|
|
47
|
+
So, if you have no `Monitor`, **use `--stall-signal`. It was built for exactly
|
|
48
|
+
this and it is not optional here:**
|
|
48
49
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
sign that anything is wrong.
|
|
50
|
+
```
|
|
51
|
+
patchcord subscribe --stall-signal
|
|
52
|
+
```
|
|
53
53
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
54
|
+
run as a background command with your harness's stall wake set to **15
|
|
55
|
+
seconds** (jcode: `stall_wake_seconds: 15`).
|
|
56
|
+
|
|
57
|
+
1. **What `--stall-signal` does.** While idle it writes a `HEARTBEAT:` line
|
|
58
|
+
every 5 s, so the pipe never looks silent and the stall never fires on
|
|
59
|
+
nothing. When a real message arrives it writes the `PATCHCORD:` line and
|
|
60
|
+
then goes QUIET ON PURPOSE for 15 s, so your harness's stall detector fires
|
|
61
|
+
because a message came in. It turns wake-on-silence into wake-on-message
|
|
62
|
+
using only the primitive your harness has.
|
|
63
|
+
|
|
64
|
+
**Run it raw. Do not pipe it through `grep`.** Step 2's command filters
|
|
65
|
+
stdout down to `^PATCHCORD:` lines, and that filter removes the keepalives
|
|
66
|
+
this mode depends on. Your harness measures the stall on the OUTPUT of the
|
|
67
|
+
pipeline, so a filtered stream is silent all the time: the wake fires on
|
|
68
|
+
nothing every 15 s, and the deliberate quiet after a real message looks
|
|
69
|
+
exactly like idle. Filtering here does not merely add noise, it deletes the
|
|
70
|
+
signal. Ignore the `HEARTBEAT:` lines when you read the output; they exist
|
|
71
|
+
for the stall detector, not for you.
|
|
72
|
+
|
|
73
|
+
2. **The two numbers must agree.** Your stall wake MUST equal `--stall-signal`'s
|
|
74
|
+
quiet window. Both default to 15 s, so setting `15` and passing no inline
|
|
75
|
+
value is correct. If you pass a custom triple
|
|
76
|
+
(`--stall-signal keepaliveMs:quietMs:stallMs`), your stall wake must equal
|
|
77
|
+
the `stallMs` you passed. Too short and it fires on nothing; too long and it
|
|
78
|
+
never fires on a real message. **Do not use 30** — that number predates this
|
|
79
|
+
mode and is simply wrong for it, not safer.
|
|
59
80
|
|
|
60
81
|
3. **NEVER read the last `PATCHCORD:` line in the task output as news.** It is
|
|
61
|
-
scrollback
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
82
|
+
scrollback and it may be a line you already handled. Two lines look almost
|
|
83
|
+
alike: a live arrival, and a `... waiting in inbox (snapshot at HH:MM:SS)`
|
|
84
|
+
written once when the listener connected and drained the queue. The
|
|
85
|
+
timestamp on the second is there so you can see it is old — check it. In a
|
|
86
|
+
real jcode session an agent re-announced that same drain line repeatedly and
|
|
87
|
+
missed five actual messages while doing so. On every wake call
|
|
88
|
+
`mcp__patchcord__inbox` and believe only that. If it is empty, say nothing
|
|
89
|
+
and go back to waiting.
|
|
90
|
+
|
|
91
|
+
4. **If the inbox is empty, check the listener is still alive** before assuming
|
|
92
|
+
a harmless false wake. A dead listener produces the same silence. If a
|
|
93
|
+
restart is refused with `already running (pid N)`, the process outlived the
|
|
94
|
+
task that tracked it: restart with `--replace`, which is the only sanctioned
|
|
95
|
+
way to remove a running listener. Never `kill`/`pkill` by hand.
|
|
96
|
+
|
|
97
|
+
**Only if your harness has no stall setting at all** does this degrade to a
|
|
98
|
+
timed poll. Say so plainly to the user in that case — tell them you will check
|
|
99
|
+
regularly, never that you will be woken "as messages arrive".
|
|
74
100
|
|
|
75
101
|
# When a notification fires
|
|
76
102
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "patchcord",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.48",
|
|
4
4
|
"description": "Cross-machine agent messaging for Claude Code and Codex",
|
|
5
5
|
"scripts": {
|
|
6
6
|
"version": "node scripts/sync-plugin-version.mjs && git add .claude-plugin/plugin.json agent-plugin/plugin.json"
|
|
@@ -31,7 +31,7 @@ path.
|
|
|
31
31
|
|
|
32
32
|
```
|
|
33
33
|
Bash(
|
|
34
|
-
command: "patchcord subscribe --stall-signal
|
|
34
|
+
command: "patchcord subscribe --stall-signal",
|
|
35
35
|
run_in_background: true,
|
|
36
36
|
stall_wake_seconds: 15
|
|
37
37
|
)
|
|
@@ -46,10 +46,19 @@ path.
|
|
|
46
46
|
number predates `--stall-signal` and is shorter than nothing: it is
|
|
47
47
|
simply the wrong number for this mechanism, not a safer one.
|
|
48
48
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
your
|
|
52
|
-
the
|
|
49
|
+
**DO NOT PIPE THIS THROUGH `grep`.** Every other subscribe command in this
|
|
50
|
+
plugin filters stdout down to `^PATCHCORD:` lines, and doing that here
|
|
51
|
+
breaks the entire mechanism: your harness measures the stall on what comes
|
|
52
|
+
OUT of the pipeline, and `grep` removes exactly the `HEARTBEAT:` keepalives
|
|
53
|
+
that are supposed to keep the pipe looking alive. Filtered, the stream is
|
|
54
|
+
silent all the time, so the wake fires on nothing every 15 s and the quiet
|
|
55
|
+
window after a real message is indistinguishable from idle. That is the
|
|
56
|
+
noisy, useless wake loop `--stall-signal` exists to replace.
|
|
57
|
+
|
|
58
|
+
So the raw stream is what the background task must see. It carries
|
|
59
|
+
`HEARTBEAT: <timestamp>` every 5 s and `PATCHCORD: ...` on a real message.
|
|
60
|
+
Ignore the heartbeats when you read the output; they are for the stall
|
|
61
|
+
detector, not for you.
|
|
53
62
|
|
|
54
63
|
3. **Tell the user one line:** "Patchcord listener active — I'll check
|
|
55
64
|
regularly for new messages." Do not say "as messages arrive" — the
|
|
@@ -83,7 +92,7 @@ cannot tell which one happened from the wake alone:
|
|
|
83
92
|
|
|
84
93
|
```
|
|
85
94
|
Bash(
|
|
86
|
-
command: "patchcord subscribe --replace --stall-signal
|
|
95
|
+
command: "patchcord subscribe --replace --stall-signal",
|
|
87
96
|
run_in_background: true,
|
|
88
97
|
stall_wake_seconds: 15
|
|
89
98
|
)
|
|
@@ -21,8 +21,21 @@
|
|
|
21
21
|
// pidfile — all things subscribe.mjs does as soon as it runs.
|
|
22
22
|
//
|
|
23
23
|
// THE KEEPALIVE LINE ITSELF (written by subscribe.mjs, not here) starts with
|
|
24
|
-
// "HEARTBEAT:", which does not match `^PATCHCORD
|
|
25
|
-
//
|
|
24
|
+
// "HEARTBEAT:", which does not match `^PATCHCORD:`.
|
|
25
|
+
//
|
|
26
|
+
// THAT USED TO BE DESCRIBED HERE AS A CONVENIENCE — the grep every other
|
|
27
|
+
// subscribe skill uses "drops it for free". It is the opposite, and the
|
|
28
|
+
// mistake cost a working listener in production: the harness measures the
|
|
29
|
+
// stall on what comes OUT of the pipeline, so a `| grep '^PATCHCORD:'` in
|
|
30
|
+
// front of a --stall-signal listener deletes every keepalive before anything
|
|
31
|
+
// can see it. The stream is then silent all the time, the wake fires on
|
|
32
|
+
// nothing every stall window, and the deliberate quiet after a real message
|
|
33
|
+
// is indistinguishable from idle. Filtering does not add noise here, it
|
|
34
|
+
// removes the signal.
|
|
35
|
+
//
|
|
36
|
+
// So the keepalive prefix exists to be IGNORED BY A READER, not removed by a
|
|
37
|
+
// filter. --stall-signal must be run raw. Every skill that documents it is
|
|
38
|
+
// pinned to that by tests in tests/test-stall-signal.mjs.
|
|
26
39
|
|
|
27
40
|
export const STALL_SIGNAL_DEFAULTS = Object.freeze({
|
|
28
41
|
// Written to stdout on this interval while idle. Must stay well under
|
package/scripts/subscribe.mjs
CHANGED
|
@@ -53,10 +53,12 @@ const STALL_SIGNAL_MODE = STALL_SIGNAL_OPTS !== null;
|
|
|
53
53
|
if (STALL_SIGNAL_MODE && HERMES_MODE) {
|
|
54
54
|
die("--stall-signal and --hermes are mutually exclusive — each is its own wake mechanism, pick one");
|
|
55
55
|
}
|
|
56
|
-
// The keepalive line's own prefix, chosen so it can never collide with a
|
|
57
|
-
//
|
|
58
|
-
//
|
|
59
|
-
//
|
|
56
|
+
// The keepalive line's own prefix, chosen so it can never collide with a real
|
|
57
|
+
// notification. It must be IGNORED by whoever reads the output, and never
|
|
58
|
+
// FILTERED OUT of it: a `| grep '^PATCHCORD:'` in front of a --stall-signal
|
|
59
|
+
// listener removes exactly the lines that keep the pipe from looking idle,
|
|
60
|
+
// which turns wake-on-message back into wake-on-silence. See the header of
|
|
61
|
+
// scripts/lib/stall-signal.mjs.
|
|
60
62
|
const STALL_SIGNAL_KEEPALIVE_PREFIX = "HEARTBEAT:";
|
|
61
63
|
// Suppress keepalives until this timestamp (ms since epoch). 0 = never
|
|
62
64
|
// suppressed. Set by notify() on every real message.
|
|
@@ -295,7 +297,20 @@ async function drainQueueOnce(baseUrl, token) {
|
|
|
295
297
|
count = JSON.parse(res.body).pending_count ?? 0;
|
|
296
298
|
} catch (_) {}
|
|
297
299
|
if (count > 0) {
|
|
298
|
-
|
|
300
|
+
// SELF-DATING, DELIBERATELY. This line and a live-arrival line both
|
|
301
|
+
// start with "PATCHCORD:" and both stay in the task output forever.
|
|
302
|
+
// On a harness that wakes on a stall, the agent re-reads the tail of
|
|
303
|
+
// that output on every wake, and an undated snapshot from twenty
|
|
304
|
+
// minutes ago is indistinguishable from a message that just landed -
|
|
305
|
+
// reported in a jcode session where five real messages were missed
|
|
306
|
+
// while the same old line was announced repeatedly as news.
|
|
307
|
+
// The clock is what makes a re-read visibly stale.
|
|
308
|
+
const at = new Date().toTimeString().slice(0, 8);
|
|
309
|
+
await notify(`PATCHCORD: ${count} waiting in inbox (snapshot at ${at})`, {
|
|
310
|
+
count,
|
|
311
|
+
kind: "pending",
|
|
312
|
+
at,
|
|
313
|
+
});
|
|
299
314
|
}
|
|
300
315
|
return;
|
|
301
316
|
} catch (e) {
|
|
@@ -489,6 +504,13 @@ async function run() {
|
|
|
489
504
|
`subscribe: stall-signal keepalive=${STALL_SIGNAL_OPTS.keepaliveMs}ms ` +
|
|
490
505
|
`quiet=${STALL_SIGNAL_OPTS.quietMs}ms stall=${STALL_SIGNAL_OPTS.stallMs}ms`
|
|
491
506
|
);
|
|
507
|
+
// Said out loud on every start, because the mistake is invisible once
|
|
508
|
+
// made: a filtered stream looks like a working listener that simply never
|
|
509
|
+
// wakes anyone, which is exactly what was reported from production.
|
|
510
|
+
logErr(
|
|
511
|
+
"subscribe: stall-signal writes HEARTBEAT: lines to stdout — do NOT pipe " +
|
|
512
|
+
"this through `grep '^PATCHCORD:'`, it deletes them and the wake stops working"
|
|
513
|
+
);
|
|
492
514
|
stallSignalTimer = setInterval(() => {
|
|
493
515
|
if (Date.now() >= stallSignalQuietUntil) {
|
|
494
516
|
process.stdout.write(`${STALL_SIGNAL_KEEPALIVE_PREFIX} ${new Date().toISOString()}\n`);
|
|
@@ -44,33 +44,59 @@ WAKES YOU: the pipe fills with messages you are never told about, and you
|
|
|
44
44
|
discover them only when the user asks why you did not answer. That has happened
|
|
45
45
|
in production, on jcode, and is the reason this section exists.
|
|
46
46
|
|
|
47
|
-
So, if you have no `Monitor
|
|
47
|
+
So, if you have no `Monitor`, **use `--stall-signal`. It was built for exactly
|
|
48
|
+
this and it is not optional here:**
|
|
48
49
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
sign that anything is wrong.
|
|
50
|
+
```
|
|
51
|
+
patchcord subscribe --stall-signal
|
|
52
|
+
```
|
|
53
53
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
54
|
+
run as a background command with your harness's stall wake set to **15
|
|
55
|
+
seconds** (jcode: `stall_wake_seconds: 15`).
|
|
56
|
+
|
|
57
|
+
1. **What `--stall-signal` does.** While idle it writes a `HEARTBEAT:` line
|
|
58
|
+
every 5 s, so the pipe never looks silent and the stall never fires on
|
|
59
|
+
nothing. When a real message arrives it writes the `PATCHCORD:` line and
|
|
60
|
+
then goes QUIET ON PURPOSE for 15 s, so your harness's stall detector fires
|
|
61
|
+
because a message came in. It turns wake-on-silence into wake-on-message
|
|
62
|
+
using only the primitive your harness has.
|
|
63
|
+
|
|
64
|
+
**Run it raw. Do not pipe it through `grep`.** Step 2's command filters
|
|
65
|
+
stdout down to `^PATCHCORD:` lines, and that filter removes the keepalives
|
|
66
|
+
this mode depends on. Your harness measures the stall on the OUTPUT of the
|
|
67
|
+
pipeline, so a filtered stream is silent all the time: the wake fires on
|
|
68
|
+
nothing every 15 s, and the deliberate quiet after a real message looks
|
|
69
|
+
exactly like idle. Filtering here does not merely add noise, it deletes the
|
|
70
|
+
signal. Ignore the `HEARTBEAT:` lines when you read the output; they exist
|
|
71
|
+
for the stall detector, not for you.
|
|
72
|
+
|
|
73
|
+
2. **The two numbers must agree.** Your stall wake MUST equal `--stall-signal`'s
|
|
74
|
+
quiet window. Both default to 15 s, so setting `15` and passing no inline
|
|
75
|
+
value is correct. If you pass a custom triple
|
|
76
|
+
(`--stall-signal keepaliveMs:quietMs:stallMs`), your stall wake must equal
|
|
77
|
+
the `stallMs` you passed. Too short and it fires on nothing; too long and it
|
|
78
|
+
never fires on a real message. **Do not use 30** — that number predates this
|
|
79
|
+
mode and is simply wrong for it, not safer.
|
|
59
80
|
|
|
60
81
|
3. **NEVER read the last `PATCHCORD:` line in the task output as news.** It is
|
|
61
|
-
scrollback
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
82
|
+
scrollback and it may be a line you already handled. Two lines look almost
|
|
83
|
+
alike: a live arrival, and a `... waiting in inbox (snapshot at HH:MM:SS)`
|
|
84
|
+
written once when the listener connected and drained the queue. The
|
|
85
|
+
timestamp on the second is there so you can see it is old — check it. In a
|
|
86
|
+
real jcode session an agent re-announced that same drain line repeatedly and
|
|
87
|
+
missed five actual messages while doing so. On every wake call
|
|
88
|
+
`mcp__patchcord__inbox` and believe only that. If it is empty, say nothing
|
|
89
|
+
and go back to waiting.
|
|
90
|
+
|
|
91
|
+
4. **If the inbox is empty, check the listener is still alive** before assuming
|
|
92
|
+
a harmless false wake. A dead listener produces the same silence. If a
|
|
93
|
+
restart is refused with `already running (pid N)`, the process outlived the
|
|
94
|
+
task that tracked it: restart with `--replace`, which is the only sanctioned
|
|
95
|
+
way to remove a running listener. Never `kill`/`pkill` by hand.
|
|
96
|
+
|
|
97
|
+
**Only if your harness has no stall setting at all** does this degrade to a
|
|
98
|
+
timed poll. Say so plainly to the user in that case — tell them you will check
|
|
99
|
+
regularly, never that you will be woken "as messages arrive".
|
|
74
100
|
|
|
75
101
|
# When a notification fires
|
|
76
102
|
|