@oh-my-pi/pi-coding-agent 17.0.7 → 17.0.8
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 +59 -0
- package/dist/cli.js +4967 -4948
- package/dist/types/config/model-registry.d.ts +1 -1
- package/dist/types/config/model-resolver.d.ts +5 -1
- package/dist/types/config/settings-schema.d.ts +16 -0
- package/dist/types/dap/client.d.ts +19 -0
- package/dist/types/extensibility/extensions/runner.d.ts +4 -2
- package/dist/types/extensibility/extensions/types.d.ts +3 -0
- package/dist/types/extensibility/typebox.d.ts +4 -0
- package/dist/types/hindsight/client.d.ts +10 -0
- package/dist/types/hindsight/config.d.ts +8 -0
- package/dist/types/mcp/tool-bridge.d.ts +9 -0
- package/dist/types/mnemopi/state.d.ts +2 -0
- package/dist/types/modes/components/user-message.d.ts +25 -1
- package/dist/types/modes/controllers/extension-ui-controller.d.ts +8 -0
- package/dist/types/modes/interactive-mode.d.ts +7 -1
- package/dist/types/modes/types.d.ts +14 -1
- package/dist/types/modes/utils/ui-helpers.d.ts +12 -8
- package/dist/types/sdk.d.ts +1 -0
- package/dist/types/session/agent-session-error-log.test.d.ts +1 -0
- package/dist/types/session/agent-session.d.ts +78 -2
- package/dist/types/task/executor.d.ts +10 -0
- package/dist/types/task/index.d.ts +1 -0
- package/dist/types/task/prewalk.d.ts +3 -0
- package/dist/types/tools/ask.d.ts +10 -0
- package/dist/types/tools/tool-timeouts.d.ts +8 -2
- package/dist/types/utils/changelog.d.ts +4 -6
- package/package.json +12 -13
- package/src/cli/models-cli.ts +37 -17
- package/src/cli/update-cli.ts +14 -1
- package/src/config/model-registry.ts +47 -24
- package/src/config/model-resolver.ts +56 -31
- package/src/config/settings-schema.ts +5 -0
- package/src/config/settings.ts +68 -5
- package/src/dap/client.ts +86 -7
- package/src/edit/diff.ts +4 -4
- package/src/eval/py/prelude.py +5 -5
- package/src/export/html/template.js +23 -9
- package/src/extensibility/extensions/runner.ts +9 -4
- package/src/extensibility/extensions/types.ts +3 -0
- package/src/extensibility/typebox.ts +22 -9
- package/src/hindsight/client.test.ts +42 -0
- package/src/hindsight/client.ts +40 -3
- package/src/hindsight/config.ts +18 -0
- package/src/launch/broker.ts +31 -5
- package/src/lsp/index.ts +1 -1
- package/src/main.ts +15 -5
- package/src/mcp/tool-bridge.ts +9 -0
- package/src/mnemopi/state.ts +70 -3
- package/src/modes/components/agent-dashboard.ts +6 -2
- package/src/modes/components/chat-transcript-builder.ts +13 -2
- package/src/modes/components/diff.ts +2 -2
- package/src/modes/components/plan-review-overlay.ts +20 -4
- package/src/modes/components/user-message.ts +100 -1
- package/src/modes/controllers/command-controller.ts +22 -5
- package/src/modes/controllers/event-controller.ts +4 -1
- package/src/modes/controllers/extension-ui-controller.ts +18 -0
- package/src/modes/controllers/input-controller.ts +47 -12
- package/src/modes/controllers/selector-controller.ts +82 -6
- package/src/modes/interactive-mode.ts +36 -4
- package/src/modes/types.ts +16 -3
- package/src/modes/utils/ui-helpers.ts +40 -20
- package/src/prompts/system/workflow-notice.md +1 -1
- package/src/sdk.ts +134 -48
- package/src/session/agent-session-error-log.test.ts +59 -0
- package/src/session/agent-session.ts +300 -26
- package/src/system-prompt.ts +1 -2
- package/src/task/executor.ts +41 -27
- package/src/task/index.ts +11 -0
- package/src/task/prewalk.ts +6 -0
- package/src/task/worktree.ts +25 -11
- package/src/tools/ask.ts +15 -0
- package/src/tools/bash.ts +14 -6
- package/src/tools/browser.ts +1 -1
- package/src/tools/debug.ts +1 -1
- package/src/tools/eval.ts +4 -5
- package/src/tools/fetch.ts +1 -1
- package/src/tools/hub/launch.ts +7 -0
- package/src/tools/tool-timeouts.ts +10 -3
- package/src/tools/write.ts +31 -0
- package/src/utils/changelog.ts +54 -58
- package/src/utils/git.ts +57 -49
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@oh-my-pi/pi-coding-agent",
|
|
4
|
-
"version": "17.0.
|
|
4
|
+
"version": "17.0.8",
|
|
5
5
|
"description": "Coding agent CLI with read, bash, edit, write tools and session management",
|
|
6
6
|
"homepage": "https://omp.sh",
|
|
7
7
|
"author": "Can Boluk",
|
|
@@ -52,17 +52,17 @@
|
|
|
52
52
|
"@agentclientprotocol/sdk": "1.2.1",
|
|
53
53
|
"@babel/parser": "^7.29.7",
|
|
54
54
|
"@mozilla/readability": "^0.6.0",
|
|
55
|
-
"@oh-my-pi/hashline": "17.0.
|
|
56
|
-
"@oh-my-pi/omp-stats": "17.0.
|
|
57
|
-
"@oh-my-pi/pi-agent-core": "17.0.
|
|
58
|
-
"@oh-my-pi/pi-ai": "17.0.
|
|
59
|
-
"@oh-my-pi/pi-catalog": "17.0.
|
|
60
|
-
"@oh-my-pi/pi-mnemopi": "17.0.
|
|
61
|
-
"@oh-my-pi/pi-natives": "17.0.
|
|
62
|
-
"@oh-my-pi/pi-tui": "17.0.
|
|
63
|
-
"@oh-my-pi/pi-utils": "17.0.
|
|
64
|
-
"@oh-my-pi/pi-wire": "17.0.
|
|
65
|
-
"@oh-my-pi/snapcompact": "17.0.
|
|
55
|
+
"@oh-my-pi/hashline": "17.0.8",
|
|
56
|
+
"@oh-my-pi/omp-stats": "17.0.8",
|
|
57
|
+
"@oh-my-pi/pi-agent-core": "17.0.8",
|
|
58
|
+
"@oh-my-pi/pi-ai": "17.0.8",
|
|
59
|
+
"@oh-my-pi/pi-catalog": "17.0.8",
|
|
60
|
+
"@oh-my-pi/pi-mnemopi": "17.0.8",
|
|
61
|
+
"@oh-my-pi/pi-natives": "17.0.8",
|
|
62
|
+
"@oh-my-pi/pi-tui": "17.0.8",
|
|
63
|
+
"@oh-my-pi/pi-utils": "17.0.8",
|
|
64
|
+
"@oh-my-pi/pi-wire": "17.0.8",
|
|
65
|
+
"@oh-my-pi/snapcompact": "17.0.8",
|
|
66
66
|
"@opentelemetry/api": "^1.9.1",
|
|
67
67
|
"@opentelemetry/api-logs": "^0.220.0",
|
|
68
68
|
"@opentelemetry/context-async-hooks": "^2.9.0",
|
|
@@ -79,7 +79,6 @@
|
|
|
79
79
|
"@xterm/headless": "^6.0.0",
|
|
80
80
|
"arktype": "2.2.3",
|
|
81
81
|
"chalk": "^5.6.2",
|
|
82
|
-
"diff": "^9.0.0",
|
|
83
82
|
"fast-xml-parser": "^5.9.3",
|
|
84
83
|
"handlebars": "^4.7.9",
|
|
85
84
|
"header-generator": "^2.1.82",
|
package/src/cli/models-cli.ts
CHANGED
|
@@ -17,8 +17,14 @@ import { formatNumber, getProjectDir } from "@oh-my-pi/pi-utils";
|
|
|
17
17
|
import chalk from "chalk";
|
|
18
18
|
import { ModelRegistry } from "../config/model-registry";
|
|
19
19
|
import { Settings } from "../config/settings";
|
|
20
|
-
import {
|
|
20
|
+
import {
|
|
21
|
+
discoverAndLoadExtensions,
|
|
22
|
+
ExtensionRunner,
|
|
23
|
+
emitSessionShutdownEvent,
|
|
24
|
+
loadExtensions,
|
|
25
|
+
} from "../extensibility/extensions";
|
|
21
26
|
import { discoverAuthStorage } from "../sdk";
|
|
27
|
+
import { SessionManager } from "../session/session-manager";
|
|
22
28
|
import { EventBus } from "../utils/event-bus";
|
|
23
29
|
|
|
24
30
|
export type ModelsAction = "ls" | "find" | "refresh";
|
|
@@ -303,25 +309,39 @@ export async function runModelsListing(options: RunModelsListingOptions): Promis
|
|
|
303
309
|
eventBus,
|
|
304
310
|
disabledExtensionIds,
|
|
305
311
|
);
|
|
312
|
+
const extensionRunner =
|
|
313
|
+
extensionsResult.extensions.length > 0
|
|
314
|
+
? new ExtensionRunner(
|
|
315
|
+
extensionsResult.extensions,
|
|
316
|
+
extensionsResult.runtime,
|
|
317
|
+
cwd,
|
|
318
|
+
SessionManager.inMemory(cwd),
|
|
319
|
+
modelRegistry,
|
|
320
|
+
)
|
|
321
|
+
: undefined;
|
|
306
322
|
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
323
|
+
try {
|
|
324
|
+
for (const { path: extPath, error } of extensionsResult.errors) {
|
|
325
|
+
process.stderr.write(`Failed to load extension: ${extPath}: ${error}\n`);
|
|
326
|
+
}
|
|
310
327
|
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
328
|
+
// Mirror sdk.ts: drain pending provider registrations into the registry.
|
|
329
|
+
const activeSources = extensionsResult.extensions.map(extension => extension.path);
|
|
330
|
+
modelRegistry.syncExtensionSources(activeSources);
|
|
331
|
+
for (const sourceId of new Set(activeSources)) {
|
|
332
|
+
modelRegistry.clearSourceRegistrations(sourceId);
|
|
333
|
+
}
|
|
334
|
+
for (const { name, config, sourceId } of extensionsResult.runtime.pendingProviderRegistrations) {
|
|
335
|
+
modelRegistry.registerProvider(name, config, sourceId);
|
|
336
|
+
}
|
|
337
|
+
extensionsResult.runtime.pendingProviderRegistrations = [];
|
|
338
|
+
// Discover runtime (extension) provider catalogs now that they are registered.
|
|
339
|
+
await modelRegistry.refreshRuntimeProviders(action === "refresh" ? "online" : "online-if-uncached");
|
|
323
340
|
|
|
324
|
-
|
|
341
|
+
renderProviderModels(modelRegistry, action, pattern, json);
|
|
342
|
+
} finally {
|
|
343
|
+
await emitSessionShutdownEvent(extensionRunner);
|
|
344
|
+
}
|
|
325
345
|
}
|
|
326
346
|
|
|
327
347
|
/**
|
package/src/cli/update-cli.ts
CHANGED
|
@@ -556,6 +556,19 @@ async function pruneBunCacheAfterGlobalInstall(): Promise<BunInstallCachePruneRe
|
|
|
556
556
|
return await pruneBunInstallCache(cacheDir, packageNames.size === 0 ? undefined : packageNames);
|
|
557
557
|
}
|
|
558
558
|
|
|
559
|
+
/**
|
|
560
|
+
* Detect a musl-libc Linux host (Alpine, Void-musl) so self-update replaces a
|
|
561
|
+
* musl binary with the musl release asset instead of the glibc build, which
|
|
562
|
+
* would fail to start on the next run. Mirrors the detection in
|
|
563
|
+
* scripts/install.sh.
|
|
564
|
+
*/
|
|
565
|
+
function isMuslLinux(): boolean {
|
|
566
|
+
if (process.platform !== "linux") return false;
|
|
567
|
+
if (fs.existsSync("/etc/alpine-release")) return true;
|
|
568
|
+
const loaderArch = process.arch === "arm64" ? "aarch64" : "x86_64";
|
|
569
|
+
return fs.existsSync(`/lib/ld-musl-${loaderArch}.so.1`);
|
|
570
|
+
}
|
|
571
|
+
|
|
559
572
|
/**
|
|
560
573
|
* Get the appropriate binary name for this platform.
|
|
561
574
|
*/
|
|
@@ -566,7 +579,7 @@ function getBinaryName(): string {
|
|
|
566
579
|
let os: string;
|
|
567
580
|
switch (platform) {
|
|
568
581
|
case "linux":
|
|
569
|
-
os = "linux";
|
|
582
|
+
os = isMuslLinux() ? "linux-musl" : "linux";
|
|
570
583
|
break;
|
|
571
584
|
case "darwin":
|
|
572
585
|
os = "darwin";
|
|
@@ -23,6 +23,7 @@ import { getBundledModels, getBundledProviders } from "@oh-my-pi/pi-catalog/mode
|
|
|
23
23
|
import {
|
|
24
24
|
googleAntigravityModelManagerOptions,
|
|
25
25
|
googleGeminiCliModelManagerOptions,
|
|
26
|
+
type OpenAICodexAccount,
|
|
26
27
|
openaiCodexModelManagerOptions,
|
|
27
28
|
PROVIDER_DESCRIPTORS,
|
|
28
29
|
} from "@oh-my-pi/pi-catalog/provider-models";
|
|
@@ -157,7 +158,7 @@ interface ProviderOverride {
|
|
|
157
158
|
export function mergeDiscoveredModel<TApi extends Api>(
|
|
158
159
|
model: Model<TApi>,
|
|
159
160
|
existing: Model<Api> | undefined,
|
|
160
|
-
providerOverride?: Pick<ProviderOverride, "baseUrl" | "headers" | "remoteCompaction" | "transport">,
|
|
161
|
+
providerOverride?: Pick<ProviderOverride, "baseUrl" | "compat" | "headers" | "remoteCompaction" | "transport">,
|
|
161
162
|
): Model<TApi> {
|
|
162
163
|
if (existing) {
|
|
163
164
|
const supportsTools = model.supportsTools ?? existing.supportsTools;
|
|
@@ -171,7 +172,7 @@ export function mergeDiscoveredModel<TApi extends Api>(
|
|
|
171
172
|
providerOverride?.remoteCompaction,
|
|
172
173
|
),
|
|
173
174
|
...(supportsTools !== undefined ? { supportsTools } : {}),
|
|
174
|
-
compat: model.compatConfig,
|
|
175
|
+
compat: mergeCompat(model.compatConfig, providerOverride?.compat),
|
|
175
176
|
} as ModelSpec<TApi>);
|
|
176
177
|
}
|
|
177
178
|
if (providerOverride) {
|
|
@@ -184,7 +185,7 @@ export function mergeDiscoveredModel<TApi extends Api>(
|
|
|
184
185
|
model.remoteCompaction,
|
|
185
186
|
providerOverride.remoteCompaction,
|
|
186
187
|
),
|
|
187
|
-
compat: model.compatConfig,
|
|
188
|
+
compat: mergeCompat(model.compatConfig, providerOverride.compat),
|
|
188
189
|
} as ModelSpec<TApi>);
|
|
189
190
|
}
|
|
190
191
|
return model;
|
|
@@ -427,20 +428,35 @@ function getOAuthCredentialsForProvider(authStorage: AuthStorage, provider: stri
|
|
|
427
428
|
return entries.filter((entry): entry is OAuthCredential => entry.type === "oauth");
|
|
428
429
|
}
|
|
429
430
|
|
|
430
|
-
|
|
431
|
+
/**
|
|
432
|
+
* Resolve every configured Codex OAuth account for catalog discovery, refreshing
|
|
433
|
+
* each credential exactly once. Codex `/models` is account-scoped, so discovery
|
|
434
|
+
* must fetch per account and union the results; resolving a single access token
|
|
435
|
+
* (as before) hid models available only through a sibling account (#6265).
|
|
436
|
+
*
|
|
437
|
+
* Returns `null` when any stored account fails to resolve (e.g. a transient
|
|
438
|
+
* refresh failure): the Codex manager is authoritative, so unioning only the
|
|
439
|
+
* accounts that resolved would cache a partial catalog and hide the failed
|
|
440
|
+
* account's models for the cache TTL. Aborting keeps the previous/bundled
|
|
441
|
+
* catalog instead.
|
|
442
|
+
*/
|
|
443
|
+
async function resolveCodexDiscoveryAccounts(
|
|
431
444
|
authStorage: AuthStorage,
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
const
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
}
|
|
440
|
-
if (
|
|
441
|
-
|
|
445
|
+
resolvedAccessToken: string,
|
|
446
|
+
): Promise<OpenAICodexAccount[] | null> {
|
|
447
|
+
const accesses = await authStorage.getOAuthAccesses("openai-codex");
|
|
448
|
+
const accounts: OpenAICodexAccount[] = [];
|
|
449
|
+
for (const access of accesses) {
|
|
450
|
+
if (!access.ok) return null;
|
|
451
|
+
accounts.push({ accessToken: access.accessToken, accountId: access.accountId });
|
|
452
|
+
}
|
|
453
|
+
if (!accounts.some(account => account.accessToken === resolvedAccessToken)) {
|
|
454
|
+
const matchingCredential = getOAuthCredentialsForProvider(authStorage, "openai-codex").find(
|
|
455
|
+
credential => credential.access === resolvedAccessToken,
|
|
456
|
+
);
|
|
457
|
+
accounts.push({ accessToken: resolvedAccessToken, accountId: matchingCredential?.accountId });
|
|
442
458
|
}
|
|
443
|
-
return
|
|
459
|
+
return accounts;
|
|
444
460
|
}
|
|
445
461
|
|
|
446
462
|
function mergeCompat<TBase extends object, TOverride extends object>(
|
|
@@ -1143,8 +1159,18 @@ export class ModelRegistry {
|
|
|
1143
1159
|
models.push(spec);
|
|
1144
1160
|
continue;
|
|
1145
1161
|
}
|
|
1146
|
-
|
|
1147
|
-
|
|
1162
|
+
// Current unrestorable markers prove that neither same-id nor
|
|
1163
|
+
// request-model bundled headers matched the live model. Only markers
|
|
1164
|
+
// from the old id-only writer may recover through `requestModelId`.
|
|
1165
|
+
const unrestorable = unrestorableHeaderIds.has(spec.id);
|
|
1166
|
+
const bundledHeaders = (
|
|
1167
|
+
unrestorable
|
|
1168
|
+
? cache.legacyHeaderRestoreMarkers && spec.requestModelId
|
|
1169
|
+
? bundledById?.get(spec.requestModelId)
|
|
1170
|
+
: undefined
|
|
1171
|
+
: (bundledById?.get(spec.id) ??
|
|
1172
|
+
(spec.requestModelId ? bundledById?.get(spec.requestModelId) : undefined))
|
|
1173
|
+
)?.headers;
|
|
1148
1174
|
if (!bundledHeaders) continue;
|
|
1149
1175
|
models.push({ ...spec, headers: bundledHeaders });
|
|
1150
1176
|
}
|
|
@@ -1723,14 +1749,11 @@ export class ModelRegistry {
|
|
|
1723
1749
|
providerId: "openai-codex",
|
|
1724
1750
|
authoritative: true,
|
|
1725
1751
|
resolveKey: value => value,
|
|
1726
|
-
createOptions: accessToken =>
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
accessToken,
|
|
1730
|
-
accountId,
|
|
1752
|
+
createOptions: accessToken =>
|
|
1753
|
+
openaiCodexModelManagerOptions({
|
|
1754
|
+
resolveAccounts: () => resolveCodexDiscoveryAccounts(this.authStorage, accessToken),
|
|
1731
1755
|
fetch: this.#fetch,
|
|
1732
|
-
})
|
|
1733
|
-
},
|
|
1756
|
+
}),
|
|
1734
1757
|
},
|
|
1735
1758
|
];
|
|
1736
1759
|
const disabledProviders = getDisabledProviderIdsFromSettings();
|
|
@@ -1598,11 +1598,33 @@ export function filterAvailableModelsByEnabledPatterns(
|
|
|
1598
1598
|
|
|
1599
1599
|
return includeSyntheticAllowedModels(available, allowedModels);
|
|
1600
1600
|
}
|
|
1601
|
+
function findExactCliModel(
|
|
1602
|
+
selector: string,
|
|
1603
|
+
allModels: Model<Api>[],
|
|
1604
|
+
availableModels: Model<Api>[],
|
|
1605
|
+
): Model<Api> | undefined {
|
|
1606
|
+
// Explicit provider/id references stay authoritative against the full catalog.
|
|
1607
|
+
const referenced = findExactModelReferenceMatch(selector, allModels);
|
|
1608
|
+
if (referenced) return referenced;
|
|
1609
|
+
|
|
1610
|
+
// Flat-id (or full-selector-string) matches prefer authenticated providers,
|
|
1611
|
+
// then fall back to catalog order. This covers aggregator-style flat ids
|
|
1612
|
+
// that merely look provider-qualified (e.g. "openai/gpt-oss-120b" hosted on
|
|
1613
|
+
// OpenRouter), where the provider/id decomposition above found nothing.
|
|
1614
|
+
const lower = selector.toLowerCase();
|
|
1615
|
+
const isFlatMatch = (model: Model<Api>) =>
|
|
1616
|
+
model.id.toLowerCase() === lower || formatModelString(model).toLowerCase() === lower;
|
|
1617
|
+
const preferred = availableModels.find(isFlatMatch);
|
|
1618
|
+
if (preferred) return preferred;
|
|
1619
|
+
return availableModels === allModels ? undefined : allModels.find(isFlatMatch);
|
|
1620
|
+
}
|
|
1601
1621
|
|
|
1602
1622
|
export interface ResolveCliModelResult {
|
|
1603
1623
|
model: Model<Api> | undefined;
|
|
1604
|
-
/** configuredPatterns
|
|
1624
|
+
/** configuredPatterns contains the role's ordered primary candidates. */
|
|
1605
1625
|
configuredPatterns?: string[];
|
|
1626
|
+
/** configuredRole identifies the role expanded into configuredPatterns. */
|
|
1627
|
+
configuredRole?: string;
|
|
1606
1628
|
/** configuredPatternIndex identifies the configured role pattern that matched an available model. */
|
|
1607
1629
|
configuredPatternIndex?: number;
|
|
1608
1630
|
selector?: string;
|
|
@@ -1620,17 +1642,19 @@ export function resolveCliModel(options: {
|
|
|
1620
1642
|
cliProvider?: string;
|
|
1621
1643
|
cliModel?: string;
|
|
1622
1644
|
modelRegistry: CliModelRegistry;
|
|
1645
|
+
/** Authenticated models to prefer for unqualified selectors; omit to preserve catalog-order behavior. */
|
|
1646
|
+
availableModels?: Model<Api>[];
|
|
1623
1647
|
settings?: Settings;
|
|
1624
1648
|
preferences?: ModelMatchPreferences;
|
|
1625
1649
|
}): ResolveCliModelResult {
|
|
1626
|
-
const { cliProvider, cliModel, modelRegistry, settings, preferences } = options;
|
|
1650
|
+
const { cliProvider, cliModel, modelRegistry, settings, preferences, availableModels: preferredModels } = options;
|
|
1627
1651
|
|
|
1628
1652
|
if (!cliModel) {
|
|
1629
1653
|
return { model: undefined, selector: undefined, warning: undefined, error: undefined };
|
|
1630
1654
|
}
|
|
1631
1655
|
|
|
1632
|
-
const
|
|
1633
|
-
if (
|
|
1656
|
+
const allModels = modelRegistry.getAll();
|
|
1657
|
+
if (allModels.length === 0) {
|
|
1634
1658
|
return {
|
|
1635
1659
|
model: undefined,
|
|
1636
1660
|
selector: undefined,
|
|
@@ -1639,8 +1663,9 @@ export function resolveCliModel(options: {
|
|
|
1639
1663
|
};
|
|
1640
1664
|
}
|
|
1641
1665
|
|
|
1666
|
+
const availableModels = preferredModels ?? allModels;
|
|
1642
1667
|
const providerMap = new Map<string, string>();
|
|
1643
|
-
for (const model of
|
|
1668
|
+
for (const model of allModels) {
|
|
1644
1669
|
providerMap.set(model.provider.toLowerCase(), model.provider);
|
|
1645
1670
|
}
|
|
1646
1671
|
|
|
@@ -1656,19 +1681,7 @@ export function resolveCliModel(options: {
|
|
|
1656
1681
|
|
|
1657
1682
|
const trimmedModel = cliModel.trim();
|
|
1658
1683
|
if (!provider) {
|
|
1659
|
-
const
|
|
1660
|
-
// When input has provider/id format (e.g. "zai/glm-5"), prefer decomposed
|
|
1661
|
-
// provider+id match over flat id match. Without this, a model with id
|
|
1662
|
-
// "zai/glm-5" on provider "vercel-ai-gateway" wins over provider "zai"
|
|
1663
|
-
// with id "glm-5", because Array.find returns the first catalog hit.
|
|
1664
|
-
let exact = findExactModelReferenceMatch(trimmedModel, availableModels);
|
|
1665
|
-
if (!exact) {
|
|
1666
|
-
// Flat exact id (or full selector) by catalog order: CLI resolution
|
|
1667
|
-
// stays deterministic across runs regardless of usage-based ranking.
|
|
1668
|
-
exact = availableModels.find(
|
|
1669
|
-
model => model.id.toLowerCase() === lower || `${model.provider}/${model.id}`.toLowerCase() === lower,
|
|
1670
|
-
);
|
|
1671
|
-
}
|
|
1684
|
+
const exact = findExactCliModel(trimmedModel, allModels, availableModels);
|
|
1672
1685
|
if (exact) {
|
|
1673
1686
|
return {
|
|
1674
1687
|
model: exact,
|
|
@@ -1684,15 +1697,7 @@ export function resolveCliModel(options: {
|
|
|
1684
1697
|
MAX_THINKING_SUFFIX_OPTIONS,
|
|
1685
1698
|
);
|
|
1686
1699
|
if (exactThinkingLevel) {
|
|
1687
|
-
|
|
1688
|
-
if (!exactSuffixed) {
|
|
1689
|
-
const lowerExactBase = exactBase.toLowerCase();
|
|
1690
|
-
exactSuffixed = availableModels.find(
|
|
1691
|
-
model =>
|
|
1692
|
-
model.id.toLowerCase() === lowerExactBase ||
|
|
1693
|
-
`${model.provider}/${model.id}`.toLowerCase() === lowerExactBase,
|
|
1694
|
-
);
|
|
1695
|
-
}
|
|
1700
|
+
const exactSuffixed = findExactCliModel(exactBase, allModels, availableModels);
|
|
1696
1701
|
if (exactSuffixed) {
|
|
1697
1702
|
return {
|
|
1698
1703
|
model: exactSuffixed,
|
|
@@ -1718,15 +1723,28 @@ export function resolveCliModel(options: {
|
|
|
1718
1723
|
? `${formatModelRoleAlias(bareRoleName)}${bareRoleThinkingLevel ? `:${bareRoleThinkingLevel}` : ""}`
|
|
1719
1724
|
: undefined;
|
|
1720
1725
|
if (roleSelector) {
|
|
1726
|
+
const { base: roleAlias } = splitThinkingSuffix(
|
|
1727
|
+
roleSelector,
|
|
1728
|
+
modelRoleAliasPrefixLength(roleSelector) ?? -1,
|
|
1729
|
+
MAX_THINKING_SUFFIX_OPTIONS,
|
|
1730
|
+
);
|
|
1731
|
+
const configuredRole = getModelRoleAlias(roleAlias, settings);
|
|
1721
1732
|
configuredPatterns = resolveConfiguredModelPatterns([roleSelector], settings);
|
|
1722
|
-
const
|
|
1733
|
+
const availableResolved = resolveModelRoleValue(roleSelector, availableModels, {
|
|
1723
1734
|
settings,
|
|
1724
1735
|
matchPreferences: preferences,
|
|
1725
1736
|
});
|
|
1737
|
+
const resolved = availableResolved.model
|
|
1738
|
+
? availableResolved
|
|
1739
|
+
: resolveModelRoleValue(roleSelector, allModels, {
|
|
1740
|
+
settings,
|
|
1741
|
+
matchPreferences: preferences,
|
|
1742
|
+
});
|
|
1726
1743
|
if (resolved.model) {
|
|
1727
1744
|
return {
|
|
1728
1745
|
model: resolved.model,
|
|
1729
1746
|
selector: formatModelString(resolved.model),
|
|
1747
|
+
configuredRole,
|
|
1730
1748
|
configuredPatterns,
|
|
1731
1749
|
configuredPatternIndex: resolved.matchedPatternIndex,
|
|
1732
1750
|
thinkingLevel: resolved.thinkingLevel,
|
|
@@ -1738,6 +1756,7 @@ export function resolveCliModel(options: {
|
|
|
1738
1756
|
return {
|
|
1739
1757
|
model: undefined,
|
|
1740
1758
|
configuredPatterns,
|
|
1759
|
+
configuredRole,
|
|
1741
1760
|
selector: undefined,
|
|
1742
1761
|
thinkingLevel: undefined,
|
|
1743
1762
|
warning: resolved.warning,
|
|
@@ -1767,7 +1786,7 @@ export function resolveCliModel(options: {
|
|
|
1767
1786
|
}
|
|
1768
1787
|
|
|
1769
1788
|
if (provider) {
|
|
1770
|
-
const exactProviderMatch = resolveProviderModelReference(provider, pattern,
|
|
1789
|
+
const exactProviderMatch = resolveProviderModelReference(provider, pattern, allModels);
|
|
1771
1790
|
if (exactProviderMatch) {
|
|
1772
1791
|
return {
|
|
1773
1792
|
model: exactProviderMatch,
|
|
@@ -1779,10 +1798,16 @@ export function resolveCliModel(options: {
|
|
|
1779
1798
|
}
|
|
1780
1799
|
}
|
|
1781
1800
|
|
|
1782
|
-
const candidates = provider ?
|
|
1783
|
-
|
|
1801
|
+
const candidates = provider ? allModels.filter(model => model.provider === provider) : availableModels;
|
|
1802
|
+
let parsed = parseModelPattern(pattern, candidates, preferences, {
|
|
1784
1803
|
allowInvalidThinkingSelectorFallback: false,
|
|
1785
1804
|
});
|
|
1805
|
+
if (!parsed.model && !provider) {
|
|
1806
|
+
parsed = parseModelPattern(pattern, allModels, preferences, {
|
|
1807
|
+
allowInvalidThinkingSelectorFallback: false,
|
|
1808
|
+
});
|
|
1809
|
+
}
|
|
1810
|
+
const { model, thinkingLevel, warning, upstream } = parsed;
|
|
1786
1811
|
|
|
1787
1812
|
if (!model) {
|
|
1788
1813
|
const display = provider ? `${provider}/${pattern}` : cliModel;
|
|
@@ -2861,6 +2861,11 @@ export const SETTINGS_SCHEMA = {
|
|
|
2861
2861
|
|
|
2862
2862
|
"hindsight.debug": { type: "boolean", default: false },
|
|
2863
2863
|
|
|
2864
|
+
"hindsight.requestTimeoutMs": { type: "number", default: 30_000 },
|
|
2865
|
+
"hindsight.reflectTimeoutMs": { type: "number", default: 120_000 },
|
|
2866
|
+
"hindsight.recallTimeoutMs": { type: "number", default: 30_000 },
|
|
2867
|
+
"hindsight.retainTimeoutMs": { type: "number", default: 60_000 },
|
|
2868
|
+
|
|
2864
2869
|
"hindsight.mentalModelsEnabled": {
|
|
2865
2870
|
type: "boolean",
|
|
2866
2871
|
default: true,
|
package/src/config/settings.ts
CHANGED
|
@@ -330,6 +330,8 @@ export class Settings {
|
|
|
330
330
|
#modified = new Set<string>();
|
|
331
331
|
/** Individual project model roles modified during this session */
|
|
332
332
|
#modifiedProjectModelRoles = new Set<string>();
|
|
333
|
+
/** Individual global model roles modified during this session (for partial save) */
|
|
334
|
+
#modifiedGlobalModelRoles = new Set<string>();
|
|
333
335
|
/**
|
|
334
336
|
* Original process-wide model-role overrides captured before a project edit
|
|
335
337
|
* temporarily replaced them via `#updateRuntimeModelRoleOverride`. Restored
|
|
@@ -551,7 +553,7 @@ export class Settings {
|
|
|
551
553
|
if (this.#projectSavePromise) {
|
|
552
554
|
await this.#projectSavePromise;
|
|
553
555
|
}
|
|
554
|
-
if (this.#modified.size > 0) {
|
|
556
|
+
if (this.#modified.size > 0 || this.#modifiedGlobalModelRoles.size > 0) {
|
|
555
557
|
await this.#saveNow();
|
|
556
558
|
}
|
|
557
559
|
if (this.#modifiedProjectModelRoles.size > 0) {
|
|
@@ -833,13 +835,22 @@ export class Settings {
|
|
|
833
835
|
* stale skip in place.
|
|
834
836
|
*/
|
|
835
837
|
setModelRole(role: ModelRole | string, modelId: string | undefined): void {
|
|
838
|
+
const prev = this.get("modelRoles");
|
|
836
839
|
const current = this.#modelRolesFromLayer(this.#global);
|
|
837
840
|
if (modelId === undefined) {
|
|
838
841
|
delete current[role];
|
|
839
842
|
} else {
|
|
840
843
|
current[role] = modelId;
|
|
841
844
|
}
|
|
842
|
-
|
|
845
|
+
// Persist per-role rather than marking the whole `modelRoles` path
|
|
846
|
+
// modified: #saveNow merges only the changed role into the re-read
|
|
847
|
+
// file, so a concurrent external edit to a sibling role is not
|
|
848
|
+
// clobbered by this process's stale in-memory snapshot.
|
|
849
|
+
setByPath(this.#global, ["modelRoles"], current);
|
|
850
|
+
this.#modifiedGlobalModelRoles.add(role);
|
|
851
|
+
this.#rebuildMerged();
|
|
852
|
+
this.#queueSave();
|
|
853
|
+
this.#fireEffectiveSettingChanged("modelRoles", this.get("modelRoles"), prev);
|
|
843
854
|
if (this.isProjectModelRoleRuntimeOverrideActive(role)) {
|
|
844
855
|
return;
|
|
845
856
|
}
|
|
@@ -1619,7 +1630,8 @@ export class Settings {
|
|
|
1619
1630
|
clearTimeout(this.#saveTimer);
|
|
1620
1631
|
this.#saveTimer = setTimeout(() => {
|
|
1621
1632
|
this.#saveTimer = undefined;
|
|
1622
|
-
const
|
|
1633
|
+
const previousSave = this.#savePromise;
|
|
1634
|
+
const savePromise = previousSave ? previousSave.then(() => this.#saveNow()) : this.#saveNow();
|
|
1623
1635
|
this.#savePromise = savePromise;
|
|
1624
1636
|
savePromise
|
|
1625
1637
|
.catch(err => {
|
|
@@ -1634,27 +1646,75 @@ export class Settings {
|
|
|
1634
1646
|
}
|
|
1635
1647
|
|
|
1636
1648
|
async #saveNow(): Promise<void> {
|
|
1637
|
-
if (!this.#persist || !this.#configPath
|
|
1649
|
+
if (!this.#persist || !this.#configPath) return;
|
|
1650
|
+
if (this.#modified.size === 0 && this.#modifiedGlobalModelRoles.size === 0) return;
|
|
1638
1651
|
|
|
1639
1652
|
const configPath = this.#configPath;
|
|
1640
1653
|
const modifiedPaths = [...this.#modified];
|
|
1654
|
+
const modifiedModelRoles = [...this.#modifiedGlobalModelRoles];
|
|
1655
|
+
const globalRolesAtStart = this.#modelRolesFromLayer(this.#global);
|
|
1641
1656
|
this.#modified.clear();
|
|
1657
|
+
this.#modifiedGlobalModelRoles.clear();
|
|
1642
1658
|
|
|
1643
1659
|
try {
|
|
1644
1660
|
await withFileLock(configPath, async () => {
|
|
1645
1661
|
// Re-read to preserve external changes
|
|
1646
1662
|
const current = await this.#loadYaml(configPath);
|
|
1647
1663
|
|
|
1648
|
-
// Apply only our modified paths
|
|
1664
|
+
// Apply only our modified whole-value paths
|
|
1649
1665
|
for (const modPath of modifiedPaths) {
|
|
1650
1666
|
const segments = modPath.split(".");
|
|
1651
1667
|
const value = getByPath(this.#global, segments);
|
|
1652
1668
|
setByPath(current, segments, value);
|
|
1653
1669
|
}
|
|
1654
1670
|
|
|
1671
|
+
// Merge only the model roles captured by this save. Then retain
|
|
1672
|
+
// any role changed while the async read/lock was pending before
|
|
1673
|
+
// replacing #global, so the follow-up save still sees its value.
|
|
1674
|
+
const latestGlobalRoles = this.#modelRolesFromLayer(this.#global);
|
|
1675
|
+
const rolesToPreserve = new Set(this.#modifiedGlobalModelRoles);
|
|
1676
|
+
for (const role in globalRolesAtStart) {
|
|
1677
|
+
if (globalRolesAtStart[role] !== latestGlobalRoles[role]) {
|
|
1678
|
+
rolesToPreserve.add(role);
|
|
1679
|
+
}
|
|
1680
|
+
}
|
|
1681
|
+
for (const role in latestGlobalRoles) {
|
|
1682
|
+
if (globalRolesAtStart[role] !== latestGlobalRoles[role]) {
|
|
1683
|
+
rolesToPreserve.add(role);
|
|
1684
|
+
}
|
|
1685
|
+
}
|
|
1686
|
+
if (modifiedModelRoles.length > 0 || rolesToPreserve.size > 0) {
|
|
1687
|
+
const currentRoles = getByPath(current, ["modelRoles"]);
|
|
1688
|
+
const mergedRoles: Record<string, unknown> = isRecord(currentRoles) ? { ...currentRoles } : {};
|
|
1689
|
+
for (const role of modifiedModelRoles) {
|
|
1690
|
+
if (Object.hasOwn(globalRolesAtStart, role)) {
|
|
1691
|
+
mergedRoles[role] = globalRolesAtStart[role];
|
|
1692
|
+
} else {
|
|
1693
|
+
delete mergedRoles[role];
|
|
1694
|
+
}
|
|
1695
|
+
}
|
|
1696
|
+
for (const role of rolesToPreserve) {
|
|
1697
|
+
if (Object.hasOwn(latestGlobalRoles, role)) {
|
|
1698
|
+
mergedRoles[role] = latestGlobalRoles[role];
|
|
1699
|
+
} else {
|
|
1700
|
+
delete mergedRoles[role];
|
|
1701
|
+
}
|
|
1702
|
+
}
|
|
1703
|
+
setByPath(current, ["modelRoles"], mergedRoles);
|
|
1704
|
+
}
|
|
1705
|
+
|
|
1655
1706
|
// Update our global with any external changes we preserved
|
|
1656
1707
|
this.#global = current;
|
|
1657
1708
|
await Bun.write(configPath, YAML.stringify(this.#global, null, 2));
|
|
1709
|
+
// These pending roles were included in this write. Remove each
|
|
1710
|
+
// only if no newer local change arrived while Bun.write was in
|
|
1711
|
+
// flight; a newer value still needs the queued follow-up save.
|
|
1712
|
+
const globalRolesAfterWrite = this.#modelRolesFromLayer(this.#global);
|
|
1713
|
+
for (const role of rolesToPreserve) {
|
|
1714
|
+
if (latestGlobalRoles[role] === globalRolesAfterWrite[role]) {
|
|
1715
|
+
this.#modifiedGlobalModelRoles.delete(role);
|
|
1716
|
+
}
|
|
1717
|
+
}
|
|
1658
1718
|
});
|
|
1659
1719
|
} catch (error) {
|
|
1660
1720
|
logger.warn("Settings: save failed", { error: String(error) });
|
|
@@ -1662,6 +1722,9 @@ export class Settings {
|
|
|
1662
1722
|
for (const p of modifiedPaths) {
|
|
1663
1723
|
this.#modified.add(p);
|
|
1664
1724
|
}
|
|
1725
|
+
for (const role of modifiedModelRoles) {
|
|
1726
|
+
this.#modifiedGlobalModelRoles.add(role);
|
|
1727
|
+
}
|
|
1665
1728
|
}
|
|
1666
1729
|
|
|
1667
1730
|
this.#rebuildMerged();
|