@plurnk/plurnk-service 1.4.0 → 1.6.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 +37 -31
- package/INSTALL.md +16 -11
- package/README.md +1 -0
- package/SPEC.md +631 -397
- package/dist/Paths.d.ts.map +1 -1
- package/dist/Paths.js +2 -3
- package/dist/Paths.js.map +1 -1
- package/dist/build-info.json +1 -1
- package/dist/content/edit-collision.d.ts +5 -0
- package/dist/content/edit-collision.d.ts.map +1 -0
- package/dist/content/edit-collision.js +14 -0
- package/dist/content/edit-collision.js.map +1 -0
- package/dist/content/edit-receipt.d.ts +3 -2
- package/dist/content/edit-receipt.d.ts.map +1 -1
- package/dist/content/edit-receipt.js +58 -13
- package/dist/content/edit-receipt.js.map +1 -1
- package/dist/content/index.d.ts +5 -1
- package/dist/content/index.d.ts.map +1 -1
- package/dist/content/index.js +4 -1
- package/dist/content/index.js.map +1 -1
- package/dist/content/line-anchors.d.ts +41 -0
- package/dist/content/line-anchors.d.ts.map +1 -0
- package/dist/content/line-anchors.js +191 -0
- package/dist/content/line-anchors.js.map +1 -0
- package/dist/content/line-marker.d.ts +7 -3
- package/dist/content/line-marker.d.ts.map +1 -1
- package/dist/content/line-marker.js +2 -1
- package/dist/content/line-marker.js.map +1 -1
- package/dist/content/matcher.d.ts +1 -1
- package/dist/content/matcher.d.ts.map +1 -1
- package/dist/content/matcher.js +2 -2
- package/dist/content/matcher.js.map +1 -1
- package/dist/content/read-projector.d.ts +20 -0
- package/dist/content/read-projector.d.ts.map +1 -0
- package/dist/content/read-projector.js +122 -0
- package/dist/content/read-projector.js.map +1 -0
- package/dist/content/read-resolve.d.ts +2 -7
- package/dist/content/read-resolve.d.ts.map +1 -1
- package/dist/content/read-resolve.js +32 -107
- package/dist/content/read-resolve.js.map +1 -1
- package/dist/core/BranchReceipt.d.ts +0 -1
- package/dist/core/BranchReceipt.d.ts.map +1 -1
- package/dist/core/BranchReceipt.js +0 -3
- package/dist/core/BranchReceipt.js.map +1 -1
- package/dist/core/BudgetReadout.d.ts.map +1 -1
- package/dist/core/BudgetReadout.js +9 -2
- package/dist/core/BudgetReadout.js.map +1 -1
- package/dist/core/ChannelWrite.d.ts +3 -0
- package/dist/core/ChannelWrite.d.ts.map +1 -1
- package/dist/core/ChannelWrite.js +3 -3
- package/dist/core/ChannelWrite.js.map +1 -1
- package/dist/core/ChannelWrite.sql +4 -3
- package/dist/core/CoreSchemeServices.d.ts +10 -2
- package/dist/core/CoreSchemeServices.d.ts.map +1 -1
- package/dist/core/CoreSchemeServices.js.map +1 -1
- package/dist/core/Dispatcher.d.ts +14 -8
- package/dist/core/Dispatcher.d.ts.map +1 -1
- package/dist/core/Dispatcher.js +656 -312
- package/dist/core/Dispatcher.js.map +1 -1
- package/dist/core/DurableStatement.d.ts.map +1 -1
- package/dist/core/DurableStatement.js +2 -0
- package/dist/core/DurableStatement.js.map +1 -1
- package/dist/core/Engine.d.ts +24 -18
- package/dist/core/Engine.d.ts.map +1 -1
- package/dist/core/Engine.js +620 -392
- package/dist/core/Engine.js.map +1 -1
- package/dist/core/Engine.sql +188 -119
- package/dist/core/ExecutableTools.d.ts +10 -0
- package/dist/core/ExecutableTools.d.ts.map +1 -0
- package/dist/core/ExecutableTools.js +41 -0
- package/dist/core/ExecutableTools.js.map +1 -0
- package/dist/core/ExecutorRegistry.d.ts +3 -3
- package/dist/core/ExecutorRegistry.d.ts.map +1 -1
- package/dist/core/ExecutorRegistry.js +2 -2
- package/dist/core/ExecutorRegistry.js.map +1 -1
- package/dist/core/LogBody.d.ts +4 -1
- package/dist/core/LogBody.d.ts.map +1 -1
- package/dist/core/LogBody.js +46 -2
- package/dist/core/LogBody.js.map +1 -1
- package/dist/core/LogEntryProjection.d.ts +14 -0
- package/dist/core/LogEntryProjection.d.ts.map +1 -0
- package/dist/core/LogEntryProjection.js +38 -0
- package/dist/core/LogEntryProjection.js.map +1 -0
- package/dist/core/LoopLifecycle.d.ts +1 -2
- package/dist/core/LoopLifecycle.d.ts.map +1 -1
- package/dist/core/LoopLifecycle.js +1 -4
- package/dist/core/LoopLifecycle.js.map +1 -1
- package/dist/core/LoopLifecycle.sql +1 -4
- package/dist/core/ModelCall.d.ts +27 -0
- package/dist/core/ModelCall.d.ts.map +1 -0
- package/dist/core/ModelCall.js +125 -0
- package/dist/core/ModelCall.js.map +1 -0
- package/dist/core/PacketBuilder.d.ts +13 -12
- package/dist/core/PacketBuilder.d.ts.map +1 -1
- package/dist/core/PacketBuilder.js +146 -118
- package/dist/core/PacketBuilder.js.map +1 -1
- package/dist/core/ProblemLog.d.ts.map +1 -1
- package/dist/core/ProblemLog.js +1 -0
- package/dist/core/ProblemLog.js.map +1 -1
- package/dist/core/ProposalLifecycle.d.ts.map +1 -1
- package/dist/core/ProposalLifecycle.js +3 -4
- package/dist/core/ProposalLifecycle.js.map +1 -1
- package/dist/core/ProviderInstantiate.d.ts.map +1 -1
- package/dist/core/ProviderInstantiate.js +23 -6
- package/dist/core/ProviderInstantiate.js.map +1 -1
- package/dist/core/SchemeRegistry.js +7 -7
- package/dist/core/SchemeRegistry.js.map +1 -1
- package/dist/core/StrikeRail.d.ts +5 -2
- package/dist/core/StrikeRail.d.ts.map +1 -1
- package/dist/core/StrikeRail.js +10 -7
- package/dist/core/StrikeRail.js.map +1 -1
- package/dist/core/TerminalResult.d.ts +16 -0
- package/dist/core/TerminalResult.d.ts.map +1 -0
- package/dist/core/TerminalResult.js +49 -0
- package/dist/core/TerminalResult.js.map +1 -0
- package/dist/core/caps/CapsResolve.d.ts +5 -1
- package/dist/core/caps/CapsResolve.d.ts.map +1 -1
- package/dist/core/caps/CapsResolve.js +9 -5
- package/dist/core/caps/CapsResolve.js.map +1 -1
- package/dist/core/caps/DbChannelCaps.d.ts +1 -1
- package/dist/core/caps/DbChannelCaps.d.ts.map +1 -1
- package/dist/core/caps/DbChannelCaps.js +6 -4
- package/dist/core/caps/DbChannelCaps.js.map +1 -1
- package/dist/core/caps/DbEntryCaps.d.ts +2 -1
- package/dist/core/caps/DbEntryCaps.d.ts.map +1 -1
- package/dist/core/caps/DbEntryCaps.js +21 -8
- package/dist/core/caps/DbEntryCaps.js.map +1 -1
- package/dist/core/caps/DbNotifyCaps.d.ts +1 -1
- package/dist/core/caps/DbNotifyCaps.d.ts.map +1 -1
- package/dist/core/caps/DbNotifyCaps.js +6 -4
- package/dist/core/caps/DbNotifyCaps.js.map +1 -1
- package/dist/core/caps/DbProjectionCaps.d.ts +1 -0
- package/dist/core/caps/DbProjectionCaps.d.ts.map +1 -1
- package/dist/core/caps/DbProjectionCaps.js +29 -0
- package/dist/core/caps/DbProjectionCaps.js.map +1 -1
- package/dist/core/caps/DbSubscriptionCaps.d.ts +4 -6
- package/dist/core/caps/DbSubscriptionCaps.d.ts.map +1 -1
- package/dist/core/caps/DbSubscriptionCaps.js +27 -13
- package/dist/core/caps/DbSubscriptionCaps.js.map +1 -1
- package/dist/core/caps/SchemeCtxImpl.d.ts +11 -3
- package/dist/core/caps/SchemeCtxImpl.d.ts.map +1 -1
- package/dist/core/caps/SchemeCtxImpl.js +10 -8
- package/dist/core/caps/SchemeCtxImpl.js.map +1 -1
- package/dist/core/fork.d.ts.map +1 -1
- package/dist/core/fork.js +23 -7
- package/dist/core/fork.js.map +1 -1
- package/dist/core/fork.sql +35 -14
- package/dist/core/git-iso.d.ts +3 -2
- package/dist/core/git-iso.d.ts.map +1 -1
- package/dist/core/git-iso.js +27 -12
- package/dist/core/git-iso.js.map +1 -1
- package/dist/core/git-membership.d.ts +1 -0
- package/dist/core/git-membership.d.ts.map +1 -1
- package/dist/core/git-membership.js +87 -26
- package/dist/core/git-membership.js.map +1 -1
- package/dist/core/git-state.d.ts +8 -1
- package/dist/core/git-state.d.ts.map +1 -1
- package/dist/core/git-state.js +36 -17
- package/dist/core/git-state.js.map +1 -1
- package/dist/core/namespace.d.ts +1 -0
- package/dist/core/namespace.d.ts.map +1 -1
- package/dist/core/namespace.js +20 -0
- package/dist/core/namespace.js.map +1 -1
- package/dist/core/optimistic-settlement.d.ts +2 -0
- package/dist/core/optimistic-settlement.d.ts.map +1 -0
- package/dist/core/optimistic-settlement.js +14 -0
- package/dist/core/optimistic-settlement.js.map +1 -0
- package/dist/core/packet-wire.d.ts +5 -2
- package/dist/core/packet-wire.d.ts.map +1 -1
- package/dist/core/packet-wire.js +318 -114
- package/dist/core/packet-wire.js.map +1 -1
- package/dist/core/provider-accounting.d.ts +24 -0
- package/dist/core/provider-accounting.d.ts.map +1 -0
- package/dist/core/provider-accounting.js +76 -0
- package/dist/core/provider-accounting.js.map +1 -0
- package/dist/core/results.d.ts +4 -2
- package/dist/core/results.d.ts.map +1 -1
- package/dist/core/results.js +6 -0
- package/dist/core/results.js.map +1 -1
- package/dist/core/scheme-types.d.ts +0 -12
- 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 +2 -2
- package/dist/core/search-gate.d.ts.map +1 -1
- package/dist/core/search-gate.js +8 -8
- package/dist/core/search-gate.js.map +1 -1
- package/dist/core/token-ruler.js +1 -1
- package/dist/core/token-ruler.js.map +1 -1
- package/dist/core/turn-scheduler.js +1 -1
- package/dist/core/turn-scheduler.js.map +1 -1
- package/dist/core/worker-ops.sql +4 -4
- package/dist/core/workspace-settings.js +1 -1
- package/dist/core/workspace-settings.js.map +1 -1
- package/dist/digest/Digest.d.ts.map +1 -1
- package/dist/digest/Digest.js +349 -76
- package/dist/digest/Digest.js.map +1 -1
- package/dist/digest/digest.sql +42 -16
- package/dist/schemes/Exec.d.ts +4 -4
- package/dist/schemes/Exec.d.ts.map +1 -1
- package/dist/schemes/Exec.js +167 -134
- package/dist/schemes/Exec.js.map +1 -1
- package/dist/schemes/ExecOutputScheme.d.ts +4 -5
- package/dist/schemes/ExecOutputScheme.d.ts.map +1 -1
- package/dist/schemes/ExecOutputScheme.js +12 -20
- package/dist/schemes/ExecOutputScheme.js.map +1 -1
- package/dist/schemes/File.d.ts +5 -6
- package/dist/schemes/File.d.ts.map +1 -1
- package/dist/schemes/File.js +35 -41
- package/dist/schemes/File.js.map +1 -1
- package/dist/schemes/Log.d.ts +16 -5
- package/dist/schemes/Log.d.ts.map +1 -1
- package/dist/schemes/Log.js +229 -262
- package/dist/schemes/Log.js.map +1 -1
- package/dist/schemes/Log.sql +50 -27
- package/dist/schemes/Prompt.d.ts +3 -4
- package/dist/schemes/Prompt.d.ts.map +1 -1
- package/dist/schemes/Prompt.js +5 -5
- package/dist/schemes/Prompt.js.map +1 -1
- package/dist/schemes/Skill.d.ts +4 -5
- package/dist/schemes/Skill.d.ts.map +1 -1
- package/dist/schemes/Skill.js +3 -3
- package/dist/schemes/Skill.js.map +1 -1
- package/dist/schemes/Worker.d.ts +7 -7
- package/dist/schemes/Worker.d.ts.map +1 -1
- package/dist/schemes/Worker.js +99 -84
- package/dist/schemes/Worker.js.map +1 -1
- package/dist/schemes/_entry-crud.d.ts +2 -2
- package/dist/schemes/_entry-crud.d.ts.map +1 -1
- package/dist/schemes/_entry-crud.js +29 -10
- package/dist/schemes/_entry-crud.js.map +1 -1
- package/dist/schemes/_entry-crud.sql +11 -17
- package/dist/schemes/_entry-find.d.ts +41 -14
- package/dist/schemes/_entry-find.d.ts.map +1 -1
- package/dist/schemes/_entry-find.js +159 -166
- package/dist/schemes/_entry-find.js.map +1 -1
- package/dist/schemes/_entry-find.sql +2 -21
- package/dist/schemes/_entry-graph.sql +1 -1
- package/dist/schemes/_entry-manifest.d.ts +5 -17
- package/dist/schemes/_entry-manifest.d.ts.map +1 -1
- package/dist/schemes/_entry-manifest.js +28 -24
- package/dist/schemes/_entry-manifest.js.map +1 -1
- package/dist/schemes/_entry-ops.d.ts +13 -8
- package/dist/schemes/_entry-ops.d.ts.map +1 -1
- package/dist/schemes/_entry-ops.js +98 -119
- package/dist/schemes/_entry-ops.js.map +1 -1
- package/dist/schemes/_entry-ops.sql +31 -6
- package/dist/schemes/_entry-semantic.d.ts +5 -7
- package/dist/schemes/_entry-semantic.d.ts.map +1 -1
- package/dist/schemes/_entry-semantic.js +10 -31
- package/dist/schemes/_entry-semantic.js.map +1 -1
- package/dist/schemes/_entry-semantic.sql +2 -2
- package/dist/schemes/_entry-send.js +4 -4
- package/dist/schemes/_entry-send.js.map +1 -1
- package/dist/schemes/_search-index.d.ts.map +1 -1
- package/dist/schemes/_search-index.js +16 -6
- package/dist/schemes/_search-index.js.map +1 -1
- package/dist/schemes/exec-abort.js +2 -2
- package/dist/schemes/exec-abort.js.map +1 -1
- package/dist/server/BranchBatches.d.ts +1 -0
- package/dist/server/BranchBatches.d.ts.map +1 -1
- package/dist/server/BranchBatches.js +1 -0
- package/dist/server/BranchBatches.js.map +1 -1
- package/dist/server/Daemon.d.ts +6 -8
- package/dist/server/Daemon.d.ts.map +1 -1
- package/dist/server/Daemon.js +238 -68
- package/dist/server/Daemon.js.map +1 -1
- package/dist/server/DaemonModule.d.ts +3 -3
- package/dist/server/DaemonModule.d.ts.map +1 -1
- package/dist/server/branch-batch.sql +3 -1
- package/dist/server/client-input.d.ts +2 -1
- package/dist/server/client-input.d.ts.map +1 -1
- package/dist/server/client-input.js +7 -1
- package/dist/server/client-input.js.map +1 -1
- package/dist/server/drain.sql +8 -7
- package/dist/server/envelope.d.ts +0 -2
- package/dist/server/envelope.d.ts.map +1 -1
- package/dist/server/envelope.js +0 -1
- package/dist/server/envelope.js.map +1 -1
- package/dist/server/envelope.sql +3 -4
- package/dist/server/lifecycle-recovery.sql +44 -1
- package/dist/server/logEntry.d.ts +2 -1
- package/dist/server/logEntry.d.ts.map +1 -1
- package/dist/server/logEntry.js +15 -2
- package/dist/server/logEntry.js.map +1 -1
- package/dist/server/logEntry.sql +5 -1
- package/dist/service.d.ts.map +1 -1
- package/dist/service.js +13 -1
- package/dist/service.js.map +1 -1
- package/migrations/001_schema.sql +730 -221
- package/package.json +53 -46
- package/dist/core/BudgetOverflow.d.ts +0 -23
- package/dist/core/BudgetOverflow.d.ts.map +0 -1
- package/dist/core/BudgetOverflow.js +0 -61
- package/dist/core/BudgetOverflow.js.map +0 -1
- package/dist/core/caps/DbTagCaps.d.ts +0 -10
- package/dist/core/caps/DbTagCaps.d.ts.map +0 -1
- package/dist/core/caps/DbTagCaps.js +0 -44
- package/dist/core/caps/DbTagCaps.js.map +0 -1
- package/dist/matrix/live-specimen.d.ts +0 -9
- package/dist/matrix/live-specimen.d.ts.map +0 -1
- package/dist/matrix/live-specimen.js +0 -37
- package/dist/matrix/live-specimen.js.map +0 -1
package/dist/core/Dispatcher.js
CHANGED
|
@@ -5,27 +5,40 @@ import WorkerControlAddress from "./WorkerControlAddress.js";
|
|
|
5
5
|
import { entryPathnameOf, foldAuthorityIntoPath, renderAddress, renderTarget, schemeNameOf } from "./plurnk-uri.js";
|
|
6
6
|
import Fork from "./fork.js";
|
|
7
7
|
import WorkerCap from "./worker-cap.js";
|
|
8
|
-
import {
|
|
8
|
+
import { PathSyntax, TagSignal } from "@plurnk/plurnk-contracts";
|
|
9
9
|
import Namespace from "./namespace.js";
|
|
10
10
|
import LoopFlagsReader from "./LoopFlagsReader.js";
|
|
11
11
|
import ChannelWrite from "./ChannelWrite.js";
|
|
12
|
-
import { assertEditBatchReceipt, assertEditReceipt, assertResourceEffects, editReceipt, LineMarkerOps, MimetypeBinary, PathMimetype, projectEditReceipt, } from "../content/index.js";
|
|
12
|
+
import { assertEditBatchReceipt, assertEditReceipt, assertResourceEffects, EditCollision, editReceipt, LineAnchors, LineMarkerOps, MimetypeBinary, PathMimetype, ReadProjector, projectEditReceipt, } from "../content/index.js";
|
|
13
|
+
import DbProjectionCaps from "./caps/DbProjectionCaps.js";
|
|
13
14
|
import SchemeCtxImpl from "./caps/SchemeCtxImpl.js";
|
|
14
15
|
import EntryCrud from "../schemes/_entry-crud.js";
|
|
15
16
|
import EntryOps from "../schemes/_entry-ops.js";
|
|
17
|
+
import EntryFind from "../schemes/_entry-find.js";
|
|
16
18
|
import WorkspaceSettings from "./workspace-settings.js";
|
|
17
19
|
import LoopLifecycle from "./LoopLifecycle.js";
|
|
20
|
+
import TerminalResult from "./TerminalResult.js";
|
|
18
21
|
import Results from "./results.js";
|
|
19
22
|
import { OperationFailureError } from "./results.js";
|
|
20
23
|
import EffectPolicy from "../schemes/EffectPolicy.js";
|
|
21
|
-
import { CoreSchemeAdapterBase } from "./CoreSchemeServices.js";
|
|
24
|
+
import { CoreSchemeAdapterBase, } from "./CoreSchemeServices.js";
|
|
22
25
|
import { InvalidOperationResultError, NetworkAddress, } from "@plurnk/plurnk-schemes";
|
|
23
26
|
import DurableStatement from "./DurableStatement.js";
|
|
24
27
|
// SPEC {§scheme-surface}: writer must be in target scheme's manifest.writableBy.
|
|
25
28
|
// OPEN/FOLD/READ/FIND are not gated — they curate the log or read, never mutating an entry.
|
|
26
29
|
const MUTATING_OPS = new Set(["EDIT", "SEND", "COPY", "MOVE", "EXEC", "KILL", "FORK", "WORK"]);
|
|
30
|
+
const assertClassifyingSignal = (statement) => {
|
|
31
|
+
if (statement.op === "FIND"
|
|
32
|
+
|| statement.op === "READ"
|
|
33
|
+
|| statement.op === "EDIT"
|
|
34
|
+
|| statement.op === "COPY"
|
|
35
|
+
|| statement.op === "MOVE"
|
|
36
|
+
|| statement.op === "BARE") {
|
|
37
|
+
TagSignal.applied(statement.signal);
|
|
38
|
+
}
|
|
39
|
+
};
|
|
27
40
|
// Op dispatch ({§op-methods-op-dispatch}): gates (writableBy, loop flags), the
|
|
28
|
-
// engine-owned op orchestrations (COPY/MOVE/KILL/SEND
|
|
41
|
+
// engine-owned op orchestrations (COPY/MOVE/KILL/SEND), scheme
|
|
29
42
|
// routing, the durable log write, and the proposal pause.
|
|
30
43
|
class Dispatcher {
|
|
31
44
|
static #failure(code, status, detail, fields = {}, extensions = {}) {
|
|
@@ -53,7 +66,7 @@ class Dispatcher {
|
|
|
53
66
|
#branchCompletionGate;
|
|
54
67
|
#cancelWorker;
|
|
55
68
|
#cancelDescendants;
|
|
56
|
-
// {§send-premature-terminate}/SEND
|
|
69
|
+
// {§send-premature-terminate}/SEND signal 202 with scope — the engine-owned park-deadline registry (loopId → seconds;
|
|
57
70
|
// -1 = indefinite). The dispatcher WRITES at park; the daemon's drain park-exit consumes.
|
|
58
71
|
#parkDeadlines;
|
|
59
72
|
#searchGate;
|
|
@@ -95,8 +108,14 @@ class Dispatcher {
|
|
|
95
108
|
const handlerCtx = this.#handlerContext(scheme, ctx);
|
|
96
109
|
return this.#schemes.manifestFor(scheme)?.category === "data" ? handlerCtx : null;
|
|
97
110
|
}
|
|
98
|
-
|
|
111
|
+
#coreCrud(scheme) {
|
|
99
112
|
const handler = this.#schemes.get(scheme);
|
|
113
|
+
return handler instanceof CoreSchemeAdapterBase
|
|
114
|
+
? handler
|
|
115
|
+
: undefined;
|
|
116
|
+
}
|
|
117
|
+
async #readEntry(scheme, pathname, ctx) {
|
|
118
|
+
const handler = this.#coreCrud(scheme);
|
|
100
119
|
const handlerCtx = this.#entryContext(scheme, ctx);
|
|
101
120
|
if (typeof handler?.readEntry === "function" && handlerCtx !== null) {
|
|
102
121
|
return Results.assert(await handler.readEntry(pathname, handlerCtx));
|
|
@@ -116,11 +135,16 @@ class Dispatcher {
|
|
|
116
135
|
status: result.status,
|
|
117
136
|
entry: result.entry === null
|
|
118
137
|
? null
|
|
119
|
-
: {
|
|
138
|
+
: {
|
|
139
|
+
channels: { ...result.entry.channels },
|
|
140
|
+
...(result.entry.attributes === undefined
|
|
141
|
+
? {}
|
|
142
|
+
: { attributes: { ...result.entry.attributes } }),
|
|
143
|
+
},
|
|
120
144
|
});
|
|
121
145
|
}
|
|
122
146
|
async #writeEntry(scheme, pathname, entry, ctx) {
|
|
123
|
-
const handler = this.#
|
|
147
|
+
const handler = this.#coreCrud(scheme);
|
|
124
148
|
const handlerCtx = this.#entryContext(scheme, ctx);
|
|
125
149
|
if (typeof handler?.writeEntry === "function" && handlerCtx !== null) {
|
|
126
150
|
return Results.assert(await handler.writeEntry(pathname, entry, handlerCtx));
|
|
@@ -137,7 +161,7 @@ class Dispatcher {
|
|
|
137
161
|
return Results.assert(await caps.write(pathname, entry, scheme === "prompt" ? "worker" : "commons"));
|
|
138
162
|
}
|
|
139
163
|
async #deleteEntry(scheme, pathname, ctx) {
|
|
140
|
-
const handler = this.#
|
|
164
|
+
const handler = this.#coreCrud(scheme);
|
|
141
165
|
const handlerCtx = this.#entryContext(scheme, ctx);
|
|
142
166
|
if (typeof handler?.deleteEntry === "function" && handlerCtx !== null) {
|
|
143
167
|
return Results.assert(await handler.deleteEntry(pathname, handlerCtx));
|
|
@@ -154,7 +178,7 @@ class Dispatcher {
|
|
|
154
178
|
return Results.assert(await caps.delete(pathname, scheme === "prompt" ? "worker" : "commons"));
|
|
155
179
|
}
|
|
156
180
|
async #deleteChannel(scheme, pathname, channel, ctx) {
|
|
157
|
-
const handler = this.#
|
|
181
|
+
const handler = this.#coreCrud(scheme);
|
|
158
182
|
const handlerCtx = this.#entryContext(scheme, ctx);
|
|
159
183
|
if (typeof handler?.deleteChannel === "function" && handlerCtx !== null) {
|
|
160
184
|
return Results.assert(await handler.deleteChannel(pathname, channel, handlerCtx));
|
|
@@ -189,11 +213,114 @@ class Dispatcher {
|
|
|
189
213
|
if (key !== null)
|
|
190
214
|
target.pathname = key;
|
|
191
215
|
}
|
|
216
|
+
async #resolveEditAnchors(statements, identity, schemeName, manifest, ctx) {
|
|
217
|
+
const anchored = statements.filter(({ lineMarker }) => LineAnchors.hasAnchor(lineMarker));
|
|
218
|
+
if (anchored.length === 0) {
|
|
219
|
+
return { statements: statements, precondition: null };
|
|
220
|
+
}
|
|
221
|
+
if (manifest.textEditScopes !== true || !manifest.writableBy.includes("model")) {
|
|
222
|
+
return {
|
|
223
|
+
result: _a.#failure("line-anchor-unsupported", 400, `Scheme '${schemeName}' does not support textual EDIT scopes.`, {}, {
|
|
224
|
+
scheme: schemeName,
|
|
225
|
+
operation: "EDIT",
|
|
226
|
+
recovery: "Remove the scope and submit the scheme's complete editable unit.",
|
|
227
|
+
retryable: false,
|
|
228
|
+
}),
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
if (identity === null) {
|
|
232
|
+
return {
|
|
233
|
+
result: _a.#failure("edit-target-required", 400, "A line-anchored EDIT requires a target resource.", {}, { recovery: "Provide the target that rendered the line anchor.", retryable: false }),
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
const first = statements[0];
|
|
237
|
+
if (first === undefined)
|
|
238
|
+
return { statements: [], precondition: null };
|
|
239
|
+
const current = await this.#run(schemeName, {
|
|
240
|
+
op: "READ",
|
|
241
|
+
suffix: first.suffix,
|
|
242
|
+
signal: null,
|
|
243
|
+
target: first.target,
|
|
244
|
+
lineMarker: { marks: [1, -1] },
|
|
245
|
+
body: null,
|
|
246
|
+
position: first.position,
|
|
247
|
+
}, ctx);
|
|
248
|
+
if (current.status === 204 || current.status === 404) {
|
|
249
|
+
return { result: EditCollision.result(identity) };
|
|
250
|
+
}
|
|
251
|
+
if (current.status >= 300) {
|
|
252
|
+
return {
|
|
253
|
+
result: _a.#failure("line-anchor-validation-failed", current.status, `EDIT could not validate its line anchor at ${identity}: ${current.problem?.detail ?? `READ returned ${current.status}`}`, {}, {
|
|
254
|
+
target: identity,
|
|
255
|
+
upstreamStatus: current.status,
|
|
256
|
+
stage: "mutation-precondition",
|
|
257
|
+
retryable: false,
|
|
258
|
+
}),
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
if (current.status !== 200) {
|
|
262
|
+
return { result: EditCollision.result(identity) };
|
|
263
|
+
}
|
|
264
|
+
const content = current.content;
|
|
265
|
+
if (typeof content !== "string") {
|
|
266
|
+
throw new InvalidOperationResultError(`Scheme '${schemeName}' returned READ ${current.status} without textual content while validating an EDIT anchor.`);
|
|
267
|
+
}
|
|
268
|
+
const lineAnchors = current.lineAnchors;
|
|
269
|
+
const lineAnchorIdentity = current.lineAnchorIdentity;
|
|
270
|
+
try {
|
|
271
|
+
LineAnchors.assertProjection(content, lineAnchors);
|
|
272
|
+
if (typeof lineAnchorIdentity !== "string" || lineAnchorIdentity.length === 0) {
|
|
273
|
+
throw new TypeError("READ line anchors require their canonical derivation identity.");
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
catch (cause) {
|
|
277
|
+
throw new InvalidOperationResultError(`Scheme '${schemeName}' returned READ 200 without its core-owned line-anchor projection.`, { cause });
|
|
278
|
+
}
|
|
279
|
+
const resolved = [];
|
|
280
|
+
const checks = [];
|
|
281
|
+
for (const statement of statements) {
|
|
282
|
+
if (statement.lineMarker === null || !LineAnchors.hasAnchor(statement.lineMarker)) {
|
|
283
|
+
resolved.push(statement);
|
|
284
|
+
continue;
|
|
285
|
+
}
|
|
286
|
+
const resolution = LineAnchors.resolve(lineAnchors, statement.lineMarker);
|
|
287
|
+
if (!resolution.ok) {
|
|
288
|
+
const { anchor, kind } = resolution.failure;
|
|
289
|
+
const invalid = kind === "invalid";
|
|
290
|
+
if (!invalid)
|
|
291
|
+
return { result: EditCollision.result(lineAnchorIdentity) };
|
|
292
|
+
return {
|
|
293
|
+
result: _a.#failure("line-anchor-invalid", 400, `${anchor} is not valid in that EDIT line-coordinate position.`, {}, {
|
|
294
|
+
anchor,
|
|
295
|
+
target: identity,
|
|
296
|
+
recovery: "Use anchors only where the EDIT scope denotes a line.",
|
|
297
|
+
retryable: false,
|
|
298
|
+
}),
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
for (const [index, anchor] of statement.lineMarker.marks.entries()) {
|
|
302
|
+
if (typeof anchor !== "string")
|
|
303
|
+
continue;
|
|
304
|
+
const line = resolution.marker.marks[index];
|
|
305
|
+
if (typeof line !== "number") {
|
|
306
|
+
throw new InvalidOperationResultError("An EDIT line anchor did not lower to a numeric line.");
|
|
307
|
+
}
|
|
308
|
+
checks.push({ anchor, line });
|
|
309
|
+
}
|
|
310
|
+
resolved.push({ ...statement, lineMarker: resolution.marker });
|
|
311
|
+
}
|
|
312
|
+
const uniqueChecks = [...new Map(checks.map((check) => [`${check.anchor}:${check.line}`, check])).values()];
|
|
313
|
+
return {
|
|
314
|
+
statements: resolved,
|
|
315
|
+
precondition: { identity: lineAnchorIdentity, checks: uniqueChecks },
|
|
316
|
+
};
|
|
317
|
+
}
|
|
192
318
|
async prepareEditBatches(statements, context) {
|
|
193
319
|
const { workspaceId, workerId, loopId, turnId, origin } = context;
|
|
194
320
|
const schemeCtx = this.#buildSchemeCtx({ workspaceId, workerId, loopId, turnId, origin });
|
|
195
321
|
const groups = new Map();
|
|
196
322
|
for (const statement of statements) {
|
|
323
|
+
assertClassifyingSignal(statement); // {§log-tag-signal}
|
|
197
324
|
const target = this.#extractTarget(statement.target);
|
|
198
325
|
await this.#canonColumns(target, workspaceId);
|
|
199
326
|
const key = JSON.stringify([
|
|
@@ -205,11 +332,15 @@ class Dispatcher {
|
|
|
205
332
|
]);
|
|
206
333
|
const group = groups.get(key);
|
|
207
334
|
if (group === undefined)
|
|
208
|
-
groups.set(key,
|
|
335
|
+
groups.set(key, {
|
|
336
|
+
identity: renderTarget(target),
|
|
337
|
+
statements: [statement],
|
|
338
|
+
});
|
|
209
339
|
else
|
|
210
|
-
group.push(statement);
|
|
340
|
+
group.statements.push(statement);
|
|
211
341
|
}
|
|
212
|
-
for (const
|
|
342
|
+
for (const preparedGroup of groups.values()) {
|
|
343
|
+
const group = preparedGroup.statements;
|
|
213
344
|
const first = group[0];
|
|
214
345
|
const schemeName = schemeNameOf(first.target);
|
|
215
346
|
let initial;
|
|
@@ -240,8 +371,20 @@ class Dispatcher {
|
|
|
240
371
|
}
|
|
241
372
|
else {
|
|
242
373
|
try {
|
|
243
|
-
const
|
|
244
|
-
|
|
374
|
+
const resolved = await this.#resolveEditAnchors(group, preparedGroup.identity, schemeName, manifest, schemeCtx);
|
|
375
|
+
if ("result" in resolved) {
|
|
376
|
+
initial = resolved.result;
|
|
377
|
+
}
|
|
378
|
+
else {
|
|
379
|
+
const addressedScheme = first.target?.kind === "url" ? first.target.scheme : schemeName;
|
|
380
|
+
const publishedChannel = first.target?.kind === "url"
|
|
381
|
+
? first.target.fragment ?? manifest.defaultChannel
|
|
382
|
+
: manifest.defaultChannel;
|
|
383
|
+
initial = Results.assert(await method.call(handler, resolved.statements, new SchemeCtxImpl(schemeCtx, addressedScheme ?? schemeName, manifest, this.#liveSubscriptions, {
|
|
384
|
+
publishedChannel,
|
|
385
|
+
editPrecondition: resolved.precondition,
|
|
386
|
+
})));
|
|
387
|
+
}
|
|
245
388
|
}
|
|
246
389
|
catch (err) {
|
|
247
390
|
if (err instanceof InvalidOperationResultError)
|
|
@@ -284,6 +427,7 @@ class Dispatcher {
|
|
|
284
427
|
}
|
|
285
428
|
}
|
|
286
429
|
async dispatch(context) {
|
|
430
|
+
assertClassifyingSignal(context.statement); // {§log-tag-signal}
|
|
287
431
|
const result = await this.#dispatchOne(context);
|
|
288
432
|
if (context.statement.op === "EDIT") {
|
|
289
433
|
const prepared = this.#preparedEdits.get(context.statement);
|
|
@@ -297,31 +441,16 @@ class Dispatcher {
|
|
|
297
441
|
return result;
|
|
298
442
|
}
|
|
299
443
|
async #dispatchOne(context) {
|
|
300
|
-
const { statement, workspaceId, workerId, loopId, turnId, sequence, origin, onDispatch,
|
|
444
|
+
const { statement, workspaceId, workerId, loopId, turnId, sequence, origin, onDispatch, } = context;
|
|
301
445
|
const schemeCtx = this.#buildSchemeCtx({ workspaceId, workerId, loopId, turnId, origin });
|
|
302
446
|
let result;
|
|
303
|
-
let
|
|
304
|
-
|
|
305
|
-
? _a.#failure("operation-not-allowed", 409, `${statement.op} was not executed because ${operationConstraint.detail}.`, {}, {
|
|
306
|
-
constraint: operationConstraint.code,
|
|
307
|
-
allowedOperations: [...operationConstraint.allowedOperations],
|
|
308
|
-
retryable: false,
|
|
309
|
-
})
|
|
310
|
-
: null;
|
|
311
|
-
if (denial === null)
|
|
312
|
-
denial = this.#checkWritable(statement, origin);
|
|
447
|
+
let curationPlan = null;
|
|
448
|
+
let denial = this.#checkWritable(statement, origin);
|
|
313
449
|
if (denial === null)
|
|
314
450
|
denial = await this.#checkFlagsGate(statement, loopId);
|
|
315
451
|
if (denial !== null) {
|
|
316
452
|
result = denial;
|
|
317
453
|
}
|
|
318
|
-
else if (this.#readFansOut(statement)) {
|
|
319
|
-
// Multi-resource scopes, relation matchers, and acquisition-scheme
|
|
320
|
-
// matchers use FIND for selection and write their own rows. An exact
|
|
321
|
-
// stored-resource content matcher remains a direct READ, preserving
|
|
322
|
-
// content-level outcomes such as 203.
|
|
323
|
-
return await this.#handleReadFanout(statement, schemeCtx, { workerId, loopId, turnId, sequence, origin, onDispatch });
|
|
324
|
-
}
|
|
325
454
|
else {
|
|
326
455
|
// {§scheme-surface-exception-500} Scheme-handler
|
|
327
456
|
// exceptions become the action-entry's outcome (status 500), not a
|
|
@@ -363,7 +492,9 @@ class Dispatcher {
|
|
|
363
492
|
result = await this.#handleSendBroadcast(statement, { workspaceId, workerId, loopId, turnId, sequence });
|
|
364
493
|
}
|
|
365
494
|
else if (statement.op === "OPEN" || statement.op === "FOLD") {
|
|
366
|
-
|
|
495
|
+
const curation = await this.#runLogCuration(statement, schemeCtx);
|
|
496
|
+
result = curation.result;
|
|
497
|
+
curationPlan = curation.plan;
|
|
367
498
|
}
|
|
368
499
|
else if (statement.op === "FORK" || statement.op === "WORK") {
|
|
369
500
|
result = await this.#handleWorkerControl(statement, schemeCtx);
|
|
@@ -381,10 +512,8 @@ class Dispatcher {
|
|
|
381
512
|
result = this.#handlePlan(statement);
|
|
382
513
|
}
|
|
383
514
|
else if (statement.op === "EXEC") {
|
|
384
|
-
// EXEC
|
|
385
|
-
//
|
|
386
|
-
// exec scheme; the scheme handler reads runtime
|
|
387
|
-
// (signal), cwd (target), and command (body).
|
|
515
|
+
// EXEC routes unconditionally to its operation owner. The
|
|
516
|
+
// resolved runtime declaration owns body/target semantics.
|
|
388
517
|
result = await this.#gatedExec(statement, schemeCtx, loopId, turnId);
|
|
389
518
|
}
|
|
390
519
|
else {
|
|
@@ -411,19 +540,30 @@ class Dispatcher {
|
|
|
411
540
|
// {§join-blocking-collect}
|
|
412
541
|
if (typeof result.awaitWorker === "string")
|
|
413
542
|
this.#joinTargets.add(loopId);
|
|
414
|
-
const logEntryId = await this.#writeLog({
|
|
543
|
+
const logEntryId = await this.#writeLog({
|
|
544
|
+
statement,
|
|
545
|
+
result,
|
|
546
|
+
workspaceId,
|
|
547
|
+
workerId,
|
|
548
|
+
loopId,
|
|
549
|
+
turnId,
|
|
550
|
+
sequence,
|
|
551
|
+
origin,
|
|
552
|
+
curationPlan,
|
|
553
|
+
modelCallId: null,
|
|
554
|
+
});
|
|
415
555
|
// {§search-gate} — register successful searches AFTER #writeLog stamps the runtime
|
|
416
556
|
// entry's coordinate onto result.attrs.pathname (the gate's dedup serves from it).
|
|
417
557
|
if (statement.op === "EXEC" && result.status < 400) {
|
|
418
558
|
const rt = ("signal" in statement && typeof statement.signal === "string" && statement.signal.length > 0) ? statement.signal : "sh";
|
|
419
|
-
const
|
|
559
|
+
const body = ("body" in statement && typeof statement.body === "string") ? statement.body : "";
|
|
420
560
|
const attrPath = result.attrs?.pathname;
|
|
421
561
|
if (typeof attrPath === "string")
|
|
422
|
-
this.#searchGate?.registerPending(loopId, turnId, rt,
|
|
562
|
+
this.#searchGate?.registerPending(loopId, turnId, rt, body, attrPath);
|
|
423
563
|
}
|
|
424
564
|
onDispatch?.(logEntryId);
|
|
425
565
|
// Proposal lifecycle (SPEC.md {§engine-rails} + {§methods-proposal-resolve}; {§proposal-202-pauses}). When a
|
|
426
|
-
// side-effecting op returns status 202 (a broadcast SEND
|
|
566
|
+
// side-effecting op returns status 202 (a broadcast SEND signal 202 park is model
|
|
427
567
|
// speech, not a proposal — #isProposal), the entry is written
|
|
428
568
|
// state='proposed'; dispatch then PAUSES on a per-entry waiter until
|
|
429
569
|
// resolution arrives via Engine.resolveProposal (from a client-interface resume,
|
|
@@ -531,11 +671,49 @@ class Dispatcher {
|
|
|
531
671
|
origin: "client",
|
|
532
672
|
});
|
|
533
673
|
const schemeCtx = new SchemeCtxImpl(coreCtx, addressedScheme, manifest, this.#liveSubscriptions);
|
|
674
|
+
const resolved = await this.#resolveDataEntryAddress({
|
|
675
|
+
target,
|
|
676
|
+
routedScheme,
|
|
677
|
+
handler,
|
|
678
|
+
manifest,
|
|
679
|
+
ctx: coreCtx,
|
|
680
|
+
schemeCtx,
|
|
681
|
+
});
|
|
682
|
+
if (resolved.address === null)
|
|
683
|
+
return null;
|
|
684
|
+
const rendered = target.kind === "url"
|
|
685
|
+
? renderTarget({ ...target, fragment: null })
|
|
686
|
+
: renderTarget({ scheme: null, pathname: target.raw, fragment: null });
|
|
687
|
+
if (rendered === null)
|
|
688
|
+
throw new TypeError("Resolved entry target did not render.");
|
|
689
|
+
return {
|
|
690
|
+
ownerId: resolved.address.ownerId,
|
|
691
|
+
scheme: resolved.address.scheme,
|
|
692
|
+
pathname: resolved.address.pathname,
|
|
693
|
+
target: rendered,
|
|
694
|
+
};
|
|
695
|
+
}
|
|
696
|
+
async #resolveDataEntryAddress({ target, routedScheme, handler, manifest, ctx, schemeCtx, }) {
|
|
697
|
+
const addressedScheme = target.kind === "url" ? target.scheme : routedScheme;
|
|
698
|
+
const identityTarget = target.kind === "url"
|
|
699
|
+
? {
|
|
700
|
+
...target,
|
|
701
|
+
pathname: PathSyntax.decodeParens(target.pathname),
|
|
702
|
+
fragment: null,
|
|
703
|
+
}
|
|
704
|
+
: { ...target, raw: PathSyntax.decodeParens(target.raw) };
|
|
534
705
|
const resolved = handler.resolveEntryAddress === undefined
|
|
535
|
-
? { pathname: entryPathnameOf(
|
|
536
|
-
: await handler.resolveEntryAddress(
|
|
706
|
+
? { pathname: entryPathnameOf(identityTarget), owner: "commons" }
|
|
707
|
+
: await handler.resolveEntryAddress(identityTarget, schemeCtx);
|
|
537
708
|
if (resolved === null)
|
|
538
|
-
return null;
|
|
709
|
+
return { address: null, result: null };
|
|
710
|
+
if ("status" in resolved) {
|
|
711
|
+
const result = Results.assert(resolved);
|
|
712
|
+
if (result.status < 300) {
|
|
713
|
+
throw new InvalidOperationResultError(`Scheme '${routedScheme}' returned a successful operation result instead of an entry address.`);
|
|
714
|
+
}
|
|
715
|
+
return { address: null, result };
|
|
716
|
+
}
|
|
539
717
|
if (typeof resolved.pathname !== "string") {
|
|
540
718
|
throw new TypeError(`Scheme '${routedScheme}' returned an invalid entry pathname.`);
|
|
541
719
|
}
|
|
@@ -547,10 +725,10 @@ class Dispatcher {
|
|
|
547
725
|
ownerId = resolved.ownerId;
|
|
548
726
|
}
|
|
549
727
|
else if (resolved.owner === "worker") {
|
|
550
|
-
ownerId = workerId;
|
|
728
|
+
ownerId = ctx.workerId;
|
|
551
729
|
}
|
|
552
730
|
else if (resolved.owner === "commons") {
|
|
553
|
-
ownerId = await Owner.commonsId(this.#db, workspaceId);
|
|
731
|
+
ownerId = await Owner.commonsId(this.#db, ctx.workspaceId);
|
|
554
732
|
}
|
|
555
733
|
else {
|
|
556
734
|
throw new TypeError(`Scheme '${routedScheme}' returned an invalid entry owner.`);
|
|
@@ -558,16 +736,48 @@ class Dispatcher {
|
|
|
558
736
|
if (!Number.isSafeInteger(ownerId) || ownerId < 1) {
|
|
559
737
|
throw new TypeError(`Scheme '${routedScheme}' returned an invalid entry owner id.`);
|
|
560
738
|
}
|
|
561
|
-
const rendered = target.kind === "url"
|
|
562
|
-
? renderTarget({ ...target, fragment: null })
|
|
563
|
-
: renderTarget({ scheme: null, pathname: target.raw, fragment: null });
|
|
564
|
-
if (rendered === null)
|
|
565
|
-
throw new TypeError("Resolved entry target did not render.");
|
|
566
739
|
return {
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
740
|
+
address: {
|
|
741
|
+
ownerId,
|
|
742
|
+
scheme: manifest.storedScheme ?? addressedScheme,
|
|
743
|
+
pathname: resolved.pathname,
|
|
744
|
+
},
|
|
745
|
+
result: null,
|
|
746
|
+
};
|
|
747
|
+
}
|
|
748
|
+
async #prepareDataRepresentation({ target, routedScheme, handler, manifest, ctx, schemeCtx, publishedChannel, }) {
|
|
749
|
+
const resolved = await this.#resolveDataEntryAddress({
|
|
750
|
+
target,
|
|
751
|
+
routedScheme,
|
|
752
|
+
handler,
|
|
753
|
+
manifest,
|
|
754
|
+
ctx,
|
|
755
|
+
schemeCtx,
|
|
756
|
+
});
|
|
757
|
+
if (resolved.address === null
|
|
758
|
+
|| resolved.result !== null
|
|
759
|
+
|| typeof handler.prepareRepresentation !== "function") {
|
|
760
|
+
return resolved;
|
|
761
|
+
}
|
|
762
|
+
const address = resolved.address;
|
|
763
|
+
const selectionNeutralTarget = target.kind === "url"
|
|
764
|
+
? {
|
|
765
|
+
...target,
|
|
766
|
+
raw: renderTarget({ ...target, fragment: null }) ?? target.raw,
|
|
767
|
+
fragment: null,
|
|
768
|
+
}
|
|
769
|
+
: target;
|
|
770
|
+
const preparationCtx = new SchemeCtxImpl(ctx, target.kind === "url" ? target.scheme : routedScheme, manifest, this.#liveSubscriptions, {
|
|
771
|
+
ownerId: address.ownerId,
|
|
772
|
+
publishedChannel,
|
|
773
|
+
});
|
|
774
|
+
const prepared = Results.assertRepresentationPreparation(await handler.prepareRepresentation({
|
|
775
|
+
target: selectionNeutralTarget,
|
|
776
|
+
pathname: address.pathname,
|
|
777
|
+
}, preparationCtx));
|
|
778
|
+
return {
|
|
779
|
+
address,
|
|
780
|
+
result: prepared.status === 200 ? null : prepared,
|
|
571
781
|
};
|
|
572
782
|
}
|
|
573
783
|
// An accepted EXEC reads a non-file source through the same registered
|
|
@@ -611,8 +821,8 @@ class Dispatcher {
|
|
|
611
821
|
return null;
|
|
612
822
|
if (statement.op === "SEND" && statement.target === null)
|
|
613
823
|
return null;
|
|
614
|
-
// EXEC's
|
|
615
|
-
//
|
|
824
|
+
// EXEC's operation authority always belongs to the exec scheme;
|
|
825
|
+
// runtime-specific resource authority is gated separately below.
|
|
616
826
|
if (statement.op === "EXEC") {
|
|
617
827
|
return this.#denyIfDisallowed("exec", origin);
|
|
618
828
|
}
|
|
@@ -644,8 +854,8 @@ class Dispatcher {
|
|
|
644
854
|
// the prior durable digest and the per-turn cap refuses without execution.
|
|
645
855
|
async #gatedExec(statement, ctx, loopId, turnId) {
|
|
646
856
|
const runtime = ("signal" in statement && typeof statement.signal === "string" && statement.signal.length > 0) ? statement.signal : "sh";
|
|
647
|
-
const
|
|
648
|
-
const verdict = this.#searchGate?.check(loopId, turnId, runtime,
|
|
857
|
+
const body = ("body" in statement && typeof statement.body === "string") ? statement.body : "";
|
|
858
|
+
const verdict = this.#searchGate?.check(loopId, turnId, runtime, body) ?? { verdict: "pass" };
|
|
649
859
|
if (verdict.verdict === "capped") {
|
|
650
860
|
return _a.#failure("search-limit-reached", 429, `This turn already used its ${verdict.cap} permitted searches.`, {}, {
|
|
651
861
|
searchLimit: verdict.cap,
|
|
@@ -752,12 +962,17 @@ class Dispatcher {
|
|
|
752
962
|
if (statement.op === "COPY" || statement.op === "MOVE") {
|
|
753
963
|
return check(statement.target) ?? check(statement.body?.target ?? null);
|
|
754
964
|
}
|
|
755
|
-
// {§exec-target-routing} —
|
|
756
|
-
//
|
|
965
|
+
// {§exec-target-routing} — only a runtime-declared resource target adds
|
|
966
|
+
// source authority. Literal identifiers and path targets stay executor-local.
|
|
757
967
|
if (statement.op === "EXEC") {
|
|
758
968
|
const operationDenial = checkScheme("exec");
|
|
759
969
|
if (operationDenial !== null)
|
|
760
970
|
return operationDenial;
|
|
971
|
+
const requested = typeof statement.signal === "string" ? statement.signal : "";
|
|
972
|
+
const runtime = requested === "" ? "sh" : requested;
|
|
973
|
+
const targetKind = this.#executors()?.entry(runtime)?.invocation.target?.kind;
|
|
974
|
+
if (targetKind !== "resource")
|
|
975
|
+
return null;
|
|
761
976
|
const sourceScheme = schemeNameOf(statement.target);
|
|
762
977
|
return sourceScheme === null || sourceScheme === "file" ? null : checkScheme(sourceScheme);
|
|
763
978
|
}
|
|
@@ -829,7 +1044,7 @@ class Dispatcher {
|
|
|
829
1044
|
if (statement.op === "FORK") {
|
|
830
1045
|
// Branch the current worker's log into a named sister.
|
|
831
1046
|
const branchWorkerId = await Fork.fork(this.#db, ctx.workerId, name);
|
|
832
|
-
await ctx.injectWorker({ workspaceId: ctx.workspaceId, workerId: branchWorkerId, prompt, flags });
|
|
1047
|
+
await ctx.injectWorker({ workspaceId: ctx.workspaceId, workerId: branchWorkerId, prompt, flags, parentLoopId: ctx.loopId });
|
|
833
1048
|
return { status: 200, body: name };
|
|
834
1049
|
}
|
|
835
1050
|
// WORK — a fresh worker sister named <name>.
|
|
@@ -838,7 +1053,7 @@ class Dispatcher {
|
|
|
838
1053
|
});
|
|
839
1054
|
if (row === undefined)
|
|
840
1055
|
throw new Error("worker spawn: worker insert returned no row");
|
|
841
|
-
await ctx.injectWorker({ workspaceId: ctx.workspaceId, workerId: row.id, prompt, flags });
|
|
1056
|
+
await ctx.injectWorker({ workspaceId: ctx.workspaceId, workerId: row.id, prompt, flags, parentLoopId: ctx.loopId });
|
|
842
1057
|
return { status: 200, body: name };
|
|
843
1058
|
}
|
|
844
1059
|
async #handleCopy(statement, ctx) {
|
|
@@ -870,11 +1085,19 @@ class Dispatcher {
|
|
|
870
1085
|
retryable: false,
|
|
871
1086
|
});
|
|
872
1087
|
}
|
|
1088
|
+
const sourceMarks = statement.lineMarker?.marks;
|
|
1089
|
+
const sourceLineMarker = sourceMarks?.length === 2
|
|
1090
|
+
&& sourceMarks[0] === 1
|
|
1091
|
+
&& sourceMarks[1] === -1
|
|
1092
|
+
? null
|
|
1093
|
+
: statement.lineMarker;
|
|
873
1094
|
return this.#moveOrchestration({
|
|
874
1095
|
statement,
|
|
875
1096
|
source: {
|
|
876
1097
|
target: statement.target,
|
|
877
|
-
|
|
1098
|
+
// Canonicalize only the execution selection. #writeLog retains
|
|
1099
|
+
// the authored marker as operation evidence. {§move-canonical-whole-source}
|
|
1100
|
+
lineMarker: sourceLineMarker,
|
|
878
1101
|
},
|
|
879
1102
|
destination: statement.body,
|
|
880
1103
|
ctx,
|
|
@@ -949,185 +1172,48 @@ class Dispatcher {
|
|
|
949
1172
|
// carries the delete target to review (#isProposal fires on 202). Plurnk-internal deletes execute inline.
|
|
950
1173
|
return this.#deleteEntry(schemeName, entryPathnameOf(path), ctx);
|
|
951
1174
|
}
|
|
952
|
-
|
|
953
|
-
// READ row per selected resource. Match coordinates remain metadata on that
|
|
954
|
-
// resource; they never create extra rows or replace its content.
|
|
955
|
-
#readFansOut(statement) {
|
|
956
|
-
if (statement.op !== "READ")
|
|
957
|
-
return false;
|
|
958
|
-
const body = statement.body;
|
|
959
|
-
if (body?.dialect === "semantic" || body?.dialect === "graph")
|
|
960
|
-
return true;
|
|
961
|
-
const schemeName = schemeNameOf(statement.target);
|
|
962
|
-
const handler = schemeName === null
|
|
963
|
-
? undefined
|
|
964
|
-
: this.#schemes.get(schemeName);
|
|
965
|
-
if (body !== null && typeof handler?.prepareFind === "function")
|
|
966
|
-
return true;
|
|
967
|
-
const t = statement.target;
|
|
968
|
-
const p = t === null ? "" : (t.kind === "url" ? t.pathname : t.raw);
|
|
969
|
-
if (PathSyntax.hasGlob(p))
|
|
970
|
-
return true;
|
|
971
|
-
return p.endsWith("/") && schemeName !== null && this.#schemes.manifestFor(schemeName)?.folderScopes === true;
|
|
972
|
-
}
|
|
973
|
-
static #retargetRead(statement, pathname, lineMarker, body) {
|
|
974
|
-
const t = statement.target;
|
|
975
|
-
const target = t !== null && t.kind === "url"
|
|
976
|
-
? { ...t, pathname, raw: `${t.scheme}://${pathname}` }
|
|
977
|
-
: { ...t, raw: pathname };
|
|
978
|
-
return { ...statement, target: target, lineMarker, body };
|
|
979
|
-
}
|
|
980
|
-
// READ scope always projects text. Semantic READ reserves a leading decimal
|
|
981
|
-
// for similarity selection; any remaining integers are the text projection.
|
|
982
|
-
// FIND retains its own public result-range interpretation.
|
|
983
|
-
static #readMarkers(statement) {
|
|
984
|
-
const marker = statement.lineMarker;
|
|
985
|
-
if (statement.body?.dialect !== "semantic") {
|
|
986
|
-
return { selection: null, projection: marker };
|
|
987
|
-
}
|
|
988
|
-
if (marker === null)
|
|
989
|
-
return { selection: null, projection: null };
|
|
990
|
-
const [first, ...rest] = marker.marks;
|
|
991
|
-
if (Number.isInteger(first)) {
|
|
992
|
-
return { selection: null, projection: marker };
|
|
993
|
-
}
|
|
994
|
-
return {
|
|
995
|
-
selection: { marks: [first] },
|
|
996
|
-
projection: rest.length === 0 ? null : { marks: rest },
|
|
997
|
-
};
|
|
998
|
-
}
|
|
999
|
-
#standardEntryManifest(schemeName, statement) {
|
|
1000
|
-
const handler = this.#schemes.get(schemeName);
|
|
1001
|
-
if (typeof handler?.find === "function")
|
|
1002
|
-
return null;
|
|
1003
|
-
const target = statement.target;
|
|
1004
|
-
if (target === null || target.kind !== "url" || target.scheme === null)
|
|
1005
|
-
return null;
|
|
1006
|
-
const manifest = this.#schemes.manifestFor(schemeName);
|
|
1007
|
-
return manifest === undefined ? null : { ...manifest, name: target.scheme };
|
|
1008
|
-
}
|
|
1009
|
-
static #storedEntryRead(statement, pathname, lineMarker, body, storage) {
|
|
1010
|
-
const target = statement.target;
|
|
1011
|
-
if (target === null || target.kind !== "url")
|
|
1012
|
-
return _a.#retargetRead(statement, pathname, lineMarker, body);
|
|
1013
|
-
if (NetworkAddress.supports(target.scheme)) {
|
|
1014
|
-
const parsed = parsePath(renderAddress(target.scheme, pathname));
|
|
1015
|
-
if (parsed === null || parsed.kind !== "url") {
|
|
1016
|
-
throw new Error(`failed to reconstruct network entry target for ${target.scheme}:${pathname}`);
|
|
1017
|
-
}
|
|
1018
|
-
const reconstructed = { ...parsed, fragment: target.fragment };
|
|
1019
|
-
const raw = renderTarget(reconstructed);
|
|
1020
|
-
if (raw === null)
|
|
1021
|
-
throw new Error(`failed to render network entry target for ${target.scheme}:${pathname}`);
|
|
1022
|
-
return {
|
|
1023
|
-
...statement,
|
|
1024
|
-
target: { ...reconstructed, raw },
|
|
1025
|
-
lineMarker,
|
|
1026
|
-
body,
|
|
1027
|
-
};
|
|
1028
|
-
}
|
|
1029
|
-
const hostPrefix = target.hostname === null ? null : `/${target.hostname}`;
|
|
1030
|
-
const displayPath = hostPrefix !== null && pathname.startsWith(`${hostPrefix}/`)
|
|
1031
|
-
? pathname.slice(hostPrefix.length)
|
|
1032
|
-
: pathname;
|
|
1033
|
-
return {
|
|
1034
|
-
...statement,
|
|
1035
|
-
target: storage
|
|
1036
|
-
? { ...target, hostname: null, pathname, raw: `${target.scheme}://${pathname}` }
|
|
1037
|
-
: { ...target, pathname: displayPath, raw: `${target.scheme}://${target.hostname ?? ""}${displayPath}` },
|
|
1038
|
-
lineMarker,
|
|
1039
|
-
body,
|
|
1040
|
-
};
|
|
1041
|
-
}
|
|
1042
|
-
async #handleReadFanout(statement, ctx, ids) {
|
|
1043
|
-
const { workerId, loopId, turnId, sequence, origin, onDispatch } = ids;
|
|
1044
|
-
const schemeName = schemeNameOf(statement.target);
|
|
1045
|
-
const read = statement;
|
|
1046
|
-
const markers = _a.#readMarkers(read);
|
|
1047
|
-
const findStatement = {
|
|
1048
|
-
...statement,
|
|
1049
|
-
op: "FIND",
|
|
1050
|
-
lineMarker: markers.selection,
|
|
1051
|
-
};
|
|
1052
|
-
const standardEntryManifest = schemeName === null ? null : this.#standardEntryManifest(schemeName, statement);
|
|
1053
|
-
const found = await this.#run(schemeName, findStatement, ctx);
|
|
1054
|
-
const matches = found.matches ?? [];
|
|
1055
|
-
const omittedItems = typeof found.omittedItems === "number" ? found.omittedItems : null;
|
|
1056
|
-
if (omittedItems !== null) {
|
|
1057
|
-
const maximumItems = typeof found.maximumItems === "number" ? found.maximumItems : null;
|
|
1058
|
-
const result = _a.#failure("read-materialization-too-large", 413, `READ selected ${omittedItems} resources, exceeding the materialization limit${maximumItems === null ? "" : ` of ${maximumItems}`}.`, {}, {
|
|
1059
|
-
resources: omittedItems,
|
|
1060
|
-
...(maximumItems === null ? {} : { maximumResources: maximumItems }),
|
|
1061
|
-
stage: "materialization",
|
|
1062
|
-
recovery: "Narrow the target or matcher.",
|
|
1063
|
-
retryable: false,
|
|
1064
|
-
});
|
|
1065
|
-
const readRowId = await this.#writeLog({ statement, result, workspaceId: ctx.workspaceId, workerId, loopId, turnId, sequence, origin });
|
|
1066
|
-
onDispatch?.(readRowId);
|
|
1067
|
-
return { ...result, rowsWritten: 1 };
|
|
1068
|
-
}
|
|
1069
|
-
// Find-less scheme, a matcher/scope error, or zero matches → a single row carrying the
|
|
1070
|
-
// status, exactly like a non-fanned READ. The model sees the empty/failed result, not silence.
|
|
1071
|
-
if (found.status !== 200 || matches.length === 0) {
|
|
1072
|
-
const result = found.status === 200
|
|
1073
|
-
? { status: 204, ...(read.body === null ? {} : { matches: [] }) }
|
|
1074
|
-
: found;
|
|
1075
|
-
const id = await this.#writeLog({ statement, result, workspaceId: ctx.workspaceId, workerId, loopId, turnId, sequence, origin });
|
|
1076
|
-
onDispatch?.(id);
|
|
1077
|
-
return { ...result, rowsWritten: 1 };
|
|
1078
|
-
}
|
|
1079
|
-
const fannedStatuses = [];
|
|
1080
|
-
const scopeNormalizations = [];
|
|
1081
|
-
let written = 0;
|
|
1082
|
-
for (const match of matches) {
|
|
1083
|
-
ctx.signal?.throwIfAborted();
|
|
1084
|
-
const executionStatement = standardEntryManifest === null
|
|
1085
|
-
? _a.#retargetRead(statement, match.pathname, markers.projection, null)
|
|
1086
|
-
: _a.#storedEntryRead(statement, match.pathname, markers.projection, null, true);
|
|
1087
|
-
const readResult = standardEntryManifest === null
|
|
1088
|
-
? await this.#run(schemeName, executionStatement, ctx)
|
|
1089
|
-
: await EntryOps.readWorkspaceEntry(executionStatement, ctx, standardEntryManifest);
|
|
1090
|
-
const result = read.body === null
|
|
1091
|
-
? readResult
|
|
1092
|
-
: { ...readResult, matches: match.matches };
|
|
1093
|
-
const loggedStatement = standardEntryManifest === null
|
|
1094
|
-
? _a.#retargetRead(statement, match.pathname, statement.lineMarker, read.body)
|
|
1095
|
-
: _a.#storedEntryRead(statement, match.pathname, statement.lineMarker, read.body, false);
|
|
1096
|
-
const id = await this.#writeLog({ statement: loggedStatement, result, workspaceId: ctx.workspaceId, workerId, loopId, turnId, sequence: sequence + written, origin });
|
|
1097
|
-
onDispatch?.(id);
|
|
1098
|
-
fannedStatuses.push(result.status);
|
|
1099
|
-
scopeNormalizations.push(...(result.scopeNormalizations ?? []));
|
|
1100
|
-
written++;
|
|
1101
|
-
}
|
|
1102
|
-
return {
|
|
1103
|
-
status: 200,
|
|
1104
|
-
rowsWritten: written,
|
|
1105
|
-
fannedStatuses,
|
|
1106
|
-
...(scopeNormalizations.length === 0 ? {} : { scopeNormalizations }),
|
|
1107
|
-
};
|
|
1108
|
-
}
|
|
1109
|
-
// {§model-entry} — mirror an admitted model emission back as an actionless `model` log row, so
|
|
1110
|
-
// the model can inspect and curate its own prior behavior.
|
|
1111
|
-
// Born FOLDED by default (budget-neutral until OPENed); the turn-0 exemplar passes folded:false
|
|
1112
|
-
// (born open — the one worked example the model orients on, thinning the grammar). text/vnd.plurnk.
|
|
1113
|
-
async writeModelEntry({ verbatim, workerId, loopId, turnId, sequence, folded, origin = "model", reasoningItems }) {
|
|
1175
|
+
async #writeActionlessEntry({ verbatim, workerId, loopId, turnId, sequence, origin, kind, folded, modelCallId = null, attrs = {} }) {
|
|
1114
1176
|
const row = await this.#db.engine_insert_log_entry.get({
|
|
1115
1177
|
worker_id: workerId, loop_id: loopId, turn_id: turnId, sequence,
|
|
1116
|
-
origin, source: null,
|
|
1178
|
+
origin, source: null, model_call_id: modelCallId,
|
|
1179
|
+
op: null, suffix: "", signal: null,
|
|
1117
1180
|
scheme: null, username: null, password: null, hostname: null, port: null,
|
|
1118
1181
|
pathname: null, query: null, fragment: null, lineMarker: null,
|
|
1119
1182
|
tx: "", mimetype_tx: "text/vnd.plurnk",
|
|
1120
1183
|
rx: JSON.stringify({ content: verbatim, mimetype: "text/vnd.plurnk" }),
|
|
1121
1184
|
mimetype_rx: "application/json",
|
|
1122
1185
|
status_rx: 200, tokens: this.#tokenize(verbatim), state: "resolved", outcome: null,
|
|
1123
|
-
attrs:
|
|
1186
|
+
attrs: JSON.stringify({ ...attrs, kind }),
|
|
1124
1187
|
});
|
|
1125
1188
|
if (row === undefined)
|
|
1126
|
-
throw new Error("Dispatcher
|
|
1189
|
+
throw new Error("Dispatcher.#writeActionlessEntry: insert returned no row");
|
|
1127
1190
|
if (folded)
|
|
1128
1191
|
await this.#db.engine_fold_log_entry.run({ id: row.id });
|
|
1129
1192
|
return row.id;
|
|
1130
1193
|
}
|
|
1194
|
+
// {§worker-initialization-entry} — the kernel-authored, born-open worked
|
|
1195
|
+
// turn that initializes a worker's log without impersonating model output.
|
|
1196
|
+
async writeInitializationEntry({ verbatim, workerId, loopId, turnId, sequence }) {
|
|
1197
|
+
return this.#writeActionlessEntry({
|
|
1198
|
+
verbatim, workerId, loopId, turnId, sequence,
|
|
1199
|
+
origin: "plurnk", kind: "initialization", folded: false,
|
|
1200
|
+
});
|
|
1201
|
+
}
|
|
1202
|
+
// {§model-entry} — mirror a model emission back as an actionless log row, so
|
|
1203
|
+
// the model can inspect and curate its own prior behavior. Ordinary admitted
|
|
1204
|
+
// emissions are folded; the bounded invalid-emission lifeline is the one
|
|
1205
|
+
// born-open rejected item under {§invalid-emission-attempts} and labels
|
|
1206
|
+
// that admission fact structurally.
|
|
1207
|
+
async writeModelEntry({ verbatim, workerId, loopId, turnId, sequence, folded, modelCallId, admission = "accepted", reasoningItems }) {
|
|
1208
|
+
return this.#writeActionlessEntry({
|
|
1209
|
+
verbatim, workerId, loopId, turnId, sequence,
|
|
1210
|
+
origin: "model", kind: "model_emission", folded, modelCallId,
|
|
1211
|
+
attrs: {
|
|
1212
|
+
...(admission === "rejected" ? { admission } : {}),
|
|
1213
|
+
...(reasoningItems !== undefined && reasoningItems.length > 0 ? { reasoning: reasoningItems } : {}),
|
|
1214
|
+
},
|
|
1215
|
+
});
|
|
1216
|
+
}
|
|
1131
1217
|
// PLAN — the model's intended-goals op. An ordinary op: dispatched like any
|
|
1132
1218
|
// other, logged, and broadcast to the client as a log entry — but a pure no-op for
|
|
1133
1219
|
// state (PLAN ∉ MUTATING_OPS); its body serializes into the log row's tx, no effect.
|
|
@@ -1136,6 +1222,20 @@ class Dispatcher {
|
|
|
1136
1222
|
throw new Error("unreachable");
|
|
1137
1223
|
return { status: 200 };
|
|
1138
1224
|
}
|
|
1225
|
+
// {§bare-inference} Provider work is prepared concurrently by Engine; the
|
|
1226
|
+
// dispatcher owns only the ordinary operation-result commit and notification.
|
|
1227
|
+
async recordBareResult(context, result, modelCallId) {
|
|
1228
|
+
assertClassifyingSignal(context.statement);
|
|
1229
|
+
Results.assert(result);
|
|
1230
|
+
const logEntryId = await this.#writeLog({
|
|
1231
|
+
...context,
|
|
1232
|
+
result,
|
|
1233
|
+
curationPlan: null,
|
|
1234
|
+
modelCallId,
|
|
1235
|
+
});
|
|
1236
|
+
context.onDispatch?.(logEntryId);
|
|
1237
|
+
return result;
|
|
1238
|
+
}
|
|
1139
1239
|
static #isDispatchResult(value) {
|
|
1140
1240
|
return "status" in value;
|
|
1141
1241
|
}
|
|
@@ -1201,8 +1301,81 @@ class Dispatcher {
|
|
|
1201
1301
|
manifest,
|
|
1202
1302
|
};
|
|
1203
1303
|
}
|
|
1204
|
-
|
|
1205
|
-
|
|
1304
|
+
#resolveResourceLineMarker(selection, content, operation) {
|
|
1305
|
+
if (!LineAnchors.hasAnchor(selection.lineMarker)) {
|
|
1306
|
+
return {
|
|
1307
|
+
selection: {
|
|
1308
|
+
...selection,
|
|
1309
|
+
lineMarker: selection.lineMarker,
|
|
1310
|
+
},
|
|
1311
|
+
precondition: null,
|
|
1312
|
+
};
|
|
1313
|
+
}
|
|
1314
|
+
const target = this.#resourceAddress(selection);
|
|
1315
|
+
if (selection.manifest.textEditScopes !== true || !selection.manifest.writableBy.includes("model")) {
|
|
1316
|
+
return {
|
|
1317
|
+
result: _a.#failure("line-anchor-unsupported", 400, `The representation at ${target} does not publish line anchors.`, {}, {
|
|
1318
|
+
operation,
|
|
1319
|
+
target,
|
|
1320
|
+
recovery: "Use numeric text coordinates.",
|
|
1321
|
+
retryable: false,
|
|
1322
|
+
}),
|
|
1323
|
+
};
|
|
1324
|
+
}
|
|
1325
|
+
const resolution = LineAnchors.resolve(LineAnchors.tokens(target, content), selection.lineMarker);
|
|
1326
|
+
if (!resolution.ok) {
|
|
1327
|
+
if (resolution.failure.kind === "invalid") {
|
|
1328
|
+
return {
|
|
1329
|
+
result: _a.#failure("line-anchor-invalid", 400, "A line anchor appeared outside a line-coordinate position.", {}, {
|
|
1330
|
+
operation,
|
|
1331
|
+
target,
|
|
1332
|
+
recovery: "Use anchors only for L, SL, or EL; columns remain numeric.",
|
|
1333
|
+
retryable: false,
|
|
1334
|
+
}),
|
|
1335
|
+
};
|
|
1336
|
+
}
|
|
1337
|
+
return {
|
|
1338
|
+
result: _a.#failure("line-anchor-collision", 409, `${operation} coordinates collided with current content at ${target}.`, {}, {
|
|
1339
|
+
operation,
|
|
1340
|
+
target,
|
|
1341
|
+
recovery: `READ ${target} again and retry against its current coordinates.`,
|
|
1342
|
+
retryable: true,
|
|
1343
|
+
}),
|
|
1344
|
+
};
|
|
1345
|
+
}
|
|
1346
|
+
return {
|
|
1347
|
+
selection: { ...selection, lineMarker: resolution.marker },
|
|
1348
|
+
precondition: {
|
|
1349
|
+
identity: target,
|
|
1350
|
+
checks: LineAnchors.checks(selection.lineMarker, resolution.marker),
|
|
1351
|
+
},
|
|
1352
|
+
};
|
|
1353
|
+
}
|
|
1354
|
+
async #selectSource(selection, ctx, operation) {
|
|
1355
|
+
const handler = this.#schemes.get(selection.scheme);
|
|
1356
|
+
if (handler === undefined) {
|
|
1357
|
+
throw new InvalidOperationResultError(`Resolved COPY/MOVE source scheme '${selection.scheme}' is no longer registered.`);
|
|
1358
|
+
}
|
|
1359
|
+
const addressedScheme = selection.target.kind === "url"
|
|
1360
|
+
? selection.target.scheme
|
|
1361
|
+
: selection.scheme;
|
|
1362
|
+
const schemeCtx = new SchemeCtxImpl(ctx, addressedScheme, selection.manifest, this.#liveSubscriptions, { publishedChannel: selection.channel });
|
|
1363
|
+
const prepared = await this.#prepareDataRepresentation({
|
|
1364
|
+
target: selection.target,
|
|
1365
|
+
routedScheme: selection.scheme,
|
|
1366
|
+
handler,
|
|
1367
|
+
manifest: selection.manifest,
|
|
1368
|
+
ctx,
|
|
1369
|
+
schemeCtx,
|
|
1370
|
+
publishedChannel: selection.channel,
|
|
1371
|
+
});
|
|
1372
|
+
if (prepared.result !== null)
|
|
1373
|
+
return prepared.result;
|
|
1374
|
+
if (prepared.address === null) {
|
|
1375
|
+
return _a.#failure("entry-not-found", 404, `No entry exists at ${this.#resourceAddress(selection)}.`, {}, { target: this.#resourceAddress(selection) });
|
|
1376
|
+
}
|
|
1377
|
+
const storageAddress = prepared.address;
|
|
1378
|
+
const read = await EntryCrud.readEntry(storageAddress.pathname, ctx, storageAddress.scheme, storageAddress.ownerId);
|
|
1206
1379
|
if (read.status >= 400)
|
|
1207
1380
|
return read;
|
|
1208
1381
|
if (read.status !== 200 || read.entry === null) {
|
|
@@ -1210,13 +1383,16 @@ class Dispatcher {
|
|
|
1210
1383
|
}
|
|
1211
1384
|
const selected = read.entry.channels[selection.channel];
|
|
1212
1385
|
if (selected === undefined) {
|
|
1213
|
-
return _a.#failure("channel-not-found", 404, `No channel named #${selection.channel} exists at ${renderAddress(
|
|
1214
|
-
target: renderAddress(
|
|
1386
|
+
return _a.#failure("channel-not-found", 404, `No channel named #${selection.channel} exists at ${renderAddress(storageAddress.scheme, storageAddress.pathname)}.`, {}, {
|
|
1387
|
+
target: renderAddress(storageAddress.scheme, storageAddress.pathname),
|
|
1215
1388
|
requestedChannel: selection.channel,
|
|
1216
1389
|
availableChannels: Object.keys(read.entry.channels),
|
|
1217
1390
|
retryable: false,
|
|
1218
1391
|
});
|
|
1219
1392
|
}
|
|
1393
|
+
const resolvedMarker = this.#resolveResourceLineMarker(selection, selected.content, operation);
|
|
1394
|
+
if ("result" in resolvedMarker)
|
|
1395
|
+
return resolvedMarker.result;
|
|
1220
1396
|
let content = selected.content;
|
|
1221
1397
|
let scopeNormalizations;
|
|
1222
1398
|
if (await MimetypeBinary.isBinaryMimetype(selected.mimetype, ctx.mimetypes)) {
|
|
@@ -1227,17 +1403,23 @@ class Dispatcher {
|
|
|
1227
1403
|
retryable: false,
|
|
1228
1404
|
});
|
|
1229
1405
|
}
|
|
1230
|
-
if (selection.lineMarker !== null) {
|
|
1231
|
-
const sliced = LineMarkerOps.sliceLinesRaw(content, selection.lineMarker);
|
|
1406
|
+
if (resolvedMarker.selection.lineMarker !== null) {
|
|
1407
|
+
const sliced = LineMarkerOps.sliceLinesRaw(content, resolvedMarker.selection.lineMarker);
|
|
1232
1408
|
if (sliced.status !== 200)
|
|
1233
1409
|
return Results.assert(sliced);
|
|
1234
1410
|
content = sliced.text ?? "";
|
|
1235
1411
|
scopeNormalizations = sliced.scopeNormalizations;
|
|
1236
1412
|
}
|
|
1413
|
+
if (selected.producerResult !== undefined && selected.producerResult.status >= 400) {
|
|
1414
|
+
return Results.assert(selected.producerResult);
|
|
1415
|
+
}
|
|
1237
1416
|
return {
|
|
1238
|
-
...selection,
|
|
1417
|
+
...resolvedMarker.selection,
|
|
1418
|
+
storageAddress,
|
|
1239
1419
|
content,
|
|
1420
|
+
completeContent: selected.content,
|
|
1240
1421
|
mimetype: selected.mimetype,
|
|
1422
|
+
lineAnchorPrecondition: resolvedMarker.precondition,
|
|
1241
1423
|
...(scopeNormalizations === undefined ? {} : { scopeNormalizations }),
|
|
1242
1424
|
};
|
|
1243
1425
|
}
|
|
@@ -1252,6 +1434,18 @@ class Dispatcher {
|
|
|
1252
1434
|
],
|
|
1253
1435
|
});
|
|
1254
1436
|
}
|
|
1437
|
+
static #mergeLineAnchorPreconditions(...values) {
|
|
1438
|
+
const present = values.filter((value) => value !== null);
|
|
1439
|
+
if (present.length === 0)
|
|
1440
|
+
return null;
|
|
1441
|
+
const identity = present[0].identity;
|
|
1442
|
+
if (present.some((value) => value.identity !== identity)) {
|
|
1443
|
+
throw new TypeError("Line-anchor preconditions for one edit batch must share a resource identity.");
|
|
1444
|
+
}
|
|
1445
|
+
const checks = [...new Map(present.flatMap(({ checks: valueChecks }) => valueChecks)
|
|
1446
|
+
.map((check) => [`${check.anchor}:${check.line}`, check])).values()];
|
|
1447
|
+
return { identity, checks };
|
|
1448
|
+
}
|
|
1255
1449
|
#resourceAddress(selection) {
|
|
1256
1450
|
const address = renderTarget({
|
|
1257
1451
|
scheme: selection.scheme === "file" ? null : selection.scheme,
|
|
@@ -1471,7 +1665,7 @@ class Dispatcher {
|
|
|
1471
1665
|
},
|
|
1472
1666
|
};
|
|
1473
1667
|
}
|
|
1474
|
-
async #invokeEditBatch(selection, edits, ctx) {
|
|
1668
|
+
async #invokeEditBatch(selection, edits, ctx, precondition = null) {
|
|
1475
1669
|
const handler = this.#schemes.get(selection.scheme);
|
|
1476
1670
|
if (typeof handler?.editBatch !== "function") {
|
|
1477
1671
|
return _a.#failure("operation-not-implemented", 501, `Scheme '${selection.scheme}' does not implement EDIT batches.`, {}, {
|
|
@@ -1480,10 +1674,10 @@ class Dispatcher {
|
|
|
1480
1674
|
retryable: false,
|
|
1481
1675
|
});
|
|
1482
1676
|
}
|
|
1483
|
-
const statements = edits.map(({ marker, body,
|
|
1677
|
+
const statements = edits.map(({ marker, body, position }) => ({
|
|
1484
1678
|
op: "EDIT",
|
|
1485
1679
|
suffix: "",
|
|
1486
|
-
signal:
|
|
1680
|
+
signal: null,
|
|
1487
1681
|
target: selection.target,
|
|
1488
1682
|
lineMarker: marker,
|
|
1489
1683
|
body,
|
|
@@ -1493,7 +1687,10 @@ class Dispatcher {
|
|
|
1493
1687
|
? selection.target.scheme
|
|
1494
1688
|
: selection.scheme;
|
|
1495
1689
|
try {
|
|
1496
|
-
const result = Results.assert(await handler.editBatch(statements, new SchemeCtxImpl(ctx, addressedScheme, selection.manifest, this.#liveSubscriptions
|
|
1690
|
+
const result = Results.assert(await handler.editBatch(statements, new SchemeCtxImpl(ctx, addressedScheme, selection.manifest, this.#liveSubscriptions, {
|
|
1691
|
+
publishedChannel: selection.channel,
|
|
1692
|
+
editPrecondition: precondition,
|
|
1693
|
+
})));
|
|
1497
1694
|
return this.#withProposalRoute(result, selection);
|
|
1498
1695
|
}
|
|
1499
1696
|
catch (err) {
|
|
@@ -1529,7 +1726,6 @@ class Dispatcher {
|
|
|
1529
1726
|
retryable: false,
|
|
1530
1727
|
});
|
|
1531
1728
|
}
|
|
1532
|
-
const tags = Array.isArray(statement.signal) ? statement.signal : [];
|
|
1533
1729
|
const destinationEffect = this.#pendingEffect(destination, destinationChannel === undefined ? "create" : "update");
|
|
1534
1730
|
if (destination.lineMarker !== null) {
|
|
1535
1731
|
if (destinationChannel === undefined) {
|
|
@@ -1545,13 +1741,15 @@ class Dispatcher {
|
|
|
1545
1741
|
retryable: false,
|
|
1546
1742
|
});
|
|
1547
1743
|
}
|
|
1548
|
-
const
|
|
1549
|
-
|
|
1744
|
+
const resolvedMarker = this.#resolveResourceLineMarker(destination, destinationChannel.content, statement.op);
|
|
1745
|
+
if ("result" in resolvedMarker)
|
|
1746
|
+
return resolvedMarker.result;
|
|
1747
|
+
const edited = await this.#invokeEditBatch(resolvedMarker.selection, [{
|
|
1748
|
+
marker: resolvedMarker.selection.lineMarker,
|
|
1550
1749
|
body: source.content,
|
|
1551
|
-
tags,
|
|
1552
1750
|
position: statement.position,
|
|
1553
|
-
}], ctx);
|
|
1554
|
-
return this.#finalizeEffects(edited,
|
|
1751
|
+
}], ctx, resolvedMarker.precondition);
|
|
1752
|
+
return this.#finalizeEffects(edited, resolvedMarker.selection, [destinationEffect]);
|
|
1555
1753
|
}
|
|
1556
1754
|
if (destinationChannel !== undefined
|
|
1557
1755
|
&& destinationChannel.content !== source.content) {
|
|
@@ -1560,10 +1758,7 @@ class Dispatcher {
|
|
|
1560
1758
|
retryable: false,
|
|
1561
1759
|
});
|
|
1562
1760
|
}
|
|
1563
|
-
|
|
1564
|
-
const mergedTags = [...new Set([...priorTags, ...tags])];
|
|
1565
|
-
const addsTags = mergedTags.length !== priorTags.length;
|
|
1566
|
-
if (destinationChannel !== undefined && !addsTags)
|
|
1761
|
+
if (destinationChannel !== undefined)
|
|
1567
1762
|
return { status: 304 };
|
|
1568
1763
|
const channels = {
|
|
1569
1764
|
...(existing?.channels ?? {}),
|
|
@@ -1572,15 +1767,18 @@ class Dispatcher {
|
|
|
1572
1767
|
mimetype: source.mimetype,
|
|
1573
1768
|
},
|
|
1574
1769
|
};
|
|
1575
|
-
const written = await this.#writeEntry(destination.scheme, destination.pathname, { channels
|
|
1770
|
+
const written = await this.#writeEntry(destination.scheme, destination.pathname, { channels }, ctx);
|
|
1576
1771
|
const exactWritten = Results.assert(written);
|
|
1772
|
+
const parseIssues = exactWritten.status === 200 || exactWritten.status === 201
|
|
1773
|
+
? await new DbProjectionCaps(ctx).parseIssues(source.content, source.mimetype)
|
|
1774
|
+
: undefined;
|
|
1577
1775
|
const materialized = source.lineMarker === null
|
|
1578
1776
|
|| (exactWritten.status !== 200 && exactWritten.status !== 201 && exactWritten.status !== 202)
|
|
1579
1777
|
? exactWritten
|
|
1580
|
-
: _a.#withEditMaterialization(exactWritten, editReceipt(
|
|
1778
|
+
: _a.#withEditMaterialization(exactWritten, editReceipt("", source.content, [{
|
|
1581
1779
|
marker: { marks: [1, -1] },
|
|
1582
1780
|
body: source.content,
|
|
1583
|
-
}]));
|
|
1781
|
+
}], parseIssues));
|
|
1584
1782
|
return this.#finalizeEffects(materialized, destination, [destinationEffect]);
|
|
1585
1783
|
}
|
|
1586
1784
|
async #copyOrchestration({ statement, source, destination, ctx, }) {
|
|
@@ -1590,13 +1788,13 @@ class Dispatcher {
|
|
|
1590
1788
|
const resolvedDestination = await this.#resolveResourceSelection(destination, ctx);
|
|
1591
1789
|
if (_a.#isDispatchResult(resolvedDestination))
|
|
1592
1790
|
return resolvedDestination;
|
|
1593
|
-
const selected = await this.#selectSource(resolvedSource, ctx);
|
|
1791
|
+
const selected = await this.#selectSource(resolvedSource, ctx, "COPY");
|
|
1594
1792
|
if (_a.#isDispatchResult(selected))
|
|
1595
1793
|
return selected;
|
|
1596
1794
|
const result = await this.#writeDestination(statement, selected, resolvedDestination, ctx);
|
|
1597
1795
|
return _a.#prependScopeNormalizations(result, selected.scopeNormalizations);
|
|
1598
1796
|
}
|
|
1599
|
-
#deferredMoveSource(source, destination) {
|
|
1797
|
+
#deferredMoveSource(source, destination, lineAnchorPrecondition) {
|
|
1600
1798
|
return {
|
|
1601
1799
|
target: source.target,
|
|
1602
1800
|
lineMarker: source.lineMarker,
|
|
@@ -1604,6 +1802,7 @@ class Dispatcher {
|
|
|
1604
1802
|
pathname: source.pathname,
|
|
1605
1803
|
channel: source.channel,
|
|
1606
1804
|
destination: this.#resourceAddress(destination),
|
|
1805
|
+
lineAnchorPrecondition,
|
|
1607
1806
|
};
|
|
1608
1807
|
}
|
|
1609
1808
|
#moveFailureAfterDestination(result, destination, destinationEffects) {
|
|
@@ -1625,7 +1824,7 @@ class Dispatcher {
|
|
|
1625
1824
|
});
|
|
1626
1825
|
return _a.#withCombinedEffects(failed, destinationEffects);
|
|
1627
1826
|
}
|
|
1628
|
-
async #removeMoveSource(statement, source, ctx) {
|
|
1827
|
+
async #removeMoveSource(statement, source, ctx, lineAnchorPrecondition = null) {
|
|
1629
1828
|
const effect = this.#pendingEffect(source, source.lineMarker === null ? "delete" : "update");
|
|
1630
1829
|
if (source.lineMarker === null) {
|
|
1631
1830
|
const deleted = await this.#deleteChannel(source.scheme, source.pathname, source.channel, ctx);
|
|
@@ -1634,9 +1833,8 @@ class Dispatcher {
|
|
|
1634
1833
|
const edited = await this.#invokeEditBatch(source, [{
|
|
1635
1834
|
marker: source.lineMarker,
|
|
1636
1835
|
body: "",
|
|
1637
|
-
tags: null,
|
|
1638
1836
|
position: statement.position,
|
|
1639
|
-
}], ctx);
|
|
1837
|
+
}], ctx, lineAnchorPrecondition);
|
|
1640
1838
|
return this.#finalizeEffects(edited, source, [effect]);
|
|
1641
1839
|
}
|
|
1642
1840
|
async #moveWithinChannel(statement, source, destination, ctx) {
|
|
@@ -1653,22 +1851,24 @@ class Dispatcher {
|
|
|
1653
1851
|
if (destination.lineMarker === null) {
|
|
1654
1852
|
return this.#writeDestination(statement, source, destination, ctx);
|
|
1655
1853
|
}
|
|
1656
|
-
const
|
|
1854
|
+
const resolvedDestination = this.#resolveResourceLineMarker(destination, source.completeContent, "MOVE");
|
|
1855
|
+
if ("result" in resolvedDestination)
|
|
1856
|
+
return resolvedDestination.result;
|
|
1857
|
+
const precondition = _a.#mergeLineAnchorPreconditions(source.lineAnchorPrecondition, resolvedDestination.precondition);
|
|
1858
|
+
const moved = await this.#invokeEditBatch(resolvedDestination.selection, [
|
|
1657
1859
|
{
|
|
1658
|
-
marker:
|
|
1860
|
+
marker: resolvedDestination.selection.lineMarker,
|
|
1659
1861
|
body: source.content,
|
|
1660
|
-
tags: Array.isArray(statement.signal) ? statement.signal : [],
|
|
1661
1862
|
position: statement.position,
|
|
1662
1863
|
},
|
|
1663
1864
|
{
|
|
1664
1865
|
marker: source.lineMarker,
|
|
1665
1866
|
body: "",
|
|
1666
|
-
tags: null,
|
|
1667
1867
|
position: statement.position,
|
|
1668
1868
|
},
|
|
1669
|
-
], ctx);
|
|
1670
|
-
const effect = this.#pendingEffect(
|
|
1671
|
-
return this.#finalizeEffects(moved,
|
|
1869
|
+
], ctx, precondition);
|
|
1870
|
+
const effect = this.#pendingEffect(resolvedDestination.selection, "update");
|
|
1871
|
+
return this.#finalizeEffects(moved, resolvedDestination.selection, [effect, effect]);
|
|
1672
1872
|
}
|
|
1673
1873
|
async #moveOrchestration({ statement, source, destination, ctx, }) {
|
|
1674
1874
|
const resolvedSource = await this.#resolveResourceSelection(source, ctx);
|
|
@@ -1677,7 +1877,7 @@ class Dispatcher {
|
|
|
1677
1877
|
const resolvedDestination = await this.#resolveResourceSelection(destination, ctx);
|
|
1678
1878
|
if (_a.#isDispatchResult(resolvedDestination))
|
|
1679
1879
|
return resolvedDestination;
|
|
1680
|
-
const selected = await this.#selectSource(resolvedSource, ctx);
|
|
1880
|
+
const selected = await this.#selectSource(resolvedSource, ctx, "MOVE");
|
|
1681
1881
|
if (_a.#isDispatchResult(selected))
|
|
1682
1882
|
return selected;
|
|
1683
1883
|
if (this.#sameChannel(resolvedSource, resolvedDestination)) {
|
|
@@ -1696,11 +1896,11 @@ class Dispatcher {
|
|
|
1696
1896
|
...destinationResult,
|
|
1697
1897
|
attrs: {
|
|
1698
1898
|
...destinationResult.attrs,
|
|
1699
|
-
moveSource: this.#deferredMoveSource(
|
|
1899
|
+
moveSource: this.#deferredMoveSource(selected, resolvedDestination, selected.lineAnchorPrecondition),
|
|
1700
1900
|
},
|
|
1701
1901
|
};
|
|
1702
1902
|
}
|
|
1703
|
-
const sourceResult = await this.#removeMoveSource(statement,
|
|
1903
|
+
const sourceResult = await this.#removeMoveSource(statement, selected, ctx, selected.lineAnchorPrecondition);
|
|
1704
1904
|
if (sourceResult.status === 202) {
|
|
1705
1905
|
return {
|
|
1706
1906
|
...sourceResult,
|
|
@@ -1796,7 +1996,10 @@ class Dispatcher {
|
|
|
1796
1996
|
|| resolvedSource.channel !== deferred.channel) {
|
|
1797
1997
|
throw new InvalidOperationResultError("A deferred MOVE source no longer resolves to its recorded identity.");
|
|
1798
1998
|
}
|
|
1799
|
-
const removed = await this.#removeMoveSource(statement,
|
|
1999
|
+
const removed = await this.#removeMoveSource(statement, {
|
|
2000
|
+
...resolvedSource,
|
|
2001
|
+
lineMarker: resolvedSource.lineMarker,
|
|
2002
|
+
}, ctx, deferred.lineAnchorPrecondition);
|
|
1800
2003
|
if (removed.status >= 400) {
|
|
1801
2004
|
return {
|
|
1802
2005
|
resolution: settlement.resolution,
|
|
@@ -1834,7 +2037,7 @@ class Dispatcher {
|
|
|
1834
2037
|
// {§send-premature-terminate} — the unified PENDING SET, judged at the terminal's OWN dispatch
|
|
1835
2038
|
// (post-batch: the emission's earlier ops already executed, so a same-turn KILL+[200] repairs in
|
|
1836
2039
|
// ONE turn, and a same-turn WORK+[200] is caught — the spawn is live by the time the SEND lands).
|
|
1837
|
-
// pending = open streams ∪ live children ∪ THIS turn's retrievals (READ/FIND/OPEN, results unseen
|
|
2040
|
+
// pending = open streams ∪ live children ∪ THIS turn's retrievals (READ/FIND/OPEN/BARE, results unseen
|
|
1838
2041
|
// until next packet). Failed operations are the separate next-packet leg shared by explicit
|
|
1839
2042
|
// completion and empty-join completion. Nothing pending may be silently discarded; 499 discards
|
|
1840
2043
|
// BY STATED INTENT and is never gated.
|
|
@@ -1847,30 +2050,32 @@ class Dispatcher {
|
|
|
1847
2050
|
const liveChild = await this.#db.engine_worker_has_live_child.get({ worker_id: workerId });
|
|
1848
2051
|
if (liveChild !== undefined)
|
|
1849
2052
|
pending.push("surviving workers");
|
|
1850
|
-
const
|
|
1851
|
-
if (
|
|
2053
|
+
const boundaries = await this.#nextPacketBoundaries(workerId, turnId);
|
|
2054
|
+
if (boundaries.retrievals)
|
|
1852
2055
|
pending.push("this turn's retrieval results (they land in the NEXT packet's Log)");
|
|
1853
|
-
if (
|
|
2056
|
+
if (boundaries.streamTerminations) {
|
|
1854
2057
|
pending.push("completed stream results that land in the NEXT packet's Log");
|
|
1855
2058
|
}
|
|
1856
|
-
if (
|
|
2059
|
+
if (boundaries.childTerminations)
|
|
1857
2060
|
pending.push("worker results that arrived during this turn (they land NEXT turn)");
|
|
1858
2061
|
return pending;
|
|
1859
2062
|
}
|
|
1860
|
-
// Results cross an observation boundary only when they have appeared in a
|
|
1861
|
-
//
|
|
1862
|
-
//
|
|
1863
|
-
//
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
2063
|
+
// Results cross an observation boundary only when they have appeared in a packet;
|
|
2064
|
+
// successful FOLD effects likewise become useful through the curated next packet.
|
|
2065
|
+
// Keep every next-packet boundary in one classifier while letting the callers apply
|
|
2066
|
+
// their distinct contracts: retrievals block explicit completion, whereas FOLD only
|
|
2067
|
+
// prevents an empty wait from being inferred as completion.
|
|
2068
|
+
async #nextPacketBoundaries(workerId, turnId) {
|
|
2069
|
+
const [turnBoundaries, streamTermination, childTermination] = await Promise.all([
|
|
2070
|
+
this.#db.engine_turn_packet_boundaries.all({ turn_id: turnId }),
|
|
1867
2071
|
this.#db.engine_worker_has_undelivered_stream_term
|
|
1868
2072
|
.get({ worker_id: workerId }),
|
|
1869
2073
|
this.#db.engine_worker_has_undelivered_child_term
|
|
1870
2074
|
.get({ worker_id: workerId }),
|
|
1871
2075
|
]);
|
|
1872
2076
|
return {
|
|
1873
|
-
retrievals:
|
|
2077
|
+
retrievals: turnBoundaries.some(({ op }) => op !== "FOLD"),
|
|
2078
|
+
folds: turnBoundaries.some(({ op }) => op === "FOLD"),
|
|
1874
2079
|
streamTerminations: streamTermination !== undefined,
|
|
1875
2080
|
childTerminations: childTermination !== undefined,
|
|
1876
2081
|
};
|
|
@@ -1911,15 +2116,15 @@ class Dispatcher {
|
|
|
1911
2116
|
if (status === null) {
|
|
1912
2117
|
return _a.#failure("send-status-required", 400, "SEND requires a numeric status.", {}, { retryable: false });
|
|
1913
2118
|
}
|
|
1914
|
-
const raw = statement.body === null ? "" :
|
|
1915
|
-
// The park rides SEND
|
|
2119
|
+
const raw = statement.body === null ? "" : statement.body.raw;
|
|
2120
|
+
// The park rides SEND signal 202 only ({§park-202-only}). A scoped signal 102 is neither
|
|
1916
2121
|
// a wait nor a meaningful continuation, so reject it instead of preserving the
|
|
1917
2122
|
// retired dual spelling.
|
|
1918
2123
|
if (status === 102 && statement.lineMarker !== null) {
|
|
1919
|
-
return _a.#failure("send-scope-invalid", 400, "
|
|
2124
|
+
return _a.#failure("send-scope-invalid", 400, "`## SEND0 [102]` does not accept a scope.", {}, {
|
|
1920
2125
|
requestedStatus: 102,
|
|
1921
2126
|
scope: statement.lineMarker,
|
|
1922
|
-
recovery: "Use
|
|
2127
|
+
recovery: "Use `## SEND0 [202] <scope>` to wait, or remove the scope to continue.",
|
|
1923
2128
|
retryable: false,
|
|
1924
2129
|
});
|
|
1925
2130
|
}
|
|
@@ -1927,13 +2132,13 @@ class Dispatcher {
|
|
|
1927
2132
|
// indefinite park. {§join-blocking-collect}
|
|
1928
2133
|
const joinArmed = this.#joinTargets.delete(loopId);
|
|
1929
2134
|
if (status === 102 && statement.lineMarker === null && joinArmed) {
|
|
1930
|
-
if (!await this.#lifecycle.park(loopId
|
|
2135
|
+
if (!await this.#lifecycle.park(loopId)) {
|
|
1931
2136
|
return _a.#statusResult(await this.#lifecycle.status(loopId), "loop-already-terminal", "The loop was already terminal when SEND attempted to park it.");
|
|
1932
2137
|
}
|
|
1933
2138
|
this.#parkDeadlines.set(loopId, -1); // indefinite: the bounded child's terminal is the wake edge
|
|
1934
2139
|
return { status: 102, attrs: { parked: -1, join: true } };
|
|
1935
2140
|
}
|
|
1936
|
-
// {§wait-obligation-matrix} — SEND
|
|
2141
|
+
// {§wait-obligation-matrix} — SEND signal 202 is the obligation-checked join. A live
|
|
1937
2142
|
// obligation (a spawned child or open stream, J) BLOCKS the loop until it concludes and
|
|
1938
2143
|
// reawakens it ({§worker-lifecycle-child-wake}); a wait on nothing (∅) is already satisfied and
|
|
1939
2144
|
// resolves like 200, so <-1>+∅ self-resolves rather than hang the agent; a pending own
|
|
@@ -1942,7 +2147,7 @@ class Dispatcher {
|
|
|
1942
2147
|
const marks = statement.lineMarker?.marks[0];
|
|
1943
2148
|
const seconds = typeof marks === "number" ? marks : -1; // bare 202 / absent T = indefinite, bounded by the join
|
|
1944
2149
|
if (await this.#hasLiveWork(workerId)) {
|
|
1945
|
-
if (!await this.#lifecycle.park(loopId
|
|
2150
|
+
if (!await this.#lifecycle.park(loopId)) {
|
|
1946
2151
|
return _a.#statusResult(await this.#lifecycle.status(loopId), "loop-already-terminal", "The loop was already terminal when SEND attempted to wait.");
|
|
1947
2152
|
}
|
|
1948
2153
|
this.#parkDeadlines.set(loopId, seconds);
|
|
@@ -1952,8 +2157,8 @@ class Dispatcher {
|
|
|
1952
2157
|
// all complete-but-unobserved. Their wake edge may already have
|
|
1953
2158
|
// fired, so do not park; continue directly to the packet that
|
|
1954
2159
|
// materializes them.
|
|
1955
|
-
const
|
|
1956
|
-
if (
|
|
2160
|
+
const boundaries = await this.#nextPacketBoundaries(workerId, turnId);
|
|
2161
|
+
if (boundaries.retrievals || boundaries.folds || boundaries.streamTerminations || boundaries.childTerminations) {
|
|
1957
2162
|
return { status: 102 };
|
|
1958
2163
|
}
|
|
1959
2164
|
const failCount = await this.#unobservedFailureCount(turnId);
|
|
@@ -1964,7 +2169,7 @@ class Dispatcher {
|
|
|
1964
2169
|
return branchDenial;
|
|
1965
2170
|
// The joined set is already drained. Awaiting an empty task group completes
|
|
1966
2171
|
// immediately; it never parks and needs no corrective model turn.
|
|
1967
|
-
const finished = await this.#lifecycle.finish(loopId,
|
|
2172
|
+
const finished = await this.#lifecycle.finish(loopId, TerminalResult.success(raw));
|
|
1968
2173
|
return {
|
|
1969
2174
|
status: finished !== null ? 200 : await this.#lifecycle.status(loopId),
|
|
1970
2175
|
attrs: { joined: true, pending: 0 },
|
|
@@ -2008,7 +2213,7 @@ class Dispatcher {
|
|
|
2008
2213
|
return _a.#failure("retrieval-results-unobserved", 409, "Last turn both performed retrieval operations and attempted to terminate. Retrieval operations force an additional turn so their results can be reviewed.", {}, {
|
|
2009
2214
|
pending: [...pending],
|
|
2010
2215
|
stage: "completion",
|
|
2011
|
-
recovery: "Review the results, then use only
|
|
2216
|
+
recovery: "Review the results, then use only `# PLAN0` and `## SEND0 [200]` to conclude.",
|
|
2012
2217
|
retryable: false,
|
|
2013
2218
|
});
|
|
2014
2219
|
}
|
|
@@ -2022,7 +2227,7 @@ class Dispatcher {
|
|
|
2022
2227
|
const branchDenial = await this.#branchCompletionGate?.(workerId) ?? null;
|
|
2023
2228
|
if (branchDenial !== null)
|
|
2024
2229
|
return branchDenial;
|
|
2025
|
-
const finished = await this.#lifecycle.finish(loopId,
|
|
2230
|
+
const finished = await this.#lifecycle.finish(loopId, TerminalResult.success(raw));
|
|
2026
2231
|
return _a.#statusResult(finished !== null ? 200 : await this.#lifecycle.status(loopId), "loop-already-terminal", "The loop was already terminal when SEND attempted to conclude it.");
|
|
2027
2232
|
}
|
|
2028
2233
|
if (status === 499) {
|
|
@@ -2035,7 +2240,7 @@ class Dispatcher {
|
|
|
2035
2240
|
turn_id: turnId,
|
|
2036
2241
|
});
|
|
2037
2242
|
if (seqs === undefined) {
|
|
2038
|
-
throw new Error(`SEND
|
|
2243
|
+
throw new Error(`SEND signal 499: no coordinate for loop=${loopId} turn=${turnId}`);
|
|
2039
2244
|
}
|
|
2040
2245
|
Results.attachInstance(failure, `log:///${seqs.loop_seq}/${seqs.turn_seq}/${ctx.sequence}/SEND`);
|
|
2041
2246
|
const finished = await this.#lifecycle.finish(loopId, failure);
|
|
@@ -2050,7 +2255,20 @@ class Dispatcher {
|
|
|
2050
2255
|
}
|
|
2051
2256
|
async #run(schemeName, statement, ctx) {
|
|
2052
2257
|
if (schemeName === null) {
|
|
2053
|
-
|
|
2258
|
+
const fields = statement.op === "READ"
|
|
2259
|
+
? { content: null, mimetype: null, channel: null }
|
|
2260
|
+
: statement.op === "FIND"
|
|
2261
|
+
? {
|
|
2262
|
+
content: null,
|
|
2263
|
+
mimetype: null,
|
|
2264
|
+
results: [],
|
|
2265
|
+
itemsTokenTotal: 0,
|
|
2266
|
+
returnedItemsTokenTotal: 0,
|
|
2267
|
+
matchingPathCount: 0,
|
|
2268
|
+
matchLocationCount: 0,
|
|
2269
|
+
}
|
|
2270
|
+
: {};
|
|
2271
|
+
return _a.#failure("target-scheme-required", 400, `${statement.op} requires a target scheme.`, fields, { operation: statement.op, retryable: false });
|
|
2054
2272
|
}
|
|
2055
2273
|
if (statement.op === "SEND" && statement.signal === 499 && statement.target?.kind === "url") {
|
|
2056
2274
|
const addressedScheme = statement.target.scheme;
|
|
@@ -2084,16 +2302,128 @@ class Dispatcher {
|
|
|
2084
2302
|
const manifest = this.#schemes.manifestFor(schemeName);
|
|
2085
2303
|
if (manifest === undefined)
|
|
2086
2304
|
throw new Error(`scheme '${schemeName}' has no manifest`);
|
|
2087
|
-
const
|
|
2088
|
-
|
|
2089
|
-
|
|
2305
|
+
const publishedChannel = statement.target?.kind === "url"
|
|
2306
|
+
? statement.target.fragment ?? manifest.defaultChannel
|
|
2307
|
+
: manifest.defaultChannel;
|
|
2308
|
+
const schemeCtx = new SchemeCtxImpl(ctx, addressedScheme ?? schemeName, manifest, this.#liveSubscriptions, { publishedChannel });
|
|
2309
|
+
if (statement.op === "READ"
|
|
2310
|
+
&& handler instanceof CoreSchemeAdapterBase
|
|
2311
|
+
&& typeof handler.resolveCoreRepresentation === "function") {
|
|
2312
|
+
const selectionNeutralTarget = statement.target?.kind === "url"
|
|
2313
|
+
? {
|
|
2314
|
+
...statement.target,
|
|
2315
|
+
raw: renderTarget({ ...statement.target, fragment: null }) ?? statement.target.raw,
|
|
2316
|
+
fragment: null,
|
|
2317
|
+
}
|
|
2318
|
+
: statement.target;
|
|
2319
|
+
const resolved = await handler
|
|
2320
|
+
.resolveCoreRepresentation(selectionNeutralTarget, schemeCtx);
|
|
2321
|
+
if ("result" in resolved)
|
|
2322
|
+
return Results.assertReadResult(resolved.result);
|
|
2323
|
+
if (selectionNeutralTarget === null) {
|
|
2324
|
+
throw new InvalidOperationResultError(`Core scheme '${schemeName}' resolved a targetless READ representation.`);
|
|
2325
|
+
}
|
|
2326
|
+
const target = renderTarget(selectionNeutralTarget.kind === "url"
|
|
2327
|
+
? selectionNeutralTarget
|
|
2328
|
+
: { scheme: null, pathname: selectionNeutralTarget.raw });
|
|
2329
|
+
if (target === null) {
|
|
2330
|
+
throw new TypeError(`Core scheme '${schemeName}' resolved an unrenderable READ target.`);
|
|
2331
|
+
}
|
|
2332
|
+
return Results.assertReadResult(await ReadProjector.project({
|
|
2333
|
+
statement,
|
|
2334
|
+
manifest,
|
|
2335
|
+
target,
|
|
2336
|
+
identity: target,
|
|
2337
|
+
representation: resolved.representation,
|
|
2338
|
+
mimetypes: ctx.mimetypes,
|
|
2339
|
+
}));
|
|
2340
|
+
}
|
|
2341
|
+
if (statement.op === "READ" && manifest.category === "data") {
|
|
2342
|
+
const prepared = statement.target === null
|
|
2343
|
+
? { address: null, result: null }
|
|
2344
|
+
: await this.#prepareDataRepresentation({
|
|
2345
|
+
target: statement.target,
|
|
2346
|
+
routedScheme: schemeName,
|
|
2347
|
+
handler: handler,
|
|
2348
|
+
manifest,
|
|
2349
|
+
ctx,
|
|
2350
|
+
schemeCtx,
|
|
2351
|
+
publishedChannel,
|
|
2352
|
+
});
|
|
2353
|
+
if (prepared.result !== null)
|
|
2354
|
+
return prepared.result;
|
|
2355
|
+
const resolved = prepared.address;
|
|
2356
|
+
if (statement.target !== null && resolved === null) {
|
|
2357
|
+
const target = renderTarget(statement.target.kind === "url"
|
|
2358
|
+
? statement.target
|
|
2359
|
+
: { scheme: null, pathname: statement.target.raw });
|
|
2360
|
+
return Results.failure(`scheme:${schemeName}`, "entry-not-found", 404, `No entry exists at ${target ?? "the requested address"}.`, { content: null, mimetype: null, channel: null }, { target });
|
|
2361
|
+
}
|
|
2362
|
+
const storageScheme = resolved?.scheme ?? manifest.storedScheme ?? addressedScheme ?? schemeName;
|
|
2363
|
+
const projected = Results.assertReadResult(await EntryOps.readWorkspaceEntry(statement, ctx, { ...manifest, name: storageScheme, storedScheme: storageScheme }, resolved === null
|
|
2364
|
+
? {}
|
|
2365
|
+
: {
|
|
2366
|
+
ownerId: resolved.ownerId,
|
|
2367
|
+
pathname: resolved.pathname,
|
|
2368
|
+
}));
|
|
2369
|
+
return projected;
|
|
2370
|
+
}
|
|
2090
2371
|
if (statement.op !== "FIND" || manifest.category !== "data") {
|
|
2372
|
+
if (typeof method === "function") {
|
|
2373
|
+
return Results.assert(await method.call(handler, statement, schemeCtx));
|
|
2374
|
+
}
|
|
2091
2375
|
return _a.#failure("operation-not-implemented", 501, `Scheme '${schemeName}' does not implement ${statement.op}.`, {}, {
|
|
2092
2376
|
scheme: schemeName,
|
|
2093
2377
|
operation: statement.op,
|
|
2094
2378
|
retryable: false,
|
|
2095
2379
|
});
|
|
2096
2380
|
}
|
|
2381
|
+
const targetPathname = statement.target?.kind === "url"
|
|
2382
|
+
? statement.target.pathname
|
|
2383
|
+
: statement.target?.raw ?? "";
|
|
2384
|
+
const collectionTarget = manifest.folderScopes === true
|
|
2385
|
+
&& (targetPathname === "" || targetPathname.endsWith("/"));
|
|
2386
|
+
const exactTarget = statement.target !== null
|
|
2387
|
+
&& !collectionTarget
|
|
2388
|
+
&& !PathSyntax.hasGlob(entryPathnameOf(statement.target));
|
|
2389
|
+
if (exactTarget && statement.target !== null) {
|
|
2390
|
+
const prepared = await this.#prepareDataRepresentation({
|
|
2391
|
+
target: statement.target,
|
|
2392
|
+
routedScheme: schemeName,
|
|
2393
|
+
handler: handler,
|
|
2394
|
+
manifest,
|
|
2395
|
+
ctx,
|
|
2396
|
+
schemeCtx,
|
|
2397
|
+
publishedChannel,
|
|
2398
|
+
});
|
|
2399
|
+
if (prepared.result !== null)
|
|
2400
|
+
return prepared.result;
|
|
2401
|
+
if (prepared.address === null) {
|
|
2402
|
+
const target = renderTarget(statement.target.kind === "url"
|
|
2403
|
+
? statement.target
|
|
2404
|
+
: { scheme: null, pathname: statement.target.raw });
|
|
2405
|
+
return Results.failure(`scheme:${schemeName}`, "entry-not-found", 404, `No entry exists at ${target ?? "the requested address"}.`, {
|
|
2406
|
+
content: null,
|
|
2407
|
+
mimetype: null,
|
|
2408
|
+
results: [],
|
|
2409
|
+
itemsTokenTotal: 0,
|
|
2410
|
+
returnedItemsTokenTotal: 0,
|
|
2411
|
+
matchingPathCount: 0,
|
|
2412
|
+
matchLocationCount: 0,
|
|
2413
|
+
}, { target });
|
|
2414
|
+
}
|
|
2415
|
+
const storageScheme = prepared.address.scheme
|
|
2416
|
+
?? manifest.storedScheme
|
|
2417
|
+
?? addressedScheme
|
|
2418
|
+
?? schemeName;
|
|
2419
|
+
return Results.assert(await EntryFind.findWorkspaceEntries(statement, ctx, { ...manifest, name: storageScheme, storedScheme: storageScheme }, {
|
|
2420
|
+
ownerId: prepared.address.ownerId,
|
|
2421
|
+
pathname: prepared.address.pathname,
|
|
2422
|
+
}));
|
|
2423
|
+
}
|
|
2424
|
+
if (typeof method === "function") {
|
|
2425
|
+
return Results.assert(await method.call(handler, statement, schemeCtx));
|
|
2426
|
+
}
|
|
2097
2427
|
const prepareFind = handler.prepareFind;
|
|
2098
2428
|
if (typeof prepareFind === "function") {
|
|
2099
2429
|
const prepared = await prepareFind.call(handler, statement, schemeCtx);
|
|
@@ -2105,11 +2435,14 @@ class Dispatcher {
|
|
|
2105
2435
|
async #runLogCuration(statement, ctx) {
|
|
2106
2436
|
const addressedScheme = schemeNameOf(statement.target);
|
|
2107
2437
|
if (addressedScheme !== null && addressedScheme !== "log") {
|
|
2108
|
-
return
|
|
2109
|
-
|
|
2110
|
-
|
|
2111
|
-
|
|
2112
|
-
|
|
2438
|
+
return {
|
|
2439
|
+
result: _a.#failure("operation-not-implemented", 501, `Scheme '${addressedScheme}' does not implement ${statement.op}.`, {}, {
|
|
2440
|
+
scheme: addressedScheme,
|
|
2441
|
+
operation: statement.op,
|
|
2442
|
+
retryable: false,
|
|
2443
|
+
}),
|
|
2444
|
+
plan: null,
|
|
2445
|
+
};
|
|
2113
2446
|
}
|
|
2114
2447
|
const handler = this.#schemes.get("log");
|
|
2115
2448
|
const manifest = this.#schemes.manifestFor("log");
|
|
@@ -2117,21 +2450,20 @@ class Dispatcher {
|
|
|
2117
2450
|
throw new Error("the core log curation owner is not registered");
|
|
2118
2451
|
}
|
|
2119
2452
|
const schemeCtx = new SchemeCtxImpl(ctx, "log", manifest, this.#liveSubscriptions);
|
|
2120
|
-
|
|
2121
|
-
|
|
2122
|
-
: Results.assert(await handler.fold(statement, schemeCtx));
|
|
2453
|
+
const outcome = await handler.curate(statement, schemeCtx);
|
|
2454
|
+
return { result: Results.assert(outcome.result), plan: outcome.plan };
|
|
2123
2455
|
}
|
|
2124
2456
|
// {§proposal}/{§send} — status 202 is a proposal except for broadcast
|
|
2125
|
-
// SEND
|
|
2457
|
+
// SEND signal 202, which parks the loop. The operation disambiguates the status.
|
|
2126
2458
|
static #isProposal(statement, result) {
|
|
2127
|
-
// {§send-300-choices} — SEND
|
|
2459
|
+
// {§send-300-choices} — SEND signal 300 is the proposal exception.
|
|
2128
2460
|
if (result.status !== 202)
|
|
2129
2461
|
return false;
|
|
2130
2462
|
if (statement.op === "SEND" && statement.signal === 300)
|
|
2131
2463
|
return true;
|
|
2132
2464
|
return !(statement.op === "SEND" && statement.target === null);
|
|
2133
2465
|
}
|
|
2134
|
-
async #writeLog({ statement, result, workspaceId, workerId, loopId, turnId, sequence, origin, }) {
|
|
2466
|
+
async #writeLog({ statement, result, workspaceId, workerId, loopId, turnId, sequence, origin, curationPlan, modelCallId, }) {
|
|
2135
2467
|
const durableStatement = DurableStatement.project(statement);
|
|
2136
2468
|
const target = this.#extractTarget(durableStatement.target);
|
|
2137
2469
|
await this.#canonColumns(target, workspaceId); // {§fs-answer-in-canon}
|
|
@@ -2141,12 +2473,23 @@ class Dispatcher {
|
|
|
2141
2473
|
// A proposal (status 202 from a side-effecting op) is written to the log in
|
|
2142
2474
|
// state='proposed' until the proposal lifecycle resolves it; attrs holds the
|
|
2143
2475
|
// scheme-supplied payload (file diff, exec command, etc.) the client renders
|
|
2144
|
-
// for review and the scheme consumes on accept. A broadcast SEND
|
|
2476
|
+
// for review and the scheme consumes on accept. A broadcast SEND signal 202 is a
|
|
2145
2477
|
// parked-terminal, not a proposal (#isProposal) → state='resolved'.
|
|
2146
2478
|
const isProposed = _a.#isProposal(statement, result);
|
|
2147
2479
|
let attrsObj = (result.attrs !== undefined && result.attrs !== null)
|
|
2148
2480
|
? { ...result.attrs }
|
|
2149
2481
|
: {};
|
|
2482
|
+
if (curationPlan !== null) {
|
|
2483
|
+
if (Object.hasOwn(attrsObj, "__plurnk_curation")) {
|
|
2484
|
+
throw new Error("Dispatcher.#writeLog: result attrs collide with private log curation state");
|
|
2485
|
+
}
|
|
2486
|
+
attrsObj.__plurnk_curation = {
|
|
2487
|
+
ids: curationPlan.targetLogEntryIds,
|
|
2488
|
+
expanded: curationPlan.expanded,
|
|
2489
|
+
add: curationPlan.add,
|
|
2490
|
+
remove: curationPlan.remove,
|
|
2491
|
+
};
|
|
2492
|
+
}
|
|
2150
2493
|
const seqs = statement.op === "EXEC" || result.problem !== undefined
|
|
2151
2494
|
? await this.#db.engine_loop_turn_seqs.get({
|
|
2152
2495
|
loop_id: loopId,
|
|
@@ -2197,6 +2540,7 @@ class Dispatcher {
|
|
|
2197
2540
|
sequence: sequence,
|
|
2198
2541
|
origin,
|
|
2199
2542
|
source: null, // dispatch entries are self-authored; {§env-delta} deltas set this
|
|
2543
|
+
model_call_id: modelCallId,
|
|
2200
2544
|
op: durableStatement.op,
|
|
2201
2545
|
suffix: durableStatement.suffix,
|
|
2202
2546
|
signal: this.#signalToJson(durableStatement.signal),
|