@oh-my-pi/pi-coding-agent 15.1.2 → 15.1.3

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.
Files changed (141) hide show
  1. package/CHANGELOG.md +42 -0
  2. package/dist/types/cli/auth-broker-cli.d.ts +25 -0
  3. package/dist/types/cli/auth-gateway-cli.d.ts +18 -0
  4. package/dist/types/cli/grievances-cli.d.ts +12 -0
  5. package/dist/types/commands/auth-broker.d.ts +54 -0
  6. package/dist/types/commands/auth-gateway.d.ts +32 -0
  7. package/dist/types/commands/grievances.d.ts +1 -1
  8. package/dist/types/commit/agentic/tools/propose-commit.d.ts +9 -1
  9. package/dist/types/commit/agentic/tools/schemas.d.ts +9 -1
  10. package/dist/types/commit/agentic/tools/split-commit.d.ts +9 -1
  11. package/dist/types/config/model-registry.d.ts +3 -0
  12. package/dist/types/config/models-config-schema.d.ts +1 -0
  13. package/dist/types/config/settings-schema.d.ts +46 -0
  14. package/dist/types/discovery/agents.d.ts +12 -1
  15. package/dist/types/edit/renderer.d.ts +3 -0
  16. package/dist/types/eval/index.d.ts +0 -2
  17. package/dist/types/goals/tools/goal-tool.d.ts +10 -2
  18. package/dist/types/index.d.ts +0 -1
  19. package/dist/types/internal-urls/index.d.ts +1 -1
  20. package/dist/types/internal-urls/{pi-protocol.d.ts → omp-protocol.d.ts} +3 -3
  21. package/dist/types/internal-urls/types.d.ts +1 -1
  22. package/dist/types/modes/acp/acp-agent.d.ts +1 -0
  23. package/dist/types/modes/emoji-autocomplete.d.ts +16 -0
  24. package/dist/types/modes/interactive-mode.d.ts +1 -1
  25. package/dist/types/modes/prompt-action-autocomplete.d.ts +4 -0
  26. package/dist/types/plan-mode/approved-plan.d.ts +4 -0
  27. package/dist/types/sdk.d.ts +10 -3
  28. package/dist/types/session/agent-session.d.ts +1 -1
  29. package/dist/types/session/auth-broker-config.d.ts +13 -0
  30. package/dist/types/session/auth-storage.d.ts +1 -1
  31. package/dist/types/tools/eval.d.ts +41 -7
  32. package/dist/types/tools/irc.d.ts +8 -2
  33. package/dist/types/tools/report-tool-issue.d.ts +118 -1
  34. package/dist/types/tools/resolve.d.ts +8 -2
  35. package/examples/custom-tools/README.md +3 -12
  36. package/examples/extensions/README.md +2 -15
  37. package/examples/extensions/api-demo.ts +1 -7
  38. package/package.json +7 -7
  39. package/src/autoresearch/tools/init-experiment.ts +11 -33
  40. package/src/autoresearch/tools/log-experiment.ts +10 -24
  41. package/src/autoresearch/tools/run-experiment.ts +1 -1
  42. package/src/autoresearch/tools/update-notes.ts +2 -9
  43. package/src/cli/auth-broker-cli.ts +746 -0
  44. package/src/cli/auth-gateway-cli.ts +342 -0
  45. package/src/cli/grievances-cli.ts +109 -16
  46. package/src/cli.ts +4 -2
  47. package/src/commands/auth-broker.ts +96 -0
  48. package/src/commands/auth-gateway.ts +61 -0
  49. package/src/commands/grievances.ts +13 -8
  50. package/src/commands/launch.ts +1 -1
  51. package/src/commit/agentic/agent.ts +2 -0
  52. package/src/commit/agentic/tools/analyze-file.ts +2 -2
  53. package/src/commit/agentic/tools/git-file-diff.ts +2 -2
  54. package/src/commit/agentic/tools/git-hunk.ts +3 -3
  55. package/src/commit/agentic/tools/git-overview.ts +2 -2
  56. package/src/commit/agentic/tools/propose-changelog.ts +1 -3
  57. package/src/commit/agentic/tools/recent-commits.ts +1 -1
  58. package/src/commit/agentic/tools/schemas.ts +1 -9
  59. package/src/config/model-equivalence.ts +279 -174
  60. package/src/config/model-registry.ts +37 -6
  61. package/src/config/model-resolver.ts +13 -8
  62. package/src/config/models-config-schema.ts +8 -0
  63. package/src/config/settings-schema.ts +52 -0
  64. package/src/cursor.ts +1 -1
  65. package/src/debug/log-formatting.ts +1 -1
  66. package/src/debug/log-viewer.ts +1 -1
  67. package/src/debug/profiler.ts +4 -0
  68. package/src/debug/raw-sse-buffer.ts +100 -59
  69. package/src/debug/raw-sse.ts +1 -1
  70. package/src/discovery/agents.ts +15 -4
  71. package/src/edit/modes/apply-patch.ts +1 -5
  72. package/src/edit/modes/patch.ts +5 -5
  73. package/src/edit/modes/replace.ts +5 -5
  74. package/src/edit/renderer.ts +2 -1
  75. package/src/edit/streaming.ts +1 -1
  76. package/src/eval/index.ts +0 -2
  77. package/src/eval/js/shared/runtime.ts +25 -0
  78. package/src/eval/py/kernel.ts +1 -1
  79. package/src/exa/researcher.ts +4 -4
  80. package/src/exa/search.ts +10 -22
  81. package/src/exa/websets.ts +33 -33
  82. package/src/goals/tools/goal-tool.ts +3 -3
  83. package/src/index.ts +0 -3
  84. package/src/internal-urls/docs-index.generated.ts +21 -18
  85. package/src/internal-urls/index.ts +1 -1
  86. package/src/internal-urls/{pi-protocol.ts → omp-protocol.ts} +10 -10
  87. package/src/internal-urls/router.ts +3 -3
  88. package/src/internal-urls/types.ts +1 -1
  89. package/src/lsp/types.ts +8 -11
  90. package/src/main.ts +3 -0
  91. package/src/mcp/tool-bridge.ts +3 -3
  92. package/src/modes/acp/acp-agent.ts +88 -25
  93. package/src/modes/components/bash-execution.ts +1 -1
  94. package/src/modes/components/diff.ts +1 -2
  95. package/src/modes/components/eval-execution.ts +1 -1
  96. package/src/modes/components/oauth-selector.ts +38 -2
  97. package/src/modes/components/tool-execution.ts +1 -2
  98. package/src/modes/controllers/command-controller.ts +95 -34
  99. package/src/modes/controllers/input-controller.ts +4 -3
  100. package/src/modes/data/emojis.json +1 -0
  101. package/src/modes/emoji-autocomplete.ts +285 -0
  102. package/src/modes/interactive-mode.ts +92 -19
  103. package/src/modes/print-mode.ts +3 -3
  104. package/src/modes/prompt-action-autocomplete.ts +14 -0
  105. package/src/plan-mode/approved-plan.ts +9 -0
  106. package/src/prompts/system/system-prompt.md +1 -1
  107. package/src/prompts/system/ttsr-tool-reminder.md +5 -0
  108. package/src/prompts/tools/eval.md +25 -26
  109. package/src/prompts/tools/read.md +1 -1
  110. package/src/prompts/tools/resolve.md +1 -1
  111. package/src/prompts/tools/search.md +1 -1
  112. package/src/prompts/tools/web-search.md +1 -1
  113. package/src/sdk.ts +78 -7
  114. package/src/session/agent-session.ts +176 -77
  115. package/src/session/agent-storage.ts +7 -2
  116. package/src/session/auth-broker-config.ts +102 -0
  117. package/src/session/auth-storage.ts +7 -1
  118. package/src/session/streaming-output.ts +1 -1
  119. package/src/task/types.ts +10 -35
  120. package/src/tools/bash-interactive.ts +4 -1
  121. package/src/tools/bash-pty-selection.ts +2 -2
  122. package/src/tools/browser.ts +12 -20
  123. package/src/tools/eval.ts +77 -100
  124. package/src/tools/gh.ts +21 -45
  125. package/src/tools/hindsight-recall.ts +1 -1
  126. package/src/tools/hindsight-reflect.ts +2 -2
  127. package/src/tools/hindsight-retain.ts +3 -7
  128. package/src/tools/index.ts +8 -1
  129. package/src/tools/inspect-image.ts +4 -1
  130. package/src/tools/irc.ts +4 -12
  131. package/src/tools/job.ts +3 -11
  132. package/src/tools/report-tool-issue.ts +462 -17
  133. package/src/tools/resolve.ts +2 -7
  134. package/src/tools/todo-write.ts +8 -15
  135. package/src/utils/title-generator.ts +3 -0
  136. package/src/web/search/index.ts +6 -6
  137. package/dist/types/eval/parse.d.ts +0 -28
  138. package/dist/types/eval/sniff.d.ts +0 -11
  139. package/src/eval/eval.lark +0 -36
  140. package/src/eval/parse.ts +0 -407
  141. package/src/eval/sniff.ts +0 -28
