@plurnk/plurnk-service 1.3.12 → 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.defaults +95 -91
- package/INSTALL.md +54 -28
- package/README.md +25 -10
- package/SPEC.md +2426 -942
- package/dist/Paths.d.ts +4 -1
- package/dist/Paths.d.ts.map +1 -1
- package/dist/Paths.js +24 -23
- package/dist/Paths.js.map +1 -1
- package/dist/build-info.json +1 -1
- package/dist/content/edit-receipt.d.ts +13 -28
- package/dist/content/edit-receipt.d.ts.map +1 -1
- package/dist/content/edit-receipt.js +285 -104
- package/dist/content/edit-receipt.js.map +1 -1
- package/dist/content/edited-span.d.ts.map +1 -1
- package/dist/content/edited-span.js +2 -5
- package/dist/content/edited-span.js.map +1 -1
- package/dist/content/index.d.ts +4 -3
- package/dist/content/index.d.ts.map +1 -1
- package/dist/content/index.js +2 -1
- package/dist/content/index.js.map +1 -1
- package/dist/content/line-marker.d.ts +10 -9
- package/dist/content/line-marker.d.ts.map +1 -1
- package/dist/content/line-marker.js +8 -11
- package/dist/content/line-marker.js.map +1 -1
- package/dist/content/matcher.d.ts +15 -26
- package/dist/content/matcher.d.ts.map +1 -1
- package/dist/content/matcher.js +77 -107
- package/dist/content/matcher.js.map +1 -1
- package/dist/content/mimetype-binary.d.ts +2 -2
- package/dist/content/mimetype-binary.d.ts.map +1 -1
- package/dist/content/mimetype-binary.js +10 -10
- package/dist/content/mimetype-binary.js.map +1 -1
- package/dist/content/path-mimetype.js +3 -3
- package/dist/content/path-mimetype.js.map +1 -1
- package/dist/content/read-projector.d.ts +14 -0
- package/dist/content/read-projector.d.ts.map +1 -0
- package/dist/content/read-projector.js +86 -0
- package/dist/content/read-projector.js.map +1 -0
- package/dist/content/read-resolve.d.ts +6 -7
- package/dist/content/read-resolve.d.ts.map +1 -1
- package/dist/content/read-resolve.js +36 -59
- package/dist/content/read-resolve.js.map +1 -1
- package/dist/core/BranchReceipt.d.ts +6 -0
- package/dist/core/BranchReceipt.d.ts.map +1 -0
- package/dist/core/BranchReceipt.js +24 -0
- package/dist/core/BranchReceipt.js.map +1 -0
- package/dist/core/BudgetOverflow.d.ts +20 -0
- package/dist/core/BudgetOverflow.d.ts.map +1 -0
- package/dist/core/BudgetOverflow.js +49 -0
- package/dist/core/BudgetOverflow.js.map +1 -0
- package/dist/core/BudgetReadout.d.ts +8 -0
- package/dist/core/BudgetReadout.d.ts.map +1 -0
- package/dist/core/BudgetReadout.js +79 -0
- package/dist/core/BudgetReadout.js.map +1 -0
- package/dist/core/ChannelWrite.d.ts +31 -9
- package/dist/core/ChannelWrite.d.ts.map +1 -1
- package/dist/core/ChannelWrite.js +30 -23
- package/dist/core/ChannelWrite.js.map +1 -1
- package/dist/core/ChannelWrite.sql +14 -12
- package/dist/core/CoreSchemeServices.d.ts +18 -4
- package/dist/core/CoreSchemeServices.d.ts.map +1 -1
- package/dist/core/CoreSchemeServices.js +7 -1
- package/dist/core/CoreSchemeServices.js.map +1 -1
- package/dist/core/Dispatcher.d.ts +26 -21
- package/dist/core/Dispatcher.d.ts.map +1 -1
- package/dist/core/Dispatcher.js +1626 -671
- package/dist/core/Dispatcher.js.map +1 -1
- package/dist/core/DurableStatement.d.ts +6 -0
- package/dist/core/DurableStatement.d.ts.map +1 -0
- package/dist/core/DurableStatement.js +51 -0
- package/dist/core/DurableStatement.js.map +1 -0
- package/dist/core/Engine.d.ts +64 -29
- package/dist/core/Engine.d.ts.map +1 -1
- package/dist/core/Engine.js +1348 -835
- package/dist/core/Engine.js.map +1 -1
- package/dist/core/Engine.sql +329 -234
- package/dist/core/EnvFlags.js +1 -1
- package/dist/core/EnvFlags.js.map +1 -1
- package/dist/core/ErrorDetail.d.ts +6 -0
- package/dist/core/ErrorDetail.d.ts.map +1 -0
- package/dist/core/ErrorDetail.js +20 -0
- package/dist/core/ErrorDetail.js.map +1 -0
- package/dist/core/ExecutorRegistry.d.ts +14 -3
- package/dist/core/ExecutorRegistry.d.ts.map +1 -1
- package/dist/core/ExecutorRegistry.js +56 -30
- package/dist/core/ExecutorRegistry.js.map +1 -1
- package/dist/core/GitBranch.d.ts +20 -0
- package/dist/core/GitBranch.d.ts.map +1 -0
- package/dist/core/GitBranch.js +110 -0
- package/dist/core/GitBranch.js.map +1 -0
- package/dist/core/JournalTurn.d.ts +9 -0
- package/dist/core/JournalTurn.d.ts.map +1 -0
- package/dist/core/JournalTurn.js +14 -0
- package/dist/core/JournalTurn.js.map +1 -0
- package/dist/core/JournalTurn.sql +10 -0
- package/dist/core/LogBody.d.ts +18 -0
- package/dist/core/LogBody.d.ts.map +1 -0
- package/dist/core/LogBody.js +180 -0
- package/dist/core/LogBody.js.map +1 -0
- package/dist/core/LoopFlagsReader.d.ts +7 -0
- package/dist/core/LoopFlagsReader.d.ts.map +1 -0
- package/dist/core/LoopFlagsReader.js +33 -0
- package/dist/core/LoopFlagsReader.js.map +1 -0
- package/dist/core/LoopLifecycle.d.ts +8 -2
- package/dist/core/LoopLifecycle.d.ts.map +1 -1
- package/dist/core/LoopLifecycle.js +54 -11
- package/dist/core/LoopLifecycle.js.map +1 -1
- package/dist/core/LoopLifecycle.sql +10 -7
- package/dist/core/NoticeChannel.d.ts +13 -0
- package/dist/core/NoticeChannel.d.ts.map +1 -0
- package/dist/core/NoticeChannel.js +40 -0
- package/dist/core/NoticeChannel.js.map +1 -0
- package/dist/core/Owner.d.ts +0 -1
- package/dist/core/Owner.d.ts.map +1 -1
- package/dist/core/Owner.js +2 -5
- package/dist/core/Owner.js.map +1 -1
- package/dist/core/PacketBuilder.d.ts +27 -25
- package/dist/core/PacketBuilder.d.ts.map +1 -1
- package/dist/core/PacketBuilder.js +321 -383
- package/dist/core/PacketBuilder.js.map +1 -1
- package/dist/core/ProblemLog.d.ts +26 -0
- package/dist/core/ProblemLog.d.ts.map +1 -0
- package/dist/core/ProblemLog.js +58 -0
- package/dist/core/ProblemLog.js.map +1 -0
- package/dist/core/ProposalLifecycle.d.ts +15 -21
- package/dist/core/ProposalLifecycle.d.ts.map +1 -1
- package/dist/core/ProposalLifecycle.js +291 -78
- package/dist/core/ProposalLifecycle.js.map +1 -1
- package/dist/core/ProviderInstantiate.d.ts.map +1 -1
- package/dist/core/ProviderInstantiate.js +51 -48
- package/dist/core/ProviderInstantiate.js.map +1 -1
- package/dist/core/SchemeRegistry.d.ts +14 -8
- package/dist/core/SchemeRegistry.d.ts.map +1 -1
- package/dist/core/SchemeRegistry.js +189 -114
- package/dist/core/SchemeRegistry.js.map +1 -1
- package/dist/core/ServiceTeardown.d.ts +11 -0
- package/dist/core/ServiceTeardown.d.ts.map +1 -0
- package/dist/core/ServiceTeardown.js +70 -0
- package/dist/core/ServiceTeardown.js.map +1 -0
- package/dist/core/StoredPacket.d.ts +29 -0
- package/dist/core/StoredPacket.d.ts.map +1 -0
- package/dist/core/StoredPacket.js +147 -0
- package/dist/core/StoredPacket.js.map +1 -0
- package/dist/core/StrikeRail.d.ts +6 -3
- package/dist/core/StrikeRail.d.ts.map +1 -1
- package/dist/core/StrikeRail.js +27 -56
- package/dist/core/StrikeRail.js.map +1 -1
- package/dist/core/TerminalResult.d.ts +16 -0
- package/dist/core/TerminalResult.d.ts.map +1 -0
- package/dist/core/TerminalResult.js +49 -0
- package/dist/core/TerminalResult.js.map +1 -0
- package/dist/core/WorkerControlAddress.d.ts +17 -0
- package/dist/core/WorkerControlAddress.d.ts.map +1 -0
- package/dist/core/WorkerControlAddress.js +44 -0
- package/dist/core/WorkerControlAddress.js.map +1 -0
- package/dist/core/WorkerName.d.ts +31 -0
- package/dist/core/WorkerName.d.ts.map +1 -0
- package/dist/core/WorkerName.js +95 -0
- package/dist/core/WorkerName.js.map +1 -0
- package/dist/core/WorkerName.sql +36 -0
- package/dist/core/WorkspaceGate.d.ts +14 -0
- package/dist/core/WorkspaceGate.d.ts.map +1 -0
- package/dist/core/WorkspaceGate.js +155 -0
- package/dist/core/WorkspaceGate.js.map +1 -0
- package/dist/core/caps/CapsResolve.d.ts +5 -1
- package/dist/core/caps/CapsResolve.d.ts.map +1 -1
- package/dist/core/caps/CapsResolve.js +8 -4
- package/dist/core/caps/CapsResolve.js.map +1 -1
- package/dist/core/caps/DbChannelCaps.d.ts +5 -11
- package/dist/core/caps/DbChannelCaps.d.ts.map +1 -1
- package/dist/core/caps/DbChannelCaps.js +30 -12
- package/dist/core/caps/DbChannelCaps.js.map +1 -1
- package/dist/core/caps/DbEntryCaps.d.ts +5 -14
- package/dist/core/caps/DbEntryCaps.d.ts.map +1 -1
- package/dist/core/caps/DbEntryCaps.js +27 -8
- package/dist/core/caps/DbEntryCaps.js.map +1 -1
- package/dist/core/caps/DbNotifyCaps.d.ts +1 -1
- package/dist/core/caps/DbNotifyCaps.d.ts.map +1 -1
- package/dist/core/caps/DbNotifyCaps.js +18 -15
- package/dist/core/caps/DbNotifyCaps.js.map +1 -1
- package/dist/core/caps/DbProjectionCaps.d.ts +5 -5
- package/dist/core/caps/DbProjectionCaps.d.ts.map +1 -1
- package/dist/core/caps/DbProjectionCaps.js +21 -4
- package/dist/core/caps/DbProjectionCaps.js.map +1 -1
- package/dist/core/caps/DbSubscriptionCaps.d.ts +4 -6
- package/dist/core/caps/DbSubscriptionCaps.d.ts.map +1 -1
- package/dist/core/caps/DbSubscriptionCaps.js +75 -66
- package/dist/core/caps/DbSubscriptionCaps.js.map +1 -1
- package/dist/core/caps/DbTagCaps.d.ts +5 -12
- package/dist/core/caps/DbTagCaps.d.ts.map +1 -1
- package/dist/core/caps/DbTagCaps.js +20 -11
- package/dist/core/caps/DbTagCaps.js.map +1 -1
- package/dist/core/caps/SchemeCtxImpl.d.ts +6 -1
- package/dist/core/caps/SchemeCtxImpl.d.ts.map +1 -1
- package/dist/core/caps/SchemeCtxImpl.js +6 -6
- package/dist/core/caps/SchemeCtxImpl.js.map +1 -1
- package/dist/core/content-hash.js +1 -1
- package/dist/core/content-hash.js.map +1 -1
- package/dist/core/env-defaults.d.ts.map +1 -1
- package/dist/core/env-defaults.js.map +1 -1
- package/dist/core/fork.d.ts.map +1 -1
- package/dist/core/fork.js +55 -30
- package/dist/core/fork.js.map +1 -1
- package/dist/core/fork.sql +43 -29
- package/dist/core/git-env.d.ts +2 -0
- package/dist/core/git-env.d.ts.map +1 -1
- package/dist/core/git-env.js +13 -18
- package/dist/core/git-env.js.map +1 -1
- package/dist/core/git-iso.d.ts +3 -2
- package/dist/core/git-iso.d.ts.map +1 -1
- package/dist/core/git-iso.js +36 -27
- package/dist/core/git-iso.js.map +1 -1
- package/dist/core/git-membership.d.ts +2 -1
- package/dist/core/git-membership.d.ts.map +1 -1
- package/dist/core/git-membership.js +276 -162
- package/dist/core/git-membership.js.map +1 -1
- package/dist/core/git-state.d.ts +8 -1
- package/dist/core/git-state.d.ts.map +1 -1
- package/dist/core/git-state.js +44 -26
- package/dist/core/git-state.js.map +1 -1
- package/dist/core/namespace.d.ts +1 -0
- package/dist/core/namespace.d.ts.map +1 -1
- package/dist/core/namespace.js +20 -0
- package/dist/core/namespace.js.map +1 -1
- package/dist/core/optimistic-settlement.d.ts +2 -0
- package/dist/core/optimistic-settlement.d.ts.map +1 -0
- package/dist/core/optimistic-settlement.js +14 -0
- package/dist/core/optimistic-settlement.js.map +1 -0
- package/dist/core/owner.sql +1 -1
- package/dist/core/packet-inject.d.ts.map +1 -1
- package/dist/core/packet-inject.js +7 -11
- package/dist/core/packet-inject.js.map +1 -1
- package/dist/core/packet-wire.d.ts +3 -21
- package/dist/core/packet-wire.d.ts.map +1 -1
- package/dist/core/packet-wire.js +305 -385
- package/dist/core/packet-wire.js.map +1 -1
- package/dist/core/plurnk-uri.d.ts +12 -1
- package/dist/core/plurnk-uri.d.ts.map +1 -1
- package/dist/core/plurnk-uri.js +49 -29
- package/dist/core/plurnk-uri.js.map +1 -1
- package/dist/core/results.d.ts +18 -5
- package/dist/core/results.d.ts.map +1 -1
- package/dist/core/results.js +33 -18
- package/dist/core/results.js.map +1 -1
- package/dist/core/ruler_count.d.ts +4 -0
- package/dist/core/ruler_count.d.ts.map +1 -0
- package/dist/core/ruler_count.js +7 -0
- package/dist/core/ruler_count.js.map +1 -0
- package/dist/core/scheme-types.d.ts +3 -17
- package/dist/core/scheme-types.d.ts.map +1 -1
- package/dist/core/scheme-types.js.map +1 -1
- package/dist/core/search-gate.d.ts.map +1 -1
- package/dist/core/search-gate.js +3 -11
- package/dist/core/search-gate.js.map +1 -1
- package/dist/core/teaching-corpus.d.ts +6 -0
- package/dist/core/teaching-corpus.d.ts.map +1 -0
- package/dist/core/teaching-corpus.js +23 -0
- package/dist/core/teaching-corpus.js.map +1 -0
- package/dist/core/teaching.d.ts +0 -1
- package/dist/core/teaching.d.ts.map +1 -1
- package/dist/core/teaching.js +2 -8
- package/dist/core/teaching.js.map +1 -1
- package/dist/core/token-ruler.d.ts.map +1 -1
- package/dist/core/token-ruler.js +3 -16
- package/dist/core/token-ruler.js.map +1 -1
- package/dist/core/turn-scheduler.d.ts +1 -1
- package/dist/core/turn-scheduler.d.ts.map +1 -1
- package/dist/core/turn-scheduler.js +1 -1
- package/dist/core/turn-scheduler.js.map +1 -1
- package/dist/core/worker-cap.d.ts +2 -4
- package/dist/core/worker-cap.d.ts.map +1 -1
- package/dist/core/worker-cap.js +13 -4
- package/dist/core/worker-cap.js.map +1 -1
- package/dist/core/{run-ops.sql → worker-ops.sql} +14 -14
- package/dist/core/workspace-settings.d.ts.map +1 -1
- package/dist/core/workspace-settings.js +17 -9
- package/dist/core/workspace-settings.js.map +1 -1
- package/dist/digest/Digest.d.ts +2 -1
- package/dist/digest/Digest.d.ts.map +1 -1
- package/dist/digest/Digest.js +510 -149
- package/dist/digest/Digest.js.map +1 -1
- package/dist/digest/digest.sql +23 -10
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -5
- package/dist/index.js.map +1 -1
- package/dist/observe/api.d.ts +6 -0
- package/dist/observe/api.d.ts.map +1 -0
- package/dist/observe/api.js +11 -0
- package/dist/observe/api.js.map +1 -0
- package/dist/observe/init.d.ts +7 -0
- package/dist/observe/init.d.ts.map +1 -0
- package/dist/observe/init.js +159 -0
- package/dist/observe/init.js.map +1 -0
- package/dist/observe/metrics.d.ts +6 -0
- package/dist/observe/metrics.d.ts.map +1 -0
- package/dist/observe/metrics.js +15 -0
- package/dist/observe/metrics.js.map +1 -0
- package/dist/observe/spans.d.ts +4 -0
- package/dist/observe/spans.d.ts.map +1 -0
- package/dist/observe/spans.js +51 -0
- package/dist/observe/spans.js.map +1 -0
- package/dist/schemes/EffectPolicy.d.ts +1 -0
- package/dist/schemes/EffectPolicy.d.ts.map +1 -1
- package/dist/schemes/EffectPolicy.js +5 -2
- package/dist/schemes/EffectPolicy.js.map +1 -1
- package/dist/schemes/Exec.d.ts +9 -14
- package/dist/schemes/Exec.d.ts.map +1 -1
- package/dist/schemes/Exec.js +402 -262
- package/dist/schemes/Exec.js.map +1 -1
- package/dist/schemes/ExecOutputScheme.d.ts +9 -9
- package/dist/schemes/ExecOutputScheme.d.ts.map +1 -1
- package/dist/schemes/ExecOutputScheme.js +49 -18
- package/dist/schemes/ExecOutputScheme.js.map +1 -1
- package/dist/schemes/File.d.ts +7 -13
- package/dist/schemes/File.d.ts.map +1 -1
- package/dist/schemes/File.js +280 -151
- package/dist/schemes/File.js.map +1 -1
- package/dist/schemes/Log.d.ts +8 -12
- package/dist/schemes/Log.d.ts.map +1 -1
- package/dist/schemes/Log.js +402 -233
- package/dist/schemes/Log.js.map +1 -1
- package/dist/schemes/Log.sql +71 -32
- package/dist/schemes/Prompt.d.ts +3 -3
- package/dist/schemes/Prompt.d.ts.map +1 -1
- package/dist/schemes/Prompt.js +10 -10
- package/dist/schemes/Prompt.js.map +1 -1
- package/dist/schemes/Skill.d.ts +2 -3
- package/dist/schemes/Skill.d.ts.map +1 -1
- package/dist/schemes/Skill.js +2 -6
- package/dist/schemes/Skill.js.map +1 -1
- package/dist/schemes/Worker.d.ts +11 -19
- package/dist/schemes/Worker.d.ts.map +1 -1
- package/dist/schemes/Worker.js +210 -89
- package/dist/schemes/Worker.js.map +1 -1
- package/dist/schemes/_entry-chunk.d.ts.map +1 -1
- package/dist/schemes/_entry-chunk.js +12 -5
- package/dist/schemes/_entry-chunk.js.map +1 -1
- package/dist/schemes/_entry-crud.d.ts +11 -9
- package/dist/schemes/_entry-crud.d.ts.map +1 -1
- package/dist/schemes/_entry-crud.js +72 -13
- package/dist/schemes/_entry-crud.js.map +1 -1
- package/dist/schemes/_entry-crud.sql +28 -10
- package/dist/schemes/_entry-find.d.ts +49 -18
- package/dist/schemes/_entry-find.d.ts.map +1 -1
- package/dist/schemes/_entry-find.js +348 -195
- package/dist/schemes/_entry-find.js.map +1 -1
- package/dist/schemes/_entry-find.sql +20 -8
- package/dist/schemes/_entry-graph.d.ts +4 -2
- package/dist/schemes/_entry-graph.d.ts.map +1 -1
- package/dist/schemes/_entry-graph.js +55 -31
- package/dist/schemes/_entry-graph.js.map +1 -1
- package/dist/schemes/_entry-graph.sql +37 -22
- package/dist/schemes/_entry-manifest.d.ts +10 -15
- package/dist/schemes/_entry-manifest.d.ts.map +1 -1
- package/dist/schemes/_entry-manifest.js +50 -268
- package/dist/schemes/_entry-manifest.js.map +1 -1
- package/dist/schemes/_entry-ops.d.ts +14 -15
- package/dist/schemes/_entry-ops.d.ts.map +1 -1
- package/dist/schemes/_entry-ops.js +121 -110
- package/dist/schemes/_entry-ops.js.map +1 -1
- package/dist/schemes/_entry-ops.sql +2 -3
- package/dist/schemes/_entry-semantic.d.ts +22 -9
- package/dist/schemes/_entry-semantic.d.ts.map +1 -1
- package/dist/schemes/_entry-semantic.js +127 -67
- package/dist/schemes/_entry-semantic.js.map +1 -1
- package/dist/schemes/_entry-semantic.sql +63 -45
- package/dist/schemes/_entry-send.d.ts +3 -4
- package/dist/schemes/_entry-send.d.ts.map +1 -1
- package/dist/schemes/_entry-send.js +48 -23
- package/dist/schemes/_entry-send.js.map +1 -1
- package/dist/schemes/_path-scope.d.ts +23 -0
- package/dist/schemes/_path-scope.d.ts.map +1 -0
- package/dist/schemes/_path-scope.js +61 -0
- package/dist/schemes/_path-scope.js.map +1 -0
- package/dist/schemes/_search-candidate.d.ts +20 -0
- package/dist/schemes/_search-candidate.d.ts.map +1 -0
- package/dist/schemes/_search-candidate.js +19 -0
- package/dist/schemes/_search-candidate.js.map +1 -0
- package/dist/schemes/_search-exclusion.d.ts +7 -0
- package/dist/schemes/_search-exclusion.d.ts.map +1 -0
- package/dist/schemes/_search-exclusion.js +23 -0
- package/dist/schemes/_search-exclusion.js.map +1 -0
- package/dist/schemes/_search-index.d.ts +6 -0
- package/dist/schemes/_search-index.d.ts.map +1 -0
- package/dist/schemes/_search-index.js +328 -0
- package/dist/schemes/_search-index.js.map +1 -0
- package/dist/schemes/cosine.js +1 -1
- package/dist/schemes/cosine.js.map +1 -1
- package/dist/schemes/exec-abort.js +3 -3
- package/dist/schemes/exec-abort.js.map +1 -1
- package/dist/schemes/exec-env.js +3 -3
- package/dist/schemes/exec-env.js.map +1 -1
- package/dist/server/BranchBatches.d.ts +49 -0
- package/dist/server/BranchBatches.d.ts.map +1 -0
- package/dist/server/BranchBatches.js +592 -0
- package/dist/server/BranchBatches.js.map +1 -0
- package/dist/server/Daemon.d.ts +39 -71
- package/dist/server/Daemon.d.ts.map +1 -1
- package/dist/server/Daemon.js +1133 -491
- package/dist/server/Daemon.js.map +1 -1
- package/dist/server/DaemonModule.d.ts +31 -0
- package/dist/server/DaemonModule.d.ts.map +1 -0
- package/dist/server/DaemonModule.js +2 -0
- package/dist/server/DaemonModule.js.map +1 -0
- package/dist/server/branch-batch.sql +154 -0
- package/dist/server/client-input.d.ts +13 -0
- package/dist/server/client-input.d.ts.map +1 -1
- package/dist/server/client-input.js +268 -57
- package/dist/server/client-input.js.map +1 -1
- package/dist/server/dispatch-as-plurnk.d.ts +1 -1
- package/dist/server/dispatch-as-plurnk.d.ts.map +1 -1
- package/dist/server/dispatch-as-plurnk.js +15 -7
- package/dist/server/dispatch-as-plurnk.js.map +1 -1
- package/dist/server/drain.sql +96 -65
- package/dist/server/envelope.d.ts +4 -7
- package/dist/server/envelope.d.ts.map +1 -1
- package/dist/server/envelope.js +118 -95
- package/dist/server/envelope.js.map +1 -1
- package/dist/server/envelope.sql +18 -23
- package/dist/server/exec-poll-backoff.js +1 -1
- package/dist/server/exec-poll-backoff.js.map +1 -1
- package/dist/server/lifecycle-recovery.sql +112 -2
- package/dist/server/logEntry.d.ts +4 -2
- package/dist/server/logEntry.d.ts.map +1 -1
- package/dist/server/logEntry.js +6 -3
- package/dist/server/logEntry.js.map +1 -1
- package/dist/server/logEntry.sql +1 -1
- package/dist/server/loop-model.d.ts.map +1 -1
- package/dist/server/loop-model.js +22 -9
- package/dist/server/loop-model.js.map +1 -1
- package/dist/server/loopDocs.d.ts.map +1 -1
- package/dist/server/loopDocs.js +5 -4
- package/dist/server/loopDocs.js.map +1 -1
- package/dist/server/seam-entry-read.sql +15 -10
- package/dist/server/seam-log-read.sql +2 -2
- package/dist/server/seam-loop.sql +1 -1
- package/dist/server/seam-proposal-list.sql +17 -7
- package/dist/service.d.ts.map +1 -1
- package/dist/service.js +79 -61
- package/dist/service.js.map +1 -1
- package/migrations/001_schema.sql +1010 -0
- package/package.json +65 -32
- package/dist/core/PluginLoader.d.ts +0 -20
- package/dist/core/PluginLoader.d.ts.map +0 -1
- package/dist/core/PluginLoader.js +0 -142
- package/dist/core/PluginLoader.js.map +0 -1
- package/dist/core/TelemetryChannel.d.ts +0 -37
- package/dist/core/TelemetryChannel.d.ts.map +0 -1
- package/dist/core/TelemetryChannel.js +0 -80
- package/dist/core/TelemetryChannel.js.map +0 -1
- package/dist/core/path-decode.d.ts +0 -3
- package/dist/core/path-decode.d.ts.map +0 -1
- package/dist/core/path-decode.js +0 -9
- package/dist/core/path-decode.js.map +0 -1
- package/dist/core/plugin-attribution.d.ts +0 -5
- package/dist/core/plugin-attribution.d.ts.map +0 -1
- package/dist/core/plugin-attribution.js +0 -39
- package/dist/core/plugin-attribution.js.map +0 -1
- package/dist/core/world-state.d.ts +0 -10
- package/dist/core/world-state.d.ts.map +0 -1
- package/dist/core/world-state.js +0 -35
- package/dist/core/world-state.js.map +0 -1
- package/dist/core/world-state.sql +0 -35
- package/dist/core/zero-pin.d.ts +0 -3
- package/dist/core/zero-pin.d.ts.map +0 -1
- package/dist/core/zero-pin.js +0 -16
- package/dist/core/zero-pin.js.map +0 -1
- package/dist/server/auto.d.ts +0 -6
- package/dist/server/auto.d.ts.map +0 -1
- package/dist/server/auto.js +0 -63
- package/dist/server/auto.js.map +0 -1
- package/dist/server/clientTurn.d.ts +0 -6
- package/dist/server/clientTurn.d.ts.map +0 -1
- package/dist/server/clientTurn.js +0 -22
- package/dist/server/clientTurn.js.map +0 -1
- package/dist/server/clientTurn.sql +0 -10
- package/dist/server/noProposals.d.ts +0 -6
- package/dist/server/noProposals.d.ts.map +0 -1
- package/dist/server/noProposals.js +0 -37
- package/dist/server/noProposals.js.map +0 -1
- package/dist/server/version-info.d.ts +0 -14
- package/dist/server/version-info.d.ts.map +0 -1
- package/dist/server/version-info.js +0 -69
- package/dist/server/version-info.js.map +0 -1
- package/migrations/0000-00-00.01_schema.sql +0 -536
- package/migrations/0004_loop-provider.sql +0 -3
- package/migrations/0005_subscription-published-channel.sql +0 -3
- package/migrations/0006_loop-max-turns.sql +0 -3
package/dist/server/Daemon.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
// Top-level daemon orchestrator. Owns the DB connection, engine, registries,
|
|
2
|
-
// the plugin-module seam (
|
|
3
|
-
// SPEC §rpc.
|
|
2
|
+
// the transport-free plugin-module seam ({§rpc}).
|
|
4
3
|
import { readFile } from "node:fs/promises";
|
|
5
4
|
import { resolve, dirname } from "node:path";
|
|
5
|
+
import { setTimeout as delay } from "node:timers/promises";
|
|
6
6
|
import { execPollBackoffMs } from "./exec-poll-backoff.js";
|
|
7
7
|
import ChannelWrite from "../core/ChannelWrite.js";
|
|
8
8
|
import { Paths } from "../index.js";
|
|
@@ -10,37 +10,63 @@ import Engine from "../core/Engine.js";
|
|
|
10
10
|
import ExecutorRegistry from "../core/ExecutorRegistry.js";
|
|
11
11
|
import SchemeRegistry from "../core/SchemeRegistry.js";
|
|
12
12
|
import { Mimetypes } from "@plurnk/plurnk-mimetypes";
|
|
13
|
+
import { parsePath, Validator, } from "@plurnk/plurnk-contracts";
|
|
13
14
|
import LogEntry from "./logEntry.js";
|
|
14
15
|
import Envelope from "./envelope.js";
|
|
15
16
|
import ClientInput from "./client-input.js";
|
|
16
|
-
import
|
|
17
|
+
import JournalTurn from "../core/JournalTurn.js";
|
|
17
18
|
import LoopDocs from "./loopDocs.js";
|
|
18
19
|
import GitMembership from "../core/git-membership.js";
|
|
19
20
|
import Fork from "../core/fork.js";
|
|
21
|
+
import WorkerName from "../core/WorkerName.js";
|
|
20
22
|
import LoopLifecycle from "../core/LoopLifecycle.js";
|
|
21
23
|
import { promptLoopPrefix } from "../core/plurnk-uri.js";
|
|
22
24
|
import { rulerCount } from "../core/token-ruler.js";
|
|
23
25
|
import { parseAliasesFromEnv, resolveActiveAlias } from "@plurnk/plurnk-providers";
|
|
24
26
|
import ProviderInstantiate from "../core/ProviderInstantiate.js";
|
|
25
27
|
import { resolveLoopAlias } from "./loop-model.js";
|
|
26
|
-
import Auto from "./auto.js";
|
|
27
|
-
import NoProposals from "./noProposals.js";
|
|
28
28
|
import { DEFAULT_LOOP_FLAGS } from "../core/scheme-types.js";
|
|
29
|
+
import LoopFlagsReader from "../core/LoopFlagsReader.js";
|
|
30
|
+
import Results, { OperationFailureError } from "../core/results.js";
|
|
31
|
+
import WorkspaceGate from "../core/WorkspaceGate.js";
|
|
32
|
+
import BranchBatches from "./BranchBatches.js";
|
|
33
|
+
import ErrorDetail from "../core/ErrorDetail.js";
|
|
34
|
+
import { observed, observedSync } from "../observe/spans.js";
|
|
35
|
+
import { LOOP_TERMINALS, recordCounter } from "../observe/metrics.js";
|
|
36
|
+
import { readOptimisticSettlementMs } from "../core/optimistic-settlement.js";
|
|
37
|
+
const clientActionFailure = (error) => {
|
|
38
|
+
if (error instanceof OperationFailureError)
|
|
39
|
+
return error.result;
|
|
40
|
+
console.error("Client action failed outside its operation result contract:", error);
|
|
41
|
+
return Results.failure("daemon:client", "action-threw", 500, "The client action failed outside its operation result contract.", {}, {
|
|
42
|
+
stage: "client-action",
|
|
43
|
+
retryable: false,
|
|
44
|
+
});
|
|
45
|
+
};
|
|
46
|
+
const daemonFailure = (owner, code, status, detail, extensions = {}) => new OperationFailureError(Results.failure(owner, code, status, detail, {}, extensions));
|
|
47
|
+
const entryReadResult = (result) => Validator.assertEntryReadResult(result);
|
|
29
48
|
export default class Daemon {
|
|
30
49
|
#db;
|
|
31
50
|
#engine;
|
|
51
|
+
#workspaceGate;
|
|
52
|
+
#branchBatches;
|
|
32
53
|
#lifecycle;
|
|
33
54
|
#schemes;
|
|
34
55
|
#mimetypes;
|
|
56
|
+
#ownsMimetypes;
|
|
35
57
|
#provider;
|
|
36
58
|
#nodeModulesPath;
|
|
37
59
|
#discoveryCwd;
|
|
38
|
-
#started = false; //
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
60
|
+
#started = false; // {§module-lifecycle}: one discovery/module boot; no listener
|
|
61
|
+
#capabilitiesPublished = false;
|
|
62
|
+
#modules = [];
|
|
63
|
+
#moduleClosers = [];
|
|
64
|
+
#moduleActions = new Map();
|
|
65
|
+
// {§methods-event-subscribe} — the broadcast's in-process event source. A transport
|
|
66
|
+
// module (plurnk-agui) subscribes and fans out to its OWN clients; core emits, never owns
|
|
67
|
+
// client transport or connection state.
|
|
42
68
|
#eventSubscribers = new Set();
|
|
43
|
-
//
|
|
69
|
+
// Worker-level drain registry. At most one drain per worker. The stored object
|
|
44
70
|
// is the drain's identity handle: start/exit compare it by reference so a
|
|
45
71
|
// drain exiting never clobbers a successor that raced in, and a loop
|
|
46
72
|
// enqueued during teardown is never stranded. A drain is a pure queue
|
|
@@ -48,27 +74,32 @@ export default class Daemon {
|
|
|
48
74
|
// (subscriptions + Exec.idle), and a concluding stream routes through
|
|
49
75
|
// inject() like any other loop source.
|
|
50
76
|
#activeDrains = new Map();
|
|
51
|
-
|
|
77
|
+
#drainExitTasks = new Set();
|
|
78
|
+
// Per-worker cancellation scope. Loops AND the streams they spawn (execs)
|
|
52
79
|
// share this signal, so loop.cancel / shutdown abort it once and every
|
|
53
80
|
// in-flight subscription tears down — even a spawn that registers AFTER the
|
|
54
81
|
// cancel self-aborts against the already-aborted signal (no race). Outlives
|
|
55
82
|
// any single (ephemeral) drain; replaced with a fresh controller once
|
|
56
|
-
// aborted so a later
|
|
83
|
+
// aborted so a later runLoop request isn't born cancelled.
|
|
57
84
|
#workerAborts = new Map();
|
|
58
85
|
// grammar 0.74.20 EXEC `<T,P>` — per-worker hibernation poll-wake timer. When a loop parks at
|
|
59
|
-
// a park with a polled stream, a timer fires every P seconds to resume it (§exec-poll). One
|
|
86
|
+
// a park with a polled stream, a timer fires every P seconds to resume it ({§exec-poll}). One
|
|
60
87
|
// per worker (the tightest cadence); cleared/replaced on each park and on cancel.
|
|
61
88
|
#parkTimers = new Map();
|
|
62
89
|
#pollTimers = new Map();
|
|
63
|
-
#pollBackoff = new Map(); //
|
|
64
|
-
// Per-
|
|
90
|
+
#pollBackoff = new Map(); // {§exec-poll} — backoff step per worker
|
|
91
|
+
// Per-worker drain-transition lock — see #withDrainLock (R4 / {§worker-lifecycle-single-drain}).
|
|
65
92
|
#drainLocks = new Map();
|
|
66
|
-
// §worker-lifecycle-child-wake —
|
|
67
|
-
// mid-turn (not yet slept), so #wakeParkedWorker could not resume it. A worker
|
|
93
|
+
// {§worker-lifecycle-child-wake} — workers owed a wake: a child/stream conclusion fired while the worker was
|
|
94
|
+
// mid-turn (not yet slept), so #wakeParkedWorker could not resume it. A child-worker conclusion is a
|
|
68
95
|
// BOUNDED, lossless wake (a worker always concludes), so a hibernation awaiting one MUST return —
|
|
69
96
|
// never deadlock. The drain honors the owed wake at the worker's next park, closing the conclude-
|
|
70
97
|
// before-park race. (Only a live exec stream, unbounded absent a timeout, may hold a park open.)
|
|
71
98
|
#owedWakes = new Set();
|
|
99
|
+
// {§worker-optimistic-settlement} — one non-sliding completion-wake batch
|
|
100
|
+
// per worker. Durable conclusions and client events land before this gate;
|
|
101
|
+
// only the parked loop's provider-dispatch requeue waits.
|
|
102
|
+
#completionWakeGates = new Map();
|
|
72
103
|
constructor({ db, schemes, mimetypes, provider, nodeModulesPath, }) {
|
|
73
104
|
this.#db = db;
|
|
74
105
|
this.#lifecycle = new LoopLifecycle(db);
|
|
@@ -81,7 +112,10 @@ export default class Daemon {
|
|
|
81
112
|
this.#nodeModulesPath = nodeModulesPath ?? resolve(process.cwd(), "node_modules");
|
|
82
113
|
this.#discoveryCwd = dirname(this.#nodeModulesPath);
|
|
83
114
|
// Mimetypes owns discovery + detection; default mimetype text/markdown. (Token counting
|
|
84
|
-
// is NOT wired here — the engine's ruler below is §tokenomics-agnostic-ruler.)
|
|
115
|
+
// is NOT wired here — the engine's ruler below is {§tokenomics-agnostic-ruler}.)
|
|
116
|
+
// Constructor ownership is the lifecycle boundary
|
|
117
|
+
// ({§mimetype-owned-lifecycle}).
|
|
118
|
+
this.#ownsMimetypes = mimetypes === undefined;
|
|
85
119
|
this.#mimetypes = mimetypes ?? new Mimetypes({
|
|
86
120
|
defaultMimetype: "text/markdown",
|
|
87
121
|
discoverOptions: { cwd: this.#discoveryCwd },
|
|
@@ -90,9 +124,60 @@ export default class Daemon {
|
|
|
90
124
|
if (this.#provider !== null && bootSpec !== null) {
|
|
91
125
|
ProviderInstantiate.registerInstance(this.#provider, bootSpec);
|
|
92
126
|
}
|
|
127
|
+
this.#workspaceGate = new WorkspaceGate(async (workerId, rootWorkerId) => {
|
|
128
|
+
const row = await this.#db.branch_batch_worker_lineage.get({
|
|
129
|
+
worker_id: workerId,
|
|
130
|
+
root_worker_id: rootWorkerId,
|
|
131
|
+
});
|
|
132
|
+
return row !== undefined;
|
|
133
|
+
});
|
|
134
|
+
this.#branchBatches = new BranchBatches(db, this.#workspaceGate, {
|
|
135
|
+
settleWorkspace: async (workspaceId) => this.#engine.drainWorkspaceDerivations(workspaceId),
|
|
136
|
+
createChild: async ({ workspaceId, parentWorkerId, parentLoopId, op, name, prompt, flags, origin }) => {
|
|
137
|
+
const parentPolicy = await this.#providerPolicyForLoop(parentLoopId);
|
|
138
|
+
const providerSpec = parentPolicy.childProviderSpec ?? parentPolicy.providerSpec;
|
|
139
|
+
const workerName = WorkerName.assert(name);
|
|
140
|
+
const workerId = op === "FORK"
|
|
141
|
+
? await Fork.fork(this.#db, parentWorkerId, workerName)
|
|
142
|
+
: (await this.#db.fork_insert_worker.get({
|
|
143
|
+
workspace_id: workspaceId,
|
|
144
|
+
name: workerName,
|
|
145
|
+
parent_worker_id: parentWorkerId,
|
|
146
|
+
origin,
|
|
147
|
+
}))?.id;
|
|
148
|
+
if (workerId === undefined)
|
|
149
|
+
throw new Error("Branch worker insert returned no row");
|
|
150
|
+
const loopId = await this.#enqueueFreshLoop({
|
|
151
|
+
workerId,
|
|
152
|
+
prompt,
|
|
153
|
+
providerSpec,
|
|
154
|
+
childProviderSpec: parentPolicy.childProviderSpec,
|
|
155
|
+
flags,
|
|
156
|
+
});
|
|
157
|
+
return { workerId, loopId };
|
|
158
|
+
},
|
|
159
|
+
startChild: async (workspaceId, workerId, loopId) => {
|
|
160
|
+
const systemPrompt = await readFile(Paths.instructionsSystem, "utf8");
|
|
161
|
+
const started = await this.#ensureDrain({ workspaceId, workerId, systemPrompt });
|
|
162
|
+
if (started === null)
|
|
163
|
+
throw new Error(`Branch worker ${workerId} already has a live drain`);
|
|
164
|
+
const result = await started.firstLoopPromise;
|
|
165
|
+
if (result.loopId !== loopId) {
|
|
166
|
+
throw new Error(`Branch worker ${workerId} drained loop ${result.loopId}, expected ${loopId}`);
|
|
167
|
+
}
|
|
168
|
+
return result.result;
|
|
169
|
+
},
|
|
170
|
+
wakeParent: async (workspaceId, workerId) => {
|
|
171
|
+
const systemPrompt = await readFile(Paths.instructionsSystem, "utf8");
|
|
172
|
+
await this.#settleCompletionWake(workspaceId, workerId, systemPrompt, false);
|
|
173
|
+
},
|
|
174
|
+
notify: (workspaceId, payload) => {
|
|
175
|
+
this.#broadcast({ workspaceId }, "workspace/branch-batch", payload);
|
|
176
|
+
},
|
|
177
|
+
});
|
|
93
178
|
this.#engine = new Engine({
|
|
94
179
|
db, schemes: this.#schemes, mimetypes: this.#mimetypes,
|
|
95
|
-
// §tokenomics-agnostic-ruler — the ONE model-facing token ruler (chars/2), NOT the
|
|
180
|
+
// {§tokenomics-agnostic-ruler} — the ONE model-facing token ruler (chars/2), NOT the
|
|
96
181
|
// boot provider: token accounting is workspace-wide across many concurrent models, so
|
|
97
182
|
// the write-time + catalog counts must be model-independent. Exact per-model counting
|
|
98
183
|
// lives only at the packet-materialization fit-gate.
|
|
@@ -103,50 +188,46 @@ export default class Daemon {
|
|
|
103
188
|
// Daemon.inject (active sister → fold; idle → enqueue + drain). The
|
|
104
189
|
// daemon owns provider + the law-file system prompt; the worker scheme
|
|
105
190
|
// handler carries neither. Fire-and-forget: the returned drain runs
|
|
106
|
-
// independently (the sister is its own worker). §machine-processes
|
|
107
|
-
injectWorker: async ({ workspaceId, workerId, prompt, flags }) => {
|
|
108
|
-
if (this.#provider === null)
|
|
109
|
-
throw new Error("injectWorker: no provider configured");
|
|
191
|
+
// independently (the sister is its own worker). {§machine-processes}
|
|
192
|
+
injectWorker: async ({ workspaceId, workerId, prompt, flags, parentLoopId }) => {
|
|
110
193
|
const systemPrompt = await readFile(Paths.instructionsSystem, "utf8");
|
|
111
|
-
const
|
|
194
|
+
const parentPolicy = parentLoopId === undefined
|
|
195
|
+
? null
|
|
196
|
+
: await this.#providerPolicyForLoop(parentLoopId);
|
|
197
|
+
const providerSpec = parentPolicy === null
|
|
198
|
+
? resolveActiveAlias()
|
|
199
|
+
: parentPolicy.childProviderSpec ?? parentPolicy.providerSpec;
|
|
112
200
|
if (providerSpec === null)
|
|
113
201
|
throw new Error("injectWorker: active provider has no resolvable alias");
|
|
114
|
-
const { action, loopId } = await this.inject({
|
|
202
|
+
const { action, loopId } = await this.inject({
|
|
203
|
+
workspaceId,
|
|
204
|
+
workerId,
|
|
205
|
+
prompt,
|
|
206
|
+
providerSpec,
|
|
207
|
+
...(parentPolicy === null ? {} : { childProviderSpec: parentPolicy.childProviderSpec }),
|
|
208
|
+
systemPrompt,
|
|
209
|
+
...(flags === undefined ? {} : { flags }),
|
|
210
|
+
});
|
|
115
211
|
return { action, loopId };
|
|
116
212
|
},
|
|
213
|
+
branchWorker: async (args) => this.#branchBatches.enqueue(args),
|
|
214
|
+
branchCompletionGate: async (workerId) => this.#branchBatches.completionGate(workerId),
|
|
215
|
+
acquireWorkspaceTurn: async (workspaceId, workerId) => this.#workspaceGate.acquireTurn(workspaceId, workerId),
|
|
216
|
+
workspaceTurnCompleted: async ({ turnId }) => this.#branchBatches.sealTurn(turnId),
|
|
117
217
|
// worker:// KILL (terminate) — cancel the addressed worker subtree and
|
|
118
218
|
// tear down its held streams before the operation completes.
|
|
119
219
|
cancelWorker: async (workerId, reason) => this.#cancelWorkerTree(workerId, reason),
|
|
120
220
|
cancelDescendants: async (workerId, reason) => this.#cancelTree(workerId, reason, false),
|
|
121
|
-
|
|
221
|
+
noticeNotify: (workspaceId, payload) => this.notifyNotice(workspaceId, payload),
|
|
122
222
|
});
|
|
123
223
|
// Wire proposal-pending events to the loop/proposal WS notification.
|
|
124
224
|
// Sessionid scopes the broadcast to clients on the same workspace.
|
|
125
225
|
this.#engine.onProposalPending((event) => {
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
workerId: event.workerId,
|
|
129
|
-
loopId: event.loopId,
|
|
130
|
-
turnId: event.turnId,
|
|
131
|
-
op: event.op,
|
|
132
|
-
target: event.target,
|
|
133
|
-
body: event.body,
|
|
134
|
-
attrs: event.attrs,
|
|
135
|
-
// event.flags is carried for discoverability — a client in
|
|
136
|
-
// loop-auto mode (event.flags.auto=true) knows to skip
|
|
137
|
-
// rendering review UI because the entry will resolve in-
|
|
138
|
-
// process before any human can react.
|
|
139
|
-
flags: event.flags,
|
|
140
|
-
});
|
|
226
|
+
const { workspaceId, ...proposal } = event;
|
|
227
|
+
this.#broadcast({ workspaceId }, "loop/proposal", proposal);
|
|
141
228
|
});
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
// Inverse policy: auto-REJECT proposals in-process when the loop's
|
|
145
|
-
// persisted flags.noProposals === true (client has no review channel).
|
|
146
|
-
// The model sees an ordinary 400, never the orchestration reason.
|
|
147
|
-
NoProposals.attachNoProposals(this.#engine, this.#db);
|
|
148
|
-
}
|
|
149
|
-
// The client-interface seam (#355). A transport module subscribes to the daemon's in-process
|
|
229
|
+
}
|
|
230
|
+
// {§methods-event-subscribe}. A transport module subscribes to the daemon's in-process
|
|
150
231
|
// event source: it receives every workspace-scoped engine event as `(workspaceId, method, params)`
|
|
151
232
|
// and fans out to its OWN clients — core emits, it never fans out for the module. Returns an
|
|
152
233
|
// unsubscribe. `workspaceId` is the event's workspace, or null for a global event (e.g. workspace/created).
|
|
@@ -155,55 +236,104 @@ export default class Daemon {
|
|
|
155
236
|
this.#eventSubscribers.add(handler);
|
|
156
237
|
return () => { this.#eventSubscribers.delete(handler); };
|
|
157
238
|
}
|
|
158
|
-
//
|
|
239
|
+
// {§methods-proposal-resolve} — proposal HITL. A transport module reads the stopped-world
|
|
159
240
|
// proposals for a workspace (rendering each as a TOOL_CALL) and feeds back the human's decision. The
|
|
160
241
|
// gate, validation, and applyResolution stay core (Engine.resolveProposal); the seam is the read +
|
|
161
242
|
// the resolve, never the mechanism. `resolveProposal` throws for an unknown/already-resolved id.
|
|
162
243
|
async pendingProposals(workspaceId) {
|
|
163
|
-
|
|
244
|
+
const checkedWorkspaceId = ClientInput.assertId("pendingProposals", "workspaceId", workspaceId);
|
|
245
|
+
return this.#engine.pendingProposals(checkedWorkspaceId);
|
|
164
246
|
}
|
|
165
247
|
resolveProposal(logEntryId, resolution) {
|
|
166
|
-
|
|
248
|
+
const checkedLogEntryId = ClientInput.assertId("resolveProposal", "logEntryId", logEntryId);
|
|
249
|
+
const checkedResolution = ClientInput.assertProposalResolution("resolveProposal", resolution);
|
|
250
|
+
this.#engine.resolveProposal(checkedLogEntryId, checkedResolution);
|
|
167
251
|
}
|
|
168
|
-
//
|
|
252
|
+
// {§methods-loop-run} — drive/steer a loop. The module supplies only workspace/worker/prompt;
|
|
169
253
|
// the provider and the law-file system prompt are core's and stay inside. Returns immediately — the
|
|
170
254
|
// loop runs async and its outcome arrives on the event source (loop/terminated). `cancelDrain` (public)
|
|
171
255
|
// is the cancel hook. Both funnel through the unified `inject`, which owns the drain lifecycle.
|
|
172
256
|
async runLoop(args) {
|
|
173
|
-
const
|
|
174
|
-
|
|
175
|
-
|
|
257
|
+
const workspaceId = ClientInput.assertId("runLoop", "workspaceId", args.workspaceId);
|
|
258
|
+
const workerId = ClientInput.assertId("runLoop", "workerId", args.workerId);
|
|
259
|
+
const prompt = ClientInput.assertPrompt("runLoop", args.prompt);
|
|
260
|
+
const requestedMaxTurns = ClientInput.assertMaxTurns("runLoop", args.maxTurns);
|
|
261
|
+
const openPaths = ClientInput.assertOpenPaths("runLoop", args.openPaths);
|
|
262
|
+
const alias = ClientInput.assertOptionalSelector("runLoop", "alias", args.alias);
|
|
263
|
+
const model = ClientInput.assertOptionalSelector("runLoop", "model", args.model);
|
|
264
|
+
const childAlias = ClientInput.assertOptionalChildAlias("runLoop", args.childAlias);
|
|
265
|
+
const childModel = ClientInput.assertOptionalSelector("runLoop", "childModel", args.childModel);
|
|
266
|
+
if (childAlias === null && childModel !== undefined) {
|
|
267
|
+
throw daemonFailure("daemon:input", "child-provider-conflict", 400, "childModel cannot accompany an inherited child provider policy.", { field: "childModel", recovery: "Omit childModel or select a child alias.", retryable: false });
|
|
268
|
+
}
|
|
269
|
+
const flags = ClientInput.normalizeLoopFlags("runLoop", args.flags);
|
|
270
|
+
// {§methods-loop-run-model} — a client sends alias/model on every loop, so a
|
|
271
|
+
// switch takes effect turn-to-turn. `model` (client-resolved <provider>/<model>) wins
|
|
176
272
|
// over `alias`; neither → the boot default. Instantiation is cached, so ping-ponging
|
|
177
273
|
// between two models is cheap, and an unresolvable alias/model fails loud here.
|
|
178
|
-
const selection = await this.#resolveLoopProvider(
|
|
179
|
-
if (selection === null)
|
|
180
|
-
throw new
|
|
274
|
+
const selection = await this.#resolveLoopProvider(alias, model);
|
|
275
|
+
if (selection === null) {
|
|
276
|
+
throw new OperationFailureError(Results.failure("daemon:provider", "not-configured", 501, "No provider is configured for this loop.", {}, {
|
|
277
|
+
stage: "provider-selection",
|
|
278
|
+
recovery: "Select a configured model provider.",
|
|
279
|
+
retryable: false,
|
|
280
|
+
}));
|
|
281
|
+
}
|
|
282
|
+
// {§methods-loop-run-child-provider}
|
|
283
|
+
const configuredChildAlias = childAlias === undefined && childModel === undefined
|
|
284
|
+
? process.env.PLURNK_MODEL_CHILD
|
|
285
|
+
: childAlias;
|
|
286
|
+
const childSelection = configuredChildAlias === null
|
|
287
|
+
|| (configuredChildAlias === undefined && childModel === undefined)
|
|
288
|
+
? null
|
|
289
|
+
: await this.#resolveLoopProvider(configuredChildAlias, childModel);
|
|
181
290
|
const systemPrompt = await readFile(Paths.instructionsSystem, "utf8");
|
|
182
|
-
// §machine-processes — the model NEVER runs in a client-origin
|
|
183
|
-
// client
|
|
291
|
+
// {§machine-processes} — the model NEVER runs in a client-origin worker (its packets would carry
|
|
292
|
+
// client-action rows). The module resolves the model worker via ensureModelWorker and passes it (or a
|
|
184
293
|
// fork); a client worker here is a caller error, refused loudly rather than silently rehomed.
|
|
185
|
-
const target = await this.#db.envelope_get_worker_by_id.get({ id:
|
|
186
|
-
if (target === undefined)
|
|
187
|
-
throw
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
294
|
+
const target = await this.#db.envelope_get_worker_by_id.get({ id: workerId });
|
|
295
|
+
if (target === undefined) {
|
|
296
|
+
throw daemonFailure("daemon:worker", "worker-not-found", 404, `Worker ${workerId} does not exist.`, { workerId });
|
|
297
|
+
}
|
|
298
|
+
if (target.workspace_id !== workspaceId) {
|
|
299
|
+
throw daemonFailure("daemon:worker", "workspace-mismatch", 409, `Worker ${workerId} does not belong to workspace ${workspaceId}.`, {
|
|
300
|
+
workerId,
|
|
301
|
+
workspaceId,
|
|
302
|
+
actualWorkspaceId: target.workspace_id,
|
|
303
|
+
retryable: false,
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
if (target.origin === "client") {
|
|
307
|
+
throw daemonFailure("daemon:worker", "model-worker-required", 409, `Worker ${workerId} is not a model worker.`, {
|
|
308
|
+
workerId,
|
|
309
|
+
recovery: "Select or create a model worker for this loop.",
|
|
310
|
+
retryable: false,
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
// {§operator-config-max-turns-ceiling} — the operator ceiling clamps a per-call maxTurns; a
|
|
191
314
|
// seam caller must not bypass operator policy (inject only DEFAULTS from env, never clamps).
|
|
192
315
|
const ceiling = Number(process.env.PLURNK_SERVICE_MAX_TURNS ?? "-1");
|
|
193
|
-
const requested =
|
|
316
|
+
const requested = requestedMaxTurns ?? ceiling;
|
|
194
317
|
const maxTurns = ceiling < 0 ? requested : (requested < 0 ? ceiling : Math.min(requested, ceiling));
|
|
195
|
-
const
|
|
318
|
+
const turnCeiling = {
|
|
319
|
+
effective: maxTurns,
|
|
320
|
+
source: requestedMaxTurns === undefined ? "implicit" : "explicit",
|
|
321
|
+
};
|
|
196
322
|
const { action, loopId, turnSeq } = await this.inject({
|
|
197
|
-
|
|
323
|
+
workspaceId,
|
|
324
|
+
workerId,
|
|
325
|
+
prompt,
|
|
198
326
|
...(flags !== undefined ? { flags } : {}),
|
|
199
|
-
|
|
327
|
+
...(openPaths !== undefined ? { openPaths } : {}),
|
|
328
|
+
turnCeiling,
|
|
200
329
|
providerSpec: selection,
|
|
330
|
+
childProviderSpec: childSelection,
|
|
201
331
|
systemPrompt,
|
|
202
332
|
});
|
|
203
|
-
return { action, loopId, ...(turnSeq !== undefined ? { turnSeq } : {}) };
|
|
333
|
+
return { status: 100, action, loopId, ...(turnSeq !== undefined ? { turnSeq } : {}) };
|
|
204
334
|
}
|
|
205
|
-
//
|
|
206
|
-
// (<provider>/<model>, client-resolved
|
|
335
|
+
// {§methods-loop-run-model} — resolve a per-loop model override to a cached Provider. `model`
|
|
336
|
+
// (<provider>/<model>, client-resolved) wins over a named `alias`; absent both, the
|
|
207
337
|
// boot default. A named alias missing from the env cascade, or a malformed model spec, throws
|
|
208
338
|
// legibly rather than silently running the wrong model.
|
|
209
339
|
async #resolveLoopProvider(alias, model) {
|
|
@@ -211,43 +341,92 @@ export default class Daemon {
|
|
|
211
341
|
if (requested === null && this.#provider === null)
|
|
212
342
|
return null;
|
|
213
343
|
const spec = requested ?? resolveActiveAlias();
|
|
214
|
-
if (spec === null)
|
|
215
|
-
throw
|
|
216
|
-
|
|
344
|
+
if (spec === null) {
|
|
345
|
+
throw daemonFailure("daemon:provider", "active-alias-unresolved", 500, "The active provider has no resolvable alias.", { stage: "provider-selection", retryable: false });
|
|
346
|
+
}
|
|
347
|
+
// Resolve eagerly so runLoop fails before enqueue when the provider
|
|
217
348
|
// cannot be constructed. The drain later retrieves this cached handle
|
|
218
349
|
// from the loop's durable spec at the claim boundary.
|
|
219
|
-
|
|
350
|
+
try {
|
|
351
|
+
await ProviderInstantiate.instantiateProvider(spec);
|
|
352
|
+
}
|
|
353
|
+
catch (cause) {
|
|
354
|
+
if (cause instanceof OperationFailureError)
|
|
355
|
+
throw cause;
|
|
356
|
+
console.error(`Provider alias '${spec.alias}' could not be instantiated:`, cause);
|
|
357
|
+
throw daemonFailure("daemon:provider", "provider-unavailable", 503, `Provider alias '${spec.alias}' is unavailable.`, {
|
|
358
|
+
alias: spec.alias,
|
|
359
|
+
provider: spec.provider,
|
|
360
|
+
model: spec.model,
|
|
361
|
+
stage: "provider-selection",
|
|
362
|
+
retryable: false,
|
|
363
|
+
});
|
|
364
|
+
}
|
|
220
365
|
return spec;
|
|
221
366
|
}
|
|
222
|
-
|
|
223
|
-
const row = await this.#db.drain_loop_provider_spec.get({ loop_id: loopId });
|
|
224
|
-
if (row === undefined)
|
|
225
|
-
throw new Error(`loop ${loopId}: provider selection row is missing`);
|
|
367
|
+
#parseProviderSpec(loopId, field, encoded) {
|
|
226
368
|
let parsed;
|
|
227
369
|
try {
|
|
228
|
-
parsed = JSON.parse(
|
|
370
|
+
parsed = JSON.parse(encoded);
|
|
229
371
|
}
|
|
230
372
|
catch {
|
|
231
|
-
throw new Error(`loop ${loopId}: persisted
|
|
373
|
+
throw new Error(`loop ${loopId}: persisted ${field} is malformed`);
|
|
232
374
|
}
|
|
233
|
-
if (parsed === null
|
|
234
|
-
|
|
375
|
+
if (parsed === null)
|
|
376
|
+
return null;
|
|
377
|
+
if (typeof parsed.alias !== "string" || parsed.alias.length === 0
|
|
235
378
|
|| typeof parsed.provider !== "string" || parsed.provider.length === 0
|
|
236
379
|
|| typeof parsed.model !== "string" || parsed.model.length === 0
|
|
237
380
|
|| (parsed.baseUrl !== undefined && typeof parsed.baseUrl !== "string")) {
|
|
238
|
-
throw new Error(`loop ${loopId}: persisted
|
|
381
|
+
throw new Error(`loop ${loopId}: persisted ${field} is invalid`);
|
|
239
382
|
}
|
|
240
383
|
return parsed;
|
|
241
384
|
}
|
|
385
|
+
async #providerPolicyForLoop(loopId) {
|
|
386
|
+
const row = await this.#db.drain_loop_provider_spec.get({ loop_id: loopId });
|
|
387
|
+
if (row === undefined)
|
|
388
|
+
throw new Error(`loop ${loopId}: provider selection row is missing`);
|
|
389
|
+
const providerSpec = this.#parseProviderSpec(loopId, "provider_spec", row.provider_spec);
|
|
390
|
+
if (providerSpec === null) {
|
|
391
|
+
throw new Error(`loop ${loopId}: persisted provider selection is missing or invalid — refusing boot-default substitution`);
|
|
392
|
+
}
|
|
393
|
+
return {
|
|
394
|
+
providerSpec,
|
|
395
|
+
childProviderSpec: this.#parseProviderSpec(loopId, "child_provider_spec", row.child_provider_spec),
|
|
396
|
+
};
|
|
397
|
+
}
|
|
398
|
+
async #providerSpecForLoop(loopId) {
|
|
399
|
+
return (await this.#providerPolicyForLoop(loopId)).providerSpec;
|
|
400
|
+
}
|
|
242
401
|
async #providerForLoop(loopId) {
|
|
243
402
|
return ProviderInstantiate.instantiateProvider(await this.#providerSpecForLoop(loopId));
|
|
244
403
|
}
|
|
245
404
|
async #assertLoopProvider(loopId, requested) {
|
|
246
405
|
const selected = await this.#providerSpecForLoop(loopId);
|
|
247
406
|
if (JSON.stringify(selected) !== JSON.stringify(requested)) {
|
|
248
|
-
throw
|
|
249
|
-
|
|
250
|
-
|
|
407
|
+
throw daemonFailure("daemon:provider", "loop-provider-conflict", 409, `Loop ${loopId} uses provider alias '${selected.alias}', not '${requested.alias}'.`, {
|
|
408
|
+
loopId,
|
|
409
|
+
selectedAlias: selected.alias,
|
|
410
|
+
selectedModel: `${selected.provider}/${selected.model}`,
|
|
411
|
+
requestedAlias: requested.alias,
|
|
412
|
+
requestedModel: `${requested.provider}/${requested.model}`,
|
|
413
|
+
stage: "loop-injection",
|
|
414
|
+
recovery: "Cancel or conclude the loop before selecting another provider.",
|
|
415
|
+
retryable: false,
|
|
416
|
+
});
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
async #assertLoopChildProvider(loopId, requested) {
|
|
420
|
+
const selected = (await this.#providerPolicyForLoop(loopId)).childProviderSpec;
|
|
421
|
+
if (JSON.stringify(selected) !== JSON.stringify(requested)) {
|
|
422
|
+
throw daemonFailure("daemon:provider", "loop-child-provider-conflict", 409, `Loop ${loopId} already has a different child provider policy.`, {
|
|
423
|
+
loopId,
|
|
424
|
+
selectedChildAlias: selected?.alias ?? null,
|
|
425
|
+
requestedChildAlias: requested?.alias ?? null,
|
|
426
|
+
stage: "loop-injection",
|
|
427
|
+
recovery: "Cancel or conclude the loop before changing its child provider policy.",
|
|
428
|
+
retryable: false,
|
|
429
|
+
});
|
|
251
430
|
}
|
|
252
431
|
}
|
|
253
432
|
async #assertLoopMaxTurns(loopId, requested) {
|
|
@@ -257,29 +436,38 @@ export default class Daemon {
|
|
|
257
436
|
if (durable === undefined)
|
|
258
437
|
throw new Error(`inject: loop ${loopId} has no durable turn ceiling`);
|
|
259
438
|
if (durable.max_turns !== requested) {
|
|
260
|
-
throw
|
|
439
|
+
throw daemonFailure("daemon:loop", "turn-ceiling-conflict", 409, `Loop ${loopId} has turn ceiling ${durable.max_turns}, not ${requested}.`, {
|
|
440
|
+
loopId,
|
|
441
|
+
selectedMaximumTurns: durable.max_turns,
|
|
442
|
+
requestedMaximumTurns: requested,
|
|
443
|
+
stage: "loop-injection",
|
|
444
|
+
recovery: "Cancel or conclude the loop before selecting another turn ceiling.",
|
|
445
|
+
retryable: false,
|
|
446
|
+
});
|
|
261
447
|
}
|
|
262
448
|
}
|
|
263
|
-
// §
|
|
264
|
-
//
|
|
449
|
+
// {§methods-model-worker} — the workspace's model worker (created on first use), distinct from the client
|
|
450
|
+
// worker so the model's packets never carry client-action rows. The module binds its threads to this.
|
|
265
451
|
ensureModelWorker(workspaceId) {
|
|
266
|
-
return Envelope.ensureModelWorker(this.#db, workspaceId);
|
|
452
|
+
return Envelope.ensureModelWorker(this.#db, ClientInput.assertId("worker.ensure-model", "workspaceId", workspaceId));
|
|
267
453
|
}
|
|
268
|
-
//
|
|
454
|
+
// {§methods-op-mirror} — execute parsed ops on behalf of a client, journaled as a
|
|
269
455
|
// client-origin turn (the log is core's, a client op is a first-class citizen), dispatched through
|
|
270
456
|
// the engine, then emitted as log/entry on the event source. One seam op backs the whole op_*
|
|
271
457
|
// family (read/edit/copy/find/fold/look/move/open/send/exec); the module parses at its edge with the
|
|
272
458
|
// grammar package and hands over the statement, then fans the emitted entry out to its own clients.
|
|
273
459
|
async dispatchAsClient(args) {
|
|
274
|
-
const
|
|
460
|
+
const workspaceId = ClientInput.assertId("operation.dispatch", "workspaceId", args.workspaceId);
|
|
461
|
+
const workerId = ClientInput.assertId("operation.dispatch", "workerId", args.workerId);
|
|
462
|
+
const { statement } = args;
|
|
275
463
|
const clientLoopId = await Envelope.ensureClientLoop(this.#db, workerId);
|
|
276
464
|
try {
|
|
277
465
|
const result = await this.#dispatchClientStatement({ workspaceId, workerId, loopId: clientLoopId, statement });
|
|
278
|
-
await Envelope.closeClientLoop(this.#db, clientLoopId, 200);
|
|
466
|
+
await Envelope.closeClientLoop(this.#db, clientLoopId, { status: 200 });
|
|
279
467
|
return result;
|
|
280
468
|
}
|
|
281
469
|
catch (error) {
|
|
282
|
-
await Envelope.closeClientLoop(this.#db, clientLoopId,
|
|
470
|
+
await Envelope.closeClientLoop(this.#db, clientLoopId, clientActionFailure(error));
|
|
283
471
|
throw error;
|
|
284
472
|
}
|
|
285
473
|
}
|
|
@@ -288,7 +476,9 @@ export default class Daemon {
|
|
|
288
476
|
// keep this promise (and segment) open across interrupt/resume; settlement closes
|
|
289
477
|
// it. The journal is durable evidence for the action, not a second client lifecycle.
|
|
290
478
|
async dispatchClientAction(args) {
|
|
291
|
-
const
|
|
479
|
+
const workspaceId = ClientInput.assertId("operation.dispatch-batch", "workspaceId", args.workspaceId);
|
|
480
|
+
const workerId = ClientInput.assertId("operation.dispatch-batch", "workerId", args.workerId);
|
|
481
|
+
const { statements } = args;
|
|
292
482
|
if (statements.length === 0)
|
|
293
483
|
return [];
|
|
294
484
|
const clientLoopId = await Envelope.ensureClientLoop(this.#db, workerId);
|
|
@@ -297,58 +487,107 @@ export default class Daemon {
|
|
|
297
487
|
for (const statement of statements) {
|
|
298
488
|
results.push(await this.#dispatchClientStatement({ workspaceId, workerId, loopId: clientLoopId, statement }));
|
|
299
489
|
}
|
|
300
|
-
await Envelope.closeClientLoop(this.#db, clientLoopId, 200);
|
|
490
|
+
await Envelope.closeClientLoop(this.#db, clientLoopId, { status: 200 });
|
|
301
491
|
return results;
|
|
302
492
|
}
|
|
303
493
|
catch (error) {
|
|
304
|
-
await Envelope.closeClientLoop(this.#db, clientLoopId,
|
|
494
|
+
await Envelope.closeClientLoop(this.#db, clientLoopId, clientActionFailure(error));
|
|
305
495
|
throw error;
|
|
306
496
|
}
|
|
307
497
|
}
|
|
308
498
|
async #dispatchClientStatement(args) {
|
|
309
499
|
const { workspaceId, workerId, loopId, statement } = args;
|
|
310
|
-
const
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
this.#
|
|
500
|
+
const release = await this.#workspaceGate.acquireTurn(workspaceId, workerId);
|
|
501
|
+
try {
|
|
502
|
+
const { id: turnId } = await JournalTurn.insert(this.#db, loopId);
|
|
503
|
+
const entryIds = [];
|
|
504
|
+
const result = await this.#engine.dispatch({
|
|
505
|
+
statement, workspaceId, workerId, loopId, turnId, sequence: 1,
|
|
506
|
+
origin: "client", onDispatch: (logEntryId) => { entryIds.push(logEntryId); },
|
|
507
|
+
});
|
|
508
|
+
await this.#branchBatches.sealTurn(turnId);
|
|
509
|
+
for (const logEntryId of entryIds) {
|
|
510
|
+
const entry = await LogEntry.fetchLogEntry(this.#db, logEntryId);
|
|
511
|
+
this.#broadcast({ workspaceId }, "log/entry", { entry });
|
|
512
|
+
}
|
|
513
|
+
return result;
|
|
514
|
+
}
|
|
515
|
+
finally {
|
|
516
|
+
release();
|
|
319
517
|
}
|
|
320
|
-
return result;
|
|
321
518
|
}
|
|
322
|
-
// op
|
|
323
|
-
// full scheme resolver and return its content, writing NO log row — the client's
|
|
519
|
+
// {§op-look} — the pure READ-projection query on the seam: resolve a READ through the
|
|
520
|
+
// full scheme resolver and return its content, writing NO log row — the client's out-of-band
|
|
324
521
|
// inspection primitive (the module rewrites LOOK→READ and parses at its edge, exactly like
|
|
325
522
|
// dispatchClientAction). Its closed observation segment supplies the numeric loop coordinate
|
|
326
523
|
// required by plugin context and relative log:/// addresses without impersonating an active
|
|
327
524
|
// client lifecycle. It creates no turn or log row. Engine.look enforces READ-only.
|
|
328
525
|
async look(args) {
|
|
329
|
-
const
|
|
526
|
+
const workspaceId = ClientInput.assertId("operation.look", "workspaceId", args.workspaceId);
|
|
527
|
+
const workerId = ClientInput.assertId("operation.look", "workerId", args.workerId);
|
|
528
|
+
const { statement } = args;
|
|
529
|
+
const release = await this.#workspaceGate.acquireTurn(workspaceId, workerId);
|
|
330
530
|
const clientLoopId = await Envelope.ensureClientLoop(this.#db, workerId);
|
|
331
531
|
try {
|
|
332
532
|
const result = await this.#engine.look({ statement, workspaceId, workerId, loopId: clientLoopId });
|
|
333
|
-
await Envelope.closeClientLoop(this.#db, clientLoopId, 200);
|
|
533
|
+
await Envelope.closeClientLoop(this.#db, clientLoopId, { status: 200 });
|
|
334
534
|
return result;
|
|
335
535
|
}
|
|
336
536
|
catch (error) {
|
|
337
|
-
await Envelope.closeClientLoop(this.#db, clientLoopId,
|
|
537
|
+
await Envelope.closeClientLoop(this.#db, clientLoopId, clientActionFailure(error));
|
|
338
538
|
throw error;
|
|
339
539
|
}
|
|
540
|
+
finally {
|
|
541
|
+
release();
|
|
542
|
+
}
|
|
340
543
|
}
|
|
341
|
-
//
|
|
342
|
-
// ownership-verified against the workspace (a workspace reads only its own
|
|
343
|
-
//
|
|
544
|
+
// {§methods-log-read} — a workspace's journal, the module's primary render input. The worker is
|
|
545
|
+
// ownership-verified against the workspace (a workspace reads only its own workers — the model worker included,
|
|
546
|
+
// {§methods-log-coordinate}); entries filter by loop/turn/since-id or the full L/T/S display coordinate. Core owns the
|
|
344
547
|
// journal + the invariant; the module shapes the entries into AG-UI messages at its edge.
|
|
345
548
|
async readLog(args) {
|
|
346
|
-
const
|
|
549
|
+
const workspaceId = ClientInput.assertId("log.read", "workspaceId", args.workspaceId);
|
|
550
|
+
const workerId = ClientInput.assertId("log.read", "workerId", args.workerId);
|
|
347
551
|
const target = await this.#db.envelope_get_worker_by_id.get({ id: workerId });
|
|
348
|
-
if (target === undefined)
|
|
349
|
-
throw
|
|
350
|
-
|
|
351
|
-
|
|
552
|
+
if (target === undefined) {
|
|
553
|
+
throw daemonFailure("daemon:worker", "worker-not-found", 404, `Worker ${workerId} does not exist.`, { workerId });
|
|
554
|
+
}
|
|
555
|
+
if (target.workspace_id !== workspaceId) {
|
|
556
|
+
throw daemonFailure("daemon:worker", "workspace-mismatch", 409, `Worker ${workerId} does not belong to workspace ${workspaceId}.`, {
|
|
557
|
+
workerId,
|
|
558
|
+
workspaceId,
|
|
559
|
+
actualWorkspaceId: target.workspace_id,
|
|
560
|
+
retryable: false,
|
|
561
|
+
});
|
|
562
|
+
}
|
|
563
|
+
const coordinateFields = {
|
|
564
|
+
loopId: args.loopId,
|
|
565
|
+
turnId: args.turnId,
|
|
566
|
+
sinceId: args.sinceId,
|
|
567
|
+
loopSeq: args.loopSeq,
|
|
568
|
+
turnSeq: args.turnSeq,
|
|
569
|
+
sequence: args.sequence,
|
|
570
|
+
};
|
|
571
|
+
for (const [field, value] of Object.entries(coordinateFields)) {
|
|
572
|
+
if (value !== undefined && (!Number.isSafeInteger(value) || value < 0)) {
|
|
573
|
+
throw daemonFailure("daemon:log", "coordinate-invalid", 400, `Log coordinate field '${field}' is not a non-negative safe integer.`, {
|
|
574
|
+
field,
|
|
575
|
+
value,
|
|
576
|
+
stage: "log-read",
|
|
577
|
+
recovery: "Use a non-negative integer coordinate.",
|
|
578
|
+
retryable: false,
|
|
579
|
+
});
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
if (args.limit !== undefined && (!Number.isSafeInteger(args.limit) || args.limit < 1)) {
|
|
583
|
+
throw daemonFailure("daemon:log", "limit-invalid", 400, `Log limit ${args.limit} is not a positive safe integer.`, {
|
|
584
|
+
field: "limit",
|
|
585
|
+
value: args.limit,
|
|
586
|
+
stage: "log-read",
|
|
587
|
+
recovery: "Use a positive integer log limit.",
|
|
588
|
+
retryable: false,
|
|
589
|
+
});
|
|
590
|
+
}
|
|
352
591
|
const rows = await this.#db.log_read_recent_ids.all({
|
|
353
592
|
worker_id: workerId,
|
|
354
593
|
loop_id: args.loopId ?? null, turn_id: args.turnId ?? null, since_id: args.sinceId ?? null,
|
|
@@ -360,7 +599,7 @@ export default class Daemon {
|
|
|
360
599
|
entries.push(await LogEntry.fetchLogEntry(this.#db, r.id));
|
|
361
600
|
return entries;
|
|
362
601
|
}
|
|
363
|
-
//
|
|
602
|
+
// {§methods} — the module's render surface beyond the journal. Thin delegations
|
|
364
603
|
// into core's envelope / membership / provider machinery; the module fans the results into its own views.
|
|
365
604
|
listProviders() {
|
|
366
605
|
const active = resolveActiveAlias();
|
|
@@ -369,186 +608,368 @@ export default class Daemon {
|
|
|
369
608
|
const isActive = active !== null && active.alias === a.alias;
|
|
370
609
|
return {
|
|
371
610
|
alias: a.alias, provider: a.provider, model: a.model, active: isActive,
|
|
372
|
-
//
|
|
373
|
-
//
|
|
611
|
+
// The same effective model-facing budget loop usage reports, including
|
|
612
|
+
// optional virtual pressure; known for the active alias, null elsewhere.
|
|
374
613
|
promptBudget: isActive && this.#provider !== null ? this.#engine.promptBudgetFor(this.#provider) : null,
|
|
375
614
|
};
|
|
376
615
|
}),
|
|
377
616
|
};
|
|
378
617
|
}
|
|
618
|
+
// {§client-display-capabilities} Core composes the installed family
|
|
619
|
+
// declarations; interface modules expose this contracts-owned wire without
|
|
620
|
+
// inventing presentation policy. `exec` is operation machinery, not an
|
|
621
|
+
// addressable URI scheme; its runtime-tag scheme faces remain discoverable.
|
|
622
|
+
async listClientDisplayCapabilities() {
|
|
623
|
+
const schemes = this.#schemes.list()
|
|
624
|
+
.filter((scheme) => scheme !== "exec")
|
|
625
|
+
.map((scheme) => {
|
|
626
|
+
const glyph = this.#schemes.manifestFor(scheme)?.glyph;
|
|
627
|
+
return {
|
|
628
|
+
kind: "scheme",
|
|
629
|
+
scheme,
|
|
630
|
+
display: glyph === undefined ? {} : { glyph },
|
|
631
|
+
};
|
|
632
|
+
});
|
|
633
|
+
const mimetypes = (await this.#mimetypes.displayMetadata())
|
|
634
|
+
.map(({ mimetype, glyph }) => ({
|
|
635
|
+
kind: "mimetype",
|
|
636
|
+
mimetype,
|
|
637
|
+
display: glyph.length === 0 ? {} : { glyph },
|
|
638
|
+
}));
|
|
639
|
+
return Validator.assertClientDisplayCapabilities([...schemes, ...mimetypes]);
|
|
640
|
+
}
|
|
379
641
|
listWorkspaces() { return Envelope.listWorkspaces(this.#db); }
|
|
380
|
-
listWorkers(workspaceId) {
|
|
381
|
-
|
|
382
|
-
|
|
642
|
+
listWorkers(workspaceId) {
|
|
643
|
+
return Envelope.listWorkersForWorkspace(this.#db, ClientInput.assertId("workspace.workers", "workspaceId", workspaceId));
|
|
644
|
+
}
|
|
645
|
+
// {§methods-workspace-prompts}: root-conversation loop seeds, newest-first.
|
|
646
|
+
listPrompts(workspaceId, limit) {
|
|
647
|
+
const checkedWorkspaceId = ClientInput.assertId("workspace.prompts", "workspaceId", workspaceId);
|
|
648
|
+
const checkedLimit = ClientInput.assertLimit("workspace.prompts", limit);
|
|
649
|
+
return Envelope.listPromptsForWorkspace(this.#db, checkedWorkspaceId, checkedLimit ?? 100);
|
|
650
|
+
}
|
|
651
|
+
async listMembers(workspaceId) {
|
|
652
|
+
const checkedWorkspaceId = ClientInput.assertId("workspace.members", "workspaceId", workspaceId);
|
|
653
|
+
const release = await this.#workspaceGate.acquireTurn(checkedWorkspaceId, 0);
|
|
654
|
+
try {
|
|
655
|
+
return await GitMembership.resolveMembershipEffects(this.#db, checkedWorkspaceId, undefined);
|
|
656
|
+
}
|
|
657
|
+
finally {
|
|
658
|
+
release();
|
|
659
|
+
}
|
|
660
|
+
}
|
|
383
661
|
listConstraints(workspaceId) {
|
|
384
|
-
|
|
662
|
+
const checkedWorkspaceId = ClientInput.assertId("workspace.constraints", "workspaceId", workspaceId);
|
|
663
|
+
return this.#db.crud_list_workspace_constraints.all({ workspace_id: checkedWorkspaceId });
|
|
385
664
|
}
|
|
386
665
|
workspaceDerivationStatus(workspaceId) {
|
|
387
|
-
return this.#engine.workspaceDerivationStatus(workspaceId);
|
|
666
|
+
return this.#engine.workspaceDerivationStatus(ClientInput.assertId("workspace.derivation", "workspaceId", workspaceId));
|
|
388
667
|
}
|
|
389
|
-
//
|
|
390
|
-
//
|
|
391
|
-
// array, roots absolute); core owns the envelope, its reserved-name + name-uniqueness invariants,
|
|
668
|
+
// {§methods-workspace-create}: the module owns protocol decoding; core validates the typed seam
|
|
669
|
+
// inputs and owns the envelope, its reserved-name + name-uniqueness invariants,
|
|
392
670
|
// membership resolution, warmWorkspaceDerivations, and the workspace/created emit. No connection state
|
|
393
671
|
// (which client is on which workspace) lives here — that's the module's.
|
|
394
672
|
async createWorkspace(args) {
|
|
395
|
-
// The
|
|
396
|
-
//
|
|
397
|
-
//
|
|
673
|
+
// The seam fails hard on malformed semantic input so every module inherits one wall:
|
|
674
|
+
// the settings bag
|
|
675
|
+
// ({§operator-config-workspace-settings}),
|
|
676
|
+
// constraints, and absolute projectRoot.
|
|
677
|
+
const name = ClientInput.assertOptionalName("workspace.create", "name", args.name);
|
|
398
678
|
const projectRoot = ClientInput.assertProjectRoot("workspace.create", args.projectRoot);
|
|
399
679
|
const settings = ClientInput.parseSettings(args.settings);
|
|
400
680
|
const constraints = ClientInput.parseConstraints(args.constraints);
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
await this.#db
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
681
|
+
return observed(// {§observability-boundary}
|
|
682
|
+
"workspace.create", {}, async (span) => {
|
|
683
|
+
const envelope = await Envelope.createClientEnvelope(this.#db, { name, projectRoot, settings });
|
|
684
|
+
span.setAttribute("workspace.id", envelope.workspaceId);
|
|
685
|
+
for (const { effect, glob } of constraints) {
|
|
686
|
+
await this.#db.crud_insert_workspace_constraint.run({ workspace_id: envelope.workspaceId, effect, glob });
|
|
687
|
+
}
|
|
688
|
+
if (constraints.length > 0)
|
|
689
|
+
await GitMembership.resolveGitMembership(this.#db, envelope.workspaceId, undefined);
|
|
690
|
+
await LoopDocs.materialize(this.#engine, this.#db, envelope.workspaceId);
|
|
691
|
+
void this.#engine.warmWorkspaceDerivations(envelope.workspaceId).catch(() => { });
|
|
692
|
+
this.#broadcast("all", "workspace/created", { id: envelope.workspaceId, name: envelope.workspaceName, projectRoot: envelope.projectRoot });
|
|
693
|
+
return envelope;
|
|
694
|
+
});
|
|
411
695
|
}
|
|
412
696
|
async attachWorkspace(args) {
|
|
413
|
-
// attachToWorkspace owns the reserved-name +
|
|
414
|
-
const
|
|
697
|
+
// attachToWorkspace owns the reserved-name + worker-ownership invariants; the seam just delegates + warms.
|
|
698
|
+
const workspaceId = ClientInput.assertId("workspace.attach", "workspaceId", args.workspaceId);
|
|
699
|
+
const workerId = args.workerId === undefined
|
|
700
|
+
? undefined
|
|
701
|
+
: ClientInput.assertId("workspace.attach", "workerId", args.workerId);
|
|
702
|
+
const workerName = ClientInput.assertOptionalWorkerName("workspace.attach", "workerName", args.workerName);
|
|
703
|
+
const envelope = await Envelope.attachToWorkspace(this.#db, workspaceId, { workerId, workerName });
|
|
415
704
|
void this.#engine.warmWorkspaceDerivations(envelope.workspaceId).catch(() => { });
|
|
416
705
|
return envelope;
|
|
417
706
|
}
|
|
418
707
|
async renameWorkspace(workspaceId, name) {
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
return { id: workspaceId, name: await Envelope.updateWorkspaceName(this.#db, workspaceId, name) };
|
|
708
|
+
const checkedWorkspaceId = ClientInput.assertId("workspace.rename", "workspaceId", workspaceId);
|
|
709
|
+
const checkedName = ClientInput.assertOptionalName("workspace.rename", "name", name);
|
|
710
|
+
if (checkedName === undefined)
|
|
711
|
+
throw new Error("ClientInput.assertOptionalName accepted a required name as undefined");
|
|
712
|
+
return { id: checkedWorkspaceId, name: await Envelope.updateWorkspaceName(this.#db, checkedWorkspaceId, checkedName) };
|
|
425
713
|
}
|
|
426
714
|
async constrain(workspaceId, effect, glob) {
|
|
427
|
-
ClientInput.
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
void this.#engine.warmWorkspaceDerivations(workspaceId).catch(() => { });
|
|
442
|
-
return { effect, glob };
|
|
715
|
+
const checkedWorkspaceId = ClientInput.assertId("workspace.constrain", "workspaceId", workspaceId);
|
|
716
|
+
const release = await this.#workspaceGate.acquireTurn(checkedWorkspaceId, 0);
|
|
717
|
+
try {
|
|
718
|
+
ClientInput.assertConstraint("workspace.constrain", effect, glob);
|
|
719
|
+
await this.#db.crud_insert_workspace_constraint.run({ workspace_id: checkedWorkspaceId, effect, glob });
|
|
720
|
+
await GitMembership.resolveGitMembership(this.#db, checkedWorkspaceId, undefined);
|
|
721
|
+
// Members may have just landed — begin warming now, but return the constraint response
|
|
722
|
+
// immediately so prompts do not wait for the complete derivation corpus.
|
|
723
|
+
void this.#engine.warmWorkspaceDerivations(checkedWorkspaceId).catch(() => { });
|
|
724
|
+
return { effect, glob };
|
|
725
|
+
}
|
|
726
|
+
finally {
|
|
727
|
+
release();
|
|
728
|
+
}
|
|
443
729
|
}
|
|
444
730
|
async unconstrain(workspaceId, effect, glob) {
|
|
445
|
-
ClientInput.
|
|
446
|
-
await this.#
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
731
|
+
const checkedWorkspaceId = ClientInput.assertId("workspace.unconstrain", "workspaceId", workspaceId);
|
|
732
|
+
const release = await this.#workspaceGate.acquireTurn(checkedWorkspaceId, 0);
|
|
733
|
+
try {
|
|
734
|
+
ClientInput.assertConstraint("workspace.unconstrain", effect, glob);
|
|
735
|
+
await this.#db.crud_delete_workspace_constraint.run({ workspace_id: checkedWorkspaceId, effect, glob });
|
|
736
|
+
await GitMembership.resolveGitMembership(this.#db, checkedWorkspaceId, undefined);
|
|
737
|
+
void this.#engine.warmWorkspaceDerivations(checkedWorkspaceId).catch(() => { });
|
|
738
|
+
return { effect, glob };
|
|
739
|
+
}
|
|
740
|
+
finally {
|
|
741
|
+
release();
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
// Contracts {§entry-read-result}: resolve through the scheme's address law,
|
|
745
|
+
// then project one owner-scoped entry without exposing persistence columns.
|
|
454
746
|
async readEntry(args) {
|
|
455
|
-
const
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
let channelRows;
|
|
466
|
-
if (args.channel === undefined) {
|
|
467
|
-
channelRows = await this.#db.entry_read_channels.all({ entry_id: row.id });
|
|
747
|
+
const workspaceId = ClientInput.assertId("entry.read", "workspaceId", args.workspaceId);
|
|
748
|
+
const workerId = ClientInput.assertId("entry.read", "workerId", args.workerId);
|
|
749
|
+
if (typeof args.target !== "string" || args.target.length === 0) {
|
|
750
|
+
throw daemonFailure("daemon:input", "target-invalid", 400, "target is not a non-empty string.", {
|
|
751
|
+
context: "entry.read",
|
|
752
|
+
field: "target",
|
|
753
|
+
stage: "input-validation",
|
|
754
|
+
recovery: "Provide an entry URI.",
|
|
755
|
+
retryable: false,
|
|
756
|
+
});
|
|
468
757
|
}
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
758
|
+
const channel = ClientInput.assertOptionalChannel("entry.read", args.channel);
|
|
759
|
+
const worker = await this.#db.envelope_get_worker_by_id.get({ id: workerId });
|
|
760
|
+
if (worker === undefined) {
|
|
761
|
+
throw daemonFailure("daemon:worker", "worker-not-found", 404, `Worker ${workerId} does not exist.`, { workerId });
|
|
762
|
+
}
|
|
763
|
+
if (worker.workspace_id !== workspaceId) {
|
|
764
|
+
throw daemonFailure("daemon:worker", "workspace-mismatch", 409, `Worker ${workerId} does not belong to workspace ${workspaceId}.`, {
|
|
765
|
+
workerId,
|
|
766
|
+
workspaceId,
|
|
767
|
+
actualWorkspaceId: worker.workspace_id,
|
|
768
|
+
retryable: false,
|
|
769
|
+
});
|
|
770
|
+
}
|
|
771
|
+
const release = await this.#workspaceGate.acquireTurn(workspaceId, workerId);
|
|
772
|
+
try {
|
|
773
|
+
let parsed;
|
|
774
|
+
try {
|
|
775
|
+
parsed = parsePath(args.target);
|
|
776
|
+
}
|
|
777
|
+
catch {
|
|
778
|
+
parsed = null;
|
|
779
|
+
}
|
|
780
|
+
if (parsed === null || parsed.kind !== "url") {
|
|
781
|
+
return entryReadResult(Results.failure("daemon:entry", "target-invalid", 400, `The entry target '${args.target}' is not URL-shaped.`, { entry: null }, {
|
|
782
|
+
target: args.target,
|
|
783
|
+
stage: "entry-read",
|
|
784
|
+
recovery: "Use a scheme://path target.",
|
|
785
|
+
retryable: false,
|
|
786
|
+
}));
|
|
787
|
+
}
|
|
788
|
+
if (args.offset !== undefined && channel === undefined) {
|
|
789
|
+
return entryReadResult(Results.failure("daemon:entry", "offset-channel-required", 400, "An entry offset requires a channel.", { entry: null }, {
|
|
790
|
+
offset: args.offset,
|
|
791
|
+
stage: "entry-read",
|
|
792
|
+
recovery: "Select the channel to read from the offset.",
|
|
793
|
+
retryable: false,
|
|
794
|
+
}));
|
|
795
|
+
}
|
|
796
|
+
if (args.offset !== undefined && (!Number.isSafeInteger(args.offset) || args.offset < 0)) {
|
|
797
|
+
return entryReadResult(Results.failure("daemon:entry", "offset-invalid", 400, `Entry offset ${args.offset} is not a non-negative safe integer.`, { entry: null }, {
|
|
798
|
+
offset: args.offset,
|
|
799
|
+
stage: "entry-read",
|
|
800
|
+
recovery: "Use a non-negative integer offset.",
|
|
801
|
+
retryable: false,
|
|
802
|
+
}));
|
|
803
|
+
}
|
|
804
|
+
if (parsed.username !== null || parsed.password !== null) {
|
|
805
|
+
return entryReadResult(Results.failure("daemon:entry", "userinfo-not-allowed", 400, "Entry target URL userinfo is not allowed.", { entry: null }, {
|
|
806
|
+
stage: "entry-read",
|
|
807
|
+
recovery: "Remove credentials from the entry URL.",
|
|
808
|
+
retryable: false,
|
|
809
|
+
}));
|
|
810
|
+
}
|
|
811
|
+
const location = await this.#engine.resolveEntryAddress({
|
|
812
|
+
workspaceId,
|
|
813
|
+
workerId,
|
|
814
|
+
target: parsed,
|
|
815
|
+
});
|
|
816
|
+
if (location === null) {
|
|
817
|
+
return entryReadResult(Results.failure("daemon:entry", "entry-not-found", 404, "No visible entry exists at the requested target.", { entry: null }, { target: args.target }));
|
|
818
|
+
}
|
|
819
|
+
const row = await this.#db.entry_read_lookup.get({
|
|
820
|
+
workspace_id: workspaceId,
|
|
821
|
+
owner_id: location.ownerId,
|
|
822
|
+
scheme: location.scheme,
|
|
823
|
+
pathname: location.pathname,
|
|
824
|
+
});
|
|
825
|
+
if (row === undefined) {
|
|
826
|
+
return entryReadResult(Results.failure("daemon:entry", "entry-not-found", 404, `No visible entry exists at ${location.target}.`, { entry: null }, { target: location.target }));
|
|
827
|
+
}
|
|
828
|
+
let channelRows;
|
|
829
|
+
if (channel === undefined) {
|
|
830
|
+
channelRows = await this.#db.entry_read_channels.all({ entry_id: row.id });
|
|
831
|
+
}
|
|
832
|
+
else {
|
|
833
|
+
const r = await this.#db.entry_read_channel_slice.get({ entry_id: row.id, channel, offset: args.offset ?? 0 });
|
|
834
|
+
if (r === undefined) {
|
|
835
|
+
const availableChannels = (await this.#db.entry_read_channels.all({ entry_id: row.id }))
|
|
836
|
+
.map(({ name }) => name);
|
|
837
|
+
return entryReadResult(Results.failure("daemon:entry", "channel-not-found", 404, `Channel #${channel} does not exist at ${location.target}.`, { entry: null }, {
|
|
838
|
+
target: location.target,
|
|
839
|
+
requestedChannel: channel,
|
|
840
|
+
availableChannels,
|
|
841
|
+
...(availableChannels.length === 0
|
|
842
|
+
? {}
|
|
843
|
+
: { recovery: `Use one of the available channels: ${availableChannels.map((channel) => `#${channel}`).join(", ")}.` }),
|
|
844
|
+
retryable: false,
|
|
845
|
+
}));
|
|
846
|
+
}
|
|
847
|
+
channelRows = [r];
|
|
848
|
+
}
|
|
849
|
+
const channels = {};
|
|
850
|
+
for (const c of channelRows) {
|
|
851
|
+
channels[c.name] = {
|
|
852
|
+
content: c.content,
|
|
853
|
+
contentOffset: c.contentOffset,
|
|
854
|
+
contentLength: c.contentLength,
|
|
855
|
+
mimetype: c.mimetype,
|
|
856
|
+
tokens: c.tokens,
|
|
857
|
+
state: c.state,
|
|
858
|
+
};
|
|
859
|
+
}
|
|
860
|
+
const tagRows = await this.#db.crud_read_tags.all({ entry_id: row.id });
|
|
861
|
+
return entryReadResult({
|
|
862
|
+
status: 200,
|
|
863
|
+
entry: {
|
|
864
|
+
entryId: row.id,
|
|
865
|
+
target: location.target,
|
|
866
|
+
channels,
|
|
867
|
+
tags: tagRows.map((tag) => tag.tag),
|
|
868
|
+
},
|
|
869
|
+
});
|
|
870
|
+
}
|
|
871
|
+
finally {
|
|
872
|
+
release();
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
// {§methods-conversation-worker}: a fresh conversation is a model-origin root worker with an empty private log.
|
|
876
|
+
// AG-UI threads map to these workers while the workspace world remains shared ({§machine-processes}).
|
|
486
877
|
async createConversationWorker(args) {
|
|
487
|
-
const
|
|
488
|
-
|
|
489
|
-
throw new Error("run.create: name must be a non-empty string");
|
|
878
|
+
const workspaceId = ClientInput.assertId("worker.create", "workspaceId", args.workspaceId);
|
|
879
|
+
const name = ClientInput.assertOptionalWorkerName("worker.create", "name", args.name);
|
|
490
880
|
const workspace = await this.#db.envelope_get_workspace.get({ id: workspaceId });
|
|
491
|
-
if (workspace === undefined)
|
|
492
|
-
throw
|
|
881
|
+
if (workspace === undefined) {
|
|
882
|
+
throw daemonFailure("daemon:workspace", "workspace-not-found", 404, `Workspace ${workspaceId} does not exist.`, { workspaceId });
|
|
883
|
+
}
|
|
493
884
|
if (name !== undefined) {
|
|
494
|
-
if (Envelope.RESERVED_RUN_NAMES.has(name.toLowerCase()))
|
|
495
|
-
throw new Error(`run.create: name "${name}" is reserved for a non-client actor`);
|
|
496
885
|
const taken = await this.#db.envelope_get_worker_by_name.get({ workspace_id: workspaceId, name });
|
|
497
|
-
if (taken !== undefined)
|
|
498
|
-
throw
|
|
886
|
+
if (taken !== undefined) {
|
|
887
|
+
throw daemonFailure("daemon:worker", "name-conflict", 409, `Worker name '${name}' is already in use in workspace ${workspaceId}.`, { workspaceId, name, recovery: "Choose another worker name.", retryable: false });
|
|
888
|
+
}
|
|
499
889
|
}
|
|
500
|
-
const
|
|
501
|
-
return { workerId:
|
|
890
|
+
const worker = await Envelope.createModelWorker(this.#db, workspaceId, name);
|
|
891
|
+
return { workerId: worker.id, workerName: worker.name };
|
|
502
892
|
}
|
|
503
|
-
//
|
|
893
|
+
// {§worker-scheme-fork} — branch a worker's log while sharing the workspace world.
|
|
894
|
+
// Core owns the workspace check and immutable worker-name admission.
|
|
504
895
|
async forkWorker(args) {
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
const
|
|
896
|
+
const workspaceId = ClientInput.assertId("worker.fork", "workspaceId", args.workspaceId);
|
|
897
|
+
const workerId = ClientInput.assertId("worker.fork", "workerId", args.workerId);
|
|
898
|
+
const name = ClientInput.assertOptionalWorkerName("worker.fork", "name", args.name);
|
|
508
899
|
const owner = await this.#db.envelope_get_worker_by_id.get({ id: workerId });
|
|
509
|
-
if (owner === undefined)
|
|
510
|
-
throw
|
|
511
|
-
|
|
512
|
-
|
|
900
|
+
if (owner === undefined) {
|
|
901
|
+
throw daemonFailure("daemon:worker", "worker-not-found", 404, `Worker ${workerId} does not exist.`, { workerId });
|
|
902
|
+
}
|
|
903
|
+
if (owner.workspace_id !== workspaceId) {
|
|
904
|
+
throw daemonFailure("daemon:worker", "workspace-mismatch", 409, `Worker ${workerId} does not belong to workspace ${workspaceId}.`, {
|
|
905
|
+
workerId,
|
|
906
|
+
workspaceId,
|
|
907
|
+
actualWorkspaceId: owner.workspace_id,
|
|
908
|
+
retryable: false,
|
|
909
|
+
});
|
|
910
|
+
}
|
|
513
911
|
if (name !== undefined) {
|
|
514
|
-
if (Envelope.RESERVED_RUN_NAMES.has(name.toLowerCase()))
|
|
515
|
-
throw new Error(`forkWorker: name "${name}" is reserved for a non-client actor`);
|
|
516
912
|
const taken = await this.#db.envelope_get_worker_by_name.get({ workspace_id: workspaceId, name });
|
|
517
|
-
if (taken !== undefined)
|
|
518
|
-
throw
|
|
913
|
+
if (taken !== undefined) {
|
|
914
|
+
throw daemonFailure("daemon:worker", "name-conflict", 409, `Worker name '${name}' is already in use in workspace ${workspaceId}.`, { workspaceId, name, recovery: "Choose another worker name.", retryable: false });
|
|
915
|
+
}
|
|
519
916
|
}
|
|
520
917
|
const branchWorkerId = await Fork.fork(this.#db, workerId, name);
|
|
521
918
|
const branch = await this.#db.envelope_get_worker_by_id.get({ id: branchWorkerId });
|
|
522
919
|
return { workerId: branchWorkerId, workerName: branch?.name ?? null, parentWorkerId: workerId };
|
|
523
920
|
}
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
// (reserved / cross-family collision, #240) gates the tag before registering.
|
|
530
|
-
hotloadRuntime(reg) {
|
|
531
|
-
const { decl, executor, availability } = reg;
|
|
532
|
-
this.#engine.hotloadRuntime(decl.name, {
|
|
921
|
+
async registerRuntime({ namespaceOwner, decl, executor, availability, scheme }) {
|
|
922
|
+
if (typeof namespaceOwner !== "string" || namespaceOwner.trim().length === 0) {
|
|
923
|
+
throw new Error("registerRuntime: namespaceOwner must be a non-empty string");
|
|
924
|
+
}
|
|
925
|
+
this.#engine.registerRuntime(decl.name, {
|
|
533
926
|
executor,
|
|
927
|
+
namespaceOwner: { kind: "module", name: namespaceOwner },
|
|
534
928
|
glyph: decl.glyph ?? "",
|
|
535
929
|
example: decl.example ?? "",
|
|
536
930
|
documentation: decl.documentation ?? "",
|
|
537
931
|
available: availability.available,
|
|
538
932
|
detail: availability.detail,
|
|
539
|
-
});
|
|
933
|
+
}, scheme);
|
|
934
|
+
if (this.#capabilitiesPublished) {
|
|
935
|
+
for (const workspace of await Envelope.listWorkspaces(this.#db)) {
|
|
936
|
+
await LoopDocs.materialize(this.#engine, this.#db, workspace.id);
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
async registerScheme(name, handler) {
|
|
941
|
+
this.#schemes.register(name, handler);
|
|
942
|
+
if (this.#capabilitiesPublished) {
|
|
943
|
+
await this.#schemes.ready();
|
|
944
|
+
for (const workspace of await Envelope.listWorkspaces(this.#db)) {
|
|
945
|
+
await LoopDocs.materialize(this.#engine, this.#db, workspace.id);
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
registerModuleAction(name, handler) {
|
|
950
|
+
if (name.length === 0)
|
|
951
|
+
throw new Error("registerModuleAction: action name must not be empty");
|
|
952
|
+
if (this.#moduleActions.has(name))
|
|
953
|
+
throw new Error(`module action '${name}' is already registered`);
|
|
954
|
+
this.#moduleActions.set(name, handler);
|
|
955
|
+
}
|
|
956
|
+
listModuleActions() {
|
|
957
|
+
return [...this.#moduleActions.keys()].toSorted();
|
|
958
|
+
}
|
|
959
|
+
async invokeModuleAction(name, params) {
|
|
960
|
+
const handler = this.#moduleActions.get(name);
|
|
961
|
+
if (handler === undefined)
|
|
962
|
+
throw new Error(`module action '${name}' is not registered`);
|
|
963
|
+
return handler(params);
|
|
540
964
|
}
|
|
541
965
|
get engine() { return this.#engine; }
|
|
542
966
|
get provider() { return this.#provider; }
|
|
543
967
|
get schemes() { return this.#schemes; }
|
|
544
968
|
get mimetypes() { return this.#mimetypes; }
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
#moduleInits = [];
|
|
550
|
-
registerModule(init) {
|
|
551
|
-
this.#moduleInits.push(init);
|
|
969
|
+
registerModule(module) {
|
|
970
|
+
if (this.#started)
|
|
971
|
+
throw new Error("registerModule: modules must be registered before daemon start");
|
|
972
|
+
this.#modules.push(module);
|
|
552
973
|
}
|
|
553
974
|
async start() {
|
|
554
975
|
if (this.#started)
|
|
@@ -557,18 +978,27 @@ export default class Daemon {
|
|
|
557
978
|
// Mimetypes owns its own discovery scan over @plurnk/plurnk-mimetypes-*
|
|
558
979
|
// packages; pre-warm it so first index render doesn't pay the cost.
|
|
559
980
|
await this.#mimetypes.ready();
|
|
981
|
+
for (const name of await this.#mimetypes.skippedPackages()) {
|
|
982
|
+
console.warn(`mimetype discovery: '${name}' is discovered but untrusted (PLURNK_PLUGINS_TRUSTED_ONLY); not registered`);
|
|
983
|
+
}
|
|
560
984
|
// Discover + probe the installed executor siblings, then hand the
|
|
561
|
-
// registry to the engine for exec dispatch (
|
|
985
|
+
// registry to the engine for exec dispatch ({§exec-registry-resolves}). The
|
|
562
986
|
// shell is the default runtime, so its executor must boot usable.
|
|
563
987
|
const executors = await ExecutorRegistry.build({ defaultRuntime: "sh", cwd: this.#discoveryCwd });
|
|
564
988
|
this.#engine.setExecutors(executors);
|
|
565
|
-
// §exec — mint a scheme per runtime tag so exec output entries address by tag
|
|
989
|
+
// {§exec} — mint a scheme per runtime tag so exec output entries address by tag
|
|
566
990
|
// authority (sh:///l/t/s). The "exec" scheme stays for the EXEC op dispatch.
|
|
567
991
|
this.#schemes.registerRuntimeSchemes(executors);
|
|
568
992
|
// Discover external @plurnk/plurnk-schemes-* siblings + register them
|
|
569
993
|
// (agnostic, by plurnk.kind:"scheme"). They light up http://, etc. with
|
|
570
|
-
// no further engine change — #run wraps their
|
|
994
|
+
// no further engine change — #run wraps their context in SchemeCtxImpl ({§plugin-discovery}).
|
|
571
995
|
await this.#schemes.discoverExternal(this.#discoveryCwd);
|
|
996
|
+
const setupSeam = this;
|
|
997
|
+
for (const module of this.#modules) {
|
|
998
|
+
if (module.close !== undefined)
|
|
999
|
+
this.#moduleClosers.push(module);
|
|
1000
|
+
await module.setup?.(setupSeam);
|
|
1001
|
+
}
|
|
572
1002
|
await this.#schemes.ready();
|
|
573
1003
|
// Reconcile the kernel-published documentation surface once per existing workspace.
|
|
574
1004
|
// Installed capabilities and operator configuration are now fully known; model loops
|
|
@@ -576,16 +1006,29 @@ export default class Daemon {
|
|
|
576
1006
|
for (const workspace of await Envelope.listWorkspaces(this.#db)) {
|
|
577
1007
|
await LoopDocs.materialize(this.#engine, this.#db, workspace.id);
|
|
578
1008
|
}
|
|
1009
|
+
this.#capabilitiesPublished = true;
|
|
579
1010
|
await this.#recoverLifecycle();
|
|
580
|
-
//
|
|
581
|
-
|
|
582
|
-
|
|
1011
|
+
// {§module-lifecycle} — the daemon opens no transport. Modules start their listeners only
|
|
1012
|
+
// after capability publication and durable lifecycle recovery are complete.
|
|
1013
|
+
for (const module of this.#modules) {
|
|
1014
|
+
const started = await module.start?.(this);
|
|
1015
|
+
if (started !== undefined && !this.#moduleClosers.includes(started)) {
|
|
1016
|
+
this.#moduleClosers.push(started);
|
|
1017
|
+
}
|
|
1018
|
+
}
|
|
583
1019
|
}
|
|
584
1020
|
async #recoverLifecycle() {
|
|
585
1021
|
await this.#db.recovery_fail_active_loops.run({});
|
|
1022
|
+
await this.#db.recovery_fail_open_provider_attempts.run({});
|
|
1023
|
+
await this.#db.recovery_fail_ownerless_proposals.run({});
|
|
586
1024
|
await this.#db.recovery_error_orphan_subscription_channels.run({});
|
|
587
1025
|
await this.#db.recovery_fail_orphan_subscriptions.run({});
|
|
588
1026
|
await this.#db.recovery_resume_unblocked_parks.run({});
|
|
1027
|
+
await this.#branchBatches.recover();
|
|
1028
|
+
const orphanSources = await this.#db.recovery_orphan_prompt_sources.all({});
|
|
1029
|
+
for (const source of orphanSources) {
|
|
1030
|
+
await this.#reconcileOrphanedPrompts(source.worker_id, source.loop_id);
|
|
1031
|
+
}
|
|
589
1032
|
const systemPrompt = await readFile(Paths.instructionsSystem, "utf8");
|
|
590
1033
|
const queued = await this.#db.recovery_queued_workers.all({});
|
|
591
1034
|
for (const row of queued) {
|
|
@@ -607,6 +1050,13 @@ export default class Daemon {
|
|
|
607
1050
|
if (!this.#started)
|
|
608
1051
|
return;
|
|
609
1052
|
this.#started = false;
|
|
1053
|
+
// Stop accepting external work immediately, but do not await listener
|
|
1054
|
+
// closure before cancelling active workers: an SSE connection may itself be
|
|
1055
|
+
// waiting for the worker cancellation that follows.
|
|
1056
|
+
const moduleClose = Promise.allSettled(this.#moduleClosers
|
|
1057
|
+
.toReversed()
|
|
1058
|
+
.map((module) => Promise.resolve().then(() => module.close())));
|
|
1059
|
+
this.#moduleClosers = [];
|
|
610
1060
|
// Drain order: (1) abort in-flight loops via #activeDrains so
|
|
611
1061
|
// strike paths don't keep going, (2) await each drain's promise
|
|
612
1062
|
// to completion, (3) drain streaming schemes' background work
|
|
@@ -614,12 +1064,15 @@ export default class Daemon {
|
|
|
614
1064
|
// upstream — drain queries hit the DB right up until they exit.
|
|
615
1065
|
// Abort every worker's cancellation scope — stops in-flight loops AND the
|
|
616
1066
|
// streams (background execs) linked to them, so idle() doesn't block on
|
|
617
|
-
// a long-running command. Covers
|
|
1067
|
+
// a long-running command. Covers workers whose drain already exited but
|
|
618
1068
|
// whose exec is still in flight.
|
|
619
1069
|
// Settle the stopped world FIRST: a drain paused at a pending proposal awaits a resolution
|
|
620
1070
|
// that will never arrive once clients are gone — allSettled(drains) below would deadlock
|
|
621
1071
|
// the stop forever (a daemon with a pending HITL proposal could not shut down).
|
|
1072
|
+
const derivationAbort = new DOMException("daemon stopping", "AbortError");
|
|
622
1073
|
this.#engine.cancelAllProposals("daemon_stopping");
|
|
1074
|
+
this.#engine.cancelDerivations(derivationAbort);
|
|
1075
|
+
this.#branchBatches.beginStop();
|
|
623
1076
|
for (const scope of this.#workerAborts.values()) {
|
|
624
1077
|
if (!scope.signal.aborted)
|
|
625
1078
|
scope.abort("daemon_stopping");
|
|
@@ -628,17 +1081,32 @@ export default class Daemon {
|
|
|
628
1081
|
clearTimeout(t); // drop pending hibernation poll-wakes
|
|
629
1082
|
this.#pollBackoff.clear();
|
|
630
1083
|
this.#pollTimers.clear();
|
|
631
|
-
//
|
|
1084
|
+
// Cancel park-deadline timers before DB close; otherwise a late #wakeParkedWorker would run after
|
|
632
1085
|
// stop/db-close if left pending — an unhandled rejection (SqlRite closed) that abnormally
|
|
633
1086
|
// exits the worker under load. Symmetric with the poll-wakes above; both must be reaped.
|
|
634
1087
|
for (const t of this.#parkTimers.values())
|
|
635
1088
|
clearTimeout(t);
|
|
636
1089
|
this.#parkTimers.clear();
|
|
1090
|
+
await this.#branchBatches.idle();
|
|
637
1091
|
const drainPromises = [...this.#activeDrains.values()].map((d) => d.promise);
|
|
638
1092
|
await Promise.allSettled(drainPromises);
|
|
639
|
-
await this.#
|
|
640
|
-
|
|
641
|
-
await this.#
|
|
1093
|
+
await Promise.allSettled([...this.#drainExitTasks]);
|
|
1094
|
+
const closeResults = await moduleClose;
|
|
1095
|
+
const [streamingResult] = await Promise.allSettled([this.#drainStreamingSchemes()]);
|
|
1096
|
+
const [derivationResult] = await Promise.allSettled([
|
|
1097
|
+
this.#engine.drainDerivations(derivationAbort), // active workspace warms settle before the db closes upstream
|
|
1098
|
+
]);
|
|
1099
|
+
const mimetypeResults = this.#ownsMimetypes
|
|
1100
|
+
? await Promise.allSettled([this.#mimetypes.dispose()])
|
|
1101
|
+
: [];
|
|
1102
|
+
const [schemeResult] = await Promise.allSettled([this.#schemes.close()]);
|
|
1103
|
+
const closeErrors = [...closeResults, streamingResult, derivationResult, ...mimetypeResults, schemeResult]
|
|
1104
|
+
.filter((result) => result.status === "rejected")
|
|
1105
|
+
.flatMap((result) => result.reason instanceof AggregateError
|
|
1106
|
+
? [...result.reason.errors]
|
|
1107
|
+
: [result.reason]);
|
|
1108
|
+
if (closeErrors.length > 0)
|
|
1109
|
+
throw new AggregateError(closeErrors, "daemon shutdown failed");
|
|
642
1110
|
}
|
|
643
1111
|
// Per-scheme idle awaits for clean shutdown. New streaming schemes
|
|
644
1112
|
// (SSE, WS) add themselves here as they land.
|
|
@@ -650,48 +1118,47 @@ export default class Daemon {
|
|
|
650
1118
|
/**
|
|
651
1119
|
* Emit a stream/event notification scoped to the workspace containing the
|
|
652
1120
|
* entry. ChannelWrite helpers (src/core/ChannelWrite.ts) invoke this when
|
|
653
|
-
* they update channel content or state. SPEC §notifications.
|
|
1121
|
+
* they update channel content or state. SPEC {§notifications}.
|
|
654
1122
|
*/
|
|
655
1123
|
notifyStreamEvent(workspaceId, event) {
|
|
656
1124
|
this.#broadcast({ workspaceId }, "stream/event", event);
|
|
657
1125
|
}
|
|
658
1126
|
/**
|
|
659
|
-
* Emit a
|
|
660
|
-
* the loop. TelemetryChannel.push invokes this for every TelemetryEvent
|
|
661
|
-
* (parse_error, strike, cycle, sudden_death, no_ops, max_commands_exceeded,
|
|
662
|
-
* action_failure) the moment it lands in the loop's telemetry buffer.
|
|
663
|
-
* SPEC §telemetry.
|
|
1127
|
+
* Emit a transient notice scoped to the workspace containing the loop.
|
|
664
1128
|
*/
|
|
665
|
-
|
|
666
|
-
this.#broadcast({ workspaceId }, "
|
|
1129
|
+
notifyNotice(workspaceId, payload) {
|
|
1130
|
+
this.#broadcast({ workspaceId }, "notice/event", payload);
|
|
667
1131
|
}
|
|
668
1132
|
/**
|
|
669
1133
|
* Inject a prompt into a worker. Two paths:
|
|
670
|
-
* - Active drain: writes
|
|
671
|
-
*
|
|
1134
|
+
* - Active drain: writes the next prompt:///<loop>/<N> entry via
|
|
1135
|
+
* Engine.inject. The current loop publishes it at its next
|
|
672
1136
|
* turn. Returns immediately with {action: "injected_next_turn"}.
|
|
673
1137
|
* - No active drain: enqueues a fresh loop with the prompt at
|
|
674
1138
|
* status=100, starts a drain. Returns the drain promise so the
|
|
675
1139
|
* caller can await full completion.
|
|
676
1140
|
*
|
|
677
|
-
*
|
|
678
|
-
*
|
|
1141
|
+
* Both `runLoop` and wake-on-completion go through this method
|
|
1142
|
+
* ({§actor-boundary-passive-wake}).
|
|
679
1143
|
*/
|
|
680
|
-
//
|
|
681
|
-
// prompt folding into a live/parked loop cannot re-flag it mid-flight — and it must never PRETEND
|
|
682
|
-
// to: an inject carrying flags that DIFFER from the target loop's effective flags is refused
|
|
683
|
-
// legibly (cancel the loop or omit the flags), never a silent posture discard. Identical or
|
|
684
|
-
// absent flags fold clean.
|
|
1144
|
+
// {§methods-loop-run-fold-consistency} — a folded prompt cannot reconfigure its loop.
|
|
685
1145
|
async #assertFoldPosture(workerId, flags, loopId) {
|
|
686
1146
|
if (flags === undefined || Object.keys(flags).length === 0)
|
|
687
1147
|
return;
|
|
688
|
-
const
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
1148
|
+
const effective = await LoopFlagsReader.read(this.#db, loopId);
|
|
1149
|
+
const requested = Object.entries(flags);
|
|
1150
|
+
const conflicts = requested
|
|
1151
|
+
.filter(([key, value]) => value !== undefined && effective[key] !== value)
|
|
1152
|
+
.map(([key, value]) => `${key}: ${JSON.stringify(effective[key])} -> ${JSON.stringify(value)}`);
|
|
693
1153
|
if (conflicts.length > 0) {
|
|
694
|
-
throw
|
|
1154
|
+
throw daemonFailure("daemon:loop", "loop-flags-conflict", 409, "The requested loop flags differ from the active loop flags.", {
|
|
1155
|
+
workerId,
|
|
1156
|
+
loopId,
|
|
1157
|
+
conflicts,
|
|
1158
|
+
stage: "loop-injection",
|
|
1159
|
+
recovery: "Cancel the active loop before changing flags, or omit flags to keep its current posture.",
|
|
1160
|
+
retryable: false,
|
|
1161
|
+
});
|
|
695
1162
|
}
|
|
696
1163
|
}
|
|
697
1164
|
async inject(args) {
|
|
@@ -700,29 +1167,33 @@ export default class Daemon {
|
|
|
700
1167
|
// engine.inject returns null when no loop is currently executing, so
|
|
701
1168
|
// we enqueue a fresh loop below and ensure a drain claims it.
|
|
702
1169
|
if (this.#activeDrains.has(workerId)) {
|
|
703
|
-
await this.#assertFoldPosture(workerId, args.flags); // #368 — a fold never silently discards intent
|
|
704
1170
|
const active = await this.#db.drain_current_loop_for_worker.get({ worker_id: workerId });
|
|
705
1171
|
if (active !== undefined) {
|
|
1172
|
+
await this.#assertFoldPosture(workerId, args.flags, active.id); // compare with the exact durable loop
|
|
706
1173
|
await this.#assertLoopProvider(active.id, args.providerSpec);
|
|
707
|
-
|
|
1174
|
+
if (args.childProviderSpec !== undefined)
|
|
1175
|
+
await this.#assertLoopChildProvider(active.id, args.childProviderSpec);
|
|
1176
|
+
await this.#assertLoopMaxTurns(active.id, args.turnCeiling?.source === "explicit" ? args.turnCeiling.effective : undefined);
|
|
708
1177
|
}
|
|
709
|
-
const result = await this.#engine.inject(workerId, prompt);
|
|
1178
|
+
const result = await this.#engine.inject(workerId, prompt, args.openPaths ?? []);
|
|
710
1179
|
if (result !== null) {
|
|
711
1180
|
return { action: "injected_next_turn", loopId: result.loopId, turnSeq: result.turnSeq };
|
|
712
1181
|
}
|
|
713
1182
|
}
|
|
714
|
-
//
|
|
1183
|
+
// {§worker-lifecycle-wake-requeue-not-terminal} — a worker parked at 202 resumes that loop in place:
|
|
715
1184
|
// is a wake edge like a stream/child conclusion, not a fresh loop that orphans the parked one
|
|
716
1185
|
// (which would leave the worker non-quiescent forever). engine.inject writes the message as the
|
|
717
1186
|
// slept loop's next-turn prompt (the directed message — distinct from the env door, which
|
|
718
|
-
// resumes promptless); then re-queue + drain it. §worker-lifecycle-wake-liveness.
|
|
1187
|
+
// resumes promptless); then re-queue + drain it. {§worker-lifecycle-wake-liveness}.
|
|
719
1188
|
if (!this.#activeDrains.has(workerId)) {
|
|
720
1189
|
const slept = await this.#db.drain_find_slept_loop.get({ worker_id: workerId });
|
|
721
1190
|
if (slept !== undefined) {
|
|
722
|
-
await this.#assertFoldPosture(workerId, args.flags, slept.id); //
|
|
1191
|
+
await this.#assertFoldPosture(workerId, args.flags, slept.id); // resume drops nothing silently
|
|
723
1192
|
await this.#assertLoopProvider(slept.id, args.providerSpec);
|
|
724
|
-
|
|
725
|
-
|
|
1193
|
+
if (args.childProviderSpec !== undefined)
|
|
1194
|
+
await this.#assertLoopChildProvider(slept.id, args.childProviderSpec);
|
|
1195
|
+
await this.#assertLoopMaxTurns(slept.id, args.turnCeiling?.source === "explicit" ? args.turnCeiling.effective : undefined);
|
|
1196
|
+
const injected = await this.#engine.inject(workerId, prompt, args.openPaths ?? []);
|
|
726
1197
|
await this.#lifecycle.wake(slept.id);
|
|
727
1198
|
const started = await this.#ensureDrain({
|
|
728
1199
|
workspaceId, workerId, systemPrompt: args.systemPrompt,
|
|
@@ -730,44 +1201,63 @@ export default class Daemon {
|
|
|
730
1201
|
return { action: "injected_next_turn", loopId: slept.id, ...(injected?.turnSeq !== undefined ? { turnSeq: injected.turnSeq } : {}), ...(started ?? {}) };
|
|
731
1202
|
}
|
|
732
1203
|
}
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
1204
|
+
const loopId = await this.#enqueueFreshLoop({
|
|
1205
|
+
workerId,
|
|
1206
|
+
prompt,
|
|
1207
|
+
providerSpec: args.providerSpec,
|
|
1208
|
+
childProviderSpec: args.childProviderSpec ?? null,
|
|
1209
|
+
maxTurns: args.turnCeiling?.effective,
|
|
1210
|
+
flags: args.flags,
|
|
1211
|
+
openPaths: args.openPaths,
|
|
741
1212
|
});
|
|
742
|
-
if (loopRow === undefined)
|
|
743
|
-
throw new Error("inject: loop enqueue returned no row");
|
|
744
|
-
const loopId = loopRow.id;
|
|
745
|
-
if (args.flags !== undefined) {
|
|
746
|
-
const merged = { ...DEFAULT_LOOP_FLAGS, ...args.flags };
|
|
747
|
-
await this.#db.engine_set_loop_flags.run({
|
|
748
|
-
loop_id: loopId, flags: JSON.stringify(merged),
|
|
749
|
-
});
|
|
750
|
-
}
|
|
751
|
-
// #260 — persist client-passed @file paths before the drain claims the loop, so turn 0 foists them.
|
|
752
|
-
if (args.openPaths !== undefined && args.openPaths.length > 0) {
|
|
753
|
-
await this.#db.engine_set_loop_open_paths.run({
|
|
754
|
-
loop_id: loopId, open_paths: JSON.stringify(args.openPaths),
|
|
755
|
-
});
|
|
756
|
-
}
|
|
757
1213
|
// Guarantee a drain claims the loop we just enqueued. #ensureDrain runs its
|
|
758
|
-
// check-and-start UNDER the per-worker drain lock (§worker-lifecycle-single-drain),
|
|
1214
|
+
// check-and-start UNDER the per-worker drain lock ({§worker-lifecycle-single-drain}),
|
|
759
1215
|
// serialized against a draining sibling's teardown relinquish so the two can't
|
|
760
1216
|
// both register a drain (R4). A live drain re-claims the loop in its own
|
|
761
1217
|
// iteration or its lock-held exit re-claim, so it's never stranded.
|
|
762
|
-
// firstLoopPromise is present only when THIS call started the drain —
|
|
1218
|
+
// firstLoopPromise is present only when THIS call started the drain — runLoop
|
|
763
1219
|
// keys its fast-path response on that.
|
|
764
1220
|
const started = await this.#ensureDrain({
|
|
765
1221
|
workspaceId, workerId, systemPrompt: args.systemPrompt,
|
|
766
1222
|
});
|
|
767
1223
|
return { action: "enqueued_new_loop", loopId, ...(started ?? {}) };
|
|
768
1224
|
}
|
|
1225
|
+
async #enqueueFreshLoop(args) {
|
|
1226
|
+
// {§worker-lifecycle-single-drain}: sequence allocation and insertion
|
|
1227
|
+
// are one queue mutation; another accepted prompt cannot claim the gap.
|
|
1228
|
+
return this.#withDrainLock(args.workerId, async () => {
|
|
1229
|
+
const seqRow = await this.#db.loop_run_next_sequence.get({
|
|
1230
|
+
worker_id: args.workerId,
|
|
1231
|
+
});
|
|
1232
|
+
if (seqRow === undefined)
|
|
1233
|
+
throw new Error("enqueueFreshLoop: next-sequence query returned no row");
|
|
1234
|
+
const loopRow = await this.#db.drain_enqueue_loop.get({
|
|
1235
|
+
worker_id: args.workerId,
|
|
1236
|
+
sequence: seqRow.next,
|
|
1237
|
+
prompt: args.prompt,
|
|
1238
|
+
provider_spec: JSON.stringify(args.providerSpec),
|
|
1239
|
+
child_provider_spec: JSON.stringify(args.childProviderSpec),
|
|
1240
|
+
max_turns: args.maxTurns ?? Number(process.env.PLURNK_SERVICE_MAX_TURNS ?? "50"),
|
|
1241
|
+
});
|
|
1242
|
+
if (loopRow === undefined)
|
|
1243
|
+
throw new Error("enqueueFreshLoop: loop enqueue returned no row");
|
|
1244
|
+
if (args.flags !== undefined) {
|
|
1245
|
+
await this.#db.engine_set_loop_flags.run({
|
|
1246
|
+
loop_id: loopRow.id,
|
|
1247
|
+
flags: JSON.stringify({ ...DEFAULT_LOOP_FLAGS, ...args.flags }),
|
|
1248
|
+
});
|
|
1249
|
+
}
|
|
1250
|
+
if (args.openPaths !== undefined && args.openPaths.length > 0) {
|
|
1251
|
+
await this.#db.engine_set_loop_open_paths.run({
|
|
1252
|
+
loop_id: loopRow.id,
|
|
1253
|
+
open_paths: JSON.stringify(args.openPaths),
|
|
1254
|
+
});
|
|
1255
|
+
}
|
|
1256
|
+
return loopRow.id;
|
|
1257
|
+
});
|
|
1258
|
+
}
|
|
769
1259
|
/**
|
|
770
|
-
* Start a drain for the given
|
|
1260
|
+
* Start a drain for the given worker. The drain claims queued loops via
|
|
771
1261
|
* drain_claim_next_loop (atomic 100→102 flip), executes each via
|
|
772
1262
|
* Engine.runLoop, and re-checks. Stream-aware: when the queue is empty
|
|
773
1263
|
* but the worker has active subscriptions, the drain parks on a
|
|
@@ -775,7 +1265,7 @@ export default class Daemon {
|
|
|
775
1265
|
* exits when queue is empty AND no active subscriptions remain.
|
|
776
1266
|
*
|
|
777
1267
|
* Returns both `firstLoopPromise` (resolves once the first loop the
|
|
778
|
-
* drain processes completes — used by
|
|
1268
|
+
* drain processes completes — used by runLoop to give the caller a
|
|
779
1269
|
* fast response containing their loop's result) and `drainPromise`
|
|
780
1270
|
* (resolves only when the whole drain finishes, queue+subs settled).
|
|
781
1271
|
*/
|
|
@@ -798,7 +1288,7 @@ export default class Daemon {
|
|
|
798
1288
|
const drainPromise = (async () => {
|
|
799
1289
|
let loopsDrained = 0;
|
|
800
1290
|
let lastResult = null;
|
|
801
|
-
let currentLoopId = null; // the loop being drained — for
|
|
1291
|
+
let currentLoopId = null; // the loop being drained — for abort→499 settlement
|
|
802
1292
|
try {
|
|
803
1293
|
while (true) {
|
|
804
1294
|
controller.signal.throwIfAborted();
|
|
@@ -821,35 +1311,40 @@ export default class Daemon {
|
|
|
821
1311
|
break;
|
|
822
1312
|
}
|
|
823
1313
|
currentLoopId = loopRow.id;
|
|
824
|
-
//
|
|
1314
|
+
// {§methods-loop-run-model} — provider identity belongs to the claimed loop, not the
|
|
825
1315
|
// drain that happened to claim it. A drain can consume multiple
|
|
826
1316
|
// queued loops; resolve each durable selection at this boundary.
|
|
827
1317
|
const provider = await this.#providerForLoop(loopRow.id);
|
|
828
1318
|
const onDispatch = (logEntryId) => {
|
|
829
|
-
//
|
|
830
|
-
// fire-and-forget void); a log-broadcast failure must never crash the drain.
|
|
1319
|
+
// {§methods-event-subscribe} — a log-broadcast failure must never crash the drain.
|
|
831
1320
|
void (async () => {
|
|
832
1321
|
const entry = await LogEntry.fetchLogEntry(this.#db, logEntryId);
|
|
833
1322
|
this.#broadcast({ workspaceId }, "log/entry", { entry });
|
|
834
1323
|
})().catch((e) => console.error("log/entry broadcast failed:", e instanceof Error ? e.message : String(e)));
|
|
835
1324
|
};
|
|
836
|
-
const result = await
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
1325
|
+
const result = await observed(// {§observability-boundary}
|
|
1326
|
+
"loop.run", { workspaceId, workerId, "loop.id": loopRow.id }, async (span) => {
|
|
1327
|
+
const loopResult = await this.#engine.runLoop({
|
|
1328
|
+
provider, workspaceId, workerId, loopId: loopRow.id, maxTurns: loopRow.max_turns,
|
|
1329
|
+
messages: [
|
|
1330
|
+
{ role: "system", content: systemPrompt },
|
|
1331
|
+
{ role: "user", content: loopRow.prompt },
|
|
1332
|
+
],
|
|
1333
|
+
origin: "model",
|
|
1334
|
+
onDispatch,
|
|
1335
|
+
signal: controller.signal,
|
|
1336
|
+
});
|
|
1337
|
+
span.setAttribute("status", loopResult.result.status);
|
|
1338
|
+
recordCounter(LOOP_TERMINALS, { status: loopResult.result.status });
|
|
1339
|
+
return loopResult;
|
|
845
1340
|
});
|
|
846
|
-
if (result.
|
|
847
|
-
// The loop
|
|
1341
|
+
if (result.result.status === 202) {
|
|
1342
|
+
// The loop slept via SEND[202] — suspended, not terminated. Leave it at 202
|
|
848
1343
|
// (resumable); no loop/terminated, no orphan-reconcile. A stream conclusion
|
|
849
1344
|
// (#handleWakeWorker) re-queues it; and if it holds a polled stream, a poll timer
|
|
850
|
-
// wakes it every P to inspect (§exec-poll). §worker-lifecycle-wake-liveness.
|
|
1345
|
+
// wakes it every P to inspect ({§exec-poll}). {§worker-lifecycle-wake-liveness}.
|
|
851
1346
|
void this.#schedulePollWake(workspaceId, workerId, systemPrompt).catch((err) => console.error("poll-wake scheduling failed:", err instanceof Error ? err.message : String(err)));
|
|
852
|
-
// §send-premature-terminate/[
|
|
1347
|
+
// {§send-premature-terminate}/SEND[202]<T> — the park deadline:
|
|
853
1348
|
// dispatcher recorded the marker's seconds; a bounded park is woken at T
|
|
854
1349
|
// regardless of arrivals, so a park always has a next turn. -1 (indefinite:
|
|
855
1350
|
// the butler, a [300] ask) schedules nothing — irc/inject/conclusions wake it.
|
|
@@ -871,37 +1366,44 @@ export default class Daemon {
|
|
|
871
1366
|
this.#parkTimers.set(workerId, t);
|
|
872
1367
|
}
|
|
873
1368
|
}
|
|
874
|
-
// Honor an OWED wake (§worker-lifecycle-child-wake): a child/stream concluded while
|
|
1369
|
+
// Honor an OWED wake ({§worker-lifecycle-child-wake}): a child/stream concluded while
|
|
875
1370
|
// this worker was mid-turn, before it slept — resume in place rather than park blind,
|
|
876
|
-
// so a worker
|
|
1371
|
+
// so a worker hibernation always returns. The loop is 202 here; reset to
|
|
877
1372
|
// claimable and the drain re-runs it on the next claim below.
|
|
878
1373
|
if (this.#owedWakes.delete(workerId)) {
|
|
879
1374
|
await this.#lifecycle.wake(loopRow.id);
|
|
1375
|
+
currentLoopId = null;
|
|
880
1376
|
continue;
|
|
881
1377
|
}
|
|
882
|
-
// The loop is blocked at 202 on a live obligation (§wait-obligation-matrix);
|
|
1378
|
+
// The loop is blocked at 202 on a live obligation ({§wait-obligation-matrix});
|
|
883
1379
|
// that obligation's conclusion is its wake edge (the owed-wake above covers the
|
|
884
1380
|
// conclude-before-block race). An idle wait never reaches here — it concluded at dispatch.
|
|
1381
|
+
currentLoopId = null;
|
|
885
1382
|
continue;
|
|
886
1383
|
}
|
|
887
1384
|
this.#owedWakes.delete(workerId); // the loop concluded (non-202) — no park to honor a held wake at
|
|
888
|
-
const usage = await
|
|
889
|
-
|
|
1385
|
+
const [usage, attributions, turnIds] = await Promise.all([
|
|
1386
|
+
this.#engine.loopUsage(loopRow.id),
|
|
1387
|
+
this.#engine.loopAttributions(loopRow.id),
|
|
1388
|
+
this.#lifecycle.turnIds(loopRow.id),
|
|
1389
|
+
]);
|
|
890
1390
|
this.#broadcast({ workspaceId }, "loop/terminated", {
|
|
891
1391
|
workerId,
|
|
892
1392
|
loopId: loopRow.id,
|
|
893
|
-
|
|
1393
|
+
result: result.result,
|
|
894
1394
|
hitMaxTurns: result.hitMaxTurns,
|
|
895
1395
|
turnIds,
|
|
896
1396
|
usage,
|
|
1397
|
+
attributions,
|
|
897
1398
|
});
|
|
898
1399
|
loopsDrained++;
|
|
899
1400
|
const loopResult = {
|
|
900
1401
|
loopId: loopRow.id,
|
|
901
1402
|
turnIds,
|
|
902
|
-
|
|
1403
|
+
result: result.result,
|
|
903
1404
|
hitMaxTurns: result.hitMaxTurns,
|
|
904
1405
|
usage,
|
|
1406
|
+
attributions,
|
|
905
1407
|
};
|
|
906
1408
|
lastResult = loopResult;
|
|
907
1409
|
if (!firstSettled) {
|
|
@@ -909,67 +1411,106 @@ export default class Daemon {
|
|
|
909
1411
|
resolveFirst(loopResult);
|
|
910
1412
|
}
|
|
911
1413
|
// A next-turn prompt this loop ended before consuming (a
|
|
912
|
-
// wake conclusion or a
|
|
1414
|
+
// wake conclusion or a runLoop-while-active prompt) is promoted to
|
|
913
1415
|
// a fresh queued loop so it's never silently dropped.
|
|
914
|
-
await this.#
|
|
1416
|
+
await this.#reconcileOrphanedPrompts(workerId, loopRow.id);
|
|
1417
|
+
currentLoopId = null;
|
|
915
1418
|
}
|
|
916
1419
|
}
|
|
917
1420
|
catch (err) {
|
|
918
1421
|
if (controller.signal.aborted) {
|
|
919
|
-
//
|
|
920
|
-
// is the loop's TERMINAL state (499), delivered via loop/terminated (
|
|
1422
|
+
// {§methods-loop-cancel} — loop.cancel / shutdown aborted the live drain. A cancellation
|
|
1423
|
+
// is the loop's TERMINAL state (499), delivered via loop/terminated (runLoop no
|
|
921
1424
|
// longer blocks to return it). A genuine error rejects firstLoopPromise.
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
:
|
|
1425
|
+
let usage = {
|
|
1426
|
+
promptTokens: 0,
|
|
1427
|
+
completionTokens: 0,
|
|
1428
|
+
reasoningTokens: 0,
|
|
1429
|
+
cachedTokens: 0,
|
|
1430
|
+
costUsd: 0,
|
|
1431
|
+
costs: [],
|
|
1432
|
+
contextTokens: 0,
|
|
1433
|
+
promptBudget: null,
|
|
1434
|
+
meta: {},
|
|
1435
|
+
};
|
|
1436
|
+
let attributions = [];
|
|
1437
|
+
const message = ErrorDetail.preview(controller.signal.reason ?? "user_cancelled")
|
|
1438
|
+
|| "no reason was supplied";
|
|
925
1439
|
if (currentLoopId !== null) {
|
|
926
|
-
//
|
|
927
|
-
//
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
1440
|
+
// {§methods-loop-cancel}/{§worker-lifecycle-terminal-result} —
|
|
1441
|
+
// persist the exact 499 cancellation result before broadcasting it.
|
|
1442
|
+
const cancelled = await this.#lifecycle.finish(currentLoopId, Results.failure("lifecycle:cancel", "loop-cancelled", 499, `The loop was cancelled: ${message}.`, {}, {
|
|
1443
|
+
reason: message,
|
|
1444
|
+
stage: "loop",
|
|
1445
|
+
retryable: false,
|
|
1446
|
+
}), { terminatedBy: "cancel" });
|
|
1447
|
+
[usage, attributions] = await Promise.all([
|
|
1448
|
+
this.#engine.loopUsage(currentLoopId),
|
|
1449
|
+
this.#engine.loopAttributions(currentLoopId),
|
|
1450
|
+
]);
|
|
1451
|
+
if (cancelled !== null) {
|
|
934
1452
|
this.#broadcast({ workspaceId }, "loop/terminated", {
|
|
935
1453
|
workerId,
|
|
936
1454
|
loopId: currentLoopId,
|
|
937
|
-
|
|
1455
|
+
result: cancelled,
|
|
938
1456
|
hitMaxTurns: false,
|
|
939
1457
|
turnIds: await this.#lifecycle.turnIds(currentLoopId),
|
|
940
1458
|
usage,
|
|
941
|
-
|
|
1459
|
+
attributions,
|
|
942
1460
|
});
|
|
943
1461
|
}
|
|
944
1462
|
}
|
|
945
1463
|
if (!firstSettled) {
|
|
946
1464
|
firstSettled = true;
|
|
947
|
-
resolveFirst({
|
|
1465
|
+
resolveFirst({
|
|
1466
|
+
loopId: currentLoopId ?? 0,
|
|
1467
|
+
turnIds: [],
|
|
1468
|
+
result: currentLoopId === null
|
|
1469
|
+
? Results.failure("lifecycle:cancel", "loop-cancelled", 499, `The loop was cancelled: ${message}.`, {}, {
|
|
1470
|
+
reason: message,
|
|
1471
|
+
stage: "loop",
|
|
1472
|
+
retryable: false,
|
|
1473
|
+
})
|
|
1474
|
+
: await this.#lifecycle.result(currentLoopId)
|
|
1475
|
+
?? Results.failure("lifecycle:cancel", "loop-cancelled", 499, `The loop was cancelled: ${message}.`, {}, {
|
|
1476
|
+
reason: message,
|
|
1477
|
+
stage: "loop",
|
|
1478
|
+
retryable: false,
|
|
1479
|
+
}),
|
|
1480
|
+
hitMaxTurns: false,
|
|
1481
|
+
usage,
|
|
1482
|
+
});
|
|
948
1483
|
}
|
|
949
1484
|
}
|
|
950
1485
|
else {
|
|
951
|
-
//
|
|
952
|
-
//
|
|
953
|
-
//
|
|
954
|
-
// (failed) — distinct from an abort's 499 — for every error, not just the pre-first one.
|
|
955
|
-
// #506 — the WHY must reach every forensic channel, not one. The old handler
|
|
956
|
-
// fed only the loop row + broadcast; run54 died with the daemon log silent, zero
|
|
957
|
-
// error telemetry, and a bare 500 — the cause (a stack) reachable nowhere. The
|
|
958
|
-
// daemon-log line + the error telemetry event fire even when currentLoopId is null
|
|
959
|
-
// or the row-write itself is what failed, so a death is never traceless again.
|
|
1486
|
+
// {§worker-lifecycle-terminal-result} — a non-abort drain
|
|
1487
|
+
// failure becomes an exact durable 500 and terminal notification;
|
|
1488
|
+
// daemon diagnostics retain the complete caught error.
|
|
960
1489
|
console.error(`drain error (workspace ${workspaceId}, worker ${workerId}, loop ${currentLoopId ?? "?"}):`, err);
|
|
961
1490
|
if (currentLoopId !== null) {
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
const
|
|
969
|
-
|
|
970
|
-
|
|
1491
|
+
const failure = err instanceof OperationFailureError
|
|
1492
|
+
? err.result
|
|
1493
|
+
: Results.failure("daemon:drain", "loop-threw", 500, "The loop failed outside its operation result contract.", {}, {
|
|
1494
|
+
stage: "loop",
|
|
1495
|
+
retryable: false,
|
|
1496
|
+
});
|
|
1497
|
+
const settled = await this.#lifecycle.finish(currentLoopId, failure)
|
|
1498
|
+
?? await this.#lifecycle.result(currentLoopId);
|
|
1499
|
+
if (settled === null) {
|
|
1500
|
+
throw new Error(`drain could not settle loop ${currentLoopId}`, { cause: err });
|
|
1501
|
+
}
|
|
1502
|
+
const [usage, attributions] = await Promise.all([
|
|
1503
|
+
this.#engine.loopUsage(currentLoopId),
|
|
1504
|
+
this.#engine.loopAttributions(currentLoopId),
|
|
1505
|
+
]);
|
|
971
1506
|
this.#broadcast({ workspaceId }, "loop/terminated", {
|
|
972
|
-
workerId,
|
|
1507
|
+
workerId,
|
|
1508
|
+
loopId: currentLoopId,
|
|
1509
|
+
result: settled,
|
|
1510
|
+
hitMaxTurns: false,
|
|
1511
|
+
turnIds: await this.#lifecycle.turnIds(currentLoopId),
|
|
1512
|
+
usage,
|
|
1513
|
+
attributions,
|
|
973
1514
|
});
|
|
974
1515
|
}
|
|
975
1516
|
if (!firstSettled) {
|
|
@@ -991,11 +1532,15 @@ export default class Daemon {
|
|
|
991
1532
|
})();
|
|
992
1533
|
handle.promise = drainPromise;
|
|
993
1534
|
this.#activeDrains.set(workerId, handle);
|
|
994
|
-
// Topology join (§
|
|
1535
|
+
// Topology join ({§worker-loop-lifecycle}): when this drain exits having CONCLUDED the worker, wake its parent
|
|
995
1536
|
// if parked. Runs after the drain fully tears down (settled promise) so the quiescence check sees
|
|
996
1537
|
// final state; speculative (#onDrainExit no-ops unless the worker concluded AND the parent is parked).
|
|
997
|
-
|
|
1538
|
+
const drainExitTask = drainPromise.then(() => this.#onDrainExit(workspaceId, workerId, systemPrompt), () => this.#onDrainExit(workspaceId, workerId, systemPrompt));
|
|
1539
|
+
this.#drainExitTasks.add(drainExitTask);
|
|
1540
|
+
void drainExitTask.catch((err) => {
|
|
998
1541
|
console.error(`parent wake after worker ${workerId} settlement failed:`, err);
|
|
1542
|
+
}).finally(() => {
|
|
1543
|
+
this.#drainExitTasks.delete(drainExitTask);
|
|
999
1544
|
});
|
|
1000
1545
|
// Swallow unhandled rejections (drain aborts with no awaiter); the
|
|
1001
1546
|
// error already surfaced via firstLoopPromise or was logged inside.
|
|
@@ -1003,12 +1548,12 @@ export default class Daemon {
|
|
|
1003
1548
|
firstLoopPromise.catch(() => { });
|
|
1004
1549
|
return { firstLoopPromise, drainPromise };
|
|
1005
1550
|
}
|
|
1006
|
-
// Per-
|
|
1551
|
+
// Per-worker drain-transition lock (R4 / {§worker-lifecycle-single-drain}). #ensureDrain's
|
|
1007
1552
|
// start and a drain's teardown relinquish both run under it, serialized, so the two
|
|
1008
1553
|
// can't interleave and register two drains for one worker. The critical section is the
|
|
1009
1554
|
// registry decision only (never a loop's work) — a sub-ms hop at drain boundaries.
|
|
1010
1555
|
// A promise-chain mutex: each caller awaits the prior holder; the tail self-prunes
|
|
1011
|
-
// when idle so the Map stays bounded to
|
|
1556
|
+
// when idle so the Map stays bounded to workers mid-transition.
|
|
1012
1557
|
#withDrainLock(workerId, fn) {
|
|
1013
1558
|
const prev = this.#drainLocks.get(workerId) ?? Promise.resolve();
|
|
1014
1559
|
const run = prev.then(fn, fn);
|
|
@@ -1032,34 +1577,50 @@ export default class Daemon {
|
|
|
1032
1577
|
return this.#startDrain(opts);
|
|
1033
1578
|
});
|
|
1034
1579
|
}
|
|
1035
|
-
// After a loop terminates, promote
|
|
1036
|
-
//
|
|
1037
|
-
//
|
|
1038
|
-
//
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1580
|
+
// After a loop terminates, promote every next-turn frame it never consumed
|
|
1581
|
+
// into one source-keyed queued loop. The first frame occupies the loop seed;
|
|
1582
|
+
// later frames retain separate prompt entries and publish in the same turn.
|
|
1583
|
+
// Re-entry and boot recovery complete that same queued identity.
|
|
1584
|
+
async #reconcileOrphanedPrompts(workerId, endedLoopId) {
|
|
1585
|
+
await this.#withDrainLock(workerId, async () => {
|
|
1586
|
+
const endedSeq = (await this.#db.engine_loop_sequence.get({ loop_id: endedLoopId }))?.sequence ?? endedLoopId;
|
|
1587
|
+
const prefix = promptLoopPrefix(endedSeq);
|
|
1588
|
+
const frames = await this.#db.drain_orphaned_prompts_for_loop.all({ loop_id: endedLoopId, owner_id: workerId, pattern: `${prefix}%`, prefix_len: prefix.length });
|
|
1589
|
+
const first = frames[0];
|
|
1590
|
+
if (first === undefined)
|
|
1591
|
+
return;
|
|
1592
|
+
const seqRow = await this.#db.loop_run_next_sequence.get({ worker_id: workerId });
|
|
1593
|
+
if (seqRow === undefined)
|
|
1594
|
+
throw new Error("reconcileOrphanedPrompts: next-sequence query returned no row");
|
|
1595
|
+
const recovery = await this.#db.drain_enqueue_orphan_recovery_loop.get({
|
|
1596
|
+
worker_id: workerId,
|
|
1597
|
+
sequence: seqRow.next,
|
|
1598
|
+
prompt: first.body,
|
|
1599
|
+
flags: first.flags,
|
|
1600
|
+
provider_spec: first.provider_spec,
|
|
1601
|
+
child_provider_spec: first.child_provider_spec,
|
|
1602
|
+
max_turns: first.max_turns,
|
|
1603
|
+
open_paths: first.open_paths ?? "[]",
|
|
1604
|
+
orphan_source_loop_id: endedLoopId,
|
|
1605
|
+
});
|
|
1606
|
+
if (recovery === undefined)
|
|
1607
|
+
throw new Error("reconcileOrphanedPrompts: enqueue returned no row");
|
|
1608
|
+
if (recovery.status !== 100)
|
|
1609
|
+
return;
|
|
1610
|
+
const moved = await this.#db.drain_rehome_orphaned_prompt_frames.all({
|
|
1611
|
+
owner_id: workerId,
|
|
1612
|
+
source_loop_id: endedLoopId,
|
|
1613
|
+
source_pattern: `${prefix}%`,
|
|
1614
|
+
source_prefix_len: prefix.length,
|
|
1615
|
+
target_prefix: promptLoopPrefix(recovery.sequence),
|
|
1616
|
+
});
|
|
1617
|
+
if (moved.length !== frames.length) {
|
|
1618
|
+
throw new Error(`reconcileOrphanedPrompts: expected to re-home ${frames.length} frames, moved ${moved.length}`);
|
|
1619
|
+
}
|
|
1054
1620
|
});
|
|
1055
|
-
if (fresh === undefined)
|
|
1056
|
-
throw new Error("reconcileOrphanedWake: enqueue returned no row");
|
|
1057
|
-
if (orphan.flags !== null) {
|
|
1058
|
-
await this.#db.engine_set_loop_flags.run({ loop_id: fresh.id, flags: orphan.flags });
|
|
1059
|
-
}
|
|
1060
1621
|
}
|
|
1061
1622
|
// The worker's cancellation scope — lazily created, and replaced once aborted
|
|
1062
|
-
// so a later
|
|
1623
|
+
// so a later runLoop gets a live signal. The drain and the execs its loops
|
|
1063
1624
|
// spawn all run under it.
|
|
1064
1625
|
#workerSignal(workerId) {
|
|
1065
1626
|
const existing = this.#workerAborts.get(workerId);
|
|
@@ -1089,19 +1650,22 @@ export default class Daemon {
|
|
|
1089
1650
|
scope.abort(reason);
|
|
1090
1651
|
}
|
|
1091
1652
|
await Promise.all(cancelled.workerIds.map(async (targetWorkerId) => this.#reapWorkerStreams(targetWorkerId)));
|
|
1092
|
-
for (const { loopId, workerId: targetWorkerId } of cancelled.loops) {
|
|
1653
|
+
for (const { loopId, workerId: targetWorkerId, result } of cancelled.loops) {
|
|
1093
1654
|
const row = await this.#db.drain_get_worker_workspace.get({ worker_id: targetWorkerId });
|
|
1094
1655
|
if (row === undefined)
|
|
1095
1656
|
continue;
|
|
1096
|
-
const usage = await
|
|
1657
|
+
const [usage, attributions] = await Promise.all([
|
|
1658
|
+
this.#engine.loopUsage(loopId),
|
|
1659
|
+
this.#engine.loopAttributions(loopId),
|
|
1660
|
+
]);
|
|
1097
1661
|
this.#broadcast({ workspaceId: row.workspace_id }, "loop/terminated", {
|
|
1098
1662
|
workerId: targetWorkerId,
|
|
1099
1663
|
loopId,
|
|
1100
|
-
|
|
1664
|
+
result,
|
|
1101
1665
|
hitMaxTurns: false,
|
|
1102
1666
|
turnIds: await this.#lifecycle.turnIds(loopId),
|
|
1103
1667
|
usage,
|
|
1104
|
-
|
|
1668
|
+
attributions,
|
|
1105
1669
|
});
|
|
1106
1670
|
}
|
|
1107
1671
|
}
|
|
@@ -1110,10 +1674,11 @@ export default class Daemon {
|
|
|
1110
1674
|
}
|
|
1111
1675
|
/**
|
|
1112
1676
|
* Cancel the worker's in-flight work (loop.cancel). One abort, one scope: the
|
|
1113
|
-
*
|
|
1677
|
+
* worker signal stops the running loop's turn generation AND tears down every
|
|
1114
1678
|
* stream linked to it — a background exec that outlived its loop, or even a
|
|
1115
1679
|
* spawn that registers after this abort (it self-aborts against the aborted
|
|
1116
|
-
* signal). Returns cancelled iff there was work
|
|
1680
|
+
* signal). Returns cancelled iff there was process-local work; durable
|
|
1681
|
+
* unresolved loops in the worker tree are terminalized independently.
|
|
1117
1682
|
*/
|
|
1118
1683
|
cancelDrain(workerId, reason = "user_cancelled") {
|
|
1119
1684
|
const hadDrain = this.#activeDrains.has(workerId);
|
|
@@ -1135,12 +1700,12 @@ export default class Daemon {
|
|
|
1135
1700
|
}
|
|
1136
1701
|
// Does the worker have an in-flight stream (a background exec)? Used only for
|
|
1137
1702
|
// loop.cancel's cancelled=true/false answer; the teardown itself rides the
|
|
1138
|
-
//
|
|
1703
|
+
// worker signal. Duck-typed like #drainStreamingSchemes.
|
|
1139
1704
|
#workerHasActiveStreams(workerId) {
|
|
1140
1705
|
const exec = this.#schemes.get("exec");
|
|
1141
1706
|
return exec?.hasActiveSpawns?.(workerId) ?? false;
|
|
1142
1707
|
}
|
|
1143
|
-
// The contract-routed reap (§worker-lifecycle-total-reap): durable rows enumerate
|
|
1708
|
+
// The contract-routed reap ({§worker-lifecycle-total-reap}): durable rows enumerate
|
|
1144
1709
|
// every open subscription; the live registry invokes its exact callable owner.
|
|
1145
1710
|
// The worker signal is only the fast path. An exec mid-spawn or a background exec
|
|
1146
1711
|
// from a past loop is caught regardless of listener timing. Idempotent — a stream
|
|
@@ -1151,79 +1716,76 @@ export default class Daemon {
|
|
|
1151
1716
|
}
|
|
1152
1717
|
/**
|
|
1153
1718
|
* Wake-on-completion handler. Streaming schemes call this when a
|
|
1154
|
-
* subscription closes.
|
|
1155
|
-
*
|
|
1156
|
-
*
|
|
1157
|
-
* summary as the user prompt so the model gets a chance to react.
|
|
1158
|
-
*
|
|
1159
|
-
* Skipped on closeStatus=499 (aborted): the model already knows about
|
|
1160
|
-
* its own SEND[499], and a forcefully-cancelled loop's spawn-abort
|
|
1161
|
-
* shouldn't resurrect into a wake loop (defeats the cancel).
|
|
1719
|
+
* subscription closes. A parked loop resumes in place; an active loop
|
|
1720
|
+
* observes the channel transition at its next turn boundary. No synthetic
|
|
1721
|
+
* prompt or replacement loop is created.
|
|
1162
1722
|
*
|
|
1163
|
-
*
|
|
1723
|
+
* Skipped on result.status=499 (aborted): the model already knows about
|
|
1724
|
+
* its own SEND[499], and a forcefully-cancelled loop's spawn-abort must
|
|
1725
|
+
* not resurrect the worker.
|
|
1164
1726
|
*/
|
|
1165
1727
|
async #handleWakeWorker(payload) {
|
|
1166
|
-
|
|
1728
|
+
const { entryOwnerId, ...wake } = payload;
|
|
1729
|
+
const conclusion = { ...wake, workerId: entryOwnerId };
|
|
1730
|
+
// {§search-gate} — settle the dedup registration: promote on a 200 conclusion, drop on
|
|
1167
1731
|
// failure (a dead search must never serve as a duplicate). No-op for non-search streams.
|
|
1168
|
-
this.#engine.searchGate.settle(payload.target.replace(/^[a-z+.-]+:\/\//, "/").replace(/^\/+/, "/"), payload.
|
|
1732
|
+
this.#engine.searchGate.settle(payload.target.replace(/^[a-z+.-]+:\/\//, "/").replace(/^\/+/, "/"), payload.result.status);
|
|
1169
1733
|
// Aborted streams don't wake — the abort was deliberate.
|
|
1170
|
-
if (payload.
|
|
1734
|
+
if (payload.result.status === 499) {
|
|
1171
1735
|
this.#broadcast({ workspaceId: payload.workspaceId }, "stream/concluded", {
|
|
1172
|
-
...
|
|
1736
|
+
...conclusion, wakeAction: "skipped-aborted",
|
|
1173
1737
|
});
|
|
1174
1738
|
return;
|
|
1175
1739
|
}
|
|
1176
|
-
// No resurrection (§worker-lifecycle-no-resurrection): a non-499 completion whose
|
|
1177
|
-
//
|
|
1740
|
+
// No resurrection ({§worker-lifecycle-no-resurrection}): a non-499 completion whose
|
|
1741
|
+
// worker was cancelled (idle + its scope aborted) must not start a fresh drain —
|
|
1178
1742
|
// the cancel was deliberate. The deliverable is already in the channel/log and
|
|
1179
|
-
// surfaces as a `collect` environment delta (§env-delta) if the worker is read or
|
|
1180
|
-
// resumed; we just don't inject a turn. (An active
|
|
1181
|
-
// next turn via inject below; a resumed
|
|
1743
|
+
// surfaces as a `collect` environment delta ({§env-delta}) if the worker is read or
|
|
1744
|
+
// resumed; we just don't inject a turn. (An active worker folds the wake into its
|
|
1745
|
+
// next turn via inject below; a resumed worker is active, never aborted, so it is
|
|
1182
1746
|
// unaffected.)
|
|
1183
1747
|
const scope = this.#workerAborts.get(payload.workerId);
|
|
1184
1748
|
if (scope?.signal.aborted === true && !this.#activeDrains.has(payload.workerId)) {
|
|
1185
1749
|
this.#broadcast({ workspaceId: payload.workspaceId }, "stream/concluded", {
|
|
1186
|
-
...
|
|
1750
|
+
...conclusion, wakeAction: "skipped-cancelled",
|
|
1187
1751
|
});
|
|
1188
1752
|
return;
|
|
1189
1753
|
}
|
|
1190
1754
|
try {
|
|
1191
1755
|
const systemPrompt = await readFile(Paths.instructionsSystem, "utf8");
|
|
1192
|
-
// A slept (202) loop means the worker
|
|
1756
|
+
// A slept (202) loop means the worker parked via SEND[202] → resume it in place: re-queue
|
|
1193
1757
|
// it (202→100) so the drain re-claims and CONTINUES it (seq>1 → no re-foist). Checked
|
|
1194
1758
|
// FIRST: the slept status is the worker's true disposition regardless of a draining
|
|
1195
1759
|
// sibling mid-teardown (the #ensureDrain lock serializes the re-claim). No fresh loop,
|
|
1196
1760
|
// no summary-as-prompt — the resumed loop reads the concluded stream's own state from
|
|
1197
|
-
// the manifest. §worker-lifecycle-wake-liveness.
|
|
1761
|
+
// the manifest. {§worker-lifecycle-wake-liveness}.
|
|
1198
1762
|
const slept = await this.#db.drain_find_slept_loop.get({ worker_id: payload.workerId });
|
|
1199
1763
|
if (slept !== undefined) {
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1764
|
+
// {§worker-optimistic-settlement} — publish this conclusion now,
|
|
1765
|
+
// then let the worker-local gate coalesce only the provider
|
|
1766
|
+
// dispatch. Concurrent stream/child callbacks join that one gate.
|
|
1767
|
+
void this.#settleCompletionWake(payload.workspaceId, payload.workerId, systemPrompt, false).catch((err) => {
|
|
1768
|
+
console.error("completion wake settlement failed:", err instanceof Error ? err.message : String(err));
|
|
1204
1769
|
});
|
|
1205
1770
|
this.#broadcast({ workspaceId: payload.workspaceId }, "stream/concluded", {
|
|
1206
|
-
...
|
|
1207
|
-
});
|
|
1208
|
-
started?.drainPromise?.catch((err) => {
|
|
1209
|
-
console.error("wake resume drain failed:", err instanceof Error ? err.message : String(err));
|
|
1771
|
+
...conclusion, wakeAction: "resumed-loop", wakeLoopId: slept.id,
|
|
1210
1772
|
});
|
|
1211
1773
|
return;
|
|
1212
1774
|
}
|
|
1213
1775
|
// No slept loop. A live loop surfaces the concluded stream ambiently via the
|
|
1214
|
-
// environment-observation injector (§exec-stream) on its next turn — there is no prompt
|
|
1776
|
+
// environment-observation injector ({§exec-stream}) on its next turn — there is no prompt
|
|
1215
1777
|
// to inject and NO task to overwrite. The obsolete "automated environment update"
|
|
1216
1778
|
// synthesis (which clobbered the model's actual goal) is retired; just tell the client.
|
|
1217
1779
|
if (this.#activeDrains.has(payload.workerId)) {
|
|
1218
1780
|
this.#broadcast({ workspaceId: payload.workspaceId }, "stream/concluded", {
|
|
1219
|
-
...
|
|
1781
|
+
...conclusion, wakeAction: "no-op-active-loop",
|
|
1220
1782
|
});
|
|
1221
1783
|
return;
|
|
1222
1784
|
}
|
|
1223
|
-
// No slept loop, no active drain — nothing to resume (e.g. a SEND[200]-done
|
|
1785
|
+
// No slept loop, no active drain — nothing to resume (e.g. a SEND[200]-done worker whose
|
|
1224
1786
|
// streams were swept). Surface the conclusion without opening a loop.
|
|
1225
1787
|
this.#broadcast({ workspaceId: payload.workspaceId }, "stream/concluded", {
|
|
1226
|
-
...
|
|
1788
|
+
...conclusion, wakeAction: "no-loop",
|
|
1227
1789
|
});
|
|
1228
1790
|
}
|
|
1229
1791
|
catch (err) {
|
|
@@ -1234,8 +1796,8 @@ export default class Daemon {
|
|
|
1234
1796
|
* grammar 0.74.20 EXEC `<T,P>` — schedule a hibernation poll-wake. Called when a loop parks at
|
|
1235
1797
|
* a park; if the worker holds an open polled stream, arm a timer for its tightest cadence P that
|
|
1236
1798
|
* resumes the slept loop so the model inspects progress. While the loop is ACTIVE there is no
|
|
1237
|
-
* poll work — ambient folded stream deltas already surface progress (§exec-stream); the wake
|
|
1238
|
-
* matters only across hibernation. A wake-edge-less 202 (no polled stream) gets no timer. §exec-poll
|
|
1799
|
+
* poll work — ambient folded stream deltas already surface progress ({§exec-stream}); the wake
|
|
1800
|
+
* matters only across hibernation. A wake-edge-less 202 (no polled stream) gets no timer. {§exec-poll}
|
|
1239
1801
|
*/
|
|
1240
1802
|
async #schedulePollWake(workspaceId, workerId, systemPrompt) {
|
|
1241
1803
|
const existing = this.#pollTimers.get(workerId);
|
|
@@ -1249,14 +1811,8 @@ export default class Daemon {
|
|
|
1249
1811
|
return;
|
|
1250
1812
|
}
|
|
1251
1813
|
const pollSec = row?.poll_seconds ?? null;
|
|
1252
|
-
//
|
|
1253
|
-
//
|
|
1254
|
-
// explicit <,0> → poll_seconds=0 stored → blind opt-out (an exec a model wants unwatched).
|
|
1255
|
-
// absent <,P> + a LIVE stream → EXPONENTIAL BACKOFF (base*2^min(step,turns-1)), so a hung
|
|
1256
|
-
// exec is no longer park-blind-forever: the model regains a turn every tick to read
|
|
1257
|
-
// partial output and re-park a slow long-runner or KILL a stuck one (no auto-kill — only
|
|
1258
|
-
// the model tells a silent deadlock from a silent `cargo build`).
|
|
1259
|
-
// no open stream at all → nothing to poll (a child-join park is woken by the child terminal).
|
|
1814
|
+
// {§exec-poll} — a positive explicit cadence wins, zero opts out,
|
|
1815
|
+
// and an absent cadence uses the worker's exponential-backoff step.
|
|
1260
1816
|
let delayMs;
|
|
1261
1817
|
if (pollSec !== null && pollSec > 0) {
|
|
1262
1818
|
this.#pollBackoff.delete(workerId);
|
|
@@ -1275,21 +1831,23 @@ export default class Daemon {
|
|
|
1275
1831
|
delayMs = execPollBackoffMs(step, base, turns);
|
|
1276
1832
|
this.#pollBackoff.set(workerId, step + 1);
|
|
1277
1833
|
}
|
|
1278
|
-
// Floored by the
|
|
1279
|
-
// faster than
|
|
1280
|
-
const
|
|
1834
|
+
// Floored by the optimistic settlement cap so a `<…,1>` cannot wake a
|
|
1835
|
+
// parked loop faster than the preceding turn's settlement scale.
|
|
1836
|
+
const optimisticWaitMs = readOptimisticSettlementMs();
|
|
1281
1837
|
const timer = setTimeout(() => {
|
|
1282
1838
|
this.#pollTimers.delete(workerId);
|
|
1283
1839
|
void this.#wakeParkedWorker(workspaceId, workerId, systemPrompt);
|
|
1284
|
-
}, Math.max(delayMs,
|
|
1840
|
+
}, Math.max(delayMs, optimisticWaitMs));
|
|
1285
1841
|
timer.unref();
|
|
1286
1842
|
this.#pollTimers.set(workerId, timer);
|
|
1287
1843
|
}
|
|
1288
1844
|
/** Resume `workerId`'s slept (202) loop in place — the same 202→100 resume #handleWakeWorker uses, minus a
|
|
1289
|
-
* wake payload. The shared wake primitive: a poll cadence (§exec-poll), a watched stream concluding,
|
|
1290
|
-
* or a child worker finishing (§
|
|
1845
|
+
* wake payload. The shared wake primitive: a poll cadence ({§exec-poll}), a watched stream concluding,
|
|
1846
|
+
* or a child worker finishing ({§worker-loop-lifecycle} topology join) all call this. A no-op if the worker was
|
|
1291
1847
|
* cancelled or isn't actually parked (no slept loop) — so calling it speculatively is safe. */
|
|
1292
|
-
async #wakeParkedWorker(workspaceId, workerId, systemPrompt) {
|
|
1848
|
+
async #wakeParkedWorker(workspaceId, workerId, systemPrompt, oweIfActive = true) {
|
|
1849
|
+
if (!this.#started)
|
|
1850
|
+
return;
|
|
1293
1851
|
const scope = this.#workerAborts.get(workerId);
|
|
1294
1852
|
if (scope?.signal.aborted === true && !this.#activeDrains.has(workerId))
|
|
1295
1853
|
return; // cancelled — no resurrection
|
|
@@ -1297,25 +1855,108 @@ export default class Daemon {
|
|
|
1297
1855
|
if (slept === undefined) {
|
|
1298
1856
|
// Not parked. If a drain is still ACTIVE, the worker is mid-turn and about to park — the
|
|
1299
1857
|
// conclusion that fired this wake arrived before the 202 committed (the conclude-before-park
|
|
1300
|
-
// race).
|
|
1858
|
+
// race). Owe the wake: the drain honors it at park so a worker hibernation never deadlocks.
|
|
1301
1859
|
// (No active drain → already concluded/running; nothing to wake.)
|
|
1302
|
-
if (this.#activeDrains.has(workerId))
|
|
1303
|
-
|
|
1860
|
+
if (this.#activeDrains.has(workerId)) {
|
|
1861
|
+
if (oweIfActive)
|
|
1862
|
+
this.#owedWakes.add(workerId);
|
|
1863
|
+
}
|
|
1304
1864
|
return;
|
|
1305
1865
|
}
|
|
1306
|
-
await this.#lifecycle.wake(slept.id);
|
|
1866
|
+
const woke = await this.#lifecycle.wake(slept.id);
|
|
1867
|
+
if (!woke)
|
|
1868
|
+
return;
|
|
1307
1869
|
const started = await this.#ensureDrain({
|
|
1308
1870
|
workspaceId, workerId, systemPrompt,
|
|
1309
1871
|
});
|
|
1310
1872
|
started?.drainPromise?.catch((err) => {
|
|
1311
|
-
|
|
1873
|
+
if (this.#started) {
|
|
1874
|
+
console.error("wake-parked resume drain failed:", err instanceof Error ? err.message : String(err));
|
|
1875
|
+
}
|
|
1312
1876
|
});
|
|
1313
1877
|
}
|
|
1878
|
+
async #workerHasLiveObligation(workerId) {
|
|
1879
|
+
const [openSubscriptions, liveChild] = await Promise.all([
|
|
1880
|
+
this.#db.find_open_subscriptions_for_worker.all({ worker_id: workerId }),
|
|
1881
|
+
this.#db.engine_worker_has_live_child.get({ worker_id: workerId }),
|
|
1882
|
+
]);
|
|
1883
|
+
return openSubscriptions.length > 0 || liveChild !== undefined;
|
|
1884
|
+
}
|
|
1885
|
+
#settleCompletionWake(workspaceId, workerId, systemPrompt, oweIfActive = true) {
|
|
1886
|
+
const existing = this.#completionWakeGates.get(workerId);
|
|
1887
|
+
if (existing !== undefined) {
|
|
1888
|
+
existing.conclusions++;
|
|
1889
|
+
existing.poke.resolve();
|
|
1890
|
+
return existing.promise;
|
|
1891
|
+
}
|
|
1892
|
+
const completed = Promise.withResolvers();
|
|
1893
|
+
const gate = {
|
|
1894
|
+
conclusions: 1,
|
|
1895
|
+
poke: Promise.withResolvers(),
|
|
1896
|
+
promise: completed.promise,
|
|
1897
|
+
};
|
|
1898
|
+
this.#completionWakeGates.set(workerId, gate);
|
|
1899
|
+
void this.#runCompletionWake(workspaceId, workerId, systemPrompt, oweIfActive, gate).then(completed.resolve, completed.reject).finally(() => {
|
|
1900
|
+
if (this.#completionWakeGates.get(workerId) === gate) {
|
|
1901
|
+
this.#completionWakeGates.delete(workerId);
|
|
1902
|
+
}
|
|
1903
|
+
});
|
|
1904
|
+
return gate.promise;
|
|
1905
|
+
}
|
|
1906
|
+
async #runCompletionWake(workspaceId, workerId, systemPrompt, oweIfActive, gate) {
|
|
1907
|
+
const slept = await this.#db.drain_find_slept_loop.get({ worker_id: workerId });
|
|
1908
|
+
if (slept === undefined) {
|
|
1909
|
+
this.#releaseCompletionWake(workerId, gate);
|
|
1910
|
+
return this.#wakeParkedWorker(workspaceId, workerId, systemPrompt, oweIfActive);
|
|
1911
|
+
}
|
|
1912
|
+
const timeoutMs = readOptimisticSettlementMs();
|
|
1913
|
+
if (timeoutMs === 0 || !(await this.#workerHasLiveObligation(workerId))) {
|
|
1914
|
+
this.#releaseCompletionWake(workerId, gate);
|
|
1915
|
+
return this.#wakeParkedWorker(workspaceId, workerId, systemPrompt, false);
|
|
1916
|
+
}
|
|
1917
|
+
return observed("worker.wake.settlement", { "window.ms": timeoutMs }, async (span) => {
|
|
1918
|
+
const startedAt = performance.now();
|
|
1919
|
+
const signal = this.#workerAborts.get(workerId)?.signal;
|
|
1920
|
+
const deadline = delay(timeoutMs, "deadline", { signal, ref: false })
|
|
1921
|
+
.catch((cause) => {
|
|
1922
|
+
if (signal?.aborted === true)
|
|
1923
|
+
return "cancelled";
|
|
1924
|
+
throw cause;
|
|
1925
|
+
});
|
|
1926
|
+
let release = "quiescent";
|
|
1927
|
+
while (await this.#workerHasLiveObligation(workerId)) {
|
|
1928
|
+
const poke = gate.poke;
|
|
1929
|
+
const outcome = await Promise.race([
|
|
1930
|
+
poke.promise.then(() => "arrival"),
|
|
1931
|
+
deadline,
|
|
1932
|
+
]);
|
|
1933
|
+
if (outcome === "arrival") {
|
|
1934
|
+
if (gate.poke === poke)
|
|
1935
|
+
gate.poke = Promise.withResolvers();
|
|
1936
|
+
continue;
|
|
1937
|
+
}
|
|
1938
|
+
release = outcome;
|
|
1939
|
+
break;
|
|
1940
|
+
}
|
|
1941
|
+
span.setAttribute("release", release);
|
|
1942
|
+
span.setAttribute("conclusions", gate.conclusions);
|
|
1943
|
+
span.setAttribute("elapsed.ms", Math.round(performance.now() - startedAt));
|
|
1944
|
+
this.#releaseCompletionWake(workerId, gate);
|
|
1945
|
+
if (release === "cancelled")
|
|
1946
|
+
return;
|
|
1947
|
+
return this.#wakeParkedWorker(workspaceId, workerId, systemPrompt, false);
|
|
1948
|
+
});
|
|
1949
|
+
}
|
|
1950
|
+
#releaseCompletionWake(workerId, gate) {
|
|
1951
|
+
if (this.#completionWakeGates.get(workerId) === gate) {
|
|
1952
|
+
this.#completionWakeGates.delete(workerId);
|
|
1953
|
+
}
|
|
1954
|
+
}
|
|
1314
1955
|
/** A worker's drain exited. If the worker truly CONCLUDED — no 202-blocked loop, no open stream — then
|
|
1315
1956
|
* wake its PARENT in place if the parent is blocked on the join (the structured-concurrency join — a
|
|
1316
|
-
* child finishing is the wake edge for a parent that waited on it, §worker-lifecycle-child-wake). A worker
|
|
1957
|
+
* child finishing is the wake edge for a parent that waited on it, {§worker-lifecycle-child-wake}). A worker
|
|
1317
1958
|
* blocked at 202, or still holding a stream, is NOT concluded — its own wake edges drive it, not this.
|
|
1318
|
-
* The parent reads the child's deliverable from its own log (the §worker-scheme-collect delta) on
|
|
1959
|
+
* The parent reads the child's deliverable from its own log (the {§worker-scheme-collect} delta) on
|
|
1319
1960
|
* resume — control edge here, never an injected prompt. Recurses up via the parent's own drain-exit. */
|
|
1320
1961
|
async #onDrainExit(workspaceId, workerId, systemPrompt) {
|
|
1321
1962
|
const slept = await this.#db.drain_find_slept_loop.get({ worker_id: workerId });
|
|
@@ -1326,13 +1967,11 @@ export default class Daemon {
|
|
|
1326
1967
|
return; // a stream still runs — its conclusion re-evaluates, not this exit
|
|
1327
1968
|
const parent = await this.#db.worker_parent_id.get({ worker_id: workerId });
|
|
1328
1969
|
if (parent?.parent_worker_id == null)
|
|
1329
|
-
return; // a root
|
|
1330
|
-
await this.#
|
|
1970
|
+
return; // a root worker — nobody to wake
|
|
1971
|
+
await this.#settleCompletionWake(workspaceId, parent.parent_worker_id, systemPrompt);
|
|
1331
1972
|
}
|
|
1332
|
-
//
|
|
1333
|
-
//
|
|
1334
|
-
// unhandled rejection in the one then-uncaught dispatch void). The transport's failure is
|
|
1335
|
-
// its own — logged loudly per event, never the engine's crash.
|
|
1973
|
+
// {§methods-event-subscribe} — subscriber failures are transport-local:
|
|
1974
|
+
// log them at this boundary and never re-enter engine control flow.
|
|
1336
1975
|
#emitTo(workspaceId, method, params) {
|
|
1337
1976
|
for (const sub of this.#eventSubscribers) {
|
|
1338
1977
|
try {
|
|
@@ -1344,16 +1983,19 @@ export default class Daemon {
|
|
|
1344
1983
|
}
|
|
1345
1984
|
}
|
|
1346
1985
|
#broadcast(target, method, params) {
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1986
|
+
observedSync(// {§observability-boundary}
|
|
1987
|
+
"stream.broadcast", { method, ...(target === "all" ? {} : { "workspace.id": target.workspaceId }) }, () => {
|
|
1988
|
+
if (target === "all") {
|
|
1989
|
+
// {§notifications-envelope-carries-workspaceid}: global events carry workspaceId null.
|
|
1990
|
+
this.#emitTo(null, method, params);
|
|
1991
|
+
return;
|
|
1992
|
+
}
|
|
1993
|
+
// {§methods-event-subscribe}: publish to the in-process source; transport modules subscribe
|
|
1994
|
+
// here (plurnk-agui renders to AG-UI+). Each subscriber owns its own fan-out; core just emits.
|
|
1995
|
+
// Scope-stamping onto the notification envelope ({§notifications-envelope-carries-workspaceid})
|
|
1996
|
+
// is each subscriber's edge concern now — the seam hands (workspaceId, method, params) raw.
|
|
1997
|
+
this.#emitTo(target.workspaceId, method, params);
|
|
1998
|
+
});
|
|
1357
1999
|
}
|
|
1358
2000
|
}
|
|
1359
2001
|
//# sourceMappingURL=Daemon.js.map
|