@oh-my-pi/pi-coding-agent 16.3.9 → 16.3.10

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.
@@ -33,7 +33,7 @@ import { TranscriptContainer } from "./components/transcript-container.js";
33
33
  import { EventController } from "./controllers/event-controller.js";
34
34
  import { type LoopLimitRuntime } from "./loop-limit.js";
35
35
  import { OAuthManualInputManager } from "./oauth-manual-input.js";
36
- import type { ObservableSession } from "./session-observer-registry.js";
36
+ import { type ObservableSession } from "./session-observer-registry.js";
37
37
  import type { Theme } from "./theme/theme.js";
38
38
  import type { CompactionQueuedMessage, InteractiveModeContext, InteractiveModeInitOptions, InteractiveSelectorDialogOptions, SubmittedUserInput, TodoItem, TodoPhase } from "./types.js";
39
39
  /**
@@ -63,7 +63,7 @@ export interface InteractiveModeOptions {
63
63
  }
64
64
  /**
65
65
  * Build the anchored subagent HUD block: a bold accent "Subagents" header plus
66
- * one tree row per running agent in the same `Id: description` shape the
66
+ * a bounded set of running-agent rows in the same `Id: description` shape the
67
67
  * inline task rows use (muted task preview when no description was given).
68
68
  * Layout mirrors the Todos HUD exactly: unindented header, then
69
69
  * `renderTreeList` rows (dim connectors) shifted right by one space.
@@ -217,7 +217,9 @@ export declare class InteractiveMode implements InteractiveModeContext {
217
217
  finishPendingSubmission(input: SubmittedUserInput): void;
218
218
  updateEditorBorderColor(): void;
219
219
  /** Refresh the running-subagents status badge from the active local or collab registry. */
220
- syncRunningSubagentBadge(): void;
220
+ syncRunningSubagentBadge(options?: {
221
+ requestRender?: boolean;
222
+ }): void;
221
223
  rebuildChatFromMessages(): void;
222
224
  /**
223
225
  * Render the ctrl+p model-role cycle chip track into its own anchored
@@ -21,10 +21,12 @@ export interface ObservableSession {
21
21
  /** Latest progress snapshot from the subagent executor */
22
22
  progress?: AgentProgress;
23
23
  }
24
+ /** Coarse source of an observer change; callers use it to separate lifecycle work from high-frequency progress. */
25
+ export type SessionObserverChangeKind = "main" | "reset" | "lifecycle" | "progress";
24
26
  export declare class SessionObserverRegistry {
25
27
  #private;
26
28
  /** Add a change listener. Returns unsubscribe function. */
27
- onChange(cb: () => void): () => void;
29
+ onChange(cb: (kind: SessionObserverChangeKind) => void): () => void;
28
30
  setMainSession(sessionFile?: string): void;
29
31
  getSessions(): ObservableSession[];
30
32
  getActiveSubagentCount(): number;
@@ -423,6 +423,7 @@ export interface SessionStats {
423
423
  };
424
424
  premiumRequests: number;
425
425
  cost: number;
426
+ contextUsage?: ContextUsage;
426
427
  }
427
428
  /** Advisor statistics for /advisor status command. */
