@cleocode/runtime 2026.5.131 → 2026.5.132

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.
package/dist/index.d.ts CHANGED
@@ -1,5 +1,7 @@
1
1
  import * as _cleocode_contracts from '@cleocode/contracts';
2
- import { Transport, ConduitMessage, AgentRegistryAPI } from '@cleocode/contracts';
2
+ import { Transport, ConduitMessage, AgentRegistryAPI, Subsystem } from '@cleocode/contracts';
3
+ import { FSWatcher } from 'node:fs';
4
+ import { BootstrapDaemonOptions, StudioSupervisor, acquireLock } from '@cleocode/core/sentient';
3
5
 
4
6
  /**
5
7
  * AgentPoller — Polls for messages via HttpTransport AND group conversations.
@@ -200,6 +202,80 @@ declare class SseConnectionService {
200
202
  };
201
203
  }
202
204
 
205
+ /**
206
+ * Sentient daemon subsystem adapter — R4 (T11255).
207
+ *
208
+ * Expresses the full sentient daemon lifecycle (Studio supervision, Tier-1
209
+ * tick cron, Tier-2 proposal cron, skill curator cron, nightly hygiene loop)
210
+ * as a single {@link Subsystem} via `defineSubsystem` from
211
+ * `@cleocode/runtime/daemon`.
212
+ *
213
+ * ### Package placement
214
+ * This file lives in `@cleocode/runtime` (NOT `@cleocode/core`) to respect the
215
+ * dependency direction: `@cleocode/runtime` → `@cleocode/core` (runtime
216
+ * consumes core; core must not import runtime to avoid a circular dep).
217
+ *
218
+ * ### Standalone-daemon path (daemon-entry.ts)
219
+ * The legacy `bootstrapDaemon` in `@cleocode/core/sentient` continues to
220
+ * function as the entry point for the standalone `cleo daemon start --foreground`
221
+ * path. `createSentientSubsystem` is the adapter for hosts that want to embed
222
+ * the sentient daemon inside a `SubsystemRegistry`-driven process.
223
+ *
224
+ * @packageDocumentation
225
+ * @module @cleocode/runtime
226
+ *
227
+ * @epic T11255 R4 — migrate sentient/daemon.ts → daemon subsystem
228
+ * @saga T11243 SG-RUNTIME-UNIFICATION
229
+ */
230
+
231
+ /**
232
+ * Runtime context threaded from `start()` into `shutdown()`.
233
+ *
234
+ * Holds every handle the shutdown path needs to tear down cleanly.
235
+ * Kept internal to this module — callers interact via `SubsystemRegistry`.
236
+ */
237
+ interface SentientContext {
238
+ readonly lockPath: string;
239
+ readonly statePath: string;
240
+ readonly studioSupervisor: StudioSupervisor | null;
241
+ readonly watcher: FSWatcher | null;
242
+ readonly lock: Awaited<ReturnType<typeof acquireLock>>;
243
+ }
244
+ /**
245
+ * Declare the sentient daemon as a supervised daemon {@link Subsystem}.
246
+ *
247
+ * The returned subsystem (frozen by `defineSubsystem`) is registered with a
248
+ * `SubsystemRegistry` in a host process. It carries the full lifecycle
249
+ * that the legacy `bootstrapDaemon` ran inline:
250
+ *
251
+ * 1. `start()` — acquires the advisory lock, persists pid/startedAt,
252
+ * optionally starts Studio supervision, watches the state file, and
253
+ * schedules all four cron jobs (Tier-1, Tier-2, curator, hygiene). Runs
254
+ * one boot tick before returning.
255
+ * 2. `healthProbe()` — returns a `SubsystemHealth` row based on the pid
256
+ * recorded in state.json.
257
+ * 3. `shutdown()` — cascades Studio SIGTERM → watcher.close() → state patch
258
+ * → lock release.
259
+ *
260
+ * @param projectRoot - Absolute path to the project (contains `.cleo/`).
261
+ * @param opts - Optional overrides for Studio supervision and config path.
262
+ * @returns A frozen {@link Subsystem} for `SubsystemRegistry.register()`.
263
+ */
264
+ declare function createSentientSubsystem(projectRoot: string, opts?: BootstrapDaemonOptions): Subsystem<SentientContext>;
265
+ /**
266
+ * Bootstrap the sentient daemon via a SubsystemRegistry.
267
+ *
268
+ * Alternative to `bootstrapDaemon` for host processes that want to drive the
269
+ * sentient daemon through the uniform `start → healthProbe → shutdown`
270
+ * lifecycle (e.g., a unified CLEO daemon host that also runs the GC subsystem
271
+ * and the gateway subsystem). Installs SIGTERM/SIGINT handlers that call
272
+ * `shutdownAll()` and keeps the process alive via node-cron.
273
+ *
274
+ * @param projectRoot - Absolute path to the project (contains `.cleo/`).
275
+ * @param opts - Optional Studio supervision and config overrides.
276
+ */
277
+ declare function bootstrapSentientRegistry(projectRoot: string, opts?: BootstrapDaemonOptions): Promise<void>;
278
+
203
279
  /** Configuration for createRuntime(). */
