@drawcall/create 0.2.0 → 0.2.2
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/dist/harness.d.ts +9 -2
- package/dist/harness.js +107 -15
- package/dist/shell.js +34 -6
- package/dist/stages.d.ts +3 -1
- package/dist/stages.js +9 -6
- package/dist/supervisor.d.ts +1 -0
- package/dist/supervisor.js +17 -11
- package/package.json +1 -1
package/dist/harness.d.ts
CHANGED
|
@@ -2,7 +2,8 @@ import { Context, Effect, Layer } from "effect";
|
|
|
2
2
|
import { type HarnessName } from "./constants.js";
|
|
3
3
|
import { PreflightError } from "./errors.js";
|
|
4
4
|
import { type CommandResult, Shell } from "./shell.js";
|
|
5
|
-
export declare function
|
|
5
|
+
export declare function sessionCapable(harness: HarnessName): boolean;
|
|
6
|
+
export declare function harnessInvocation(harness: HarnessName, prompt: string, cwd: string, harnessArgs?: ReadonlyArray<string>, sessionArgs?: ReadonlyArray<string>): {
|
|
6
7
|
command: string;
|
|
7
8
|
args: string[];
|
|
8
9
|
};
|
|
@@ -12,9 +13,15 @@ export interface HarnessTurn {
|
|
|
12
13
|
readonly cwd: string;
|
|
13
14
|
readonly harnessArgs?: ReadonlyArray<string>;
|
|
14
15
|
readonly timeoutMs?: number;
|
|
16
|
+
readonly session?: {
|
|
17
|
+
readonly token?: string;
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
export interface HarnessResult extends CommandResult {
|
|
21
|
+
readonly sessionToken?: string;
|
|
15
22
|
}
|
|
16
23
|
export interface HarnessService {
|
|
17
|
-
readonly runTurn: (turn: HarnessTurn) => Effect.Effect<
|
|
24
|
+
readonly runTurn: (turn: HarnessTurn) => Effect.Effect<HarnessResult>;
|
|
18
25
|
readonly select: (requested: HarnessName | undefined) => Effect.Effect<HarnessName, PreflightError>;
|
|
19
26
|
}
|
|
20
27
|
declare const Harness_base: Context.TagClass<Harness, "Harness", HarnessService>;
|
package/dist/harness.js
CHANGED
|
@@ -1,48 +1,140 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
1
2
|
import { Context, Effect, Layer } from "effect";
|
|
2
3
|
import { HARNESS_NAMES } from "./constants.js";
|
|
3
4
|
import { PreflightError } from "./errors.js";
|
|
4
5
|
import { Shell } from "./shell.js";
|
|
6
|
+
const preassign = (begin, resume) => ({
|
|
7
|
+
begin: () => {
|
|
8
|
+
const id = randomUUID();
|
|
9
|
+
return { args: begin(id), id };
|
|
10
|
+
},
|
|
11
|
+
resume
|
|
12
|
+
});
|
|
13
|
+
// Per-harness session continuity. Mechanisms verified against each harness's docs/source (2026).
|
|
14
|
+
const SESSION_ADAPTERS = {
|
|
15
|
+
// opencode mints `ses_…`; `run --print-logs` surfaces it on the first turn, `run --session <id>`
|
|
16
|
+
// continues it on later turns (validated: context is retained across invocations).
|
|
17
|
+
opencode: {
|
|
18
|
+
begin: () => ({ args: ["--print-logs"] }),
|
|
19
|
+
resume: (id) => ["--session", id],
|
|
20
|
+
capture: (output) => output.match(/\bses_[A-Za-z0-9]{8,}\b/)?.[0]
|
|
21
|
+
},
|
|
22
|
+
// Claude Code: pre-set the id with --session-id, continue with --resume.
|
|
23
|
+
claude: preassign((id) => ["--session-id", id], (id) => ["--resume", id]),
|
|
24
|
+
// Gemini CLI: same shape as Claude.
|
|
25
|
+
gemini: preassign((id) => ["--session-id", id], (id) => ["--resume", id]),
|
|
26
|
+
// Grok CLI: a single --session flag both creates-with-id and resumes.
|
|
27
|
+
grok: preassign((id) => ["--session", id], (id) => ["--session", id]),
|
|
28
|
+
// forge: --conversation-id both starts and resumes a stored conversation.
|
|
29
|
+
forge: preassign((id) => ["--conversation-id", id], (id) => ["--conversation-id", id]),
|
|
30
|
+
// pi: --session takes the id for both start and resume.
|
|
31
|
+
pi: preassign((id) => ["--session", id], (id) => ["--session", id]),
|
|
32
|
+
// codex mints a thread id; `exec --json` surfaces it, `exec resume <id>` continues it.
|
|
33
|
+
codex: {
|
|
34
|
+
begin: () => ({ args: ["--json"] }),
|
|
35
|
+
resume: (id) => ["resume", id],
|
|
36
|
+
capture: (output) => output.match(/\bthread_[A-Za-z0-9-]{8,}\b/)?.[0]
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
// Whether a harness can continue a session across build turns. All currently-supported harnesses can;
|
|
40
|
+
// the supervisor only enables same-session for those that do, and degrades to cold turns otherwise.
|
|
41
|
+
export function sessionCapable(harness) {
|
|
42
|
+
return SESSION_ADAPTERS[harness] !== undefined;
|
|
43
|
+
}
|
|
5
44
|
// How each harness is invoked headlessly: the flags that make it run one non-interactive turn and
|
|
6
|
-
// auto-approve the tool calls a build needs (writes, npm, proof runs).
|
|
7
|
-
//
|
|
8
|
-
// the
|
|
9
|
-
export function harnessInvocation(harness, prompt, cwd, harnessArgs = []) {
|
|
45
|
+
// auto-approve the tool calls a build needs (writes, npm, proof runs). `sessionArgs` are the adapter's
|
|
46
|
+
// continuity flags, placed right after the harness's leading subcommand/mode flags so they apply to
|
|
47
|
+
// this turn. This is hard-won per-harness knowledge — the rewrite changes the orchestration around it.
|
|
48
|
+
export function harnessInvocation(harness, prompt, cwd, harnessArgs = [], sessionArgs = []) {
|
|
10
49
|
switch (harness) {
|
|
11
50
|
case "opencode":
|
|
12
51
|
return {
|
|
13
52
|
command: "opencode",
|
|
14
|
-
args: [
|
|
53
|
+
args: [
|
|
54
|
+
"run",
|
|
55
|
+
"--dangerously-skip-permissions",
|
|
56
|
+
...sessionArgs,
|
|
57
|
+
"--dir",
|
|
58
|
+
cwd,
|
|
59
|
+
...harnessArgs,
|
|
60
|
+
prompt
|
|
61
|
+
]
|
|
15
62
|
};
|
|
16
63
|
case "codex":
|
|
17
|
-
return {
|
|
64
|
+
return {
|
|
65
|
+
command: "codex",
|
|
66
|
+
args: ["exec", ...sessionArgs, "--skip-git-repo-check", ...harnessArgs, prompt]
|
|
67
|
+
};
|
|
18
68
|
case "claude":
|
|
19
69
|
return {
|
|
20
70
|
command: "claude",
|
|
21
|
-
args: [
|
|
71
|
+
args: [
|
|
72
|
+
"--print",
|
|
73
|
+
...sessionArgs,
|
|
74
|
+
"--permission-mode",
|
|
75
|
+
"bypassPermissions",
|
|
76
|
+
...harnessArgs,
|
|
77
|
+
prompt
|
|
78
|
+
]
|
|
22
79
|
};
|
|
23
80
|
case "pi":
|
|
24
|
-
return { command: "pi", args: [...harnessArgs, "-p", prompt] };
|
|
81
|
+
return { command: "pi", args: [...sessionArgs, ...harnessArgs, "-p", prompt] };
|
|
25
82
|
case "gemini":
|
|
26
83
|
return {
|
|
27
84
|
command: "gemini",
|
|
28
|
-
args: [
|
|
85
|
+
args: [
|
|
86
|
+
"--approval-mode",
|
|
87
|
+
"yolo",
|
|
88
|
+
"--skip-trust",
|
|
89
|
+
...sessionArgs,
|
|
90
|
+
...harnessArgs,
|
|
91
|
+
"--prompt",
|
|
92
|
+
prompt
|
|
93
|
+
]
|
|
29
94
|
};
|
|
30
95
|
case "grok":
|
|
31
96
|
return {
|
|
32
97
|
command: "grok",
|
|
33
|
-
args: [
|
|
98
|
+
args: [
|
|
99
|
+
"--always-approve",
|
|
100
|
+
"--output-format",
|
|
101
|
+
"plain",
|
|
102
|
+
...sessionArgs,
|
|
103
|
+
...harnessArgs,
|
|
104
|
+
"-p",
|
|
105
|
+
prompt
|
|
106
|
+
]
|
|
34
107
|
};
|
|
35
108
|
case "forge":
|
|
36
|
-
return { command: "forge", args: ["-C", cwd, ...harnessArgs, "-p", prompt] };
|
|
109
|
+
return { command: "forge", args: ["-C", cwd, ...sessionArgs, ...harnessArgs, "-p", prompt] };
|
|
37
110
|
}
|
|
38
111
|
}
|
|
39
112
|
export class Harness extends Context.Tag("Harness")() {
|
|
40
113
|
}
|
|
41
114
|
const live = (shell) => ({
|
|
42
|
-
runTurn: ({ harness, prompt, cwd, harnessArgs, timeoutMs }) => {
|
|
43
|
-
const
|
|
44
|
-
|
|
45
|
-
|
|
115
|
+
runTurn: ({ harness, prompt, cwd, harnessArgs, timeoutMs, session }) => Effect.gen(function* () {
|
|
116
|
+
const adapter = session ? SESSION_ADAPTERS[harness] : undefined;
|
|
117
|
+
let sessionArgs = [];
|
|
118
|
+
let begunId;
|
|
119
|
+
if (adapter && session) {
|
|
120
|
+
if (session.token) {
|
|
121
|
+
sessionArgs = adapter.resume(session.token);
|
|
122
|
+
}
|
|
123
|
+
else {
|
|
124
|
+
const begun = adapter.begin();
|
|
125
|
+
sessionArgs = begun.args;
|
|
126
|
+
begunId = begun.id;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
const { command, args } = harnessInvocation(harness, prompt, cwd, harnessArgs, sessionArgs);
|
|
130
|
+
const result = yield* shell.run({ command, args, cwd, timeoutMs });
|
|
131
|
+
// The continuation token: the one we resumed, the one we minted (preassign), or — for a capture
|
|
132
|
+
// harness's first turn — whatever id it surfaced in its output. Undefined keeps the next turn cold.
|
|
133
|
+
const sessionToken = session?.token ??
|
|
134
|
+
begunId ??
|
|
135
|
+
(session && adapter?.capture ? adapter.capture(result.output) : undefined);
|
|
136
|
+
return { ...result, sessionToken };
|
|
137
|
+
}),
|
|
46
138
|
select: (requested) => Effect.gen(function* () {
|
|
47
139
|
if (requested) {
|
|
48
140
|
if (yield* shell.exists(requested))
|
package/dist/shell.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
|
-
import { createInterface } from "node:readline";
|
|
3
2
|
import { delimiter, dirname, join, resolve } from "node:path";
|
|
4
3
|
import which from "which";
|
|
5
4
|
import { Context, Effect, Layer } from "effect";
|
|
@@ -64,16 +63,43 @@ const live = (env, logger) => ({
|
|
|
64
63
|
env: buildSubprocessEnv(cwd, env),
|
|
65
64
|
stdio: ["ignore", "pipe", "pipe"]
|
|
66
65
|
});
|
|
67
|
-
|
|
68
|
-
|
|
66
|
+
const onLine = (line) => {
|
|
67
|
+
captured.push(line);
|
|
68
|
+
Effect.runSync(logger.captured(line));
|
|
69
|
+
};
|
|
70
|
+
// Capture per stream, collapsing carriage-return redraws to their final frame: a harness
|
|
71
|
+
// progress spinner ("⠧ Processing 16m") rewrites one line with \r thousands of times, which
|
|
72
|
+
// would otherwise flood the log and bury real events. Emitting only on \n (a \r resets the
|
|
73
|
+
// line buffer) keeps each line whole and turns a slow step into a visible time gap between
|
|
74
|
+
// log lines instead of spinner spam. Real output lines are unaffected.
|
|
75
|
+
const flushers = [];
|
|
69
76
|
for (const stream of [child.stdout, child.stderr]) {
|
|
70
77
|
if (!stream)
|
|
71
78
|
continue;
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
79
|
+
let pending = "";
|
|
80
|
+
stream.setEncoding("utf8");
|
|
81
|
+
stream.on("data", (chunk) => {
|
|
82
|
+
for (const ch of chunk) {
|
|
83
|
+
if (ch === "\n") {
|
|
84
|
+
onLine(pending);
|
|
85
|
+
pending = "";
|
|
86
|
+
}
|
|
87
|
+
else if (ch === "\r") {
|
|
88
|
+
pending = "";
|
|
89
|
+
}
|
|
90
|
+
else {
|
|
91
|
+
pending += ch;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
flushers.push(() => {
|
|
96
|
+
if (pending.length > 0) {
|
|
97
|
+
onLine(pending);
|
|
98
|
+
pending = "";
|
|
99
|
+
}
|
|
75
100
|
});
|
|
76
101
|
}
|
|
102
|
+
const flushStreams = () => flushers.forEach((flush) => flush());
|
|
77
103
|
let timedOut = false;
|
|
78
104
|
let killTimer;
|
|
79
105
|
const timeout = timeoutMs === undefined
|
|
@@ -92,6 +118,7 @@ const live = (env, logger) => ({
|
|
|
92
118
|
};
|
|
93
119
|
child.once("error", (error) => {
|
|
94
120
|
cleanup();
|
|
121
|
+
flushStreams();
|
|
95
122
|
// spawn failed (command not found, etc.): surface as a non-zero exit with the error text so
|
|
96
123
|
// the caller can classify it like any other failure.
|
|
97
124
|
captured.push(String(error.message ?? error));
|
|
@@ -99,6 +126,7 @@ const live = (env, logger) => ({
|
|
|
99
126
|
});
|
|
100
127
|
child.once("exit", (code, signal) => {
|
|
101
128
|
cleanup();
|
|
129
|
+
flushStreams();
|
|
102
130
|
const exitCode = timedOut ? TIMEOUT_EXIT_CODE : signal ? 1 : (code ?? 1);
|
|
103
131
|
const elapsed = Math.round((Date.now() - startedAt) / 1000);
|
|
104
132
|
const failure = !timedOut && exitCode !== 0 ? ` — exit ${exitCode}` : "";
|
package/dist/stages.d.ts
CHANGED
|
@@ -14,5 +14,7 @@ export interface RunContext {
|
|
|
14
14
|
}
|
|
15
15
|
type StageEnv = Harness | Scaffold | Git;
|
|
16
16
|
export declare const runStage: (stage: Exclude<PipelineStage, "build">, ctx: RunContext, runMeta: RunMeta | undefined) => Effect.Effect<void, RunFailure, StageEnv>;
|
|
17
|
-
export declare const buildTurn: (ctx: RunContext, turnNumber: number
|
|
17
|
+
export declare const buildTurn: (ctx: RunContext, turnNumber: number, session?: {
|
|
18
|
+
readonly token?: string;
|
|
19
|
+
}) => Effect.Effect<string | undefined, RunFailure, StageEnv>;
|
|
18
20
|
export {};
|
package/dist/stages.js
CHANGED
|
@@ -12,16 +12,17 @@ import { buildBuildPrompt, buildGoalPrompt, buildPlanPrompt, buildSurveyAssetsPr
|
|
|
12
12
|
// Run one harness turn for a stage, turning a non-zero exit / timeout into a classified, explainable
|
|
13
13
|
// StageFailure. A clean exit is success — the stage's own assertions (did GOAL.md appear?) catch the
|
|
14
14
|
// "ran fine but produced nothing" case separately.
|
|
15
|
-
const turn = (stage, prompt, ctx) => Effect.flatMap(Harness, (harness) => harness
|
|
15
|
+
const turn = (stage, prompt, ctx, session) => Effect.flatMap(Harness, (harness) => harness
|
|
16
16
|
.runTurn({
|
|
17
17
|
harness: ctx.harness,
|
|
18
18
|
prompt,
|
|
19
19
|
cwd: ctx.cwd,
|
|
20
20
|
harnessArgs: ctx.harnessArgs,
|
|
21
|
-
timeoutMs: ctx.timeoutMs
|
|
21
|
+
timeoutMs: ctx.timeoutMs,
|
|
22
|
+
session
|
|
22
23
|
})
|
|
23
24
|
.pipe(Effect.flatMap((r) => r.exitCode === 0
|
|
24
|
-
? Effect.
|
|
25
|
+
? Effect.succeed(r)
|
|
25
26
|
: Effect.fail(new StageFailure({
|
|
26
27
|
stage,
|
|
27
28
|
reason: classifyHarnessOutput(r.output, r.exitCode, r.timedOut),
|
|
@@ -96,10 +97,12 @@ const plan = (ctx) => Effect.gen(function* () {
|
|
|
96
97
|
// genuinely consumed it deletes PLAN.md, which is our "done" signal. The checkpoint is --allow-empty,
|
|
97
98
|
// so a turn that committed real work and a turn that did nothing both leave a marker — and the
|
|
98
99
|
// supervisor tells them apart by the tree hash, not by whether a commit appeared.
|
|
99
|
-
const build = (ctx, turnNumber) => Effect.gen(function* () {
|
|
100
|
-
yield* turn("build", buildBuildPrompt(ctx.prompt, turnNumber), ctx);
|
|
100
|
+
const build = (ctx, turnNumber, session) => Effect.gen(function* () {
|
|
101
|
+
const result = yield* turn("build", buildBuildPrompt(ctx.prompt, turnNumber), ctx, session);
|
|
101
102
|
const done = !existsSync(join(ctx.cwd, PLAN_FILE));
|
|
102
103
|
yield* checkpoint(ctx.cwd, done ? "done" : "build");
|
|
104
|
+
// The token to continue this build's warm session next turn (undefined keeps the next turn cold).
|
|
105
|
+
return result.sessionToken;
|
|
103
106
|
});
|
|
104
107
|
// Dispatch a non-build stage. Build is driven directly by the supervisor (it owns the turn number
|
|
105
108
|
// and budget), so it is intentionally excluded from this signature.
|
|
@@ -117,4 +120,4 @@ export const runStage = (stage, ctx, runMeta) => {
|
|
|
117
120
|
return plan(ctx);
|
|
118
121
|
}
|
|
119
122
|
};
|
|
120
|
-
export const buildTurn = (ctx, turnNumber) => build(ctx, turnNumber);
|
|
123
|
+
export const buildTurn = (ctx, turnNumber, session) => build(ctx, turnNumber, session);
|
package/dist/supervisor.d.ts
CHANGED
|
@@ -17,6 +17,7 @@ export interface RunOptions {
|
|
|
17
17
|
readonly harnessTimeoutMs?: number;
|
|
18
18
|
readonly maxTurns?: number;
|
|
19
19
|
readonly skipTemplate?: boolean;
|
|
20
|
+
readonly buildSession?: boolean;
|
|
20
21
|
readonly backoff?: (attempt: number) => Duration.Duration;
|
|
21
22
|
}
|
|
22
23
|
export type Outcome = "done" | "stuck" | "budget-exhausted";
|
package/dist/supervisor.js
CHANGED
|
@@ -5,7 +5,7 @@ import { humanReason } from "./classify.js";
|
|
|
5
5
|
import { MAX_BUILD_TURNS } from "./constants.js";
|
|
6
6
|
import { PreflightError, reasonOf } from "./errors.js";
|
|
7
7
|
import { Git } from "./git.js";
|
|
8
|
-
import { Harness } from "./harness.js";
|
|
8
|
+
import { Harness, sessionCapable } from "./harness.js";
|
|
9
9
|
import { Logger } from "./logger.js";
|
|
10
10
|
import { planNext } from "./resume.js";
|
|
11
11
|
import { buildTurn, runStage } from "./stages.js";
|
|
@@ -55,12 +55,10 @@ export const run = (options) => Effect.gen(function* () {
|
|
|
55
55
|
maxTurns,
|
|
56
56
|
skipTemplate: ctx.skipTemplate
|
|
57
57
|
};
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
errors: []
|
|
61
|
-
});
|
|
58
|
+
const sessionOn = options.buildSession ?? process.env.DC_BUILD_SESSION !== "off";
|
|
59
|
+
return yield* loop(ctx, runMeta, maxTurns, options.backoff ?? backoff, { staleAttempts: 0, errors: [] }, sessionOn, undefined);
|
|
62
60
|
});
|
|
63
|
-
const loop = (ctx, runMeta, maxTurns, backoffFor, state) => Effect.gen(function* () {
|
|
61
|
+
const loop = (ctx, runMeta, maxTurns, backoffFor, state, sessionOn, buildToken) => Effect.gen(function* () {
|
|
64
62
|
const git = yield* Git;
|
|
65
63
|
const before = yield* fingerprint(ctx.cwd);
|
|
66
64
|
const step = yield* planNext(ctx.cwd);
|
|
@@ -72,10 +70,18 @@ const loop = (ctx, runMeta, maxTurns, backoffFor, state) => Effect.gen(function*
|
|
|
72
70
|
return finish(ctx.cwd, "budget-exhausted", state, turns);
|
|
73
71
|
}
|
|
74
72
|
yield* Console.log(`→ ${label(step.stage, ctx.harness)}`);
|
|
73
|
+
// Same-session only spans consecutive build turns, and only for harnesses that support it.
|
|
74
|
+
const session = step.stage === "build" && sessionOn && sessionCapable(ctx.harness)
|
|
75
|
+
? { token: buildToken }
|
|
76
|
+
: undefined;
|
|
75
77
|
const work = step.stage === "build"
|
|
76
|
-
? buildTurn(ctx, turns + 1)
|
|
77
|
-
: runStage(step.stage, ctx, step.stage === "scaffold" ? runMeta : undefined);
|
|
78
|
+
? buildTurn(ctx, turns + 1, session)
|
|
79
|
+
: runStage(step.stage, ctx, step.stage === "scaffold" ? runMeta : undefined).pipe(Effect.as(undefined));
|
|
78
80
|
const outcome = yield* Effect.either(work);
|
|
81
|
+
// The token to continue with next turn — only from a successful build turn. Any failure runs
|
|
82
|
+
// resetClean (the tree returns to the last checkpoint), which makes the warm session inconsistent
|
|
83
|
+
// with the working tree, so we drop it and the next build turn begins a fresh session.
|
|
84
|
+
const nextToken = outcome._tag === "Right" && step.stage === "build" ? outcome.right : undefined;
|
|
79
85
|
let reason;
|
|
80
86
|
if (outcome._tag === "Left") {
|
|
81
87
|
reason = reasonOf(outcome.left);
|
|
@@ -93,8 +99,8 @@ const loop = (ctx, runMeta, maxTurns, backoffFor, state) => Effect.gen(function*
|
|
|
93
99
|
if (outcome._tag === "Right")
|
|
94
100
|
yield* Console.log(`✓ ${label(step.stage, ctx.harness)}`);
|
|
95
101
|
// Forward progress — reset the stale counter and the error accumulator; only the failures that
|
|
96
|
-
// actually precede a stop should appear in its report.
|
|
97
|
-
return yield* loop(ctx, runMeta, maxTurns, backoffFor, { staleAttempts: 0, errors: [] });
|
|
102
|
+
// actually precede a stop should appear in its report. Carry the warm-session token forward.
|
|
103
|
+
return yield* loop(ctx, runMeta, maxTurns, backoffFor, { staleAttempts: 0, errors: [] }, sessionOn, nextToken);
|
|
98
104
|
}
|
|
99
105
|
const staleAttempts = state.staleAttempts + 1;
|
|
100
106
|
const errors = reason ? [...state.errors, reason] : state.errors;
|
|
@@ -105,7 +111,7 @@ const loop = (ctx, runMeta, maxTurns, backoffFor, state) => Effect.gen(function*
|
|
|
105
111
|
}
|
|
106
112
|
yield* note(`no progress; attempt ${staleAttempts}/${MAX_STALE_ATTEMPTS}, backing off`);
|
|
107
113
|
yield* Effect.sleep(backoffFor(staleAttempts));
|
|
108
|
-
return yield* loop(ctx, runMeta, maxTurns, backoffFor, { staleAttempts, errors });
|
|
114
|
+
return yield* loop(ctx, runMeta, maxTurns, backoffFor, { staleAttempts, errors }, sessionOn, undefined);
|
|
109
115
|
});
|
|
110
116
|
const finish = (projectDir, outcome, state, buildTurns) => ({
|
|
111
117
|
projectDir,
|