@phnx-labs/agents-cli 1.22.57 → 1.22.59
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 +294 -0
- package/README.md +29 -0
- package/dist/bootstrap.js +39 -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 +198 -23
- 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/routines.test-fixture.js +5 -0
- package/dist/commands/send.d.ts +2 -1
- package/dist/commands/send.js +7 -5
- package/dist/commands/sessions-picker.d.ts +11 -0
- package/dist/commands/sessions-picker.js +16 -0
- package/dist/commands/sessions-stats.js +37 -5
- package/dist/commands/sessions.js +40 -5
- package/dist/commands/share.d.ts +14 -0
- package/dist/commands/share.js +43 -2
- package/dist/commands/ssh.js +12 -1
- package/dist/commands/status.js +1 -1
- package/dist/commands/sync.js +83 -7
- package/dist/commands/traces.js +7 -0
- package/dist/commands/versions.js +12 -4
- package/dist/commands/view.js +7 -2
- 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/auto-pull-worker.js +7 -2
- 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/cloud/rush.d.ts +7 -0
- package/dist/lib/cloud/rush.js +29 -1
- package/dist/lib/daemon/daemon.d.ts +22 -0
- package/dist/lib/daemon/daemon.js +39 -0
- package/dist/lib/daemon/runner.d.ts +3 -0
- package/dist/lib/daemon/runner.js +86 -45
- package/dist/lib/daemon/session-index-service.js +9 -1
- 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/daemon-ticks.d.ts +15 -0
- package/dist/lib/daemon-ticks.js +26 -0
- package/dist/lib/device-config.d.ts +5 -1
- package/dist/lib/device-config.js +2 -2
- package/dist/lib/devices/connect.d.ts +17 -8
- package/dist/lib/devices/connect.js +31 -14
- package/dist/lib/devices/health.js +5 -1
- package/dist/lib/devices/pool.d.ts +25 -2
- package/dist/lib/devices/pool.js +32 -2
- package/dist/lib/devices/stats-cache.d.ts +0 -6
- package/dist/lib/devices/stats-cache.js +2 -9
- package/dist/lib/doctor-diff.d.ts +14 -0
- package/dist/lib/doctor-diff.js +120 -9
- package/dist/lib/fleet/manifest.d.ts +17 -0
- package/dist/lib/fleet/manifest.js +26 -0
- package/dist/lib/git.d.ts +38 -0
- package/dist/lib/git.js +58 -0
- package/dist/lib/hooks/install.d.ts +27 -11
- package/dist/lib/hooks/install.js +42 -17
- package/dist/lib/hosts/ready.d.ts +8 -0
- package/dist/lib/hosts/ready.js +13 -2
- 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 +43 -133
- package/dist/lib/installations/versions.js +94 -206
- package/dist/lib/monitors/config.d.ts +71 -3
- package/dist/lib/monitors/config.js +100 -12
- package/dist/lib/monitors/pid-watch.d.ts +35 -0
- package/dist/lib/monitors/pid-watch.js +45 -0
- package/dist/lib/monitors/remote.d.ts +18 -0
- package/dist/lib/monitors/remote.js +11 -0
- package/dist/lib/permissions.js +7 -2
- package/dist/lib/plugins/plugins.d.ts +17 -3
- package/dist/lib/plugins/plugins.js +84 -9
- package/dist/lib/plugins/skills.d.ts +8 -1
- package/dist/lib/plugins/skills.js +18 -2
- package/dist/lib/pty-server.d.ts +14 -0
- package/dist/lib/pty-server.js +49 -5
- 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/drivers/rush.js +5 -0
- 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 +65 -0
- package/dist/lib/self-update.js +138 -0
- package/dist/lib/session/active.d.ts +13 -1
- package/dist/lib/session/active.js +2 -0
- package/dist/lib/session/cloud.js +5 -0
- package/dist/lib/session/db.d.ts +51 -6
- package/dist/lib/session/db.js +266 -20
- 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/smart-launch.d.ts +6 -0
- package/dist/lib/smart-launch.js +5 -2
- package/dist/lib/staleness/writers/plugins.js +5 -2
- package/dist/lib/staleness/writers/sources.d.ts +5 -0
- package/dist/lib/staleness/writers/sources.js +2 -1
- package/dist/lib/staleness/writers/subagents.js +13 -3
- package/dist/lib/state.d.ts +7 -4
- package/dist/lib/state.js +7 -4
- package/dist/lib/subagents.js +8 -2
- 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/teams/scheduler.d.ts +10 -0
- package/dist/lib/teams/scheduler.js +8 -0
- 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 +128 -6
- package/dist/lib/traces/sync.js +294 -35
- package/dist/lib/traces/worker-template.js +154 -1
- package/dist/lib/view-types.d.ts +12 -0
- package/package.json +2 -2
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 = 44;
|
|
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,55 @@ 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
|
+
}
|
|
1208
|
+
if (fromVersion < 44) {
|
|
1209
|
+
// v43 -> v44: backfill duration_ms for sessions whose harness scan extractor
|
|
1210
|
+
// never derived it (PHNX-3457). rush/grok/kimi/cursor/muse/antigravity/hermes/
|
|
1211
|
+
// openclaw left duration_ms NULL — 52% of the corpus, 100% of rush — so the
|
|
1212
|
+
// console median was computed over only the ~48% that carried it, skewing it
|
|
1213
|
+
// short. Going forward resolveDurationMs() populates it at every upsert; this
|
|
1214
|
+
// repairs already-indexed rows in place from the timestamps they already store
|
|
1215
|
+
// (last_activity, itself resolved from the last-message time else file mtime,
|
|
1216
|
+
// minus the creation timestamp), so no transcript is re-parsed. julianday is
|
|
1217
|
+
// avoided because it does not accept a trailing 'Z'; the arithmetic is done in
|
|
1218
|
+
// JS with the exact Date.parse resolveDurationMs uses, keeping the backfill and
|
|
1219
|
+
// the live path consistent. Only rows with a positive span are touched; a NULL
|
|
1220
|
+
// that cannot be resolved stays NULL rather than becoming a fabricated 0.
|
|
1221
|
+
const nullDurationRows = db.prepare(`SELECT id, timestamp, last_activity FROM sessions
|
|
1222
|
+
WHERE duration_ms IS NULL AND last_activity IS NOT NULL`).all();
|
|
1223
|
+
// Runs inside migrateSchema's own transaction (db.ts:1468), so no nested
|
|
1224
|
+
// db.transaction() here — that would raise "transaction within a transaction".
|
|
1225
|
+
const update = db.prepare(`UPDATE sessions SET duration_ms = ? WHERE id = ?`);
|
|
1226
|
+
for (const row of nullDurationRows) {
|
|
1227
|
+
const startMs = Date.parse(row.timestamp);
|
|
1228
|
+
const lastMs = Date.parse(row.last_activity);
|
|
1229
|
+
if (Number.isFinite(startMs) && Number.isFinite(lastMs) && lastMs > startMs) {
|
|
1230
|
+
update.run(lastMs - startMs, row.id);
|
|
1231
|
+
}
|
|
1232
|
+
}
|
|
1233
|
+
}
|
|
1155
1234
|
}
|
|
1156
1235
|
/**
|
|
1157
1236
|
* Stamp `account_key` / `account_org` / `account` on every Claude row from its
|
|
@@ -2022,7 +2101,7 @@ export function upsertSession(meta, content, scan, assistantContent = '') {
|
|
|
2022
2101
|
cache_write_tokens: meta.cacheWriteTokens ?? null,
|
|
2023
2102
|
cost_usd: meta.costUsd ?? null,
|
|
2024
2103
|
cost_usd_nocache: meta.costUsdNoCache ?? null,
|
|
2025
|
-
duration_ms: meta
|
|
2104
|
+
duration_ms: resolveDurationMs(meta, scan),
|
|
2026
2105
|
model: meta.model ?? null,
|
|
2027
2106
|
tool_call_count: meta.toolCallCount ?? null,
|
|
2028
2107
|
file_path: meta.filePath,
|
|
@@ -2110,6 +2189,23 @@ export function upsertSessionsBatch(entries) {
|
|
|
2110
2189
|
? { ...entry, meta: { ...entry.meta, ...fanOutCounts(entry.events, entry.meta.agent) } }
|
|
2111
2190
|
: entry;
|
|
2112
2191
|
}
|
|
2192
|
+
// Harnesses whose scanners produce no events AND whose parseSession reads a
|
|
2193
|
+
// potentially large flat transcript file (not a compact SQLite DB). Calling
|
|
2194
|
+
// parseSession on the warm tick for an active large session wedges the Node
|
|
2195
|
+
// event loop for seconds, making browser IPC miss its connection window
|
|
2196
|
+
// (PHNX-3411). Defer their tool-call indexing to runDeferredToolIndex, which
|
|
2197
|
+
// uses ensureToolIndex with tool_scan_ledger stamps and byte/file budget caps.
|
|
2198
|
+
// NOT opencode — parseOpenCode issues a targeted SQLite query, so it is fast
|
|
2199
|
+
// even for large sessions and its results populate recentDirectoriesTouched.
|
|
2200
|
+
const LARGE_TRANSCRIPT_AGENTS = new Set(['kimi', 'grok']);
|
|
2201
|
+
if (!entry.events && LARGE_TRANSCRIPT_AGENTS.has(entry.meta.agent)) {
|
|
2202
|
+
return entry;
|
|
2203
|
+
}
|
|
2204
|
+
// Enrich the entry. When the scanner already provided events, use them
|
|
2205
|
+
// directly (no transcript re-read). When it didn't — e.g. OpenCode whose
|
|
2206
|
+
// scanner produces only metadata but whose parseOpenCode is a fast SQLite
|
|
2207
|
+
// query — fall back to parseSession. Agents that would call an expensive
|
|
2208
|
+
// flat-file parse already returned above.
|
|
2113
2209
|
try {
|
|
2114
2210
|
const toolSourcePath = toolEvidenceSourcePath(entry.meta.filePath, entry.meta.agent);
|
|
2115
2211
|
const toolScan = toolSourcePath === entry.meta.filePath
|
|
@@ -2118,11 +2214,23 @@ export function upsertSessionsBatch(entries) {
|
|
|
2118
2214
|
const stat = fs.statSync(toolSourcePath);
|
|
2119
2215
|
return { fileMtimeMs: stat.mtimeMs, fileSize: stat.size };
|
|
2120
2216
|
})();
|
|
2121
|
-
// Some non-resumable scanners already normalized the transcript while
|
|
2122
|
-
// deriving metadata. Reuse those events; scanners that only read summary
|
|
2123
|
-
// metadata fall back to exactly one normalized parse here.
|
|
2124
2217
|
const events = entry.events ?? parseSession(entry.meta.filePath, entry.meta.agent);
|
|
2125
2218
|
writeResourceUsage(entry.meta.id, events, entry.meta.cwd);
|
|
2219
|
+
// Resume the tool index from the last scan of this append-only stream when
|
|
2220
|
+
// it is safe to (PHNX-3411). A live session's transcript grows every tick,
|
|
2221
|
+
// so a full re-derive re-sanitizes its ENTIRE tool history each time — the
|
|
2222
|
+
// synchronous O(session) work that wedged the daemon event loop for the
|
|
2223
|
+
// 11 non-streaming harnesses. `planEventToolResume` returns the prior
|
|
2224
|
+
// snapshot only when the file is still an append of what was scanned
|
|
2225
|
+
// before; otherwise `prior` is null and this is a full replace from
|
|
2226
|
+
// event 0 (identical index, just re-derived).
|
|
2227
|
+
// No tool stamp (a scanner that carried no scan record) means the resume
|
|
2228
|
+
// point cannot be size-guarded, so full-scan rather than trust a stale
|
|
2229
|
+
// offset. persistToolCalls below also skips a resume-less write.
|
|
2230
|
+
const prior = toolScan
|
|
2231
|
+
? planEventToolResume(db, entry.meta.id, toolSourcePath, toolScan, events.length)
|
|
2232
|
+
: null;
|
|
2233
|
+
const scanned = scanEventToolCalls(events, prior ?? undefined);
|
|
2126
2234
|
return {
|
|
2127
2235
|
...entry,
|
|
2128
2236
|
meta: {
|
|
@@ -2131,11 +2239,14 @@ export function upsertSessionsBatch(entries) {
|
|
|
2131
2239
|
recentDirectoriesTouched: extractRecentDirectoriesTouched(events, entry.meta.cwd),
|
|
2132
2240
|
...fanOutCounts(events, entry.meta.agent),
|
|
2133
2241
|
},
|
|
2134
|
-
|
|
2242
|
+
// The CHANGED calls only. On a resume these are the newly appended tail
|
|
2243
|
+
// (append-safe upsert); on a full scan they are the whole history.
|
|
2244
|
+
toolCalls: scanned.calls,
|
|
2135
2245
|
toolScan,
|
|
2136
|
-
|
|
2137
|
-
//
|
|
2138
|
-
|
|
2246
|
+
toolIndexMode: (prior ? 'append' : 'replace'),
|
|
2247
|
+
// Persist where the NEXT scan resumes: the collector snapshot + how many
|
|
2248
|
+
// events this scan folded.
|
|
2249
|
+
toolResume: { parserState: JSON.stringify(scanned.snapshot), parsedOffset: scanned.eventCount },
|
|
2139
2250
|
};
|
|
2140
2251
|
}
|
|
2141
2252
|
catch {
|
|
@@ -2231,7 +2342,7 @@ export function upsertSessionsBatch(entries) {
|
|
|
2231
2342
|
cache_write_tokens: meta.cacheWriteTokens ?? null,
|
|
2232
2343
|
cost_usd: meta.costUsd ?? null,
|
|
2233
2344
|
cost_usd_nocache: meta.costUsdNoCache ?? null,
|
|
2234
|
-
duration_ms: meta
|
|
2345
|
+
duration_ms: resolveDurationMs(meta, scan),
|
|
2235
2346
|
model: meta.model ?? null,
|
|
2236
2347
|
tool_call_count: meta.toolCallCount ?? null,
|
|
2237
2348
|
file_path: meta.filePath,
|
|
@@ -2284,7 +2395,13 @@ export function upsertSessionsBatch(entries) {
|
|
|
2284
2395
|
if (!toolScan || !entry.toolCalls)
|
|
2285
2396
|
continue;
|
|
2286
2397
|
try {
|
|
2287
|
-
|
|
2398
|
+
// `resume` is set only by the full-file harness path above; claude/codex
|
|
2399
|
+
// pass none, so their tool ledger keeps carrying no event-offset resume
|
|
2400
|
+
// point (their resume rides the content-scan ledger instead) — unchanged.
|
|
2401
|
+
persistToolCalls(db, entry.meta, entry.toolCalls, toolScan, {
|
|
2402
|
+
mode: entry.toolIndexMode ?? 'replace',
|
|
2403
|
+
resume: entry.toolResume,
|
|
2404
|
+
});
|
|
2288
2405
|
}
|
|
2289
2406
|
catch {
|
|
2290
2407
|
// Boundary is intentionally retryable via tool_scan_ledger.
|
|
@@ -2522,6 +2639,36 @@ function resolveLastActivity(meta, scan) {
|
|
|
2522
2639
|
return new Date(scan.fileMtimeMs).toISOString();
|
|
2523
2640
|
return meta.timestamp;
|
|
2524
2641
|
}
|
|
2642
|
+
/**
|
|
2643
|
+
* The persisted wall-clock span for a session (PHNX-3457).
|
|
2644
|
+
*
|
|
2645
|
+
* `durationMs` is canonically `lastTs − firstTs`. Some harness scan extractors
|
|
2646
|
+
* derive it themselves from per-event timestamps (claude/codex/droid/gemini/
|
|
2647
|
+
* opencode set `meta.durationMs`); the rest (rush/grok/kimi/cursor/muse/
|
|
2648
|
+
* antigravity/hermes/openclaw) never did, so `duration_ms` landed NULL for them —
|
|
2649
|
+
* 52% of the corpus, including 100% of the dominant `rush` usage — and the
|
|
2650
|
+
* console median was computed over only the ~48% that happened to carry it,
|
|
2651
|
+
* skewing it short and misleading.
|
|
2652
|
+
*
|
|
2653
|
+
* This closes the gap at the single write boundary every harness funnels
|
|
2654
|
+
* through, so parity is automatic rather than per-extractor: when the extractor
|
|
2655
|
+
* already computed a precise span, keep it; otherwise derive it from the same
|
|
2656
|
+
* `timestamp` (creation) and `resolveLastActivity` (last event, itself resolved
|
|
2657
|
+
* from the harness's last-message time, else file mtime) that the row already
|
|
2658
|
+
* stores. Returns null only when no positive span can be established (a single
|
|
2659
|
+
* timestamped event, or a clock that runs backwards), which reads as NULL rather
|
|
2660
|
+
* than a fabricated 0.
|
|
2661
|
+
*/
|
|
2662
|
+
function resolveDurationMs(meta, scan) {
|
|
2663
|
+
if (meta.durationMs != null)
|
|
2664
|
+
return meta.durationMs;
|
|
2665
|
+
const startMs = Date.parse(meta.timestamp);
|
|
2666
|
+
const lastMs = Date.parse(resolveLastActivity(meta, scan));
|
|
2667
|
+
if (Number.isFinite(startMs) && Number.isFinite(lastMs) && lastMs > startMs) {
|
|
2668
|
+
return lastMs - startMs;
|
|
2669
|
+
}
|
|
2670
|
+
return null;
|
|
2671
|
+
}
|
|
2525
2672
|
export function isSessionActivityFresh(row, maxAgeMs, nowMs) {
|
|
2526
2673
|
const parsedActivityMs = Date.parse(row.last_activity ?? row.timestamp);
|
|
2527
2674
|
const activityMs = Number.isFinite(parsedActivityMs) ? parsedActivityMs : row.file_mtime_ms ?? undefined;
|
|
@@ -2815,6 +2962,26 @@ export function querySessions(options = {}) {
|
|
|
2815
2962
|
const trimmed = options.limit ? live.slice(0, options.limit) : live;
|
|
2816
2963
|
return trimmed.map(rowToMeta);
|
|
2817
2964
|
}
|
|
2965
|
+
/**
|
|
2966
|
+
* Cheap query for the daemon's deferred tool-index pass (PHNX-3411).
|
|
2967
|
+
*
|
|
2968
|
+
* Returns the most-recently-active sessions whose parseSession reads a large
|
|
2969
|
+
* flat transcript (kimi: wire.jsonl, grok: chat_history.jsonl). Their scanners
|
|
2970
|
+
* produce no events, so upsertSessionsBatch skips them in the warm tick to
|
|
2971
|
+
* avoid wedging the event loop. ensureToolIndex uses tool_scan_ledger stamps
|
|
2972
|
+
* to skip already-current rows and applies byte/file budget caps.
|
|
2973
|
+
*/
|
|
2974
|
+
export function querySessionsForDeferredToolIndex(limit) {
|
|
2975
|
+
const db = getDB();
|
|
2976
|
+
const rows = db.prepare(`
|
|
2977
|
+
SELECT * FROM sessions
|
|
2978
|
+
WHERE file_path IS NOT NULL
|
|
2979
|
+
AND agent IN ('kimi', 'grok')
|
|
2980
|
+
ORDER BY last_activity DESC, timestamp DESC
|
|
2981
|
+
LIMIT ?
|
|
2982
|
+
`).all(limit);
|
|
2983
|
+
return rows.map(rowToMeta);
|
|
2984
|
+
}
|
|
2818
2985
|
/** Count sessions matching the given filter options. */
|
|
2819
2986
|
export function countSessions(options = {}) {
|
|
2820
2987
|
const db = getDB();
|
|
@@ -2950,6 +3117,59 @@ export function writeSessionTopics(entries) {
|
|
|
2950
3117
|
}
|
|
2951
3118
|
})();
|
|
2952
3119
|
}
|
|
3120
|
+
/** Read cached failure phenotypes only when their transcript byte stamps still match. */
|
|
3121
|
+
export function readSessionPhenotypes(ids) {
|
|
3122
|
+
const db = getDB();
|
|
3123
|
+
const out = new Map();
|
|
3124
|
+
if (ids.length === 0)
|
|
3125
|
+
return out;
|
|
3126
|
+
const CHUNK = 400;
|
|
3127
|
+
for (let i = 0; i < ids.length; i += CHUNK) {
|
|
3128
|
+
const chunk = ids.slice(i, i + CHUNK);
|
|
3129
|
+
const placeholders = chunk.map(() => '?').join(',');
|
|
3130
|
+
const rows = db.prepare(`
|
|
3131
|
+
SELECT sp.session_id AS id, sp.phenotype_json AS phenotypeJson
|
|
3132
|
+
FROM session_phenotypes sp
|
|
3133
|
+
JOIN sessions s ON s.id = sp.session_id
|
|
3134
|
+
WHERE sp.session_id IN (${placeholders})
|
|
3135
|
+
AND sp.extractor_version = ?
|
|
3136
|
+
AND sp.file_mtime_ms IS s.file_mtime_ms
|
|
3137
|
+
AND sp.file_size IS s.file_size
|
|
3138
|
+
`).all(...chunk, SESSION_PHENOTYPE_EXTRACTOR_VERSION);
|
|
3139
|
+
for (const row of rows) {
|
|
3140
|
+
try {
|
|
3141
|
+
out.set(row.id, JSON.parse(row.phenotypeJson));
|
|
3142
|
+
}
|
|
3143
|
+
catch {
|
|
3144
|
+
// Invalid derived cache data is a miss and self-heals on the next write.
|
|
3145
|
+
}
|
|
3146
|
+
}
|
|
3147
|
+
}
|
|
3148
|
+
return out;
|
|
3149
|
+
}
|
|
3150
|
+
/** Persist failure phenotypes against the exact transcript bytes used to classify them. */
|
|
3151
|
+
export function writeSessionPhenotypes(entries) {
|
|
3152
|
+
if (entries.length === 0)
|
|
3153
|
+
return;
|
|
3154
|
+
const db = getDB();
|
|
3155
|
+
const stmt = db.prepare(`
|
|
3156
|
+
INSERT INTO session_phenotypes
|
|
3157
|
+
(session_id, file_mtime_ms, file_size, extractor_version, computed_at, phenotype_json)
|
|
3158
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
3159
|
+
ON CONFLICT(session_id) DO UPDATE SET
|
|
3160
|
+
file_mtime_ms = excluded.file_mtime_ms,
|
|
3161
|
+
file_size = excluded.file_size,
|
|
3162
|
+
extractor_version = excluded.extractor_version,
|
|
3163
|
+
computed_at = excluded.computed_at,
|
|
3164
|
+
phenotype_json = excluded.phenotype_json
|
|
3165
|
+
`);
|
|
3166
|
+
const now = Date.now();
|
|
3167
|
+
db.transaction(() => {
|
|
3168
|
+
for (const entry of entries) {
|
|
3169
|
+
stmt.run(entry.id, entry.fileMtimeMs, entry.fileSize, SESSION_PHENOTYPE_EXTRACTOR_VERSION, now, JSON.stringify(entry.phenotype));
|
|
3170
|
+
}
|
|
3171
|
+
})();
|
|
3172
|
+
}
|
|
2953
3173
|
/** Read one derived preview only when it matches the transcript bytes on disk. */
|
|
2954
3174
|
export function readSessionPreviewCache(id, sourceStamp) {
|
|
2955
3175
|
const row = getDB().prepare(`
|
|
@@ -3203,17 +3423,43 @@ export function queryResourceUsageStats(options) {
|
|
|
3203
3423
|
return db.prepare(sql).all(...allParams);
|
|
3204
3424
|
}
|
|
3205
3425
|
/**
|
|
3206
|
-
* Coverage of the resource-usage signal
|
|
3207
|
-
*
|
|
3208
|
-
*
|
|
3209
|
-
*
|
|
3210
|
-
*
|
|
3426
|
+
* Coverage of the resource-usage signal, as three honest facts:
|
|
3427
|
+
*
|
|
3428
|
+
* - `scanned` — sessions the resource extractor has actually processed, i.e.
|
|
3429
|
+
* those carrying a `resource_scan_ledger` row at the current
|
|
3430
|
+
* `RESOURCE_INDEX_VERSION`. This is the true "has the historical backfill run"
|
|
3431
|
+
* signal: the ledger is stamped for EVERY scanned session, including ones that
|
|
3432
|
+
* invoked nothing (`resource_count = 0`), so `scanned/total` rises to ~1 after
|
|
3433
|
+
* `agents sessions backfill resources` regardless of how sparse explicit
|
|
3434
|
+
* invocations are.
|
|
3435
|
+
* - `covered` — distinct sessions that carry AT LEAST ONE row in
|
|
3436
|
+
* `session_resource_usage`, i.e. that actually recorded an explicit invocation.
|
|
3437
|
+
* This is an ABSOLUTE signal count, not a coverage ratio: it stays small even
|
|
3438
|
+
* at full scan coverage because most sessions invoke no skill/command, and a
|
|
3439
|
+
* non-recording harness contributes none by construction.
|
|
3440
|
+
* - `total` — sessions indexed.
|
|
3441
|
+
*
|
|
3442
|
+
* The two were previously conflated: `covered/total` was framed as coverage and
|
|
3443
|
+
* read ~1.2% even after a full backfill (most sessions genuinely invoke nothing),
|
|
3444
|
+
* so the "run the backfill" hint never cleared. Keying the hint on `scanned/total`
|
|
3445
|
+
* fixes that — see `commands/sessions-stats.ts` (PHNX-2301).
|
|
3211
3446
|
*/
|
|
3212
3447
|
export function resourceUsageCoverage() {
|
|
3213
3448
|
const db = getDB();
|
|
3214
3449
|
const covered = db.prepare(`SELECT COUNT(DISTINCT session_id) AS n FROM session_resource_usage`).get().n;
|
|
3450
|
+
// JOIN sessions so a ledger row for a since-vanished transcript (dropped from
|
|
3451
|
+
// `sessions` but not yet from the ledger) can't inflate scan coverage past the
|
|
3452
|
+
// indexed set. Only rows at the current extractor version count as scanned —
|
|
3453
|
+
// a stale-version row is re-derived on the next backfill, so it is not yet
|
|
3454
|
+
// "covered" for this extractor.
|
|
3455
|
+
const scanned = db.prepare(`
|
|
3456
|
+
SELECT COUNT(*) AS n
|
|
3457
|
+
FROM resource_scan_ledger l
|
|
3458
|
+
JOIN sessions s ON s.id = l.session_id
|
|
3459
|
+
WHERE l.extractor_version = ?
|
|
3460
|
+
`).get(RESOURCE_INDEX_VERSION).n;
|
|
3215
3461
|
const total = db.prepare(`SELECT COUNT(*) AS n FROM sessions`).get().n;
|
|
3216
|
-
return { covered, total };
|
|
3462
|
+
return { covered, scanned, total };
|
|
3217
3463
|
}
|
|
3218
3464
|
/** Has this session's resource usage been derived at the current extractor version for this exact file? */
|
|
3219
3465
|
function needsResourceIndex(db, sessionId, stamp) {
|
|
@@ -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
|
}
|