@oh-my-pi/pi-coding-agent 16.0.10 → 16.0.11

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 (86) hide show
  1. package/CHANGELOG.md +35 -0
  2. package/dist/cli.js +3208 -3199
  3. package/dist/types/advisor/index.d.ts +1 -0
  4. package/dist/types/advisor/transcript-recorder.d.ts +52 -0
  5. package/dist/types/commit/agentic/agent.d.ts +1 -1
  6. package/dist/types/config/settings-schema.d.ts +0 -4
  7. package/dist/types/edit/file-snapshot-store.d.ts +1 -1
  8. package/dist/types/extensibility/extensions/types.d.ts +7 -0
  9. package/dist/types/modes/components/agent-hub.d.ts +6 -1
  10. package/dist/types/modes/components/agent-transcript-viewer.d.ts +39 -0
  11. package/dist/types/modes/components/chat-transcript-builder.d.ts +42 -0
  12. package/dist/types/modes/controllers/command-controller.d.ts +3 -2
  13. package/dist/types/modes/interactive-mode.d.ts +2 -1
  14. package/dist/types/modes/types.d.ts +2 -1
  15. package/dist/types/registry/agent-registry.d.ts +10 -3
  16. package/dist/types/session/compact-modes.d.ts +60 -0
  17. package/dist/types/session/streaming-output.d.ts +0 -2
  18. package/dist/types/tools/__tests__/json-tree.test.d.ts +1 -0
  19. package/package.json +12 -12
  20. package/src/advisor/index.ts +1 -0
  21. package/src/advisor/transcript-recorder.ts +136 -0
  22. package/src/cli/stats-cli.ts +2 -11
  23. package/src/collab/host.ts +25 -13
  24. package/src/commit/agentic/agent.ts +2 -1
  25. package/src/commit/agentic/tools/git-file-diff.ts +2 -2
  26. package/src/commit/changelog/index.ts +1 -1
  27. package/src/commit/map-reduce/map-phase.ts +1 -1
  28. package/src/commit/map-reduce/utils.ts +1 -1
  29. package/src/config/settings-schema.ts +0 -5
  30. package/src/config/settings.ts +0 -6
  31. package/src/edit/file-snapshot-store.ts +1 -1
  32. package/src/edit/renderer.ts +7 -7
  33. package/src/eval/js/tool-bridge.ts +3 -2
  34. package/src/eval/py/prelude.py +3 -2
  35. package/src/export/html/tool-views.generated.js +28 -28
  36. package/src/extensibility/extensions/types.ts +7 -0
  37. package/src/hindsight/mental-models.ts +1 -1
  38. package/src/internal-urls/docs-index.generated.txt +1 -1
  39. package/src/internal-urls/history-protocol.ts +8 -3
  40. package/src/irc/bus.ts +8 -0
  41. package/src/lsp/index.ts +2 -2
  42. package/src/main.ts +4 -1
  43. package/src/modes/acp/acp-agent.ts +63 -0
  44. package/src/modes/components/agent-hub.ts +97 -920
  45. package/src/modes/components/agent-transcript-viewer.ts +461 -0
  46. package/src/modes/components/chat-transcript-builder.ts +462 -0
  47. package/src/modes/components/diff.ts +12 -35
  48. package/src/modes/controllers/command-controller.ts +12 -2
  49. package/src/modes/controllers/event-controller.ts +1 -1
  50. package/src/modes/controllers/input-controller.ts +8 -1
  51. package/src/modes/controllers/selector-controller.ts +4 -1
  52. package/src/modes/interactive-mode.ts +4 -2
  53. package/src/modes/types.ts +2 -1
  54. package/src/prompts/tools/read.md +1 -1
  55. package/src/registry/agent-registry.ts +13 -4
  56. package/src/sdk.ts +1 -1
  57. package/src/session/agent-session.ts +92 -3
  58. package/src/session/compact-modes.ts +105 -0
  59. package/src/session/session-dump-format.ts +1 -1
  60. package/src/session/session-history-format.ts +1 -1
  61. package/src/session/streaming-output.ts +5 -5
  62. package/src/slash-commands/builtin-registry.ts +16 -4
  63. package/src/task/executor.ts +1 -1
  64. package/src/task/output-manager.ts +5 -0
  65. package/src/tools/__tests__/json-tree.test.ts +35 -0
  66. package/src/tools/approval.ts +1 -1
  67. package/src/tools/bash.ts +0 -1
  68. package/src/tools/browser.ts +0 -1
  69. package/src/tools/eval.ts +1 -1
  70. package/src/tools/gh.ts +1 -1
  71. package/src/tools/irc.ts +1 -1
  72. package/src/tools/json-tree.ts +22 -5
  73. package/src/tools/read.ts +5 -6
  74. package/src/web/scrapers/firefox-addons.ts +1 -1
  75. package/src/web/scrapers/github.ts +1 -1
  76. package/src/web/scrapers/go-pkg.ts +2 -2
  77. package/src/web/scrapers/metacpan.ts +2 -2
  78. package/src/web/scrapers/nvd.ts +2 -2
  79. package/src/web/scrapers/ollama.ts +1 -1
  80. package/src/web/scrapers/opencorporates.ts +1 -1
  81. package/src/web/scrapers/pub-dev.ts +1 -1
  82. package/src/web/scrapers/repology.ts +1 -1
  83. package/src/web/scrapers/sourcegraph.ts +1 -1
  84. package/src/web/scrapers/terraform.ts +6 -6
  85. package/src/web/scrapers/wikidata.ts +2 -2
  86. package/src/workspace-tree.ts +1 -1
