fix(metis): switch primary model to claude-sonnet-4-6 + correct AGENTS.md inaccuracies
Source code change: - src/shared/model-requirements.ts: prepend claude-sonnet-4-6 to metis fallback chain so Sonnet becomes the default. Opus 4.7 max remains as the immediate fallback for callers who want extra reasoning. - src/shared/model-requirements.test.ts: update assertion to expect Sonnet primary + Opus secondary. AGENTS.md accuracy fixes (verified against source): - Agent modes: Sisyphus/Hephaestus are 'primary' (not 'all'); Sisyphus-Junior is 'subagent' (not 'all'). Confirmed via 'const MODE: AgentMode = ...' in each agent file. Also clarified Prometheus has no agentSources factory and is built via buildPrometheusAgentConfig. - Sisyphus fallback chain: corrected order to kimi-k2.6 → k2p5 → kimi-k2.5 → gpt-5.5 medium → glm-5 → big-pickle (was missing kimi-k2.5). - Librarian/Explore: added missing minimax-m2.7 step between -highspeed and claude-haiku-4-5. - Metis chain: removed fictitious gemini-3.1-pro entry. - Sisyphus-Junior chain: spelled out the actual fallback (was 'user-configurable'). - Temperatures: Sisyphus/Hephaestus do not set explicit temperature (model default); Sisyphus-Junior is 0.1 via SISYPHUS_JUNIOR_DEFAULTS. - Quick category default: gpt-5.4-mini (not gpt-5.4-mini-fast). Team-mode corrections: - Eligibility registry has 3 verdicts: eligible (sisyphus, atlas, sisyphus-junior), conditional (hephaestus — needs D-36 teammate permission), hard-reject (oracle, librarian, explore, multimodal-looker, metis, momus, prometheus). - Schema has 11 fields, not 4: added max_messages_per_run, max_wall_clock_minutes, max_member_turns, base_dir, message_payload_max_bytes, recipient_unread_max_bytes, mailbox_poll_interval_ms. - Hooks: 'team-session-events' is 4 sub-handlers in src/plugin/event.ts (team-idle-wake-hint, team-lead-orphan-handler, team-member-error-handler, team-member-status-handler), not a single Continuation-tier hook. - Tier counts now show base + team-mode: ToolGuard 14/15, Transform 5/7. - Total: 52 base hooks, 59 with team-mode. Doc cascade for the Metis change: - docs/guide/orchestration.md, agent-model-matching.md, installation.md - docs/reference/configuration.md, features.md
This commit is contained in:
@@ -10,19 +10,26 @@ User docs: [`docs/guide/team-mode.md`](file:///Users/yeongyu/local-workspaces/om
|
||||
|
||||
## CONFIG
|
||||
|
||||
Full schema: [`src/config/schema/team-mode.ts`](file:///Users/yeongyu/local-workspaces/omo/src/config/schema/team-mode.ts).
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"team_mode": {
|
||||
"enabled": true,
|
||||
"max_parallel_members": 4, // concurrent active members
|
||||
"max_members": 8, // hard cap on team size
|
||||
"tmux_visualization": false // optional tmux pane layout
|
||||
"enabled": false, // gate
|
||||
"tmux_visualization": false, // optional tmux pane layout
|
||||
"max_parallel_members": 4, // 1..8
|
||||
"max_members": 8, // 1..8 hard cap
|
||||
"max_messages_per_run": 10000, // 1..∞
|
||||
"max_wall_clock_minutes": 120, // 1..∞
|
||||
"max_member_turns": 500, // 1..∞
|
||||
"base_dir": null, // optional override of ~/.omo/teams or <project>/.omo/teams
|
||||
"message_payload_max_bytes": 32768, // 1024..∞ — per-message payload cap
|
||||
"recipient_unread_max_bytes": 262144, // 1024..∞ — per-recipient inbox cap
|
||||
"mailbox_poll_interval_ms": 3000 // 500..∞ — recipient poll cadence
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Schema: [`src/config/schema/team-mode.ts`](file:///Users/yeongyu/local-workspaces/omo/src/config/schema/team-mode.ts).
|
||||
|
||||
## 12 TEAM_* TOOLS
|
||||
|
||||
Registered via [`src/plugin/tool-registry.ts`](file:///Users/yeongyu/local-workspaces/omo/src/plugin/tool-registry.ts) `teamModeToolsRecord` only when enabled.
|
||||
@@ -44,14 +51,15 @@ Registered via [`src/plugin/tool-registry.ts`](file:///Users/yeongyu/local-works
|
||||
|
||||
## ELIGIBLE AGENTS
|
||||
|
||||
```
|
||||
ALLOWED: sisyphus, atlas, sisyphus-junior, hephaestus
|
||||
REJECTED at parse: oracle, librarian, explore, multimodal-looker, metis, momus, prometheus
|
||||
```
|
||||
[`AGENT_ELIGIBILITY_REGISTRY`](file:///Users/yeongyu/local-workspaces/omo/src/features/team-mode/types.ts) in `types.ts` — three verdict tiers, each with its own rejection message:
|
||||
|
||||
Read-only and orchestration-only agents are blocked at TeamSpec parse time. For those, the lead delegates via `task` (delegate-task) instead.
|
||||
| Verdict | Agents | Notes |
|
||||
|---------|--------|-------|
|
||||
| `eligible` | sisyphus, atlas, sisyphus-junior | Three only |
|
||||
| `conditional` | hephaestus | Lacks `teammate: "allow"` permission by default. Either apply D-36 patch (add `teammate: "allow"` in `tool-config-handler.ts`) or use `subagent_type: "sisyphus"` instead |
|
||||
| `hard-reject` | oracle, librarian, explore, multimodal-looker, metis, momus, prometheus | Read-only or plan-mode-only — cannot write to mailbox; use `task` (delegate-task) instead |
|
||||
|
||||
Eligibility registry: [`types.ts`](file:///Users/yeongyu/local-workspaces/omo/src/features/team-mode/types.ts) `AGENT_ELIGIBILITY_REGISTRY`.
|
||||
Hard-reject agents throw at TeamSpec parse with a specific message ("Agent 'X' is read-only…"). The error message points members at delegate-task as the right escape hatch.
|
||||
|
||||
## MEMBER KINDS
|
||||
|
||||
@@ -131,13 +139,12 @@ team-mode/
|
||||
| Where | What |
|
||||
|-------|------|
|
||||
| [`src/index.ts`](file:///Users/yeongyu/local-workspaces/omo/src/index.ts) (entry) | `checkTeamModeDependencies()` + `ensureBaseDirs()` if `team_mode.enabled` |
|
||||
| [`src/plugin/tool-registry.ts`](file:///Users/yeongyu/local-workspaces/omo/src/plugin/tool-registry.ts) | `teamModeToolsRecord` gate registers 12 tools |
|
||||
| `src/hooks/team-mode-status-injector/` | Injects `<team_mode_status>` block into messages |
|
||||
| `src/hooks/team-mailbox-injector/` | Pulls pending mailbox messages into agent context |
|
||||
| `src/hooks/team-session-events/` | React to member session lifecycle |
|
||||
| `src/hooks/team-tool-gating/` | Restrict `team_*` tools by member role |
|
||||
| [`src/plugin/tool-registry.ts`](file:///Users/yeongyu/local-workspaces/omo/src/plugin/tool-registry.ts) `teamModeToolsRecord` | Registers 12 `team_*` tools |
|
||||
| [`create-transform-hooks.ts`](file:///Users/yeongyu/local-workspaces/omo/src/plugin/hooks/create-transform-hooks.ts) | Conditionally builds `teamModeStatusInjector` (`team-mode-status-injector` hook) and `teamMailboxInjector` (`team-mailbox-injector` hook) — both Transform tier |
|
||||
| [`create-tool-guard-hooks.ts`](file:///Users/yeongyu/local-workspaces/omo/src/plugin/hooks/create-tool-guard-hooks.ts) | Conditionally builds `teamToolGating` (`team-tool-gating` hook) — Tool Guard tier |
|
||||
| [`src/plugin/event.ts`](file:///Users/yeongyu/local-workspaces/omo/src/plugin/event.ts) | Registers 4 team-session-event handlers from `src/hooks/team-session-events/`: `team-idle-wake-hint`, `team-lead-orphan-handler`, `team-member-error-handler`, `team-member-status-handler` |
|
||||
| [`src/cli/doctor/checks/team-mode.ts`](file:///Users/yeongyu/local-workspaces/omo/src/cli/doctor/checks/team-mode.ts) | Doctor check for team-mode prerequisites |
|
||||
| [`src/features/builtin-skills/skills/team-mode.ts`](file:///Users/yeongyu/local-workspaces/omo/src/features/builtin-skills/skills/team-mode.ts) | Built-in skill that documents the tools — only loaded when enabled |
|
||||
| [`src/features/builtin-skills/skills/team-mode.ts`](file:///Users/yeongyu/local-workspaces/omo/src/features/builtin-skills/skills/team-mode.ts) | Built-in skill documenting the 12 tools — gated on `team_mode.enabled` |
|
||||
|
||||
## WHERE TO LOOK
|
||||
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
import type { TmuxConfig } from "../../config/schema"
|
||||
import { log } from "../../shared"
|
||||
import type { TrackedSession } from "./types"
|
||||
import { queryWindowState } from "./pane-state-querier"
|
||||
import { executeAction } from "./action-executor"
|
||||
|
||||
export async function cleanupTmuxSessions(params: {
|
||||
tmuxConfig: TmuxConfig
|
||||
serverUrl: string
|
||||
sourcePaneId: string | undefined
|
||||
sessions: Map<string, TrackedSession>
|
||||
stopPolling: () => void
|
||||
}): Promise<void> {
|
||||
params.stopPolling()
|
||||
|
||||
if (params.sessions.size === 0) {
|
||||
log("[tmux-session-manager] cleanup complete")
|
||||
return
|
||||
}
|
||||
|
||||
log("[tmux-session-manager] closing all panes", { count: params.sessions.size })
|
||||
const state = params.sourcePaneId ? await queryWindowState(params.sourcePaneId) : null
|
||||
|
||||
if (state) {
|
||||
const closePromises = Array.from(params.sessions.values()).map((tracked) =>
|
||||
executeAction(
|
||||
{ type: "close", paneId: tracked.paneId, sessionId: tracked.sessionId },
|
||||
{ config: params.tmuxConfig, serverUrl: params.serverUrl, windowState: state },
|
||||
).catch((error) =>
|
||||
log("[tmux-session-manager] cleanup error for pane", {
|
||||
paneId: tracked.paneId,
|
||||
error: String(error),
|
||||
}),
|
||||
),
|
||||
)
|
||||
|
||||
await Promise.all(closePromises)
|
||||
}
|
||||
|
||||
params.sessions.clear()
|
||||
log("[tmux-session-manager] cleanup complete")
|
||||
}
|
||||
@@ -0,0 +1,175 @@
|
||||
import type { PluginInput } from "@opencode-ai/plugin"
|
||||
import type { TmuxConfig } from "../../config/schema"
|
||||
import type { CapacityConfig, TrackedSession } from "./types"
|
||||
import { log } from "../../shared"
|
||||
import { queryWindowState } from "./pane-state-querier"
|
||||
import { decideSpawnActions, type SessionMapping } from "./decision-engine"
|
||||
import { executeActions } from "./action-executor"
|
||||
import type { SessionCreatedEvent } from "./session-created-event"
|
||||
import { createTrackedSession } from "./tracked-session-state"
|
||||
|
||||
type OpencodeClient = PluginInput["client"]
|
||||
|
||||
export interface SessionCreatedHandlerDeps {
|
||||
client: OpencodeClient
|
||||
tmuxConfig: TmuxConfig
|
||||
serverUrl: string
|
||||
sourcePaneId: string | undefined
|
||||
sessions: Map<string, TrackedSession>
|
||||
pendingSessions: Set<string>
|
||||
isInsideTmux: () => boolean
|
||||
isEnabled: () => boolean
|
||||
getCapacityConfig: () => CapacityConfig
|
||||
getSessionMappings: () => SessionMapping[]
|
||||
waitForSessionReady: (sessionId: string) => Promise<boolean>
|
||||
startPolling: () => void
|
||||
}
|
||||
|
||||
export async function handleSessionCreated(
|
||||
deps: SessionCreatedHandlerDeps,
|
||||
event: SessionCreatedEvent,
|
||||
): Promise<void> {
|
||||
const enabled = deps.isEnabled()
|
||||
log("[tmux-session-manager] onSessionCreated called", {
|
||||
enabled,
|
||||
tmuxConfigEnabled: deps.tmuxConfig.enabled,
|
||||
isInsideTmux: deps.isInsideTmux(),
|
||||
eventType: event.type,
|
||||
infoId: event.properties?.info?.id,
|
||||
infoParentID: event.properties?.info?.parentID,
|
||||
})
|
||||
|
||||
if (!enabled) return
|
||||
if (event.type !== "session.created") return
|
||||
|
||||
const info = event.properties?.info
|
||||
if (!info?.id || !info?.parentID) return
|
||||
|
||||
const sessionId = info.id
|
||||
const title = info.title ?? "Subagent"
|
||||
|
||||
if (deps.sessions.has(sessionId) || deps.pendingSessions.has(sessionId)) {
|
||||
log("[tmux-session-manager] session already tracked or pending", { sessionId })
|
||||
return
|
||||
}
|
||||
|
||||
if (!deps.sourcePaneId) {
|
||||
log("[tmux-session-manager] no source pane id")
|
||||
return
|
||||
}
|
||||
|
||||
deps.pendingSessions.add(sessionId)
|
||||
|
||||
try {
|
||||
const state = await queryWindowState(deps.sourcePaneId)
|
||||
if (!state) {
|
||||
log("[tmux-session-manager] failed to query window state")
|
||||
return
|
||||
}
|
||||
|
||||
log("[tmux-session-manager] window state queried", {
|
||||
windowWidth: state.windowWidth,
|
||||
mainPane: state.mainPane?.paneId,
|
||||
agentPaneCount: state.agentPanes.length,
|
||||
agentPanes: state.agentPanes.map((p) => p.paneId),
|
||||
})
|
||||
|
||||
const decision = decideSpawnActions(
|
||||
state,
|
||||
sessionId,
|
||||
title,
|
||||
deps.getCapacityConfig(),
|
||||
deps.getSessionMappings(),
|
||||
)
|
||||
|
||||
log("[tmux-session-manager] spawn decision", {
|
||||
canSpawn: decision.canSpawn,
|
||||
reason: decision.reason,
|
||||
actionCount: decision.actions.length,
|
||||
actions: decision.actions.map((a) => {
|
||||
if (a.type === "close") return { type: "close", paneId: a.paneId }
|
||||
if (a.type === "replace") {
|
||||
return { type: "replace", paneId: a.paneId, newSessionId: a.newSessionId }
|
||||
}
|
||||
return { type: "spawn", sessionId: a.sessionId }
|
||||
}),
|
||||
})
|
||||
|
||||
if (!decision.canSpawn) {
|
||||
log("[tmux-session-manager] cannot spawn", { reason: decision.reason })
|
||||
return
|
||||
}
|
||||
|
||||
const result = await executeActions(decision.actions, {
|
||||
config: deps.tmuxConfig,
|
||||
serverUrl: deps.serverUrl,
|
||||
windowState: state,
|
||||
})
|
||||
|
||||
for (const { action, result: actionResult } of result.results) {
|
||||
if (action.type === "close" && actionResult.success) {
|
||||
deps.sessions.delete(action.sessionId)
|
||||
log("[tmux-session-manager] removed closed session from cache", {
|
||||
sessionId: action.sessionId,
|
||||
})
|
||||
}
|
||||
if (action.type === "replace" && actionResult.success) {
|
||||
deps.sessions.delete(action.oldSessionId)
|
||||
log("[tmux-session-manager] removed replaced session from cache", {
|
||||
oldSessionId: action.oldSessionId,
|
||||
newSessionId: action.newSessionId,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if (!result.success || !result.spawnedPaneId) {
|
||||
log("[tmux-session-manager] spawn failed", {
|
||||
success: result.success,
|
||||
results: result.results.map((r) => ({
|
||||
type: r.action.type,
|
||||
success: r.result.success,
|
||||
error: r.result.error,
|
||||
})),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
const sessionReady = await deps.waitForSessionReady(sessionId)
|
||||
if (!sessionReady) {
|
||||
log("[tmux-session-manager] session not ready after timeout, closing spawned pane", {
|
||||
sessionId,
|
||||
paneId: result.spawnedPaneId,
|
||||
})
|
||||
|
||||
await executeActions(
|
||||
[{ type: "close", paneId: result.spawnedPaneId, sessionId }],
|
||||
{
|
||||
config: deps.tmuxConfig,
|
||||
serverUrl: deps.serverUrl,
|
||||
windowState: state,
|
||||
},
|
||||
)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
deps.sessions.set(
|
||||
sessionId,
|
||||
createTrackedSession({
|
||||
sessionId,
|
||||
paneId: result.spawnedPaneId,
|
||||
description: title,
|
||||
}),
|
||||
)
|
||||
|
||||
log("[tmux-session-manager] pane spawned and tracked", {
|
||||
sessionId,
|
||||
paneId: result.spawnedPaneId,
|
||||
sessionReady,
|
||||
})
|
||||
|
||||
deps.startPolling()
|
||||
} finally {
|
||||
deps.pendingSessions.delete(sessionId)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
import type { TmuxConfig } from "../../config/schema"
|
||||
import type { TrackedSession } from "./types"
|
||||
import { log } from "../../shared"
|
||||
import { queryWindowState } from "./pane-state-querier"
|
||||
import { decideCloseAction, type SessionMapping } from "./decision-engine"
|
||||
import { executeAction } from "./action-executor"
|
||||
|
||||
export interface SessionDeletedHandlerDeps {
|
||||
tmuxConfig: TmuxConfig
|
||||
serverUrl: string
|
||||
sourcePaneId: string | undefined
|
||||
sessions: Map<string, TrackedSession>
|
||||
isEnabled: () => boolean
|
||||
getSessionMappings: () => SessionMapping[]
|
||||
stopPolling: () => void
|
||||
}
|
||||
|
||||
export async function handleSessionDeleted(
|
||||
deps: SessionDeletedHandlerDeps,
|
||||
event: { sessionID: string },
|
||||
): Promise<void> {
|
||||
if (!deps.isEnabled()) return
|
||||
if (!deps.sourcePaneId) return
|
||||
|
||||
const tracked = deps.sessions.get(event.sessionID)
|
||||
if (!tracked) return
|
||||
|
||||
log("[tmux-session-manager] onSessionDeleted", { sessionId: event.sessionID })
|
||||
|
||||
const state = await queryWindowState(deps.sourcePaneId)
|
||||
if (!state) {
|
||||
deps.sessions.delete(event.sessionID)
|
||||
return
|
||||
}
|
||||
|
||||
const closeAction = decideCloseAction(state, event.sessionID, deps.getSessionMappings())
|
||||
if (closeAction) {
|
||||
await executeAction(closeAction, {
|
||||
config: deps.tmuxConfig,
|
||||
serverUrl: deps.serverUrl,
|
||||
windowState: state,
|
||||
})
|
||||
}
|
||||
|
||||
deps.sessions.delete(event.sessionID)
|
||||
|
||||
if (deps.sessions.size === 0) {
|
||||
deps.stopPolling()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user