killeros 2.0.21 → 2.1.22

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.
@@ -70,7 +70,7 @@ const BUILTIN_COMMANDS: ReadonlyArray<{ name: string; description: string }> = [
70
70
  ];
71
71
 
72
72
  const COMMAND_SYNTAX_HINTS: Readonly<Record<string, string>> = {
73
- goal: "/goal [objective|clear|edit|pause|resume]",
73
+ goal: "/goal [objective|start|check|checks|limit|history|clear|edit|pause|resume]",
74
74
  handoff: "/handoff [next-session focus]",
75
75
  variants: "/variants [level]",
76
76
  model: "/model [provider/model]",
@@ -1,4 +1,5 @@
1
1
  import { execFile } from "node:child_process";
2
+ import { watch } from "node:fs";
2
3
  import { type ExtensionAPI, type ExtensionContext, type Theme, type ThemeColor } from "@earendil-works/pi-coding-agent";
3
4
  import { truncateToWidth, visibleWidth, type TUI } from "@earendil-works/pi-tui";
4
5
  import { isCodexFastEnabled, subscribeCodexFast } from "./codex-fast-state.ts";
@@ -9,18 +10,45 @@ import { safeTerminalText } from "./safe-terminal-text.ts";
9
10
  import { LEVEL_COLORS, type ThinkingLevel } from "./variants.ts";
10
11
 
11
12
  const GIT_STATUS_REFRESH_INTERVAL_MS = 30_000;
13
+ const GIT_STATUS_TIMEOUT_MS = 5_000;
14
+ const GIT_STATUS_WATCH_DEBOUNCE_MS = 250;
15
+ const GIT_STATUS_WATCH_INTERVAL_MS = 5_000;
12
16
  const CODEX_PROVIDER = "openai-codex";
13
17
  const colorDirectory = (text: string): string => `\x1B[38;2;240;248;154m${text}\x1B[39m`;
14
18
 
15
- function resolveUncommittedFileCount(cwd: string): Promise<number | undefined> {
19
+ export interface GitFileChanges {
20
+ modified: number;
21
+ added: number;
22
+ deleted: number;
23
+ }
24
+
25
+ type GitStatusExecutor = (
26
+ file: string,
27
+ args: string[],
28
+ options: {
29
+ encoding: "utf8";
30
+ env: NodeJS.ProcessEnv;
31
+ maxBuffer: number;
32
+ timeout: number;
33
+ windowsHide: true;
34
+ },
35
+ callback: (error: Error | null, stdout: string) => void,
36
+ ) => unknown;
37
+
38
+ /** Resolves changed-file counts with a bounded asynchronous Git status process. */
39
+ export function resolveGitFileChanges(
40
+ cwd: string,
41
+ execute: GitStatusExecutor = execFile,
42
+ ): Promise<GitFileChanges | undefined> {
16
43
  return new Promise((resolve) => {
17
- execFile(
44
+ execute(
18
45
  "git",
19
46
  ["-C", cwd, "status", "--porcelain=v1", "-z", "--untracked-files=all"],
20
47
  {
21
48
  encoding: "utf8",
49
+ env: { ...process.env, GIT_OPTIONAL_LOCKS: "0" },
22
50
  maxBuffer: 4 * 1024 * 1024,
23
- timeout: 1_000,
51
+ timeout: GIT_STATUS_TIMEOUT_MS,
24
52
  windowsHide: true,
25
53
  },
26
54
  (error, stdout) => {
@@ -29,25 +57,32 @@ function resolveUncommittedFileCount(cwd: string): Promise<number | undefined> {
29
57
  return;
30
58
  }
31
59
 
60
+ const changes: GitFileChanges = { modified: 0, added: 0, deleted: 0 };
32
61
  const entries = stdout.split("\0");
33
- let count = 0;
34
62
  for (let index = 0; index < entries.length; index += 1) {
35
63
  const entry = entries[index];
36
64
  if (!entry) continue;
37
- count += 1;
38
- if (entry[0] === "R" || entry[0] === "C" || entry[1] === "R" || entry[1] === "C") index += 1;
65
+ const status = entry.slice(0, 2);
66
+ if (status.includes("D")) changes.deleted += 1;
67
+ else if (status === "??" || status.includes("A")) changes.added += 1;
68
+ else changes.modified += 1;
69
+ if (status.includes("R") || status.includes("C")) index += 1;
39
70
  }
40
- resolve(count);
71
+ resolve(changes);
41
72
  },
42
73
  );
43
74
  });
44
75
  }
45
76
 
46
- /** Coalesces Git status requests to one active scan and one queued follow-up. */
47
- export function createGitStatusRefresh(
77
+ async function resolveUncommittedFileCount(cwd: string): Promise<number | undefined> {
78
+ const changes = await resolveGitFileChanges(cwd);
79
+ return changes && changes.modified + changes.added + changes.deleted;
80
+ }
81
+
82
+ function createGitRefresh<T>(
48
83
  cwd: string,
49
- onCount: (count: number | undefined) => void,
50
- resolveCount: (cwd: string) => Promise<number | undefined> = resolveUncommittedFileCount,
84
+ onResult: (result: T | undefined) => void,
85
+ resolveResult: (cwd: string) => Promise<T | undefined>,
51
86
  ): { request: () => void; dispose: () => void } {
52
87
  let disposed = false;
53
88
  let pending = false;
@@ -59,9 +94,16 @@ export function createGitStatusRefresh(
59
94
  return;
60
95
  }
61
96
  pending = true;
62
- void resolveCount(cwd).then((count) => {
63
- if (!disposed) onCount(count);
64
- }).finally(() => {
97
+ let result: Promise<T | undefined>;
98
+ try {
99
+ result = resolveResult(cwd);
100
+ } catch {
101
+ result = Promise.resolve(undefined);
102
+ }
103
+ void result.then(
104
+ (value) => { if (!disposed) onResult(value); },
105
+ () => { if (!disposed) onResult(undefined); },
106
+ ).finally(() => {
65
107
  pending = false;
66
108
  if (!disposed && queued) {
67
109
  queued = false;
@@ -78,7 +120,29 @@ export function createGitStatusRefresh(
78
120
  };
79
121
  }
80
122
 
123
+ /** Coalesces Git status requests to one active scan and one queued follow-up. */
124
+ export function createGitStatusRefresh(
125
+ cwd: string,
126
+ onCount: (count: number | undefined) => void,
127
+ resolveCount: (cwd: string) => Promise<number | undefined> = resolveUncommittedFileCount,
128
+ ): { request: () => void; dispose: () => void } {
129
+ return createGitRefresh(cwd, onCount, resolveCount);
130
+ }
131
+
132
+ /** Coalesces file-change scans and emits only successful results. */
133
+ export function createGitFileChangesRefresh(
134
+ cwd: string,
135
+ onChanges: (changes: GitFileChanges) => void,
136
+ resolveChanges: (cwd: string) => Promise<GitFileChanges | undefined> = resolveGitFileChanges,
137
+ ): { request: () => void; dispose: () => void } {
138
+ return createGitRefresh(cwd, (changes) => {
139
+ if (changes !== undefined) onChanges(changes);
140
+ }, resolveChanges);
141
+ }
142
+
81
143
  type ScheduleFallback = (refresh: () => void, intervalMs: number) => () => void;
144
+ type WatchDirectory = (cwd: string, onChange: () => void, onError: () => void) => () => void;
145
+ type ScheduleDelay = (refresh: () => void, delayMs: number) => () => void;
82
146
 
83
147
  const scheduleFallback: ScheduleFallback = (refresh, intervalMs) => {
84
148
  const timer = setInterval(refresh, intervalMs);
@@ -86,6 +150,18 @@ const scheduleFallback: ScheduleFallback = (refresh, intervalMs) => {
86
150
  return () => clearInterval(timer);
87
151
  };
88
152
 
153
+ const watchDirectory: WatchDirectory = (cwd, onChange, onError) => {
154
+ const watcher = watch(cwd, { recursive: true }, onChange);
155
+ watcher.on("error", onError);
156
+ return () => watcher.close();
157
+ };
158
+
159
+ const scheduleDelay: ScheduleDelay = (refresh, delayMs) => {
160
+ const timer = setTimeout(refresh, delayMs);
161
+ timer.unref?.();
162
+ return () => clearTimeout(timer);
163
+ };
164
+
89
165
  /** Schedules the fallback Git scan independently from footer rendering. */
90
166
  export function scheduleGitStatusFallback(
91
167
  refresh: () => void,
@@ -94,6 +170,55 @@ export function scheduleGitStatusFallback(
94
170
  return schedule(refresh, GIT_STATUS_REFRESH_INTERVAL_MS);
95
171
  }
96
172
 
173
+ /** Throttles filesystem changes and falls back silently when watching is unavailable. */
174
+ export function scheduleGitStatusWatch(
175
+ cwd: string,
176
+ refresh: () => void,
177
+ startWatching: WatchDirectory = watchDirectory,
178
+ schedule: ScheduleDelay = scheduleDelay,
179
+ ): () => void {
180
+ let disposed = false;
181
+ let coolingDown = false;
182
+ let queued = false;
183
+ let stopTimer: (() => void) | undefined;
184
+ let stopWatching: (() => void) | undefined;
185
+ const stop = (): void => {
186
+ if (disposed) return;
187
+ disposed = true;
188
+ stopTimer?.();
189
+ stopTimer = undefined;
190
+ stopWatching?.();
191
+ stopWatching = undefined;
192
+ };
193
+ const changed = (): void => {
194
+ if (disposed) return;
195
+ if (stopTimer) {
196
+ if (coolingDown) queued = true;
197
+ return;
198
+ }
199
+ stopTimer = schedule(() => {
200
+ stopTimer = undefined;
201
+ if (disposed) return;
202
+ refresh();
203
+ coolingDown = true;
204
+ stopTimer = schedule(() => {
205
+ stopTimer = undefined;
206
+ coolingDown = false;
207
+ if (queued) {
208
+ queued = false;
209
+ changed();
210
+ }
211
+ }, GIT_STATUS_WATCH_INTERVAL_MS);
212
+ }, GIT_STATUS_WATCH_DEBOUNCE_MS);
213
+ };
214
+ try {
215
+ stopWatching = startWatching(cwd, changed, stop);
216
+ } catch {
217
+ disposed = true;
218
+ }
219
+ return stop;
220
+ }
221
+
97
222
  export function formatCost(usd: number): string {
98
223
  if (!Number.isFinite(usd)) return "$—";
99
224
  return `$${usd.toFixed(2)}`;
@@ -229,13 +354,20 @@ function renderFooter(rows: string[], width: number, theme: Theme): string[] {
229
354
  function formatGoalFooter(state: GoalState | undefined, theme: Theme): string {
230
355
  if (!state) return "";
231
356
  if (state.status === "active") {
232
- return theme.fg("warning", `/goal is active (${formatTime(goalElapsedMilliseconds(state, Date.now()))})`);
357
+ const turns = state.maxTurns === undefined ? "" : ` ${state.turns}/${state.maxTurns}`;
358
+ return theme.fg("warning", `/goal is active${turns} (${formatTime(goalElapsedMilliseconds(state, Date.now()))})`);
233
359
  }
234
360
  if (state.status === "paused") return theme.fg("warning", "/goal is paused");
235
361
  if (state.status === "blocked") return theme.fg("error", "/goal is blocked");
236
362
  return "";
237
363
  }
238
364
 
365
+ function formatGitFileChanges(changes: GitFileChanges, theme: Theme): string {
366
+ const total = changes.modified + changes.added + changes.deleted;
367
+ if (total === 0) return "";
368
+ return `${theme.fg("dim", `±${total} [`)}${theme.fg("warning", `~${changes.modified}`)} ${theme.fg("success", `+${changes.added}`)} ${theme.fg("error", `−${changes.deleted}`)}${theme.fg("dim", "]")}`;
369
+ }
370
+
239
371
  export function registerFooter(pi: ExtensionAPI, goalRuntime: GoalRuntime): void {
240
372
  let currentModel: ExtensionContext["model"];
241
373
  let thinkingLevel: ThinkingLevel = "off";
@@ -243,7 +375,6 @@ export function registerFooter(pi: ExtensionAPI, goalRuntime: GoalRuntime): void
243
375
  let cachedSessionCost = 0;
244
376
  let sessionCostDirty = true;
245
377
  let unsubscribeCodexFast: (() => void) | undefined;
246
- let requestGitStatusRefresh: (() => void) | undefined;
247
378
  const resetSessionCost = (): void => {
248
379
  cachedSessionCost = 0;
249
380
  sessionCostDirty = true;
@@ -273,25 +404,25 @@ export function registerFooter(pi: ExtensionAPI, goalRuntime: GoalRuntime): void
273
404
 
274
405
  ctx.ui.setFooter((tui, theme, footerData) => {
275
406
  activeTui = tui;
276
- let uncommittedFileCount: number | undefined;
277
- const gitStatus = createGitStatusRefresh(ctx.cwd, (count) => {
278
- if (count === uncommittedFileCount) return;
279
- uncommittedFileCount = count;
407
+ let gitFileChanges: GitFileChanges | undefined;
408
+ const gitStatus = createGitFileChangesRefresh(ctx.cwd, (changes) => {
409
+ if (JSON.stringify(changes) === JSON.stringify(gitFileChanges)) return;
410
+ gitFileChanges = changes;
280
411
  tui.requestRender();
281
412
  });
282
- requestGitStatusRefresh = gitStatus.request;
283
413
  const unsubscribe = footerData.onBranchChange(() => {
284
414
  gitStatus.request();
285
415
  tui.requestRender();
286
416
  });
287
417
  gitStatus.request();
418
+ const stopWatch = scheduleGitStatusWatch(ctx.cwd, gitStatus.request);
288
419
  const stopFallback = scheduleGitStatusFallback(gitStatus.request);
289
420
  return {
290
421
  dispose() {
291
422
  unsubscribe();
423
+ stopWatch();
292
424
  stopFallback();
293
425
  gitStatus.dispose();
294
- if (requestGitStatusRefresh === gitStatus.request) requestGitStatusRefresh = undefined;
295
426
  if (activeTui === tui) activeTui = undefined;
296
427
  },
297
428
  invalidate() {},
@@ -332,10 +463,9 @@ export function registerFooter(pi: ExtensionAPI, goalRuntime: GoalRuntime): void
332
463
  : footerRowFits(primaryFocused, "", width)
333
464
  ? renderFooterRow(primaryFocused, "", width)
334
465
  : renderFooterRow(essentialModel, context, width);
466
+ const changes = gitFileChanges ? formatGitFileChanges(gitFileChanges, theme) : "";
335
467
  const branchLabel = branch
336
- ? uncommittedFileCount
337
- ? `${theme.fg("dim", `${branch} · `)}${theme.fg("warning", `${uncommittedFileCount} changed`)}`
338
- : theme.fg("dim", branch)
468
+ ? `${theme.fg("dim", branch)}${changes ? `${theme.fg("dim", " · ")}${changes}` : ""}`
339
469
  : "";
340
470
  const workspaceRight = goal || fullDirectory;
341
471
  const secondaryRow = footerRowFits(branchLabel, workspaceRight, width)
@@ -359,12 +489,8 @@ export function registerFooter(pi: ExtensionAPI, goalRuntime: GoalRuntime): void
359
489
  thinkingLevel = event.level;
360
490
  activeTui?.requestRender();
361
491
  });
362
- const refreshAfterActivity = (): void => {
363
- invalidateSessionCost();
364
- requestGitStatusRefresh?.();
365
- };
366
- pi.on("turn_end", refreshAfterActivity);
367
- pi.on("session_compact", refreshAfterActivity);
492
+ pi.on("turn_end", invalidateSessionCost);
493
+ pi.on("session_compact", invalidateSessionCost);
368
494
  pi.on("session_tree", () => {
369
495
  resetSessionCost();
370
496
  activeTui?.requestRender();
@@ -374,7 +500,6 @@ export function registerFooter(pi: ExtensionAPI, goalRuntime: GoalRuntime): void
374
500
  unsubscribeCodexFast = undefined;
375
501
  resetSessionCost();
376
502
  activeTui = undefined;
377
- requestGitStatusRefresh = undefined;
378
503
  goalRuntime.requestRender = undefined;
379
504
  });
380
505
  }
@@ -0,0 +1,120 @@
1
+ import { parseArgs } from "node:util";
2
+ import { GOAL_CHECK_NAME_PATTERN, GOAL_MAX_TURNS, validateGoalObjective } from "./goal-state.ts";
3
+
4
+ const GOAL_START_USAGE = "Usage: /goal start [--check <name>] [--turns <count>] -- <objective>";
5
+ const GOAL_CHECK_USAGE = "Usage: /goal check <name|clear>";
6
+ const GOAL_CHECKS_USAGE = "Usage: /goal checks";
7
+ const GOAL_LIMIT_USAGE = "Usage: /goal limit <count|clear>";
8
+ const GOAL_HISTORY_USAGE = "Usage: /goal history [count]";
9
+ const RESERVED_OBJECTIVE_USAGE = "Objective begins with a reserved goal command. Use /goal start -- <objective>.";
10
+
11
+ interface ControlledGoalStart {
12
+ objective: string;
13
+ completionCheckName?: string;
14
+ maxTurns?: number;
15
+ }
16
+
17
+ export type GoalCommand =
18
+ | { kind: "status" }
19
+ | { kind: "objective"; objective: string }
20
+ | { kind: "start"; objective: string; completionCheckName?: string; maxTurns?: number }
21
+ | { kind: "check"; value: { kind: "clear" } | { kind: "named"; name: string } }
22
+ | { kind: "checks" }
23
+ | { kind: "limit"; value: { kind: "clear" } | { kind: "count"; count: number } }
24
+ | { kind: "history"; count: number }
25
+ | { kind: "clear" }
26
+ | { kind: "edit" }
27
+ | { kind: "pause" }
28
+ | { kind: "resume" }
29
+ | { kind: "invalid"; message: string };
30
+
31
+ const RESERVED_GOAL_WORDS = ["start", "check", "checks", "limit", "history", "clear", "edit", "pause", "resume"] as const;
32
+
33
+ function parseBoundedInteger(value: string, maximum: number): number | undefined {
34
+ if (!/^[1-9][0-9]*$/u.test(value)) return undefined;
35
+ const parsed = Number(value);
36
+ return Number.isSafeInteger(parsed) && parsed <= maximum ? parsed : undefined;
37
+ }
38
+
39
+ function parseControlledGoalStart(input: string): ControlledGoalStart | undefined {
40
+ const separator = input.startsWith("-- ") ? 0 : input.indexOf(" -- ");
41
+ if (separator < 0) return undefined;
42
+ const optionText = input.slice(0, separator).trim();
43
+ const objective = validateGoalObjective(input.slice(separator + (separator === 0 ? 3 : 4)));
44
+ if (!objective) return undefined;
45
+ const tokens = optionText ? optionText.split(/\s+/u) : [];
46
+ if (tokens.filter((token) => token === "--check" || token.startsWith("--check=")).length > 1
47
+ || tokens.filter((token) => token === "--turns" || token.startsWith("--turns=")).length > 1) return undefined;
48
+ try {
49
+ const parsed = parseArgs({
50
+ args: tokens,
51
+ options: { check: { type: "string" }, turns: { type: "string" } },
52
+ strict: true,
53
+ allowPositionals: false,
54
+ });
55
+ const check = parsed.values.check;
56
+ const turns = parsed.values.turns;
57
+ if (check !== undefined && !GOAL_CHECK_NAME_PATTERN.test(check)) return undefined;
58
+ const maxTurns = turns === undefined ? undefined : parseBoundedInteger(turns, GOAL_MAX_TURNS);
59
+ if (turns !== undefined && maxTurns === undefined) return undefined;
60
+ return {
61
+ objective,
62
+ ...(check === undefined ? {} : { completionCheckName: check }),
63
+ ...(maxTurns === undefined ? {} : { maxTurns }),
64
+ };
65
+ } catch {
66
+ return undefined;
67
+ }
68
+ }
69
+
70
+ /** Parses one raw /goal argument string into a closed command variant. */
71
+ export function parseGoalCommand(args: string): GoalCommand {
72
+ const input = args.trim();
73
+ if (!input) return { kind: "status" };
74
+ const parts = input.split(/\s+/u);
75
+ const rawFirstWord = parts[0] ?? "";
76
+ const firstWord = RESERVED_GOAL_WORDS.find((word) => word === rawFirstWord.toLowerCase());
77
+ if (!firstWord) {
78
+ const objective = validateGoalObjective(input);
79
+ return objective
80
+ ? { kind: "objective", objective }
81
+ : { kind: "invalid", message: "A goal objective may not exceed 4,000 characters" };
82
+ }
83
+
84
+ if (firstWord === "start") {
85
+ const start = parseControlledGoalStart(input.slice(rawFirstWord.length).trimStart());
86
+ if (start) return { kind: "start", ...start };
87
+ return { kind: "invalid", message: rawFirstWord === firstWord ? GOAL_START_USAGE : RESERVED_OBJECTIVE_USAGE };
88
+ }
89
+ if (firstWord === "check") {
90
+ const value = parts[1];
91
+ if (parts.length === 2 && value?.toLowerCase() === "clear") return { kind: "check", value: { kind: "clear" } };
92
+ if (parts.length === 2 && value && GOAL_CHECK_NAME_PATTERN.test(value)) return { kind: "check", value: { kind: "named", name: value } };
93
+ return { kind: "invalid", message: rawFirstWord !== firstWord && parts.length > 2 ? RESERVED_OBJECTIVE_USAGE : GOAL_CHECK_USAGE };
94
+ }
95
+ if (firstWord === "checks") {
96
+ return parts.length === 1
97
+ ? { kind: "checks" }
98
+ : { kind: "invalid", message: rawFirstWord === firstWord ? GOAL_CHECKS_USAGE : RESERVED_OBJECTIVE_USAGE };
99
+ }
100
+ if (firstWord === "limit") {
101
+ const value = parts[1];
102
+ if (parts.length === 2 && value?.toLowerCase() === "clear") return { kind: "limit", value: { kind: "clear" } };
103
+ const count = parts.length === 2 && value ? parseBoundedInteger(value, GOAL_MAX_TURNS) : undefined;
104
+ if (count !== undefined) return { kind: "limit", value: { kind: "count", count } };
105
+ return { kind: "invalid", message: rawFirstWord !== firstWord && parts.length > 2 ? RESERVED_OBJECTIVE_USAGE : GOAL_LIMIT_USAGE };
106
+ }
107
+ if (firstWord === "history") {
108
+ if (parts.length === 1) return { kind: "history", count: 20 };
109
+ const count = parts.length === 2 && parts[1] ? parseBoundedInteger(parts[1], 50) : undefined;
110
+ if (count !== undefined) return { kind: "history", count };
111
+ return { kind: "invalid", message: rawFirstWord !== firstWord && parts.length > 2 ? RESERVED_OBJECTIVE_USAGE : GOAL_HISTORY_USAGE };
112
+ }
113
+ if (parts.length === 1) return { kind: firstWord };
114
+ return {
115
+ kind: "invalid",
116
+ message: rawFirstWord === firstWord
117
+ ? `Usage: /goal ${firstWord}`
118
+ : RESERVED_OBJECTIVE_USAGE,
119
+ };
120
+ }
@@ -0,0 +1,71 @@
1
+ import { formatTime, formatTokens } from "./display.ts";
2
+ import { parseGoalState } from "./goal-state.ts";
3
+ import type { GoalState } from "./runtime.ts";
4
+ import { safeTerminalText } from "./safe-terminal-text.ts";
5
+
6
+ const GOAL_ENTRY_TYPE = "killeros-goal";
7
+ const VALID_GOAL_EVENTS: ReadonlySet<string> = new Set([
8
+ "set", "replace", "edit", "check", "limit", "turn", "pause", "resume",
9
+ "blocker-audit", "blocked", "complete", "error", "clear", "checkpoint",
10
+ ]);
11
+ function isUnknownRecord(value: unknown): value is Record<string, unknown> {
12
+ return typeof value === "object" && value !== null && !Array.isArray(value);
13
+ }
14
+
15
+ function tokenUsage(entry: Record<string, unknown>): number | undefined {
16
+ if (entry.type === "message" && isUnknownRecord(entry.message)
17
+ && (entry.message.role === "assistant" || entry.message.role === "toolResult")
18
+ && isUnknownRecord(entry.message.usage)
19
+ && typeof entry.message.usage.totalTokens === "number"
20
+ && Number.isFinite(entry.message.usage.totalTokens)
21
+ && entry.message.usage.totalTokens >= 0) {
22
+ return entry.message.usage.totalTokens;
23
+ }
24
+ if ((entry.type === "compaction" || entry.type === "branch_summary")
25
+ && isUnknownRecord(entry.usage)
26
+ && typeof entry.usage.totalTokens === "number"
27
+ && Number.isFinite(entry.usage.totalTokens)
28
+ && entry.usage.totalTokens >= 0) {
29
+ return entry.usage.totalTokens;
30
+ }
31
+ return undefined;
32
+ }
33
+
34
+ function preview(value: string): string {
35
+ const safe = safeTerminalText(value).replaceAll("\n", " ").trim();
36
+ const characters = [...safe];
37
+ return characters.length <= 160 ? safe : `${characters.slice(0, 159).join("")}…`;
38
+ }
39
+
40
+ function eventDetail(event: string, state: GoalState): string {
41
+ if (event === "check") return state.completionCheck ? `check ${state.completionCheck.name}` : "check cleared";
42
+ if (event === "limit") return state.maxTurns === undefined ? "limit cleared" : `limit ${state.maxTurns}`;
43
+ if (event === "blocker-audit" && state.blockerAudit) {
44
+ return `Blocker ${state.blockerAudit.streak}/3: ${state.blockerAudit.evidence ?? state.blockerAudit.key}`;
45
+ }
46
+ return state.result || state.objective;
47
+ }
48
+
49
+ /** Projects branch entries into the latest bounded goal-history rows. */
50
+ export function formatGoalHistory(entries: readonly unknown[], count: number): string | undefined {
51
+ const lines: string[] = [];
52
+ let tokens = 0;
53
+ let previousState: GoalState | undefined;
54
+ for (const value of entries) {
55
+ if (!isUnknownRecord(value)) continue;
56
+ const usage = tokenUsage(value);
57
+ if (usage !== undefined) {
58
+ tokens += usage;
59
+ continue;
60
+ }
61
+ if (value.type !== "custom" || value.customType !== GOAL_ENTRY_TYPE || !isUnknownRecord(value.data)) continue;
62
+ const event = value.data.event;
63
+ if (typeof event !== "string" || !VALID_GOAL_EVENTS.has(event)) continue;
64
+ const state = value.data.state === null ? previousState : parseGoalState(value.data.state);
65
+ if (!state) continue;
66
+ if (value.data.state !== null) previousState = state;
67
+ if (event === "turn" || event === "checkpoint") continue;
68
+ lines.push(`+${formatTime(Math.max(0, state.updatedAt - state.createdAt))} ${event} turn ${state.turns} ${formatTokens(Math.max(0, tokens - state.baselineTokens))} tokens ${preview(eventDetail(event, state))}`);
69
+ }
70
+ return lines.length ? lines.slice(-count).join("\n") : undefined;
71
+ }