@smthrs/std 1.0.0-rc.7 → 1.0.0-rc.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +4 -0
- package/README.md +38 -36
- package/dist/cjs/Checkpoints.d.ts +37 -0
- package/dist/cjs/Checkpoints.d.ts.map +1 -1
- package/dist/cjs/Checkpoints.js +75 -0
- package/dist/cjs/Checkpoints.js.map +2 -2
- package/dist/cjs/internal/EnvelopePrecheck.d.ts +2 -0
- package/dist/cjs/internal/EnvelopePrecheck.d.ts.map +1 -1
- package/dist/cjs/internal/EnvelopePrecheck.js +217 -34
- package/dist/cjs/internal/EnvelopePrecheck.js.map +3 -3
- package/dist/cjs/internal/Exec.d.ts +12 -7
- package/dist/cjs/internal/Exec.d.ts.map +1 -1
- package/dist/cjs/internal/Exec.js +8 -4
- package/dist/cjs/internal/Exec.js.map +2 -2
- package/dist/esm/Checkpoints.d.ts +37 -0
- package/dist/esm/Checkpoints.d.ts.map +1 -1
- package/dist/esm/Checkpoints.js +85 -0
- package/dist/esm/Checkpoints.js.map +1 -1
- package/dist/esm/internal/EnvelopePrecheck.d.ts +2 -0
- package/dist/esm/internal/EnvelopePrecheck.d.ts.map +1 -1
- package/dist/esm/internal/EnvelopePrecheck.js +295 -60
- package/dist/esm/internal/EnvelopePrecheck.js.map +1 -1
- package/dist/esm/internal/Exec.d.ts +12 -7
- package/dist/esm/internal/Exec.d.ts.map +1 -1
- package/dist/esm/internal/Exec.js +30 -9
- package/dist/esm/internal/Exec.js.map +1 -1
- package/docs/README.md +3 -1
- package/docs/api.md +20 -17
- package/docs/guides/pin-a-checkpoint.md +20 -0
- package/docs/installation.md +13 -9
- package/docs/quickstart.md +1 -1
- package/docs/troubleshooting.md +12 -0
- package/package.json +6 -6
- package/src/Checkpoints.ts +137 -0
- package/src/internal/EnvelopePrecheck.ts +295 -63
- package/src/internal/Exec.ts +32 -8
|
@@ -28,13 +28,15 @@ import { elideMiddle } from "./Text.js";
|
|
|
28
28
|
* @category models
|
|
29
29
|
* @since 1.0.0
|
|
30
30
|
*/
|
|
31
|
-
export const ExecErrorCode = Schema.Literals(["timeout", "spawn_error", "capture_overflow"]);
|
|
31
|
+
export const ExecErrorCode = Schema.Literals(["timeout", "spawn_error", "arguments_too_long", "capture_overflow"]);
|
|
32
32
|
/**
|
|
33
33
|
* A command that never produced an exit code.
|
|
34
34
|
*
|
|
35
35
|
* A non-zero exit is a successful {@link ExecResult}, not a failure; this
|
|
36
|
-
* error is reserved for a command that could not start
|
|
37
|
-
*
|
|
36
|
+
* error is reserved for a command that could not start (`arguments_too_long`
|
|
37
|
+
* when the operating system refused it for the size of its arguments) or was
|
|
38
|
+
* cut off by its timeout, or whose captured output exceeded a refusing capture
|
|
39
|
+
* bound.
|
|
38
40
|
*
|
|
39
41
|
* @category errors
|
|
40
42
|
* @since 1.0.0
|
|
@@ -160,6 +162,20 @@ const capture = (stream, maxBytes, overflow) => maxBytes === undefined
|
|
|
160
162
|
: overflow === "ends"
|
|
161
163
|
? boundedEnds(stream, maxBytes)
|
|
162
164
|
: boundedTail(stream, maxBytes, overflow);
|
|
165
|
+
/**
|
|
166
|
+
* A command whose arguments exceed the operating system's limit fails `execve`
|
|
167
|
+
* with `E2BIG` before it starts: one argument over 128 KiB on Linux, all of
|
|
168
|
+
* them over 1 MiB on macOS. A heredoc in a command line, or a file's text
|
|
169
|
+
* passed as an argument, is one such argument. The platform error's own text
|
|
170
|
+
* repeats the whole command line, so this text is written here instead, and a
|
|
171
|
+
* caller may show it as it stands.
|
|
172
|
+
*/
|
|
173
|
+
const argumentsTooLong = (error) => error.cause?.code === "E2BIG"
|
|
174
|
+
? new ExecError({
|
|
175
|
+
code: "arguments_too_long",
|
|
176
|
+
message: "Command not started: its arguments exceed the operating system's size limit (E2BIG). Send long text on standard input or in a file, not in the command line or its arguments."
|
|
177
|
+
})
|
|
178
|
+
: undefined;
|
|
163
179
|
const unbounded = (command, options) => Effect.gen(function* () {
|
|
164
180
|
const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
|
|
165
181
|
const settings = {
|
|
@@ -202,7 +218,7 @@ const unbounded = (command, options) => Effect.gen(function* () {
|
|
|
202
218
|
stdoutDroppedBytes: stdout.droppedBytes,
|
|
203
219
|
stderrDroppedBytes: stderr.droppedBytes
|
|
204
220
|
};
|
|
205
|
-
}).pipe(Effect.scoped, Effect.mapError((error) => error instanceof ExecError ? error : new ExecError({
|
|
221
|
+
}).pipe(Effect.scoped, Effect.mapError((error) => error instanceof ExecError ? error : argumentsTooLong(error) ?? new ExecError({
|
|
206
222
|
code: "spawn_error",
|
|
207
223
|
message: `exec: ${command}: ${error.message}`
|
|
208
224
|
})));
|
|
@@ -232,18 +248,21 @@ export const exec = (command, options = {}) => options.timeoutMs === undefined
|
|
|
232
248
|
*
|
|
233
249
|
* A timeout keeps its own code, because "the command never finished" and "the
|
|
234
250
|
* command never started" are different things to a caller deciding whether to
|
|
235
|
-
* retry.
|
|
251
|
+
* retry. Arguments the operating system refused for their size are the
|
|
252
|
+
* caller's own input, which a shell flow publishes, so they read as
|
|
253
|
+
* `invalid_input`. Every other way of producing no exit code is a host failure.
|
|
236
254
|
*
|
|
237
255
|
* @category errors
|
|
238
256
|
* @since 1.0.0
|
|
239
257
|
*/
|
|
240
|
-
export const toStdErrorCode = (error) => error.code === "timeout" ? "timeout" : "command_failed";
|
|
258
|
+
export const toStdErrorCode = (error) => error.code === "timeout" ? "timeout" : error.code === "arguments_too_long" ? "invalid_input" : "command_failed";
|
|
241
259
|
/**
|
|
242
260
|
* The model-facing failure one execution error becomes.
|
|
243
261
|
*
|
|
244
262
|
* A timeout names the command, which is what the caller asked for and all a
|
|
245
|
-
* timeout has to say.
|
|
246
|
-
*
|
|
263
|
+
* timeout has to say. Arguments too long to start carry the reason and the
|
|
264
|
+
* fix, and nothing of the command. Any other failure carries the message the
|
|
265
|
+
* execution produced, which already names the command.
|
|
247
266
|
*
|
|
248
267
|
* @category errors
|
|
249
268
|
* @since 1.0.0
|
|
@@ -252,7 +271,9 @@ export const toStdError = (command, error) => new StdError.StdError({
|
|
|
252
271
|
code: toStdErrorCode(error),
|
|
253
272
|
message: error.code === "timeout"
|
|
254
273
|
? `Command timed out: ${command}`
|
|
255
|
-
:
|
|
274
|
+
: error.code === "arguments_too_long"
|
|
275
|
+
? error.message
|
|
276
|
+
: `Command failed to start: ${error.message}`,
|
|
256
277
|
...(error.limitMillis === undefined ? {} : { limitMillis: error.limitMillis })
|
|
257
278
|
});
|
|
258
279
|
//# sourceMappingURL=Exec.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Exec.js","sourceRoot":"","sources":["../../../src/internal/Exec.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Buffered shell execution over the permission-aware process spawner.\n *\n * `@smthrs/kernel` used to expose a `Shell` service whose `exec` returned a\n * buffered `{ stdout, stderr, exitCode }`. The kernel replaced it with\n * `ChildProcessSpawner`, Effect's own process boundary, which streams and has\n * no buffered form. This module is the buffered form the shell flows still\n * want, expressed in terms of the spawner so the `proc:spawn` capability check\n * and the command-line resource stay exactly where the kernel put them.\n *\n * Commands run through the platform shell (`shell: true`), matching the old\n * service: the flows take a command *line*, not an argv.\n *\n * @since 1.0.0\n */\n\nimport * as ChildProcessEnvironment from \"@smthrs/kernel/ChildProcessEnvironment\"\nimport * as ChildProcessSpawner from \"@smthrs/kernel/ChildProcessSpawner\"\nimport * as Effect from \"effect/Effect\"\nimport * as ChildProcess from \"effect/process/ChildProcess\"\nimport * as Schema from \"effect/Schema\"\nimport * as Stream from \"effect/Stream\"\nimport * as StdError from \"../StdError.ts\"\nimport { elideMiddle } from \"./Text.ts\"\n\n/**\n * Failure codes a buffered execution can report, matching the codes the\n * removed kernel `Shell` used so handler-level mapping is unchanged.\n *\n * @category models\n * @since 1.0.0\n */\nexport const ExecErrorCode = Schema.Literals([\"timeout\", \"spawn_error\", \"capture_overflow\"])\n\n/**\n * Failure codes a buffered execution can report.\n *\n * @category models\n * @since 1.0.0\n */\nexport type ExecErrorCode = typeof ExecErrorCode.Type\n\n/**\n * A command that never produced an exit code.\n *\n * A non-zero exit is a successful {@link ExecResult}, not a failure; this\n * error is reserved for a command that could not start or was cut off by its\n * timeout, or whose captured output exceeded a refusing capture bound.\n *\n * @category errors\n * @since 1.0.0\n */\nexport class ExecError extends Schema.TaggedError<ExecError>()(\"@smthrs/std/ExecError\", {\n code: ExecErrorCode,\n message: Schema.String,\n /** The limit a `timeout` ran past, in milliseconds. */\n limitMillis: Schema.optional(Schema.Number)\n}) {}\n\n/**\n * The buffered result of one command.\n *\n * @category models\n * @since 1.0.0\n */\nexport interface ExecResult {\n readonly stdout: string\n readonly stderr: string\n readonly exitCode: number\n /** UTF-8 bytes of stdout dropped to stay inside the capture bound; see `overflow`. */\n readonly stdoutDroppedBytes: number\n /** UTF-8 bytes of stderr dropped to stay inside the capture bound; see `overflow`. */\n readonly stderrDroppedBytes: number\n}\n\n/**\n * Options accepted by {@link exec}.\n *\n * @category models\n * @since 1.0.0\n */\nexport interface ExecOptions {\n readonly cwd?: string | undefined\n readonly env?: Readonly<Record<string, string>> | undefined\n readonly timeoutMs?: number | undefined\n /**\n * The program's arguments. Present means an argv, absent means a command\n * line for the platform shell. An argv is how a payload reaches a program\n * without being quoted into a string: nothing between here and `execve`\n * re-parses it.\n */\n readonly args?: ReadonlyArray<string> | undefined\n /**\n * Text written to the program's standard input, as data. A script delivered\n * this way is never quoted, escaped, or heredoc-terminated, which is the\n * whole point: quoting corruption cost the measured SWE-bench program twelve\n * probe failures and one instance's most expensive frame.\n */\n readonly stdin?: string | undefined\n /**\n * Bytes retained per captured stream. A stream that prints more keeps what\n * `overflow` names and reports what it dropped, so a command printing\n * gigabytes costs a bounded amount of memory rather than the whole of what it\n * printed.\n *\n * Absent means unbounded, which is what a caller wants when it needs every\n * byte a tool produced (`rg --files`, `git worktree list`).\n */\n readonly maxCaptureBytes?: number | undefined\n /**\n * What a stream past the bound keeps. `tail`, the default, keeps its last\n * bytes. `ends` keeps its first and last halves and puts one line naming the\n * dropped bytes where the middle was. `refuse` fails if either stream exceeds\n * the bound.\n */\n readonly overflow?: Overflow | undefined\n}\n\ntype Overflow = \"tail\" | \"ends\" | \"refuse\"\n\n/** One captured stream: the text kept, and the bytes dropped to keep it bounded. */\ninterface Capture {\n readonly text: string\n readonly droppedBytes: number\n}\n\ninterface Tail {\n chunks: Array<Uint8Array>\n bytes: number\n dropped: number\n}\n\n/** A stream's first bytes and its last ones: what `overflow: \"ends\"` keeps. */\ninterface Ends {\n readonly head: Array<Uint8Array>\n headBytes: number\n readonly tail: Tail\n}\n\nconst decoder = new TextDecoder(\"utf-8\")\n\nconst emptyTail = (): Tail => ({ chunks: [], bytes: 0, dropped: 0 })\n\n/** A UTF-8 continuation byte is `10xxxxxx`; every other byte starts a character. */\nconst isContinuation = (byte: number): boolean => (byte & 0b1100_0000) === 0b1000_0000\n\nconst join = (chunks: ReadonlyArray<Uint8Array>, size: number): Uint8Array => {\n const bytes = new Uint8Array(size)\n let offset = 0\n for (const chunk of chunks) {\n bytes.set(chunk, offset)\n offset += chunk.byteLength\n }\n return bytes\n}\n\n/** Appends one chunk and drops from the front whatever no longer fits. */\nconst keepTail = (state: Tail, chunk: Uint8Array, maxBytes: number): void => {\n if (chunk.byteLength === 0) return\n state.chunks.push(chunk)\n state.bytes += chunk.byteLength\n while (state.bytes > maxBytes && state.chunks.length > 0) {\n const excess = state.bytes - maxBytes\n const first = state.chunks[0]!\n if (first.byteLength <= excess) {\n state.chunks.shift()\n state.bytes -= first.byteLength\n state.dropped += first.byteLength\n } else {\n state.chunks[0] = first.subarray(excess)\n state.bytes -= excess\n state.dropped += excess\n }\n }\n}\n\n/**\n * Joins the retained chunks and decodes them, skipping the partial code point a\n * dropped head can leave behind.\n *\n * A code point is at most four bytes, so at most three leading continuation\n * bytes can precede the first whole character. Skipping them is counted as\n * dropped rather than decoded to a replacement character the process never\n * printed.\n */\nconst decodeTail = (state: Tail): Capture => {\n const bytes = join(state.chunks, state.bytes)\n if (state.dropped === 0) return { text: decoder.decode(bytes), droppedBytes: 0 }\n let start = 0\n while (start < bytes.byteLength && start < 3 && isContinuation(bytes[start]!)) start++\n return { text: decoder.decode(bytes.subarray(start)), droppedBytes: state.dropped + start }\n}\n\n/**\n * The length of a cut head without the character the cut left unfinished.\n *\n * Its last character starts at the last byte that is not a continuation byte,\n * and that byte says how many bytes the character needs. One that needs more\n * than the head holds is dropped, for the same reason {@link decodeTail} skips\n * a partial code point.\n */\nconst wholeLength = (bytes: Uint8Array): number => {\n for (let back = 1; back <= Math.min(3, bytes.byteLength); back++) {\n const byte = bytes[bytes.byteLength - back]!\n if (isContinuation(byte)) continue\n const width = byte >= 0b1111_0000 ? 4 : byte >= 0b1110_0000 ? 3 : byte >= 0b1100_0000 ? 2 : 1\n return width > back ? bytes.byteLength - back : bytes.byteLength\n }\n return bytes.byteLength\n}\n\n/**\n * Decodes a stream kept by its ends. A stream that fit is decoded whole;\n * otherwise each end is cut on a character boundary and one line naming every\n * byte between them stands where the middle was.\n */\nconst decodeEnds = (state: Ends): Capture => {\n if (state.tail.dropped === 0) {\n const whole = join([...state.head, ...state.tail.chunks], state.headBytes + state.tail.bytes)\n return { text: decoder.decode(whole), droppedBytes: 0 }\n }\n const head = join(state.head, state.headBytes)\n const kept = wholeLength(head)\n const tail = decodeTail(state.tail)\n const droppedBytes = head.byteLength - kept + tail.droppedBytes\n return { text: elideMiddle(decoder.decode(head.subarray(0, kept)), droppedBytes, tail.text), droppedBytes }\n}\n\nconst boundedTail = <E>(\n stream: Stream.Stream<Uint8Array, E>,\n maxBytes: number,\n overflow: Overflow | undefined\n): Effect.Effect<Capture, E> =>\n Stream.runFold(stream, emptyTail, (state, chunk) => {\n keepTail(state, chunk, maxBytes)\n return state\n }).pipe(Effect.map((state) =>\n overflow === \"refuse\" && state.dropped > 0\n ? { text: \"\", droppedBytes: state.dropped }\n : decodeTail(state)\n ))\n\n/**\n * Keeps the first half of the bound and the last half. The head fills first,\n * and every byte after it passes through the tail, which drops from its front:\n * what falls out of the tail is the middle.\n */\nconst boundedEnds = <E>(stream: Stream.Stream<Uint8Array, E>, maxBytes: number): Effect.Effect<Capture, E> => {\n const headBytes = Math.floor(maxBytes / 2)\n return Stream.runFold(stream, (): Ends => ({ head: [], headBytes: 0, tail: emptyTail() }), (state, chunk) => {\n const taken = Math.min(headBytes - state.headBytes, chunk.byteLength)\n if (taken > 0) {\n state.head.push(chunk.subarray(0, taken))\n state.headBytes += taken\n }\n keepTail(state.tail, chunk.subarray(taken), maxBytes - headBytes)\n return state\n }).pipe(Effect.map(decodeEnds))\n}\n\nconst capture = <E>(\n stream: Stream.Stream<Uint8Array, E>,\n maxBytes: number | undefined,\n overflow: Overflow | undefined\n): Effect.Effect<Capture, E> =>\n maxBytes === undefined\n ? Effect.map(Stream.mkString(Stream.decodeText(stream)), (text) => ({ text, droppedBytes: 0 }))\n : overflow === \"ends\"\n ? boundedEnds(stream, maxBytes)\n : boundedTail(stream, maxBytes, overflow)\n\nconst unbounded = (\n command: string,\n options: ExecOptions\n): Effect.Effect<ExecResult, ExecError, ChildProcessSpawner.ChildProcessSpawner> =>\n Effect.gen(function*() {\n const spawner = yield* ChildProcessSpawner.ChildProcessSpawner\n const settings: ChildProcess.CommandOptions = {\n ...(options.args === undefined ? { shell: true } : {}),\n ...(options.cwd === undefined ? {} : { cwd: options.cwd }),\n env: ChildProcessEnvironment.make(process.env, options.env),\n extendEnv: false,\n ...(options.stdin === undefined\n ? {}\n : { stdin: Stream.make(new TextEncoder().encode(options.stdin)) })\n }\n const handle = yield* spawner.spawn(\n options.args === undefined\n ? ChildProcess.make(command, settings)\n : ChildProcess.make(command, [...options.args], settings)\n )\n // stdout, stderr, and the exit code have to be consumed concurrently: a\n // command that fills a pipe blocks until the pipe is drained, so waiting\n // for exit first would deadlock on any output larger than the buffer.\n const [stdout, stderr, exitCode] = yield* Effect.all(\n [\n capture(handle.stdout, options.maxCaptureBytes, options.overflow),\n capture(handle.stderr, options.maxCaptureBytes, options.overflow),\n handle.exitCode\n ],\n { concurrency: \"unbounded\" }\n )\n if (options.overflow === \"refuse\") {\n const overflowed = [\n stdout.droppedBytes > 0 ? \"stdout\" : undefined,\n stderr.droppedBytes > 0 ? \"stderr\" : undefined\n ]\n .filter((stream): stream is string => stream !== undefined)\n if (overflowed.length > 0) {\n return yield* Effect.fail(\n new ExecError({\n code: \"capture_overflow\",\n message: `${command} ${\n overflowed.join(\" and \")\n } exceeded the ${options.maxCaptureBytes}-byte capture cap; refusing partial output`\n })\n )\n }\n }\n return {\n stdout: stdout.text,\n stderr: stderr.text,\n exitCode,\n stdoutDroppedBytes: stdout.droppedBytes,\n stderrDroppedBytes: stderr.droppedBytes\n }\n }).pipe(\n Effect.scoped,\n Effect.mapError((error) =>\n error instanceof ExecError ? error : new ExecError({\n code: \"spawn_error\",\n message: `exec: ${command}: ${error.message}`\n })\n )\n )\n\n/**\n * Runs a command line through the platform shell and buffers its output.\n *\n * Cancellation is scope closure: the spawn is scoped, so an interrupted fiber\n * — including the one a `timeoutMs` interrupts — terminates the process rather\n * than leaking it.\n *\n * @category execution\n * @since 1.0.0\n */\nexport const exec = (\n command: string,\n options: ExecOptions = {}\n): Effect.Effect<ExecResult, ExecError, ChildProcessSpawner.ChildProcessSpawner> =>\n options.timeoutMs === undefined\n ? unbounded(command, options)\n : Effect.timeoutOrElse(unbounded(command, options), {\n duration: options.timeoutMs,\n orElse: () =>\n Effect.fail(\n new ExecError({\n code: \"timeout\",\n message: `exec: \\`${command}\\` exceeded ${options.timeoutMs}ms`,\n limitMillis: options.timeoutMs\n })\n )\n })\n\n/**\n * The model-facing failure code a command that never produced an exit code\n * carries.\n *\n * A timeout keeps its own code, because \"the command never finished\" and \"the\n * command never started\" are different things to a caller deciding whether to\n * retry. Every other way of producing no exit code is a host failure.\n *\n * @category errors\n * @since 1.0.0\n */\nexport const toStdErrorCode = (error: ExecError): StdError.Code =>\n error.code === \"timeout\" ? \"timeout\" : \"command_failed\"\n\n/**\n * The model-facing failure one execution error becomes.\n *\n * A timeout names the command, which is what the caller asked for and all a\n * timeout has to say. Any other failure carries the message the execution\n * produced, which already names the command.\n *\n * @category errors\n * @since 1.0.0\n */\nexport const toStdError = (command: string, error: ExecError): StdError.StdError =>\n new StdError.StdError({\n code: toStdErrorCode(error),\n message: error.code === \"timeout\"\n ? `Command timed out: ${command}`\n : `Command failed to start: ${error.message}`,\n ...(error.limitMillis === undefined ? {} : { limitMillis: error.limitMillis })\n })\n"]}
|
|
1
|
+
{"version":3,"file":"Exec.js","sourceRoot":"","sources":["../../../src/internal/Exec.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Buffered shell execution over the permission-aware process spawner.\n *\n * `@smthrs/kernel` used to expose a `Shell` service whose `exec` returned a\n * buffered `{ stdout, stderr, exitCode }`. The kernel replaced it with\n * `ChildProcessSpawner`, Effect's own process boundary, which streams and has\n * no buffered form. This module is the buffered form the shell flows still\n * want, expressed in terms of the spawner so the `proc:spawn` capability check\n * and the command-line resource stay exactly where the kernel put them.\n *\n * Commands run through the platform shell (`shell: true`), matching the old\n * service: the flows take a command *line*, not an argv.\n *\n * @since 1.0.0\n */\n\nimport * as ChildProcessEnvironment from \"@smthrs/kernel/ChildProcessEnvironment\"\nimport * as ChildProcessSpawner from \"@smthrs/kernel/ChildProcessSpawner\"\nimport * as Effect from \"effect/Effect\"\nimport * as ChildProcess from \"effect/process/ChildProcess\"\nimport * as Schema from \"effect/Schema\"\nimport * as Stream from \"effect/Stream\"\nimport * as StdError from \"../StdError.ts\"\nimport { elideMiddle } from \"./Text.ts\"\n\n/**\n * Failure codes a buffered execution can report, matching the codes the\n * removed kernel `Shell` used so handler-level mapping is unchanged.\n *\n * @category models\n * @since 1.0.0\n */\nexport const ExecErrorCode = Schema.Literals([\"timeout\", \"spawn_error\", \"arguments_too_long\", \"capture_overflow\"])\n\n/**\n * Failure codes a buffered execution can report.\n *\n * @category models\n * @since 1.0.0\n */\nexport type ExecErrorCode = typeof ExecErrorCode.Type\n\n/**\n * A command that never produced an exit code.\n *\n * A non-zero exit is a successful {@link ExecResult}, not a failure; this\n * error is reserved for a command that could not start (`arguments_too_long`\n * when the operating system refused it for the size of its arguments) or was\n * cut off by its timeout, or whose captured output exceeded a refusing capture\n * bound.\n *\n * @category errors\n * @since 1.0.0\n */\nexport class ExecError extends Schema.TaggedError<ExecError>()(\"@smthrs/std/ExecError\", {\n code: ExecErrorCode,\n message: Schema.String,\n /** The limit a `timeout` ran past, in milliseconds. */\n limitMillis: Schema.optional(Schema.Number)\n}) {}\n\n/**\n * The buffered result of one command.\n *\n * @category models\n * @since 1.0.0\n */\nexport interface ExecResult {\n readonly stdout: string\n readonly stderr: string\n readonly exitCode: number\n /** UTF-8 bytes of stdout dropped to stay inside the capture bound; see `overflow`. */\n readonly stdoutDroppedBytes: number\n /** UTF-8 bytes of stderr dropped to stay inside the capture bound; see `overflow`. */\n readonly stderrDroppedBytes: number\n}\n\n/**\n * Options accepted by {@link exec}.\n *\n * @category models\n * @since 1.0.0\n */\nexport interface ExecOptions {\n readonly cwd?: string | undefined\n readonly env?: Readonly<Record<string, string>> | undefined\n readonly timeoutMs?: number | undefined\n /**\n * The program's arguments. Present means an argv, absent means a command\n * line for the platform shell. An argv is how a payload reaches a program\n * without being quoted into a string: nothing between here and `execve`\n * re-parses it.\n */\n readonly args?: ReadonlyArray<string> | undefined\n /**\n * Text written to the program's standard input, as data. A script delivered\n * this way is never quoted, escaped, or heredoc-terminated, which is the\n * whole point: quoting corruption cost the measured SWE-bench program twelve\n * probe failures and one instance's most expensive frame.\n */\n readonly stdin?: string | undefined\n /**\n * Bytes retained per captured stream. A stream that prints more keeps what\n * `overflow` names and reports what it dropped, so a command printing\n * gigabytes costs a bounded amount of memory rather than the whole of what it\n * printed.\n *\n * Absent means unbounded, which is what a caller wants when it needs every\n * byte a tool produced (`rg --files`, `git worktree list`).\n */\n readonly maxCaptureBytes?: number | undefined\n /**\n * What a stream past the bound keeps. `tail`, the default, keeps its last\n * bytes. `ends` keeps its first and last halves and puts one line naming the\n * dropped bytes where the middle was. `refuse` fails if either stream exceeds\n * the bound.\n */\n readonly overflow?: Overflow | undefined\n}\n\ntype Overflow = \"tail\" | \"ends\" | \"refuse\"\n\n/** One captured stream: the text kept, and the bytes dropped to keep it bounded. */\ninterface Capture {\n readonly text: string\n readonly droppedBytes: number\n}\n\ninterface Tail {\n chunks: Array<Uint8Array>\n bytes: number\n dropped: number\n}\n\n/** A stream's first bytes and its last ones: what `overflow: \"ends\"` keeps. */\ninterface Ends {\n readonly head: Array<Uint8Array>\n headBytes: number\n readonly tail: Tail\n}\n\nconst decoder = new TextDecoder(\"utf-8\")\n\nconst emptyTail = (): Tail => ({ chunks: [], bytes: 0, dropped: 0 })\n\n/** A UTF-8 continuation byte is `10xxxxxx`; every other byte starts a character. */\nconst isContinuation = (byte: number): boolean => (byte & 0b1100_0000) === 0b1000_0000\n\nconst join = (chunks: ReadonlyArray<Uint8Array>, size: number): Uint8Array => {\n const bytes = new Uint8Array(size)\n let offset = 0\n for (const chunk of chunks) {\n bytes.set(chunk, offset)\n offset += chunk.byteLength\n }\n return bytes\n}\n\n/** Appends one chunk and drops from the front whatever no longer fits. */\nconst keepTail = (state: Tail, chunk: Uint8Array, maxBytes: number): void => {\n if (chunk.byteLength === 0) return\n state.chunks.push(chunk)\n state.bytes += chunk.byteLength\n while (state.bytes > maxBytes && state.chunks.length > 0) {\n const excess = state.bytes - maxBytes\n const first = state.chunks[0]!\n if (first.byteLength <= excess) {\n state.chunks.shift()\n state.bytes -= first.byteLength\n state.dropped += first.byteLength\n } else {\n state.chunks[0] = first.subarray(excess)\n state.bytes -= excess\n state.dropped += excess\n }\n }\n}\n\n/**\n * Joins the retained chunks and decodes them, skipping the partial code point a\n * dropped head can leave behind.\n *\n * A code point is at most four bytes, so at most three leading continuation\n * bytes can precede the first whole character. Skipping them is counted as\n * dropped rather than decoded to a replacement character the process never\n * printed.\n */\nconst decodeTail = (state: Tail): Capture => {\n const bytes = join(state.chunks, state.bytes)\n if (state.dropped === 0) return { text: decoder.decode(bytes), droppedBytes: 0 }\n let start = 0\n while (start < bytes.byteLength && start < 3 && isContinuation(bytes[start]!)) start++\n return { text: decoder.decode(bytes.subarray(start)), droppedBytes: state.dropped + start }\n}\n\n/**\n * The length of a cut head without the character the cut left unfinished.\n *\n * Its last character starts at the last byte that is not a continuation byte,\n * and that byte says how many bytes the character needs. One that needs more\n * than the head holds is dropped, for the same reason {@link decodeTail} skips\n * a partial code point.\n */\nconst wholeLength = (bytes: Uint8Array): number => {\n for (let back = 1; back <= Math.min(3, bytes.byteLength); back++) {\n const byte = bytes[bytes.byteLength - back]!\n if (isContinuation(byte)) continue\n const width = byte >= 0b1111_0000 ? 4 : byte >= 0b1110_0000 ? 3 : byte >= 0b1100_0000 ? 2 : 1\n return width > back ? bytes.byteLength - back : bytes.byteLength\n }\n return bytes.byteLength\n}\n\n/**\n * Decodes a stream kept by its ends. A stream that fit is decoded whole;\n * otherwise each end is cut on a character boundary and one line naming every\n * byte between them stands where the middle was.\n */\nconst decodeEnds = (state: Ends): Capture => {\n if (state.tail.dropped === 0) {\n const whole = join([...state.head, ...state.tail.chunks], state.headBytes + state.tail.bytes)\n return { text: decoder.decode(whole), droppedBytes: 0 }\n }\n const head = join(state.head, state.headBytes)\n const kept = wholeLength(head)\n const tail = decodeTail(state.tail)\n const droppedBytes = head.byteLength - kept + tail.droppedBytes\n return { text: elideMiddle(decoder.decode(head.subarray(0, kept)), droppedBytes, tail.text), droppedBytes }\n}\n\nconst boundedTail = <E>(\n stream: Stream.Stream<Uint8Array, E>,\n maxBytes: number,\n overflow: Overflow | undefined\n): Effect.Effect<Capture, E> =>\n Stream.runFold(stream, emptyTail, (state, chunk) => {\n keepTail(state, chunk, maxBytes)\n return state\n }).pipe(Effect.map((state) =>\n overflow === \"refuse\" && state.dropped > 0\n ? { text: \"\", droppedBytes: state.dropped }\n : decodeTail(state)\n ))\n\n/**\n * Keeps the first half of the bound and the last half. The head fills first,\n * and every byte after it passes through the tail, which drops from its front:\n * what falls out of the tail is the middle.\n */\nconst boundedEnds = <E>(stream: Stream.Stream<Uint8Array, E>, maxBytes: number): Effect.Effect<Capture, E> => {\n const headBytes = Math.floor(maxBytes / 2)\n return Stream.runFold(stream, (): Ends => ({ head: [], headBytes: 0, tail: emptyTail() }), (state, chunk) => {\n const taken = Math.min(headBytes - state.headBytes, chunk.byteLength)\n if (taken > 0) {\n state.head.push(chunk.subarray(0, taken))\n state.headBytes += taken\n }\n keepTail(state.tail, chunk.subarray(taken), maxBytes - headBytes)\n return state\n }).pipe(Effect.map(decodeEnds))\n}\n\nconst capture = <E>(\n stream: Stream.Stream<Uint8Array, E>,\n maxBytes: number | undefined,\n overflow: Overflow | undefined\n): Effect.Effect<Capture, E> =>\n maxBytes === undefined\n ? Effect.map(Stream.mkString(Stream.decodeText(stream)), (text) => ({ text, droppedBytes: 0 }))\n : overflow === \"ends\"\n ? boundedEnds(stream, maxBytes)\n : boundedTail(stream, maxBytes, overflow)\n\n/**\n * A command whose arguments exceed the operating system's limit fails `execve`\n * with `E2BIG` before it starts: one argument over 128 KiB on Linux, all of\n * them over 1 MiB on macOS. A heredoc in a command line, or a file's text\n * passed as an argument, is one such argument. The platform error's own text\n * repeats the whole command line, so this text is written here instead, and a\n * caller may show it as it stands.\n */\nconst argumentsTooLong = (error: { readonly cause?: unknown }): ExecError | undefined =>\n (error.cause as { readonly code?: unknown } | undefined)?.code === \"E2BIG\"\n ? new ExecError({\n code: \"arguments_too_long\",\n message:\n \"Command not started: its arguments exceed the operating system's size limit (E2BIG). Send long text on standard input or in a file, not in the command line or its arguments.\"\n })\n : undefined\n\nconst unbounded = (\n command: string,\n options: ExecOptions\n): Effect.Effect<ExecResult, ExecError, ChildProcessSpawner.ChildProcessSpawner> =>\n Effect.gen(function*() {\n const spawner = yield* ChildProcessSpawner.ChildProcessSpawner\n const settings: ChildProcess.CommandOptions = {\n ...(options.args === undefined ? { shell: true } : {}),\n ...(options.cwd === undefined ? {} : { cwd: options.cwd }),\n env: ChildProcessEnvironment.make(process.env, options.env),\n extendEnv: false,\n ...(options.stdin === undefined\n ? {}\n : { stdin: Stream.make(new TextEncoder().encode(options.stdin)) })\n }\n const handle = yield* spawner.spawn(\n options.args === undefined\n ? ChildProcess.make(command, settings)\n : ChildProcess.make(command, [...options.args], settings)\n )\n // stdout, stderr, and the exit code have to be consumed concurrently: a\n // command that fills a pipe blocks until the pipe is drained, so waiting\n // for exit first would deadlock on any output larger than the buffer.\n const [stdout, stderr, exitCode] = yield* Effect.all(\n [\n capture(handle.stdout, options.maxCaptureBytes, options.overflow),\n capture(handle.stderr, options.maxCaptureBytes, options.overflow),\n handle.exitCode\n ],\n { concurrency: \"unbounded\" }\n )\n if (options.overflow === \"refuse\") {\n const overflowed = [\n stdout.droppedBytes > 0 ? \"stdout\" : undefined,\n stderr.droppedBytes > 0 ? \"stderr\" : undefined\n ]\n .filter((stream): stream is string => stream !== undefined)\n if (overflowed.length > 0) {\n return yield* Effect.fail(\n new ExecError({\n code: \"capture_overflow\",\n message: `${command} ${\n overflowed.join(\" and \")\n } exceeded the ${options.maxCaptureBytes}-byte capture cap; refusing partial output`\n })\n )\n }\n }\n return {\n stdout: stdout.text,\n stderr: stderr.text,\n exitCode,\n stdoutDroppedBytes: stdout.droppedBytes,\n stderrDroppedBytes: stderr.droppedBytes\n }\n }).pipe(\n Effect.scoped,\n Effect.mapError((error) =>\n error instanceof ExecError ? error : argumentsTooLong(error) ?? new ExecError({\n code: \"spawn_error\",\n message: `exec: ${command}: ${error.message}`\n })\n )\n )\n\n/**\n * Runs a command line through the platform shell and buffers its output.\n *\n * Cancellation is scope closure: the spawn is scoped, so an interrupted fiber\n * — including the one a `timeoutMs` interrupts — terminates the process rather\n * than leaking it.\n *\n * @category execution\n * @since 1.0.0\n */\nexport const exec = (\n command: string,\n options: ExecOptions = {}\n): Effect.Effect<ExecResult, ExecError, ChildProcessSpawner.ChildProcessSpawner> =>\n options.timeoutMs === undefined\n ? unbounded(command, options)\n : Effect.timeoutOrElse(unbounded(command, options), {\n duration: options.timeoutMs,\n orElse: () =>\n Effect.fail(\n new ExecError({\n code: \"timeout\",\n message: `exec: \\`${command}\\` exceeded ${options.timeoutMs}ms`,\n limitMillis: options.timeoutMs\n })\n )\n })\n\n/**\n * The model-facing failure code a command that never produced an exit code\n * carries.\n *\n * A timeout keeps its own code, because \"the command never finished\" and \"the\n * command never started\" are different things to a caller deciding whether to\n * retry. Arguments the operating system refused for their size are the\n * caller's own input, which a shell flow publishes, so they read as\n * `invalid_input`. Every other way of producing no exit code is a host failure.\n *\n * @category errors\n * @since 1.0.0\n */\nexport const toStdErrorCode = (error: ExecError): StdError.Code =>\n error.code === \"timeout\" ? \"timeout\" : error.code === \"arguments_too_long\" ? \"invalid_input\" : \"command_failed\"\n\n/**\n * The model-facing failure one execution error becomes.\n *\n * A timeout names the command, which is what the caller asked for and all a\n * timeout has to say. Arguments too long to start carry the reason and the\n * fix, and nothing of the command. Any other failure carries the message the\n * execution produced, which already names the command.\n *\n * @category errors\n * @since 1.0.0\n */\nexport const toStdError = (command: string, error: ExecError): StdError.StdError =>\n new StdError.StdError({\n code: toStdErrorCode(error),\n message: error.code === \"timeout\"\n ? `Command timed out: ${command}`\n : error.code === \"arguments_too_long\"\n ? error.message\n : `Command failed to start: ${error.message}`,\n ...(error.limitMillis === undefined ? {} : { limitMillis: error.limitMillis })\n })\n"]}
|
package/docs/README.md
CHANGED
|
@@ -42,7 +42,9 @@ the details that decide whether a model gets its next step right:
|
|
|
42
42
|
|
|
43
43
|
## Install
|
|
44
44
|
|
|
45
|
-
|
|
45
|
+
```bash
|
|
46
|
+
npm install @smthrs/std@next effect
|
|
47
|
+
```
|
|
46
48
|
|
|
47
49
|
The handlers
|
|
48
50
|
ask the host for services such as `FileSystem`, `Path`, and
|
package/docs/api.md
CHANGED
|
@@ -307,23 +307,26 @@ How the project under test runs its suite, declared once by the host.
|
|
|
307
307
|
|
|
308
308
|
Pinned trees, and the scratch checkouts a call runs against.
|
|
309
309
|
|
|
310
|
-
| Export | Type
|
|
311
|
-
| ------------------ |
|
|
312
|
-
| `Checkpoints` | interface and `Context.Service` tag
|
|
313
|
-
| `Snapshot` | `Schema.Class`
|
|
314
|
-
| `Materialized` | interface
|
|
315
|
-
| `baseId` | `"base"`
|
|
316
|
-
| `scratchDirectory` | `".flows-checkpoints"`
|
|
317
|
-
| `configSection` | `"flows-checkpoint"`
|
|
318
|
-
| `make` | `(service: Checkpoints) => Checkpoints`
|
|
319
|
-
| `unavailable` | `(id: string) => StdError`
|
|
320
|
-
| `makeNoop` | `() => Checkpoints`
|
|
321
|
-
| `layerNoop` | `Layer<Checkpoints>`
|
|
322
|
-
| `GitOptions` | interface
|
|
323
|
-
| `makeGit` | `(options: GitOptions) => Effect<Checkpoints, never, ChildProcessSpawner>`
|
|
324
|
-
| `layerGit` | `(options: GitOptions) => Layer<Checkpoints, never, ChildProcessSpawner>`
|
|
325
|
-
| `
|
|
326
|
-
| `
|
|
310
|
+
| Export | Type | Meaning |
|
|
311
|
+
| ------------------ | --------------------------------------------------------------------------------------------- | ---------------------------------------------------------------- |
|
|
312
|
+
| `Checkpoints` | interface and `Context.Service` tag | `{ capture, materialize }`. |
|
|
313
|
+
| `Snapshot` | `Schema.Class` | `{ id, ref }`, identified as `@smthrs/std/Checkpoints/Snapshot`. |
|
|
314
|
+
| `Materialized` | interface | `id`, `host`, `guest`, `root`, `guestRoot`. |
|
|
315
|
+
| `baseId` | `"base"` | The id naming the tree the run opened on. |
|
|
316
|
+
| `scratchDirectory` | `".flows-checkpoints"` | Where a checkpoint is materialized, relative to the root. |
|
|
317
|
+
| `configSection` | `"flows-checkpoint"` | The git-config section minted checkpoints are recorded under. |
|
|
318
|
+
| `make` | `(service: Checkpoints) => Checkpoints` | |
|
|
319
|
+
| `unavailable` | `(id: string) => StdError` | The refusal a host that pins nothing answers with. |
|
|
320
|
+
| `makeNoop` | `() => Checkpoints` | Fails both calls with `unavailable`. |
|
|
321
|
+
| `layerNoop` | `Layer<Checkpoints>` | |
|
|
322
|
+
| `GitOptions` | interface | `root`, `cwd?`, `baseRef?`. |
|
|
323
|
+
| `makeGit` | `(options: GitOptions) => Effect<Checkpoints, never, ChildProcessSpawner>` | |
|
|
324
|
+
| `layerGit` | `(options: GitOptions) => Layer<Checkpoints, never, ChildProcessSpawner>` | |
|
|
325
|
+
| `ContainerOptions` | interface | `container`, `cwd?`. |
|
|
326
|
+
| `makeContainer` | `(options: ContainerOptions) => Effect<Checkpoints, never, ChildProcessSpawner \| Container>` | Runs git inside the container `bash` runs in. |
|
|
327
|
+
| `layerContainer` | `(options: ContainerOptions) => Layer<Checkpoints, never, ChildProcessSpawner \| Container>` | |
|
|
328
|
+
| `Relocation` | tagged union | `Relocated`, `UnsupportedFlow`, `AbsolutePath`, `OutsideTree`. |
|
|
329
|
+
| `relocate` | `(flow: string, input: Json, materialized: Materialized) => Relocation` | Rewrites one call's input onto a checkpoint. |
|
|
327
330
|
|
|
328
331
|
`capture(id)` returns a `Snapshot`. `materialize(id, use)` is scoped: it hands
|
|
329
332
|
the tree to `use` and removes the checkout however that effect ends. Host git
|
|
@@ -32,6 +32,26 @@ const store = Checkpoints.layerGit({
|
|
|
32
32
|
`Checkpoints.layerNoop`, whose refusal says to take the reading on the live tree
|
|
33
33
|
instead.
|
|
34
34
|
|
|
35
|
+
## Bind the container store
|
|
36
|
+
|
|
37
|
+
When the repository lives only in a container and `bash` runs there, bind
|
|
38
|
+
`layerContainer` instead. It runs git inside that container through the
|
|
39
|
+
`Container` transport `bash` uses, so `ctx.base` and `ctx.checkpoint()` read the
|
|
40
|
+
container's tree. A git store would look for the repository on the host and
|
|
41
|
+
refuse every call that names a checkpoint.
|
|
42
|
+
|
|
43
|
+
```ts
|
|
44
|
+
const store = Checkpoints.layerContainer({
|
|
45
|
+
container: "task", // the container `bash` runs in
|
|
46
|
+
cwd: "/app" // optional; absent is the container's working directory
|
|
47
|
+
})
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
`layerContainer` requires a `ChildProcessSpawner` and a `Container`. It records
|
|
51
|
+
and names a checkpoint as the git store does, and checks a tree out at the same
|
|
52
|
+
`<top>/.flows-checkpoints/<id>-<lease>` path inside the container, so a
|
|
53
|
+
relocated `bash` call keeps its subdirectory.
|
|
54
|
+
|
|
35
55
|
## Capture and materialize
|
|
36
56
|
|
|
37
57
|
```ts
|
package/docs/installation.md
CHANGED
|
@@ -7,7 +7,11 @@ sidebar:
|
|
|
7
7
|
|
|
8
8
|
## Install the package
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
```bash
|
|
11
|
+
npm install @smthrs/std@next effect
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
The `next` tag carries the 1.0 release candidates.
|
|
11
15
|
|
|
12
16
|
It requires
|
|
13
17
|
Node.js 26.4.0 or later and ships as both ESM and CommonJS with TypeScript
|
|
@@ -92,14 +96,14 @@ Six flows reach the host through a service rather than through a platform layer.
|
|
|
92
96
|
Each one has a refusal layer for a host that cannot serve it, so a flow whose
|
|
93
97
|
service is missing says so instead of succeeding quietly.
|
|
94
98
|
|
|
95
|
-
| Service | Refusal layer | Working layer
|
|
96
|
-
| ---------------- | -------------------------- |
|
|
97
|
-
| `Search` | `Search.layerNoop` | `PortableSearch.layer` or `NativeSearch.layer`
|
|
98
|
-
| `Container` | `Container.layerNoop` | `Container.layerCommand({ program: "docker" })`
|
|
99
|
-
| `TestRunner` | `TestRunner.layerNoop` | `TestRunner.layer(runner)`
|
|
100
|
-
| `Checkpoints` | `Checkpoints.layerNoop` | `Checkpoints.layerGit({ root })`
|
|
101
|
-
| `WebSearch` | `WebSearch.layerNoop` | `ExaWebSearch.layer(credentialId)`
|
|
102
|
-
| `LanguageServer` | `LanguageServer.layerNoop` | `NodeLanguageServer.layer(config)`
|
|
99
|
+
| Service | Refusal layer | Working layer |
|
|
100
|
+
| ---------------- | -------------------------- | ------------------------------------------------------------------------------- |
|
|
101
|
+
| `Search` | `Search.layerNoop` | `PortableSearch.layer` or `NativeSearch.layer` |
|
|
102
|
+
| `Container` | `Container.layerNoop` | `Container.layerCommand({ program: "docker" })` |
|
|
103
|
+
| `TestRunner` | `TestRunner.layerNoop` | `TestRunner.layer(runner)` |
|
|
104
|
+
| `Checkpoints` | `Checkpoints.layerNoop` | `Checkpoints.layerGit({ root })` or `Checkpoints.layerContainer({ container })` |
|
|
105
|
+
| `WebSearch` | `WebSearch.layerNoop` | `ExaWebSearch.layer(credentialId)` |
|
|
106
|
+
| `LanguageServer` | `LanguageServer.layerNoop` | `NodeLanguageServer.layer(config)` |
|
|
103
107
|
|
|
104
108
|
`NativeSearch.layer` drives the `rg` executable, so a host that binds it needs
|
|
105
109
|
ripgrep on the path. `PortableSearch.layer` needs nothing beyond `FileSystem`
|
package/docs/quickstart.md
CHANGED
|
@@ -14,7 +14,7 @@ without an external binary and without an API key.
|
|
|
14
14
|
## Prerequisites
|
|
15
15
|
|
|
16
16
|
- Node.js 26.4.0 or later.
|
|
17
|
-
- A
|
|
17
|
+
- A project with the packages installed: `npm install @smthrs/std@next @effect/platform-node effect`.
|
|
18
18
|
|
|
19
19
|
## Make a tree to search
|
|
20
20
|
|
package/docs/troubleshooting.md
CHANGED
|
@@ -160,6 +160,18 @@ The same rule refuses a hermetic `script` whose `interpreter` is not a shell:
|
|
|
160
160
|
the pre-check reads shell text, and Python is not shell text. Run it
|
|
161
161
|
unhermetically, or express it as shell.
|
|
162
162
|
|
|
163
|
+
## invalid_input: Command not started: its arguments exceed the operating system's size limit (E2BIG)
|
|
164
|
+
|
|
165
|
+
**What happened.** The operating system refused to start the command because
|
|
166
|
+
its arguments are too large: one argument over 128 KiB on Linux, or all of them
|
|
167
|
+
over 1 MiB on macOS. A long heredoc in `command`, or a file's text passed in
|
|
168
|
+
`args`, is one such argument. For a `container` call the host's `docker exec`
|
|
169
|
+
carries the same arguments, so the same limit applies.
|
|
170
|
+
|
|
171
|
+
**What to change.** Send the text on standard input, which has no size limit.
|
|
172
|
+
Put a program and its data in `script`, or keep `command` short and pass the
|
|
173
|
+
data in `stdin`. Never put long text in `command` or `args`.
|
|
174
|
+
|
|
163
175
|
## provider_unavailable: this host has no container transport
|
|
164
176
|
|
|
165
177
|
**What happened.** A call named a `container` and no `Container` service was
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@smthrs/std",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "1.0.0-rc.
|
|
4
|
+
"version": "1.0.0-rc.8",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"smthrs": {
|
|
7
7
|
"group": "agent"
|
|
@@ -401,11 +401,11 @@
|
|
|
401
401
|
"tag": "next"
|
|
402
402
|
},
|
|
403
403
|
"dependencies": {
|
|
404
|
-
"@smthrs/capability": "1.0.0-rc.
|
|
405
|
-
"@smthrs/control": "1.0.0-rc.
|
|
406
|
-
"@smthrs/core": "1.0.0-rc.
|
|
407
|
-
"@smthrs/kernel": "1.0.0-rc.
|
|
408
|
-
"@smthrs/model": "1.0.0-rc.
|
|
404
|
+
"@smthrs/capability": "1.0.0-rc.8",
|
|
405
|
+
"@smthrs/control": "1.0.0-rc.8",
|
|
406
|
+
"@smthrs/core": "1.0.0-rc.8",
|
|
407
|
+
"@smthrs/kernel": "1.0.0-rc.8",
|
|
408
|
+
"@smthrs/model": "1.0.0-rc.8"
|
|
409
409
|
},
|
|
410
410
|
"devDependencies": {
|
|
411
411
|
"@effect/language-service": "0.87.2",
|
package/src/Checkpoints.ts
CHANGED
|
@@ -72,6 +72,8 @@
|
|
|
72
72
|
|
|
73
73
|
import { ChildProcessSpawner } from "@smthrs/kernel/ChildProcessSpawner"
|
|
74
74
|
import { Context, Effect, Layer, Schema } from "effect"
|
|
75
|
+
import * as Container from "./Container.ts"
|
|
76
|
+
import * as Exec from "./internal/Exec.ts"
|
|
75
77
|
import { GitWorktree } from "./internal/GitWorktree.ts"
|
|
76
78
|
import { withoutTrailingSlash } from "./internal/Paths.ts"
|
|
77
79
|
import * as Prune from "./internal/Prune.ts"
|
|
@@ -409,6 +411,141 @@ const gitStore = (options: GitOptions, spawner: ChildProcessSpawner["Service"]):
|
|
|
409
411
|
*/
|
|
410
412
|
export const layerGit = (options: GitOptions): Layer.Layer<Checkpoints, never, ChildProcessSpawner> =>
|
|
411
413
|
Layer.effect(Checkpoints)(makeGit(options))
|
|
414
|
+
|
|
415
|
+
/**
|
|
416
|
+
* What {@link makeContainer} needs to know about the container it pins in.
|
|
417
|
+
*
|
|
418
|
+
* @category models
|
|
419
|
+
* @since 1.0.0-rc.8
|
|
420
|
+
*/
|
|
421
|
+
export interface ContainerOptions {
|
|
422
|
+
/** The container `bash` runs in. */
|
|
423
|
+
readonly container: string
|
|
424
|
+
/** A directory of the repository inside it; absent is the container's own working directory. */
|
|
425
|
+
readonly cwd?: string | undefined
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
/**
|
|
429
|
+
* `$1` is the id. Records the working tree with `stash create` over a copy of
|
|
430
|
+
* the index, names the commit in config as {@link makeGit} does, and prints it.
|
|
431
|
+
*/
|
|
432
|
+
const pinScript = [
|
|
433
|
+
"set -e",
|
|
434
|
+
"cd \"$(git rev-parse --show-toplevel)\"",
|
|
435
|
+
"copy=$(mktemp -d)",
|
|
436
|
+
"trap 'rm -rf -- \"$copy\"' EXIT",
|
|
437
|
+
"index=$(git rev-parse --git-path index)",
|
|
438
|
+
"if [ -e \"$index\" ]; then cp -p -- \"$index\" \"$copy/index\"; fi",
|
|
439
|
+
"commit=$(GIT_INDEX_FILE=\"$copy/index\" git -c user.name=flows -c user.email=flows@localhost stash create \"flows checkpoint $1\")",
|
|
440
|
+
"[ -n \"$commit\" ] || commit=$(git rev-parse --verify 'HEAD^{commit}')",
|
|
441
|
+
`git config "${configSection}.$1" "$commit"`,
|
|
442
|
+
"printf '%s\\n' \"$commit\""
|
|
443
|
+
].join("\n")
|
|
444
|
+
|
|
445
|
+
/**
|
|
446
|
+
* `$1` is the id, `$2` the lease. Checks the tree out at
|
|
447
|
+
* `<top>/.flows-checkpoints/<id>-<lease>` in a repository of its own whose
|
|
448
|
+
* objects are the workspace's, and prints the top and the checkout.
|
|
449
|
+
*/
|
|
450
|
+
const checkoutScript = [
|
|
451
|
+
"set -e",
|
|
452
|
+
"top=$(git rev-parse --show-toplevel)",
|
|
453
|
+
"cd \"$top\"",
|
|
454
|
+
"common=$(cd \"$(git rev-parse --git-common-dir)\" && pwd -P)",
|
|
455
|
+
`if [ "$1" = ${baseId} ]; then`,
|
|
456
|
+
` commit=$(git rev-parse --verify --quiet '${TestRunner.captureBase}^{commit}' || git rev-parse --verify --quiet 'HEAD^{commit}') || { echo "No commit resolves from ${TestRunner.captureBase} or HEAD in $top." >&2; exit 1; }`,
|
|
457
|
+
"else",
|
|
458
|
+
` commit=$(git config --get "${configSection}.$1") || { echo "No checkpoint is stored under $1 in $top. Take the reading on the live tree instead." >&2; exit 1; }`,
|
|
459
|
+
"fi",
|
|
460
|
+
`dir="$top/${scratchDirectory}/$1-$2"`,
|
|
461
|
+
"trap 'rm -rf -- \"$dir\"' EXIT",
|
|
462
|
+
`mkdir -p -- "$top/${scratchDirectory}"`,
|
|
463
|
+
"git init --quiet \"$dir\"",
|
|
464
|
+
"printf '%s/objects\\n' \"$common\" > \"$dir/.git/objects/info/alternates\"",
|
|
465
|
+
"git -C \"$dir\" checkout --quiet --detach \"$commit\"",
|
|
466
|
+
"trap - EXIT",
|
|
467
|
+
"printf '%s\\n%s\\n' \"$top\" \"$dir\""
|
|
468
|
+
].join("\n")
|
|
469
|
+
|
|
470
|
+
/**
|
|
471
|
+
* Builds the store for a host whose `bash` runs in one container.
|
|
472
|
+
*
|
|
473
|
+
* The repository is the container's, so every git command runs there, through
|
|
474
|
+
* the same {@link Container.Container} transport `bash` takes. The host store
|
|
475
|
+
* looked in the CLI's own directory instead and refused every `at` in 112 of
|
|
476
|
+
* 123 trials of the 2026-10-14 SWE-bench Pro run. This keeps that store's
|
|
477
|
+
* shape: a checkpoint is a `stash create` commit named in config, and a tree
|
|
478
|
+
* is checked out under the repository for one call, so a relocated `bash`
|
|
479
|
+
* call keeps its subdirectory and still finds what the workspace installs
|
|
480
|
+
* beside it. That git may run what the repository configures, but only inside
|
|
481
|
+
* the container the agent already commands.
|
|
482
|
+
*
|
|
483
|
+
* @category constructors
|
|
484
|
+
* @since 1.0.0-rc.8
|
|
485
|
+
*/
|
|
486
|
+
export const makeContainer = (
|
|
487
|
+
options: ContainerOptions
|
|
488
|
+
): Effect.Effect<Checkpoints, never, ChildProcessSpawner | Container.Container> =>
|
|
489
|
+
Effect.gen(function*() {
|
|
490
|
+
const spawner = yield* ChildProcessSpawner
|
|
491
|
+
const transport = yield* Container.Container
|
|
492
|
+
const sh = (script: string, args: ReadonlyArray<string>) =>
|
|
493
|
+
transport.exec({
|
|
494
|
+
container: options.container,
|
|
495
|
+
file: "sh",
|
|
496
|
+
args: ["-c", script, "sh", ...args],
|
|
497
|
+
cwd: options.cwd,
|
|
498
|
+
stdin: false
|
|
499
|
+
}).pipe(
|
|
500
|
+
Effect.flatMap((plan) =>
|
|
501
|
+
Exec.exec(plan.file, { args: plan.args, env: plan.env }).pipe(
|
|
502
|
+
Effect.mapError((error) => Exec.toStdError(plan.file, error))
|
|
503
|
+
)
|
|
504
|
+
),
|
|
505
|
+
Effect.flatMap((result) =>
|
|
506
|
+
result.exitCode === 0
|
|
507
|
+
? Effect.succeed(result.stdout.trim().split("\n"))
|
|
508
|
+
: Effect.fail(failed(`git in container ${options.container} failed: ${result.stderr.trim()}`))
|
|
509
|
+
),
|
|
510
|
+
Effect.provideService(ChildProcessSpawner, spawner)
|
|
511
|
+
)
|
|
512
|
+
const named = (id: string) =>
|
|
513
|
+
namable.test(id)
|
|
514
|
+
? Effect.void
|
|
515
|
+
: Effect.fail(failed(`A checkpoint id must match ${namable.source}; ${id} does not.`, "invalid_input"))
|
|
516
|
+
return make({
|
|
517
|
+
capture: (id) =>
|
|
518
|
+
named(id).pipe(
|
|
519
|
+
Effect.andThen(sh(pinScript, [id])),
|
|
520
|
+
Effect.map(([commit]) => new Snapshot({ id, ref: commit! }))
|
|
521
|
+
),
|
|
522
|
+
materialize: (id, use) =>
|
|
523
|
+
named(id).pipe(Effect.andThen(Effect.acquireUseRelease(
|
|
524
|
+
sh(checkoutScript, [id, globalThis.crypto.randomUUID()]),
|
|
525
|
+
([root, checkout]) => use({ id, host: checkout!, guest: checkout!, root: root!, guestRoot: root! }),
|
|
526
|
+
([, checkout]) =>
|
|
527
|
+
sh("rm -rf -- \"$1\"", [checkout!]).pipe(
|
|
528
|
+
Effect.catch((error) =>
|
|
529
|
+
Effect.logWarning("could not remove a scratch checkout").pipe(
|
|
530
|
+
Effect.annotateLogs({ path: checkout, reason: error.message })
|
|
531
|
+
)
|
|
532
|
+
)
|
|
533
|
+
)
|
|
534
|
+
)))
|
|
535
|
+
})
|
|
536
|
+
})
|
|
537
|
+
|
|
538
|
+
/**
|
|
539
|
+
* Provides {@link makeContainer}.
|
|
540
|
+
*
|
|
541
|
+
* @category layers
|
|
542
|
+
* @since 1.0.0-rc.8
|
|
543
|
+
*/
|
|
544
|
+
export const layerContainer = (
|
|
545
|
+
options: ContainerOptions
|
|
546
|
+
): Layer.Layer<Checkpoints, never, ChildProcessSpawner | Container.Container> =>
|
|
547
|
+
Layer.effect(Checkpoints)(makeContainer(options))
|
|
548
|
+
|
|
412
549
|
/**
|
|
413
550
|
* Rewrites one call's input so the call runs against a materialized checkpoint.
|
|
414
551
|
*
|