@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.
Files changed (123) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +16 -0
  3. package/package.json +13 -9
  4. package/src/accounting/format.ts +4 -3
  5. package/src/accounting/spent.ts +128 -0
  6. package/src/accounting/summary.ts +40 -32
  7. package/src/agent-cli/cli.ts +35 -22
  8. package/src/agent-cli/client.ts +1 -27
  9. package/src/allowance-command.ts +8 -10
  10. package/src/attempt-ending.ts +5 -7
  11. package/src/attempt-output.ts +15 -32
  12. package/src/attempt-view.ts +34 -33
  13. package/src/author-typebox.ts +13 -0
  14. package/src/client/checks.ts +94 -0
  15. package/src/client/exported.ts +17 -0
  16. package/src/client/index.ts +122 -0
  17. package/src/client/locate.ts +34 -0
  18. package/src/client/published.ts +3 -0
  19. package/src/client/runs.ts +337 -0
  20. package/src/command-line.ts +64 -0
  21. package/src/commands.ts +7 -1
  22. package/src/control-plane.ts +41 -11
  23. package/src/coordinator-prompt.ts +4 -3
  24. package/src/coordinator-socket.ts +41 -33
  25. package/src/deadlines.ts +40 -7
  26. package/src/decisions/directory.ts +51 -40
  27. package/src/decisions/fake.ts +1 -1
  28. package/src/decisions/openrouter.ts +4 -2
  29. package/src/decisions/seam.ts +4 -4
  30. package/src/display-path.ts +3 -3
  31. package/src/duration.ts +43 -27
  32. package/src/emit-command.ts +88 -58
  33. package/src/emit.ts +254 -56
  34. package/src/event-ids.ts +31 -0
  35. package/src/files.ts +44 -4
  36. package/src/hand-over.ts +150 -0
  37. package/src/help/allowance.txt +4 -3
  38. package/src/help/emit.txt +18 -6
  39. package/src/help/inbox.txt +6 -4
  40. package/src/help/logs.txt +9 -6
  41. package/src/help/open.txt +29 -0
  42. package/src/help/reply.txt +6 -8
  43. package/src/help/run.txt +30 -11
  44. package/src/help/runs.txt +4 -2
  45. package/src/help/tick.txt +9 -4
  46. package/src/herdr-placement.ts +3 -33
  47. package/src/herdr-run-session.ts +7 -15
  48. package/src/herdr-workspace-marks.ts +7 -17
  49. package/src/holder-command.ts +56 -133
  50. package/src/in-lines.ts +28 -0
  51. package/src/inbox-command.ts +40 -57
  52. package/src/index.ts +1 -1
  53. package/src/init-command.ts +8 -17
  54. package/src/init-template/hello/workflow.test.ts.template +3 -3
  55. package/src/init-template/hello/workflow.ts.template +5 -4
  56. package/src/jsonl.ts +10 -6
  57. package/src/keep-alive.ts +15 -12
  58. package/src/logs-command.ts +128 -47
  59. package/src/message-operation.ts +6 -3
  60. package/src/messaging/limits.ts +4 -4
  61. package/src/messaging/messaging.ts +31 -28
  62. package/src/messaging/routes.ts +1 -1
  63. package/src/messaging/text.ts +9 -7
  64. package/src/open-command.ts +770 -0
  65. package/src/operation-events.ts +4 -6
  66. package/src/operation-liveness.ts +19 -4
  67. package/src/operator-cli.ts +224 -94
  68. package/src/operator-runtime.ts +29 -7
  69. package/src/ops.ts +270 -0
  70. package/src/pane-layout.ts +105 -59
  71. package/src/progress-view.ts +10 -10
  72. package/src/project-engine.ts +79 -0
  73. package/src/projects.ts +13 -2
  74. package/src/refusals.ts +119 -0
  75. package/src/result-slots.ts +11 -31
  76. package/src/result-validation.ts +2 -2
  77. package/src/run-command.ts +45 -28
  78. package/src/run-commands.ts +43 -115
  79. package/src/run-event-log.ts +1 -1
  80. package/src/run-lines.ts +159 -0
  81. package/src/run-lock.ts +57 -1
  82. package/src/{stage-plan.ts → run-policy/stage-plan.ts} +46 -42
  83. package/src/run-policy/stages.ts +113 -0
  84. package/src/run-policy/waits.ts +452 -0
  85. package/src/run-prepare.ts +205 -43
  86. package/src/run-read.ts +109 -41
  87. package/src/run-state.ts +170 -50
  88. package/src/run-text.ts +54 -0
  89. package/src/run-unreachable.ts +18 -0
  90. package/src/run-usage.ts +18 -12
  91. package/src/run-view.ts +81 -17
  92. package/src/runs-command.ts +83 -63
  93. package/src/runs.ts +310 -212
  94. package/src/sandbox-homes.ts +65 -13
  95. package/src/sandboxes.ts +182 -28
  96. package/src/session-lines.ts +93 -0
  97. package/src/skills/run-skills.ts +8 -1
  98. package/src/sources.ts +28 -29
  99. package/src/spend.ts +333 -0
  100. package/src/stage-ledger.ts +119 -131
  101. package/src/stop-crashed.ts +6 -5
  102. package/src/stopped.ts +4 -4
  103. package/src/test-command.ts +36 -14
  104. package/src/testing/operation-events.ts +3 -23
  105. package/src/testing.ts +73 -26
  106. package/src/tick-command.ts +145 -15
  107. package/src/tools.ts +25 -16
  108. package/src/turn-ends.ts +37 -0
  109. package/src/waits.ts +651 -0
  110. package/src/wake.ts +84 -54
  111. package/src/workflow-loader.ts +54 -4
  112. package/src/workflow-runner.ts +805 -580
  113. package/src/workflow-test-preload.ts +4 -1
  114. package/src/workflow-testing/coordinators.ts +3 -3
  115. package/src/workflow-testing/fake-client.ts +387 -0
  116. package/src/workflow-testing/host.ts +121 -53
  117. package/src/workflow-testing/index.ts +33 -842
  118. package/src/workflow-testing/run-test.ts +1128 -0
  119. package/src/workflow-testing/script.ts +74 -45
  120. package/src/hold-run-lock.fixture.ts +0 -8
  121. package/src/mailbox.ts +0 -990
  122. package/src/workflow-testing/coordinator.typecheck.ts +0 -54
  123. package/src/workflow-testing/workflow-testing.typecheck.ts +0 -64