204
280
  interface RuntimeConfig {
205
281
  /** Agent ID to run as. If omitted, uses the most recently used active agent. */
@@ -253,4 +329,4 @@ interface RuntimeHandle {
253
329
  */
254
330
  declare function createRuntime(registry: AgentRegistryAPI, config?: RuntimeConfig): Promise<RuntimeHandle>;
255
331
 
256
- export { AgentPoller, type AgentPollerConfig, type HeartbeatConfig, HeartbeatService, type KeyRotationConfig, KeyRotationService, type MessageHandler, type RuntimeConfig, type RuntimeHandle, type SseConnectionConfig, SseConnectionService, type SseMessageHandler, createRuntime };
332
+ export { AgentPoller, type AgentPollerConfig, type HeartbeatConfig, HeartbeatService, type KeyRotationConfig, KeyRotationService, type MessageHandler, type RuntimeConfig, type RuntimeHandle, type SseConnectionConfig, SseConnectionService, type SseMessageHandler, bootstrapSentientRegistry, createRuntime, createSentientSubsystem };
package/dist/index.js CHANGED
@@ -1,3 +1,9 @@
1
+ import {
2
+ SubsystemRegistry,
3
+ createSubsystemLogger,
4
+ defineSubsystem
5
+ } from "./chunk-WSQQ5DQL.js";
6
+
1
7
  // src/index.ts
2
8
  import { conduit } from "@cleocode/core";
3
9
 
@@ -297,6 +303,250 @@ var SseConnectionService = class {
297
303
  }
298
304
  };
299
305
 
306
+ // src/sentient-subsystem.ts
307
+ import { watch } from "fs";
308
+ import { join } from "path";
309
+ import {
310
+ acquireLock,
311
+ curatorCronExpression,
312
+ patchSentientState,
313
+ readCuratorConfig,
314
+ readSentientState,
315
+ readSuperviseStudioConfig,
316
+ releaseLock,
317
+ SENTIENT_CRON_EXPR,
318
+ SENTIENT_HYGIENE_CRON_EXPR,
319
+ SENTIENT_LOCK_FILE,
320
+ SENTIENT_PROPOSE_CRON_EXPR,
321
+ SENTIENT_STATE_FILE,
322
+ StudioSupervisor,
323
+ safeRunCrossProjectHygiene,
324
+ safeRunProposeTick,
325
+ safeRunTick,
326
+ warmupWorktreeBackend
327
+ } from "@cleocode/core/sentient";
328
+ import cron from "node-cron";
329
+ var log = createSubsystemLogger("sentient");
330
+ function createSentientSubsystem(projectRoot, opts = {}) {
331
+ const statePath = join(projectRoot, SENTIENT_STATE_FILE);
332
+ const lockPath = join(projectRoot, SENTIENT_LOCK_FILE);
333
+ return defineSubsystem({
334
+ name: "sentient",
335
+ async start() {
336
+ const lock = await acquireLock(lockPath);
337
+ if (!lock) {
338
+ log.error({}, "lock acquisition failed \u2014 another daemon is running");
339
+ process.exit(2);
340
+ }
341
+ await patchSentientState(statePath, {
342
+ pid: process.pid,
343
+ startedAt: (/* @__PURE__ */ new Date()).toISOString()
344
+ });
345
+ let studioSupervisor = null;
346
+ let shouldSuperviseStudio;
347
+ if (typeof opts.superviseStudio === "boolean") {
348
+ shouldSuperviseStudio = opts.superviseStudio;
349
+ } else {
350
+ const { getCleoHome } = await import("@cleocode/core");
351
+ const defaultConfigPath = join(getCleoHome(), "config.json");
352
+ const configPath = opts.globalConfigPath ?? defaultConfigPath;
353
+ shouldSuperviseStudio = await readSuperviseStudioConfig(configPath);
354
+ }
355
+ if (shouldSuperviseStudio) {
356
+ studioSupervisor = new StudioSupervisor(opts.studioOptions ?? {});
357
+ try {
358
+ studioSupervisor.start();
359
+ log.info({}, "Studio supervision enabled");
360
+ } catch (err) {
361
+ log.error({ err }, "Studio supervision start failed (non-fatal; ticks continue)");
362
+ studioSupervisor = null;
363
+ }
364
+ } else {
365
+ log.info({}, "Studio supervision disabled (daemon.superviseStudio=false)");
366
+ }
367
+ let watcher = null;
368
+ try {
369
+ watcher = watch(statePath, { persistent: false }, () => {
370
+ });
371
+ } catch {
372
+ watcher = null;
373
+ }
374
+ await warmupWorktreeBackend();
375
+ const tickOptions = { projectRoot, statePath };
376
+ await patchSentientState(statePath, { lastCronFiredAt: (/* @__PURE__ */ new Date()).toISOString() });
377
+ const outcome = await safeRunTick(tickOptions);
378
+ log.info(
379
+ { kind: outcome.kind, taskId: outcome.taskId ?? null },
380
+ `boot tick: ${outcome.kind} (task=${outcome.taskId ?? "n/a"}) ${outcome.detail}`
381
+ );
382
+ cron.schedule(
383
+ SENTIENT_CRON_EXPR,
384
+ async () => {
385
+ try {
386
+ await patchSentientState(statePath, { lastCronFiredAt: (/* @__PURE__ */ new Date()).toISOString() });
387
+ } catch (err) {
388
+ log.warn({ err }, "heartbeat write failed");
389
+ }
390
+ try {
391
+ const result = await safeRunTick(tickOptions);
392
+ log.info(
393
+ { kind: result.kind, taskId: result.taskId ?? null },
394
+ `tick: ${result.kind} (task=${result.taskId ?? "n/a"}) ${result.detail}`
395
+ );
396
+ } catch (err) {
397
+ log.error({ err }, "tick error (caught at cron boundary)");
398
+ }
399
+ },
400
+ { timezone: "UTC", noOverlap: true, name: "cleo-sentient" }
401
+ );
402
+ const proposeOptions = { projectRoot, statePath };
403
+ cron.schedule(
404
+ SENTIENT_PROPOSE_CRON_EXPR,
405
+ async () => {
406
+ try {
407
+ const state = await readSentientState(statePath);
408
+ if (!state.tier2Enabled) return;
409
+ const result = await safeRunProposeTick(proposeOptions);
410
+ log.info(
411
+ { written: result.written, count: result.count },
412
+ `propose: ${result.kind} ${result.detail}`
413
+ );
414
+ } catch (err) {
415
+ log.error({ err }, "propose error (caught at cron boundary)");
416
+ }
417
+ },
418
+ { timezone: "UTC", noOverlap: true, name: "cleo-sentient-propose" }
419
+ );
420
+ {
421
+ const { getCleoHome } = await import("@cleocode/core");
422
+ const curatorConfigPath = opts.globalConfigPath ?? join(getCleoHome(), "config.json");
423
+ const curatorCfg = await readCuratorConfig(curatorConfigPath);
424
+ if (curatorCfg.enabled) {
425
+ const expr = curatorCronExpression(curatorCfg.runEveryHours);
426
+ log.info({ interval: curatorCfg.runEveryHours, expr }, `curator enabled`);
427
+ cron.schedule(
428
+ expr,
429
+ async () => {
430
+ try {
431
+ const curatorState = await readSentientState(statePath);
432
+ if (curatorState.killSwitch) return;
433
+ const { runCuratorTick } = await import("@cleocode/core/sentient");
434
+ const result = await runCuratorTick({
435
+ staleAfterDays: curatorCfg.staleAfterDays,
436
+ archiveAfterDays: curatorCfg.archiveAfterDays
437
+ });
438
+ log.info(
439
+ { checked: result.summary.checked, stale: result.summary.markedStale },
440
+ "curator tick complete"
441
+ );
442
+ } catch (err) {
443
+ log.error({ err }, "curator error (caught at cron boundary)");
444
+ }
445
+ },
446
+ { timezone: "UTC", noOverlap: true, name: "cleo-sentient-curator" }
447
+ );
448
+ } else {
449
+ log.info({}, "curator disabled (daemon.curator.enabled=false)");
450
+ }
451
+ }
452
+ cron.schedule(
453
+ SENTIENT_HYGIENE_CRON_EXPR,
454
+ async () => {
455
+ try {
456
+ const hygieneState = await readSentientState(statePath);
457
+ if (hygieneState.killSwitch) return;
458
+ log.info({}, "nightly cross-project hygiene loop starting");
459
+ const digest = await safeRunCrossProjectHygiene();
460
+ await patchSentientState(statePath, {
461
+ hygieneLastRunAt: digest.completedAt,
462
+ hygieneSummary: digest.summary,
463
+ hygieneStats: {
464
+ projectsChecked: digest.nexusIntegrity.total,
465
+ projectsHealthy: digest.nexusIntegrity.healthy,
466
+ tempGcCandidates: digest.tempGc.candidates.length,
467
+ duplicateEpicGroups: digest.duplicateEpics.groups.length,
468
+ worktreesPruned: digest.worktreePrune.totalPruned
469
+ }
470
+ });
471
+ log.info({}, `hygiene complete: ${digest.summary}`);
472
+ } catch (err) {
473
+ log.error({ err }, "hygiene error (caught at cron boundary)");
474
+ }
475
+ },
476
+ { noOverlap: true, name: "cleo-sentient-hygiene" }
477
+ );
478
+ return { lockPath, statePath, studioSupervisor, watcher, lock };
479
+ },
480
+ async healthProbe() {
481
+ try {
482
+ const state = await readSentientState(statePath);
483
+ const running = state.pid ? (() => {
484
+ try {
485
+ process.kill(state.pid, 0);
486
+ return true;
487
+ } catch {
488
+ return false;
489
+ }
490
+ })() : false;
491
+ return {
492
+ child_id: "sentient",
493
+ pid: running ? state.pid ?? 0 : 0,
494
+ state: running ? "running" : "stopped",
495
+ restart_count: 0,
496
+ detail: `killSwitch=${state.killSwitch} lastTickAt=${state.lastTickAt ?? "never"}`
497
+ };
498
+ } catch {
499
+ return { child_id: "sentient", pid: 0, state: "stopped", restart_count: 0 };
500
+ }
501
+ },
502
+ async shutdown(context) {
503
+ if (context.studioSupervisor !== null) {
504
+ log.info({}, "forwarding shutdown to Studio (SIGTERM)");
505
+ try {
506
+ await context.studioSupervisor.stop();
507
+ } catch {
508
+ }
509
+ }
510
+ try {
511
+ context.watcher?.close();
512
+ } catch {
513
+ }
514
+ try {
515
+ await patchSentientState(context.statePath, {
516
+ pid: null,
517
+ killSwitchReason: "subsystem shutdown"
518
+ });
519
+ } catch {
520
+ }
521
+ try {
522
+ if (context.lock) await releaseLock(context.lock);
523
+ } catch {
524
+ }
525
+ log.info({}, "sentient subsystem stopped");
526
+ }
527
+ });
528
+ }
529
+ async function bootstrapSentientRegistry(projectRoot, opts = {}) {
530
+ const registry = new SubsystemRegistry({
531
+ onStart: (name) => log.info({ name }, `subsystem started: ${name}`),
532
+ onShutdown: (name) => log.info({ name }, `subsystem shutdown: ${name}`),
533
+ onError: (name, err, phase) => log.error({ name, phase, err }, `subsystem lifecycle error [${phase}]`)
534
+ });
535
+ registry.register(createSentientSubsystem(projectRoot, opts));
536
+ await registry.startAll();
537
+ const shutdown = async (reason) => {
538
+ log.info({ reason }, "shutdown signal received");
539
+ await registry.shutdownAll();
540
+ process.exit(0);
541
+ };
542
+ process.on("SIGTERM", () => {
543
+ void shutdown("SIGTERM");
544
+ });
545
+ process.on("SIGINT", () => {
546
+ void shutdown("SIGINT");
547
+ });
548
+ }
549
+
300
550
  // src/index.ts
301
551
  var { resolveTransport } = conduit;
302
552
  async function createRuntime(registry, config) {
@@ -375,6 +625,8 @@ export {
375
625
  HeartbeatService,
376
626
  KeyRotationService,
377
627
  SseConnectionService,
378
- createRuntime
628
+ bootstrapSentientRegistry,
629
+ createRuntime,
630
+ createSentientSubsystem
379
631
  };
380
632
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts","../src/services/agent-poller.ts","../src/services/heartbeat.ts","../src/services/key-rotation.ts","../src/services/sse-connection.ts"],"sourcesContent":["/**\n * @cleocode/runtime — Long-running process layer for CLEO.\n *\n * Provides background services: agent polling, SSE connections,\n * heartbeat intervals, and credential rotation.\n *\n * @module runtime\n */\n\nimport type { AgentRegistryAPI, Transport } from '@cleocode/contracts';\nimport { conduit } from '@cleocode/core';\n\nconst { resolveTransport } = conduit;\n\nimport type { AgentPollerConfig } from './services/agent-poller.js';\nimport { AgentPoller } from './services/agent-poller.js';\nimport { HeartbeatService } from './services/heartbeat.js';\nimport { KeyRotationService } from './services/key-rotation.js';\nimport { SseConnectionService } from './services/sse-connection.js';\n\nexport type { AgentPollerConfig, MessageHandler } from './services/agent-poller.js';\nexport { AgentPoller } from './services/agent-poller.js';\nexport type { HeartbeatConfig } from './services/heartbeat.js';\nexport { HeartbeatService } from './services/heartbeat.js';\nexport type { KeyRotationConfig } from './services/key-rotation.js';\nexport { KeyRotationService } from './services/key-rotation.js';\nexport type { SseConnectionConfig, SseMessageHandler } from './services/sse-connection.js';\nexport { SseConnectionService } from './services/sse-connection.js';\n\n/** Configuration for createRuntime(). */\nexport interface RuntimeConfig {\n /** Agent ID to run as. If omitted, uses the most recently used active agent. */\n agentId?: string;\n /** Poll interval in milliseconds. Default: 5000. */\n pollIntervalMs?: number;\n /** Known group conversation IDs to monitor for @mentions. */\n groupConversationIds?: string[];\n /** Max messages per group conversation poll. Default: 15. */\n groupPollLimit?: number;\n /** Heartbeat interval in milliseconds. Default: 30000. Set to 0 to disable. */\n heartbeatIntervalMs?: number;\n /** Max key age in milliseconds before rotation. Default: 30 days. Set to 0 to disable. */\n maxKeyAgeMs?: number;\n /** SSE endpoint URL. If set, enables persistent SSE connection. */\n sseEndpoint?: string;\n /** Transport factory for SSE connection. Caller provides to avoid circular deps. */\n createSseTransport?: () => import('@cleocode/contracts').Transport;\n /**\n * Pre-created transport instance. When provided, bypasses auto-resolution.\n * The transport must NOT be connected yet — createRuntime handles connection.\n */\n transport?: Transport;\n}\n\n/** Handle returned by createRuntime(). */\nexport interface RuntimeHandle {\n /** The AgentPoller instance. */\n poller: AgentPoller;\n /** The HeartbeatService instance (null if disabled). */\n heartbeat: HeartbeatService | null;\n /** The KeyRotationService instance (null if disabled). */\n keyRotation: KeyRotationService | null;\n /** The SseConnectionService instance (null if no SSE endpoint). */\n sseConnection: SseConnectionService | null;\n /** The resolved transport (local, sse, or http). */\n transport: Transport;\n /** The agent ID the runtime is running as. */\n agentId: string;\n /** Stop all runtime services and clean up. */\n stop: () => void;\n}\n\n/**\n * Create and start a runtime from the agent registry.\n *\n * Resolves the agent credential, configures the poller, and starts polling.\n * Returns a handle to register message handlers and stop the runtime.\n *\n * @param registry - AgentRegistryAPI instance for credential lookup.\n * @param config - Optional runtime configuration overrides.\n * @returns A RuntimeHandle with the poller, agentId, and stop function.\n */\nexport async function createRuntime(\n registry: AgentRegistryAPI,\n config?: RuntimeConfig,\n): Promise<RuntimeHandle> {\n const credential = config?.agentId\n ? await registry.get(config.agentId)\n : await registry.getActive();\n\n if (!credential) {\n throw new Error(\n 'No agent credential found. Run: cleo agent register --id <id> --api-key <key>',\n );\n }\n\n // Resolve transport: caller-provided > auto-detected (Local > SSE > HTTP)\n const transport = config?.transport ?? resolveTransport(credential);\n await transport.connect({\n agentId: credential.agentId,\n apiKey: credential.apiKey,\n apiBaseUrl: credential.apiBaseUrl,\n ...credential.transportConfig,\n });\n\n const pollerConfig: AgentPollerConfig = {\n agentId: credential.agentId,\n apiKey: credential.apiKey,\n apiBaseUrl: credential.apiBaseUrl,\n pollIntervalMs: config?.pollIntervalMs ?? credential.transportConfig.pollIntervalMs ?? 5000,\n groupConversationIds: config?.groupConversationIds,\n groupPollLimit: config?.groupPollLimit,\n transport,\n };\n\n const poller = new AgentPoller(pollerConfig);\n\n // Heartbeat service (disabled when intervalMs is 0)\n let heartbeat: HeartbeatService | null = null;\n if (config?.heartbeatIntervalMs !== 0) {\n heartbeat = new HeartbeatService({\n agentId: credential.agentId,\n apiKey: credential.apiKey,\n apiBaseUrl: credential.apiBaseUrl,\n intervalMs: config?.heartbeatIntervalMs,\n });\n heartbeat.start();\n }\n\n // Key rotation service (disabled when maxKeyAgeMs is 0)\n let keyRotation: KeyRotationService | null = null;\n if (config?.maxKeyAgeMs !== 0) {\n keyRotation = new KeyRotationService({\n agentId: credential.agentId,\n registry,\n maxKeyAgeMs: config?.maxKeyAgeMs,\n });\n keyRotation.start();\n }\n\n // SSE connection service (enabled when sseEndpoint + transport factory provided)\n let sseConnection: SseConnectionService | null = null;\n const sseEndpoint = config?.sseEndpoint ?? credential.transportConfig.sseEndpoint;\n if (sseEndpoint && config?.createSseTransport) {\n sseConnection = new SseConnectionService({\n agentId: credential.agentId,\n apiKey: credential.apiKey,\n apiBaseUrl: credential.apiBaseUrl,\n sseEndpoint,\n transport: config.createSseTransport(),\n });\n // Start is async but we don't block createRuntime on it\n void sseConnection.start();\n }\n\n return {\n poller,\n heartbeat,\n keyRotation,\n sseConnection,\n transport,\n agentId: credential.agentId,\n stop: () => {\n poller.stop();\n heartbeat?.stop();\n keyRotation?.stop();\n void sseConnection?.stop();\n void transport.disconnect();\n },\n };\n}\n","/**\n * AgentPoller — Polls for messages via HttpTransport AND group conversations.\n *\n * Fixes the group @mention blind spot: the peek endpoint only matches\n * to_agent_id (DMs). This poller ALSO checks known group conversation\n * messages for @agentId content matches.\n *\n * @task T183\n */\n\nimport type { ConduitMessage, Transport } from '@cleocode/contracts';\n\n/** Message handler callback. */\nexport type MessageHandler = (message: ConduitMessage) => void;\n\n/** Poller configuration. */\nexport interface AgentPollerConfig {\n /** Agent ID to poll as. */\n agentId: string;\n /** API key for authentication. */\n apiKey: string;\n /** API base URL. */\n apiBaseUrl: string;\n /** Poll interval in milliseconds. Default: 5000. */\n pollIntervalMs?: number;\n /** Known group conversation IDs to monitor for @mentions. */\n groupConversationIds?: string[];\n /** Max messages to fetch per group conversation poll. Default: 15. */\n groupPollLimit?: number;\n /**\n * Transport instance for polling messages.\n * When provided, poll() delegates to transport.poll() instead of raw HTTP.\n * The transport must already be connected before passing to AgentPoller.\n */\n transport?: Transport;\n}\n\n/** Tracks seen message IDs for dedup. */\nconst DEFAULT_POLL_INTERVAL = 5000;\nconst DEFAULT_GROUP_POLL_LIMIT = 15;\n\n/**\n * AgentPoller service — polls peek endpoint AND group conversations.\n * Deduplicates messages by ID across both sources.\n */\nexport class AgentPoller {\n private config: AgentPollerConfig;\n private handler: MessageHandler | null = null;\n private interval: ReturnType<typeof setInterval> | null = null;\n private seenMessageIds = new Set<string>();\n private running = false;\n\n constructor(config: AgentPollerConfig) {\n this.config = config;\n }\n\n /** Register a message handler. */\n onMessage(handler: MessageHandler): void {\n this.handler = handler;\n }\n\n /** Start the polling loop. */\n start(): void {\n if (this.running) return;\n this.running = true;\n\n const intervalMs = this.config.pollIntervalMs ?? DEFAULT_POLL_INTERVAL;\n\n // Initial poll immediately\n void this.pollCycle();\n\n this.interval = setInterval(() => {\n void this.pollCycle();\n }, intervalMs);\n }\n\n /** Stop the polling loop. */\n stop(): void {\n this.running = false;\n if (this.interval) {\n clearInterval(this.interval);\n this.interval = null;\n }\n }\n\n /** Get poller status. */\n status(): { running: boolean; seenCount: number } {\n return {\n running: this.running,\n seenCount: this.seenMessageIds.size,\n };\n }\n\n /** Single poll cycle — peek + group conversations. */\n private async pollCycle(): Promise<void> {\n if (!this.handler) return;\n\n const newMessages: ConduitMessage[] = [];\n\n // Track 1: Standard peek endpoint (catches DMs)\n try {\n const peekMessages = await this.peekMessages();\n for (const msg of peekMessages) {\n if (!this.seenMessageIds.has(msg.id)) {\n this.seenMessageIds.add(msg.id);\n newMessages.push(msg);\n }\n }\n } catch {\n // Best-effort — don't crash the loop\n }\n\n // Track 2: Group conversation polling (catches @mentions in group messages)\n const groupIds = this.config.groupConversationIds ?? [];\n for (const convId of groupIds) {\n try {\n const groupMessages = await this.pollGroupConversation(convId);\n for (const msg of groupMessages) {\n if (!this.seenMessageIds.has(msg.id)) {\n this.seenMessageIds.add(msg.id);\n newMessages.push(msg);\n }\n }\n } catch {\n // Best-effort per conversation\n }\n }\n\n // Deliver new messages to handler\n for (const msg of newMessages) {\n this.handler(msg);\n }\n\n // Prevent unbounded growth of seen set (keep last 5000)\n if (this.seenMessageIds.size > 5000) {\n const entries = [...this.seenMessageIds];\n this.seenMessageIds = new Set(entries.slice(-3000));\n }\n }\n\n /** Peek for messages mentioning this agent. Delegates to transport when available. */\n private async peekMessages(): Promise<ConduitMessage[]> {\n if (this.config.transport) {\n return this.config.transport.poll({ limit: 50 });\n }\n\n // Fallback: raw HTTP when no transport injected\n const params = new URLSearchParams();\n params.set('mentioned', this.config.agentId);\n params.set('limit', '50');\n\n const url = `${this.config.apiBaseUrl}/messages/peek?${params}`;\n const response = await fetch(url, {\n method: 'GET',\n headers: this.headers(),\n });\n\n if (!response.ok) return [];\n\n const data = (await response.json()) as {\n data?: {\n messages?: Array<{\n id: string;\n fromAgentId?: string;\n content?: string;\n conversationId?: string;\n createdAt?: string;\n }>;\n };\n };\n\n return (data.data?.messages ?? []).map((m) => ({\n id: m.id,\n from: m.fromAgentId ?? 'unknown',\n content: m.content ?? '',\n threadId: m.conversationId,\n timestamp: m.createdAt ?? new Date().toISOString(),\n }));\n }\n\n /**\n * Poll a group conversation for recent messages that @mention this agent.\n * This is the fix for the group @mention blind spot.\n */\n private async pollGroupConversation(conversationId: string): Promise<ConduitMessage[]> {\n const limit = this.config.groupPollLimit ?? DEFAULT_GROUP_POLL_LIMIT;\n const url = `${this.config.apiBaseUrl}/conversations/${conversationId}/messages?sort=desc&limit=${limit}`;\n\n const response = await fetch(url, {\n method: 'GET',\n headers: this.headers(),\n });\n\n if (!response.ok) return [];\n\n const data = (await response.json()) as {\n data?: {\n messages?: Array<{\n id: string;\n fromAgentId?: string;\n content?: string;\n conversationId?: string;\n createdAt?: string;\n }>;\n };\n };\n\n const mentionPattern = new RegExp(`@${this.config.agentId}\\\\b|@all\\\\b`, 'i');\n\n return (data.data?.messages ?? [])\n .filter((m) => {\n // Only deliver messages that mention us or @all\n const content = m.content ?? '';\n return mentionPattern.test(content) && m.fromAgentId !== this.config.agentId;\n })\n .map((m) => ({\n id: m.id,\n from: m.fromAgentId ?? 'unknown',\n content: m.content ?? '',\n threadId: m.conversationId ?? conversationId,\n timestamp: m.createdAt ?? new Date().toISOString(),\n }));\n }\n\n /** Build auth headers. */\n private headers(): Record<string, string> {\n return {\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${this.config.apiKey}`,\n 'X-Agent-Id': this.config.agentId,\n };\n }\n}\n","/**\n * HeartbeatService — Periodic online status heartbeat.\n *\n * Sends a heartbeat to the SignalDock API at a configurable interval\n * to maintain the agent's online status. If the heartbeat fails,\n * it retries silently — the agent continues operating regardless.\n *\n * @task T218\n */\n\n/** Heartbeat service configuration. */\nexport interface HeartbeatConfig {\n /** Agent ID to send heartbeats for. */\n agentId: string;\n /** API key for authentication. */\n apiKey: string;\n /** API base URL. */\n apiBaseUrl: string;\n /** Heartbeat interval in milliseconds. Default: 30000 (30s). */\n intervalMs?: number;\n}\n\n/** Default heartbeat interval: 30 seconds. */\nconst DEFAULT_INTERVAL_MS = 30_000;\n\n/** HeartbeatService sends periodic online status to the cloud API. */\nexport class HeartbeatService {\n private config: HeartbeatConfig;\n private timer: ReturnType<typeof setInterval> | null = null;\n private running = false;\n private consecutiveFailures = 0;\n\n constructor(config: HeartbeatConfig) {\n this.config = config;\n }\n\n /** Start sending heartbeats at the configured interval. */\n start(): void {\n if (this.running) return;\n this.running = true;\n this.consecutiveFailures = 0;\n\n const intervalMs = this.config.intervalMs ?? DEFAULT_INTERVAL_MS;\n\n // Send initial heartbeat immediately\n void this.sendHeartbeat();\n\n this.timer = setInterval(() => {\n void this.sendHeartbeat();\n }, intervalMs);\n }\n\n /** Stop sending heartbeats. */\n stop(): void {\n this.running = false;\n if (this.timer) {\n clearInterval(this.timer);\n this.timer = null;\n }\n }\n\n /** Get heartbeat service status. */\n status(): { running: boolean; consecutiveFailures: number } {\n return {\n running: this.running,\n consecutiveFailures: this.consecutiveFailures,\n };\n }\n\n /** Send a single heartbeat to the cloud API. */\n private async sendHeartbeat(): Promise<void> {\n try {\n const response = await fetch(\n `${this.config.apiBaseUrl}/agents/${this.config.agentId}/heartbeat`,\n {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${this.config.apiKey}`,\n 'X-Agent-Id': this.config.agentId,\n },\n body: JSON.stringify({ status: 'online' }),\n signal: AbortSignal.timeout(10_000),\n },\n );\n\n if (response.ok) {\n this.consecutiveFailures = 0;\n } else {\n this.consecutiveFailures++;\n }\n } catch {\n this.consecutiveFailures++;\n }\n }\n}\n","/**\n * KeyRotationService — Automatic API key rotation based on credential age.\n *\n * Monitors the age of the agent's API key and triggers rotation when\n * the key exceeds the configured threshold. Uses the AgentRegistryAPI\n * to perform the actual rotation (which calls the cloud API and\n * re-encrypts the new key locally).\n *\n * @task T218\n */\n\nimport type { AgentRegistryAPI } from '@cleocode/contracts';\n\n/** Key rotation service configuration. */\nexport interface KeyRotationConfig {\n /** Agent ID to monitor. */\n agentId: string;\n /** AgentRegistryAPI instance for credential lookup and rotation. */\n registry: AgentRegistryAPI;\n /** Check interval in milliseconds. Default: 3600000 (1 hour). */\n checkIntervalMs?: number;\n /** Max key age in milliseconds before rotation. Default: 2592000000 (30 days). */\n maxKeyAgeMs?: number;\n}\n\n/** Default check interval: 1 hour. */\nconst DEFAULT_CHECK_INTERVAL_MS = 3_600_000;\n\n/** Default max key age: 30 days. */\nconst DEFAULT_MAX_KEY_AGE_MS = 30 * 24 * 60 * 60 * 1000;\n\n/** KeyRotationService monitors credential age and auto-rotates when threshold is exceeded. */\nexport class KeyRotationService {\n private config: KeyRotationConfig;\n private timer: ReturnType<typeof setInterval> | null = null;\n private running = false;\n private lastRotationAt: string | null = null;\n\n constructor(config: KeyRotationConfig) {\n this.config = config;\n }\n\n /** Start monitoring key age at the configured interval. */\n start(): void {\n if (this.running) return;\n this.running = true;\n\n const intervalMs = this.config.checkIntervalMs ?? DEFAULT_CHECK_INTERVAL_MS;\n\n // Initial check after a short delay (don't rotate immediately on startup)\n setTimeout(() => {\n void this.checkAndRotate();\n }, 5000);\n\n this.timer = setInterval(() => {\n void this.checkAndRotate();\n }, intervalMs);\n }\n\n /** Stop monitoring. */\n stop(): void {\n this.running = false;\n if (this.timer) {\n clearInterval(this.timer);\n this.timer = null;\n }\n }\n\n /** Get rotation service status. */\n status(): { running: boolean; lastRotationAt: string | null } {\n return {\n running: this.running,\n lastRotationAt: this.lastRotationAt,\n };\n }\n\n /** Check credential age and rotate if needed. */\n private async checkAndRotate(): Promise<void> {\n try {\n const credential = await this.config.registry.get(this.config.agentId);\n if (!credential) return;\n\n const maxAge = this.config.maxKeyAgeMs ?? DEFAULT_MAX_KEY_AGE_MS;\n const credentialAge = Date.now() - new Date(credential.updatedAt).getTime();\n\n if (credentialAge > maxAge) {\n await this.config.registry.rotateKey(this.config.agentId);\n this.lastRotationAt = new Date().toISOString();\n }\n } catch {\n // Rotation failure is non-fatal — will retry next interval\n }\n }\n}\n","/**\n * SseConnectionService — Persistent SSE connection manager.\n *\n * Maintains a persistent SSE connection to the SignalDock API for\n * real-time message delivery. Wraps SseTransport with lifecycle\n * management: start, stop, reconnect, and message forwarding.\n *\n * When SSE is available, messages arrive in real-time. When it falls\n * back to HTTP polling (managed by SseTransport internally), the\n * service continues operating transparently.\n *\n * @task T218\n */\n\nimport type { ConduitMessage, Transport } from '@cleocode/contracts';\n\n/** SSE connection service configuration. */\nexport interface SseConnectionConfig {\n /** Agent ID to connect as. */\n agentId: string;\n /** API key for authentication. */\n apiKey: string;\n /** API base URL. */\n apiBaseUrl: string;\n /** SSE endpoint URL. If omitted, uses apiBaseUrl + /sse. */\n sseEndpoint?: string;\n /** Transport instance to use. Injected by createRuntime. */\n transport: Transport;\n}\n\n/** Message handler callback. */\nexport type SseMessageHandler = (message: ConduitMessage) => void;\n\n/** SseConnectionService manages a persistent transport with subscribe() support. */\nexport class SseConnectionService {\n private config: SseConnectionConfig;\n private handler: SseMessageHandler | null = null;\n private unsubscribe: (() => void) | null = null;\n private running = false;\n\n constructor(config: SseConnectionConfig) {\n this.config = config;\n }\n\n /** Register a message handler for incoming messages. */\n onMessage(handler: SseMessageHandler): void {\n this.handler = handler;\n }\n\n /** Start the SSE connection. */\n async start(): Promise<void> {\n if (this.running) return;\n this.running = true;\n\n await this.config.transport.connect({\n agentId: this.config.agentId,\n apiKey: this.config.apiKey,\n apiBaseUrl: this.config.apiBaseUrl,\n sseEndpoint: this.config.sseEndpoint,\n });\n\n // Subscribe to incoming messages if transport supports it\n if (this.handler && this.config.transport.subscribe) {\n this.unsubscribe = this.config.transport.subscribe(this.handler);\n }\n }\n\n /** Stop the connection and clean up. */\n async stop(): Promise<void> {\n this.running = false;\n\n if (this.unsubscribe) {\n this.unsubscribe();\n this.unsubscribe = null;\n }\n\n await this.config.transport.disconnect();\n }\n\n /** Get connection service status. */\n status(): { running: boolean; transportName: string } {\n return {\n running: this.running,\n transportName: this.config.transport.name,\n };\n }\n}\n"],"mappings":";AAUA,SAAS,eAAe;;;AC4BxB,IAAM,wBAAwB;AAC9B,IAAM,2BAA2B;AAM1B,IAAM,cAAN,MAAkB;AAAA,EACf;AAAA,EACA,UAAiC;AAAA,EACjC,WAAkD;AAAA,EAClD,iBAAiB,oBAAI,IAAY;AAAA,EACjC,UAAU;AAAA,EAElB,YAAY,QAA2B;AACrC,SAAK,SAAS;AAAA,EAChB;AAAA;AAAA,EAGA,UAAU,SAA+B;AACvC,SAAK,UAAU;AAAA,EACjB;AAAA;AAAA,EAGA,QAAc;AACZ,QAAI,KAAK,QAAS;AAClB,SAAK,UAAU;AAEf,UAAM,aAAa,KAAK,OAAO,kBAAkB;AAGjD,SAAK,KAAK,UAAU;AAEpB,SAAK,WAAW,YAAY,MAAM;AAChC,WAAK,KAAK,UAAU;AAAA,IACtB,GAAG,UAAU;AAAA,EACf;AAAA;AAAA,EAGA,OAAa;AACX,SAAK,UAAU;AACf,QAAI,KAAK,UAAU;AACjB,oBAAc,KAAK,QAAQ;AAC3B,WAAK,WAAW;AAAA,IAClB;AAAA,EACF;AAAA;AAAA,EAGA,SAAkD;AAChD,WAAO;AAAA,MACL,SAAS,KAAK;AAAA,MACd,WAAW,KAAK,eAAe;AAAA,IACjC;AAAA,EACF;AAAA;AAAA,EAGA,MAAc,YAA2B;AACvC,QAAI,CAAC,KAAK,QAAS;AAEnB,UAAM,cAAgC,CAAC;AAGvC,QAAI;AACF,YAAM,eAAe,MAAM,KAAK,aAAa;AAC7C,iBAAW,OAAO,cAAc;AAC9B,YAAI,CAAC,KAAK,eAAe,IAAI,IAAI,EAAE,GAAG;AACpC,eAAK,eAAe,IAAI,IAAI,EAAE;AAC9B,sBAAY,KAAK,GAAG;AAAA,QACtB;AAAA,MACF;AAAA,IACF,QAAQ;AAAA,IAER;AAGA,UAAM,WAAW,KAAK,OAAO,wBAAwB,CAAC;AACtD,eAAW,UAAU,UAAU;AAC7B,UAAI;AACF,cAAM,gBAAgB,MAAM,KAAK,sBAAsB,MAAM;AAC7D,mBAAW,OAAO,eAAe;AAC/B,cAAI,CAAC,KAAK,eAAe,IAAI,IAAI,EAAE,GAAG;AACpC,iBAAK,eAAe,IAAI,IAAI,EAAE;AAC9B,wBAAY,KAAK,GAAG;AAAA,UACtB;AAAA,QACF;AAAA,MACF,QAAQ;AAAA,MAER;AAAA,IACF;AAGA,eAAW,OAAO,aAAa;AAC7B,WAAK,QAAQ,GAAG;AAAA,IAClB;AAGA,QAAI,KAAK,eAAe,OAAO,KAAM;AACnC,YAAM,UAAU,CAAC,GAAG,KAAK,cAAc;AACvC,WAAK,iBAAiB,IAAI,IAAI,QAAQ,MAAM,IAAK,CAAC;AAAA,IACpD;AAAA,EACF;AAAA;AAAA,EAGA,MAAc,eAA0C;AACtD,QAAI,KAAK,OAAO,WAAW;AACzB,aAAO,KAAK,OAAO,UAAU,KAAK,EAAE,OAAO,GAAG,CAAC;AAAA,IACjD;AAGA,UAAM,SAAS,IAAI,gBAAgB;AACnC,WAAO,IAAI,aAAa,KAAK,OAAO,OAAO;AAC3C,WAAO,IAAI,SAAS,IAAI;AAExB,UAAM,MAAM,GAAG,KAAK,OAAO,UAAU,kBAAkB,MAAM;AAC7D,UAAM,WAAW,MAAM,MAAM,KAAK;AAAA,MAChC,QAAQ;AAAA,MACR,SAAS,KAAK,QAAQ;AAAA,IACxB,CAAC;AAED,QAAI,CAAC,SAAS,GAAI,QAAO,CAAC;AAE1B,UAAM,OAAQ,MAAM,SAAS,KAAK;AAYlC,YAAQ,KAAK,MAAM,YAAY,CAAC,GAAG,IAAI,CAAC,OAAO;AAAA,MAC7C,IAAI,EAAE;AAAA,MACN,MAAM,EAAE,eAAe;AAAA,MACvB,SAAS,EAAE,WAAW;AAAA,MACtB,UAAU,EAAE;AAAA,MACZ,WAAW,EAAE,cAAa,oBAAI,KAAK,GAAE,YAAY;AAAA,IACnD,EAAE;AAAA,EACJ;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,MAAc,sBAAsB,gBAAmD;AACrF,UAAM,QAAQ,KAAK,OAAO,kBAAkB;AAC5C,UAAM,MAAM,GAAG,KAAK,OAAO,UAAU,kBAAkB,cAAc,6BAA6B,KAAK;AAEvG,UAAM,WAAW,MAAM,MAAM,KAAK;AAAA,MAChC,QAAQ;AAAA,MACR,SAAS,KAAK,QAAQ;AAAA,IACxB,CAAC;AAED,QAAI,CAAC,SAAS,GAAI,QAAO,CAAC;AAE1B,UAAM,OAAQ,MAAM,SAAS,KAAK;AAYlC,UAAM,iBAAiB,IAAI,OAAO,IAAI,KAAK,OAAO,OAAO,eAAe,GAAG;AAE3E,YAAQ,KAAK,MAAM,YAAY,CAAC,GAC7B,OAAO,CAAC,MAAM;AAEb,YAAM,UAAU,EAAE,WAAW;AAC7B,aAAO,eAAe,KAAK,OAAO,KAAK,EAAE,gBAAgB,KAAK,OAAO;AAAA,IACvE,CAAC,EACA,IAAI,CAAC,OAAO;AAAA,MACX,IAAI,EAAE;AAAA,MACN,MAAM,EAAE,eAAe;AAAA,MACvB,SAAS,EAAE,WAAW;AAAA,MACtB,UAAU,EAAE,kBAAkB;AAAA,MAC9B,WAAW,EAAE,cAAa,oBAAI,KAAK,GAAE,YAAY;AAAA,IACnD,EAAE;AAAA,EACN;AAAA;AAAA,EAGQ,UAAkC;AACxC,WAAO;AAAA,MACL,gBAAgB;AAAA,MAChB,eAAe,UAAU,KAAK,OAAO,MAAM;AAAA,MAC3C,cAAc,KAAK,OAAO;AAAA,IAC5B;AAAA,EACF;AACF;;;ACjNA,IAAM,sBAAsB;AAGrB,IAAM,mBAAN,MAAuB;AAAA,EACpB;AAAA,EACA,QAA+C;AAAA,EAC/C,UAAU;AAAA,EACV,sBAAsB;AAAA,EAE9B,YAAY,QAAyB;AACnC,SAAK,SAAS;AAAA,EAChB;AAAA;AAAA,EAGA,QAAc;AACZ,QAAI,KAAK,QAAS;AAClB,SAAK,UAAU;AACf,SAAK,sBAAsB;AAE3B,UAAM,aAAa,KAAK,OAAO,cAAc;AAG7C,SAAK,KAAK,cAAc;AAExB,SAAK,QAAQ,YAAY,MAAM;AAC7B,WAAK,KAAK,cAAc;AAAA,IAC1B,GAAG,UAAU;AAAA,EACf;AAAA;AAAA,EAGA,OAAa;AACX,SAAK,UAAU;AACf,QAAI,KAAK,OAAO;AACd,oBAAc,KAAK,KAAK;AACxB,WAAK,QAAQ;AAAA,IACf;AAAA,EACF;AAAA;AAAA,EAGA,SAA4D;AAC1D,WAAO;AAAA,MACL,SAAS,KAAK;AAAA,MACd,qBAAqB,KAAK;AAAA,IAC5B;AAAA,EACF;AAAA;AAAA,EAGA,MAAc,gBAA+B;AAC3C,QAAI;AACF,YAAM,WAAW,MAAM;AAAA,QACrB,GAAG,KAAK,OAAO,UAAU,WAAW,KAAK,OAAO,OAAO;AAAA,QACvD;AAAA,UACE,QAAQ;AAAA,UACR,SAAS;AAAA,YACP,gBAAgB;AAAA,YAChB,eAAe,UAAU,KAAK,OAAO,MAAM;AAAA,YAC3C,cAAc,KAAK,OAAO;AAAA,UAC5B;AAAA,UACA,MAAM,KAAK,UAAU,EAAE,QAAQ,SAAS,CAAC;AAAA,UACzC,QAAQ,YAAY,QAAQ,GAAM;AAAA,QACpC;AAAA,MACF;AAEA,UAAI,SAAS,IAAI;AACf,aAAK,sBAAsB;AAAA,MAC7B,OAAO;AACL,aAAK;AAAA,MACP;AAAA,IACF,QAAQ;AACN,WAAK;AAAA,IACP;AAAA,EACF;AACF;;;ACrEA,IAAM,4BAA4B;AAGlC,IAAM,yBAAyB,KAAK,KAAK,KAAK,KAAK;AAG5C,IAAM,qBAAN,MAAyB;AAAA,EACtB;AAAA,EACA,QAA+C;AAAA,EAC/C,UAAU;AAAA,EACV,iBAAgC;AAAA,EAExC,YAAY,QAA2B;AACrC,SAAK,SAAS;AAAA,EAChB;AAAA;AAAA,EAGA,QAAc;AACZ,QAAI,KAAK,QAAS;AAClB,SAAK,UAAU;AAEf,UAAM,aAAa,KAAK,OAAO,mBAAmB;AAGlD,eAAW,MAAM;AACf,WAAK,KAAK,eAAe;AAAA,IAC3B,GAAG,GAAI;AAEP,SAAK,QAAQ,YAAY,MAAM;AAC7B,WAAK,KAAK,eAAe;AAAA,IAC3B,GAAG,UAAU;AAAA,EACf;AAAA;AAAA,EAGA,OAAa;AACX,SAAK,UAAU;AACf,QAAI,KAAK,OAAO;AACd,oBAAc,KAAK,KAAK;AACxB,WAAK,QAAQ;AAAA,IACf;AAAA,EACF;AAAA;AAAA,EAGA,SAA8D;AAC5D,WAAO;AAAA,MACL,SAAS,KAAK;AAAA,MACd,gBAAgB,KAAK;AAAA,IACvB;AAAA,EACF;AAAA;AAAA,EAGA,MAAc,iBAAgC;AAC5C,QAAI;AACF,YAAM,aAAa,MAAM,KAAK,OAAO,SAAS,IAAI,KAAK,OAAO,OAAO;AACrE,UAAI,CAAC,WAAY;AAEjB,YAAM,SAAS,KAAK,OAAO,eAAe;AAC1C,YAAM,gBAAgB,KAAK,IAAI,IAAI,IAAI,KAAK,WAAW,SAAS,EAAE,QAAQ;AAE1E,UAAI,gBAAgB,QAAQ;AAC1B,cAAM,KAAK,OAAO,SAAS,UAAU,KAAK,OAAO,OAAO;AACxD,aAAK,kBAAiB,oBAAI,KAAK,GAAE,YAAY;AAAA,MAC/C;AAAA,IACF,QAAQ;AAAA,IAER;AAAA,EACF;AACF;;;AC3DO,IAAM,uBAAN,MAA2B;AAAA,EACxB;AAAA,EACA,UAAoC;AAAA,EACpC,cAAmC;AAAA,EACnC,UAAU;AAAA,EAElB,YAAY,QAA6B;AACvC,SAAK,SAAS;AAAA,EAChB;AAAA;AAAA,EAGA,UAAU,SAAkC;AAC1C,SAAK,UAAU;AAAA,EACjB;AAAA;AAAA,EAGA,MAAM,QAAuB;AAC3B,QAAI,KAAK,QAAS;AAClB,SAAK,UAAU;AAEf,UAAM,KAAK,OAAO,UAAU,QAAQ;AAAA,MAClC,SAAS,KAAK,OAAO;AAAA,MACrB,QAAQ,KAAK,OAAO;AAAA,MACpB,YAAY,KAAK,OAAO;AAAA,MACxB,aAAa,KAAK,OAAO;AAAA,IAC3B,CAAC;AAGD,QAAI,KAAK,WAAW,KAAK,OAAO,UAAU,WAAW;AACnD,WAAK,cAAc,KAAK,OAAO,UAAU,UAAU,KAAK,OAAO;AAAA,IACjE;AAAA,EACF;AAAA;AAAA,EAGA,MAAM,OAAsB;AAC1B,SAAK,UAAU;AAEf,QAAI,KAAK,aAAa;AACpB,WAAK,YAAY;AACjB,WAAK,cAAc;AAAA,IACrB;AAEA,UAAM,KAAK,OAAO,UAAU,WAAW;AAAA,EACzC;AAAA;AAAA,EAGA,SAAsD;AACpD,WAAO;AAAA,MACL,SAAS,KAAK;AAAA,MACd,eAAe,KAAK,OAAO,UAAU;AAAA,IACvC;AAAA,EACF;AACF;;;AJ1EA,IAAM,EAAE,iBAAiB,IAAI;AAsE7B,eAAsB,cACpB,UACA,QACwB;AACxB,QAAM,aAAa,QAAQ,UACvB,MAAM,SAAS,IAAI,OAAO,OAAO,IACjC,MAAM,SAAS,UAAU;AAE7B,MAAI,CAAC,YAAY;AACf,UAAM,IAAI;AAAA,MACR;AAAA,IACF;AAAA,EACF;AAGA,QAAM,YAAY,QAAQ,aAAa,iBAAiB,UAAU;AAClE,QAAM,UAAU,QAAQ;AAAA,IACtB,SAAS,WAAW;AAAA,IACpB,QAAQ,WAAW;AAAA,IACnB,YAAY,WAAW;AAAA,IACvB,GAAG,WAAW;AAAA,EAChB,CAAC;AAED,QAAM,eAAkC;AAAA,IACtC,SAAS,WAAW;AAAA,IACpB,QAAQ,WAAW;AAAA,IACnB,YAAY,WAAW;AAAA,IACvB,gBAAgB,QAAQ,kBAAkB,WAAW,gBAAgB,kBAAkB;AAAA,IACvF,sBAAsB,QAAQ;AAAA,IAC9B,gBAAgB,QAAQ;AAAA,IACxB;AAAA,EACF;AAEA,QAAM,SAAS,IAAI,YAAY,YAAY;AAG3C,MAAI,YAAqC;AACzC,MAAI,QAAQ,wBAAwB,GAAG;AACrC,gBAAY,IAAI,iBAAiB;AAAA,MAC/B,SAAS,WAAW;AAAA,MACpB,QAAQ,WAAW;AAAA,MACnB,YAAY,WAAW;AAAA,MACvB,YAAY,QAAQ;AAAA,IACtB,CAAC;AACD,cAAU,MAAM;AAAA,EAClB;AAGA,MAAI,cAAyC;AAC7C,MAAI,QAAQ,gBAAgB,GAAG;AAC7B,kBAAc,IAAI,mBAAmB;AAAA,MACnC,SAAS,WAAW;AAAA,MACpB;AAAA,MACA,aAAa,QAAQ;AAAA,IACvB,CAAC;AACD,gBAAY,MAAM;AAAA,EACpB;AAGA,MAAI,gBAA6C;AACjD,QAAM,cAAc,QAAQ,eAAe,WAAW,gBAAgB;AACtE,MAAI,eAAe,QAAQ,oBAAoB;AAC7C,oBAAgB,IAAI,qBAAqB;AAAA,MACvC,SAAS,WAAW;AAAA,MACpB,QAAQ,WAAW;AAAA,MACnB,YAAY,WAAW;AAAA,MACvB;AAAA,MACA,WAAW,OAAO,mBAAmB;AAAA,IACvC,CAAC;AAED,SAAK,cAAc,MAAM;AAAA,EAC3B;AAEA,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA,SAAS,WAAW;AAAA,IACpB,MAAM,MAAM;AACV,aAAO,KAAK;AACZ,iBAAW,KAAK;AAChB,mBAAa,KAAK;AAClB,WAAK,eAAe,KAAK;AACzB,WAAK,UAAU,WAAW;AAAA,IAC5B;AAAA,EACF;AACF;","names":[]}
1
+ {"version":3,"sources":["../src/index.ts","../src/services/agent-poller.ts","../src/services/heartbeat.ts","../src/services/key-rotation.ts","../src/services/sse-connection.ts","../src/sentient-subsystem.ts"],"sourcesContent":["/**\n * @cleocode/runtime — Long-running process layer for CLEO.\n *\n * Provides background services: agent polling, SSE connections,\n * heartbeat intervals, and credential rotation.\n *\n * @module runtime\n */\n\nimport type { AgentRegistryAPI, Transport } from '@cleocode/contracts';\nimport { conduit } from '@cleocode/core';\n\nconst { resolveTransport } = conduit;\n\nimport type { AgentPollerConfig } from './services/agent-poller.js';\nimport { AgentPoller } from './services/agent-poller.js';\nimport { HeartbeatService } from './services/heartbeat.js';\nimport { KeyRotationService } from './services/key-rotation.js';\nimport { SseConnectionService } from './services/sse-connection.js';\n\n// R4 (T11255) — sentient daemon subsystem adapter\nexport {\n bootstrapSentientRegistry,\n createSentientSubsystem,\n} from './sentient-subsystem.js';\nexport type { AgentPollerConfig, MessageHandler } from './services/agent-poller.js';\nexport { AgentPoller } from './services/agent-poller.js';\nexport type { HeartbeatConfig } from './services/heartbeat.js';\nexport { HeartbeatService } from './services/heartbeat.js';\nexport type { KeyRotationConfig } from './services/key-rotation.js';\nexport { KeyRotationService } from './services/key-rotation.js';\nexport type { SseConnectionConfig, SseMessageHandler } from './services/sse-connection.js';\nexport { SseConnectionService } from './services/sse-connection.js';\n\n/** Configuration for createRuntime(). */\nexport interface RuntimeConfig {\n /** Agent ID to run as. If omitted, uses the most recently used active agent. */\n agentId?: string;\n /** Poll interval in milliseconds. Default: 5000. */\n pollIntervalMs?: number;\n /** Known group conversation IDs to monitor for @mentions. */\n groupConversationIds?: string[];\n /** Max messages per group conversation poll. Default: 15. */\n groupPollLimit?: number;\n /** Heartbeat interval in milliseconds. Default: 30000. Set to 0 to disable. */\n heartbeatIntervalMs?: number;\n /** Max key age in milliseconds before rotation. Default: 30 days. Set to 0 to disable. */\n maxKeyAgeMs?: number;\n /** SSE endpoint URL. If set, enables persistent SSE connection. */\n sseEndpoint?: string;\n /** Transport factory for SSE connection. Caller provides to avoid circular deps. */\n createSseTransport?: () => import('@cleocode/contracts').Transport;\n /**\n * Pre-created transport instance. When provided, bypasses auto-resolution.\n * The transport must NOT be connected yet — createRuntime handles connection.\n */\n transport?: Transport;\n}\n\n/** Handle returned by createRuntime(). */\nexport interface RuntimeHandle {\n /** The AgentPoller instance. */\n poller: AgentPoller;\n /** The HeartbeatService instance (null if disabled). */\n heartbeat: HeartbeatService | null;\n /** The KeyRotationService instance (null if disabled). */\n keyRotation: KeyRotationService | null;\n /** The SseConnectionService instance (null if no SSE endpoint). */\n sseConnection: SseConnectionService | null;\n /** The resolved transport (local, sse, or http). */\n transport: Transport;\n /** The agent ID the runtime is running as. */\n agentId: string;\n /** Stop all runtime services and clean up. */\n stop: () => void;\n}\n\n/**\n * Create and start a runtime from the agent registry.\n *\n * Resolves the agent credential, configures the poller, and starts polling.\n * Returns a handle to register message handlers and stop the runtime.\n *\n * @param registry - AgentRegistryAPI instance for credential lookup.\n * @param config - Optional runtime configuration overrides.\n * @returns A RuntimeHandle with the poller, agentId, and stop function.\n */\nexport async function createRuntime(\n registry: AgentRegistryAPI,\n config?: RuntimeConfig,\n): Promise<RuntimeHandle> {\n const credential = config?.agentId\n ? await registry.get(config.agentId)\n : await registry.getActive();\n\n if (!credential) {\n throw new Error(\n 'No agent credential found. Run: cleo agent register --id <id> --api-key <key>',\n );\n }\n\n // Resolve transport: caller-provided > auto-detected (Local > SSE > HTTP)\n const transport = config?.transport ?? resolveTransport(credential);\n await transport.connect({\n agentId: credential.agentId,\n apiKey: credential.apiKey,\n apiBaseUrl: credential.apiBaseUrl,\n ...credential.transportConfig,\n });\n\n const pollerConfig: AgentPollerConfig = {\n agentId: credential.agentId,\n apiKey: credential.apiKey,\n apiBaseUrl: credential.apiBaseUrl,\n pollIntervalMs: config?.pollIntervalMs ?? credential.transportConfig.pollIntervalMs ?? 5000,\n groupConversationIds: config?.groupConversationIds,\n groupPollLimit: config?.groupPollLimit,\n transport,\n };\n\n const poller = new AgentPoller(pollerConfig);\n\n // Heartbeat service (disabled when intervalMs is 0)\n let heartbeat: HeartbeatService | null = null;\n if (config?.heartbeatIntervalMs !== 0) {\n heartbeat = new HeartbeatService({\n agentId: credential.agentId,\n apiKey: credential.apiKey,\n apiBaseUrl: credential.apiBaseUrl,\n intervalMs: config?.heartbeatIntervalMs,\n });\n heartbeat.start();\n }\n\n // Key rotation service (disabled when maxKeyAgeMs is 0)\n let keyRotation: KeyRotationService | null = null;\n if (config?.maxKeyAgeMs !== 0) {\n keyRotation = new KeyRotationService({\n agentId: credential.agentId,\n registry,\n maxKeyAgeMs: config?.maxKeyAgeMs,\n });\n keyRotation.start();\n }\n\n // SSE connection service (enabled when sseEndpoint + transport factory provided)\n let sseConnection: SseConnectionService | null = null;\n const sseEndpoint = config?.sseEndpoint ?? credential.transportConfig.sseEndpoint;\n if (sseEndpoint && config?.createSseTransport) {\n sseConnection = new SseConnectionService({\n agentId: credential.agentId,\n apiKey: credential.apiKey,\n apiBaseUrl: credential.apiBaseUrl,\n sseEndpoint,\n transport: config.createSseTransport(),\n });\n // Start is async but we don't block createRuntime on it\n void sseConnection.start();\n }\n\n return {\n poller,\n heartbeat,\n keyRotation,\n sseConnection,\n transport,\n agentId: credential.agentId,\n stop: () => {\n poller.stop();\n heartbeat?.stop();\n keyRotation?.stop();\n void sseConnection?.stop();\n void transport.disconnect();\n },\n };\n}\n","/**\n * AgentPoller — Polls for messages via HttpTransport AND group conversations.\n *\n * Fixes the group @mention blind spot: the peek endpoint only matches\n * to_agent_id (DMs). This poller ALSO checks known group conversation\n * messages for @agentId content matches.\n *\n * @task T183\n */\n\nimport type { ConduitMessage, Transport } from '@cleocode/contracts';\n\n/** Message handler callback. */\nexport type MessageHandler = (message: ConduitMessage) => void;\n\n/** Poller configuration. */\nexport interface AgentPollerConfig {\n /** Agent ID to poll as. */\n agentId: string;\n /** API key for authentication. */\n apiKey: string;\n /** API base URL. */\n apiBaseUrl: string;\n /** Poll interval in milliseconds. Default: 5000. */\n pollIntervalMs?: number;\n /** Known group conversation IDs to monitor for @mentions. */\n groupConversationIds?: string[];\n /** Max messages to fetch per group conversation poll. Default: 15. */\n groupPollLimit?: number;\n /**\n * Transport instance for polling messages.\n * When provided, poll() delegates to transport.poll() instead of raw HTTP.\n * The transport must already be connected before passing to AgentPoller.\n */\n transport?: Transport;\n}\n\n/** Tracks seen message IDs for dedup. */\nconst DEFAULT_POLL_INTERVAL = 5000;\nconst DEFAULT_GROUP_POLL_LIMIT = 15;\n\n/**\n * AgentPoller service — polls peek endpoint AND group conversations.\n * Deduplicates messages by ID across both sources.\n */\nexport class AgentPoller {\n private config: AgentPollerConfig;\n private handler: MessageHandler | null = null;\n private interval: ReturnType<typeof setInterval> | null = null;\n private seenMessageIds = new Set<string>();\n private running = false;\n\n constructor(config: AgentPollerConfig) {\n this.config = config;\n }\n\n /** Register a message handler. */\n onMessage(handler: MessageHandler): void {\n this.handler = handler;\n }\n\n /** Start the polling loop. */\n start(): void {\n if (this.running) return;\n this.running = true;\n\n const intervalMs = this.config.pollIntervalMs ?? DEFAULT_POLL_INTERVAL;\n\n // Initial poll immediately\n void this.pollCycle();\n\n this.interval = setInterval(() => {\n void this.pollCycle();\n }, intervalMs);\n }\n\n /** Stop the polling loop. */\n stop(): void {\n this.running = false;\n if (this.interval) {\n clearInterval(this.interval);\n this.interval = null;\n }\n }\n\n /** Get poller status. */\n status(): { running: boolean; seenCount: number } {\n return {\n running: this.running,\n seenCount: this.seenMessageIds.size,\n };\n }\n\n /** Single poll cycle — peek + group conversations. */\n private async pollCycle(): Promise<void> {\n if (!this.handler) return;\n\n const newMessages: ConduitMessage[] = [];\n\n // Track 1: Standard peek endpoint (catches DMs)\n try {\n const peekMessages = await this.peekMessages();\n for (const msg of peekMessages) {\n if (!this.seenMessageIds.has(msg.id)) {\n this.seenMessageIds.add(msg.id);\n newMessages.push(msg);\n }\n }\n } catch {\n // Best-effort — don't crash the loop\n }\n\n // Track 2: Group conversation polling (catches @mentions in group messages)\n const groupIds = this.config.groupConversationIds ?? [];\n for (const convId of groupIds) {\n try {\n const groupMessages = await this.pollGroupConversation(convId);\n for (const msg of groupMessages) {\n if (!this.seenMessageIds.has(msg.id)) {\n this.seenMessageIds.add(msg.id);\n newMessages.push(msg);\n }\n }\n } catch {\n // Best-effort per conversation\n }\n }\n\n // Deliver new messages to handler\n for (const msg of newMessages) {\n this.handler(msg);\n }\n\n // Prevent unbounded growth of seen set (keep last 5000)\n if (this.seenMessageIds.size > 5000) {\n const entries = [...this.seenMessageIds];\n this.seenMessageIds = new Set(entries.slice(-3000));\n }\n }\n\n /** Peek for messages mentioning this agent. Delegates to transport when available. */\n private async peekMessages(): Promise<ConduitMessage[]> {\n if (this.config.transport) {\n return this.config.transport.poll({ limit: 50 });\n }\n\n // Fallback: raw HTTP when no transport injected\n const params = new URLSearchParams();\n params.set('mentioned', this.config.agentId);\n params.set('limit', '50');\n\n const url = `${this.config.apiBaseUrl}/messages/peek?${params}`;\n const response = await fetch(url, {\n method: 'GET',\n headers: this.headers(),\n });\n\n if (!response.ok) return [];\n\n const data = (await response.json()) as {\n data?: {\n messages?: Array<{\n id: string;\n fromAgentId?: string;\n content?: string;\n conversationId?: string;\n createdAt?: string;\n }>;\n };\n };\n\n return (data.data?.messages ?? []).map((m) => ({\n id: m.id,\n from: m.fromAgentId ?? 'unknown',\n content: m.content ?? '',\n threadId: m.conversationId,\n timestamp: m.createdAt ?? new Date().toISOString(),\n }));\n }\n\n /**\n * Poll a group conversation for recent messages that @mention this agent.\n * This is the fix for the group @mention blind spot.\n */\n private async pollGroupConversation(conversationId: string): Promise<ConduitMessage[]> {\n const limit = this.config.groupPollLimit ?? DEFAULT_GROUP_POLL_LIMIT;\n const url = `${this.config.apiBaseUrl}/conversations/${conversationId}/messages?sort=desc&limit=${limit}`;\n\n const response = await fetch(url, {\n method: 'GET',\n headers: this.headers(),\n });\n\n if (!response.ok) return [];\n\n const data = (await response.json()) as {\n data?: {\n messages?: Array<{\n id: string;\n fromAgentId?: string;\n content?: string;\n conversationId?: string;\n createdAt?: string;\n }>;\n };\n };\n\n const mentionPattern = new RegExp(`@${this.config.agentId}\\\\b|@all\\\\b`, 'i');\n\n return (data.data?.messages ?? [])\n .filter((m) => {\n // Only deliver messages that mention us or @all\n const content = m.content ?? '';\n return mentionPattern.test(content) && m.fromAgentId !== this.config.agentId;\n })\n .map((m) => ({\n id: m.id,\n from: m.fromAgentId ?? 'unknown',\n content: m.content ?? '',\n threadId: m.conversationId ?? conversationId,\n timestamp: m.createdAt ?? new Date().toISOString(),\n }));\n }\n\n /** Build auth headers. */\n private headers(): Record<string, string> {\n return {\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${this.config.apiKey}`,\n 'X-Agent-Id': this.config.agentId,\n };\n }\n}\n","/**\n * HeartbeatService — Periodic online status heartbeat.\n *\n * Sends a heartbeat to the SignalDock API at a configurable interval\n * to maintain the agent's online status. If the heartbeat fails,\n * it retries silently — the agent continues operating regardless.\n *\n * @task T218\n */\n\n/** Heartbeat service configuration. */\nexport interface HeartbeatConfig {\n /** Agent ID to send heartbeats for. */\n agentId: string;\n /** API key for authentication. */\n apiKey: string;\n /** API base URL. */\n apiBaseUrl: string;\n /** Heartbeat interval in milliseconds. Default: 30000 (30s). */\n intervalMs?: number;\n}\n\n/** Default heartbeat interval: 30 seconds. */\nconst DEFAULT_INTERVAL_MS = 30_000;\n\n/** HeartbeatService sends periodic online status to the cloud API. */\nexport class HeartbeatService {\n private config: HeartbeatConfig;\n private timer: ReturnType<typeof setInterval> | null = null;\n private running = false;\n private consecutiveFailures = 0;\n\n constructor(config: HeartbeatConfig) {\n this.config = config;\n }\n\n /** Start sending heartbeats at the configured interval. */\n start(): void {\n if (this.running) return;\n this.running = true;\n this.consecutiveFailures = 0;\n\n const intervalMs = this.config.intervalMs ?? DEFAULT_INTERVAL_MS;\n\n // Send initial heartbeat immediately\n void this.sendHeartbeat();\n\n this.timer = setInterval(() => {\n void this.sendHeartbeat();\n }, intervalMs);\n }\n\n /** Stop sending heartbeats. */\n stop(): void {\n this.running = false;\n if (this.timer) {\n clearInterval(this.timer);\n this.timer = null;\n }\n }\n\n /** Get heartbeat service status. */\n status(): { running: boolean; consecutiveFailures: number } {\n return {\n running: this.running,\n consecutiveFailures: this.consecutiveFailures,\n };\n }\n\n /** Send a single heartbeat to the cloud API. */\n private async sendHeartbeat(): Promise<void> {\n try {\n const response = await fetch(\n `${this.config.apiBaseUrl}/agents/${this.config.agentId}/heartbeat`,\n {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${this.config.apiKey}`,\n 'X-Agent-Id': this.config.agentId,\n },\n body: JSON.stringify({ status: 'online' }),\n signal: AbortSignal.timeout(10_000),\n },\n );\n\n if (response.ok) {\n this.consecutiveFailures = 0;\n } else {\n this.consecutiveFailures++;\n }\n } catch {\n this.consecutiveFailures++;\n }\n }\n}\n","/**\n * KeyRotationService — Automatic API key rotation based on credential age.\n *\n * Monitors the age of the agent's API key and triggers rotation when\n * the key exceeds the configured threshold. Uses the AgentRegistryAPI\n * to perform the actual rotation (which calls the cloud API and\n * re-encrypts the new key locally).\n *\n * @task T218\n */\n\nimport type { AgentRegistryAPI } from '@cleocode/contracts';\n\n/** Key rotation service configuration. */\nexport interface KeyRotationConfig {\n /** Agent ID to monitor. */\n agentId: string;\n /** AgentRegistryAPI instance for credential lookup and rotation. */\n registry: AgentRegistryAPI;\n /** Check interval in milliseconds. Default: 3600000 (1 hour). */\n checkIntervalMs?: number;\n /** Max key age in milliseconds before rotation. Default: 2592000000 (30 days). */\n maxKeyAgeMs?: number;\n}\n\n/** Default check interval: 1 hour. */\nconst DEFAULT_CHECK_INTERVAL_MS = 3_600_000;\n\n/** Default max key age: 30 days. */\nconst DEFAULT_MAX_KEY_AGE_MS = 30 * 24 * 60 * 60 * 1000;\n\n/** KeyRotationService monitors credential age and auto-rotates when threshold is exceeded. */\nexport class KeyRotationService {\n private config: KeyRotationConfig;\n private timer: ReturnType<typeof setInterval> | null = null;\n private running = false;\n private lastRotationAt: string | null = null;\n\n constructor(config: KeyRotationConfig) {\n this.config = config;\n }\n\n /** Start monitoring key age at the configured interval. */\n start(): void {\n if (this.running) return;\n this.running = true;\n\n const intervalMs = this.config.checkIntervalMs ?? DEFAULT_CHECK_INTERVAL_MS;\n\n // Initial check after a short delay (don't rotate immediately on startup)\n setTimeout(() => {\n void this.checkAndRotate();\n }, 5000);\n\n this.timer = setInterval(() => {\n void this.checkAndRotate();\n }, intervalMs);\n }\n\n /** Stop monitoring. */\n stop(): void {\n this.running = false;\n if (this.timer) {\n clearInterval(this.timer);\n this.timer = null;\n }\n }\n\n /** Get rotation service status. */\n status(): { running: boolean; lastRotationAt: string | null } {\n return {\n running: this.running,\n lastRotationAt: this.lastRotationAt,\n };\n }\n\n /** Check credential age and rotate if needed. */\n private async checkAndRotate(): Promise<void> {\n try {\n const credential = await this.config.registry.get(this.config.agentId);\n if (!credential) return;\n\n const maxAge = this.config.maxKeyAgeMs ?? DEFAULT_MAX_KEY_AGE_MS;\n const credentialAge = Date.now() - new Date(credential.updatedAt).getTime();\n\n if (credentialAge > maxAge) {\n await this.config.registry.rotateKey(this.config.agentId);\n this.lastRotationAt = new Date().toISOString();\n }\n } catch {\n // Rotation failure is non-fatal — will retry next interval\n }\n }\n}\n","/**\n * SseConnectionService — Persistent SSE connection manager.\n *\n * Maintains a persistent SSE connection to the SignalDock API for\n * real-time message delivery. Wraps SseTransport with lifecycle\n * management: start, stop, reconnect, and message forwarding.\n *\n * When SSE is available, messages arrive in real-time. When it falls\n * back to HTTP polling (managed by SseTransport internally), the\n * service continues operating transparently.\n *\n * @task T218\n */\n\nimport type { ConduitMessage, Transport } from '@cleocode/contracts';\n\n/** SSE connection service configuration. */\nexport interface SseConnectionConfig {\n /** Agent ID to connect as. */\n agentId: string;\n /** API key for authentication. */\n apiKey: string;\n /** API base URL. */\n apiBaseUrl: string;\n /** SSE endpoint URL. If omitted, uses apiBaseUrl + /sse. */\n sseEndpoint?: string;\n /** Transport instance to use. Injected by createRuntime. */\n transport: Transport;\n}\n\n/** Message handler callback. */\nexport type SseMessageHandler = (message: ConduitMessage) => void;\n\n/** SseConnectionService manages a persistent transport with subscribe() support. */\nexport class SseConnectionService {\n private config: SseConnectionConfig;\n private handler: SseMessageHandler | null = null;\n private unsubscribe: (() => void) | null = null;\n private running = false;\n\n constructor(config: SseConnectionConfig) {\n this.config = config;\n }\n\n /** Register a message handler for incoming messages. */\n onMessage(handler: SseMessageHandler): void {\n this.handler = handler;\n }\n\n /** Start the SSE connection. */\n async start(): Promise<void> {\n if (this.running) return;\n this.running = true;\n\n await this.config.transport.connect({\n agentId: this.config.agentId,\n apiKey: this.config.apiKey,\n apiBaseUrl: this.config.apiBaseUrl,\n sseEndpoint: this.config.sseEndpoint,\n });\n\n // Subscribe to incoming messages if transport supports it\n if (this.handler && this.config.transport.subscribe) {\n this.unsubscribe = this.config.transport.subscribe(this.handler);\n }\n }\n\n /** Stop the connection and clean up. */\n async stop(): Promise<void> {\n this.running = false;\n\n if (this.unsubscribe) {\n this.unsubscribe();\n this.unsubscribe = null;\n }\n\n await this.config.transport.disconnect();\n }\n\n /** Get connection service status. */\n status(): { running: boolean; transportName: string } {\n return {\n running: this.running,\n transportName: this.config.transport.name,\n };\n }\n}\n","/**\n * Sentient daemon subsystem adapter — R4 (T11255).\n *\n * Expresses the full sentient daemon lifecycle (Studio supervision, Tier-1\n * tick cron, Tier-2 proposal cron, skill curator cron, nightly hygiene loop)\n * as a single {@link Subsystem} via `defineSubsystem` from\n * `@cleocode/runtime/daemon`.\n *\n * ### Package placement\n * This file lives in `@cleocode/runtime` (NOT `@cleocode/core`) to respect the\n * dependency direction: `@cleocode/runtime` → `@cleocode/core` (runtime\n * consumes core; core must not import runtime to avoid a circular dep).\n *\n * ### Standalone-daemon path (daemon-entry.ts)\n * The legacy `bootstrapDaemon` in `@cleocode/core/sentient` continues to\n * function as the entry point for the standalone `cleo daemon start --foreground`\n * path. `createSentientSubsystem` is the adapter for hosts that want to embed\n * the sentient daemon inside a `SubsystemRegistry`-driven process.\n *\n * @packageDocumentation\n * @module @cleocode/runtime\n *\n * @epic T11255 R4 — migrate sentient/daemon.ts → daemon subsystem\n * @saga T11243 SG-RUNTIME-UNIFICATION\n */\n\nimport type { FSWatcher } from 'node:fs';\nimport { watch } from 'node:fs';\nimport { join } from 'node:path';\nimport type { Subsystem, SubsystemHealth } from '@cleocode/contracts';\nimport {\n acquireLock,\n type BootstrapDaemonOptions,\n curatorCronExpression,\n patchSentientState,\n readCuratorConfig,\n readSentientState,\n readSuperviseStudioConfig,\n releaseLock,\n SENTIENT_CRON_EXPR,\n SENTIENT_HYGIENE_CRON_EXPR,\n SENTIENT_LOCK_FILE,\n SENTIENT_PROPOSE_CRON_EXPR,\n SENTIENT_STATE_FILE,\n StudioSupervisor,\n safeRunCrossProjectHygiene,\n safeRunProposeTick,\n safeRunTick,\n warmupWorktreeBackend,\n} from '@cleocode/core/sentient';\nimport cron from 'node-cron';\nimport { createSubsystemLogger, defineSubsystem, SubsystemRegistry } from './daemon/index.js';\n\n// Internal logger for the subsystem.\nconst log = createSubsystemLogger('sentient');\n\n/**\n * Runtime context threaded from `start()` into `shutdown()`.\n *\n * Holds every handle the shutdown path needs to tear down cleanly.\n * Kept internal to this module — callers interact via `SubsystemRegistry`.\n */\ninterface SentientContext {\n readonly lockPath: string;\n readonly statePath: string;\n readonly studioSupervisor: StudioSupervisor | null;\n readonly watcher: FSWatcher | null;\n readonly lock: Awaited<ReturnType<typeof acquireLock>>;\n}\n\n/**\n * Declare the sentient daemon as a supervised daemon {@link Subsystem}.\n *\n * The returned subsystem (frozen by `defineSubsystem`) is registered with a\n * `SubsystemRegistry` in a host process. It carries the full lifecycle\n * that the legacy `bootstrapDaemon` ran inline:\n *\n * 1. `start()` — acquires the advisory lock, persists pid/startedAt,\n * optionally starts Studio supervision, watches the state file, and\n * schedules all four cron jobs (Tier-1, Tier-2, curator, hygiene). Runs\n * one boot tick before returning.\n * 2. `healthProbe()` — returns a `SubsystemHealth` row based on the pid\n * recorded in state.json.\n * 3. `shutdown()` — cascades Studio SIGTERM → watcher.close() → state patch\n * → lock release.\n *\n * @param projectRoot - Absolute path to the project (contains `.cleo/`).\n * @param opts - Optional overrides for Studio supervision and config path.\n * @returns A frozen {@link Subsystem} for `SubsystemRegistry.register()`.\n */\nexport function createSentientSubsystem(\n projectRoot: string,\n opts: BootstrapDaemonOptions = {},\n): Subsystem<SentientContext> {\n const statePath = join(projectRoot, SENTIENT_STATE_FILE);\n const lockPath = join(projectRoot, SENTIENT_LOCK_FILE);\n\n return defineSubsystem<SentientContext>({\n name: 'sentient',\n\n async start(): Promise<SentientContext> {\n // 1. Advisory lock.\n const lock = await acquireLock(lockPath);\n if (!lock) {\n log.error({}, 'lock acquisition failed — another daemon is running');\n process.exit(2);\n }\n\n // 2. Persist pid + startedAt.\n await patchSentientState(statePath, {\n pid: process.pid,\n startedAt: new Date().toISOString(),\n });\n\n // 3. Studio supervision.\n let studioSupervisor: StudioSupervisor | null = null;\n let shouldSuperviseStudio: boolean;\n if (typeof opts.superviseStudio === 'boolean') {\n shouldSuperviseStudio = opts.superviseStudio;\n } else {\n const { getCleoHome } = await import('@cleocode/core');\n const defaultConfigPath = join(getCleoHome(), 'config.json');\n const configPath = opts.globalConfigPath ?? defaultConfigPath;\n shouldSuperviseStudio = await readSuperviseStudioConfig(configPath);\n }\n\n if (shouldSuperviseStudio) {\n studioSupervisor = new StudioSupervisor(opts.studioOptions ?? {});\n try {\n studioSupervisor.start();\n log.info({}, 'Studio supervision enabled');\n } catch (err) {\n log.error({ err }, 'Studio supervision start failed (non-fatal; ticks continue)');\n studioSupervisor = null;\n }\n } else {\n log.info({}, 'Studio supervision disabled (daemon.superviseStudio=false)');\n }\n\n // 4. State file watcher.\n let watcher: FSWatcher | null = null;\n try {\n watcher = watch(statePath, { persistent: false }, () => {\n // Next tick will re-read state and honour kill-switch.\n });\n } catch {\n watcher = null;\n }\n\n // 5. Warm up worktree backend.\n await warmupWorktreeBackend();\n\n // 6. Boot tick (Tier-1 immediate run).\n const tickOptions = { projectRoot, statePath };\n await patchSentientState(statePath, { lastCronFiredAt: new Date().toISOString() });\n const outcome = await safeRunTick(tickOptions);\n log.info(\n { kind: outcome.kind, taskId: outcome.taskId ?? null },\n `boot tick: ${outcome.kind} (task=${outcome.taskId ?? 'n/a'}) ${outcome.detail}`,\n );\n\n // 7. Schedule cron jobs.\n\n // Tier-1: every 5 minutes.\n cron.schedule(\n SENTIENT_CRON_EXPR,\n async () => {\n try {\n await patchSentientState(statePath, { lastCronFiredAt: new Date().toISOString() });\n } catch (err) {\n log.warn({ err }, 'heartbeat write failed');\n }\n try {\n const result = await safeRunTick(tickOptions);\n log.info(\n { kind: result.kind, taskId: result.taskId ?? null },\n `tick: ${result.kind} (task=${result.taskId ?? 'n/a'}) ${result.detail}`,\n );\n } catch (err) {\n log.error({ err }, 'tick error (caught at cron boundary)');\n }\n },\n { timezone: 'UTC', noOverlap: true, name: 'cleo-sentient' },\n );\n\n // Tier-2: every 2 hours.\n const proposeOptions = { projectRoot, statePath };\n cron.schedule(\n SENTIENT_PROPOSE_CRON_EXPR,\n async () => {\n try {\n const state = await readSentientState(statePath);\n if (!state.tier2Enabled) return;\n const result = await safeRunProposeTick(proposeOptions);\n log.info(\n { written: result.written, count: result.count },\n `propose: ${result.kind} ${result.detail}`,\n );\n } catch (err) {\n log.error({ err }, 'propose error (caught at cron boundary)');\n }\n },\n { timezone: 'UTC', noOverlap: true, name: 'cleo-sentient-propose' },\n );\n\n // Skill curator: opt-in, default off.\n {\n const { getCleoHome } = await import('@cleocode/core');\n const curatorConfigPath = opts.globalConfigPath ?? join(getCleoHome(), 'config.json');\n const curatorCfg = await readCuratorConfig(curatorConfigPath);\n if (curatorCfg.enabled) {\n const expr = curatorCronExpression(curatorCfg.runEveryHours);\n log.info({ interval: curatorCfg.runEveryHours, expr }, `curator enabled`);\n cron.schedule(\n expr,\n async () => {\n try {\n const curatorState = await readSentientState(statePath);\n if (curatorState.killSwitch) return;\n const { runCuratorTick } = await import('@cleocode/core/sentient');\n const result = await runCuratorTick({\n staleAfterDays: curatorCfg.staleAfterDays,\n archiveAfterDays: curatorCfg.archiveAfterDays,\n });\n log.info(\n { checked: result.summary.checked, stale: result.summary.markedStale },\n 'curator tick complete',\n );\n } catch (err) {\n log.error({ err }, 'curator error (caught at cron boundary)');\n }\n },\n { timezone: 'UTC', noOverlap: true, name: 'cleo-sentient-curator' },\n );\n } else {\n log.info({}, 'curator disabled (daemon.curator.enabled=false)');\n }\n }\n\n // Nightly hygiene: 02:00 UTC (or CLEO_HYGIENE_CRON override).\n cron.schedule(\n SENTIENT_HYGIENE_CRON_EXPR,\n async () => {\n try {\n const hygieneState = await readSentientState(statePath);\n if (hygieneState.killSwitch) return;\n log.info({}, 'nightly cross-project hygiene loop starting');\n const digest = await safeRunCrossProjectHygiene();\n await patchSentientState(statePath, {\n hygieneLastRunAt: digest.completedAt,\n hygieneSummary: digest.summary,\n hygieneStats: {\n projectsChecked: digest.nexusIntegrity.total,\n projectsHealthy: digest.nexusIntegrity.healthy,\n tempGcCandidates: digest.tempGc.candidates.length,\n duplicateEpicGroups: digest.duplicateEpics.groups.length,\n worktreesPruned: digest.worktreePrune.totalPruned,\n },\n });\n log.info({}, `hygiene complete: ${digest.summary}`);\n } catch (err) {\n log.error({ err }, 'hygiene error (caught at cron boundary)');\n }\n },\n { noOverlap: true, name: 'cleo-sentient-hygiene' },\n );\n\n return { lockPath, statePath, studioSupervisor, watcher, lock };\n },\n\n async healthProbe(): Promise<SubsystemHealth> {\n try {\n const state = await readSentientState(statePath);\n const running = state.pid\n ? (() => {\n try {\n process.kill(state.pid, 0);\n return true;\n } catch {\n return false;\n }\n })()\n : false;\n return {\n child_id: 'sentient',\n pid: running ? (state.pid ?? 0) : 0,\n state: running ? 'running' : 'stopped',\n restart_count: 0,\n detail: `killSwitch=${state.killSwitch} lastTickAt=${state.lastTickAt ?? 'never'}`,\n };\n } catch {\n return { child_id: 'sentient', pid: 0, state: 'stopped', restart_count: 0 };\n }\n },\n\n async shutdown(context: SentientContext): Promise<void> {\n // 1. Stop Studio.\n if (context.studioSupervisor !== null) {\n log.info({}, 'forwarding shutdown to Studio (SIGTERM)');\n try {\n await context.studioSupervisor.stop();\n } catch {\n // ignore\n }\n }\n // 2. Close state file watcher.\n try {\n context.watcher?.close();\n } catch {\n // ignore\n }\n // 3. Patch state — clear pid.\n try {\n await patchSentientState(context.statePath, {\n pid: null,\n killSwitchReason: 'subsystem shutdown',\n });\n } catch {\n // ignore\n }\n // 4. Release advisory lock.\n try {\n if (context.lock) await releaseLock(context.lock);\n } catch {\n // ignore\n }\n log.info({}, 'sentient subsystem stopped');\n },\n });\n}\n\n/**\n * Bootstrap the sentient daemon via a SubsystemRegistry.\n *\n * Alternative to `bootstrapDaemon` for host processes that want to drive the\n * sentient daemon through the uniform `start → healthProbe → shutdown`\n * lifecycle (e.g., a unified CLEO daemon host that also runs the GC subsystem\n * and the gateway subsystem). Installs SIGTERM/SIGINT handlers that call\n * `shutdownAll()` and keeps the process alive via node-cron.\n *\n * @param projectRoot - Absolute path to the project (contains `.cleo/`).\n * @param opts - Optional Studio supervision and config overrides.\n */\nexport async function bootstrapSentientRegistry(\n projectRoot: string,\n opts: BootstrapDaemonOptions = {},\n): Promise<void> {\n const registry = new SubsystemRegistry({\n onStart: (name: string) => log.info({ name }, `subsystem started: ${name}`),\n onShutdown: (name: string) => log.info({ name }, `subsystem shutdown: ${name}`),\n onError: (name: string, err: Error, phase: string) =>\n log.error({ name, phase, err }, `subsystem lifecycle error [${phase}]`),\n });\n\n registry.register(createSentientSubsystem(projectRoot, opts));\n await registry.startAll();\n\n const shutdown = async (reason: string): Promise<void> => {\n log.info({ reason }, 'shutdown signal received');\n await registry.shutdownAll();\n process.exit(0);\n };\n\n process.on('SIGTERM', () => {\n void shutdown('SIGTERM');\n });\n process.on('SIGINT', () => {\n void shutdown('SIGINT');\n });\n // Process stays alive via node-cron scheduler.\n}\n"],"mappings":"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","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cleocode/runtime",
3
- "version": "2026.5.131",
3
+ "version": "2026.5.132",
4
4
  "description": "Long-running process layer for CLEO — agent polling, SSE connections, heartbeat, key rotation",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -32,8 +32,9 @@
32
32
  }
33
33
  },
34
34
  "dependencies": {
35
- "@cleocode/contracts": "2026.5.131",
36
- "@cleocode/core": "2026.5.131"
35
+ "node-cron": "^4.2.1",
36
+ "@cleocode/contracts": "2026.5.132",
37
+ "@cleocode/core": "2026.5.132"
37
38
  },
38
39
  "devDependencies": {
39
40
  "tsup": "^8.0.0",