@kmmao/happy-agent 0.4.1 → 0.5.0

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.cts CHANGED
@@ -399,17 +399,26 @@ interface SchedulerStatus {
399
399
  errorMessage?: string;
400
400
  }>;
401
401
  }
402
+ type AuditCallback = (event: {
403
+ kind: "job_enqueued" | "job_dispatched" | "job_completed" | "job_failed" | "job_retried";
404
+ jobId: string;
405
+ dedupeKey: string;
406
+ message?: string;
407
+ errorMessage?: string;
408
+ }) => void;
402
409
  interface SchedulerOptions {
403
410
  maxConcurrentJobs?: number;
404
411
  retryDelayMs?: number;
405
412
  maxAttempts?: number;
406
413
  maxRecentCompletions?: number;
414
+ onAudit?: AuditCallback;
407
415
  }
408
416
  declare class AutomationScheduler {
409
417
  private readonly maxConcurrentJobs;
410
418
  private readonly retryDelayMs;
411
419
  private readonly defaultMaxAttempts;
412
420
  private readonly maxRecentCompletions;
421
+ private readonly onAudit;
413
422
  /** Active jobs indexed by id. */
414
423
  private readonly jobs;
415
424
  /** dedupeKey → jobId for fast dedup lookups. */
@@ -429,6 +438,179 @@ declare class AutomationScheduler {
429
438
  private finalize;
430
439
  }
431
440
 
441
+ /**
442
+ * GuardianSessionRegistry — session reuse for recurring automation.
443
+ *
444
+ * When a loop or supervisor trigger spawns a session, the registry
445
+ * remembers which Happy session ID was used. On subsequent runs,
446
+ * it resolves the existing session so the spawner can --resume it
447
+ * instead of creating a fresh session every iteration.
448
+ *
449
+ * Key hierarchy: loop:{loopId} > project:{projectId}
450
+ *
451
+ * In-memory only — no file persistence (daemon restart = fresh).
452
+ */
453
+ interface GuardianEntry {
454
+ readonly key: string;
455
+ sessionId: string;
456
+ loopId?: string;
457
+ projectId?: string;
458
+ updatedAt: number;
459
+ }
460
+ interface ResolveInput {
461
+ loopId?: string;
462
+ projectId?: string;
463
+ }
464
+ declare class GuardianSessionRegistry {
465
+ private readonly entries;
466
+ /**
467
+ * Find an existing session to reuse.
468
+ * Tries loop key first, then project key.
469
+ */
470
+ resolve(input: ResolveInput): string | null;
471
+ /**
472
+ * Remember a session for future reuse.
473
+ * Stores under both loop and project keys if available.
474
+ */
475
+ remember(sessionId: string, input: ResolveInput): void;
476
+ /**
477
+ * Forget a specific session (e.g., after it exits).
478
+ */
479
+ forgetSession(sessionId: string): number;
480
+ /**
481
+ * Forget all entries for a loop.
482
+ */
483
+ forgetLoop(loopId: string): boolean;
484
+ /**
485
+ * Get all entries (for observability).
486
+ */
487
+ getSnapshot(): GuardianEntry[];
488
+ /**
489
+ * Number of tracked guardian entries.
490
+ */
491
+ get size(): number;
492
+ }
493
+
494
+ /**
495
+ * AgentLoopCoordinator — lightweight loop scheduler for agent daemon.
496
+ *
497
+ * A "loop" is a recurring autonomous task: run a prompt in a directory
498
+ * at a fixed interval. The coordinator polls every second, enqueues
499
+ * due loops into the AutomationScheduler, and tracks iteration state.
500
+ *
501
+ * Simplified from CLI's full coordinator — no cron, quiet hours,
502
+ * downstream chains, notifications, or memory snapshots.
503
+ */
504
+
505
+ type LoopState = "idle" | "active" | "paused" | "blocked";
506
+ interface AgentLoop {
507
+ readonly id: string;
508
+ readonly name: string;
509
+ readonly prompt: string;
510
+ readonly directory: string;
511
+ readonly intervalMs: number;
512
+ readonly createdAt: number;
513
+ state: LoopState;
514
+ iteration: number;
515
+ nextRunAt: number;
516
+ lastStartedAt?: number;
517
+ lastCompletedAt?: number;
518
+ activeJobId?: string;
519
+ consecutiveFailures: number;
520
+ maxConsecutiveFailures: number;
521
+ maxIterations: number;
522
+ errorMessage?: string;
523
+ }
524
+ interface CreateLoopInput {
525
+ name: string;
526
+ prompt: string;
527
+ directory: string;
528
+ intervalMs: number;
529
+ maxConsecutiveFailures?: number;
530
+ maxIterations?: number;
531
+ }
532
+ interface LoopSummary {
533
+ id: string;
534
+ name: string;
535
+ state: LoopState;
536
+ iteration: number;
537
+ intervalMs: number;
538
+ nextRunAt: number;
539
+ lastCompletedAt?: number;
540
+ }
541
+ declare class AgentLoopCoordinator {
542
+ private readonly loops;
543
+ private readonly scheduler;
544
+ private readonly serverUrl;
545
+ private readonly authToken;
546
+ private readonly guardian;
547
+ private tickTimer;
548
+ constructor(scheduler: AutomationScheduler, serverUrl: string, authToken: string, guardian?: GuardianSessionRegistry);
549
+ start(): void;
550
+ shutdown(): void;
551
+ createLoop(input: CreateLoopInput): AgentLoop;
552
+ getLoop(id: string): AgentLoop | undefined;
553
+ listLoops(): LoopSummary[];
554
+ pauseLoop(id: string): boolean;
555
+ resumeLoop(id: string): boolean;
556
+ deleteLoop(id: string): boolean;
557
+ onJobTerminal(loopId: string, status: "completed" | "failed", errorMessage?: string): void;
558
+ private tick;
559
+ private enqueueLoop;
560
+ }
561
+
562
+ /**
563
+ * AutomationAuditStore — in-memory ring buffer for automation events.
564
+ *
565
+ * Records job lifecycle events (enqueued, started, completed, failed)
566
+ * for observability. Capped at maxEntries to prevent unbounded growth.
567
+ * Queryable via RPC for dashboard / debugging.
568
+ */
569
+ type AuditEventKind = "job_enqueued" | "job_dispatched" | "job_completed" | "job_failed" | "job_retried" | "loop_started" | "loop_blocked" | "loop_paused";
570
+ interface AuditEvent {
571
+ readonly id: number;
572
+ readonly kind: AuditEventKind;
573
+ readonly timestamp: number;
574
+ readonly jobId?: string;
575
+ readonly dedupeKey?: string;
576
+ readonly loopId?: string;
577
+ readonly loopName?: string;
578
+ readonly status?: string;
579
+ readonly message?: string;
580
+ readonly errorMessage?: string;
581
+ }
582
+ interface AuditQuery {
583
+ kind?: AuditEventKind;
584
+ loopId?: string;
585
+ limit?: number;
586
+ since?: number;
587
+ }
588
+ interface AuditStorOptions {
589
+ maxEntries?: number;
590
+ }
591
+ declare class AutomationAuditStore {
592
+ private readonly maxEntries;
593
+ private readonly events;
594
+ private nextId;
595
+ constructor(options?: AuditStorOptions);
596
+ /**
597
+ * Record an audit event.
598
+ */
599
+ record(event: Omit<AuditEvent, "id" | "timestamp">): AuditEvent;
600
+ /**
601
+ * Query events with optional filters.
602
+ */
603
+ query(filter?: AuditQuery): AuditEvent[];
604
+ /**
605
+ * Summary counts by kind.
606
+ */
607
+ summarize(): Record<AuditEventKind, number>;
608
+ /**
609
+ * Total number of stored events.
610
+ */
611
+ get size(): number;
612
+ }
613
+
432
614
  /**
433
615
  * Machine WebSocket client — trimmed from CLI's ApiMachineClient.
434
616
  *
@@ -516,8 +698,12 @@ declare class MachineClient {
516
698
  private automationServerUrl;
517
699
  private automationAuthToken;
518
700
  private scheduler;
701
+ private loopCoordinator;
702
+ private auditStore;
519
703
  constructor(opts: MachineClientOptions);
520
704
  private registerMachineHandlers;
705
+ private registerLoopHandlers;
706
+ private registerAuditHandlers;
521
707
  connect(): void;
522
708
  updateMachineMetadata(handler: (metadata: MachineMetadata | null) => MachineMetadata): Promise<void>;
523
709
  updateDaemonState(handler: (state: DaemonState | null) => DaemonState): Promise<void>;
@@ -579,7 +765,7 @@ declare class MachineClient {
579
765
  * Enable automation handling — agent will process webhook, supervisor,
580
766
  * and task triggers from the server by spawning Happy CLI sessions.
581
767
  */
582
- enableAutomation(serverUrl: string, authToken: string, scheduler: AutomationScheduler): void;
768
+ enableAutomation(serverUrl: string, authToken: string, scheduler: AutomationScheduler, loopCoordinator?: AgentLoopCoordinator, auditStore?: AutomationAuditStore): void;
583
769
  /** Internal dispatch for ephemeral events that need automation handling. */
584
770
  private handleAutomationEvent;
585
771
  /** Seed initial Tailscale info detected before connect. */
package/dist/index.d.mts CHANGED
@@ -399,17 +399,26 @@ interface SchedulerStatus {
399
399
  errorMessage?: string;
400
400
  }>;
401
401
  }
