From 83a2cf4a4343f9a0722ae2612340d878bba98074 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Burak=20Emre=20Kabakc=C4=B1?= Date: Tue, 19 May 2026 17:46:00 +0100 Subject: [PATCH 1/9] wip(spike): delete dead worker module lifecycle wiring MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The worker's session loop invokes `initModuleWorkspace`, `onSessionStart`, and `collectModuleData` from `agent-worker/src/modules/lifecycle.ts`. Each call resolves `moduleRegistry.getWorkerModules()`, which by registration intent surfaces every module that has `onBeforeResponse` in its prototype. In practice, the only modules ever registered are `ModelProviderModule`s (Claude OAuth, ChatGPT OAuth, Bedrock, Gemini CLI, the API-key catalog module) — all inheriting from `BaseModule`, whose lifecycle hooks are no-ops. Three `for` loops over no-op methods run on every session start, plus three `await import("../modules/lifecycle")` dynamic imports that violate the static-import rule documented in CLAUDE.md. Delete the call sites in `openclaw/worker.ts` and the whole `agent-worker/src/modules/` directory. The `WorkerModule` / `BaseModule` interface surface in `@lobu/core` and `gateway/modules/module-system.ts` stays — it's public-API, and removing it is a separate, larger decision (see REPORT.md "Do not do"). Re-adding the call sites later, when an actual module needs them, is ~20 LOC. Validation: `make typecheck` clean from worktree root (server + owletto). `bunx tsc --noEmit` clean inside `packages/agent-worker`. --- .../agent-worker/src/modules/lifecycle.ts | 92 ------------------- packages/agent-worker/src/openclaw/worker.ts | 35 ------- 2 files changed, 127 deletions(-) delete mode 100644 packages/agent-worker/src/modules/lifecycle.ts diff --git a/packages/agent-worker/src/modules/lifecycle.ts b/packages/agent-worker/src/modules/lifecycle.ts deleted file mode 100644 index 24a5e29df..000000000 --- a/packages/agent-worker/src/modules/lifecycle.ts +++ /dev/null @@ -1,92 +0,0 @@ -import { - createLogger, - type ModuleSessionContext, - moduleRegistry, - type SessionContext, - type WorkerModule, -} from "@lobu/core"; - -const logger = createLogger("worker"); - -/** - * Execute an operation on all worker modules with consistent error handling. - * Errors in individual modules are logged but do not halt iteration. - */ -async function executeForAllModules( - operation: (module: WorkerModule) => Promise, - operationName: string -): Promise { - const workerModules = moduleRegistry.getWorkerModules(); - const results: T[] = []; - for (const module of workerModules) { - try { - results.push(await operation(module)); - } catch (error) { - logger.error( - `Failed to execute ${operationName} for module ${module.name}:`, - error - ); - } - } - return results; -} - -export async function onSessionStart( - context: SessionContext -): Promise { - // Convert to module session context - const moduleContext: ModuleSessionContext = { - userId: context.userId, - conversationId: context.conversationId || "", - systemPrompt: context.customInstructions || "", - workspace: undefined, - }; - - let updatedContext = moduleContext; - - await executeForAllModules(async (module) => { - updatedContext = await module.onSessionStart(updatedContext); - }, "onSessionStart"); - - // Merge back into original context, mapping systemPrompt back to customInstructions - return { - ...context, - customInstructions: - updatedContext.systemPrompt || context.customInstructions, - }; -} - -/** - * Configuration for module workspace initialization - */ -interface ModuleWorkspaceConfig { - workspaceDir: string; - username: string; - sessionKey: string; -} - -export async function initModuleWorkspace( - config: ModuleWorkspaceConfig -): Promise { - await executeForAllModules( - (module) => module.initWorkspace(config), - "initWorkspace" - ); -} - -export async function collectModuleData(context: { - workspaceDir: string; - userId: string; - conversationId: string; -}): Promise> { - const moduleData: Record = {}; - - await executeForAllModules(async (module) => { - const data = await module.onBeforeResponse(context); - if (data !== null) { - moduleData[module.name] = data; - } - }, "onBeforeResponse"); - - return moduleData; -} diff --git a/packages/agent-worker/src/openclaw/worker.ts b/packages/agent-worker/src/openclaw/worker.ts index a728c0ed9..1dd5a68a6 100644 --- a/packages/agent-worker/src/openclaw/worker.ts +++ b/packages/agent-worker/src/openclaw/worker.ts @@ -394,13 +394,6 @@ export class OpenClawWorker implements WorkerExecutor { this.config.userId, this.config.sessionKey ); - - const { initModuleWorkspace } = await import("../modules/lifecycle"); - await initModuleWorkspace({ - workspaceDir: this.workspaceManager.getCurrentWorkingDirectory(), - username: this.config.userId, - sessionKey: this.config.sessionKey, - }); } ); @@ -425,26 +418,6 @@ export class OpenClawWorker implements WorkerExecutor { } ); - // Module hooks may modify the system prompt before agent execution. - try { - const { onSessionStart } = await import("../modules/lifecycle"); - const moduleContext = await onSessionStart({ - platform: this.config.platform, - channelId: this.config.channelId, - userId: this.config.userId, - conversationId: this.config.conversationId, - messageId: this.config.responseId, - workingDirectory: this.workspaceManager.getCurrentWorkingDirectory(), - customInstructions, - }); - if (moduleContext.customInstructions) { - customInstructions = moduleContext.customInstructions; - } - } catch (error) { - logger.error("Failed to call onSessionStart hooks:", error); - } - - // Add file I/O instructions AFTER module hooks so they aren't overwritten customInstructions += this.getFileIOInstructions(); logger.info( @@ -507,14 +480,6 @@ export class OpenClawWorker implements WorkerExecutor { } ); - const { collectModuleData } = await import("../modules/lifecycle"); - const moduleData = await collectModuleData({ - workspaceDir: this.workspaceManager.getCurrentWorkingDirectory(), - userId: this.config.userId, - conversationId: this.config.conversationId, - }); - this.workerTransport.setModuleData(moduleData); - if (result.success) { // Snapshot writer in cleanup() reads this to discriminate the row. // Hydrate skips non-completed snapshots, so getting this right is From 8a5f6868fe28346e9cde1c2725d9c76d67fd44d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Burak=20Emre=20Kabakc=C4=B1?= Date: Tue, 19 May 2026 18:03:20 +0100 Subject: [PATCH 2/9] wip(spike): hoist MessagePayload + JobType into @lobu/core MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `MessagePayload` is the gateway↔worker wire contract: produced by `MessageConsumer` and `EmbeddedDeploymentManager.dispatch*`, consumed by `GatewayClient.handleThreadMessage` / `handleExecJob` over SSE. Both sides need to agree on the shape, but the type was declared twice and had already drifted: the worker's copy in `agent-worker/src/gateway/ types.ts` was missing `organizationId`, `networkConfig`, `egressConfig`, `mcpConfig`, `nixConfig`, and `preApprovedTools` (all present on the gateway side). The worker's zod schema was patched with `.passthrough()` to keep the extra fields from being stripped at parse time (PR #871 regression), but the static type silently lied — TS would have happily accepted reads of e.g. `payload.preApprovedTools` as `undefined` even when the gateway always populates it. Move both `MessagePayload` and `JobType` (plus the worker-side `QueuedMessage` envelope) into `packages/core/src/worker/wire.ts` and re-export them from `@lobu/core`. The worker and gateway both import from there; `queue-producer.ts` and `gateway/types.ts` keep a re-export so the existing `from "./types"` / `from "../infrastructure/queue/ queue-producer"` paths continue to resolve without churn. Type unification: - `agentOptions: AgentOptions` (was `Record` on the gateway, `AgentOptions` on the worker). The worker reads `.model`, `.allowedTools`, `.disallowedTools`, `.timeoutMinutes` directly, so `AgentOptions` is the honest shape. `Record` is assignable into it via the `[key: string]: unknown` index signature. - `platformMetadata: Record` (was `Record` on the gateway, a richer named interface on the worker). The unknown flavour forces three new `typeof === "string"` guards inside `base-deployment-manager.ts` where the env-var builder reads `originalMessageTs`, `botResponseTs`, and `teamId` off the bag. - `teamId?: string` (was `string` on the gateway, `string | undefined` on the worker). The worker SSE zod schema marks it `.optional()`, and the worker has explicit fallback logic (`payload.teamId ?? platformMetadata.teamId`). The gateway-side `buildMessagePayload` always sets a non-empty string, so concrete callers are unaffected. Validation: `make build-packages` clean. `make typecheck` clean (server + owletto). `bunx tsc --noEmit` clean inside `packages/agent-worker`. --- packages/agent-worker/src/gateway/types.ts | 83 ++--------- packages/core/src/index.ts | 2 + packages/core/src/worker/wire.ts | 135 ++++++++++++++++++ .../infrastructure/queue/queue-producer.ts | 96 +------------ .../orchestration/base-deployment-manager.ts | 14 +- 5 files changed, 165 insertions(+), 165 deletions(-) create mode 100644 packages/core/src/worker/wire.ts diff --git a/packages/agent-worker/src/gateway/types.ts b/packages/agent-worker/src/gateway/types.ts index 97f170bab..001404290 100644 --- a/packages/agent-worker/src/gateway/types.ts +++ b/packages/agent-worker/src/gateway/types.ts @@ -1,79 +1,18 @@ /** - * Shared types for gateway communication + * Worker-side gateway-communication types. + * + * `MessagePayload`, `JobType`, and `QueuedMessage` live in `@lobu/core` — + * see `packages/core/src/worker/wire.ts` — and are re-exported here so the + * existing `from "./types"` imports inside the worker keep resolving. */ -import type { AgentOptions, ThreadResponsePayload } from "@lobu/core"; +import type { ThreadResponsePayload } from "@lobu/core"; -/** - * Platform-specific metadata (e.g., Slack team_id, channel, thread_ts) - */ -interface PlatformMetadata { - team_id?: string; - channel?: string; - ts?: string; - thread_ts?: string; - files?: unknown[]; - traceId?: string; // Trace ID for end-to-end observability - [key: string]: unknown; -} - -/** - * Job type for queue messages - * - message: Standard agent message execution - * - exec: Direct command execution in sandbox - */ -export type JobType = "message" | "exec"; - -/** - * Message payload for agent execution - */ -export interface MessagePayload { - botId: string; - userId: string; - agentId: string; - conversationId: string; - platform: string; - channelId: string; - messageId: string; - messageText: string; - platformMetadata: PlatformMetadata; - agentOptions: AgentOptions; - jobId?: string; // Optional job ID from gateway - teamId?: string; // Optional team ID (WhatsApp uses top-level, Slack uses platformMetadata) - - // The runs.id of the row that dispatched this job. Set by the gateway - // MessageConsumer (stamped from the runs-queue claim's job.id) and - // threaded into WorkerConfig.runId. The worker's cleanup() uses it to - // attribute the agent_transcript_snapshot row to the correct run — - // see codex P1#1 on PR #865. - runId?: number; - - // Per-run worker JWT bound to `runId` above. Minted by MessageConsumer - // and threaded into WorkerConfig.runJobToken. The worker uses THIS - // token (not the deployment-lifetime WORKER_TOKEN) when calling the - // snapshot endpoint, so the route's `tokenData.runId === body.runId` - // equality check can reject any cross-run impersonation — codex round - // 2 finding A on PR #865. - runJobToken?: string; - - // Job type (default: "message") - jobType?: JobType; - - // Exec-specific fields (only used when jobType === "exec") - execId?: string; // Unique ID for exec job (for response routing) - execCommand?: string; // Command to execute - execCwd?: string; // Working directory for command - execEnv?: Record; // Additional environment variables - execTimeout?: number; // Timeout in milliseconds -} - -/** - * Queued message with timestamp - */ -export interface QueuedMessage { - payload: MessagePayload; - timestamp: number; -} +export type { + JobType, + MessagePayload, + QueuedMessage, +} from "@lobu/core"; /** * Response data sent back to gateway diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 91c06780f..82c69b611 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -160,3 +160,5 @@ export type { WorkerTransport, WorkerTransportConfig, } from "./worker/transport"; +// Gateway ↔ worker wire contract (MessagePayload, JobType, QueuedMessage). +export type { JobType, MessagePayload, QueuedMessage } from "./worker/wire"; diff --git a/packages/core/src/worker/wire.ts b/packages/core/src/worker/wire.ts new file mode 100644 index 000000000..e36975a79 --- /dev/null +++ b/packages/core/src/worker/wire.ts @@ -0,0 +1,135 @@ +/** + * Gateway ↔ worker wire contract. + * + * `MessagePayload` is what `MessageConsumer` (gateway) enqueues on the runs + * queue, what `EmbeddedDeploymentManager.dispatch*` writes to the worker SSE + * stream, and what the worker's `GatewayClient.handleThreadMessage` / + * `handleExecJob` consumes. Same shape on both sides — keep it here. + * + * Before this lived in core, the worker had its own `MessagePayload` + * declaration that was a structural subset of the gateway's (missing + * `organizationId`, `networkConfig`, `egressConfig`, `mcpConfig`, `nixConfig`, + * `preApprovedTools`). At runtime the worker's zod schema was patched with + * `.passthrough()` so the extra fields survived parsing, but the static type + * silently lied. Hoisting closes the gap. + */ + +import type { + AgentEgressConfig, + AgentMcpConfig, + AgentOptions, + NetworkConfig, + NixConfig, +} from "../types"; + +/** + * Job type for queue messages. + * - `message`: standard agent message execution. + * - `exec`: direct command execution in the sandbox. + */ +export type JobType = "message" | "exec"; + +/** + * Universal message payload for every gateway → worker hop. + * Used by: platform inbound → runs queue → MessageConsumer → worker. + */ +export interface MessagePayload { + // ── Core identifiers (used by gateway for routing) ────────────────── + userId: string; + conversationId: string; + messageId: string; + channelId: string; + /** + * Team/workspace ID. Required in the gateway-produced payload (always + * stamped by `buildMessagePayload`), but optional in the wire type + * because Slack carries the workspace ID in `platformMetadata` and the + * worker reads it defensively (`payload.teamId ?? platformMetadata.teamId`). + * The worker SSE schema parses it with `z.string().optional()`. + */ + teamId?: string; + /** Agent / session ID for tenant isolation. */ + agentId: string; + /** + * Owning organization of the agent. Plumbed through so child queries + * (grants, user-agents, channel-bindings, secrets) can scope by org — + * agent IDs are per-org-unique, so `agent_id = ?` alone is ambiguous. + */ + organizationId?: string; + + // ── Bot & platform info (passed through to worker) ───────────────── + /** Bot identifier. */ + botId: string; + /** Platform name (`slack`, `telegram`, ...). */ + platform: string; + + // ── Message content (used by worker) ─────────────────────────────── + messageText: string; + + // ── Platform-specific data (used by worker for context) ──────────── + platformMetadata: Record; + + // ── Agent configuration (used by worker) ─────────────────────────── + agentOptions: AgentOptions; + + // ── Per-agent network configuration for sandbox isolation ────────── + networkConfig?: NetworkConfig; + + /** + * The runs.id of the row the runs-queue claimed when this message was + * dispatched. Threaded all the way to the worker so the per-run + * agent_transcript_snapshot POST can attribute the snapshot to the + * correct run unambiguously — codex P1#1 on PR #865. + */ + runId?: number; + + /** + * Per-run worker JWT bound to `runId` above. Minted by the runs-queue + * dispatcher (`MessageConsumer.handleMessage`) so the snapshot route can + * require `tokenData.runId === body.runId` and reject any attempt by a + * same-(org, agent, conv) deployment-lifetime token to write under a + * different run's slot — codex round 2 finding A on PR #865. + */ + runJobToken?: string; + + /** Per-agent egress judge configuration. */ + egressConfig?: AgentEgressConfig; + + /** Per-agent MCP configuration (additive to global MCPs). */ + mcpConfig?: AgentMcpConfig; + + /** Nix environment configuration for the agent workspace. */ + nixConfig?: NixConfig; + + /** + * MCP tool grant patterns the operator has pre-approved. + * Synced to the grant store at deployment time to bypass the approval card. + */ + preApprovedTools?: string[]; + + /** + * Job ID from the gateway (set when the payload rode through the worker + * SSE stream). Optional — direct-enqueue paths leave it unset. + */ + jobId?: string; + + /** Job type (default: `message`). */ + jobType?: JobType; + + // ── Exec-specific fields (only used when jobType === "exec") ─────── + /** Unique ID for the exec job (for response routing). */ + execId?: string; + /** Command to execute. */ + execCommand?: string; + /** Working directory for the command. */ + execCwd?: string; + /** Additional environment variables. */ + execEnv?: Record; + /** Timeout in milliseconds. */ + execTimeout?: number; +} + +/** Queued message envelope used by the worker's in-process batcher. */ +export interface QueuedMessage { + payload: MessagePayload; + timestamp: number; +} diff --git a/packages/server/src/gateway/infrastructure/queue/queue-producer.ts b/packages/server/src/gateway/infrastructure/queue/queue-producer.ts index 20854b724..dcff8ca21 100644 --- a/packages/server/src/gateway/infrastructure/queue/queue-producer.ts +++ b/packages/server/src/gateway/infrastructure/queue/queue-producer.ts @@ -1,99 +1,15 @@ #!/usr/bin/env bun -import { - type AgentEgressConfig, - type AgentMcpConfig, - createLogger, - type NetworkConfig, - type NixConfig, -} from "@lobu/core"; +import { createLogger, type MessagePayload } from "@lobu/core"; import type { IMessageQueue } from "./types.js"; const logger = createLogger("queue-producer"); -/** - * Job type for queue messages - * - message: Standard agent message execution - * - exec: Direct command execution in sandbox - */ -export type JobType = "message" | "exec"; - -/** - * Universal message payload for all queue stages - * Used by: Slack events → Queue → Message Consumer → Job Router → Worker - */ -export interface MessagePayload { - // Core identifiers (used by gateway for routing) - userId: string; // Platform user ID - conversationId: string; // Conversation ID (must be root conversation ID) - messageId: string; // Individual message ID - channelId: string; // Platform channel ID - teamId: string; // Team/workspace ID (required for all platforms) - agentId: string; // Agent/session ID for isolation (universal identifier) - // Organization id of the agent. Plumbed through so child queries (grants, - // user-agents, channel-bindings, secrets) can scope by org — agent ids - // are per-org-unique, so `agent_id = ?` alone is ambiguous. - organizationId?: string; - - // Bot & platform info (passed through to worker) - botId: string; // Bot identifier - platform: string; // Platform name - - // Message content (used by worker) - messageText: string; // The actual message text - - // Platform-specific data (used by worker for context) - platformMetadata: Record; - - // Agent configuration (used by worker) - agentOptions: Record; - - // Per-agent network configuration for sandbox isolation - networkConfig?: NetworkConfig; - - // The runs.id of the row the runs-queue claimed when this message was - // dispatched. Threaded all the way to the worker so the per-run - // agent_transcript_snapshot POST can attribute the snapshot to the - // correct run unambiguously. Without this, the snapshot route would - // have to guess via "latest run for (org, agent, conv)", which races - // with the next user message enqueuing a fresh run while the previous - // worker is still in cleanup() — see codex P1#1 on PR #865. - runId?: number; - - // Per-run worker JWT bound to `runId` above. Minted by the runs-queue - // dispatcher (`MessageConsumer.handleMessage`) so the snapshot route - // can require `tokenData.runId === body.runId` and reject any attempt - // by a same-(org, agent, conv) deployment-lifetime token to write - // under a different run's slot — codex round 2 finding A on PR #865. - // Optional only because non-runs-queue paths (e.g. direct enqueue from - // legacy code) still go through MessagePayload; in those cases the - // worker falls back to the deployment-lifetime WORKER_TOKEN and the - // snapshot path is skipped if the env var requires it. - runJobToken?: string; - - // Per-agent egress judge configuration (operator-level overrides for the LLM egress judge). - egressConfig?: AgentEgressConfig; - - // Per-agent MCP configuration (additive to global MCPs) - mcpConfig?: AgentMcpConfig; - - // Nix environment configuration for agent workspace - nixConfig?: NixConfig; - - // MCP tool grant patterns the operator has pre-approved. - // Synced to the grant store at deployment time to bypass the approval card. - preApprovedTools?: string[]; - - // Job type (default: "message") - jobType?: JobType; - - // Exec-specific fields (only used when jobType === "exec") - execId?: string; // Unique ID for exec job (for response routing) - execCommand?: string; // Command to execute - execCwd?: string; // Working directory for command - execEnv?: Record; // Additional environment variables - execTimeout?: number; // Timeout in milliseconds -} +// `MessagePayload` and `JobType` are the gateway↔worker wire contract — both +// sides need to agree, so they live in `@lobu/core` (see +// `packages/core/src/worker/wire.ts`). Re-exported here so existing +// `from "../infrastructure/queue/queue-producer"` callers don't change. +export type { JobType, MessagePayload } from "@lobu/core"; /** * Queue producer for dispatching messages to the runs queue. diff --git a/packages/server/src/gateway/orchestration/base-deployment-manager.ts b/packages/server/src/gateway/orchestration/base-deployment-manager.ts index 03249de33..06107b5d0 100644 --- a/packages/server/src/gateway/orchestration/base-deployment-manager.ts +++ b/packages/server/src/gateway/orchestration/base-deployment-manager.ts @@ -637,7 +637,11 @@ export abstract class BaseDeploymentManager { DEPLOYMENT_NAME: deploymentName, CHANNEL_ID: channelId, ORIGINAL_MESSAGE_TS: - platformMetadata?.originalMessageTs || messageData.messageId || "", + (typeof platformMetadata?.originalMessageTs === "string" + ? platformMetadata.originalMessageTs + : "") || + messageData.messageId || + "", LOG_LEVEL: "info", WORKSPACE_DIR: "/workspace", CONVERSATION_ID: conversationId, @@ -659,7 +663,7 @@ export abstract class BaseDeploymentManager { XDG_CACHE_HOME: "/workspace/.cache", }; - if (platformMetadata?.botResponseTs) { + if (typeof platformMetadata?.botResponseTs === "string") { envVars.BOT_RESPONSE_TS = platformMetadata.botResponseTs; } @@ -811,7 +815,11 @@ export abstract class BaseDeploymentManager { const validated = this.validateMessageData(deploymentName, messageData); const { conversationId, channelId, platformMetadata, agentId, platform } = validated; - const teamId = validated.teamId || platformMetadata?.teamId; + const teamId = + validated.teamId || + (typeof platformMetadata?.teamId === "string" + ? platformMetadata.teamId + : undefined); const traceId = extractTraceId(validated); const providerContext: ProviderCredentialContext = { userId, From 9a401f5444134f5fdcb83bfea965b500f5e97504 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Burak=20Emre=20Kabakc=C4=B1?= Date: Tue, 19 May 2026 18:06:42 +0100 Subject: [PATCH 3/9] wip(spike): hoist session.jsonl parser to @lobu/core MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two HTTP surfaces parse the worker's `.openclaw/session.jsonl`: - the worker's own `/session/messages` and `/session/stats` (rooted at `WORKSPACE_DIR`), and - the gateway's `/session/messages` and `/session/stats` proxy fallback (rooted at `workspaces/`, used when the worker is offline). The gateway proxies to the worker when the worker is online and falls back to its own copy otherwise — so the two parsers MUST agree on the JSONL line shape. They had drifted. The worker used `safeJsonParse` (a core util that debug-logs malformed lines); the gateway inlined a bare `try { JSON.parse } catch {}`. The worker carried two extra `SessionEntry` fields (`tokensBefore`, `firstKeptEntryId`) that no parser actually read. The display-projection logic in `entryToMessage` was duplicated character-for-character. Move the shared parts (`SessionEntry`, `ParsedMessage`, `parseSessionEntries`, `entryToMessage`) into `packages/core/src/utils/session-file.ts` and have both call sites import from `@lobu/core`. `safeJsonParse` wins as the canonical line parser — it's already in core, and the debug-log on malformed lines is strictly more useful than silently swallowing them. What does NOT move: `findSessionFile`. The two call sites have intentionally different path policies (worker scans its own `WORKSPACE_DIR` one level deep; gateway scans `workspaces/` three levels deep with a `SAFE_AGENT_ID` regex guard against path traversal). Collapsing them would force one side to inherit the other's policy — not a behaviour change to make silently. Dropped from the canonical `SessionEntry`: `tokensBefore` and `firstKeptEntryId`. They were declared on the worker's local interface but nothing reads them — only `agent-worker/src/__tests__/ memory-flush-runtime.test.ts` mentions them in a fixture. Reintroduce when an actual consumer needs them. Validation: `make build-packages` clean. `make typecheck` clean (server + owletto). `bunx tsc --noEmit` clean inside `packages/agent-worker`. --- packages/agent-worker/src/server.ts | 123 ++------------- packages/core/src/index.ts | 7 + packages/core/src/utils/session-file.ts | 141 ++++++++++++++++++ .../gateway/routes/public/agent-history.ts | 111 ++------------ 4 files changed, 177 insertions(+), 205 deletions(-) create mode 100644 packages/core/src/utils/session-file.ts diff --git a/packages/agent-worker/src/server.ts b/packages/agent-worker/src/server.ts index d02028668..16f6ae16a 100644 --- a/packages/agent-worker/src/server.ts +++ b/packages/agent-worker/src/server.ts @@ -9,9 +9,11 @@ import { join } from "node:path"; import { getRequestListener } from "@hono/node-server"; import { createLogger, + entryToMessage, getOptionalEnv, getOptionalNumber, - safeJsonParse, + type ParsedMessage, + parseSessionEntries, } from "@lobu/core"; import { Hono } from "hono"; @@ -19,6 +21,16 @@ const logger = createLogger("worker-http"); const app = new Hono(); +/** + * Locate a `.openclaw/session.jsonl` under the worker's own `WORKSPACE_DIR`. + * + * Different from the gateway-side `findSessionFile` (in + * `packages/server/src/gateway/routes/public/agent-history.ts`) on purpose + * — the worker's tree is single-agent, anchored at `WORKSPACE_DIR`, and + * only one level deep; the gateway scans up to three levels under + * `workspaces/` with a `SAFE_AGENT_ID` regex guard. Path-policy + * stays per-caller. + */ async function findSessionFile(): Promise { const workspaceDir = getOptionalEnv("WORKSPACE_DIR", "/workspace"); @@ -57,111 +69,6 @@ async function findSessionFile(): Promise { return null; } -interface SessionEntry { - type: string; - id: string; - parentId: string | null; - timestamp: string; - message?: { - role: string; - content: unknown; - usage?: { inputTokens?: number; outputTokens?: number }; - }; - summary?: string; - provider?: string; - modelId?: string; - customType?: string; - content?: unknown; - display?: boolean; - tokensBefore?: number; - firstKeptEntryId?: string; -} - -interface ParsedMessage { - id: string; - type: string; - role?: string; - content: unknown; - model?: string; - timestamp: string; - isVerbose?: boolean; - usage?: { inputTokens?: number; outputTokens?: number }; -} - -function parseSessionFile(content: string): { - entries: SessionEntry[]; - sessionId?: string; -} { - const lines = content.split("\n").filter((l) => l.trim()); - const entries: SessionEntry[] = []; - let sessionId: string | undefined; - - for (const line of lines) { - const parsed = safeJsonParse(line); - if (!parsed) { - // Skip malformed lines - continue; - } - if (parsed.type === "session") { - sessionId = parsed.id; - continue; - } - entries.push(parsed); - } - - return { entries, sessionId }; -} - -function entryToMessage(entry: SessionEntry): ParsedMessage | null { - if (entry.type === "message" && entry.message) { - const role = entry.message.role; - const isVerbose = role === "toolResult"; - return { - id: entry.id, - type: "message", - role, - content: entry.message.content, - timestamp: entry.timestamp, - isVerbose, - usage: entry.message.usage, - }; - } - - if (entry.type === "compaction") { - return { - id: entry.id, - type: "compaction", - content: entry.summary || "", - timestamp: entry.timestamp, - isVerbose: true, - }; - } - - if (entry.type === "model_change") { - return { - id: entry.id, - type: "model_change", - content: `${entry.provider}/${entry.modelId}`, - model: `${entry.provider}/${entry.modelId}`, - timestamp: entry.timestamp, - isVerbose: true, - }; - } - - if (entry.type === "custom_message") { - return { - id: entry.id, - type: "custom_message", - role: "user", - content: entry.content, - timestamp: entry.timestamp, - isVerbose: !entry.display, - }; - } - - return null; -} - app.get("/health", (c) => c.json({ status: "ok" })); app.get("/session/messages", async (c) => { @@ -179,7 +86,7 @@ app.get("/session/messages", async (c) => { }); } const content = await readFile(sessionPath, "utf-8"); - const { entries, sessionId } = parseSessionFile(content); + const { entries, sessionId } = parseSessionEntries(content); // Convert all entries to messages, filtering nulls const allMessages: ParsedMessage[] = []; @@ -241,7 +148,7 @@ app.get("/session/stats", async (c) => { }); } const content = await readFile(sessionPath, "utf-8"); - const { entries, sessionId } = parseSessionFile(content); + const { entries, sessionId } = parseSessionEntries(content); let messageCount = 0; let userMessages = 0; diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 82c69b611..b5685060e 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -154,6 +154,13 @@ export type { McpStatus, McpToolDef } from "./utils/mcp-tool-instructions"; export * from "./utils/network-domains"; export * from "./utils/retry"; export * from "./utils/sanitize"; +// Shared OpenClaw session.jsonl parser (gateway + worker). +export { + entryToMessage, + parseSessionEntries, + type ParsedMessage, + type SessionEntry, +} from "./utils/session-file"; export * from "./utils/urls"; export * from "./worker/auth"; export type { diff --git a/packages/core/src/utils/session-file.ts b/packages/core/src/utils/session-file.ts new file mode 100644 index 000000000..1a89174f7 --- /dev/null +++ b/packages/core/src/utils/session-file.ts @@ -0,0 +1,141 @@ +/** + * Shared parser for OpenClaw `session.jsonl` files. + * + * Two HTTP surfaces read these files: the worker's `/session/messages` / + * `/session/stats` endpoints (rooted at the worker's own `WORKSPACE_DIR`) + * and the gateway's `/session/messages` / `/session/stats` REST endpoints + * (rooted at the gateway's `workspaces/` tree, queried when the + * worker is offline). The gateway proxies to the worker when it's online + * and falls back to its own copy otherwise — so the two parsers must + * agree, and historically they had drifted (different fields kept on + * `SessionEntry`, different `JSON.parse` error handling, the same logic + * copy-pasted twice). + * + * Anything path-policy related (where to *look* for the file) stays at + * the call site — the worker scans one level under `WORKSPACE_DIR`; the + * gateway scans up to three levels under the per-agent workspace dir + * with a `SAFE_AGENT_ID` regex guarding the join. Those are intentionally + * different and must not be collapsed without an operator decision. + */ + +import { safeJsonParse } from "./json"; + +/** + * Raw entry shape as written to `session.jsonl` by the worker. + * + * `tokensBefore` / `firstKeptEntryId` (worker memory-flush bookkeeping) + * are not read by either parser today — left off this canonical shape on + * purpose; reintroduce when a consumer actually needs them. + */ +export interface SessionEntry { + type: string; + id: string; + parentId: string | null; + timestamp: string; + message?: { + role: string; + content: unknown; + usage?: { inputTokens?: number; outputTokens?: number }; + }; + summary?: string; + provider?: string; + modelId?: string; + customType?: string; + content?: unknown; + display?: boolean; +} + +/** Display-friendly projection emitted to API consumers (`/session/messages`). */ +export interface ParsedMessage { + id: string; + type: string; + role?: string; + content: unknown; + model?: string; + timestamp: string; + isVerbose?: boolean; + usage?: { inputTokens?: number; outputTokens?: number }; +} + +/** + * Parse a session.jsonl blob into entries + the synthetic session id + * found on the leading `{type: "session", id}` line. + * + * - Splits on `\n` and skips blank lines (same as both pre-existing copies). + * - Uses {@link safeJsonParse} so malformed lines are skipped quietly with + * a debug log (debug-only because production sessions occasionally + * contain partial writes after crash/kill). + * - The leading `session` entry is extracted, not pushed into `entries`. + */ +export function parseSessionEntries(content: string): { + entries: SessionEntry[]; + sessionId?: string; +} { + const lines = content.split("\n").filter((l) => l.trim()); + const entries: SessionEntry[] = []; + let sessionId: string | undefined; + for (const line of lines) { + const parsed = safeJsonParse(line); + if (!parsed) continue; + if (parsed.type === "session") { + sessionId = parsed.id; + continue; + } + entries.push(parsed); + } + return { entries, sessionId }; +} + +/** + * Project a single {@link SessionEntry} into the {@link ParsedMessage} + * display shape, or `null` for entry kinds that don't surface as + * user-visible messages (everything other than `message`, `compaction`, + * `model_change`, `custom_message`). + * + * `isVerbose` marks entries the UI hides behind a "verbose" toggle — + * tool results, compaction/model-change markers, custom system events + * that aren't explicitly displayed. + */ +export function entryToMessage(entry: SessionEntry): ParsedMessage | null { + if (entry.type === "message" && entry.message) { + return { + id: entry.id, + type: "message", + role: entry.message.role, + content: entry.message.content, + timestamp: entry.timestamp, + isVerbose: entry.message.role === "toolResult", + usage: entry.message.usage, + }; + } + if (entry.type === "compaction") { + return { + id: entry.id, + type: "compaction", + content: entry.summary || "", + timestamp: entry.timestamp, + isVerbose: true, + }; + } + if (entry.type === "model_change") { + return { + id: entry.id, + type: "model_change", + content: `${entry.provider}/${entry.modelId}`, + model: `${entry.provider}/${entry.modelId}`, + timestamp: entry.timestamp, + isVerbose: true, + }; + } + if (entry.type === "custom_message") { + return { + id: entry.id, + type: "custom_message", + role: "user", + content: entry.content, + timestamp: entry.timestamp, + isVerbose: !entry.display, + }; + } + return null; +} diff --git a/packages/server/src/gateway/routes/public/agent-history.ts b/packages/server/src/gateway/routes/public/agent-history.ts index bdf2019fe..29f1e3d12 100644 --- a/packages/server/src/gateway/routes/public/agent-history.ts +++ b/packages/server/src/gateway/routes/public/agent-history.ts @@ -6,8 +6,12 @@ import { readdir, readFile, stat } from "node:fs/promises"; import { join, resolve } from "node:path"; -import type { AgentConfigStore } from "@lobu/core"; -import { createLogger } from "@lobu/core"; +import type { AgentConfigStore, ParsedMessage } from "@lobu/core"; +import { + createLogger, + entryToMessage, + parseSessionEntries, +} from "@lobu/core"; import type { Context } from "hono"; import { Hono } from "hono"; import type { UserAgentsStore } from "../../auth/user-agents-store.js"; @@ -66,35 +70,14 @@ function isSafeAgentId(id: string): boolean { } // ─── Direct session file reader (fallback) ───────────────────────────────── - -interface SessionEntry { - type: string; - id: string; - parentId: string | null; - timestamp: string; - message?: { - role: string; - content: unknown; - usage?: { inputTokens?: number; outputTokens?: number }; - }; - summary?: string; - provider?: string; - modelId?: string; - customType?: string; - content?: unknown; - display?: boolean; -} - -interface ParsedMessage { - id: string; - type: string; - role?: string; - content: unknown; - model?: string; - timestamp: string; - isVerbose?: boolean; - usage?: { inputTokens?: number; outputTokens?: number }; -} +// +// `SessionEntry`, `ParsedMessage`, `parseSessionEntries`, and `entryToMessage` +// live in `@lobu/core/utils/session-file` so the worker's +// `/session/messages` route (`packages/agent-worker/src/server.ts`) and +// this gateway-side fallback can't drift again. `findSessionFile` stays +// here because the path-policy differs from the worker's — gateway scans +// `workspaces/` up to three levels deep with a SAFE_AGENT_ID +// guard; the worker scans its own `WORKSPACE_DIR` one level deep. async function findSessionFile(agentId: string): Promise { if (!isSafeAgentId(agentId)) return null; @@ -141,72 +124,6 @@ async function findSessionFile(agentId: string): Promise { return null; } -function parseSessionEntries(content: string): { - entries: SessionEntry[]; - sessionId?: string; -} { - const lines = content.split("\n").filter((l) => l.trim()); - const entries: SessionEntry[] = []; - let sessionId: string | undefined; - for (const line of lines) { - try { - const parsed = JSON.parse(line); - if (parsed.type === "session") { - sessionId = parsed.id; - continue; - } - entries.push(parsed); - } catch { - // Skip malformed - } - } - return { entries, sessionId }; -} - -function entryToMessage(entry: SessionEntry): ParsedMessage | null { - if (entry.type === "message" && entry.message) { - return { - id: entry.id, - type: "message", - role: entry.message.role, - content: entry.message.content, - timestamp: entry.timestamp, - isVerbose: entry.message.role === "toolResult", - usage: entry.message.usage, - }; - } - if (entry.type === "compaction") { - return { - id: entry.id, - type: "compaction", - content: entry.summary || "", - timestamp: entry.timestamp, - isVerbose: true, - }; - } - if (entry.type === "model_change") { - return { - id: entry.id, - type: "model_change", - content: `${entry.provider}/${entry.modelId}`, - model: `${entry.provider}/${entry.modelId}`, - timestamp: entry.timestamp, - isVerbose: true, - }; - } - if (entry.type === "custom_message") { - return { - id: entry.id, - type: "custom_message", - role: "user", - content: entry.content, - timestamp: entry.timestamp, - isVerbose: !entry.display, - }; - } - return null; -} - async function readSessionMessages( agentId: string, cursorParam: string, From 8b1ac2115722da3d0dcd4c52064bde7b1c8ece94 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Burak=20Emre=20Kabakc=C4=B1?= Date: Tue, 19 May 2026 18:36:26 +0100 Subject: [PATCH 4/9] wip(spike): delete wire-type re-export shims, import from @lobu/core directly Spike 8a5f6868 hoisted `MessagePayload`/`JobType`/`QueuedMessage` into `@lobu/core` but left three re-export shims so existing import paths kept resolving. The user's global CLAUDE.md is explicit ("Do NOT add backwards- compatibility shims, deprecated annotations, or fallback aliases"), and codex flagged the leftover aliases on review. Delete them: - `packages/server/src/gateway/infrastructure/queue/queue-producer.ts` drop `export type { JobType, MessagePayload } from "@lobu/core"`. - `packages/server/src/gateway/orchestration/base-deployment-manager.ts` drop `import type { MessagePayload } from "../infrastructure/queue/ queue-producer.js"` + the trailing `export type { MessagePayload }` alias; pull the type from `@lobu/core` instead. - `packages/agent-worker/src/gateway/types.ts` shrinks to its only remaining concern: the worker-only `ResponseData = ThreadResponsePayload & { originalMessageId: string }`. The wire-contract types are gone. Every importer migrated to `import type { MessagePayload, ... } from "@lobu/core"`: - worker: `sse-client.ts`, `message-batcher.ts`, `__tests__/message- batcher.test.ts`. - gateway: `orchestration/{impl/embedded-deployment,message-consumer}.ts`, `services/platform-helpers.ts`, three orchestration tests (`embedded-deployment.test.ts`, `orchestration-harden.test.ts`, `base-deployment-grants.test.ts`). Validation: `make build-packages` clean. `make typecheck` clean (server + owletto). `bunx tsc --noEmit` clean inside `packages/agent-worker`. --- .../src/__tests__/message-batcher.test.ts | 2 +- .../agent-worker/src/gateway/message-batcher.ts | 3 +-- packages/agent-worker/src/gateway/sse-client.ts | 3 ++- packages/agent-worker/src/gateway/types.ts | 17 ++++------------- .../__tests__/base-deployment-grants.test.ts | 2 +- .../__tests__/embedded-deployment.test.ts | 7 ++----- .../__tests__/orchestration-harden.test.ts | 2 +- .../infrastructure/queue/queue-producer.ts | 6 ------ .../orchestration/base-deployment-manager.ts | 4 +--- .../orchestration/impl/embedded-deployment.ts | 8 ++++++-- .../gateway/orchestration/message-consumer.ts | 2 +- .../src/gateway/services/platform-helpers.ts | 2 +- 12 files changed, 21 insertions(+), 37 deletions(-) diff --git a/packages/agent-worker/src/__tests__/message-batcher.test.ts b/packages/agent-worker/src/__tests__/message-batcher.test.ts index 49ffdaf51..33cdb5f76 100644 --- a/packages/agent-worker/src/__tests__/message-batcher.test.ts +++ b/packages/agent-worker/src/__tests__/message-batcher.test.ts @@ -13,8 +13,8 @@ */ import { describe, expect, test } from "bun:test"; +import type { QueuedMessage } from "@lobu/core"; import { MessageBatcher } from "../gateway/message-batcher"; -import type { QueuedMessage } from "../gateway/types"; function makeMsg( messageId: string, diff --git a/packages/agent-worker/src/gateway/message-batcher.ts b/packages/agent-worker/src/gateway/message-batcher.ts index bb5bf32c1..04fe78209 100644 --- a/packages/agent-worker/src/gateway/message-batcher.ts +++ b/packages/agent-worker/src/gateway/message-batcher.ts @@ -2,8 +2,7 @@ * Message batching for grouping rapid messages */ -import { createLogger } from "@lobu/core"; -import type { QueuedMessage } from "./types"; +import { createLogger, type QueuedMessage } from "@lobu/core"; const logger = createLogger("message-batcher"); diff --git a/packages/agent-worker/src/gateway/sse-client.ts b/packages/agent-worker/src/gateway/sse-client.ts index e9b95f85d..c8beb25a1 100644 --- a/packages/agent-worker/src/gateway/sse-client.ts +++ b/packages/agent-worker/src/gateway/sse-client.ts @@ -8,6 +8,8 @@ import { createLogger, extractTraceId, flushTracing, + type MessagePayload, + type QueuedMessage, SpanStatusCode, stripEnv, } from "@lobu/core"; @@ -16,7 +18,6 @@ import type { WorkerConfig, WorkerExecutor } from "../core/types"; import { SENSITIVE_WORKER_ENV_KEYS } from "../shared/worker-env-keys"; import { HttpWorkerTransport } from "./gateway-integration"; import { MessageBatcher } from "./message-batcher"; -import type { MessagePayload, QueuedMessage } from "./types"; const logger = createLogger("sse-client"); diff --git a/packages/agent-worker/src/gateway/types.ts b/packages/agent-worker/src/gateway/types.ts index 001404290..cd68264e7 100644 --- a/packages/agent-worker/src/gateway/types.ts +++ b/packages/agent-worker/src/gateway/types.ts @@ -1,22 +1,13 @@ /** - * Worker-side gateway-communication types. + * Worker-side response payload returned to the gateway over HTTP. * - * `MessagePayload`, `JobType`, and `QueuedMessage` live in `@lobu/core` — - * see `packages/core/src/worker/wire.ts` — and are re-exported here so the - * existing `from "./types"` imports inside the worker keep resolving. + * The gateway↔worker wire contract (`MessagePayload`, `JobType`, + * `QueuedMessage`) lives in `@lobu/core/worker/wire` — import from there + * directly, not from this file. */ import type { ThreadResponsePayload } from "@lobu/core"; -export type { - JobType, - MessagePayload, - QueuedMessage, -} from "@lobu/core"; - -/** - * Response data sent back to gateway - */ export type ResponseData = ThreadResponsePayload & { originalMessageId: string; }; diff --git a/packages/server/src/gateway/__tests__/base-deployment-grants.test.ts b/packages/server/src/gateway/__tests__/base-deployment-grants.test.ts index 9138d9b44..98173a0a5 100644 --- a/packages/server/src/gateway/__tests__/base-deployment-grants.test.ts +++ b/packages/server/src/gateway/__tests__/base-deployment-grants.test.ts @@ -1,5 +1,5 @@ import { beforeAll, beforeEach, describe, expect, spyOn, test } from "bun:test"; -import type { MessagePayload } from "../infrastructure/queue/queue-producer.js"; +import type { MessagePayload } from "@lobu/core"; import { BaseDeploymentManager, type DeploymentInfo, diff --git a/packages/server/src/gateway/__tests__/embedded-deployment.test.ts b/packages/server/src/gateway/__tests__/embedded-deployment.test.ts index 8ba24e0df..85256e0b1 100644 --- a/packages/server/src/gateway/__tests__/embedded-deployment.test.ts +++ b/packages/server/src/gateway/__tests__/embedded-deployment.test.ts @@ -10,11 +10,8 @@ import { import { EventEmitter } from "node:events"; import fs from "node:fs"; import path from "node:path"; -import { ErrorCode, OrchestratorError } from "@lobu/core"; -import type { - MessagePayload, - OrchestratorConfig, -} from "../orchestration/base-deployment-manager.js"; +import { ErrorCode, type MessagePayload, OrchestratorError } from "@lobu/core"; +import type { OrchestratorConfig } from "../orchestration/base-deployment-manager.js"; // --------------------------------------------------------------------------- // Mock child_process.spawn to return a fake ChildProcess diff --git a/packages/server/src/gateway/__tests__/orchestration-harden.test.ts b/packages/server/src/gateway/__tests__/orchestration-harden.test.ts index 6c1d08e06..8ebd7dff2 100644 --- a/packages/server/src/gateway/__tests__/orchestration-harden.test.ts +++ b/packages/server/src/gateway/__tests__/orchestration-harden.test.ts @@ -80,12 +80,12 @@ mock.module("node:child_process", () => ({ // ── Import classes after mock ──────────────────────────────────────────────── +import type { MessagePayload } from "@lobu/core"; import { EmbeddedDeploymentManager } from "../orchestration/impl/embedded-deployment.js"; import { buildCanonicalConversationKey, generateDeploymentName, type OrchestratorConfig, - type MessagePayload, } from "../orchestration/base-deployment-manager.js"; import { backoffSeconds, diff --git a/packages/server/src/gateway/infrastructure/queue/queue-producer.ts b/packages/server/src/gateway/infrastructure/queue/queue-producer.ts index dcff8ca21..ae45d148f 100644 --- a/packages/server/src/gateway/infrastructure/queue/queue-producer.ts +++ b/packages/server/src/gateway/infrastructure/queue/queue-producer.ts @@ -5,12 +5,6 @@ import type { IMessageQueue } from "./types.js"; const logger = createLogger("queue-producer"); -// `MessagePayload` and `JobType` are the gateway↔worker wire contract — both -// sides need to agree, so they live in `@lobu/core` (see -// `packages/core/src/worker/wire.ts`). Re-exported here so existing -// `from "../infrastructure/queue/queue-producer"` callers don't change. -export type { JobType, MessagePayload } from "@lobu/core"; - /** * Queue producer for dispatching messages to the runs queue. * Handles both direct_message and thread_message queues with bot isolation. diff --git a/packages/server/src/gateway/orchestration/base-deployment-manager.ts b/packages/server/src/gateway/orchestration/base-deployment-manager.ts index 06107b5d0..2ab004ead 100644 --- a/packages/server/src/gateway/orchestration/base-deployment-manager.ts +++ b/packages/server/src/gateway/orchestration/base-deployment-manager.ts @@ -4,10 +4,10 @@ import { ErrorCode, extractTraceId, generateWorkerToken, + type MessagePayload, OrchestratorError, } from "@lobu/core"; import type { ProviderCredentialContext } from "../embedded.js"; -import type { MessagePayload } from "../infrastructure/queue/queue-producer.js"; import type { ModelProviderModule } from "../modules/module-system.js"; import type { GrantStore } from "../permissions/grant-store.js"; import { @@ -23,8 +23,6 @@ import { persistSecretValue, type WritableSecretStore, } from "../secrets/index.js"; -// Re-export MessagePayload for use by deployment implementations -export type { MessagePayload }; const logger = createLogger("orchestrator"); diff --git a/packages/server/src/gateway/orchestration/impl/embedded-deployment.ts b/packages/server/src/gateway/orchestration/impl/embedded-deployment.ts index 1a4e1e62f..3a86a059a 100644 --- a/packages/server/src/gateway/orchestration/impl/embedded-deployment.ts +++ b/packages/server/src/gateway/orchestration/impl/embedded-deployment.ts @@ -1,13 +1,17 @@ import { type ChildProcess, execFileSync, spawn } from "node:child_process"; import fs from "node:fs"; import path from "node:path"; -import { createLogger, ErrorCode, OrchestratorError } from "@lobu/core"; +import { + createLogger, + ErrorCode, + type MessagePayload, + OrchestratorError, +} from "@lobu/core"; import { getDb } from "../../../db/client.js"; import type { ModelProviderModule } from "../../modules/module-system.js"; import { BaseDeploymentManager, type DeploymentInfo, - type MessagePayload, type ModuleEnvVarsBuilder, type OrchestratorConfig, } from "../base-deployment-manager.js"; diff --git a/packages/server/src/gateway/orchestration/message-consumer.ts b/packages/server/src/gateway/orchestration/message-consumer.ts index b752b07a7..1c1966191 100644 --- a/packages/server/src/gateway/orchestration/message-consumer.ts +++ b/packages/server/src/gateway/orchestration/message-consumer.ts @@ -7,6 +7,7 @@ import { generateWorkerToken, getTraceparent, type GuardrailRegistry, + type MessagePayload, OrchestratorError, retryWithBackoff, runGuardrails, @@ -25,7 +26,6 @@ import { type BaseDeploymentManager, buildCanonicalConversationKey, generateDeploymentName, - type MessagePayload, type OrchestratorConfig, } from "./base-deployment-manager.js"; diff --git a/packages/server/src/gateway/services/platform-helpers.ts b/packages/server/src/gateway/services/platform-helpers.ts index e34ae4350..751d6b83b 100644 --- a/packages/server/src/gateway/services/platform-helpers.ts +++ b/packages/server/src/gateway/services/platform-helpers.ts @@ -5,6 +5,7 @@ import { createLogger, + type MessagePayload, type PluginConfig, type PluginsConfig, } from "@lobu/core"; @@ -12,7 +13,6 @@ import type { AgentSettingsStore } from "../auth/settings/agent-settings-store.j import { resolveEffectiveModelRef } from "../auth/settings/model-selection.js"; import type { ChannelBindingService } from "../channels/binding-service.js"; import { buildMemoryPlugins, getInternalGatewayUrl } from "../config/index.js"; -import type { MessagePayload } from "../infrastructure/queue/queue-producer.js"; const logger = createLogger("platform-helpers"); const LOBU_PLUGIN_SOURCE = "@lobu/openclaw-plugin"; From 9de939d385a531b82ae3b809dc36d0efdbde0b3c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Burak=20Emre=20Kabakc=C4=B1?= Date: Tue, 19 May 2026 18:39:35 +0100 Subject: [PATCH 5/9] wip(spike): delete dead module-lifecycle and dispatcher surfaces MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Spike 83a2cf4a deleted the worker call sites for `initModuleWorkspace`, `onSessionStart`, and `collectModuleData` but left the interfaces and no-op base implementations they targeted. Codex flagged the dead surfaces on review; the rule from CLAUDE.md is "remove the old code entirely instead of keeping it alongside the new code." Verified zero callers anywhere in the monorepo before deleting: for sym in onSessionStart onSessionEnd initWorkspace \ onBeforeResponse generateActionButtons handleAction \ ActionButton ModuleSessionContext DispatcherModule \ DispatcherContext getContainerAddress getWorkerModules; do grep -rn "\\b$sym\\b" packages/ scripts/ examples/ config/ done After this commit the only matches are `initWorkspaceProvider` in `packages/server/src/workspace/` (different concept, unrelated). The sibling `WorkerContext` symbol in `packages/server/src/gateway/routes/ internal/*.ts` is a Hono variable-context type; the deleted one was the worker-module hook context and is removed only from `packages/core/src/ modules.ts`. `BaseProviderModule` (the only `BaseModule` subclass — Claude / ChatGPT / Bedrock / Gemini / API-key catalog) was verified to NOT override any of the deleted methods (it overrides `isEnabled`, `buildEnvVars`, `getModelOptions`, and the provider-specific surface only), so trimming `BaseModule` is type-safe. Deletions: - `packages/core/src/modules.ts`: `WorkerContext`, `WorkerModule`, `ActionButton`, `ModuleSessionContext` interfaces; `IModuleRegistry.getWorkerModules` + `ModuleRegistry.getWorkerModules` method. - `packages/core/src/index.ts`: `ActionButton` and `ModuleSessionContext` type re-exports. - `packages/server/src/gateway/modules/module-system.ts`: `DispatcherContext`, `DispatcherModule` interfaces; the `WorkerModule` and `DispatcherModule` clauses on `BaseModule implements`; the `initWorkspace`, `onSessionStart`, `onSessionEnd`, `onBeforeResponse`, `generateActionButtons`, `handleAction`, `getContainerAddress` methods on `BaseModule`; and the `getContainerAddress(): string` member on the `OrchestratorModule` interface. Validation: `make typecheck` clean (server + owletto). `bunx tsc --noEmit` clean inside `packages/agent-worker`. `make build-packages` clean. --- packages/core/src/index.ts | 1 - packages/core/src/modules.ts | 43 ----------- .../src/gateway/modules/module-system.ts | 75 +------------------ 3 files changed, 3 insertions(+), 116 deletions(-) diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index b5685060e..9f89cb51d 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -70,7 +70,6 @@ export { } from "./lobu-toml-schema"; export * from "./logger"; // Module system -export type { ActionButton, ModuleSessionContext } from "./modules"; export * from "./modules"; export type { OtelConfig, Span, Tracer } from "./otel"; // OpenTelemetry tracing diff --git a/packages/core/src/modules.ts b/packages/core/src/modules.ts index 100bbf101..db4e5dec7 100644 --- a/packages/core/src/modules.ts +++ b/packages/core/src/modules.ts @@ -20,49 +20,12 @@ export interface ModuleInterface<_TModuleData = unknown> { registerEndpoints(app: any): void; } -export interface WorkerContext { - workspaceDir: string; - userId: string; - conversationId: string; -} - -export interface WorkerModule - extends ModuleInterface { - /** Initialize workspace - called when worker starts session */ - initWorkspace(config: any): Promise; - - /** Called at session start - can modify system prompt */ - onSessionStart(context: ModuleSessionContext): Promise; - - /** Called at session end - can add action buttons */ - onSessionEnd(context: ModuleSessionContext): Promise; - - /** Collect module-specific data before sending response. Return null if no data. */ - onBeforeResponse(context: WorkerContext): Promise; -} - -export interface ModuleSessionContext { - userId: string; - conversationId: string; - systemPrompt: string; - workspace?: any; -} - -export interface ActionButton { - text: string; - action_id: string; - style?: "primary" | "danger"; - value?: string; - url?: string; -} - // ============================================================================ // Module Registry // ============================================================================ export interface IModuleRegistry { register(module: ModuleInterface): void; - getWorkerModules(): WorkerModule[]; registerAvailableModules(modulePackages?: string[]): Promise; initAll(): Promise; registerEndpoints(app: any): void; @@ -162,12 +125,6 @@ export class ModuleRegistry implements IModuleRegistry { } } - getWorkerModules(): WorkerModule[] { - return Array.from(this.modules.values()).filter( - (m): m is WorkerModule => "onBeforeResponse" in m - ); - } - getModules(): ModuleInterface[] { return Array.from(this.modules.values()); } diff --git a/packages/server/src/gateway/modules/module-system.ts b/packages/server/src/gateway/modules/module-system.ts index cb9deaed7..84f88464b 100644 --- a/packages/server/src/gateway/modules/module-system.ts +++ b/packages/server/src/gateway/modules/module-system.ts @@ -1,12 +1,5 @@ import type { CliBackendConfig } from "@lobu/core"; -import { - type ActionButton, - type ModuleInterface, - type ModuleSessionContext, - moduleRegistry, - type WorkerContext, - type WorkerModule, -} from "@lobu/core"; +import { type ModuleInterface, moduleRegistry } from "@lobu/core"; import type { ProviderCredentialContext } from "../embedded.js"; export interface ModelOption { @@ -21,7 +14,6 @@ interface OrchestratorModule baseEnv: Record, context?: ProviderCredentialContext ): Promise>; - getContainerAddress(): string; } export interface ProviderUpstreamConfig { @@ -79,32 +71,8 @@ export interface ModelProviderModule extends OrchestratorModule { }>; } -interface DispatcherContext { - userId: string; - channelId: string; - threadTs: string; - platformClient?: unknown; - moduleData: TModuleData; -} - -interface DispatcherModule - extends ModuleInterface { - generateActionButtons( - context: DispatcherContext - ): Promise; - handleAction( - actionId: string, - userId: string, - agentId: string, - context: any - ): Promise; -} - -export abstract class BaseModule - implements - WorkerModule, - DispatcherModule, - OrchestratorModule +export abstract class BaseModule<_TModuleData = unknown> + implements OrchestratorModule<_TModuleData> { abstract name: string; abstract isEnabled(): boolean; @@ -117,24 +85,6 @@ export abstract class BaseModule // no-op } - async initWorkspace(_config: any): Promise { - // no-op - } - - async onSessionStart( - context: ModuleSessionContext - ): Promise { - return context; - } - - async onSessionEnd(_context: ModuleSessionContext): Promise { - return []; - } - - async onBeforeResponse(_context: WorkerContext): Promise { - return null; - } - async buildEnvVars( _agentId: string, baseEnv: Record, @@ -149,25 +99,6 @@ export abstract class BaseModule ): Promise { return []; } - - getContainerAddress(): string { - return ""; - } - - async generateActionButtons( - _context: DispatcherContext - ): Promise { - return []; - } - - async handleAction( - _actionId: string, - _userId: string, - _agentId: string, - _context: any - ): Promise { - return false; - } } export function getOrchestratorModules(): OrchestratorModule[] { From ee9e6e7c51aa694f7012650ef146b3b419f07c6e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Burak=20Emre=20Kabakc=C4=B1?= Date: Tue, 19 May 2026 18:41:04 +0100 Subject: [PATCH 6/9] wip(spike): delete unused dynamic-import plugin path in moduleRegistry MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `ModuleRegistry.registerAvailableModules(modulePackages?: string[])` exists to dynamically `await import(packageName)` plugin packages at startup. Both call sites pass no args — the loop body is unreachable: packages/agent-worker/src/index.ts:42 → `registerAvailableModules()` packages/server/src/gateway/services/core-services.ts:844 → same Verified monorepo-wide: grep -rn "registerAvailableModules" packages/ scripts/ examples/ config/ Two hits, both no-arg. The method is also the only dynamic `import()` in `@lobu/core`, which conflicts with the project memory rule "No dynamic imports — always static `import`" (`feedback_no_dynamic_imports`). Deletions: - `packages/core/src/modules.ts`: `IModuleRegistry.registerAvailableModules` signature and `ModuleRegistry.registerAvailableModules` method (~40 lines including the `@example` block). - `packages/agent-worker/src/index.ts:42`: drop the no-arg call. - `packages/server/src/gateway/services/core-services.ts:844`: drop the no-arg call; the comment above the remaining `initAll()` is updated to read "Initialize all registered modules" (registration now happens at the explicit `moduleRegistry.register(...)` sites earlier in `core-services.ts`). Behaviour: zero change. Both code paths previously entered the function, iterated an empty array, and returned. Removing them removes the only dynamic-import in `@lobu/core` and drops one no-op step out of every gateway/worker boot. Validation: `make typecheck` clean (server + owletto). `bunx tsc --noEmit` clean in `packages/agent-worker`. `make build-packages` clean. --- packages/agent-worker/src/index.ts | 1 - packages/core/src/modules.ts | 43 ------------------- .../src/gateway/services/core-services.ts | 3 +- 3 files changed, 1 insertion(+), 46 deletions(-) diff --git a/packages/agent-worker/src/index.ts b/packages/agent-worker/src/index.ts index 00a7b8813..7d6b45506 100644 --- a/packages/agent-worker/src/index.ts +++ b/packages/agent-worker/src/index.ts @@ -39,7 +39,6 @@ async function main() { logger.info(`Tracing initialized: lobu-worker -> ${otlpEndpoint}`); } - await moduleRegistry.registerAvailableModules(); await moduleRegistry.initAll(); logger.info("✅ Modules initialized"); diff --git a/packages/core/src/modules.ts b/packages/core/src/modules.ts index db4e5dec7..4050e24ff 100644 --- a/packages/core/src/modules.ts +++ b/packages/core/src/modules.ts @@ -26,7 +26,6 @@ export interface ModuleInterface<_TModuleData = unknown> { export interface IModuleRegistry { register(module: ModuleInterface): void; - registerAvailableModules(modulePackages?: string[]): Promise; initAll(): Promise; registerEndpoints(app: any): void; /** Return all registered modules as base ModuleInterface array. */ @@ -62,48 +61,6 @@ export class ModuleRegistry implements IModuleRegistry { } } - /** - * Automatically discover and register available modules. - * Tries to import module packages and registers them if available. - * - * @param modulePackages - List of module package names to try loading. - * Users can provide custom modules to register. - * - * @example - * // Register custom modules - * await moduleRegistry.registerAvailableModules([ - * '@mycompany/slack-module', - * '@mycompany/jira-module' - * ]); - */ - async registerAvailableModules(modulePackages: string[] = []): Promise { - for (const packageName of modulePackages) { - try { - // Dynamic import to avoid build-time dependencies - const moduleExports = await import(packageName); - - // Try common export patterns - const ModuleClass = - moduleExports.default || - Object.values(moduleExports).find( - (exp) => typeof exp === "function" && exp.name.endsWith("Module") - ); - - if (ModuleClass && typeof ModuleClass === "function") { - const moduleInstance = new (ModuleClass as any)(); - if (!this.modules.has(moduleInstance.name)) { - this.register(moduleInstance); - logger.debug(`${packageName} registered`); - } - } else { - logger.debug(`${packageName}: No module class found in exports`); - } - } catch { - logger.debug(`${packageName} not available`); - } - } - } - async initAll(): Promise { for (const module of this.modules.values()) { logger.debug(`Initializing module: ${module.name}`); diff --git a/packages/server/src/gateway/services/core-services.ts b/packages/server/src/gateway/services/core-services.ts index df936e899..bf66bec36 100644 --- a/packages/server/src/gateway/services/core-services.ts +++ b/packages/server/src/gateway/services/core-services.ts @@ -840,8 +840,7 @@ export class CoreServices { ); logger.debug("Worker gateway initialized"); - // Discover and initialize all available modules - await moduleRegistry.registerAvailableModules(); + // Initialize all registered modules await moduleRegistry.initAll(); logger.debug("Modules initialized"); } From 21a05e2ee6b97860204cfef8decb0647ac7515b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Burak=20Emre=20Kabakc=C4=B1?= Date: Tue, 19 May 2026 18:44:33 +0100 Subject: [PATCH 7/9] wip(spike): dedupe ModelOption and PrefillSkill; @lobu/core is the source MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two type duplicates that codex flagged on review: 1. `ModelOption` was declared in BOTH `packages/core/src/api-types.ts:38` AND `packages/server/src/gateway/modules/module-system.ts:5`. Same shape (`{label: string; value: string}`), declared twice. Every provider module imported the server-local copy; the core export was carried only by the public SDK surface. 2. `PrefillSkill` was declared in BOTH `packages/core/src/api-types.ts:94` AND `packages/server/src/gateway/auth/settings/token-service.ts:4`. Same shape. The token-service file also carried a local `PrefillMcpServer` interface that was structurally identical to core's `PrefillMcp` except for a narrowed `type: "sse" | "streamable-http" | "stdio"` vs core's `type?: string`. The narrowed type is declaration- only and never narrowed at runtime, so widening to core's shape is behaviour-neutral; the token route emits whatever string the operator passed in either way. `@lobu/core` wins (it's the public API surface; deleting it would be a breaking change for SDK consumers). The server-local duplicates go. Migrated imports — every consumer now pulls `ModelOption` / `PrefillMcp` / `PrefillSkill` from `@lobu/core` directly: - `packages/server/src/gateway/auth/gemini/cli-module.ts` - `packages/server/src/gateway/auth/api-key-provider-module.ts` - `packages/server/src/gateway/auth/claude/oauth-module.ts` - `packages/server/src/gateway/auth/bedrock/provider-module.ts` - `packages/server/src/gateway/auth/chatgpt/chatgpt-oauth-module.ts` - `packages/server/src/gateway/auth/provider-model-options.ts` - `packages/server/src/gateway/auth/settings/token-service.ts` (`prefillMcpServers?: PrefillMcp[]` — field name stays, element type pulled from core). - `packages/server/src/gateway/routes/public/agent-config.ts` - `packages/server/src/gateway/modules/module-system.ts` (`ModelOption` now re-imported from `@lobu/core` for the `OrchestratorModule` / `ModelProviderModule` interfaces in this file). Verification: grep -rn "^export interface ModelOption\\b\\|^interface ModelOption\\b" \ "^export interface PrefillSkill\\b\\|^interface PrefillSkill\\b" \ "^export interface PrefillMcp\\b\\|^interface PrefillMcpServer\\b" \ packages/ 2>/dev/null | grep -v "/dist/" returns one match per symbol — all under `packages/core/src/api-types.ts`. Validation: `make typecheck` clean (server + owletto). `bunx tsc --noEmit` clean inside `packages/agent-worker`. `make build-packages` clean. --- .../gateway/auth/api-key-provider-module.ts | 3 +- .../gateway/auth/bedrock/provider-module.ts | 3 +- .../auth/chatgpt/chatgpt-oauth-module.ts | 3 +- .../src/gateway/auth/claude/oauth-module.ts | 3 +- .../src/gateway/auth/gemini/cli-module.ts | 2 +- .../gateway/auth/provider-model-options.ts | 7 ++-- .../gateway/auth/settings/token-service.ts | 34 ++----------------- .../src/gateway/modules/module-system.ts | 7 +--- .../src/gateway/routes/public/agent-config.ts | 7 ++-- 9 files changed, 15 insertions(+), 54 deletions(-) diff --git a/packages/server/src/gateway/auth/api-key-provider-module.ts b/packages/server/src/gateway/auth/api-key-provider-module.ts index e31b63cf1..c33aad0c6 100644 --- a/packages/server/src/gateway/auth/api-key-provider-module.ts +++ b/packages/server/src/gateway/auth/api-key-provider-module.ts @@ -1,5 +1,4 @@ -import type { ConfigProviderMeta } from "@lobu/core"; -import type { ModelOption } from "../modules/module-system.js"; +import type { ConfigProviderMeta, ModelOption } from "@lobu/core"; import { BaseProviderModule } from "./base-provider-module.js"; import type { AuthProfilesManager } from "./settings/auth-profiles-manager.js"; import { fetchModelOptions } from "./utils/fetch-model-options.js"; diff --git a/packages/server/src/gateway/auth/bedrock/provider-module.ts b/packages/server/src/gateway/auth/bedrock/provider-module.ts index debbc567c..9d7be3f66 100644 --- a/packages/server/src/gateway/auth/bedrock/provider-module.ts +++ b/packages/server/src/gateway/auth/bedrock/provider-module.ts @@ -1,5 +1,4 @@ -import type { ConfigProviderMeta } from "@lobu/core"; -import type { ModelOption } from "../../modules/module-system.js"; +import type { ConfigProviderMeta, ModelOption } from "@lobu/core"; import type { BedrockModelCatalog } from "../../services/bedrock-model-catalog.js"; import { BaseProviderModule } from "../base-provider-module.js"; import type { AuthProfilesManager } from "../settings/auth-profiles-manager.js"; diff --git a/packages/server/src/gateway/auth/chatgpt/chatgpt-oauth-module.ts b/packages/server/src/gateway/auth/chatgpt/chatgpt-oauth-module.ts index 4002cbae4..2c2b258b5 100644 --- a/packages/server/src/gateway/auth/chatgpt/chatgpt-oauth-module.ts +++ b/packages/server/src/gateway/auth/chatgpt/chatgpt-oauth-module.ts @@ -1,5 +1,4 @@ -import { createLogger } from "@lobu/core"; -import type { ModelOption } from "../../modules/module-system.js"; +import { createLogger, type ModelOption } from "@lobu/core"; import { BaseProviderModule } from "../base-provider-module.js"; import { type AuthProfilesManager, diff --git a/packages/server/src/gateway/auth/claude/oauth-module.ts b/packages/server/src/gateway/auth/claude/oauth-module.ts index df1c2e2db..0fcb9f200 100644 --- a/packages/server/src/gateway/auth/claude/oauth-module.ts +++ b/packages/server/src/gateway/auth/claude/oauth-module.ts @@ -1,5 +1,4 @@ -import { createLogger } from "@lobu/core"; -import type { ModelOption } from "../../modules/module-system.js"; +import { createLogger, type ModelOption } from "@lobu/core"; import { BaseProviderModule } from "../base-provider-module.js"; import { resolveEnv } from "../mcp/string-substitution.js"; import type { OAuthCredentials } from "../oauth/credentials.js"; diff --git a/packages/server/src/gateway/auth/gemini/cli-module.ts b/packages/server/src/gateway/auth/gemini/cli-module.ts index 5e2ac284d..7164fa0b3 100644 --- a/packages/server/src/gateway/auth/gemini/cli-module.ts +++ b/packages/server/src/gateway/auth/gemini/cli-module.ts @@ -1,4 +1,4 @@ -import type { ModelOption } from "../../modules/module-system.js"; +import type { ModelOption } from "@lobu/core"; import { CliBackendOnlyModule } from "../cli-backend-only-module.js"; import type { AuthProfilesManager } from "../settings/auth-profiles-manager.js"; diff --git a/packages/server/src/gateway/auth/provider-model-options.ts b/packages/server/src/gateway/auth/provider-model-options.ts index 1e1800777..f5e615841 100644 --- a/packages/server/src/gateway/auth/provider-model-options.ts +++ b/packages/server/src/gateway/auth/provider-model-options.ts @@ -1,8 +1,5 @@ -import { createLogger } from "@lobu/core"; -import { - getModelProviderModules, - type ModelOption, -} from "../modules/module-system.js"; +import { createLogger, type ModelOption } from "@lobu/core"; +import { getModelProviderModules } from "../modules/module-system.js"; const logger = createLogger("provider-model-options"); diff --git a/packages/server/src/gateway/auth/settings/token-service.ts b/packages/server/src/gateway/auth/settings/token-service.ts index f109bd74e..5128bab1c 100644 --- a/packages/server/src/gateway/auth/settings/token-service.ts +++ b/packages/server/src/gateway/auth/settings/token-service.ts @@ -1,34 +1,4 @@ -/** - * Pre-filled skill configuration for an agent config session - */ -interface PrefillSkill { - /** Skill repository (e.g., "anthropics/skills/pdf") */ - repo: string; - /** Display name */ - name?: string; - /** Description */ - description?: string; -} - -/** - * Pre-filled MCP server configuration for an agent config session - */ -interface PrefillMcpServer { - /** MCP server ID (key in mcpServers record) */ - id: string; - /** Display name/description */ - name?: string; - /** Server URL (for SSE type) */ - url?: string; - /** Server type */ - type?: "sse" | "streamable-http" | "stdio"; - /** Command (for stdio type) */ - command?: string; - /** Args (for stdio type) */ - args?: string[]; - /** Environment variables needed (just the keys, user fills values) */ - envVars?: string[]; -} +import type { PrefillMcp, PrefillSkill } from "@lobu/core"; /** * Source message context where settings link was requested. @@ -70,7 +40,7 @@ export interface SettingsTokenPayload { /** Optional skills to pre-fill (user confirms to enable) */ prefillSkills?: PrefillSkill[]; /** Optional MCP servers to pre-fill (user confirms to enable) */ - prefillMcpServers?: PrefillMcpServer[]; + prefillMcpServers?: PrefillMcp[]; /** Optional Nix packages to pre-fill */ prefillNixPackages?: string[]; /** Optional domain patterns to pre-fill as grants */ diff --git a/packages/server/src/gateway/modules/module-system.ts b/packages/server/src/gateway/modules/module-system.ts index 84f88464b..162bcaa02 100644 --- a/packages/server/src/gateway/modules/module-system.ts +++ b/packages/server/src/gateway/modules/module-system.ts @@ -1,12 +1,7 @@ -import type { CliBackendConfig } from "@lobu/core"; +import type { CliBackendConfig, ModelOption } from "@lobu/core"; import { type ModuleInterface, moduleRegistry } from "@lobu/core"; import type { ProviderCredentialContext } from "../embedded.js"; -export interface ModelOption { - value: string; - label: string; -} - interface OrchestratorModule extends ModuleInterface { buildEnvVars( diff --git a/packages/server/src/gateway/routes/public/agent-config.ts b/packages/server/src/gateway/routes/public/agent-config.ts index e78a99656..821ec87b1 100644 --- a/packages/server/src/gateway/routes/public/agent-config.ts +++ b/packages/server/src/gateway/routes/public/agent-config.ts @@ -5,7 +5,11 @@ */ import { createRoute, OpenAPIHono, z } from "@hono/zod-openapi"; -import type { AgentConfigStore, SkillConfig } from "@lobu/core"; +import type { + AgentConfigStore, + ModelOption, + SkillConfig, +} from "@lobu/core"; import type { ProviderCatalogService } from "../../auth/provider-catalog.js"; import { collectProviderModelOptions } from "../../auth/provider-model-options.js"; @@ -28,7 +32,6 @@ import type { WorkerConnectionManager } from "../../gateway/connection-manager.j import type { IMessageQueue } from "../../infrastructure/queue/index.js"; import { getModelProviderModules, - type ModelOption, type ModelProviderModule, } from "../../modules/module-system.js"; import type { GrantStore } from "../../permissions/grant-store.js"; From ff599d2eb0f7cc59217e08c61b830d19107ab23b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Burak=20Emre=20Kabakc=C4=B1?= Date: Tue, 19 May 2026 18:51:46 +0100 Subject: [PATCH 8/9] wip(spike): drop unused TModuleData generic; fix stale wire-path comment MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two trivial leftovers from rounds 1–3: 1. The `` type parameter on `ModuleInterface` (core), `OrchestratorModule` (server module-system), and `BaseModule` (server module-system) was a placeholder for the deleted `WorkerModule` surface (`onBeforeResponse(): Promise`). With that surface gone in spike 9de939d3 the parameter has no semantic use — the core declaration already had it underscored (`_TModuleData`), and every instantiation site (`ModelProviderModule extends OrchestratorModule`, `OrchestratorModule[]`, `(m): m is OrchestratorModule` guard) was passing no arg. Removed the type parameter from all three declarations. 2. `packages/agent-worker/src/gateway/types.ts:5` carried a stale doc comment pointing imports at `@lobu/core/worker/wire`. That subpath isn't declared in `packages/core/package.json`'s `exports` field — only `.` is exported — so the path doesn't resolve. Every actual import in the tree already uses `@lobu/core` (the package root). Comment updated to match. No code import paths changed. Verification (`grep -rn`, excluding `/dist/`): - `BaseModule<` / `OrchestratorModule<` / `ModuleInterface<` → zero matches. - `TModuleData` → zero matches. - `@lobu/core/worker/wire` → zero matches. `make typecheck` clean (server + owletto). `bunx tsc --noEmit` clean in `packages/agent-worker`. `make build-packages` clean. --- packages/agent-worker/src/gateway/types.ts | 2 +- packages/core/src/modules.ts | 2 +- packages/server/src/gateway/modules/module-system.ts | 7 ++----- 3 files changed, 4 insertions(+), 7 deletions(-) diff --git a/packages/agent-worker/src/gateway/types.ts b/packages/agent-worker/src/gateway/types.ts index cd68264e7..51b826554 100644 --- a/packages/agent-worker/src/gateway/types.ts +++ b/packages/agent-worker/src/gateway/types.ts @@ -2,7 +2,7 @@ * Worker-side response payload returned to the gateway over HTTP. * * The gateway↔worker wire contract (`MessagePayload`, `JobType`, - * `QueuedMessage`) lives in `@lobu/core/worker/wire` — import from there + * `QueuedMessage`) is exported from `@lobu/core` — import from there * directly, not from this file. */ diff --git a/packages/core/src/modules.ts b/packages/core/src/modules.ts index 4050e24ff..fde876f35 100644 --- a/packages/core/src/modules.ts +++ b/packages/core/src/modules.ts @@ -6,7 +6,7 @@ const logger = createLogger("modules"); // Module Type Definitions // ============================================================================ -export interface ModuleInterface<_TModuleData = unknown> { +export interface ModuleInterface { /** Module identifier */ name: string; diff --git a/packages/server/src/gateway/modules/module-system.ts b/packages/server/src/gateway/modules/module-system.ts index 162bcaa02..9d17018af 100644 --- a/packages/server/src/gateway/modules/module-system.ts +++ b/packages/server/src/gateway/modules/module-system.ts @@ -2,8 +2,7 @@ import type { CliBackendConfig, ModelOption } from "@lobu/core"; import { type ModuleInterface, moduleRegistry } from "@lobu/core"; import type { ProviderCredentialContext } from "../embedded.js"; -interface OrchestratorModule - extends ModuleInterface { +interface OrchestratorModule extends ModuleInterface { buildEnvVars( agentId: string, baseEnv: Record, @@ -66,9 +65,7 @@ export interface ModelProviderModule extends OrchestratorModule { }>; } -export abstract class BaseModule<_TModuleData = unknown> - implements OrchestratorModule<_TModuleData> -{ +export abstract class BaseModule implements OrchestratorModule { abstract name: string; abstract isEnabled(): boolean; From 676dc563d0c4fcc780b5c5735c964e04aad9e2b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Burak=20Emre=20Kabakc=C4=B1?= Date: Tue, 19 May 2026 19:01:29 +0100 Subject: [PATCH 9/9] wip(spike): delete dead moduleData wire plumbing; fix stale subpath comment MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two trivial leftovers from spike 9de939d3 (the module-lifecycle deletion) and spike 9a401f54 (the session-file hoist): 1. `moduleData` was wire-plumbing for the deleted `collectModuleData()` path. The collector was removed in 9de939d3, so nothing calls `setModuleData` anymore — `this.moduleData` is always `undefined`, and the two `sendResponse` sites that read it serialize `undefined` into every response. Verified monorepo-wide: grep -rn "\\bmoduleData\\b\\|setModuleData" packages/ scripts/ \ examples/ config/ Five hits, all on the write side (declaration / setter / two reads of the always-undefined field / the wire-type slot). Zero readers on the gateway end. Deleted: - `packages/core/src/types.ts`: `moduleData?` field on `ThreadResponsePayload`. - `packages/core/src/worker/transport.ts`: `setModuleData` method on the `WorkerTransport` interface. - `packages/agent-worker/src/gateway/gateway-integration.ts`: the `private moduleData?` field, the `setModuleData` method, and the two `moduleData: this.moduleData` lines in `sendStreamDelta` / `signalCompletion`. 2. `packages/server/src/gateway/routes/public/agent-history.ts:75` pointed at `@lobu/core/utils/session-file`. That subpath isn't declared in `packages/core/package.json`'s `exports` — only `.` is exported. Every actual import in the tree uses `@lobu/core`. Comment updated to match reality. `make typecheck` clean (server + owletto). `bunx tsc --noEmit` clean in `packages/agent-worker`. `make build-packages` clean. Monorepo grep for `moduleData` / `setModuleData` / `@lobu/core/utils/session-file` returns zero matches. --- packages/agent-worker/src/gateway/gateway-integration.ts | 7 ------- packages/core/src/types.ts | 1 - packages/core/src/worker/transport.ts | 6 ------ .../server/src/gateway/routes/public/agent-history.ts | 8 ++++---- 4 files changed, 4 insertions(+), 18 deletions(-) diff --git a/packages/agent-worker/src/gateway/gateway-integration.ts b/packages/agent-worker/src/gateway/gateway-integration.ts index ce17ddfcf..a66827713 100644 --- a/packages/agent-worker/src/gateway/gateway-integration.ts +++ b/packages/agent-worker/src/gateway/gateway-integration.ts @@ -27,7 +27,6 @@ export class HttpWorkerTransport implements WorkerTransport { private botResponseTs?: string; public processedMessageIds: string[] = []; private jobId?: string; - private moduleData?: Record; private teamId: string; private platform?: string; private platformMetadata?: Record; @@ -52,10 +51,6 @@ export class HttpWorkerTransport implements WorkerTransport { this.jobId = jobId; } - setModuleData(moduleData: Record): void { - this.moduleData = moduleData; - } - async signalDone(finalDelta?: string): Promise { // Send final delta if there is one if (finalDelta) { @@ -137,7 +132,6 @@ export class HttpWorkerTransport implements WorkerTransport { await this.sendResponse( this.buildBaseResponse({ delta: actualDelta, - moduleData: this.moduleData, isFullReplacement, }) ); @@ -147,7 +141,6 @@ export class HttpWorkerTransport implements WorkerTransport { await this.sendResponse( this.buildBaseResponse({ processedMessageIds: this.processedMessageIds, - moduleData: this.moduleData, }) ); } diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts index d90b66a25..aefec6c46 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -495,7 +495,6 @@ export interface ThreadResponsePayload { errorCode?: string; timestamp: number; originalMessageId?: string; - moduleData?: Record; botResponseId?: string; ephemeral?: boolean; // If true, message should be sent as ephemeral (only visible to user) platformMetadata?: Record; diff --git a/packages/core/src/worker/transport.ts b/packages/core/src/worker/transport.ts index 160776468..0d4b7507d 100644 --- a/packages/core/src/worker/transport.ts +++ b/packages/core/src/worker/transport.ts @@ -19,12 +19,6 @@ export interface WorkerTransport { */ setJobId(jobId: string): void; - /** - * Set module-specific data to be included in responses - * Allows modules to attach metadata to worker responses - */ - setModuleData(moduleData: Record): void; - /** * Send a streaming delta to the gateway * diff --git a/packages/server/src/gateway/routes/public/agent-history.ts b/packages/server/src/gateway/routes/public/agent-history.ts index 29f1e3d12..528c8adbd 100644 --- a/packages/server/src/gateway/routes/public/agent-history.ts +++ b/packages/server/src/gateway/routes/public/agent-history.ts @@ -72,10 +72,10 @@ function isSafeAgentId(id: string): boolean { // ─── Direct session file reader (fallback) ───────────────────────────────── // // `SessionEntry`, `ParsedMessage`, `parseSessionEntries`, and `entryToMessage` -// live in `@lobu/core/utils/session-file` so the worker's -// `/session/messages` route (`packages/agent-worker/src/server.ts`) and -// this gateway-side fallback can't drift again. `findSessionFile` stays -// here because the path-policy differs from the worker's — gateway scans +// are exported from `@lobu/core` so the worker's `/session/messages` +// route (`packages/agent-worker/src/server.ts`) and this gateway-side +// fallback can't drift again. `findSessionFile` stays here because the +// path-policy differs from the worker's — gateway scans // `workspaces/` up to three levels deep with a SAFE_AGENT_ID // guard; the worker scans its own `WORKSPACE_DIR` one level deep.