@oh-my-pi/pi-coding-agent 13.11.1 → 13.12.0

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 (53) hide show
  1. package/CHANGELOG.md +58 -0
  2. package/package.json +7 -7
  3. package/src/capability/rule.ts +4 -0
  4. package/src/cli/commands/init-xdg.ts +27 -0
  5. package/src/cli/config-cli.ts +8 -3
  6. package/src/cli/shell-cli.ts +1 -1
  7. package/src/commands/config.ts +1 -1
  8. package/src/config/model-registry.ts +63 -10
  9. package/src/config/model-resolver.ts +84 -21
  10. package/src/config/settings-schema.ts +803 -637
  11. package/src/discovery/helpers.ts +8 -2
  12. package/src/exec/bash-executor.ts +62 -25
  13. package/src/extensibility/custom-tools/types.ts +2 -3
  14. package/src/extensibility/extensions/types.ts +2 -0
  15. package/src/extensibility/hooks/types.ts +2 -0
  16. package/src/index.ts +6 -6
  17. package/src/internal-urls/docs-index.generated.ts +2 -2
  18. package/src/memories/index.ts +20 -7
  19. package/src/memories/storage.ts +46 -32
  20. package/src/modes/components/agent-dashboard.ts +23 -35
  21. package/src/modes/components/assistant-message.ts +25 -2
  22. package/src/modes/components/btw-panel.ts +104 -0
  23. package/src/modes/components/settings-defs.ts +1 -1
  24. package/src/modes/components/settings-selector.ts +6 -6
  25. package/src/modes/controllers/btw-controller.ts +193 -0
  26. package/src/modes/controllers/command-controller.ts +1 -1
  27. package/src/modes/controllers/event-controller.ts +4 -0
  28. package/src/modes/controllers/input-controller.ts +10 -1
  29. package/src/modes/interactive-mode.ts +22 -0
  30. package/src/modes/prompt-action-autocomplete.ts +17 -3
  31. package/src/modes/rpc/rpc-client.ts +30 -19
  32. package/src/modes/theme/theme.ts +28 -36
  33. package/src/modes/types.ts +4 -0
  34. package/src/modes/utils/ui-helpers.ts +3 -0
  35. package/src/prompts/system/btw-user.md +8 -0
  36. package/src/prompts/system/custom-system-prompt.md +1 -1
  37. package/src/prompts/system/system-prompt.md +1 -0
  38. package/src/sdk.ts +17 -25
  39. package/src/session/agent-session.ts +65 -37
  40. package/src/session/blob-store.ts +32 -0
  41. package/src/session/compaction/compaction.ts +27 -6
  42. package/src/session/history-storage.ts +2 -2
  43. package/src/session/session-manager.ts +116 -44
  44. package/src/slash-commands/builtin-registry.ts +11 -0
  45. package/src/system-prompt.ts +4 -17
  46. package/src/task/agents.ts +1 -1
  47. package/src/task/index.ts +9 -8
  48. package/src/tools/browser.ts +11 -0
  49. package/src/tools/output-meta.ts +96 -3
  50. package/src/utils/title-generator.ts +70 -92
  51. package/src/utils/tools-manager.ts +1 -1
  52. package/src/web/scrapers/index.ts +7 -7
  53. package/src/web/scrapers/utils.ts +1 -0
@@ -297,6 +297,7 @@ When a tool call fails, read the full error before doing anything else. When a f
297
297
  - You **MUST** exhaust tools/context/files first — explore.
298
298
  ## 7. Verification
299
299
  - Test everything rigorously → Future contributor cannot break behavior without failure. Prefer unit/e2e.
300
+ - You **MUST NOT** rely on mocks — they invent behaviors that never happen in production and hide real bugs.
300
301
  - You **SHOULD** run only tests you added/modified unless asked otherwise.
301
302
  - You **MUST NOT** yield without proof when non-trivial work, self-assessment is deceptive: tests, linters, type checks, repro steps… exhaust all external verification.
302
303
 
