@oh-my-pi/pi-coding-agent 12.2.1 → 12.4.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.
@@ -17,6 +17,7 @@ import { reset as resetCapabilities } from "../../capability";
17
17
  import { loadCustomShare } from "../../export/custom-share";
18
18
  import type { CompactOptions } from "../../extensibility/extensions/types";
19
19
  import { getGatewayStatus } from "../../ipy/gateway-coordinator";
20
+ import { buildMemoryToolDeveloperInstructions, clearMemoryData, enqueueMemoryConsolidation } from "../../memories";
20
21
  import { BashExecutionComponent } from "../../modes/components/bash-execution";
21
22
  import { BorderedLoader } from "../../modes/components/bordered-loader";
22
23
  import { DynamicBorder } from "../../modes/components/dynamic-border";
@@ -408,6 +409,51 @@ export class CommandController {
408
409
  this.ctx.ui.requestRender();
409
410
  }
410
411
 
412
+ async handleMemoryCommand(text: string): Promise<void> {
413
+ const argumentText = text.slice(7).trim();
414
+ const action = argumentText.split(/\s+/, 1)[0]?.toLowerCase() || "view";
415
+ const agentDir = this.ctx.settings.getAgentDir();
416
+
417
+ if (action === "view") {
418
+ const payload = await buildMemoryToolDeveloperInstructions(agentDir, this.ctx.settings);
419
+ if (!payload) {
420
+ this.ctx.showWarning("Memory payload is empty (memories disabled or no memory summary found).");
421
+ return;
422
+ }
423
+ this.ctx.chatContainer.addChild(new Spacer(1));
424
+ this.ctx.chatContainer.addChild(new DynamicBorder());
425
+ this.ctx.chatContainer.addChild(new Text(theme.bold(theme.fg("accent", "Memory Injection Payload")), 1, 0));
426
+ this.ctx.chatContainer.addChild(new Spacer(1));
427
+ this.ctx.chatContainer.addChild(new Markdown(payload, 1, 1, getMarkdownTheme()));
428
+ this.ctx.chatContainer.addChild(new DynamicBorder());
429
+ this.ctx.ui.requestRender();
430
+ return;
431
+ }
432
+
433
+ if (action === "reset" || action === "clear") {
434
+ try {
435
+ await clearMemoryData(agentDir, this.ctx.sessionManager.getCwd());
436
+ await this.ctx.session.refreshBaseSystemPrompt();
437
+ this.ctx.showStatus("Memory data cleared and system prompt refreshed.");
438
+ } catch (error) {
439
+ this.ctx.showError(`Memory clear failed: ${error instanceof Error ? error.message : String(error)}`);
440
+ }
441
+ return;
442
+ }
443
+
444
+ if (action === "enqueue" || action === "rebuild") {
445
+ try {
446
+ enqueueMemoryConsolidation(agentDir);
447
+ this.ctx.showStatus("Memory consolidation enqueued.");
448
+ } catch (error) {
449
+ this.ctx.showError(`Memory enqueue failed: ${error instanceof Error ? error.message : String(error)}`);
450
+ }
451
+ return;
452
+ }
453
+
454
+ this.ctx.showError("Usage: /memory <view|clear|reset|enqueue|rebuild>");
455
+ }
456
+
411
457
  async handleClearCommand(): Promise<void> {
412
458
  if (this.ctx.loadingAnimation) {
413
459
  this.ctx.loadingAnimation.stop();
@@ -333,6 +333,11 @@ export class InputController {
333
333
  this.ctx.editor.setText("");
334
334
  return;
335
335
  }
336
+ if (text === "/memory" || text.startsWith("/memory ")) {
337
+ this.ctx.editor.setText("");
338
+ await this.ctx.handleMemoryCommand(text);
339
+ return;
340
+ }
336
341
  if (text === "/resume") {
337
342
  this.ctx.showSessionSelector();
338
343
  this.ctx.editor.setText("");
@@ -919,6 +919,10 @@ export class InteractiveMode implements InteractiveModeContext {
919
919
  return this.#commandController.handleMoveCommand(targetPath);
920
920
  }
921
921
 
922
+ handleMemoryCommand(text: string): Promise<void> {
923
+ return this.#commandController.handleMemoryCommand(text);
924
+ }
925
+
922
926
  showDebugSelector(): void {
923
927
  this.#selectorController.showDebugSelector();
924
928
  }
@@ -151,6 +151,7 @@ export interface InteractiveModeContext {
151
151
  handleCompactCommand(customInstructions?: string): Promise<void>;
152
152
  handleHandoffCommand(customInstructions?: string): Promise<void>;
153
153
  handleMoveCommand(targetPath: string): Promise<void>;
154
+ handleMemoryCommand(text: string): Promise<void>;
154
155
  executeCompaction(customInstructionsOrOptions?: string | CompactOptions, isAuto?: boolean): Promise<void>;
155
156
  openInBrowser(urlOrPath: string): void;
156
157
  refreshSlashCommandState(cwd?: string): Promise<void>;
@@ -0,0 +1,30 @@
1
+ You are the memory consolidation agent.
2
+ Memory root: {{memory_root}}
3
+ Input corpus (raw memories):
4
+ {{raw_memories}}
5
+ Input corpus (rollout summaries):
6
+ {{rollout_summaries}}
7
+ Produce strict JSON only with this schema:
8
+ {
9
+ "memory_md": "string",
10
+ "memory_summary": "string",
11
+ "skills": [
12
+ {
13
+ "name": "string",
14
+ "content": "string",
15
+ "scripts": [{ "path": "string", "content": "string" }],
16
+ "templates": [{ "path": "string", "content": "string" }],
17
+ "examples": [{ "path": "string", "content": "string" }]
18
+ }
19
+ ]
20
+ }
21
+ Requirements:
22
+ - memory_md: full long-term memory document, curated and readable.
23
+ - memory_summary: compact prompt-time memory guidance.
24
+ - skills: reusable procedural playbooks. Empty array allowed.
25
+ - Each skill.name maps to skills/<name>/.
26
+ - Each skill.content maps to skills/<name>/SKILL.md.
27
+ - scripts/templates/examples are optional. When present, each entry writes to skills/<name>/<bucket>/<path>.
28
+ - Only include files worth keeping long-term; omit stale assets so they are pruned.
29
+ - Preserve useful prior themes; remove stale or contradictory guidance.
30
+ - Keep memory advisory: current repository state wins.
@@ -0,0 +1,11 @@
1
+ # Memory Guidance
2
+ Memory root: {{base_path}}
3
+ Operational rules:
4
+ 1) Read `{{base_path}}/memory_summary.md` first.
5
+ 2) If needed, inspect `{{base_path}}/MEMORY.md` and `{{base_path}}/skills/*/SKILL.md`.
6
+ 3) Decision boundary: trust memory for heuristics/process context; trust current repo files, runtime output, and user instruction for factual state and final decisions.
7
+ 4) Citation policy: when memory changes your plan, cite the memory artifact path you used (for example `memories/skills/<name>/SKILL.md`) and pair it with current-repo evidence before acting.
8
+ 5) Conflict workflow: if memory disagrees with repo state or user instruction, prefer repo/user, treat memory as stale, proceed with corrected behavior, then update/regenerate memory artifacts through normal execution.
9
+ 6) Escalate confidence only after repository verification; memory alone is never sufficient proof.
10
+ Memory summary:
11
+ {{memory_summary}}
@@ -0,0 +1,7 @@
1
+ rollout_path: {{rollout_path}}
2
+ cwd: {{cwd}}
3
+
4
+ Persistable response items (JSON):
5
+ {{response_items_json}}
6
+
7
+ Extract durable memory now.
@@ -0,0 +1,21 @@
1
+ You are memory-stage-one extractor.
2
+
3
+ Return strict JSON only, no markdown, no commentary.
4
+
5
+ Extraction goals:
6
+ - Distill reusable durable knowledge from rollout history.
7
+ - Keep concrete technical signal (constraints, decisions, workflows, pitfalls, resolved failures).
8
+ - Exclude transient chatter and low-signal noise.
9
+
10
+ Output contract (required keys):
11
+ {
12
+ "rollout_summary": "string",
13
+ "rollout_slug": "string | null",
14
+ "raw_memory": "string"
15
+ }
16
+
17
+ Rules:
18
+ - rollout_summary: compact synopsis of what future runs should remember.
19
+ - rollout_slug: short lowercase slug (letters/numbers/_), or null.
20
+ - raw_memory: detailed durable memory blocks with enough context to reuse.
21
+ - If no durable signal exists, return empty strings for rollout_summary/raw_memory and null rollout_slug.
package/src/sdk.ts CHANGED
@@ -46,6 +46,7 @@ import {
46
46
  } from "./internal-urls";
