@oh-my-pi/pi-coding-agent 17.2.2 → 17.2.4
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 +55 -0
- package/dist/{CHANGELOG-xfpkakrn.md → CHANGELOG-bpmhv26t.md} +55 -0
- package/dist/cli.js +3294 -3294
- package/dist/types/advisor/advise-tool.d.ts +6 -0
- package/dist/types/advisor/runtime.d.ts +4 -4
- package/dist/types/capability/mcp.d.ts +9 -0
- package/dist/types/cli/models-cli.d.ts +1 -1
- package/dist/types/config/keybindings.d.ts +1 -1
- package/dist/types/config/settings-schema.d.ts +1 -1
- package/dist/types/discovery/helpers.d.ts +6 -0
- package/dist/types/discovery/omp-extension-roots.d.ts +21 -2
- package/dist/types/extensibility/extensions/loader.d.ts +6 -2
- package/dist/types/extensibility/plugins/marketplace/registry.d.ts +2 -2
- package/dist/types/launch/paths.d.ts +2 -1
- package/dist/types/mcp/request-id.d.ts +19 -0
- package/dist/types/mcp/types.d.ts +14 -0
- package/dist/types/memory-backend/index.d.ts +1 -0
- package/dist/types/memory-backend/messages.d.ts +15 -0
- package/dist/types/modes/components/ask-dialog.d.ts +0 -1
- package/dist/types/modes/components/model-browser.d.ts +7 -5
- package/dist/types/modes/components/model-picker.d.ts +9 -3
- package/dist/types/modes/components/read-tool-group.d.ts +9 -0
- package/dist/types/modes/types.d.ts +1 -1
- package/dist/types/registry/agent-lifecycle.d.ts +14 -5
- package/dist/types/registry/agent-registry.d.ts +4 -3
- package/dist/types/sdk.d.ts +1 -1
- package/dist/types/secrets/index.d.ts +6 -8
- package/dist/types/session/agent-session.d.ts +6 -1
- package/dist/types/session/model-controls.d.ts +0 -1
- package/dist/types/session/session-advisors.d.ts +7 -0
- package/dist/types/session/session-maintenance.d.ts +1 -0
- package/dist/types/session/session-manager.d.ts +11 -5
- package/dist/types/system-prompt.d.ts +2 -0
- package/dist/types/tools/browser/launch.d.ts +30 -0
- package/dist/types/tools/browser/registry.d.ts +6 -1
- package/dist/types/tools/browser/shared-daemon.d.ts +26 -0
- package/dist/types/tools/default-renderer.d.ts +3 -0
- package/package.json +12 -12
- package/scripts/legacy-pi-virtual-module.ts +12 -2
- package/src/advisor/advise-tool.ts +18 -0
- package/src/advisor/runtime.ts +6 -6
- package/src/capability/mcp.ts +7 -0
- package/src/cli/models-cli.ts +13 -16
- package/src/collab/host.ts +1 -1
- package/src/config/keybindings.ts +14 -9
- package/src/config/mcp-schema.json +5 -0
- package/src/config/model-registry.ts +7 -1
- package/src/config/settings-schema.ts +1 -1
- package/src/discovery/builtin.ts +10 -0
- package/src/discovery/helpers.ts +10 -0
- package/src/discovery/mcp-json.ts +11 -1
- package/src/discovery/omp-extension-roots.ts +79 -20
- package/src/discovery/omp-plugins.ts +10 -1
- package/src/extensibility/extensions/loader.ts +66 -28
- package/src/extensibility/plugins/marketplace/registry.ts +4 -8
- package/src/launch/broker.ts +59 -41
- package/src/launch/paths.ts +2 -5
- package/src/main.ts +24 -22
- package/src/mcp/config.ts +1 -0
- package/src/mcp/request-id.ts +24 -0
- package/src/mcp/transports/http.ts +4 -2
- package/src/mcp/transports/sse.ts +4 -2
- package/src/mcp/transports/stdio.ts +4 -2
- package/src/mcp/types.ts +15 -0
- package/src/memory-backend/index.ts +1 -0
- package/src/memory-backend/messages.ts +19 -0
- package/src/modes/components/agent-hub.ts +1 -1
- package/src/modes/components/ask-dialog.ts +65 -23
- package/src/modes/components/custom-editor.ts +1 -1
- package/src/modes/components/model-browser.ts +25 -17
- package/src/modes/components/model-picker.ts +10 -6
- package/src/modes/components/read-tool-group.ts +26 -0
- package/src/modes/components/tool-execution.ts +77 -20
- package/src/modes/controllers/command-controller.ts +2 -2
- package/src/modes/controllers/event-controller.ts +182 -10
- package/src/modes/controllers/selector-controller.ts +27 -6
- package/src/modes/types.ts +5 -1
- package/src/prompts/tools/bash.md +1 -1
- package/src/registry/agent-lifecycle.ts +36 -11
- package/src/registry/agent-registry.ts +12 -5
- package/src/sdk.ts +68 -29
- package/src/secrets/index.ts +15 -17
- package/src/session/agent-session.ts +11 -2
- package/src/session/agent-storage.ts +6 -4
- package/src/session/history-storage.ts +4 -2
- package/src/session/messages.ts +31 -7
- package/src/session/model-controls.ts +0 -1
- package/src/session/session-advisors.ts +19 -1
- package/src/session/session-maintenance.ts +5 -2
- package/src/session/session-manager.ts +128 -76
- package/src/session/session-tools.ts +25 -23
- package/src/slash-commands/builtin-registry.ts +2 -2
- package/src/system-prompt.ts +6 -1
- package/src/task/executor.ts +8 -1
- package/src/tiny/text.ts +12 -0
- package/src/tools/bash.ts +11 -5
- package/src/tools/browser/launch.ts +54 -9
- package/src/tools/browser/registry.ts +123 -30
- package/src/tools/browser/shared-daemon.ts +190 -0
- package/src/tools/browser.ts +1 -1
- package/src/tools/default-renderer.ts +19 -4
- package/src/tools/index.ts +10 -6
- package/src/web/search/providers/anthropic.ts +3 -1
- package/src/web/search/providers/codex.ts +31 -6
package/src/sdk.ts
CHANGED
|
@@ -66,6 +66,7 @@ import { CursorExecHandlers, type CursorMcpResourceAdapter } from "./cursor";
|
|
|
66
66
|
import { createBridgeEditTool, createBridgeGrepFactory } from "./cursor-bridge-tools";
|
|
67
67
|
import "./discovery";
|
|
68
68
|
import { initializeWithSettings } from "./discovery";
|
|
69
|
+
import { withOmpExtensionRootScope } from "./discovery/omp-extension-roots";
|
|
69
70
|
import { disposeAllJuliaKernelSessions, disposeJuliaKernelSessionsByOwner } from "./eval/jl/executor";
|
|
70
71
|
import { disposeVmContextsByOwner } from "./eval/js/context-manager";
|
|
71
72
|
import { disposeAllKernelSessions, disposeKernelSessionsByOwner } from "./eval/py/executor";
|
|
@@ -695,12 +696,15 @@ export async function discoverSessionExtensionPaths(
|
|
|
695
696
|
cwd: string,
|
|
696
697
|
settings: Settings,
|
|
697
698
|
): Promise<string[]> {
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
const
|
|
702
|
-
|
|
703
|
-
|
|
699
|
+
const configuredPaths = options.disableExtensionDiscovery
|
|
700
|
+
? (options.additionalExtensionPaths ?? [])
|
|
701
|
+
: [...(options.additionalExtensionPaths ?? []), ...(settings.get("extensions") ?? [])];
|
|
702
|
+
const disabledExtensionIds = options.disableExtensionDiscovery
|
|
703
|
+
? undefined
|
|
704
|
+
: (settings.get("disabledExtensions") ?? []);
|
|
705
|
+
return discoverExtensionPaths(configuredPaths, cwd, disabledExtensionIds, {
|
|
706
|
+
ambient: !options.disableExtensionDiscovery,
|
|
707
|
+
});
|
|
704
708
|
}
|
|
705
709
|
|
|
706
710
|
/**
|
|
@@ -778,9 +782,11 @@ export async function discoverSkills(
|
|
|
778
782
|
export async function discoverContextFiles(
|
|
779
783
|
cwd?: string,
|
|
780
784
|
_agentDir?: string,
|
|
785
|
+
disabledExtensions?: string[],
|
|
781
786
|
): Promise<Array<{ path: string; content: string; depth?: number }>> {
|
|
782
787
|
return await loadContextFilesInternal({
|
|
783
788
|
cwd: cwd ?? getProjectDir(),
|
|
789
|
+
disabledExtensions,
|
|
784
790
|
});
|
|
785
791
|
}
|
|
786
792
|
|
|
@@ -1214,6 +1220,13 @@ export function createAutoLearnCaptureRunner(
|
|
|
1214
1220
|
* ```
|
|
1215
1221
|
*/
|
|
1216
1222
|
export async function createAgentSession(options: CreateAgentSessionOptions = {}): Promise<CreateAgentSessionResult> {
|
|
1223
|
+
const rootMode = options.disableExtensionDiscovery ? "explicit-only" : "merge";
|
|
1224
|
+
return await withOmpExtensionRootScope(options.additionalExtensionPaths ?? [], rootMode, () =>
|
|
1225
|
+
createAgentSessionScoped(options),
|
|
1226
|
+
);
|
|
1227
|
+
}
|
|
1228
|
+
|
|
1229
|
+
async function createAgentSessionScoped(options: CreateAgentSessionOptions): Promise<CreateAgentSessionResult> {
|
|
1217
1230
|
const cwd = options.cwd ?? getProjectDir();
|
|
1218
1231
|
const agentDir = options.agentDir ?? getAgentDir();
|
|
1219
1232
|
const eventBus = options.eventBus ?? new EventBus();
|
|
@@ -1277,14 +1290,15 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
1277
1290
|
? Promise.resolve(options.contextFiles)
|
|
1278
1291
|
: logger.time("discoverContextFiles", discoverContextFiles, cwd, agentDir);
|
|
1279
1292
|
contextFilesPromise.catch(() => {});
|
|
1280
|
-
const
|
|
1293
|
+
const resolveRepoContext = async (repoCwd: string) => {
|
|
1281
1294
|
try {
|
|
1282
|
-
return await resolveActiveRepoContext(
|
|
1295
|
+
return await resolveActiveRepoContext(repoCwd);
|
|
1283
1296
|
} catch (err) {
|
|
1284
1297
|
logger.debug("Failed to resolve active repo context", { err: String(err) });
|
|
1285
1298
|
return null;
|
|
1286
1299
|
}
|
|
1287
|
-
}
|
|
1300
|
+
};
|
|
1301
|
+
const activeRepoContextPromise = logger.time("resolveActiveRepoContext", resolveRepoContext, cwd);
|
|
1288
1302
|
activeRepoContextPromise.catch(() => {});
|
|
1289
1303
|
const watchdogFilesPromise = logger.time("discoverWatchdogFiles", () => discoverWatchdogFiles(cwd, agentDir));
|
|
1290
1304
|
watchdogFilesPromise.catch(() => {});
|
|
@@ -1377,11 +1391,15 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
1377
1391
|
// headless run on an unwritable default config root pays for a feature it
|
|
1378
1392
|
// does not use.
|
|
1379
1393
|
const needsPlaceholderKey = secretEntriesNeedPlaceholderKey([...envEntries, ...fileEntries]);
|
|
1394
|
+
const explicitAgentDir = options.agentDir;
|
|
1380
1395
|
const placeholderKey = needsPlaceholderKey
|
|
1381
|
-
? await getSecretPlaceholderKey(
|
|
1382
|
-
: await getExistingSecretPlaceholderKey(
|
|
1396
|
+
? await getSecretPlaceholderKey(explicitAgentDir)
|
|
1397
|
+
: await getExistingSecretPlaceholderKey(explicitAgentDir);
|
|
1383
1398
|
if (allEntries.length > 0) {
|
|
1384
|
-
obfuscator = new SecretObfuscator(
|
|
1399
|
+
obfuscator = new SecretObfuscator(
|
|
1400
|
+
allEntries,
|
|
1401
|
+
placeholderKey ?? (() => getSecretPlaceholderKeySync(explicitAgentDir)),
|
|
1402
|
+
);
|
|
1385
1403
|
}
|
|
1386
1404
|
if (obfuscator?.hasSecrets() !== true && placeholderKey !== undefined) {
|
|
1387
1405
|
// No configured entry produced an active secret (e.g. only ignored short
|
|
@@ -1591,7 +1609,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
1591
1609
|
}
|
|
1592
1610
|
return result;
|
|
1593
1611
|
};
|
|
1594
|
-
const [
|
|
1612
|
+
const [initialContextFiles, resolvedWorkspaceTree, watchdogFiles, initialActiveRepoContext, discoveredAdvisors] =
|
|
1595
1613
|
await Promise.all([
|
|
1596
1614
|
contextFilesPromise,
|
|
1597
1615
|
raceWithDeadline("buildWorkspaceTree", workspaceTreePromise),
|
|
@@ -1599,6 +1617,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
1599
1617
|
activeRepoContextPromise,
|
|
1600
1618
|
advisorConfigsPromise,
|
|
1601
1619
|
]);
|
|
1620
|
+
let contextFiles = initialContextFiles;
|
|
1602
1621
|
|
|
1603
1622
|
let agent: Agent;
|
|
1604
1623
|
let session!: AgentSession;
|
|
@@ -1633,14 +1652,19 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
1633
1652
|
const agentKind = (options.taskDepth ?? 0) > 0 || options.parentTaskPrefix ? ("sub" as const) : ("main" as const);
|
|
1634
1653
|
let registeredAgentRef: AgentRef | undefined;
|
|
1635
1654
|
/**
|
|
1636
|
-
* Forget the agent ref on teardown — unless
|
|
1637
|
-
*
|
|
1638
|
-
*
|
|
1655
|
+
* Forget the agent ref on teardown — unless it is a retained terminal ref.
|
|
1656
|
+
* Parking disposes the session but keeps the ref addressable (history://,
|
|
1657
|
+
* revive); a hard kill leaves it as a terminal `aborted` tombstone. Both are
|
|
1658
|
+
* detached (session === null) by the time dispose runs, per the AgentRef
|
|
1659
|
+
* invariant, so preserving them never keeps a disposed session reachable — an
|
|
1660
|
+
* aborted ref that still holds a live session is a bug and is unregistered
|
|
1661
|
+
* rather than handed to ensureLive. Only process teardown / a plain release
|
|
1662
|
+
* unregisters.
|
|
1639
1663
|
*/
|
|
1640
1664
|
const unregisterUnlessParked = (): void => {
|
|
1641
1665
|
const ref = registeredAgentRef;
|
|
1642
1666
|
if (!ref || agentRegistry.get(resolvedAgentId) !== ref) return;
|
|
1643
|
-
if (ref.status === "parked") return;
|
|
1667
|
+
if (ref.status === "parked" || (ref.status === "aborted" && !ref.session)) return;
|
|
1644
1668
|
if (AgentLifecycleManager.global().isParking(resolvedAgentId, ref)) return;
|
|
1645
1669
|
agentRegistry.unregister(resolvedAgentId, ref);
|
|
1646
1670
|
};
|
|
@@ -2704,10 +2728,11 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
2704
2728
|
|
|
2705
2729
|
// Existing staged/device paths need write registered before active-set assembly.
|
|
2706
2730
|
// Deferred MCP also registers it now, but refresh activates it only after a server connects.
|
|
2731
|
+
// xd:// mounts never register write: xdev state only exists when the session
|
|
2732
|
+
// already granted a write tool (see createTools), so mounting rides that grant.
|
|
2707
2733
|
const hasDeferrableTools = Array.from(toolRegistry.values()).some(tool => tool.deferrable === true);
|
|
2708
|
-
const hasXdevTools = (toolSession.xdev?.mountedNames.size ?? 0) > 0;
|
|
2709
2734
|
const planModeAvailable = settings.get("plan.enabled");
|
|
2710
|
-
if (!restrictToolNames && (hasDeferrableTools ||
|
|
2735
|
+
if (!restrictToolNames && (hasDeferrableTools || planModeAvailable || deferMCPDiscoveryForUI)) {
|
|
2711
2736
|
await ensureWriteRegistered();
|
|
2712
2737
|
}
|
|
2713
2738
|
|
|
@@ -2783,6 +2808,17 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
2783
2808
|
tools: Map<string, AgentTool>,
|
|
2784
2809
|
): Promise<BuildSystemPromptResult> => {
|
|
2785
2810
|
toolContextStore.setToolNames(toolNames);
|
|
2811
|
+
const promptCwd = sessionManager.getCwd();
|
|
2812
|
+
const activeRepoContext = hasSession
|
|
2813
|
+
? await logger.time("resolveActiveRepoContext", resolveRepoContext, promptCwd)
|
|
2814
|
+
: initialActiveRepoContext;
|
|
2815
|
+
if (hasSession && options.contextFiles === undefined) {
|
|
2816
|
+
contextFiles = await logger.time("discoverContextFiles", discoverContextFiles, promptCwd, agentDir, [
|
|
2817
|
+
...(settings.get("disabledExtensions") ?? []),
|
|
2818
|
+
]);
|
|
2819
|
+
toolSession.contextFiles = contextFiles;
|
|
2820
|
+
session.setAdvisorContextPrompt(formatAdvisorContextPrompt(contextFiles));
|
|
2821
|
+
}
|
|
2786
2822
|
const memoryBackend = restrictToolNames ? undefined : await resolveMemoryBackend(settings);
|
|
2787
2823
|
const memoryInstructions = memoryBackend
|
|
2788
2824
|
? await memoryBackend.buildDeveloperInstructions(agentDir, settings, session)
|
|
@@ -2852,7 +2888,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
2852
2888
|
: options.appendSystemPrompt;
|
|
2853
2889
|
}
|
|
2854
2890
|
const defaultPrompt = await buildSystemPromptInternal({
|
|
2855
|
-
cwd,
|
|
2891
|
+
cwd: promptCwd,
|
|
2856
2892
|
additionalWorkspaceRoots: sessionManager.getAdditionalDirectories(),
|
|
2857
2893
|
xdevTools: toolSession.xdev ? xdevEntries(toolSession.xdev) : [],
|
|
2858
2894
|
xdevDocs: toolSession.xdev
|
|
@@ -2953,6 +2989,9 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
2953
2989
|
const xdevReadAvailable =
|
|
2954
2990
|
builtInRegistryToolNames.has("read") &&
|
|
2955
2991
|
(explicitlyRequestedToolNameSet === undefined || explicitlyRequestedToolNameSet.has("read"));
|
|
2992
|
+
const xdevWriteAvailable =
|
|
2993
|
+
builtInRegistryToolNames.has("write") &&
|
|
2994
|
+
(explicitlyRequestedToolNameSet === undefined || explicitlyRequestedToolNameSet.has("write"));
|
|
2956
2995
|
const initialRequestedActiveToolNames = options.toolNames
|
|
2957
2996
|
? requestedActiveToolNames
|
|
2958
2997
|
: requestedActiveToolNames.filter(name => !defaultInactiveToolNames.has(name));
|
|
@@ -2998,23 +3037,23 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
2998
3037
|
|
|
2999
3038
|
// Partition the initial enabled set for the xd:// transport. Tool instances
|
|
3000
3039
|
// remain in the canonical map; only presentation names move between layers.
|
|
3040
|
+
// Mounting requires both transport halves in the granted set (`read xd://`
|
|
3041
|
+
// discovers, `write xd://<tool>` executes); a session without either keeps
|
|
3042
|
+
// every tool top-level instead of auto-granting the missing transport.
|
|
3001
3043
|
if (toolSession.xdev) {
|
|
3002
3044
|
const topLevelToolNames: string[] = [];
|
|
3003
3045
|
const mountedNames: string[] = [];
|
|
3004
3046
|
for (const name of initialToolNames) {
|
|
3005
3047
|
const tool = toolRegistry.get(name);
|
|
3006
3048
|
const explicitlyRequested = explicitlyRequestedToolNameSet?.has(name) === true;
|
|
3007
|
-
if (tool && xdevReadAvailable && !explicitlyRequested && isMountableUnderXdev(tool))
|
|
3049
|
+
if (tool && xdevReadAvailable && xdevWriteAvailable && !explicitlyRequested && isMountableUnderXdev(tool))
|
|
3008
3050
|
mountedNames.push(name);
|
|
3009
3051
|
else topLevelToolNames.push(name);
|
|
3010
3052
|
}
|
|
3011
|
-
const writeTransportAvailable = mountedNames.length === 0 || (await ensureWriteRegistered());
|
|
3012
3053
|
toolSession.xdev.mountedNames.clear();
|
|
3013
|
-
|
|
3014
|
-
|
|
3015
|
-
|
|
3016
|
-
if (mountedNames.length > 0 && !initialToolNames.includes("write")) initialToolNames.push("write");
|
|
3017
|
-
}
|
|
3054
|
+
for (const name of mountedNames) toolSession.xdev.mountedNames.add(name);
|
|
3055
|
+
initialToolNames = topLevelToolNames;
|
|
3056
|
+
if (mountedNames.length > 0 && !initialToolNames.includes("write")) initialToolNames.push("write");
|
|
3018
3057
|
}
|
|
3019
3058
|
|
|
3020
3059
|
setActiveToolNames(initialToolNames);
|
|
@@ -3271,8 +3310,8 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
3271
3310
|
.map(tool => new ExtensionToolWrapper(wrapToolWithMetaNotice(tool), extensionRunner) as Tool);
|
|
3272
3311
|
|
|
3273
3312
|
const advisorWatchdogPrompts = [...watchdogFiles];
|
|
3274
|
-
if (
|
|
3275
|
-
advisorWatchdogPrompts.push(formatActiveRepoWatchdogPrompt(
|
|
3313
|
+
if (initialActiveRepoContext) {
|
|
3314
|
+
advisorWatchdogPrompts.push(formatActiveRepoWatchdogPrompt(initialActiveRepoContext));
|
|
3276
3315
|
}
|
|
3277
3316
|
const advisorWatchdogPrompt = advisorWatchdogPrompts.length > 0 ? advisorWatchdogPrompts.join("\n\n") : undefined;
|
|
3278
3317
|
// Hand the advisor the same project context files (AGENTS.md, etc.) the
|
package/src/secrets/index.ts
CHANGED
|
@@ -2,7 +2,7 @@ import * as crypto from "node:crypto";
|
|
|
2
2
|
import * as fs from "node:fs";
|
|
3
3
|
import * as path from "node:path";
|
|
4
4
|
import { SENSITIVE_TOKEN_RE } from "@oh-my-pi/pi-ai/providers/transform-messages";
|
|
5
|
-
import {
|
|
5
|
+
import { getSecretPlaceholderKeyPath, isEnoent, logger } from "@oh-my-pi/pi-utils";
|
|
6
6
|
import { YAML } from "bun";
|
|
7
7
|
import { regexHasUnresolvableShortMatchFallback, type SecretEntry, sanitizeSecretFriendlyName } from "./obfuscator";
|
|
8
8
|
import { compileSecretRegex } from "./regex";
|
|
@@ -11,17 +11,15 @@ const PLACEHOLDER_KEY_RE = /^[A-Za-z0-9_-]{43}$/;
|
|
|
11
11
|
const cachedPlaceholderKeys = new Map<string, string>();
|
|
12
12
|
|
|
13
13
|
/**
|
|
14
|
-
* Per-install secret key for the placeholder digest. Persisted under
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
* key file live sessions use, per `~/.omp/agent/secret-placeholder.key` in
|
|
21
|
-
* docs/secrets.md.
|
|
14
|
+
* Per-install secret key for the placeholder digest. Persisted under XDG state
|
|
15
|
+
* and never sent to a provider, so model-visible placeholders cannot be reversed
|
|
16
|
+
* by dictionary-hashing candidate secrets. Stable across sessions so persisted
|
|
17
|
+
* transcripts deobfuscate consistently. Defaults to `getSecretPlaceholderKeyPath()`
|
|
18
|
+
* — `$XDG_STATE_HOME/omp/secret-placeholder.key` (or `~/.omp/agent/secret-placeholder.key`
|
|
19
|
+
* without XDG), per docs/secrets.md.
|
|
22
20
|
*/
|
|
23
|
-
export async function getSecretPlaceholderKey(keyDir
|
|
24
|
-
const keyPath = path.join(keyDir, "secret-placeholder.key");
|
|
21
|
+
export async function getSecretPlaceholderKey(keyDir?: string): Promise<string> {
|
|
22
|
+
const keyPath = keyDir ? path.join(keyDir, "secret-placeholder.key") : getSecretPlaceholderKeyPath();
|
|
25
23
|
const cached = cachedPlaceholderKeys.get(keyPath);
|
|
26
24
|
if (cached !== undefined) return cached;
|
|
27
25
|
|
|
@@ -32,7 +30,7 @@ export async function getSecretPlaceholderKey(keyDir: string = getAgentDir()): P
|
|
|
32
30
|
}
|
|
33
31
|
|
|
34
32
|
const generated = crypto.randomBytes(32).toString("base64url");
|
|
35
|
-
await fs.promises.mkdir(
|
|
33
|
+
await fs.promises.mkdir(path.dirname(keyPath), { recursive: true });
|
|
36
34
|
try {
|
|
37
35
|
await fs.promises.writeFile(keyPath, generated, { flag: "wx", mode: 0o600 });
|
|
38
36
|
cachedPlaceholderKeys.set(keyPath, generated);
|
|
@@ -53,8 +51,8 @@ export async function getSecretPlaceholderKey(keyDir: string = getAgentDir()): P
|
|
|
53
51
|
}
|
|
54
52
|
|
|
55
53
|
/** Return an existing placeholder key for redaction without creating a new key file. */
|
|
56
|
-
export async function getExistingSecretPlaceholderKey(keyDir
|
|
57
|
-
const keyPath = path.join(keyDir, "secret-placeholder.key");
|
|
54
|
+
export async function getExistingSecretPlaceholderKey(keyDir?: string): Promise<string | undefined> {
|
|
55
|
+
const keyPath = keyDir ? path.join(keyDir, "secret-placeholder.key") : getSecretPlaceholderKeyPath();
|
|
58
56
|
const cached = cachedPlaceholderKeys.get(keyPath);
|
|
59
57
|
if (cached !== undefined) return cached;
|
|
60
58
|
// Redaction-only: this key is loaded solely to redact an existing key file from
|
|
@@ -85,8 +83,8 @@ let ephemeralSyncPlaceholderKey: string | undefined;
|
|
|
85
83
|
* throws: an unreadable or unwritable key file degrades to a process-ephemeral
|
|
86
84
|
* key (with a warning) instead of breaking the session.
|
|
87
85
|
*/
|
|
88
|
-
export function getSecretPlaceholderKeySync(keyDir
|
|
89
|
-
const keyPath = path.join(keyDir, "secret-placeholder.key");
|
|
86
|
+
export function getSecretPlaceholderKeySync(keyDir?: string): string {
|
|
87
|
+
const keyPath = keyDir ? path.join(keyDir, "secret-placeholder.key") : getSecretPlaceholderKeyPath();
|
|
90
88
|
const cached = cachedPlaceholderKeys.get(keyPath);
|
|
91
89
|
if (cached !== undefined) return cached;
|
|
92
90
|
try {
|
|
@@ -100,7 +98,7 @@ export function getSecretPlaceholderKeySync(keyDir: string = getAgentDir()): str
|
|
|
100
98
|
}
|
|
101
99
|
const generated = crypto.randomBytes(32).toString("base64url");
|
|
102
100
|
try {
|
|
103
|
-
fs.mkdirSync(
|
|
101
|
+
fs.mkdirSync(path.dirname(keyPath), { recursive: true });
|
|
104
102
|
fs.writeFileSync(keyPath, generated, { flag: "wx", mode: 0o600 });
|
|
105
103
|
cachedPlaceholderKeys.set(keyPath, generated);
|
|
106
104
|
return generated;
|
|
@@ -1382,6 +1382,7 @@ export class AgentSession {
|
|
|
1382
1382
|
extensionRunner: this.#extensionRunner,
|
|
1383
1383
|
sideStreamFn: this.#sideStreamFn,
|
|
1384
1384
|
providerSessionState: this.#providerSessionState,
|
|
1385
|
+
preferWebsockets: this.#preferWebsockets,
|
|
1385
1386
|
model: () => this.model,
|
|
1386
1387
|
thinkingLevel: () => this.thinkingLevel,
|
|
1387
1388
|
isDisposed: () => this.#isDisposed,
|
|
@@ -6333,7 +6334,6 @@ export class AgentSession {
|
|
|
6333
6334
|
selector?: string;
|
|
6334
6335
|
thinkingLevel?: ThinkingLevel;
|
|
6335
6336
|
persist?: boolean;
|
|
6336
|
-
currentContextTokens?: number;
|
|
6337
6337
|
},
|
|
6338
6338
|
): Promise<{ switched: boolean }> {
|
|
6339
6339
|
return this.#models.setModel(model, role, options);
|
|
@@ -7018,7 +7018,7 @@ export class AgentSession {
|
|
|
7018
7018
|
// shared provider state map is still required so Codex can allocate
|
|
7019
7019
|
// websocket state under that side-channel session id.
|
|
7020
7020
|
sessionId: `${cacheSessionId}:side:${Snowflake.next()}`,
|
|
7021
|
-
promptCacheKey:
|
|
7021
|
+
promptCacheKey: this.agent.promptCacheKey ?? this.agent.sessionId,
|
|
7022
7022
|
preferWebsockets: this.#preferWebsockets,
|
|
7023
7023
|
providerSessionState: this.#providerSessionState,
|
|
7024
7024
|
reasoning: toReasoningEffort(this.thinkingLevel),
|
|
@@ -8719,6 +8719,15 @@ export class AgentSession {
|
|
|
8719
8719
|
return this.#advisors.applyAdvisorConfigs(advisors, sharedInstructions);
|
|
8720
8720
|
}
|
|
8721
8721
|
|
|
8722
|
+
/**
|
|
8723
|
+
* Refresh the project context prompt advisor sessions run against after
|
|
8724
|
+
* context files change on `/reload-plugins`. Rebuilds live advisor runtimes so
|
|
8725
|
+
* they stop evaluating turns against stale `AGENTS.md` instructions.
|
|
8726
|
+
*/
|
|
8727
|
+
setAdvisorContextPrompt(contextPrompt: string | undefined): void {
|
|
8728
|
+
this.#advisors.setContextPrompt(contextPrompt);
|
|
8729
|
+
}
|
|
8730
|
+
|
|
8722
8731
|
/**
|
|
8723
8732
|
* Whether the advisor setting is enabled for this session.
|
|
8724
8733
|
*/
|
|
@@ -8,7 +8,7 @@ import {
|
|
|
8
8
|
SqliteAuthCredentialStore,
|
|
9
9
|
type StoredAuthCredential,
|
|
10
10
|
} from "@oh-my-pi/pi-ai";
|
|
11
|
-
import { AsyncDrain, getAgentDbPath, getStatsDbPath, isRecord, logger } from "@oh-my-pi/pi-utils";
|
|
11
|
+
import { AsyncDrain, getAgentDbPath, getDbBusyTimeoutMs, getStatsDbPath, isRecord, logger } from "@oh-my-pi/pi-utils";
|
|
12
12
|
import type { RawSettings as Settings } from "../config/settings";
|
|
13
13
|
|
|
14
14
|
/** Row shape for settings table queries */
|
|
@@ -199,8 +199,10 @@ ON CONFLICT(model_key) DO UPDATE SET
|
|
|
199
199
|
// Install the busy handler BEFORE any lock-taking statement (incl.
|
|
200
200
|
// `PRAGMA journal_mode=WAL`, which acquires an exclusive lock during WAL
|
|
201
201
|
// recovery). Without this, concurrent omp startups can crash here with
|
|
202
|
-
// `SQLITE_BUSY` / `SQLITE_BUSY_RECOVERY`. See issue #2421.
|
|
203
|
-
|
|
202
|
+
// `SQLITE_BUSY` / `SQLITE_BUSY_RECOVERY`. See issue #2421. Headless
|
|
203
|
+
// hosts bound the wait so lock contention cannot freeze the protocol
|
|
204
|
+
// loop for the full interactive timeout.
|
|
205
|
+
this.#db.run(`PRAGMA busy_timeout = ${getDbBusyTimeoutMs()}`);
|
|
204
206
|
this.#db.run(`
|
|
205
207
|
PRAGMA journal_mode=WAL;
|
|
206
208
|
PRAGMA synchronous=NORMAL;
|
|
@@ -571,7 +573,7 @@ FROM model_usage_legacy
|
|
|
571
573
|
async backfillModelPerfFromStats(statsDbPath: string): Promise<number> {
|
|
572
574
|
const statsDb = new Database(statsDbPath, { readonly: true });
|
|
573
575
|
try {
|
|
574
|
-
statsDb.run(
|
|
576
|
+
statsDb.run(`PRAGMA busy_timeout = ${getDbBusyTimeoutMs()}`);
|
|
575
577
|
const select = statsDb.prepare(
|
|
576
578
|
`SELECT rowid, timestamp, provider, model, output_tokens, duration, ttft
|
|
577
579
|
FROM messages
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Database, type Statement } from "bun:sqlite";
|
|
2
2
|
import * as fs from "node:fs";
|
|
3
3
|
import * as path from "node:path";
|
|
4
|
-
import { AsyncDrain, getHistoryDbPath, logger } from "@oh-my-pi/pi-utils";
|
|
4
|
+
import { AsyncDrain, getDbBusyTimeoutMs, getHistoryDbPath, logger } from "@oh-my-pi/pi-utils";
|
|
5
5
|
|
|
6
6
|
export interface HistoryEntry {
|
|
7
7
|
id: number;
|
|
@@ -51,7 +51,9 @@ export class HistoryStorage {
|
|
|
51
51
|
this.#db = new Database(dbPath);
|
|
52
52
|
|
|
53
53
|
// Install the busy handler BEFORE any lock-taking statement. See #2421.
|
|
54
|
-
|
|
54
|
+
// Headless hosts bound the wait so lock contention cannot freeze the
|
|
55
|
+
// protocol loop for the full interactive timeout.
|
|
56
|
+
this.#db.run(`PRAGMA busy_timeout = ${getDbBusyTimeoutMs()}`);
|
|
55
57
|
|
|
56
58
|
const hasFts = this.#db.prepare("SELECT 1 FROM sqlite_master WHERE type='table' AND name='history_fts'").get();
|
|
57
59
|
this.#db.run(`
|
package/src/session/messages.ts
CHANGED
|
@@ -24,6 +24,7 @@ import type {
|
|
|
24
24
|
} from "@oh-my-pi/pi-ai";
|
|
25
25
|
import * as AIError from "@oh-my-pi/pi-ai/error";
|
|
26
26
|
import { isRecord, logger, prompt } from "@oh-my-pi/pi-utils";
|
|
27
|
+
import { COLLAB_PROMPT_MESSAGE_TYPE } from "@oh-my-pi/pi-wire";
|
|
27
28
|
import userInterjectionTemplate from "../prompts/steering/user-interjection.md" with { type: "text" };
|
|
28
29
|
import { formatTitleConversationContext, type TitleConversationTurn } from "../tiny/message-preproc";
|
|
29
30
|
|
|
@@ -614,8 +615,18 @@ export function normalizeCustomMessagePayload<T = unknown>(
|
|
|
614
615
|
};
|
|
615
616
|
}
|
|
616
617
|
|
|
617
|
-
|
|
618
|
-
|
|
618
|
+
type SteeringUserMessage =
|
|
619
|
+
| (UserMessage & { steering: true })
|
|
620
|
+
| (CustomMessage & {
|
|
621
|
+
customType: typeof COLLAB_PROMPT_MESSAGE_TYPE;
|
|
622
|
+
attribution: "user";
|
|
623
|
+
});
|
|
624
|
+
|
|
625
|
+
function isSteeringUserMessage(message: AgentMessage | undefined): message is SteeringUserMessage {
|
|
626
|
+
if (message?.role === "user") return message.steering === true;
|
|
627
|
+
return (
|
|
628
|
+
message?.role === "custom" && message.customType === COLLAB_PROMPT_MESSAGE_TYPE && message.attribution === "user"
|
|
629
|
+
);
|
|
619
630
|
}
|
|
620
631
|
|
|
621
632
|
function userMessageWithoutSteering(message: UserMessage): UserMessage {
|
|
@@ -655,17 +666,26 @@ function getArrayContentImages(content: (TextContent | ImageContent)[]): ImageCo
|
|
|
655
666
|
return images ?? [];
|
|
656
667
|
}
|
|
657
668
|
|
|
658
|
-
function wrapSteeringUserMessage(message:
|
|
669
|
+
function wrapSteeringUserMessage(message: SteeringUserMessage): UserMessage {
|
|
670
|
+
const userMessage: UserMessage =
|
|
671
|
+
message.role === "user"
|
|
672
|
+
? userMessageWithoutSteering(message)
|
|
673
|
+
: {
|
|
674
|
+
role: "user",
|
|
675
|
+
content: message.content,
|
|
676
|
+
attribution: "user",
|
|
677
|
+
timestamp: message.timestamp,
|
|
678
|
+
};
|
|
659
679
|
if (typeof message.content === "string") {
|
|
660
|
-
if (message.content.length === 0) return message;
|
|
661
|
-
return { ...
|
|
680
|
+
if (message.content.length === 0) return message.role === "user" ? message : userMessage;
|
|
681
|
+
return { ...userMessage, content: renderSteeringEnvelope(message.content) };
|
|
662
682
|
}
|
|
663
683
|
|
|
664
684
|
const text = getArrayContentText(message.content);
|
|
665
|
-
if (text.length === 0) return message;
|
|
685
|
+
if (text.length === 0) return message.role === "user" ? message : userMessage;
|
|
666
686
|
const content: (TextContent | ImageContent)[] = [{ type: "text", text: renderSteeringEnvelope(text) }];
|
|
667
687
|
content.push(...getArrayContentImages(message.content));
|
|
668
|
-
return { ...
|
|
688
|
+
return { ...userMessage, content };
|
|
669
689
|
}
|
|
670
690
|
|
|
671
691
|
export function wrapSteeringForModel(messages: AgentMessage[]): AgentMessage[] {
|
|
@@ -1146,6 +1166,10 @@ function convertOne(m: AgentMessage, interruptedNext: boolean): Message[] {
|
|
|
1146
1166
|
}
|
|
1147
1167
|
case "custom": {
|
|
1148
1168
|
if (!isCustomMessageContent(m.content)) return [];
|
|
1169
|
+
if (isSteeringUserMessage(m)) {
|
|
1170
|
+
const converted = convertMessageToLlm(wrapSteeringUserMessage(m));
|
|
1171
|
+
return converted ? [converted] : [];
|
|
1172
|
+
}
|
|
1149
1173
|
if (isUserInvokedSkillPrompt(m)) {
|
|
1150
1174
|
return [
|
|
1151
1175
|
{
|
|
@@ -209,7 +209,6 @@ export class ModelControls {
|
|
|
209
209
|
selector?: string;
|
|
210
210
|
thinkingLevel?: ThinkingLevel;
|
|
211
211
|
persist?: boolean;
|
|
212
|
-
currentContextTokens?: number;
|
|
213
212
|
},
|
|
214
213
|
): Promise<{ switched: boolean }> {
|
|
215
214
|
const previousEditMode = this.#host.resolveActiveEditMode();
|
|
@@ -877,7 +877,10 @@ export class SessionAdvisors {
|
|
|
877
877
|
this.#maintainAdvisorContext(advisorRef, incomingTokens, signal),
|
|
878
878
|
obfuscator: this.#host.obfuscator,
|
|
879
879
|
getModelIdentity: () => formatModelString(advisorRef.agent.state.model),
|
|
880
|
-
beginAdvisorUpdate:
|
|
880
|
+
beginAdvisorUpdate: inProgress => {
|
|
881
|
+
advisorRef.adviseTool.beginUpdate(inProgress);
|
|
882
|
+
advisorRef.emissionGuard.beginUpdate();
|
|
883
|
+
},
|
|
881
884
|
onTurnError: (error, failedMessages, signal) =>
|
|
882
885
|
this.#recoverAdvisorTurn(advisorRef, error, failedMessages, signal),
|
|
883
886
|
onTurnSuccess: async () => {
|
|
@@ -1449,6 +1452,7 @@ export class SessionAdvisors {
|
|
|
1449
1452
|
promptCacheKey: advisorProviderSessionId,
|
|
1450
1453
|
metadata: advisorMetadata,
|
|
1451
1454
|
providerSessionState: this.#host.providerSessionState,
|
|
1455
|
+
preferWebsockets: this.#host.preferWebsockets,
|
|
1452
1456
|
codexCompaction,
|
|
1453
1457
|
},
|
|
1454
1458
|
);
|
|
@@ -1579,6 +1583,20 @@ export class SessionAdvisors {
|
|
|
1579
1583
|
return this.#advisors.length;
|
|
1580
1584
|
}
|
|
1581
1585
|
|
|
1586
|
+
/**
|
|
1587
|
+
* Swap the project context prompt handed to advisor sessions after context
|
|
1588
|
+
* files change (`/reload-plugins` edit/disable). Rebuilds live runtimes in
|
|
1589
|
+
* place so the next advisor turn evaluates against the current instructions;
|
|
1590
|
+
* a no-op when the rendered prompt is unchanged.
|
|
1591
|
+
*/
|
|
1592
|
+
setContextPrompt(contextPrompt: string | undefined): void {
|
|
1593
|
+
if (contextPrompt === this.#advisorContextPrompt) return;
|
|
1594
|
+
this.#advisorContextPrompt = contextPrompt;
|
|
1595
|
+
if (!this.#advisorEnabled || this.#advisors.length === 0) return;
|
|
1596
|
+
this.#stopAdvisorRuntime();
|
|
1597
|
+
this.#buildAdvisorRuntime(true);
|
|
1598
|
+
}
|
|
1599
|
+
|
|
1582
1600
|
/**
|
|
1583
1601
|
* Whether the advisor setting is enabled for this session.
|
|
1584
1602
|
*/
|
|
@@ -180,6 +180,7 @@ export interface SessionMaintenanceHost {
|
|
|
180
180
|
extensionRunner: ExtensionRunner | undefined;
|
|
181
181
|
sideStreamFn: StreamFn;
|
|
182
182
|
providerSessionState: Map<string, ProviderSessionState>;
|
|
183
|
+
preferWebsockets: boolean | undefined;
|
|
183
184
|
model(): Model | undefined;
|
|
184
185
|
thinkingLevel(): ThinkingLevel | undefined;
|
|
185
186
|
isDisposed(): boolean;
|
|
@@ -1539,8 +1540,9 @@ export class SessionMaintenance {
|
|
|
1539
1540
|
thinkingLevel: this.#host.thinkingLevel(),
|
|
1540
1541
|
tools: this.#host.agent.state.tools,
|
|
1541
1542
|
sessionId: this.#host.sessionId(),
|
|
1542
|
-
promptCacheKey: this.#host.sessionId
|
|
1543
|
+
promptCacheKey: this.#host.agent.promptCacheKey ?? this.#host.agent.sessionId,
|
|
1543
1544
|
providerSessionState: this.#host.providerSessionState,
|
|
1545
|
+
preferWebsockets: this.#host.preferWebsockets,
|
|
1544
1546
|
// Route every summarization HTTP request through the
|
|
1545
1547
|
// session's side-stream transport so the provider
|
|
1546
1548
|
// concurrency cap (e.g. providers.ollama-cloud.maxConcurrency)
|
|
@@ -2585,8 +2587,9 @@ export class SessionMaintenance {
|
|
|
2585
2587
|
thinkingLevel: this.#host.thinkingLevel(),
|
|
2586
2588
|
tools: this.#host.agent.state.tools,
|
|
2587
2589
|
sessionId: this.#host.sessionId(),
|
|
2588
|
-
promptCacheKey: this.#host.sessionId
|
|
2590
|
+
promptCacheKey: this.#host.agent.promptCacheKey ?? this.#host.agent.sessionId,
|
|
2589
2591
|
providerSessionState: this.#host.providerSessionState,
|
|
2592
|
+
preferWebsockets: this.#host.preferWebsockets,
|
|
2590
2593
|
codexCompaction,
|
|
2591
2594
|
},
|
|
2592
2595
|
);
|