@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.
- package/CHANGELOG.md +39 -0
- package/README.md +2 -2
- package/dist/api/index.js +19 -10
- package/dist/cli/index.js +368 -193
- package/dist/daemon/index.js +317 -163
- package/dist/index.js +339 -175
- package/dist/lib/executor.d.ts +14 -1
- package/dist/lib/mode.js +1 -1
- package/dist/lib/storage/index.js +56 -5
- package/dist/lib/storage/sqlite.js +56 -5
- package/dist/lib/store.d.ts +7 -0
- package/dist/lib/store.js +56 -5
- package/dist/lib/template-kit.d.ts +6 -0
- package/dist/mcp/index.d.ts +1 -1
- package/dist/mcp/index.js +321 -166
- package/dist/runner/index.d.ts +3 -1
- package/dist/runner/index.js +130 -11
- package/dist/sdk/index.js +320 -167
- package/docs/USAGE.md +2 -2
- package/package.json +1 -1
package/dist/lib/executor.d.ts
CHANGED
|
@@ -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
|
@@ -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
|
|
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(
|
|
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
|
|
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
|
|
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
|
|
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(
|
|
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
|
|
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
|
|
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
|
|
package/dist/lib/store.d.ts
CHANGED
|
@@ -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
|
|
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(
|
|
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
|
|
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
|
|
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: {
|
package/dist/mcp/index.d.ts
CHANGED
|
@@ -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
|
|
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[];
|