@@ -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 { type AttemptRecord, RUN_EVENT_VERSIONS } from "@agentswf/contract/records";
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 { projectOf, projectRunRoot } from "./projects";
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
- endTurnsLine,
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: AttemptRecord[]; warnings: string[] };
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: { loaded: LoadedWorkflow; id: string; argv: readonly string[] } }
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
- return { refused: `load: ${messageOf(error)}` };
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 { refused };
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, prepareArgs(loaded.workflow, command.workflowArgs, project));
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 { opens: { loaded, id, argv: command.workflowArgs } };
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 { refused: `${error instanceof RunRefused ? "" : "prepare: "}${messageOf(error)}` };
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
- return { loaded, prepared: await prepareRun(command, loaded, now), command };
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 { refused: `${error instanceof RunRefused ? "" : "prepare: "}${messageOf(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 {
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) => prepareArgs(workflow, argv, cwd);
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 = prepare(argv, cwd);
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 records = await readStageRecords(run.dir);
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 = attemptRefusal(attempts, records, { fromStage: command.fromStage !== undefined });
135
- if (refused) throw new RunRefused(refusalMessage(id, refused, now));
136
- const { fromStage } = command;
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
- !records.has(fromStage) &&
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
- return { args, continued: { run, attempts, warnings: unrecorded } };
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
- if (command.fromStage !== undefined || command.values !== undefined)
164
- return "a run of a workflow with events takes no --from-stage or --values";
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 "a continue takes no arguments: it goes on with those its run was started with";
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 `a run of a workflow with events runs in its file's project, as every wake starts it: it takes no ${unkept.join(", ")}`;
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(`the workflow's id(args) failed: ${messageOf(error)}; --id gives one`);
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
- /** Claims the run's next attempt, naming each earlier one found interrupted. */
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 endTurnsLine(run.dir)
388
+ await endLogLine(run.dir)
227
389
  .then(() =>
228
390
  appendEvent(run.dir, {
229
391
  version: RUN_EVENT_VERSIONS["attempt-started"],
230
- attempt: claimed.attempt.record.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. */