428
429
  export interface AdvisorStats {
@@ -1160,15 +1161,18 @@ export declare class AgentSession {
1160
1161
  /** Whether there are pending Python messages waiting to be flushed */
1161
1162
  get hasPendingPythonMessages(): boolean;
1162
1163
  /**
1163
- * Surfaces (and consumes) pending IRC incoming records before the next model
1164
+ * Surfaces and consumes pending IRC incoming records before the next model
1164
1165
  * step can inject them automatically.
1165
1166
  *
1166
- * The inbox tool injects the formatted body into the tool result, so the
1167
- * model sees it once via the result. Leaving the record in either pending
1168
- * IRC queue would deliver it a second time at the next step boundary —
1169
- * including on `peek`, which is why peek also drains here.
1170
- */
1171
- drainPendingIrcInboxMessages(agentId: string): IrcMessage[];
1167
+ * Tool results already expose the formatted body to the model. Leaving the
1168
+ * same record in either pending IRC queue would deliver it a second time at
1169
+ * the next step boundary including on `peek`, which is why inbox peeks
1170
+ * also drain here.
1171
+ */
1172
+ drainPendingIrcInboxMessages(agentId: string, opts?: {
1173
+ from?: string;
1174
+ limit?: number;
1175
+ }): IrcMessage[];
1172
1176
  /**
1173
1177
  * Deliver an IRC message into this session (recipient side; called by the
1174
1178
  * IrcBus). Emits the `irc_message` session event for UI cards and injects
@@ -10,5 +10,4 @@ export declare const taskToolRenderer: {
10
10
  readonly renderCall: typeof renderCall;
11
11
  readonly renderResult: typeof renderResult;
12
12
  readonly mergeCallAndResult: true;
13
- readonly animatedPartialResult: true;
14
13
  };
@@ -1,3 +1,14 @@
1
+ /**
2
+ * Marks a run-scoped promise as observed without changing its behavior for awaited callers.
3
+ *
4
+ * Browser run teardown aborts can reject promises created for evaluated code after user code
5
+ * has stopped observing them (for example fire-and-forget `wait()`/facade calls). In 16.3.0
6
+ * those zero-consumer rejections reached the process-level `unhandledRejection` handler and
7
+ * killed every subagent sharing the process (issues #4499/#4672). Attaching a no-op rejection
8
+ * handler at creation makes the promise observed while returning the original promise so callers
9
+ * that do await it still receive the rejection.
10
+ */
11
+ export declare function markHandled<T>(promise: Promise<T>): Promise<T>;
1
12
  /** Sleeps inside evaluated browser code while honoring the owning run's cancellation signal. */
2
13
  export declare function waitForBrowserRun(ms: number, signal: AbortSignal): Promise<void>;
3
14
  /** Binds a long-lived browser facade to one evaluated run's abort signal. */
@@ -80,6 +80,7 @@ export type WorkerInbound = {
80
80
  } | {
81
81
  type: "abort";
82
82
  id: string;
83
+ expectedCleanup?: boolean;
83
84
  } | {
84
85
  type: "tool-reply";
85
86
  id: string;
@@ -19,7 +19,7 @@ export declare class ToolError extends Error {
19
19
  */
20
20
  export declare class ToolAbortError extends Error {
21
21
  static readonly MESSAGE = "Operation aborted";
22
- constructor(message?: string);
22
+ constructor(message?: string, options?: ErrorOptions);
23
23
  }
24
24
  /**
25
25
  * Throw ToolAbortError if the signal is aborted.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@oh-my-pi/pi-coding-agent",
4
- "version": "16.3.9",
4
+ "version": "16.3.10",
5
5
  "description": "Coding agent CLI with read, bash, edit, write tools and session management",
6
6
  "homepage": "https://omp.sh",
7
7
  "author": "Can Boluk",
@@ -56,17 +56,17 @@
56
56
  "@agentclientprotocol/sdk": "0.25.0",
57
57
  "@babel/parser": "^7.29.7",
58
58
  "@mozilla/readability": "^0.6.0",
59
- "@oh-my-pi/hashline": "16.3.9",
60
- "@oh-my-pi/omp-stats": "16.3.9",
61
- "@oh-my-pi/pi-agent-core": "16.3.9",
62
- "@oh-my-pi/pi-ai": "16.3.9",
63
- "@oh-my-pi/pi-catalog": "16.3.9",
64
- "@oh-my-pi/pi-mnemopi": "16.3.9",
65
- "@oh-my-pi/pi-natives": "16.3.9",
66
- "@oh-my-pi/pi-tui": "16.3.9",
67
- "@oh-my-pi/pi-utils": "16.3.9",
68
- "@oh-my-pi/pi-wire": "16.3.9",
69
- "@oh-my-pi/snapcompact": "16.3.9",
59
+ "@oh-my-pi/hashline": "16.3.10",
60
+ "@oh-my-pi/omp-stats": "16.3.10",
61
+ "@oh-my-pi/pi-agent-core": "16.3.10",
62
+ "@oh-my-pi/pi-ai": "16.3.10",
63
+ "@oh-my-pi/pi-catalog": "16.3.10",
64
+ "@oh-my-pi/pi-mnemopi": "16.3.10",
65
+ "@oh-my-pi/pi-natives": "16.3.10",
66
+ "@oh-my-pi/pi-tui": "16.3.10",
67
+ "@oh-my-pi/pi-utils": "16.3.10",
68
+ "@oh-my-pi/pi-wire": "16.3.10",
69
+ "@oh-my-pi/snapcompact": "16.3.10",
70
70
  "@opentelemetry/api": "^1.9.1",
71
71
  "@opentelemetry/context-async-hooks": "^2.7.1",
72
72
  "@opentelemetry/exporter-trace-otlp-proto": "^0.218.0",
@@ -42,6 +42,8 @@ describe("SkillMessageComponent", () => {
42
42
  // New look: an icon-tagged "skill" header with the name and a single meta line.
43
43
  expect(text).toContain("skill");
44
44
  expect(text).toContain("atomic-commit");
45
+ expect(text).toContain("skill atomic-commit");
46
+ expect(text).not.toContain("skill atomic-commit");
45
47
  expect(text).toContain("88 lines");
46
48
 
47
49
  // The card is drawn with an outline.
@@ -34,6 +34,7 @@ import { DynamicBorder } from "./dynamic-border";
34
34
 
35
35
  /** Refresh cadence for the relative-time column */
36
36
  const AGE_TICK_MS = 5_000;
37
+ const DATA_CHANGE_RENDER_COALESCE_MS = 100;
37
38
  /** Double-tap window for the table's left-left "close hub" gesture. */
38
39
  const LEFT_TAP_WINDOW_MS = 500;
39
40
 
@@ -198,6 +199,7 @@ export class AgentHubOverlayComponent extends Container {
198
199
  #hubKeys: KeyId[];
199
200
  #unsubscribers: Array<() => void> = [];
200
201
  #ageTimer: NodeJS.Timeout | undefined;
202
+ #dataChangeTimer?: NodeJS.Timeout;
201
203
  #remote: AgentHubRemote | undefined;
202
204
  /** Resolves after persisted historical subagents have been registered and rows refreshed. */
203
205
  readonly persistedSubagentsReady: Promise<void>;
@@ -251,8 +253,8 @@ export class AgentHubOverlayComponent extends Container {
251
253
  this.#expandKeys = deps.expandKeys ?? ["ctrl+o"];
252
254
  this.#focusAgent = deps.focusAgent;
253
255
 
254
- this.#unsubscribers.push(this.#registry.onChange(() => this.#onDataChange()));
255
- this.#unsubscribers.push(this.#observers.onChange(() => this.#onDataChange()));
256
+ this.#unsubscribers.push(this.#registry.onChange(() => this.#scheduleDataChange()));
257
+ this.#unsubscribers.push(this.#observers.onChange(() => this.#scheduleDataChange()));
256
258
  this.#ageTimer = setInterval(() => this.#requestRender(), AGE_TICK_MS);
257
259
  this.#ageTimer.unref?.();
258
260
 
@@ -285,6 +287,10 @@ export class AgentHubOverlayComponent extends Container {
285
287
  clearInterval(this.#ageTimer);
286
288
  this.#ageTimer = undefined;
287
289
  }
290
+ if (this.#dataChangeTimer) {
291
+ clearTimeout(this.#dataChangeTimer);
292
+ this.#dataChangeTimer = undefined;
293
+ }
288
294
  this.#closeTranscriptOverlay();
289
295
  }
290
296
 
@@ -355,6 +361,15 @@ export class AgentHubOverlayComponent extends Container {
355
361
  // Live data plumbing
356
362
  // ========================================================================
357
363
 
364
+ #scheduleDataChange(): void {
365
+ if (this.#dataChangeTimer) return;
366
+ this.#dataChangeTimer = setTimeout(() => {
367
+ this.#dataChangeTimer = undefined;
368
+ this.#onDataChange();
369
+ }, DATA_CHANGE_RENDER_COALESCE_MS);
370
+ this.#dataChangeTimer.unref?.();
371
+ }
372
+
358
373
  #onDataChange(): void {
359
374
  this.#refreshRows();
360
375
  this.#requestRender();
@@ -50,7 +50,7 @@ export class SkillMessageComponent extends Container {
50
50
 
51
51
  // Header: icon-tag + skill name, with the invocation args trailing dimmed.
52
52
  const tag = theme.fg("customMessageLabel", theme.bold(`${theme.icon.extensionSkill} skill`));
53
- let header = `${tag} ${theme.fg("customMessageText", theme.bold(name))}`;
53
+ let header = `${tag} ${theme.fg("customMessageText", theme.bold(name))}`;
54
54
  if (args) {
55
55
  header += ` ${theme.fg("dim", args)}`;
56
56
  }
@@ -1,6 +1,3 @@
1
- import { emergencyTerminalRestore } from "@oh-my-pi/pi-tui";
2
- import { postmortem } from "@oh-my-pi/pi-utils";
3
-
4
1
  /**
5
2
  * Interactive mode and embeddable RPC client exports for the coding agent.
6
3
  *
@@ -11,7 +8,3 @@ import { postmortem } from "@oh-my-pi/pi-utils";
11
8
  export * from "./interactive-mode";
12
9
  export * from "./rpc/rpc-client";
13
10
  export * from "./rpc/rpc-types";
14
-
15
- postmortem.register("terminal-restore", () => {
16
- emergencyTerminalRestore();
17
- });
@@ -161,8 +161,11 @@ import {
161
161
  } from "./loop-limit";
162
162
  import { OAuthManualInputManager } from "./oauth-manual-input";
163
163
  import { countRunningSubagentBadgeAgents, getRunningSubagentBadgeRegistry } from "./running-subagent-badge";
164
- import type { ObservableSession } from "./session-observer-registry";
165
- import { SessionObserverRegistry } from "./session-observer-registry";
164
+ import {
165
+ type ObservableSession,
166
+ type SessionObserverChangeKind,
167
+ SessionObserverRegistry,
168
+ } from "./session-observer-registry";
166
169
  import { createSessionTeardown, type SessionTeardown } from "./session-teardown";
167
170
  import { runProviderSetupWizard } from "./setup-wizard/lazy";
168
171
  import { interruptHint } from "./shared";
@@ -343,9 +346,12 @@ class AnchoredLiveContainer extends Container implements NativeScrollbackLiveReg
343
346
  * before it auto-clears, mirroring the todo HUD's auto-clear timer. */
344
347
  const MODEL_CYCLE_TRACK_CLEAR_MS = 4000;
345
348
 
349
+ const SUBAGENT_HUD_VISIBLE_LIMIT = 8;
350
+ const SUBAGENT_OBSERVER_UI_COALESCE_MS = 100;
351
+
346
352
  /**
347
353
  * Build the anchored subagent HUD block: a bold accent "Subagents" header plus
348
- * one tree row per running agent in the same `Id: description` shape the
354
+ * a bounded set of running-agent rows in the same `Id: description` shape the
349
355
  * inline task rows use (muted task preview when no description was given).
350
356
  * Layout mirrors the Todos HUD exactly: unindented header, then
351
357
  * `renderTreeList` rows (dim connectors) shifted right by one space.
@@ -361,9 +367,11 @@ export function renderSubagentHudLines(sessions: ObservableSession[], columns: n
361
367
  if (running.length === 0) return [];
362
368
 
363
369
  const dot = theme.styledSymbol("status.done", "accent");
370
+ const visible = running.slice(0, SUBAGENT_HUD_VISIBLE_LIMIT);
371
+ const hiddenCount = running.length - visible.length;
364
372
  const rows = renderTreeList(
365
373
  {
366
- items: running,
374
+ items: visible,
367
375
  expanded: true,
368
376
  renderItem: session => {
369
377
  const displayId = formatTaskId(session.id);
@@ -385,6 +393,9 @@ export function renderSubagentHudLines(sessions: ObservableSession[], columns: n
385
393
  },
386
394
  theme,
387
395
  );
396
+ if (hiddenCount > 0) {
397
+ rows.push(theme.fg("dim", `… ${hiddenCount} more running — open Agent Hub for full list`));
398
+ }
388
399
  return ["", theme.bold(theme.fg("accent", "Subagents")), ...rows.map(line => ` ${line}`)];
389
400
  }
390
401
 
@@ -588,6 +599,8 @@ export class InteractiveMode implements InteractiveModeContext {
588
599
  #observerRegistry: SessionObserverRegistry;
589
600
  #eventBus?: EventBus;
590
601
  #eventBusUnsubscribers: Array<() => void> = [];
602
+ #observerUiSyncTimer?: NodeJS.Timeout;
603
+ #observerUiSyncNeedsTodoReconcile = false;
591
604
  #agentRegistryUnsubscribe?: () => void;
592
605
  #agentRegistrySubscriptionTarget?: AgentRegistry;
593
606
  #mcpStatusOrder: string[] = [];
@@ -917,17 +930,8 @@ export class InteractiveMode implements InteractiveModeContext {
917
930
  }
918
931
  this.#observerRegistry.setMainSession(this.sessionManager.getSessionFile() ?? undefined);
919
932
  this.syncRunningSubagentBadge();
920
- this.#observerRegistry.onChange(() => {
921
- this.syncRunningSubagentBadge();
922
- // Auto-checkmark todos whose matching subagent just succeeded, then
923
- // re-render so the running override (the static "live" glyph when a
924
- // subagent is doing the work for a still-pending todo) updates as
925
- // subagents start, finish, or fail.
926
- this.#reconcileTodosWithSubagents();
927
- this.#syncTodoAutoClearTimer();
928
- this.#renderTodoList();
929
- this.#renderSubagentList();
930
- this.ui.requestRender();
933
+ this.#observerRegistry.onChange(kind => {
934
+ this.#scheduleObserverUiSync(kind);
931
935
  });
932
936
 
933
937
  // Load initial todos
@@ -1521,19 +1525,18 @@ export class InteractiveMode implements InteractiveModeContext {
1521
1525
  }
1522
1526
 
1523
1527
  /** Refresh the running-subagents status badge from the active local or collab registry. */
1524
- syncRunningSubagentBadge(): void {
1528
+ syncRunningSubagentBadge(options: { requestRender?: boolean } = {}): void {
1525
1529
  const registry = getRunningSubagentBadgeRegistry(this.collabGuest);
1526
1530
  if (this.#agentRegistrySubscriptionTarget !== registry) {
1527
1531
  this.#agentRegistryUnsubscribe?.();
1528
1532
  this.#agentRegistrySubscriptionTarget = registry;
1529
1533
  this.#agentRegistryUnsubscribe = registry.onChange(() => {
1530
1534
  this.syncRunningSubagentBadge();
1531
- this.ui.requestRender();
1532
1535
  });
1533
1536
  }
1534
1537
  const count = countRunningSubagentBadgeAgents(registry);
1535
1538
  this.statusLine.setSubagentCount(count);
1536
- this.ui.requestRender();
1539
+ if (options.requestRender !== false) this.ui.requestRender();
1537
1540
  }
1538
1541
 
1539
1542
  rebuildChatFromMessages(): void {
@@ -1754,6 +1757,38 @@ export class InteractiveMode implements InteractiveModeContext {
1754
1757
  return active ?? nonEmpty[nonEmpty.length - 1];
1755
1758
  }
1756
1759
 
1760
+ #scheduleObserverUiSync(kind: SessionObserverChangeKind): void {
1761
+ if (kind !== "progress") {
1762
+ this.#observerUiSyncNeedsTodoReconcile = true;
1763
+ }
1764
+ if (this.#observerUiSyncTimer) return;
1765
+ this.#observerUiSyncTimer = setTimeout(() => {
1766
+ this.#observerUiSyncTimer = undefined;
1767
+ this.#flushObserverUiSync();
1768
+ }, SUBAGENT_OBSERVER_UI_COALESCE_MS);
1769
+ this.#observerUiSyncTimer.unref?.();
1770
+ }
1771
+
1772
+ #flushObserverUiSync(): void {
1773
+ this.syncRunningSubagentBadge({ requestRender: false });
1774
+ if (this.#observerUiSyncNeedsTodoReconcile) {
1775
+ this.#observerUiSyncNeedsTodoReconcile = false;
1776
+ this.#reconcileTodosWithSubagents();
1777
+ }
1778
+ this.#syncTodoAutoClearTimer();
1779
+ this.#renderTodoList();
1780
+ this.#renderSubagentList();
1781
+ this.ui.requestRender();
1782
+ }
1783
+
1784
+ #cancelObserverUiSyncTimer(): void {
1785
+ if (this.#observerUiSyncTimer) {
1786
+ clearTimeout(this.#observerUiSyncTimer);
1787
+ this.#observerUiSyncTimer = undefined;
1788
+ }
1789
+ this.#observerUiSyncNeedsTodoReconcile = false;
1790
+ }
1791
+
1757
1792
  #renderTodoList(): void {
1758
1793
  this.todoContainer.clear();
1759
1794
  const phases = this.todoPhases.filter(phase => phase.tasks.length > 0);
@@ -3306,6 +3341,7 @@ export class InteractiveMode implements InteractiveModeContext {
3306
3341
  }
3307
3342
  this.#cleanupMicAnimation();
3308
3343
  this.#cancelTodoAutoClearTimer();
3344
+ this.#cancelObserverUiSyncTimer();
3309
3345
  this.#cancelGoalContinuation();
3310
3346
  if (this.#sttController) {
3311
3347
  this.#sttController.dispose();
@@ -24,6 +24,9 @@ export interface ObservableSession {
24
24
  progress?: AgentProgress;
25
25
  }
26
26
 
27
+ /** Coarse source of an observer change; callers use it to separate lifecycle work from high-frequency progress. */
28
+ export type SessionObserverChangeKind = "main" | "reset" | "lifecycle" | "progress";
29
+
27
30
  const STATUS_MAP: Record<string, ObservableSession["status"]> = {
28
31
  started: "active",
29
32
  completed: "completed",
@@ -33,20 +36,20 @@ const STATUS_MAP: Record<string, ObservableSession["status"]> = {
33
36
 
34
37
  export class SessionObserverRegistry {
35
38
  #sessions = new Map<string, ObservableSession>();
36
- #listeners = new Set<() => void>();
39
+ #listeners = new Set<(kind: SessionObserverChangeKind) => void>();
37
40
  #eventBusUnsubscribers: Array<() => void> = [];
38
41
  #sortOrderById = new Map<string, number>();
39
42
  #parentSortOrderById = new Map<string, number>();
40
43
  #nextSortOrder = 0;
41
44
 
42
45
  /** Add a change listener. Returns unsubscribe function. */
43
- onChange(cb: () => void): () => void {
46
+ onChange(cb: (kind: SessionObserverChangeKind) => void): () => void {
44
47
  this.#listeners.add(cb);
45
48
  return () => this.#listeners.delete(cb);
46
49
  }
47
50
 
48
- #notifyListeners(): void {
49
- for (const cb of this.#listeners) cb();
51
+ #notifyListeners(kind: SessionObserverChangeKind): void {
52
+ for (const cb of this.#listeners) cb(kind);
50
53
  }
51
54
 
52
55
  #ensureSortOrder(id: string): number {
@@ -85,7 +88,7 @@ export class SessionObserverRegistry {
85
88
  sessionFile: sessionFile ?? existing?.sessionFile,
86
89
  lastUpdate: Date.now(),
87
90
  });
88
- this.#notifyListeners();
91
+ this.#notifyListeners("main");
89
92
  }
90
93
 
91
94
  getSessions(): ObservableSession[] {
@@ -121,7 +124,7 @@ export class SessionObserverRegistry {
121
124
  this.#sortOrderById.clear();
122
125
  this.#parentSortOrderById.clear();
123
126
  this.#nextSortOrder = 0;
124
- this.#notifyListeners();
127
+ this.#notifyListeners("reset");
125
128
  }
126
129
 
127
130
  dispose(): void {
@@ -171,7 +174,7 @@ export class SessionObserverRegistry {
171
174
  lastUpdate: Date.now(),
172
175
  });
173
176
  }
174
- this.#notifyListeners();
177
+ this.#notifyListeners("lifecycle");
175
178
  }),
176
179
  );
177
180
 
@@ -208,7 +211,7 @@ export class SessionObserverRegistry {
208
211
  progress,
209
212
  });
210
213
  }
211
- this.#notifyListeners();
214
+ this.#notifyListeners("progress");
212
215
  }),
213
216
  );