47
47
  import { disposeAllKernelSessions } from "./ipy/executor";
48
48
  import { discoverAndLoadMCPTools, type MCPManager, type MCPToolsLoadResult } from "./mcp";
49
+ import { buildMemoryToolDeveloperInstructions, startMemoryStartupTask } from "./memories";
49
50
  import { AgentSession } from "./session/agent-session";
50
51
  import { AuthStorage } from "./session/auth-storage";
51
52
  import { convertToLlm } from "./session/messages";
@@ -914,6 +915,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
914
915
 
915
916
  const rebuildSystemPrompt = async (toolNames: string[], tools: Map<string, AgentTool>): Promise<string> => {
916
917
  toolContextStore.setToolNames(toolNames);
918
+ const memoryInstructions = await buildMemoryToolDeveloperInstructions(agentDir, settings);
917
919
  const defaultPrompt = await buildSystemPromptInternal({
918
920
  cwd,
919
921
  skills,
@@ -923,6 +925,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
923
925
  toolNames,
924
926
  rules: rulebookRules,
925
927
  skillsSettings: settings.getGroup("skills") as SkillsSettings,
928
+ appendSystemPrompt: memoryInstructions,
926
929
  });
927
930
 
928
931
  if (options.systemPrompt === undefined) {
@@ -939,6 +942,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
939
942
  rules: rulebookRules,
940
943
  skillsSettings: settings.getGroup("skills") as SkillsSettings,
941
944
  customPrompt: options.systemPrompt,
945
+ appendSystemPrompt: memoryInstructions,
942
946
  });
