@agentswf/engine 0.1.0-beta.5 → 0.1.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/LICENSE +21 -0
- package/README.md +16 -0
- package/package.json +13 -9
- package/src/accounting/format.ts +4 -3
- package/src/accounting/spent.ts +128 -0
- package/src/accounting/summary.ts +40 -32
- package/src/agent-cli/cli.ts +28 -55
- package/src/agent-cli/client.ts +1 -27
- package/src/allowance-command.ts +8 -10
- package/src/attempt-ending.ts +4 -4
- package/src/attempt-output.ts +13 -31
- package/src/attempt-view.ts +33 -32
- package/src/author-typebox.ts +13 -0
- package/src/client/checks.ts +94 -0
- package/src/client/exported.ts +17 -0
- package/src/client/index.ts +122 -0
- package/src/client/locate.ts +34 -0
- package/src/client/published.ts +3 -0
- package/src/client/runs.ts +337 -0
- package/src/command-line.ts +64 -0
- package/src/commands.ts +7 -1
- package/src/control-plane.ts +4 -6
- package/src/coordinator-prompt.ts +4 -3
- package/src/coordinator-socket.ts +27 -28
- package/src/deadlines.ts +6 -40
- package/src/decisions/directory.ts +50 -37
- package/src/decisions/fake.ts +1 -1
- package/src/decisions/openrouter.ts +4 -2
- package/src/decisions/seam.ts +4 -4
- package/src/display-path.ts +3 -3
- package/src/duration.ts +19 -33
- package/src/emit-command.ts +38 -22
- package/src/emit.ts +148 -59
- package/src/event-ids.ts +31 -0
- package/src/files.ts +44 -4
- package/src/hand-over.ts +150 -0
- package/src/help/allowance.txt +4 -3
- package/src/help/emit.txt +11 -7
- package/src/help/inbox.txt +5 -3
- package/src/help/logs.txt +7 -5
- package/src/help/open.txt +29 -0
- package/src/help/reply.txt +6 -8
- package/src/help/run.txt +13 -8
- package/src/help/runs.txt +4 -2
- package/src/help/tick.txt +9 -4
- package/src/herdr-placement.ts +3 -33
- package/src/herdr-run-session.ts +5 -13
- package/src/herdr-workspace-marks.ts +7 -17
- package/src/holder-command.ts +54 -136
- package/src/in-lines.ts +28 -0
- package/src/inbox-command.ts +42 -56
- package/src/index.ts +1 -1
- package/src/init-command.ts +8 -17
- package/src/init-template/hello/workflow.ts.template +3 -2
- package/src/jsonl.ts +10 -6
- package/src/keep-alive.ts +7 -19
- package/src/logs-command.ts +134 -53
- package/src/message-operation.ts +6 -3
- package/src/messaging/messaging.ts +8 -10
- package/src/messaging/text.ts +2 -2
- package/src/open-command.ts +770 -0
- package/src/operation-events.ts +4 -6
- package/src/operation-liveness.ts +19 -4
- package/src/operator-cli.ts +206 -90
- package/src/operator-runtime.ts +24 -2
- package/src/ops.ts +270 -0
- package/src/pane-layout.ts +49 -96
- package/src/progress-view.ts +2 -2
- package/src/project-engine.ts +79 -0
- package/src/projects.ts +13 -2
- package/src/refusals.ts +119 -0
- package/src/result-slots.ts +11 -31
- package/src/result-validation.ts +2 -2
- package/src/run-command.ts +27 -31
- package/src/run-commands.ts +42 -114
- package/src/run-event-log.ts +1 -1
- package/src/run-lines.ts +159 -0
- package/src/run-lock.ts +57 -1
- package/src/run-policy/stage-plan.ts +154 -0
- package/src/run-policy/stages.ts +113 -0
- package/src/run-policy/waits.ts +452 -0
- package/src/run-prepare.ts +165 -41
- package/src/run-read.ts +109 -41
- package/src/run-state.ts +170 -50
- package/src/run-text.ts +54 -0
- package/src/run-unreachable.ts +18 -0
- package/src/run-usage.ts +17 -11
- package/src/run-view.ts +73 -14
- package/src/runs-command.ts +81 -63
- package/src/runs.ts +284 -222
- package/src/sandbox-homes.ts +65 -13
- package/src/sandboxes.ts +182 -28
- package/src/session-lines.ts +93 -0
- package/src/skills/run-skills.ts +8 -1
- package/src/sources.ts +2 -39
- package/src/spend.ts +333 -0
- package/src/stage-ledger.ts +111 -153
- package/src/stop-crashed.ts +6 -5
- package/src/stopped.ts +4 -4
- package/src/test-command.ts +37 -19
- package/src/testing/operation-events.ts +3 -23
- package/src/testing.ts +66 -18
- package/src/tick-command.ts +145 -15
- package/src/tools.ts +11 -30
- package/src/turn-ends.ts +37 -0
- package/src/waits.ts +651 -0
- package/src/wake.ts +84 -54
- package/src/workflow-loader.ts +49 -2
- package/src/workflow-runner.ts +525 -618
- package/src/workflow-test-preload.ts +4 -1
- package/src/workflow-testing/coordinators.ts +1 -1
- package/src/workflow-testing/fake-client.ts +387 -0
- package/src/workflow-testing/host.ts +28 -29
- package/src/workflow-testing/index.ts +9 -27
- package/src/workflow-testing/run-test.ts +201 -286
- package/src/workflow-testing/script.ts +7 -39
- package/src/deprecations.ts +0 -177
- package/src/hold-run-lock.fixture.ts +0 -8
- package/src/mailbox.ts +0 -972
- package/src/stage-plan.ts +0 -186
- package/src/workflow-testing/coordinator.typecheck.ts +0 -54
- package/src/workflow-testing/workflow-testing.typecheck.ts +0 -67
package/src/run-prepare.ts
CHANGED
|
@@ -1,13 +1,24 @@
|
|
|
1
1
|
import { statSync } from "node:fs";
|
|
2
|
+
import { unlink } from "node:fs/promises";
|
|
2
3
|
import { homedir } from "node:os";
|
|
3
4
|
import { join } from "node:path";
|
|
4
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
type ClaimedAttempt,
|
|
7
|
+
type RefusalCode,
|
|
8
|
+
RUN_EVENT_VERSIONS,
|
|
9
|
+
type RunPanes,
|
|
10
|
+
} from "@agentswf/contract/records";
|
|
5
11
|
import { isEventWorkflow, type JsonValue } from "@agentswf/contract/workflow";
|
|
6
12
|
import { refusalMessage } from "./attempt-view";
|
|
7
13
|
import { messageOf } from "./errors";
|
|
8
14
|
import { exists } from "./files";
|
|
9
|
-
import {
|
|
15
|
+
import { type EarlierOps, listOps } from "./ops";
|
|
16
|
+
import { EngineRefused } from "./project-engine";
|
|
17
|
+
import { projectOf, projectRunRoot, recordProject } from "./projects";
|
|
10
18
|
import type { RunCommand } from "./run-command";
|
|
19
|
+
import { claimRunLines, type RunLines, readRunLines } from "./run-lines";
|
|
20
|
+
import { crashedRedo } from "./run-policy/stages";
|
|
21
|
+
import { isSystemError, RunUnreachable } from "./run-unreachable";
|
|
11
22
|
import {
|
|
12
23
|
type Attempt,
|
|
13
24
|
appendEvent,
|
|
@@ -15,15 +26,16 @@ import {
|
|
|
15
26
|
checkContinue,
|
|
16
27
|
checkFree,
|
|
17
28
|
claimAttempt,
|
|
18
|
-
|
|
29
|
+
endLogLine,
|
|
19
30
|
generateId,
|
|
20
31
|
idProblem,
|
|
32
|
+
launchLog,
|
|
33
|
+
openedRefusal,
|
|
21
34
|
openRun,
|
|
22
35
|
previousGone,
|
|
23
36
|
type Run,
|
|
24
37
|
RunRefused,
|
|
25
38
|
readAttempts,
|
|
26
|
-
readStageRecords,
|
|
27
39
|
readTurns,
|
|
28
40
|
} from "./runs";
|
|
29
41
|
import {
|
|
@@ -38,7 +50,12 @@ export type PreparedRun = {
|
|
|
38
50
|
/** A new run's id, from `--id` or the workflow's `id(args)`; absent for one to generate. */
|
|
39
51
|
id?: string;
|
|
40
52
|
/** The run `--continue` names, its earlier attempts, and what to warn of before it goes on. */
|
|
41
|
-
continued?: { run: Run; attempts:
|
|
53
|
+
continued?: { run: Run; attempts: ClaimedAttempt[]; warnings: string[] };
|
|
54
|
+
/**
|
|
55
|
+
* The `--from-stage` a continue goes on with though none was given: that of the attempt before
|
|
56
|
+
* it, which crashed inside its start stage.
|
|
57
|
+
*/
|
|
58
|
+
resumes?: string;
|
|
42
59
|
};
|
|
43
60
|
|
|
44
61
|
/** `awf run --id` of a workflow with events, which starts the run detached, or wakes it. */
|
|
@@ -59,25 +76,26 @@ export async function loadAndPrepare(
|
|
|
59
76
|
home: string = homedir(),
|
|
60
77
|
): Promise<
|
|
61
78
|
| { loaded: LoadedWorkflow; prepared: PreparedRun; command: RunCommand }
|
|
62
|
-
| { refused: string }
|
|
79
|
+
| { refused: string; code: RefusalCode }
|
|
63
80
|
| { opens: OpensEventRun }
|
|
64
81
|
> {
|
|
65
82
|
let loaded: LoadedWorkflow;
|
|
66
83
|
try {
|
|
67
|
-
loaded = await loadWorkflowFile(given.workflowFile, given.shellCwd);
|
|
84
|
+
loaded = await loadWorkflowFile(given.workflowFile, given.shellCwd, home);
|
|
68
85
|
} catch (error) {
|
|
69
|
-
|
|
86
|
+
const code = error instanceof EngineRefused ? error.code : "file";
|
|
87
|
+
return { refused: `load: ${messageOf(error)}`, code };
|
|
70
88
|
}
|
|
71
89
|
let command = given;
|
|
72
90
|
if (isEventWorkflow(loaded.workflow)) {
|
|
73
91
|
const refused = eventRunRefusal(command);
|
|
74
|
-
if (refused) return
|
|
92
|
+
if (refused) return refused;
|
|
75
93
|
// An event run lives and works in its file's project, whoever starts or continues it (Q15).
|
|
76
94
|
let project: string;
|
|
77
95
|
try {
|
|
78
96
|
project = await projectOf(loaded.file, home);
|
|
79
97
|
} catch (error) {
|
|
80
|
-
return { refused: messageOf(error) };
|
|
98
|
+
return { refused: messageOf(error), code: "project" };
|
|
81
99
|
}
|
|
82
100
|
if (!command.runRootGiven) command = { ...command, runRoot: projectRunRoot(project) };
|
|
83
101
|
if (command.continueId === undefined) {
|
|
@@ -85,7 +103,7 @@ export async function loadAndPrepare(
|
|
|
85
103
|
try {
|
|
86
104
|
const id =
|
|
87
105
|
command.id ??
|
|
88
|
-
derivedId(loaded.workflow,
|
|
106
|
+
derivedId(loaded.workflow, refusedArgs(loaded.workflow, command.workflowArgs, project)) ??
|
|
89
107
|
// Without starts no sender names its runs, so one may be generated, as a plain run's is.
|
|
90
108
|
(loaded.workflow.starts === undefined
|
|
91
109
|
? await unusedId(command.runRoot, loaded.workflow.meta.name, now)
|
|
@@ -93,6 +111,12 @@ export async function loadAndPrepare(
|
|
|
93
111
|
if (id === undefined)
|
|
94
112
|
return {
|
|
95
113
|
refused: `a run of ${loaded.file}, which starts on events, is named by its id, which --id or the workflow's id(args) gives: awf run --id {id} starts one, awf emit --target ${loaded.file} --id {id} {event} {json} starts one with an event`,
|
|
114
|
+
code: "no-id",
|
|
115
|
+
};
|
|
116
|
+
if (command.json)
|
|
117
|
+
return {
|
|
118
|
+
refused: `a run of ${loaded.file}, which starts on events, opens detached and prints no document: leave out --json, or start it with an event with awf emit --json`,
|
|
119
|
+
code: "usage",
|
|
96
120
|
};
|
|
97
121
|
return {
|
|
98
122
|
opens: {
|
|
@@ -103,14 +127,57 @@ export async function loadAndPrepare(
|
|
|
103
127
|
},
|
|
104
128
|
};
|
|
105
129
|
} catch (error) {
|
|
106
|
-
return
|
|
130
|
+
return prepareRefused(error);
|
|
107
131
|
}
|
|
108
132
|
}
|
|
109
133
|
}
|
|
134
|
+
if (!isEventWorkflow(loaded.workflow) && loaded.workflow.file !== undefined) {
|
|
135
|
+
// A plain workflow that gives `file` keeps its runs in its project, registered, so a run a
|
|
136
|
+
// program started is continued from anywhere (ADR 0016); only the run root moves.
|
|
137
|
+
let project: string;
|
|
138
|
+
try {
|
|
139
|
+
project = await projectOf(loaded.file, home);
|
|
140
|
+
} catch (error) {
|
|
141
|
+
return { refused: messageOf(error), code: "project" };
|
|
142
|
+
}
|
|
143
|
+
if (!command.runRootGiven) command = { ...command, runRoot: projectRunRoot(project) };
|
|
144
|
+
try {
|
|
145
|
+
if (command.continueId === undefined && !command.runRootGiven)
|
|
146
|
+
await recordProject(home, project);
|
|
147
|
+
} catch (error) {
|
|
148
|
+
return { refused: messageOf(error), code: "internal-error" };
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
try {
|
|
152
|
+
const prepared = await prepareRun(command, loaded, now);
|
|
153
|
+
const { resumes } = prepared;
|
|
154
|
+
return {
|
|
155
|
+
loaded,
|
|
156
|
+
prepared,
|
|
157
|
+
command: resumes === undefined ? command : { ...command, fromStage: resumes },
|
|
158
|
+
};
|
|
159
|
+
} catch (error) {
|
|
160
|
+
return prepareRefused(error);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** A refusal while preparing: the run's own, or the workflow's `prepare` refusing its argv. */
|
|
165
|
+
function prepareRefused(error: unknown): { refused: string; code: RefusalCode } {
|
|
166
|
+
// Preparing reads the run, when it is continued, and writes nothing: `readFailureCode`'s reading.
|
|
167
|
+
if (error instanceof RunRefused || error instanceof RunUnreachable)
|
|
168
|
+
return { refused: messageOf(error), code: error.code };
|
|
169
|
+
return {
|
|
170
|
+
refused: messageOf(error),
|
|
171
|
+
code: isSystemError(error) ? "unreachable" : "internal-error",
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/** `prepareArgs`, its refusal of the argv a `RunRefused`, so a failure of awf's isn't one. */
|
|
176
|
+
function refusedArgs(workflow: AnyWorkflow, argv: readonly string[], cwd: string): JsonValue {
|
|
110
177
|
try {
|
|
111
|
-
return
|
|
178
|
+
return prepareArgs(workflow, argv, cwd);
|
|
112
179
|
} catch (error) {
|
|
113
|
-
|
|
180
|
+
throw new RunRefused(`prepare: ${messageOf(error)}`, "prepare");
|
|
114
181
|
}
|
|
115
182
|
}
|
|
116
183
|
|
|
@@ -125,7 +192,7 @@ async function prepareRun(
|
|
|
125
192
|
): Promise<PreparedRun> {
|
|
126
193
|
const { workflow, file } = loaded;
|
|
127
194
|
const { meta } = workflow;
|
|
128
|
-
const prepare = (argv: readonly string[], cwd: string) =>
|
|
195
|
+
const prepare = (argv: readonly string[], cwd: string) => refusedArgs(workflow, argv, cwd);
|
|
129
196
|
if (command.continueId === undefined) {
|
|
130
197
|
const args = prepare(command.workflowArgs, command.cwd);
|
|
131
198
|
const id = command.id ?? derivedId(workflow, args);
|
|
@@ -141,30 +208,51 @@ async function prepareRun(
|
|
|
141
208
|
});
|
|
142
209
|
const { id, argv, cwd } = run.record;
|
|
143
210
|
if (!statSync(cwd, { throwIfNoEntry: false })?.isDirectory()) {
|
|
144
|
-
throw new RunRefused(`${id} works in ${cwd}, which is no longer a directory
|
|
211
|
+
throw new RunRefused(`${id} works in ${cwd}, which is no longer a directory`, "not-started");
|
|
145
212
|
}
|
|
146
213
|
let args: JsonValue;
|
|
147
214
|
try {
|
|
148
|
-
args
|
|
215
|
+
// A run a program started runs with the args it was given, never prepared (ADR 0016).
|
|
216
|
+
args = run.record.args !== undefined ? run.record.args : prepareArgs(workflow, argv, cwd);
|
|
149
217
|
} catch (error) {
|
|
150
218
|
throw new RunRefused(
|
|
151
219
|
`the recorded argv of ${id} no longer parses with ${file}: ${messageOf(error)}; start a new run`,
|
|
220
|
+
"prepare",
|
|
152
221
|
);
|
|
153
222
|
}
|
|
154
223
|
const attempts = await readAttempts(run);
|
|
155
|
-
const
|
|
224
|
+
const { view } = await readRunLines(run.dir);
|
|
156
225
|
// The claim refuses it too; this says so before `--here` opens a tab, or `--session` waits.
|
|
157
|
-
const refused =
|
|
158
|
-
|
|
159
|
-
|
|
226
|
+
const refused =
|
|
227
|
+
attemptRefusal(attempts, view, { fromStage: command.fromStage !== undefined }) ??
|
|
228
|
+
(await openedRefusal(run));
|
|
229
|
+
if (refused) throw new RunRefused(refusalMessage(id, refused, now), "not-started");
|
|
230
|
+
// A plain run's redo that crashed inside its stage wrote no line, so nothing says it: it goes on
|
|
231
|
+
// from there, as 018 has a crash mid-stage do. A wake carries no flag, so an event run's is lost.
|
|
232
|
+
const resumes =
|
|
233
|
+
command.fromStage === undefined && !isEventWorkflow(workflow)
|
|
234
|
+
? crashedRedo(attempts, view)
|
|
235
|
+
: undefined;
|
|
236
|
+
const fromStage = command.fromStage ?? resumes;
|
|
160
237
|
// Once per start point: a run started at a stage goes on to it attempt after attempt.
|
|
161
238
|
const unrecorded =
|
|
162
239
|
fromStage !== undefined &&
|
|
163
|
-
|
|
240
|
+
view.stages[fromStage] === undefined &&
|
|
164
241
|
attempts.at(-1)?.flags.fromStage !== fromStage
|
|
165
242
|
? [`awf: nothing is recorded for ${fromStage}; the attempt stops if it never reaches it`]
|
|
166
243
|
: [];
|
|
167
|
-
|
|
244
|
+
const warnings =
|
|
245
|
+
resumes === undefined
|
|
246
|
+
? unrecorded
|
|
247
|
+
: [
|
|
248
|
+
`awf: attempt ${attempts.at(-1)?.attempt} of ${id} ended without an ending inside --from-stage ${resumes}; this attempt starts there again`,
|
|
249
|
+
...unrecorded,
|
|
250
|
+
];
|
|
251
|
+
return {
|
|
252
|
+
args,
|
|
253
|
+
continued: { run, attempts, warnings },
|
|
254
|
+
...(resumes === undefined ? {} : { resumes }),
|
|
255
|
+
};
|
|
168
256
|
}
|
|
169
257
|
|
|
170
258
|
/** The workflow's args from `argv`, as its `prepare` makes them, checked to be JSON. */
|
|
@@ -186,23 +274,30 @@ async function unusedId(runRoot: string, workflow: string, now: number): Promise
|
|
|
186
274
|
for (let tries = 1; ; tries += 1) {
|
|
187
275
|
const id = generateId(new Date(now));
|
|
188
276
|
if (!(await exists(join(runRoot, workflow, id)))) return id;
|
|
189
|
-
if (tries >= 5) throw new RunRefused("every id generated was taken; --id gives one");
|
|
277
|
+
if (tries >= 5) throw new RunRefused("every id generated was taken; --id gives one", "id");
|
|
190
278
|
}
|
|
191
279
|
}
|
|
192
280
|
|
|
193
281
|
/**
|
|
194
282
|
* Why `awf run` doesn't take a workflow with `events` as asked: every attempt after the first is
|
|
195
283
|
* started by a wake, which passes on none of the flags a run would have to keep. A `--from-stage`
|
|
196
|
-
* is the one attempt's, and its
|
|
284
|
+
* is the one attempt's, and its stage line keeps what it redid.
|
|
197
285
|
*/
|
|
198
|
-
function eventRunRefusal(command: RunCommand): string | undefined {
|
|
286
|
+
function eventRunRefusal(command: RunCommand): { refused: string; code: RefusalCode } | undefined {
|
|
287
|
+
const usage = (refused: string) => ({ refused, code: "usage" as const });
|
|
199
288
|
if (command.values !== undefined)
|
|
200
|
-
return
|
|
289
|
+
return usage(
|
|
290
|
+
"a run of a workflow with events takes no --values: before --from-stage, a stage with no record runs",
|
|
291
|
+
);
|
|
201
292
|
if (command.fromStage !== undefined && command.continueId === undefined)
|
|
202
|
-
return "a run of a workflow with events takes --from-stage only with --continue";
|
|
293
|
+
return usage("a run of a workflow with events takes --from-stage only with --continue");
|
|
203
294
|
if (command.continueId !== undefined) {
|
|
295
|
+
// Refused as a plain run's continue with arguments is.
|
|
204
296
|
if (command.workflowArgs.length > 0)
|
|
205
|
-
return
|
|
297
|
+
return {
|
|
298
|
+
refused: "a continue takes no arguments: it goes on with those its run was started with",
|
|
299
|
+
code: "other-args",
|
|
300
|
+
};
|
|
206
301
|
return undefined;
|
|
207
302
|
}
|
|
208
303
|
const unkept = [
|
|
@@ -212,7 +307,9 @@ function eventRunRefusal(command: RunCommand): string | undefined {
|
|
|
212
307
|
command.runRootGiven ? "--run-root" : undefined,
|
|
213
308
|
].filter((flag) => flag !== undefined);
|
|
214
309
|
if (unkept.length > 0)
|
|
215
|
-
return
|
|
310
|
+
return usage(
|
|
311
|
+
`a run of a workflow with events runs in its file's project, as every wake starts it: it takes no ${unkept.join(", ")}`,
|
|
312
|
+
);
|
|
216
313
|
return undefined;
|
|
217
314
|
}
|
|
218
315
|
|
|
@@ -223,23 +320,32 @@ function derivedId(workflow: AnyWorkflow, args: JsonValue): string | undefined {
|
|
|
223
320
|
try {
|
|
224
321
|
id = workflow.id(args);
|
|
225
322
|
} catch (error) {
|
|
226
|
-
throw new RunRefused(
|
|
323
|
+
throw new RunRefused(
|
|
324
|
+
`the workflow's id(args) failed: ${messageOf(error)}; --id gives one`,
|
|
325
|
+
"prepare",
|
|
326
|
+
);
|
|
227
327
|
}
|
|
228
328
|
const problem =
|
|
229
329
|
typeof id === "string" ? idProblem(id) : `it returned a ${typeof id}, not a string`;
|
|
230
|
-
if (problem) throw new RunRefused(`the workflow's id(args): ${problem}; --id gives one
|
|
330
|
+
if (problem) throw new RunRefused(`the workflow's id(args): ${problem}; --id gives one`, "id");
|
|
231
331
|
return id as string;
|
|
232
332
|
}
|
|
233
333
|
|
|
234
|
-
/**
|
|
334
|
+
/**
|
|
335
|
+
* Claims the run's next attempt, naming each earlier one found interrupted. `launch`: the
|
|
336
|
+
* background launch that started this process, recorded only when it is this run's, its
|
|
337
|
+
* `launches/{launch}.log` there.
|
|
338
|
+
*/
|
|
235
339
|
export async function claimNext(
|
|
236
340
|
run: Run,
|
|
237
341
|
loaded: LoadedWorkflow,
|
|
238
342
|
command: RunCommand,
|
|
239
343
|
notice: (text: string) => void,
|
|
240
344
|
now: number,
|
|
345
|
+
launch?: string,
|
|
241
346
|
): Promise<Attempt> {
|
|
242
347
|
const { meta } = loaded.workflow;
|
|
348
|
+
const ours = launch !== undefined && (await exists(launchLog(run.dir, launch)));
|
|
243
349
|
const claimed = await claimAttempt(
|
|
244
350
|
run,
|
|
245
351
|
{
|
|
@@ -249,23 +355,41 @@ export async function claimNext(
|
|
|
249
355
|
...(command.timeout === undefined ? {} : { timeout: command.timeout }),
|
|
250
356
|
...(command.fromStage === undefined ? {} : { fromStage: command.fromStage }),
|
|
251
357
|
},
|
|
358
|
+
...(ours ? { launch } : {}),
|
|
252
359
|
},
|
|
253
360
|
{ fromStage: command.fromStage !== undefined },
|
|
254
361
|
);
|
|
255
362
|
if ("refused" in claimed) {
|
|
256
|
-
throw new RunRefused(refusalMessage(run.record.id, claimed.refused, now));
|
|
363
|
+
throw new RunRefused(refusalMessage(run.record.id, claimed.refused, now), "not-started");
|
|
364
|
+
}
|
|
365
|
+
// Claimed first, which `wake` waits for holding the run's lock, then waited on before this
|
|
366
|
+
// attempt writes anything, so the attempt before it has finished its own records and two never
|
|
367
|
+
// share an agent's session.
|
|
368
|
+
const n = claimed.attempt.record.attempt;
|
|
369
|
+
await previousGone(run, n, notice);
|
|
370
|
+
// Numbered on from what earlier attempts left, now that none still writes: an id is never reused.
|
|
371
|
+
let ops: EarlierOps;
|
|
372
|
+
try {
|
|
373
|
+
ops = await listOps(run.dir);
|
|
374
|
+
} catch (error) {
|
|
375
|
+
await unlink(claimed.attempt.file).catch(() => undefined);
|
|
376
|
+
throw new Error(`ops/ was not listed: ${messageOf(error)}`, { cause: error });
|
|
377
|
+
}
|
|
378
|
+
let lines: RunLines;
|
|
379
|
+
try {
|
|
380
|
+
lines = await claimRunLines(run.dir, n, claimed.attempt.record.started);
|
|
381
|
+
} catch (error) {
|
|
382
|
+
// Without its fence the attempt may not write: it goes, as a refused claim's does.
|
|
383
|
+
await unlink(claimed.attempt.file).catch(() => undefined);
|
|
384
|
+
throw new Error(`run.jsonl was not claimed: ${messageOf(error)}`, { cause: error });
|
|
257
385
|
}
|
|
258
|
-
// An event run's: claimed first, which `wake` waits for holding the run's lock, then waited on
|
|
259
|
-
// before this attempt writes anything, so the attempt before it has finished its own records and
|
|
260
|
-
// two never share an agent's session.
|
|
261
|
-
if (run.record.events) await previousGone(run, claimed.attempt.record.attempt, notice);
|
|
262
386
|
// A crash may have torn the last line an earlier attempt appended; this attempt's first line
|
|
263
387
|
// must not be glued to it.
|
|
264
|
-
await
|
|
388
|
+
await endLogLine(run.dir)
|
|
265
389
|
.then(() =>
|
|
266
390
|
appendEvent(run.dir, {
|
|
267
391
|
version: RUN_EVENT_VERSIONS["attempt-started"],
|
|
268
|
-
attempt:
|
|
392
|
+
attempt: n,
|
|
269
393
|
at: claimed.attempt.record.started,
|
|
270
394
|
kind: "attempt-started",
|
|
271
395
|
}),
|
|
@@ -279,7 +403,7 @@ export async function claimNext(
|
|
|
279
403
|
`awf: attempt ${earlier.attempt} of ${run.record.id} was interrupted${stage ? ` in ${stage}` : ""}; its panes may still be open in Herdr workspace "${workspaceLabel(meta.name, run.record.id, earlier.attempt)}"`,
|
|
280
404
|
);
|
|
281
405
|
}
|
|
282
|
-
return claimed.attempt;
|
|
406
|
+
return { ...claimed.attempt, lines, ops };
|
|
283
407
|
}
|
|
284
408
|
|
|
285
409
|
/** A run's Herdr workspace, which names the attempt: a dead one's may still be open. */
|