@@ -1,4 +1,4 @@
1
- Searches the web for up-to-date information beyond Claude's knowledge cutoff.
1
+ Searches the web for up-to-date information beyond knowledge cutoff.
2
2
 
3
3
  <instruction>
4
4
  - You SHOULD prefer primary sources (papers, official docs) and corroborate key claims with multiple sources
package/src/sdk.ts CHANGED
@@ -7,7 +7,13 @@ import {
7
7
  INTENT_FIELD,
8
8
  type ThinkingLevel,
9
9
  } from "@oh-my-pi/pi-agent-core";
10
- import type { CredentialDisabledEvent, Message, Model, SimpleStreamOptions } from "@oh-my-pi/pi-ai";
10
+ import {
11
+ type CredentialDisabledEvent,
12
+ type Message,
13
+ type Model,
14
+ type SimpleStreamOptions,
15
+ streamSimple,
16
+ } from "@oh-my-pi/pi-ai";
11
17
  import {
12
18
  getOpenAICodexTransportDetails,
13
19
  prewarmOpenAICodexResponses,
@@ -93,7 +99,8 @@ import {
93
99
  SecretObfuscator,
94
100
  } from "./secrets";
95
101
  import { AgentSession } from "./session/agent-session";
96
- import { AuthStorage } from "./session/auth-storage";
102
+ import { resolveAuthBrokerConfig } from "./session/auth-broker-config";
103
+ import { AuthBrokerClient, AuthStorage, RemoteAuthCredentialStore } from "./session/auth-storage";
97
104
  import { convertToLlm } from "./session/messages";
98
105
  import { SessionManager } from "./session/session-manager";
99
106
  import { closeAllConnections } from "./ssh/connection-manager";
@@ -317,13 +324,37 @@ function getDefaultAgentDir(): string {
317
324
  // Discovery Functions
318
325
 
319
326
  /**
320
- * Create an AuthStorage instance with fallback support.
321
- * Reads from primary path first, then falls back to legacy paths (.pi, .claude).
327
+ * Create an AuthStorage instance.
328
+ *
329
+ * Default: local SQLite store at `<agentDir>/agent.db`.
330
+ *
331
+ * Broker mode: when `OMP_AUTH_BROKER_URL` is set, credentials are pulled from
332
+ * a remote auth-broker over the wire. Refresh tokens never leave the broker;
333
+ * the client receives access tokens with `refresh = "__remote__"` and calls
334
+ * back into the broker through the {@link AuthStorageOptions.refreshOAuthCredential}
335
+ * override to re-mint access tokens when needed.
322
336
  */
323
337
  export async function discoverAuthStorage(agentDir: string = getDefaultAgentDir()): Promise<AuthStorage> {
338
+ const brokerConfig = await resolveAuthBrokerConfig();
339
+ if (brokerConfig) {
340
+ const client = new AuthBrokerClient({ url: brokerConfig.url, token: brokerConfig.token });
341
+ const initialResult = await client.fetchSnapshot();
342
+ if (initialResult.status !== 200) throw new Error("Auth broker returned no initial snapshot");
343
+ const store = new RemoteAuthCredentialStore({ client, initialSnapshot: initialResult.snapshot });
344
+ // Refresh + usage hooks live on RemoteAuthCredentialStore; AuthStorage
345
+ // discovers them automatically when no explicit option overrides them.
346
+ const storage = new AuthStorage(store, {
347
+ configValueResolver: resolveConfigValue,
348
+ sourceLabel: `broker ${brokerConfig.url}`,
349
+ });
350
+ await storage.reload();
351
+ return storage;
352
+ }
324
353
  const dbPath = getAgentDbPath(agentDir);
325
-
326
- const storage = await AuthStorage.create(dbPath, { configValueResolver: resolveConfigValue });
354
+ const storage = await AuthStorage.create(dbPath, {
355
+ configValueResolver: resolveConfigValue,
356
+ sourceLabel: `local ${dbPath}`,
357
+ });
327
358
  await storage.reload();
328
359
  return storage;
329
360
  }
@@ -885,6 +916,13 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
885
916
  thinkingLevel = logger.time("resolveThinkingLevelForModel", () =>
886
917
  resolveThinkingLevelForModel(resolvedModel, thinkingLevel),
887
918
  );
919
+ // Fire-and-forget TLS+H2 handshake to the model's host so it overlaps
920
+ // with the rest of session setup (extension/skill load, tool registry,
921
+ // system prompt build). Without this, the first `fetch(...)` pays the
922
+ // full handshake serially — 100–300 ms transcontinental for
923
+ // api.anthropic.com from a residential IP. Every mode benefits
924
+ // (interactive, print, rpc, acp).
925
+ preconnectModelHost(model.baseUrl);
888
926
  }
889
927
 
890
928
  let skills: Skill[];
@@ -1763,6 +1801,18 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
1763
1801
  }
