@prismer/runtime 1.9.1 → 1.9.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/CHANGELOG.md +178 -0
  2. package/README.md +363 -229
  3. package/dist/cli.cjs +10269 -0
  4. package/dist/cli.cjs.map +1 -0
  5. package/dist/cli.js +10257 -0
  6. package/dist/cli.js.map +1 -0
  7. package/dist/index.cjs +10603 -0
  8. package/dist/index.cjs.map +1 -0
  9. package/dist/index.d.cts +1643 -0
  10. package/dist/index.d.ts +1496 -1916
  11. package/dist/index.js +9702 -9177
  12. package/dist/index.js.map +1 -1
  13. package/package.json +31 -33
  14. package/dist/adapter-registry-EMZFKFYK.mjs +0 -8
  15. package/dist/adapter-registry-EMZFKFYK.mjs.map +0 -1
  16. package/dist/artifacts-uploader-N2HNBSTW.mjs +0 -159
  17. package/dist/artifacts-uploader-N2HNBSTW.mjs.map +0 -1
  18. package/dist/auto-register-M4YSPJS6.mjs +0 -204
  19. package/dist/auto-register-M4YSPJS6.mjs.map +0 -1
  20. package/dist/bin/prismer.js +0 -14988
  21. package/dist/chunk-BJTO5JO5.mjs +0 -11
  22. package/dist/chunk-BJTO5JO5.mjs.map +0 -1
  23. package/dist/chunk-JIEDFDVI.mjs +0 -148
  24. package/dist/chunk-JIEDFDVI.mjs.map +0 -1
  25. package/dist/chunk-NDNX2G6O.mjs +0 -68
  26. package/dist/chunk-NDNX2G6O.mjs.map +0 -1
  27. package/dist/chunk-POWV475F.mjs +0 -45
  28. package/dist/chunk-POWV475F.mjs.map +0 -1
  29. package/dist/chunk-VTFKZAUY.mjs +0 -68
  30. package/dist/chunk-VTFKZAUY.mjs.map +0 -1
  31. package/dist/dispatch-mux-MW4HHICS.mjs +0 -8
  32. package/dist/dispatch-mux-MW4HHICS.mjs.map +0 -1
  33. package/dist/dispatch-rpc-J7H6KC2V.mjs +0 -78
  34. package/dist/dispatch-rpc-J7H6KC2V.mjs.map +0 -1
  35. package/dist/fs-rpc-OSHJZYK6.mjs +0 -58
  36. package/dist/fs-rpc-OSHJZYK6.mjs.map +0 -1
  37. package/dist/heartbeat-loop-H2LAI3V5.mjs +0 -95
  38. package/dist/heartbeat-loop-H2LAI3V5.mjs.map +0 -1
  39. package/dist/index.d.mts +0 -2063
  40. package/dist/index.mjs +0 -8807
  41. package/dist/index.mjs.map +0 -1
  42. package/dist/mode-b-DXJ7FJAL.mjs +0 -152
  43. package/dist/mode-b-DXJ7FJAL.mjs.map +0 -1
  44. package/dist/registry-ZYU2HDFL.mjs +0 -12
  45. package/dist/registry-ZYU2HDFL.mjs.map +0 -1
  46. /package/{dist → assets}/icon +0 -0
  47. /package/{dist → assets}/smallicon +0 -0
package/dist/index.d.ts CHANGED
@@ -1,2063 +1,1643 @@
1
- import * as http from 'node:http';
2
- import { FsContext } from '@prismer/sandbox-runtime';
3
- import { ParaEvent } from '@prismer/wire';
1
+ import { z } from 'zod';
4
2
  import { EventEmitter } from 'node:events';
3
+ import Database from 'better-sqlite3';
4
+ import { IncomingMessage, ServerResponse } from 'node:http';
5
+ import { Command } from 'commander';
5
6
 
6
- type DaemonState = 'starting' | 'running' | 'shutting_down' | 'stopped';
7
- interface DaemonOptions {
8
- pidFile?: string;
9
- dataDir?: string;
10
- logFile?: string;
11
- installSignalHandlers?: boolean;
12
- }
13
- interface ShutdownHandler {
14
- name: string;
15
- handler: () => Promise<void> | void;
16
- }
17
- declare class DaemonAlreadyRunningError extends Error {
18
- readonly existingPid: number;
19
- readonly pidFile: string;
20
- constructor(pid: number, pidFile: string);
21
- }
22
- declare class DaemonProcess {
23
- private _state;
24
- private readonly _pidFile;
25
- private readonly _dataDir;
26
- private readonly _logFile;
27
- private readonly _installSignalHandlers;
28
- private readonly _shutdownHandlers;
29
- private readonly _signalHistory;
30
- private _shutdownInProgress;
31
- private _sigTermHandler;
32
- private _sigIntHandler;
33
- private _sigHupHandler;
34
- private _signalReceived;
35
- constructor(opts?: DaemonOptions);
36
- get state(): DaemonState;
37
- get pid(): number;
38
- get pidFile(): string;
39
- get dataDir(): string;
40
- get signalHistory(): ReadonlyArray<{
41
- signal: string;
42
- ts: number;
43
- }>;
44
- start(): Promise<void>;
45
- onShutdown(h: ShutdownHandler): void;
46
- shutdown(signal?: NodeJS.Signals | 'manual'): Promise<void>;
47
- reload(): Promise<void>;
48
- static isRunning(pidFile: string): boolean;
49
- static cleanupStalePidFile(pidFile: string): boolean;
50
- private _atomicPidWrite;
51
- private _recordSignal;
52
- private _readPidFromFile;
53
- }
54
-
55
- interface EventBusEnvelope<T = unknown> {
56
- topic: string;
57
- ts: number;
58
- payload: T;
59
- source?: string;
60
- requestId?: string;
61
- }
7
+ type AdapterKind = 'long-running' | 'interactive';
62
8
  /**
63
- * Unified subscription handler that accepts EventBusEnvelope.
64
- * This matches the EventBus API expectation.
9
+ * Adapter definition: how the daemon hosts one class of agent
10
+ * (Hermes, Claude Code, OpenClaw, ...).
11
+ *
12
+ * One Adapter ←(1:N)─ Agent ←(1:N)─ AgentProfile
13
+ *
14
+ * Long-running adapters implement {@link AdapterDef.ensureService} and
15
+ * return a reusable {@link AdapterService} (e.g. HTTP client to
16
+ * `hermes gateway`). Interactive adapters implement
17
+ * {@link AdapterDef.dispatch} and spawn a fresh subprocess per task
18
+ * (e.g. `claude --headless`).
65
19
  */
