@martintrojer/murmur 0.1.4 → 0.2.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/ARCHITECTURE.md +740 -225
- package/CHANGELOG.md +97 -0
- package/README.md +136 -27
- package/dist/cli.js +1302 -833
- package/dist/cli.js.map +1 -1
- package/dist/extension/murmur-pi.js +189 -107
- package/dist/extension/murmur-pi.js.map +1 -1
- package/dist/extension/store.js +413 -198
- package/dist/extension/store.js.map +1 -1
- package/dist/index.d.ts +479 -187
- package/dist/index.js +848 -607
- package/dist/index.js.map +1 -1
- package/package.json +3 -2
|
@@ -3,6 +3,19 @@ import { execFileSync as execFileSync2 } from "child_process";
|
|
|
3
3
|
|
|
4
4
|
// src/mux.ts
|
|
5
5
|
import { execFileSync } from "child_process";
|
|
6
|
+
|
|
7
|
+
// src/ids.ts
|
|
8
|
+
function asSessionId(raw) {
|
|
9
|
+
return raw;
|
|
10
|
+
}
|
|
11
|
+
function asWindowId(raw) {
|
|
12
|
+
return raw;
|
|
13
|
+
}
|
|
14
|
+
function asPaneId(raw) {
|
|
15
|
+
return raw;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// src/mux.ts
|
|
6
19
|
function runTmux(args) {
|
|
7
20
|
try {
|
|
8
21
|
return execFileSync("tmux", args, {
|
|
@@ -14,10 +27,20 @@ function runTmux(args) {
|
|
|
14
27
|
return null;
|
|
15
28
|
}
|
|
16
29
|
}
|
|
30
|
+
function exactSession(session) {
|
|
31
|
+
return `=${session}`;
|
|
32
|
+
}
|
|
33
|
+
function exactPaneTarget(session) {
|
|
34
|
+
return `=${session}:`;
|
|
35
|
+
}
|
|
36
|
+
function tmuxBadgeState(state) {
|
|
37
|
+
return state === "running" ? "working" : state;
|
|
38
|
+
}
|
|
17
39
|
var tmux = {
|
|
18
40
|
currentWindow() {
|
|
19
|
-
const
|
|
20
|
-
if (!
|
|
41
|
+
const raw = process.env.TMUX_PANE;
|
|
42
|
+
if (!raw) return null;
|
|
43
|
+
const pane = asPaneId(raw);
|
|
21
44
|
const fields = runTmux([
|
|
22
45
|
"display-message",
|
|
23
46
|
"-t",
|
|
@@ -28,38 +51,31 @@ var tmux = {
|
|
|
28
51
|
const [session, window, sessionName, windowName] = fields?.split(" ") ?? [];
|
|
29
52
|
if (!session || !window) return null;
|
|
30
53
|
return {
|
|
31
|
-
session,
|
|
32
|
-
window,
|
|
54
|
+
session: asSessionId(session),
|
|
55
|
+
window: asWindowId(window),
|
|
33
56
|
pane,
|
|
34
57
|
session_name: sessionName || null,
|
|
35
58
|
window_name: windowName || null
|
|
36
59
|
};
|
|
37
60
|
},
|
|
38
|
-
// Which of this host's
|
|
39
|
-
//
|
|
40
|
-
//
|
|
41
|
-
//
|
|
61
|
+
// Which of this host's PANES still exist. The only liveness question tmux is
|
|
62
|
+
// ever asked, and the one that matches how an agent is addressed: a pane keeps
|
|
63
|
+
// its id when it moves between windows, so a recorded window id can be gone
|
|
64
|
+
// while the agent is very much alive.
|
|
42
65
|
//
|
|
43
|
-
// null means
|
|
44
|
-
//
|
|
45
|
-
//
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
// Unlike currentWindow, this deliberately asks tmux rather than reading the
|
|
49
|
-
// environment, and it is right to: "which windows exist on this host" is a
|
|
50
|
-
// server-wide question with one answer, and export runs over ssh with no
|
|
51
|
-
// pane of its own. currentWindow asks "which pane am I in", which only
|
|
52
|
-
// $TMUX_PANE can answer.
|
|
53
|
-
liveWindows() {
|
|
54
|
-
const out = runTmux(["list-windows", "-a", "-F", "#{window_id}"]);
|
|
66
|
+
// null means tmux could not answer; an empty set means it did and there are
|
|
67
|
+
// none. Conflating the two would delete every agent on the host the moment
|
|
68
|
+
// tmux was briefly unreachable.
|
|
69
|
+
livePanes() {
|
|
70
|
+
const out = runTmux(["list-panes", "-a", "-F", "#{pane_id}"]);
|
|
55
71
|
if (out === null) return null;
|
|
56
|
-
return new Set(out.split("\n").filter(Boolean));
|
|
72
|
+
return new Set(out.split("\n").filter(Boolean).map(asPaneId));
|
|
57
73
|
},
|
|
58
|
-
|
|
74
|
+
setWindowBadge(window, state) {
|
|
59
75
|
if (state === null) {
|
|
60
76
|
runTmux(["set-window-option", "-qu", "-t", window, "@agent_state"]);
|
|
61
77
|
} else {
|
|
62
|
-
runTmux(["set-window-option", "-q", "-t", window, "@agent_state", state]);
|
|
78
|
+
runTmux(["set-window-option", "-q", "-t", window, "@agent_state", tmuxBadgeState(state)]);
|
|
63
79
|
runTmux(["set-window-option", "-q", "-t", window, "@pane_agent", "1"]);
|
|
64
80
|
}
|
|
65
81
|
runTmux(["refresh-client", "-S"]);
|
|
@@ -68,45 +84,51 @@ var tmux = {
|
|
|
68
84
|
runTmux(["switch-client", "-t", session]);
|
|
69
85
|
return runTmux(["select-window", "-t", window]) !== null;
|
|
70
86
|
},
|
|
71
|
-
// Window ids are what the log stores, because they are stable; names are
|
|
72
|
-
// what a human recognises in a picker. Names are live tmux state, not
|
|
73
|
-
// history, so they are resolved at render time rather than recorded.
|
|
74
|
-
windowNames() {
|
|
75
|
-
const out = runTmux(["list-windows", "-a", "-F", "#{window_id} #{window_name}"]);
|
|
76
|
-
const names = /* @__PURE__ */ new Map();
|
|
77
|
-
for (const line of out?.split("\n") ?? []) {
|
|
78
|
-
const [id, name] = line.split(" ");
|
|
79
|
-
if (id && name) names.set(id, name);
|
|
80
|
-
}
|
|
81
|
-
return names;
|
|
82
|
-
},
|
|
83
|
-
// First window carrying this exact name, or null. Used to reuse a per-host
|
|
84
|
-
// ssh window instead of opening another one.
|
|
85
87
|
// Sibling panes, for deciding whether an unowned pane may clear the window's
|
|
86
88
|
// badge. A window holding an agent and a shell must not lose the badge when
|
|
87
89
|
// you focus the shell.
|
|
88
90
|
panesInWindow(window) {
|
|
89
91
|
const out = runTmux(["list-panes", "-t", window, "-F", "#{pane_id}"]);
|
|
90
|
-
return out?.split("\n").filter(Boolean) ?? [];
|
|
92
|
+
return out?.split("\n").filter(Boolean).map(asPaneId) ?? [];
|
|
91
93
|
},
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
}
|
|
98
|
-
return null;
|
|
94
|
+
// Which client to send home when the remote attach exits. `switch-client`
|
|
95
|
+
// with no -c moves whichever client tmux considers current, and `murmur pick`
|
|
96
|
+
// usually runs in a popup -- a client of its own, which dies with the popup.
|
|
97
|
+
// Naming the real client is what lets the return outlive the picker.
|
|
98
|
+
clientName() {
|
|
99
|
+
return runTmux(["display-message", "-p", "#{client_name}"]) || null;
|
|
99
100
|
},
|
|
100
|
-
|
|
101
|
-
|
|
101
|
+
// Where the jump started, as a switch-client target. Window-level, not just
|
|
102
|
+
// the session: coming back to the right session but the wrong window is
|
|
103
|
+
// still the wrong place. The window id is stable where its index is not,
|
|
104
|
+
// since renumber-windows renumbers on every close.
|
|
105
|
+
currentTarget() {
|
|
106
|
+
return runTmux(["display-message", "-p", "#{session_name}:#{window_id}"]) || null;
|
|
107
|
+
},
|
|
108
|
+
// Whether a wrapper session for this host already exists. Deliberately not
|
|
109
|
+
// returning an id: a session is addressed by name, so a `#{session_id}` would
|
|
110
|
+
// only have to be turned back into one.
|
|
111
|
+
sessionNamed(name) {
|
|
112
|
+
const out = runTmux(["list-sessions", "-F", "#{session_name}"]);
|
|
113
|
+
if (out === null) return false;
|
|
114
|
+
return out.split("\n").includes(name);
|
|
115
|
+
},
|
|
116
|
+
newSession(name, command) {
|
|
117
|
+
return runTmux(["new-session", "-d", "-s", name, command]) !== null;
|
|
118
|
+
},
|
|
119
|
+
setSessionOption(session, option, value) {
|
|
120
|
+
runTmux(["set-option", "-t", exactPaneTarget(session), option, value]);
|
|
102
121
|
},
|
|
103
|
-
|
|
104
|
-
|
|
122
|
+
switchClient(client, session) {
|
|
123
|
+
const target = exactSession(session);
|
|
124
|
+
const args = client ? ["switch-client", "-c", client, "-t", target] : ["switch-client", "-t", target];
|
|
125
|
+
return runTmux(args) !== null;
|
|
105
126
|
},
|
|
106
|
-
// The window a pane belongs to, for a pane murmur
|
|
127
|
+
// The window a pane belongs to, for a pane murmur holds no row for. Clearing
|
|
107
128
|
// a badge is a tmux operation and does not require murmur to own the pane.
|
|
108
129
|
windowForPane(pane) {
|
|
109
|
-
|
|
130
|
+
const out = runTmux(["display-message", "-t", pane, "-p", "#{window_id}"]);
|
|
131
|
+
return out ? asWindowId(out) : null;
|
|
110
132
|
},
|
|
111
133
|
capture(pane, lines) {
|
|
112
134
|
const args = ["capture-pane", "-p", "-t", pane];
|
|
@@ -116,16 +138,16 @@ var tmux = {
|
|
|
116
138
|
};
|
|
117
139
|
|
|
118
140
|
// src/extension/decide.ts
|
|
119
|
-
function
|
|
120
|
-
if (muManaged2) return
|
|
121
|
-
return focused2 ?
|
|
141
|
+
function settledState(focused2, muManaged2) {
|
|
142
|
+
if (muManaged2) return null;
|
|
143
|
+
return focused2 ? null : "done";
|
|
122
144
|
}
|
|
123
145
|
function driverFromEnv(env) {
|
|
124
146
|
return env.MU_MANAGED_AGENT === "1" || env.MU_AGENT_NAME ? "orchestrated" : "human";
|
|
125
147
|
}
|
|
126
148
|
|
|
127
149
|
// src/extension/murmur-pi.ts
|
|
128
|
-
var storeModule = "@martintrojer/murmur/extension-store";
|
|
150
|
+
var storeModule = process.env.MURMUR_STORE_MODULE || "@martintrojer/murmur/extension-store";
|
|
129
151
|
var muManaged = process.env.MU_MANAGED_AGENT === "1";
|
|
130
152
|
var driver = driverFromEnv(process.env);
|
|
131
153
|
function focused(pane) {
|
|
@@ -153,89 +175,149 @@ function safeSessionName(pi) {
|
|
|
153
175
|
}
|
|
154
176
|
}
|
|
155
177
|
function murmurPi(pi) {
|
|
156
|
-
const
|
|
157
|
-
if (!
|
|
158
|
-
let
|
|
159
|
-
|
|
178
|
+
const startLocation = tmux.currentWindow();
|
|
179
|
+
if (!startLocation) return;
|
|
180
|
+
let lastWindow = startLocation.window;
|
|
181
|
+
const here = () => {
|
|
182
|
+
const location = tmux.currentWindow() ?? startLocation;
|
|
183
|
+
if (location.window !== lastWindow) {
|
|
184
|
+
try {
|
|
185
|
+
tmux.setWindowBadge(lastWindow, null);
|
|
186
|
+
} catch {
|
|
187
|
+
}
|
|
188
|
+
lastWindow = location.window;
|
|
189
|
+
}
|
|
190
|
+
return location;
|
|
191
|
+
};
|
|
192
|
+
const meta = () => ({
|
|
193
|
+
// mu names its agents; pi names its sessions. Both beat a window name when
|
|
194
|
+
// present, and neither can be recovered from tmux.
|
|
195
|
+
agent_name: process.env.MU_AGENT_NAME ?? null,
|
|
196
|
+
pi_session: safeSessionName(pi),
|
|
197
|
+
workstream: process.env.MU_WORKSTREAM ?? null,
|
|
198
|
+
role: process.env.MU_ROLE ?? null,
|
|
199
|
+
cli: "pi",
|
|
200
|
+
driver
|
|
201
|
+
});
|
|
202
|
+
let state = { kind: "untried" };
|
|
203
|
+
let refused = false;
|
|
204
|
+
let agentId = null;
|
|
160
205
|
let queue = Promise.resolve();
|
|
161
206
|
const enqueue = (work) => {
|
|
162
207
|
queue = queue.then(work, work);
|
|
163
208
|
return queue;
|
|
164
209
|
};
|
|
210
|
+
const dropStore = () => {
|
|
211
|
+
if (state.kind === "open") {
|
|
212
|
+
try {
|
|
213
|
+
state.store.close();
|
|
214
|
+
} catch {
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
state = { kind: "untried" };
|
|
218
|
+
};
|
|
165
219
|
const getStore = async () => {
|
|
166
|
-
if (
|
|
220
|
+
if (state.kind === "absent") return null;
|
|
221
|
+
if (refused) return null;
|
|
222
|
+
if (state.kind === "open") return state.store;
|
|
167
223
|
try {
|
|
168
224
|
const { loadIdentity, openStore } = await import(storeModule);
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
return store;
|
|
225
|
+
if (!loadIdentity()) {
|
|
226
|
+
state = { kind: "absent" };
|
|
227
|
+
return null;
|
|
173
228
|
}
|
|
174
|
-
store = openStore();
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
229
|
+
const store = openStore();
|
|
230
|
+
const claim = store.claimAgent({
|
|
231
|
+
location: here(),
|
|
232
|
+
owner_pid: process.pid,
|
|
233
|
+
meta: meta()
|
|
234
|
+
});
|
|
235
|
+
if (claim.outcome === "refused") {
|
|
236
|
+
store.close();
|
|
237
|
+
refused = true;
|
|
238
|
+
state = { kind: "absent" };
|
|
239
|
+
return null;
|
|
240
|
+
}
|
|
241
|
+
agentId = claim.agent_id;
|
|
242
|
+
state = { kind: "open", store };
|
|
178
243
|
return store;
|
|
179
|
-
}
|
|
180
|
-
};
|
|
181
|
-
const dropStore = () => {
|
|
182
|
-
try {
|
|
183
|
-
store?.close();
|
|
184
244
|
} catch {
|
|
245
|
+
state = { kind: "absent" };
|
|
246
|
+
return null;
|
|
185
247
|
}
|
|
186
|
-
store = null;
|
|
187
248
|
};
|
|
188
|
-
const
|
|
249
|
+
const report = async (activity, location) => {
|
|
189
250
|
try {
|
|
190
|
-
const
|
|
191
|
-
if (!
|
|
192
|
-
|
|
193
|
-
agent_id: `${hostId}:${location.pane}`,
|
|
194
|
-
session: location.session,
|
|
195
|
-
window: location.window,
|
|
196
|
-
pane: location.pane,
|
|
197
|
-
session_name: location.session_name,
|
|
198
|
-
window_name: location.window_name,
|
|
199
|
-
// mu names its agents; pi names its sessions. Both beat a window name
|
|
200
|
-
// when present, and neither can be recovered from tmux.
|
|
201
|
-
agent_name: process.env.MU_AGENT_NAME ?? null,
|
|
202
|
-
pi_session: safeSessionName(pi),
|
|
203
|
-
workstream: process.env.MU_WORKSTREAM ?? null,
|
|
204
|
-
role: process.env.MU_ROLE ?? null,
|
|
205
|
-
cli: "pi",
|
|
206
|
-
driver,
|
|
207
|
-
kind: "state",
|
|
208
|
-
state,
|
|
209
|
-
message: "",
|
|
210
|
-
pid,
|
|
211
|
-
synthetic: false,
|
|
212
|
-
reason: "",
|
|
213
|
-
extra: {}
|
|
214
|
-
});
|
|
251
|
+
const store = await getStore();
|
|
252
|
+
if (!store || !agentId) return false;
|
|
253
|
+
return store.setActivity({ agent_id: agentId, owner_pid: process.pid, activity, location });
|
|
215
254
|
} catch {
|
|
216
255
|
dropStore();
|
|
256
|
+
return false;
|
|
217
257
|
}
|
|
218
258
|
};
|
|
259
|
+
void enqueue(async () => {
|
|
260
|
+
await getStore();
|
|
261
|
+
});
|
|
262
|
+
const badge = (location, state2) => {
|
|
263
|
+
if (refused) return;
|
|
264
|
+
tmux.setWindowBadge(location.window, state2);
|
|
265
|
+
};
|
|
219
266
|
pi.on("agent_start", () => {
|
|
220
267
|
void enqueue(async () => {
|
|
221
|
-
|
|
222
|
-
await
|
|
268
|
+
const location = here();
|
|
269
|
+
if (await report("running", location)) badge(location, "running");
|
|
223
270
|
});
|
|
224
271
|
});
|
|
225
272
|
pi.on("agent_end", () => {
|
|
226
273
|
void enqueue(async () => {
|
|
227
|
-
const
|
|
228
|
-
|
|
229
|
-
|
|
274
|
+
const location = here();
|
|
275
|
+
await report("stopped", location);
|
|
276
|
+
badge(location, null);
|
|
277
|
+
});
|
|
278
|
+
});
|
|
279
|
+
pi.on("agent_settled", () => {
|
|
280
|
+
void enqueue(async () => {
|
|
281
|
+
const location = here();
|
|
282
|
+
const settled = settledState(focused(location.pane), muManaged);
|
|
283
|
+
if (settled === null) return;
|
|
284
|
+
try {
|
|
285
|
+
const store = await getStore();
|
|
286
|
+
if (!store || refused) return;
|
|
287
|
+
store.requestAttention({
|
|
288
|
+
kind: settled,
|
|
289
|
+
location,
|
|
290
|
+
message: "",
|
|
291
|
+
source: "pi"
|
|
292
|
+
});
|
|
293
|
+
tmux.setWindowBadge(location.window, settled);
|
|
294
|
+
} catch {
|
|
295
|
+
dropStore();
|
|
296
|
+
}
|
|
230
297
|
});
|
|
231
298
|
});
|
|
232
299
|
pi.on("session_shutdown", async () => {
|
|
233
300
|
await enqueue(async () => {
|
|
234
|
-
|
|
235
|
-
|
|
301
|
+
const location = here();
|
|
302
|
+
badge(location, null);
|
|
303
|
+
try {
|
|
304
|
+
if (state.kind === "open" && agentId) {
|
|
305
|
+
state.store.releaseAgent({ agent_id: agentId, owner_pid: process.pid });
|
|
306
|
+
}
|
|
307
|
+
} catch {
|
|
308
|
+
}
|
|
309
|
+
agentId = null;
|
|
236
310
|
dropStore();
|
|
237
311
|
});
|
|
238
312
|
});
|
|
313
|
+
pi.on("session_start", () => {
|
|
314
|
+
void enqueue(async () => {
|
|
315
|
+
if (state.kind === "absent") state = { kind: "untried" };
|
|
316
|
+
const location = here();
|
|
317
|
+
lastWindow = location.window;
|
|
318
|
+
await getStore();
|
|
319
|
+
});
|
|
320
|
+
});
|
|
239
321
|
}
|
|
240
322
|
export {
|
|
241
323
|
murmurPi as default
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/extension/murmur-pi.ts","../../src/mux.ts","../../src/extension/decide.ts"],"sourcesContent":["import { execFileSync } from \"node:child_process\";\nimport { tmux } from \"../mux.js\";\nimport type { Store } from \"../store.js\";\nimport type { AgentState } from \"../types.js\";\nimport { driverFromEnv, endState } from \"./decide.js\";\nimport type { StoreModule } from \"./store-api.js\";\n\n// Declared here rather than imported: murmur must not depend on pi to build,\n// and this is the whole surface the extension touches. getSessionName is\n// optional because an older pi does not have it, and a missing method must\n// degrade to \"no name\" rather than break the extension.\ntype ExtensionAPI = {\n on(\n event: \"agent_start\" | \"agent_end\" | \"session_shutdown\",\n handler: () => void | Promise<void>,\n ): void;\n getSessionName?(): string | undefined;\n};\n\n// `murmur link pi` rewrites this line to an absolute path at install time.\n// The bare specifier only resolves when murmur is a dependency of the importer,\n// which it never is: the extension is copied into ~/.pi/agent/extensions, and a\n// globally linked or installed murmur is not resolvable from there. Falling\n// back to the bare specifier keeps a hand-copied extension working inside a\n// project that does depend on murmur.\nconst storeModule = \"@martintrojer/murmur/extension-store\";\nconst muManaged = process.env.MU_MANAGED_AGENT === \"1\";\nconst driver = driverFromEnv(process.env);\n\nfunction focused(pane: string): boolean {\n try {\n return (\n execFileSync(\n \"tmux\",\n [\n \"display-message\",\n \"-t\",\n pane,\n \"-p\",\n \"#{&&:#{pane_active},#{&&:#{window_active},#{session_attached}}}\",\n ],\n { encoding: \"utf8\", timeout: 3000, stdio: [\"ignore\", \"pipe\", \"ignore\"] },\n ).trim() === \"1\"\n );\n } catch {\n return false;\n }\n}\n\n// pi.getSessionName() is a live read and a session can be unnamed, so this must\n// never be the reason an event is lost.\nfunction safeSessionName(pi: ExtensionAPI): string | null {\n try {\n return pi.getSessionName?.() || null;\n } catch {\n return null;\n }\n}\n\nexport default function murmurPi(pi: ExtensionAPI): void {\n const location = tmux.currentWindow();\n if (!location) return;\n\n let store: Store | null | undefined;\n let hostId: string | null = null;\n let queue: Promise<void> = Promise.resolve();\n\n const enqueue = (work: () => Promise<void>): Promise<void> => {\n queue = queue.then(work, work);\n return queue;\n };\n\n const getStore = async (): Promise<Store | null> => {\n if (store !== undefined) return store;\n try {\n const { loadIdentity, openStore } = (await import(storeModule)) as StoreModule;\n hostId = loadIdentity()?.host_id ?? null;\n if (!hostId) {\n store = null;\n return store;\n }\n store = openStore();\n return store;\n } catch {\n store = null;\n return store;\n }\n };\n\n // Drop the cached handle, closing it first. The catch in `append` used to\n // just assign null, which left an open SQLite connection to garbage\n // collection while the next event opened another one -- so a peer with a\n // recurring transient write failure leaked a connection and its WAL read\n // state per event, inside a pi process that can run for days. Shared with\n // session_shutdown so there is one way to let go of the store.\n const dropStore = (): void => {\n try {\n store?.close();\n } catch {\n // Best effort: extension failures must never reach pi.\n }\n store = null;\n };\n\n const append = async (state: AgentState, pid: number | null): Promise<void> => {\n try {\n const currentStore = await getStore();\n if (!currentStore || !hostId) return;\n currentStore.append({\n agent_id: `${hostId}:${location.pane}`,\n session: location.session,\n window: location.window,\n pane: location.pane,\n session_name: location.session_name,\n window_name: location.window_name,\n // mu names its agents; pi names its sessions. Both beat a window name\n // when present, and neither can be recovered from tmux.\n agent_name: process.env.MU_AGENT_NAME ?? null,\n pi_session: safeSessionName(pi),\n workstream: process.env.MU_WORKSTREAM ?? null,\n role: process.env.MU_ROLE ?? null,\n cli: \"pi\",\n driver,\n kind: \"state\",\n state,\n message: \"\",\n pid,\n synthetic: false,\n reason: \"\",\n extra: {},\n });\n } catch {\n dropStore();\n }\n };\n\n pi.on(\"agent_start\", () => {\n void enqueue(async () => {\n tmux.setState(location.window, \"working\");\n await append(\"working\", process.pid);\n });\n });\n\n pi.on(\"agent_end\", () => {\n void enqueue(async () => {\n const state = endState(focused(location.pane), muManaged);\n tmux.setState(location.window, state === \"cleared\" ? null : state);\n await append(state, null);\n });\n });\n\n pi.on(\"session_shutdown\", async () => {\n await enqueue(async () => {\n tmux.setState(location.window, null);\n await append(\"cleared\", null);\n dropStore();\n });\n });\n}\n","import { execFileSync } from \"node:child_process\";\nimport type { AgentState } from \"./types.js\";\n\nexport type Location = {\n session: string;\n window: string;\n pane: string;\n session_name: string | null;\n window_name: string | null;\n};\n\nexport interface Mux {\n currentWindow(): Location | null;\n liveWindows(): Set<string> | null;\n setState(window: string, state: AgentState | null): void;\n // Reports whether the attach actually happened. runTmux swallows failures to\n // return null, and a jump that silently failed looked exactly like \"enter did\n // nothing\" -- the symptom the remote probe was added to prevent, reproduced\n // on the local path.\n attach(session: string, window: string): boolean;\n windowNames(): Map<string, string>;\n windowForPane(pane: string): string | null;\n panesInWindow(window: string): string[];\n windowNamed(name: string): string | null;\n selectWindow(window: string): boolean;\n newWindow(name: string, command: string): boolean;\n capture(pane: string, lines?: number): string | null;\n}\n\nfunction runTmux(args: string[]): string | null {\n try {\n return execFileSync(\"tmux\", args, {\n encoding: \"utf8\",\n timeout: 3000,\n stdio: [\"ignore\", \"pipe\", \"ignore\"],\n }).trim();\n } catch {\n return null;\n }\n}\n\nexport const tmux: Mux = {\n currentWindow() {\n // $TMUX_PANE is the only trustworthy signal that we are inside a pane, and\n // it is set by tmux for every process in one.\n //\n // Asking tmux instead does not work: `display-message` answers from any\n // process on a machine with a running server, and reports whichever pane\n // that server considers active. A pi started outside tmux -- a bare ssh\n // login, a plain terminal, cron -- would then record itself as living in\n // some unrelated agent's pane and overwrite that agent's state. Falling\n // back to `display-message` here was exactly that bug.\n const pane = process.env.TMUX_PANE;\n if (!pane) return null;\n\n // One call for ids and names together. The names are recorded on every\n // event because a reader cannot resolve a remote window id against its own\n // tmux, so they have to travel with the event.\n const fields = runTmux([\n \"display-message\",\n \"-t\",\n pane,\n \"-p\",\n \"#{session_id}\\t#{window_id}\\t#{session_name}\\t#{window_name}\",\n ]);\n const [session, window, sessionName, windowName] = fields?.split(\"\\t\") ?? [];\n if (!session || !window) return null;\n return {\n session,\n window,\n pane,\n session_name: sessionName || null,\n window_name: windowName || null,\n };\n },\n\n // Which of this host's windows still exist. Only the authoring node can\n // answer this, which is why the check runs on export rather than on the\n // reader: a peer holding a `blocked` row for a window that died has nothing\n // to supersede it, and the agent stays in every HUD forever.\n //\n // null means \"could not tell\" (no tmux server, tmux missing) and is\n // deliberately distinct from an empty set, which means \"tmux answered, and\n // there are no windows\". Treating the first as the second would clear every\n // agent on the host the moment tmux was unreachable.\n //\n // Unlike currentWindow, this deliberately asks tmux rather than reading the\n // environment, and it is right to: \"which windows exist on this host\" is a\n // server-wide question with one answer, and export runs over ssh with no\n // pane of its own. currentWindow asks \"which pane am I in\", which only\n // $TMUX_PANE can answer.\n liveWindows() {\n const out = runTmux([\"list-windows\", \"-a\", \"-F\", \"#{window_id}\"]);\n if (out === null) return null;\n return new Set(out.split(\"\\n\").filter(Boolean));\n },\n\n setState(window, state) {\n if (state === null) {\n runTmux([\"set-window-option\", \"-qu\", \"-t\", window, \"@agent_state\"]);\n } else {\n runTmux([\"set-window-option\", \"-q\", \"-t\", window, \"@agent_state\", state]);\n runTmux([\"set-window-option\", \"-q\", \"-t\", window, \"@pane_agent\", \"1\"]);\n }\n runTmux([\"refresh-client\", \"-S\"]);\n },\n\n attach(session, window) {\n // Two steps, because switch-client alone is a no-op when the target window\n // is in the session you are already attached to — which is the common case\n // for a local agent, and why \"enter\" appeared to do nothing.\n // switch-client moves the client between sessions; select-window moves\n // that session to the right window.\n //\n // Only select-window decides the result. switch-client legitimately fails\n // when there is no client to switch (running outside tmux), and treating\n // that as a failed jump would report an error for a working attach.\n runTmux([\"switch-client\", \"-t\", session]);\n return runTmux([\"select-window\", \"-t\", window]) !== null;\n },\n\n // Window ids are what the log stores, because they are stable; names are\n // what a human recognises in a picker. Names are live tmux state, not\n // history, so they are resolved at render time rather than recorded.\n windowNames() {\n const out = runTmux([\"list-windows\", \"-a\", \"-F\", \"#{window_id}\\t#{window_name}\"]);\n const names = new Map<string, string>();\n for (const line of out?.split(\"\\n\") ?? []) {\n const [id, name] = line.split(\"\\t\");\n if (id && name) names.set(id, name);\n }\n return names;\n },\n\n // First window carrying this exact name, or null. Used to reuse a per-host\n // ssh window instead of opening another one.\n // Sibling panes, for deciding whether an unowned pane may clear the window's\n // badge. A window holding an agent and a shell must not lose the badge when\n // you focus the shell.\n panesInWindow(window) {\n const out = runTmux([\"list-panes\", \"-t\", window, \"-F\", \"#{pane_id}\"]);\n return out?.split(\"\\n\").filter(Boolean) ?? [];\n },\n\n windowNamed(name) {\n const out = runTmux([\"list-windows\", \"-a\", \"-F\", \"#{window_id}\\t#{window_name}\"]);\n for (const line of out?.split(\"\\n\") ?? []) {\n const [id, windowName] = line.split(\"\\t\");\n if (id && windowName === name) return id;\n }\n return null;\n },\n\n selectWindow(window) {\n return runTmux([\"select-window\", \"-t\", window]) !== null;\n },\n\n newWindow(name, command) {\n return runTmux([\"new-window\", \"-n\", name, command]) !== null;\n },\n\n // The window a pane belongs to, for a pane murmur has no event for. Clearing\n // a badge is a tmux operation and does not require murmur to own the pane.\n windowForPane(pane) {\n return runTmux([\"display-message\", \"-t\", pane, \"-p\", \"#{window_id}\"]) || null;\n },\n\n capture(pane, lines) {\n const args = [\"capture-pane\", \"-p\", \"-t\", pane];\n if (lines !== undefined) args.push(\"-S\", `-${lines}`);\n return runTmux(args);\n },\n};\n\nexport function pidAlive(pid: number): boolean {\n try {\n process.kill(pid, 0);\n return true;\n } catch (error) {\n return (error as NodeJS.ErrnoException).code !== \"ESRCH\";\n }\n}\n","import type { Driver } from \"../types.js\";\n\nexport function endState(focused: boolean, muManaged: boolean): \"cleared\" | \"done\" {\n if (muManaged) return \"cleared\";\n return focused ? \"cleared\" : \"done\";\n}\n\nexport function driverFromEnv(env: NodeJS.ProcessEnv): Driver {\n return env.MU_MANAGED_AGENT === \"1\" || env.MU_AGENT_NAME ? \"orchestrated\" : \"human\";\n}\n"],"mappings":"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","names":["execFileSync","focused","muManaged","execFileSync"]}
|
|
1
|
+
{"version":3,"sources":["../../src/extension/murmur-pi.ts","../../src/mux.ts","../../src/ids.ts","../../src/extension/decide.ts"],"sourcesContent":["import { execFileSync } from \"node:child_process\";\nimport { tmux } from \"../mux.js\";\nimport type { Store } from \"../store.js\";\nimport type { Activity, AgentMeta, Location } from \"../types.js\";\nimport { driverFromEnv, settledState } from \"./decide.js\";\nimport type { StoreModule } from \"./store-api.js\";\n\n// Declared here rather than imported: murmur must not depend on pi to build,\n// and this is the whole surface the extension touches. getSessionName is\n// optional because an older pi does not have it, and a missing method must\n// degrade to \"no name\" rather than break the extension.\n//\n// The five events murmur needs, and no `reason` on any of them. pi puts a reason\n// on session_shutdown (\"quit\" | \"reload\" | \"new\" | \"resume\" | \"fork\"), but the\n// correct response is the same for all five: release the agent, clear the badge,\n// drop the store handle. What differs is only whether anything follows, and\n// session_start answers that by firing.\ntype ExtensionAPI = {\n on(\n event: \"agent_start\" | \"agent_end\" | \"agent_settled\" | \"session_shutdown\" | \"session_start\",\n handler: () => void | Promise<void>,\n ): void;\n getSessionName?(): string | undefined;\n};\n\n// Where to import the store from.\n//\n// The bare specifier only resolves when murmur is a dependency of the importer,\n// which it never is: the extension is loaded from ~/.pi/agent/extensions, and a\n// globally linked or installed murmur is not resolvable from there. Unpinned,\n// the import throws, getStore swallows it, and every write silently no-ops\n// while the tmux badge still paints -- so nothing looks broken while the store\n// stays empty and the node exports nothing.\n//\n// Two ways it gets pinned, because there are two install shapes:\n//\n// $MURMUR_STORE_MODULE set by the shim `murmur link pi` writes, which is a\n// re-export of THIS file from the murmur install. The\n// shim cannot rewrite this constant (it does not copy\n// the source), so it passes the path instead.\n// link pi --copy inlines this file and rewrites the string literal.\nconst storeModule = process.env.MURMUR_STORE_MODULE || \"@martintrojer/murmur/extension-store\";\nconst muManaged = process.env.MU_MANAGED_AGENT === \"1\";\nconst driver = driverFromEnv(process.env);\n\nfunction focused(pane: string): boolean {\n try {\n return (\n execFileSync(\n \"tmux\",\n [\n \"display-message\",\n \"-t\",\n pane,\n \"-p\",\n \"#{&&:#{pane_active},#{&&:#{window_active},#{session_attached}}}\",\n ],\n { encoding: \"utf8\", timeout: 3000, stdio: [\"ignore\", \"pipe\", \"ignore\"] },\n ).trim() === \"1\"\n );\n } catch {\n return false;\n }\n}\n\n// pi.getSessionName() is a live read and a session can be unnamed, so this must\n// never be the reason a report is lost.\nfunction safeSessionName(pi: ExtensionAPI): string | null {\n try {\n return pi.getSessionName?.() || null;\n } catch {\n return null;\n }\n}\n\nexport default function murmurPi(pi: ExtensionAPI): void {\n const startLocation = tmux.currentWindow();\n if (!startLocation) return;\n\n /**\n * Where this agent is NOW, not where it started.\n *\n * A pane can be moved between windows -- `move-pane`, `break-pane`, or a\n * keybinding that wraps them -- and tmux keeps the pane id while the window\n * id changes. Resolving the window once at startup meant an agent that was\n * moved painted its badge on the window it used to live in and recorded a\n * stale location on every later write.\n *\n * The pane is the address and does not change, so the agent row stays put;\n * only the location is re-read. Falls back to the startup location if tmux\n * cannot answer, which keeps a transient failure from rewriting an agent's\n * address to nothing.\n */\n let lastWindow = startLocation.window;\n const here = (): Location => {\n const location = tmux.currentWindow() ?? startLocation;\n // A move leaves the badge behind on the window the agent used to be in,\n // where nothing else will ever clear it: the badge belongs to the window,\n // and the only process that knows this agent left is this one.\n if (location.window !== lastWindow) {\n try {\n tmux.setWindowBadge(lastWindow, null);\n } catch {\n // Best effort; the new window's badge matters more than the old one's.\n }\n lastWindow = location.window;\n }\n return location;\n };\n\n const meta = (): AgentMeta => ({\n // mu names its agents; pi names its sessions. Both beat a window name when\n // present, and neither can be recovered from tmux.\n agent_name: process.env.MU_AGENT_NAME ?? null,\n pi_session: safeSessionName(pi),\n workstream: process.env.MU_WORKSTREAM ?? null,\n role: process.env.MU_ROLE ?? null,\n cli: \"pi\",\n driver,\n });\n\n /**\n * One variable, three named states, so the combinations that must not exist\n * cannot be written down.\n *\n * This was a `Store | null | undefined` plus a separate `absent` boolean --\n * six combinations for three meanings -- and conflating two of them silenced\n * the extension for the life of the process: `null` meant both \"murmur is not\n * installed, stop trying\" and \"a write failed, let go of the handle\", so one\n * transient failure latched reporting off while the tmux badge still painted.\n *\n * Only `absent` is permanent, and only a failed import, a missing identity or\n * a REFUSED claim produces it. A dropped handle returns to `untried`, so the\n * next event reopens.\n */\n type StoreState = { kind: \"untried\" } | { kind: \"open\"; store: Store } | { kind: \"absent\" };\n let state: StoreState = { kind: \"untried\" };\n /**\n * This process is nested, permanently and unrecoverably.\n *\n * Separate from `absent`, which `session_start` re-arms: a missing murmur and\n * a missing identity are both fixable from outside a running pi, but a second\n * live process in one pane never becomes the owner. Re-arming that would let a\n * nested pi start reporting as the parent agent after the first /reload.\n */\n let refused = false;\n /** This process's agent row, for the life of the process. */\n let agentId: string | null = null;\n let queue: Promise<void> = Promise.resolve();\n\n const enqueue = (work: () => Promise<void>): Promise<void> => {\n queue = queue.then(work, work);\n return queue;\n };\n\n const dropStore = (): void => {\n if (state.kind === \"open\") {\n try {\n state.store.close();\n } catch {\n // Best effort: extension failures must never reach pi.\n }\n }\n state = { kind: \"untried\" };\n };\n\n /**\n * Open the store and claim the pane, in that order, once.\n *\n * `refused` is the nested-agent case, and it is permanent for this process: a\n * pi launched inside an agent's pane inherits $TMUX_PANE and would otherwise\n * report AS the parent agent. Six pids once wrote to one pane that way and the\n * parent read as idle while it was working. The claim's liveness probe answers\n * this with the database rather than with an environment marker a process\n * launched in an unusual way could drop -- and a refused caller registers\n * nothing, paints nothing, and says nothing.\n */\n const getStore = async (): Promise<Store | null> => {\n // Permanent: murmur is not installed, this node has no identity, or this\n // process is nested. None becomes false later in the same process, so\n // retrying would pay a failed dynamic import per turn forever.\n // `session_start` re-arms it, because the first two ARE fixable from\n // outside a running pi.\n if (state.kind === \"absent\") return null;\n if (refused) return null;\n if (state.kind === \"open\") return state.store;\n try {\n const { loadIdentity, openStore } = (await import(storeModule)) as StoreModule;\n // Read, never minted: an extension load must not bring a node into\n // existence.\n if (!loadIdentity()) {\n state = { kind: \"absent\" };\n return null;\n }\n const store = openStore();\n const claim = store.claimAgent({\n location: here(),\n owner_pid: process.pid,\n meta: meta(),\n });\n if (claim.outcome === \"refused\") {\n store.close();\n refused = true;\n state = { kind: \"absent\" };\n return null;\n }\n // `retained` is what makes /reload a no-op: pi re-runs this factory in the\n // same process, and the store recognises our own pid.\n agentId = claim.agent_id;\n state = { kind: \"open\", store };\n return store;\n } catch {\n state = { kind: \"absent\" };\n return null;\n }\n };\n\n /**\n * Report activity, and answer whether this process is still the owner.\n *\n * `setActivity` returning false is not an error and is not retried: it means\n * this process is no longer the owner of record, and the correct response is\n * silence.\n *\n * The boolean is what the badge is gated on. It has to be, because the badge\n * is the only part of a report a human sees directly: painting it before the\n * write is how a silently non-reporting extension looks healthy for the life\n * of a process. A window whose agent row belongs to someone else must not\n * carry this process's glyph.\n */\n const report = async (activity: Activity, location: Location): Promise<boolean> => {\n try {\n const store = await getStore();\n if (!store || !agentId) return false;\n return store.setActivity({ agent_id: agentId, owner_pid: process.pid, activity, location });\n } catch {\n dropStore();\n return false;\n }\n };\n\n /**\n * Claim the pane NOW, not on the first event.\n *\n * A nested process must paint no badge, and the badge is painted by the same\n * handler that reports -- so ownership has to be settled before any handler\n * can run.\n *\n * ONE DEVIATION FROM THE CONTRACT, stated because it is visible: §9.1 says a\n * refused process registers no handlers. It cannot, quite. The store arrives\n * through a dynamic `import()` of a path pinned at runtime, so the claim is\n * asynchronous, and pi's extension factory is not -- handlers must be attached\n * before the first `await` resolves or the extension misses events it does own.\n *\n * The observable behaviour is identical, which is what the contract is\n * actually about: the claim goes on the queue that already serialises every\n * handler, so each handler runs after it, and a refused process writes\n * nothing, paints nothing and holds no store handle. `refused` is checked in\n * both places that could act -- the badge and the store -- rather than being\n * relied on to be checked once.\n */\n void enqueue(async () => {\n await getStore();\n });\n\n /** Paint only if we own the pane. A nested agent is deliberately invisible. */\n const badge = (location: Location, state: \"running\" | null): void => {\n if (refused) return;\n tmux.setWindowBadge(location.window, state);\n };\n\n pi.on(\"agent_start\", () => {\n void enqueue(async () => {\n const location = here();\n // Ownership first, glyph second. A process whose pane was taken over\n // while its handle was dropped learns that from the claim inside\n // `report`, and a badge painted before it would announce an agent that\n // no longer lives in this window.\n if (await report(\"running\", location)) badge(location, \"running\");\n });\n });\n\n pi.on(\"agent_end\", () => {\n void enqueue(async () => {\n const location = here();\n // Clearing is safe whatever the answer -- it retracts this process's own\n // glyph and can only ever say less -- but it is still ordered after the\n // write so that both halves read the same ownership answer.\n await report(\"stopped\", location);\n badge(location, null);\n });\n });\n\n // The event that produces `done`. agent_end alone cannot express it: agent_end\n // fires when a run's loop ends, which is not the same as \"nothing more will\n // happen\" -- pi re-enters the loop for a retry, a compaction, or a queued\n // message, and each re-entry emits its own start/end pair. Only\n // `agent_settled` means finished and waiting. See the table in decide.ts.\n pi.on(\"agent_settled\", () => {\n void enqueue(async () => {\n const location = here();\n const settled = settledState(focused(location.pane), muManaged);\n if (settled === null) return;\n try {\n const store = await getStore();\n if (!store || refused) return;\n // Attention is pane-addressed, and this call structurally cannot name an\n // agent, a pid or an activity. Completion is `done`; `blocked` is never\n // authored by an owner.\n store.requestAttention({\n kind: settled,\n location,\n message: \"\",\n source: \"pi\",\n });\n tmux.setWindowBadge(location.window, settled);\n } catch {\n dropStore();\n }\n });\n });\n\n // `session_shutdown` does not mean \"the process is exiting\". pi fires it for\n // `/reload`, and for session switch, resume and fork, then rebinds and keeps\n // going -- its own docs say to clean up here and reestablish in\n // `session_start`. Treating it as terminal killed reporting permanently on\n // the first `/reload`.\n //\n // Releasing the agent deletes the row but deliberately NOT its attention: a\n // `done` raised at settle must survive the process quitting, or completion\n // becomes invisible the moment the agent exits.\n pi.on(\"session_shutdown\", async () => {\n await enqueue(async () => {\n const location = here();\n badge(location, null);\n try {\n if (state.kind === \"open\" && agentId) {\n state.store.releaseAgent({ agent_id: agentId, owner_pid: process.pid });\n }\n } catch {\n // The handle goes either way.\n }\n agentId = null;\n dropStore();\n });\n });\n\n // Reestablish, per pi's documented contract. A reload leaves this instance\n // live but with its store dropped and its cached location possibly wrong --\n // the pane can have moved while the session was being switched.\n pi.on(\"session_start\", () => {\n void enqueue(async () => {\n if (state.kind === \"absent\") state = { kind: \"untried\" };\n const location = here();\n lastWindow = location.window;\n // Re-claim NOW, not on the next agent event.\n //\n // `session_shutdown` released the agent row, so between it and this\n // handler the pane has no owner and `claimAgent` would refuse nobody. If\n // the re-claim waited for an agent event -- which may be minutes away, or\n // never, since /reload happens while the agent is idle -- a pi started in\n // this pane in the meantime claims it legitimately, and this process is\n // then refused permanently: silent for the rest of its life while its\n // badge still paints. pi fires session_start immediately after the\n // shutdown for exactly this reestablishment, which bounds the unowned\n // window to the gap between two synchronous handler calls.\n await getStore();\n });\n });\n}\n","import { execFileSync } from \"node:child_process\";\nimport {\n asPaneId,\n asSessionId,\n asWindowId,\n type PaneId,\n type SessionId,\n type WindowId,\n} from \"./ids.js\";\nimport type { Location } from \"./types.js\";\nimport type { RenderState } from \"./view.js\";\n\nexport interface Mux {\n currentWindow(): Location | null;\n livePanes(): Set<PaneId> | null;\n // Sets `@agent_state` on a WINDOW, even though the attention it expresses\n // belongs to a pane. The asymmetry is tmux's: the status bar and the `tms`\n // picker read a window option, and there is no per-pane equivalent they\n // would read instead. Its consequence is that a pane moving between windows\n // must clear the badge it left behind, since nothing else knows it moved.\n setWindowBadge(window: WindowId, state: RenderState | null): void;\n // Reports whether the attach actually happened. runTmux swallows failures to\n // return null, and a jump that silently failed looked exactly like \"enter did\n // nothing\" -- the symptom the remote probe was added to prevent, reproduced\n // on the local path.\n attach(session: SessionId, window: WindowId): boolean;\n windowForPane(pane: PaneId): WindowId | null;\n panesInWindow(window: WindowId): PaneId[];\n capture(pane: PaneId, lines?: number): string | null;\n // --- remote-jump session seam -------------------------------------------\n // A remote attach lives in its own local session rather than a window, so it\n // can be full-screen (no local status bar) and prefix-free (no nested ^b).\n // See jumpToAgent for why that is worth five extra methods.\n clientName(): string | null;\n currentTarget(): string | null;\n sessionNamed(name: string): boolean;\n newSession(name: string, command: string): boolean;\n setSessionOption(session: string, option: string, value: string): void;\n switchClient(client: string | null, session: string): boolean;\n}\n\nfunction runTmux(args: string[]): string | null {\n try {\n return execFileSync(\"tmux\", args, {\n encoding: \"utf8\",\n timeout: 3000,\n stdio: [\"ignore\", \"pipe\", \"ignore\"],\n }).trim();\n } catch {\n return null;\n }\n}\n\n/**\n * A session name as an exact target, in the two spellings tmux needs.\n *\n * Bare names match by PREFIX, so a wrapper for host `bub` silently retargets a\n * session called `bubba` once one exists -- verified, and it sets options on\n * the wrong session rather than failing. A leading `=` demands an exact match.\n * (`name=` is not the syntax; it reads as part of the name and matches nothing.)\n *\n * The trailing colon is the part that is easy to get wrong. `switch-client -t`\n * takes a target-SESSION, where `=name` is right, but `set-option -t` and\n * `show-options -t` take a target-PANE, where `=name` fails outright with `no\n * such session` and the exact form is `=name:` -- the empty window/pane part\n * resolving to the session's current pane.\n *\n * Neither rescues a name starting with `@`, `$` or `%`: those introduce tmux's\n * window, session and pane id syntax. remoteSessionName keeps them out.\n *\n * Both take a session NAME -- not a SessionId, which is why neither is branded.\n * `exactPaneTarget` is named for what it RETURNS, a tmux target-pane, because\n * what it takes and what it produces are different things and the old name\n * `exactPane` read as though it took a pane.\n */\nexport function exactSession(session: string): string {\n return `=${session}`;\n}\n\nexport function exactPaneTarget(session: string): string {\n return `=${session}:`;\n}\n\nexport function tmuxBadgeState(state: RenderState): string {\n // @agent_state is consumed by existing tmux configuration, whose public\n // vocabulary calls active work \"working\". Keep the internal activity named\n // \"running\" without forcing a coordinated config rollout.\n return state === \"running\" ? \"working\" : state;\n}\n\nexport const tmux: Mux = {\n currentWindow() {\n // $TMUX_PANE is the only trustworthy signal that we are inside a pane, and\n // it is set by tmux for every process in one.\n //\n // Asking tmux instead does not work: `display-message` answers from any\n // process on a machine with a running server, and reports whichever pane\n // that server considers active. A pi started outside tmux -- a bare ssh\n // login, a plain terminal, cron -- would then record itself as living in\n // some unrelated agent's pane and overwrite that agent's state. Falling\n // back to `display-message` here was exactly that bug.\n const raw = process.env.TMUX_PANE;\n if (!raw) return null;\n const pane = asPaneId(raw);\n\n // One call for ids and names together. The names travel with every row a\n // snapshot carries, because a reader cannot resolve a remote session or\n // window id against its own tmux.\n const fields = runTmux([\n \"display-message\",\n \"-t\",\n pane,\n \"-p\",\n \"#{session_id}\\t#{window_id}\\t#{session_name}\\t#{window_name}\",\n ]);\n const [session, window, sessionName, windowName] = fields?.split(\"\\t\") ?? [];\n if (!session || !window) return null;\n return {\n session: asSessionId(session),\n window: asWindowId(window),\n pane,\n session_name: sessionName || null,\n window_name: windowName || null,\n };\n },\n\n // Which of this host's PANES still exist. The only liveness question tmux is\n // ever asked, and the one that matches how an agent is addressed: a pane keeps\n // its id when it moves between windows, so a recorded window id can be gone\n // while the agent is very much alive.\n //\n // null means tmux could not answer; an empty set means it did and there are\n // none. Conflating the two would delete every agent on the host the moment\n // tmux was briefly unreachable.\n livePanes() {\n const out = runTmux([\"list-panes\", \"-a\", \"-F\", \"#{pane_id}\"]);\n if (out === null) return null;\n return new Set(out.split(\"\\n\").filter(Boolean).map(asPaneId));\n },\n\n setWindowBadge(window, state) {\n if (state === null) {\n runTmux([\"set-window-option\", \"-qu\", \"-t\", window, \"@agent_state\"]);\n } else {\n runTmux([\"set-window-option\", \"-q\", \"-t\", window, \"@agent_state\", tmuxBadgeState(state)]);\n runTmux([\"set-window-option\", \"-q\", \"-t\", window, \"@pane_agent\", \"1\"]);\n }\n runTmux([\"refresh-client\", \"-S\"]);\n },\n\n attach(session, window) {\n // Two steps, because switch-client alone is a no-op when the target window\n // is in the session you are already attached to — which is the common case\n // for a local agent, and why \"enter\" appeared to do nothing.\n // switch-client moves the client between sessions; select-window moves\n // that session to the right window.\n //\n // Only select-window decides the result. switch-client legitimately fails\n // when there is no client to switch (running outside tmux), and treating\n // that as a failed jump would report an error for a working attach.\n runTmux([\"switch-client\", \"-t\", session]);\n return runTmux([\"select-window\", \"-t\", window]) !== null;\n },\n\n // Sibling panes, for deciding whether an unowned pane may clear the window's\n // badge. A window holding an agent and a shell must not lose the badge when\n // you focus the shell.\n panesInWindow(window) {\n const out = runTmux([\"list-panes\", \"-t\", window, \"-F\", \"#{pane_id}\"]);\n return out?.split(\"\\n\").filter(Boolean).map(asPaneId) ?? [];\n },\n\n // Which client to send home when the remote attach exits. `switch-client`\n // with no -c moves whichever client tmux considers current, and `murmur pick`\n // usually runs in a popup -- a client of its own, which dies with the popup.\n // Naming the real client is what lets the return outlive the picker.\n clientName() {\n return runTmux([\"display-message\", \"-p\", \"#{client_name}\"]) || null;\n },\n\n // Where the jump started, as a switch-client target. Window-level, not just\n // the session: coming back to the right session but the wrong window is\n // still the wrong place. The window id is stable where its index is not,\n // since renumber-windows renumbers on every close.\n currentTarget() {\n return runTmux([\"display-message\", \"-p\", \"#{session_name}:#{window_id}\"]) || null;\n },\n\n // Whether a wrapper session for this host already exists. Deliberately not\n // returning an id: a session is addressed by name, so a `#{session_id}` would\n // only have to be turned back into one.\n sessionNamed(name) {\n const out = runTmux([\"list-sessions\", \"-F\", \"#{session_name}\"]);\n if (out === null) return false;\n return out.split(\"\\n\").includes(name);\n },\n\n newSession(name, command) {\n // Detached, because the caller sets the per-session options before showing\n // it. Creating it attached would paint one frame with the local status bar\n // up and the local prefix live, which is the flicker this design exists to\n // remove.\n return runTmux([\"new-session\", \"-d\", \"-s\", name, command]) !== null;\n },\n\n setSessionOption(session, option, value) {\n runTmux([\"set-option\", \"-t\", exactPaneTarget(session), option, value]);\n },\n\n switchClient(client, session) {\n const target = exactSession(session);\n const args = client\n ? [\"switch-client\", \"-c\", client, \"-t\", target]\n : [\"switch-client\", \"-t\", target];\n return runTmux(args) !== null;\n },\n\n // The window a pane belongs to, for a pane murmur holds no row for. Clearing\n // a badge is a tmux operation and does not require murmur to own the pane.\n windowForPane(pane) {\n const out = runTmux([\"display-message\", \"-t\", pane, \"-p\", \"#{window_id}\"]);\n return out ? asWindowId(out) : null;\n },\n\n capture(pane, lines) {\n const args = [\"capture-pane\", \"-p\", \"-t\", pane];\n if (lines !== undefined) args.push(\"-S\", `-${lines}`);\n return runTmux(args);\n },\n};\n\nexport function pidAlive(pid: number): boolean {\n try {\n process.kill(pid, 0);\n return true;\n } catch (error) {\n return (error as NodeJS.ErrnoException).code !== \"ESRCH\";\n }\n}\n","/**\n * tmux's three id kinds, kept apart by the type system.\n *\n * tmux itself is unambiguous about this and prints a sigil on every id --\n * `session=$25 window=@75 pane=%89` -- but they are all strings, so murmur\n * could and did pass one where another was meant. Twice, in shipped code: a\n * sweep keyed on window liveness deleted ten live agents, and a window cached\n * at extension startup badged the window a moved pane had left.\n *\n * An agent is addressed by its PANE, which keeps its id across `move-pane`,\n * `break-pane`, and a window closed and reopened. A session and a window are\n * only where that pane currently lives, and both may differ between two reports\n * from one agent. So the rule the brands enforce is:\n *\n * only a pane may decide whether an agent exists.\n *\n * Branding is a compile-time fiction: at runtime these are the same strings\n * tmux printed, which is what keeps the snapshot document and every stored row\n * byte-identical.\n */\n\ndeclare const brand: unique symbol;\n\n/** A tmux session id, `$N`. Mutable location. */\nexport type SessionId = string & { readonly [brand]: \"session\" };\n\n/** A tmux window id, `@N`. Mutable location -- never an agent's identity. */\nexport type WindowId = string & { readonly [brand]: \"window\" };\n\n/** A tmux pane id, `%N`. The agent's identity, stable for its whole life. */\nexport type PaneId = string & { readonly [brand]: \"pane\" };\n\n/*\n * The boundary. Every raw string that becomes an id passes through one of these\n * three, so the unsafe step is in one file and countable rather than scattered\n * as `as` at each call site.\n *\n * Deliberately not validating the sigil. These are called on tmux stdout, on\n * JSON off the wire, on sqlite rows and on argv, and a node that recorded an id\n * murmur does not recognise -- a future tmux, a different harness -- must still\n * round-trip it. Rejecting here would turn a naming change into a behaviour\n * change.\n */\n\nexport function asSessionId(raw: string): SessionId {\n return raw as SessionId;\n}\n\nexport function asWindowId(raw: string): WindowId {\n return raw as WindowId;\n}\n\nexport function asPaneId(raw: string): PaneId {\n return raw as PaneId;\n}\n","import type { Driver } from \"../types.js\";\n\n/**\n * THE THREE-EVENT DECISION TABLE. Read this before touching either function.\n *\n * pi fires three events murmur turns into state, and it fires them in a fixed\n * order that was verified at runtime against the shipped pi (0.84.3), not read\n * off the .d.ts:\n *\n * agent_start a run begins\n * agent_end that run's loop ended\n * agent_settled no retry, compaction or queued continuation will follow\n *\n * agent_end is NOT per turn -- `turn_start`/`turn_end` are. A three-tool-call\n * prompt fires one agent_start, three turn_end, one agent_end, one settled.\n * But agent_end CAN fire more than once per settle, because pi re-enters the\n * loop for a retry, a compaction, or a message queued by an agent_end handler,\n * and each re-entry emits its own agent_start first. Observed:\n *\n * start, end, start, end, settled (a queued continuation)\n *\n * So agent_start/agent_end always pair, and settled arrives exactly once, last,\n * ~60ms after the final agent_end.\n *\n * The two axes are independent. `agent_start` and `agent_end` write ACTIVITY\n * (running / stopped) and nothing else; `agent_settled` may raise ATTENTION and\n * never touches activity. Nothing resolves one against the other, so the table\n * is short:\n *\n * pane driver agent_start agent_end agent_settled\n * -------- ------------- ----------- --------- -----------------\n * focused human running stopped (nothing)\n * unfocused human running stopped attention: done\n * focused orchestrated running stopped (nothing)\n * unfocused orchestrated running stopped (nothing)\n *\n * Why each \"nothing\":\n *\n * FOCUSED. There is nothing to request -- the user is already looking at the\n * pane. Re-asserting attention at a human who is watching is the\n * badge-that-outlives-its-cause bug.\n *\n * ORCHESTRATED. A crew agent settling is not a human's problem: mu placed the\n * work and mu consumes the result. Raising attention here would put every\n * finishing worker into the status bar and un-hide those rows in the picker.\n *\n * Completion is `done`. `blocked` is never authored by an owner -- it comes only\n * from an external notifier -- and that split is what makes attention and\n * activity genuinely independent rather than two spellings of one enum.\n */\n\n/**\n * Whether `agent_settled` raises attention, and of which kind. Null means say\n * nothing.\n *\n * `\"done\" | null` is the whole range: an owner reports that it finished, and only\n * a notifier can report that someone is wanted.\n */\nexport function settledState(focused: boolean, muManaged: boolean): \"done\" | null {\n if (muManaged) return null;\n return focused ? null : \"done\";\n}\n\nexport function driverFromEnv(env: NodeJS.ProcessEnv): Driver {\n return env.MU_MANAGED_AGENT === \"1\" || env.MU_AGENT_NAME ? \"orchestrated\" : \"human\";\n}\n"],"mappings":"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","names":["execFileSync","focused","muManaged","execFileSync","state"]}
|