1764
1802
  return key;
1765
1803
  },
1804
+ streamFn: (streamModel, context, streamOptions) =>
1805
+ streamSimple(streamModel, context, {
1806
+ ...streamOptions,
1807
+ onAuthError: async (provider, oldKey, error) => {
1808
+ await modelRegistry.authStorage.invalidateCredentialMatching(provider, oldKey, streamOptions?.signal);
1809
+ logger.debug("Retrying provider request after credential invalidation", {
1810
+ provider,
1811
+ error: error instanceof Error ? error.message : String(error),
1812
+ });
1813
+ return modelRegistry.getApiKeyForProvider(provider, agent.sessionId);
1814
+ },
1815
+ }),
1766
1816
  cursorExecHandlers,
1767
1817
  transformToolCallArguments: (args, _toolName) => {
1768
1818
  let result = args;
@@ -1899,8 +1949,12 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
1899
1949
  }
1900
1950
 
1901
1951
  // Start LSP warmup in the background so startup does not block on language server initialization.
1952
+ // Print/script invocations (`hasUI=false`) don't render the warmup status indicator AND typically
1953
+ // finish before LSP servers would have stabilized — warming them just spends CPU parsing big
1954
+ // `initialize` responses concurrently with the LLM stream consumer, jittering perceived latency.
1955
+ // Tools that need an LSP server still spin one up on demand through `getOrCreateClient`.
1902
1956
  let lspServers: CreateAgentSessionResult["lspServers"];
