|
|
@@ -19,6 +19,7 @@ import {
|
|
|
AGENT_ALIASES,
|
|
|
DEFAULT_MAX_CONTEXT_LINES,
|
|
|
DEFAULT_MAX_RETAINED_SNAPSHOTS,
|
|
|
+ DEFAULT_MAX_SESSION_METADATA_ENTRIES,
|
|
|
DEFAULT_MAX_SESSIONS_PER_AGENT,
|
|
|
DEFAULT_READ_CONTEXT_MAX_FILES,
|
|
|
DEFAULT_READ_CONTEXT_MIN_LINES,
|
|
|
@@ -75,6 +76,7 @@ import {
|
|
|
} from './utils';
|
|
|
import { isPluginDisabledByEnv } from './utils/env';
|
|
|
import { initLogger, log } from './utils/logger';
|
|
|
+import { SessionMetadataStore } from './utils/session-metadata';
|
|
|
import { collapseSystemInPlace } from './utils/system-collapse';
|
|
|
|
|
|
/**
|
|
|
@@ -147,10 +149,15 @@ const OhMyOpenCodeLite: Plugin = async (ctx) => {
|
|
|
let multiplexerEnabled: boolean;
|
|
|
let multiplexerSessionManager: MultiplexerSessionManager;
|
|
|
let autoUpdateChecker: ReturnType<typeof createAutoUpdateCheckerHook>;
|
|
|
- let sessionAgentMap: Map<string, string>;
|
|
|
- // ponytail: cache sessionID -> project directory so TUI model writes
|
|
|
- // land in the right per-project file after a project switch (ctx.directory is stale)
|
|
|
- const sessionDirectories = new Map<string, string>();
|
|
|
+ const sessionMetadata = new SessionMetadataStore({
|
|
|
+ maxEntries: DEFAULT_MAX_SESSION_METADATA_ENTRIES,
|
|
|
+ onEvict: (sessionID) => {
|
|
|
+ log('[session] evicted oldest session metadata', {
|
|
|
+ threshold: DEFAULT_MAX_SESSION_METADATA_ENTRIES,
|
|
|
+ droppedSessionId: sessionID,
|
|
|
+ });
|
|
|
+ },
|
|
|
+ });
|
|
|
let sessionLifecycle: SessionLifecycle;
|
|
|
|
|
|
let chatHeadersHook: ReturnType<typeof createChatHeadersHook>;
|
|
|
@@ -257,7 +264,30 @@ const OhMyOpenCodeLite: Plugin = async (ctx) => {
|
|
|
Object.keys(config.acpAgents ?? {}).length > 0
|
|
|
? { acp_run: createAcpRunTool(config.acpAgents) }
|
|
|
: {};
|
|
|
- webfetch = createWebfetchTool(ctx);
|
|
|
+ const webfetchModel = config.webfetch?.model;
|
|
|
+ const webfetchModels = (() => {
|
|
|
+ if (!webfetchModel) return undefined;
|
|
|
+ const entries = Array.isArray(webfetchModel)
|
|
|
+ ? webfetchModel
|
|
|
+ : [webfetchModel];
|
|
|
+ type ModelRefInput = string | { id: string; variant?: string };
|
|
|
+ const models: Array<{ id: string; variant?: string }> = [];
|
|
|
+ for (const entry of entries as ModelRefInput[]) {
|
|
|
+ const id = typeof entry === 'string' ? entry : entry.id;
|
|
|
+ if (!id) continue;
|
|
|
+ models.push({
|
|
|
+ id,
|
|
|
+ ...(typeof entry === 'object' && entry.variant
|
|
|
+ ? { variant: entry.variant }
|
|
|
+ : {}),
|
|
|
+ });
|
|
|
+ }
|
|
|
+ return models.length > 0 ? models : undefined;
|
|
|
+ })();
|
|
|
+ webfetch = createWebfetchTool(ctx, {
|
|
|
+ binaryDir: undefined,
|
|
|
+ webfetchModels,
|
|
|
+ });
|
|
|
backgroundJobBoard = new BackgroundJobBoard({
|
|
|
maxReusablePerAgent:
|
|
|
config.backgroundJobs?.maxSessionsPerAgent ??
|
|
|
@@ -296,9 +326,6 @@ const OhMyOpenCodeLite: Plugin = async (ctx) => {
|
|
|
companion: config.companion,
|
|
|
});
|
|
|
|
|
|
- // Track session → agent mapping for serve-mode system prompt injection
|
|
|
- sessionAgentMap = new Map<string, string>();
|
|
|
-
|
|
|
chatHeadersHook = createChatHeadersHook(ctx);
|
|
|
|
|
|
// Initialize foreground fallback manager for runtime model switching.
|
|
|
@@ -332,9 +359,9 @@ const OhMyOpenCodeLite: Plugin = async (ctx) => {
|
|
|
continueOnIdle: config.backgroundJobs?.continueOnIdle === true,
|
|
|
backgroundJobBoard: backgroundJobCoordinator,
|
|
|
shouldManageSession: (sessionID) =>
|
|
|
- sessionAgentMap.get(sessionID) === 'orchestrator',
|
|
|
+ sessionMetadata.getAgent(sessionID) === 'orchestrator',
|
|
|
registerSessionAsOrchestrator: (sessionID) => {
|
|
|
- sessionAgentMap.set(sessionID, 'orchestrator');
|
|
|
+ sessionMetadata.setAgent(sessionID, 'orchestrator');
|
|
|
},
|
|
|
isFallbackInProgress: (sessionID) =>
|
|
|
foregroundFallback.isFallbackInProgress(sessionID),
|
|
|
@@ -373,7 +400,7 @@ const OhMyOpenCodeLite: Plugin = async (ctx) => {
|
|
|
// Both message transforms share this gate so a rejected nudge cannot be
|
|
|
// followed by a phase reminder in the same outgoing turn.
|
|
|
const shouldInjectOrchestratorReminder = (sessionID: string) =>
|
|
|
- sessionAgentMap.get(sessionID) === 'orchestrator';
|
|
|
+ sessionMetadata.getAgent(sessionID) === 'orchestrator';
|
|
|
|
|
|
phaseReminder = createPhaseReminderHook({
|
|
|
shouldInject: shouldInjectOrchestratorReminder,
|
|
|
@@ -415,24 +442,25 @@ const OhMyOpenCodeLite: Plugin = async (ctx) => {
|
|
|
client: ctx.client,
|
|
|
backgroundJobBoard: backgroundJobCoordinator,
|
|
|
shouldManageSession: (sessionID) =>
|
|
|
- sessionAgentMap.get(sessionID) === 'orchestrator',
|
|
|
+ sessionMetadata.getAgent(sessionID) === 'orchestrator',
|
|
|
});
|
|
|
waitForUserTools = createWaitForUserTool({
|
|
|
shouldManageSession: (sessionID) =>
|
|
|
- sessionAgentMap.get(sessionID) === 'orchestrator',
|
|
|
+ sessionMetadata.getAgent(sessionID) === 'orchestrator',
|
|
|
resolveAgentName: (agent) => resolveRuntimeAgentName(config, agent),
|
|
|
registerSessionAsOrchestrator: (sessionID) => {
|
|
|
- sessionAgentMap.set(sessionID, 'orchestrator');
|
|
|
+ sessionMetadata.setAgent(sessionID, 'orchestrator');
|
|
|
},
|
|
|
beginUserWait: (sessionID) =>
|
|
|
taskSessionManagerHook.beginUserWait(sessionID),
|
|
|
});
|
|
|
|
|
|
+ const shouldRegisterWebfetch = config.webfetch?.enabled !== false;
|
|
|
tools = {
|
|
|
...cancelTaskTools,
|
|
|
...waitForUserTools,
|
|
|
...acpRunTools,
|
|
|
- webfetch,
|
|
|
+ ...(shouldRegisterWebfetch ? { webfetch } : {}),
|
|
|
ast_grep_search,
|
|
|
ast_grep_replace,
|
|
|
};
|
|
|
@@ -467,8 +495,10 @@ const OhMyOpenCodeLite: Plugin = async (ctx) => {
|
|
|
Array.isArray(config.disabled_mcps) && config.disabled_mcps.length > 0
|
|
|
? 0
|
|
|
: HEALTH_CHECK.minMcps;
|
|
|
- const toolThreshold = minimumExpectedToolCount(config.disabled_tools);
|
|
|
-
|
|
|
+ const toolThreshold = minimumExpectedToolCount(
|
|
|
+ config.disabled_tools,
|
|
|
+ config.webfetch?.enabled !== false,
|
|
|
+ );
|
|
|
if (
|
|
|
agentCount < HEALTH_CHECK.minAgents ||
|
|
|
toolCount < toolThreshold ||
|
|
|
@@ -918,6 +948,24 @@ const OhMyOpenCodeLite: Plugin = async (ctx) => {
|
|
|
};
|
|
|
};
|
|
|
|
|
|
+ const eventSessionID =
|
|
|
+ event.properties?.info?.id ?? event.properties?.sessionID;
|
|
|
+ const statusType = event.properties?.status?.type;
|
|
|
+ if (eventSessionID) {
|
|
|
+ if (
|
|
|
+ event.type === 'session.status' &&
|
|
|
+ (statusType === 'busy' || statusType === 'retry')
|
|
|
+ ) {
|
|
|
+ sessionMetadata.markOrchestratorActive(eventSessionID);
|
|
|
+ } else if (
|
|
|
+ event.type === 'session.idle' ||
|
|
|
+ (event.type === 'session.status' && statusType === 'idle') ||
|
|
|
+ event.type === 'session.deleted'
|
|
|
+ ) {
|
|
|
+ sessionMetadata.markOrchestratorIdle(eventSessionID);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
if (event.type === 'message.updated') {
|
|
|
const info = event.properties?.info;
|
|
|
const providerID =
|
|
|
@@ -942,7 +990,7 @@ const OhMyOpenCodeLite: Plugin = async (ctx) => {
|
|
|
model,
|
|
|
variant: variant ?? null,
|
|
|
},
|
|
|
- (info?.sessionID && sessionDirectories.get(info.sessionID)) ??
|
|
|
+ (info?.sessionID && sessionMetadata.getDirectory(info.sessionID)) ??
|
|
|
ctx.directory,
|
|
|
);
|
|
|
}
|
|
|
@@ -952,7 +1000,7 @@ const OhMyOpenCodeLite: Plugin = async (ctx) => {
|
|
|
const createdSessionId = event.properties?.info?.id;
|
|
|
const createdSessionDir = event.properties?.info?.directory;
|
|
|
if (createdSessionId && createdSessionDir) {
|
|
|
- sessionDirectories.set(createdSessionId, createdSessionDir);
|
|
|
+ sessionMetadata.setDirectory(createdSessionId, createdSessionDir);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
@@ -1014,7 +1062,7 @@ const OhMyOpenCodeLite: Plugin = async (ctx) => {
|
|
|
const sessionID = props?.sessionID;
|
|
|
companionManager.onSessionStatus({
|
|
|
sessionId: sessionID,
|
|
|
- agent: sessionID ? sessionAgentMap.get(sessionID) : undefined,
|
|
|
+ agent: sessionID ? sessionMetadata.getAgent(sessionID) : undefined,
|
|
|
status: props?.status?.type,
|
|
|
});
|
|
|
}
|
|
|
@@ -1030,8 +1078,7 @@ const OhMyOpenCodeLite: Plugin = async (ctx) => {
|
|
|
}
|
|
|
companionManager.onSessionDeleted(sessionID);
|
|
|
if (sessionID) {
|
|
|
- sessionAgentMap.delete(sessionID);
|
|
|
- sessionDirectories.delete(sessionID);
|
|
|
+ sessionMetadata.delete(sessionID);
|
|
|
}
|
|
|
}
|
|
|
},
|
|
|
@@ -1090,6 +1137,11 @@ const OhMyOpenCodeLite: Plugin = async (ctx) => {
|
|
|
input: {
|
|
|
sessionID: string;
|
|
|
agent?: string;
|
|
|
+ model?: {
|
|
|
+ providerID: string;
|
|
|
+ modelID: string;
|
|
|
+ };
|
|
|
+ variant?: string;
|
|
|
parts?: unknown[];
|
|
|
/** OpenCode chat.message message identity when present. */
|
|
|
messageID?: string;
|
|
|
@@ -1100,6 +1152,11 @@ const OhMyOpenCodeLite: Plugin = async (ctx) => {
|
|
|
agent?: string;
|
|
|
role?: string;
|
|
|
sessionID?: string;
|
|
|
+ model?: {
|
|
|
+ providerID: string;
|
|
|
+ modelID: string;
|
|
|
+ variant?: string;
|
|
|
+ };
|
|
|
};
|
|
|
parts?: unknown[];
|
|
|
},
|
|
|
@@ -1119,7 +1176,7 @@ const OhMyOpenCodeLite: Plugin = async (ctx) => {
|
|
|
|
|
|
if (agent) {
|
|
|
foregroundFallback.registerSessionAgent(input.sessionID, agent);
|
|
|
- sessionAgentMap.set(input.sessionID, agent);
|
|
|
+ sessionMetadata.setAgent(input.sessionID, agent);
|
|
|
// A chat message means this session is actively working. This also
|
|
|
// covers the race where session.status busy fires before the
|
|
|
// session's agent is known.
|
|
|
@@ -1143,7 +1200,7 @@ const OhMyOpenCodeLite: Plugin = async (ctx) => {
|
|
|
output: { system: string[] },
|
|
|
): Promise<void> => {
|
|
|
const agentName = input.sessionID
|
|
|
- ? sessionAgentMap.get(input.sessionID)
|
|
|
+ ? sessionMetadata.getAgent(input.sessionID)
|
|
|
: undefined;
|
|
|
if (agentName === 'orchestrator') {
|
|
|
const alreadyInjected = output.system.some(
|
|
|
@@ -1153,12 +1210,12 @@ const OhMyOpenCodeLite: Plugin = async (ctx) => {
|
|
|
s.includes('orchestrator'),
|
|
|
);
|
|
|
if (!alreadyInjected) {
|
|
|
- // Prepend the orchestrator prompt to the system array. Use the
|
|
|
- // resolved prompt from the orchestrator agent definition (which
|
|
|
- // includes any custom replacement or append from orchestrator.md
|
|
|
- // / orchestrator_append.md) Fall back to
|
|
|
- // buildOrchestratorPrompt only if the resolved prompt is
|
|
|
- // missing.
|
|
|
+ // Place the orchestrator prompt after AGENTS.md so the user's
|
|
|
+ // behavioral rules (language, code conventions, etc.) retain
|
|
|
+ // their intended priority. AGENTS.md is injected by OpenCode
|
|
|
+ // core into system[0]; prepending the orchestrator prompt before
|
|
|
+ // it buries user-defined rules under thousands of lines of
|
|
|
+ // orchestration instructions.
|
|
|
const orchestratorDef = agentDefs.find(
|
|
|
(a) => a.name === 'orchestrator',
|
|
|
);
|
|
|
@@ -1167,8 +1224,8 @@ const OhMyOpenCodeLite: Plugin = async (ctx) => {
|
|
|
? orchestratorDef.config.prompt
|
|
|
: buildOrchestratorPrompt(disabledAgents);
|
|
|
output.system[0] =
|
|
|
- orchestratorPrompt +
|
|
|
- (output.system[0] ? `\n\n${output.system[0]}` : '');
|
|
|
+ (output.system[0] || '') +
|
|
|
+ `\n\n${orchestratorPrompt}`;
|
|
|
}
|
|
|
}
|
|
|
|