coder(providers): capture + persist opencode's live ACP commands (no dispatch needed)
The cold ACP probe captured available_commands but read probedCommands synchronously right after newSession, racing opencode's async available_commands_update notification -> captured nothing, only the static manifest showed. The probe now waits (poll <=3s + 300ms settle) for the notification. Captured commands persist to a new available_agents.commands column and are served (merged with the manifest) on the tier-2-skip path, so the agent's discovered commands survive once models are warm and show without a dispatch. Boot warms via the force:true startup snapshot. Caveat: relies on opencode emitting available_commands_update on session creation, not only post-prompt. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -130,6 +130,17 @@ export async function probeAcpProvider(
|
||||
});
|
||||
|
||||
const session = await connection.newSession({ cwd, mcpServers: [] });
|
||||
// available_commands_update is an async session notification opencode sends
|
||||
// shortly AFTER newSession resolves — reading probedCommands synchronously
|
||||
// here races it and captures nothing. Wait briefly for the first batch, then
|
||||
// a short settle for any stragglers (capped well under PROBE_TIMEOUT_MS).
|
||||
const deadline = Date.now() + 3_000;
|
||||
while (probedCommands.length === 0 && Date.now() < deadline) {
|
||||
await new Promise((r) => setTimeout(r, 150));
|
||||
}
|
||||
if (probedCommands.length > 0) {
|
||||
await new Promise((r) => setTimeout(r, 300));
|
||||
}
|
||||
const result = parseSessionResponse(session, agent);
|
||||
result.commands = probedCommands;
|
||||
await connection.closeSession({ sessionId: session.sessionId }).catch(() => {});
|
||||
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
PROVIDER_MANIFEST,
|
||||
} from './provider-manifest.js';
|
||||
import { probeAcpProvider } from './acp-probe.js';
|
||||
import type { ProviderModel, ProviderSnapshotEntry } from './provider-types.js';
|
||||
import type { ProviderModel, ProviderSnapshotEntry, AgentCommand } from './provider-types.js';
|
||||
import { getManifestCommands, mergeCommands } from './provider-commands.js';
|
||||
import { readQwenSettingsModels } from './qwen-settings.js';
|
||||
import { getResolvedRegistry, type ResolvedProviderDef } from './provider-config-registry.js';
|
||||
@@ -22,6 +22,7 @@ interface AgentRow {
|
||||
install_path: string | null;
|
||||
supports_acp: boolean;
|
||||
models: ProviderModel[] | null;
|
||||
commands: AgentCommand[] | null;
|
||||
label: string | null;
|
||||
transport: string | null;
|
||||
last_probed_at: string | Date | null;
|
||||
@@ -82,6 +83,9 @@ async function buildProviderEntry(
|
||||
const fallbackModes = getManifestModes(name);
|
||||
const defaultModeId = getManifestDefaultModeId(name);
|
||||
const manifestCommands = getManifestCommands(name);
|
||||
// Manifest + persisted live ACP commands (captured on a prior cold probe), so
|
||||
// the agent's discovered commands show even when the tier-2 probe is skipped.
|
||||
const dbCommands = mergeCommands(manifestCommands, agentRow?.commands ?? []);
|
||||
const label = agentRow?.label ?? resolved.configLabel ?? resolved.label;
|
||||
const descr = resolved.configDescription ? { description: resolved.configDescription } : {};
|
||||
|
||||
@@ -175,7 +179,7 @@ async function buildProviderEntry(
|
||||
}
|
||||
return {
|
||||
name, label, transport, status: 'ready', enabled: true, installed: true,
|
||||
models: withConfigModels(skipModels), modes: fallbackModes, defaultModeId, commands: manifestCommands,
|
||||
models: withConfigModels(skipModels), modes: fallbackModes, defaultModeId, commands: dbCommands,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -213,7 +217,7 @@ async function buildProviderEntry(
|
||||
}
|
||||
return {
|
||||
name, label, transport, status: 'ready', enabled: true, installed: true,
|
||||
models: withConfigModels(models), modes: fallbackModes, defaultModeId, commands: manifestCommands,
|
||||
models: withConfigModels(models), modes: fallbackModes, defaultModeId, commands: dbCommands,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -242,7 +246,7 @@ export async function getProviderSnapshot(
|
||||
const build = async (): Promise<ProviderSnapshotEntry[]> => {
|
||||
const llamaModels = await fetchLlamaSwapModels(config);
|
||||
const agents = await sql<AgentRow[]>`
|
||||
SELECT name, install_path, supports_acp, models, label, transport, last_probed_at FROM available_agents
|
||||
SELECT name, install_path, supports_acp, models, commands, label, transport, last_probed_at FROM available_agents
|
||||
`;
|
||||
const agentMap = new Map(agents.map((a) => [a.name, a]));
|
||||
const ttlMs = config.PROVIDER_PROBE_TTL_MS;
|
||||
@@ -284,16 +288,34 @@ export async function persistProbedModels(
|
||||
): Promise<void> {
|
||||
let count = 0;
|
||||
for (const entry of entries) {
|
||||
if (entry.name === 'boocode' || entry.models.length === 0) continue;
|
||||
const flatModels = entry.models.map(({ id, label }) => ({ id, label }));
|
||||
await sql`
|
||||
UPDATE available_agents
|
||||
SET models = ${sql.json(flatModels as never)}, last_probed_at = clock_timestamp()
|
||||
WHERE name = ${entry.name}
|
||||
`;
|
||||
count++;
|
||||
if (entry.name === 'boocode') continue;
|
||||
let persisted = false;
|
||||
if (entry.models.length > 0) {
|
||||
const flatModels = entry.models.map(({ id, label }) => ({ id, label }));
|
||||
await sql`
|
||||
UPDATE available_agents
|
||||
SET models = ${sql.json(flatModels as never)}, last_probed_at = clock_timestamp()
|
||||
WHERE name = ${entry.name}
|
||||
`;
|
||||
persisted = true;
|
||||
}
|
||||
// Persist captured ACP commands so they survive the tier-2 probe skip and
|
||||
// show without a dispatch. Only when non-empty — never clobber a prior set.
|
||||
if (entry.commands.length > 0) {
|
||||
const flatCommands = entry.commands.map((c) => ({
|
||||
name: c.name,
|
||||
...(c.description ? { description: c.description } : {}),
|
||||
}));
|
||||
await sql`
|
||||
UPDATE available_agents
|
||||
SET commands = ${sql.json(flatCommands as never)}, last_probed_at = clock_timestamp()
|
||||
WHERE name = ${entry.name}
|
||||
`;
|
||||
persisted = true;
|
||||
}
|
||||
if (persisted) count++;
|
||||
}
|
||||
if (count > 0) {
|
||||
log.info({ count }, 'provider-snapshot: persisted models to available_agents');
|
||||
log.info({ count }, 'provider-snapshot: persisted models/commands to available_agents');
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user