1903
- if (enableLsp && settings.get("lsp.diagnosticsOnWrite")) {
1957
+ if (enableLsp && options.hasUI && settings.get("lsp.diagnosticsOnWrite")) {
1904
1958
  lspServers = discoverStartupLspServers(cwd);
1905
1959
  if (lspServers.length > 0) {
1906
1960
  void (async () => {
@@ -2017,3 +2071,20 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
2017
2071
  throw error;
2018
2072
  }
2019
2073
  }
2074
+
2075
+ /**
2076
+ * Best-effort preconnect to the model's API host. Bun's `fetch.preconnect`
2077
+ * primes DNS + TCP + TLS + H2 so the first real request reuses the warm
2078
+ * connection. Errors are swallowed: preconnect is an optimization, never a
2079
+ * hard dependency.
2080
+ */
2081
+ function preconnectModelHost(baseUrl: string | undefined): void {
2082
+ if (!baseUrl) return;
2083
+ const preconnect = (globalThis.fetch as typeof fetch & { preconnect?: (url: string) => void }).preconnect;
2084
+ if (typeof preconnect !== "function") return;
2085
+ try {
2086
+ preconnect(baseUrl);
2087
+ } catch {
2088
+ // Best effort.
2089
+ }
2090
+ }
@@ -18,12 +18,15 @@ import * as fs from "node:fs";
18
18
  import * as path from "node:path";
19
19
  import { scheduler } from "node:timers/promises";
20
20
  import {
21
+ type AfterToolCallContext,
22
+ type AfterToolCallResult,
21
23
  type Agent,
22
24
  AgentBusyError,
23
25
  type AgentEvent,
24
26
  type AgentMessage,
25
27
  type AgentState,
26
28
  type AgentTool,
29
+ resolveTelemetry,
27
30
  ThinkingLevel,
28
31
  } from "@oh-my-pi/pi-agent-core";
29
32
  import {
@@ -153,6 +156,7 @@ import planModeToolDecisionReminderPrompt from "../prompts/system/plan-mode-tool
153
156
  type: "text",
154
157
  };
155
158
  import ttsrInterruptTemplate from "../prompts/system/ttsr-interrupt.md" with { type: "text" };
159
+ import ttsrToolReminderTemplate from "../prompts/system/ttsr-tool-reminder.md" with { type: "text" };
156
160
  import { type AgentRegistry, MAIN_AGENT_ID } from "../registry/agent-registry";
157
161
  import { deobfuscateSessionContext, type SecretObfuscator } from "../secrets/obfuscator";
158
162
  import { invalidateHostMetadata } from "../ssh/connection-manager";
@@ -767,6 +771,10 @@ export class AgentSession {
767
771
  // TTSR manager for time-traveling stream rules
768
772
  #ttsrManager: TtsrManager | undefined = undefined;
769
773
  #pendingTtsrInjections: Rule[] = [];
774
+ /** Per-tool TTSR rules whose `interruptMode` opted out of aborting the stream.
775
+ * These are folded into the matched tool call's `toolResult` content as an
776
+ * in-band system reminder, instead of spawning a separate follow-up turn. */
777
+ #perToolTtsrInjections = new Map<string, Rule[]>();
770
778
  #ttsrAbortPending = false;
771
779
  #ttsrRetryToken = 0;
772
780
  #ttsrResumePromise: Promise<void> | undefined = undefined;
@@ -880,16 +888,28 @@ export class AgentSession {
880
888
  this.#transformContext = config.transformContext ?? (messages => messages);
881
889
  this.#onPayload = config.onPayload;
882
890
  this.rawSseDebugBuffer = config.rawSseDebugBuffer ?? new RawSseDebugBuffer();
891
+ // Avoid wrapping in an `async` closure when no user callback is configured: the
892
+ // outer await on `#onResponse` (provider-response.ts) tolerates a sync void return,
893
+ // and skipping the wrapper drops a per-event `newPromiseCapability` allocation that
894
+ // shows up as ~3.5% self time in streaming profiles.
883
895
  const configuredOnResponse = config.onResponse;
884
- this.#onResponse = async (response, model) => {
885
- this.rawSseDebugBuffer.recordResponse(response, model);
886
- await configuredOnResponse?.(response, model);
887
- };
896
+ this.#onResponse = configuredOnResponse
897
+ ? async (response, model) => {
898
+ this.rawSseDebugBuffer.recordResponse(response, model);
899
+ await configuredOnResponse(response, model);
900
+ }
901
+ : (response, model) => {
902
+ this.rawSseDebugBuffer.recordResponse(response, model);
903
+ };
888
904
  const configuredOnSseEvent = config.onSseEvent;
889
- this.#onSseEvent = (event, model) => {
890
- this.rawSseDebugBuffer.recordEvent(event, model);
891
- configuredOnSseEvent?.(event, model);
892
- };
905
+ this.#onSseEvent = configuredOnSseEvent
906
+ ? (event, model) => {
907
+ this.rawSseDebugBuffer.recordEvent(event, model);
908
+ configuredOnSseEvent(event, model);
909
+ }
910
+ : (event, model) => {
911
+ this.rawSseDebugBuffer.recordEvent(event, model);
912
+ };
893
913
  this.agent.setProviderResponseInterceptor(this.#onResponse);
894
914
  this.agent.setRawSseEventInterceptor(this.#onSseEvent);
895
915
  this.#convertToLlm = config.convertToLlm ?? convertToLlm;
@@ -932,6 +952,8 @@ export class AgentSession {
932
952
  this.#preCacheStreamingEditFile(event);
933
953
  this.#maybeAbortStreamingEdit(event);
934
954
  });
955
+ // Per-tool TTSR reminders are folded into the matched tool's result via this hook.
956
+ this.agent.afterToolCall = ctx => this.#ttsrAfterToolCall(ctx);
935
957
  this.agent.providerSessionState = this.#providerSessionState;
936
958
  this.#syncAgentSessionId();
937
959
  this.#syncTodoPhasesFromBranch();
@@ -1325,77 +1347,87 @@ export class AgentSession {
1325
1347
  if (matchContext && "delta" in assistantEvent) {
1326
1348
  const matches = this.#ttsrManager.checkDelta(assistantEvent.delta, matchContext);
1327
1349
  if (matches.length > 0) {
1328
- // Queue rules for injection; mark as injected only after successful enqueue.
1329
-
1330
- this.#addPendingTtsrInjections(matches);
1331
-
1332
- if (this.#shouldInterruptForTtsrMatch(matches, matchContext)) {
1333
- // Abort the stream immediately — do not gate on extension callbacks
1334
- this.#ttsrAbortPending = true;
1335
- this.#ensureTtsrResumePromise();
1336
- this.agent.abort();
1337
- // Notify extensions (fire-and-forget, does not block abort)
1350
+ // Decide first: a non-interrupting tool-source match attaches to the
1351
+ // specific tool call's result instead of driving a loop-wide follow-up.
1352
+ const shouldInterrupt = this.#shouldInterruptForTtsrMatch(matches, matchContext);
1353
+ const perToolId = shouldInterrupt ? undefined : this.#extractTtsrToolCallId(matchContext);
1354
+ if (perToolId) {
1355
+ this.#addPerToolTtsrInjections(perToolId, matches);
1338
1356
  this.#emitSessionEvent({ type: "ttsr_triggered", rules: matches }).catch(() => {});
1339
- // Schedule retry after a short delay
1340
- const retryToken = ++this.#ttsrRetryToken;
1341
- const generation = this.#promptGeneration;
1342
- const targetMessageTimestamp =
1343
- event.message.role === "assistant" ? event.message.timestamp : undefined;
1344
- this.#schedulePostPromptTask(
1345
- async () => {
1346
- if (this.#ttsrRetryToken !== retryToken) {
1347
- this.#resolveTtsrResume();
1348
- return;
1349
- }
1357
+ } else {
1358
+ // Queue rules for injection; mark as injected only after successful enqueue.
1359
+ this.#addPendingTtsrInjections(matches);
1360
+
1361
+ if (shouldInterrupt) {
1362
+ // Abort the stream immediately — do not gate on extension callbacks
1363
+ this.#ttsrAbortPending = true;
1364
+ this.#ensureTtsrResumePromise();
1365
+ this.agent.abort();
1366
+ // Notify extensions (fire-and-forget, does not block abort)
1367
+ this.#emitSessionEvent({ type: "ttsr_triggered", rules: matches }).catch(() => {});
1368
+ // Schedule retry after a short delay
1369
+ const retryToken = ++this.#ttsrRetryToken;
1370
+ const generation = this.#promptGeneration;
1371
+ const targetMessageTimestamp =
1372
+ event.message.role === "assistant" ? event.message.timestamp : undefined;
1373
+ this.#schedulePostPromptTask(
1374
+ async () => {
1375
+ if (this.#ttsrRetryToken !== retryToken) {
1376
+ this.#resolveTtsrResume();
1377
+ return;
1378
+ }
1350
1379
 
1351
- const targetAssistantIndex = this.#findTtsrAssistantIndex(targetMessageTimestamp);
1352
- if (
1353
- !this.#ttsrAbortPending ||
1354
- this.#promptGeneration !== generation ||
1355
- targetAssistantIndex === -1
1356
- ) {
1380
+ const targetAssistantIndex = this.#findTtsrAssistantIndex(targetMessageTimestamp);
1381
+ if (
1382
+ !this.#ttsrAbortPending ||
1383
+ this.#promptGeneration !== generation ||
1384
+ targetAssistantIndex === -1
1385
+ ) {
1386
+ this.#ttsrAbortPending = false;
1387
+ this.#pendingTtsrInjections = [];
1388
+ this.#perToolTtsrInjections.clear();
1389
+ this.#resolveTtsrResume();
1390
+ return;
1391
+ }
1357
1392
  this.#ttsrAbortPending = false;
1358
- this.#pendingTtsrInjections = [];
1359
- this.#resolveTtsrResume();
1360
- return;
1361
- }
1362
- this.#ttsrAbortPending = false;
1363
- const ttsrSettings = this.#ttsrManager?.getSettings();
1364
- if (ttsrSettings?.contextMode === "discard") {
1365
- // Remove the partial/aborted assistant turn from agent state
1366
- this.agent.replaceMessages(this.agent.state.messages.slice(0, targetAssistantIndex));
1367
- }
1368
- // Inject TTSR rules as system reminder before retry
1369
- const injection = this.#getTtsrInjectionContent();
1370
- if (injection) {
1371
- const details = { rules: injection.rules.map(rule => rule.name) };
1372
- this.agent.appendMessage({
1373
- role: "custom",
1374
- customType: "ttsr-injection",
1375
- content: injection.content,
1376
- display: false,
1377
- details,
1378
- attribution: "agent",
1379
- timestamp: Date.now(),
1380
- });
1381
- this.sessionManager.appendCustomMessageEntry(
1382
- "ttsr-injection",
1383
- injection.content,
1384
- false,
1385
- details,
1386
- "agent",
1387
- );
1388
- this.#markTtsrInjected(details.rules);
1389
- }
1390
- try {
1391
- await this.agent.continue();
1392
- } catch {
1393
- this.#resolveTtsrResume();
1394
- }
1395
- },
1396
- { delayMs: 50 },
1397
- );
1398
- return;
1393
+ this.#perToolTtsrInjections.clear();
1394
+ const ttsrSettings = this.#ttsrManager?.getSettings();
1395
+ if (ttsrSettings?.contextMode === "discard") {
1396
+ // Remove the partial/aborted assistant turn from agent state
1397
+ this.agent.replaceMessages(this.agent.state.messages.slice(0, targetAssistantIndex));
1398
+ }
1399
+ // Inject TTSR rules as system reminder before retry
1400
+ const injection = this.#getTtsrInjectionContent();
1401
+ if (injection) {
1402
+ const details = { rules: injection.rules.map(rule => rule.name) };
1403
+ this.agent.appendMessage({
1404
+ role: "custom",
1405
+ customType: "ttsr-injection",
1406
+ content: injection.content,
1407
+ display: false,
1408
+ details,
1409
+ attribution: "agent",
1410
+ timestamp: Date.now(),
1411
+ });
1412
+ this.sessionManager.appendCustomMessageEntry(
1413
+ "ttsr-injection",
1414
+ injection.content,
1415
+ false,
1416
+ details,
1417
+ "agent",
1418
+ );
1419
+ this.#markTtsrInjected(details.rules);
1420
+ }
1421
+ try {
1422
+ await this.agent.continue();
1423
+ } catch {
1424
+ this.#resolveTtsrResume();
1425
+ }
1426
+ },
1427
+ { delayMs: 50 },
1428
+ );
1429
+ return;
1430
+ }
1399
1431
  }
1400
1432
  }
1401
1433
  }
@@ -1804,6 +1836,61 @@ export class AgentSession {
1804
1836
  }
1805
1837
  }
1806
1838
 
1839
+ /** Tool-call id whose argument deltas triggered a TTSR match, when known. */
1840
+ #extractTtsrToolCallId(matchContext: TtsrMatchContext): string | undefined {
1841
+ if (matchContext.source !== "tool") return undefined;
1842
+ const key = matchContext.streamKey;
1843
+ if (typeof key !== "string" || !key.startsWith("toolcall:")) return undefined;
1844
+ const id = key.slice("toolcall:".length);
1845
+ return id.length > 0 ? id : undefined;
1846
+ }
1847
+
1848
+ #addPerToolTtsrInjections(toolCallId: string, rules: Rule[]): void {
1849
+ const bucket = this.#perToolTtsrInjections.get(toolCallId) ?? [];
1850
+ const seen = new Set(bucket.map(rule => rule.name));
1851
+ // Dedupe against rules already bucketed for other tool calls in this
1852
+ // same assistant message so one rule attaches to exactly one tool call.
1853
+ const claimedElsewhere = new Set<string>();
1854
+ for (const [otherId, otherBucket] of this.#perToolTtsrInjections) {
1855
+ if (otherId === toolCallId) continue;
1856
+ for (const rule of otherBucket) claimedElsewhere.add(rule.name);
1857
+ }
1858
+ const newlyAdded: string[] = [];
1859
+ for (const rule of rules) {
1860
+ if (seen.has(rule.name) || claimedElsewhere.has(rule.name)) continue;
1861
+ bucket.push(rule);
1862
+ seen.add(rule.name);
1863
+ newlyAdded.push(rule.name);
1864
+ }
1865
+ if (bucket.length === 0) return;
1866
+ this.#perToolTtsrInjections.set(toolCallId, bucket);
1867
+ // Claim the rules in the TTSR manager so subsequent deltas in this same
1868
+ // turn (e.g. a sibling tool call's argument stream) don't re-match them.
1869
+ // Persistence still happens in #ttsrAfterToolCall when the tool actually
1870
+ // produces a result we can fold the reminder into.
1871
+ if (newlyAdded.length > 0) {
1872
+ this.#ttsrManager?.markInjectedByNames(newlyAdded);
1873
+ }
1874
+ }
1875
+
1876
+ /** `afterToolCall` hook: fold any per-tool TTSR reminders into the result. */
1877
+ #ttsrAfterToolCall(ctx: AfterToolCallContext): AfterToolCallResult | undefined {
1878
+ const rules = this.#perToolTtsrInjections.get(ctx.toolCall.id);
1879
+ if (!rules || rules.length === 0) return undefined;
1880
+ this.#perToolTtsrInjections.delete(ctx.toolCall.id);
1881
+ const reminder = rules
1882
+ .map(r => prompt.render(ttsrToolReminderTemplate, { name: r.name, path: r.path, content: r.content }))
1883
+ .join("\n\n");
1884
+ // The TTSR manager was already claimed at bucket time; only persistence remains.
1885
+ const ruleNames = rules.map(r => r.name.trim()).filter(n => n.length > 0);
1886
+ if (ruleNames.length > 0) {
1887
+ this.sessionManager.appendTtsrInjection(ruleNames);
1888
+ }
1889
+ return {
1890
+ content: [{ type: "text", text: reminder }, ...ctx.result.content],
1891
+ };
1892
+ }
1893
+
1807
1894
  #extractTtsrRuleNames(details: unknown): string[] {
