@oh-my-pi/pi-coding-agent 16.2.5 → 16.2.6
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 +18 -1
- package/dist/cli.js +2991 -3011
- package/dist/types/cli/bench-cli.d.ts +6 -1
- package/dist/types/commands/bench.d.ts +8 -0
- package/dist/types/config/model-discovery.d.ts +6 -1
- package/dist/types/config/settings-schema.d.ts +1 -1
- package/dist/types/edit/index.d.ts +1 -0
- package/dist/types/edit/renderer.d.ts +4 -0
- package/dist/types/edit/snapshot-details.d.ts +33 -0
- package/dist/types/mcp/oauth-flow.d.ts +4 -6
- package/dist/types/modes/controllers/tool-args-reveal.d.ts +9 -2
- package/dist/types/session/agent-session.d.ts +4 -0
- package/dist/types/session/session-manager.d.ts +3 -4
- package/dist/types/web/search/providers/duckduckgo.d.ts +2 -2
- package/dist/types/web/search/types.d.ts +1 -1
- package/package.json +12 -12
- package/src/cli/args.ts +32 -1
- package/src/cli/bench-cli.ts +89 -21
- package/src/cli/web-search-cli.ts +6 -1
- package/src/commands/bench.ts +10 -1
- package/src/config/mcp-schema.json +1 -1
- package/src/config/model-discovery.ts +66 -8
- package/src/config/model-registry.ts +13 -6
- package/src/edit/hashline/execute.ts +15 -9
- package/src/edit/index.ts +19 -6
- package/src/edit/modes/patch.ts +3 -2
- package/src/edit/modes/replace.ts +3 -2
- package/src/edit/renderer.ts +4 -0
- package/src/edit/snapshot-details.ts +77 -0
- package/src/extensibility/plugins/legacy-pi-compat.ts +2 -2
- package/src/internal-urls/docs-index.generated.txt +1 -1
- package/src/mcp/oauth-flow.ts +10 -8
- package/src/mcp/transports/stdio.ts +9 -17
- package/src/modes/controllers/event-controller.ts +17 -8
- package/src/modes/controllers/tool-args-reveal.ts +100 -22
- package/src/prompts/bench.md +4 -10
- package/src/prompts/tools/irc.md +19 -29
- package/src/prompts/tools/job.md +8 -14
- package/src/prompts/tools/lsp.md +19 -30
- package/src/prompts/tools/task.md +42 -62
- package/src/sdk.ts +1 -0
- package/src/session/agent-session.ts +10 -0
- package/src/session/session-listing.ts +9 -8
- package/src/session/session-loader.ts +98 -3
- package/src/session/session-manager.ts +34 -4
- package/src/subprocess/worker-client.ts +12 -4
- package/src/tools/path-utils.ts +4 -2
- package/src/web/search/index.ts +14 -8
- package/src/web/search/providers/duckduckgo.ts +136 -78
- package/src/web/search/providers/gemini.ts +268 -185
- package/src/web/search/types.ts +1 -1
|
@@ -1,91 +1,71 @@
|
|
|
1
|
-
{{#if asyncEnabled}}{{#if batchEnabled}}
|
|
1
|
+
{{#if asyncEnabled}}{{#if batchEnabled}}Delegate work to background subagents by passing multiple items in a single `tasks[]` batch.{{else}}Delegate work to ONE background subagent per call.{{/if}}
|
|
2
|
+
Execution does not block your turn: you receive agent and job IDs immediately, and the final results deliver themselves when the subagents finish.{{else}}{{#if batchEnabled}}Run subagents synchronously by passing items in a `tasks[]` batch.{{else}}Run ONE subagent synchronously per call.{{/if}}
|
|
3
|
+
Execution blocks your turn: the call only returns once the work is completely finished.{{/if}}
|
|
2
4
|
|
|
3
|
-
|
|
4
|
-
-
|
|
5
|
-
-
|
|
6
|
-
|
|
5
|
+
# Delegation Strategy
|
|
6
|
+
- **Maximize parallelism:** Break work into the widest possible {{#if batchEnabled}}array of `tasks[]`{{else}}set of parallel `task` calls{{/if}}. NEVER serialize work that can run concurrently. Tasks touching different files or independent refactors should run in parallel; agents resolve their own file collisions live.
|
|
7
|
+
- **Sequence only when necessary:** The only reason to run A before B is if B strictly requires A's output to function (e.g., a core API contract or schema migration). {{#if ircEnabled}}If the missing piece is small, run them in parallel and have B ask A via `irc`!{{/if}}
|
|
8
|
+
- **Role matching:** Assign each subagent a specific `role` (e.g. "Security Reviewer", "DB Migrator"). Do not spawn generic workers.
|
|
9
|
+
- **No overhead:** Each assignment MUST instruct its agent to skip formatters, linters, and project-wide test suites. You will run those once at the end.
|
|
10
|
+
- **Do your own thinking:** NEVER assign reasoning, architecture, or design to `quick_task` or `explore`. They are for mechanical lookups only. Keep hard decisions in your own context or use `task`, `plan`, or `oracle`.
|
|
11
|
+
- **One-pass agents:** Prefer agents that investigate **and** edit in a single pass; only spin a read-only discovery step (e.g. `explore`) when the affected files are genuinely unknown.
|
|
7
12
|
|
|
8
|
-
|
|
9
|
-
-
|
|
10
|
-
{{/if}}
|
|
11
|
-
{{#if ircEnabled}}
|
|
12
|
-
- Coordinate with agents via `irc` using their ids. Agents reach you and their siblings live the same way.
|
|
13
|
-
{{/if}}
|
|
14
|
-
|
|
15
|
-
<parameters>
|
|
16
|
-
- `agent`: agent type to spawn
|
|
13
|
+
# Inputs
|
|
14
|
+
- `agent`: The base agent type to use (e.g., `task`, `explore`).
|
|
17
15
|
{{#if batchEnabled}}
|
|
18
|
-
- `context`:
|
|
19
|
-
- `tasks`:
|
|
20
|
-
- `assignment`:
|
|
21
|
-
- `id`: stable
|
|
22
|
-
- `description`: UI label only
|
|
23
|
-
- `role`: specialist
|
|
16
|
+
- `context`: Shared project state, constraints, and contracts. Applies to the entire batch; do not duplicate this background into individual tasks.
|
|
17
|
+
- `tasks[]`: Array of subagents to spawn.
|
|
18
|
+
- `assignment`: Complete, self-contained instructions. One-liners or missing acceptance criteria are PROHIBITED.
|
|
19
|
+
- `id`: A stable CamelCase identifier (≤32 chars). Generated automatically if omitted.
|
|
20
|
+
- `description`: A UI label only; the subagent NEVER sees it.
|
|
21
|
+
- `role`: The specialist this subagent embodies. Tailor per spawn; do not clone a generic worker.
|
|
24
22
|
{{#if isolationEnabled}}
|
|
25
|
-
- `isolated`:
|
|
23
|
+
- `isolated`: Run in a dedicated worktree and return patches. Isolated agents are destroyed upon completion and cannot be addressed afterward.
|
|
26
24
|
{{/if}}
|
|
27
25
|
{{else}}
|
|
28
|
-
- `
|
|
29
|
-
- `
|
|
30
|
-
- `
|
|
31
|
-
- `
|
|
26
|
+
- `assignment`: Complete, self-contained instructions. One-liners or missing acceptance criteria are PROHIBITED.
|
|
27
|
+
- `id`: A stable CamelCase identifier (≤32 chars). Generated automatically if omitted.
|
|
28
|
+
- `description`: A UI label only; the subagent NEVER sees it.
|
|
29
|
+
- `role`: The specialist this subagent embodies. Tailor per spawn; do not clone a generic worker.
|
|
32
30
|
{{#if isolationEnabled}}
|
|
33
|
-
- `isolated`:
|
|
31
|
+
- `isolated`: Run in a dedicated worktree and return patches. Isolated agents are destroyed upon completion and cannot be addressed afterward.
|
|
34
32
|
{{/if}}
|
|
35
33
|
{{/if}}
|
|
36
|
-
</parameters>
|
|
37
34
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
- **Subagents do not verify, lint, or format.** Every assignment MUST instruct the subagent to skip all gates, formatters, and project-wide build/test/lint. You run them once at the end across the union of changed files.
|
|
41
|
-
- No globs, no "update all", no package-wide scope. Fan out.
|
|
42
|
-
- **Tailor every spawn with a `role`.** A role naming the specialist (e.g. "Parser edge-case tester", "SSE backpressure specialist") makes a sharper agent than a bare generic `task`/`quick_task` worker; decompose into named specialists, never clones of one generic worker. A role-less generic spawn is the exception.
|
|
43
|
-
- NEVER slow down or serialize because tasks might overlap on some files. Agents resolve collisions among themselves in real time.
|
|
44
|
-
- Subagents have no conversation history. Every fact, file path, and direction they need MUST be explicit in {{#if batchEnabled}}`context` or the item's `assignment`{{else}}the `assignment`{{/if}}.
|
|
35
|
+
# Context and Communication
|
|
36
|
+
Subagents start blank. They have no access to your conversation history.
|
|
45
37
|
{{#if batchEnabled}}
|
|
46
|
-
-
|
|
38
|
+
- Pass large payloads using `local://<path>` URIs, never inline text.
|
|
47
39
|
{{else}}
|
|
48
|
-
-
|
|
40
|
+
- *Note: The single-spawn shape has no `context` field.* Write shared project state ONCE to a `local://` file (e.g., `local://ctx.md`) and reference that URL in your assignments. Pass large payloads using `local://<path>` URIs, never inline text.
|
|
49
41
|
{{/if}}
|
|
50
|
-
- Prefer agents that investigate **and** edit in one pass; only spin a read-only discovery step when affected files are genuinely unknown.
|
|
51
|
-
- **Read-only agents**: Agents tagged READ-ONLY (e.g. `explore`) have no edit/write/command tools. NEVER hand them an assignment that requires changing files or running commands. Use them to investigate and report back; do the edits yourself or delegate to a writing agent (`task`, `oracle`, `designer`).
|
|
52
|
-
- **No reasoning offload**: NEVER offload reasoning, analysis, design, or decision-making to `quick_task` or `explore` — they run minimal-effort / small models for mechanical lookups and data collection only. Keep judgment and synthesis in your own context; delegate hard thinking to `task`, `plan`, or `oracle`.
|
|
53
|
-
</rules>
|
|
54
|
-
|
|
55
|
-
<parallelization>
|
|
56
42
|
{{#if ircEnabled}}
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
Test: can task B run correctly without seeing A's output? If no, sequence A → B.
|
|
62
|
-
Sequential when one task produces a contract (types, API, schema, core module) the other consumes.
|
|
63
|
-
Parallel when tasks touch disjoint files or are independent refactors/tests.
|
|
43
|
+
- Once spawned, coordinate with live agents via `irc` using their IDs. If task B depends on task A, B SHOULD message A directly.
|
|
44
|
+
{{/if}}
|
|
45
|
+
{{#if asyncEnabled}}
|
|
46
|
+
- If you run out of things to do and are genuinely blocked waiting for a subagent, use `job poll`. Use `job cancel` only for stalled work.
|
|
64
47
|
{{/if}}
|
|
65
|
-
{{#if ircEnabled}}Sequenced follow-ups SHOULD message the agent that produced the prerequisite — it already holds the context.{{/if}}
|
|
66
|
-
</parallelization>
|
|
67
48
|
|
|
49
|
+
# Format Contracts
|
|
68
50
|
{{#if batchEnabled}}
|
|
69
|
-
|
|
70
|
-
# Goal ←
|
|
71
|
-
# Constraints ←
|
|
72
|
-
# Contract ←
|
|
73
|
-
</context-fmt>
|
|
51
|
+
The `context` field MUST follow this format:
|
|
52
|
+
# Goal ← what the batch accomplishes
|
|
53
|
+
# Constraints ← rules and session decisions
|
|
54
|
+
# Contract ← shared interfaces
|
|
74
55
|
{{/if}}
|
|
75
56
|
|
|
76
|
-
|
|
57
|
+
The `assignment` field MUST follow this format:
|
|
77
58
|
# Target ← exact files and symbols; explicit non-goals
|
|
78
59
|
# Change ← step-by-step add/remove/rename; APIs and patterns
|
|
79
60
|
# Acceptance ← observable result; no project-wide commands
|
|
80
|
-
</assignment-fmt>
|
|
81
61
|
|
|
82
|
-
|
|
62
|
+
# Available Agents
|
|
83
63
|
{{#if spawningDisabled}}
|
|
84
|
-
Agent spawning is disabled
|
|
64
|
+
Agent spawning is currently disabled.
|
|
85
65
|
{{else}}
|
|
86
66
|
{{#list agents join="\n"}}
|
|
87
|
-
|
|
67
|
+
### {{name}}{{#if readOnly}} (READ-ONLY: no edit/write/command tools){{/if}}
|
|
88
68
|
{{description}}
|
|
69
|
+
{{#if readOnly}}Use ONLY for investigation and reporting; do the edits yourself or assign them to a writing agent.{{/if}}
|
|
89
70
|
{{/list}}
|
|
90
71
|
{{/if}}
|
|
91
|
-
</agents>
|
package/src/sdk.ts
CHANGED
|
@@ -2718,6 +2718,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
2718
2718
|
onResponse,
|
|
2719
2719
|
sideStreamFn: settingsAwareStreamFn,
|
|
2720
2720
|
advisorStreamFn: settingsAwareStreamFn,
|
|
2721
|
+
preferWebsockets: preferOpenAICodexWebsockets,
|
|
2721
2722
|
convertToLlm: convertToLlmFinal,
|
|
2722
2723
|
rebuildSystemPrompt,
|
|
2723
2724
|
reloadSshTool,
|
|
@@ -549,6 +549,8 @@ export interface AgentSessionConfig {
|
|
|
549
549
|
* main agent. Defaults to plain `streamSimple` when omitted.
|
|
550
550
|
*/
|
|
551
551
|
advisorStreamFn?: StreamFn;
|
|
552
|
+
/** Hint that OpenAI Codex requests should prefer websocket transport when supported. */
|
|
553
|
+
preferWebsockets?: boolean;
|
|
552
554
|
/** Provider payload hook used by the active session request path */
|
|
553
555
|
onPayload?: SimpleStreamOptions["onPayload"];
|
|
554
556
|
/** Provider response hook used by the active session request path */
|
|
@@ -1468,6 +1470,7 @@ export class AgentSession {
|
|
|
1468
1470
|
#transformProviderContext: ((context: Context, model: Model) => Context | Promise<Context>) | undefined;
|
|
1469
1471
|
#sideStreamFn: StreamFn;
|
|
1470
1472
|
#advisorStreamFn: StreamFn | undefined;
|
|
1473
|
+
#preferWebsockets: boolean | undefined;
|
|
1471
1474
|
#convertToLlm: (messages: AgentMessage[]) => Message[] | Promise<Message[]>;
|
|
1472
1475
|
#rebuildSystemPrompt:
|
|
1473
1476
|
| ((toolNames: string[], tools: Map<string, AgentTool>) => Promise<{ systemPrompt: string[] }>)
|
|
@@ -1798,6 +1801,7 @@ export class AgentSession {
|
|
|
1798
1801
|
this.#transformProviderContext = config.transformProviderContext;
|
|
1799
1802
|
this.#sideStreamFn = config.sideStreamFn ?? streamSimple;
|
|
1800
1803
|
this.#advisorStreamFn = config.advisorStreamFn;
|
|
1804
|
+
this.#preferWebsockets = config.preferWebsockets;
|
|
1801
1805
|
this.#onPayload = config.onPayload;
|
|
1802
1806
|
this.rawSseDebugBuffer = config.rawSseDebugBuffer ?? new RawSseDebugBuffer();
|
|
1803
1807
|
// Avoid wrapping in an `async` closure when no user callback is configured: the
|
|
@@ -2139,6 +2143,7 @@ export class AgentSession {
|
|
|
2139
2143
|
sessionId: advisorSessionId,
|
|
2140
2144
|
promptCacheKey: advisorSessionId,
|
|
2141
2145
|
providerSessionState: this.#providerSessionState,
|
|
2146
|
+
preferWebsockets: this.#preferWebsockets,
|
|
2142
2147
|
getApiKey: requestModel => this.#modelRegistry.resolver(requestModel, advisorSessionId),
|
|
2143
2148
|
streamFn: this.#advisorStreamFn,
|
|
2144
2149
|
onPayload: this.#onPayload,
|
|
@@ -2624,6 +2629,11 @@ export class AgentSession {
|
|
|
2624
2629
|
return this.#providerSessionState;
|
|
2625
2630
|
}
|
|
2626
2631
|
|
|
2632
|
+
/** Hint forwarded to provider calls that support websocket transport. */
|
|
2633
|
+
get preferWebsockets(): boolean | undefined {
|
|
2634
|
+
return this.#preferWebsockets;
|
|
2635
|
+
}
|
|
2636
|
+
|
|
2627
2637
|
getHindsightSessionState(): HindsightSessionState | undefined {
|
|
2628
2638
|
return this.#hindsightSessionState;
|
|
2629
2639
|
}
|
|
@@ -245,20 +245,21 @@ function countMessageMarkers(content: string): number {
|
|
|
245
245
|
return count;
|
|
246
246
|
}
|
|
247
247
|
|
|
248
|
-
function
|
|
249
|
-
|
|
250
|
-
|
|
248
|
+
function extractFirstDisplayMessageFromPrefix(content: string): string | undefined {
|
|
249
|
+
let fallback: string | undefined;
|
|
250
|
+
let index = content.indexOf('"role"');
|
|
251
251
|
|
|
252
|
-
let index = roleIndex;
|
|
253
252
|
while (index !== -1) {
|
|
254
253
|
const role = extractStringProperty(content, "role", index);
|
|
255
|
-
|
|
256
|
-
|
|
254
|
+
const text = extractStringProperty(content, "content", index) ?? extractStringProperty(content, "text", index);
|
|
255
|
+
if (text) {
|
|
256
|
+
if (role === "user") return text;
|
|
257
|
+
if (!fallback && (role === "developer" || role === "assistant")) fallback = text;
|
|
257
258
|
}
|
|
258
259
|
index = content.indexOf('"role"', index + 6);
|
|
259
260
|
}
|
|
260
261
|
|
|
261
|
-
return
|
|
262
|
+
return fallback;
|
|
262
263
|
}
|
|
263
264
|
|
|
264
265
|
interface SessionListHeader {
|
|
@@ -394,7 +395,7 @@ async function scanSessionFile(
|
|
|
394
395
|
}
|
|
395
396
|
}
|
|
396
397
|
|
|
397
|
-
firstMessage ||=
|
|
398
|
+
firstMessage ||= extractFirstDisplayMessageFromPrefix(content) ?? "";
|
|
398
399
|
const messageCount = Math.max(parsedMessageCount, countMessageMarkers(content));
|
|
399
400
|
return {
|
|
400
401
|
path: file,
|
|
@@ -1,8 +1,11 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as readline from "node:readline";
|
|
1
3
|
import type { AgentMessage } from "@oh-my-pi/pi-agent-core";
|
|
2
4
|
import { getBlobsDir, isEnoent, parseJsonlLenient } from "@oh-my-pi/pi-utils";
|
|
3
5
|
import { BlobStore, isBlobRef, resolveImageData, resolveImageDataUrl } from "./blob-store";
|
|
4
6
|
import { buildSessionContext } from "./session-context";
|
|
5
7
|
import {
|
|
8
|
+
type CompactionEntry,
|
|
6
9
|
type FileEntry,
|
|
7
10
|
type RawFileEntry,
|
|
8
11
|
SESSION_TITLE_SLOT_BYTES,
|
|
@@ -20,6 +23,10 @@ import {
|
|
|
20
23
|
titleUpdateFromSlot,
|
|
21
24
|
} from "./session-title-slot";
|
|
22
25
|
|
|
26
|
+
const STREAM_LOAD_THRESHOLD_BYTES = 8 * 1024 * 1024;
|
|
27
|
+
const ELIDED_COMPACTION_SUMMARY = "[Superseded compaction summary elided during session load]";
|
|
28
|
+
const ELIDED_COMPACTION_SHORT_SUMMARY = "Superseded compaction elided";
|
|
29
|
+
|
|
23
30
|
function splitTitleSlot(content: string): { body: string; slot: SessionTitleUpdate | undefined } {
|
|
24
31
|
const slot = titleUpdateFromSlot(parseTitleSlotFromContent(content));
|
|
25
32
|
if (!slot) return { body: content, slot: undefined };
|
|
@@ -54,6 +61,89 @@ export function parseSessionContent(content: string): {
|
|
|
54
61
|
return { entries: foldTitleSlot(entries, slot), titleSlot: slot };
|
|
55
62
|
}
|
|
56
63
|
|
|
64
|
+
function elideCompactionSummary(entry: CompactionEntry | undefined): boolean {
|
|
65
|
+
if (!entry) return false;
|
|
66
|
+
if (
|
|
67
|
+
entry.summary === ELIDED_COMPACTION_SUMMARY &&
|
|
68
|
+
entry.shortSummary === ELIDED_COMPACTION_SHORT_SUMMARY &&
|
|
69
|
+
entry.preserveData === undefined
|
|
70
|
+
) {
|
|
71
|
+
return false;
|
|
72
|
+
}
|
|
73
|
+
entry.summary = ELIDED_COMPACTION_SUMMARY;
|
|
74
|
+
entry.shortSummary = ELIDED_COMPACTION_SHORT_SUMMARY;
|
|
75
|
+
entry.preserveData = undefined;
|
|
76
|
+
return true;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function collectActiveBranchIds(entries: FileEntry[]): Set<string> {
|
|
80
|
+
const byId = new Map<string, SessionEntry>();
|
|
81
|
+
for (const entry of entries) {
|
|
82
|
+
const id = (entry as SessionEntry).id;
|
|
83
|
+
if (typeof id === "string") byId.set(id, entry as SessionEntry);
|
|
84
|
+
}
|
|
85
|
+
const branchIds = new Set<string>();
|
|
86
|
+
let cursor = entries[entries.length - 1] as SessionEntry | undefined;
|
|
87
|
+
while (cursor && typeof cursor.id === "string" && !branchIds.has(cursor.id)) {
|
|
88
|
+
branchIds.add(cursor.id);
|
|
89
|
+
const parentId = cursor.parentId;
|
|
90
|
+
cursor = parentId ? byId.get(parentId) : undefined;
|
|
91
|
+
}
|
|
92
|
+
return branchIds;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function elideSupersededCompactionEntries(entries: FileEntry[]): void {
|
|
96
|
+
const branchIds = collectActiveBranchIds(entries);
|
|
97
|
+
let previousCompaction: CompactionEntry | undefined;
|
|
98
|
+
for (const entry of entries) {
|
|
99
|
+
if (entry.type !== "compaction") continue;
|
|
100
|
+
if (!branchIds.has(entry.id)) continue;
|
|
101
|
+
elideCompactionSummary(previousCompaction);
|
|
102
|
+
previousCompaction = entry;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
async function loadEntriesFromFileStream(filePath: string): Promise<{
|
|
107
|
+
entries: FileEntry[];
|
|
108
|
+
titleSlot: SessionTitleUpdate | undefined;
|
|
109
|
+
}> {
|
|
110
|
+
const entries: FileEntry[] = [];
|
|
111
|
+
let titleSlot: SessionTitleUpdate | undefined;
|
|
112
|
+
let sawBodyLine = false;
|
|
113
|
+
const input = fs.createReadStream(filePath, { encoding: "utf8" });
|
|
114
|
+
const lines = readline.createInterface({ input, crlfDelay: Infinity });
|
|
115
|
+
|
|
116
|
+
try {
|
|
117
|
+
for await (const rawLine of lines) {
|
|
118
|
+
const line = rawLine.trim();
|
|
119
|
+
if (!line) continue;
|
|
120
|
+
if (!sawBodyLine) {
|
|
121
|
+
const slot = parseTitleSlotLine(line);
|
|
122
|
+
if (slot) {
|
|
123
|
+
titleSlot = titleUpdateFromSlot(slot);
|
|
124
|
+
sawBodyLine = true;
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
sawBodyLine = true;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
let entry: FileEntry;
|
|
131
|
+
try {
|
|
132
|
+
entry = JSON.parse(line) as FileEntry;
|
|
133
|
+
} catch {
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
entries.push(entry);
|
|
137
|
+
}
|
|
138
|
+
} catch (err) {
|
|
139
|
+
input.destroy();
|
|
140
|
+
if (isEnoent(err)) return { entries: [], titleSlot: undefined };
|
|
141
|
+
throw err;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return { entries: foldTitleSlot(entries, titleSlot), titleSlot };
|
|
145
|
+
}
|
|
146
|
+
|
|
57
147
|
/** Read only the fixed-size head window to detect a physical title slot. */
|
|
58
148
|
export async function readTitleSlotFromFile(
|
|
59
149
|
filePath: string,
|
|
@@ -80,14 +170,19 @@ export async function loadEntriesFromFile(
|
|
|
80
170
|
filePath: string,
|
|
81
171
|
storage: SessionStorage = new FileSessionStorage(),
|
|
82
172
|
): Promise<FileEntry[]> {
|
|
83
|
-
let
|
|
173
|
+
let loaded: { entries: FileEntry[]; titleSlot: SessionTitleUpdate | undefined };
|
|
84
174
|
try {
|
|
85
|
-
|
|
175
|
+
const stat = storage.statSync(filePath);
|
|
176
|
+
loaded =
|
|
177
|
+
storage instanceof FileSessionStorage && stat.size >= STREAM_LOAD_THRESHOLD_BYTES
|
|
178
|
+
? await loadEntriesFromFileStream(filePath)
|
|
179
|
+
: parseSessionContent(await storage.readText(filePath));
|
|
86
180
|
} catch (err) {
|
|
87
181
|
if (isEnoent(err)) return [];
|
|
88
182
|
throw err;
|
|
89
183
|
}
|
|
90
|
-
const { entries } =
|
|
184
|
+
const { entries } = loaded;
|
|
185
|
+
elideSupersededCompactionEntries(entries);
|
|
91
186
|
|
|
92
187
|
// Validate session header
|
|
93
188
|
if (entries.length === 0) return entries;
|
|
@@ -66,6 +66,8 @@ import {
|
|
|
66
66
|
import { type SessionTitleUpdate, serializeTitleSlot } from "./session-title-slot";
|
|
67
67
|
|
|
68
68
|
const JSONL_SUFFIX_LENGTH = ".jsonl".length;
|
|
69
|
+
const SUPERSEDED_COMPACTION_SUMMARY = "[Superseded compaction summary elided after a newer compaction]";
|
|
70
|
+
const SUPERSEDED_COMPACTION_SHORT_SUMMARY = "Superseded compaction elided";
|
|
69
71
|
|
|
70
72
|
function mintSessionId(): string {
|
|
71
73
|
return Bun.randomUUIDv7();
|
|
@@ -502,6 +504,26 @@ export class SessionManager {
|
|
|
502
504
|
return this.#forceFileCreation || this.#fileIsCurrent || this.#historyContainsAssistantMessage();
|
|
503
505
|
}
|
|
504
506
|
|
|
507
|
+
#elideSupersededCompactionsOnBranch(leafId: string | null): boolean {
|
|
508
|
+
if (!leafId) return false;
|
|
509
|
+
let changed = false;
|
|
510
|
+
for (const entry of this.#index.pathTo(leafId)) {
|
|
511
|
+
if (entry.type !== "compaction") continue;
|
|
512
|
+
if (
|
|
513
|
+
entry.summary === SUPERSEDED_COMPACTION_SUMMARY &&
|
|
514
|
+
entry.shortSummary === SUPERSEDED_COMPACTION_SHORT_SUMMARY &&
|
|
515
|
+
entry.preserveData === undefined
|
|
516
|
+
) {
|
|
517
|
+
continue;
|
|
518
|
+
}
|
|
519
|
+
entry.summary = SUPERSEDED_COMPACTION_SUMMARY;
|
|
520
|
+
entry.shortSummary = SUPERSEDED_COMPACTION_SHORT_SUMMARY;
|
|
521
|
+
entry.preserveData = undefined;
|
|
522
|
+
changed = true;
|
|
523
|
+
}
|
|
524
|
+
return changed;
|
|
525
|
+
}
|
|
526
|
+
|
|
505
527
|
/**
|
|
506
528
|
* Synchronously rewrite the whole file (header + entries) and keep no open
|
|
507
529
|
* writer; the next append re-opens one. `writeTextSync` returns with the
|
|
@@ -1024,14 +1046,18 @@ export class SessionManager {
|
|
|
1024
1046
|
}
|
|
1025
1047
|
|
|
1026
1048
|
/**
|
|
1027
|
-
* Synchronously
|
|
1028
|
-
*
|
|
1029
|
-
*
|
|
1030
|
-
* lands mid-`writeFileSync`.
|
|
1049
|
+
* Synchronously makes the current append-only session durable. Avoid rewriting
|
|
1050
|
+
* an already-current file: large restored sessions can contain GiB of compacted
|
|
1051
|
+
* history, and Ctrl+C must not rebuild the whole JSONL string just to flush.
|
|
1031
1052
|
*/
|
|
1032
1053
|
flushSync(): void {
|
|
1033
1054
|
if (!this.#persist || !this.#sessionFile) return;
|
|
1034
1055
|
if (this.#diskFailure) throw this.#diskFailure;
|
|
1056
|
+
if (this.#fileIsCurrent && !this.#rewriteRequired) {
|
|
1057
|
+
const writerError = this.#writer?.getError();
|
|
1058
|
+
if (writerError) throw writerError;
|
|
1059
|
+
return;
|
|
1060
|
+
}
|
|
1035
1061
|
this.#rewriteSynchronously();
|
|
1036
1062
|
if (this.#diskFailure) throw this.#diskFailure;
|
|
1037
1063
|
}
|
|
@@ -1305,6 +1331,7 @@ export class SessionManager {
|
|
|
1305
1331
|
fromExtension?: boolean,
|
|
1306
1332
|
preserveData?: Record<string, unknown>,
|
|
1307
1333
|
): string {
|
|
1334
|
+
const elidedSupersededCompactions = this.#elideSupersededCompactionsOnBranch(this.#index.leafId());
|
|
1308
1335
|
const entry: CompactionEntry<T> = {
|
|
1309
1336
|
type: "compaction",
|
|
1310
1337
|
...this.#freshEntryFields(),
|
|
@@ -1317,6 +1344,9 @@ export class SessionManager {
|
|
|
1317
1344
|
preserveData,
|
|
1318
1345
|
};
|
|
1319
1346
|
this.#recordEntry(entry);
|
|
1347
|
+
if (elidedSupersededCompactions) {
|
|
1348
|
+
void this.#rewriteAtomically().catch(err => this.#noteDiskFailure(err));
|
|
1349
|
+
}
|
|
1320
1350
|
return entry.id;
|
|
1321
1351
|
}
|
|
1322
1352
|
|
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
import * as path from "node:path";
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
$env,
|
|
4
|
+
isBunTestRuntime,
|
|
5
|
+
isCompiledBinary,
|
|
6
|
+
logger,
|
|
7
|
+
stripWindowsExtendedLengthPathPrefix,
|
|
8
|
+
workerHostEntry,
|
|
9
|
+
} from "@oh-my-pi/pi-utils";
|
|
3
10
|
import type { Subprocess } from "bun";
|
|
4
11
|
|
|
5
12
|
/**
|
|
@@ -90,13 +97,14 @@ export const SMOKE_TEST_TIMEOUT_MS = 30_000;
|
|
|
90
97
|
* embedding).
|
|
91
98
|
*/
|
|
92
99
|
export function resolveWorkerSpawnCmd(workerArg: string): WorkerSpawnCommand {
|
|
93
|
-
|
|
100
|
+
const executable = stripWindowsExtendedLengthPathPrefix(process.execPath);
|
|
101
|
+
if (isCompiledBinary()) return { cmd: [executable, workerArg] };
|
|
94
102
|
const hostEntry = workerHostEntry();
|
|
95
103
|
if (hostEntry) {
|
|
96
|
-
return { cmd: [
|
|
104
|
+
return { cmd: [executable, path.basename(hostEntry), workerArg], cwd: path.dirname(hostEntry) };
|
|
97
105
|
}
|
|
98
106
|
const packageRoot = path.resolve(import.meta.dir, "..", "..");
|
|
99
|
-
return { cmd: [
|
|
107
|
+
return { cmd: [executable, "src/cli.ts", workerArg], cwd: packageRoot };
|
|
100
108
|
}
|
|
101
109
|
|
|
102
110
|
/**
|
package/src/tools/path-utils.ts
CHANGED
|
@@ -2,7 +2,7 @@ import * as fs from "node:fs";
|
|
|
2
2
|
import * as os from "node:os";
|
|
3
3
|
import * as path from "node:path";
|
|
4
4
|
import * as url from "node:url";
|
|
5
|
-
import { isEnoent } from "@oh-my-pi/pi-utils";
|
|
5
|
+
import { isEnoent, stripWindowsExtendedLengthPathPrefix } from "@oh-my-pi/pi-utils";
|
|
6
6
|
import type { Skill } from "../extensibility/skills";
|
|
7
7
|
import { InternalUrlRouter, type LocalProtocolOptions } from "../internal-urls";
|
|
8
8
|
import { ToolError } from "./tool-errors";
|
|
@@ -147,7 +147,9 @@ export function expandTilde(filePath: string, home?: string): string {
|
|
|
147
147
|
}
|
|
148
148
|
|
|
149
149
|
export function expandPath(filePath: string): string {
|
|
150
|
-
const normalized =
|
|
150
|
+
const normalized = stripWindowsExtendedLengthPathPrefix(
|
|
151
|
+
stripFileUrl(normalizeUnicodeSpaces(normalizeAtPrefix(filePath))),
|
|
152
|
+
);
|
|
151
153
|
return expandTilde(normalized);
|
|
152
154
|
}
|
|
153
155
|
|
package/src/web/search/index.ts
CHANGED
|
@@ -138,14 +138,20 @@ async function executeSearch(
|
|
|
138
138
|
options: ExecuteSearchOptions,
|
|
139
139
|
): Promise<{ content: Array<{ type: "text"; text: string }>; details: SearchRenderDetails }> {
|
|
140
140
|
const { authStorage, sessionId, signal } = options;
|
|
141
|
-
const
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
141
|
+
const explicitProvider = params.provider;
|
|
142
|
+
let providers: SearchProvider[];
|
|
143
|
+
if (explicitProvider && explicitProvider !== "auto") {
|
|
144
|
+
const provider = await getSearchProvider(explicitProvider);
|
|
145
|
+
providers = (await provider.isExplicitlyAvailable(authStorage))
|
|
146
|
+
? [provider]
|
|
147
|
+
: await resolveProviderChain(authStorage, "auto");
|
|
148
|
+
} else if (explicitProvider === "auto") {
|
|
149
|
+
// Explicit `--provider auto` bypasses the configured preferred provider
|
|
150
|
+
// for this invocation; exclusions still apply.
|
|
151
|
+
providers = await resolveProviderChain(authStorage, "auto");
|
|
152
|
+
} else {
|
|
153
|
+
providers = await resolveProviderChain(authStorage);
|
|
154
|
+
}
|
|
149
155
|
if (providers.length === 0) {
|
|
150
156
|
const message = "No web search provider configured.";
|
|
151
157
|
return {
|