package/src/sdk.ts CHANGED
@@ -227,17 +227,17 @@ export {
227
227
  BashTool,
228
228
  // Tool classes and factories
229
229
  BUILTIN_TOOLS,
230
- HIDDEN_TOOLS,
231
230
  createTools,
232
231
  EditTool,
233
232
  FindTool,
234
233
  GrepTool,
234
+ HIDDEN_TOOLS,
235
235
  loadSshTool,
236
236
  PythonTool,
237
237
  ReadTool,
238
238
  ResolveTool,
239
- WriteTool,
240
239
  type ToolSession,
240
+ WriteTool,
241
241
  };
242
242
 
243
243
  // Helper Functions
@@ -702,19 +702,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
702
702
  }
703
703
  }
704
704
 
705
- // For subagent sessions using GitHub Copilot, add X-Initiator header
706
- // to ensure proper billing (agent-initiated vs user-initiated)
707
705
  const taskDepth = options.taskDepth ?? 0;
708
- const forceCopilotAgentInitiator = taskDepth > 0;
709
- if (forceCopilotAgentInitiator && model?.provider === "github-copilot") {
710
- model = {
711
- ...model,
712
- headers: {
713
- ...model.headers,
714
- "X-Initiator": "agent",
715
- },
716
- };
717
- }
718
706
 
719
707
  let thinkingLevel = options.thinkingLevel;
720
708
 
@@ -1340,6 +1328,16 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
1340
1328
  if (!obfuscator?.hasSecrets()) return converted;
1341
1329
  return obfuscateMessages(obfuscator, converted);
1342
1330
  };
1331
+ const transformContext = extensionRunner
1332
+ ? async (messages: AgentMessage[], _signal?: AbortSignal) => {
1333
+ return await extensionRunner.emitContext(messages);
1334
+ }
1335
+ : undefined;
1336
+ const onPayload = extensionRunner
1337
+ ? async (payload: unknown, _model?: Model) => {
1338
+ return await extensionRunner.emitBeforeProviderRequest(payload);
1339
+ }
1340
+ : undefined;
1343
1341
 
1344
1342
  const setToolUIContext = (uiContext: ExtensionUIContext, hasUI: boolean) => {
1345
1343
  toolContextStore.setUIContext(uiContext, hasUI);
@@ -1362,17 +1360,9 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
1362
1360
  tools: initialTools,
1363
1361
  },
1364
1362
  convertToLlm: convertToLlmFinal,
1365
- onPayload: extensionRunner
1366
- ? async (payload, _model) => {
1367
- return extensionRunner.emitBeforeProviderRequest(payload);
1368
- }
1369
- : undefined,
1363
+ onPayload,
1370
1364
  sessionId: sessionManager.getSessionId(),
1371
- transformContext: extensionRunner
1372
- ? async messages => {
1373
- return extensionRunner.emitContext(messages);
1374
- }
1375
- : undefined,
1365
+ transformContext,
1376
1366
  steeringMode: settings.get("steeringMode") ?? "one-at-a-time",
1377
1367
  followUpMode: settings.get("followUpMode") ?? "one-at-a-time",
1378
1368
  interruptMode: settings.get("interruptMode") ?? "immediate",
@@ -1447,9 +1437,11 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
1447
1437
  skillsSettings: settings.getGroup("skills") as Required<SkillsSettings>,
1448
1438
  modelRegistry,
1449
1439
  toolRegistry,
1440
+ transformContext,
1441
+ onPayload,
1442
+ convertToLlm: convertToLlmFinal,
1450
1443
  rebuildSystemPrompt,
1451
1444
  ttsrManager,
1452
- forceCopilotAgentInitiator,
1453
1445
  obfuscator,
1454
1446
  asyncJobManager,
1455
1447
  pendingActionStore,
