mirror of
https://github.com/MoonshotAI/kimi-code.git
synced 2026-08-25 16:46:17 +00:00
refactor(agent-core-v2): clean up workspace-domain leftovers and docs
- Drop dead code: v2 mergeCallerMcpServers, the transitional ISessionContext.additionalDirs field, an unreachable guard - Fix stale domain references in comments; correct test names - Give the fs-watch refresh test a realistic wait budget under load - Document the four-scope model and workspace domain in AGENTS.md, agent-core-v2 docs, and the agent-core-dev skill
This commit is contained in:
parent
acf2059365
commit
cf3e93591f
37 changed files with 119 additions and 175 deletions
|
|
@ -14,7 +14,7 @@ v1 is a **VSCode-style singleton container**: services self-register with `regis
|
|||
|---|---|---|
|
||||
| Registration | `registerSingleton(IX, X, InstantiationType.Delayed)` | `registerScopedService(LifecycleScope.X, IX, X, ScopeActivation.OnDemand, 'domain')` |
|
||||
| DI import | `from '../../di'` | `from '#/_base/di/scope'` / `'#/_base/di/instantiation'` / `'#/_base/di/lifecycle'` |
|
||||
| Lifetime | implicit singleton-per-container | explicit `LifecycleScope` (App/Session/Agent) — see orient.md |
|
||||
| Lifetime | implicit singleton-per-container | explicit `LifecycleScope` (App/Workspace/Session/Agent) — see orient.md |
|
||||
| Domain granularity | coarse (`session`, `tool`, `loop`) | fine, split by scope + responsibility |
|
||||
| Test import | `from '@moonshot-ai/agent-core/di/test'` | `from '#/_base/di/test'` |
|
||||
| Resolve SUT in tests | `ix.createInstance(Impl)` (common) | `ix.get(IX)` by interface — see test.md |
|
||||
|
|
@ -63,7 +63,7 @@ Worked example — v1 `ISessionService` (one class, ~600 lines) holds:
|
|||
- this session's metadata → **per-session** unit → v2 `sessionMetaStore` (`ISessionMetaStore`, Session);
|
||||
- this session's activity / status → **per-session** unit → v2 `sessionActivity`;
|
||||
- this session's context projection → **per-session** unit → v2 `sessionContext`;
|
||||
- child-agent lifecycle driven by a session → **per-session** unit → v2 `agentLifecycle`; create/close/archive/fork of the session itself → **global** unit → v2 `sessionLifecycle` (App).
|
||||
- child-agent lifecycle driven by a session → **per-session** unit → v2 `agentLifecycle`; create/close/archive/fork of the session itself → **per-workspace** unit → v2 `workspaceHandler` (Workspace, one per live workspace handler).
|
||||
|
||||
A v1 class that maps cleanly to one v1 decorator often becomes **three to five** v2 Services. That is expected and correct — do not try to keep the v1 class shape.
|
||||
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ A Service = a bundle of **state** + a set of **behaviors**, bound to a **lifetim
|
|||
| Scope | State identity (keyed by) | Lifetime |
|
||||
|---|---|---|
|
||||
| `App` | none (single global instance) | the process |
|
||||
| `Workspace` | `workspaceId` | one workspace handler (materialized once per workspace, never closed — dies with the process) |
|
||||
| `Session` | `sessionId` | one session |
|
||||
| `Agent` | `agentId` | one agent |
|
||||
|
||||
|
|
@ -33,6 +34,7 @@ A Service = a bundle of **state** + a set of **behaviors**, bound to a **lifetim
|
|||
**Q2. What is the identity of that state?**
|
||||
|
||||
- one global instance → **`App`**
|
||||
- one per workspace (shared by every session of that workspace) → **`Workspace`**
|
||||
- one per session → **`Session`**
|
||||
- one per agent → **`Agent`**
|
||||
- a mix (a global registry *and* per-instance state) → **split it** (see §3).
|
||||
|
|
@ -70,7 +72,7 @@ The standard split is "global registry / factory" + "per-instance":
|
|||
| Tier | Role | Naming tends to |
|
||||
|---|---|---|
|
||||
| `App` | global registry / catalog / factory — knows "all of them" and how to create one | `XxxStore` / `XxxRegistry` / `XxxCatalog` |
|
||||
| `Session` / `Agent` | one instance — only the state of "this one" | `XxxService` / `ISessionXxx` / `IAgentXxx` |
|
||||
| `Workspace` / `Session` / `Agent` | one instance — only the state of "this one" | `XxxService` / `IWorkspaceXxx` / `ISessionXxx` / `IAgentXxx` |
|
||||
|
||||
Canonical splits in the codebase:
|
||||
|
||||
|
|
@ -189,8 +191,9 @@ domain: `<name>` (owning scope: <Scope>)
|
|||
│ └─ (accessor) <ConsumerDomain> @<Scope> — <what they use me for>
|
||||
├─ exposes (interfaces I provide, by scope)
|
||||
│ ├─ App : <IXxxRegistry> — <role>
|
||||
│ ├─ Session : <ISessionXxx> — <role>
|
||||
│ └─ Agent : <IAgentXxx> — <role>
|
||||
│ ├─ Workspace : <IWorkspaceXxx> — <role>
|
||||
│ ├─ Session : <ISessionXxx> — <role>
|
||||
│ └─ Agent : <IAgentXxx> — <role>
|
||||
└─ depends (what I inject) tag = calling style
|
||||
└─ <DepDomain> @<Scope> direct/event/hook — <what for>
|
||||
```
|
||||
|
|
@ -225,47 +228,54 @@ Read it as:
|
|||
- `──holds──►` = the ancestor owns a handle to the child scope (it stores the key, not the service). DI allows this.
|
||||
- `accessor.get(...)` = a **runtime borrow**, not a dependency edge. It must cross an `IScopeHandle`, run on demand, never be cached, and finish before the child scope is disposed.
|
||||
|
||||
Worked example — `sessionLifecycle`:
|
||||
Worked example — `workspaceHandler`:
|
||||
|
||||
```text
|
||||
domain: `sessionLifecycle` (owning scope: App)
|
||||
domain: `workspaceHandler` (owning scope: Workspace)
|
||||
├─ serves (who uses me)
|
||||
│ ├─ (inject) — (none yet)
|
||||
│ ├─ (inject) — (none)
|
||||
│ └─ (accessor)
|
||||
│ ├─ sessionLegacy @App(edge) — v1-compatible create/fork/archive/…
|
||||
│ └─ gateway / rpc @App(edge) — native v2 session lifecycle actions
|
||||
├─ exposes (interfaces I provide, by scope)
|
||||
│ ├─ App : ISessionLifecycleService — owns the live session scope tree
|
||||
│ ├─ Workspace : IWorkspaceHandlerService — owns this workspace's live session scope tree
|
||||
│ ├─ Session : — — (per-session state lives in sessionMetadata / agentLifecycle / …)
|
||||
│ └─ Agent : — — (per-agent state lives in agentLifecycle)
|
||||
└─ depends (what I inject)
|
||||
├─ bootstrap @App direct — addresses session storage
|
||||
├─ hostEnvironment @App direct — gates scope creation on the probe
|
||||
├─ sessionIndex @App direct — persisted read model for cold resumes
|
||||
├─ storage @App direct — atomic docs + append logs
|
||||
├─ workspace @App direct — resolves a session's workspace
|
||||
└─ event @App direct — broadcasts session-level facts (e.g. archived)
|
||||
├─ workspaceContext @Workspace seed — handler identity + persistence scope
|
||||
├─ bootstrap @App direct — addresses session storage
|
||||
├─ hostEnvironment @App direct — gates scope creation on the probe
|
||||
├─ sessionIndex @App direct — persisted read model for cold resumes
|
||||
├─ storage @App direct — atomic docs + append logs
|
||||
├─ workspaceDirs / workspaceSkillCatalog / workspaceMcp / …
|
||||
│ @Workspace direct — the handler's shared resource services
|
||||
└─ event @App direct — broadcasts session-level facts (e.g. archived)
|
||||
```
|
||||
|
||||
Cross-scope borrow for `sessionLifecycle`:
|
||||
Cross-scope borrow for `workspaceHandler`:
|
||||
|
||||
```text
|
||||
App scope
|
||||
SessionLifecycleService ──holds──┐
|
||||
GatewayService ───────────holds──┼──► IScopeHandle(sessionId)
|
||||
│
|
||||
│ accessor.get(ISessionMetadata) …
|
||||
│ └── resolve runs inside the Session scope
|
||||
▼
|
||||
Session scope (sessionId)
|
||||
sessionMetadata / agentLifecycle / … ← per-session services live here
|
||||
WorkspaceLifecycleService ──holds──► IScopeHandle(workspaceId) (one per live handler)
|
||||
│
|
||||
│ accessor.get(IWorkspaceHandlerService)
|
||||
│ └── resolve runs inside the Workspace scope
|
||||
▼
|
||||
Workspace scope (workspaceId)
|
||||
WorkspaceHandlerService ──holds──► IScopeHandle(sessionId)
|
||||
│
|
||||
│ accessor.get(ISessionMetadata) …
|
||||
│ └── resolve runs inside the Session scope
|
||||
▼
|
||||
Session scope (sessionId)
|
||||
sessionMetadata / agentLifecycle / … ← per-session services live here
|
||||
```
|
||||
|
||||
How the three lenses shaped it:
|
||||
|
||||
- **Scope (§2)** → the live registry of session scopes is process-wide, so it is App-scoped; per-session data stays in Session-scoped services, reached through the handle's `accessor`.
|
||||
- **Dependency direction (§5)** → `sessionLifecycle` is consumed by the edge via `accessor` borrows; it never imports the edge. Every downward arrow lands on a peer or a more foundational Service.
|
||||
- **Extension points (§4)** → new per-session behavior plugs into the Session-scoped services (`sessionMetadata`, `agentLifecycle`, `sessionActivity`); new transports stay at the edge. Neither edits `sessionLifecycle`.
|
||||
- **Scope (§2)** → the live registry of one workspace's session scopes is per-handler, so it is Workspace-scoped; the process-wide handler registry lives in the App-scoped `workspaceLifecycle`; per-session data stays in Session-scoped services, reached through the handle's `accessor`.
|
||||
- **Dependency direction (§5)** → `workspaceHandler` is consumed by the edge via `accessor` borrows; it never imports the edge. Every downward arrow lands on a peer or a more foundational Service.
|
||||
- **Extension points (§4)** → new per-session behavior plugs into the Session-scoped services (`sessionMetadata`, `agentLifecycle`, `sessionActivity`); new transports stay at the edge. Neither edits `workspaceHandler`.
|
||||
|
||||
For a multi-scope split, the `exposes` block fills more than one scope — see the `records` pattern in §3.
|
||||
|
||||
|
|
|
|||
|
|
@ -82,7 +82,7 @@ The `session` domain owns only Session-level identity, metadata, lifecycle comma
|
|||
|---|---|---|
|
||||
| `sessionId`, `workspaceId`, `sessionDir`, `metaScope` | `sessionContext` | Seeded facts; no IO |
|
||||
| `SessionMeta` | `sessionMetadata` | Durable atomic document; entity-like |
|
||||
| Open session scope registry | `sessionLifecycle` | App-scope live handles; not the persisted entity table |
|
||||
| Open session scope registry | `workspaceHandler` | Workspace-scope live handles, one registry per workspace handler (the process-wide handler registry is `workspaceLifecycle`); not the persisted entity table |
|
||||
| Session commands such as `archive()` | `session` | Orchestrates metadata, agent teardown, and events |
|
||||
| Persisted session list / get / count | `sessionIndex` | Backend-neutral read model |
|
||||
| Running / idle / awaiting status | `sessionActivity` | Derived from interactions and active turns; owns no state |
|
||||
|
|
|
|||
|
|
@ -6,10 +6,11 @@ The transport (`/api/v2` over HTTP + WS) lives in the **edge** layer (`gateway`/
|
|||
|
||||
## 1. The edge model
|
||||
|
||||
Three scopes, three URL shapes, one dispatcher:
|
||||
Four scopes, four URL shapes, one dispatcher:
|
||||
|
||||
```text
|
||||
GET|POST /api/v2/:sa Core
|
||||
GET|POST /api/v2/workspace/:workspace_id/:sa Workspace
|
||||
GET|POST /api/v2/session/:session_id/:sa Session
|
||||
GET|POST /api/v2/session/:session_id/agent/:agent_id/:sa Agent
|
||||
```
|
||||
|
|
@ -26,9 +27,10 @@ GET|POST /api/v2/session/:session_id/agent/:agent_id/:sa Agent
|
|||
```ts
|
||||
// actionMap — the allowlist; hides internal domain names.
|
||||
const actionMap = {
|
||||
core: { 'sessions:list': { service: ISessionIndex, method: 'list' }, ... },
|
||||
session: { 'session:read': { service: ISessionMetadata, method: 'read' }, ... },
|
||||
agent: { 'profile:getModel': { service: IProfileService, method: 'getModel' }, ... },
|
||||
core: { 'sessions:list': { service: ISessionIndex, method: 'list' }, ... },
|
||||
workspace: { 'skills:list': { service: IWorkspaceSkillCatalog, method: 'list' }, ... },
|
||||
session: { 'session:read': { service: ISessionMetadata, method: 'read' }, ... },
|
||||
agent: { 'profile:getModel': { service: IProfileService, method: 'getModel' }, ... },
|
||||
};
|
||||
```
|
||||
|
||||
|
|
@ -83,14 +85,14 @@ Read = `GET`, write = `POST`. `sid` = `session_id`, `aid` = `agent_id`.
|
|||
| `session` | `setArchived` | ISessionMetadata.setArchived | POST |
|
||||
| `session` | `status` | ISessionActivity.status | GET |
|
||||
| `session` | `isIdle` | ISessionActivity.isIdle | GET |
|
||||
| `session` | `archive` | ISessionLifecycleService.archive | POST |
|
||||
| `session` | `archive` | IWorkspaceHandlerService.archive | POST |
|
||||
| `approvals` | `listPending` | IApprovalService.listPending | GET |
|
||||
| `approvals` | `decide` | IApprovalService.decide | POST |
|
||||
| `questions` | `listPending` | IQuestionService.listPending | GET |
|
||||
| `questions` | `answer` | IQuestionService.answer | POST |
|
||||
| `interactions` | `listPending` | IInteractionService.listPending | GET |
|
||||
| `interactions` | `respond` | IInteractionService.respond | POST |
|
||||
| `workspace` | `workDir` / `additionalDirs` / `resolve` | IWorkspaceContext.* | GET |
|
||||
| `workspace` | `workDir` / `additionalDirs` / `resolve` | ISessionWorkspaceContext.* | GET |
|
||||
|
||||
### Agent (`/api/v2/session/:sid/agent/:aid/:resource:action`)
|
||||
|
||||
|
|
@ -126,7 +128,7 @@ These fail §2 and must be wrapped in a facade that takes ids and returns data:
|
|||
|
||||
| Service | Why not direct | Facade shape |
|
||||
|---|---|---|
|
||||
| ISessionLifecycleService | returns `IScopeHandle` | `sessions.create` / `fork` / `close` / `archive` → wire Session |
|
||||
| IWorkspaceHandlerService | returns `IScopeHandle` | `sessions.create` / `fork` / `close` / `archive` → wire Session |
|
||||
| IAgentPromptService / IAgentTurnService | returns `Turn` handle | `prompts.submit` / `steer` / `abort` / `undo` |
|
||||
| ILLMRequester | `AsyncIterable` stream | stream over WS, not RPC |
|
||||
| ISubagentHost | `SubagentHandle` | `subagents.spawn` / `resume` → info |
|
||||
|
|
|
|||
|
|
@ -246,7 +246,7 @@ If A needs B while being created and B needs A while being created, the containe
|
|||
|
||||
### Why cycles are disallowed
|
||||
|
||||
- Scope layering makes normal dependencies a DAG (Agent → Session → App, resolving upward); a cycle is almost always a design smell.
|
||||
- Scope layering makes normal dependencies a DAG (Agent → Session → Workspace → App, resolving upward); a cycle is almost always a design smell.
|
||||
- "Making the cycle happen to work" turns construction order into an implicit contract — hard to debug.
|
||||
|
||||
v2's stance: **the dependency graph must be acyclic.**
|
||||
|
|
|
|||
|
|
@ -75,7 +75,7 @@ So a Session-scoped service is not "L1" — e.g. `session` is Session-scoped but
|
|||
|
||||
- **Header only.** Comments live solely in the top-of-file `/** */` block — never beside functions, methods, or statements. The code is the source of truth for *how*; the header states *what the module exposes and the responsibility it owns*.
|
||||
- **Identity line first.** Start with `` `<domain>` domain (Ln) — <one-line role>. `` Keep an existing `(cross-cutting)` label as-is. Write the role as a responsibility ("drives the turn lifecycle"), not a symbol list.
|
||||
- **Scope is in the filename.** `session*.ts` = Session, `agent*.ts` = Agent, no prefix = App (see service-authoring.md). State the same scope in the header so the two never drift.
|
||||
- **Scope is in the filename.** `workspace*.ts` = Workspace, `session*.ts` = Session, `agent*.ts` = Agent, no prefix = App (see service-authoring.md). State the same scope in the header so the two never drift.
|
||||
- **Interface files** (`<name>.ts`) state the public contract + scope: which `IXxx` they define and what it is for.
|
||||
- **Impl files** (`<name>Service.ts`) add collaborators + scope: list every imported cross-domain collaborator as a role ("persists records through `records`"); read scope from `registerScopedService(LifecycleScope.X, …)`.
|
||||
- **Contribution files** (`<targetDomain>.ts` / `<what>.contrib.ts`) state what they register into the target domain (e.g. "registers the `log` config section into `config`").
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ One folder per domain, **camelCase**: `session/`, `sessionActivity/`, `contextMe
|
|||
```
|
||||
|
||||
- **Strictly one service per file.** An interface file holds exactly one injectable interface and exactly one `createDecorator(...)`; an impl file holds exactly one service implementation class and exactly one `registerScopedService(...)`. No exceptions for "tightly-coupled" groups: even same-scope collaborators each get their own `<name>.ts` + `<name>Service.ts` pair.
|
||||
- **Scope is in the filename.** `session*.ts` = Session, `agent*.ts` = Agent, no scope prefix = App (see [Naming](#naming)). The header comment restates the same scope.
|
||||
- **Scope is in the filename.** `workspace*.ts` = Workspace, `session*.ts` = Session, `agent*.ts` = Agent, no scope prefix = App (see [Naming](#naming)). The header comment restates the same scope.
|
||||
- A domain therefore has as many impl files as it has services (e.g. `logService.ts` for the App `ILogService`, `sessionLogService.ts` for the Session `ISessionLogService`). See [Multi-Service domains](#multi-service-domains).
|
||||
|
||||
The package entry `src/index.ts` imports and `export *`s every domain's leaf files precisely (one line per leaf), so importing the package still runs every `registerScopedService(...)` side effect — exactly as the old per-domain barrels did.
|
||||
|
|
@ -28,12 +28,12 @@ The package entry `src/index.ts` imports and `export *`s every domain's leaf fil
|
|||
|
||||
| Artifact | Rule | Example |
|
||||
|---|---|---|
|
||||
| Interface | `I` + scope prefix + PascalCase domain + role suffix. Scope prefix: `Session` / `Agent` / none (= App). Role suffix is usually `Service`. | `ISessionLogService`, `IAgentLoopService`, `ILogService` (App) |
|
||||
| Interface | `I` + scope prefix + PascalCase domain + role suffix. Scope prefix: `Workspace` / `Session` / `Agent` / none (= App). Role suffix is usually `Service`. | `IWorkspaceDirs`, `ISessionLogService`, `IAgentLoopService`, `ILogService` (App) |
|
||||
| Class | the interface name minus the leading `I`, plus `Service` if it does not already end in `Service`; `implements` the interface | `SessionLogService implements ISessionLogService`, `AppendLogStoreService implements IAppendLogStore` |
|
||||
| Decorator string | lowerCamelCase of the interface name minus the leading `I`; **globally unique and stable** (it surfaces in `CyclicDependencyError.path` and "no service registered" errors) | `createDecorator<ISessionLogService>('sessionLogService')` |
|
||||
| Model / non-service types | PascalCase, no `I` prefix | `SessionMeta`, `LogEntry`, `ConfigSection` |
|
||||
|
||||
The scope prefix makes a service's lifetime readable from its name. App services carry **no** prefix (App is the default, longest-lived tier); Session and Agent services always carry `Session` / `Agent`. The prefix applies to the interface, the class, and therefore the file names.
|
||||
The scope prefix makes a service's lifetime readable from its name. App services carry **no** prefix (App is the default, longest-lived tier); Workspace, Session and Agent services always carry `Workspace` / `Session` / `Agent`. The prefix applies to the interface, the class, and therefore the file names.
|
||||
|
||||
> Do **not** use the scope prefix to re-merge domains by lifetime. `IAgentEntityService`, `IAgentDataService`, and `ISessionEntityService` are still banned — the prefix marks lifetime, the rest of the name must still be the real owning domain (`IBackgroundTaskEntityService`, `ISessionMetadata`, `IPermissionRulesService`). See [domain-boundaries.md](domain-boundaries.md).
|
||||
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -2,6 +2,10 @@
|
|||
|
||||
> New agent engine built on the DI Scope architecture — work-in-progress port of `packages/agent-core`. Design: `plan/PLAN.md`. Porting status: `GAP_ANALYSIS.md`.
|
||||
|
||||
## Scopes
|
||||
|
||||
Four `LifecycleScope` tiers — `App` (0) / `Workspace` (1) / `Session` (2) / `Agent` (3) (`src/_base/di/scope.ts`). The `workspace/` domain owns the Workspace tier: the App-scope `workspaceLifecycle` holds the live handler registry (one handler per workspaceId, create-or-get + join, never closed), and each handler's `workspaceHandler` owns the session lifecycle (create/resume/fork/close) as its child scopes. Workspace-scope services (`workspaceSkillCatalog` / `workspaceAgentProfileCatalog` / `workspaceInstructions` / `workspaceMcp` / `workspaceDirs` / `workspaceFs` / `workspaceFsWatch` / `workspaceProcess` / `workspaceGit` / `workspaceToolPolicy`) hold the handler-shared resources — loaded once at handler materialization, then refreshed by fs watch — and sessions consume them through session-domain seed contracts with change events. Dependency red line: **Session/Agent never import the Workspace domain**; the App-level `ISessionLifecycleService` / `ISessionMcpService` / `ISessionFsService` are gone — compose `sessionIndex` → `workspaceLifecycle.handlerFor` → the handler instead.
|
||||
|
||||
## Examples
|
||||
|
||||
> The runnable examples have moved to the standalone `kimi-code-mini-bench` package at `../kimi-code-mini-bench`. They are wired to `agent-core-v2` through a pnpm `link:` dependency and run as a separate Vitest project.
|
||||
|
|
|
|||
|
|
@ -139,15 +139,16 @@ const meta = accessor.get(ISessionMetadata); // 类型是 ISessionMetadata
|
|||
|
||||
> 你要做的:每个会话一份、或每个 agent 一份。参考 [`sessionMetadata`](../src/session/sessionMetadata/sessionMetadata.ts)、[`turn`](../src/turn/turn.ts)。
|
||||
|
||||
这一步引入:**`LifecycleScope` 三层生命周期** 与 **父子 scope 的可见性**。
|
||||
这一步引入:**`LifecycleScope` 四层生命周期** 与 **父子 scope 的可见性**。
|
||||
|
||||
### 3.1 三层,按寿命从长到短
|
||||
### 3.1 四层,按寿命从长到短
|
||||
|
||||
```ts
|
||||
export enum LifecycleScope {
|
||||
App = 0, // 进程级,全局一份
|
||||
Session = 1, // 一次会话
|
||||
Agent = 2, // 一个 agent
|
||||
App = 0, // 进程级,全局一份
|
||||
Workspace = 1, // 一个工作区 handler(与 Session 一对多)
|
||||
Session = 2, // 一次会话
|
||||
Agent = 3, // 一个 agent
|
||||
}
|
||||
```
|
||||
|
||||
|
|
@ -171,15 +172,16 @@ Scope 是一棵树,`kind` 必须沿父子方向**严格递增**:
|
|||
|
||||
```
|
||||
App (0)
|
||||
└── Session (1)
|
||||
└── Agent (2)
|
||||
└── Workspace (1)
|
||||
└── Session (2)
|
||||
└── Agent (3)
|
||||
```
|
||||
|
||||
解析服务时,容器先看自己这一层,没有就**递归问父 scope**。所以一条铁律:
|
||||
|
||||
> **短寿命的服务可以注入长寿命的服务,反过来不行。**
|
||||
|
||||
- ✅ Agent 服务注入 Session / App 服务(往上找,找得到)。
|
||||
- ✅ Agent 服务注入 Session / Workspace / App 服务(往上找,找得到)。
|
||||
- ❌ App 服务注入 Session 服务(App 创建时 Session 还不存在,且父不会往下找)。
|
||||
|
||||
这条规则由树的结构强制保证,不靠纪律维持。
|
||||
|
|
@ -350,7 +352,7 @@ A 创建中要 B,B 创建中又要 A——容器会抛 `CyclicDependencyError`
|
|||
|
||||
### 9.2 为什么不允许
|
||||
|
||||
- scope 分层让正常依赖天然是 DAG(Agent → Session → App 向上找),一个环几乎总是设计味道。
|
||||
- scope 分层让正常依赖天然是 DAG(Agent → Session → Workspace → App 向上找),一个环几乎总是设计味道。
|
||||
- 靠「让环刚好能跑」会把构造顺序变成隐式约定,难调试、难排错。
|
||||
|
||||
所以 v2 的立场是:**依赖图必须是无环的。**
|
||||
|
|
|
|||
|
|
@ -65,7 +65,7 @@
|
|||
- W3 Session 域借 main agent 的 wire 写(todo/cron),main 缺失时**静默丢写**
|
||||
(`sessionTodoService.ts:99-100`),且要 `as never` 绕过类型。
|
||||
- W4 fork 直接在 appendLogStore 层改写 wire log,绕过全部写模型
|
||||
(`sessionLifecycleService.ts:303-337`)。
|
||||
(`workspaceHandlerService.ts` 的 `fork` / `copyAgentWire`)。
|
||||
- W5 restore 期 append 在 wireRecord 层被静默吞掉(`wireRecordService.ts:81`),
|
||||
但 recordService 仍然 foldViews、仍然跑 facet——"进内存不进磁盘"完全隐式。
|
||||
|
||||
|
|
@ -98,7 +98,7 @@
|
|||
onChange 处理器若 append 会无检测地重入。
|
||||
- L3 restore 正确性依赖三重隐式契约:DI 构造顺序 + hook 注册顺序 +
|
||||
"resumer 先于 hooks";`doResume` 需手动预热 contextMemory
|
||||
(`sessionLifecycleService.ts:158-162`)。
|
||||
(现 `workspaceHandlerService.ts` 的 `doResume` / `materializeSession`)。
|
||||
- L4 相位规则(restoring / postRestoring / live)在 append/signal/push/hook
|
||||
四条通道上各不相同,没有一处集中定义。
|
||||
|
||||
|
|
@ -189,7 +189,7 @@
|
|||
逻辑 seq 顺序,因此边缘 journal 的 seq 与核心逻辑 seq 单调一致。
|
||||
- fork 保持现实现(复制 main 的 wire log);接口上表达为
|
||||
`stream.forkInto(target)`,实现仍走 appendLogStore(W4 的接口层收口:
|
||||
唯一入口,不再散落在 sessionLifecycle 里手写)。
|
||||
唯一入口,不再散落在 workspaceHandler 里手写)。
|
||||
- App scope 一条逻辑流(config/model catalog/session 生命周期),取代
|
||||
`IEventService`(V4)——App 流本就无持久化,纯接口替换。
|
||||
- **Topic = 流上的类型化过滤视角**,不是独立机制。订阅方用
|
||||
|
|
|
|||
|
|
@ -37,11 +37,12 @@ Every principle below derives from two root questions:
|
|||
|
||||
**First principle: Scope = the identity + lifetime of the owned state.**
|
||||
|
||||
`App` / `Session` / `Agent` are three tiers of identity + lifetime:
|
||||
`App` / `Workspace` / `Session` / `Agent` are four tiers of identity + lifetime:
|
||||
|
||||
| Scope | State identity (keyed by) | Lifetime |
|
||||
|---|---|---|
|
||||
| `App` | none (single global instance) | the process |
|
||||
| `Workspace` | `workspaceId` | one workspace handler (materialized once per workspace, never closed — dies with the process) |
|
||||
| `Session` | `sessionId` | one session |
|
||||
| `Agent` | `agentId` | one agent |
|
||||
|
||||
|
|
@ -55,6 +56,7 @@ Every principle below derives from two root questions:
|
|||
**Q2. What is the identity of that state?**
|
||||
|
||||
- one global instance → **`App`**
|
||||
- one per workspace (shared by every session of that workspace) → **`Workspace`**
|
||||
- one per session → **`Session`**
|
||||
- one per agent → **`Agent`**
|
||||
- a mix (a global registry *and* per-instance state) → **do not put it in one Service;
|
||||
|
|
@ -108,7 +110,7 @@ job well.
|
|||
| Tier | Role | Naming tends to |
|
||||
|---|---|---|
|
||||
| `App` | **global registry / catalog / factory** — knows "all of them" and how to create one | `XxxStore` / `XxxRegistry` / `XxxCatalog` |
|
||||
| `Session` / `Agent` | **one instance** — only the state of "this one" | `XxxService` / `ISessionXxx` / `IAgentXxx` |
|
||||
| `Workspace` / `Session` / `Agent` | **one instance** — only the state of "this one" | `XxxService` / `IWorkspaceXxx` / `ISessionXxx` / `IAgentXxx` |
|
||||
|
||||
This pattern recurs throughout the codebase and confirms the rule:
|
||||
|
||||
|
|
|
|||
|
|
@ -228,7 +228,7 @@ const DOMAIN_LAYER = new Map([
|
|||
// `activityView` is the Agent-scope read model folding the agent's own event
|
||||
// bus into the activity projection (`agent.activity.updated`); it owns no
|
||||
// authoritative state (turn mechanics live in `loop`, admission/drain in
|
||||
// `sessionLifecycle`, background bookkeeping in `agentLifecycle`).
|
||||
// `workspaceHandler`, background bookkeeping in `agentLifecycle`).
|
||||
['activityView', 4],
|
||||
['context', 4],
|
||||
['message', 4],
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
* File content metadata helpers — binary detection, line counting, etag, and
|
||||
* extension-based mime / language guessing.
|
||||
*
|
||||
* Shared by the fs edge domains (`sessionFs`) and the kap-server fs routes so
|
||||
* Shared by the fs edge domains (`workspaceFs`) and the kap-server fs routes so
|
||||
* every read-style surface classifies and labels file content the same way.
|
||||
* Pure functions over bytes, text, and stat-like shapes; no io happens here.
|
||||
* Binary detection samples the leading `FS_BINARY_SAMPLE_BYTES` of a file and
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ const ISO_8601_REGEX =
|
|||
/**
|
||||
* Wire-schema primitive for ISO 8601 datetime strings: validates the shape and
|
||||
* normalizes to `Date#toISOString()` output. Shared by the edge DTO schemas
|
||||
* (`sessionFs`, `file`, `terminal`, `auth`, …) that expose timestamps.
|
||||
* (`workspaceFs`, `file`, `terminal`, `auth`, …) that expose timestamps.
|
||||
*/
|
||||
export const isoDateTimeSchema = z
|
||||
.string()
|
||||
|
|
|
|||
|
|
@ -21,18 +21,3 @@ export async function resolveSessionMcpConfig(
|
|||
if (Object.keys(servers).length === 0) return undefined;
|
||||
return { servers };
|
||||
}
|
||||
|
||||
export function mergeCallerMcpServers(
|
||||
base: SessionMcpConfig | undefined,
|
||||
callerServers: Readonly<Record<string, McpServerConfig>> | undefined,
|
||||
): SessionMcpConfig | undefined {
|
||||
if (callerServers === undefined || Object.keys(callerServers).length === 0) {
|
||||
return base;
|
||||
}
|
||||
return {
|
||||
servers: {
|
||||
...base?.servers,
|
||||
...callerServers,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
* Top-level boolean preference (`default_plan_mode` on disk, v1-compatible):
|
||||
* when `true`, every freshly created session starts in plan mode. Resumed /
|
||||
* forked sessions restore plan state from wire records and ignore this. Read by
|
||||
* `sessionLifecycle` at session creation; runtime plan state lives on the wire
|
||||
* `workspaceHandler` at session creation; runtime plan state lives on the wire
|
||||
* `PlanModel`, not here.
|
||||
*/
|
||||
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@
|
|||
* `agent.status.updated` planMode slice — are NOT part of `apply`: they run
|
||||
* after `wire.dispatch` on the live path, and `wire.replay` rebuilds the
|
||||
* Model silently from the persisted `plan_mode.*` / `plan.revision` records
|
||||
* (seeded by `sessionLifecycle`). The legacy `toReplay: plan_updated`
|
||||
* (seeded by `workspaceHandler`). The legacy `toReplay: plan_updated`
|
||||
* projection is dropped (inert — nothing reads it). `plan.revision` carries
|
||||
* a `toEvent` so the live transcript projector can map it onto a marker plus
|
||||
* the plan badge; replay never emits it. Consumed by the Agent-scope
|
||||
|
|
|
|||
|
|
@ -2,8 +2,9 @@
|
|||
* `gateway` domain (L7) — REST/WS gateways.
|
||||
*
|
||||
* Defines the public contracts of the gateway layer: the `IRestGateway` /
|
||||
* `IWSGateway` entry points. Session scope creation is owned by
|
||||
* `sessionLifecycle`; the gateway resolves sessions through it.
|
||||
* `IWSGateway` entry points. Session scope creation is owned by the workspace
|
||||
* handler (`workspaceHandler`); the gateway resolves sessions through the live
|
||||
* handler registry (`workspaceLifecycle`).
|
||||
* App-scoped — shared across the application.
|
||||
*/
|
||||
|
||||
|
|
|
|||
|
|
@ -3,8 +3,8 @@
|
|||
*
|
||||
* Defines the `IHostFolderBrowser` used by the program side (TUI / server) to
|
||||
* let the user browse the real local filesystem when choosing a workspace
|
||||
* folder. Distinct from the Session-side `sessionFs`, which is sandboxed and may
|
||||
* be remote. App-scoped.
|
||||
* folder. Distinct from the Workspace-side `workspaceFs`, which is sandboxed and
|
||||
* may be remote. App-scoped.
|
||||
*
|
||||
* The wire shapes (`FsBrowseResponse` / `FsHomeResponse`) are defined here as
|
||||
* zod schemas so the `/api/v1` and `/api/v2` transports share one contract.
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
* query facade over the set of persisted sessions (open or closed). It
|
||||
* enumerates sessions and derives session identity (`workspaceId`), returning
|
||||
* data (`SessionSummary`) or counts — never filesystem paths or live handles.
|
||||
* Writes (create / archive) live in `sessionLifecycle` / `session`; the index
|
||||
* Writes (create / archive) live in `workspaceHandler` / `session`; the index
|
||||
* is a read model. Backends are deployment-specific (local filesystem today;
|
||||
* database / query store on a server).
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -5,12 +5,12 @@
|
|||
* metadata merge, and the cross-domain `agent_config` patch),
|
||||
* `GET /sessions/{id}/status` (`status`), and `GET /sessions/{id}/goal`
|
||||
* (`goal`) on top of the native v2 services
|
||||
* (`ISessionLifecycleService`, `IAgentProfileService`, …).
|
||||
* (`IWorkspaceHandlerService`, `IAgentProfileService`, …).
|
||||
*
|
||||
* The thin pass-through actions (`fork` / `compact` / `abort` / `archive`), the
|
||||
* `:undo` action, and the `/sessions/{id}/children` endpoints are deliberately
|
||||
* NOT wrapped here: the edge route calls the native services directly —
|
||||
* `ISessionLifecycleService.fork` / `archive` / `createChild`,
|
||||
* `IWorkspaceHandlerService.fork` / `archive` / `createChild`,
|
||||
* `IAgentFullCompactionService.begin`, `IAgentRPCService.cancel`,
|
||||
* `IAgentPromptService.undo`, and `ISessionIndex.list({ childOf })` — because
|
||||
* none of them carries v1-only projection worth centralizing beyond what the
|
||||
|
|
|
|||
|
|
@ -11,7 +11,8 @@
|
|||
*
|
||||
* Async initialization: probing (`ready`) discovers the shell path — on
|
||||
* Windows this may run `git.exe --exec-path`. The composition root
|
||||
* (`sessionLifecycle`) `await`s `ready` before creating any Session scope, so
|
||||
* (`workspaceLifecycle` / `workspaceHandler`) `await`s `ready` before creating
|
||||
* any Session scope, so
|
||||
* every Session/Agent-scope consumer reads the sync fields safely.
|
||||
*
|
||||
* App-scoped — one shared instance for the whole process.
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
* Defines the `IHostFsWatchService`, a thin primitive over the host OS file
|
||||
* watcher. It reports raw create/modify/delete events under an absolute path
|
||||
* and knows nothing about sessions, connections, workspaces or wire frames.
|
||||
* App-scoped — one shared instance. Higher layers (e.g. `sessionFsWatch`)
|
||||
* App-scoped — one shared instance. Higher layers (e.g. `workspaceFsWatch`)
|
||||
* subscribe, confine events to a workspace, debounce/coalesce and re-expose
|
||||
* them as domain events.
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -5,9 +5,10 @@
|
|||
* registry (`get` / `list` / `remove`), and the lifecycle events — plus the
|
||||
* session-wide fan-outs only the live registry can reach
|
||||
* (`broadcastPermissionMode`). Driving turns on an agent — and the hook/event
|
||||
* surface those runs announce — lives in the `subagent` domain; session-level
|
||||
* MCP lives in the `sessionMcp` domain. Session-scoped — one instance per
|
||||
* session.
|
||||
* surface those runs announce — lives in the `subagent` domain; the shared MCP
|
||||
* connection manager lives in the Workspace-scope `workspaceMcp` domain and
|
||||
* reaches agents through the seeded `ISessionMcpHandle`. Session-scoped — one
|
||||
* instance per session.
|
||||
*
|
||||
* Invariants:
|
||||
* - The registry is flat: agents have no nesting. There is no parent/child or
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@
|
|||
* and returns an already-created main agent as-is — so concurrent
|
||||
* bootstrappers always receive the same, fully-bootstrapped handle (activity
|
||||
* lane `idle`). Session services activated when their scope is created (cron,
|
||||
* external hooks) are materialized by `sessionLifecycle.materializeSession`;
|
||||
* external hooks) are materialized by `workspaceHandler.materializeSession`;
|
||||
* the default permission posture is
|
||||
* applied in `bindBootstrap`.
|
||||
*
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
/**
|
||||
* `session` domain error codes — shared across the session layer
|
||||
* (`sessionLifecycle` / `sessionLegacy` / `messageLegacy`).
|
||||
* (`workspaceHandler` / `sessionLegacy` / `messageLegacy`).
|
||||
*/
|
||||
|
||||
import { registerErrorDomain, type ErrorDomain } from '#/_base/errors/codes';
|
||||
|
|
|
|||
|
|
@ -3,16 +3,15 @@
|
|||
*
|
||||
* Defines the `ISessionContext` carrying the session's identity, storage
|
||||
* addressing (`sessionId`, `workspaceId`, `sessionDir`, `metaScope`), the
|
||||
* session's working directory (`cwd`) — frozen at session creation — the
|
||||
* materialization-time snapshot of the handler's additional workspace
|
||||
* directories (`additionalDirs`), and a `scope(subKey?)`
|
||||
* session's working directory (`cwd`) — frozen at session creation — and a
|
||||
* `scope(subKey?)`
|
||||
* helper that returns the session's persistence scope (or a child under it,
|
||||
* e.g. `scope('agents/main/cron')`). Seeded into the Session scope by
|
||||
* `workspaceHandler` when the session is created.
|
||||
*
|
||||
* `cwd` is the default root the `process` runner spawns in and the seed the
|
||||
* `workspaceContext` derives its read-only `workDir` / `additionalDirs` from.
|
||||
* Pure facts — no store, no IO. Session-scoped.
|
||||
* `workspaceContext` derives its read-only `workDir` from. Pure facts — no
|
||||
* store, no IO. Session-scoped.
|
||||
*/
|
||||
|
||||
import { createDecorator, type ServiceIdentifier } from '#/_base/di/instantiation';
|
||||
|
|
@ -26,14 +25,6 @@ export interface ISessionContext {
|
|||
readonly sessionDir: string;
|
||||
readonly metaScope: string;
|
||||
readonly cwd: string;
|
||||
/**
|
||||
* Extra directories beyond `cwd` the session may touch — a snapshot of the
|
||||
* handler-shared set (`workspaceDirs`: project-local `.kimi-code/local.toml`
|
||||
* ∪ caller-provided dirs) taken at materialization. Live updates reach
|
||||
* consumers through the `ISessionWorkspaceInfo` seed; this field stays the
|
||||
* creation-time snapshot.
|
||||
*/
|
||||
readonly additionalDirs?: readonly string[];
|
||||
scope(subKey?: string): string;
|
||||
}
|
||||
|
||||
|
|
@ -50,7 +41,6 @@ export function makeSessionContext(input: {
|
|||
readonly sessionDir: string;
|
||||
readonly sessionScope: string;
|
||||
readonly cwd: string;
|
||||
readonly additionalDirs?: readonly string[];
|
||||
readonly metaScope?: string;
|
||||
}): ISessionContext {
|
||||
const { sessionScope } = input;
|
||||
|
|
@ -61,7 +51,6 @@ export function makeSessionContext(input: {
|
|||
sessionDir: input.sessionDir,
|
||||
metaScope: input.metaScope ?? sessionScope,
|
||||
cwd: input.cwd,
|
||||
additionalDirs: input.additionalDirs,
|
||||
scope: (subKey?: string): string =>
|
||||
subKey === undefined || subKey === '' ? sessionScope : `${sessionScope}/${subKey}`,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -154,9 +154,11 @@ export class WorkspaceDirsService extends Disposable implements IWorkspaceDirs {
|
|||
* Watch the project root recursively, pruned to the `local.toml`
|
||||
* candidate: watching the file directly never fires when its parent
|
||||
* `.kimi-code` directory does not exist yet either.
|
||||
*
|
||||
* Runs only after `ready` resolves, so `reloadFromDisk` has already
|
||||
* populated `projectRoot` / `configPath`.
|
||||
*/
|
||||
private watchLocalToml(): void {
|
||||
if (this.configPath === '') return;
|
||||
try {
|
||||
const handle = this.fsWatch.watch(this.projectRoot, {
|
||||
recursive: true,
|
||||
|
|
|
|||
|
|
@ -226,8 +226,8 @@ export class WorkspaceHandlerService extends Disposable implements IWorkspaceHan
|
|||
// across all sessions of this workspace, §6.1) — the workspace dirs
|
||||
// service owns the local.toml set and its watch; sessions read the
|
||||
// combined view through the `ISessionWorkspaceInfo` seed below. Await
|
||||
// the initial local.toml load first so the ctx snapshot and the seed
|
||||
// both start from the assembled set.
|
||||
// the initial local.toml load first so the seed starts from the
|
||||
// assembled set.
|
||||
await this.workspaceDirs.ready;
|
||||
await this.workspaceDirs.mergeAdditionalDirs(opts.workDir, opts.additionalDirs ?? []);
|
||||
const ctx: ISessionContext = {
|
||||
|
|
@ -237,7 +237,6 @@ export class WorkspaceHandlerService extends Disposable implements IWorkspaceHan
|
|||
sessionDir,
|
||||
metaScope,
|
||||
cwd: opts.workDir,
|
||||
additionalDirs: this.workspaceDirs.additionalDirs,
|
||||
scope: (subKey?: string): string =>
|
||||
subKey === undefined || subKey === '' ? sessionScope : `${sessionScope}/${subKey}`,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,58 +0,0 @@
|
|||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import { mergeCallerMcpServers, type SessionMcpConfig } from '#/agent/mcp/session-config';
|
||||
import type { McpServerConfig } from '#/agent/mcp/config-schema';
|
||||
|
||||
const stdio = (command: string): McpServerConfig => ({
|
||||
transport: 'stdio',
|
||||
command,
|
||||
});
|
||||
|
||||
const http = (url: string): McpServerConfig => ({
|
||||
transport: 'http',
|
||||
url,
|
||||
});
|
||||
|
||||
describe('mergeCallerMcpServers', () => {
|
||||
it('returns base unchanged when callerServers is undefined', () => {
|
||||
const base: SessionMcpConfig = { servers: { fs: stdio('fs') } };
|
||||
expect(mergeCallerMcpServers(base, undefined)).toBe(base);
|
||||
});
|
||||
|
||||
it('returns base unchanged when callerServers is empty', () => {
|
||||
const base: SessionMcpConfig = { servers: { fs: stdio('fs') } };
|
||||
expect(mergeCallerMcpServers(base, {})).toBe(base);
|
||||
});
|
||||
|
||||
it('returns undefined when both base and callerServers are absent', () => {
|
||||
expect(mergeCallerMcpServers(undefined, undefined)).toBeUndefined();
|
||||
expect(mergeCallerMcpServers(undefined, {})).toBeUndefined();
|
||||
});
|
||||
|
||||
it('promotes a caller-only payload into a fresh SessionMcpConfig when base is undefined', () => {
|
||||
const callerServers = { docs: http('https://mcp.example.com') };
|
||||
expect(mergeCallerMcpServers(undefined, callerServers)).toEqual({
|
||||
servers: { docs: http('https://mcp.example.com') },
|
||||
});
|
||||
});
|
||||
|
||||
it('layers caller on top of base with caller winning on key collision', () => {
|
||||
const base: SessionMcpConfig = {
|
||||
servers: {
|
||||
shared: stdio('disk-version'),
|
||||
diskOnly: stdio('disk-only'),
|
||||
},
|
||||
};
|
||||
const callerServers = {
|
||||
shared: stdio('caller-version'),
|
||||
callerOnly: http('https://caller.example.com'),
|
||||
};
|
||||
expect(mergeCallerMcpServers(base, callerServers)).toEqual({
|
||||
servers: {
|
||||
shared: stdio('caller-version'),
|
||||
diskOnly: stdio('disk-only'),
|
||||
callerOnly: http('https://caller.example.com'),
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -371,7 +371,7 @@ function makeSession(
|
|||
const emptyHandler: RunHandler = () => ({ stdout: '', exitCode: 0 });
|
||||
|
||||
describe('WorkspaceFsService.gitStatus', () => {
|
||||
it('delegates to IGitService with the session cwd and a confined filter', async () => {
|
||||
it('delegates to IWorkspaceGitService with the handler root and a confined filter', async () => {
|
||||
const calls: Array<{ cwd: string; filter: ReadonlySet<string> | undefined }> = [];
|
||||
const git: IGitService = {
|
||||
_serviceBrand: undefined,
|
||||
|
|
@ -413,7 +413,7 @@ describe('WorkspaceFsService.gitStatus', () => {
|
|||
});
|
||||
|
||||
describe('WorkspaceFsService.diff', () => {
|
||||
it('delegates to IGitService with confined rel and abs paths', async () => {
|
||||
it('delegates to IWorkspaceGitService with confined rel and abs paths', async () => {
|
||||
const calls: Array<{ cwd: string; rel: string; abs: string }> = [];
|
||||
const git: IGitService = {
|
||||
_serviceBrand: undefined,
|
||||
|
|
|
|||
|
|
@ -416,7 +416,9 @@ describe('workspace resource sharing (handler chain)', () => {
|
|||
() => {
|
||||
expect(catalog.catalog.getSkill('watched-skill')?.description).toBe('from watch');
|
||||
},
|
||||
{ timeout: 10000, interval: 100 },
|
||||
// Real FSEvents delivery + the 200 ms source debounce + a real disk
|
||||
// rescan: under high parallel load the 10 s budget flakes, so allow 30 s.
|
||||
{ timeout: 30000, interval: 100 },
|
||||
);
|
||||
}, 20000);
|
||||
}, 60000);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
/**
|
||||
* The `fs:open` / `fs:open_in` / `fs:reveal` request schemas — the only fs
|
||||
* wire shapes the engine does not own (the `sessionFs` domain in agent-core-v2
|
||||
* holds the rest). Also home of `fsOpenInAppIdSchema`, referenced by the
|
||||
* wire shapes the engine does not own (the `workspaceFs` domain in
|
||||
* agent-core-v2 holds the rest). Also home of `fsOpenInAppIdSchema`,
|
||||
* referenced by the
|
||||
* `/v1/meta` capabilities document.
|
||||
*/
|
||||
|
||||
|
|
|
|||
|
|
@ -537,7 +537,7 @@ function sendMappedError(reply: Reply, req: { id: string }, err: unknown): void
|
|||
case ErrorCodes.SESSION_NOT_FOUND:
|
||||
reply.send(errEnvelope(ErrorCode.SESSION_NOT_FOUND, err.message, requestId, err.stack));
|
||||
return;
|
||||
// hostFs errors that escaped the sessionFs layer keep their `os.fs.*`
|
||||
// hostFs errors that escaped the workspaceFs layer keep their `os.fs.*`
|
||||
// code; map them onto the closest v1 wire code (ENOTDIR collapses into
|
||||
// path-not-found, matching `mapFsError`).
|
||||
case ErrorCodes.OS_FS_NOT_FOUND:
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@
|
|||
* All file handling lives here in the transport layer on top of the os
|
||||
* `IHostFileSystem` primitives — the engine deliberately has no "unconfined
|
||||
* read" domain Service. The mime / etag helpers are shared with the engine's
|
||||
* `sessionFs` via `agent-core-v2/_base/utils/fileMeta` so both surfaces label
|
||||
* `workspaceFs` via `agent-core-v2/_base/utils/fileMeta` so both surfaces label
|
||||
* content the same way. `IHostFileSystem` failures arrive as coded `os.fs.*`
|
||||
* errors and are mapped here:
|
||||
*
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
* against a live engine scope and mirrors kap-server's dispatcher semantics
|
||||
* (reflection call, non-function members are property reads, `main` agent
|
||||
* auto-materialized via `ensureMainAgent`). Scope routing walks
|
||||
* `ISessionLifecycleService` / `IAgentLifecycleService` exactly like the
|
||||
* `IWorkspaceLifecycleService` / `IAgentLifecycleService` exactly like the
|
||||
* server's `resolveScope`. Every argument, result, and event payload passes
|
||||
* through `wireClone` (a JSON round-trip), so consumers observe
|
||||
* byte-identical data no matter whether the call crossed a socket or stayed
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue