taskplane 0.22.12 → 0.22.14
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/bin/rpc-wrapper.mjs +43 -3
- package/dashboard/public/app.js +108 -55
- package/dashboard/public/style.css +18 -0
- package/dashboard/server.cjs +29 -3
- package/extensions/task-runner.ts +474 -89
- package/extensions/taskplane/abort.ts +2 -5
- package/extensions/taskplane/cleanup.ts +30 -9
- package/extensions/taskplane/engine.ts +11 -12
- package/extensions/taskplane/execution.ts +20 -3
- package/extensions/taskplane/extension.ts +499 -8
- package/extensions/taskplane/merge.ts +90 -8
- package/extensions/taskplane/supervisor-primer.md +6 -0
- package/package.json +1 -1
- package/templates/agents/task-worker.md +15 -3
package/bin/rpc-wrapper.mjs
CHANGED
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
*/
|
|
29
29
|
|
|
30
30
|
import { spawn } from "node:child_process";
|
|
31
|
-
import { readFileSync, writeFileSync, appendFileSync, mkdirSync, readdirSync, renameSync } from "node:fs";
|
|
31
|
+
import { readFileSync, writeFileSync, appendFileSync, mkdirSync, readdirSync, renameSync, unlinkSync } from "node:fs";
|
|
32
32
|
import { dirname, resolve, join, basename } from "node:path";
|
|
33
33
|
import { StringDecoder } from "node:string_decoder";
|
|
34
34
|
|
|
@@ -46,6 +46,7 @@ function parseArgs(argv) {
|
|
|
46
46
|
passthrough: [],
|
|
47
47
|
help: false,
|
|
48
48
|
mailboxDir: null,
|
|
49
|
+
steeringPendingPath: null,
|
|
49
50
|
};
|
|
50
51
|
|
|
51
52
|
let i = 2; // skip "node" and script path
|
|
@@ -78,6 +79,9 @@ function parseArgs(argv) {
|
|
|
78
79
|
} else if (arg === "--mailbox-dir" && i + 1 < argv.length) {
|
|
79
80
|
args.mailboxDir = argv[++i];
|
|
80
81
|
i++;
|
|
82
|
+
} else if (arg === "--steering-pending-path" && i + 1 < argv.length) {
|
|
83
|
+
args.steeringPendingPath = argv[++i];
|
|
84
|
+
i++;
|
|
81
85
|
} else if (arg === "--") {
|
|
82
86
|
args.passthrough = argv.slice(i + 1);
|
|
83
87
|
break;
|
|
@@ -108,6 +112,7 @@ Optional:
|
|
|
108
112
|
--tools <t1,t2,...> Comma-separated tool names
|
|
109
113
|
--extensions <e1,e2,...> Comma-separated extension paths
|
|
110
114
|
--mailbox-dir <path> Mailbox directory for agent steering (TP-089)
|
|
115
|
+
--steering-pending-path <p> Path to .steering-pending JSONL flag file (TP-090)
|
|
111
116
|
-h, --help Show this help
|
|
112
117
|
`
|
|
113
118
|
);
|
|
@@ -509,9 +514,10 @@ const MAILBOX_MESSAGE_TYPES = new Set(["steer", "query", "abort", "info", "reply
|
|
|
509
514
|
*
|
|
510
515
|
* @param {string} mailboxDir - Session mailbox directory (e.g., .pi/mailbox/{batchId}/{session})
|
|
511
516
|
* @param {object} proc - The spawned pi process (must have writable stdin)
|
|
517
|
+
* @param {string|null} steeringPendingPath - Path to .steering-pending JSONL flag file (TP-090, worker-only)
|
|
512
518
|
* @returns {{ delivered: number, skipped: number }} Delivery stats
|
|
513
519
|
*/
|
|
514
|
-
function checkMailboxAndSteer(mailboxDir, proc) {
|
|
520
|
+
function checkMailboxAndSteer(mailboxDir, proc, steeringPendingPath) {
|
|
515
521
|
const stats = { delivered: 0, skipped: 0 };
|
|
516
522
|
|
|
517
523
|
// Derive expected values from path structure:
|
|
@@ -616,6 +622,17 @@ function checkMailboxAndSteer(mailboxDir, proc) {
|
|
|
616
622
|
|
|
617
623
|
stats.delivered++;
|
|
618
624
|
process.stderr.write(`\n[STEERING] Delivered message ${message.id}\n`);
|
|
625
|
+
|
|
626
|
+
// TP-090: Append to .steering-pending JSONL flag for task-runner STATUS.md annotation.
|
|
627
|
+
// Worker-only: steeringPendingPath is only set for worker sessions.
|
|
628
|
+
if (steeringPendingPath) {
|
|
629
|
+
try {
|
|
630
|
+
const entry = JSON.stringify({ ts: message.timestamp, content: message.content, id: message.id }) + "\n";
|
|
631
|
+
appendFileSync(steeringPendingPath, entry, "utf-8");
|
|
632
|
+
} catch (err) {
|
|
633
|
+
process.stderr.write(`\n[STEERING] WARNING: failed to write .steering-pending: ${err.message}\n`);
|
|
634
|
+
}
|
|
635
|
+
}
|
|
619
636
|
} catch (err) {
|
|
620
637
|
process.stderr.write(`\n[STEERING] WARNING: failed to deliver ${filename}: ${err.message}\n`);
|
|
621
638
|
stats.skipped++;
|
|
@@ -759,6 +776,29 @@ const proc = spawn("pi", piArgs, {
|
|
|
759
776
|
shell: true, // Required for Windows: resolves pi.cmd shim. Matches task-runner.ts pattern.
|
|
760
777
|
});
|
|
761
778
|
|
|
779
|
+
// ── TP-097: Write PID file for orphan cleanup ──────────────────
|
|
780
|
+
// Write both the wrapper PID and the pi child PID alongside the sidecar file.
|
|
781
|
+
// The task-runner reads this on session end to kill orphan processes.
|
|
782
|
+
// Format: JSON with wrapperPid and childPid fields.
|
|
783
|
+
const pidFilePath = args.sidecarPath + ".pid";
|
|
784
|
+
try {
|
|
785
|
+
const pidData = {
|
|
786
|
+
wrapperPid: process.pid,
|
|
787
|
+
childPid: proc.pid ?? null,
|
|
788
|
+
startedAt: Date.now(),
|
|
789
|
+
};
|
|
790
|
+
writeFileSync(pidFilePath, JSON.stringify(pidData) + "\n", "utf-8");
|
|
791
|
+
process.stderr.write(`[rpc-wrapper] PID file written: ${pidFilePath} (wrapper=${process.pid}, child=${proc.pid})\n`);
|
|
792
|
+
} catch (err) {
|
|
793
|
+
process.stderr.write(`[rpc-wrapper] WARNING: failed to write PID file: ${err.message}\n`);
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
// Clean up PID file on process exit (best-effort)
|
|
797
|
+
function cleanupPidFile() {
|
|
798
|
+
try { unlinkSync(pidFilePath); } catch { /* ignore */ }
|
|
799
|
+
}
|
|
800
|
+
process.on("exit", cleanupPidFile);
|
|
801
|
+
|
|
762
802
|
// ── Send prompt via JSONL stdin ──────────────────────────────────────
|
|
763
803
|
|
|
764
804
|
const promptCmd = { type: "prompt", message: promptContent };
|
|
@@ -852,7 +892,7 @@ function handleEvent(event) {
|
|
|
852
892
|
// Only active when --mailbox-dir is provided (backward compatible).
|
|
853
893
|
if (args.mailboxDir) {
|
|
854
894
|
try {
|
|
855
|
-
checkMailboxAndSteer(args.mailboxDir, proc);
|
|
895
|
+
checkMailboxAndSteer(args.mailboxDir, proc, args.steeringPendingPath || null);
|
|
856
896
|
} catch (err) {
|
|
857
897
|
// Never crash on mailbox I/O errors
|
|
858
898
|
process.stderr.write(`\n[STEERING] ERROR: ${err.message}\n`);
|
package/dashboard/public/app.js
CHANGED
|
@@ -621,6 +621,58 @@ function renderLanesTasks(batch, tmuxSessions) {
|
|
|
621
621
|
|
|
622
622
|
// ─── Render: Merge Agents ───────────────────────────────────────────────────
|
|
623
623
|
|
|
624
|
+
/** Build full telemetry HTML for a merge agent (parity with worker stats).
|
|
625
|
+
* Shows: elapsed, tool count, context %, cost, current tool, retry/compaction badges.
|
|
626
|
+
* Returns empty string if no meaningful telemetry exists.
|
|
627
|
+
*/
|
|
628
|
+
function mergeTelemetryHtml(tel, alive) {
|
|
629
|
+
if (!tel) return '<span class="merge-no-data">—</span>';
|
|
630
|
+
const hasData = (tel.inputTokens || 0) > 0 || (tel.outputTokens || 0) > 0 ||
|
|
631
|
+
(tel.toolCalls || 0) > 0 || (tel.cost || 0) > 0;
|
|
632
|
+
if (!hasData) return '<span class="merge-no-data">—</span>';
|
|
633
|
+
|
|
634
|
+
let html = '<div class="merge-stats">';
|
|
635
|
+
|
|
636
|
+
// Elapsed time
|
|
637
|
+
if (tel.startedAt) {
|
|
638
|
+
const elapsed = Date.now() - tel.startedAt;
|
|
639
|
+
html += `<span class="worker-stat" title="Merge elapsed">⏱ ${formatDuration(elapsed)}</span>`;
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
// Tool calls
|
|
643
|
+
if (tel.toolCalls > 0) {
|
|
644
|
+
html += `<span class="worker-stat" title="Tool calls">🔧 ${tel.toolCalls}</span>`;
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
// Context %
|
|
648
|
+
if (tel.contextPct > 0) {
|
|
649
|
+
html += `<span class="worker-stat" title="Context window used">📊 ${Math.round(tel.contextPct)}%</span>`;
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
// Tokens + cost
|
|
653
|
+
const inp = (tel.inputTokens || 0) + (tel.cacheReadTokens || 0);
|
|
654
|
+
const out = tel.outputTokens || 0;
|
|
655
|
+
const cost = tel.cost || 0;
|
|
656
|
+
if (inp > 0 || out > 0) {
|
|
657
|
+
let tokenStr = `↑${formatTokens(inp)} ↓${formatTokens(out)}`;
|
|
658
|
+
if (cost > 0) tokenStr += ` ${formatCost(cost)}`;
|
|
659
|
+
html += `<span class="worker-stat" title="Tokens">🪙 ${tokenStr}</span>`;
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
// Current tool (if alive/active) or last tool (completed merges)
|
|
663
|
+
if (alive && tel.currentTool) {
|
|
664
|
+
html += `<span class="worker-stat worker-last-tool" title="Current tool">${escapeHtml(tel.currentTool)}</span>`;
|
|
665
|
+
} else if (!alive && tel.lastTool) {
|
|
666
|
+
html += `<span class="worker-stat worker-last-tool" title="Last tool">${escapeHtml(tel.lastTool)}</span>`;
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
// Retry/compaction badges (reuse shared helper)
|
|
670
|
+
html += telemetryBadgesHtml(tel);
|
|
671
|
+
|
|
672
|
+
html += '</div>';
|
|
673
|
+
return html;
|
|
674
|
+
}
|
|
675
|
+
|
|
624
676
|
function renderMergeAgents(batch, tmuxSessions) {
|
|
625
677
|
const mergeResults = batch?.mergeResults || [];
|
|
626
678
|
const tmuxSet = new Set(tmuxSessions || []);
|
|
@@ -642,8 +694,8 @@ function renderMergeAgents(batch, tmuxSessions) {
|
|
|
642
694
|
mergePrefix = laneMatch[1] + "-merge";
|
|
643
695
|
}
|
|
644
696
|
}
|
|
645
|
-
// Helper: get merge session name for a
|
|
646
|
-
const getMergeSessionName = (
|
|
697
|
+
// Helper: get merge session name for a merge number
|
|
698
|
+
const getMergeSessionName = (mergeNum) => `${mergePrefix}-${mergeNum}`;
|
|
647
699
|
|
|
648
700
|
if (mergeResults.length === 0 && mergeSessions.length === 0) {
|
|
649
701
|
$mergeBody.innerHTML = '<div class="empty-state">No merge agents active</div>';
|
|
@@ -671,49 +723,64 @@ function renderMergeAgents(batch, tmuxSessions) {
|
|
|
671
723
|
: mr.status === "partial" ? "status-stalled"
|
|
672
724
|
: "status-failed";
|
|
673
725
|
|
|
674
|
-
//
|
|
675
|
-
// Merge sessions
|
|
676
|
-
//
|
|
677
|
-
const
|
|
678
|
-
const
|
|
679
|
-
const
|
|
680
|
-
|
|
726
|
+
// Merge session mapping: derive from lane numbers involved in this wave.
|
|
727
|
+
// Merge sessions are named by lane number (e.g., ...-merge-1), not wave index.
|
|
728
|
+
// Extract lane numbers from repoResults or from batch tasks for this wave.
|
|
729
|
+
const waveLaneNums = new Set();
|
|
730
|
+
const repoResults2 = mr.repoResults || [];
|
|
731
|
+
for (const rr of repoResults2) {
|
|
732
|
+
for (const ln of (rr.laneNumbers || [])) waveLaneNums.add(ln);
|
|
733
|
+
}
|
|
734
|
+
// Fallback: find lane numbers from tasks assigned to this wave
|
|
735
|
+
if (waveLaneNums.size === 0 && batch.wavePlan && batch.wavePlan[mr.waveIndex]) {
|
|
736
|
+
const waveTaskIds = new Set(batch.wavePlan[mr.waveIndex]);
|
|
737
|
+
for (const t of (batch.tasks || [])) {
|
|
738
|
+
if (waveTaskIds.has(t.taskId) && t.laneNumber != null) {
|
|
739
|
+
waveLaneNums.add(t.laneNumber);
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
// Find alive merge sessions matching the wave's lane numbers
|
|
744
|
+
let effectiveSession = null;
|
|
745
|
+
for (const ln of waveLaneNums) {
|
|
746
|
+
const candidate = getMergeSessionName(ln);
|
|
747
|
+
if (tmuxSet.has(candidate) && !shownSessions.has(candidate)) {
|
|
748
|
+
effectiveSession = candidate;
|
|
749
|
+
break;
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
// Fallback: any unshown alive merge session
|
|
753
|
+
if (!effectiveSession) {
|
|
754
|
+
effectiveSession = mergeSessions.find(s => tmuxSet.has(s) && !shownSessions.has(s)) || null;
|
|
755
|
+
}
|
|
756
|
+
const effectiveAlive = !!effectiveSession;
|
|
757
|
+
if (effectiveSession) shownSessions.add(effectiveSession);
|
|
681
758
|
|
|
682
|
-
//
|
|
759
|
+
// Find merge telemetry: try sessions by lane number first
|
|
683
760
|
let mergeTel = null;
|
|
684
|
-
for (const
|
|
685
|
-
|
|
761
|
+
for (const ln of waveLaneNums) {
|
|
762
|
+
const candidate = getMergeSessionName(ln);
|
|
763
|
+
if (telemetry[candidate]) { mergeTel = telemetry[candidate]; break; }
|
|
686
764
|
}
|
|
765
|
+
// Fallback: effective session telemetry or any merge session
|
|
766
|
+
if (!mergeTel && effectiveSession) mergeTel = telemetry[effectiveSession] || null;
|
|
767
|
+
if (!mergeTel) mergeTel = mergeSessions.reduce((found, ms) => found || telemetry[ms] || null, null);
|
|
687
768
|
|
|
688
769
|
html += `<tr>`;
|
|
689
|
-
html += `<td
|
|
770
|
+
html += `<td class="merge-wave-cell">Wave ${mr.waveIndex + 1}</td>`;
|
|
690
771
|
html += `<td><span class="status-badge ${statusCls}">${mr.status}</span></td>`;
|
|
691
|
-
html += `<td
|
|
692
|
-
//
|
|
693
|
-
html += `<td
|
|
694
|
-
if (mergeTel) {
|
|
695
|
-
const totalTok = (mergeTel.inputTokens || 0) + (mergeTel.outputTokens || 0);
|
|
696
|
-
const cost = mergeTel.cost || 0;
|
|
697
|
-
if (totalTok > 0 || cost > 0) {
|
|
698
|
-
html += `<span style="color:var(--text-muted);">${totalTok > 0 ? totalTok.toLocaleString() + " tok" : ""}`;
|
|
699
|
-
if (cost > 0) html += ` · $${cost.toFixed(4)}`;
|
|
700
|
-
html += `</span>`;
|
|
701
|
-
} else {
|
|
702
|
-
html += '<span style="color:var(--text-faint);">—</span>';
|
|
703
|
-
}
|
|
704
|
-
} else {
|
|
705
|
-
html += '<span style="color:var(--text-faint);">—</span>';
|
|
706
|
-
}
|
|
707
|
-
html += `</td>`;
|
|
772
|
+
html += `<td class="merge-session-cell">${effectiveAlive ? escapeHtml(effectiveSession) : "—"}</td>`;
|
|
773
|
+
// Full telemetry cell
|
|
774
|
+
html += `<td class="merge-telemetry-cell">${mergeTelemetryHtml(mergeTel, effectiveAlive)}</td>`;
|
|
708
775
|
html += `<td>`;
|
|
709
|
-
if (
|
|
710
|
-
const cmd = `tmux attach -t ${
|
|
711
|
-
html += `<span class="tmux-cmd" data-tmux="${escapeHtml(
|
|
776
|
+
if (effectiveAlive) {
|
|
777
|
+
const cmd = `tmux attach -t ${effectiveSession}`;
|
|
778
|
+
html += `<span class="tmux-cmd" data-tmux="${escapeHtml(effectiveSession)}" onclick="copyTmuxCmd('${escapeHtml(effectiveSession)}')" title="Click to copy">${escapeHtml(cmd)}</span>`;
|
|
712
779
|
} else {
|
|
713
|
-
html += '<span
|
|
780
|
+
html += '<span class="merge-no-data">—</span>';
|
|
714
781
|
}
|
|
715
782
|
html += `</td>`;
|
|
716
|
-
html += `<td
|
|
783
|
+
html += `<td class="merge-detail-cell">${mr.failureReason ? escapeHtml(mr.failureReason) : "—"}</td>`;
|
|
717
784
|
html += `</tr>`;
|
|
718
785
|
|
|
719
786
|
// Per-repo sub-rows: show when workspace mode has repo results
|
|
@@ -732,10 +799,10 @@ function renderMergeAgents(batch, tmuxSessions) {
|
|
|
732
799
|
html += `<tr class="merge-repo-row">`;
|
|
733
800
|
html += `<td>${repoBadgeHtml(rr.repoId)}</td>`;
|
|
734
801
|
html += `<td><span class="status-badge ${rrStatusCls}">${rr.status}</span></td>`;
|
|
735
|
-
html += `<td
|
|
802
|
+
html += `<td class="merge-session-cell">${rrLanes}</td>`;
|
|
736
803
|
html += `<td></td>`; /* telemetry placeholder */
|
|
737
804
|
html += `<td></td>`; /* attach placeholder */
|
|
738
|
-
html += `<td
|
|
805
|
+
html += `<td class="merge-detail-cell">${rrDetail}</td>`;
|
|
739
806
|
html += `</tr>`;
|
|
740
807
|
}
|
|
741
808
|
}
|
|
@@ -748,25 +815,11 @@ function renderMergeAgents(batch, tmuxSessions) {
|
|
|
748
815
|
const sessTel = telemetry[sess] || null;
|
|
749
816
|
const cmd = `tmux attach -t ${sess}`;
|
|
750
817
|
html += `<tr>`;
|
|
751
|
-
html += `<td
|
|
818
|
+
html += `<td class="merge-wave-cell">—</td>`;
|
|
752
819
|
html += `<td><span class="status-badge status-running"><span class="status-dot running"></span> merging</span></td>`;
|
|
753
|
-
html += `<td
|
|
754
|
-
//
|
|
755
|
-
html += `<td
|
|
756
|
-
if (sessTel) {
|
|
757
|
-
const totalTok = (sessTel.inputTokens || 0) + (sessTel.outputTokens || 0);
|
|
758
|
-
const cost = sessTel.cost || 0;
|
|
759
|
-
if (totalTok > 0 || cost > 0) {
|
|
760
|
-
html += `<span style="color:var(--text-muted);">${totalTok > 0 ? totalTok.toLocaleString() + " tok" : ""}`;
|
|
761
|
-
if (cost > 0) html += ` · $${cost.toFixed(4)}`;
|
|
762
|
-
html += `</span>`;
|
|
763
|
-
} else {
|
|
764
|
-
html += '<span style="color:var(--text-faint);">—</span>';
|
|
765
|
-
}
|
|
766
|
-
} else {
|
|
767
|
-
html += '<span style="color:var(--text-faint);">—</span>';
|
|
768
|
-
}
|
|
769
|
-
html += `</td>`;
|
|
820
|
+
html += `<td class="merge-session-cell">${escapeHtml(sess)}</td>`;
|
|
821
|
+
// Full telemetry cell for active merge session
|
|
822
|
+
html += `<td class="merge-telemetry-cell">${mergeTelemetryHtml(sessTel, true)}</td>`;
|
|
770
823
|
html += `<td><span class="tmux-cmd" data-tmux="${escapeHtml(sess)}" onclick="copyTmuxCmd('${escapeHtml(sess)}')" title="Click to copy">${escapeHtml(cmd)}</span></td>`;
|
|
771
824
|
html += `<td>—</td>`;
|
|
772
825
|
html += `</tr>`;
|
|
@@ -712,6 +712,24 @@ body {
|
|
|
712
712
|
.merge-table tr:last-child td { border-bottom: none; }
|
|
713
713
|
.merge-table tr:hover td { background: var(--bg-surface-hover); }
|
|
714
714
|
|
|
715
|
+
.merge-wave-cell { font-family: var(--font-mono); }
|
|
716
|
+
.merge-session-cell { font-family: var(--font-mono); font-size: 0.8rem; }
|
|
717
|
+
.merge-detail-cell { font-size: 0.8rem; color: var(--text-muted); }
|
|
718
|
+
.merge-no-data { color: var(--text-faint); }
|
|
719
|
+
|
|
720
|
+
.merge-stats {
|
|
721
|
+
display: flex;
|
|
722
|
+
flex-wrap: wrap;
|
|
723
|
+
gap: 4px 8px;
|
|
724
|
+
align-items: center;
|
|
725
|
+
font-size: 0.75rem;
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
.merge-telemetry-cell {
|
|
729
|
+
font-size: 0.75rem;
|
|
730
|
+
min-width: 120px;
|
|
731
|
+
}
|
|
732
|
+
|
|
715
733
|
/* ─── Terminal Panel ────────────────────────────────────────────────────── */
|
|
716
734
|
|
|
717
735
|
.terminal-panel .panel-header {
|
package/dashboard/server.cjs
CHANGED
|
@@ -427,8 +427,9 @@ function loadTelemetryData(batchState) {
|
|
|
427
427
|
const fresh = {
|
|
428
428
|
inputTokens: 0, outputTokens: 0, cacheReadTokens: 0,
|
|
429
429
|
cacheWriteTokens: 0, cost: 0, toolCalls: 0,
|
|
430
|
-
lastTool: "", retries: 0, retryActive: false,
|
|
430
|
+
lastTool: "", currentTool: "", retries: 0, retryActive: false,
|
|
431
431
|
lastRetryError: "", compactions: 0, latestTotalTokens: 0,
|
|
432
|
+
contextPct: 0, startedAt: 0,
|
|
432
433
|
};
|
|
433
434
|
telemetryAccumulators.set(prefix, fresh);
|
|
434
435
|
// Also reset tail states for ALL files of this prefix to re-read from beginning
|
|
@@ -450,8 +451,9 @@ function loadTelemetryData(batchState) {
|
|
|
450
451
|
if (ts && ts.wasReset) {
|
|
451
452
|
acc.inputTokens = 0; acc.outputTokens = 0; acc.cacheReadTokens = 0;
|
|
452
453
|
acc.cacheWriteTokens = 0; acc.cost = 0; acc.toolCalls = 0;
|
|
453
|
-
acc.lastTool = ""; acc.retries = 0; acc.retryActive = false;
|
|
454
|
+
acc.lastTool = ""; acc.currentTool = ""; acc.retries = 0; acc.retryActive = false;
|
|
454
455
|
acc.lastRetryError = ""; acc.compactions = 0; acc.latestTotalTokens = 0;
|
|
456
|
+
acc.contextPct = 0; acc.startedAt = 0;
|
|
455
457
|
ts.wasReset = false;
|
|
456
458
|
}
|
|
457
459
|
for (const event of events) {
|
|
@@ -497,7 +499,31 @@ function loadTelemetryData(batchState) {
|
|
|
497
499
|
}
|
|
498
500
|
}
|
|
499
501
|
}
|
|
500
|
-
|
|
502
|
+
const toolLabel = argPreview ? `${toolDesc} ${argPreview}` : toolDesc;
|
|
503
|
+
acc.lastTool = toolLabel;
|
|
504
|
+
acc.currentTool = toolLabel;
|
|
505
|
+
break;
|
|
506
|
+
}
|
|
507
|
+
case "tool_execution_end": {
|
|
508
|
+
acc.currentTool = "";
|
|
509
|
+
break;
|
|
510
|
+
}
|
|
511
|
+
case "agent_start": {
|
|
512
|
+
if (event.ts && !acc.startedAt) {
|
|
513
|
+
acc.startedAt = event.ts;
|
|
514
|
+
}
|
|
515
|
+
break;
|
|
516
|
+
}
|
|
517
|
+
case "response": {
|
|
518
|
+
// Extract context usage from get_session_stats responses
|
|
519
|
+
const ctxUsage = event.data?.contextUsage;
|
|
520
|
+
if (ctxUsage) {
|
|
521
|
+
// Support both percent (current) and percentUsed (legacy pi versions)
|
|
522
|
+
const pct = typeof ctxUsage.percent === "number" ? ctxUsage.percent
|
|
523
|
+
: typeof ctxUsage.percentUsed === "number" ? ctxUsage.percentUsed
|
|
524
|
+
: null;
|
|
525
|
+
if (pct !== null) acc.contextPct = pct;
|
|
526
|
+
}
|
|
501
527
|
break;
|
|
502
528
|
}
|
|
503
529
|
case "auto_retry_start": {
|