@hasna/loops 0.3.55 → 0.3.57

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,6 +1,6 @@
1
1
  import type { Loop, LoopRun } from "../types.js";
2
2
  import type { Store } from "./store.js";
3
- export type RunFailureClassification = "rate_limit" | "auth" | "model_not_found" | "context_length" | "schema_response_format" | "node_init" | "preflight" | "timeout" | "sigsegv" | "skipped_previous_active" | "unknown";
3
+ export type RunFailureClassification = "rate_limit" | "auth" | "model_not_found" | "context_length" | "schema_response_format" | "node_init" | "preflight" | "route_functional" | "timeout" | "sigsegv" | "skipped_previous_active" | "unknown";
4
4
  export interface RunFailureSignal {
5
5
  classification: RunFailureClassification;
6
6
  fingerprint: string;
@@ -34,7 +34,7 @@ export interface LoopExpectationResult {
34
34
  loop: Pick<Loop, "id" | "name" | "status" | "nextRunAt">;
35
35
  ok: boolean;
36
36
  check: {
37
- id: "latest-run-succeeded";
37
+ id: "latest-run-succeeded" | "route-functional-health";
38
38
  status: "pass" | "fail" | "warn";
39
39
  message: string;
40
40
  };
@@ -140,6 +140,9 @@ export declare class Store {
140
140
  project: number;
141
141
  projectGroup?: number;
142
142
  };
143
+ requeueWorkflowWorkItem(id: string, patch?: {
144
+ reason?: string;
145
+ }): WorkflowWorkItem;
143
146
  admitWorkflowWorkItem(id: string, patch: {
144
147
  workflowId: string;
145
148
  loopId: string;
package/dist/lib/store.js CHANGED
@@ -459,6 +459,9 @@ function validateTarget(value, label, opts) {
459
459
  }
460
460
  if (value.model !== undefined)
461
461
  assertString(value.model, `${label}.model`);
462
+ if (value.provider === "opencode" && (typeof value.model !== "string" || value.model.trim() === "")) {
463
+ throw new Error(`${label}.model is required for provider opencode; pass a provider/model id such as openrouter/google/gemini-2.5-flash`);
464
+ }
462
465
  if (value.variant !== undefined)
463
466
  assertString(value.variant, `${label}.variant`);
464
467
  if (value.agent !== undefined)
@@ -1761,7 +1764,7 @@ class Store {
1761
1764
  if (!sourceDedupeKey)
1762
1765
  throw new Error("cannot refresh workflow invocation without sourceRef.dedupeKey");
1763
1766
  const now = nowIso();
1764
- const claimableStatuses = ["queued", "deferred", "failed", "dead_letter", "cancelled"];
1767
+ const claimableStatuses = ["queued", "deferred"];
1765
1768
  const statusBindings = Object.fromEntries(claimableStatuses.map((status, index) => [`$status${index}`, status]));
1766
1769
  const placeholders = claimableStatuses.map((_, index) => `$status${index}`).join(",");
1767
1770
  const result = this.db.query(`UPDATE workflow_invocations
@@ -1842,28 +1845,35 @@ class Store {
1842
1845
  project_group=excluded.project_group,
1843
1846
  priority=excluded.priority,
1844
1847
  status=CASE
1845
- WHEN workflow_work_items.status IN ('succeeded', 'admitted', 'running')
1848
+ WHEN workflow_work_items.status IN ('succeeded', 'admitted', 'running', 'failed', 'dead_letter', 'cancelled')
1846
1849
  THEN workflow_work_items.status
1847
1850
  ELSE excluded.status
1848
1851
  END,
1849
1852
  workflow_id=CASE
1850
- WHEN workflow_work_items.status IN ('succeeded', 'admitted', 'running') THEN workflow_work_items.workflow_id
1853
+ WHEN workflow_work_items.status IN ('succeeded', 'admitted', 'running', 'failed', 'dead_letter', 'cancelled') THEN workflow_work_items.workflow_id
1851
1854
  ELSE NULL
1852
1855
  END,
1853
1856
  loop_id=CASE
1854
- WHEN workflow_work_items.status IN ('succeeded', 'admitted', 'running') THEN workflow_work_items.loop_id
1857
+ WHEN workflow_work_items.status IN ('succeeded', 'admitted', 'running', 'failed', 'dead_letter', 'cancelled') THEN workflow_work_items.loop_id
1855
1858
  ELSE NULL
1856
1859
  END,
1857
1860
  workflow_run_id=CASE
1858
- WHEN workflow_work_items.status IN ('succeeded', 'admitted', 'running') THEN workflow_work_items.workflow_run_id
1861
+ WHEN workflow_work_items.status IN ('succeeded', 'admitted', 'running', 'failed', 'dead_letter', 'cancelled') THEN workflow_work_items.workflow_run_id
1859
1862
  ELSE NULL
1860
1863
  END,
1861
1864
  lease_expires_at=CASE
1862
- WHEN workflow_work_items.status IN ('succeeded', 'admitted', 'running') THEN workflow_work_items.lease_expires_at
1865
+ WHEN workflow_work_items.status IN ('succeeded', 'admitted', 'running', 'failed', 'dead_letter', 'cancelled') THEN workflow_work_items.lease_expires_at
1863
1866
  ELSE NULL
1864
1867
  END,
1865
1868
  next_attempt_at=excluded.next_attempt_at,
1866
- last_reason=COALESCE(excluded.last_reason, workflow_work_items.last_reason),
1869
+ last_reason=CASE
1870
+ WHEN workflow_work_items.attempts > 0
1871
+ AND workflow_work_items.status IN ('queued', 'deferred')
1872
+ AND workflow_work_items.last_reason IS NOT NULL
1873
+ AND excluded.last_reason IS NOT NULL
1874
+ THEN workflow_work_items.last_reason || '; ' || excluded.last_reason
1875
+ ELSE COALESCE(excluded.last_reason, workflow_work_items.last_reason)
1876
+ END,
1867
1877
  updated_at=excluded.updated_at`).run({
1868
1878
  $id: id,
1869
1879
  $routeKey: input.routeKey,
@@ -1916,11 +1926,39 @@ class Store {
1916
1926
  const projectGroup = args.projectGroup ? this.db.query(`SELECT COUNT(*) AS count FROM workflow_work_items WHERE status IN (${placeholders}) AND project_group = ?`).get(...active, args.projectGroup)?.count ?? 0 : undefined;
1917
1927
  return { global, project, ...projectGroup !== undefined ? { projectGroup } : {} };
1918
1928
  }
1929
+ requeueWorkflowWorkItem(id, patch = {}) {
1930
+ const current = this.getWorkflowWorkItem(id);
1931
+ if (!current)
1932
+ throw new Error(`workflow work item not found: ${id}`);
1933
+ const requeueableStatuses = ["succeeded", "failed", "dead_letter", "cancelled"];
1934
+ if (!requeueableStatuses.includes(current.status)) {
1935
+ throw new Error(`workflow work item is not requeueable: ${id} status=${current.status}`);
1936
+ }
1937
+ const now = nowIso();
1938
+ const reason = patch.reason?.trim() || `requeued from ${current.status}`;
1939
+ const placeholders = requeueableStatuses.map(() => "?").join(",");
1940
+ const res = this.db.query(`UPDATE workflow_work_items
1941
+ SET status='queued', workflow_id=NULL, loop_id=NULL, workflow_run_id=NULL,
1942
+ next_attempt_at=NULL, lease_expires_at=NULL, last_reason=?, updated_at=?
1943
+ WHERE id=? AND status IN (${placeholders})`).run(reason, now, id, ...requeueableStatuses);
1944
+ const item = this.getWorkflowWorkItem(id);
1945
+ if (!item)
1946
+ throw new Error(`workflow work item not found after requeue: ${id}`);
1947
+ if (res.changes !== 1)
1948
+ throw new Error(`workflow work item was not requeued: ${id} status=${item.status}`);
1949
+ return item;
1950
+ }
1919
1951
  admitWorkflowWorkItem(id, patch) {
1920
1952
  const now = nowIso();
1921
1953
  const res = this.db.query(`UPDATE workflow_work_items
1922
1954
  SET status='admitted', attempts=attempts + 1, workflow_id=$workflowId, loop_id=$loopId,
1923
- next_attempt_at=NULL, lease_expires_at=NULL, last_reason=$reason, updated_at=$updated
1955
+ next_attempt_at=NULL,
1956
+ lease_expires_at=NULL,
1957
+ last_reason=CASE
1958
+ WHEN last_reason IS NOT NULL AND $reason IS NOT NULL THEN last_reason || '; ' || $reason
1959
+ ELSE COALESCE($reason, last_reason)
1960
+ END,
1961
+ updated_at=$updated
1924
1962
  WHERE id=$id AND status IN ('queued', 'deferred')`).run({
1925
1963
  $id: id,
1926
1964
  $workflowId: patch.workflowId,
@@ -41,6 +41,8 @@ export interface TodosTaskWorkflowTemplateInput {
41
41
  worktreeRoot?: string;
42
42
  worktreeBranchPrefix?: string;
43
43
  timeoutMs?: TimeoutMs;
44
+ verifierIdleTimeoutMs?: number;
45
+ prHandoff?: boolean;
44
46
  eventId?: string;
45
47
  eventType?: string;
46
48
  }
@@ -78,6 +80,7 @@ export interface EventWorkflowTemplateInput {
78
80
  worktreeRoot?: string;
79
81
  worktreeBranchPrefix?: string;
80
82
  timeoutMs?: TimeoutMs;
83
+ verifierIdleTimeoutMs?: number;
81
84
  }
82
85
  export interface BoundedAgentWorkflowTemplateInput {
83
86
  name?: string;
@@ -110,6 +113,7 @@ export interface BoundedAgentWorkflowTemplateInput {
110
113
  worktreeRoot?: string;
111
114
  worktreeBranchPrefix?: string;
112
115
  timeoutMs?: TimeoutMs;
116
+ verifierIdleTimeoutMs?: number;
113
117
  }
114
118
  export type LoopTemplateSourceFilter = LoopTemplateSource | "all";
115
119
  export interface ListLoopTemplatesOptions {
@@ -0,0 +1,12 @@
1
+ #!/usr/bin/env bun
2
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
+ export interface LoopsMcpToolMetadata {
4
+ name: string;
5
+ description: string;
6
+ readOnly: boolean;
7
+ guarded?: boolean;
8
+ requiresEnv?: string;
9
+ }
10
+ export declare const LOOPS_MCP_TOOLS: LoopsMcpToolMetadata[];
11
+ export declare function createLoopsMcpServer(): McpServer;
12
+ export declare function listToolsForCli(): LoopsMcpToolMetadata[];