1808
1895
  if (!details || typeof details !== "object" || Array.isArray(details)) {
1809
1896
  return [];
@@ -1854,6 +1941,11 @@ export class AgentSession {
1854
1941
  }
1855
1942
 
1856
1943
  #queueDeferredTtsrInjectionIfNeeded(assistantMsg: AssistantMessage): void {
1944
+ if (assistantMsg.stopReason === "aborted" || assistantMsg.stopReason === "error") {
1945
+ // Tools that hadn't started by abort/error will never produce results to
1946
+ // fold injections into — drop their stale per-tool entries.
1947
+ this.#perToolTtsrInjections.clear();
1948
+ }
1857
1949
  if (this.#ttsrAbortPending || this.#pendingTtsrInjections.length === 0) {
1858
1950
  return;
1859
1951
  }
@@ -5249,6 +5341,7 @@ export class AgentSession {
5249
5341
  convertToLlm,
5250
5342
  initiatorOverride: "agent",
5251
5343
  metadata: this.agent.metadataForProvider(model.provider),
5344
+ telemetry: resolveTelemetry(this.agent.telemetry, this.sessionId),
5252
5345
  },
5253
5346
  handoffSignal,
5254
5347
  );
@@ -5961,6 +6054,7 @@ export class AgentSession {
5961
6054
  options?: SummaryOptions,
5962
6055
  ): Promise<CompactionResult> {
5963
6056
  const candidates = this.#getCompactionModelCandidates(this.#modelRegistry.getAvailable());
6057
+ const telemetry = resolveTelemetry(this.agent.telemetry, this.sessionId);
5964
6058
 
5965
6059
  for (const candidate of candidates) {
5966
6060
  const apiKey = await this.#modelRegistry.getApiKey(candidate, this.sessionId);
@@ -5971,6 +6065,7 @@ export class AgentSession {
5971
6065
  ...options,
5972
6066
  metadata: this.agent.metadataForProvider(candidate.provider),
5973
6067
  convertToLlm,
6068
+ telemetry,
5974
6069
  });
5975
6070
  } catch (error) {
5976
6071
  if (!this.#isCompactionAuthFailure(error)) {
@@ -6207,6 +6302,7 @@ export class AgentSession {
6207
6302
  } else {
6208
6303
  const candidates = this.#getCompactionModelCandidates(availableModels);
6209
6304
  const retrySettings = this.settings.getGroup("retry");
6305
+ const telemetry = resolveTelemetry(this.agent.telemetry, this.sessionId);
6210
6306
  let compactResult: CompactionResult | undefined;
6211
6307
  let lastError: unknown;
6212
6308
 
@@ -6224,6 +6320,7 @@ export class AgentSession {
6224
6320
  metadata: this.agent.metadataForProvider(candidate.provider),
6225
6321
  initiatorOverride: "agent",
6226
6322
  convertToLlm,
6323
+ telemetry,
6227
6324
  });
6228
6325
  break;
6229
6326
  } catch (error) {
@@ -7828,6 +7925,7 @@ export class AgentSession {
7828
7925
  reserveTokens: branchSummarySettings.reserveTokens,
7829
7926
  metadata: this.agent.metadataForProvider(model.provider),
7830
7927
  convertToLlm,
7928
+ telemetry: resolveTelemetry(this.agent.telemetry, this.sessionId),
7831
7929
  });
7832
7930
  this.#branchSummaryAbortController = undefined;
7833
7931
  if (result.aborted) {
@@ -8063,11 +8161,12 @@ export class AgentSession {
8063
8161
  };
8064
8162
  }
8065
8163
 
8066
- async fetchUsageReports(): Promise<UsageReport[] | null> {
8164
+ async fetchUsageReports(signal?: AbortSignal): Promise<UsageReport[] | null> {
8067
8165
  const authStorage = this.#modelRegistry.authStorage;
8068
8166
  if (!authStorage.fetchUsageReports) return null;
8069
8167
  return authStorage.fetchUsageReports({
8070
8168
  baseUrlResolver: provider => this.#modelRegistry.getProviderBaseUrl?.(provider),
8169
+ signal,
8071
8170
  });
8072
8171
  }
8073
8172
 
@@ -1,7 +1,12 @@
1
1
  import { Database, type Statement } from "bun:sqlite";
2
2
  import * as fs from "node:fs";
3
3
  import * as path from "node:path";
4
- import { type AuthCredential, AuthCredentialStore, type StoredAuthCredential } from "@oh-my-pi/pi-ai";
4
+ import {
5
+ type AuthCredential,
6
+ type AuthCredentialStore,
7
+ SqliteAuthCredentialStore,
8
+ type StoredAuthCredential,
9
+ } from "@oh-my-pi/pi-ai";
5
10
  import { getAgentDbPath, isRecord, logger } from "@oh-my-pi/pi-utils";
6
11
  import type { RawSettings as Settings } from "../config/settings";
7
12
 
@@ -57,7 +62,7 @@ export class AgentStorage {
57
62
  this.#hardenPermissions(dbPath);
58
63
 
59
64
  // Create AuthCredentialStore with our open database
60
- this.#authStore = new AuthCredentialStore(this.#db);
65
+ this.#authStore = new SqliteAuthCredentialStore(this.#db);
61
66
 
62
67
  this.#listSettingsStmt = this.#db.prepare("SELECT key, value FROM settings");
63
68
  this.#upsertModelUsageStmt = this.#db.prepare(
@@ -0,0 +1,102 @@
1
+ /**
2
+ * Resolve auth-broker connection configuration for the local omp client.
3
+ *
4
+ * Precedence (highest first):
5
+ * 1. `OMP_AUTH_BROKER_URL` / `OMP_AUTH_BROKER_TOKEN` env vars.
6
+ * 2. `auth.broker.url` / `auth.broker.token` in `~/.omp/agent/config.yml`
7
+ * (hidden from the settings UI; `!command` resolution supported).
8
+ * 3. Token file `~/.omp/auth-broker.token` (paired with URL from env or config).
9
+ *
10
+ * Returns null when no broker URL is configured — caller falls back to the
11
+ * local SQLite store.
12
+ *
13
+ * Reads config.yml directly (instead of going through `Settings.init`) because
14
+ * `discoverAuthStorage` runs before the settings singleton is initialized in
15
+ * `runRootCommand`, and we want hand-edited config entries to be honoured at
16
+ * boot without forcing a startup reorder.
17
+ */
18
+ import * as path from "node:path";
19
+ import { getAgentDir, getConfigRootDir, isEnoent, logger } from "@oh-my-pi/pi-utils";
20
+ import { YAML } from "bun";
21
+ import { resolveConfigValue } from "../config/resolve-config-value";
22
+
23
+ export interface AuthBrokerClientConfig {
24
+ url: string;
25
+ token: string;
26
+ }
27
+
28
+ /** Path to the local bearer token file. Created on the broker host by `omp auth-broker token`. */
29
+ export function getAuthBrokerTokenFilePath(): string {
30
+ return path.join(getConfigRootDir(), "auth-broker.token");
31
+ }
32
+
33
+ async function readTokenFile(): Promise<string | null> {
34
+ try {
35
+ const raw = await Bun.file(getAuthBrokerTokenFilePath()).text();
36
+ const trimmed = raw.trim();
37
+ return trimmed.length > 0 ? trimmed : null;
38
+ } catch (err) {
39
+ if (isEnoent(err)) return null;
40
+ logger.warn("auth-broker token file unreadable", { error: String(err) });
41
+ return null;
42
+ }
43
+ }
44
+
45
+ interface ConfigSnapshot {
46
+ url?: string;
47
+ token?: string;
48
+ }
49
+
50
+ async function readConfigYaml(): Promise<ConfigSnapshot> {
51
+ const configPath = path.join(getAgentDir(), "config.yml");
52
+ try {
53
+ const raw = await Bun.file(configPath).text();
54
+ const parsed = YAML.parse(raw);
55
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return {};
56
+ const record = parsed as Record<string, unknown>;
57
+ const url = typeof record["auth.broker.url"] === "string" ? (record["auth.broker.url"] as string) : undefined;
58
+ const token =
59
+ typeof record["auth.broker.token"] === "string" ? (record["auth.broker.token"] as string) : undefined;
60
+ return { url, token };
61
+ } catch (err) {
62
+ if (isEnoent(err)) return {};
63
+ logger.warn("auth-broker config.yml unreadable", { error: String(err) });
64
+ return {};
65
+ }
66
+ }
67
+
68
+ /**
69
+ * Read broker configuration. Returns null when the URL is missing
70
+ * (broker disabled — local store is used). Throws when URL is set but no
71
+ * token is available — the caller cannot fall back silently because the
72
+ * user explicitly asked to use the broker.
73
+ */
74
+ export async function resolveAuthBrokerConfig(): Promise<AuthBrokerClientConfig | null> {
75
+ const envUrl = process.env.OMP_AUTH_BROKER_URL;
76
+ const envToken = process.env.OMP_AUTH_BROKER_TOKEN;
77
+
78
+ let url = envUrl && envUrl.length > 0 ? envUrl : undefined;
79
+ let configToken: string | undefined;
80
+ if (!url || !envToken) {
81
+ const fromConfig = await readConfigYaml();
82
+ if (!url && fromConfig.url) {
83
+ const resolved = await resolveConfigValue(fromConfig.url);
84
+ if (resolved && resolved.length > 0) url = resolved;
85
+ }
86
+ if (fromConfig.token) {
87
+ const resolved = await resolveConfigValue(fromConfig.token);
88
+ if (resolved && resolved.length > 0) configToken = resolved;
89
+ }
90
+ }
91
+ if (!url) return null;
92
+
93
+ const token =
94
+ (envToken && envToken.length > 0 ? envToken : undefined) ?? configToken ?? (await readTokenFile()) ?? undefined;
95
+ if (!token) {
96
+ throw new Error(
97
+ `OMP_AUTH_BROKER_URL is set (${url}) but no bearer token is available. ` +
98
+ `Set OMP_AUTH_BROKER_TOKEN, the \`auth.broker.token\` config entry, or place one at ${getAuthBrokerTokenFilePath()}.`,
99
+ );
100
+ }
101
+ return { url, token };
102
+ }
@@ -14,4 +14,10 @@ export type {
14
14
  SerializedAuthStorage,
15
15
  StoredAuthCredential,
16
16
  } from "@oh-my-pi/pi-ai";
17
- export { AuthStorage } from "@oh-my-pi/pi-ai";
17
+ export {
18
+ AuthBrokerClient,
19
+ AuthStorage,
20
+ REMOTE_REFRESH_SENTINEL,
21
+ RemoteAuthCredentialStore,
22
+ SqliteAuthCredentialStore,
23
+ } from "@oh-my-pi/pi-ai";
@@ -1,5 +1,5 @@
1
1
  import type { AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
2
- import { sanitizeText } from "@oh-my-pi/pi-natives";
2
+ import { sanitizeText } from "@oh-my-pi/pi-utils";
3
3
  import { formatBytes } from "../tools/render-utils";
4
4
  import { sanitizeWithOptionalSixelPassthrough } from "../utils/sixel";
5
5