@oh-my-pi/pi-coding-agent 16.2.1 → 16.2.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.
- package/CHANGELOG.md +72 -0
- package/dist/cli.js +3621 -3579
- package/dist/types/advisor/__tests__/config.test.d.ts +1 -0
- package/dist/types/advisor/advise-tool.d.ts +8 -4
- package/dist/types/advisor/config.d.ts +88 -0
- package/dist/types/advisor/index.d.ts +1 -0
- package/dist/types/advisor/runtime.d.ts +15 -1
- package/dist/types/advisor/transcript-recorder.d.ts +13 -2
- package/dist/types/advisor/watchdog.d.ts +20 -0
- package/dist/types/collab/guest.d.ts +29 -0
- package/dist/types/config/model-roles.d.ts +1 -1
- package/dist/types/config/settings-schema.d.ts +113 -12
- package/dist/types/debug/log-viewer.d.ts +1 -0
- package/dist/types/debug/raw-sse.d.ts +1 -0
- package/dist/types/discovery/omp-extension-roots.d.ts +3 -3
- package/dist/types/edit/hashline/diff.d.ts +0 -11
- package/dist/types/edit/index.d.ts +18 -0
- package/dist/types/edit/streaming.d.ts +30 -0
- package/dist/types/extensibility/custom-tools/types.d.ts +1 -0
- package/dist/types/extensibility/shared-events.d.ts +1 -0
- package/dist/types/extensibility/tool-event-input.d.ts +7 -0
- package/dist/types/extensibility/utils.d.ts +12 -0
- package/dist/types/mcp/oauth-discovery.d.ts +0 -11
- package/dist/types/mcp/transports/index.d.ts +1 -0
- package/dist/types/mcp/transports/sse.d.ts +20 -0
- package/dist/types/modes/components/advisor-config.d.ts +59 -0
- package/dist/types/modes/components/index.d.ts +1 -0
- package/dist/types/modes/components/model-selector.d.ts +9 -1
- package/dist/types/modes/components/settings-selector.d.ts +1 -0
- package/dist/types/modes/components/status-line/component.d.ts +30 -3
- package/dist/types/modes/components/status-line/types.d.ts +13 -1
- package/dist/types/modes/controllers/selector-controller.d.ts +1 -0
- package/dist/types/modes/interactive-mode.d.ts +10 -4
- package/dist/types/modes/skill-command.d.ts +32 -0
- package/dist/types/modes/types.d.ts +7 -2
- package/dist/types/sdk.d.ts +1 -1
- package/dist/types/session/agent-session.d.ts +84 -12
- package/dist/types/session/indexed-session-storage.d.ts +7 -1
- package/dist/types/session/messages.d.ts +32 -7
- package/dist/types/session/messages.test.d.ts +1 -0
- package/dist/types/session/session-entries.d.ts +31 -3
- package/dist/types/session/session-history-format.d.ts +6 -0
- package/dist/types/session/session-loader.d.ts +9 -1
- package/dist/types/session/session-manager.d.ts +6 -4
- package/dist/types/session/session-storage.d.ts +11 -0
- package/dist/types/session/session-title-slot.d.ts +19 -0
- package/dist/types/session/settings-stream-fn.d.ts +21 -0
- package/dist/types/session/turn-persistence.d.ts +88 -0
- package/dist/types/ssh/connection-manager.d.ts +47 -0
- package/dist/types/ssh/utils.d.ts +16 -0
- package/dist/types/task/executor.d.ts +3 -16
- package/dist/types/task/render.d.ts +0 -5
- package/dist/types/task/renderer.d.ts +13 -0
- package/dist/types/task/types.d.ts +16 -0
- package/dist/types/task/yield-assembly.d.ts +28 -0
- package/dist/types/tiny/text.d.ts +8 -0
- package/dist/types/tools/render-utils.d.ts +2 -0
- package/dist/types/tools/review.d.ts +6 -4
- package/dist/types/tools/ssh.d.ts +1 -1
- package/dist/types/tools/todo.d.ts +6 -0
- package/dist/types/tools/yield.d.ts +8 -3
- package/dist/types/utils/thinking-display.d.ts +4 -0
- package/package.json +12 -12
- package/src/advisor/__tests__/advisor.test.ts +438 -10
- package/src/advisor/__tests__/config.test.ts +173 -0
- package/src/advisor/advise-tool.ts +11 -6
- package/src/advisor/config.ts +256 -0
- package/src/advisor/index.ts +1 -0
- package/src/advisor/runtime.ts +77 -4
- package/src/advisor/transcript-recorder.ts +25 -2
- package/src/advisor/watchdog.ts +57 -31
- package/src/auto-thinking/classifier.ts +2 -2
- package/src/autoresearch/index.ts +7 -2
- package/src/cli/gc-cli.ts +17 -10
- package/src/collab/guest.ts +43 -7
- package/src/config/model-registry.ts +80 -18
- package/src/config/model-resolver.ts +5 -1
- package/src/config/model-roles.ts +3 -3
- package/src/config/settings-schema.ts +107 -8
- package/src/debug/index.ts +32 -7
- package/src/debug/log-viewer.ts +111 -53
- package/src/debug/raw-sse.ts +68 -48
- package/src/discovery/codex.ts +13 -5
- package/src/discovery/omp-extension-roots.ts +38 -13
- package/src/edit/hashline/diff.ts +57 -4
- package/src/edit/index.ts +21 -0
- package/src/edit/streaming.ts +170 -0
- package/src/eval/js/shared/local-module-loader.ts +23 -1
- package/src/export/html/template.js +13 -7
- package/src/extensibility/custom-tools/types.ts +1 -0
- package/src/extensibility/extensions/loader.ts +5 -3
- package/src/extensibility/extensions/wrapper.ts +9 -3
- package/src/extensibility/hooks/loader.ts +3 -3
- package/src/extensibility/hooks/tool-wrapper.ts +13 -4
- package/src/extensibility/plugins/legacy-pi-bundled-keys.ts +18 -1
- package/src/extensibility/plugins/legacy-pi-bundled-registry.ts +59 -2
- package/src/extensibility/plugins/manager.ts +76 -5
- package/src/extensibility/shared-events.ts +1 -0
- package/src/extensibility/tool-event-input.ts +23 -0
- package/src/extensibility/utils.ts +74 -0
- package/src/internal-urls/docs-index.generated.txt +1 -1
- package/src/mcp/client.ts +3 -1
- package/src/mcp/manager.ts +12 -5
- package/src/mcp/oauth-discovery.ts +5 -29
- package/src/mcp/transports/http.ts +3 -1
- package/src/mcp/transports/index.ts +1 -0
- package/src/mcp/transports/sse.ts +377 -0
- package/src/memories/index.ts +15 -6
- package/src/mnemopi/backend.ts +2 -2
- package/src/modes/acp/acp-agent.ts +1 -1
- package/src/modes/components/advisor-config.ts +555 -0
- package/src/modes/components/advisor-message.ts +9 -2
- package/src/modes/components/agent-hub.ts +9 -4
- package/src/modes/components/assistant-message.ts +5 -5
- package/src/modes/components/index.ts +2 -0
- package/src/modes/components/model-selector.ts +79 -48
- package/src/modes/components/settings-selector.ts +1 -0
- package/src/modes/components/status-line/component.ts +145 -14
- package/src/modes/components/status-line/segments.ts +47 -22
- package/src/modes/components/status-line/types.ts +13 -1
- package/src/modes/components/tool-execution.ts +47 -6
- package/src/modes/controllers/command-controller.ts +23 -2
- package/src/modes/controllers/event-controller.ts +114 -11
- package/src/modes/controllers/extension-ui-controller.ts +1 -1
- package/src/modes/controllers/input-controller.ts +61 -61
- package/src/modes/controllers/selector-controller.ts +100 -9
- package/src/modes/interactive-mode.ts +65 -10
- package/src/modes/print-mode.ts +1 -1
- package/src/modes/skill-command.ts +116 -0
- package/src/modes/types.ts +7 -2
- package/src/modes/utils/transcript-render-helpers.ts +2 -2
- package/src/modes/utils/ui-helpers.ts +46 -27
- package/src/prompts/agents/reviewer.md +11 -10
- package/src/prompts/review-custom-request.md +1 -2
- package/src/prompts/review-request.md +1 -2
- package/src/prompts/system/interrupted-thinking.md +7 -0
- package/src/prompts/system/recap-user.md +9 -0
- package/src/prompts/system/subagent-system-prompt.md +8 -5
- package/src/prompts/system/subagent-yield-reminder.md +6 -5
- package/src/prompts/system/system-prompt.md +0 -1
- package/src/prompts/tools/irc.md +2 -2
- package/src/prompts/tools/read.md +2 -2
- package/src/sdk.ts +40 -50
- package/src/session/agent-session.ts +1139 -600
- package/src/session/indexed-session-storage.ts +86 -13
- package/src/session/messages.test.ts +125 -0
- package/src/session/messages.ts +192 -21
- package/src/session/redis-session-storage.ts +49 -2
- package/src/session/session-entries.ts +39 -2
- package/src/session/session-history-format.ts +29 -2
- package/src/session/session-listing.ts +54 -24
- package/src/session/session-loader.ts +66 -3
- package/src/session/session-manager.ts +113 -19
- package/src/session/session-persistence.ts +96 -3
- package/src/session/session-storage.ts +36 -0
- package/src/session/session-title-slot.ts +141 -0
- package/src/session/settings-stream-fn.ts +49 -0
- package/src/session/sql-session-storage.ts +71 -11
- package/src/session/turn-persistence.ts +142 -0
- package/src/session/unexpected-stop-classifier.ts +2 -2
- package/src/slash-commands/builtin-registry.ts +16 -3
- package/src/slash-commands/helpers/mcp.ts +2 -1
- package/src/ssh/__tests__/connection-manager-args.test.ts +123 -1
- package/src/ssh/__tests__/file-transfer-posix-guard.test.ts +55 -18
- package/src/ssh/connection-manager.ts +139 -12
- package/src/ssh/file-transfer.ts +23 -18
- package/src/ssh/ssh-executor.ts +2 -13
- package/src/ssh/utils.ts +19 -0
- package/src/task/executor.ts +21 -23
- package/src/task/render.ts +162 -20
- package/src/task/renderer.ts +14 -0
- package/src/task/types.ts +17 -0
- package/src/task/yield-assembly.ts +207 -0
- package/src/tiny/models.ts +8 -6
- package/src/tiny/text.ts +37 -7
- package/src/tools/ask.ts +55 -4
- package/src/tools/image-gen.ts +2 -1
- package/src/tools/render-utils.ts +2 -0
- package/src/tools/renderers.ts +8 -2
- package/src/tools/review.ts +17 -7
- package/src/tools/ssh.ts +8 -4
- package/src/tools/todo.ts +17 -1
- package/src/tools/tts.ts +2 -1
- package/src/tools/yield.ts +140 -31
- package/src/utils/thinking-display.ts +15 -0
- package/src/utils/title-generator.ts +1 -1
- package/src/web/search/providers/tavily.ts +36 -19
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
type SessionStorageBackend,
|
|
5
5
|
type SessionStorageIndexEntry,
|
|
6
6
|
} from "./indexed-session-storage";
|
|
7
|
+
import type { SessionTitleUpdate } from "./session-title-slot";
|
|
7
8
|
|
|
8
9
|
/**
|
|
9
10
|
* Minimal subset of the `bun:redis` `RedisClient` surface used by
|
|
@@ -44,6 +45,28 @@ export interface RedisSessionStorageOptions {
|
|
|
44
45
|
const DEFAULT_PREFIX = "omp:sessions:";
|
|
45
46
|
const DEFAULT_SCAN_COUNT = 500;
|
|
46
47
|
|
|
48
|
+
function encodeTitleMeta(title: SessionTitleUpdate): string {
|
|
49
|
+
return JSON.stringify(title);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function decodeTitleMeta(raw: string | undefined): SessionTitleUpdate | undefined {
|
|
53
|
+
if (!raw) return undefined;
|
|
54
|
+
try {
|
|
55
|
+
const parsed = JSON.parse(raw) as unknown;
|
|
56
|
+
if (typeof parsed !== "object" || parsed === null) return undefined;
|
|
57
|
+
const record = parsed as Record<string, unknown>;
|
|
58
|
+
if (typeof record.updatedAt !== "string") return undefined;
|
|
59
|
+
const source = record.source === "auto" || record.source === "user" ? record.source : undefined;
|
|
60
|
+
return {
|
|
61
|
+
title: typeof record.title === "string" ? record.title : undefined,
|
|
62
|
+
source,
|
|
63
|
+
updatedAt: record.updatedAt,
|
|
64
|
+
};
|
|
65
|
+
} catch {
|
|
66
|
+
return undefined;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
47
70
|
/**
|
|
48
71
|
* Redis-backed implementation of {@link SessionStorage}. Each session JSONL
|
|
49
72
|
* file maps to a Redis STRING key, with per-key metadata (mtime) tracked in a
|
|
@@ -83,8 +106,9 @@ class RedisSessionStorageBackend implements SessionStorageBackend {
|
|
|
83
106
|
async loadIndex(): Promise<SessionStorageIndexEntry[]> {
|
|
84
107
|
const filePrefix = this.#fileKey("");
|
|
85
108
|
const metaRaw = await this.#client.hgetall(this.#metaKey());
|
|
109
|
+
const titleRaw = await this.#client.hgetall(this.#titleMetaKey());
|
|
86
110
|
const meta: Record<string, string> = metaRaw ?? {};
|
|
87
|
-
|
|
111
|
+
const titles: Record<string, string> = titleRaw ?? {};
|
|
88
112
|
const seen = new Set<string>();
|
|
89
113
|
let cursor = "0";
|
|
90
114
|
do {
|
|
@@ -106,10 +130,14 @@ class RedisSessionStorageBackend implements SessionStorageBackend {
|
|
|
106
130
|
const size = await this.#client.strlen(key);
|
|
107
131
|
const rawMtime = meta[path];
|
|
108
132
|
const parsedMtime = rawMtime === undefined ? Number.NaN : Number(rawMtime);
|
|
133
|
+
const title = decodeTitleMeta(titles[path]);
|
|
109
134
|
return {
|
|
110
135
|
path,
|
|
111
136
|
size,
|
|
112
137
|
mtimeMs: Number.isFinite(parsedMtime) ? parsedMtime : fallbackMtimeMs,
|
|
138
|
+
title: title?.title,
|
|
139
|
+
titleSource: title?.source,
|
|
140
|
+
titleUpdatedAt: title?.updatedAt,
|
|
113
141
|
};
|
|
114
142
|
}),
|
|
115
143
|
);
|
|
@@ -126,9 +154,14 @@ class RedisSessionStorageBackend implements SessionStorageBackend {
|
|
|
126
154
|
return Promise.all([head, tail]);
|
|
127
155
|
}
|
|
128
156
|
|
|
129
|
-
async writeFull(path: string, content: string, mtimeMs: number): Promise<void> {
|
|
157
|
+
async writeFull(path: string, content: string, mtimeMs: number, title?: SessionTitleUpdate): Promise<void> {
|
|
130
158
|
await this.#client.set(this.#fileKey(path), content);
|
|
131
159
|
await this.#client.hset(this.#metaKey(), path, String(mtimeMs));
|
|
160
|
+
if (title) {
|
|
161
|
+
await this.#client.hset(this.#titleMetaKey(), path, encodeTitleMeta(title));
|
|
162
|
+
} else {
|
|
163
|
+
await this.#client.hdel(this.#titleMetaKey(), path);
|
|
164
|
+
}
|
|
132
165
|
}
|
|
133
166
|
|
|
134
167
|
async append(path: string, line: string, mtimeMs: number): Promise<void> {
|
|
@@ -136,6 +169,11 @@ class RedisSessionStorageBackend implements SessionStorageBackend {
|
|
|
136
169
|
await this.#client.hset(this.#metaKey(), path, String(mtimeMs));
|
|
137
170
|
}
|
|
138
171
|
|
|
172
|
+
async updateSessionTitle(path: string, title: SessionTitleUpdate, mtimeMs: number): Promise<void> {
|
|
173
|
+
await this.#client.hset(this.#metaKey(), path, String(mtimeMs));
|
|
174
|
+
await this.#client.hset(this.#titleMetaKey(), path, encodeTitleMeta(title));
|
|
175
|
+
}
|
|
176
|
+
|
|
139
177
|
async truncate(path: string, mtimeMs: number): Promise<void> {
|
|
140
178
|
await this.writeFull(path, "", mtimeMs);
|
|
141
179
|
}
|
|
@@ -144,13 +182,18 @@ class RedisSessionStorageBackend implements SessionStorageBackend {
|
|
|
144
182
|
if (paths.length === 0) return;
|
|
145
183
|
await this.#client.del(...paths.map(path => this.#fileKey(path)));
|
|
146
184
|
await this.#client.hdel(this.#metaKey(), ...paths);
|
|
185
|
+
await this.#client.hdel(this.#titleMetaKey(), ...paths);
|
|
147
186
|
}
|
|
148
187
|
|
|
149
188
|
async move(src: string, dst: string, mtimeMs: number): Promise<void> {
|
|
150
189
|
await this.#client.rename(this.#fileKey(src), this.#fileKey(dst));
|
|
151
190
|
try {
|
|
191
|
+
const titleMeta = await this.#client.hgetall(this.#titleMetaKey());
|
|
152
192
|
await this.#client.hdel(this.#metaKey(), src);
|
|
153
193
|
await this.#client.hset(this.#metaKey(), dst, String(mtimeMs));
|
|
194
|
+
await this.#client.hdel(this.#titleMetaKey(), src);
|
|
195
|
+
const title = titleMeta[src];
|
|
196
|
+
if (title !== undefined) await this.#client.hset(this.#titleMetaKey(), dst, title);
|
|
154
197
|
} catch (err) {
|
|
155
198
|
logger.warn("Redis session storage meta rename failed", {
|
|
156
199
|
src,
|
|
@@ -167,4 +210,8 @@ class RedisSessionStorageBackend implements SessionStorageBackend {
|
|
|
167
210
|
#metaKey(): string {
|
|
168
211
|
return `${this.#prefix}meta`;
|
|
169
212
|
}
|
|
213
|
+
|
|
214
|
+
#titleMetaKey(): string {
|
|
215
|
+
return `${this.#prefix}title`;
|
|
216
|
+
}
|
|
170
217
|
}
|
|
@@ -3,6 +3,24 @@ import type { ImageContent, MessageAttribution, ServiceTier, TextContent } from
|
|
|
3
3
|
|
|
4
4
|
export const CURRENT_SESSION_VERSION = 3;
|
|
5
5
|
|
|
6
|
+
export const SESSION_TITLE_SLOT_BYTES = 256;
|
|
7
|
+
|
|
8
|
+
export const SESSION_TITLE_SLOT_ENTRY_TYPE = "title";
|
|
9
|
+
|
|
10
|
+
export const TITLE_CHANGE_ENTRY_TYPE = "title_change";
|
|
11
|
+
|
|
12
|
+
export type SessionTitleSource = "auto" | "user";
|
|
13
|
+
|
|
14
|
+
/** Fixed-width first-line slot carrying the mutable current session title. */
|
|
15
|
+
export interface SessionTitleSlotEntry {
|
|
16
|
+
type: typeof SESSION_TITLE_SLOT_ENTRY_TYPE;
|
|
17
|
+
v: 1;
|
|
18
|
+
title: string;
|
|
19
|
+
source?: SessionTitleSource;
|
|
20
|
+
updatedAt: string;
|
|
21
|
+
pad: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
6
24
|
export const EPHEMERAL_MODEL_CHANGE_ROLE = "fallback";
|
|
7
25
|
|
|
8
26
|
export interface SessionHeader {
|
|
@@ -10,7 +28,7 @@ export interface SessionHeader {
|
|
|
10
28
|
version?: number; // v1 sessions don't have this
|
|
11
29
|
id: string;
|
|
12
30
|
title?: string; // Auto-generated title from first message
|
|
13
|
-
titleSource?:
|
|
31
|
+
titleSource?: SessionTitleSource;
|
|
14
32
|
timestamp: string;
|
|
15
33
|
cwd: string;
|
|
16
34
|
parentSession?: string;
|
|
@@ -105,6 +123,21 @@ export interface LabelEntry extends SessionEntryBase {
|
|
|
105
123
|
label: string | undefined;
|
|
106
124
|
}
|
|
107
125
|
|
|
126
|
+
/** Append-only audit entry recording a session title change. */
|
|
127
|
+
export interface TitleChangeEntry extends SessionEntryBase {
|
|
128
|
+
type: typeof TITLE_CHANGE_ENTRY_TYPE;
|
|
129
|
+
title: string;
|
|
130
|
+
previousTitle?: string;
|
|
131
|
+
source: SessionTitleSource;
|
|
132
|
+
trigger?: string;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
declare module "@oh-my-pi/pi-agent-core/compaction/entries" {
|
|
136
|
+
interface CustomCompactionSessionEntries {
|
|
137
|
+
titleChange: TitleChangeEntry;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
108
141
|
/** TTSR injection entry - tracks which time-traveling rules have been injected this session. */
|
|
109
142
|
export interface TtsrInjectionEntry extends SessionEntryBase {
|
|
110
143
|
type: "ttsr_injection";
|
|
@@ -178,14 +211,18 @@ export type SessionEntry =
|
|
|
178
211
|
| CustomEntry
|
|
179
212
|
| CustomMessageEntry
|
|
180
213
|
| LabelEntry
|
|
214
|
+
| TitleChangeEntry
|
|
181
215
|
| TtsrInjectionEntry
|
|
182
216
|
| MCPToolSelectionEntry
|
|
183
217
|
| SessionInitEntry
|
|
184
218
|
| ModeChangeEntry;
|
|
185
219
|
|
|
186
|
-
/** Raw file entry
|
|
220
|
+
/** Raw logical file entry after loaders strip any fixed-width title slot. */
|
|
187
221
|
export type FileEntry = SessionHeader | SessionEntry;
|
|
188
222
|
|
|
223
|
+
/** Physical JSONL entry before slot-aware loaders fold the title slot. */
|
|
224
|
+
export type RawFileEntry = SessionTitleSlotEntry | FileEntry;
|
|
225
|
+
|
|
189
226
|
/** Tree node for getTree() - defensive copy of session structure */
|
|
190
227
|
export interface SessionTreeNode {
|
|
191
228
|
entry: SessionEntry;
|
|
@@ -40,6 +40,12 @@ export interface HistoryFormatOptions {
|
|
|
40
40
|
* still collapse to a one-liner.
|
|
41
41
|
*/
|
|
42
42
|
expandPrimaryContext?: boolean;
|
|
43
|
+
/**
|
|
44
|
+
* Append the full unified diff (from a tool result's `details.diff`) below
|
|
45
|
+
* edit/apply_patch tool lines, instead of just the path. The advisor sets
|
|
46
|
+
* this so it sees what changed without re-reading the file.
|
|
47
|
+
*/
|
|
48
|
+
expandEditDiffs?: boolean;
|
|
43
49
|
}
|
|
44
50
|
|
|
45
51
|
/** Max length of the primary-arg summary inside `→ tool(...)` lines. */
|
|
@@ -139,12 +145,24 @@ function primaryArg(name: string, args: Record<string, unknown> | undefined): st
|
|
|
139
145
|
}
|
|
140
146
|
}
|
|
141
147
|
|
|
148
|
+
/**
|
|
149
|
+
* Wrap a diff body in a backtick fence sized to outlast the longest backtick
|
|
150
|
+
* run inside it, so a diff that touches markdown (triple backticks) can't break
|
|
151
|
+
* out of the fence. Info string `diff` for syntax highlighting.
|
|
152
|
+
*/
|
|
153
|
+
function fenceDiff(diff: string): string {
|
|
154
|
+
const longest = diff.match(/`+/g)?.reduce((m, run) => Math.max(m, run.length), 0) ?? 0;
|
|
155
|
+
const fence = "`".repeat(Math.max(3, longest + 1));
|
|
156
|
+
return `${fence}diff\n${diff}\n${fence}`;
|
|
157
|
+
}
|
|
158
|
+
|
|
142
159
|
/** One line per tool call: `→ read(src/foo.ts:50-80) ⇒ ok · 31 lines`. */
|
|
143
160
|
function toolCallLine(
|
|
144
161
|
name: string,
|
|
145
162
|
args: Record<string, unknown> | undefined,
|
|
146
163
|
result: ToolResultMessage | undefined,
|
|
147
164
|
includeToolIntent?: boolean,
|
|
165
|
+
expandEditDiffs?: boolean,
|
|
148
166
|
): string {
|
|
149
167
|
const head = `→ ${name}(${primaryArg(name, args)})`;
|
|
150
168
|
let base: string;
|
|
@@ -162,6 +180,13 @@ function toolCallLine(
|
|
|
162
180
|
}
|
|
163
181
|
}
|
|
164
182
|
|
|
183
|
+
if (expandEditDiffs) {
|
|
184
|
+
const diff = (result?.details as { diff?: unknown } | undefined)?.diff;
|
|
185
|
+
if (typeof diff === "string" && diff.trim()) {
|
|
186
|
+
base = `${base}\n${fenceDiff(diff)}`;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
165
190
|
const intent = includeToolIntent ? args?.[INTENT_FIELD] : undefined;
|
|
166
191
|
if (typeof intent === "string" && intent.trim()) {
|
|
167
192
|
const formattedIntent = oneLine(intent, 80);
|
|
@@ -280,7 +305,9 @@ export function formatSessionHistoryMarkdown(messages: unknown[], opts?: History
|
|
|
280
305
|
} else if (block.type === "toolCall") {
|
|
281
306
|
const result = resultsByCallId.get(block.id);
|
|
282
307
|
if (result) consumed.add(block.id);
|
|
283
|
-
body.push(
|
|
308
|
+
body.push(
|
|
309
|
+
toolCallLine(block.name, block.arguments, result, opts?.includeToolIntent, opts?.expandEditDiffs),
|
|
310
|
+
);
|
|
284
311
|
} else if (opts?.includeThinking && block.type === "thinking" && block.thinking.trim()) {
|
|
285
312
|
body.push(`_thinking:_ ${block.thinking}`);
|
|
286
313
|
}
|
|
@@ -303,7 +330,7 @@ export function formatSessionHistoryMarkdown(messages: unknown[], opts?: History
|
|
|
303
330
|
case "toolResult": {
|
|
304
331
|
// Normally consumed by its toolCall; orphans (e.g. truncated history) get their own line.
|
|
305
332
|
if (consumed.has(msg.toolCallId)) break;
|
|
306
|
-
lines.push(toolCallLine(msg.toolName, undefined, msg, opts?.includeToolIntent), "");
|
|
333
|
+
lines.push(toolCallLine(msg.toolName, undefined, msg, opts?.includeToolIntent, opts?.expandEditDiffs), "");
|
|
307
334
|
lastWatchedLabel = undefined;
|
|
308
335
|
break;
|
|
309
336
|
}
|
|
@@ -270,39 +270,69 @@ interface SessionListHeader {
|
|
|
270
270
|
timestamp?: string;
|
|
271
271
|
}
|
|
272
272
|
|
|
273
|
-
function
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
const parsedHeader = entries[0];
|
|
278
|
-
if (parsedHeader?.type === "session" && typeof parsedHeader.id === "string") {
|
|
279
|
-
return {
|
|
280
|
-
type: "session",
|
|
281
|
-
id: parsedHeader.id,
|
|
282
|
-
cwd: typeof parsedHeader.cwd === "string" ? parsedHeader.cwd : undefined,
|
|
283
|
-
title: typeof parsedHeader.title === "string" ? parsedHeader.title : undefined,
|
|
284
|
-
parentSession: typeof parsedHeader.parentSession === "string" ? parsedHeader.parentSession : undefined,
|
|
285
|
-
timestamp: typeof parsedHeader.timestamp === "string" ? parsedHeader.timestamp : undefined,
|
|
286
|
-
};
|
|
287
|
-
}
|
|
273
|
+
function normalizeTitleOverride(title: string | undefined): string | null | undefined {
|
|
274
|
+
if (title === undefined) return undefined;
|
|
275
|
+
return title.trim() ? title : null;
|
|
276
|
+
}
|
|
288
277
|
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
278
|
+
function sessionListHeaderFromRecord(
|
|
279
|
+
record: Record<string, unknown> | undefined,
|
|
280
|
+
titleOverride?: string | null,
|
|
281
|
+
): SessionListHeader | undefined {
|
|
282
|
+
if (record?.type !== "session" || typeof record.id !== "string") return undefined;
|
|
283
|
+
return {
|
|
284
|
+
type: "session",
|
|
285
|
+
id: record.id,
|
|
286
|
+
cwd: typeof record.cwd === "string" ? record.cwd : undefined,
|
|
287
|
+
title:
|
|
288
|
+
titleOverride === null
|
|
289
|
+
? undefined
|
|
290
|
+
: (titleOverride ?? (typeof record.title === "string" ? record.title : undefined)),
|
|
291
|
+
parentSession: typeof record.parentSession === "string" ? record.parentSession : undefined,
|
|
292
|
+
timestamp: typeof record.timestamp === "string" ? record.timestamp : undefined,
|
|
293
|
+
};
|
|
294
|
+
}
|
|
292
295
|
|
|
293
|
-
|
|
296
|
+
function parseSessionListHeaderLine(line: string, titleOverride?: string | null): SessionListHeader | undefined {
|
|
297
|
+
if (extractStringProperty(line, "type") !== "session") return undefined;
|
|
298
|
+
const id = extractStringProperty(line, "id");
|
|
294
299
|
if (!id) return undefined;
|
|
295
|
-
|
|
296
300
|
return {
|
|
297
301
|
type: "session",
|
|
298
302
|
id,
|
|
299
|
-
cwd: extractStringProperty(
|
|
300
|
-
title: extractStringProperty(
|
|
301
|
-
parentSession: extractStringProperty(
|
|
302
|
-
timestamp: extractStringProperty(
|
|
303
|
+
cwd: extractStringProperty(line, "cwd"),
|
|
304
|
+
title: titleOverride === null ? undefined : (titleOverride ?? extractStringProperty(line, "title")),
|
|
305
|
+
parentSession: extractStringProperty(line, "parentSession"),
|
|
306
|
+
timestamp: extractStringProperty(line, "timestamp"),
|
|
303
307
|
};
|
|
304
308
|
}
|
|
305
309
|
|
|
310
|
+
function parseSessionListHeader(
|
|
311
|
+
content: string,
|
|
312
|
+
entries: Array<Record<string, unknown>>,
|
|
313
|
+
): SessionListHeader | undefined {
|
|
314
|
+
const firstEntry = entries[0];
|
|
315
|
+
const parsedSlotTitle = normalizeTitleOverride(
|
|
316
|
+
firstEntry?.type === "title" && typeof firstEntry.title === "string" ? firstEntry.title : undefined,
|
|
317
|
+
);
|
|
318
|
+
const parsedHeader = sessionListHeaderFromRecord(entries[firstEntry?.type === "title" ? 1 : 0], parsedSlotTitle);
|
|
319
|
+
if (parsedHeader) return parsedHeader;
|
|
320
|
+
|
|
321
|
+
let slotTitle: string | null | undefined;
|
|
322
|
+
let firstNonEmpty = true;
|
|
323
|
+
for (const rawLine of content.split(/\r?\n/)) {
|
|
324
|
+
const line = rawLine.trim();
|
|
325
|
+
if (!line) continue;
|
|
326
|
+
if (firstNonEmpty && extractStringProperty(line, "type") === "title") {
|
|
327
|
+
slotTitle = normalizeTitleOverride(extractStringProperty(line, "title"));
|
|
328
|
+
firstNonEmpty = false;
|
|
329
|
+
continue;
|
|
330
|
+
}
|
|
331
|
+
return parseSessionListHeaderLine(line, slotTitle);
|
|
332
|
+
}
|
|
333
|
+
return undefined;
|
|
334
|
+
}
|
|
335
|
+
|
|
306
336
|
function getSessionListWorkerCount(fileCount: number): number {
|
|
307
337
|
if (fileCount <= SESSION_LIST_PARALLEL_THRESHOLD) return 1;
|
|
308
338
|
return Math.min(
|
|
@@ -2,14 +2,77 @@ import type { AgentMessage } from "@oh-my-pi/pi-agent-core";
|
|
|
2
2
|
import { getBlobsDir, isEnoent, parseJsonlLenient } from "@oh-my-pi/pi-utils";
|
|
3
3
|
import { BlobStore, isBlobRef, resolveImageData, resolveImageDataUrl } from "./blob-store";
|
|
4
4
|
import { buildSessionContext } from "./session-context";
|
|
5
|
-
import
|
|
5
|
+
import {
|
|
6
|
+
type FileEntry,
|
|
7
|
+
type RawFileEntry,
|
|
8
|
+
SESSION_TITLE_SLOT_BYTES,
|
|
9
|
+
type SessionEntry,
|
|
10
|
+
type SessionHeader,
|
|
11
|
+
type SessionTitleSlotEntry,
|
|
12
|
+
} from "./session-entries";
|
|
6
13
|
import { migrateToCurrentVersion } from "./session-migrations";
|
|
7
14
|
import { isImageBlock, isImageDataPayload } from "./session-persistence";
|
|
8
15
|
import { FileSessionStorage, type SessionStorage } from "./session-storage";
|
|
16
|
+
import {
|
|
17
|
+
parseTitleSlotFromContent,
|
|
18
|
+
parseTitleSlotLine,
|
|
19
|
+
type SessionTitleUpdate,
|
|
20
|
+
titleUpdateFromSlot,
|
|
21
|
+
} from "./session-title-slot";
|
|
9
22
|
|
|
23
|
+
function splitTitleSlot(content: string): { body: string; slot: SessionTitleUpdate | undefined } {
|
|
24
|
+
const slot = titleUpdateFromSlot(parseTitleSlotFromContent(content));
|
|
25
|
+
if (!slot) return { body: content, slot: undefined };
|
|
26
|
+
const newlineIndex = content.indexOf("\n");
|
|
27
|
+
return { body: content.slice(newlineIndex + 1), slot };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function foldTitleSlot(entries: FileEntry[], slot: SessionTitleUpdate | undefined): FileEntry[] {
|
|
31
|
+
if (!slot || entries.length === 0) return entries;
|
|
32
|
+
const header = entries[0] as SessionHeader;
|
|
33
|
+
if (header.type !== "session" || typeof header.id !== "string") return entries;
|
|
34
|
+
if (slot.title && slot.title.length > 0) {
|
|
35
|
+
header.title = slot.title;
|
|
36
|
+
} else {
|
|
37
|
+
delete header.title;
|
|
38
|
+
}
|
|
39
|
+
if (slot.source) {
|
|
40
|
+
header.titleSource = slot.source;
|
|
41
|
+
} else {
|
|
42
|
+
delete header.titleSource;
|
|
43
|
+
}
|
|
44
|
+
return entries;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Parse session JSONL while stripping and folding the optional fixed title slot. */
|
|
48
|
+
export function parseSessionContent(content: string): {
|
|
49
|
+
entries: FileEntry[];
|
|
50
|
+
titleSlot: SessionTitleUpdate | undefined;
|
|
51
|
+
} {
|
|
52
|
+
const { body, slot } = splitTitleSlot(content);
|
|
53
|
+
const entries = parseJsonlLenient<RawFileEntry>(body) as FileEntry[];
|
|
54
|
+
return { entries: foldTitleSlot(entries, slot), titleSlot: slot };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Read only the fixed-size head window to detect a physical title slot. */
|
|
58
|
+
export async function readTitleSlotFromFile(
|
|
59
|
+
filePath: string,
|
|
60
|
+
storage: SessionStorage = new FileSessionStorage(),
|
|
61
|
+
): Promise<SessionTitleSlotEntry | undefined> {
|
|
62
|
+
let head: string;
|
|
63
|
+
try {
|
|
64
|
+
[head] = await storage.readTextSlices(filePath, SESSION_TITLE_SLOT_BYTES, 0);
|
|
65
|
+
} catch (err) {
|
|
66
|
+
if (isEnoent(err)) return undefined;
|
|
67
|
+
throw err;
|
|
68
|
+
}
|
|
69
|
+
const newlineIndex = head.indexOf("\n");
|
|
70
|
+
if (newlineIndex < 0) return undefined;
|
|
71
|
+
return parseTitleSlotLine(head.slice(0, newlineIndex));
|
|
72
|
+
}
|
|
10
73
|
/** Exported for compaction.test.ts */
|
|
11
74
|
export function parseSessionEntries(content: string): FileEntry[] {
|
|
12
|
-
return
|
|
75
|
+
return parseSessionContent(content).entries;
|
|
13
76
|
}
|
|
14
77
|
|
|
15
78
|
/** Exported for testing */
|
|
@@ -24,7 +87,7 @@ export async function loadEntriesFromFile(
|
|
|
24
87
|
if (isEnoent(err)) return [];
|
|
25
88
|
throw err;
|
|
26
89
|
}
|
|
27
|
-
const entries =
|
|
90
|
+
const { entries } = parseSessionContent(content);
|
|
28
91
|
|
|
29
92
|
// Validate session header
|
|
30
93
|
if (entries.length === 0) return entries;
|