@namzu/sdk 39.0.0 → 40.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +151 -0
- package/dist/connector/mcp/adapter.d.ts.map +1 -1
- package/dist/connector/mcp/adapter.js +20 -6
- package/dist/connector/mcp/adapter.js.map +1 -1
- package/dist/manager/run/persistence.d.ts +8 -0
- package/dist/manager/run/persistence.d.ts.map +1 -1
- package/dist/manager/run/persistence.js +12 -0
- package/dist/manager/run/persistence.js.map +1 -1
- package/dist/public-runtime.d.ts +3 -1
- package/dist/public-runtime.d.ts.map +1 -1
- package/dist/public-runtime.js +5 -1
- package/dist/public-runtime.js.map +1 -1
- package/dist/public-tools.d.ts +11 -0
- package/dist/public-tools.d.ts.map +1 -1
- package/dist/public-tools.js +14 -0
- package/dist/public-tools.js.map +1 -1
- package/dist/registry/tool/execute.d.ts.map +1 -1
- package/dist/registry/tool/execute.js +2 -3
- package/dist/registry/tool/execute.js.map +1 -1
- package/dist/registry/tool/portable.d.ts +65 -0
- package/dist/registry/tool/portable.d.ts.map +1 -0
- package/dist/registry/tool/portable.js +244 -0
- package/dist/registry/tool/portable.js.map +1 -0
- package/dist/registry/tool/schema.d.ts +32 -5
- package/dist/registry/tool/schema.d.ts.map +1 -1
- package/dist/registry/tool/schema.js +35 -9
- package/dist/registry/tool/schema.js.map +1 -1
- package/dist/registry/toolset/catalog.js +8 -8
- package/dist/registry/toolset/catalog.js.map +1 -1
- package/dist/runtime/jobs/awaited-jobs.d.ts +215 -0
- package/dist/runtime/jobs/awaited-jobs.d.ts.map +1 -0
- package/dist/runtime/jobs/awaited-jobs.js +259 -0
- package/dist/runtime/jobs/awaited-jobs.js.map +1 -0
- package/dist/runtime/jobs/registry.d.ts +33 -2
- package/dist/runtime/jobs/registry.d.ts.map +1 -1
- package/dist/runtime/jobs/registry.js +37 -0
- package/dist/runtime/jobs/registry.js.map +1 -1
- package/dist/runtime/query/executor.d.ts +28 -0
- package/dist/runtime/query/executor.d.ts.map +1 -1
- package/dist/runtime/query/executor.js +39 -1
- package/dist/runtime/query/executor.js.map +1 -1
- package/dist/runtime/query/file-evidence-context.d.ts.map +1 -1
- package/dist/runtime/query/file-evidence-context.js +159 -43
- package/dist/runtime/query/file-evidence-context.js.map +1 -1
- package/dist/runtime/query/file-evidence-replay.d.ts +260 -0
- package/dist/runtime/query/file-evidence-replay.d.ts.map +1 -0
- package/dist/runtime/query/file-evidence-replay.js +647 -0
- package/dist/runtime/query/file-evidence-replay.js.map +1 -0
- package/dist/runtime/query/file-evidence-seed.d.ts +50 -0
- package/dist/runtime/query/file-evidence-seed.d.ts.map +1 -0
- package/dist/runtime/query/file-evidence-seed.js +100 -0
- package/dist/runtime/query/file-evidence-seed.js.map +1 -0
- package/dist/runtime/query/index.d.ts.map +1 -1
- package/dist/runtime/query/index.js +94 -2
- package/dist/runtime/query/index.js.map +1 -1
- package/dist/runtime/query/iteration/index.d.ts +87 -9
- package/dist/runtime/query/iteration/index.d.ts.map +1 -1
- package/dist/runtime/query/iteration/index.js +193 -28
- package/dist/runtime/query/iteration/index.js.map +1 -1
- package/dist/runtime/query/iteration/phases/context.d.ts +10 -0
- package/dist/runtime/query/iteration/phases/context.d.ts.map +1 -1
- package/dist/runtime/query/iteration/phases/context.js.map +1 -1
- package/dist/runtime/query/iteration/phases/tool-review.d.ts.map +1 -1
- package/dist/runtime/query/iteration/phases/tool-review.js +5 -1
- package/dist/runtime/query/iteration/phases/tool-review.js.map +1 -1
- package/dist/runtime/query/plugin-hooks.d.ts +14 -0
- package/dist/runtime/query/plugin-hooks.d.ts.map +1 -1
- package/dist/runtime/query/plugin-hooks.js +18 -0
- package/dist/runtime/query/plugin-hooks.js.map +1 -1
- package/dist/runtime/query/repeat-call.d.ts +17 -4
- package/dist/runtime/query/repeat-call.d.ts.map +1 -1
- package/dist/runtime/query/repeat-call.js +26 -19
- package/dist/runtime/query/repeat-call.js.map +1 -1
- package/dist/runtime/query/steering.d.ts +11 -1
- package/dist/runtime/query/steering.d.ts.map +1 -1
- package/dist/runtime/query/steering.js +12 -1
- package/dist/runtime/query/steering.js.map +1 -1
- package/dist/runtime/query/tooling.d.ts +2 -0
- package/dist/runtime/query/tooling.d.ts.map +1 -1
- package/dist/runtime/query/tooling.js +1 -0
- package/dist/runtime/query/tooling.js.map +1 -1
- package/dist/scheduler/completion-inbox.d.ts +48 -2
- package/dist/scheduler/completion-inbox.d.ts.map +1 -1
- package/dist/scheduler/completion-inbox.js +102 -10
- package/dist/scheduler/completion-inbox.js.map +1 -1
- package/dist/tools/builtins/bash.d.ts.map +1 -1
- package/dist/tools/builtins/bash.js +4 -10
- package/dist/tools/builtins/bash.js.map +1 -1
- package/dist/tools/builtins/edit-apply.d.ts +126 -0
- package/dist/tools/builtins/edit-apply.d.ts.map +1 -0
- package/dist/tools/builtins/edit-apply.js +360 -0
- package/dist/tools/builtins/edit-apply.js.map +1 -0
- package/dist/tools/builtins/edit.d.ts +143 -1
- package/dist/tools/builtins/edit.d.ts.map +1 -1
- package/dist/tools/builtins/edit.js +37 -219
- package/dist/tools/builtins/edit.js.map +1 -1
- package/dist/tools/builtins/index.d.ts +1 -0
- package/dist/tools/builtins/index.d.ts.map +1 -1
- package/dist/tools/builtins/index.js +9 -3
- package/dist/tools/builtins/index.js.map +1 -1
- package/dist/tools/builtins/job.js +1 -1
- package/dist/tools/builtins/job.js.map +1 -1
- package/dist/tools/builtins/read-file.d.ts +2 -2
- package/dist/tools/builtins/read-file.d.ts.map +1 -1
- package/dist/tools/builtins/read-file.js +50 -65
- package/dist/tools/builtins/read-file.js.map +1 -1
- package/dist/tools/builtins/read-render.d.ts +56 -0
- package/dist/tools/builtins/read-render.d.ts.map +1 -0
- package/dist/tools/builtins/read-render.js +73 -0
- package/dist/tools/builtins/read-render.js.map +1 -0
- package/dist/tools/builtins/wait-for-job-bounds.d.ts +67 -0
- package/dist/tools/builtins/wait-for-job-bounds.d.ts.map +1 -0
- package/dist/tools/builtins/wait-for-job-bounds.js +108 -0
- package/dist/tools/builtins/wait-for-job-bounds.js.map +1 -0
- package/dist/tools/builtins/wait-for-job.d.ts +6 -0
- package/dist/tools/builtins/wait-for-job.d.ts.map +1 -0
- package/dist/tools/builtins/wait-for-job.js +162 -0
- package/dist/tools/builtins/wait-for-job.js.map +1 -0
- package/dist/tools/builtins/write-file.js +5 -0
- package/dist/tools/builtins/write-file.js.map +1 -1
- package/dist/tools/coordinator/index.d.ts.map +1 -1
- package/dist/tools/coordinator/index.js +1 -7
- package/dist/tools/coordinator/index.js.map +1 -1
- package/dist/tools/file-read-tracker.d.ts.map +1 -1
- package/dist/tools/file-read-tracker.js +88 -10
- package/dist/tools/file-read-tracker.js.map +1 -1
- package/dist/types/message/index.d.ts +1 -1
- package/dist/types/message/index.d.ts.map +1 -1
- package/dist/types/message/index.js +2 -0
- package/dist/types/message/index.js.map +1 -1
- package/dist/types/run/entity.d.ts +13 -0
- package/dist/types/run/entity.d.ts.map +1 -1
- package/dist/types/sandbox/index.d.ts +15 -14
- package/dist/types/sandbox/index.d.ts.map +1 -1
- package/dist/types/sandbox/index.js.map +1 -1
- package/dist/types/tool/index.d.ts +109 -0
- package/dist/types/tool/index.d.ts.map +1 -1
- package/dist/types/tool/index.js.map +1 -1
- package/dist/utils/env.d.ts +19 -0
- package/dist/utils/env.d.ts.map +1 -0
- package/dist/utils/env.js +25 -0
- package/dist/utils/env.js.map +1 -0
- package/package.json +1 -1
- package/src/connector/mcp/adapter.ts +20 -6
- package/src/manager/run/persistence.ts +12 -0
- package/src/public-runtime.ts +9 -1
- package/src/public-tools.ts +18 -0
- package/src/registry/tool/execute.ts +2 -4
- package/src/registry/tool/portable.ts +264 -0
- package/src/registry/tool/schema.ts +38 -8
- package/src/registry/toolset/catalog.ts +8 -9
- package/src/runtime/jobs/awaited-jobs.ts +271 -0
- package/src/runtime/jobs/registry.ts +50 -0
- package/src/runtime/query/executor.ts +49 -1
- package/src/runtime/query/file-evidence-context.ts +190 -46
- package/src/runtime/query/file-evidence-replay.ts +776 -0
- package/src/runtime/query/file-evidence-seed.ts +126 -0
- package/src/runtime/query/index.ts +104 -2
- package/src/runtime/query/iteration/index.ts +202 -28
- package/src/runtime/query/iteration/phases/context.ts +10 -0
- package/src/runtime/query/iteration/phases/tool-review.ts +4 -0
- package/src/runtime/query/plugin-hooks.ts +20 -0
- package/src/runtime/query/repeat-call.ts +28 -18
- package/src/runtime/query/steering.ts +11 -0
- package/src/runtime/query/tooling.ts +3 -0
- package/src/scheduler/completion-inbox.ts +105 -9
- package/src/tools/builtins/bash.ts +4 -10
- package/src/tools/builtins/edit-apply.ts +456 -0
- package/src/tools/builtins/edit.ts +39 -270
- package/src/tools/builtins/index.ts +9 -3
- package/src/tools/builtins/job.ts +1 -1
- package/src/tools/builtins/read-file.ts +56 -77
- package/src/tools/builtins/read-render.ts +104 -0
- package/src/tools/builtins/wait-for-job-bounds.ts +179 -0
- package/src/tools/builtins/wait-for-job.ts +184 -0
- package/src/tools/builtins/write-file.ts +5 -0
- package/src/tools/coordinator/index.ts +1 -7
- package/src/tools/file-read-tracker.ts +85 -7
- package/src/types/message/index.ts +2 -0
- package/src/types/run/entity.ts +14 -0
- package/src/types/sandbox/index.ts +15 -14
- package/src/types/tool/index.ts +104 -0
- package/src/utils/env.ts +23 -0
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How `read` turns a file's body into the text the model sees.
|
|
3
|
+
*
|
|
4
|
+
* Pulled out of the tool for the reason `edit-apply` was: a second caller —
|
|
5
|
+
* the resume seed, deciding whether a `read` receipt in history shows exactly
|
|
6
|
+
* the body it already believes the file had — has to produce byte-for-byte
|
|
7
|
+
* what the tool produced, and a parallel implementation of the numbering or
|
|
8
|
+
* the window arithmetic would silently stop agreeing with it.
|
|
9
|
+
*
|
|
10
|
+
* Pure by construction: no filesystem access, no `ToolContext`.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Lines returned when the caller specifies no window.
|
|
15
|
+
*
|
|
16
|
+
* Chosen to cover the overwhelming majority of source files whole while
|
|
17
|
+
* bounding the pathological case.
|
|
18
|
+
*/
|
|
19
|
+
export const DEFAULT_READ_LINES = 2000
|
|
20
|
+
|
|
21
|
+
/** The window fields of `read`'s input; the path plays no part in rendering. */
|
|
22
|
+
export interface ReadWindowRequest {
|
|
23
|
+
/**
|
|
24
|
+
* `[start, end]`, 1-indexed inclusive.
|
|
25
|
+
*
|
|
26
|
+
* Typed as a number array rather than a pair because the tool's schema
|
|
27
|
+
* pins the length at two with `.length(2)` instead of a `z.tuple` — a
|
|
28
|
+
* tuple renders as draft-07 `items: [a, b]`, which a 2020-12 wire refuses.
|
|
29
|
+
* The parse still rejects any other length, so a caller reaching here has
|
|
30
|
+
* two numbers; `resolveReadWindow` reads them defensively anyway, because
|
|
31
|
+
* this interface is also implemented by hand elsewhere in the kernel.
|
|
32
|
+
*/
|
|
33
|
+
readonly readRange?: readonly number[]
|
|
34
|
+
readonly offset?: number
|
|
35
|
+
readonly limit?: number
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface RenderedRead {
|
|
39
|
+
/** Exactly what `read` returns as its output for this body and window. */
|
|
40
|
+
readonly output: string
|
|
41
|
+
readonly totalLines: number
|
|
42
|
+
readonly returnedLines: number
|
|
43
|
+
/** Whether the window left any of the file out, which is what adds the notice. */
|
|
44
|
+
readonly partial: boolean
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Render `content` the way `read` renders it.
|
|
49
|
+
*
|
|
50
|
+
* The numbering is what makes this reversible enough to compare against: every
|
|
51
|
+
* line goes out behind its own `${n}\t`, so two different bodies cannot render
|
|
52
|
+
* to one string, and the partial-view notice below — whose lines carry no such
|
|
53
|
+
* prefix — cannot be mistaken for part of a body.
|
|
54
|
+
*/
|
|
55
|
+
export function renderNumberedRead(content: string, input: ReadWindowRequest): RenderedRead {
|
|
56
|
+
const lines = content.split('\n')
|
|
57
|
+
const { start, end } = resolveReadWindow(input, lines.length)
|
|
58
|
+
const selected = lines.slice(start, end)
|
|
59
|
+
const numbered = selected.map((line, i) => `${start + i + 1}\t${line}`).join('\n')
|
|
60
|
+
return {
|
|
61
|
+
output: numbered + partialViewNotice(start, selected.length, lines.length),
|
|
62
|
+
totalLines: lines.length,
|
|
63
|
+
returnedLines: selected.length,
|
|
64
|
+
partial: selected.length < lines.length,
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Tell the model, explicitly, when it is looking at a window rather than
|
|
70
|
+
* the file.
|
|
71
|
+
*
|
|
72
|
+
* Without this a truncated read is indistinguishable from a short file, and
|
|
73
|
+
* the agent reasons about a fragment as if it were the whole thing — the
|
|
74
|
+
* most expensive silent failure a read tool can have. The notice names the
|
|
75
|
+
* exact next call rather than describing it.
|
|
76
|
+
*/
|
|
77
|
+
function partialViewNotice(start: number, returned: number, total: number): string {
|
|
78
|
+
if (returned >= total) return ''
|
|
79
|
+
const shownTo = start + returned
|
|
80
|
+
return [
|
|
81
|
+
'',
|
|
82
|
+
'',
|
|
83
|
+
`[PARTIAL view — lines ${start + 1}-${shownTo} of ${total}.`,
|
|
84
|
+
`Continue with read({ offset: ${shownTo}, limit: N }), or narrow with grep.]`,
|
|
85
|
+
].join('\n')
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function resolveReadWindow(
|
|
89
|
+
input: ReadWindowRequest,
|
|
90
|
+
totalLines: number,
|
|
91
|
+
): { start: number; end: number } {
|
|
92
|
+
const [first, last] = input.readRange ?? []
|
|
93
|
+
if (first !== undefined && last !== undefined) {
|
|
94
|
+
const start = Math.max(0, first - 1)
|
|
95
|
+
const end = Math.min(totalLines, Math.max(start, last))
|
|
96
|
+
return { start, end }
|
|
97
|
+
}
|
|
98
|
+
const start = Math.max(0, input.offset ?? 0)
|
|
99
|
+
// A bare `read({ path })` used to return the entire file, so a 2 MB
|
|
100
|
+
// lockfile became ~500k tokens in one tool_result. Default to a window
|
|
101
|
+
// and say so in the output; the model asks for more when it needs it.
|
|
102
|
+
const end = input.limit ? start + input.limit : Math.min(totalLines, start + DEFAULT_READ_LINES)
|
|
103
|
+
return { start, end }
|
|
104
|
+
}
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import type { BackgroundJobRegistryRef } from '../../types/tool/index.js'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Waiting on a background job, bounded by two different questions — the
|
|
5
|
+
* shell-job counterpart to `waitForTaskWithBounds`
|
|
6
|
+
* (`../coordinator/wait-with-idle-bound.ts`), which this mirrors.
|
|
7
|
+
*
|
|
8
|
+
* A delegated agent task reports its own progress through
|
|
9
|
+
* `TaskScheduler.onTaskProgress`. A shell job has no such channel: the only
|
|
10
|
+
* signal it ever produces is bytes on stdout/stderr. So where the task
|
|
11
|
+
* version resets its idle clock on a progress EVENT, this one resets it on
|
|
12
|
+
* OBSERVED OUTPUT GROWTH — read on every tick, compared against the last
|
|
13
|
+
* tick's offset. A tick that finds nothing new is not progress; treating it
|
|
14
|
+
* as progress would make the idle bound unable to fire for a wedged job at
|
|
15
|
+
* all, which is the exact failure this exists to catch.
|
|
16
|
+
*
|
|
17
|
+
* - the **run bound** counts elapsed time and is never refreshed. It
|
|
18
|
+
* exists for a job that stays busy forever (a server, a stuck build).
|
|
19
|
+
* - the **idle bound** counts time since output last grew, and resets
|
|
20
|
+
* whenever it does. It exists for a job that stopped producing anything
|
|
21
|
+
* without exiting.
|
|
22
|
+
*
|
|
23
|
+
* Neither bound cancels the job. A wait that ran out is a statement about
|
|
24
|
+
* the WAITER, not the work — the job keeps running, and its output is still
|
|
25
|
+
* there to read with `job` or a later `wait_for_job` call.
|
|
26
|
+
*/
|
|
27
|
+
export interface JobWaitOptions {
|
|
28
|
+
/** Elapsed-time ceiling, never refreshed. */
|
|
29
|
+
readonly runMs: number
|
|
30
|
+
/**
|
|
31
|
+
* Time-without-new-output ceiling, refreshed whenever `read` returns
|
|
32
|
+
* more than it did last tick. Omit to bound by the run clock alone.
|
|
33
|
+
*/
|
|
34
|
+
readonly idleMs?: number
|
|
35
|
+
/** Resume from here rather than the start of what the job has retained. */
|
|
36
|
+
readonly fromOffset?: number
|
|
37
|
+
/** Stop preempts a wait that has not resolved yet; the job is untouched. */
|
|
38
|
+
readonly signal?: AbortSignal
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
interface JobWaitProgress {
|
|
42
|
+
readonly output: string
|
|
43
|
+
readonly nextOffset: number
|
|
44
|
+
readonly droppedBytes: number
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export type JobWaitOutcome =
|
|
48
|
+
| ({
|
|
49
|
+
readonly kind: 'exited'
|
|
50
|
+
readonly status: string
|
|
51
|
+
readonly exitCode?: number
|
|
52
|
+
} & JobWaitProgress)
|
|
53
|
+
| ({
|
|
54
|
+
readonly kind: 'timeout'
|
|
55
|
+
/** Which clock ran out. */
|
|
56
|
+
readonly cause: 'idle' | 'run'
|
|
57
|
+
readonly elapsedMs: number
|
|
58
|
+
} & JobWaitProgress)
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* How often the bounds are checked, and how often output is drained.
|
|
62
|
+
*
|
|
63
|
+
* Coarse on purpose, same reasoning as the task version's own interval:
|
|
64
|
+
* both bounds are measured in minutes, so a second of latency noticing
|
|
65
|
+
* either one is irrelevant, and this is an internal timer, not a model
|
|
66
|
+
* turn — it costs nothing external no matter how often it fires.
|
|
67
|
+
*/
|
|
68
|
+
const POLL_INTERVAL_MS = 1_000
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Await a job under both bounds, accumulating its output as it goes.
|
|
72
|
+
*
|
|
73
|
+
* Returns the output gathered so far either way: a completed wait has all
|
|
74
|
+
* of it, and a timed-out one has everything read up to the moment it gave
|
|
75
|
+
* up, so the caller never has to throw away a partial answer.
|
|
76
|
+
*/
|
|
77
|
+
export async function waitForJobWithBounds(
|
|
78
|
+
jobs: Pick<BackgroundJobRegistryRef, 'read' | 'waitForExit'>,
|
|
79
|
+
id: string,
|
|
80
|
+
options: JobWaitOptions,
|
|
81
|
+
now: () => number = Date.now,
|
|
82
|
+
): Promise<JobWaitOutcome> {
|
|
83
|
+
if (!jobs.waitForExit) {
|
|
84
|
+
throw new Error('This background job registry cannot wait for a job to exit.')
|
|
85
|
+
}
|
|
86
|
+
const waitForExit = jobs.waitForExit.bind(jobs)
|
|
87
|
+
|
|
88
|
+
const startedAt = now()
|
|
89
|
+
let lastProgressAt = startedAt
|
|
90
|
+
let cursor = options.fromOffset ?? 0
|
|
91
|
+
let output = ''
|
|
92
|
+
let droppedBytes = 0
|
|
93
|
+
let settled = false
|
|
94
|
+
|
|
95
|
+
/** Read whatever is new since `cursor`, and count it as progress if it is. */
|
|
96
|
+
const drain = (): void => {
|
|
97
|
+
const chunk = jobs.read(id, { fromOffset: cursor })
|
|
98
|
+
if (chunk.droppedBytes > 0) droppedBytes += chunk.droppedBytes
|
|
99
|
+
if (chunk.nextOffset > cursor) lastProgressAt = now()
|
|
100
|
+
cursor = chunk.nextOffset
|
|
101
|
+
if (chunk.chunk) output += chunk.chunk
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
try {
|
|
105
|
+
const exited = waitForExit(id, { signal: options.signal }).then((job): JobWaitOutcome => {
|
|
106
|
+
// One last read: the job can exit between ticks, and the bytes
|
|
107
|
+
// it wrote in its final moment are exactly the ones a caller
|
|
108
|
+
// most wants — the error, the summary line, the exit trace.
|
|
109
|
+
drain()
|
|
110
|
+
return {
|
|
111
|
+
kind: 'exited',
|
|
112
|
+
status: job.status,
|
|
113
|
+
...(job.exitCode === undefined ? {} : { exitCode: job.exitCode }),
|
|
114
|
+
output,
|
|
115
|
+
nextOffset: cursor,
|
|
116
|
+
droppedBytes,
|
|
117
|
+
}
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
const expiry = new Promise<JobWaitOutcome>((resolve) => {
|
|
121
|
+
// Polled rather than scheduled, for the same reason the task
|
|
122
|
+
// version is: the idle deadline MOVES on every byte of new
|
|
123
|
+
// output, and a timer armed for it would have to be cleared and
|
|
124
|
+
// rearmed on every tick that mattered.
|
|
125
|
+
const tick = setInterval(() => {
|
|
126
|
+
if (settled) return
|
|
127
|
+
drain()
|
|
128
|
+
const elapsed = now() - startedAt
|
|
129
|
+
if (elapsed >= options.runMs) {
|
|
130
|
+
clearInterval(tick)
|
|
131
|
+
resolve({
|
|
132
|
+
kind: 'timeout',
|
|
133
|
+
cause: 'run',
|
|
134
|
+
elapsedMs: elapsed,
|
|
135
|
+
output,
|
|
136
|
+
nextOffset: cursor,
|
|
137
|
+
droppedBytes,
|
|
138
|
+
})
|
|
139
|
+
return
|
|
140
|
+
}
|
|
141
|
+
if (options.idleMs !== undefined) {
|
|
142
|
+
const quietFor = now() - lastProgressAt
|
|
143
|
+
if (quietFor >= options.idleMs) {
|
|
144
|
+
clearInterval(tick)
|
|
145
|
+
resolve({
|
|
146
|
+
kind: 'timeout',
|
|
147
|
+
cause: 'idle',
|
|
148
|
+
elapsedMs: elapsed,
|
|
149
|
+
output,
|
|
150
|
+
nextOffset: cursor,
|
|
151
|
+
droppedBytes,
|
|
152
|
+
})
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}, POLL_INTERVAL_MS)
|
|
156
|
+
// Never the reason a process stays alive — this races a real
|
|
157
|
+
// exit promise, so the wait is held open by work that is
|
|
158
|
+
// genuinely outstanding rather than by this timer.
|
|
159
|
+
;(tick as { unref?: () => void }).unref?.()
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
return await Promise.race([exited, expiry])
|
|
163
|
+
} finally {
|
|
164
|
+
settled = true
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/** What to tell the model, in the words that fit what actually happened. */
|
|
169
|
+
export function describeJobWaitTimeout(
|
|
170
|
+
id: string,
|
|
171
|
+
outcome: Extract<JobWaitOutcome, { kind: 'timeout' }>,
|
|
172
|
+
): string {
|
|
173
|
+
const seconds = Math.round(outcome.elapsedMs / 1000)
|
|
174
|
+
const resume = `call wait_for_job again, or job read with from_offset ${outcome.nextOffset}, to see what it does next`
|
|
175
|
+
if (outcome.cause === 'idle') {
|
|
176
|
+
return `Job ${id} went quiet: no new output for a while, after ${seconds}s. It has not been stopped and may still be working — ${resume}.`
|
|
177
|
+
}
|
|
178
|
+
return `Job ${id} has been running for ${seconds}s without finishing. It has not been stopped — ${resume}.`
|
|
179
|
+
}
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import { z } from 'zod'
|
|
2
|
+
|
|
3
|
+
import { readPositiveIntEnv } from '../../utils/env.js'
|
|
4
|
+
import { defineTool } from '../defineTool.js'
|
|
5
|
+
import { describeJobWaitTimeout, waitForJobWithBounds } from './wait-for-job-bounds.js'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Block until a background job ends, instead of reading it in a loop.
|
|
9
|
+
*
|
|
10
|
+
* `job`'s own tool used to say, in as many words, "poll a job with action
|
|
11
|
+
* read" — and a model that took the instruction literally paid for it: one
|
|
12
|
+
* recorded run launched a single background job, then spent six `job read`
|
|
13
|
+
* calls, three `job list` calls and an improvised `sleep 30` waiting on it,
|
|
14
|
+
* burning more tokens on the wait than the work it was waiting for cost.
|
|
15
|
+
* `wait_for_task` already solved this for delegated agent work by blocking
|
|
16
|
+
* inside ONE tool call instead of asking the model to check back; this is
|
|
17
|
+
* the same fix for shell jobs, built the same way — see
|
|
18
|
+
* `wait-for-job-bounds.ts` and its task-surface counterpart.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
const DEFAULT_TIMEOUT_MS = readPositiveIntEnv('NAMZU_JOB_WAIT_TIMEOUT_MS', 5 * 60 * 1000)
|
|
22
|
+
const DEFAULT_IDLE_TIMEOUT_MS = readPositiveIntEnv('NAMZU_JOB_WAIT_IDLE_MS', 2 * 60 * 1000)
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* The longest either bound will accept from the model.
|
|
26
|
+
*
|
|
27
|
+
* Same number `wait_for_task` uses for a delegated agent, and the same
|
|
28
|
+
* reasoning applies: a generic stopwatch is the wrong instrument for a job
|
|
29
|
+
* that is making progress, so this is "how long is too long" rather than a
|
|
30
|
+
* guess at any one job's real duration, and a request past it is REFUSED by
|
|
31
|
+
* the schema rather than silently clamped — see `bash`'s own `timeout` field
|
|
32
|
+
* for the same trade.
|
|
33
|
+
*/
|
|
34
|
+
const MAX_WAIT_MS = readPositiveIntEnv('NAMZU_JOB_WAIT_MAX_MS', 60 * 60 * 1000)
|
|
35
|
+
|
|
36
|
+
const inputSchema = z.object({
|
|
37
|
+
id: z.string().describe('The job id, as returned by bash with run_in_background.'),
|
|
38
|
+
timeout_ms: z
|
|
39
|
+
.number()
|
|
40
|
+
.int()
|
|
41
|
+
.positive()
|
|
42
|
+
.max(MAX_WAIT_MS)
|
|
43
|
+
.optional()
|
|
44
|
+
.describe(
|
|
45
|
+
`Give up after this long even if the job keeps producing output, in milliseconds. Default: ${DEFAULT_TIMEOUT_MS}, maximum: ${MAX_WAIT_MS}. The job is never stopped by this running out.`,
|
|
46
|
+
),
|
|
47
|
+
idle_timeout_ms: z
|
|
48
|
+
.number()
|
|
49
|
+
.int()
|
|
50
|
+
.positive()
|
|
51
|
+
.max(MAX_WAIT_MS)
|
|
52
|
+
.optional()
|
|
53
|
+
.describe(
|
|
54
|
+
`Give up if the job produces no new output for this long, in milliseconds. Default: ${DEFAULT_IDLE_TIMEOUT_MS}. Resets on every new byte of output, so a job that is still working is not cut off; only real silence ends the wait early.`,
|
|
55
|
+
),
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
type WaitForJobInput = z.infer<typeof inputSchema>
|
|
59
|
+
|
|
60
|
+
export const WaitForJobTool = defineTool({
|
|
61
|
+
name: 'wait_for_job',
|
|
62
|
+
description:
|
|
63
|
+
'Block until a background job started by bash with run_in_background ends, and return its accumulated output in one call. Use this instead of calling job with action "read" in a loop: it costs one call and no waiting turns. Gives up — WITHOUT stopping the job — if it runs too long or goes quiet for too long; either outcome says so, and the job keeps running either way.',
|
|
64
|
+
inputSchema,
|
|
65
|
+
category: 'shell',
|
|
66
|
+
permissions: ['shell_execute'],
|
|
67
|
+
// Waiting is watching, not acting: this tool only ever reads a job's
|
|
68
|
+
// output and status through the same registry `job read` uses, and has
|
|
69
|
+
// no branch that touches a job's lifetime. `wait_for_task` is `readOnly:
|
|
70
|
+
// true` for the identical reason, and unlike `job` there is no second
|
|
71
|
+
// action here that would make this a function of input.
|
|
72
|
+
readOnly: true,
|
|
73
|
+
destructive: false,
|
|
74
|
+
concurrencySafe: true,
|
|
75
|
+
// A tool whose whole purpose is to wait must not be cut off for waiting.
|
|
76
|
+
// A margin over MAX_WAIT_MS so the tool's own bound — which reports a
|
|
77
|
+
// clean timeout result — always fires before the executor's harsher
|
|
78
|
+
// "abandoned" one would.
|
|
79
|
+
timeoutMs: MAX_WAIT_MS + 30_000,
|
|
80
|
+
|
|
81
|
+
presentCall(input) {
|
|
82
|
+
return {
|
|
83
|
+
kind: 'generic',
|
|
84
|
+
presentation: 'activity',
|
|
85
|
+
label: `Wait for background job · ${input.id ?? '(missing id)'}`,
|
|
86
|
+
}
|
|
87
|
+
},
|
|
88
|
+
|
|
89
|
+
async execute(input: WaitForJobInput, context) {
|
|
90
|
+
if (!context.backgroundJobs) {
|
|
91
|
+
return {
|
|
92
|
+
success: false,
|
|
93
|
+
output: '',
|
|
94
|
+
error: 'This host provides no background job registry, so there is no job to wait for.',
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
const jobs = context.backgroundJobs
|
|
98
|
+
|
|
99
|
+
try {
|
|
100
|
+
jobs.get(input.id)
|
|
101
|
+
} catch (err) {
|
|
102
|
+
return {
|
|
103
|
+
success: false,
|
|
104
|
+
output: '',
|
|
105
|
+
error: err instanceof Error ? err.message : String(err),
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (!jobs.waitForExit) {
|
|
110
|
+
return {
|
|
111
|
+
success: false,
|
|
112
|
+
output: '',
|
|
113
|
+
error:
|
|
114
|
+
'This host\'s background job registry cannot wait for a job to exit. Use job with action "read" instead.',
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// The wait IS the intent, so it is recorded before the bounds are: a
|
|
119
|
+
// call that gives up at `timeout_ms` with the job still running is the
|
|
120
|
+
// case the kernel's hold exists to back up, and one that started the
|
|
121
|
+
// wait and then ended its turn is the same case a beat later. Nothing
|
|
122
|
+
// infers this from the job's existence — a dev server the model never
|
|
123
|
+
// waited on never holds a run open. See `runtime/jobs/awaited-jobs.ts`.
|
|
124
|
+
jobs.markAwaited?.(input.id)
|
|
125
|
+
|
|
126
|
+
let outcome: Awaited<ReturnType<typeof waitForJobWithBounds>>
|
|
127
|
+
try {
|
|
128
|
+
outcome = await waitForJobWithBounds(jobs, input.id, {
|
|
129
|
+
runMs: input.timeout_ms ?? DEFAULT_TIMEOUT_MS,
|
|
130
|
+
idleMs: input.idle_timeout_ms ?? DEFAULT_IDLE_TIMEOUT_MS,
|
|
131
|
+
signal: context.abortSignal,
|
|
132
|
+
})
|
|
133
|
+
} catch {
|
|
134
|
+
// The signal fired before either bound did — Stop, or the run's
|
|
135
|
+
// own deadline. The job is untouched: ending a WAIT is not
|
|
136
|
+
// ending the WORK. The executor has already raced this same
|
|
137
|
+
// signal against the whole call and reports the cancellation
|
|
138
|
+
// itself; this only keeps that rejection from reaching the
|
|
139
|
+
// model as an unlabelled tool failure.
|
|
140
|
+
return {
|
|
141
|
+
success: false,
|
|
142
|
+
output: `This wait for job ${input.id} was abandoned before it finished; it is still running and was not stopped. Call wait_for_job again, or job read with from_offset, to pick up where this left off.`,
|
|
143
|
+
data: { jobId: input.id, abandoned: true },
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (outcome.kind === 'timeout') {
|
|
148
|
+
return {
|
|
149
|
+
success: false,
|
|
150
|
+
output: describeJobWaitTimeout(input.id, outcome),
|
|
151
|
+
data: {
|
|
152
|
+
jobId: input.id,
|
|
153
|
+
timedOut: outcome.cause,
|
|
154
|
+
nextOffset: outcome.nextOffset,
|
|
155
|
+
droppedBytes: outcome.droppedBytes,
|
|
156
|
+
},
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// The dropped count is stated, never absorbed — same rule `job read`
|
|
161
|
+
// follows, for the same reason: a job whose middle vanished quietly
|
|
162
|
+
// reads as a complete result that happens to be short.
|
|
163
|
+
const notice =
|
|
164
|
+
outcome.droppedBytes > 0
|
|
165
|
+
? `[${outcome.droppedBytes} bytes were dropped before this point — the job produced output faster than the retention cap holds]\n`
|
|
166
|
+
: ''
|
|
167
|
+
const status =
|
|
168
|
+
outcome.exitCode === undefined
|
|
169
|
+
? outcome.status
|
|
170
|
+
: `${outcome.status} with code ${outcome.exitCode}`
|
|
171
|
+
|
|
172
|
+
return {
|
|
173
|
+
success: true,
|
|
174
|
+
output: `${notice}${outcome.output || '(no output)'}\n\n[job ${input.id} is ${status}; next_offset ${outcome.nextOffset}]`,
|
|
175
|
+
data: {
|
|
176
|
+
jobId: input.id,
|
|
177
|
+
status: outcome.status,
|
|
178
|
+
nextOffset: outcome.nextOffset,
|
|
179
|
+
droppedBytes: outcome.droppedBytes,
|
|
180
|
+
...(outcome.exitCode === undefined ? {} : { exitCode: outcome.exitCode }),
|
|
181
|
+
},
|
|
182
|
+
}
|
|
183
|
+
},
|
|
184
|
+
})
|
|
@@ -204,6 +204,11 @@ function enforceFreshOverwrite(
|
|
|
204
204
|
// fingerprint the host never promised to capture.
|
|
205
205
|
const observed = context.fileReadTracker?.fingerprint?.(key)
|
|
206
206
|
if (observed !== undefined && observed !== fingerprintContent(currentContent)) {
|
|
207
|
+
// The disk has just been read and found moved. Say so — without saying
|
|
208
|
+
// what was found, which would re-baseline the comparison right above —
|
|
209
|
+
// so a reader that cannot touch the filesystem stops referencing this
|
|
210
|
+
// path. Optional on the interface; an older tracker keeps its behavior.
|
|
211
|
+
context.fileReadTracker?.recordDriftObserved?.(key)
|
|
207
212
|
return { success: false, output: '', error: staleFileError(key, 'write') }
|
|
208
213
|
}
|
|
209
214
|
return null
|
|
@@ -8,6 +8,7 @@ import type { ResumeHandler } from '../../types/hitl/index.js'
|
|
|
8
8
|
import type { RunId, TaskId } from '../../types/ids/index.js'
|
|
9
9
|
import type { TaskStore } from '../../types/task/index.js'
|
|
10
10
|
import type { ToolDefinition } from '../../types/tool/index.js'
|
|
11
|
+
import { readPositiveIntEnv } from '../../utils/env.js'
|
|
11
12
|
import { toErrorMessage } from '../../utils/error.js'
|
|
12
13
|
import { asTaskId } from '../../utils/id.js'
|
|
13
14
|
import { defineTool } from '../defineTool.js'
|
|
@@ -374,13 +375,6 @@ export const DELEGATION_TIMEOUT_MS = 60 * 60 * 1000
|
|
|
374
375
|
*/
|
|
375
376
|
export const DELEGATION_IDLE_MS = readPositiveIntEnv('NAMZU_DELEGATION_IDLE_MS', 5 * 60 * 1000)
|
|
376
377
|
|
|
377
|
-
function readPositiveIntEnv(key: string, fallback: number): number {
|
|
378
|
-
const value = process.env[key]?.trim()
|
|
379
|
-
if (!value) return fallback
|
|
380
|
-
const parsed = Number(value)
|
|
381
|
-
return Number.isSafeInteger(parsed) && parsed > 0 ? parsed : fallback
|
|
382
|
-
}
|
|
383
|
-
|
|
384
378
|
/**
|
|
385
379
|
* The answer a delegated child produced, as the string a parent model reads.
|
|
386
380
|
*
|
|
@@ -6,17 +6,95 @@ export function createFileReadTracker(): FileReadTracker {
|
|
|
6
6
|
const paths = new Set<string>()
|
|
7
7
|
const fingerprints = new Map<string, string>()
|
|
8
8
|
const writes = new Map<string, string>()
|
|
9
|
+
// Edits stacked on top of `writes.get(key)`, in order. Kept beside the
|
|
10
|
+
// write id rather than replacing it, because the write is the base a
|
|
11
|
+
// replay starts from and is still needed once edits sit on it — while
|
|
12
|
+
// `writeCallId` must stop reporting it the moment the file's body stops
|
|
13
|
+
// being that call's body.
|
|
14
|
+
const chains = new Map<string, string[]>()
|
|
15
|
+
// Reads whose receipt still holds the whole body, with the fingerprint of
|
|
16
|
+
// the rendering that receipt is supposed to be. Kept apart from `writes`
|
|
17
|
+
// rather than folded into it: a write's body is the model's own argument
|
|
18
|
+
// and an edit can be replayed onto it, while this one exists only as text
|
|
19
|
+
// formatted for a reader — so it roots nothing, and the rendering is the
|
|
20
|
+
// only thing anyone is allowed to check it against.
|
|
21
|
+
const reads = new Map<string, { callId: string; renderedFingerprint: string }>()
|
|
22
|
+
// Paths a built-in mutation refused for drift. A flag and not an
|
|
23
|
+
// observation: the refusing tool did read the disk, but it reported only
|
|
24
|
+
// that what it found differs — writing a fingerprint from it would
|
|
25
|
+
// re-baseline the very check that refused and let the next mutation
|
|
26
|
+
// through. So this touches nothing `fingerprint`, `hasRead`, `writeCallId`,
|
|
27
|
+
// `editChain` or `readWitness` answers; it says only that what this ledger
|
|
28
|
+
// holds for the path is known to be behind the disk.
|
|
29
|
+
const drifted = new Set<string>()
|
|
30
|
+
// One observation, whatever witness the caller goes on to record against it.
|
|
31
|
+
// A shared function rather than `this.recordRead`, so a destructured method
|
|
32
|
+
// keeps working.
|
|
33
|
+
function observe(key: string, content?: string, fullWriteCallId?: string): void {
|
|
34
|
+
paths.add(key)
|
|
35
|
+
drifted.delete(key)
|
|
36
|
+
const next = content === undefined ? undefined : fingerprintContent(content)
|
|
37
|
+
if (next === undefined || next !== fingerprints.get(key)) {
|
|
38
|
+
writes.delete(key)
|
|
39
|
+
chains.delete(key)
|
|
40
|
+
reads.delete(key)
|
|
41
|
+
}
|
|
42
|
+
if (next !== undefined) fingerprints.set(key, next)
|
|
43
|
+
else fingerprints.delete(key)
|
|
44
|
+
if (next !== undefined && fullWriteCallId) {
|
|
45
|
+
writes.set(key, fullWriteCallId)
|
|
46
|
+
// A full body arrived whole in one call, so the chain is empty
|
|
47
|
+
// again: nothing has been applied on top of what is now on disk.
|
|
48
|
+
chains.delete(key)
|
|
49
|
+
}
|
|
50
|
+
}
|
|
9
51
|
return {
|
|
10
|
-
recordRead
|
|
52
|
+
recordRead: observe,
|
|
53
|
+
recordFullRead(key, content, callId, renderedFingerprint) {
|
|
54
|
+
observe(key, content)
|
|
55
|
+
// A surviving write witness outranks this one: it names a body the
|
|
56
|
+
// model composed itself, and the chain machinery can replay onto it.
|
|
57
|
+
// This is the same observation either way — only the witness differs.
|
|
58
|
+
if (!writes.has(key)) reads.set(key, { callId, renderedFingerprint })
|
|
59
|
+
},
|
|
60
|
+
readWitness: (key) => reads.get(key),
|
|
61
|
+
recordEdit(key, content, callId) {
|
|
62
|
+
// Both conditions are about the PRE-image, not this edit. A chain
|
|
63
|
+
// replays only if the content this edit ran against is itself
|
|
64
|
+
// reproducible: there has to be a witnessed write underneath it, and
|
|
65
|
+
// a fingerprint for what the edit actually started from — which is
|
|
66
|
+
// also the value the tool's own drift check compared before writing.
|
|
67
|
+
// Without both, this is an observation of a body nobody can replay.
|
|
68
|
+
const chained = writes.has(key) && fingerprints.has(key) && callId.length > 0
|
|
11
69
|
paths.add(key)
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
70
|
+
drifted.delete(key)
|
|
71
|
+
// Unconditionally, even where the content is unchanged: a read
|
|
72
|
+
// witness points at a receipt showing the body BEFORE this edit, and
|
|
73
|
+
// no chain can be rooted at a rendering to carry it forward.
|
|
74
|
+
reads.delete(key)
|
|
75
|
+
fingerprints.set(key, fingerprintContent(content))
|
|
76
|
+
if (!chained) {
|
|
77
|
+
writes.delete(key)
|
|
78
|
+
chains.delete(key)
|
|
79
|
+
return
|
|
80
|
+
}
|
|
81
|
+
chains.set(key, [...(chains.get(key) ?? []), callId])
|
|
17
82
|
},
|
|
83
|
+
recordDriftObserved(key) {
|
|
84
|
+
drifted.add(key)
|
|
85
|
+
},
|
|
86
|
+
driftObserved: (key) => drifted.has(key),
|
|
18
87
|
hasRead: (key) => paths.has(key),
|
|
19
88
|
fingerprint: (key) => fingerprints.get(key),
|
|
20
|
-
|
|
89
|
+
// Withheld while edits sit on top of it. A consumer that knows only this
|
|
90
|
+
// method asks "is the current body the body of a call I can see?", and
|
|
91
|
+
// once an edit has landed the honest answer is no.
|
|
92
|
+
writeCallId: (key) => (chains.has(key) ? undefined : writes.get(key)),
|
|
93
|
+
editChain: (key) => {
|
|
94
|
+
const editCallIds = chains.get(key)
|
|
95
|
+
const rootWriteCallId = writes.get(key)
|
|
96
|
+
if (!editCallIds?.length || rootWriteCallId === undefined) return undefined
|
|
97
|
+
return { rootWriteCallId, editCallIds: [...editCallIds] }
|
|
98
|
+
},
|
|
21
99
|
}
|
|
22
100
|
}
|
package/src/types/run/entity.ts
CHANGED
|
@@ -123,6 +123,20 @@ export interface Run {
|
|
|
123
123
|
*/
|
|
124
124
|
abandonedTaskIds?: readonly string[]
|
|
125
125
|
|
|
126
|
+
/**
|
|
127
|
+
* Background jobs the model was waiting on that were still running when
|
|
128
|
+
* this run ended.
|
|
129
|
+
*
|
|
130
|
+
* Only jobs `wait_for_job` named: a dev server nobody awaited is not work
|
|
131
|
+
* this run walked away from, it is work it deliberately left behind. The
|
|
132
|
+
* run holds itself open for these, bounded, and names the ones the bound
|
|
133
|
+
* ran out on — the same honesty `abandonedTaskIds` owes for a delegated
|
|
134
|
+
* worker, and with the same limit: naming a job is not stopping it. A
|
|
135
|
+
* run-owned job is still stopped by the run's own teardown; one bound to
|
|
136
|
+
* the host's session keeps running, which is what it was started for.
|
|
137
|
+
*/
|
|
138
|
+
abandonedJobIds?: readonly string[]
|
|
139
|
+
|
|
126
140
|
parentRunId?: RunId
|
|
127
141
|
|
|
128
142
|
depth?: number
|
|
@@ -296,22 +296,23 @@ export interface Sandbox {
|
|
|
296
296
|
* Open a real pseudo-terminal whose complete process tree is confined to
|
|
297
297
|
* and owned by this sandbox.
|
|
298
298
|
*
|
|
299
|
-
* Optional, and a backend that cannot provide one must **
|
|
300
|
-
* than hand back a pipe — the same
|
|
301
|
-
*
|
|
302
|
-
* work: bytes would flow, and every
|
|
303
|
-
* take its non-interactive branch. The
|
|
304
|
-
* exits immediately, the progress bar
|
|
305
|
-
* nothing says why.
|
|
299
|
+
* Optional, and a backend that cannot provide one must **omit this
|
|
300
|
+
* method** rather than hand back a pipe — the same skip-if-unavailable
|
|
301
|
+
* rule {@link Sandbox.openTcpConnection} states below, and for a sharper
|
|
302
|
+
* reason. A pipe would appear to work: bytes would flow, and every
|
|
303
|
+
* program that calls `isatty` would take its non-interactive branch. The
|
|
304
|
+
* prompt never appears, the REPL exits immediately, the progress bar
|
|
305
|
+
* prints ten thousand lines, and nothing says why.
|
|
306
306
|
*
|
|
307
|
-
* A backend that
|
|
308
|
-
* await every terminal it returned. Merely starting a host
|
|
309
|
-
* with `rootDir` as its working directory does not
|
|
310
|
-
* confinement or the ownership contract.
|
|
307
|
+
* A backend that DOES implement this method MUST make {@link destroy}
|
|
308
|
+
* kill and await every terminal it returned. Merely starting a host
|
|
309
|
+
* pseudo-terminal with `rootDir` as its working directory does not
|
|
310
|
+
* satisfy either the confinement or the ownership contract.
|
|
311
311
|
*
|
|
312
|
-
* The Firecracker backend satisfies both guarantees by owning the PTY in
|
|
313
|
-
* guest and awaiting its exit before the microVM is released.
|
|
314
|
-
* cannot provide that boundary omit the capability
|
|
312
|
+
* The Firecracker backend satisfies both guarantees by owning the PTY in
|
|
313
|
+
* the guest and awaiting its exit before the microVM is released.
|
|
314
|
+
* Backends that cannot provide that boundary omit the capability, as
|
|
315
|
+
* stated above.
|
|
315
316
|
*/
|
|
316
317
|
openTerminal?(options: OpenTerminalOptions): Promise<TerminalSession>
|
|
317
318
|
/**
|