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