@phnx-labs/agents-cli 1.22.29 → 1.22.31
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/CHANGELOG.md +88 -0
- package/README.md +44 -5
- package/dist/bin/agents +0 -0
- package/dist/commands/accounts.d.ts +13 -0
- package/dist/commands/accounts.js +32 -0
- package/dist/commands/daemon.d.ts +18 -0
- package/dist/commands/daemon.js +581 -0
- package/dist/commands/exec.js +66 -20
- package/dist/commands/focus.d.ts +4 -1
- package/dist/commands/focus.js +19 -4
- package/dist/commands/routines.js +29 -11
- package/dist/commands/secrets.d.ts +37 -0
- package/dist/commands/secrets.js +86 -105
- package/dist/commands/sessions-bookmark.d.ts +20 -0
- package/dist/commands/{sessions-favorite.js → sessions-bookmark.js} +42 -42
- package/dist/commands/sessions-browser.d.ts +10 -8
- package/dist/commands/sessions-browser.js +61 -32
- package/dist/commands/sessions-picker.d.ts +33 -1
- package/dist/commands/sessions-picker.js +102 -27
- package/dist/commands/sessions-stats.js +1 -1
- package/dist/commands/sessions.d.ts +21 -8
- package/dist/commands/sessions.js +328 -74
- package/dist/commands/view.d.ts +11 -0
- package/dist/commands/view.js +56 -29
- package/dist/index.js +37 -2
- package/dist/lib/account-labels.d.ts +24 -0
- package/dist/lib/account-labels.js +72 -0
- package/dist/lib/agents.d.ts +32 -1
- package/dist/lib/agents.js +96 -31
- package/dist/lib/daemon-health.d.ts +24 -0
- package/dist/lib/daemon-health.js +84 -0
- package/dist/lib/daemon-ticks.d.ts +81 -0
- package/dist/lib/daemon-ticks.js +190 -0
- package/dist/lib/daemon.d.ts +68 -18
- package/dist/lib/daemon.js +303 -338
- package/dist/lib/device-config.d.ts +10 -0
- package/dist/lib/device-config.js +27 -0
- package/dist/lib/exec.d.ts +27 -0
- package/dist/lib/exec.js +49 -2
- package/dist/lib/hosts/dispatch.d.ts +4 -0
- package/dist/lib/hosts/dispatch.js +4 -0
- package/dist/lib/hosts/remote-cmd.js +1 -0
- package/dist/lib/hosts/run-target.d.ts +1 -0
- package/dist/lib/hosts/run-target.js +1 -0
- package/dist/lib/import.js +7 -6
- package/dist/lib/memory-cache.d.ts +19 -0
- package/dist/lib/memory-cache.js +31 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/CodeResources +0 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
- package/dist/lib/migrate.d.ts +1 -1
- package/dist/lib/migrate.js +13 -2
- package/dist/lib/picker.d.ts +6 -3
- package/dist/lib/picker.js +7 -2
- package/dist/lib/routine-activation.d.ts +2 -0
- package/dist/lib/routine-activation.js +16 -0
- package/dist/lib/runner.d.ts +18 -0
- package/dist/lib/runner.js +52 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
- package/dist/lib/secrets/agent.d.ts +19 -1
- package/dist/lib/secrets/agent.js +32 -6
- package/dist/lib/secrets/scope.d.ts +3 -3
- package/dist/lib/secrets/scope.js +3 -3
- package/dist/lib/secrets/session-store.d.ts +0 -4
- package/dist/lib/secrets/session-store.js +0 -5
- package/dist/lib/session/{favorites.d.ts → bookmarks.d.ts} +15 -15
- package/dist/lib/session/{favorites.js → bookmarks.js} +23 -23
- package/dist/lib/session/db.d.ts +15 -0
- package/dist/lib/session/db.js +90 -15
- package/dist/lib/session/discover.js +91 -39
- package/dist/lib/session/parse.d.ts +63 -0
- package/dist/lib/session/parse.js +165 -20
- package/dist/lib/session/session-cache.d.ts +9 -6
- package/dist/lib/session/session-cache.js +23 -6
- package/dist/lib/shims.js +12 -0
- package/dist/lib/startup/command-registry.d.ts +15 -1
- package/dist/lib/startup/command-registry.js +49 -0
- package/dist/lib/usage-refresh.js +3 -2
- package/dist/lib/usage.d.ts +12 -10
- package/dist/lib/usage.js +63 -144
- package/package.json +4 -1
- package/dist/commands/sessions-favorite.d.ts +0 -20
|
@@ -142,6 +142,27 @@ export function shouldSelfHealForUpgrade(persistent, storeSize, runningVersion,
|
|
|
142
142
|
export function shouldTeardownVersionSkewedBroker(realHeldBundles) {
|
|
143
143
|
return realHeldBundles === 0;
|
|
144
144
|
}
|
|
145
|
+
/**
|
|
146
|
+
* Whether a version-skewed client may evict the reachable broker at all. The
|
|
147
|
+
* held-bundle gate above is necessary but not sufficient: a broker the always-on
|
|
148
|
+
* daemon is hosting must NEVER be client-evicted, even when it holds zero
|
|
149
|
+
* unlocks. teardownStaleBroker() recognizes only the standalone broker's
|
|
150
|
+
* pidPath() O_EXCL claim (the daemon writes ownerPath(), never pidPath()), so
|
|
151
|
+
* evicting a daemon-hosted broker unlinks its socket WITHOUT stopping the daemon;
|
|
152
|
+
* the daemon then keeps hostedBroker != null and shouldTakeOverBroker() refuses
|
|
153
|
+
* to re-host, orphaning its broker until the daemon restarts while every reader
|
|
154
|
+
* falls onto cold one-off brokers that re-prompt Touch ID — the storm. Deferring
|
|
155
|
+
* is safe: daemon code-version upgrades are handled by postinstall.js restarting
|
|
156
|
+
* it, and agentPing() already gated on PROTOCOL_VERSION, so a code-skewed daemon
|
|
157
|
+
* broker is still wire-compatible. Only when NO daemon owns the broker (churning
|
|
158
|
+
* dev installs with a dead/absent daemon — the case #435's client twin was built
|
|
159
|
+
* for) does the zero-held-bundles teardown apply, exactly as before.
|
|
160
|
+
*/
|
|
161
|
+
export function shouldClientEvictSkewedBroker(daemonRunning, realHeldBundles) {
|
|
162
|
+
if (daemonRunning)
|
|
163
|
+
return false;
|
|
164
|
+
return shouldTeardownVersionSkewedBroker(realHeldBundles);
|
|
165
|
+
}
|
|
145
166
|
function onDarwin() {
|
|
146
167
|
return process.platform === 'darwin';
|
|
147
168
|
}
|
|
@@ -175,6 +196,10 @@ function agentDir() {
|
|
|
175
196
|
function socketPath() {
|
|
176
197
|
return path.join(agentDir(), 'agent.sock');
|
|
177
198
|
}
|
|
199
|
+
/** Public accessor for the broker's socket path — `agents daemon status`/`services` reads it for display. */
|
|
200
|
+
export function secretsBrokerSocketPath() {
|
|
201
|
+
return socketPath();
|
|
202
|
+
}
|
|
178
203
|
function pidPath() {
|
|
179
204
|
return path.join(agentDir(), 'agent.pid');
|
|
180
205
|
}
|
|
@@ -345,7 +370,7 @@ export function handleAgentRequest(store, req, now = Date.now()) {
|
|
|
345
370
|
// the broker is running pre-upgrade code and should be restarted.
|
|
346
371
|
return { ok: true, cmd: 'ping', version: PROTOCOL_VERSION, cliVersion: getCliVersion() };
|
|
347
372
|
case 'get': {
|
|
348
|
-
// Walk own-harness → global so
|
|
373
|
+
// Walk own-harness → global so an `--agent` grant wins over a global one and
|
|
349
374
|
// an unscoped unlock serves every harness (bundleScopeChain).
|
|
350
375
|
for (const scope of bundleScopeChain(req.harness)) {
|
|
351
376
|
const key = scopedBundleKey(req.name, scope);
|
|
@@ -1285,10 +1310,6 @@ export async function agentLock(name) {
|
|
|
1285
1310
|
const r = await request({ cmd: 'lock', name });
|
|
1286
1311
|
return r?.ok === true && r.cmd === 'lock' ? r.wiped : 0;
|
|
1287
1312
|
}
|
|
1288
|
-
export async function agentRevoke(leaseId) {
|
|
1289
|
-
const r = await request({ cmd: 'revoke', leaseId });
|
|
1290
|
-
return r?.ok === true && r.cmd === 'revoke' ? r.wiped : 0;
|
|
1291
|
-
}
|
|
1292
1313
|
/** List currently-unlocked bundles, or [] when no broker is running. The
|
|
1293
1314
|
* internal `secrets list` metadata-cache entry is filtered out here as well as
|
|
1294
1315
|
* server-side: during a rollout a NEW client can talk to an OLD broker that
|
|
@@ -1334,7 +1355,12 @@ export async function ensureAgentRunning(timeoutMs = 5000) {
|
|
|
1334
1355
|
if (ping.reachable) {
|
|
1335
1356
|
if (ping.cliVersion === undefined || ping.cliVersion === getCliVersionFresh())
|
|
1336
1357
|
return true;
|
|
1337
|
-
|
|
1358
|
+
// A reachable but version-skewed broker: tear it down ONLY when no daemon
|
|
1359
|
+
// hosts it and it holds no unlocks. Evicting a daemon-hosted broker orphans
|
|
1360
|
+
// the daemon's socket and starts the Touch ID storm — see
|
|
1361
|
+
// shouldClientEvictSkewedBroker.
|
|
1362
|
+
const { isDaemonRunning } = await import('../daemon.js');
|
|
1363
|
+
if (!shouldClientEvictSkewedBroker(isDaemonRunning(), (await agentStatus()).length))
|
|
1338
1364
|
return true;
|
|
1339
1365
|
await teardownStaleBroker();
|
|
1340
1366
|
}
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* (bundles.ts).
|
|
5
5
|
*
|
|
6
6
|
* A grant is stored under a scope and read under a scope; the two must agree or
|
|
7
|
-
* the bundle is invisible. `agents secrets unlock --
|
|
7
|
+
* the bundle is invisible. `agents secrets unlock --agent <agent>` exists to NARROW
|
|
8
8
|
* a grant to one harness, so an unlock without it is global by definition.
|
|
9
9
|
*
|
|
10
10
|
* This module deliberately has NO imports: agent.ts and session-store.ts already
|
|
@@ -13,14 +13,14 @@
|
|
|
13
13
|
* and throw at runtime even though tsc is happy.
|
|
14
14
|
*/
|
|
15
15
|
/**
|
|
16
|
-
* Scope of an unlock that was not narrowed with `--
|
|
16
|
+
* Scope of an unlock that was not narrowed with `--agent`: readable by every
|
|
17
17
|
* harness. Not a valid harness name, so it can never collide with one.
|
|
18
18
|
*/
|
|
19
19
|
export declare const GLOBAL_HARNESS = "*";
|
|
20
20
|
/**
|
|
21
21
|
* Scopes a reader consults, most specific first: its own harness, then the global
|
|
22
22
|
* grant. This is the resolution order of the scoped-grant model — a narrow
|
|
23
|
-
* `--
|
|
23
|
+
* `--agent claude` unlock stays claude-only while an unscoped unlock serves
|
|
24
24
|
* everyone — not a fallback papering over a miss.
|
|
25
25
|
*/
|
|
26
26
|
export declare function bundleScopeChain(harness: string | undefined): string[];
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* (bundles.ts).
|
|
5
5
|
*
|
|
6
6
|
* A grant is stored under a scope and read under a scope; the two must agree or
|
|
7
|
-
* the bundle is invisible. `agents secrets unlock --
|
|
7
|
+
* the bundle is invisible. `agents secrets unlock --agent <agent>` exists to NARROW
|
|
8
8
|
* a grant to one harness, so an unlock without it is global by definition.
|
|
9
9
|
*
|
|
10
10
|
* This module deliberately has NO imports: agent.ts and session-store.ts already
|
|
@@ -13,14 +13,14 @@
|
|
|
13
13
|
* and throw at runtime even though tsc is happy.
|
|
14
14
|
*/
|
|
15
15
|
/**
|
|
16
|
-
* Scope of an unlock that was not narrowed with `--
|
|
16
|
+
* Scope of an unlock that was not narrowed with `--agent`: readable by every
|
|
17
17
|
* harness. Not a valid harness name, so it can never collide with one.
|
|
18
18
|
*/
|
|
19
19
|
export const GLOBAL_HARNESS = '*';
|
|
20
20
|
/**
|
|
21
21
|
* Scopes a reader consults, most specific first: its own harness, then the global
|
|
22
22
|
* grant. This is the resolution order of the scoped-grant model — a narrow
|
|
23
|
-
* `--
|
|
23
|
+
* `--agent claude` unlock stays claude-only while an unscoped unlock serves
|
|
24
24
|
* everyone — not a fallback papering over a miss.
|
|
25
25
|
*/
|
|
26
26
|
export function bundleScopeChain(harness) {
|
|
@@ -93,10 +93,6 @@ export declare function deleteBundleSessions(name: string): void;
|
|
|
93
93
|
/** Delete one bundle's session blob and prune it from the index. */
|
|
94
94
|
export declare function deleteSession(name: string, harness?: string): void;
|
|
95
95
|
export declare function deleteLeaseSession(leaseId: string): number;
|
|
96
|
-
export declare function activeLeaseSessions(now?: number): Array<{
|
|
97
|
-
name: string;
|
|
98
|
-
lease: SecretLease;
|
|
99
|
-
}>;
|
|
100
96
|
/** Delete every session blob + the index (for `secrets lock --all`). */
|
|
101
97
|
export declare function deleteAllSessions(): void;
|
|
102
98
|
/** Rehydrate every unexpired session into `[name, entry]` pairs for the broker to
|
|
@@ -208,11 +208,6 @@ export function deleteLeaseSession(leaseId) {
|
|
|
208
208
|
}
|
|
209
209
|
return deleted;
|
|
210
210
|
}
|
|
211
|
-
export function activeLeaseSessions(now = Date.now()) {
|
|
212
|
-
return rehydrateSessions(now)
|
|
213
|
-
.filter((item) => Boolean(item.entry.lease))
|
|
214
|
-
.map(({ name, entry }) => ({ name, lease: entry.lease }));
|
|
215
|
-
}
|
|
216
211
|
/** Delete every session blob + the index (for `secrets lock --all`). */
|
|
217
212
|
export function deleteAllSessions() {
|
|
218
213
|
if (!shouldPersist())
|
|
@@ -1,39 +1,39 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
2
|
+
* Bookmarked sessions — the durable "keep this one handy" mark a human puts on a
|
|
3
|
+
* session, deliberately kept OUT of the session index.
|
|
4
4
|
*
|
|
5
5
|
* `sessions.db` is a rebuildable CACHE: a reindex or a schema bump throws its
|
|
6
|
-
* rows away and re-derives them from the transcripts on disk. A
|
|
6
|
+
* rows away and re-derives them from the transcripts on disk. A bookmark is not
|
|
7
7
|
* derivable from a transcript — it is a human's choice — so a column there would
|
|
8
8
|
* be silently lost on the next rebuild. It lives in `~/.agents/.history/` instead,
|
|
9
9
|
* next to the actor sidecars, which is never pruned.
|
|
10
10
|
*
|
|
11
11
|
* One flat set of session ids. The id is the transcript's own uuid, so it is
|
|
12
12
|
* stable and machine-independent — but the file is NOT synced today: session sync
|
|
13
|
-
* carries `.history/backups/` (`lib/session/sync/agents.ts`), not this.
|
|
13
|
+
* carries `.history/backups/` (`lib/session/sync/agents.ts`), not this. Bookmarks
|
|
14
14
|
* are therefore per-machine; carrying them across the fleet would mean adding
|
|
15
15
|
* them to the sync manifest, which this does not do.
|
|
16
16
|
*
|
|
17
|
-
* Reads are memoized against the file's mtime — the picker asks `
|
|
17
|
+
* Reads are memoized against the file's mtime — the picker asks `isBookmarked` once
|
|
18
18
|
* per rendered row, and re-reading a JSON file per row on every keystroke is the
|
|
19
19
|
* kind of cost that makes a TUI feel broken.
|
|
20
20
|
*/
|
|
21
|
-
export declare function
|
|
21
|
+
export declare function bookmarksFilePath(): string;
|
|
22
22
|
/** Drop the memoized read. Tests that write the file directly need this; nothing
|
|
23
23
|
* in the CLI does, because every mutation here refreshes the cache itself. */
|
|
24
|
-
export declare function
|
|
24
|
+
export declare function clearBookmarksCache(): void;
|
|
25
25
|
/**
|
|
26
|
-
* Every
|
|
27
|
-
* unreadable, or malformed — a corrupt
|
|
26
|
+
* Every bookmarked session id. Empty (never throws) when the file is absent,
|
|
27
|
+
* unreadable, or malformed — a corrupt bookmarks file must not take down
|
|
28
28
|
* `agents sessions`.
|
|
29
29
|
*/
|
|
30
|
-
export declare function
|
|
31
|
-
export declare function
|
|
30
|
+
export declare function listBookmarks(): Set<string>;
|
|
31
|
+
export declare function isBookmarked(sessionId: string | undefined): boolean;
|
|
32
32
|
/**
|
|
33
|
-
* Set (or clear) the
|
|
33
|
+
* Set (or clear) the bookmark mark on a session. Returns the resulting state, so
|
|
34
34
|
* a caller can report it without a second read. A no-op write is skipped, which
|
|
35
35
|
* keeps the file's mtime — and every other process's memoized read — untouched.
|
|
36
36
|
*/
|
|
37
|
-
export declare function
|
|
38
|
-
/** Flip the mark; returns the new state (`true` = now
|
|
39
|
-
export declare function
|
|
37
|
+
export declare function setBookmark(sessionId: string, on: boolean): boolean;
|
|
38
|
+
/** Flip the mark; returns the new state (`true` = now bookmarked). */
|
|
39
|
+
export declare function toggleBookmark(sessionId: string): boolean;
|
|
@@ -1,28 +1,28 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
2
|
+
* Bookmarked sessions — the durable "keep this one handy" mark a human puts on a
|
|
3
|
+
* session, deliberately kept OUT of the session index.
|
|
4
4
|
*
|
|
5
5
|
* `sessions.db` is a rebuildable CACHE: a reindex or a schema bump throws its
|
|
6
|
-
* rows away and re-derives them from the transcripts on disk. A
|
|
6
|
+
* rows away and re-derives them from the transcripts on disk. A bookmark is not
|
|
7
7
|
* derivable from a transcript — it is a human's choice — so a column there would
|
|
8
8
|
* be silently lost on the next rebuild. It lives in `~/.agents/.history/` instead,
|
|
9
9
|
* next to the actor sidecars, which is never pruned.
|
|
10
10
|
*
|
|
11
11
|
* One flat set of session ids. The id is the transcript's own uuid, so it is
|
|
12
12
|
* stable and machine-independent — but the file is NOT synced today: session sync
|
|
13
|
-
* carries `.history/backups/` (`lib/session/sync/agents.ts`), not this.
|
|
13
|
+
* carries `.history/backups/` (`lib/session/sync/agents.ts`), not this. Bookmarks
|
|
14
14
|
* are therefore per-machine; carrying them across the fleet would mean adding
|
|
15
15
|
* them to the sync manifest, which this does not do.
|
|
16
16
|
*
|
|
17
|
-
* Reads are memoized against the file's mtime — the picker asks `
|
|
17
|
+
* Reads are memoized against the file's mtime — the picker asks `isBookmarked` once
|
|
18
18
|
* per rendered row, and re-reading a JSON file per row on every keystroke is the
|
|
19
19
|
* kind of cost that makes a TUI feel broken.
|
|
20
20
|
*/
|
|
21
21
|
import fs from 'node:fs';
|
|
22
22
|
import path from 'node:path';
|
|
23
23
|
import { getHistoryDir } from '../state.js';
|
|
24
|
-
export function
|
|
25
|
-
return path.join(getHistoryDir(), '
|
|
24
|
+
export function bookmarksFilePath() {
|
|
25
|
+
return path.join(getHistoryDir(), 'bookmarks.json');
|
|
26
26
|
}
|
|
27
27
|
/** Memoized parse, invalidated by the file's mtime+size (another process — or
|
|
28
28
|
* another machine's sync — can rewrite it under us). */
|
|
@@ -38,16 +38,16 @@ function statKey(file) {
|
|
|
38
38
|
}
|
|
39
39
|
/** Drop the memoized read. Tests that write the file directly need this; nothing
|
|
40
40
|
* in the CLI does, because every mutation here refreshes the cache itself. */
|
|
41
|
-
export function
|
|
41
|
+
export function clearBookmarksCache() {
|
|
42
42
|
cache = null;
|
|
43
43
|
}
|
|
44
44
|
/**
|
|
45
|
-
* Every
|
|
46
|
-
* unreadable, or malformed — a corrupt
|
|
45
|
+
* Every bookmarked session id. Empty (never throws) when the file is absent,
|
|
46
|
+
* unreadable, or malformed — a corrupt bookmarks file must not take down
|
|
47
47
|
* `agents sessions`.
|
|
48
48
|
*/
|
|
49
|
-
export function
|
|
50
|
-
const file =
|
|
49
|
+
export function listBookmarks() {
|
|
50
|
+
const file = bookmarksFilePath();
|
|
51
51
|
const key = statKey(file);
|
|
52
52
|
if (cache && cache.key === key)
|
|
53
53
|
return cache.ids;
|
|
@@ -64,14 +64,14 @@ export function listFavorites() {
|
|
|
64
64
|
cache = { key, ids };
|
|
65
65
|
return ids;
|
|
66
66
|
}
|
|
67
|
-
export function
|
|
67
|
+
export function isBookmarked(sessionId) {
|
|
68
68
|
if (!sessionId)
|
|
69
69
|
return false;
|
|
70
|
-
return
|
|
70
|
+
return listBookmarks().has(sessionId);
|
|
71
71
|
}
|
|
72
72
|
/** Atomic write (tmp + rename) so a concurrent reader never sees a half file. */
|
|
73
|
-
function
|
|
74
|
-
const file =
|
|
73
|
+
function writeBookmarks(ids) {
|
|
74
|
+
const file = bookmarksFilePath();
|
|
75
75
|
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
76
76
|
const body = { version: 1, sessionIds: [...ids].sort() };
|
|
77
77
|
const tmp = `${file}.${process.pid}.tmp`;
|
|
@@ -80,22 +80,22 @@ function writeFavorites(ids) {
|
|
|
80
80
|
cache = { key: statKey(file), ids };
|
|
81
81
|
}
|
|
82
82
|
/**
|
|
83
|
-
* Set (or clear) the
|
|
83
|
+
* Set (or clear) the bookmark mark on a session. Returns the resulting state, so
|
|
84
84
|
* a caller can report it without a second read. A no-op write is skipped, which
|
|
85
85
|
* keeps the file's mtime — and every other process's memoized read — untouched.
|
|
86
86
|
*/
|
|
87
|
-
export function
|
|
88
|
-
const ids = new Set(
|
|
87
|
+
export function setBookmark(sessionId, on) {
|
|
88
|
+
const ids = new Set(listBookmarks());
|
|
89
89
|
if (ids.has(sessionId) === on)
|
|
90
90
|
return on;
|
|
91
91
|
if (on)
|
|
92
92
|
ids.add(sessionId);
|
|
93
93
|
else
|
|
94
94
|
ids.delete(sessionId);
|
|
95
|
-
|
|
95
|
+
writeBookmarks(ids);
|
|
96
96
|
return on;
|
|
97
97
|
}
|
|
98
|
-
/** Flip the mark; returns the new state (`true` = now
|
|
99
|
-
export function
|
|
100
|
-
return
|
|
98
|
+
/** Flip the mark; returns the new state (`true` = now bookmarked). */
|
|
99
|
+
export function toggleBookmark(sessionId) {
|
|
100
|
+
return setBookmark(sessionId, !isBookmarked(sessionId));
|
|
101
101
|
}
|
package/dist/lib/session/db.d.ts
CHANGED
|
@@ -28,6 +28,7 @@ export declare const RESOURCE_INDEX_VERSION = 1;
|
|
|
28
28
|
*/
|
|
29
29
|
/** Bump when facet extraction changes so cached rows recompute (stalls-by-model v6). */
|
|
30
30
|
export declare const INSIGHTS_EXTRACTOR_VERSION = 6;
|
|
31
|
+
export declare const PREVIEW_EXTRACTOR_VERSION = 1;
|
|
31
32
|
/** Raw row shape returned from the sessions table. */
|
|
32
33
|
export interface SessionRow {
|
|
33
34
|
id: string;
|
|
@@ -390,6 +391,20 @@ export declare function writeSessionInsights<T>(entries: Array<{
|
|
|
390
391
|
}>): void;
|
|
391
392
|
/** Drop every cached facet row. Backs `agents insights --refresh`. */
|
|
392
393
|
export declare function clearSessionInsights(): void;
|
|
394
|
+
/** Read one derived preview only when it matches the transcript bytes on disk. */
|
|
395
|
+
export declare function readSessionPreviewCache<T>(id: string, sourceStamp: {
|
|
396
|
+
fileMtimeMs: number | null;
|
|
397
|
+
fileSize: number | null;
|
|
398
|
+
}): T | undefined;
|
|
399
|
+
/** Persist normalized preview data against the exact transcript bytes parsed. */
|
|
400
|
+
export declare function writeSessionPreviewCache<T>(entry: {
|
|
401
|
+
id: string;
|
|
402
|
+
fileMtimeMs: number | null;
|
|
403
|
+
fileSize: number | null;
|
|
404
|
+
preview: T;
|
|
405
|
+
}): void;
|
|
406
|
+
/** Plugin provenance already indexed for resources used by one session. */
|
|
407
|
+
export declare function getSessionPlugins(id: string): string[];
|
|
393
408
|
export type UsageRollupGroup = 'agent' | 'project' | 'day' | 'account';
|
|
394
409
|
/**
|
|
395
410
|
* Smart-launch affinity priors: group sessions by origin machine, harness, or
|
package/dist/lib/session/db.js
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
import * as fs from 'fs';
|
|
10
10
|
import * as path from 'path';
|
|
11
11
|
import Database from '../sqlite.js';
|
|
12
|
-
import { parseSession } from './parse.js';
|
|
12
|
+
import { parseSession, sessionFilePathContainer } from './parse.js';
|
|
13
13
|
import { extractRecentDirectoriesTouched, extractTodoProgressFromEvents } from './state.js';
|
|
14
14
|
import { getSessionsDir, getSessionsDbPath } from '../state.js';
|
|
15
15
|
import { query as queryEvents, queryToolUsageForSessions } from '../events.js';
|
|
@@ -305,6 +305,18 @@ CREATE TABLE IF NOT EXISTS session_insights (
|
|
|
305
305
|
computed_at INTEGER NOT NULL,
|
|
306
306
|
facets TEXT NOT NULL
|
|
307
307
|
);
|
|
308
|
+
|
|
309
|
+
-- Normalized data behind sessions preview. Like session_insights this is a
|
|
310
|
+
-- lazy, stamp-validated cache: opening one session parses only that transcript,
|
|
311
|
+
-- while subsequent processes reuse the derived preview until its bytes change.
|
|
312
|
+
CREATE TABLE IF NOT EXISTS session_preview_cache (
|
|
313
|
+
session_id TEXT PRIMARY KEY,
|
|
314
|
+
file_mtime_ms INTEGER,
|
|
315
|
+
file_size INTEGER,
|
|
316
|
+
extractor_version INTEGER NOT NULL,
|
|
317
|
+
computed_at INTEGER NOT NULL,
|
|
318
|
+
preview_json TEXT NOT NULL
|
|
319
|
+
);
|
|
308
320
|
`;
|
|
309
321
|
/**
|
|
310
322
|
* Bumping this invalidates every cached facet row without touching the schema
|
|
@@ -314,6 +326,7 @@ CREATE TABLE IF NOT EXISTS session_insights (
|
|
|
314
326
|
*/
|
|
315
327
|
/** Bump when facet extraction changes so cached rows recompute (stalls-by-model v6). */
|
|
316
328
|
export const INSIGHTS_EXTRACTOR_VERSION = 6;
|
|
329
|
+
export const PREVIEW_EXTRACTOR_VERSION = 1;
|
|
317
330
|
let dbInstance = null;
|
|
318
331
|
/**
|
|
319
332
|
* Apply schema migrations from `fromVersion` → SCHEMA_VERSION. The new
|
|
@@ -2291,18 +2304,36 @@ function clearSessionExistenceCache() {
|
|
|
2291
2304
|
directoryMembershipSweepCount = 0;
|
|
2292
2305
|
}
|
|
2293
2306
|
function findMissingFilePaths(filePaths) {
|
|
2307
|
+
// Existence is decided on the CONTAINER file, never the raw stored path. A
|
|
2308
|
+
// composite `file_path` (`<container>#<id>`, e.g. OpenCode's `opencode.db#ses_…`)
|
|
2309
|
+
// names a row INSIDE a shared file — its basename is never a directory entry,
|
|
2310
|
+
// so a dirname/basename membership check on the composite string classified
|
|
2311
|
+
// every such row as deleted and pruned it (RUSH-2357). Group the original
|
|
2312
|
+
// paths under the container we actually stat, and map the verdict back so the
|
|
2313
|
+
// returned set still holds the original `file_path` strings the caller keys on.
|
|
2294
2314
|
const byDir = new Map();
|
|
2295
2315
|
for (const p of filePaths) {
|
|
2296
|
-
const
|
|
2297
|
-
|
|
2298
|
-
|
|
2299
|
-
|
|
2300
|
-
|
|
2316
|
+
const container = sessionFilePathContainer(p);
|
|
2317
|
+
const dir = path.dirname(container);
|
|
2318
|
+
const base = path.basename(container);
|
|
2319
|
+
let bases = byDir.get(dir);
|
|
2320
|
+
if (!bases) {
|
|
2321
|
+
bases = new Map();
|
|
2322
|
+
byDir.set(dir, bases);
|
|
2301
2323
|
}
|
|
2302
|
-
|
|
2324
|
+
let originals = bases.get(base);
|
|
2325
|
+
if (!originals) {
|
|
2326
|
+
originals = [];
|
|
2327
|
+
bases.set(base, originals);
|
|
2328
|
+
}
|
|
2329
|
+
originals.push(p);
|
|
2303
2330
|
}
|
|
2304
2331
|
const missing = new Set();
|
|
2305
|
-
|
|
2332
|
+
const markMissing = (originals) => {
|
|
2333
|
+
for (const original of originals)
|
|
2334
|
+
missing.add(original);
|
|
2335
|
+
};
|
|
2336
|
+
for (const [dir, bases] of byDir) {
|
|
2306
2337
|
let entries;
|
|
2307
2338
|
try {
|
|
2308
2339
|
const stat = fs.statSync(dir);
|
|
@@ -2324,17 +2355,16 @@ function findMissingFilePaths(filePaths) {
|
|
|
2324
2355
|
// Directory itself is gone (or unreadable) — every file in it is missing.
|
|
2325
2356
|
// Also covers the race where readdir loses to a concurrent delete: fall
|
|
2326
2357
|
// back to a direct stat rather than assuming existence.
|
|
2327
|
-
for (const base of
|
|
2358
|
+
for (const [base, originals] of bases) {
|
|
2328
2359
|
const filePath = path.join(dir, base);
|
|
2329
2360
|
if (!fs.existsSync(filePath))
|
|
2330
|
-
|
|
2361
|
+
markMissing(originals);
|
|
2331
2362
|
}
|
|
2332
2363
|
continue;
|
|
2333
2364
|
}
|
|
2334
|
-
for (const base of
|
|
2335
|
-
const filePath = path.join(dir, base);
|
|
2365
|
+
for (const [base, originals] of bases) {
|
|
2336
2366
|
if (!entries.has(base))
|
|
2337
|
-
|
|
2367
|
+
markMissing(originals);
|
|
2338
2368
|
}
|
|
2339
2369
|
}
|
|
2340
2370
|
return missing;
|
|
@@ -2471,6 +2501,49 @@ export function writeSessionInsights(entries) {
|
|
|
2471
2501
|
export function clearSessionInsights() {
|
|
2472
2502
|
getDB().exec(`DELETE FROM session_insights`);
|
|
2473
2503
|
}
|
|
2504
|
+
/** Read one derived preview only when it matches the transcript bytes on disk. */
|
|
2505
|
+
export function readSessionPreviewCache(id, sourceStamp) {
|
|
2506
|
+
const row = getDB().prepare(`
|
|
2507
|
+
SELECT pc.preview_json AS previewJson
|
|
2508
|
+
FROM session_preview_cache pc
|
|
2509
|
+
WHERE pc.session_id = ?
|
|
2510
|
+
AND pc.extractor_version = ?
|
|
2511
|
+
AND pc.file_mtime_ms IS ?
|
|
2512
|
+
AND pc.file_size IS ?
|
|
2513
|
+
`).get(id, PREVIEW_EXTRACTOR_VERSION, sourceStamp.fileMtimeMs, sourceStamp.fileSize);
|
|
2514
|
+
if (!row)
|
|
2515
|
+
return undefined;
|
|
2516
|
+
try {
|
|
2517
|
+
return JSON.parse(row.previewJson);
|
|
2518
|
+
}
|
|
2519
|
+
catch {
|
|
2520
|
+
return undefined;
|
|
2521
|
+
}
|
|
2522
|
+
}
|
|
2523
|
+
/** Persist normalized preview data against the exact transcript bytes parsed. */
|
|
2524
|
+
export function writeSessionPreviewCache(entry) {
|
|
2525
|
+
getDB().prepare(`
|
|
2526
|
+
INSERT INTO session_preview_cache
|
|
2527
|
+
(session_id, file_mtime_ms, file_size, extractor_version, computed_at, preview_json)
|
|
2528
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
2529
|
+
ON CONFLICT(session_id) DO UPDATE SET
|
|
2530
|
+
file_mtime_ms = excluded.file_mtime_ms,
|
|
2531
|
+
file_size = excluded.file_size,
|
|
2532
|
+
extractor_version = excluded.extractor_version,
|
|
2533
|
+
computed_at = excluded.computed_at,
|
|
2534
|
+
preview_json = excluded.preview_json
|
|
2535
|
+
`).run(entry.id, entry.fileMtimeMs, entry.fileSize, PREVIEW_EXTRACTOR_VERSION, Date.now(), JSON.stringify(entry.preview));
|
|
2536
|
+
}
|
|
2537
|
+
/** Plugin provenance already indexed for resources used by one session. */
|
|
2538
|
+
export function getSessionPlugins(id) {
|
|
2539
|
+
const rows = getDB().prepare(`
|
|
2540
|
+
SELECT DISTINCT plugin
|
|
2541
|
+
FROM session_resource_usage
|
|
2542
|
+
WHERE session_id = ? AND plugin IS NOT NULL AND plugin <> ''
|
|
2543
|
+
ORDER BY plugin COLLATE NOCASE
|
|
2544
|
+
`).all(id);
|
|
2545
|
+
return rows.map(row => row.plugin);
|
|
2546
|
+
}
|
|
2474
2547
|
export function queryAffinityRollup(options) {
|
|
2475
2548
|
const db = getDB();
|
|
2476
2549
|
const where = [];
|
|
@@ -2708,7 +2781,9 @@ export function backfillResourceUsage(filter = {}, onProgress) {
|
|
|
2708
2781
|
result.scanned++;
|
|
2709
2782
|
let stamp;
|
|
2710
2783
|
try {
|
|
2711
|
-
|
|
2784
|
+
// Composite rows (`<container>#<id>`) stat their container file — the
|
|
2785
|
+
// per-session bytes/mtime stamp is derived during discovery, not here.
|
|
2786
|
+
const st = fs.statSync(sessionFilePathContainer(meta.filePath));
|
|
2712
2787
|
stamp = { fileMtimeMs: st.mtimeMs, fileSize: st.size };
|
|
2713
2788
|
}
|
|
2714
2789
|
catch {
|
|
@@ -2787,7 +2862,7 @@ export function topSessionsByCost(n, options = {}) {
|
|
|
2787
2862
|
// Over-fetch a small buffer to survive the on-disk liveness filter below.
|
|
2788
2863
|
const sql = `SELECT * FROM sessions ${whereCost} ORDER BY cost_usd DESC, timestamp DESC LIMIT ${limit + 16}`;
|
|
2789
2864
|
const rows = db.prepare(sql).all(...params);
|
|
2790
|
-
const live = rows.filter(r => !r.file_path || fs.existsSync(r.file_path));
|
|
2865
|
+
const live = rows.filter(r => !r.file_path || fs.existsSync(sessionFilePathContainer(r.file_path)));
|
|
2791
2866
|
return live.slice(0, limit).map(r => ({
|
|
2792
2867
|
meta: rowToMeta(r),
|
|
2793
2868
|
costUsd: r.cost_usd ?? 0,
|