summaryrefslogtreecommitdiffhomepage
path: root/packages/host-bin
diff options
context:
space:
mode:
authorAdam Malczewski <[email protected]>2026-06-27 01:13:10 +0900
committerAdam Malczewski <[email protected]>2026-06-27 01:13:10 +0900
commitad9d135e583c99a0d93327115defa43187cde1c3 (patch)
treedb704495d3a3d3e24fdadf422976367f2313486d /packages/host-bin
parent98b0638838a8e754927d8c030ce8bded18d63e7d (diff)
downloaddispatch-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.json72
-rw-r--r--packages/host-bin/src/main.ts350
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);
});