patchcord 0.5.58 → 0.5.60

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "patchcord",
3
- "version": "0.5.58",
3
+ "version": "0.5.60",
4
4
  "description": "Cross-machine agent messaging for Claude Code and Codex",
5
5
  "author": "ppravdin",
6
6
  "license": "MIT",
@@ -95,33 +95,30 @@ To message a user outside your namespace, use `@username` as the to_agent. Examp
95
95
 
96
96
  ## File sharing
97
97
 
98
- Three modes:
99
-
100
- **Relay from URL (preferred for public files):**
98
+ **Files on disk → `patchcord upload` (CLI, preferred):**
101
99
  ```
102
- attachment(relay=true, path_or_url="https://example.com/file.md", filename="file.md")
100
+ patchcord upload /path/to/report.md --mime text/markdown
103
101
  ```
104
- Server fetches the URL and stores it. ~50 tokens instead of thousands for the file content.
102
+ Prints the storage path. Pass it to `send_message`. No curl, no base64 in chat. 25MB cap.
105
103
 
106
- **Presigned upload (preferred for local files):**
104
+ **Public URLs → `attachment(relay=true, ...)`:**
107
105
  ```
108
- attachment(upload=true, filename="report.md") -> returns {url, path}
109
- curl -X PUT -H "Content-Type: text/markdown" --data-binary @/path/to/report.md "<url>"
106
+ attachment(relay=true, path_or_url="https://example.com/file.md", filename="file.md")
110
107
  ```
111
- Then send the `path` to the other agent. No base64, no token waste.
108
+ Server fetches and stores. Use when the file already lives at a public URL.
112
109
 
113
- **Inline base64 (last resort — small generated content only):**
110
+ **Inline base64 last resort:**
114
111
  ```
115
112
  attachment(upload=true, filename="notes.txt", file_data="<base64>")
116
113
  ```
117
- Base64 adds ~33% overhead and wastes context tokens. Never use this for files on disk — use presigned upload above instead.
114
+ Only if you cannot run shell commands. Wastes context tokens.
118
115
 
119
116
  **Downloading:**
120
117
  ```
121
118
  attachment(path_or_url="namespace/agent/timestamp_file.md")
122
119
  ```
123
120
 
124
- Send the returned `path` to the other agent in your message so they can download it.
121
+ Always send the storage path (not file content) to the other agent.
125
122
 
126
123
  ## Rules
127
124
 
@@ -22,6 +22,11 @@ const RECONNECT_BACKOFF_MS = [1000, 2000, 4000, 8000, 15_000, 30_000];
22
22
  // gap means three missed heartbeats. Force a reconnect via the outer loop.
23
23
  const FRESHNESS_CHECK_INTERVAL_MS = 30_000;
24
24
  const FRESHNESS_STALE_MS = 90_000;
25
+ // Re-emit pending count every minute as a safety net. The Realtime push
26
+ // + Monitor pipeline can drop notifications when an agent is mid-tool-call
27
+ // (especially for long-running work like vector-DB searches). Re-emitting
28
+ // gives Monitor multiple chances to surface the line on a later idle tick.
29
+ const PENDING_HEARTBEAT_MS = 60_000;
25
30
 
26
31
  // Short HH:MM:SS prefix so the Monitor output can be scanned at a glance.
27
32
  // Local time — Monitor's reader is always a human looking at one machine.
@@ -243,6 +248,7 @@ function runOnce(ticket, baseUrl, token, refreshTicket) {
243
248
  let heartbeatTimer = null;
244
249
  let refreshTimer = null;
245
250
  let freshnessTimer = null;
251
+ let pendingHeartbeatTimer = null;
246
252
  let lastEventAt = Date.now();
247
253
  let currentJwt = ticket.jwt;
248
254
  let settled = false;
@@ -253,6 +259,7 @@ function runOnce(ticket, baseUrl, token, refreshTicket) {
253
259
  if (heartbeatTimer) clearInterval(heartbeatTimer);
254
260
  if (refreshTimer) clearTimeout(refreshTimer);
255
261
  if (freshnessTimer) clearInterval(freshnessTimer);
262
+ if (pendingHeartbeatTimer) clearInterval(pendingHeartbeatTimer);
256
263
  try {
257
264
  ws.close();
258
265
  } catch (_) {}
@@ -311,6 +318,20 @@ function runOnce(ticket, baseUrl, token, refreshTicket) {
311
318
  }
312
319
  }, FRESHNESS_CHECK_INTERVAL_MS);
