@ghl-ai/aw 0.1.37-beta.73 → 0.1.37-beta.75
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/commands/telemetry.mjs +345 -13
- package/hooks/capabilities/telemetry.mjs +31 -0
- package/package.json +1 -1
package/commands/telemetry.mjs
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
|
-
// commands/telemetry.mjs — `aw telemetry [push|flush|enable|disable|status]`
|
|
1
|
+
// commands/telemetry.mjs — `aw telemetry [push|flush|flush-queue|scan-prs|enable|disable|status]`
|
|
2
2
|
//
|
|
3
3
|
// Subcommands:
|
|
4
|
-
// push
|
|
5
|
-
// flush
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
|
|
10
|
-
|
|
4
|
+
// push — Automatic telemetry push after every task run (called by collect-telemetry.sh)
|
|
5
|
+
// flush — Flush token data from Claude JSONL sessions
|
|
6
|
+
// flush-queue — Retry queued payloads that failed to push (offline queue)
|
|
7
|
+
// scan-prs — Scan GitHub repos for AI-authored PRs (Co-Authored-By attribution)
|
|
8
|
+
// enable — Re-enable anonymous analytics
|
|
9
|
+
// disable — Opt out of anonymous analytics
|
|
10
|
+
// status — Show telemetry status (default)
|
|
11
|
+
|
|
12
|
+
import { readFileSync, writeFileSync, existsSync, readdirSync, statSync, unlinkSync } from 'node:fs';
|
|
11
13
|
import { execSync } from 'node:child_process';
|
|
12
14
|
import { join } from 'node:path';
|
|
13
15
|
import { homedir } from 'node:os';
|
|
@@ -59,6 +61,10 @@ export async function telemetryCommand(args) {
|
|
|
59
61
|
await pushCommand(args);
|
|
60
62
|
} else if (subcommand === 'flush') {
|
|
61
63
|
await flushCommand(args);
|
|
64
|
+
} else if (subcommand === 'flush-queue') {
|
|
65
|
+
await flushQueueCommand();
|
|
66
|
+
} else if (subcommand === 'scan-prs') {
|
|
67
|
+
await scanPrsCommand(args);
|
|
62
68
|
} else if (subcommand === 'disable') {
|
|
63
69
|
disableTelemetry();
|
|
64
70
|
fmt.logSuccess('Telemetry disabled. No anonymous usage data will be sent.');
|
|
@@ -73,12 +79,14 @@ export async function telemetryCommand(args) {
|
|
|
73
79
|
fmt.logStep(`Machine ID: ${chalk.dim(status.machine_id)}`);
|
|
74
80
|
fmt.logStep(`Config: ${chalk.dim(status.config_path)}`);
|
|
75
81
|
fmt.logMessage('');
|
|
76
|
-
fmt.logMessage(` ${chalk.dim('aw telemetry disable')}
|
|
77
|
-
fmt.logMessage(` ${chalk.dim('aw telemetry enable')}
|
|
78
|
-
fmt.logMessage(` ${chalk.dim('aw telemetry push')}
|
|
79
|
-
fmt.logMessage(` ${chalk.dim('aw telemetry flush')}
|
|
82
|
+
fmt.logMessage(` ${chalk.dim('aw telemetry disable')} — opt out of anonymous analytics`);
|
|
83
|
+
fmt.logMessage(` ${chalk.dim('aw telemetry enable')} — re-enable analytics`);
|
|
84
|
+
fmt.logMessage(` ${chalk.dim('aw telemetry push')} — push telemetry data`);
|
|
85
|
+
fmt.logMessage(` ${chalk.dim('aw telemetry flush')} — flush token data from sessions`);
|
|
86
|
+
fmt.logMessage(` ${chalk.dim('aw telemetry flush-queue')} — retry queued payloads (offline queue)`);
|
|
87
|
+
fmt.logMessage(` ${chalk.dim('aw telemetry scan-prs')} — scan GitHub for AI-authored PRs`);
|
|
80
88
|
} else {
|
|
81
|
-
fmt.cancel(`Unknown telemetry subcommand: ${subcommand}. Use: aw telemetry [push|flush|enable|disable|status]`);
|
|
89
|
+
fmt.cancel(`Unknown telemetry subcommand: ${subcommand}. Use: aw telemetry [push|flush|flush-queue|scan-prs|enable|disable|status]`);
|
|
82
90
|
}
|
|
83
91
|
}
|
|
84
92
|
|
|
@@ -146,6 +154,24 @@ async function pushCommand(args) {
|
|
|
146
154
|
agents_used: record.agents_used ?? [],
|
|
147
155
|
skills_applied: record.skills_applied ?? [],
|
|
148
156
|
github_login: record.github_login ?? resolveGithubLogin() ?? undefined,
|
|
157
|
+
// Phase 2 fields
|
|
158
|
+
cache_creation_tokens: record.cache_creation_tokens ?? null,
|
|
159
|
+
cache_read_tokens: record.cache_read_tokens ?? null,
|
|
160
|
+
project_hash: record.project_hash ?? null,
|
|
161
|
+
platform: record.platform ?? null,
|
|
162
|
+
platform_version: record.platform_version ?? null,
|
|
163
|
+
session_id: record.session_id ?? null,
|
|
164
|
+
session_duration_ms: record.session_duration_ms ?? null,
|
|
165
|
+
compaction_count: record.compaction_count ?? null,
|
|
166
|
+
mcp_tools_used: record.mcp_tools_used ?? [],
|
|
167
|
+
checkpoint_from: record.checkpoint_from ?? null,
|
|
168
|
+
checkpoint_to: record.checkpoint_to ?? null,
|
|
169
|
+
pr_url: record.pr_url ?? null,
|
|
170
|
+
pr_number: record.pr_number ?? null,
|
|
171
|
+
pr_repo: record.pr_repo ?? null,
|
|
172
|
+
pr_files_changed: record.pr_files_changed ?? null,
|
|
173
|
+
error_type: record.error_type ?? null,
|
|
174
|
+
error_message: record.error_message ?? null,
|
|
149
175
|
};
|
|
150
176
|
|
|
151
177
|
if (!dto.shell_run_id || !dto.command) {
|
|
@@ -393,3 +419,309 @@ async function pushDto(dto) {
|
|
|
393
419
|
clearTimeout(timer);
|
|
394
420
|
}
|
|
395
421
|
}
|
|
422
|
+
|
|
423
|
+
// ── telemetry flush-queue ────────────────────────────────────────────────────
|
|
424
|
+
|
|
425
|
+
async function flushQueueCommand() {
|
|
426
|
+
const queuePath = join(homedir(), '.aw', 'telemetry-queue.jsonl');
|
|
427
|
+
|
|
428
|
+
if (!existsSync(queuePath)) {
|
|
429
|
+
fmt.logSuccess('Offline queue is empty — nothing to flush.');
|
|
430
|
+
return;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
let lines;
|
|
434
|
+
try {
|
|
435
|
+
lines = readFileSync(queuePath, 'utf8').trim().split('\n').filter(Boolean);
|
|
436
|
+
} catch {
|
|
437
|
+
fmt.logWarn('flush-queue: could not read queue file');
|
|
438
|
+
return;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
if (lines.length === 0) {
|
|
442
|
+
fmt.logSuccess('Offline queue is empty — nothing to flush.');
|
|
443
|
+
return;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
fmt.logStep(`Found ${lines.length} queued payload(s). Flushing...`);
|
|
447
|
+
|
|
448
|
+
const QUEUE_TTL_MS = 7 * 24 * 60 * 60 * 1000;
|
|
449
|
+
const remaining = [];
|
|
450
|
+
let flushed = 0;
|
|
451
|
+
let expired = 0;
|
|
452
|
+
|
|
453
|
+
for (const line of lines) {
|
|
454
|
+
let entry;
|
|
455
|
+
try { entry = JSON.parse(line); } catch { continue; }
|
|
456
|
+
|
|
457
|
+
// Skip expired (>7 days)
|
|
458
|
+
if (Date.now() - new Date(entry.queued_at).getTime() > QUEUE_TTL_MS) {
|
|
459
|
+
expired++;
|
|
460
|
+
continue;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
const apiKey = process.env['AW_API_KEY'] ?? '';
|
|
464
|
+
const controller = new AbortController();
|
|
465
|
+
const timer = setTimeout(() => controller.abort(), TIMEOUT_MS);
|
|
466
|
+
try {
|
|
467
|
+
const res = await fetch(AW_TELEMETRY_URL, {
|
|
468
|
+
method: 'POST',
|
|
469
|
+
headers: {
|
|
470
|
+
'Content-Type': 'application/json',
|
|
471
|
+
...(apiKey ? { 'X-API-Key': apiKey } : {}),
|
|
472
|
+
},
|
|
473
|
+
body: JSON.stringify(entry.payload),
|
|
474
|
+
signal: controller.signal,
|
|
475
|
+
});
|
|
476
|
+
clearTimeout(timer);
|
|
477
|
+
|
|
478
|
+
if (res.ok) {
|
|
479
|
+
flushed++;
|
|
480
|
+
} else {
|
|
481
|
+
entry.attempts = (entry.attempts || 0) + 1;
|
|
482
|
+
remaining.push(entry);
|
|
483
|
+
}
|
|
484
|
+
} catch {
|
|
485
|
+
clearTimeout(timer);
|
|
486
|
+
entry.attempts = (entry.attempts || 0) + 1;
|
|
487
|
+
remaining.push(entry);
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
// Rewrite queue with remaining entries
|
|
492
|
+
if (remaining.length > 0) {
|
|
493
|
+
writeFileSync(queuePath, remaining.map(e => JSON.stringify(e)).join('\n') + '\n');
|
|
494
|
+
} else {
|
|
495
|
+
try { unlinkSync(queuePath); } catch { /* already gone */ }
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
fmt.logSuccess(`Queue flush complete: ${flushed} pushed, ${expired} expired, ${remaining.length} still queued.`);
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
// ── telemetry scan-prs ───────────────────────────────────────────────────────
|
|
502
|
+
// Scans GitHub repos for commits with Co-Authored-By trailers from AI coding
|
|
503
|
+
// assistants. For each match, finds the containing PR and POSTs attribution
|
|
504
|
+
// back to the telemetry API.
|
|
505
|
+
//
|
|
506
|
+
// Usage:
|
|
507
|
+
// aw telemetry scan-prs # scan repos from config
|
|
508
|
+
// aw telemetry scan-prs --org=myorg # scan all repos in an org
|
|
509
|
+
// aw telemetry scan-prs --repo=owner/repo # scan a specific repo
|
|
510
|
+
// aw telemetry scan-prs --days=7 # how far back to scan (default: 7)
|
|
511
|
+
|
|
512
|
+
const CO_AUTHOR_PATTERNS = [
|
|
513
|
+
'Co-Authored-By: Claude',
|
|
514
|
+
'Co-Authored-By: claude',
|
|
515
|
+
'Co-authored-by: Claude',
|
|
516
|
+
'Co-authored-by: claude',
|
|
517
|
+
// Cursor / Copilot / generic AI
|
|
518
|
+
'Co-Authored-By: GitHub Copilot',
|
|
519
|
+
'Co-authored-by: GitHub Copilot',
|
|
520
|
+
];
|
|
521
|
+
|
|
522
|
+
// GitHub search query qualifier for co-authored commits
|
|
523
|
+
const SEARCH_TRAILER = 'Co-Authored-By Claude';
|
|
524
|
+
|
|
525
|
+
async function scanPrsCommand(args) {
|
|
526
|
+
const ghToken = process.env.GITHUB_TOKEN || resolveGhToken();
|
|
527
|
+
if (!ghToken) {
|
|
528
|
+
fmt.cancel('scan-prs: GITHUB_TOKEN not set. Export it or run `gh auth login`.');
|
|
529
|
+
return;
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
const days = parseInt(args['--days'] || '7', 10);
|
|
533
|
+
const sinceDate = new Date(Date.now() - days * 24 * 60 * 60 * 1000).toISOString().split('T')[0];
|
|
534
|
+
const specificRepo = args['--repo'] || null;
|
|
535
|
+
const org = args['--org'] || null;
|
|
536
|
+
|
|
537
|
+
fmt.intro('aw telemetry scan-prs');
|
|
538
|
+
fmt.logStep(`Scanning for AI-authored commits since ${sinceDate}...`);
|
|
539
|
+
|
|
540
|
+
// Determine repos to scan
|
|
541
|
+
let repos = [];
|
|
542
|
+
if (specificRepo) {
|
|
543
|
+
repos = [specificRepo];
|
|
544
|
+
} else if (org) {
|
|
545
|
+
repos = await listOrgRepos(org, ghToken);
|
|
546
|
+
if (repos.length === 0) {
|
|
547
|
+
fmt.logWarn(`No repos found for org "${org}"`);
|
|
548
|
+
return;
|
|
549
|
+
}
|
|
550
|
+
fmt.logStep(`Found ${repos.length} repos in ${org}`);
|
|
551
|
+
} else {
|
|
552
|
+
// Try to get repos from config
|
|
553
|
+
const configPath = join(homedir(), '.aw', 'config.json');
|
|
554
|
+
if (existsSync(configPath)) {
|
|
555
|
+
try {
|
|
556
|
+
const cfg = JSON.parse(readFileSync(configPath, 'utf8'));
|
|
557
|
+
if (cfg.scan_repos && Array.isArray(cfg.scan_repos)) {
|
|
558
|
+
repos = cfg.scan_repos;
|
|
559
|
+
}
|
|
560
|
+
if (cfg.scan_org) {
|
|
561
|
+
repos = await listOrgRepos(cfg.scan_org, ghToken);
|
|
562
|
+
}
|
|
563
|
+
} catch { /* use empty */ }
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
if (repos.length === 0) {
|
|
567
|
+
// Fallback: try to detect from current git remote
|
|
568
|
+
try {
|
|
569
|
+
const remote = execSync('git remote get-url origin', { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] }).trim();
|
|
570
|
+
const match = remote.match(/github\.com[:/]([^/]+\/[^/.]+)/);
|
|
571
|
+
if (match) repos = [match[1]];
|
|
572
|
+
} catch { /* no git */ }
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
if (repos.length === 0) {
|
|
576
|
+
fmt.cancel('scan-prs: no repos to scan. Use --repo=owner/repo, --org=myorg, or add scan_repos to ~/.aw/config.json');
|
|
577
|
+
return;
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
let totalFound = 0;
|
|
582
|
+
let totalAttributed = 0;
|
|
583
|
+
|
|
584
|
+
for (const repo of repos) {
|
|
585
|
+
const commits = await findAiAuthoredCommits(repo, sinceDate, ghToken);
|
|
586
|
+
if (commits.length === 0) continue;
|
|
587
|
+
|
|
588
|
+
fmt.logStep(`${repo}: found ${commits.length} AI-authored commit(s)`);
|
|
589
|
+
totalFound += commits.length;
|
|
590
|
+
|
|
591
|
+
for (const commit of commits) {
|
|
592
|
+
const pr = await findPrForCommit(repo, commit.sha, ghToken);
|
|
593
|
+
if (!pr) continue;
|
|
594
|
+
|
|
595
|
+
// POST attribution to API
|
|
596
|
+
const dto = {
|
|
597
|
+
shell_run_id: `pr-scan-${commit.sha.slice(0, 12)}`,
|
|
598
|
+
command: 'pr-scan',
|
|
599
|
+
status: 'complete',
|
|
600
|
+
github_login: commit.author,
|
|
601
|
+
pr_url: pr.html_url,
|
|
602
|
+
pr_number: pr.number,
|
|
603
|
+
pr_repo: repo,
|
|
604
|
+
pr_files_changed: pr.changed_files || null,
|
|
605
|
+
branch: pr.head?.ref || null,
|
|
606
|
+
};
|
|
607
|
+
|
|
608
|
+
const controller = new AbortController();
|
|
609
|
+
const timer = setTimeout(() => controller.abort(), TIMEOUT_MS);
|
|
610
|
+
try {
|
|
611
|
+
const apiKey = process.env['AW_API_KEY'] ?? '';
|
|
612
|
+
const res = await fetch(AW_TELEMETRY_URL, {
|
|
613
|
+
method: 'POST',
|
|
614
|
+
headers: {
|
|
615
|
+
'Content-Type': 'application/json',
|
|
616
|
+
...(apiKey ? { 'X-API-Key': apiKey } : {}),
|
|
617
|
+
},
|
|
618
|
+
body: JSON.stringify(dto),
|
|
619
|
+
signal: controller.signal,
|
|
620
|
+
});
|
|
621
|
+
clearTimeout(timer);
|
|
622
|
+
if (res.ok) totalAttributed++;
|
|
623
|
+
} catch {
|
|
624
|
+
clearTimeout(timer);
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
fmt.logSuccess(`Scan complete: ${totalFound} AI commits found, ${totalAttributed} PRs attributed.`);
|
|
630
|
+
|
|
631
|
+
// Save last scan timestamp
|
|
632
|
+
const configPath = join(homedir(), '.aw', 'config.json');
|
|
633
|
+
try {
|
|
634
|
+
let cfg = {};
|
|
635
|
+
if (existsSync(configPath)) {
|
|
636
|
+
try { cfg = JSON.parse(readFileSync(configPath, 'utf8')); } catch { cfg = {}; }
|
|
637
|
+
}
|
|
638
|
+
cfg.last_pr_scan = new Date().toISOString();
|
|
639
|
+
cfg.last_pr_scan_found = totalFound;
|
|
640
|
+
cfg.last_pr_scan_attributed = totalAttributed;
|
|
641
|
+
writeFileSync(configPath, JSON.stringify(cfg, null, 2) + '\n');
|
|
642
|
+
} catch { /* best effort */ }
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
/** Resolve GitHub token from gh CLI auth. */
|
|
646
|
+
function resolveGhToken() {
|
|
647
|
+
try {
|
|
648
|
+
return execSync('gh auth token', { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] }).trim();
|
|
649
|
+
} catch { return null; }
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
/** List repos in a GitHub org (up to 100, public + private). */
|
|
653
|
+
async function listOrgRepos(org, token) {
|
|
654
|
+
try {
|
|
655
|
+
const res = await fetch(`https://api.github.com/orgs/${encodeURIComponent(org)}/repos?per_page=100&sort=pushed&direction=desc`, {
|
|
656
|
+
headers: { Authorization: `Bearer ${token}`, Accept: 'application/vnd.github+json' },
|
|
657
|
+
});
|
|
658
|
+
if (!res.ok) return [];
|
|
659
|
+
const data = await res.json();
|
|
660
|
+
return data.map(r => r.full_name);
|
|
661
|
+
} catch { return []; }
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
/**
|
|
665
|
+
* Find commits with AI Co-Authored-By trailers in a repo.
|
|
666
|
+
* Uses GitHub Search API for commit messages.
|
|
667
|
+
*/
|
|
668
|
+
async function findAiAuthoredCommits(repo, since, token) {
|
|
669
|
+
const commits = [];
|
|
670
|
+
|
|
671
|
+
// GitHub commit search: search for "Co-Authored-By Claude" in the repo
|
|
672
|
+
// The search API is rate-limited (30 req/min for auth users), so we batch.
|
|
673
|
+
const query = encodeURIComponent(`repo:${repo} "${SEARCH_TRAILER}" committer-date:>=${since}`);
|
|
674
|
+
const url = `https://api.github.com/search/commits?q=${query}&per_page=100&sort=committer-date&order=desc`;
|
|
675
|
+
|
|
676
|
+
try {
|
|
677
|
+
const res = await fetch(url, {
|
|
678
|
+
headers: {
|
|
679
|
+
Authorization: `Bearer ${token}`,
|
|
680
|
+
Accept: 'application/vnd.github.cloak-preview+json',
|
|
681
|
+
},
|
|
682
|
+
});
|
|
683
|
+
if (!res.ok) {
|
|
684
|
+
if (res.status === 422) {
|
|
685
|
+
// Validation failed — repo might not exist or search syntax issue
|
|
686
|
+
return commits;
|
|
687
|
+
}
|
|
688
|
+
fmt.logWarn(`scan-prs: GitHub search returned ${res.status} for ${repo}`);
|
|
689
|
+
return commits;
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
const data = await res.json();
|
|
693
|
+
for (const item of (data.items || [])) {
|
|
694
|
+
const message = item.commit?.message || '';
|
|
695
|
+
if (CO_AUTHOR_PATTERNS.some(p => message.includes(p))) {
|
|
696
|
+
commits.push({
|
|
697
|
+
sha: item.sha,
|
|
698
|
+
message: message.slice(0, 256),
|
|
699
|
+
author: item.author?.login || item.commit?.author?.email || 'unknown',
|
|
700
|
+
date: item.commit?.committer?.date || null,
|
|
701
|
+
});
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
} catch (err) {
|
|
705
|
+
fmt.logWarn(`scan-prs: search failed for ${repo} — ${err.message}`);
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
return commits;
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
/**
|
|
712
|
+
* Find the PR that contains a specific commit SHA.
|
|
713
|
+
* Returns the first PR found, or null.
|
|
714
|
+
*/
|
|
715
|
+
async function findPrForCommit(repo, sha, token) {
|
|
716
|
+
try {
|
|
717
|
+
const res = await fetch(`https://api.github.com/repos/${repo}/commits/${sha}/pulls`, {
|
|
718
|
+
headers: {
|
|
719
|
+
Authorization: `Bearer ${token}`,
|
|
720
|
+
Accept: 'application/vnd.github+json',
|
|
721
|
+
},
|
|
722
|
+
});
|
|
723
|
+
if (!res.ok) return null;
|
|
724
|
+
const prs = await res.json();
|
|
725
|
+
return prs.length > 0 ? prs[0] : null;
|
|
726
|
+
} catch { return null; }
|
|
727
|
+
}
|
|
@@ -254,6 +254,8 @@ function extractFromTranscript(transcriptPath) {
|
|
|
254
254
|
let toolTotal = 0;
|
|
255
255
|
let toolFailed = 0;
|
|
256
256
|
const prUrls = new Set();
|
|
257
|
+
let errorType = null;
|
|
258
|
+
let errorMessage = null;
|
|
257
259
|
|
|
258
260
|
for (const line of lines) {
|
|
259
261
|
try {
|
|
@@ -323,6 +325,21 @@ function extractFromTranscript(transcriptPath) {
|
|
|
323
325
|
if (ec && ec !== 0) toolFailed++;
|
|
324
326
|
}
|
|
325
327
|
}
|
|
328
|
+
|
|
329
|
+
// ── Error detection ──────────────────────────────────────────────
|
|
330
|
+
// Capture last error from transcript for structured error reporting.
|
|
331
|
+
const entryError = entry.error || entry.message?.error;
|
|
332
|
+
if (entryError) {
|
|
333
|
+
const errStr = typeof entryError === 'string' ? entryError : (entryError.message || JSON.stringify(entryError));
|
|
334
|
+
errorMessage = errStr.slice(0, 1024);
|
|
335
|
+
// Classify error type
|
|
336
|
+
if (/rate.?limit|429|too many requests/i.test(errStr)) errorType = 'rate_limit';
|
|
337
|
+
else if (/context.?(overflow|length|window|limit|too long)/i.test(errStr)) errorType = 'context_overflow';
|
|
338
|
+
else if (/timeout|timed?\s*out|ETIMEDOUT/i.test(errStr)) errorType = 'timeout';
|
|
339
|
+
else if (/cancel|abort|interrupt/i.test(errStr)) errorType = 'user_cancel';
|
|
340
|
+
else if (/tool.?(fail|error)|execution.?error/i.test(errStr)) errorType = 'tool_failure';
|
|
341
|
+
else errorType = 'unknown';
|
|
342
|
+
}
|
|
326
343
|
} catch { /* skip malformed lines */ }
|
|
327
344
|
}
|
|
328
345
|
|
|
@@ -362,6 +379,8 @@ function extractFromTranscript(transcriptPath) {
|
|
|
362
379
|
toolPassed: toolTotal - toolFailed,
|
|
363
380
|
toolFailed,
|
|
364
381
|
prUrls: [...prUrls],
|
|
382
|
+
errorType,
|
|
383
|
+
errorMessage,
|
|
365
384
|
};
|
|
366
385
|
} catch {
|
|
367
386
|
return null;
|
|
@@ -687,6 +706,12 @@ export async function handleStop(input) {
|
|
|
687
706
|
session.skills_applied = [...existing];
|
|
688
707
|
}
|
|
689
708
|
|
|
709
|
+
// Track last error
|
|
710
|
+
if (transcript?.errorType) {
|
|
711
|
+
session.error_type = transcript.errorType;
|
|
712
|
+
session.error_message = transcript.errorMessage;
|
|
713
|
+
}
|
|
714
|
+
|
|
690
715
|
writeSession(sessionId, session);
|
|
691
716
|
}
|
|
692
717
|
|
|
@@ -773,6 +798,9 @@ export async function handlePreCompact(input) {
|
|
|
773
798
|
pr_url: prUrl,
|
|
774
799
|
pr_number: prNumber,
|
|
775
800
|
pr_repo: prRepo,
|
|
801
|
+
// Error detail
|
|
802
|
+
error_type: session.error_type || null,
|
|
803
|
+
error_message: session.error_message || null,
|
|
776
804
|
};
|
|
777
805
|
|
|
778
806
|
const ok = await pushToApi(dto, session.namespace);
|
|
@@ -865,6 +893,9 @@ export async function handleSessionEnd(input) {
|
|
|
865
893
|
pr_url: prUrl,
|
|
866
894
|
pr_number: prNumber,
|
|
867
895
|
pr_repo: prRepo,
|
|
896
|
+
// Error detail
|
|
897
|
+
error_type: session.error_type || null,
|
|
898
|
+
error_message: session.error_message || null,
|
|
868
899
|
};
|
|
869
900
|
|
|
870
901
|
const ok = await pushToApi(dto, session.namespace);
|