66
- type SubscriptionHandler<T = unknown> = (ev: EventBusEnvelope<T>) => void | Promise<void>;
67
- interface Subscription {
68
- id: string;
69
- pattern: string;
70
- unsubscribe(): void;
71
- }
72
- interface EventBusOptions {
73
- onSubscriberError?: (err: unknown, sub: Subscription, ev: EventBusEnvelope) => void;
74
- queueWarnThreshold?: number;
75
- }
76
- declare class EventBus {
77
- private readonly _subscribers;
78
- private _monotonicTs;
79
- private _totalPublished;
80
- private readonly _onError;
81
- private readonly _warnThreshold;
82
- constructor(opts?: EventBusOptions);
83
- publish<T>(topic: string, payload: T, meta?: {
84
- source?: string;
85
- requestId?: string;
86
- }): void;
87
- subscribe<T>(topic: string, handler: SubscriptionHandler<T>): Subscription;
88
- unsubscribeAll(): void;
89
- get subscriberCount(): number;
90
- get totalPublished(): number;
91
- private _drain;
92
- }
93
-
94
- type AgentState = 'registered' | 'spawning' | 'running' | 'degraded' | 'crashed' | 'backoff' | 'stopping' | 'stopped' | 'failed';
95
- interface AgentDescriptor {
96
- id: string;
20
+ interface AdapterDef {
21
+ /** Unique name, e.g. 'hermes', 'claude-code'. Matches `IMAgentCard.adapterName`. */
97
22
  name: string;
98
- command: string;
99
- args?: string[];
100
- cwd?: string;
101
- env?: NodeJS.ProcessEnv;
102
- healthCheck?: () => Promise<boolean> | boolean;
103
- backoff?: {
104
- initialMs?: number;
105
- multiplier?: number;
106
- maxMs?: number;
107
- maxRestarts?: number;
108
- resetAfterMs?: number;
109
- };
110
- attachPid?: number;
23
+ /** Long-running services expose ensureService; interactive adapters expose dispatch. */
24
+ kind: AdapterKind;
25
+ /** Capability tags surfaced to mobile (e.g. ['shell', 'code', 'mcp']). */
26
+ capabilities: string[];
27
+ /** Zod schema validating `AgentProfile.config` for this adapter. */
28
+ workspaceSchema: z.ZodSchema;
29
+ /** Long-running: ensure the underlying service is reachable. May spawn or just connect. */
30
+ ensureService?(profile: AgentProfile): Promise<AdapterService>;
31
+ /** Interactive: one-shot dispatch (spawn-per-task). */
32
+ dispatch?(profile: AgentProfile, task: TaskInput): Promise<TaskResult>;
33
+ /** Validate config before persisting `AgentProfile.config`. */
34
+ validate(config: unknown): ValidationResult;
35
+ /** Health probe — binary in PATH, HTTP endpoint reachable, etc. */
36
+ health(): Promise<HealthStatus>;
111
37
  }
112
- interface AgentStatus {
113
- id: string;
114
- state: AgentState;
115
- pid?: number;
116
- startedAt?: number;
117
- restarts: number;
118
- lastExitCode?: number;
119
- lastSignal?: NodeJS.Signals | null;
120
- lastHealthError?: string;
121
- nextRestartAt?: number;
122
- }
123
- interface SupervisorOptions {
124
- eventBus: EventBus;
125
- healthProbeIntervalMs?: number;
126
- degradedThreshold?: number;
127
- stopTimeoutMs?: number;
128
- }
129
- declare class AgentSupervisor {
130
- private readonly _bus;
131
- private readonly _probeIntervalMs;
132
- private readonly _degradedThreshold;
133
- private readonly _stopTimeoutMs;
134
- private readonly _agents;
135
- constructor(opts: SupervisorOptions);
136
- register(descriptor: AgentDescriptor): void;
137
- unregister(id: string): Promise<void>;
138
- spawn(id: string): Promise<void>;
139
- stop(id: string, reason?: string): Promise<void>;
140
- restart(id: string): Promise<void>;
141
- attach(id: string, pid: number): Promise<void>;
142
- get(id: string): AgentStatus | undefined;
143
- list(): AgentStatus[];
144
- shutdown(): Promise<void>;
145
- private _doSpawn;
146
- private _handleCrash;
147
- private _scheduleRestart;
148
- private _killOwned;
149
- private _cancelBackoff;
150
- private _startProbe;
151
- private _stopProbe;
152
- private _runProbe;
153
- private _pollAttached;
154
- private _require;
155
- private _publish;
156
- }
157
-
158
38
  /**
159
- * Prismer Runtime Adapter Registry (Sprint A3, D4 dispatch mux).
39
+ * AgentProfile mirrors the cloud `im_agent_profiles` row.
160
40
  *
161
- * One process-wide registry maps adapter `name` `AdapterImpl`. The
162
- * registry is the data store; selection policy lives in DispatchMux
163
- * (dispatch-mux.ts) so the same registry can drive multiple selectors
164
- * (capability-based, name-based, weighted load, etc.) without coupling.
41
+ * Field names match Track A's `IMAgentProfile` Prisma model + `AgentProfileDTO`
42
+ * in `src/im/api/agent-profiles.ts` (m1, merged in d5186ca / a4b3d89).
165
43
  *
166
- * An adapter advertises:
167
- * - `name` : stable identifier (matches catalog name).
168
- * - `tiersSupported` : PARA Tier numbers it can host (e.g. [1..7]).
169
- * - `capabilityTags` : tags from the task's `requiresCapability`
170
- * vocabulary that this adapter can satisfy.
171
- *
172
- * On register conflict, replace — adapter modules are expected to be
173
- * the only source of truth for their own descriptor.
174
- *
175
- * Adapters typically run as out-of-process workers (Claude Code CLI,
176
- * OpenClaw daemon plug-in, Hermes Python). The `dispatch` boundary
177
- * here is the *runtime side* of that bridge — it returns a Promise
178
- * whose resolution corresponds to the adapter's reported completion.
44
+ * Daemon-local SQLite mirror columns use snake_case
45
+ * (workspace_id / agent_im_user_id) per docs/refactor/04-daemon-runtime.md
46
+ * §SQLite schema; the TS types stay camelCase across the boundary.
179
47
  */
180
- interface AdapterDescriptor {
181
- /** Stable identifier — must match the catalog entry. */
48
+ interface AgentProfile {
49
+ id: string;
50
+ workspaceId: string;
51
+ agentImUserId: string;
52
+ adapterName: string;
182
53
  name: string;
183
- /** PARA Tier numbers (L1–L10). */
184
- tiersSupported: number[];
185
- /** Capability tag vocabulary (e.g. ["code.write", "code.review"]). */
186
- capabilityTags: string[];
187
- /** Free-form for telemetry — version, build, etc. */
188
- metadata?: Record<string, unknown>;
54
+ config: Record<string, unknown>;
55
+ version: number;
56
+ createdAt: Date;
57
+ updatedAt: Date;
58
+ /**
59
+ * IMUser.username of the agent bound to this profile. Used by long-running
60
+ * adapters (e.g. Hermes) to derive a stable, human-readable per-agent
61
+ * profile directory and to scope MCP env so per-call agent identity is not
62
+ * lost when multiple agents share a daemon. Parallel task F1 populates this
63
+ * on the cloud DTO; runtime treats it as optional to stay backwards-compat
64
+ * with snapshots produced before F1 lands.
65
+ */
66
+ agentUsername?: string;
189
67
  }
190
- interface AdapterDispatchInput {
191
- /** Cloud task ID (im_tasks.id). */
68
+ interface TaskInput {
192
69
  taskId: string;
193
- /** Step index when the task is multi-step (route step). */
194
- stepIdx?: number;
195
- /** Capability the cloud asked for — drives adapter selection. */
196
- capability: string;
197
- /** User-facing prompt / instruction. */
198
70
  prompt: string;
199
- /** Free-form metadata passed through to the adapter. */
200
71
  metadata?: Record<string, unknown>;
201
- /** Optional deadline (ms epoch). Adapters should respect it best-effort. */
202
- deadlineAt?: number;
203
- }
204
- interface AdapterDispatchResult {
72
+ timeoutMs?: number;
73
+ onProgress?: (p: {
74
+ progress: number;
75
+ message?: string;
76
+ detail?: Record<string, unknown>;
77
+ }) => void;
78
+ signal?: AbortSignal;
79
+ }
80
+ interface TaskResult {
205
81
  ok: boolean;
206
- /** Output text (stdout, summary, etc.). */
207
82
  output?: string;
208
- /** Files produced by the adapter (paths are relative to the agent
209
- * workspace; cloud uploads them via the artifact stream). */
83
+ error?: {
84
+ code: string;
85
+ message: string;
86
+ };
210
87
  artifacts?: Array<{
211
- path: string;
212
- bytes: number;
88
+ kind: string;
89
+ storageUri: string;
213
90
  mime?: string;
91
+ size?: number;
214
92
  }>;
215
- /** Failure reason — must be set when ok=false. */
216
- error?: string;
217
- /** Free-form telemetry — token counts, latency, model, etc. */
93
+ metrics?: {
94
+ tokensUsed?: number;
95
+ durationMs?: number;
96
+ };
97
+ /** Adapter-private metadata used by daemon-side bridges/observability. */
218
98
  metadata?: Record<string, unknown>;
219
99
  }
220
- interface AdapterImpl extends AdapterDescriptor {
221
- /** Dispatch a task step to this adapter. Should not throw — errors
222
- * belong in the result. */
223
- dispatch(input: AdapterDispatchInput): Promise<AdapterDispatchResult>;
224
- /** Optional: per-adapter health probe. Defaults to "healthy". */
225
- health?(): Promise<{
226
- healthy: boolean;
227
- reason?: string;
228
- }>;
229
- /**
230
- * Reset adapter state for a specific agent (or all agents if undefined).
231
- *
232
- * v1.9.x remote-command `agent_restart` semantic: abort/clear whatever
233
- * per-agent context the adapter holds. For stateless adapters (CLI shim),
234
- * this is a no-op; for Mode B adapters, it's typically a POST /reset to
235
- * the loopback so the adapter host clears its own session state.
236
- *
237
- * NOT a process restart — the adapter is not expected to own a PID.
238
- *
239
- * @param agentName Optional agent name to scope the reset. Undefined = reset all.
240
- * @returns Arbitrary result; ok:true for success, ok:false+reason otherwise.
241
- */
242
- reset?(agentName?: string): Promise<{
243
- ok: boolean;
244
- state?: string;
245
- reason?: string;
246
- [k: string]: unknown;
247
- }>;
100
+ /**
101
+ * Reusable handle to a long-running adapter's underlying service.
102
+ * For Hermes this wraps an HTTP client to `hermes gateway`; for OpenClaw
103
+ * (1.9.5+) it wraps an in-process TS worker.
104
+ */
105
+ interface AdapterService {
106
+ /** Service-specific identifier (e.g. 'http://127.0.0.1:8642' for hermes). */
107
+ id: string;
108
+ dispatch(task: TaskInput): Promise<TaskResult>;
109
+ healthy(): Promise<boolean>;
110
+ /** Optional: stop the service. May be no-op if user owns lifecycle. */
111
+ shutdown?(): Promise<void>;
112
+ /** Optional: subscribe to crash events. */
113
+ on?(event: 'crash', cb: (err: Error) => void): void;
114
+ }
115
+ interface ValidationResult {
116
+ ok: boolean;
117
+ errors?: string[];
248
118
  }
119
+ interface HealthStatus {
120
+ available: boolean;
121
+ reason?: string;
122
+ hint?: string;
123
+ }
124
+
125
+ /**
126
+ * Process-wide adapter registry.
127
+ *
128
+ * Maps `adapter.name` → {@link AdapterDef}. Selection policy lives in
129
+ * `daemon/dispatch.ts`; the registry is just a typed Map.
130
+ *
131
+ * Shell cherry-picked (conceptually, not via cp) from
132
+ * `feat/release190-milestone:sdk/prismer-cloud/runtime/src/adapter-registry.ts`.
133
+ * The registered type was swapped `AdapterImpl` → `AdapterDef` per the
134
+ * 1.9.x adapter contract; `tiersSupported` (PARA tier) is dropped — 1.9.x
135
+ * uses capability tags only.
136
+ */
249
137
  declare class AdapterRegistry {
250
138
  private readonly adapters;
251
- register(adapter: AdapterImpl): void;
139
+ register(adapter: AdapterDef): void;
252
140
  unregister(name: string): boolean;
253
141
  has(name: string): boolean;
254
- get(name: string): AdapterImpl | undefined;
255
- list(): AdapterDescriptor[];
142
+ get(name: string): AdapterDef | undefined;
143
+ list(): AdapterDef[];
256
144
  size(): number;
257
145
  /**
258
- * Find adapters that can satisfy the given capability tag.
146
+ * Find adapters that satisfy a capability tag.
259
147
  *
260
- * An adapter matches if `capabilityTags` includes the tag verbatim or
261
- * if the adapter declared a wildcard prefix match (e.g. `code.*` matches
262
- * `code.write`). Returns deterministic ordering adapters are sorted by
263
- * name so callers see the same result for the same registry state.
264
- */
265
- findByCapability(capability: string): AdapterImpl[];
266
- /**
267
- * Find adapters that can host a given PARA tier.
148
+ * Match rules (deterministic, sorted by name):
149
+ * - exact: adapter declared the tag verbatim
150
+ * - wildcard: adapter declared `code.*` matches `code.write`
268
151
  */
269
- findByTier(tier: number): AdapterImpl[];
152
+ findByCapability(capability: string): AdapterDef[];
270
153
  }
271
154
 
155
+ declare const HermesProfileConfigSchema: z.ZodObject<{
156
+ /** Hermes profile name (default: AgentProfile.id slice). */
157
+ hermesProfileName: z.ZodOptional<z.ZodString>;
158
+ /** Hermes API server port. Distinct profiles use distinct ports to avoid clashes. */
159
+ port: z.ZodDefault<z.ZodNumber>;
160
+ /** Bearer token from `~/.hermes/.env API_SERVER_KEY`. */
161
+ apiKey: z.ZodString;
162
+ /** Auto-spawn `hermes -p <name> gateway run` if not reachable. Default false. */
163
+ autoStart: z.ZodDefault<z.ZodBoolean>;
164
+ /** Wait timeout when autoStart=true. */
165
+ startupTimeoutMs: z.ZodDefault<z.ZodNumber>;
166
+ /**
167
+ * Configure Hermes' inference provider to use Prismer Cloud's existing
168
+ * OpenAI-compatible provider endpoint before starting the gateway.
169
+ */
170
+ configurePrismerProvider: z.ZodDefault<z.ZodBoolean>;
171
+ /**
172
+ * Auto-install the @prismer/mcp-server stdio MCP server into the Hermes
173
+ * profile so the long-running role agent can drive Workspace tasks
174
+ * (create / update / approve / move) through tool calls. Default true.
175
+ */
176
+ installPrismerMcpServer: z.ZodDefault<z.ZodBoolean>;
177
+ /**
178
+ * Override the absolute path to @prismer/mcp-server's `dist/index.js`.
179
+ * If unset the adapter resolves it via Node's module resolver from the
180
+ * runtime install location, then falls back to PRISMER_MCP_SERVER env.
181
+ */
182
+ prismerMcpServerPath: z.ZodOptional<z.ZodString>;
183
+ /** Model sent to Prismer's /api/v1/chat/completions endpoint. */
184
+ model: z.ZodDefault<z.ZodString>;
185
+ /** Named custom provider written into Hermes config.yaml. */
186
+ prismerProviderName: z.ZodDefault<z.ZodString>;
187
+ /** Override cloud provider base. Defaults to PRISMER_BASE_URL + /api/v1. */
188
+ prismerProviderBaseUrl: z.ZodOptional<z.ZodString>;
189
+ /** Env key Hermes reads from profile .env for the Prismer API key. */
190
+ prismerApiKeyEnv: z.ZodDefault<z.ZodString>;
191
+ /**
192
+ * Mirror Prismer work_item projections into Hermes native Kanban as triage
193
+ * cards. Triage avoids duplicate execution; Prismer's agent_run remains the
194
+ * executable source of truth.
195
+ */
196
+ mirrorNativeKanban: z.ZodDefault<z.ZodBoolean>;
197
+ /**
198
+ * Mirror Prismer standing-objective IMTask projections into Hermes' native
199
+ * per-session GoalManager state (`state.db.state_meta["goal:<session_id>"]`).
200
+ * Hermes has no public goals REST/CLI surface; this writes the documented
201
+ * native state key directly and records the exact bridge result on the task.
202
+ */
203
+ mirrorNativeGoals: z.ZodDefault<z.ZodBoolean>;
204
+ /**
205
+ * Optional checkout path for Hermes Agent source. Used only as a fallback
206
+ * when the installed `hermes` binary is older than the native Kanban CLI
207
+ * surface but the local source tree contains hermes_cli/kanban_db.py.
208
+ */
209
+ hermesSourceDir: z.ZodOptional<z.ZodString>;
210
+ nativeMirrorTimeoutMs: z.ZodDefault<z.ZodNumber>;
211
+ }, "strip", z.ZodTypeAny, {
212
+ port: number;
213
+ apiKey: string;
214
+ autoStart: boolean;
215
+ startupTimeoutMs: number;
216
+ configurePrismerProvider: boolean;
217
+ installPrismerMcpServer: boolean;
218
+ model: string;
219
+ prismerProviderName: string;
220
+ prismerApiKeyEnv: string;
221
+ mirrorNativeKanban: boolean;
222
+ mirrorNativeGoals: boolean;
223
+ nativeMirrorTimeoutMs: number;
224
+ hermesProfileName?: string | undefined;
225
+ prismerMcpServerPath?: string | undefined;
226
+ prismerProviderBaseUrl?: string | undefined;
227
+ hermesSourceDir?: string | undefined;
228
+ }, {
229
+ apiKey: string;
230
+ hermesProfileName?: string | undefined;
231
+ port?: number | undefined;
232
+ autoStart?: boolean | undefined;
233
+ startupTimeoutMs?: number | undefined;
234
+ configurePrismerProvider?: boolean | undefined;
235
+ installPrismerMcpServer?: boolean | undefined;
236
+ prismerMcpServerPath?: string | undefined;
237
+ model?: string | undefined;
238
+ prismerProviderName?: string | undefined;
239
+ prismerProviderBaseUrl?: string | undefined;
240
+ prismerApiKeyEnv?: string | undefined;
241
+ mirrorNativeKanban?: boolean | undefined;
242
+ mirrorNativeGoals?: boolean | undefined;
243
+ hermesSourceDir?: string | undefined;
244
+ nativeMirrorTimeoutMs?: number | undefined;
245
+ }>;
246
+ type HermesProfileConfig = z.infer<typeof HermesProfileConfigSchema>;
247
+ declare const hermesAdapter: AdapterDef;
248
+
249
+ interface WsClientOptions {
250
+ /** Cloud WS URL, e.g. `wss://cloud.prismer.dev/ws` or `ws://127.0.0.1:3000/ws`. */
251
+ url: string;
252
+ /** API key passed as `?token=<apiKey>` (matches 1.7.x token query auth). */
253
+ apiKey: string;
254
+ /** Initial reconnect delay (ms). Defaults to 1000. */
255
+ reconnectInitialMs?: number;
256
+ /** Reconnect cap (ms). Defaults to 60_000. */
257
+ reconnectMaxMs?: number;
258
+ /** Max consecutive reconnect attempts before entering degraded mode. Defaults to 30. */
259
+ maxReconnectAttempts?: number;
260
+ /** How long to stay in degraded mode before retrying (ms). Defaults to 5 minutes. */
261
+ degradedCooldownMs?: number;
262
+ }
263
+ /** Subset of close codes we special-case. */
264
+ declare const WS_CLOSE: {
265
+ /** RFC 6455 normal closure — no reconnect. */
266
+ readonly NORMAL: 1000;
267
+ /** Custom auth failure — no auto-reconnect; surface to user. */
268
+ readonly AUTH: 4001;
269
+ };
272
270
  /**
273
- * Prismer Runtime — Dispatch Mux (Sprint A3, D4).
274
- *
275
- * Picks an adapter for an incoming task and forwards the dispatch call.
276
- * Selection policy (in priority order):
277
- *
278
- * 1. `preferAdapter` (caller-supplied) wins if registered AND the
279
- * adapter can satisfy the requested capability.
280
- * 2. If exactly one adapter matches the capability, use it.
281
- * 3. If multiple match, pick deterministically — adapters sorted by
282
- * `name` (so the same task always lands on the same adapter when
283
- * multiple are eligible). Future: load/latency-aware ranking.
284
- * 4. None matches → return a structured "no_adapter" failure rather
285
- * than throw — the caller (cloud) decides whether to reroute.
271
+ * Events emitted:
272
+ * - `open` ws successfully opened
273
+ * - `message` (msg) incoming JSON-parsed message
274
+ * - `close` (code, reason) connection closed (auto-reconnect happens after this)
275
+ * - `error` (err) transport error or invalid JSON received
276
+ * - `drop` (msg) send() called when not OPEN; caller may enqueue
277
+ * - `auth-failed` close code 4001 user must rerun setup; no auto-reconnect
278
+ * - `degraded` max reconnect attempts hit; pausing for cooldown
279
+ * - `reconnect-scheduled` (ms) reconnect timer set
280
+ */
281
+ declare class WsClient extends EventEmitter {
282
+ private opts;
283
+ private ws?;
284
+ private reconnectMs;
285
+ private reconnectAttempts;
286
+ private closed;
287
+ private reconnectTimer?;
288
+ constructor(opts: WsClientOptions);
289
+ start(): void;
290
+ /** Send a JSON-serializable message. Caller is responsible for envelope construction. */
291
+ send(msg: unknown): void;
292
+ /** Close intentionally — no reconnect. */
293
+ close(code?: 1000): void;
294
+ isOpen(): boolean;
295
+ private connect;
296
+ private scheduleReconnect;
297
+ }
298
+
299
+ type LocalDb = Database.Database;
300
+ /**
301
+ * Open `~/.prismer/local.db` (or any path), enable WAL, run migrations.
302
+ * Idempotent — safe to call multiple times.
286
303
  *
287
- * The mux never throws on adapter errors; it always returns a result
288
- * with `ok=false` so the cloud-side TaskRouter can report `step_failed`
289
- * instead of seeing a daemon-side stack trace.
304
+ * Pass `:memory:` for in-test usage.
290
305
  */
306
+ declare function openLocalDb(path: string): LocalDb;
307
+ declare function runMigrations(db: LocalDb): void;
308
+ declare function currentSchemaVersion(db: LocalDb): number;
309
+ declare const TARGET_SCHEMA_VERSION = 2;
291
310
 
292
- interface DispatchMuxRequest extends AdapterDispatchInput {
293
- /** If set and registered, win selection. */
294
- preferAdapter?: string;
311
+ type SyncResourceType = 'workspace' | 'agent' | 'agent_profile';
312
+ type SyncOperation = 'create' | 'update' | 'delete';
313
+ type SyncStatus = 'pending' | 'failed_conflict' | 'failed_other';
314
+ interface SyncQueueRow {
315
+ id: number;
316
+ resource_type: SyncResourceType;
317
+ resource_id: string;
318
+ operation: SyncOperation;
319
+ payload: string;
320
+ attempt_count: number;
321
+ last_attempt_at: number | null;
322
+ next_attempt_at: number;
323
+ status: SyncStatus;
324
+ }
325
+ declare function nextBackoffMs(attemptCount: number): number;
326
+ declare class SyncQueue {
327
+ private db;
328
+ constructor(db: LocalDb);
329
+ enqueue(input: {
330
+ resourceType: SyncResourceType;
331
+ resourceId: string;
332
+ operation: SyncOperation;
333
+ payload: unknown;
334
+ /** Defaults to now (ready immediately). */
335
+ runAt?: number;
336
+ }): number;
337
+ /** Take up to `limit` rows whose next_attempt_at <= now and status='pending'. Oldest first. */
338
+ dequeueBatch(limit: number, now?: number): SyncQueueRow[];
339
+ markCompleted(id: number): void;
340
+ markConflict(id: number): void;
341
+ /** Increment attempt + reschedule via backoff. Caller passes the current attempt_count. */
342
+ markBackoff(id: number, currentAttemptCount: number): void;
343
+ /** Permanent failure (4xx other than 409). */
344
+ markFailedOther(id: number): void;
345
+ pendingCount(): number;
346
+ /** All rows for a given resource (debugging / CLI status). */
347
+ listForResource(type: SyncResourceType, id: string): SyncQueueRow[];
295
348
  }
296
- interface DispatchMuxResult extends AdapterDispatchResult {
297
- /** Which adapter handled this request. Useful for telemetry +
298
- * debugging when the answer "feels wrong". */
299
- adapter?: string;
349
+
350
+ interface FlushResult {
351
+ ok: boolean;
352
+ /** HTTP status when applicable. 409 → markConflict; 400-499 (other) → markFailedOther; else markBackoff. */
353
+ status?: number;
354
+ message?: string;
355
+ }
356
+ /** Caller injects the actual cloud HTTP push (m3). Scaffold uses a noop that returns ok:true. */
357
+ type FlushFn = (row: SyncQueueRow) => Promise<FlushResult>;
358
+ interface SyncWorkerOptions {
359
+ queue: SyncQueue;
360
+ flush: FlushFn;
361
+ /** Tick interval in ms. Defaults to 5000. */
362
+ tickMs?: number;
363
+ /** Max rows per tick. Defaults to 10. */
364
+ batchSize?: number;
300
365
  }
301
- declare class DispatchMux {
302
- private readonly registry;
303
- constructor(registry: AdapterRegistry);
366
+ /**
367
+ * Events:
368
+ * - `tick` ({ pending, processed }) each completed tick
369
+ * - `flushed` (row, result) one row push outcome
370
+ * - `error` (err, row?) unexpected error in flush()
371
+ */
372
+ declare class SyncWorker extends EventEmitter {
373
+ private opts;
374
+ private timer?;
375
+ private running;
376
+ constructor(opts: SyncWorkerOptions);
377
+ start(): void;
378
+ stop(): void;
304
379
  /**
305
- * Resolve the adapter that would handle a given request without
306
- * actually dispatching. Returned undefined means "no adapter matches".
307
- * Useful for cloud-side capability probing.
380
+ * Drain up to `batchSize` ready rows. Public for test injection.
381
+ * Re-entrant guarded concurrent ticks return immediately.
308
382
  */
309
- resolve(req: {
310
- capability: string;
311
- preferAdapter?: string;
312
- }): {
313
- adapter: string;
383
+ tick(): Promise<{
384
+ processed: number;
385
+ }>;
386
+ private handleResult;
387
+ }
388
+
389
+ interface RoleTemplate {
390
+ templateName: string;
391
+ displayName: string;
392
+ description: string;
393
+ applicableAdapters: string[];
394
+ configSchema: Record<string, unknown>;
395
+ }
396
+ declare const BUILTIN_ROLE_TEMPLATES: ReadonlyArray<RoleTemplate>;
397
+ declare function getRoleTemplate(name: string): RoleTemplate | undefined;
398
+ declare function listRoleTemplates(): ReadonlyArray<{
399
+ name: string;
400
+ displayName: string;
401
+ description: string;
402
+ }>;
403
+
404
+ declare const ConfigSchema: z.ZodObject<{
405
+ /** API key from `prismer setup`, or env override. */
406
+ api_key: z.ZodString;
407
+ /** Cloud REST + WS base; `ws://` is derived by stripping `http`. */
408
+ cloud_api_base: z.ZodString;
409
+ /** Stable per-machine daemon identifier. Generated once on first setup. */
410
+ daemon_id: z.ZodString;
411
+ /** Optional adapter-specific overrides keyed by adapter name. */
412
+ adapters: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodRecord<z.ZodString, z.ZodUnknown>>>;
413
+ /** Local daemon shell execution. Default disabled. */
414
+ shell: z.ZodOptional<z.ZodObject<{
415
+ enabled: z.ZodDefault<z.ZodBoolean>;
416
+ default_cwd: z.ZodOptional<z.ZodString>;
417
+ defaultCwd: z.ZodOptional<z.ZodString>;
418
+ shell: z.ZodOptional<z.ZodEnum<["bash", "zsh", "sh"]>>;
419
+ max_timeout_ms: z.ZodOptional<z.ZodNumber>;
420
+ maxTimeoutMs: z.ZodOptional<z.ZodNumber>;
421
+ max_output_bytes: z.ZodOptional<z.ZodNumber>;
422
+ maxOutputBytes: z.ZodOptional<z.ZodNumber>;
423
+ allowed_workspaces: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
424
+ allowedWorkspaces: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
425
+ }, "strip", z.ZodTypeAny, {
426
+ enabled: boolean;
427
+ shell?: "bash" | "zsh" | "sh" | undefined;
428
+ default_cwd?: string | undefined;
429
+ defaultCwd?: string | undefined;
430
+ max_timeout_ms?: number | undefined;
431
+ maxTimeoutMs?: number | undefined;
432
+ max_output_bytes?: number | undefined;
433
+ maxOutputBytes?: number | undefined;
434
+ allowed_workspaces?: string[] | undefined;
435
+ allowedWorkspaces?: string[] | undefined;
436
+ }, {
437
+ shell?: "bash" | "zsh" | "sh" | undefined;
438
+ enabled?: boolean | undefined;
439
+ default_cwd?: string | undefined;
440
+ defaultCwd?: string | undefined;
441
+ max_timeout_ms?: number | undefined;
442
+ maxTimeoutMs?: number | undefined;
443
+ max_output_bytes?: number | undefined;
444
+ maxOutputBytes?: number | undefined;
445
+ allowed_workspaces?: string[] | undefined;
446
+ allowedWorkspaces?: string[] | undefined;
447
+ }>>;
448
+ /** Local cache settings. */
449
+ cache: z.ZodOptional<z.ZodObject<{
450
+ max_bytes: z.ZodDefault<z.ZodNumber>;
451
+ }, "strip", z.ZodTypeAny, {
452
+ max_bytes: number;
453
+ }, {
454
+ max_bytes?: number | undefined;
455
+ }>>;
456
+ }, "strip", z.ZodTypeAny, {
457
+ api_key: string;
458
+ cloud_api_base: string;
459
+ daemon_id: string;
460
+ shell?: {
461
+ enabled: boolean;
462
+ shell?: "bash" | "zsh" | "sh" | undefined;
463
+ default_cwd?: string | undefined;
464
+ defaultCwd?: string | undefined;
465
+ max_timeout_ms?: number | undefined;
466
+ maxTimeoutMs?: number | undefined;
467
+ max_output_bytes?: number | undefined;
468
+ maxOutputBytes?: number | undefined;
469
+ allowed_workspaces?: string[] | undefined;
470
+ allowedWorkspaces?: string[] | undefined;
471
+ } | undefined;
472
+ adapters?: Record<string, Record<string, unknown>> | undefined;
473
+ cache?: {
474
+ max_bytes: number;
314
475
  } | undefined;
315
- dispatch(req: DispatchMuxRequest): Promise<DispatchMuxResult>;
476
+ }, {
477
+ api_key: string;
478
+ cloud_api_base: string;
479
+ daemon_id: string;
480
+ shell?: {
481
+ shell?: "bash" | "zsh" | "sh" | undefined;
482
+ enabled?: boolean | undefined;
483
+ default_cwd?: string | undefined;
484
+ defaultCwd?: string | undefined;
485
+ max_timeout_ms?: number | undefined;
486
+ maxTimeoutMs?: number | undefined;
487
+ max_output_bytes?: number | undefined;
488
+ maxOutputBytes?: number | undefined;
489
+ allowed_workspaces?: string[] | undefined;
490
+ allowedWorkspaces?: string[] | undefined;
491
+ } | undefined;
492
+ adapters?: Record<string, Record<string, unknown>> | undefined;
493
+ cache?: {
494
+ max_bytes?: number | undefined;
495
+ } | undefined;
496
+ }>;
497
+ type Config = z.infer<typeof ConfigSchema>;
498
+ interface ConfigPaths {
499
+ /** Default `~/.prismer`. */
500
+ root: string;
501
+ configFile: string;
502
+ localDb: string;
503
+ cacheDir: string;
504
+ logsDir: string;
505
+ /**
506
+ * Per-task run scratch dirs. Each chat-mention dispatch creates
507
+ * `${runsDir}/${taskId}/_outbox/` for adapter-produced files; the
508
+ * OutboxWatcher uploads from there into IMAssets.
509
+ */
510
+ runsDir: string;
316
511
  }
512
+ /**
513
+ * Resolve paths for the prismer home directory. Honors `PRISMER_HOME` env var;
514
+ * defaults to `~/.prismer`.
515
+ */
516
+ declare function resolvePaths(home?: string): ConfigPaths;
517
+ declare function configExists(paths?: ConfigPaths): boolean;
518
+ /**
519
+ * Read and validate config. Env vars override file values for `api_key` and
520
+ * `cloud_api_base` (handy in CI / dev without rewriting config.toml).
521
+ */
522
+ declare function loadConfig(paths?: ConfigPaths): Config;
523
+ declare function saveConfig(config: Config, paths?: ConfigPaths): void;
524
+ /** Derive a `wss?://` URL from a `https?://` base. Trailing `/ws` appended. */
525
+ declare function deriveWsUrl(httpBase: string): string;
317
526
 
318
- interface PairOfferRecord {
319
- offer: string;
320
- uri: string;
321
- expiresAt: number;
322
- createdAt: number;
323
- paired: boolean;
324
- bindingId?: string;
325
- deviceName?: string;
326
- transport?: 'lan' | 'relay';
327
- clientPubKey?: string;
527
+ declare function newDaemonId(): string;
528
+ declare function isDaemonId(s: string): boolean;
529
+
530
+ interface CloudClientOptions {
531
+ baseUrl: string;
532
+ apiKey: string;
533
+ /** Per-request timeout (ms). Defaults to 30s. */
534
+ defaultTimeoutMs?: number;
535
+ /** Pluggable fetch (tests inject a stub). */
536
+ fetchImpl?: typeof fetch;
328
537
  }
329
- interface PairingStatus {
330
- paired: boolean;
331
- expired: boolean;
332
- bindingId?: string;
333
- deviceName?: string;
334
- transport?: 'lan' | 'relay';
335
- }
336
- declare class PairingManager {
337
- private readonly offers;
338
- createOffer(ttlSec?: number): PairOfferRecord;
339
- getStatus(offer: string): PairingStatus | null;
340
- confirm(offer: string, input?: {
341
- bindingId?: string;
342
- deviceName?: string;
343
- transport?: 'lan' | 'relay';
344
- clientPubKey?: string;
345
- }): PairingStatus;
346
- cleanExpired(): void;
538
+ interface CloudResponse<T> {
539
+ ok: boolean;
540
+ status: number;
541
+ data?: T;
542
+ error?: {
543
+ code: string;
544
+ message: string;
545
+ };
347
546
  }
547
+ declare class CloudClient {
548
+ private readonly opts;
549
+ private readonly fetchImpl;
550
+ constructor(opts: CloudClientOptions);
551
+ /** Low-level request. Caller decides on retry; sync-worker handles backoff. */
552
+ request<T = unknown>(method: 'GET' | 'POST' | 'PATCH' | 'PUT' | 'DELETE', path: string, init?: {
553
+ body?: unknown;
554
+ headers?: Record<string, string>;
555
+ signal?: AbortSignal;
556
+ timeoutMs?: number;
557
+ auth?: boolean;
558
+ }): Promise<CloudResponse<T>>;
559
+ /** Convenience: GET expecting `{ ok, data }` envelope. Returns `data` or throws. */
560
+ get<T>(path: string, opts?: {
561
+ signal?: AbortSignal;
562
+ }): Promise<T>;
563
+ urlFor(path: string): string;
564
+ /** Raw byte download (for asset cache). Returns response so caller can stream body. */
565
+ fetchRaw(path: string, init?: {
566
+ headers?: Record<string, string>;
567
+ signal?: AbortSignal;
568
+ timeoutMs?: number;
569
+ }): Promise<Response>;
570
+ }
571
+ declare class CloudError extends Error {
572
+ readonly status: number;
573
+ readonly code: string;
574
+ constructor(status: number, code: string, message: string);
575
+ }
576
+
577
+ interface Envelope<T = unknown> {
578
+ type: string;
579
+ payload: T;
580
+ requestId?: string;
581
+ timestamp: number;
582
+ }
583
+ declare function envelope<T>(type: string, payload: T, requestId?: string): Envelope<T>;
348
584
 
585
+ interface CachedAsset {
586
+ contentHash: string;
587
+ sizeBytes: number;
588
+ mime: string | null;
589
+ localPath: string;
590
+ fetchedAt: number;
591
+ lastUsedAt: number;
592
+ pin: boolean;
593
+ }
594
+ interface AssetCacheOptions {
595
+ db: LocalDb;
596
+ cloud: CloudClient;
597
+ /** ~/.prismer/cache. */
598
+ cacheDir: string;
599
+ /** LRU cap in bytes. Default 5 GiB. */
600
+ maxBytes?: number;
601
+ }
349
602
  /**
350
- * event-handler.ts PARA event subscription and processing for Daemon
351
- *
352
- * Implements PARA event subscription, Tier-based event routing, and
353
- * integration with daemon startup workflow.
603
+ * Local asset cache.
354
604
  *
355
- * Reference: docs/version190/04-sandbox-permissions.md §5.3
605
+ * `getOrFetch(hash, opts)` returns the local path; downloads from
606
+ * `GET /api/im/assets/by-hash/:hash?wsId=…` on miss. Pinned rows survive LRU.
356
607
  */
608
+ declare class AssetCache {
609
+ private readonly db;
610
+ private readonly cloud;
611
+ private readonly cacheDir;
612
+ private readonly maxBytes;
613
+ constructor(opts: AssetCacheOptions);
614
+ /** Local file path for a content hash. Files split by first 2 chars for fs scaling. */
615
+ pathFor(hash: string): string;
616
+ get(hash: string): CachedAsset | undefined;
617
+ /** Bump `last_used_at` (call when an asset is consulted). */
618
+ touch(hash: string): void;
619
+ pin(hash: string): void;
620
+ unpin(hash: string): void;
621
+ /**
622
+ * Get or fetch by hash. On miss, downloads the raw bytes and caches them.
623
+ *
624
+ * Why two endpoints: `GET /api/im/assets/by-hash/:hash` returns the JSON
625
+ * `AssetDTO` (id, contentHash, storageUri, …) — *metadata only*. The raw
626
+ * bytes live at `GET /api/im/assets/:id`. If `opts.assetId` is supplied
627
+ * the caller already has the id from an `AssetRef`, so we go straight to
628
+ * the byte endpoint; otherwise we resolve `hash → id` first (one extra
629
+ * round-trip on the metadata route) before downloading the body.
630
+ *
631
+ * Earlier this method called `by-hash` and treated the JSON body as the
632
+ * asset content — the daemon stored the 500-byte metadata blob on disk
633
+ * and `resolveAssetRefs` inlined that JSON into the prompt. The agent
634
+ * saw `{"id":"…","storageUri":"…"}` instead of the user's file and
635
+ * concluded the upload had failed.
636
+ */
637
+ getOrFetch(hash: string, opts?: {
638
+ workspaceIdHint?: string;
639
+ signal?: AbortSignal;
640
+ assetId?: string;
641
+ }): Promise<CachedAsset>;
642
+ /** Insert an already-on-disk asset into the cache (e.g., asset just produced by adapter). */
643
+ registerLocal(hash: string, localPath: string, mime?: string): CachedAsset;
644
+ /** Total bytes across all rows (incl. pinned). */
645
+ totalBytes(): number;
646
+ /** Run LRU eviction until under cap. Pinned rows are never evicted. */
647
+ evictIfOver(): {
648
+ removed: number;
649
+ freed: number;
650
+ };
651
+ }
357
652
 
358
- interface EventHandlerOptions {
359
- bus: EventBus;
360
- supervisor: AgentSupervisor;
361
- /** Optional tier filter for event routing */
362
- tierFilter?: number;
363
- }
364
- declare class EventHandler {
365
- private readonly _bus;
366
- private readonly _supervisor;
367
- private readonly _tierFilter?;
368
- private readonly _subscriptions;
369
- private readonly _blockedEvents;
370
- private _registered;
371
- constructor(opts: EventHandlerOptions);
653
+ interface WorkspaceFileBinding {
654
+ workspaceId: string;
655
+ path: string;
656
+ assetId: string;
657
+ contentHash: string;
658
+ version: number;
659
+ syncedAt: number | null;
660
+ dirty: boolean;
661
+ }
662
+ interface WorkspaceMirrorOptions {
663
+ db: LocalDb;
664
+ cloud: CloudClient;
665
+ workspaceId: string;
372
666
  /**
373
- * Start subscribing to PARA events.
374
- * Must be called after daemon is fully initialized.
667
+ * Per-workspace state dir. Caller should pass ~/.prismer/<wid>/ so different
668
+ * paired workspaces don't race on the cursor file.
375
669
  */
376
- start(): void;
670
+ workspaceStateDir: string;
671
+ }
672
+ /**
673
+ * Mirror reads cloud-side path→asset bindings into SQLite for fast local
674
+ * lookup. The cursor file is the only piece of state that must survive
675
+ * daemon restart; the SQLite mirror is rebuilt from scratch by the very
676
+ * first pullDelta() with no cursor.
677
+ */
678
+ declare class WorkspaceMirror {
679
+ private readonly db;
680
+ private readonly cloud;
681
+ private readonly workspaceId;
682
+ private readonly cursorPath;
683
+ constructor(opts: WorkspaceMirrorOptions);
684
+ /** Persisted cursor for this workspace, or null on first run / corrupted file. */
685
+ readCursor(): string | null;
686
+ private writeCursor;
687
+ /**
688
+ * Pull workspace_file changes since the persisted cursor and upsert into
689
+ * the local mirror. Soft-deleted rows are removed from the mirror; active
690
+ * rows are upserted with synced_at = now and dirty = 0.
691
+ *
692
+ * Returns the count of items applied + the new cursor (may be unchanged
693
+ * when the cloud has nothing new). Idempotent — safe to retry.
694
+ */
695
+ pullDelta(opts?: {
696
+ signal?: AbortSignal;
697
+ }): Promise<{
698
+ applied: number;
699
+ cursor: string | null;
700
+ }>;
701
+ /** Lookup the active binding for a workspace path. Returns undefined if not mirrored. */
702
+ lookupByPath(path: string): WorkspaceFileBinding | undefined;
703
+ /** Find an asset's contentHash via any path binding (asset may map to many paths). */
704
+ hashForAssetId(assetId: string): string | undefined;
705
+ /** All active bindings for this workspace — used by status / debug surfaces. */
706
+ list(): WorkspaceFileBinding[];
707
+ /** Wipe the mirror for this workspace (e.g., re-pair). Cursor file is left intact. */
708
+ clear(): void;
709
+ }
710
+
711
+ interface ParseClaim {
712
+ id: string;
713
+ workspaceId: string;
714
+ assetId: string;
715
+ ingestVersion: number;
716
+ claimantImUserId: string;
717
+ claimantDeviceId: string;
718
+ status: 'active' | 'completed' | 'abandoned';
719
+ claimedAt: string;
720
+ heartbeatAt: string;
721
+ completedAt: string | null;
722
+ }
723
+ type AcquireResult = {
724
+ kind: 'acquired';
725
+ claim: ParseClaim;
726
+ } | {
727
+ kind: 'refreshed';
728
+ claim: ParseClaim;
729
+ } | {
730
+ kind: 'claimed_active';
731
+ current: ParseClaim;
732
+ } | {
733
+ kind: 'already_complete';
734
+ current: ParseClaim;
735
+ };
736
+ type ClaimLostReason = 'claim_taken_over' | 'claim_inactive';
737
+ interface ParseClaimControllerOptions {
738
+ cloud: CloudClient;
739
+ /** Daemon's persisted device id. Same value across heartbeats / completions. */
740
+ deviceId: string;
741
+ /** Heartbeat interval (ms). Default 10s — well below the 30s server TTL. */
742
+ heartbeatIntervalMs?: number;
743
+ }
744
+ interface HeartbeatHandle {
745
+ stop: () => void;
377
746
  /**
378
- * Stop all event subscriptions.
747
+ * Symbol.dispose so consumers can write `using hb = ctrl.startHeartbeat(...)`
748
+ * once they're on TypeScript 5.2 + Node 22 explicit-resource-management.
749
+ * Without that runtime support the field is harmless dead weight; the
750
+ * explicit `stop()` is the universal contract.
379
751
  */
380
- stop(): void;
752
+ [Symbol.dispose]: () => void;
753
+ }
754
+ /**
755
+ * Coordinates a single daemon's asset-parse claims. Owns no global state —
756
+ * callers may run multiple controllers if they share the same device id.
757
+ */
758
+ declare class ParseClaimController {
759
+ private readonly cloud;
760
+ private readonly deviceId;
761
+ private readonly heartbeatIntervalMs;
762
+ constructor(opts: ParseClaimControllerOptions);
381
763
  /**
382
- * Subscribe to a specific PARA event type with optional tier filter.
764
+ * Acquire (or refresh) a claim. The return value is a 4-variant tagged
765
+ * union — callers must branch on `kind`. Throws on transport / 5xx / 4xx
766
+ * codes that aren't part of the documented contract (e.g. `claim_race`
767
+ * is mapped to a thrown Error so the worker can retry).
383
768
  */
384
- private _subscribe;
769
+ acquire(input: {
770
+ assetId: string;
771
+ ingestVersion: number;
772
+ }): Promise<AcquireResult>;
385
773
  /**
386
- * Unsubscribe from a specific event type.
774
+ * Refresh the heartbeat for an active claim. Throws `ClaimLostError` on
775
+ * HTTP 410 with code `claim_taken_over` or `claim_inactive` — caller
776
+ * should abort the parse work. Throws plain `Error` on any other failure.
387
777
  */
388
- unsubscribe(eventType: string, handler?: SubscriptionHandler<ParaEvent>): void;
778
+ sendHeartbeat(claimId: string): Promise<ParseClaim>;
389
779
  /**
390
- * Handle agent registration event
780
+ * Mark a claim complete. Throws `ClaimLostError` on 410 (claim was
781
+ * stolen before the worker finished — output should be discarded).
391
782
  */
392
- private _handleAgentRegister;
783
+ complete(claimId: string): Promise<ParseClaim>;
393
784
  /**
394
- * Handle session started event
785
+ * Start a heartbeat loop in the background. Returns a handle with `stop()`
786
+ * and a Symbol.dispose alias for the `using` pattern.
787
+ *
788
+ * On 410 (claim taken over or marked inactive) the loop stops itself and
789
+ * invokes `opts.onLost` so the parse worker can abort. Other transient
790
+ * errors (network blip, 5xx) are logged to stderr and the loop retries on
791
+ * the next tick — the server-side TTL (default 30s) will only drop us if
792
+ * the partition outlasts the TTL.
395
793
  */
396
- private _handleSessionStarted;
794
+ startHeartbeat(claimId: string, opts?: {
795
+ onLost?: (reason: ClaimLostReason) => void;
796
+ }): HeartbeatHandle;
797
+ }
798
+
799
+ type PrismerUriType = 'asset' | 'file';
800
+ interface ParsedPrismerUri {
801
+ /** Original full URI string. */
802
+ raw: string;
803
+ /**
804
+ * Owner segment for legacy `prismer://<owner>/...` form (informational,
805
+ * not ACL-checked). For `prismer://workspace/<wid>/...` this is the
806
+ * literal string 'workspace' — prefer `workspaceId` instead.
807
+ */
808
+ owner: string;
809
+ type: PrismerUriType;
810
+ /** For type='asset': the sha256 hash. For legacy file URIs: '<wsId>/<path>'. */
811
+ rest: string;
812
+ /** Convenience parses for type='file' and for workspace-form 'asset'. */
813
+ workspaceId?: string;
814
+ filePath?: string;
815
+ }
816
+ /**
817
+ * Find every recognized `prismer://...` reference in a string.
818
+ * Only `asset` and `file` types are recognized — other types (memory,
819
+ * context, conversation, evolution, task, pair) pass through untouched.
820
+ */
821
+ declare function parseUris(text: string): ParsedPrismerUri[];
822
+ interface UriResolverOptions {
823
+ db: LocalDb;
824
+ cloud: CloudClient;
825
+ assetCache: AssetCache;
826
+ }
827
+ declare class UriResolver {
828
+ private readonly db;
829
+ private readonly cloud;
830
+ private readonly assetCache;
831
+ constructor(opts: UriResolverOptions);
832
+ /**
833
+ * Resolve a single URI to a local file path. Throws on auth/4xx;
834
+ * caller decides whether to leave the URI in place (best-effort) or fail.
835
+ */
836
+ resolveOne(uri: ParsedPrismerUri, opts?: {
837
+ signal?: AbortSignal;
838
+ }): Promise<string>;
839
+ /**
840
+ * Walk a string, replace every `prismer://(asset|file)/...` with `file://<localPath>`.
841
+ * Unrecognized URIs pass through. Returns rewritten text + the list of pinned hashes.
842
+ */
843
+ rewrite(text: string, opts?: {
844
+ pin?: boolean;
845
+ signal?: AbortSignal;
846
+ }): Promise<{
847
+ text: string;
848
+ resolvedHashes: string[];
849
+ }>;
850
+ /** Bulk-rewrite an array of strings (e.g. context entries' content). */
851
+ rewriteAll(texts: string[], opts?: {
852
+ pin?: boolean;
853
+ signal?: AbortSignal;
854
+ }): Promise<{
855
+ texts: string[];
856
+ resolvedHashes: string[];
857
+ }>;
858
+ private lookupFile;
859
+ }
860
+
861
+ type IMAgentStatus = 'online' | 'busy' | 'idle' | 'offline';
862
+ interface HostedAgentDeclaration {
863
+ imUserId: string;
864
+ name: string;
865
+ adapterName: string;
866
+ capabilities: string[];
867
+ profiles: Array<{
868
+ id: string;
869
+ version: number;
870
+ }>;
871
+ }
872
+ interface AgentHostDeclarePayload {
873
+ daemonId: string;
874
+ daemonVersion: string;
875
+ platform: 'darwin' | 'linux' | 'win32';
876
+ agents: HostedAgentDeclaration[];
877
+ }
878
+ interface HostAckedPayload {
879
+ workspaceId: string;
880
+ syncCursor: {
881
+ workspaces: number;
882
+ agent_profiles: number;
883
+ [key: string]: number;
884
+ };
885
+ profilesToSync: string[];
886
+ }
887
+ interface AgentStatusChangedPayload {
888
+ agentImUserId: string;
889
+ status: IMAgentStatus;
890
+ activeProfileId?: string;
891
+ runningTaskIds?: string[];
892
+ }
893
+ interface TaskDispatchContextEntry {
894
+ sender: string;
895
+ senderRole: 'human' | 'agent' | 'admin' | 'system';
896
+ content: string;
897
+ createdAt: string;
898
+ /** Wave-8 W1: assets the human attached to THIS chat message. */
899
+ attachedAssetIds?: string[];
900
+ }
901
+ /** Wave-8 W1: hydrated asset reference attached to a dispatch. */
902
+ interface AssetRef {
903
+ assetId: string;
904
+ contentHash: string;
905
+ mime: string | null;
906
+ sizeBytes: number | null;
907
+ kind: string;
908
+ workspaceId: string;
909
+ role: 'attachment' | 'context';
910
+ }
911
+ interface TaskDispatchRequestPayload {
912
+ taskId: string;
913
+ /** Agent target for runtimeRoute='agent'. Shell dispatches do not use this. */
914
+ agentImUserId?: string;
915
+ /** Runtime/device target for runtimeRoute='shell'. */
916
+ targetDaemonId?: string;
917
+ profileId: string;
918
+ capability: string;
919
+ prompt: string;
920
+ /** Execution surface. `shell` is daemon-local command execution. */
921
+ runtimeRoute?: 'agent' | 'sandbox' | 'shell';
922
+ metadata?: Record<string, unknown>;
923
+ timeoutMs?: number;
924
+ context?: TaskDispatchContextEntry[];
925
+ conversationId?: string;
926
+ /**
927
+ * Channel mode for the originating conversation. Optional and
928
+ * forward-compatible: when missing, daemon renders 'unknown' in the
929
+ * [Channel context] prompt block. See appendChannelContext in
930
+ * daemon/dispatch.ts.
931
+ * - `direct`: 1:1 DM (no @-mention needed in reply).
932
+ * - `group`: multi-party room (end reply with `@<recipient>` to
933
+ * continue the chain).
934
+ */
935
+ conversationType?: 'direct' | 'group';
936
+ /**
937
+ * Active participants of the dispatch's conversation. Daemon injects this
938
+ * into [Channel context] so the agent knows the authoritative recipient list
939
+ * without hallucinating or having to call `prismer.conversation.listAgents`. Capped at 50
940
+ * entries server-side.
941
+ */
942
+ participants?: Array<{
943
+ imUserId: string;
944
+ username: string;
945
+ displayName: string;
946
+ role: string;
947
+ agentType?: string | null;
948
+ }>;
949
+ /** Wave-8 W1: assets cloud wants daemon to fold into agent context. */
950
+ assetRefs?: AssetRef[];
951
+ }
952
+ interface TaskDispatchProgressPayload {
953
+ taskId: string;
954
+ progress: number;
955
+ message?: string;
956
+ detail?: Record<string, unknown>;
957
+ }
958
+ /** Wave-8 W1: how the daemon handled a single AssetRef. */
959
+ type AssetDispatchStrategy = 'inline-text' | 'inline-text-truncated' | 'uri-only' | 'error';
960
+ interface AssetDispatchObservation {
961
+ assetId: string;
962
+ contentHash: string;
963
+ mime: string | null;
964
+ sizeBytes: number | null;
965
+ strategy: AssetDispatchStrategy;
966
+ inlinedBytes?: number;
967
+ error?: string;
968
+ }
969
+ interface TaskDispatchReplyPayload {
970
+ taskId: string;
971
+ ok: boolean;
972
+ output?: string;
973
+ error?: {
974
+ code: string;
975
+ message: string;
976
+ };
977
+ assetIds?: string[];
978
+ metrics?: {
979
+ tokensUsed?: number;
980
+ durationMs?: number;
981
+ };
982
+ /** Wave-8 W1: per-asset handling report. */
983
+ assetObservability?: AssetDispatchObservation[];
984
+ }
985
+ interface TaskCancelPayload {
986
+ taskId: string;
987
+ reason?: string;
988
+ }
989
+ interface IMWSMessage<T = unknown> {
990
+ type: string;
991
+ payload: T;
992
+ requestId?: string;
993
+ timestamp: number;
994
+ }
995
+ interface WorkspaceChangedPayload {
996
+ workspaceId: string;
997
+ /** ISO-8601 timestamp from im_workspaces.updatedAt. */
998
+ updatedAt: string;
999
+ }
1000
+ interface AgentProfileChangedPayload {
1001
+ profileId: string;
1002
+ version: number;
1003
+ }
1004
+ interface AgentChangedPayload {
1005
+ agentImUserId: string;
1006
+ fields: {
1007
+ displayName?: string;
1008
+ capabilities?: string[];
1009
+ };
1010
+ }
1011
+ interface WorkspaceFileChangedPayload {
1012
+ workspaceId: string;
1013
+ path: string;
1014
+ operation: 'create' | 'update' | 'delete';
1015
+ assetId?: string;
1016
+ contentHash?: string;
1017
+ version: number;
1018
+ }
1019
+
1020
+ interface OutboxWatcherOptions {
1021
+ /**
1022
+ * Default directory to watch when no setActiveTask has narrowed it.
1023
+ * Container mode: `/workspace/_outbox`. Host mode: omit (no default
1024
+ * scan — only watch when an active task points at a specific dir).
1025
+ */
1026
+ outboxDir?: string;
1027
+ /** Cloud client (already configured with apiKey + baseUrl). */
1028
+ cloud: CloudClient;
1029
+ /**
1030
+ * Container identifier — required by the cloud's kind=sandbox-output
1031
+ * contract as metadata.containerId. Read from env PRISMER_CONTAINER_ID
1032
+ * when controller injected one; otherwise the daemon_id as fallback so
1033
+ * the request still passes validation. Host-mode (kind=agent-output)
1034
+ * uploads bypass containerId validation.
1035
+ */
1036
+ containerId: string;
1037
+ /**
1038
+ * Resolver for the owning workspace at upload time. Returns null when no
1039
+ * workspace context is yet known — the watcher will skip the file and
1040
+ * retry on the next scan tick.
1041
+ */
1042
+ workspaceId: () => string | null;
1043
+ /** Poll interval in ms. Default 2000. */
1044
+ pollIntervalMs?: number;
1045
+ /** Optional log sink (default writes to stdout/stderr). */
1046
+ log?: {
1047
+ info: (msg: string) => void;
1048
+ warn: (msg: string) => void;
1049
+ };
1050
+ }
1051
+ interface ActiveTask {
1052
+ taskId: string;
1053
+ /** Adapter name; surfaces in upload metadata for observability. */
1054
+ adapter?: string;
397
1055
  /**
398
- * Handle session ended event
1056
+ * Per-task outbox directory. When set, scans this directory in
1057
+ * addition to `opts.outboxDir`. Files uploaded from here are tagged
1058
+ * with `kind=agent-output` (host-mode contract).
399
1059
  */
400
- private _handleSessionEnded;
1060
+ outboxDir?: string;
1061
+ }
1062
+ declare class OutboxWatcher {
1063
+ private opts;
1064
+ private timer?;
1065
+ private uploaded;
401
1066
  /**
402
- * Handle agent state change
1067
+ * Host-mode tasks keyed by taskId. Each entry has its own `outboxDir`
1068
+ * and the watcher scans every entry's directory each tick. Concurrent
1069
+ * dispatches (Wave-9: orchestrator + spawned worker on the same daemon)
1070
+ * each occupy their own slot — finishing one dispatch only removes its
1071
+ * slot, leaving every other concurrent dispatch's tracking intact.
1072
+ *
1073
+ * Replaces the v1 single `activeTask` slot which was last-wins and
1074
+ * caused cross-dispatch clobbering: an orchestrator finishing first
1075
+ * would `setActiveTask(null)` and silently discard the concurrent
1076
+ * worker's outbox tracking.
403
1077
  */
404
- private _handleAgentState;
1078
+ private activeTasks;
405
1079
  /**
406
- * Handle skill activation
1080
+ * Legacy container/sandbox task. Used only when an `ActiveTask` is set
1081
+ * **without** an `outboxDir` (i.e. the legacy `setActiveTask` shape
1082
+ * where the watcher's default `opts.outboxDir` is the scan target).
1083
+ * Single slot because the legacy container deployment only ever runs
1084
+ * one task at a time per container.
407
1085
  */
408
- private _handleSkillActivated;
1086
+ private legacyContainerTask;
1087
+ private busy;
409
1088
  /**
410
- * Handle skill deactivation
1089
+ * Per-task accumulated assetIds. Populated on each successful upload
1090
+ * keyed by the task whose outbox dir the file came from.
1091
+ * `flushPending(taskId)` drains and returns this list so dispatch.ts
1092
+ * can stamp it onto task.dispatch.reply.assetIds.
411
1093
  */
412
- private _handleSkillDeactivated;
1094
+ private pendingByTask;
1095
+ constructor(opts: OutboxWatcherOptions);
413
1096
  /**
414
- * Handle approval request
1097
+ * Register a host-mode task envelope. Each call adds (or replaces) an
1098
+ * entry keyed by `task.taskId` so concurrent dispatches do not clobber
1099
+ * each other.
1100
+ *
1101
+ * Requires `task.outboxDir` to be set — the per-task workdir is the
1102
+ * isolation boundary. For the legacy container/sandbox single-slot
1103
+ * shape (no per-task dir), use `setActiveTask` instead.
415
1104
  */
416
- private _handleApprovalRequest;
1105
+ addActiveTask(task: ActiveTask): void;
417
1106
  /**
418
- * Handle approval result
1107
+ * Remove a host-mode task entry by id. No-op if the id was not
1108
+ * registered. Idempotent — dispatch.ts calls this in `finally{}` after
1109
+ * `flushPending` so a failed dispatch still clears its slot.
419
1110
  */
420
- private _handleApprovalResult;
1111
+ removeActiveTask(taskId: string): void;
421
1112
  /**
422
- * Handle task creation
1113
+ * Legacy single-slot API.
1114
+ *
1115
+ * - Non-null with `outboxDir` → forwards to `addActiveTask` (host
1116
+ * mode). The caller is still responsible for `removeActiveTask`
1117
+ * (or another `setActiveTask(null)`) at teardown.
1118
+ * - Non-null **without** `outboxDir` → store in the legacy container
1119
+ * slot. Uploads from `opts.outboxDir` are tagged with this task.
1120
+ * - `null` → clear the legacy container slot only. Does NOT touch
1121
+ * the host-mode `activeTasks` map; concurrent host-mode dispatches
1122
+ * keep their own slots until they call `removeActiveTask`
1123
+ * themselves.
1124
+ *
1125
+ * The null-clears-only-legacy semantic is the core fix for the
1126
+ * Wave-9 concurrency race: previously a finishing orchestrator's
1127
+ * `setActiveTask(null)` would clobber a still-running worker's
1128
+ * tracking, dropping its uploads on the floor.
423
1129
  */
424
- private _handleTaskCreated;
1130
+ setActiveTask(task: ActiveTask | null): void;
425
1131
  /**
426
- * Handle task completion
1132
+ * Drain and return the assetIds collected for `taskId` since the last
1133
+ * flush. Idempotent — calling twice with the same id returns `[]` the
1134
+ * second time. dispatch.ts calls this once at reply-build time.
427
1135
  */
428
- private _handleTaskCompleted;
1136
+ flushPending(taskId: string): string[];
1137
+ start(): void;
1138
+ stop(): void;
429
1139
  /**
430
- * Handle LLM pre-request
1140
+ * Force a synchronous scan — useful for tests + e2e harnesses that want to
1141
+ * trigger upload after writing a file rather than wait for the next tick.
1142
+ * Also used by dispatch.ts at reply-build time to drain any pending files
1143
+ * before flushPending() is called.
431
1144
  */
432
- private _handleLlmPre;
1145
+ scanNow(): Promise<void>;
1146
+ private log;
433
1147
  /**
434
- * Handle LLM post-request
1148
+ * Build the list of directories to scan this tick — one entry per
1149
+ * concurrently-active task plus the legacy container slot (if any).
1150
+ * Each entry carries its own `task` envelope so uploads can be tagged
1151
+ * and accumulated per-task without sharing global mutable state.
1152
+ *
1153
+ * Dedup so a directory configured in both spots only scans once
1154
+ * (legacy single-task case where opts.outboxDir == legacyTask.outboxDir
1155
+ * is unlikely but cheap to handle).
435
1156
  */
436
- private _handleLlmPost;
437
- }
438
-
439
- /** Authenticated identity returned by the authenticate callback. */
440
- interface AuthenticatedIdentity {
441
- agentId: string;
442
- bearerSub?: string;
443
- }
444
- interface DaemonHttpOptions {
445
- host?: string;
446
- port?: number;
447
- eventBus: EventBus;
448
- supervisor: AgentSupervisor;
449
- fsContextProvider?: (req: {
450
- agentId: string;
451
- workspace?: string;
452
- }) => FsContext | undefined;
453
- pairingManager?: PairingManager;
454
- eventHandler?: EventHandler;
455
- /** When provided, every non-health request must carry a valid Bearer token (null = 401).
456
- * When absent, localhost trust mode: body agentId is used as-is. */
457
- authenticate?: (bearerToken: string | undefined) => AuthenticatedIdentity | null;
458
- }
459
- /** Generic extension-point route handler registered via registerRoute() (Q3). */
460
- type RouteHandler = (req: http.IncomingMessage, res: http.ServerResponse, ctx: {
461
- authed: AuthenticatedIdentity | null;
462
- body: Buffer;
463
- }) => Promise<void> | void;
464
- declare class DaemonHttpServer {
465
- private readonly _host;
466
- private readonly _port;
467
- private readonly _bus;
468
- private readonly _supervisor;
469
- private readonly _fsCtxProvider?;
470
- private readonly _authenticate?;
471
- private readonly _pairingManager;
472
- private readonly _eventHandler?;
473
- private _server;
474
- private _url;
475
- private _running;
476
- private readonly _sseClients;
477
- private readonly _inFlight;
478
- private readonly _startedAt;
479
- private readonly _customRoutes;
480
- constructor(opts: DaemonHttpOptions);
481
- get url(): string | undefined;
482
- get isRunning(): boolean;
483
- /** Register a custom route handler. Built-in routes take precedence. */
484
- registerRoute(method: 'GET' | 'POST' | 'PATCH' | 'DELETE', path: string, handler: RouteHandler): void;
485
- /** Remove a previously registered custom route. */
486
- unregisterRoute(method: string, path: string): void;
487
- private _findCustomRoute;
488
- start(): Promise<{
489
- host: string;
490
- port: number;
491
- }>;
492
- stop(timeoutMs?: number): Promise<void>;
493
- private _handleRequest;
494
- private _handleHealth;
495
- private _handlePairOffer;
496
- private _handlePairStatus;
497
- private _handlePairConfirm;
498
- private _handleTransportStatus;
499
- /** Read-only snapshot of transport state, used by /gui/status. Returns
500
- * null when the multi-path transport isn't enabled. */
501
- private _snapshotTransportStatus;
502
- private _handleLanProbe;
503
- private _handleTransportReprobe;
504
- }
505
-
506
- interface LLMMessage {
507
- role: 'system' | 'user' | 'assistant';
508
- content: string;
509
- }
510
- interface LLMRequest {
511
- messages: LLMMessage[];
512
- maxTokens?: number;
513
- temperature?: number;
514
- maxCostUsd?: number;
515
- maxLatencyMs?: number;
516
- capabilities?: string[];
517
- }
518
- interface LLMResponse {
519
- content: string;
520
- usage: {
521
- promptTokens: number;
522
- completionTokens: number;
523
- totalTokens: number;
524
- };
525
- provider: string;
526
- model: string;
527
- latencyMs: number;
528
- costUsd?: number;
529
- }
530
- interface LLMProvider {
531
- name: string;
532
- model: string;
533
- priority: number;
534
- capabilities?: string[];
535
- pricing?: {
536
- inputPer1kUsd: number;
537
- outputPer1kUsd: number;
538
- };
539
- invoke: (req: LLMRequest, signal: AbortSignal) => Promise<Omit<LLMResponse, 'provider' | 'model' | 'latencyMs'>>;
540
- healthCheck?: () => Promise<boolean>;
541
- }
542
- interface RoutingPolicy {
543
- strategy: 'priority' | 'cheapest' | 'fastest' | 'round-robin';
544
- retryOnFailure?: boolean;
545
- maxRetries?: number;
546
- healthCheckIntervalMs?: number;
547
- timeoutMs?: number;
548
- }
549
- interface ProviderStats {
550
- invocations: number;
551
- successes: number;
552
- failures: number;
553
- p50Ms: number;
554
- p95Ms: number;
555
- avgCostUsd: number;
556
- }
557
- declare class AllProvidersFailedError extends Error {
558
- attempts: Array<{
559
- provider: string;
560
- error: string;
561
- }>;
562
- constructor(attempts: AllProvidersFailedError['attempts']);
563
- }
564
- declare class LLMDispatcher {
565
- private readonly providers;
566
- private policy;
567
- private readonly state;
568
- private rrCounter;
569
- private healthTimer;
570
- constructor(providers: LLMProvider[], policy?: RoutingPolicy);
571
- complete(req: LLMRequest): Promise<LLMResponse>;
572
- get stats(): Record<string, ProviderStats>;
573
- setPolicy(policy: RoutingPolicy): void;
574
- markProviderDown(name: string): void;
575
- markProviderUp(name: string): void;
576
- stopHealthChecks(): void;
577
- private startHealthChecks;
578
- private runHealthChecks;
579
- _runHealthChecksNow(): Promise<void>;
580
- private isDown;
581
- private estimateCostUsd;
582
- private selectProviders;
1157
+ private currentScanDirs;
1158
+ private tick;
1159
+ private scanDir;
1160
+ private upload;
583
1161
  }
584
1162
 
1163
+ interface DispatchDeps {
1164
+ registry: AdapterRegistry;
1165
+ cloud: CloudClient;
1166
+ uriResolver: UriResolver;
1167
+ assetCache: AssetCache;
1168
+ ws: WsClient;
1169
+ /** Cap on total chars of joined context history. Default 8000. */
1170
+ contextMaxChars?: number;
1171
+ /** Resolves a long-running adapter's service handle (cached across calls). */
1172
+ ensureService: (profile: AgentProfile, adapter: AdapterDef) => Promise<AdapterService>;
1173
+ /** Aborts the in-flight adapter dispatch when cloud sends task.cancel. */
1174
+ signal?: AbortSignal;
1175
+ /** Hook for tests / observability. */
1176
+ onProgress?: (taskId: string, p: TaskDispatchProgressPayload) => void;
1177
+ /**
1178
+ * Outbox uploader. When provided, dispatch creates a per-task outbox
1179
+ * directory under `paths.runsDir`, points the watcher at it via
1180
+ * setActiveTask, and drains its assetIds into reply.assetIds at flush
1181
+ * time. Optional so tests can omit it.
1182
+ */
1183
+ outboxWatcher?: OutboxWatcher;
1184
+ /** Daemon paths — used to derive the per-task outbox dir. */
1185
+ paths?: ConfigPaths;
1186
+ }
1187
+ declare function handleDispatch(payload: TaskDispatchRequestPayload, requestId: string | undefined, deps: DispatchDeps): Promise<TaskDispatchReplyPayload>;
585
1188
  /**
586
- * Prismer Runtime Evolution Gateway HTTP API
587
- *
588
- * Exposes evolution gateway operations via HTTP for daemon process.
589
- * Integrates with LLM Dispatcher for distillation triggers.
1189
+ * Concatenate context entries + current prompt. Trims oldest entries first when
1190
+ * total chars exceed `maxChars` (matches Track C's `trimContextWindow` behavior).
590
1191
  *
591
- * Endpoints:
592
- * - POST /evolution/signal Extract signals from tool output / log
593
- * - POST /evolution/record Record gene execution outcome
594
- * - POST /evolution/analyze Analyze signals and recommend gene
595
- * - POST /evolution/genes — Create new gene
596
- * - GET /evolution/genes — Query available genes
597
- * - GET /evolution/personality — Get agent personality
598
- * - POST /evolution/distill — Trigger distillation
599
- *
600
- * @see docs/version190/IMPLEMENTATION-PLAN-RUNTIME-GAP.md
1192
+ * `assetBlocks` (Wave-8 W1) is prepended above the conversation history. They
1193
+ * are *not* counted against `maxChars` assets are content the user
1194
+ * explicitly attached, so dropping them silently when the chat history
1195
+ * happens to be long would defeat the whole point of the attachment.
601
1196
  */
1197
+ declare function composePrompt(currentPrompt: string, context: TaskDispatchContextEntry[], maxChars: number, assetBlocks?: string[]): string;
602
1198
 
603
- interface EvolutionGatewayOptions {
604
- eventBus: EventBus;
605
- supervisor: AgentSupervisor;
606
- llmDispatcher?: LLMDispatcher;
607
- /** Cloud API base URL (default: https://prismer.cloud) */
608
- cloudApiBase?: string;
609
- /** Authenticated identity (from Bearer token or API key) */
610
- authenticate?: (bearerToken: string | undefined) => AuthenticatedIdentity | null;
1199
+ declare class ServicePool {
1200
+ private readonly services;
611
1201
  /**
612
- * v1.9.0 B.7.a API key used as the Bearer token when forwarding daemon
613
- * requests to the cloud Evolution API. When undefined, outbound requests
614
- * omit the Authorization header (callers get 401 from cloud until the
615
- * daemon is started with `apiKey`).
1202
+ * Returns a service handle for the profile, lazily calling `adapter.ensureService`
1203
+ * on first use or after a crash. Re-checks `healthy()` before returning a cached entry.
616
1204
  */
617
- cloudApiKey?: string;
618
- }
619
- interface SignalExtractionRequest {
620
- toolOutput?: {
621
- toolName?: string;
622
- output?: string;
623
- exitCode?: number;
624
- error?: string;
625
- durationMs?: number;
626
- };
627
- logFile?: {
628
- logPath?: string;
629
- logContent?: string;
630
- timestamp?: string;
631
- };
632
- provider?: string;
633
- stage?: string;
634
- severity?: string;
635
- tags?: string[];
636
- }
637
- interface AnalyzeRequest {
638
- signals: string[];
639
- taskCapability?: string;
640
- provider?: string;
641
- stage?: string;
642
- severity?: string;
643
- }
644
- interface RecordRequest {
645
- geneId: string;
646
- signals: string[];
647
- outcome: 'success' | 'failed';
648
- score?: number;
649
- summary?: string;
650
- costCredits?: number;
651
- transitionReason?: 'gene_applied' | 'fallback_relaxed' | 'fallback_neighbor' | 'baseline';
652
- }
653
- interface CreateGeneRequest {
654
- category: 'repair' | 'optimize' | 'innovate' | 'diagnostic';
655
- signalsMatch: string[];
656
- strategy: string[];
657
- preconditions?: string[];
658
- constraints?: {
659
- maxCredits?: number;
660
- maxRetries?: number;
661
- maxExecutionTime?: number;
662
- };
1205
+ ensureService(profile: AgentProfile, adapter: AdapterDef): Promise<AdapterService>;
1206
+ /** Drop a service handle (e.g. on profile delete or daemon shutdown). */
1207
+ drop(profileId: string): Promise<void>;
1208
+ /** Drop all handles, e.g. on graceful shutdown. */
1209
+ shutdown(): Promise<void>;
1210
+ size(): number;
663
1211
  }
664
- interface DistillationRequest {
665
- /** Skip LLM verification and trigger immediately */
666
- dryRun?: boolean;
667
- }
668
- declare class EvolutionGatewayHttpHandler {
669
- private readonly eventBus;
670
- private readonly supervisor;
671
- private readonly llmDispatcher;
672
- private readonly cloudApiBase;
673
- private readonly authenticate;
674
- private readonly cloudApiKey;
675
- private readonly routes;
676
- constructor(opts: EvolutionGatewayOptions);
677
- private cloudAuthHeaders;
678
- private registerRoutes;
679
- /** Get all registered routes */
680
- getRoutes(): Map<string, RouteHandler>;
681
- /** Get a specific route handler */
682
- getRoute(method: string, path: string): RouteHandler | undefined;
683
- /**
684
- * POST /evolution/signal
685
- * Extract signals from tool output or log file.
686
- */
687
- private handleExtractSignal;
688
- /**
689
- * POST /evolution/analyze
690
- * Analyze signals and recommend the best gene.
691
- */
692
- private handleAnalyze;
693
- /**
694
- * POST /evolution/record
695
- * Record gene execution outcome.
696
- */
697
- private handleRecord;
1212
+
1213
+ interface LocalServerOptions {
1214
+ port: number;
1215
+ /** Snapshot getter — runner provides current state on demand. */
1216
+ getState: () => LocalServerState;
698
1217
  /**
699
- * POST /evolution/genes
700
- * Create a new gene.
1218
+ * Optional dispatch sink called when POST /v1/runs receives a task envelope
1219
+ * from the sandbox controller. Phase 1 contract: the sink is fire-and-forget;
1220
+ * daemon acks immediately with `{ runId, status: 'accepted' }`. Cloud-side
1221
+ * task lifecycle (status → done/failed) is closed asynchronously by the
1222
+ * daemon's WS upstream channel in S4 follow-up. The runner provides this
1223
+ * sink; tests may pass a stub.
701
1224
  */
702
- private handleCreateGene;
1225
+ onDispatch?: (payload: DispatchPayload, runId: string) => void;
703
1226
  /**
704
- * GET /evolution/genes
705
- * Query available genes.
1227
+ * Install or update one hosted agent/profile in the daemon's local mirror.
1228
+ * The runner persists it, reloads hostedAgents, and redeclares to cloud.
706
1229
  */
707
- private handleQueryGenes;
1230
+ onInstallAgent?: (payload: InstallAgentPayload) => Promise<InstallAgentResult>;
708
1231
  /**
709
- * GET /evolution/personality
710
- * Get agent personality.
1232
+ * Filesystem root for snapshot manifests. Default `/workspace`. POST
1233
+ * /v1/snapshot walks this tree, computes per-file sha256 + size + mtime,
1234
+ * and returns the manifest. Cloud-side persistence (POST
1235
+ * /api/sandboxes/:id/snapshot/manifest) is a separate step in Phase 1.
711
1236
  */
712
- private handleGetPersonality;
1237
+ snapshotRoot?: string;
713
1238
  /**
714
- * POST /evolution/distill
715
- * Trigger gene distillation.
1239
+ * Optional first-pass handler for `/local/memory/*` routes. Returns true
1240
+ * if the request was handled (response written); false to let the normal
1241
+ * route table fall through. Wired by the daemon runner from
1242
+ * `attachMemoryRpc()` in `daemon/memory/rpc.ts` when memory is enabled.
1243
+ * When set, /healthz reports `memoryReady: true`.
716
1244
  */
717
- private handleDistill;
1245
+ attachMemory?: (req: IncomingMessage, res: ServerResponse) => Promise<boolean>;
718
1246
  /**
719
- * GET /evolution/unmatched
720
- * Get unmatched signals (evolution frontier).
1247
+ * Optional handler for `POST /local/asset/write` — agent-gen adapter
1248
+ * (daemon/asset/origin/agent-gen.ts). Synchronous: agent posts bytes,
1249
+ * daemon uploads to cloud, returns prismer:// URI in one round-trip.
1250
+ * Sink receives the parsed JSON body and returns a structured result.
721
1251
  */
722
- private handleGetUnmatched;
723
- private parseCloudResponse;
724
- private callCloudAnalyze;
725
- private callCloudRecord;
726
- private callCloudCreateGene;
727
- private callCloudQueryGenes;
728
- private callCloudGetPersonality;
729
- private callCloudDistill;
730
- private callCloudGetUnmatched;
731
- private triggerLocalDistillation;
732
- private extractSignalsLocal;
1252
+ onAssetWrite?: (body: unknown) => Promise<AssetWriteHandlerResult>;
1253
+ }
1254
+ interface AssetWriteHandlerResult {
1255
+ status: 200 | 400 | 502;
1256
+ body: unknown;
733
1257
  }
734
-
735
1258
  /**
736
- * Prismer Runtime Task Router
1259
+ * Dispatch envelope received from the sandbox controller proxy. The schema
1260
+ * mirrors `DaemonDispatchSchema` in `infra/sandbox-controller/src/api/v1/
1261
+ * containers.ts` — kept permissive so the controller can pass through any
1262
+ * future fields without a daemon-side bump.
737
1263
  *
738
- * v1.9.0 Task Router: HTTP API for routing tasks to agents,
739
- * capability matching, and SSE streaming of task state changes.
740
- *
741
- * Integrates with:
742
- * - Cloud API (/api/im/tasks) for persistent task store
743
- * - AgentSupervisor for local agent status
744
- * - EventBus for SSE streaming
745
- *
746
- * Design reference: docs/version190/IMPLEMENTATION-PLAN-RUNTIME-GAP.md §P1
747
- */
748
-
749
- /**
750
- * Task routing state machine per §16.4.
751
- *
752
- * State transitions:
753
- * created → dispatching → step_running → step_completed → (next step or completed)
754
- * step_running → step_failed → retrying (×3) → retry_exhausted → needs_human
755
- * step_running → step_timeout → rerouting → step_running (new agent)
1264
+ * Phase 1 escape hatch: when `shellCommand` is present, the daemon spawns
1265
+ * it via `bash -c` with cwd=/workspace as a fire-and-forget subprocess.
1266
+ * This bridges the gap until S4 wires the WS-mediated adapter dispatch +
1267
+ * completion path. Production traffic does NOT set shellCommand — task
1268
+ * service forwards it only when present in IMTask.metadata.shellCommand
1269
+ * (a test escape hatch).
756
1270
  */
757
- declare enum TaskRouteState {
758
- Created = "created",
759
- Dispatching = "dispatching",
760
- StepRunning = "step_running",
761
- StepCompleted = "step_completed",
762
- StepFailed = "step_failed",
763
- StepTimeout = "step_timeout",
764
- Retrying = "retrying",
765
- RetryExhausted = "retry_exhausted",
766
- NeedsHuman = "needs_human",
767
- Rerouting = "rerouting",
768
- Completed = "completed",
769
- Cancelled = "cancelled"
770
- }
771
- interface TaskRouterOptions {
772
- eventBus: EventBus;
773
- supervisor: AgentSupervisor;
774
- cloudBaseUrl?: string;
775
- apiToken?: string;
776
- }
777
- interface TaskInfo {
778
- id: string;
779
- title: string;
780
- status: TaskRouteState | string;
781
- requiresCapability?: string | null;
782
- runtimeRoute?: unknown[] | null;
783
- assigneeId?: string | null;
784
- progress?: number | null;
785
- statusMessage?: string | null;
786
- createdAt: string;
787
- updatedAt: string;
788
- }
789
- interface RouteTaskRequest {
790
- taskId?: string;
791
- priority?: 'high' | 'normal' | 'low';
792
- preferredAgentId?: string;
793
- }
794
- interface RouteTaskResponse {
1271
+ interface DispatchPayload {
795
1272
  taskId: string;
796
- agentId: string;
797
- capability: string;
798
- stepIdx: number;
799
- totalSteps: number;
800
- }
801
- interface AssignTaskRequest {
802
- agentId: string;
803
- taskId?: string;
804
- }
805
- interface StepCompletedRequest {
806
- taskId?: string;
807
- stepId: string;
808
- result?: Record<string, unknown>;
809
- metadata?: Record<string, unknown>;
810
- }
811
- interface CancelTaskRequest {
812
- taskId?: string;
813
- reason: string;
814
- }
815
- interface StepFailedRequest {
816
- taskId?: string;
817
- stepId: string;
818
- error?: string;
819
- metadata?: Record<string, unknown>;
820
- }
821
- interface StepTimeoutRequest {
822
- taskId?: string;
823
- stepId: string;
824
- capability?: string;
825
- metadata?: Record<string, unknown>;
826
- }
827
- declare class TaskRouter {
828
- private readonly _bus;
829
- private readonly _supervisor;
830
- private readonly _cloudBaseUrl;
831
- private readonly _apiToken;
832
- private _sseClients;
833
- /** Per-step retry counters: key = `${taskId}:${stepId}` */
834
- private readonly _retryCounters;
835
- constructor(opts: TaskRouterOptions);
836
- /**
837
- * Register HTTP routes with the daemon server.
838
- * Call this from daemon-http.ts to expose task router endpoints.
839
- */
840
- registerRoutes(server: {
841
- registerRoute: (method: 'GET' | 'POST' | 'PATCH' | 'DELETE', path: string, handler: RouteHandler) => void;
842
- }): void;
843
- private _handleRouteTask;
844
- private _handleAssignTask;
845
- private _handleStepCompleted;
846
- private _handleCancelTask;
847
- private _handleStepFailed;
848
- private _handleStepTimeout;
849
- private _handleGetTask;
850
- private _handleListTasks;
851
- /** Publish a task.state event with the given TaskRouteState and optional extra fields. */
852
- private _publishState;
853
- private _callCloudApi;
854
- }
855
-
856
- type KeychainBackend = 'macos-keychain' | 'libsecret' | 'pass' | 'encrypted-file';
857
- interface KeychainAdapter {
858
- name: KeychainBackend;
859
- available(): Promise<boolean>;
860
- get(service: string, account: string): Promise<string | null>;
861
- set(service: string, account: string, value: string): Promise<void>;
862
- delete(service: string, account: string): Promise<void>;
863
- list(service: string): Promise<string[]>;
864
- }
865
- interface KeychainOptions {
866
- preferredBackend?: KeychainBackend;
867
- masterPassphrase?: string;
868
- encryptedFilePath?: string;
869
- }
870
- declare class NoKeychainBackendError extends Error {
871
- constructor();
872
- }
873
- declare class KeychainOperationError extends Error {
874
- constructor(op: string, cause: unknown);
875
- }
876
- declare class Keychain {
877
- private readonly opts;
878
- private resolvedBackend;
879
- private detecting;
880
- constructor(opts?: KeychainOptions);
881
- backend(): Promise<KeychainAdapter>;
882
- private buildCandidateList;
883
- private makeAdapter;
884
- get(service: string, account: string): Promise<string | null>;
885
- set(service: string, account: string, value: string): Promise<void>;
886
- delete(service: string, account: string): Promise<void>;
887
- list(service: string): Promise<string[]>;
888
- }
889
-
890
- interface PrismerConfig {
891
- apiKey?: string;
892
- apiBase?: string;
893
- daemon?: {
894
- host?: string;
895
- port?: number;
1273
+ adapter?: string;
1274
+ prompt?: string;
1275
+ env?: Record<string, string>;
1276
+ shellCommand?: string;
1277
+ [k: string]: unknown;
1278
+ }
1279
+ interface InstallAgentPayload {
1280
+ workspaceId: string;
1281
+ imUserId: string;
1282
+ name: string;
1283
+ adapterName: string;
1284
+ capabilities: string[];
1285
+ profile: {
1286
+ id: string;
1287
+ name: string;
1288
+ adapterName: string;
1289
+ config: Record<string, unknown>;
1290
+ version: number;
896
1291
  };
897
- agents?: Record<string, {
898
- enabled?: boolean;
899
- apiKey?: string;
900
- [key: string]: unknown;
901
- }>;
902
- [key: string]: unknown;
903
- }
904
- interface LoadConfigOptions {
905
- path?: string;
906
- keychain?: Keychain;
907
- resolvePlaceholders?: boolean;
908
- }
909
- declare class ConfigError extends Error {
910
- constructor(message: string);
911
- }
912
- declare function parseKeyringPlaceholder(value: string): {
913
- service: string;
914
- account: string;
915
- } | null;
916
- declare function loadConfig(opts?: LoadConfigOptions): Promise<PrismerConfig>;
917
- declare function writeConfig(config: PrismerConfig, opts?: {
918
- path?: string;
919
- }): Promise<void>;
920
-
921
- interface MigrateStep {
922
- level: 'info' | 'ok' | 'warn' | 'error';
923
- message: string;
924
- detail?: string;
925
- }
926
- interface MigrateSecretsOptions {
927
- configPath?: string;
928
- keychain: Keychain;
929
- dryRun?: boolean;
930
- onStep?: (step: MigrateStep) => void;
931
- }
932
- interface MigrateSecretsResult {
933
- migrated: Array<{
934
- path: string;
935
- service: string;
936
- account: string;
937
- }>;
938
- skipped: Array<{
939
- path: string;
940
- reason: string;
941
- }>;
942
- errors: Array<{
943
- path: string;
944
- error: string;
945
- }>;
946
1292
  }
947
- declare function migrateSecrets(opts: MigrateSecretsOptions): Promise<MigrateSecretsResult>;
948
-
949
- interface DaemonRunnerOptions {
950
- host?: string;
951
- port?: number;
952
- pidFile?: string;
953
- dataDir?: string;
954
- installSignalHandlers?: boolean;
955
- authBearer?: string;
956
- apiKey?: string;
957
- daemonId?: string;
958
- userId?: string;
959
- enableTransport?: boolean;
960
- forceProbe?: boolean;
961
- workspace?: string;
962
- /**
963
- * v1.9.0 B.7.a — cloud base URL (https://prismer.cloud for prod,
964
- * https://cloud.prismer.dev for test). Forwarded to EvolutionGateway so
965
- * daemon→cloud calls hit the same env the API key was issued against.
966
- */
967
- cloudApiBase?: string;
968
- }
969
- interface DaemonRunnerHandle {
970
- stop(): Promise<void>;
971
- readonly url: string;
972
- readonly pid: number;
973
- readonly dataDir: string;
974
- }
975
- declare function startDaemonRunner(opts?: DaemonRunnerOptions): Promise<DaemonRunnerHandle>;
976
-
977
- declare function assertBrandVoice(text: string, label?: string): void;
978
-
979
- type OutputMode = 'pretty' | 'json' | 'quiet';
980
- interface UIOptions {
981
- mode?: OutputMode;
982
- color?: boolean;
983
- stream?: NodeJS.WritableStream;
984
- errStream?: NodeJS.WritableStream;
985
- }
986
- interface TableRow {
987
- [column: string]: string;
988
- }
989
- interface TableOptions {
990
- columns: string[];
991
- maxWidth?: number;
992
- }
993
- interface LegacyTableOptions {
994
- columns: string[];
995
- rows: TableRow[];
996
- maxWidth?: number;
997
- }
998
-
999
- declare class UI {
1000
- readonly mode: OutputMode;
1001
- readonly colorEnabled: boolean;
1002
- private readonly stream;
1003
- private readonly errStream;
1004
- constructor(opts?: UIOptions);
1005
- private ansi;
1006
- private green;
1007
- private red;
1008
- private yellow;
1009
- private cyan;
1010
- private dim;
1011
- private bold;
1012
- private gray;
1013
- private brandMark;
1014
- private colorBrandLine;
1015
- write(text: string): void;
1016
- writeErr(text: string): void;
1017
- header(text: string): void;
1018
- smallHeader(subtitle?: string): void;
1019
- banner(subtitle?: string, opts?: {
1020
- full?: boolean;
1021
- }): void;
1022
- blank(): void;
1023
- line(text: string): void;
1024
- info(text: string): void;
1025
- secondary(text: string, indent?: number): void;
1026
- tip(text: string): void;
1027
- next(text: string): void;
1028
- ok(text: string, detail?: string): void;
1029
- success(text: string, detail?: string): void;
1030
- fail(text: string, detail?: string): void;
1031
- online(text: string): void;
1032
- offline(text: string): void;
1033
- notInstalled(text: string): void;
1034
- pending(text: string): void;
1035
- warn(text: string, detail?: string): void;
1036
- error(what: string, cause?: string, fix?: string): void;
1037
- table(rows: TableRow[], opts: TableOptions): void;
1038
- table(opts: LegacyTableOptions): void;
1039
- spinner(text: string): {
1040
- update(t: string): void;
1041
- stop(final?: string): void;
1042
- };
1043
- progress(text: string, total: number): {
1044
- update(current: number, detail?: string): void;
1045
- stop(final?: string): void;
1293
+ interface InstallAgentResult {
1294
+ ok: true;
1295
+ daemonId: string;
1296
+ installedAgent: {
1297
+ imUserId: string;
1298
+ name: string;
1299
+ adapterName: string;
1300
+ profileId: string;
1046
1301
  };
1047
- json(payload: unknown, opts?: {
1048
- pretty?: boolean;
1049
- }): void;
1050
- result<T>(pretty: () => void, jsonPayload: T): void;
1051
- }
1052
- declare function getUI(): UI;
1053
- declare function setUI(ui: UI): void;
1054
- declare function applyCommonFlags(argv: string[]): {
1055
- mode: OutputMode;
1056
- color: boolean;
1057
- restArgv: string[];
1058
- };
1059
-
1060
- interface CliContext {
1061
- ui: UI;
1062
- keychain: Keychain;
1063
- cwd: string;
1064
- argv: string[];
1065
- }
1066
- declare function createCliContext(opts?: {
1067
- argv?: string[];
1068
- ui?: UI;
1069
- }): Promise<CliContext>;
1070
-
1071
- interface AgentCatalogEntry {
1072
- name: string;
1073
- displayName: string;
1074
- packPackage: string;
1075
- packVersionRange: string;
1076
- hookConfigPath: string;
1077
- mcpConfigPath?: string;
1078
- upstreamBinary: string;
1079
- upstreamVersionRange?: string;
1080
- localSourcePath?: string;
1081
- installCommand?: string;
1082
- tiersSupported: number[];
1083
- capabilityTags: string[];
1084
- detect: () => Promise<{
1085
- found: boolean;
1086
- binaryPath?: string;
1087
- version?: string;
1302
+ hostedAgents: Array<{
1303
+ imUserId: string;
1304
+ name: string;
1305
+ adapterName: string;
1088
1306
  }>;
1089
1307
  }
1090
- declare const AGENT_CATALOG: AgentCatalogEntry[];
1091
- declare function getAgent(name: string): AgentCatalogEntry | undefined;
1092
-
1093
- type HookFormat = 'v1.8' | 'v1.9';
1094
- interface HookEntry {
1095
- command?: string;
1096
- webhook?: string;
1097
- enabled?: boolean;
1098
- [key: string]: unknown;
1099
- }
1100
- interface HookConfig {
1101
- hooks: Record<string, HookEntry | HookEntry[]>;
1102
- [key: string]: unknown;
1103
- }
1104
- interface MergeResult {
1105
- merged: HookConfig;
1106
- added: string[];
1107
- preserved: string[];
1108
- replaced: string[];
1109
- backupPath?: string;
1110
- }
1111
- interface MergeOptions {
1112
- daemonUrl: string;
1113
- dryRun?: boolean;
1114
- /** Absolute path to the installed claude-code-plugin root (contains hooks/para-emit.mjs). */
1115
- pluginRoot?: string;
1116
- }
1117
- declare function mergeHooks(existing: HookConfig | null, opts: MergeOptions): MergeResult;
1118
- declare function readHookConfig(filePath: string): Promise<HookConfig | null>;
1119
- declare function writeHookConfig(filePath: string, cfg: HookConfig): Promise<void>;
1120
- declare function installHooks(configPath: string, existing: HookConfig | null, opts: MergeOptions): Promise<MergeResult>;
1121
- declare function rollbackHooks(configPath: string): Promise<{
1122
- restored: boolean;
1123
- fromBackup: string | null;
1124
- }>;
1125
-
1126
- interface PairOffer {
1127
- offer: string;
1128
- uri: string;
1129
- expiresAt: number;
1130
- relayUrl?: string;
1131
- lanHost?: string;
1132
- lanPort?: number;
1133
- }
1134
- interface PairedDevice {
1135
- id: string;
1136
- name: string;
1137
- method: 'qr' | 'api-key';
1138
- transport: 'lan' | 'relay';
1139
- lastSeenAt: number;
1140
- pairedAt: number;
1141
- }
1142
-
1143
- interface MigrateOptions {
1144
- dryRun?: boolean;
1145
- configPath?: string;
1146
- homeDir?: string;
1147
- yes?: boolean;
1148
- /**
1149
- * Dependency-injection hook for tests — override the TTY confirmation prompt.
1150
- * When provided, replaces the readline-based promptConfirm call entirely.
1151
- */
1152
- confirmer?: () => Promise<boolean>;
1153
- }
1154
- interface MigrateResult {
1155
- apiKeyMigrated: boolean;
1156
- hooksBackedUp: string[];
1157
- hooksRedirected: string[];
1158
- memoryFilesImported: number;
1159
- networkEndpointsRewritten: Array<{
1160
- file: string;
1161
- rewrites: number;
1162
- backupPath: string;
1163
- }>;
1164
- errors: Array<{
1165
- step: string;
1166
- error: string;
1308
+ interface LocalServerState {
1309
+ daemonId: string;
1310
+ cloudBaseUrl?: string;
1311
+ workspaceId?: string | null;
1312
+ pid: number;
1313
+ startedAt: number;
1314
+ wsConnected: boolean;
1315
+ hostedAgents: Array<{
1316
+ imUserId: string;
1317
+ name: string;
1318
+ adapterName: string;
1167
1319
  }>;
1168
- }
1169
-
1170
- interface EncryptionConfig {
1171
- enabled: boolean;
1172
- key?: Buffer;
1173
- }
1174
- interface MemoryFile {
1175
- id: string;
1176
- ownerId: string;
1177
- ownerType: 'user' | 'agent';
1178
- scope: string;
1179
- path: string;
1180
- content: string;
1181
- version: number;
1182
- memoryType?: string;
1183
- description?: string;
1184
- contentHash: string;
1185
- createdAt: string;
1186
- updatedAt: string;
1187
- stale: boolean;
1188
- }
1189
- interface MemoryFileVersion {
1190
- id: string;
1191
- fileId: string;
1192
- version: number;
1193
- content: string;
1194
- createdAt: string;
1195
- }
1196
- interface DreamCompaction {
1197
- id: string;
1198
- ownerId: string;
1199
- scope: string;
1200
- summary: string;
1201
- createdAt: string;
1202
- }
1203
- interface WriteMemoryFileInput {
1204
- ownerId: string;
1205
- ownerType: 'user' | 'agent';
1206
- scope?: string;
1207
- path: string;
1208
- content: string;
1209
- memoryType?: string;
1210
- description?: string;
1211
- }
1212
- interface MemoryFileFilters {
1213
- ownerId?: string;
1214
- scope?: string;
1215
- path?: string;
1216
- memoryType?: string;
1217
- stale?: boolean;
1218
- limit?: number;
1219
- offset?: number;
1220
- }
1221
- interface MemorySearchResult extends MemoryFile {
1222
- relevance: number;
1223
- snippet?: string;
1224
- }
1225
- interface MemoryStats {
1226
- fileCount: number;
1227
- totalSize: number;
1228
- totalBytes: number;
1229
- staleFiles: number;
1230
- ftsIndexed: number;
1231
- recallP95: number;
1232
- }
1233
- declare class MemoryDB {
1234
- private readonly filePath;
1235
- private readonly db;
1236
- private readonly crypto;
1237
- constructor(_encryptionConfig?: EncryptionConfig, opts?: {
1238
- filePath?: string;
1239
- });
1240
- writeMemoryFile(input: WriteMemoryFileInput): MemoryFile;
1241
- getMemoryFileById(id: string): MemoryFile | null;
1242
- deleteMemoryFile(id: string): boolean;
1243
- listMemoryFiles(filters?: MemoryFileFilters): MemoryFile[];
1244
- searchMemoryFiles(keyword: string, filters?: MemoryFileFilters): MemorySearchResult[];
1245
- getStats(ownerId?: string): MemoryStats;
1246
- close(): void;
1247
- }
1248
- declare function getMemoryDB(): MemoryDB;
1249
- declare function closeMemoryDB(): void;
1250
- declare function generateSalt(): Buffer;
1251
- declare function deriveKey(password: string, salt: Buffer): Buffer;
1252
- declare function encrypt(plaintext: string, key: Buffer): string;
1253
- declare function decrypt(encoded: string, key: Buffer): string;
1254
-
1255
- interface WriteMemoryRequest {
1256
- ownerId: string;
1257
- ownerType: 'user' | 'agent';
1258
- path: string;
1259
- content: string;
1260
- scope?: string;
1261
- memoryType?: string;
1262
- description?: string;
1263
- encrypt?: boolean;
1264
- }
1265
- interface RecallRequest {
1266
- keyword: string;
1267
- ownerId?: string;
1268
- scope?: string;
1269
- limit?: number;
1270
- useCloudFallback?: boolean;
1271
- }
1272
- interface ListMemoryRequest {
1273
- ownerId?: string;
1274
- scope?: string;
1275
- path?: string;
1276
- memoryType?: string;
1277
- stale?: boolean;
1278
- limit?: number;
1279
- offset?: number;
1280
- }
1281
- interface MemoryResponse {
1282
- success: boolean;
1283
- data?: unknown;
1284
- error?: {
1285
- code: string;
1286
- message: string;
1320
+ runningTaskIds: string[];
1321
+ observability?: {
1322
+ adapters?: Record<string, unknown>;
1323
+ lastTaskError?: {
1324
+ taskId: string;
1325
+ message: string;
1326
+ at: string;
1327
+ };
1287
1328
  };
1288
1329
  }
1289
-
1290
- interface DreamResult {
1291
- ok: boolean;
1292
- compactedFiles: number;
1293
- summary?: DreamCompaction;
1294
- }
1295
- interface DreamSchedulerOptions {
1296
- ownerId?: string;
1297
- scope?: string;
1298
- intervalMs?: number;
1299
- }
1300
- declare class DreamScheduler {
1301
- private readonly opts;
1302
- private timer;
1303
- constructor(opts?: DreamSchedulerOptions);
1304
- start(): void;
1305
- stop(): void;
1306
- }
1307
- declare function createDreamScheduler(opts?: DreamSchedulerOptions): DreamScheduler;
1308
- declare function runDream(opts?: DreamSchedulerOptions): Promise<DreamResult>;
1309
-
1310
- /**
1311
- * PARA L8 — Session Export Trace Writer
1312
- *
1313
- * Per PARA spec §4.2 L8 (docs/version190/03-para-spec.md):
1314
- * session export 必须是 pre-compaction 的原始 trace。compaction 会把历史消息替换为 summary——
1315
- * 这种 adapter 必须额外持久化原始 trace 到 `.prismer/trace/<sessionId>.jsonl.zst`
1316
- * (append-only + zstd 压缩),compaction 只影响 working memory,不动 trace。
1317
- *
1318
- * Implementation:
1319
- * - Each `append()` JSON-stringifies the envelope, adds '\n', zstd-compresses it,
1320
- * then sync-appends the compressed bytes to the trace file.
1321
- * - zstd supports concatenated frames — multiple independent compressed blobs
1322
- * catenated together decompress back to the concatenation of their inputs.
1323
- * This is what makes append-only valid without holding a long-lived stream.
1324
- * - File mode 0o600 (trace may contain sensitive payloads).
1325
- * - Node 22+ gives us native `zlib.zstdCompressSync`. Older Node: append is a no-op
1326
- * and a single stderr warning is emitted at construction time. We do NOT throw
1327
- * or crash the daemon — daemon must still start on Node 20.
1328
- */
1329
-
1330
- interface TraceWriterOptions {
1331
- sessionId: string;
1330
+ declare class LocalServer {
1331
+ private opts;
1332
+ private server?;
1333
+ constructor(opts: LocalServerOptions);
1334
+ start(): Promise<void>;
1335
+ stop(): Promise<void>;
1336
+ private route;
1337
+ private routeStandard;
1332
1338
  /**
1333
- * Override home directory; trace file will live at
1334
- * `<homeDir>/.prismer/trace/<sessionId>.jsonl.zst`. Defaults to `os.homedir()`.
1339
+ * POST /v1/agents/install cloud/controller installs one hosted agent on
1340
+ * this daemon. The payload is intentionally explicit and idempotent: the
1341
+ * runner upserts local `agents` and `agent_profiles` rows, reloads the
1342
+ * in-memory declaration set, then sends `agent.host.declare` immediately.
1335
1343
  */
1336
- homeDir?: string;
1344
+ private handleInstallAgent;
1337
1345
  /**
1338
- * Explicit trace directory. Overrides homeDir if provided. Useful when the
1339
- * daemon runs with a non-default `dataDir`.
1346
+ * POST /v1/snapshot daemon-first FS manifest snapshot.
1347
+ *
1348
+ * Walks `snapshotRoot` (default `/workspace`), computes per-file
1349
+ * sha256 + sizeBytes + mtime, returns `{ files: [...], rootPath }`.
1350
+ * Skips dotfiles + the `_outbox/_uploaded` reserved subdir. Phase 1
1351
+ * caller (cloud or controller) is responsible for POSTing the result
1352
+ * to `/api/sandboxes/:id/snapshot/manifest`.
1340
1353
  */
1341
- traceDir?: string;
1342
- }
1343
- declare class TraceWriter {
1344
- private readonly _sessionId;
1345
- private readonly _filePath;
1346
- private readonly _compress;
1347
- private _bytesWritten;
1348
- private _warnedAppendError;
1349
- private _closed;
1350
- constructor(opts: TraceWriterOptions);
1351
- /** Absolute path of the trace file: `<home>/.prismer/trace/<sessionId>.jsonl.zst`. */
1352
- get filePath(): string;
1353
- /** Session ID this writer is bound to. */
1354
- get sessionId(): string;
1355
- /** Total compressed bytes appended since construction. */
1356
- get bytesWritten(): number;
1357
- /** True if zstd is unavailable (Node < 22); append() is a no-op. */
1358
- get disabled(): boolean;
1354
+ private handleSnapshot;
1359
1355
  /**
1360
- * Synchronously append one event envelope to the trace as a new zstd frame.
1361
- * Safe to call after close() — becomes a no-op. Errors are logged once and swallowed.
1356
+ * POST /local/asset/write agent-gen adapter RPC.
1357
+ *
1358
+ * Body: { workspaceId, bytes (base64), filename, mime?, path?, description?, sourceAgentImUserId? }
1359
+ * Response 200: { assetId, contentHash, prismerUri }
1360
+ * 400 on validation failure; 502 on cloud upstream failure; 501 if no sink wired.
1362
1361
  */
1363
- append(event: EventBusEnvelope): void;
1362
+ private handleAssetWrite;
1364
1363
  /**
1365
- * Close the writer. Currently a no-op because we use sync append with one
1366
- * self-contained zstd frame per event. The API exists so callers can switch to
1367
- * a streaming implementation later without changing integration code.
1364
+ * POST /v1/runs dispatch ack-only (Cloud 3 S3 Phase 1).
1365
+ *
1366
+ * The sandbox controller resolves the pod IP and POSTs a task envelope here.
1367
+ * Phase 1 just acknowledges receipt and forwards the payload to onDispatch
1368
+ * (the runner). Actual task execution + cloud-side completion are wired in
1369
+ * S4 (daemon WS upstream emits task.dispatch.reply once the adapter finishes).
1370
+ *
1371
+ * Response on success: 202 + `{ runId, status: 'accepted' }`.
1372
+ * Bad JSON / missing taskId: 400. Internal error: 500.
1368
1373
  */
1369
- close(): void;
1370
- }
1371
- interface TraceWriterManagerOptions {
1372
- /** Override home directory (tests). Defaults to os.homedir(). */
1373
- homeDir?: string;
1374
- /** Explicit trace directory — takes precedence over homeDir when set. */
1375
- traceDir?: string;
1376
- }
1377
- /**
1378
- * Routes events from the EventBus (`bus.subscribe('*', ...)`) to the per-session
1379
- * TraceWriter.
1380
- *
1381
- * agent.session.started → open writer for payload.sessionId
1382
- * agent.session.ended → close + remove writer
1383
- * any other event → pick the writer by payload.sessionId if present;
1384
- * events without sessionId are skipped (not guessed)
1385
- */
1386
- declare class TraceWriterManager {
1387
- private readonly _writers;
1388
- private readonly _homeDir;
1389
- private readonly _traceDir;
1390
- private readonly _warnedOpenFailures;
1391
- constructor(opts?: TraceWriterManagerOptions);
1392
- /** Event-bus subscription handler. Pass via `bus.subscribe('*', mgr.handle)`. */
1393
- readonly handle: (ev: EventBusEnvelope) => void;
1394
- /** Close all writers — call on daemon shutdown. */
1395
- shutdown(): void;
1396
- /** Test / introspection helper. */
1397
- activeSessions(): string[];
1398
- private _openWriter;
1399
- private _closeWriter;
1374
+ private handleDispatch;
1400
1375
  }
1401
1376
 
1402
- /**
1403
- * Prismer Runtime E2EE Crypto Module (v1.9.0)
1404
- *
1405
- * End-to-end encryption for LAN direct connections.
1406
- * Uses X25519 for key exchange and XSalsa20-Poly1305 for encryption.
1407
- *
1408
- * Security model:
1409
- * - Daemon and mobile each generate X25519 keypair
1410
- * - Keys are exchanged via QR code or relay (out-of-band)
1411
- * - Each session derives unique encryption key via HKDF
1412
- * - All LAN traffic is encrypted - relay never sees plaintext
1413
- *
1414
- * This implementation uses Node.js built-in crypto module for
1415
- * compatibility and no external dependencies.
1416
- */
1417
- interface KeyPair {
1418
- publicKey: Buffer;
1419
- privateKey: Buffer;
1420
- }
1421
- interface E2EEContext {
1422
- localKeyPair: KeyPair;
1423
- remotePublicKey: Buffer;
1424
- sharedSecret: Buffer;
1425
- sendKey: Buffer;
1426
- recvKey: Buffer;
1427
- sendNonce: Buffer;
1428
- recvNonce: Buffer;
1429
- }
1430
- interface EncryptedEnvelope {
1431
- version: number;
1432
- nonce: Buffer;
1433
- ciphertext: Buffer;
1434
- authTag: Buffer;
1435
- }
1436
- /**
1437
- * Generate X25519 keypair for E2EE
1438
- */
1439
- declare function generateKeyPair(): KeyPair;
1440
- /**
1441
- * Derive shared secret from local private key and remote public key (X25519)
1442
- */
1443
- declare function deriveSharedSecret(localPrivateKey: Buffer, remotePublicKey: Buffer): Buffer;
1444
- /**
1445
- * Derive send/recv keys from shared secret using HKDF-SHA256.
1446
- *
1447
- * Role is determined by lexicographic comparison of the two public keys
1448
- * so both peers agree without an explicit role flag. Without the keys
1449
- * (legacy single-context callers), defaults to the "A" role.
1450
- */
1451
- declare function deriveSessionKeys(sharedSecret: Buffer, localPublicKey?: Buffer, remotePublicKey?: Buffer): {
1452
- sendKey: Buffer;
1453
- recvKey: Buffer;
1454
- };
1455
- /**
1456
- * Serialize encrypted envelope to buffer
1457
- */
1458
- declare function serializeEnvelope(envelope: EncryptedEnvelope): Buffer;
1459
- /**
1460
- * Deserialize encrypted envelope from buffer
1461
- */
1462
- declare function deserializeEnvelope(data: Buffer): EncryptedEnvelope;
1463
- /**
1464
- * Create E2EE context for a session
1465
- */
1466
- declare function createE2EEContext(localKeyPair: KeyPair, remotePublicKey: Buffer): E2EEContext;
1467
- /**
1468
- * Encrypt message using E2EE context
1469
- */
1470
- declare function encryptMessage(context: E2EEContext, plaintext: Buffer): Buffer;
1471
- /**
1472
- * Decrypt message using E2EE context
1473
- */
1474
- declare function decryptMessage(context: E2EEContext, data: Buffer): Buffer;
1475
-
1476
- /**
1477
- * Prismer Runtime — E2EE Key Storage (v1.9.0)
1478
- *
1479
- * Secure storage for E2EE keys on daemon side.
1480
- * Uses keychain backend with encrypted JSON fallback for offline/keyless systems.
1481
- *
1482
- * Storage model:
1483
- * - Primary: Keychain (macOS Keychain / libsecret / pass)
1484
- * - Fallback: Encrypted JSON file (~/.prismer/e2ee-keys.json)
1485
- * - Per-session keys with TTL (30 min default)
1486
- * - Automatic cleanup of expired keys
1487
- *
1488
- * Security:
1489
- * - Keys encrypted at rest (keychain or encrypted file)
1490
- * - Master passphrase required for file fallback
1491
- * - Automatic key rotation on session expiry
1492
- * - Forward secrecy: ephemeral keys discarded after session
1493
- */
1494
-
1495
- interface E2EEKeyEntry {
1496
- /** Session ID (userId:daemonId or userId:deviceId) */
1497
- sessionId: string;
1498
- /** User ID who owns this key */
1499
- userId: string;
1500
- /** Daemon ID or Device ID */
1501
- endpointId: string;
1502
- /** Local keypair (ephemeral) */
1503
- keyPair: {
1504
- publicKey: string;
1505
- privateKey: string;
1506
- };
1507
- /** Remote public key */
1508
- remotePublicKey: string;
1509
- /** When this key was created */
1510
- createdAt: number;
1511
- /** When this key expires (unix timestamp) */
1512
- expiresAt: number;
1513
- /** Current sequence number */
1514
- seq: number;
1515
- }
1516
- interface E2EEStorageStats {
1517
- totalKeys: number;
1518
- activeKeys: number;
1519
- expiredKeys: number;
1520
- oldestKeyAge: number;
1521
- newestKeyAge: number;
1522
- }
1523
- declare class E2EEKeyStorage {
1524
- private keychain;
1525
- private useEncryptedFile;
1526
- private masterPassphrase?;
1527
- private cleanupInterval?;
1528
- constructor(options?: {
1529
- masterPassphrase?: string;
1530
- });
1531
- /**
1532
- * Initialize storage (try keychain, fallback to encrypted file)
1533
- */
1534
- initialize(): Promise<void>;
1535
- /**
1536
- * Store E2EE key for a session
1537
- */
1538
- storeKey(entry: E2EEKeyEntry): Promise<void>;
1539
- /**
1540
- * Retrieve E2EE key for a session
1541
- */
1542
- getKey(sessionId: string): Promise<E2EEKeyEntry | null>;
1377
+ interface RunnerOptions {
1378
+ /** Override config path; defaults to ~/.prismer. */
1379
+ paths?: ConfigPaths;
1380
+ /** Skip starting the local 127.0.0.1 server (useful in tests). */
1381
+ startLocalServer?: boolean;
1382
+ /** Local server port; defaults to 3210. */
1383
+ localPort?: number;
1384
+ /** Override daemonVersion reported in agent.host.declare. */
1385
+ daemonVersion?: string;
1386
+ /** Pre-loaded config (skip filesystem read). */
1387
+ configOverride?: Config;
1388
+ /** Override built-in adapter list (tests). */
1389
+ adaptersOverride?: AdapterDef[];
1390
+ }
1391
+ declare class Runner extends EventEmitter {
1392
+ private opts;
1393
+ private config;
1394
+ private paths;
1395
+ private db;
1396
+ private cloud;
1397
+ private ws;
1398
+ private syncWorker;
1399
+ private syncQueue;
1400
+ private assetCache;
1401
+ private uriResolver;
1402
+ private registry;
1403
+ private servicePool;
1404
+ private shellConfig;
1405
+ private localServer?;
1406
+ private outboxWatcher?;
1407
+ private memoryWiring?;
1408
+ private state;
1409
+ private startedAt;
1410
+ private workspaceId;
1411
+ private wsConnected;
1412
+ private readonly hostedAgents;
1413
+ private readonly runningTasks;
1414
+ private lastTaskError?;
1415
+ private heartbeatTimer?;
1416
+ private taskReaperTimer?;
1417
+ constructor(opts?: RunnerOptions);
1418
+ start(): Promise<void>;
1419
+ stop(): Promise<void>;
1420
+ isRunning(): boolean;
1543
1421
  /**
1544
- * Remove E2EE key for a session
1422
+ * Phase 1 escape hatch see `DispatchPayload.shellCommand`. Spawns
1423
+ * `bash -c <cmd>` with cwd=/workspace, mirrors output to pod logs.
1424
+ * Fire-and-forget; nothing here writes back to cloud.
1545
1425
  */
1546
- removeKey(sessionId: string): Promise<void>;
1426
+ private runShellCommand;
1427
+ snapshotState(): LocalServerState;
1547
1428
  /**
1548
- * Get all keys for a user (for recovery)
1429
+ * Re-read the local `agents` table (populated by `prismer agent register`)
1430
+ * and re-populate `hostedAgents` map. Profiles per agent come from local
1431
+ * `agent_profiles` table (filled by host.acked sync).
1549
1432
  */
1550
- getAllKeysForUser(userId: string): Promise<E2EEKeyEntry[]>;
1433
+ loadAgentsFromDb(): void;
1551
1434
  /**
1552
- * Get storage statistics
1435
+ * Register an in-process AgentProfile snapshot (called by agent CLI / sync layer).
1436
+ * Used to populate the `agents` list in agent.host.declare.
1553
1437
  */
1554
- getStats(userId?: string): Promise<E2EEStorageStats>;
1555
- /**
1556
- * Cleanup expired keys
1438
+ setHostedAgent(agent: {
1439
+ imUserId: string;
1440
+ name: string;
1441
+ adapterName: string;
1442
+ capabilities: string[];
1443
+ profiles: Array<{
1444
+ id: string;
1445
+ version: number;
1446
+ }>;
1447
+ }): void;
1448
+ private installHostedAgent;
1449
+ private installStaticHostedAgentFromEnv;
1450
+ private wireWsHandlers;
1451
+ private sendDeclare;
1452
+ private handleIncoming;
1453
+ private onHostAcked;
1454
+ private onTaskDispatch;
1455
+ private onTaskCancel;
1456
+ private onAgentChanged;
1457
+ private onAgentProfileChanged;
1458
+ private syncProfileFromCloud;
1459
+ private resolveOwnedAgent;
1460
+ private onWorkspaceChanged;
1461
+ private onWorkspaceFileChanged;
1462
+ private snapshotAdapterObservability;
1463
+ /**
1464
+ * SyncWorker FlushFn — pushes local writes to cloud.
1465
+ *
1466
+ * Maps:
1467
+ * workspace → PATCH /api/im/workspaces/:id
1468
+ * agent_profile → PATCH /api/im/agent_profiles/:id
1469
+ * agent → PATCH /api/im/agents/:imUserId
1470
+ * On 'create' we POST instead. On 'delete' we DELETE.
1557
1471
  */
1558
- cleanupExpiredKeys(): Promise<void>;
1559
1472
  /**
1560
- * Shutdown storage (cleanup interval)
1561
- */
1562
- destroy(): void;
1563
- private ensureStorageFile;
1564
- private readEncryptedFile;
1565
- private writeEncryptedFile;
1566
- private encryptFile;
1567
- private decryptFile;
1568
- private storeInEncryptedFile;
1569
- private getFromEncryptedFile;
1570
- private removeFromEncryptedFile;
1571
- private getAllFromEncryptedFile;
1572
- private storeInKeychain;
1573
- private getFromKeychain;
1574
- private removeFromKeychain;
1575
- private getAllFromKeychain;
1576
- }
1577
- /**
1578
- * Generate session ID for E2EE storage
1579
- * Format: {userId}:{endpointId}
1580
- */
1581
- declare function generateSessionId(userId: string, endpointId: string): string;
1582
- /**
1583
- * Parse session ID into components
1584
- */
1585
- declare function parseSessionId(sessionId: string): {
1586
- userId: string;
1587
- endpointId: string;
1588
- };
1589
- /**
1590
- * Create E2EE key entry from runtime context
1591
- */
1592
- declare function createKeyEntry(userId: string, endpointId: string, keyPair: KeyPair, remotePublicKey: string, seq?: number): E2EEKeyEntry;
1593
-
1594
- /**
1595
- * secret-scan — local-side secret detection for memory team sync.
1596
- *
1597
- * Mirror of src/im/services/memory/team-sync.service.ts SECRET_PATTERNS.
1598
- * Kept in sync by convention — if you add a pattern there, add it here.
1599
- *
1600
- * Used by memory-team-sync.ts before POSTing bytes to the server (so secrets
1601
- * never leave the device). Server has its own independent scanner (defense
1602
- * in depth).
1603
- *
1604
- * Design: docs/version190/14e-memory-cc-compat.md §8.5
1605
- */
1606
- interface SecretHit {
1607
- /** Pattern name (e.g. 'aws-access-key'). */
1608
- pattern: string;
1609
- /** Truncated match (never more than 80 chars — no full secret echo). */
1610
- match: string;
1611
- /** 1-based line number. */
1612
- line: number;
1613
- /**
1614
- * If true, this hit is informational only — not strong enough evidence
1615
- * to block a push on its own (e.g. JWT-shaped strings).
1473
+ * SyncWorker FlushFn pushes one local sync row to cloud via CloudClient.
1474
+ *
1475
+ * resource_type × operation → endpoint:
1476
+ * workspace.create → POST /api/im/workspaces
1477
+ * workspace.update → PATCH /api/im/workspaces/:id
1478
+ * workspace.delete → DELETE /api/im/workspaces/:id
1479
+ * agent.create → POST /api/im/register (the only public path
1480
+ * that creates an
1481
+ * IMUser of role='agent')
1482
+ * agent.update → PATCH /api/im/agents/:id
1483
+ * agent.delete → DELETE /api/im/agents/:id
1484
+ * agent_profile.create → POST /api/im/agent_profiles
1485
+ * agent_profile.update → PATCH /api/im/agent_profiles/:id
1486
+ * agent_profile.delete → DELETE /api/im/agent_profiles/:id
1487
+ *
1488
+ * Error classification per docs/refactor/13-error-handling.md §2.1 / §2.7:
1489
+ * 2xx → ok:true (SyncWorker drops the row)
1490
+ * 408 / 429 → retryable (SyncWorker re-queues with exponential backoff)
1491
+ * 5xx / net err retryable
1492
+ * 4xx (other) → permanent (SyncWorker marks failed; 409 is conflict)
1616
1493
  */
1617
- warnOnly: boolean;
1494
+ private flushSyncRow;
1618
1495
  }
1619
- /** Scan content for known secret patterns. Returns the list of hits. */
1620
- declare function scanForSecrets(content: string): SecretHit[];
1621
- /**
1622
- * Convenience: true if content has at least one blocking (non-warnOnly) hit.
1623
- */
1624
- declare function hasBlockingSecret(content: string): boolean;
1625
1496
 
1626
- /**
1627
- * memory-team-sync — client-side delta push + server-wins pull for team memory.
1628
- *
1629
- * Walks a local directory (typically `~/.claude/memory/team/<owner>/<repo>/`),
1630
- * diffs content hashes against a persisted `last-sync.json`, pushes only the
1631
- * changed files, and writes any server-side-newer rows back to disk.
1632
- *
1633
- * Design: docs/version190/14e-memory-cc-compat.md §8.5
1634
- *
1635
- * Secret scanning (local): any file whose content matches a blocking pattern
1636
- * is rejected before it leaves the device. The server also scans (defense in
1637
- * depth).
1638
- */
1639
- /** Per-file content size limit (matches server). */
1640
- declare const MEMORY_TEAM_SYNC_MAX_BYTES: number;
1641
- /** Name of the sidecar state file. */
1642
- declare const LAST_SYNC_FILE = ".prismer-team-sync.json";
1643
- interface SyncTeamMemoryOptions {
1644
- /** Team identifier, e.g. `'acme/widgets'`. */
1645
- team: string;
1646
- /** Root directory to sync (scanned recursively). */
1647
- rootDir: string;
1648
- /** Prismer API key (Bearer). */
1649
- apiKey: string;
1650
- /**
1651
- * Base URL for the IM API — typically the cloud host.
1652
- * e.g. `'https://prismer.cloud/api/im'` or `'http://localhost:3000/api/im'`.
1653
- */
1654
- baseUrl: string;
1655
- /**
1656
- * Optional override of the since timestamp. Normally we read this from the
1657
- * sidecar state file and ignore this param. Exposed mainly for testing.
1658
- */
1659
- since?: string;
1660
- /**
1661
- * Custom fetch (for tests). Defaults to the global fetch.
1662
- */
1497
+ interface PairOptions {
1498
+ cloudBaseUrl: string;
1499
+ /** Human-readable label visible to the approver in mobile UI. */
1500
+ deviceName?: string;
1501
+ /** Override config save path (tests). */
1502
+ paths?: ConfigPaths;
1503
+ /** Number of poll attempts (5 s each). Default 60 → 5 min. */
1504
+ maxPollAttempts?: number;
1505
+ /** Pluggable fetch (tests). */
1663
1506
  fetchImpl?: typeof fetch;
1664
- /**
1665
- * If true, do not write pulled[] rows to disk and do not update sidecar.
1666
- * Useful for dry-run/preflight.
1667
- */
1668
- dryRun?: boolean;
1669
- }
1670
- interface SyncTeamMemoryResult {
1671
- pushed: number;
1672
- pulled: number;
1673
- rejected: Array<{
1674
- path: string;
1675
- reason: string;
1676
- detail?: string;
1677
- }>;
1678
- skippedLocalSecrets: Array<{
1679
- path: string;
1680
- pattern: string;
1681
- line: number;
1682
- }>;
1683
- serverTime: string;
1684
- }
1685
- /** Walk rootDir recursively for .md files (only). Returns POSIX-style rel paths. */
1686
- declare function listMarkdownFiles(rootDir: string): string[];
1687
- /**
1688
- * Sync a team-memory directory with the cloud.
1689
- *
1690
- * 1. Walk rootDir for .md files
1691
- * 2. Hash each; diff against last-sync.json → push[] candidates
1692
- * 3. Local secret scan → any hits are skipped (not pushed)
1693
- * 4. POST /memory/team/sync with push[] and since
1694
- * 5. Write pulled[] to disk (server wins)
1695
- * 6. Update last-sync.json
1696
- */
1697
- declare function syncTeamMemory(opts: SyncTeamMemoryOptions): Promise<SyncTeamMemoryResult>;
1698
- /**
1699
- * Remove the sidecar state file — forces the next sync to re-push everything.
1700
- * Useful from CLI tooling.
1701
- */
1702
- declare function resetTeamSyncState(rootDir: string): void;
1703
-
1704
- /**
1705
- * Shamir Secret Sharing over GF(256) — byte-oriented implementation.
1706
- *
1707
- * Split a secret Buffer into N shares such that any M (threshold) shares can
1708
- * reconstruct the original, but fewer than M reveal zero information.
1709
- *
1710
- * Field: GF(256) with AES irreducible polynomial 0x11B.
1711
- * Addition = XOR. Multiplication = carryless multiply mod 0x11B.
1712
- * Share-id x ∈ {1..255} (x=0 is the secret itself).
1713
- *
1714
- * Each byte of the secret is split independently using a polynomial of degree
1715
- * m-1: P(x) = s + a_1*x + a_2*x^2 + ... + a_{m-1}*x^{m-1} (all arithmetic in GF(256))
1716
- * where s is the secret byte and a_1..a_{m-1} are uniformly random.
1717
- *
1718
- * Recovery uses Lagrange interpolation evaluated at x=0.
1719
- *
1720
- * Mnemonic format: Crockford base32 with 1-byte version header (0x01) and
1721
- * CRC-16/CCITT-FALSE checksum. Groups of 4 characters separated by hyphens.
1722
- *
1723
- * No external npm dependencies.
1724
- *
1725
- * See: Shamir, "How to Share a Secret", 1979. GF(256) byte-level variant used
1726
- * by industry (Trezor Shamir Backup, HashiCorp Vault, etc.).
1727
- */
1728
- /**
1729
- * Branded `Buffer` that has been validated as a Shamir share by this module.
1730
- *
1731
- * Nominal typing prevents accidentally passing an arbitrary `Buffer` (e.g.
1732
- * `Buffer.from('hello')`) into {@link combineShares} or
1733
- * {@link encodeShareAsMnemonic}. TypeScript structural equality is defeated
1734
- * by the `__brand` phantom field, so the only path to a `ShamirShare` value
1735
- * is through this module's own functions (`splitSecret`,
1736
- * `decodeShareFromMnemonic`) or the explicit escape hatch
1737
- * {@link unsafeAsShamirShare}.
1738
- *
1739
- * This is a compile-time guard only; runtime validation (duplicate ids,
1740
- * inconsistent lengths, CRC) still happens inside `combineShares` /
1741
- * `decodeShareFromMnemonic`.
1742
- */
1743
- type ShamirShare = Buffer & {
1744
- readonly __brand: unique symbol;
1745
- };
1746
- /**
1747
- * Escape hatch for interop paths that already hold raw share Buffers
1748
- * (e.g. legacy callers that read bytes off the wire themselves and cannot
1749
- * route through {@link decodeShareFromMnemonic}).
1750
- *
1751
- * This is a **sharp edge**: the caller is asserting the buffer is a
1752
- * well-formed share byte string (`[share_id, y_byte_1, y_byte_2, ...]` with
1753
- * `share_id ∈ 1..255`). Passing an arbitrary buffer here will compile, and
1754
- * will pass length / id / duplicate checks inside `combineShares`, but will
1755
- * reconstruct a plausibly-shaped yet **semantically wrong** secret.
1756
- *
1757
- * Prefer `decodeShareFromMnemonic` wherever possible — the CRC-16 check
1758
- * there catches tampered or wrong-batch shares.
1759
- */
1760
- declare function unsafeAsShamirShare(buf: Buffer): ShamirShare;
1761
- /**
1762
- * Split a secret into n shares; any m can recombine.
1763
- *
1764
- * Each returned share Buffer is [share_id_byte, y_byte_1, y_byte_2, ...].
1765
- * The share-id is 1..n (never 0).
1766
- *
1767
- * @param secret plaintext bytes
1768
- * @param n total shares (2..255)
1769
- * @param m threshold (2..n)
1770
- */
1771
- declare function splitSecret(secret: Buffer, n: number, m: number): ShamirShare[];
1772
- /**
1773
- * Combine shares to recover the secret. Any m of n suffice (where m is the
1774
- * threshold used at split time).
1775
- *
1776
- * @param shares array of share Buffers (each [x_byte, y_bytes...])
1777
- * @returns recovered secret
1778
- */
1779
- declare function combineShares(shares: ShamirShare[]): Buffer;
1780
- /**
1781
- * Encode a share as a mnemonic string.
1782
- *
1783
- * Framing (before base32 encoding):
1784
- * [0x01 version] [share bytes...] [CRC-16/CCITT-FALSE of (version||share)]
1785
- *
1786
- * Output is Crockford base32, grouped in blocks of 4 characters separated by
1787
- * hyphens for readability. Case-insensitive on decode.
1788
- */
1789
- declare function encodeShareAsMnemonic(share: ShamirShare): string;
1790
- /**
1791
- * Decode a mnemonic string back into a share Buffer. Strips whitespace,
1792
- * hyphens, and normalizes case. Validates the version byte and CRC-16.
1793
- */
1794
- declare function decodeShareFromMnemonic(encoded: string): ShamirShare;
1507
+ /** Allow overwriting existing config. Default false. */
1508
+ force?: boolean;
1509
+ /** Hook for test capture of the QR URL. */
1510
+ onQrReady?: (qrUrl: string) => void;
1511
+ /**
1512
+ * LAN-dev bypass: skip QR + mobile approval and auto-approve the offer
1513
+ * as the named human IMUser. Requires `LOCAL_ONLY=1` in this process AND
1514
+ * cloud-side `LOCAL_ONLY=1 && NODE_ENV !== 'production'`. Without those
1515
+ * env conditions the cloud returns 403 and we surface the error.
1516
+ */
1517
+ asUserEmail?: string;
1518
+ /** Override LOCAL_ONLY env read (tests). */
1519
+ isLocalOnly?: () => boolean;
1520
+ /** Override poll wait (tests). Default 5_000ms. */
1521
+ pollIntervalMs?: number;
1522
+ }
1523
+ interface PairResult {
1524
+ config: Config;
1525
+ paths: ConfigPaths;
1526
+ }
1527
+ declare function pair(opts: PairOptions): Promise<PairResult>;
1795
1528
 
1796
- interface TimelineEntry {
1797
- seq: number;
1798
- opcode: number;
1799
- slot: number;
1800
- payload: Buffer;
1801
- createdAt: number;
1802
- }
1803
- interface OutboxEntry {
1804
- id: number;
1805
- seq: number;
1806
- frame: Buffer;
1807
- createdAt: number;
1808
- attempts: number;
1809
- }
1810
- interface DeadLetterEntry {
1811
- id: number;
1812
- originalId: number;
1813
- seq: number;
1814
- createdAt: number;
1815
- failedAt: number;
1816
- attempts: number;
1817
- }
1818
- interface DroppedFrame {
1819
- id: number;
1820
- seq: number;
1821
- attempts: number;
1822
- }
1823
- interface DaemonOutboxOptions {
1824
- bindingId: string;
1825
- dataDir?: string;
1826
- timelineCap?: number;
1827
- outboxCap?: number;
1828
- maxAttempts?: number;
1829
- onDrop?: (entries: DroppedFrame[]) => void;
1830
- }
1831
- declare class DaemonOutbox {
1832
- private db;
1833
- private bindingId;
1834
- private timelineCap;
1835
- private outboxCap;
1836
- private maxAttempts;
1837
- private onDrop?;
1838
- private currentSeq;
1839
- private droppedCount;
1840
- constructor(opts: DaemonOutboxOptions);
1841
- private initSchema;
1842
- appendSent(opcode: number, slot: number, payload: Buffer): number;
1843
- getTimelineSince(lastSeq: number, limit?: number): TimelineEntry[];
1844
- getCurrentSeq(): number;
1845
- private enforceTimelineCap;
1846
- queue(seq: number, frame: Buffer): number;
1847
- drain(limit?: number): OutboxEntry[];
1848
- ack(outboxId: number): void;
1849
- bumpAttempts(outboxId: number): {
1850
- attempts: number;
1851
- deadLettered: boolean;
1852
- };
1853
- private moveToDeadLetter;
1854
- getDeadLetterCount(): number;
1855
- getDeadLetterEntries(limit?: number, includeFrame?: boolean): (DeadLetterEntry & {
1856
- frame?: Buffer;
1857
- })[];
1858
- pendingCount(): number;
1859
- getDroppedCount(): number;
1860
- private enforceOutboxCap;
1861
- close(): void;
1862
- }
1863
- /** 2-byte header + payload per §5.6.4 */
1864
- declare function frameFromParts(opcode: number, slot: number, payload: Buffer): Buffer;
1865
-
1866
- /**
1867
- * Prismer Runtime — WSS Relay Client (v1.9.0)
1868
- *
1869
- * WebSocket client for cloud relay communication. Connects to the main
1870
- * cloud host (derived from `cloudApiBase`) under the `/ws/daemon/*` path
1871
- * prefix — there is no separate relay subdomain.
1872
- *
1873
- * Responsibilities:
1874
- * - Register daemon binding with API key
1875
- * - Receive remote command pushes (control channel)
1876
- * - Route encrypted envelopes (data channel)
1877
- * - Heartbeat keepalive (30s interval)
1878
- * - Auto-reconnect with exponential backoff
1879
- */
1529
+ declare const CCConfigSchema: z.ZodObject<{
1530
+ cwd: z.ZodString;
1531
+ model: z.ZodDefault<z.ZodString>;
1532
+ systemPrompt: z.ZodOptional<z.ZodString>;
1533
+ envVars: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
1534
+ mcpServers: z.ZodOptional<z.ZodArray<z.ZodObject<{
1535
+ name: z.ZodString;
1536
+ command: z.ZodString;
1537
+ args: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
1538
+ env: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
1539
+ }, "strip", z.ZodTypeAny, {
1540
+ name: string;
1541
+ command: string;
1542
+ env?: Record<string, string> | undefined;
1543
+ args?: string[] | undefined;
1544
+ }, {
1545
+ name: string;
1546
+ command: string;
1547
+ env?: Record<string, string> | undefined;
1548
+ args?: string[] | undefined;
1549
+ }>, "many">>;
1550
+ allowedTools: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
1551
+ maxTurns: z.ZodDefault<z.ZodNumber>;
1552
+ baseURL: z.ZodOptional<z.ZodString>;
1553
+ apiKeyRef: z.ZodOptional<z.ZodString>;
1554
+ route: z.ZodDefault<z.ZodEnum<["default", "prismer", "omniroute"]>>;
1555
+ }, "strip", z.ZodTypeAny, {
1556
+ model: string;
1557
+ cwd: string;
1558
+ maxTurns: number;
1559
+ route: "default" | "prismer" | "omniroute";
1560
+ systemPrompt?: string | undefined;
1561
+ allowedTools?: string[] | undefined;
1562
+ envVars?: Record<string, string> | undefined;
1563
+ mcpServers?: {
1564
+ name: string;
1565
+ command: string;
1566
+ env?: Record<string, string> | undefined;
1567
+ args?: string[] | undefined;
1568
+ }[] | undefined;
1569
+ baseURL?: string | undefined;
1570
+ apiKeyRef?: string | undefined;
1571
+ }, {
1572
+ cwd: string;
1573
+ model?: string | undefined;
1574
+ systemPrompt?: string | undefined;
1575
+ allowedTools?: string[] | undefined;
1576
+ envVars?: Record<string, string> | undefined;
1577
+ mcpServers?: {
1578
+ name: string;
1579
+ command: string;
1580
+ env?: Record<string, string> | undefined;
1581
+ args?: string[] | undefined;
1582
+ }[] | undefined;
1583
+ maxTurns?: number | undefined;
1584
+ baseURL?: string | undefined;
1585
+ apiKeyRef?: string | undefined;
1586
+ route?: "default" | "prismer" | "omniroute" | undefined;
1587
+ }>;
1588
+ type ClaudeCodeConfig = z.infer<typeof CCConfigSchema>;
1589
+ declare const claudeCodeAdapter: AdapterDef;
1880
1590
 
1881
- declare const OPCODE: {
1882
- readonly JSON_CONTROL: 0;
1883
- readonly AGENT_OUTPUT: 1;
1884
- readonly TERMINAL_IO: 2;
1885
- readonly FILE_CHUNK: 3;
1886
- readonly AUDIT_TAP: 4;
1887
- readonly BACKFILL_REQUEST: 5;
1888
- readonly BACKFILL_CHUNK: 6;
1889
- };
1890
- interface RelayClientOptions {
1891
- apiKey: string;
1892
- daemonId: string;
1893
- userId: string;
1894
- /**
1895
- * Base WSS URL for the relay (e.g. `wss://cloud.prismer.dev`). The client
1896
- * appends `/ws/daemon/control` and `/ws/daemon/data` to this base. Callers
1897
- * typically derive it from `cloudApiBase` via `deriveWsFromHttp()`.
1898
- */
1899
- relayUrl: string;
1900
- heartbeatIntervalMs?: number;
1901
- reconnectDelayMs?: number;
1902
- maxReconnectDelayMs?: number;
1903
- autoReconnect?: boolean;
1904
- /** v1.9.0 — bindingId enables daemon-side outbox + timeline backfill (§5.6.5). */
1905
- bindingId?: string;
1906
- /** Override outbox location (for tests); defaults to ~/.prismer/daemon/{bindingId}. */
1907
- outboxDataDir?: string;
1908
- }
1909
- interface RelayState {
1910
- connected: boolean;
1911
- channel: 'control' | 'data' | null;
1912
- lastHeartbeat?: number;
1913
- reconnectAttempts: number;
1914
- lastError?: string;
1915
- }
1916
- interface RemoteCommand {
1917
- id: string;
1918
- type: string;
1919
- payload: Record<string, unknown>;
1920
- createdAt: number;
1921
- }
1922
- declare class RelayClient extends EventEmitter {
1923
- private apiKey;
1924
- private daemonId;
1925
- private userId;
1926
- private relayUrl;
1927
- private heartbeatIntervalMs;
1928
- private reconnectDelayMs;
1929
- private maxReconnectDelayMs;
1930
- private autoReconnect;
1931
- private controlWs;
1932
- private dataWs;
1933
- private heartbeatInterval?;
1934
- private heartbeatTimer?;
1935
- private reconnectTimer?;
1936
- private state;
1937
- private isShuttingDown;
1938
- private outbox?;
1939
- constructor(opts: RelayClientOptions);
1940
- /**
1941
- * Connect to relay server (both control and data channels)
1942
- */
1943
- connect(): Promise<void>;
1944
- /**
1945
- * Disconnect from relay server
1946
- */
1947
- disconnect(): Promise<void>;
1948
- /**
1949
- * Send command to mobile client via relay (data channel).
1950
- *
1951
- * v1.9.0 — if bindingId was provided, every tracked data-plane frame
1952
- * (AGENT_OUTPUT / TERMINAL_IO / FILE_CHUNK) is recorded in the local
1953
- * timeline before send. If the data channel is down, the frame is queued
1954
- * to the outbox and will be replayed on next successful connect.
1955
- */
1956
- /**
1957
- * Send JSON control message to cloud via the control channel.
1958
- * v1.9.x — used by daemon to ack remote commands (e.g. command.result).
1959
- * Returns false silently if control channel is not connected.
1960
- */
1961
- sendControl(message: unknown): boolean;
1962
- sendCommand(data: Buffer | string): boolean;
1963
- /**
1964
- * Get current connection state
1965
- */
1966
- getState(): RelayState;
1967
- /**
1968
- * Get connection status summary
1969
- */
1970
- getStatus(): {
1971
- controlConnected: boolean;
1972
- dataConnected: boolean;
1973
- lastHeartbeat?: number;
1974
- reconnectAttempts: number;
1975
- };
1976
- /**
1977
- * Connect to control channel (/ws/daemon/control)
1978
- */
1979
- private connectControlChannel;
1980
- /**
1981
- * Connect to data channel (/ws/daemon/data)
1982
- */
1983
- private connectDataChannel;
1984
- private relayEndpoint;
1985
- /**
1986
- * Register daemon with relay server
1987
- */
1988
- private registerDaemon;
1989
- /**
1990
- * Handle incoming control channel messages
1991
- */
1992
- private handleControlMessage;
1993
- /** RPC method handlers registered by the daemon process. Keyed by method
1994
- * name (e.g. 'fs.read'). Result is serialized into the rpc.response
1995
- * envelope. Thrown errors → `error` field. */
1996
- private rpcHandlers;
1997
- /** Register an RPC handler. Daemon code calls this at startup for each
1998
- * method it wants to expose to cloud relay (mobile → cloud → daemon). */
1999
- registerRpcHandler(method: string, handler: (params: unknown) => Promise<unknown>): void;
2000
- private handleRpcRequest;
2001
- private sendRpcResponse;
2002
- /**
2003
- * Handle incoming data channel messages.
2004
- *
2005
- * v1.9.0 — intercept backfill-request (opcode 0x05) from the cloud
2006
- * (cloud forwards it on behalf of a reconnecting mobile client) and
2007
- * respond with backfill-chunk frames (opcode 0x06) for every timeline
2008
- * entry with seq > payload.lastSeq.
2009
- */
2010
- private handleDataMessage;
2011
- /**
2012
- * Drain outbox by replaying queued frames over the data channel.
2013
- * Called after reconnect() succeeds. Stops on the first send failure
2014
- * to preserve ordering.
2015
- */
2016
- private replayOutbox;
2017
- /** Pending count for status reporting. */
2018
- getOutboxPending(): number;
2019
- /**
2020
- * Handle remote command from mobile
2021
- */
2022
- private handleRemoteCommand;
1591
+ declare const CodexConfigSchema: z.ZodObject<{
1592
+ /** Working directory for the codex subprocess. */
1593
+ cwd: z.ZodString;
2023
1594
  /**
2024
- * Start heartbeat interval
1595
+ * Model identifier passed to --model (e.g. 'codex-mini-latest', 'o4-mini').
1596
+ * Default matches the Codex CLI built-in default as of 2026-05.
2025
1597
  */
2026
- private startHeartbeat;
1598
+ model: z.ZodDefault<z.ZodString>;
2027
1599
  /**
2028
- * Clear heartbeat timers
1600
+ * Sandbox level for the codex subprocess.
1601
+ * read-only — analysis only; code-gen tasks will stall
1602
+ * workspace-write — write inside cwd, read-only outside (recommended)
1603
+ * danger-full-access — no restrictions; safe only inside isolated containers
2029
1604
  */
2030
- private clearHeartbeat;
1605
+ sandbox: z.ZodDefault<z.ZodEnum<["read-only", "workspace-write", "danger-full-access"]>>;
1606
+ /** Optional system prompt prepended to the user prompt as a preamble (see U4). */
1607
+ systemPrompt: z.ZodOptional<z.ZodString>;
1608
+ /** Extra env vars merged into the codex subprocess environment. */
1609
+ envVars: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
2031
1610
  /**
2032
- * Send heartbeat to relay
1611
+ * Name of the env var holding the OpenAI API key. Defaults to
1612
+ * 'OPENAI_API_KEY' (Codex CLI default). Override if using a workspace-
1613
+ * scoped key under a different name.
2033
1614
  */
2034
- private sendHeartbeat;
2035
- /**
2036
- * Acknowledge heartbeat from relay
2037
- */
2038
- private sendHeartbeatAck;
2039
- /**
2040
- * Handle disconnection from one or both channels
2041
- */
2042
- private handleDisconnect;
2043
- /**
2044
- * Schedule reconnection with exponential backoff
2045
- */
2046
- private scheduleReconnect;
2047
- /**
2048
- * Clear reconnect timer
2049
- */
2050
- private clearReconnectTimer;
2051
- /**
2052
- * Close control channel
2053
- */
2054
- private closeControlChannel;
2055
- /**
2056
- * Close data channel
2057
- */
2058
- private closeDataChannel;
2059
- }
1615
+ apiKeyEnv: z.ZodDefault<z.ZodString>;
1616
+ }, "strip", z.ZodTypeAny, {
1617
+ model: string;
1618
+ sandbox: "read-only" | "workspace-write" | "danger-full-access";
1619
+ cwd: string;
1620
+ apiKeyEnv: string;
1621
+ systemPrompt?: string | undefined;
1622
+ envVars?: Record<string, string> | undefined;
1623
+ }, {
1624
+ cwd: string;
1625
+ model?: string | undefined;
1626
+ systemPrompt?: string | undefined;
1627
+ sandbox?: "read-only" | "workspace-write" | "danger-full-access" | undefined;
1628
+ envVars?: Record<string, string> | undefined;
1629
+ apiKeyEnv?: string | undefined;
1630
+ }>;
1631
+ type CodexConfig = z.infer<typeof CodexConfigSchema>;
1632
+ /**
1633
+ * Parse `codex exec --json` stdout. The CLI emits newline-delimited JSON
1634
+ * lines on stdout; we look for the final-message line and extract its
1635
+ * content. Tolerant: any parse failure falls back to raw stdout.
1636
+ */
1637
+ declare function parseCodexOutput(stdout: string): string;
1638
+ declare const codexAdapter: AdapterDef;
2060
1639
 
2061
- declare const RUNTIME_VERSION = "1.9.0";
1640
+ declare function buildProgram(): Command;
1641
+ declare function runCli(argv?: string[]): Promise<void>;
2062
1642
 
2063
- export { AGENT_CATALOG, type AdapterDescriptor, type AdapterDispatchInput, type AdapterDispatchResult, type AdapterImpl, AdapterRegistry, type AgentCatalogEntry, type AgentDescriptor, type AgentState, type AgentStatus, AgentSupervisor, AllProvidersFailedError, type AnalyzeRequest, type AssignTaskRequest, type AuthenticatedIdentity, type CancelTaskRequest, type CliContext, ConfigError, type CreateGeneRequest, DaemonAlreadyRunningError, type DaemonHttpOptions, DaemonHttpServer, type DaemonOptions, DaemonOutbox, type DaemonOutboxOptions, DaemonProcess, type DaemonRunnerHandle, type DaemonRunnerOptions, type DaemonState, DispatchMux, type DispatchMuxRequest, type DispatchMuxResult, type DistillationRequest, type DreamCompaction, type DreamResult, DreamScheduler, type DreamSchedulerOptions, type E2EEContext, type E2EEKeyEntry, type KeyPair as E2EEKeyPair, E2EEKeyStorage, type E2EEStorageStats, type EncryptedEnvelope, type EncryptionConfig, EventBus, type EventBusEnvelope, type EventBusOptions, EvolutionGatewayHttpHandler, type EvolutionGatewayOptions, type HookConfig, type HookEntry, type HookFormat, Keychain, type KeychainAdapter, type KeychainBackend, KeychainOperationError, type KeychainOptions, LAST_SYNC_FILE, LLMDispatcher, type LLMMessage, type LLMProvider, type LLMRequest, type LLMResponse, type ListMemoryRequest, type LoadConfigOptions, MEMORY_TEAM_SYNC_MAX_BYTES, MemoryDB, type MemoryFile, type MemoryFileVersion, type MemoryResponse, type MergeOptions, type MergeResult, type MigrateOptions, type MigrateResult, type MigrateSecretsOptions, type MigrateSecretsResult, NoKeychainBackendError, OPCODE, type OutboxEntry, type OutputMode, type PairOffer, type PairedDevice, type PrismerConfig, type ProviderStats, RUNTIME_VERSION, type RecallRequest, type RecordRequest, RelayClient, type RelayClientOptions, type RelayState, type RemoteCommand, type RouteHandler, type RouteTaskRequest, type RouteTaskResponse, type RoutingPolicy, type SecretHit, type ShamirShare, type ShutdownHandler, type SignalExtractionRequest, type StepCompletedRequest, type StepFailedRequest, type StepTimeoutRequest, type Subscription, type SubscriptionHandler, type SupervisorOptions, type SyncTeamMemoryOptions, type SyncTeamMemoryResult, type TableOptions, type TableRow, type TaskInfo, TaskRouteState, TaskRouter, type TaskRouterOptions, type TimelineEntry, TraceWriter, TraceWriterManager, type TraceWriterManagerOptions, type TraceWriterOptions, UI, type UIOptions, type WriteMemoryRequest, applyCommonFlags, assertBrandVoice, closeMemoryDB, combineShares, createCliContext, createDreamScheduler, createE2EEContext, createKeyEntry, decodeShareFromMnemonic, decrypt, decryptMessage, deriveKey, deriveSessionKeys, deriveSharedSecret, deserializeEnvelope, encodeShareAsMnemonic, encrypt, encryptMessage, frameFromParts, generateKeyPair as generateE2EEKeyPair, generateSalt, generateSessionId, getAgent, getMemoryDB, getUI, hasBlockingSecret, installHooks, listMarkdownFiles, loadConfig, mergeHooks, migrateSecrets, parseKeyringPlaceholder, parseSessionId, readHookConfig, resetTeamSyncState, rollbackHooks, runDream, scanForSecrets, serializeEnvelope, setUI, splitSecret, startDaemonRunner, syncTeamMemory, unsafeAsShamirShare, writeConfig, writeHookConfig };
1643
+ export { type AcquireResult, type AdapterDef, type AdapterKind, AdapterRegistry, type AdapterService, type AgentChangedPayload, type AgentHostDeclarePayload, type AgentProfile, type AgentProfileChangedPayload, type AgentStatusChangedPayload, AssetCache, type AssetCacheOptions, type AssetDispatchObservation, type AssetDispatchStrategy, type AssetRef, BUILTIN_ROLE_TEMPLATES, type CachedAsset, type ClaudeCodeConfig, CloudClient, type CloudClientOptions, CloudError, type CloudResponse, type CodexConfig, type Config, type ConfigPaths, ConfigSchema, type DispatchDeps, type Envelope, type FlushFn, type FlushResult, type HealthStatus, type HermesProfileConfig, type HostAckedPayload, type HostedAgentDeclaration, type IMAgentStatus, type IMWSMessage, type LocalDb, LocalServer, type LocalServerOptions, type LocalServerState, type PairOptions, type PairResult, type ParseClaim, ParseClaimController, type ParseClaimControllerOptions, type ParsedPrismerUri, type PrismerUriType, type RoleTemplate, Runner, type RunnerOptions, ServicePool, type SyncOperation, SyncQueue, type SyncQueueRow, type SyncResourceType, type SyncStatus, SyncWorker, type SyncWorkerOptions, TARGET_SCHEMA_VERSION, type TaskCancelPayload, type TaskDispatchContextEntry, type TaskDispatchProgressPayload, type TaskDispatchReplyPayload, type TaskDispatchRequestPayload, type TaskInput, type TaskResult, UriResolver, type UriResolverOptions, type ValidationResult, WS_CLOSE, type WorkspaceChangedPayload, type WorkspaceFileBinding, type WorkspaceFileChangedPayload, WorkspaceMirror, type WorkspaceMirrorOptions, WsClient, type WsClientOptions, buildProgram, claudeCodeAdapter, codexAdapter, composePrompt, configExists, currentSchemaVersion, deriveWsUrl, envelope, getRoleTemplate, handleDispatch, hermesAdapter, isDaemonId, listRoleTemplates, loadConfig, newDaemonId, nextBackoffMs, openLocalDb, pair, parseCodexOutput, parseUris, resolvePaths, runCli, runMigrations, saveConfig };