Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions apps/desktop/src/main/runtime-host-boot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1298,6 +1298,11 @@ function registerHostClientIpc(
emitTargetConnectionListChanged();
sendToRenderer("settings:externalChanged", { ts: Date.now() });
});
// No `settings:externalChanged` here: the user's settings did not move, the
// Host just resolved the same connections against a newer model catalog.
const unsubscribeConnectionCatalogChanges = client.subscribeConnectionCatalogChanges(() => {
emitTargetConnectionListChanged();
});
const unsubscribeSessionCatalogChanges = client.subscribeSessionCatalogChanges(
({ sessionId }) => emitTargetSessionsChanged("updated", sessionId),
);
Expand Down Expand Up @@ -1556,6 +1561,7 @@ function registerHostClientIpc(
registerTaskSubmissionReadinessIpc(taskSubmissionReadinessService, scopedIpc);
return async () => {
unsubscribeConfigurationChanges();
unsubscribeConnectionCatalogChanges();
unsubscribeSessionCatalogChanges();
unsubscribeProjectCatalogChanges();
unsubscribeScheduledTaskChanges();
Expand Down
5 changes: 5 additions & 0 deletions apps/desktop/src/main/runtime-host-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -356,6 +356,11 @@ export class DesktopRuntimeHostClient {
return this.connection.subscribeConfigurationChanges(listener);
}

subscribeConnectionCatalogChanges(listener: (revision: number) => void): () => void {
this.#assertOpen();
return this.connection.subscribeConnectionCatalogChanges(listener);
}

subscribeProjectCatalogChanges(listener: (revision: number) => void): () => void {
this.#assertOpen();
return this.connection.subscribeProjectCatalogChanges(listener);
Expand Down
2 changes: 1 addition & 1 deletion docs/code-origin-audit.md
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,7 @@ Upstream is MIT, Copyright (c) 2025 opencode. The repository now resolves to `an

### models.dev data snapshot

`packages/core/src/model-metadata.generated.ts` and `packages/runtime/src/telemetry/model-pricing.generated.ts` are build-time, untracked derivations of the committed `scripts/model-metadata/models-dev-api.snapshot.json` projection selected from `https://models.dev/api.json`. This is a two-level authority boundary: models.dev remains the upstream refresh source, while the committed snapshot is the sole build input for a particular repository revision and release. An explicit refresh imports upstream changes for review; normal installation and build paths never fetch a moving latest response. Upstream `anomalyco/models.dev` is MIT, Copyright (c) 2025 models.dev. The individual entries are facts and are not themselves copyrightable, but the selection and arrangement — which providers and fields are carried, and upstream's normalized structures such as `lifecycle` and `thinkingOptions.efforts` — come from that database. The same generator boundary applies: models.dev is not an npm dependency, so the root `LICENSE` records its source, repository, copyright, MIT permission notice, generated outputs, and snapshot provenance explicitly. The committed snapshot and generated headers bind the redistributed projection to recorded digests, making the fixed input identifiable without relying on the npm notice generator or a runtime network request.
`packages/core/src/model-metadata.generated.ts` and `packages/runtime/src/telemetry/model-pricing.generated.ts` are build-time, untracked derivations of the committed `scripts/model-metadata/models-dev-api.snapshot.json` projection selected from `https://models.dev/api.json`. This is a two-level authority boundary: models.dev remains the upstream refresh source, while the committed snapshot is the sole build input for a particular repository revision and release. An explicit refresh imports upstream changes for review; normal installation and build paths never fetch a moving latest response. At run time the Runtime Host fetches `https://models.dev/api.json` once at startup and holds the projection in memory for that process; it is never written to disk and never enters a build, so the redistributed artifact stays bound to the committed snapshot. That fetch goes through the same outbound admission as the WebFetch tool, so privacy mode suppresses it and a configured proxy carries it. Upstream `anomalyco/models.dev` is MIT, Copyright (c) 2025 models.dev. The individual entries are facts and are not themselves copyrightable, but the selection and arrangement — which providers and fields are carried, and upstream's normalized structures such as `lifecycle` and `thinkingOptions.efforts` — come from that database. The same generator boundary applies: models.dev is not an npm dependency, so the root `LICENSE` records its source, repository, copyright, MIT permission notice, generated outputs, and snapshot provenance explicitly. The committed snapshot and generated headers bind the redistributed projection to recorded digests, making the fixed input identifiable without relying on the npm notice generator or a runtime network request.

### PawWork browser port

Expand Down
4 changes: 4 additions & 0 deletions packages/cli/src/__tests__/runtime-host-cli-context.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ test('CLI Runtime Host bootstrap launches the execution composition', async () =
closed: new Promise<void>(() => {}),
status: async () => ({ state: 'ready' }),
subscribeConfigurationChanges: () => () => {},
subscribeConnectionCatalogChanges: () => () => {},
subscribeProjectCatalogChanges: () => () => {},
subscribeSessionCatalogChanges: () => () => {},
subscribeScheduledTaskChanges: () => () => {},
Expand Down Expand Up @@ -247,6 +248,7 @@ test('remote CLI profiles pin root identity and resolve credential outside the p
closed: new Promise<void>(() => {}),
status: async () => ({ state: 'ready' }),
subscribeConfigurationChanges: () => () => {},
subscribeConnectionCatalogChanges: () => () => {},
subscribeProjectCatalogChanges: () => () => {},
subscribeSessionCatalogChanges: () => () => {},
subscribeScheduledTaskChanges: () => () => {},
Expand Down Expand Up @@ -347,6 +349,7 @@ test('remote CLI profile state and Client identity use the explicit Client Data
closed: new Promise<void>(() => {}),
status: async () => ({ state: 'ready' }),
subscribeConfigurationChanges: () => () => {},
subscribeConnectionCatalogChanges: () => () => {},
subscribeProjectCatalogChanges: () => () => {},
subscribeSessionCatalogChanges: () => () => {},
subscribeScheduledTaskChanges: () => () => {},
Expand Down Expand Up @@ -425,6 +428,7 @@ test('remote CLI enables SSH prompts only for an explicitly interactive TTY', as
closed: new Promise<void>(() => {}),
status: async () => ({ state: 'ready' }),
subscribeConfigurationChanges: () => () => {},
subscribeConnectionCatalogChanges: () => () => {},
subscribeProjectCatalogChanges: () => () => {},
subscribeSessionCatalogChanges: () => () => {},
subscribeScheduledTaskChanges: () => () => {},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -860,6 +860,7 @@ function connectionHarness(
return { registrationId: 'registration-a', revision: harness.unregisters };
},
subscribeConfigurationChanges: () => () => undefined,
subscribeConnectionCatalogChanges: () => () => undefined,
subscribeProjectCatalogChanges: () => () => undefined,
subscribeSessionCatalogChanges: () => () => undefined,
subscribeScheduledTaskChanges: () => () => undefined,
Expand Down
90 changes: 50 additions & 40 deletions packages/cli/src/pi-tui-runner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,17 @@ export interface MakaPiTuiInput {
};
subscribeSessionTitleChanges?: (listener: (sessionId: string) => void) => () => void;
subscribeShellRunUpdates?: (listener: (update: ShellRunUpdate) => void) => () => void;
/**
* The Host re-resolved its model catalog and handed back the new projection.
* Adopt it wholesale — the picker shows what the Host says, never a local
* merge of it.
*/
subscribeModelCatalogChanges?: (
listener: (refresh: {
readonly modelChoices: readonly ModelChoice[];
readonly connectionIdentities: readonly ConnectionIdentity[];
}) => void,
) => () => void;
listShellRunUpdates?: (sessionId: string) => Promise<ShellRunUpdate[]>;
/** Host-owned invocable Skill catalog used for picker, completion, and token highlighting. */
listSkills?: (cwd: string) => Promise<readonly InvocableSkillEntry[]>;
Expand Down Expand Up @@ -439,17 +450,9 @@ export async function runMakaPiTui(input: MakaPiTuiInput): Promise<void> {
let model = input.model;
let connectionId = input.connectionId;
let connectionSlug = input.connectionSlug;
let modelContextWindow = input.modelContextWindow;
let permissionMode = input.permissionMode;
let orchestrationMode = input.driver.getOrchestrationMode?.() ?? 'default';
let thinkingLevel: ThinkingLevel | undefined = undefined;
// The Host resolved these when it projected the choice — including a relay's
// declared `relayModelProfiles[model].thinkingLevels`. A model no choice
// describes offers none rather than a locally guessed list.
let thinkingLevels: readonly ThinkingLevel[] =
input.modelChoices?.find(
(choice) => choice.connectionSlug === connectionSlug && choice.model === model,
)?.thinkingLevels ?? [];
let sessionListScope: 'current' | 'all' = input.sessionListScope ?? 'current';
let connectionIdentityNotice: string | undefined;
let busy = false;
Expand Down Expand Up @@ -608,11 +611,11 @@ export async function runMakaPiTui(input: MakaPiTuiInput): Promise<void> {
permissionMode,
orchestrationMode,
thinkingLevel,
thinkingLevels,
thinkingLevels: currentThinkingLevels(),
sessionId: input.driver.getSessionId(),
busy,
usage: state.usage,
modelContextWindow,
modelContextWindow: currentModelContextWindow(),
turnElapsedMs: turnStartedAt !== undefined ? Date.now() - turnStartedAt : undefined,
providerRetry: state.providerRetry,
uiLocale: locale,
Expand Down Expand Up @@ -919,8 +922,10 @@ export async function runMakaPiTui(input: MakaPiTuiInput): Promise<void> {
process.off('unhandledRejection', handleUnhandledRejection);
};

let unsubscribeModelCatalogChanges: (() => void) | undefined;
const restoreTerminal = () => {
removeProcessHandlers();
unsubscribeModelCatalogChanges?.();
unsubscribeSessionTitleChanges();
unsubscribeGoalChanges?.();
void sideConversation?.stopParentObserver?.();
Expand Down Expand Up @@ -1239,6 +1244,39 @@ export async function runMakaPiTui(input: MakaPiTuiInput): Promise<void> {
// available — the single source the picker and connection/model lookups read.
let modelChoices = input.modelChoices;
let connectionIdentities = input.connectionIdentities;
// Derived, never mirrored: both the choices and the selected target move —
// the Host republishes its catalog, the user switches model — and a stored
// copy of what the two imply has to be resynchronized at every one of those
// points or go stale at the one that was missed.
// The slug and model identify the target; the id narrows it only once the
// caller or a session summary has supplied one, because two connections can
// share a slug across a rebind but a caller need not know either id.
const currentModelChoice = (): ModelChoice | undefined =>
modelChoices?.find(
(choice) =>
choice.connectionSlug === connectionSlug &&
choice.model === model &&
(connectionId === undefined || choice.connectionId === connectionId),
);
const onInitialTarget = (): boolean =>
connectionId === input.connectionId &&
connectionSlug === input.connectionSlug &&
model === input.model;
/** The caller's value stands only while no choice describes the target it came with. */
const currentModelContextWindow = (): number | undefined =>
currentModelChoice()?.contextWindow ??
(onInitialTarget() ? input.modelContextWindow : undefined);
// The Host resolved these when it projected the choice — including a relay's
// declared `relayModelProfiles[model].thinkingLevels`. A model no choice
// describes offers none rather than a locally guessed list.
const currentThinkingLevels = (): readonly ThinkingLevel[] =>
currentModelChoice()?.thinkingLevels ?? [];
unsubscribeModelCatalogChanges = input.subscribeModelCatalogChanges?.((refresh) => {
if (closed) return;
modelChoices = refresh.modelChoices;
connectionIdentities = refresh.connectionIdentities;
requestRender();
});
// Monotonic attempt id: each setup submit captures one, and any transition
// that abandons the in-flight attempt (back, re-pick, close) increments it so
// a late verify/save settlement cannot clobber a newer attempt.
Expand Down Expand Up @@ -1515,9 +1553,6 @@ export async function runMakaPiTui(input: MakaPiTuiInput): Promise<void> {
const adoptSessionMetadata = (summary: SessionSummary, announceIdentity = true) => {
cwd = summary.cwd ?? cwd;
setSessionTitle(summary.name);
const previousModel = model;
const previousConnectionId = connectionId;
const previousConnectionSlug = connectionSlug;
model = summary.model;
connectionId = summary.llmConnectionId;
connectionSlug = summary.llmConnectionSlug;
Expand All @@ -1530,25 +1565,9 @@ export async function runMakaPiTui(input: MakaPiTuiInput): Promise<void> {
state.entries.push({ kind: 'notice', level: 'error', text: identityNotice });
}
connectionIdentityNotice = identityNotice;
const contextWindowMatch = modelChoices?.find(
(choice) =>
choice.connectionId === summary.llmConnectionId &&
choice.connectionSlug === summary.llmConnectionSlug &&
choice.model === summary.model,
);
if (contextWindowMatch) {
modelContextWindow = contextWindowMatch.contextWindow;
} else if (
previousConnectionId !== summary.llmConnectionId ||
previousConnectionSlug !== summary.llmConnectionSlug ||
previousModel !== summary.model
) {
modelContextWindow = undefined;
}
permissionMode = input.driver.getPermissionMode?.() ?? summary.permissionMode;
orchestrationMode = summary.orchestrationMode ?? 'default';
thinkingLevel = summary.thinkingLevel;
thinkingLevels = contextWindowMatch?.thinkingLevels ?? [];
refreshEditorCwd?.(cwd);
};

Expand All @@ -1565,15 +1584,7 @@ export async function runMakaPiTui(input: MakaPiTuiInput): Promise<void> {
const previousModel = transcriptLastUsedModel ?? model;
await input.driver.setModel(nextModel);
model = nextModel;
// Same-connection switch: scope the choice lookup to the live connection
// (another connection may expose the same model id with different
// declared thinking levels).
const match = modelChoices?.find(
(choice) => choice.connectionSlug === connectionSlug && choice.model === nextModel,
);
if (match) modelContextWindow = match.contextWindow;
thinkingLevel = undefined;
thinkingLevels = match?.thinkingLevels ?? [];
state.entries.push({
kind: 'notice',
level: 'info',
Expand All @@ -1599,9 +1610,7 @@ export async function runMakaPiTui(input: MakaPiTuiInput): Promise<void> {
model = choice.model;
connectionId = choice.connectionId;
connectionSlug = choice.connectionSlug;
modelContextWindow = choice.contextWindow;
thinkingLevel = undefined;
thinkingLevels = choice.thinkingLevels;
state.entries.push({
kind: 'notice',
level: 'info',
Expand Down Expand Up @@ -2854,7 +2863,7 @@ export async function runMakaPiTui(input: MakaPiTuiInput): Promise<void> {
};

const showThinkingLevelList = () => {
const items = thinkingLevelPickerItems(thinkingLevels, thinkingLevel, locale);
const items = thinkingLevelPickerItems(currentThinkingLevels(), thinkingLevel, locale);
showSelectPicker(
pickerCopy.thinkingPickerTitle,
thinkingLevel ?? 'default',
Expand Down Expand Up @@ -3507,6 +3516,7 @@ export async function runMakaPiTui(input: MakaPiTuiInput): Promise<void> {
description: primaryGuidance.commands.thinking,
midTurn: 'refuse',
run: (parts: string[]) => {
const thinkingLevels = currentThinkingLevels();
if (parts.length === 1) {
if (thinkingLevels.length === 0) {
state.entries.push({
Expand Down
1 change: 1 addition & 0 deletions packages/cli/src/runtime-host-tui-command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ export async function runRuntimeHostTui(input: RunRuntimeHostTuiInput): Promise<
)
.map((choice) => choice.model),
modelChoices: context.modelChoices,
subscribeModelCatalogChanges: context.subscribeModelCatalogChanges,
connectionSlug: context.connectionSlug,
connectionId: context.connectionId,
connectionIdentities: context.connectionIdentities,
Expand Down
26 changes: 26 additions & 0 deletions packages/cli/src/runtime-host-tui-context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ import {
} from '@maka/storage/process-lifetime-owner';
import {
readRuntimeHostAgentGraphEpochs,
readRuntimeHostConnectionCatalog,
readRuntimeHostInvocableSkills,
readRuntimeHostProjects,
isRuntimeHostReconnectingConnection,
Expand Down Expand Up @@ -84,6 +85,18 @@ export interface RuntimeHostTuiContext {
readonly model: string;
readonly modelContextWindow?: number;
readonly modelChoices: readonly ModelChoice[];
/**
* The Host now resolves connection catalogs differently — it refreshed its
* models.dev catalog. Re-read and re-project rather than patching what is
* held: which models are offerable and what is true about them are both the
* Host's answers.
*/
readonly subscribeModelCatalogChanges: (
listener: (refresh: {
readonly modelChoices: readonly ModelChoice[];
readonly connectionIdentities: readonly ConnectionIdentity[];
}) => void,
) => () => void;
/**
* Mode a Session created right now would start in, for display only. The
* driver never receives it: an omitted create field is what lets the Host
Expand Down Expand Up @@ -207,6 +220,19 @@ export async function createRuntimeHostTuiContext(
model: selectedTarget.model,
...(modelContextWindow === undefined ? {} : { modelContextWindow }),
modelChoices,
subscribeModelCatalogChanges: (listener) =>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 — Close the initial-query/subscription gap. The TUI captures connected.catalog at line 142, performs several awaited setup/recovery operations, returns this subscription closure, and only invokes it later in runMakaPiTui (pi-tui-runner.ts:1252). The Host publishes this refresh once, and RuntimeHostConnection does not retain/replay the revision when its local listener set is empty. If the startup fetch completes in that interval, the frame is discarded and this TUI keeps the bundled metadata for its whole lifetime even though the Host installed the live table. Subscribe before accepting the initial snapshot, or subscribe first and immediately re-read/reconcile so a refresh in the gap cannot be lost.

connection.subscribeConnectionCatalogChanges(() => {
void readRuntimeHostConnectionCatalog(connection)
.then((refreshed) =>
listener({
modelChoices: projectRuntimeHostModelChoices(refreshed),
connectionIdentities: projectRuntimeHostConnectionIdentities(refreshed),
}),
)
// A catalog that will not read leaves the choices the TUI already
// has. The Host announces again the next time it changes.
.catch(() => undefined);
}),
prospectivePermissionMode,
turnActivity: createHostOwnedTurnActivity(),
listSkills: (cwd) =>
Expand Down
3 changes: 3 additions & 0 deletions packages/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@
"./long-term-memory": "./dist/long-term-memory.js",
"./local-memory": "./dist/local-memory.js",
"./web-search": "./dist/web-search.js",
"./bounded-response": "./dist/bounded-response.js",
"./incognito": "./dist/incognito.js",
"./backend-types": "./dist/backend-types.js",
"./codex-model-compatibility": "./dist/codex-model-compatibility.js",
Expand All @@ -90,6 +91,8 @@
"./model-catalog": "./dist/model-catalog.js",
"./model-facts": "./dist/model-facts.js",
"./model-metadata": "./dist/model-metadata.js",
"./models-dev-projection": "./dist/models-dev-projection.js",
"./models-dev-refresh": "./dist/models-dev-refresh.js",
"./model-web-search": "./dist/model-web-search.js",
"./model-thinking": "./dist/model-thinking.js",
"./persisted-value": "./dist/persisted-value.js",
Expand Down
Loading