@sema-agent/core 7.26.2 → 7.26.3
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 +27 -0
- package/dist/core/runner/prepare-delegation-surface.d.ts +4 -0
- package/dist/core/runner/prepare-delegation-surface.js +2 -1
- package/dist/core/runner/prepare-task.js +1 -1
- package/dist/tools/fs/dead-cwd.d.ts +48 -0
- package/dist/tools/fs/dead-cwd.js +56 -0
- package/dist/tools/fs/fs-bash.js +4 -32
- package/dist/tools/monitor.d.ts +4 -0
- package/dist/tools/monitor.js +6 -0
- package/package.json +1 -1
- package/wire-changes/core@7.26.3.json +65 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,32 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 7.26.3 — 2026-09-21
|
|
4
|
+
|
|
5
|
+
### Monitor probes the tracked working directory before it spawns (#1018; behaviour NARROWING; +0 exports; @server @cli @test)
|
|
6
|
+
|
|
7
|
+
- **What was wrong.** `Monitor` handed the tracked cwd straight to the background seam. Node reports a bad spawn cwd on the async `'error'` event, so `spawnBackground` returns ok whatever the directory's state is: the watch registers, the model is handed a task id and told the watch is running, and no line ever arrives — indistinguishable, from the model's seat, from a quiet log. The shell's background leg has probed for exactly this since the dead-cwd diagnosis was written; this leg did not.
|
|
8
|
+
- **One probe, one diagnosis, two legs.** `isDeadCwd` and `diagnoseDeadCwd` moved to `src/tools/fs/dead-cwd.ts` and both tools call them (module-internal — no package-root export). "Is this directory usable?" has one answer and the refusal has one wording; the tool name is the only thing that differs between the two legs' sentences, and a pin holds them equal on every branch. Rule count: −1 (two copies became one), +0 special cases.
|
|
9
|
+
- **Monitor and Bash now share one root.** `MonitorToolOptions` gains the optional `rootCanonical` — the same canonical spelling `createBashTool` is built with, resolved once at the driver's root-receiving point and carried, never canonicalized a second time. A dead cwd with a live root therefore self-heals identically on both tools: the tracked cwd is reset to the root and the model is told a retry works.
|
|
10
|
+
- **NARROWING.** Deleting the working directory (or replacing it with a file, or leaving a dangling symlink there) and then starting a watch now refuses loudly and does not spawn, where it used to report a watch as running. Probe order is probe → diagnose → the leg's existing pre-spawn abort recheck → spawn, so the cancellation window the recheck closes is not reopened.
|
|
11
|
+
- **The probe itself got two shapes more truthful, on BOTH legs.** A probe error that says the path stands under a FILE (the ENOTDIR class — an ancestor replaced, where a stat on the cwd returns no kind at all) now counts as death; it used to pass through as healthy. And a symlink cwd retargeted at a file is now judged by what it LANDS on, not merely by whether it resolves. Every verdict that changed changed from healthy to dead, and each of those spawns genuinely fails — no shape the released probe called dead is called healthy now, and no live directory became dead. `docs/KNOWN-LIMITS.md` KL-1018d records the one classification the adapter's error vocabulary cannot yet express.
|
|
12
|
+
- **A probe that did not run says nothing.** On the resolution step a cancelled, refused (permissions) or unsupported `canonicalPath` reads as UNKNOWN — healthy — and a cancellation that lands during the probes never resets the tracked cwd; only a chain that will not resolve (gone, not a directory, or the adapter's catch-all where a loop lands) is dead. Before this release the shell's own leg read every resolution failure as death.
|
|
13
|
+
- **Cancellation reaches the probes.** Both shared helpers take the call's abort signal and hand it to every `fileInfo` / `canonicalPath`, so a watch or a command stopped while a remote probe is in flight is released by the stop instead of waiting the probe out. A cancelled probe never establishes death and never heals anything.
|
|
14
|
+
- **Honest wording when there is no root.** A direct mount that was never handed one gets a branch of its own: the cwd is named as gone and the tool says it has no reset target, rather than calling that directory "the task root" or declaring the tree unrecoverable — claims such a caller cannot back, since the root may well be alive one `cd` away. Nothing is reset on that branch.
|
|
15
|
+
- **Who is hurt.** A healthy cwd is byte-unchanged: one spawn, the same cwd and the same computed timeout, one extra `fileInfo` per watch start. A probe that FAILS for any reason other than "gone" (permissions, IO, an env that does not implement the face) is read as healthy — a watch tool that refused on a flaky stat would be worse than the bug. A deployment whose test asserts that a watch over a deleted directory registers must re-pin: that watch now refuses.
|
|
16
|
+
- **Consumers.** `MonitorToolOptions.rootCanonical` is an additive optional key on a type nobody downstream constructs directly (zero `createMonitorTool` / `MonitorToolOptions` sites in the server and the shell); a run prepared through `prepareTask` gets it wired automatically, so no downstream change is required to receive the self-heal.
|
|
17
|
+
### The wire manifest reads union arms by their own word, and watches every type the package root reaches (#1012; release tooling only; ZERO runtime behaviour; +0 exports; @server @cli @client-core)
|
|
18
|
+
|
|
19
|
+
- **What changed.** Two holes in the `typeKeys` leg of `wire-changes/core@<ver>.json`, both of which hid changes from the same release. A union's arms were read TOGETHER unless the union discriminated on a property literally named `type` or `kind`, so a key added to one arm moved nothing whenever a sibling arm already spelled that name — `RemoveResult`'s `no-op` arm gained the read-back word its `removed` arm already carried, and 7.26.0's manifest reported neither. And the watched set was the frozen export surface's own roster, so `InheritedGate.approverPosture` — the same key, added in the same release to a type held only as `RunInternals.inheritedGate` — was outside the denominator by construction.
|
|
20
|
+
- **One rule each, not two readings bolted on.** The `type` / `kind` name list is gone: the discriminating property is the one every key-bearing arm declares with literal words no other arm shares. Measured over the export surface — of the 98 roster unions with two or more key-bearing arms, 26 were named by the old list and 82 are named by this rule; 25 of the 26 keep their exact words and the 26th is the one the old list labelled with a word two of its arms share (it named nothing, and is disclosed instead); no union had two properties qualify. The roster is now the SEED of the watched set rather than the set itself: its transitive closure over interface members, alias bodies, `extends` clauses and published signatures (parameters and index signatures included), 943 -> 1361 watched type declarations. MODULE-LEVEL EXPORTED declarations only: a declaration a public type exposes without exporting it is inlined into the emitted `.d.ts` and is genuinely visible to a consumer, but this model's type identity is the bare name, so admitting private declarations to it merged unrelated same-named ones and masked a real removal on the published one. That needs file-qualified declaration identity through the index, the closure and the inheritance fold — a design change, recorded as KL-1012e rather than guessed at here.
|
|
21
|
+
- **What a reader sees.** A key on a discriminated arm is spelled `<word>.<key>` (`RemoveResult` -> `no-op.stillLive`), one entry per word where an arm's word is itself a union. A row for a type the barrel does not publish by name carries `via` with the shortest path it is reached by (`RunInternals.inheritedGate`); a published type's row has no `via`, because absence is what says "published outright". A union whose arms carry no discriminating word is still read flat AND says so — `arms: "flattened"` on the row, and a new `typeKeysUnreadableArms` list naming the type, its declaration site and every reason at once. That list is drawn on the types THIS RANGE moved keys on (a census of every flat-read union in the tree is a wall nobody reads); the population it came from is `typeKeysFlattenedCount`, so an empty list never reads as "none".
|
|
22
|
+
- **Who is hurt.** Nobody at runtime — zero `src/` change, zero exports moved. A consumer that parses the manifest gains one optional list and two optional row keys; `manifestShapeProblems` accepts a manifest with or without them, so an older one still reads. The manifests already in the tree are NOT regenerated (a released manifest is a historical claim, and the packaged copy is what a pickup gate reads): the two keys 7.26.0 missed stay recorded by hand in `docs/wire-consumption/7.26.0.md`.
|
|
23
|
+
- **The alphabet is chosen once, for both refs.** Each ref reads its own syntax, so a union can change what its arms can be named by while staying additive — a third arm reusing an existing word, one more literal property on every arm, one arm extracted into an interface. Comparing `a.x` against `x` would report every key of every arm as REMOVED. Where the two refs do not name a type's arms the same way, the diff drops to the coordinate they share (the union's flat key set) and says so, rather than manufacturing a breaking line. An arm this reader cannot open (a reference, a generic) makes the whole union unnamable, for the same reason: naming the arms it CAN open publishes the others' keys as absent.
|
|
24
|
+
- **A key this reader can no longer see is REPORTED, with its doubt named.** It does not follow a reference out of a union arm or into an unresolvable base, so a key present at `from` and missing at `to` may have moved in there rather than left the type. It is reported REMOVED and breaking all the same, carrying `uncertain` naming the reference it could have gone into — a release document that goes quiet about a key it can no longer see costs a consumer its install, and one coordinate for the writer to check costs a minute. Measured over `7.25.1 -> 7.26.0`, `7.26.0 -> 7.26.1` and `7.26.1 -> 7.26.2`: zero uncertain removals in all three. Naming an arm and opening it stay two questions — the tree's largest union spells every arm as `({ type: "x"; … } & Identity)` and keeps all its arm names.
|
|
25
|
+
- **An OPTIONAL property discriminates nothing.** `settledBy?: "human" | "timeout" | "policy"` is one property of ONE shape carrying a three-word vocabulary; reading it as a discriminant split that single arm into three arms that do not exist, and 7.26.2's added word came out as five keys under a variant nobody can match on. A property a value may simply not carry cannot tell two shapes apart, so it is not a candidate, and a union whose only literal properties are optional is read flat. What that vocabulary change now moves in the manifest is nothing at all (KL-1012d): a word list living on a property is neither an exported alias nor an array constant, so no leg watches it — reading it through a wrong rule was an accident that happened to make noise, not coverage.
|
|
26
|
+
- **The two refs agree on an alphabet before they give up on one.** Each ref records every property that discriminates a union's arms, and the diff reads both sides under one that qualifies at BOTH — because a release adding a second qualifying property moves the sort-order winner, and two key sets written in different alphabets fall back to the flat one, where an arm losing a key a sibling still spells moves nothing at all. Where no property qualifies at both, the flat comparison stands and the row carries `uncertain` saying so: an additive-only verdict this reading cannot support is never made in silence.
|
|
27
|
+
- **Three more shapes that read as "nothing moved".** A declaration emptied to `{}` used to leave the model altogether, so deleting every key of a reachable alias reported nothing (it is a reading now, not a missing one). A row whose arms could not be aligned across the two refs now SURVIVES a zero delta, carrying `uncertain`: where the comparison is the flat key set, a key that left one arm while a sibling still spells it shows up as nothing at all, and vanishing is the one thing that row must not do. A nested intersection arm (`A & (B & C)`) was collected only to its outermost operands, so the same type written the other way round grew an arm and swapping between the spellings came out as removed keys. And the four real-library range pins now live in `test/backlog1012-wire-typekeys-real-ranges.test.ts`, which fails loudly rather than skipping where the release tags are absent.
|
|
28
|
+
- **Compensation.** `v7.26.1 -> v7.26.2` is pinned as the regression control: every segment of the manifest in the tree comes back word for word and `typeKeys` may only GROW (`test/backlog1012-wire-typekeys-union-arms-and-denominator.test.ts` ④). On that range the wider reading really does add — `AskOutcome`'s `policy` arm and `Settlement`'s `policy_refused` word, both invisible to the old reader. KL-1012a records that an arm-prefixed key's `criteriaCovered` is always false (criteria cite real property chains; that field feeds no gate).
|
|
29
|
+
|
|
3
30
|
## 7.26.2 — 2026-09-21
|
|
4
31
|
|
|
5
32
|
### An assignment word is not a path; its VALUE is, and the question about a value is who may open it (#1004; behaviour NARROWING and RELAXING, both; +0 exports; @cli @test)
|
|
@@ -30,6 +30,10 @@ export interface PrepareDelegationSurfaceInput {
|
|
|
30
30
|
taskScope: string;
|
|
31
31
|
/** borrowed-readonly — the SETTLED task root (the worktree tools' repoRoot). */
|
|
32
32
|
taskRootFinal: string;
|
|
33
|
+
/** borrowed-readonly — the settled root in the CANONICAL spelling every fence judges with (the root-receiving
|
|
34
|
+
* point resolves it once). The Monitor mount's reset target: the shell tool is built with this same value, so
|
|
35
|
+
* a dead-cwd self-heal lands both tools on one directory rather than two spellings of it. */
|
|
36
|
+
taskRootCanonical: string;
|
|
33
37
|
/** borrowed-readonly — the run's resolved env the Monitor and worktree tools operate. */
|
|
34
38
|
executionEnv: ExecutionEnv;
|
|
35
39
|
/** borrowed-readonly — the trusted Runner self-reference the SendMessage / AgentTranscript mounts execute through; undefined
|
|
@@ -30,7 +30,7 @@ function buildTier3ReviveSpawn(delegationForRevive, enrichSpecToolCtx) {
|
|
|
30
30
|
};
|
|
31
31
|
}
|
|
32
32
|
export function prepareDelegationSurface(input) {
|
|
33
|
-
const { spec, deps, internals, resume, sessionId, hostTaskId, taskScope, taskRootFinal, executionEnv, runnerSelf, backgroundTaskToolsActive, workflowToolsActive, peerLaneActive, peerSendMessageBuiltIn, peerLaneRefs, roster, toolEffects, axisExplicitNegatives, envHandToolNames, toolFaceSnapshot, firstPartyOffload, offloadStore, subagentRetain, enrichSpecToolCtx, handsCwdRef, wsSnapshot, rebaseWsPath, rebaseRestoredPath, workspaceStateSettle } = input;
|
|
33
|
+
const { spec, deps, internals, resume, sessionId, hostTaskId, taskScope, taskRootFinal, taskRootCanonical, executionEnv, runnerSelf, backgroundTaskToolsActive, workflowToolsActive, peerLaneActive, peerSendMessageBuiltIn, peerLaneRefs, roster, toolEffects, axisExplicitNegatives, envHandToolNames, toolFaceSnapshot, firstPartyOffload, offloadStore, subagentRetain, enrichSpecToolCtx, handsCwdRef, wsSnapshot, rebaseWsPath, rebaseRestoredPath, workspaceStateSettle } = input;
|
|
34
34
|
let worktreeSessionRef;
|
|
35
35
|
const delegationSurfaceActive = backgroundTaskToolsActive || workflowToolsActive;
|
|
36
36
|
if (delegationSurfaceActive || internals?.parentNotify !== undefined || peerLaneActive) {
|
|
@@ -110,6 +110,7 @@ export function prepareDelegationSurface(input) {
|
|
|
110
110
|
...(sessionId !== undefined ? { sessionId } : {}),
|
|
111
111
|
...(internals?.onTaskNotification !== undefined ? { onTaskNotification: internals.onTaskNotification } : {}),
|
|
112
112
|
...(handsCwdRef !== undefined ? { cwdRef: handsCwdRef } : {}),
|
|
113
|
+
rootCanonical: taskRootCanonical,
|
|
113
114
|
...(offloadStore !== undefined ? { toolResultStore: offloadStore } : {}),
|
|
114
115
|
...(spec.retainBackgroundProcesses === true ? { retainBackgroundProcesses: true } : {}),
|
|
115
116
|
})), { source: "builtin", mountedBy: "backgroundTasks" });
|
|
@@ -523,7 +523,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
523
523
|
handsLessResolvedFace = handsReadFace.handsLessResolvedFace;
|
|
524
524
|
handsLessResolvedWriteFace = handsReadFace.handsLessResolvedWriteFace;
|
|
525
525
|
shellGatedTools = handsReadFace.shellGatedTools;
|
|
526
|
-
const { worktreeSessionRef } = prepareDelegationSurface({ spec, deps, internals, resume, sessionId, hostTaskId, taskScope, taskRootFinal, executionEnv, runnerSelf, backgroundTaskToolsActive, shellReadAdmission, workflowToolsActive, peerLaneActive, peerSendMessageBuiltIn, peerLaneRefs, roster, toolEffects, axisExplicitNegatives, envHandToolNames, toolFaceSnapshot, firstPartyOffload, offloadStore, subagentRetain, enrichSpecToolCtx, handsCwdRef, wsSnapshot, rebaseWsPath, rebaseRestoredPath, workspaceStateSettle });
|
|
526
|
+
const { worktreeSessionRef } = prepareDelegationSurface({ spec, deps, internals, resume, sessionId, hostTaskId, taskScope, taskRootFinal, taskRootCanonical, executionEnv, runnerSelf, backgroundTaskToolsActive, shellReadAdmission, workflowToolsActive, peerLaneActive, peerSendMessageBuiltIn, peerLaneRefs, roster, toolEffects, axisExplicitNegatives, envHandToolNames, toolFaceSnapshot, firstPartyOffload, offloadStore, subagentRetain, enrichSpecToolCtx, handsCwdRef, wsSnapshot, rebaseWsPath, rebaseRestoredPath, workspaceStateSettle });
|
|
527
527
|
const { liveQuestionFace, contentAskBindings, contentAskRoutable, lateStrandedAnswers, discloseStrandedAnswers, settleContentAskBindings, durableQuestionFace, questionToolMounted, mountedQuestionTool } = prepareQuestionFace({ frozenOnQuestion, spec, deps, checkpointStore, runtimeCaps, resolvedInteractionPosture, resume, sessionId, roster, inheritedUnavailableAsks: () => inheritedUnavailableAsks });
|
|
528
528
|
if (spec.handsReadOnly !== true) {
|
|
529
529
|
const sessionScope = sessionId ?? spec.principal ?? spec.taskId ?? "default";
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The tracked-cwd liveness probe and its diagnosis, shared by the three sites that spawn into the tracked
|
|
3
|
+
* logical cwd: the shell's foreground and background legs, and the watch tool's background leg. A second
|
|
4
|
+
* copy of either half would be a second answer to "is this directory usable?" and a second refusal
|
|
5
|
+
* wording for the same fact, so both are minted here once and each leg passes only its own tool name.
|
|
6
|
+
*/
|
|
7
|
+
import type { ExecutionEnv } from "../../internal/harness-types.js";
|
|
8
|
+
/**
|
|
9
|
+
* Is `dir` USABLE as a spawn cwd? "Dead" is the whole class, not just deletion: gone, replaced by a FILE
|
|
10
|
+
* of the same name or standing under one, or a symlink that does not resolve to a directory. `fileInfo`
|
|
11
|
+
* is lstat-semantics, so a symlink is judged by FOLLOWING it (`canonicalPath`, then one probe of the
|
|
12
|
+
* target — a link retargeted at a file resolves perfectly well and is still not a cwd).
|
|
13
|
+
*
|
|
14
|
+
* The failure direction is deliberately asymmetric. Where a failure says nothing about the chain (the
|
|
15
|
+
* initial lstat, the target's own), an error means death only when it SAYS so — refusing a command, or
|
|
16
|
+
* resetting somebody's working directory, over one flaky stat would be worse than the failure being
|
|
17
|
+
* diagnosed. The RESOLUTION step is the exception: a chain that will not resolve is no directory by any
|
|
18
|
+
* reading; see {@link resolutionFailureMeansDead} for the one code that step reads as death on top.
|
|
19
|
+
*/
|
|
20
|
+
export declare function isDeadCwd(env: ExecutionEnv, dir: string, signal?: AbortSignal): Promise<boolean>;
|
|
21
|
+
/**
|
|
22
|
+
* Dead-cwd diagnosis for a never-started command (spawn_error / shell_unavailable): a command that
|
|
23
|
+
* DELETED or REPLACED the tracked cwd (e.g. `opam clean` after `cd` into a build dir) leaves the
|
|
24
|
+
* persistent session pinned to a dead directory — every later spawn fails ENOENT/ENOTDIR with a message
|
|
25
|
+
* blaming the SHELL BINARY ("spawn /bin/bash ENOENT"), the model concludes bash is gone, and the run is
|
|
26
|
+
* permanently disabled (TB compile-compcert, 14 contaminated calls). The env can't distinguish this (spawn
|
|
27
|
+
* reports the same code either way), so probe here:
|
|
28
|
+
* - tracked cwd dead + task root alive → SELF-HEAL: reset the tracked cwd to the root, tell the model
|
|
29
|
+
* what happened and that a retry works;
|
|
30
|
+
* - tracked cwd dead + root ALSO dead (never promise a retry that must fail) → honest
|
|
31
|
+
* unrecoverable verdict;
|
|
32
|
+
* - cwd IS the root and it's dead (`rm -rf "$PWD"` in place — a diverged-guard would
|
|
33
|
+
* skip diagnosis entirely) → honest root-gone verdict (no reset target exists);
|
|
34
|
+
* - the caller knows NO root (`root === undefined` — a direct mount that was never handed one) → say the
|
|
35
|
+
* cwd is gone and that this tool has no reset target, and stop there. Calling that directory "the task
|
|
36
|
+
* root" or declaring the tree unrecoverable would both be claims the caller cannot back: the root may
|
|
37
|
+
* well be alive, and a `cd` to it recovers. Nothing is reset.
|
|
38
|
+
* - cwd healthy → return undefined (the spawn failure has another cause; keep the original error).
|
|
39
|
+
* Returns the model-facing message, or undefined when this is not a dead-cwd case. Uses only the public
|
|
40
|
+
* env faces (`fileInfo`/`canonicalPath`) — works for any env that maps never-started failures to
|
|
41
|
+
* spawn_error/shell_unavailable (contract note on ExecutionErrorCode).
|
|
42
|
+
*
|
|
43
|
+
* `root` is REQUIRED-but-nullable on purpose (not an optional parameter): a caller without a root has to
|
|
44
|
+
* say so at the call site, so "this mount has no root" can never be spelled the same way as "I forgot".
|
|
45
|
+
*/
|
|
46
|
+
export declare function diagnoseDeadCwd(env: ExecutionEnv, toolName: string, effectiveCwd: string, root: string | undefined, cwdRef?: {
|
|
47
|
+
current: string;
|
|
48
|
+
}, signal?: AbortSignal): Promise<string | undefined>;
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
function errorMeansDead(code) {
|
|
2
|
+
return code === "not_found" || code === "not_directory";
|
|
3
|
+
}
|
|
4
|
+
function resolutionFailureMeansDead(code) {
|
|
5
|
+
return errorMeansDead(code) || code === "unknown";
|
|
6
|
+
}
|
|
7
|
+
function cancelled(signal) {
|
|
8
|
+
return signal?.aborted === true;
|
|
9
|
+
}
|
|
10
|
+
export async function isDeadCwd(env, dir, signal) {
|
|
11
|
+
const info = await env.fileInfo(dir, signal);
|
|
12
|
+
if (!info.ok) {
|
|
13
|
+
if (info.error.code !== "unknown")
|
|
14
|
+
return errorMeansDead(info.error.code);
|
|
15
|
+
const resolved = await env.canonicalPath(dir, signal);
|
|
16
|
+
return !resolved.ok && resolutionFailureMeansDead(resolved.error.code);
|
|
17
|
+
}
|
|
18
|
+
if (info.value.kind === "directory")
|
|
19
|
+
return false;
|
|
20
|
+
if (info.value.kind !== "symlink")
|
|
21
|
+
return true;
|
|
22
|
+
const target = await env.canonicalPath(dir, signal);
|
|
23
|
+
if (!target.ok)
|
|
24
|
+
return resolutionFailureMeansDead(target.error.code);
|
|
25
|
+
const followed = await env.fileInfo(target.value, signal);
|
|
26
|
+
if (!followed.ok)
|
|
27
|
+
return errorMeansDead(followed.error.code);
|
|
28
|
+
return followed.value.kind !== "directory" && followed.value.kind !== "symlink";
|
|
29
|
+
}
|
|
30
|
+
export async function diagnoseDeadCwd(env, toolName, effectiveCwd, root, cwdRef, signal) {
|
|
31
|
+
if (!(await isDeadCwd(env, effectiveCwd, signal)))
|
|
32
|
+
return undefined;
|
|
33
|
+
if (cancelled(signal))
|
|
34
|
+
return undefined;
|
|
35
|
+
if (root === undefined) {
|
|
36
|
+
return (`Error (${toolName}): command did not run — the working directory no longer exists (or is no longer a directory): ${effectiveCwd} ` +
|
|
37
|
+
`(it was likely deleted or replaced by a previous command). No reset target is known to this tool; ` +
|
|
38
|
+
"re-establish the working directory with a Bash `cd` to a live directory and retry.");
|
|
39
|
+
}
|
|
40
|
+
if (cwdRef && cwdRef.current !== root) {
|
|
41
|
+
const rootAlive = !(await isDeadCwd(env, root, signal));
|
|
42
|
+
if (cancelled(signal))
|
|
43
|
+
return undefined;
|
|
44
|
+
if (rootAlive) {
|
|
45
|
+
cwdRef.current = root;
|
|
46
|
+
return (`Error (${toolName}): command did not run — the working directory no longer exists (or is no longer a directory): ${effectiveCwd}\n` +
|
|
47
|
+
`(it was likely deleted or replaced by a previous command). The working directory has been reset to ${root}; ` +
|
|
48
|
+
`re-run your command (cd first if you need a different directory).`);
|
|
49
|
+
}
|
|
50
|
+
return (`Error (${toolName}): command did not run — the working directory ${effectiveCwd} AND the task root ${root} no longer exist. ` +
|
|
51
|
+
`The shell has no usable directory to run in; this task cannot recover its working tree.`);
|
|
52
|
+
}
|
|
53
|
+
return (`Error (${toolName}): command did not run — the task root directory no longer exists (or is no longer a directory): ${effectiveCwd}. ` +
|
|
54
|
+
`It was likely deleted by a previous command (e.g. rm -rf on the current directory). There is no directory to reset to; ` +
|
|
55
|
+
`this task cannot recover its working tree.`);
|
|
56
|
+
}
|
package/dist/tools/fs/fs-bash.js
CHANGED
|
@@ -9,6 +9,7 @@ import { hasBackgroundShell } from "../../core/background-shell.js";
|
|
|
9
9
|
import { delimitUntrusted } from "../../core/untrusted-text.js";
|
|
10
10
|
import { MCP_IMAGE_MAX_BASE64 } from "../../core/mcp.js";
|
|
11
11
|
import { imageMagicMatches, withinAnyRoot } from "./safety.js";
|
|
12
|
+
import { isDeadCwd, diagnoseDeadCwd } from "./dead-cwd.js";
|
|
12
13
|
import { boundaryIsTheSandbox, hasDestroy, isIsolated } from "../../core/remote-env.js";
|
|
13
14
|
import { ghRateLimitHint } from "./gh-rate-limit.js";
|
|
14
15
|
import { resolveBashTimeoutCaps, bashTimeoutCapsSec, bashTimeoutArgRefusal, bashTimeoutParamDescription, envErrorDetail, bashMaxOutputChars, clipShellOutput, writeShellOverflowFile, shellRecoveryHint, CWD_SENTINEL, stripCwdSentinel, } from "./fs-shared.js";
|
|
@@ -98,35 +99,6 @@ function dataUriImageFromStdout(stdout) {
|
|
|
98
99
|
}
|
|
99
100
|
return undefined;
|
|
100
101
|
}
|
|
101
|
-
async function isDeadCwd(env, dir) {
|
|
102
|
-
const info = await env.fileInfo(dir);
|
|
103
|
-
if (!info.ok)
|
|
104
|
-
return info.error.code === "not_found";
|
|
105
|
-
if (info.value.kind === "directory")
|
|
106
|
-
return false;
|
|
107
|
-
if (info.value.kind === "symlink") {
|
|
108
|
-
const target = await env.canonicalPath(dir);
|
|
109
|
-
return !target.ok;
|
|
110
|
-
}
|
|
111
|
-
return true;
|
|
112
|
-
}
|
|
113
|
-
async function diagnoseDeadCwd(env, toolName, effectiveCwd, root, cwdRef) {
|
|
114
|
-
if (!(await isDeadCwd(env, effectiveCwd)))
|
|
115
|
-
return undefined;
|
|
116
|
-
if (cwdRef && cwdRef.current !== root) {
|
|
117
|
-
if (!(await isDeadCwd(env, root))) {
|
|
118
|
-
cwdRef.current = root;
|
|
119
|
-
return (`Error (${toolName}): command did not run — the working directory no longer exists (or is no longer a directory): ${effectiveCwd}\n` +
|
|
120
|
-
`(it was likely deleted or replaced by a previous command). The working directory has been reset to ${root}; ` +
|
|
121
|
-
`re-run your command (cd first if you need a different directory).`);
|
|
122
|
-
}
|
|
123
|
-
return (`Error (${toolName}): command did not run — the working directory ${effectiveCwd} AND the task root ${root} no longer exist. ` +
|
|
124
|
-
`The shell has no usable directory to run in; this task cannot recover its working tree.`);
|
|
125
|
-
}
|
|
126
|
-
return (`Error (${toolName}): command did not run — the task root directory no longer exists (or is no longer a directory): ${effectiveCwd}. ` +
|
|
127
|
-
`It was likely deleted by a previous command (e.g. rm -rf on the current directory). There is no directory to reset to; ` +
|
|
128
|
-
`this task cannot recover its working tree.`);
|
|
129
|
-
}
|
|
130
102
|
const AUTO_BACKGROUND_BLOCKED_FIRST_WORDS = new Set(["sleep"]);
|
|
131
103
|
function stripAutoBackgroundWrapperPrefixes(segment) {
|
|
132
104
|
let s = segment.trim();
|
|
@@ -371,7 +343,7 @@ async function runShell(env, cwd, toolName, command, timeoutSec, caps, signal, c
|
|
|
371
343
|
}
|
|
372
344
|
if (!res.ok) {
|
|
373
345
|
if (res.error.code === "spawn_error" || res.error.code === "shell_unavailable") {
|
|
374
|
-
const diagnosis = await diagnoseDeadCwd(env, toolName, effectiveCwd, cwd, cwdRef);
|
|
346
|
+
const diagnosis = await diagnoseDeadCwd(env, toolName, effectiveCwd, cwd, cwdRef, signal);
|
|
375
347
|
throw new Error(diagnosis ?? `Error (${toolName}): command did not run (${res.error.code}): ${res.error.message}`);
|
|
376
348
|
}
|
|
377
349
|
if (res.error.code === "transport_lost") {
|
|
@@ -656,8 +628,8 @@ export function createBashTool(env, rootCanonical, coAuthor = false, cwdRef = {
|
|
|
656
628
|
if (!hasBackgroundShell(env)) {
|
|
657
629
|
return errorResult("Error (Bash): this environment does not support background processes (run_in_background).");
|
|
658
630
|
}
|
|
659
|
-
if (cwdRef && (await isDeadCwd(env, cwdRef.current))) {
|
|
660
|
-
const diagnosis = await diagnoseDeadCwd(env, "Bash", cwdRef.current, rootCanonical, cwdRef);
|
|
631
|
+
if (cwdRef && (await isDeadCwd(env, cwdRef.current, ctx.signal))) {
|
|
632
|
+
const diagnosis = await diagnoseDeadCwd(env, "Bash", cwdRef.current, rootCanonical, cwdRef, ctx.signal);
|
|
661
633
|
if (diagnosis !== undefined)
|
|
662
634
|
throw new Error(diagnosis);
|
|
663
635
|
}
|
package/dist/tools/monitor.d.ts
CHANGED
|
@@ -78,5 +78,9 @@ export interface MonitorToolOptions {
|
|
|
78
78
|
* could only see the env lane, so a deployment retaining per spec still got the "session ends it"
|
|
79
79
|
* wording in the persistent-watch receipt. Purely a RECEIPT input — enforcement stays at the sweep. */
|
|
80
80
|
retainBackgroundProcesses?: boolean;
|
|
81
|
+
/** The task root in the SAME canonical spelling `createBashTool` is built with, so a dead-cwd self-heal
|
|
82
|
+
* lands both tools on one directory. Absent ⇒ the diagnosis says it has no reset target, rather than
|
|
83
|
+
* naming the cwd "the task root" or promising a recovery it cannot make. */
|
|
84
|
+
rootCanonical?: string;
|
|
81
85
|
}
|
|
82
86
|
export declare function createMonitorTool(env: ExecutionEnv, opts: MonitorToolOptions): AgentTool;
|
package/dist/tools/monitor.js
CHANGED
|
@@ -3,6 +3,7 @@ import { defineTool, errorResult } from "../core/tools.js";
|
|
|
3
3
|
import { hasBackgroundShell } from "../core/background-shell.js";
|
|
4
4
|
import { MONITOR_DEFAULT_TIMEOUT_MS, MONITOR_MAX_TIMEOUT_MS, } from "../core/task-registry.js";
|
|
5
5
|
import { toolFace } from "../core/tool-catalog-entries.js";
|
|
6
|
+
import { isDeadCwd, diagnoseDeadCwd } from "./fs/dead-cwd.js";
|
|
6
7
|
import { thrownText } from "../core/thrown-value.js";
|
|
7
8
|
const MONITOR_DESCRIPTION = "Run a shell command in the background and watch its stdout as a stream of events.\n" +
|
|
8
9
|
"\n" +
|
|
@@ -71,6 +72,11 @@ export function createMonitorTool(env, opts) {
|
|
|
71
72
|
const refused = await opts.shellReadAdmission?.("Monitor", ctx.toolCallId, command, ctx.signal);
|
|
72
73
|
if (refused !== undefined)
|
|
73
74
|
return refused;
|
|
75
|
+
if (opts.cwdRef !== undefined && (await isDeadCwd(env, opts.cwdRef.current, ctx.signal))) {
|
|
76
|
+
const diagnosis = await diagnoseDeadCwd(env, "Monitor", opts.cwdRef.current, opts.rootCanonical, opts.cwdRef, ctx.signal);
|
|
77
|
+
if (diagnosis !== undefined)
|
|
78
|
+
return errorResult(diagnosis);
|
|
79
|
+
}
|
|
74
80
|
if (ctx.signal?.aborted === true)
|
|
75
81
|
return errorResult("Error (Monitor): the call was cancelled before the watch started.");
|
|
76
82
|
const spawn = await env.spawnBackground(command, {
|
package/package.json
CHANGED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
{
|
|
2
|
+
"repo": "core",
|
|
3
|
+
"version": "7.26.3",
|
|
4
|
+
"from": "f41acbdc6dace7426c1a4dafa1a9c184d010cdb7",
|
|
5
|
+
"to": "v7.26.3",
|
|
6
|
+
"added": [],
|
|
7
|
+
"removed": [],
|
|
8
|
+
"changed": [],
|
|
9
|
+
"narrowed": [
|
|
10
|
+
{
|
|
11
|
+
"id": "F1",
|
|
12
|
+
"source": "behaviorFlips.summary",
|
|
13
|
+
"text": "NARROWING (#1018, P3, Monitor tool — user-visible): the watch tool probes the tracked working directory before it spawns, through the same probe and diagnosis the shell's legs use (`src/tools/fs/dead-cwd.ts`, one mint). A dead cwd (gone / replaced by a file / standing under a file / a symlink that does not resolve to a directory) refuses loudly with the shell's dead-cwd sentence and, when the task root is alive, resets the tracked cwd to the root (self-heal, same as Bash); a mount that was handed no root says it has no reset target. `MonitorToolOptions.rootCanonical?` (additive) carries the root in the same canonical spelling `createBashTool` receives; `prepareTask` wires it. Two probe shapes got truthful on BOTH legs: an ENOTDIR-class probe error and a symlink retargeted at a file now count as dead."
|
|
14
|
+
}
|
|
15
|
+
],
|
|
16
|
+
"closedSetMembers": [],
|
|
17
|
+
"closedSetsUnreadable": [],
|
|
18
|
+
"typeKeys": [
|
|
19
|
+
{
|
|
20
|
+
"type": "MonitorToolOptions",
|
|
21
|
+
"at": [
|
|
22
|
+
"src/tools/monitor.ts:61"
|
|
23
|
+
],
|
|
24
|
+
"added": [
|
|
25
|
+
"rootCanonical"
|
|
26
|
+
],
|
|
27
|
+
"removed": [],
|
|
28
|
+
"breaking": false,
|
|
29
|
+
"criteriaCovered": true
|
|
30
|
+
}
|
|
31
|
+
],
|
|
32
|
+
"typeKeysUnreadableArms": [],
|
|
33
|
+
"typeKeysFlattenedCount": 212,
|
|
34
|
+
"declarationMoves": [],
|
|
35
|
+
"mintMoves": [],
|
|
36
|
+
"criteriaObserves": [
|
|
37
|
+
{
|
|
38
|
+
"kind": "typeMember",
|
|
39
|
+
"name": "MonitorToolOptions.rootCanonical",
|
|
40
|
+
"assertions": [
|
|
41
|
+
"G1.a"
|
|
42
|
+
]
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
"kind": "typeMember",
|
|
46
|
+
"name": "RunInternals.inheritedGate",
|
|
47
|
+
"assertions": [
|
|
48
|
+
"G2.a"
|
|
49
|
+
]
|
|
50
|
+
}
|
|
51
|
+
],
|
|
52
|
+
"consumersHint": {
|
|
53
|
+
"cli": [
|
|
54
|
+
"MonitorToolOptions"
|
|
55
|
+
],
|
|
56
|
+
"server": [
|
|
57
|
+
"MonitorToolOptions"
|
|
58
|
+
],
|
|
59
|
+
"sdk": [],
|
|
60
|
+
"note": "scanned cli@7ea76c7c server@bcf41646 sdk@2bff51e — a name listed is MENTIONED there today, which is evidence of consumption, not proof of it"
|
|
61
|
+
},
|
|
62
|
+
"breakingCount": 0,
|
|
63
|
+
"breakingItems": [],
|
|
64
|
+
"toResolvedAt": "ee393c7d629f3ae8a2f3dbd906441aab40c31cd1"
|
|
65
|
+
}
|