diff options
| author | Adam Malczewski <[email protected]> | 2026-06-27 01:13:10 +0900 |
|---|---|---|
| committer | Adam Malczewski <[email protected]> | 2026-06-27 01:13:10 +0900 |
| commit | ad9d135e583c99a0d93327115defa43187cde1c3 (patch) | |
| tree | db704495d3a3d3e24fdadf422976367f2313486d /packages/host-bin | |
| parent | 98b0638838a8e754927d8c030ce8bded18d63e7d (diff) | |
| download | dispatch-ad9d135e583c99a0d93327115defa43187cde1c3.tar.gz dispatch-ad9d135e583c99a0d93327115defa43187cde1c3.zip | |
style: reformat heartbeat merge to 2-space indentation
Diffstat (limited to 'packages/host-bin')
| -rw-r--r-- | packages/host-bin/package.json | 72 | ||||
| -rw-r--r-- | packages/host-bin/src/main.ts | 350 |
2 files changed, 211 insertions, 211 deletions
diff --git a/packages/host-bin/package.json b/packages/host-bin/package.json index f27bf06..65ea305 100644 --- a/packages/host-bin/package.json +++ b/packages/host-bin/package.json @@ -1,38 +1,38 @@ { - "name": "@dispatch/host-bin", - "version": "0.0.0", - "type": "module", - "private": true, - "dependencies": { - "@dispatch/kernel": "workspace:*", - "@dispatch/storage-sqlite": "workspace:*", - "@dispatch/conversation-store": "workspace:*", - "@dispatch/auth-apikey": "workspace:*", - "@dispatch/cache-warming": "workspace:*", - "@dispatch/credential-store": "workspace:*", - "@dispatch/exec-backend": "workspace:*", - "@dispatch/heartbeat": "workspace:*", - "@dispatch/provider-openai-compat": "workspace:*", - "@dispatch/provider-umans": "workspace:*", - "@dispatch/message-queue": "workspace:*", - "@dispatch/mcp": "workspace:*", - "@dispatch/session-orchestrator": "workspace:*", - "@dispatch/skills": "workspace:*", - "@dispatch/ssh": "workspace:*", - "@dispatch/throughput-store": "workspace:*", - "@dispatch/todo": "workspace:*", - "@dispatch/transport-http": "workspace:*", - "@dispatch/tool-read-file": "workspace:*", - "@dispatch/tool-shell": "workspace:*", - "@dispatch/tool-edit-file": "workspace:*", - "@dispatch/tool-write-file": "workspace:*", - "@dispatch/tool-web-search": "workspace:*", - "@dispatch/tool-youtube-transcript": "workspace:*", - "@dispatch/journal-sink": "workspace:*", - "@dispatch/lsp": "workspace:*", - "@dispatch/surface-loaded-extensions": "workspace:*", - "@dispatch/surface-registry": "workspace:*", - "@dispatch/transport-ws": "workspace:*", - "@dispatch/system-prompt": "workspace:*" - } + "name": "@dispatch/host-bin", + "version": "0.0.0", + "type": "module", + "private": true, + "dependencies": { + "@dispatch/kernel": "workspace:*", + "@dispatch/storage-sqlite": "workspace:*", + "@dispatch/conversation-store": "workspace:*", + "@dispatch/auth-apikey": "workspace:*", + "@dispatch/cache-warming": "workspace:*", + "@dispatch/credential-store": "workspace:*", + "@dispatch/exec-backend": "workspace:*", + "@dispatch/heartbeat": "workspace:*", + "@dispatch/provider-openai-compat": "workspace:*", + "@dispatch/provider-umans": "workspace:*", + "@dispatch/message-queue": "workspace:*", + "@dispatch/mcp": "workspace:*", + "@dispatch/session-orchestrator": "workspace:*", + "@dispatch/skills": "workspace:*", + "@dispatch/ssh": "workspace:*", + "@dispatch/throughput-store": "workspace:*", + "@dispatch/todo": "workspace:*", + "@dispatch/transport-http": "workspace:*", + "@dispatch/tool-read-file": "workspace:*", + "@dispatch/tool-shell": "workspace:*", + "@dispatch/tool-edit-file": "workspace:*", + "@dispatch/tool-write-file": "workspace:*", + "@dispatch/tool-web-search": "workspace:*", + "@dispatch/tool-youtube-transcript": "workspace:*", + "@dispatch/journal-sink": "workspace:*", + "@dispatch/lsp": "workspace:*", + "@dispatch/surface-loaded-extensions": "workspace:*", + "@dispatch/surface-registry": "workspace:*", + "@dispatch/transport-ws": "workspace:*", + "@dispatch/system-prompt": "workspace:*" + } } diff --git a/packages/host-bin/src/main.ts b/packages/host-bin/src/main.ts index 9919432..8633052 100644 --- a/packages/host-bin/src/main.ts +++ b/packages/host-bin/src/main.ts @@ -8,18 +8,18 @@ import { createExecBackendExtension } from "@dispatch/exec-backend"; import { extension as heartbeatExt } from "@dispatch/heartbeat"; import { createJournalSink } from "@dispatch/journal-sink"; import { - type ConfigAccess, - createBus, - createHost, - createLogger, - type EventsEmitter, - type Extension, - type HostDeps, - type LogDeps, - type PermissionGate, - type ScheduledJob, - type SecretsAccess, - type StorageNamespace, + type ConfigAccess, + createBus, + createHost, + createLogger, + type EventsEmitter, + type Extension, + type HostDeps, + type LogDeps, + type PermissionGate, + type ScheduledJob, + type SecretsAccess, + type StorageNamespace, } from "@dispatch/kernel"; import { extension as lspExt } from "@dispatch/lsp"; import { extension as mcpExt } from "@dispatch/mcp"; @@ -49,192 +49,192 @@ import { configMapToAccess, envToConfigMap } from "./config.js"; import { loadExternalExtensions } from "./load-external.js"; function createEmptySecrets(): SecretsAccess { - return { - get: async () => null, - set: async () => {}, - delete: async () => {}, - }; + return { + get: async () => null, + set: async () => {}, + delete: async () => {}, + }; } function createAllowAllPermissions(): PermissionGate { - return { - check: async () => ({ allowed: true }), - }; + return { + check: async () => ({ allowed: true }), + }; } function createNoopScheduler(): { readonly register: (job: ScheduledJob) => void } { - return { register: () => {} }; + return { register: () => {} }; } function createNoopEvents(): EventsEmitter { - return { emit: () => {} }; + return { emit: () => {} }; } // Core extensions EXCEPT the credential-store, which is assembled in boot() so // its credential list can include any credentials backed by external providers // (e.g. a `claude` credential once the external Anthropic provider is loaded). const CORE_EXTENSIONS: readonly Extension[] = [ - storageSqliteExt, - conversationStoreExt, - authApikeyExt, - providerOpenaiCompatExt, - providerUmansExt, - // exec-backend must precede the tool extensions that - // `dependsOn: ["exec-backend"]` (tool-edit-file/read/shell/write). It - // provides the ExecBackendResolver the tools resolve through; placing it - // here keeps the activation DAG honest (it depends only on kernel). - createExecBackendExtension(), - toolEditFileExt, - toolReadFileExt, - toolShellExt, - toolWriteFileExt, - toolWebSearchExt, - toolYoutubeTranscriptExt, - throughputStoreExt, - todoExt, - messageQueueExt, - mcpExt, - sessionOrchestratorExt, - skillsExt, - systemPromptExt, - cacheWarmingExt, - lspExt, - // ssh declares `dependsOn: ["exec-backend"]` and PROVIDES the remote - // exec-backend factory + the ComputerService the HTTP routes delegate to. - // Its lookups are lazy (tool-/request-time), but it is placed after - // exec-backend and the tool extensions (alongside the other standard - // tool-serving extensions) to keep the DAG honest — and before - // transport-http, whose routes consume the ComputerService it provides. - sshExt, - // heartbeat PROVIDES the HeartbeatService (per-workspace AI loop) the - // HTTP routes delegate to. Placed before transport-http (which depends on - // it) — mirrors how ssh precedes transport-http for the same reason. - heartbeatExt, - createTransportHttpExtension(), - // Surface extensions — dependency order: surface-registry first, then consumers. - createSurfaceRegistryExtension(), - createTransportWsExtension(), - createLoadedExtensionsExtension(), + storageSqliteExt, + conversationStoreExt, + authApikeyExt, + providerOpenaiCompatExt, + providerUmansExt, + // exec-backend must precede the tool extensions that + // `dependsOn: ["exec-backend"]` (tool-edit-file/read/shell/write). It + // provides the ExecBackendResolver the tools resolve through; placing it + // here keeps the activation DAG honest (it depends only on kernel). + createExecBackendExtension(), + toolEditFileExt, + toolReadFileExt, + toolShellExt, + toolWriteFileExt, + toolWebSearchExt, + toolYoutubeTranscriptExt, + throughputStoreExt, + todoExt, + messageQueueExt, + mcpExt, + sessionOrchestratorExt, + skillsExt, + systemPromptExt, + cacheWarmingExt, + lspExt, + // ssh declares `dependsOn: ["exec-backend"]` and PROVIDES the remote + // exec-backend factory + the ComputerService the HTTP routes delegate to. + // Its lookups are lazy (tool-/request-time), but it is placed after + // exec-backend and the tool extensions (alongside the other standard + // tool-serving extensions) to keep the DAG honest — and before + // transport-http, whose routes consume the ComputerService it provides. + sshExt, + // heartbeat PROVIDES the HeartbeatService (per-workspace AI loop) the + // HTTP routes delegate to. Placed before transport-http (which depends on + // it) — mirrors how ssh precedes transport-http for the same reason. + heartbeatExt, + createTransportHttpExtension(), + // Surface extensions — dependency order: surface-registry first, then consumers. + createSurfaceRegistryExtension(), + createTransportWsExtension(), + createLoadedExtensionsExtension(), ]; /** Parse the comma-separated list of external extension module specifiers. */ function parseExternalSpecifiers(env: Readonly<Record<string, string | undefined>>): string[] { - return (env.DISPATCH_EXTERNAL_EXTENSIONS ?? "") - .split(",") - .map((s) => s.trim()) - .filter((s) => s.length > 0); + return (env.DISPATCH_EXTERNAL_EXTENSIONS ?? "") + .split(",") + .map((s) => s.trim()) + .filter((s) => s.length > 0); } async function boot(): Promise<void> { - const journalPath = process.env.DISPATCH_JOURNAL ?? "./.dispatch/journal/app.ndjson"; - mkdirSync(dirname(journalPath), { recursive: true }); - const logSink = createJournalSink({ path: journalPath }); - const logDeps: LogDeps = { now: () => Date.now(), newId: () => crypto.randomUUID() }; - const logger = createLogger({ extensionId: "host-bin" }, logSink, logDeps); - - const traceDbPath = process.env.DISPATCH_TRACE_DB ?? "./.dispatch-data/traces.db"; - - // Only start the collector supervisor in dev mode (source files available). - // Compiled binaries don't have the source tree, so the collector can't spawn. - let supervisor: ReturnType<typeof createCollectorSupervisor> | undefined; - if (existsSync("packages/observability-collector/src/main.ts")) { - supervisor = createCollectorSupervisor({ - spawn: (cmd: string[]) => { - const proc = Bun.spawn(cmd, { stdout: "inherit", stderr: "inherit" }); - const handle: ChildHandle = { - kill: (signal?: string) => proc.kill(signal as NodeJS.Signals), - exited: proc.exited, - }; - return handle; - }, - journalPath, - dbPath: traceDbPath, - logger: logger.child({ extensionId: "collector-supervisor" }), - }); - supervisor.start(); - } - - const dbPath = process.env.DISPATCH_DB ?? "./.dispatch-data/dispatch.db"; - mkdirSync(dirname(dbPath), { recursive: true }); - const sqliteBackend = createSqliteStorage({ path: dbPath }); - const storageFactory = (namespace: string): StorageNamespace => sqliteBackend.storage(namespace); - - const configMap = envToConfigMap(process.env as Readonly<Record<string, string | undefined>>); - const config: ConfigAccess = configMapToAccess(configMap); - - const deps: HostDeps = { - logger, - config, - storageFactory, - secrets: createEmptySecrets(), - permissions: createAllowAllPermissions(), - scheduler: createNoopScheduler(), - bus: createBus(logger), - events: createNoopEvents(), - logSink, - logDeps, - }; - - // Load external (out-of-repo) extensions declared via DISPATCH_EXTERNAL_EXTENSIONS. - const externalSpecifiers = parseExternalSpecifiers( - process.env as Readonly<Record<string, string | undefined>>, - ); - const externalExtensions = await loadExternalExtensions(externalSpecifiers, logger); - - // Assemble the credential list. MVP keeps the hardcoded `opencode` credential - // and adds a `claude` credential when an external Anthropic provider is loaded. - const credentials = [{ name: "opencode", providerId: "openai-compat" }]; - - // The umans credential is always listed (it's the model-catalog index); the - // provider itself only registers when UMANS_API_KEY is set, so listCatalog - // gracefully skips it when the provider is absent. - if (process.env.UMANS_API_KEY) { - credentials.push({ name: "umans", providerId: "umans" }); - logger.info(`Registered credential "umans" → umans provider`); - } - const hasAnthropic = externalExtensions.some((e) => - e.manifest.contributes?.providers?.includes("anthropic"), - ); - if (hasAnthropic) { - const claudeName = process.env.DISPATCH_CLAUDE_CREDENTIAL ?? "claude"; - credentials.push({ name: claudeName, providerId: "anthropic" }); - logger.info(`Registered credential "${claudeName}" → anthropic provider`); - } - - const extensions: Extension[] = [ - ...CORE_EXTENSIONS, - createCredentialStoreExtension({ credentials }), - ...externalExtensions, - ]; - - const host = createHost(extensions, deps); - await host.activate(); - - const disabled = host.getDisabled(); - if (disabled.length > 0) { - for (const d of disabled) { - logger.warn(`Extension "${d.manifest.id}" disabled: ${d.reason}`); - } - } - - let shuttingDown = false; - const shutdown = async () => { - if (shuttingDown) return; - shuttingDown = true; - logger.info("Shutting down — deactivating extensions"); - await host.deactivate(); - logger.info("Draining collector"); - await supervisor?.stop(); - process.exit(0); - }; - process.on("SIGINT", shutdown); - process.on("SIGTERM", shutdown); - - logger.info("Dispatch booted"); - console.info("Dispatch booted"); + const journalPath = process.env.DISPATCH_JOURNAL ?? "./.dispatch/journal/app.ndjson"; + mkdirSync(dirname(journalPath), { recursive: true }); + const logSink = createJournalSink({ path: journalPath }); + const logDeps: LogDeps = { now: () => Date.now(), newId: () => crypto.randomUUID() }; + const logger = createLogger({ extensionId: "host-bin" }, logSink, logDeps); + + const traceDbPath = process.env.DISPATCH_TRACE_DB ?? "./.dispatch-data/traces.db"; + + // Only start the collector supervisor in dev mode (source files available). + // Compiled binaries don't have the source tree, so the collector can't spawn. + let supervisor: ReturnType<typeof createCollectorSupervisor> | undefined; + if (existsSync("packages/observability-collector/src/main.ts")) { + supervisor = createCollectorSupervisor({ + spawn: (cmd: string[]) => { + const proc = Bun.spawn(cmd, { stdout: "inherit", stderr: "inherit" }); + const handle: ChildHandle = { + kill: (signal?: string) => proc.kill(signal as NodeJS.Signals), + exited: proc.exited, + }; + return handle; + }, + journalPath, + dbPath: traceDbPath, + logger: logger.child({ extensionId: "collector-supervisor" }), + }); + supervisor.start(); + } + + const dbPath = process.env.DISPATCH_DB ?? "./.dispatch-data/dispatch.db"; + mkdirSync(dirname(dbPath), { recursive: true }); + const sqliteBackend = createSqliteStorage({ path: dbPath }); + const storageFactory = (namespace: string): StorageNamespace => sqliteBackend.storage(namespace); + + const configMap = envToConfigMap(process.env as Readonly<Record<string, string | undefined>>); + const config: ConfigAccess = configMapToAccess(configMap); + + const deps: HostDeps = { + logger, + config, + storageFactory, + secrets: createEmptySecrets(), + permissions: createAllowAllPermissions(), + scheduler: createNoopScheduler(), + bus: createBus(logger), + events: createNoopEvents(), + logSink, + logDeps, + }; + + // Load external (out-of-repo) extensions declared via DISPATCH_EXTERNAL_EXTENSIONS. + const externalSpecifiers = parseExternalSpecifiers( + process.env as Readonly<Record<string, string | undefined>>, + ); + const externalExtensions = await loadExternalExtensions(externalSpecifiers, logger); + + // Assemble the credential list. MVP keeps the hardcoded `opencode` credential + // and adds a `claude` credential when an external Anthropic provider is loaded. + const credentials = [{ name: "opencode", providerId: "openai-compat" }]; + + // The umans credential is always listed (it's the model-catalog index); the + // provider itself only registers when UMANS_API_KEY is set, so listCatalog + // gracefully skips it when the provider is absent. + if (process.env.UMANS_API_KEY) { + credentials.push({ name: "umans", providerId: "umans" }); + logger.info(`Registered credential "umans" → umans provider`); + } + const hasAnthropic = externalExtensions.some((e) => + e.manifest.contributes?.providers?.includes("anthropic"), + ); + if (hasAnthropic) { + const claudeName = process.env.DISPATCH_CLAUDE_CREDENTIAL ?? "claude"; + credentials.push({ name: claudeName, providerId: "anthropic" }); + logger.info(`Registered credential "${claudeName}" → anthropic provider`); + } + + const extensions: Extension[] = [ + ...CORE_EXTENSIONS, + createCredentialStoreExtension({ credentials }), + ...externalExtensions, + ]; + + const host = createHost(extensions, deps); + await host.activate(); + + const disabled = host.getDisabled(); + if (disabled.length > 0) { + for (const d of disabled) { + logger.warn(`Extension "${d.manifest.id}" disabled: ${d.reason}`); + } + } + + let shuttingDown = false; + const shutdown = async () => { + if (shuttingDown) return; + shuttingDown = true; + logger.info("Shutting down — deactivating extensions"); + await host.deactivate(); + logger.info("Draining collector"); + await supervisor?.stop(); + process.exit(0); + }; + process.on("SIGINT", shutdown); + process.on("SIGTERM", shutdown); + + logger.info("Dispatch booted"); + console.info("Dispatch booted"); } boot().catch((err) => { - console.error("Fatal boot error:", err); - process.exit(1); + console.error("Fatal boot error:", err); + process.exit(1); }); |
