@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
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { ToolResult } from '../../types/events.js';
|
|
2
2
|
import { answerText, fieldLine, sectionLead, sectionText, textAnswer } from './result-text.js';
|
|
3
3
|
import { type FileText, MAX_LINE_CHARS, numberedLine, safeCut } from './text.js';
|
|
4
|
+
import { didNothing } from './answers.js';
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* One page of a read, held to the result limit as its text measures it: a
|
|
@@ -184,7 +185,12 @@ export function characterPage(path: string, file: FileText, offsetChars: number,
|
|
|
184
185
|
const text = file.raw;
|
|
185
186
|
const start = Math.min(offsetChars, text.length);
|
|
186
187
|
if (splitsPair(text, start)) {
|
|
187
|
-
return {
|
|
188
|
+
return {
|
|
189
|
+
success: false,
|
|
190
|
+
isError: true,
|
|
191
|
+
error: didNothing('read', `"offsetChars" ${start} falls between the two halves of a surrogate pair, one character in UTF-16. `
|
|
192
|
+
+ `Give ${start - 1} or ${start + 1}; a page's nextOffsetChars never splits one.`),
|
|
193
|
+
};
|
|
188
194
|
}
|
|
189
195
|
let end = start + Math.min(limitChars, text.length - start);
|
|
190
196
|
if (splitsPair(text, end)) end++;
|
|
@@ -53,9 +53,27 @@ import {
|
|
|
53
53
|
sameRootIdentity,
|
|
54
54
|
} from './disk-agreement.js';
|
|
55
55
|
import type { BranchIntent, BranchIntents, ConflictKind, ConflictRecord, DiskCounterpart } from './branch-intent.js';
|
|
56
|
+
import { storedBytes, type StoredBytes } from './sync.js';
|
|
56
57
|
import {
|
|
58
|
+
BENEATH_SYMLINK,
|
|
59
|
+
type Cause,
|
|
60
|
+
CHANGED,
|
|
61
|
+
CHANGED_WALK,
|
|
62
|
+
DIRECTORY,
|
|
63
|
+
EDITOR_FILE_REASON,
|
|
64
|
+
FILE_CHANGED,
|
|
57
65
|
IGNORED,
|
|
66
|
+
LEAVES_MOUNT,
|
|
67
|
+
LINK_NOT_WALKED,
|
|
68
|
+
LINK_TO_DIRECTORY,
|
|
58
69
|
type MountView,
|
|
70
|
+
NOT_A_DIRECTORY,
|
|
71
|
+
type NotShown,
|
|
72
|
+
NOT_FOLLOWED,
|
|
73
|
+
NOT_REGULAR,
|
|
74
|
+
OUTSIDE_PARENT,
|
|
75
|
+
RESOLVES_OUTSIDE,
|
|
76
|
+
ROOT_UNAVAILABLE,
|
|
59
77
|
OTHER_MOUNT,
|
|
60
78
|
ROOT_REPLACED,
|
|
61
79
|
type Walk,
|
|
@@ -74,7 +92,8 @@ import {
|
|
|
74
92
|
walkScope,
|
|
75
93
|
withinIncludes,
|
|
76
94
|
} from './observe.js';
|
|
77
|
-
import { EffectFailed, type Expect, chainOf, chmodContained, narrowedTo, syncDirectories, unlinkContained, writeContained } from './effects.js';
|
|
95
|
+
import { EffectFailed, type Expect, callFailed, chainOf, chmodContained, narrowedTo, syncDirectories, unlinkContained, writeContained } from './effects.js';
|
|
96
|
+
import { OUTSIDE_STEP, RESOLVED_OUTSIDE_STEP, UNFOLLOWED_STEP, errnoClause, errnoWords } from './answers.js';
|
|
78
97
|
|
|
79
98
|
export type EntryState =
|
|
80
99
|
| 'synced'
|
|
@@ -103,6 +122,7 @@ export interface PathReport {
|
|
|
103
122
|
mimeType?: string;
|
|
104
123
|
conflict?: ConflictReport;
|
|
105
124
|
note?: string;
|
|
125
|
+
noteCause?: Cause;
|
|
106
126
|
/**
|
|
107
127
|
* For a conflict, what this pass found on disk at the path: grep's account
|
|
108
128
|
* of a conflict's disk side needs it. Listings don't show it.
|
|
@@ -147,6 +167,8 @@ export interface PassResult {
|
|
|
147
167
|
* still the selected branch, synchronously, before it reads.
|
|
148
168
|
*/
|
|
149
169
|
branchId?: string;
|
|
170
|
+
/** Why it decided nothing, where it didn't. */
|
|
171
|
+
undecided?: string;
|
|
150
172
|
}
|
|
151
173
|
|
|
152
174
|
export interface MountRuntime {
|
|
@@ -222,7 +244,7 @@ export const WALK_CAP = 5000;
|
|
|
222
244
|
/** What a pass learned about D for one path. */
|
|
223
245
|
type DiskFact =
|
|
224
246
|
| { kind: 'absent' }
|
|
225
|
-
| { kind: 'unobserved'
|
|
247
|
+
| ({ kind: 'unobserved' } & NotShown)
|
|
226
248
|
| {
|
|
227
249
|
kind: 'file';
|
|
228
250
|
hash: string;
|
|
@@ -376,6 +398,7 @@ interface Verdict {
|
|
|
376
398
|
conflict?: ConflictKind;
|
|
377
399
|
op?: TreeOp;
|
|
378
400
|
note?: string;
|
|
401
|
+
noteCause?: Cause;
|
|
379
402
|
/**
|
|
380
403
|
* Disk shows what a pending push of ours intended, unconfirmed: the state
|
|
381
404
|
* is decided from it, and nothing at all is recorded (see module doc).
|
|
@@ -464,21 +487,22 @@ function settleAdoption(p: Pending, s: StoreSide | null): Agreed | 'forget' {
|
|
|
464
487
|
/**
|
|
465
488
|
* What's in the way, and the way out, when disk's shape keeps a push from a
|
|
466
489
|
* path: no push writes a file over a directory or unlinks one, unlinks
|
|
467
|
-
* anything but a regular file or a followed symlink to one,
|
|
468
|
-
*
|
|
469
|
-
*
|
|
470
|
-
*
|
|
490
|
+
* anything but a regular file or a followed symlink to one, writes below a
|
|
491
|
+
* file or a symlink the walk won't take, or writes or unlinks through a
|
|
492
|
+
* symlink the mount doesn't follow or one that leads out of the mount
|
|
493
|
+
* (`writeContained`, `unlinkContained`), forced or not. Null for any other
|
|
494
|
+
* reason.
|
|
471
495
|
*/
|
|
472
496
|
function inTheWay(reason: string | undefined, deletion: boolean): string | null {
|
|
473
497
|
// Observation's words for what a deletion never unlinks, which the unlink
|
|
474
498
|
// refuses in the same words.
|
|
475
|
-
if (deletion && (reason ===
|
|
476
|
-
const what = reason ===
|
|
499
|
+
if (deletion && (reason === NOT_REGULAR || reason === LINK_TO_DIRECTORY)) {
|
|
500
|
+
const what = reason === NOT_REGULAR
|
|
477
501
|
? 'a pipe, socket or device at this path, or a symlink to one'
|
|
478
502
|
: 'a symlink to a directory at this path';
|
|
479
503
|
return `disk has ${what}, and no push unlinks it, forced or not — ask whoever runs this host if it should go`;
|
|
480
504
|
}
|
|
481
|
-
if (reason ===
|
|
505
|
+
if (reason === DIRECTORY) {
|
|
482
506
|
return deletion
|
|
483
507
|
? 'disk has a directory at this path, and no push unlinks one, forced or not — if it should go, move or remove it on disk'
|
|
484
508
|
: 'disk has a directory at this path, and no push writes a file over one, forced or not — ' +
|
|
@@ -486,13 +510,99 @@ function inTheWay(reason: string | undefined, deletion: boolean): string | null
|
|
|
486
510
|
}
|
|
487
511
|
// A deletion below a file finds nothing there to unlink, so only a write
|
|
488
512
|
// meets this one.
|
|
489
|
-
if (reason ===
|
|
513
|
+
if (reason === NOT_A_DIRECTORY) {
|
|
490
514
|
return 'disk has a file or a symlink where this path needs a directory, and no push writes below one, forced or not — ' +
|
|
491
515
|
'move or remove it on disk and materialize again, or write the file under another path';
|
|
492
516
|
}
|
|
517
|
+
const link = reason === NOT_FOLLOWED ? 'disk has a symlink at this path, which this mount does not follow'
|
|
518
|
+
: reason === LEAVES_MOUNT ? 'this path leads out of the mount through a symlink on it'
|
|
519
|
+
: null;
|
|
520
|
+
if (link !== null) {
|
|
521
|
+
return deletion
|
|
522
|
+
? `${link}, and no push unlinks ${reason === NOT_FOLLOWED ? 'one' : 'through one'}, forced or not — ask whoever runs this host if it should go`
|
|
523
|
+
: `${link}, and no push writes through one, forced or not — write the file under another path, ` +
|
|
524
|
+
`or whoever runs this host can remove the link, and ${THEN.write}`;
|
|
525
|
+
}
|
|
526
|
+
if (deletion && reason === BENEATH_SYMLINK) {
|
|
527
|
+
return 'a symlink on this path does not lead to a directory inside the mount, so nothing beneath it is in this mount, and no push unlinks beneath one, '
|
|
528
|
+
+ `forced or not — whoever runs this host can remove the link or make it lead to a directory inside the mount, and ${THEN.unlink}`;
|
|
529
|
+
}
|
|
530
|
+
if (reason === OUTSIDE_PARENT) {
|
|
531
|
+
return `a directory on this path leads out of the mount, and no push ${deletion ? 'unlinks' : 'writes'} beneath one, forced or not — ` +
|
|
532
|
+
(deletion ? 'ask whoever runs this host if what is there should go' : 'write the file under another path');
|
|
533
|
+
}
|
|
534
|
+
if (reason === NOT_REGULAR || reason === LINK_TO_DIRECTORY) {
|
|
535
|
+
const what = reason === NOT_REGULAR ? 'a pipe, socket or device at this path, or a symlink to one' : 'a symlink to a directory at this path';
|
|
536
|
+
return `disk has ${what}, and no push writes a file over it, forced or not — write the file under another path, ` +
|
|
537
|
+
`or whoever runs this host can remove it, and ${THEN.write}`;
|
|
538
|
+
}
|
|
493
539
|
return null;
|
|
494
540
|
}
|
|
495
541
|
|
|
542
|
+
export type Meets = 'write' | 'unlink' | 'sync' | 'list' | 'read';
|
|
543
|
+
|
|
544
|
+
const AGAIN: Record<Meets, string> = {
|
|
545
|
+
write: 'materializing again',
|
|
546
|
+
unlink: 'materializing again with applyDeletions',
|
|
547
|
+
sync: 'syncing this path again',
|
|
548
|
+
list: 'listing it',
|
|
549
|
+
read: 'calling again',
|
|
550
|
+
};
|
|
551
|
+
|
|
552
|
+
const THEN: Record<Meets, string> = {
|
|
553
|
+
write: 'materializing again then gets past it',
|
|
554
|
+
unlink: 'materializing again with applyDeletions then gets past it',
|
|
555
|
+
sync: 'a sync of this path then takes it in',
|
|
556
|
+
list: 'the workspace then reaches it',
|
|
557
|
+
read: 'a read then reaches it',
|
|
558
|
+
};
|
|
559
|
+
|
|
560
|
+
const CHANGED_WHILE: ReadonlySet<string> = new Set([CHANGED, CHANGED_WALK, FILE_CHANGED]);
|
|
561
|
+
|
|
562
|
+
const WALKED_PAST: ReadonlySet<string> = new Set([IGNORED, EDITOR_FILE_REASON, LINK_NOT_WALKED]);
|
|
563
|
+
|
|
564
|
+
export function explained(reason: string, cause: Cause, meets: Meets): string {
|
|
565
|
+
if (meets === 'write' || meets === 'unlink') {
|
|
566
|
+
const shaped = inTheWay(reason, meets === 'unlink');
|
|
567
|
+
if (shaped !== null) return shaped;
|
|
568
|
+
}
|
|
569
|
+
return withWayOn(reason, cause, meets);
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
function withWayOn(reason: string, cause: Cause, meets: Meets): string {
|
|
573
|
+
const on = wayOn(reason, cause, meets);
|
|
574
|
+
return on === null ? reason : `${reason} — ${on}`;
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
export function wayOn(reason: string, cause: Cause, meets: Meets): string | null {
|
|
578
|
+
const lasting = `${AGAIN[meets]} won't change that; `;
|
|
579
|
+
if (reason === ROOT_UNAVAILABLE) {
|
|
580
|
+
if (cause.errno === 'ENOENT' || cause.errno === 'ENOTDIR') return `${lasting}whoever runs this host can put the mount's directory back at its path, and ${THEN[meets]}`;
|
|
581
|
+
if (cause.errno === undefined) return `${AGAIN[meets]} checks it afresh; if it's still unavailable, whoever runs this host can check the mount's directory and its path`;
|
|
582
|
+
}
|
|
583
|
+
if (cause.errno !== undefined) return errnoClause(cause.errno, AGAIN[meets], THEN[meets]);
|
|
584
|
+
if (cause.again === true || CHANGED_WHILE.has(reason)) return `${AGAIN[meets]} checks it afresh`;
|
|
585
|
+
switch (reason) {
|
|
586
|
+
case NOT_FOLLOWED:
|
|
587
|
+
return lasting + UNFOLLOWED_STEP;
|
|
588
|
+
case LEAVES_MOUNT:
|
|
589
|
+
return lasting + OUTSIDE_STEP;
|
|
590
|
+
case BENEATH_SYMLINK:
|
|
591
|
+
return `${lasting}nothing beneath it is in this mount, and whoever runs this host can make it lead to a directory inside the mount, or, where it leads to a directory outside, mount that directory`;
|
|
592
|
+
case OUTSIDE_PARENT:
|
|
593
|
+
case RESOLVES_OUTSIDE:
|
|
594
|
+
return lasting + RESOLVED_OUTSIDE_STEP;
|
|
595
|
+
case NOT_REGULAR:
|
|
596
|
+
return `${lasting}whoever runs this host can put a regular file there, and ${THEN[meets]}`;
|
|
597
|
+
case DIRECTORY:
|
|
598
|
+
return `${lasting}ls of this path lists what's in it`;
|
|
599
|
+
case LINK_TO_DIRECTORY:
|
|
600
|
+
return `${lasting}ls of the directory it leads to lists what's in it`;
|
|
601
|
+
default:
|
|
602
|
+
return meets === 'sync' && WALKED_PAST.has(reason) ? 'a sync of this path checks it' : null;
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
|
|
496
606
|
/**
|
|
497
607
|
* The forced way out the refusal of a disk copy that couldn't be verified
|
|
498
608
|
* names, if any. A forced push skips the verification, so it gets past the
|
|
@@ -506,10 +616,18 @@ function inTheWay(reason: string | undefined, deletion: boolean): string | null
|
|
|
506
616
|
* to pass.
|
|
507
617
|
*/
|
|
508
618
|
function forcedWay(note: string | undefined, deletion: boolean, forced: string): string {
|
|
509
|
-
if (note ===
|
|
619
|
+
if (note === `cannot open (${errnoWords('EACCES')})`) return `, or, if disk lets the workspace ${deletion ? 'unlink' : 'write'} the file, ${forced}`;
|
|
510
620
|
return '';
|
|
511
621
|
}
|
|
512
622
|
|
|
623
|
+
const UNSETTLED = "whether disk holds the change isn't known until it's checked: a materialize of this path checks it and says how it ended";
|
|
624
|
+
|
|
625
|
+
function unverifiedCopy(v: Verdict, meets: 'write' | 'unlink', forced: string): string {
|
|
626
|
+
const note = v.note ?? 'not observed';
|
|
627
|
+
const on = wayOn(note, v.noteCause ?? {}, meets);
|
|
628
|
+
return `cannot verify disk copy (${note})${on !== null ? ` — ${on}` : ''}${forcedWay(v.note, meets === 'unlink', forced)}`;
|
|
629
|
+
}
|
|
630
|
+
|
|
513
631
|
/**
|
|
514
632
|
* The synchronous precondition of every workspace mutation: evidence owed a
|
|
515
633
|
* barrier is made durable first, and an adoption left pending on the
|
|
@@ -635,6 +753,10 @@ function adoptDisk(d: Extract<DiskFact, { kind: 'absent' | 'file' }>, s: StoreSi
|
|
|
635
753
|
return { state: 'disk-only', ...(s ? { adopt: 'drop' as const, op: 'deleted' as const } : {}), p: 'forget', intent: {} };
|
|
636
754
|
}
|
|
637
755
|
|
|
756
|
+
function causeOf({ errno, ownStep }: NotShown): Cause {
|
|
757
|
+
return { ...(errno !== undefined ? { errno } : {}), ...(ownStep ? { ownStep } : {}) };
|
|
758
|
+
}
|
|
759
|
+
|
|
638
760
|
/** The rule (see module doc). Pure. */
|
|
639
761
|
function decide(
|
|
640
762
|
d: DiskFact,
|
|
@@ -650,9 +772,9 @@ function decide(
|
|
|
650
772
|
if (d.kind === 'unobserved') {
|
|
651
773
|
// Disk proves nothing, but an adoption on this branch settles by the tree.
|
|
652
774
|
if (pIn?.kind === 'pending' && pIn.effect === 'adopt' && pIn.branchId === branchId && !adopt) {
|
|
653
|
-
return { state: 'unverified', note: d.reason, p: settleAdoption(pIn, s) };
|
|
775
|
+
return { state: 'unverified', note: d.reason, noteCause: causeOf(d), p: settleAdoption(pIn, s) };
|
|
654
776
|
}
|
|
655
|
-
return { state: 'unverified', note: d.reason };
|
|
777
|
+
return { state: 'unverified', note: d.reason, noteCause: causeOf(d) };
|
|
656
778
|
}
|
|
657
779
|
if (d.kind === 'unhashed') {
|
|
658
780
|
// Observed this way only when nothing tracks the path, which is then a
|
|
@@ -838,6 +960,7 @@ async function gather(store: JsStore, agreement: DiskAgreement, mount: MountRunt
|
|
|
838
960
|
// is followed, as `rootReal` is: `followSymlinks` governs links within it.
|
|
839
961
|
const root = await locateRoot(mount.view, mount.rootReal);
|
|
840
962
|
const rootUnavailable = 'reason' in root ? root.reason : null;
|
|
963
|
+
const rootCause = 'reason' in root && root.errno !== undefined ? { errno: root.errno } : {};
|
|
841
964
|
|
|
842
965
|
// The branch is labelled where the tracked candidates are read, with no
|
|
843
966
|
// await between: the decision is checked against this label.
|
|
@@ -853,7 +976,7 @@ async function gather(store: JsStore, agreement: DiskAgreement, mount: MountRunt
|
|
|
853
976
|
if (rootUnavailable !== null) {
|
|
854
977
|
// Said whether or not anything is tracked: an unavailable mount must not
|
|
855
978
|
// read as a verified empty directory.
|
|
856
|
-
gathered.incomplete.push({ path: scope.kind === 'dir' ? scope.dir : '', reason: rootUnavailable });
|
|
979
|
+
gathered.incomplete.push({ path: scope.kind === 'dir' ? scope.dir : '', reason: explained(rootUnavailable, rootCause, 'list') });
|
|
857
980
|
}
|
|
858
981
|
|
|
859
982
|
// Which paths to decide: what disk shows, and what the store, P and intent
|
|
@@ -892,7 +1015,7 @@ async function gather(store: JsStore, agreement: DiskAgreement, mount: MountRunt
|
|
|
892
1015
|
// Observe. Tracked facts are read again, synchronously, when deciding.
|
|
893
1016
|
for (const path of candidates) {
|
|
894
1017
|
if (rootUnavailable !== null) {
|
|
895
|
-
gathered.facts.set(path, { kind: 'unobserved', reason: rootUnavailable });
|
|
1018
|
+
gathered.facts.set(path, { kind: 'unobserved', reason: rootUnavailable, ...rootCause });
|
|
896
1019
|
continue;
|
|
897
1020
|
}
|
|
898
1021
|
// What the mount passes over by design stays unobserved by a pass that
|
|
@@ -914,8 +1037,8 @@ async function gather(store: JsStore, agreement: DiskAgreement, mount: MountRunt
|
|
|
914
1037
|
byDesign = OTHER_MOUNT;
|
|
915
1038
|
} else if (!opts.named) {
|
|
916
1039
|
const slash = path.lastIndexOf('/');
|
|
917
|
-
const above = slash < 0 ? undefined : passedOverAt(path.slice(0, slash), mount.view.ignore, mount.view.others);
|
|
918
|
-
const own = above === undefined && !walk ? passedOverAt(path, mount.view.ignore, mount.view.others) : undefined;
|
|
1040
|
+
const above = slash < 0 ? undefined : passedOverAt(path.slice(0, slash), mount.view.ignore, mount.view.others, true);
|
|
1041
|
+
const own = above === undefined && !walk ? passedOverAt(path, mount.view.ignore, mount.view.others, false) : undefined;
|
|
919
1042
|
byDesign = above ?? own;
|
|
920
1043
|
} else if (scope.kind === 'dir') {
|
|
921
1044
|
byDesign = passedOverBetween(scope.dir, path, ignoreBeneath(scope.dir, mount.view.ignore), mount.view.others);
|
|
@@ -942,7 +1065,9 @@ async function gather(store: JsStore, agreement: DiskAgreement, mount: MountRunt
|
|
|
942
1065
|
if (r.kind === 'symlink' && r.path === path) continue;
|
|
943
1066
|
if (region === undefined || r.path.length > region.path.length) region = r;
|
|
944
1067
|
}
|
|
945
|
-
gathered.facts.set(path,
|
|
1068
|
+
gathered.facts.set(path, region
|
|
1069
|
+
? { kind: 'unobserved', reason: region.reason, ...(region.errno !== undefined ? { errno: region.errno } : {}) }
|
|
1070
|
+
: { kind: 'unobserved', reason: 'not reached by the walk' });
|
|
946
1071
|
continue;
|
|
947
1072
|
}
|
|
948
1073
|
const p = agreement.get(mount.name, path);
|
|
@@ -959,11 +1084,12 @@ async function gather(store: JsStore, agreement: DiskAgreement, mount: MountRunt
|
|
|
959
1084
|
const after = await locateRoot(mount.view, mount.rootReal);
|
|
960
1085
|
if (!('identity' in after) || !sameRootIdentity(after.identity, gathered.rootIdentity)) {
|
|
961
1086
|
const reason = 'reason' in after ? after.reason : ROOT_REPLACED;
|
|
962
|
-
|
|
1087
|
+
const errno = 'errno' in after ? after.errno : undefined;
|
|
1088
|
+
for (const path of gathered.facts.keys()) gathered.facts.set(path, { kind: 'unobserved', reason, ...(errno !== undefined ? { errno } : {}) });
|
|
963
1089
|
gathered.rootIdentity = null;
|
|
964
1090
|
gathered.walk = null;
|
|
965
1091
|
gathered.dirs = [];
|
|
966
|
-
gathered.incomplete.push({ path: scope.kind === 'dir' ? scope.dir : '', reason });
|
|
1092
|
+
gathered.incomplete.push({ path: scope.kind === 'dir' ? scope.dir : '', reason: explained(reason, errno !== undefined ? { errno } : {}, 'list') });
|
|
967
1093
|
}
|
|
968
1094
|
}
|
|
969
1095
|
return gathered;
|
|
@@ -1000,14 +1126,13 @@ export async function reconcilePass(
|
|
|
1000
1126
|
}
|
|
1001
1127
|
}
|
|
1002
1128
|
if (gathered === null) {
|
|
1003
|
-
result.
|
|
1129
|
+
result.undecided = 'the selected branch kept changing while the scan ran';
|
|
1130
|
+
result.incomplete.push({ path: scope.kind === 'dir' ? scope.dir : '', reason: explained(result.undecided, { again: true }, 'list') });
|
|
1004
1131
|
return result;
|
|
1005
1132
|
}
|
|
1006
1133
|
if (opts.branchId !== undefined && gathered.branchId !== opts.branchId) {
|
|
1007
|
-
result.
|
|
1008
|
-
|
|
1009
|
-
reason: 'the selected branch changed before this sync ran; nothing was taken from disk — sync again on this branch to take it',
|
|
1010
|
-
});
|
|
1134
|
+
result.undecided = 'the selected branch changed before this sync ran; nothing was taken from disk — sync again on this branch to take it';
|
|
1135
|
+
result.incomplete.push({ path: scope.kind === 'dir' ? scope.dir : scope.paths.join(', '), reason: result.undecided });
|
|
1011
1136
|
return result;
|
|
1012
1137
|
}
|
|
1013
1138
|
const { walk, facts } = gathered;
|
|
@@ -1028,9 +1153,13 @@ export async function reconcilePass(
|
|
|
1028
1153
|
}
|
|
1029
1154
|
|
|
1030
1155
|
// The evidence about to be recorded describes the root it was gathered
|
|
1031
|
-
// under, which is recorded first
|
|
1032
|
-
//
|
|
1033
|
-
|
|
1156
|
+
// under, which is recorded first, so no crash can leave evidence without
|
|
1157
|
+
// its root. Once a root is recorded, every pass that found it offers it,
|
|
1158
|
+
// evidence or none: the same root on a new device, or with its first
|
|
1159
|
+
// birth time, is recorded again (recordRootIdentity writes nothing
|
|
1160
|
+
// otherwise), and a quiet mount's passes are where an old record learns
|
|
1161
|
+
// its birth time.
|
|
1162
|
+
if (gathered.rootIdentity && (agreement.rootIdentity(mount.name) !== undefined || planned.some((x) => x.verdict.adopt !== undefined || x.verdict.p !== undefined))) {
|
|
1034
1163
|
agreement.recordRootIdentity(mount.name, gathered.rootIdentity);
|
|
1035
1164
|
}
|
|
1036
1165
|
|
|
@@ -1128,6 +1257,7 @@ export async function reconcilePass(
|
|
|
1128
1257
|
if (x.d.kind === 'file' || x.d.kind === 'absent') report.diskNow = x.d.kind;
|
|
1129
1258
|
}
|
|
1130
1259
|
if (x.verdict.note) report.note = x.verdict.note;
|
|
1260
|
+
if (x.verdict.noteCause !== undefined) report.noteCause = x.verdict.noteCause;
|
|
1131
1261
|
if (!(x.verdict.state === 'synced' && !entry)) result.reports.set(x.path, report);
|
|
1132
1262
|
}
|
|
1133
1263
|
if (walk) {
|
|
@@ -1157,7 +1287,9 @@ export async function reconcilePass(
|
|
|
1157
1287
|
// holder), so naming the directory as unreached would never clear.
|
|
1158
1288
|
const wholeScope = scope.kind === 'dir' && scope.dir !== '' && region.path === scope.dir;
|
|
1159
1289
|
const unreached = hides && region.reason !== OTHER_MOUNT;
|
|
1160
|
-
if (region.kind === 'error' || region.kind === 'cap' || unreached || wholeScope)
|
|
1290
|
+
if (region.kind === 'error' || region.kind === 'cap' || unreached || wholeScope) {
|
|
1291
|
+
result.incomplete.push({ path: region.path, reason: explained(region.reason, region.errno !== undefined ? { errno: region.errno } : {}, 'list') });
|
|
1292
|
+
}
|
|
1161
1293
|
// A name passed over by design that hides nothing tracked is reported nowhere else:
|
|
1162
1294
|
// counted, so a sync that says 0 also says what it didn't look at
|
|
1163
1295
|
// (agent-framework #276). A symlink region isn't counted: a link the
|
|
@@ -1264,7 +1396,8 @@ export async function pushPaths(
|
|
|
1264
1396
|
// unmounted from. The push plans nothing, so it reports no branch.
|
|
1265
1397
|
const root = await locateRoot(mount.view, mount.rootReal);
|
|
1266
1398
|
if ('reason' in root) {
|
|
1267
|
-
|
|
1399
|
+
const why = explained(root.reason, root.errno !== undefined ? { errno: root.errno } : {}, 'write');
|
|
1400
|
+
for (const path of paths) result.skipped.push({ path, reason: why });
|
|
1268
1401
|
return result;
|
|
1269
1402
|
}
|
|
1270
1403
|
// Another mount's directory nested in this one is that mount's: no push
|
|
@@ -1337,7 +1470,7 @@ export async function pushPaths(
|
|
|
1337
1470
|
// nothing is decided or recorded from them.
|
|
1338
1471
|
const after = await locateRoot(mount.view, mount.rootReal);
|
|
1339
1472
|
if (!('identity' in after) || !sameRootIdentity(after.identity, root.identity)) {
|
|
1340
|
-
const reason = 'reason' in after ? after.reason : ROOT_REPLACED;
|
|
1473
|
+
const reason = 'reason' in after ? explained(after.reason, after.errno !== undefined ? { errno: after.errno } : {}, 'write') : ROOT_REPLACED;
|
|
1341
1474
|
for (const path of paths) result.skipped.push({ path, reason });
|
|
1342
1475
|
return result;
|
|
1343
1476
|
}
|
|
@@ -1358,9 +1491,6 @@ export async function pushPaths(
|
|
|
1358
1491
|
const p = agreement.get(mount.name, path);
|
|
1359
1492
|
const bi = mount.intents.get(path);
|
|
1360
1493
|
const v = decide(d, s, p, bi, branchId, false);
|
|
1361
|
-
// The ways out a refusal names, as materialize takes them: a deletion
|
|
1362
|
-
// reaches disk only with applyDeletions.
|
|
1363
|
-
const again = s ? 'materialize again' : 'materialize again with applyDeletions';
|
|
1364
1494
|
// Where disk has deleted a file the workspace holds, a sync takes the
|
|
1365
1495
|
// deletion, which removes the workspace's copy, and a forced push writes
|
|
1366
1496
|
// the workspace's copy back: there is no disk copy to adopt or overwrite.
|
|
@@ -1386,12 +1516,12 @@ export async function pushPaths(
|
|
|
1386
1516
|
continue;
|
|
1387
1517
|
}
|
|
1388
1518
|
const priorP = p && (p.kind === 'absent' || p.kind === 'content') ? p : null;
|
|
1389
|
-
const
|
|
1519
|
+
const stored = storedBytes(store, s.hash);
|
|
1390
1520
|
const forcedMode = mount.modesObserved && s.mode !== undefined && s.mode !== (d.kind === 'file' ? d.mode : undefined) ? s.mode : undefined;
|
|
1391
1521
|
const agreedMode = priorP?.kind === 'content' ? priorP.mode : undefined;
|
|
1392
|
-
if (
|
|
1522
|
+
if ('bytes' in stored) {
|
|
1393
1523
|
plans.push({
|
|
1394
|
-
path, kind: 'write', blob, hash: s.hash, prior: priorP, before: agreement.get(mount.name, path), expect,
|
|
1524
|
+
path, kind: 'write', blob: stored.bytes, hash: s.hash, prior: priorP, before: agreement.get(mount.name, path), expect,
|
|
1395
1525
|
...(forcedMode !== undefined ? { apply: forcedMode } : {}),
|
|
1396
1526
|
...(forcedMode !== undefined && d.kind === 'file' && agreedMode !== d.mode ? { overDisk: true as const } : {}),
|
|
1397
1527
|
...(s.mode !== undefined ? { storeMode: s.mode } : {}),
|
|
@@ -1400,7 +1530,7 @@ export async function pushPaths(
|
|
|
1400
1530
|
...(agreedMode !== undefined ? { priorMode: agreedMode } : {}),
|
|
1401
1531
|
...(d.kind === 'file' && d.mode !== undefined ? { seenMode: d.mode } : {}),
|
|
1402
1532
|
});
|
|
1403
|
-
} else result.skipped.push({ path, reason:
|
|
1533
|
+
} else result.skipped.push({ path, reason: unstoredCopy(stored) });
|
|
1404
1534
|
continue;
|
|
1405
1535
|
}
|
|
1406
1536
|
if (v.mode === 'adopt' && s && d.kind === 'file') {
|
|
@@ -1426,7 +1556,7 @@ export async function pushPaths(
|
|
|
1426
1556
|
}
|
|
1427
1557
|
// Whatever the verdict learned about disk (an agreement, a resolved
|
|
1428
1558
|
// intent) is recorded before anything is pushed over it, and the root it
|
|
1429
|
-
// was learned under before that
|
|
1559
|
+
// was learned under before that, as a pass does.
|
|
1430
1560
|
if (v.p !== undefined) agreement.recordRootIdentity(mount.name, root.identity);
|
|
1431
1561
|
if (v.p === 'forget') agreement.forget(mount.name, path);
|
|
1432
1562
|
else if (v.p !== undefined) agreement.set(mount.name, path, v.p, { afterCommittedState: true });
|
|
@@ -1476,8 +1606,7 @@ export async function pushPaths(
|
|
|
1476
1606
|
? d.kind === 'absent' || d.kind === 'file'
|
|
1477
1607
|
? `${v.note} — ${sync}`
|
|
1478
1608
|
: `${v.note}, and disk's copy can't be verified (${d.kind === 'unobserved' ? d.reason : 'not hashed'}) — once it can be, ${sync}`
|
|
1479
|
-
: inTheWay(v.note, !s) ??
|
|
1480
|
-
`cannot verify disk copy (${v.note ?? 'not observed'}) — fix it and ${again}${forcedWay(v.note, !s, forced)}`,
|
|
1609
|
+
: inTheWay(v.note, !s) ?? unverifiedCopy(v, s ? 'write' : 'unlink', forced),
|
|
1481
1610
|
});
|
|
1482
1611
|
continue;
|
|
1483
1612
|
}
|
|
@@ -1542,7 +1671,7 @@ export async function pushPaths(
|
|
|
1542
1671
|
// A deletion no push makes, forced or not (inTheWay), is refused for
|
|
1543
1672
|
// that, as an unforced push refuses it, rather than left for
|
|
1544
1673
|
// applyDeletions, which couldn't make it either.
|
|
1545
|
-
const lasting = inTheWay(v.note, true);
|
|
1674
|
+
const lasting = inTheWay(v.note, true) ?? (v.noteCause?.ownStep === true ? v.note ?? null : null);
|
|
1546
1675
|
if (lasting !== null) result.skipped.push({ path, reason: lasting });
|
|
1547
1676
|
else result.pendingDeletions.push(path);
|
|
1548
1677
|
continue;
|
|
@@ -1551,11 +1680,12 @@ export async function pushPaths(
|
|
|
1551
1680
|
plans.push({ path, kind: 'unlink', prior, before: agreement.get(mount.name, path), expect });
|
|
1552
1681
|
continue;
|
|
1553
1682
|
}
|
|
1554
|
-
const
|
|
1555
|
-
if (!
|
|
1556
|
-
result.skipped.push({ path, reason:
|
|
1683
|
+
const stored = storedBytes(store, s.hash);
|
|
1684
|
+
if (!('bytes' in stored)) {
|
|
1685
|
+
result.skipped.push({ path, reason: unstoredCopy(stored) });
|
|
1557
1686
|
continue;
|
|
1558
1687
|
}
|
|
1688
|
+
const blob = stored.bytes;
|
|
1559
1689
|
// The store's mode goes to disk with its bytes where the file is new,
|
|
1560
1690
|
// the push is forced, or disk still has the mode they last agreed on. A
|
|
1561
1691
|
// push that completes our own unconfirmed one, whose bytes landed, is a
|
|
@@ -1671,18 +1801,18 @@ export async function pushPaths(
|
|
|
1671
1801
|
effected.push({ step, dirs: [await unlinkContained(mount.view, mount.rootReal, step.path, step.expect)] });
|
|
1672
1802
|
}
|
|
1673
1803
|
} catch (err) {
|
|
1674
|
-
const failure = err instanceof EffectFailed ? err :
|
|
1804
|
+
const failure = err instanceof EffectFailed ? err : callFailed('the push failed', err, true, 'colon');
|
|
1675
1805
|
if (!failure.touched) {
|
|
1676
1806
|
// Disk at this path is as planning saw it: its evidence goes back to
|
|
1677
1807
|
// exactly what it was, whatever kind that was.
|
|
1678
1808
|
agreement.restore(mount.name, step.path, step.before);
|
|
1679
1809
|
recorded = true;
|
|
1680
|
-
const why =
|
|
1810
|
+
const why = step.kind === 'chmod' ? withWayOn(failure.message, failure.detail, 'write') : explained(failure.message, failure.detail, step.kind);
|
|
1681
1811
|
result.skipped.push({ path: step.path, reason: `not ${step.kind === 'unlink' ? 'unlinked' : step.kind === 'chmod' ? 'given the workspace\'s mode' : 'written'}: ${why}` });
|
|
1682
1812
|
} else {
|
|
1683
1813
|
// No completion: the intent stays pending, and the next observation
|
|
1684
1814
|
// resolves it from whatever disk then holds.
|
|
1685
|
-
result.skipped.push({ path: step.path, reason: `${failure.message}
|
|
1815
|
+
result.skipped.push({ path: step.path, reason: `${failure.message} — ${UNSETTLED}` });
|
|
1686
1816
|
}
|
|
1687
1817
|
}
|
|
1688
1818
|
}
|
|
@@ -1702,7 +1832,7 @@ export async function pushPaths(
|
|
|
1702
1832
|
result.skipped.push({
|
|
1703
1833
|
path: step.path,
|
|
1704
1834
|
reason: `could not make the ${step.kind === 'write' ? 'write' : 'unlink'} durable (directory fsync of ${failed === '' ? 'the mount root' : failed}): ` +
|
|
1705
|
-
`${unsynced.get(failed)}
|
|
1835
|
+
`${unsynced.get(failed)} — ${UNSETTLED}`,
|
|
1706
1836
|
});
|
|
1707
1837
|
continue;
|
|
1708
1838
|
}
|
|
@@ -1774,3 +1904,8 @@ export async function pushPaths(
|
|
|
1774
1904
|
agreement.maybeCheckpoint();
|
|
1775
1905
|
return result;
|
|
1776
1906
|
}
|
|
1907
|
+
|
|
1908
|
+
function unstoredCopy(stored: Exclude<StoredBytes, { bytes: Buffer }>): string {
|
|
1909
|
+
return `${'missing' in stored ? 'the workspace copy is missing from the store' : `the workspace copy is unreadable in the store (${stored.unreadable})`}`
|
|
1910
|
+
+ " — a read of this path takes disk's copy where it can, and otherwise says what can be done";
|
|
1911
|
+
}
|
|
@@ -30,6 +30,8 @@ export function shownValue(value: string | number | boolean | null): string {
|
|
|
30
30
|
return JSON.stringify(value).replace(/[\u0085\u2028\u2029]/g, (c) => `\\u${c.charCodeAt(0).toString(16).padStart(4, '0')}`);
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
+
export type DiskState = 'done' | 'waits for materialize' | 'waits for materialize with applyDeletions' | 'in conflict with disk';
|
|
34
|
+
|
|
33
35
|
/** A header line: a field's key and its value as shownValue writes it. */
|
|
34
36
|
export const fieldLine = (key: string, value: string | number | boolean | null): string => `${key}: ${shownValue(value)}`;
|
|
35
37
|
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import { createHash } from 'node:crypto';
|
|
9
|
+
import type { JsStore } from '@animalabs/chronicle';
|
|
9
10
|
|
|
10
11
|
export const DEFAULT_MAX_FILE_SIZE = 5 * 1024 * 1024; // 5MB
|
|
11
12
|
|
|
@@ -18,3 +19,18 @@ export function hashContent(content: string | Buffer): string {
|
|
|
18
19
|
hash.update(content);
|
|
19
20
|
return hash.digest('hex');
|
|
20
21
|
}
|
|
22
|
+
|
|
23
|
+
export type StoredBytes = { bytes: Buffer } | { missing: true } | { unreadable: string };
|
|
24
|
+
|
|
25
|
+
export function storedBytes(store: JsStore, hash: string): StoredBytes {
|
|
26
|
+
try {
|
|
27
|
+
const blob = store.getBlob(hash);
|
|
28
|
+
return blob ? { bytes: blob } : { missing: true };
|
|
29
|
+
} catch (error) {
|
|
30
|
+
if (store.isClosed()) throw error;
|
|
31
|
+
// Chronicle's getBlob throws, rather than answering null, for a blob file
|
|
32
|
+
// it can't read whole or whose format, checksum or hash doesn't check out,
|
|
33
|
+
// such as one a write cut short left truncated.
|
|
34
|
+
return { unreadable: error instanceof Error ? error.message : String(error) };
|
|
35
|
+
}
|
|
36
|
+
}
|