@@ -34,6 +34,7 @@ import type {
34
34
  Model,
35
35
  ProviderSessionState,
36
36
  ServiceTier,
37
+ SimpleStreamOptions,
37
38
  TextContent,
38
39
  ToolCall,
39
40
  ToolChoice,
@@ -104,6 +105,7 @@ import { outputMeta } from "../tools/output-meta";
104
105
  import { resolveToCwd } from "../tools/path-utils";
105
106
  import type { PendingActionStore } from "../tools/pending-action";
106
107
  import { getLatestTodoPhasesFromEntries, type TodoItem, type TodoPhase } from "../tools/todo-write";
108
+ import { clampTimeout } from "../tools/tool-timeouts";
107
109
  import { parseCommandArgs } from "../utils/command-args";
108
110
  import { resolveFileDisplayMode } from "../utils/file-display-mode";
109
111
  import { extractFileMentions, generateFileMentionMessages } from "../utils/file-mentions";
@@ -126,6 +128,7 @@ import {
126
128
  bashExecutionToText,
127
129
  type CompactionSummaryMessage,
128
130
  type CustomMessage,
131
+ convertToLlm,
129
132
  type FileMentionMessage,
130
133
  type HookMessage,
131
134
  type PythonExecutionMessage,
@@ -145,6 +148,8 @@ export type AgentSessionEvent =
145
148
  aborted: boolean;
146
149
  willRetry: boolean;
147
150
  errorMessage?: string;
151
+ /** True when compaction was skipped for a benign reason (no model, no candidates, nothing to compact). */
152
+ skipped?: boolean;
148
153
  }
149
154
  | { type: "auto_retry_start"; attempt: number; maxAttempts: number; delayMs: number; errorMessage: string }
150
155
  | { type: "auto_retry_end"; success: boolean; attempt: number; finalError?: string }
@@ -191,12 +196,16 @@ export interface AgentSessionConfig {
191
196
  modelRegistry: ModelRegistry;
192
197
  /** Tool registry for LSP and settings */
193
198
  toolRegistry?: Map<string, AgentTool>;
199
+ /** Current session pre-LLM message transform pipeline */
200
+ transformContext?: (messages: AgentMessage[], signal?: AbortSignal) => AgentMessage[] | Promise<AgentMessage[]>;
201
+ /** Provider payload hook used by the active session request path */
202
+ onPayload?: SimpleStreamOptions["onPayload"];
203
+ /** Current session message-to-LLM conversion pipeline */
204
+ convertToLlm?: (messages: AgentMessage[]) => Message[] | Promise<Message[]>;
194
205
  /** System prompt builder that can consider tool availability */
195
206
  rebuildSystemPrompt?: (toolNames: string[], tools: Map<string, AgentTool>) => Promise<string>;
196
207
  /** TTSR manager for time-traveling stream rules */
197
208
  ttsrManager?: TtsrManager;
198
- /** Force X-Initiator: agent for GitHub Copilot model selections in this session. */
199
- forceCopilotAgentInitiator?: boolean;
200
209
  /** Secret obfuscator for deobfuscating streaming edit content */
201
210
  obfuscator?: SecretObfuscator;
202
211
  /** Pending action store for preview/apply workflows */
@@ -381,9 +390,11 @@ export class AgentSession {
381
390
 
382
391
  // Tool registry and prompt builder for extensions
383
392
  #toolRegistry: Map<string, AgentTool>;
393
+ #transformContext: (messages: AgentMessage[], signal?: AbortSignal) => AgentMessage[] | Promise<AgentMessage[]>;
394
+ #onPayload: SimpleStreamOptions["onPayload"] | undefined;
395
+ #convertToLlm: (messages: AgentMessage[]) => Message[] | Promise<Message[]>;
384
396
  #rebuildSystemPrompt: ((toolNames: string[], tools: Map<string, AgentTool>) => Promise<string>) | undefined;
385
397
  #baseSystemPrompt: string;
386
- #forceCopilotAgentInitiator = false;
387
398
 
388
399
  // TTSR manager for time-traveling stream rules
389
400
  #ttsrManager: TtsrManager | undefined = undefined;
@@ -425,10 +436,12 @@ export class AgentSession {
425
436
  this.#skillsSettings = config.skillsSettings;
426
437
  this.#modelRegistry = config.modelRegistry;
427
438
  this.#toolRegistry = config.toolRegistry ?? new Map();
439
+ this.#transformContext = config.transformContext ?? (messages => messages);
440
+ this.#onPayload = config.onPayload;
441
+ this.#convertToLlm = config.convertToLlm ?? convertToLlm;
428
442
  this.#rebuildSystemPrompt = config.rebuildSystemPrompt;
429
443
  this.#baseSystemPrompt = this.agent.state.systemPrompt;
430
444
  this.#ttsrManager = config.ttsrManager;
431
- this.#forceCopilotAgentInitiator = config.forceCopilotAgentInitiator ?? false;
432
445
  this.#obfuscator = config.obfuscator;
433
446
  this.agent.providerSessionState = this.#providerSessionState;
434
447
  this.#pendingActionStore = config.pendingActionStore;
@@ -580,7 +593,7 @@ export class AgentSession {
580
593
 
581
594
  this.#addPendingTtsrInjections(matches);
582
595
 
583
- if (this.#shouldInterruptForTtsrMatch(matchContext)) {
596
+ if (this.#shouldInterruptForTtsrMatch(matches, matchContext)) {
584
597
  // Abort the stream immediately — do not gate on extension callbacks
585
598
  this.#ttsrAbortPending = true;
586
599
  this.#ensureTtsrResumePromise();
@@ -1014,18 +1027,17 @@ export class AgentSession {
1014
1027
  return -1;
1015
1028
  }
1016
1029
 
1017
- #shouldInterruptForTtsrMatch(matchContext: TtsrMatchContext): boolean {
1018
- const mode = this.#ttsrManager?.getSettings().interruptMode ?? "always";
1019
- if (mode === "never") {
1020
- return false;
1021
- }
1022
- if (mode === "prose-only") {
1023
- return matchContext.source === "text" || matchContext.source === "thinking";
1024
- }
1025
- if (mode === "tool-only") {
1026
- return matchContext.source === "tool";
1030
+ #shouldInterruptForTtsrMatch(matches: Rule[], matchContext: TtsrMatchContext): boolean {
1031
+ const globalMode = this.#ttsrManager?.getSettings().interruptMode ?? "always";
1032
+ for (const rule of matches) {
1033
+ const mode = rule.interruptMode ?? globalMode;
1034
+ if (mode === "never") continue;
1035
+ if (mode === "prose-only" && (matchContext.source === "text" || matchContext.source === "thinking"))
1036
+ return true;
1037
+ if (mode === "tool-only" && matchContext.source === "tool") return true;
1038
+ if (mode === "always") return true;
1027
1039
  }
1028
- return true;
1040
+ return false;
1029
1041
  }
1030
1042
 
1031
1043
  #queueDeferredTtsrInjectionIfNeeded(assistantMsg: AssistantMessage): void {
@@ -1442,6 +1454,7 @@ export class AgentSession {
1442
1454
  aborted: event.aborted,
1443
1455
  willRetry: event.willRetry,
1444
1456
  errorMessage: event.errorMessage,
1457
+ skipped: event.skipped,
1445
1458
  });
1446
1459
  } else if (event.type === "auto_retry_start") {
1447
1460
  await this.#extensionRunner.emit({
@@ -1526,7 +1539,7 @@ export class AgentSession {
1526
1539
  if (drained === false && deliveryState) {
1527
1540
  logger.warn("Async job completion deliveries still pending during dispose", { ...deliveryState });
1528
1541
  }
1529
- await this.sessionManager.flush();
1542
+ await this.sessionManager.close();
1530
1543
  for (const state of this.#providerSessionState.values()) {
1531
1544
  state.close();
1532
1545
  }
@@ -1551,19 +1564,6 @@ export class AgentSession {
1551
1564
  return this.agent.state.model;
1552
1565
  }
1553
1566
 
1554
- #applySessionModelOverrides(model: Model): Model {
1555
- if (!this.#forceCopilotAgentInitiator || model.provider !== "github-copilot") {
1556
- return model;
1557
- }
1558
- return {
1559
- ...model,
1560
- headers: {
1561
- ...model.headers,
1562
- "X-Initiator": "agent",
1563
- },
1564
- };
1565
- }
1566
-
1567
1567
  /** Current thinking level */
1568
1568
  get thinkingLevel(): ThinkingLevel | undefined {
1569
1569
  return this.#thinkingLevel;
@@ -1714,6 +1714,31 @@ export class AgentSession {
1714
1714
  return this.agent.state.messages;
1715
1715
  }
1716
1716
 
1717
+ /** Convert session messages using the same pre-LLM pipeline as the active session. */
1718
+ async convertMessagesToLlm(messages: AgentMessage[], signal?: AbortSignal): Promise<Message[]> {
1719
+ const transformedMessages = await this.#transformContext(messages, signal);
1720
+ return await this.#convertToLlm(transformedMessages);
1721
+ }
1722
+
1723
+ /** Apply session-level stream hooks to a direct side request. */
1724
+ prepareSimpleStreamOptions(options: SimpleStreamOptions): SimpleStreamOptions {
1725
+ if (!this.#onPayload) return options;
1726
+ if (!options.onPayload) {
1727
+ return { ...options, onPayload: this.#onPayload };
1728
+ }
1729
+ const sessionOnPayload = this.#onPayload;
1730
+ const requestOnPayload = options.onPayload;
1731
+ return {
1732
+ ...options,
1733
+ onPayload: async (payload, model) => {
1734
+ const sessionPayload = await sessionOnPayload(payload, model);
1735
+ const sessionResolvedPayload = sessionPayload ?? payload;
1736
+ const requestPayload = await requestOnPayload(sessionResolvedPayload, model);
1737
+ return requestPayload ?? sessionResolvedPayload;
1738
+ },
1739
+ };
1740
+ }
1741
+
1717
1742
  /** Current steering mode */
1718
1743
  get steeringMode(): "all" | "one-at-a-time" {
1719
1744
  return this.agent.getSteeringMode();
@@ -2905,14 +2930,12 @@ export class AgentSession {
2905
2930
  * Cycle to next thinking level.
2906
2931
  * @returns New level, or undefined if model doesn't support thinking
2907
2932
  */
2908
- cycleThinkingLevel(): Effort | undefined {
2933
+ cycleThinkingLevel(): ThinkingLevel | undefined {
2909
2934
  if (!this.model?.reasoning) return undefined;
2910
2935
 
2911
- const levels = this.getAvailableThinkingLevels();
2912
- const currentIndex =
2913
- this.thinkingLevel && this.thinkingLevel !== ThinkingLevel.Off && this.thinkingLevel !== ThinkingLevel.Inherit
2914
- ? levels.indexOf(this.thinkingLevel)
2915
- : -1;
2936
+ const levels = [ThinkingLevel.Off, ...this.getAvailableThinkingLevels()];
2937
+ const currentLevel = this.thinkingLevel === ThinkingLevel.Inherit ? ThinkingLevel.Off : this.thinkingLevel;
2938
+ const currentIndex = currentLevel ? levels.indexOf(currentLevel) : -1;
2916
2939
  const nextIndex = (currentIndex + 1) % levels.length;
2917
2940
  const nextLevel = levels[nextIndex];
2918
2941
  if (!nextLevel) return undefined;
@@ -3685,7 +3708,7 @@ export class AgentSession {
3685
3708
  if (currentModel) {
3686
3709
  this.#closeProviderSessionsForModelSwitch(currentModel, model);
3687
3710
  }
3688
- this.agent.setModel(this.#applySessionModelOverrides(model));
3711
+ this.agent.setModel(model);
3689
3712
  }
3690
3713
 
3691
3714
  #closeCodexProviderSessionsForHistoryRewrite(): void {
@@ -3853,6 +3876,7 @@ export class AgentSession {
3853
3876
  result: undefined,
3854
3877
  aborted: false,
3855
3878
  willRetry: false,
3879
+ skipped: true,
3856
3880
  });
3857
3881
  return;
3858
3882
  }
@@ -3865,6 +3889,7 @@ export class AgentSession {
3865
3889
  result: undefined,
3866
3890
  aborted: false,
3867
3891
  willRetry: false,
3892
+ skipped: true,
3868
3893
  });
3869
3894
  return;
3870
3895
  }
@@ -3879,6 +3904,7 @@ export class AgentSession {
3879
3904
  result: undefined,
3880
3905
  aborted: false,
3881
3906
  willRetry: false,
3907
+ skipped: true,
3882
3908
  });
3883
3909
  if (!willRetry && this.agent.hasQueuedMessages()) {
3884
3910
  this.#scheduleAgentContinue({
@@ -3966,6 +3992,7 @@ export class AgentSession {
3966
3992
  promptOverride: hookPrompt,
3967
3993
  extraContext: hookContext,
3968
3994
  remoteInstructions: this.#baseSystemPrompt,
3995
+ initiatorOverride: "agent",
3969
3996
  });
3970
3997
  break;
3971
3998
  } catch (error) {
@@ -4429,6 +4456,7 @@ export class AgentSession {
4429
4456
  onChunk,
4430
4457
  signal: this.#bashAbortController.signal,
4431
4458
  sessionKey: this.sessionId,
4459
+ timeout: clampTimeout("bash") * 1000,
4432
4460
  });
4433
4461
 
4434
4462
  this.recordBashResult(command, result, options);
@@ -74,6 +74,21 @@ export function parseBlobRef(data: string): string | null {
74
74
  return data.slice(BLOB_PREFIX.length);
75
75
  }
76
76
 
77
+ /** Identify provider transport image data URLs so persistence can externalize and restore them losslessly. */
78
+ export function isImageDataUrl(data: string): boolean {
79
+ return data.startsWith("data:image/") && data.includes(";base64,");
80
+ }
81
+
82
+ /**
83
+ * Externalize a provider image data URL to the blob store, returning a blob reference.
84
+ * The full data URL string is preserved so transport-native history can be reconstructed on resume.
85
+ */
86
+ export async function externalizeImageDataUrl(blobStore: BlobStore, dataUrl: string): Promise<string> {
87
+ if (isBlobRef(dataUrl)) return dataUrl;
88
+ const { ref } = await blobStore.put(Buffer.from(dataUrl, "utf8"));
89
+ return ref;
90
+ }
91
+
77
92
  /**
78
93
  * Externalize an image's base64 data to the blob store, returning a blob reference.
79
94
  * If the data is already a blob reference, returns it unchanged.
@@ -85,6 +100,23 @@ export async function externalizeImageData(blobStore: BlobStore, base64Data: str
85
100
  return ref;
86
101
  }
87
102
 
103
+ /**
104
+ * Resolve an externalized provider image data URL back to its original string.
105
+ * If the data is not a blob reference, returns it unchanged.
106
+ * If the blob is missing, logs a warning and returns the reference as-is.
107
+ */
108
+ export async function resolveImageDataUrl(blobStore: BlobStore, data: string): Promise<string> {
109
+ const hash = parseBlobRef(data);
110
+ if (!hash) return data;
111
+
112
+ const buffer = await blobStore.get(hash);
113
+ if (!buffer) {
114
+ logger.warn("Blob not found for persisted image data URL", { hash });
115
+ return data;
116
+ }
117
+ return buffer.toString("utf8");
118
+ }
119
+
88
120
  /**
89
121
  * Resolve a blob reference back to base64 data.
90
122
  * If the data is not a blob reference, returns it unchanged.
@@ -5,7 +5,14 @@
5
5
  * and after compaction the session is reloaded.
6
6
  */
7
7
  import type { AgentMessage } from "@oh-my-pi/pi-agent-core";
8
- import { type AssistantMessage, completeSimple, Effort, type Model, type Usage } from "@oh-my-pi/pi-ai";
8
+ import {
9
+ type AssistantMessage,
10
+ completeSimple,
11
+ Effort,
12
+ type MessageAttribution,
13
+ type Model,
14
+ type Usage,
15
+ } from "@oh-my-pi/pi-ai";
9
16
  import {
10
17
  CODEX_BASE_URL,
11
18
  getCodexAccountId,
@@ -935,6 +942,7 @@ export interface SummaryOptions {
935
942
  extraContext?: string[];
936
943
  remoteEndpoint?: string;
937
944
  remoteInstructions?: string;
945
+ initiatorOverride?: MessageAttribution;
938
946
  }
939
947
 
940
948
  export async function generateSummary(
@@ -990,7 +998,7 @@ export async function generateSummary(
990
998
  const response = await completeSimple(
991
999
  model,
992
1000
  { systemPrompt: SUMMARIZATION_SYSTEM_PROMPT, messages: summarizationMessages },
993
- { maxTokens, signal, apiKey, reasoning: Effort.High },
1001
+ { maxTokens, signal, apiKey, reasoning: Effort.High, initiatorOverride: options?.initiatorOverride },
994
1002
  );
995
1003
 
996
1004
  if (response.stopReason === "error") {
@@ -1039,7 +1047,7 @@ async function generateShortSummary(
1039
1047
  systemPrompt: SUMMARIZATION_SYSTEM_PROMPT,
1040
1048
  messages: [{ role: "user", content: [{ type: "text", text: promptText }], timestamp: Date.now() }],
1041
1049
  },
1042
- { maxTokens, signal, apiKey, reasoning: Effort.High },
1050
+ { maxTokens, signal, apiKey, reasoning: Effort.High, initiatorOverride: options?.initiatorOverride },
1043
1051
  );
1044
1052
 
1045
1053
  if (response.stopReason === "error") {
@@ -1218,6 +1226,7 @@ export async function compact(
1218
1226
  extraContext: options?.extraContext,
1219
1227
  remoteEndpoint: settings.remoteEnabled === false ? undefined : settings.remoteEndpoint,
1220
1228
  remoteInstructions: options?.remoteInstructions,
1229
+ initiatorOverride: options?.initiatorOverride,
1221
1230
  };
1222
1231
 
1223
1232
  let preserveData = withOpenAiRemoteCompactionPreserveData(previousPreserveData, undefined);
@@ -1266,7 +1275,14 @@ export async function compact(
1266
1275
  summaryOptions,
1267
1276
  )
1268
1277
  : Promise.resolve("No prior history."),
1269
- generateTurnPrefixSummary(turnPrefixMessages, model, settings.reserveTokens, apiKey, signal),
1278
+ generateTurnPrefixSummary(
1279
+ turnPrefixMessages,
1280
+ model,
1281
+ settings.reserveTokens,
1282
+ apiKey,
1283
+ signal,
1284
+ summaryOptions.initiatorOverride,
1285
+ ),
1270
1286
  ]);
1271
1287
  // Merge into single summary
1272
1288
  summary = `${historyResult}\n\n---\n\n**Turn Context (split turn):**\n\n${turnPrefixResult}`;
@@ -1297,7 +1313,11 @@ export async function compact(
1297
1313
  settings.reserveTokens,
1298
1314
  apiKey,
1299
1315
  signal,
1300
- { extraContext: options?.extraContext, remoteEndpoint: summaryOptions.remoteEndpoint },
1316
+ {
1317
+ extraContext: options?.extraContext,
1318
+ remoteEndpoint: summaryOptions.remoteEndpoint,
1319
+ initiatorOverride: summaryOptions.initiatorOverride,
1320
+ },
1301
1321
  );
1302
1322
 
1303
1323
  // Compute file lists and append to summary
@@ -1327,6 +1347,7 @@ async function generateTurnPrefixSummary(
1327
1347
  reserveTokens: number,
1328
1348
  apiKey: string,
1329
1349
  signal?: AbortSignal,
1350
+ initiatorOverride?: MessageAttribution,
1330
1351
  ): Promise<string> {
1331
1352
  const maxTokens = Math.floor(0.5 * reserveTokens); // Smaller budget for turn prefix
1332
1353
 
@@ -1344,7 +1365,7 @@ async function generateTurnPrefixSummary(
1344
1365
  const response = await completeSimple(
1345
1366
  model,
1346
1367
  { systemPrompt: SUMMARIZATION_SYSTEM_PROMPT, messages: summarizationMessages },
1347
- { maxTokens, signal, apiKey, reasoning: Effort.High },
1368
+ { maxTokens, signal, apiKey, reasoning: Effort.High, initiatorOverride },
1348
1369
  );
1349
1370
 
1350
1371
  if (response.stopReason === "error") {
@@ -1,7 +1,7 @@
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 { getAgentDir, logger } from "@oh-my-pi/pi-utils";
4
+ import { getHistoryDbPath, logger } from "@oh-my-pi/pi-utils";
5
5
 
6
6
  export interface HistoryEntry {
7
7
  id: number;
@@ -78,7 +78,7 @@ END;
78
78
  this.#lastPromptCache = last?.prompt ?? null;
79
79
  }
80
80
 
81
- static open(dbPath: string = path.join(getAgentDir(), "history.db")): HistoryStorage {
81
+ static open(dbPath: string = getHistoryDbPath()): HistoryStorage {
82
82
  if (!HistoryStorage.#instance) {
83
83
  HistoryStorage.#instance = new HistoryStorage(dbPath);
84
84
  }