@@ -1,3 +1,4 @@
1
1
  export * from "./advise-tool";
2
2
  export * from "./runtime";
3
+ export * from "./transcript-recorder";
3
4
  export * from "./watchdog";
@@ -0,0 +1,52 @@
1
+ import type { AgentMessage } from "@oh-my-pi/pi-agent-core";
2
+ /**
3
+ * Reserved transcript stem for advisor session files. Chosen so it cannot
4
+ * collide with a task subagent's `<id>.jsonl` (task ids are reserved against
5
+ * this exact stem in {@link AgentOutputManager}).
6
+ */
7
+ export declare const ADVISOR_TRANSCRIPT_STEM = "__advisor";
8
+ export declare const ADVISOR_TRANSCRIPT_FILENAME = "__advisor.jsonl";
9
+ /**
10
+ * Append-only persister for an advisor agent's transcript.
11
+ *
12
+ * The advisor is a passive reviewer with its own model usage, so — like a task
13
+ * subagent — its turns are written to a JSONL inside the owning session's
14
+ * artifacts dir (`<session>/__advisor.jsonl`, `<session>/<SubId>/__advisor.jsonl`
15
+ * for subagent advisors). That single file gives the advisor model proper usage
16
+ * attribution in `omp stats` (the stats parser scans the session dir
17
+ * recursively) and a read-only transcript in the Agent Hub, without making the
18
+ * advisor a registered, messageable peer.
19
+ *
20
+ * The target is derived from the *session file* (`getSessionFile()`), never
21
+ * `getArtifactsDir()` — subagents adopt the parent's artifact manager, so the
22
+ * artifacts dir points at the parent root and every subagent advisor would
23
+ * collide. The file path is resolved synchronously when a message finalizes and
24
+ * captured for the queued write, so a `/new`, resume, or session switch in
25
+ * flight can never misattribute an old advisor turn into the new session's file.
26
+ * On such a switch the previous writer is closed and the new file opened on the
27
+ * next recorded turn. The recorder never truncates: the advisor's in-memory
28
+ * context resets/compacts independently, but every billed turn is appended here.
29
+ */
30
+ export declare class AdvisorTranscriptRecorder {
31
+ #private;
32
+ private readonly resolveSessionFile;
33
+ private readonly resolveCwd;
34
+ /**
35
+ * @param after Optional barrier the queue starts behind — used on the advisor
36
+ * on→off→on toggle so a fresh recorder's first `open` waits for the prior
37
+ * recorder's `close` and the two never hold the same `__advisor.jsonl` at once.
38
+ */
39
+ constructor(resolveSessionFile: () => string | undefined, resolveCwd: () => string, after?: Promise<unknown>);
40
+ /**
41
+ * Persist one finalized advisor message. Assistant turns carry the usage the
42
+ * stats parser reads; tool results round out the Hub transcript; user deltas
43
+ * (the advisor's "session update" prompts) are persisted but flagged
44
+ * `synthetic`/agent-attributed so they never inflate user-message metrics.
45
+ * Non-conversational message kinds are skipped.
46
+ */
47
+ record(message: AgentMessage): void;
48
+ /** Flush pending writes (best-effort). */
49
+ flush(): Promise<void>;
50
+ /** Flush and close the writer, releasing the session file. */
51
+ close(): Promise<void>;
52
+ }
@@ -1,4 +1,4 @@
1
- import { type ThinkingLevel } from "@oh-my-pi/pi-agent-core";
1
+ import type { ThinkingLevel } from "@oh-my-pi/pi-agent-core";
2
2
  import type { Api, Model } from "@oh-my-pi/pi-ai";
3
3
  import type { ModelRegistry } from "../../config/model-registry";
4
4
  import type { Settings } from "../../config/settings";
@@ -775,10 +775,6 @@ export declare const SETTINGS_SCHEMA: {
775
775
  readonly description: "Remove the 1-character horizontal padding from the left and right of the terminal output";
776
776
  };
777
777
  };
