@animalabs/agent-framework 0.27.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 +145 -41
- package/dist/src/agent.d.ts +29 -2
- package/dist/src/agent.d.ts.map +1 -1
- package/dist/src/agent.js +73 -12
- 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 +11 -5
- package/dist/src/api/server.js.map +1 -1
- package/dist/src/auto-continue.d.ts +5 -2
- package/dist/src/auto-continue.d.ts.map +1 -1
- package/dist/src/auto-continue.js +9 -5
- 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/cut-off-round.d.ts +24 -0
- package/dist/src/cut-off-round.d.ts.map +1 -0
- package/dist/src/cut-off-round.js +87 -0
- package/dist/src/cut-off-round.js.map +1 -0
- package/dist/src/framework.d.ts +468 -125
- package/dist/src/framework.d.ts.map +1 -1
- package/dist/src/framework.js +3651 -1111
- package/dist/src/framework.js.map +1 -1
- package/dist/src/gate/event-gate.d.ts +133 -13
- package/dist/src/gate/event-gate.d.ts.map +1 -1
- package/dist/src/gate/event-gate.js +654 -59
- package/dist/src/gate/event-gate.js.map +1 -1
- package/dist/src/gate/index.d.ts +3 -2
- package/dist/src/gate/index.d.ts.map +1 -1
- package/dist/src/gate/index.js +2 -1
- package/dist/src/gate/index.js.map +1 -1
- package/dist/src/gate/reply-judge.d.ts +116 -0
- package/dist/src/gate/reply-judge.d.ts.map +1 -0
- package/dist/src/gate/reply-judge.js +467 -0
- package/dist/src/gate/reply-judge.js.map +1 -0
- package/dist/src/gate/types.d.ts +173 -2
- package/dist/src/gate/types.d.ts.map +1 -1
- package/dist/src/heartbeat/heartbeat.d.ts +202 -0
- package/dist/src/heartbeat/heartbeat.d.ts.map +1 -0
- package/dist/src/heartbeat/heartbeat.js +778 -0
- package/dist/src/heartbeat/heartbeat.js.map +1 -0
- package/dist/src/heartbeat/reminder-time.d.ts +5 -0
- package/dist/src/heartbeat/reminder-time.d.ts.map +1 -0
- package/dist/src/heartbeat/reminder-time.js +107 -0
- package/dist/src/heartbeat/reminder-time.js.map +1 -0
- 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 +5 -1
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +3 -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/capability-mask.d.ts.map +1 -1
- package/dist/src/mcpl/capability-mask.js +3 -2
- package/dist/src/mcpl/capability-mask.js.map +1 -1
- 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 +133 -54
- 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 +29 -11
- package/dist/src/mcpl/inbound-source.d.ts.map +1 -1
- package/dist/src/mcpl/inbound-source.js +43 -70
- 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 +18 -8
- package/dist/src/mcpl/server-connection.d.ts.map +1 -1
- package/dist/src/mcpl/server-connection.js +70 -30
- 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/tags.d.ts +9 -2
- package/dist/src/mcpl/tags.d.ts.map +1 -1
- package/dist/src/mcpl/tags.js +12 -2
- package/dist/src/mcpl/tags.js.map +1 -1
- package/dist/src/mcpl/tool-presence.d.ts +66 -14
- package/dist/src/mcpl/tool-presence.d.ts.map +1 -1
- package/dist/src/mcpl/tool-presence.js +150 -80
- package/dist/src/mcpl/tool-presence.js.map +1 -1
- package/dist/src/mcpl/tool-signature.d.ts +10 -3
- package/dist/src/mcpl/tool-signature.d.ts.map +1 -1
- package/dist/src/mcpl/tool-signature.js +60 -30
- package/dist/src/mcpl/tool-signature.js.map +1 -1
- package/dist/src/mcpl/toolset-host.d.ts +88 -55
- package/dist/src/mcpl/toolset-host.d.ts.map +1 -1
- package/dist/src/mcpl/toolset-host.js +220 -111
- package/dist/src/mcpl/toolset-host.js.map +1 -1
- package/dist/src/mcpl/toolsets-module.d.ts +27 -35
- package/dist/src/mcpl/toolsets-module.d.ts.map +1 -1
- package/dist/src/mcpl/toolsets-module.js +313 -91
- 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 +31 -13
- 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 +10 -8
- package/dist/src/module-registry.d.ts.map +1 -1
- package/dist/src/module-registry.js +15 -9
- 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.d.ts.map +1 -1
- package/dist/src/modules/history/index.js +30 -17
- 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 +26 -0
- package/dist/src/modules/workspace/answers.d.ts.map +1 -0
- package/dist/src/modules/workspace/answers.js +138 -0
- package/dist/src/modules/workspace/answers.js.map +1 -0
- package/dist/src/modules/workspace/branch-intent.d.ts +5 -3
- package/dist/src/modules/workspace/branch-intent.d.ts.map +1 -1
- package/dist/src/modules/workspace/branch-intent.js +6 -2
- package/dist/src/modules/workspace/branch-intent.js.map +1 -1
- package/dist/src/modules/workspace/direct-write.d.ts +3 -0
- package/dist/src/modules/workspace/direct-write.d.ts.map +1 -1
- package/dist/src/modules/workspace/direct-write.js +59 -24
- package/dist/src/modules/workspace/direct-write.js.map +1 -1
- package/dist/src/modules/workspace/disk-agreement.d.ts +33 -11
- package/dist/src/modules/workspace/disk-agreement.d.ts.map +1 -1
- package/dist/src/modules/workspace/disk-agreement.js +48 -17
- package/dist/src/modules/workspace/disk-agreement.js.map +1 -1
- package/dist/src/modules/workspace/edit-text.d.ts +2 -1
- package/dist/src/modules/workspace/edit-text.d.ts.map +1 -1
- package/dist/src/modules/workspace/edit-text.js +4 -2
- package/dist/src/modules/workspace/edit-text.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 +67 -50
- package/dist/src/modules/workspace/effects.js.map +1 -1
- package/dist/src/modules/workspace/index.d.ts +63 -51
- package/dist/src/modules/workspace/index.d.ts.map +1 -1
- package/dist/src/modules/workspace/index.js +980 -547
- package/dist/src/modules/workspace/index.js.map +1 -1
- package/dist/src/modules/workspace/observe.d.ts +125 -82
- package/dist/src/modules/workspace/observe.d.ts.map +1 -1
- package/dist/src/modules/workspace/observe.js +465 -241
- package/dist/src/modules/workspace/observe.js.map +1 -1
- package/dist/src/modules/workspace/read-page.d.ts.map +1 -1
- package/dist/src/modules/workspace/read-page.js +7 -1
- package/dist/src/modules/workspace/read-page.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 +156 -50
- package/dist/src/modules/workspace/reconcile.js.map +1 -1
- package/dist/src/modules/workspace/result-text.d.ts +1 -0
- package/dist/src/modules/workspace/result-text.d.ts.map +1 -1
- package/dist/src/modules/workspace/result-text.js.map +1 -1
- package/dist/src/modules/workspace/sync.d.ts +9 -0
- package/dist/src/modules/workspace/sync.d.ts.map +1 -1
- package/dist/src/modules/workspace/sync.js +14 -0
- package/dist/src/modules/workspace/sync.js.map +1 -1
- package/dist/src/modules/workspace/types.d.ts +96 -43
- package/dist/src/modules/workspace/types.d.ts.map +1 -1
- package/dist/src/modules/workspace/types.js.map +1 -1
- package/dist/src/modules/workspace/watcher.d.ts.map +1 -1
- package/dist/src/modules/workspace/watcher.js +8 -3
- package/dist/src/modules/workspace/watcher.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/prose-drafts.d.ts +4 -3
- package/dist/src/prose-drafts.d.ts.map +1 -1
- package/dist/src/prose-drafts.js +3 -2
- package/dist/src/prose-drafts.js.map +1 -1
- package/dist/src/reasoning-loss-notice.d.ts +23 -0
- package/dist/src/reasoning-loss-notice.d.ts.map +1 -0
- package/dist/src/reasoning-loss-notice.js +52 -0
- package/dist/src/reasoning-loss-notice.js.map +1 -0
- package/dist/src/reasoning-loss.d.ts +24 -0
- package/dist/src/reasoning-loss.d.ts.map +1 -0
- package/dist/src/reasoning-loss.js +102 -0
- package/dist/src/reasoning-loss.js.map +1 -0
- package/dist/src/send-tools.d.ts +3 -2
- package/dist/src/send-tools.d.ts.map +1 -1
- package/dist/src/send-tools.js +3 -2
- package/dist/src/send-tools.js.map +1 -1
- package/dist/src/sent-rows.js +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/thinking-drops.d.ts +4 -11
- package/dist/src/thinking-drops.d.ts.map +1 -1
- package/dist/src/thinking-drops.js +7 -14
- package/dist/src/thinking-drops.js.map +1 -1
- package/dist/src/tool-args.d.ts +12 -0
- package/dist/src/tool-args.d.ts.map +1 -1
- package/dist/src/tool-args.js +39 -4
- 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/tool-result-guard.d.ts +8 -4
- package/dist/src/tool-result-guard.d.ts.map +1 -1
- package/dist/src/tool-result-guard.js +23 -11
- package/dist/src/tool-result-guard.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/tune-out/tools.d.ts +4 -1
- package/dist/src/tune-out/tools.d.ts.map +1 -1
- package/dist/src/types/agent.d.ts +63 -18
- package/dist/src/types/agent.d.ts.map +1 -1
- package/dist/src/types/events.d.ts +18 -8
- package/dist/src/types/events.d.ts.map +1 -1
- package/dist/src/types/framework.d.ts +58 -0
- package/dist/src/types/framework.d.ts.map +1 -1
- package/dist/src/types/module.d.ts +27 -13
- package/dist/src/types/module.d.ts.map +1 -1
- package/dist/src/types/trace.d.ts +34 -1
- package/dist/src/types/trace.d.ts.map +1 -1
- package/package.json +3 -3
- package/src/agent.ts +76 -12
- package/src/api/mcp-server-core.ts +3 -14
- package/src/api/server.ts +11 -5
- package/src/auto-continue.ts +12 -5
- package/src/code-execution/isolate/runner.ts +7 -5
- package/src/code-execution/isolate/scripts.ts +3 -2
- package/src/cut-off-round.ts +98 -0
- package/src/framework.ts +3772 -1138
- package/src/gate/event-gate.ts +705 -60
- package/src/gate/index.ts +9 -1
- package/src/gate/reply-judge.ts +512 -0
- package/src/gate/types.ts +176 -3
- package/src/heartbeat/heartbeat.ts +843 -0
- package/src/heartbeat/reminder-time.ts +107 -0
- package/src/held-writes.ts +3 -3
- package/src/host-presentation.ts +605 -0
- package/src/index.ts +5 -1
- package/src/line-reader.ts +58 -0
- package/src/mcpl/capability-mask.ts +3 -2
- package/src/mcpl/channel-registry.ts +139 -53
- 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 +45 -70
- 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 +76 -34
- package/src/mcpl/skip-reply-wake.ts +35 -0
- package/src/mcpl/tags.ts +13 -2
- package/src/mcpl/tool-presence.ts +149 -82
- package/src/mcpl/tool-signature.ts +64 -32
- package/src/mcpl/toolset-host.ts +212 -110
- package/src/mcpl/toolsets-module.ts +277 -91
- package/src/mcpl/transport.ts +18 -14
- package/src/mcpl/types.ts +32 -13
- package/src/module-registry.ts +14 -9
- package/src/modules/api/index.ts +4 -2
- package/src/modules/discord/index.ts +37 -25
- package/src/modules/history/index.ts +30 -18
- package/src/modules/sender-name.ts +9 -0
- package/src/modules/workspace/answers.ts +151 -0
- package/src/modules/workspace/branch-intent.ts +10 -5
- package/src/modules/workspace/direct-write.ts +59 -23
- package/src/modules/workspace/disk-agreement.ts +52 -21
- package/src/modules/workspace/edit-text.ts +7 -5
- package/src/modules/workspace/effects.ts +69 -50
- package/src/modules/workspace/index.ts +966 -556
- package/src/modules/workspace/observe.ts +497 -242
- package/src/modules/workspace/read-page.ts +7 -1
- package/src/modules/workspace/reconcile.ts +184 -49
- package/src/modules/workspace/result-text.ts +2 -0
- package/src/modules/workspace/sync.ts +16 -0
- package/src/modules/workspace/types.ts +96 -43
- package/src/modules/workspace/watcher.ts +8 -3
- package/src/notice-tags.ts +84 -0
- package/src/prose-drafts.ts +4 -3
- package/src/reasoning-loss-notice.ts +64 -0
- package/src/reasoning-loss.ts +120 -0
- package/src/send-tools.ts +3 -2
- package/src/sent-rows.ts +1 -1
- package/src/silent-heartbeat.ts +4 -3
- package/src/supplied-text.ts +154 -0
- package/src/thinking-drops.ts +10 -16
- package/src/tool-args.ts +38 -4
- package/src/tool-image-ledger.ts +2 -1
- package/src/tool-presentation.ts +5 -4
- package/src/tool-result-guard.ts +26 -11
- package/src/tune-out/coordinator.ts +25 -27
- package/src/tune-out/tools.ts +4 -1
- package/src/types/agent.ts +63 -18
- package/src/types/events.ts +18 -8
- package/src/types/framework.ts +59 -0
- package/src/types/module.ts +28 -13
- package/src/types/trace.ts +36 -1
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import { constants } from 'node:os';
|
|
2
|
+
import { boundedErrorMessage } from '../../error-message.js';
|
|
3
|
+
|
|
4
|
+
export class Refusal extends Error {
|
|
5
|
+
constructor(message: string) {
|
|
6
|
+
super(message);
|
|
7
|
+
this.name = 'Refusal';
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function didNothing(tool: string, body: string, to?: string): string {
|
|
12
|
+
return `The ${tool} did nothing${to !== undefined ? ` to ${JSON.stringify(to)}` : ''}: ${body}`;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function onlyTheHost(can: string): string {
|
|
16
|
+
return `Calling again won't change that; whoever runs this host can ${can}.`;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// Node's own message for a failed system call names the host's absolute
|
|
20
|
+
// path, so an agent gets the code's plain words instead.
|
|
21
|
+
export function errnoWords(code: string | undefined): string {
|
|
22
|
+
switch (code) {
|
|
23
|
+
case 'ENOENT': return 'nothing is there';
|
|
24
|
+
case 'ENOTDIR': return 'a part of the path isn\'t a directory';
|
|
25
|
+
case 'EACCES': return 'permission denied';
|
|
26
|
+
case 'EPERM': return 'not permitted';
|
|
27
|
+
case 'EISDIR': return 'it\'s a directory';
|
|
28
|
+
case 'EEXIST': return 'something is already there';
|
|
29
|
+
case 'ENOTEMPTY': return 'the directory isn\'t empty';
|
|
30
|
+
case 'EXDEV': return 'the move would cross from one filesystem to another';
|
|
31
|
+
case 'ELOOP': return 'too many symlinks, or a loop of them';
|
|
32
|
+
case 'ENAMETOOLONG': return 'a name too long for the filesystem';
|
|
33
|
+
case 'ENOSPC': return 'no space left on the disk';
|
|
34
|
+
case 'EDQUOT': return 'the disk quota is used up';
|
|
35
|
+
case 'EFBIG': return 'the file would be too large for the filesystem';
|
|
36
|
+
case 'EROFS': return 'the filesystem is read-only';
|
|
37
|
+
case 'EIO': return 'an input/output error on the disk';
|
|
38
|
+
case 'EBUSY': case 'ETXTBSY': return 'the file is busy';
|
|
39
|
+
case 'EMFILE': case 'ENFILE': return 'too many files are open on this host';
|
|
40
|
+
case 'ENOMEM': return 'this host is out of memory';
|
|
41
|
+
case 'EAGAIN': case 'EWOULDBLOCK': return 'the system was busy';
|
|
42
|
+
case 'EINTR': return 'the system call was interrupted';
|
|
43
|
+
case 'ETIMEDOUT': return 'the system call timed out';
|
|
44
|
+
case 'ESTALE': return 'a stale network file handle';
|
|
45
|
+
case 'EINVAL': return 'the system refused an argument as invalid';
|
|
46
|
+
case undefined: return 'an error the system gave no code for';
|
|
47
|
+
default: return `a system error (${code})`;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function hostFix(code: string | undefined): string | undefined {
|
|
52
|
+
switch (code) {
|
|
53
|
+
case 'ENOSPC': return 'free space on its disk';
|
|
54
|
+
case 'EFBIG': return 'raise the limit on a file\'s size, or keep the files on a filesystem that holds larger ones';
|
|
55
|
+
case 'EDQUOT': return 'raise or free the disk quota';
|
|
56
|
+
case 'EROFS': return 'make the filesystem writable';
|
|
57
|
+
case 'EIO': return 'check the disk';
|
|
58
|
+
case 'EACCES': case 'EPERM': return 'change the permissions';
|
|
59
|
+
case 'EMFILE': case 'ENFILE': return 'raise the limit on open files';
|
|
60
|
+
case 'ELOOP': return 'fix the symlinks on that path';
|
|
61
|
+
case 'ENOMEM': return 'free memory';
|
|
62
|
+
default: return undefined;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const PASSING = new Set(['EIO', 'EMFILE', 'ENFILE', 'ENOMEM']);
|
|
67
|
+
const AS_DISK_IS = new Set(['EISDIR', 'ENOTDIR', 'EEXIST', 'ENOTEMPTY', 'EXDEV']);
|
|
68
|
+
|
|
69
|
+
export function systemWords(code: string | undefined, error: unknown): string {
|
|
70
|
+
return code !== undefined ? errnoWords(code) : boundedErrorMessage(error);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function errnoWayOn(code: string | undefined, otherwise = 'Calling again may work.', hostsName = false): string {
|
|
74
|
+
if (code === 'ENAMETOOLONG' && hostsName) return onlyTheHost("give the mount's directory a shorter path");
|
|
75
|
+
const fix = hostFix(code);
|
|
76
|
+
if (fix !== undefined) {
|
|
77
|
+
return code !== undefined && PASSING.has(code) ? `Calling again may work; if it fails the same way, whoever runs this host can ${fix}.` : onlyTheHost(fix);
|
|
78
|
+
}
|
|
79
|
+
if (code === 'ENAMETOOLONG') return "Calling again won't change that; a shorter name would.";
|
|
80
|
+
if (code !== undefined && AS_DISK_IS.has(code)) return "Calling again won't change that while disk holds what it does there.";
|
|
81
|
+
return otherwise;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function errnoClause(code: string | undefined, again: string, then: string): string {
|
|
85
|
+
const lasting = `${again} won't change that; `;
|
|
86
|
+
if (code === 'ENAMETOOLONG') return `${lasting}a shorter name would get past it`;
|
|
87
|
+
const fix = hostFix(code);
|
|
88
|
+
if (fix !== undefined) {
|
|
89
|
+
return code !== undefined && PASSING.has(code)
|
|
90
|
+
? `${again} may get past it; if it fails the same way, whoever runs this host can ${fix}`
|
|
91
|
+
: `${lasting}whoever runs this host can ${fix}, and ${then}`;
|
|
92
|
+
}
|
|
93
|
+
return `${again} checks it afresh; if the same reason comes back, whoever runs this host can fix it on disk`;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const OS_ERRNO = new Map<number, string>();
|
|
97
|
+
for (const [name, number] of Object.entries(constants.errno)) if (!OS_ERRNO.has(number)) OS_ERRNO.set(number, name);
|
|
98
|
+
|
|
99
|
+
// Chronicle's store errors carry the code GenericFailure, and an I/O
|
|
100
|
+
// failure's message is "IO error: " and Rust's std::io::Error, which ends
|
|
101
|
+
// with the OS error's number: an errno on POSIX, a Win32 code on Windows.
|
|
102
|
+
export function errnoCode(error: unknown): string | undefined {
|
|
103
|
+
const code = typeof error === 'object' && error !== null ? (error as { code?: unknown }).code : undefined;
|
|
104
|
+
if (typeof code === 'string' && /^E[A-Z0-9]+$/.test(code)) return code;
|
|
105
|
+
const os = error instanceof Error && process.platform !== 'win32' ? /^IO error: .*\(os error (\d+)\)$/s.exec(error.message) : null;
|
|
106
|
+
return os !== null ? OS_ERRNO.get(Number(os[1])) : undefined;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const CHANGING = new Set(['write', 'edit', 'edit_many', 'delete', 'materialize', 'sync']);
|
|
110
|
+
|
|
111
|
+
export function changesFiles(tool: string): boolean {
|
|
112
|
+
return CHANGING.has(tool);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export const UNFOLLOWED_STEP = 'whoever runs this host can set followSymlinks: true on this mount if it leads inside the mount, mount what it leads to, or a directory that holds it, if it leads outside, or remove it if it leads nowhere';
|
|
116
|
+
|
|
117
|
+
export const MOUNT_IT = 'whoever runs this host can mount it, or a directory that holds it';
|
|
118
|
+
|
|
119
|
+
export const OUTSIDE_STEP = `nothing done through this mount reaches what it leads to, and ${MOUNT_IT}`;
|
|
120
|
+
|
|
121
|
+
export const RESOLVED_OUTSIDE_STEP = 'nothing done through this mount reaches where it resolves, and whoever runs this host can mount that place';
|
|
122
|
+
|
|
123
|
+
export const ROOT_RECHECK = "Calling again may work; if it fails the same way, whoever runs this host can check the mount's directory and its path.";
|
|
124
|
+
|
|
125
|
+
export const AWAITS_REOPEN = "The workspace can't check its files against disk until its store is reopened, so calling again won't change that; whoever runs this host can reopen it with a restart.";
|
|
126
|
+
|
|
127
|
+
export interface Unexpected {
|
|
128
|
+
stored?: 'unknown' | 'not' | 'held';
|
|
129
|
+
said?: string;
|
|
130
|
+
reopen?: boolean;
|
|
131
|
+
look?: string;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export function unexpectedAnswer(tool: string, subject: string | undefined, error: unknown, how: Unexpected = {}): string {
|
|
135
|
+
const stored = how.stored ?? 'unknown';
|
|
136
|
+
const code = errnoCode(error);
|
|
137
|
+
const words = how.said ?? systemWords(code, error);
|
|
138
|
+
const before = stored !== 'not' ? '' : tool === 'edit_many' ? ', before any of its edits was stored' : `, before the ${tool === 'delete' ? 'deletion' : tool} was stored`;
|
|
139
|
+
const head = `The ${tool}${subject !== undefined ? ` of ${subject}` : ''} stopped on an error the workspace didn't expect${before}: ${words.replace(/\.$/, '')}.`;
|
|
140
|
+
if (stored === 'held') {
|
|
141
|
+
return `${head} ${subject !== undefined ? subject : 'Its files'} already held what the call gave, so ${subject !== undefined ? 'it holds' : 'they hold'} that in the workspace either way.`;
|
|
142
|
+
}
|
|
143
|
+
const changing = stored === 'unknown' && CHANGING.has(tool);
|
|
144
|
+
if (how.reopen === true) return [head, AWAITS_REOPEN, ...(changing ? ["Whether it changed anything isn't known until then."] : [])].join(' ');
|
|
145
|
+
const look = !changing ? null
|
|
146
|
+
: how.look ?? (subject === undefined ? 'read its files to see what they hold now' : `read ${subject} to see what the workspace holds there now`);
|
|
147
|
+
const on = code !== undefined
|
|
148
|
+
? errnoWayOn(code, "Calling again may work; if it fails the same way, whoever runs this host can find the whole error in this host's log.")
|
|
149
|
+
: "Calling again may work; if it fails the same way, whoever runs this host can find the whole error in this host's log.";
|
|
150
|
+
return [head, ...(look !== null ? [`Whether it changed anything isn't known: ${look}.`] : []), on].join(' ');
|
|
151
|
+
}
|
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
*/
|
|
20
20
|
|
|
21
21
|
import type { JsStore } from '@animalabs/chronicle';
|
|
22
|
+
import { storedBytes } from './sync.js';
|
|
22
23
|
|
|
23
24
|
export type ConflictKind =
|
|
24
25
|
/** Disk, workspace and the last agreement all differ. */
|
|
@@ -56,9 +57,11 @@ export interface BranchIntent {
|
|
|
56
57
|
origin?: 'store';
|
|
57
58
|
conflict?: ConflictRecord;
|
|
58
59
|
/**
|
|
59
|
-
* Why a recorded intent can't be read (its blob missing from the store,
|
|
60
|
-
* not an intent): a tombstone or conflict
|
|
61
|
-
*
|
|
60
|
+
* Why a recorded intent can't be read (its blob missing from the store,
|
|
61
|
+
* a blob chronicle can't read, or not an intent): a tombstone or conflict
|
|
62
|
+
* it held is unknown, never absent.
|
|
63
|
+
* Never written; a put replaces the damaged record, unless it puts the same
|
|
64
|
+
* contents over a blob file chronicle can't read, which chronicle keeps.
|
|
62
65
|
*/
|
|
63
66
|
unreadable?: string;
|
|
64
67
|
}
|
|
@@ -70,8 +73,10 @@ function isEmpty(intent: BranchIntent): boolean {
|
|
|
70
73
|
|
|
71
74
|
/** A recorded intent's blob, read; a damaged one says why it can't be. */
|
|
72
75
|
function readIntent(store: JsStore, blobHash: string): BranchIntent {
|
|
73
|
-
const
|
|
74
|
-
if (
|
|
76
|
+
const stored = storedBytes(store, blobHash);
|
|
77
|
+
if ('missing' in stored) return { unreadable: "its record's blob is missing from the store" };
|
|
78
|
+
if ('unreadable' in stored) return { unreadable: `its record's blob can't be read from the store (${stored.unreadable})` };
|
|
79
|
+
const blob = stored.bytes;
|
|
75
80
|
try {
|
|
76
81
|
const parsed = JSON.parse(blob.toString('utf-8')) as unknown;
|
|
77
82
|
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) return parsed as BranchIntent;
|
|
@@ -56,19 +56,27 @@ import type { BigIntStats } from 'node:fs';
|
|
|
56
56
|
import { lstat, open, realpath } from 'node:fs/promises';
|
|
57
57
|
import { relative, sep } from 'node:path';
|
|
58
58
|
import { refusedLanding } from './effects.js';
|
|
59
|
+
import { OUTSIDE_STEP, RESOLVED_OUTSIDE_STEP, ROOT_RECHECK, UNFOLLOWED_STEP, errnoWayOn, errnoWords, onlyTheHost, systemWords } from './answers.js';
|
|
59
60
|
import {
|
|
60
61
|
type CheckedDir,
|
|
61
62
|
type MountView,
|
|
62
63
|
NONBLOCK,
|
|
64
|
+
LEAVES_MOUNT,
|
|
65
|
+
NOT_A_DIRECTORY,
|
|
63
66
|
NOT_FOLLOWED,
|
|
64
67
|
OUTSIDE_PARENT,
|
|
68
|
+
ROOT_UNAVAILABLE,
|
|
65
69
|
checkDir,
|
|
66
70
|
checkRoot,
|
|
67
71
|
errno,
|
|
72
|
+
rootAbsence,
|
|
68
73
|
nameOf,
|
|
69
74
|
parentOf,
|
|
70
75
|
} from './observe.js';
|
|
71
76
|
|
|
77
|
+
const DIRECTORY_THERE = 'A file can\'t be written over a directory; give another path.';
|
|
78
|
+
const NOT_REGULAR_THERE = onlyTheHost('put a regular file there');
|
|
79
|
+
|
|
72
80
|
/** An open that follows no link at the path, where the platform has one. */
|
|
73
81
|
const NOFOLLOW = typeof fsConstants.O_NOFOLLOW === 'number' ? fsConstants.O_NOFOLLOW : 0;
|
|
74
82
|
|
|
@@ -119,7 +127,29 @@ export const sameIdentity = (a: FileIdentity, b: FileIdentity): boolean =>
|
|
|
119
127
|
export class FileChanged extends Error {}
|
|
120
128
|
|
|
121
129
|
/** Why a direct write didn't happen, in words for its answer; disk is as it was. */
|
|
122
|
-
export class WriteRefused extends Error {
|
|
130
|
+
export class WriteRefused extends Error {
|
|
131
|
+
constructor(message: string, readonly wayOn: string | null = null, readonly errno?: string) {
|
|
132
|
+
super(message);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const AFRESH = 'Calling again checks it afresh.';
|
|
137
|
+
|
|
138
|
+
const NAME_ITS_OWN = `Calling again won't change that; ${UNFOLLOWED_STEP}.`;
|
|
139
|
+
|
|
140
|
+
function rootWayOn(code: string | undefined): string {
|
|
141
|
+
return code === 'ENOENT' || code === 'ENOTDIR' ? onlyTheHost("put the mount's directory back at its path") : errnoWayOn(code, ROOT_RECHECK, true);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function refusedWayOn(reason: string, code?: string, hostsName = false): string | null {
|
|
145
|
+
if (reason === ROOT_UNAVAILABLE) return rootWayOn(code);
|
|
146
|
+
if (code !== undefined) return errnoWayOn(code, undefined, hostsName);
|
|
147
|
+
if (reason === NOT_FOLLOWED) return NAME_ITS_OWN;
|
|
148
|
+
if (reason === LEAVES_MOUNT) return `Calling again won't change that; ${OUTSIDE_STEP}.`;
|
|
149
|
+
if (reason === OUTSIDE_PARENT) return `Calling again won't change that; ${RESOLVED_OUTSIDE_STEP}.`;
|
|
150
|
+
if (reason === NOT_A_DIRECTORY) return "Calling again won't change that while something other than a directory stands where the path needs one; another path, or removing what stands there, would.";
|
|
151
|
+
return null;
|
|
152
|
+
}
|
|
123
153
|
|
|
124
154
|
/** A file's new bytes staged beside it, ready to be renamed over it (stageContained). */
|
|
125
155
|
export interface Staged {
|
|
@@ -170,7 +200,8 @@ async function directoryOf(view: MountView, rootReal: string, rel: string, makeD
|
|
|
170
200
|
const checked = await checkDir(view, rootReal, parentOf(rel), { create: makeDirectory });
|
|
171
201
|
if (checked.kind === 'checked') return checked.dir;
|
|
172
202
|
if (checked.kind === 'absent') throw new FileChanged(rel);
|
|
173
|
-
|
|
203
|
+
const reason = checked.outside ? OUTSIDE_PARENT : checked.reason;
|
|
204
|
+
throw new WriteRefused(reason, refusedWayOn(reason, checked.errno));
|
|
174
205
|
}
|
|
175
206
|
|
|
176
207
|
/**
|
|
@@ -193,29 +224,34 @@ export async function stageContained(
|
|
|
193
224
|
{ makeDirectory = false }: { makeDirectory?: boolean } = {},
|
|
194
225
|
): Promise<Staged> {
|
|
195
226
|
const root = await checkRoot(view, rootReal);
|
|
196
|
-
if ('reason' in root)
|
|
227
|
+
if ('reason' in root) {
|
|
228
|
+
const code = root.errno ?? await rootAbsence(rootReal);
|
|
229
|
+
throw new WriteRefused(root.reason, rootWayOn(code), code);
|
|
230
|
+
}
|
|
197
231
|
let parent = await directoryOf(view, rootReal, rel, makeDirectory);
|
|
198
232
|
let path = parent.at(nameOf(rel));
|
|
199
233
|
let info = await lstat(path, { bigint: true }).catch((err: unknown) => {
|
|
200
234
|
if (errno(err) === 'ENOENT') return null;
|
|
201
|
-
throw new WriteRefused(`cannot stat the file (${errno(err)
|
|
235
|
+
throw new WriteRefused(`cannot stat the file (${systemWords(errno(err), err)})`, errnoWayOn(errno(err)), errno(err));
|
|
202
236
|
});
|
|
203
237
|
if (info?.isSymbolicLink()) {
|
|
204
|
-
if (!view.followSymlinks) throw new WriteRefused(NOT_FOLLOWED);
|
|
238
|
+
if (!view.followSymlinks) throw new WriteRefused(NOT_FOLLOWED, NAME_ITS_OWN);
|
|
205
239
|
// A followed link stands for the file it leads to, as a read takes it:
|
|
206
240
|
// the write goes there, judged where it lands, and the link stays.
|
|
207
241
|
const real = await realpath(path).catch((err: unknown) => {
|
|
208
|
-
throw
|
|
242
|
+
throw errno(err) === 'ENOENT'
|
|
243
|
+
? new WriteRefused("a symlink whose target doesn't exist", onlyTheHost('remove the link or make it lead to a file'))
|
|
244
|
+
: new WriteRefused(`cannot resolve the symlink (${systemWords(errno(err), err)})`, errnoWayOn(errno(err)), errno(err));
|
|
209
245
|
});
|
|
210
246
|
const leaves = await refusedLanding(view, rootReal, rel, real);
|
|
211
|
-
if (leaves !== null) throw new WriteRefused(leaves);
|
|
247
|
+
if (leaves !== null) throw new WriteRefused(leaves, refusedWayOn(leaves));
|
|
212
248
|
const realRel = relativeTo(rootReal, real);
|
|
213
249
|
parent = await directoryOf(view, rootReal, realRel, false);
|
|
214
250
|
path = parent.at(nameOf(realRel));
|
|
215
251
|
info = await lstat(path, { bigint: true }).catch((err: unknown) => {
|
|
216
252
|
// Gone since realpath found it: another writer's change.
|
|
217
253
|
if (errno(err) === 'ENOENT') throw new FileChanged(rel);
|
|
218
|
-
throw new WriteRefused(`cannot stat the file the symlink leads to (${errno(err)
|
|
254
|
+
throw new WriteRefused(`cannot stat the file the symlink leads to (${systemWords(errno(err), err)})`, errnoWayOn(errno(err)), errno(err));
|
|
219
255
|
});
|
|
220
256
|
if (info.isSymbolicLink()) throw new FileChanged(rel);
|
|
221
257
|
} else {
|
|
@@ -223,10 +259,10 @@ export async function stageContained(
|
|
|
223
259
|
// lands where the mount may not act: in a nested mount's directory, or
|
|
224
260
|
// on a file its own include leaves out (refusedLanding).
|
|
225
261
|
const leaves = await refusedLanding(view, rootReal, rel, path);
|
|
226
|
-
if (leaves !== null) throw new WriteRefused(leaves);
|
|
262
|
+
if (leaves !== null) throw new WriteRefused(leaves, refusedWayOn(leaves));
|
|
227
263
|
}
|
|
228
|
-
if (info?.isDirectory()) throw new WriteRefused('a directory');
|
|
229
|
-
if (info && !info.isFile()) throw new WriteRefused('not a regular file');
|
|
264
|
+
if (info?.isDirectory()) throw new WriteRefused('a directory', DIRECTORY_THERE);
|
|
265
|
+
if (info && !info.isFile()) throw new WriteRefused('not a regular file', NOT_REGULAR_THERE);
|
|
230
266
|
// A fixed-length name, so a file whose own name is near the 255-byte limit can still be replaced.
|
|
231
267
|
const temp = parent.at(`${STAGED_PREFIX}${randomUUID()}`);
|
|
232
268
|
let made = false;
|
|
@@ -266,8 +302,8 @@ export async function stageContained(
|
|
|
266
302
|
// A staged file that couldn't be made names the directory: a push's
|
|
267
303
|
// write in place needs only the file, and this one needs both.
|
|
268
304
|
throw new WriteRefused(made
|
|
269
|
-
? `cannot write the staged file beside it (${errno(err)
|
|
270
|
-
: `cannot make the staged file in its directory (${errno(err)
|
|
305
|
+
? `cannot write the staged file beside it (${systemWords(errno(err), err)})`
|
|
306
|
+
: `cannot make the staged file in its directory (${systemWords(errno(err), err)}): a direct write needs to write the file's directory as well as the file`, errnoWayOn(errno(err)), errno(err));
|
|
271
307
|
}
|
|
272
308
|
}
|
|
273
309
|
|
|
@@ -278,7 +314,7 @@ export async function stageContained(
|
|
|
278
314
|
*/
|
|
279
315
|
export async function placedStill(staged: Staged): Promise<void> {
|
|
280
316
|
if (!(await staged.root.stillHere())) throw new WriteRefused(ROOT_MOVED);
|
|
281
|
-
if (!(await staged.parent.stillHere())) throw new WriteRefused('the directory changed since it was checked');
|
|
317
|
+
if (!(await staged.parent.stillHere())) throw new WriteRefused('the directory changed since it was checked', AFRESH);
|
|
282
318
|
}
|
|
283
319
|
|
|
284
320
|
/**
|
|
@@ -309,13 +345,13 @@ export function checkUnchanged(staged: Staged, expected: { identity: FileIdentit
|
|
|
309
345
|
try {
|
|
310
346
|
now = lstatSync(staged.path, { bigint: true });
|
|
311
347
|
} catch (err) {
|
|
312
|
-
if (errno(err) !== 'ENOENT') throw new WriteRefused(`cannot check that it hadn't changed (${errno(err)
|
|
348
|
+
if (errno(err) !== 'ENOENT') throw new WriteRefused(`cannot check that it hadn't changed (${systemWords(errno(err), err)})`, errnoWayOn(errno(err)), errno(err));
|
|
313
349
|
if (expected !== null) throw new FileChanged(staged.path);
|
|
314
350
|
return;
|
|
315
351
|
}
|
|
316
|
-
if (now.isSymbolicLink()) throw new WriteRefused('it became a symlink while this call was being applied');
|
|
317
|
-
if (now.isDirectory()) throw new WriteRefused('a directory');
|
|
318
|
-
if (!now.isFile()) throw new WriteRefused('not a regular file');
|
|
352
|
+
if (now.isSymbolicLink()) throw new WriteRefused('it became a symlink while this call was being applied', AFRESH);
|
|
353
|
+
if (now.isDirectory()) throw new WriteRefused('a directory', DIRECTORY_THERE);
|
|
354
|
+
if (!now.isFile()) throw new WriteRefused('not a regular file', NOT_REGULAR_THERE);
|
|
319
355
|
// As a push's write, which opens the file for writing, would be refused:
|
|
320
356
|
// the rename needs only the directory's permission, and would replace a
|
|
321
357
|
// file its owner made read-only.
|
|
@@ -329,9 +365,9 @@ export function checkUnchanged(staged: Staged, expected: { identity: FileIdentit
|
|
|
329
365
|
throw new FileChanged(staged.path);
|
|
330
366
|
}
|
|
331
367
|
if (code === 'EACCES' || code === 'EPERM') {
|
|
332
|
-
throw new WriteRefused(`a file the host's process may not write (${code}): its mode or owner keeps that process out, and calling again is refused until the file's owner, or whoever runs the host, changes that`);
|
|
368
|
+
throw new WriteRefused(`a file the host's process may not write (${errnoWords(code)}): its mode or owner keeps that process out, and calling again is refused until the file's owner, or whoever runs the host, changes that`);
|
|
333
369
|
}
|
|
334
|
-
throw new WriteRefused(`cannot check that the host's process may write it (${code
|
|
370
|
+
throw new WriteRefused(`cannot check that the host's process may write it (${systemWords(code, err)})`, errnoWayOn(code), code);
|
|
335
371
|
}
|
|
336
372
|
if (expected === null) return;
|
|
337
373
|
let bytes: Buffer;
|
|
@@ -353,7 +389,7 @@ export function checkUnchanged(staged: Staged, expected: { identity: FileIdentit
|
|
|
353
389
|
// ELOOP: a link put at the path since the lstat; EAGAIN: a pipe, with a
|
|
354
390
|
// writer and nothing in it yet. Either is another writer's change.
|
|
355
391
|
if (errno(err) === 'ENOENT' || errno(err) === 'ELOOP' || errno(err) === 'EAGAIN') throw new FileChanged(staged.path);
|
|
356
|
-
throw new WriteRefused(`cannot check that it hadn't changed (${errno(err)
|
|
392
|
+
throw new WriteRefused(`cannot check that it hadn't changed (${systemWords(errno(err), err)})`, errnoWayOn(errno(err)), errno(err));
|
|
357
393
|
}
|
|
358
394
|
if (!sameIdentity(identityOf(after), expected.identity) || !bytes.equals(expected.bytes)) throw new FileChanged(staged.path);
|
|
359
395
|
}
|
|
@@ -384,13 +420,13 @@ export function renameInto(staged: Staged): void {
|
|
|
384
420
|
// The staged file is still the one this call wrote, not another put at its name.
|
|
385
421
|
const temp = lstatSync(staged.temp, { bigint: true });
|
|
386
422
|
if (!temp.isFile() || !sameIdentity(identityOf(temp), staged.tempIdentity)) {
|
|
387
|
-
throw new WriteRefused('another writer replaced the staged copy of the new text before it could be renamed over the file, which is as it was');
|
|
423
|
+
throw new WriteRefused('another writer replaced the staged copy of the new text before it could be renamed over the file, which is as it was', 'Calling again writes it afresh.');
|
|
388
424
|
}
|
|
389
425
|
otherLinks = otherLinksAt(staged.path);
|
|
390
426
|
renameSync(staged.temp, staged.path);
|
|
391
427
|
} catch (err) {
|
|
392
428
|
if (err instanceof WriteRefused) throw err;
|
|
393
|
-
throw new WriteRefused(`cannot rename the staged file over it (${errno(err)
|
|
429
|
+
throw new WriteRefused(`cannot rename the staged file over it (${systemWords(errno(err), err)})`, errnoWayOn(errno(err)), errno(err));
|
|
394
430
|
}
|
|
395
431
|
// Set once the rename has happened, so a count never stands for a split that didn't.
|
|
396
432
|
staged.otherLinks = otherLinks;
|
|
@@ -10,14 +10,14 @@
|
|
|
10
10
|
* intent — tombstones, store origin, conflicts — lives in BranchIntents and
|
|
11
11
|
* rewinds with the tree.
|
|
12
12
|
*
|
|
13
|
-
* P is evidence about one directory, so each mount also records the identity
|
|
14
|
-
*
|
|
15
|
-
* that isn't that directory any more (an
|
|
16
|
-
* now empty, a replaced directory) is an
|
|
17
|
-
* which every file was deleted; only an
|
|
18
|
-
* that starts its evidence afresh. A place,
|
|
19
|
-
* binds no root: it only says where the next
|
|
20
|
-
* directory the root is by then.
|
|
13
|
+
* P is evidence about one directory, so each mount also records the identity of
|
|
14
|
+
* the root that evidence was gathered under (sameRootIdentity says when two
|
|
15
|
+
* name the same directory). A root that isn't that directory any more (an
|
|
16
|
+
* unmounted drive whose mountpoint is now empty, a replaced directory) is an
|
|
17
|
+
* unavailable mount, never a disk on which every file was deleted; only an
|
|
18
|
+
* explicit acceptance takes it, and that starts its evidence afresh. A place,
|
|
19
|
+
* where a walk stopped (`walks`), binds no root: it only says where the next
|
|
20
|
+
* walk starts, in whatever directory the root is by then.
|
|
21
21
|
*
|
|
22
22
|
* Every transition is write-ahead (see reconcile.ts): an `intent` entry
|
|
23
23
|
* (`pending`, with the prior value and the expected one) is durable before the
|
|
@@ -33,9 +33,18 @@
|
|
|
33
33
|
* succeed, since its completion releases a draft's protection.
|
|
34
34
|
*/
|
|
35
35
|
|
|
36
|
+
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
36
37
|
import { resolve } from 'node:path';
|
|
37
38
|
import type { JsStore } from '@animalabs/chronicle';
|
|
38
39
|
import { RecordJournal } from '../../record-journal.js';
|
|
40
|
+
import { Refusal, onlyTheHost } from './answers.js';
|
|
41
|
+
|
|
42
|
+
const UNTIL_REOPENED = "the workspace can't check its files against disk until its store is reopened";
|
|
43
|
+
const REOPEN = onlyTheHost('reopen it with a restart');
|
|
44
|
+
|
|
45
|
+
export class EvidenceUnavailable extends Refusal {}
|
|
46
|
+
|
|
47
|
+
export const callGate = new AsyncLocalStorage<{ passed: boolean }>();
|
|
39
48
|
|
|
40
49
|
/** Everything `stat` says about a file that can vouch for unchanged bytes. */
|
|
41
50
|
export interface Fingerprint {
|
|
@@ -94,10 +103,15 @@ export interface Interrupted {
|
|
|
94
103
|
|
|
95
104
|
export type Physical = Agreed | Pending | Interrupted;
|
|
96
105
|
|
|
97
|
-
/**
|
|
106
|
+
/**
|
|
107
|
+
* Which directory a mount root is: its device and inode, and its birth time
|
|
108
|
+
* in nanoseconds as the runtime reports it, where that is above zero, as decimal
|
|
109
|
+
* strings.
|
|
110
|
+
*/
|
|
98
111
|
export interface RootIdentity {
|
|
99
112
|
dev: string;
|
|
100
113
|
ino: string;
|
|
114
|
+
born?: string;
|
|
101
115
|
}
|
|
102
116
|
|
|
103
117
|
/** Whether two configured roots name the same path, whatever their spelling
|
|
@@ -106,8 +120,17 @@ function sameRootPath(a: string, b: string): boolean {
|
|
|
106
120
|
return resolve(a) === resolve(b);
|
|
107
121
|
}
|
|
108
122
|
|
|
123
|
+
/**
|
|
124
|
+
* Whether two identities name the same directory: the same device and inode,
|
|
125
|
+
* or the same inode and birth time on another device, since macOS can give a
|
|
126
|
+
* volume another device number after a reboot. A birth time in whole seconds
|
|
127
|
+
* doesn't count across devices: filesystems made in the same second can give
|
|
128
|
+
* their roots one inode (2 on ext4) and one birth time.
|
|
129
|
+
*/
|
|
109
130
|
export function sameRootIdentity(a: RootIdentity, b: RootIdentity): boolean {
|
|
110
|
-
|
|
131
|
+
if (a.ino !== b.ino) return false;
|
|
132
|
+
if (a.dev === b.dev) return true;
|
|
133
|
+
return a.born !== undefined && a.born === b.born && !a.born.endsWith('000000000');
|
|
111
134
|
}
|
|
112
135
|
|
|
113
136
|
type JournalEntry =
|
|
@@ -219,18 +242,18 @@ export class DiskAgreement {
|
|
|
219
242
|
*/
|
|
220
243
|
ensureReconciled(): void {
|
|
221
244
|
if (this.journal.needsReopen) {
|
|
222
|
-
throw new
|
|
223
|
-
'Disk-agreement evidence is unavailable until the store is reopened, as at the next start: ' +
|
|
224
|
-
'an append to its journal failed inside the store, and the evidence may lack what it recorded',
|
|
225
|
-
);
|
|
245
|
+
throw new EvidenceUnavailable(`${UNTIL_REOPENED}: an append to its record of what disk last agreed with failed inside the store, so that record may lack what the append held. ${REOPEN}`);
|
|
226
246
|
}
|
|
227
247
|
if (!this.whole) {
|
|
228
|
-
throw new
|
|
229
|
-
'Disk-agreement evidence is unavailable until the store is reopened, as at the next start: ' +
|
|
230
|
-
'reading it back from the store failed',
|
|
231
|
-
);
|
|
248
|
+
throw new EvidenceUnavailable(`${UNTIL_REOPENED}: reading back its record of what disk last agreed with failed. ${REOPEN}`);
|
|
232
249
|
}
|
|
233
250
|
if (this.journal.needsReconcile) this.rebuild();
|
|
251
|
+
const gate = callGate.getStore();
|
|
252
|
+
if (gate) gate.passed = true;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
get awaitsReopen(): boolean {
|
|
256
|
+
return this.journal.needsReopen || !this.whole;
|
|
234
257
|
}
|
|
235
258
|
|
|
236
259
|
/** Whether evidence recorded so far still awaits a barrier. */
|
|
@@ -314,11 +337,19 @@ export class DiskAgreement {
|
|
|
314
337
|
|
|
315
338
|
/**
|
|
316
339
|
* Record the root's identity just before the first evidence a mount gains:
|
|
317
|
-
* the caller saw it as the root while gathering that evidence.
|
|
318
|
-
*
|
|
340
|
+
* the caller saw it as the root while gathering that evidence. Once one is
|
|
341
|
+
* recorded, the same directory seen on another device, or with a birth
|
|
342
|
+
* time the record lacks, replaces it and keeps the evidence; another
|
|
343
|
+
* directory is acceptRoot's alone.
|
|
319
344
|
*/
|
|
320
345
|
recordRootIdentity(mount: string, identity: RootIdentity): void {
|
|
321
|
-
|
|
346
|
+
const recorded = this.rootIdentity(mount);
|
|
347
|
+
if (recorded !== undefined) {
|
|
348
|
+
if (!sameRootIdentity(recorded, identity)) return;
|
|
349
|
+
const renumbered = recorded.dev !== identity.dev;
|
|
350
|
+
const bornLearned = recorded.born === undefined && identity.born !== undefined;
|
|
351
|
+
if (!renumbered && !bornLearned) return;
|
|
352
|
+
}
|
|
322
353
|
this.write({ t: 'root', mount, identity }, {});
|
|
323
354
|
}
|
|
324
355
|
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
|
|
14
14
|
import type { ToolResult } from '../../types/events.js';
|
|
15
15
|
import { type Change, type FilePlan, type Leaves, type MatchProblem, SHOWN_MATCHES } from './edit-plan.js';
|
|
16
|
-
import { answerText, fieldLine, sectionLead, sectionText, shownValue, textAnswer } from './result-text.js';
|
|
16
|
+
import { answerText, fieldLine, sectionLead, sectionText, shownValue, textAnswer, type DiskState } from './result-text.js';
|
|
17
17
|
import { MAX_LINE_CHARS, numberedLine, safeCut } from './text.js';
|
|
18
18
|
|
|
19
19
|
/** The tool a call came through, which words its refusals and receipt. */
|
|
@@ -548,8 +548,8 @@ function listedFileLine(file: ListedFile): string {
|
|
|
548
548
|
}
|
|
549
549
|
|
|
550
550
|
/** edit_many's receipt's text before its changed regions: its counts, and the files it lists. */
|
|
551
|
-
function manyHeadText(data: { edits: number; replacements: number; files: ListedFile[]; filesNotListed?: number }): string {
|
|
552
|
-
const header = [fieldLine('edits', data.edits), fieldLine('replacements', data.replacements)];
|
|
551
|
+
function manyHeadText(data: { edits: number; replacements: number; disk: DiskState; files: ListedFile[]; filesNotListed?: number }): string {
|
|
552
|
+
const header = [fieldLine('edits', data.edits), fieldLine('replacements', data.replacements), fieldLine('disk', data.disk)];
|
|
553
553
|
if (data.filesNotListed !== undefined) header.push(fieldLine('filesNotListed', data.filesNotListed));
|
|
554
554
|
return answerText(header, [sectionText('files', data.files.map(listedFileLine).join('\n'))]);
|
|
555
555
|
}
|
|
@@ -571,7 +571,7 @@ function manyHeadText(data: { edits: number; replacements: number; files: Listed
|
|
|
571
571
|
* call answers ResultTooLarge. Otherwise the model reads the text, and a
|
|
572
572
|
* script gets the receipt's fields beside it.
|
|
573
573
|
*/
|
|
574
|
-
export function editReceipt(tool: EditTool, files: StoredFile[], editCount: number, maxChars: number): ToolResult {
|
|
574
|
+
export function editReceipt(tool: EditTool, files: StoredFile[], editCount: number, maxChars: number, disk: DiskState): ToolResult {
|
|
575
575
|
const reserve = receiptReserve(tool, files, maxChars);
|
|
576
576
|
const notesOf = ({ plan, notes }: StoredFile): string[] => {
|
|
577
577
|
const lineEnds = lineEndNote(plan);
|
|
@@ -595,6 +595,7 @@ export function editReceipt(tool: EditTool, files: StoredFile[], editCount: numb
|
|
|
595
595
|
path: file.path,
|
|
596
596
|
size: file.plan.bytes.length,
|
|
597
597
|
replacements: file.plan.replacements,
|
|
598
|
+
disk,
|
|
598
599
|
...(notes.length ? { note: notes.join(' ') } : {}),
|
|
599
600
|
...(file.modeKept ? { modeKept: file.modeKept } : {}),
|
|
600
601
|
};
|
|
@@ -604,9 +605,10 @@ export function editReceipt(tool: EditTool, files: StoredFile[], editCount: numb
|
|
|
604
605
|
return textAnswer(answerText(header, [sectionText('changed', changed)]), { ...head, changed });
|
|
605
606
|
}
|
|
606
607
|
const listed: ListedFile[] = [];
|
|
607
|
-
const data: { edits: number; replacements: number; files: ListedFile[]; filesNotListed?: number; changed: string } = {
|
|
608
|
+
const data: { edits: number; replacements: number; disk: DiskState; files: ListedFile[]; filesNotListed?: number; changed: string } = {
|
|
608
609
|
edits: editCount,
|
|
609
610
|
replacements: files.reduce((sum, file) => sum + file.plan.replacements, 0),
|
|
611
|
+
disk,
|
|
610
612
|
files: listed,
|
|
611
613
|
changed: '',
|
|
612
614
|
};
|