943
947
  }
944
948
  return options.systemPrompt(defaultPrompt);
@@ -1133,6 +1137,14 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
1133
1137
  }
1134
1138
  }
1135
1139
 
1140
+ startMemoryStartupTask({
1141
+ session,
1142
+ settings,
1143
+ modelRegistry,
1144
+ agentDir,
1145
+ taskDepth,
1146
+ });
1147
+
1136
1148
  debugStartup("sdk:return");
1137
1149
  return {
1138
1150
  session,
@@ -998,6 +998,14 @@ export class AgentSession {
998
998
  }
999
999
  }
1000
1000
 
1001
+ /** Rebuild the base system prompt using the current active tool set. */
1002
+ async refreshBaseSystemPrompt(): Promise<void> {
1003
+ if (!this.#rebuildSystemPrompt) return;
1004
+ const activeToolNames = this.getActiveToolNames();
1005
+ this.#baseSystemPrompt = await this.#rebuildSystemPrompt(activeToolNames, this.#toolRegistry);
1006
+ this.agent.setSystemPrompt(this.#baseSystemPrompt);
1007
+ }
1008
+
1001
1009
  /**
1002
1010
  * Replace MCP tools in the registry and activate the latest MCP tool set immediately.
1003
1011
  * This allows /mcp add/remove/reauth to take effect without restarting the session.
@@ -1,4 +1,4 @@
1
- import { sanitizeText } from "@oh-my-pi/pi-utils";
1
+ import { sanitizeText } from "@oh-my-pi/pi-natives";
2
2
  import { DEFAULT_MAX_BYTES } from "../tools/truncate";
3
3
 
4
4
  export interface OutputSummary {
@@ -3,7 +3,7 @@
3
3
  */
4
4
  import * as os from "node:os";
5
5
  import { getSystemInfo as getNativeSystemInfo, type SystemInfo } from "@oh-my-pi/pi-natives";
6
- import { $env, logger } from "@oh-my-pi/pi-utils";
6
+ import { $env, hasFsCode, isEnoent, logger } from "@oh-my-pi/pi-utils";
7
7
  import { getGpuCachePath, getProjectDir } from "@oh-my-pi/pi-utils/dirs";
8
8
  import { $ } from "bun";
9
9
  import { contextFileCapability } from "./capability/context-file";
@@ -346,19 +346,18 @@ async function getEnvironmentInfo(): Promise<Array<{ label: string; value: strin
346
346
  export async function resolvePromptInput(input: string | undefined, description: string): Promise<string | undefined> {
347
347
  if (!input) {
348
348
  return undefined;
349
+ } else if (input.includes("\n")) {
350
+ return input;
349
351
  }
350
352
 
351
- const file = Bun.file(input);
352
- if (await file.exists()) {
353
- try {
354
- return await file.text();
355
- } catch (error) {
353
+ try {
354
+ return await Bun.file(input).text();
355
+ } catch (error) {
356
+ if (!hasFsCode(error, "ENAMETOOLONG") && !isEnoent(error)) {
356
357
  logger.warn(`Could not read ${description} file`, { path: input, error: String(error) });
357
- return input;
358
358
  }
359
+ return input;
359
360
  }
360
-
361
- return input;
362
361
  }
363
362
 
364
363
  export interface LoadContextFilesOptions {
@@ -1,5 +1,5 @@
1
1
  import type { AgentToolContext } from "@oh-my-pi/pi-agent-core";
2
- import { type PtyRunResult, PtySession } from "@oh-my-pi/pi-natives";
2
+ import { type PtyRunResult, PtySession, sanitizeText } from "@oh-my-pi/pi-natives";
3
3
  import {
4
4
  type Component,
5
5
  matchesKey,
@@ -23,9 +23,8 @@ export interface BashInteractiveResult extends OutputSummary {
23
23
  }
24
24
 
25
25
  function normalizeCaptureChunk(chunk: string): string {
26
- const noAnsi = Bun.stripANSI(chunk);
27
- const normalized = noAnsi.replace(/\r\n/gu, "\n").replace(/\r/gu, "\n");
28
- return normalized.replace(/[\x00-\x08\x0B-\x1F\x7F]/gu, "");
26
+ const normalized = chunk.replace(/\r\n/gu, "\n").replace(/\r/gu, "\n");
27
+ return sanitizeText(normalized);
29
28
  }
30
29
 
31
30
  const XtermTerminal = xterm.Terminal;
@@ -168,7 +167,7 @@ class BashInteractiveOverlayComponent implements Component {
168
167
  const visibleLines: string[] = [];
169
168
  for (let i = 0; i < maxContentRows; i++) {
170
169
  const line = buffer.getLine(viewportY + i)?.translateToString(true) ?? "";
171
- visibleLines.push(truncateToWidth(replaceTabs(line), innerWidth));
170
+ visibleLines.push(truncateToWidth(replaceTabs(sanitizeText(line)), innerWidth));
172
171
  }
173
172
  return visibleLines;
174
173
  }
@@ -350,7 +349,12 @@ export async function runInteractiveBashPty(
350
349
  },
351
350
  (err, chunk) => {
352
351
  if (err || !chunk) return;
353
- component.appendOutput(chunk);
352
+ try {
353
+ component.appendOutput(chunk);
354
+ } catch {
355
+ const normalizedChunk = normalizeCaptureChunk(chunk);
356
+ component.appendOutput(normalizedChunk);
357
+ }
354
358
  const normalizedChunk = normalizeCaptureChunk(chunk);
355
359
  pendingChunks = pendingChunks.then(() => sink.push(normalizedChunk)).catch(() => {});
356
360
  tui.requestRender();
@@ -241,7 +241,7 @@ async function tryContentNegotiation(
241
241
  if (!result.ok) return null;
242
242
 
243
243
  const mime = normalizeMime(result.contentType);
244
- if (mime.includes("markdown") || mime === "text/plain") {
244
+ if ((mime.includes("markdown") || mime === "text/plain") && !looksLikeHtml(result.content)) {
245
245
  return { content: result.content, type: result.contentType };
246
246
  }
247
247
 
@@ -21,6 +21,7 @@ export interface TruncationMeta {
21
21
  totalBytes: number;
22
22
  outputLines: number;
23
23
  outputBytes: number;
24
+ maxBytes?: number;
24
25
  /** Line range shown (1-indexed, inclusive) */
25
26
  shownRange?: { start: number; end: number };
26
27
  /** Artifact ID if full output was saved */
@@ -128,6 +129,7 @@ export class OutputMetaBuilder {
128
129
  totalBytes: result.totalBytes,
129
130
  outputLines: result.outputLines,
130
131
  outputBytes: result.outputBytes,
132
+ maxBytes: result.maxBytes,
131
133
  shownRange: { start: shownStart, end: shownEnd },
132
134
  artifactId,
133
135
  nextOffset: direction === "head" ? shownEnd + 1 : undefined,
@@ -212,6 +214,7 @@ export class OutputMetaBuilder {
212
214
  totalBytes,
213
215
  outputLines,
214
216
  outputBytes,
217
+ maxBytes: options.maxBytes,
215
218
  shownRange: { start: shownStart, end: shownEnd },
216
219
  nextOffset: options.direction === "head" ? shownEnd + 1 : undefined,
217
220
  };
@@ -319,14 +322,15 @@ export function formatOutputNotice(meta: OutputMeta | undefined): string {
319
322
  const range = t.shownRange;
320
323
  let notice: string;
321
324
 
322
- if (range) {
325
+ if (range && range.end >= range.start) {
323
326
  notice = `Showing lines ${range.start}-${range.end} of ${t.totalLines}`;
324
327
  } else {
325
328
  notice = `Showing ${t.outputLines} of ${t.totalLines} lines`;
326
329
  }
327
330
 
328
331
  if (t.truncatedBy === "bytes") {
329
- notice += ` (${formatSize(t.outputBytes)} limit)`;
332
+ const maxBytes = t.maxBytes ?? t.outputBytes;
333
+ notice += ` (${formatSize(maxBytes)} limit)`;
330
334
  }
331
335
 
332
336
  if (t.nextOffset != null) {
@@ -92,6 +92,7 @@ export async function loadPage(url: string, options: LoadPageOptions = {}): Prom
92
92
  "User-Agent": userAgent,
93
93
  Accept: "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
94
94
  "Accept-Language": "en-US,en;q=0.5",
95
+ "Accept-Encoding": "identity", // Cloudflare Markdown-for-Agents returns corrupted bytes when compression is negotiated
95
96
  ...headers,
96
97
  },
97
98
  redirect: "follow",