402
+ type AuditCallback = (event: {
403
+ kind: "job_enqueued" | "job_dispatched" | "job_completed" | "job_failed" | "job_retried";
404
+ jobId: string;
405
+ dedupeKey: string;
406
+ message?: string;
407
+ errorMessage?: string;
408
+ }) => void;
402
409
  interface SchedulerOptions {
403
410
  maxConcurrentJobs?: number;
404
411
  retryDelayMs?: number;
405
412
  maxAttempts?: number;
406
413
  maxRecentCompletions?: number;
414
+ onAudit?: AuditCallback;
407
415
  }
408
416
  declare class AutomationScheduler {
409
417
  private readonly maxConcurrentJobs;
410
418
  private readonly retryDelayMs;
411
419
  private readonly defaultMaxAttempts;
412
420
  private readonly maxRecentCompletions;
421
+ private readonly onAudit;
413
422
  /** Active jobs indexed by id. */
414
423
  private readonly jobs;
415
424
  /** dedupeKey → jobId for fast dedup lookups. */
@@ -429,6 +438,179 @@ declare class AutomationScheduler {
429
438
  private finalize;
430
439
  }
431
440
 
441
+ /**
442
+ * GuardianSessionRegistry — session reuse for recurring automation.
443
+ *
444
+ * When a loop or supervisor trigger spawns a session, the registry
445
+ * remembers which Happy session ID was used. On subsequent runs,
446
+ * it resolves the existing session so the spawner can --resume it
447
+ * instead of creating a fresh session every iteration.
448
+ *
449
+ * Key hierarchy: loop:{loopId} > project:{projectId}
450
+ *
451
+ * In-memory only — no file persistence (daemon restart = fresh).
452
+ */
453
+ interface GuardianEntry {
454
+ readonly key: string;
455
+ sessionId: string;
456
+ loopId?: string;
457
+ projectId?: string;
458
+ updatedAt: number;
459
+ }
460
+ interface ResolveInput {
461
+ loopId?: string;
462
+ projectId?: string;
463
+ }
464
+ declare class GuardianSessionRegistry {
465
+ private readonly entries;
466
+ /**
467
+ * Find an existing session to reuse.
468
+ * Tries loop key first, then project key.
469
+ */
470
+ resolve(input: ResolveInput): string | null;
471
+ /**
472
+ * Remember a session for future reuse.
473
+ * Stores under both loop and project keys if available.
474
+ */
475
+ remember(sessionId: string, input: ResolveInput): void;
476
+ /**
477
+ * Forget a specific session (e.g., after it exits).
478
+ */
479
+ forgetSession(sessionId: string): number;
480
+ /**
481
+ * Forget all entries for a loop.
482
+ */
483
+ forgetLoop(loopId: string): boolean;
484
+ /**
485
+ * Get all entries (for observability).
486
+ */
487
+ getSnapshot(): GuardianEntry[];
488
+ /**
489
+ * Number of tracked guardian entries.
490
+ */
491
+ get size(): number;
492
+ }
493
+
494
+ /**
495
+ * AgentLoopCoordinator — lightweight loop scheduler for agent daemon.
496
+ *
497
+ * A "loop" is a recurring autonomous task: run a prompt in a directory
498
+ * at a fixed interval. The coordinator polls every second, enqueues
499
+ * due loops into the AutomationScheduler, and tracks iteration state.
500
+ *
501
+ * Simplified from CLI's full coordinator — no cron, quiet hours,
502
+ * downstream chains, notifications, or memory snapshots.
503
+ */
504
+
505
+ type LoopState = "idle" | "active" | "paused" | "blocked";
506
+ interface AgentLoop {
507
+ readonly id: string;
508
+ readonly name: string;
509
+ readonly prompt: string;
510
+ readonly directory: string;
511
+ readonly intervalMs: number;
512
+ readonly createdAt: number;
513
+ state: LoopState;
514
+ iteration: number;
515
+ nextRunAt: number;
516
+ lastStartedAt?: number;
517
+ lastCompletedAt?: number;
518
+ activeJobId?: string;
519
+ consecutiveFailures: number;
520
+ maxConsecutiveFailures: number;
521
+ maxIterations: number;
522
+ errorMessage?: string;
523
+ }
524
+ interface CreateLoopInput {
525
+ name: string;
526
+ prompt: string;
527
+ directory: string;
528
+ intervalMs: number;
529
+ maxConsecutiveFailures?: number;
530
+ maxIterations?: number;
531
+ }
532
+ interface LoopSummary {
533
+ id: string;
534
+ name: string;
535
+ state: LoopState;
536
+ iteration: number;
537
+ intervalMs: number;
538
+ nextRunAt: number;
539
+ lastCompletedAt?: number;
540
+ }
541
+ declare class AgentLoopCoordinator {
542
+ private readonly loops;
543
+ private readonly scheduler;
544
+ private readonly serverUrl;
545
+ private readonly authToken;
546
+ private readonly guardian;
547
+ private tickTimer;
548
+ constructor(scheduler: AutomationScheduler, serverUrl: string, authToken: string, guardian?: GuardianSessionRegistry);
549
+ start(): void;
550
+ shutdown(): void;
551
+ createLoop(input: CreateLoopInput): AgentLoop;
552
+ getLoop(id: string): AgentLoop | undefined;
553
+ listLoops(): LoopSummary[];
554
+ pauseLoop(id: string): boolean;
555
+ resumeLoop(id: string): boolean;
556
+ deleteLoop(id: string): boolean;
557
+ onJobTerminal(loopId: string, status: "completed" | "failed", errorMessage?: string): void;
558
+ private tick;
559
+ private enqueueLoop;
560
+ }
561
+
562
+ /**
563
+ * AutomationAuditStore — in-memory ring buffer for automation events.
564
+ *
565
+ * Records job lifecycle events (enqueued, started, completed, failed)
566
+ * for observability. Capped at maxEntries to prevent unbounded growth.
567
+ * Queryable via RPC for dashboard / debugging.
568
+ */
569
+ type AuditEventKind = "job_enqueued" | "job_dispatched" | "job_completed" | "job_failed" | "job_retried" | "loop_started" | "loop_blocked" | "loop_paused";
570
+ interface AuditEvent {
571
+ readonly id: number;
572
+ readonly kind: AuditEventKind;
573
+ readonly timestamp: number;
574
+ readonly jobId?: string;
575
+ readonly dedupeKey?: string;
576
+ readonly loopId?: string;
577
+ readonly loopName?: string;
578
+ readonly status?: string;
579
+ readonly message?: string;
580
+ readonly errorMessage?: string;
581
+ }
582
+ interface AuditQuery {
583
+ kind?: AuditEventKind;
584
+ loopId?: string;
585
+ limit?: number;
586
+ since?: number;
587
+ }
588
+ interface AuditStorOptions {
589
+ maxEntries?: number;
590
+ }
591
+ declare class AutomationAuditStore {
592
+ private readonly maxEntries;
593
+ private readonly events;
594
+ private nextId;
595
+ constructor(options?: AuditStorOptions);
596
+ /**
597
+ * Record an audit event.
598
+ */
599
+ record(event: Omit<AuditEvent, "id" | "timestamp">): AuditEvent;
600
+ /**
601
+ * Query events with optional filters.
602
+ */
603
+ query(filter?: AuditQuery): AuditEvent[];
604
+ /**
605
+ * Summary counts by kind.
606
+ */
607
+ summarize(): Record<AuditEventKind, number>;
608
+ /**
609
+ * Total number of stored events.
610
+ */
611
+ get size(): number;
612
+ }
613
+
432
614
  /**
433
615
  * Machine WebSocket client — trimmed from CLI's ApiMachineClient.
434
616
  *
@@ -516,8 +698,12 @@ declare class MachineClient {
516
698
  private automationServerUrl;
517
699
  private automationAuthToken;
518
700
  private scheduler;
701
+ private loopCoordinator;
702
+ private auditStore;
519
703
  constructor(opts: MachineClientOptions);
520
704
  private registerMachineHandlers;
705
+ private registerLoopHandlers;
706
+ private registerAuditHandlers;
521
707
  connect(): void;
522
708
  updateMachineMetadata(handler: (metadata: MachineMetadata | null) => MachineMetadata): Promise<void>;
523
709
  updateDaemonState(handler: (state: DaemonState | null) => DaemonState): Promise<void>;
@@ -579,7 +765,7 @@ declare class MachineClient {
579
765
  * Enable automation handling — agent will process webhook, supervisor,
580
766
  * and task triggers from the server by spawning Happy CLI sessions.
581
767
  */
582
- enableAutomation(serverUrl: string, authToken: string, scheduler: AutomationScheduler): void;
768
+ enableAutomation(serverUrl: string, authToken: string, scheduler: AutomationScheduler, loopCoordinator?: AgentLoopCoordinator, auditStore?: AutomationAuditStore): void;
583
769
  /** Internal dispatch for ephemeral events that need automation handling. */
584
770
  private handleAutomationEvent;
585
771
  /** Seed initial Tailscale info detected before connect. */