@plurnk/plurnk-service 1.0.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 +208 -0
- package/INSTALL.md +69 -0
- package/LICENSE +21 -0
- package/README.md +46 -0
- package/SPEC.md +1511 -0
- package/dist/Paths.d.ts +13 -0
- package/dist/Paths.d.ts.map +1 -0
- package/dist/Paths.js +60 -0
- package/dist/Paths.js.map +1 -0
- package/dist/content/edited-span.d.ts +2 -0
- package/dist/content/edited-span.d.ts.map +1 -0
- package/dist/content/edited-span.js +33 -0
- package/dist/content/edited-span.js.map +1 -0
- package/dist/content/index.d.ts +10 -0
- package/dist/content/index.d.ts.map +1 -0
- package/dist/content/index.js +11 -0
- package/dist/content/index.js.map +1 -0
- package/dist/content/line-marker.d.ts +15 -0
- package/dist/content/line-marker.d.ts.map +1 -0
- package/dist/content/line-marker.js +21 -0
- package/dist/content/line-marker.js.map +1 -0
- package/dist/content/matcher.d.ts +44 -0
- package/dist/content/matcher.d.ts.map +1 -0
- package/dist/content/matcher.js +126 -0
- package/dist/content/matcher.js.map +1 -0
- package/dist/content/mimetype-binary.d.ts +8 -0
- package/dist/content/mimetype-binary.d.ts.map +1 -0
- package/dist/content/mimetype-binary.js +17 -0
- package/dist/content/mimetype-binary.js.map +1 -0
- package/dist/content/path-mimetype.d.ts +5 -0
- package/dist/content/path-mimetype.d.ts.map +1 -0
- package/dist/content/path-mimetype.js +15 -0
- package/dist/content/path-mimetype.js.map +1 -0
- package/dist/content/read-resolve.d.ts +20 -0
- package/dist/content/read-resolve.d.ts.map +1 -0
- package/dist/content/read-resolve.js +68 -0
- package/dist/content/read-resolve.js.map +1 -0
- package/dist/core/ChannelWrite.d.ts +100 -0
- package/dist/core/ChannelWrite.d.ts.map +1 -0
- package/dist/core/ChannelWrite.js +112 -0
- package/dist/core/ChannelWrite.js.map +1 -0
- package/dist/core/ChannelWrite.sql +84 -0
- package/dist/core/Db.d.ts +14 -0
- package/dist/core/Db.d.ts.map +1 -0
- package/dist/core/Db.js +7 -0
- package/dist/core/Db.js.map +1 -0
- package/dist/core/Dispatcher.d.ts +67 -0
- package/dist/core/Dispatcher.d.ts.map +1 -0
- package/dist/core/Dispatcher.js +1021 -0
- package/dist/core/Dispatcher.js.map +1 -0
- package/dist/core/Engine.d.ts +113 -0
- package/dist/core/Engine.d.ts.map +1 -0
- package/dist/core/Engine.js +1410 -0
- package/dist/core/Engine.js.map +1 -0
- package/dist/core/Engine.sql +538 -0
- package/dist/core/EnvFlags.d.ts +13 -0
- package/dist/core/EnvFlags.d.ts.map +1 -0
- package/dist/core/EnvFlags.js +84 -0
- package/dist/core/EnvFlags.js.map +1 -0
- package/dist/core/ExecutorRegistry.d.ts +45 -0
- package/dist/core/ExecutorRegistry.d.ts.map +1 -0
- package/dist/core/ExecutorRegistry.js +128 -0
- package/dist/core/ExecutorRegistry.js.map +1 -0
- package/dist/core/PacketBuilder.d.ts +67 -0
- package/dist/core/PacketBuilder.d.ts.map +1 -0
- package/dist/core/PacketBuilder.js +524 -0
- package/dist/core/PacketBuilder.js.map +1 -0
- package/dist/core/PluginLoader.d.ts +17 -0
- package/dist/core/PluginLoader.d.ts.map +1 -0
- package/dist/core/PluginLoader.js +97 -0
- package/dist/core/PluginLoader.js.map +1 -0
- package/dist/core/ProposalLifecycle.d.ts +59 -0
- package/dist/core/ProposalLifecycle.d.ts.map +1 -0
- package/dist/core/ProposalLifecycle.js +215 -0
- package/dist/core/ProposalLifecycle.js.map +1 -0
- package/dist/core/ProviderInstantiate.d.ts +9 -0
- package/dist/core/ProviderInstantiate.d.ts.map +1 -0
- package/dist/core/ProviderInstantiate.js +156 -0
- package/dist/core/ProviderInstantiate.js.map +1 -0
- package/dist/core/SchemeRegistry.d.ts +26 -0
- package/dist/core/SchemeRegistry.d.ts.map +1 -0
- package/dist/core/SchemeRegistry.js +205 -0
- package/dist/core/SchemeRegistry.js.map +1 -0
- package/dist/core/StrikeRail.d.ts +28 -0
- package/dist/core/StrikeRail.d.ts.map +1 -0
- package/dist/core/StrikeRail.js +165 -0
- package/dist/core/StrikeRail.js.map +1 -0
- package/dist/core/TelemetryChannel.d.ts +37 -0
- package/dist/core/TelemetryChannel.d.ts.map +1 -0
- package/dist/core/TelemetryChannel.js +77 -0
- package/dist/core/TelemetryChannel.js.map +1 -0
- package/dist/core/TokenGauge.d.ts +16 -0
- package/dist/core/TokenGauge.d.ts.map +1 -0
- package/dist/core/TokenGauge.js +58 -0
- package/dist/core/TokenGauge.js.map +1 -0
- package/dist/core/caps/CapsResolve.d.ts +5 -0
- package/dist/core/caps/CapsResolve.d.ts.map +1 -0
- package/dist/core/caps/CapsResolve.js +13 -0
- package/dist/core/caps/CapsResolve.js.map +1 -0
- package/dist/core/caps/DbChannelCaps.d.ts +16 -0
- package/dist/core/caps/DbChannelCaps.d.ts.map +1 -0
- package/dist/core/caps/DbChannelCaps.js +41 -0
- package/dist/core/caps/DbChannelCaps.js.map +1 -0
- package/dist/core/caps/DbEntryCaps.d.ts +19 -0
- package/dist/core/caps/DbEntryCaps.d.ts.map +1 -0
- package/dist/core/caps/DbEntryCaps.js +28 -0
- package/dist/core/caps/DbEntryCaps.js.map +1 -0
- package/dist/core/caps/DbNotifyCaps.d.ts +8 -0
- package/dist/core/caps/DbNotifyCaps.d.ts.map +1 -0
- package/dist/core/caps/DbNotifyCaps.js +35 -0
- package/dist/core/caps/DbNotifyCaps.js.map +1 -0
- package/dist/core/caps/DbSubscriptionCaps.d.ts +10 -0
- package/dist/core/caps/DbSubscriptionCaps.d.ts.map +1 -0
- package/dist/core/caps/DbSubscriptionCaps.js +86 -0
- package/dist/core/caps/DbSubscriptionCaps.js.map +1 -0
- package/dist/core/caps/DbTagCaps.d.ts +17 -0
- package/dist/core/caps/DbTagCaps.d.ts.map +1 -0
- package/dist/core/caps/DbTagCaps.js +37 -0
- package/dist/core/caps/DbTagCaps.js.map +1 -0
- package/dist/core/caps/SchemeCtxImpl.d.ts +18 -0
- package/dist/core/caps/SchemeCtxImpl.d.ts.map +1 -0
- package/dist/core/caps/SchemeCtxImpl.js +42 -0
- package/dist/core/caps/SchemeCtxImpl.js.map +1 -0
- package/dist/core/env-defaults.d.ts +19 -0
- package/dist/core/env-defaults.d.ts.map +1 -0
- package/dist/core/env-defaults.js +89 -0
- package/dist/core/env-defaults.js.map +1 -0
- package/dist/core/fork.d.ts +6 -0
- package/dist/core/fork.d.ts.map +1 -0
- package/dist/core/fork.js +84 -0
- package/dist/core/fork.js.map +1 -0
- package/dist/core/fork.sql +85 -0
- package/dist/core/git-membership.d.ts +23 -0
- package/dist/core/git-membership.d.ts.map +1 -0
- package/dist/core/git-membership.js +366 -0
- package/dist/core/git-membership.js.map +1 -0
- package/dist/core/git-state.d.ts +15 -0
- package/dist/core/git-state.d.ts.map +1 -0
- package/dist/core/git-state.js +67 -0
- package/dist/core/git-state.js.map +1 -0
- package/dist/core/packet-inject.d.ts +5 -0
- package/dist/core/packet-inject.d.ts.map +1 -0
- package/dist/core/packet-inject.js +48 -0
- package/dist/core/packet-inject.js.map +1 -0
- package/dist/core/packet-wire.d.ts +46 -0
- package/dist/core/packet-wire.d.ts.map +1 -0
- package/dist/core/packet-wire.js +453 -0
- package/dist/core/packet-wire.js.map +1 -0
- package/dist/core/path-decode.d.ts +2 -0
- package/dist/core/path-decode.d.ts.map +1 -0
- package/dist/core/path-decode.js +8 -0
- package/dist/core/path-decode.js.map +1 -0
- package/dist/core/plugin-attribution.d.ts +5 -0
- package/dist/core/plugin-attribution.d.ts.map +1 -0
- package/dist/core/plugin-attribution.js +39 -0
- package/dist/core/plugin-attribution.js.map +1 -0
- package/dist/core/plurnk-uri.d.ts +7 -0
- package/dist/core/plurnk-uri.d.ts.map +1 -0
- package/dist/core/plurnk-uri.js +52 -0
- package/dist/core/plurnk-uri.js.map +1 -0
- package/dist/core/resolveForLoop.d.ts +5 -0
- package/dist/core/resolveForLoop.d.ts.map +1 -0
- package/dist/core/resolveForLoop.js +12 -0
- package/dist/core/resolveForLoop.js.map +1 -0
- package/dist/core/results.d.ts +12 -0
- package/dist/core/results.d.ts.map +1 -0
- package/dist/core/results.js +27 -0
- package/dist/core/results.js.map +1 -0
- package/dist/core/run-cap.d.ts +8 -0
- package/dist/core/run-cap.d.ts.map +1 -0
- package/dist/core/run-cap.js +20 -0
- package/dist/core/run-cap.js.map +1 -0
- package/dist/core/run-ops.sql +43 -0
- package/dist/core/scheme-types.d.ts +45 -0
- package/dist/core/scheme-types.d.ts.map +1 -0
- package/dist/core/scheme-types.js +6 -0
- package/dist/core/scheme-types.js.map +1 -0
- package/dist/core/session-settings.d.ts +23 -0
- package/dist/core/session-settings.d.ts.map +1 -0
- package/dist/core/session-settings.js +55 -0
- package/dist/core/session-settings.js.map +1 -0
- package/dist/core/teaching.d.ts +3 -0
- package/dist/core/teaching.d.ts.map +1 -0
- package/dist/core/teaching.js +13 -0
- package/dist/core/teaching.js.map +1 -0
- package/dist/core/types.d.ts +3 -0
- package/dist/core/types.d.ts.map +1 -0
- package/dist/core/types.js +2 -0
- package/dist/core/types.js.map +1 -0
- package/dist/digest/Digest.d.ts +20 -0
- package/dist/digest/Digest.d.ts.map +1 -0
- package/dist/digest/Digest.js +451 -0
- package/dist/digest/Digest.js.map +1 -0
- package/dist/digest/Digest.sql +1 -0
- package/dist/digest/digest.sql +55 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +28 -0
- package/dist/index.js.map +1 -0
- package/dist/schemes/EffectPolicy.d.ts +6 -0
- package/dist/schemes/EffectPolicy.d.ts.map +1 -0
- package/dist/schemes/EffectPolicy.js +18 -0
- package/dist/schemes/EffectPolicy.js.map +1 -0
- package/dist/schemes/Exec.d.ts +39 -0
- package/dist/schemes/Exec.d.ts.map +1 -0
- package/dist/schemes/Exec.js +510 -0
- package/dist/schemes/Exec.js.map +1 -0
- package/dist/schemes/ExecOutputScheme.d.ts +20 -0
- package/dist/schemes/ExecOutputScheme.d.ts.map +1 -0
- package/dist/schemes/ExecOutputScheme.js +39 -0
- package/dist/schemes/ExecOutputScheme.js.map +1 -0
- package/dist/schemes/File.d.ts +42 -0
- package/dist/schemes/File.d.ts.map +1 -0
- package/dist/schemes/File.js +356 -0
- package/dist/schemes/File.js.map +1 -0
- package/dist/schemes/Known.d.ts +17 -0
- package/dist/schemes/Known.d.ts.map +1 -0
- package/dist/schemes/Known.js +40 -0
- package/dist/schemes/Known.js.map +1 -0
- package/dist/schemes/Log.d.ts +22 -0
- package/dist/schemes/Log.d.ts.map +1 -0
- package/dist/schemes/Log.js +269 -0
- package/dist/schemes/Log.js.map +1 -0
- package/dist/schemes/Log.sql +59 -0
- package/dist/schemes/Plurnk.d.ts +17 -0
- package/dist/schemes/Plurnk.d.ts.map +1 -0
- package/dist/schemes/Plurnk.js +46 -0
- package/dist/schemes/Plurnk.js.map +1 -0
- package/dist/schemes/Run.d.ts +25 -0
- package/dist/schemes/Run.d.ts.map +1 -0
- package/dist/schemes/Run.js +172 -0
- package/dist/schemes/Run.js.map +1 -0
- package/dist/schemes/Skill.d.ts +17 -0
- package/dist/schemes/Skill.d.ts.map +1 -0
- package/dist/schemes/Skill.js +40 -0
- package/dist/schemes/Skill.js.map +1 -0
- package/dist/schemes/Unknown.d.ts +17 -0
- package/dist/schemes/Unknown.d.ts.map +1 -0
- package/dist/schemes/Unknown.js +39 -0
- package/dist/schemes/Unknown.js.map +1 -0
- package/dist/schemes/_entry-chunk.d.ts +11 -0
- package/dist/schemes/_entry-chunk.d.ts.map +1 -0
- package/dist/schemes/_entry-chunk.js +100 -0
- package/dist/schemes/_entry-chunk.js.map +1 -0
- package/dist/schemes/_entry-crud.d.ts +31 -0
- package/dist/schemes/_entry-crud.d.ts.map +1 -0
- package/dist/schemes/_entry-crud.js +77 -0
- package/dist/schemes/_entry-crud.js.map +1 -0
- package/dist/schemes/_entry-crud.sql +101 -0
- package/dist/schemes/_entry-find.d.ts +32 -0
- package/dist/schemes/_entry-find.d.ts.map +1 -0
- package/dist/schemes/_entry-find.js +226 -0
- package/dist/schemes/_entry-find.js.map +1 -0
- package/dist/schemes/_entry-find.sql +54 -0
- package/dist/schemes/_entry-graph.d.ts +16 -0
- package/dist/schemes/_entry-graph.d.ts.map +1 -0
- package/dist/schemes/_entry-graph.js +94 -0
- package/dist/schemes/_entry-graph.js.map +1 -0
- package/dist/schemes/_entry-graph.sql +52 -0
- package/dist/schemes/_entry-manifest.d.ts +25 -0
- package/dist/schemes/_entry-manifest.d.ts.map +1 -0
- package/dist/schemes/_entry-manifest.js +266 -0
- package/dist/schemes/_entry-manifest.js.map +1 -0
- package/dist/schemes/_entry-ops.d.ts +32 -0
- package/dist/schemes/_entry-ops.d.ts.map +1 -0
- package/dist/schemes/_entry-ops.js +216 -0
- package/dist/schemes/_entry-ops.js.map +1 -0
- package/dist/schemes/_entry-ops.sql +31 -0
- package/dist/schemes/_entry-semantic.d.ts +38 -0
- package/dist/schemes/_entry-semantic.d.ts.map +1 -0
- package/dist/schemes/_entry-semantic.js +202 -0
- package/dist/schemes/_entry-semantic.js.map +1 -0
- package/dist/schemes/_entry-semantic.sql +95 -0
- package/dist/schemes/_entry-send.d.ts +11 -0
- package/dist/schemes/_entry-send.d.ts.map +1 -0
- package/dist/schemes/_entry-send.js +66 -0
- package/dist/schemes/_entry-send.js.map +1 -0
- package/dist/schemes/cosine.d.ts +4 -0
- package/dist/schemes/cosine.d.ts.map +1 -0
- package/dist/schemes/cosine.js +9 -0
- package/dist/schemes/cosine.js.map +1 -0
- package/dist/schemes/exec-abort.d.ts +15 -0
- package/dist/schemes/exec-abort.d.ts.map +1 -0
- package/dist/schemes/exec-abort.js +29 -0
- package/dist/schemes/exec-abort.js.map +1 -0
- package/dist/schemes/exec-env.d.ts +5 -0
- package/dist/schemes/exec-env.d.ts.map +1 -0
- package/dist/schemes/exec-env.js +34 -0
- package/dist/schemes/exec-env.js.map +1 -0
- package/dist/server/Daemon.d.ts +244 -0
- package/dist/server/Daemon.d.ts.map +1 -0
- package/dist/server/Daemon.js +1119 -0
- package/dist/server/Daemon.js.map +1 -0
- package/dist/server/client-input.d.ts +11 -0
- package/dist/server/client-input.d.ts.map +1 -0
- package/dist/server/client-input.js +145 -0
- package/dist/server/client-input.js.map +1 -0
- package/dist/server/clientTurn.d.ts +6 -0
- package/dist/server/clientTurn.d.ts.map +1 -0
- package/dist/server/clientTurn.js +23 -0
- package/dist/server/clientTurn.js.map +1 -0
- package/dist/server/clientTurn.sql +10 -0
- package/dist/server/dispatch-as-plurnk.d.ts +7 -0
- package/dist/server/dispatch-as-plurnk.d.ts.map +1 -0
- package/dist/server/dispatch-as-plurnk.js +23 -0
- package/dist/server/dispatch-as-plurnk.js.map +1 -0
- package/dist/server/drain.sql +124 -0
- package/dist/server/envelope.d.ts +53 -0
- package/dist/server/envelope.d.ts.map +1 -0
- package/dist/server/envelope.js +172 -0
- package/dist/server/envelope.js.map +1 -0
- package/dist/server/envelope.sql +76 -0
- package/dist/server/logEntry.d.ts +35 -0
- package/dist/server/logEntry.d.ts.map +1 -0
- package/dist/server/logEntry.js +49 -0
- package/dist/server/logEntry.js.map +1 -0
- package/dist/server/logEntry.sql +10 -0
- package/dist/server/loopDocs.d.ts +6 -0
- package/dist/server/loopDocs.d.ts.map +1 -0
- package/dist/server/loopDocs.js +22 -0
- package/dist/server/loopDocs.js.map +1 -0
- package/dist/server/noProposals.d.ts +6 -0
- package/dist/server/noProposals.d.ts.map +1 -0
- package/dist/server/noProposals.js +37 -0
- package/dist/server/noProposals.js.map +1 -0
- package/dist/server/seam-entry-read.sql +21 -0
- package/dist/server/seam-log-read.sql +21 -0
- package/dist/server/seam-loop.sql +9 -0
- package/dist/server/seam-proposal-list.sql +13 -0
- package/dist/server/version-info.d.ts +14 -0
- package/dist/server/version-info.d.ts.map +1 -0
- package/dist/server/version-info.js +69 -0
- package/dist/server/version-info.js.map +1 -0
- package/dist/server/yolo.d.ts +6 -0
- package/dist/server/yolo.d.ts.map +1 -0
- package/dist/server/yolo.js +64 -0
- package/dist/server/yolo.js.map +1 -0
- package/dist/service.d.ts +6 -0
- package/dist/service.d.ts.map +1 -0
- package/dist/service.js +309 -0
- package/dist/service.js.map +1 -0
- package/migrations/0000-00-00.01_schema.sql +500 -0
- package/package.json +86 -0
|
@@ -0,0 +1,1021 @@
|
|
|
1
|
+
var _a;
|
|
2
|
+
import { parsePath } from "@plurnk/plurnk-grammar";
|
|
3
|
+
import { foldAuthorityIntoPath, schemeNameOf } from "./plurnk-uri.js";
|
|
4
|
+
import Fork from "./fork.js";
|
|
5
|
+
import RunCap from "./run-cap.js";
|
|
6
|
+
import { decodePathParens } from "./path-decode.js";
|
|
7
|
+
import { DEFAULT_LOOP_FLAGS } from "./scheme-types.js";
|
|
8
|
+
import { LineMarkerOps, MimetypeBinary } from "../content/index.js";
|
|
9
|
+
import SchemeCtxImpl from "./caps/SchemeCtxImpl.js";
|
|
10
|
+
import SessionSettings from "./session-settings.js";
|
|
11
|
+
// SPEC §scheme-surface: writer must be in target scheme's manifest.writableBy.
|
|
12
|
+
// OPEN/FOLD/READ/FIND are not gated — they curate the log or read, never mutating an entry.
|
|
13
|
+
const MUTATING_OPS = new Set(["EDIT", "SEND", "COPY", "MOVE", "EXEC", "KILL", "FORK", "WORK"]);
|
|
14
|
+
const pathnameFromPath = (path) => {
|
|
15
|
+
if (path.kind === "regex")
|
|
16
|
+
return path.raw; // regex source — parens are syntax, never encoded
|
|
17
|
+
return decodePathParens(path.kind === "url" ? path.pathname : path.raw); // #239 item 4
|
|
18
|
+
};
|
|
19
|
+
// Op dispatch (§op-methods-op-dispatch): gates (writableBy, loop flags), the
|
|
20
|
+
// engine-owned op orchestrations (COPY/MOVE/KILL/SEND/READ-fanout), scheme
|
|
21
|
+
// routing, the durable log write, and the proposal pause.
|
|
22
|
+
class Dispatcher {
|
|
23
|
+
#db;
|
|
24
|
+
#schemes;
|
|
25
|
+
#mimetypes;
|
|
26
|
+
#tokenize;
|
|
27
|
+
#telemetry;
|
|
28
|
+
#proposals;
|
|
29
|
+
// Boot-discovered runtime executors, late-injected on Engine — thunked.
|
|
30
|
+
#executors;
|
|
31
|
+
// Per-loop abort signal, owned by Engine.runLoop — thunked.
|
|
32
|
+
#loopSignal;
|
|
33
|
+
#streamEventNotify;
|
|
34
|
+
#wakeRunNotify;
|
|
35
|
+
#injectRun;
|
|
36
|
+
#cancelRun;
|
|
37
|
+
// §send-premature-terminate/[102]<T> — the engine-owned park-deadline registry (loopId → seconds;
|
|
38
|
+
// -1 = indefinite). The dispatcher WRITES at park; the daemon's drain park-exit consumes.
|
|
39
|
+
#parkDeadlines;
|
|
40
|
+
// §join-blocking-collect (#354) — loops with a READ(run://running-child) armed this turn; a bare
|
|
41
|
+
// SEND[102] parks on it (blocking join). Engine-owned, twin of #parkDeadlines.
|
|
42
|
+
#joinTargets;
|
|
43
|
+
constructor({ db, schemes, mimetypes, tokenize, telemetry, proposals, executors, loopSignal, streamEventNotify, wakeRunNotify, injectRun, cancelRun, parkDeadlines, joinTargets }) {
|
|
44
|
+
this.#db = db;
|
|
45
|
+
this.#schemes = schemes;
|
|
46
|
+
this.#mimetypes = mimetypes;
|
|
47
|
+
this.#tokenize = tokenize;
|
|
48
|
+
this.#telemetry = telemetry;
|
|
49
|
+
this.#proposals = proposals;
|
|
50
|
+
this.#executors = executors;
|
|
51
|
+
this.#loopSignal = loopSignal;
|
|
52
|
+
this.#streamEventNotify = streamEventNotify;
|
|
53
|
+
this.#wakeRunNotify = wakeRunNotify;
|
|
54
|
+
this.#injectRun = injectRun;
|
|
55
|
+
this.#cancelRun = cancelRun;
|
|
56
|
+
this.#parkDeadlines = parkDeadlines ?? new Map();
|
|
57
|
+
this.#joinTargets = joinTargets ?? new Set();
|
|
58
|
+
}
|
|
59
|
+
async dispatch(context) {
|
|
60
|
+
const { statement, sessionId, runId, loopId, turnId, sequence, origin, onDispatch, gauge, turnParseErrors } = context;
|
|
61
|
+
const schemeCtx = this.#buildSchemeCtx({ sessionId, runId, loopId, turnId, origin, gauge });
|
|
62
|
+
let result;
|
|
63
|
+
let denial = this.#checkWritable(statement, origin);
|
|
64
|
+
if (denial === null)
|
|
65
|
+
denial = await this.#checkFlagsGate(statement, loopId);
|
|
66
|
+
if (denial !== null) {
|
|
67
|
+
result = denial;
|
|
68
|
+
}
|
|
69
|
+
else if (_a.#readFansOut(statement)) {
|
|
70
|
+
// READ honors FIND: a glob/folder scope or a matcher fans out to one log row per MATCH
|
|
71
|
+
// (its own writeLogs), returning early. A READ never proposes, so it bypasses the
|
|
72
|
+
// single-row path below. A bare entry, body-less, falls through to the direct read. #286
|
|
73
|
+
return await this.#handleReadFanout(statement, schemeCtx, { runId, loopId, turnId, sequence, origin, onDispatch });
|
|
74
|
+
}
|
|
75
|
+
else {
|
|
76
|
+
// SPEC §scheme-surface + plurnk-schemes#1: action-entry-as-outcome. Scheme-handler
|
|
77
|
+
// exceptions become the action-entry's outcome (status 500), not a
|
|
78
|
+
// thrown bubble. The log_entry is the durable record; engine never
|
|
79
|
+
// skips it. Logging failures (#writeLog throws) are NOT caught —
|
|
80
|
+
// those are system failures.
|
|
81
|
+
try {
|
|
82
|
+
if (statement.op === "SEND" && statement.target === null) {
|
|
83
|
+
result = await this.#handleSendBroadcast(statement, { sessionId, runId, loopId, turnId, turnParseErrors });
|
|
84
|
+
}
|
|
85
|
+
else if (statement.op === "FORK" || statement.op === "WORK") {
|
|
86
|
+
result = await this.#handleRunControl(statement, schemeCtx);
|
|
87
|
+
}
|
|
88
|
+
else if (statement.op === "COPY") {
|
|
89
|
+
result = await this.#handleCopy(statement, schemeCtx);
|
|
90
|
+
}
|
|
91
|
+
else if (statement.op === "MOVE") {
|
|
92
|
+
result = await this.#handleMove(statement, schemeCtx);
|
|
93
|
+
}
|
|
94
|
+
else if (statement.op === "KILL") {
|
|
95
|
+
result = await this.#handleKill(statement, schemeCtx);
|
|
96
|
+
}
|
|
97
|
+
else if (statement.op === "PLAN") {
|
|
98
|
+
result = this.#handlePlan(statement);
|
|
99
|
+
}
|
|
100
|
+
else if (statement.op === "EXEC") {
|
|
101
|
+
// EXEC's target slot is `cwd`, not a scheme address.
|
|
102
|
+
// Per plurnk.md the op routes unconditionally to the
|
|
103
|
+
// exec scheme; the scheme handler reads runtime
|
|
104
|
+
// (signal), cwd (target), and command (body).
|
|
105
|
+
result = await this.#run("exec", statement, schemeCtx);
|
|
106
|
+
}
|
|
107
|
+
else {
|
|
108
|
+
result = await this.#run(schemeNameOf(statement.target), statement, schemeCtx); // §op-methods-op-dispatch
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
catch (err) { // a scheme exception becomes the op's 500 outcome — §scheme-surface-exception-500
|
|
112
|
+
result = {
|
|
113
|
+
status: 500,
|
|
114
|
+
error: err instanceof Error ? err.message : String(err),
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
// §fold-open-meta-operations — OPEN/FOLD are render directives, not actions. They ARE
|
|
119
|
+
// recorded (#382 — a curation act with no forensic trace is how run43's task-frame fold
|
|
120
|
+
// stayed invisible), but engine_render_log suppresses them from the packet, so the receipt
|
|
121
|
+
// rents zero model-facing space — the original clutter concern, resolved by hide-not-drop.
|
|
122
|
+
// §join-blocking-collect (#354) — Run.read on a still-running child returns an awaitRun signal;
|
|
123
|
+
// arm the join so THIS turn's bare SEND[102] parks (the blocking collect) instead of spinning.
|
|
124
|
+
if (typeof result.awaitRun === "string")
|
|
125
|
+
this.#joinTargets.add(loopId);
|
|
126
|
+
const logEntryId = await this.#writeLog({ statement, result, runId, loopId, turnId, sequence, origin });
|
|
127
|
+
onDispatch?.(logEntryId);
|
|
128
|
+
// Proposal lifecycle (SPEC.md §engine-rails + §methods loop.resolve; §proposal-202-pauses). When a
|
|
129
|
+
// side-effecting op returns status 202 (a broadcast SEND[202] park is model
|
|
130
|
+
// speech, not a proposal — #isProposal, #255), the entry is written
|
|
131
|
+
// state='proposed'; dispatch then PAUSES on a per-entry waiter until
|
|
132
|
+
// resolution arrives via Engine.resolveProposal (from the loop/resolve RPC,
|
|
133
|
+
// YOLO listener, or timeout). The post-resolution status replaces 202 in the
|
|
134
|
+
// result the caller sees, so runTurn never branches on a pending state.
|
|
135
|
+
if (_a.#isProposal(statement, result)) {
|
|
136
|
+
// Effect-gated auto-run (read/pure runtimes, plurnk-service#182):
|
|
137
|
+
// no human gate, no loop/proposal notification. Accept + apply
|
|
138
|
+
// in-process; the model sees the outcome directly, never a review.
|
|
139
|
+
if (result.attrs?.inline === true) {
|
|
140
|
+
const effective = await this.#proposals.runApply(statement, result, { decision: "accept" }, { sessionId, runId, loopId, turnId });
|
|
141
|
+
return this.#proposals.applyResolution(logEntryId, effective);
|
|
142
|
+
}
|
|
143
|
+
// Register the resolution waiter SYNCHRONOUSLY before any await
|
|
144
|
+
// yields. A same-tick resolveProposal() (e.g. from a test that
|
|
145
|
+
// awaits the onDispatch callback and immediately resolves) must
|
|
146
|
+
// find the waiter registered — adding an await between insert
|
|
147
|
+
// and waiter-registration would open a race window.
|
|
148
|
+
const resolutionPromise = this.#proposals.awaitResolution(logEntryId);
|
|
149
|
+
// Notify external listeners (Daemon broadcasts loop/proposal;
|
|
150
|
+
// YOLO listener auto-resolves) BEFORE awaiting — they may
|
|
151
|
+
// resolve synchronously inside their handlers.
|
|
152
|
+
const target = this.#extractTarget(statement.target);
|
|
153
|
+
const flags = await this.#loadLoopFlags(loopId); // the loop/proposal notification carries flags (yolo) — §dual-yolo-proposal-carries-flags
|
|
154
|
+
// #note10 — if the target diverged on disk this turn, the model's EDIT is based
|
|
155
|
+
// on a stale read; flag it so a YOLO auto-accept rejects instead of clobbering.
|
|
156
|
+
const diverged = await this.#db.engine_target_diverged_this_turn.get({ run_id: runId, turn_id: turnId, scheme: target.scheme, pathname: target.pathname });
|
|
157
|
+
const event = {
|
|
158
|
+
logEntryId, sessionId, runId, loopId, turnId,
|
|
159
|
+
op: statement.op,
|
|
160
|
+
target: { scheme: target.scheme, pathname: target.pathname },
|
|
161
|
+
body: typeof result.body === "string" ? result.body : "",
|
|
162
|
+
attrs: (result.attrs ?? {}),
|
|
163
|
+
flags,
|
|
164
|
+
staleClobberRisk: diverged !== undefined,
|
|
165
|
+
};
|
|
166
|
+
this.#proposals.notifyPending(event);
|
|
167
|
+
const resolution = await resolutionPromise;
|
|
168
|
+
// Run the scheme's applyResolution hook on accept (writes the
|
|
169
|
+
// file, spawns the process, etc.). If applyResolution returns a
|
|
170
|
+
// 4xx/5xx or throws, the resolution is downgraded to a reject
|
|
171
|
+
// with the failure outcome — engine treats it like a client
|
|
172
|
+
// rejection.
|
|
173
|
+
const effective = await this.#proposals.runApply(statement, result, resolution, { sessionId, runId, loopId, turnId });
|
|
174
|
+
// MOVE into a proposed dest: the deferred source-delete fires ONLY now,
|
|
175
|
+
// after the dest write landed (accept). On reject the source survives.
|
|
176
|
+
if (effective.decision === "accept") {
|
|
177
|
+
const moveSource = result.attrs?.moveSource;
|
|
178
|
+
if (moveSource !== undefined) {
|
|
179
|
+
const srcHandler = this.#schemes.get(moveSource.scheme);
|
|
180
|
+
if (srcHandler !== undefined && typeof srcHandler.deleteEntry === "function") {
|
|
181
|
+
const del = await srcHandler.deleteEntry(moveSource.pathname, schemeCtx);
|
|
182
|
+
// A host-effecting source-delete PROPOSES (202); the MOVE proposal already gated the whole
|
|
183
|
+
// create+kill, so apply the source-delete now — never raise a second review for one MOVE.
|
|
184
|
+
if (del.status === 202 && del.attrs !== undefined && typeof srcHandler.applyResolution === "function")
|
|
185
|
+
await srcHandler.applyResolution({ attrs: del.attrs }, schemeCtx);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
const post = await this.#proposals.applyResolution(logEntryId, effective);
|
|
190
|
+
return post;
|
|
191
|
+
}
|
|
192
|
+
return result;
|
|
193
|
+
}
|
|
194
|
+
// op.look (#283) — resolve a READ and return its content WITHOUT writing a
|
|
195
|
+
// log_entries row: the client's off-run inspection primitive (LOOK → READ,
|
|
196
|
+
// invisible to the model). READ never mutates and never proposes, so this is
|
|
197
|
+
// dispatch's resolve path minus #writeLog. Runs on the client loop, so the
|
|
198
|
+
// human's inspection is never constrained by a model loop's flags. {§op-look}
|
|
199
|
+
async look(context) {
|
|
200
|
+
const { statement, sessionId, runId, loopId, origin = "client" } = context;
|
|
201
|
+
if (statement.op !== "READ")
|
|
202
|
+
throw new Error(`look resolves READ only; got ${statement.op}`);
|
|
203
|
+
// turnId is a write-time FK only — a look writes no row, so 0 (no turn) is inert.
|
|
204
|
+
const schemeCtx = this.#buildSchemeCtx({ sessionId, runId, loopId, turnId: 0, origin });
|
|
205
|
+
const denial = await this.#checkFlagsGate(statement, loopId);
|
|
206
|
+
if (denial !== null)
|
|
207
|
+
return denial;
|
|
208
|
+
return this.#run(schemeNameOf(statement.target), statement, schemeCtx);
|
|
209
|
+
}
|
|
210
|
+
#buildSchemeCtx(ids) {
|
|
211
|
+
const { sessionId, runId, loopId, turnId, origin, gauge } = ids;
|
|
212
|
+
return {
|
|
213
|
+
db: this.#db,
|
|
214
|
+
sessionId, runId, loopId, turnId,
|
|
215
|
+
writer: origin,
|
|
216
|
+
signal: this.#loopSignal(loopId),
|
|
217
|
+
streamEventNotify: this.#streamEventNotify,
|
|
218
|
+
wakeRunNotify: this.#wakeRunNotify,
|
|
219
|
+
injectRun: this.#injectRun,
|
|
220
|
+
mimetypes: this.#mimetypes,
|
|
221
|
+
tokenize: this.#tokenize,
|
|
222
|
+
gauge,
|
|
223
|
+
pushTelemetry: (event) => this.#telemetry.push(sessionId, loopId, event),
|
|
224
|
+
executors: this.#executors(),
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
// Loads loops.flags (json column) and merges over DEFAULT_LOOP_FLAGS so
|
|
228
|
+
// missing keys read as their documented defaults. Single read site —
|
|
229
|
+
// ProposalPendingEvent.flags is constructed from this, and listeners
|
|
230
|
+
// (Daemon broadcast, YOLO auto-accept) share the result.
|
|
231
|
+
async #loadLoopFlags(loopId) {
|
|
232
|
+
const row = await this.#db.engine_get_loop_flags.get({ loop_id: loopId });
|
|
233
|
+
if (row === undefined)
|
|
234
|
+
return DEFAULT_LOOP_FLAGS;
|
|
235
|
+
try {
|
|
236
|
+
const parsed = JSON.parse(row.flags);
|
|
237
|
+
return { ...DEFAULT_LOOP_FLAGS, ...parsed };
|
|
238
|
+
}
|
|
239
|
+
catch {
|
|
240
|
+
return DEFAULT_LOOP_FLAGS;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
// SPEC §scheme-surface: engine rejects writes whose origin is outside the target
|
|
244
|
+
// scheme's manifest.writableBy.
|
|
245
|
+
// - Read-side ops (READ, FIND, OPEN, FOLD) are not gated.
|
|
246
|
+
// - SEND broadcast (path=null) has no target scheme; not gated.
|
|
247
|
+
// - COPY: dst scheme writableBy applies.
|
|
248
|
+
// - MOVE: both src (delete) and dst (write) schemes' writableBy apply.
|
|
249
|
+
// - Schemes without a manifest are not gated (legacy / future allowance).
|
|
250
|
+
#checkWritable(statement, origin) {
|
|
251
|
+
if (!MUTATING_OPS.has(statement.op))
|
|
252
|
+
return null;
|
|
253
|
+
if (statement.op === "SEND" && statement.target === null)
|
|
254
|
+
return null;
|
|
255
|
+
// EXEC's target slot is `cwd`, not a scheme address. The op's
|
|
256
|
+
// authority always belongs to the exec scheme regardless of cwd.
|
|
257
|
+
if (statement.op === "EXEC") {
|
|
258
|
+
return this.#denyIfDisallowed("exec", origin);
|
|
259
|
+
}
|
|
260
|
+
// Run control (FORK/WORK → run://<name>, spawn or fork) is gated by run://'s writableBy — its
|
|
261
|
+
// body is a seed prompt, not a dst path, so the entry-COPY dst-parse below doesn't apply.
|
|
262
|
+
// §machine-processes
|
|
263
|
+
if (this.#isRunControl(statement))
|
|
264
|
+
return this.#denyIfDisallowed("run", origin);
|
|
265
|
+
if (statement.op === "COPY" || statement.op === "MOVE") {
|
|
266
|
+
const dst = statement.op === "COPY" ? (statement.body === null ? null : parsePath(statement.body)) : statement.body;
|
|
267
|
+
const dstScheme = schemeNameOf(dst);
|
|
268
|
+
const dstDenial = this.#denyIfDisallowed(dstScheme, origin);
|
|
269
|
+
if (dstDenial !== null)
|
|
270
|
+
return dstDenial;
|
|
271
|
+
if (statement.op === "MOVE") {
|
|
272
|
+
const srcScheme = schemeNameOf(statement.target);
|
|
273
|
+
if (srcScheme !== dstScheme) {
|
|
274
|
+
const srcDenial = this.#denyIfDisallowed(srcScheme, origin);
|
|
275
|
+
if (srcDenial !== null)
|
|
276
|
+
return srcDenial;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
return null;
|
|
280
|
+
}
|
|
281
|
+
const target = schemeNameOf(statement.target);
|
|
282
|
+
return this.#denyIfDisallowed(target, origin);
|
|
283
|
+
}
|
|
284
|
+
#denyIfDisallowed(schemeName, origin) {
|
|
285
|
+
if (schemeName === null)
|
|
286
|
+
return null;
|
|
287
|
+
const handler = this.#schemes.get(schemeName);
|
|
288
|
+
if (handler === undefined)
|
|
289
|
+
return null;
|
|
290
|
+
const manifest = handler.constructor.manifest;
|
|
291
|
+
if (manifest === undefined)
|
|
292
|
+
return null;
|
|
293
|
+
if (manifest.writableBy.includes(origin))
|
|
294
|
+
return null;
|
|
295
|
+
return { status: 403, error: `writer '${origin}' is not in writableBy for scheme '${schemeName}'` }; // §scheme-surface-writableby-403
|
|
296
|
+
}
|
|
297
|
+
// Per-loop flag gating. Schemes self-declare their flag affinity in
|
|
298
|
+
// their manifest (excludedInAsk / requiresWeb /
|
|
299
|
+
// requiresInteraction); SchemeRegistry.resolveForLoop returns the
|
|
300
|
+
// active set under the loop's persisted flags. Anything outside the
|
|
301
|
+
// set returns 403 — action-entry-as-outcome carries the rejection.
|
|
302
|
+
async #checkFlagsGate(statement, loopId) {
|
|
303
|
+
// Broadcast SEND has no scheme to gate.
|
|
304
|
+
if (statement.op === "SEND" && statement.target === null)
|
|
305
|
+
return null;
|
|
306
|
+
const flags = await this.#loadLoopFlags(loopId);
|
|
307
|
+
// Fast path: default flags gate nothing. (yolo never gates.)
|
|
308
|
+
if (!flags.noWeb && !flags.noInteraction && flags.mode === "act")
|
|
309
|
+
return null;
|
|
310
|
+
// §mode-ask-read-only — the ancient contract: an ask-mode loop NEVER changes the world. The
|
|
311
|
+
// filesystem writes (EDIT/COPY-dest/MOVE/KILL touching the `file` scheme — each proposes disk
|
|
312
|
+
// egress, §membership) are refused HERE, regardless of the scheme's read-activity, because
|
|
313
|
+
// `file` stays active for READs. The EXEC host runtime is refused by its excludedInAsk scheme
|
|
314
|
+
// below. This lived only in SPEC (line 65) with no anchor → no guard → it silently regressed.
|
|
315
|
+
if (flags.mode === "ask") {
|
|
316
|
+
const isFile = (t) => schemeNameOf(t) === "file";
|
|
317
|
+
// Each branch narrows statement.op so statement.body is correctly typed (COPY dest is a
|
|
318
|
+
// string to parse; MOVE dest is already a path). EDIT/KILL write the target; COPY writes
|
|
319
|
+
// the dest; MOVE deletes the source AND writes the dest — any `file` touch is a write.
|
|
320
|
+
let writesFilesystem = false;
|
|
321
|
+
if (statement.op === "EDIT" || statement.op === "KILL")
|
|
322
|
+
writesFilesystem = isFile(statement.target);
|
|
323
|
+
else if (statement.op === "COPY")
|
|
324
|
+
writesFilesystem = isFile(statement.body === null ? null : parsePath(statement.body));
|
|
325
|
+
else if (statement.op === "MOVE")
|
|
326
|
+
writesFilesystem = isFile(statement.target) || isFile(statement.body);
|
|
327
|
+
if (writesFilesystem) {
|
|
328
|
+
return { status: 403, error: `'${statement.op}' cannot change the filesystem in an ask-mode loop — ask is read-only (no side-effecting ops). Answer or advise the user directly; an act-mode loop is required to edit files.` };
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
const active = this.#schemes.resolveForLoop(flags);
|
|
332
|
+
// #367 — the steer NAMES the restriction and says DO NOT RETRY: the old vague "inactive under
|
|
333
|
+
// current loop flags" invited an identical re-emit each turn → the StrikeRail's 508. An
|
|
334
|
+
// unavailable op is not a failing one; the model must change course, not repeat.
|
|
335
|
+
const restriction = flags.mode === "ask"
|
|
336
|
+
? "this is an ask-mode (read-only) loop — you cannot run commands or take host actions here"
|
|
337
|
+
: flags.noWeb && flags.noInteraction ? "web and interaction are disabled for this loop"
|
|
338
|
+
: flags.noWeb ? "web access is disabled for this loop"
|
|
339
|
+
: "interaction is disabled for this loop";
|
|
340
|
+
const check = (target) => {
|
|
341
|
+
const scheme = schemeNameOf(target);
|
|
342
|
+
if (scheme === null)
|
|
343
|
+
return null;
|
|
344
|
+
if (active.has(scheme))
|
|
345
|
+
return null;
|
|
346
|
+
return { status: 403, error: `'${scheme}' is unavailable: ${restriction}. Do NOT retry it — it is unavailable, not failing; answer or advise the user directly (an act-mode loop is required to use it).` };
|
|
347
|
+
};
|
|
348
|
+
if (this.#isRunControl(statement))
|
|
349
|
+
return check(statement.target); // body is a spawn/fork task, not a dst path
|
|
350
|
+
if (statement.op === "COPY" || statement.op === "MOVE") {
|
|
351
|
+
return check(statement.target) ?? check(statement.op === "COPY" ? (statement.body === null ? null : parsePath(statement.body)) : statement.body);
|
|
352
|
+
}
|
|
353
|
+
return check(statement.target);
|
|
354
|
+
}
|
|
355
|
+
// Run control is FORK/WORK (grammar 0.74.55), not COPY — its body
|
|
356
|
+
// is the new run's seed prompt, not a destination path. The COPY gates and #handleCopy
|
|
357
|
+
// branch on this so they never parse the prompt as a dst path.
|
|
358
|
+
#isRunControl(statement) {
|
|
359
|
+
return statement.op === "FORK" || statement.op === "WORK"; // run control targets run://<name> (grammar 0.74.55)
|
|
360
|
+
}
|
|
361
|
+
// FORK/WORK(run://<name>):task — run control (grammar 0.74.55):
|
|
362
|
+
// • run://self → FORK: deep-copy the current run's log into a new sister (Fork), then
|
|
363
|
+
// continue it with the prompt (§machine-processes-fork-copies-the-log).
|
|
364
|
+
// • run://<name> → SPAWN: a fresh sister (empty log) named <name>, started on the prompt.
|
|
365
|
+
// A LIVE sister already holding <name> is a 409 conflict; a free or terminated name is
|
|
366
|
+
// reclaimed (§run-scheme-spawn). The self form is fork; only a name spawns.
|
|
367
|
+
// Both ride the daemon inject and obey the active-runs cap (508, §run-scheme-cap).
|
|
368
|
+
// FORK/WORK — run control (grammar 0.74.55). Both name a NEW run in the target authority
|
|
369
|
+
// (run://<name>) and carry its seed task in the body. WORK spawns a fresh worker; FORK branches
|
|
370
|
+
// the current run's log into a named sister. Replaces the COPY(run://) overload — one verb, one
|
|
371
|
+
// intent, so the model never conflates the target slot with the body (grammar#52).
|
|
372
|
+
async #handleRunControl(statement, ctx) {
|
|
373
|
+
const target = statement.target;
|
|
374
|
+
if (target === null)
|
|
375
|
+
return { status: 400, error: `${statement.op} requires a run target (${statement.op}(run://<name>))` };
|
|
376
|
+
const name = target.kind === "url" ? (target.hostname ?? "") : ""; // §run-scheme — run is the AUTHORITY (run://<name>), not the path
|
|
377
|
+
if (name === "")
|
|
378
|
+
return { status: 400, error: `${statement.op} requires a run name (run://<name>)` };
|
|
379
|
+
if (name === "self")
|
|
380
|
+
return { status: 400, error: `'self' is the current run — ${statement.op} names a NEW run (run://<name>)` };
|
|
381
|
+
if (ctx.injectRun === undefined)
|
|
382
|
+
throw new Error("run control: injectRun capability absent");
|
|
383
|
+
const denied = await RunCap.deny(this.#db, ctx.sessionId);
|
|
384
|
+
if (denied !== null)
|
|
385
|
+
return denied;
|
|
386
|
+
const prompt = typeof statement.body === "string" ? statement.body : "";
|
|
387
|
+
// §run-delegation-inherits-flags — authority flows down the delegation edge: the child's live
|
|
388
|
+
// loop runs with ITS DELEGATOR'S flags. A flagless (non-YOLO) child's every side-effecting op
|
|
389
|
+
// proposes into a resolver-less void — 300s auto-cancel per attempt was the fan-out wedge.
|
|
390
|
+
const flags = await this.#loadLoopFlags(ctx.loopId);
|
|
391
|
+
// A name is frozen per run but reclaimable across time (§machine-processes-run-origin): a LIVE
|
|
392
|
+
// sister holding it is a 409 (legible, never a raw UNIQUE 500); a free/terminated name reclaims.
|
|
393
|
+
const live = await this.#db.run_live_by_name.get({ session_id: ctx.sessionId, name });
|
|
394
|
+
if (live !== undefined)
|
|
395
|
+
return { status: 409, error: `run '${name}' is already running` };
|
|
396
|
+
if (statement.op === "FORK") {
|
|
397
|
+
// Branch the current run's log into a named sister.
|
|
398
|
+
const branchRunId = await Fork.fork(this.#db, ctx.runId, name);
|
|
399
|
+
await ctx.injectRun({ sessionId: ctx.sessionId, runId: branchRunId, prompt, flags });
|
|
400
|
+
return { status: 200, body: name };
|
|
401
|
+
}
|
|
402
|
+
// WORK — a fresh worker sister named <name>.
|
|
403
|
+
const row = await this.#db.fork_insert_run.get({
|
|
404
|
+
session_id: ctx.sessionId, name, parent_run_id: ctx.runId, origin: ctx.writer,
|
|
405
|
+
});
|
|
406
|
+
if (row === undefined)
|
|
407
|
+
throw new Error("run spawn: run insert returned no row");
|
|
408
|
+
await ctx.injectRun({ sessionId: ctx.sessionId, runId: row.id, prompt, flags });
|
|
409
|
+
return { status: 200, body: name };
|
|
410
|
+
}
|
|
411
|
+
async #handleCopy(statement, ctx) {
|
|
412
|
+
if (statement.op !== "COPY")
|
|
413
|
+
throw new Error("unreachable");
|
|
414
|
+
const srcPath = statement.target;
|
|
415
|
+
// COPY is entry-copy only — run control (spawn/fork) moved to the FORK/WORK verbs
|
|
416
|
+
// (grammar 0.74.55). COPY's body is a dest path (grammar §COPY); an unparseable dest → 400.
|
|
417
|
+
const dstPath = statement.body === null ? null : parsePath(statement.body);
|
|
418
|
+
if (srcPath === null)
|
|
419
|
+
return { status: 400, error: "COPY requires source path" };
|
|
420
|
+
if (dstPath === null)
|
|
421
|
+
return { status: 400, error: "COPY destination must be a parseable path in the body slot" };
|
|
422
|
+
return await this.#copyOrchestration({ statement, srcPath, dstPath, ctx });
|
|
423
|
+
}
|
|
424
|
+
async #handleMove(statement, ctx) {
|
|
425
|
+
if (statement.op !== "MOVE")
|
|
426
|
+
throw new Error("unreachable");
|
|
427
|
+
const srcPath = statement.target;
|
|
428
|
+
const dstPath = statement.body;
|
|
429
|
+
if (srcPath === null)
|
|
430
|
+
return { status: 400, error: "MOVE requires source path" };
|
|
431
|
+
// MOVE is relocation only — deletion is KILL's job (§move, §move-dev-null-not-special). The /dev/null
|
|
432
|
+
// and null-body delete-by-MOVE back-compat is retired: no silent debt.
|
|
433
|
+
if (dstPath === null)
|
|
434
|
+
return { status: 400, error: "MOVE requires a destination; use KILL to delete" }; // §move-null-body-400
|
|
435
|
+
const srcSchemeName = schemeNameOf(srcPath);
|
|
436
|
+
if (srcSchemeName === null)
|
|
437
|
+
return { status: 400, error: "MOVE source must be a URL path with a scheme" };
|
|
438
|
+
const srcHandler = this.#schemes.get(srcSchemeName);
|
|
439
|
+
if (srcHandler === undefined || typeof srcHandler.deleteEntry !== "function")
|
|
440
|
+
return { status: 501 };
|
|
441
|
+
// Relocation: COPY then DELETE source (§move-relocation-deletes-source).
|
|
442
|
+
const copyResult = await this.#copyOrchestration({ statement, srcPath, dstPath, ctx });
|
|
443
|
+
if (copyResult.status >= 400)
|
|
444
|
+
return copyResult;
|
|
445
|
+
const srcPathname = pathnameFromPath(srcPath);
|
|
446
|
+
// If the dest write is a pending proposal (file dest → §membership review), the
|
|
447
|
+
// source-delete MUST wait until the dest actually lands — a rejected
|
|
448
|
+
// proposal would otherwise lose the source. Thread it into the resolution:
|
|
449
|
+
// dispatch deletes the source AFTER the dest applies on accept.
|
|
450
|
+
if (copyResult.status === 202) {
|
|
451
|
+
return { ...copyResult, attrs: { ...copyResult.attrs, moveSource: { scheme: srcSchemeName, pathname: srcPathname } } };
|
|
452
|
+
}
|
|
453
|
+
const delResult = await srcHandler.deleteEntry(srcPathname, ctx);
|
|
454
|
+
if (delResult.status >= 400)
|
|
455
|
+
return { status: delResult.status };
|
|
456
|
+
return copyResult;
|
|
457
|
+
}
|
|
458
|
+
// KILL — scheme-polymorphic destroy (plurnk-grammar#203 / 0.28.0). Entry-KILL
|
|
459
|
+
// permanently deletes the entry: the canonical delete now, MOVE→/dev/null
|
|
460
|
+
// retired from the model's vocabulary. Process-KILL (exec:///) aborts the
|
|
461
|
+
// running spawn's controller (the same teardown loop.cancel rides), addressed
|
|
462
|
+
// by coordinate pathname (#203). The KILL body is an opaque
|
|
463
|
+
// annotation with no runtime meaning; it survives into the log row's tx for
|
|
464
|
+
// free via the statement serialization. Status: 200 killed · 404 unknown ·
|
|
465
|
+
// 405 log:/// (append-only) · 403 writableBy (the #checkWritable gate, KILL ∈
|
|
466
|
+
// MUTATING_OPS) · 200/410/304/404 exec (killed / killed-earlier / exited / unknown) · 501 no-kill/delete scheme.
|
|
467
|
+
async #handleKill(statement, ctx) {
|
|
468
|
+
if (statement.op !== "KILL")
|
|
469
|
+
throw new Error("unreachable");
|
|
470
|
+
const path = statement.target;
|
|
471
|
+
if (path === null)
|
|
472
|
+
return { status: 400, error: "KILL requires a target path" };
|
|
473
|
+
const schemeName = schemeNameOf(path);
|
|
474
|
+
if (schemeName === null)
|
|
475
|
+
return { status: 400, error: "KILL target must be a URL path with a scheme" };
|
|
476
|
+
// KILL on log:/// erases the log row(s) — the model's DB-storage curation lever
|
|
477
|
+
// (plurnk.md:36, :98), routed to Log.kill below via the killable.kill path. The old
|
|
478
|
+
// "append-only" 405 forbade what the grammar requires; FOLD only collapses the render.
|
|
479
|
+
// Process-KILL: any scheme whose handler exposes kill() aborts a live stream — the
|
|
480
|
+
// exec handler, registered as "exec" + under every runtime tag (sh/node), so a tag-
|
|
481
|
+
// addressed stream (sh:///l/t/s) routes here, not to deleteEntry. §exec
|
|
482
|
+
const killable = this.#schemes.get(schemeName);
|
|
483
|
+
if (killable !== undefined && typeof killable.kill === "function") {
|
|
484
|
+
return await killable.kill(pathnameFromPath(path), statement.signal, ctx);
|
|
485
|
+
}
|
|
486
|
+
if (schemeName === "run") {
|
|
487
|
+
// Entry-path present → KILL a run-scope scratch ENTRY (delete it), self-only —
|
|
488
|
+
// NOT run cancellation. The authority (hostname) names the owner, the pathname the
|
|
489
|
+
// entry; only the path-ABSENT form (run://<name>) terminates the run-as-actor. §run-scheme
|
|
490
|
+
const entryPath = path.kind === "url" ? (path.pathname ?? "") : "";
|
|
491
|
+
if (entryPath !== "" && entryPath !== "/") {
|
|
492
|
+
const runHandler = this.#schemes.get("run");
|
|
493
|
+
return await runHandler.deleteEntry(statement, ctx);
|
|
494
|
+
}
|
|
495
|
+
// terminate — abort any run by address; whoever holds it may end it.
|
|
496
|
+
// `run://self` = self. cancelRun (→ Daemon.cancelDrain) aborts the run's signal
|
|
497
|
+
// (its loop closes 499); an idle run is a no-op-200, a missing run 404.
|
|
498
|
+
const name = path.kind === "url" ? (path.hostname ?? "") : ""; // §run-scheme — run is the AUTHORITY
|
|
499
|
+
if (name === "")
|
|
500
|
+
return { status: 400, error: "run:// kill requires a run name or 'self' (run://<name>)" };
|
|
501
|
+
let runId = ctx.runId;
|
|
502
|
+
if (name !== "self") {
|
|
503
|
+
const row = await this.#db.run_resolve_by_name.get({ session_id: ctx.sessionId, name });
|
|
504
|
+
if (row === undefined)
|
|
505
|
+
return { status: 404, error: `run://${name} not found in this session` };
|
|
506
|
+
runId = row.id;
|
|
507
|
+
}
|
|
508
|
+
if (this.#cancelRun === undefined)
|
|
509
|
+
throw new Error("run kill: cancelRun capability absent");
|
|
510
|
+
// §op-synchronous — KILL is DECISIVE: flip the run's live loops to 499 NOW so the
|
|
511
|
+
// same-turn premature-terminate gate sees it dead (KILL … SEND[200] concludes in ONE
|
|
512
|
+
// turn — the model never reasons about async reap timing). The physical scope reap
|
|
513
|
+
// (drain abort + stream teardown) then rides cancelRun; a killed loop can't heal back
|
|
514
|
+
// because cancelRun aborts its drain's signal.
|
|
515
|
+
await this.#db.engine_terminate_run_live_loops.run({ run_id: runId, message: "killed via run:// KILL" });
|
|
516
|
+
this.#cancelRun(runId);
|
|
517
|
+
return { status: 200 };
|
|
518
|
+
}
|
|
519
|
+
const handler = this.#schemes.get(schemeName);
|
|
520
|
+
if (handler === undefined || typeof handler.deleteEntry !== "function")
|
|
521
|
+
return { status: 501 };
|
|
522
|
+
// A host-effecting delete (file) returns 202 to PROPOSE — pass its attrs through so the proposal
|
|
523
|
+
// carries the delete target to review (§isProposal fires on 202). Plurnk-internal deletes execute inline.
|
|
524
|
+
const delResult = await handler.deleteEntry(pathnameFromPath(path), ctx);
|
|
525
|
+
return delResult.attrs !== undefined ? { status: delResult.status, attrs: delResult.attrs } : { status: delResult.status };
|
|
526
|
+
}
|
|
527
|
+
// Multi-file READ fan-out (SPEC §matcher-result — "the companion to FIND's survey"). A glob
|
|
528
|
+
// READ target resolves to MANY files; READ returns one log row per file that matches, each
|
|
529
|
+
// holding that file's matching lines. The matched SET is exactly FIND's survey (which files
|
|
530
|
+
// + where — matchLines), so we reuse the scheme's own find, then READ each matched file. One
|
|
531
|
+
// model command, N log rows — each row addresses its concrete file, so it folds/kills/re-READs
|
|
532
|
+
// on its own. The running sequence counter in runTurn advances by rowsWritten.
|
|
533
|
+
// A READ fans out (honors FIND) when it resolves to more than the single exact entry: a glob
|
|
534
|
+
// or folder scope, OR a matcher (which selects per-match within whatever the target resolved).
|
|
535
|
+
// A bare entry, body-less, is the one direct read. #286
|
|
536
|
+
static #readFansOut(statement) {
|
|
537
|
+
if (statement.op !== "READ")
|
|
538
|
+
return false;
|
|
539
|
+
if ("body" in statement && statement.body !== null)
|
|
540
|
+
return true; // a matcher → per-match fan-out
|
|
541
|
+
const t = statement.target;
|
|
542
|
+
const p = t === null ? "" : (t.kind === "url" ? t.pathname : t.raw);
|
|
543
|
+
return p.includes("*") || p.endsWith("/"); // glob/folder scope → fan out its contents
|
|
544
|
+
}
|
|
545
|
+
// Clone the READ onto one concrete match — the FIND already matched, so the per-match READ
|
|
546
|
+
// delivers content at the span: strip the body (no re-match) and set <L> to the span. A null
|
|
547
|
+
// span (body-less folder/glob fan-out) reads the whole entry. #286
|
|
548
|
+
static #retargetRead(statement, pathname, span) {
|
|
549
|
+
const t = statement.target;
|
|
550
|
+
const target = t !== null && t.kind === "url"
|
|
551
|
+
? { ...t, pathname, raw: `${t.scheme}://${pathname}` }
|
|
552
|
+
: { ...t, raw: pathname };
|
|
553
|
+
const lineMarker = span !== null ? { marks: [span.lineStart, span.lineEnd] } : null;
|
|
554
|
+
return { ...statement, target: target, lineMarker, body: null };
|
|
555
|
+
}
|
|
556
|
+
async #handleReadFanout(statement, ctx, ids) {
|
|
557
|
+
const { runId, loopId, turnId, sequence, origin, onDispatch } = ids;
|
|
558
|
+
const schemeName = schemeNameOf(statement.target);
|
|
559
|
+
const found = await this.#run(schemeName, { ...statement, op: "FIND" }, ctx);
|
|
560
|
+
const matches = found.matches ?? [];
|
|
561
|
+
// Find-less scheme, a matcher/scope error, or zero matches → a single row carrying the
|
|
562
|
+
// status, exactly like a non-fanned READ. The model sees the empty/failed result, not silence.
|
|
563
|
+
if (found.status !== 200 || matches.length === 0) {
|
|
564
|
+
const result = { status: found.status === 200 ? 204 : found.status };
|
|
565
|
+
const id = await this.#writeLog({ statement, result, runId, loopId, turnId, sequence, origin });
|
|
566
|
+
onDispatch?.(id);
|
|
567
|
+
return { ...result, rowsWritten: 1 };
|
|
568
|
+
}
|
|
569
|
+
// One READ row per MATCH — the span's source lines (or the whole entry for a body-less
|
|
570
|
+
// folder/glob). The match span is SOURCE LINES, so deliver via a raw line-slice — NOT the
|
|
571
|
+
// scheme's <L> (which is item-index for application/json, structural for xml). Read each
|
|
572
|
+
// distinct entry's content once, then line-slice per match. #286
|
|
573
|
+
// §matcher-selection-signal — the SELECTION SUMMARY row: the internal FIND that located
|
|
574
|
+
// the matches is WRITTEN (op=FIND, its full result — per-hit matchSpan + matchPath, the
|
|
575
|
+
// canonical dialect coordinate) before the per-match deliveries, exactly as if the model
|
|
576
|
+
// had FINDed then READ. On a degenerate single-line document the N delivery rows are
|
|
577
|
+
// identical whole-file lines; the summary row is what tells the model its query hit N
|
|
578
|
+
// times and WHERE (run30: two hits indistinguishable from failure; 17 retries, 508).
|
|
579
|
+
const findRowId = await this.#writeLog({ statement: { ...statement, op: "FIND" }, result: found, runId, loopId, turnId, sequence, origin });
|
|
580
|
+
onDispatch?.(findRowId);
|
|
581
|
+
const wholeByPath = new Map();
|
|
582
|
+
const fannedStatuses = [];
|
|
583
|
+
let written = 1;
|
|
584
|
+
for (const m of matches) {
|
|
585
|
+
let whole = wholeByPath.get(m.pathname);
|
|
586
|
+
if (whole === undefined) {
|
|
587
|
+
whole = await this.#run(schemeName, _a.#retargetRead(statement, m.pathname, null), ctx);
|
|
588
|
+
wholeByPath.set(m.pathname, whole);
|
|
589
|
+
}
|
|
590
|
+
const result = _a.#sliceMatch(whole, m.span);
|
|
591
|
+
const id = await this.#writeLog({ statement: _a.#retargetRead(statement, m.pathname, m.span), result, runId, loopId, turnId, sequence: sequence + written, origin });
|
|
592
|
+
onDispatch?.(id);
|
|
593
|
+
fannedStatuses.push(result.status);
|
|
594
|
+
written++;
|
|
595
|
+
}
|
|
596
|
+
return { status: 200, rowsWritten: written, fannedStatuses };
|
|
597
|
+
}
|
|
598
|
+
// Deliver one match: the whole entry (body-less, span null) or the source lines at the span —
|
|
599
|
+
// a RAW line-slice, so a structural mimetype (json item-index / xml) doesn't mis-slice a span
|
|
600
|
+
// that is, by construction, source line numbers (#286).
|
|
601
|
+
static #sliceMatch(whole, span) {
|
|
602
|
+
if (whole.status !== 200 || span === null)
|
|
603
|
+
return whole;
|
|
604
|
+
const sliced = LineMarkerOps.sliceLines(typeof whole.content === "string" ? whole.content : "", { marks: [span.lineStart, span.lineEnd] });
|
|
605
|
+
if (sliced.status !== 200)
|
|
606
|
+
return { status: sliced.status, error: sliced.error };
|
|
607
|
+
return { status: 200, content: sliced.text ?? "", mimetype: "text/markdown", startLine: sliced.startLine ?? span.lineStart };
|
|
608
|
+
}
|
|
609
|
+
// §model-entry — mirror a verbatim model emission back as an actionless `model` log row, so
|
|
610
|
+
// the model can finally SEE its own prior output (and reason through its own syntax errors).
|
|
611
|
+
// Born FOLDED by default (budget-neutral until OPENed); the turn-0 exemplar passes folded:false
|
|
612
|
+
// (born open — the one worked example the model orients on, thinning the grammar). text/vnd.plurnk.
|
|
613
|
+
async writeModelEntry({ verbatim, runId, loopId, turnId, sequence, folded, origin = "model" }) {
|
|
614
|
+
const row = await this.#db.engine_insert_log_entry.get({
|
|
615
|
+
run_id: runId, loop_id: loopId, turn_id: turnId, sequence,
|
|
616
|
+
origin, source: null, op: "model", suffix: "", signal: null,
|
|
617
|
+
scheme: null, username: null, password: null, hostname: null, port: null,
|
|
618
|
+
pathname: null, params: null, fragment: null, lineMarker: null,
|
|
619
|
+
tx: "", mimetype_tx: "text/vnd.plurnk",
|
|
620
|
+
rx: JSON.stringify({ content: verbatim, mimetype: "text/vnd.plurnk" }),
|
|
621
|
+
mimetype_rx: "application/json",
|
|
622
|
+
status_rx: 200, tokens: this.#tokenize(verbatim), state: "resolved", outcome: null, attrs: "{}",
|
|
623
|
+
});
|
|
624
|
+
if (row === undefined)
|
|
625
|
+
throw new Error("Dispatcher.writeModelEntry: insert returned no row");
|
|
626
|
+
if (folded)
|
|
627
|
+
await this.#db.engine_fold_log_entry.run({ id: row.id });
|
|
628
|
+
return row.id;
|
|
629
|
+
}
|
|
630
|
+
// PLAN — the model's reasoning op (the 11th op). An ordinary op: dispatched like any
|
|
631
|
+
// other, logged, and broadcast to the client as a log entry — but a pure no-op for
|
|
632
|
+
// state (PLAN ∉ MUTATING_OPS); its body serializes into the log row's tx, no effect.
|
|
633
|
+
#handlePlan(statement) {
|
|
634
|
+
if (statement.op !== "PLAN")
|
|
635
|
+
throw new Error("unreachable");
|
|
636
|
+
return { status: 200 };
|
|
637
|
+
}
|
|
638
|
+
// Same- and cross-scheme COPY share one orchestrator — §copy-cross-scheme-copy §move-cross-scheme-move
|
|
639
|
+
async #copyOrchestration({ statement, srcPath, dstPath, ctx }) {
|
|
640
|
+
const srcSchemeName = schemeNameOf(srcPath);
|
|
641
|
+
const dstSchemeName = schemeNameOf(dstPath);
|
|
642
|
+
if (srcSchemeName === null || dstSchemeName === null)
|
|
643
|
+
return { status: 400, error: "COPY/MOVE require URL paths with schemes" };
|
|
644
|
+
const srcHandler = this.#schemes.get(srcSchemeName);
|
|
645
|
+
const dstHandler = this.#schemes.get(dstSchemeName);
|
|
646
|
+
if (srcHandler === undefined || dstHandler === undefined)
|
|
647
|
+
return { status: 501 };
|
|
648
|
+
if (typeof srcHandler.readEntry !== "function" || typeof dstHandler.writeEntry !== "function")
|
|
649
|
+
return { status: 501 };
|
|
650
|
+
const srcPathname = pathnameFromPath(srcPath);
|
|
651
|
+
const dstPathname = pathnameFromPath(dstPath);
|
|
652
|
+
const srcResult = await srcHandler.readEntry(srcPathname, ctx);
|
|
653
|
+
if (srcResult.status !== 200 || srcResult.entry === null)
|
|
654
|
+
return { status: 404, error: `COPY/MOVE source not found: ${srcSchemeName}://${srcPathname}` }; // §copy-missing-source-404 §move-missing-source-404
|
|
655
|
+
const entry = srcResult.entry;
|
|
656
|
+
// Destination read — the conflict/no-op verdict is deferred until the
|
|
657
|
+
// to-be-written content is known (after <L> slice + tag resolution below),
|
|
658
|
+
// so an identical re-copy resolves to 304 instead of a phantom 409.
|
|
659
|
+
const dstExisting = typeof dstHandler.readEntry === "function"
|
|
660
|
+
? await dstHandler.readEntry(dstPathname, ctx)
|
|
661
|
+
: null;
|
|
662
|
+
// Mimetype compatibility check against the destination scheme's manifest
|
|
663
|
+
const dstManifest = dstHandler.constructor.manifest;
|
|
664
|
+
const dstChannels = dstManifest?.channels ?? {};
|
|
665
|
+
for (const [channelName, channelData] of Object.entries(entry.channels)) {
|
|
666
|
+
const expectedMimetype = dstChannels[channelName];
|
|
667
|
+
if (expectedMimetype !== undefined && expectedMimetype !== channelData.mimetype) {
|
|
668
|
+
return { status: 415, error: `mimetype mismatch on channel '${channelName}': ${channelData.mimetype} vs ${expectedMimetype}` }; // cross-mimetype COPY/MOVE → 415, never coerce — §channel-mimetype-cross-mimetype-415
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
// `<L>` source range slicing per SPEC.md §op-invariants (symmetric with READ
|
|
672
|
+
// `<L>` — source range, no line-number prefix).
|
|
673
|
+
// Applied to every channel of the source entry. Binary channels return
|
|
674
|
+
// 415 since line semantics don't apply.
|
|
675
|
+
const lineMarker = statement.lineMarker ?? null;
|
|
676
|
+
let channels = entry.channels;
|
|
677
|
+
if (lineMarker !== null) {
|
|
678
|
+
const sliced = {};
|
|
679
|
+
for (const [channelName, channelData] of Object.entries(entry.channels)) {
|
|
680
|
+
if (MimetypeBinary.isBinaryMimetype(channelData.mimetype)) {
|
|
681
|
+
return { status: 415, error: `cannot slice <L> on binary channel '${channelName}' (${channelData.mimetype})` };
|
|
682
|
+
}
|
|
683
|
+
const r = LineMarkerOps.sliceLinesRaw(channelData.content ?? "", lineMarker);
|
|
684
|
+
if (r.status !== 200)
|
|
685
|
+
return { status: r.status, error: r.error };
|
|
686
|
+
sliced[channelName] = { ...channelData, content: r.text ?? "" };
|
|
687
|
+
}
|
|
688
|
+
channels = sliced;
|
|
689
|
+
}
|
|
690
|
+
// Tag resolution: signal = replace (§copy-signal-replaces-source-tags); absent/empty = carry from source (§copy-no-signal-carries-source-tags)
|
|
691
|
+
const tags = (Array.isArray(statement.signal) && statement.signal.length > 0)
|
|
692
|
+
? statement.signal
|
|
693
|
+
: entry.tags;
|
|
694
|
+
// 304/409 on an existing destination (SPEC §copy): a re-copy that would write
|
|
695
|
+
// exactly what's already there — same channel contents, same tags — is a no-op
|
|
696
|
+
// (304), mirroring EDIT's 304-on-noop (§edit). A divergent destination is a real
|
|
697
|
+
// collision (409); COPY/MOVE never clobbers.
|
|
698
|
+
if (dstExisting !== null && dstExisting.status === 200 && dstExisting.entry !== null) {
|
|
699
|
+
const dstChannels = dstExisting.entry.channels;
|
|
700
|
+
const writeNames = Object.keys(channels).sort();
|
|
701
|
+
const dstNames = Object.keys(dstChannels).sort();
|
|
702
|
+
const sameContent = writeNames.length === dstNames.length
|
|
703
|
+
&& writeNames.every((n, i) => n === dstNames[i] && (channels[n]?.content ?? "") === (dstChannels[n]?.content ?? ""));
|
|
704
|
+
const sameTags = [...tags].sort().join("") === [...dstExisting.entry.tags].sort().join("");
|
|
705
|
+
if (sameContent && sameTags)
|
|
706
|
+
return { status: 304 }; // identical → §copy-noop-304
|
|
707
|
+
return { status: 409, error: `COPY/MOVE destination exists: ${dstSchemeName}://${dstPathname}` }; // §copy-conflict-409
|
|
708
|
+
}
|
|
709
|
+
const writeResult = await dstHandler.writeEntry(dstPathname, { channels, tags }, ctx);
|
|
710
|
+
// A file dest returns 202 (disk write → §membership review): propagate the
|
|
711
|
+
// proposal so dispatch runs the gate + routes applyResolution to the dest.
|
|
712
|
+
if (writeResult.status === 202)
|
|
713
|
+
return { status: 202, attrs: writeResult.attrs, body: writeResult.body };
|
|
714
|
+
return { status: writeResult.status, entryId: writeResult.entryId, created: writeResult.created };
|
|
715
|
+
}
|
|
716
|
+
// §send-premature-terminate — the unified PENDING SET, judged at the terminal's OWN dispatch
|
|
717
|
+
// (post-batch: the emission's earlier ops already executed, so a same-turn KILL+[200] repairs in
|
|
718
|
+
// ONE turn, and a same-turn WORK+[200] is caught — the spawn is live by the time the SEND lands).
|
|
719
|
+
// pending = open streams ∪ live children ∪ THIS turn's retrievals (READ/FIND/OPEN, results unseen
|
|
720
|
+
// until next packet). One rule, one steer, one repair family: nothing pending may be silently
|
|
721
|
+
// discarded; 499 discards BY STATED INTENT and is never gated.
|
|
722
|
+
async #pendingSet(runId, turnId) {
|
|
723
|
+
const pending = [];
|
|
724
|
+
const openSubs = await this.#db.find_open_subscriptions_for_run.all({ run_id: runId });
|
|
725
|
+
const execHandler = this.#schemes.get("exec");
|
|
726
|
+
if (openSubs.length > 0 || execHandler?.hasActiveSpawns?.(runId) === true)
|
|
727
|
+
pending.push("open streams");
|
|
728
|
+
const liveChild = await this.#db.engine_run_has_live_child.get({ run_id: runId });
|
|
729
|
+
if (liveChild !== undefined)
|
|
730
|
+
pending.push("live worker runs");
|
|
731
|
+
const retrievals = await this.#db.engine_turn_retrievals.all({ turn_id: turnId });
|
|
732
|
+
if (retrievals.length > 0)
|
|
733
|
+
pending.push("results of this turn's READ/FIND/OPEN (they arrive NEXT turn)");
|
|
734
|
+
// §send-undelivered-child-term — a worker that concluded DURING this turn's generation is no
|
|
735
|
+
// longer "live" but its deliverable hasn't reached any packet yet; concluding discards it.
|
|
736
|
+
const undelivered = await this.#db.engine_run_has_undelivered_child_term.get({ run_id: runId, turn_id: turnId });
|
|
737
|
+
if (undelivered !== undefined)
|
|
738
|
+
pending.push("worker results that arrived during this turn (they land NEXT turn)");
|
|
739
|
+
return pending;
|
|
740
|
+
}
|
|
741
|
+
// J — a live obligation to WAIT on: a spawned child or an open stream (NOT retrievals, which land
|
|
742
|
+
// next turn regardless). The wait-side twin of #pendingSet's stream+child legs (§wait-obligation-matrix).
|
|
743
|
+
async #hasLiveWork(runId) {
|
|
744
|
+
const openSubs = await this.#db.find_open_subscriptions_for_run.all({ run_id: runId });
|
|
745
|
+
if (openSubs.length > 0)
|
|
746
|
+
return true;
|
|
747
|
+
const execHandler = this.#schemes.get("exec");
|
|
748
|
+
if (execHandler?.hasActiveSpawns?.(runId) === true)
|
|
749
|
+
return true;
|
|
750
|
+
const liveChild = await this.#db.engine_run_has_live_child.get({ run_id: runId });
|
|
751
|
+
return liveChild !== undefined;
|
|
752
|
+
}
|
|
753
|
+
async #handleSendBroadcast(statement, ctx) {
|
|
754
|
+
if (statement.op !== "SEND")
|
|
755
|
+
throw new Error("unreachable");
|
|
756
|
+
const { runId, loopId, turnId } = ctx;
|
|
757
|
+
const status = statement.signal;
|
|
758
|
+
if (status === null)
|
|
759
|
+
return { status: 400 };
|
|
760
|
+
const raw = statement.body === null ? "" : typeof statement.body === "string" ? statement.body : statement.body.raw;
|
|
761
|
+
// [102]<T> — waiting is a mode of CONTINUING (grammar 0.75.0, the terminal redesign): park
|
|
762
|
+
// up to T seconds, woken early by any arrival (stream/child conclusion, irc, inject), woken
|
|
763
|
+
// at T regardless — a park ALWAYS has a next turn, so nothing can be orphaned. <-1> parks
|
|
764
|
+
// indefinitely (the butler/worker pattern — owner-ruled ungated; the instrumentation renders
|
|
765
|
+
// it legibly). Internally the parked state remains loops.status=202: the model-facing SIGNAL
|
|
766
|
+
// retired, the engine's park state did not.
|
|
767
|
+
// §join-blocking-collect (#354) — a READ(run://running-child) this turn armed a join. A BARE
|
|
768
|
+
// continue becomes an indefinite PARK: the blocking collect, on the same park machinery <-1>
|
|
769
|
+
// uses. The armed READ said "I want this result"; parking IS the collect (the model never had
|
|
770
|
+
// to know the park syntax — the engine holds the join). Any SEND clears the per-turn arm; a
|
|
771
|
+
// terminal with a live child stays the existing premature-terminate steer (#354 decision #1).
|
|
772
|
+
const joinArmed = this.#joinTargets.delete(loopId);
|
|
773
|
+
if (status === 102 && statement.lineMarker === null && joinArmed) {
|
|
774
|
+
await this.#db.engine_loop_set_status.run({ status: 202, loop_id: loopId, message: raw.length > 0 ? raw : "parked — awaiting a worker's result (blocking collect)" });
|
|
775
|
+
this.#parkDeadlines.set(loopId, -1); // indefinite: the bounded child's terminal is the wake edge
|
|
776
|
+
return { status: 102, attrs: { parked: -1, join: true } };
|
|
777
|
+
}
|
|
778
|
+
// §wait-obligation-matrix — the WAIT: SEND[202], and the legacy SEND[102]<T> the terminal
|
|
779
|
+
// redesign spelled the same park with, are one obligation-checked wait (waitpid). A live
|
|
780
|
+
// obligation (a spawned child or open stream, J) BLOCKS the loop until it concludes and
|
|
781
|
+
// reawakens it (§run-lifecycle-child-wake); a wait on nothing (∅) is already satisfied and
|
|
782
|
+
// resolves like 200, so <-1>+∅ self-resolves rather than hang the agent; a pending own
|
|
783
|
+
// retrieval (R) just lands next turn, so the wait continues.
|
|
784
|
+
if (status === 202 || (status === 102 && statement.lineMarker !== null)) {
|
|
785
|
+
const marks = statement.lineMarker?.marks[0];
|
|
786
|
+
const seconds = typeof marks === "number" ? marks : -1; // bare 202 / absent T = indefinite, bounded by the join
|
|
787
|
+
if (await this.#hasLiveWork(runId)) {
|
|
788
|
+
await this.#db.engine_loop_set_status.run({ status: 202, loop_id: loopId, message: raw.length > 0 ? raw : "waiting on live work" });
|
|
789
|
+
this.#parkDeadlines.set(loopId, seconds);
|
|
790
|
+
return { status: 202, attrs: { waiting: seconds } };
|
|
791
|
+
}
|
|
792
|
+
const retrievals = await this.#db.engine_turn_retrievals.all({ turn_id: turnId });
|
|
793
|
+
if (retrievals.length > 0)
|
|
794
|
+
return { status: 102 }; // R lands next turn — continue, don't conclude over it
|
|
795
|
+
// §send-undelivered-child-term — the fan-out race: a worker that concluded DURING this
|
|
796
|
+
// turn's generation is not live (J misses it), but its collect delta is queued for the
|
|
797
|
+
// NEXT build. The wait is NOT on nothing — the results are on the doorstep. Parking
|
|
798
|
+
// would hang (the wake edges already fired into an unparked run); R semantics: continue,
|
|
799
|
+
// the deltas land next turn, the model weighs them and concludes.
|
|
800
|
+
const undelivered = await this.#db.engine_run_has_undelivered_child_term.get({ run_id: runId, turn_id: turnId });
|
|
801
|
+
if (undelivered !== undefined)
|
|
802
|
+
return { status: 102 };
|
|
803
|
+
// ∅ — a wait on zero obligations is already satisfied; conclude like 200 (incl. <-1>+∅).
|
|
804
|
+
// #379 (owner ruling): the ENGINE concluded this loop, not the model — mark it as state
|
|
805
|
+
// so the COLLECT/delta render the act instead of presenting the park text as a result.
|
|
806
|
+
// The model's words (terminal_message) are never rewritten.
|
|
807
|
+
await this.#db.engine_loop_set_status.run({ status: 200, loop_id: loopId, message: raw === "" ? null : raw });
|
|
808
|
+
await this.#db.engine_loop_mark_terminated_by.run({ terminated_by: "collapse", loop_id: loopId });
|
|
809
|
+
return { status: 200 };
|
|
810
|
+
}
|
|
811
|
+
// §send-300-choices — ask the operator and park (the answer returns via loop.inject).
|
|
812
|
+
// The three-state cascade (owner design): PLURNK_QUESTIONS unset = ALLOWED (not enabled);
|
|
813
|
+
// =0 = DENIED servicewide (a ceiling the client cannot override); ENABLED requires the
|
|
814
|
+
// client to affirmatively pass it per session (settings.questions — the interactive client
|
|
815
|
+
// with a human enables its own sessions; headless/bench never asks). Enabled sessions ALSO
|
|
816
|
+
// get the questions.md teaching injected (docEntries) — capability and teaching gate as one.
|
|
817
|
+
// Disabled → refused with a self-decide steer, never a park into the void.
|
|
818
|
+
if (status === 300) {
|
|
819
|
+
if (!(await SessionSettings.questionsEnabled(this.#db, ctx.sessionId))) {
|
|
820
|
+
return { status: 409, error: "Operator asks ([300]) are not enabled in this environment — no one is watching to answer. Decide yourself and proceed: SEND[102] to continue, or [200] to conclude." };
|
|
821
|
+
}
|
|
822
|
+
// Owner ruling (#346): the question rides the SAME proposal system as file edits and
|
|
823
|
+
// MCP auths — stop the world. Returning 202 here routes through the proposal seam
|
|
824
|
+
// (#isProposal admits signal-300 SENDs): loop/proposal carries {question, choices},
|
|
825
|
+
// loop.resolve's accept body IS the answer, and applyResolution writes it into the
|
|
826
|
+
// model-facing rx — the answer arrives as the result of the ask itself. Timeout is
|
|
827
|
+
// the standard §proposal-timeout-cancels. Zero options = an open question.
|
|
828
|
+
const parts = raw.split(";").map((x) => x.trim()).filter((x) => x.length > 0);
|
|
829
|
+
const [question = "", ...choices] = parts;
|
|
830
|
+
return { status: 202, attrs: choices.length > 0 ? { question, choices } : { question } };
|
|
831
|
+
}
|
|
832
|
+
// [200] — terminate, gated by the pending set (post-batch). The row records the refused
|
|
833
|
+
// attempt faithfully (status_rx=409, never erased); the loop stays a continue; the strike
|
|
834
|
+
// couples in runTurn. [499] abandons regardless — discard by stated intent.
|
|
835
|
+
if (status === 200) {
|
|
836
|
+
// §send-200-failed-ops (#363, owner ruling: never 200 over a failed op) — the failure
|
|
837
|
+
// twin of the pending set: this turn's failed op results (and this emission's parse
|
|
838
|
+
// errors, threaded — they mint as rows only after dispatch) are UNSEEN until the next
|
|
839
|
+
// packet, so concluding over them is concluding blind. Refused 409; next turn, the
|
|
840
|
+
// errors in-log and weighed, [200] stands. [499] below is never gated — declaring
|
|
841
|
+
// failure IS weighing it.
|
|
842
|
+
const failedRows = await this.#db.engine_turn_failures.all({ turn_id: turnId });
|
|
843
|
+
const failCount = failedRows.length + (ctx.turnParseErrors ?? 0);
|
|
844
|
+
if (failCount > 0) {
|
|
845
|
+
return { status: 409, error: `Termination attempted despite ${failCount} failed operation(s) this turn. The errors land in your log next turn — weigh them, then conclude (or SEND[499] to abandon).` };
|
|
846
|
+
}
|
|
847
|
+
const pending = await this.#pendingSet(runId, turnId);
|
|
848
|
+
if (pending.length > 0) {
|
|
849
|
+
// Kind-specific steer (owner wording, xpath/topo forensics): retrievals-only is
|
|
850
|
+
// gemma's read-and-conclude idiom — no lever to pull, the results simply arrive;
|
|
851
|
+
// KILL/park advice only muddies it. Streams/children keep the remedy steer.
|
|
852
|
+
const retrievalsOnly = pending.every((k) => k.startsWith("results of this turn's"));
|
|
853
|
+
if (retrievalsOnly) {
|
|
854
|
+
// attrs.retrievalOnly — the strike decoupler (owner ruling): atomic-turn-
|
|
855
|
+
// pretrained models pair fetch-and-answer by habit; the refusal teaches,
|
|
856
|
+
// the strike executed. Engine reads this to skip the strike.
|
|
857
|
+
// #384 (owner wording) — the steer speaks PACKET-TIME, not dispatch-time: the
|
|
858
|
+
// model reads this 409 in the same packet where the retrieval results already
|
|
859
|
+
// sit, so it names their presence and the move (review, react) — never a
|
|
860
|
+
// future tense ("next turn" read as wait-more: a model parked on it).
|
|
861
|
+
return { status: 409, error: "Termination attempted despite this turn's retrieval operations. Their results are now in the log for review and reaction.", attrs: { retrievalOnly: true } };
|
|
862
|
+
}
|
|
863
|
+
return { status: 409, error: `Attempted [200] termination with pending work: ${pending.join("; ")}. KILL what you no longer need; SEND[102] (or [102]<seconds>) to receive the rest; then conclude.` };
|
|
864
|
+
}
|
|
865
|
+
await this.#db.engine_loop_set_status.run({ status: 200, loop_id: loopId, message: raw === "" ? null : raw });
|
|
866
|
+
return { status: 200 };
|
|
867
|
+
}
|
|
868
|
+
if (status === 499) {
|
|
869
|
+
await this.#db.engine_loop_set_status.run({ status: 499, loop_id: loopId, message: raw === "" ? null : raw });
|
|
870
|
+
return { status: 499 };
|
|
871
|
+
}
|
|
872
|
+
// Every other signal — 102 bare, 202 (retired as a terminal; now ordinary mid-comms), 1xx —
|
|
873
|
+
// is a plain broadcast row: no loop transition.
|
|
874
|
+
return { status };
|
|
875
|
+
}
|
|
876
|
+
async #run(schemeName, statement, ctx) {
|
|
877
|
+
if (schemeName === null)
|
|
878
|
+
return { status: 400 };
|
|
879
|
+
const handler = this.#schemes.get(schemeName);
|
|
880
|
+
if (handler === undefined)
|
|
881
|
+
return { status: 501 };
|
|
882
|
+
const methodName = statement.op.toLowerCase();
|
|
883
|
+
const method = handler[methodName];
|
|
884
|
+
if (typeof method !== "function")
|
|
885
|
+
return { status: 501 };
|
|
886
|
+
// External @plurnk/plurnk-schemes-* siblings receive the DB-free SchemeCtx
|
|
887
|
+
// (caps), never the raw PlurnkSchemeContext (schemes SPEC §channels). The dynamic
|
|
888
|
+
// dispatch is typed for in-tree schemes; the cast bridges the ctx shapes —
|
|
889
|
+
// the sibling reads caps, the in-tree handler reads db.
|
|
890
|
+
if (this.#schemes.isExternal(schemeName)) {
|
|
891
|
+
return method.call(handler, statement, new SchemeCtxImpl(ctx, schemeName));
|
|
892
|
+
}
|
|
893
|
+
return method.call(handler, statement, ctx);
|
|
894
|
+
}
|
|
895
|
+
// A status-202 result is a reviewable PROPOSAL (a side-effecting op — EDIT/EXEC/
|
|
896
|
+
// directed write — paused for client resolution) UNLESS it is a broadcast SEND.
|
|
897
|
+
// A broadcast SEND[202] is the model PARKING the loop (a terminal disposition,
|
|
898
|
+
// plurnk.md), never a side-effect — #255: gating the propose/await path on the
|
|
899
|
+
// bare 202 surfaced model speech as a loop/proposal and froze clients. The 202
|
|
900
|
+
// is overloaded (proposal-pause vs parked-terminal); the op disambiguates it.
|
|
901
|
+
static #isProposal(statement, result) {
|
|
902
|
+
// A broadcast SEND park is model speech, not a proposal (#255) — EXCEPT a [300] question,
|
|
903
|
+
// which IS a proposal by owner ruling (#346: the same stop-the-world system as file edits).
|
|
904
|
+
if (result.status !== 202)
|
|
905
|
+
return false;
|
|
906
|
+
if (statement.op === "SEND" && statement.signal === 300)
|
|
907
|
+
return true;
|
|
908
|
+
return !(statement.op === "SEND" && statement.target === null);
|
|
909
|
+
}
|
|
910
|
+
async #writeLog({ statement, result, runId, loopId, turnId, sequence, origin, }) {
|
|
911
|
+
const target = this.#extractTarget(statement.target);
|
|
912
|
+
const lineMarkerJson = "lineMarker" in statement && statement.lineMarker !== null
|
|
913
|
+
? JSON.stringify(statement.lineMarker)
|
|
914
|
+
: null;
|
|
915
|
+
// A proposal (status 202 from a side-effecting op) is written to the log in
|
|
916
|
+
// state='proposed' until the proposal lifecycle resolves it; attrs holds the
|
|
917
|
+
// scheme-supplied payload (file diff, exec command, etc.) the client renders
|
|
918
|
+
// for review and the scheme consumes on accept. A broadcast SEND[202] is a
|
|
919
|
+
// parked-terminal, NOT a proposal (#isProposal / #255) → state='resolved'.
|
|
920
|
+
const isProposed = _a.#isProposal(statement, result);
|
|
921
|
+
let attrsObj = (result.attrs !== undefined && result.attrs !== null)
|
|
922
|
+
? { ...result.attrs }
|
|
923
|
+
: {};
|
|
924
|
+
// EXEC produces a stream entry addressed by RUNTIME TAG as authority (§exec): it lives
|
|
925
|
+
// at <runtime>:///<loop_seq>/<turn_seq>/<sequence> (e.g. sh:///1/1/2). That address is a
|
|
926
|
+
// SEPARATE `stream` link in attrs — NOT an overload of `target`, which stays faithful to
|
|
927
|
+
// the EXEC's own slot (the cwd, or the path to the executable). The log:/// coordinate
|
|
928
|
+
// shares the trailing <loop>/<turn>/<seq>, so the op still correlates to its stream.
|
|
929
|
+
// Runtime comes from statement.signal (EXEC's runtime slot), resolvable for failed execs
|
|
930
|
+
// too; empty/absent = the default shell.
|
|
931
|
+
if (statement.op === "EXEC") {
|
|
932
|
+
const seqs = await this.#db.engine_loop_turn_seqs.get({
|
|
933
|
+
loop_id: loopId, turn_id: turnId,
|
|
934
|
+
});
|
|
935
|
+
if (seqs === undefined)
|
|
936
|
+
throw new Error(`Dispatcher.#writeLog: loop_turn_seqs returned no row for loop=${loopId} turn=${turnId}`);
|
|
937
|
+
const runtime = (typeof statement.signal === "string" && statement.signal.length > 0) ? statement.signal : "sh";
|
|
938
|
+
const coordPathname = `/${seqs.loop_seq}/${seqs.turn_seq}/${sequence}`;
|
|
939
|
+
attrsObj.pathname = coordPathname;
|
|
940
|
+
attrsObj.stream = `${runtime}://${coordPathname}`;
|
|
941
|
+
// Mutate the in-memory result.attrs too: the dispatch path
|
|
942
|
+
// hands originalResult.attrs to handler.applyResolution after
|
|
943
|
+
// proposal accept (see ProposalLifecycle.runApply). Both views —
|
|
944
|
+
// the stored row AND the in-memory proposal — need the same
|
|
945
|
+
// pathname so applyResolution writes the entry at the same URI.
|
|
946
|
+
if (result.attrs !== undefined && result.attrs !== null) {
|
|
947
|
+
result.attrs.pathname = coordPathname;
|
|
948
|
+
}
|
|
949
|
+
}
|
|
950
|
+
const attrs = JSON.stringify(attrsObj);
|
|
951
|
+
const txJson = JSON.stringify(statement);
|
|
952
|
+
const rxJson = JSON.stringify(result);
|
|
953
|
+
const row = await this.#db.engine_insert_log_entry.get({
|
|
954
|
+
run_id: runId,
|
|
955
|
+
loop_id: loopId,
|
|
956
|
+
turn_id: turnId,
|
|
957
|
+
sequence: sequence,
|
|
958
|
+
origin,
|
|
959
|
+
source: null, // dispatch entries are self-authored; §env-delta deltas set this
|
|
960
|
+
op: statement.op,
|
|
961
|
+
suffix: statement.suffix,
|
|
962
|
+
signal: this.#signalToJson(statement.signal),
|
|
963
|
+
scheme: target.scheme,
|
|
964
|
+
username: target.username,
|
|
965
|
+
password: target.password,
|
|
966
|
+
hostname: target.hostname,
|
|
967
|
+
port: target.port,
|
|
968
|
+
pathname: target.pathname,
|
|
969
|
+
params: target.params,
|
|
970
|
+
fragment: target.fragment,
|
|
971
|
+
lineMarker: lineMarkerJson,
|
|
972
|
+
tx: txJson,
|
|
973
|
+
mimetype_tx: "application/json",
|
|
974
|
+
rx: rxJson,
|
|
975
|
+
mimetype_rx: "application/json",
|
|
976
|
+
status_rx: result.status,
|
|
977
|
+
tokens: this.#tokenize(txJson) + this.#tokenize(rxJson),
|
|
978
|
+
state: isProposed ? "proposed" : "resolved",
|
|
979
|
+
outcome: null,
|
|
980
|
+
attrs,
|
|
981
|
+
});
|
|
982
|
+
if (row === undefined)
|
|
983
|
+
throw new Error("Dispatcher.#writeLog: INSERT ... RETURNING produced no row");
|
|
984
|
+
return row.id;
|
|
985
|
+
}
|
|
986
|
+
// Normalize a parsed path for storage. The `file` scheme is a routing
|
|
987
|
+
// internal — never stored, never rendered to the model. Both bare paths
|
|
988
|
+
// and `file:///...` inputs collapse to scheme=null at this boundary, so
|
|
989
|
+
// entries.scheme / log_entries.scheme never carry the string "file".
|
|
990
|
+
#extractTarget(path) {
|
|
991
|
+
if (path === null)
|
|
992
|
+
return { scheme: null, username: null, password: null, hostname: null, port: null, pathname: null, params: null, fragment: null };
|
|
993
|
+
// `local` (bare path) and `regex` (grammar 0.46 `#pattern#flags` target) carry no URL parts — store the raw text as the pathname for the log record, scheme=null.
|
|
994
|
+
if (path.kind === "regex")
|
|
995
|
+
return { scheme: null, username: null, password: null, hostname: null, port: null, pathname: path.raw, params: null, fragment: null }; // regex source — no decode
|
|
996
|
+
if (path.kind === "local")
|
|
997
|
+
return { scheme: null, username: null, password: null, hostname: null, port: null, pathname: decodePathParens(path.raw), params: null, fragment: null }; // #239 item 4
|
|
998
|
+
const scheme = path.scheme === "file" ? null : path.scheme;
|
|
999
|
+
// Every registered (plurnk-namespace) scheme uses its authority as a namespace segment — fold
|
|
1000
|
+
// it into the canonical pathname so known://x ≡ known:///x ≡ /x and the log keys identically to
|
|
1001
|
+
// the entry (/prompt/<run>/<loop>/<N>, /docs/x.md). A foreign web host (http://, unregistered) is NOT a
|
|
1002
|
+
// namespace: keep it in hostname. run:// is the one registered EXCEPTION — its authority IS the
|
|
1003
|
+
// run selector (§run-scheme), and run://self must stay distinct from run://name, so Run.ts
|
|
1004
|
+
// folds the owner into the storage path itself, never here.
|
|
1005
|
+
const foldNs = scheme !== null && scheme !== "run" && this.#schemes.has(scheme);
|
|
1006
|
+
return {
|
|
1007
|
+
scheme, username: path.username, password: path.password,
|
|
1008
|
+
hostname: foldNs ? null : path.hostname, port: path.port,
|
|
1009
|
+
pathname: decodePathParens(foldNs ? foldAuthorityIntoPath(path.hostname, path.pathname) : path.pathname), // #239 item 4
|
|
1010
|
+
params: JSON.stringify(path.params), fragment: path.fragment,
|
|
1011
|
+
};
|
|
1012
|
+
}
|
|
1013
|
+
#signalToJson(signal) {
|
|
1014
|
+
if (signal === null || signal === undefined)
|
|
1015
|
+
return null;
|
|
1016
|
+
return JSON.stringify(signal);
|
|
1017
|
+
}
|
|
1018
|
+
}
|
|
1019
|
+
_a = Dispatcher;
|
|
1020
|
+
export default Dispatcher;
|
|
1021
|
+
//# sourceMappingURL=Dispatcher.js.map
|