fix: reuse runtime processes across project sessions

Multiple projects could spawn duplicate heavyweight runtimes, while settings
changes and remote reconnection could cancel or strand native conversations.
Share compatible local OpenCode and DeepSeek Harness process owners and
Agent-owned ACP connections, retaining workspace routing, per-session modes,
model attribution, question ownership, targeted cancellation and idle cleanup.

Restore remote pending questions and terminal task projections across reconnect
and desktop restart, and preserve accepted ordinary requests during settings
replacement while cancelling expert-team parents with their replaced scheduler.
Extend regression coverage and Agent type checks. Record measured resource and
real-model validation results, including failures and limitations.

Also include the conversation media-generation design drafts (not an implemented
feature), documentation entry links and dedicated Linux test-directory guidance.
Desktop and Agent changes must be delivered together for shared process results.

Validation: typecheck and lint passed; staged content and documentation links
checked. Prior full regression and live-model evidence are recorded in progress
notes; no new full test suite or model calls were run for this commit.

Release note: 多项目会话现在复用兼容的 OpenCode 和 DeepSeek Harness 进程,减少重复启动并保持工作区、工具和取消隔离。修复远端问答在断线、重启及配置更新后的恢复与状态同步问题,Desktop 与 Agent 需配套更新。
This commit is contained in:
mesalogo
2026-09-15 08:14:42 +08:00
parent b755951ecd
commit e5aa9ed688
65 changed files with 4116 additions and 365 deletions
+105 -34
View File
@@ -204,11 +204,13 @@ export type DeepSeekHarnessRuntimeOptions = {
skillPackages?: RuntimeSkillPackage[]
extensionPackages?: ControlledHarnessExtensionPackage[]
toolProvider?: ModelToolProviderLike
createToolProvider?: (workspace: string) => ModelToolProviderLike
loadAcpSdk?: () => Promise<DeepSeekHarnessAcpSdk>
}
type ActiveRun = {
request: AgentExecutionRequest
toolProvider?: ModelToolProviderLike
toolController: AbortController
authorize?: RuntimeAuthorizer
updates: AcpSessionNotification['update'][]
@@ -454,7 +456,8 @@ export class DeepSeekHarnessRuntime implements AgentRuntime {
private disposed = false
private fatalError?: Error
private stderrBytes = 0
private readonly sessions = new Map<string, string>()
private readonly sessions = new Map<string, { id: string; workspace: string }>()
private readonly workspaceTools = new Map<string, ModelToolProviderLike>()
private readonly proxyToolCatalogs = new Map<string, ModelToolDefinition[]>()
private readonly sessionInitializations = new Map<
string,
@@ -832,10 +835,8 @@ export class DeepSeekHarnessRuntime implements AgentRuntime {
'DeepSeek Harness MCP 工具上下文不可用'
)
}
if (!this.options.toolProvider) {
return { tools: [] }
}
const run = this.activeRuns.get(params.sessionId)
if (!run?.toolProvider || run.closed) return { tools: [] }
const context = {
conversationId:
run?.request.conversationId ??
@@ -848,7 +849,7 @@ export class DeepSeekHarnessRuntime implements AgentRuntime {
knowledgeCapabilityToken:
run?.request.knowledgeCapabilityToken
}
const tools = await this.options.toolProvider.listTools(
const tools = await run.toolProvider.listTools(
context,
connection.signal
)
@@ -869,7 +870,7 @@ export class DeepSeekHarnessRuntime implements AgentRuntime {
if (
!run ||
run.closed ||
!this.options.toolProvider ||
!run.toolProvider ||
typeof name !== 'string' ||
!argumentsValue ||
typeof argumentsValue !== 'object' ||
@@ -935,7 +936,7 @@ export class DeepSeekHarnessRuntime implements AgentRuntime {
}
if (!isWebTool) {
const approval =
this.options.toolProvider.getApproval(
run.toolProvider.getApproval(
tool,
argumentsValue as Record<string, unknown>,
argumentSummary,
@@ -950,7 +951,7 @@ export class DeepSeekHarnessRuntime implements AgentRuntime {
)
}
}
const result = await this.options.toolProvider.callTool(
const result = await run.toolProvider.callTool(
name,
argumentsValue as Record<string, unknown>,
run.toolController.signal,
@@ -1091,6 +1092,35 @@ export class DeepSeekHarnessRuntime implements AgentRuntime {
}
async getStatus(): Promise<AgentRuntimeStatus> {
return {
id: 'deepseek-harness',
label: 'DeepSeek Harness',
available: !this.disposed && !this.fatalError,
supportsToolExecution: true,
detail: this.disposed
? 'DeepSeek Harness Runtime 已关闭'
: this.fatalError?.message ??
(this.state
? `DeepSeek Harness ${this.state.capabilities.harnessVersion} · 当前用户权限`
: 'DeepSeek Harness 已配置,将在首次使用时启动')
}
}
get hasRetainedSessions(): boolean {
return this.sessions.size > 0 || this.sessionInitializations.size > 0
}
private toolsFor(workspace: string): ModelToolProviderLike | undefined {
if (!this.options.createToolProvider) return this.options.toolProvider
let provider = this.workspaceTools.get(workspace)
if (!provider) {
provider = this.options.createToolProvider(workspace)
this.workspaceTools.set(workspace, provider)
}
return provider
}
async testConnection(): Promise<AgentRuntimeStatus> {
try {
await this.getState()
return {
@@ -1114,10 +1144,13 @@ export class DeepSeekHarnessRuntime implements AgentRuntime {
}
}
async getNativeSnapshot(): Promise<RuntimeNativeSnapshot> {
async getNativeSnapshot(workspace?: string): Promise<RuntimeNativeSnapshot> {
const state = await this.getState()
const response = await withTimeout(
state.agent.extMethod(GOODBUDDY_NATIVE_SNAPSHOT, {}),
state.agent.extMethod(
GOODBUDDY_NATIVE_SNAPSHOT,
workspace ? { workspace } : {}
),
this.initializationTimeoutMs,
'原生能力清单'
)
@@ -1302,11 +1335,15 @@ export class DeepSeekHarnessRuntime implements AgentRuntime {
private async getSession(
state: HarnessState,
conversationId: string
conversationId: string,
workspace: string
): Promise<string> {
const current = this.sessions.get(conversationId)
if (current?.workspace === workspace) {
return current.id
}
if (current) {
return current
await this.releaseConversation(conversationId)
}
const pending = this.sessionInitializations.get(conversationId)
if (pending) {
@@ -1314,14 +1351,14 @@ export class DeepSeekHarnessRuntime implements AgentRuntime {
}
const creation = state.agent
.newSession({
cwd: state.workspace,
cwd: workspace,
mcpServers: []
})
.then((response) => {
if (!response.sessionId) {
throw new Error('DeepSeek Harness 未返回 ACP 会话 ID')
}
this.sessions.set(conversationId, response.sessionId)
this.sessions.set(conversationId, { id: response.sessionId, workspace })
return response.sessionId
})
this.sessionInitializations.set(conversationId, creation)
@@ -1445,12 +1482,17 @@ export class DeepSeekHarnessRuntime implements AgentRuntime {
try {
state = await this.getState()
signal.throwIfAborted()
const workspace = request.executionWorkspace
? await realpath(request.executionWorkspace)
: state.workspace
sessionId = await this.getSession(
state,
request.conversationId
request.conversationId,
workspace
)
run = {
request,
toolProvider: this.toolsFor(workspace),
toolController,
authorize,
updates: [],
@@ -1586,7 +1628,14 @@ export class DeepSeekHarnessRuntime implements AgentRuntime {
this.shutdownTimeoutMs,
'取消请求'
).catch(async () => {
await this.terminate(state!.child)
await withTimeout(
state!.agent.extMethod(GOODBUDDY_RELEASE, { sessionId }),
this.shutdownTimeoutMs,
'释放已取消会话'
).catch(async () => {
await this.terminate(state!.child)
})
await this.forgetSession(request.conversationId)
})
}
}
@@ -1609,25 +1658,39 @@ export class DeepSeekHarnessRuntime implements AgentRuntime {
}
async releaseConversation(conversationId: string): Promise<void> {
const sessionId = this.sessions.get(conversationId)
this.sessions.delete(conversationId)
if (sessionId) this.proxyToolCatalogs.delete(sessionId)
if (!sessionId || !this.state) {
await this.options.toolProvider
?.releaseConversation(conversationId)
.catch(() => undefined)
return
await this.sessionInitializations.get(conversationId)?.catch(() => undefined)
const session = this.sessions.get(conversationId)
const sessionId = session?.id
if (sessionId && this.state) {
await this.state.agent
.extMethod(GOODBUDDY_RELEASE, { sessionId })
.catch(async () => {
await this.state?.agent
.cancel({ sessionId })
.catch(() => undefined)
})
}
await this.state.agent
.extMethod(GOODBUDDY_RELEASE, { sessionId })
.catch(async () => {
await this.state?.agent
.cancel({ sessionId })
.catch(() => undefined)
})
await this.options.toolProvider
await this.forgetSession(conversationId)
}
private async forgetSession(conversationId: string): Promise<void> {
const session = this.sessions.get(conversationId)
const provider = session
? this.workspaceTools.get(session.workspace) ?? this.options.toolProvider
: this.options.toolProvider
this.sessions.delete(conversationId)
if (session) this.proxyToolCatalogs.delete(session.id)
await provider
?.releaseConversation(conversationId)
.catch(() => undefined)
if (
session && provider &&
this.workspaceTools.get(session.workspace) === provider &&
![...this.sessions.values()].some((other) => other.workspace === session.workspace)
) {
this.workspaceTools.delete(session.workspace)
await provider.dispose()
}
}
async dispose(): Promise<void> {
@@ -1662,8 +1725,16 @@ export class DeepSeekHarnessRuntime implements AgentRuntime {
this.proxyToolCatalogs.clear()
this.sessionInitializations.clear()
this.conversationTails.clear()
const providers = new Set([
...this.workspaceTools.values(),
...(this.options.toolProvider ? [this.options.toolProvider] : [])
])
this.workspaceTools.clear()
const disposeTools = () => Promise.allSettled(
[...providers].map((provider) => provider.dispose())
)
if (!state) {
await this.options.toolProvider?.dispose().catch(() => undefined)
await disposeTools()
return
}
await withTimeout(
@@ -1671,7 +1742,7 @@ export class DeepSeekHarnessRuntime implements AgentRuntime {
this.shutdownTimeoutMs,
'内部控制面关闭'
).catch(() => undefined)
await this.options.toolProvider?.dispose().catch(() => undefined)
await disposeTools()
await this.terminate(state.child)
await withTimeout(
Promise.allSettled([