handmux 0.30.0 → 0.31.0
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 +33 -562
- package/dist/connectors/codebuddy/index.js +49 -0
- package/dist/connectors/hookBridge.js +560 -0
- package/dist/hooks/handmux-codebuddy-notify.sh +116 -0
- package/dist/hooks/handmux-notify.sh +2 -0
- package/dist/hooks/handmux-write.cjs +72 -5
- package/dist/package.json +7 -7
- package/dist/public/assets/{abnfDiagram-O67JEVCF-CmVVGJqt.js → abnfDiagram-O67JEVCF-BsRDIQEB.js} +1 -1
- package/dist/public/assets/{arc-aYtCI03A.js → arc-DLtLfMnX.js} +1 -1
- package/dist/public/assets/{architectureDiagram-NJMV4G6O-CyZZDTaL.js → architectureDiagram-NJMV4G6O-DoIXhWqZ.js} +1 -1
- package/dist/public/assets/{blockDiagram-BEXU5L5S-D_ODtFS0.js → blockDiagram-BEXU5L5S-W0HiOcVA.js} +1 -1
- package/dist/public/assets/{c4Diagram-YGBWAQC7-C8qZFFmC.js → c4Diagram-YGBWAQC7-72uAUMvX.js} +1 -1
- package/dist/public/assets/channel-CzALSnb0.js +1 -0
- package/dist/public/assets/{chunk-2Q5K7J3B-Bo85fSxI.js → chunk-2Q5K7J3B-B9wSRFQ0.js} +1 -1
- package/dist/public/assets/{chunk-3FUC2YCW-BnET3fKr.js → chunk-3FUC2YCW-ryphp4s0.js} +2 -2
- package/dist/public/assets/{chunk-5VM5RSS4-DDw_KNv1.js → chunk-5VM5RSS4-Cm4KeKby.js} +1 -1
- package/dist/public/assets/{chunk-GTNCS2PH-B_K2164R.js → chunk-GTNCS2PH-Du4_i7r3.js} +1 -1
- package/dist/public/assets/{chunk-GWA4HPMP-1GA2Su8A.js → chunk-GWA4HPMP-CL1biVIA.js} +1 -1
- package/dist/public/assets/{chunk-JWPE2WC7-Cfc-8kSE.js → chunk-JWPE2WC7-B8eAVfuB.js} +1 -1
- package/dist/public/assets/{chunk-NETBCI7D-C7zyDFCq.js → chunk-NETBCI7D-D273GzYg.js} +1 -1
- package/dist/public/assets/{chunk-WEXAMYUT-BI2Zijn_.js → chunk-WEXAMYUT-DipTfRbc.js} +1 -1
- package/dist/public/assets/{chunk-XXDRQBXY-Bl50fDL9.js → chunk-XXDRQBXY-D4SqXrQ8.js} +1 -1
- package/dist/public/assets/{classDiagram-v2-NBCMYWYE-BMWMChZ4.js → classDiagram-v2-NBCMYWYE-PrpnIXcA.js} +1 -1
- package/dist/public/assets/{cose-bilkent-JH36ORCC-B_HUrBN4.js → cose-bilkent-JH36ORCC-BfcYMYTa.js} +1 -1
- package/dist/public/assets/{cynefin-OW5HDTMX-2jTRe-JA.js → cynefin-OW5HDTMX-Du659bmz.js} +1 -1
- package/dist/public/assets/{cynefinDiagram-VND7K2PF-ClDtZNOK.js → cynefinDiagram-VND7K2PF-DR_2Copy.js} +1 -1
- package/dist/public/assets/{dagre-6A5THRUB-CJS3bbcm.js → dagre-6A5THRUB-COsChaV5.js} +1 -1
- package/dist/public/assets/{diagram-22UHCM2B-BpRgaKhU.js → diagram-22UHCM2B-Cx2T1HvL.js} +1 -1
- package/dist/public/assets/{diagram-3UASUU5V-BNwvXb9K.js → diagram-3UASUU5V-Cp04LOka.js} +1 -1
- package/dist/public/assets/{diagram-ATOU4E4O-DyphKhOl.js → diagram-ATOU4E4O-DlqFSshY.js} +1 -1
- package/dist/public/assets/{diagram-CDSNMT55-BRazAxS2.js → diagram-CDSNMT55-goq40ctA.js} +1 -1
- package/dist/public/assets/{diagram-MLGK6HIB-NN_P6wZp.js → diagram-MLGK6HIB-Bdzjsy4r.js} +1 -1
- package/dist/public/assets/{diagram-MPIPVDR6-DHBe5DA4.js → diagram-MPIPVDR6-DtU7cM6P.js} +1 -1
- package/dist/public/assets/{ebnfDiagram-ZINNZB2B-BCTHXp7J.js → ebnfDiagram-ZINNZB2B-XyVCfmED.js} +1 -1
- package/dist/public/assets/{elk-276RUBZZ-yXNwrgXQ.js → elk-276RUBZZ-aHVIw-2x.js} +1 -1
- package/dist/public/assets/{erDiagram-OPXOYQCR-YsAJcCqg.js → erDiagram-OPXOYQCR-Bq_PhTIL.js} +1 -1
- package/dist/public/assets/{flowDiagram-KWPJA3E3-CyX8qF-3.js → flowDiagram-KWPJA3E3-WuqnDSiF.js} +1 -1
- package/dist/public/assets/{ganttDiagram-FUAMR5RP-BMFOvdDK.js → ganttDiagram-FUAMR5RP-DZ4lXzkC.js} +1 -1
- package/dist/public/assets/{gitGraphDiagram-X574FWY7-D1sZiiWD.js → gitGraphDiagram-X574FWY7-t6l07b9g.js} +1 -1
- package/dist/public/assets/index-DiYff8oR.css +32 -0
- package/dist/public/assets/index-YZNK723o.js +203 -0
- package/dist/public/assets/{infoDiagram-VRGFBTTK-CiayIr0f.js → infoDiagram-VRGFBTTK-iUGqoOFp.js} +1 -1
- package/dist/public/assets/{ishikawaDiagram-OU5B5YK6-Bf4mzvKk.js → ishikawaDiagram-OU5B5YK6-B0TmoUOK.js} +1 -1
- package/dist/public/assets/{journeyDiagram-ZHPQQLJL-mvwzOMZ7.js → journeyDiagram-ZHPQQLJL-CQE61MCu.js} +1 -1
- package/dist/public/assets/{kanban-definition-PNTS6WVX-CMFYAtvx.js → kanban-definition-PNTS6WVX-D2MWCUyS.js} +1 -1
- package/dist/public/assets/{linear-BcK1QK3e.js → linear-WKGxVNP1.js} +1 -1
- package/dist/public/assets/{mermaid.core-BmCR7le1.js → mermaid.core-DPEwUsaj.js} +4 -4
- package/dist/public/assets/{mindmap-definition-NLK3R4M7-hvUFWkg7.js → mindmap-definition-NLK3R4M7-DsYEsBsI.js} +1 -1
- package/dist/public/assets/{pegDiagram-GJSIUBJH-B8FGnth6.js → pegDiagram-GJSIUBJH-BS52Fbxt.js} +1 -1
- package/dist/public/assets/{pieDiagram-5QR66LMP-BdhppGvz.js → pieDiagram-5QR66LMP-D4o8g7nW.js} +1 -1
- package/dist/public/assets/{quadrantDiagram-O4NWA36T-i6XjZJ8W.js → quadrantDiagram-O4NWA36T-DnYUxWll.js} +1 -1
- package/dist/public/assets/{railroadDiagram-XR7U4H2S-C7XoA5kk.js → railroadDiagram-XR7U4H2S-BKy_7ULV.js} +1 -1
- package/dist/public/assets/{requirementDiagram-PLB6GJNP-tq6onssc.js → requirementDiagram-PLB6GJNP-Ce5F4u8K.js} +1 -1
- package/dist/public/assets/{sankeyDiagram-IPEJSGJF-DRW13BH8.js → sankeyDiagram-IPEJSGJF-CCJDrut_.js} +1 -1
- package/dist/public/assets/{sequenceDiagram-PO4LG4MO-BJqhIpnw.js → sequenceDiagram-PO4LG4MO-D5E8ISM1.js} +1 -1
- package/dist/public/assets/{sizeCapture-INFHLROL-BCpyNpYK.js → sizeCapture-INFHLROL-BW3KmxDN.js} +1 -1
- package/dist/public/assets/{stateDiagram-v2-GCMORJYK-Da3aSdeG.js → stateDiagram-v2-GCMORJYK-Bw2ZzQOE.js} +1 -1
- package/dist/public/assets/{swimlanes-2SLR337P-oT62nN2e.js → swimlanes-2SLR337P-CLJ-6Ex_.js} +1 -1
- package/dist/public/assets/swimlanesDiagram-TC7HE7FX-BD8rQfV7.js +8 -0
- package/dist/public/assets/{timeline-definition-EJHVYXUP-CQ9Q15bt.js → timeline-definition-EJHVYXUP-C_lFJ-4W.js} +1 -1
- package/dist/public/assets/{usecaseDiagram-POWQR4AR-De1gJb8v.js → usecaseDiagram-POWQR4AR-DWXawL2A.js} +1 -1
- package/dist/public/assets/{vennDiagram-UO4OBE2U-BusPyTiq.js → vennDiagram-UO4OBE2U-D4O5oZal.js} +1 -1
- package/dist/public/assets/{wardleyDiagram-VNRHLVJA-D-vaN_gJ.js → wardleyDiagram-VNRHLVJA-B6HElk__.js} +1 -1
- package/dist/public/assets/{xychartDiagram-PMCCYNJV-BcOmOj-j.js → xychartDiagram-PMCCYNJV-C5OAXnJy.js} +1 -1
- package/dist/public/index.html +2 -2
- package/dist/src/agent-runtime/adapter.js +54 -10
- package/dist/src/agent-runtime/builtinRuntime.js +67 -6
- package/dist/src/agent-runtime/conversation.js +49 -9
- package/dist/src/agent-runtime/interaction.js +26 -12
- package/dist/src/agent-runtime/interactionLiveHub.js +4 -0
- package/dist/src/agent-runtime/runtime.js +83 -27
- package/dist/src/agent-runtime/tmuxRuntime.js +36 -6
- package/dist/src/agents/claude.js +2 -19
- package/dist/src/agents/claudeInteraction.js +2 -132
- package/dist/src/agents/claudePaneInput.js +22 -22
- package/dist/src/agents/codebuddy.js +100 -0
- package/dist/src/agents/codebuddyConversation.js +414 -0
- package/dist/src/agents/codebuddyInteraction.js +4 -0
- package/dist/src/agents/codebuddyNativeTail.js +206 -0
- package/dist/src/agents/codebuddyPaneInput.js +76 -0
- package/dist/src/agents/codebuddyTranscriptParse.js +178 -0
- package/dist/src/agents/hookEvents.js +143 -0
- package/dist/src/agents/hookInteraction.js +178 -0
- package/dist/src/agents/index.js +2 -1
- package/dist/src/claudeEvents.js +4 -5
- package/dist/src/cli/agentIntegration.js +31 -2
- package/dist/src/cli/codebuddyHooks.js +286 -0
- package/dist/src/cli/hookScaffold.js +25 -12
- package/dist/src/cli/i18n/en.js +1 -1
- package/dist/src/cli/i18n/zh.js +1 -1
- package/dist/src/cli/state.js +4 -0
- package/dist/src/cli/supervisor.js +2 -1
- package/dist/src/codebuddyEvents.js +151 -0
- package/dist/src/docs.js +17 -6
- package/dist/src/logDetail.js +27 -0
- package/dist/src/paneInput.js +107 -10
- package/dist/src/pendingPrompt.js +21 -5
- package/dist/src/routes/system.js +12 -2
- package/dist/src/server.js +69 -8
- package/dist/src/workspace/checkpointer.js +48 -4
- package/dist/src/workspace/lock.js +54 -10
- package/dist/src/workspace/runtime.js +23 -8
- package/dist/src/workspaceProtocol.js +1 -0
- package/package.json +3 -3
- package/dist/public/assets/channel-kkiOw5AC.js +0 -1
- package/dist/public/assets/index-DqCUBYR8.css +0 -32
- package/dist/public/assets/index-OW7snGiD.js +0 -362
- package/dist/public/assets/swimlanesDiagram-TC7HE7FX-YHG5AQi4.js +0 -8
|
@@ -1,9 +1,17 @@
|
|
|
1
1
|
import { captureWorkspace } from './capture.js';
|
|
2
2
|
import { detectEnvironmentChange } from './environment.js';
|
|
3
|
+
import { lockOwnerLabel } from './lock.js';
|
|
3
4
|
import { fingerprintSnapshot } from './schema.js';
|
|
4
5
|
function sameSnapshot(left, right) {
|
|
5
6
|
return fingerprintSnapshot(left) === fingerprintSnapshot(right);
|
|
6
7
|
}
|
|
8
|
+
function formatDuration(ms) {
|
|
9
|
+
const seconds = Math.max(0, Math.round(ms / 1000));
|
|
10
|
+
if (seconds < 60)
|
|
11
|
+
return `${seconds}s`;
|
|
12
|
+
const minutes = Math.round(seconds / 60);
|
|
13
|
+
return minutes < 60 ? `${minutes}m` : `${Math.round(minutes / 60)}h`;
|
|
14
|
+
}
|
|
7
15
|
function snapshotEnvironment(observed) {
|
|
8
16
|
return {
|
|
9
17
|
id: observed.id,
|
|
@@ -13,8 +21,16 @@ function snapshotEnvironment(observed) {
|
|
|
13
21
|
}
|
|
14
22
|
async function reconcileOnce(deps, cause) {
|
|
15
23
|
const handle = await deps.lock.tryAcquire({ operationId: `checkpointer:${cause}` });
|
|
16
|
-
if (!handle)
|
|
17
|
-
|
|
24
|
+
if (!handle) {
|
|
25
|
+
// Report who is holding it: "locked" on its own is indistinguishable from a stuck lock, which is
|
|
26
|
+
// exactly how a blocked checkpointer stayed invisible.
|
|
27
|
+
let owner = null;
|
|
28
|
+
try {
|
|
29
|
+
owner = await deps.lock.readOwner?.() ?? null;
|
|
30
|
+
}
|
|
31
|
+
catch { /* the holder may be unreadable */ }
|
|
32
|
+
return { status: 'locked', owner };
|
|
33
|
+
}
|
|
18
34
|
try {
|
|
19
35
|
const observed = await deps.observeEnvironment();
|
|
20
36
|
if (!observed || observed.status === 'unknown')
|
|
@@ -75,6 +91,19 @@ export function createCheckpointer({ setInterval = globalThis.setInterval, clear
|
|
|
75
91
|
let stopPromise = null;
|
|
76
92
|
let lastResult = null;
|
|
77
93
|
let lastError = null;
|
|
94
|
+
let blockedSince = null;
|
|
95
|
+
let blockedOwner = null;
|
|
96
|
+
function recordResult(result) {
|
|
97
|
+
lastResult = result;
|
|
98
|
+
if (result.status !== 'locked') {
|
|
99
|
+
blockedSince = null;
|
|
100
|
+
blockedOwner = null;
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
if (blockedSince === null)
|
|
104
|
+
blockedSince = now();
|
|
105
|
+
blockedOwner = lockOwnerLabel(result.owner);
|
|
106
|
+
}
|
|
78
107
|
function launch(cause) {
|
|
79
108
|
const current = reconcileOnce(deps, cause);
|
|
80
109
|
running = current;
|
|
@@ -89,7 +118,7 @@ export function createCheckpointer({ setInterval = globalThis.setInterval, clear
|
|
|
89
118
|
}
|
|
90
119
|
};
|
|
91
120
|
current.then((result) => {
|
|
92
|
-
|
|
121
|
+
recordResult(result);
|
|
93
122
|
lastError = null;
|
|
94
123
|
settled();
|
|
95
124
|
}, () => {
|
|
@@ -131,6 +160,12 @@ export function createCheckpointer({ setInterval = globalThis.setInterval, clear
|
|
|
131
160
|
const outstanding = () => (running ?? pendingConfirmation?.promise ?? null);
|
|
132
161
|
return {
|
|
133
162
|
reconcile,
|
|
163
|
+
// How long the writer lock has been unavailable, with whoever holds it. Null while reconciliation is
|
|
164
|
+
// free to run. Readiness cannot answer this — a restore holding the lock is legitimately "locked" and
|
|
165
|
+
// the launcher must still see the server as up — so the protection status consumes this instead.
|
|
166
|
+
blocked() {
|
|
167
|
+
return blockedSince === null ? null : { since: blockedSince, owner: blockedOwner };
|
|
168
|
+
},
|
|
134
169
|
health() {
|
|
135
170
|
if (stopping || stopped)
|
|
136
171
|
return { status: 'degraded', detail: 'workspace-stopped' };
|
|
@@ -142,7 +177,16 @@ export function createCheckpointer({ setInterval = globalThis.setInterval, clear
|
|
|
142
177
|
return { status: 'ready', detail: null };
|
|
143
178
|
}
|
|
144
179
|
if (lastResult.status === 'locked') {
|
|
145
|
-
|
|
180
|
+
// Still "ready": a restore holds this lock on purpose, and the launcher must not read that as the
|
|
181
|
+
// server being down. The detail carries the holder and the duration so a lock that never clears is
|
|
182
|
+
// diagnosable from the outside.
|
|
183
|
+
const held = blockedSince === null ? null : formatDuration(now() - blockedSince);
|
|
184
|
+
return {
|
|
185
|
+
status: 'ready',
|
|
186
|
+
detail: 'workspace-writer-locked'
|
|
187
|
+
+ (blockedOwner ? `: ${blockedOwner}` : '')
|
|
188
|
+
+ (held ? ` for ${held}` : ''),
|
|
189
|
+
};
|
|
146
190
|
}
|
|
147
191
|
return { status: 'degraded', detail: `workspace-${lastResult.status || 'unknown'}` };
|
|
148
192
|
},
|
|
@@ -17,13 +17,19 @@ function defaultProcessAlive(pid) {
|
|
|
17
17
|
}
|
|
18
18
|
}
|
|
19
19
|
const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
20
|
-
|
|
20
|
+
/** Who a lock record says is holding it, or null when the record says nothing usable. */
|
|
21
|
+
export function lockOwnerLabel(value) {
|
|
21
22
|
const owner = asRecord(value);
|
|
22
23
|
if (!owner)
|
|
23
|
-
return
|
|
24
|
-
const operation = typeof owner.operationId === 'string' && owner.operationId ? owner.operationId :
|
|
25
|
-
const pid = Number.isInteger(owner.pid) ? owner.pid :
|
|
26
|
-
|
|
24
|
+
return null;
|
|
25
|
+
const operation = typeof owner.operationId === 'string' && owner.operationId ? owner.operationId : null;
|
|
26
|
+
const pid = Number.isInteger(owner.pid) ? owner.pid : null;
|
|
27
|
+
if (!operation && pid === null)
|
|
28
|
+
return null;
|
|
29
|
+
return `${operation || 'unknown operation'} (pid ${pid ?? 'unknown'})`;
|
|
30
|
+
}
|
|
31
|
+
function ownerLabel(value) {
|
|
32
|
+
return lockOwnerLabel(value) ?? 'unknown owner';
|
|
27
33
|
}
|
|
28
34
|
export function createWorkspaceLock({ dir, fs = fsp, pid = process.pid, now = Date.now, isProcessAlive = defaultProcessAlive, wait = delay, timeoutMs = 5_000, retryMs = 50, staleGraceMs = 5_000, randomUUID = crypto.randomUUID, }) {
|
|
29
35
|
if (typeof dir !== 'string' || !dir)
|
|
@@ -46,21 +52,59 @@ export function createWorkspaceLock({ dir, fs = fsp, pid = process.pid, now = Da
|
|
|
46
52
|
return { value: null, mtimeMs: stat.mtimeMs };
|
|
47
53
|
}
|
|
48
54
|
}
|
|
55
|
+
async function reclaimUnreadableOwner(record) {
|
|
56
|
+
// The record is missing, truncated, or malformed — the shape a holder leaves when it dies between
|
|
57
|
+
// creating the directory and writing its record. Age is the directory's own, which is when it appeared.
|
|
58
|
+
// `rmdir` only removes an EMPTY directory, so this can never take the lock from a holder that has
|
|
59
|
+
// already written its record; an empty directory is by definition nobody's. The worst a contender can
|
|
60
|
+
// do to a holder is hit the microseconds before that holder's own record exists, and that holder then
|
|
61
|
+
// fails loudly instead of silently sharing the lock.
|
|
62
|
+
try {
|
|
63
|
+
await fs.rmdir(dir);
|
|
64
|
+
return true;
|
|
65
|
+
}
|
|
66
|
+
catch (error) {
|
|
67
|
+
if (errorCode(error) === 'ENOENT')
|
|
68
|
+
return true;
|
|
69
|
+
if (errorCode(error) !== 'ENOTEMPTY')
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
// Not empty after all: a record exists but cannot be read. Move the whole directory aside. The
|
|
73
|
+
// destination comes from the observed mtime, never from the unvalidated record, so no path is built
|
|
74
|
+
// from content we could not verify — and because the directory is not empty, the tombstone it leaves
|
|
75
|
+
// cannot be replaced by a lock taken after this look.
|
|
76
|
+
const staleDir = `${dir}.stale.unreadable-${Math.round(record.mtimeMs)}`;
|
|
77
|
+
try {
|
|
78
|
+
await fs.rename(dir, staleDir);
|
|
79
|
+
}
|
|
80
|
+
catch (error) {
|
|
81
|
+
if (errorCode(error) === 'ENOENT')
|
|
82
|
+
return true;
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
return true;
|
|
86
|
+
}
|
|
49
87
|
async function reclaimIfStale(record) {
|
|
50
88
|
if (!record)
|
|
51
89
|
return true;
|
|
52
90
|
const owner = asRecord(record.value);
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
91
|
+
const candidate = owner?.token;
|
|
92
|
+
const token = typeof candidate === 'string' && SAFE_TOKEN.test(candidate) ? candidate : null;
|
|
93
|
+
// An unusable record carries no trustworthy clock, so its age comes from the directory itself.
|
|
94
|
+
const startedAt = token && owner && typeof owner.startedAt === 'string' ? Date.parse(owner.startedAt) : Number.NaN;
|
|
56
95
|
const age = now() - (Number.isFinite(startedAt) ? startedAt : record.mtimeMs);
|
|
96
|
+
// The grace period is what keeps a contender out of the window between a holder's `mkdir` and the
|
|
97
|
+
// record it is about to write.
|
|
57
98
|
if (age < staleGraceMs)
|
|
58
99
|
return false;
|
|
59
|
-
|
|
100
|
+
const pid = owner && typeof owner.pid === 'number' && Number.isInteger(owner.pid) ? owner.pid : null;
|
|
101
|
+
if (pid !== null && await isProcessAlive(pid))
|
|
60
102
|
return false;
|
|
103
|
+
if (!token)
|
|
104
|
+
return reclaimUnreadableOwner(record);
|
|
61
105
|
// Every contender that observed this owner uses the SAME destination. The winner leaves the renamed
|
|
62
106
|
// directory as a tombstone, so a loser cannot later rename a newly-created lock out of the way.
|
|
63
|
-
const staleDir = `${dir}.stale.${
|
|
107
|
+
const staleDir = `${dir}.stale.${token}`;
|
|
64
108
|
try {
|
|
65
109
|
await fs.rename(dir, staleDir);
|
|
66
110
|
}
|
|
@@ -7,6 +7,10 @@ import { buildRecoveryMapping, } from './mapping.js';
|
|
|
7
7
|
const recordOf = (value) => (value && typeof value === 'object' && !Array.isArray(value)
|
|
8
8
|
? value : null);
|
|
9
9
|
const errorMessage = (error) => error instanceof Error ? error.message : String(error);
|
|
10
|
+
// A restore holds the writer lock for as long as it runs, and that is normal — so a lock somebody else is
|
|
11
|
+
// holding only means the workspace stopped being captured once it has been held far longer than a restore
|
|
12
|
+
// takes. Nothing here steals the lock; it changes what the status reports.
|
|
13
|
+
const WRITER_BLOCK_THRESHOLD_MS = 5 * 60_000;
|
|
10
14
|
function unwrapCheckpoint(result) {
|
|
11
15
|
const record = recordOf(result);
|
|
12
16
|
if (record?.status !== 'ok' || !record.value) {
|
|
@@ -192,17 +196,28 @@ export function createWorkspaceRuntime({ store, tmux, lock, checkpointer, now =
|
|
|
192
196
|
catch {
|
|
193
197
|
return { status: 'degraded', lastSuccessfulCaptureAt: null, errorCode: 'live-unavailable' };
|
|
194
198
|
}
|
|
199
|
+
if (live.status !== 'ok' && live.status !== 'empty') {
|
|
200
|
+
return {
|
|
201
|
+
status: 'degraded',
|
|
202
|
+
lastSuccessfulCaptureAt: null,
|
|
203
|
+
errorCode: live.status === 'corrupt' ? 'live-corrupt' : 'live-unavailable',
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
// A healthy live copy is not protection: if the writer lock has been out of reach for long enough, the
|
|
207
|
+
// copy is simply the last one that was ever written, and capture is what has stopped.
|
|
208
|
+
const blocked = checkpointer.blocked?.();
|
|
209
|
+
if (blocked && Number.isFinite(blocked.since) && now() - blocked.since >= WRITER_BLOCK_THRESHOLD_MS) {
|
|
210
|
+
return {
|
|
211
|
+
status: 'degraded',
|
|
212
|
+
lastSuccessfulCaptureAt: live.status === 'ok' ? live.value.capturedAt || null : null,
|
|
213
|
+
errorCode: 'writer-locked',
|
|
214
|
+
blockedBy: blocked.owner,
|
|
215
|
+
};
|
|
216
|
+
}
|
|
195
217
|
if (live.status === 'ok') {
|
|
196
218
|
return { status: 'protected', lastSuccessfulCaptureAt: live.value.capturedAt || null, errorCode: null };
|
|
197
219
|
}
|
|
198
|
-
|
|
199
|
-
return { status: 'unprotected', lastSuccessfulCaptureAt: null, errorCode: null };
|
|
200
|
-
}
|
|
201
|
-
return {
|
|
202
|
-
status: 'degraded',
|
|
203
|
-
lastSuccessfulCaptureAt: null,
|
|
204
|
-
errorCode: live.status === 'corrupt' ? 'live-corrupt' : 'live-unavailable',
|
|
205
|
-
};
|
|
220
|
+
return { status: 'unprotected', lastSuccessfulCaptureAt: null, errorCode: null };
|
|
206
221
|
}
|
|
207
222
|
async function performRestore(operationId, request, onProgress, onRunning, onTerminal) {
|
|
208
223
|
let result;
|
|
@@ -42,6 +42,7 @@ export function parseWorkspaceProtectionStatus(value) {
|
|
|
42
42
|
...optionalField('status', memberOf(PROTECTION_STATUSES, status.status)),
|
|
43
43
|
...optionalField('lastSuccessfulCaptureAt', optionalString(status.lastSuccessfulCaptureAt)),
|
|
44
44
|
...optionalField('errorCode', optionalString(status.errorCode)),
|
|
45
|
+
...optionalField('blockedBy', optionalString(status.blockedBy)),
|
|
45
46
|
};
|
|
46
47
|
}
|
|
47
48
|
export function parseWorkspaceRestoreStart(value) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "handmux",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.31.0",
|
|
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,13 +75,13 @@
|
|
|
75
75
|
"vitest": "^2.0.0"
|
|
76
76
|
},
|
|
77
77
|
"whatsNew": [
|
|
78
|
+
{"version":"0.31.0","date":"2026-09-22","zh":"CodeBuddy 对话接入 · Agent 状态更可靠","en":"CodeBuddy chat support · more reliable Agent state"},
|
|
78
79
|
{"version":"0.30.0","date":"2026-09-19","zh":"文档预览全面升级:朗读、目录、查找、图表","en":"A full document reader: read-aloud, outline, search, diagrams"},
|
|
79
80
|
{"version":"0.29.2","date":"2026-09-18","zh":"修复 Linux 下 Claude 事件上报的兼容问题","en":"Compatibility fix for Claude event reporting on Linux"},
|
|
80
81
|
{"version":"0.29.1","date":"2026-09-18","zh":"修复 Claude 对话入口丢失 · 长时间运行不再变卡","en":"Fix lost Claude chat entries · no slowdown after long runs"},
|
|
81
82
|
{"version":"0.29.0","date":"2026-09-16","zh":"可信设备与可信地址保护","en":"Trusted device and address protection"},
|
|
82
83
|
{"version":"0.28.0","date":"2026-09-11","zh":"Claude 对话更可靠 · 工具图片预览","en":"More reliable Claude chat · tool image previews"},
|
|
83
84
|
{"version":"0.27.2","date":"2026-09-08","zh":"修复多标签页偶发鉴权弹窗","en":"Fix intermittent authentication prompts across tabs"},
|
|
84
|
-
{"version":"0.27.1","date":"2026-09-08","zh":"腾讯语音 · 终端光标修复 · Codex 对话恢复","en":"Tencent voice · terminal cursor fixes · Codex chat recovery"}
|
|
85
|
-
{"version":"0.27.0","date":"2026-09-05","zh":"腾讯语音输入 · 终端输入更准确","en":"Tencent voice input · more accurate terminal typing"}
|
|
85
|
+
{"version":"0.27.1","date":"2026-09-08","zh":"腾讯语音 · 终端光标修复 · Codex 对话恢复","en":"Tencent voice · terminal cursor fixes · Codex chat recovery"}
|
|
86
86
|
]
|
|
87
87
|
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{U as o,a as n}from"./mermaid.core-BmCR7le1.js";const t=(r,a)=>o.lang.round(n.parse(r)[a]);export{t as c};
|