778
- readonly "display.tabWidth": {
779
- readonly type: "number";
780
- readonly default: 3;
781
- };
782
778
  readonly "display.shimmer": {
783
779
  readonly type: "enum";
784
780
  readonly values: readonly ["classic", "kitt", "disabled"];
@@ -48,7 +48,7 @@ export declare function recordFileSnapshot(session: FileSnapshotStoreOwner, abso
48
48
  /**
49
49
  * The 1-indexed file lines a hashline-formatted body actually displayed.
50
50
  * Single `NN:` rows contribute that line; a collapsed summary `NN-MM:` row
51
- * (a `{ .. }` brace pair) contributes only its boundary lines `NN` and `MM` —
51
+ * (a `{ }` brace pair) contributes only its boundary lines `NN` and `MM` —
52
52
  * the elided interior was never shown, so editing inside it must be rejected.
53
53
  */
54
54
  export declare function parseSeenLinesFromHashlineBody(body: string): number[];
@@ -25,6 +25,7 @@ import type * as PiCodingAgent from "../../index";
25
25
  import type { MemoryRuntimeContext } from "../../memory-backend";
26
26
  import type { CustomEditor } from "../../modes/components/custom-editor";
27
27
  import type { Theme } from "../../modes/theme/theme";
28
+ import type { CompactMode } from "../../session/compact-modes";
28
29
  import type { CustomMessage } from "../../session/messages";
29
30
  import type { ReadonlySessionManager, SessionManager } from "../../session/session-manager";
30
31
  import type { BashToolDetails, BashToolInput, FindToolDetails, FindToolInput, ReadToolDetails, ReadToolInput, SearchToolDetails, SearchToolInput, WriteToolInput } from "../../tools";
@@ -171,6 +172,12 @@ export interface ContextUsage {
171
172
  export interface CompactOptions {
172
173
  onComplete?: (result: CompactionResult) => void;
173
174
  onError?: (error: Error) => void;
175
+ /**
176
+ * Force a one-off compaction mode for this invocation, overriding the
177
+ * configured `compaction.strategy` / `remoteEnabled` (the `/compact`
178
+ * subcommands: `soft` | `remote` | `snapcompact`). Omitted = configured behavior.
179
+ */
180
+ mode?: CompactMode;
174
181
  }
175
182
  /**
176
183
  * Context passed to extension event handlers.
@@ -62,6 +62,11 @@ export declare class AgentHubOverlayComponent extends Container {
62
62
  dispose(): void;
63
63
  render(width: number): readonly string[];
64
64
  handleInput(keyData: string): void;
65
- /** Open the chat view for an agent id (public for table Enter and tests). */
65
+ /**
66
+ * Open the fullscreen transcript viewer for an agent id (public for table Enter
67
+ * and tests). Mounts {@link AgentTranscriptViewer} as a `fullscreen` overlay so it
68
+ * owns the alternate screen; the hub table stays mounted underneath and is
69
+ * restored when the viewer closes. No-op without a real TUI (render-only test stub).
70
+ */
66
71
  openChat(id: string): void;
67
72
  }
@@ -0,0 +1,39 @@
1
+ import type { AgentTool } from "@oh-my-pi/pi-agent-core";
2
+ import { type Component, type TUI } from "@oh-my-pi/pi-tui";
3
+ import type { KeyId } from "../../config/keybindings";
4
+ import type { MessageRenderer } from "../../extensibility/extensions/types";
5
+ import type { AgentLifecycleManager } from "../../registry/agent-lifecycle";
6
+ import type { AgentRegistry } from "../../registry/agent-registry";
7
+ import type { SessionObserverRegistry } from "../session-observer-registry";
8
+ import type { AgentHubRemote } from "./agent-hub";
9
+ export interface AgentTranscriptViewerDeps {
10
+ agentId: string;
11
+ registry: AgentRegistry;
12
+ /** Collab guest: read transcript from the host instead of a local file. */
13
+ remote?: AgentHubRemote;
14
+ /** Progress/cost snapshot source for the stats line. */
15
+ observers?: SessionObserverRegistry;
16
+ /** Revive+prompt path for messageable local agents. Lazy to avoid touching the global. */
17
+ lifecycle?: () => AgentLifecycleManager;
18
+ ui: TUI;
19
+ getTool?: (name: string) => AgentTool | undefined;
20
+ getMessageRenderer?: (customType: string) => MessageRenderer | undefined;
21
+ cwd: string;
22
+ hideThinkingBlock?: () => boolean;
23
+ expandKeys: KeyId[];
24
+ /** Keys that toggle the whole hub closed (app.agents.hub + app.session.observe). */
25
+ hubKeys: KeyId[];
26
+ requestRender: () => void;
27
+ /** Close just this viewer (Esc), returning to the hub table. */
28
+ onClose: () => void;
29
+ /** Close this viewer AND the hub (hub-toggle keys). */
30
+ onHubClose: () => void;
31
+ }
32
+ export declare class AgentTranscriptViewer implements Component {
33
+ #private;
34
+ private readonly deps;
35
+ constructor(deps: AgentTranscriptViewerDeps);
36
+ dispose(): void;
37
+ handleInput(data: string): void;
38
+ render(width: number): readonly string[];
39
+ }
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Builds transcript components from persisted session message entries — the
3
+ * file/remote-backed counterpart to {@link UiHelpers.addMessageToChat} (which is
4
+ * bound to the live InteractiveModeContext). Used by the fullscreen transcript
5
+ * viewer ({@link AgentTranscriptViewer}) to render a parked subagent / advisor /
6
+ * collab-guest transcript that has no live session.
7
+ *
8
+ * Unlike the old incremental hub sync, {@link ChatTranscriptBuilder.rebuild}
9
+ * always discards prior components and rebuilds the whole transcript from the
10
+ * supplied entries. Re-rendering a growing transcript is therefore O(n) in the
11
+ * entry count, but it cannot duplicate or misorder rows the way incremental
12
+ * component reuse could.
13
+ */
14
+ import type { AgentTool } from "@oh-my-pi/pi-agent-core";
15
+ import { type TUI } from "@oh-my-pi/pi-tui";
16
+ import type { MessageRenderer } from "../../extensibility/extensions/types";
17
+ import type { SessionMessageEntry } from "../../session/session-entries";
18
+ import { TranscriptContainer } from "./transcript-container";
19
+ export interface ChatTranscriptBuilderDeps {
20
+ ui: TUI;
21
+ getTool?: (name: string) => AgentTool | undefined;
22
+ getMessageRenderer?: (customType: string) => MessageRenderer | undefined;
23
+ cwd: string;
24
+ hideThinkingBlock?: () => boolean;
25
+ requestRender: () => void;
26
+ }
27
+ export declare class ChatTranscriptBuilder {
28
+ #private;
29
+ private readonly deps;
30
+ readonly container: TranscriptContainer;
31
+ constructor(deps: ChatTranscriptBuilderDeps);
32
+ /** Whether the transcript currently holds any rendered rows. */
33
+ get isEmpty(): boolean;
34
+ /** Discard all components and rebuild the whole transcript from `entries`. */
35
+ rebuild(entries: SessionMessageEntry[]): void;
36
+ /** Toggle tool-output expansion across every expandable component. */
37
+ setExpanded(expanded: boolean): void;
38
+ get expanded(): boolean;
39
+ /** Tear down components (sealing pending spinners) and clear build state. */
40
+ reset(): void;
41
+ dispose(): void;
42
+ }
@@ -3,6 +3,7 @@ import { type ProviderDetails, type UsageReport } from "@oh-my-pi/pi-ai";
3
3
  import type { CompactOptions } from "../../extensibility/extensions/types";
4
4
  import { theme } from "../../modes/theme/theme";
5
5
  import type { InteractiveModeContext } from "../../modes/types";
6
+ import type { CompactMode } from "../../session/compact-modes";
6
7
  import { type ShakeMode } from "../../session/shake-types";
7
8
  export declare class CommandController {
8
9
  #private;
@@ -31,14 +32,14 @@ export declare class CommandController {
31
32
  handleRenameCommand(title: string): Promise<void>;
32
33
  handleBashCommand(command: string, excludeFromContext?: boolean): Promise<void>;
33
34
  handlePythonCommand(code: string, excludeFromContext?: boolean): Promise<void>;
34
- handleCompactCommand(customInstructions?: string, beforeFlush?: (outcome: CompactionOutcome) => void | Promise<void>): Promise<CompactionOutcome>;
35
+ handleCompactCommand(customInstructions?: string, mode?: CompactMode, beforeFlush?: (outcome: CompactionOutcome) => void | Promise<void>): Promise<CompactionOutcome>;
35
36
  /**
36
37
  * TUI handler for `/shake`. `elide` drops heavy structural content and
37
38
  * `images` strips image blocks. Rebuilds the chat and reports counts.
38
39
  */
39
40
  handleShakeCommand(mode: ShakeMode): Promise<void>;
40
41
  handleSkillCommand(skillPath: string, args: string): Promise<void>;
41
- executeCompaction(customInstructionsOrOptions?: string | CompactOptions, isAuto?: boolean, beforeFlush?: (outcome: CompactionOutcome) => void | Promise<void>): Promise<CompactionOutcome>;
42
+ executeCompaction(customInstructionsOrOptions?: string | CompactOptions, isAuto?: boolean, beforeFlush?: (outcome: CompactionOutcome) => void | Promise<void>, mode?: CompactMode): Promise<CompactionOutcome>;
42
43
  handleHandoffCommand(customInstructions?: string): Promise<void>;
43
44
  }
44
45
  export declare function renderProviderSection(details: ProviderDetails, uiTheme: Pick<typeof theme, "fg">): string;
@@ -12,6 +12,7 @@ import type { CompactOptions } from "../extensibility/extensions/types";
12
12
  import type { MCPManager } from "../mcp";
13
13
  import { type PlanApprovalDetails } from "../plan-mode/approved-plan";
14
14
  import type { AgentSession } from "../session/agent-session";
15
+ import type { CompactMode } from "../session/compact-modes";
15
16
  import { HistoryStorage } from "../session/history-storage";
16
17
  import type { SessionContext } from "../session/session-context";
17
18
  import type { SessionManager } from "../session/session-manager";
@@ -298,7 +299,7 @@ export declare class InteractiveMode implements InteractiveModeContext {
298
299
  handlePythonCommand(code: string, excludeFromContext?: boolean): Promise<void>;
299
300
  handleMCPCommand(text: string): Promise<void>;
300
301
  handleSSHCommand(text: string): Promise<void>;
301
- handleCompactCommand(customInstructions?: string, beforeFlush?: (outcome: CompactionOutcome) => void | Promise<void>): Promise<CompactionOutcome>;
302
+ handleCompactCommand(customInstructions?: string, mode?: CompactMode, beforeFlush?: (outcome: CompactionOutcome) => void | Promise<void>): Promise<CompactionOutcome>;
302
303
  handleHandoffCommand(customInstructions?: string): Promise<void>;
303
304
  handleShakeCommand(mode: ShakeMode): Promise<void>;
304
305
  executeCompaction(customInstructionsOrOptions?: string | CompactOptions, isAuto?: boolean): Promise<CompactionOutcome>;
@@ -11,6 +11,7 @@ import type { CompactOptions } from "../extensibility/extensions/types";
11
11
  import type { MCPManager } from "../mcp";
12
12
  import type { PlanApprovalDetails } from "../plan-mode/approved-plan";
13
13
  import type { AgentSession } from "../session/agent-session";
14
+ import type { CompactMode } from "../session/compact-modes";
14
15
  import type { HistoryStorage } from "../session/history-storage";
15
16
  import type { SessionContext } from "../session/session-context";
16
17
  import type { SessionManager } from "../session/session-manager";
@@ -273,7 +274,7 @@ export interface InteractiveModeContext {
273
274
  handlePythonCommand(code: string, excludeFromContext?: boolean): Promise<void>;
274
275
  handleMCPCommand(text: string): Promise<void>;
275
276
  handleSSHCommand(text: string): Promise<void>;
276
- handleCompactCommand(customInstructions?: string): Promise<CompactionOutcome>;
277
+ handleCompactCommand(customInstructions?: string, mode?: CompactMode): Promise<CompactionOutcome>;
277
278
  handleHandoffCommand(customInstructions?: string): Promise<void>;
278
279
  handleShakeCommand(mode: ShakeMode): Promise<void>;
279
280
  handleMoveCommand(targetPath: string): Promise<void>;
@@ -18,7 +18,13 @@ export declare const MAIN_AGENT_ID = "Main";
18
18
  * - `aborted`: hard-killed, terminal.
19
19
  */
20
20
  export type AgentStatus = "running" | "idle" | "parked" | "aborted";
21
- export type AgentKind = "main" | "sub";
21
+ /**
22
+ * - `main`/`sub`: the user-facing agent tree (driving agent + task subagents).
23
+ * - `advisor`: a passive review transcript persisted like a subagent for usage
24
+ * attribution and Agent Hub observability, but never a peer — hidden from
25
+ * agent-facing rosters (`irc`, `history://`) and not messageable/revivable.
26
+ */
27
+ export type AgentKind = "main" | "sub" | "advisor";
22
28
  export interface AgentRef {
23
29
  id: string;
24
30
  displayName: string;
@@ -81,8 +87,9 @@ export declare class AgentRegistry {
81
87
  get(id: string): AgentRef | undefined;
82
88
  list(): AgentRef[];
83
89
  /**
84
- * Returns every alive agent (running | idle) except the caller.
85
- * Flat namespace: every agent can see every other agent.
90
+ * Returns every alive agent (running | idle) except the caller. Advisor refs
91
+ * are observability-only transcripts, never peers, so they are excluded.
92
+ * Flat namespace: every other agent is visible.
86
93
  */
87
94
  listVisibleTo(id: string): AgentRef[];
88
95
  onChange(listener: RegistryListener): () => void;
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Manual `/compact` subcommands. Kept in a dependency-free leaf module so the
3
+ * slash-command registry, the interactive controllers, and `AgentSession`
4
+ * can all import the mode metadata + parser without pulling in the heavy
5
+ * `agent-session` module graph (which would form an import cycle through the
6
+ * slash-command registry) — same rationale as `shake-types.ts`.
7
+ *
8
+ * Each mode is a one-off override layered on top of the configured
9
+ * `compaction.*` settings for a single invocation; it never mutates settings.
10
+ * Adding a mode is a single entry here: the command surface (autocomplete +
11
+ * ACP hint), the parser, and the engine override all read this table.
12
+ */
13
+ /** Subcommand selecting a one-off compaction mode for manual `/compact`. */
14
+ export type CompactMode = "soft" | "remote" | "snapcompact";
15
+ /**
16
+ * Per-invocation overrides merged over the configured `compaction.*` settings.
17
+ * Narrowed to the two knobs the modes actually flip; the result stays
18
+ * assignable to the full `CompactionSettings`.
19
+ */
20
+ export interface CompactionOverride {
21
+ strategy?: "context-full" | "snapcompact";
22
+ remoteEnabled?: boolean;
23
+ }
24
+ export interface CompactModeDef {
25
+ readonly name: CompactMode;
26
+ /** One-line description surfaced in autocomplete + help. */
27
+ readonly description: string;
28
+ /** Settings overrides applied on top of `compaction.*` for this run. */
29
+ readonly overrides: CompactionOverride;
30
+ /**
31
+ * When true, the mode produces no LLM summary, so trailing focus text is
32
+ * meaningless and rejected by the parser (snapcompact archives history into
33
+ * images without a directed summary).
34
+ */
35
+ readonly rejectsFocus?: boolean;
36
+ /**
37
+ * When true, the mode explicitly demands a remote path; the engine warns and
38
+ * falls back to a local summary if neither a remote endpoint nor a
39
+ * provider-native compaction path is available.
40
+ */
41
+ readonly requiresRemote?: boolean;
42
+ }
43
+ export declare const COMPACT_MODES: readonly CompactModeDef[];
44
+ /** Resolve a subcommand token (case-insensitive) to its mode definition. */
45
+ export declare function findCompactMode(name: string): CompactModeDef | undefined;
46
+ /** Parsed `/compact` arguments: an optional mode plus optional focus text. */
47
+ export interface ParsedCompactArgs {
48
+ mode?: CompactMode;
49
+ instructions?: string;
50
+ }
51
+ /**
52
+ * Split `/compact` args into a leading mode subcommand + focus instructions.
53
+ *
54
+ * Backward compatible: when the first token is not a known mode, the entire
55
+ * argument string is treated as focus instructions (the historical behavior).
56
+ * A recognized mode with `rejectsFocus` and trailing text is an error.
57
+ */
58
+ export declare function parseCompactArgs(args: string): ParsedCompactArgs | {
59
+ error: string;
60
+ };
@@ -163,8 +163,6 @@ export declare function truncateMiddle(content: string, options?: TruncationOpti
163
163
  export interface InlineByteCapOptions {
164
164
  /** Inline byte budget. Defaults to {@link DEFAULT_MAX_BYTES}. */
165
165
  maxBytes?: number;
166
- /** What the text is, for the elision marker (e.g. "bash output"). */
167
- label: string;
168
166
  /**
169
167
  * Persist the full text as a session artifact. When an artifact id is
170
168
  * returned, a `[raw output: artifact://<id>]` footer is appended so the
@@ -0,0 +1 @@
1
+ export {};
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.0.10",
4
+ "version": "16.0.11",
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",
@@ -48,17 +48,17 @@
48
48
  "@agentclientprotocol/sdk": "0.25.0",
49
49
  "@babel/parser": "^7.29.7",
50
50
  "@mozilla/readability": "^0.6.0",
51
- "@oh-my-pi/hashline": "16.0.10",
52
- "@oh-my-pi/omp-stats": "16.0.10",
53
- "@oh-my-pi/pi-agent-core": "16.0.10",
54
- "@oh-my-pi/pi-ai": "16.0.10",
55
- "@oh-my-pi/pi-catalog": "16.0.10",
56
- "@oh-my-pi/pi-mnemopi": "16.0.10",
57
- "@oh-my-pi/pi-natives": "16.0.10",
58
- "@oh-my-pi/pi-tui": "16.0.10",
59
- "@oh-my-pi/pi-utils": "16.0.10",
60
- "@oh-my-pi/pi-wire": "16.0.10",
61
- "@oh-my-pi/snapcompact": "16.0.10",
51
+ "@oh-my-pi/hashline": "16.0.11",
52
+ "@oh-my-pi/omp-stats": "16.0.11",
53
+ "@oh-my-pi/pi-agent-core": "16.0.11",
54
+ "@oh-my-pi/pi-ai": "16.0.11",
55
+ "@oh-my-pi/pi-catalog": "16.0.11",
56
+ "@oh-my-pi/pi-mnemopi": "16.0.11",
57
+ "@oh-my-pi/pi-natives": "16.0.11",
58
+ "@oh-my-pi/pi-tui": "16.0.11",
59
+ "@oh-my-pi/pi-utils": "16.0.11",
60
+ "@oh-my-pi/pi-wire": "16.0.11",
61
+ "@oh-my-pi/snapcompact": "16.0.11",
62
62
  "@opentelemetry/api": "^1.9.1",
63
63
  "@opentelemetry/context-async-hooks": "^2.7.1",
64
64
  "@opentelemetry/exporter-trace-otlp-proto": "^0.218.0",
@@ -1,3 +1,4 @@
1
1
  export * from "./advise-tool";
2
2
  export * from "./runtime";
3
+ export * from "./transcript-recorder";
3
4
  export * from "./watchdog";
@@ -0,0 +1,136 @@
1
+ import * as path from "node:path";
2
+ import type { AgentMessage } from "@oh-my-pi/pi-agent-core";
3
+ import type { Message, UserMessage } from "@oh-my-pi/pi-ai";
4
+ import { logger } from "@oh-my-pi/pi-utils";
5
+ import { SessionManager } from "../session/session-manager";
6
+
7
+ /**
8
+ * Reserved transcript stem for advisor session files. Chosen so it cannot
9
+ * collide with a task subagent's `<id>.jsonl` (task ids are reserved against
10
+ * this exact stem in {@link AgentOutputManager}).
11
+ */
12
+ export const ADVISOR_TRANSCRIPT_STEM = "__advisor";
13
+ export const ADVISOR_TRANSCRIPT_FILENAME = `${ADVISOR_TRANSCRIPT_STEM}.jsonl`;
14
+
15
+ const JSONL_SUFFIX = ".jsonl";
16
+
17
+ /**
18
+ * Append-only persister for an advisor agent's transcript.
19
+ *
20
+ * The advisor is a passive reviewer with its own model usage, so — like a task
21
+ * subagent — its turns are written to a JSONL inside the owning session's
22
+ * artifacts dir (`<session>/__advisor.jsonl`, `<session>/<SubId>/__advisor.jsonl`
23
+ * for subagent advisors). That single file gives the advisor model proper usage
24
+ * attribution in `omp stats` (the stats parser scans the session dir
25
+ * recursively) and a read-only transcript in the Agent Hub, without making the
26
+ * advisor a registered, messageable peer.
27
+ *
28
+ * The target is derived from the *session file* (`getSessionFile()`), never
29
+ * `getArtifactsDir()` — subagents adopt the parent's artifact manager, so the
30
+ * artifacts dir points at the parent root and every subagent advisor would
31
+ * collide. The file path is resolved synchronously when a message finalizes and
32
+ * captured for the queued write, so a `/new`, resume, or session switch in
33
+ * flight can never misattribute an old advisor turn into the new session's file.
34
+ * On such a switch the previous writer is closed and the new file opened on the
35
+ * next recorded turn. The recorder never truncates: the advisor's in-memory
36
+ * context resets/compacts independently, but every billed turn is appended here.
37
+ */
38
+ export class AdvisorTranscriptRecorder {
39
+ #manager: SessionManager | undefined;
40
+ #file: string | undefined;
41
+ /** Serializes the async open/close against synchronous appends so records land in order. */
42
+ #queue: Promise<void>;
43
+
44
+ /**
45
+ * @param after Optional barrier the queue starts behind — used on the advisor
46
+ * on→off→on toggle so a fresh recorder's first `open` waits for the prior
47
+ * recorder's `close` and the two never hold the same `__advisor.jsonl` at once.
48
+ */
49
+ constructor(
50
+ private readonly resolveSessionFile: () => string | undefined,
51
+ private readonly resolveCwd: () => string,
52
+ after?: Promise<unknown>,
53
+ ) {
54
+ this.#queue = after
55
+ ? after.then(
56
+ () => {},
57
+ () => {},
58
+ )
59
+ : Promise.resolve();
60
+ }
61
+
62
+ /**
63
+ * Persist one finalized advisor message. Assistant turns carry the usage the
64
+ * stats parser reads; tool results round out the Hub transcript; user deltas
65
+ * (the advisor's "session update" prompts) are persisted but flagged
66
+ * `synthetic`/agent-attributed so they never inflate user-message metrics.
67
+ * Non-conversational message kinds are skipped.
68
+ */
69
+ record(message: AgentMessage): void {
70
+ let persisted: Message;
71
+ switch (message.role) {
72
+ case "assistant":
73
+ case "toolResult":
74
+ persisted = message;
75
+ break;
76
+ case "user":
77
+ // Clone so the live advisor message stays untouched; mark synthetic so
78
+ // stats' user-message metrics skip these agent-internal review prompts.
79
+ persisted = { ...(message as UserMessage), synthetic: true, attribution: "agent" };
80
+ break;
81
+ default:
82
+ return;
83
+ }
84
+ const sessionFile = this.resolveSessionFile();
85
+ if (!sessionFile?.endsWith(JSONL_SUFFIX)) return;
86
+ const file = path.join(sessionFile.slice(0, -JSONL_SUFFIX.length), ADVISOR_TRANSCRIPT_FILENAME);
87
+ const cwd = this.resolveCwd();
88
+ this.#enqueue(async () => {
89
+ if (file !== this.#file) {
90
+ await this.#closeManager();
91
+ this.#manager = await SessionManager.open(file, undefined, undefined, {
92
+ initialCwd: cwd,
93
+ suppressBreadcrumb: true,
94
+ });
95
+ this.#file = file;
96
+ }
97
+ this.#manager?.appendMessage(persisted);
98
+ });
99
+ }
100
+
101
+ /** Flush pending writes (best-effort). */
102
+ flush(): Promise<void> {
103
+ return this.#enqueueResult(async () => {
104
+ if (this.#manager) await this.#manager.flush();
105
+ });
106
+ }
107
+
108
+ /** Flush and close the writer, releasing the session file. */
109
+ close(): Promise<void> {
110
+ return this.#enqueueResult(() => this.#closeManager());
111
+ }
112
+
113
+ async #closeManager(): Promise<void> {
114
+ const manager = this.#manager;
115
+ this.#manager = undefined;
116
+ this.#file = undefined;
117
+ if (!manager) return;
118
+ try {
119
+ await manager.close();
120
+ } catch (err) {
121
+ logger.debug("advisor transcript close failed", { err: String(err) });
122
+ }
123
+ }
124
+
125
+ #enqueue(work: () => Promise<void>): void {
126
+ this.#queue = this.#queue.then(work, work).catch(err => {
127
+ logger.debug("advisor transcript record failed", { err: String(err) });
128
+ });
129
+ }
130
+
131
+ #enqueueResult(work: () => Promise<void>): Promise<void> {
132
+ const next = this.#queue.then(work, work);
133
+ this.#queue = next.catch(() => {});
134
+ return next;
135
+ }
136
+ }
@@ -4,6 +4,7 @@
4
4
  * Handles `omp stats` subcommand for viewing AI usage statistics.
