@phnx-labs/agents-cli 1.22.66 → 1.22.69
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 +56 -0
- package/README.md +21 -8
- package/dist/bootstrap.js +6 -3
- package/dist/commands/browser.js +46 -21
- package/dist/commands/daemon-test-harness.d.ts +1 -1
- package/dist/commands/daemon-test-harness.js +2 -2
- package/dist/commands/daemon.js +21 -22
- package/dist/commands/exec.js +50 -0
- package/dist/commands/feed.js +20 -7
- package/dist/commands/monitors.js +5 -2
- package/dist/commands/projects.d.ts +26 -6
- package/dist/commands/projects.js +55 -22
- package/dist/commands/repo.js +57 -19
- package/dist/commands/resume.d.ts +16 -0
- package/dist/commands/resume.js +41 -8
- package/dist/commands/routines.js +42 -21
- package/dist/commands/send.js +29 -2
- package/dist/commands/sessions-inject.d.ts +58 -0
- package/dist/commands/sessions-inject.js +143 -7
- package/dist/commands/sessions-optimize.js +1 -1
- package/dist/commands/sessions-picker.js +1 -0
- package/dist/commands/sessions.js +4 -11
- package/dist/commands/share.d.ts +5 -3
- package/dist/commands/share.js +73 -20
- package/dist/commands/ssh.js +205 -2
- package/dist/lib/accounting/account-pool-collect.d.ts +6 -4
- package/dist/lib/accounting/account-pool-collect.js +6 -4
- package/dist/lib/accounting/usage-ingest.js +4 -2
- package/dist/lib/accounting/usage-sync.d.ts +38 -93
- package/dist/lib/accounting/usage-sync.js +66 -210
- package/dist/lib/accounting/usage.d.ts +18 -5
- package/dist/lib/accounting/usage.js +165 -20
- package/dist/lib/auth-health.d.ts +8 -0
- package/dist/lib/auth-health.js +4 -4
- package/dist/lib/boot-profile.d.ts +14 -0
- package/dist/lib/boot-profile.js +66 -0
- package/dist/lib/browser/caller-identity.d.ts +12 -0
- package/dist/lib/browser/caller-identity.js +19 -0
- package/dist/lib/browser/ipc.d.ts +37 -32
- package/dist/lib/browser/ipc.js +146 -94
- package/dist/lib/browser/task-index.d.ts +10 -2
- package/dist/lib/browser/task-index.js +22 -3
- package/dist/lib/channels/providers/desktop.d.ts +5 -4
- package/dist/lib/channels/providers/desktop.js +5 -4
- package/dist/lib/claude-account-token.js +108 -4
- package/dist/lib/daemon/account-state-daemon-service.d.ts +49 -9
- package/dist/lib/daemon/account-state-daemon-service.js +81 -18
- package/dist/lib/daemon/auth-sync-service.d.ts +4 -4
- package/dist/lib/daemon/auth-sync-service.js +17 -6
- package/dist/lib/daemon/catchup-service.d.ts +51 -0
- package/dist/lib/daemon/catchup-service.js +51 -0
- package/dist/lib/daemon/daemon.d.ts +12 -22
- package/dist/lib/daemon/daemon.js +463 -176
- package/dist/lib/daemon/runner.js +2 -0
- package/dist/lib/daemon/service.d.ts +22 -4
- package/dist/lib/daemon/service.js +2 -2
- package/dist/lib/daemon/supervisor.d.ts +55 -15
- package/dist/lib/daemon/supervisor.js +119 -29
- package/dist/lib/daemon/usage-sync-service.d.ts +4 -6
- package/dist/lib/daemon/usage-sync-service.js +22 -18
- package/dist/lib/daemon-health.js +36 -31
- package/dist/lib/daemon-services.d.ts +1 -1
- package/dist/lib/daemon-services.js +12 -2
- package/dist/lib/daemon-ticks.d.ts +9 -6
- package/dist/lib/daemon-ticks.js +14 -8
- package/dist/lib/devices/health.d.ts +38 -2
- package/dist/lib/devices/health.js +43 -5
- package/dist/lib/devices/registry.js +2 -0
- package/dist/lib/devices/worker-pick.d.ts +1 -1
- package/dist/lib/devices/worker-pick.js +4 -1
- package/dist/lib/exec.js +15 -0
- package/dist/lib/feed/watch.d.ts +3 -0
- package/dist/lib/feed/watch.js +13 -3
- package/dist/lib/feed-broadcast.d.ts +64 -5
- package/dist/lib/feed-broadcast.js +124 -22
- package/dist/lib/fleet-shared-repo-sync.d.ts +36 -0
- package/dist/lib/fleet-shared-repo-sync.js +333 -0
- package/dist/lib/fleet-shared-state.d.ts +38 -0
- package/dist/lib/fleet-shared-state.js +105 -0
- package/dist/lib/hosts/remote-cmd.d.ts +2 -0
- package/dist/lib/hosts/remote-cmd.js +12 -3
- package/dist/lib/lock-compromise.d.ts +8 -0
- package/dist/lib/lock-compromise.js +12 -0
- package/dist/lib/monitors/engine.d.ts +2 -1
- package/dist/lib/monitors/engine.js +27 -2
- package/dist/lib/monitors/sources/command.js +13 -3
- package/dist/lib/monitors/sources/failure.d.ts +32 -0
- package/dist/lib/monitors/sources/failure.js +52 -0
- package/dist/lib/monitors/sources/types.d.ts +9 -0
- package/dist/lib/owner-message.d.ts +12 -0
- package/dist/lib/owner-message.js +44 -0
- package/dist/lib/refresh-coordinator.js +2 -0
- package/dist/lib/run-trace-sync.d.ts +28 -0
- package/dist/lib/run-trace-sync.js +99 -0
- package/dist/lib/secrets/filestore.d.ts +4 -0
- package/dist/lib/secrets/filestore.js +164 -3
- package/dist/lib/secrets/push.d.ts +10 -0
- package/dist/lib/secrets/push.js +86 -7
- package/dist/lib/secrets/remote.d.ts +18 -6
- package/dist/lib/secrets/remote.js +29 -4
- package/dist/lib/secrets/reserved-sync.d.ts +28 -27
- package/dist/lib/secrets/reserved-sync.js +119 -101
- package/dist/lib/session/active.d.ts +13 -1
- package/dist/lib/session/active.js +5 -0
- package/dist/lib/session/actor-sidecar.d.ts +12 -0
- package/dist/lib/session/actor-sidecar.js +2 -0
- package/dist/lib/session/db.d.ts +16 -2
- package/dist/lib/session/db.js +73 -22
- package/dist/lib/session/discover.d.ts +12 -3
- package/dist/lib/session/discover.js +187 -26
- package/dist/lib/session/linear.d.ts +13 -0
- package/dist/lib/session/linear.js +44 -0
- package/dist/lib/session/live-metadata.js +1 -0
- package/dist/lib/session/parse.js +2 -3
- package/dist/lib/session/prompt.d.ts +17 -0
- package/dist/lib/session/prompt.js +35 -0
- package/dist/lib/session/recovery.d.ts +43 -6
- package/dist/lib/session/recovery.js +80 -10
- package/dist/lib/session/remote/remote-list.d.ts +17 -1
- package/dist/lib/session/remote/remote-list.js +29 -4
- package/dist/lib/session/remote/watch.d.ts +25 -2
- package/dist/lib/session/remote/watch.js +188 -11
- package/dist/lib/session/session-cache.d.ts +2 -1
- package/dist/lib/session/session-cache.js +1 -0
- package/dist/lib/session/state.js +11 -13
- package/dist/lib/session/types.d.ts +2 -0
- package/dist/lib/share/backend.d.ts +2 -2
- package/dist/lib/share/backend.js +20 -9
- package/dist/lib/share/delete.d.ts +5 -1
- package/dist/lib/share/delete.js +7 -2
- package/dist/lib/share/http-error.d.ts +52 -0
- package/dist/lib/share/http-error.js +65 -0
- package/dist/lib/share/publish.d.ts +67 -11
- package/dist/lib/share/publish.js +98 -16
- package/dist/lib/share/worker-template.js +105 -9
- package/dist/lib/smart-launch.js +27 -4
- package/dist/lib/ssh-exec.d.ts +2 -0
- package/dist/lib/ssh-exec.js +20 -4
- package/dist/lib/storage/index.d.ts +14 -0
- package/dist/lib/storage/index.js +14 -0
- package/dist/lib/storage/selection.d.ts +48 -0
- package/dist/lib/storage/selection.js +39 -0
- package/dist/lib/storage/visibility.d.ts +82 -0
- package/dist/lib/storage/visibility.js +99 -0
- package/dist/lib/teams/agents.js +3 -1
- package/dist/lib/teams/placement-probe.js +1 -0
- package/dist/lib/teams/registry.js +2 -0
- package/dist/lib/teams/scheduler.d.ts +8 -1
- package/dist/lib/teams/scheduler.js +4 -1
- package/dist/lib/testdata/daemon-health-writer.d.ts +1 -0
- package/dist/lib/testdata/daemon-health-writer.js +8 -0
- package/dist/lib/traces/backend.js +13 -2
- package/dist/lib/traces/sync.d.ts +7 -0
- package/dist/lib/traces/sync.js +9 -0
- package/dist/lib/usage-refresh.d.ts +8 -2
- package/dist/lib/usage-refresh.js +3 -3
- package/dist/lib/worktree/held.d.ts +166 -0
- package/dist/lib/worktree/held.js +368 -0
- package/package.json +2 -2
- package/dist/lib/account-state-service.d.ts +0 -21
- package/dist/lib/account-state-service.js +0 -60
|
@@ -71,6 +71,7 @@ interface ClaudeSessionScan {
|
|
|
71
71
|
version?: string;
|
|
72
72
|
model?: string;
|
|
73
73
|
topic?: string;
|
|
74
|
+
firstUserMessage?: string;
|
|
74
75
|
/** Harness-owned session name (`/rename` or Claude's generated `ai-title`). */
|
|
75
76
|
label?: string;
|
|
76
77
|
messageCount: number;
|
|
@@ -135,6 +136,7 @@ interface CodexSessionScan {
|
|
|
135
136
|
version?: string;
|
|
136
137
|
model?: string;
|
|
137
138
|
topic?: string;
|
|
139
|
+
firstUserMessage?: string;
|
|
138
140
|
messageCount: number;
|
|
139
141
|
tokenCount?: number;
|
|
140
142
|
/** Real generated (output) tokens, excluding cache-read/-write context. */
|
|
@@ -418,6 +420,7 @@ interface ClaudeParseState {
|
|
|
418
420
|
version?: string;
|
|
419
421
|
model?: string;
|
|
420
422
|
topic?: string;
|
|
423
|
+
firstUserMessage?: string;
|
|
421
424
|
customTitle?: string;
|
|
422
425
|
aiTitle?: string;
|
|
423
426
|
entrypoint?: string;
|
|
@@ -492,7 +495,7 @@ export declare function scanClaudeSession(filePath: string): Promise<ClaudeSessi
|
|
|
492
495
|
* exact even when the recent window is smaller than the true count.
|
|
493
496
|
*/
|
|
494
497
|
export interface ClaudeParserState {
|
|
495
|
-
v:
|
|
498
|
+
v: 6;
|
|
496
499
|
/**
|
|
497
500
|
* Fan-out tallies carried across a RESUMED parse (RUSH-3091/3095). They must
|
|
498
501
|
* live in the durable blob: the resumable parser reads only new bytes, so
|
|
@@ -512,6 +515,7 @@ export interface ClaudeParserState {
|
|
|
512
515
|
entrypoint?: string;
|
|
513
516
|
firstTsMs?: number;
|
|
514
517
|
topic?: string;
|
|
518
|
+
firstUserMessage?: string;
|
|
515
519
|
customTitle?: string;
|
|
516
520
|
aiTitle?: string;
|
|
517
521
|
plan?: string;
|
|
@@ -629,6 +633,7 @@ interface CodexParseState {
|
|
|
629
633
|
version?: string;
|
|
630
634
|
model?: string;
|
|
631
635
|
topic?: string;
|
|
636
|
+
firstUserMessage?: string;
|
|
632
637
|
messageCount: number;
|
|
633
638
|
tokenCount?: number;
|
|
634
639
|
lastTotalTokenUsage?: any;
|
|
@@ -667,7 +672,7 @@ export declare function initCodexParseState(): CodexParseState;
|
|
|
667
672
|
* finalize is identical after a resume.
|
|
668
673
|
*/
|
|
669
674
|
export interface CodexParserState {
|
|
670
|
-
v:
|
|
675
|
+
v: 4;
|
|
671
676
|
offset: number;
|
|
672
677
|
jsonlDroppingOversizedLine?: boolean;
|
|
673
678
|
sessionId?: string;
|
|
@@ -677,6 +682,7 @@ export interface CodexParserState {
|
|
|
677
682
|
version?: string;
|
|
678
683
|
model?: string;
|
|
679
684
|
topic?: string;
|
|
685
|
+
firstUserMessage?: string;
|
|
680
686
|
messageCount: number;
|
|
681
687
|
tokenCount?: number;
|
|
682
688
|
lastTotalTokenUsage?: any;
|
|
@@ -743,6 +749,8 @@ export declare function __codexScanBranchCountsForTest(): {
|
|
|
743
749
|
};
|
|
744
750
|
/** Test seam: reset the Codex parse-branch counters to observe a scan from a clean slate. */
|
|
745
751
|
export declare function __resetCodexScanBranchCountsForTest(): void;
|
|
752
|
+
/** Extract the version number from a managed versions/<agent>/<version>/... path under either repo. */
|
|
753
|
+
export declare function extractVersionFromManagedPath(agent: SessionAgentId, sourcePath?: string): string | undefined;
|
|
746
754
|
/** Parse one Cursor transcript and enrich it with the matching chat meta.json. */
|
|
747
755
|
export declare function readCursorMeta(filePath: string, currentVersion?: string): {
|
|
748
756
|
meta: SessionMeta;
|
|
@@ -765,11 +773,12 @@ export declare function readKimiMeta(filePath: string, priorRow?: {
|
|
|
765
773
|
* from `offset` + adding the appended tail's deltas equals a full parse.
|
|
766
774
|
*/
|
|
767
775
|
export interface KimiParserState {
|
|
768
|
-
v:
|
|
776
|
+
v: 2;
|
|
769
777
|
offset: number;
|
|
770
778
|
messageCount: number;
|
|
771
779
|
tokenCount: number;
|
|
772
780
|
outputTokens: number;
|
|
781
|
+
firstUserMessage?: string;
|
|
773
782
|
}
|
|
774
783
|
/**
|
|
775
784
|
* Incrementally parse Kimi's wire.jsonl for message-count and token counters,
|
|
@@ -26,7 +26,7 @@ import { getConfigSymlinkVersion } from '../installations/shims.js';
|
|
|
26
26
|
import { SESSION_AGENTS } from './types.js';
|
|
27
27
|
import { deriveShortId } from './short-id.js';
|
|
28
28
|
import { buildClaudeAccountIndex, resolveClaudeAccount } from './claude-accounts.js';
|
|
29
|
-
import { extractSessionTopic, extractSlashCommandName, extractSlashCommandFromToolInput, cleanGeneratedSessionLabel } from './prompt.js';
|
|
29
|
+
import { cleanFirstUserMessage, extractSessionTopic, extractSlashCommandName, extractSlashCommandFromToolInput, cleanGeneratedSessionLabel } from './prompt.js';
|
|
30
30
|
import { isBackgroundShellStart, isSkillInvocation, extractSkills, extractSlashCommands, isSubAgentTool } from './highlights.js';
|
|
31
31
|
import { parseAntigravity, parseCursor, splitSessionFilePath } from './parse.js';
|
|
32
32
|
import { extractPrUrl, detectWorktree, detectTicket, isPrCreateCommand, detectSpawnedTeam, isTicketCreateTool, extractCreatedTicket, extractRecentDirectoriesTouched, extractTodoProgressFromEvents } from './state.js';
|
|
@@ -1200,6 +1200,7 @@ async function readClaudeMeta(filePath, sessionId, scanStamp, priorRow, label) {
|
|
|
1200
1200
|
accountKey: acct.key,
|
|
1201
1201
|
accountOrg: acct.orgName ?? undefined,
|
|
1202
1202
|
topic: scan.topic,
|
|
1203
|
+
firstUserMessage: scan.firstUserMessage,
|
|
1203
1204
|
// The live sessions metadata file contains only user `/rename` values;
|
|
1204
1205
|
// prefer it when present, then fall back to the title parsed from JSONL.
|
|
1205
1206
|
label: label || scan.label,
|
|
@@ -1256,6 +1257,7 @@ async function readClaudeMeta(filePath, sessionId, scanStamp, priorRow, label) {
|
|
|
1256
1257
|
costUsdNoCache: scan.costUsdNoCache,
|
|
1257
1258
|
durationMs: scan.durationMs,
|
|
1258
1259
|
topic: scan.topic,
|
|
1260
|
+
firstUserMessage: scan.firstUserMessage,
|
|
1259
1261
|
isTeamOrigin,
|
|
1260
1262
|
prUrl: scan.prUrl,
|
|
1261
1263
|
prNumber: scan.prNumber,
|
|
@@ -1565,6 +1567,7 @@ export async function readCodexMeta(filePath, resolveAccount, currentVersion, sc
|
|
|
1565
1567
|
version: resolveSessionVersion('codex', filePath, scan.version, currentVersion),
|
|
1566
1568
|
model: scan.model,
|
|
1567
1569
|
topic: scan.topic,
|
|
1570
|
+
firstUserMessage: scan.firstUserMessage,
|
|
1568
1571
|
messageCount: scan.messageCount,
|
|
1569
1572
|
tokenCount: scan.tokenCount,
|
|
1570
1573
|
outputTokens: scan.outputTokens,
|
|
@@ -1804,6 +1807,7 @@ function readGeminiMeta(filePath, hashDir, projectMap, currentVersion) {
|
|
|
1804
1807
|
version: resolveSessionVersion('gemini', filePath, embeddedVersion, currentVersion),
|
|
1805
1808
|
model: sessionModel,
|
|
1806
1809
|
topic,
|
|
1810
|
+
firstUserMessage: userTexts.map(cleanFirstUserMessage).find((text) => !!text),
|
|
1807
1811
|
messageCount,
|
|
1808
1812
|
tokenCount: sawTokenCount ? tokenCount : undefined,
|
|
1809
1813
|
outputTokens: sawTokenCount ? outputTokens : undefined,
|
|
@@ -2046,7 +2050,13 @@ async function scanOpenCodeIncremental(onProgress) {
|
|
|
2046
2050
|
fileSize: stat.size,
|
|
2047
2051
|
};
|
|
2048
2052
|
const prev = getScanStampByPath(OPENCODE_DB);
|
|
2049
|
-
|
|
2053
|
+
const extractorUpgrade = prev != null && prev.extractorVersion !== CONTENT_INDEX_VERSION;
|
|
2054
|
+
// OpenCode has a shared database rather than one transcript file per session,
|
|
2055
|
+
// so this container-level gate must honor extractor-version invalidation too.
|
|
2056
|
+
if (prev &&
|
|
2057
|
+
prev.fileMtimeMs === currentScan.fileMtimeMs &&
|
|
2058
|
+
prev.fileSize === currentScan.fileSize &&
|
|
2059
|
+
prev.extractorVersion === CONTENT_INDEX_VERSION) {
|
|
2050
2060
|
return;
|
|
2051
2061
|
}
|
|
2052
2062
|
const currentVersion = await getCurrentAgentVersion('opencode');
|
|
@@ -2123,7 +2133,7 @@ async function scanOpenCodeIncremental(onProgress) {
|
|
|
2123
2133
|
) stats ON stats.session_id = s.id
|
|
2124
2134
|
WHERE s.parent_id IS NULL
|
|
2125
2135
|
ORDER BY time_created DESC
|
|
2126
|
-
LIMIT 1000;
|
|
2136
|
+
${extractorUpgrade ? '' : 'LIMIT 1000'};
|
|
2127
2137
|
`.replace(/\n/g, ' ');
|
|
2128
2138
|
const account = getOpenCodeAccount();
|
|
2129
2139
|
const rows = db.prepare(query).all();
|
|
@@ -2154,11 +2164,31 @@ async function scanOpenCodeIncremental(onProgress) {
|
|
|
2154
2164
|
const prevStamp = priorStamps.get(c.filePath);
|
|
2155
2165
|
return !prevStamp
|
|
2156
2166
|
|| prevStamp.fileMtimeMs !== c.scan.fileMtimeMs
|
|
2157
|
-
|| prevStamp.fileSize !== c.scan.fileSize
|
|
2167
|
+
|| prevStamp.fileSize !== c.scan.fileSize
|
|
2168
|
+
|| prevStamp.extractorVersion !== CONTENT_INDEX_VERSION;
|
|
2158
2169
|
});
|
|
2170
|
+
// Role lives on the message row and the genuine user text lives on its
|
|
2171
|
+
// ordered text parts. Poisoned JSON is ignored per row, never per database.
|
|
2172
|
+
const firstUserStmt = db.prepare(`
|
|
2173
|
+
SELECT json_extract(p.data, '$.text') AS text
|
|
2174
|
+
FROM part p
|
|
2175
|
+
WHERE p.message_id = (
|
|
2176
|
+
SELECT m.id FROM message m
|
|
2177
|
+
WHERE m.session_id = ?
|
|
2178
|
+
AND json_valid(m.data) AND json_extract(m.data, '$.role') = 'user'
|
|
2179
|
+
ORDER BY m.time_created ASC
|
|
2180
|
+
LIMIT 1
|
|
2181
|
+
)
|
|
2182
|
+
AND json_valid(p.data) AND json_extract(p.data, '$.type') = 'text'
|
|
2183
|
+
ORDER BY p.time_created ASC
|
|
2184
|
+
`);
|
|
2159
2185
|
const entries = [];
|
|
2160
2186
|
for (const { row, id, filePath, scan } of changed) {
|
|
2161
2187
|
const title = typeof row.title === 'string' ? row.title : '';
|
|
2188
|
+
const firstUserMessage = firstUserStmt.all(id)
|
|
2189
|
+
.map((part) => (typeof part.text === 'string' ? part.text : ''))
|
|
2190
|
+
.join('')
|
|
2191
|
+
.trim() || undefined;
|
|
2162
2192
|
const directory = typeof row.directory === 'string' ? row.directory : '';
|
|
2163
2193
|
const version = typeof row.version === 'string' ? row.version : '';
|
|
2164
2194
|
const timeCreated = asInt(row.time_created);
|
|
@@ -2212,6 +2242,7 @@ async function scanOpenCodeIncremental(onProgress) {
|
|
|
2212
2242
|
version: resolveSessionVersion('opencode', OPENCODE_DB, version || undefined, currentVersion),
|
|
2213
2243
|
account,
|
|
2214
2244
|
topic,
|
|
2245
|
+
firstUserMessage,
|
|
2215
2246
|
model,
|
|
2216
2247
|
costUsd,
|
|
2217
2248
|
durationMs,
|
|
@@ -2420,6 +2451,7 @@ async function readRushMeta(filePath, sessionId) {
|
|
|
2420
2451
|
project: scan.agentId,
|
|
2421
2452
|
filePath,
|
|
2422
2453
|
topic: scan.topic,
|
|
2454
|
+
firstUserMessage: scan.firstUserMessage,
|
|
2423
2455
|
messageCount: scan.messageCount,
|
|
2424
2456
|
};
|
|
2425
2457
|
return { meta, content: scan.contentText || '', assistantContent: scan.assistantText || '' };
|
|
@@ -2430,6 +2462,7 @@ async function scanRushSession(filePath) {
|
|
|
2430
2462
|
const rl = readline.createInterface({ input: stream, crlfDelay: Infinity });
|
|
2431
2463
|
let timestamp;
|
|
2432
2464
|
let topic;
|
|
2465
|
+
let firstUserMessage;
|
|
2433
2466
|
let agentId;
|
|
2434
2467
|
let messageCount = 0;
|
|
2435
2468
|
const userTexts = [];
|
|
@@ -2467,6 +2500,8 @@ async function scanRushSession(filePath) {
|
|
|
2467
2500
|
messageCount++;
|
|
2468
2501
|
if (parsed.role === 'user') {
|
|
2469
2502
|
userTexts.push(cleaned);
|
|
2503
|
+
if (!firstUserMessage)
|
|
2504
|
+
firstUserMessage = cleanFirstUserMessage(cleaned);
|
|
2470
2505
|
if (!topic)
|
|
2471
2506
|
topic = extractSessionTopic(cleaned);
|
|
2472
2507
|
}
|
|
@@ -2482,6 +2517,7 @@ async function scanRushSession(filePath) {
|
|
|
2482
2517
|
return {
|
|
2483
2518
|
timestamp,
|
|
2484
2519
|
topic,
|
|
2520
|
+
firstUserMessage,
|
|
2485
2521
|
agentId,
|
|
2486
2522
|
messageCount,
|
|
2487
2523
|
contentText: userTexts.length > 0 ? userTexts.join('\n') : undefined,
|
|
@@ -2559,6 +2595,7 @@ function readHermesMeta(filePath) {
|
|
|
2559
2595
|
const userTexts = [];
|
|
2560
2596
|
const assistantTexts = [];
|
|
2561
2597
|
let topic;
|
|
2598
|
+
let firstUserMessage;
|
|
2562
2599
|
let messageCount = 0;
|
|
2563
2600
|
for (const msg of messages) {
|
|
2564
2601
|
const text = extractHermesMessageText(msg?.content);
|
|
@@ -2567,6 +2604,8 @@ function readHermesMeta(filePath) {
|
|
|
2567
2604
|
messageCount++;
|
|
2568
2605
|
if (msg?.role === 'user') {
|
|
2569
2606
|
userTexts.push(text);
|
|
2607
|
+
if (!firstUserMessage)
|
|
2608
|
+
firstUserMessage = cleanFirstUserMessage(text);
|
|
2570
2609
|
if (!topic)
|
|
2571
2610
|
topic = extractSessionTopic(text);
|
|
2572
2611
|
}
|
|
@@ -2593,6 +2632,7 @@ function readHermesMeta(filePath) {
|
|
|
2593
2632
|
version: model,
|
|
2594
2633
|
model,
|
|
2595
2634
|
topic,
|
|
2635
|
+
firstUserMessage,
|
|
2596
2636
|
messageCount: messageCount || (typeof session.message_count === 'number' ? session.message_count : undefined),
|
|
2597
2637
|
};
|
|
2598
2638
|
return { meta, content: userTexts.join('\n'), assistantContent: assistantTexts.join('\n') };
|
|
@@ -2695,6 +2735,7 @@ function readMuseMeta(filePath) {
|
|
|
2695
2735
|
const userTexts = [];
|
|
2696
2736
|
const assistantTexts = [];
|
|
2697
2737
|
let topic;
|
|
2738
|
+
let firstUserMessage;
|
|
2698
2739
|
let messageCount = 0;
|
|
2699
2740
|
let model;
|
|
2700
2741
|
let project;
|
|
@@ -2731,6 +2772,8 @@ function readMuseMeta(filePath) {
|
|
|
2731
2772
|
if (cmd?.kind === 'turn_submit' && typeof cmd.prompt === 'string' && cmd.prompt.trim()) {
|
|
2732
2773
|
messageCount++;
|
|
2733
2774
|
userTexts.push(cmd.prompt.trim());
|
|
2775
|
+
if (!firstUserMessage)
|
|
2776
|
+
firstUserMessage = cleanFirstUserMessage(cmd.prompt.trim());
|
|
2734
2777
|
if (!topic)
|
|
2735
2778
|
topic = extractSessionTopic(cmd.prompt.trim());
|
|
2736
2779
|
}
|
|
@@ -2762,6 +2805,7 @@ function readMuseMeta(filePath) {
|
|
|
2762
2805
|
filePath,
|
|
2763
2806
|
model,
|
|
2764
2807
|
topic,
|
|
2808
|
+
firstUserMessage,
|
|
2765
2809
|
messageCount: messageCount || undefined,
|
|
2766
2810
|
};
|
|
2767
2811
|
return { meta, content: userTexts.join('\n'), assistantContent: assistantTexts.join('\n') };
|
|
@@ -2865,6 +2909,7 @@ async function readDroidMeta(filePath, currentVersion) {
|
|
|
2865
2909
|
version: resolveSessionVersion('droid', filePath, undefined, currentVersion),
|
|
2866
2910
|
model,
|
|
2867
2911
|
topic: scan.topic,
|
|
2912
|
+
firstUserMessage: scan.firstUserMessage,
|
|
2868
2913
|
messageCount: scan.messageCount,
|
|
2869
2914
|
tokenCount,
|
|
2870
2915
|
outputTokens: settings.usage?.outputTokens,
|
|
@@ -2913,6 +2958,7 @@ async function scanDroidSession(filePath) {
|
|
|
2913
2958
|
let title;
|
|
2914
2959
|
let sessionTitle;
|
|
2915
2960
|
let firstUserTopic;
|
|
2961
|
+
let firstUserMessage;
|
|
2916
2962
|
let model;
|
|
2917
2963
|
let messageCount = 0;
|
|
2918
2964
|
let firstTsMs;
|
|
@@ -2965,6 +3011,8 @@ async function scanDroidSession(filePath) {
|
|
|
2965
3011
|
messageCount++;
|
|
2966
3012
|
if (msg.role === 'user') {
|
|
2967
3013
|
userTexts.push(text);
|
|
3014
|
+
if (!firstUserMessage)
|
|
3015
|
+
firstUserMessage = cleanFirstUserMessage(text);
|
|
2968
3016
|
if (!firstUserTopic)
|
|
2969
3017
|
firstUserTopic = extractSessionTopic(text);
|
|
2970
3018
|
}
|
|
@@ -2987,6 +3035,7 @@ async function scanDroidSession(filePath) {
|
|
|
2987
3035
|
// Prefer Droid's auto-title, then the raw first-prompt title, then the
|
|
2988
3036
|
// derived first-user-message topic.
|
|
2989
3037
|
topic: sessionTitle || title || firstUserTopic,
|
|
3038
|
+
firstUserMessage,
|
|
2990
3039
|
model,
|
|
2991
3040
|
messageCount,
|
|
2992
3041
|
durationMs,
|
|
@@ -3020,6 +3069,7 @@ export function initClaudeParseState() {
|
|
|
3020
3069
|
version: undefined,
|
|
3021
3070
|
model: undefined,
|
|
3022
3071
|
topic: undefined,
|
|
3072
|
+
firstUserMessage: undefined,
|
|
3023
3073
|
customTitle: undefined,
|
|
3024
3074
|
aiTitle: undefined,
|
|
3025
3075
|
entrypoint: undefined,
|
|
@@ -3202,6 +3252,8 @@ export function applyClaudeLine(state, parsed) {
|
|
|
3202
3252
|
if (parsed.type === 'user') {
|
|
3203
3253
|
const text = extractClaudeUserText(parsed);
|
|
3204
3254
|
if (text) {
|
|
3255
|
+
if (!state.firstUserMessage)
|
|
3256
|
+
state.firstUserMessage = cleanFirstUserMessage(text);
|
|
3205
3257
|
state.messageCount++;
|
|
3206
3258
|
state.userTexts.push(text);
|
|
3207
3259
|
if (!state.topic)
|
|
@@ -3297,6 +3349,7 @@ export function finalizeClaudeScan(state) {
|
|
|
3297
3349
|
version: state.version,
|
|
3298
3350
|
model: state.model,
|
|
3299
3351
|
topic: state.topic,
|
|
3352
|
+
firstUserMessage: state.firstUserMessage,
|
|
3300
3353
|
label,
|
|
3301
3354
|
entrypoint: state.entrypoint,
|
|
3302
3355
|
messageCount: state.messageCount,
|
|
@@ -3366,7 +3419,7 @@ export function serializeClaudeParserState(state, offset, jsonlDroppingOversized
|
|
|
3366
3419
|
: allIds;
|
|
3367
3420
|
const ticket = detectTicket(state.userTexts.join('\n') || undefined, state.gitBranch);
|
|
3368
3421
|
return {
|
|
3369
|
-
v:
|
|
3422
|
+
v: 6,
|
|
3370
3423
|
subAgents: state.subAgents,
|
|
3371
3424
|
backgroundShells: state.backgroundShells,
|
|
3372
3425
|
offset,
|
|
@@ -3379,6 +3432,7 @@ export function serializeClaudeParserState(state, offset, jsonlDroppingOversized
|
|
|
3379
3432
|
entrypoint: state.entrypoint,
|
|
3380
3433
|
firstTsMs: state.firstTsMs,
|
|
3381
3434
|
topic: state.topic,
|
|
3435
|
+
firstUserMessage: state.firstUserMessage,
|
|
3382
3436
|
customTitle: state.customTitle,
|
|
3383
3437
|
aiTitle: state.aiTitle,
|
|
3384
3438
|
plan: state.plan,
|
|
@@ -3449,6 +3503,7 @@ export function hydrateClaudeParseState(prior) {
|
|
|
3449
3503
|
version: prior.version,
|
|
3450
3504
|
model: prior.model,
|
|
3451
3505
|
topic: prior.topic,
|
|
3506
|
+
firstUserMessage: prior.firstUserMessage,
|
|
3452
3507
|
customTitle: prior.customTitle,
|
|
3453
3508
|
aiTitle: prior.aiTitle,
|
|
3454
3509
|
entrypoint: prior.entrypoint,
|
|
@@ -3613,7 +3668,7 @@ function parsePriorClaudeState(row) {
|
|
|
3613
3668
|
return null;
|
|
3614
3669
|
try {
|
|
3615
3670
|
const parsed = JSON.parse(row.parserState);
|
|
3616
|
-
if (parsed?.v !==
|
|
3671
|
+
if (parsed?.v !== 6 || typeof parsed.offset !== 'number' || parsed.toolCalls?.v !== 1)
|
|
3617
3672
|
return null;
|
|
3618
3673
|
return parsed;
|
|
3619
3674
|
}
|
|
@@ -3644,6 +3699,7 @@ export function initCodexParseState() {
|
|
|
3644
3699
|
version: undefined,
|
|
3645
3700
|
model: undefined,
|
|
3646
3701
|
topic: undefined,
|
|
3702
|
+
firstUserMessage: undefined,
|
|
3647
3703
|
messageCount: 0,
|
|
3648
3704
|
tokenCount: undefined,
|
|
3649
3705
|
lastTotalTokenUsage: undefined,
|
|
@@ -3748,7 +3804,8 @@ function applyCodexLine(state, parsed) {
|
|
|
3748
3804
|
return;
|
|
3749
3805
|
}
|
|
3750
3806
|
if (parsed.type === 'response_item' && parsed.payload?.type === 'message') {
|
|
3751
|
-
const
|
|
3807
|
+
const payloadRole = parsed.payload.role;
|
|
3808
|
+
const role = payloadRole === 'user' || payloadRole === 'developer'
|
|
3752
3809
|
? 'user'
|
|
3753
3810
|
: 'assistant';
|
|
3754
3811
|
const text = extractCodexMessageText(parsed.payload.content, role);
|
|
@@ -3756,9 +3813,17 @@ function applyCodexLine(state, parsed) {
|
|
|
3756
3813
|
return;
|
|
3757
3814
|
state.messageCount++;
|
|
3758
3815
|
if (role === 'user') {
|
|
3759
|
-
|
|
3816
|
+
const genuine = cleanFirstUserMessage(text);
|
|
3817
|
+
if (!genuine)
|
|
3818
|
+
return;
|
|
3819
|
+
state.userTexts.push(genuine);
|
|
3820
|
+
// Codex records developer instructions as response_item messages too.
|
|
3821
|
+
// They remain searchable for compatibility, but only an actual user role
|
|
3822
|
+
// may become the session's first request.
|
|
3823
|
+
if (payloadRole === 'user' && !state.firstUserMessage)
|
|
3824
|
+
state.firstUserMessage = genuine;
|
|
3760
3825
|
if (!state.topic)
|
|
3761
|
-
state.topic = extractSessionTopic(
|
|
3826
|
+
state.topic = extractSessionTopic(genuine);
|
|
3762
3827
|
}
|
|
3763
3828
|
else {
|
|
3764
3829
|
state.assistantTexts.push(text);
|
|
@@ -3827,6 +3892,7 @@ function finalizeCodexScan(state) {
|
|
|
3827
3892
|
version: state.version,
|
|
3828
3893
|
model: state.model,
|
|
3829
3894
|
topic: state.topic,
|
|
3895
|
+
firstUserMessage: state.firstUserMessage,
|
|
3830
3896
|
messageCount: state.messageCount,
|
|
3831
3897
|
tokenCount: state.tokenCount,
|
|
3832
3898
|
outputTokens,
|
|
@@ -3882,7 +3948,7 @@ async function scanCodexSession(filePath) {
|
|
|
3882
3948
|
export function serializeCodexParserState(state, offset, jsonlDroppingOversizedLine = false) {
|
|
3883
3949
|
const ticket = detectTicket(state.userTexts.join('\n') || undefined, state.gitBranch);
|
|
3884
3950
|
return {
|
|
3885
|
-
v:
|
|
3951
|
+
v: 4,
|
|
3886
3952
|
offset,
|
|
3887
3953
|
jsonlDroppingOversizedLine: jsonlDroppingOversizedLine || undefined,
|
|
3888
3954
|
sessionId: state.sessionId,
|
|
@@ -3892,6 +3958,7 @@ export function serializeCodexParserState(state, offset, jsonlDroppingOversizedL
|
|
|
3892
3958
|
version: state.version,
|
|
3893
3959
|
model: state.model,
|
|
3894
3960
|
topic: state.topic,
|
|
3961
|
+
firstUserMessage: state.firstUserMessage,
|
|
3895
3962
|
messageCount: state.messageCount,
|
|
3896
3963
|
tokenCount: state.tokenCount,
|
|
3897
3964
|
lastTotalTokenUsage: state.lastTotalTokenUsage,
|
|
@@ -3934,6 +4001,7 @@ function hydrateCodexParseState(prior) {
|
|
|
3934
4001
|
version: prior.version,
|
|
3935
4002
|
model: prior.model,
|
|
3936
4003
|
topic: prior.topic,
|
|
4004
|
+
firstUserMessage: prior.firstUserMessage,
|
|
3937
4005
|
messageCount: prior.messageCount,
|
|
3938
4006
|
tokenCount: prior.tokenCount,
|
|
3939
4007
|
lastTotalTokenUsage: prior.lastTotalTokenUsage,
|
|
@@ -4073,7 +4141,7 @@ function parsePriorCodexState(row) {
|
|
|
4073
4141
|
return null;
|
|
4074
4142
|
try {
|
|
4075
4143
|
const parsed = JSON.parse(row.parserState);
|
|
4076
|
-
if (parsed?.v !==
|
|
4144
|
+
if (parsed?.v !== 4 || typeof parsed.offset !== 'number' || parsed.toolCalls?.v !== 1)
|
|
4077
4145
|
return null;
|
|
4078
4146
|
return parsed;
|
|
4079
4147
|
}
|
|
@@ -4208,7 +4276,7 @@ function getClaudeUsageTotal(usage) {
|
|
|
4208
4276
|
usage.cache_read_input_tokens,
|
|
4209
4277
|
]);
|
|
4210
4278
|
}
|
|
4211
|
-
/** Extract text from Codex message content blocks
|
|
4279
|
+
/** Extract text from Codex message content blocks; the caller classifies user scaffolding. */
|
|
4212
4280
|
function extractCodexMessageText(contentBlocks, role) {
|
|
4213
4281
|
if (!Array.isArray(contentBlocks))
|
|
4214
4282
|
return undefined;
|
|
@@ -4217,14 +4285,7 @@ function extractCodexMessageText(contentBlocks, role) {
|
|
|
4217
4285
|
: contentBlocks.filter((block) => block.type === 'output_text');
|
|
4218
4286
|
const text = matches
|
|
4219
4287
|
.map((block) => String(block.text || '').trim())
|
|
4220
|
-
.find((value) =>
|
|
4221
|
-
if (!value)
|
|
4222
|
-
return false;
|
|
4223
|
-
if (role === 'user' && (value.length >= 2000 || value.includes('<permissions instructions>') || value.startsWith('# AGENTS.md instructions'))) {
|
|
4224
|
-
return false;
|
|
4225
|
-
}
|
|
4226
|
-
return true;
|
|
4227
|
-
});
|
|
4288
|
+
.find((value) => !!value);
|
|
4228
4289
|
return text || undefined;
|
|
4229
4290
|
}
|
|
4230
4291
|
/** Trim and normalize a version string, returning undefined for empty values. */
|
|
@@ -4233,11 +4294,18 @@ function normalizeVersion(version) {
|
|
|
4233
4294
|
return trimmed ? trimmed : undefined;
|
|
4234
4295
|
}
|
|
4235
4296
|
/** Extract the version number from a managed versions/<agent>/<version>/... path under either repo. */
|
|
4236
|
-
function extractVersionFromManagedPath(agent, sourcePath) {
|
|
4297
|
+
export function extractVersionFromManagedPath(agent, sourcePath) {
|
|
4237
4298
|
if (!sourcePath)
|
|
4238
4299
|
return undefined;
|
|
4239
4300
|
const candidates = [sourcePath, safeRealpathSync(sourcePath) || ''];
|
|
4240
4301
|
const markers = [`/.agents/versions/${agent}/`, `/.agents-system/versions/${agent}/`];
|
|
4302
|
+
// Codex is relocated by CODEX_HOME to `~/.agents/.codex-homes/<version>/`
|
|
4303
|
+
// (shims.ts `codexHomeShimBash`), NOT the `versions/<agent>/<version>/home/…`
|
|
4304
|
+
// layout every other isolated agent uses — so its rollout transcripts live
|
|
4305
|
+
// under `.codex-homes/<version>/sessions/…` and the markers above never match,
|
|
4306
|
+
// leaving `version` NULL and native resume degraded to `/continue` (PHNX-3626).
|
|
4307
|
+
if (agent === 'codex')
|
|
4308
|
+
markers.push('/.agents/.codex-homes/');
|
|
4241
4309
|
for (const candidate of candidates) {
|
|
4242
4310
|
if (!candidate)
|
|
4243
4311
|
continue;
|
|
@@ -4585,16 +4653,30 @@ export function readKimiMeta(filePath, priorRow) {
|
|
|
4585
4653
|
project,
|
|
4586
4654
|
filePath,
|
|
4587
4655
|
topic,
|
|
4656
|
+
firstUserMessage: newState.firstUserMessage,
|
|
4588
4657
|
messageCount,
|
|
4589
4658
|
tokenCount: tokenCount > 0 ? tokenCount : undefined,
|
|
4590
4659
|
outputTokens: outputTokens > 0 ? outputTokens : undefined,
|
|
4591
4660
|
};
|
|
4592
4661
|
return { meta, content: lastPrompt || '', parserState: JSON.stringify(newState) };
|
|
4593
4662
|
}
|
|
4663
|
+
function kimiWireUserText(event) {
|
|
4664
|
+
if (event?.type !== 'context.append_message' || event?.message?.role !== 'user')
|
|
4665
|
+
return undefined;
|
|
4666
|
+
const content = event.message.content;
|
|
4667
|
+
const raw = typeof content === 'string'
|
|
4668
|
+
? content
|
|
4669
|
+
: Array.isArray(content)
|
|
4670
|
+
? content.map((part) => typeof part === 'string' ? part : typeof part?.text === 'string' ? part.text : '').join('\n')
|
|
4671
|
+
: '';
|
|
4672
|
+
return cleanFirstUserMessage(raw);
|
|
4673
|
+
}
|
|
4594
4674
|
/** Fold one parsed Kimi wire event into the additive counters, in place. */
|
|
4595
4675
|
function applyKimiWireEvent(acc, event) {
|
|
4596
4676
|
if (event.type === 'context.append_message') {
|
|
4597
4677
|
acc.messageCount++;
|
|
4678
|
+
if (!acc.firstUserMessage)
|
|
4679
|
+
acc.firstUserMessage = kimiWireUserText(event);
|
|
4598
4680
|
}
|
|
4599
4681
|
else if (event.type === 'usage.record' && event.usage) {
|
|
4600
4682
|
// Kimi usage structure: inputOther + output + inputCacheRead + inputCacheCreation
|
|
@@ -4624,7 +4706,7 @@ export function parseKimiWireMetricsIncremental(sessionDir, prior) {
|
|
|
4624
4706
|
if (!stat) {
|
|
4625
4707
|
// No wire.jsonl (yet): zero counters, offset 0 so a later append is a clean
|
|
4626
4708
|
// full parse.
|
|
4627
|
-
return { messageCount: 0, tokenCount: 0, outputTokens: 0, newState: { v:
|
|
4709
|
+
return { messageCount: 0, tokenCount: 0, outputTokens: 0, newState: { v: 2, offset: 0, messageCount: 0, tokenCount: 0, outputTokens: 0 } };
|
|
4628
4710
|
}
|
|
4629
4711
|
// INCREMENTAL only when a usable prior exists AND the file grew past its
|
|
4630
4712
|
// offset; otherwise FULL from byte 0 with fresh counters (cold start OR the
|
|
@@ -4643,8 +4725,8 @@ export function parseKimiWireMetricsIncremental(sessionDir, prior) {
|
|
|
4643
4725
|
const canIncrement = prior !== null && stat.size > prior.offset;
|
|
4644
4726
|
const fromOffset = canIncrement ? prior.offset : 0;
|
|
4645
4727
|
const acc = canIncrement
|
|
4646
|
-
? { messageCount: prior.messageCount, tokenCount: prior.tokenCount, outputTokens: prior.outputTokens }
|
|
4647
|
-
: { messageCount: 0, tokenCount: 0, outputTokens: 0 };
|
|
4728
|
+
? { messageCount: prior.messageCount, tokenCount: prior.tokenCount, outputTokens: prior.outputTokens, firstUserMessage: prior.firstUserMessage }
|
|
4729
|
+
: { messageCount: 0, tokenCount: 0, outputTokens: 0, firstUserMessage: undefined };
|
|
4648
4730
|
let consumedBytes = 0;
|
|
4649
4731
|
let fd;
|
|
4650
4732
|
try {
|
|
@@ -4699,7 +4781,7 @@ export function parseKimiWireMetricsIncremental(sessionDir, prior) {
|
|
|
4699
4781
|
messageCount: acc.messageCount,
|
|
4700
4782
|
tokenCount: acc.tokenCount,
|
|
4701
4783
|
outputTokens: acc.outputTokens,
|
|
4702
|
-
newState: { v:
|
|
4784
|
+
newState: { v: 2, offset: fromOffset + consumedBytes, messageCount: acc.messageCount, tokenCount: acc.tokenCount, outputTokens: acc.outputTokens, firstUserMessage: acc.firstUserMessage },
|
|
4703
4785
|
};
|
|
4704
4786
|
}
|
|
4705
4787
|
/**
|
|
@@ -4713,7 +4795,7 @@ function parsePriorKimiState(row) {
|
|
|
4713
4795
|
return null;
|
|
4714
4796
|
try {
|
|
4715
4797
|
const parsed = JSON.parse(row.parserState);
|
|
4716
|
-
if (parsed?.v !==
|
|
4798
|
+
if (parsed?.v !== 2 || typeof parsed.offset !== 'number')
|
|
4717
4799
|
return null;
|
|
4718
4800
|
return parsed;
|
|
4719
4801
|
}
|
|
@@ -4790,6 +4872,84 @@ async function scanGrokIncremental(onProgress) {
|
|
|
4790
4872
|
upsertSessionsBatch(scanEntries);
|
|
4791
4873
|
recordScans(touched);
|
|
4792
4874
|
}
|
|
4875
|
+
/**
|
|
4876
|
+
* Bounded read of a Grok `chat_history.jsonl` for the genuine full first user
|
|
4877
|
+
* turn (PHNX-3621 leftover from #3359). Grok's cheap scan reads only
|
|
4878
|
+
* `summary.json` (a LARGE-transcript harness whose full message log is
|
|
4879
|
+
* deliberately not parsed on the hot tick), but the first turn is at the START
|
|
4880
|
+
* of the log, so a bounded prefix read recovers it without paying the full-file
|
|
4881
|
+
* cost. Production logs lead with a `type:system` record, then a huge
|
|
4882
|
+
* `<user_info>` user dump (no `synthetic_reason`), then a `synthetic_reason`
|
|
4883
|
+
* reminder, then the originating turn at `prompt_index: 0` wrapped in
|
|
4884
|
+
* `<user_query>`. Skip that scaffolding and prefer `prompt_index` when present.
|
|
4885
|
+
* Returns undefined when no genuine user turn appears within the budget.
|
|
4886
|
+
*/
|
|
4887
|
+
function readGrokFirstUserMessage(sessionDir, maxBytes = 262_144) {
|
|
4888
|
+
const historyPath = path.join(sessionDir, 'chat_history.jsonl');
|
|
4889
|
+
let fd;
|
|
4890
|
+
try {
|
|
4891
|
+
const stat = safeStatSync(historyPath);
|
|
4892
|
+
if (!stat)
|
|
4893
|
+
return undefined;
|
|
4894
|
+
const bytesToRead = Math.min(maxBytes, stat.size);
|
|
4895
|
+
if (bytesToRead <= 0)
|
|
4896
|
+
return undefined;
|
|
4897
|
+
const buf = Buffer.allocUnsafe(bytesToRead);
|
|
4898
|
+
fd = fs.openSync(historyPath, 'r');
|
|
4899
|
+
let read = 0;
|
|
4900
|
+
while (read < bytesToRead) {
|
|
4901
|
+
const n = fs.readSync(fd, buf, read, bytesToRead - read, read);
|
|
4902
|
+
if (n <= 0)
|
|
4903
|
+
break;
|
|
4904
|
+
read += n;
|
|
4905
|
+
}
|
|
4906
|
+
const chunk = buf.subarray(0, read).toString('utf-8');
|
|
4907
|
+
// Drop a trailing partial line so we never parse half a JSON record.
|
|
4908
|
+
const lastNl = chunk.lastIndexOf('\n');
|
|
4909
|
+
const complete = lastNl === -1 ? chunk : chunk.slice(0, lastNl);
|
|
4910
|
+
let firstGenuine;
|
|
4911
|
+
for (const line of complete.split('\n')) {
|
|
4912
|
+
if (!line.trim())
|
|
4913
|
+
continue;
|
|
4914
|
+
let msg;
|
|
4915
|
+
try {
|
|
4916
|
+
msg = JSON.parse(line);
|
|
4917
|
+
}
|
|
4918
|
+
catch {
|
|
4919
|
+
continue;
|
|
4920
|
+
}
|
|
4921
|
+
if (msg?.type !== 'user')
|
|
4922
|
+
continue;
|
|
4923
|
+
if (msg.synthetic_reason)
|
|
4924
|
+
continue;
|
|
4925
|
+
const raw = msg.content;
|
|
4926
|
+
const text = typeof raw === 'string'
|
|
4927
|
+
? raw
|
|
4928
|
+
: Array.isArray(raw)
|
|
4929
|
+
? raw.map((p) => (typeof p?.text === 'string' ? p.text : '')).join('')
|
|
4930
|
+
: '';
|
|
4931
|
+
const genuine = cleanFirstUserMessage(text);
|
|
4932
|
+
if (!genuine)
|
|
4933
|
+
continue;
|
|
4934
|
+
if (msg.prompt_index != null)
|
|
4935
|
+
return genuine;
|
|
4936
|
+
if (!firstGenuine)
|
|
4937
|
+
firstGenuine = genuine;
|
|
4938
|
+
}
|
|
4939
|
+
return firstGenuine;
|
|
4940
|
+
}
|
|
4941
|
+
catch {
|
|
4942
|
+
return undefined;
|
|
4943
|
+
}
|
|
4944
|
+
finally {
|
|
4945
|
+
if (fd !== undefined) {
|
|
4946
|
+
try {
|
|
4947
|
+
fs.closeSync(fd);
|
|
4948
|
+
}
|
|
4949
|
+
catch { /* already closed / gone */ }
|
|
4950
|
+
}
|
|
4951
|
+
}
|
|
4952
|
+
}
|
|
4793
4953
|
/** Parse a single Grok session summary.json into session metadata. */
|
|
4794
4954
|
export function readGrokMeta(filePath, currentVersion) {
|
|
4795
4955
|
let summary;
|
|
@@ -4842,6 +5002,7 @@ export function readGrokMeta(filePath, currentVersion) {
|
|
|
4842
5002
|
filePath,
|
|
4843
5003
|
version: resolveSessionVersion('grok', filePath, embeddedVersion, currentVersion),
|
|
4844
5004
|
topic,
|
|
5005
|
+
firstUserMessage: readGrokFirstUserMessage(sessionDir),
|
|
4845
5006
|
messageCount,
|
|
4846
5007
|
};
|
|
4847
5008
|
return { meta, content: topic || '' };
|
|
@@ -1,3 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Team prefixes that match the key shape but are not trackers — unit strings and
|
|
3
|
+
* common acronyms. Canonical here (the Linear module) so both the URL linkifier
|
|
4
|
+
* and the session-transcript ticket detector (`detectTicket` in `state.ts`) agree
|
|
5
|
+
* on what is a real key rather than each keeping its own copy.
|
|
6
|
+
*/
|
|
7
|
+
export declare const LINEAR_KEY_DENYLIST: ReadonlySet<string>;
|
|
8
|
+
/**
|
|
9
|
+
* Every distinct real Linear key mentioned in free text, in first-seen order.
|
|
10
|
+
* Honours {@link LINEAR_KEY_DENYLIST} so `UTF-8` / `X86-64` never count. Used to
|
|
11
|
+
* linkify ticket ids an owner ping only names in prose (no `session.ticketId`).
|
|
12
|
+
*/
|
|
13
|
+
export declare function linearIssueKeys(text: string | undefined): string[];
|
|
1
14
|
/** The configured Linear workspace slug, or undefined when unknown. Cached per process. */
|
|
2
15
|
export declare function linearWorkspace(): string | undefined;
|
|
3
16
|
/** Canonical Linear issue URL for a tracker key, or undefined if unresolvable. */
|