@phnx-labs/agents-cli 1.22.72 → 1.22.74
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 +73 -0
- package/README.md +2 -0
- package/dist/cli/command-registry.d.ts +1 -1
- package/dist/cli/command-registry.js +2 -1
- package/dist/commands/packages-materialize.d.ts +17 -0
- package/dist/commands/packages-materialize.js +93 -0
- package/dist/commands/packages.d.ts +6 -4
- package/dist/commands/packages.js +8 -4
- package/dist/commands/repo.js +8 -8
- package/dist/commands/sessions-picker.js +38 -7
- package/dist/commands/sessions.d.ts +50 -1
- package/dist/commands/sessions.js +85 -10
- package/dist/lib/actor.d.ts +36 -4
- package/dist/lib/actor.js +73 -9
- package/dist/lib/agent-spec/index.d.ts +4 -0
- package/dist/lib/agent-spec/index.js +5 -0
- package/dist/lib/agent-spec/materialize.d.ts +13 -0
- package/dist/lib/agent-spec/materialize.js +414 -0
- package/dist/lib/agent-spec/package-resolve.d.ts +12 -0
- package/dist/lib/agent-spec/package-resolve.js +274 -0
- package/dist/lib/agent-spec/package-schema.d.ts +5 -0
- package/dist/lib/agent-spec/package-schema.js +147 -0
- package/dist/lib/agent-spec/package-types.d.ts +121 -0
- package/dist/lib/agent-spec/package-types.js +11 -0
- package/dist/lib/cloud/rush.d.ts +1 -1
- package/dist/lib/cloud/rush.js +2 -2
- package/dist/lib/daemon/usage-sync-service.d.ts +12 -5
- package/dist/lib/daemon/usage-sync-service.js +27 -5
- package/dist/lib/exec.js +3 -0
- package/dist/lib/fleet-shared-state.d.ts +30 -0
- package/dist/lib/fleet-shared-state.js +5 -0
- package/dist/lib/hooks/install.d.ts +31 -1
- package/dist/lib/hooks/install.js +44 -2
- package/dist/lib/mcp.d.ts +14 -2
- package/dist/lib/mcp.js +12 -2
- package/dist/lib/packages/output-home.d.ts +39 -0
- package/dist/lib/packages/output-home.js +203 -0
- package/dist/lib/paths.d.ts +9 -0
- package/dist/lib/paths.js +26 -0
- package/dist/lib/project-resources.d.ts +6 -2
- package/dist/lib/project-resources.js +133 -44
- package/dist/lib/rush-session.d.ts +10 -2
- package/dist/lib/rush-session.js +12 -3
- package/dist/lib/secrets/drivers/rush.js +1 -1
- package/dist/lib/session/active.d.ts +12 -0
- package/dist/lib/session/active.js +5 -1
- package/dist/lib/session/actor-sidecar.d.ts +7 -0
- package/dist/lib/session/actor-sidecar.js +2 -0
- package/dist/lib/session/cloud.js +1 -1
- package/dist/lib/session/db.d.ts +60 -1
- package/dist/lib/session/db.js +191 -6
- package/dist/lib/session/live-metadata.d.ts +24 -0
- package/dist/lib/session/live-metadata.js +55 -0
- package/dist/lib/session/mirror.d.ts +67 -0
- package/dist/lib/session/mirror.js +158 -0
- package/dist/lib/session/types.d.ts +18 -0
- package/dist/lib/spinner.d.ts +39 -0
- package/dist/lib/spinner.js +41 -0
- package/dist/lib/startup/command-registry.js +1 -1
- package/dist/lib/types.d.ts +6 -0
- package/package.json +1 -1
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { execFileSync } from 'child_process';
|
|
1
2
|
import * as fs from 'fs';
|
|
2
3
|
import * as path from 'path';
|
|
3
4
|
import { AGENTS, agentConfigDirName, isAgentHardDeprecated } from './agents.js';
|
|
@@ -39,10 +40,14 @@ export function syncProjectResourcesToAgent(agent, version, projectAgentsDir) {
|
|
|
39
40
|
// .agents/{commands,skills,…}, refreshed on every launch. Left untracked it
|
|
40
41
|
// dirties `git status` and can block `git merge`/`checkout` when a stray
|
|
41
42
|
// commit of the same path collides. So the generator owns its ignore rule:
|
|
42
|
-
// reconcile a per-agent marker block
|
|
43
|
-
//
|
|
44
|
-
//
|
|
45
|
-
|
|
43
|
+
// reconcile a per-agent marker block listing exactly the paths it manages.
|
|
44
|
+
// That block lives in `.git/info/exclude` — git's per-clone, uncommitted
|
|
45
|
+
// ignore file — NOT the tracked `.gitignore`: these entries are never
|
|
46
|
+
// committed upstream, so writing them into `.gitignore` left every launch
|
|
47
|
+
// with a permanent `M .gitignore` that blocked `git pull` (PHNX-3718).
|
|
48
|
+
// Passing the manifest set (empty when a sync clears a harness) also prunes
|
|
49
|
+
// the block. See PHNX-3717 for the self-managed-ignore feature.
|
|
50
|
+
reconcileManagedIgnore(projectRoot, agent, agentRoot, Array.from(next).sort());
|
|
46
51
|
}
|
|
47
52
|
return result;
|
|
48
53
|
}
|
|
@@ -75,7 +80,11 @@ function gitignoreMarkers(agent) {
|
|
|
75
80
|
}
|
|
76
81
|
/**
|
|
77
82
|
* Turn the manifest's managed paths (relative to agentRoot) into anchored,
|
|
78
|
-
* POSIX,
|
|
83
|
+
* POSIX, `referenceRoot`-relative ignore entries. `referenceRoot` is the
|
|
84
|
+
* directory the anchored `/…` patterns resolve against — the git worktree root
|
|
85
|
+
* for a `.git/info/exclude` block, since git anchors info/exclude patterns at
|
|
86
|
+
* the top of the working tree (not at the harness dir). Two guards keep it
|
|
87
|
+
* honest:
|
|
79
88
|
* - drop any path that escapes the harness config dir (e.g. grok writes
|
|
80
89
|
* commands back into the tracked `.agents/` tree via a `../` subdir —
|
|
81
90
|
* ignoring that would hide tracked source; separate bug, PHNX-3718);
|
|
@@ -84,7 +93,7 @@ function gitignoreMarkers(agent) {
|
|
|
84
93
|
* these never masks a hand-authored or committed file (e.g. a repo that
|
|
85
94
|
* commits its own `.claude/CLAUDE.md` keeps it — it is not in the manifest).
|
|
86
95
|
*/
|
|
87
|
-
export function managedGitignoreEntries(agentRoot,
|
|
96
|
+
export function managedGitignoreEntries(agentRoot, referenceRoot, managed) {
|
|
88
97
|
const root = path.resolve(agentRoot);
|
|
89
98
|
const entries = new Set();
|
|
90
99
|
for (const rel of managed) {
|
|
@@ -93,31 +102,63 @@ export function managedGitignoreEntries(agentRoot, projectRoot, managed) {
|
|
|
93
102
|
const abs = path.resolve(agentRoot, rel);
|
|
94
103
|
if (abs !== root && !abs.startsWith(root + path.sep))
|
|
95
104
|
continue;
|
|
96
|
-
const
|
|
97
|
-
if (!
|
|
105
|
+
const fromRoot = toPosixRel(path.relative(referenceRoot, abs));
|
|
106
|
+
if (!fromRoot || fromRoot === '..' || fromRoot.startsWith('../'))
|
|
98
107
|
continue;
|
|
99
|
-
entries.add('/' +
|
|
108
|
+
entries.add('/' + fromRoot);
|
|
100
109
|
}
|
|
101
110
|
return Array.from(entries).sort();
|
|
102
111
|
}
|
|
103
|
-
/**
|
|
104
|
-
*
|
|
105
|
-
*
|
|
106
|
-
*
|
|
107
|
-
*
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
112
|
+
/**
|
|
113
|
+
* Ask git where the local, per-clone ignore file lives and where the worktree
|
|
114
|
+
* top is, resolved robustly for every layout by delegating to git itself:
|
|
115
|
+
* - normal repo → `<root>/.git/info/exclude`;
|
|
116
|
+
* - monorepo subdir → the same file even when `.git` is several levels up
|
|
117
|
+
* (`projectRoot`, the parent of `.agents/`, is not the git root);
|
|
118
|
+
* - linked worktree / submodule → `.git` is a FILE (`gitdir: …`), and
|
|
119
|
+
* `--git-path info/exclude` resolves to the shared COMMON dir so the block
|
|
120
|
+
* applies across every worktree.
|
|
121
|
+
* `--path-format=absolute` forces absolute paths regardless of the `-C` cwd.
|
|
122
|
+
* One `git rev-parse` yields both paths (exclude path first, worktree root
|
|
123
|
+
* second), so the launch path spawns git ONCE, not twice.
|
|
124
|
+
* Returns null when `dir` is not inside a git repo (git exits non-zero), which
|
|
125
|
+
* fails the feature open (no-op) exactly like the old in-tree check did.
|
|
126
|
+
*/
|
|
127
|
+
function resolveGitExcludeTarget(dir) {
|
|
128
|
+
try {
|
|
129
|
+
const out = execFileSync('git', ['-C', dir, 'rev-parse', '--path-format=absolute', '--git-path', 'info/exclude', '--show-toplevel'], { encoding: 'utf-8', stdio: ['ignore', 'pipe', 'ignore'] });
|
|
130
|
+
const [excludePath, worktreeRoot] = out.split('\n').map((l) => l.trim());
|
|
131
|
+
if (!excludePath || !worktreeRoot)
|
|
132
|
+
return null;
|
|
133
|
+
// Fail open on anything that isn't a clean pair of ABSOLUTE paths. A git
|
|
134
|
+
// older than 2.31 (predates `--path-format`) echoes the unrecognized flag
|
|
135
|
+
// back on stdout instead of erroring, which would otherwise shift the parse
|
|
136
|
+
// and have mkdirSync create a stray `--path-format=absolute` dir. The
|
|
137
|
+
// absolute-path check turns that into a clean no-op.
|
|
138
|
+
if (!path.isAbsolute(excludePath) || !path.isAbsolute(worktreeRoot))
|
|
139
|
+
return null;
|
|
140
|
+
return { excludePath, worktreeRoot };
|
|
141
|
+
}
|
|
142
|
+
catch {
|
|
143
|
+
return null;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
/** True when git tracks `absPath` in the repo `dir` sits in. */
|
|
147
|
+
function isTrackedByGit(dir, absPath) {
|
|
148
|
+
try {
|
|
149
|
+
execFileSync('git', ['-C', dir, 'ls-files', '--error-unmatch', '--', absPath], {
|
|
150
|
+
stdio: ['ignore', 'ignore', 'ignore'],
|
|
151
|
+
});
|
|
152
|
+
return true;
|
|
153
|
+
}
|
|
154
|
+
catch {
|
|
155
|
+
return false;
|
|
117
156
|
}
|
|
118
157
|
}
|
|
119
158
|
/**
|
|
120
|
-
* Apply this agent's managed block to
|
|
159
|
+
* Apply this agent's managed block to ignore-file content, IN PLACE — used both
|
|
160
|
+
* to write the block into `.git/info/exclude` and to strip a leftover block from
|
|
161
|
+
* a legacy tracked `.gitignore` (entries `[]` prunes).
|
|
121
162
|
*
|
|
122
163
|
* In-place replacement (not strip-then-append) is load-bearing: appending would
|
|
123
164
|
* move this agent's block behind every other agent's block on each resync, so in
|
|
@@ -141,9 +182,11 @@ function applyManagedBlock(content, begin, end, entries) {
|
|
|
141
182
|
if (entries.length > 0) {
|
|
142
183
|
return [...lines.slice(0, bi), begin, ...entries, end, ...lines.slice(ei + 1)].join('\n');
|
|
143
184
|
}
|
|
144
|
-
// Prune the block, tidying the blank lines that hugged it.
|
|
145
|
-
//
|
|
146
|
-
// so entries.length is never 0 there)
|
|
185
|
+
// Prune the block, tidying the blank lines that hugged it. Unreached on the
|
|
186
|
+
// reconcileManagedIgnore write path (its entries always include the manifest,
|
|
187
|
+
// so entries.length is never 0 there), but the load-bearing case for
|
|
188
|
+
// stripLegacyManagedGitignoreBlock, which calls with entries=[] to migrate a
|
|
189
|
+
// leftover block out of the tracked .gitignore.
|
|
147
190
|
const before = lines.slice(0, bi);
|
|
148
191
|
const after = lines.slice(ei + 1);
|
|
149
192
|
while (before.length && before[before.length - 1].trim() === '')
|
|
@@ -160,30 +203,42 @@ function applyManagedBlock(content, begin, end, entries) {
|
|
|
160
203
|
return body.length > 0 ? `${body}\n\n${block}\n` : `${block}\n`;
|
|
161
204
|
}
|
|
162
205
|
/**
|
|
163
|
-
* Reconcile a per-agent managed block in
|
|
164
|
-
*
|
|
165
|
-
* and convergent: replaces the block in place
|
|
166
|
-
* actually changes, so the launch hot path does
|
|
167
|
-
* watchers) every run — even in a project synced by
|
|
168
|
-
* sync clears a harness's resources the block does not
|
|
169
|
-
* lone `.agents-managed.json` entry (that file still
|
|
170
|
-
* must stay ignored), so the block is only ever
|
|
171
|
-
* this call path.
|
|
206
|
+
* Reconcile a per-agent managed block in `.git/info/exclude` so the generated
|
|
207
|
+
* per-harness resource dir never shows as untracked dirt — WITHOUT dirtying the
|
|
208
|
+
* tracked `.gitignore`. Idempotent and convergent: replaces the block in place
|
|
209
|
+
* and writes only when the content actually changes, so the launch hot path does
|
|
210
|
+
* not churn the file (or its watchers) every run — even in a project synced by
|
|
211
|
+
* several harnesses. When a sync clears a harness's resources the block does not
|
|
212
|
+
* vanish: it shrinks to the lone `.agents-managed.json` entry (that file still
|
|
213
|
+
* sits in the harness dir and must stay ignored), so the block is only ever
|
|
214
|
+
* fully pruned by hand, never via this call path. Fails open (no-op) outside a
|
|
215
|
+
* git working tree.
|
|
216
|
+
*
|
|
217
|
+
* Also self-heals repos dirtied by the previous behavior: PHNX-3717 wrote these
|
|
218
|
+
* blocks into `<projectRoot>/.gitignore`, which is never committed upstream, so
|
|
219
|
+
* every launch left a permanent `M .gitignore` that blocked `git pull`
|
|
220
|
+
* (PHNX-3718). `stripLegacyManagedGitignoreBlock` removes this agent's leftover
|
|
221
|
+
* block from that tracked file on the next launch, cleaning the diff instead of
|
|
222
|
+
* stranding it.
|
|
172
223
|
*/
|
|
173
|
-
function
|
|
174
|
-
|
|
175
|
-
if
|
|
176
|
-
|
|
224
|
+
function reconcileManagedIgnore(projectRoot, agent, agentRoot, managed) {
|
|
225
|
+
// Migrate away from the old tracked-.gitignore location first, so an already
|
|
226
|
+
// dirtied repo cleans itself even if git resolution below fails.
|
|
227
|
+
stripLegacyManagedGitignoreBlock(projectRoot, agent);
|
|
228
|
+
const target = resolveGitExcludeTarget(projectRoot);
|
|
229
|
+
if (!target)
|
|
230
|
+
return; // not a git repo — fail open
|
|
177
231
|
const { begin, end } = gitignoreMarkers(agent);
|
|
178
232
|
// Ignore the manifest marker file too, not just the synced resources: the
|
|
179
233
|
// sync always writes `<agentRoot>/.agents-managed.json`, so without this the
|
|
180
234
|
// harness dir still shows as untracked in `git status` on the strength of that
|
|
181
235
|
// one file (defeating the whole point). It lives at agentRoot, so it resolves
|
|
182
|
-
// through the same anchoring + escape guard as any managed path.
|
|
183
|
-
|
|
236
|
+
// through the same anchoring + escape guard as any managed path. Anchored to
|
|
237
|
+
// the worktree root, since info/exclude patterns resolve against the tree top.
|
|
238
|
+
const entries = managedGitignoreEntries(agentRoot, target.worktreeRoot, [MANIFEST_FILE, ...managed]);
|
|
184
239
|
let original = '';
|
|
185
240
|
try {
|
|
186
|
-
original = fs.readFileSync(
|
|
241
|
+
original = fs.readFileSync(target.excludePath, 'utf-8');
|
|
187
242
|
}
|
|
188
243
|
catch {
|
|
189
244
|
original = '';
|
|
@@ -191,8 +246,42 @@ function reconcileProjectGitignore(projectRoot, agent, agentRoot, managed) {
|
|
|
191
246
|
const next = applyManagedBlock(original, begin, end, entries);
|
|
192
247
|
if (next === null || next === original)
|
|
193
248
|
return;
|
|
194
|
-
|
|
249
|
+
fs.mkdirSync(path.dirname(target.excludePath), { recursive: true }); // create info/ if missing
|
|
250
|
+
const tmp = target.excludePath + '.tmp';
|
|
195
251
|
fs.writeFileSync(tmp, next);
|
|
252
|
+
fs.renameSync(tmp, target.excludePath);
|
|
253
|
+
}
|
|
254
|
+
/**
|
|
255
|
+
* Remove this agent's leftover managed block from a tracked `<projectRoot>/
|
|
256
|
+
* .gitignore` written by the pre-PHNX-3718 behavior. Strips ONLY the fenced
|
|
257
|
+
* block (leaving every hand-written rule untouched), never creates the file,
|
|
258
|
+
* and never touches a `.gitignore` that carries no block of ours. If stripping
|
|
259
|
+
* empties a file we created (its only content was our block), the empty file is
|
|
260
|
+
* removed when git does not track it — an empty untracked `.gitignore` would
|
|
261
|
+
* still read as `?? .gitignore` dirt, the very thing this migration clears.
|
|
262
|
+
*/
|
|
263
|
+
function stripLegacyManagedGitignoreBlock(projectRoot, agent) {
|
|
264
|
+
const gitignorePath = path.join(projectRoot, '.gitignore');
|
|
265
|
+
let original;
|
|
266
|
+
try {
|
|
267
|
+
original = fs.readFileSync(gitignorePath, 'utf-8');
|
|
268
|
+
}
|
|
269
|
+
catch {
|
|
270
|
+
return; // no .gitignore — nothing to migrate
|
|
271
|
+
}
|
|
272
|
+
const { begin, end } = gitignoreMarkers(agent);
|
|
273
|
+
// Empty entries → applyManagedBlock prunes the block; returns `original`
|
|
274
|
+
// unchanged when there is no block, or null on an orphaned begin marker
|
|
275
|
+
// (which we refuse to touch rather than truncate the user's rules).
|
|
276
|
+
const stripped = applyManagedBlock(original, begin, end, []);
|
|
277
|
+
if (stripped === null || stripped === original)
|
|
278
|
+
return;
|
|
279
|
+
if (stripped === '' && !isTrackedByGit(projectRoot, gitignorePath)) {
|
|
280
|
+
removePath(gitignorePath);
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
const tmp = gitignorePath + '.tmp';
|
|
284
|
+
fs.writeFileSync(tmp, stripped);
|
|
196
285
|
fs.renameSync(tmp, gitignorePath);
|
|
197
286
|
}
|
|
198
287
|
function removeManagedPath(agentRoot, rel) {
|
|
@@ -2,8 +2,16 @@
|
|
|
2
2
|
* Rush session freshness — the ONE judgment shared by every consumer of
|
|
3
3
|
* ~/.rush/user.yaml (cloud dispatch, cloud session source, secrets sync driver).
|
|
4
4
|
*
|
|
5
|
-
* A Rush session stores `expires_at` as Unix
|
|
6
|
-
*
|
|
5
|
+
* A Rush session stores `expires_at` as Unix MILLISECONDS — the same unit
|
|
6
|
+
* rush/cli writes and reads it in (`sessionTokenFresh` in auth_reader.go compares
|
|
7
|
+
* against `time.Now()...UnixMilli()`), and the same unit the JWT `exp` claim maps
|
|
8
|
+
* to (`exp * 1000`). So freshness must compare it against `Date.now()` (ms), NOT
|
|
9
|
+
* `Date.now() / 1000` (seconds). Comparing ms against seconds was PHNX-3805: a
|
|
10
|
+
* long-expired session (e.g. `expires_at: 1788157222000`) read as `1.788e12 <=
|
|
11
|
+
* 1.788e9` → always false → never expired, so `agents cloud providers` reported
|
|
12
|
+
* Rush `available: true` on a dead session and every dispatch failed with a 401.
|
|
13
|
+
*
|
|
14
|
+
* Two values mean "never expires" and MUST NOT be read as an absolute timestamp:
|
|
7
15
|
* - `0` — an opaque Phoenix `pid_` bearer written by `rush login`;
|
|
8
16
|
* `0` = non-expiring by contract (mirrors rush/cli's isFresh,
|
|
9
17
|
* the same fix RUSH-1310 landed for the daemon-less freshness
|
package/dist/lib/rush-session.js
CHANGED
|
@@ -2,8 +2,16 @@
|
|
|
2
2
|
* Rush session freshness — the ONE judgment shared by every consumer of
|
|
3
3
|
* ~/.rush/user.yaml (cloud dispatch, cloud session source, secrets sync driver).
|
|
4
4
|
*
|
|
5
|
-
* A Rush session stores `expires_at` as Unix
|
|
6
|
-
*
|
|
5
|
+
* A Rush session stores `expires_at` as Unix MILLISECONDS — the same unit
|
|
6
|
+
* rush/cli writes and reads it in (`sessionTokenFresh` in auth_reader.go compares
|
|
7
|
+
* against `time.Now()...UnixMilli()`), and the same unit the JWT `exp` claim maps
|
|
8
|
+
* to (`exp * 1000`). So freshness must compare it against `Date.now()` (ms), NOT
|
|
9
|
+
* `Date.now() / 1000` (seconds). Comparing ms against seconds was PHNX-3805: a
|
|
10
|
+
* long-expired session (e.g. `expires_at: 1788157222000`) read as `1.788e12 <=
|
|
11
|
+
* 1.788e9` → always false → never expired, so `agents cloud providers` reported
|
|
12
|
+
* Rush `available: true` on a dead session and every dispatch failed with a 401.
|
|
13
|
+
*
|
|
14
|
+
* Two values mean "never expires" and MUST NOT be read as an absolute timestamp:
|
|
7
15
|
* - `0` — an opaque Phoenix `pid_` bearer written by `rush login`;
|
|
8
16
|
* `0` = non-expiring by contract (mirrors rush/cli's isFresh,
|
|
9
17
|
* the same fix RUSH-1310 landed for the daemon-less freshness
|
|
@@ -20,5 +28,6 @@ export function isRushSessionExpired(expiresAt) {
|
|
|
20
28
|
// `0` (non-expiring pid_ bearer) and a missing value are never expired.
|
|
21
29
|
if (typeof expiresAt !== 'number' || expiresAt === 0)
|
|
22
30
|
return false;
|
|
23
|
-
|
|
31
|
+
// expires_at is Unix milliseconds — compare ms to ms.
|
|
32
|
+
return expiresAt <= Date.now();
|
|
24
33
|
}
|
|
@@ -29,7 +29,7 @@ function readRushToken() {
|
|
|
29
29
|
}
|
|
30
30
|
const expiresAt = data.session?.expires_at;
|
|
31
31
|
if (isRushSessionExpired(expiresAt)) {
|
|
32
|
-
const expiredAt = new Date(expiresAt
|
|
32
|
+
const expiredAt = new Date(expiresAt).toISOString();
|
|
33
33
|
throw new Error(`Rush session expired at ${expiredAt}. Run \`rush login\` to refresh.`);
|
|
34
34
|
}
|
|
35
35
|
return token;
|
|
@@ -402,6 +402,18 @@ export interface ActiveSession {
|
|
|
402
402
|
* did not inherit a terminal id.
|
|
403
403
|
*/
|
|
404
404
|
terminalId?: string;
|
|
405
|
+
/**
|
|
406
|
+
* The launch id (`AGENT_LAUNCH_ID`) the CLI stamps on every agent at spawn — a
|
|
407
|
+
* stable UUID that is identical locally and across an SSH hop and survives a
|
|
408
|
+
* session-id rotation (`/clear`, exit-and-rerun). Unlike `sessionId` (which the
|
|
409
|
+
* non-Claude harnesses only mint after boot) it exists from the first tick, so
|
|
410
|
+
* it is the join key a client uses to re-identify a session on the watch stream.
|
|
411
|
+
* Populated wherever the by-pid launch registry resolves — reliably for
|
|
412
|
+
* `agents run`-launched processes; still absent for editor-launched terminals
|
|
413
|
+
* whose shell pid does not line up with the agent-pid registry today (the same
|
|
414
|
+
* limitation the `readPidSessionEntry` call in `listTerminalsActive` documents).
|
|
415
|
+
*/
|
|
416
|
+
launchId?: string;
|
|
405
417
|
/**
|
|
406
418
|
* tmux pane id (`%N`) when this row was discovered via the tmux source AND its
|
|
407
419
|
* session id could not be resolved (a born-unidentifiable non-Claude pane). It
|
|
@@ -1059,6 +1059,8 @@ export async function listTerminalsActive() {
|
|
|
1059
1059
|
tty: procByPid.get(t.pid)?.tty,
|
|
1060
1060
|
pid: t.pid,
|
|
1061
1061
|
sessionId: t.sessionId ?? sessionIdFromFile(sessionFile),
|
|
1062
|
+
launchId: pidEntry?.launchId,
|
|
1063
|
+
terminalId: pidEntry?.terminalId,
|
|
1062
1064
|
cwd: t.cwd ?? undefined,
|
|
1063
1065
|
label,
|
|
1064
1066
|
name,
|
|
@@ -1558,7 +1560,8 @@ async function listUnattributedActiveLive(attributed) {
|
|
|
1558
1560
|
lastActivityMs: mtimeMs,
|
|
1559
1561
|
pidCount: 1 + (foldedByRoot.get(pid) ?? 0),
|
|
1560
1562
|
owner: resolveOwner(entry?.actor, resolvedId),
|
|
1561
|
-
|
|
1563
|
+
launchId: entry?.launchId ?? hookRec?.launch_id,
|
|
1564
|
+
terminalId: entry?.terminalId ?? hookRec?.terminal_id,
|
|
1562
1565
|
}, state, sessionFile, true));
|
|
1563
1566
|
}
|
|
1564
1567
|
// Housekeeping: drop registry files for pids that have since died.
|
|
@@ -1773,6 +1776,7 @@ export async function listTmuxAgentSessions() {
|
|
|
1773
1776
|
// Factory / --active join key: AGENT_TERMINAL_ID stamped on the launch
|
|
1774
1777
|
// registry and preserved by SessionStart. Without this, Grok/Codex tmux
|
|
1775
1778
|
// panes never surface terminalId even when by-pid has it (RUSH-2192).
|
|
1779
|
+
launchId: liveEntry?.launchId,
|
|
1776
1780
|
terminalId: liveEntry?.terminalId,
|
|
1777
1781
|
// An id-less pane keys its dedupe on the unique pane, so two anonymous
|
|
1778
1782
|
// co-located panes stay two rows instead of folding into one.
|
|
@@ -5,6 +5,13 @@ export interface SessionActorRecord {
|
|
|
5
5
|
actor?: string;
|
|
6
6
|
/** Actor kind (`resolveActor().kind`). */
|
|
7
7
|
initiatedBy?: 'human' | 'agent';
|
|
8
|
+
/**
|
|
9
|
+
* Phoenix id of the responsible actor (`resolveActor().phoenixId`) — the
|
|
10
|
+
* tailnet human's stable Phoenix identity, resolved from the `actors:` map at
|
|
11
|
+
* spawn (PHNX-3798). Pairs with {@link actor}: joined onto the session index at
|
|
12
|
+
* scan time so a durable listing can surface the Phoenix id, not just the email.
|
|
13
|
+
*/
|
|
14
|
+
phoenixId?: string;
|
|
8
15
|
/** Effective permissions mode used by the launcher. */
|
|
9
16
|
mode?: SessionRunMode;
|
|
10
17
|
/**
|
|
@@ -39,6 +39,7 @@ function isSafeAlias(alias) {
|
|
|
39
39
|
}
|
|
40
40
|
function hasRecordData(record) {
|
|
41
41
|
return typeof record.actor === 'string'
|
|
42
|
+
|| typeof record.phoenixId === 'string'
|
|
42
43
|
|| typeof record.mode === 'string'
|
|
43
44
|
|| typeof record.version === 'string'
|
|
44
45
|
|| typeof record.harness === 'string'
|
|
@@ -84,6 +85,7 @@ export function writeSessionAliasRecord(sessionId, alias) {
|
|
|
84
85
|
sessionId,
|
|
85
86
|
actor: previous?.actor,
|
|
86
87
|
initiatedBy: previous?.initiatedBy,
|
|
88
|
+
phoenixId: previous?.phoenixId,
|
|
87
89
|
mode: previous?.mode,
|
|
88
90
|
version: previous?.version,
|
|
89
91
|
harness: previous?.harness,
|
|
@@ -32,7 +32,7 @@ function readToken() {
|
|
|
32
32
|
}
|
|
33
33
|
const expiresAt = data.session?.expires_at;
|
|
34
34
|
if (isRushSessionExpired(expiresAt)) {
|
|
35
|
-
const expiredAt = new Date(expiresAt
|
|
35
|
+
const expiredAt = new Date(expiresAt).toISOString();
|
|
36
36
|
throw new Error(`Rush session expired at ${expiredAt}. Run \`rush login\` to refresh.`);
|
|
37
37
|
}
|
|
38
38
|
return token;
|
package/dist/lib/session/db.d.ts
CHANGED
|
@@ -13,7 +13,7 @@ import { type ToolScanResumePoint } from './tool-store.js';
|
|
|
13
13
|
/** Current schema version; bumped when migrations are added. Exported so tests
|
|
14
14
|
* assert against the constant instead of hardcoding a number that every bump
|
|
15
15
|
* then has to chase (docs/sessions.md calls the constant the source of truth). */
|
|
16
|
-
export declare const SCHEMA_VERSION =
|
|
16
|
+
export declare const SCHEMA_VERSION = 47;
|
|
17
17
|
/**
|
|
18
18
|
* Bump to force the content extractor (assistant-answer text, alongside the
|
|
19
19
|
* user-prompt text every harness already accumulates) to re-derive on every
|
|
@@ -443,6 +443,65 @@ export declare function readSessionContent(id: string): string | undefined;
|
|
|
443
443
|
* survives for this session.
|
|
444
444
|
*/
|
|
445
445
|
export declare function readArchivedSessionPreview<T>(id: string): T | undefined;
|
|
446
|
+
/** A local-origin session, projected to the compact fields the fleet mirror publishes. */
|
|
447
|
+
export interface LocalMirrorSource {
|
|
448
|
+
id: string;
|
|
449
|
+
shortId: string;
|
|
450
|
+
agent: string;
|
|
451
|
+
version: string | null;
|
|
452
|
+
machine: string | null;
|
|
453
|
+
cwd: string | null;
|
|
454
|
+
topic: string | null;
|
|
455
|
+
firstUserMessage: string | null;
|
|
456
|
+
label: string | null;
|
|
457
|
+
lastActivity: string | null;
|
|
458
|
+
timestamp: string;
|
|
459
|
+
ticketId: string | null;
|
|
460
|
+
prUrl: string | null;
|
|
461
|
+
}
|
|
462
|
+
/**
|
|
463
|
+
* The most-recently-active sessions whose transcript is genuinely local to this
|
|
464
|
+
* box — a real `file_path`, not a peer mirror — for publishing to the fleet
|
|
465
|
+
* session mirror (PHNX-3792). Bounded and team-origin-excluded so the published
|
|
466
|
+
* payload stays the size a picker would show. Never returns a row this box is
|
|
467
|
+
* itself mirroring from a peer (`mirror_synced_at IS NULL`).
|
|
468
|
+
*/
|
|
469
|
+
export declare function queryLocalOriginSessionsForMirror(self: string, limit: number): LocalMirrorSource[];
|
|
470
|
+
/** One peer session digest to write into this box's local mirror. */
|
|
471
|
+
export interface MirrorSessionUpsert {
|
|
472
|
+
id: string;
|
|
473
|
+
shortId: string;
|
|
474
|
+
agent: string;
|
|
475
|
+
version?: string | null;
|
|
476
|
+
machine: string;
|
|
477
|
+
cwd?: string | null;
|
|
478
|
+
topic?: string | null;
|
|
479
|
+
firstUser?: string | null;
|
|
480
|
+
label?: string | null;
|
|
481
|
+
lastActivity?: string | null;
|
|
482
|
+
timestamp: string;
|
|
483
|
+
ticketId?: string | null;
|
|
484
|
+
prUrl?: string | null;
|
|
485
|
+
}
|
|
486
|
+
/**
|
|
487
|
+
* Write one peer session's digest into the local `sessions` index as a mirror
|
|
488
|
+
* row (PHNX-3792), so the picker/list/focus render its topic/preview inline
|
|
489
|
+
* with no per-row SSH. The upsert is GUARDED: it never overwrites a genuine
|
|
490
|
+
* local transcript row (`file_path` non-empty and not itself a mirror), so a
|
|
491
|
+
* mirror can only create a new peer-only row or enrich an existing empty-file
|
|
492
|
+
* host-dispatch stub / older mirror. Returns true when a row was written (the
|
|
493
|
+
* caller then persists the matching preview digest); false when a real row was
|
|
494
|
+
* protected. `machine` carries the publisher's recorded execution host so the
|
|
495
|
+
* row collapses with the live fan-out row under the same `machine:id` key.
|
|
496
|
+
*/
|
|
497
|
+
export declare function upsertMirrorSession(row: MirrorSessionUpsert, source: string, syncedAt: number): boolean;
|
|
498
|
+
/**
|
|
499
|
+
* Drop peer mirror rows (and their cached digests) whose last sync predates the
|
|
500
|
+
* cutoff — the size ceiling / staleness pruner for the fleet session mirror
|
|
501
|
+
* (PHNX-3792). Only ever touches mirror rows (`mirror_synced_at IS NOT NULL`),
|
|
502
|
+
* never a genuine local or host-dispatch row. Returns the number of rows pruned.
|
|
503
|
+
*/
|
|
504
|
+
export declare function pruneMirrorSessions(cutoffMs: number): number;
|
|
446
505
|
/** Plugin provenance already indexed for resources used by one session. */
|
|
447
506
|
export declare function getSessionPlugins(id: string): string[];
|
|
448
507
|
export type UsageRollupGroup = 'agent' | 'project' | 'day' | 'account';
|