diff --git a/docs/architecture/agent-memory-system/spec.md b/docs/architecture/agent-memory-system/spec.md
index 9d22c9b0f..685178c17 100644
--- a/docs/architecture/agent-memory-system/spec.md
+++ b/docs/architecture/agent-memory-system/spec.md
@@ -697,8 +697,8 @@ flowchart TD
- **`tape_context`** expands specific entry ids with a bounded window (before/after default 2, clamp ≤ 20),
an entry cap (default 50), and byte budgets (per-entry default 2048 / max 8192; total default 16384 / max
65536), UTF-8-safe truncated. Tape tools are DeepChat-agent-only and `tape_context` is advertised only when
- the tool-runtime port exposes `getTapeContext` (wired to `AgentSessionPresenter`, not the memory kernel; in
- practice always present).
+ the tool-runtime port exposes `getTapeContext` (wired directly to `SessionProjectionCoordinator`, not the
+ memory kernel; in practice always present).
- The whole projection/search layer is fail-open: any error degrades to a coarser search over the effective
tape rather than throwing (the one exception is an unparseable time boundary, which is reported).
diff --git a/docs/architecture/agent-system-layered-runtime/README.md b/docs/architecture/agent-system-layered-runtime/README.md
index 943399e81..253e9a6f4 100644
--- a/docs/architecture/agent-system-layered-runtime/README.md
+++ b/docs/architecture/agent-system-layered-runtime/README.md
@@ -38,11 +38,12 @@ DeepChat loop + `AcpProvider` compatibility。
Memory runtime orchestration 已收敛到唯一 `MemoryRuntimeCoordinator`,通过 awaited
`MemoryPromptContributor` 与 background `MemoryIngestionObserver` 接入;Tape tool facts 已迁到 stable
-per-fact `TapeRecorder` path,causal observation 只读联结现有 Tape/message/trace。`AgentSessionPresenter`
-仅保留 compatibility forwarding;core lifecycle、turn、assignment 与 projection 已由四个
-composition-owned session application coordinators 承担。typed routes 直接组合 history、translation、
-export、usage、RTK 与 catalog owner,startup hooks 直接调用 migration/maintenance owner。
-`AgentRuntimePresenter` 保留 DeepChat state/delegate 与 adapter wiring;两者不再构成 generic agent runtime。
+per-fact `TapeRecorder` path,causal observation 只读联结现有 Tape/message/trace。core lifecycle、turn、
+assignment 与 projection 已由四个 composition-owned session application coordinators 承担;
+`AgentSessionPresenter` 与 main-process `IAgentSessionPresenter` 已退休。typed routes、Tool、MCP、Floating
+与 hooks 直接使用分离的 coordinator ports;history、translation、export、usage、RTK 与 catalog owner
+继续独立。startup hooks 直接调用 migration/maintenance owner。`AgentRuntimePresenter` 保留 DeepChat
+state/delegate 与 adapter wiring,不再构成 generic agent runtime。
current docs、architecture guards 与 baseline generator
已在 `ASLR-091` 收敛;`ASLR-092` 已完成 canonical baseline write、全量
main/renderer/Memory/native/build/E2E gates 与最终契约 diff。
@@ -173,7 +174,7 @@ accept input / claim pending item
typed routes -> SessionService / ChatService -- narrow ports ─┐
remote -> RemoteConversationRunner -- four narrow ports ─────┤
cron -> Cron session starter -- Lifecycle / Turn ports ──────┤
-AgentSessionPresenter compatibility façade -- forwarding ─────┘
+Tool / MCP / Floating / hooks -- owner-specific ports ────────┘
▼
Lifecycle / Turn / AgentAssignment / Projection
├─ AppSessionService ─ new_sessions / window binding / shared CRUD
@@ -275,7 +276,6 @@ src/main/agent/
src/main/presenter/
├── sessionApplication/ # lifecycle/turn/assignment/projection coordinators
-├── agentSessionPresenter/ # retained compatibility façade
└── agentRuntimePresenter/ # retained DeepChat state/delegate + message/Tape/resource adapters
```
diff --git a/docs/architecture/agent-system-layered-runtime/modules/shared-data-and-io.md b/docs/architecture/agent-system-layered-runtime/modules/shared-data-and-io.md
index 0fe96c680..020b2d0b9 100644
--- a/docs/architecture/agent-system-layered-runtime/modules/shared-data-and-io.md
+++ b/docs/architecture/agent-system-layered-runtime/modules/shared-data-and-io.md
@@ -103,8 +103,8 @@ backend kind 每次由 `agent_id -> AgentDescriptor` 解析。backend 不能复
### ASLR-072 shared data ports
-production switch 没有把 shared data methods 塞进 `AgentManager`。`AgentSessionPresenter` 与 direct ACP
-backend 通过四个独立 facet 访问现有 owner:
+production switch 没有把 shared data methods 塞进 `AgentManager`。session application coordinators 与
+direct ACP backend 通过四个独立 facet 访问现有 owner:
```ts
interface AgentSharedDataPorts {
diff --git a/docs/architecture/agent-system.md b/docs/architecture/agent-system.md
index c4ea5be8c..66892ecea 100644
--- a/docs/architecture/agent-system.md
+++ b/docs/architecture/agent-system.md
@@ -34,7 +34,8 @@ flowchart TD
UI["Renderer / typed routes"] --> RouteOwners["Session route owners
search / translation / export / usage / catalog"]
UI --> Services["SessionService / ChatService"]
Services --> App["Session application coordinators"]
- Compat["AgentSessionPresenter
compatibility forwarding"] --> App
+ UI --> RoutePorts["Main route runtime
four separate session ports"]
+ RoutePorts --> App
App --> Sessions["AppSessionService"]
App --> Manager["AgentManager"]
Manager --> Catalog["strict executable catalog"]
@@ -58,8 +59,8 @@ flowchart TD
- `SessionLifecycleCoordinator`、`SessionTurnCoordinator`、`SessionAgentAssignmentCoordinator` 和
`SessionProjectionCoordinator` 分别拥有 core session application invariants;typed Session/Chat、Remote
和 Cron 通过 consumer-owned narrow ports 调用它们。
-- `AgentSessionPresenter` 仅保留 compatibility public surface;core session methods 只转发到
- composition-owned coordinators,不拥有 application policy、state 或 session-boundary capabilities。
+- `AgentSessionPresenter` 与 main-process `IAgentSessionPresenter` 已退休;main route runtime、Tool、MCP、
+ Floating 与 hooks 直接调用 composition-owned coordinators,不经过 aggregate façade。
- typed session routes 直接组合 `SessionHistorySearch`、`SessionTranslation`、
`AgentSessionExportService`、`UsageStatsService`、RTK runtime service 与 available-agent catalog policy;
lifecycle hooks 直接调用 startup migration/maintenance owner。
@@ -173,7 +174,7 @@ DeepChat app projection;`acp_turns` 只是 protocol metadata。
仍保留:
-- `AgentSessionPresenter` compatibility façade 和 `AgentRuntimePresenter` state/delegate façade;
+- `AgentRuntimePresenter` state/delegate façade;
- `AcpProvider` 的 DeepChat + ACP-provider compatibility;
- `startupMigrations/LegacyChatImportService`、旧 conversations/messages、`SessionPresenter`
export/thread/data compatibility;current agent-session export 由 `AgentSessionExportService` 拥有;
@@ -181,6 +182,7 @@ DeepChat app projection;`acp_turns` 只是 protocol metadata。
已经退休并由 guard 阻止回流:
+- `AgentSessionPresenter` compatibility façade 与 main-process `IAgentSessionPresenter`;
- fake `AgentRegistry`;
- unified optional implementation interface;
- reflection-based legacy backend;
@@ -193,7 +195,7 @@ DeepChat app projection;`acp_turns` 只是 protocol metadata。
1. kind/session routing:`src/main/agent/manager/agentManager.ts`
2. core session application behavior:`src/main/presenter/sessionApplication/`
-3. compatibility forwarding:`src/main/presenter/agentSessionPresenter/index.ts`
+3. route composition:`src/main/routes/index.ts`
4. session search/translation:`src/main/routes/sessions/`
5. current session export:`src/main/presenter/exporter/agentSessionExporter.ts`
6. usage/startup/catalog:`src/main/presenter/{usageStatsService.ts,startupMigrations/}` 与
diff --git a/docs/architecture/baselines/agent-system-layered-runtime-baseline.json b/docs/architecture/baselines/agent-system-layered-runtime-baseline.json
index 05a62c13a..0be9916f4 100644
--- a/docs/architecture/baselines/agent-system-layered-runtime-baseline.json
+++ b/docs/architecture/baselines/agent-system-layered-runtime-baseline.json
@@ -1,7 +1,7 @@
{
"schemaVersion": 2,
"goal": "agent-system-layered-runtime",
- "headCommit": "1122b240619afae1bd97c2395f0c820c41ecf048",
+ "headCommit": "30d57c3873cec94e342ce616d8ccafe9f71676b3",
"relevantWorkingTree": {
"dirty": false,
"files": []
@@ -95,9 +95,12 @@
"src/main/presenter/agentRuntimePresenter/messageStore.ts",
"src/main/presenter/agentRuntimePresenter/process.ts",
"src/main/presenter/agentRuntimePresenter/tapeService.ts",
- "src/main/presenter/agentSessionPresenter/index.ts",
"src/main/presenter/index.ts",
- "src/main/presenter/llmProviderPresenter/providers/acpProvider.ts"
+ "src/main/presenter/llmProviderPresenter/providers/acpProvider.ts",
+ "src/main/presenter/sessionApplication/agentAssignmentCoordinator.ts",
+ "src/main/presenter/sessionApplication/lifecycleCoordinator.ts",
+ "src/main/presenter/sessionApplication/projectionCoordinator.ts",
+ "src/main/presenter/sessionApplication/turnCoordinator.ts"
],
"expectedFiles": {
"src/main/agent/acp/instance/acpAgentInstance.ts": true,
@@ -121,9 +124,12 @@
"src/main/presenter/agentRuntimePresenter/messageStore.ts": true,
"src/main/presenter/agentRuntimePresenter/process.ts": true,
"src/main/presenter/agentRuntimePresenter/tapeService.ts": true,
- "src/main/presenter/agentSessionPresenter/index.ts": true,
"src/main/presenter/index.ts": true,
- "src/main/presenter/llmProviderPresenter/providers/acpProvider.ts": true
+ "src/main/presenter/llmProviderPresenter/providers/acpProvider.ts": true,
+ "src/main/presenter/sessionApplication/agentAssignmentCoordinator.ts": true,
+ "src/main/presenter/sessionApplication/lifecycleCoordinator.ts": true,
+ "src/main/presenter/sessionApplication/projectionCoordinator.ts": true,
+ "src/main/presenter/sessionApplication/turnCoordinator.ts": true
},
"ownerEvidence": {
"agentManager": {
@@ -186,8 +192,23 @@
"exists": true,
"declarationCount": 1
},
- "retainedAgentSessionFacade": {
- "file": "src/main/presenter/agentSessionPresenter/index.ts",
+ "sessionProjectionCoordinator": {
+ "file": "src/main/presenter/sessionApplication/projectionCoordinator.ts",
+ "exists": true,
+ "declarationCount": 1
+ },
+ "sessionAgentAssignmentCoordinator": {
+ "file": "src/main/presenter/sessionApplication/agentAssignmentCoordinator.ts",
+ "exists": true,
+ "declarationCount": 1
+ },
+ "sessionTurnCoordinator": {
+ "file": "src/main/presenter/sessionApplication/turnCoordinator.ts",
+ "exists": true,
+ "declarationCount": 1
+ },
+ "sessionLifecycleCoordinator": {
+ "file": "src/main/presenter/sessionApplication/lifecycleCoordinator.ts",
"exists": true,
"declarationCount": 1
},
@@ -201,10 +222,13 @@
"paths": {
"src/main/agent/manager/legacyAgentBackends.ts": 0,
"src/main/lib/agentRuntime": 0,
- "src/main/presenter/agentSessionPresenter/agentRegistry.ts": 0
+ "src/main/presenter/agentSessionPresenter": 0,
+ "src/shared/types/presenters/agent-session.presenter.d.ts": 0
},
"symbols": {
"AgentRegistry": 0,
+ "AgentSessionPresenter": 0,
+ "IAgentSessionPresenter": 0,
"IAgentImplementation": 0,
"createLegacyAgentBackend": 0,
"LegacyDeepChatSessionBackend": 0,
@@ -238,9 +262,12 @@
"src/main/presenter/agentRuntimePresenter/messageStore.ts",
"src/main/presenter/agentRuntimePresenter/process.ts",
"src/main/presenter/agentRuntimePresenter/tapeService.ts",
- "src/main/presenter/agentSessionPresenter/index.ts",
"src/main/presenter/index.ts",
- "src/main/presenter/llmProviderPresenter/providers/acpProvider.ts"
+ "src/main/presenter/llmProviderPresenter/providers/acpProvider.ts",
+ "src/main/presenter/sessionApplication/agentAssignmentCoordinator.ts",
+ "src/main/presenter/sessionApplication/lifecycleCoordinator.ts",
+ "src/main/presenter/sessionApplication/projectionCoordinator.ts",
+ "src/main/presenter/sessionApplication/turnCoordinator.ts"
]
},
"contracts": {
@@ -407,7 +434,7 @@
"providers",
"settings_activity"
],
- "sha256": "ca9306d19cb9066d6447268aac2e9485ca867fc65f89abda5a1e4b54d4f21974"
+ "sha256": "16882d6b4c22ec84e0e28c4eb7f5aeb7a867503648e67eeb42461f4f838fe198"
},
"memoryDuckDbSidecar": {
"files": [
@@ -428,7 +455,7 @@
"src/main/presenter/lifecyclePresenter/hooks/beforeQuit/presenterDestroyHook.ts",
"src/main/presenter/lifecyclePresenter/index.ts"
],
- "sha256": "90836b4d0ef4078293ac7d001ef2992f572809a10c67d35fdd8a972e8a66fb86"
+ "sha256": "d42c0862ef9e41fa77cb9296dcb625f5559b5ae594bd1fbba3ba248b2a4e5203"
},
"dependencyMetrics": {
"loopFiles": [
diff --git a/docs/architecture/baselines/archive-reference-report.md b/docs/architecture/baselines/archive-reference-report.md
index ec38a8e70..e84a257fc 100644
--- a/docs/architecture/baselines/archive-reference-report.md
+++ b/docs/architecture/baselines/archive-reference-report.md
@@ -1,5 +1,5 @@
# Archive Reference Baseline
-Generated on 2026-07-13.
+Generated on 2026-07-14.
- Total references: 0
diff --git a/docs/architecture/baselines/dependency-report.md b/docs/architecture/baselines/dependency-report.md
index 22dc3f6b8..c0092a7b9 100644
--- a/docs/architecture/baselines/dependency-report.md
+++ b/docs/architecture/baselines/dependency-report.md
@@ -1,20 +1,20 @@
# Dependency Baseline
-Generated on 2026-07-13.
+Generated on 2026-07-14.
## main
-- Total files: 563
-- Internal dependency edges: 1526
+- Total files: 565
+- Internal dependency edges: 1537
- Cycles detected: 36
### Top outgoing dependencies
-- `presenter/index.ts`: 70
-- `presenter/agentRuntimePresenter/index.ts`: 45
+- `presenter/index.ts`: 69
+- `presenter/agentRuntimePresenter/index.ts`: 48
- `presenter/sqlitePresenter/index.ts`: 40
- `presenter/sqlitePresenter/schemaCatalog.ts`: 37
-- `routes/index.ts`: 34
+- `routes/index.ts`: 36
- `presenter/configPresenter/index.ts`: 27
- `presenter/lifecyclePresenter/hooks/index.ts`: 23
- `presenter/toolPresenter/agentTools/agentToolManager.ts`: 22
@@ -40,7 +40,7 @@ Generated on 2026-07-13.
- `presenter/sqlitePresenter/index.ts`: 21
- `presenter/memoryPresenter/ports.ts`: 20
- `presenter/remoteControlPresenter/services/remoteConversationRunner.ts`: 16
-- `presenter/memoryPresenter/domain/types.ts`: 14
+- `presenter/memoryPresenter/domain/types.ts`: 15
- `presenter/filePresenter/BaseFileAdapter.ts`: 13
- `presenter/memoryPresenter/context.ts`: 12
diff --git a/docs/architecture/baselines/main-kernel-boundary-baseline.md b/docs/architecture/baselines/main-kernel-boundary-baseline.md
index 36bf710cd..46e38738d 100644
--- a/docs/architecture/baselines/main-kernel-boundary-baseline.md
+++ b/docs/architecture/baselines/main-kernel-boundary-baseline.md
@@ -1,6 +1,6 @@
# Main Kernel Boundary Baseline
-Generated on 2026-07-13.
+Generated on 2026-07-14.
Current phase: P5.
## Metric Snapshot
@@ -17,8 +17,8 @@ Current phase: P5.
| `renderer.business.windowApi.count` | 0 |
| `renderer.quarantine.windowApi.count` | 0 |
| `renderer.quarantine.sourceFile.count` | 0 |
-| `hotpath.presenterEdge.count` | 9 |
-| `runtime.rawTimer.count` | 206 |
+| `hotpath.presenterEdge.count` | 8 |
+| `runtime.rawTimer.count` | 211 |
| `migrated.rawChannel.count` | 0 |
| `bridge.active.count` | 0 |
| `bridge.expired.count` | 0 |
@@ -55,12 +55,11 @@ Current phase: P5.
## Hot Path Direct Dependencies
-- Direct edge count: 9
+- Direct edge count: 8
- `src/main/presenter/agentRuntimePresenter/index.ts -> src/main/eventbus.ts`
- `src/main/presenter/index.ts -> src/main/eventbus.ts`
- `src/main/presenter/index.ts -> src/main/presenter/agentRuntimePresenter/index.ts`
-- `src/main/presenter/index.ts -> src/main/presenter/agentSessionPresenter/index.ts`
- `src/main/presenter/index.ts -> src/main/presenter/llmProviderPresenter/index.ts`
- `src/main/presenter/index.ts -> src/main/presenter/sessionPresenter/index.ts`
- `src/main/presenter/llmProviderPresenter/index.ts -> src/main/eventbus.ts`
@@ -87,7 +86,7 @@ Current phase: P5.
## Raw Timers
-- Total count: 206
+- Total count: 211
- `src/main/presenter/githubCopilotDeviceFlow.ts`: 6
- `src/main/presenter/browser/BrowserTab.ts`: 5
@@ -95,12 +94,12 @@ Current phase: P5.
- `src/main/presenter/llmProviderPresenter/aiSdk/runtime.ts`: 5
- `src/main/presenter/remoteControlPresenter/index.ts`: 5
- `src/renderer/src/pages/ChatPage.vue`: 5
+- `src/main/presenter/memoryPresenter/infra/vectorStoreManager.ts`: 4
- `src/main/presenter/memoryPresenter/services/maintenanceService.ts`: 4
- `src/renderer/src/components/message/MessageToolbar.vue`: 4
- `src/renderer/src/composables/message/useMessageScroll.ts`: 4
- `src/main/agent/acp/launch/acpInitHelper.ts`: 3
- `src/main/agent/shared/process/backgroundExecSessionManager.ts`: 3
-- `src/main/lib/fileWatcher/watcherHost.ts`: 3
## Migrated Path Raw Channel Literals
diff --git a/docs/architecture/baselines/main-kernel-migration-scoreboard.json b/docs/architecture/baselines/main-kernel-migration-scoreboard.json
index 237af9d2b..abb756d14 100644
--- a/docs/architecture/baselines/main-kernel-migration-scoreboard.json
+++ b/docs/architecture/baselines/main-kernel-migration-scoreboard.json
@@ -1,6 +1,6 @@
{
"program": "main-kernel-refactor",
- "generatedOn": "2026-07-13",
+ "generatedOn": "2026-07-14",
"currentPhase": "P5",
"metrics": {
"renderer.usePresenter.count": 0,
@@ -13,8 +13,8 @@
"renderer.business.windowApi.count": 0,
"renderer.quarantine.windowApi.count": 0,
"renderer.quarantine.sourceFile.count": 0,
- "hotpath.presenterEdge.count": 9,
- "runtime.rawTimer.count": 206,
+ "hotpath.presenterEdge.count": 8,
+ "runtime.rawTimer.count": 211,
"migrated.rawChannel.count": 0,
"bridge.active.count": 0,
"bridge.expired.count": 0
@@ -61,7 +61,6 @@
"src/main/presenter/agentRuntimePresenter/index.ts -> src/main/eventbus.ts",
"src/main/presenter/index.ts -> src/main/eventbus.ts",
"src/main/presenter/index.ts -> src/main/presenter/agentRuntimePresenter/index.ts",
- "src/main/presenter/index.ts -> src/main/presenter/agentSessionPresenter/index.ts",
"src/main/presenter/index.ts -> src/main/presenter/llmProviderPresenter/index.ts",
"src/main/presenter/index.ts -> src/main/presenter/sessionPresenter/index.ts",
"src/main/presenter/llmProviderPresenter/index.ts -> src/main/eventbus.ts",
diff --git a/docs/architecture/baselines/main-kernel-migration-scoreboard.md b/docs/architecture/baselines/main-kernel-migration-scoreboard.md
index 5e409c2de..25b0a98e3 100644
--- a/docs/architecture/baselines/main-kernel-migration-scoreboard.md
+++ b/docs/architecture/baselines/main-kernel-migration-scoreboard.md
@@ -1,6 +1,6 @@
# Main Kernel Migration Scoreboard
-Generated on 2026-07-13.
+Generated on 2026-07-14.
Current phase: P5.
Phase 0 establishes the comparison baseline. Later phases should update this report and compare against this checkpoint.
@@ -17,8 +17,8 @@ Phase 0 establishes the comparison baseline. Later phases should update this rep
| `renderer.business.windowApi.count` | 0 | baseline |
| `renderer.quarantine.windowApi.count` | 0 | baseline |
| `renderer.quarantine.sourceFile.count` | 0 | baseline |
-| `hotpath.presenterEdge.count` | 9 | baseline |
-| `runtime.rawTimer.count` | 206 | baseline |
+| `hotpath.presenterEdge.count` | 8 | baseline |
+| `runtime.rawTimer.count` | 211 | baseline |
| `migrated.rawChannel.count` | 0 | baseline |
| `bridge.active.count` | 0 | baseline |
| `bridge.expired.count` | 0 | baseline |
diff --git a/docs/architecture/baselines/zero-inbound-candidates.md b/docs/architecture/baselines/zero-inbound-candidates.md
index 8cfdd19d8..899b7bf73 100644
--- a/docs/architecture/baselines/zero-inbound-candidates.md
+++ b/docs/architecture/baselines/zero-inbound-candidates.md
@@ -1,6 +1,6 @@
# Zero Inbound Candidates
-Generated on 2026-07-13.
+Generated on 2026-07-14.
These files have no in-repo importers inside their scope and need manual classification before deletion.
diff --git a/docs/architecture/deepchat-vs-acp-agents/spec.md b/docs/architecture/deepchat-vs-acp-agents/spec.md
index 68e4fe111..1c7007cf0 100644
--- a/docs/architecture/deepchat-vs-acp-agents/spec.md
+++ b/docs/architecture/deepchat-vs-acp-agents/spec.md
@@ -56,7 +56,7 @@ Flow:
```text
Renderer send
- -> AgentSessionPresenter
+ -> SessionService / ChatService -> Lifecycle / Turn coordinator
-> AgentManager -> typed DeepChat handle
-> DeepChatAgentInstance preparation
-> create/register LoopRun
@@ -88,7 +88,7 @@ Flow:
```text
Renderer send
- -> AgentSessionPresenter
+ -> SessionService / ChatService -> Lifecycle / Turn coordinator
-> AgentManager -> direct ACP handle
-> validate descriptor/config/workdir identity
-> AcpAgentRuntime hydrate/prepare AcpAgentInstance
diff --git a/docs/architecture/event-system.md b/docs/architecture/event-system.md
index a003a92d6..a3dda8e1b 100644
--- a/docs/architecture/event-system.md
+++ b/docs/architecture/event-system.md
@@ -36,7 +36,7 @@ Current event families include:
| Family | Examples | Publisher owner |
| --- | --- | --- |
| `chat.*` | `chat.stream.updated`, `chat.stream.completed`, `chat.stream.failed`, `chat.plan.updated` | `agentRuntimePresenter`, `dispatch` |
-| `sessions.*` | `sessions.updated`, `sessions.status.changed`, `sessions.pendingInputs.changed` | `agentSessionPresenter`, runtime services |
+| `sessions.*` | `sessions.updated`, `sessions.status.changed`, `sessions.pendingInputs.changed` | session application coordinators, runtime services |
| `settings.*` | `settings.changed`, `settings.navigateRequested`, `settings.checkForUpdatesRequested` | config/settings/window flows |
| `config.*` | language, theme, system prompts, agents, shortcut keys | `configPresenter` helpers |
| `providers.*` and `models.*` | provider/model/rate-limit updates | provider runtime |
diff --git a/docs/architecture/history-read-hot-path/spec.md b/docs/architecture/history-read-hot-path/spec.md
index 63c84d7e9..189d9bb49 100644
--- a/docs/architecture/history-read-hot-path/spec.md
+++ b/docs/architecture/history-read-hot-path/spec.md
@@ -4,6 +4,9 @@
已实现。
+> Historical implementation note: the boolean history checks now belong to the Lifecycle and Turn
+> coordinators. `AgentSessionPresenter` and its main-process interface were retired in stage 3.
+
## 问题
发送消息前,`AgentSessionPresenter` 只需要知道会话里“有没有消息”,但当前会读取并组装
diff --git a/docs/architecture/retire-agent-session-presenter/plan.md b/docs/architecture/retire-agent-session-presenter/plan.md
new file mode 100644
index 000000000..d85bff39a
--- /dev/null
+++ b/docs/architecture/retire-agent-session-presenter/plan.md
@@ -0,0 +1,70 @@
+# Retire Agent Session Presenter - Implementation Plan
+
+## Approach
+
+This is a deletion migration, not another extraction:
+
+1. replace each facade call with the already-existing owner;
+2. remove the facade from the composition root and route runtime;
+3. delete the class and shared interface after repository-wide production references reach zero;
+4. redirect integration tests to the same coordinator instances;
+5. convert facade-preservation guards into retirement guards and regenerate architecture baselines.
+
+## Wiring Plan
+
+### Composition root
+
+- Remove the `IAgentSessionPresenter` import, `AgentSessionPresenter` import, public property, and
+ construction.
+- Use Projection for lookup, message, Tape, hook, floating-widget, and MCP consumers.
+- Use Lifecycle for subagent creation.
+- Use Turn for send and cancel.
+- Use AgentAssignment for permission/settings and subagent Tape operations.
+- Keep permission cleanup on the existing `SessionPermissionPort`.
+
+### Route runtime
+
+- Replace `agentSessionPresenter` with separate lifecycle, turn, assignment, and projection
+ dependencies.
+- Continue passing lifecycle/projection into SessionService and turn/projection into ChatService.
+- Dispatch direct route cases to the same four dependencies by the ownership table in `spec.md`.
+- Update route tests to provide four focused doubles and assert the owning port.
+
+### Remaining consumers
+
+- Floating widget: Projection list and activate.
+- MCP conversation search: Projection session/message reads.
+- MCP tool manager: Projection session lookup.
+- Hooks notification adapter: Projection session/message lookup.
+- Agent tool runtime and skill state adapter: direct coordinator properties captured by closures.
+
+### Tests
+
+- Delete tests whose only subject is forwarding.
+- Move retained cross-owner and runtime integration tests under `test/main/presenter/sessionApplication/`.
+- Replace aggregate test variables with explicit Lifecycle, Turn, AgentAssignment, and Projection
+ variables.
+- Preserve renderer tests because their local `agentSessionPresenter` names represent route clients,
+ not the retired main-process interface.
+
+### Guards and docs
+
+- Mark the facade directory and interface file as retired paths.
+- Reject retired symbols in `src/main`, `src/shared`, and `test/main`.
+- Remove facade ownership evidence and deleted files from architecture-baseline generation.
+- Update current session-management and stage-2 documentation to state that stage 3 is complete.
+- Verify baseline generation in an isolated output directory; regenerate canonical reports only
+ from a clean committed tree.
+
+## Validation
+
+Run in this order:
+
+1. repository-wide retired-symbol/path search;
+2. focused route, sessionApplication, floating, MCP, and composition tests;
+3. `pnpm run lint:architecture`;
+4. `pnpm run typecheck`;
+5. `pnpm run test:main` and `pnpm run test:renderer` (rerun any isolated timeout once to distinguish
+ contention from a functional failure);
+6. `pnpm run format`, `pnpm run i18n`, and `pnpm run lint`;
+7. final clean-worktree and diff review.
diff --git a/docs/architecture/retire-agent-session-presenter/spec.md b/docs/architecture/retire-agent-session-presenter/spec.md
new file mode 100644
index 000000000..64cdf4fa3
--- /dev/null
+++ b/docs/architecture/retire-agent-session-presenter/spec.md
@@ -0,0 +1,140 @@
+# Retire Agent Session Presenter
+
+> Status: implemented and validated
+> Base: `dev@750f229d7`
+> Branch: `task/retire-agent-session-presenter`
+
+## Context
+
+The session-boundary cleanup is complete through two stages:
+
+1. foreign capabilities moved to explicit history, import, migration, usage, RTK, export,
+ translation, and catalog owners;
+2. Lifecycle, Turn, AgentAssignment, and Projection behavior moved to four composition-owned
+ coordinators, while SessionService, ChatService, Remote, and Cron adopted narrow ports.
+
+`AgentSessionPresenter` now contains forwarding only. Its shared `IAgentSessionPresenter` contract is
+used only by the main process, but still makes the forwarding surface appear to be a domain owner.
+
+## Problem
+
+The compatibility facade creates four remaining architecture failures:
+
+1. the composition root constructs a fifth session object that owns no behavior;
+2. typed route dispatch retains one aggregate dependency alongside its existing narrow ports;
+3. Tool, MCP, floating-widget, hooks, and agent-runtime adapters reach session behavior through the
+ facade instead of the actual coordinator;
+4. `IPresenter` exports the main-only aggregate contract through shared types, allowing new callers
+ to couple to it again.
+
+## Goals
+
+1. Remove `src/main/presenter/agentSessionPresenter/` and the `AgentSessionPresenter` class.
+2. Remove the main-process `IAgentSessionPresenter` declaration, export, and `IPresenter` property.
+3. Wire the composition root directly to the existing Lifecycle, Turn, AgentAssignment, and
+ Projection coordinator instances.
+4. Give route runtime four separate session dependencies and route every command/read to its owner.
+5. Rewire remaining main-process consumers to the narrow coordinator they actually use.
+6. Preserve all route, event, IPC, renderer-client, persistence, and runtime behavior.
+7. Add architecture enforcement that keeps the retired facade, interface, path, and aggregate
+ dependency from returning.
+
+## Non-goals
+
+- Changing coordinator behavior, transaction ordering, DTOs, route names, preload APIs, or renderer
+ clients.
+- Moving state between AppSessionService, AgentManager, backend instances, transcript, Tape,
+ permission, skill, or UI owners.
+- Combining the four coordinators into a replacement `SessionApplicationServices`, facade, registry,
+ service container, or command bus.
+- Renaming renderer-side session clients or test doubles that do not reference the retired
+ main-process presenter contract.
+- GitHub issue synchronization.
+
+## Target Dependency Shape
+
+```text
+Presenter composition root
+ |- SessionLifecycleCoordinator
+ |- SessionTurnCoordinator
+ |- SessionAgentAssignmentCoordinator
+ `- SessionProjectionCoordinator
+
+main route runtime
+ |- lifecycle -> lifecycle routes + SessionService
+ |- turn -> turn routes + ChatService
+ |- assignment -> assignment/settings routes
+ `- projection -> read/window/Tape routes + SessionService + ChatService
+
+other main consumers
+ |- agent tool runtime -> lifecycle/turn/assignment/projection
+ |- hooks -> projection
+ |- floating widget -> projection
+ `- MCP tools -> projection
+```
+
+No object may re-export all four capability groups.
+
+## Ownership Mapping
+
+| Retired facade calls | Direct owner |
+| --- | --- |
+| create, detached create, subagent create, draft ensure, fork, delete | Lifecycle |
+| send, steer, pending input, message mutation, compaction, cancel, interaction | Turn |
+| transfer, runtime settings, ACP config/commands, subagent Tape merge/discard | AgentAssignment |
+| session/message/Tape reads, window activation, rename/pin, title/status events | Projection |
+| permission cleanup | existing `SessionPermissionPort` |
+
+## Boundary Rules
+
+1. Production and main-process tests must not import, construct, type, or access
+ `AgentSessionPresenter`, `IAgentSessionPresenter`, or `agentSessionPresenter`.
+2. The composition root constructs exactly one instance of each existing session coordinator.
+3. Route runtime exposes four separate session dependencies; it must not expose a combined session
+ application object.
+4. Required dependencies remain required. No optional method probes, fallback no-ops, setters, or
+ `as unknown as` wiring may replace the facade.
+5. Projection remains a singleton because it owns active-window bindings and status snapshots.
+6. Existing stage-1 owners remain independent and are not absorbed by any coordinator.
+7. Renderer-side route clients remain unchanged because the retired interface is main-process-only.
+
+## Compatibility Invariants
+
+- Every typed route keeps the same input parsing, output parsing, scheduler, timeout, and error
+ behavior.
+- SessionService and ChatService continue using their existing narrow ports and shared coordinator
+ instances.
+- Tool runtime session lookup, subagent creation, Tape operations, send, and cancellation preserve
+ their current return and failure behavior.
+- Hooks continue reading sessions/messages from the same Projection singleton.
+- Floating-widget list and activation behavior remains optional only with respect to app lifecycle,
+ not coordinator capabilities.
+- MCP conversation history and ACP conversation lookup retain existing missing-session and error
+ handling.
+- Remote and Cron wiring remains unchanged apart from removal of obsolete facade references.
+
+## Acceptance Criteria
+
+1. The `agentSessionPresenter` production and main-test directories are deleted.
+2. `AgentSessionPresenter`, `IAgentSessionPresenter`, and main-process `agentSessionPresenter`
+ references are zero.
+3. The shared presenter barrel and `IPresenter` no longer export the retired contract/property.
+4. The composition root constructs no forwarding facade and wires every former caller to one of the
+ four existing coordinators or `SessionPermissionPort`.
+5. Route runtime contains four separate session dependencies and no aggregate replacement.
+6. Existing integration coverage calls coordinators directly; forwarding-only tests are removed.
+7. Architecture guards reject retired paths/symbols and continue rejecting combined facades and
+ duplicate coordinator construction.
+8. Current architecture docs and baseline generation describe the post-retirement graph; isolated
+ generation succeeds, while canonical reports remain restricted to a clean committed tree.
+9. Formatting, i18n validation, lint, typecheck, main tests, renderer tests, and architecture guards
+ pass.
+
+## Risks
+
+- A mechanical route remap can send a method to the wrong coordinator while still type-checking if
+ test doubles are overly broad.
+- Reconstructing Projection per consumer would split active-window and status state.
+- Deleting facade tests without redirecting integration coverage would hide cross-owner regressions.
+- Generated architecture baselines can retain deleted edges unless their source configuration is
+ updated before regeneration.
diff --git a/docs/architecture/retire-agent-session-presenter/tasks.md b/docs/architecture/retire-agent-session-presenter/tasks.md
new file mode 100644
index 000000000..3e33df3bc
--- /dev/null
+++ b/docs/architecture/retire-agent-session-presenter/tasks.md
@@ -0,0 +1,42 @@
+# Retire Agent Session Presenter - Tasks
+
+## Specification
+
+- [x] Confirm the facade contains forwarding only.
+- [x] Inventory production, shared-type, test, guard, and baseline references.
+- [x] Define direct owner mapping and compatibility invariants.
+- [x] Resolve all clarification items.
+
+## Composition and consumers
+
+- [x] Remove facade construction/property/imports from the composition root.
+- [x] Rewire agent tool runtime, skill state, hooks, floating widget, and MCP consumers.
+- [x] Replace the route-runtime facade dependency with four separate owner dependencies.
+- [x] Keep SessionService, ChatService, Remote, and Cron on their existing narrow ports.
+
+## Retirement
+
+- [x] Delete `AgentSessionPresenter` and its production directory.
+- [x] Delete `IAgentSessionPresenter`, its barrel export, and the shared `IPresenter` property.
+- [x] Remove all main-process and main-test facade symbols.
+
+## Tests and enforcement
+
+- [x] Remove forwarding-only tests and relocate retained integration coverage.
+- [x] Update route/composition consumer tests to use explicit owners.
+- [x] Convert architecture checks and fixtures to enforce retirement.
+- [x] Update baseline generation and verify reports in an isolated output directory.
+
+## Documentation and validation
+
+- [x] Update maintained session architecture documentation.
+- [x] Run focused tests and architecture guards.
+- [x] Run full typecheck, main tests, and renderer tests.
+- [x] Run format, i18n validation, and lint.
+- [x] Review final diff and mark the SDD implemented.
+
+## Baseline note
+
+Canonical reports are generated only from a clean committed tree. The updated generator completed
+successfully against an isolated output directory; canonical regeneration belongs to the later
+commit/PR workflow.
diff --git a/docs/architecture/session-application-coordinators/plan.md b/docs/architecture/session-application-coordinators/plan.md
index 058c94049..10d38d012 100644
--- a/docs/architecture/session-application-coordinators/plan.md
+++ b/docs/architecture/session-application-coordinators/plan.md
@@ -1,5 +1,8 @@
# Session Application Coordinators — Implementation Plan
+> Historical stage-2 plan. Stage 3 subsequently removed the compatibility façade and connected the
+> remaining consumers directly to the four coordinators.
+
## Approach
The migration uses a strangler sequence inside the existing compatibility façade:
diff --git a/docs/architecture/session-application-coordinators/spec.md b/docs/architecture/session-application-coordinators/spec.md
index 3430e450f..819a0df22 100644
--- a/docs/architecture/session-application-coordinators/spec.md
+++ b/docs/architecture/session-application-coordinators/spec.md
@@ -4,6 +4,8 @@
> Original base: `dev@28e2a0e92`
> Integrated base: `dev@135779210` via merge commit `1122b2406`
> Branch: `task/session-application-coordinators`
+> Follow-up: stage 3 retired `AgentSessionPresenter` and `IAgentSessionPresenter`; current callers use
+> the four coordinator ports directly.
## Context
diff --git a/docs/architecture/session-application-coordinators/tasks.md b/docs/architecture/session-application-coordinators/tasks.md
index 19918c568..7ca130729 100644
--- a/docs/architecture/session-application-coordinators/tasks.md
+++ b/docs/architecture/session-application-coordinators/tasks.md
@@ -1,5 +1,8 @@
# Session Application Coordinators — Tasks
+> Historical stage-2 checklist. Presenter retirement is tracked in
+> `docs/architecture/retire-agent-session-presenter/`.
+
## SDD and Inventory
- [x] Audit Lifecycle, Turn, AgentAssignment, and Projection methods, state, dependencies, and tests.
diff --git a/docs/architecture/session-boundary-cleanup/spec.md b/docs/architecture/session-boundary-cleanup/spec.md
index 213b386a6..d78a01a51 100644
--- a/docs/architecture/session-boundary-cleanup/spec.md
+++ b/docs/architecture/session-boundary-cleanup/spec.md
@@ -1,5 +1,8 @@
# Session Boundary Cleanup
+> Historical stage-1 specification. Stages 2 and 3 later extracted the four session application
+> coordinators and retired `AgentSessionPresenter` / `IAgentSessionPresenter`.
+
> Status: implemented and merged in PR #1957
> Base: `dev@28e2a0e92`
> Branch: `codex/session-boundary-cleanup`
diff --git a/docs/architecture/session-management.md b/docs/architecture/session-management.md
index 60f9dc15e..81ee3f718 100644
--- a/docs/architecture/session-management.md
+++ b/docs/architecture/session-management.md
@@ -1,6 +1,6 @@
# 会话管理架构详解
-当前会话管理分成五个明确边界:
+当前会话管理分成四个明确边界:
- app-session shell:`AppSessionService` 管理 `new_sessions`、window binding 和 shared CRUD;
- application coordination:四个 `sessionApplication` coordinator 分别拥有 Lifecycle、Turn、
@@ -8,8 +8,9 @@
- agent execution routing:`AgentManager` 按 executable descriptor kind 返回 typed handle;
- route/application owners:typed session routes 直接组合 search、translation、export、usage、RTK 与 agent
catalog owner;
-- compatibility forwarding:`AgentSessionPresenter` 保留尚未退休的 public surface,并把 core session 方法
- 转发到四个 coordinator,不拥有 application behavior。
+
+`AgentSessionPresenter` 和 main-process `IAgentSessionPresenter` 已退休。Composition root 与 main route
+runtime 直接暴露四个分离的 coordinator port,不存在 aggregate session façade。
`SessionPresenter` 是旧 conversations/messages 的 compatibility/data façade,不在当前 agent execution
链路中。
@@ -19,7 +20,6 @@
| 组件 | 位置 | 当前职责 |
| --- | --- | --- |
| Session application coordinators | `src/main/presenter/sessionApplication/` | Lifecycle、Turn、AgentAssignment、Projection application invariants |
-| `AgentSessionPresenter` | `src/main/presenter/agentSessionPresenter/index.ts` | core session public compatibility forwarding;不拥有 application behavior |
| Session route owners | `src/main/routes/sessions/` | history search、translation 与 typed route orchestration |
| Session export | `src/main/presenter/exporter/agentSessionExporter.ts` | current agent-session export mapping and format dispatch |
| Startup/maintenance owners | `src/main/presenter/startupMigrations/`, `usageStatsService.ts` | legacy import、session-data migrations、usage backfill/dashboard;RTK 由其 runtime service 自有 |
@@ -127,9 +127,9 @@ ACP-provider source 只清 compatibility binding,不被误判成 direct ACP。
- exporter 的旧消息格式化。
当前 session create/send/cancel/tool interaction 从 `SessionService/ChatService -> sessionApplication
-coordinator -> AgentManager -> typed backend` 开始追踪。兼容调用可能先进入 `AgentSessionPresenter`,但其
-core session methods 只转发到同一 coordinator 实例。DeepChat state/loop 看 `agent/deepchat`,direct ACP
-看 `agent/acp/instance`。
+coordinator -> AgentManager -> typed backend` 开始追踪。Main route runtime、Tool、MCP、Floating 与 hooks
+也直接接到同一组 composition-owned coordinator。DeepChat state/loop 看 `agent/deepchat`,direct ACP 看
+`agent/acp/instance`。
旧聊天数据导入由 `presenter/startupMigrations/legacyChatImportService.ts` 拥有;当前 agent-session export
-由 `presenter/exporter/agentSessionExporter.ts` 拥有。两者都不属于 `AgentSessionPresenter`。
+由 `presenter/exporter/agentSessionExporter.ts` 拥有。两者都不属于 session application coordinator。
diff --git a/docs/architecture/tool-system.md b/docs/architecture/tool-system.md
index cf480fc3f..9eaf136d2 100644
--- a/docs/architecture/tool-system.md
+++ b/docs/architecture/tool-system.md
@@ -100,7 +100,7 @@ port 负责提供:
- conversation workdir 解析
- 已批准路径查询
- settings approval 消费
-- `agentSessionPresenter` 会话上下文桥接
+- Lifecycle / Turn / AgentAssignment / Projection session ports
## FFF Search
diff --git a/scripts/agent-cleanup-guard.mjs b/scripts/agent-cleanup-guard.mjs
index 078fdb6f2..a77daca21 100644
--- a/scripts/agent-cleanup-guard.mjs
+++ b/scripts/agent-cleanup-guard.mjs
@@ -23,7 +23,6 @@ const LEGACY_MAIN_DIRS = [
const PRIMARY_MAIN_GUARD_PATHS = [
path.join(ROOT, 'src/main/agent'),
- path.join(ROOT, 'src/main/presenter/agentSessionPresenter'),
path.join(ROOT, 'src/main/presenter/agentRuntimePresenter'),
path.join(ROOT, 'src/main/presenter/skillPresenter'),
path.join(ROOT, 'src/main/presenter/mcpPresenter/toolManager.ts'),
@@ -156,7 +155,6 @@ function buildViolation(kind, filePath, specifier) {
async function findViolations() {
const scanRoots = [
path.join(ROOT, 'src/main/agent'),
- path.join(ROOT, 'src/main/presenter/agentSessionPresenter'),
path.join(ROOT, 'src/main/presenter/agentRuntimePresenter'),
path.join(ROOT, 'src/main/presenter/skillPresenter'),
path.join(ROOT, 'src/main/presenter/mcpPresenter/toolManager.ts'),
diff --git a/scripts/architecture-guard.mjs b/scripts/architecture-guard.mjs
index 64a852ae6..c48bbae2e 100644
--- a/scripts/architecture-guard.mjs
+++ b/scripts/architecture-guard.mjs
@@ -19,17 +19,14 @@ const SOURCE_EXTENSIONS = new Set([
])
const MAIN_GUARD_PATHS = [
- path.join(ROOT, 'src/main/presenter/agentSessionPresenter'),
path.join(ROOT, 'src/main/presenter/agentRuntimePresenter'),
path.join(ROOT, 'src/main/agent')
]
const REGULAR_MAIN_TEST_ROOT = path.join(ROOT, 'test/main')
const INTERNAL_AGENT_KIND_ROOTS = [
path.join(ROOT, 'src/main/agent'),
- path.join(ROOT, 'src/main/presenter/agentSessionPresenter'),
path.join(ROOT, 'src/main/presenter/agentRuntimePresenter'),
path.join(ROOT, 'test/main/agent'),
- path.join(ROOT, 'test/main/presenter/agentSessionPresenter'),
path.join(ROOT, 'test/main/presenter/agentRuntimePresenter')
]
const AGENT_HANDLE_BACKEND_RUNTIME_KIND_ROOTS = [
@@ -49,8 +46,16 @@ const RETIRED_RENDERER_LEGACY_ENTRY_PATHS = [
]
const RETIRED_MAIN_PATHS = [
path.join(ROOT, 'src/main/lib/agentRuntime'),
- path.join(ROOT, 'src/main/agent/manager/legacyAgentBackends.ts')
+ path.join(ROOT, 'src/main/agent/manager/legacyAgentBackends.ts'),
+ path.join(ROOT, 'src/main/presenter/agentSessionPresenter'),
+ path.join(ROOT, 'src/shared/types/presenters/agent-session.presenter.d.ts'),
+ path.join(ROOT, 'test/main/presenter/agentSessionPresenter')
]
+const RETIRED_SESSION_FACADE_NAMES = new Set([
+ 'AgentSessionPresenter',
+ 'IAgentSessionPresenter',
+ 'agentSessionPresenter'
+])
const RENDERER_TYPED_BOUNDARY_WINDOW_API_ALLOWLIST = [
path.join(ROOT, 'src/renderer/api/runtime.ts')
]
@@ -127,14 +132,6 @@ const SESSION_FACADE_CAPABILITY_CATEGORIES = new Map([
])
const SESSION_PHASE_ONE_FOREIGN_IMPORT_PATTERN =
/(?:^|[/_.-])(?:history|export(?:er)?|usage(?:[-_.]?stats?)?|rtk|legacy[-_.]?import(?:er|s)?|import(?:er|s)?|migrations?|translation|catalog)(?:$|[/_.-])/
-const AGENT_SESSION_PRESENTER_PATH = path.join(
- ROOT,
- 'src/main/presenter/agentSessionPresenter/index.ts'
-)
-const AGENT_SESSION_PRESENTER_INTERFACE_PATH = path.join(
- ROOT,
- 'src/shared/types/presenters/agent-session.presenter.d.ts'
-)
const SESSION_BOUNDARY_STARTUP_HOOK_PATHS = new Set(
[
'disabledSearchToolCleanupHook.ts',
@@ -146,37 +143,6 @@ const SESSION_BOUNDARY_STARTUP_HOOK_PATHS = new Set(
path.join(ROOT, 'src/main/presenter/lifecyclePresenter/hooks/after-start', fileName)
)
)
-const REMOVED_AGENT_SESSION_CAPABILITY_METHODS = new Set([
- 'searchHistory',
- 'getLegacyImportStatus',
- 'retryLegacyImport',
- 'startLegacyImport',
- 'startLegacyImportTask',
- 'startUsageStatsBackfill',
- 'startUsageStatsBackfillTask',
- 'startMainlineNormalizationBackfill',
- 'startMainlineNormalizationBackfillTask',
- 'startDisabledSearchToolCleanupBackfill',
- 'startDisabledSearchToolCleanupBackfillTask',
- 'startRtkHealthCheck',
- 'startRtkHealthCheckTask',
- 'retryRtkHealthCheck',
- 'getUsageDashboard',
- 'repairImportedLegacySessionSkills',
- 'translateText',
- 'getAgents',
- 'exportSession'
-])
-const AGENT_SESSION_FORBIDDEN_IMPORTS = [
- ['legacy import', /(?:^|\/)legacy(?:Chat)?ImportService$/],
- ['startup migrations', /(?:^|\/)startupMigrations(?:\/|$)/],
- ['usage owner or policy', /(?:^|\/)usageStats(?:Service)?$/],
- ['RTK runtime', /(?:^|\/)rtkRuntimeService$/],
- ['exporter formats', /(?:^|\/)exporter\/formats(?:\/|$)/],
- ['history search', /(?:^|\/)sessionHistorySearch$/],
- ['session translation', /(?:^|\/)sessionTranslation$/],
- ['agent catalog', /(?:^|\/)availableAgentCatalog$/]
-]
const PHASE_ORDER = new Map([
['P0', 0],
['P1', 1],
@@ -210,7 +176,6 @@ const MIGRATED_RAW_CHANNEL_GUARD_PATHS = [
path.join(ROOT, 'src/renderer/settings'),
path.join(ROOT, 'src/main/presenter/windowPresenter'),
path.join(ROOT, 'src/main/presenter/configPresenter'),
- path.join(ROOT, 'src/main/presenter/agentSessionPresenter'),
path.join(ROOT, 'src/main/presenter/agentRuntimePresenter'),
path.join(ROOT, 'src/main/presenter/sessionPresenter'),
path.join(ROOT, 'src/main/presenter/llmProviderPresenter'),
@@ -227,7 +192,6 @@ const MIGRATED_RAW_CHANNEL_BASELINE = new Map()
const HOT_PATH_FILES = [
path.join(ROOT, 'src/main/presenter/index.ts'),
path.join(ROOT, 'src/main/eventbus.ts'),
- path.join(ROOT, 'src/main/presenter/agentSessionPresenter/index.ts'),
path.join(ROOT, 'src/main/presenter/agentRuntimePresenter/index.ts'),
path.join(ROOT, 'src/main/presenter/llmProviderPresenter/index.ts'),
path.join(ROOT, 'src/main/presenter/sessionPresenter/index.ts')
@@ -832,25 +796,6 @@ function findNamedClassDeclarations(sourceFile, className) {
return declarations
}
-function findNamedDeclarationMembers(source, filePath, declarationName) {
- const sourceFile = sourceFileForAst(source, filePath)
- const names = []
- const visit = (node) => {
- if (
- (ts.isClassDeclaration(node) || ts.isInterfaceDeclaration(node)) &&
- node.name?.text === declarationName
- ) {
- for (const member of node.members) {
- const name = member.name ? propertyNameText(member.name) : null
- if (name) names.push(name)
- }
- }
- ts.forEachChild(node, visit)
- }
- visit(sourceFile)
- return names
-}
-
function analyzeSessionBoundaryStartupHook(source, filePath) {
const sourceFile = sourceFileForAst(source, filePath)
let agentSessionPresenterDependencies = 0
@@ -1604,7 +1549,10 @@ export async function runArchitectureGuard({ virtualFiles = new Map(), memoryCom
}
for (const retiredPath of RETIRED_MAIN_PATHS) {
- if (await pathExists(retiredPath)) {
+ const virtualPathExists = [...normalizedVirtualFiles.keys()].some((filePath) =>
+ isUnder(filePath, retiredPath)
+ )
+ if ((await pathExists(retiredPath)) || virtualPathExists) {
violations.push(`[main-retired-path] ${relativePath(retiredPath)} must remain deleted`)
}
}
@@ -1613,9 +1561,25 @@ export async function runArchitectureGuard({ virtualFiles = new Map(), memoryCom
const source = await readSource(filePath)
const specifiers = extractModuleSpecifiers(source)
const isMainSource = isUnder(filePath, MAIN_SOURCE_ROOT)
+ const scansRetiredSessionFacade =
+ isMainSource ||
+ isUnder(filePath, SHARED_SOURCE_ROOT) ||
+ isUnder(filePath, REGULAR_MAIN_TEST_ROOT)
+ const sourceFile = scansRetiredSessionFacade ? sourceFileForAst(source, filePath) : null
+
+ if (scansRetiredSessionFacade) {
+ const retiredFacadeNames = findIdentifierNames(
+ sourceFile,
+ RETIRED_SESSION_FACADE_NAMES
+ )
+ for (const name of retiredFacadeNames) {
+ violations.push(
+ `[session-retired-facade-symbol] ${relativePath(filePath)} must not reference ${name}`
+ )
+ }
+ }
if (isMainSource) {
- const sourceFile = sourceFileForAst(source, filePath)
const importRecords = importRecordsFromSourceFile(sourceFile)
if (isSessionMigratedConsumerPath(filePath)) {
@@ -1711,42 +1675,6 @@ export async function runArchitectureGuard({ virtualFiles = new Map(), memoryCom
}
}
- if (path.resolve(filePath) === path.resolve(AGENT_SESSION_PRESENTER_PATH)) {
- const removedMethods = findNamedDeclarationMembers(
- source,
- filePath,
- 'AgentSessionPresenter'
- ).filter((name) => REMOVED_AGENT_SESSION_CAPABILITY_METHODS.has(name))
- if (removedMethods.length > 0) {
- violations.push(
- `[session-boundary-presenter-method] ${relativePath(filePath)} must not declare moved capabilities: ${removedMethods.join(', ')}`
- )
- }
-
- for (const specifier of specifiers) {
- for (const [owner, pattern] of AGENT_SESSION_FORBIDDEN_IMPORTS) {
- if (pattern.test(specifier)) {
- violations.push(
- `[session-boundary-presenter-import] ${relativePath(filePath)} must not import ${owner}: ${specifier}`
- )
- }
- }
- }
- }
-
- if (path.resolve(filePath) === path.resolve(AGENT_SESSION_PRESENTER_INTERFACE_PATH)) {
- const removedMethods = findNamedDeclarationMembers(
- source,
- filePath,
- 'IAgentSessionPresenter'
- ).filter((name) => REMOVED_AGENT_SESSION_CAPABILITY_METHODS.has(name))
- if (removedMethods.length > 0) {
- violations.push(
- `[session-boundary-interface-method] ${relativePath(filePath)} must not declare moved capabilities: ${removedMethods.join(', ')}`
- )
- }
- }
-
if (SESSION_BOUNDARY_STARTUP_HOOK_PATHS.has(path.resolve(filePath))) {
const hook = analyzeSessionBoundaryStartupHook(source, filePath)
if (hook.agentSessionPresenterDependencies > 0) {
@@ -1803,7 +1731,7 @@ export async function runArchitectureGuard({ virtualFiles = new Map(), memoryCom
if (isUnder(filePath, MAIN_SOURCE_ROOT)) {
if (source.includes('MemoryRuntimeCoordinator')) {
const coordinatorClasses = findNamedClassDeclarations(
- sourceFileForAst(source, filePath),
+ sourceFile,
'MemoryRuntimeCoordinator'
)
memoryCoordinatorOwners.push(
diff --git a/scripts/generate-architecture-baseline.mjs b/scripts/generate-architecture-baseline.mjs
index f6f4a85fc..a300a807b 100644
--- a/scripts/generate-architecture-baseline.mjs
+++ b/scripts/generate-architecture-baseline.mjs
@@ -17,7 +17,10 @@ const AGENT_SYSTEM_SOURCE_ROOTS = [
]
const AGENT_SYSTEM_PRESENTER_BOUNDARY_FILES = [
'src/main/presenter/index.ts',
- 'src/main/presenter/agentSessionPresenter/index.ts',
+ 'src/main/presenter/sessionApplication/projectionCoordinator.ts',
+ 'src/main/presenter/sessionApplication/agentAssignmentCoordinator.ts',
+ 'src/main/presenter/sessionApplication/turnCoordinator.ts',
+ 'src/main/presenter/sessionApplication/lifecycleCoordinator.ts',
'src/main/presenter/agentRuntimePresenter/index.ts',
'src/main/presenter/agentRuntimePresenter/process.ts',
'src/main/presenter/agentRuntimePresenter/dispatch.ts',
@@ -98,9 +101,24 @@ const AGENT_SYSTEM_OWNER_EVIDENCE = [
/\bclass AcpAgentInstance\b/g
],
[
- 'retainedAgentSessionFacade',
- 'src/main/presenter/agentSessionPresenter/index.ts',
- /\bclass AgentSessionPresenter\b/g
+ 'sessionProjectionCoordinator',
+ 'src/main/presenter/sessionApplication/projectionCoordinator.ts',
+ /\bclass SessionProjectionCoordinator\b/g
+ ],
+ [
+ 'sessionAgentAssignmentCoordinator',
+ 'src/main/presenter/sessionApplication/agentAssignmentCoordinator.ts',
+ /\bclass SessionAgentAssignmentCoordinator\b/g
+ ],
+ [
+ 'sessionTurnCoordinator',
+ 'src/main/presenter/sessionApplication/turnCoordinator.ts',
+ /\bclass SessionTurnCoordinator\b/g
+ ],
+ [
+ 'sessionLifecycleCoordinator',
+ 'src/main/presenter/sessionApplication/lifecycleCoordinator.ts',
+ /\bclass SessionLifecycleCoordinator\b/g
],
[
'retainedDeepChatStateDelegateFacade',
@@ -111,10 +129,13 @@ const AGENT_SYSTEM_OWNER_EVIDENCE = [
const AGENT_SYSTEM_RETIRED_PATHS = [
'src/main/agent/manager/legacyAgentBackends.ts',
'src/main/lib/agentRuntime',
- 'src/main/presenter/agentSessionPresenter/agentRegistry.ts'
+ 'src/main/presenter/agentSessionPresenter',
+ 'src/shared/types/presenters/agent-session.presenter.d.ts'
]
const AGENT_SYSTEM_RETIRED_SYMBOL_PATTERNS = [
['AgentRegistry', /\bAgentRegistry\b/g],
+ ['AgentSessionPresenter', /\bAgentSessionPresenter\b/g],
+ ['IAgentSessionPresenter', /\bIAgentSessionPresenter\b/g],
['IAgentImplementation', /\bIAgentImplementation\b/g],
['createLegacyAgentBackend', /\bcreateLegacyAgentBackend\b/g],
['LegacyDeepChatSessionBackend', /\bLegacyDeepChatSessionBackend\b/g],
@@ -185,7 +206,6 @@ const BRIDGE_REGISTER_PATH = path.join(
const HOT_PATH_FILES = [
path.join(ROOT, 'src/main/presenter/index.ts'),
path.join(ROOT, 'src/main/eventbus.ts'),
- path.join(ROOT, 'src/main/presenter/agentSessionPresenter/index.ts'),
path.join(ROOT, 'src/main/presenter/agentRuntimePresenter/index.ts'),
path.join(ROOT, 'src/main/presenter/llmProviderPresenter/index.ts'),
path.join(ROOT, 'src/main/presenter/sessionPresenter/index.ts')
@@ -203,7 +223,6 @@ const MIGRATED_RAW_CHANNEL_GUARD_PATHS = [
path.join(ROOT, 'src/renderer/src/pages/NewThreadPage.vue'),
path.join(ROOT, 'src/main/presenter/windowPresenter'),
path.join(ROOT, 'src/main/presenter/configPresenter'),
- path.join(ROOT, 'src/main/presenter/agentSessionPresenter'),
path.join(ROOT, 'src/main/presenter/agentRuntimePresenter'),
path.join(ROOT, 'src/main/presenter/sessionPresenter'),
path.join(ROOT, 'src/main/presenter/llmProviderPresenter'),
diff --git a/src/main/presenter/agentSessionPresenter/index.ts b/src/main/presenter/agentSessionPresenter/index.ts
deleted file mode 100644
index 7ef479931..000000000
--- a/src/main/presenter/agentSessionPresenter/index.ts
+++ /dev/null
@@ -1,427 +0,0 @@
-import type {
- AgentTapeAnchorResult,
- AgentTapeAnchorsOptions,
- AgentTapeContextOptions,
- AgentTapeContextResult,
- AgentTapeInfo,
- AgentTapeSearchOptions,
- AgentTapeSearchResult,
- AgentTransferImpact,
- ChatMessagePageResult,
- ChatMessageRecord,
- CreateDetachedSessionInput,
- CreateSessionInput,
- MessagePageCursor,
- MessageStartResult,
- MessageTraceRecord,
- PermissionMode,
- SendMessageInput,
- SessionCompactionState,
- SessionGenerationSettings,
- SessionLightweightListResult,
- SessionListItem,
- SessionPageCursor,
- SessionWithState,
- ToolInteractionResponse,
- ToolInteractionResult
-} from '@shared/types/agent-interface'
-import type { SearchResult } from '@shared/types/core/search'
-import type { DeepChatTapeViewManifestRecord } from '@shared/types/tape-view-manifest'
-import type {
- DeepChatTapeReplayExportOptions,
- DeepChatTapeReplaySlice
-} from '@shared/types/tape-replay'
-import type { AcpConfigState } from '@shared/presenter'
-import { SessionProjectionCoordinator } from '../sessionApplication/projectionCoordinator'
-import type {
- SessionAgentAssignmentPort,
- SessionLifecyclePort,
- SessionLifecycleSubagentInput,
- SessionTurnPort
-} from '../sessionApplication/ports'
-import type { SessionPermissionPort } from '../runtimePorts'
-
-export class AgentSessionPresenter {
- private sessionProjection: SessionProjectionCoordinator
- private sessionLifecycle: SessionLifecyclePort
- private sessionAssignment: SessionAgentAssignmentPort
- private sessionTurn: SessionTurnPort
- private sessionPermissionPort?: SessionPermissionPort
-
- constructor(
- sessionProjection: SessionProjectionCoordinator,
- sessionLifecycle: SessionLifecyclePort,
- sessionAssignment: SessionAgentAssignmentPort,
- sessionTurn: SessionTurnPort,
- runtimePorts?: {
- sessionPermissionPort?: SessionPermissionPort
- }
- ) {
- this.sessionProjection = sessionProjection
- this.sessionLifecycle = sessionLifecycle
- this.sessionAssignment = sessionAssignment
- this.sessionTurn = sessionTurn
- this.sessionPermissionPort = runtimePorts?.sessionPermissionPort
- }
-
- // ---- IPC-facing methods ----
-
- async createSession(input: CreateSessionInput, webContentsId: number): Promise {
- return await this.sessionLifecycle.createSession(input, webContentsId)
- }
-
- async createDetachedSession(input: CreateDetachedSessionInput): Promise {
- return await this.sessionLifecycle.createDetachedSession(input)
- }
-
- async createSubagentSession(input: SessionLifecycleSubagentInput): Promise {
- return await this.sessionLifecycle.createSubagentSession(input)
- }
-
- async ensureAcpDraftSession(input: {
- agentId: string
- projectDir: string
- permissionMode?: PermissionMode
- }): Promise {
- return await this.sessionLifecycle.ensureAcpDraftSession(input)
- }
-
- async sendMessage(
- sessionId: string,
- content: string | SendMessageInput,
- options?: { maxProviderRounds?: number }
- ): Promise {
- return await this.sessionTurn.sendMessage(sessionId, content, options)
- }
-
- async steerActiveTurn(sessionId: string, content: string | SendMessageInput): Promise {
- await this.sessionTurn.steerActiveTurn(sessionId, content)
- }
-
- async listPendingInputs(sessionId: string) {
- return await this.sessionTurn.listPendingInputs(sessionId)
- }
-
- async queuePendingInput(sessionId: string, content: string | SendMessageInput) {
- return await this.sessionTurn.queuePendingInput(sessionId, content)
- }
-
- async updateQueuedInput(sessionId: string, itemId: string, content: string | SendMessageInput) {
- return await this.sessionTurn.updateQueuedInput(sessionId, itemId, content)
- }
-
- async moveQueuedInput(sessionId: string, itemId: string, toIndex: number) {
- return await this.sessionTurn.moveQueuedInput(sessionId, itemId, toIndex)
- }
-
- async convertPendingInputToSteer(sessionId: string, itemId: string) {
- return await this.sessionTurn.convertPendingInputToSteer(sessionId, itemId)
- }
-
- async steerPendingInput(sessionId: string, itemId: string) {
- return await this.sessionTurn.steerPendingInput(sessionId, itemId)
- }
-
- async deletePendingInput(sessionId: string, itemId: string): Promise {
- await this.sessionTurn.deletePendingInput(sessionId, itemId)
- }
-
- async retryMessage(sessionId: string, messageId: string): Promise {
- await this.sessionTurn.retryMessage(sessionId, messageId)
- }
-
- async deleteMessage(sessionId: string, messageId: string): Promise {
- await this.sessionTurn.deleteMessage(sessionId, messageId)
- }
-
- async editUserMessage(
- sessionId: string,
- messageId: string,
- text: string
- ): Promise {
- return await this.sessionTurn.editUserMessage(sessionId, messageId, text)
- }
-
- async forkSession(
- sourceSessionId: string,
- targetMessageId: string,
- newTitle?: string
- ): Promise {
- return await this.sessionLifecycle.forkSession(sourceSessionId, targetMessageId, newTitle)
- }
-
- async getSessionList(filters?: {
- agentId?: string
- projectDir?: string
- includeSubagents?: boolean
- parentSessionId?: string
- }): Promise {
- return await this.sessionProjection.listSessions(filters)
- }
-
- async getLightweightSessionList(options?: {
- limit?: number
- cursor?: SessionPageCursor | null
- includeSubagents?: boolean
- agentId?: string
- prioritizeSessionId?: string
- }): Promise {
- return await this.sessionProjection.listLightweight(options)
- }
-
- async getLightweightSessionsByIds(sessionIds: string[]): Promise {
- return await this.sessionProjection.getLightweightByIds(sessionIds)
- }
-
- async getSession(sessionId: string): Promise {
- return await this.sessionProjection.getSession(sessionId)
- }
-
- async getMessages(sessionId: string): Promise {
- return await this.sessionProjection.getMessages(sessionId)
- }
-
- async listMessagesPage(
- sessionId: string,
- options?: {
- limit?: number
- cursor?: MessagePageCursor | null
- }
- ): Promise {
- return await this.sessionProjection.listMessagesPage(sessionId, options)
- }
-
- async getSessionCompactionState(sessionId: string): Promise {
- return await this.sessionTurn.getSessionCompactionState(sessionId)
- }
-
- async compactSession(
- sessionId: string
- ): Promise<{ compacted: boolean; state: SessionCompactionState }> {
- return await this.sessionTurn.compactSession(sessionId)
- }
-
- async getTapeInfo(sessionId: string): Promise {
- return await this.sessionProjection.getTapeInfo(sessionId)
- }
-
- async searchTape(
- sessionId: string,
- query: string,
- options?: AgentTapeSearchOptions
- ): Promise {
- return await this.sessionProjection.searchTape(sessionId, query, options)
- }
-
- async getTapeContext(
- sessionId: string,
- entryIds: number[],
- options?: AgentTapeContextOptions
- ): Promise {
- return await this.sessionProjection.getTapeContext(sessionId, entryIds, options)
- }
-
- async listTapeAnchors(
- sessionId: string,
- options?: AgentTapeAnchorsOptions
- ): Promise {
- return await this.sessionProjection.listTapeAnchors(sessionId, options)
- }
-
- async handoffTape(
- sessionId: string,
- name: string,
- state: Record = {}
- ): Promise {
- return await this.sessionProjection.handoffTape(sessionId, name, state)
- }
-
- async listMessageViewManifests(messageId: string): Promise {
- return await this.sessionProjection.listMessageViewManifests(messageId)
- }
-
- async exportMessageTapeReplaySlice(
- messageId: string,
- options?: DeepChatTapeReplayExportOptions
- ): Promise {
- return await this.sessionProjection.exportMessageTapeReplaySlice(messageId, options)
- }
-
- async mergeSubagentTape(
- parentSessionId: string,
- childSessionId: string,
- meta: Record = {}
- ): Promise {
- await this.sessionAssignment.mergeSubagentTape(parentSessionId, childSessionId, meta)
- }
-
- async discardSubagentTape(
- parentSessionId: string,
- childSessionId: string,
- meta: Record = {}
- ): Promise {
- await this.sessionAssignment.discardSubagentTape(parentSessionId, childSessionId, meta)
- }
-
- async getSearchResults(messageId: string, searchId?: string): Promise {
- return await this.sessionProjection.getSearchResults(messageId, searchId)
- }
-
- async listMessageTraces(messageId: string): Promise {
- return await this.sessionProjection.listMessageTraces(messageId)
- }
-
- async getMessageTraceCount(messageId: string): Promise {
- return await this.sessionProjection.getMessageTraceCount(messageId)
- }
-
- async getMessageIds(sessionId: string): Promise {
- return await this.sessionProjection.getMessageIds(sessionId)
- }
-
- async getMessage(messageId: string): Promise {
- return await this.sessionProjection.getMessage(messageId)
- }
-
- async activateSession(webContentsId: number, sessionId: string): Promise {
- await this.sessionProjection.activate(webContentsId, sessionId)
- }
-
- async deactivateSession(webContentsId: number): Promise {
- await this.sessionProjection.deactivate(webContentsId)
- }
-
- async getActiveSession(webContentsId: number): Promise {
- return await this.sessionProjection.getActive(webContentsId)
- }
-
- getActiveSessionId(webContentsId: number): string | null {
- return this.sessionProjection.getActiveId(webContentsId)
- }
-
- async renameSession(sessionId: string, title: string): Promise {
- await this.sessionProjection.renameSession(sessionId, title)
- }
-
- async toggleSessionPinned(sessionId: string, pinned: boolean): Promise {
- await this.sessionProjection.toggleSessionPinned(sessionId, pinned)
- }
-
- async clearSessionMessages(sessionId: string): Promise {
- await this.sessionTurn.clearSessionMessages(sessionId)
- }
-
- async deleteSession(sessionId: string): Promise {
- await this.sessionLifecycle.deleteSession(sessionId)
- }
-
- async getAgentTransferImpact(agentId: string): Promise {
- return await this.sessionAssignment.getAgentTransferImpact(agentId)
- }
-
- async moveAgentSessions(
- fromAgentId: string,
- toAgentId: string
- ): Promise<{ movedSessionIds: string[]; deletedSessionIds: string[] }> {
- return await this.sessionAssignment.moveAgentSessions(fromAgentId, toAgentId)
- }
-
- async deleteAgentSessions(agentId: string): Promise {
- return await this.sessionAssignment.deleteAgentSessions(agentId)
- }
-
- async moveSessionToAgent(sessionId: string, toAgentId: string): Promise {
- return await this.sessionAssignment.moveSessionToAgent(sessionId, toAgentId)
- }
-
- async cancelGeneration(sessionId: string): Promise {
- await this.sessionTurn.cancelGeneration(sessionId)
- }
-
- clearSessionPermissions(sessionId: string): void {
- this.sessionPermissionPort?.clearSessionPermissions(sessionId)
- }
-
- async respondToolInteraction(
- sessionId: string,
- messageId: string,
- toolCallId: string,
- response: ToolInteractionResponse
- ): Promise {
- return await this.sessionTurn.respondToolInteraction(sessionId, messageId, toolCallId, response)
- }
-
- async getAcpSessionCommands(sessionId: string): Promise<
- Array<{
- name: string
- description: string
- input?: { hint: string } | null
- }>
- > {
- return await this.sessionAssignment.getAcpSessionCommands(sessionId)
- }
-
- async getAcpSessionConfigOptions(sessionId: string): Promise {
- return await this.sessionAssignment.getAcpSessionConfigOptions(sessionId)
- }
-
- async setAcpSessionConfigOption(
- sessionId: string,
- configId: string,
- value: string | boolean
- ): Promise {
- return await this.sessionAssignment.setAcpSessionConfigOption(sessionId, configId, value)
- }
-
- async getPermissionMode(sessionId: string): Promise {
- return await this.sessionAssignment.getPermissionMode(sessionId)
- }
-
- async setPermissionMode(sessionId: string, mode: PermissionMode): Promise {
- await this.sessionAssignment.setPermissionMode(sessionId, mode)
- }
-
- async setSessionSubagentEnabled(sessionId: string, enabled: boolean): Promise {
- return await this.sessionAssignment.setSessionSubagentEnabled(sessionId, enabled)
- }
-
- async setSessionModel(
- sessionId: string,
- providerId: string,
- modelId: string
- ): Promise {
- return await this.sessionAssignment.setSessionModel(sessionId, providerId, modelId)
- }
-
- async setSessionProjectDir(
- sessionId: string,
- projectDir: string | null
- ): Promise {
- return await this.sessionAssignment.setSessionProjectDir(sessionId, projectDir)
- }
-
- async getSessionGenerationSettings(sessionId: string): Promise {
- return await this.sessionAssignment.getSessionGenerationSettings(sessionId)
- }
-
- async getSessionDisabledAgentTools(sessionId: string): Promise {
- return await this.sessionAssignment.getSessionDisabledAgentTools(sessionId)
- }
-
- async updateSessionDisabledAgentTools(
- sessionId: string,
- disabledAgentTools: string[]
- ): Promise {
- return await this.sessionAssignment.updateSessionDisabledAgentTools(
- sessionId,
- disabledAgentTools
- )
- }
-
- async updateSessionGenerationSettings(
- sessionId: string,
- settings: Partial
- ): Promise {
- return await this.sessionAssignment.updateSessionGenerationSettings(sessionId, settings)
- }
-}
diff --git a/src/main/presenter/floatingButtonPresenter/index.ts b/src/main/presenter/floatingButtonPresenter/index.ts
index 4c69dff7a..1c2fca8aa 100644
--- a/src/main/presenter/floatingButtonPresenter/index.ts
+++ b/src/main/presenter/floatingButtonPresenter/index.ts
@@ -612,17 +612,12 @@ export class FloatingButtonPresenter {
}
private async loadSessions(): Promise {
- const agentSessionPresenter = presenter.agentSessionPresenter as
- | {
- getSessionList?: (filters?: { agentId?: string }) => Promise
- }
- | undefined
-
- if (!agentSessionPresenter?.getSessionList) {
+ const projection = presenter?.sessionProjectionCoordinator
+ if (!projection) {
return []
}
- return await agentSessionPresenter.getSessionList()
+ return await projection.listSessions()
}
private async loadAgents(): Promise {
@@ -631,13 +626,8 @@ export class FloatingButtonPresenter {
private async openSession(sessionId: string): Promise {
try {
- const agentSessionPresenter = presenter.agentSessionPresenter as
- | {
- activateSession?: (webContentsId: number, sessionId: string) => Promise
- }
- | undefined
-
- if (!agentSessionPresenter?.activateSession) {
+ const projection = presenter?.sessionProjectionCoordinator
+ if (!projection) {
return
}
@@ -646,7 +636,7 @@ export class FloatingButtonPresenter {
return
}
- await agentSessionPresenter.activateSession(targetWindow.webContents.id, sessionId)
+ await projection.activate(targetWindow.webContents.id, sessionId)
presenter.windowPresenter.show(targetWindow.id, true)
this.setExpanded(false)
} catch (error) {
diff --git a/src/main/presenter/index.ts b/src/main/presenter/index.ts
index 178175c49..6d59b6b86 100644
--- a/src/main/presenter/index.ts
+++ b/src/main/presenter/index.ts
@@ -29,7 +29,6 @@ import {
IYoBrowserPresenter,
ISkillPresenter,
ISkillSyncPresenter,
- IAgentSessionPresenter,
IProjectPresenter,
IRemoteControlPresenter
} from '@shared/presenter'
@@ -76,7 +75,6 @@ import { toAppSessionId } from '@/agent/shared/agentSessionIds'
import { resolveAssistantModelSelection } from '@/agent/shared/assistantModelSelection'
import { AgentUnavailableError } from '@/agent/shared/agentCatalogCodec'
import { resolveAcpAgentAlias } from '@shared/utils/acpAgentAlias'
-import { AgentSessionPresenter } from './agentSessionPresenter'
import { SessionProjectionCoordinator } from './sessionApplication/projectionCoordinator'
import { SessionAgentAssignmentPolicy } from './sessionApplication/agentAssignmentPolicy'
import { SessionAgentAssignmentCoordinator } from './sessionApplication/agentAssignmentCoordinator'
@@ -173,7 +171,6 @@ export class Presenter implements IPresenter {
lifecycleManager: ILifecycleManager
skillPresenter: ISkillPresenter
skillSyncPresenter: ISkillSyncPresenter
- agentSessionPresenter: IAgentSessionPresenter
sessionProjectionCoordinator: SessionProjectionCoordinator
sessionAgentAssignmentPolicy: SessionAgentAssignmentPolicy
sessionAgentAssignmentCoordinator: SessionAgentAssignmentCoordinator
@@ -301,7 +298,7 @@ export class Presenter implements IPresenter {
const agentToolRuntime: AgentToolRuntimePort = {
resolveConversationWorkdir: async (conversationId) => {
try {
- const session = await this.agentSessionPresenter?.getSession(conversationId)
+ const session = await this.sessionProjectionCoordinator.getSession(conversationId)
const normalized = session?.projectDir?.trim()
if (normalized) {
return normalized
@@ -316,25 +313,20 @@ export class Presenter implements IPresenter {
return null
},
resolveConversationSessionInfo: async (conversationId) => {
- const session = await this.agentSessionPresenter?.getSession(conversationId)
+ const session = await this.sessionProjectionCoordinator.getSession(conversationId)
if (!session) {
return null
}
const agent = await this.configPresenter.getAgent(session.agentId)
const agentType = await this.configPresenter.getAgentType(session.agentId)
- const permissionMode =
- typeof this.agentSessionPresenter?.getPermissionMode === 'function'
- ? await this.agentSessionPresenter.getPermissionMode(session.id)
- : 'full_access'
+ const permissionMode = await this.sessionAgentAssignmentCoordinator.getPermissionMode(
+ session.id
+ )
const generationSettings =
- typeof this.agentSessionPresenter?.getSessionGenerationSettings === 'function'
- ? await this.agentSessionPresenter.getSessionGenerationSettings(session.id)
- : null
+ await this.sessionAgentAssignmentCoordinator.getSessionGenerationSettings(session.id)
const disabledAgentTools =
- typeof this.agentSessionPresenter?.getSessionDisabledAgentTools === 'function'
- ? await this.agentSessionPresenter.getSessionDisabledAgentTools(session.id)
- : []
+ await this.sessionAgentAssignmentCoordinator.getSessionDisabledAgentTools(session.id)
const activeSkills = await this.skillPresenter.getActiveSkills(session.id)
const availableSubagentSlots =
agentType === 'deepchat' && session.sessionKind === 'regular'
@@ -363,19 +355,23 @@ export class Presenter implements IPresenter {
}
},
getTapeInfo: async (conversationId) => {
- return await this.agentSessionPresenter.getTapeInfo(conversationId)
+ return await this.sessionProjectionCoordinator.getTapeInfo(conversationId)
},
searchTape: async (conversationId, query, options) => {
- return await this.agentSessionPresenter.searchTape(conversationId, query, options)
+ return await this.sessionProjectionCoordinator.searchTape(conversationId, query, options)
},
getTapeContext: async (conversationId, entryIds, options) => {
- return await this.agentSessionPresenter.getTapeContext(conversationId, entryIds, options)
+ return await this.sessionProjectionCoordinator.getTapeContext(
+ conversationId,
+ entryIds,
+ options
+ )
},
listTapeAnchors: async (conversationId, options) => {
- return await this.agentSessionPresenter.listTapeAnchors(conversationId, options)
+ return await this.sessionProjectionCoordinator.listTapeAnchors(conversationId, options)
},
handoffTape: async (conversationId, name, state) => {
- return await this.agentSessionPresenter.handoffTape(conversationId, name, state)
+ return await this.sessionProjectionCoordinator.handoffTape(conversationId, name, state)
},
isMemoryEnabled: (agentId) => this.memoryPresenter.isEnabled(agentId),
rememberMemory: async (agentId, input, sourceSession, model) =>
@@ -409,29 +405,28 @@ export class Presenter implements IPresenter {
listCronJobRuns: async (jobId, limit) => this.cronJobs.listRuns(jobId, limit),
previewCronSchedule: async (input) => this.cronJobs.previewSchedule(input),
createSubagentSession: async (input) => {
- const agentSessionPresenter = this.agentSessionPresenter as IAgentSessionPresenter & {
- createSubagentSession?: (createInput: typeof input) => Promise<{
- id: string
- } | null>
- }
- const created = await agentSessionPresenter.createSubagentSession?.(input)
- if (!created?.id) {
- return null
- }
-
+ const created = await this.sessionLifecycleCoordinator.createSubagentSession(input)
return await agentToolRuntime.resolveConversationSessionInfo(created.id)
},
mergeSubagentTape: async (parentSessionId, childSessionId, meta) => {
- await this.agentSessionPresenter.mergeSubagentTape(parentSessionId, childSessionId, meta)
+ await this.sessionAgentAssignmentCoordinator.mergeSubagentTape(
+ parentSessionId,
+ childSessionId,
+ meta
+ )
},
discardSubagentTape: async (parentSessionId, childSessionId, meta) => {
- await this.agentSessionPresenter.discardSubagentTape(parentSessionId, childSessionId, meta)
+ await this.sessionAgentAssignmentCoordinator.discardSubagentTape(
+ parentSessionId,
+ childSessionId,
+ meta
+ )
},
sendConversationMessage: async (conversationId, content) => {
- await this.agentSessionPresenter.sendMessage(conversationId, content)
+ await this.sessionTurnCoordinator.sendMessage(conversationId, content)
},
cancelConversation: async (conversationId) => {
- await this.agentSessionPresenter.cancelGeneration(conversationId)
+ await this.sessionTurnCoordinator.cancelGeneration(conversationId)
},
subscribeDeepChatSessionUpdates: (listener) =>
subscribeDeepChatInternalSessionUpdates(listener),
@@ -501,7 +496,7 @@ export class Presenter implements IPresenter {
const skillSessionStatePort: SkillSessionStatePort = {
hasNewSession: async (conversationId) => {
try {
- return Boolean(await this.agentSessionPresenter?.getSession(conversationId))
+ return Boolean(await this.sessionProjectionCoordinator.getSession(conversationId))
} catch {
return false
}
@@ -537,8 +532,8 @@ export class Presenter implements IPresenter {
// Initialize new agent architecture presenters first (needed by hooksNotifications)
this.hooksNotifications = new HooksNotificationsService(this.configPresenter, {
- getSession: async () => null,
- getMessage: async () => null
+ getSession: (sessionId) => this.sessionProjectionCoordinator.getSession(sessionId),
+ getMessage: (messageId) => this.sessionProjectionCoordinator.getMessage(messageId)
})
this.cronJobs = new CronJobsService({
sqlitePresenter: this.sqlitePresenter as unknown as SQLitePresenter,
@@ -944,15 +939,6 @@ export class Presenter implements IPresenter {
configPresenter: this.configPresenter,
llmProviderPresenter: this.llmproviderPresenter
})
- this.agentSessionPresenter = new AgentSessionPresenter(
- this.sessionProjectionCoordinator,
- this.sessionLifecycleCoordinator,
- this.sessionAgentAssignmentCoordinator,
- this.sessionTurnCoordinator,
- {
- sessionPermissionPort
- }
- )
this.projectPresenter = new ProjectPresenter(
this.sqlitePresenter as unknown as import('./sqlitePresenter').SQLitePresenter,
this.devicePresenter,
@@ -972,12 +958,6 @@ export class Presenter implements IPresenter {
this.remoteControlPresenter = this.#remoteControlPresenter
this.cronJobs.setRemoteDeliveryPort(this.#remoteControlPresenter)
- // Update hooksNotifications with actual dependencies now that agentSessionPresenter is ready
- this.hooksNotifications = new HooksNotificationsService(this.configPresenter, {
- getSession: this.agentSessionPresenter.getSession.bind(this.agentSessionPresenter),
- getMessage: this.agentSessionPresenter.getMessage.bind(this.agentSessionPresenter)
- })
-
this.setupEventBus()
}
@@ -1367,10 +1347,10 @@ const buildMainKernelRouteRuntime = () =>
configPresenter: presenter.configPresenter,
llmProviderPresenter: presenter.llmproviderPresenter,
acpProviderAdminPort: presenter.acpProviderAdminPort,
- agentSessionPresenter: presenter.agentSessionPresenter,
sessionLifecyclePort: presenter.sessionLifecycleCoordinator,
sessionProjectionPort: presenter.sessionProjectionCoordinator,
sessionTurnPort: presenter.sessionTurnCoordinator,
+ sessionAssignmentPort: presenter.sessionAgentAssignmentCoordinator,
sessionPermissionPort: presenter.sessionPermissionPort,
skillPresenter: presenter.skillPresenter,
skillSyncPresenter: presenter.skillSyncPresenter,
diff --git a/src/main/presenter/mcpPresenter/inMemoryServers/conversationSearchServer.ts b/src/main/presenter/mcpPresenter/inMemoryServers/conversationSearchServer.ts
index 7a31b1c4f..0de9edc64 100644
--- a/src/main/presenter/mcpPresenter/inMemoryServers/conversationSearchServer.ts
+++ b/src/main/presenter/mcpPresenter/inMemoryServers/conversationSearchServer.ts
@@ -266,11 +266,11 @@ export class ConversationSearchServer {
// 获取对话历史
private async getConversationHistory(conversationId: string, includeSystem: boolean = false) {
try {
- const session = await presenter.agentSessionPresenter.getSession(conversationId)
+ const session = await presenter.sessionProjectionCoordinator.getSession(conversationId)
if (!session) {
throw new Error(`Session not found: ${conversationId}`)
}
- const records = await presenter.agentSessionPresenter.getMessages(conversationId)
+ const records = await presenter.sessionProjectionCoordinator.getMessages(conversationId)
const filteredMessages = includeSystem
? records
diff --git a/src/main/presenter/mcpPresenter/toolManager.ts b/src/main/presenter/mcpPresenter/toolManager.ts
index 4aa87b565..e9cc56126 100644
--- a/src/main/presenter/mcpPresenter/toolManager.ts
+++ b/src/main/presenter/mcpPresenter/toolManager.ts
@@ -436,7 +436,7 @@ export class ToolManager {
}
try {
- const session = await presenter.agentSessionPresenter.getSession(sessionId)
+ const session = await presenter.sessionProjectionCoordinator.getSession(sessionId)
const agentId = session?.agentId?.trim()
const providerId = session?.providerId?.trim()
if (session && providerId === 'acp' && agentId) {
diff --git a/src/main/routes/index.ts b/src/main/routes/index.ts
index 62ce50514..8d842ccb7 100644
--- a/src/main/routes/index.ts
+++ b/src/main/routes/index.ts
@@ -1,6 +1,5 @@
import { BrowserWindow, app, type IpcMain, type IpcMainInvokeEvent } from 'electron'
import type {
- IAgentSessionPresenter,
IConfigPresenter,
IConversationExporter,
IDevicePresenter,
@@ -403,11 +402,7 @@ import {
} from '@shared/contracts/routes/memory.routes'
import type { ChatMessageRecord } from '@shared/types/agent-interface'
import { buildEffectiveTapeView } from '../presenter/agentRuntimePresenter/tapeEffectiveView'
-import {
- ChatService,
- type ChatServiceProjectionPort,
- type ChatServiceTurnPort
-} from './chat/chatService'
+import { ChatService, type ChatServiceProjectionPort } from './chat/chatService'
import { dispatchConfigRoute } from './config/configRouteHandler'
import { createPresenterHotPathPorts } from './hotPathPorts'
import { dispatchModelRoute } from './models/modelRouteHandler'
@@ -424,11 +419,7 @@ import { ProviderImportService } from './providers/providerImportService'
import { ProviderService } from './providers/providerService'
import { createSettingsRouteAdapter } from './settings/settingsAdapter'
import { createSettingsRouteHandler } from './settings/settingsHandler'
-import {
- SessionService,
- type SessionServiceLifecyclePort,
- type SessionServiceProjectionPort
-} from './sessions/sessionService'
+import { SessionService, type SessionServiceProjectionPort } from './sessions/sessionService'
import type { StartupWorkloadCoordinator } from '@/presenter/startupWorkloadCoordinator'
import type { PluginPresenter } from '@/presenter/pluginPresenter'
import type { DatabaseSecurityPresenter } from '@/presenter/databaseSecurityPresenter'
@@ -447,6 +438,12 @@ import type { SessionHistorySearch } from './sessions/sessionHistorySearch'
import type { SessionTranslation } from './sessions/sessionTranslation'
import type { AgentSessionExportService } from '@/presenter/exporter/agentSessionExporter'
import { listAvailableAgents } from '@/agent/shared/availableAgentCatalog'
+import type {
+ SessionAgentAssignmentPort,
+ SessionLifecyclePort,
+ SessionTurnPort
+} from '@/presenter/sessionApplication/ports'
+import type { SessionProjectionCoordinator } from '@/presenter/sessionApplication/projectionCoordinator'
const MEMORY_PERSONA_STATES = ['draft', 'active', 'superseded', 'rejected'] as const
type MemoryPersonaState = (typeof MEMORY_PERSONA_STATES)[number]
@@ -456,7 +453,10 @@ export type MainKernelRouteRuntime = {
configPresenter: IConfigPresenter
llmProviderPresenter: ILlmProviderPresenter
acpProviderAdminPort: AcpProviderAdminPort
- agentSessionPresenter: IAgentSessionPresenter
+ sessionLifecyclePort: SessionLifecyclePort
+ sessionProjectionPort: MainKernelSessionProjectionPort
+ sessionTurnPort: SessionTurnPort
+ sessionAssignmentPort: SessionAgentAssignmentPort
skillPresenter: ISkillPresenter
skillSyncPresenter: ISkillSyncPresenter
exporter: IConversationExporter
@@ -494,6 +494,22 @@ export type MainKernelRouteRuntime = {
sessionTranslation: Pick
}
+export type MainKernelSessionProjectionPort = SessionServiceProjectionPort &
+ ChatServiceProjectionPort &
+ Pick<
+ SessionProjectionCoordinator,
+ | 'getActiveId'
+ | 'listLightweight'
+ | 'getLightweightByIds'
+ | 'getSearchResults'
+ | 'getTapeContext'
+ | 'listMessageTraces'
+ | 'listMessageViewManifests'
+ | 'exportMessageTapeReplaySlice'
+ | 'renameSession'
+ | 'toggleSessionPinned'
+ >
+
export function formatMemorySourceRecordContent(record: ChatMessageRecord): string {
try {
const parsed = JSON.parse(record.content) as unknown
@@ -739,10 +755,10 @@ export function createMainKernelRouteRuntime(deps: {
configPresenter: IConfigPresenter
llmProviderPresenter: ILlmProviderPresenter
acpProviderAdminPort: AcpProviderAdminPort
- agentSessionPresenter: IAgentSessionPresenter
- sessionLifecyclePort: SessionServiceLifecyclePort
- sessionProjectionPort: SessionServiceProjectionPort & ChatServiceProjectionPort
- sessionTurnPort: ChatServiceTurnPort
+ sessionLifecyclePort: SessionLifecyclePort
+ sessionProjectionPort: MainKernelSessionProjectionPort
+ sessionTurnPort: SessionTurnPort
+ sessionAssignmentPort: SessionAgentAssignmentPort
sessionPermissionPort: Pick
skillPresenter: ISkillPresenter
skillSyncPresenter: ISkillSyncPresenter
@@ -798,7 +814,10 @@ export function createMainKernelRouteRuntime(deps: {
configPresenter: deps.configPresenter,
llmProviderPresenter: deps.llmProviderPresenter,
acpProviderAdminPort: deps.acpProviderAdminPort,
- agentSessionPresenter: deps.agentSessionPresenter,
+ sessionLifecyclePort: deps.sessionLifecyclePort,
+ sessionProjectionPort: deps.sessionProjectionPort,
+ sessionTurnPort: deps.sessionTurnPort,
+ sessionAssignmentPort: deps.sessionAssignmentPort,
skillPresenter: deps.skillPresenter,
skillSyncPresenter: deps.skillSyncPresenter,
exporter: deps.exporter,
@@ -2743,13 +2762,10 @@ export async function dispatchDeepchatRoute(
const coordinator = (runtime as Partial).startupWorkloadCoordinator
if (!coordinator) {
- const activeSessionId = runtime.agentSessionPresenter.getActiveSessionId(
- context.webContentsId
- )
+ const activeSessionId = runtime.sessionProjectionPort.getActiveId(context.webContentsId)
const activeSession = activeSessionId
- ? ((
- await runtime.agentSessionPresenter.getLightweightSessionsByIds([activeSessionId])
- )[0] ?? null)
+ ? ((await runtime.sessionProjectionPort.getLightweightByIds([activeSessionId]))[0] ??
+ null)
: null
const [agents, acpEnabled, defaultChatWorkspacePath] = await Promise.all([
runtime.configPresenter.listAgents(),
@@ -2793,13 +2809,10 @@ export async function dispatchDeepchatRoute(
runId: coordinator.getRunId('main'),
run: async () => {
const startupRunId = coordinator.getRunId('main')
- const activeSessionId = runtime.agentSessionPresenter.getActiveSessionId(
- context.webContentsId
- )
+ const activeSessionId = runtime.sessionProjectionPort.getActiveId(context.webContentsId)
const activeSession = activeSessionId
- ? ((
- await runtime.agentSessionPresenter.getLightweightSessionsByIds([activeSessionId])
- )[0] ?? null)
+ ? ((await runtime.sessionProjectionPort.getLightweightByIds([activeSessionId]))[0] ??
+ null)
: null
const [agents, acpEnabled, defaultChatWorkspacePath] = await Promise.all([
runtime.configPresenter.listAgents(),
@@ -2865,16 +2878,14 @@ export async function dispatchDeepchatRoute(
case sessionsListLightweightRoute.name: {
return await runTrackedRouteTask(runtime, routeName, context, async () => {
const input = sessionsListLightweightRoute.input.parse(rawInput)
- const page = await runtime.agentSessionPresenter.getLightweightSessionList(input)
+ const page = await runtime.sessionProjectionPort.listLightweight(input)
return sessionsListLightweightRoute.output.parse(page)
})
}
case sessionsGetLightweightByIdsRoute.name: {
const input = sessionsGetLightweightByIdsRoute.input.parse(rawInput)
- const items = await runtime.agentSessionPresenter.getLightweightSessionsByIds(
- input.sessionIds
- )
+ const items = await runtime.sessionProjectionPort.getLightweightByIds(input.sessionIds)
return sessionsGetLightweightByIdsRoute.output.parse({ items })
}
@@ -2898,28 +2909,25 @@ export async function dispatchDeepchatRoute(
case sessionsEnsureAcpDraftRoute.name: {
const input = sessionsEnsureAcpDraftRoute.input.parse(rawInput)
- const session = await runtime.agentSessionPresenter.ensureAcpDraftSession(input)
+ const session = await runtime.sessionLifecyclePort.ensureAcpDraftSession(input)
return sessionsEnsureAcpDraftRoute.output.parse({ session })
}
case sessionsListPendingInputsRoute.name: {
const input = sessionsListPendingInputsRoute.input.parse(rawInput)
- const items = await runtime.agentSessionPresenter.listPendingInputs(input.sessionId)
+ const items = await runtime.sessionTurnPort.listPendingInputs(input.sessionId)
return sessionsListPendingInputsRoute.output.parse({ items })
}
case sessionsQueuePendingInputRoute.name: {
const input = sessionsQueuePendingInputRoute.input.parse(rawInput)
- const item = await runtime.agentSessionPresenter.queuePendingInput(
- input.sessionId,
- input.content
- )
+ const item = await runtime.sessionTurnPort.queuePendingInput(input.sessionId, input.content)
return sessionsQueuePendingInputRoute.output.parse({ item })
}
case sessionsUpdateQueuedInputRoute.name: {
const input = sessionsUpdateQueuedInputRoute.input.parse(rawInput)
- const item = await runtime.agentSessionPresenter.updateQueuedInput(
+ const item = await runtime.sessionTurnPort.updateQueuedInput(
input.sessionId,
input.itemId,
input.content
@@ -2929,7 +2937,7 @@ export async function dispatchDeepchatRoute(
case sessionsMoveQueuedInputRoute.name: {
const input = sessionsMoveQueuedInputRoute.input.parse(rawInput)
- const items = await runtime.agentSessionPresenter.moveQueuedInput(
+ const items = await runtime.sessionTurnPort.moveQueuedInput(
input.sessionId,
input.itemId,
input.toIndex
@@ -2939,7 +2947,7 @@ export async function dispatchDeepchatRoute(
case sessionsConvertPendingInputToSteerRoute.name: {
const input = sessionsConvertPendingInputToSteerRoute.input.parse(rawInput)
- const item = await runtime.agentSessionPresenter.convertPendingInputToSteer(
+ const item = await runtime.sessionTurnPort.convertPendingInputToSteer(
input.sessionId,
input.itemId
)
@@ -2948,34 +2956,31 @@ export async function dispatchDeepchatRoute(
case sessionsSteerPendingInputRoute.name: {
const input = sessionsSteerPendingInputRoute.input.parse(rawInput)
- const item = await runtime.agentSessionPresenter.steerPendingInput(
- input.sessionId,
- input.itemId
- )
+ const item = await runtime.sessionTurnPort.steerPendingInput(input.sessionId, input.itemId)
return sessionsSteerPendingInputRoute.output.parse({ item })
}
case sessionsDeletePendingInputRoute.name: {
const input = sessionsDeletePendingInputRoute.input.parse(rawInput)
- await runtime.agentSessionPresenter.deletePendingInput(input.sessionId, input.itemId)
+ await runtime.sessionTurnPort.deletePendingInput(input.sessionId, input.itemId)
return sessionsDeletePendingInputRoute.output.parse({ deleted: true })
}
case sessionsRetryMessageRoute.name: {
const input = sessionsRetryMessageRoute.input.parse(rawInput)
- await runtime.agentSessionPresenter.retryMessage(input.sessionId, input.messageId)
+ await runtime.sessionTurnPort.retryMessage(input.sessionId, input.messageId)
return sessionsRetryMessageRoute.output.parse({ retried: true })
}
case sessionsDeleteMessageRoute.name: {
const input = sessionsDeleteMessageRoute.input.parse(rawInput)
- await runtime.agentSessionPresenter.deleteMessage(input.sessionId, input.messageId)
+ await runtime.sessionTurnPort.deleteMessage(input.sessionId, input.messageId)
return sessionsDeleteMessageRoute.output.parse({ deleted: true })
}
case sessionsEditUserMessageRoute.name: {
const input = sessionsEditUserMessageRoute.input.parse(rawInput)
- const message = await runtime.agentSessionPresenter.editUserMessage(
+ const message = await runtime.sessionTurnPort.editUserMessage(
input.sessionId,
input.messageId,
input.text
@@ -2985,7 +2990,7 @@ export async function dispatchDeepchatRoute(
case sessionsForkRoute.name: {
const input = sessionsForkRoute.input.parse(rawInput)
- const session = await runtime.agentSessionPresenter.forkSession(
+ const session = await runtime.sessionLifecyclePort.forkSession(
input.sourceSessionId,
input.targetMessageId,
input.newTitle
@@ -3001,7 +3006,7 @@ export async function dispatchDeepchatRoute(
case sessionsGetSearchResultsRoute.name: {
const input = sessionsGetSearchResultsRoute.input.parse(rawInput)
- const results = await runtime.agentSessionPresenter.getSearchResults(
+ const results = await runtime.sessionProjectionPort.getSearchResults(
input.messageId,
input.searchId
)
@@ -3010,7 +3015,7 @@ export async function dispatchDeepchatRoute(
case sessionsGetTapeContextRoute.name: {
const input = sessionsGetTapeContextRoute.input.parse(rawInput)
- const context = await runtime.agentSessionPresenter.getTapeContext(
+ const context = await runtime.sessionProjectionPort.getTapeContext(
input.sessionId,
input.entryIds,
input.options
@@ -3020,8 +3025,8 @@ export async function dispatchDeepchatRoute(
case sessionsListMessageTracesRoute.name: {
const input = sessionsListMessageTracesRoute.input.parse(rawInput)
- const traces = await runtime.agentSessionPresenter.listMessageTraces(input.messageId)
- const manifests = await runtime.agentSessionPresenter.listMessageViewManifests(
+ const traces = await runtime.sessionProjectionPort.listMessageTraces(input.messageId)
+ const manifests = await runtime.sessionProjectionPort.listMessageViewManifests(
input.messageId
)
return sessionsListMessageTracesRoute.output.parse({ traces, manifests })
@@ -3029,7 +3034,7 @@ export async function dispatchDeepchatRoute(
case sessionsExportMessageTapeReplaySliceRoute.name: {
const input = sessionsExportMessageTapeReplaySliceRoute.input.parse(rawInput)
- const slice = await runtime.agentSessionPresenter.exportMessageTapeReplaySlice(
+ const slice = await runtime.sessionProjectionPort.exportMessageTapeReplaySlice(
input.messageId,
input.options
)
@@ -3066,25 +3071,25 @@ export async function dispatchDeepchatRoute(
case sessionsRenameRoute.name: {
const input = sessionsRenameRoute.input.parse(rawInput)
- await runtime.agentSessionPresenter.renameSession(input.sessionId, input.title)
+ await runtime.sessionProjectionPort.renameSession(input.sessionId, input.title)
return sessionsRenameRoute.output.parse({ updated: true })
}
case sessionsTogglePinnedRoute.name: {
const input = sessionsTogglePinnedRoute.input.parse(rawInput)
- await runtime.agentSessionPresenter.toggleSessionPinned(input.sessionId, input.pinned)
+ await runtime.sessionProjectionPort.toggleSessionPinned(input.sessionId, input.pinned)
return sessionsTogglePinnedRoute.output.parse({ updated: true })
}
case sessionsClearMessagesRoute.name: {
const input = sessionsClearMessagesRoute.input.parse(rawInput)
- await runtime.agentSessionPresenter.clearSessionMessages(input.sessionId)
+ await runtime.sessionTurnPort.clearSessionMessages(input.sessionId)
return sessionsClearMessagesRoute.output.parse({ cleared: true })
}
case sessionsCompactRoute.name: {
const input = sessionsCompactRoute.input.parse(rawInput)
- const result = await runtime.agentSessionPresenter.compactSession(input.sessionId)
+ const result = await runtime.sessionTurnPort.compactSession(input.sessionId)
return sessionsCompactRoute.output.parse(result)
}
@@ -3096,19 +3101,19 @@ export async function dispatchDeepchatRoute(
case sessionsDeleteRoute.name: {
const input = sessionsDeleteRoute.input.parse(rawInput)
- await runtime.agentSessionPresenter.deleteSession(input.sessionId)
+ await runtime.sessionLifecyclePort.deleteSession(input.sessionId)
return sessionsDeleteRoute.output.parse({ deleted: true })
}
case sessionsGetAgentTransferImpactRoute.name: {
const input = sessionsGetAgentTransferImpactRoute.input.parse(rawInput)
- const impact = await runtime.agentSessionPresenter.getAgentTransferImpact(input.agentId)
+ const impact = await runtime.sessionAssignmentPort.getAgentTransferImpact(input.agentId)
return sessionsGetAgentTransferImpactRoute.output.parse({ impact })
}
case sessionsMoveAgentSessionsRoute.name: {
const input = sessionsMoveAgentSessionsRoute.input.parse(rawInput)
- const result = await runtime.agentSessionPresenter.moveAgentSessions(
+ const result = await runtime.sessionAssignmentPort.moveAgentSessions(
input.fromAgentId,
input.toAgentId
)
@@ -3117,7 +3122,7 @@ export async function dispatchDeepchatRoute(
case sessionsDeleteAgentSessionsRoute.name: {
const input = sessionsDeleteAgentSessionsRoute.input.parse(rawInput)
- const deletedSessionIds = await runtime.agentSessionPresenter.deleteAgentSessions(
+ const deletedSessionIds = await runtime.sessionAssignmentPort.deleteAgentSessions(
input.agentId
)
return sessionsDeleteAgentSessionsRoute.output.parse({ deletedSessionIds })
@@ -3125,7 +3130,7 @@ export async function dispatchDeepchatRoute(
case sessionsMoveToAgentRoute.name: {
const input = sessionsMoveToAgentRoute.input.parse(rawInput)
- const session = await runtime.agentSessionPresenter.moveSessionToAgent(
+ const session = await runtime.sessionAssignmentPort.moveSessionToAgent(
input.sessionId,
input.toAgentId
)
@@ -3134,19 +3139,19 @@ export async function dispatchDeepchatRoute(
case sessionsGetAcpSessionCommandsRoute.name: {
const input = sessionsGetAcpSessionCommandsRoute.input.parse(rawInput)
- const commands = await runtime.agentSessionPresenter.getAcpSessionCommands(input.sessionId)
+ const commands = await runtime.sessionAssignmentPort.getAcpSessionCommands(input.sessionId)
return sessionsGetAcpSessionCommandsRoute.output.parse({ commands })
}
case sessionsGetAcpSessionConfigOptionsRoute.name: {
const input = sessionsGetAcpSessionConfigOptionsRoute.input.parse(rawInput)
- const state = await runtime.agentSessionPresenter.getAcpSessionConfigOptions(input.sessionId)
+ const state = await runtime.sessionAssignmentPort.getAcpSessionConfigOptions(input.sessionId)
return sessionsGetAcpSessionConfigOptionsRoute.output.parse({ state })
}
case sessionsSetAcpSessionConfigOptionRoute.name: {
const input = sessionsSetAcpSessionConfigOptionRoute.input.parse(rawInput)
- const state = await runtime.agentSessionPresenter.setAcpSessionConfigOption(
+ const state = await runtime.sessionAssignmentPort.setAcpSessionConfigOption(
input.sessionId,
input.configId,
input.value
@@ -3156,19 +3161,19 @@ export async function dispatchDeepchatRoute(
case sessionsGetPermissionModeRoute.name: {
const input = sessionsGetPermissionModeRoute.input.parse(rawInput)
- const mode = await runtime.agentSessionPresenter.getPermissionMode(input.sessionId)
+ const mode = await runtime.sessionAssignmentPort.getPermissionMode(input.sessionId)
return sessionsGetPermissionModeRoute.output.parse({ mode })
}
case sessionsSetPermissionModeRoute.name: {
const input = sessionsSetPermissionModeRoute.input.parse(rawInput)
- await runtime.agentSessionPresenter.setPermissionMode(input.sessionId, input.mode)
+ await runtime.sessionAssignmentPort.setPermissionMode(input.sessionId, input.mode)
return sessionsSetPermissionModeRoute.output.parse({ updated: true })
}
case sessionsSetSubagentEnabledRoute.name: {
const input = sessionsSetSubagentEnabledRoute.input.parse(rawInput)
- const session = await runtime.agentSessionPresenter.setSessionSubagentEnabled(
+ const session = await runtime.sessionAssignmentPort.setSessionSubagentEnabled(
input.sessionId,
input.enabled
)
@@ -3177,7 +3182,7 @@ export async function dispatchDeepchatRoute(
case sessionsSetModelRoute.name: {
const input = sessionsSetModelRoute.input.parse(rawInput)
- const session = await runtime.agentSessionPresenter.setSessionModel(
+ const session = await runtime.sessionAssignmentPort.setSessionModel(
input.sessionId,
input.providerId,
input.modelId
@@ -3187,7 +3192,7 @@ export async function dispatchDeepchatRoute(
case sessionsSetProjectDirRoute.name: {
const input = sessionsSetProjectDirRoute.input.parse(rawInput)
- const session = await runtime.agentSessionPresenter.setSessionProjectDir(
+ const session = await runtime.sessionAssignmentPort.setSessionProjectDir(
input.sessionId,
input.projectDir
)
@@ -3196,7 +3201,7 @@ export async function dispatchDeepchatRoute(
case sessionsGetGenerationSettingsRoute.name: {
const input = sessionsGetGenerationSettingsRoute.input.parse(rawInput)
- const settings = await runtime.agentSessionPresenter.getSessionGenerationSettings(
+ const settings = await runtime.sessionAssignmentPort.getSessionGenerationSettings(
input.sessionId
)
return sessionsGetGenerationSettingsRoute.output.parse({ settings })
@@ -3204,7 +3209,7 @@ export async function dispatchDeepchatRoute(
case sessionsGetDisabledAgentToolsRoute.name: {
const input = sessionsGetDisabledAgentToolsRoute.input.parse(rawInput)
- const disabledAgentTools = await runtime.agentSessionPresenter.getSessionDisabledAgentTools(
+ const disabledAgentTools = await runtime.sessionAssignmentPort.getSessionDisabledAgentTools(
input.sessionId
)
return sessionsGetDisabledAgentToolsRoute.output.parse({ disabledAgentTools })
@@ -3213,7 +3218,7 @@ export async function dispatchDeepchatRoute(
case sessionsUpdateDisabledAgentToolsRoute.name: {
const input = sessionsUpdateDisabledAgentToolsRoute.input.parse(rawInput)
const disabledAgentTools =
- await runtime.agentSessionPresenter.updateSessionDisabledAgentTools(
+ await runtime.sessionAssignmentPort.updateSessionDisabledAgentTools(
input.sessionId,
input.disabledAgentTools
)
@@ -3222,7 +3227,7 @@ export async function dispatchDeepchatRoute(
case sessionsUpdateGenerationSettingsRoute.name: {
const input = sessionsUpdateGenerationSettingsRoute.input.parse(rawInput)
- const settings = await runtime.agentSessionPresenter.updateSessionGenerationSettings(
+ const settings = await runtime.sessionAssignmentPort.updateSessionGenerationSettings(
input.sessionId,
input.settings
)
diff --git a/src/shared/types/presenters/agent-session.presenter.d.ts b/src/shared/types/presenters/agent-session.presenter.d.ts
deleted file mode 100644
index 418e6cfc2..000000000
--- a/src/shared/types/presenters/agent-session.presenter.d.ts
+++ /dev/null
@@ -1,189 +0,0 @@
-import type {
- SessionListItem,
- SessionLightweightListResult,
- SessionPageCursor,
- MessagePageCursor,
- ChatMessagePageResult,
- CreateSessionInput,
- CreateDetachedSessionInput,
- SessionWithState,
- ChatMessageRecord,
- MessageTraceRecord,
- PermissionMode,
- SessionGenerationSettings,
- SessionCompactionState,
- PendingSessionInputRecord,
- SendMessageInput,
- MessageStartResult,
- ToolInteractionResponse,
- ToolInteractionResult,
- AgentTapeInfo,
- AgentTapeAnchorsOptions,
- AgentTapeContextOptions,
- AgentTapeContextResult,
- AgentTapeSearchOptions,
- AgentTapeSearchResult,
- AgentTapeAnchorResult,
- AgentTransferImpact
-} from '../agent-interface'
-import type { DeepChatTapeViewManifestRecord } from '../tape-view-manifest'
-import type { DeepChatTapeReplayExportOptions, DeepChatTapeReplaySlice } from '../tape-replay'
-import type { AcpConfigState } from './llmprovider.presenter'
-import type { SearchResult } from './thread.presenter'
-
-export interface IAgentSessionPresenter {
- createSession(input: CreateSessionInput, webContentsId: number): Promise
- createDetachedSession(input: CreateDetachedSessionInput): Promise
- ensureAcpDraftSession(input: {
- agentId: string
- projectDir: string
- permissionMode?: PermissionMode
- }): Promise
- listPendingInputs(sessionId: string): Promise
- queuePendingInput(
- sessionId: string,
- content: string | SendMessageInput
- ): Promise
- updateQueuedInput(
- sessionId: string,
- itemId: string,
- content: string | SendMessageInput
- ): Promise
- moveQueuedInput(
- sessionId: string,
- itemId: string,
- toIndex: number
- ): Promise
- convertPendingInputToSteer(sessionId: string, itemId: string): Promise
- steerPendingInput(sessionId: string, itemId: string): Promise
- deletePendingInput(sessionId: string, itemId: string): Promise
- sendMessage(
- sessionId: string,
- content: string | SendMessageInput,
- options?: { maxProviderRounds?: number }
- ): Promise
- steerActiveTurn(sessionId: string, content: string | SendMessageInput): Promise
- retryMessage(sessionId: string, messageId: string): Promise
- deleteMessage(sessionId: string, messageId: string): Promise
- editUserMessage(sessionId: string, messageId: string, text: string): Promise
- forkSession(
- sourceSessionId: string,
- targetMessageId: string,
- newTitle?: string
- ): Promise
- getSessionList(filters?: {
- agentId?: string
- projectDir?: string
- includeSubagents?: boolean
- parentSessionId?: string
- }): Promise
- getSession(sessionId: string): Promise
- getMessages(sessionId: string): Promise
- listMessagesPage(
- sessionId: string,
- options?: {
- limit?: number
- cursor?: MessagePageCursor | null
- }
- ): Promise
- getSessionCompactionState(sessionId: string): Promise
- compactSession(sessionId: string): Promise<{ compacted: boolean; state: SessionCompactionState }>
- getTapeInfo(sessionId: string): Promise
- searchTape(
- sessionId: string,
- query: string,
- options?: AgentTapeSearchOptions
- ): Promise
- getTapeContext(
- sessionId: string,
- entryIds: number[],
- options?: AgentTapeContextOptions
- ): Promise
- listTapeAnchors(
- sessionId: string,
- options?: AgentTapeAnchorsOptions
- ): Promise
- handoffTape(
- sessionId: string,
- name: string,
- state?: Record
- ): Promise
- mergeSubagentTape(
- parentSessionId: string,
- childSessionId: string,
- meta?: Record
- ): Promise
- discardSubagentTape(
- parentSessionId: string,
- childSessionId: string,
- meta?: Record
- ): Promise
- getSearchResults(messageId: string, searchId?: string): Promise
- listMessageTraces(messageId: string): Promise
- listMessageViewManifests(messageId: string): Promise
- exportMessageTapeReplaySlice(
- messageId: string,
- options?: DeepChatTapeReplayExportOptions
- ): Promise
- getMessageTraceCount(messageId: string): Promise
- getMessageIds(sessionId: string): Promise
- getMessage(messageId: string): Promise
- activateSession(webContentsId: number, sessionId: string): Promise
- deactivateSession(webContentsId: number): Promise
- getActiveSession(webContentsId: number): Promise
- getActiveSessionId(webContentsId: number): string | null
- getLightweightSessionList(options?: {
- limit?: number
- cursor?: SessionPageCursor | null
- includeSubagents?: boolean
- agentId?: string
- prioritizeSessionId?: string
- }): Promise
- getLightweightSessionsByIds(sessionIds: string[]): Promise
- renameSession(sessionId: string, title: string): Promise
- toggleSessionPinned(sessionId: string, pinned: boolean): Promise
- clearSessionMessages(sessionId: string): Promise
- deleteSession(sessionId: string): Promise
- getAgentTransferImpact(agentId: string): Promise
- moveAgentSessions(
- fromAgentId: string,
- toAgentId: string
- ): Promise<{ movedSessionIds: string[]; deletedSessionIds: string[] }>
- deleteAgentSessions(agentId: string): Promise
- moveSessionToAgent(sessionId: string, toAgentId: string): Promise
- cancelGeneration(sessionId: string): Promise
- respondToolInteraction(
- sessionId: string,
- messageId: string,
- toolCallId: string,
- response: ToolInteractionResponse
- ): Promise
- getAcpSessionCommands(sessionId: string): Promise<
- Array<{
- name: string
- description: string
- input?: { hint: string } | null
- }>
- >
- getAcpSessionConfigOptions(sessionId: string): Promise
- setAcpSessionConfigOption(
- sessionId: string,
- configId: string,
- value: string | boolean
- ): Promise
- getPermissionMode(sessionId: string): Promise
- setPermissionMode(sessionId: string, mode: PermissionMode): Promise
- setSessionSubagentEnabled(sessionId: string, enabled: boolean): Promise
- setSessionModel(sessionId: string, providerId: string, modelId: string): Promise
- setSessionProjectDir(sessionId: string, projectDir: string | null): Promise
- getSessionGenerationSettings(sessionId: string): Promise
- getSessionDisabledAgentTools(sessionId: string): Promise
- updateSessionDisabledAgentTools(
- sessionId: string,
- disabledAgentTools: string[]
- ): Promise
- updateSessionGenerationSettings(
- sessionId: string,
- settings: Partial
- ): Promise
-}
diff --git a/src/shared/types/presenters/core.presenter.d.ts b/src/shared/types/presenters/core.presenter.d.ts
index a24ec249e..18e48ae03 100644
--- a/src/shared/types/presenters/core.presenter.d.ts
+++ b/src/shared/types/presenters/core.presenter.d.ts
@@ -20,7 +20,6 @@ import type { IWorkspacePresenter } from './workspace'
import type { IToolPresenter } from './tool.presenter'
import type { ISkillPresenter } from '../skill'
import type { ISkillSyncPresenter } from '../skillSync'
-import type { IAgentSessionPresenter } from './agent-session.presenter'
import type { IProjectPresenter } from './project.presenter'
import type { BrowserPageInfo, DownloadInfo, ScreenshotOptions, YoBrowserStatus } from '../browser'
import type { IWindowPresenter, TabData } from './window.presenter'
@@ -446,7 +445,6 @@ export interface IPresenter {
toolPresenter: IToolPresenter
skillPresenter: ISkillPresenter
skillSyncPresenter: ISkillSyncPresenter
- agentSessionPresenter: IAgentSessionPresenter
projectPresenter: IProjectPresenter
init(): void
destroy(): Promise
diff --git a/src/shared/types/presenters/index.d.ts b/src/shared/types/presenters/index.d.ts
index 8ad8f3de3..637c5d81a 100644
--- a/src/shared/types/presenters/index.d.ts
+++ b/src/shared/types/presenters/index.d.ts
@@ -100,7 +100,6 @@ export type {
} from './acp.presenter'
// New agent architecture types
-export type { IAgentSessionPresenter } from './agent-session.presenter'
export type { IProjectPresenter } from './project.presenter'
export type {
ChannelSettingsMap,
diff --git a/test/main/presenter/floatingButtonPresenter/index.test.ts b/test/main/presenter/floatingButtonPresenter/index.test.ts
index b8e4da961..af6fe3a1e 100644
--- a/test/main/presenter/floatingButtonPresenter/index.test.ts
+++ b/test/main/presenter/floatingButtonPresenter/index.test.ts
@@ -166,9 +166,9 @@ vi.mock('../../../../src/main/presenter/floatingButtonPresenter/FloatingButtonWi
vi.mock('../../../../src/main/presenter/index', () => ({
presenter: {
- agentSessionPresenter: {
- getSessionList: getSessionListMock,
- activateSession: vi.fn()
+ sessionProjectionCoordinator: {
+ listSessions: getSessionListMock,
+ activate: vi.fn()
},
windowPresenter: {
mainWindow: null,
diff --git a/test/main/presenter/mcpPresenter/toolManager.test.ts b/test/main/presenter/mcpPresenter/toolManager.test.ts
index 375a2eaa4..663e4b6a7 100644
--- a/test/main/presenter/mcpPresenter/toolManager.test.ts
+++ b/test/main/presenter/mcpPresenter/toolManager.test.ts
@@ -7,7 +7,7 @@ const eventBusMocks = vi.hoisted(() => ({
}))
const presenterMocks = vi.hoisted(() => ({
- agentSessionPresenter: {
+ sessionProjectionCoordinator: {
getSession: vi.fn()
}
}))
@@ -192,7 +192,7 @@ describe('ToolManager', () => {
configPresenter.getAcpAgents.mockResolvedValue([{ id: 'agent-1', name: 'Agent 1' }])
configPresenter.getAgentMcpSelections.mockResolvedValue([])
- presenterMocks.agentSessionPresenter.getSession.mockResolvedValue({
+ presenterMocks.sessionProjectionCoordinator.getSession.mockResolvedValue({
id: 'session-1',
agentId: 'agent-1',
title: 'New Chat',
@@ -346,7 +346,7 @@ describe('ToolManager', () => {
it('skips ACP session resolution when provider hint is non-ACP', async () => {
const client = createClient('open-server')
const configPresenter = createConfigPresenter('open-server')
- presenterMocks.agentSessionPresenter.getSession.mockResolvedValue(null)
+ presenterMocks.sessionProjectionCoordinator.getSession.mockResolvedValue(null)
const manager = new ToolManager(
configPresenter as never,
@@ -367,7 +367,7 @@ describe('ToolManager', () => {
expect(result.isError).toBe(false)
expect(result.content).toBe('ok')
expect(client.callTool).toHaveBeenCalledWith('echo', {})
- expect(presenterMocks.agentSessionPresenter.getSession).not.toHaveBeenCalled()
+ expect(presenterMocks.sessionProjectionCoordinator.getSession).not.toHaveBeenCalled()
expect(configPresenter.getAgentMcpSelections).not.toHaveBeenCalled()
expect(
warnSpy.mock.calls.some((call) =>
@@ -682,7 +682,7 @@ describe('ToolManager', () => {
const client = createClient('open-server')
const configPresenter = createConfigPresenter('open-server')
- presenterMocks.agentSessionPresenter.getSession.mockResolvedValue({
+ presenterMocks.sessionProjectionCoordinator.getSession.mockResolvedValue({
id: 'session-2',
agentId: 'deepchat',
title: 'Normal Chat',
@@ -813,7 +813,7 @@ describe('ToolManager', () => {
it('treats missing provider hint as a fallback to new session resolution', async () => {
const client = createClient('open-server')
const configPresenter = createConfigPresenter('open-server')
- presenterMocks.agentSessionPresenter.getSession.mockResolvedValue(null)
+ presenterMocks.sessionProjectionCoordinator.getSession.mockResolvedValue(null)
const manager = new ToolManager(
configPresenter as never,
@@ -833,7 +833,9 @@ describe('ToolManager', () => {
expect(result.isError).toBe(false)
expect(result.content).toBe('ok')
expect(client.callTool).toHaveBeenCalledWith('echo', {})
- expect(presenterMocks.agentSessionPresenter.getSession).toHaveBeenCalledWith('conv-fallback')
+ expect(presenterMocks.sessionProjectionCoordinator.getSession).toHaveBeenCalledWith(
+ 'conv-fallback'
+ )
expect(configPresenter.getAgentMcpSelections).not.toHaveBeenCalled()
})
})
diff --git a/test/main/presenter/presenterCallErrorHandler.test.ts b/test/main/presenter/presenterCallErrorHandler.test.ts
index 43f6ea757..e24f78408 100644
--- a/test/main/presenter/presenterCallErrorHandler.test.ts
+++ b/test/main/presenter/presenterCallErrorHandler.test.ts
@@ -31,7 +31,7 @@ describe('presenterCallErrorHandler', () => {
await expect(
handlePresenterCallResult(Promise.reject(error), {
webContentsId: 7,
- presenterName: 'agentSessionPresenter',
+ presenterName: 'sessionLifecycleCoordinator',
methodName: 'createSession'
})
).rejects.toThrow(error)
@@ -56,7 +56,7 @@ describe('presenterCallErrorHandler', () => {
await expect(
handlePresenterCallResult(Promise.reject(new Error(errorMessage)), {
webContentsId: 9,
- presenterName: 'agentSessionPresenter',
+ presenterName: 'sessionLifecycleCoordinator',
methodName: 'createSession'
})
).rejects.toThrow(errorMessage)
@@ -80,7 +80,7 @@ describe('presenterCallErrorHandler', () => {
await expect(
handlePresenterCallResult(Promise.reject(new Error(errorMessage)), {
webContentsId: 11,
- presenterName: 'agentSessionPresenter',
+ presenterName: 'sessionLifecycleCoordinator',
methodName: 'createSession'
})
).rejects.toThrow(errorMessage)
diff --git a/test/main/presenter/agentSessionPresenter/assignmentCoordinatorFixture.ts b/test/main/presenter/sessionApplication/assignmentCoordinatorFixture.ts
similarity index 100%
rename from test/main/presenter/agentSessionPresenter/assignmentCoordinatorFixture.ts
rename to test/main/presenter/sessionApplication/assignmentCoordinatorFixture.ts
diff --git a/test/main/presenter/agentSessionPresenter/projectionCoordinatorFixture.ts b/test/main/presenter/sessionApplication/projectionCoordinatorFixture.ts
similarity index 100%
rename from test/main/presenter/agentSessionPresenter/projectionCoordinatorFixture.ts
rename to test/main/presenter/sessionApplication/projectionCoordinatorFixture.ts
diff --git a/test/main/presenter/agentSessionPresenter/integration.test.ts b/test/main/presenter/sessionApplication/runtimeIntegration.test.ts
similarity index 93%
rename from test/main/presenter/agentSessionPresenter/integration.test.ts
rename to test/main/presenter/sessionApplication/runtimeIntegration.test.ts
index 5ba4e5d79..d28c1b88b 100644
--- a/test/main/presenter/agentSessionPresenter/integration.test.ts
+++ b/test/main/presenter/sessionApplication/runtimeIntegration.test.ts
@@ -1,6 +1,5 @@
import { AppSessionService } from '@/agent/shared/appSessionService'
import { describe, it, expect, vi, beforeEach } from 'vitest'
-import { AgentSessionPresenter } from '@/presenter/agentSessionPresenter/index'
import { AgentRuntimePresenter } from '@/presenter/agentRuntimePresenter/index'
import { estimateMessagesTokens } from '@/presenter/agentRuntimePresenter/contextBuilder'
import { NewSessionHooksBridge } from '@/presenter/hooksNotifications/newSessionBridge'
@@ -676,7 +675,9 @@ describe('Integration: createSession end-to-end', () => {
let sqlitePresenter: ReturnType
let llmProvider: ReturnType
let configPresenter: ReturnType
- let agentPresenter: AgentSessionPresenter
+ let lifecycle: ReturnType['lifecycle']
+ let turn: ReturnType['turn']
+ let projection: ReturnType
beforeEach(() => {
vi.clearAllMocks()
@@ -703,7 +704,7 @@ describe('Integration: createSession end-to-end', () => {
transcriptMutation: deepchatAgent,
tape: deepchatAgent
}
- const projection = createProjectionCoordinatorFixture({
+ projection = createProjectionCoordinatorFixture({
agentManager,
appSessionService,
llmProviderPresenter: llmProvider,
@@ -720,16 +721,12 @@ describe('Integration: createSession end-to-end', () => {
projection,
acp: llmProvider
})
- agentPresenter = new AgentSessionPresenter(
- projection,
- sessionApplications.lifecycle,
- sessionApplications.assignment,
- sessionApplications.turn
- )
+ lifecycle = sessionApplications.lifecycle
+ turn = sessionApplications.turn
})
it('createSession → new_sessions row + deepchat_sessions row + messages + events', async () => {
- const session = await agentPresenter.createSession(
+ const session = await lifecycle.createSession(
{
agentId: 'deepchat',
message: 'Tell me a joke',
@@ -805,26 +802,23 @@ describe('Integration: createSession end-to-end', () => {
})
it('session list returns enriched sessions', async () => {
- await agentPresenter.createSession({ agentId: 'deepchat', message: 'Hello' }, 1)
+ await lifecycle.createSession({ agentId: 'deepchat', message: 'Hello' }, 1)
// Wait for processMessage to complete
await new Promise((r) => setTimeout(r, 50))
- const sessions = await agentPresenter.getSessionList()
+ const sessions = await projection.listSessions()
expect(sessions).toHaveLength(1)
expect(sessions[0].status).toBe('idle')
expect(sessions[0].providerId).toBe('openai')
})
it('deleteSession cleans up all data', async () => {
- const session = await agentPresenter.createSession(
- { agentId: 'deepchat', message: 'To delete' },
- 1
- )
+ const session = await lifecycle.createSession({ agentId: 'deepchat', message: 'To delete' }, 1)
await new Promise((r) => setTimeout(r, 50))
- await agentPresenter.deleteSession(session.id)
+ await lifecycle.deleteSession(session.id)
expect(sqlitePresenter.deepchatMessagesTable.deleteBySession).toHaveBeenCalledWith(session.id)
expect(sqlitePresenter.deepchatSessionsTable.delete).toHaveBeenCalledWith(session.id)
@@ -832,14 +826,11 @@ describe('Integration: createSession end-to-end', () => {
})
it('clearSessionMessages clears messages but keeps session row', async () => {
- const session = await agentPresenter.createSession(
- { agentId: 'deepchat', message: 'To clear' },
- 1
- )
+ const session = await lifecycle.createSession({ agentId: 'deepchat', message: 'To clear' }, 1)
await new Promise((r) => setTimeout(r, 50))
- await agentPresenter.clearSessionMessages(session.id)
+ await turn.clearSessionMessages(session.id)
expect(sqlitePresenter.deepchatMessagesTable.deleteBySession).toHaveBeenCalledWith(session.id)
expect(sqlitePresenter.newSessionsTable.delete).not.toHaveBeenCalledWith(session.id)
@@ -853,7 +844,7 @@ describe('Integration: ACP hooks bridge', () => {
let sqlitePresenter: ReturnType
let llmProvider: ReturnType
let configPresenter: ReturnType
- let agentPresenter: AgentSessionPresenter
+ let lifecycle: ReturnType['lifecycle']
let hookDispatcher: { dispatchEvent: ReturnType }
beforeEach(() => {
@@ -901,16 +892,11 @@ describe('Integration: ACP hooks bridge', () => {
projection,
acp: llmProvider
})
- agentPresenter = new AgentSessionPresenter(
- projection,
- sessionApplications.lifecycle,
- sessionApplications.assignment,
- sessionApplications.turn
- )
+ lifecycle = sessionApplications.lifecycle
})
it('dispatches lifecycle hooks for ACP sessions through the new bridge', async () => {
- const session = await agentPresenter.createSession(
+ const session = await lifecycle.createSession(
{
agentId: 'coder',
providerId: 'acp',
@@ -966,7 +952,10 @@ describe('Integration: multi-turn context', () => {
let llmProvider: ReturnType
let configPresenter: ReturnType
let deepchatAgent: AgentRuntimePresenter
- let agentPresenter: AgentSessionPresenter
+ let lifecycle: ReturnType['lifecycle']
+ let turn: ReturnType['turn']
+ let assignment: ReturnType['assignment']
+ let projection: ReturnType
beforeEach(() => {
vi.clearAllMocks()
@@ -993,7 +982,7 @@ describe('Integration: multi-turn context', () => {
transcriptMutation: deepchatAgent,
tape: deepchatAgent
}
- const projection = createProjectionCoordinatorFixture({
+ projection = createProjectionCoordinatorFixture({
agentManager,
appSessionService,
llmProviderPresenter: llmProvider,
@@ -1010,17 +999,14 @@ describe('Integration: multi-turn context', () => {
projection,
acp: llmProvider
})
- agentPresenter = new AgentSessionPresenter(
- projection,
- sessionApplications.lifecycle,
- sessionApplications.assignment,
- sessionApplications.turn
- )
+ lifecycle = sessionApplications.lifecycle
+ turn = sessionApplications.turn
+ assignment = sessionApplications.assignment
})
it('second message includes first exchange in LLM context', async () => {
// Send first message
- const session = await agentPresenter.createSession(
+ const session = await lifecycle.createSession(
{ agentId: 'deepchat', message: 'Hello', projectDir: null },
1
)
@@ -1029,7 +1015,7 @@ describe('Integration: multi-turn context', () => {
await new Promise((r) => setTimeout(r, 50))
// Send second message
- await agentPresenter.sendMessage(session.id, 'Follow up question')
+ await turn.sendMessage(session.id, 'Follow up question')
// Wait for second processMessage to complete
await new Promise((r) => setTimeout(r, 50))
@@ -1093,16 +1079,16 @@ describe('Integration: multi-turn context', () => {
})
it('supports both string and object sendMessage input', async () => {
- const session = await agentPresenter.createSession(
+ const session = await lifecycle.createSession(
{ agentId: 'deepchat', message: 'Hello', projectDir: null },
1
)
await new Promise((r) => setTimeout(r, 50))
- await agentPresenter.sendMessage(session.id, 'Follow up (string)')
+ await turn.sendMessage(session.id, 'Follow up (string)')
await new Promise((r) => setTimeout(r, 50))
- await agentPresenter.sendMessage(session.id, {
+ await turn.sendMessage(session.id, {
text: 'Follow up (object)',
files: [{ name: 'a.md', path: '/tmp/a.md', mimeType: 'text/markdown', content: '# a' } as any]
})
@@ -1130,7 +1116,7 @@ describe('Integration: multi-turn context', () => {
}
llmProvider.getProviderInstance.mockReturnValue(providerInstance)
- const session = await agentPresenter.createSession(
+ const session = await lifecycle.createSession(
{ agentId: 'deepchat', message: 'First turn', projectDir: null },
1
)
@@ -1166,16 +1152,16 @@ describe('Integration: multi-turn context', () => {
}
llmProvider.getProviderInstance.mockReturnValue(providerInstance)
- const session = await agentPresenter.createSession(
+ const session = await lifecycle.createSession(
{ agentId: 'deepchat', message: 'First turn', projectDir: null },
1
)
await new Promise((r) => setTimeout(r, 80))
- await agentPresenter.sendMessage(session.id, 'Immediate follow up')
+ await turn.sendMessage(session.id, 'Immediate follow up')
await new Promise((r) => setTimeout(r, 20))
- await expect(agentPresenter.listPendingInputs(session.id)).resolves.toEqual([])
+ await expect(turn.listPendingInputs(session.id)).resolves.toEqual([])
const messagesDuringSecondTurn = sqlitePresenter.deepchatMessagesTable.getBySession(session.id)
const userMessagesDuringSecondTurn = messagesDuringSecondTurn.filter(
@@ -1207,15 +1193,15 @@ describe('Integration: multi-turn context', () => {
}
llmProvider.getProviderInstance.mockReturnValue(providerInstance)
- const session = await agentPresenter.createSession(
+ const session = await lifecycle.createSession(
{ agentId: 'deepchat', message: 'First turn', projectDir: null },
1
)
await new Promise((r) => setTimeout(r, 20))
- await agentPresenter.queuePendingInput(session.id, 'Queued follow up')
+ await turn.queuePendingInput(session.id, 'Queued follow up')
- const pendingBeforeRelease = await agentPresenter.listPendingInputs(session.id)
+ const pendingBeforeRelease = await turn.listPendingInputs(session.id)
expect(pendingBeforeRelease).toHaveLength(1)
expect(pendingBeforeRelease[0].mode).toBe('queue')
@@ -1231,7 +1217,7 @@ describe('Integration: multi-turn context', () => {
const afterUserMessages = afterMessages.filter((message: any) => message.role === 'user')
expect(afterUserMessages).toHaveLength(2)
expect(JSON.parse(afterUserMessages[1].content).text).toBe('Queued follow up')
- await expect(agentPresenter.listPendingInputs(session.id)).resolves.toEqual([])
+ await expect(turn.listPendingInputs(session.id)).resolves.toEqual([])
})
it('drains converted steer inputs as visible user messages before queued messages', async () => {
@@ -1253,18 +1239,18 @@ describe('Integration: multi-turn context', () => {
}
llmProvider.getProviderInstance.mockReturnValue(providerInstance)
- const session = await agentPresenter.createSession(
+ const session = await lifecycle.createSession(
{ agentId: 'deepchat', message: 'Turn one', projectDir: null },
1
)
await new Promise((r) => setTimeout(r, 20))
- await agentPresenter.queuePendingInput(session.id, 'Steer instruction')
- await agentPresenter.queuePendingInput(session.id, 'Queued target')
+ await turn.queuePendingInput(session.id, 'Steer instruction')
+ await turn.queuePendingInput(session.id, 'Queued target')
- const pendingInputs = await agentPresenter.listPendingInputs(session.id)
+ const pendingInputs = await turn.listPendingInputs(session.id)
expect(pendingInputs).toHaveLength(2)
- await agentPresenter.convertPendingInputToSteer(session.id, pendingInputs[0].id)
+ await turn.convertPendingInputToSteer(session.id, pendingInputs[0].id)
releaseFirstTurn?.()
await vi.waitFor(() => {
@@ -1297,7 +1283,7 @@ describe('Integration: multi-turn context', () => {
'Steer instruction',
'Queued target'
])
- await expect(agentPresenter.listPendingInputs(session.id)).resolves.toEqual([])
+ await expect(turn.listPendingInputs(session.id)).resolves.toEqual([])
})
it('rebudgets long converted steer inputs as their own visible turn', async () => {
@@ -1322,18 +1308,18 @@ describe('Integration: multi-turn context', () => {
}
llmProvider.getProviderInstance.mockReturnValue(providerInstance)
- const session = await agentPresenter.createSession(
+ const session = await lifecycle.createSession(
{ agentId: 'deepchat', message: firstPrompt, projectDir: null },
1
)
await new Promise((r) => setTimeout(r, 20))
- await agentPresenter.updateSessionGenerationSettings(session.id, {
+ await assignment.updateSessionGenerationSettings(session.id, {
contextLength: 2048,
maxTokens: 128
})
- await agentPresenter.queuePendingInput(session.id, {
+ await turn.queuePendingInput(session.id, {
text: steerUserText,
files: [
{
@@ -1343,11 +1329,11 @@ describe('Integration: multi-turn context', () => {
} as any
]
})
- await agentPresenter.queuePendingInput(session.id, 'Queued target')
+ await turn.queuePendingInput(session.id, 'Queued target')
- const pendingInputs = await agentPresenter.listPendingInputs(session.id)
+ const pendingInputs = await turn.listPendingInputs(session.id)
expect(pendingInputs).toHaveLength(2)
- await agentPresenter.convertPendingInputToSteer(session.id, pendingInputs[0].id)
+ await turn.convertPendingInputToSteer(session.id, pendingInputs[0].id)
releaseFirstTurn?.()
await vi.waitFor(() => {
@@ -1483,19 +1469,19 @@ describe('Integration: multi-turn context', () => {
}
llmProvider.getProviderInstance.mockReturnValue(providerInstance)
- const session = await agentPresenter.createSession(
+ const session = await lifecycle.createSession(
{ agentId: 'deepchat', message: 'Fail first', projectDir: null },
1
)
await new Promise((r) => setTimeout(r, 50))
- const failedSession = await agentPresenter.getSession(session.id)
+ const failedSession = await projection.getSession(session.id)
expect(failedSession?.status).toBe('error')
- await agentPresenter.sendMessage(session.id, 'Recover after error')
+ await turn.sendMessage(session.id, 'Recover after error')
await new Promise((r) => setTimeout(r, 80))
- const recoveredSession = await agentPresenter.getSession(session.id)
+ const recoveredSession = await projection.getSession(session.id)
expect(recoveredSession?.status).toBe('idle')
expect(providerInstance.coreStream).toHaveBeenCalledTimes(2)
@@ -1503,7 +1489,7 @@ describe('Integration: multi-turn context', () => {
const userMessages = messages.filter((message: any) => message.role === 'user')
expect(userMessages).toHaveLength(2)
expect(JSON.parse(userMessages[1].content).text).toBe('Recover after error')
- await expect(agentPresenter.listPendingInputs(session.id)).resolves.toEqual([])
+ await expect(turn.listPendingInputs(session.id)).resolves.toEqual([])
})
it('drains queued turns when a new message is enqueued after a session error', async () => {
@@ -1524,30 +1510,30 @@ describe('Integration: multi-turn context', () => {
}
llmProvider.getProviderInstance.mockReturnValue(providerInstance)
- const session = await agentPresenter.createSession(
+ const session = await lifecycle.createSession(
{ agentId: 'deepchat', message: 'Turn that errors', projectDir: null },
1
)
await new Promise((r) => setTimeout(r, 20))
- await agentPresenter.queuePendingInput(session.id, 'Queued while failing')
+ await turn.queuePendingInput(session.id, 'Queued while failing')
releaseFirstTurn?.()
await new Promise((r) => setTimeout(r, 80))
- const failedSession = await agentPresenter.getSession(session.id)
+ const failedSession = await projection.getSession(session.id)
expect(failedSession?.status).toBe('error')
- const pendingAfterError = await agentPresenter.listPendingInputs(session.id)
+ const pendingAfterError = await turn.listPendingInputs(session.id)
expect(pendingAfterError).toHaveLength(1)
expect(pendingAfterError[0].mode).toBe('queue')
// Enqueuing from an errored session drains the backlog (no manual resume step).
- await agentPresenter.queuePendingInput(session.id, 'New message after error')
+ await turn.queuePendingInput(session.id, 'New message after error')
await vi.waitFor(() => {
expect(providerInstance.coreStream).toHaveBeenCalledTimes(3)
})
- const recoveredSession = await agentPresenter.getSession(session.id)
+ const recoveredSession = await projection.getSession(session.id)
expect(recoveredSession?.status).toBe('idle')
expect(providerInstance.coreStream).toHaveBeenCalledTimes(3)
@@ -1556,7 +1542,7 @@ describe('Integration: multi-turn context', () => {
expect(userMessages).toHaveLength(3)
expect(JSON.parse(userMessages[1].content).text).toBe('Queued while failing')
expect(JSON.parse(userMessages[2].content).text).toBe('New message after error')
- await expect(agentPresenter.listPendingInputs(session.id)).resolves.toEqual([])
+ await expect(turn.listPendingInputs(session.id)).resolves.toEqual([])
})
})
diff --git a/test/main/presenter/agentSessionPresenter/agentSessionPresenter.test.ts b/test/main/presenter/sessionApplication/sessionApplication.integration.test.ts
similarity index 92%
rename from test/main/presenter/agentSessionPresenter/agentSessionPresenter.test.ts
rename to test/main/presenter/sessionApplication/sessionApplication.integration.test.ts
index 047db1e41..e9eaec676 100644
--- a/test/main/presenter/agentSessionPresenter/agentSessionPresenter.test.ts
+++ b/test/main/presenter/sessionApplication/sessionApplication.integration.test.ts
@@ -7,7 +7,6 @@ import { AgentUnavailableError } from '@/agent/shared/agentCatalogCodec'
import type { AcpAgentDescriptor } from '@/agent/shared/agentDescriptors'
import type { AppSessionId } from '@/agent/shared/agentSessionIds'
import { AgentRepository } from '@/presenter/agentRepository'
-import { AgentSessionPresenter } from '@/presenter/agentSessionPresenter/index'
import { resolveAcpAgentAlias } from '@shared/utils/acpAgentAlias'
import { createDeepChatAgentBackendFixture } from '../../agent/manager/deepChatAgentBackendFixture'
import { createProjectionCoordinatorFixture } from './projectionCoordinatorFixture'
@@ -598,16 +597,11 @@ function createDescriptorIndependentDeleteHarness(options: {
skillPresenter,
sessionPermissionPort
})
- const presenter = new AgentSessionPresenter(
- projection,
- sessionApplications.lifecycle,
- sessionApplications.assignment,
- sessionApplications.turn,
- { sessionPermissionPort }
- )
-
return {
- presenter,
+ lifecycle: sessionApplications.lifecycle,
+ turn: sessionApplications.turn,
+ assignment: sessionApplications.assignment,
+ projection,
manager,
sessions,
deleteSessionRow,
@@ -621,7 +615,7 @@ function createDescriptorIndependentDeleteHarness(options: {
}
}
-describe('AgentSessionPresenter', () => {
+describe('Session application coordinators', () => {
let deepChatAgent: ReturnType
let llmProviderPresenter: ReturnType
let configPresenter: ReturnType
@@ -640,7 +634,10 @@ describe('AgentSessionPresenter', () => {
resolveSubagentFacet: ReturnType
cleanupSessionBackends: ReturnType
}
- let presenter: AgentSessionPresenter
+ let lifecycle: ReturnType['lifecycle']
+ let turn: ReturnType['turn']
+ let assignment: ReturnType['assignment']
+ let projection: ReturnType
beforeEach(() => {
vi.clearAllMocks()
@@ -730,7 +727,7 @@ describe('AgentSessionPresenter', () => {
transcriptMutation: deepChatAgent,
tape: deepChatAgent
} as any
- const projection = createProjectionCoordinatorFixture({
+ projection = createProjectionCoordinatorFixture({
agentManager: agentManager as any,
appSessionService,
llmProviderPresenter,
@@ -749,12 +746,9 @@ describe('AgentSessionPresenter', () => {
acp: llmProviderPresenter,
skillPresenter
})
- presenter = new AgentSessionPresenter(
- projection,
- sessionApplications.lifecycle,
- sessionApplications.assignment,
- sessionApplications.turn
- )
+ lifecycle = sessionApplications.lifecycle
+ turn = sessionApplications.turn
+ assignment = sessionApplications.assignment
})
it('routes public session operations through the real catalog and manager chain', async () => {
@@ -1001,23 +995,18 @@ describe('AgentSessionPresenter', () => {
acp: llmProviderPresenter,
skillPresenter
})
- const integratedPresenter = new AgentSessionPresenter(
- projection,
- sessionApplications.lifecycle,
- sessionApplications.assignment,
- sessionApplications.turn
- )
-
- await expect(integratedPresenter.sendMessage('deepchat-session', 'Hello')).resolves.toEqual({
+ await expect(
+ sessionApplications.turn.sendMessage('deepchat-session', 'Hello')
+ ).resolves.toEqual({
requestId: null,
messageId: null
})
- await expect(integratedPresenter.sendMessage('acp-session', 'Hello')).resolves.toEqual({
+ await expect(sessionApplications.turn.sendMessage('acp-session', 'Hello')).resolves.toEqual({
requestId: null,
messageId: null
})
- await integratedPresenter.queuePendingInput('acp-session', 'Later')
- await integratedPresenter.steerActiveTurn('acp-session', 'Now')
+ await sessionApplications.turn.queuePendingInput('acp-session', 'Later')
+ await sessionApplications.turn.steerActiveTurn('acp-session', 'Now')
await new Promise((resolve) => setTimeout(resolve, 0))
expect(deepchatImplementation.queuePendingInput).toHaveBeenCalledWith(
'deepchat-session',
@@ -1047,21 +1036,20 @@ describe('AgentSessionPresenter', () => {
'claude-acp'
)
await expect(
- integratedPresenter.sendMessage('unconfigured-session', 'Hello')
+ sessionApplications.turn.sendMessage('unconfigured-session', 'Hello')
).rejects.toMatchObject({ code: 'AGENT_UNAVAILABLE', reason: 'invalid-config' })
- await expect(integratedPresenter.deleteSession('unconfigured-session')).resolves.toBeUndefined()
+ await expect(
+ sessionApplications.lifecycle.deleteSession('unconfigured-session')
+ ).resolves.toBeUndefined()
expect(directAcpRuntime.cleanupSession).toHaveBeenCalledWith('unconfigured-session')
expect(deepchatImplementation.queuePendingInput).toHaveBeenCalledTimes(1)
expect(llmProviderPresenter.setAcpWorkdir).not.toHaveBeenCalled()
- await expect(integratedPresenter.sendMessage('missing-session', 'Hello')).rejects.toMatchObject(
- {
- code: 'AGENT_NOT_FOUND'
- }
- )
- await expect(integratedPresenter.sendMessage('broken-session', 'Hello')).rejects.toMatchObject({
- code: 'AGENT_UNAVAILABLE',
- reason: 'missing-manual-command'
- })
+ await expect(
+ sessionApplications.turn.sendMessage('missing-session', 'Hello')
+ ).rejects.toMatchObject({ code: 'AGENT_NOT_FOUND' })
+ await expect(
+ sessionApplications.turn.sendMessage('broken-session', 'Hello')
+ ).rejects.toMatchObject({ code: 'AGENT_UNAVAILABLE', reason: 'missing-manual-command' })
})
it.each([
@@ -1130,7 +1118,7 @@ describe('AgentSessionPresenter', () => {
agents: agent ? [agent] : []
})
- await expect(harness.presenter.deleteSession('delete-target')).resolves.toBeUndefined()
+ await expect(harness.lifecycle.deleteSession('delete-target')).resolves.toBeUndefined()
expect(harness.resolveExecutableDescriptor).not.toHaveBeenCalled()
expect(harness.resolveInput).not.toHaveBeenCalled()
@@ -1194,7 +1182,7 @@ describe('AgentSessionPresenter', () => {
]
})
- await expect(harness.presenter.deleteSession('parent-session')).resolves.toBeUndefined()
+ await expect(harness.lifecycle.deleteSession('parent-session')).resolves.toBeUndefined()
expect(harness.resolveExecutableDescriptor).not.toHaveBeenCalled()
expect(harness.resolveInput).not.toHaveBeenCalled()
@@ -1230,7 +1218,7 @@ describe('AgentSessionPresenter', () => {
await resolved.handle.snapshot()
harness.resolveExecutableDescriptor.mockClear()
- await expect(harness.presenter.deleteSession('deepchat-session')).resolves.toBeUndefined()
+ await expect(harness.lifecycle.deleteSession('deepchat-session')).resolves.toBeUndefined()
expect(harness.resolveExecutableDescriptor).not.toHaveBeenCalled()
expect(harness.deepchatImplementation.cancelGeneration).toHaveBeenCalledExactlyOnceWith(
@@ -1260,7 +1248,7 @@ describe('AgentSessionPresenter', () => {
})
harness.resolveExecutableDescriptor.mockClear()
- await expect(harness.presenter.deleteSession('direct-session')).resolves.toBeUndefined()
+ await expect(harness.lifecycle.deleteSession('direct-session')).resolves.toBeUndefined()
expect(harness.resolveExecutableDescriptor).not.toHaveBeenCalled()
expect(harness.resolveInput).not.toHaveBeenCalled()
@@ -1274,7 +1262,7 @@ describe('AgentSessionPresenter', () => {
describe('createSession', () => {
it('creates session with correct parameters', async () => {
- const result = await presenter.createSession(
+ const result = await lifecycle.createSession(
{ agentId: 'deepchat', message: 'Hello world', projectDir: '/tmp/proj' },
1
)
@@ -1307,20 +1295,20 @@ describe('AgentSessionPresenter', () => {
it('derives title from first 50 chars of message', async () => {
const longMessage = 'A'.repeat(100)
- const result = await presenter.createSession({ agentId: 'deepchat', message: longMessage }, 1)
+ const result = await lifecycle.createSession({ agentId: 'deepchat', message: longMessage }, 1)
expect(result.title).toBe('A'.repeat(50))
})
it('defaults to "New Chat" when message is empty', async () => {
- const result = await presenter.createSession({ agentId: 'deepchat', message: '' }, 1)
+ const result = await lifecycle.createSession({ agentId: 'deepchat', message: '' }, 1)
expect(result.title).toBe('New Chat')
expect(llmProviderPresenter.summaryTitles).not.toHaveBeenCalled()
})
it('calls agent.initSession and queues the first message', async () => {
- await presenter.createSession({ agentId: 'deepchat', message: 'Hello' }, 1)
+ await lifecycle.createSession({ agentId: 'deepchat', message: 'Hello' }, 1)
expect(deepChatAgent.initSession).toHaveBeenCalledWith(
'mock-session-id',
@@ -1359,7 +1347,7 @@ describe('AgentSessionPresenter', () => {
})
deepChatAgent.queuePendingInput.mockImplementation(queuePendingInput)
- await presenter.createSession(
+ await lifecycle.createSession(
{ agentId: 'deepchat', message: 'Hello', projectDir: '/tmp/proj' },
1
)
@@ -1373,7 +1361,7 @@ describe('AgentSessionPresenter', () => {
})
it('publishes typed created session update', async () => {
- await presenter.createSession({ agentId: 'deepchat', message: 'Hello' }, 42)
+ await lifecycle.createSession({ agentId: 'deepchat', message: 'Hello' }, 42)
expectSessionsUpdated({
sessionIds: ['mock-session-id'],
@@ -1384,7 +1372,7 @@ describe('AgentSessionPresenter', () => {
})
it('uses default provider/model from config when not specified', async () => {
- await presenter.createSession({ agentId: 'deepchat', message: 'Hi' }, 1)
+ await lifecycle.createSession({ agentId: 'deepchat', message: 'Hi' }, 1)
expect(deepChatAgent.initSession).toHaveBeenCalledWith(
expect.any(String),
@@ -1403,7 +1391,7 @@ describe('AgentSessionPresenter', () => {
defaultProjectPath: '/workspaces/agent-default'
})
- await presenter.createSession({ agentId: 'deepchat', message: 'Hi' }, 1)
+ await lifecycle.createSession({ agentId: 'deepchat', message: 'Hi' }, 1)
expect(deepChatAgent.initSession).toHaveBeenCalledWith(
expect.any(String),
@@ -1424,7 +1412,7 @@ describe('AgentSessionPresenter', () => {
configPresenter.resolveDeepChatAgentConfig.mockResolvedValue({})
configPresenter.getDefaultProjectPath.mockReturnValue('/workspaces/global-default')
- await presenter.createSession({ agentId: 'deepchat', message: 'Hi' }, 1)
+ await lifecycle.createSession({ agentId: 'deepchat', message: 'Hi' }, 1)
expect(deepChatAgent.initSession).toHaveBeenCalledWith(
expect.any(String),
@@ -1447,7 +1435,7 @@ describe('AgentSessionPresenter', () => {
})
configPresenter.getDefaultProjectPath.mockReturnValue('/workspaces/global-default')
- await presenter.createSession({ agentId: 'deepchat', message: 'Hi', projectDir: null }, 1)
+ await lifecycle.createSession({ agentId: 'deepchat', message: 'Hi', projectDir: null }, 1)
expect(deepChatAgent.initSession).toHaveBeenCalledWith(
expect.any(String),
@@ -1465,7 +1453,7 @@ describe('AgentSessionPresenter', () => {
})
it('uses input provider/model when specified', async () => {
- await presenter.createSession(
+ await lifecycle.createSession(
{ agentId: 'deepchat', message: 'Hi', providerId: 'anthropic', modelId: 'claude-3' },
1
)
@@ -1483,7 +1471,7 @@ describe('AgentSessionPresenter', () => {
})
it('uses input permission mode when specified', async () => {
- await presenter.createSession(
+ await lifecycle.createSession(
{
agentId: 'deepchat',
message: 'Hi',
@@ -1507,7 +1495,7 @@ describe('AgentSessionPresenter', () => {
})
it('passes generationSettings to agent.initSession', async () => {
- await presenter.createSession(
+ await lifecycle.createSession(
{
agentId: 'deepchat',
message: 'Hi',
@@ -1542,7 +1530,7 @@ describe('AgentSessionPresenter', () => {
})
it('persists disabled agent tools for deepchat sessions', async () => {
- await presenter.createSession(
+ await lifecycle.createSession(
{
agentId: 'deepchat',
message: 'Hi',
@@ -1567,12 +1555,12 @@ describe('AgentSessionPresenter', () => {
configPresenter.getDefaultModel.mockReturnValue(null)
await expect(
- presenter.createSession({ agentId: 'deepchat', message: 'Hi' }, 1)
+ lifecycle.createSession({ agentId: 'deepchat', message: 'Hi' }, 1)
).rejects.toThrow('No provider or model configured')
})
it('passes active skills as initial message-scoped skills without pinning the session', async () => {
- await presenter.createSession(
+ await lifecycle.createSession(
{
agentId: 'deepchat',
message: 'Hello',
@@ -1648,7 +1636,7 @@ describe('AgentSessionPresenter', () => {
}
])
- await presenter.createSession({ agentId: 'deepchat', message: 'Please summarize' }, 1)
+ await lifecycle.createSession({ agentId: 'deepchat', message: 'Please summarize' }, 1)
await new Promise((r) => setTimeout(r, 20))
expect(llmProviderPresenter.summaryTitles).toHaveBeenCalled()
@@ -1705,7 +1693,7 @@ describe('AgentSessionPresenter', () => {
vi.useFakeTimers()
try {
- await presenter.createSession({ agentId: 'deepchat', message: 'Please summarize' }, 1)
+ await lifecycle.createSession({ agentId: 'deepchat', message: 'Please summarize' }, 1)
await vi.advanceTimersByTimeAsync(20)
expect(llmProviderPresenter.summaryTitles).not.toHaveBeenCalled()
@@ -1789,7 +1777,7 @@ describe('AgentSessionPresenter', () => {
vi.useFakeTimers()
try {
- await presenter.createSession({ agentId: 'deepchat', message: 'Please summarize' }, 1)
+ await lifecycle.createSession({ agentId: 'deepchat', message: 'Please summarize' }, 1)
await vi.advanceTimersByTimeAsync(20)
expect(llmProviderPresenter.summaryTitles).not.toHaveBeenCalled()
@@ -1816,8 +1804,8 @@ describe('AgentSessionPresenter', () => {
})
)
- await presenter.createSession({ agentId: 'deepchat', message: 'Original prompt' }, 1)
- await presenter.renameSession('mock-session-id', 'Manual title')
+ await lifecycle.createSession({ agentId: 'deepchat', message: 'Original prompt' }, 1)
+ await projection.renameSession('mock-session-id', 'Manual title')
resolveMessages([
{
id: 'u1',
@@ -1854,9 +1842,9 @@ describe('AgentSessionPresenter', () => {
})
)
- await presenter.createSession({ agentId: 'deepchat', message: 'Original prompt' }, 1)
+ await lifecycle.createSession({ agentId: 'deepchat', message: 'Original prompt' }, 1)
await vi.waitFor(() => expect(llmProviderPresenter.summaryTitles).toHaveBeenCalledOnce())
- await presenter.renameSession('mock-session-id', 'Manual title')
+ await projection.renameSession('mock-session-id', 'Manual title')
resolveTitle('Generated title')
await new Promise((resolve) => setTimeout(resolve, 0))
@@ -1885,7 +1873,7 @@ describe('AgentSessionPresenter', () => {
.mockRejectedValueOnce(new Error('assistant unavailable'))
.mockResolvedValueOnce('Fallback title')
- await presenter.createSession(
+ await lifecycle.createSession(
{
agentId: 'deepchat',
message: 'Original prompt',
@@ -1925,7 +1913,7 @@ describe('AgentSessionPresenter', () => {
permissionMode: 'full_access'
})
- await presenter.createSession(
+ await lifecycle.createSession(
{
agentId: 'acp-coder',
message: 'Hello ACP',
@@ -1966,7 +1954,7 @@ describe('AgentSessionPresenter', () => {
llmProviderPresenter.setAcpWorkdir.mockRejectedValueOnce(new Error('sync failed'))
await expect(
- presenter.createSession(
+ lifecycle.createSession(
{
agentId: 'acp-coder',
message: 'Hello ACP',
@@ -1994,7 +1982,7 @@ describe('AgentSessionPresenter', () => {
deepChatAgent.destroySession.mockRejectedValueOnce(closeError)
await expect(
- presenter.createSession(
+ lifecycle.createSession(
{
agentId: 'deepchat',
message: 'Hello ACP',
@@ -2015,7 +2003,7 @@ describe('AgentSessionPresenter', () => {
expect(deepChatAgent.destroySession.mock.invocationCallOrder[0]).toBeLessThan(
sqlitePresenter.newSessionsTable.delete.mock.invocationCallOrder[0]
)
- expect(presenter.getActiveSessionId(1)).toBeNull()
+ expect(projection.getActiveId(1)).toBeNull()
expect(publishDeepchatEvent).not.toHaveBeenCalled()
expect(sessionUiPort.refreshSessionUi).not.toHaveBeenCalled()
expect(warnSpy).toHaveBeenCalledWith(
@@ -2032,7 +2020,7 @@ describe('AgentSessionPresenter', () => {
describe('createDetachedSession', () => {
it('creates a detached session without window activation', async () => {
- const result = await presenter.createDetachedSession({
+ const result = await lifecycle.createDetachedSession({
title: 'Remote Session',
agentId: 'deepchat'
})
@@ -2065,7 +2053,7 @@ describe('AgentSessionPresenter', () => {
})
configPresenter.getAgentType.mockResolvedValue('deepchat')
- await presenter.createDetachedSession({
+ await lifecycle.createDetachedSession({
title: 'Remote Agent Session',
agentId: 'deepchat-remote'
})
@@ -2102,9 +2090,9 @@ describe('AgentSessionPresenter', () => {
describe('draft turn promotion failures', () => {
it.each([
- ['send', () => presenter.sendMessage('s-draft', 'New prompt')],
- ['steer', () => presenter.steerActiveTurn('s-draft', 'New prompt')],
- ['queue', () => presenter.queuePendingInput('s-draft', 'New prompt')]
+ ['send', () => turn.sendMessage('s-draft', 'New prompt')],
+ ['steer', () => turn.steerActiveTurn('s-draft', 'New prompt')],
+ ['queue', () => turn.queuePendingInput('s-draft', 'New prompt')]
])('does not roll back draft promotion when %s fails later', async (_name, invoke) => {
const row = {
id: 's-draft',
@@ -2170,7 +2158,7 @@ describe('AgentSessionPresenter', () => {
permissionMode: 'full_access'
})
- await presenter.sendMessage('s-draft', 'Hello ACP')
+ await turn.sendMessage('s-draft', 'Hello ACP')
expect(sqlitePresenter.newSessionsTable.update).toHaveBeenCalledWith('s-draft', {
is_draft: 0,
@@ -2202,7 +2190,7 @@ describe('AgentSessionPresenter', () => {
})
deepChatAgent.hasMessages.mockResolvedValue(true)
- await presenter.sendMessage('s1', 'Follow-up')
+ await turn.sendMessage('s1', 'Follow-up')
expect(agentManager.resolveSessionHandle).toHaveBeenCalledWith('s1')
expect(deepChatAgent.queuePendingInput).toHaveBeenCalledWith(
's1',
@@ -2242,7 +2230,7 @@ describe('AgentSessionPresenter', () => {
}
})
- await presenter.sendMessage('s1', 'Scheduled prompt', { maxProviderRounds: 4 })
+ await turn.sendMessage('s1', 'Scheduled prompt', { maxProviderRounds: 4 })
expect(send).toHaveBeenCalledWith({
content: { text: 'Scheduled prompt', files: [] },
@@ -2269,7 +2257,7 @@ describe('AgentSessionPresenter', () => {
permissionMode: 'full_access'
})
- await presenter.sendMessage('s1', 'Refine this')
+ await turn.sendMessage('s1', 'Refine this')
expect(deepChatAgent.queuePendingInput).toHaveBeenCalledWith(
's1',
@@ -2284,9 +2272,7 @@ describe('AgentSessionPresenter', () => {
it('throws for unknown session', async () => {
sqlitePresenter.newSessionsTable.get.mockReturnValue(undefined)
- await expect(presenter.sendMessage('unknown', 'hi')).rejects.toThrow(
- 'Session not found: unknown'
- )
+ await expect(turn.sendMessage('unknown', 'hi')).rejects.toThrow('Session not found: unknown')
})
})
@@ -2316,7 +2302,7 @@ describe('AgentSessionPresenter', () => {
updated_at: 1000
})
- await presenter.queuePendingInput('s1', 'Later')
+ await turn.queuePendingInput('s1', 'Later')
expect(queuePendingInput).toHaveBeenCalledWith(
's1',
@@ -2350,11 +2336,11 @@ describe('AgentSessionPresenter', () => {
deepChatAgent.convertPendingInputToSteer.mockResolvedValueOnce(converted)
deepChatAgent.steerPendingInput.mockResolvedValueOnce(steered)
- await expect(presenter.updateQueuedInput('s1', 'pending-1', 'Updated')).resolves.toBe(updated)
- await expect(presenter.moveQueuedInput('s1', 'pending-1', 2)).resolves.toBe(moved)
- await expect(presenter.convertPendingInputToSteer('s1', 'pending-1')).resolves.toBe(converted)
- await expect(presenter.steerPendingInput('s1', 'pending-1')).resolves.toBe(steered)
- await expect(presenter.deletePendingInput('s1', 'pending-1')).resolves.toBeUndefined()
+ await expect(turn.updateQueuedInput('s1', 'pending-1', 'Updated')).resolves.toBe(updated)
+ await expect(turn.moveQueuedInput('s1', 'pending-1', 2)).resolves.toBe(moved)
+ await expect(turn.convertPendingInputToSteer('s1', 'pending-1')).resolves.toBe(converted)
+ await expect(turn.steerPendingInput('s1', 'pending-1')).resolves.toBe(steered)
+ await expect(turn.deletePendingInput('s1', 'pending-1')).resolves.toBeUndefined()
expect(deepChatAgent.updateQueuedInput).toHaveBeenCalledWith('s1', 'pending-1', {
text: 'Updated',
@@ -2367,11 +2353,11 @@ describe('AgentSessionPresenter', () => {
})
it.each([
- ['update', () => presenter.updateQueuedInput('missing', 'pending-1', 'Updated')],
- ['move', () => presenter.moveQueuedInput('missing', 'pending-1', 1)],
- ['convert', () => presenter.convertPendingInputToSteer('missing', 'pending-1')],
- ['steer', () => presenter.steerPendingInput('missing', 'pending-1')],
- ['delete', () => presenter.deletePendingInput('missing', 'pending-1')]
+ ['update', () => turn.updateQueuedInput('missing', 'pending-1', 'Updated')],
+ ['move', () => turn.moveQueuedInput('missing', 'pending-1', 1)],
+ ['convert', () => turn.convertPendingInputToSteer('missing', 'pending-1')],
+ ['steer', () => turn.steerPendingInput('missing', 'pending-1')],
+ ['delete', () => turn.deletePendingInput('missing', 'pending-1')]
])('rejects %s before resolving a handle for a missing session', async (_name, invoke) => {
sqlitePresenter.newSessionsTable.get.mockReturnValue(undefined)
@@ -2383,7 +2369,7 @@ describe('AgentSessionPresenter', () => {
it('returns an empty pending list for a missing session without resolving a handle', async () => {
sqlitePresenter.newSessionsTable.get.mockReturnValue(undefined)
- await expect(presenter.listPendingInputs('missing')).resolves.toEqual([])
+ await expect(turn.listPendingInputs('missing')).resolves.toEqual([])
expect(agentManager.resolveSessionHandle).not.toHaveBeenCalled()
})
@@ -2409,7 +2395,7 @@ describe('AgentSessionPresenter', () => {
projectDir: '/retry/project'
})
- await presenter.retryMessage('s1', 'message-1')
+ await turn.retryMessage('s1', 'message-1')
expect(deepChatAgent.prepareRetryMessage).toHaveBeenCalledWith('s1', 'message-1')
expect(deepChatAgent.processMessage).toHaveBeenCalledWith(
@@ -2427,12 +2413,12 @@ describe('AgentSessionPresenter', () => {
const cancelError = new Error('cancel failed')
deepChatAgent.cancelGeneration.mockRejectedValueOnce(cancelError)
- await expect(presenter.deleteMessage('s1', 'message-1')).rejects.toBe(cancelError)
+ await expect(turn.deleteMessage('s1', 'message-1')).rejects.toBe(cancelError)
expect(deepChatAgent.deleteMessage).not.toHaveBeenCalled()
deepChatAgent.cancelGeneration.mockResolvedValueOnce(undefined)
- await presenter.deleteMessage('s1', 'message-1')
+ await turn.deleteMessage('s1', 'message-1')
expect(deepChatAgent.deleteMessage).toHaveBeenCalledWith('s1', 'message-1')
expect(deepChatAgent.cancelGeneration.mock.invocationCallOrder.at(-1)).toBeLessThan(
@@ -2444,7 +2430,7 @@ describe('AgentSessionPresenter', () => {
const edited = { id: 'message-1', sessionId: 's1', role: 'user', content: 'Edited' }
deepChatAgent.editUserMessage.mockResolvedValueOnce(edited)
- await expect(presenter.editUserMessage('s1', 'message-1', 'Edited')).resolves.toBe(edited)
+ await expect(turn.editUserMessage('s1', 'message-1', 'Edited')).resolves.toBe(edited)
expect(deepChatAgent.editUserMessage).toHaveBeenCalledWith('s1', 'message-1', 'Edited')
expect(deepChatAgent.cancelGeneration).not.toHaveBeenCalled()
@@ -2470,7 +2456,7 @@ describe('AgentSessionPresenter', () => {
}
})
- await presenter.setSessionProjectDir('s1', '/tmp/workspace')
+ await assignment.setSessionProjectDir('s1', '/tmp/workspace')
expect(sqlitePresenter.newSessionsTable.update).toHaveBeenCalledWith('s1', {
project_dir: '/tmp/workspace'
@@ -2497,7 +2483,7 @@ describe('AgentSessionPresenter', () => {
const runtimeError = new Error('runtime project update failed')
deepChatAgent.setSessionProjectDir.mockRejectedValueOnce(runtimeError)
- await expect(presenter.setSessionProjectDir('s1', '/tmp/workspace')).rejects.toBe(
+ await expect(assignment.setSessionProjectDir('s1', '/tmp/workspace')).rejects.toBe(
runtimeError
)
@@ -2538,7 +2524,7 @@ describe('AgentSessionPresenter', () => {
permissionMode: 'full_access'
})
- const session = await presenter.ensureAcpDraftSession({
+ const session = await lifecycle.ensureAcpDraftSession({
agentId: 'acp-coder',
projectDir: '/tmp/workspace'
})
@@ -2584,7 +2570,7 @@ describe('AgentSessionPresenter', () => {
permissionMode: 'full_access'
})
- const session = await presenter.ensureAcpDraftSession({
+ const session = await lifecycle.ensureAcpDraftSession({
agentId: 'acp-coder',
projectDir: '/tmp/workspace'
})
@@ -2641,7 +2627,7 @@ describe('AgentSessionPresenter', () => {
permissionMode: 'full_access'
})
- const session = await presenter.createSubagentSession({
+ const session = await lifecycle.createSubagentSession({
parentSessionId: 'parent-1',
agentId: ' kimi-cli ',
slotId: 'reviewer',
@@ -2735,7 +2721,7 @@ describe('AgentSessionPresenter', () => {
permissionMode: 'full_access'
})
- const session = await presenter.createSubagentSession({
+ const session = await lifecycle.createSubagentSession({
parentSessionId: 'parent-1',
agentId: 'acp-reviewer',
slotId: 'reviewer',
@@ -2813,7 +2799,7 @@ describe('AgentSessionPresenter', () => {
}
})
- const session = await presenter.createSubagentSession({
+ const session = await lifecycle.createSubagentSession({
parentSessionId: 'parent-1',
agentId: 'deepchat',
slotId: 'reviewer',
@@ -2854,7 +2840,7 @@ describe('AgentSessionPresenter', () => {
return undefined
})
- await presenter[action]('parent', 'child', { source: 'test' })
+ await assignment[action]('parent', 'child', { source: 'test' })
expect(deepChatAgent[method]).toHaveBeenCalledWith('parent', 'child', { source: 'test' })
}
@@ -2867,7 +2853,7 @@ describe('AgentSessionPresenter', () => {
return undefined
})
- await expect(presenter.mergeSubagentTape('parent', 'child')).rejects.toThrow(
+ await expect(assignment.mergeSubagentTape('parent', 'child')).rejects.toThrow(
'Session child is not a child of parent.'
)
expect(agentManager.resolveSubagentFacet).not.toHaveBeenCalled()
@@ -2911,7 +2897,7 @@ describe('AgentSessionPresenter', () => {
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {})
await expect(
- presenter.forkSession('source-session', 'message-1', 'Forked title')
+ lifecycle.forkSession('source-session', 'message-1', 'Forked title')
).rejects.toBe(forkError)
expect(deepChatAgent.initSession).toHaveBeenCalledWith(
@@ -2965,7 +2951,7 @@ describe('AgentSessionPresenter', () => {
permissionMode: 'full_access'
})
- const sessions = await presenter.getSessionList()
+ const sessions = await projection.listSessions()
expect(sessions).toHaveLength(1)
expect(sessions[0].providerId).toBe('summary-provider')
@@ -2987,7 +2973,7 @@ describe('AgentSessionPresenter', () => {
}
])
- const sessions = await presenter.getSessionList()
+ const sessions = await projection.listSessions()
expect(sessions).toHaveLength(1)
expect(sessions[0].status).toBe('idle')
expect(sessions[0].providerId).toBe('openai')
@@ -3017,7 +3003,7 @@ describe('AgentSessionPresenter', () => {
}
])
- const sessions = await presenter.getSessionList()
+ const sessions = await projection.listSessions()
expect(sessions).toHaveLength(1)
expect(sessions[0].id).toBe('s1')
@@ -3062,7 +3048,7 @@ describe('AgentSessionPresenter', () => {
}
})
- const sessions = await presenter.getSessionList()
+ const sessions = await projection.listSessions()
expect(sessions).toHaveLength(1)
expect(sessions[0].id).toBe('healthy-state')
@@ -3099,10 +3085,10 @@ describe('AgentSessionPresenter', () => {
permissionMode: 'full_access'
})
- await expect(presenter.getSession('s1')).resolves.toMatchObject({ status: 'generating' })
+ await expect(projection.getSession('s1')).resolves.toMatchObject({ status: 'generating' })
agentManager.resolveSessionHandle.mockClear()
- await expect(presenter.getLightweightSessionList()).resolves.toMatchObject({
+ await expect(projection.listLightweight()).resolves.toMatchObject({
items: [expect.objectContaining({ id: 's1', status: 'generating' })],
nextCursor: null,
hasMore: false
@@ -3129,7 +3115,7 @@ describe('AgentSessionPresenter', () => {
}
sqlitePresenter.newSessionsTable.listPage.mockReturnValue({ rows: [row], hasMore: false })
- const result = await presenter.getLightweightSessionList()
+ const result = await projection.listLightweight()
expect(result.items).toEqual([expect.objectContaining({ id: 's1', status: 'idle' })])
expect(agentManager.resolveSessionHandle).not.toHaveBeenCalled()
@@ -3148,14 +3134,14 @@ describe('AgentSessionPresenter', () => {
updated_at: 2000
})
- const session = await presenter.getSession('s1')
+ const session = await projection.getSession('s1')
expect(session).not.toBeNull()
expect(session!.status).toBe('idle')
})
it('returns null for unknown session', async () => {
sqlitePresenter.newSessionsTable.get.mockReturnValue(undefined)
- expect(await presenter.getSession('unknown')).toBeNull()
+ expect(await projection.getSession('unknown')).toBeNull()
})
it('returns null when session agent is unavailable', async () => {
@@ -3170,7 +3156,7 @@ describe('AgentSessionPresenter', () => {
updated_at: 2000
})
- expect(await presenter.getSession('s-disabled')).toBeNull()
+ expect(await projection.getSession('s-disabled')).toBeNull()
expect(warnSpy).toHaveBeenCalledWith(
'[SessionProjectionCoordinator] Skipping unavailable session id=s-disabled agent=disabled-agent:',
expect.any(Error)
@@ -3196,7 +3182,7 @@ describe('AgentSessionPresenter', () => {
summaryUpdatedAt: 123
})
- const state = await presenter.getSessionCompactionState('s1')
+ const state = await turn.getSessionCompactionState('s1')
expect(deepChatAgent.getSessionCompactionState).toHaveBeenCalledWith('s1')
expect(state).toEqual({
@@ -3217,7 +3203,7 @@ describe('AgentSessionPresenter', () => {
updated_at: 2000
})
- await expect(presenter.getSessionCompactionState('s-acp')).resolves.toEqual({
+ await expect(turn.getSessionCompactionState('s-acp')).resolves.toEqual({
status: 'idle',
cursorOrderSeq: 1,
summaryUpdatedAt: null
@@ -3237,7 +3223,7 @@ describe('AgentSessionPresenter', () => {
updated_at: 2000
})
- await expect(presenter.compactSession('s-acp')).rejects.toThrow(
+ await expect(turn.compactSession('s-acp')).rejects.toThrow(
'Agent acp-coder does not support manual compaction.'
)
@@ -3257,7 +3243,7 @@ describe('AgentSessionPresenter', () => {
permissionMode: 'full_access'
})
- await expect(presenter.compactSession('s-compat')).rejects.toThrow(
+ await expect(turn.compactSession('s-compat')).rejects.toThrow(
'Manual compaction is only available for DeepChat agent sessions.'
)
expect(deepChatAgent.compactSession).not.toHaveBeenCalled()
@@ -3276,7 +3262,7 @@ describe('AgentSessionPresenter', () => {
})
deepChatAgent.compactSession.mockRejectedValueOnce(compactError)
- await expect(presenter.compactSession('s1')).rejects.toBe(compactError)
+ await expect(turn.compactSession('s1')).rejects.toBe(compactError)
expect(deepChatAgent.compactSession).toHaveBeenCalledWith('s1')
})
@@ -3300,7 +3286,7 @@ describe('AgentSessionPresenter', () => {
}
])
- const traces = await presenter.listMessageTraces('m1')
+ const traces = await projection.listMessageTraces('m1')
expect(traces).toEqual([
{
id: 't2',
@@ -3319,14 +3305,14 @@ describe('AgentSessionPresenter', () => {
})
it('returns empty list for blank message id', async () => {
- const traces = await presenter.listMessageTraces(' ')
+ const traces = await projection.listMessageTraces(' ')
expect(traces).toEqual([])
expect(sqlitePresenter.deepchatMessageTracesTable.listByMessageId).not.toHaveBeenCalled()
})
it('returns trace count by message id', async () => {
sqlitePresenter.deepchatMessageTracesTable.countByMessageId.mockReturnValue(3)
- await expect(presenter.getMessageTraceCount('m1')).resolves.toBe(3)
+ await expect(projection.getMessageTraceCount('m1')).resolves.toBe(3)
expect(sqlitePresenter.deepchatMessageTracesTable.countByMessageId).toHaveBeenCalledWith('m1')
})
})
@@ -3336,7 +3322,7 @@ describe('AgentSessionPresenter', () => {
const message = { id: 'm1', sessionId: 's1', role: 'assistant' }
deepChatAgent.getMessage.mockResolvedValue(message as any)
- await expect(presenter.getMessage('m1')).resolves.toBe(message)
+ await expect(projection.getMessage('m1')).resolves.toBe(message)
expect(deepChatAgent.getMessage).toHaveBeenCalledWith('m1')
})
@@ -3354,14 +3340,16 @@ describe('AgentSessionPresenter', () => {
}
])
- await expect(presenter.getSearchResults(' message-1 ', 'missing-search')).resolves.toEqual([
- expect.objectContaining({
- title: 'Legacy result',
- url: 'https://example.com',
- rank: 2,
- searchId: undefined
- })
- ])
+ await expect(projection.getSearchResults(' message-1 ', 'missing-search')).resolves.toEqual(
+ [
+ expect.objectContaining({
+ title: 'Legacy result',
+ url: 'https://example.com',
+ rank: 2,
+ searchId: undefined
+ })
+ ]
+ )
expect(
sqlitePresenter.deepchatMessageSearchResultsTable.listByMessageId
@@ -3395,8 +3383,8 @@ describe('AgentSessionPresenter', () => {
deepChatAgent.listMessageViewManifests.mockRejectedValueOnce(new Error('manifest failed'))
deepChatAgent.exportMessageTapeReplaySlice.mockRejectedValueOnce(new Error('replay failed'))
- await expect(presenter.listMessageViewManifests('message-1')).resolves.toEqual([])
- await expect(presenter.exportMessageTapeReplaySlice('message-1')).resolves.toBeNull()
+ await expect(projection.listMessageViewManifests('message-1')).resolves.toEqual([])
+ await expect(projection.exportMessageTapeReplaySlice('message-1')).resolves.toBeNull()
expect(deepChatAgent.listMessageViewManifests).toHaveBeenCalledWith('s1', 'message-1')
expect(deepChatAgent.exportMessageTapeReplaySlice).toHaveBeenCalledWith(
@@ -3409,9 +3397,6 @@ describe('AgentSessionPresenter', () => {
describe('session update publication', () => {
it('trims and dedupes ids, applies reason defaults, and refreshes session UI', () => {
- const projection = Reflect.get(presenter, 'sessionProjection') as {
- notify(options?: { sessionIds?: string[] }): void
- }
const emit = projection.notify.bind(projection) as (options?: {
sessionIds?: string[]
}) => void
@@ -3442,7 +3427,7 @@ describe('AgentSessionPresenter', () => {
describe('activateSession', () => {
it('binds window and publishes typed activated update', async () => {
- await presenter.activateSession(42, 's1')
+ await projection.activate(42, 's1')
expectSessionsUpdated({
webContentsId: 42,
sessionIds: ['s1'],
@@ -3457,7 +3442,7 @@ describe('AgentSessionPresenter', () => {
describe('deactivateSession', () => {
it('unbinds window and publishes typed deactivated update', async () => {
- await presenter.deactivateSession(42)
+ await projection.deactivate(42)
expectSessionsUpdated({
sessionIds: [],
reason: 'deactivated',
@@ -3480,7 +3465,7 @@ describe('AgentSessionPresenter', () => {
updated_at: 2000
})
- await presenter.deleteSession('s1')
+ await lifecycle.deleteSession('s1')
expect(deepChatAgent.destroySession).toHaveBeenCalledWith('s1')
expect(sqlitePresenter.newSessionsTable.delete).toHaveBeenCalledWith('s1')
expectSessionsUpdated({ reason: 'deleted', sessionIds: ['s1'] })
@@ -3488,7 +3473,7 @@ describe('AgentSessionPresenter', () => {
it('no-ops for unknown session', async () => {
sqlitePresenter.newSessionsTable.get.mockReturnValue(undefined)
- await presenter.deleteSession('unknown') // should not throw
+ await lifecycle.deleteSession('unknown') // should not throw
expect(deepChatAgent.destroySession).not.toHaveBeenCalled()
})
@@ -3512,7 +3497,7 @@ describe('AgentSessionPresenter', () => {
permissionMode: 'full_access'
})
- await presenter.deleteSession('s-acp')
+ await lifecycle.deleteSession('s-acp')
expect(llmProviderPresenter.clearAcpSession).not.toHaveBeenCalled()
expect(deepChatAgent.destroySession).toHaveBeenCalledWith('s-acp')
@@ -3535,7 +3520,7 @@ describe('AgentSessionPresenter', () => {
})
agentManager.cleanupSessionBackends.mockRejectedValueOnce(cleanupError)
- await expect(presenter.deleteSession('s1')).rejects.toBe(cleanupError)
+ await expect(lifecycle.deleteSession('s1')).rejects.toBe(cleanupError)
expect(deepChatAgent.destroySession).toHaveBeenCalledExactlyOnceWith('s1')
expect(skillPresenter.clearNewAgentSessionSkills).not.toHaveBeenCalled()
@@ -3557,7 +3542,7 @@ describe('AgentSessionPresenter', () => {
agentManager.cleanupSessionBackends.mockRejectedValueOnce(backendError)
deepChatAgent.destroySession.mockRejectedValueOnce(sharedError)
- await expect(presenter.deleteSession('s1')).rejects.toBe(backendError)
+ await expect(lifecycle.deleteSession('s1')).rejects.toBe(backendError)
expect(deepChatAgent.destroySession).toHaveBeenCalledExactlyOnceWith('s1')
expect(skillPresenter.clearNewAgentSessionSkills).not.toHaveBeenCalled()
@@ -3578,7 +3563,7 @@ describe('AgentSessionPresenter', () => {
})
deepChatAgent.destroySession.mockRejectedValueOnce(sharedError)
- await expect(presenter.deleteSession('s1')).rejects.toBe(sharedError)
+ await expect(lifecycle.deleteSession('s1')).rejects.toBe(sharedError)
expect(agentManager.cleanupSessionBackends).toHaveBeenCalledExactlyOnceWith('s1')
expect(skillPresenter.clearNewAgentSessionSkills).not.toHaveBeenCalled()
@@ -3599,14 +3584,14 @@ describe('AgentSessionPresenter', () => {
updated_at: 1000
})
- await presenter.cancelGeneration('s1')
+ await turn.cancelGeneration('s1')
expect(deepChatAgent.cancelGeneration).toHaveBeenCalledWith('s1')
})
it('is a no-op for a missing session', async () => {
sqlitePresenter.newSessionsTable.get.mockReturnValue(undefined)
- await expect(presenter.cancelGeneration('missing')).resolves.toBeUndefined()
+ await expect(turn.cancelGeneration('missing')).resolves.toBeUndefined()
expect(agentManager.resolveSessionHandle).not.toHaveBeenCalled()
expect(deepChatAgent.cancelGeneration).not.toHaveBeenCalled()
@@ -3628,7 +3613,7 @@ describe('AgentSessionPresenter', () => {
deepChatAgent.respondToolInteraction.mockResolvedValueOnce({ resumed: true })
await expect(
- presenter.respondToolInteraction('s1', 'message-1', 'tool-1', response)
+ turn.respondToolInteraction('s1', 'message-1', 'tool-1', response)
).resolves.toEqual({ resumed: true })
expect(deepChatAgent.respondToolInteraction).toHaveBeenCalledWith(
@@ -3643,7 +3628,7 @@ describe('AgentSessionPresenter', () => {
sqlitePresenter.newSessionsTable.get.mockReturnValue(undefined)
await expect(
- presenter.respondToolInteraction('missing', 'message-1', 'tool-1', {
+ turn.respondToolInteraction('missing', 'message-1', 'tool-1', {
kind: 'permission',
granted: true
})
@@ -3666,7 +3651,7 @@ describe('AgentSessionPresenter', () => {
updated_at: 1000
})
- const settings = await presenter.getSessionGenerationSettings('s1')
+ const settings = await assignment.getSessionGenerationSettings('s1')
expect(deepChatAgent.getGenerationSettings).toHaveBeenCalledWith('s1')
expect(settings).toEqual({
@@ -3688,7 +3673,7 @@ describe('AgentSessionPresenter', () => {
updated_at: 1000
})
- const updated = await presenter.updateSessionGenerationSettings('s1', {
+ const updated = await assignment.updateSessionGenerationSettings('s1', {
temperature: 1.4,
reasoningEffort: 'high'
})
@@ -3704,11 +3689,11 @@ describe('AgentSessionPresenter', () => {
it('throws when generation settings methods target unknown session', async () => {
sqlitePresenter.newSessionsTable.get.mockReturnValue(undefined)
- await expect(presenter.getSessionGenerationSettings('unknown')).rejects.toThrow(
+ await expect(assignment.getSessionGenerationSettings('unknown')).rejects.toThrow(
'Session not found: unknown'
)
await expect(
- presenter.updateSessionGenerationSettings('unknown', { temperature: 1 })
+ assignment.updateSessionGenerationSettings('unknown', { temperature: 1 })
).rejects.toThrow('Session not found: unknown')
})
})
@@ -3726,7 +3711,7 @@ describe('AgentSessionPresenter', () => {
})
sqlitePresenter.newSessionsTable.getDisabledAgentTools.mockReturnValue(['exec', 'cdp_send'])
- const disabledTools = await presenter.getSessionDisabledAgentTools('s1')
+ const disabledTools = await assignment.getSessionDisabledAgentTools('s1')
expect(disabledTools).toEqual(['exec', 'cdp_send'])
})
@@ -3742,7 +3727,7 @@ describe('AgentSessionPresenter', () => {
updated_at: 1000
})
- const disabledTools = await presenter.updateSessionDisabledAgentTools('s1', [
+ const disabledTools = await assignment.updateSessionDisabledAgentTools('s1', [
'grep',
'ls',
'cdp_send',
@@ -3777,7 +3762,7 @@ describe('AgentSessionPresenter', () => {
updated_at: 1000
})
- await expect(presenter.setSessionSubagentEnabled('s-acp', true)).rejects.toThrow(
+ await expect(assignment.setSessionSubagentEnabled('s-acp', true)).rejects.toThrow(
'Only DeepChat sessions can change subagent state.'
)
@@ -3807,7 +3792,7 @@ describe('AgentSessionPresenter', () => {
})
deepChatAgent.getSessionState.mockRejectedValueOnce(new Error('state unavailable'))
- await expect(presenter.setSessionSubagentEnabled('s1', true)).rejects.toThrow(
+ await expect(assignment.setSessionSubagentEnabled('s1', true)).rejects.toThrow(
'Failed to build session state for sessionId: s1'
)
@@ -3837,7 +3822,7 @@ describe('AgentSessionPresenter', () => {
permissionMode: 'full_access'
})
- const updated = await presenter.setSessionModel('s1', 'anthropic', 'claude-3-5-sonnet')
+ const updated = await assignment.setSessionModel('s1', 'anthropic', 'claude-3-5-sonnet')
expect(deepChatAgent.setSessionModel).toHaveBeenCalledWith(
's1',
@@ -3863,7 +3848,7 @@ describe('AgentSessionPresenter', () => {
{ id: 'acp-coder', name: 'ACP Coder', command: 'acp-coder' }
])
- await expect(presenter.setSessionModel('s-acp', 'openai', 'gpt-4')).rejects.toThrow(
+ await expect(assignment.setSessionModel('s-acp', 'openai', 'gpt-4')).rejects.toThrow(
'ACP session model is locked.'
)
expect(deepChatAgent.setSessionModel).not.toHaveBeenCalled()
@@ -3910,7 +3895,7 @@ describe('AgentSessionPresenter', () => {
async (sessionId: string) => sessionId === 's-ready'
)
- const impact = await presenter.getAgentTransferImpact('deepchat-writer')
+ const impact = await assignment.getAgentTransferImpact('deepchat-writer')
expect(impact.totalSessions).toBe(2)
expect(impact.movableSessions).toBe(1)
@@ -3943,7 +3928,7 @@ describe('AgentSessionPresenter', () => {
configPresenter.getAgentType.mockResolvedValue('deepchat')
deepChatAgent.hasMessages.mockRejectedValue(new Error('query failed'))
- const impact = await presenter.getAgentTransferImpact('deepchat-writer')
+ const impact = await assignment.getAgentTransferImpact('deepchat-writer')
expect(impact.movableSessions).toBe(1)
expect(impact.emptyDrafts).toBe(0)
@@ -3970,7 +3955,7 @@ describe('AgentSessionPresenter', () => {
deepChatAgent.hasMessages.mockResolvedValue(true)
deepChatAgent.listPendingInputs.mockRejectedValue(new Error('pending query failed'))
- await expect(presenter.moveSessionToAgent('s1', 'deepchat-coder')).rejects.toThrow(
+ await expect(assignment.moveSessionToAgent('s1', 'deepchat-coder')).rejects.toThrow(
'Session s1 cannot be moved: pending-input'
)
@@ -3981,7 +3966,7 @@ describe('AgentSessionPresenter', () => {
})
it('rejects blank agent ids for destructive agent-session deletion', async () => {
- await expect(presenter.deleteAgentSessions(' ')).rejects.toThrow('Agent id is required.')
+ await expect(assignment.deleteAgentSessions(' ')).rejects.toThrow('Agent id is required.')
expect(sqlitePresenter.newSessionsTable.list).not.toHaveBeenCalled()
expect(sqlitePresenter.newSessionsTable.delete).not.toHaveBeenCalled()
})
@@ -4037,7 +4022,7 @@ describe('AgentSessionPresenter', () => {
permissionMode: 'default'
})
- const updated = await presenter.moveSessionToAgent('s1', 'deepchat-coder')
+ const updated = await assignment.moveSessionToAgent('s1', 'deepchat-coder')
expect(deepChatAgent.setSessionAgentContext).toHaveBeenCalledWith(
's1',
@@ -4122,7 +4107,7 @@ describe('AgentSessionPresenter', () => {
}))
await expect(
- presenter.moveAgentSessions('deepchat-writer', 'deepchat-coder')
+ assignment.moveAgentSessions('deepchat-writer', 'deepchat-coder')
).rejects.toThrow('Session s-active cannot be moved: active')
expect(deepChatAgent.setSessionAgentContext).not.toHaveBeenCalled()
@@ -4188,7 +4173,7 @@ describe('AgentSessionPresenter', () => {
}
})
- const updated = await presenter.moveSessionToAgent('s-acp', 'deepchat-coder')
+ const updated = await assignment.moveSessionToAgent('s-acp', 'deepchat-coder')
expect(deepChatAgent.setSessionAgentContext).toHaveBeenCalledWith(
's-acp',
@@ -4254,7 +4239,7 @@ describe('AgentSessionPresenter', () => {
permissionMode: 'full_access'
})
- await expect(presenter.moveSessionToAgent('s-acp', 'deepchat-coder')).rejects.toThrow(
+ await expect(assignment.moveSessionToAgent('s-acp', 'deepchat-coder')).rejects.toThrow(
'ownership update failed'
)
@@ -4348,7 +4333,7 @@ describe('AgentSessionPresenter', () => {
})
await expect(
- presenter.moveAgentSessions('deepchat-writer', 'deepchat-coder')
+ assignment.moveAgentSessions('deepchat-writer', 'deepchat-coder')
).rejects.toThrow('Partial transfer completed: 1 moved.')
expect(rows.get('s-ready-1').agent_id).toBe('deepchat-coder')
@@ -4385,7 +4370,7 @@ describe('AgentSessionPresenter', () => {
})
deepChatAgent.hasMessages.mockResolvedValue(true)
- await expect(presenter.moveSessionToAgent('s-deepchat', 'acp-coder')).rejects.toThrow(
+ await expect(assignment.moveSessionToAgent('s-deepchat', 'acp-coder')).rejects.toThrow(
'Conversation history cannot be moved to ACP agents.'
)
expect(deepChatAgent.setSessionAgentContext).not.toHaveBeenCalled()
@@ -4405,7 +4390,7 @@ describe('AgentSessionPresenter', () => {
return null
})
- await expect(presenter.moveAgentSessions('deepchat-writer', 'acp-coder')).rejects.toThrow(
+ await expect(assignment.moveAgentSessions('deepchat-writer', 'acp-coder')).rejects.toThrow(
'Conversation history cannot be moved to ACP agents.'
)
expect(sqlitePresenter.newSessionsTable.updateAgentId).not.toHaveBeenCalled()
@@ -4445,7 +4430,7 @@ describe('AgentSessionPresenter', () => {
deepChatAgent.hasMessages.mockResolvedValue(true)
await expect(
- presenter.moveSessionToAgent('s-deepchat', 'deepchat-acp-default')
+ assignment.moveSessionToAgent('s-deepchat', 'deepchat-acp-default')
).rejects.toThrow('Conversation history cannot be moved to ACP agents.')
expect(deepChatAgent.setSessionAgentContext).not.toHaveBeenCalled()
expect(sqlitePresenter.newSessionsTable.updateAgentId).not.toHaveBeenCalled()
@@ -4481,7 +4466,7 @@ describe('AgentSessionPresenter', () => {
permissionMode: 'full_access'
})
- await expect(presenter.moveSessionToAgent('s-acp', 'acp-reviewer')).rejects.toThrow(
+ await expect(assignment.moveSessionToAgent('s-acp', 'acp-reviewer')).rejects.toThrow(
'Conversation history cannot be moved to ACP agents.'
)
expect(deepChatAgent.setSessionAgentContext).not.toHaveBeenCalled()
@@ -4502,7 +4487,7 @@ describe('AgentSessionPresenter', () => {
updated_at: 1000
})
- await presenter.deleteSession('s1')
+ await lifecycle.deleteSession('s1')
expect(skillPresenter.clearNewAgentSessionSkills).toHaveBeenCalledWith('s1')
})
})
@@ -4519,7 +4504,7 @@ describe('AgentSessionPresenter', () => {
updated_at: 1000
})
- await presenter.renameSession('s1', ' New Title ')
+ await projection.renameSession('s1', ' New Title ')
expect(sqlitePresenter.newSessionsTable.update).toHaveBeenCalledWith('s1', {
title: 'New Title'
@@ -4538,7 +4523,7 @@ describe('AgentSessionPresenter', () => {
updated_at: 1000
})
- await presenter.toggleSessionPinned('s1', true)
+ await projection.toggleSessionPinned('s1', true)
expect(sqlitePresenter.newSessionsTable.update).toHaveBeenCalledWith('s1', {
is_pinned: 1
@@ -4557,7 +4542,7 @@ describe('AgentSessionPresenter', () => {
updated_at: 1000
})
- await presenter.clearSessionMessages('s1')
+ await turn.clearSessionMessages('s1')
expect(deepChatAgent.clearMessages).toHaveBeenCalledWith('s1')
expect(deepChatAgent.cancelGeneration.mock.invocationCallOrder[0]).toBeLessThan(
@@ -4580,7 +4565,7 @@ describe('AgentSessionPresenter', () => {
const cancelError = new Error('cancel failed')
deepChatAgent.cancelGeneration.mockRejectedValueOnce(cancelError)
- await expect(presenter.clearSessionMessages('s1')).rejects.toBe(cancelError)
+ await expect(turn.clearSessionMessages('s1')).rejects.toBe(cancelError)
expect(deepChatAgent.clearMessages).not.toHaveBeenCalled()
expect(publishDeepchatEvent).not.toHaveBeenCalled()
@@ -4600,7 +4585,7 @@ describe('AgentSessionPresenter', () => {
updated_at: 1000
})
- const commands = await presenter.getAcpSessionCommands('s1')
+ const commands = await assignment.getAcpSessionCommands('s1')
expect(commands).toEqual([])
expect(llmProviderPresenter.getAcpSessionCommands).not.toHaveBeenCalled()
})
@@ -4625,7 +4610,7 @@ describe('AgentSessionPresenter', () => {
permissionMode: 'full_access'
})
- const commands = await presenter.getAcpSessionCommands('s-acp')
+ const commands = await assignment.getAcpSessionCommands('s-acp')
expect(directAcpControl.getCommands).toHaveBeenCalledOnce()
expect(llmProviderPresenter.getAcpSessionCommands).not.toHaveBeenCalled()
@@ -4650,7 +4635,7 @@ describe('AgentSessionPresenter', () => {
permissionMode: 'full_access'
})
- const commands = await presenter.getAcpSessionCommands('s-compat')
+ const commands = await assignment.getAcpSessionCommands('s-compat')
expect(llmProviderPresenter.getAcpSessionCommands).toHaveBeenCalledWith('s-compat')
expect(directAcpControl.getCommands).not.toHaveBeenCalled()
@@ -4670,7 +4655,7 @@ describe('AgentSessionPresenter', () => {
updated_at: 1000
})
- const result = await presenter.getAcpSessionConfigOptions('s1')
+ const result = await assignment.getAcpSessionConfigOptions('s1')
expect(result).toBeNull()
expect(llmProviderPresenter.getAcpSessionConfigOptions).not.toHaveBeenCalled()
@@ -4696,7 +4681,7 @@ describe('AgentSessionPresenter', () => {
permissionMode: 'full_access'
})
- const result = await presenter.getAcpSessionConfigOptions('s-acp')
+ const result = await assignment.getAcpSessionConfigOptions('s-acp')
expect(directAcpControl.getConfigOptions).toHaveBeenCalledOnce()
expect(llmProviderPresenter.getAcpSessionConfigOptions).not.toHaveBeenCalled()
@@ -4723,7 +4708,7 @@ describe('AgentSessionPresenter', () => {
permissionMode: 'full_access'
})
- const result = await presenter.setAcpSessionConfigOption('s-acp', 'model', 'gpt-5-mini')
+ const result = await assignment.setAcpSessionConfigOption('s-acp', 'model', 'gpt-5-mini')
expect(directAcpControl.setConfigOption).toHaveBeenCalledWith('model', 'gpt-5-mini')
expect(llmProviderPresenter.setAcpSessionConfigOption).not.toHaveBeenCalled()
@@ -4747,8 +4732,8 @@ describe('AgentSessionPresenter', () => {
permissionMode: 'full_access'
})
- const state = await presenter.getAcpSessionConfigOptions('s-compat')
- const updated = await presenter.setAcpSessionConfigOption('s-compat', 'model', 'gpt-5-mini')
+ const state = await assignment.getAcpSessionConfigOptions('s-compat')
+ const updated = await assignment.setAcpSessionConfigOption('s-compat', 'model', 'gpt-5-mini')
expect(llmProviderPresenter.getAcpSessionConfigOptions).toHaveBeenCalledWith('s-compat')
expect(llmProviderPresenter.setAcpSessionConfigOption).toHaveBeenCalledWith(
@@ -4765,7 +4750,7 @@ describe('AgentSessionPresenter', () => {
describe('getActiveSession', () => {
it('returns null when no session bound', async () => {
- expect(await presenter.getActiveSession(99)).toBeNull()
+ expect(await projection.getActive(99)).toBeNull()
})
it('returns session when bound', async () => {
@@ -4779,25 +4764,25 @@ describe('AgentSessionPresenter', () => {
updated_at: 2000
})
- await presenter.activateSession(1, 's1')
- const session = await presenter.getActiveSession(1)
+ await projection.activate(1, 's1')
+ const session = await projection.getActive(1)
expect(session).not.toBeNull()
expect(session!.id).toBe('s1')
})
it('keeps bindings isolated by window', async () => {
- await presenter.activateSession(1, 's1')
- await presenter.activateSession(2, 's2')
- await presenter.deactivateSession(1)
+ await projection.activate(1, 's1')
+ await projection.activate(2, 's2')
+ await projection.deactivate(1)
- expect(presenter.getActiveSessionId(1)).toBeNull()
- expect(presenter.getActiveSessionId(2)).toBe('s2')
+ expect(projection.getActiveId(1)).toBeNull()
+ expect(projection.getActiveId(2)).toBe('s2')
})
it('returns null and clears binding when bound session becomes unavailable', async () => {
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {})
- await presenter.activateSession(1, 's-disabled')
+ await projection.activate(1, 's-disabled')
vi.mocked(publishDeepchatEvent).mockClear()
sqlitePresenter.newSessionsTable.get.mockReturnValueOnce({
id: 's-disabled',
@@ -4809,7 +4794,7 @@ describe('AgentSessionPresenter', () => {
updated_at: 2000
})
- await expect(presenter.getActiveSession(1)).resolves.toBeNull()
+ await expect(projection.getActive(1)).resolves.toBeNull()
expect(publishDeepchatEvent).not.toHaveBeenCalled()
sqlitePresenter.newSessionsTable.get.mockReturnValue({
@@ -4821,7 +4806,7 @@ describe('AgentSessionPresenter', () => {
created_at: 1000,
updated_at: 2000
})
- await expect(presenter.getActiveSession(1)).resolves.toBeNull()
+ await expect(projection.getActive(1)).resolves.toBeNull()
expect(warnSpy).toHaveBeenCalledWith(
'[SessionProjectionCoordinator] Skipping unavailable session id=s-disabled agent=disabled-agent:',
expect.any(Error)
diff --git a/test/main/presenter/sessionBoundaryComposition.test.ts b/test/main/presenter/sessionBoundaryComposition.test.ts
index f9aa75d56..73ec89097 100644
--- a/test/main/presenter/sessionBoundaryComposition.test.ts
+++ b/test/main/presenter/sessionBoundaryComposition.test.ts
@@ -22,4 +22,20 @@ describe('session boundary composition', () => {
)
expect(legacyHookSource).toContain('presenter.legacyChatImportService.start(false)')
})
+
+ it('keeps hooks notifications on one instance with lazy projection dependencies', async () => {
+ const { readFileSync } = await vi.importActual('node:fs')
+ const presenterSource = readFileSync(
+ path.resolve(process.cwd(), 'src/main/presenter/index.ts'),
+ 'utf8'
+ )
+
+ expect(presenterSource.match(/new HooksNotificationsService\(/g)).toHaveLength(1)
+ expect(presenterSource).toContain(
+ 'getSession: (sessionId) => this.sessionProjectionCoordinator.getSession(sessionId)'
+ )
+ expect(presenterSource).toContain(
+ 'getMessage: (messageId) => this.sessionProjectionCoordinator.getMessage(messageId)'
+ )
+ })
})
diff --git a/test/main/routes/dispatcher.test.ts b/test/main/routes/dispatcher.test.ts
index 8eaa339b8..7eb079155 100644
--- a/test/main/routes/dispatcher.test.ts
+++ b/test/main/routes/dispatcher.test.ts
@@ -1,5 +1,4 @@
import type {
- IAgentSessionPresenter,
IConfigPresenter,
IConversationExporter,
IDevicePresenter,
@@ -420,118 +419,6 @@ function createRuntime() {
})
} as unknown as IConfigPresenter
- const agentSessionPresenter = {
- getActiveSessionId: vi.fn(() => null),
- getLightweightSessionsByIds: vi.fn().mockResolvedValue([]),
- createDetachedSession: vi.fn().mockResolvedValue({
- id: 'session-1',
- agentId: 'deepchat',
- title: 'New Chat',
- projectDir: '/workspace',
- isPinned: false,
- isDraft: false,
- sessionKind: 'regular',
- parentSessionId: null,
- subagentEnabled: false,
- subagentMeta: null,
- createdAt: 1,
- updatedAt: 2,
- status: 'idle',
- providerId: 'openai',
- modelId: 'gpt-5.4'
- }),
- createSession: vi.fn().mockResolvedValue({
- id: 'session-1',
- agentId: 'deepchat',
- title: 'New Chat',
- projectDir: '/workspace',
- isPinned: false,
- isDraft: false,
- sessionKind: 'regular',
- parentSessionId: null,
- subagentEnabled: false,
- subagentMeta: null,
- createdAt: 1,
- updatedAt: 2,
- status: 'idle',
- providerId: 'openai',
- modelId: 'gpt-5.4'
- }),
- getSession: vi.fn().mockResolvedValue({
- id: 'session-1',
- agentId: 'deepchat',
- title: 'Restored',
- projectDir: '/workspace',
- isPinned: false,
- isDraft: false,
- sessionKind: 'regular',
- parentSessionId: null,
- subagentEnabled: false,
- subagentMeta: null,
- createdAt: 1,
- updatedAt: 2,
- status: 'idle',
- providerId: 'openai',
- modelId: 'gpt-5.4'
- }),
- getMessages: vi.fn().mockResolvedValue([
- {
- id: 'message-1',
- sessionId: 'session-1',
- orderSeq: 1,
- role: 'user',
- content: '{"text":"hello"}',
- status: 'sent',
- isContextEdge: 0,
- metadata: '{}',
- createdAt: 1,
- updatedAt: 1
- }
- ]),
- getSessionList: vi.fn().mockResolvedValue([]),
- getActiveSession: vi.fn().mockResolvedValue(null),
- activateSession: vi.fn().mockResolvedValue(undefined),
- deactivateSession: vi.fn().mockResolvedValue(undefined),
- getSessionGenerationSettings: vi.fn().mockResolvedValue({
- systemPrompt: '',
- temperature: 0.7,
- contextLength: 32000,
- maxTokens: 4096,
- timeout: 5000
- }),
- updateSessionGenerationSettings: vi
- .fn()
- .mockImplementation(async (_sessionId: string, settings: { timeout?: number }) => ({
- systemPrompt: '',
- temperature: 0.7,
- contextLength: 32000,
- maxTokens: 4096,
- timeout: settings.timeout ?? 5000
- })),
- sendMessage: vi.fn().mockResolvedValue({
- requestId: 'message-2',
- messageId: 'message-2'
- }),
- steerActiveTurn: vi.fn().mockResolvedValue(undefined),
- compactSession: vi.fn().mockResolvedValue({
- compacted: true,
- state: {
- status: 'compacted',
- cursorOrderSeq: 5,
- summaryUpdatedAt: 123
- }
- }),
- cancelGeneration: vi.fn().mockResolvedValue(undefined),
- getMessage: vi.fn().mockResolvedValue({
- id: 'message-1',
- sessionId: 'session-1'
- }),
- respondToolInteraction: vi.fn().mockResolvedValue({
- resumed: true
- }),
- clearSessionPermissions: vi.fn()
- } as unknown as IAgentSessionPresenter
-
const sessionSnapshot = {
id: 'session-1',
agentId: 'deepchat',
@@ -550,7 +437,12 @@ function createRuntime() {
modelId: 'gpt-5.4'
}
const sessionLifecyclePort = {
- createSession: vi.fn().mockResolvedValue({ ...sessionSnapshot, title: 'New Chat' })
+ createSession: vi.fn().mockResolvedValue({ ...sessionSnapshot, title: 'New Chat' }),
+ createDetachedSession: vi.fn().mockResolvedValue(sessionSnapshot),
+ createSubagentSession: vi.fn().mockResolvedValue(sessionSnapshot),
+ ensureAcpDraftSession: vi.fn().mockResolvedValue(sessionSnapshot),
+ forkSession: vi.fn().mockResolvedValue(sessionSnapshot),
+ deleteSession: vi.fn().mockResolvedValue(undefined)
}
const sessionProjectionPort = {
getSession: vi.fn().mockResolvedValue(sessionSnapshot),
@@ -576,6 +468,20 @@ function createRuntime() {
activate: vi.fn().mockResolvedValue(undefined),
deactivate: vi.fn().mockResolvedValue(undefined),
getActive: vi.fn().mockResolvedValue(null),
+ getActiveId: vi.fn(() => null),
+ listLightweight: vi.fn().mockResolvedValue({
+ sessions: [],
+ nextCursor: null,
+ hasMore: false
+ }),
+ getLightweightByIds: vi.fn().mockResolvedValue([]),
+ getSearchResults: vi.fn().mockResolvedValue([]),
+ getTapeContext: vi.fn().mockResolvedValue({ entries: [] }),
+ listMessageTraces: vi.fn().mockResolvedValue([]),
+ listMessageViewManifests: vi.fn().mockResolvedValue([]),
+ exportMessageTapeReplaySlice: vi.fn().mockResolvedValue(null),
+ renameSession: vi.fn().mockResolvedValue(undefined),
+ toggleSessionPinned: vi.fn().mockResolvedValue(undefined),
getMessage: vi.fn().mockResolvedValue({
id: 'message-1',
sessionId: 'session-1',
@@ -595,9 +501,61 @@ function createRuntime() {
messageId: 'message-2'
}),
steerActiveTurn: vi.fn().mockResolvedValue(undefined),
+ listPendingInputs: vi.fn().mockResolvedValue([]),
+ queuePendingInput: vi.fn().mockResolvedValue({}),
+ updateQueuedInput: vi.fn().mockResolvedValue({}),
+ moveQueuedInput: vi.fn().mockResolvedValue([]),
+ convertPendingInputToSteer: vi.fn().mockResolvedValue({}),
+ steerPendingInput: vi.fn().mockResolvedValue({}),
+ deletePendingInput: vi.fn().mockResolvedValue(undefined),
+ retryMessage: vi.fn().mockResolvedValue(undefined),
+ deleteMessage: vi.fn().mockResolvedValue(undefined),
+ editUserMessage: vi.fn().mockResolvedValue({}),
+ getSessionCompactionState: vi.fn().mockResolvedValue({ status: 'idle' }),
+ compactSession: vi.fn().mockResolvedValue({
+ compacted: true,
+ state: {
+ status: 'compacted',
+ cursorOrderSeq: 5,
+ summaryUpdatedAt: 123
+ }
+ }),
+ clearSessionMessages: vi.fn().mockResolvedValue(undefined),
cancelGeneration: vi.fn().mockResolvedValue(undefined),
respondToolInteraction: vi.fn().mockResolvedValue({ resumed: true })
}
+ const sessionAssignmentPort = {
+ getAgentTransferImpact: vi.fn().mockResolvedValue({}),
+ moveAgentSessions: vi.fn().mockResolvedValue({ movedSessionIds: [], deletedSessionIds: [] }),
+ deleteAgentSessions: vi.fn().mockResolvedValue([]),
+ moveSessionToAgent: vi.fn().mockResolvedValue(sessionSnapshot),
+ getAcpSessionCommands: vi.fn().mockResolvedValue([]),
+ getAcpSessionConfigOptions: vi.fn().mockResolvedValue(null),
+ setAcpSessionConfigOption: vi.fn().mockResolvedValue(null),
+ getPermissionMode: vi.fn().mockResolvedValue('full_access'),
+ setPermissionMode: vi.fn().mockResolvedValue(undefined),
+ setSessionSubagentEnabled: vi.fn().mockResolvedValue(sessionSnapshot),
+ setSessionModel: vi.fn().mockResolvedValue(sessionSnapshot),
+ setSessionProjectDir: vi.fn().mockResolvedValue(sessionSnapshot),
+ getSessionGenerationSettings: vi.fn().mockResolvedValue({
+ systemPrompt: '',
+ temperature: 0.7,
+ contextLength: 32000,
+ maxTokens: 4096,
+ timeout: 5000
+ }),
+ getSessionDisabledAgentTools: vi.fn().mockResolvedValue([]),
+ updateSessionDisabledAgentTools: vi.fn().mockResolvedValue([]),
+ updateSessionGenerationSettings: vi
+ .fn()
+ .mockImplementation(async (_sessionId: string, settings: { timeout?: number }) => ({
+ systemPrompt: '',
+ temperature: 0.7,
+ contextLength: 32000,
+ maxTokens: 4096,
+ timeout: settings.timeout ?? 5000
+ }))
+ }
const sessionPermissionPort = {
clearSessionPermissions: vi.fn()
}
@@ -1344,10 +1302,10 @@ function createRuntime() {
configPresenter,
llmProviderPresenter,
acpProviderAdminPort,
- agentSessionPresenter,
sessionLifecyclePort,
sessionProjectionPort,
sessionTurnPort,
+ sessionAssignmentPort,
sessionPermissionPort,
skillPresenter,
skillSyncPresenter,
@@ -1375,10 +1333,10 @@ function createRuntime() {
configPresenter,
llmProviderPresenter,
acpProviderAdminPort,
- agentSessionPresenter,
sessionLifecyclePort,
sessionProjectionPort,
sessionTurnPort,
+ sessionAssignmentPort,
sessionPermissionPort,
skillPresenter,
skillSyncPresenter,
@@ -3635,8 +3593,7 @@ describe('dispatchDeepchatRoute', () => {
})
it('dispatches session and chat routes with renderer context', async () => {
- const { runtime, agentSessionPresenter, sessionLifecyclePort, sessionTurnPort } =
- createRuntime()
+ const { runtime, sessionLifecyclePort, sessionTurnPort } = createRuntime()
const createResult = await dispatchDeepchatRoute(
runtime,
@@ -3709,7 +3666,7 @@ describe('dispatchDeepchatRoute', () => {
}
)
- expect(agentSessionPresenter.compactSession).toHaveBeenCalledWith('session-1')
+ expect(sessionTurnPort.compactSession).toHaveBeenCalledWith('session-1')
expect(compactResult).toEqual({
compacted: true,
state: {
@@ -3721,7 +3678,7 @@ describe('dispatchDeepchatRoute', () => {
})
it('dispatches session generation settings routes without dropping timeout', async () => {
- const { runtime, agentSessionPresenter } = createRuntime()
+ const { runtime, sessionAssignmentPort } = createRuntime()
const updateResult = await dispatchDeepchatRoute(
runtime,
@@ -3750,7 +3707,7 @@ describe('dispatchDeepchatRoute', () => {
}
)
- expect(agentSessionPresenter.updateSessionGenerationSettings).toHaveBeenCalledWith(
+ expect(sessionAssignmentPort.updateSessionGenerationSettings).toHaveBeenCalledWith(
'session-1',
{
timeout: 5000
@@ -3765,7 +3722,7 @@ describe('dispatchDeepchatRoute', () => {
timeout: 5000
}
})
- expect(agentSessionPresenter.getSessionGenerationSettings).toHaveBeenCalledWith('session-1')
+ expect(sessionAssignmentPort.getSessionGenerationSettings).toHaveBeenCalledWith('session-1')
expect(getResult).toEqual({
settings: {
systemPrompt: '',
diff --git a/test/main/scripts/architectureGuard.test.ts b/test/main/scripts/architectureGuard.test.ts
index fff2f8ef4..a1b5145ed 100644
--- a/test/main/scripts/architectureGuard.test.ts
+++ b/test/main/scripts/architectureGuard.test.ts
@@ -112,7 +112,6 @@ const CAUSAL_OBSERVATION_ARROW_FIXTURE = path.join(
)
const PRESENTER_ROOT_ENTRY = path.join(ROOT, 'src/main/presenter/index.ts')
const SESSION_APPLICATION_ROOT = path.join(ROOT, 'src/main/presenter/sessionApplication')
-const SESSION_APPLICATION_PORTS_PATH = path.join(SESSION_APPLICATION_ROOT, 'ports.ts')
const REMOTE_CONTROL_PRESENTER_PATH = path.join(
ROOT,
'src/main/presenter/remoteControlPresenter/index.ts'
@@ -264,12 +263,6 @@ const SESSION_WHOLE_DEPENDENCY_FIXTURES = [
specifier: '../index',
localName: 'AppPresenter'
},
- {
- dependency: 'IAgentSessionPresenter',
- specifier: '@shared/presenter',
- localName: 'SessionFacade',
- filePath: SESSION_APPLICATION_PORTS_PATH
- },
{
dependency: 'AgentSharedDataPorts',
specifier: '@/agent/shared/agentSharedData',
@@ -938,15 +931,14 @@ describe('architecture guard', () => {
expect(result.stdout).toContain('Architecture guard passed.')
})
- it('guards the production Remote presenter path from broad session presenter access', () => {
+ it('guards the production Remote presenter path from retired session facade access', () => {
const fixtureViolations = sessionViolationsForFile(
violations,
SESSION_PRODUCTION_CONSUMER_FIXTURE.filePath
)
- expect(fixtureViolations).toEqual([
- expect.stringContaining('[session-consumer-presenter-type]'),
- expect.stringContaining('[session-consumer-presenter-facade]')
- ])
+ expect(fixtureViolations.join('\n')).toContain('[session-consumer-presenter-type]')
+ expect(fixtureViolations.join('\n')).toContain('[session-consumer-presenter-facade]')
+ expect(fixtureViolations.join('\n')).toContain('[session-retired-facade-symbol]')
})
it.each(SESSION_DUPLICATE_CONSTRUCTION_FIXTURES)(
@@ -1021,27 +1013,21 @@ describe('architecture guard', () => {
expect(fixtureViolations).toContain('[memory-legacy-list-caller]')
})
- it('keeps moved capabilities and owners out of AgentSessionPresenter', () => {
+ it('keeps the AgentSessionPresenter path and symbol retired', () => {
const fixtureViolations = forFile(violations, AGENT_SESSION_PRESENTER_PATH).join('\n')
- expect(fixtureViolations).toContain('[session-boundary-presenter-method]')
- for (const owner of [
- 'legacy import',
- 'startup migrations',
- 'usage owner or policy',
- 'RTK runtime',
- 'exporter formats',
- 'history search',
- 'session translation',
- 'agent catalog'
- ]) {
- expect(fixtureViolations).toContain(`must not import ${owner}`)
- }
+ expect(violations.join('\n')).toContain(
+ '[main-retired-path] src/main/presenter/agentSessionPresenter'
+ )
+ expect(fixtureViolations).toContain('[session-retired-facade-symbol]')
})
- it('keeps moved capabilities out of IAgentSessionPresenter', () => {
- expect(forFile(violations, AGENT_SESSION_PRESENTER_INTERFACE_PATH).join('\n')).toContain(
- '[session-boundary-interface-method]'
- )
+ it('keeps the IAgentSessionPresenter path and symbol retired', () => {
+ const fixtureViolations = forFile(
+ violations,
+ AGENT_SESSION_PRESENTER_INTERFACE_PATH
+ ).join('\n')
+ expect(fixtureViolations).toContain('[main-retired-path]')
+ expect(fixtureViolations).toContain('[session-retired-facade-symbol]')
})
it(
@@ -1104,7 +1090,7 @@ describe('architecture guard', () => {
expect(unsafeCastResult.join('\n')).toContain('[session-boundary-hook-type-cast]')
expect(unsafeCastResult.join('\n')).not.toContain('[session-boundary-hook-optional-task]')
},
- 30_000
+ 60_000
)
it('keeps Memory orchestration and injection callbacks out of the runtime presenter', () => {