ferrycat 0.3.3 → 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/daemon.d.ts CHANGED
@@ -3,6 +3,7 @@ import { type FerryCatConfigFile, type HandlerDefinition } from './schemas.js';
3
3
  export interface DaemonOptions {
4
4
  configPath?: string;
5
5
  dbPath?: string;
6
+ journalPath?: string;
6
7
  targetAgent?: string;
7
8
  lockId?: string;
8
9
  leaseTtlSec?: number;
@@ -11,15 +12,86 @@ export interface DaemonOptions {
11
12
  once?: boolean;
12
13
  notify?: boolean;
13
14
  }
15
+ export type DaemonJournalEvent = 'TASK_DISPATCHING' | 'CHILD_SPAWNED' | 'TASK_SETTLED' | 'CRASH_RECOVERED';
16
+ export type JournalRecoveryAction = 'RELEASED_TO_QUEUE' | 'MARKED_FAILED_MAX_RELEASES' | 'MARKED_FAILED_MUTATION_UNSAFE' | 'MARKED_FAILED_MISSING_CLAIM_TOKEN' | 'QUARANTINED_ORPHAN_ALIVE' | 'ALREADY_SETTLED';
17
+ export interface DaemonJournalEntry {
18
+ seq: number;
19
+ timestamp: number;
20
+ event: DaemonJournalEvent;
21
+ daemon_owner_token: string;
22
+ daemon_pid: number;
23
+ /** R3: wall-clock start time of the writing daemon process, used to disambiguate PID reuse. */
24
+ daemon_started_at?: number;
25
+ lock_id: string;
26
+ contract_id: string;
27
+ target_agent: string;
28
+ task_type?: string;
29
+ claim_token?: string;
30
+ exec_file?: string;
31
+ child_pid?: number | null;
32
+ child_started_at?: number;
33
+ final_status?: string;
34
+ previous_owner_token?: string;
35
+ recovery_action?: JournalRecoveryAction;
36
+ }
37
+ export interface JournalRecoveryItem {
38
+ contract_id: string;
39
+ target_agent: string;
40
+ claim_token?: string;
41
+ previous_owner_token: string;
42
+ previous_pid: number;
43
+ child_pid?: number | null;
44
+ action: JournalRecoveryAction;
45
+ }
46
+ /**
47
+ * Non-destructive OS liveness probe via `process.kill(pid, 0)`.
48
+ * Never sends a termination signal (respecting CK-05 PID-reuse safety), only checks whether
49
+ * the OS kernel still reports an active process for `pid`.
50
+ */
51
+ export declare function isProcessAlive(pid?: number | null): boolean;
14
52
  export declare function loadFerryCatConfig(customConfigPath?: string): {
15
53
  config: FerryCatConfigFile;
16
54
  resolvedPath: string | null;
17
55
  };
18
- export declare function notifyOperator(title: string, message: string): void;
56
+ export type NotifyLevel = 'info' | 'warn' | 'error';
57
+ export interface NotifyOptions {
58
+ level?: NotifyLevel;
59
+ /** Stable machine-readable event name forwarded to the webhook payload. */
60
+ event?: string;
61
+ /** Explicit webhook endpoint; falls back to FERRYCAT_WEBHOOK_URL when omitted. */
62
+ webhookUrl?: string;
63
+ /** Set false to suppress the native OS toast (e.g. in CI or headless runs). */
64
+ native?: boolean;
65
+ }
66
+ export interface WebhookPayload {
67
+ event: string;
68
+ title: string;
69
+ message: string;
70
+ timestamp: string;
71
+ level: NotifyLevel;
72
+ }
73
+ export declare function setNotifyWebhook(url: string | undefined): void;
74
+ export declare function getNotifyWebhook(): string | undefined;
75
+ /**
76
+ * Resolve the platform-native notification command (FC-403).
77
+ * Windows uses a WinRT Toast driven by the PowerShell host's own AppId so no module install is
78
+ * required; macOS uses `display notification`; Linux uses libnotify's `notify-send`.
79
+ */
80
+ export declare function resolveNativeCommand(title: string, message: string, platform?: NodeJS.Platform): {
81
+ cmd: string;
82
+ args: string[];
83
+ };
84
+ /**
85
+ * Deliver one webhook payload. Exported so tests can await the real HTTP round-trip; production
86
+ * call sites use the fire-and-forget wrapper below.
87
+ */
88
+ export declare function dispatchWebhook(url: string, payload: WebhookPayload, timeoutMs?: number): Promise<boolean>;
89
+ export declare function notifyOperator(title: string, message: string, opts?: NotifyOptions): void;
19
90
  export declare class FerryCatDaemon {
20
91
  readonly engine: FerryCatEngine;
21
92
  readonly config: FerryCatConfigFile;
22
93
  readonly configPath: string | null;
94
+ readonly journalPath: string;
23
95
  readonly lockId: string;
24
96
  readonly leaseTtlSec: number;
25
97
  readonly heartbeatIntervalMs: number;
@@ -27,8 +99,16 @@ export declare class FerryCatDaemon {
27
99
  readonly ownerToken: string;
28
100
  readonly targetAgentFilter?: string;
29
101
  readonly notifyEnabled: boolean;
102
+ readonly webhookUrl?: string;
103
+ /** R3: wall-clock start of this daemon instance, journaled so a successor can disambiguate PID reuse. */
104
+ readonly daemonStartedAt: number;
105
+ journalDegraded: boolean;
106
+ lastJournalError: string | null;
30
107
  private isRunning;
31
108
  private isLeader;
109
+ private journalSeq;
110
+ private settledSinceLastCompact;
111
+ private journalDegradedReported;
32
112
  private heartbeatTimer;
33
113
  private pollTimer;
34
114
  private signalCleanupRegistered;
@@ -36,6 +116,62 @@ export declare class FerryCatDaemon {
36
116
  constructor(options?: DaemonOptions);
37
117
  registerSignalHandlers(): void;
38
118
  acquireLease(): boolean;
119
+ /**
120
+ * Synchronously appends a write-ahead record to `daemon-journal.jsonl` (Phase 3 Step 2).
121
+ * Guaranteed to flush before child spawn and after task completion so sudden SIGKILL / power loss
122
+ * leaves an auditable breadcrumb for the next daemon leader to self-heal.
123
+ */
124
+ appendJournalEntry(partial: Omit<DaemonJournalEntry, 'seq' | 'timestamp' | 'daemon_owner_token' | 'daemon_pid' | 'daemon_started_at' | 'lock_id'>): DaemonJournalEntry;
125
+ /**
126
+ * Reads all valid JSONL records from this daemon's journal file, tolerating partial/corrupt tail lines.
127
+ *
128
+ * R6 (upgrade compatibility): before per-lock_id journal files existed, every daemon wrote into a
129
+ * single shared `daemon-journal.jsonl`. When this instance is a non-`global` lock whose own file does
130
+ * not exist yet, fall back to that legacy file and keep only the records belonging to this lock_id,
131
+ * so in-flight contracts written by the previous version are still recovered instead of silently
132
+ * stranded as CLAIMED until the TTL sweep kills them.
133
+ */
134
+ readJournalEntries(): DaemonJournalEntry[];
135
+ /**
136
+ * Scans `daemon-journal.jsonl` upon acquiring leadership and heals any in-flight contracts
137
+ * orphaned by a crashed predecessor daemon instance (Phase 3 Step 2, FC-404).
138
+ *
139
+ * Safety invariants:
140
+ * - CK-05 (PID reuse safety): never sends kill signals to bare numeric PIDs from old runs.
141
+ * - Liveness quarantine: probes `isProcessAlive(child_pid)` and `isProcessAlive(daemon_pid)`.
142
+ * If the predecessor daemon or orphaned child is still alive (e.g. GC pause / lease expiry),
143
+ * quarantines the contract as `FAILED` (`QUARANTINED_ORPHAN_ALIVE`) instead of re-queueing
144
+ * to prevent concurrent double-execution on the same workspace.
145
+ * - Mutation task protection: `implementation` and `test_generation` tasks interrupted mid-flight
146
+ * are never auto-requeued (`MARKED_FAILED_MUTATION_UNSAFE`) to protect working-tree integrity.
147
+ * - Read-only tasks (`code_review` / `architecture_audit`) whose predecessor & child are confirmed
148
+ * dead are released back to the `SUBMITTED` queue (or marked `FAILED` if `MAX_RELEASES` is reached).
149
+ */
150
+ recoverFromJournal(): JournalRecoveryItem[];
151
+ compactJournalIfSettled(): boolean;
152
+ /**
153
+ * R7: operator-facing snapshot of the daemon's lease and journal health.
154
+ * Exposes `journalDegraded` / `lastJournalError`, which were previously only internal fields
155
+ * with no reader — a silently degraded write-ahead journal now shows up here and on stderr.
156
+ */
157
+ getStatus(): {
158
+ lock_id: string;
159
+ is_leader: boolean;
160
+ owner_token: string;
161
+ daemon_pid: number;
162
+ daemon_started_at: number;
163
+ journal_path: string;
164
+ journal_degraded: boolean;
165
+ last_journal_error: string | null;
166
+ journal_entries: number;
167
+ lease: {
168
+ owner_token: string;
169
+ heartbeat_at: number;
170
+ expires_at: number;
171
+ metadata: unknown;
172
+ } | null;
173
+ lease_held_by_self: boolean;
174
+ };
39
175
  renewLease(): boolean;
40
176
  releaseLease(): void;
41
177
  resolveHandler(targetAgent: string, taskType: string): HandlerDefinition | null;