@agentswf/engine 0.1.0-beta.4 → 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 +35 -22
- package/src/agent-cli/client.ts +1 -27
- package/src/allowance-command.ts +8 -10
- package/src/attempt-ending.ts +5 -7
- package/src/attempt-output.ts +15 -32
- package/src/attempt-view.ts +34 -33
- 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 +41 -11
- package/src/coordinator-prompt.ts +4 -3
- package/src/coordinator-socket.ts +41 -33
- package/src/deadlines.ts +40 -7
- package/src/decisions/directory.ts +51 -40
- 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 +43 -27
- package/src/emit-command.ts +88 -58
- package/src/emit.ts +254 -56
- 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 +18 -6
- package/src/help/inbox.txt +6 -4
- package/src/help/logs.txt +9 -6
- package/src/help/open.txt +29 -0
- package/src/help/reply.txt +6 -8
- package/src/help/run.txt +30 -11
- 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 +7 -15
- package/src/herdr-workspace-marks.ts +7 -17
- package/src/holder-command.ts +56 -133
- package/src/in-lines.ts +28 -0
- package/src/inbox-command.ts +40 -57
- package/src/index.ts +1 -1
- package/src/init-command.ts +8 -17
- package/src/init-template/hello/workflow.test.ts.template +3 -3
- package/src/init-template/hello/workflow.ts.template +5 -4
- package/src/jsonl.ts +10 -6
- package/src/keep-alive.ts +15 -12
- package/src/logs-command.ts +128 -47
- package/src/message-operation.ts +6 -3
- package/src/messaging/limits.ts +4 -4
- package/src/messaging/messaging.ts +31 -28
- package/src/messaging/routes.ts +1 -1
- package/src/messaging/text.ts +9 -7
- 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 +224 -94
- package/src/operator-runtime.ts +29 -7
- package/src/ops.ts +270 -0
- package/src/pane-layout.ts +105 -59
- package/src/progress-view.ts +10 -10
- 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 +45 -28
- package/src/run-commands.ts +43 -115
- package/src/run-event-log.ts +1 -1
- package/src/run-lines.ts +159 -0
- package/src/run-lock.ts +57 -1
- package/src/{stage-plan.ts → run-policy/stage-plan.ts} +46 -42
- package/src/run-policy/stages.ts +113 -0
- package/src/run-policy/waits.ts +452 -0
- package/src/run-prepare.ts +205 -43
- 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 +18 -12
- package/src/run-view.ts +81 -17
- package/src/runs-command.ts +83 -63
- package/src/runs.ts +310 -212
- 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 +28 -29
- package/src/spend.ts +333 -0
- package/src/stage-ledger.ts +119 -131
- package/src/stop-crashed.ts +6 -5
- package/src/stopped.ts +4 -4
- package/src/test-command.ts +36 -14
- package/src/testing/operation-events.ts +3 -23
- package/src/testing.ts +73 -26
- package/src/tick-command.ts +145 -15
- package/src/tools.ts +25 -16
- package/src/turn-ends.ts +37 -0
- package/src/waits.ts +651 -0
- package/src/wake.ts +84 -54
- package/src/workflow-loader.ts +54 -4
- package/src/workflow-runner.ts +805 -580
- package/src/workflow-test-preload.ts +4 -1
- package/src/workflow-testing/coordinators.ts +3 -3
- package/src/workflow-testing/fake-client.ts +387 -0
- package/src/workflow-testing/host.ts +121 -53
- package/src/workflow-testing/index.ts +33 -842
- package/src/workflow-testing/run-test.ts +1128 -0
- package/src/workflow-testing/script.ts +74 -45
- package/src/hold-run-lock.fixture.ts +0 -8
- package/src/mailbox.ts +0 -990
- package/src/workflow-testing/coordinator.typecheck.ts +0 -54
- package/src/workflow-testing/workflow-testing.typecheck.ts +0 -64
package/src/run-prepare.ts
CHANGED
|
@@ -1,11 +1,24 @@
|
|
|
1
1
|
import { statSync } from "node:fs";
|
|
2
|
+
import { unlink } from "node:fs/promises";
|
|
2
3
|
import { homedir } from "node:os";
|
|
3
|
-
import {
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import {
|
|
6
|
+
type ClaimedAttempt,
|
|
7
|
+
type RefusalCode,
|
|
8
|
+
RUN_EVENT_VERSIONS,
|
|
9
|
+
type RunPanes,
|
|
10
|
+
} from "@agentswf/contract/records";
|
|
4
11
|
import { isEventWorkflow, type JsonValue } from "@agentswf/contract/workflow";
|
|
5
12
|
import { refusalMessage } from "./attempt-view";
|
|
6
13
|
import { messageOf } from "./errors";
|
|
7
|
-
import {
|
|
14
|
+
import { exists } from "./files";
|
|
15
|
+
import { type EarlierOps, listOps } from "./ops";
|
|
16
|
+
import { EngineRefused } from "./project-engine";
|
|
17
|
+
import { projectOf, projectRunRoot, recordProject } from "./projects";
|
|
8
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";
|
|
9
22
|
import {
|
|
10
23
|
type Attempt,
|
|
11
24
|
appendEvent,
|
|
@@ -13,14 +26,16 @@ import {
|
|
|
13
26
|
checkContinue,
|
|
14
27
|
checkFree,
|
|
15
28
|
claimAttempt,
|
|
16
|
-
|
|
29
|
+
endLogLine,
|
|
30
|
+
generateId,
|
|
17
31
|
idProblem,
|
|
32
|
+
launchLog,
|
|
33
|
+
openedRefusal,
|
|
18
34
|
openRun,
|
|
19
35
|
previousGone,
|
|
20
36
|
type Run,
|
|
21
37
|
RunRefused,
|
|
22
38
|
readAttempts,
|
|
23
|
-
readStageRecords,
|
|
24
39
|
readTurns,
|
|
25
40
|
} from "./runs";
|
|
26
41
|
import {
|
|
@@ -35,7 +50,20 @@ export type PreparedRun = {
|
|
|
35
50
|
/** A new run's id, from `--id` or the workflow's `id(args)`; absent for one to generate. */
|
|
36
51
|
id?: string;
|
|
37
52
|
/** The run `--continue` names, its earlier attempts, and what to warn of before it goes on. */
|
|
38
|
-
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;
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
/** `awf run --id` of a workflow with events, which starts the run detached, or wakes it. */
|
|
62
|
+
export type OpensEventRun = {
|
|
63
|
+
loaded: LoadedWorkflow;
|
|
64
|
+
id: string;
|
|
65
|
+
argv: readonly string[];
|
|
66
|
+
panes?: RunPanes;
|
|
39
67
|
};
|
|
40
68
|
|
|
41
69
|
/**
|
|
@@ -48,25 +76,26 @@ export async function loadAndPrepare(
|
|
|
48
76
|
home: string = homedir(),
|
|
49
77
|
): Promise<
|
|
50
78
|
| { loaded: LoadedWorkflow; prepared: PreparedRun; command: RunCommand }
|
|
51
|
-
| { refused: string }
|
|
52
|
-
| { opens:
|
|
79
|
+
| { refused: string; code: RefusalCode }
|
|
80
|
+
| { opens: OpensEventRun }
|
|
53
81
|
> {
|
|
54
82
|
let loaded: LoadedWorkflow;
|
|
55
83
|
try {
|
|
56
|
-
loaded = await loadWorkflowFile(given.workflowFile, given.shellCwd);
|
|
84
|
+
loaded = await loadWorkflowFile(given.workflowFile, given.shellCwd, home);
|
|
57
85
|
} catch (error) {
|
|
58
|
-
|
|
86
|
+
const code = error instanceof EngineRefused ? error.code : "file";
|
|
87
|
+
return { refused: `load: ${messageOf(error)}`, code };
|
|
59
88
|
}
|
|
60
89
|
let command = given;
|
|
61
90
|
if (isEventWorkflow(loaded.workflow)) {
|
|
62
91
|
const refused = eventRunRefusal(command);
|
|
63
|
-
if (refused) return
|
|
92
|
+
if (refused) return refused;
|
|
64
93
|
// An event run lives and works in its file's project, whoever starts or continues it (Q15).
|
|
65
94
|
let project: string;
|
|
66
95
|
try {
|
|
67
96
|
project = await projectOf(loaded.file, home);
|
|
68
97
|
} catch (error) {
|
|
69
|
-
return { refused: messageOf(error) };
|
|
98
|
+
return { refused: messageOf(error), code: "project" };
|
|
70
99
|
}
|
|
71
100
|
if (!command.runRootGiven) command = { ...command, runRoot: projectRunRoot(project) };
|
|
72
101
|
if (command.continueId === undefined) {
|
|
@@ -74,21 +103,81 @@ export async function loadAndPrepare(
|
|
|
74
103
|
try {
|
|
75
104
|
const id =
|
|
76
105
|
command.id ??
|
|
77
|
-
derivedId(loaded.workflow,
|
|
106
|
+
derivedId(loaded.workflow, refusedArgs(loaded.workflow, command.workflowArgs, project)) ??
|
|
107
|
+
// Without starts no sender names its runs, so one may be generated, as a plain run's is.
|
|
108
|
+
(loaded.workflow.starts === undefined
|
|
109
|
+
? await unusedId(command.runRoot, loaded.workflow.meta.name, now)
|
|
110
|
+
: undefined);
|
|
78
111
|
if (id === undefined)
|
|
79
112
|
return {
|
|
80
|
-
refused: `a run of ${loaded.file} 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`,
|
|
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",
|
|
81
120
|
};
|
|
82
|
-
return {
|
|
121
|
+
return {
|
|
122
|
+
opens: {
|
|
123
|
+
loaded,
|
|
124
|
+
id,
|
|
125
|
+
argv: command.workflowArgs,
|
|
126
|
+
...(command.panes ? { panes: command.panes } : {}),
|
|
127
|
+
},
|
|
128
|
+
};
|
|
83
129
|
} catch (error) {
|
|
84
|
-
return
|
|
130
|
+
return prepareRefused(error);
|
|
85
131
|
}
|
|
86
132
|
}
|
|
87
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
|
+
}
|
|
88
151
|
try {
|
|
89
|
-
|
|
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
|
+
};
|
|
90
159
|
} catch (error) {
|
|
91
|
-
return
|
|
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 {
|
|
177
|
+
try {
|
|
178
|
+
return prepareArgs(workflow, argv, cwd);
|
|
179
|
+
} catch (error) {
|
|
180
|
+
throw new RunRefused(`prepare: ${messageOf(error)}`, "prepare");
|
|
92
181
|
}
|
|
93
182
|
}
|
|
94
183
|
|
|
@@ -103,7 +192,7 @@ async function prepareRun(
|
|
|
103
192
|
): Promise<PreparedRun> {
|
|
104
193
|
const { workflow, file } = loaded;
|
|
105
194
|
const { meta } = workflow;
|
|
106
|
-
const prepare = (argv: readonly string[], cwd: string) =>
|
|
195
|
+
const prepare = (argv: readonly string[], cwd: string) => refusedArgs(workflow, argv, cwd);
|
|
107
196
|
if (command.continueId === undefined) {
|
|
108
197
|
const args = prepare(command.workflowArgs, command.cwd);
|
|
109
198
|
const id = command.id ?? derivedId(workflow, args);
|
|
@@ -115,33 +204,55 @@ async function prepareRun(
|
|
|
115
204
|
argv: command.workflowArgs,
|
|
116
205
|
...(command.cwdGiven ? { cwd: command.cwd } : {}),
|
|
117
206
|
...(command.sandbox === undefined ? {} : { sandbox: command.sandbox }),
|
|
207
|
+
...(command.panes === undefined ? {} : { panes: command.panes }),
|
|
118
208
|
});
|
|
119
209
|
const { id, argv, cwd } = run.record;
|
|
120
210
|
if (!statSync(cwd, { throwIfNoEntry: false })?.isDirectory()) {
|
|
121
|
-
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");
|
|
122
212
|
}
|
|
123
213
|
let args: JsonValue;
|
|
124
214
|
try {
|
|
125
|
-
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);
|
|
126
217
|
} catch (error) {
|
|
127
218
|
throw new RunRefused(
|
|
128
219
|
`the recorded argv of ${id} no longer parses with ${file}: ${messageOf(error)}; start a new run`,
|
|
220
|
+
"prepare",
|
|
129
221
|
);
|
|
130
222
|
}
|
|
131
223
|
const attempts = await readAttempts(run);
|
|
132
|
-
const
|
|
224
|
+
const { view } = await readRunLines(run.dir);
|
|
133
225
|
// The claim refuses it too; this says so before `--here` opens a tab, or `--session` waits.
|
|
134
|
-
const refused =
|
|
135
|
-
|
|
136
|
-
|
|
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;
|
|
137
237
|
// Once per start point: a run started at a stage goes on to it attempt after attempt.
|
|
138
238
|
const unrecorded =
|
|
139
239
|
fromStage !== undefined &&
|
|
140
|
-
|
|
240
|
+
view.stages[fromStage] === undefined &&
|
|
141
241
|
attempts.at(-1)?.flags.fromStage !== fromStage
|
|
142
242
|
? [`awf: nothing is recorded for ${fromStage}; the attempt stops if it never reaches it`]
|
|
143
243
|
: [];
|
|
144
|
-
|
|
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
|
+
};
|
|
145
256
|
}
|
|
146
257
|
|
|
147
258
|
/** The workflow's args from `argv`, as its `prepare` makes them, checked to be JSON. */
|
|
@@ -155,16 +266,38 @@ export function prepareArgs(
|
|
|
155
266
|
return args;
|
|
156
267
|
}
|
|
157
268
|
|
|
269
|
+
/**
|
|
270
|
+
* A generated id no run of `workflow` holds: `awf run` of one already there would wake it. Taken
|
|
271
|
+
* between this and the open, by a run started the same minute with the same four digits, it is.
|
|
272
|
+
*/
|
|
273
|
+
async function unusedId(runRoot: string, workflow: string, now: number): Promise<string> {
|
|
274
|
+
for (let tries = 1; ; tries += 1) {
|
|
275
|
+
const id = generateId(new Date(now));
|
|
276
|
+
if (!(await exists(join(runRoot, workflow, id)))) return id;
|
|
277
|
+
if (tries >= 5) throw new RunRefused("every id generated was taken; --id gives one", "id");
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
158
281
|
/**
|
|
159
282
|
* Why `awf run` doesn't take a workflow with `events` as asked: every attempt after the first is
|
|
160
|
-
* started by a wake, which passes on none of the flags a run would have to keep.
|
|
283
|
+
* started by a wake, which passes on none of the flags a run would have to keep. A `--from-stage`
|
|
284
|
+
* is the one attempt's, and its stage line keeps what it redid.
|
|
161
285
|
*/
|
|
162
|
-
function eventRunRefusal(command: RunCommand): string | undefined {
|
|
163
|
-
|
|
164
|
-
|
|
286
|
+
function eventRunRefusal(command: RunCommand): { refused: string; code: RefusalCode } | undefined {
|
|
287
|
+
const usage = (refused: string) => ({ refused, code: "usage" as const });
|
|
288
|
+
if (command.values !== undefined)
|
|
289
|
+
return usage(
|
|
290
|
+
"a run of a workflow with events takes no --values: before --from-stage, a stage with no record runs",
|
|
291
|
+
);
|
|
292
|
+
if (command.fromStage !== undefined && command.continueId === undefined)
|
|
293
|
+
return usage("a run of a workflow with events takes --from-stage only with --continue");
|
|
165
294
|
if (command.continueId !== undefined) {
|
|
295
|
+
// Refused as a plain run's continue with arguments is.
|
|
166
296
|
if (command.workflowArgs.length > 0)
|
|
167
|
-
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
|
+
};
|
|
168
301
|
return undefined;
|
|
169
302
|
}
|
|
170
303
|
const unkept = [
|
|
@@ -174,7 +307,9 @@ function eventRunRefusal(command: RunCommand): string | undefined {
|
|
|
174
307
|
command.runRootGiven ? "--run-root" : undefined,
|
|
175
308
|
].filter((flag) => flag !== undefined);
|
|
176
309
|
if (unkept.length > 0)
|
|
177
|
-
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
|
+
);
|
|
178
313
|
return undefined;
|
|
179
314
|
}
|
|
180
315
|
|
|
@@ -185,23 +320,32 @@ function derivedId(workflow: AnyWorkflow, args: JsonValue): string | undefined {
|
|
|
185
320
|
try {
|
|
186
321
|
id = workflow.id(args);
|
|
187
322
|
} catch (error) {
|
|
188
|
-
throw new RunRefused(
|
|
323
|
+
throw new RunRefused(
|
|
324
|
+
`the workflow's id(args) failed: ${messageOf(error)}; --id gives one`,
|
|
325
|
+
"prepare",
|
|
326
|
+
);
|
|
189
327
|
}
|
|
190
328
|
const problem =
|
|
191
329
|
typeof id === "string" ? idProblem(id) : `it returned a ${typeof id}, not a string`;
|
|
192
|
-
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");
|
|
193
331
|
return id as string;
|
|
194
332
|
}
|
|
195
333
|
|
|
196
|
-
/**
|
|
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
|
+
*/
|
|
197
339
|
export async function claimNext(
|
|
198
340
|
run: Run,
|
|
199
341
|
loaded: LoadedWorkflow,
|
|
200
342
|
command: RunCommand,
|
|
201
343
|
notice: (text: string) => void,
|
|
202
344
|
now: number,
|
|
345
|
+
launch?: string,
|
|
203
346
|
): Promise<Attempt> {
|
|
204
347
|
const { meta } = loaded.workflow;
|
|
348
|
+
const ours = launch !== undefined && (await exists(launchLog(run.dir, launch)));
|
|
205
349
|
const claimed = await claimAttempt(
|
|
206
350
|
run,
|
|
207
351
|
{
|
|
@@ -211,23 +355,41 @@ export async function claimNext(
|
|
|
211
355
|
...(command.timeout === undefined ? {} : { timeout: command.timeout }),
|
|
212
356
|
...(command.fromStage === undefined ? {} : { fromStage: command.fromStage }),
|
|
213
357
|
},
|
|
358
|
+
...(ours ? { launch } : {}),
|
|
214
359
|
},
|
|
215
360
|
{ fromStage: command.fromStage !== undefined },
|
|
216
361
|
);
|
|
217
362
|
if ("refused" in claimed) {
|
|
218
|
-
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 });
|
|
219
385
|
}
|
|
220
|
-
// An event run's: claimed first, which `wake` waits for holding the run's lock, then waited on
|
|
221
|
-
// before this attempt writes anything, so the attempt before it has finished its own records and
|
|
222
|
-
// two never share an agent's session.
|
|
223
|
-
if (run.record.events) await previousGone(run, claimed.attempt.record.attempt, notice);
|
|
224
386
|
// A crash may have torn the last line an earlier attempt appended; this attempt's first line
|
|
225
387
|
// must not be glued to it.
|
|
226
|
-
await
|
|
388
|
+
await endLogLine(run.dir)
|
|
227
389
|
.then(() =>
|
|
228
390
|
appendEvent(run.dir, {
|
|
229
391
|
version: RUN_EVENT_VERSIONS["attempt-started"],
|
|
230
|
-
attempt:
|
|
392
|
+
attempt: n,
|
|
231
393
|
at: claimed.attempt.record.started,
|
|
232
394
|
kind: "attempt-started",
|
|
233
395
|
}),
|
|
@@ -241,7 +403,7 @@ export async function claimNext(
|
|
|
241
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)}"`,
|
|
242
404
|
);
|
|
243
405
|
}
|
|
244
|
-
return claimed.attempt;
|
|
406
|
+
return { ...claimed.attempt, lines, ops };
|
|
245
407
|
}
|
|
246
408
|
|
|
247
409
|
/** A run's Herdr workspace, which names the attempt: a dead one's may still be open. */
|