5
5
  */
6
6
 
7
+ import { truncateToWidth } from "@oh-my-pi/pi-tui/utils";
7
8
  import { APP_NAME, formatDuration, formatNumber, formatPercent } from "@oh-my-pi/pi-utils";
8
9
  import chalk from "chalk";
9
10
  import { openPath } from "../utils/open";
@@ -32,7 +33,7 @@ function createSyncProgressReporter(): {
32
33
  const counter = chalk.cyan(`[${event.current}/${event.total}]`);
33
34
  const line = `${counter} ${pct}% ${label}`;
34
35
  const columns = stream.columns ?? 120;
35
- const trimmed = truncateToColumns(line, columns - 1);
36
+ const trimmed = truncateToWidth(line, columns - 1);
36
37
  stream.write(`\r${trimmed.padEnd(lastWidth)}`);
37
38
  lastWidth = trimmed.length;
38
39
  },
@@ -50,16 +51,6 @@ function shortenSessionFile(p: string): string {
50
51
  return idx >= 0 ? p.slice(idx + marker.length) : p;
51
52
  }
52
53
 
53
- function truncateToColumns(s: string, max: number): string {
54
- if (max <= 0) return "";
55
- const width = Bun.stringWidth(s, { countAnsiEscapeCodes: false });
56
- if (width <= max) return s;
57
- // Cheap right-trim with an ellipsis - we don't need ANSI-aware slicing
58
- // because the colored prefix is short and the truncated tail is the
59
- // dim filename, where dropping bytes is fine.
60
- return `${s.slice(0, Math.max(0, max - 1))}\u2026`;
61
- }
62
-
63
54
  // =============================================================================
