@phnx-labs/agents-cli 1.22.57 → 1.22.58
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/dist/bootstrap.js +8 -1
- package/dist/commands/accounts.js +7 -3
- package/dist/commands/apply.js +10 -2
- package/dist/commands/fork.d.ts +23 -10
- package/dist/commands/fork.js +115 -58
- package/dist/commands/monitors.js +11 -0
- package/dist/commands/prune.js +5 -3
- package/dist/commands/routines.d.ts +8 -0
- package/dist/commands/routines.js +57 -3
- package/dist/commands/sessions-picker.d.ts +11 -0
- package/dist/commands/sessions-picker.js +16 -0
- package/dist/commands/sessions.js +1 -0
- package/dist/commands/share.d.ts +14 -0
- package/dist/commands/share.js +43 -2
- package/dist/commands/status.js +1 -1
- package/dist/commands/sync.js +83 -7
- package/dist/commands/traces.js +7 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +6 -1
- package/dist/lib/account-registry.d.ts +5 -1
- package/dist/lib/account-registry.js +47 -14
- package/dist/lib/accounting/capacity.d.ts +18 -7
- package/dist/lib/accounting/capacity.js +19 -8
- package/dist/lib/accounting/usage-sync.d.ts +29 -1
- package/dist/lib/accounting/usage-sync.js +76 -2
- package/dist/lib/accounting/usage.js +7 -1
- package/dist/lib/auth-mint.d.ts +11 -1
- package/dist/lib/auth-mint.js +21 -6
- package/dist/lib/browser/ipc.d.ts +8 -0
- package/dist/lib/browser/ipc.js +87 -0
- package/dist/lib/browser/service.d.ts +19 -0
- package/dist/lib/browser/service.js +96 -11
- package/dist/lib/browser/sessions-list.js +10 -1
- package/dist/lib/daemon/runner.d.ts +3 -0
- package/dist/lib/daemon/runner.js +86 -45
- package/dist/lib/daemon/usage-sync-service.d.ts +3 -3
- package/dist/lib/daemon/usage-sync-service.js +14 -8
- package/dist/lib/daemon-services.js +1 -1
- package/dist/lib/devices/connect.d.ts +17 -8
- package/dist/lib/devices/connect.js +31 -14
- package/dist/lib/doctor-diff.js +77 -7
- package/dist/lib/fleet/manifest.d.ts +17 -0
- package/dist/lib/fleet/manifest.js +26 -0
- package/dist/lib/hooks/install.d.ts +27 -11
- package/dist/lib/hooks/install.js +42 -17
- package/dist/lib/hosts/reconnect.d.ts +52 -203
- package/dist/lib/hosts/reconnect.js +64 -284
- package/dist/lib/installations/migrate.d.ts +6 -120
- package/dist/lib/installations/migrate.js +27 -259
- package/dist/lib/installations/shims.d.ts +13 -95
- package/dist/lib/installations/shims.js +22 -139
- package/dist/lib/installations/store.js +1 -1
- package/dist/lib/installations/versions.d.ts +26 -133
- package/dist/lib/installations/versions.js +41 -204
- package/dist/lib/plugins/skills.d.ts +8 -1
- package/dist/lib/plugins/skills.js +18 -2
- package/dist/lib/refresh.d.ts +9 -0
- package/dist/lib/refresh.js +3 -1
- package/dist/lib/routine-readiness.d.ts +15 -1
- package/dist/lib/routine-readiness.js +41 -0
- package/dist/lib/sandbox.d.ts +4 -1
- package/dist/lib/sandbox.js +30 -1
- package/dist/lib/secrets/agent.d.ts +80 -225
- package/dist/lib/secrets/agent.js +139 -401
- package/dist/lib/secrets/bundles.d.ts +73 -222
- package/dist/lib/secrets/bundles.js +168 -467
- package/dist/lib/secrets/reaper.d.ts +28 -70
- package/dist/lib/secrets/reaper.js +30 -85
- package/dist/lib/secrets/remote.d.ts +42 -129
- package/dist/lib/secrets/remote.js +55 -173
- package/dist/lib/self-heal/checks/install-staging.d.ts +4 -0
- package/dist/lib/self-heal/checks/install-staging.js +96 -0
- package/dist/lib/self-heal/registry.js +2 -0
- package/dist/lib/self-heal/types.d.ts +1 -1
- package/dist/lib/self-update.d.ts +23 -0
- package/dist/lib/self-update.js +50 -0
- package/dist/lib/session/active.d.ts +13 -1
- package/dist/lib/session/active.js +2 -0
- package/dist/lib/session/db.d.ts +20 -1
- package/dist/lib/session/db.js +139 -9
- package/dist/lib/session/fork.d.ts +45 -26
- package/dist/lib/session/fork.js +32 -95
- package/dist/lib/session/tool-calls.d.ts +43 -1
- package/dist/lib/session/tool-calls.js +74 -44
- package/dist/lib/session/tool-store.d.ts +33 -2
- package/dist/lib/session/tool-store.js +56 -3
- package/dist/lib/staleness/writers/sources.d.ts +5 -0
- package/dist/lib/staleness/writers/sources.js +2 -1
- package/dist/lib/sync-status.d.ts +22 -0
- package/dist/lib/sync-status.js +27 -0
- package/dist/lib/sync-umbrella.d.ts +9 -0
- package/dist/lib/sync-umbrella.js +21 -2
- package/dist/lib/traces/insights.d.ts +47 -14
- package/dist/lib/traces/insights.js +92 -21
- package/dist/lib/traces/phenotype.d.ts +23 -3
- package/dist/lib/traces/phenotype.js +72 -24
- package/dist/lib/traces/sync.d.ts +15 -0
- package/dist/lib/traces/sync.js +104 -19
- package/dist/lib/traces/worker-template.js +154 -1
- package/package.json +1 -1
|
@@ -145,6 +145,8 @@ export function backfillActiveRowsFromMeta(sessions, metaById) {
|
|
|
145
145
|
continue;
|
|
146
146
|
if (!s.version && m.version)
|
|
147
147
|
s.version = m.version;
|
|
148
|
+
if (!s.account && m.account)
|
|
149
|
+
s.account = m.account;
|
|
148
150
|
if (!s.label && m.label)
|
|
149
151
|
s.label = m.label;
|
|
150
152
|
if (!s.ticket && m.ticketId)
|
package/dist/lib/session/db.d.ts
CHANGED
|
@@ -9,10 +9,11 @@
|
|
|
9
9
|
import Database from '../sqlite.js';
|
|
10
10
|
import type { SessionAgentId, SessionEvent, SessionMeta } from './types.js';
|
|
11
11
|
import { type IndexedToolCall } from './tool-calls.js';
|
|
12
|
+
import { type ToolScanResumePoint } from './tool-store.js';
|
|
12
13
|
/** Current schema version; bumped when migrations are added. Exported so tests
|
|
13
14
|
* assert against the constant instead of hardcoding a number that every bump
|
|
14
15
|
* then has to chase (docs/sessions.md calls the constant the source of truth). */
|
|
15
|
-
export declare const SCHEMA_VERSION =
|
|
16
|
+
export declare const SCHEMA_VERSION = 43;
|
|
16
17
|
/**
|
|
17
18
|
* Bump to force the content extractor (assistant-answer text, alongside the
|
|
18
19
|
* user-prompt text every harness already accumulates) to re-derive on every
|
|
@@ -35,6 +36,8 @@ export declare const CONTENT_INDEX_VERSION = 1;
|
|
|
35
36
|
export declare const INSIGHTS_EXTRACTOR_VERSION = 7;
|
|
36
37
|
/** Bump when classifyTopic's output changes so cached topics recompute (human task taxonomy v2). */
|
|
37
38
|
export declare const SESSION_TOPIC_EXTRACTOR_VERSION = 2;
|
|
39
|
+
/** Bump when classifyPhenotype's output changes so cached phenotypes recompute (PHNX-3327 v1). */
|
|
40
|
+
export declare const SESSION_PHENOTYPE_EXTRACTOR_VERSION = 1;
|
|
38
41
|
/** File stat snapshot used to detect changes between scan runs. */
|
|
39
42
|
export interface ScanStamp {
|
|
40
43
|
fileMtimeMs: number;
|
|
@@ -249,6 +252,13 @@ export declare function upsertSessionsBatch(entries: Array<{
|
|
|
249
252
|
toolCalls?: IndexedToolCall[];
|
|
250
253
|
toolScan?: ScanStamp;
|
|
251
254
|
toolIndexMode?: 'replace' | 'append';
|
|
255
|
+
/**
|
|
256
|
+
* Where the NEXT scan of this full-file-harness session may resume its tool
|
|
257
|
+
* index (PHNX-3411). Computed internally in the enrichment map below; not
|
|
258
|
+
* supplied by callers. Persisted alongside the tool ledger so an active
|
|
259
|
+
* session re-derives only its newly appended tool calls next tick.
|
|
260
|
+
*/
|
|
261
|
+
toolResume?: ToolScanResumePoint | null;
|
|
252
262
|
}>): void;
|
|
253
263
|
/**
|
|
254
264
|
* Sync labels for a set of sessions. For each id in the map, if the stored
|
|
@@ -377,6 +387,15 @@ export declare function writeSessionTopics<T>(entries: Array<{
|
|
|
377
387
|
fileSize: number | null;
|
|
378
388
|
topic: T;
|
|
379
389
|
}>): void;
|
|
390
|
+
/** Read cached failure phenotypes only when their transcript byte stamps still match. */
|
|
391
|
+
export declare function readSessionPhenotypes<T>(ids: string[]): Map<string, T>;
|
|
392
|
+
/** Persist failure phenotypes against the exact transcript bytes used to classify them. */
|
|
393
|
+
export declare function writeSessionPhenotypes<T>(entries: Array<{
|
|
394
|
+
id: string;
|
|
395
|
+
fileMtimeMs: number | null;
|
|
396
|
+
fileSize: number | null;
|
|
397
|
+
phenotype: T;
|
|
398
|
+
}>): void;
|
|
380
399
|
/** Read one derived preview only when it matches the transcript bytes on disk. */
|
|
381
400
|
export declare function readSessionPreviewCache<T>(id: string, sourceStamp: {
|
|
382
401
|
fileMtimeMs: number | null;
|
package/dist/lib/session/db.js
CHANGED
|
@@ -15,8 +15,8 @@ import { getSessionsDir, getSessionsDbPath } from '../state.js';
|
|
|
15
15
|
import { query as queryEvents, queryToolUsageForSessions } from '../feed/events.js';
|
|
16
16
|
import { machineForSessionFile } from '../origin-machine.js';
|
|
17
17
|
import { loadSessionActorIndex, readSessionActorRecord } from './actor-sidecar.js';
|
|
18
|
-
import {
|
|
19
|
-
import { persistToolCalls, toolEvidenceSourcePath } from './tool-store.js';
|
|
18
|
+
import { scanEventToolCalls } from './tool-calls.js';
|
|
19
|
+
import { persistToolCalls, planEventToolResume, toolEvidenceSourcePath } from './tool-store.js';
|
|
20
20
|
import { buildClaudeAccountIndex, resolveClaudeAccount } from './claude-accounts.js';
|
|
21
21
|
import { extractBackgroundShells, extractSkills, extractSlashCommands, harnessTracksBackgroundShells, isSubAgentTool, } from './highlights.js';
|
|
22
22
|
import { resolveResource } from '../resources.js';
|
|
@@ -27,7 +27,7 @@ const DB_PATH = getSessionsDbPath();
|
|
|
27
27
|
/** Current schema version; bumped when migrations are added. Exported so tests
|
|
28
28
|
* assert against the constant instead of hardcoding a number that every bump
|
|
29
29
|
* then has to chase (docs/sessions.md calls the constant the source of truth). */
|
|
30
|
-
export const SCHEMA_VERSION =
|
|
30
|
+
export const SCHEMA_VERSION = 43;
|
|
31
31
|
/**
|
|
32
32
|
* Bump to force the content extractor (assistant-answer text, alongside the
|
|
33
33
|
* user-prompt text every harness already accumulates) to re-derive on every
|
|
@@ -207,6 +207,11 @@ CREATE TABLE IF NOT EXISTS tool_calls (
|
|
|
207
207
|
ordinal INTEGER NOT NULL,
|
|
208
208
|
source_call_id TEXT,
|
|
209
209
|
timestamp TEXT NOT NULL,
|
|
210
|
+
-- When the call's RESULT record arrived (its own end time). end_timestamp
|
|
211
|
+
-- minus timestamp is the call's own blocking duration, which the traces
|
|
212
|
+
-- insight engine attributes as a failed call's wasted time (PHNX-3437). NULL
|
|
213
|
+
-- for a call that never produced a result and for rows an older extractor stored.
|
|
214
|
+
end_timestamp TEXT,
|
|
210
215
|
tool TEXT NOT NULL,
|
|
211
216
|
input TEXT NOT NULL,
|
|
212
217
|
outcome TEXT NOT NULL,
|
|
@@ -352,6 +357,26 @@ CREATE TABLE IF NOT EXISTS session_topics (
|
|
|
352
357
|
topic_json TEXT NOT NULL
|
|
353
358
|
);
|
|
354
359
|
|
|
360
|
+
-- Derived failure phenotype for traces sync (PHNX-3327). Like session_topics /
|
|
361
|
+
-- session_insights, this is a lazy, stamp-validated cache keyed on
|
|
362
|
+
-- (file_mtime_ms, file_size) and intentionally independent of SCHEMA_VERSION.
|
|
363
|
+
-- Classifying a phenotype needs the full derived SessionTrajectory (ordered
|
|
364
|
+
-- steps, gaps), which buildIndexShard does NOT have from flat tool_calls rows —
|
|
365
|
+
-- so it is computed per-session ONCE (parse -> trajectory -> classify) and cached
|
|
366
|
+
-- here, then read for the WHOLE corpus on every sync. That is what lets the
|
|
367
|
+
-- phenotype grouping dimension fold two identically-signatured sessions into one
|
|
368
|
+
-- cluster regardless of which incremental batch each was first synced in, without
|
|
369
|
+
-- re-parsing transcripts at 10k+ session scale. phenotype_json holds
|
|
370
|
+
-- { phenotype: FailurePhenotype | null } (null = no failure phenotype matched).
|
|
371
|
+
CREATE TABLE IF NOT EXISTS session_phenotypes (
|
|
372
|
+
session_id TEXT PRIMARY KEY,
|
|
373
|
+
file_mtime_ms INTEGER,
|
|
374
|
+
file_size INTEGER,
|
|
375
|
+
extractor_version INTEGER NOT NULL,
|
|
376
|
+
computed_at INTEGER NOT NULL,
|
|
377
|
+
phenotype_json TEXT NOT NULL
|
|
378
|
+
);
|
|
379
|
+
|
|
355
380
|
-- Normalized data behind sessions preview. Like session_insights this is a
|
|
356
381
|
-- lazy, stamp-validated cache: opening one session parses only that transcript,
|
|
357
382
|
-- while subsequent processes reuse the derived preview until its bytes change.
|
|
@@ -437,7 +462,12 @@ CREATE INDEX IF NOT EXISTS idx_computer_sessions_started ON computer_sessions(st
|
|
|
437
462
|
export const INSIGHTS_EXTRACTOR_VERSION = 7;
|
|
438
463
|
/** Bump when classifyTopic's output changes so cached topics recompute (human task taxonomy v2). */
|
|
439
464
|
export const SESSION_TOPIC_EXTRACTOR_VERSION = 2;
|
|
440
|
-
|
|
465
|
+
// Bumped to 2 (PHNX-2973): the digest now carries `changedFiles` (per-file
|
|
466
|
+
// paths). Bumping invalidates v1 cache rows so a fresh recompute populates the
|
|
467
|
+
// new field instead of serving a stale digest that predates it.
|
|
468
|
+
const PREVIEW_EXTRACTOR_VERSION = 2;
|
|
469
|
+
/** Bump when classifyPhenotype's output changes so cached phenotypes recompute (PHNX-3327 v1). */
|
|
470
|
+
export const SESSION_PHENOTYPE_EXTRACTOR_VERSION = 1;
|
|
441
471
|
let dbInstance = null;
|
|
442
472
|
/**
|
|
443
473
|
* Apply schema migrations from `fromVersion` → SCHEMA_VERSION. The new
|
|
@@ -1152,6 +1182,29 @@ function migrateSchema(db, fromVersion) {
|
|
|
1152
1182
|
// Claude/Codex resumable continuation) stay intact for rows that don't
|
|
1153
1183
|
// need a full reparse for any OTHER reason.
|
|
1154
1184
|
}
|
|
1185
|
+
if (fromVersion < 43) {
|
|
1186
|
+
// v42 -> v43: persist a per-tool-call END timestamp (PHNX-3437). The traces
|
|
1187
|
+
// insight engine could only book a failed call's wasted time from the
|
|
1188
|
+
// bounded gap to the NEXT call — so a call that BLOCKED for minutes and was
|
|
1189
|
+
// the last in its session (or was followed quickly by an unrelated call)
|
|
1190
|
+
// registered as ~0 waste. The call's result record already carried its own
|
|
1191
|
+
// timestamp at ingestion; this column persists it so `end_timestamp -
|
|
1192
|
+
// timestamp` (the call's own blocking duration) becomes the primary
|
|
1193
|
+
// attribution, with the gap heuristic kept as the fallback for NULL rows.
|
|
1194
|
+
//
|
|
1195
|
+
// Additive, nullable column — no ledger flush (the v33->v34 contract that
|
|
1196
|
+
// adding a column keeps warm session ledgers warm). Pre-upgrade rows stay
|
|
1197
|
+
// NULL until re-indexed, and `insights.ts` degrades a NULL end back to the
|
|
1198
|
+
// bounded-gap behavior, so nothing crashes or yields NaN. The paired
|
|
1199
|
+
// TOOL_INDEX_VERSION bump (7 -> 8) is what re-derives it on a re-index; the
|
|
1200
|
+
// tool index is deliberately independent of SCHEMA_VERSION and is never
|
|
1201
|
+
// force-rescanned by a migration (only by the explicit tool backfill or a
|
|
1202
|
+
// normal incremental append), so a bare ALTER here would otherwise leave
|
|
1203
|
+
// existing rows dark forever.
|
|
1204
|
+
const cols = new Set(db.prepare(`PRAGMA table_info(tool_calls)`).all().map((c) => c.name));
|
|
1205
|
+
if (!cols.has('end_timestamp'))
|
|
1206
|
+
db.exec(`ALTER TABLE tool_calls ADD COLUMN end_timestamp TEXT`);
|
|
1207
|
+
}
|
|
1155
1208
|
}
|
|
1156
1209
|
/**
|
|
1157
1210
|
* Stamp `account_key` / `account_org` / `account` on every Claude row from its
|
|
@@ -2123,6 +2176,21 @@ export function upsertSessionsBatch(entries) {
|
|
|
2123
2176
|
// metadata fall back to exactly one normalized parse here.
|
|
2124
2177
|
const events = entry.events ?? parseSession(entry.meta.filePath, entry.meta.agent);
|
|
2125
2178
|
writeResourceUsage(entry.meta.id, events, entry.meta.cwd);
|
|
2179
|
+
// Resume the tool index from the last scan of this append-only stream when
|
|
2180
|
+
// it is safe to (PHNX-3411). A live session's transcript grows every tick,
|
|
2181
|
+
// so a full re-derive re-sanitizes its ENTIRE tool history each time — the
|
|
2182
|
+
// synchronous O(session) work that wedged the daemon event loop for the
|
|
2183
|
+
// 11 non-streaming harnesses. `planEventToolResume` returns the prior
|
|
2184
|
+
// snapshot only when the file is still an append of what was scanned
|
|
2185
|
+
// before; otherwise `prior` is null and this is a full replace from
|
|
2186
|
+
// event 0 (identical index, just re-derived).
|
|
2187
|
+
// No tool stamp (a scanner that carried no scan record) means the resume
|
|
2188
|
+
// point cannot be size-guarded, so full-scan rather than trust a stale
|
|
2189
|
+
// offset. persistToolCalls below also skips a resume-less write.
|
|
2190
|
+
const prior = toolScan
|
|
2191
|
+
? planEventToolResume(db, entry.meta.id, toolSourcePath, toolScan, events.length)
|
|
2192
|
+
: null;
|
|
2193
|
+
const scanned = scanEventToolCalls(events, prior ?? undefined);
|
|
2126
2194
|
return {
|
|
2127
2195
|
...entry,
|
|
2128
2196
|
meta: {
|
|
@@ -2131,11 +2199,14 @@ export function upsertSessionsBatch(entries) {
|
|
|
2131
2199
|
recentDirectoriesTouched: extractRecentDirectoriesTouched(events, entry.meta.cwd),
|
|
2132
2200
|
...fanOutCounts(events, entry.meta.agent),
|
|
2133
2201
|
},
|
|
2134
|
-
|
|
2202
|
+
// The CHANGED calls only. On a resume these are the newly appended tail
|
|
2203
|
+
// (append-safe upsert); on a full scan they are the whole history.
|
|
2204
|
+
toolCalls: scanned.calls,
|
|
2135
2205
|
toolScan,
|
|
2136
|
-
|
|
2137
|
-
//
|
|
2138
|
-
|
|
2206
|
+
toolIndexMode: (prior ? 'append' : 'replace'),
|
|
2207
|
+
// Persist where the NEXT scan resumes: the collector snapshot + how many
|
|
2208
|
+
// events this scan folded.
|
|
2209
|
+
toolResume: { parserState: JSON.stringify(scanned.snapshot), parsedOffset: scanned.eventCount },
|
|
2139
2210
|
};
|
|
2140
2211
|
}
|
|
2141
2212
|
catch {
|
|
@@ -2284,7 +2355,13 @@ export function upsertSessionsBatch(entries) {
|
|
|
2284
2355
|
if (!toolScan || !entry.toolCalls)
|
|
2285
2356
|
continue;
|
|
2286
2357
|
try {
|
|
2287
|
-
|
|
2358
|
+
// `resume` is set only by the full-file harness path above; claude/codex
|
|
2359
|
+
// pass none, so their tool ledger keeps carrying no event-offset resume
|
|
2360
|
+
// point (their resume rides the content-scan ledger instead) — unchanged.
|
|
2361
|
+
persistToolCalls(db, entry.meta, entry.toolCalls, toolScan, {
|
|
2362
|
+
mode: entry.toolIndexMode ?? 'replace',
|
|
2363
|
+
resume: entry.toolResume,
|
|
2364
|
+
});
|
|
2288
2365
|
}
|
|
2289
2366
|
catch {
|
|
2290
2367
|
// Boundary is intentionally retryable via tool_scan_ledger.
|
|
@@ -2950,6 +3027,59 @@ export function writeSessionTopics(entries) {
|
|
|
2950
3027
|
}
|
|
2951
3028
|
})();
|
|
2952
3029
|
}
|
|
3030
|
+
/** Read cached failure phenotypes only when their transcript byte stamps still match. */
|
|
3031
|
+
export function readSessionPhenotypes(ids) {
|
|
3032
|
+
const db = getDB();
|
|
3033
|
+
const out = new Map();
|
|
3034
|
+
if (ids.length === 0)
|
|
3035
|
+
return out;
|
|
3036
|
+
const CHUNK = 400;
|
|
3037
|
+
for (let i = 0; i < ids.length; i += CHUNK) {
|
|
3038
|
+
const chunk = ids.slice(i, i + CHUNK);
|
|
3039
|
+
const placeholders = chunk.map(() => '?').join(',');
|
|
3040
|
+
const rows = db.prepare(`
|
|
3041
|
+
SELECT sp.session_id AS id, sp.phenotype_json AS phenotypeJson
|
|
3042
|
+
FROM session_phenotypes sp
|
|
3043
|
+
JOIN sessions s ON s.id = sp.session_id
|
|
3044
|
+
WHERE sp.session_id IN (${placeholders})
|
|
3045
|
+
AND sp.extractor_version = ?
|
|
3046
|
+
AND sp.file_mtime_ms IS s.file_mtime_ms
|
|
3047
|
+
AND sp.file_size IS s.file_size
|
|
3048
|
+
`).all(...chunk, SESSION_PHENOTYPE_EXTRACTOR_VERSION);
|
|
3049
|
+
for (const row of rows) {
|
|
3050
|
+
try {
|
|
3051
|
+
out.set(row.id, JSON.parse(row.phenotypeJson));
|
|
3052
|
+
}
|
|
3053
|
+
catch {
|
|
3054
|
+
// Invalid derived cache data is a miss and self-heals on the next write.
|
|
3055
|
+
}
|
|
3056
|
+
}
|
|
3057
|
+
}
|
|
3058
|
+
return out;
|
|
3059
|
+
}
|
|
3060
|
+
/** Persist failure phenotypes against the exact transcript bytes used to classify them. */
|
|
3061
|
+
export function writeSessionPhenotypes(entries) {
|
|
3062
|
+
if (entries.length === 0)
|
|
3063
|
+
return;
|
|
3064
|
+
const db = getDB();
|
|
3065
|
+
const stmt = db.prepare(`
|
|
3066
|
+
INSERT INTO session_phenotypes
|
|
3067
|
+
(session_id, file_mtime_ms, file_size, extractor_version, computed_at, phenotype_json)
|
|
3068
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
3069
|
+
ON CONFLICT(session_id) DO UPDATE SET
|
|
3070
|
+
file_mtime_ms = excluded.file_mtime_ms,
|
|
3071
|
+
file_size = excluded.file_size,
|
|
3072
|
+
extractor_version = excluded.extractor_version,
|
|
3073
|
+
computed_at = excluded.computed_at,
|
|
3074
|
+
phenotype_json = excluded.phenotype_json
|
|
3075
|
+
`);
|
|
3076
|
+
const now = Date.now();
|
|
3077
|
+
db.transaction(() => {
|
|
3078
|
+
for (const entry of entries) {
|
|
3079
|
+
stmt.run(entry.id, entry.fileMtimeMs, entry.fileSize, SESSION_PHENOTYPE_EXTRACTOR_VERSION, now, JSON.stringify(entry.phenotype));
|
|
3080
|
+
}
|
|
3081
|
+
})();
|
|
3082
|
+
}
|
|
2953
3083
|
/** Read one derived preview only when it matches the transcript bytes on disk. */
|
|
2954
3084
|
export function readSessionPreviewCache(id, sourceStamp) {
|
|
2955
3085
|
const row = getDB().prepare(`
|
|
@@ -1,32 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session forking — branch an existing conversation into a new, independent
|
|
3
|
+
* sibling that continues the work, leaving the original untouched.
|
|
4
|
+
*
|
|
5
|
+
* `resume` continues the SAME conversation (same id, same file — it appends).
|
|
6
|
+
* `fork` launches a NEW same-harness session, load-balanced, seeded with a
|
|
7
|
+
* recap of the source so it picks up where the original left off. This is the
|
|
8
|
+
* "git branch" of conversations.
|
|
9
|
+
*
|
|
10
|
+
* The recap — not a transcript copy — is what makes fork work across every
|
|
11
|
+
* device and every REPL harness: the sibling is handed plain text as its opening
|
|
12
|
+
* input, so it never has to reach a transcript that may live on another box. The
|
|
13
|
+
* source is resolved cross-fleet by the same resolver `preview` uses; this module
|
|
14
|
+
* owns only the pure recap text the resolved data folds into.
|
|
15
|
+
*/
|
|
1
16
|
import type { SessionMeta } from './types.js';
|
|
2
|
-
/**
|
|
3
|
-
export
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
filePath: string;
|
|
14
|
-
/** The label applied to the fork. */
|
|
17
|
+
/** File-change tally as `sessions preview --json` serializes it (digest.changes). */
|
|
18
|
+
export interface ForkRecapChanges {
|
|
19
|
+
created: number;
|
|
20
|
+
modified: number;
|
|
21
|
+
deleted: number;
|
|
22
|
+
}
|
|
23
|
+
/** Everything the recap seed is built from — resolved cross-fleet before launch. */
|
|
24
|
+
export interface ForkRecapInput {
|
|
25
|
+
/** Source harness id — the sibling launches the same one. */
|
|
26
|
+
agent: string;
|
|
27
|
+
/** Display label for the source (label → topic → short id, resolved by the caller). */
|
|
15
28
|
label: string;
|
|
29
|
+
/** Source working directory, so the sibling re-roots itself. */
|
|
30
|
+
cwd?: string;
|
|
31
|
+
/** Linear/GitHub ticket the source was bound to, if any. */
|
|
32
|
+
ticketId?: string;
|
|
33
|
+
/** Device that owns the source transcript, for the `/continue` escape hatch. */
|
|
34
|
+
machine?: string;
|
|
35
|
+
/** Short + full id, so the sibling can pull full history with `/continue <id>`. */
|
|
36
|
+
shortId: string;
|
|
37
|
+
id: string;
|
|
38
|
+
/** The source's last assistant line — the single best "where it left off" signal. */
|
|
39
|
+
lastAssistant?: string;
|
|
40
|
+
/** Changed-files tally so far. */
|
|
41
|
+
changes?: ForkRecapChanges;
|
|
16
42
|
}
|
|
43
|
+
/** Resolve the human display label the caller passes in from a raw SessionMeta. */
|
|
44
|
+
export declare function forkLabelFor(session: Pick<SessionMeta, 'label' | 'topic' | 'shortId'>): string;
|
|
17
45
|
/**
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
* Copies `source.filePath` to a new `<uuid>.jsonl` beside it, rewrites the
|
|
21
|
-
* embedded session id, registers the new session in the index, and records a
|
|
22
|
-
* `--name`-style label. Returns the new ids/path. Throws if the source
|
|
23
|
-
* transcript is missing.
|
|
46
|
+
* Build the recap-seed prompt handed to the forked sibling as its opening input.
|
|
24
47
|
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
* @param now ISO timestamp to stamp the fork with (injectable for tests).
|
|
48
|
+
* Pure and deterministic (unit-tested) — no filesystem, no spawn — so the launch
|
|
49
|
+
* orchestration in `commands/fork.ts` stays the only side-effecting layer.
|
|
28
50
|
*/
|
|
29
|
-
export declare function
|
|
30
|
-
name?: string;
|
|
31
|
-
now?: string;
|
|
32
|
-
}): ForkResult;
|
|
51
|
+
export declare function buildForkRecap(input: ForkRecapInput): string;
|
package/dist/lib/session/fork.js
CHANGED
|
@@ -1,102 +1,39 @@
|
|
|
1
|
-
/**
|
|
2
|
-
*
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
*
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
* resumes natively via `--resume`. A fork is therefore: copy the transcript to
|
|
12
|
-
* a new-uuid filename in the same directory, rewrite the embedded `sessionId`
|
|
13
|
-
* on each line, register the new session in the index, and label it. Other
|
|
14
|
-
* agents (codex single-file; grok/kimi multi-file; opencode DB-only) are a
|
|
15
|
-
* natural follow-up and are refused up front for now.
|
|
16
|
-
*/
|
|
17
|
-
import { randomUUID } from 'crypto';
|
|
18
|
-
import * as fs from 'fs';
|
|
19
|
-
import * as path from 'path';
|
|
20
|
-
import { upsertSession } from './db.js';
|
|
21
|
-
import { recordRunName } from './run-names.js';
|
|
22
|
-
import { deriveShortId } from '../text/short-id.js';
|
|
23
|
-
/** Agents that `fork` can branch today (see the module doc for why). */
|
|
24
|
-
export const FORKABLE_AGENTS = ['claude'];
|
|
25
|
-
/** Whether a session's agent can be forked by {@link forkSession}. */
|
|
26
|
-
export function isForkableAgent(agent) {
|
|
27
|
-
return FORKABLE_AGENTS.includes(agent);
|
|
1
|
+
/** Longest last-assistant excerpt carried into the seed — enough to convey intent
|
|
2
|
+
* without pasting a wall of text (decision: Recap, not Full digest). */
|
|
3
|
+
const LAST_LINE_CAP = 400;
|
|
4
|
+
/** Collapse whitespace and cap length so a multi-paragraph final message becomes
|
|
5
|
+
* one scannable recap line. */
|
|
6
|
+
function trimLastLine(text) {
|
|
7
|
+
const collapsed = text.replace(/\s+/g, ' ').trim();
|
|
8
|
+
if (collapsed.length <= LAST_LINE_CAP)
|
|
9
|
+
return collapsed;
|
|
10
|
+
return `${collapsed.slice(0, LAST_LINE_CAP).trimEnd()}…`;
|
|
28
11
|
}
|
|
29
|
-
/**
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
* (keeps the in-file id consistent with the new filename); malformed lines are
|
|
33
|
-
* passed through untouched.
|
|
34
|
-
*/
|
|
35
|
-
function rewriteSessionId(transcript, newId) {
|
|
36
|
-
return transcript
|
|
37
|
-
.split('\n')
|
|
38
|
-
.map((line) => {
|
|
39
|
-
if (!line.trim())
|
|
40
|
-
return line;
|
|
41
|
-
try {
|
|
42
|
-
const obj = JSON.parse(line);
|
|
43
|
-
if (typeof obj.sessionId === 'string') {
|
|
44
|
-
obj.sessionId = newId;
|
|
45
|
-
return JSON.stringify(obj);
|
|
46
|
-
}
|
|
47
|
-
return line;
|
|
48
|
-
}
|
|
49
|
-
catch {
|
|
50
|
-
return line;
|
|
51
|
-
}
|
|
52
|
-
})
|
|
53
|
-
.join('\n');
|
|
12
|
+
/** Resolve the human display label the caller passes in from a raw SessionMeta. */
|
|
13
|
+
export function forkLabelFor(session) {
|
|
14
|
+
return session.label || session.topic || session.shortId;
|
|
54
15
|
}
|
|
55
16
|
/**
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
* Copies `source.filePath` to a new `<uuid>.jsonl` beside it, rewrites the
|
|
59
|
-
* embedded session id, registers the new session in the index, and records a
|
|
60
|
-
* `--name`-style label. Returns the new ids/path. Throws if the source
|
|
61
|
-
* transcript is missing.
|
|
17
|
+
* Build the recap-seed prompt handed to the forked sibling as its opening input.
|
|
62
18
|
*
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
-
* @param now ISO timestamp to stamp the fork with (injectable for tests).
|
|
19
|
+
* Pure and deterministic (unit-tested) — no filesystem, no spawn — so the launch
|
|
20
|
+
* orchestration in `commands/fork.ts` stays the only side-effecting layer.
|
|
66
21
|
*/
|
|
67
|
-
export function
|
|
68
|
-
|
|
69
|
-
|
|
22
|
+
export function buildForkRecap(input) {
|
|
23
|
+
const lines = [];
|
|
24
|
+
lines.push(`Continue a prior ${input.agent} session ("${input.label}"). Pick up where it left off — do not restart it.`);
|
|
25
|
+
if (input.cwd)
|
|
26
|
+
lines.push(`Working directory: ${input.cwd}`);
|
|
27
|
+
if (input.ticketId)
|
|
28
|
+
lines.push(`Ticket: ${input.ticketId}`);
|
|
29
|
+
const last = input.lastAssistant ? trimLastLine(input.lastAssistant) : '';
|
|
30
|
+
if (last)
|
|
31
|
+
lines.push(`It last said: "${last}"`);
|
|
32
|
+
const chg = input.changes;
|
|
33
|
+
if (chg && (chg.created || chg.modified || chg.deleted)) {
|
|
34
|
+
lines.push(`Changes so far: +${chg.created} ~${chg.modified} -${chg.deleted}.`);
|
|
70
35
|
}
|
|
71
|
-
const
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
const filePath = path.join(dir, `${newId}.jsonl`);
|
|
75
|
-
const transcript = fs.readFileSync(source.filePath, 'utf-8');
|
|
76
|
-
const rewritten = rewriteSessionId(transcript, newId);
|
|
77
|
-
fs.writeFileSync(filePath, rewritten);
|
|
78
|
-
const original = source.label || source.topic || source.shortId;
|
|
79
|
-
const label = opts.name || `fork of ${original}`;
|
|
80
|
-
// Label sidecar (seeds the DB label; survives rescans until an agent title
|
|
81
|
-
// supersedes it), mirroring `agents run --name`.
|
|
82
|
-
recordRunName({ sessionId: newId, name: label, agent: source.agent, cwd: source.cwd });
|
|
83
|
-
// Register the new session so it resolves immediately (by `agents sessions resume`,
|
|
84
|
-
// `agents sessions`, etc.) without waiting for the next scan.
|
|
85
|
-
const stamp = opts.now ?? new Date().toISOString();
|
|
86
|
-
const meta = {
|
|
87
|
-
...source,
|
|
88
|
-
id: newId,
|
|
89
|
-
shortId,
|
|
90
|
-
filePath,
|
|
91
|
-
label,
|
|
92
|
-
timestamp: stamp,
|
|
93
|
-
lastActivity: stamp,
|
|
94
|
-
// The fork has not opened its own PR / team; drop origin-specific refs.
|
|
95
|
-
prUrl: undefined,
|
|
96
|
-
prNumber: undefined,
|
|
97
|
-
teamOrigin: undefined,
|
|
98
|
-
spawnedTeam: undefined,
|
|
99
|
-
};
|
|
100
|
-
upsertSession(meta, rewritten);
|
|
101
|
-
return { newId, shortId, filePath, label };
|
|
36
|
+
const origin = input.machine ? ` on ${input.machine}` : '';
|
|
37
|
+
lines.push(`Source session ${input.shortId}${origin} — run \`/continue ${input.id}\` if you need the full transcript.`);
|
|
38
|
+
return lines.join('\n');
|
|
102
39
|
}
|
|
@@ -10,12 +10,21 @@ export declare const TOOL_CHANGED_MAX_CALLS = 10000;
|
|
|
10
10
|
export declare const TOOL_INDEX_LIMIT_ORDINAL: number;
|
|
11
11
|
export declare const TOOL_TEXT_PROCESSING_MAX_BYTES: number;
|
|
12
12
|
export declare const TOOL_SHELL_PARSE_MAX_BYTES: number;
|
|
13
|
-
export declare const TOOL_INDEX_VERSION =
|
|
13
|
+
export declare const TOOL_INDEX_VERSION = 8;
|
|
14
14
|
export type ToolCallOutcome = 'ok' | 'error' | 'unknown';
|
|
15
15
|
export interface IndexedToolCall {
|
|
16
16
|
ordinal: number;
|
|
17
17
|
sourceCallId?: string;
|
|
18
18
|
timestamp: string;
|
|
19
|
+
/**
|
|
20
|
+
* When the call's RESULT record arrived — the call's own end time, taken from
|
|
21
|
+
* the tool_result transcript record at `finish()` (PHNX-3437). `timestamp` is
|
|
22
|
+
* the start; `endTimestamp - timestamp` is the call's own blocking duration,
|
|
23
|
+
* which the traces insight engine attributes as a failed call's wasted time.
|
|
24
|
+
* Undefined for a call that never produced a result (still pending at scan end)
|
|
25
|
+
* and for rows produced by an older extractor.
|
|
26
|
+
*/
|
|
27
|
+
endTimestamp?: string;
|
|
19
28
|
tool: string;
|
|
20
29
|
programs: string[];
|
|
21
30
|
programOccurrences: ShellProgramOccurrence[];
|
|
@@ -81,6 +90,39 @@ export declare class ToolCallCollector {
|
|
|
81
90
|
private removePending;
|
|
82
91
|
private markChanged;
|
|
83
92
|
}
|
|
93
|
+
/** The prior scan's resume point for an append-only event stream. */
|
|
94
|
+
export interface EventToolScanResumePoint {
|
|
95
|
+
/** ToolCallCollector snapshot after folding `eventCount` events. */
|
|
96
|
+
snapshot: ToolCallCollectorSnapshot;
|
|
97
|
+
/** How many events had been folded when the snapshot was taken. */
|
|
98
|
+
eventCount: number;
|
|
99
|
+
}
|
|
100
|
+
export interface EventToolScanResult {
|
|
101
|
+
/** The CHANGED calls — an append-safe upsert set, not the whole history. */
|
|
102
|
+
calls: IndexedToolCall[];
|
|
103
|
+
/** Serialized-ready snapshot to persist for the next incremental scan. */
|
|
104
|
+
snapshot: ToolCallCollectorSnapshot;
|
|
105
|
+
/** Events folded so far — the next scan's resume offset into `events`. */
|
|
106
|
+
eventCount: number;
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Derive tool calls from a full-file harness's normalized events, optionally
|
|
110
|
+
* RESUMING from a prior scan of the same append-only stream.
|
|
111
|
+
*
|
|
112
|
+
* Without `prior` this is a full parse from event 0 (identical to the legacy
|
|
113
|
+
* {@link toolCallsFromEvents}). With `prior`, the collector is seeded from the
|
|
114
|
+
* prior snapshot and only events at or after `prior.eventCount` are folded — so
|
|
115
|
+
* an active session that grew by a few turns re-derives (and re-redacts) only
|
|
116
|
+
* those new tool calls instead of re-sanitizing its entire history on every
|
|
117
|
+
* daemon warm tick (PHNX-3411). The ordinals continue deterministically from the
|
|
118
|
+
* snapshot, so folding [0..k) then [k..n) yields the same index as folding
|
|
119
|
+
* [0..n) once, and the CHANGED set is safe to persist with `mode: 'append'`.
|
|
120
|
+
*
|
|
121
|
+
* The caller is responsible for only supplying `prior` when the stream is still
|
|
122
|
+
* an append of what was scanned before (same source, un-truncated,
|
|
123
|
+
* `prior.eventCount <= events.length`); anything else must full-scan.
|
|
124
|
+
*/
|
|
125
|
+
export declare function scanEventToolCalls(events: SessionEvent[], prior?: EventToolScanResumePoint): EventToolScanResult;
|
|
84
126
|
/** Build indexed calls from the normalized parser contract used by full-file harnesses. */
|
|
85
127
|
export declare function toolCallsFromEvents(events: SessionEvent[]): IndexedToolCall[];
|
|
86
128
|
export declare function toolCallKey(sessionId: string, ordinal: number): string;
|