@martintrojer/murmur 0.1.3 → 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 +742 -219
- package/CHANGELOG.md +172 -0
- package/README.md +136 -27
- package/dist/cli.js +1419 -869
- package/dist/cli.js.map +1 -1
- package/dist/extension/murmur-pi.js +190 -102
- package/dist/extension/murmur-pi.js.map +1 -1
- package/dist/extension/store.js +413 -190
- package/dist/extension/store.js.map +1 -1
- package/dist/index.d.ts +492 -147
- package/dist/index.js +970 -641
- package/dist/index.js.map +1 -1
- package/package.json +4 -3
package/dist/extension/store.js
CHANGED
|
@@ -11,240 +11,463 @@ function stateDir() {
|
|
|
11
11
|
return process.env.MURMUR_STATE_DIR ?? join(process.env.XDG_STATE_HOME ?? join(homedir(), ".local", "state"), "murmur");
|
|
12
12
|
}
|
|
13
13
|
function dbPath() {
|
|
14
|
-
return join(stateDir(), "
|
|
14
|
+
return join(stateDir(), "state.db");
|
|
15
15
|
}
|
|
16
16
|
|
|
17
17
|
// src/identity.ts
|
|
18
|
-
function
|
|
19
|
-
|
|
20
|
-
return existsSync(path) ? JSON.parse(readFileSync(path, "utf8")) : null;
|
|
18
|
+
function identityPath() {
|
|
19
|
+
return join2(stateDir(), "identity.json");
|
|
21
20
|
}
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
`);
|
|
21
|
+
var cache = null;
|
|
22
|
+
function loadIdentity() {
|
|
23
|
+
const path = identityPath();
|
|
24
|
+
if (cache?.path === path) return cache.identity;
|
|
25
|
+
const identity = existsSync(path) ? JSON.parse(readFileSync(path, "utf8")) : null;
|
|
26
|
+
cache = { path, identity };
|
|
29
27
|
return identity;
|
|
30
28
|
}
|
|
31
29
|
|
|
32
30
|
// src/store.ts
|
|
33
|
-
import {
|
|
31
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
32
|
+
import { mkdirSync as mkdirSync2, rmSync } from "fs";
|
|
33
|
+
import { dirname } from "path";
|
|
34
34
|
import Database from "better-sqlite3";
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
function
|
|
38
|
-
|
|
35
|
+
|
|
36
|
+
// src/ids.ts
|
|
37
|
+
function asSessionId(raw) {
|
|
38
|
+
return raw;
|
|
39
|
+
}
|
|
40
|
+
function asWindowId(raw) {
|
|
41
|
+
return raw;
|
|
42
|
+
}
|
|
43
|
+
function asPaneId(raw) {
|
|
44
|
+
return raw;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// src/mux.ts
|
|
48
|
+
import { execFileSync } from "child_process";
|
|
49
|
+
function pidAlive(pid) {
|
|
50
|
+
try {
|
|
51
|
+
process.kill(pid, 0);
|
|
52
|
+
return true;
|
|
53
|
+
} catch (error) {
|
|
54
|
+
return error.code !== "ESRCH";
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// src/version.ts
|
|
59
|
+
import { createRequire } from "module";
|
|
60
|
+
function readVersion() {
|
|
61
|
+
const require2 = createRequire(import.meta.url);
|
|
62
|
+
for (const candidate of ["../package.json", "../../package.json"]) {
|
|
63
|
+
try {
|
|
64
|
+
return require2(candidate).version;
|
|
65
|
+
} catch {
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
throw new Error("cannot locate package.json to read the murmur version");
|
|
69
|
+
}
|
|
70
|
+
var MURMUR_VERSION = readVersion();
|
|
71
|
+
|
|
72
|
+
// src/view.ts
|
|
73
|
+
var RENDER_PRIORITY = [
|
|
74
|
+
"crashed",
|
|
75
|
+
"blocked",
|
|
76
|
+
"done",
|
|
77
|
+
"running",
|
|
78
|
+
"idle"
|
|
79
|
+
];
|
|
80
|
+
var ORDER = new Map(RENDER_PRIORITY.map((state, index) => [state, index]));
|
|
81
|
+
|
|
82
|
+
// src/store.ts
|
|
83
|
+
var SCHEMA_USER_VERSION = 3;
|
|
84
|
+
var SCHEMA = `
|
|
85
|
+
CREATE TABLE agents (
|
|
86
|
+
agent_id TEXT NOT NULL PRIMARY KEY,
|
|
87
|
+
pane TEXT NOT NULL UNIQUE,
|
|
88
|
+
owner_pid INTEGER NOT NULL CHECK (owner_pid > 0),
|
|
89
|
+
activity TEXT NOT NULL CHECK (activity IN ('running', 'stopped')),
|
|
90
|
+
session TEXT NOT NULL,
|
|
91
|
+
window TEXT NOT NULL,
|
|
92
|
+
session_name TEXT,
|
|
93
|
+
window_name TEXT,
|
|
94
|
+
agent_name TEXT,
|
|
95
|
+
pi_session TEXT,
|
|
96
|
+
workstream TEXT,
|
|
97
|
+
role TEXT,
|
|
98
|
+
cli TEXT NOT NULL,
|
|
99
|
+
driver TEXT NOT NULL CHECK (driver IN ('human', 'orchestrated')),
|
|
100
|
+
claimed_at INTEGER NOT NULL,
|
|
101
|
+
updated_at INTEGER NOT NULL
|
|
102
|
+
) STRICT;
|
|
103
|
+
|
|
104
|
+
CREATE TABLE attention (
|
|
105
|
+
pane TEXT NOT NULL,
|
|
106
|
+
kind TEXT NOT NULL CHECK (kind IN ('done', 'blocked', 'crashed')),
|
|
107
|
+
message TEXT NOT NULL,
|
|
108
|
+
source TEXT NOT NULL,
|
|
109
|
+
session TEXT NOT NULL,
|
|
110
|
+
window TEXT NOT NULL,
|
|
111
|
+
session_name TEXT,
|
|
112
|
+
window_name TEXT,
|
|
113
|
+
requested_at INTEGER NOT NULL,
|
|
114
|
+
PRIMARY KEY (pane, kind)
|
|
115
|
+
) STRICT;
|
|
116
|
+
|
|
117
|
+
CREATE TABLE peers (
|
|
118
|
+
name TEXT NOT NULL PRIMARY KEY,
|
|
119
|
+
target TEXT NOT NULL,
|
|
120
|
+
host_id TEXT,
|
|
121
|
+
display_name TEXT,
|
|
122
|
+
snapshot TEXT,
|
|
123
|
+
snapshot_at INTEGER,
|
|
124
|
+
fetched_at INTEGER,
|
|
125
|
+
last_attempt_at INTEGER,
|
|
126
|
+
last_error TEXT,
|
|
127
|
+
murmur_version TEXT,
|
|
128
|
+
snapshot_version INTEGER
|
|
129
|
+
) STRICT;
|
|
130
|
+
`;
|
|
131
|
+
function salvagePeers(path) {
|
|
39
132
|
try {
|
|
40
133
|
const existing = new Database(path, { fileMustExist: true });
|
|
41
|
-
|
|
42
|
-
|
|
134
|
+
try {
|
|
135
|
+
const version = existing.pragma("user_version", { simple: true }) ?? 0;
|
|
136
|
+
if (version === SCHEMA_USER_VERSION) return [];
|
|
137
|
+
return existing.prepare("SELECT name, target FROM peers").all();
|
|
138
|
+
} catch {
|
|
139
|
+
return [];
|
|
140
|
+
} finally {
|
|
43
141
|
existing.close();
|
|
44
|
-
return salvaged;
|
|
45
142
|
}
|
|
143
|
+
} catch {
|
|
144
|
+
return [];
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
function needsReset(path) {
|
|
148
|
+
try {
|
|
149
|
+
const existing = new Database(path, { fileMustExist: true });
|
|
46
150
|
try {
|
|
47
|
-
|
|
48
|
-
}
|
|
151
|
+
return (existing.pragma("user_version", { simple: true }) ?? 0) !== SCHEMA_USER_VERSION;
|
|
152
|
+
} finally {
|
|
153
|
+
existing.close();
|
|
49
154
|
}
|
|
50
|
-
existing.close();
|
|
51
155
|
} catch {
|
|
52
|
-
return
|
|
156
|
+
return false;
|
|
53
157
|
}
|
|
54
|
-
for (const suffix of ["", "-wal", "-shm"]) rmSync(`${path}${suffix}`, { force: true });
|
|
55
|
-
return salvaged;
|
|
56
158
|
}
|
|
57
|
-
function
|
|
58
|
-
return
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
event.window,
|
|
65
|
-
event.pane,
|
|
66
|
-
event.session_name,
|
|
67
|
-
event.window_name,
|
|
68
|
-
event.agent_name,
|
|
69
|
-
event.pi_session,
|
|
70
|
-
event.workstream,
|
|
71
|
-
event.role,
|
|
72
|
-
event.cli,
|
|
73
|
-
event.driver,
|
|
74
|
-
event.kind,
|
|
75
|
-
event.state,
|
|
76
|
-
event.message,
|
|
77
|
-
event.pid,
|
|
78
|
-
Number(event.synthetic),
|
|
79
|
-
event.reason,
|
|
80
|
-
JSON.stringify(event.extra)
|
|
81
|
-
];
|
|
159
|
+
function toAttention(row) {
|
|
160
|
+
return {
|
|
161
|
+
kind: row.kind,
|
|
162
|
+
message: row.message,
|
|
163
|
+
source: row.source,
|
|
164
|
+
requested_at: row.requested_at
|
|
165
|
+
};
|
|
82
166
|
}
|
|
83
|
-
function
|
|
167
|
+
function toAgent(row) {
|
|
84
168
|
return {
|
|
85
|
-
|
|
169
|
+
agent_id: row.agent_id,
|
|
170
|
+
activity: row.activity,
|
|
171
|
+
agent_name: row.agent_name,
|
|
172
|
+
pi_session: row.pi_session,
|
|
173
|
+
workstream: row.workstream,
|
|
174
|
+
role: row.role,
|
|
175
|
+
cli: row.cli,
|
|
86
176
|
driver: row.driver,
|
|
87
|
-
|
|
88
|
-
|
|
177
|
+
claimed_at: row.claimed_at,
|
|
178
|
+
updated_at: row.updated_at
|
|
89
179
|
};
|
|
90
180
|
}
|
|
181
|
+
var PRIORITY = new Map(RENDER_PRIORITY.map((kind, index) => [kind, index]));
|
|
182
|
+
function attentionOrder(left, right) {
|
|
183
|
+
return (PRIORITY.get(left.kind) ?? 99) - (PRIORITY.get(right.kind) ?? 99);
|
|
184
|
+
}
|
|
91
185
|
function openStore() {
|
|
92
|
-
const identity = ensureIdentity();
|
|
93
186
|
const path = dbPath();
|
|
94
|
-
|
|
187
|
+
mkdirSync2(dirname(path), { recursive: true });
|
|
188
|
+
const salvaged = salvagePeers(path);
|
|
189
|
+
if (needsReset(path)) {
|
|
190
|
+
for (const suffix of ["", "-wal", "-shm"]) rmSync(`${path}${suffix}`, { force: true });
|
|
191
|
+
}
|
|
95
192
|
const database = new Database(path);
|
|
96
193
|
database.pragma("journal_mode = WAL");
|
|
97
|
-
database.pragma(
|
|
98
|
-
database.
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
session TEXT NOT NULL,
|
|
105
|
-
window TEXT NOT NULL,
|
|
106
|
-
pane TEXT NOT NULL,
|
|
107
|
-
session_name TEXT,
|
|
108
|
-
window_name TEXT,
|
|
109
|
-
agent_name TEXT,
|
|
110
|
-
pi_session TEXT,
|
|
111
|
-
workstream TEXT,
|
|
112
|
-
role TEXT,
|
|
113
|
-
cli TEXT,
|
|
114
|
-
driver TEXT,
|
|
115
|
-
kind TEXT NOT NULL,
|
|
116
|
-
state TEXT NOT NULL,
|
|
117
|
-
message TEXT NOT NULL,
|
|
118
|
-
pid INTEGER,
|
|
119
|
-
synthetic INTEGER NOT NULL,
|
|
120
|
-
reason TEXT NOT NULL,
|
|
121
|
-
extra TEXT NOT NULL,
|
|
122
|
-
PRIMARY KEY (host_id, seq)
|
|
123
|
-
);
|
|
124
|
-
CREATE INDEX IF NOT EXISTS events_agent_seq ON events (agent_id, seq);
|
|
125
|
-
CREATE TABLE IF NOT EXISTS peers (
|
|
126
|
-
name TEXT PRIMARY KEY,
|
|
127
|
-
target TEXT NOT NULL,
|
|
128
|
-
host_id TEXT,
|
|
129
|
-
display_name TEXT,
|
|
130
|
-
watermark INTEGER NOT NULL,
|
|
131
|
-
fetched_at INTEGER,
|
|
132
|
-
-- When a jump last proved this peer's tmux was not answering. Reader
|
|
133
|
-
-- state, not an event: this node cannot author facts about another
|
|
134
|
-
-- node's agents, and a jump is a local observation, not something the
|
|
135
|
-
-- peer said. Cleared by the next successful collect.
|
|
136
|
-
tmux_down_at INTEGER
|
|
137
|
-
);
|
|
138
|
-
`);
|
|
139
|
-
try {
|
|
140
|
-
database.exec("ALTER TABLE peers ADD COLUMN tmux_down_at INTEGER");
|
|
141
|
-
} catch {
|
|
142
|
-
}
|
|
143
|
-
if (salvagedPeers.length > 0) {
|
|
144
|
-
const restore = database.prepare(
|
|
145
|
-
`INSERT OR IGNORE INTO peers (name, target, host_id, display_name, watermark, fetched_at)
|
|
146
|
-
VALUES (?, ?, ?, ?, 0, NULL)`
|
|
147
|
-
);
|
|
148
|
-
for (const peer of salvagedPeers) {
|
|
149
|
-
restore.run(peer.name, peer.target, peer.host_id ?? null, peer.display_name ?? null);
|
|
150
|
-
}
|
|
194
|
+
database.pragma("busy_timeout = 5000");
|
|
195
|
+
const version = database.pragma("user_version", { simple: true }) ?? 0;
|
|
196
|
+
if (version !== SCHEMA_USER_VERSION) {
|
|
197
|
+
database.exec(SCHEMA);
|
|
198
|
+
database.pragma(`user_version = ${SCHEMA_USER_VERSION}`);
|
|
199
|
+
const restore = database.prepare("INSERT OR IGNORE INTO peers (name, target) VALUES (?, ?)");
|
|
200
|
+
for (const peer of salvaged) restore.run(peer.name, peer.target);
|
|
151
201
|
}
|
|
152
|
-
const
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
);
|
|
161
|
-
const
|
|
162
|
-
|
|
202
|
+
const selectAgentByPane = database.prepare("SELECT * FROM agents WHERE pane = ?");
|
|
203
|
+
const insertAgent = database.prepare(`
|
|
204
|
+
INSERT INTO agents (agent_id, pane, owner_pid, activity, session, window,
|
|
205
|
+
session_name, window_name, agent_name, pi_session,
|
|
206
|
+
workstream, role, cli, driver, claimed_at, updated_at)
|
|
207
|
+
VALUES (@agent_id, @pane, @owner_pid, @activity, @session, @window,
|
|
208
|
+
@session_name, @window_name, @agent_name, @pi_session,
|
|
209
|
+
@workstream, @role, @cli, @driver, @claimed_at, @updated_at)
|
|
210
|
+
`);
|
|
211
|
+
const retainAgent = database.prepare(`
|
|
212
|
+
UPDATE agents
|
|
213
|
+
SET session = @session, window = @window, session_name = @session_name,
|
|
214
|
+
window_name = @window_name, agent_name = @agent_name,
|
|
215
|
+
pi_session = @pi_session, workstream = @workstream, role = @role,
|
|
216
|
+
cli = @cli, driver = @driver, updated_at = @updated_at
|
|
217
|
+
WHERE agent_id = @agent_id
|
|
218
|
+
`);
|
|
219
|
+
const deleteAgentByPane = database.prepare("DELETE FROM agents WHERE pane = ?");
|
|
220
|
+
const deleteAttentionForPane = database.prepare("DELETE FROM attention WHERE pane = ?");
|
|
221
|
+
const updateActivity = database.prepare(`
|
|
222
|
+
UPDATE agents
|
|
223
|
+
SET activity = @activity, session = @session, window = @window,
|
|
224
|
+
session_name = @session_name, window_name = @window_name,
|
|
225
|
+
updated_at = @updated_at
|
|
226
|
+
WHERE agent_id = @agent_id AND owner_pid = @owner_pid
|
|
227
|
+
`);
|
|
228
|
+
const deleteAgentOwned = database.prepare(
|
|
229
|
+
"DELETE FROM agents WHERE agent_id = ? AND owner_pid = ?"
|
|
163
230
|
);
|
|
164
|
-
const
|
|
165
|
-
|
|
231
|
+
const upsertAttention = database.prepare(`
|
|
232
|
+
INSERT INTO attention (pane, kind, message, source, session, window,
|
|
233
|
+
session_name, window_name, requested_at)
|
|
234
|
+
VALUES (@pane, @kind, @message, @source, @session, @window,
|
|
235
|
+
@session_name, @window_name, @requested_at)
|
|
236
|
+
ON CONFLICT (pane, kind) DO UPDATE SET
|
|
237
|
+
message = excluded.message,
|
|
238
|
+
source = excluded.source,
|
|
239
|
+
session = excluded.session,
|
|
240
|
+
window = excluded.window,
|
|
241
|
+
session_name = excluded.session_name,
|
|
242
|
+
window_name = excluded.window_name
|
|
243
|
+
`);
|
|
244
|
+
const selectAgents = database.prepare("SELECT * FROM agents");
|
|
245
|
+
const selectAttention = database.prepare("SELECT * FROM attention");
|
|
246
|
+
const setActivityByPane = database.prepare(
|
|
247
|
+
"UPDATE agents SET activity = ?, updated_at = ? WHERE pane = ?"
|
|
166
248
|
);
|
|
167
|
-
const
|
|
168
|
-
const
|
|
169
|
-
const
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
249
|
+
const claimAgent = database.transaction((claim) => {
|
|
250
|
+
const now = claim.now ?? Date.now();
|
|
251
|
+
const isAlive = claim.isAlive ?? pidAlive;
|
|
252
|
+
const { location, meta, owner_pid } = claim;
|
|
253
|
+
const incumbent = selectAgentByPane.get(location.pane);
|
|
254
|
+
const values = {
|
|
255
|
+
pane: location.pane,
|
|
256
|
+
owner_pid,
|
|
257
|
+
session: location.session,
|
|
258
|
+
window: location.window,
|
|
259
|
+
session_name: location.session_name,
|
|
260
|
+
window_name: location.window_name,
|
|
261
|
+
agent_name: meta.agent_name,
|
|
262
|
+
pi_session: meta.pi_session,
|
|
263
|
+
workstream: meta.workstream,
|
|
264
|
+
role: meta.role,
|
|
265
|
+
cli: meta.cli,
|
|
266
|
+
driver: meta.driver,
|
|
267
|
+
updated_at: now
|
|
178
268
|
};
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
269
|
+
if (!incumbent) {
|
|
270
|
+
const agentId2 = randomUUID2();
|
|
271
|
+
insertAgent.run({ ...values, agent_id: agentId2, activity: "stopped", claimed_at: now });
|
|
272
|
+
return { outcome: "claimed", agent_id: agentId2 };
|
|
273
|
+
}
|
|
274
|
+
if (incumbent.owner_pid === owner_pid) {
|
|
275
|
+
retainAgent.run({ ...values, agent_id: incumbent.agent_id });
|
|
276
|
+
return { outcome: "retained", agent_id: incumbent.agent_id };
|
|
277
|
+
}
|
|
278
|
+
if (isAlive(incumbent.owner_pid)) {
|
|
279
|
+
return { outcome: "refused", held_by_pid: incumbent.owner_pid };
|
|
280
|
+
}
|
|
281
|
+
deleteAgentByPane.run(location.pane);
|
|
282
|
+
deleteAttentionForPane.run(location.pane);
|
|
283
|
+
const agentId = randomUUID2();
|
|
284
|
+
insertAgent.run({ ...values, agent_id: agentId, activity: "stopped", claimed_at: now });
|
|
285
|
+
return { outcome: "replaced", agent_id: agentId, previous_agent_id: incumbent.agent_id };
|
|
286
|
+
}).immediate;
|
|
287
|
+
const reconcileLocal = database.transaction((world) => {
|
|
288
|
+
const summary = { crashed: [], removed: [], attention_removed: [] };
|
|
289
|
+
if (world.panes === null) return summary;
|
|
290
|
+
const live = world.panes;
|
|
291
|
+
const isAlive = world.isAlive ?? pidAlive;
|
|
292
|
+
const now = world.now ?? Date.now();
|
|
293
|
+
const alreadyCrashed = new Set(
|
|
294
|
+
selectAttention.all().filter((row) => row.kind === "crashed").map((row) => row.pane)
|
|
295
|
+
);
|
|
296
|
+
for (const row of selectAgents.all()) {
|
|
297
|
+
const pane = asPaneId(row.pane);
|
|
298
|
+
if (!live.has(pane)) {
|
|
299
|
+
deleteAgentByPane.run(row.pane);
|
|
300
|
+
deleteAttentionForPane.run(row.pane);
|
|
301
|
+
summary.removed.push(pane);
|
|
302
|
+
continue;
|
|
303
|
+
}
|
|
304
|
+
if (isAlive(row.owner_pid)) continue;
|
|
305
|
+
if (row.activity === "running") {
|
|
306
|
+
setActivityByPane.run("stopped", now, row.pane);
|
|
307
|
+
upsertAttention.run({
|
|
308
|
+
pane: row.pane,
|
|
309
|
+
kind: "crashed",
|
|
310
|
+
message: "",
|
|
311
|
+
source: "murmur",
|
|
312
|
+
session: row.session,
|
|
313
|
+
window: row.window,
|
|
314
|
+
session_name: row.session_name,
|
|
315
|
+
window_name: row.window_name,
|
|
316
|
+
requested_at: now
|
|
317
|
+
});
|
|
318
|
+
summary.crashed.push(pane);
|
|
319
|
+
} else if (!alreadyCrashed.has(row.pane)) {
|
|
320
|
+
deleteAgentByPane.run(row.pane);
|
|
321
|
+
summary.removed.push(pane);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
for (const row of selectAttention.all()) {
|
|
325
|
+
const pane = asPaneId(row.pane);
|
|
326
|
+
if (live.has(pane)) continue;
|
|
327
|
+
deleteAttentionForPane.run(row.pane);
|
|
328
|
+
if (!summary.attention_removed.includes(pane)) summary.attention_removed.push(pane);
|
|
329
|
+
}
|
|
330
|
+
return summary;
|
|
331
|
+
}).immediate;
|
|
332
|
+
const readLocalPanes = database.transaction(() => {
|
|
333
|
+
const agents = selectAgents.all();
|
|
334
|
+
const attention = selectAttention.all();
|
|
335
|
+
const panes = /* @__PURE__ */ new Map();
|
|
336
|
+
const locate = (row) => {
|
|
337
|
+
const existing = panes.get(row.pane);
|
|
338
|
+
if (existing) return existing;
|
|
339
|
+
const created = {
|
|
340
|
+
pane: asPaneId(row.pane),
|
|
341
|
+
session: asSessionId(row.session),
|
|
342
|
+
window: asWindowId(row.window),
|
|
343
|
+
session_name: row.session_name,
|
|
344
|
+
window_name: row.window_name,
|
|
345
|
+
agent: null,
|
|
346
|
+
attention: []
|
|
347
|
+
};
|
|
348
|
+
panes.set(row.pane, created);
|
|
349
|
+
return created;
|
|
350
|
+
};
|
|
351
|
+
for (const row of agents) locate(row).agent = toAgent(row);
|
|
352
|
+
for (const row of attention) locate(row).attention.push(toAttention(row));
|
|
353
|
+
for (const pane of panes.values()) pane.attention.sort(attentionOrder);
|
|
354
|
+
return [...panes.values()].sort((left, right) => left.pane.localeCompare(right.pane));
|
|
186
355
|
});
|
|
356
|
+
function peerRecord(row) {
|
|
357
|
+
let snapshot = null;
|
|
358
|
+
if (row.snapshot !== null) {
|
|
359
|
+
try {
|
|
360
|
+
snapshot = JSON.parse(row.snapshot);
|
|
361
|
+
} catch {
|
|
362
|
+
snapshot = null;
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
return {
|
|
366
|
+
name: row.name,
|
|
367
|
+
target: row.target,
|
|
368
|
+
host_id: row.host_id,
|
|
369
|
+
display_name: row.display_name,
|
|
370
|
+
snapshot,
|
|
371
|
+
snapshot_at: row.snapshot_at,
|
|
372
|
+
fetched_at: row.fetched_at,
|
|
373
|
+
last_attempt_at: row.last_attempt_at,
|
|
374
|
+
last_error: row.last_error,
|
|
375
|
+
murmur_version: row.murmur_version,
|
|
376
|
+
snapshot_version: row.snapshot_version
|
|
377
|
+
};
|
|
378
|
+
}
|
|
187
379
|
return {
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
380
|
+
claimAgent,
|
|
381
|
+
reconcileLocal,
|
|
382
|
+
setActivity(update) {
|
|
383
|
+
return updateActivity.run({
|
|
384
|
+
activity: update.activity,
|
|
385
|
+
session: update.location.session,
|
|
386
|
+
window: update.location.window,
|
|
387
|
+
session_name: update.location.session_name,
|
|
388
|
+
window_name: update.location.window_name,
|
|
389
|
+
updated_at: update.now ?? Date.now(),
|
|
390
|
+
agent_id: update.agent_id,
|
|
391
|
+
owner_pid: update.owner_pid
|
|
392
|
+
}).changes === 1;
|
|
193
393
|
},
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
return rows.map(toEvent);
|
|
394
|
+
releaseAgent(release) {
|
|
395
|
+
return deleteAgentOwned.run(release.agent_id, release.owner_pid).changes === 1;
|
|
197
396
|
},
|
|
198
|
-
|
|
199
|
-
|
|
397
|
+
requestAttention(request) {
|
|
398
|
+
upsertAttention.run({
|
|
399
|
+
pane: request.location.pane,
|
|
400
|
+
kind: request.kind,
|
|
401
|
+
message: request.message,
|
|
402
|
+
source: request.source,
|
|
403
|
+
session: request.location.session,
|
|
404
|
+
window: request.location.window,
|
|
405
|
+
session_name: request.location.session_name,
|
|
406
|
+
window_name: request.location.window_name,
|
|
407
|
+
requested_at: request.now ?? Date.now()
|
|
408
|
+
});
|
|
200
409
|
},
|
|
201
|
-
|
|
202
|
-
return
|
|
203
|
-
DELETE FROM events
|
|
204
|
-
WHERE ts < ?
|
|
205
|
-
AND (host_id, seq) NOT IN (
|
|
206
|
-
SELECT host_id, seq FROM (
|
|
207
|
-
SELECT host_id, seq,
|
|
208
|
-
ROW_NUMBER() OVER (PARTITION BY agent_id ORDER BY ts DESC, seq DESC) rn
|
|
209
|
-
FROM events
|
|
210
|
-
) WHERE rn = 1
|
|
211
|
-
)
|
|
212
|
-
`).run(Date.now() - horizonMs).changes;
|
|
213
|
-
},
|
|
214
|
-
peers() {
|
|
215
|
-
return database.prepare("SELECT * FROM peers ORDER BY name").all();
|
|
410
|
+
acknowledgePane(pane) {
|
|
411
|
+
return deleteAttentionForPane.run(pane).changes;
|
|
216
412
|
},
|
|
217
|
-
|
|
218
|
-
return
|
|
413
|
+
localPanes() {
|
|
414
|
+
return readLocalPanes();
|
|
219
415
|
},
|
|
220
|
-
|
|
221
|
-
|
|
416
|
+
buildLocalSnapshot(identity, world) {
|
|
417
|
+
reconcileLocal(world);
|
|
418
|
+
return {
|
|
419
|
+
murmur_snapshot: 1,
|
|
420
|
+
host_id: identity.host_id,
|
|
421
|
+
display_name: identity.display_name,
|
|
422
|
+
murmur_version: MURMUR_VERSION,
|
|
423
|
+
generated_at: world.now ?? Date.now(),
|
|
424
|
+
// Rule 3: a pane with no agent and no attention must not be published.
|
|
425
|
+
// A no-op against today's `readLocalPanes`, which builds a pane entry
|
|
426
|
+
// only from a row and so cannot produce an empty one -- kept because the
|
|
427
|
+
// rule belongs to the DOCUMENT, and the validator rejects such an entry
|
|
428
|
+
// outright. Without it, one narrowing of the local read would make this
|
|
429
|
+
// node reachable-but-broken on every peer that collects it, and the
|
|
430
|
+
// symptom would show up on the other machines.
|
|
431
|
+
panes: readLocalPanes().filter((pane) => pane.agent !== null || pane.attention.length > 0)
|
|
432
|
+
};
|
|
222
433
|
},
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
INSERT INTO peers (name, target, host_id, display_name, watermark, fetched_at, tmux_down_at)
|
|
227
|
-
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
228
|
-
ON CONFLICT(name) DO UPDATE SET
|
|
229
|
-
target = excluded.target,
|
|
230
|
-
host_id = excluded.host_id,
|
|
231
|
-
display_name = excluded.display_name,
|
|
232
|
-
watermark = excluded.watermark,
|
|
233
|
-
fetched_at = excluded.fetched_at,
|
|
234
|
-
tmux_down_at = excluded.tmux_down_at
|
|
235
|
-
`).run(
|
|
236
|
-
peer.name,
|
|
237
|
-
peer.target,
|
|
238
|
-
peer.host_id !== void 0 ? peer.host_id : current?.host_id ?? null,
|
|
239
|
-
peer.display_name !== void 0 ? peer.display_name : current?.display_name ?? null,
|
|
240
|
-
peer.watermark !== void 0 ? peer.watermark : current?.watermark ?? 0,
|
|
241
|
-
peer.fetched_at !== void 0 ? peer.fetched_at : current?.fetched_at ?? null,
|
|
242
|
-
peer.tmux_down_at !== void 0 ? peer.tmux_down_at : current?.tmux_down_at ?? null
|
|
434
|
+
peers() {
|
|
435
|
+
return database.prepare("SELECT * FROM peers ORDER BY name").all().map(
|
|
436
|
+
peerRecord
|
|
243
437
|
);
|
|
244
438
|
},
|
|
439
|
+
addPeer(name, target) {
|
|
440
|
+
database.prepare(
|
|
441
|
+
`INSERT INTO peers (name, target) VALUES (?, ?)
|
|
442
|
+
ON CONFLICT(name) DO UPDATE SET target = excluded.target`
|
|
443
|
+
).run(name, target);
|
|
444
|
+
},
|
|
245
445
|
removePeer(name) {
|
|
246
446
|
return database.prepare("DELETE FROM peers WHERE name = ?").run(name).changes > 0;
|
|
247
447
|
},
|
|
448
|
+
replacePeerSnapshot(name, fetch) {
|
|
449
|
+
if (!fetch.ok) {
|
|
450
|
+
database.prepare("UPDATE peers SET last_attempt_at = ?, last_error = ? WHERE name = ?").run(fetch.at, fetch.error, name);
|
|
451
|
+
return;
|
|
452
|
+
}
|
|
453
|
+
database.prepare(
|
|
454
|
+
`UPDATE peers
|
|
455
|
+
SET snapshot = ?, snapshot_at = ?, fetched_at = ?, last_attempt_at = ?,
|
|
456
|
+
last_error = NULL, host_id = ?, display_name = ?,
|
|
457
|
+
murmur_version = ?, snapshot_version = ?
|
|
458
|
+
WHERE name = ?`
|
|
459
|
+
).run(
|
|
460
|
+
JSON.stringify(fetch.snapshot),
|
|
461
|
+
fetch.snapshot.generated_at,
|
|
462
|
+
fetch.at,
|
|
463
|
+
fetch.at,
|
|
464
|
+
fetch.snapshot.host_id,
|
|
465
|
+
fetch.snapshot.display_name,
|
|
466
|
+
fetch.snapshot.murmur_version,
|
|
467
|
+
fetch.snapshot.murmur_snapshot,
|
|
468
|
+
name
|
|
469
|
+
);
|
|
470
|
+
},
|
|
248
471
|
close() {
|
|
249
472
|
database.close();
|
|
250
473
|
}
|