patchcord 0.6.47 → 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.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "patchcord",
3
3
  "description": "Cross-machine agent messaging. Messages from other agents land in the inbox and wake the agent to reply.",
4
- "version": "0.6.47",
4
+ "version": "0.6.48",
5
5
  "author": {
6
6
  "name": "ppravdin"
7
7
  },
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "patchcord",
3
- "version": "0.6.47",
3
+ "version": "0.6.48",
4
4
  "description": "Cross-machine agent messaging for Claude Code and Codex",
5
5
  "author": {
6
6
  "name": "ppravdin",
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json",
3
3
  "name": "patchcord",
4
- "version": "0.6.47",
4
+ "version": "0.6.48",
5
5
  "description": "Cross-machine agent messaging for Claude Code and Codex",
6
6
  "author": {
7
7
  "name": "ppravdin",
@@ -48,7 +48,7 @@ So, if you have no `Monitor`, **use `--stall-signal`. It was built for exactly
48
48
  this and it is not optional here:**
49
49
 
50
50
  ```
51
- patchcord subscribe --stall-signal | grep --line-buffered '^PATCHCORD:'; exit ${PIPESTATUS[0]}
51
+ patchcord subscribe --stall-signal
52
52
  ```
53
53
 
54
54
  run as a background command with your harness's stall wake set to **15
@@ -61,8 +61,14 @@ seconds** (jcode: `stall_wake_seconds: 15`).
61
61
  because a message came in. It turns wake-on-silence into wake-on-message
62
62
  using only the primitive your harness has.
63
63
 
64
- The `grep` drops the `HEARTBEAT:` lines from what you see; they still keep
65
- the pipe alive, which is their whole job.
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.
66
72
 
67
73
  2. **The two numbers must agree.** Your stall wake MUST equal `--stall-signal`'s
68
74
  quiet window. Both default to 15 s, so setting `15` and passing no inline
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "patchcord",
3
- "version": "0.6.47",
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 | grep --line-buffered '^PATCHCORD:'; exit ${PIPESTATUS[0]}",
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
- The grep keeps only `PATCHCORD:` lines in the visible output; `HEARTBEAT:`
50
- lines never match that pattern, so they never need special handling on
51
- your end. `${PIPESTATUS[0]}` preserves subscribe's own exit code through
52
- the pipe.
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 | grep --line-buffered '^PATCHCORD:'; exit ${PIPESTATUS[0]}",
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:` — the grep filter every
25
- // subscribe skill uses already drops it for free.
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
@@ -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
- // real notification: every subscribe skill's grep is `^PATCHCORD:`, and
58
- // "HEARTBEAT:" does not start with "PATCHCORD" at all, so nothing else needs
59
- // to change for a consumer that already filters on that pattern.
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.
@@ -502,6 +504,13 @@ async function run() {
502
504
  `subscribe: stall-signal keepalive=${STALL_SIGNAL_OPTS.keepaliveMs}ms ` +
503
505
  `quiet=${STALL_SIGNAL_OPTS.quietMs}ms stall=${STALL_SIGNAL_OPTS.stallMs}ms`
504
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
+ );
505
514
  stallSignalTimer = setInterval(() => {
506
515
  if (Date.now() >= stallSignalQuietUntil) {
507
516
  process.stdout.write(`${STALL_SIGNAL_KEEPALIVE_PREFIX} ${new Date().toISOString()}\n`);
@@ -48,7 +48,7 @@ So, if you have no `Monitor`, **use `--stall-signal`. It was built for exactly
48
48
  this and it is not optional here:**
49
49
 
50
50
  ```
51
- patchcord subscribe --stall-signal | grep --line-buffered '^PATCHCORD:'; exit ${PIPESTATUS[0]}
51
+ patchcord subscribe --stall-signal
52
52
  ```
53
53
 
54
54
  run as a background command with your harness's stall wake set to **15
@@ -61,8 +61,14 @@ seconds** (jcode: `stall_wake_seconds: 15`).
61
61
  because a message came in. It turns wake-on-silence into wake-on-message
62
62
  using only the primitive your harness has.
63
63
 
64
- The `grep` drops the `HEARTBEAT:` lines from what you see; they still keep
65
- the pipe alive, which is their whole job.
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.
66
72
 
67
73
  2. **The two numbers must agree.** Your stall wake MUST equal `--stall-signal`'s
68
74
  quiet window. Both default to 15 s, so setting `15` and passing no inline