@animalabs/agent-framework 0.28.0 → 0.29.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/README.md +88 -25
- package/dist/src/agent.d.ts.map +1 -1
- package/dist/src/agent.js +4 -1
- package/dist/src/agent.js.map +1 -1
- package/dist/src/api/mcp-server-core.d.ts.map +1 -1
- package/dist/src/api/mcp-server-core.js +3 -12
- package/dist/src/api/mcp-server-core.js.map +1 -1
- package/dist/src/api/server.d.ts.map +1 -1
- package/dist/src/api/server.js +7 -3
- package/dist/src/api/server.js.map +1 -1
- package/dist/src/auto-continue.d.ts +2 -0
- package/dist/src/auto-continue.d.ts.map +1 -1
- package/dist/src/auto-continue.js +5 -2
- package/dist/src/auto-continue.js.map +1 -1
- package/dist/src/code-execution/isolate/runner.d.ts +7 -5
- package/dist/src/code-execution/isolate/runner.d.ts.map +1 -1
- package/dist/src/code-execution/isolate/runner.js +7 -5
- package/dist/src/code-execution/isolate/runner.js.map +1 -1
- package/dist/src/code-execution/isolate/scripts.d.ts.map +1 -1
- package/dist/src/code-execution/isolate/scripts.js +3 -2
- package/dist/src/code-execution/isolate/scripts.js.map +1 -1
- package/dist/src/framework.d.ts +164 -36
- package/dist/src/framework.d.ts.map +1 -1
- package/dist/src/framework.js +1120 -479
- package/dist/src/framework.js.map +1 -1
- package/dist/src/gate/event-gate.d.ts +34 -10
- package/dist/src/gate/event-gate.d.ts.map +1 -1
- package/dist/src/gate/event-gate.js +118 -48
- package/dist/src/gate/event-gate.js.map +1 -1
- package/dist/src/gate/index.d.ts +1 -1
- package/dist/src/gate/index.d.ts.map +1 -1
- package/dist/src/gate/index.js +1 -1
- package/dist/src/gate/index.js.map +1 -1
- package/dist/src/gate/types.d.ts +12 -2
- package/dist/src/gate/types.d.ts.map +1 -1
- package/dist/src/heartbeat/heartbeat.d.ts +18 -0
- package/dist/src/heartbeat/heartbeat.d.ts.map +1 -1
- package/dist/src/heartbeat/heartbeat.js +34 -8
- package/dist/src/heartbeat/heartbeat.js.map +1 -1
- package/dist/src/held-writes.d.ts +3 -3
- package/dist/src/held-writes.js +3 -3
- package/dist/src/host-presentation.d.ts +170 -0
- package/dist/src/host-presentation.d.ts.map +1 -0
- package/dist/src/host-presentation.js +561 -0
- package/dist/src/host-presentation.js.map +1 -0
- package/dist/src/index.d.ts +2 -2
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +1 -1
- package/dist/src/index.js.map +1 -1
- package/dist/src/line-reader.d.ts +4 -0
- package/dist/src/line-reader.d.ts.map +1 -0
- package/dist/src/line-reader.js +59 -0
- package/dist/src/line-reader.js.map +1 -0
- package/dist/src/mcpl/channel-registry.d.ts +34 -6
- package/dist/src/mcpl/channel-registry.d.ts.map +1 -1
- package/dist/src/mcpl/channel-registry.js +131 -53
- package/dist/src/mcpl/channel-registry.js.map +1 -1
- package/dist/src/mcpl/conversation-router.d.ts.map +1 -1
- package/dist/src/mcpl/conversation-router.js +2 -1
- package/dist/src/mcpl/conversation-router.js.map +1 -1
- package/dist/src/mcpl/convert-block.d.ts +1 -0
- package/dist/src/mcpl/convert-block.d.ts.map +1 -1
- package/dist/src/mcpl/convert-block.js +16 -4
- package/dist/src/mcpl/convert-block.js.map +1 -1
- package/dist/src/mcpl/hook-orchestrator.d.ts.map +1 -1
- package/dist/src/mcpl/hook-orchestrator.js +4 -3
- package/dist/src/mcpl/hook-orchestrator.js.map +1 -1
- package/dist/src/mcpl/inbound-source.d.ts +3 -1
- package/dist/src/mcpl/inbound-source.d.ts.map +1 -1
- package/dist/src/mcpl/inbound-source.js +3 -1
- package/dist/src/mcpl/inbound-source.js.map +1 -1
- package/dist/src/mcpl/modern-connection.d.ts.map +1 -1
- package/dist/src/mcpl/modern-connection.js +11 -7
- package/dist/src/mcpl/modern-connection.js.map +1 -1
- package/dist/src/mcpl/protocol-family.d.ts +3 -2
- package/dist/src/mcpl/protocol-family.d.ts.map +1 -1
- package/dist/src/mcpl/protocol-family.js +25 -3
- package/dist/src/mcpl/protocol-family.js.map +1 -1
- package/dist/src/mcpl/push-handler.js +2 -2
- package/dist/src/mcpl/push-handler.js.map +1 -1
- package/dist/src/mcpl/references.d.ts +12 -40
- package/dist/src/mcpl/references.d.ts.map +1 -1
- package/dist/src/mcpl/references.js +29 -23
- package/dist/src/mcpl/references.js.map +1 -1
- package/dist/src/mcpl/server-connection.d.ts.map +1 -1
- package/dist/src/mcpl/server-connection.js +20 -20
- package/dist/src/mcpl/server-connection.js.map +1 -1
- package/dist/src/mcpl/skip-reply-wake.d.ts +25 -0
- package/dist/src/mcpl/skip-reply-wake.d.ts.map +1 -1
- package/dist/src/mcpl/skip-reply-wake.js +25 -0
- package/dist/src/mcpl/skip-reply-wake.js.map +1 -1
- package/dist/src/mcpl/tool-presence.d.ts.map +1 -1
- package/dist/src/mcpl/tool-presence.js +18 -17
- package/dist/src/mcpl/tool-presence.js.map +1 -1
- package/dist/src/mcpl/tool-signature.d.ts.map +1 -1
- package/dist/src/mcpl/tool-signature.js +17 -16
- package/dist/src/mcpl/tool-signature.js.map +1 -1
- package/dist/src/mcpl/toolset-host.d.ts +8 -26
- package/dist/src/mcpl/toolset-host.d.ts.map +1 -1
- package/dist/src/mcpl/toolset-host.js +16 -6
- package/dist/src/mcpl/toolset-host.js.map +1 -1
- package/dist/src/mcpl/toolsets-module.d.ts +14 -25
- package/dist/src/mcpl/toolsets-module.d.ts.map +1 -1
- package/dist/src/mcpl/toolsets-module.js +180 -50
- package/dist/src/mcpl/toolsets-module.js.map +1 -1
- package/dist/src/mcpl/transport.d.ts +1 -1
- package/dist/src/mcpl/transport.d.ts.map +1 -1
- package/dist/src/mcpl/transport.js +16 -16
- package/dist/src/mcpl/transport.js.map +1 -1
- package/dist/src/mcpl/types.d.ts +28 -12
- package/dist/src/mcpl/types.d.ts.map +1 -1
- package/dist/src/mcpl/types.js.map +1 -1
- package/dist/src/module-registry.d.ts +6 -7
- package/dist/src/module-registry.d.ts.map +1 -1
- package/dist/src/module-registry.js +6 -7
- package/dist/src/module-registry.js.map +1 -1
- package/dist/src/modules/api/index.d.ts.map +1 -1
- package/dist/src/modules/api/index.js +4 -2
- package/dist/src/modules/api/index.js.map +1 -1
- package/dist/src/modules/discord/index.d.ts.map +1 -1
- package/dist/src/modules/discord/index.js +33 -25
- package/dist/src/modules/discord/index.js.map +1 -1
- package/dist/src/modules/history/index.js +8 -6
- package/dist/src/modules/history/index.js.map +1 -1
- package/dist/src/modules/sender-name.d.ts +7 -0
- package/dist/src/modules/sender-name.d.ts.map +1 -0
- package/dist/src/modules/sender-name.js +10 -0
- package/dist/src/modules/sender-name.js.map +1 -0
- package/dist/src/modules/workspace/answers.d.ts +5 -1
- package/dist/src/modules/workspace/answers.d.ts.map +1 -1
- package/dist/src/modules/workspace/answers.js +16 -1
- package/dist/src/modules/workspace/answers.js.map +1 -1
- package/dist/src/modules/workspace/direct-write.d.ts.map +1 -1
- package/dist/src/modules/workspace/direct-write.js +6 -4
- package/dist/src/modules/workspace/direct-write.js.map +1 -1
- package/dist/src/modules/workspace/effects.d.ts +4 -2
- package/dist/src/modules/workspace/effects.d.ts.map +1 -1
- package/dist/src/modules/workspace/effects.js +50 -40
- package/dist/src/modules/workspace/effects.js.map +1 -1
- package/dist/src/modules/workspace/index.d.ts +1 -0
- package/dist/src/modules/workspace/index.d.ts.map +1 -1
- package/dist/src/modules/workspace/index.js +66 -36
- package/dist/src/modules/workspace/index.js.map +1 -1
- package/dist/src/modules/workspace/observe.d.ts +51 -20
- package/dist/src/modules/workspace/observe.d.ts.map +1 -1
- package/dist/src/modules/workspace/observe.js +120 -82
- package/dist/src/modules/workspace/observe.js.map +1 -1
- package/dist/src/modules/workspace/reconcile.d.ts +7 -1
- package/dist/src/modules/workspace/reconcile.d.ts.map +1 -1
- package/dist/src/modules/workspace/reconcile.js +133 -37
- package/dist/src/modules/workspace/reconcile.js.map +1 -1
- package/dist/src/notice-tags.d.ts +79 -0
- package/dist/src/notice-tags.d.ts.map +1 -0
- package/dist/src/notice-tags.js +83 -0
- package/dist/src/notice-tags.js.map +1 -0
- package/dist/src/reasoning-loss-notice.d.ts.map +1 -1
- package/dist/src/reasoning-loss-notice.js +5 -3
- package/dist/src/reasoning-loss-notice.js.map +1 -1
- package/dist/src/silent-heartbeat.d.ts +1 -53
- package/dist/src/silent-heartbeat.d.ts.map +1 -1
- package/dist/src/silent-heartbeat.js +56 -3
- package/dist/src/silent-heartbeat.js.map +1 -1
- package/dist/src/supplied-text.d.ts +35 -0
- package/dist/src/supplied-text.d.ts.map +1 -0
- package/dist/src/supplied-text.js +141 -0
- package/dist/src/supplied-text.js.map +1 -0
- package/dist/src/tool-args.d.ts.map +1 -1
- package/dist/src/tool-args.js +9 -8
- package/dist/src/tool-args.js.map +1 -1
- package/dist/src/tool-image-ledger.d.ts.map +1 -1
- package/dist/src/tool-image-ledger.js +2 -1
- package/dist/src/tool-image-ledger.js.map +1 -1
- package/dist/src/tool-presentation.d.ts.map +1 -1
- package/dist/src/tool-presentation.js +5 -4
- package/dist/src/tool-presentation.js.map +1 -1
- package/dist/src/tune-out/coordinator.d.ts +3 -2
- package/dist/src/tune-out/coordinator.d.ts.map +1 -1
- package/dist/src/tune-out/coordinator.js +24 -26
- package/dist/src/tune-out/coordinator.js.map +1 -1
- package/dist/src/types/agent.d.ts +23 -9
- package/dist/src/types/agent.d.ts.map +1 -1
- package/dist/src/types/framework.d.ts +43 -0
- package/dist/src/types/framework.d.ts.map +1 -1
- package/dist/src/types/module.d.ts +22 -11
- package/dist/src/types/module.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/agent.ts +4 -1
- package/src/api/mcp-server-core.ts +3 -14
- package/src/api/server.ts +7 -3
- package/src/auto-continue.ts +8 -2
- package/src/code-execution/isolate/runner.ts +7 -5
- package/src/code-execution/isolate/scripts.ts +3 -2
- package/src/framework.ts +1132 -492
- package/src/gate/event-gate.ts +128 -47
- package/src/gate/index.ts +1 -1
- package/src/gate/types.ts +12 -2
- package/src/heartbeat/heartbeat.ts +48 -9
- package/src/held-writes.ts +3 -3
- package/src/host-presentation.ts +605 -0
- package/src/index.ts +2 -2
- package/src/line-reader.ts +58 -0
- package/src/mcpl/channel-registry.ts +137 -52
- package/src/mcpl/conversation-router.ts +2 -1
- package/src/mcpl/convert-block.ts +17 -4
- package/src/mcpl/hook-orchestrator.ts +4 -3
- package/src/mcpl/inbound-source.ts +3 -1
- package/src/mcpl/modern-connection.ts +11 -7
- package/src/mcpl/protocol-family.ts +24 -3
- package/src/mcpl/push-handler.ts +2 -2
- package/src/mcpl/references.ts +31 -23
- package/src/mcpl/server-connection.ts +20 -18
- package/src/mcpl/skip-reply-wake.ts +35 -0
- package/src/mcpl/tool-presence.ts +18 -17
- package/src/mcpl/tool-signature.ts +17 -16
- package/src/mcpl/toolset-host.ts +18 -6
- package/src/mcpl/toolsets-module.ts +142 -50
- package/src/mcpl/transport.ts +18 -14
- package/src/mcpl/types.ts +29 -12
- package/src/module-registry.ts +6 -7
- package/src/modules/api/index.ts +4 -2
- package/src/modules/discord/index.ts +37 -25
- package/src/modules/history/index.ts +8 -6
- package/src/modules/sender-name.ts +9 -0
- package/src/modules/workspace/answers.ts +20 -2
- package/src/modules/workspace/direct-write.ts +4 -3
- package/src/modules/workspace/effects.ts +50 -40
- package/src/modules/workspace/index.ts +58 -34
- package/src/modules/workspace/observe.ts +139 -88
- package/src/modules/workspace/reconcile.ts +159 -36
- package/src/notice-tags.ts +84 -0
- package/src/reasoning-loss-notice.ts +5 -3
- package/src/silent-heartbeat.ts +4 -3
- package/src/supplied-text.ts +154 -0
- package/src/tool-args.ts +9 -8
- package/src/tool-image-ledger.ts +2 -1
- package/src/tool-presentation.ts +5 -4
- package/src/tune-out/coordinator.ts +25 -27
- package/src/types/agent.ts +23 -9
- package/src/types/framework.ts +44 -0
- package/src/types/module.ts +23 -11
|
@@ -45,12 +45,16 @@ import { constants as fsConstants } from 'node:fs';
|
|
|
45
45
|
import { lstat, open, realpath, stat, unlink } from 'node:fs/promises';
|
|
46
46
|
import type { FileHandle } from 'node:fs/promises';
|
|
47
47
|
import {
|
|
48
|
+
type Cause,
|
|
48
49
|
type CheckedDir,
|
|
49
50
|
type MountView,
|
|
51
|
+
DIRECTORY,
|
|
50
52
|
LEAVES_MOUNT,
|
|
53
|
+
LINK_TO_DIRECTORY,
|
|
51
54
|
NONBLOCK,
|
|
52
55
|
NOT_A_DIRECTORY,
|
|
53
56
|
NOT_FOLLOWED,
|
|
57
|
+
NOT_REGULAR,
|
|
54
58
|
OUTSIDE_PARENT,
|
|
55
59
|
ROOT_REPLACED,
|
|
56
60
|
contained,
|
|
@@ -63,6 +67,7 @@ import {
|
|
|
63
67
|
nestedAtReal,
|
|
64
68
|
parentOf,
|
|
65
69
|
} from './observe.js';
|
|
70
|
+
import { errnoCode, systemWords } from './answers.js';
|
|
66
71
|
|
|
67
72
|
/** What a write or unlink may find at its path when it acts. */
|
|
68
73
|
export type Expect =
|
|
@@ -79,12 +84,18 @@ export class EffectFailed extends Error {
|
|
|
79
84
|
/** Whether the path's entry was touched before the failure. */
|
|
80
85
|
readonly touched: boolean;
|
|
81
86
|
|
|
82
|
-
constructor(message: string, touched: boolean) {
|
|
87
|
+
constructor(message: string, touched: boolean, readonly detail: Cause = {}) {
|
|
83
88
|
super(message);
|
|
84
89
|
this.touched = touched;
|
|
85
90
|
}
|
|
86
91
|
}
|
|
87
92
|
|
|
93
|
+
export function callFailed(what: string, err: unknown, touched: boolean, sep: 'paren' | 'colon' = 'paren'): EffectFailed {
|
|
94
|
+
const code = errnoCode(err);
|
|
95
|
+
const words = systemWords(code, err);
|
|
96
|
+
return new EffectFailed(sep === 'paren' ? `${what} (${words})` : `${what}: ${words}`, touched, code !== undefined ? { errno: code } : {});
|
|
97
|
+
}
|
|
98
|
+
|
|
88
99
|
const CHANGED = 'disk changed since it was checked';
|
|
89
100
|
const MODE_CHANGED = 'disk\'s mode changed since it was checked; a sync takes it in';
|
|
90
101
|
/**
|
|
@@ -115,10 +126,6 @@ const HARD_LINKED = 'a file with other hard links, which writing it in place wou
|
|
|
115
126
|
+ 'this; to write this path alone, give it a copy of its own first (copy it to a new name, then move that over it) '
|
|
116
127
|
+ 'and materialize again';
|
|
117
128
|
|
|
118
|
-
function message(err: unknown): string {
|
|
119
|
-
return err instanceof Error ? err.message : String(err);
|
|
120
|
-
}
|
|
121
|
-
|
|
122
129
|
function noFollowFlag(view: MountView): number {
|
|
123
130
|
return !view.followSymlinks && typeof fsConstants.O_NOFOLLOW === 'number' ? fsConstants.O_NOFOLLOW : 0;
|
|
124
131
|
}
|
|
@@ -134,9 +141,9 @@ function noFollowFlag(view: MountView): number {
|
|
|
134
141
|
*/
|
|
135
142
|
async function parentFor(view: MountView, rootReal: string, rel: string, create: boolean) {
|
|
136
143
|
const root = await checkRoot(view, rootReal);
|
|
137
|
-
if ('reason' in root) throw new EffectFailed(root.reason, false);
|
|
144
|
+
if ('reason' in root) throw new EffectFailed(root.reason, false, root.errno !== undefined ? { errno: root.errno } : {});
|
|
138
145
|
const checked = await checkDir(view, rootReal, parentOf(rel), { create });
|
|
139
|
-
if (checked.kind === 'unobserved') throw new EffectFailed(checked.outside ? OUTSIDE_PARENT : checked.reason, false);
|
|
146
|
+
if (checked.kind === 'unobserved') throw new EffectFailed(checked.outside ? OUTSIDE_PARENT : checked.reason, false, checked.errno !== undefined ? { errno: checked.errno } : {});
|
|
140
147
|
return { ...checked, root: root.dir };
|
|
141
148
|
}
|
|
142
149
|
|
|
@@ -171,11 +178,11 @@ async function hashAt(target: string, id: { dev: bigint; ino: bigint }, flags: n
|
|
|
171
178
|
try {
|
|
172
179
|
handle = await open(target, fsConstants.O_RDONLY | NONBLOCK | flags);
|
|
173
180
|
} catch (err) {
|
|
174
|
-
throw
|
|
181
|
+
throw callFailed('cannot verify the disk copy', err, false);
|
|
175
182
|
}
|
|
176
183
|
try {
|
|
177
184
|
const info = await handle.stat({ bigint: true });
|
|
178
|
-
if (info.dev !== id.dev || info.ino !== id.ino) throw new EffectFailed(CHANGED, false);
|
|
185
|
+
if (info.dev !== id.dev || info.ino !== id.ino) throw new EffectFailed(CHANGED, false, { again: true });
|
|
179
186
|
const hash = createHash('sha256');
|
|
180
187
|
const chunk = Buffer.alloc(1 << 20);
|
|
181
188
|
for (let offset = 0; ;) {
|
|
@@ -185,6 +192,9 @@ async function hashAt(target: string, id: { dev: bigint; ino: bigint }, flags: n
|
|
|
185
192
|
offset += bytesRead;
|
|
186
193
|
}
|
|
187
194
|
return hash.digest('hex');
|
|
195
|
+
} catch (err) {
|
|
196
|
+
if (err instanceof EffectFailed) throw err;
|
|
197
|
+
throw callFailed('cannot verify the disk copy', err, false);
|
|
188
198
|
} finally {
|
|
189
199
|
await handle.close();
|
|
190
200
|
}
|
|
@@ -266,10 +276,10 @@ export async function writeContained(
|
|
|
266
276
|
const leads = link ? await stat(target).then(() => 'somewhere', (e: unknown) => errno(e)) : null;
|
|
267
277
|
throw new EffectFailed(leads === 'ENOENT' || leads === 'ENOTDIR'
|
|
268
278
|
? 'a symlink that leads nowhere is at this path, and a push makes a file only where nothing is, forced or not; '
|
|
269
|
-
+ 'whoever runs this host can remove the link
|
|
270
|
-
: 'something appeared at this path since it was checked', false);
|
|
279
|
+
+ 'whoever runs this host can remove the link, and materializing again then gets past it'
|
|
280
|
+
: 'something appeared at this path since it was checked', false, leads === 'ENOENT' || leads === 'ENOTDIR' ? {} : { again: true });
|
|
271
281
|
}
|
|
272
|
-
throw
|
|
282
|
+
throw callFailed('cannot create the file', err, false);
|
|
273
283
|
}
|
|
274
284
|
};
|
|
275
285
|
let handle: FileHandle;
|
|
@@ -282,11 +292,11 @@ export async function writeContained(
|
|
|
282
292
|
} catch (err) {
|
|
283
293
|
const code = errno(err);
|
|
284
294
|
if (code === 'ENOENT' && expect.kind === 'any') handle = await create();
|
|
285
|
-
else if (code === 'ENOENT') throw new EffectFailed('the file was removed since it was checked', false);
|
|
295
|
+
else if (code === 'ENOENT') throw new EffectFailed('the file was removed since it was checked', false, { again: true });
|
|
286
296
|
else if (code === 'ELOOP' && !view.followSymlinks) throw new EffectFailed(NOT_FOLLOWED, false);
|
|
287
|
-
else if (code === 'EISDIR') throw new EffectFailed(
|
|
288
|
-
else if (code === 'ENXIO') throw new EffectFailed(
|
|
289
|
-
else throw
|
|
297
|
+
else if (code === 'EISDIR') throw new EffectFailed((await lstat(target).catch(() => null))?.isSymbolicLink() === true ? LINK_TO_DIRECTORY : DIRECTORY, false);
|
|
298
|
+
else if (code === 'ENXIO') throw new EffectFailed(NOT_REGULAR, false); // a FIFO with no reader
|
|
299
|
+
else throw callFailed('cannot open the file', err, false);
|
|
290
300
|
}
|
|
291
301
|
}
|
|
292
302
|
|
|
@@ -379,7 +389,7 @@ export async function writeContained(
|
|
|
379
389
|
return { perm: Number((await handle.stat({ bigint: true })).mode & 0o777n), widenRefused, modeKept };
|
|
380
390
|
} catch (err) {
|
|
381
391
|
if (err instanceof EffectFailed) throw err;
|
|
382
|
-
throw
|
|
392
|
+
throw callFailed(`could not ${stage}`, err, touched, 'colon');
|
|
383
393
|
} finally {
|
|
384
394
|
await handle.close();
|
|
385
395
|
}
|
|
@@ -429,30 +439,30 @@ async function checkOpened(
|
|
|
429
439
|
touched: boolean,
|
|
430
440
|
): Promise<void> {
|
|
431
441
|
const info = await handle.stat({ bigint: true });
|
|
432
|
-
if (!info.isFile()) throw new EffectFailed(
|
|
442
|
+
if (!info.isFile()) throw new EffectFailed(NOT_REGULAR, touched);
|
|
433
443
|
const located = async (): Promise<string> => {
|
|
434
444
|
const real = await realpath(target).catch(() => null);
|
|
435
|
-
if (real === null) throw new EffectFailed(CHANGED, touched);
|
|
445
|
+
if (real === null) throw new EffectFailed(CHANGED, touched, { again: true });
|
|
436
446
|
const refused = await refusedLanding(view, rootReal, rel, real);
|
|
437
447
|
if (refused !== null) throw new EffectFailed(refused, touched);
|
|
438
448
|
return real;
|
|
439
449
|
};
|
|
440
450
|
if (expect.kind === 'content') {
|
|
441
451
|
await located();
|
|
442
|
-
if ((await hashAt(target, info, noFollow)) !== expect.hash) throw new EffectFailed(CHANGED, touched);
|
|
452
|
+
if ((await hashAt(target, info, noFollow)) !== expect.hash) throw new EffectFailed(CHANGED, touched, { again: true });
|
|
443
453
|
}
|
|
444
454
|
const real = await located();
|
|
445
455
|
const named = await stat(real, { bigint: true }).catch(() => null);
|
|
446
|
-
if (named === null || named.dev !== info.dev || named.ino !== info.ino) throw new EffectFailed(CHANGED, touched);
|
|
456
|
+
if (named === null || named.dev !== info.dev || named.ino !== info.ino) throw new EffectFailed(CHANGED, touched, { again: true });
|
|
447
457
|
if (!view.followSymlinks) {
|
|
448
458
|
const entry = await lstat(target, { bigint: true }).catch(() => null);
|
|
449
459
|
if (entry?.isSymbolicLink()) throw new EffectFailed(NOT_FOLLOWED, touched);
|
|
450
|
-
if (entry === null || !entry.isFile() || entry.dev !== info.dev || entry.ino !== info.ino) throw new EffectFailed(CHANGED, touched);
|
|
460
|
+
if (entry === null || !entry.isFile() || entry.dev !== info.dev || entry.ino !== info.ino) throw new EffectFailed(CHANGED, touched, { again: true });
|
|
451
461
|
}
|
|
452
462
|
if (info.nlink > 1n) throw new EffectFailed(HARD_LINKED, touched);
|
|
453
463
|
const gone = await moved(root, parent);
|
|
454
|
-
if (gone !== null) throw new EffectFailed(gone, touched);
|
|
455
|
-
if (!unchanged(info, await handle.stat({ bigint: true }).catch(() => null))) throw new EffectFailed(CHANGED, touched);
|
|
464
|
+
if (gone !== null) throw new EffectFailed(gone, touched, gone === MOVED ? { again: true } : {});
|
|
465
|
+
if (!unchanged(info, await handle.stat({ bigint: true }).catch(() => null))) throw new EffectFailed(CHANGED, touched, { again: true });
|
|
456
466
|
}
|
|
457
467
|
|
|
458
468
|
/** The sticky bit, the one special bit the chmod before a write's bytes keeps. */
|
|
@@ -490,7 +500,7 @@ export async function chmodContained(
|
|
|
490
500
|
view: MountView, rootReal: string, rel: string, mode: number, expect: Expect, from?: number,
|
|
491
501
|
): Promise<{ refused: boolean; narrowing: boolean }> {
|
|
492
502
|
const checked = await parentFor(view, rootReal, rel, false);
|
|
493
|
-
if (checked.kind !== 'checked') throw new EffectFailed('the file was removed since it was checked', false);
|
|
503
|
+
if (checked.kind !== 'checked') throw new EffectFailed('the file was removed since it was checked', false, { again: true });
|
|
494
504
|
const parent = checked.dir;
|
|
495
505
|
const root = checked.root;
|
|
496
506
|
const target = parent.at(nameOf(rel));
|
|
@@ -503,9 +513,9 @@ export async function chmodContained(
|
|
|
503
513
|
handle = await open(target, fsConstants.O_RDONLY | NONBLOCK | noFollow);
|
|
504
514
|
} catch (err) {
|
|
505
515
|
const code = errno(err);
|
|
506
|
-
if (code === 'ENOENT') throw new EffectFailed('the file was removed since it was checked', false);
|
|
516
|
+
if (code === 'ENOENT') throw new EffectFailed('the file was removed since it was checked', false, { again: true });
|
|
507
517
|
if (code === 'ELOOP' && !view.followSymlinks) throw new EffectFailed(NOT_FOLLOWED, false);
|
|
508
|
-
throw
|
|
518
|
+
throw callFailed('cannot open the file', err, false);
|
|
509
519
|
}
|
|
510
520
|
let touched = false;
|
|
511
521
|
let stage = 'set the file\'s mode';
|
|
@@ -526,7 +536,7 @@ export async function chmodContained(
|
|
|
526
536
|
return { refused: false, narrowing };
|
|
527
537
|
} catch (err) {
|
|
528
538
|
if (err instanceof EffectFailed) throw err;
|
|
529
|
-
throw
|
|
539
|
+
throw callFailed(`could not ${stage}`, err, touched, 'colon');
|
|
530
540
|
} finally {
|
|
531
541
|
await handle.close();
|
|
532
542
|
}
|
|
@@ -559,16 +569,16 @@ export async function unlinkContained(view: MountView, rootReal: string, rel: st
|
|
|
559
569
|
} catch (err) {
|
|
560
570
|
if (errno(err) === 'ENOENT') {
|
|
561
571
|
const gone = await moved(root, parent);
|
|
562
|
-
if (gone !== null) throw new EffectFailed(gone, false);
|
|
572
|
+
if (gone !== null) throw new EffectFailed(gone, false, gone === MOVED ? { again: true } : {});
|
|
563
573
|
return parent.rel;
|
|
564
574
|
}
|
|
565
|
-
throw
|
|
575
|
+
throw callFailed('cannot stat', err, false);
|
|
566
576
|
}
|
|
567
577
|
// Confirming an unlink that already happened never removes a file that
|
|
568
578
|
// appeared since.
|
|
569
579
|
if (expect.kind === 'absent') {
|
|
570
580
|
const kind = info.isDirectory() ? 'a directory' : info.isSymbolicLink() ? 'a symlink' : info.isFile() ? 'a file' : 'a pipe, socket or device';
|
|
571
|
-
throw new EffectFailed(`${kind} appeared at this path since it was checked`, false);
|
|
581
|
+
throw new EffectFailed(`${kind} appeared at this path since it was checked`, false, { again: true });
|
|
572
582
|
}
|
|
573
583
|
let file = info;
|
|
574
584
|
if (info.isSymbolicLink()) {
|
|
@@ -586,21 +596,21 @@ export async function unlinkContained(view: MountView, rootReal: string, rel: st
|
|
|
586
596
|
throw new EffectFailed(holder.names(directory) ? leadsInto(view.name, rel, holder) : LEAVES_MOUNT, false);
|
|
587
597
|
}
|
|
588
598
|
const through = await stat(real, { bigint: true }).catch(() => null);
|
|
589
|
-
if (through === null) throw new EffectFailed(CHANGED, false);
|
|
599
|
+
if (through === null) throw new EffectFailed(CHANGED, false, { again: true });
|
|
590
600
|
// A followed link stands for the file it leads to, so it's removed only
|
|
591
601
|
// for a regular one, as the file the workspace deleted. This refusal,
|
|
592
602
|
// and the one below for what isn't a regular file at the path, use
|
|
593
603
|
// observation's words for the path, so the push answers a forced unlink
|
|
594
604
|
// and an unforced one alike (reconcile.ts inTheWay).
|
|
595
|
-
if (!through.isFile()) throw new EffectFailed(through.isDirectory() ?
|
|
605
|
+
if (!through.isFile()) throw new EffectFailed(through.isDirectory() ? LINK_TO_DIRECTORY : NOT_REGULAR, false);
|
|
596
606
|
// One leading to a file this mount's own include leaves out leaves the
|
|
597
607
|
// mount too, and that file isn't read for a hash (coversLanding).
|
|
598
608
|
if (!(await coversLanding(view, rootReal, real, rel))) throw new EffectFailed(LEAVES_MOUNT, false);
|
|
599
609
|
file = through;
|
|
600
610
|
} else if (info.isDirectory()) {
|
|
601
|
-
throw new EffectFailed(
|
|
611
|
+
throw new EffectFailed(DIRECTORY, false);
|
|
602
612
|
} else if (!info.isFile()) {
|
|
603
|
-
throw new EffectFailed(
|
|
613
|
+
throw new EffectFailed(NOT_REGULAR, false);
|
|
604
614
|
} else {
|
|
605
615
|
// A regular file reached through a link among its directories lands
|
|
606
616
|
// where the mount may not act: in a nested mount's directory, or on a
|
|
@@ -610,15 +620,15 @@ export async function unlinkContained(view: MountView, rootReal: string, rel: st
|
|
|
610
620
|
const refused = await refusedLanding(view, rootReal, rel, target);
|
|
611
621
|
if (refused !== null) throw new EffectFailed(refused, false);
|
|
612
622
|
}
|
|
613
|
-
if (expect.kind === 'content' && (await hashAt(target, file, noFollowFlag(view))) !== expect.hash) throw new EffectFailed(CHANGED, false);
|
|
623
|
+
if (expect.kind === 'content' && (await hashAt(target, file, noFollowFlag(view))) !== expect.hash) throw new EffectFailed(CHANGED, false, { again: true });
|
|
614
624
|
const gone = await moved(root, parent);
|
|
615
|
-
if (gone !== null) throw new EffectFailed(gone, false);
|
|
625
|
+
if (gone !== null) throw new EffectFailed(gone, false, gone === MOVED ? { again: true } : {});
|
|
616
626
|
// The entry checked is still the one at the path, unwritten since it was
|
|
617
627
|
// hashed, as close to the unlink as Node allows (see the header): a file
|
|
618
628
|
// replaced or edited while it was hashed isn't removed.
|
|
619
629
|
const again = await lstat(target, { bigint: true }).catch(() => null);
|
|
620
630
|
const named = again?.isSymbolicLink() ? await stat(target, { bigint: true }).catch(() => null) : again;
|
|
621
|
-
if (!unchanged(file, named)) throw new EffectFailed(CHANGED, false);
|
|
631
|
+
if (!unchanged(file, named)) throw new EffectFailed(CHANGED, false, { again: true });
|
|
622
632
|
try {
|
|
623
633
|
await unlink(target);
|
|
624
634
|
} catch (err) {
|
|
@@ -626,7 +636,7 @@ export async function unlinkContained(view: MountView, rootReal: string, rel: st
|
|
|
626
636
|
// An unlink is all or nothing: one refused outright changed nothing, so
|
|
627
637
|
// the path's evidence can go back as it was. Anything else may have
|
|
628
638
|
// landed (a network filesystem's EIO, say): the next observation resolves it.
|
|
629
|
-
if (code !== 'ENOENT') throw
|
|
639
|
+
if (code !== 'ENOENT') throw callFailed('could not unlink the file', err, !UNLINK_REFUSALS.has(code ?? ''), 'colon');
|
|
630
640
|
}
|
|
631
641
|
return parent.rel;
|
|
632
642
|
}
|
|
@@ -661,7 +671,7 @@ export async function syncDirectories(view: MountView, rootReal: string, rels: I
|
|
|
661
671
|
await checked.dir.sync();
|
|
662
672
|
if (!(await root.dir.stillHere())) failed.set(rel, ROOT_REPLACED);
|
|
663
673
|
} catch (err) {
|
|
664
|
-
failed.set(rel,
|
|
674
|
+
failed.set(rel, systemWords(errno(err), err));
|
|
665
675
|
}
|
|
666
676
|
}
|
|
667
677
|
return failed;
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* auto-sync between real filesystem and Chronicle, and manual materialization.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
+
import { NOTICE_TAG } from '../../notice-tags.js';
|
|
8
9
|
import { constants as fsConstants } from 'node:fs';
|
|
9
10
|
import type { BigIntStats, Stats } from 'node:fs';
|
|
10
11
|
import { open, readdir, readlink, stat, lstat, realpath } from 'node:fs/promises';
|
|
@@ -40,7 +41,7 @@ import { MountWatcher, type FsChange } from './watcher.js';
|
|
|
40
41
|
import { hashContent, storedBytes, DEFAULT_MAX_FILE_SIZE, type StoredBytes } from './sync.js';
|
|
41
42
|
import { DiskAgreement, EvidenceUnavailable, callGate, sameRootIdentity } from './disk-agreement.js';
|
|
42
43
|
import { type BranchIntent, BranchIntents } from './branch-intent.js';
|
|
43
|
-
import { CHANGED, DIRECTORY, IGNORED,
|
|
44
|
+
import { CHANGED, type Cause, DIRECTORY, IGNORED, NONBLOCK, OTHER_MOUNT, OUTSIDE_INCLUDE, ROOT_REPLACED, ROOT_UNAVAILABLE, beneathRoot, checkDir, checkRoot, confirmAbsent, coversLanding, directoryAt, diskSpelling, filesystemTrustsCtime, foldsCase, inOtherMount, insideMount, leadsInto, linkOnTheWay, linkRefusal, linkWords, locateRoot, looksBinary, namesHolder, nestedAtReal, nestedGuess, nestedHolder, nestedStep, observePath, parsedSpelling, passedOverAt, pathSegments, patternRefusal, readBytes, readPrefix, realSegments, restBeneath, rootAbsence, sameRoot, shownLeads, sniffImageMime, surelyAtRoot, walkScope, withinIncludes, type CheckedDir, type LinkFound, type MountView, type NestedGuess, type NestedMount, type WorkspaceEntries } from './observe.js';
|
|
44
45
|
import { TOOL_BATCH_HOOK_TIMEOUT_MS, ensureStateRegistered } from '../../module-registry.js';
|
|
45
46
|
import { DEFAULT_TOOL_RESULT_INLINE_MAX_CHARS } from '../../tool-result-history.js';
|
|
46
47
|
import { type EditSpec, type FilePlan, countBudget, decodeForEdit, hintBudget, planFile } from './edit-plan.js';
|
|
@@ -60,12 +61,14 @@ import {
|
|
|
60
61
|
NOT_PUSHED_IN_OTHER_MOUNT,
|
|
61
62
|
WALK_CAP,
|
|
62
63
|
entryMode,
|
|
64
|
+
explained,
|
|
63
65
|
pushPaths,
|
|
64
66
|
reconcilePass,
|
|
65
67
|
settleAdoptionBeforeMutation,
|
|
68
|
+
wayOn,
|
|
66
69
|
} from './reconcile.js';
|
|
67
70
|
import { EffectFailed, unlinkContained } from './effects.js';
|
|
68
|
-
import { AWAITS_REOPEN,
|
|
71
|
+
import { AWAITS_REOPEN, MOUNT_IT, ROOT_RECHECK, Refusal, UNFOLLOWED_STEP, changesFiles, didNothing, errnoCode, errnoWayOn, errnoWords, hostFix, onlyTheHost, systemWords, unexpectedAnswer } from './answers.js';
|
|
69
72
|
import { unknownToolMessage } from '../unknown-tool.js';
|
|
70
73
|
import { closestNames } from '../../closest-names.js';
|
|
71
74
|
import { RecordJournal } from '../../record-journal.js';
|
|
@@ -270,6 +273,12 @@ class NotRegularFile extends WorkspaceReadError {
|
|
|
270
273
|
}
|
|
271
274
|
}
|
|
272
275
|
|
|
276
|
+
class LinkOnTheWay extends WorkspaceReadError {
|
|
277
|
+
constructor(code: WorkspaceReadErrorCode, message: string, readonly reason: string, errno?: string) {
|
|
278
|
+
super(code, message, 'readlink', errno);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
273
282
|
class RootUnavailable extends WorkspaceReadError {
|
|
274
283
|
constructor(readonly mount: string, readonly reason: string | null, stage: WorkspaceReadStage, errno?: string, readonly refusal?: Refusal) {
|
|
275
284
|
super('mount_unavailable', reason === null ? `Mount unavailable: ${mount}` : `Mount unavailable: ${mount}: ${reason}`, stage, errno);
|
|
@@ -316,10 +325,10 @@ export interface ReadFileFromDiskOptions {
|
|
|
316
325
|
* Why a pass decided nothing, or null where it decided its paths. A pass gives
|
|
317
326
|
* up deciding, and reports no path, when the selected branch kept changing
|
|
318
327
|
* while it gathered, or when a sync's branch was no longer selected; it then
|
|
319
|
-
* carries no branchId, and
|
|
328
|
+
* carries no branchId, and says why in `undecided`.
|
|
320
329
|
*/
|
|
321
330
|
function undecidedOf(pass: PassResult): string | null {
|
|
322
|
-
return pass.branchId === undefined ? (pass.
|
|
331
|
+
return pass.branchId === undefined ? (pass.undecided ?? 'the pass decided nothing') : null;
|
|
323
332
|
}
|
|
324
333
|
|
|
325
334
|
function errnoOf(err: unknown): string | undefined {
|
|
@@ -385,15 +394,16 @@ async function linkReadError(
|
|
|
385
394
|
rootReal: string,
|
|
386
395
|
): Promise<WorkspaceReadError> {
|
|
387
396
|
if (!('link' in found)) {
|
|
388
|
-
|
|
397
|
+
const reason = linkWords(mount.config.name, found, null);
|
|
398
|
+
return new LinkOnTheWay('not_found', `Unable to read file: ${mountPrefixedPath}: ${reason}`, reason, found.code);
|
|
389
399
|
}
|
|
390
400
|
const leads = await shownLeads(rootReal, rootReal, found, mount.config.include);
|
|
391
|
-
const reason =
|
|
401
|
+
const reason = linkWords(mount.config.name, found, leads, nested, mount.config.include);
|
|
392
402
|
const holder = leads?.kind === 'inside' ? nestedHolder(leads.target, nested) : undefined;
|
|
393
403
|
const code = leads?.kind === 'outside' ? 'escape'
|
|
394
404
|
: leads?.kind === 'inside' && holder && namesHolder(leads, nested) ? 'other_mount'
|
|
395
405
|
: relativePath === found.link ? 'symlink' : 'parent_symlink';
|
|
396
|
-
return new
|
|
406
|
+
return new LinkOnTheWay(code, `${mountPrefixedPath}: ${reason}`, reason);
|
|
397
407
|
}
|
|
398
408
|
|
|
399
409
|
/**
|
|
@@ -2290,13 +2300,13 @@ export class WorkspaceModule implements Module {
|
|
|
2290
2300
|
|
|
2291
2301
|
let text: string;
|
|
2292
2302
|
if (paths.length === 1) {
|
|
2293
|
-
text = `[
|
|
2303
|
+
text = `[${NOTICE_TAG.workspaceEvent} · ${op} · ${paths[0]}]`;
|
|
2294
2304
|
} else {
|
|
2295
2305
|
const list = paths.map(p => `- ${p}`).join('\n');
|
|
2296
|
-
text = `[
|
|
2306
|
+
text = `[${NOTICE_TAG.workspaceEvent} · ${op} · ${paths.length} files in ${mountName}/]\n${list}`;
|
|
2297
2307
|
}
|
|
2298
2308
|
if (conflicts && conflicts.length > 0) {
|
|
2299
|
-
text += `\n[
|
|
2309
|
+
text += `\n[${NOTICE_TAG.conflictsWithDisk}: ${conflicts.join(', ')}; reading each shows how to settle it]`;
|
|
2300
2310
|
}
|
|
2301
2311
|
|
|
2302
2312
|
return {
|
|
@@ -3133,6 +3143,14 @@ export class WorkspaceModule implements Module {
|
|
|
3133
3143
|
return names.length === 0 ? 'this workspace has no mounts' : `the mounts are ${listed(names.map((name) => JSON.stringify(name)))}`;
|
|
3134
3144
|
}
|
|
3135
3145
|
|
|
3146
|
+
private emptyPathField(mount: string | undefined, bare: string): string {
|
|
3147
|
+
const empty = '"path" is "", which names no file or directory.';
|
|
3148
|
+
if (mount === undefined) return `${empty} Leaving "path" out ${bare}.`;
|
|
3149
|
+
if (mount === '') return `${empty} "mount" is "" too; leaving both out ${bare}.`;
|
|
3150
|
+
if (this.mounts.has(mount)) return `${empty} Leaving "path" out covers mount ${JSON.stringify(mount)}.`;
|
|
3151
|
+
return `${empty} "mount" is ${JSON.stringify(mount)}, which is no mount's name; ${this.mountList()}. Leaving both out ${bare}.`;
|
|
3152
|
+
}
|
|
3153
|
+
|
|
3136
3154
|
private noSuchMountField(given: string, bare: string): string {
|
|
3137
3155
|
const [close] = closestNames(given, [...this.mounts.keys()], 1);
|
|
3138
3156
|
return `"mount" is ${JSON.stringify(given)}, which is no mount's name; ${this.mountList()}.`
|
|
@@ -3896,7 +3914,7 @@ export class WorkspaceModule implements Module {
|
|
|
3896
3914
|
return `${shown} is in conflict: the workspace deleted it and disk changed it since. ` +
|
|
3897
3915
|
'Sync this path to take the disk version.';
|
|
3898
3916
|
case 'unverified':
|
|
3899
|
-
return uncheckedFile(shown, report.note);
|
|
3917
|
+
return uncheckedFile(shown, report.note, report.noteCause ?? {});
|
|
3900
3918
|
default:
|
|
3901
3919
|
return missing ?? `there's no file at ${shown}. ls or glob lists the files that exist.`;
|
|
3902
3920
|
}
|
|
@@ -4063,8 +4081,8 @@ export class WorkspaceModule implements Module {
|
|
|
4063
4081
|
const rootReal = await realpath(root).catch(() => root);
|
|
4064
4082
|
const view = this.viewOf(mount, this.agreementOrThrow().rootIdentityForRead(mount.config.name));
|
|
4065
4083
|
const proof = await confirmAbsent(view, rootReal, relativePath);
|
|
4066
|
-
if (
|
|
4067
|
-
return { unreadable: proof === CHANGED ? containedRefusal(err, path) : proof };
|
|
4084
|
+
if (!('reason' in proof)) return 'none';
|
|
4085
|
+
return { unreadable: proof.reason === CHANGED ? containedRefusal(err, path) : proof.reason };
|
|
4068
4086
|
}
|
|
4069
4087
|
|
|
4070
4088
|
private async restoreEach<T extends { relativePath: string; file: string; hash: string; size: number; what: 'saved' | 'conflict-disk' }>(
|
|
@@ -5031,7 +5049,7 @@ export class WorkspaceModule implements Module {
|
|
|
5031
5049
|
// whether the file is gone, so it's taken as the agent's write too.
|
|
5032
5050
|
if (error instanceof EffectFailed && error.touched) {
|
|
5033
5051
|
this.recordDirectWrite(mount, agentName, since);
|
|
5034
|
-
return { success: false, isError: true, error: `The delete of ${path} failed after it reached the file, so the file may be gone: ${directReason(error)}. ls shows whether it is.` };
|
|
5052
|
+
return { success: false, isError: true, error: `The delete of ${JSON.stringify(path)} failed after it reached the file, so the file may be gone: ${directReason(error)}. ls shows whether it is.` };
|
|
5035
5053
|
}
|
|
5036
5054
|
return { success: false, isError: true, error: directDidNothing('delete', path, error) };
|
|
5037
5055
|
}
|
|
@@ -5384,7 +5402,8 @@ export class WorkspaceModule implements Module {
|
|
|
5384
5402
|
.filter((region) => region.kind === 'error' || region.kind === 'cap' || (region.kind === 'ignored' && region.path === dir && dir !== ''))
|
|
5385
5403
|
.map((region) => ({
|
|
5386
5404
|
path: region.path,
|
|
5387
|
-
reason: region.reason === OTHER_MOUNT ? this.otherMountNote(mount.config.name, region.path, 'list', moved)
|
|
5405
|
+
reason: region.reason === OTHER_MOUNT ? this.otherMountNote(mount.config.name, region.path, 'list', moved)
|
|
5406
|
+
: explained(region.reason, region.errno !== undefined ? { errno: region.errno } : {}, 'list'),
|
|
5388
5407
|
}));
|
|
5389
5408
|
return {
|
|
5390
5409
|
view,
|
|
@@ -6003,7 +6022,10 @@ export class WorkspaceModule implements Module {
|
|
|
6003
6022
|
const modesKept: Array<{ mount: string; path: string; reason: string }> = [];
|
|
6004
6023
|
|
|
6005
6024
|
let explicit: { mount: MountState; relativePath: string } | null = null;
|
|
6006
|
-
if (input.path) {
|
|
6025
|
+
if (input.path !== undefined) {
|
|
6026
|
+
if (input.path === '') {
|
|
6027
|
+
return { success: false, error: didNothing('materialize', this.emptyPathField(input.mount, "covers every mount that takes writes and doesn't already write them straight to disk")), isError: true };
|
|
6028
|
+
}
|
|
6007
6029
|
const parsed = await this.parsePath(input.path);
|
|
6008
6030
|
explicit = parsed;
|
|
6009
6031
|
// A path outside the mount's include that's no scope (refusesAsOutside)
|
|
@@ -6017,7 +6039,7 @@ export class WorkspaceModule implements Module {
|
|
|
6017
6039
|
let mountsToMaterialize: Array<{ name: string; mount: MountState }>;
|
|
6018
6040
|
if (explicit) {
|
|
6019
6041
|
mountsToMaterialize = [{ name: explicit.mount.config.name, mount: explicit.mount }];
|
|
6020
|
-
} else if (input.mount) {
|
|
6042
|
+
} else if (input.mount !== undefined) {
|
|
6021
6043
|
const m = this.mounts.get(input.mount);
|
|
6022
6044
|
if (!m) {
|
|
6023
6045
|
return { success: false, error: didNothing('materialize', this.noSuchMountField(input.mount, "covers every mount that takes writes and doesn't already write them straight to disk")), isError: true };
|
|
@@ -6224,7 +6246,8 @@ export class WorkspaceModule implements Module {
|
|
|
6224
6246
|
|
|
6225
6247
|
// A path names its own mount; otherwise the given mount, or every mount.
|
|
6226
6248
|
let targets: Array<{ name: string; mount: MountState; relativePath: string }>;
|
|
6227
|
-
if (input.path) {
|
|
6249
|
+
if (input.path !== undefined) {
|
|
6250
|
+
if (input.path === '') return { success: false, error: didNothing('sync', this.emptyPathField(input.mount, 'covers every mount')), isError: true };
|
|
6228
6251
|
const { mount, relativePath, moved, root } = await this.parsePath(input.path);
|
|
6229
6252
|
// A path outside the mount's include that's no scope (refusesAsOutside)
|
|
6230
6253
|
// is refused; beneath a directory, the walk passes over what the include
|
|
@@ -6233,7 +6256,7 @@ export class WorkspaceModule implements Module {
|
|
|
6233
6256
|
return { success: false, error: didNothing('sync', new OutsideIncludeError(input.path, mount.config).message), isError: true };
|
|
6234
6257
|
}
|
|
6235
6258
|
targets = [{ name: mount.config.name, mount, relativePath }];
|
|
6236
|
-
} else if (input.mount) {
|
|
6259
|
+
} else if (input.mount !== undefined) {
|
|
6237
6260
|
const m = this.mounts.get(input.mount);
|
|
6238
6261
|
if (!m) {
|
|
6239
6262
|
return { success: false, error: didNothing('sync', this.noSuchMountField(input.mount, 'covers every mount')), isError: true };
|
|
@@ -6312,7 +6335,7 @@ export class WorkspaceModule implements Module {
|
|
|
6312
6335
|
// refused" don't look identical from the outside.
|
|
6313
6336
|
for (const r of pass.reports.values()) {
|
|
6314
6337
|
if (r.state === 'unverified') {
|
|
6315
|
-
allSkipped.push({ mount: name, path: r.path, reason: `disk could not be checked: ${r.note ?? 'not observed'}` });
|
|
6338
|
+
allSkipped.push({ mount: name, path: r.path, reason: `disk could not be checked: ${explained(r.note ?? 'not observed', r.noteCause ?? {}, 'sync')}` });
|
|
6316
6339
|
} else if (r.state === 'disk-only') {
|
|
6317
6340
|
allSkipped.push({ mount: name, path: r.path, reason: 'disk-only: binary or over the size limit, never stored' });
|
|
6318
6341
|
}
|
|
@@ -6799,7 +6822,13 @@ function directReason(error: unknown): string {
|
|
|
6799
6822
|
}
|
|
6800
6823
|
|
|
6801
6824
|
function directWayOn(error: unknown): string | null {
|
|
6802
|
-
|
|
6825
|
+
if (error instanceof FileChanged) return 'Calling again reads it afresh.';
|
|
6826
|
+
if (error instanceof WriteRefused) return error.wayOn;
|
|
6827
|
+
if (error instanceof EffectFailed) {
|
|
6828
|
+
if (error.detail.errno !== undefined) return errnoWayOn(error.detail.errno);
|
|
6829
|
+
if (error.detail.again === true) return 'Calling again checks it afresh.';
|
|
6830
|
+
}
|
|
6831
|
+
return null;
|
|
6803
6832
|
}
|
|
6804
6833
|
|
|
6805
6834
|
function directDidNothing(tool: string, path: string, error: unknown): string {
|
|
@@ -6837,16 +6866,11 @@ function deletedOnDisk(path: string): string {
|
|
|
6837
6866
|
return `there's no file at ${JSON.stringify(path)} any more: disk's copy was deleted since the workspace last agreed with it, and the workspace took that in first. ls or glob lists the files that exist.`;
|
|
6838
6867
|
}
|
|
6839
6868
|
|
|
6840
|
-
function uncheckedFile(shown: string, note: string | undefined): string {
|
|
6869
|
+
function uncheckedFile(shown: string, note: string | undefined, cause: Cause): string {
|
|
6841
6870
|
if (note === DIRECTORY) return `${shown} is a directory, not a file; ls ${shown} lists what's in it.`;
|
|
6842
|
-
const on = note ===
|
|
6843
|
-
: note ===
|
|
6844
|
-
|
|
6845
|
-
: note === LEAVES_MOUNT ? "Calling again won't change that; if a mount holds the file it leads to, name it by its path there."
|
|
6846
|
-
: note === NOT_REGULAR ? onlyTheHost('put a regular file there')
|
|
6847
|
-
: note === CHANGED ? 'Calling again checks it afresh.'
|
|
6848
|
-
: 'Calling again checks disk again; if the same reason comes back, whoever runs this host can fix it on disk.';
|
|
6849
|
-
return `${shown} isn't in the workspace, and disk couldn't be checked: ${sentence(note ?? 'not observed')}${on !== null ? ` ${on}` : ''}`;
|
|
6871
|
+
const on = note === ROOT_REPLACED || cause.ownStep === true ? null
|
|
6872
|
+
: (note === undefined ? null : wayOn(note, cause, 'read')) ?? 'calling again checks disk again; if the same reason comes back, whoever runs this host can fix it on disk';
|
|
6873
|
+
return `${shown} isn't in the workspace, and disk couldn't be checked: ${sentence(note ?? 'not observed')}${on !== null ? ` ${on[0]!.toUpperCase()}${sentence(on.slice(1))}` : ''}`;
|
|
6850
6874
|
}
|
|
6851
6875
|
|
|
6852
6876
|
function namesRoot(path: string, mount: string): string {
|
|
@@ -6887,16 +6911,16 @@ function readRefusal(err: WorkspaceReadError, path: string): string {
|
|
|
6887
6911
|
}
|
|
6888
6912
|
return `${shown} can't be read: ${sentence(err.reason)}`;
|
|
6889
6913
|
}
|
|
6890
|
-
if (err
|
|
6914
|
+
if (err instanceof LinkOnTheWay) return `${shown} can't be reached: ${sentence(err.reason)}`;
|
|
6891
6915
|
switch (err.code) {
|
|
6892
6916
|
case 'directory':
|
|
6893
6917
|
return `${shown} is a directory, not a file; ls ${shown} lists what's in it.`;
|
|
6894
6918
|
case 'symlink':
|
|
6895
6919
|
return `${shown} is a symlink, and its mount doesn't follow symlinks, so the workspace's tools don't go through it. `
|
|
6896
|
-
+
|
|
6920
|
+
+ `Calling again won't change that; ${UNFOLLOWED_STEP}.`;
|
|
6897
6921
|
case 'escape':
|
|
6898
6922
|
return `${shown} leads through a symlink to a place outside its mount, so the workspace's tools don't reach it. `
|
|
6899
|
-
+
|
|
6923
|
+
+ `Calling again won't change that; ${MOUNT_IT}.`;
|
|
6900
6924
|
case 'changed':
|
|
6901
6925
|
return `${shown} changed on disk while it was being read. Calling again reads it afresh.`;
|
|
6902
6926
|
case 'not_found':
|
|
@@ -6970,7 +6994,7 @@ function remedyOfLost(lost: LostContents): string {
|
|
|
6970
6994
|
*/
|
|
6971
6995
|
function headMoveText(mounts: readonly string[]): string {
|
|
6972
6996
|
const one = mounts.length === 1;
|
|
6973
|
-
return `[workspace] Writes to ${listed(mounts.map((name) => `\`${name}\``))} from turns the history no longer holds are still on disk. `
|
|
6997
|
+
return `[${NOTICE_TAG.workspace}] Writes to ${listed(mounts.map((name) => `\`${name}\``))} from turns the history no longer holds are still on disk. `
|
|
6974
6998
|
+ `If ${one ? "it's a git checkout" : "they're git checkouts"}, \`git status\` and \`git diff\` show them; `
|
|
6975
6999
|
+ 'a `git checkout` of a file drops every unstaged change in it, including the ones the history still holds.';
|
|
6976
7000
|
}
|