@phnx-labs/agents-cli 1.22.26 → 1.22.27
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 +244 -0
- package/README.md +18 -1
- package/dist/bin/agents +0 -0
- package/dist/commands/bench.d.ts +2 -0
- package/dist/commands/bench.js +101 -0
- package/dist/commands/doctor.js +24 -10
- package/dist/commands/exec.js +17 -16
- package/dist/commands/insights.js +39 -5
- package/dist/commands/output.js +100 -25
- package/dist/commands/resume.d.ts +11 -0
- package/dist/commands/resume.js +51 -0
- package/dist/commands/sessions-picker.js +12 -7
- package/dist/commands/sessions.d.ts +47 -1
- package/dist/commands/sessions.js +345 -37
- package/dist/commands/ssh.js +184 -11
- package/dist/commands/teams.js +3 -3
- package/dist/index.js +16 -20
- package/dist/lib/auth-health.d.ts +47 -3
- package/dist/lib/auth-health.js +74 -15
- package/dist/lib/bench/index.d.ts +4 -0
- package/dist/lib/bench/index.js +4 -0
- package/dist/lib/bench/runner.d.ts +16 -0
- package/dist/lib/bench/runner.js +111 -0
- package/dist/lib/bench/schema.d.ts +5 -0
- package/dist/lib/bench/schema.js +91 -0
- package/dist/lib/bench/storage.d.ts +5 -0
- package/dist/lib/bench/storage.js +32 -0
- package/dist/lib/bench/types.d.ts +40 -0
- package/dist/lib/bench/types.js +1 -0
- package/dist/lib/claude-account-token.d.ts +2 -0
- package/dist/lib/claude-account-token.js +27 -1
- package/dist/lib/crabbox/cli.d.ts +2 -0
- package/dist/lib/crabbox/cli.js +2 -0
- package/dist/lib/crabbox/lease.js +7 -1
- package/dist/lib/daemon.js +20 -0
- package/dist/lib/devices/connect.d.ts +2 -0
- package/dist/lib/devices/connect.js +7 -0
- package/dist/lib/devices/doctor-findings.d.ts +4 -1
- package/dist/lib/devices/doctor-findings.js +15 -1
- package/dist/lib/devices/harness-inventory.d.ts +97 -0
- package/dist/lib/devices/harness-inventory.js +0 -0
- package/dist/lib/devices/registry.d.ts +2 -0
- package/dist/lib/devices/ssh-config.js +3 -0
- package/dist/lib/devices/windows-ssh-enrollment.d.ts +20 -0
- package/dist/lib/devices/windows-ssh-enrollment.js +98 -0
- package/dist/lib/exec.bench.d.ts +1 -0
- package/dist/lib/exec.bench.js +186 -0
- package/dist/lib/exec.js +18 -8
- package/dist/lib/fleet/apply.js +9 -7
- package/dist/lib/fleet/remote-login.d.ts +4 -3
- package/dist/lib/fleet/remote-login.js +11 -9
- package/dist/lib/gemini-settings.d.ts +0 -1
- package/dist/lib/gemini-settings.js +12 -7
- package/dist/lib/hooks.d.ts +8 -5
- package/dist/lib/hooks.js +10 -6
- package/dist/lib/hosts/dispatch.js +7 -3
- package/dist/lib/hosts/passthrough.d.ts +22 -0
- package/dist/lib/hosts/passthrough.js +14 -8
- package/dist/lib/hosts/progress.d.ts +2 -1
- package/dist/lib/hosts/progress.js +4 -3
- package/dist/lib/hosts/providers/devices.js +1 -0
- package/dist/lib/hosts/ready.d.ts +4 -2
- package/dist/lib/hosts/ready.js +24 -8
- package/dist/lib/hosts/reconcile.d.ts +1 -1
- package/dist/lib/hosts/reconcile.js +16 -7
- package/dist/lib/hosts/reconnect.d.ts +45 -12
- package/dist/lib/hosts/reconnect.js +90 -34
- package/dist/lib/hosts/registry.d.ts +2 -2
- package/dist/lib/hosts/registry.js +3 -5
- package/dist/lib/hosts/remote-cmd.d.ts +17 -0
- package/dist/lib/hosts/remote-cmd.js +29 -0
- package/dist/lib/hosts/tasks.d.ts +2 -0
- package/dist/lib/hosts/types.d.ts +1 -0
- package/dist/lib/hosts/types.js +3 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/CodeResources +0 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
- package/dist/lib/menubar/install-menubar.d.ts +9 -6
- package/dist/lib/menubar/install-menubar.js +20 -9
- package/dist/lib/models.d.ts +30 -1
- package/dist/lib/models.js +42 -144
- package/dist/lib/pricing/cost.d.ts +9 -0
- package/dist/lib/pricing/cost.js +24 -0
- package/dist/lib/pricing/index.d.ts +1 -1
- package/dist/lib/pricing/index.js +1 -1
- package/dist/lib/redact.js +8 -3
- package/dist/lib/remote-agents-json.d.ts +32 -0
- package/dist/lib/remote-agents-json.js +47 -16
- package/dist/lib/resource-profiles.js +1 -2
- package/dist/lib/routine-notify-owner.d.ts +102 -0
- package/dist/lib/routine-notify-owner.js +232 -0
- package/dist/lib/routines.d.ts +10 -0
- package/dist/lib/runner.d.ts +9 -1
- package/dist/lib/runner.js +182 -17
- package/dist/lib/sandbox.d.ts +0 -2
- package/dist/lib/sandbox.js +2 -19
- package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
- package/dist/lib/secrets/agent.js +3 -2
- package/dist/lib/secrets/lease.d.ts +25 -0
- package/dist/lib/secrets/lease.js +44 -0
- package/dist/lib/self-update.d.ts +20 -5
- package/dist/lib/self-update.js +93 -16
- package/dist/lib/session/db.d.ts +19 -2
- package/dist/lib/session/db.js +54 -5
- package/dist/lib/session/discover.d.ts +21 -1
- package/dist/lib/session/discover.js +124 -32
- package/dist/lib/session/insights.d.ts +19 -0
- package/dist/lib/session/insights.js +78 -10
- package/dist/lib/session/recovery.d.ts +17 -1
- package/dist/lib/session/recovery.js +111 -4
- package/dist/lib/session/remote-list.d.ts +10 -9
- package/dist/lib/session/remote-list.js +25 -23
- package/dist/lib/session/resume-owner.d.ts +55 -0
- package/dist/lib/session/resume-owner.js +69 -0
- package/dist/lib/session/team-filter.d.ts +65 -0
- package/dist/lib/session/team-filter.js +98 -3
- package/dist/lib/session/types.d.ts +33 -0
- package/dist/lib/smart-launch.d.ts +31 -5
- package/dist/lib/smart-launch.js +43 -7
- package/dist/lib/ssh-exec.d.ts +27 -0
- package/dist/lib/ssh-exec.js +34 -1
- package/dist/lib/ssh-tunnel.d.ts +3 -2
- package/dist/lib/ssh-tunnel.js +25 -16
- package/dist/lib/startup/command-registry.d.ts +1 -0
- package/dist/lib/startup/command-registry.js +2 -0
- package/dist/lib/teams/placement-probe.d.ts +1 -1
- package/dist/lib/teams/placement-probe.js +24 -24
- package/dist/lib/tmux/session.d.ts +8 -0
- package/dist/lib/tmux/session.js +22 -0
- package/dist/lib/types.d.ts +2 -0
- package/package.json +1 -1
|
@@ -20,6 +20,8 @@ import { parseTimeFilter } from './relative-time.js';
|
|
|
20
20
|
const execFileAsync = promisify(execFile);
|
|
21
21
|
import { AGENTS, agentConfigDirName, getCliVersion } from '../agents.js';
|
|
22
22
|
import { walkForFilesWithStat } from '../fs-walk.js';
|
|
23
|
+
import { hasCommand } from '../cli-resources.js';
|
|
24
|
+
import { execFileShellSpec } from '../platform/exec.js';
|
|
23
25
|
import { getConfigSymlinkVersion } from '../shims.js';
|
|
24
26
|
import { SESSION_AGENTS } from './types.js';
|
|
25
27
|
import { deriveShortId } from './short-id.js';
|
|
@@ -28,7 +30,7 @@ import { extractSessionTopic, extractSlashCommandName, extractSlashCommandFromTo
|
|
|
28
30
|
import { isSkillInvocation, extractSkills, extractSlashCommands } from './highlights.js';
|
|
29
31
|
import { parseAntigravity, parseCursor } from './parse.js';
|
|
30
32
|
import { extractPrUrl, detectWorktree, detectTicket, isPrCreateCommand, detectSpawnedTeam, isTicketCreateTool, extractCreatedTicket, extractRecentDirectoriesTouched, extractTodoProgressFromEvents } from './state.js';
|
|
31
|
-
import { costOfUsage } from '../pricing/index.js';
|
|
33
|
+
import { costOfUsage, costOfUsageNoCache } from '../pricing/index.js';
|
|
32
34
|
import { machineForSessionFile } from './origin-machine.js';
|
|
33
35
|
export { machineForSessionFile } from './origin-machine.js';
|
|
34
36
|
import { mapBounded } from '../concurrency.js';
|
|
@@ -1096,7 +1098,11 @@ async function readClaudeMeta(filePath, sessionId, scanStamp, priorRow, label) {
|
|
|
1096
1098
|
toolCallCount: scan.toolCallCount,
|
|
1097
1099
|
tokenCount: scan.tokenCount,
|
|
1098
1100
|
outputTokens: scan.outputTokens,
|
|
1101
|
+
inputTokens: scan.inputTokens,
|
|
1102
|
+
cacheReadTokens: scan.cacheReadTokens,
|
|
1103
|
+
cacheWriteTokens: scan.cacheWriteTokens,
|
|
1099
1104
|
costUsd: scan.costUsd,
|
|
1105
|
+
costUsdNoCache: scan.costUsdNoCache,
|
|
1100
1106
|
durationMs: scan.durationMs,
|
|
1101
1107
|
isTeamOrigin,
|
|
1102
1108
|
prUrl: scan.prUrl,
|
|
@@ -1130,7 +1136,11 @@ async function readClaudeMeta(filePath, sessionId, scanStamp, priorRow, label) {
|
|
|
1130
1136
|
toolCallCount: scan.toolCallCount,
|
|
1131
1137
|
tokenCount: scan.tokenCount,
|
|
1132
1138
|
outputTokens: scan.outputTokens,
|
|
1139
|
+
inputTokens: scan.inputTokens,
|
|
1140
|
+
cacheReadTokens: scan.cacheReadTokens,
|
|
1141
|
+
cacheWriteTokens: scan.cacheWriteTokens,
|
|
1133
1142
|
costUsd: scan.costUsd,
|
|
1143
|
+
costUsdNoCache: scan.costUsdNoCache,
|
|
1134
1144
|
durationMs: scan.durationMs,
|
|
1135
1145
|
topic: scan.topic,
|
|
1136
1146
|
isTeamOrigin,
|
|
@@ -1438,7 +1448,11 @@ export async function readCodexMeta(filePath, resolveAccount, currentVersion, sc
|
|
|
1438
1448
|
messageCount: scan.messageCount,
|
|
1439
1449
|
tokenCount: scan.tokenCount,
|
|
1440
1450
|
outputTokens: scan.outputTokens,
|
|
1451
|
+
inputTokens: scan.inputTokens,
|
|
1452
|
+
cacheReadTokens: scan.cacheReadTokens,
|
|
1453
|
+
cacheWriteTokens: scan.cacheWriteTokens,
|
|
1441
1454
|
costUsd: scan.costUsd,
|
|
1455
|
+
costUsdNoCache: scan.costUsdNoCache,
|
|
1442
1456
|
durationMs: scan.durationMs,
|
|
1443
1457
|
account: resolveAccount?.(),
|
|
1444
1458
|
prUrl: scan.prUrl,
|
|
@@ -1576,8 +1590,11 @@ function readGeminiMeta(filePath, hashDir, projectMap, currentVersion) {
|
|
|
1576
1590
|
let messageCount = 0;
|
|
1577
1591
|
let tokenCount = 0;
|
|
1578
1592
|
let outputTokens = 0;
|
|
1593
|
+
let inputTokens = 0;
|
|
1594
|
+
let cacheReadTokens = 0;
|
|
1579
1595
|
let sawTokenCount = false;
|
|
1580
1596
|
let costUsd = 0;
|
|
1597
|
+
let costUsdNoCache = 0;
|
|
1581
1598
|
let sawCost = false;
|
|
1582
1599
|
let firstTsMs;
|
|
1583
1600
|
let lastTsMs;
|
|
@@ -1622,21 +1639,28 @@ function readGeminiMeta(filePath, hashDir, projectMap, currentVersion) {
|
|
|
1622
1639
|
(typeof gtk.output === 'number' ? gtk.output : 0) +
|
|
1623
1640
|
(typeof gtk.thoughts === 'number' ? gtk.thoughts : 0) +
|
|
1624
1641
|
(typeof gtk.tool === 'number' ? gtk.tool : 0);
|
|
1642
|
+
// Burn split (RUSH-2287): Gemini has uncached input + cache-read, no cache-write.
|
|
1643
|
+
if (typeof gtk.input === 'number')
|
|
1644
|
+
inputTokens += gtk.input;
|
|
1645
|
+
if (typeof gtk.cached === 'number')
|
|
1646
|
+
cacheReadTokens += gtk.cached;
|
|
1625
1647
|
}
|
|
1626
1648
|
// Per-message cost: directional tokens × this message's model price.
|
|
1627
1649
|
const msgModel = (typeof message.model === 'string' ? message.model : undefined) || sessionModel;
|
|
1628
1650
|
const tk = message.tokens;
|
|
1629
1651
|
if (msgModel && tk && typeof tk === 'object') {
|
|
1630
|
-
const
|
|
1652
|
+
const usage = {
|
|
1631
1653
|
model: msgModel,
|
|
1632
1654
|
inputTokens: typeof tk.input === 'number' ? tk.input : undefined,
|
|
1633
1655
|
outputTokens: (typeof tk.output === 'number' ? tk.output : 0) +
|
|
1634
1656
|
(typeof tk.thoughts === 'number' ? tk.thoughts : 0) +
|
|
1635
1657
|
(typeof tk.tool === 'number' ? tk.tool : 0),
|
|
1636
1658
|
cacheReadTokens: typeof tk.cached === 'number' ? tk.cached : undefined,
|
|
1637
|
-
}
|
|
1659
|
+
};
|
|
1660
|
+
const c = costOfUsage(usage);
|
|
1638
1661
|
if (c > 0) {
|
|
1639
1662
|
costUsd += c;
|
|
1663
|
+
costUsdNoCache += costOfUsageNoCache(usage);
|
|
1640
1664
|
sawCost = true;
|
|
1641
1665
|
}
|
|
1642
1666
|
}
|
|
@@ -1659,7 +1683,10 @@ function readGeminiMeta(filePath, hashDir, projectMap, currentVersion) {
|
|
|
1659
1683
|
messageCount,
|
|
1660
1684
|
tokenCount: sawTokenCount ? tokenCount : undefined,
|
|
1661
1685
|
outputTokens: sawTokenCount ? outputTokens : undefined,
|
|
1686
|
+
inputTokens: sawTokenCount ? inputTokens : undefined,
|
|
1687
|
+
cacheReadTokens: sawTokenCount ? cacheReadTokens : undefined,
|
|
1662
1688
|
costUsd: sawCost ? costUsd : undefined,
|
|
1689
|
+
costUsdNoCache: sawCost ? costUsdNoCache : undefined,
|
|
1663
1690
|
durationMs,
|
|
1664
1691
|
};
|
|
1665
1692
|
return { meta, content: userTexts.join('\n') };
|
|
@@ -2053,13 +2080,12 @@ async function scanOpenCodeIncremental() {
|
|
|
2053
2080
|
// ---------------------------------------------------------------------------
|
|
2054
2081
|
/** Scan active OpenClaw channels and cron jobs via the openclaw CLI. */
|
|
2055
2082
|
async function scanOpenClawIncremental() {
|
|
2056
|
-
// Check if openclaw is installed — silently skip if not.
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
|
|
2083
|
+
// Check if openclaw is installed — silently skip if not. `which` is POSIX-only
|
|
2084
|
+
// (Windows resolves PATH with `where`), so a bare `which` throws ENOENT on every
|
|
2085
|
+
// Windows run and silently disabled the entire OpenClaw scan there — its sessions
|
|
2086
|
+
// never reached the index (RUSH-2286). hasCommand() probes cross-platform.
|
|
2087
|
+
if (!hasCommand('openclaw'))
|
|
2061
2088
|
return;
|
|
2062
|
-
}
|
|
2063
2089
|
// TTL cache: skip subprocess calls if we scanned recently. Stored in the
|
|
2064
2090
|
// meta table so we skip even when no channels/cron exist to produce rows.
|
|
2065
2091
|
const db = getDB();
|
|
@@ -2073,8 +2099,13 @@ async function scanOpenClawIncremental() {
|
|
|
2073
2099
|
const scan = { fileMtimeMs: now, fileSize: 0 };
|
|
2074
2100
|
const entries = [];
|
|
2075
2101
|
try {
|
|
2076
|
-
|
|
2102
|
+
// On Windows `openclaw` resolves to a .cmd/.ps1 shim that execFile can't launch
|
|
2103
|
+
// directly; execFileShellSpec composes a shell-safe invocation there and is a
|
|
2104
|
+
// no-op passthrough on POSIX (RUSH-2286).
|
|
2105
|
+
const channelsSpec = execFileShellSpec('openclaw', ['channels', 'status']);
|
|
2106
|
+
const { stdout: output } = await execFileAsync(channelsSpec.command, channelsSpec.args, {
|
|
2077
2107
|
encoding: 'utf-8',
|
|
2108
|
+
shell: channelsSpec.shell,
|
|
2078
2109
|
});
|
|
2079
2110
|
for (const line of output.split('\n')) {
|
|
2080
2111
|
const match = line.match(/^-\s+\w+\s+(\S+)\s+\((\w+)\):\s*(.+)/);
|
|
@@ -2103,8 +2134,10 @@ async function scanOpenClawIncremental() {
|
|
|
2103
2134
|
/* channels command failed */
|
|
2104
2135
|
}
|
|
2105
2136
|
try {
|
|
2106
|
-
const
|
|
2137
|
+
const cronSpec = execFileShellSpec('openclaw', ['cron', 'list']);
|
|
2138
|
+
const { stdout: output } = await execFileAsync(cronSpec.command, cronSpec.args, {
|
|
2107
2139
|
encoding: 'utf-8',
|
|
2140
|
+
shell: cronSpec.shell,
|
|
2108
2141
|
});
|
|
2109
2142
|
const lines = output.split('\n');
|
|
2110
2143
|
for (let i = 1; i < lines.length; i++) {
|
|
@@ -2610,15 +2643,19 @@ async function readDroidMeta(filePath, currentVersion) {
|
|
|
2610
2643
|
const settings = readDroidSettings(filePath.replace(/\.jsonl$/, '.settings.json'));
|
|
2611
2644
|
const model = settings.model || scan.model;
|
|
2612
2645
|
const tokenCount = settings.tokenCount;
|
|
2613
|
-
|
|
2614
|
-
|
|
2646
|
+
// Droid records a full split (input / cache-read / cache-write / output) in its
|
|
2647
|
+
// settings sidecar, so both the actual and no-cache cost are derivable (RUSH-2287).
|
|
2648
|
+
const usageForCost = model && settings.usage
|
|
2649
|
+
? {
|
|
2615
2650
|
model,
|
|
2616
2651
|
inputTokens: settings.usage.inputTokens,
|
|
2617
2652
|
outputTokens: settings.usage.outputTokens,
|
|
2618
2653
|
cacheReadTokens: settings.usage.cacheReadTokens,
|
|
2619
2654
|
cacheCreationTokens: settings.usage.cacheCreationTokens,
|
|
2620
|
-
}
|
|
2621
|
-
:
|
|
2655
|
+
}
|
|
2656
|
+
: undefined;
|
|
2657
|
+
const costUsd = usageForCost ? costOfUsage(usageForCost) : 0;
|
|
2658
|
+
const costUsdNoCache = usageForCost ? costOfUsageNoCache(usageForCost) : 0;
|
|
2622
2659
|
const stat = safeStatSync(filePath);
|
|
2623
2660
|
const cwd = normalizeCwd(scan.cwd || '');
|
|
2624
2661
|
const meta = {
|
|
@@ -2636,7 +2673,11 @@ async function readDroidMeta(filePath, currentVersion) {
|
|
|
2636
2673
|
messageCount: scan.messageCount,
|
|
2637
2674
|
tokenCount,
|
|
2638
2675
|
outputTokens: settings.usage?.outputTokens,
|
|
2676
|
+
inputTokens: settings.usage?.inputTokens,
|
|
2677
|
+
cacheReadTokens: settings.usage?.cacheReadTokens,
|
|
2678
|
+
cacheWriteTokens: settings.usage?.cacheCreationTokens,
|
|
2639
2679
|
costUsd: costUsd > 0 ? costUsd : undefined,
|
|
2680
|
+
costUsdNoCache: costUsd > 0 ? costUsdNoCache : undefined,
|
|
2640
2681
|
durationMs: scan.durationMs,
|
|
2641
2682
|
};
|
|
2642
2683
|
return { meta, content: scan.contentText || '' };
|
|
@@ -2784,8 +2825,12 @@ export function initClaudeParseState() {
|
|
|
2784
2825
|
toolCallCount: 0,
|
|
2785
2826
|
tokenCount: 0,
|
|
2786
2827
|
outputTokens: 0,
|
|
2828
|
+
inputTokens: 0,
|
|
2829
|
+
cacheReadTokens: 0,
|
|
2830
|
+
cacheWriteTokens: 0,
|
|
2787
2831
|
sawTokenCount: false,
|
|
2788
2832
|
costUsd: 0,
|
|
2833
|
+
costUsdNoCache: 0,
|
|
2789
2834
|
sawCost: false,
|
|
2790
2835
|
firstTsMs: undefined,
|
|
2791
2836
|
lastTsMs: undefined,
|
|
@@ -2982,21 +3027,34 @@ export function applyClaudeLine(state, parsed) {
|
|
|
2982
3027
|
}
|
|
2983
3028
|
if (typeof usageObj?.output_tokens === 'number')
|
|
2984
3029
|
state.outputTokens += usageObj.output_tokens;
|
|
3030
|
+
// Burn split (RUSH-2287): accumulate the raw directional counts so `agents
|
|
3031
|
+
// output` can report uncached-input / cache-read / cache-write separately.
|
|
3032
|
+
if (usageObj && typeof usageObj === 'object') {
|
|
3033
|
+
if (typeof usageObj.input_tokens === 'number')
|
|
3034
|
+
state.inputTokens += usageObj.input_tokens;
|
|
3035
|
+
if (typeof usageObj.cache_read_input_tokens === 'number')
|
|
3036
|
+
state.cacheReadTokens += usageObj.cache_read_input_tokens;
|
|
3037
|
+
if (typeof usageObj.cache_creation_input_tokens === 'number')
|
|
3038
|
+
state.cacheWriteTokens += usageObj.cache_creation_input_tokens;
|
|
3039
|
+
}
|
|
2985
3040
|
// Per-assistant-message cost: each event carries its own model, so we
|
|
2986
3041
|
// multiply that event's raw token directions by that model's price.
|
|
2987
3042
|
const model = parsed.message?.model;
|
|
2988
3043
|
if (typeof model === 'string' && model)
|
|
2989
3044
|
state.model = model;
|
|
2990
3045
|
if (model && usageObj && typeof usageObj === 'object') {
|
|
2991
|
-
const
|
|
3046
|
+
const usageForCost = {
|
|
2992
3047
|
model,
|
|
2993
3048
|
inputTokens: usageObj.input_tokens,
|
|
2994
3049
|
outputTokens: usageObj.output_tokens,
|
|
2995
3050
|
cacheReadTokens: usageObj.cache_read_input_tokens,
|
|
2996
3051
|
cacheCreationTokens: usageObj.cache_creation_input_tokens,
|
|
2997
|
-
}
|
|
3052
|
+
};
|
|
3053
|
+
const eventCost = costOfUsage(usageForCost);
|
|
2998
3054
|
if (eventCost > 0) {
|
|
2999
3055
|
state.costUsd += eventCost;
|
|
3056
|
+
// No-cache scenario: reprice this same event's cache tokens at the input rate.
|
|
3057
|
+
state.costUsdNoCache += costOfUsageNoCache(usageForCost);
|
|
3000
3058
|
state.sawCost = true;
|
|
3001
3059
|
}
|
|
3002
3060
|
}
|
|
@@ -3026,7 +3084,11 @@ export function finalizeClaudeScan(state) {
|
|
|
3026
3084
|
toolCallCount: state.toolCallCount,
|
|
3027
3085
|
tokenCount: state.sawTokenCount ? state.tokenCount : undefined,
|
|
3028
3086
|
outputTokens: state.sawTokenCount ? state.outputTokens : undefined,
|
|
3087
|
+
inputTokens: state.sawTokenCount ? state.inputTokens : undefined,
|
|
3088
|
+
cacheReadTokens: state.sawTokenCount ? state.cacheReadTokens : undefined,
|
|
3089
|
+
cacheWriteTokens: state.sawTokenCount ? state.cacheWriteTokens : undefined,
|
|
3029
3090
|
costUsd: state.sawCost ? state.costUsd : undefined,
|
|
3091
|
+
costUsdNoCache: state.sawCost ? state.costUsdNoCache : undefined,
|
|
3030
3092
|
durationMs,
|
|
3031
3093
|
lastActivity: state.lastTsMs !== undefined ? new Date(state.lastTsMs).toISOString() : undefined,
|
|
3032
3094
|
contentText: state.userTexts.length > 0 ? state.userTexts.join('\n') : undefined,
|
|
@@ -3082,7 +3144,7 @@ export function serializeClaudeParserState(state, offset, jsonlDroppingOversized
|
|
|
3082
3144
|
: allIds;
|
|
3083
3145
|
const ticket = detectTicket(state.userTexts.join('\n') || undefined, state.gitBranch);
|
|
3084
3146
|
return {
|
|
3085
|
-
v:
|
|
3147
|
+
v: 3,
|
|
3086
3148
|
offset,
|
|
3087
3149
|
jsonlDroppingOversizedLine: jsonlDroppingOversizedLine || undefined,
|
|
3088
3150
|
timestamp: state.timestamp,
|
|
@@ -3101,9 +3163,13 @@ export function serializeClaudeParserState(state, offset, jsonlDroppingOversized
|
|
|
3101
3163
|
toolCallCount: state.toolCallCount,
|
|
3102
3164
|
tokenCount: state.tokenCount,
|
|
3103
3165
|
outputTokens: state.outputTokens,
|
|
3166
|
+
inputTokens: state.inputTokens,
|
|
3167
|
+
cacheReadTokens: state.cacheReadTokens,
|
|
3168
|
+
cacheWriteTokens: state.cacheWriteTokens,
|
|
3104
3169
|
sawTokenCount: state.sawTokenCount,
|
|
3105
3170
|
sawCost: state.sawCost,
|
|
3106
3171
|
costUsd: state.costUsd,
|
|
3172
|
+
costUsdNoCache: state.costUsdNoCache,
|
|
3107
3173
|
seenIdsSize: state.seenAssistantIds.size,
|
|
3108
3174
|
seenIdsRecent,
|
|
3109
3175
|
sawPrCreate: state.sawPrCreate,
|
|
@@ -3163,8 +3229,12 @@ export function hydrateClaudeParseState(prior) {
|
|
|
3163
3229
|
toolCallCount: prior.toolCallCount ?? 0,
|
|
3164
3230
|
tokenCount: prior.tokenCount,
|
|
3165
3231
|
outputTokens: prior.outputTokens,
|
|
3232
|
+
inputTokens: prior.inputTokens ?? 0,
|
|
3233
|
+
cacheReadTokens: prior.cacheReadTokens ?? 0,
|
|
3234
|
+
cacheWriteTokens: prior.cacheWriteTokens ?? 0,
|
|
3166
3235
|
sawTokenCount: prior.sawTokenCount,
|
|
3167
3236
|
costUsd: prior.costUsd,
|
|
3237
|
+
costUsdNoCache: prior.costUsdNoCache ?? 0,
|
|
3168
3238
|
sawCost: prior.sawCost,
|
|
3169
3239
|
firstTsMs: prior.firstTsMs,
|
|
3170
3240
|
lastTsMs: prior.lastTsMs,
|
|
@@ -3313,7 +3383,7 @@ function parsePriorClaudeState(row) {
|
|
|
3313
3383
|
return null;
|
|
3314
3384
|
try {
|
|
3315
3385
|
const parsed = JSON.parse(row.parserState);
|
|
3316
|
-
if (parsed?.v !==
|
|
3386
|
+
if (parsed?.v !== 3 || typeof parsed.offset !== 'number' || parsed.toolCalls?.v !== 1)
|
|
3317
3387
|
return null;
|
|
3318
3388
|
return parsed;
|
|
3319
3389
|
}
|
|
@@ -3469,17 +3539,34 @@ export function applyCodexLine(state, parsed) {
|
|
|
3469
3539
|
* exact return-building {@link scanCodexSession} used to run inline.
|
|
3470
3540
|
*/
|
|
3471
3541
|
export function finalizeCodexScan(state) {
|
|
3472
|
-
//
|
|
3542
|
+
// Codex reports one cumulative snapshot: uncached input, cached (cache-read)
|
|
3543
|
+
// input, and output+reasoning. It has no cache-write bucket. Derive the burn
|
|
3544
|
+
// split and both costs (actual + no-cache) from that final snapshot (RUSH-2287).
|
|
3545
|
+
const snap = state.lastTotalTokenUsage;
|
|
3546
|
+
const outputTokens = snap
|
|
3547
|
+
? (snap.output_tokens ?? 0) + (snap.reasoning_output_tokens ?? 0)
|
|
3548
|
+
: undefined;
|
|
3473
3549
|
let costUsd;
|
|
3474
|
-
|
|
3475
|
-
|
|
3550
|
+
let costUsdNoCache;
|
|
3551
|
+
let inputTokens;
|
|
3552
|
+
let cacheReadTokens;
|
|
3553
|
+
if (snap) {
|
|
3554
|
+
inputTokens = typeof snap.input_tokens === 'number' ? snap.input_tokens : undefined;
|
|
3555
|
+
cacheReadTokens = typeof snap.cached_input_tokens === 'number' ? snap.cached_input_tokens : undefined;
|
|
3556
|
+
}
|
|
3557
|
+
// Price the final cumulative token snapshot once, against the session model.
|
|
3558
|
+
if (state.model && snap) {
|
|
3559
|
+
const usage = {
|
|
3476
3560
|
model: state.model,
|
|
3477
|
-
inputTokens:
|
|
3478
|
-
outputTokens
|
|
3479
|
-
cacheReadTokens:
|
|
3480
|
-
}
|
|
3481
|
-
|
|
3561
|
+
inputTokens: snap.input_tokens,
|
|
3562
|
+
outputTokens,
|
|
3563
|
+
cacheReadTokens: snap.cached_input_tokens,
|
|
3564
|
+
};
|
|
3565
|
+
const c = costOfUsage(usage);
|
|
3566
|
+
if (c > 0) {
|
|
3482
3567
|
costUsd = c;
|
|
3568
|
+
costUsdNoCache = costOfUsageNoCache(usage);
|
|
3569
|
+
}
|
|
3483
3570
|
}
|
|
3484
3571
|
const durationMs = state.firstTsMs !== undefined && state.lastTsMs !== undefined && state.lastTsMs > state.firstTsMs
|
|
3485
3572
|
? state.lastTsMs - state.firstTsMs
|
|
@@ -3496,10 +3583,12 @@ export function finalizeCodexScan(state) {
|
|
|
3496
3583
|
topic: state.topic,
|
|
3497
3584
|
messageCount: state.messageCount,
|
|
3498
3585
|
tokenCount: state.tokenCount,
|
|
3499
|
-
outputTokens
|
|
3500
|
-
|
|
3501
|
-
|
|
3586
|
+
outputTokens,
|
|
3587
|
+
inputTokens,
|
|
3588
|
+
cacheReadTokens,
|
|
3589
|
+
cacheWriteTokens: undefined,
|
|
3502
3590
|
costUsd,
|
|
3591
|
+
costUsdNoCache,
|
|
3503
3592
|
durationMs,
|
|
3504
3593
|
lastActivity: state.lastTsMs !== undefined ? new Date(state.lastTsMs).toISOString() : undefined,
|
|
3505
3594
|
contentText: state.userTexts.length > 0 ? state.userTexts.join('\n') : undefined,
|
|
@@ -4457,10 +4546,13 @@ export function readGrokMeta(filePath, currentVersion) {
|
|
|
4457
4546
|
? summary.num_messages
|
|
4458
4547
|
: undefined;
|
|
4459
4548
|
// Grok records its managed home in summary.grok_home
|
|
4460
|
-
// (…/versions/grok/<version>/home/.grok) — recover the version from it.
|
|
4549
|
+
// (…/versions/grok/<version>/home/.grok) — recover the version from it. The
|
|
4550
|
+
// value is written by the Grok CLI in the writing host's native separators, so
|
|
4551
|
+
// a Windows-authored summary is backslash-separated; normalize to `/` before
|
|
4552
|
+
// matching or the version never resolves on Windows (RUSH-2286).
|
|
4461
4553
|
let embeddedVersion;
|
|
4462
4554
|
if (typeof summary?.grok_home === 'string') {
|
|
4463
|
-
embeddedVersion = summary.grok_home.match(/versions\/grok\/([^/]+)\//)?.[1];
|
|
4555
|
+
embeddedVersion = summary.grok_home.replace(/\\/g, '/').match(/versions\/grok\/([^/]+)\//)?.[1];
|
|
4464
4556
|
}
|
|
4465
4557
|
const meta = {
|
|
4466
4558
|
id: sessionId,
|
|
@@ -21,11 +21,25 @@
|
|
|
21
21
|
* the raw file count rather than quietly differing.
|
|
22
22
|
*/
|
|
23
23
|
import type { SessionEvent } from './types.js';
|
|
24
|
+
/**
|
|
25
|
+
* Minimum quiet time after the assistant's last event before a user message
|
|
26
|
+
* is classified as an **agent silent stall**: the model went quiet on its own
|
|
27
|
+
* and sat idle until the human resumed. Shorter gaps are normal turn-taking.
|
|
28
|
+
* 5 minutes is long enough to exclude "user typing the next instruction" and
|
|
29
|
+
* short enough to catch "went silent mid-task until I said continue."
|
|
30
|
+
*/
|
|
31
|
+
export declare const SILENT_STALL_SECONDS = 300;
|
|
24
32
|
/** Behavioural facets of one session. Serialized as JSON into `session_insights`. */
|
|
25
33
|
export interface InsightFacets {
|
|
26
34
|
toolCounts: Record<string, number>;
|
|
27
35
|
/** Per-model assistant turn counts. `/insights` has no model dimension at all. */
|
|
28
36
|
models: Record<string, number>;
|
|
37
|
+
/**
|
|
38
|
+
* Silent stalls attributed to the model that last spoke before the idle gap
|
|
39
|
+
* (from the nearest preceding `usage`/message model tag). Some models go
|
|
40
|
+
* quiet more often — this is the laziness split. Key = shortened model id.
|
|
41
|
+
*/
|
|
42
|
+
silentStallsByModel: Record<string, number>;
|
|
29
43
|
languages: Record<string, number>;
|
|
30
44
|
/** Slash commands the user invoked, by name. */
|
|
31
45
|
slashCommands: Record<string, number>;
|
|
@@ -84,6 +98,11 @@ export interface InsightFacets {
|
|
|
84
98
|
* can be re-bucketed for a different display timezone without re-parsing.
|
|
85
99
|
*/
|
|
86
100
|
export declare function computeInsightFacets(events: SessionEvent[], timezoneOffsetMinutes?: number): InsightFacets;
|
|
101
|
+
/**
|
|
102
|
+
* Classify a long quiet gap after the assistant's last event as an agent silent stall.
|
|
103
|
+
* Optionally also marks an explicit resume nudge ("continue", …) after that silence.
|
|
104
|
+
*/
|
|
105
|
+
export declare function classifySilentStall(gapSeconds: number, userText: string, friction: Record<string, number>, corrections: Record<string, number>): void;
|
|
87
106
|
/** Percentile of a numeric sample, nearest-rank. Returns 0 for an empty sample. */
|
|
88
107
|
export declare function percentile(values: number[], p: number): number;
|
|
89
108
|
/** Bucket response gaps for display. Returns every bucket, including empty ones. */
|
|
@@ -47,18 +47,35 @@ const ERROR_CATEGORIES = [
|
|
|
47
47
|
[['exit code', 'command failed', 'error:'], 'Command Failed'],
|
|
48
48
|
];
|
|
49
49
|
/**
|
|
50
|
-
* Gaps longer than this
|
|
51
|
-
*
|
|
50
|
+
* Gaps longer than this leave the reply-latency percentiles (someone left for
|
|
51
|
+
* lunch / overnight). They still count as silent stalls when the assistant
|
|
52
|
+
* was the last speaker — the agent had already stopped before the user left.
|
|
52
53
|
*/
|
|
53
54
|
const GAP_CEILING_SECONDS = 3600;
|
|
55
|
+
/**
|
|
56
|
+
* Minimum quiet time after the assistant's last event before a user message
|
|
57
|
+
* is classified as an **agent silent stall**: the model went quiet on its own
|
|
58
|
+
* and sat idle until the human resumed. Shorter gaps are normal turn-taking.
|
|
59
|
+
* 5 minutes is long enough to exclude "user typing the next instruction" and
|
|
60
|
+
* short enough to catch "went silent mid-task until I said continue."
|
|
61
|
+
*/
|
|
62
|
+
export const SILENT_STALL_SECONDS = 300;
|
|
54
63
|
/** Response-gap buckets, in ascending order. Upper bound is exclusive. */
|
|
55
64
|
const GAP_BUCKETS = [
|
|
56
65
|
['<10s', 10], ['10-30s', 30], ['30s-1m', 60], ['1-2m', 120],
|
|
57
66
|
['2-5m', 300], ['5-15m', 900], ['15-60m', Infinity],
|
|
58
67
|
];
|
|
68
|
+
/** Silent-stall duration buckets (agent idle after its last event). */
|
|
69
|
+
const SILENT_STALL_BUCKETS = [
|
|
70
|
+
// label, min inclusive, max exclusive (Infinity = open)
|
|
71
|
+
['silent stall: 5-15m', 300, 900],
|
|
72
|
+
['silent stall: 15-60m', 900, 3600],
|
|
73
|
+
['silent stall: 1h+', 3600, Infinity],
|
|
74
|
+
];
|
|
59
75
|
function emptyFacets() {
|
|
60
76
|
return {
|
|
61
|
-
toolCounts: {}, models: {},
|
|
77
|
+
toolCounts: {}, models: {}, silentStallsByModel: {}, languages: {},
|
|
78
|
+
slashCommands: {}, errorCategories: {},
|
|
62
79
|
interruptions: 0, responseGaps: [], gapsOverCeiling: 0,
|
|
63
80
|
linesTouchedBefore: 0, linesTouchedAfter: 0, editingToolCalls: 0,
|
|
64
81
|
filesCreated: 0, filesModified: 0, filesDeleted: 0, gitCommits: 0, gitPushes: 0,
|
|
@@ -142,10 +159,18 @@ export function computeInsightFacets(events, timezoneOffsetMinutes = new Date().
|
|
|
142
159
|
// reported p50 by 63% (143s against a true 88s), because fast replies are common and
|
|
143
160
|
// dropping them all shifts the median right. A 0-second reply is a real reply.
|
|
144
161
|
//
|
|
145
|
-
// The upper bound stays
|
|
146
|
-
//
|
|
147
|
-
// the
|
|
162
|
+
// The upper bound stays for *percentiles only*: past an hour the user often left the
|
|
163
|
+
// desk, which is not "reply latency." Those gaps still feed silent-stall classification
|
|
164
|
+
// (the agent had already stopped). `gapsOverCeiling` reports how many so percentiles
|
|
165
|
+
// are never quietly truncated.
|
|
166
|
+
//
|
|
167
|
+
// Silent stall (agent idle): when the gap after the assistant's last event is long
|
|
168
|
+
// enough that the model clearly stopped mid-session and waited for a human nudge
|
|
169
|
+
// ("continue", "keep going", or any later message after minutes of silence). This is
|
|
170
|
+
// the inverse framing of reply latency: same timestamps, attributed to the agent.
|
|
148
171
|
let lastAssistantTs = null;
|
|
172
|
+
/** Shortened model id of the last assistant activity (for stall attribution). */
|
|
173
|
+
let lastAssistantModel = null;
|
|
149
174
|
let lastFailedTool = null;
|
|
150
175
|
for (const e of events) {
|
|
151
176
|
const ts = new Date(e.timestamp).getTime();
|
|
@@ -157,8 +182,14 @@ export function computeInsightFacets(events, timezoneOffsetMinutes = new Date().
|
|
|
157
182
|
case 'usage':
|
|
158
183
|
// shortenModel so the label matches `agents sessions <id>` and `insights mix`
|
|
159
184
|
// rather than printing the raw id beside their shortened one.
|
|
160
|
-
if (e.model)
|
|
161
|
-
|
|
185
|
+
if (e.model) {
|
|
186
|
+
const m = shortenModel(e.model);
|
|
187
|
+
bump(f.models, m);
|
|
188
|
+
// Usage rows are the reliable model tag for Claude turns; keep as
|
|
189
|
+
// "last model" even when the timestamp is missing so a following
|
|
190
|
+
// tool_use still attributes a stall correctly.
|
|
191
|
+
lastAssistantModel = m;
|
|
192
|
+
}
|
|
162
193
|
break;
|
|
163
194
|
case 'error':
|
|
164
195
|
bump(f.errorCategories, categorizeError(e.content ?? e.output ?? ''));
|
|
@@ -175,12 +206,18 @@ export function computeInsightFacets(events, timezoneOffsetMinutes = new Date().
|
|
|
175
206
|
if (e.role === 'assistant') {
|
|
176
207
|
if (hasTs)
|
|
177
208
|
lastAssistantTs = ts;
|
|
209
|
+
if (e.model)
|
|
210
|
+
lastAssistantModel = shortenModel(e.model);
|
|
178
211
|
break;
|
|
179
212
|
}
|
|
180
213
|
if (e.role !== 'user')
|
|
181
214
|
break;
|
|
182
|
-
|
|
183
|
-
|
|
215
|
+
// Synthetic user rows (stop-hook feedback, injected meta) are not a human
|
|
216
|
+
// resume — skip correction/stall classification and leave lastAssistantTs so
|
|
217
|
+
// the next real user message still measures the full idle gap.
|
|
218
|
+
if (e._synthetic)
|
|
219
|
+
break;
|
|
220
|
+
classifyCorrection(e.content ?? '', f.correctionSignals);
|
|
184
221
|
if (hasTs) {
|
|
185
222
|
// Local-time hour. parse.ts falls back to `new Date()` for a record with no
|
|
186
223
|
// timestamp; those are indistinguishable here, but they are rare and would
|
|
@@ -196,6 +233,11 @@ export function computeInsightFacets(events, timezoneOffsetMinutes = new Date().
|
|
|
196
233
|
f.responseGaps.push(gap);
|
|
197
234
|
else if (gap >= GAP_CEILING_SECONDS)
|
|
198
235
|
f.gapsOverCeiling++;
|
|
236
|
+
// Agent silent stall: model stopped; session sat idle until this message.
|
|
237
|
+
if (gap >= SILENT_STALL_SECONDS) {
|
|
238
|
+
classifySilentStall(gap, e.content ?? '', f.frictionSignals, f.correctionSignals);
|
|
239
|
+
bump(f.silentStallsByModel, lastAssistantModel ?? 'unknown');
|
|
240
|
+
}
|
|
199
241
|
}
|
|
200
242
|
}
|
|
201
243
|
lastAssistantTs = null;
|
|
@@ -207,6 +249,8 @@ export function computeInsightFacets(events, timezoneOffsetMinutes = new Date().
|
|
|
207
249
|
break;
|
|
208
250
|
if (hasTs)
|
|
209
251
|
lastAssistantTs = ts;
|
|
252
|
+
if (e.model)
|
|
253
|
+
lastAssistantModel = shortenModel(e.model);
|
|
210
254
|
const args = e.args ?? {};
|
|
211
255
|
const toolName = e.tool ?? '';
|
|
212
256
|
if (/askuserquestion/i.test(toolName))
|
|
@@ -257,6 +301,24 @@ const CORRECTION_PATTERNS = [
|
|
|
257
301
|
[/\b(?:check now|check again|try now|did it work)\b/i, 'check now'],
|
|
258
302
|
[/\b(?:don'?t ask|just do it|run what)\b/i, "don't ask / just do it"],
|
|
259
303
|
];
|
|
304
|
+
/** User text that is a pure resume nudge after the agent went silent. */
|
|
305
|
+
const RESUME_NUDGE_PATTERN = /\b(?:continue|keep going|don'?t stop|resume|pick up|you stopped|still there|wake up|hello\??|are you (?:there|stuck)|go on)\b/i;
|
|
306
|
+
/**
|
|
307
|
+
* Classify a long quiet gap after the assistant's last event as an agent silent stall.
|
|
308
|
+
* Optionally also marks an explicit resume nudge ("continue", …) after that silence.
|
|
309
|
+
*/
|
|
310
|
+
export function classifySilentStall(gapSeconds, userText, friction, corrections) {
|
|
311
|
+
for (const [label, min, max] of SILENT_STALL_BUCKETS) {
|
|
312
|
+
if (gapSeconds >= min && gapSeconds < max) {
|
|
313
|
+
bump(friction, label);
|
|
314
|
+
break;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
const normalized = userText.replace(/<[^>]+>/g, ' ').replace(/\s+/g, ' ').trim();
|
|
318
|
+
if (normalized && RESUME_NUDGE_PATTERN.test(normalized)) {
|
|
319
|
+
bump(corrections, 'resume after silent stall');
|
|
320
|
+
}
|
|
321
|
+
}
|
|
260
322
|
const AUTOMATION_PATTERNS = [
|
|
261
323
|
[/\bgh pr (?:checks|view|merge)\b/i, 'PR babysitting'],
|
|
262
324
|
[/\bagents secrets (?:list|exec|unlock|export)\b/i, 'secrets unlock dance'],
|
|
@@ -360,6 +422,8 @@ export function mergeFacets(into, add) {
|
|
|
360
422
|
bump(into.toolCounts, k, v);
|
|
361
423
|
for (const [k, v] of Object.entries(add.models))
|
|
362
424
|
bump(into.models, k, v);
|
|
425
|
+
for (const [k, v] of Object.entries(add.silentStallsByModel ?? {}))
|
|
426
|
+
bump(into.silentStallsByModel, k, v);
|
|
363
427
|
for (const [k, v] of Object.entries(add.languages))
|
|
364
428
|
bump(into.languages, k, v);
|
|
365
429
|
for (const [k, v] of Object.entries(add.slashCommands))
|
|
@@ -406,6 +470,7 @@ export function topEntries(counts, limit) {
|
|
|
406
470
|
export function buildInsightActions(sessions) {
|
|
407
471
|
const specs = [
|
|
408
472
|
{ source: 'correctionSignals', label: 'continue / keep going', category: 'rule', action: 'Keep working through the delivery chain without waiting for another “continue”.' },
|
|
473
|
+
{ source: 'correctionSignals', label: 'resume after silent stall', category: 'rule', action: 'Never stop mid-task waiting for a human ping — finish the current goal or park with an explicit blocker; keep driving CI/review/merge without going idle.' },
|
|
409
474
|
{ source: 'correctionSignals', label: 'approval repeated', category: 'rule', action: 'Treat the original build or ship request as authorization for routine follow-through.' },
|
|
410
475
|
{ source: 'correctionSignals', label: 'done end-to-end?', category: 'rule', action: 'Verify the user-visible outcome before declaring the task complete.' },
|
|
411
476
|
{ source: 'correctionSignals', label: 'did you merge?', category: 'automation', action: 'Automate PR review, CI watching, and merge-on-green as one durable workflow.' },
|
|
@@ -416,6 +481,9 @@ export function buildInsightActions(sessions) {
|
|
|
416
481
|
{ source: 'correctionSignals', label: "Ask stall: what's next?", category: 'rule', action: 'Remove workflow-stall “what next?” prompts from agent guidance.' },
|
|
417
482
|
{ source: 'correctionSignals', label: 'Ask stall: merge / reconcile', category: 'skill', action: 'Encode the safe merge and reconcile path in the git workflow skill.' },
|
|
418
483
|
{ source: 'correctionSignals', label: 'Ask stall: direction / approach', category: 'rule', action: 'Let agents choose implementation details after scope is clear.' },
|
|
484
|
+
{ source: 'frictionSignals', label: 'silent stall: 5-15m', category: 'rule', action: 'Stop ending turns while work remains open — after a tool batch, take the next step or schedule a real background wait that re-invokes you; do not sit idle until the user says continue.' },
|
|
485
|
+
{ source: 'frictionSignals', label: 'silent stall: 15-60m', category: 'rule', action: 'Long idle after the assistant last spoke is an agent stop, not a user pause — drive open PRs/CI/todos to completion or name a true external blocker instead of going silent.' },
|
|
486
|
+
{ source: 'frictionSignals', label: 'silent stall: 1h+', category: 'rule', action: 'Sessions that sit idle for an hour+ after the model stops are stranded work — use stop-gates, background watches, and queue drain so a human is not the only resume signal.' },
|
|
419
487
|
{ source: 'frictionSignals', label: 'blocked guard', category: 'product', action: 'Make guard failures return the safe next command and exact blocked operation.' },
|
|
420
488
|
{ source: 'frictionSignals', label: 'CI red loop', category: 'automation', action: 'Deduplicate CI watchers and turn repeated red checks into one stateful wait.' },
|
|
421
489
|
{ source: 'frictionSignals', label: 'merge conflict', category: 'skill', action: 'Standardize conflict diagnosis and fix-forward reconciliation.' },
|
|
@@ -5,6 +5,7 @@ export type SessionRecoveryTarget = {
|
|
|
5
5
|
mode: 'native';
|
|
6
6
|
agent: AgentId;
|
|
7
7
|
version: string;
|
|
8
|
+
cwd?: string;
|
|
8
9
|
reason: string;
|
|
9
10
|
} | {
|
|
10
11
|
mode: 'continue';
|
|
@@ -12,6 +13,13 @@ export type SessionRecoveryTarget = {
|
|
|
12
13
|
version: string;
|
|
13
14
|
reason: string;
|
|
14
15
|
};
|
|
16
|
+
export type NativeResumeInspection = {
|
|
17
|
+
available: true;
|
|
18
|
+
cwd?: string;
|
|
19
|
+
} | {
|
|
20
|
+
available: false;
|
|
21
|
+
reason: string;
|
|
22
|
+
};
|
|
15
23
|
export declare class SessionRecoveryError extends Error {
|
|
16
24
|
constructor(message: string);
|
|
17
25
|
}
|
|
@@ -21,6 +29,14 @@ export declare function sessionOriginDevice(session: Pick<SessionMeta, 'machine'
|
|
|
21
29
|
export declare function sessionRecoveryPeer(session: Pick<SessionMeta, 'machine'>, selfCheck?: (host: string) => boolean): string | undefined;
|
|
22
30
|
/** Whether an explicit placement names the session's origin device. */
|
|
23
31
|
export declare function sessionRecoveryDestinationMatches(session: Pick<SessionMeta, 'machine'>, requestedHost: string, self?: string): boolean;
|
|
32
|
+
/**
|
|
33
|
+
* Prove that the indexed transcript is reachable from the exact active version
|
|
34
|
+
* home that would receive native resume. Retained trash/backup transcripts are
|
|
35
|
+
* intentionally rejected here: they remain readable by `/continue`, but a new
|
|
36
|
+
* installation with the same version number must not native-resume an empty
|
|
37
|
+
* isolated home.
|
|
38
|
+
*/
|
|
39
|
+
export declare function inspectNativeResumeSession(session: SessionMeta, versionHome: string): NativeResumeInspection;
|
|
24
40
|
/**
|
|
25
41
|
* Decide how a durable session resumes on the device that owns it.
|
|
26
42
|
*
|
|
@@ -29,7 +45,7 @@ export declare function sessionRecoveryDestinationMatches(session: Pick<SessionM
|
|
|
29
45
|
* same harness and uses `/continue`, whose indexed transcript reader can reach
|
|
30
46
|
* retained version trash. No healthy same-harness account is a loud failure.
|
|
31
47
|
*/
|
|
32
|
-
export declare function resolveSessionRecoveryFromCandidates(session: SessionMeta, candidates: RotateCandidate[], supportsNative?: (agent: AgentId, version?: string) => boolean): SessionRecoveryTarget;
|
|
48
|
+
export declare function resolveSessionRecoveryFromCandidates(session: SessionMeta, candidates: RotateCandidate[], supportsNative?: (agent: AgentId, version?: string) => boolean, nativeInspection?: NativeResumeInspection): SessionRecoveryTarget;
|
|
33
49
|
export declare function resolveSessionRecovery(session: SessionMeta): Promise<SessionRecoveryTarget>;
|
|
34
50
|
/** Stable self-command used by focus, resume, and attach. The owning device runs
|
|
35
51
|
* the recovery resolver above; callers must not native-resume another version's
|