fix(opencode): switch session models without restart
This commit is contained in:
@@ -271,6 +271,16 @@ export function createOpencodeClient(options: OpencodeClientOptions) {
|
||||
body: JSON.stringify(payload),
|
||||
signal: options?.signal,
|
||||
}),
|
||||
switchSessionModel: (
|
||||
sessionID: string,
|
||||
model: OpencodeModelReference,
|
||||
options?: OpencodeRuntimeRequestOptions,
|
||||
): Promise<void> =>
|
||||
request<void>(`/api/session/${encodeURIComponent(sessionID)}/model`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ model }),
|
||||
signal: options?.signal,
|
||||
}),
|
||||
executeSessionCommand: (
|
||||
sessionID: string,
|
||||
payload: ExecuteOpencodeSessionCommandInput,
|
||||
|
||||
@@ -24,6 +24,12 @@ interface ProjectAgentRuntimeState {
|
||||
desiredFingerprint: string;
|
||||
appliedFingerprint: string | null;
|
||||
bootstrapCandidateFingerprint: string | null;
|
||||
desiredAgentHashes: Map<string, string>;
|
||||
desiredAgentDefinitions: Map<string, string>;
|
||||
appliedAgentHashes: Map<string, string>;
|
||||
bootstrapCandidateAgentHashes: Map<string, string> | null;
|
||||
hotAddCandidateAgentIds: Set<string>;
|
||||
knownAgentIds: Set<string>;
|
||||
}
|
||||
|
||||
export interface ProjectAgentRuntimeSnapshot extends ProjectAgentRuntimeState {
|
||||
@@ -56,6 +62,59 @@ function canonicalProjectPath(projectPath: string): string {
|
||||
}
|
||||
}
|
||||
|
||||
function buildProjectAgentHashes(config: ProjectConfig): Map<string, string> {
|
||||
return new Map(buildProjectAgentManifest(config).entries.map((entry) => [
|
||||
path.posix.basename(entry.relativePath, '.md'),
|
||||
entry.contentHash,
|
||||
]));
|
||||
}
|
||||
|
||||
function buildProjectAgentDefinitions(config: ProjectConfig): Map<string, string> {
|
||||
return new Map(config.agents.map((agent) => [agent.id, JSON.stringify(agent)]));
|
||||
}
|
||||
|
||||
function appliedManifestMatchesDesired(state: ProjectAgentRuntimeState): boolean {
|
||||
return [...state.desiredAgentHashes].every(
|
||||
([agentId, desiredHash]) => state.appliedAgentHashes.get(agentId) === desiredHash,
|
||||
);
|
||||
}
|
||||
|
||||
function refreshAppliedFingerprint(state: ProjectAgentRuntimeState): void {
|
||||
state.appliedFingerprint = appliedManifestMatchesDesired(state)
|
||||
? state.desiredFingerprint
|
||||
: null;
|
||||
}
|
||||
|
||||
function createRuntimeState(
|
||||
manager: ProjectAgentRuntimeManager,
|
||||
config: ProjectConfig,
|
||||
options: { forcePending?: boolean } = {},
|
||||
): ProjectAgentRuntimeState {
|
||||
const runtimeGeneration = manager.getRuntimeGeneration?.() ?? 0;
|
||||
const provenance: RuntimeGenerationProvenance = manager.getRuntimeGenerationProvenance?.() ?? 'unknown';
|
||||
const manifest = buildProjectAgentManifest(config);
|
||||
const desiredAgentHashes = buildProjectAgentHashes(config);
|
||||
const desiredAgentDefinitions = buildProjectAgentDefinitions(config);
|
||||
const authoritativeFresh = provenance === 'fresh' && !options.forcePending;
|
||||
return {
|
||||
runtimeGeneration,
|
||||
runtimeGenerationProvenance: provenance,
|
||||
desiredFingerprint: manifest.fingerprint,
|
||||
appliedFingerprint: authoritativeFresh ? manifest.fingerprint : null,
|
||||
bootstrapCandidateFingerprint: provenance === 'starting' && !options.forcePending
|
||||
? manifest.fingerprint
|
||||
: null,
|
||||
desiredAgentHashes,
|
||||
desiredAgentDefinitions,
|
||||
appliedAgentHashes: authoritativeFresh ? new Map(desiredAgentHashes) : new Map(),
|
||||
bootstrapCandidateAgentHashes: provenance === 'starting' && !options.forcePending
|
||||
? new Map(desiredAgentHashes)
|
||||
: null,
|
||||
hotAddCandidateAgentIds: new Set(),
|
||||
knownAgentIds: new Set(desiredAgentHashes.keys()),
|
||||
};
|
||||
}
|
||||
|
||||
function getManagerMap<T>(registry: WeakMap<object, Map<string, T>>, manager: object): Map<string, T> {
|
||||
let entries = registry.get(manager);
|
||||
if (!entries) {
|
||||
@@ -109,28 +168,55 @@ function observeState(
|
||||
const states = getManagerMap(runtimeStates, manager as object);
|
||||
const runtimeGeneration = manager.getRuntimeGeneration?.() ?? 0;
|
||||
const provenance: RuntimeGenerationProvenance = manager.getRuntimeGenerationProvenance?.() ?? 'unknown';
|
||||
const desiredFingerprint = buildProjectAgentManifest(config).fingerprint;
|
||||
const desiredManifest = buildProjectAgentManifest(config);
|
||||
const desiredAgentHashes = buildProjectAgentHashes(config);
|
||||
const desiredAgentDefinitions = buildProjectAgentDefinitions(config);
|
||||
const current = states.get(canonicalPath);
|
||||
if (!current || current.runtimeGeneration !== runtimeGeneration) {
|
||||
const next = {
|
||||
runtimeGeneration,
|
||||
runtimeGenerationProvenance: provenance,
|
||||
desiredFingerprint,
|
||||
appliedFingerprint: provenance === 'fresh' ? desiredFingerprint : null,
|
||||
bootstrapCandidateFingerprint: provenance === 'starting' ? desiredFingerprint : null,
|
||||
};
|
||||
const next = createRuntimeState(manager, config);
|
||||
states.set(canonicalPath, next);
|
||||
return next;
|
||||
}
|
||||
current.desiredFingerprint = desiredFingerprint;
|
||||
for (const [agentId, desiredHash] of desiredAgentHashes) {
|
||||
const previousDesiredHash = current.desiredAgentHashes.get(agentId);
|
||||
if (
|
||||
current.desiredAgentDefinitions.get(agentId) === desiredAgentDefinitions.get(agentId)
|
||||
&& previousDesiredHash
|
||||
&& current.appliedAgentHashes.get(agentId) === previousDesiredHash
|
||||
) {
|
||||
current.appliedAgentHashes.set(agentId, desiredHash);
|
||||
}
|
||||
if (
|
||||
!current.knownAgentIds.has(agentId)
|
||||
&& (provenance === 'fresh' || provenance === 'starting')
|
||||
) {
|
||||
current.hotAddCandidateAgentIds.add(agentId);
|
||||
}
|
||||
current.knownAgentIds.add(agentId);
|
||||
}
|
||||
for (const agentId of current.hotAddCandidateAgentIds) {
|
||||
if (!desiredAgentHashes.has(agentId)) current.hotAddCandidateAgentIds.delete(agentId);
|
||||
}
|
||||
for (const agentId of current.desiredAgentHashes.keys()) {
|
||||
if (desiredAgentHashes.has(agentId)) continue;
|
||||
current.appliedAgentHashes.delete(agentId);
|
||||
current.bootstrapCandidateAgentHashes?.delete(agentId);
|
||||
}
|
||||
current.desiredFingerprint = desiredManifest.fingerprint;
|
||||
current.desiredAgentHashes = desiredAgentHashes;
|
||||
current.desiredAgentDefinitions = desiredAgentDefinitions;
|
||||
if (provenance !== 'fresh') {
|
||||
current.appliedFingerprint = null;
|
||||
} else if (current.runtimeGenerationProvenance === 'starting') {
|
||||
current.appliedFingerprint = current.bootstrapCandidateFingerprint === desiredFingerprint
|
||||
? desiredFingerprint
|
||||
: null;
|
||||
const bootstrapHashes = current.bootstrapCandidateAgentHashes ?? new Map();
|
||||
current.appliedAgentHashes = new Map([...bootstrapHashes].filter(
|
||||
([agentId, hash]) => desiredAgentHashes.get(agentId) === hash,
|
||||
));
|
||||
refreshAppliedFingerprint(current);
|
||||
} else if (current.runtimeGenerationProvenance !== 'fresh') {
|
||||
current.appliedFingerprint = null;
|
||||
} else {
|
||||
refreshAppliedFingerprint(current);
|
||||
}
|
||||
current.runtimeGenerationProvenance = provenance;
|
||||
return current;
|
||||
@@ -171,19 +257,19 @@ export async function observeProjectAgentRuntimeGeneration(
|
||||
const runtimeGeneration = manager.getRuntimeGeneration?.() ?? 0;
|
||||
const provenance: RuntimeGenerationProvenance = manager.getRuntimeGenerationProvenance?.() ?? 'unknown';
|
||||
if (current.runtimeGeneration !== runtimeGeneration) {
|
||||
current.runtimeGeneration = runtimeGeneration;
|
||||
current.runtimeGenerationProvenance = provenance;
|
||||
current.appliedFingerprint = provenance === 'fresh' ? current.desiredFingerprint : null;
|
||||
current.bootstrapCandidateFingerprint = provenance === 'starting'
|
||||
? current.desiredFingerprint
|
||||
: null;
|
||||
const config = await loadValidProjectConfig(canonicalPath);
|
||||
const next = createRuntimeState(manager, config);
|
||||
states.set(canonicalPath, next);
|
||||
return { projectPath: canonicalPath, ...next };
|
||||
} else if (provenance !== 'fresh') {
|
||||
current.appliedFingerprint = null;
|
||||
current.runtimeGenerationProvenance = provenance;
|
||||
} else if (current.runtimeGenerationProvenance === 'starting') {
|
||||
current.appliedFingerprint = current.bootstrapCandidateFingerprint === current.desiredFingerprint
|
||||
? current.desiredFingerprint
|
||||
: null;
|
||||
const bootstrapHashes = current.bootstrapCandidateAgentHashes ?? new Map();
|
||||
current.appliedAgentHashes = new Map([...bootstrapHashes].filter(
|
||||
([agentId, hash]) => current.desiredAgentHashes.get(agentId) === hash,
|
||||
));
|
||||
refreshAppliedFingerprint(current);
|
||||
current.runtimeGenerationProvenance = provenance;
|
||||
} else if (current.runtimeGenerationProvenance !== 'fresh') {
|
||||
current.appliedFingerprint = null;
|
||||
@@ -200,13 +286,7 @@ export async function markProjectAgentRuntimePending(
|
||||
): Promise<ProjectAgentRuntimeSnapshot> {
|
||||
const canonicalPath = canonicalProjectPath(projectPath);
|
||||
return await withProjectRuntimeLock(manager as object, canonicalPath, async () => {
|
||||
const state = {
|
||||
runtimeGeneration: manager.getRuntimeGeneration?.() ?? 0,
|
||||
runtimeGenerationProvenance: manager.getRuntimeGenerationProvenance?.() ?? 'unknown',
|
||||
desiredFingerprint: buildProjectAgentManifest(config).fingerprint,
|
||||
appliedFingerprint: null,
|
||||
bootstrapCandidateFingerprint: null,
|
||||
};
|
||||
const state = createRuntimeState(manager, config, { forcePending: true });
|
||||
getManagerMap(runtimeStates, manager as object).set(canonicalPath, state);
|
||||
return { projectPath: canonicalPath, ...state };
|
||||
});
|
||||
@@ -224,13 +304,7 @@ export async function mutateProjectAgentRuntime<T>(
|
||||
observeState(manager, canonicalPath, result.previousConfig);
|
||||
observeState(manager, canonicalPath, result.config);
|
||||
} else {
|
||||
const state: ProjectAgentRuntimeState = {
|
||||
runtimeGeneration: manager.getRuntimeGeneration?.() ?? 0,
|
||||
runtimeGenerationProvenance: manager.getRuntimeGenerationProvenance?.() ?? 'unknown',
|
||||
desiredFingerprint: buildProjectAgentManifest(result.config).fingerprint,
|
||||
appliedFingerprint: null,
|
||||
bootstrapCandidateFingerprint: null,
|
||||
};
|
||||
const state = createRuntimeState(manager, result.config, { forcePending: true });
|
||||
getManagerMap(runtimeStates, manager as object).set(canonicalPath, state);
|
||||
}
|
||||
return result.value;
|
||||
@@ -278,20 +352,32 @@ export async function acceptProjectAgentRuntime<T>(
|
||||
const config = await loadValidProjectConfig(canonicalPath);
|
||||
signal?.throwIfAborted();
|
||||
const state = observeState(manager, canonicalPath, config);
|
||||
const configuredAgentIds = config.agents.map((agent) => agent.id);
|
||||
if (
|
||||
!configuredAgentIds.includes(selectedAgentId)
|
||||
|| state.appliedFingerprint !== state.desiredFingerprint
|
||||
) {
|
||||
const desiredHash = state.desiredAgentHashes.get(selectedAgentId);
|
||||
if (!desiredHash) {
|
||||
return { ready: false, runtimeGeneration: state.runtimeGeneration };
|
||||
}
|
||||
const alreadyApplied = state.runtimeGenerationProvenance === 'fresh'
|
||||
&& state.appliedAgentHashes.get(selectedAgentId) === desiredHash;
|
||||
const hotAddCandidate = state.runtimeGenerationProvenance === 'fresh'
|
||||
&& state.hotAddCandidateAgentIds.has(selectedAgentId);
|
||||
if (!alreadyApplied && !hotAddCandidate) {
|
||||
return { ready: false, runtimeGeneration: state.runtimeGeneration };
|
||||
}
|
||||
|
||||
signal?.throwIfAborted();
|
||||
const liveIds = liveAgentIds(await awaitAbortable(client.listAgents({ signal }), signal));
|
||||
if (configuredAgentIds.some((agentId) => !liveIds.has(agentId))) {
|
||||
state.appliedFingerprint = null;
|
||||
if (!liveIds.has(selectedAgentId)) {
|
||||
if (alreadyApplied) {
|
||||
state.appliedAgentHashes.delete(selectedAgentId);
|
||||
refreshAppliedFingerprint(state);
|
||||
}
|
||||
return { ready: false, runtimeGeneration: state.runtimeGeneration };
|
||||
}
|
||||
if (hotAddCandidate) {
|
||||
state.appliedAgentHashes.set(selectedAgentId, desiredHash);
|
||||
state.hotAddCandidateAgentIds.delete(selectedAgentId);
|
||||
refreshAppliedFingerprint(state);
|
||||
}
|
||||
signal?.throwIfAborted();
|
||||
const value = await awaitAbortable(accept(), signal);
|
||||
return {
|
||||
|
||||
Reference in New Issue
Block a user