@hasna/workflows 0.1.3 → 0.2.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.
@@ -1,16 +1,82 @@
1
+ import { type RunToCompletionOptions } from "./daemon.js";
2
+ import { openStore } from "./store.js";
1
3
  import type { WorkflowGraph } from "./graph.js";
2
4
  import type { HealthReport, ReadinessReport, WorkflowsConfig } from "./types.js";
3
- export declare function packageVersion(): string;
5
+ export { packageVersion } from "./version.js";
4
6
  /** Resolve the service configuration from env + explicit overrides. */
5
7
  export declare function resolveWorkflowsConfig(overrides?: Partial<WorkflowsConfig>): WorkflowsConfig;
6
8
  export declare class WorkflowsService {
9
+ private readonly execution;
7
10
  readonly name = "workflows";
8
11
  readonly version: string;
9
12
  readonly config: WorkflowsConfig;
10
13
  private readonly startedAt;
11
- constructor(config?: Partial<WorkflowsConfig>);
14
+ private readonly databaseEnv;
15
+ constructor(config?: Partial<WorkflowsConfig>, execution?: RunToCompletionOptions);
12
16
  health(): HealthReport;
13
- ready(): ReadinessReport;
17
+ ready(): Promise<ReadinessReport>;
18
+ private openEngineStore;
19
+ private withStore;
20
+ /** Explicit operator bootstrap; ordinary reads and server startup never create tables. */
21
+ initializeStorage(): Promise<void>;
22
+ listRuns(options?: Parameters<ReturnType<typeof openStore>["listRuns"]>[0]): Promise<import("./store.js").RunRow[]>;
23
+ getRun(id: string): Promise<import("./store.js").RunRow | undefined>;
24
+ listNodes(id: string): Promise<import("./store.js").RunNodeRow[]>;
25
+ events(id: string): import("./wal.js").WalEntry[];
26
+ cancelRun(id: string): Promise<import("./store.js").RunRow | undefined>;
27
+ resumeRun(id: string): Promise<{
28
+ runId: string;
29
+ status: "cancelled" | "pending" | "running" | "completed" | "failed" | "interrupted";
30
+ error: string | null;
31
+ result: any;
32
+ reused: boolean;
33
+ }>;
34
+ sessions(action: "list" | "pull"): {
35
+ seq: number;
36
+ at: string;
37
+ operation: "claim_acquired" | "claim_released" | "graph_registered" | "run_started" | "run_finished" | "node_started" | "node_finished" | "memo_set";
38
+ }[] | {
39
+ entries: number;
40
+ torn: boolean;
41
+ repaired: boolean;
42
+ liveClaims: {
43
+ runId: string;
44
+ worker: string;
45
+ fencing: number;
46
+ expiresAt: string;
47
+ }[];
48
+ };
49
+ machines(action: "list" | "status"): {
50
+ name: string;
51
+ role: string;
52
+ version: string;
53
+ }[] | {
54
+ health: HealthReport;
55
+ sessions: {
56
+ seq: number;
57
+ at: string;
58
+ operation: "claim_acquired" | "claim_released" | "graph_registered" | "run_started" | "run_finished" | "node_started" | "node_finished" | "memo_set";
59
+ }[] | {
60
+ entries: number;
61
+ torn: boolean;
62
+ repaired: boolean;
63
+ liveClaims: {
64
+ runId: string;
65
+ worker: string;
66
+ fencing: number;
67
+ expiresAt: string;
68
+ }[];
69
+ };
70
+ name: string;
71
+ role: string;
72
+ version: string;
73
+ };
74
+ daemonStatus(): {
75
+ activeClaims: number;
76
+ mode: string;
77
+ continuousDaemon: boolean;
78
+ };
79
+ listMemos(): Promise<import("./store.js").MemoRow[]>;
14
80
  /**
15
81
  * The authenticated trigger: run a graph to a terminal state (bounded
16
82
  * cycles) and return its summary. The HTTP layer authenticates; this
@@ -29,4 +95,9 @@ export declare class WorkflowsService {
29
95
  reused: boolean;
30
96
  }>;
31
97
  }
32
- export declare function createWorkflowsService(config?: Partial<WorkflowsConfig>): WorkflowsService;
98
+ export declare function createWorkflowsService(config?: Partial<WorkflowsConfig>, execution?: RunToCompletionOptions): WorkflowsService;
99
+ export declare class WorkflowRequestError extends Error {
100
+ readonly code: string;
101
+ readonly status: number;
102
+ constructor(code: string, message: string, status?: number);
103
+ }
@@ -1,5 +1,6 @@
1
1
  import { SessionWAL } from "./wal.js";
2
- import type { RunRow, WorkflowsStore } from "./store.js";
2
+ import type { RunRow, WorkflowsStore, EngineStore } from "./store.js";
3
+ import type { WorkflowGraph } from "./graph.js";
3
4
  export interface RepairResult {
4
5
  /** runs found running with no live claim */
5
6
  interrupted: number;
@@ -9,18 +10,41 @@ export interface RepairResult {
9
10
  failed: number;
10
11
  }
11
12
  export declare const DEFAULT_MAX_ATTEMPTS = 3;
12
- export declare function repairTornRuns(store: WorkflowsStore, wal: SessionWAL, opts?: {
13
+ export declare function repairTornRuns<S extends EngineStore>(store: S, wal: SessionWAL, opts?: {
13
14
  maxAttempts?: number;
14
15
  now?: number;
15
- }): RepairResult;
16
+ }): S extends WorkflowsStore ? RepairResult : Promise<RepairResult>;
16
17
  /** Stable hash of a JSON-serializable payload. */
17
18
  export declare function inputHash(payload: unknown): string;
18
19
  /** Memo key: graphName:nodeId:<inputHash>. */
19
20
  export declare function memoKey(graphName: string, nodeId: string, input: unknown): string;
20
21
  /** Look up a memo by input; a hit increments hitCount and returns the row. */
21
- export declare function tryMemoHit(store: WorkflowsStore, graphName: string, nodeId: string, input: unknown): import("./store.js").MemoRow | undefined;
22
+ export declare function tryMemoHit<S extends EngineStore>(store: S, graphName: string, nodeId: string, input: unknown): ReturnType<S["memoGet"]>;
22
23
  /** Persist a node output for future runs with the same input. */
23
- export declare function recordMemo(store: WorkflowsStore, graphName: string, nodeId: string, input: unknown, output: unknown): void;
24
+ export declare function recordMemo<S extends EngineStore>(store: S, graphName: string, nodeId: string, input: unknown, output: unknown): ReturnType<S["memoPut"]>;
25
+ /** One watched file's fingerprint entry (mtime + sha256). */
26
+ export interface MemoWatchEntry {
27
+ /** The pattern as declared on the graph. */
28
+ pattern: string;
29
+ /** The resolved absolute path (or the glob-relative file path). */
30
+ path: string;
31
+ missing: boolean;
32
+ mtimeMs: number | null;
33
+ size: number | null;
34
+ sha256: string | null;
35
+ }
36
+ /**
37
+ * Fingerprint of the graph's memoWatch files: mtimes + content hashes.
38
+ * Stress V3/V4 F2 (measured 2026-08-30): a memoized node reading
39
+ * $WORKFLOWS_DATA_DIR/scratch/wf2.cnt served a stale cached "0" while the
40
+ * file said "2" — the memo key covered only {command, prompt, context}. The
41
+ * fingerprint joins the memo input, so a changed watched file invalidates the
42
+ * cache exactly when the command's live result can differ. Exact paths and
43
+ * glob patterns are supported (relative to `dataDir` or absolute); a missing
44
+ * exact path keeps a stable `missing` entry so its later appearance (or a
45
+ * re-creation with different content) still changes the fingerprint.
46
+ */
47
+ export declare function memoWatchFingerprint(graph: WorkflowGraph, dataDir: string): MemoWatchEntry[] | undefined;
24
48
  /** Requeue a cancelled run's in-flight nodes (used by cancel + resume). */
25
49
  export declare function resetRunNodes(store: WorkflowsStore, run: RunRow): void;
26
50
  export interface RestoreResult {
@@ -45,7 +69,6 @@ export declare function restoreInterruptedRun(store: WorkflowsStore, runId: stri
45
69
  /**
46
70
  * Idempotent run lookup: the run whose context carries the given idempotency
47
71
  * key, if any. The three-table law forbids an extra column, so the key lives
48
- * in the reserved __wf namespace of context_json; the scan is bounded by the
49
- * store's own listRuns cap (1000 rows).
72
+ * in the reserved __wf namespace of context_json; a database lookup covers the entire run history.
50
73
  */
51
- export declare function findRunByIdempotencyKey(store: WorkflowsStore, key: string): RunRow | undefined;
74
+ export declare function findRunByIdempotencyKey<S extends EngineStore>(store: S, key: string): ReturnType<S["getRunByIdempotencyKey"]>;
@@ -0,0 +1,71 @@
1
+ import { type IdentityEnvironment } from "./identity.js";
2
+ type SetupOptions = {
3
+ apiUrl?: string;
4
+ replace?: boolean;
5
+ agent?: string;
6
+ env?: IdentityEnvironment;
7
+ fetchImpl?: typeof fetch;
8
+ };
9
+ export declare function setupWorkflows(options?: SetupOptions): Promise<{
10
+ app: string;
11
+ action: string;
12
+ credentials_path: string;
13
+ mode: string;
14
+ authority: string;
15
+ verified_before_write: boolean;
16
+ verified_after_write: boolean;
17
+ effective_tier: "disk";
18
+ effective_source: string;
19
+ preservation: string | undefined;
20
+ warning: null;
21
+ }>;
22
+ export declare function loginWorkflows(options?: SetupOptions, suppliedKey?: string): Promise<{
23
+ app: string;
24
+ action: string;
25
+ credentials_path: string;
26
+ mode: string;
27
+ authority: string;
28
+ verified_before_write: boolean;
29
+ verified_after_write: boolean;
30
+ effective_tier: "disk";
31
+ effective_source: string;
32
+ preservation: string | undefined;
33
+ warning: null;
34
+ }>;
35
+ /** Sign out only a key written by login; environment and setup credentials stay owned by their source. */
36
+ export declare function logoutWorkflows(options?: {
37
+ revoke?: boolean;
38
+ env?: IdentityEnvironment;
39
+ }): {
40
+ app: string;
41
+ action: string;
42
+ exit_code: number;
43
+ message: string;
44
+ } | {
45
+ app: string;
46
+ action: string;
47
+ credentials_path: string;
48
+ preservation: string;
49
+ exit_code: number;
50
+ message: string;
51
+ };
52
+ export declare function workflowsWhoami(options?: {
53
+ apiUrl?: string;
54
+ agent?: string;
55
+ env?: IdentityEnvironment;
56
+ }): {
57
+ app: string;
58
+ agent: string;
59
+ agent_source: string;
60
+ authority: string;
61
+ credential_tier: import("@hasna/contracts").CredentialTier;
62
+ credential_source: string | null;
63
+ } | {
64
+ app: string;
65
+ agent: string;
66
+ agent_source: string;
67
+ authority: string;
68
+ credential_tier: string;
69
+ credential_source: null;
70
+ };
71
+ export {};
package/types/store.d.ts CHANGED
@@ -48,11 +48,35 @@ export interface CreateRunNodeInput {
48
48
  nodeId: string;
49
49
  lane?: string;
50
50
  }
51
+ export interface StepCommit {
52
+ runId: string;
53
+ nodeRowId: string;
54
+ status: NodeStatus;
55
+ patch?: Parameters<WorkflowsStore["setRunNodeStatus"]>[2];
56
+ incrementAttempts?: boolean;
57
+ context?: unknown;
58
+ memo?: {
59
+ key: string;
60
+ graphName: string;
61
+ nodeId: string;
62
+ inputHash: string;
63
+ outputJson: string;
64
+ };
65
+ }
66
+ export type DispatchResult<T> = {
67
+ started: false;
68
+ } | {
69
+ started: true;
70
+ result: T;
71
+ };
51
72
  export interface WorkflowsStore {
73
+ /** The data dir this store was opened on (resolves relative memoWatch paths). */
74
+ readonly dataDir: string;
52
75
  close(): void;
53
76
  listTables(): string[];
54
77
  createRun(input: CreateRunInput): RunRow;
55
78
  getRun(id: string): RunRow | undefined;
79
+ getRunByIdempotencyKey(key: string): RunRow | undefined;
56
80
  listRuns(opts?: {
57
81
  status?: RunStatus;
58
82
  limit?: number;
@@ -63,12 +87,21 @@ export interface WorkflowsStore {
63
87
  startedAt?: string;
64
88
  finishedAt?: string;
65
89
  }): void;
90
+ compareRunStatus(id: string, expected: RunStatus[], status: RunStatus, patch?: Parameters<WorkflowsStore["setRunStatus"]>[2]): boolean;
66
91
  /** Persist the run's durable execution state (the engine's cursor + steps). */
67
92
  setRunContext(id: string, context: unknown): void;
93
+ setRunningContext(id: string, context: unknown): boolean;
94
+ blockRunResume(id: string): void;
95
+ finishDispatch(id: string, unconfirmed: boolean): void;
96
+ dispatchIfRunning<T>(id: string, action: () => T): DispatchResult<T>;
97
+ commitStep(input: StepCommit): boolean;
68
98
  bumpAttempts(id: string): void;
69
99
  createRunNode(input: CreateRunNodeInput): RunNodeRow;
70
100
  getRunNode(id: string): RunNodeRow | undefined;
71
101
  listRunNodes(runId: string): RunNodeRow[];
102
+ /** The newest run_nodes row for a (run, node) pair — a node may own one row
103
+ * per while iteration, and `nodes show` + engine row-reuse need the latest. */
104
+ getLatestRunNode(runId: string, nodeId: string): RunNodeRow | undefined;
72
105
  setRunNodeStatus(id: string, status: NodeStatus, patch?: {
73
106
  exitCode?: number | null;
74
107
  output?: unknown;
@@ -84,3 +117,5 @@ export interface WorkflowsStore {
84
117
  memoClear(): void;
85
118
  }
86
119
  export declare function openStore(dataDir: string): WorkflowsStore;
120
+ /** One execution engine accepts the synchronous embedded and asynchronous hosted stores. */
121
+ export type EngineStore = WorkflowsStore | import("./db/postgres-store.js").AsyncWorkflowsStore;
@@ -0,0 +1,18 @@
1
+ export interface McpTool {
2
+ name: string;
3
+ description: string;
4
+ inputSchema: {
5
+ type: "object";
6
+ properties: Record<string, unknown>;
7
+ required?: string[];
8
+ };
9
+ }
10
+ export interface ToolCallResult {
11
+ content: {
12
+ type: "text";
13
+ text: string;
14
+ }[];
15
+ isError: boolean;
16
+ }
17
+ export declare function textResult(payload: unknown): ToolCallResult;
18
+ export declare function workflowsTools(_service?: unknown): McpTool[];
package/types/types.d.ts CHANGED
@@ -20,9 +20,17 @@ export interface WorkflowsConfig {
20
20
  apiUrl?: string;
21
21
  /** Optional client API key (WORKFLOWS_API_KEY) — resolved, never printed. */
22
22
  apiKey?: string;
23
+ /** Gateway origin identity. When configured, every /v1 route requires it. */
24
+ originAuth?: string;
25
+ /** Hosted server authority. SQLite requires an explicit embedded selection. */
26
+ storageBackend: "postgresql" | "sqlite";
27
+ databaseUrl?: string;
28
+ databaseSchema?: string;
23
29
  }
24
30
  export interface HealthReport {
25
31
  ok: boolean;
32
+ status: "ok";
33
+ backend: "postgresql" | "sqlite";
26
34
  service: string;
27
35
  version: string;
28
36
  pid: number;
@@ -0,0 +1 @@
1
+ export declare function packageVersion(): string;
package/types/wal.d.ts CHANGED
@@ -71,14 +71,22 @@ export declare const WAL_FILE_NAME = "session.wal";
71
71
  export declare const SESSIONS_DIR_NAME = "sessions";
72
72
  export declare class SessionWAL {
73
73
  readonly filePath: string;
74
+ private journalLocked;
74
75
  private constructor();
75
76
  static open(dataDir: string): SessionWAL;
76
77
  static openAt(filePath: string): SessionWAL;
77
78
  /** Append one op and return the persisted entry (seq is 1-based). */
78
79
  append(op: WalOp): WalEntry;
80
+ private withJournalLock;
81
+ /** Atomic journal claim/release decisions use the same cross-process mutex. */
82
+ exclusive<T>(operation: () => T): T;
83
+ private appendLocked;
79
84
  private nextSeq;
80
85
  /** Read and verify the whole WAL; truncate the first torn tail. */
81
- replay(): WalReplay;
86
+ replay(options?: {
87
+ repair?: boolean;
88
+ }): WalReplay;
89
+ private replayLocked;
82
90
  close(): void;
83
91
  }
84
92
  /** Write a fresh (empty) WAL — used by tests and by init. */