@kmmao/happy-agent 0.4.1 → 0.5.1
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.cjs +588 -7
- package/dist/index.d.cts +187 -1
- package/dist/index.d.mts +187 -1
- package/dist/index.mjs +590 -9
- package/package.json +2 -2
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. */
|