214
217
  }
package/src/sdk.ts CHANGED
@@ -2132,6 +2132,24 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
2132
2132
  }
2133
2133
  }
2134
2134
 
2135
+ if (model) {
2136
+ const selectedModel = model;
2137
+ const refreshedModel = await logger.time("refreshInitialModelMetadata", () =>
2138
+ modelRegistry.refreshSelectedModelMetadata(selectedModel),
2139
+ );
2140
+ if (refreshedModel !== selectedModel) {
2141
+ model = refreshedModel;
2142
+ thinkingLevel = pickInitialThinkingLevel(refreshedModel);
2143
+ autoThinking = thinkingLevel === AUTO_THINKING;
2144
+ effectiveThinkingLevel = concreteThinkingLevel(thinkingLevel);
2145
+ effectiveThinkingLevel = logger.time("resolveThinkingLevelForModel", () =>
2146
+ autoThinking
2147
+ ? resolveProvisionalAutoLevel(refreshedModel)
2148
+ : resolveThinkingLevelForModel(refreshedModel, effectiveThinkingLevel),
2149
+ );
2150
+ }
2151
+ }
2152
+
2135
2153
  // Discover custom commands (TypeScript slash commands)
2136
2154
  const customCommandsResult: CustomCommandsLoadResult = options.disableExtensionDiscovery
