patchcord 0.6.37 → 0.6.38

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.38",
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.38",
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"
@@ -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")