@hasna/loops 0.4.4 → 0.4.6

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,10 +1,21 @@
1
1
  import type { LanguageModel } from "ai";
2
- import type { AgentTarget, ExecutableTarget, ExecutorResult, Loop, LoopMachineRef, LoopRun, PersistGuardOptions } from "../types.js";
2
+ import type { AgentProvider, AgentTarget, ExecutableTarget, ExecutorResult, Loop, LoopMachineRef, LoopRun, PersistGuardOptions } from "../types.js";
3
3
  export interface SpawnedProcessInfo {
4
4
  pid: number;
5
5
  pgid: number;
6
6
  processStartedAt: string;
7
7
  }
8
+ export interface AgentProgressInfo {
9
+ provider: AgentProvider;
10
+ agentId?: string;
11
+ status?: string;
12
+ summary?: string;
13
+ statusReason?: string;
14
+ threadId?: string;
15
+ rolloutPath?: string;
16
+ pid?: number;
17
+ lastEventSeq?: number;
18
+ }
8
19
  export interface ExecuteOptions extends PersistGuardOptions {
9
20
  maxOutputBytes?: number;
10
21
  env?: NodeJS.ProcessEnv;
@@ -14,6 +25,8 @@ export interface ExecuteOptions extends PersistGuardOptions {
14
25
  onSpawn?: (pid: number) => void;
15
26
  /** Children are spawned detached in their own process group, so pgid === pid. */
16
27
  onSpawnProcess?: (info: SpawnedProcessInfo) => void;
28
+ /** Progress from durable provider controllers, for example Codewith background agents. */
29
+ onAgentProgress?: (info: AgentProgressInfo) => void;
17
30
  machine?: LoopMachineRef;
18
31
  machineResolver?: (machine: LoopMachineRef) => LoopMachineRef;
19
32
  machineCommandResolver?: (machineId: string, command: string) => MachineCommandPlan;
package/dist/lib/mode.js CHANGED
@@ -2,7 +2,7 @@
2
2
  // package.json
3
3
  var package_default = {
4
4
  name: "@hasna/loops",
5
- version: "0.4.4",
5
+ version: "0.4.6",
6
6
  description: "Persistent local loop and workflow runner for deterministic commands and headless AI coding agents",
7
7
  type: "module",
8
8
  main: "dist/index.js",
@@ -1491,11 +1491,14 @@ function ensurePrivateStorePath(file) {
1491
1491
  class Store {
1492
1492
  db;
1493
1493
  rootDir;
1494
+ memoryRootDir;
1494
1495
  constructor(path) {
1495
1496
  const file = path ?? dbPath();
1496
1497
  if (file !== ":memory:")
1497
1498
  ensurePrivateStorePath(file);
1498
1499
  this.rootDir = file === ":memory:" ? mkdtempSync(join3(tmpdir(), "open-loops-store-")) : dirname2(file);
1500
+ if (file === ":memory:")
1501
+ this.memoryRootDir = this.rootDir;
1499
1502
  this.db = new Database(file);
1500
1503
  this.db.exec("PRAGMA foreign_keys = ON;");
1501
1504
  this.db.exec("PRAGMA busy_timeout = 5000;");
@@ -1640,7 +1643,6 @@ class Store {
1640
1643
  CREATE INDEX IF NOT EXISTS idx_runs_status ON loop_runs(status);
1641
1644
  CREATE INDEX IF NOT EXISTS idx_runs_status_lease ON loop_runs(status, lease_expires_at);
1642
1645
  CREATE INDEX IF NOT EXISTS idx_runs_scheduled ON loop_runs(scheduled_for);
1643
- CREATE INDEX IF NOT EXISTS idx_runs_claim_token ON loop_runs(claim_token) WHERE claim_token IS NOT NULL;
1644
1646
 
1645
1647
  CREATE TABLE IF NOT EXISTS daemon_lease (
1646
1648
  id TEXT PRIMARY KEY,
@@ -1949,9 +1951,12 @@ class Store {
1949
1951
  const rows = this.db.query("SELECT * FROM loops WHERE name = ? ORDER BY created_at DESC LIMIT 2").all(idOrName);
1950
1952
  if (rows.length === 0)
1951
1953
  throw new LoopNotFoundError(idOrName);
1952
- if (rows.length > 1)
1954
+ if (rows.length === 1)
1955
+ return rowToLoop(rows[0]);
1956
+ const active = this.db.query("SELECT * FROM loops WHERE name = ? AND archived_at IS NULL ORDER BY created_at DESC LIMIT 2").all(idOrName);
1957
+ if (active.length !== 1)
1953
1958
  throw new AmbiguousNameError(idOrName);
1954
- return rowToLoop(rows[0]);
1959
+ return rowToLoop(active[0]);
1955
1960
  }
1956
1961
  requireLoop(idOrName) {
1957
1962
  return this.getLoop(idOrName) ?? this.findLoopByName(idOrName) ?? (() => {
@@ -2292,6 +2297,7 @@ class Store {
2292
2297
  return this.transact(() => {
2293
2298
  const loop = this.requireLoop(idOrName);
2294
2299
  this.setWorkflowWorkItemsForLoop(loop.id, "cancelled", "loop deleted", nowIso());
2300
+ this.db.query("DELETE FROM loop_runs WHERE loop_id = ?").run(loop.id);
2295
2301
  const res = this.db.query("DELETE FROM loops WHERE id = ?").run(loop.id);
2296
2302
  return res.changes > 0;
2297
2303
  });
@@ -2776,7 +2782,10 @@ class Store {
2776
2782
  return rowToGoal(row);
2777
2783
  }
2778
2784
  if (context.sourceType && context.sourceId) {
2779
- const row = this.db.query("SELECT * FROM goals WHERE source_type = ? AND source_id = ? ORDER BY created_at DESC LIMIT 1").get(context.sourceType, context.sourceId);
2785
+ const terminalPlaceholders = GOAL_TERMINAL.map(() => "?").join(", ");
2786
+ const row = this.db.query(`SELECT * FROM goals
2787
+ WHERE source_type = ? AND source_id = ? AND status NOT IN (${terminalPlaceholders})
2788
+ ORDER BY created_at DESC LIMIT 1`).get(context.sourceType, context.sourceId, ...GOAL_TERMINAL);
2780
2789
  if (row)
2781
2790
  return rowToGoal(row);
2782
2791
  }
@@ -3196,6 +3205,41 @@ class Store {
3196
3205
  throw new Error(`workflow step run not found after pid update: ${workflowRunId}/${stepId}`);
3197
3206
  return run;
3198
3207
  }
3208
+ recordWorkflowStepProgress(workflowRunId, stepId, progress, opts = {}) {
3209
+ const now = (opts.now ?? new Date).toISOString();
3210
+ this.db.exec("BEGIN IMMEDIATE");
3211
+ try {
3212
+ const res = this.db.query(`UPDATE workflow_step_runs
3213
+ SET stdout=COALESCE($stdout, stdout),
3214
+ stderr=COALESCE($stderr, stderr),
3215
+ updated_at=$updated
3216
+ WHERE workflow_run_id=$workflowRunId AND step_id=$stepId AND status='running'
3217
+ AND ($daemonLeaseId IS NULL OR EXISTS (
3218
+ SELECT 1 FROM daemon_lease WHERE id=$daemonLeaseId AND expires_at > $now
3219
+ ))`).run({
3220
+ $workflowRunId: workflowRunId,
3221
+ $stepId: stepId,
3222
+ $stdout: progress.stdout === undefined ? null : scrubbedOrNull(progress.stdout),
3223
+ $stderr: progress.stderr === undefined ? null : scrubbedOrNull(progress.stderr),
3224
+ $updated: now,
3225
+ $daemonLeaseId: opts.daemonLeaseId ?? null,
3226
+ $now: now
3227
+ });
3228
+ if (res.changes === 1) {
3229
+ this.appendWorkflowEvent(workflowRunId, "step_progress", stepId, progress.payload);
3230
+ }
3231
+ this.db.exec("COMMIT");
3232
+ } catch (error) {
3233
+ try {
3234
+ this.db.exec("ROLLBACK");
3235
+ } catch {}
3236
+ throw error;
3237
+ }
3238
+ const run = this.getWorkflowStepRun(workflowRunId, stepId);
3239
+ if (!run)
3240
+ throw new Error(`workflow step run not found after progress update: ${workflowRunId}/${stepId}`);
3241
+ return run;
3242
+ }
3199
3243
  recoverWorkflowRun(workflowRunId, reason = "workflow run recovered for retry") {
3200
3244
  return this.transact(() => {
3201
3245
  const now = nowIso();
@@ -3633,7 +3677,8 @@ class Store {
3633
3677
  AND ($daemonLeaseId IS NULL OR EXISTS (
3634
3678
  SELECT 1 FROM daemon_lease WHERE id=$daemonLeaseId AND expires_at > $now
3635
3679
  ))`).run(params) : this.db.query(`UPDATE loop_runs SET status=$status, finished_at=$finished, claim_token=NULL, lease_expires_at=NULL, pid=$pid, exit_code=$exitCode,
3636
- duration_ms=$durationMs, stdout=$stdout, stderr=$stderr, error=$error, updated_at=$updated WHERE id=$id`).run(params);
3680
+ duration_ms=$durationMs, stdout=$stdout, stderr=$stderr, error=$error, updated_at=$updated
3681
+ WHERE id=$id AND status='running'`).run(params);
3637
3682
  const run = this.getRun(id);
3638
3683
  if (!run)
3639
3684
  throw new Error(`run not found after finalize: ${id}`);
@@ -4161,6 +4206,12 @@ class Store {
4161
4206
  }
4162
4207
  close() {
4163
4208
  this.db.close();
4209
+ if (this.memoryRootDir) {
4210
+ try {
4211
+ rmSync2(this.memoryRootDir, { recursive: true, force: true });
4212
+ } catch {}
4213
+ this.memoryRootDir = undefined;
4214
+ }
4164
4215
  }
4165
4216
  }
4166
4217
 
@@ -1491,11 +1491,14 @@ function ensurePrivateStorePath(file) {
1491
1491
  class Store {
1492
1492
  db;
1493
1493
  rootDir;
1494
+ memoryRootDir;
1494
1495
  constructor(path) {
1495
1496
  const file = path ?? dbPath();
1496
1497
  if (file !== ":memory:")
1497
1498
  ensurePrivateStorePath(file);
1498
1499
  this.rootDir = file === ":memory:" ? mkdtempSync(join3(tmpdir(), "open-loops-store-")) : dirname2(file);
1500
+ if (file === ":memory:")
1501
+ this.memoryRootDir = this.rootDir;
1499
1502
  this.db = new Database(file);
1500
1503
  this.db.exec("PRAGMA foreign_keys = ON;");
1501
1504
  this.db.exec("PRAGMA busy_timeout = 5000;");
@@ -1640,7 +1643,6 @@ class Store {
1640
1643
  CREATE INDEX IF NOT EXISTS idx_runs_status ON loop_runs(status);
1641
1644
  CREATE INDEX IF NOT EXISTS idx_runs_status_lease ON loop_runs(status, lease_expires_at);
1642
1645
  CREATE INDEX IF NOT EXISTS idx_runs_scheduled ON loop_runs(scheduled_for);
1643
- CREATE INDEX IF NOT EXISTS idx_runs_claim_token ON loop_runs(claim_token) WHERE claim_token IS NOT NULL;
1644
1646
 
1645
1647
  CREATE TABLE IF NOT EXISTS daemon_lease (
1646
1648
  id TEXT PRIMARY KEY,
@@ -1949,9 +1951,12 @@ class Store {
1949
1951
  const rows = this.db.query("SELECT * FROM loops WHERE name = ? ORDER BY created_at DESC LIMIT 2").all(idOrName);
1950
1952
  if (rows.length === 0)
1951
1953
  throw new LoopNotFoundError(idOrName);
1952
- if (rows.length > 1)
1954
+ if (rows.length === 1)
1955
+ return rowToLoop(rows[0]);
1956
+ const active = this.db.query("SELECT * FROM loops WHERE name = ? AND archived_at IS NULL ORDER BY created_at DESC LIMIT 2").all(idOrName);
1957
+ if (active.length !== 1)
1953
1958
  throw new AmbiguousNameError(idOrName);
1954
- return rowToLoop(rows[0]);
1959
+ return rowToLoop(active[0]);
1955
1960
  }
1956
1961
  requireLoop(idOrName) {
1957
1962
  return this.getLoop(idOrName) ?? this.findLoopByName(idOrName) ?? (() => {
@@ -2292,6 +2297,7 @@ class Store {
2292
2297
  return this.transact(() => {
2293
2298
  const loop = this.requireLoop(idOrName);
2294
2299
  this.setWorkflowWorkItemsForLoop(loop.id, "cancelled", "loop deleted", nowIso());
2300
+ this.db.query("DELETE FROM loop_runs WHERE loop_id = ?").run(loop.id);
2295
2301
  const res = this.db.query("DELETE FROM loops WHERE id = ?").run(loop.id);
2296
2302
  return res.changes > 0;
2297
2303
  });
@@ -2776,7 +2782,10 @@ class Store {
2776
2782
  return rowToGoal(row);
2777
2783
  }
2778
2784
  if (context.sourceType && context.sourceId) {
2779
- const row = this.db.query("SELECT * FROM goals WHERE source_type = ? AND source_id = ? ORDER BY created_at DESC LIMIT 1").get(context.sourceType, context.sourceId);
2785
+ const terminalPlaceholders = GOAL_TERMINAL.map(() => "?").join(", ");
2786
+ const row = this.db.query(`SELECT * FROM goals
2787
+ WHERE source_type = ? AND source_id = ? AND status NOT IN (${terminalPlaceholders})
2788
+ ORDER BY created_at DESC LIMIT 1`).get(context.sourceType, context.sourceId, ...GOAL_TERMINAL);
2780
2789
  if (row)
2781
2790
  return rowToGoal(row);
2782
2791
  }
@@ -3196,6 +3205,41 @@ class Store {
3196
3205
  throw new Error(`workflow step run not found after pid update: ${workflowRunId}/${stepId}`);
3197
3206
  return run;
3198
3207
  }
3208
+ recordWorkflowStepProgress(workflowRunId, stepId, progress, opts = {}) {
3209
+ const now = (opts.now ?? new Date).toISOString();
3210
+ this.db.exec("BEGIN IMMEDIATE");
3211
+ try {
3212
+ const res = this.db.query(`UPDATE workflow_step_runs
3213
+ SET stdout=COALESCE($stdout, stdout),
3214
+ stderr=COALESCE($stderr, stderr),
3215
+ updated_at=$updated
3216
+ WHERE workflow_run_id=$workflowRunId AND step_id=$stepId AND status='running'
3217
+ AND ($daemonLeaseId IS NULL OR EXISTS (
3218
+ SELECT 1 FROM daemon_lease WHERE id=$daemonLeaseId AND expires_at > $now
3219
+ ))`).run({
3220
+ $workflowRunId: workflowRunId,
3221
+ $stepId: stepId,
3222
+ $stdout: progress.stdout === undefined ? null : scrubbedOrNull(progress.stdout),
3223
+ $stderr: progress.stderr === undefined ? null : scrubbedOrNull(progress.stderr),
3224
+ $updated: now,
3225
+ $daemonLeaseId: opts.daemonLeaseId ?? null,
3226
+ $now: now
3227
+ });
3228
+ if (res.changes === 1) {
3229
+ this.appendWorkflowEvent(workflowRunId, "step_progress", stepId, progress.payload);
3230
+ }
3231
+ this.db.exec("COMMIT");
3232
+ } catch (error) {
3233
+ try {
3234
+ this.db.exec("ROLLBACK");
3235
+ } catch {}
3236
+ throw error;
3237
+ }
3238
+ const run = this.getWorkflowStepRun(workflowRunId, stepId);
3239
+ if (!run)
3240
+ throw new Error(`workflow step run not found after progress update: ${workflowRunId}/${stepId}`);
3241
+ return run;
3242
+ }
3199
3243
  recoverWorkflowRun(workflowRunId, reason = "workflow run recovered for retry") {
3200
3244
  return this.transact(() => {
3201
3245
  const now = nowIso();
@@ -3633,7 +3677,8 @@ class Store {
3633
3677
  AND ($daemonLeaseId IS NULL OR EXISTS (
3634
3678
  SELECT 1 FROM daemon_lease WHERE id=$daemonLeaseId AND expires_at > $now
3635
3679
  ))`).run(params) : this.db.query(`UPDATE loop_runs SET status=$status, finished_at=$finished, claim_token=NULL, lease_expires_at=NULL, pid=$pid, exit_code=$exitCode,
3636
- duration_ms=$durationMs, stdout=$stdout, stderr=$stderr, error=$error, updated_at=$updated WHERE id=$id`).run(params);
3680
+ duration_ms=$durationMs, stdout=$stdout, stderr=$stderr, error=$error, updated_at=$updated
3681
+ WHERE id=$id AND status='running'`).run(params);
3637
3682
  const run = this.getRun(id);
3638
3683
  if (!run)
3639
3684
  throw new Error(`run not found after finalize: ${id}`);
@@ -4161,6 +4206,12 @@ class Store {
4161
4206
  }
4162
4207
  close() {
4163
4208
  this.db.close();
4209
+ if (this.memoryRootDir) {
4210
+ try {
4211
+ rmSync2(this.memoryRootDir, { recursive: true, force: true });
4212
+ } catch {}
4213
+ this.memoryRootDir = undefined;
4214
+ }
4164
4215
  }
4165
4216
  }
4166
4217
 
@@ -125,6 +125,8 @@ export interface RecordGoalEventInput {
125
125
  export declare class Store {
126
126
  private db;
127
127
  private rootDir;
128
+ /** Temp dir created for a `:memory:` store, removed in close() so tests/short-lived instances don't leak it. */
129
+ private memoryRootDir?;
128
130
  constructor(path?: string);
129
131
  private migrate;
130
132
  private migrations;
@@ -279,6 +281,11 @@ export declare class Store {
279
281
  isWorkflowRunTerminal(workflowRunId: string): boolean;
280
282
  startWorkflowStepRun(workflowRunId: string, stepId: string, opts?: DaemonLeaseFence): WorkflowStepRun;
281
283
  markWorkflowStepPid(workflowRunId: string, stepId: string, pid: number, opts?: DaemonLeaseFence): WorkflowStepRun;
284
+ recordWorkflowStepProgress(workflowRunId: string, stepId: string, progress: {
285
+ stdout?: string;
286
+ stderr?: string;
287
+ payload?: Record<string, unknown>;
288
+ }, opts?: DaemonLeaseFence): WorkflowStepRun;
282
289
  recoverWorkflowRun(workflowRunId: string, reason?: string): {
283
290
  run: WorkflowRun;
284
291
  recoveredSteps: WorkflowStepRun[];
package/dist/lib/store.js CHANGED
@@ -1491,11 +1491,14 @@ function ensurePrivateStorePath(file) {
1491
1491
  class Store {
1492
1492
  db;
1493
1493
  rootDir;
1494
+ memoryRootDir;
1494
1495
  constructor(path) {
1495
1496
  const file = path ?? dbPath();
1496
1497
  if (file !== ":memory:")
1497
1498
  ensurePrivateStorePath(file);
1498
1499
  this.rootDir = file === ":memory:" ? mkdtempSync(join3(tmpdir(), "open-loops-store-")) : dirname2(file);
1500
+ if (file === ":memory:")
1501
+ this.memoryRootDir = this.rootDir;
1499
1502
  this.db = new Database(file);
1500
1503
  this.db.exec("PRAGMA foreign_keys = ON;");
1501
1504
  this.db.exec("PRAGMA busy_timeout = 5000;");
@@ -1640,7 +1643,6 @@ class Store {
1640
1643
  CREATE INDEX IF NOT EXISTS idx_runs_status ON loop_runs(status);
1641
1644
  CREATE INDEX IF NOT EXISTS idx_runs_status_lease ON loop_runs(status, lease_expires_at);
1642
1645
  CREATE INDEX IF NOT EXISTS idx_runs_scheduled ON loop_runs(scheduled_for);
1643
- CREATE INDEX IF NOT EXISTS idx_runs_claim_token ON loop_runs(claim_token) WHERE claim_token IS NOT NULL;
1644
1646
 
1645
1647
  CREATE TABLE IF NOT EXISTS daemon_lease (
1646
1648
  id TEXT PRIMARY KEY,
@@ -1949,9 +1951,12 @@ class Store {
1949
1951
  const rows = this.db.query("SELECT * FROM loops WHERE name = ? ORDER BY created_at DESC LIMIT 2").all(idOrName);
1950
1952
  if (rows.length === 0)
1951
1953
  throw new LoopNotFoundError(idOrName);
1952
- if (rows.length > 1)
1954
+ if (rows.length === 1)
1955
+ return rowToLoop(rows[0]);
1956
+ const active = this.db.query("SELECT * FROM loops WHERE name = ? AND archived_at IS NULL ORDER BY created_at DESC LIMIT 2").all(idOrName);
1957
+ if (active.length !== 1)
1953
1958
  throw new AmbiguousNameError(idOrName);
1954
- return rowToLoop(rows[0]);
1959
+ return rowToLoop(active[0]);
1955
1960
  }
1956
1961
  requireLoop(idOrName) {
1957
1962
  return this.getLoop(idOrName) ?? this.findLoopByName(idOrName) ?? (() => {
@@ -2292,6 +2297,7 @@ class Store {
2292
2297
  return this.transact(() => {
2293
2298
  const loop = this.requireLoop(idOrName);
2294
2299
  this.setWorkflowWorkItemsForLoop(loop.id, "cancelled", "loop deleted", nowIso());
2300
+ this.db.query("DELETE FROM loop_runs WHERE loop_id = ?").run(loop.id);
2295
2301
  const res = this.db.query("DELETE FROM loops WHERE id = ?").run(loop.id);
2296
2302
  return res.changes > 0;
2297
2303
  });
@@ -2776,7 +2782,10 @@ class Store {
2776
2782
  return rowToGoal(row);
2777
2783
  }
2778
2784
  if (context.sourceType && context.sourceId) {
2779
- const row = this.db.query("SELECT * FROM goals WHERE source_type = ? AND source_id = ? ORDER BY created_at DESC LIMIT 1").get(context.sourceType, context.sourceId);
2785
+ const terminalPlaceholders = GOAL_TERMINAL.map(() => "?").join(", ");
2786
+ const row = this.db.query(`SELECT * FROM goals
2787
+ WHERE source_type = ? AND source_id = ? AND status NOT IN (${terminalPlaceholders})
2788
+ ORDER BY created_at DESC LIMIT 1`).get(context.sourceType, context.sourceId, ...GOAL_TERMINAL);
2780
2789
  if (row)
2781
2790
  return rowToGoal(row);
2782
2791
  }
@@ -3196,6 +3205,41 @@ class Store {
3196
3205
  throw new Error(`workflow step run not found after pid update: ${workflowRunId}/${stepId}`);
3197
3206
  return run;
3198
3207
  }
3208
+ recordWorkflowStepProgress(workflowRunId, stepId, progress, opts = {}) {
3209
+ const now = (opts.now ?? new Date).toISOString();
3210
+ this.db.exec("BEGIN IMMEDIATE");
3211
+ try {
3212
+ const res = this.db.query(`UPDATE workflow_step_runs
3213
+ SET stdout=COALESCE($stdout, stdout),
3214
+ stderr=COALESCE($stderr, stderr),
3215
+ updated_at=$updated
3216
+ WHERE workflow_run_id=$workflowRunId AND step_id=$stepId AND status='running'
3217
+ AND ($daemonLeaseId IS NULL OR EXISTS (
3218
+ SELECT 1 FROM daemon_lease WHERE id=$daemonLeaseId AND expires_at > $now
3219
+ ))`).run({
3220
+ $workflowRunId: workflowRunId,
3221
+ $stepId: stepId,
3222
+ $stdout: progress.stdout === undefined ? null : scrubbedOrNull(progress.stdout),
3223
+ $stderr: progress.stderr === undefined ? null : scrubbedOrNull(progress.stderr),
3224
+ $updated: now,
3225
+ $daemonLeaseId: opts.daemonLeaseId ?? null,
3226
+ $now: now
3227
+ });
3228
+ if (res.changes === 1) {
3229
+ this.appendWorkflowEvent(workflowRunId, "step_progress", stepId, progress.payload);
3230
+ }
3231
+ this.db.exec("COMMIT");
3232
+ } catch (error) {
3233
+ try {
3234
+ this.db.exec("ROLLBACK");
3235
+ } catch {}
3236
+ throw error;
3237
+ }
3238
+ const run = this.getWorkflowStepRun(workflowRunId, stepId);
3239
+ if (!run)
3240
+ throw new Error(`workflow step run not found after progress update: ${workflowRunId}/${stepId}`);
3241
+ return run;
3242
+ }
3199
3243
  recoverWorkflowRun(workflowRunId, reason = "workflow run recovered for retry") {
3200
3244
  return this.transact(() => {
3201
3245
  const now = nowIso();
@@ -3633,7 +3677,8 @@ class Store {
3633
3677
  AND ($daemonLeaseId IS NULL OR EXISTS (
3634
3678
  SELECT 1 FROM daemon_lease WHERE id=$daemonLeaseId AND expires_at > $now
3635
3679
  ))`).run(params) : this.db.query(`UPDATE loop_runs SET status=$status, finished_at=$finished, claim_token=NULL, lease_expires_at=NULL, pid=$pid, exit_code=$exitCode,
3636
- duration_ms=$durationMs, stdout=$stdout, stderr=$stderr, error=$error, updated_at=$updated WHERE id=$id`).run(params);
3680
+ duration_ms=$durationMs, stdout=$stdout, stderr=$stderr, error=$error, updated_at=$updated
3681
+ WHERE id=$id AND status='running'`).run(params);
3637
3682
  const run = this.getRun(id);
3638
3683
  if (!run)
3639
3684
  throw new Error(`run not found after finalize: ${id}`);
@@ -4161,6 +4206,12 @@ class Store {
4161
4206
  }
4162
4207
  close() {
4163
4208
  this.db.close();
4209
+ if (this.memoryRootDir) {
4210
+ try {
4211
+ rmSync2(this.memoryRootDir, { recursive: true, force: true });
4212
+ } catch {}
4213
+ this.memoryRootDir = undefined;
4214
+ }
4164
4215
  }
4165
4216
  }
4166
4217
  export {
@@ -37,6 +37,12 @@ export declare function roleFragment(role: string, flow: BuiltinFlow): string;
37
37
  * that rendering is contract-tested, so composition must preserve both shapes.
38
38
  */
39
39
  export declare function goalHeaderFragment(goal: string, role: string, flow: BuiltinFlow): string[];
40
+ /**
41
+ * Bounded lifecycle steps must not open native Codewith `/goal` state. The
42
+ * workflow itself already owns durability and sequencing; each agent step only
43
+ * needs a finite role/objective header plus exact task evidence instructions.
44
+ */
45
+ export declare function boundedStepHeaderFragment(objective: string, role: string, flow: BuiltinFlow): string[];
40
46
  export declare function adversarialReviewFragment(inspect: string, focus: string): string;
41
47
  export declare const DEFAULT_VERIFIER_IDLE_TIMEOUT_MS: number;
42
48
  export declare function verifierIdleTimeoutMs(input: {
@@ -9,7 +9,7 @@ export interface LoopsMcpToolMetadata {
9
9
  guarded?: boolean;
10
10
  requiresEnv?: string;
11
11
  }
12
- /** Tool metadata derived from TOOL_REGISTRATIONS; served via loops://tools and `loops mcp list-tools`. */
12
+ /** Tool metadata derived from TOOL_REGISTRATIONS; served via loops://tools and the loops-mcp bin's `list-tools` argv. */
13
13
  export declare const LOOPS_MCP_TOOLS: LoopsMcpToolMetadata[];
14
14
  export declare function createLoopsMcpServer(): McpServer;
15
15
  export declare function listToolsForCli(): LoopsMcpToolMetadata[];