@phnx-labs/agents-cli 1.22.105 → 1.22.107
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 +32 -0
- package/README.md +28 -6
- package/dist/browser.js +0 -0
- package/dist/commands/exec.d.ts +78 -8
- package/dist/commands/exec.js +407 -284
- package/dist/commands/resume.d.ts +6 -21
- package/dist/commands/resume.js +18 -55
- package/dist/commands/run-account-picker.d.ts +11 -0
- package/dist/commands/run-account-picker.js +11 -1
- package/dist/commands/sessions-resume.d.ts +4 -0
- package/dist/commands/sessions-resume.js +132 -49
- package/dist/commands/sessions.js +32 -5
- package/dist/index.js +0 -0
- package/dist/lib/accounting/account-launch.d.ts +54 -0
- package/dist/lib/accounting/account-launch.js +117 -0
- package/dist/lib/accounting/account-pool-collect.js +2 -1
- package/dist/lib/accounting/account-pool.d.ts +2 -0
- package/dist/lib/accounting/account-pool.js +1 -0
- package/dist/lib/accounting/rotate.d.ts +32 -6
- package/dist/lib/accounting/rotate.js +70 -42
- package/dist/lib/accounting/usage.d.ts +71 -0
- package/dist/lib/accounting/usage.js +160 -11
- package/dist/lib/exec-account-home.d.ts +3 -1
- package/dist/lib/exec-account-home.js +2 -2
- package/dist/lib/exec.d.ts +27 -1
- package/dist/lib/exec.js +150 -27
- package/dist/lib/hosts/dispatch.d.ts +1 -1
- package/dist/lib/hosts/dispatch.js +1 -1
- package/dist/lib/models.d.ts +1 -1
- package/dist/lib/models.js +4 -4
- package/dist/lib/session/actor-sidecar.d.ts +3 -11
- package/dist/lib/session/actor-sidecar.js +3 -0
- package/dist/lib/session/claude-accounts.d.ts +12 -73
- package/dist/lib/session/claude-accounts.js +32 -70
- package/dist/lib/session/db.d.ts +1 -1
- package/dist/lib/session/db.js +18 -5
- package/dist/lib/session/discover.d.ts +4 -0
- package/dist/lib/session/discover.js +116 -15
- package/dist/lib/session/recovery.d.ts +30 -34
- package/dist/lib/session/recovery.js +212 -76
- package/dist/lib/session/types.d.ts +2 -0
- package/dist/lib/teams/placement-probe.js +1 -1
- package/dist/session-tracker/dist/adapters/claude.d.ts +10 -0
- package/dist/session-tracker/dist/adapters/claude.js +45 -0
- package/dist/session-tracker/dist/hook.sh +191 -0
- package/dist/session-tracker/dist/index.d.ts +19 -0
- package/dist/session-tracker/dist/index.js +67 -0
- package/dist/session-tracker/dist/install-hook.d.ts +19 -0
- package/dist/session-tracker/dist/install-hook.js +245 -0
- package/dist/session-tracker/dist/prune-state.d.ts +2 -0
- package/dist/session-tracker/dist/prune-state.js +7 -0
- package/dist/session-tracker/dist/reader.d.ts +7 -0
- package/dist/session-tracker/dist/reader.js +151 -0
- package/dist/session-tracker/dist/state-file.d.ts +10 -0
- package/dist/session-tracker/dist/state-file.js +119 -0
- package/dist/session-tracker/dist/types.d.ts +32 -0
- package/dist/session-tracker/dist/types.js +1 -0
- package/dist/session-tracker/dist/writer.d.ts +12 -0
- package/dist/session-tracker/dist/writer.js +27 -0
- package/package.json +1 -1
|
@@ -14,18 +14,10 @@ export interface SessionActorRecord {
|
|
|
14
14
|
phoenixId?: string;
|
|
15
15
|
/** Effective permissions mode used by the launcher. */
|
|
16
16
|
mode?: SessionRunMode;
|
|
17
|
-
/**
|
|
18
|
-
* The agents-cli version-home id this session launched under (e.g. `2.1.207`,
|
|
19
|
-
* codex `0.146.0`) — the same namespace `listInstalledVersions` /
|
|
20
|
-
* `collectRunCandidates` use, so a native resume can pin the exact origin
|
|
21
|
-
* version. Recorded at launch by the SessionStart hook (from `AGENTS_RUN_VERSION`),
|
|
22
|
-
* because a harness coins its real session id only AFTER spawn — the same reason
|
|
23
|
-
* `mode` rides the hook rather than a spawn-time `writeSessionActorRecord`. Joined
|
|
24
|
-
* onto the session index at scan time so a session whose transcript carries no
|
|
25
|
-
* embedded/derivable version (codex's `.codex-homes/<version>/` layout) no longer
|
|
26
|
-
* degrades native resume to `/continue` for lack of a recorded origin (PHNX-3626).
|
|
27
|
-
*/
|
|
17
|
+
/** Installed executable label at launch; provenance only, never account identity. */
|
|
28
18
|
version?: string;
|
|
19
|
+
/** Credential account used at launch, independent of the installed executable. */
|
|
20
|
+
accountId?: string;
|
|
29
21
|
/**
|
|
30
22
|
* Custom harness / profile name when launched via `agents run <profile>`
|
|
31
23
|
* (e.g. `deepseek`). Joined onto the session index at scan time so a
|
|
@@ -42,6 +42,7 @@ function hasRecordData(record) {
|
|
|
42
42
|
|| typeof record.phoenixId === 'string'
|
|
43
43
|
|| typeof record.mode === 'string'
|
|
44
44
|
|| typeof record.version === 'string'
|
|
45
|
+
|| typeof record.accountId === 'string'
|
|
45
46
|
|| typeof record.harness === 'string'
|
|
46
47
|
|| (Array.isArray(record.aliases) && record.aliases.some(alias => typeof alias === 'string'));
|
|
47
48
|
}
|
|
@@ -69,6 +70,7 @@ export function writeSessionActorRecord(record) {
|
|
|
69
70
|
writeRecord({
|
|
70
71
|
...previous,
|
|
71
72
|
...record,
|
|
73
|
+
accountId: previous?.accountId ?? record.accountId,
|
|
72
74
|
aliases: normalizedAliases([...(previous?.aliases ?? []), ...(record.aliases ?? [])]),
|
|
73
75
|
});
|
|
74
76
|
}
|
|
@@ -88,6 +90,7 @@ export function writeSessionAliasRecord(sessionId, alias) {
|
|
|
88
90
|
phoenixId: previous?.phoenixId,
|
|
89
91
|
mode: previous?.mode,
|
|
90
92
|
version: previous?.version,
|
|
93
|
+
accountId: previous?.accountId,
|
|
91
94
|
harness: previous?.harness,
|
|
92
95
|
aliases: normalizedAliases([...(previous?.aliases ?? []), alias]),
|
|
93
96
|
startedAtMs: previous?.startedAtMs ?? Date.now(),
|
|
@@ -1,63 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Which Claude account produced a transcript.
|
|
3
|
-
*
|
|
4
|
-
* A Claude `.jsonl` records `sessionId`, `cwd`, `version`, `gitBranch` and per-message
|
|
5
|
-
* `usage`, but carries **no account identity** — no `accountUuid`, no
|
|
6
|
-
* `organizationUuid`, no email. What agents-cli does have is the version layout: every
|
|
7
|
-
* installed version gets its own home with its own `.claude.json` (`CLAUDE_CONFIG_DIR`
|
|
8
|
-
* is swapped per version, see lib/exec.ts), so a home identifies an account.
|
|
9
|
-
*
|
|
10
|
-
* This matters because the default run strategy is `balanced` (lib/rotate.ts), which
|
|
11
|
-
* sprays sessions across every signed-in account. Before this module the scanner
|
|
12
|
-
* resolved ONE email process-globally and stamped it on every Claude session, so a
|
|
13
|
-
* machine with several accounts reported all of its history under whichever one
|
|
14
|
-
* happened to resolve first.
|
|
15
|
-
*
|
|
16
|
-
* Grouping is keyed on the **org** (`usageKey`), never the email: two orgs under one
|
|
17
|
-
* email (a Team seat and a personal Max plan) are separate quota buckets and must stay
|
|
18
|
-
* distinct — the same invariant `candidateIdentity` enforces in lib/rotate.ts.
|
|
19
|
-
*
|
|
20
|
-
* ## Evidence tiers
|
|
21
|
-
*
|
|
22
|
-
* Attribution is a pure function of (path, recorded version). It performs no per-file
|
|
23
|
-
* I/O and does not need the transcript to still exist, which is what lets the v33
|
|
24
|
-
* migration backfill already-indexed rows without re-parsing anything.
|
|
25
|
-
*
|
|
26
|
-
* 1. **The path names a home we can identify.** Strongest: the file physically lives in
|
|
27
|
-
* that home, including a retired `trash/` snapshot, which keeps its `.claude.json`.
|
|
28
|
-
* 1b. **The path names a home that exists but is signed out.** Dark, named after that
|
|
29
|
-
* home. The location proves which config dir Claude used, so this deliberately beats
|
|
30
|
-
* a recorded version — attributing it to some other version's account would be a
|
|
31
|
-
* guess dressed as evidence.
|
|
32
|
-
* 2. **The path is outside every known home, and the row records a version.** Resolve
|
|
33
|
-
* that version's own home. Covers the mutable `~/.claude` symlink and the routine
|
|
34
|
-
* archives under `<historyDir>/runs` that `readRoutineArchiveMeta` feeds in. The
|
|
35
|
-
* symlink's target moves with `agents use`, so "whatever it points at now" is weak
|
|
36
|
-
* evidence for old rows: on the machine this was developed against only 684 of 1,334
|
|
37
|
-
* such rows came from the version the symlink currently names, and 322 came from
|
|
38
|
-
* versions belonging to a *different* org.
|
|
39
|
-
* 3. **Under the symlink with no recorded version at all.** Its current target is the
|
|
40
|
-
* only evidence there is, and the bucket says so via `evidence`. A version that IS
|
|
41
|
-
* recorded but resolves to no home stops at tier 2 and stays dark — it never
|
|
42
|
-
* reaches here.
|
|
43
|
-
* 4. **None of the above.** An explicitly dark bucket, labelled with why. Never folded
|
|
44
|
-
* into a real account and never dropped.
|
|
45
|
-
*
|
|
46
|
-
* ## Harness scope: Claude only, deliberately
|
|
47
|
-
*
|
|
48
|
-
* Attribution is implemented for Claude and no other harness. It depends on the
|
|
49
|
-
* per-version home carrying an `oauthAccount` in `.claude.json`, which is what makes a
|
|
50
|
-
* home equal an account. The other harnesses do have per-version credential files
|
|
51
|
-
* (`CREDENTIAL_FILE_SEGMENTS` in lib/agents.ts), so the mechanism generalizes — codex
|
|
52
|
-
* stores an `auth.json` JWT, gemini a `google_accounts.json` — but each needs its own
|
|
53
|
-
* identity extractor and its own notion of a quota bucket, and none of them has the
|
|
54
|
-
* two-orgs-one-email problem that motivated keying on the org here.
|
|
55
|
-
*
|
|
56
|
-
* Until that lands, a non-Claude session has a NULL `account_key` and rolls up under
|
|
57
|
-
* `unattributed:<agent>` — named after its harness rather than implying we tried and
|
|
58
|
-
* failed. `--by account` on `agents insights cost` / `agents insights output` therefore reports Claude
|
|
59
|
-
* accounts plus one bucket per other harness.
|
|
60
|
-
*/
|
|
61
1
|
/** The account a transcript is attributed to. */
|
|
62
2
|
export interface ClaudeAccountBucket {
|
|
63
3
|
/**
|
|
@@ -84,7 +24,7 @@ interface HomeEntry {
|
|
|
84
24
|
}
|
|
85
25
|
/** Resolver over the Claude homes present on this machine. */
|
|
86
26
|
export interface ClaudeAccountIndex {
|
|
87
|
-
/** Version- and trash-home prefixes, longest first. Excludes the `~/.claude` symlink. */
|
|
27
|
+
/** Version-, account-slot-, and trash-home prefixes, longest first. Excludes the `~/.claude` symlink. */
|
|
88
28
|
entries: HomeEntry[];
|
|
89
29
|
/**
|
|
90
30
|
* Config-dir prefixes of homes that exist but carry no `oauthAccount`. Kept
|
|
@@ -102,6 +42,15 @@ export interface ClaudeAccountIndex {
|
|
|
102
42
|
* as dark rather than guessed.
|
|
103
43
|
*/
|
|
104
44
|
byVersion: Map<string, ClaudeAccountBucket | 'ambiguous'>;
|
|
45
|
+
/**
|
|
46
|
+
* Account-slot id (`<historyDir>/accounts/claude/<accountId>/`, PHNX-3940) →
|
|
47
|
+
* the identity read from that slot's own `.claude.json`. A slot's identity is
|
|
48
|
+
* proven the same way a version home's is — tier 1 evidence, see
|
|
49
|
+
* {@link resolveClaudeAccount} — so this map exists only to let a launch-
|
|
50
|
+
* recorded accountId (once the actor sidecar carries one) resolve straight to
|
|
51
|
+
* a bucket without re-deriving it from a path.
|
|
52
|
+
*/
|
|
53
|
+
byAccountId: Map<string, ClaudeAccountBucket>;
|
|
105
54
|
/** Whatever `~/.claude` points at right now; tier-3 evidence only. */
|
|
106
55
|
symlinkBucket: ClaudeAccountBucket | null;
|
|
107
56
|
/** Literal prefix of the live symlinked config dir. */
|
|
@@ -113,16 +62,6 @@ export interface ClaudeAccountIndex {
|
|
|
113
62
|
* its version was rotated out stays attributable.
|
|
114
63
|
*/
|
|
115
64
|
export declare function buildClaudeAccountIndex(): ClaudeAccountIndex;
|
|
116
|
-
/**
|
|
117
|
-
|
|
118
|
-
* version stored on the session row (`sessions.version`), which is what disambiguates
|
|
119
|
-
* rows sitting under the mutable `~/.claude` symlink.
|
|
120
|
-
*
|
|
121
|
-
* Never returns null: a transcript that matches no known home resolves to an
|
|
122
|
-
* explicitly dark bucket rather than being dropped or folded into a real account.
|
|
123
|
-
* Backup mirrors (`<historyDir>/backups/claude/<stamp>/projects/…`) carry no
|
|
124
|
-
* `.claude.json` of their own, so they resolve by recorded version like any other
|
|
125
|
-
* out-of-home path, and go dark only when that version names no home.
|
|
126
|
-
*/
|
|
127
|
-
export declare function resolveClaudeAccount(index: ClaudeAccountIndex, filePath: string, recordedVersion?: string | null): ClaudeAccountBucket;
|
|
65
|
+
/** Resolve a quota bucket without replacing recorded login provenance. */
|
|
66
|
+
export declare function resolveClaudeAccount(index: ClaudeAccountIndex, filePath: string, recordedVersion?: string | null, launchAccountId?: string | null): ClaudeAccountBucket;
|
|
128
67
|
export {};
|
|
@@ -1,62 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* `organizationUuid`, no email. What agents-cli does have is the version layout: every
|
|
7
|
-
* installed version gets its own home with its own `.claude.json` (`CLAUDE_CONFIG_DIR`
|
|
8
|
-
* is swapped per version, see lib/exec.ts), so a home identifies an account.
|
|
9
|
-
*
|
|
10
|
-
* This matters because the default run strategy is `balanced` (lib/rotate.ts), which
|
|
11
|
-
* sprays sessions across every signed-in account. Before this module the scanner
|
|
12
|
-
* resolved ONE email process-globally and stamped it on every Claude session, so a
|
|
13
|
-
* machine with several accounts reported all of its history under whichever one
|
|
14
|
-
* happened to resolve first.
|
|
15
|
-
*
|
|
16
|
-
* Grouping is keyed on the **org** (`usageKey`), never the email: two orgs under one
|
|
17
|
-
* email (a Team seat and a personal Max plan) are separate quota buckets and must stay
|
|
18
|
-
* distinct — the same invariant `candidateIdentity` enforces in lib/rotate.ts.
|
|
19
|
-
*
|
|
20
|
-
* ## Evidence tiers
|
|
21
|
-
*
|
|
22
|
-
* Attribution is a pure function of (path, recorded version). It performs no per-file
|
|
23
|
-
* I/O and does not need the transcript to still exist, which is what lets the v33
|
|
24
|
-
* migration backfill already-indexed rows without re-parsing anything.
|
|
25
|
-
*
|
|
26
|
-
* 1. **The path names a home we can identify.** Strongest: the file physically lives in
|
|
27
|
-
* that home, including a retired `trash/` snapshot, which keeps its `.claude.json`.
|
|
28
|
-
* 1b. **The path names a home that exists but is signed out.** Dark, named after that
|
|
29
|
-
* home. The location proves which config dir Claude used, so this deliberately beats
|
|
30
|
-
* a recorded version — attributing it to some other version's account would be a
|
|
31
|
-
* guess dressed as evidence.
|
|
32
|
-
* 2. **The path is outside every known home, and the row records a version.** Resolve
|
|
33
|
-
* that version's own home. Covers the mutable `~/.claude` symlink and the routine
|
|
34
|
-
* archives under `<historyDir>/runs` that `readRoutineArchiveMeta` feeds in. The
|
|
35
|
-
* symlink's target moves with `agents use`, so "whatever it points at now" is weak
|
|
36
|
-
* evidence for old rows: on the machine this was developed against only 684 of 1,334
|
|
37
|
-
* such rows came from the version the symlink currently names, and 322 came from
|
|
38
|
-
* versions belonging to a *different* org.
|
|
39
|
-
* 3. **Under the symlink with no recorded version at all.** Its current target is the
|
|
40
|
-
* only evidence there is, and the bucket says so via `evidence`. A version that IS
|
|
41
|
-
* recorded but resolves to no home stops at tier 2 and stays dark — it never
|
|
42
|
-
* reaches here.
|
|
43
|
-
* 4. **None of the above.** An explicitly dark bucket, labelled with why. Never folded
|
|
44
|
-
* into a real account and never dropped.
|
|
45
|
-
*
|
|
46
|
-
* ## Harness scope: Claude only, deliberately
|
|
47
|
-
*
|
|
48
|
-
* Attribution is implemented for Claude and no other harness. It depends on the
|
|
49
|
-
* per-version home carrying an `oauthAccount` in `.claude.json`, which is what makes a
|
|
50
|
-
* home equal an account. The other harnesses do have per-version credential files
|
|
51
|
-
* (`CREDENTIAL_FILE_SEGMENTS` in lib/agents.ts), so the mechanism generalizes — codex
|
|
52
|
-
* stores an `auth.json` JWT, gemini a `google_accounts.json` — but each needs its own
|
|
53
|
-
* identity extractor and its own notion of a quota bucket, and none of them has the
|
|
54
|
-
* two-orgs-one-email problem that motivated keying on the org here.
|
|
55
|
-
*
|
|
56
|
-
* Until that lands, a non-Claude session has a NULL `account_key` and rolls up under
|
|
57
|
-
* `unattributed:<agent>` — named after its harness rather than implying we tried and
|
|
58
|
-
* failed. `--by account` on `agents insights cost` / `agents insights output` therefore reports Claude
|
|
59
|
-
* accounts plus one bucket per other harness.
|
|
2
|
+
* Claude quota attribution, separate from the stable login identity in accountId.
|
|
3
|
+
* Launch-recorded identity wins over current credentials in a reused home.
|
|
4
|
+
* Older rows retain their evidence-labelled path/version inference for historical
|
|
5
|
+
* quota reports; recovery never treats that org-scoped accountKey as login proof.
|
|
60
6
|
*/
|
|
61
7
|
import * as os from 'os';
|
|
62
8
|
import * as path from 'path';
|
|
@@ -126,6 +72,7 @@ export function buildClaudeAccountIndex() {
|
|
|
126
72
|
const darkHomes = [];
|
|
127
73
|
const liveByVersion = new Map();
|
|
128
74
|
const trashByVersion = new Map();
|
|
75
|
+
const byAccountId = new Map();
|
|
129
76
|
const addHome = (home, version, retired) => {
|
|
130
77
|
const bucket = bucketForHome(home, 'version-home');
|
|
131
78
|
if (!bucket) {
|
|
@@ -161,6 +108,26 @@ export function buildClaudeAccountIndex() {
|
|
|
161
108
|
addHome(path.join(trashBase, version, stamp, 'home'), version, true);
|
|
162
109
|
}
|
|
163
110
|
}
|
|
111
|
+
// Account slots (PHNX-3940): a named account gets its own HOME-shaped dir at
|
|
112
|
+
// <historyDir>/accounts/claude/<accountId>/, sharing the one managed install
|
|
113
|
+
// rather than owning a version home of its own (lib/accounts/slots.ts
|
|
114
|
+
// `slotDir`). Each slot's `.claude.json` proves ITS identity exactly as
|
|
115
|
+
// directly as a version home's — tier 1 evidence in `resolveClaudeAccount` —
|
|
116
|
+
// so without this, every transcript a slot-launched session wrote was
|
|
117
|
+
// discoverable (the account is registered and runnable) yet permanently
|
|
118
|
+
// unattributed here, and `isManagedSessionFile`/`getAgentSessionDirs` would
|
|
119
|
+
// never even have scanned it in the first place. An unconfigured slot (never
|
|
120
|
+
// signed in) yields no bucket and is simply skipped, not recorded dark — a
|
|
121
|
+
// slot with nothing written to it yet owns no transcript to misattribute.
|
|
122
|
+
const accountsBase = path.join(getHistoryDir(), 'accounts', 'claude');
|
|
123
|
+
for (const accountId of listDirs(accountsBase)) {
|
|
124
|
+
const home = path.join(accountsBase, accountId);
|
|
125
|
+
const bucket = bucketForHome(home, 'version-home');
|
|
126
|
+
if (!bucket)
|
|
127
|
+
continue;
|
|
128
|
+
entries.push({ prefix: path.join(home, '.claude'), bucket });
|
|
129
|
+
byAccountId.set(accountId, bucket);
|
|
130
|
+
}
|
|
164
131
|
// A live home is authoritative for its version. Otherwise the retired snapshots
|
|
165
132
|
// decide, but only when they agree — disagreement is reported, not resolved.
|
|
166
133
|
const byVersion = new Map();
|
|
@@ -178,6 +145,7 @@ export function buildClaudeAccountIndex() {
|
|
|
178
145
|
entries,
|
|
179
146
|
darkHomes,
|
|
180
147
|
byVersion,
|
|
148
|
+
byAccountId,
|
|
181
149
|
symlinkBucket: bucketForHome(HOME, 'symlink-target'),
|
|
182
150
|
symlinkPrefix: path.join(HOME, '.claude'),
|
|
183
151
|
};
|
|
@@ -187,18 +155,12 @@ function versionFromPath(filePath) {
|
|
|
187
155
|
const m = /[/\\]versions[/\\]claude[/\\]([^/\\]+)[/\\]/.exec(filePath);
|
|
188
156
|
return m ? m[1] : null;
|
|
189
157
|
}
|
|
190
|
-
/**
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
* explicitly dark bucket rather than being dropped or folded into a real account.
|
|
197
|
-
* Backup mirrors (`<historyDir>/backups/claude/<stamp>/projects/…`) carry no
|
|
198
|
-
* `.claude.json` of their own, so they resolve by recorded version like any other
|
|
199
|
-
* out-of-home path, and go dark only when that version names no home.
|
|
200
|
-
*/
|
|
201
|
-
export function resolveClaudeAccount(index, filePath, recordedVersion) {
|
|
158
|
+
/** Resolve a quota bucket without replacing recorded login provenance. */
|
|
159
|
+
export function resolveClaudeAccount(index, filePath, recordedVersion, launchAccountId) {
|
|
160
|
+
// Launch provenance survives credential changes and transcript relocation.
|
|
161
|
+
if (launchAccountId) {
|
|
162
|
+
return index.byAccountId.get(launchAccountId) ?? unattributed(`recorded account ${launchAccountId}`);
|
|
163
|
+
}
|
|
202
164
|
// Tier 1 — the file physically lives in a home we can identify.
|
|
203
165
|
for (const entry of index.entries) {
|
|
204
166
|
if (filePath.startsWith(entry.prefix + path.sep))
|
package/dist/lib/session/db.d.ts
CHANGED
|
@@ -14,7 +14,7 @@ import { type TimelineState } from './timeline.js';
|
|
|
14
14
|
/** Current schema version; bumped when migrations are added. Exported so tests
|
|
15
15
|
* assert against the constant instead of hardcoding a number that every bump
|
|
16
16
|
* then has to chase (docs/sessions.md calls the constant the source of truth). */
|
|
17
|
-
export declare const SCHEMA_VERSION =
|
|
17
|
+
export declare const SCHEMA_VERSION = 49;
|
|
18
18
|
/**
|
|
19
19
|
* Bump to force the content extractor (assistant-answer text, alongside the
|
|
20
20
|
* user-prompt text every harness already accumulates) to re-derive on every
|
package/dist/lib/session/db.js
CHANGED
|
@@ -29,7 +29,7 @@ const DB_PATH = getSessionsDbPath();
|
|
|
29
29
|
/** Current schema version; bumped when migrations are added. Exported so tests
|
|
30
30
|
* assert against the constant instead of hardcoding a number that every bump
|
|
31
31
|
* then has to chase (docs/sessions.md calls the constant the source of truth). */
|
|
32
|
-
export const SCHEMA_VERSION =
|
|
32
|
+
export const SCHEMA_VERSION = 49;
|
|
33
33
|
/**
|
|
34
34
|
* Bump to force the content extractor (assistant-answer text, alongside the
|
|
35
35
|
* user-prompt text every harness already accumulates) to re-derive on every
|
|
@@ -95,6 +95,7 @@ CREATE TABLE IF NOT EXISTS sessions (
|
|
|
95
95
|
version TEXT,
|
|
96
96
|
account TEXT,
|
|
97
97
|
account_key TEXT,
|
|
98
|
+
account_id TEXT,
|
|
98
99
|
account_org TEXT,
|
|
99
100
|
mode TEXT,
|
|
100
101
|
timestamp TEXT NOT NULL,
|
|
@@ -1309,6 +1310,11 @@ function migrateSchema(db, fromVersion) {
|
|
|
1309
1310
|
// The idx_sessions_mirror_synced index is created unconditionally after this
|
|
1310
1311
|
// block (fresh DBs skip migrations), alongside idx_sessions_last_activity.
|
|
1311
1312
|
}
|
|
1313
|
+
if (fromVersion < 49) {
|
|
1314
|
+
const cols = new Set(db.prepare(`PRAGMA table_info(sessions)`).all().map(c => c.name));
|
|
1315
|
+
if (!cols.has('account_id'))
|
|
1316
|
+
db.exec(`ALTER TABLE sessions ADD COLUMN account_id TEXT`);
|
|
1317
|
+
}
|
|
1312
1318
|
if (fromVersion < 48) {
|
|
1313
1319
|
// v47 -> v48: retain the LATEST genuine user turn beside the first
|
|
1314
1320
|
// (PHNX-3939). For a /continue, a redirect, or an interrupted-and-restated
|
|
@@ -1365,7 +1371,7 @@ function backfillClaudeAccounts(db, scope = 'all') {
|
|
|
1365
1371
|
// `account = excluded.account` unconditionally. A dark row reads NULL.
|
|
1366
1372
|
const update = db.prepare(`UPDATE sessions SET account_key = ?, account_org = ?, account = ? WHERE id = ?`);
|
|
1367
1373
|
for (const row of rows) {
|
|
1368
|
-
const bucket = resolveClaudeAccount(index, row.file_path ?? '', row.version);
|
|
1374
|
+
const bucket = resolveClaudeAccount(index, row.file_path ?? '', row.version, readSessionActorRecord(row.id)?.accountId);
|
|
1369
1375
|
update.run(bucket.key, bucket.orgName, bucket.email, row.id);
|
|
1370
1376
|
}
|
|
1371
1377
|
}
|
|
@@ -1418,6 +1424,9 @@ export function getDB() {
|
|
|
1418
1424
|
// scan column here rather than in SCHEMA (PHNX-3792).
|
|
1419
1425
|
db.exec(`CREATE INDEX IF NOT EXISTS idx_sessions_mirror_synced ON sessions(mirror_synced_at)`);
|
|
1420
1426
|
db.exec(`CREATE INDEX IF NOT EXISTS idx_sessions_routine_run_id ON sessions(routine_run_id)`);
|
|
1427
|
+
const sessionColumns = db.prepare('PRAGMA table_info(sessions)').all();
|
|
1428
|
+
if (!sessionColumns.some(column => column.name === 'account_id'))
|
|
1429
|
+
db.exec('ALTER TABLE sessions ADD COLUMN account_id TEXT');
|
|
1421
1430
|
// Account attribution repair. Two ways a Claude row ends up wrong even at v33:
|
|
1422
1431
|
// an older CLI (whose INSERT does not name the column) writes NULL, and a DB
|
|
1423
1432
|
// migrated by a build that predates the "clear the stale email on a dark row" fix
|
|
@@ -1883,7 +1892,7 @@ export function recordDirScans(entries) {
|
|
|
1883
1892
|
const upsertSessionStmt = (db) => db.prepare(`
|
|
1884
1893
|
INSERT INTO sessions (
|
|
1885
1894
|
id, short_id, agent, harness, origin, routine_name, routine_run_id,
|
|
1886
|
-
version, account, account_key, account_org, mode, timestamp, last_activity,
|
|
1895
|
+
version, account, account_key, account_id, account_org, mode, timestamp, last_activity,
|
|
1887
1896
|
project, cwd, git_branch, topic, first_user_message, last_user_message, label, message_count, token_count,
|
|
1888
1897
|
output_tokens, input_tokens, cache_read_tokens, cache_write_tokens,
|
|
1889
1898
|
cost_usd, cost_usd_nocache, duration_ms, model, tool_call_count,
|
|
@@ -1894,7 +1903,7 @@ const upsertSessionStmt = (db) => db.prepare(`
|
|
|
1894
1903
|
actor, initiated_by, phoenix_id, used_browser, used_computer
|
|
1895
1904
|
) VALUES (
|
|
1896
1905
|
@id, @short_id, @agent, @harness, @origin, @routine_name, @routine_run_id,
|
|
1897
|
-
@version, @account, @account_key, @account_org, @mode, @timestamp, @last_activity,
|
|
1906
|
+
@version, @account, @account_key, @account_id, @account_org, @mode, @timestamp, @last_activity,
|
|
1898
1907
|
@project, @cwd, @git_branch, @topic, @first_user_message, @last_user_message, @label, @message_count, @token_count,
|
|
1899
1908
|
@output_tokens, @input_tokens, @cache_read_tokens, @cache_write_tokens,
|
|
1900
1909
|
@cost_usd, @cost_usd_nocache, @duration_ms, @model, @tool_call_count,
|
|
@@ -1925,6 +1934,7 @@ const upsertSessionStmt = (db) => db.prepare(`
|
|
|
1925
1934
|
account = excluded.account,
|
|
1926
1935
|
account_key = excluded.account_key,
|
|
1927
1936
|
account_org = excluded.account_org,
|
|
1937
|
+
account_id = COALESCE(sessions.account_id, excluded.account_id),
|
|
1928
1938
|
mode = COALESCE(excluded.mode, sessions.mode),
|
|
1929
1939
|
timestamp = excluded.timestamp,
|
|
1930
1940
|
last_activity = excluded.last_activity,
|
|
@@ -2220,7 +2230,7 @@ export function upsertSession(meta, content, scan, assistantContent = '') {
|
|
|
2220
2230
|
// person. The ON CONFLICT COALESCEs actor/initiated_by, so this fills a fresh
|
|
2221
2231
|
// row AND backfills one indexed null-first (before its sidecar existed), while a
|
|
2222
2232
|
// rescan carrying no actor still keeps the stored owner.
|
|
2223
|
-
const actorRec = meta.actor ? undefined : readSessionActorRecord(meta.id);
|
|
2233
|
+
const actorRec = meta.actor && meta.accountId ? undefined : readSessionActorRecord(meta.id);
|
|
2224
2234
|
const toolUsage = detectToolUsage(meta.id);
|
|
2225
2235
|
const db = getDB();
|
|
2226
2236
|
const { upsert, delText, insText, readLabel } = stmts(db);
|
|
@@ -2239,6 +2249,7 @@ export function upsertSession(meta, content, scan, assistantContent = '') {
|
|
|
2239
2249
|
version: meta.version ?? actorRec?.version ?? null,
|
|
2240
2250
|
account: meta.account ?? null,
|
|
2241
2251
|
account_key: meta.accountKey ?? null,
|
|
2252
|
+
account_id: meta.accountId ?? actorRec?.accountId ?? null,
|
|
2242
2253
|
account_org: meta.accountOrg ?? null,
|
|
2243
2254
|
mode: meta.mode ?? actorRec?.mode ?? null,
|
|
2244
2255
|
timestamp: meta.timestamp,
|
|
@@ -2480,6 +2491,7 @@ export function upsertSessionsBatch(entries) {
|
|
|
2480
2491
|
version: meta.version ?? actorIndex.get(meta.id)?.version ?? null,
|
|
2481
2492
|
account: meta.account ?? null,
|
|
2482
2493
|
account_key: meta.accountKey ?? null,
|
|
2494
|
+
account_id: meta.accountId ?? actorIndex.get(meta.id)?.accountId ?? null,
|
|
2483
2495
|
account_org: meta.accountOrg ?? null,
|
|
2484
2496
|
mode: meta.mode ?? actorIndex.get(meta.id)?.mode ?? null,
|
|
2485
2497
|
timestamp: meta.timestamp,
|
|
@@ -2735,6 +2747,7 @@ function rowToMeta(row) {
|
|
|
2735
2747
|
version: row.version ?? undefined,
|
|
2736
2748
|
account: row.account ?? undefined,
|
|
2737
2749
|
accountKey: row.account_key ?? undefined,
|
|
2750
|
+
accountId: row.account_id ?? undefined,
|
|
2738
2751
|
accountOrg: row.account_org ?? undefined,
|
|
2739
2752
|
mode: isSessionRunMode(row.mode) ? row.mode : undefined,
|
|
2740
2753
|
topic: row.topic ?? undefined,
|
|
@@ -172,6 +172,10 @@ interface CodexSessionScan {
|
|
|
172
172
|
* TTL, no external lock files needed.
|
|
173
173
|
*/
|
|
174
174
|
export declare function discoverSessions(options?: DiscoverOptions): Promise<SessionMeta[]>;
|
|
175
|
+
/** Read only matching Claude/Codex transcripts on a cold ID lookup; indexing stays with the daemon. */
|
|
176
|
+
export declare function findLocalSessionTranscripts(selector: string, agent?: SessionAgentId): Promise<SessionMeta[]>;
|
|
177
|
+
/** Repair local metadata before deciding that a conversation has no context. */
|
|
178
|
+
export declare function hydrateSessionTranscript(session: SessionMeta): Promise<SessionMeta>;
|
|
175
179
|
/** What one incremental scan actually did. */
|
|
176
180
|
interface IncrementalScanResult {
|
|
177
181
|
/** True when this process won the single-flight claim and ran the scan. */
|
|
@@ -15,7 +15,6 @@ import { execFile } from 'child_process';
|
|
|
15
15
|
import { promisify } from 'util';
|
|
16
16
|
import Database from '../sqlite.js';
|
|
17
17
|
import { getAgentsDir, getUserAgentsDir, getHistoryDir, getRunsDir } from '../state.js';
|
|
18
|
-
import { shortCodexHome } from '../codex-home.js';
|
|
19
18
|
import { parseTimeFilter } from './relative-time.js';
|
|
20
19
|
const execFileAsync = promisify(execFile);
|
|
21
20
|
import { AGENTS, agentConfigDirName, getCliVersion, resolveOpenCodeAccountId } from '../agents.js';
|
|
@@ -31,6 +30,8 @@ import { isBackgroundShellStart, isSkillInvocation, extractSkills, extractSlashC
|
|
|
31
30
|
import { parseAntigravity, parseCursor, splitSessionFilePath } from './parse.js';
|
|
32
31
|
import { extractPrUrl, detectWorktree, detectTicket, isPrCreateCommand, detectSpawnedTeam, isTicketCreateTool, extractCreatedTicket, extractRecentDirectoriesTouched, extractTodoProgressFromEvents } from './state.js';
|
|
33
32
|
import { costOfUsage, costOfUsageNoCache } from '../pricing/index.js';
|
|
33
|
+
import { isSelfHost } from '../devices/self-host.js';
|
|
34
|
+
import { readSessionActorRecord } from './actor-sidecar.js';
|
|
34
35
|
import { machineForSessionFile } from './origin-machine.js';
|
|
35
36
|
export { machineForSessionFile } from './origin-machine.js';
|
|
36
37
|
import { mapBounded } from '../concurrency.js';
|
|
@@ -170,6 +171,72 @@ export async function discoverSessions(options) {
|
|
|
170
171
|
skipExistenceCheck: options?.skipExistenceCheck ?? false,
|
|
171
172
|
});
|
|
172
173
|
}
|
|
174
|
+
/** Read only matching Claude/Codex transcripts on a cold ID lookup; indexing stays with the daemon. */
|
|
175
|
+
export async function findLocalSessionTranscripts(selector, agent) {
|
|
176
|
+
const matches = new Map();
|
|
177
|
+
const seen = new Set();
|
|
178
|
+
for (const harness of ['claude', 'codex']) {
|
|
179
|
+
if (agent && agent !== harness)
|
|
180
|
+
continue;
|
|
181
|
+
const subdir = harness === 'claude' ? 'projects' : 'sessions';
|
|
182
|
+
const roots = [...getAgentSessionDirs(harness, subdir), ...getRoutineArchiveSessionDirs(harness, subdir)];
|
|
183
|
+
for (const root of roots) {
|
|
184
|
+
for (const file of walkForFilesWithStat(root, '.jsonl', Number.MAX_SAFE_INTEGER)) {
|
|
185
|
+
const name = path.basename(file.path, '.jsonl');
|
|
186
|
+
const id = harness === 'claude' ? name : name.match(/([0-9a-f]{8}-[0-9a-f-]{27})$/i)?.[1];
|
|
187
|
+
if (!id?.toLowerCase().startsWith(selector.toLowerCase()))
|
|
188
|
+
continue;
|
|
189
|
+
const real = fs.realpathSync(file.path);
|
|
190
|
+
if (seen.has(real))
|
|
191
|
+
continue;
|
|
192
|
+
seen.add(real);
|
|
193
|
+
const result = harness === 'claude'
|
|
194
|
+
? await readClaudeMeta(real, id, { fileMtimeMs: Math.floor(file.mtimeMs), fileSize: file.size }, undefined)
|
|
195
|
+
: await readCodexMeta(real);
|
|
196
|
+
if (!result?.meta.id.toLowerCase().startsWith(selector.toLowerCase()) || !result.meta.messageCount)
|
|
197
|
+
continue;
|
|
198
|
+
const sidecar = readSessionActorRecord(result.meta.id);
|
|
199
|
+
const row = {
|
|
200
|
+
...result.meta, accountId: sidecar?.accountId, mode: sidecar?.mode,
|
|
201
|
+
actor: sidecar?.actor, initiatedBy: sidecar?.initiatedBy,
|
|
202
|
+
phoenixId: sidecar?.phoenixId, harness: sidecar?.harness ?? result.meta.harness,
|
|
203
|
+
machine: machineForSessionFile(real, harness),
|
|
204
|
+
};
|
|
205
|
+
if (!matches.has(row.id))
|
|
206
|
+
matches.set(row.id, row);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
return [...matches.values()];
|
|
211
|
+
}
|
|
212
|
+
/** Repair local metadata before deciding that a conversation has no context. */
|
|
213
|
+
export async function hydrateSessionTranscript(session) {
|
|
214
|
+
const accountId = session.accountId ?? readSessionActorRecord(session.id)?.accountId;
|
|
215
|
+
if (accountId && !session.accountId)
|
|
216
|
+
session = { ...session, accountId };
|
|
217
|
+
const file = splitSessionFilePath(session.filePath).container;
|
|
218
|
+
try {
|
|
219
|
+
if (file && fs.statSync(file).isFile() && fs.statSync(file).size > 0)
|
|
220
|
+
return session;
|
|
221
|
+
}
|
|
222
|
+
catch { /* A moved account home needs a canonical rescan. */ }
|
|
223
|
+
if (session.machine && !isSelfHost(session.machine))
|
|
224
|
+
return session;
|
|
225
|
+
if (session.agent === 'claude' || session.agent === 'codex') {
|
|
226
|
+
const local = (await findLocalSessionTranscripts(session.id, session.agent)).find(row => row.id === session.id);
|
|
227
|
+
return local ? { ...session, ...local, accountId: session.accountId ?? local.accountId } : session;
|
|
228
|
+
}
|
|
229
|
+
let { claimed } = await scanSessionsIncremental({ agent: session.agent });
|
|
230
|
+
if (!claimed) {
|
|
231
|
+
if (!await waitForScanToSettle())
|
|
232
|
+
throw new Error(`Transcript verification for ${session.shortId} is incomplete: another index scan is still running. Retry when it finishes.`);
|
|
233
|
+
({ claimed } = await scanSessionsIncremental({ agent: session.agent }));
|
|
234
|
+
if (!claimed)
|
|
235
|
+
throw new Error(`Transcript verification for ${session.shortId} is incomplete: the index is busy. Retry shortly.`);
|
|
236
|
+
}
|
|
237
|
+
const indexed = getSessionById(session.id);
|
|
238
|
+
return indexed ? { ...session, ...indexed, accountId: session.accountId ?? indexed.accountId } : session;
|
|
239
|
+
}
|
|
173
240
|
/** Separate write half so daemon warming does not pay for listing or external enrichment. */
|
|
174
241
|
export async function scanSessionsIncremental(options) {
|
|
175
242
|
// Touch the DB so the schema is ready and connection is cached for this run.
|
|
@@ -410,8 +477,16 @@ export function isManagedSessionFile(filePath) {
|
|
|
410
477
|
path.join(getHistoryDir(), 'backups'),
|
|
411
478
|
// Codex's managed home is not always under versions/. On macOS the versioned path
|
|
412
479
|
// overflows SUN_LEN for codex's control socket, so the shim relocates it to
|
|
413
|
-
// `<agentsUserDir>/.codex-homes/<
|
|
480
|
+
// `<agentsUserDir>/.codex-homes/<key>/` (lib/codex-home.ts) — keyed by version OR
|
|
481
|
+
// by account short key (`a-<accountId prefix>`, lib/codex-home.ts `codexShortKey`).
|
|
414
482
|
path.join(getUserAgentsDir(), '.codex-homes'),
|
|
483
|
+
// Account slots (PHNX-3940): a named account's HOME-shaped dir under
|
|
484
|
+
// `<historyDir>/accounts/<agent>/<accountId>/` (lib/accounts/slots.ts
|
|
485
|
+
// `slotDir`), sharing the one managed install rather than owning a version
|
|
486
|
+
// home of its own. Without this root, every account-slot transcript read as
|
|
487
|
+
// unmanaged the moment any version was managed, hiding a fully registered,
|
|
488
|
+
// runnable account's entire history from the default listing.
|
|
489
|
+
path.join(getHistoryDir(), 'accounts'),
|
|
415
490
|
// Routine archives are agents-cli's OWN run output — managed by definition.
|
|
416
491
|
getRunsDir(),
|
|
417
492
|
];
|
|
@@ -809,21 +884,47 @@ export function getAgentSessionDirs(agent, subdir) {
|
|
|
809
884
|
try {
|
|
810
885
|
for (const version of fs.readdirSync(versionsBase)) {
|
|
811
886
|
addDir(path.join(versionsBase, version, 'home', configDirName, subdir));
|
|
812
|
-
// Codex's managed home is not always where the version layout says. On macOS
|
|
813
|
-
// the versioned path overflows SUN_LEN (104 bytes) for codex's control
|
|
814
|
-
// socket, so the shim relocates the home to
|
|
815
|
-
// `<agentsUserDir>/.codex-homes/<version>/.codex` (lib/codex-home.ts). Every
|
|
816
|
-
// transcript an isolated codex writes lands there, and nothing scanned it —
|
|
817
|
-
// `agents sessions --roots` listed only the user's own ~/.codex, so a managed
|
|
818
|
-
// copy's own history was invisible. addDir skips what does not exist, so this
|
|
819
|
-
// is inert on Linux and for versions that never needed relocating.
|
|
820
|
-
if (agent === 'codex') {
|
|
821
|
-
addDir(path.join(shortCodexHome(getUserAgentsDir(), version), subdir));
|
|
822
|
-
}
|
|
823
887
|
}
|
|
824
888
|
}
|
|
825
889
|
catch { /* dir unreadable */ }
|
|
826
890
|
}
|
|
891
|
+
// Codex's managed home is not always where the version layout says. On macOS
|
|
892
|
+
// the versioned path overflows SUN_LEN (104 bytes) for codex's control socket,
|
|
893
|
+
// so the shim relocates the home to `<agentsUserDir>/.codex-homes/<key>/.codex`
|
|
894
|
+
// (lib/codex-home.ts) — `<key>` is the version for a version home, or
|
|
895
|
+
// `a-<accountId prefix>` (`codexShortKey`) for an account slot under
|
|
896
|
+
// `<historyDir>/accounts/codex/`. Walking `.codex-homes/` directly — rather
|
|
897
|
+
// than deriving keys from the installed-version list — is what catches BOTH:
|
|
898
|
+
// an account short key is not a vendor version and never appears in
|
|
899
|
+
// `versions/codex/`, so iterating only installed versions silently dropped
|
|
900
|
+
// every transcript a codex account slot wrote. addDir skips what does not
|
|
901
|
+
// exist, so this is inert on Linux and for homes that never needed relocating.
|
|
902
|
+
if (agent === 'codex') {
|
|
903
|
+
const codexHomesBase = path.join(getUserAgentsDir(), '.codex-homes');
|
|
904
|
+
try {
|
|
905
|
+
for (const key of fs.readdirSync(codexHomesBase)) {
|
|
906
|
+
addDir(path.join(codexHomesBase, key, '.codex', subdir));
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
catch { /* dir absent or unreadable */ }
|
|
910
|
+
}
|
|
911
|
+
// Account slots (PHNX-3940): a named account gets its own HOME-shaped dir
|
|
912
|
+
// under `<historyDir>/accounts/<agent>/<accountId>/`, sharing the one managed
|
|
913
|
+
// binary install rather than owning a version home of its own
|
|
914
|
+
// (lib/accounts/slots.ts `slotDir`). A slot-launched transcript lives ONLY
|
|
915
|
+
// there — never under `versions/` — so without this root it is fully
|
|
916
|
+
// discoverable by the account machinery (the account is registered and
|
|
917
|
+
// runnable) yet invisible to `agents sessions`, reading as if the history had
|
|
918
|
+
// vanished. `addDir` follows the realpath, so a codex slot whose `.codex` is a
|
|
919
|
+
// symlink onto its `.codex-homes/<key>` short home (SUN_LEN relocation, above)
|
|
920
|
+
// is deduplicated against that same target rather than double-counted.
|
|
921
|
+
const accountsBase = path.join(getHistoryDir(), 'accounts', agent);
|
|
922
|
+
try {
|
|
923
|
+
for (const accountId of fs.readdirSync(accountsBase)) {
|
|
924
|
+
addDir(path.join(accountsBase, accountId, configDirName, subdir));
|
|
925
|
+
}
|
|
926
|
+
}
|
|
927
|
+
catch { /* dir absent or unreadable */ }
|
|
827
928
|
const backupsBase = path.join(getHistoryDir(), 'backups', agent);
|
|
828
929
|
if (fs.existsSync(backupsBase)) {
|
|
829
930
|
try {
|
|
@@ -1180,7 +1281,7 @@ async function readClaudeMeta(filePath, sessionId, scanStamp, priorRow, label) {
|
|
|
1180
1281
|
// Which account produced this transcript. Resolved from the path plus the version
|
|
1181
1282
|
// recorded inside the file, so rows under the mutable ~/.claude symlink are
|
|
1182
1283
|
// attributed to the version that actually wrote them. See claude-accounts.ts.
|
|
1183
|
-
const acct = resolveClaudeAccount(claudeAccountIndex(), filePath, scan.version);
|
|
1284
|
+
const acct = resolveClaudeAccount(claudeAccountIndex(), filePath, scan.version, readSessionActorRecord(sessionId)?.accountId);
|
|
1184
1285
|
let meta;
|
|
1185
1286
|
if (scan.timestamp) {
|
|
1186
1287
|
const cwd = normalizeCwd(scan.cwd || '');
|
|
@@ -4315,7 +4416,7 @@ export function extractVersionFromManagedPath(agent, sourcePath) {
|
|
|
4315
4416
|
if (start === -1)
|
|
4316
4417
|
continue;
|
|
4317
4418
|
const version = normalized.slice(start + marker.length).split('/')[0];
|
|
4318
|
-
if (version)
|
|
4419
|
+
if (version && !(marker.endsWith('/.codex-homes/') && version.startsWith('a-')))
|
|
4319
4420
|
return version;
|
|
4320
4421
|
}
|
|
4321
4422
|
}
|