313
320
 
321
+ // Pending-inbox heartbeat. The Realtime INSERT push that triggers the
322
+ // initial "PATCHCORD: 1 new from <sender>" notification can be lost
323
+ // by the Monitor → agent-context surface layer if the agent happens
324
+ // to be mid-tool-call when the line is emitted (heavy agents with
325
+ // long tool calls — vector-DB search, CrossRef lookups — are most
326
+ // exposed). Re-emit the pending count once a minute as long as the
327
+ // inbox is non-empty so a later idle tick has another chance to
328
+ // wake the agent. drainQueueOnce stays silent if pending_count == 0.
329
+ pendingHeartbeatTimer = setInterval(() => {
330
+ drainQueueOnce(baseUrl, token).catch((e) => {
331
+ logErr(`subscribe: pending heartbeat failed: ${e.message}`);
332
+ });
333
+ }, PENDING_HEARTBEAT_MS);
334
+
314
335
  const scheduleRefresh = (ttlSec) => {
315
336
  const refreshIn = Math.max((ttlSec - JWT_REFRESH_SAFETY_MARGIN_SEC) * 1000, 30_000);
316
337
  refreshTimer = setTimeout(doRefresh, refreshIn);
@@ -83,34 +83,31 @@ To message a user outside your namespace, use `@username` as the to_agent. Examp
83
83
 
84
84
  ## File sharing
85
85
 
86
- Three modes, choose based on context:
87
-
88
- **Relay from URL (preferred for public files):**
86
+ **Files on disk `patchcord upload` (CLI, preferred):**
89
87
  ```
90
- attachment(relay=true, path_or_url="https://example.com/file.md", filename="file.md")
88
+ patchcord upload /path/to/report.md --mime text/markdown
91
89
  ```
92
- Server fetches the URL and stores it. You send only a URL string (~50 tokens) instead of the file content (thousands of tokens). Always prefer relay when the file is at a public URL.
90
+ Prints the storage path. Pass that path to `send_message`. No curl, no base64 in chat, no presigned URLs. 25MB cap.
93
91
 
94
- **Presigned upload (preferred for local files):**
92
+ **Public URLs → `attachment(relay=true, ...)`:**
95
93
  ```
96
- attachment(upload=true, filename="report.md") -> returns {url, path}
97
- curl -X PUT -H "Content-Type: text/markdown" --data-binary @/path/to/report.md "<url>"
94
+ attachment(relay=true, path_or_url="https://example.com/file.md", filename="file.md")
98
95
  ```
99
- Then send the `path` to the other agent. No base64, no token waste.
96
+ Server fetches the URL and stores it. Use when the file already lives at a public URL.
100
97
 
101
- **Inline base64 (last resort small generated content only):**
98
+ **Web agents (no shell) inline base64 last resort:**
102
99
  ```
103
100
  attachment(upload=true, filename="notes.txt", file_data="<base64>")
104
101
  ```
105
- Base64 adds ~33% overhead and wastes context tokens. Never use this for files on disk — use presigned upload above instead.
102
+ Only for agents that cannot run shell commands. Wastes context tokens. Never use if you can run `patchcord upload`.
106
103
 
107
104
  **Downloading:**
108
105
  ```
109
106
  attachment(path_or_url="namespace/agent/timestamp_file.md")
110
107
  ```
111
- Use the path from the sender's message.
108
+ Pass the storage path from the sender's message.
112
109
 
113
- Send the returned `path` to the other agent in your message so they can download it.
110
+ Always send the storage path (not the file content) to the other agent.
114
111
 
115
112
  ## Identity (`patchcord whoami` / `patchcord agents`)
116
113