@oh-my-pi/pi-coding-agent 15.7.2 → 15.7.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 +114 -6
- package/dist/types/cli/args.d.ts +1 -1
- package/dist/types/cli/extension-flags.d.ts +36 -0
- package/dist/types/config/config-file.d.ts +4 -0
- package/dist/types/config/file-lock.d.ts +23 -0
- package/dist/types/config/keybindings.d.ts +2 -1
- package/dist/types/config/model-registry.d.ts +6 -0
- package/dist/types/config/settings-schema.d.ts +69 -65
- package/dist/types/edit/hashline/diff.d.ts +3 -3
- package/dist/types/eval/__tests__/agent-bridge.test.d.ts +1 -0
- package/dist/types/eval/__tests__/budget-bridge.test.d.ts +1 -0
- package/dist/types/eval/__tests__/idle-timeout.test.d.ts +1 -0
- package/dist/types/eval/agent-bridge.d.ts +25 -0
- package/dist/types/eval/backend.d.ts +17 -2
- package/dist/types/eval/budget-bridge.d.ts +29 -0
- package/dist/types/eval/idle-timeout.d.ts +28 -0
- package/dist/types/eval/js/executor.d.ts +8 -0
- package/dist/types/eval/js/tool-bridge.d.ts +2 -1
- package/dist/types/eval/py/executor.d.ts +13 -0
- package/dist/types/exec/bash-executor.d.ts +1 -0
- package/dist/types/extensibility/custom-tools/types.d.ts +2 -2
- package/dist/types/extensibility/extensions/runner.d.ts +7 -0
- package/dist/types/extensibility/plugins/git-url.d.ts +11 -1
- package/dist/types/extensibility/plugins/manager.d.ts +12 -1
- package/dist/types/extensibility/shared-events.d.ts +2 -2
- package/dist/types/internal-urls/local-protocol.d.ts +19 -9
- package/dist/types/internal-urls/types.d.ts +14 -0
- package/dist/types/lsp/client.d.ts +3 -0
- package/dist/types/mcp/manager.d.ts +14 -5
- package/dist/types/memory-backend/index.d.ts +1 -1
- package/dist/types/memory-backend/resolve.d.ts +1 -1
- package/dist/types/memory-backend/types.d.ts +3 -3
- package/dist/types/mnemopi/backend.d.ts +4 -0
- package/dist/types/mnemopi/config.d.ts +29 -0
- package/dist/types/mnemopi/state.d.ts +72 -0
- package/dist/types/modes/components/custom-editor.d.ts +2 -2
- package/dist/types/modes/components/omfg-panel.d.ts +19 -0
- package/dist/types/modes/controllers/command-controller.d.ts +6 -0
- package/dist/types/modes/controllers/input-controller.d.ts +1 -3
- package/dist/types/modes/controllers/omfg-controller.d.ts +10 -0
- package/dist/types/modes/controllers/omfg-rule.d.ts +26 -0
- package/dist/types/modes/gradient-highlight.d.ts +5 -1
- package/dist/types/modes/interactive-mode.d.ts +7 -3
- package/dist/types/modes/magic-keywords.d.ts +14 -0
- package/dist/types/modes/markdown-prose.d.ts +27 -0
- package/dist/types/modes/orchestrate.d.ts +7 -2
- package/dist/types/modes/shared.d.ts +1 -1
- package/dist/types/modes/turn-budget.d.ts +18 -0
- package/dist/types/modes/types.d.ts +7 -3
- package/dist/types/modes/ultrathink.d.ts +7 -2
- package/dist/types/modes/workflow.d.ts +15 -0
- package/dist/types/sdk.d.ts +13 -3
- package/dist/types/session/agent-session.d.ts +36 -17
- package/dist/types/session/session-manager.d.ts +18 -0
- package/dist/types/session/session-storage.d.ts +6 -0
- package/dist/types/session/shake-types.d.ts +24 -0
- package/dist/types/task/executor.d.ts +2 -2
- package/dist/types/task/repair-args.d.ts +52 -0
- package/dist/types/tiny/models.d.ts +1 -1
- package/dist/types/tiny/title-client.d.ts +28 -2
- package/dist/types/tiny/title-protocol.d.ts +8 -5
- package/dist/types/tools/find.d.ts +1 -1
- package/dist/types/tools/index.d.ts +19 -3
- package/dist/types/tools/memory-edit.d.ts +1 -1
- package/dist/types/tools/path-utils.d.ts +7 -0
- package/dist/types/tui/output-block.d.ts +7 -7
- package/examples/sdk/09-api-keys-and-oauth.ts +2 -2
- package/package.json +10 -10
- package/scripts/build-binary.ts +0 -1
- package/src/autoresearch/tools/run-experiment.ts +45 -113
- package/src/cli/args.ts +39 -16
- package/src/cli/extension-flags.ts +48 -0
- package/src/cli/plugin-cli.ts +11 -2
- package/src/cli.ts +59 -0
- package/src/config/config-file.ts +98 -13
- package/src/config/file-lock.ts +60 -17
- package/src/config/keybindings.ts +78 -27
- package/src/config/model-registry.ts +7 -1
- package/src/config/settings-schema.ts +73 -67
- package/src/config/settings.ts +22 -0
- package/src/edit/hashline/diff.ts +81 -24
- package/src/edit/renderer.ts +16 -12
- package/src/eval/__tests__/agent-bridge.test.ts +433 -0
- package/src/eval/__tests__/budget-bridge.test.ts +69 -0
- package/src/eval/__tests__/idle-timeout.test.ts +66 -0
- package/src/eval/__tests__/shared-executors.test.ts +21 -0
- package/src/eval/agent-bridge.ts +295 -0
- package/src/eval/backend.ts +17 -2
- package/src/eval/budget-bridge.ts +48 -0
- package/src/eval/idle-timeout.ts +80 -0
- package/src/eval/js/executor.ts +35 -7
- package/src/eval/js/index.ts +2 -1
- package/src/eval/js/shared/prelude.txt +85 -1
- package/src/eval/js/tool-bridge.ts +9 -0
- package/src/eval/py/executor.ts +41 -14
- package/src/eval/py/index.ts +2 -1
- package/src/eval/py/prelude.py +132 -1
- package/src/exec/bash-executor.ts +2 -3
- package/src/extensibility/custom-tools/types.ts +2 -2
- package/src/extensibility/extensions/runner.ts +12 -2
- package/src/extensibility/plugins/git-url.ts +90 -4
- package/src/extensibility/plugins/manager.ts +103 -7
- package/src/extensibility/shared-events.ts +2 -2
- package/src/internal-urls/docs-index.generated.ts +88 -88
- package/src/internal-urls/local-protocol.ts +23 -11
- package/src/internal-urls/types.ts +15 -0
- package/src/lsp/client.ts +28 -5
- package/src/main.ts +44 -55
- package/src/mcp/manager.ts +87 -4
- package/src/memory-backend/index.ts +1 -1
- package/src/memory-backend/resolve.ts +3 -3
- package/src/memory-backend/types.ts +3 -3
- package/src/{mnemosyne → mnemopi}/backend.ts +70 -70
- package/src/{mnemosyne → mnemopi}/config.ts +36 -36
- package/src/{mnemosyne → mnemopi}/state.ts +67 -67
- package/src/modes/components/agent-dashboard.ts +6 -6
- package/src/modes/components/custom-editor.ts +4 -11
- package/src/modes/components/extensions/state-manager.ts +3 -4
- package/src/modes/components/footer.ts +8 -9
- package/src/modes/components/hook-selector.ts +86 -20
- package/src/modes/components/oauth-selector.ts +93 -21
- package/src/modes/components/omfg-panel.ts +141 -0
- package/src/modes/components/settings-defs.ts +2 -2
- package/src/modes/components/tips.txt +2 -1
- package/src/modes/components/tool-execution.ts +38 -19
- package/src/modes/components/tree-selector.ts +4 -3
- package/src/modes/components/user-message-selector.ts +94 -19
- package/src/modes/components/user-message.ts +8 -1
- package/src/modes/controllers/command-controller.ts +25 -0
- package/src/modes/controllers/event-controller.ts +68 -3
- package/src/modes/controllers/input-controller.ts +14 -11
- package/src/modes/controllers/mcp-command-controller.ts +1 -1
- package/src/modes/controllers/omfg-controller.ts +283 -0
- package/src/modes/controllers/omfg-rule.ts +647 -0
- package/src/modes/controllers/selector-controller.ts +1 -0
- package/src/modes/gradient-highlight.ts +23 -6
- package/src/modes/interactive-mode.ts +41 -7
- package/src/modes/magic-keywords.ts +20 -0
- package/src/modes/markdown-prose.ts +247 -0
- package/src/modes/orchestrate.ts +17 -11
- package/src/modes/shared.ts +3 -11
- package/src/modes/turn-budget.ts +31 -0
- package/src/modes/types.ts +7 -1
- package/src/modes/ultrathink.ts +16 -10
- package/src/modes/utils/hotkeys-markdown.ts +1 -1
- package/src/modes/workflow.ts +42 -0
- package/src/prompts/system/omfg-user.md +51 -0
- package/src/prompts/system/project-prompt.md +3 -2
- package/src/prompts/system/subagent-system-prompt.md +12 -8
- package/src/prompts/system/system-prompt.md +9 -6
- package/src/prompts/system/workflow-notice.md +70 -0
- package/src/prompts/tools/eval.md +13 -1
- package/src/prompts/tools/memory-edit.md +1 -1
- package/src/sdk.ts +63 -33
- package/src/session/agent-session.ts +290 -55
- package/src/session/session-manager.ts +32 -0
- package/src/session/session-storage.ts +68 -8
- package/src/session/shake-types.ts +43 -0
- package/src/slash-commands/builtin-registry.ts +39 -16
- package/src/task/executor.ts +17 -7
- package/src/task/index.ts +9 -8
- package/src/task/repair-args.ts +117 -0
- package/src/tiny/models.ts +2 -2
- package/src/tiny/title-client.ts +133 -43
- package/src/tiny/title-protocol.ts +10 -5
- package/src/tiny/worker.ts +3 -46
- package/src/tools/ast-edit.ts +3 -0
- package/src/tools/ast-grep.ts +3 -0
- package/src/tools/eval.ts +202 -26
- package/src/tools/find.ts +20 -6
- package/src/tools/gh.ts +1 -0
- package/src/tools/grouped-file-output.ts +9 -2
- package/src/tools/index.ts +17 -5
- package/src/tools/memory-edit.ts +4 -4
- package/src/tools/memory-recall.ts +5 -5
- package/src/tools/memory-reflect.ts +5 -5
- package/src/tools/memory-retain.ts +4 -4
- package/src/tools/path-utils.ts +13 -2
- package/src/tools/read.ts +1 -0
- package/src/tools/render-utils.ts +2 -1
- package/src/tools/search.ts +491 -76
- package/src/tui/output-block.ts +37 -75
- package/src/utils/git.ts +9 -3
- package/dist/types/mnemosyne/backend.d.ts +0 -4
- package/dist/types/mnemosyne/config.d.ts +0 -29
- package/dist/types/mnemosyne/state.d.ts +0 -72
- /package/dist/types/{mnemosyne → mnemopi}/index.d.ts +0 -0
- /package/src/{mnemosyne → mnemopi}/index.ts +0 -0
package/src/config/file-lock.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
1
2
|
import * as fs from "node:fs/promises";
|
|
2
|
-
import { isEnoent } from "@oh-my-pi/pi-utils";
|
|
3
|
+
import { isEnoent, logger } from "@oh-my-pi/pi-utils";
|
|
3
4
|
|
|
4
5
|
export interface FileLockOptions {
|
|
5
6
|
staleMs?: number;
|
|
@@ -16,14 +17,15 @@ const DEFAULT_OPTIONS: Required<FileLockOptions> = {
|
|
|
16
17
|
interface LockInfo {
|
|
17
18
|
pid: number;
|
|
18
19
|
timestamp: number;
|
|
20
|
+
token: string;
|
|
19
21
|
}
|
|
20
22
|
|
|
21
23
|
function getLockPath(filePath: string): string {
|
|
22
24
|
return `${filePath}.lock`;
|
|
23
25
|
}
|
|
24
26
|
|
|
25
|
-
async function writeLockInfo(lockPath: string): Promise<void> {
|
|
26
|
-
const info: LockInfo = { pid: process.pid, timestamp: Date.now() };
|
|
27
|
+
async function writeLockInfo(lockPath: string, token: string): Promise<void> {
|
|
28
|
+
const info: LockInfo = { pid: process.pid, timestamp: Date.now(), token };
|
|
27
29
|
await Bun.write(`${lockPath}/info`, JSON.stringify(info));
|
|
28
30
|
}
|
|
29
31
|
|
|
@@ -47,33 +49,58 @@ function isProcessAlive(pid: number): boolean {
|
|
|
47
49
|
|
|
48
50
|
async function isLockStale(lockPath: string, staleMs: number): Promise<boolean> {
|
|
49
51
|
const info = await readLockInfo(lockPath);
|
|
50
|
-
if (
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
52
|
+
if (info) {
|
|
53
|
+
if (!isProcessAlive(info.pid)) return true;
|
|
54
|
+
if (Date.now() - info.timestamp > staleMs) return true;
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
55
57
|
|
|
56
|
-
|
|
58
|
+
// No info file. Either the lock holder is between mkdir and writeLockInfo
|
|
59
|
+
// (fresh dir, do not reap) or the dir was already removed (also do not
|
|
60
|
+
// reap — there is nothing to clean up, and an unguarded fs.rm here would
|
|
61
|
+
// race with another contender's successful mkdir and wipe their dir).
|
|
62
|
+
try {
|
|
63
|
+
const stat = await fs.stat(lockPath);
|
|
64
|
+
return Date.now() - stat.mtimeMs > staleMs;
|
|
65
|
+
} catch (err) {
|
|
66
|
+
if (isEnoent(err)) return false;
|
|
67
|
+
throw err;
|
|
68
|
+
}
|
|
57
69
|
}
|
|
58
70
|
|
|
59
|
-
async function tryAcquireLock(lockPath: string): Promise<
|
|
71
|
+
async function tryAcquireLock(lockPath: string): Promise<string | null> {
|
|
60
72
|
try {
|
|
61
73
|
await fs.mkdir(lockPath);
|
|
62
|
-
|
|
63
|
-
|
|
74
|
+
const token = randomUUID();
|
|
75
|
+
await writeLockInfo(lockPath, token);
|
|
76
|
+
return token;
|
|
64
77
|
} catch (error) {
|
|
65
78
|
if ((error as NodeJS.ErrnoException).code === "EEXIST") {
|
|
66
|
-
return
|
|
79
|
+
return null;
|
|
67
80
|
}
|
|
68
81
|
throw error;
|
|
69
82
|
}
|
|
70
83
|
}
|
|
71
84
|
|
|
72
|
-
async function releaseLock(lockPath: string): Promise<void> {
|
|
85
|
+
async function releaseLock(lockPath: string, expectedToken?: string): Promise<void> {
|
|
73
86
|
try {
|
|
87
|
+
if (expectedToken !== undefined) {
|
|
88
|
+
const info = await readLockInfo(lockPath);
|
|
89
|
+
if (!info || info.token !== expectedToken) {
|
|
90
|
+
// We are not the owner. The lock either expired and was reaped
|
|
91
|
+
// or another process has reclaimed it. Do nothing — releasing
|
|
92
|
+
// here would wipe the rightful owner's lock.
|
|
93
|
+
logger.debug("file-lock: skipping release for non-owned lock", {
|
|
94
|
+
lockPath,
|
|
95
|
+
expectedToken,
|
|
96
|
+
actualToken: info?.token,
|
|
97
|
+
});
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
74
101
|
await fs.rm(lockPath, { recursive: true });
|
|
75
102
|
} catch {
|
|
76
|
-
// Ignore errors on release
|
|
103
|
+
// Ignore errors on release.
|
|
77
104
|
}
|
|
78
105
|
}
|
|
79
106
|
|
|
@@ -92,11 +119,14 @@ async function acquireLock(filePath: string, options: FileLockOptions = {}): Pro
|
|
|
92
119
|
const lockPath = getLockPath(filePath);
|
|
93
120
|
|
|
94
121
|
for (let attempt = 0; attempt < opts.retries; attempt++) {
|
|
95
|
-
|
|
96
|
-
|
|
122
|
+
const token = await tryAcquireLock(lockPath);
|
|
123
|
+
if (token !== null) {
|
|
124
|
+
return () => releaseLock(lockPath, token);
|
|
97
125
|
}
|
|
98
126
|
|
|
99
127
|
if ((await lockExists(lockPath)) && (await isLockStale(lockPath, opts.staleMs))) {
|
|
128
|
+
// Reaping a stale lock — no token because we didn't acquire it. The
|
|
129
|
+
// rightful owner is presumed dead; rm without ownership check.
|
|
100
130
|
await releaseLock(lockPath);
|
|
101
131
|
continue;
|
|
102
132
|
}
|
|
@@ -119,3 +149,16 @@ export async function withFileLock<T>(
|
|
|
119
149
|
await release();
|
|
120
150
|
}
|
|
121
151
|
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Test-only handles for the internal lock primitives. These are NOT part of
|
|
155
|
+
* the public API — they exist so the contract tests can validate token-keyed
|
|
156
|
+
* release semantics and the mkdir-race window without re-implementing them.
|
|
157
|
+
*/
|
|
158
|
+
export const __internalsForTesting = {
|
|
159
|
+
tryAcquireLock,
|
|
160
|
+
releaseLock,
|
|
161
|
+
readLockInfo,
|
|
162
|
+
isLockStale,
|
|
163
|
+
getLockPath,
|
|
164
|
+
};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
2
|
import * as path from "node:path";
|
|
3
3
|
import {
|
|
4
4
|
type Keybinding,
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
KeybindingsManager as TuiKeybindingsManager,
|
|
11
11
|
} from "@oh-my-pi/pi-tui";
|
|
12
12
|
import { getAgentDir, isEnoent, logger } from "@oh-my-pi/pi-utils";
|
|
13
|
+
import { YAML } from "bun";
|
|
13
14
|
|
|
14
15
|
/**
|
|
15
16
|
* Application-level keybindings (coding agent specific).
|
|
@@ -330,17 +331,29 @@ function orderKeybindingsConfig(config: KeybindingsConfig): KeybindingsConfig {
|
|
|
330
331
|
return ordered;
|
|
331
332
|
}
|
|
332
333
|
|
|
334
|
+
const KEYBINDINGS_YML = "keybindings.yml";
|
|
335
|
+
const KEYBINDINGS_YAML = "keybindings.yaml";
|
|
336
|
+
const LEGACY_KEYBINDINGS_JSON = "keybindings.json";
|
|
337
|
+
|
|
338
|
+
interface KeybindingsConfigPaths {
|
|
339
|
+
readPath: string;
|
|
340
|
+
writeBackPath: string;
|
|
341
|
+
}
|
|
342
|
+
|
|
333
343
|
/**
|
|
334
344
|
* Load raw config from a file synchronously.
|
|
335
|
-
* Returns parsed JSON or null if file doesn't exist or is invalid.
|
|
345
|
+
* Returns parsed JSON/YAML or null if file doesn't exist or is invalid.
|
|
336
346
|
*/
|
|
337
347
|
function loadRawConfig(filePath: string): unknown {
|
|
338
348
|
try {
|
|
339
|
-
|
|
340
|
-
|
|
349
|
+
const content = fs.readFileSync(filePath, "utf-8");
|
|
350
|
+
if (filePath.endsWith(".json")) {
|
|
351
|
+
return JSON.parse(content);
|
|
352
|
+
}
|
|
353
|
+
if (filePath.endsWith(".yml") || filePath.endsWith(".yaml")) {
|
|
354
|
+
return YAML.parse(content);
|
|
341
355
|
}
|
|
342
|
-
|
|
343
|
-
return JSON.parse(content);
|
|
356
|
+
throw new Error(`Unsupported keybindings config extension: ${filePath}`);
|
|
344
357
|
} catch (error) {
|
|
345
358
|
if (isEnoent(error)) {
|
|
346
359
|
return null;
|
|
@@ -350,34 +363,67 @@ function loadRawConfig(filePath: string): unknown {
|
|
|
350
363
|
}
|
|
351
364
|
}
|
|
352
365
|
|
|
366
|
+
function writeKeybindingsConfig(filePath: string, config: KeybindingsConfig): boolean {
|
|
367
|
+
try {
|
|
368
|
+
fs.writeFileSync(filePath, YAML.stringify(config, null, 2), "utf-8");
|
|
369
|
+
logger.debug("Migrated keybindings config", { path: filePath });
|
|
370
|
+
return true;
|
|
371
|
+
} catch (error) {
|
|
372
|
+
logger.warn("Failed to write migrated keybindings config", { path: filePath, error: String(error) });
|
|
373
|
+
return false;
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
function resolveKeybindingsConfigPaths(agentDir: string): KeybindingsConfigPaths {
|
|
378
|
+
const ymlPath = path.join(agentDir, KEYBINDINGS_YML);
|
|
379
|
+
if (fs.existsSync(ymlPath)) {
|
|
380
|
+
return { readPath: ymlPath, writeBackPath: ymlPath };
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
const yamlPath = path.join(agentDir, KEYBINDINGS_YAML);
|
|
384
|
+
if (fs.existsSync(yamlPath)) {
|
|
385
|
+
return { readPath: yamlPath, writeBackPath: yamlPath };
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
const jsonPath = path.join(agentDir, LEGACY_KEYBINDINGS_JSON);
|
|
389
|
+
if (fs.existsSync(jsonPath)) {
|
|
390
|
+
return { readPath: jsonPath, writeBackPath: ymlPath };
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
return { readPath: ymlPath, writeBackPath: ymlPath };
|
|
394
|
+
}
|
|
395
|
+
|
|
353
396
|
/**
|
|
354
|
-
*
|
|
355
|
-
*
|
|
397
|
+
* Load and migrate keybindings config.
|
|
398
|
+
* Legacy JSON is read for compatibility, but successful write-back goes to YAML.
|
|
356
399
|
*/
|
|
357
|
-
function loadKeybindingsConfig(
|
|
400
|
+
function loadKeybindingsConfig(
|
|
401
|
+
filePath: string,
|
|
402
|
+
writeBackPath: string | undefined,
|
|
403
|
+
): {
|
|
404
|
+
config: KeybindingsConfig;
|
|
405
|
+
persistedPath: string;
|
|
406
|
+
} {
|
|
358
407
|
const rawConfig = loadRawConfig(filePath);
|
|
359
408
|
|
|
360
409
|
if (rawConfig === null) {
|
|
361
|
-
return {};
|
|
410
|
+
return { config: {}, persistedPath: filePath };
|
|
362
411
|
}
|
|
363
412
|
|
|
364
413
|
const { config: migratedConfig, migrated } = migrateKeybindingNames(rawConfig);
|
|
365
|
-
|
|
414
|
+
const shouldWriteBack = writeBackPath !== undefined && (migrated || writeBackPath !== filePath);
|
|
415
|
+
if (shouldWriteBack) {
|
|
366
416
|
const ordered = orderKeybindingsConfig(migratedConfig);
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
logger.debug("Migrated keybindings config", { path: filePath });
|
|
370
|
-
} catch (error) {
|
|
371
|
-
logger.warn("Failed to write migrated keybindings config", { path: filePath, error: String(error) });
|
|
372
|
-
}
|
|
417
|
+
const persistedPath = writeKeybindingsConfig(writeBackPath, ordered) ? writeBackPath : filePath;
|
|
418
|
+
return { config: migratedConfig, persistedPath };
|
|
373
419
|
}
|
|
374
420
|
|
|
375
|
-
return migratedConfig;
|
|
421
|
+
return { config: migratedConfig, persistedPath: filePath };
|
|
376
422
|
}
|
|
377
423
|
|
|
378
424
|
function migrateKeybindingsConfigFile(agentDir: string): void {
|
|
379
|
-
const
|
|
380
|
-
loadKeybindingsConfig(
|
|
425
|
+
const { readPath, writeBackPath } = resolveKeybindingsConfigPaths(agentDir);
|
|
426
|
+
loadKeybindingsConfig(readPath, writeBackPath);
|
|
381
427
|
}
|
|
382
428
|
|
|
383
429
|
/**
|
|
@@ -393,12 +439,13 @@ export class KeybindingsManager extends TuiKeybindingsManager {
|
|
|
393
439
|
}
|
|
394
440
|
|
|
395
441
|
/**
|
|
396
|
-
* Create from config file at agentDir/keybindings.
|
|
442
|
+
* Create from config file at agentDir/keybindings.yml.
|
|
443
|
+
* Legacy keybindings.json is migrated to keybindings.yml on load.
|
|
397
444
|
*/
|
|
398
445
|
static create(agentDir: string = getAgentDir()): KeybindingsManager {
|
|
399
|
-
const
|
|
400
|
-
const userBindings = KeybindingsManager.#loadFromFile(
|
|
401
|
-
const manager = new KeybindingsManager(userBindings,
|
|
446
|
+
const { readPath, writeBackPath } = resolveKeybindingsConfigPaths(agentDir);
|
|
447
|
+
const { config: userBindings, persistedPath } = KeybindingsManager.#loadFromFile(readPath, writeBackPath);
|
|
448
|
+
const manager = new KeybindingsManager(userBindings, persistedPath);
|
|
402
449
|
// Set globally so getKeybindings() returns this manager
|
|
403
450
|
setKeybindings(manager);
|
|
404
451
|
return manager;
|
|
@@ -416,7 +463,8 @@ export class KeybindingsManager extends TuiKeybindingsManager {
|
|
|
416
463
|
*/
|
|
417
464
|
reload(): void {
|
|
418
465
|
if (!this.#configPath) return;
|
|
419
|
-
|
|
466
|
+
const { config } = KeybindingsManager.#loadFromFile(this.#configPath);
|
|
467
|
+
this.setUserBindings(config);
|
|
420
468
|
}
|
|
421
469
|
|
|
422
470
|
/**
|
|
@@ -437,8 +485,11 @@ export class KeybindingsManager extends TuiKeybindingsManager {
|
|
|
437
485
|
/**
|
|
438
486
|
* Load user bindings from a file, migrating old names if needed.
|
|
439
487
|
*/
|
|
440
|
-
static #loadFromFile(
|
|
441
|
-
|
|
488
|
+
static #loadFromFile(
|
|
489
|
+
filePath: string,
|
|
490
|
+
writeBackPath?: string,
|
|
491
|
+
): { config: KeybindingsConfig; persistedPath: string } {
|
|
492
|
+
return loadKeybindingsConfig(filePath, writeBackPath);
|
|
442
493
|
}
|
|
443
494
|
}
|
|
444
495
|
|
|
@@ -851,6 +851,12 @@ export class ModelRegistry {
|
|
|
851
851
|
|
|
852
852
|
/**
|
|
853
853
|
* @param authStorage - Auth storage for API key resolution
|
|
854
|
+
*
|
|
855
|
+
* Sync constructor — eagerly loads bundled + cached models so tests and
|
|
856
|
+
* synchronous callers see a fully-populated registry immediately. Production
|
|
857
|
+
* boot paths SHOULD prefer {@link ModelRegistry.create} so the YAML/JSONC
|
|
858
|
+
* migration step lands off the event loop's hot path before the first
|
|
859
|
+
* `tryLoad()` runs.
|
|
854
860
|
*/
|
|
855
861
|
constructor(
|
|
856
862
|
readonly authStorage: AuthStorage,
|
|
@@ -866,7 +872,7 @@ export class ModelRegistry {
|
|
|
866
872
|
}
|
|
867
873
|
return undefined;
|
|
868
874
|
});
|
|
869
|
-
// Load models synchronously in constructor
|
|
875
|
+
// Load models synchronously in constructor.
|
|
870
876
|
this.#loadModels();
|
|
871
877
|
}
|
|
872
878
|
|
|
@@ -1136,12 +1136,13 @@ export const SETTINGS_SCHEMA = {
|
|
|
1136
1136
|
|
|
1137
1137
|
"compaction.strategy": {
|
|
1138
1138
|
type: "enum",
|
|
1139
|
-
values: ["context-full", "handoff", "off"] as const,
|
|
1139
|
+
values: ["context-full", "handoff", "shake", "off"] as const,
|
|
1140
1140
|
default: "context-full",
|
|
1141
1141
|
ui: {
|
|
1142
1142
|
tab: "context",
|
|
1143
1143
|
label: "Compaction Strategy",
|
|
1144
|
-
description:
|
|
1144
|
+
description:
|
|
1145
|
+
"Choose in-place context-full maintenance, auto-handoff, surgical shake (drop heavy content), or disable auto maintenance (off)",
|
|
1145
1146
|
options: [
|
|
1146
1147
|
{
|
|
1147
1148
|
value: "context-full",
|
|
@@ -1149,6 +1150,11 @@ export const SETTINGS_SCHEMA = {
|
|
|
1149
1150
|
description: "Summarize in-place and keep the current session",
|
|
1150
1151
|
},
|
|
1151
1152
|
{ value: "handoff", label: "Handoff", description: "Generate handoff and continue in a new session" },
|
|
1153
|
+
{
|
|
1154
|
+
value: "shake",
|
|
1155
|
+
label: "Shake",
|
|
1156
|
+
description: "Drop heavy content (tool results + large blocks) in place; recover via artifact",
|
|
1157
|
+
},
|
|
1152
1158
|
{
|
|
1153
1159
|
value: "off",
|
|
1154
1160
|
label: "Off",
|
|
@@ -1327,70 +1333,70 @@ export const SETTINGS_SCHEMA = {
|
|
|
1327
1333
|
"memories.summaryInjectionTokenLimit": { type: "number", default: 5000 },
|
|
1328
1334
|
|
|
1329
1335
|
// Memory backend selector — picks between local memories pipeline,
|
|
1330
|
-
//
|
|
1336
|
+
// Mnemopi local SQLite, Hindsight remote memory, or off. Legacy
|
|
1331
1337
|
// `memories.enabled` keeps gating the local backend; see config/settings.ts
|
|
1332
1338
|
// migration for details.
|
|
1333
1339
|
"memory.backend": {
|
|
1334
1340
|
type: "enum",
|
|
1335
|
-
values: ["off", "local", "hindsight", "
|
|
1341
|
+
values: ["off", "local", "hindsight", "mnemopi"] as const,
|
|
1336
1342
|
default: "off",
|
|
1337
1343
|
ui: {
|
|
1338
1344
|
tab: "memory",
|
|
1339
1345
|
label: "Memory Backend",
|
|
1340
|
-
description: "Off, local summary pipeline,
|
|
1346
|
+
description: "Off, local summary pipeline, Mnemopi SQLite, or Hindsight remote memory",
|
|
1341
1347
|
options: [
|
|
1342
1348
|
{ value: "off", label: "Off", description: "No memory subsystem runs" },
|
|
1343
1349
|
{ value: "local", label: "Local", description: "Local rollout summarisation pipeline (memory_summary.md)" },
|
|
1344
1350
|
{ value: "hindsight", label: "Hindsight", description: "Vectorize Hindsight remote memory service" },
|
|
1345
1351
|
{
|
|
1346
|
-
value: "
|
|
1347
|
-
label: "
|
|
1352
|
+
value: "mnemopi",
|
|
1353
|
+
label: "Mnemopi",
|
|
1348
1354
|
description: "Local SQLite recall/retain backend with optional embeddings",
|
|
1349
1355
|
},
|
|
1350
1356
|
],
|
|
1351
1357
|
},
|
|
1352
1358
|
},
|
|
1353
1359
|
|
|
1354
|
-
//
|
|
1355
|
-
"
|
|
1360
|
+
// Mnemopi local SQLite memory backend.
|
|
1361
|
+
"mnemopi.dbPath": {
|
|
1356
1362
|
type: "string",
|
|
1357
1363
|
default: undefined,
|
|
1358
1364
|
ui: {
|
|
1359
1365
|
tab: "memory",
|
|
1360
|
-
label: "
|
|
1366
|
+
label: "Mnemopi DB Path",
|
|
1361
1367
|
description: "Optional SQLite DB path. Defaults to the agent memories directory.",
|
|
1362
|
-
condition: "
|
|
1368
|
+
condition: "mnemopiActive",
|
|
1363
1369
|
},
|
|
1364
1370
|
},
|
|
1365
|
-
"
|
|
1371
|
+
"mnemopi.bank": {
|
|
1366
1372
|
type: "string",
|
|
1367
1373
|
default: undefined,
|
|
1368
1374
|
ui: {
|
|
1369
1375
|
tab: "memory",
|
|
1370
|
-
label: "
|
|
1376
|
+
label: "Mnemopi Bank",
|
|
1371
1377
|
description: "Optional shared bank base name. Per-project modes derive project-local banks from it.",
|
|
1372
|
-
condition: "
|
|
1378
|
+
condition: "mnemopiActive",
|
|
1373
1379
|
},
|
|
1374
1380
|
},
|
|
1375
|
-
"
|
|
1381
|
+
"mnemopi.scoping": {
|
|
1376
1382
|
type: "enum",
|
|
1377
1383
|
values: ["global", "per-project", "per-project-tagged"] as const,
|
|
1378
1384
|
default: "per-project",
|
|
1379
1385
|
ui: {
|
|
1380
1386
|
tab: "memory",
|
|
1381
|
-
label: "
|
|
1387
|
+
label: "Mnemopi Scoping",
|
|
1382
1388
|
description:
|
|
1383
1389
|
"global = one shared bank; per-project = isolated bank per cwd; per-project-tagged = project-local writes plus global recall visibility",
|
|
1384
1390
|
options: [
|
|
1385
1391
|
{
|
|
1386
1392
|
value: "global",
|
|
1387
1393
|
label: "Global",
|
|
1388
|
-
description: "One shared
|
|
1394
|
+
description: "One shared Mnemopi bank for every project",
|
|
1389
1395
|
},
|
|
1390
1396
|
{
|
|
1391
1397
|
value: "per-project",
|
|
1392
1398
|
label: "Per project",
|
|
1393
|
-
description: "Project-local
|
|
1399
|
+
description: "Project-local Mnemopi bank per cwd basename",
|
|
1394
1400
|
},
|
|
1395
1401
|
{
|
|
1396
1402
|
value: "per-project-tagged",
|
|
@@ -1398,121 +1404,121 @@ export const SETTINGS_SCHEMA = {
|
|
|
1398
1404
|
description: "Write to a project-local bank but merge project + shared recall results",
|
|
1399
1405
|
},
|
|
1400
1406
|
],
|
|
1401
|
-
condition: "
|
|
1407
|
+
condition: "mnemopiActive",
|
|
1402
1408
|
},
|
|
1403
1409
|
},
|
|
1404
|
-
"
|
|
1410
|
+
"mnemopi.autoRecall": {
|
|
1405
1411
|
type: "boolean",
|
|
1406
1412
|
default: true,
|
|
1407
1413
|
ui: {
|
|
1408
1414
|
tab: "memory",
|
|
1409
|
-
label: "
|
|
1415
|
+
label: "Mnemopi Auto Recall",
|
|
1410
1416
|
description: "Recall local memories into the first turn of each session",
|
|
1411
|
-
condition: "
|
|
1417
|
+
condition: "mnemopiActive",
|
|
1412
1418
|
},
|
|
1413
1419
|
},
|
|
1414
|
-
"
|
|
1420
|
+
"mnemopi.autoRetain": {
|
|
1415
1421
|
type: "boolean",
|
|
1416
1422
|
default: true,
|
|
1417
1423
|
ui: {
|
|
1418
1424
|
tab: "memory",
|
|
1419
|
-
label: "
|
|
1420
|
-
description: "Retain completed conversation turns into local
|
|
1421
|
-
condition: "
|
|
1425
|
+
label: "Mnemopi Auto Retain",
|
|
1426
|
+
description: "Retain completed conversation turns into local Mnemopi memory",
|
|
1427
|
+
condition: "mnemopiActive",
|
|
1422
1428
|
},
|
|
1423
1429
|
},
|
|
1424
|
-
"
|
|
1430
|
+
"mnemopi.noEmbeddings": {
|
|
1425
1431
|
type: "boolean",
|
|
1426
1432
|
default: false,
|
|
1427
1433
|
ui: {
|
|
1428
1434
|
tab: "memory",
|
|
1429
|
-
label: "
|
|
1435
|
+
label: "Mnemopi Disable Embeddings",
|
|
1430
1436
|
description: "Force deterministic FTS-only recall instead of vector embeddings",
|
|
1431
|
-
condition: "
|
|
1437
|
+
condition: "mnemopiActive",
|
|
1432
1438
|
},
|
|
1433
1439
|
},
|
|
1434
|
-
"
|
|
1440
|
+
"mnemopi.embeddingModel": {
|
|
1435
1441
|
type: "string",
|
|
1436
1442
|
default: undefined,
|
|
1437
1443
|
ui: {
|
|
1438
1444
|
tab: "memory",
|
|
1439
|
-
label: "
|
|
1440
|
-
description: "Optional embedding model override passed to
|
|
1441
|
-
condition: "
|
|
1445
|
+
label: "Mnemopi Embedding Model",
|
|
1446
|
+
description: "Optional embedding model override passed to Mnemopi",
|
|
1447
|
+
condition: "mnemopiActive",
|
|
1442
1448
|
},
|
|
1443
1449
|
},
|
|
1444
|
-
"
|
|
1450
|
+
"mnemopi.embeddingApiUrl": {
|
|
1445
1451
|
type: "string",
|
|
1446
1452
|
default: undefined,
|
|
1447
1453
|
ui: {
|
|
1448
1454
|
tab: "memory",
|
|
1449
|
-
label: "
|
|
1450
|
-
description: "Optional OpenAI-compatible embedding endpoint passed to
|
|
1451
|
-
condition: "
|
|
1455
|
+
label: "Mnemopi Embedding API URL",
|
|
1456
|
+
description: "Optional OpenAI-compatible embedding endpoint passed to Mnemopi",
|
|
1457
|
+
condition: "mnemopiActive",
|
|
1452
1458
|
},
|
|
1453
1459
|
},
|
|
1454
|
-
"
|
|
1460
|
+
"mnemopi.embeddingApiKey": {
|
|
1455
1461
|
type: "string",
|
|
1456
1462
|
default: undefined,
|
|
1457
1463
|
ui: {
|
|
1458
1464
|
tab: "memory",
|
|
1459
|
-
label: "
|
|
1460
|
-
description: "Optional embedding API key passed to
|
|
1461
|
-
condition: "
|
|
1465
|
+
label: "Mnemopi Embedding API Key",
|
|
1466
|
+
description: "Optional embedding API key passed to Mnemopi",
|
|
1467
|
+
condition: "mnemopiActive",
|
|
1462
1468
|
},
|
|
1463
1469
|
},
|
|
1464
|
-
"
|
|
1470
|
+
"mnemopi.llmMode": {
|
|
1465
1471
|
type: "enum",
|
|
1466
1472
|
values: ["none", "smol", "remote"] as const,
|
|
1467
1473
|
default: "smol",
|
|
1468
1474
|
ui: {
|
|
1469
1475
|
tab: "memory",
|
|
1470
|
-
label: "
|
|
1476
|
+
label: "Mnemopi LLM Mode",
|
|
1471
1477
|
description: "Use no LLM, the configured smol model, or a remote OpenAI-compatible endpoint",
|
|
1472
|
-
condition: "
|
|
1478
|
+
condition: "mnemopiActive",
|
|
1473
1479
|
options: [
|
|
1474
|
-
{ value: "none", label: "None", description: "Disable
|
|
1480
|
+
{ value: "none", label: "None", description: "Disable Mnemopi LLM-backed extraction" },
|
|
1475
1481
|
{ value: "smol", label: "Smol", description: "Use the configured pi-ai smol model" },
|
|
1476
|
-
{ value: "remote", label: "Remote", description: "Use the
|
|
1482
|
+
{ value: "remote", label: "Remote", description: "Use the Mnemopi remote LLM settings below" },
|
|
1477
1483
|
],
|
|
1478
1484
|
},
|
|
1479
1485
|
},
|
|
1480
|
-
"
|
|
1486
|
+
"mnemopi.llmBaseUrl": {
|
|
1481
1487
|
type: "string",
|
|
1482
1488
|
default: undefined,
|
|
1483
1489
|
ui: {
|
|
1484
1490
|
tab: "memory",
|
|
1485
|
-
label: "
|
|
1486
|
-
description: "Optional OpenAI-compatible LLM endpoint for
|
|
1487
|
-
condition: "
|
|
1491
|
+
label: "Mnemopi LLM Base URL",
|
|
1492
|
+
description: "Optional OpenAI-compatible LLM endpoint for Mnemopi remote mode",
|
|
1493
|
+
condition: "mnemopiActive",
|
|
1488
1494
|
},
|
|
1489
1495
|
},
|
|
1490
|
-
"
|
|
1496
|
+
"mnemopi.llmApiKey": {
|
|
1491
1497
|
type: "string",
|
|
1492
1498
|
default: undefined,
|
|
1493
1499
|
ui: {
|
|
1494
1500
|
tab: "memory",
|
|
1495
|
-
label: "
|
|
1496
|
-
description: "Optional LLM API key for
|
|
1497
|
-
condition: "
|
|
1501
|
+
label: "Mnemopi LLM API Key",
|
|
1502
|
+
description: "Optional LLM API key for Mnemopi remote mode",
|
|
1503
|
+
condition: "mnemopiActive",
|
|
1498
1504
|
},
|
|
1499
1505
|
},
|
|
1500
|
-
"
|
|
1506
|
+
"mnemopi.llmModel": {
|
|
1501
1507
|
type: "string",
|
|
1502
1508
|
default: undefined,
|
|
1503
1509
|
ui: {
|
|
1504
1510
|
tab: "memory",
|
|
1505
|
-
label: "
|
|
1506
|
-
description: "Optional LLM model name for
|
|
1507
|
-
condition: "
|
|
1511
|
+
label: "Mnemopi LLM Model",
|
|
1512
|
+
description: "Optional LLM model name for Mnemopi remote mode",
|
|
1513
|
+
condition: "mnemopiActive",
|
|
1508
1514
|
},
|
|
1509
1515
|
},
|
|
1510
|
-
"
|
|
1511
|
-
"
|
|
1512
|
-
"
|
|
1513
|
-
"
|
|
1514
|
-
"
|
|
1515
|
-
"
|
|
1516
|
+
"mnemopi.retainEveryNTurns": { type: "number", default: 4 },
|
|
1517
|
+
"mnemopi.recallLimit": { type: "number", default: 8 },
|
|
1518
|
+
"mnemopi.recallContextTurns": { type: "number", default: 3 },
|
|
1519
|
+
"mnemopi.recallMaxQueryChars": { type: "number", default: 4000 },
|
|
1520
|
+
"mnemopi.injectionTokenLimit": { type: "number", default: 5000 },
|
|
1521
|
+
"mnemopi.debug": { type: "boolean", default: false },
|
|
1516
1522
|
|
|
1517
1523
|
// Hindsight (https://hindsight.vectorize.io)
|
|
1518
1524
|
"hindsight.apiUrl": {
|
|
@@ -2984,8 +2990,8 @@ export const SETTINGS_SCHEMA = {
|
|
|
2984
2990
|
tab: "memory",
|
|
2985
2991
|
label: "Memory Model",
|
|
2986
2992
|
description:
|
|
2987
|
-
"
|
|
2988
|
-
condition: "
|
|
2993
|
+
"Mnemopi LLM for fact extraction + consolidation: online (smol/remote) by default, or a local on-device model",
|
|
2994
|
+
condition: "mnemopiActive",
|
|
2989
2995
|
options: TINY_MEMORY_MODEL_OPTIONS,
|
|
2990
2996
|
},
|
|
2991
2997
|
},
|
|
@@ -3292,7 +3298,7 @@ export type TreeFilterMode = SettingValue<"treeFilterMode">;
|
|
|
3292
3298
|
|
|
3293
3299
|
export interface CompactionSettings {
|
|
3294
3300
|
enabled: boolean;
|
|
3295
|
-
strategy: "context-full" | "handoff" | "off";
|
|
3301
|
+
strategy: "context-full" | "handoff" | "shake" | "off";
|
|
3296
3302
|
thresholdPercent: number;
|
|
3297
3303
|
thresholdTokens: number;
|
|
3298
3304
|
reserveTokens: number;
|
package/src/config/settings.ts
CHANGED
|
@@ -663,6 +663,16 @@ export class Settings {
|
|
|
663
663
|
raw["edit.mode"] = "hashline";
|
|
664
664
|
}
|
|
665
665
|
|
|
666
|
+
// compaction.strategy: removed local-model shake-summary mode; plain shake
|
|
667
|
+
// keeps the same mechanical artifact-backed reduction without background CPU.
|
|
668
|
+
const compactionObj = raw.compaction as Record<string, unknown> | undefined;
|
|
669
|
+
if (compactionObj?.strategy === "shake-summary") {
|
|
670
|
+
compactionObj.strategy = "shake";
|
|
671
|
+
}
|
|
672
|
+
if (raw["compaction.strategy"] === "shake-summary") {
|
|
673
|
+
raw["compaction.strategy"] = "shake";
|
|
674
|
+
}
|
|
675
|
+
|
|
666
676
|
// statusLine: rename "plan_mode" segment to "mode"
|
|
667
677
|
const statusLineObj = raw.statusLine as Record<string, unknown> | undefined;
|
|
668
678
|
if (statusLineObj) {
|
|
@@ -692,6 +702,18 @@ export class Settings {
|
|
|
692
702
|
raw.memory = memoryRoot;
|
|
693
703
|
}
|
|
694
704
|
|
|
705
|
+
// Rename the legacy local `mnemosyne` memory backend to `mnemopi`.
|
|
706
|
+
// - `memory.backend: "mnemosyne"` now selects the renamed backend.
|
|
707
|
+
// - the top-level `mnemosyne` settings object becomes `mnemopi`.
|
|
708
|
+
// Idempotent: skips the object move once `mnemopi` is materialised.
|
|
709
|
+
if (memoryBackendObj && memoryBackendObj.backend === "mnemosyne") {
|
|
710
|
+
memoryBackendObj.backend = "mnemopi";
|
|
711
|
+
}
|
|
712
|
+
if ("mnemosyne" in raw && !("mnemopi" in raw)) {
|
|
713
|
+
raw.mnemopi = raw.mnemosyne;
|
|
714
|
+
delete raw.mnemosyne;
|
|
715
|
+
}
|
|
716
|
+
|
|
695
717
|
// hindsight: dynamicBankId/agentName -> scoping enum + bankId
|
|
696
718
|
// - dynamicBankId=true → scoping="per-project" (closest semantic match;
|
|
697
719
|
// the legacy `agent::project::channel::user` tuple was per-project in
|