64
55
  // Types
65
56
  // =============================================================================
@@ -17,7 +17,7 @@ import { logger } from "@oh-my-pi/pi-utils";
17
17
  import type { BusChannel, AgentEvent as WireAgentEvent, SessionEntry as WireSessionEntry } from "@oh-my-pi/pi-wire";
18
18
  import type { InteractiveModeContext } from "../modes/types";
19
19
  import { AgentLifecycleManager } from "../registry/agent-lifecycle";
20
- import { AgentRegistry } from "../registry/agent-registry";
20
+ import { type AgentRef, AgentRegistry } from "../registry/agent-registry";
21
21
  import type { AgentSessionEvent } from "../session/agent-session";
22
22
  import { stripImagesFromMessage, USER_INTERRUPT_LABEL } from "../session/messages";
23
23
  import type { SessionEntry as StoredSessionEntry } from "../session/session-entries";
@@ -445,18 +445,24 @@ export class CollabHost {
445
445
  }
446
446
 
447
447
  #snapshotAgents(): AgentSnapshot[] {
448
- return AgentRegistry.global()
449
- .list()
450
- .map(ref => ({
451
- id: ref.id,
452
- displayName: ref.displayName,
453
- kind: ref.kind,
454
- parentId: ref.parentId,
455
- status: ref.status,
456
- hasSessionFile: !!ref.sessionFile,
457
- createdAt: ref.createdAt,
458
- lastActivity: ref.lastActivity,
459
- }));
448
+ return (
449
+ AgentRegistry.global()
450
+ .list()
451
+ // Advisor transcripts are local observability only; never mirror them to
452
+ // guests (the wire AgentSnapshot kind has no `advisor`, and guests must not
453
+ // be able to chat/kill/revive them).
454
+ .filter((ref): ref is AgentRef & { kind: "main" | "sub" } => ref.kind !== "advisor")
455
+ .map(ref => ({
456
+ id: ref.id,
457
+ displayName: ref.displayName,
458
+ kind: ref.kind,
459
+ parentId: ref.parentId,
460
+ status: ref.status,
461
+ hasSessionFile: !!ref.sessionFile,
462
+ createdAt: ref.createdAt,
463
+ lastActivity: ref.lastActivity,
464
+ }))
465
+ );
460
466
  }
461
467
 
462
468
  #scheduleAgentsBroadcast(): void {
@@ -472,6 +478,12 @@ export class CollabHost {
472
478
  this.#rejectReadOnly("agent control", fromPeer);
473
479
  return;
474
480
  }
481
+ // Advisor refs are excluded from snapshots, but reject control by id defensively:
482
+ // a stale/malicious client must never chat/kill/revive a read-only advisor transcript.
483
+ if (AgentRegistry.global().get(agentId)?.kind === "advisor") {
484
+ this.#socket?.send({ t: "error", message: `agent ${agentId}: advisor transcripts are read-only` }, fromPeer);
485
+ return;
486
+ }
475
487
  const fail = (err: unknown) => {
476
488
  logger.warn("collab agent-cmd failed", { cmd, agentId, error: String(err) });
477
489
  this.#socket?.send({ t: "error", message: `agent ${agentId}: ${String(err)}` }, fromPeer);