handmux 0.29.0 → 0.29.2
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/dist/connectors/claude/index.js +45 -3
- package/dist/hooks/handmux-notify.sh +12 -2
- package/dist/hooks/handmux-write.cjs +4 -1
- package/dist/package.json +13 -13
- package/dist/public/assets/{index-D0tDAqO-.js → index-2ghkmGbT.js} +95 -95
- package/dist/public/index.html +1 -1
- package/dist/src/agent-runtime/bridge.js +63 -1
- package/dist/src/agent-runtime/bridgeTypes.js +2 -0
- package/dist/src/agent-runtime/runtime.js +1 -0
- package/dist/src/agent-runtime/tmuxRuntime.js +27 -3
- package/dist/src/agents/claudeNativeTail.js +19 -3
- package/dist/src/agents/processIdentity.js +10 -1
- package/dist/src/deviceAuth/http.js +9 -3
- package/dist/src/push.js +5 -1
- package/package.json +4 -4
|
@@ -4,6 +4,8 @@ import path from 'node:path';
|
|
|
4
4
|
import { classifyClaude } from '../../src/agents/claude.js';
|
|
5
5
|
import { ClaudeNativeTailReader } from '../../src/agents/claudeNativeTail.js';
|
|
6
6
|
import { LocalConnectorBridgeClient } from '../bridgeClient.js';
|
|
7
|
+
const DEFAULT_SPOOL_MAX_FILES = 2_000;
|
|
8
|
+
const DEFAULT_SPOOL_RETENTION_MS = 24 * 60 * 60_000;
|
|
7
9
|
const EVENT_ID_RE = /^[a-zA-Z0-9][a-zA-Z0-9._:-]{0,255}$/;
|
|
8
10
|
function isRecord(value) {
|
|
9
11
|
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
@@ -187,6 +189,8 @@ export class ClaudeHookBridgeConnector {
|
|
|
187
189
|
#panes;
|
|
188
190
|
#process;
|
|
189
191
|
#pollMs;
|
|
192
|
+
#spoolMaxFiles;
|
|
193
|
+
#spoolRetentionMs;
|
|
190
194
|
#retryDelayMs;
|
|
191
195
|
#maxRetryDelayMs;
|
|
192
196
|
#logger;
|
|
@@ -198,11 +202,13 @@ export class ClaudeHookBridgeConnector {
|
|
|
198
202
|
#tail = Promise.resolve();
|
|
199
203
|
#started = false;
|
|
200
204
|
#closed = false;
|
|
201
|
-
constructor({ nativeTail = new ClaudeNativeTailReader(), socketPath, credentialFile, stateDirectory, hookStateFile, eventDirectory, panes, process, pollMs = 250, retryDelayMs = 100, maxRetryDelayMs = 5_000, logger = () => { }, createClient = (options) => new LocalConnectorBridgeClient(options), }) {
|
|
205
|
+
constructor({ nativeTail = new ClaudeNativeTailReader(), socketPath, credentialFile, stateDirectory, hookStateFile, eventDirectory, panes, process, pollMs = 250, retryDelayMs = 100, maxRetryDelayMs = 5_000, logger = () => { }, createClient = (options) => new LocalConnectorBridgeClient(options), spoolMaxFiles = DEFAULT_SPOOL_MAX_FILES, spoolRetentionMs = DEFAULT_SPOOL_RETENTION_MS, }) {
|
|
202
206
|
if (![socketPath, credentialFile, stateDirectory, hookStateFile, eventDirectory].every(path.isAbsolute)
|
|
203
207
|
|| !panes || !process || !Number.isSafeInteger(pollMs) || pollMs < 50
|
|
204
208
|
|| !Number.isSafeInteger(retryDelayMs) || retryDelayMs <= 0
|
|
205
|
-
|| !Number.isSafeInteger(maxRetryDelayMs) || maxRetryDelayMs < retryDelayMs
|
|
209
|
+
|| !Number.isSafeInteger(maxRetryDelayMs) || maxRetryDelayMs < retryDelayMs
|
|
210
|
+
|| !Number.isSafeInteger(spoolMaxFiles) || spoolMaxFiles <= 0
|
|
211
|
+
|| !Number.isSafeInteger(spoolRetentionMs) || spoolRetentionMs <= 0) {
|
|
206
212
|
throw new TypeError('Claude Hook Bridge Connector requires private paths and Runtime identity sources');
|
|
207
213
|
}
|
|
208
214
|
this.#socketPath = socketPath;
|
|
@@ -216,6 +222,8 @@ export class ClaudeHookBridgeConnector {
|
|
|
216
222
|
this.#pollMs = pollMs;
|
|
217
223
|
this.#retryDelayMs = retryDelayMs;
|
|
218
224
|
this.#maxRetryDelayMs = maxRetryDelayMs;
|
|
225
|
+
this.#spoolMaxFiles = spoolMaxFiles;
|
|
226
|
+
this.#spoolRetentionMs = spoolRetentionMs;
|
|
219
227
|
this.#logger = logger;
|
|
220
228
|
this.#createClient = createClient;
|
|
221
229
|
}
|
|
@@ -276,6 +284,40 @@ export class ClaudeHookBridgeConnector {
|
|
|
276
284
|
return [];
|
|
277
285
|
}
|
|
278
286
|
}
|
|
287
|
+
// Retention for Hook source files that never reach an acknowledgement: a pane that is gone, a client
|
|
288
|
+
// that never drains, or a durable publish that keeps failing leaves its file behind, and every poll
|
|
289
|
+
// re-reads it — an observed 170k files over nine days. Files are listed oldest-first, so a cap drops
|
|
290
|
+
// the stuck ones and an age window only ever expires files no one is waiting on. `gap-*` markers are
|
|
291
|
+
// not touched: they are bounded by pane/session and carry a degradation signal until confirmed.
|
|
292
|
+
#pruneSpool(files) {
|
|
293
|
+
const events = files.filter((file) => path.basename(file).startsWith('event-'));
|
|
294
|
+
if (events.length === 0)
|
|
295
|
+
return files;
|
|
296
|
+
const now = Date.now();
|
|
297
|
+
const dropped = new Set();
|
|
298
|
+
for (const [index, file] of events.entries()) {
|
|
299
|
+
const beyondCap = this.#spoolMaxFiles > 0 && index < events.length - this.#spoolMaxFiles;
|
|
300
|
+
let expired = false;
|
|
301
|
+
try {
|
|
302
|
+
expired = now - fs.statSync(file).mtimeMs > this.#spoolRetentionMs;
|
|
303
|
+
}
|
|
304
|
+
catch { /* removed */ }
|
|
305
|
+
if (beyondCap || expired)
|
|
306
|
+
dropped.add(file);
|
|
307
|
+
}
|
|
308
|
+
if (dropped.size === 0)
|
|
309
|
+
return files;
|
|
310
|
+
for (const file of dropped) {
|
|
311
|
+
try {
|
|
312
|
+
fs.unlinkSync(file);
|
|
313
|
+
}
|
|
314
|
+
catch { /* concurrently removed */ }
|
|
315
|
+
}
|
|
316
|
+
this.#logger?.('Dropped Claude Hook source files that could not be delivered', {
|
|
317
|
+
dropped: dropped.size, remaining: events.length - dropped.size,
|
|
318
|
+
});
|
|
319
|
+
return files.filter((file) => !dropped.has(file));
|
|
320
|
+
}
|
|
279
321
|
async #reconcile() {
|
|
280
322
|
if (this.#closed)
|
|
281
323
|
return;
|
|
@@ -297,7 +339,7 @@ export class ClaudeHookBridgeConnector {
|
|
|
297
339
|
return pending;
|
|
298
340
|
};
|
|
299
341
|
const events = [];
|
|
300
|
-
for (const eventFile of this.#eventFiles()) {
|
|
342
|
+
for (const eventFile of this.#pruneSpool(this.#eventFiles())) {
|
|
301
343
|
let event = null;
|
|
302
344
|
try {
|
|
303
345
|
const stat = fs.lstatSync(eventFile);
|
|
@@ -53,8 +53,18 @@ if [ -z "$CLAUDE_PID" ]; then
|
|
|
53
53
|
done
|
|
54
54
|
fi
|
|
55
55
|
if [ -n "$CLAUDE_PID" ]; then
|
|
56
|
-
#
|
|
57
|
-
|
|
56
|
+
# Start time in the same form the server reads (processStartedAt in tmuxRuntime.ts): the raw procfs start
|
|
57
|
+
# tick where procfs exists, otherwise the `lstart` string. A wall-clock `lstart` moves whenever the host
|
|
58
|
+
# steps the guest clock (WSL2), which makes one live process look like several. The proc root is
|
|
59
|
+
# overridable so tests can supply a fixture instead of the host's live process table.
|
|
60
|
+
PROC_ROOT="${HANDMUX_PROC_ROOT:-/proc}"
|
|
61
|
+
# comm may contain spaces/parentheses: drop "pid (comm) " first, then starttime is field 20.
|
|
62
|
+
CLAUDE_STARTED_AT=$(sed 's/.*) //' "$PROC_ROOT/$CLAUDE_PID/stat" 2>/dev/null \
|
|
63
|
+
| awk '{ print $20 }' 2>/dev/null || printf '')
|
|
64
|
+
if [ -z "$CLAUDE_STARTED_AT" ]; then
|
|
65
|
+
# `lstart` follows the process locale on macOS; force C so standalone Node can parse it consistently.
|
|
66
|
+
CLAUDE_STARTED_AT=$(LC_ALL=C ps -p "$CLAUDE_PID" -o lstart= 2>/dev/null || printf '')
|
|
67
|
+
fi
|
|
58
68
|
CLAUDE_TTY=$(ps -p "$CLAUDE_PID" -o tty= 2>/dev/null || printf '')
|
|
59
69
|
fi
|
|
60
70
|
# payload 经 stdin 原样流给 node(不在 shell 里转义,避免坏数据);pane 含 '%' 直接进 JSON 字段,
|
|
@@ -27,7 +27,10 @@ if (!file || !pane || !src) process.exit(0);
|
|
|
27
27
|
|
|
28
28
|
function processFingerprint() {
|
|
29
29
|
const pid = Number(String(claudePid).trim());
|
|
30
|
-
|
|
30
|
+
// The Hook reports the platform's start value (see handmux-notify.sh): a raw procfs tick on Linux, an
|
|
31
|
+
// lstart string on macOS. A digit-only value stays numeric — Date.parse would make it NaN.
|
|
32
|
+
const rawStartedAt = String(claudeStartedAt).trim();
|
|
33
|
+
const startedAt = /^\d+$/.test(rawStartedAt) ? Number(rawStartedAt) : Date.parse(rawStartedAt);
|
|
31
34
|
const rawTty = String(claudeTty).trim();
|
|
32
35
|
if (!Number.isSafeInteger(pid) || pid <= 0 || !Number.isFinite(startedAt)
|
|
33
36
|
|| !rawTty || rawTty === '?' || rawTty === '??') return undefined;
|
package/dist/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "handmux",
|
|
3
|
-
"version": "0.29.
|
|
3
|
+
"version": "0.29.2",
|
|
4
4
|
"description": "A mobile vibe-coding cockpit — built on tmux: drive your live session, Claude Code / Codex — anything a terminal can run — from your phone.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "AGPL-3.0-only",
|
|
@@ -75,6 +75,18 @@
|
|
|
75
75
|
"vitest": "^2.0.0"
|
|
76
76
|
},
|
|
77
77
|
"whatsNew": [
|
|
78
|
+
{
|
|
79
|
+
"version": "0.29.2",
|
|
80
|
+
"date": "2026-09-18",
|
|
81
|
+
"zh": "修复 Linux 下 Claude 事件上报的兼容问题",
|
|
82
|
+
"en": "Compatibility fix for Claude event reporting on Linux"
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
"version": "0.29.1",
|
|
86
|
+
"date": "2026-09-18",
|
|
87
|
+
"zh": "修复 Claude 对话入口丢失 · 长时间运行不再变卡",
|
|
88
|
+
"en": "Fix lost Claude chat entries · no slowdown after long runs"
|
|
89
|
+
},
|
|
78
90
|
{
|
|
79
91
|
"version": "0.29.0",
|
|
80
92
|
"date": "2026-09-16",
|
|
@@ -110,18 +122,6 @@
|
|
|
110
122
|
"date": "2026-09-04",
|
|
111
123
|
"zh": "对话精细复制 · Pi 上下文状态",
|
|
112
124
|
"en": "Precise chat copy · Pi context status"
|
|
113
|
-
},
|
|
114
|
-
{
|
|
115
|
-
"version": "0.25.2",
|
|
116
|
-
"date": "2026-09-02",
|
|
117
|
-
"zh": "修复 Agent 对话断流、排队与工具路径",
|
|
118
|
-
"en": "Fixed Agent chat streams, queues, and tool paths"
|
|
119
|
-
},
|
|
120
|
-
{
|
|
121
|
-
"version": "0.25.1",
|
|
122
|
-
"date": "2026-09-02",
|
|
123
|
-
"zh": "修复 Codex 幽灵任务与对话空白",
|
|
124
|
-
"en": "Fixed Codex ghost tasks and blank chats"
|
|
125
125
|
}
|
|
126
126
|
]
|
|
127
127
|
}
|