@namzu/sdk 58.1.1 → 60.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 +124 -0
- package/dist/account/binding.d.ts +20 -0
- package/dist/account/binding.d.ts.map +1 -0
- package/dist/account/binding.js +27 -0
- package/dist/account/binding.js.map +1 -0
- package/dist/account/index.d.ts +9 -0
- package/dist/account/index.d.ts.map +1 -0
- package/dist/account/index.js +5 -0
- package/dist/account/index.js.map +1 -0
- package/dist/account/lock.d.ts +33 -0
- package/dist/account/lock.d.ts.map +1 -0
- package/dist/account/lock.js +170 -0
- package/dist/account/lock.js.map +1 -0
- package/dist/account/registry.d.ts +21 -0
- package/dist/account/registry.d.ts.map +1 -0
- package/dist/account/registry.js +340 -0
- package/dist/account/registry.js.map +1 -0
- package/dist/account/types.d.ts +124 -0
- package/dist/account/types.d.ts.map +1 -0
- package/dist/account/types.js +35 -0
- package/dist/account/types.js.map +1 -0
- package/dist/compaction/context-window.d.ts +53 -0
- package/dist/compaction/context-window.d.ts.map +1 -1
- package/dist/compaction/context-window.js +141 -2
- package/dist/compaction/context-window.js.map +1 -1
- package/dist/execution/workflow/index.d.ts +3 -1
- package/dist/execution/workflow/index.d.ts.map +1 -1
- package/dist/execution/workflow/index.js +2 -1
- package/dist/execution/workflow/index.js.map +1 -1
- package/dist/execution/workflow/journal.d.ts +24 -4
- package/dist/execution/workflow/journal.d.ts.map +1 -1
- package/dist/execution/workflow/journal.js +131 -9
- package/dist/execution/workflow/journal.js.map +1 -1
- package/dist/execution/workflow/prelude.d.ts +2 -0
- package/dist/execution/workflow/prelude.d.ts.map +1 -1
- package/dist/execution/workflow/prelude.js +49 -8
- package/dist/execution/workflow/prelude.js.map +1 -1
- package/dist/execution/workflow/resume.d.ts +59 -0
- package/dist/execution/workflow/resume.d.ts.map +1 -0
- package/dist/execution/workflow/resume.js +273 -0
- package/dist/execution/workflow/resume.js.map +1 -0
- package/dist/execution/workflow/runtime.d.ts.map +1 -1
- package/dist/execution/workflow/runtime.js +112 -8
- package/dist/execution/workflow/runtime.js.map +1 -1
- package/dist/execution/workflow/types.d.ts +105 -1
- package/dist/execution/workflow/types.d.ts.map +1 -1
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -1
- package/dist/pals/communication/ingress-schema.d.ts +38 -38
- package/dist/peers/record.d.ts +98 -0
- package/dist/peers/record.d.ts.map +1 -1
- package/dist/peers/record.js +17 -0
- package/dist/peers/record.js.map +1 -1
- package/dist/peers/registry.d.ts.map +1 -1
- package/dist/peers/registry.js +2 -2
- package/dist/peers/registry.js.map +1 -1
- package/dist/plugin/compat.d.ts +24 -0
- package/dist/plugin/compat.d.ts.map +1 -0
- package/dist/plugin/compat.js +149 -0
- package/dist/plugin/compat.js.map +1 -0
- package/dist/plugin/loader.d.ts +2 -1
- package/dist/plugin/loader.d.ts.map +1 -1
- package/dist/plugin/loader.js +6 -0
- package/dist/plugin/loader.js.map +1 -1
- package/dist/pricing/index.d.ts.map +1 -1
- package/dist/pricing/index.js +2 -1
- package/dist/pricing/index.js.map +1 -1
- package/dist/process/kill-tree.d.ts +44 -0
- package/dist/process/kill-tree.d.ts.map +1 -1
- package/dist/process/kill-tree.js +102 -4
- package/dist/process/kill-tree.js.map +1 -1
- package/dist/prompt/coding-agent-doctrine.d.ts +1 -1
- package/dist/prompt/coding-agent-doctrine.d.ts.map +1 -1
- package/dist/prompt/coding-agent-doctrine.js +9 -1
- package/dist/prompt/coding-agent-doctrine.js.map +1 -1
- package/dist/provider/errors.d.ts.map +1 -1
- package/dist/provider/errors.js +25 -2
- package/dist/provider/errors.js.map +1 -1
- package/dist/provider/model-window-suffix.d.ts +18 -0
- package/dist/provider/model-window-suffix.d.ts.map +1 -0
- package/dist/provider/model-window-suffix.js +77 -0
- package/dist/provider/model-window-suffix.js.map +1 -0
- package/dist/provider/token-budget.d.ts.map +1 -1
- package/dist/provider/token-budget.js +9 -1
- package/dist/provider/token-budget.js.map +1 -1
- package/dist/public-runtime.d.ts +6 -3
- package/dist/public-runtime.d.ts.map +1 -1
- package/dist/public-runtime.js +6 -4
- package/dist/public-runtime.js.map +1 -1
- package/dist/public-types.d.ts +3 -0
- package/dist/public-types.d.ts.map +1 -1
- package/dist/registry/command/kernel-commands.d.ts +1 -4
- package/dist/registry/command/kernel-commands.d.ts.map +1 -1
- package/dist/registry/command/kernel-commands.js +0 -28
- package/dist/registry/command/kernel-commands.js.map +1 -1
- package/dist/runtime/jobs/awaited-jobs.d.ts +6 -0
- package/dist/runtime/jobs/awaited-jobs.d.ts.map +1 -1
- package/dist/runtime/jobs/awaited-jobs.js +11 -0
- package/dist/runtime/jobs/awaited-jobs.js.map +1 -1
- package/dist/runtime/jobs/host-exit-finalizer.d.ts +20 -0
- package/dist/runtime/jobs/host-exit-finalizer.d.ts.map +1 -0
- package/dist/runtime/jobs/host-exit-finalizer.js +57 -0
- package/dist/runtime/jobs/host-exit-finalizer.js.map +1 -0
- package/dist/runtime/jobs/liveness.d.ts +67 -0
- package/dist/runtime/jobs/liveness.d.ts.map +1 -0
- package/dist/runtime/jobs/liveness.js +94 -0
- package/dist/runtime/jobs/liveness.js.map +1 -0
- package/dist/runtime/jobs/registry.d.ts +21 -0
- package/dist/runtime/jobs/registry.d.ts.map +1 -1
- package/dist/runtime/jobs/registry.js +104 -2
- package/dist/runtime/jobs/registry.js.map +1 -1
- package/dist/runtime/jobs/wake-budget.d.ts +35 -0
- package/dist/runtime/jobs/wake-budget.d.ts.map +1 -0
- package/dist/runtime/jobs/wake-budget.js +48 -0
- package/dist/runtime/jobs/wake-budget.js.map +1 -0
- package/dist/runtime/query/detached-calls.d.ts +65 -0
- package/dist/runtime/query/detached-calls.d.ts.map +1 -0
- package/dist/runtime/query/detached-calls.js +116 -0
- package/dist/runtime/query/detached-calls.js.map +1 -0
- package/dist/runtime/query/executor.d.ts +11 -3
- package/dist/runtime/query/executor.d.ts.map +1 -1
- package/dist/runtime/query/executor.js +100 -8
- package/dist/runtime/query/executor.js.map +1 -1
- package/dist/runtime/query/index.d.ts +20 -0
- package/dist/runtime/query/index.d.ts.map +1 -1
- package/dist/runtime/query/index.js +31 -5
- package/dist/runtime/query/index.js.map +1 -1
- package/dist/runtime/query/iteration/outstanding-work.d.ts.map +1 -1
- package/dist/runtime/query/iteration/outstanding-work.js +11 -3
- package/dist/runtime/query/iteration/outstanding-work.js.map +1 -1
- package/dist/runtime/query/iteration/phases/context.d.ts +7 -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 +63 -1
- package/dist/runtime/query/iteration/phases/tool-review.js.map +1 -1
- package/dist/runtime/query/session-storage.d.ts.map +1 -1
- package/dist/runtime/query/session-storage.js +8 -3
- package/dist/runtime/query/session-storage.js.map +1 -1
- package/dist/runtime/query/steering.d.ts +29 -3
- package/dist/runtime/query/steering.d.ts.map +1 -1
- package/dist/runtime/query/steering.js +58 -3
- package/dist/runtime/query/steering.js.map +1 -1
- package/dist/runtime/query/tooling.d.ts +1 -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/runtime/system-events.d.ts +5 -0
- package/dist/runtime/system-events.d.ts.map +1 -1
- package/dist/runtime/system-events.js +7 -1
- package/dist/runtime/system-events.js.map +1 -1
- package/dist/sandbox/denial.d.ts +11 -0
- package/dist/sandbox/denial.d.ts.map +1 -0
- package/dist/sandbox/denial.js +61 -0
- package/dist/sandbox/denial.js.map +1 -0
- package/dist/sandbox/file-walk-program.d.ts.map +1 -1
- package/dist/sandbox/file-walk-program.js +31 -13
- package/dist/sandbox/file-walk-program.js.map +1 -1
- package/dist/sandbox/file-walk.d.ts +2 -0
- package/dist/sandbox/file-walk.d.ts.map +1 -1
- package/dist/sandbox/file-walk.js +64 -13
- package/dist/sandbox/file-walk.js.map +1 -1
- package/dist/sandbox/ignore-matcher.d.ts +30 -0
- package/dist/sandbox/ignore-matcher.d.ts.map +1 -0
- package/dist/sandbox/ignore-matcher.js +11 -0
- package/dist/sandbox/ignore-matcher.js.map +1 -0
- package/dist/sandbox/ignore-source.d.ts +26 -0
- package/dist/sandbox/ignore-source.d.ts.map +1 -0
- package/dist/sandbox/ignore-source.js +221 -0
- package/dist/sandbox/ignore-source.js.map +1 -0
- package/dist/sandbox/provider/local.d.ts.map +1 -1
- package/dist/sandbox/provider/local.js +11 -1
- package/dist/sandbox/provider/local.js.map +1 -1
- package/dist/scheduler/completion-inbox.d.ts.map +1 -1
- package/dist/scheduler/completion-inbox.js +1 -0
- package/dist/scheduler/completion-inbox.js.map +1 -1
- package/dist/session/log-hash.d.ts.map +1 -1
- package/dist/session/log-hash.js +18 -1
- package/dist/session/log-hash.js.map +1 -1
- package/dist/store/session-index/sqlite.d.ts.map +1 -1
- package/dist/store/session-index/sqlite.js +169 -16
- package/dist/store/session-index/sqlite.js.map +1 -1
- package/dist/tools/builtins/bash.d.ts.map +1 -1
- package/dist/tools/builtins/bash.js +10 -4
- package/dist/tools/builtins/bash.js.map +1 -1
- package/dist/tools/builtins/computer-use.d.ts +35 -0
- package/dist/tools/builtins/computer-use.d.ts.map +1 -1
- package/dist/tools/builtins/computer-use.js +512 -67
- package/dist/tools/builtins/computer-use.js.map +1 -1
- package/dist/tools/builtins/glob.d.ts +1 -0
- package/dist/tools/builtins/glob.d.ts.map +1 -1
- package/dist/tools/builtins/glob.js +65 -28
- package/dist/tools/builtins/glob.js.map +1 -1
- package/dist/tools/builtins/grep.d.ts +2 -0
- package/dist/tools/builtins/grep.d.ts.map +1 -1
- package/dist/tools/builtins/grep.js +94 -40
- package/dist/tools/builtins/grep.js.map +1 -1
- package/dist/tools/builtins/job.d.ts +1 -1
- package/dist/tools/builtins/job.d.ts.map +1 -1
- package/dist/tools/builtins/job.js +34 -7
- package/dist/tools/builtins/job.js.map +1 -1
- package/dist/tools/builtins/search-support.d.ts +44 -0
- package/dist/tools/builtins/search-support.d.ts.map +1 -0
- package/dist/tools/builtins/search-support.js +89 -0
- package/dist/tools/builtins/search-support.js.map +1 -0
- package/dist/tools/builtins/wait-for-job-bounds.d.ts +2 -0
- package/dist/tools/builtins/wait-for-job-bounds.d.ts.map +1 -1
- package/dist/tools/builtins/wait-for-job-bounds.js +3 -1
- package/dist/tools/builtins/wait-for-job-bounds.js.map +1 -1
- package/dist/tools/builtins/wait-for-job.d.ts +1 -1
- package/dist/tools/builtins/wait-for-job.d.ts.map +1 -1
- package/dist/tools/builtins/wait-for-job.js +13 -2
- package/dist/tools/builtins/wait-for-job.js.map +1 -1
- package/dist/tools/command-shell.d.ts.map +1 -1
- package/dist/tools/command-shell.js +14 -5
- package/dist/tools/command-shell.js.map +1 -1
- package/dist/types/computer-use/index.d.ts +129 -13
- package/dist/types/computer-use/index.d.ts.map +1 -1
- package/dist/types/computer-use/index.js.map +1 -1
- package/dist/types/errors/catalog.d.ts.map +1 -1
- package/dist/types/errors/catalog.js +6 -0
- package/dist/types/errors/catalog.js.map +1 -1
- package/dist/types/plugin/index.d.ts +24 -0
- package/dist/types/plugin/index.d.ts.map +1 -1
- package/dist/types/plugin/index.js +4 -0
- package/dist/types/plugin/index.js.map +1 -1
- package/dist/types/provider/errors.d.ts +7 -0
- package/dist/types/provider/errors.d.ts.map +1 -1
- package/dist/types/provider/errors.js +27 -3
- package/dist/types/provider/errors.js.map +1 -1
- package/dist/types/sandbox/index.d.ts +38 -1
- package/dist/types/sandbox/index.d.ts.map +1 -1
- package/dist/types/sandbox/index.js.map +1 -1
- package/dist/types/session/records.d.ts +479 -1
- package/dist/types/session/records.d.ts.map +1 -1
- package/dist/types/session/records.js +45 -1
- package/dist/types/session/records.js.map +1 -1
- package/dist/types/tool/index.d.ts +30 -0
- package/dist/types/tool/index.d.ts.map +1 -1
- package/dist/types/tool/index.js.map +1 -1
- package/dist/types/tool/presentation.d.ts +8 -0
- package/dist/types/tool/presentation.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/account/binding.ts +43 -0
- package/src/account/index.ts +32 -0
- package/src/account/lock.ts +184 -0
- package/src/account/registry.ts +413 -0
- package/src/account/types.ts +137 -0
- package/src/compaction/context-window.ts +163 -2
- package/src/execution/workflow/index.ts +14 -1
- package/src/execution/workflow/journal.ts +161 -11
- package/src/execution/workflow/prelude.ts +51 -8
- package/src/execution/workflow/resume.ts +313 -0
- package/src/execution/workflow/runtime.ts +127 -9
- package/src/execution/workflow/types.ts +109 -1
- package/src/index.ts +17 -0
- package/src/peers/record.ts +18 -0
- package/src/peers/registry.ts +2 -2
- package/src/plugin/compat.ts +173 -0
- package/src/plugin/loader.ts +7 -1
- package/src/pricing/index.ts +2 -1
- package/src/process/kill-tree.ts +106 -4
- package/src/prompt/coding-agent-doctrine.ts +9 -1
- package/src/provider/errors.ts +27 -1
- package/src/provider/model-window-suffix.ts +84 -0
- package/src/provider/token-budget.ts +9 -3
- package/src/public-runtime.ts +38 -2
- package/src/public-types.ts +16 -0
- package/src/registry/command/kernel-commands.ts +1 -32
- package/src/runtime/jobs/awaited-jobs.ts +11 -0
- package/src/runtime/jobs/host-exit-finalizer.ts +67 -0
- package/src/runtime/jobs/liveness.ts +144 -0
- package/src/runtime/jobs/registry.ts +114 -2
- package/src/runtime/jobs/wake-budget.ts +55 -0
- package/src/runtime/query/detached-calls.ts +135 -0
- package/src/runtime/query/executor.ts +102 -12
- package/src/runtime/query/index.ts +50 -7
- package/src/runtime/query/iteration/outstanding-work.ts +12 -3
- package/src/runtime/query/iteration/phases/context.ts +7 -0
- package/src/runtime/query/iteration/phases/tool-review.ts +73 -8
- package/src/runtime/query/session-storage.ts +8 -3
- package/src/runtime/query/steering.ts +77 -6
- package/src/runtime/query/tooling.ts +2 -0
- package/src/runtime/system-events.ts +12 -1
- package/src/sandbox/denial.ts +79 -0
- package/src/sandbox/file-walk-program.ts +32 -13
- package/src/sandbox/file-walk.ts +75 -12
- package/src/sandbox/ignore-matcher.ts +46 -0
- package/src/sandbox/ignore-source.ts +220 -0
- package/src/sandbox/provider/local.ts +11 -1
- package/src/scheduler/completion-inbox.ts +1 -0
- package/src/session/log-hash.ts +21 -0
- package/src/store/session-index/sqlite.ts +171 -16
- package/src/tools/builtins/bash.ts +11 -4
- package/src/tools/builtins/computer-use.ts +593 -68
- package/src/tools/builtins/glob.ts +79 -32
- package/src/tools/builtins/grep.ts +107 -43
- package/src/tools/builtins/job.ts +42 -12
- package/src/tools/builtins/search-support.ts +107 -0
- package/src/tools/builtins/wait-for-job-bounds.ts +5 -1
- package/src/tools/builtins/wait-for-job.ts +14 -2
- package/src/tools/command-shell.ts +13 -5
- package/src/types/computer-use/index.ts +157 -15
- package/src/types/errors/catalog.ts +6 -0
- package/src/types/plugin/index.ts +8 -0
- package/src/types/provider/errors.ts +40 -3
- package/src/types/sandbox/index.ts +40 -1
- package/src/types/session/records.ts +52 -2
- package/src/types/tool/index.ts +30 -0
- package/src/types/tool/presentation.ts +8 -0
|
@@ -113,6 +113,7 @@ import {
|
|
|
113
113
|
SandboxAcquisitionStopped,
|
|
114
114
|
createDeferredSandbox,
|
|
115
115
|
} from './deferred-sandbox.js'
|
|
116
|
+
import { DetachedCalls } from './detached-calls.js'
|
|
116
117
|
import type { EventTranslator } from './events.js'
|
|
117
118
|
import { finalizeTurn } from './finalize-turn.js'
|
|
118
119
|
import { GuardCoordinator } from './guard.js'
|
|
@@ -150,6 +151,7 @@ import { acquireSandbox } from './sandbox-lifecycle.js'
|
|
|
150
151
|
import {
|
|
151
152
|
SteeringBinding,
|
|
152
153
|
type SteeringChannel,
|
|
154
|
+
formatJobExitNotice,
|
|
153
155
|
formatMonitorEnd,
|
|
154
156
|
formatMonitorEvent,
|
|
155
157
|
isOperatorUserMessage,
|
|
@@ -808,6 +810,28 @@ export interface QueryParams {
|
|
|
808
810
|
*/
|
|
809
811
|
waitForInbound?: (signal: AbortSignal) => Promise<void>
|
|
810
812
|
|
|
813
|
+
/**
|
|
814
|
+
* Answer the operator while a long tool call is still running.
|
|
815
|
+
*
|
|
816
|
+
* Default off. When on, and `waitForOperator` reports the operator wrote while
|
|
817
|
+
* a batch had been running for two seconds, each call not yet finished is answered with a "still running"
|
|
818
|
+
* placeholder so the model can read the message and reply at once. The
|
|
819
|
+
* calls are not interrupted and not repeated: their real results reach the
|
|
820
|
+
* model later in a message that names each call, and the turn is held open
|
|
821
|
+
* for them like any outstanding work. The host's wake is then also consulted
|
|
822
|
+
* during tool batches, not only when the turn would otherwise end. Ignored
|
|
823
|
+
* under per-call review, where the batch is waiting on a person.
|
|
824
|
+
*/
|
|
825
|
+
answerWhileCallsRun?: boolean
|
|
826
|
+
|
|
827
|
+
/**
|
|
828
|
+
* Resolve when the operator wrote, and for no other reason. It is what
|
|
829
|
+
* `answerWhileCallsRun` listens to: a notice from a child agent or mail from
|
|
830
|
+
* a peer also wakes `waitForInbound`, but must not hand a running call back.
|
|
831
|
+
* Abort removes the waiter, as for `waitForInbound`.
|
|
832
|
+
*/
|
|
833
|
+
waitForOperator?: (signal: AbortSignal) => Promise<void>
|
|
834
|
+
|
|
811
835
|
/** Durable host input: normal append, flush and exact acknowledgement before inference. Requires sessionLog. */
|
|
812
836
|
durableInbound?: DurableInboundSource
|
|
813
837
|
|
|
@@ -1484,6 +1508,21 @@ export async function* query(params: QueryParams): AsyncGenerator<SessionEvent,
|
|
|
1484
1508
|
)
|
|
1485
1509
|
: undefined
|
|
1486
1510
|
awaitedJobs?.attach()
|
|
1511
|
+
// A tool result has handed the model this job's exit: the notice that
|
|
1512
|
+
// would announce the same exit is taken back, and the host is told it was
|
|
1513
|
+
// delivered so the session does not wake the model for it either.
|
|
1514
|
+
const collectJob = (id: string): void => {
|
|
1515
|
+
const withdrawn = jobNotices?.withdraw(id) ?? false
|
|
1516
|
+
awaitedJobs?.collected(id)
|
|
1517
|
+
if (!pendingJobNoticeIds.delete(id) && !withdrawn) return
|
|
1518
|
+
try {
|
|
1519
|
+
params.onJobNoticeDelivered?.([id])
|
|
1520
|
+
} catch (error) {
|
|
1521
|
+
ctx.log.warn('A background job notice observer failed', {
|
|
1522
|
+
'exception.message': error instanceof Error ? error.message : String(error),
|
|
1523
|
+
})
|
|
1524
|
+
}
|
|
1525
|
+
}
|
|
1487
1526
|
|
|
1488
1527
|
// Turn-scoped, like `toolGrants` below: what a skill loaded in this turn
|
|
1489
1528
|
// pre-approves ends with the turn. Shared by the executor, where the
|
|
@@ -1513,6 +1552,7 @@ export async function* query(params: QueryParams): AsyncGenerator<SessionEvent,
|
|
|
1513
1552
|
...(params.backgroundJobs ? { backgroundJobs: params.backgroundJobs } : {}),
|
|
1514
1553
|
...(params.backgroundJobOwner ? { backgroundJobOwner: params.backgroundJobOwner } : {}),
|
|
1515
1554
|
...(awaitedJobs ? { onJobAwaited: (id: string) => awaitedJobs.expect(id) } : {}),
|
|
1555
|
+
...(awaitedJobs ? { onJobCollected: collectJob } : {}),
|
|
1516
1556
|
...(awaitedJobs ? { onMonitorStarted: (id: string) => awaitedJobs.expectMonitor(id) } : {}),
|
|
1517
1557
|
...(params.monitorTimeoutCapMs !== undefined
|
|
1518
1558
|
? { monitorTimeoutCapMs: params.monitorTimeoutCapMs }
|
|
@@ -1611,13 +1651,13 @@ export async function* query(params: QueryParams): AsyncGenerator<SessionEvent,
|
|
|
1611
1651
|
.catch(() => {})
|
|
1612
1652
|
return
|
|
1613
1653
|
}
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
)
|
|
1654
|
+
let inlineOutput: ReturnType<NonNullable<typeof params.backgroundJobs>['read']> | undefined
|
|
1655
|
+
try {
|
|
1656
|
+
inlineOutput = params.backgroundJobs?.read(job.id)
|
|
1657
|
+
} catch {
|
|
1658
|
+
// The record is gone; the notice still stands without its output.
|
|
1659
|
+
}
|
|
1660
|
+
jobNotices?.steer(formatJobExitNotice(job, inlineOutput), job.id)
|
|
1621
1661
|
pendingJobNoticeIds.add(job.id)
|
|
1622
1662
|
void eventTranslator
|
|
1623
1663
|
.emitEvent({
|
|
@@ -1821,6 +1861,9 @@ export async function* query(params: QueryParams): AsyncGenerator<SessionEvent,
|
|
|
1821
1861
|
...(monitorNotices ? { monitorNotices } : {}),
|
|
1822
1862
|
...(monitorNotices ? { onMonitorNoticeDelivered: acknowledgeMonitorNotices } : {}),
|
|
1823
1863
|
...(awaitedJobs ? { awaitedJobs } : {}),
|
|
1864
|
+
...(params.answerWhileCallsRun && params.waitForOperator
|
|
1865
|
+
? { detachedCalls: new DetachedCalls(params.waitForOperator) }
|
|
1866
|
+
: {}),
|
|
1824
1867
|
checkpointMgr,
|
|
1825
1868
|
planManager: ctx.planManager,
|
|
1826
1869
|
taskGateway: taskScheduler,
|
|
@@ -5,6 +5,7 @@ import type { TaskHandle } from '../../../types/agent/scheduler.js'
|
|
|
5
5
|
import { createRuntimeContextMessage } from '../../../types/message/index.js'
|
|
6
6
|
import type { SessionEvent } from '../../../types/session/index.js'
|
|
7
7
|
import { readPositiveIntEnv } from '../../../utils/env.js'
|
|
8
|
+
import { formatLateResults } from '../detached-calls.js'
|
|
8
9
|
import { formatJobNote, formatMonitorNote } from '../steering.js'
|
|
9
10
|
import type { IterationContext } from './phases/index.js'
|
|
10
11
|
|
|
@@ -165,7 +166,8 @@ export async function* holdForOutstandingWork(
|
|
|
165
166
|
): AsyncGenerator<SessionEvent, boolean> {
|
|
166
167
|
const inbox = ctx.completionInbox?.holdsTurn ? ctx.completionInbox : undefined
|
|
167
168
|
const jobs = ctx.awaitedJobs?.hasPendingWork ? ctx.awaitedJobs : undefined
|
|
168
|
-
|
|
169
|
+
const detached = ctx.detachedCalls?.hasWork ? ctx.detachedCalls : undefined
|
|
170
|
+
if (!inbox && !jobs && !detached) return false
|
|
169
171
|
|
|
170
172
|
// Read HERE rather than from `forceFinalize`, which was sampled at the
|
|
171
173
|
// top of the iteration: one that has since crossed the finalize point
|
|
@@ -179,7 +181,7 @@ export async function* holdForOutstandingWork(
|
|
|
179
181
|
// to be running. A job therefore never shortens a wait, and it never
|
|
180
182
|
// lengthens one either: where a task is outstanding too, that is how
|
|
181
183
|
// long this turn was waiting anyway.
|
|
182
|
-
let graceMs = inbox ? settleGraceMs(remainingMs) : awaitedJobGraceMs(remainingMs)
|
|
184
|
+
let graceMs = inbox || detached ? settleGraceMs(remainingMs) : awaitedJobGraceMs(remainingMs)
|
|
183
185
|
// A monitor the turn owns is held for until the monitor itself ends, which
|
|
184
186
|
// its own deadline bounds; the two-minute job ceiling would walk away from
|
|
185
187
|
// a watch with most of its time left. As with a task, this only ever
|
|
@@ -207,6 +209,7 @@ export async function* holdForOutstandingWork(
|
|
|
207
209
|
await Promise.race([
|
|
208
210
|
...(inbox ? [inbox.waitForArrival(graceMs, waiting.signal)] : []),
|
|
209
211
|
...(jobs ? [jobs.waitForArrival(graceMs, waiting.signal)] : []),
|
|
212
|
+
...(detached ? [detached.waitForArrival(graceMs, waiting.signal)] : []),
|
|
210
213
|
...(ctx.waitForInbound ? [ctx.waitForInbound(waiting.signal)] : []),
|
|
211
214
|
...(ctx.durableInbound?.wait ? [ctx.durableInbound.wait(waiting.signal)] : []),
|
|
212
215
|
])
|
|
@@ -228,8 +231,14 @@ export async function* holdForOutstandingWork(
|
|
|
228
231
|
}
|
|
229
232
|
const exited = deliverAwaitedJobExits(ctx)
|
|
230
233
|
const monitored = deliverMonitorNotices(ctx)
|
|
234
|
+
const late = ctx.detachedCalls?.take() ?? []
|
|
235
|
+
if (late.length > 0) {
|
|
236
|
+
ctx.recorder.pushMessage(createRuntimeContextMessage(formatLateResults(late), 'job-exit'))
|
|
237
|
+
}
|
|
231
238
|
const inbound = await deliverInbound()
|
|
232
|
-
if (arrived.length === 0 && !exited && !monitored && inbound === 0)
|
|
239
|
+
if (arrived.length === 0 && !exited && !monitored && late.length === 0 && inbound === 0) {
|
|
240
|
+
return false
|
|
241
|
+
}
|
|
233
242
|
await ctx.emitEvent({
|
|
234
243
|
type: 'iteration_completed',
|
|
235
244
|
turnId: ctx.recorder.turnId,
|
|
@@ -40,6 +40,7 @@ import type { TaskStore } from '../../../../types/task/index.js'
|
|
|
40
40
|
import type { Logger } from '../../../../utils/logger.js'
|
|
41
41
|
import type { AwaitedJobs } from '../../../jobs/awaited-jobs.js'
|
|
42
42
|
import type { CheckpointManager } from '../../checkpoint.js'
|
|
43
|
+
import type { DetachedCalls } from '../../detached-calls.js'
|
|
43
44
|
import type { EmitEvent } from '../../events.js'
|
|
44
45
|
import type { ToolExecutor } from '../../executor.js'
|
|
45
46
|
import type { GuardCoordinator } from '../../guard.js'
|
|
@@ -171,6 +172,12 @@ export interface IterationContext {
|
|
|
171
172
|
* and a turn whose model stopped calling tools settles without waiting.
|
|
172
173
|
*/
|
|
173
174
|
readonly awaitedJobs?: AwaitedJobs
|
|
175
|
+
/**
|
|
176
|
+
* Calls answered with a placeholder because the operator wrote while they
|
|
177
|
+
* ran. Absent means no call is ever handed back and the operator's message
|
|
178
|
+
* waits for the batch, as it always has.
|
|
179
|
+
*/
|
|
180
|
+
readonly detachedCalls?: DetachedCalls
|
|
174
181
|
readonly checkpointMgr: CheckpointManager
|
|
175
182
|
readonly planManager: PlanManager
|
|
176
183
|
|
|
@@ -7,10 +7,12 @@ import type {
|
|
|
7
7
|
ToolCallDeclined,
|
|
8
8
|
ToolCallSummary,
|
|
9
9
|
} from '../../../../types/hitl/index.js'
|
|
10
|
+
import { createRuntimeContextMessage } from '../../../../types/message/index.js'
|
|
10
11
|
import type { ChatCompletionResponse } from '../../../../types/provider/index.js'
|
|
11
12
|
import type { SessionEvent } from '../../../../types/session/index.js'
|
|
12
13
|
import type { ShellDialect } from '../../../../types/tool/index.js'
|
|
13
14
|
import { DECLINED_TOOL_CALL_FEEDBACK } from '../../declined.js'
|
|
15
|
+
import { type BatchDetach, DETACH_AFTER_MS, formatLateResults } from '../../detached-calls.js'
|
|
14
16
|
import type {
|
|
15
17
|
PreparedToolBatch,
|
|
16
18
|
ReviewedExecution,
|
|
@@ -65,6 +67,60 @@ function hookAskedReason(reason: string | undefined): string {
|
|
|
65
67
|
: 'a hook asked that a person approve this call'
|
|
66
68
|
}
|
|
67
69
|
|
|
70
|
+
/**
|
|
71
|
+
* Watch for the operator writing while this batch runs.
|
|
72
|
+
*
|
|
73
|
+
* The batch is handed back once a message has arrived and the batch has run for
|
|
74
|
+
* {@link DETACH_AFTER_MS}; a short call finishes first and nothing changes. The
|
|
75
|
+
* arrival signal is the host's `waitForOperator`, which fires for operator text
|
|
76
|
+
* only, so a host that supplies none never detaches a call and a notice from a
|
|
77
|
+
* child or a peer never does.
|
|
78
|
+
*/
|
|
79
|
+
function watchForOperator(
|
|
80
|
+
ctx: IterationContext,
|
|
81
|
+
startedAt: number,
|
|
82
|
+
): { readonly detach: BatchDetach; stop(): void } | undefined {
|
|
83
|
+
const calls = ctx.detachedCalls
|
|
84
|
+
const wakes = calls?.waitForOperator ? [calls.waitForOperator] : []
|
|
85
|
+
if (!calls || wakes.length === 0) return undefined
|
|
86
|
+
const watching = new AbortController()
|
|
87
|
+
let timer: ReturnType<typeof setTimeout> | undefined
|
|
88
|
+
const requested = new Promise<void>((resolve) => {
|
|
89
|
+
const settled = Promise.race(
|
|
90
|
+
wakes.map((wake) => {
|
|
91
|
+
try {
|
|
92
|
+
return wake(watching.signal)
|
|
93
|
+
} catch {
|
|
94
|
+
return new Promise<void>(() => {})
|
|
95
|
+
}
|
|
96
|
+
}),
|
|
97
|
+
)
|
|
98
|
+
settled.then(
|
|
99
|
+
() => {
|
|
100
|
+
if (watching.signal.aborted) return
|
|
101
|
+
const wait = Math.max(0, startedAt + DETACH_AFTER_MS - Date.now())
|
|
102
|
+
timer = setTimeout(resolve, wait)
|
|
103
|
+
},
|
|
104
|
+
// An aborted wait is the batch ending, which is not a message.
|
|
105
|
+
() => {},
|
|
106
|
+
)
|
|
107
|
+
})
|
|
108
|
+
return {
|
|
109
|
+
detach: { requested, calls },
|
|
110
|
+
stop() {
|
|
111
|
+
watching.abort()
|
|
112
|
+
if (timer !== undefined) clearTimeout(timer)
|
|
113
|
+
},
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** The outcomes of calls that were handed back as still running and have since finished. */
|
|
118
|
+
function takeLateResults(ctx: IterationContext) {
|
|
119
|
+
const outcomes = ctx.detachedCalls?.take()
|
|
120
|
+
if (!outcomes || outcomes.length === 0) return undefined
|
|
121
|
+
return createRuntimeContextMessage(formatLateResults(outcomes), 'job-exit')
|
|
122
|
+
}
|
|
123
|
+
|
|
68
124
|
export type ToolReviewDecision = 'executed' | 'rejected' | 'stop'
|
|
69
125
|
|
|
70
126
|
/**
|
|
@@ -290,14 +346,21 @@ export async function* runToolReview(
|
|
|
290
346
|
): Promise<void> => {
|
|
291
347
|
await announceRefusals(denials)
|
|
292
348
|
const startedAt = Date.now()
|
|
293
|
-
const
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
349
|
+
const watch = watchForOperator(ctx, startedAt)
|
|
350
|
+
let batch: Awaited<ReturnType<typeof ctx.toolExecutor.executeBatch>>
|
|
351
|
+
try {
|
|
352
|
+
batch = await ctx.toolExecutor.executeBatch(
|
|
353
|
+
response,
|
|
354
|
+
withRepeatRefusals(denials),
|
|
355
|
+
undefined,
|
|
356
|
+
preparedBatch,
|
|
357
|
+
declines,
|
|
358
|
+
review,
|
|
359
|
+
watch?.detach,
|
|
360
|
+
)
|
|
361
|
+
} finally {
|
|
362
|
+
watch?.stop()
|
|
363
|
+
}
|
|
301
364
|
toolMs += Date.now() - startedAt
|
|
302
365
|
executed = batch.results
|
|
303
366
|
await announceBatch(batch.results)
|
|
@@ -369,6 +432,8 @@ export async function* runToolReview(
|
|
|
369
432
|
},
|
|
370
433
|
)
|
|
371
434
|
if (monitorNotice) ctx.recorder.pushMessage(monitorNotice)
|
|
435
|
+
const lateResults = takeLateResults(ctx)
|
|
436
|
+
if (lateResults) ctx.recorder.pushMessage(lateResults)
|
|
372
437
|
const credentialNotice = takeWithheldEnvNotice(ctx.recorder.messages, batch.observations)
|
|
373
438
|
if (credentialNotice) ctx.recorder.pushMessage(credentialNotice)
|
|
374
439
|
if (jobNoticeDelivered) ctx.onJobNoticeDelivered?.()
|
|
@@ -273,9 +273,14 @@ export function checkpointLogView(log: SessionLog): CheckpointLogView {
|
|
|
273
273
|
return {
|
|
274
274
|
async verifyThrough(scope, throughSeq, throughSha256) {
|
|
275
275
|
own(scope)
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
276
|
+
// Stream the walk and keep only the last pointer: the chain is
|
|
277
|
+
// verified as it is read, and a long session must not be held in
|
|
278
|
+
// memory just to look at its final entry.
|
|
279
|
+
let last: { seq: number; sha256: string } | undefined
|
|
280
|
+
for await (const entry of log.read({ mode: 'tolerant', throughSeq })) {
|
|
281
|
+
last = entry.pointer
|
|
282
|
+
}
|
|
283
|
+
return last?.seq === throughSeq && last.sha256 === throughSha256
|
|
279
284
|
},
|
|
280
285
|
async writtenDocSha256(scope, checkpointId) {
|
|
281
286
|
own(scope)
|
|
@@ -64,17 +64,31 @@ export interface SteeringChannel {
|
|
|
64
64
|
}
|
|
65
65
|
|
|
66
66
|
export class SteeringBinding implements SteeringChannel {
|
|
67
|
-
private queued: string[] = []
|
|
67
|
+
private queued: { readonly text: string; readonly key?: string }[] = []
|
|
68
68
|
|
|
69
|
-
|
|
69
|
+
/**
|
|
70
|
+
* `key` names what the text is about (a job id), so the text can be
|
|
71
|
+
* taken back with {@link withdraw} while it is still undelivered.
|
|
72
|
+
*/
|
|
73
|
+
steer(text: string, key?: string): void {
|
|
70
74
|
const trimmed = text.trim()
|
|
71
75
|
if (!trimmed) return
|
|
72
|
-
this.queued.push(trimmed)
|
|
76
|
+
this.queued.push(key === undefined ? { text: trimmed } : { text: trimmed, key })
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Take back every undelivered text queued under `key`. Returns whether any
|
|
81
|
+
* was queued. Text already drained has been delivered and is not touched.
|
|
82
|
+
*/
|
|
83
|
+
withdraw(key: string): boolean {
|
|
84
|
+
const before = this.queued.length
|
|
85
|
+
this.queued = this.queued.filter((entry) => entry.key !== key)
|
|
86
|
+
return this.queued.length !== before
|
|
73
87
|
}
|
|
74
88
|
|
|
75
89
|
drain(): string | undefined {
|
|
76
90
|
if (this.queued.length === 0) return undefined
|
|
77
|
-
const joined = this.queued.join('\n')
|
|
91
|
+
const joined = this.queued.map((entry) => entry.text).join('\n')
|
|
78
92
|
this.queued = []
|
|
79
93
|
return joined
|
|
80
94
|
}
|
|
@@ -84,6 +98,60 @@ export class SteeringBinding implements SteeringChannel {
|
|
|
84
98
|
}
|
|
85
99
|
}
|
|
86
100
|
|
|
101
|
+
/**
|
|
102
|
+
* The most output a job-exit notice carries inline. A job whose whole output
|
|
103
|
+
* fits is told with its result; a larger one is read with the job tool, so a
|
|
104
|
+
* noisy build cannot turn every exit into a large message.
|
|
105
|
+
*/
|
|
106
|
+
export const JOB_NOTICE_INLINE_OUTPUT_MAX_BYTES = 2048
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* One job's exit as the text the model reads. The output is inlined only when
|
|
110
|
+
* the whole of it fits {@link JOB_NOTICE_INLINE_OUTPUT_MAX_BYTES} and none was
|
|
111
|
+
* dropped by the retention cap; it is untrusted process output, so it rides in
|
|
112
|
+
* the system-event envelope rather than in the notice's own words.
|
|
113
|
+
*/
|
|
114
|
+
export function formatJobExitNotice(
|
|
115
|
+
job: Pick<BackgroundJob, 'id' | 'command' | 'status' | 'exitCode' | 'signal'> &
|
|
116
|
+
Partial<Pick<BackgroundJob, 'exitedAt'>>,
|
|
117
|
+
output?: {
|
|
118
|
+
readonly chunk: string
|
|
119
|
+
readonly nextOffset: number
|
|
120
|
+
readonly droppedBytes: number
|
|
121
|
+
},
|
|
122
|
+
): string {
|
|
123
|
+
const outcome =
|
|
124
|
+
job.status === 'killed'
|
|
125
|
+
? 'was stopped'
|
|
126
|
+
: `exited with code ${job.exitCode ?? 'unknown'}${job.signal ? ` (${job.signal})` : ''}`
|
|
127
|
+
const endedAt = job.exitedAt === undefined ? '' : ` at ${new Date(job.exitedAt).toISOString()}`
|
|
128
|
+
const head = `Background job ${job.id} (${job.command}) ${outcome}${endedAt}.`
|
|
129
|
+
if (
|
|
130
|
+
output === undefined ||
|
|
131
|
+
output.chunk.length === 0 ||
|
|
132
|
+
output.droppedBytes > 0 ||
|
|
133
|
+
output.nextOffset > JOB_NOTICE_INLINE_OUTPUT_MAX_BYTES
|
|
134
|
+
) {
|
|
135
|
+
return `${head} Read its output with the job tool if you need it.`
|
|
136
|
+
}
|
|
137
|
+
return `${head}\n${formatSystemEvent({
|
|
138
|
+
kind: 'job',
|
|
139
|
+
id: job.id,
|
|
140
|
+
status: job.status === 'killed' ? 'cancelled' : job.exitCode === 0 ? 'completed' : 'failed',
|
|
141
|
+
summary: `Output of ${job.id}`,
|
|
142
|
+
source: `job ${job.id}: ${oneMetadataLine(job.command)}`,
|
|
143
|
+
body: {
|
|
144
|
+
envelope: {
|
|
145
|
+
kind: 'job-output',
|
|
146
|
+
attributes: { job: job.id },
|
|
147
|
+
provenance:
|
|
148
|
+
'Everything the job printed. The command, or whatever it reads, wrote it; it is not the operator.',
|
|
149
|
+
},
|
|
150
|
+
content: output.chunk,
|
|
151
|
+
},
|
|
152
|
+
})}`
|
|
153
|
+
}
|
|
154
|
+
|
|
87
155
|
/** What the model reads when a background job it started has ended. */
|
|
88
156
|
export function formatJobNote(text: string): string {
|
|
89
157
|
return frameNotice('job', '[Background job update]', text)
|
|
@@ -116,7 +184,7 @@ const SYSTEM_EVENT_FRAME = /^<system-event-([A-Za-z0-9]+) [^\n]*\n[\s\S]*?\n<\/s
|
|
|
116
184
|
*/
|
|
117
185
|
export function formatMonitorEvent(
|
|
118
186
|
event: Pick<MonitorEvent, 'jobId' | 'description' | 'kind' | 'text'> &
|
|
119
|
-
Partial<Pick<MonitorEvent, 'transport'>>,
|
|
187
|
+
Partial<Pick<MonitorEvent, 'transport' | 'at'>>,
|
|
120
188
|
command: string,
|
|
121
189
|
): string {
|
|
122
190
|
return formatSystemEvent({
|
|
@@ -126,6 +194,7 @@ export function formatMonitorEvent(
|
|
|
126
194
|
summary: `Monitor event: "${oneMetadataLine(event.description)}"`,
|
|
127
195
|
source: `monitor ${event.jobId}: ${oneMetadataLine(command)}`,
|
|
128
196
|
more: `job {"action":"kill","id":"${event.jobId}"} stops it`,
|
|
197
|
+
...(event.at !== undefined ? { at: event.at } : {}),
|
|
129
198
|
body: {
|
|
130
199
|
envelope: {
|
|
131
200
|
kind: event.kind === 'output' ? 'monitor-output' : 'monitor-notice',
|
|
@@ -170,7 +239,8 @@ export function formatMonitorEnd(
|
|
|
170
239
|
| 'transport'
|
|
171
240
|
| 'closeCode'
|
|
172
241
|
| 'closeReason'
|
|
173
|
-
|
|
242
|
+
> &
|
|
243
|
+
Partial<Pick<BackgroundJob, 'exitedAt'>>,
|
|
174
244
|
stats: { readonly stdoutChars: number },
|
|
175
245
|
): string {
|
|
176
246
|
const end = describeMonitorEnd(job, stats)
|
|
@@ -181,6 +251,7 @@ export function formatMonitorEnd(
|
|
|
181
251
|
summary: end.summary,
|
|
182
252
|
source: `monitor ${job.id}: ${oneMetadataLine(job.command)}`,
|
|
183
253
|
more: `job {"action":"read","id":"${job.id}"} reads its output`,
|
|
254
|
+
...(job.exitedAt !== undefined ? { at: job.exitedAt } : {}),
|
|
184
255
|
})
|
|
185
256
|
}
|
|
186
257
|
|
|
@@ -46,6 +46,7 @@ export interface ToolingBootstrapConfig {
|
|
|
46
46
|
backgroundJobOwner?: string
|
|
47
47
|
/** Where `wait_for_job` records wait-intent; see the executor's own field. */
|
|
48
48
|
onJobAwaited?: (id: string) => void
|
|
49
|
+
onJobCollected?: (id: string) => void
|
|
49
50
|
/** See `ToolExecutorConfig.monitorTimeoutCapMs`. */
|
|
50
51
|
monitorTimeoutCapMs?: number
|
|
51
52
|
/** See `ToolExecutorConfig.onMonitorStarted`. */
|
|
@@ -110,6 +111,7 @@ export class ToolingBootstrap {
|
|
|
110
111
|
...(config.backgroundJobs ? { backgroundJobs: config.backgroundJobs } : {}),
|
|
111
112
|
...(config.backgroundJobOwner ? { backgroundJobOwner: config.backgroundJobOwner } : {}),
|
|
112
113
|
...(config.onJobAwaited ? { onJobAwaited: config.onJobAwaited } : {}),
|
|
114
|
+
...(config.onJobCollected ? { onJobCollected: config.onJobCollected } : {}),
|
|
113
115
|
...(config.monitorTimeoutCapMs !== undefined
|
|
114
116
|
? { monitorTimeoutCapMs: config.monitorTimeoutCapMs }
|
|
115
117
|
: {}),
|
|
@@ -102,6 +102,11 @@ export interface SystemEvent {
|
|
|
102
102
|
readonly more?: string
|
|
103
103
|
/** Omit for an event with nothing to show; renders as `(no output)`. */
|
|
104
104
|
readonly body?: SystemEventBody
|
|
105
|
+
/**
|
|
106
|
+
* When it happened (epoch ms). Rendered as an `at` attribute in ISO form so
|
|
107
|
+
* the model can tell a fresh event from one that waited in a queue.
|
|
108
|
+
*/
|
|
109
|
+
readonly at?: number
|
|
105
110
|
}
|
|
106
111
|
|
|
107
112
|
export interface FormatSystemEventOptions {
|
|
@@ -209,6 +214,12 @@ function renderBody(body: SystemEventBody | undefined, options: FormatSystemEven
|
|
|
209
214
|
)
|
|
210
215
|
}
|
|
211
216
|
|
|
217
|
+
function atAttribute(at: number | undefined): string {
|
|
218
|
+
if (at === undefined || !Number.isFinite(at)) return ''
|
|
219
|
+
const date = new Date(at)
|
|
220
|
+
return Number.isNaN(date.getTime()) ? '' : ` at="${date.toISOString()}"`
|
|
221
|
+
}
|
|
222
|
+
|
|
212
223
|
/**
|
|
213
224
|
* Render one asynchronous event as provider-required user-role context.
|
|
214
225
|
*
|
|
@@ -248,7 +259,7 @@ export function formatSystemEvent(
|
|
|
248
259
|
])
|
|
249
260
|
const closingTag = `</system-event-${nonce}>`
|
|
250
261
|
const lines = [
|
|
251
|
-
`<system-event-${nonce} kind="${escapeAttribute(event.kind)}" id="${escapeAttribute(event.id)}" status="${escapeAttribute(event.status)}">`,
|
|
262
|
+
`<system-event-${nonce} kind="${escapeAttribute(event.kind)}" id="${escapeAttribute(event.id)}" status="${escapeAttribute(event.status)}"${atAttribute(event.at)}>`,
|
|
252
263
|
SYSTEM_EVENT_HEADER,
|
|
253
264
|
nonceClosureStatement(closingTag),
|
|
254
265
|
`summary: ${summary}`,
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import type { SandboxEnvironment, SandboxExecAttribution } from '../types/sandbox/index.js'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Reading a sandboxed command's failure: did the command fail, did the
|
|
5
|
+
* sandbox refuse it, or did the launcher never get as far as running it?
|
|
6
|
+
*
|
|
7
|
+
* The model cannot tell these apart from an exit code. A denied write looks
|
|
8
|
+
* like any other failure, so it retries the same command and burns turns
|
|
9
|
+
* on something no retry can fix; a launcher that could not set up its
|
|
10
|
+
* namespaces looks like the command crashing. Both call for a different
|
|
11
|
+
* answer (ask for the escape, or report a runner fault).
|
|
12
|
+
*
|
|
13
|
+
* This is inference from stderr text, not a verdict from the kernel, and
|
|
14
|
+
* the wording says so. It only ever adds a note; the result is unchanged.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
/** Launcher setup failures print this prefix before any user command runs. */
|
|
18
|
+
const RUNNER_PREFIX: Partial<Record<SandboxEnvironment, RegExp>> = {
|
|
19
|
+
'linux-bwrap': /^bwrap: (?!execvp )/m,
|
|
20
|
+
'linux-namespace': /^unshare: /m,
|
|
21
|
+
'macos-seatbelt': /^sandbox-exec: /m,
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const EROFS = /Read-only file system|\bEROFS\b/i
|
|
25
|
+
const PERMISSION = /Permission denied|Operation not permitted|\bEACCES\b|\bEPERM\b/i
|
|
26
|
+
const ABSOLUTE_PATH = /(?:^|[\s'"`:(])(\/[^\s'"`:)]+)/g
|
|
27
|
+
|
|
28
|
+
/** Only tiers that confine file writes can produce a sandbox denial. */
|
|
29
|
+
const CONFINES_FILES: ReadonlySet<SandboxEnvironment> = new Set(['linux-bwrap', 'macos-seatbelt'])
|
|
30
|
+
|
|
31
|
+
function within(path: string, root: string): boolean {
|
|
32
|
+
return path === root || path.startsWith(root.endsWith('/') ? root : `${root}/`)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function pathsIn(line: string): string[] {
|
|
36
|
+
return [...line.matchAll(ABSOLUTE_PATH)].map((m) => m[1] as string)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface AttributionInput {
|
|
40
|
+
readonly environment: SandboxEnvironment
|
|
41
|
+
readonly exitCode: number
|
|
42
|
+
readonly stderr: string
|
|
43
|
+
readonly timedOut: boolean
|
|
44
|
+
/** Paths the sandbox lets the command write: root, added directories, scratch. */
|
|
45
|
+
readonly allowedRoots: readonly string[]
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function attributeSandboxFailure(
|
|
49
|
+
input: AttributionInput,
|
|
50
|
+
): SandboxExecAttribution | undefined {
|
|
51
|
+
if (input.exitCode === 0 || input.timedOut || input.stderr === '') return undefined
|
|
52
|
+
|
|
53
|
+
const runner = RUNNER_PREFIX[input.environment]
|
|
54
|
+
if (runner?.test(input.stderr)) {
|
|
55
|
+
const line = input.stderr.split('\n').find((l) => runner.test(l)) ?? ''
|
|
56
|
+
return {
|
|
57
|
+
kind: 'runner-failure',
|
|
58
|
+
message: `The sandbox launcher failed before the command could run (${line.trim()}). This is a runner fault, not a failure of the command; retrying the same command will not help.`,
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (!CONFINES_FILES.has(input.environment)) return undefined
|
|
63
|
+
|
|
64
|
+
for (const line of input.stderr.split('\n')) {
|
|
65
|
+
if (EROFS.test(line)) return denial()
|
|
66
|
+
if (!PERMISSION.test(line)) continue
|
|
67
|
+
const paths = pathsIn(line)
|
|
68
|
+
if (paths.some((p) => !input.allowedRoots.some((root) => within(p, root)))) return denial()
|
|
69
|
+
}
|
|
70
|
+
return undefined
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function denial(): SandboxExecAttribution {
|
|
74
|
+
return {
|
|
75
|
+
kind: 'denial',
|
|
76
|
+
message:
|
|
77
|
+
'This looks like a sandbox denial: the command touched a path the sandbox does not allow it to write. This is inferred from the error text, not proven. Retrying the same command will fail the same way; if the action is needed, ask to run it outside the sandbox instead.',
|
|
78
|
+
}
|
|
79
|
+
}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
import { IGNORE_SOURCE } from './ignore-source.js'
|
|
2
|
+
|
|
1
3
|
/** Stable guest code: host bundlers must never rewrite a function we serialize. */
|
|
2
|
-
export const FILE_WALK_PROGRAM = String.raw`
|
|
4
|
+
export const FILE_WALK_PROGRAM = `${IGNORE_SOURCE}${String.raw`
|
|
3
5
|
const fs = require('node:fs/promises');
|
|
4
6
|
const path = require('node:path');
|
|
5
7
|
const plan = JSON.parse(process.argv[1]);
|
|
@@ -9,10 +11,17 @@ const write = async value => {
|
|
|
9
11
|
await new Promise(resolve => process.stdout.once('drain', resolve));
|
|
10
12
|
}
|
|
11
13
|
};
|
|
14
|
+
let stopped;
|
|
15
|
+
let visitedTotal = 0;
|
|
16
|
+
let ignoredDirs = 0;
|
|
12
17
|
const visit = count => {
|
|
13
|
-
|
|
18
|
+
visitedTotal = count;
|
|
19
|
+
if (count <= plan.maxVisitedEntries) return true;
|
|
20
|
+
if (plan.failOnLimit) {
|
|
14
21
|
throw Object.assign(new Error('File search stopped after examining ' + plan.maxVisitedEntries + ' entries; narrow its root or pattern.'), {code:'ERR_FILE_WALK_LIMIT'});
|
|
15
22
|
}
|
|
23
|
+
stopped = 'entry-limit';
|
|
24
|
+
return false;
|
|
16
25
|
};
|
|
17
26
|
const patterns = plan.patterns.map(set => set.map(part =>
|
|
18
27
|
typeof part === 'object' && part !== null ? new RegExp(part.source, part.flags + 's') : part));
|
|
@@ -41,9 +50,13 @@ async function run() {
|
|
|
41
50
|
let emitted = 0;
|
|
42
51
|
const matchers = plan.expressions.map(({source, flags}) => new RegExp(source, flags + 's'));
|
|
43
52
|
const maxDepth = plan.maxDepth === null ? Infinity : plan.maxDepth;
|
|
44
|
-
const
|
|
45
|
-
|
|
46
|
-
|
|
53
|
+
const ignore = plan.respectIgnore ? ignCreate({fs, path, extraFiles: []}) : null;
|
|
54
|
+
const open = async (absolute, relative, depth, layered) => {
|
|
55
|
+
try {
|
|
56
|
+
const dir = await fs.opendir(absolute);
|
|
57
|
+
if (layered && ignore) await ignore.enter(absolute);
|
|
58
|
+
stack.push({dir, relative, depth, layered});
|
|
59
|
+
} catch (error) { if (!missing(error)) throw error; }
|
|
47
60
|
};
|
|
48
61
|
try {
|
|
49
62
|
if (plan.patterns.length === 0) return;
|
|
@@ -53,36 +66,42 @@ async function run() {
|
|
|
53
66
|
if (canonicalRoot !== path.resolve(plan.root)) throw new Error('File walk root follows a symbolic link; use its authorized real directory');
|
|
54
67
|
const rootInfo = await fs.lstat(plan.root);
|
|
55
68
|
if (rootInfo.isFile()) {
|
|
56
|
-
visit(++visited);
|
|
69
|
+
if (!visit(++visited)) return;
|
|
57
70
|
if (matchers.some(matcher => matcher.test(path.basename(plan.root)))) {
|
|
58
71
|
await write({type:'entry', path:plan.root, size:rootInfo.size});
|
|
59
72
|
}
|
|
60
73
|
return;
|
|
61
74
|
}
|
|
75
|
+
if (ignore) { await ignore.init(plan.root); await ignore.enter(plan.root); }
|
|
62
76
|
let start = plan.root;
|
|
63
77
|
for (const part of plan.prefix) {
|
|
64
78
|
start = path.join(start, part);
|
|
65
79
|
let info;
|
|
66
80
|
try { info = await fs.lstat(start); }
|
|
67
81
|
catch (error) { if (missing(error)) return; throw error; }
|
|
68
|
-
visit(++visited);
|
|
82
|
+
if (!visit(++visited)) return;
|
|
69
83
|
if (!info.isDirectory()) return;
|
|
84
|
+
if (ignore) await ignore.enter(start);
|
|
70
85
|
}
|
|
71
86
|
if (plan.prefix.length >= maxDepth) return;
|
|
72
|
-
await open(start, plan.prefix.join('/'), plan.prefix.length);
|
|
87
|
+
await open(start, plan.prefix.join('/'), plan.prefix.length, false);
|
|
73
88
|
while (stack.length) {
|
|
74
89
|
const frame = stack[stack.length - 1];
|
|
75
90
|
const entry = await frame.dir.read();
|
|
76
|
-
if (!entry) { stack.pop(); await frame.dir.close(); continue; }
|
|
77
|
-
visit(++visited);
|
|
91
|
+
if (!entry) { stack.pop(); if (frame.layered && ignore) ignore.leave(); await frame.dir.close(); continue; }
|
|
92
|
+
if (!visit(++visited)) return;
|
|
93
|
+
if (IGN_VCS.includes(entry.name)) continue;
|
|
78
94
|
const relative = frame.relative ? frame.relative + '/' + entry.name : entry.name;
|
|
79
95
|
const absolute = path.join(plan.root, relative);
|
|
80
96
|
const depth = frame.depth + 1;
|
|
81
97
|
if (entry.isDirectory()) {
|
|
82
|
-
if (depth
|
|
98
|
+
if (depth >= maxDepth || !canDescend(relative)) continue;
|
|
99
|
+
if (ignore && ignore.isIgnored(absolute, true)) { ignoredDirs++; continue; }
|
|
100
|
+
await open(absolute, relative, depth, true);
|
|
83
101
|
continue;
|
|
84
102
|
}
|
|
85
103
|
if (!entry.isFile() || !matchers.some(matcher => matcher.test(relative))) continue;
|
|
104
|
+
if (ignore && ignore.isIgnored(absolute, false)) continue;
|
|
86
105
|
let info;
|
|
87
106
|
try { info = await fs.lstat(absolute); }
|
|
88
107
|
catch (error) { if (missing(error)) continue; throw error; }
|
|
@@ -95,7 +114,7 @@ async function run() {
|
|
|
95
114
|
}
|
|
96
115
|
}
|
|
97
116
|
run().then(
|
|
98
|
-
() => write({type:'done'}),
|
|
117
|
+
() => write({type:'done', visited:visitedTotal, ignoredDirs, stopped}),
|
|
99
118
|
async error => { await write({type:'error', message:error.message, code:error.code}); process.exitCode = 1; },
|
|
100
119
|
);
|
|
101
|
-
`
|
|
120
|
+
`}`
|