@oh-my-pi/pi-coding-agent 17.3.4 → 17.3.5
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 +68 -0
- package/dist/{CHANGELOG-trcc215s.md → CHANGELOG-tt9k4jpr.md} +68 -0
- package/dist/cli.js +3146 -3153
- package/dist/docs-index.generated.txt +1 -1
- package/dist/types/config/model-discovery.d.ts +2 -0
- package/dist/types/config/model-registry.d.ts +6 -1
- package/dist/types/config/settings-schema.d.ts +10 -0
- package/dist/types/debug/report-bundle.d.ts +7 -2
- package/dist/types/extensibility/extensions/loader.d.ts +2 -0
- package/dist/types/extensibility/extensions/runner.d.ts +3 -3
- package/dist/types/extensibility/extensions/types.d.ts +2 -0
- package/dist/types/extensibility/legacy-pi-ai-shim.d.ts +4 -0
- package/dist/types/launch/presence.d.ts +11 -0
- package/dist/types/lsp/client.d.ts +22 -0
- package/dist/types/lsp/workspace-diagnostics.d.ts +2 -0
- package/dist/types/mcp/config.d.ts +3 -1
- package/dist/types/modes/components/tool-execution.d.ts +3 -1
- package/dist/types/registry/agent-lifecycle.d.ts +11 -0
- package/dist/types/registry/agent-registry.d.ts +4 -0
- package/dist/types/session/agent-session-types.d.ts +0 -2
- package/dist/types/session/agent-session.d.ts +20 -0
- package/dist/types/session/date-cwd-reminder.d.ts +22 -0
- package/dist/types/session/irc-bridge.d.ts +2 -0
- package/dist/types/session/messages.d.ts +4 -0
- package/dist/types/session/session-loader.d.ts +18 -11
- package/dist/types/session/session-maintenance.d.ts +6 -2
- package/dist/types/session/session-manager.d.ts +19 -3
- package/dist/types/session/session-tools.d.ts +0 -1
- package/dist/types/session/turn-recovery.d.ts +7 -7
- package/dist/types/tools/browser/attach.d.ts +16 -0
- package/dist/types/tools/browser/launch.d.ts +2 -2
- package/dist/types/tools/browser.d.ts +1 -1
- package/dist/types/tools/builtin-names.d.ts +1 -1
- package/dist/types/tools/hub/jobs.d.ts +6 -0
- package/dist/types/tools/hub/types.d.ts +6 -0
- package/dist/types/vibe/runtime.d.ts +3 -3
- package/package.json +13 -13
- package/src/auto-thinking/classifier.ts +37 -23
- package/src/cli/models-cli.ts +1 -1
- package/src/commit/analysis/conventional.ts +15 -9
- package/src/commit/analysis/summary.ts +15 -9
- package/src/commit/changelog/generate.ts +15 -9
- package/src/commit/map-reduce/map-phase.ts +3 -19
- package/src/commit/map-reduce/reduce-phase.ts +15 -9
- package/src/config/model-discovery.ts +3 -3
- package/src/config/model-registry.ts +29 -16
- package/src/config/settings-schema.ts +13 -0
- package/src/debug/report-bundle.ts +25 -59
- package/src/discovery/claude-plugins.ts +2 -1
- package/src/discovery/claude.ts +4 -2
- package/src/discovery/helpers.ts +5 -0
- package/src/edit/renderer.ts +48 -19
- package/src/extensibility/extensions/loader.ts +9 -5
- package/src/extensibility/extensions/runner.ts +124 -21
- package/src/extensibility/extensions/types.ts +2 -0
- package/src/extensibility/extensions/wrapper.ts +16 -10
- package/src/extensibility/legacy-pi-ai-shim.ts +4 -0
- package/src/extensibility/plugins/marketplace/manager.ts +15 -18
- package/src/irc/bus.ts +1 -1
- package/src/launch/broker.ts +8 -1
- package/src/launch/presence.ts +77 -1
- package/src/lsp/client.ts +35 -3
- package/src/lsp/clients/biome-client.ts +47 -93
- package/src/lsp/servers.ts +17 -11
- package/src/lsp/tool.ts +2 -4
- package/src/lsp/workspace-diagnostics.ts +24 -2
- package/src/mcp/config.ts +50 -6
- package/src/memories/index.ts +29 -25
- package/src/mnemopi/backend.ts +13 -11
- package/src/modes/components/ask-dialog.ts +25 -5
- package/src/modes/components/tool-execution.ts +10 -6
- package/src/modes/components/welcome.ts +4 -1
- package/src/modes/controllers/extension-ui-controller.ts +52 -32
- package/src/modes/controllers/selector-controller.ts +6 -0
- package/src/modes/controllers/tan-command-controller.ts +1 -0
- package/src/modes/interactive-mode.ts +49 -10
- package/src/modes/theme/defaults/birch.json +2 -2
- package/src/prompts/system/checkpoint-active-notice.md +5 -0
- package/src/prompts/system/date-cwd-reminder.md +3 -0
- package/src/prompts/system/project-prompt.md +0 -1
- package/src/prompts/system/rewind-report.md +1 -3
- package/src/prompts/tools/browser.md +1 -0
- package/src/prompts/tools/rewind.md +1 -13
- package/src/registry/agent-lifecycle.ts +34 -0
- package/src/registry/agent-registry.ts +27 -2
- package/src/registry/persisted-agents.ts +40 -20
- package/src/sdk.ts +32 -2
- package/src/session/agent-session-types.ts +0 -2
- package/src/session/agent-session.ts +178 -46
- package/src/session/date-cwd-reminder.ts +77 -0
- package/src/session/irc-bridge.ts +5 -0
- package/src/session/messages.ts +5 -1
- package/src/session/session-loader.ts +106 -64
- package/src/session/session-maintenance.ts +189 -115
- package/src/session/session-manager.ts +142 -35
- package/src/session/session-persistence.ts +4 -4
- package/src/session/session-storage.ts +18 -5
- package/src/session/session-tools.ts +5 -10
- package/src/session/turn-recovery.ts +102 -20
- package/src/session/unexpected-stop-classifier.ts +33 -21
- package/src/system-prompt.ts +0 -5
- package/src/task/executor.ts +4 -15
- package/src/task/persisted-revive.ts +3 -6
- package/src/tools/ask.ts +10 -3
- package/src/tools/browser/attach.ts +80 -25
- package/src/tools/browser/launch.ts +44 -15
- package/src/tools/browser/relay/daemon.ts +3 -7
- package/src/tools/browser/render.ts +1 -1
- package/src/tools/browser/shared-daemon.ts +3 -7
- package/src/tools/browser.ts +5 -5
- package/src/tools/builtin-names.ts +7 -3
- package/src/tools/checkpoint.ts +1 -7
- package/src/tools/hub/index.ts +1 -1
- package/src/tools/hub/jobs.ts +20 -3
- package/src/tools/hub/types.ts +6 -0
- package/src/tools/render-utils.ts +53 -21
- package/src/tools/think.ts +15 -3
- package/src/tts/speech-enhancer.ts +19 -14
- package/src/utils/commit-message-generator.ts +14 -12
- package/src/utils/title-generator.ts +23 -19
- package/src/vibe/runtime.ts +32 -17
|
@@ -17,6 +17,7 @@ import { daemonClientForGlobal } from "../../../launch/client";
|
|
|
17
17
|
import { describeQuietly, stopQuietly, waitReady } from "../../../launch/ensure";
|
|
18
18
|
import { resolveWorkerSpawnCmd } from "../../../subprocess/worker-client";
|
|
19
19
|
import { throwIfAborted } from "../../tool-errors";
|
|
20
|
+
import { probeCdpStatus } from "../attach";
|
|
20
21
|
|
|
21
22
|
/** Stable broker daemon name for the relay server. */
|
|
22
23
|
export const RELAY_DAEMON_NAME = "omp.browser.relay";
|
|
@@ -30,13 +31,8 @@ const ENSURE_ATTEMPTS = 3;
|
|
|
30
31
|
|
|
31
32
|
/** True when the relay HTTP server answers /json/version at all (200 = extension connected, 503 = waiting for it). */
|
|
32
33
|
export async function probeRelayServer(cdpUrl: string): Promise<boolean> {
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
await res.body?.cancel();
|
|
36
|
-
return res.ok || res.status === 503;
|
|
37
|
-
} catch {
|
|
38
|
-
return false;
|
|
39
|
-
}
|
|
34
|
+
const status = await probeCdpStatus(`${cdpUrl}/json/version`, { timeoutMs: PROBE_TIMEOUT_MS });
|
|
35
|
+
return status === 503 || (status !== null && status >= 200 && status < 300);
|
|
40
36
|
}
|
|
41
37
|
|
|
42
38
|
/** Auto-start is only safe for endpoints this machine can own. */
|
|
@@ -162,7 +162,7 @@ function renderOpenOrCloseLine(
|
|
|
162
162
|
let title: string;
|
|
163
163
|
if (action === "close") {
|
|
164
164
|
const all = args.all === true || (args.name === undefined && details?.name === undefined);
|
|
165
|
-
title = all ? "
|
|
165
|
+
title = all ? "Release all tabs" : `Release ${tabLabel(args, details)}`;
|
|
166
166
|
if (args.kill) title += " (kill)";
|
|
167
167
|
} else {
|
|
168
168
|
title = `Open ${tabLabel(args, details)}`;
|
|
@@ -16,6 +16,7 @@ import { describeQuietly, stopQuietly, waitReady } from "../../launch/ensure";
|
|
|
16
16
|
import { daemonRuntimeDir } from "../../launch/paths";
|
|
17
17
|
import type { DaemonSnapshot } from "../../launch/protocol";
|
|
18
18
|
import { throwIfAborted } from "../tool-errors";
|
|
19
|
+
import { probeCdpStatus } from "./attach";
|
|
19
20
|
import { resolveSharedBrowserLaunchSpec } from "./launch";
|
|
20
21
|
|
|
21
22
|
/** Chrome prints this on stderr once the CDP listener is up; the broker's ready probe captures the line. */
|
|
@@ -50,13 +51,8 @@ async function probeEndpoint(wsEndpoint: string): Promise<boolean> {
|
|
|
50
51
|
} catch {
|
|
51
52
|
return false;
|
|
52
53
|
}
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
await res.body?.cancel();
|
|
56
|
-
return res.ok;
|
|
57
|
-
} catch {
|
|
58
|
-
return false;
|
|
59
|
-
}
|
|
54
|
+
const status = await probeCdpStatus(`http://${host}/json/version`, { timeoutMs: PROBE_TIMEOUT_MS });
|
|
55
|
+
return status !== null && status >= 200 && status < 300;
|
|
60
56
|
}
|
|
61
57
|
|
|
62
58
|
/**
|
package/src/tools/browser.ts
CHANGED
|
@@ -69,7 +69,7 @@ const browserSchema = type({
|
|
|
69
69
|
"dialogs?": type("'accept' | 'dismiss'").describe("auto-handle dialogs"),
|
|
70
70
|
"code?": type("string").describe("js body to run in tab"),
|
|
71
71
|
"timeout?": type("number").describe("timeout in seconds"),
|
|
72
|
-
"all?": type("boolean").describe("
|
|
72
|
+
"all?": type("boolean").describe("release every managed tab"),
|
|
73
73
|
"kill?": type("boolean").describe("also kill spawned-app browsers"),
|
|
74
74
|
});
|
|
75
75
|
|
|
@@ -133,7 +133,7 @@ function resolveBrowserKind(params: BrowserParams, session: ToolSession): Browse
|
|
|
133
133
|
/**
|
|
134
134
|
* Browser tool: stateful, multi-tab. Three actions:
|
|
135
135
|
* - `open` → acquire/create a named tab on a browser kind (headless | spawned | connected) and optionally goto a url.
|
|
136
|
-
* - `close` → release a named tab (or all
|
|
136
|
+
* - `close` → release a named tab handle (or all handles); attached/relay pages remain open, and spawned pages remain unless killed.
|
|
137
137
|
* - `run` → execute JS code against an existing tab with `page`/`browser`/`tab` helpers in scope.
|
|
138
138
|
*/
|
|
139
139
|
export class BrowserTool implements AgentTool<typeof browserSchema, BrowserToolDetails> {
|
|
@@ -204,7 +204,7 @@ export class BrowserTool implements AgentTool<typeof browserSchema, BrowserToolD
|
|
|
204
204
|
},
|
|
205
205
|
},
|
|
206
206
|
{
|
|
207
|
-
caption: "
|
|
207
|
+
caption: "Release every managed tab and kill spawned-app processes",
|
|
208
208
|
call: { action: "close", all: true, kill: true },
|
|
209
209
|
},
|
|
210
210
|
];
|
|
@@ -362,11 +362,11 @@ export class BrowserTool implements AgentTool<typeof browserSchema, BrowserToolD
|
|
|
362
362
|
const kill = !!params.kill;
|
|
363
363
|
if (params.all) {
|
|
364
364
|
const count = await untilAborted(signal, () => releaseAllTabs({ kill, timeoutMs }));
|
|
365
|
-
details.result = `
|
|
365
|
+
details.result = `Released ${count} managed tab${count === 1 ? "" : "s"}`;
|
|
366
366
|
return toolResult(details).text(details.result).done();
|
|
367
367
|
}
|
|
368
368
|
const closed = await untilAborted(signal, () => releaseTab(name, { kill, timeoutMs }));
|
|
369
|
-
details.result = closed ? `
|
|
369
|
+
details.result = closed ? `Released managed tab ${JSON.stringify(name)}` : `No tab named ${JSON.stringify(name)}`;
|
|
370
370
|
return toolResult(details).text(details.result).done();
|
|
371
371
|
}
|
|
372
372
|
|
|
@@ -41,10 +41,14 @@ const LEGACY_BUILTIN_TOOL_NAME_ALIASES: ReadonlyMap<string, BuiltinToolName> = n
|
|
|
41
41
|
["find", "glob"],
|
|
42
42
|
]);
|
|
43
43
|
|
|
44
|
-
|
|
44
|
+
const CANONICAL_TOOL_NAMES: Record<string, true> = Object.fromEntries(
|
|
45
|
+
[...BUILTIN_TOOL_NAMES, ...HIDDEN_TOOL_NAMES].map(name => [name, true]),
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
/** Canonicalize built-in IDs and legacy aliases. Leave plugin names unchanged. */
|
|
45
49
|
export function normalizeToolName(name: string): string {
|
|
46
|
-
const
|
|
47
|
-
return LEGACY_BUILTIN_TOOL_NAME_ALIASES.get(
|
|
50
|
+
const lower = name.toLowerCase();
|
|
51
|
+
return LEGACY_BUILTIN_TOOL_NAME_ALIASES.get(lower) ?? (Object.hasOwn(CANONICAL_TOOL_NAMES, lower) ? lower : name);
|
|
48
52
|
}
|
|
49
53
|
|
|
50
54
|
/** Normalize and deduplicate tool names while preserving first-seen order. */
|
package/src/tools/checkpoint.ts
CHANGED
|
@@ -81,13 +81,7 @@ export class CheckpointTool implements AgentTool<typeof checkpointSchema, Checkp
|
|
|
81
81
|
}
|
|
82
82
|
const startedAt = new Date().toISOString();
|
|
83
83
|
return toolResult<CheckpointToolDetails>({ goal: params.goal, startedAt })
|
|
84
|
-
.text(
|
|
85
|
-
[
|
|
86
|
-
"Checkpoint created.",
|
|
87
|
-
`Goal: ${params.goal}`,
|
|
88
|
-
"Run your investigation, then call rewind with a concise report.",
|
|
89
|
-
].join("\n"),
|
|
90
|
-
)
|
|
84
|
+
.text([`Checkpoint: ${params.goal}`, "Finish exploration and formulate findings."].join("\n"))
|
|
91
85
|
.done();
|
|
92
86
|
}
|
|
93
87
|
}
|
package/src/tools/hub/index.ts
CHANGED
|
@@ -377,7 +377,7 @@ export class HubTool implements AgentTool<typeof hubSchema, HubDetails> {
|
|
|
377
377
|
// instead of blocking a full message-timeout window.
|
|
378
378
|
const hasRunningPeer = messaging.registry
|
|
379
379
|
.listVisibleTo(messaging.senderId)
|
|
380
|
-
.some(ref => ref
|
|
380
|
+
.some(ref => messaging.registry.isRunning(ref));
|
|
381
381
|
if (!hasRunningPeer) return nothingToWaitForResult(this.session);
|
|
382
382
|
}
|
|
383
383
|
return executeMessageWait(messaging, { from, timeoutMs: params.timeoutMs }, signal);
|
package/src/tools/hub/jobs.ts
CHANGED
|
@@ -87,6 +87,12 @@ export function visibleJobs(manager: AsyncJobManager, ids: string[], ownerId: st
|
|
|
87
87
|
* that activity instead of implying the system is quiet. Existence is
|
|
88
88
|
* already public via the peer roster, so listing ids here leaks nothing new;
|
|
89
89
|
* job *control* stays owner-scoped.
|
|
90
|
+
*
|
|
91
|
+
* Reporting deliberately uses the claimed `status`, not the session-corroborated
|
|
92
|
+
* `registry.isRunning` used by the wait-sustaining gates: a ref that claims
|
|
93
|
+
* `running` with no live turn is exactly the stale entry an operator must see
|
|
94
|
+
* here to cancel it (#8634). Hiding it would match the badge count to nothing
|
|
95
|
+
* and remove the only discovery path for the id.
|
|
90
96
|
*/
|
|
91
97
|
export function runningAgentsOutsideJobs(session: ToolSession): AgentActivitySnapshot[] {
|
|
92
98
|
const registry = session.agentRegistry;
|
|
@@ -113,6 +119,7 @@ export function runningAgentsOutsideJobs(session: ToolSession): AgentActivitySna
|
|
|
113
119
|
...(ref.parentId ? { parentId: ref.parentId } : {}),
|
|
114
120
|
...(ref.activity ? { activity: ref.activity } : {}),
|
|
115
121
|
ageMs: Math.max(0, now - ref.createdAt),
|
|
122
|
+
live: registry.isRunning(ref),
|
|
116
123
|
});
|
|
117
124
|
}
|
|
118
125
|
return out;
|
|
@@ -124,9 +131,15 @@ function describeAgents(agents: AgentActivitySnapshot[]): string[] {
|
|
|
124
131
|
for (const agent of agents) {
|
|
125
132
|
const parent = agent.parentId ? ` (spawned by \`${agent.parentId}\`)` : "";
|
|
126
133
|
const activity = agent.activity ? ` — ${agent.activity}` : "";
|
|
127
|
-
|
|
134
|
+
const stale = agent.live ? "" : " — no turn in flight (stale registration?)";
|
|
135
|
+
lines.push(`- \`${agent.id}\`${parent} — up ${formatDuration(agent.ageMs)}${activity}${stale}`);
|
|
128
136
|
}
|
|
129
137
|
lines.push("", "These agents have no job entry; message them via `hub` send, transcripts at `history://<id>`.");
|
|
138
|
+
if (agents.some(agent => !agent.live)) {
|
|
139
|
+
lines.push(
|
|
140
|
+
"An agent with no turn in flight cannot answer a message and never satisfies a bare `wait`; clear it with `hub` cancel.",
|
|
141
|
+
);
|
|
142
|
+
}
|
|
130
143
|
return lines;
|
|
131
144
|
}
|
|
132
145
|
|
|
@@ -690,8 +703,12 @@ export function jobsRenderResult(
|
|
|
690
703
|
maxCollapsed: COLLAPSED_LIST_LIMIT,
|
|
691
704
|
itemType: "agent",
|
|
692
705
|
renderItem: agent => {
|
|
693
|
-
const icon =
|
|
694
|
-
|
|
706
|
+
const icon = agent.live
|
|
707
|
+
? formatStatusIcon("running", uiTheme, options.spinnerFrame)
|
|
708
|
+
: formatStatusIcon("warning", uiTheme);
|
|
709
|
+
const badge = agent.live
|
|
710
|
+
? formatBadge("agent", "accent", uiTheme)
|
|
711
|
+
: formatBadge("agent · no turn", "warning", uiTheme);
|
|
695
712
|
const gist = agent.activity
|
|
696
713
|
? ` ${uiTheme.fg("toolOutput", truncateToWidth(replaceTabs(agent.activity), LABEL_MAX_WIDTH, Ellipsis.Unicode))}`
|
|
697
714
|
: "";
|
package/src/tools/hub/types.ts
CHANGED
|
@@ -74,6 +74,12 @@ export interface AgentActivitySnapshot {
|
|
|
74
74
|
activity?: string;
|
|
75
75
|
/** Time since the agent was registered. */
|
|
76
76
|
ageMs: number;
|
|
77
|
+
/**
|
|
78
|
+
* Whether an attached session corroborates the `running` claim. False marks
|
|
79
|
+
* a ref that says `running` with no turn in flight — either a spawn still
|
|
80
|
+
* wiring up or a stale registration that `hub cancel <id>` clears (#8634).
|
|
81
|
+
*/
|
|
82
|
+
live: boolean;
|
|
77
83
|
}
|
|
78
84
|
|
|
79
85
|
/** Result details for messaging and job ops; fields are disjoint per op. */
|
|
@@ -599,79 +599,111 @@ export function truncateDiffByHunk(
|
|
|
599
599
|
let keptHunks = 0;
|
|
600
600
|
|
|
601
601
|
for (const seg of segments) {
|
|
602
|
+
if (kept.length >= maxLines) break;
|
|
602
603
|
if (seg.isChange) {
|
|
604
|
+
if (keptHunks >= maxHunks) break;
|
|
603
605
|
keptHunks++;
|
|
604
|
-
if (keptHunks > maxHunks) break;
|
|
605
606
|
}
|
|
606
|
-
|
|
607
|
-
|
|
607
|
+
const take = Math.min(seg.lines.length, maxLines - kept.length);
|
|
608
|
+
for (let i = 0; i < take; i++) {
|
|
609
|
+
kept.push(seg.lines[i]!);
|
|
610
|
+
}
|
|
608
611
|
}
|
|
609
612
|
|
|
610
|
-
const keptStats = getDiffStats(kept.join("\n"));
|
|
611
613
|
return {
|
|
612
614
|
text: kept.join("\n"),
|
|
613
|
-
hiddenHunks: Math.max(0, totalStats.hunks -
|
|
615
|
+
hiddenHunks: Math.max(0, totalStats.hunks - keptHunks),
|
|
614
616
|
hiddenLines: Math.max(0, lines.length - kept.length),
|
|
615
617
|
};
|
|
616
618
|
}
|
|
617
619
|
|
|
618
620
|
const contextBudget = maxLines - changeLineCount;
|
|
619
|
-
const contextSegments = segments.filter(s => !s.isChange
|
|
621
|
+
const contextSegments = segments.filter(s => !s.isChange);
|
|
620
622
|
const totalContextLines = contextSegments.reduce((sum, s) => sum + s.lines.length, 0);
|
|
621
623
|
|
|
622
624
|
const kept: string[] = [];
|
|
623
625
|
let keptHunks = 0;
|
|
626
|
+
let keptSourceLines = 0;
|
|
624
627
|
|
|
625
628
|
if (totalContextLines <= contextBudget) {
|
|
626
629
|
for (const seg of segments) {
|
|
627
630
|
if (seg.isChange) {
|
|
631
|
+
if (keptHunks >= maxHunks) break;
|
|
628
632
|
keptHunks++;
|
|
629
|
-
if (keptHunks > maxHunks) break;
|
|
630
633
|
}
|
|
631
634
|
kept.push(...seg.lines);
|
|
635
|
+
keptSourceLines += seg.lines.length;
|
|
632
636
|
}
|
|
633
637
|
} else {
|
|
634
|
-
const contextRatio =
|
|
638
|
+
const contextRatio = totalContextLines > 0 ? contextBudget / totalContextLines : 0;
|
|
639
|
+
let remainingContextBudget = contextBudget;
|
|
635
640
|
|
|
636
641
|
for (let i = 0; i < segments.length; i++) {
|
|
637
642
|
const seg = segments[i];
|
|
638
643
|
|
|
639
644
|
if (seg.isChange) {
|
|
645
|
+
if (keptHunks >= maxHunks) break;
|
|
640
646
|
keptHunks++;
|
|
641
|
-
if (keptHunks > maxHunks) break;
|
|
642
|
-
kept.push(...seg.lines);
|
|
643
|
-
} else if (seg.isEllipsis) {
|
|
644
647
|
kept.push(...seg.lines);
|
|
648
|
+
keptSourceLines += seg.lines.length;
|
|
649
|
+
continue;
|
|
650
|
+
}
|
|
651
|
+
if (remainingContextBudget <= 0) continue;
|
|
652
|
+
|
|
653
|
+
const allowedLines = Math.min(
|
|
654
|
+
remainingContextBudget,
|
|
655
|
+
Math.max(1, Math.floor(seg.lines.length * contextRatio)),
|
|
656
|
+
);
|
|
657
|
+
const outputStart = kept.length;
|
|
658
|
+
let sourceLinesAdded = 0;
|
|
659
|
+
|
|
660
|
+
if (seg.isEllipsis || seg.lines.length <= allowedLines) {
|
|
661
|
+
for (let j = 0; j < allowedLines; j++) {
|
|
662
|
+
kept.push(seg.lines[j]!);
|
|
663
|
+
}
|
|
664
|
+
sourceLinesAdded = allowedLines;
|
|
645
665
|
} else {
|
|
646
|
-
const allowedLines = Math.max(1, Math.floor(seg.lines.length * contextRatio));
|
|
647
666
|
const isBeforeChange = segments[i + 1]?.isChange;
|
|
648
667
|
const isAfterChange = segments[i - 1]?.isChange;
|
|
649
668
|
|
|
650
669
|
if (isBeforeChange && isAfterChange) {
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
670
|
+
if (allowedLines >= 3) {
|
|
671
|
+
const sourceBudget = allowedLines - 1;
|
|
672
|
+
const firstCount = Math.ceil(sourceBudget / 2);
|
|
673
|
+
const lastCount = sourceBudget - firstCount;
|
|
674
|
+
kept.push(...seg.lines.slice(0, firstCount));
|
|
654
675
|
kept.push("");
|
|
655
|
-
kept.push(...seg.lines.slice(-
|
|
676
|
+
if (lastCount > 0) kept.push(...seg.lines.slice(-lastCount));
|
|
677
|
+
sourceLinesAdded = sourceBudget;
|
|
656
678
|
} else {
|
|
657
|
-
|
|
679
|
+
const firstCount = Math.ceil(allowedLines / 2);
|
|
680
|
+
const lastCount = allowedLines - firstCount;
|
|
681
|
+
kept.push(...seg.lines.slice(0, firstCount));
|
|
682
|
+
if (lastCount > 0) kept.push(...seg.lines.slice(-lastCount));
|
|
683
|
+
sourceLinesAdded = allowedLines;
|
|
658
684
|
}
|
|
659
685
|
} else if (isBeforeChange) {
|
|
660
686
|
kept.push(...seg.lines.slice(-allowedLines));
|
|
687
|
+
sourceLinesAdded = allowedLines;
|
|
661
688
|
} else if (isAfterChange) {
|
|
662
689
|
kept.push(...seg.lines.slice(0, allowedLines));
|
|
690
|
+
sourceLinesAdded = allowedLines;
|
|
663
691
|
} else {
|
|
664
|
-
|
|
692
|
+
const take = Math.min(allowedLines, 2);
|
|
693
|
+
kept.push(...seg.lines.slice(0, take));
|
|
694
|
+
sourceLinesAdded = take;
|
|
665
695
|
}
|
|
666
696
|
}
|
|
697
|
+
|
|
698
|
+
keptSourceLines += sourceLinesAdded;
|
|
699
|
+
remainingContextBudget -= kept.length - outputStart;
|
|
667
700
|
}
|
|
668
701
|
}
|
|
669
702
|
|
|
670
|
-
const keptStats = getDiffStats(kept.join("\n"));
|
|
671
703
|
return {
|
|
672
704
|
text: kept.join("\n"),
|
|
673
|
-
hiddenHunks: Math.max(0, totalStats.hunks -
|
|
674
|
-
hiddenLines: Math.max(0, lines.length -
|
|
705
|
+
hiddenHunks: Math.max(0, totalStats.hunks - keptHunks),
|
|
706
|
+
hiddenLines: Math.max(0, lines.length - keptSourceLines),
|
|
675
707
|
};
|
|
676
708
|
}
|
|
677
709
|
|
package/src/tools/think.ts
CHANGED
|
@@ -8,14 +8,26 @@ import { getMarkdownTheme, type Theme } from "../modes/theme/theme";
|
|
|
8
8
|
/** Whether a model transport can suppress native reasoning while private scratchpad thoughts are active. */
|
|
9
9
|
export function supportsExternalThinking(model: Model | null | undefined): boolean {
|
|
10
10
|
if (!model) return false;
|
|
11
|
+
const compat = model.compat;
|
|
11
12
|
const requiresThinking =
|
|
12
13
|
model.api === "anthropic-messages" &&
|
|
13
|
-
|
|
14
|
-
"requiresThinkingEnabled" in
|
|
15
|
-
|
|
14
|
+
compat !== undefined &&
|
|
15
|
+
"requiresThinkingEnabled" in compat &&
|
|
16
|
+
compat.requiresThinkingEnabled === true;
|
|
16
17
|
if (model.reasoning && (requiresThinking || (model.thinking?.requiresEffort && !model.thinking.suppressWhenOff))) {
|
|
17
18
|
return false;
|
|
18
19
|
}
|
|
20
|
+
// Transports that reject `reasoning.effort` (xAI Grok 4 and the other
|
|
21
|
+
// reasoning-only Responses models) cannot honour `forceReasoningOff`, so the
|
|
22
|
+
// scratchpad would run alongside native reasoning instead of replacing it.
|
|
23
|
+
if (
|
|
24
|
+
model.reasoning &&
|
|
25
|
+
compat !== undefined &&
|
|
26
|
+
(("omitReasoningEffort" in compat && compat.omitReasoningEffort === true) ||
|
|
27
|
+
("supportsReasoningEffort" in compat && compat.supportsReasoningEffort === false))
|
|
28
|
+
) {
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
19
31
|
if (model.api === "google-generative-ai" || model.api === "google-gemini-cli" || model.api === "google-vertex") {
|
|
20
32
|
return !model.reasoning || model.thinking?.mode === "budget" || model.thinking?.suppressWhenOff === true;
|
|
21
33
|
}
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
* mechanical {@link SpeakableStream} cleanup — speech never blocks on the
|
|
16
16
|
* model.
|
|
17
17
|
*/
|
|
18
|
-
import { type AssistantMessage, completeSimple } from "@oh-my-pi/pi-ai";
|
|
18
|
+
import { type AssistantMessage, completeSimple, retryTransientCompletion } from "@oh-my-pi/pi-ai";
|
|
19
19
|
import { logger, prompt } from "@oh-my-pi/pi-utils";
|
|
20
20
|
import type { ModelRegistry } from "../config/model-registry";
|
|
21
21
|
import { getModelMatchPreferences, resolveModelRoleValue } from "../config/model-resolver";
|
|
@@ -82,20 +82,25 @@ export class SpeechEnhancer {
|
|
|
82
82
|
if (!apiKey) return null;
|
|
83
83
|
// Resolve metadata after getApiKey so the session-sticky credential is recorded first.
|
|
84
84
|
const metadata = this.#deps.metadataResolver?.(model.provider);
|
|
85
|
-
const
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
85
|
+
const response = await retryTransientCompletion(
|
|
86
|
+
() => {
|
|
87
|
+
const timeout = AbortSignal.timeout(REWRITE_TIMEOUT_MS);
|
|
88
|
+
return completeSimple(
|
|
89
|
+
model,
|
|
90
|
+
{
|
|
91
|
+
systemPrompt: [SYSTEM_PROMPT],
|
|
92
|
+
messages: [{ role: "user", content: boundBlock(block), timestamp: Date.now() }],
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
apiKey: registry.resolver(model, sessionId),
|
|
96
|
+
maxTokens: ANSWER_MAX_TOKENS,
|
|
97
|
+
disableReasoning: true,
|
|
98
|
+
metadata,
|
|
99
|
+
signal: signal ? AbortSignal.any([signal, timeout]) : timeout,
|
|
100
|
+
},
|
|
101
|
+
);
|
|
98
102
|
},
|
|
103
|
+
{ signal },
|
|
99
104
|
);
|
|
100
105
|
if (response.stopReason === "error") {
|
|
101
106
|
logger.debug("speech-enhancer: rewrite errored", { error: response.errorMessage });
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import type { ThinkingLevel } from "@oh-my-pi/pi-agent-core";
|
|
6
6
|
import type { Api, Model } from "@oh-my-pi/pi-ai";
|
|
7
|
-
import { completeSimple } from "@oh-my-pi/pi-ai";
|
|
7
|
+
import { completeSimple, retryTransientCompletion } from "@oh-my-pi/pi-ai";
|
|
8
8
|
import { logger, prompt } from "@oh-my-pi/pi-utils";
|
|
9
9
|
|
|
10
10
|
import type { ModelRegistry } from "../config/model-registry";
|
|
@@ -106,17 +106,19 @@ export async function generateCommitMessage(
|
|
|
106
106
|
|
|
107
107
|
try {
|
|
108
108
|
const maxTokens = COMMIT_MAX_TOKENS;
|
|
109
|
-
const response = await
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
109
|
+
const response = await retryTransientCompletion(() =>
|
|
110
|
+
completeSimple(
|
|
111
|
+
candidate.model,
|
|
112
|
+
{
|
|
113
|
+
systemPrompt: [COMMIT_SYSTEM_PROMPT],
|
|
114
|
+
messages: [{ role: "user", content: userMessage, timestamp: Date.now() }],
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
apiKey: registry.resolver(candidate.model, sessionId),
|
|
118
|
+
maxTokens,
|
|
119
|
+
reasoning: toReasoningEffort(candidate.thinkingLevel),
|
|
120
|
+
},
|
|
121
|
+
),
|
|
120
122
|
);
|
|
121
123
|
|
|
122
124
|
if (response.stopReason === "error") {
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
import { dlopen, FFIType, ptr } from "bun:ffi";
|
|
5
5
|
import * as path from "node:path";
|
|
6
6
|
|
|
7
|
-
import { type Api, type AssistantMessage, completeSimple, type Model } from "@oh-my-pi/pi-ai";
|
|
7
|
+
import { type Api, type AssistantMessage, completeSimple, type Model, retryTransientCompletion } from "@oh-my-pi/pi-ai";
|
|
8
8
|
import { StreamMarkupHealing } from "@oh-my-pi/pi-ai/utils/stream-markup-healing";
|
|
9
9
|
import { isConPTYHosted } from "@oh-my-pi/pi-tui";
|
|
10
10
|
import { isTerminalHeadless, logger, prompt } from "@oh-my-pi/pi-utils";
|
|
@@ -254,24 +254,28 @@ export async function generateTitleOnline(
|
|
|
254
254
|
const maxTokens = TITLE_MAX_TOKENS;
|
|
255
255
|
logger.debug("title-generator: request", { ...modelContext, maxTokens });
|
|
256
256
|
|
|
257
|
-
const response = await
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
257
|
+
const response = await retryTransientCompletion(
|
|
258
|
+
() =>
|
|
259
|
+
completeSimple(
|
|
260
|
+
model,
|
|
261
|
+
{
|
|
262
|
+
systemPrompt,
|
|
263
|
+
messages: [{ role: "user", content: userMessage, timestamp: Date.now() }],
|
|
264
|
+
},
|
|
265
|
+
{
|
|
266
|
+
apiKey: registry.resolver(model, sessionId),
|
|
267
|
+
maxTokens,
|
|
268
|
+
disableReasoning: true,
|
|
269
|
+
// Greedy decode: titling is extraction, not generation. Backends that
|
|
270
|
+
// default temperature high (e.g. Ollama's 0.8) otherwise garble names
|
|
271
|
+
// from the message ("hashline" → "HasHroshi"). Providers whose models
|
|
272
|
+
// reject sampling params drop this via `supportsSamplingParams`.
|
|
273
|
+
temperature: 0,
|
|
274
|
+
metadata,
|
|
275
|
+
signal,
|
|
276
|
+
},
|
|
277
|
+
),
|
|
278
|
+
{ signal },
|
|
275
279
|
);
|
|
276
280
|
|
|
277
281
|
if (response.stopReason === "error") {
|
package/src/vibe/runtime.ts
CHANGED
|
@@ -421,11 +421,17 @@ export class VibeSessionRegistry {
|
|
|
421
421
|
}
|
|
422
422
|
|
|
423
423
|
/**
|
|
424
|
-
* Insert a bare worker record without the spawn
|
|
425
|
-
* lets
|
|
426
|
-
* fake roster + AgentRegistry session without driving a real turn.
|
|
424
|
+
* Insert a bare worker record without the spawn machinery. Test-only —
|
|
425
|
+
* lets focused runtime tests attach an optional synthetic in-flight job.
|
|
427
426
|
*/
|
|
428
|
-
registerRecordForTests(record: {
|
|
427
|
+
registerRecordForTests(record: {
|
|
428
|
+
id: string;
|
|
429
|
+
cli?: VibeCli;
|
|
430
|
+
ownerId: string;
|
|
431
|
+
state?: VibeSessionState;
|
|
432
|
+
jobId?: string;
|
|
433
|
+
}): void {
|
|
434
|
+
const now = Date.now();
|
|
429
435
|
this.#records.set(record.id, {
|
|
430
436
|
id: record.id,
|
|
431
437
|
cli: record.cli ?? "fast",
|
|
@@ -434,8 +440,11 @@ export class VibeSessionRegistry {
|
|
|
434
440
|
parentSessionFile: null,
|
|
435
441
|
agent: getBundledAgent("sonic")!,
|
|
436
442
|
state: record.state ?? "running",
|
|
437
|
-
createdAt:
|
|
438
|
-
lastActivityAt:
|
|
443
|
+
createdAt: now,
|
|
444
|
+
lastActivityAt: now,
|
|
445
|
+
turn: record.jobId
|
|
446
|
+
? { jobId: record.jobId, message: "test turn", startedAt: now, trace: [], toolCount: 0 }
|
|
447
|
+
: undefined,
|
|
439
448
|
queue: [],
|
|
440
449
|
turnCount: 0,
|
|
441
450
|
killed: false,
|
|
@@ -1113,25 +1122,31 @@ export class VibeSessionRegistry {
|
|
|
1113
1122
|
if (job?.status === "running") runningJobs.push(job);
|
|
1114
1123
|
}
|
|
1115
1124
|
|
|
1116
|
-
let
|
|
1125
|
+
let waitEndedByTimeout = false;
|
|
1117
1126
|
if (runningJobs.length > 0 && collectSettled().length === 0) {
|
|
1118
|
-
waited = true;
|
|
1119
1127
|
const timeoutMs = Math.max(1, Math.trunc(args.timeoutMs ?? DEFAULT_WAIT_TIMEOUT_MS));
|
|
1120
1128
|
const watchedJobIds = runningJobs.map(job => job.id);
|
|
1121
1129
|
manager.watchJobs(watchedJobIds);
|
|
1122
|
-
const { promise: timeoutPromise, resolve: timeoutResolve } = Promise.withResolvers<
|
|
1123
|
-
const timeoutHandle = setTimeout(() => timeoutResolve(), timeoutMs);
|
|
1124
|
-
const racePromises: Promise<
|
|
1130
|
+
const { promise: timeoutPromise, resolve: timeoutResolve } = Promise.withResolvers<"timeout">();
|
|
1131
|
+
const timeoutHandle = setTimeout(() => timeoutResolve("timeout"), timeoutMs);
|
|
1132
|
+
const racePromises: Array<Promise<"settled" | "timeout" | "aborted">> = [
|
|
1133
|
+
...runningJobs.map(job => job.promise.then(() => "settled" as const)),
|
|
1134
|
+
timeoutPromise,
|
|
1135
|
+
];
|
|
1125
1136
|
let abortCleanup: (() => void) | undefined;
|
|
1126
1137
|
if (args.signal) {
|
|
1127
|
-
const { promise: abortPromise, resolve: abortResolve } = Promise.withResolvers<
|
|
1128
|
-
const onAbort = () => abortResolve();
|
|
1129
|
-
args.signal.
|
|
1130
|
-
|
|
1138
|
+
const { promise: abortPromise, resolve: abortResolve } = Promise.withResolvers<"aborted">();
|
|
1139
|
+
const onAbort = () => abortResolve("aborted");
|
|
1140
|
+
if (args.signal.aborted) {
|
|
1141
|
+
onAbort();
|
|
1142
|
+
} else {
|
|
1143
|
+
args.signal.addEventListener("abort", onAbort, { once: true });
|
|
1144
|
+
abortCleanup = () => args.signal?.removeEventListener("abort", onAbort);
|
|
1145
|
+
}
|
|
1131
1146
|
racePromises.push(abortPromise);
|
|
1132
1147
|
}
|
|
1133
1148
|
try {
|
|
1134
|
-
await Promise.race(racePromises);
|
|
1149
|
+
waitEndedByTimeout = (await Promise.race(racePromises)) === "timeout";
|
|
1135
1150
|
} finally {
|
|
1136
1151
|
manager.unwatchJobs(watchedJobIds);
|
|
1137
1152
|
clearTimeout(timeoutHandle);
|
|
@@ -1144,7 +1159,7 @@ export class VibeSessionRegistry {
|
|
|
1144
1159
|
// Current in-flight state, independent of the snapshot: a session whose
|
|
1145
1160
|
// watched turn settled may already be mid queued follow-up.
|
|
1146
1161
|
const stillRunning = watched.filter(record => record.turn !== undefined).map(record => record.id);
|
|
1147
|
-
return { settled, stillRunning, timedOut:
|
|
1162
|
+
return { settled, stillRunning, timedOut: waitEndedByTimeout && settled.length === 0 };
|
|
1148
1163
|
}
|
|
1149
1164
|
|
|
1150
1165
|
/** Detach one parent's process-local workers without tombstoning their persisted conversations. */
|