@adrrr/tarmac 0.2.0 → 0.4.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/README.md +130 -55
- package/dist/fleet.js +45 -1
- package/dist/history.js +88 -0
- package/dist/install.js +44 -12
- package/dist/limits.js +62 -0
- package/dist/map.js +119 -0
- package/dist/reap.js +9 -4
- package/dist/render.js +851 -33
- package/dist/schema.js +1 -1
- package/dist/server.js +85 -4
- package/dist/sessions.js +36 -2
- package/dist/snapshots.js +19 -2
- package/dist/wrapper.js +101 -19
- package/package.json +2 -2
package/dist/map.js
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
// P4 — the map's model.
|
|
2
|
+
//
|
|
3
|
+
// A view over the fleet `buildFleet` already produced. It opens no second source: every
|
|
4
|
+
// field below is derived from a row that is already on the page as a table line.
|
|
5
|
+
import { isWaiting } from './sessions.js';
|
|
6
|
+
/**
|
|
7
|
+
* How recently a snapshot must have landed for its node to pulse. Two of the page's poll
|
|
8
|
+
* intervals (5s): a session whose terminal keeps drawing frames keeps its heartbeat across
|
|
9
|
+
* consecutive renders, and one that has stopped goes quiet within two of them.
|
|
10
|
+
*
|
|
11
|
+
* It is a display window, not a health threshold — `--stale-after` is the one that judges,
|
|
12
|
+
* and it wins wherever the two disagree.
|
|
13
|
+
*/
|
|
14
|
+
export const PULSE_WITHIN_MS = 10_000;
|
|
15
|
+
/**
|
|
16
|
+
* Where an agent is placed, and why it is a placement rather than a link.
|
|
17
|
+
*
|
|
18
|
+
* `claude agents --json` prints interactive and background sessions in one array, and
|
|
19
|
+
* publishes nothing that ties an agent to whoever dispatched it. The working directory is
|
|
20
|
+
* the only field both carry, so it is what an agent is placed BY — it lands next to the
|
|
21
|
+
* session sharing its directory, and nothing is ever nested inside anything. Nesting would
|
|
22
|
+
* assert a parentage the source does not contain, and it would let this page show a smaller
|
|
23
|
+
* fleet than the table beside it.
|
|
24
|
+
*
|
|
25
|
+
* The agents are gathered separately because the fleet sorts busy sessions first, so one can
|
|
26
|
+
* arrive before the session it belongs beside. An agent whose directory matches no session
|
|
27
|
+
* keeps a node of its own, at the end.
|
|
28
|
+
*/
|
|
29
|
+
export function buildMap({ rows }, { pulseWithinMs = PULSE_WITHIN_MS } = {}) {
|
|
30
|
+
// Whether this fleet still speaks the kind we know. If NOTHING calls itself `interactive`,
|
|
31
|
+
// the word moved rather than every terminal on the machine going background at once — and
|
|
32
|
+
// the map says so by drawing them all as what they almost certainly still are. It is the
|
|
33
|
+
// tolerance `buildFleet` already applies to telemetry: a signal true of every row is a
|
|
34
|
+
// change in the source.
|
|
35
|
+
const anchored = rows.some((r) => r.kind === INTERACTIVE);
|
|
36
|
+
const roleOf = (r) => !anchored || r.kind === null || r.kind === INTERACTIVE ? 'session' : 'agent';
|
|
37
|
+
const node = (row) => {
|
|
38
|
+
const reading = readingOf(row);
|
|
39
|
+
return {
|
|
40
|
+
row,
|
|
41
|
+
role: roleOf(row),
|
|
42
|
+
state: stateOf(row),
|
|
43
|
+
reading,
|
|
44
|
+
measured: row.ctxPct !== null,
|
|
45
|
+
// Three conditions, and each one is a way the halo could otherwise lie. `live` first,
|
|
46
|
+
// and not merely "young": a reading the fleet calls stale may not be animated as
|
|
47
|
+
// though it were breathing, and with `--stale-after 2s` a three-second-old reading is
|
|
48
|
+
// both stale and inside the window below. `measured` last: a file landing is not a
|
|
49
|
+
// reading landing, and a drifted fleet still writes a snapshot every frame.
|
|
50
|
+
pulse: reading === 'live' &&
|
|
51
|
+
row.ctxPct !== null &&
|
|
52
|
+
row.snapshotAgeMs !== null &&
|
|
53
|
+
row.snapshotAgeMs <= pulseWithinMs,
|
|
54
|
+
};
|
|
55
|
+
};
|
|
56
|
+
const agents = rows.filter((r) => roleOf(r) === 'agent');
|
|
57
|
+
const placed = new Set();
|
|
58
|
+
const seen = new Set();
|
|
59
|
+
const nodes = [];
|
|
60
|
+
for (const r of rows) {
|
|
61
|
+
if (roleOf(r) !== 'session')
|
|
62
|
+
continue;
|
|
63
|
+
nodes.push(node(r));
|
|
64
|
+
// Only the first session of a directory collects them, or two sessions in one checkout
|
|
65
|
+
// would each grow a copy of the same agents.
|
|
66
|
+
if (r.cwd === null || seen.has(r.cwd))
|
|
67
|
+
continue;
|
|
68
|
+
seen.add(r.cwd);
|
|
69
|
+
for (const a of agents) {
|
|
70
|
+
// Two directories nobody could read are not the same directory.
|
|
71
|
+
if (a.cwd === null || a.cwd !== r.cwd)
|
|
72
|
+
continue;
|
|
73
|
+
nodes.push(node(a));
|
|
74
|
+
placed.add(a);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
for (const a of agents)
|
|
78
|
+
if (!placed.has(a))
|
|
79
|
+
nodes.push(node(a));
|
|
80
|
+
return { nodes };
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* The kind a terminal calls itself, and the anchor this module reasons from. A background
|
|
84
|
+
* entry has since been seen beside them — `kind: 'background'`, no `pid`, its word under
|
|
85
|
+
* `state` rather than `status` — so the two are no longer a reading of that CLI's help. It is
|
|
86
|
+
* still the anchor and never the list: one observed alternative is not the vocabulary, and the
|
|
87
|
+
* heuristic above asks only whether anything on this machine still calls itself `interactive`.
|
|
88
|
+
*
|
|
89
|
+
* An ABSENT kind is not evidence of an agent either: the same rule the session status follows
|
|
90
|
+
* one module down, where unrecognised means unknown, never "the quiet one". The two mistakes
|
|
91
|
+
* are not the same size — an agent drawn as a session is a node in the wrong shape, while a
|
|
92
|
+
* session drawn as an agent is a terminal someone is working in, reduced to a footnote of a
|
|
93
|
+
* directory it merely shares.
|
|
94
|
+
*/
|
|
95
|
+
export const INTERACTIVE = 'interactive';
|
|
96
|
+
/**
|
|
97
|
+
* `stale` is not recomputed here — it is the collector's verdict, reached against the
|
|
98
|
+
* threshold this run resolved (`--stale-after`, the environment, the config file). A second
|
|
99
|
+
* opinion in this module would let the map and the table disagree about the same session on
|
|
100
|
+
* the same page.
|
|
101
|
+
*/
|
|
102
|
+
function readingOf(r) {
|
|
103
|
+
if (r.snapshotAgeMs === null)
|
|
104
|
+
return 'none';
|
|
105
|
+
// A snapshot dated after the clock reading it: an NTP correction, a mount whose time runs
|
|
106
|
+
// ahead. Its age is not a small number, it is not a number at all.
|
|
107
|
+
if (r.snapshotAgeMs < 0)
|
|
108
|
+
return 'undated';
|
|
109
|
+
return r.stale ? 'stale' : 'live';
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* The four words a node can be, out of a boolean that only has three answers.
|
|
113
|
+
*
|
|
114
|
+
* `waiting` is asked BEFORE the boolean's `null`, and that order is the point: a session
|
|
115
|
+
* halted until a human answers carries `busy: null` — "is it working" has no honest answer —
|
|
116
|
+
* which is the same null an unrecognised word gets. Left to the boolean alone, the one
|
|
117
|
+
* session that is blocked on YOU drew as the amber "tarmac does not know this word".
|
|
118
|
+
*/
|
|
119
|
+
export const stateOf = (r) => r.busy === true ? 'busy' : isWaiting(r) ? 'waiting' : r.busy === false ? 'idle' : 'unknown';
|
package/dist/reap.js
CHANGED
|
@@ -16,12 +16,17 @@
|
|
|
16
16
|
// in flight, and deleting it would be the reaper causing the corruption it prevents.
|
|
17
17
|
import fs from 'node:fs';
|
|
18
18
|
import path from 'node:path';
|
|
19
|
-
import { TEMP_PREFIX } from './wrapper.js';
|
|
19
|
+
import { SID_GLOB, TEMP_PREFIX } from './wrapper.js';
|
|
20
20
|
/** Exported so a test can build the same expectation from the same constant, escaped. */
|
|
21
21
|
export const escapeRe = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
22
|
-
// `<TEMP_PREFIX><sid>.<pid>.tmp` — the
|
|
23
|
-
//
|
|
24
|
-
|
|
22
|
+
// `<TEMP_PREFIX><sid>.<pid>.tmp` — the pid is what `$$` emits, and the sid is the wrapper's
|
|
23
|
+
// own rule, read from the constant rather than transcribed: a set of its own is how this
|
|
24
|
+
// matcher came to accept names the writer had stopped producing (#7).
|
|
25
|
+
//
|
|
26
|
+
// `SID_GLOB` goes in RAW, unlike the prefix: it is a shell pattern made of bracket
|
|
27
|
+
// expressions and literal `-`, which is already valid regex meaning the same set. Escaping
|
|
28
|
+
// it would turn the classes into literal brackets and match nothing at all.
|
|
29
|
+
const TEMP_NAME = new RegExp(`^${escapeRe(TEMP_PREFIX)}${SID_GLOB}\\.\\d+\\.tmp$`);
|
|
25
30
|
/** An hour is orders of magnitude beyond any real frame, and cheap to be wrong about. */
|
|
26
31
|
const DEFAULT_OLDER_THAN_MS = 60 * 60_000;
|
|
27
32
|
/**
|