2137
2155
  ? { commands: [], errors: [] }
@@ -933,6 +933,7 @@ export interface SessionStats {
933
933
  };
934
934
  premiumRequests: number;
935
935
  cost: number;
936
+ contextUsage?: ContextUsage;
936
937
  }
937
938
 
938
939
  /** Advisor statistics for /advisor status command. */
@@ -1824,13 +1825,19 @@ export class AgentSession {
1824
1825
  * Sticky across an in-flight prompt run: a successful `yield` makes the run
1825
1826
  * terminal for execution purposes, so any trailing empty/aborted assistant
1826
1827
  * stop must NOT trigger empty-stop/unexpected-stop/compaction continuations.
1827
- * Cleared in `#promptWithMessage` so the next prompt evaluates cleanly.
1828
+ * Cleared before every new prompt turn so the next turn evaluates cleanly.
1828
1829
  */
1829
1830
  #yieldTerminationPending = false;
1830
1831
  #providerSessionState = new Map<string, ProviderSessionState>();
1831
1832
  #hindsightSessionState: HindsightSessionState | undefined = undefined;
1832
1833
  readonly rawSseDebugBuffer: RawSseDebugBuffer;
1833
1834
 
1835
+ #resetPromptMaintenanceState(): void {
1836
+ this.#emptyStopRetryCount = 0;
1837
+ this.#unexpectedStopRetryCount = 0;
1838
+ this.#yieldTerminationPending = false;
1839
+ }
1840
+
1834
1841
  #acquirePowerAssertion(): void {
