diff options
| author | Adam Malczewski <[email protected]> | 2026-06-11 21:12:03 +0900 |
|---|---|---|
| committer | Adam Malczewski <[email protected]> | 2026-06-11 21:12:03 +0900 |
| commit | e7eada4802ceebd86c83bcd6e3eca70152e7f331 (patch) | |
| tree | 447095fd60b43980358d1565506f3ae2430e5f29 /packages/transport-http/src/app.ts | |
| parent | 35937cee7f838e414eb8147c67205e01d85a4da0 (diff) | |
| download | dispatch-e7eada4802ceebd86c83bcd6e3eca70152e7f331.tar.gz dispatch-e7eada4802ceebd86c83bcd6e3eca70152e7f331.zip | |
feat(lsp,cwd): LSP integration + per-conversation cwd; fix cache-warming cache bust
LSP + per-conversation CWD feature:
- new bundled `lsp` extension: hand-rolled JSON-RPC codec (framing/rpc), lazy
one-server-per-(serverID,root), per-cwd config resolution, on-demand `lsp` tool
- `conversation-store`: getCwd/setCwd (cwdKey); `session-orchestrator` defaults a
turn's cwd from the store
- `transport-http`: cwd + lsp status endpoints; wire types in transport-contract
- host-bin: register lsp; config wiring
Cache-warming fix (the warm read 0% on the first reheat after a message):
- warm assembled tools under a different cwd than the real turn (a reheat sends no
cwd, and the warm service had no store fallback). The skills filter rewrites the
cwd-sensitive `load_skill` description, so the tools block โ the first bytes of
the prompt-cache prefix โ diverged and the cache missed entirely. Warm now
resolves cwd as opts.cwd ?? conversationStore.getCwd(), mirroring handleMessage.
- capture warm sends as `provider.request` spans flagged `warm:true` (thread a
child logger into providerOpts) so warm vs real bodies are diffable (obs ยง3.1).
- kernel logger: span-close now merges child-bound attrs like span-open, so a
`warm:true` query finds the closed span (with usage/status), not just the open.
Tests: warm forwards a warm-flagged logger; warm falls back to stored cwd; logger
open/close attr consistency. Full suite green (873).
Diffstat (limited to 'packages/transport-http/src/app.ts')
| -rw-r--r-- | packages/transport-http/src/app.ts | 90 |
1 files changed, 89 insertions, 1 deletions
diff --git a/packages/transport-http/src/app.ts b/packages/transport-http/src/app.ts index 84c7d20..7778bad 100644 --- a/packages/transport-http/src/app.ts +++ b/packages/transport-http/src/app.ts @@ -2,6 +2,9 @@ import type { AgentEvent, Logger } from "@dispatch/kernel"; import type { ConversationHistoryResponse, ConversationMetricsResponse, + CwdResponse, + LspServerInfo, + LspStatusResponse, ModelsResponse, ThroughputResponse, WarmResponse, @@ -21,6 +24,8 @@ import { import { type ConversationStore, type CredentialStore, + type LspServerStatus, + type LspService, type SessionOrchestrator, ThroughputQueryError, type ThroughputStore, @@ -32,6 +37,7 @@ export interface CreateServerOptions { readonly orchestrator: SessionOrchestrator; readonly credentialStore: CredentialStore; readonly warmService?: WarmService; + readonly lspService?: LspService; /** Optional โ defaults to a no-op store (recording disabled, empty reports). */ readonly throughputStore?: ThroughputStore; readonly logger?: Logger; @@ -105,7 +111,7 @@ export function createApp(opts: CreateServerOptions): Hono { "*", cors({ origin: "*", - allowMethods: ["GET", "POST", "OPTIONS"], + allowMethods: ["GET", "POST", "PUT", "OPTIONS"], allowHeaders: ["Content-Type"], }), ); @@ -313,5 +319,87 @@ export function createApp(opts: CreateServerOptions): Hono { } }); + app.get("/conversations/:id/cwd", async (c) => { + const conversationId = c.req.param("id"); + try { + const cwd = await opts.conversationStore.getCwd(conversationId); + log.info("conversations: cwd read", { conversationId, hasCwd: cwd !== null }); + const body: CwdResponse = { conversationId, cwd }; + return c.json(body, 200); + } catch (err) { + log.error("conversations: cwd read failure", { err }); + return c.json({ error: "Failed to read conversation cwd" }, 500); + } + }); + + app.put("/conversations/:id/cwd", async (c) => { + const conversationId = c.req.param("id"); + let body: unknown; + try { + body = await c.req.json(); + } catch { + log.warn("conversations/cwd: invalid JSON body"); + return c.json({ error: "Invalid JSON body" }, 400); + } + + if (body === null || typeof body !== "object") { + return c.json({ error: "Request body must be a JSON object" }, 400); + } + const obj = body as Record<string, unknown>; + if (typeof obj.cwd !== "string" || obj.cwd.length === 0) { + return c.json({ error: "Field 'cwd' is required and must be a non-empty string" }, 400); + } + + try { + await opts.conversationStore.setCwd(conversationId, obj.cwd); + log.info("conversations: cwd set", { conversationId }); + const response: CwdResponse = { conversationId, cwd: obj.cwd }; + return c.json(response, 200); + } catch (err) { + log.error("conversations: cwd set failure", { err }); + return c.json({ error: "Failed to set conversation cwd" }, 500); + } + }); + + app.get("/conversations/:id/lsp", async (c) => { + const conversationId = c.req.param("id"); + try { + const cwd = await opts.conversationStore.getCwd(conversationId); + if (cwd === null) { + log.info("conversations: lsp status read (no cwd)", { conversationId }); + const body: LspStatusResponse = { conversationId, cwd: null, servers: [] }; + return c.json(body, 200); + } + + if (opts.lspService === undefined) { + log.warn("conversations: lsp service not available", { conversationId }); + return c.json({ error: "LSP service not available" }, 503); + } + + const statuses = await opts.lspService.status(cwd); + const servers: LspServerInfo[] = statuses.map((s: LspServerStatus) => { + const info: LspServerInfo = { + id: s.id, + name: s.name, + root: s.root, + extensions: s.extensions, + state: s.state, + ...(s.error !== undefined ? { error: s.error } : {}), + }; + return info; + }); + log.info("conversations: lsp status read", { + conversationId, + cwd, + serverCount: servers.length, + }); + const body: LspStatusResponse = { conversationId, cwd, servers }; + return c.json(body, 200); + } catch (err) { + log.error("conversations: lsp status failure", { err }); + return c.json({ error: "Failed to read LSP status" }, 500); + } + }); + return app; } |
