@plurnk/plurnk-service 1.3.12 → 1.5.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/.env.defaults +95 -91
- package/INSTALL.md +54 -28
- package/README.md +25 -10
- package/SPEC.md +2426 -942
- package/dist/Paths.d.ts +4 -1
- package/dist/Paths.d.ts.map +1 -1
- package/dist/Paths.js +24 -23
- package/dist/Paths.js.map +1 -1
- package/dist/build-info.json +1 -1
- package/dist/content/edit-receipt.d.ts +13 -28
- package/dist/content/edit-receipt.d.ts.map +1 -1
- package/dist/content/edit-receipt.js +285 -104
- package/dist/content/edit-receipt.js.map +1 -1
- package/dist/content/edited-span.d.ts.map +1 -1
- package/dist/content/edited-span.js +2 -5
- package/dist/content/edited-span.js.map +1 -1
- package/dist/content/index.d.ts +4 -3
- package/dist/content/index.d.ts.map +1 -1
- package/dist/content/index.js +2 -1
- package/dist/content/index.js.map +1 -1
- package/dist/content/line-marker.d.ts +10 -9
- package/dist/content/line-marker.d.ts.map +1 -1
- package/dist/content/line-marker.js +8 -11
- package/dist/content/line-marker.js.map +1 -1
- package/dist/content/matcher.d.ts +15 -26
- package/dist/content/matcher.d.ts.map +1 -1
- package/dist/content/matcher.js +77 -107
- package/dist/content/matcher.js.map +1 -1
- package/dist/content/mimetype-binary.d.ts +2 -2
- package/dist/content/mimetype-binary.d.ts.map +1 -1
- package/dist/content/mimetype-binary.js +10 -10
- package/dist/content/mimetype-binary.js.map +1 -1
- package/dist/content/path-mimetype.js +3 -3
- package/dist/content/path-mimetype.js.map +1 -1
- package/dist/content/read-projector.d.ts +14 -0
- package/dist/content/read-projector.d.ts.map +1 -0
- package/dist/content/read-projector.js +86 -0
- package/dist/content/read-projector.js.map +1 -0
- package/dist/content/read-resolve.d.ts +6 -7
- package/dist/content/read-resolve.d.ts.map +1 -1
- package/dist/content/read-resolve.js +36 -59
- package/dist/content/read-resolve.js.map +1 -1
- package/dist/core/BranchReceipt.d.ts +6 -0
- package/dist/core/BranchReceipt.d.ts.map +1 -0
- package/dist/core/BranchReceipt.js +24 -0
- package/dist/core/BranchReceipt.js.map +1 -0
- package/dist/core/BudgetOverflow.d.ts +20 -0
- package/dist/core/BudgetOverflow.d.ts.map +1 -0
- package/dist/core/BudgetOverflow.js +49 -0
- package/dist/core/BudgetOverflow.js.map +1 -0
- package/dist/core/BudgetReadout.d.ts +8 -0
- package/dist/core/BudgetReadout.d.ts.map +1 -0
- package/dist/core/BudgetReadout.js +79 -0
- package/dist/core/BudgetReadout.js.map +1 -0
- package/dist/core/ChannelWrite.d.ts +31 -9
- package/dist/core/ChannelWrite.d.ts.map +1 -1
- package/dist/core/ChannelWrite.js +30 -23
- package/dist/core/ChannelWrite.js.map +1 -1
- package/dist/core/ChannelWrite.sql +14 -12
- package/dist/core/CoreSchemeServices.d.ts +18 -4
- package/dist/core/CoreSchemeServices.d.ts.map +1 -1
- package/dist/core/CoreSchemeServices.js +7 -1
- package/dist/core/CoreSchemeServices.js.map +1 -1
- package/dist/core/Dispatcher.d.ts +26 -21
- package/dist/core/Dispatcher.d.ts.map +1 -1
- package/dist/core/Dispatcher.js +1626 -671
- package/dist/core/Dispatcher.js.map +1 -1
- package/dist/core/DurableStatement.d.ts +6 -0
- package/dist/core/DurableStatement.d.ts.map +1 -0
- package/dist/core/DurableStatement.js +51 -0
- package/dist/core/DurableStatement.js.map +1 -0
- package/dist/core/Engine.d.ts +64 -29
- package/dist/core/Engine.d.ts.map +1 -1
- package/dist/core/Engine.js +1348 -835
- package/dist/core/Engine.js.map +1 -1
- package/dist/core/Engine.sql +329 -234
- package/dist/core/EnvFlags.js +1 -1
- package/dist/core/EnvFlags.js.map +1 -1
- package/dist/core/ErrorDetail.d.ts +6 -0
- package/dist/core/ErrorDetail.d.ts.map +1 -0
- package/dist/core/ErrorDetail.js +20 -0
- package/dist/core/ErrorDetail.js.map +1 -0
- package/dist/core/ExecutorRegistry.d.ts +14 -3
- package/dist/core/ExecutorRegistry.d.ts.map +1 -1
- package/dist/core/ExecutorRegistry.js +56 -30
- package/dist/core/ExecutorRegistry.js.map +1 -1
- package/dist/core/GitBranch.d.ts +20 -0
- package/dist/core/GitBranch.d.ts.map +1 -0
- package/dist/core/GitBranch.js +110 -0
- package/dist/core/GitBranch.js.map +1 -0
- package/dist/core/JournalTurn.d.ts +9 -0
- package/dist/core/JournalTurn.d.ts.map +1 -0
- package/dist/core/JournalTurn.js +14 -0
- package/dist/core/JournalTurn.js.map +1 -0
- package/dist/core/JournalTurn.sql +10 -0
- package/dist/core/LogBody.d.ts +18 -0
- package/dist/core/LogBody.d.ts.map +1 -0
- package/dist/core/LogBody.js +180 -0
- package/dist/core/LogBody.js.map +1 -0
- package/dist/core/LoopFlagsReader.d.ts +7 -0
- package/dist/core/LoopFlagsReader.d.ts.map +1 -0
- package/dist/core/LoopFlagsReader.js +33 -0
- package/dist/core/LoopFlagsReader.js.map +1 -0
- package/dist/core/LoopLifecycle.d.ts +8 -2
- package/dist/core/LoopLifecycle.d.ts.map +1 -1
- package/dist/core/LoopLifecycle.js +54 -11
- package/dist/core/LoopLifecycle.js.map +1 -1
- package/dist/core/LoopLifecycle.sql +10 -7
- package/dist/core/NoticeChannel.d.ts +13 -0
- package/dist/core/NoticeChannel.d.ts.map +1 -0
- package/dist/core/NoticeChannel.js +40 -0
- package/dist/core/NoticeChannel.js.map +1 -0
- package/dist/core/Owner.d.ts +0 -1
- package/dist/core/Owner.d.ts.map +1 -1
- package/dist/core/Owner.js +2 -5
- package/dist/core/Owner.js.map +1 -1
- package/dist/core/PacketBuilder.d.ts +27 -25
- package/dist/core/PacketBuilder.d.ts.map +1 -1
- package/dist/core/PacketBuilder.js +321 -383
- package/dist/core/PacketBuilder.js.map +1 -1
- package/dist/core/ProblemLog.d.ts +26 -0
- package/dist/core/ProblemLog.d.ts.map +1 -0
- package/dist/core/ProblemLog.js +58 -0
- package/dist/core/ProblemLog.js.map +1 -0
- package/dist/core/ProposalLifecycle.d.ts +15 -21
- package/dist/core/ProposalLifecycle.d.ts.map +1 -1
- package/dist/core/ProposalLifecycle.js +291 -78
- package/dist/core/ProposalLifecycle.js.map +1 -1
- package/dist/core/ProviderInstantiate.d.ts.map +1 -1
- package/dist/core/ProviderInstantiate.js +51 -48
- package/dist/core/ProviderInstantiate.js.map +1 -1
- package/dist/core/SchemeRegistry.d.ts +14 -8
- package/dist/core/SchemeRegistry.d.ts.map +1 -1
- package/dist/core/SchemeRegistry.js +189 -114
- package/dist/core/SchemeRegistry.js.map +1 -1
- package/dist/core/ServiceTeardown.d.ts +11 -0
- package/dist/core/ServiceTeardown.d.ts.map +1 -0
- package/dist/core/ServiceTeardown.js +70 -0
- package/dist/core/ServiceTeardown.js.map +1 -0
- package/dist/core/StoredPacket.d.ts +29 -0
- package/dist/core/StoredPacket.d.ts.map +1 -0
- package/dist/core/StoredPacket.js +147 -0
- package/dist/core/StoredPacket.js.map +1 -0
- package/dist/core/StrikeRail.d.ts +6 -3
- package/dist/core/StrikeRail.d.ts.map +1 -1
- package/dist/core/StrikeRail.js +27 -56
- package/dist/core/StrikeRail.js.map +1 -1
- package/dist/core/TerminalResult.d.ts +16 -0
- package/dist/core/TerminalResult.d.ts.map +1 -0
- package/dist/core/TerminalResult.js +49 -0
- package/dist/core/TerminalResult.js.map +1 -0
- package/dist/core/WorkerControlAddress.d.ts +17 -0
- package/dist/core/WorkerControlAddress.d.ts.map +1 -0
- package/dist/core/WorkerControlAddress.js +44 -0
- package/dist/core/WorkerControlAddress.js.map +1 -0
- package/dist/core/WorkerName.d.ts +31 -0
- package/dist/core/WorkerName.d.ts.map +1 -0
- package/dist/core/WorkerName.js +95 -0
- package/dist/core/WorkerName.js.map +1 -0
- package/dist/core/WorkerName.sql +36 -0
- package/dist/core/WorkspaceGate.d.ts +14 -0
- package/dist/core/WorkspaceGate.d.ts.map +1 -0
- package/dist/core/WorkspaceGate.js +155 -0
- package/dist/core/WorkspaceGate.js.map +1 -0
- package/dist/core/caps/CapsResolve.d.ts +5 -1
- package/dist/core/caps/CapsResolve.d.ts.map +1 -1
- package/dist/core/caps/CapsResolve.js +8 -4
- package/dist/core/caps/CapsResolve.js.map +1 -1
- package/dist/core/caps/DbChannelCaps.d.ts +5 -11
- package/dist/core/caps/DbChannelCaps.d.ts.map +1 -1
- package/dist/core/caps/DbChannelCaps.js +30 -12
- package/dist/core/caps/DbChannelCaps.js.map +1 -1
- package/dist/core/caps/DbEntryCaps.d.ts +5 -14
- package/dist/core/caps/DbEntryCaps.d.ts.map +1 -1
- package/dist/core/caps/DbEntryCaps.js +27 -8
- package/dist/core/caps/DbEntryCaps.js.map +1 -1
- package/dist/core/caps/DbNotifyCaps.d.ts +1 -1
- package/dist/core/caps/DbNotifyCaps.d.ts.map +1 -1
- package/dist/core/caps/DbNotifyCaps.js +18 -15
- package/dist/core/caps/DbNotifyCaps.js.map +1 -1
- package/dist/core/caps/DbProjectionCaps.d.ts +5 -5
- package/dist/core/caps/DbProjectionCaps.d.ts.map +1 -1
- package/dist/core/caps/DbProjectionCaps.js +21 -4
- package/dist/core/caps/DbProjectionCaps.js.map +1 -1
- package/dist/core/caps/DbSubscriptionCaps.d.ts +4 -6
- package/dist/core/caps/DbSubscriptionCaps.d.ts.map +1 -1
- package/dist/core/caps/DbSubscriptionCaps.js +75 -66
- package/dist/core/caps/DbSubscriptionCaps.js.map +1 -1
- package/dist/core/caps/DbTagCaps.d.ts +5 -12
- package/dist/core/caps/DbTagCaps.d.ts.map +1 -1
- package/dist/core/caps/DbTagCaps.js +20 -11
- package/dist/core/caps/DbTagCaps.js.map +1 -1
- package/dist/core/caps/SchemeCtxImpl.d.ts +6 -1
- package/dist/core/caps/SchemeCtxImpl.d.ts.map +1 -1
- package/dist/core/caps/SchemeCtxImpl.js +6 -6
- package/dist/core/caps/SchemeCtxImpl.js.map +1 -1
- package/dist/core/content-hash.js +1 -1
- package/dist/core/content-hash.js.map +1 -1
- package/dist/core/env-defaults.d.ts.map +1 -1
- package/dist/core/env-defaults.js.map +1 -1
- package/dist/core/fork.d.ts.map +1 -1
- package/dist/core/fork.js +55 -30
- package/dist/core/fork.js.map +1 -1
- package/dist/core/fork.sql +43 -29
- package/dist/core/git-env.d.ts +2 -0
- package/dist/core/git-env.d.ts.map +1 -1
- package/dist/core/git-env.js +13 -18
- package/dist/core/git-env.js.map +1 -1
- package/dist/core/git-iso.d.ts +3 -2
- package/dist/core/git-iso.d.ts.map +1 -1
- package/dist/core/git-iso.js +36 -27
- package/dist/core/git-iso.js.map +1 -1
- package/dist/core/git-membership.d.ts +2 -1
- package/dist/core/git-membership.d.ts.map +1 -1
- package/dist/core/git-membership.js +276 -162
- package/dist/core/git-membership.js.map +1 -1
- package/dist/core/git-state.d.ts +8 -1
- package/dist/core/git-state.d.ts.map +1 -1
- package/dist/core/git-state.js +44 -26
- package/dist/core/git-state.js.map +1 -1
- package/dist/core/namespace.d.ts +1 -0
- package/dist/core/namespace.d.ts.map +1 -1
- package/dist/core/namespace.js +20 -0
- package/dist/core/namespace.js.map +1 -1
- package/dist/core/optimistic-settlement.d.ts +2 -0
- package/dist/core/optimistic-settlement.d.ts.map +1 -0
- package/dist/core/optimistic-settlement.js +14 -0
- package/dist/core/optimistic-settlement.js.map +1 -0
- package/dist/core/owner.sql +1 -1
- package/dist/core/packet-inject.d.ts.map +1 -1
- package/dist/core/packet-inject.js +7 -11
- package/dist/core/packet-inject.js.map +1 -1
- package/dist/core/packet-wire.d.ts +3 -21
- package/dist/core/packet-wire.d.ts.map +1 -1
- package/dist/core/packet-wire.js +305 -385
- package/dist/core/packet-wire.js.map +1 -1
- package/dist/core/plurnk-uri.d.ts +12 -1
- package/dist/core/plurnk-uri.d.ts.map +1 -1
- package/dist/core/plurnk-uri.js +49 -29
- package/dist/core/plurnk-uri.js.map +1 -1
- package/dist/core/results.d.ts +18 -5
- package/dist/core/results.d.ts.map +1 -1
- package/dist/core/results.js +33 -18
- package/dist/core/results.js.map +1 -1
- package/dist/core/ruler_count.d.ts +4 -0
- package/dist/core/ruler_count.d.ts.map +1 -0
- package/dist/core/ruler_count.js +7 -0
- package/dist/core/ruler_count.js.map +1 -0
- package/dist/core/scheme-types.d.ts +3 -17
- package/dist/core/scheme-types.d.ts.map +1 -1
- package/dist/core/scheme-types.js.map +1 -1
- package/dist/core/search-gate.d.ts.map +1 -1
- package/dist/core/search-gate.js +3 -11
- package/dist/core/search-gate.js.map +1 -1
- package/dist/core/teaching-corpus.d.ts +6 -0
- package/dist/core/teaching-corpus.d.ts.map +1 -0
- package/dist/core/teaching-corpus.js +23 -0
- package/dist/core/teaching-corpus.js.map +1 -0
- package/dist/core/teaching.d.ts +0 -1
- package/dist/core/teaching.d.ts.map +1 -1
- package/dist/core/teaching.js +2 -8
- package/dist/core/teaching.js.map +1 -1
- package/dist/core/token-ruler.d.ts.map +1 -1
- package/dist/core/token-ruler.js +3 -16
- package/dist/core/token-ruler.js.map +1 -1
- package/dist/core/turn-scheduler.d.ts +1 -1
- package/dist/core/turn-scheduler.d.ts.map +1 -1
- package/dist/core/turn-scheduler.js +1 -1
- package/dist/core/turn-scheduler.js.map +1 -1
- package/dist/core/worker-cap.d.ts +2 -4
- package/dist/core/worker-cap.d.ts.map +1 -1
- package/dist/core/worker-cap.js +13 -4
- package/dist/core/worker-cap.js.map +1 -1
- package/dist/core/{run-ops.sql → worker-ops.sql} +14 -14
- package/dist/core/workspace-settings.d.ts.map +1 -1
- package/dist/core/workspace-settings.js +17 -9
- package/dist/core/workspace-settings.js.map +1 -1
- package/dist/digest/Digest.d.ts +2 -1
- package/dist/digest/Digest.d.ts.map +1 -1
- package/dist/digest/Digest.js +510 -149
- package/dist/digest/Digest.js.map +1 -1
- package/dist/digest/digest.sql +23 -10
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -5
- package/dist/index.js.map +1 -1
- package/dist/observe/api.d.ts +6 -0
- package/dist/observe/api.d.ts.map +1 -0
- package/dist/observe/api.js +11 -0
- package/dist/observe/api.js.map +1 -0
- package/dist/observe/init.d.ts +7 -0
- package/dist/observe/init.d.ts.map +1 -0
- package/dist/observe/init.js +159 -0
- package/dist/observe/init.js.map +1 -0
- package/dist/observe/metrics.d.ts +6 -0
- package/dist/observe/metrics.d.ts.map +1 -0
- package/dist/observe/metrics.js +15 -0
- package/dist/observe/metrics.js.map +1 -0
- package/dist/observe/spans.d.ts +4 -0
- package/dist/observe/spans.d.ts.map +1 -0
- package/dist/observe/spans.js +51 -0
- package/dist/observe/spans.js.map +1 -0
- package/dist/schemes/EffectPolicy.d.ts +1 -0
- package/dist/schemes/EffectPolicy.d.ts.map +1 -1
- package/dist/schemes/EffectPolicy.js +5 -2
- package/dist/schemes/EffectPolicy.js.map +1 -1
- package/dist/schemes/Exec.d.ts +9 -14
- package/dist/schemes/Exec.d.ts.map +1 -1
- package/dist/schemes/Exec.js +402 -262
- package/dist/schemes/Exec.js.map +1 -1
- package/dist/schemes/ExecOutputScheme.d.ts +9 -9
- package/dist/schemes/ExecOutputScheme.d.ts.map +1 -1
- package/dist/schemes/ExecOutputScheme.js +49 -18
- package/dist/schemes/ExecOutputScheme.js.map +1 -1
- package/dist/schemes/File.d.ts +7 -13
- package/dist/schemes/File.d.ts.map +1 -1
- package/dist/schemes/File.js +280 -151
- package/dist/schemes/File.js.map +1 -1
- package/dist/schemes/Log.d.ts +8 -12
- package/dist/schemes/Log.d.ts.map +1 -1
- package/dist/schemes/Log.js +402 -233
- package/dist/schemes/Log.js.map +1 -1
- package/dist/schemes/Log.sql +71 -32
- package/dist/schemes/Prompt.d.ts +3 -3
- package/dist/schemes/Prompt.d.ts.map +1 -1
- package/dist/schemes/Prompt.js +10 -10
- package/dist/schemes/Prompt.js.map +1 -1
- package/dist/schemes/Skill.d.ts +2 -3
- package/dist/schemes/Skill.d.ts.map +1 -1
- package/dist/schemes/Skill.js +2 -6
- package/dist/schemes/Skill.js.map +1 -1
- package/dist/schemes/Worker.d.ts +11 -19
- package/dist/schemes/Worker.d.ts.map +1 -1
- package/dist/schemes/Worker.js +210 -89
- package/dist/schemes/Worker.js.map +1 -1
- package/dist/schemes/_entry-chunk.d.ts.map +1 -1
- package/dist/schemes/_entry-chunk.js +12 -5
- package/dist/schemes/_entry-chunk.js.map +1 -1
- package/dist/schemes/_entry-crud.d.ts +11 -9
- package/dist/schemes/_entry-crud.d.ts.map +1 -1
- package/dist/schemes/_entry-crud.js +72 -13
- package/dist/schemes/_entry-crud.js.map +1 -1
- package/dist/schemes/_entry-crud.sql +28 -10
- package/dist/schemes/_entry-find.d.ts +49 -18
- package/dist/schemes/_entry-find.d.ts.map +1 -1
- package/dist/schemes/_entry-find.js +348 -195
- package/dist/schemes/_entry-find.js.map +1 -1
- package/dist/schemes/_entry-find.sql +20 -8
- package/dist/schemes/_entry-graph.d.ts +4 -2
- package/dist/schemes/_entry-graph.d.ts.map +1 -1
- package/dist/schemes/_entry-graph.js +55 -31
- package/dist/schemes/_entry-graph.js.map +1 -1
- package/dist/schemes/_entry-graph.sql +37 -22
- package/dist/schemes/_entry-manifest.d.ts +10 -15
- package/dist/schemes/_entry-manifest.d.ts.map +1 -1
- package/dist/schemes/_entry-manifest.js +50 -268
- package/dist/schemes/_entry-manifest.js.map +1 -1
- package/dist/schemes/_entry-ops.d.ts +14 -15
- package/dist/schemes/_entry-ops.d.ts.map +1 -1
- package/dist/schemes/_entry-ops.js +121 -110
- package/dist/schemes/_entry-ops.js.map +1 -1
- package/dist/schemes/_entry-ops.sql +2 -3
- package/dist/schemes/_entry-semantic.d.ts +22 -9
- package/dist/schemes/_entry-semantic.d.ts.map +1 -1
- package/dist/schemes/_entry-semantic.js +127 -67
- package/dist/schemes/_entry-semantic.js.map +1 -1
- package/dist/schemes/_entry-semantic.sql +63 -45
- package/dist/schemes/_entry-send.d.ts +3 -4
- package/dist/schemes/_entry-send.d.ts.map +1 -1
- package/dist/schemes/_entry-send.js +48 -23
- package/dist/schemes/_entry-send.js.map +1 -1
- package/dist/schemes/_path-scope.d.ts +23 -0
- package/dist/schemes/_path-scope.d.ts.map +1 -0
- package/dist/schemes/_path-scope.js +61 -0
- package/dist/schemes/_path-scope.js.map +1 -0
- package/dist/schemes/_search-candidate.d.ts +20 -0
- package/dist/schemes/_search-candidate.d.ts.map +1 -0
- package/dist/schemes/_search-candidate.js +19 -0
- package/dist/schemes/_search-candidate.js.map +1 -0
- package/dist/schemes/_search-exclusion.d.ts +7 -0
- package/dist/schemes/_search-exclusion.d.ts.map +1 -0
- package/dist/schemes/_search-exclusion.js +23 -0
- package/dist/schemes/_search-exclusion.js.map +1 -0
- package/dist/schemes/_search-index.d.ts +6 -0
- package/dist/schemes/_search-index.d.ts.map +1 -0
- package/dist/schemes/_search-index.js +328 -0
- package/dist/schemes/_search-index.js.map +1 -0
- package/dist/schemes/cosine.js +1 -1
- package/dist/schemes/cosine.js.map +1 -1
- package/dist/schemes/exec-abort.js +3 -3
- package/dist/schemes/exec-abort.js.map +1 -1
- package/dist/schemes/exec-env.js +3 -3
- package/dist/schemes/exec-env.js.map +1 -1
- package/dist/server/BranchBatches.d.ts +49 -0
- package/dist/server/BranchBatches.d.ts.map +1 -0
- package/dist/server/BranchBatches.js +592 -0
- package/dist/server/BranchBatches.js.map +1 -0
- package/dist/server/Daemon.d.ts +39 -71
- package/dist/server/Daemon.d.ts.map +1 -1
- package/dist/server/Daemon.js +1133 -491
- package/dist/server/Daemon.js.map +1 -1
- package/dist/server/DaemonModule.d.ts +31 -0
- package/dist/server/DaemonModule.d.ts.map +1 -0
- package/dist/server/DaemonModule.js +2 -0
- package/dist/server/DaemonModule.js.map +1 -0
- package/dist/server/branch-batch.sql +154 -0
- package/dist/server/client-input.d.ts +13 -0
- package/dist/server/client-input.d.ts.map +1 -1
- package/dist/server/client-input.js +268 -57
- package/dist/server/client-input.js.map +1 -1
- package/dist/server/dispatch-as-plurnk.d.ts +1 -1
- package/dist/server/dispatch-as-plurnk.d.ts.map +1 -1
- package/dist/server/dispatch-as-plurnk.js +15 -7
- package/dist/server/dispatch-as-plurnk.js.map +1 -1
- package/dist/server/drain.sql +96 -65
- package/dist/server/envelope.d.ts +4 -7
- package/dist/server/envelope.d.ts.map +1 -1
- package/dist/server/envelope.js +118 -95
- package/dist/server/envelope.js.map +1 -1
- package/dist/server/envelope.sql +18 -23
- package/dist/server/exec-poll-backoff.js +1 -1
- package/dist/server/exec-poll-backoff.js.map +1 -1
- package/dist/server/lifecycle-recovery.sql +112 -2
- package/dist/server/logEntry.d.ts +4 -2
- package/dist/server/logEntry.d.ts.map +1 -1
- package/dist/server/logEntry.js +6 -3
- package/dist/server/logEntry.js.map +1 -1
- package/dist/server/logEntry.sql +1 -1
- package/dist/server/loop-model.d.ts.map +1 -1
- package/dist/server/loop-model.js +22 -9
- package/dist/server/loop-model.js.map +1 -1
- package/dist/server/loopDocs.d.ts.map +1 -1
- package/dist/server/loopDocs.js +5 -4
- package/dist/server/loopDocs.js.map +1 -1
- package/dist/server/seam-entry-read.sql +15 -10
- package/dist/server/seam-log-read.sql +2 -2
- package/dist/server/seam-loop.sql +1 -1
- package/dist/server/seam-proposal-list.sql +17 -7
- package/dist/service.d.ts.map +1 -1
- package/dist/service.js +79 -61
- package/dist/service.js.map +1 -1
- package/migrations/001_schema.sql +1010 -0
- package/package.json +65 -32
- package/dist/core/PluginLoader.d.ts +0 -20
- package/dist/core/PluginLoader.d.ts.map +0 -1
- package/dist/core/PluginLoader.js +0 -142
- package/dist/core/PluginLoader.js.map +0 -1
- package/dist/core/TelemetryChannel.d.ts +0 -37
- package/dist/core/TelemetryChannel.d.ts.map +0 -1
- package/dist/core/TelemetryChannel.js +0 -80
- package/dist/core/TelemetryChannel.js.map +0 -1
- package/dist/core/path-decode.d.ts +0 -3
- package/dist/core/path-decode.d.ts.map +0 -1
- package/dist/core/path-decode.js +0 -9
- package/dist/core/path-decode.js.map +0 -1
- package/dist/core/plugin-attribution.d.ts +0 -5
- package/dist/core/plugin-attribution.d.ts.map +0 -1
- package/dist/core/plugin-attribution.js +0 -39
- package/dist/core/plugin-attribution.js.map +0 -1
- package/dist/core/world-state.d.ts +0 -10
- package/dist/core/world-state.d.ts.map +0 -1
- package/dist/core/world-state.js +0 -35
- package/dist/core/world-state.js.map +0 -1
- package/dist/core/world-state.sql +0 -35
- package/dist/core/zero-pin.d.ts +0 -3
- package/dist/core/zero-pin.d.ts.map +0 -1
- package/dist/core/zero-pin.js +0 -16
- package/dist/core/zero-pin.js.map +0 -1
- package/dist/server/auto.d.ts +0 -6
- package/dist/server/auto.d.ts.map +0 -1
- package/dist/server/auto.js +0 -63
- package/dist/server/auto.js.map +0 -1
- package/dist/server/clientTurn.d.ts +0 -6
- package/dist/server/clientTurn.d.ts.map +0 -1
- package/dist/server/clientTurn.js +0 -22
- package/dist/server/clientTurn.js.map +0 -1
- package/dist/server/clientTurn.sql +0 -10
- package/dist/server/noProposals.d.ts +0 -6
- package/dist/server/noProposals.d.ts.map +0 -1
- package/dist/server/noProposals.js +0 -37
- package/dist/server/noProposals.js.map +0 -1
- package/dist/server/version-info.d.ts +0 -14
- package/dist/server/version-info.d.ts.map +0 -1
- package/dist/server/version-info.js +0 -69
- package/dist/server/version-info.js.map +0 -1
- package/migrations/0000-00-00.01_schema.sql +0 -536
- package/migrations/0004_loop-provider.sql +0 -3
- package/migrations/0005_subscription-published-channel.sql +0 -3
- package/migrations/0006_loop-max-turns.sql +0 -3
package/dist/core/Dispatcher.js
CHANGED
|
@@ -1,37 +1,48 @@
|
|
|
1
1
|
var _a;
|
|
2
|
-
import { parsePath } from "@plurnk/plurnk-grammar";
|
|
3
2
|
import Owner from "./Owner.js";
|
|
4
|
-
import {
|
|
3
|
+
import WorkerName, { WorkerNameError } from "./WorkerName.js";
|
|
4
|
+
import WorkerControlAddress from "./WorkerControlAddress.js";
|
|
5
|
+
import { entryPathnameOf, foldAuthorityIntoPath, renderAddress, renderTarget, schemeNameOf } from "./plurnk-uri.js";
|
|
5
6
|
import Fork from "./fork.js";
|
|
6
7
|
import WorkerCap from "./worker-cap.js";
|
|
7
|
-
import {
|
|
8
|
+
import { PathSyntax } from "@plurnk/plurnk-contracts";
|
|
8
9
|
import Namespace from "./namespace.js";
|
|
9
|
-
import
|
|
10
|
+
import LoopFlagsReader from "./LoopFlagsReader.js";
|
|
10
11
|
import ChannelWrite from "./ChannelWrite.js";
|
|
11
|
-
import { LineMarkerOps, MimetypeBinary } from "../content/index.js";
|
|
12
|
+
import { assertEditBatchReceipt, assertEditReceipt, assertResourceEffects, editReceipt, LineMarkerOps, MimetypeBinary, PathMimetype, ReadProjector, projectEditReceipt, } from "../content/index.js";
|
|
12
13
|
import SchemeCtxImpl from "./caps/SchemeCtxImpl.js";
|
|
13
14
|
import EntryCrud from "../schemes/_entry-crud.js";
|
|
14
|
-
import EntryFind from "../schemes/_entry-find.js";
|
|
15
15
|
import EntryOps from "../schemes/_entry-ops.js";
|
|
16
|
+
import EntryFind from "../schemes/_entry-find.js";
|
|
16
17
|
import WorkspaceSettings from "./workspace-settings.js";
|
|
17
18
|
import LoopLifecycle from "./LoopLifecycle.js";
|
|
18
|
-
|
|
19
|
+
import TerminalResult from "./TerminalResult.js";
|
|
20
|
+
import Results from "./results.js";
|
|
21
|
+
import { OperationFailureError } from "./results.js";
|
|
22
|
+
import EffectPolicy from "../schemes/EffectPolicy.js";
|
|
23
|
+
import { CoreSchemeAdapterBase, } from "./CoreSchemeServices.js";
|
|
24
|
+
import { InvalidOperationResultError, NetworkAddress, } from "@plurnk/plurnk-schemes";
|
|
25
|
+
import DurableStatement from "./DurableStatement.js";
|
|
26
|
+
// SPEC {§scheme-surface}: writer must be in target scheme's manifest.writableBy.
|
|
19
27
|
// OPEN/FOLD/READ/FIND are not gated — they curate the log or read, never mutating an entry.
|
|
20
28
|
const MUTATING_OPS = new Set(["EDIT", "SEND", "COPY", "MOVE", "EXEC", "KILL", "FORK", "WORK"]);
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
return path.raw; // regex source — parens are syntax, never encoded
|
|
24
|
-
return decodePathParens(path.kind === "url" ? path.pathname : path.raw); // #239 item 4
|
|
25
|
-
};
|
|
26
|
-
// Op dispatch (§op-methods-op-dispatch): gates (writableBy, loop flags), the
|
|
27
|
-
// engine-owned op orchestrations (COPY/MOVE/KILL/SEND/READ-fanout), scheme
|
|
29
|
+
// Op dispatch ({§op-methods-op-dispatch}): gates (writableBy, loop flags), the
|
|
30
|
+
// engine-owned op orchestrations (COPY/MOVE/KILL/SEND), scheme
|
|
28
31
|
// routing, the durable log write, and the proposal pause.
|
|
29
32
|
class Dispatcher {
|
|
33
|
+
static #failure(code, status, detail, fields = {}, extensions = {}) {
|
|
34
|
+
return Results.failure("engine:dispatcher", code, status, detail, fields, extensions);
|
|
35
|
+
}
|
|
36
|
+
static #statusResult(status, code, detail, fields = {}) {
|
|
37
|
+
return status >= 400
|
|
38
|
+
? _a.#failure(code, status, detail, fields, { retryable: false })
|
|
39
|
+
: { ...fields, status };
|
|
40
|
+
}
|
|
30
41
|
#db;
|
|
31
42
|
#schemes;
|
|
32
43
|
#mimetypes;
|
|
33
44
|
#tokenize;
|
|
34
|
-
#
|
|
45
|
+
#notices;
|
|
35
46
|
#proposals;
|
|
36
47
|
// Boot-discovered runtime executors, late-injected on Engine — thunked.
|
|
37
48
|
#executors;
|
|
@@ -40,30 +51,33 @@ class Dispatcher {
|
|
|
40
51
|
#streamEventNotify;
|
|
41
52
|
#wakeWorkerNotify;
|
|
42
53
|
#injectWorker;
|
|
54
|
+
#branchWorker;
|
|
55
|
+
#branchCompletionGate;
|
|
43
56
|
#cancelWorker;
|
|
44
57
|
#cancelDescendants;
|
|
45
|
-
// §send-premature-terminate/[
|
|
58
|
+
// {§send-premature-terminate}/SEND[202]<T> — the engine-owned park-deadline registry (loopId → seconds;
|
|
46
59
|
// -1 = indefinite). The dispatcher WRITES at park; the daemon's drain park-exit consumes.
|
|
47
60
|
#parkDeadlines;
|
|
48
61
|
#searchGate;
|
|
49
|
-
//
|
|
50
|
-
// SEND[102] parks on it (blocking join). Engine-owned, twin of #parkDeadlines.
|
|
62
|
+
// Per-turn running-worker READ obligations. {§join-blocking-collect}
|
|
51
63
|
#joinTargets;
|
|
52
64
|
#liveSubscriptions;
|
|
53
65
|
#lifecycle;
|
|
54
66
|
#preparedEdits = new WeakMap();
|
|
55
|
-
constructor({ db, schemes, mimetypes, tokenize,
|
|
67
|
+
constructor({ db, schemes, mimetypes, tokenize, notices, proposals, executors, loopSignal, streamEventNotify, wakeWorkerNotify, injectWorker, branchWorker, branchCompletionGate, cancelWorker, cancelDescendants, searchGate, parkDeadlines, joinTargets, liveSubscriptions }) {
|
|
56
68
|
this.#db = db;
|
|
57
69
|
this.#schemes = schemes;
|
|
58
70
|
this.#mimetypes = mimetypes;
|
|
59
71
|
this.#tokenize = tokenize;
|
|
60
|
-
this.#
|
|
72
|
+
this.#notices = notices;
|
|
61
73
|
this.#proposals = proposals;
|
|
62
74
|
this.#executors = executors;
|
|
63
75
|
this.#loopSignal = loopSignal;
|
|
64
76
|
this.#streamEventNotify = streamEventNotify;
|
|
65
77
|
this.#wakeWorkerNotify = wakeWorkerNotify;
|
|
66
78
|
this.#injectWorker = injectWorker;
|
|
79
|
+
this.#branchWorker = branchWorker;
|
|
80
|
+
this.#branchCompletionGate = branchCompletionGate;
|
|
67
81
|
this.#cancelWorker = cancelWorker;
|
|
68
82
|
this.#cancelDescendants = cancelDescendants;
|
|
69
83
|
this.#parkDeadlines = parkDeadlines ?? new Map();
|
|
@@ -83,39 +97,87 @@ class Dispatcher {
|
|
|
83
97
|
const handlerCtx = this.#handlerContext(scheme, ctx);
|
|
84
98
|
return this.#schemes.manifestFor(scheme)?.category === "data" ? handlerCtx : null;
|
|
85
99
|
}
|
|
86
|
-
|
|
100
|
+
#coreCrud(scheme) {
|
|
87
101
|
const handler = this.#schemes.get(scheme);
|
|
102
|
+
return handler instanceof CoreSchemeAdapterBase
|
|
103
|
+
? handler
|
|
104
|
+
: undefined;
|
|
105
|
+
}
|
|
106
|
+
async #readEntry(scheme, pathname, ctx) {
|
|
107
|
+
const handler = this.#coreCrud(scheme);
|
|
88
108
|
const handlerCtx = this.#entryContext(scheme, ctx);
|
|
89
|
-
if (typeof handler?.readEntry === "function" && handlerCtx !== null)
|
|
90
|
-
return handler.readEntry(pathname, handlerCtx);
|
|
109
|
+
if (typeof handler?.readEntry === "function" && handlerCtx !== null) {
|
|
110
|
+
return Results.assert(await handler.readEntry(pathname, handlerCtx));
|
|
111
|
+
}
|
|
91
112
|
const caps = handlerCtx?.entries;
|
|
92
|
-
if (caps === undefined)
|
|
93
|
-
return
|
|
94
|
-
|
|
95
|
-
|
|
113
|
+
if (caps === undefined) {
|
|
114
|
+
return _a.#failure("entry-read-not-implemented", 501, `The '${scheme}' scheme does not provide entry reads.`, { entry: null }, {
|
|
115
|
+
stage: "entry-read",
|
|
116
|
+
scheme,
|
|
117
|
+
target: renderAddress(scheme, pathname),
|
|
118
|
+
retryable: false,
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
const result = Results.assert(await caps.read(pathname, scheme === "prompt" ? "worker" : "commons"));
|
|
122
|
+
return Results.assert({
|
|
123
|
+
...result,
|
|
96
124
|
status: result.status,
|
|
97
125
|
entry: result.entry === null
|
|
98
126
|
? null
|
|
99
127
|
: { channels: { ...result.entry.channels }, tags: [...result.entry.tags] },
|
|
100
|
-
};
|
|
128
|
+
});
|
|
101
129
|
}
|
|
102
130
|
async #writeEntry(scheme, pathname, entry, ctx) {
|
|
103
|
-
const handler = this.#
|
|
131
|
+
const handler = this.#coreCrud(scheme);
|
|
104
132
|
const handlerCtx = this.#entryContext(scheme, ctx);
|
|
105
|
-
if (typeof handler?.writeEntry === "function" && handlerCtx !== null)
|
|
106
|
-
return handler.writeEntry(pathname, entry, handlerCtx);
|
|
133
|
+
if (typeof handler?.writeEntry === "function" && handlerCtx !== null) {
|
|
134
|
+
return Results.assert(await handler.writeEntry(pathname, entry, handlerCtx));
|
|
135
|
+
}
|
|
107
136
|
const caps = handlerCtx?.entries;
|
|
108
|
-
if (caps === undefined)
|
|
109
|
-
return
|
|
110
|
-
|
|
137
|
+
if (caps === undefined) {
|
|
138
|
+
return _a.#failure("entry-write-not-implemented", 501, `The '${scheme}' scheme does not provide entry writes.`, { created: false, entryId: null }, {
|
|
139
|
+
stage: "entry-write",
|
|
140
|
+
scheme,
|
|
141
|
+
target: renderAddress(scheme, pathname),
|
|
142
|
+
retryable: false,
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
return Results.assert(await caps.write(pathname, entry, scheme === "prompt" ? "worker" : "commons"));
|
|
111
146
|
}
|
|
112
147
|
async #deleteEntry(scheme, pathname, ctx) {
|
|
113
|
-
const handler = this.#
|
|
148
|
+
const handler = this.#coreCrud(scheme);
|
|
114
149
|
const handlerCtx = this.#entryContext(scheme, ctx);
|
|
115
|
-
if (typeof handler?.deleteEntry === "function" && handlerCtx !== null)
|
|
116
|
-
return handler.deleteEntry(pathname, handlerCtx);
|
|
150
|
+
if (typeof handler?.deleteEntry === "function" && handlerCtx !== null) {
|
|
151
|
+
return Results.assert(await handler.deleteEntry(pathname, handlerCtx));
|
|
152
|
+
}
|
|
117
153
|
const caps = handlerCtx?.entries;
|
|
118
|
-
|
|
154
|
+
if (caps === undefined) {
|
|
155
|
+
return _a.#failure("entry-delete-not-implemented", 501, `The '${scheme}' scheme does not provide entry deletion.`, {}, {
|
|
156
|
+
stage: "entry-delete",
|
|
157
|
+
scheme,
|
|
158
|
+
target: renderAddress(scheme, pathname),
|
|
159
|
+
retryable: false,
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
return Results.assert(await caps.delete(pathname, scheme === "prompt" ? "worker" : "commons"));
|
|
163
|
+
}
|
|
164
|
+
async #deleteChannel(scheme, pathname, channel, ctx) {
|
|
165
|
+
const handler = this.#coreCrud(scheme);
|
|
166
|
+
const handlerCtx = this.#entryContext(scheme, ctx);
|
|
167
|
+
if (typeof handler?.deleteChannel === "function" && handlerCtx !== null) {
|
|
168
|
+
return Results.assert(await handler.deleteChannel(pathname, channel, handlerCtx));
|
|
169
|
+
}
|
|
170
|
+
const caps = handlerCtx?.entries;
|
|
171
|
+
if (caps === undefined) {
|
|
172
|
+
return _a.#failure("channel-delete-not-implemented", 501, `The '${scheme}' scheme does not provide channel deletion.`, {}, {
|
|
173
|
+
stage: "channel-delete",
|
|
174
|
+
scheme,
|
|
175
|
+
target: renderAddress(scheme, pathname),
|
|
176
|
+
channel,
|
|
177
|
+
retryable: false,
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
return Results.assert(await caps.delete(pathname, scheme === "prompt" ? "worker" : "commons", channel));
|
|
119
181
|
}
|
|
120
182
|
async #workspaceRoot(workspaceId) {
|
|
121
183
|
if (this.#rootCache.has(workspaceId))
|
|
@@ -126,9 +188,8 @@ class Dispatcher {
|
|
|
126
188
|
return root;
|
|
127
189
|
}
|
|
128
190
|
// {§fs-answer-in-canon} — a file-class target's engine-authored address COLUMNS carry
|
|
129
|
-
// the canonical key;
|
|
130
|
-
//
|
|
131
|
-
// form: the row must still faithfully exist for the op that happened.
|
|
191
|
+
// the canonical key; tx keeps the operation spelling after the one durable request-evidence
|
|
192
|
+
// projection. An un-canonicalizable spelling keeps its raw form.
|
|
132
193
|
async #canonColumns(target, workspaceId) {
|
|
133
194
|
if (target.scheme !== null || target.pathname === null)
|
|
134
195
|
return;
|
|
@@ -172,37 +233,64 @@ class Dispatcher {
|
|
|
172
233
|
initial = denial;
|
|
173
234
|
}
|
|
174
235
|
else if (schemeName === null) {
|
|
175
|
-
initial = {
|
|
236
|
+
initial = _a.#failure("target-required", 400, "EDIT requires a target scheme.", {}, { retryable: false });
|
|
176
237
|
}
|
|
177
238
|
else {
|
|
178
239
|
const handler = this.#schemes.get(schemeName);
|
|
179
240
|
const method = handler?.editBatch;
|
|
180
241
|
const manifest = this.#schemes.manifestFor(schemeName);
|
|
181
242
|
if (typeof method !== "function" || manifest === undefined) {
|
|
182
|
-
initial = {
|
|
243
|
+
initial = _a.#failure("operation-not-implemented", 501, `Scheme '${schemeName}' does not implement EDIT batches.`, {}, {
|
|
244
|
+
scheme: schemeName,
|
|
245
|
+
operation: "EDIT",
|
|
246
|
+
retryable: false,
|
|
247
|
+
});
|
|
183
248
|
}
|
|
184
249
|
else {
|
|
185
250
|
try {
|
|
186
251
|
const addressedScheme = first.target?.kind === "url" ? first.target.scheme : schemeName;
|
|
187
|
-
|
|
252
|
+
const publishedChannel = first.target?.kind === "url"
|
|
253
|
+
? first.target.fragment ?? manifest.defaultChannel
|
|
254
|
+
: manifest.defaultChannel;
|
|
255
|
+
initial = Results.assert(await method.call(handler, group, new SchemeCtxImpl(schemeCtx, addressedScheme ?? schemeName, manifest, this.#liveSubscriptions, { publishedChannel })));
|
|
188
256
|
}
|
|
189
257
|
catch (err) {
|
|
190
|
-
|
|
258
|
+
if (err instanceof InvalidOperationResultError)
|
|
259
|
+
throw err;
|
|
260
|
+
console.error(`Scheme '${schemeName}' EDIT batch threw outside its operation result contract:`, err);
|
|
261
|
+
initial = _a.#failure("scheme-handler-threw", 500, `The '${schemeName}' scheme did not produce an EDIT result.`, {}, {
|
|
262
|
+
stage: "scheme-dispatch",
|
|
263
|
+
scheme: schemeName,
|
|
264
|
+
operation: "EDIT",
|
|
265
|
+
});
|
|
191
266
|
}
|
|
192
267
|
}
|
|
193
268
|
}
|
|
194
269
|
let resolveSettled;
|
|
195
270
|
const settled = new Promise((resolve) => { resolveSettled = resolve; });
|
|
196
|
-
const
|
|
197
|
-
|
|
198
|
-
|
|
271
|
+
const candidate = initial.editReceipt;
|
|
272
|
+
const expectedNormalizations = group.filter(({ lineMarker }) => lineMarker?.marks.length === 3).length;
|
|
273
|
+
if (initial.status < 400
|
|
274
|
+
&& (initial.scopeNormalizations?.length ?? 0) !== expectedNormalizations) {
|
|
275
|
+
throw new InvalidOperationResultError(`EDIT batch normalized ${initial.scopeNormalizations?.length ?? 0} scope(s), expected ${expectedNormalizations}.`);
|
|
276
|
+
}
|
|
277
|
+
const batch = {
|
|
199
278
|
initial,
|
|
200
279
|
settled,
|
|
280
|
+
aggregate: candidate === undefined || candidate === null
|
|
281
|
+
? undefined
|
|
282
|
+
: assertEditBatchReceipt(candidate),
|
|
201
283
|
settle: resolveSettled,
|
|
202
284
|
};
|
|
203
|
-
|
|
204
|
-
for (const [index, statement] of group.
|
|
205
|
-
|
|
285
|
+
let normalizationIndex = 0;
|
|
286
|
+
for (const [index, statement] of group.entries()) {
|
|
287
|
+
const ownsNormalization = statement.lineMarker?.marks.length === 3 && initial.status < 400;
|
|
288
|
+
this.#preparedEdits.set(statement, {
|
|
289
|
+
first: index === 0,
|
|
290
|
+
index,
|
|
291
|
+
normalizationIndex: ownsNormalization ? normalizationIndex++ : null,
|
|
292
|
+
batch,
|
|
293
|
+
});
|
|
206
294
|
}
|
|
207
295
|
}
|
|
208
296
|
}
|
|
@@ -210,13 +298,17 @@ class Dispatcher {
|
|
|
210
298
|
const result = await this.#dispatchOne(context);
|
|
211
299
|
if (context.statement.op === "EDIT") {
|
|
212
300
|
const prepared = this.#preparedEdits.get(context.statement);
|
|
213
|
-
if (prepared?.first === true)
|
|
214
|
-
prepared.
|
|
301
|
+
if (prepared?.first === true) {
|
|
302
|
+
const normalizations = prepared.batch.initial.scopeNormalizations;
|
|
303
|
+
prepared.batch.settle(normalizations === undefined
|
|
304
|
+
? result
|
|
305
|
+
: Results.assert({ ...result, scopeNormalizations: normalizations }));
|
|
306
|
+
}
|
|
215
307
|
}
|
|
216
308
|
return result;
|
|
217
309
|
}
|
|
218
310
|
async #dispatchOne(context) {
|
|
219
|
-
const { statement, workspaceId, workerId, loopId, turnId, sequence, origin, onDispatch,
|
|
311
|
+
const { statement, workspaceId, workerId, loopId, turnId, sequence, origin, onDispatch, } = context;
|
|
220
312
|
const schemeCtx = this.#buildSchemeCtx({ workspaceId, workerId, loopId, turnId, origin });
|
|
221
313
|
let result;
|
|
222
314
|
let denial = this.#checkWritable(statement, origin);
|
|
@@ -225,14 +317,8 @@ class Dispatcher {
|
|
|
225
317
|
if (denial !== null) {
|
|
226
318
|
result = denial;
|
|
227
319
|
}
|
|
228
|
-
else if (this.#readFansOut(statement)) {
|
|
229
|
-
// READ honors FIND: a glob/folder scope or a matcher fans out to one log row per MATCH
|
|
230
|
-
// (its own writeLogs), returning early. A READ never proposes, so it bypasses the
|
|
231
|
-
// single-row path below. A bare entry, body-less, falls through to the direct read. #286
|
|
232
|
-
return await this.#handleReadFanout(statement, schemeCtx, { workerId, loopId, turnId, sequence, origin, onDispatch });
|
|
233
|
-
}
|
|
234
320
|
else {
|
|
235
|
-
//
|
|
321
|
+
// {§scheme-surface-exception-500} Scheme-handler
|
|
236
322
|
// exceptions become the action-entry's outcome (status 500), not a
|
|
237
323
|
// thrown bubble. The log_entry is the durable record; engine never
|
|
238
324
|
// skips it. Logging failures (#writeLog throws) are NOT caught —
|
|
@@ -241,35 +327,38 @@ class Dispatcher {
|
|
|
241
327
|
if (statement.op === "EDIT") {
|
|
242
328
|
const prepared = this.#preparedEdits.get(statement);
|
|
243
329
|
if (prepared === undefined) {
|
|
244
|
-
|
|
330
|
+
throw new InvalidOperationResultError("EDIT reached dispatch without a prepared resource batch.");
|
|
245
331
|
}
|
|
246
332
|
else {
|
|
247
|
-
const settled = prepared.first
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
333
|
+
const settled = prepared.first
|
|
334
|
+
? prepared.batch.initial
|
|
335
|
+
: await prepared.batch.settled;
|
|
336
|
+
const normalization = prepared.normalizationIndex === null
|
|
337
|
+
? undefined
|
|
338
|
+
: settled.scopeNormalizations?.[prepared.normalizationIndex];
|
|
339
|
+
const { scopeNormalizations: _scopeNormalizations, ...projectedSettlement } = settled;
|
|
340
|
+
const statementResult = normalization === undefined
|
|
341
|
+
? projectedSettlement
|
|
342
|
+
: { ...projectedSettlement, scopeNormalizations: [normalization] };
|
|
343
|
+
const aggregate = prepared.batch.aggregate;
|
|
344
|
+
if (aggregate !== undefined) {
|
|
345
|
+
const receipt = assertEditBatchReceipt(aggregate);
|
|
346
|
+
const { editReceipt: _editReceipt, ...withoutAggregate } = statementResult;
|
|
255
347
|
result = {
|
|
256
348
|
...withoutAggregate,
|
|
257
|
-
receipt:
|
|
258
|
-
revision: receipt.revision,
|
|
259
|
-
unit: receipt.unit,
|
|
260
|
-
before: receipt.before,
|
|
261
|
-
after: receipt.after,
|
|
262
|
-
effect,
|
|
263
|
-
},
|
|
349
|
+
receipt: projectEditReceipt(receipt, prepared.index),
|
|
264
350
|
};
|
|
265
351
|
}
|
|
266
352
|
else {
|
|
267
|
-
result =
|
|
353
|
+
result = statementResult;
|
|
268
354
|
}
|
|
269
355
|
}
|
|
270
356
|
}
|
|
271
357
|
else if (statement.op === "SEND" && statement.target === null) {
|
|
272
|
-
result = await this.#handleSendBroadcast(statement, { workspaceId, workerId, loopId, turnId,
|
|
358
|
+
result = await this.#handleSendBroadcast(statement, { workspaceId, workerId, loopId, turnId, sequence });
|
|
359
|
+
}
|
|
360
|
+
else if (statement.op === "OPEN" || statement.op === "FOLD") {
|
|
361
|
+
result = await this.#runLogCuration(statement, schemeCtx);
|
|
273
362
|
}
|
|
274
363
|
else if (statement.op === "FORK" || statement.op === "WORK") {
|
|
275
364
|
result = await this.#handleWorkerControl(statement, schemeCtx);
|
|
@@ -294,26 +383,31 @@ class Dispatcher {
|
|
|
294
383
|
result = await this.#gatedExec(statement, schemeCtx, loopId, turnId);
|
|
295
384
|
}
|
|
296
385
|
else {
|
|
297
|
-
result = await this.#run(schemeNameOf(statement.target), statement, schemeCtx); // §op-methods-op-dispatch
|
|
386
|
+
result = await this.#run(schemeNameOf(statement.target), statement, schemeCtx); // {§op-methods-op-dispatch}
|
|
298
387
|
}
|
|
299
388
|
}
|
|
300
|
-
catch (err) { // a scheme exception becomes the op's 500 outcome — §scheme-surface-exception-500
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
};
|
|
389
|
+
catch (err) { // a scheme exception becomes the op's 500 outcome — {§scheme-surface-exception-500}
|
|
390
|
+
if (err instanceof InvalidOperationResultError)
|
|
391
|
+
throw err;
|
|
392
|
+
const scheme = schemeNameOf(statement.target);
|
|
393
|
+
console.error(`Scheme '${scheme ?? "unknown"}' ${statement.op} threw outside its operation result contract:`, err);
|
|
394
|
+
result = err instanceof OperationFailureError
|
|
395
|
+
? err.result
|
|
396
|
+
: _a.#failure("scheme-handler-threw", 500, `The '${scheme ?? "unknown"}' scheme did not produce a ${statement.op} result.`, {}, {
|
|
397
|
+
stage: "scheme-dispatch",
|
|
398
|
+
scheme,
|
|
399
|
+
operation: statement.op,
|
|
400
|
+
});
|
|
305
401
|
}
|
|
306
402
|
}
|
|
307
|
-
// §fold-open-meta-operations — OPEN/FOLD
|
|
308
|
-
//
|
|
309
|
-
//
|
|
310
|
-
//
|
|
311
|
-
// §join-blocking-collect (#354) — Run.read on a still-running child returns an awaitWorker signal;
|
|
312
|
-
// arm the join so THIS turn's bare SEND[102] parks (the blocking collect) instead of spinning.
|
|
403
|
+
// {§fold-open-meta-operations} — persist OPEN/FOLD for forensics;
|
|
404
|
+
// packet rendering suppresses their successful receipts.
|
|
405
|
+
// A running-worker READ arms this turn's blocking collect.
|
|
406
|
+
// {§join-blocking-collect}
|
|
313
407
|
if (typeof result.awaitWorker === "string")
|
|
314
408
|
this.#joinTargets.add(loopId);
|
|
315
409
|
const logEntryId = await this.#writeLog({ statement, result, workspaceId, workerId, loopId, turnId, sequence, origin });
|
|
316
|
-
// §search-gate — register successful searches AFTER #writeLog stamps the runtime
|
|
410
|
+
// {§search-gate} — register successful searches AFTER #writeLog stamps the runtime
|
|
317
411
|
// entry's coordinate onto result.attrs.pathname (the gate's dedup serves from it).
|
|
318
412
|
if (statement.op === "EXEC" && result.status < 400) {
|
|
319
413
|
const rt = ("signal" in statement && typeof statement.signal === "string" && statement.signal.length > 0) ? statement.signal : "sh";
|
|
@@ -323,19 +417,36 @@ class Dispatcher {
|
|
|
323
417
|
this.#searchGate?.registerPending(loopId, turnId, rt, cmd, attrPath);
|
|
324
418
|
}
|
|
325
419
|
onDispatch?.(logEntryId);
|
|
326
|
-
// Proposal lifecycle (SPEC.md §engine-rails + §methods
|
|
420
|
+
// Proposal lifecycle (SPEC.md {§engine-rails} + {§methods-proposal-resolve}; {§proposal-202-pauses}). When a
|
|
327
421
|
// side-effecting op returns status 202 (a broadcast SEND[202] park is model
|
|
328
|
-
// speech, not a proposal — #isProposal
|
|
422
|
+
// speech, not a proposal — #isProposal), the entry is written
|
|
329
423
|
// state='proposed'; dispatch then PAUSES on a per-entry waiter until
|
|
330
|
-
// resolution arrives via Engine.resolveProposal (from
|
|
331
|
-
//
|
|
424
|
+
// resolution arrives via Engine.resolveProposal (from a client-interface resume,
|
|
425
|
+
// core-owned disposition, or timeout). The post-resolution status replaces 202 in the
|
|
332
426
|
// result the caller sees, so runTurn never branches on a pending state.
|
|
333
427
|
if (_a.#isProposal(statement, result)) {
|
|
334
|
-
// Effect-gated auto-run (read/pure runtimes,
|
|
335
|
-
//
|
|
336
|
-
//
|
|
337
|
-
|
|
338
|
-
|
|
428
|
+
// Effect-gated auto-run (read/pure runtimes, {§exec-readpure-ungated}):
|
|
429
|
+
// EXEC stores its one canonical effect fact before admission. Reuse
|
|
430
|
+
// that exact fact here; no human gate or loop/proposal notification.
|
|
431
|
+
const effect = result.attrs?.effect;
|
|
432
|
+
let autoAccept = false;
|
|
433
|
+
if (statement.op === "EXEC") {
|
|
434
|
+
if (!EffectPolicy.isEffect(effect)) {
|
|
435
|
+
throw new InvalidOperationResultError("EXEC proposal omitted its canonical effect fact.");
|
|
436
|
+
}
|
|
437
|
+
autoAccept = EffectPolicy.decide(effect) === "auto";
|
|
438
|
+
}
|
|
439
|
+
if (autoAccept) {
|
|
440
|
+
const initialSettlement = await this.#proposals.workerApply(statement, result, { decision: "accept" }, { workspaceId, workerId, loopId, turnId });
|
|
441
|
+
const settlement = _a.#settleProposalEffects(result, initialSettlement);
|
|
442
|
+
const effective = await this.#settleMoveProposal({
|
|
443
|
+
statement,
|
|
444
|
+
result,
|
|
445
|
+
settlement,
|
|
446
|
+
ctx: schemeCtx,
|
|
447
|
+
ids: { workspaceId, workerId, loopId, turnId },
|
|
448
|
+
});
|
|
449
|
+
this.#recordEditSettlement(statement, effective);
|
|
339
450
|
return this.#proposals.applyResolution(logEntryId, effective);
|
|
340
451
|
}
|
|
341
452
|
// Register the resolution waiter SYNCHRONOUSLY before any await
|
|
@@ -343,56 +454,43 @@ class Dispatcher {
|
|
|
343
454
|
// awaits the onDispatch callback and immediately resolves) must
|
|
344
455
|
// find the waiter registered — adding an await between insert
|
|
345
456
|
// and waiter-registration would open a race window.
|
|
346
|
-
|
|
347
|
-
//
|
|
348
|
-
//
|
|
349
|
-
//
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
attrs: (result.attrs ?? {}),
|
|
362
|
-
flags,
|
|
363
|
-
staleClobberRisk: diverged !== undefined,
|
|
364
|
-
};
|
|
365
|
-
this.#proposals.notifyPending(event);
|
|
457
|
+
// Core derives one validated projection from the durable row for both
|
|
458
|
+
// this live event and reconnect discovery ({§proposal-projection}). Its
|
|
459
|
+
// disposition is also the one automatic settlement decision: policy is
|
|
460
|
+
// not an observer and cannot silently degrade into client ownership.
|
|
461
|
+
let resolutionPromise;
|
|
462
|
+
try {
|
|
463
|
+
resolutionPromise = this.#proposals.awaitResolution(logEntryId);
|
|
464
|
+
const event = await this.#proposals.pending(logEntryId);
|
|
465
|
+
this.#proposals.settleOwned(event);
|
|
466
|
+
this.#proposals.notifyPending(event);
|
|
467
|
+
}
|
|
468
|
+
catch (cause) {
|
|
469
|
+
await this.#proposals.failPreparation(logEntryId, cause);
|
|
470
|
+
throw cause;
|
|
471
|
+
}
|
|
366
472
|
const resolution = await resolutionPromise;
|
|
367
473
|
// Run the scheme's applyResolution hook on accept (writes the
|
|
368
|
-
// file, spawns the process, etc.).
|
|
369
|
-
//
|
|
370
|
-
//
|
|
371
|
-
|
|
372
|
-
const
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
// A host-effecting source-delete PROPOSES (202); the MOVE proposal already gated the whole
|
|
382
|
-
// create+kill, so apply the source-delete now — never raise a second review for one MOVE.
|
|
383
|
-
const sourceCtx = this.#handlerContext(moveSource.scheme, schemeCtx);
|
|
384
|
-
if (del.status === 202 && del.attrs !== undefined && typeof srcHandler.applyResolution === "function" && sourceCtx !== null)
|
|
385
|
-
await srcHandler.applyResolution({ attrs: del.attrs }, sourceCtx);
|
|
386
|
-
}
|
|
387
|
-
}
|
|
388
|
-
}
|
|
474
|
+
// file, spawns the process, etc.). Its operation result is
|
|
475
|
+
// preserved: an apply failure keeps its original status and
|
|
476
|
+
// Problem Details instead of masquerading as a client rejection.
|
|
477
|
+
const initialSettlement = await this.#proposals.workerApply(statement, result, resolution, { workspaceId, workerId, loopId, turnId });
|
|
478
|
+
const settlement = _a.#settleProposalEffects(result, initialSettlement);
|
|
479
|
+
const effective = await this.#settleMoveProposal({
|
|
480
|
+
statement,
|
|
481
|
+
result,
|
|
482
|
+
settlement,
|
|
483
|
+
ctx: schemeCtx,
|
|
484
|
+
ids: { workspaceId, workerId, loopId, turnId },
|
|
485
|
+
});
|
|
486
|
+
this.#recordEditSettlement(statement, effective);
|
|
389
487
|
const post = await this.#proposals.applyResolution(logEntryId, effective);
|
|
390
488
|
return post;
|
|
391
489
|
}
|
|
392
490
|
return result;
|
|
393
491
|
}
|
|
394
|
-
// op
|
|
395
|
-
// log_entries row: the client's
|
|
492
|
+
// {§op-look}: resolve a READ and return its content without writing a
|
|
493
|
+
// log_entries row: the client's out-of-band inspection primitive (LOOK → READ,
|
|
396
494
|
// invisible to the model). READ never mutates and never proposes, so this is
|
|
397
495
|
// dispatch's resolve path minus #writeLog. Runs on the client loop, so the
|
|
398
496
|
// human's inspection is never constrained by a model loop's flags. {§op-look}
|
|
@@ -407,6 +505,150 @@ class Dispatcher {
|
|
|
407
505
|
return denial;
|
|
408
506
|
return this.#run(schemeNameOf(statement.target), statement, schemeCtx);
|
|
409
507
|
}
|
|
508
|
+
// Resolve the client selector through the owning scheme before persistence
|
|
509
|
+
// is consulted. Public schemes choose a semantic owner; core-owned authority
|
|
510
|
+
// schemes may return the already-authorized principal key.
|
|
511
|
+
async resolveEntryAddress(context) {
|
|
512
|
+
const { target, workspaceId, workerId } = context;
|
|
513
|
+
const routedScheme = schemeNameOf(target);
|
|
514
|
+
if (routedScheme === null)
|
|
515
|
+
return null;
|
|
516
|
+
const handler = this.#schemes.get(routedScheme);
|
|
517
|
+
const manifest = this.#schemes.manifestFor(routedScheme);
|
|
518
|
+
if (handler === undefined || manifest?.category !== "data")
|
|
519
|
+
return null;
|
|
520
|
+
const addressedScheme = target.kind === "url" ? target.scheme : routedScheme;
|
|
521
|
+
const coreCtx = this.#buildSchemeCtx({
|
|
522
|
+
workspaceId,
|
|
523
|
+
workerId,
|
|
524
|
+
loopId: 0,
|
|
525
|
+
turnId: 0,
|
|
526
|
+
origin: "client",
|
|
527
|
+
});
|
|
528
|
+
const schemeCtx = new SchemeCtxImpl(coreCtx, addressedScheme, manifest, this.#liveSubscriptions);
|
|
529
|
+
const resolved = await this.#resolveDataEntryAddress({
|
|
530
|
+
target,
|
|
531
|
+
routedScheme,
|
|
532
|
+
handler,
|
|
533
|
+
manifest,
|
|
534
|
+
ctx: coreCtx,
|
|
535
|
+
schemeCtx,
|
|
536
|
+
});
|
|
537
|
+
if (resolved.address === null)
|
|
538
|
+
return null;
|
|
539
|
+
const rendered = target.kind === "url"
|
|
540
|
+
? renderTarget({ ...target, fragment: null })
|
|
541
|
+
: renderTarget({ scheme: null, pathname: target.raw, fragment: null });
|
|
542
|
+
if (rendered === null)
|
|
543
|
+
throw new TypeError("Resolved entry target did not render.");
|
|
544
|
+
return {
|
|
545
|
+
ownerId: resolved.address.ownerId,
|
|
546
|
+
scheme: resolved.address.scheme,
|
|
547
|
+
pathname: resolved.address.pathname,
|
|
548
|
+
target: rendered,
|
|
549
|
+
};
|
|
550
|
+
}
|
|
551
|
+
async #resolveDataEntryAddress({ target, routedScheme, handler, manifest, ctx, schemeCtx, }) {
|
|
552
|
+
const addressedScheme = target.kind === "url" ? target.scheme : routedScheme;
|
|
553
|
+
const identityTarget = target.kind === "url"
|
|
554
|
+
? {
|
|
555
|
+
...target,
|
|
556
|
+
pathname: PathSyntax.decodeParens(target.pathname),
|
|
557
|
+
fragment: null,
|
|
558
|
+
}
|
|
559
|
+
: { ...target, raw: PathSyntax.decodeParens(target.raw) };
|
|
560
|
+
const resolved = handler.resolveEntryAddress === undefined
|
|
561
|
+
? { pathname: entryPathnameOf(identityTarget), owner: "commons" }
|
|
562
|
+
: await handler.resolveEntryAddress(identityTarget, schemeCtx);
|
|
563
|
+
if (resolved === null)
|
|
564
|
+
return { address: null, result: null };
|
|
565
|
+
if ("status" in resolved) {
|
|
566
|
+
const result = Results.assert(resolved);
|
|
567
|
+
if (result.status < 300) {
|
|
568
|
+
throw new InvalidOperationResultError(`Scheme '${routedScheme}' returned a successful operation result instead of an entry address.`);
|
|
569
|
+
}
|
|
570
|
+
return { address: null, result };
|
|
571
|
+
}
|
|
572
|
+
if (typeof resolved.pathname !== "string") {
|
|
573
|
+
throw new TypeError(`Scheme '${routedScheme}' returned an invalid entry pathname.`);
|
|
574
|
+
}
|
|
575
|
+
let ownerId;
|
|
576
|
+
if ("ownerId" in resolved) {
|
|
577
|
+
if (!(handler instanceof CoreSchemeAdapterBase)) {
|
|
578
|
+
throw new TypeError(`Scheme '${routedScheme}' returned a core-only entry owner id.`);
|
|
579
|
+
}
|
|
580
|
+
ownerId = resolved.ownerId;
|
|
581
|
+
}
|
|
582
|
+
else if (resolved.owner === "worker") {
|
|
583
|
+
ownerId = ctx.workerId;
|
|
584
|
+
}
|
|
585
|
+
else if (resolved.owner === "commons") {
|
|
586
|
+
ownerId = await Owner.commonsId(this.#db, ctx.workspaceId);
|
|
587
|
+
}
|
|
588
|
+
else {
|
|
589
|
+
throw new TypeError(`Scheme '${routedScheme}' returned an invalid entry owner.`);
|
|
590
|
+
}
|
|
591
|
+
if (!Number.isSafeInteger(ownerId) || ownerId < 1) {
|
|
592
|
+
throw new TypeError(`Scheme '${routedScheme}' returned an invalid entry owner id.`);
|
|
593
|
+
}
|
|
594
|
+
return {
|
|
595
|
+
address: {
|
|
596
|
+
ownerId,
|
|
597
|
+
scheme: manifest.storedScheme ?? addressedScheme,
|
|
598
|
+
pathname: resolved.pathname,
|
|
599
|
+
},
|
|
600
|
+
result: null,
|
|
601
|
+
};
|
|
602
|
+
}
|
|
603
|
+
async #prepareDataRepresentation({ target, routedScheme, handler, manifest, ctx, schemeCtx, publishedChannel, }) {
|
|
604
|
+
const resolved = await this.#resolveDataEntryAddress({
|
|
605
|
+
target,
|
|
606
|
+
routedScheme,
|
|
607
|
+
handler,
|
|
608
|
+
manifest,
|
|
609
|
+
ctx,
|
|
610
|
+
schemeCtx,
|
|
611
|
+
});
|
|
612
|
+
if (resolved.address === null
|
|
613
|
+
|| resolved.result !== null
|
|
614
|
+
|| typeof handler.prepareRepresentation !== "function") {
|
|
615
|
+
return resolved;
|
|
616
|
+
}
|
|
617
|
+
const address = resolved.address;
|
|
618
|
+
const selectionNeutralTarget = target.kind === "url"
|
|
619
|
+
? {
|
|
620
|
+
...target,
|
|
621
|
+
raw: renderTarget({ ...target, fragment: null }) ?? target.raw,
|
|
622
|
+
fragment: null,
|
|
623
|
+
}
|
|
624
|
+
: target;
|
|
625
|
+
const preparationCtx = new SchemeCtxImpl(ctx, target.kind === "url" ? target.scheme : routedScheme, manifest, this.#liveSubscriptions, {
|
|
626
|
+
ownerId: address.ownerId,
|
|
627
|
+
publishedChannel,
|
|
628
|
+
});
|
|
629
|
+
const prepared = Results.assertRepresentationPreparation(await handler.prepareRepresentation({
|
|
630
|
+
target: selectionNeutralTarget,
|
|
631
|
+
pathname: address.pathname,
|
|
632
|
+
}, preparationCtx));
|
|
633
|
+
return {
|
|
634
|
+
address,
|
|
635
|
+
result: prepared.status === 200 ? null : prepared,
|
|
636
|
+
};
|
|
637
|
+
}
|
|
638
|
+
// An accepted EXEC reads a non-file source through the same registered
|
|
639
|
+
// handler and addressed context as an authored READ. {§exec-target-routing}
|
|
640
|
+
async readExecSource(statement, ctx) {
|
|
641
|
+
const schemeName = schemeNameOf(statement.target);
|
|
642
|
+
const manifest = schemeName === null ? undefined : this.#schemes.manifestFor(schemeName);
|
|
643
|
+
if (manifest !== undefined && manifest.category !== "data") {
|
|
644
|
+
return _a.#failure("exec-source-not-data", 501, `Scheme '${schemeName}' is not a data source for EXEC.`, {}, {
|
|
645
|
+
scheme: schemeName,
|
|
646
|
+
category: manifest.category,
|
|
647
|
+
retryable: false,
|
|
648
|
+
});
|
|
649
|
+
}
|
|
650
|
+
return Results.assertReadResult(await this.#run(schemeName, statement, ctx));
|
|
651
|
+
}
|
|
410
652
|
#buildSchemeCtx(ids) {
|
|
411
653
|
const { workspaceId, workerId, loopId, turnId, origin } = ids;
|
|
412
654
|
return {
|
|
@@ -419,33 +661,16 @@ class Dispatcher {
|
|
|
419
661
|
injectWorker: this.#injectWorker,
|
|
420
662
|
mimetypes: this.#mimetypes,
|
|
421
663
|
tokenize: this.#tokenize,
|
|
422
|
-
|
|
664
|
+
pushNotice: (notice) => this.#notices.push(workspaceId, loopId, notice),
|
|
423
665
|
executors: this.#executors(),
|
|
424
666
|
};
|
|
425
667
|
}
|
|
426
|
-
//
|
|
427
|
-
// missing keys read as their documented defaults. Single read site —
|
|
428
|
-
// ProposalPendingEvent.flags is constructed from this, and listeners
|
|
429
|
-
// (Daemon broadcast, loop auto) share the result.
|
|
430
|
-
async #loadLoopFlags(loopId) {
|
|
431
|
-
const row = await this.#db.engine_get_loop_flags.get({ loop_id: loopId });
|
|
432
|
-
if (row === undefined)
|
|
433
|
-
return DEFAULT_LOOP_FLAGS;
|
|
434
|
-
try {
|
|
435
|
-
const parsed = JSON.parse(row.flags);
|
|
436
|
-
return { ...DEFAULT_LOOP_FLAGS, ...parsed };
|
|
437
|
-
}
|
|
438
|
-
catch {
|
|
439
|
-
return DEFAULT_LOOP_FLAGS;
|
|
440
|
-
}
|
|
441
|
-
}
|
|
442
|
-
// SPEC §scheme-surface: engine rejects writes whose origin is outside the target
|
|
668
|
+
// SPEC {§scheme-surface}: engine rejects writes whose origin is outside the target
|
|
443
669
|
// scheme's manifest.writableBy.
|
|
444
670
|
// - Read-side ops (READ, FIND, OPEN, FOLD) are not gated.
|
|
445
671
|
// - SEND broadcast (path=null) has no target scheme; not gated.
|
|
446
672
|
// - COPY: dst scheme writableBy applies.
|
|
447
673
|
// - MOVE: both src (delete) and dst (write) schemes' writableBy apply.
|
|
448
|
-
// - Schemes without a manifest are not gated (legacy / future allowance).
|
|
449
674
|
#checkWritable(statement, origin) {
|
|
450
675
|
if (!MUTATING_OPS.has(statement.op))
|
|
451
676
|
return null;
|
|
@@ -456,13 +681,13 @@ class Dispatcher {
|
|
|
456
681
|
if (statement.op === "EXEC") {
|
|
457
682
|
return this.#denyIfDisallowed("exec", origin);
|
|
458
683
|
}
|
|
459
|
-
//
|
|
684
|
+
// Worker control (FORK/WORK → worker://<name>, spawn or fork) is gated by worker://'s writableBy — its
|
|
460
685
|
// body is a seed prompt, not a dst path, so the entry-COPY dst-parse below doesn't apply.
|
|
461
|
-
// §machine-processes
|
|
686
|
+
// {§machine-processes}
|
|
462
687
|
if (this.#isWorkerControl(statement))
|
|
463
688
|
return this.#denyIfDisallowed("worker", origin);
|
|
464
689
|
if (statement.op === "COPY" || statement.op === "MOVE") {
|
|
465
|
-
const dst = statement.
|
|
690
|
+
const dst = statement.body?.target ?? null;
|
|
466
691
|
const dstScheme = schemeNameOf(dst);
|
|
467
692
|
const dstDenial = this.#denyIfDisallowed(dstScheme, origin);
|
|
468
693
|
if (dstDenial !== null)
|
|
@@ -480,17 +705,19 @@ class Dispatcher {
|
|
|
480
705
|
const target = schemeNameOf(statement.target);
|
|
481
706
|
return this.#denyIfDisallowed(target, origin);
|
|
482
707
|
}
|
|
483
|
-
// §search-gate
|
|
484
|
-
//
|
|
485
|
-
// strike rail counts the turn failure; the failed-ops gate holds the terminal a turn)
|
|
486
|
-
// carrying the prior ranked digest re-read live — the model sees its results again, the
|
|
487
|
-
// engine never re-fetches, no provenance prose. The per-turn CAP is flood control: 429.
|
|
708
|
+
// {§search-gate} — gate only configured search runtimes; duplicates serve
|
|
709
|
+
// the prior durable digest and the per-turn cap refuses without execution.
|
|
488
710
|
async #gatedExec(statement, ctx, loopId, turnId) {
|
|
489
711
|
const runtime = ("signal" in statement && typeof statement.signal === "string" && statement.signal.length > 0) ? statement.signal : "sh";
|
|
490
712
|
const command = ("body" in statement && typeof statement.body === "string") ? statement.body : "";
|
|
491
713
|
const verdict = this.#searchGate?.check(loopId, turnId, runtime, command) ?? { verdict: "pass" };
|
|
492
714
|
if (verdict.verdict === "capped") {
|
|
493
|
-
return
|
|
715
|
+
return _a.#failure("search-limit-reached", 429, `This turn already used its ${verdict.cap} permitted searches.`, {}, {
|
|
716
|
+
searchLimit: verdict.cap,
|
|
717
|
+
stage: "search-admission",
|
|
718
|
+
recovery: "Continue without another search in this turn.",
|
|
719
|
+
retryable: false,
|
|
720
|
+
});
|
|
494
721
|
}
|
|
495
722
|
if (verdict.verdict === "duplicate") {
|
|
496
723
|
// {§stream-owner-scoped} — the prior ranked digest is the CALLER's own stream entry.
|
|
@@ -501,7 +728,10 @@ class Dispatcher {
|
|
|
501
728
|
results = JSON.parse(raw);
|
|
502
729
|
}
|
|
503
730
|
catch { /* non-JSON results serve verbatim */ }
|
|
504
|
-
return
|
|
731
|
+
return _a.#failure("duplicate-search", 409, "This search already ran in the current loop; the prior results are attached.", { results }, {
|
|
732
|
+
recovery: "Use the attached prior results.",
|
|
733
|
+
retryable: false,
|
|
734
|
+
});
|
|
505
735
|
}
|
|
506
736
|
return this.#run("exec", statement, ctx);
|
|
507
737
|
}
|
|
@@ -511,119 +741,160 @@ class Dispatcher {
|
|
|
511
741
|
const handler = this.#schemes.get(schemeName);
|
|
512
742
|
if (handler === undefined)
|
|
513
743
|
return null;
|
|
514
|
-
const manifest =
|
|
744
|
+
const manifest = this.#schemes.manifestFor(schemeName);
|
|
515
745
|
if (manifest === undefined)
|
|
516
|
-
|
|
746
|
+
throw new Error(`registered scheme '${schemeName}' has no manifest`);
|
|
517
747
|
if (manifest.writableBy.includes(origin))
|
|
518
748
|
return null;
|
|
519
|
-
return
|
|
749
|
+
return _a.#failure("writer-forbidden", 403, `Writer '${origin}' cannot modify scheme '${schemeName}'.`, {}, {
|
|
750
|
+
writer: origin,
|
|
751
|
+
scheme: schemeName,
|
|
752
|
+
allowedWriters: [...manifest.writableBy],
|
|
753
|
+
retryable: false,
|
|
754
|
+
}); // {§scheme-surface-writableby-403}
|
|
520
755
|
}
|
|
521
756
|
// Per-loop flag gating. Schemes self-declare their flag affinity in
|
|
522
757
|
// their manifest (excludedInAsk / requiresWeb /
|
|
523
758
|
// requiresInteraction); SchemeRegistry.resolveForLoop returns the
|
|
524
|
-
// active set under the loop's persisted flags.
|
|
525
|
-
// set returns 403
|
|
759
|
+
// active set under the loop's persisted flags. A registered scheme outside
|
|
760
|
+
// that set returns 403; unknown names continue to their operation owner for
|
|
761
|
+
// the ordinary registration failure. Action-entry-as-outcome carries either.
|
|
526
762
|
async #checkFlagsGate(statement, loopId) {
|
|
527
763
|
// Broadcast SEND has no scheme to gate.
|
|
528
764
|
if (statement.op === "SEND" && statement.target === null)
|
|
529
765
|
return null;
|
|
530
|
-
const flags = await this.#
|
|
766
|
+
const flags = await LoopFlagsReader.read(this.#db, loopId);
|
|
531
767
|
// Fast path: default flags gate nothing. (auto never gates.)
|
|
532
768
|
if (!flags.noWeb && !flags.noInteraction && flags.mode === "act")
|
|
533
769
|
return null;
|
|
534
|
-
// §mode-ask-read-only — the ancient contract: an ask-mode loop NEVER changes the world. The
|
|
770
|
+
// {§mode-ask-read-only} — the ancient contract: an ask-mode loop NEVER changes the world. The
|
|
535
771
|
// filesystem writes (EDIT/COPY-dest/MOVE/KILL touching the `file` scheme — each proposes disk
|
|
536
|
-
// egress, §membership) are refused HERE, regardless of the scheme's read-activity, because
|
|
772
|
+
// egress, {§membership}) are refused HERE, regardless of the scheme's read-activity, because
|
|
537
773
|
// `file` stays active for READs. The EXEC host runtime is refused by its excludedInAsk scheme
|
|
538
774
|
// below. This lived only in SPEC (line 65) with no anchor → no guard → it silently regressed.
|
|
539
775
|
if (flags.mode === "ask") {
|
|
540
776
|
const isFile = (t) => schemeNameOf(t) === "file";
|
|
541
777
|
// Each branch narrows statement.op so statement.body is correctly typed (COPY dest is a
|
|
542
|
-
//
|
|
543
|
-
//
|
|
778
|
+
// resource selection). EDIT/KILL write the target; COPY writes the dest;
|
|
779
|
+
// MOVE deletes the source AND writes the dest - any `file` touch is a write.
|
|
544
780
|
let writesFilesystem = false;
|
|
545
781
|
if (statement.op === "EDIT" || statement.op === "KILL")
|
|
546
782
|
writesFilesystem = isFile(statement.target);
|
|
547
783
|
else if (statement.op === "COPY")
|
|
548
|
-
writesFilesystem = isFile(statement.body
|
|
784
|
+
writesFilesystem = isFile(statement.body?.target ?? null);
|
|
549
785
|
else if (statement.op === "MOVE")
|
|
550
|
-
writesFilesystem = isFile(statement.target) || isFile(statement.body);
|
|
786
|
+
writesFilesystem = isFile(statement.target) || isFile(statement.body?.target ?? null);
|
|
551
787
|
if (writesFilesystem) {
|
|
552
|
-
return
|
|
788
|
+
return _a.#failure("ask-mode-read-only", 403, `${statement.op} cannot change the filesystem in an ask-mode loop.`, {}, {
|
|
789
|
+
mode: flags.mode,
|
|
790
|
+
operation: statement.op,
|
|
791
|
+
recovery: "Answer or advise the user without changing the filesystem.",
|
|
792
|
+
retryable: false,
|
|
793
|
+
});
|
|
553
794
|
}
|
|
554
795
|
}
|
|
555
796
|
const active = this.#schemes.resolveForLoop(flags);
|
|
556
|
-
//
|
|
557
|
-
// current loop flags" invited an identical re-emit each turn → the StrikeRail's 508. An
|
|
558
|
-
// unavailable op is not a failing one; the model must change course, not repeat.
|
|
797
|
+
// {§tools-loop-affinity}: name the non-retryable restriction so the model changes course.
|
|
559
798
|
const restriction = flags.mode === "ask"
|
|
560
799
|
? "this is an ask-mode (read-only) loop — you cannot run commands or take host actions here"
|
|
561
800
|
: flags.noWeb && flags.noInteraction ? "web and interaction are disabled for this loop"
|
|
562
801
|
: flags.noWeb ? "web access is disabled for this loop"
|
|
563
802
|
: "interaction is disabled for this loop";
|
|
564
|
-
const
|
|
565
|
-
|
|
566
|
-
if (scheme === null)
|
|
803
|
+
const checkScheme = (scheme) => {
|
|
804
|
+
if (scheme === null || !this.#schemes.has(scheme))
|
|
567
805
|
return null;
|
|
568
806
|
if (active.has(scheme))
|
|
569
807
|
return null;
|
|
570
|
-
return
|
|
808
|
+
return _a.#failure("scheme-unavailable", 403, `Scheme '${scheme}' is unavailable because ${restriction}.`, {}, {
|
|
809
|
+
scheme,
|
|
810
|
+
recovery: "Answer or advise the user without using the unavailable scheme.",
|
|
811
|
+
retryable: false,
|
|
812
|
+
});
|
|
571
813
|
};
|
|
814
|
+
const check = (target) => checkScheme(schemeNameOf(target));
|
|
572
815
|
if (this.#isWorkerControl(statement))
|
|
573
816
|
return check(statement.target); // body is a spawn/fork task, not a dst path
|
|
574
817
|
if (statement.op === "COPY" || statement.op === "MOVE") {
|
|
575
|
-
return check(statement.target) ?? check(statement.
|
|
818
|
+
return check(statement.target) ?? check(statement.body?.target ?? null);
|
|
819
|
+
}
|
|
820
|
+
// {§exec-target-routing} — the operation owner and a non-file source
|
|
821
|
+
// are independent authorities; local/file targets stay executor-local.
|
|
822
|
+
if (statement.op === "EXEC") {
|
|
823
|
+
const operationDenial = checkScheme("exec");
|
|
824
|
+
if (operationDenial !== null)
|
|
825
|
+
return operationDenial;
|
|
826
|
+
const sourceScheme = schemeNameOf(statement.target);
|
|
827
|
+
return sourceScheme === null || sourceScheme === "file" ? null : checkScheme(sourceScheme);
|
|
576
828
|
}
|
|
577
829
|
return check(statement.target);
|
|
578
830
|
}
|
|
579
|
-
//
|
|
831
|
+
// Worker control is FORK/WORK (grammar 0.74.55), not COPY — its body
|
|
580
832
|
// is the new worker's seed prompt, not a destination path. The COPY gates and #handleCopy
|
|
581
833
|
// branch on this so they never parse the prompt as a dst path.
|
|
582
834
|
#isWorkerControl(statement) {
|
|
583
|
-
return statement.op === "FORK" || statement.op === "WORK"; //
|
|
584
|
-
}
|
|
585
|
-
// FORK
|
|
586
|
-
//
|
|
587
|
-
// continue it with the prompt (§machine-processes-fork-copies-the-log).
|
|
588
|
-
// • worker://<name> → SPAWN: a fresh sister (empty log) named <name>, started on the prompt.
|
|
589
|
-
// A LIVE sister already holding <name> is a 409 conflict; a free or terminated name is
|
|
590
|
-
// reclaimed (§worker-scheme-spawn). The self form is fork; only a name spawns.
|
|
591
|
-
// Both ride the daemon inject and obey the active-runs cap (508, §worker-scheme-cap).
|
|
592
|
-
// FORK/WORK — run control (grammar 0.74.55). Both name a NEW run in the target authority
|
|
593
|
-
// (worker://<name>) and carry its seed task in the body. WORK spawns a fresh worker; FORK branches
|
|
594
|
-
// the current worker's log into a named sister. Replaces the COPY(worker://) overload — one verb, one
|
|
595
|
-
// intent, so the model never conflates the target slot with the body (grammar#52).
|
|
835
|
+
return statement.op === "FORK" || statement.op === "WORK"; // worker control targets worker://<name> (grammar 0.74.55)
|
|
836
|
+
}
|
|
837
|
+
// WORK and FORK name the new worker in the target authority and carry its seed task in the body.
|
|
838
|
+
// Their distinct fresh/branched histories are specified by {§worker-scheme-spawn} and {§worker-scheme-fork}.
|
|
596
839
|
async #handleWorkerControl(statement, ctx) {
|
|
597
|
-
const
|
|
598
|
-
if (
|
|
599
|
-
return
|
|
600
|
-
const name =
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
return {
|
|
840
|
+
const address = WorkerControlAddress.resolve(statement.target, statement.op);
|
|
841
|
+
if (!address.ok)
|
|
842
|
+
return address.result;
|
|
843
|
+
const name = address.authority;
|
|
844
|
+
try {
|
|
845
|
+
WorkerName.assert(name); // {§worker-name-minting}
|
|
846
|
+
}
|
|
847
|
+
catch (error) {
|
|
848
|
+
if (!(error instanceof WorkerNameError))
|
|
849
|
+
throw error;
|
|
850
|
+
return _a.#failure(`worker-${error.code}`, 400, error.message, {}, {
|
|
851
|
+
operation: statement.op,
|
|
852
|
+
worker: error.workerName,
|
|
853
|
+
recovery: error.recovery,
|
|
854
|
+
retryable: false,
|
|
855
|
+
});
|
|
856
|
+
}
|
|
608
857
|
if (ctx.injectWorker === undefined)
|
|
609
|
-
throw new Error("
|
|
858
|
+
throw new Error("worker control: injectWorker capability absent");
|
|
610
859
|
const denied = await WorkerCap.deny(this.#db, ctx.workspaceId);
|
|
611
860
|
if (denied !== null)
|
|
612
861
|
return denied;
|
|
613
|
-
const prompt =
|
|
614
|
-
// §worker-delegation-inherits-flags — authority flows down the delegation edge: the child's live
|
|
862
|
+
const prompt = statement.body;
|
|
863
|
+
// {§worker-delegation-inherits-flags} — authority flows down the delegation edge: the child's live
|
|
615
864
|
// loop runs with ITS DELEGATOR'S flags. A flagless (non-auto) child's every side-effecting op
|
|
616
865
|
// proposes into a resolver-less void — 300s auto-cancel per attempt was the fan-out wedge.
|
|
617
|
-
const flags = await this.#
|
|
618
|
-
// A name is frozen per worker but reclaimable across time (§machine-processes-worker-origin): a LIVE
|
|
866
|
+
const flags = await LoopFlagsReader.read(this.#db, ctx.loopId);
|
|
867
|
+
// A name is frozen per worker but reclaimable across time ({§machine-processes-worker-origin}): a LIVE
|
|
619
868
|
// sister holding it is a 409 (legible, never a raw UNIQUE 500); a free/terminated name reclaims.
|
|
620
869
|
const live = await this.#db.worker_live_by_name.get({ workspace_id: ctx.workspaceId, name });
|
|
621
|
-
if (live !== undefined)
|
|
622
|
-
return
|
|
870
|
+
if (live !== undefined) {
|
|
871
|
+
return _a.#failure("worker-already-running", 409, `Worker '${name}' is already running.`, {}, { worker: name, retryable: false });
|
|
872
|
+
}
|
|
873
|
+
if (typeof statement.signal === "string") {
|
|
874
|
+
if (this.#branchWorker === undefined)
|
|
875
|
+
throw new Error("branch worker control: branchWorker capability absent");
|
|
876
|
+
const child = await this.#branchWorker({
|
|
877
|
+
workspaceId: ctx.workspaceId,
|
|
878
|
+
parentWorkerId: ctx.workerId,
|
|
879
|
+
parentLoopId: ctx.loopId,
|
|
880
|
+
parentTurnId: ctx.turnId,
|
|
881
|
+
op: statement.op,
|
|
882
|
+
name,
|
|
883
|
+
branch: statement.signal,
|
|
884
|
+
prompt,
|
|
885
|
+
flags,
|
|
886
|
+
origin: ctx.writer,
|
|
887
|
+
});
|
|
888
|
+
return {
|
|
889
|
+
status: 200,
|
|
890
|
+
body: name,
|
|
891
|
+
attrs: { branch: statement.signal, workerId: child.workerId, loopId: child.loopId },
|
|
892
|
+
};
|
|
893
|
+
}
|
|
623
894
|
if (statement.op === "FORK") {
|
|
624
895
|
// Branch the current worker's log into a named sister.
|
|
625
896
|
const branchWorkerId = await Fork.fork(this.#db, ctx.workerId, name);
|
|
626
|
-
await ctx.injectWorker({ workspaceId: ctx.workspaceId, workerId: branchWorkerId, prompt, flags });
|
|
897
|
+
await ctx.injectWorker({ workspaceId: ctx.workspaceId, workerId: branchWorkerId, prompt, flags, parentLoopId: ctx.loopId });
|
|
627
898
|
return { status: 200, body: name };
|
|
628
899
|
}
|
|
629
900
|
// WORK — a fresh worker sister named <name>.
|
|
@@ -631,290 +902,140 @@ class Dispatcher {
|
|
|
631
902
|
workspace_id: ctx.workspaceId, name, parent_worker_id: ctx.workerId, origin: ctx.writer,
|
|
632
903
|
});
|
|
633
904
|
if (row === undefined)
|
|
634
|
-
throw new Error("
|
|
635
|
-
await ctx.injectWorker({ workspaceId: ctx.workspaceId, workerId: row.id, prompt, flags });
|
|
905
|
+
throw new Error("worker spawn: worker insert returned no row");
|
|
906
|
+
await ctx.injectWorker({ workspaceId: ctx.workspaceId, workerId: row.id, prompt, flags, parentLoopId: ctx.loopId });
|
|
636
907
|
return { status: 200, body: name };
|
|
637
908
|
}
|
|
638
909
|
async #handleCopy(statement, ctx) {
|
|
639
|
-
if (statement.
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
910
|
+
if (statement.target === null) {
|
|
911
|
+
return _a.#failure("copy-source-required", 400, "COPY requires a source path.", {}, { retryable: false });
|
|
912
|
+
}
|
|
913
|
+
if (statement.body === null) {
|
|
914
|
+
return _a.#failure("copy-destination-required", 400, "COPY requires a destination.", {}, { retryable: false });
|
|
915
|
+
}
|
|
916
|
+
return this.#copyOrchestration({
|
|
917
|
+
statement,
|
|
918
|
+
source: {
|
|
919
|
+
target: statement.target,
|
|
920
|
+
lineMarker: statement.lineMarker,
|
|
921
|
+
},
|
|
922
|
+
destination: statement.body,
|
|
923
|
+
ctx,
|
|
924
|
+
});
|
|
650
925
|
}
|
|
651
926
|
async #handleMove(statement, ctx) {
|
|
652
|
-
if (statement.
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
// dispatch deletes the source AFTER the dest applies on accept.
|
|
676
|
-
if (copyResult.status === 202) {
|
|
677
|
-
return { ...copyResult, attrs: { ...copyResult.attrs, moveSource: { scheme: srcSchemeName, pathname: srcPathname } } };
|
|
678
|
-
}
|
|
679
|
-
const delResult = await this.#deleteEntry(srcSchemeName, srcPathname, ctx);
|
|
680
|
-
if (delResult.status >= 400)
|
|
681
|
-
return { status: delResult.status };
|
|
682
|
-
return copyResult;
|
|
683
|
-
}
|
|
684
|
-
// KILL — scheme-polymorphic destroy (plurnk-grammar#203 / 0.28.0). Entry-KILL
|
|
685
|
-
// permanently deletes the entry: the canonical delete now, MOVE→/dev/null
|
|
686
|
-
// retired from the model's vocabulary. Process-KILL (exec:///) aborts the
|
|
687
|
-
// running spawn's controller (the same teardown loop.cancel rides), addressed
|
|
688
|
-
// by coordinate pathname (#203). The KILL body is an opaque
|
|
689
|
-
// annotation with no runtime meaning; it survives into the log row's tx for
|
|
690
|
-
// free via the statement serialization. Status: 200 killed · 404 unknown ·
|
|
691
|
-
// 405 log:/// (append-only) · 403 writableBy (the #checkWritable gate, KILL ∈
|
|
692
|
-
// MUTATING_OPS) · 200/410/304/404 exec (killed / killed-earlier / exited / unknown) · 501 no-kill/delete scheme.
|
|
927
|
+
if (statement.target === null) {
|
|
928
|
+
return _a.#failure("move-source-required", 400, "MOVE requires a source path.", {}, { retryable: false });
|
|
929
|
+
}
|
|
930
|
+
// MOVE is relocation only - deletion is KILL's job ({§move}, {§move-dev-null-not-special}). The /dev/null
|
|
931
|
+
// and null-body delete-by-MOVE has no alternate meaning.
|
|
932
|
+
if (statement.body === null) {
|
|
933
|
+
return _a.#failure("move-destination-required", 400, "MOVE requires a destination.", {}, {
|
|
934
|
+
recovery: "Use KILL when the intended operation is deletion.",
|
|
935
|
+
retryable: false,
|
|
936
|
+
});
|
|
937
|
+
}
|
|
938
|
+
return this.#moveOrchestration({
|
|
939
|
+
statement,
|
|
940
|
+
source: {
|
|
941
|
+
target: statement.target,
|
|
942
|
+
lineMarker: statement.lineMarker,
|
|
943
|
+
},
|
|
944
|
+
destination: statement.body,
|
|
945
|
+
ctx,
|
|
946
|
+
});
|
|
947
|
+
}
|
|
948
|
+
// KILL is target-polymorphic. Scheme handlers own the optional numeric code's
|
|
949
|
+
// meaning; core retains worker and entry dispatch. {§operation-code-polymorphism}
|
|
693
950
|
async #handleKill(statement, ctx) {
|
|
694
951
|
if (statement.op !== "KILL")
|
|
695
952
|
throw new Error("unreachable");
|
|
696
953
|
const path = statement.target;
|
|
697
|
-
if (path === null)
|
|
698
|
-
return
|
|
954
|
+
if (path === null) {
|
|
955
|
+
return _a.#failure("kill-target-required", 400, "KILL requires a target path.", {}, { retryable: false });
|
|
956
|
+
}
|
|
699
957
|
const schemeName = schemeNameOf(path);
|
|
700
|
-
if (schemeName === null)
|
|
701
|
-
return
|
|
702
|
-
|
|
703
|
-
//
|
|
704
|
-
//
|
|
958
|
+
if (schemeName === null) {
|
|
959
|
+
return _a.#failure("kill-target-scheme-required", 400, "KILL target requires a scheme.", {}, { retryable: false });
|
|
960
|
+
}
|
|
961
|
+
// Log targets use the same killable dispatch as streams; erasure is their
|
|
962
|
+
// permanent curation operation. {§model-entry-log-curation}
|
|
705
963
|
// Process-KILL: any scheme whose handler exposes kill() aborts a live stream — the
|
|
706
964
|
// exec handler, registered as "exec" + under every runtime tag (sh/node), so a tag-
|
|
707
|
-
// addressed stream (sh:///l/t/s) routes here, not to deleteEntry. §exec
|
|
965
|
+
// addressed stream (sh:///l/t/s) routes here, not to deleteEntry. {§exec}
|
|
708
966
|
const killable = this.#schemes.get(schemeName);
|
|
709
967
|
if (killable !== undefined && typeof killable.kill === "function") {
|
|
710
968
|
// Pass the model's OWN scheme so a stream-KILL error answers in the runtime tag the
|
|
711
969
|
// model addressed (sh:///…), not the internal `exec` ({§fs-answer-in-canon}).
|
|
712
970
|
const handlerCtx = this.#handlerContext(schemeName, ctx);
|
|
713
|
-
|
|
971
|
+
if (handlerCtx === null) {
|
|
972
|
+
throw new InvalidOperationResultError(`Registered scheme '${schemeName}' has no dispatch context.`);
|
|
973
|
+
}
|
|
974
|
+
return await killable.kill(entryPathnameOf(path), statement.signal, handlerCtx, schemeName);
|
|
714
975
|
}
|
|
715
976
|
if (schemeName === "worker") {
|
|
716
|
-
// Entry-path present → KILL a
|
|
717
|
-
// NOT
|
|
718
|
-
// entry; only the path-ABSENT form (worker://<name>) terminates the
|
|
977
|
+
// Entry-path present → KILL a private owner-held entry (delete it), self-only —
|
|
978
|
+
// NOT worker cancellation. The authority (hostname) names the owner, the pathname the
|
|
979
|
+
// entry; only the path-ABSENT form (worker://<name>) terminates the worker-as-actor. {§worker-scheme}
|
|
719
980
|
const entryPath = path.kind === "url" ? (path.pathname ?? "") : "";
|
|
720
981
|
if (entryPath !== "" && entryPath !== "/") {
|
|
721
982
|
const workerHandler = this.#schemes.get("worker");
|
|
722
983
|
const handlerCtx = this.#handlerContext("worker", ctx);
|
|
723
|
-
|
|
984
|
+
if (handlerCtx === null) {
|
|
985
|
+
throw new InvalidOperationResultError("Registered scheme 'worker' has no dispatch context.");
|
|
986
|
+
}
|
|
987
|
+
return await workerHandler.killEntry(statement, handlerCtx);
|
|
724
988
|
}
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
989
|
+
const address = WorkerControlAddress.resolve(path, "KILL");
|
|
990
|
+
if (!address.ok)
|
|
991
|
+
return address.result;
|
|
992
|
+
// `~` is the sole current-worker sigil; every other authority is a literal name.
|
|
993
|
+
// An idle worker is a no-op 200; a missing named worker is 404. {§worker-control-addressing}
|
|
994
|
+
const name = address.authority;
|
|
730
995
|
let workerId = ctx.workerId;
|
|
731
996
|
if (name !== "~") {
|
|
732
997
|
const row = await this.#db.worker_resolve_by_name.get({ workspace_id: ctx.workspaceId, name });
|
|
733
|
-
if (row === undefined)
|
|
734
|
-
return
|
|
998
|
+
if (row === undefined) {
|
|
999
|
+
return _a.#failure("worker-not-found", 404, `Worker '${name}' does not exist in this workspace.`, {}, { worker: name, retryable: false });
|
|
1000
|
+
}
|
|
735
1001
|
workerId = row.id;
|
|
736
1002
|
}
|
|
737
1003
|
if (this.#cancelWorker === undefined)
|
|
738
|
-
throw new Error("
|
|
739
|
-
// §op-synchronous — KILL is decisive. Await the one lifecycle owner so the
|
|
1004
|
+
throw new Error("worker kill: cancelWorker capability absent");
|
|
1005
|
+
// {§op-synchronous} — KILL is decisive. Await the one lifecycle owner so the
|
|
740
1006
|
// same-turn pending-work gate observes the complete subtree as terminal.
|
|
741
1007
|
await this.#cancelWorker(workerId, "killed via worker:// KILL");
|
|
742
1008
|
return { status: 200 };
|
|
743
1009
|
}
|
|
744
|
-
if (!this.#schemes.has(schemeName))
|
|
745
|
-
return {
|
|
1010
|
+
if (!this.#schemes.has(schemeName)) {
|
|
1011
|
+
return _a.#failure("scheme-not-found", 501, `Scheme '${schemeName}' is not registered.`, {}, { scheme: schemeName, retryable: false });
|
|
1012
|
+
}
|
|
746
1013
|
// A host-effecting delete (file) returns 202 to PROPOSE — pass its attrs through so the proposal
|
|
747
|
-
// carries the delete target to review (
|
|
748
|
-
|
|
749
|
-
return delResult.attrs !== undefined ? { status: delResult.status, attrs: delResult.attrs } : { status: delResult.status };
|
|
750
|
-
}
|
|
751
|
-
// Multi-file READ fan-out (SPEC §matcher-result — "the companion to FIND's survey"). A glob
|
|
752
|
-
// READ target resolves to MANY files; READ returns one log row per file that matches, each
|
|
753
|
-
// holding that file's matching lines. The matched SET is exactly FIND's survey (which files
|
|
754
|
-
// + where — matchLines), so we reuse the scheme's own find, then READ each matched file. One
|
|
755
|
-
// model command, N log rows — each row addresses its concrete file, so it folds/kills/re-READs
|
|
756
|
-
// on its own. The running sequence counter in runTurn advances by rowsWritten.
|
|
757
|
-
// A READ fans out (honors FIND) when it resolves to more than the single exact entry: a glob
|
|
758
|
-
// or folder scope, OR a matcher (which selects per-match within whatever the target resolved).
|
|
759
|
-
// A bare entry, body-less, is the one direct read. #286
|
|
760
|
-
#readFansOut(statement) {
|
|
761
|
-
if (statement.op !== "READ")
|
|
762
|
-
return false;
|
|
763
|
-
if ("body" in statement && statement.body !== null)
|
|
764
|
-
return true; // a matcher → per-match fan-out
|
|
765
|
-
const t = statement.target;
|
|
766
|
-
const p = t === null ? "" : (t.kind === "url" ? t.pathname : t.raw);
|
|
767
|
-
if (p.includes("*"))
|
|
768
|
-
return true;
|
|
769
|
-
const schemeName = schemeNameOf(t);
|
|
770
|
-
return p.endsWith("/") && schemeName !== null && this.#schemes.manifestFor(schemeName)?.folderScopes === true;
|
|
771
|
-
}
|
|
772
|
-
// Clone the READ onto one concrete match — the FIND already matched, so strip the body
|
|
773
|
-
// (no re-match) and set <L> to its readable-row span. A null span (body-less folder/glob
|
|
774
|
-
// fan-out) reads the whole entry. #286
|
|
775
|
-
static #retargetRead(statement, pathname, span) {
|
|
776
|
-
const t = statement.target;
|
|
777
|
-
const target = t !== null && t.kind === "url"
|
|
778
|
-
? { ...t, pathname, raw: `${t.scheme}://${pathname}` }
|
|
779
|
-
: { ...t, raw: pathname };
|
|
780
|
-
const lineMarker = span !== null ? { marks: [span.rowStart, span.rowEnd] } : null;
|
|
781
|
-
return { ...statement, target: target, lineMarker, body: null };
|
|
782
|
-
}
|
|
783
|
-
#standardEntryManifest(schemeName, statement) {
|
|
784
|
-
const handler = this.#schemes.get(schemeName);
|
|
785
|
-
if (typeof handler?.find === "function")
|
|
786
|
-
return null;
|
|
787
|
-
const target = statement.target;
|
|
788
|
-
if (target === null || target.kind !== "url" || target.scheme === null)
|
|
789
|
-
return null;
|
|
790
|
-
const manifest = this.#schemes.manifestFor(schemeName);
|
|
791
|
-
return manifest === undefined ? null : { ...manifest, name: target.scheme };
|
|
792
|
-
}
|
|
793
|
-
static #foldStoredEntryAuthority(statement) {
|
|
794
|
-
const target = statement.target;
|
|
795
|
-
if (target === null || target.kind !== "url")
|
|
796
|
-
return statement;
|
|
797
|
-
const pathname = foldAuthorityIntoPath(target.hostname, target.pathname);
|
|
798
|
-
return {
|
|
799
|
-
...statement,
|
|
800
|
-
target: { ...target, hostname: null, pathname, raw: `${target.scheme}://${pathname}` },
|
|
801
|
-
};
|
|
802
|
-
}
|
|
803
|
-
static #storedEntryMatchRead(statement, pathname, span, storage) {
|
|
804
|
-
const target = statement.target;
|
|
805
|
-
if (target === null || target.kind !== "url")
|
|
806
|
-
return _a.#retargetRead(statement, pathname, span);
|
|
807
|
-
const hostPrefix = target.hostname === null ? null : `/${target.hostname}`;
|
|
808
|
-
const displayPath = hostPrefix !== null && pathname.startsWith(`${hostPrefix}/`)
|
|
809
|
-
? pathname.slice(hostPrefix.length)
|
|
810
|
-
: pathname;
|
|
811
|
-
const lineMarker = span === null ? null : { marks: [span.rowStart, span.rowEnd] };
|
|
812
|
-
return {
|
|
813
|
-
...statement,
|
|
814
|
-
target: storage
|
|
815
|
-
? { ...target, hostname: null, pathname, raw: `${target.scheme}://${pathname}` }
|
|
816
|
-
: { ...target, pathname: displayPath, raw: `${target.scheme}://${target.hostname ?? ""}${displayPath}` },
|
|
817
|
-
lineMarker,
|
|
818
|
-
body: null,
|
|
819
|
-
};
|
|
1014
|
+
// carries the delete target to review (#isProposal fires on 202). Plurnk-internal deletes execute inline.
|
|
1015
|
+
return this.#deleteEntry(schemeName, entryPathnameOf(path), ctx);
|
|
820
1016
|
}
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
const found = standardEntryManifest === null
|
|
827
|
-
? await this.#run(schemeName, findStatement, ctx)
|
|
828
|
-
: await EntryFind.findWorkspaceEntries(_a.#foldStoredEntryAuthority(findStatement), ctx, standardEntryManifest);
|
|
829
|
-
const matches = found.matches ?? [];
|
|
830
|
-
const overflow = typeof found.overflow === "number" ? found.overflow : null;
|
|
831
|
-
// §find-count-not-contents applies to WORK as well as rendering. A READ
|
|
832
|
-
// over an over-budget selection writes the bounded FIND summary plus one
|
|
833
|
-
// loud refusal; it never silently truncates and never materializes a
|
|
834
|
-
// hidden prefix/suffix of the set.
|
|
835
|
-
if (overflow !== null) {
|
|
836
|
-
const findRowId = await this.#writeLog({ statement: { ...statement, op: "FIND" }, result: found, workspaceId: ctx.workspaceId, workerId, loopId, turnId, sequence, origin });
|
|
837
|
-
onDispatch?.(findRowId);
|
|
838
|
-
const result = {
|
|
839
|
-
status: 413,
|
|
840
|
-
error: `${overflow} matches exceed the READ materialization budget — narrow the target or matcher.`,
|
|
841
|
-
};
|
|
842
|
-
const readRowId = await this.#writeLog({ statement, result, workspaceId: ctx.workspaceId, workerId, loopId, turnId, sequence: sequence + 1, origin });
|
|
843
|
-
onDispatch?.(readRowId);
|
|
844
|
-
return { ...result, rowsWritten: 2 };
|
|
845
|
-
}
|
|
846
|
-
// Find-less scheme, a matcher/scope error, or zero matches → a single row carrying the
|
|
847
|
-
// status, exactly like a non-fanned READ. The model sees the empty/failed result, not silence.
|
|
848
|
-
if (found.status !== 200 || matches.length === 0) {
|
|
849
|
-
const result = { status: found.status === 200 ? 204 : found.status };
|
|
850
|
-
const id = await this.#writeLog({ statement, result, workspaceId: ctx.workspaceId, workerId, loopId, turnId, sequence, origin });
|
|
851
|
-
onDispatch?.(id);
|
|
852
|
-
return { ...result, rowsWritten: 1 };
|
|
853
|
-
}
|
|
854
|
-
// One READ row per MATCH — deliver its source lines (or the whole entry for a body-less
|
|
855
|
-
// folder/glob) while the logged statement carries the corresponding readable-row scope.
|
|
856
|
-
// The delivery uses a raw line-slice rather than reinterpreting provenance as the scheme's
|
|
857
|
-
// structural <L>. Read each distinct entry's content once, then line-slice per match. #286
|
|
858
|
-
// §matcher-selection-signal — the SELECTION SUMMARY row: the internal FIND that located
|
|
859
|
-
// the matches is WRITTEN (op=FIND, its full result — per-hit matchSpan + matchPath, the
|
|
860
|
-
// canonical dialect coordinate) before the per-match deliveries, exactly as if the model
|
|
861
|
-
// had FINDed then READ. On a degenerate single-line document the N delivery rows are
|
|
862
|
-
// identical whole-file lines; the summary row is what tells the model its query hit N
|
|
863
|
-
// times and WHERE (run30: two hits indistinguishable from failure; 17 retries, 508).
|
|
864
|
-
const findRowId = await this.#writeLog({ statement: { ...statement, op: "FIND" }, result: found, workspaceId: ctx.workspaceId, workerId, loopId, turnId, sequence, origin });
|
|
865
|
-
onDispatch?.(findRowId);
|
|
866
|
-
const wholeByPath = new Map();
|
|
867
|
-
const fannedStatuses = [];
|
|
868
|
-
let written = 1;
|
|
869
|
-
for (const m of matches) {
|
|
870
|
-
ctx.signal?.throwIfAborted();
|
|
871
|
-
let whole = wholeByPath.get(m.pathname);
|
|
872
|
-
if (whole === undefined) {
|
|
873
|
-
const retargeted = standardEntryManifest === null
|
|
874
|
-
? _a.#retargetRead(statement, m.pathname, null)
|
|
875
|
-
: _a.#storedEntryMatchRead(statement, m.pathname, null, true);
|
|
876
|
-
whole = standardEntryManifest === null
|
|
877
|
-
? await this.#run(schemeName, retargeted, ctx)
|
|
878
|
-
: await EntryOps.readWorkspaceEntry(retargeted, ctx, standardEntryManifest);
|
|
879
|
-
wholeByPath.set(m.pathname, whole);
|
|
880
|
-
}
|
|
881
|
-
const result = _a.#sliceMatch(whole, m.span);
|
|
882
|
-
const deliveredStatement = standardEntryManifest === null
|
|
883
|
-
? _a.#retargetRead(statement, m.pathname, m.span)
|
|
884
|
-
: _a.#storedEntryMatchRead(statement, m.pathname, m.span, false);
|
|
885
|
-
const id = await this.#writeLog({ statement: deliveredStatement, result, workspaceId: ctx.workspaceId, workerId, loopId, turnId, sequence: sequence + written, origin });
|
|
886
|
-
onDispatch?.(id);
|
|
887
|
-
fannedStatuses.push(result.status);
|
|
888
|
-
written++;
|
|
889
|
-
}
|
|
890
|
-
return { status: 200, rowsWritten: written, fannedStatuses };
|
|
891
|
-
}
|
|
892
|
-
// Deliver one match: the whole entry (body-less, span null) or the source lines at the span —
|
|
893
|
-
// a RAW line-slice, so a structural mimetype (json item-index / xml) doesn't mis-slice a span
|
|
894
|
-
// that is, by construction, source line numbers (#286).
|
|
895
|
-
static #sliceMatch(whole, span) {
|
|
896
|
-
if (whole.status !== 200 || span === null)
|
|
897
|
-
return whole;
|
|
898
|
-
const sliced = LineMarkerOps.sliceLines(typeof whole.content === "string" ? whole.content : "", { marks: [span.lineStart, span.lineEnd] });
|
|
899
|
-
if (sliced.status !== 200)
|
|
900
|
-
return { status: sliced.status, error: sliced.error };
|
|
901
|
-
return { status: 200, content: sliced.text ?? "", mimetype: "text/markdown", startLine: sliced.startLine ?? span.lineStart };
|
|
902
|
-
}
|
|
903
|
-
// §model-entry — mirror a verbatim model emission back as an actionless `model` log row, so
|
|
904
|
-
// the model can finally SEE its own prior output (and reason through its own syntax errors).
|
|
1017
|
+
// {§model-entry} — mirror a model emission back as an actionless log row, so
|
|
1018
|
+
// the model can inspect and curate its own prior behavior. Ordinary admitted
|
|
1019
|
+
// emissions are folded; the bounded invalid-emission lifeline is the one
|
|
1020
|
+
// born-open rejected item under {§invalid-emission-attempts} and labels
|
|
1021
|
+
// that admission fact structurally.
|
|
905
1022
|
// Born FOLDED by default (budget-neutral until OPENed); the turn-0 exemplar passes folded:false
|
|
906
1023
|
// (born open — the one worked example the model orients on, thinning the grammar). text/vnd.plurnk.
|
|
907
|
-
async writeModelEntry({ verbatim, workerId, loopId, turnId, sequence, folded, origin = "model", reasoningItems }) {
|
|
1024
|
+
async writeModelEntry({ verbatim, workerId, loopId, turnId, sequence, folded, origin = "model", admission = "accepted", reasoningItems }) {
|
|
908
1025
|
const row = await this.#db.engine_insert_log_entry.get({
|
|
909
1026
|
worker_id: workerId, loop_id: loopId, turn_id: turnId, sequence,
|
|
910
|
-
origin, source: null, op:
|
|
1027
|
+
origin, source: null, op: null, suffix: "", signal: null,
|
|
911
1028
|
scheme: null, username: null, password: null, hostname: null, port: null,
|
|
912
|
-
pathname: null,
|
|
1029
|
+
pathname: null, query: null, fragment: null, lineMarker: null,
|
|
913
1030
|
tx: "", mimetype_tx: "text/vnd.plurnk",
|
|
914
1031
|
rx: JSON.stringify({ content: verbatim, mimetype: "text/vnd.plurnk" }),
|
|
915
1032
|
mimetype_rx: "application/json",
|
|
916
1033
|
status_rx: 200, tokens: this.#tokenize(verbatim), state: "resolved", outcome: null,
|
|
917
|
-
attrs:
|
|
1034
|
+
attrs: JSON.stringify({
|
|
1035
|
+
kind: "model_emission",
|
|
1036
|
+
...(admission === "rejected" ? { admission } : {}),
|
|
1037
|
+
...(reasoningItems !== undefined && reasoningItems.length > 0 ? { reasoning: reasoningItems } : {}),
|
|
1038
|
+
}),
|
|
918
1039
|
});
|
|
919
1040
|
if (row === undefined)
|
|
920
1041
|
throw new Error("Dispatcher.writeModelEntry: insert returned no row");
|
|
@@ -922,7 +1043,7 @@ class Dispatcher {
|
|
|
922
1043
|
await this.#db.engine_fold_log_entry.run({ id: row.id });
|
|
923
1044
|
return row.id;
|
|
924
1045
|
}
|
|
925
|
-
// PLAN — the model's
|
|
1046
|
+
// PLAN — the model's intended-goals op. An ordinary op: dispatched like any
|
|
926
1047
|
// other, logged, and broadcast to the client as a log entry — but a pure no-op for
|
|
927
1048
|
// state (PLAN ∉ MUTATING_OPS); its body serializes into the log row's tx, no effect.
|
|
928
1049
|
#handlePlan(statement) {
|
|
@@ -930,93 +1051,735 @@ class Dispatcher {
|
|
|
930
1051
|
throw new Error("unreachable");
|
|
931
1052
|
return { status: 200 };
|
|
932
1053
|
}
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
1054
|
+
static #isDispatchResult(value) {
|
|
1055
|
+
return "status" in value;
|
|
1056
|
+
}
|
|
1057
|
+
async #resolveResourceSelection(selection, ctx) {
|
|
1058
|
+
const { target, lineMarker } = selection;
|
|
1059
|
+
const scheme = schemeNameOf(target);
|
|
1060
|
+
if (scheme === null) {
|
|
1061
|
+
return _a.#failure("resource-scheme-required", 400, "COPY and MOVE resources require a scheme.", {}, { retryable: false });
|
|
1062
|
+
}
|
|
1063
|
+
if (target.kind === "url" && target.scheme === "worker" && (target.hostname ?? "") !== "") {
|
|
1064
|
+
return _a.#failure("worker-copy-address-invalid", 400, "COPY and MOVE do not address named worker spaces.", {}, {
|
|
1065
|
+
recovery: "Move worker-space content with READ and EDIT.",
|
|
1066
|
+
retryable: false,
|
|
1067
|
+
});
|
|
1068
|
+
}
|
|
1069
|
+
const handler = this.#schemes.get(scheme);
|
|
1070
|
+
const manifest = this.#schemes.manifestFor(scheme);
|
|
1071
|
+
if (handler === undefined || manifest === undefined) {
|
|
1072
|
+
return _a.#failure("scheme-not-found", 501, `COPY or MOVE addressed the unregistered scheme '${scheme}'.`, {}, {
|
|
1073
|
+
scheme,
|
|
1074
|
+
retryable: false,
|
|
1075
|
+
});
|
|
1076
|
+
}
|
|
1077
|
+
if (manifest.category !== "data") {
|
|
1078
|
+
return _a.#failure("entry-operation-unsupported", 400, `COPY and MOVE require entry-bearing resources; '${scheme}' is a ${manifest.category} scheme.`, {}, {
|
|
1079
|
+
scheme,
|
|
1080
|
+
category: manifest.category,
|
|
1081
|
+
retryable: false,
|
|
1082
|
+
});
|
|
1083
|
+
}
|
|
1084
|
+
const fragment = target.kind === "url" ? target.fragment : null;
|
|
1085
|
+
const channel = fragment ?? manifest.defaultChannel;
|
|
1086
|
+
if (channel.length === 0) {
|
|
1087
|
+
return _a.#failure("channel-required", 400, `The '${scheme}' scheme has no default channel.`, {}, {
|
|
1088
|
+
scheme,
|
|
1089
|
+
recovery: "Address a named channel with a URI fragment.",
|
|
1090
|
+
retryable: false,
|
|
1091
|
+
});
|
|
1092
|
+
}
|
|
1093
|
+
if (fragment !== null
|
|
1094
|
+
&& fragment !== manifest.defaultChannel
|
|
1095
|
+
&& !Object.hasOwn(manifest.channels, fragment)) {
|
|
1096
|
+
const availableChannels = [
|
|
1097
|
+
...new Set([manifest.defaultChannel, ...Object.keys(manifest.channels)]),
|
|
1098
|
+
].filter((candidate) => candidate.length > 0);
|
|
1099
|
+
return _a.#failure("channel-not-found", 400, `Channel #${fragment} is not declared by the '${scheme}' scheme.`, {}, {
|
|
1100
|
+
requestedChannel: fragment,
|
|
1101
|
+
availableChannels,
|
|
1102
|
+
retryable: false,
|
|
1103
|
+
});
|
|
1104
|
+
}
|
|
1105
|
+
const pathname = entryPathnameOf(target);
|
|
1106
|
+
const canonicalFilePath = scheme === "file"
|
|
1107
|
+
? Namespace.canonicalizeSpelling(pathname, await this.#workspaceRoot(ctx.workspaceId))
|
|
1108
|
+
: pathname;
|
|
1109
|
+
return {
|
|
1110
|
+
target,
|
|
1111
|
+
lineMarker,
|
|
1112
|
+
scheme,
|
|
1113
|
+
pathname,
|
|
1114
|
+
identityPathname: canonicalFilePath ?? pathname,
|
|
1115
|
+
channel,
|
|
1116
|
+
manifest,
|
|
1117
|
+
};
|
|
1118
|
+
}
|
|
1119
|
+
async #selectSource(selection, ctx) {
|
|
1120
|
+
const handler = this.#schemes.get(selection.scheme);
|
|
1121
|
+
if (handler === undefined) {
|
|
1122
|
+
throw new InvalidOperationResultError(`Resolved COPY/MOVE source scheme '${selection.scheme}' is no longer registered.`);
|
|
1123
|
+
}
|
|
1124
|
+
const addressedScheme = selection.target.kind === "url"
|
|
1125
|
+
? selection.target.scheme
|
|
1126
|
+
: selection.scheme;
|
|
1127
|
+
const schemeCtx = new SchemeCtxImpl(ctx, addressedScheme, selection.manifest, this.#liveSubscriptions, { publishedChannel: selection.channel });
|
|
1128
|
+
const prepared = await this.#prepareDataRepresentation({
|
|
1129
|
+
target: selection.target,
|
|
1130
|
+
routedScheme: selection.scheme,
|
|
1131
|
+
handler,
|
|
1132
|
+
manifest: selection.manifest,
|
|
1133
|
+
ctx,
|
|
1134
|
+
schemeCtx,
|
|
1135
|
+
publishedChannel: selection.channel,
|
|
1136
|
+
});
|
|
1137
|
+
if (prepared.result !== null)
|
|
1138
|
+
return prepared.result;
|
|
1139
|
+
if (prepared.address === null) {
|
|
1140
|
+
return _a.#failure("entry-not-found", 404, `No entry exists at ${this.#resourceAddress(selection)}.`, {}, { target: this.#resourceAddress(selection) });
|
|
1141
|
+
}
|
|
1142
|
+
const storageAddress = prepared.address;
|
|
1143
|
+
const read = await EntryCrud.readEntry(storageAddress.pathname, ctx, storageAddress.scheme, storageAddress.ownerId);
|
|
1144
|
+
if (read.status >= 400)
|
|
1145
|
+
return read;
|
|
1146
|
+
if (read.status !== 200 || read.entry === null) {
|
|
1147
|
+
throw new InvalidOperationResultError(`The '${selection.scheme}' scheme returned status ${read.status} without a COPY/MOVE source entry.`);
|
|
1148
|
+
}
|
|
1149
|
+
const selected = read.entry.channels[selection.channel];
|
|
1150
|
+
if (selected === undefined) {
|
|
1151
|
+
return _a.#failure("channel-not-found", 404, `No channel named #${selection.channel} exists at ${renderAddress(storageAddress.scheme, storageAddress.pathname)}.`, {}, {
|
|
1152
|
+
target: renderAddress(storageAddress.scheme, storageAddress.pathname),
|
|
1153
|
+
requestedChannel: selection.channel,
|
|
1154
|
+
availableChannels: Object.keys(read.entry.channels),
|
|
1155
|
+
retryable: false,
|
|
1156
|
+
});
|
|
1157
|
+
}
|
|
1158
|
+
let content = selected.content;
|
|
1159
|
+
let scopeNormalizations;
|
|
1160
|
+
if (await MimetypeBinary.isBinaryMimetype(selected.mimetype, ctx.mimetypes)) {
|
|
1161
|
+
return _a.#failure("binary-source-unsupported", 415, `Channel #${selection.channel} is a binary marker and cannot be copied or moved.`, {}, {
|
|
1162
|
+
channel: selection.channel,
|
|
1163
|
+
mimetype: selected.mimetype,
|
|
1164
|
+
recovery: "Use a source with a readable text projection.",
|
|
1165
|
+
retryable: false,
|
|
1166
|
+
});
|
|
1167
|
+
}
|
|
1168
|
+
if (selection.lineMarker !== null) {
|
|
1169
|
+
const sliced = LineMarkerOps.sliceLinesRaw(content, selection.lineMarker);
|
|
1170
|
+
if (sliced.status !== 200)
|
|
1171
|
+
return Results.assert(sliced);
|
|
1172
|
+
content = sliced.text ?? "";
|
|
1173
|
+
scopeNormalizations = sliced.scopeNormalizations;
|
|
945
1174
|
}
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
1175
|
+
if (selected.producerResult !== undefined && selected.producerResult.status >= 400) {
|
|
1176
|
+
return Results.assert(selected.producerResult);
|
|
1177
|
+
}
|
|
1178
|
+
return {
|
|
1179
|
+
...selection,
|
|
1180
|
+
storageAddress,
|
|
1181
|
+
content,
|
|
1182
|
+
mimetype: selected.mimetype,
|
|
1183
|
+
...(scopeNormalizations === undefined ? {} : { scopeNormalizations }),
|
|
1184
|
+
};
|
|
1185
|
+
}
|
|
1186
|
+
static #prependScopeNormalizations(result, scopeNormalizations) {
|
|
1187
|
+
if (scopeNormalizations === undefined)
|
|
1188
|
+
return result;
|
|
1189
|
+
return Results.assert({
|
|
1190
|
+
...result,
|
|
1191
|
+
scopeNormalizations: [
|
|
1192
|
+
...scopeNormalizations,
|
|
1193
|
+
...(result.scopeNormalizations ?? []),
|
|
1194
|
+
],
|
|
1195
|
+
});
|
|
1196
|
+
}
|
|
1197
|
+
#resourceAddress(selection) {
|
|
1198
|
+
const address = renderTarget({
|
|
1199
|
+
scheme: selection.scheme === "file" ? null : selection.scheme,
|
|
1200
|
+
pathname: selection.scheme === "file"
|
|
1201
|
+
? selection.identityPathname.replace(/^\//, "")
|
|
1202
|
+
: selection.identityPathname,
|
|
1203
|
+
fragment: selection.channel === selection.manifest.defaultChannel
|
|
1204
|
+
? null
|
|
1205
|
+
: selection.channel,
|
|
1206
|
+
});
|
|
1207
|
+
if (address === null)
|
|
1208
|
+
throw new Error("resolved resource selection has no renderable address");
|
|
1209
|
+
return address;
|
|
1210
|
+
}
|
|
1211
|
+
#pendingEffect(selection, action) {
|
|
1212
|
+
return {
|
|
1213
|
+
target: this.#resourceAddress(selection),
|
|
1214
|
+
action,
|
|
1215
|
+
};
|
|
1216
|
+
}
|
|
1217
|
+
static #appliedEffects(result, pending) {
|
|
1218
|
+
// {§edit-result-copy-move-effects} — only an applied mutation earns
|
|
1219
|
+
// engine-composed effects; native scheme receipts remain internal input.
|
|
1220
|
+
const exact = Results.assert(result);
|
|
1221
|
+
if (exact.status === 304 || exact.status === 202 || exact.status >= 300)
|
|
1222
|
+
return exact;
|
|
1223
|
+
if (exact.effects !== undefined) {
|
|
1224
|
+
throw new InvalidOperationResultError("A COPY/MOVE mutation result supplied effects before engine composition.");
|
|
1225
|
+
}
|
|
1226
|
+
const batch = exact.editReceipt === undefined
|
|
1227
|
+
? undefined
|
|
1228
|
+
: assertEditBatchReceipt(exact.editReceipt);
|
|
1229
|
+
const single = batch === undefined && exact.receipt !== undefined
|
|
1230
|
+
? assertEditReceipt(exact.receipt)
|
|
1231
|
+
: undefined;
|
|
1232
|
+
const batchSize = batch === undefined
|
|
1233
|
+
? undefined
|
|
1234
|
+
: "disposition" in batch
|
|
1235
|
+
? batch.superseded.length
|
|
1236
|
+
: batch.effects.length;
|
|
1237
|
+
if (batchSize !== undefined && batchSize !== pending.length) {
|
|
1238
|
+
throw new InvalidOperationResultError(`COPY/MOVE expected ${pending.length} receipt projections, got ${batchSize}.`);
|
|
1239
|
+
}
|
|
1240
|
+
if (single !== undefined && pending.length !== 1) {
|
|
1241
|
+
throw new InvalidOperationResultError(`COPY/MOVE received one receipt for ${pending.length} resource effects.`);
|
|
1242
|
+
}
|
|
1243
|
+
let effects;
|
|
1244
|
+
if (batch !== undefined && "disposition" in batch) {
|
|
1245
|
+
const replacement = pending[0];
|
|
1246
|
+
if (replacement === undefined) {
|
|
1247
|
+
throw new InvalidOperationResultError("A reviewer-replaced COPY/MOVE batch has no resource effect.");
|
|
1248
|
+
}
|
|
1249
|
+
if (pending.some(({ target, action }) => target !== replacement.target || action !== replacement.action)) {
|
|
1250
|
+
throw new InvalidOperationResultError("A reviewer-replaced COPY/MOVE batch spans incompatible resource effects.");
|
|
967
1251
|
}
|
|
1252
|
+
effects = [{
|
|
1253
|
+
...replacement,
|
|
1254
|
+
receipt: projectEditReceipt(batch, 0),
|
|
1255
|
+
}];
|
|
968
1256
|
}
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
1257
|
+
else {
|
|
1258
|
+
effects = pending.map((effect, index) => ({
|
|
1259
|
+
...effect,
|
|
1260
|
+
...(batch !== undefined
|
|
1261
|
+
? { receipt: projectEditReceipt(batch, index) }
|
|
1262
|
+
: single !== undefined
|
|
1263
|
+
? { receipt: single }
|
|
1264
|
+
: {}),
|
|
1265
|
+
}));
|
|
1266
|
+
}
|
|
1267
|
+
assertResourceEffects(effects);
|
|
1268
|
+
const { editReceipt: _editReceipt, receipt: _receipt, ...withoutInternalReceipts } = exact;
|
|
1269
|
+
return {
|
|
1270
|
+
...withoutInternalReceipts,
|
|
1271
|
+
effects,
|
|
1272
|
+
};
|
|
1273
|
+
}
|
|
1274
|
+
// {§copy-move-observation} {§edit-result-copy-move-effects} — scoped
|
|
1275
|
+
// COPY/MOVE into an unscoped channel is a text materialization even when
|
|
1276
|
+
// CRUD creates the channel wholesale. Carry the receipt through synchronous
|
|
1277
|
+
// writes and proposals; the destination scheme owns reviewed output.
|
|
1278
|
+
static #withEditMaterialization(result, receipt) {
|
|
1279
|
+
const exact = Results.assert(result);
|
|
1280
|
+
if (exact.status !== 200 && exact.status !== 201 && exact.status !== 202)
|
|
1281
|
+
return exact;
|
|
1282
|
+
const materialized = exact.editReceipt === undefined
|
|
1283
|
+
? assertEditBatchReceipt(receipt)
|
|
1284
|
+
: assertEditBatchReceipt(exact.editReceipt);
|
|
1285
|
+
return {
|
|
1286
|
+
...exact,
|
|
1287
|
+
editReceipt: materialized,
|
|
1288
|
+
...(exact.status === 202
|
|
1289
|
+
? {
|
|
1290
|
+
attrs: {
|
|
1291
|
+
...exact.attrs,
|
|
1292
|
+
editReceipt: materialized,
|
|
1293
|
+
},
|
|
980
1294
|
}
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
1295
|
+
: {}),
|
|
1296
|
+
};
|
|
1297
|
+
}
|
|
1298
|
+
#finalizeEffects(result, selection, pending) {
|
|
1299
|
+
const routed = this.#withProposalRoute(result, selection);
|
|
1300
|
+
if (routed.status !== 202)
|
|
1301
|
+
return _a.#appliedEffects(routed, pending);
|
|
1302
|
+
return {
|
|
1303
|
+
...routed,
|
|
1304
|
+
attrs: {
|
|
1305
|
+
...routed.attrs,
|
|
1306
|
+
proposalEffects: pending,
|
|
1307
|
+
},
|
|
1308
|
+
};
|
|
1309
|
+
}
|
|
1310
|
+
static #effectsOf(result) {
|
|
1311
|
+
return result.effects === undefined
|
|
1312
|
+
? []
|
|
1313
|
+
: assertResourceEffects(result.effects);
|
|
1314
|
+
}
|
|
1315
|
+
static #withCombinedEffects(result, ...additional) {
|
|
1316
|
+
const existing = _a.#effectsOf(result);
|
|
1317
|
+
const effects = [...existing, ...additional.flat()];
|
|
1318
|
+
const { effects: _effects, ...withoutEffects } = result;
|
|
1319
|
+
return effects.length === 0
|
|
1320
|
+
? withoutEffects
|
|
1321
|
+
: { ...withoutEffects, effects: assertResourceEffects(effects) };
|
|
1322
|
+
}
|
|
1323
|
+
static #settleProposalEffects(original, settlement) {
|
|
1324
|
+
const pending = original.attrs
|
|
1325
|
+
?.proposalEffects;
|
|
1326
|
+
if (pending === undefined
|
|
1327
|
+
|| settlement.resolution.decision !== "accept"
|
|
1328
|
+
|| settlement.applied === undefined
|
|
1329
|
+
|| settlement.applied.status >= 300) {
|
|
1330
|
+
return settlement;
|
|
1331
|
+
}
|
|
1332
|
+
const projected = settlement.resolution.result ?? {};
|
|
1333
|
+
const aggregate = settlement.applied.editReceipt;
|
|
1334
|
+
const applied = _a.#appliedEffects({
|
|
1335
|
+
...projected,
|
|
1336
|
+
status: settlement.applied.status,
|
|
1337
|
+
...(aggregate === undefined ? {} : { editReceipt: aggregate }),
|
|
1338
|
+
}, pending);
|
|
1339
|
+
const { status: _status, body: _body, ...result } = applied;
|
|
1340
|
+
const { body: _resolutionBody, ...resolution } = settlement.resolution;
|
|
1341
|
+
return {
|
|
1342
|
+
...settlement,
|
|
1343
|
+
resolution: {
|
|
1344
|
+
...resolution,
|
|
1345
|
+
result,
|
|
1346
|
+
},
|
|
1347
|
+
};
|
|
1348
|
+
}
|
|
1349
|
+
#recordEditSettlement(statement, settlement) {
|
|
1350
|
+
if (statement.op !== "EDIT")
|
|
1351
|
+
return;
|
|
1352
|
+
const prepared = this.#preparedEdits.get(statement);
|
|
1353
|
+
if (prepared === undefined || !prepared.first) {
|
|
1354
|
+
throw new InvalidOperationResultError("An EDIT proposal settled without its prepared batch owner.");
|
|
1355
|
+
}
|
|
1356
|
+
const { resolution, applied } = settlement;
|
|
1357
|
+
if (resolution.decision !== "accept"
|
|
1358
|
+
|| applied === undefined
|
|
1359
|
+
|| applied.status >= 300) {
|
|
1360
|
+
prepared.batch.aggregate = undefined;
|
|
1361
|
+
return;
|
|
1362
|
+
}
|
|
1363
|
+
if (applied.editReceipt === null) {
|
|
1364
|
+
prepared.batch.aggregate = undefined;
|
|
1365
|
+
return;
|
|
1366
|
+
}
|
|
1367
|
+
if (applied.editReceipt !== undefined) {
|
|
1368
|
+
prepared.batch.aggregate = assertEditBatchReceipt(applied.editReceipt);
|
|
1369
|
+
return;
|
|
1370
|
+
}
|
|
1371
|
+
if (resolution.body !== undefined)
|
|
1372
|
+
prepared.batch.aggregate = undefined;
|
|
1373
|
+
}
|
|
1374
|
+
static #settlementEffects(settlement) {
|
|
1375
|
+
const effects = settlement.resolution.result
|
|
1376
|
+
?.effects;
|
|
1377
|
+
return effects === undefined ? [] : assertResourceEffects(effects);
|
|
1378
|
+
}
|
|
1379
|
+
static #withSettlementEffects(settlement, effects) {
|
|
1380
|
+
const projected = (settlement.resolution.result ?? {});
|
|
1381
|
+
const { effects: _effects, ...withoutEffects } = projected;
|
|
1382
|
+
const result = effects.length === 0
|
|
1383
|
+
? withoutEffects
|
|
1384
|
+
: {
|
|
1385
|
+
...withoutEffects,
|
|
1386
|
+
effects: assertResourceEffects(effects),
|
|
1387
|
+
};
|
|
1388
|
+
return {
|
|
1389
|
+
...settlement,
|
|
1390
|
+
resolution: {
|
|
1391
|
+
...settlement.resolution,
|
|
1392
|
+
result,
|
|
1393
|
+
},
|
|
1394
|
+
};
|
|
1395
|
+
}
|
|
1396
|
+
#sameChannel(left, right) {
|
|
1397
|
+
return left.scheme === right.scheme
|
|
1398
|
+
&& left.identityPathname === right.identityPathname
|
|
1399
|
+
&& left.channel === right.channel;
|
|
1400
|
+
}
|
|
1401
|
+
#withProposalRoute(result, selection) {
|
|
1402
|
+
if (result.status !== 202)
|
|
1403
|
+
return result;
|
|
1404
|
+
return {
|
|
1405
|
+
...result,
|
|
1406
|
+
attrs: {
|
|
1407
|
+
...result.attrs,
|
|
1408
|
+
proposalScheme: selection.scheme,
|
|
1409
|
+
proposalTarget: {
|
|
1410
|
+
scheme: selection.scheme,
|
|
1411
|
+
pathname: selection.identityPathname,
|
|
1412
|
+
},
|
|
1413
|
+
},
|
|
1414
|
+
};
|
|
1415
|
+
}
|
|
1416
|
+
async #invokeEditBatch(selection, edits, ctx) {
|
|
1417
|
+
const handler = this.#schemes.get(selection.scheme);
|
|
1418
|
+
if (typeof handler?.editBatch !== "function") {
|
|
1419
|
+
return _a.#failure("operation-not-implemented", 501, `Scheme '${selection.scheme}' does not implement EDIT batches.`, {}, {
|
|
1420
|
+
scheme: selection.scheme,
|
|
1421
|
+
operation: "EDIT",
|
|
1422
|
+
retryable: false,
|
|
1423
|
+
});
|
|
1424
|
+
}
|
|
1425
|
+
const statements = edits.map(({ marker, body, tags, position }) => ({
|
|
1426
|
+
op: "EDIT",
|
|
1427
|
+
suffix: "",
|
|
1428
|
+
signal: tags,
|
|
1429
|
+
target: selection.target,
|
|
1430
|
+
lineMarker: marker,
|
|
1431
|
+
body,
|
|
1432
|
+
position,
|
|
1433
|
+
}));
|
|
1434
|
+
const addressedScheme = selection.target.kind === "url"
|
|
1435
|
+
? selection.target.scheme
|
|
1436
|
+
: selection.scheme;
|
|
1437
|
+
try {
|
|
1438
|
+
const result = Results.assert(await handler.editBatch(statements, new SchemeCtxImpl(ctx, addressedScheme, selection.manifest, this.#liveSubscriptions)));
|
|
1439
|
+
return this.#withProposalRoute(result, selection);
|
|
1440
|
+
}
|
|
1441
|
+
catch (err) {
|
|
1442
|
+
if (err instanceof InvalidOperationResultError)
|
|
1443
|
+
throw err;
|
|
1444
|
+
console.error(`Scheme '${selection.scheme}' COPY/MOVE edit threw outside its operation result contract:`, err);
|
|
1445
|
+
return _a.#failure("scheme-handler-threw", 500, `The '${selection.scheme}' scheme did not produce a COPY/MOVE edit result.`, {}, {
|
|
1446
|
+
stage: "scheme-dispatch",
|
|
1447
|
+
scheme: selection.scheme,
|
|
1448
|
+
operation: "EDIT",
|
|
1449
|
+
});
|
|
1450
|
+
}
|
|
1451
|
+
}
|
|
1452
|
+
async #writeDestination(statement, source, destination, ctx) {
|
|
1453
|
+
const existingResult = await this.#readEntry(destination.scheme, destination.pathname, ctx);
|
|
1454
|
+
if (existingResult.status >= 400 && existingResult.status !== 404) {
|
|
1455
|
+
return existingResult;
|
|
1456
|
+
}
|
|
1457
|
+
const existing = existingResult.status === 200
|
|
1458
|
+
? existingResult.entry
|
|
1459
|
+
: null;
|
|
1460
|
+
if (existingResult.status === 200 && existing === null) {
|
|
1461
|
+
throw new InvalidOperationResultError(`The '${destination.scheme}' scheme returned 200 without a destination entry.`);
|
|
1462
|
+
}
|
|
1463
|
+
const destinationChannel = existing?.channels[destination.channel];
|
|
1464
|
+
const expectedMimetype = destinationChannel?.mimetype
|
|
1465
|
+
?? await PathMimetype.resolveEntryMimetype(destination.pathname, destination.manifest.channels[destination.channel] ?? source.mimetype, ctx.mimetypes);
|
|
1466
|
+
if (source.mimetype !== expectedMimetype) {
|
|
1467
|
+
return _a.#failure("mimetype-mismatch", 415, `COPY or MOVE cannot write '${source.mimetype}' into a '${expectedMimetype}' channel.`, {}, {
|
|
1468
|
+
channel: destination.channel,
|
|
1469
|
+
sourceMimetype: source.mimetype,
|
|
1470
|
+
destinationMimetype: expectedMimetype,
|
|
1471
|
+
retryable: false,
|
|
1472
|
+
});
|
|
1473
|
+
}
|
|
1474
|
+
const tags = Array.isArray(statement.signal) ? statement.signal : [];
|
|
1475
|
+
const destinationEffect = this.#pendingEffect(destination, destinationChannel === undefined ? "create" : "update");
|
|
1476
|
+
if (destination.lineMarker !== null) {
|
|
1477
|
+
if (destinationChannel === undefined) {
|
|
1478
|
+
return _a.#failure("destination-region-not-found", 404, `A destination region requires an existing #${destination.channel} channel.`, {}, {
|
|
1479
|
+
destination: this.#resourceAddress(destination),
|
|
1480
|
+
retryable: false,
|
|
1481
|
+
});
|
|
985
1482
|
}
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1483
|
+
if (await MimetypeBinary.isBinaryMimetype(destinationChannel.mimetype, ctx.mimetypes)) {
|
|
1484
|
+
return _a.#failure("binary-region-unsupported", 415, `Channel #${destination.channel} is binary and cannot receive a textual region.`, {}, {
|
|
1485
|
+
channel: destination.channel,
|
|
1486
|
+
mimetype: destinationChannel.mimetype,
|
|
1487
|
+
retryable: false,
|
|
1488
|
+
});
|
|
1489
|
+
}
|
|
1490
|
+
const edited = await this.#invokeEditBatch(destination, [{
|
|
1491
|
+
marker: destination.lineMarker,
|
|
1492
|
+
body: source.content,
|
|
1493
|
+
tags,
|
|
1494
|
+
position: statement.position,
|
|
1495
|
+
}], ctx);
|
|
1496
|
+
return this.#finalizeEffects(edited, destination, [destinationEffect]);
|
|
1497
|
+
}
|
|
1498
|
+
if (destinationChannel !== undefined
|
|
1499
|
+
&& destinationChannel.content !== source.content) {
|
|
1500
|
+
return _a.#failure("copy-destination-exists", 409, `COPY or MOVE destination ${this.#resourceAddress(destination)} already contains different content.`, {}, {
|
|
1501
|
+
destination: this.#resourceAddress(destination),
|
|
1502
|
+
retryable: false,
|
|
1503
|
+
});
|
|
1504
|
+
}
|
|
1505
|
+
const priorTags = existing?.tags ?? [];
|
|
1506
|
+
const mergedTags = [...new Set([...priorTags, ...tags])];
|
|
1507
|
+
const addsTags = mergedTags.length !== priorTags.length;
|
|
1508
|
+
if (destinationChannel !== undefined && !addsTags)
|
|
1509
|
+
return { status: 304 };
|
|
1510
|
+
const channels = {
|
|
1511
|
+
...(existing?.channels ?? {}),
|
|
1512
|
+
[destination.channel]: {
|
|
1513
|
+
content: source.content,
|
|
1514
|
+
mimetype: source.mimetype,
|
|
1515
|
+
},
|
|
1516
|
+
};
|
|
1517
|
+
const written = await this.#writeEntry(destination.scheme, destination.pathname, { channels, tags: mergedTags }, ctx);
|
|
1518
|
+
const exactWritten = Results.assert(written);
|
|
1519
|
+
const materialized = source.lineMarker === null
|
|
1520
|
+
|| (exactWritten.status !== 200 && exactWritten.status !== 201 && exactWritten.status !== 202)
|
|
1521
|
+
? exactWritten
|
|
1522
|
+
: _a.#withEditMaterialization(exactWritten, editReceipt(destinationChannel?.content ?? "", source.content, [{
|
|
1523
|
+
marker: { marks: [1, -1] },
|
|
1524
|
+
body: source.content,
|
|
1525
|
+
}]));
|
|
1526
|
+
return this.#finalizeEffects(materialized, destination, [destinationEffect]);
|
|
1527
|
+
}
|
|
1528
|
+
async #copyOrchestration({ statement, source, destination, ctx, }) {
|
|
1529
|
+
const resolvedSource = await this.#resolveResourceSelection(source, ctx);
|
|
1530
|
+
if (_a.#isDispatchResult(resolvedSource))
|
|
1531
|
+
return resolvedSource;
|
|
1532
|
+
const resolvedDestination = await this.#resolveResourceSelection(destination, ctx);
|
|
1533
|
+
if (_a.#isDispatchResult(resolvedDestination))
|
|
1534
|
+
return resolvedDestination;
|
|
1535
|
+
const selected = await this.#selectSource(resolvedSource, ctx);
|
|
1536
|
+
if (_a.#isDispatchResult(selected))
|
|
1537
|
+
return selected;
|
|
1538
|
+
const result = await this.#writeDestination(statement, selected, resolvedDestination, ctx);
|
|
1539
|
+
return _a.#prependScopeNormalizations(result, selected.scopeNormalizations);
|
|
1540
|
+
}
|
|
1541
|
+
#deferredMoveSource(source, destination) {
|
|
1542
|
+
return {
|
|
1543
|
+
target: source.target,
|
|
1544
|
+
lineMarker: source.lineMarker,
|
|
1545
|
+
scheme: source.scheme,
|
|
1546
|
+
pathname: source.pathname,
|
|
1547
|
+
channel: source.channel,
|
|
1548
|
+
destination: this.#resourceAddress(destination),
|
|
1549
|
+
};
|
|
1550
|
+
}
|
|
1551
|
+
#moveFailureAfterDestination(result, destination, destinationEffects) {
|
|
1552
|
+
const exact = Results.assert(result);
|
|
1553
|
+
if (exact.status < 400) {
|
|
1554
|
+
throw new InvalidOperationResultError("A successful MOVE source result was classified as a partial failure.");
|
|
1555
|
+
}
|
|
1556
|
+
if (exact.problem === undefined) {
|
|
1557
|
+
throw new InvalidOperationResultError("A failed MOVE source mutation has no Problem Details.");
|
|
1558
|
+
}
|
|
1559
|
+
const failed = Results.assert({
|
|
1560
|
+
...exact,
|
|
1561
|
+
problem: {
|
|
1562
|
+
...exact.problem,
|
|
1563
|
+
operation: "MOVE",
|
|
1564
|
+
destinationWritten: true,
|
|
1565
|
+
destination,
|
|
1566
|
+
},
|
|
1567
|
+
});
|
|
1568
|
+
return _a.#withCombinedEffects(failed, destinationEffects);
|
|
1569
|
+
}
|
|
1570
|
+
async #removeMoveSource(statement, source, ctx) {
|
|
1571
|
+
const effect = this.#pendingEffect(source, source.lineMarker === null ? "delete" : "update");
|
|
1572
|
+
if (source.lineMarker === null) {
|
|
1573
|
+
const deleted = await this.#deleteChannel(source.scheme, source.pathname, source.channel, ctx);
|
|
1574
|
+
return this.#finalizeEffects(Results.assert(deleted), source, [effect]);
|
|
1575
|
+
}
|
|
1576
|
+
const edited = await this.#invokeEditBatch(source, [{
|
|
1577
|
+
marker: source.lineMarker,
|
|
1578
|
+
body: "",
|
|
1579
|
+
tags: null,
|
|
1580
|
+
position: statement.position,
|
|
1581
|
+
}], ctx);
|
|
1582
|
+
return this.#finalizeEffects(edited, source, [effect]);
|
|
1583
|
+
}
|
|
1584
|
+
async #moveWithinChannel(statement, source, destination, ctx) {
|
|
1585
|
+
if (source.lineMarker === null) {
|
|
1586
|
+
if (destination.lineMarker !== null) {
|
|
1587
|
+
return _a.#failure("move-region-overlap", 409, "MOVE cannot insert a whole channel into itself and then remove that channel.", {}, {
|
|
1588
|
+
source: this.#resourceAddress(source),
|
|
1589
|
+
destination: this.#resourceAddress(destination),
|
|
1590
|
+
retryable: false,
|
|
1591
|
+
});
|
|
1592
|
+
}
|
|
1593
|
+
return this.#writeDestination(statement, source, destination, ctx);
|
|
1594
|
+
}
|
|
1595
|
+
if (destination.lineMarker === null) {
|
|
1596
|
+
return this.#writeDestination(statement, source, destination, ctx);
|
|
1597
|
+
}
|
|
1598
|
+
const moved = await this.#invokeEditBatch(destination, [
|
|
1599
|
+
{
|
|
1600
|
+
marker: destination.lineMarker,
|
|
1601
|
+
body: source.content,
|
|
1602
|
+
tags: Array.isArray(statement.signal) ? statement.signal : [],
|
|
1603
|
+
position: statement.position,
|
|
1604
|
+
},
|
|
1605
|
+
{
|
|
1606
|
+
marker: source.lineMarker,
|
|
1607
|
+
body: "",
|
|
1608
|
+
tags: null,
|
|
1609
|
+
position: statement.position,
|
|
1610
|
+
},
|
|
1611
|
+
], ctx);
|
|
1612
|
+
const effect = this.#pendingEffect(destination, "update");
|
|
1613
|
+
return this.#finalizeEffects(moved, destination, [effect, effect]);
|
|
1614
|
+
}
|
|
1615
|
+
async #moveOrchestration({ statement, source, destination, ctx, }) {
|
|
1616
|
+
const resolvedSource = await this.#resolveResourceSelection(source, ctx);
|
|
1617
|
+
if (_a.#isDispatchResult(resolvedSource))
|
|
1618
|
+
return resolvedSource;
|
|
1619
|
+
const resolvedDestination = await this.#resolveResourceSelection(destination, ctx);
|
|
1620
|
+
if (_a.#isDispatchResult(resolvedDestination))
|
|
1621
|
+
return resolvedDestination;
|
|
1622
|
+
const selected = await this.#selectSource(resolvedSource, ctx);
|
|
1623
|
+
if (_a.#isDispatchResult(selected))
|
|
1624
|
+
return selected;
|
|
1625
|
+
if (this.#sameChannel(resolvedSource, resolvedDestination)) {
|
|
1626
|
+
const result = await this.#moveWithinChannel(statement, selected, resolvedDestination, ctx);
|
|
1627
|
+
return resolvedDestination.lineMarker === null
|
|
1628
|
+
? _a.#prependScopeNormalizations(result, selected.scopeNormalizations)
|
|
1629
|
+
: result;
|
|
1630
|
+
}
|
|
1631
|
+
const destinationResult = _a.#prependScopeNormalizations(await this.#writeDestination(statement, selected, resolvedDestination, ctx), selected.scopeNormalizations);
|
|
1632
|
+
if (destinationResult.status >= 400)
|
|
1633
|
+
return destinationResult;
|
|
1634
|
+
const destinationAddress = this.#resourceAddress(resolvedDestination);
|
|
1635
|
+
const destinationEffects = _a.#effectsOf(destinationResult);
|
|
1636
|
+
if (destinationResult.status === 202) {
|
|
1637
|
+
return {
|
|
1638
|
+
...destinationResult,
|
|
1639
|
+
attrs: {
|
|
1640
|
+
...destinationResult.attrs,
|
|
1641
|
+
moveSource: this.#deferredMoveSource(resolvedSource, resolvedDestination),
|
|
1642
|
+
},
|
|
1643
|
+
};
|
|
1644
|
+
}
|
|
1645
|
+
const sourceResult = await this.#removeMoveSource(statement, resolvedSource, ctx);
|
|
1646
|
+
if (sourceResult.status === 202) {
|
|
1647
|
+
return {
|
|
1648
|
+
...sourceResult,
|
|
1649
|
+
...(destinationResult.scopeNormalizations === undefined
|
|
1650
|
+
? {}
|
|
1651
|
+
: { scopeNormalizations: destinationResult.scopeNormalizations }),
|
|
1652
|
+
attrs: {
|
|
1653
|
+
...sourceResult.attrs,
|
|
1654
|
+
moveDestinationWritten: destinationAddress,
|
|
1655
|
+
moveDestinationEffects: destinationEffects,
|
|
1656
|
+
},
|
|
1657
|
+
};
|
|
1658
|
+
}
|
|
1659
|
+
if (sourceResult.status >= 400) {
|
|
1660
|
+
return this.#moveFailureAfterDestination(destinationResult.scopeNormalizations === undefined
|
|
1661
|
+
? sourceResult
|
|
1662
|
+
: Results.assert({
|
|
1663
|
+
...sourceResult,
|
|
1664
|
+
scopeNormalizations: destinationResult.scopeNormalizations,
|
|
1665
|
+
}), destinationAddress, destinationEffects);
|
|
1666
|
+
}
|
|
1667
|
+
const base = destinationResult.status === 304
|
|
1668
|
+
? { ...destinationResult, status: 200 }
|
|
1669
|
+
: destinationResult;
|
|
1670
|
+
return _a.#withCombinedEffects(base, _a.#effectsOf(sourceResult));
|
|
1671
|
+
}
|
|
1672
|
+
async #settleMoveProposal({ statement, result, settlement, ctx, ids, }) {
|
|
1673
|
+
if (statement.op !== "MOVE")
|
|
1674
|
+
return settlement;
|
|
1675
|
+
const attrs = result.attrs;
|
|
1676
|
+
const destinationWritten = attrs?.moveDestinationWritten;
|
|
1677
|
+
if (destinationWritten !== undefined) {
|
|
1678
|
+
const destinationEffects = attrs?.moveDestinationEffects === undefined
|
|
1679
|
+
? []
|
|
1680
|
+
: assertResourceEffects(attrs.moveDestinationEffects);
|
|
1681
|
+
if (settlement.resolution.decision !== "accept") {
|
|
1682
|
+
const decision = settlement.resolution.decision;
|
|
1683
|
+
return {
|
|
1684
|
+
resolution: settlement.resolution,
|
|
1685
|
+
applied: this.#moveFailureAfterDestination(_a.#failure("move-source-not-applied", decision === "cancel" ? 499 : 409, `The MOVE destination was written, but source removal was ${decision === "cancel" ? "cancelled" : "rejected"}.`, {}, {
|
|
1686
|
+
retryable: false,
|
|
1687
|
+
}), destinationWritten, destinationEffects),
|
|
1688
|
+
};
|
|
1689
|
+
}
|
|
1690
|
+
if (settlement.applied === undefined) {
|
|
1691
|
+
return {
|
|
1692
|
+
resolution: settlement.resolution,
|
|
1693
|
+
applied: this.#moveFailureAfterDestination(_a.#failure("proposal-apply-missing", 500, "The source scheme accepted its MOVE proposal without applying the source mutation.", {}, {
|
|
1694
|
+
stage: "proposal-application",
|
|
1695
|
+
retryable: false,
|
|
1696
|
+
}), destinationWritten, destinationEffects),
|
|
1697
|
+
};
|
|
1698
|
+
}
|
|
1699
|
+
if (settlement.applied.status >= 400) {
|
|
1700
|
+
return {
|
|
1701
|
+
resolution: settlement.resolution,
|
|
1702
|
+
applied: this.#moveFailureAfterDestination(settlement.applied, destinationWritten, destinationEffects),
|
|
1703
|
+
};
|
|
1704
|
+
}
|
|
1705
|
+
return _a.#withSettlementEffects(settlement, [
|
|
1706
|
+
...destinationEffects,
|
|
1707
|
+
..._a.#settlementEffects(settlement),
|
|
1708
|
+
]);
|
|
1709
|
+
}
|
|
1710
|
+
const deferred = attrs?.moveSource;
|
|
1711
|
+
if (deferred === undefined
|
|
1712
|
+
|| settlement.resolution.decision !== "accept"
|
|
1713
|
+
|| (settlement.applied?.status ?? 200) >= 400) {
|
|
1714
|
+
return settlement;
|
|
1715
|
+
}
|
|
1716
|
+
if (settlement.applied === undefined) {
|
|
1717
|
+
return {
|
|
1718
|
+
resolution: settlement.resolution,
|
|
1719
|
+
applied: _a.#failure("proposal-apply-missing", 500, "The destination scheme accepted its MOVE proposal without applying the destination mutation.", {}, {
|
|
1720
|
+
stage: "proposal-application",
|
|
1721
|
+
retryable: false,
|
|
1722
|
+
}),
|
|
1723
|
+
};
|
|
1724
|
+
}
|
|
1725
|
+
const destinationEffects = _a.#settlementEffects(settlement);
|
|
1726
|
+
const resolvedSource = await this.#resolveResourceSelection({
|
|
1727
|
+
target: deferred.target,
|
|
1728
|
+
lineMarker: deferred.lineMarker,
|
|
1729
|
+
}, ctx);
|
|
1730
|
+
if (_a.#isDispatchResult(resolvedSource)) {
|
|
1731
|
+
return {
|
|
1732
|
+
resolution: settlement.resolution,
|
|
1733
|
+
applied: this.#moveFailureAfterDestination(resolvedSource, deferred.destination, destinationEffects),
|
|
1734
|
+
};
|
|
1735
|
+
}
|
|
1736
|
+
if (resolvedSource.scheme !== deferred.scheme
|
|
1737
|
+
|| resolvedSource.pathname !== deferred.pathname
|
|
1738
|
+
|| resolvedSource.channel !== deferred.channel) {
|
|
1739
|
+
throw new InvalidOperationResultError("A deferred MOVE source no longer resolves to its recorded identity.");
|
|
1740
|
+
}
|
|
1741
|
+
const removed = await this.#removeMoveSource(statement, resolvedSource, ctx);
|
|
1742
|
+
if (removed.status >= 400) {
|
|
1743
|
+
return {
|
|
1744
|
+
resolution: settlement.resolution,
|
|
1745
|
+
applied: this.#moveFailureAfterDestination(removed, deferred.destination, destinationEffects),
|
|
1746
|
+
};
|
|
1747
|
+
}
|
|
1748
|
+
if (removed.status !== 202) {
|
|
1749
|
+
return _a.#withSettlementEffects(settlement, [
|
|
1750
|
+
...destinationEffects,
|
|
1751
|
+
..._a.#effectsOf(removed),
|
|
1752
|
+
]);
|
|
1753
|
+
}
|
|
1754
|
+
const initialSourceSettlement = await this.#proposals.workerApply(statement, removed, { decision: "accept" }, ids);
|
|
1755
|
+
const sourceSettlement = _a.#settleProposalEffects(removed, initialSourceSettlement);
|
|
1756
|
+
if (sourceSettlement.applied === undefined) {
|
|
1757
|
+
return {
|
|
1758
|
+
resolution: settlement.resolution,
|
|
1759
|
+
applied: this.#moveFailureAfterDestination(_a.#failure("proposal-apply-missing", 500, "The source scheme accepted its MOVE proposal without applying the source mutation.", {}, {
|
|
1760
|
+
stage: "proposal-application",
|
|
1761
|
+
retryable: false,
|
|
1762
|
+
}), deferred.destination, destinationEffects),
|
|
1763
|
+
};
|
|
1764
|
+
}
|
|
1765
|
+
if (sourceSettlement.applied.status >= 400) {
|
|
1766
|
+
return {
|
|
1767
|
+
resolution: settlement.resolution,
|
|
1768
|
+
applied: this.#moveFailureAfterDestination(sourceSettlement.applied, deferred.destination, destinationEffects),
|
|
1769
|
+
};
|
|
1770
|
+
}
|
|
1771
|
+
return _a.#withSettlementEffects(settlement, [
|
|
1772
|
+
...destinationEffects,
|
|
1773
|
+
..._a.#settlementEffects(sourceSettlement),
|
|
1774
|
+
]);
|
|
1775
|
+
}
|
|
1776
|
+
// {§send-premature-terminate} — the unified PENDING SET, judged at the terminal's OWN dispatch
|
|
1015
1777
|
// (post-batch: the emission's earlier ops already executed, so a same-turn KILL+[200] repairs in
|
|
1016
1778
|
// ONE turn, and a same-turn WORK+[200] is caught — the spawn is live by the time the SEND lands).
|
|
1017
1779
|
// pending = open streams ∪ live children ∪ THIS turn's retrievals (READ/FIND/OPEN, results unseen
|
|
1018
|
-
// until next packet).
|
|
1019
|
-
//
|
|
1780
|
+
// until next packet). Failed operations are the separate next-packet leg shared by explicit
|
|
1781
|
+
// completion and empty-join completion. Nothing pending may be silently discarded; 499 discards
|
|
1782
|
+
// BY STATED INTENT and is never gated.
|
|
1020
1783
|
async #pendingSet(workerId, turnId) {
|
|
1021
1784
|
const pending = [];
|
|
1022
1785
|
const openSubs = await this.#db.find_open_subscriptions_for_worker.all({ worker_id: workerId });
|
|
@@ -1046,7 +1809,7 @@ class Dispatcher {
|
|
|
1046
1809
|
this.#db.engine_worker_has_undelivered_stream_term
|
|
1047
1810
|
.get({ worker_id: workerId }),
|
|
1048
1811
|
this.#db.engine_worker_has_undelivered_child_term
|
|
1049
|
-
.get({ worker_id: workerId
|
|
1812
|
+
.get({ worker_id: workerId }),
|
|
1050
1813
|
]);
|
|
1051
1814
|
return {
|
|
1052
1815
|
retrievals: retrievals.length > 0,
|
|
@@ -1054,8 +1817,24 @@ class Dispatcher {
|
|
|
1054
1817
|
childTerminations: childTermination !== undefined,
|
|
1055
1818
|
};
|
|
1056
1819
|
}
|
|
1820
|
+
// A failed operation is also an unobserved result: it does not enter the
|
|
1821
|
+
// model's Log until the next packet. Both explicit completion and an
|
|
1822
|
+
// already-drained join must cross that observation boundary before they
|
|
1823
|
+
// can honestly finish the loop.
|
|
1824
|
+
async #unobservedFailureCount(turnId) {
|
|
1825
|
+
const failedRows = await this.#db.engine_turn_failures.all({ turn_id: turnId });
|
|
1826
|
+
return failedRows.length;
|
|
1827
|
+
}
|
|
1828
|
+
static #unobservedFailures(failCount) {
|
|
1829
|
+
return _a.#failure("unobserved-failures", 409, `Completion was refused because ${failCount} failed operation(s) from this turn have not been observed.`, {}, {
|
|
1830
|
+
failures: failCount,
|
|
1831
|
+
stage: "completion",
|
|
1832
|
+
recovery: "Review the failed log items before concluding.",
|
|
1833
|
+
retryable: false,
|
|
1834
|
+
});
|
|
1835
|
+
}
|
|
1057
1836
|
// J — a live obligation to WAIT on: a spawned child or an open stream (NOT retrievals, which land
|
|
1058
|
-
// next turn regardless). The wait-side twin of #pendingSet's stream+child legs (§wait-obligation-matrix).
|
|
1837
|
+
// next turn regardless). The wait-side twin of #pendingSet's stream+child legs ({§wait-obligation-matrix}).
|
|
1059
1838
|
async #hasLiveWork(workerId) {
|
|
1060
1839
|
const openSubs = await this.#db.find_open_subscriptions_for_worker.all({ worker_id: workerId });
|
|
1061
1840
|
if (openSubs.length > 0)
|
|
@@ -1071,40 +1850,42 @@ class Dispatcher {
|
|
|
1071
1850
|
throw new Error("unreachable");
|
|
1072
1851
|
const { workerId, loopId, turnId } = ctx;
|
|
1073
1852
|
const status = statement.signal;
|
|
1074
|
-
if (status === null)
|
|
1075
|
-
return {
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
//
|
|
1079
|
-
//
|
|
1080
|
-
//
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1853
|
+
if (status === null) {
|
|
1854
|
+
return _a.#failure("send-status-required", 400, "SEND requires a numeric status.", {}, { retryable: false });
|
|
1855
|
+
}
|
|
1856
|
+
const raw = statement.body === null ? "" : statement.body.raw;
|
|
1857
|
+
// The park rides SEND[202] only ({§park-202-only}). A scoped SEND[102] is neither
|
|
1858
|
+
// a wait nor a meaningful continuation, so reject it instead of preserving the
|
|
1859
|
+
// retired dual spelling.
|
|
1860
|
+
if (status === 102 && statement.lineMarker !== null) {
|
|
1861
|
+
return _a.#failure("send-scope-invalid", 400, "SEND[102] does not accept a scope.", {}, {
|
|
1862
|
+
requestedStatus: 102,
|
|
1863
|
+
scope: statement.lineMarker,
|
|
1864
|
+
recovery: "Use SEND[202] with a scope to wait, or remove the scope to continue.",
|
|
1865
|
+
retryable: false,
|
|
1866
|
+
});
|
|
1867
|
+
}
|
|
1868
|
+
// A bare continue after an armed running-worker READ becomes an
|
|
1869
|
+
// indefinite park. {§join-blocking-collect}
|
|
1088
1870
|
const joinArmed = this.#joinTargets.delete(loopId);
|
|
1089
1871
|
if (status === 102 && statement.lineMarker === null && joinArmed) {
|
|
1090
|
-
if (!await this.#lifecycle.park(loopId
|
|
1091
|
-
return
|
|
1872
|
+
if (!await this.#lifecycle.park(loopId)) {
|
|
1873
|
+
return _a.#statusResult(await this.#lifecycle.status(loopId), "loop-already-terminal", "The loop was already terminal when SEND attempted to park it.");
|
|
1092
1874
|
}
|
|
1093
1875
|
this.#parkDeadlines.set(loopId, -1); // indefinite: the bounded child's terminal is the wake edge
|
|
1094
1876
|
return { status: 102, attrs: { parked: -1, join: true } };
|
|
1095
1877
|
}
|
|
1096
|
-
// §wait-obligation-matrix — SEND[202]
|
|
1097
|
-
// obligation-checked join. A live
|
|
1878
|
+
// {§wait-obligation-matrix} — SEND[202] is the obligation-checked join. A live
|
|
1098
1879
|
// obligation (a spawned child or open stream, J) BLOCKS the loop until it concludes and
|
|
1099
|
-
// reawakens it (§worker-lifecycle-child-wake); a wait on nothing (∅) is already satisfied and
|
|
1880
|
+
// reawakens it ({§worker-lifecycle-child-wake}); a wait on nothing (∅) is already satisfied and
|
|
1100
1881
|
// resolves like 200, so <-1>+∅ self-resolves rather than hang the agent; a pending own
|
|
1101
1882
|
// retrieval (R) just lands next turn, so the wait continues.
|
|
1102
|
-
if (status === 202
|
|
1883
|
+
if (status === 202) {
|
|
1103
1884
|
const marks = statement.lineMarker?.marks[0];
|
|
1104
1885
|
const seconds = typeof marks === "number" ? marks : -1; // bare 202 / absent T = indefinite, bounded by the join
|
|
1105
1886
|
if (await this.#hasLiveWork(workerId)) {
|
|
1106
|
-
if (!await this.#lifecycle.park(loopId
|
|
1107
|
-
return
|
|
1887
|
+
if (!await this.#lifecycle.park(loopId)) {
|
|
1888
|
+
return _a.#statusResult(await this.#lifecycle.status(loopId), "loop-already-terminal", "The loop was already terminal when SEND attempted to wait.");
|
|
1108
1889
|
}
|
|
1109
1890
|
this.#parkDeadlines.set(loopId, seconds);
|
|
1110
1891
|
return { status: 202, attrs: { waiting: seconds } };
|
|
@@ -1117,15 +1898,21 @@ class Dispatcher {
|
|
|
1117
1898
|
if (observations.retrievals || observations.streamTerminations || observations.childTerminations) {
|
|
1118
1899
|
return { status: 102 };
|
|
1119
1900
|
}
|
|
1901
|
+
const failCount = await this.#unobservedFailureCount(turnId);
|
|
1902
|
+
if (failCount > 0)
|
|
1903
|
+
return _a.#unobservedFailures(failCount);
|
|
1904
|
+
const branchDenial = await this.#branchCompletionGate?.(workerId) ?? null;
|
|
1905
|
+
if (branchDenial !== null)
|
|
1906
|
+
return branchDenial;
|
|
1120
1907
|
// The joined set is already drained. Awaiting an empty task group completes
|
|
1121
1908
|
// immediately; it never parks and needs no corrective model turn.
|
|
1122
|
-
const finished = await this.#lifecycle.finish(loopId,
|
|
1909
|
+
const finished = await this.#lifecycle.finish(loopId, TerminalResult.success(raw));
|
|
1123
1910
|
return {
|
|
1124
|
-
status: finished ? 200 : await this.#lifecycle.status(loopId),
|
|
1911
|
+
status: finished !== null ? 200 : await this.#lifecycle.status(loopId),
|
|
1125
1912
|
attrs: { joined: true, pending: 0 },
|
|
1126
1913
|
};
|
|
1127
1914
|
}
|
|
1128
|
-
// §send-300-choices — ask the operator and park (the answer returns via loop.inject).
|
|
1915
|
+
// {§send-300-choices} — ask the operator and park (the answer returns via loop.inject).
|
|
1129
1916
|
// The three-state cascade (owner design): PLURNK_QUESTIONS unset = ALLOWED (not enabled);
|
|
1130
1917
|
// =0 = DENIED servicewide (a ceiling the client cannot override); ENABLED requires the
|
|
1131
1918
|
// client to affirmatively pass it per workspace (settings.questions — the interactive client
|
|
@@ -1134,14 +1921,13 @@ class Dispatcher {
|
|
|
1134
1921
|
// Disabled → refused with a self-decide steer, never a park into the void.
|
|
1135
1922
|
if (status === 300) {
|
|
1136
1923
|
if (!(await WorkspaceSettings.questionsEnabled(this.#db, ctx.workspaceId))) {
|
|
1137
|
-
return
|
|
1924
|
+
return _a.#failure("operator-questions-disabled", 409, "Operator questions are disabled in this workspace.", {}, {
|
|
1925
|
+
recovery: "Decide from the available evidence and continue or conclude.",
|
|
1926
|
+
retryable: false,
|
|
1927
|
+
});
|
|
1138
1928
|
}
|
|
1139
|
-
//
|
|
1140
|
-
//
|
|
1141
|
-
// (#isProposal admits signal-300 SENDs): loop/proposal carries {question, choices},
|
|
1142
|
-
// loop.resolve's accept body IS the answer, and applyResolution writes it into the
|
|
1143
|
-
// model-facing rx — the answer arrives as the result of the ask itself. Timeout is
|
|
1144
|
-
// the standard §proposal-timeout-cancels. Zero options = an open question.
|
|
1929
|
+
// {§send-300-choices} — return a proposal whose accepted body becomes
|
|
1930
|
+
// the answer; zero choices denotes an open question.
|
|
1145
1931
|
const parts = raw.split(";").map((x) => x.trim()).filter((x) => x.length > 0);
|
|
1146
1932
|
const [question = "", ...choices] = parts;
|
|
1147
1933
|
return { status: 202, attrs: choices.length > 0 ? { question, choices } : { question } };
|
|
@@ -1150,105 +1936,261 @@ class Dispatcher {
|
|
|
1150
1936
|
// attempt faithfully (status_rx=409, never erased); the loop stays a continue; the strike
|
|
1151
1937
|
// couples in runTurn. [499] abandons and cancels the descendant scope.
|
|
1152
1938
|
if (status === 200) {
|
|
1153
|
-
// §send-
|
|
1154
|
-
//
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
// failure IS weighing it.
|
|
1159
|
-
const failedRows = await this.#db.engine_turn_failures.all({ turn_id: turnId });
|
|
1160
|
-
const failCount = failedRows.length + (ctx.turnParseErrors ?? 0);
|
|
1161
|
-
if (failCount > 0) {
|
|
1162
|
-
return { status: 409, error: `Termination attempted despite ${failCount} failed operation(s) this turn. The errors land in your log next turn — weigh them, then conclude (or SEND[499] to abandon).` };
|
|
1163
|
-
}
|
|
1939
|
+
// {§send-premature-terminate} — same-turn failures are unobserved
|
|
1940
|
+
// pending results and therefore refuse completion.
|
|
1941
|
+
const failCount = await this.#unobservedFailureCount(turnId);
|
|
1942
|
+
if (failCount > 0)
|
|
1943
|
+
return _a.#unobservedFailures(failCount);
|
|
1164
1944
|
const pending = await this.#pendingSet(workerId, turnId);
|
|
1165
1945
|
if (pending.length > 0) {
|
|
1166
|
-
//
|
|
1167
|
-
//
|
|
1168
|
-
// KILL/park advice only muddies it. Streams/children keep the remedy steer.
|
|
1946
|
+
// A retrieval-only refusal needs no KILL/park remedy menu: the results simply
|
|
1947
|
+
// arrive in the next packet. Streams and children retain their remedy steer.
|
|
1169
1948
|
const retrievalsOnly = pending.every((k) => k.startsWith("this turn's retrieval results"));
|
|
1170
1949
|
if (retrievalsOnly) {
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
// FORCE an additional turn — not blame, physics), and prescribes the concluding
|
|
1178
|
-
// emission's legal SHAPE (PLAN + SEND[200] only — no room for the justify-READ
|
|
1179
|
-
// that re-armed this gate four times while she held the correct answer).
|
|
1180
|
-
return { status: 409, error: "Last turn both performed retrieval operations and attempted to terminate. Retrieval operations force an additional turn to receive results for review and reaction. To conclude, only use PLAN and SEND[200] operations.", attrs: { retrievalOnly: true } };
|
|
1950
|
+
return _a.#failure("retrieval-results-unobserved", 409, "Last turn both performed retrieval operations and attempted to terminate. Retrieval operations force an additional turn so their results can be reviewed.", {}, {
|
|
1951
|
+
pending: [...pending],
|
|
1952
|
+
stage: "completion",
|
|
1953
|
+
recovery: "Review the results, then use only PLAN and SEND[200] to conclude.",
|
|
1954
|
+
retryable: false,
|
|
1955
|
+
});
|
|
1181
1956
|
}
|
|
1182
|
-
return
|
|
1957
|
+
return _a.#failure("work-remains", 409, `Completion was refused while work remains: ${pending.join("; ")}.`, {}, {
|
|
1958
|
+
pending: [...pending],
|
|
1959
|
+
stage: "completion",
|
|
1960
|
+
recovery: "Resolve the listed pending work before concluding.",
|
|
1961
|
+
retryable: false,
|
|
1962
|
+
});
|
|
1183
1963
|
}
|
|
1184
|
-
const
|
|
1185
|
-
|
|
1964
|
+
const branchDenial = await this.#branchCompletionGate?.(workerId) ?? null;
|
|
1965
|
+
if (branchDenial !== null)
|
|
1966
|
+
return branchDenial;
|
|
1967
|
+
const finished = await this.#lifecycle.finish(loopId, TerminalResult.success(raw));
|
|
1968
|
+
return _a.#statusResult(finished !== null ? 200 : await this.#lifecycle.status(loopId), "loop-already-terminal", "The loop was already terminal when SEND attempted to conclude it.");
|
|
1186
1969
|
}
|
|
1187
1970
|
if (status === 499) {
|
|
1188
|
-
const
|
|
1189
|
-
if (
|
|
1190
|
-
return
|
|
1971
|
+
const branchDenial = await this.#branchCompletionGate?.(workerId) ?? null;
|
|
1972
|
+
if (branchDenial !== null)
|
|
1973
|
+
return branchDenial;
|
|
1974
|
+
const failure = _a.#failure("scope-abandoned", 499, raw === "" ? "The worker abandoned its scope." : raw);
|
|
1975
|
+
const seqs = await this.#db.engine_loop_turn_seqs.get({
|
|
1976
|
+
loop_id: loopId,
|
|
1977
|
+
turn_id: turnId,
|
|
1978
|
+
});
|
|
1979
|
+
if (seqs === undefined) {
|
|
1980
|
+
throw new Error(`SEND[499]: no coordinate for loop=${loopId} turn=${turnId}`);
|
|
1981
|
+
}
|
|
1982
|
+
Results.attachInstance(failure, `log:///${seqs.loop_seq}/${seqs.turn_seq}/${ctx.sequence}/SEND`);
|
|
1983
|
+
const finished = await this.#lifecycle.finish(loopId, failure);
|
|
1984
|
+
if (finished === null)
|
|
1985
|
+
return _a.#statusResult(await this.#lifecycle.status(loopId), "loop-already-terminal", "The loop was already terminal when SEND attempted to abandon it.");
|
|
1191
1986
|
await this.#cancelDescendants?.(workerId, raw === "" ? "parent abandoned its scope" : raw);
|
|
1192
|
-
return
|
|
1987
|
+
return failure;
|
|
1193
1988
|
}
|
|
1194
1989
|
// Every other signal — 102 bare, 202 (retired as a terminal; now ordinary mid-comms), 1xx —
|
|
1195
1990
|
// is a plain broadcast row: no loop transition.
|
|
1196
|
-
return
|
|
1991
|
+
return _a.#statusResult(status, "send-broadcast-failed", raw === "" ? `SEND broadcast reported status ${status}.` : raw);
|
|
1197
1992
|
}
|
|
1198
1993
|
async #run(schemeName, statement, ctx) {
|
|
1199
|
-
if (schemeName === null)
|
|
1200
|
-
|
|
1994
|
+
if (schemeName === null) {
|
|
1995
|
+
const fields = statement.op === "READ"
|
|
1996
|
+
? { content: null, mimetype: null, channel: null }
|
|
1997
|
+
: statement.op === "FIND"
|
|
1998
|
+
? {
|
|
1999
|
+
content: null,
|
|
2000
|
+
mimetype: null,
|
|
2001
|
+
results: [],
|
|
2002
|
+
itemsTokenTotal: 0,
|
|
2003
|
+
returnedItemsTokenTotal: 0,
|
|
2004
|
+
matchingPathCount: 0,
|
|
2005
|
+
matchLocationCount: 0,
|
|
2006
|
+
}
|
|
2007
|
+
: {};
|
|
2008
|
+
return _a.#failure("target-scheme-required", 400, `${statement.op} requires a target scheme.`, fields, { operation: statement.op, retryable: false });
|
|
2009
|
+
}
|
|
1201
2010
|
if (statement.op === "SEND" && statement.signal === 499 && statement.target?.kind === "url") {
|
|
2011
|
+
const addressedScheme = statement.target.scheme;
|
|
1202
2012
|
const entry = await this.#db.crud_find_workspace_entry.get({
|
|
1203
2013
|
workspace_id: ctx.workspaceId,
|
|
1204
2014
|
owner_id: await Owner.commonsId(this.#db, ctx.workspaceId),
|
|
1205
|
-
scheme:
|
|
1206
|
-
pathname:
|
|
2015
|
+
scheme: addressedScheme,
|
|
2016
|
+
pathname: entryPathnameOf(statement.target),
|
|
1207
2017
|
});
|
|
1208
2018
|
if (entry !== undefined) {
|
|
1209
2019
|
const subscription = await ChannelWrite.findActiveSubscription(this.#db, {
|
|
1210
2020
|
workerId: ctx.workerId,
|
|
1211
2021
|
entryId: entry.id,
|
|
1212
2022
|
});
|
|
1213
|
-
if (subscription !== null && subscription.scheme ===
|
|
2023
|
+
if (subscription !== null && subscription.scheme === addressedScheme) {
|
|
1214
2024
|
const cancelled = await this.#liveSubscriptions.cancel(subscription.id);
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
2025
|
+
if (!cancelled) {
|
|
2026
|
+
throw new InvalidOperationResultError(`Subscription ${subscription.id} is durable but has no live cancellation handle.`);
|
|
2027
|
+
}
|
|
2028
|
+
return { status: 200 };
|
|
1218
2029
|
}
|
|
1219
2030
|
}
|
|
1220
2031
|
}
|
|
1221
2032
|
const handler = this.#schemes.get(schemeName);
|
|
1222
|
-
if (handler === undefined)
|
|
1223
|
-
return {
|
|
2033
|
+
if (handler === undefined) {
|
|
2034
|
+
return _a.#failure("scheme-not-found", 501, `Scheme '${schemeName}' is not registered.`, {}, { scheme: schemeName, retryable: false });
|
|
2035
|
+
}
|
|
1224
2036
|
const methodName = statement.op.toLowerCase();
|
|
1225
2037
|
const method = handler[methodName];
|
|
1226
2038
|
const addressedScheme = statement.target?.kind === "url" ? statement.target.scheme : null;
|
|
1227
2039
|
const manifest = this.#schemes.manifestFor(schemeName);
|
|
1228
2040
|
if (manifest === undefined)
|
|
1229
2041
|
throw new Error(`scheme '${schemeName}' has no manifest`);
|
|
1230
|
-
const
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
2042
|
+
const publishedChannel = statement.target?.kind === "url"
|
|
2043
|
+
? statement.target.fragment ?? manifest.defaultChannel
|
|
2044
|
+
: manifest.defaultChannel;
|
|
2045
|
+
const schemeCtx = new SchemeCtxImpl(ctx, addressedScheme ?? schemeName, manifest, this.#liveSubscriptions, { publishedChannel });
|
|
2046
|
+
if (statement.op === "READ"
|
|
2047
|
+
&& handler instanceof CoreSchemeAdapterBase
|
|
2048
|
+
&& typeof handler.resolveCoreRepresentation === "function") {
|
|
2049
|
+
const selectionNeutralTarget = statement.target?.kind === "url"
|
|
2050
|
+
? {
|
|
2051
|
+
...statement.target,
|
|
2052
|
+
raw: renderTarget({ ...statement.target, fragment: null }) ?? statement.target.raw,
|
|
2053
|
+
fragment: null,
|
|
2054
|
+
}
|
|
2055
|
+
: statement.target;
|
|
2056
|
+
const resolved = await handler
|
|
2057
|
+
.resolveCoreRepresentation(selectionNeutralTarget, schemeCtx);
|
|
2058
|
+
if ("result" in resolved)
|
|
2059
|
+
return Results.assertReadResult(resolved.result);
|
|
2060
|
+
if (selectionNeutralTarget === null) {
|
|
2061
|
+
throw new InvalidOperationResultError(`Core scheme '${schemeName}' resolved a targetless READ representation.`);
|
|
2062
|
+
}
|
|
2063
|
+
const target = renderTarget(selectionNeutralTarget.kind === "url"
|
|
2064
|
+
? selectionNeutralTarget
|
|
2065
|
+
: { scheme: null, pathname: selectionNeutralTarget.raw });
|
|
2066
|
+
if (target === null) {
|
|
2067
|
+
throw new TypeError(`Core scheme '${schemeName}' resolved an unrenderable READ target.`);
|
|
2068
|
+
}
|
|
2069
|
+
return Results.assertReadResult(await ReadProjector.project({
|
|
2070
|
+
statement,
|
|
2071
|
+
manifest,
|
|
2072
|
+
target,
|
|
2073
|
+
representation: resolved.representation,
|
|
2074
|
+
mimetypes: ctx.mimetypes,
|
|
2075
|
+
}));
|
|
2076
|
+
}
|
|
2077
|
+
if (statement.op === "READ" && manifest.category === "data") {
|
|
2078
|
+
const prepared = statement.target === null
|
|
2079
|
+
? { address: null, result: null }
|
|
2080
|
+
: await this.#prepareDataRepresentation({
|
|
2081
|
+
target: statement.target,
|
|
2082
|
+
routedScheme: schemeName,
|
|
2083
|
+
handler: handler,
|
|
2084
|
+
manifest,
|
|
2085
|
+
ctx,
|
|
2086
|
+
schemeCtx,
|
|
2087
|
+
publishedChannel,
|
|
2088
|
+
});
|
|
2089
|
+
if (prepared.result !== null)
|
|
2090
|
+
return prepared.result;
|
|
2091
|
+
const resolved = prepared.address;
|
|
2092
|
+
if (statement.target !== null && resolved === null) {
|
|
2093
|
+
const target = renderTarget(statement.target.kind === "url"
|
|
2094
|
+
? statement.target
|
|
2095
|
+
: { scheme: null, pathname: statement.target.raw });
|
|
2096
|
+
return Results.failure(`scheme:${schemeName}`, "entry-not-found", 404, `No entry exists at ${target ?? "the requested address"}.`, { content: null, mimetype: null, channel: null }, { target });
|
|
2097
|
+
}
|
|
2098
|
+
const storageScheme = resolved?.scheme ?? manifest.storedScheme ?? addressedScheme ?? schemeName;
|
|
2099
|
+
const projected = Results.assertReadResult(await EntryOps.readWorkspaceEntry(statement, ctx, { ...manifest, name: storageScheme, storedScheme: storageScheme }, resolved === null
|
|
2100
|
+
? {}
|
|
2101
|
+
: {
|
|
2102
|
+
ownerId: resolved.ownerId,
|
|
2103
|
+
pathname: resolved.pathname,
|
|
2104
|
+
}));
|
|
2105
|
+
return projected;
|
|
2106
|
+
}
|
|
2107
|
+
if (statement.op !== "FIND" || manifest.category !== "data") {
|
|
2108
|
+
if (typeof method === "function") {
|
|
2109
|
+
return Results.assert(await method.call(handler, statement, schemeCtx));
|
|
2110
|
+
}
|
|
2111
|
+
return _a.#failure("operation-not-implemented", 501, `Scheme '${schemeName}' does not implement ${statement.op}.`, {}, {
|
|
2112
|
+
scheme: schemeName,
|
|
2113
|
+
operation: statement.op,
|
|
2114
|
+
retryable: false,
|
|
2115
|
+
});
|
|
2116
|
+
}
|
|
2117
|
+
const targetPathname = statement.target?.kind === "url"
|
|
2118
|
+
? statement.target.pathname
|
|
2119
|
+
: statement.target?.raw ?? "";
|
|
2120
|
+
const collectionTarget = manifest.folderScopes === true
|
|
2121
|
+
&& (targetPathname === "" || targetPathname.endsWith("/"));
|
|
2122
|
+
const exactTarget = statement.target !== null
|
|
2123
|
+
&& !collectionTarget
|
|
2124
|
+
&& !PathSyntax.hasGlob(entryPathnameOf(statement.target));
|
|
2125
|
+
if (exactTarget && statement.target !== null) {
|
|
2126
|
+
const prepared = await this.#prepareDataRepresentation({
|
|
2127
|
+
target: statement.target,
|
|
2128
|
+
routedScheme: schemeName,
|
|
2129
|
+
handler: handler,
|
|
2130
|
+
manifest,
|
|
2131
|
+
ctx,
|
|
2132
|
+
schemeCtx,
|
|
2133
|
+
publishedChannel,
|
|
2134
|
+
});
|
|
2135
|
+
if (prepared.result !== null)
|
|
2136
|
+
return prepared.result;
|
|
2137
|
+
if (prepared.address === null) {
|
|
2138
|
+
const target = renderTarget(statement.target.kind === "url"
|
|
2139
|
+
? statement.target
|
|
2140
|
+
: { scheme: null, pathname: statement.target.raw });
|
|
2141
|
+
return Results.failure(`scheme:${schemeName}`, "entry-not-found", 404, `No entry exists at ${target ?? "the requested address"}.`, {
|
|
2142
|
+
content: null,
|
|
2143
|
+
mimetype: null,
|
|
2144
|
+
results: [],
|
|
2145
|
+
itemsTokenTotal: 0,
|
|
2146
|
+
returnedItemsTokenTotal: 0,
|
|
2147
|
+
matchingPathCount: 0,
|
|
2148
|
+
matchLocationCount: 0,
|
|
2149
|
+
}, { target });
|
|
2150
|
+
}
|
|
2151
|
+
const storageScheme = prepared.address.scheme
|
|
2152
|
+
?? manifest.storedScheme
|
|
2153
|
+
?? addressedScheme
|
|
2154
|
+
?? schemeName;
|
|
2155
|
+
return Results.assert(await EntryFind.findWorkspaceEntries(statement, ctx, { ...manifest, name: storageScheme, storedScheme: storageScheme }, {
|
|
2156
|
+
ownerId: prepared.address.ownerId,
|
|
2157
|
+
pathname: prepared.address.pathname,
|
|
2158
|
+
}));
|
|
2159
|
+
}
|
|
2160
|
+
if (typeof method === "function") {
|
|
2161
|
+
return Results.assert(await method.call(handler, statement, schemeCtx));
|
|
2162
|
+
}
|
|
1235
2163
|
const prepareFind = handler.prepareFind;
|
|
1236
2164
|
if (typeof prepareFind === "function") {
|
|
1237
2165
|
const prepared = await prepareFind.call(handler, statement, schemeCtx);
|
|
1238
2166
|
if (prepared.status >= 300)
|
|
1239
|
-
return prepared;
|
|
2167
|
+
return Results.assert(prepared);
|
|
1240
2168
|
}
|
|
1241
|
-
return schemeCtx.entries.operations.find(statement);
|
|
2169
|
+
return Results.assert(await schemeCtx.entries.operations.find(statement));
|
|
1242
2170
|
}
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
2171
|
+
async #runLogCuration(statement, ctx) {
|
|
2172
|
+
const addressedScheme = schemeNameOf(statement.target);
|
|
2173
|
+
if (addressedScheme !== null && addressedScheme !== "log") {
|
|
2174
|
+
return _a.#failure("operation-not-implemented", 501, `Scheme '${addressedScheme}' does not implement ${statement.op}.`, {}, {
|
|
2175
|
+
scheme: addressedScheme,
|
|
2176
|
+
operation: statement.op,
|
|
2177
|
+
retryable: false,
|
|
2178
|
+
});
|
|
2179
|
+
}
|
|
2180
|
+
const handler = this.#schemes.get("log");
|
|
2181
|
+
const manifest = this.#schemes.manifestFor("log");
|
|
2182
|
+
if (handler === undefined || manifest === undefined) {
|
|
2183
|
+
throw new Error("the core log curation owner is not registered");
|
|
2184
|
+
}
|
|
2185
|
+
const schemeCtx = new SchemeCtxImpl(ctx, "log", manifest, this.#liveSubscriptions);
|
|
2186
|
+
return statement.op === "OPEN"
|
|
2187
|
+
? Results.assert(await handler.open(statement, schemeCtx))
|
|
2188
|
+
: Results.assert(await handler.fold(statement, schemeCtx));
|
|
2189
|
+
}
|
|
2190
|
+
// {§proposal}/{§send} — status 202 is a proposal except for broadcast
|
|
2191
|
+
// SEND[202], which parks the loop. The operation disambiguates the status.
|
|
1249
2192
|
static #isProposal(statement, result) {
|
|
1250
|
-
//
|
|
1251
|
-
// which IS a proposal by owner ruling (#346: the same stop-the-world system as file edits).
|
|
2193
|
+
// {§send-300-choices} — SEND[300] is the proposal exception.
|
|
1252
2194
|
if (result.status !== 202)
|
|
1253
2195
|
return false;
|
|
1254
2196
|
if (statement.op === "SEND" && statement.signal === 300)
|
|
@@ -1256,21 +2198,39 @@ class Dispatcher {
|
|
|
1256
2198
|
return !(statement.op === "SEND" && statement.target === null);
|
|
1257
2199
|
}
|
|
1258
2200
|
async #writeLog({ statement, result, workspaceId, workerId, loopId, turnId, sequence, origin, }) {
|
|
1259
|
-
const
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
2201
|
+
const durableStatement = DurableStatement.project(statement);
|
|
2202
|
+
const target = this.#extractTarget(durableStatement.target);
|
|
2203
|
+
await this.#canonColumns(target, workspaceId); // {§fs-answer-in-canon}
|
|
2204
|
+
const lineMarkerJson = "lineMarker" in durableStatement && durableStatement.lineMarker !== null
|
|
2205
|
+
? JSON.stringify(durableStatement.lineMarker)
|
|
1263
2206
|
: null;
|
|
1264
2207
|
// A proposal (status 202 from a side-effecting op) is written to the log in
|
|
1265
2208
|
// state='proposed' until the proposal lifecycle resolves it; attrs holds the
|
|
1266
2209
|
// scheme-supplied payload (file diff, exec command, etc.) the client renders
|
|
1267
2210
|
// for review and the scheme consumes on accept. A broadcast SEND[202] is a
|
|
1268
|
-
// parked-terminal,
|
|
2211
|
+
// parked-terminal, not a proposal (#isProposal) → state='resolved'.
|
|
1269
2212
|
const isProposed = _a.#isProposal(statement, result);
|
|
1270
2213
|
let attrsObj = (result.attrs !== undefined && result.attrs !== null)
|
|
1271
2214
|
? { ...result.attrs }
|
|
1272
2215
|
: {};
|
|
1273
|
-
|
|
2216
|
+
const seqs = statement.op === "EXEC" || result.problem !== undefined
|
|
2217
|
+
? await this.#db.engine_loop_turn_seqs.get({
|
|
2218
|
+
loop_id: loopId,
|
|
2219
|
+
turn_id: turnId,
|
|
2220
|
+
})
|
|
2221
|
+
: undefined;
|
|
2222
|
+
if ((statement.op === "EXEC" || result.problem !== undefined) && seqs === undefined) {
|
|
2223
|
+
throw new Error(`Dispatcher.#writeLog: loop_turn_seqs returned no row for loop=${loopId} turn=${turnId}`);
|
|
2224
|
+
}
|
|
2225
|
+
if (statement.op === "READ")
|
|
2226
|
+
Results.assertReadResult(result);
|
|
2227
|
+
if (result.problem !== undefined && seqs !== undefined) {
|
|
2228
|
+
Results.attachInstance(result, `log:///${seqs.loop_seq}/${seqs.turn_seq}/${sequence}/${statement.op}`);
|
|
2229
|
+
}
|
|
2230
|
+
else {
|
|
2231
|
+
Results.assert(result);
|
|
2232
|
+
}
|
|
2233
|
+
// EXEC produces a stream entry addressed by RUNTIME TAG as authority ({§exec}): it lives
|
|
1274
2234
|
// at <runtime>:///<loop_seq>/<turn_seq>/<sequence> (e.g. sh:///1/1/2). That address is a
|
|
1275
2235
|
// SEPARATE `stream` link in attrs — NOT an overload of `target`, which stays faithful to
|
|
1276
2236
|
// the EXEC's own slot (the cwd, or the path to the executable). The log:/// coordinate
|
|
@@ -1278,11 +2238,8 @@ class Dispatcher {
|
|
|
1278
2238
|
// Runtime comes from statement.signal (EXEC's runtime slot), resolvable for failed execs
|
|
1279
2239
|
// too; empty/absent = the default shell.
|
|
1280
2240
|
if (statement.op === "EXEC") {
|
|
1281
|
-
const seqs = await this.#db.engine_loop_turn_seqs.get({
|
|
1282
|
-
loop_id: loopId, turn_id: turnId,
|
|
1283
|
-
});
|
|
1284
2241
|
if (seqs === undefined)
|
|
1285
|
-
throw new Error(
|
|
2242
|
+
throw new Error("Dispatcher.#writeLog: EXEC coordinate was not resolved");
|
|
1286
2243
|
const runtime = (typeof statement.signal === "string" && statement.signal.length > 0) ? statement.signal : "sh";
|
|
1287
2244
|
const coordPathname = `/${seqs.loop_seq}/${seqs.turn_seq}/${sequence}`;
|
|
1288
2245
|
attrsObj.pathname = coordPathname;
|
|
@@ -1297,7 +2254,7 @@ class Dispatcher {
|
|
|
1297
2254
|
}
|
|
1298
2255
|
}
|
|
1299
2256
|
const attrs = JSON.stringify(attrsObj);
|
|
1300
|
-
const txJson = JSON.stringify(
|
|
2257
|
+
const txJson = JSON.stringify(durableStatement);
|
|
1301
2258
|
const rxJson = JSON.stringify(result);
|
|
1302
2259
|
const row = await this.#db.engine_insert_log_entry.get({
|
|
1303
2260
|
worker_id: workerId,
|
|
@@ -1305,17 +2262,17 @@ class Dispatcher {
|
|
|
1305
2262
|
turn_id: turnId,
|
|
1306
2263
|
sequence: sequence,
|
|
1307
2264
|
origin,
|
|
1308
|
-
source: null, // dispatch entries are self-authored; §env-delta deltas set this
|
|
1309
|
-
op:
|
|
1310
|
-
suffix:
|
|
1311
|
-
signal: this.#signalToJson(
|
|
2265
|
+
source: null, // dispatch entries are self-authored; {§env-delta} deltas set this
|
|
2266
|
+
op: durableStatement.op,
|
|
2267
|
+
suffix: durableStatement.suffix,
|
|
2268
|
+
signal: this.#signalToJson(durableStatement.signal),
|
|
1312
2269
|
scheme: target.scheme,
|
|
1313
2270
|
username: target.username,
|
|
1314
2271
|
password: target.password,
|
|
1315
2272
|
hostname: target.hostname,
|
|
1316
2273
|
port: target.port,
|
|
1317
2274
|
pathname: target.pathname,
|
|
1318
|
-
|
|
2275
|
+
query: target.query,
|
|
1319
2276
|
fragment: target.fragment,
|
|
1320
2277
|
lineMarker: lineMarkerJson,
|
|
1321
2278
|
tx: txJson,
|
|
@@ -1332,31 +2289,29 @@ class Dispatcher {
|
|
|
1332
2289
|
throw new Error("Dispatcher.#writeLog: INSERT ... RETURNING produced no row");
|
|
1333
2290
|
return row.id;
|
|
1334
2291
|
}
|
|
1335
|
-
// Normalize a parsed
|
|
1336
|
-
//
|
|
1337
|
-
//
|
|
1338
|
-
//
|
|
2292
|
+
// Normalize a parsed target for log storage. Bare paths and `file:///...`
|
|
2293
|
+
// inputs collapse to scheme=null in log target metadata because both render
|
|
2294
|
+
// as bare paths. Addressable file entries separately persist under the
|
|
2295
|
+
// reserved `file` identity scheme ({§entry-identity-no-null}).
|
|
1339
2296
|
#extractTarget(path) {
|
|
1340
2297
|
if (path === null)
|
|
1341
|
-
return { scheme: null, username: null, password: null, hostname: null, port: null, pathname: null,
|
|
1342
|
-
// `local` (bare path)
|
|
1343
|
-
if (path.kind === "regex")
|
|
1344
|
-
return { scheme: null, username: null, password: null, hostname: null, port: null, pathname: path.raw, params: null, fragment: null }; // regex source — no decode
|
|
2298
|
+
return { scheme: null, username: null, password: null, hostname: null, port: null, pathname: null, query: null, fragment: null };
|
|
2299
|
+
// `local` (bare path) carries no URL parts — store the raw text as the pathname for the log record, scheme=null.
|
|
1345
2300
|
if (path.kind === "local")
|
|
1346
|
-
return { scheme: null, username: null, password: null, hostname: null, port: null, pathname:
|
|
2301
|
+
return { scheme: null, username: null, password: null, hostname: null, port: null, pathname: PathSyntax.decodeParens(path.raw), query: null, fragment: null }; // {§path-parentheses}
|
|
1347
2302
|
const scheme = path.scheme === "file" ? null : path.scheme;
|
|
1348
|
-
//
|
|
1349
|
-
//
|
|
1350
|
-
//
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
2303
|
+
// {§scheme-address-namespace-fold} — a registered non-network scheme folds its
|
|
2304
|
+
// namespace authority into the canonical pathname. Network authorities remain hosts;
|
|
2305
|
+
// worker:// is the registered exception because its authority selects the owner.
|
|
2306
|
+
const foldNs = scheme !== null
|
|
2307
|
+
&& scheme !== "worker"
|
|
2308
|
+
&& !NetworkAddress.supports(scheme)
|
|
2309
|
+
&& this.#schemes.has(scheme);
|
|
1355
2310
|
return {
|
|
1356
2311
|
scheme, username: path.username, password: path.password,
|
|
1357
2312
|
hostname: foldNs ? null : path.hostname, port: path.port,
|
|
1358
|
-
pathname:
|
|
1359
|
-
|
|
2313
|
+
pathname: PathSyntax.decodeParens(foldNs ? foldAuthorityIntoPath(path.hostname, path.pathname) : path.pathname), // {§path-parentheses}
|
|
2314
|
+
query: path.query, fragment: path.fragment,
|
|
1360
2315
|
};
|
|
1361
2316
|
}
|
|
1362
2317
|
#signalToJson(signal) {
|