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