patchcord 0.6.37 → 0.6.39

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.37",
4
+ "version": "0.6.39",
5
5
  "author": {
6
6
  "name": "ppravdin"
7
7
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "patchcord",
3
- "version": "0.6.37",
3
+ "version": "0.6.39",
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"
@@ -11,7 +11,7 @@ if [ -f "$HOOK_DIR/lib/inbox-guard.sh" ]; then
11
11
  else
12
12
  # Degrade to the pre-guard behaviour rather than dying: a partial install
13
13
  # must still nudge, just without the identity hint or the loop breaker.
14
- pc_inbox_reason() { printf '%s patchcord message(s) waiting. Call inbox() and reply to all immediately.' "$1"; }
14
+ pc_inbox_reason() { printf '%s patchcord message(s) waiting call inbox() and reply.' "$1"; }
15
15
  pc_streak_ok() { return 0; }
16
16
  pc_state_path() { printf ''; }
17
17
  pc_streak_n() { printf '1'; }
@@ -75,7 +75,22 @@ fi
75
75
  # Check inbox — one lightweight HTTP call
76
76
  MACHINE_NAME=$(hostname -s 2>/dev/null || echo "unknown")
77
77
  INSTALL_PATH=$(dirname "$MCP_JSON")
78
- HTTP_CODE=$(curl -s -o /tmp/patchcord_inbox.json -w "%{http_code}" --max-time 5 \
78
+
79
+ # PER-INVOCATION response file. This was a FIXED path (/tmp/patchcord_inbox.json)
80
+ # shared by every agent on the machine, and a host running several seats stops
81
+ # them at overlapping times: one hook's curl truncates and rewrites the file
82
+ # another hook is reading, and either one's `rm -f` deletes it underneath the
83
+ # other. The reader then hands a half-written body to jq, which is exactly the
84
+ # "Invalid numeric literal at line 1, column N" this produced on the Mac mini.
85
+ # A shared mutable temp path between concurrent agents is not a race that
86
+ # happens rarely; it is one that happens whenever the machine is busy.
87
+ RESP_FILE=$(mktemp "${TMPDIR:-/tmp}/patchcord_inbox.XXXXXX" 2>/dev/null || echo "")
88
+ [ -z "$RESP_FILE" ] && exit 0
89
+ # Remove it on ANY exit path, including the early returns below, so a crash
90
+ # cannot leave per-invocation files accumulating in /tmp.
91
+ trap 'rm -f "$RESP_FILE"' EXIT
92
+
93
+ HTTP_CODE=$(curl -s -o "$RESP_FILE" -w "%{http_code}" --max-time 5 \
79
94
  -H "Authorization: Bearer ${TOKEN}" \
80
95
  -H "x-patchcord-machine: ${MACHINE_NAME}" \
81
96
  -H "x-patchcord-install-path: ${INSTALL_PATH}" \
@@ -86,18 +101,25 @@ if [ "$HTTP_CODE" = "401" ] || [ "$HTTP_CODE" = "403" ]; then
86
101
  "decision": "block",
87
102
  "reason": "PATCHCORD AUTH FAILED: token rejected by server (HTTP '"$HTTP_CODE"'). Check your token in .mcp.json — it may be wrong, expired, or not yet registered on the server."
88
103
  }'
89
- rm -f /tmp/patchcord_inbox.json
90
104
  exit 0
91
105
  fi
92
106
 
93
107
  if [ "$HTTP_CODE" = "000" ]; then
94
108
  # Server unreachable — skip silently
95
- rm -f /tmp/patchcord_inbox.json
96
109
  exit 0
97
110
  fi
98
111
 
99
- RESPONSE=$(cat /tmp/patchcord_inbox.json 2>/dev/null || echo '{"count":0}')
100
- rm -f /tmp/patchcord_inbox.json
112
+ RESPONSE=$(cat "$RESP_FILE" 2>/dev/null || echo "")
113
+
114
+ # "COULD NOT TELL" IS NOT "NOTHING PENDING". Below, an unparseable body used to
115
+ # fall through jq's `// 0` default to COUNT=0, which takes the same exit path as
116
+ # a genuinely empty inbox AND clears the streak state. So a corrupted response
117
+ # told the agent it had no messages, silently, with no way to tell the two apart
118
+ # — the same absence-reads-as-a-value shape that cost this project a night.
119
+ # Refuse to answer instead: leave the streak state alone and say nothing.
120
+ if ! printf '%s' "$RESPONSE" | jq -e 'type == "object"' >/dev/null 2>&1; then
121
+ exit 0
122
+ fi
101
123
 
102
124
  # ── Auto-apply custom skill from web console ──────────────────
103
125
  # Writes to .claude/skills/patchcord-custom/SKILL.md — Claude Code
@@ -142,7 +164,14 @@ if [ "$COUNT" -eq 0 ]; then
142
164
  exit 0
143
165
  fi
144
166
 
145
- NOTIFY_LOCK="/tmp/patchcord_notify_lock"
167
+ # PER-IDENTITY lock. This was a single global path, so on a host running several
168
+ # seats one agent's notification suppressed EVERY other agent's for 5s — an
169
+ # agent with real pending messages could be silenced by an unrelated seat that
170
+ # happened to stop first. The dedupe is meant to stop ONE identity being told
171
+ # twice by the Stop and Notification hooks, never to stop a second identity
172
+ # being told at all.
173
+ NOTIFY_LOCK=$(pc_state_path "patchcord_notify_lock" "$NAMESPACE" "$AGENT_ID" 2>/dev/null || echo "")
174
+ [ -z "$NOTIFY_LOCK" ] && NOTIFY_LOCK="${TMPDIR:-/tmp}/patchcord_notify_lock_$(printf '%s' "${NAMESPACE}/${AGENT_ID}" | tr -c 'a-zA-Z0-9' '_')"
146
175
  LOCK_AGE=5
147
176
  if [ -f "$NOTIFY_LOCK" ]; then
148
177
  LOCK_MTIME=$(stat -c %Y "$NOTIFY_LOCK" 2>/dev/null || stat -f %m "$NOTIFY_LOCK" 2>/dev/null || echo "0")
@@ -17,7 +17,7 @@ elif [ -f "$HOOK_DIR/patchcord-inbox-guard.sh" ]; then
17
17
  # shellcheck source=lib/inbox-guard.sh
18
18
  . "$HOOK_DIR/patchcord-inbox-guard.sh"
19
19
  else
20
- pc_inbox_reason() { printf '%s patchcord message(s) waiting. Call inbox() and reply to all.' "$1"; }
20
+ pc_inbox_reason() { printf '%s patchcord message(s) waiting call inbox() and reply.' "$1"; }
21
21
  pc_streak_ok() { return 0; }
22
22
  pc_state_path() { printf ''; }
23
23
  pc_streak_n() { printf '1'; }
@@ -147,10 +147,15 @@ echo "$NOW $(( STREAK + 1 ))" > "$STATE_FILE"
147
147
  NAMESPACE=$(echo "$RESPONSE" | jq -r '.namespace_id // empty' 2>/dev/null || true)
148
148
  AGENT_ID=$(echo "$RESPONSE" | jq -r '.agent_id // empty' 2>/dev/null || true)
149
149
 
150
+ # The identity belongs in the FIRST sentence, not appended after it. Tacked on
151
+ # the end it reads as an afterthought and the reader has already moved on.
152
+ WHO=""
153
+ [ -n "$NAMESPACE" ] && [ -n "$AGENT_ID" ] && WHO=" for ${AGENT_ID}@${NAMESPACE}"
154
+
150
155
  if [ "$COUNT" -eq 1 ]; then
151
- MSG="Patchcord: 1 pending message. Call the patchcord inbox tool now, do the work it asks for, then reply with what you did."
156
+ MSG="Patchcord: 1 pending message${WHO} call the patchcord inbox tool, do what it asks, then reply with what you did."
152
157
  else
153
- MSG="Patchcord: ${COUNT} pending messages. Call the patchcord inbox tool now, do the work each one asks for, then reply to each with what you did."
158
+ MSG="Patchcord: ${COUNT} pending messages${WHO} call the patchcord inbox tool, do what each asks, then reply to each with what you did."
154
159
  fi
155
160
 
156
161
  # Name the identity the count belongs to. This hook reads the token from
@@ -160,7 +165,8 @@ fi
160
165
  # the old one — a contradiction neither side can see alone. See
161
166
  # lib/inbox-guard.sh.
162
167
  if [ -n "$NAMESPACE" ] && [ -n "$AGENT_ID" ]; then
163
- MSG="${MSG} These are addressed to ${AGENT_ID}@${NAMESPACE}. If the inbox tool reports a different identity, or reports 0 pending, this session is authenticated as another agent — the token on disk was replaced after it started. Do not keep retrying: say so and ask the user to restart the session."
168
+ MSG="${MSG}
169
+ If the inbox tool shows a different agent or 0 pending: the token on disk changed after this session started, so this session is signed in as someone else. Do not retry — tell the user and ask them to restart the session."
164
170
  fi
165
171
 
166
172
  jq -n --arg msg "$MSG" '{followup_message: $msg}'
@@ -25,6 +25,11 @@
25
25
  # The nudge text. Names the identity so a session authenticated as a different
26
26
  # agent can recognise the mismatch.
27
27
  #
28
+ # BOTH BRANCHES ARE READ BY A HUMAN. They are printed in the transcript, so
29
+ # every word is read by a person as well as by an agent: lead with the action,
30
+ # keep the diagnosis to one sentence, and never emit "message(s)" — singular and
31
+ # plural are already computed one line below.
32
+ #
28
33
  # THE STALE-IDENTITY EXPLANATION IS ONLY EMITTED ONCE THE SYMPTOM HAS ACTUALLY
29
34
  # OCCURRED. It used to be appended to EVERY nudge, fleet-wide, which was wrong
30
35
  # for three reasons:
@@ -53,28 +58,31 @@
53
58
  pc_inbox_reason() {
54
59
  local count="$1" ns="$2" agent="$3" call="${4:-inbox()}" hint="${5:-restart this session}"
55
60
  local streak="${6:-1}"
56
- local noun="message(s)"
61
+ local noun="messages"
57
62
  [ "$count" = "1" ] && noun="message"
58
63
  case "$streak" in ''|*[!0-9]*) streak=1 ;; esac
59
64
 
60
65
  if [ -z "$ns" ] || [ -z "$agent" ]; then
61
- printf '%s patchcord %s waiting. Call %s and reply to all immediately.' \
66
+ printf '%s patchcord %s waiting call %s and reply.' \
62
67
  "$count" "$noun" "$call"
63
68
  return
64
69
  fi
65
70
 
66
71
  if [ "$streak" -lt 2 ]; then
67
- printf '%s patchcord %s waiting for %s@%s. Call %s and reply to all immediately.' \
72
+ printf '%s patchcord %s for %s@%s call %s and reply.' \
68
73
  "$count" "$noun" "$agent" "$ns" "$call"
69
74
  return
70
75
  fi
71
76
 
72
- printf 'STILL %s patchcord %s waiting for %s@%s after a previous nudge — %s did not clear it. Call %s once more. If it reports a different identity than %s@%s, or reports 0 pending, then this session is authenticated as another agent: the token on disk was replaced after the session started. In that case do NOT keep calling %s — say so and ask the user to %s so it reloads the config.' \
77
+ # Second consecutive identical block: the nudge did NOT clear, which is the
78
+ # actual evidence of the stale-token condition. Even here the action comes
79
+ # first and the diagnosis is one sentence on its own line — a repeat is a
80
+ # worse moment to make someone parse a paragraph, not a better one.
81
+ printf 'Still %s patchcord %s for %s@%s — %s did not clear it. Call %s once more.
82
+ If it shows a different agent or 0 pending: the token on disk changed after this session started, so this session is signed in as someone else. Do not retry — tell the user and ask them to %s.' \
73
83
  "$count" "$noun" "$agent" "$ns" \
74
- "$call" \
75
- "$call" \
76
- "$agent" "$ns" \
77
- "$call" "$hint"
84
+ "$call" "$call" \
85
+ "$hint"
78
86
  }
79
87
 
80
88
  # pc_streak_n <state_file>