1835
1842
  if (process.platform !== "darwin") return;
1836
1843
  if (isBunTestRuntime()) return;
@@ -1949,6 +1956,7 @@ export class AgentSession {
1949
1956
  if (parkedFollowUps.length > 0) {
1950
1957
  this.agent.replaceQueues([...this.agent.peekSteeringQueue()], []);
1951
1958
  }
1959
+ this.#resetPromptMaintenanceState();
1952
1960
  this.#beginInFlight();
1953
1961
  void this.agent
1954
1962
  .prompt(records)
@@ -7566,12 +7574,7 @@ export class AgentSession {
7566
7574
  this.#todoReminderAwaitingProgress = false;
7567
7575
  this.#mutationsSinceLastTodoTouch = 0;
7568
7576
  this.#midRunNudgeCount = 0;
7569
- this.#emptyStopRetryCount = 0;
7570
- this.#unexpectedStopRetryCount = 0;
7571
- // A new prompt cycle starts: drop any sticky yield-termination from the
7572
- // previous run so empty-stop / unexpected-stop / compaction maintenance
7573
- // can evaluate this turn normally.
7574
- this.#yieldTerminationPending = false;
7577
+ this.#resetPromptMaintenanceState();
7575
7578
 
7576
7579
  await this.#maybeRestoreRetryFallbackPrimary();
7577
7580
 
@@ -14120,15 +14123,15 @@ export class AgentSession {
14120
14123
  // =========================================================================
14121
14124
 
14122
14125
  /**
14123
- * Surfaces (and consumes) pending IRC incoming records before the next model
14126
+ * Surfaces and consumes pending IRC incoming records before the next model
14124
14127
  * step can inject them automatically.
14125
14128
  *
14126
- * The inbox tool injects the formatted body into the tool result, so the
14127
- * model sees it once via the result. Leaving the record in either pending
14128
- * IRC queue would deliver it a second time at the next step boundary —
14129
- * including on `peek`, which is why peek also drains here.
14129
+ * Tool results already expose the formatted body to the model. Leaving the
14130
+ * same record in either pending IRC queue would deliver it a second time at
14131
+ * the next step boundary including on `peek`, which is why inbox peeks
14132
+ * also drain here.
14130
14133
  */
14131
- drainPendingIrcInboxMessages(agentId: string): IrcMessage[] {
14134
+ drainPendingIrcInboxMessages(agentId: string, opts?: { from?: string; limit?: number }): IrcMessage[] {
14132
14135
  const messages: IrcMessage[] = [];
14133
14136
  const remainingInterrupts: CustomMessage[] = [];
14134
14137
  const remainingAsides: CustomMessage[] = [];
@@ -14155,6 +14158,14 @@ export class AgentSession {
14155
14158
  queue.remaining.push(record);
14156
14159
  continue;
14157
14160
  }
14161
+ if (opts?.from !== undefined && from !== opts.from) {
14162
+ queue.remaining.push(record);
14163
+ continue;
14164
+ }
14165
+ if (opts?.limit !== undefined && messages.length >= opts.limit) {
14166
+ queue.remaining.push(record);
14167
+ continue;
14168
+ }
14158
14169
  messages.push({
14159
14170
  id,
14160
14171
  from,
@@ -15212,6 +15223,7 @@ export class AgentSession {
15212
15223
  },
15213
15224
  cost: totalCost,
15214
15225
  premiumRequests: totalPremiumRequests,
15226
+ contextUsage: this.getContextUsage(),
15215
15227
  };
15216
15228
  }
15217
15229