@plurnk/plurnk-service 0.26.0 → 0.27.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/SPEC.md +36 -34
- package/dist/Paths.d.ts +0 -1
- package/dist/Paths.d.ts.map +1 -1
- package/dist/Paths.js +3 -6
- package/dist/Paths.js.map +1 -1
- package/dist/content/path-mimetype.js +2 -2
- package/dist/content/path-mimetype.js.map +1 -1
- package/dist/core/ChannelWrite.d.ts +17 -2
- package/dist/core/ChannelWrite.d.ts.map +1 -1
- package/dist/core/ChannelWrite.js +11 -5
- package/dist/core/ChannelWrite.js.map +1 -1
- package/dist/core/Engine.d.ts +4 -5
- package/dist/core/Engine.d.ts.map +1 -1
- package/dist/core/Engine.js +113 -49
- package/dist/core/Engine.js.map +1 -1
- package/dist/core/ExecutorRegistry.d.ts +3 -0
- package/dist/core/ExecutorRegistry.d.ts.map +1 -1
- package/dist/core/ExecutorRegistry.js +8 -1
- package/dist/core/ExecutorRegistry.js.map +1 -1
- package/dist/core/SchemeRegistry.d.ts.map +1 -1
- package/dist/core/SchemeRegistry.js +39 -15
- package/dist/core/SchemeRegistry.js.map +1 -1
- package/dist/core/caps/DbNotifyCaps.d.ts.map +1 -1
- package/dist/core/caps/DbNotifyCaps.js +4 -1
- package/dist/core/caps/DbNotifyCaps.js.map +1 -1
- package/dist/core/caps/DbSubscriptionCaps.d.ts +1 -1
- package/dist/core/caps/DbSubscriptionCaps.d.ts.map +1 -1
- package/dist/core/caps/DbSubscriptionCaps.js +2 -2
- package/dist/core/caps/DbSubscriptionCaps.js.map +1 -1
- package/dist/core/git-membership.d.ts.map +1 -1
- package/dist/core/git-membership.js +51 -25
- package/dist/core/git-membership.js.map +1 -1
- package/dist/core/packet-wire.js +5 -5
- package/dist/core/packet-wire.js.map +1 -1
- package/dist/core/plugin-trust.d.ts +4 -0
- package/dist/core/plugin-trust.d.ts.map +1 -0
- package/dist/core/plugin-trust.js +23 -0
- package/dist/core/plugin-trust.js.map +1 -0
- package/dist/schemes/Exec.d.ts.map +1 -1
- package/dist/schemes/Exec.js +78 -20
- package/dist/schemes/Exec.js.map +1 -1
- package/dist/schemes/File.d.ts.map +1 -1
- package/dist/schemes/File.js +15 -12
- package/dist/schemes/File.js.map +1 -1
- package/dist/schemes/Log.js +6 -6
- package/dist/schemes/Log.js.map +1 -1
- package/dist/schemes/Plurnk.js +4 -4
- package/dist/schemes/Plurnk.js.map +1 -1
- 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-find.d.ts.map +1 -1
- package/dist/schemes/_entry-find.js +6 -2
- package/dist/schemes/_entry-find.js.map +1 -1
- package/dist/schemes/_entry-graph.js +1 -1
- package/dist/schemes/_entry-graph.js.map +1 -1
- package/dist/schemes/_entry-manifest.d.ts.map +1 -1
- package/dist/schemes/_entry-manifest.js +14 -6
- package/dist/schemes/_entry-manifest.js.map +1 -1
- package/dist/schemes/_entry-ops.js +2 -2
- package/dist/schemes/_entry-ops.js.map +1 -1
- package/dist/schemes/_entry-semantic.d.ts +22 -2
- package/dist/schemes/_entry-semantic.d.ts.map +1 -1
- package/dist/schemes/_entry-semantic.js +96 -9
- package/dist/schemes/_entry-semantic.js.map +1 -1
- 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 +29 -0
- package/dist/schemes/exec-env.js.map +1 -0
- package/dist/server/Daemon.d.ts +1 -1
- package/dist/server/Daemon.d.ts.map +1 -1
- package/dist/server/Daemon.js +11 -1
- package/dist/server/Daemon.js.map +1 -1
- package/dist/server/methods/entry_read.d.ts.map +1 -1
- package/dist/server/methods/entry_read.js +29 -7
- package/dist/server/methods/entry_read.js.map +1 -1
- package/dist/server/methods/loop_inject.d.ts +5 -0
- package/dist/server/methods/loop_inject.d.ts.map +1 -0
- package/dist/server/methods/loop_inject.js +58 -0
- package/dist/server/methods/loop_inject.js.map +1 -0
- package/dist/server/methods/loop_run.js +2 -2
- package/dist/server/methods/loop_run.js.map +1 -1
- package/dist/server/methods/op_edit.js +1 -1
- package/dist/server/methods/op_edit.js.map +1 -1
- package/dist/server/methods/op_find.js +1 -1
- package/dist/server/methods/op_find.js.map +1 -1
- package/dist/server/methods/run_fork.d.ts +5 -0
- package/dist/server/methods/run_fork.d.ts.map +1 -0
- package/dist/server/methods/run_fork.js +42 -0
- package/dist/server/methods/run_fork.js.map +1 -0
- package/dist/server/methods/session_create.d.ts +1 -0
- package/dist/server/methods/session_create.d.ts.map +1 -1
- package/dist/server/methods/session_create.js +32 -0
- package/dist/server/methods/session_create.js.map +1 -1
- package/migrations/0000-00-00.01_schema.sql +22 -10
- package/package.json +108 -111
- package/requirements.md +1 -2
package/dist/core/Engine.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
var _a;
|
|
2
|
-
import { PlurnkParser, PlurnkParseError } from "@plurnk/plurnk-grammar";
|
|
2
|
+
import { PlurnkParser, PlurnkParseError, parsePath } from "@plurnk/plurnk-grammar";
|
|
3
3
|
import { Mimetypes, emptyRegistry } from "@plurnk/plurnk-mimetypes";
|
|
4
4
|
import EntryCrud from "../schemes/_entry-crud.js";
|
|
5
5
|
import EntryManifest from "../schemes/_entry-manifest.js";
|
|
@@ -8,6 +8,8 @@ import GitState from "./git-state.js";
|
|
|
8
8
|
import { DEFAULT_LOOP_FLAGS } from "./scheme-types.js";
|
|
9
9
|
import { LineMarkerOps, MimetypeBinary, editedSpan } from "../content/index.js";
|
|
10
10
|
import { readFile } from "node:fs/promises";
|
|
11
|
+
import { fileURLToPath } from "node:url";
|
|
12
|
+
import { setTimeout as delay } from "node:timers/promises";
|
|
11
13
|
import Paths from "../Paths.js";
|
|
12
14
|
import SchemeCtxImpl from "./caps/SchemeCtxImpl.js";
|
|
13
15
|
// Shared module imported by both Engine and bin/digest.ts, so wire
|
|
@@ -275,9 +277,20 @@ class Engine {
|
|
|
275
277
|
// attaches it iff the backend supports it and silently drops it otherwise —
|
|
276
278
|
// capability is providers' concern, not ours. Pure plumbing grammar→provider.
|
|
277
279
|
async #grammarConstraint() {
|
|
278
|
-
|
|
280
|
+
// PLURNK_PROVIDERS_GBNF SELECTS the GBNF variant to constrain sampling to (#225):
|
|
281
|
+
// a bare name (`plurnk-strict.gbnf` | `plurnk.gbnf`) is a variant shipped by
|
|
282
|
+
// @plurnk/plurnk-grammar; an absolute/relative path is a BYO grammar. Empty or "0"
|
|
283
|
+
// disables — unconstrained generation. (Was a `=== "1"` boolean; the value change
|
|
284
|
+
// would otherwise read as "off", leaving every turn unconstrained.)
|
|
285
|
+
const variant = process.env.PLURNK_PROVIDERS_GBNF;
|
|
286
|
+
if (variant === undefined || variant === "" || variant === "0")
|
|
279
287
|
return undefined;
|
|
280
|
-
this.#gbnfCache
|
|
288
|
+
if (this.#gbnfCache === null) {
|
|
289
|
+
const path = variant.startsWith("/") || variant.startsWith(".")
|
|
290
|
+
? variant
|
|
291
|
+
: fileURLToPath(import.meta.resolve(`@plurnk/plurnk-grammar/${variant}`));
|
|
292
|
+
this.#gbnfCache = await readFile(path, "utf8");
|
|
293
|
+
}
|
|
281
294
|
return this.#gbnfCache;
|
|
282
295
|
}
|
|
283
296
|
// Per-loop usage totals (#197): SUM the loop's turns (usage is stored per
|
|
@@ -376,6 +389,16 @@ class Engine {
|
|
|
376
389
|
cleanup("forceful", "max_turns");
|
|
377
390
|
return { turnIds, finalStatus: 499, hitMaxTurns: true, reason: "max_turns" };
|
|
378
391
|
}
|
|
392
|
+
// PLURNK_EXEC_WAIT_MS — a post-EXEC breath: if a spawn from the prior turn
|
|
393
|
+
// is still in flight, give it a tunable beat to land in THIS turn's packet
|
|
394
|
+
// before we assemble it. A fixed grace beat, never a wait-for-completion;
|
|
395
|
+
// 0/unset = off. Abortable with the loop signal.
|
|
396
|
+
const execWaitMs = Number(process.env.PLURNK_EXEC_WAIT_MS ?? "0");
|
|
397
|
+
if (execWaitMs > 0) {
|
|
398
|
+
const execHandler = this.#schemes.get("exec");
|
|
399
|
+
if (execHandler?.hasActiveSpawns?.(runId) === true)
|
|
400
|
+
await delay(execWaitMs, undefined, { signal });
|
|
401
|
+
}
|
|
379
402
|
const turn = await this.runTurn({
|
|
380
403
|
provider, messages, persona, requirements, sessionId, runId, loopId, origin, signal, onDispatch,
|
|
381
404
|
turnNumber: turnIds.length + 1, maxTurns,
|
|
@@ -462,7 +485,7 @@ class Engine {
|
|
|
462
485
|
throw new Error("Engine.runTurn: turn open returned no row");
|
|
463
486
|
const turnId = openRow.id;
|
|
464
487
|
// Pre-model writes. Each turn opens with a system-origin EDIT
|
|
465
|
-
// against `plurnk
|
|
488
|
+
// against `plurnk:///prompt/<loop_id>/<seq>` IF there's a prompt
|
|
466
489
|
// for THIS turn the model hasn't seen yet:
|
|
467
490
|
// - Turn 1: loop.prompt is the initial user prompt.
|
|
468
491
|
// - Turn N>1: only if Engine.inject (or wake-on-completion via
|
|
@@ -475,15 +498,15 @@ class Engine {
|
|
|
475
498
|
let nextActionIndex = 1;
|
|
476
499
|
if (seq === 1) {
|
|
477
500
|
// Operator doc READs (PLURNK_MD_<ALIAS>). The docs were materialized
|
|
478
|
-
// as plurnk
|
|
501
|
+
// as plurnk:///<entry> entries by the plurnk run (loop_run, via the
|
|
479
502
|
// §actor-boundary keystone); foist a READ of each into THIS turn-0 so the model
|
|
480
503
|
// reads them inline. It sees only the READ — the materializing EDIT
|
|
481
504
|
// lives in the plurnk run's log, never the model's.
|
|
482
505
|
for (const doc of Paths.docs()) {
|
|
483
506
|
const docTarget = {
|
|
484
|
-
kind: "url", raw: `plurnk
|
|
507
|
+
kind: "url", raw: `plurnk:///${doc.entryName}`, scheme: "plurnk",
|
|
485
508
|
username: null, password: null, hostname: null, port: null,
|
|
486
|
-
pathname: doc.entryName
|
|
509
|
+
pathname: `/${doc.entryName}`, params: {}, fragment: null,
|
|
487
510
|
};
|
|
488
511
|
const docRead = {
|
|
489
512
|
op: "READ", suffix: "", signal: null, target: docTarget,
|
|
@@ -498,10 +521,10 @@ class Engine {
|
|
|
498
521
|
const promptRow = await this.#db.engine_get_loop_prompt.get({ loop_id: loopId });
|
|
499
522
|
if (promptRow !== undefined && typeof promptRow.prompt === "string" && promptRow.prompt.length > 0) {
|
|
500
523
|
const promptPath = {
|
|
501
|
-
kind: "url", raw: `plurnk
|
|
524
|
+
kind: "url", raw: `plurnk:///prompt/${loopId}/${seq}`,
|
|
502
525
|
scheme: "plurnk", username: null, password: null,
|
|
503
526
|
hostname: null, port: null,
|
|
504
|
-
pathname:
|
|
527
|
+
pathname: `/prompt/${loopId}/${seq}`, params: {}, fragment: null,
|
|
505
528
|
};
|
|
506
529
|
const promptStmt = {
|
|
507
530
|
op: "EDIT", suffix: "", signal: null,
|
|
@@ -515,7 +538,7 @@ class Engine {
|
|
|
515
538
|
nextActionIndex++;
|
|
516
539
|
}
|
|
517
540
|
}
|
|
518
|
-
// plurnk
|
|
541
|
+
// plurnk:///manifest.json — rewritten EVERY turn (a live view of the
|
|
519
542
|
// entry set, which changes each turn). A derived view like the index,
|
|
520
543
|
// NOT an action — written directly (Engine.inject's path): no log entry,
|
|
521
544
|
// no sequence slot, not dispatched. The catalog body is built in the
|
|
@@ -540,7 +563,7 @@ class Engine {
|
|
|
540
563
|
// this turn's packet reflects them.
|
|
541
564
|
const fsDivergences = await GitMembership.indexGitMembership(systemCtx);
|
|
542
565
|
await this.#logFsFictions(sessionId, fsDivergences);
|
|
543
|
-
await EntryCrud.writeEntry("manifest.json", {
|
|
566
|
+
await EntryCrud.writeEntry("/manifest.json", {
|
|
544
567
|
channels: { body: { content: await EntryManifest.buildManifestBody(systemCtx), mimetype: "application/json" } },
|
|
545
568
|
tags: [],
|
|
546
569
|
}, systemCtx, "plurnk");
|
|
@@ -555,9 +578,9 @@ class Engine {
|
|
|
555
578
|
const manifestRead = {
|
|
556
579
|
op: "READ", suffix: "", signal: null, lineMarker: null,
|
|
557
580
|
target: {
|
|
558
|
-
kind: "url", raw: "plurnk
|
|
581
|
+
kind: "url", raw: "plurnk:///manifest.json", scheme: "plurnk",
|
|
559
582
|
username: null, password: null, hostname: null, port: null,
|
|
560
|
-
pathname: "manifest.json", params: {}, fragment: null,
|
|
583
|
+
pathname: "/manifest.json", params: {}, fragment: null,
|
|
561
584
|
},
|
|
562
585
|
body: manifestItems < 0 ? null : { dialect: "jsonpath", raw: `$[0:${manifestItems}]` },
|
|
563
586
|
position: { line: 1, column: 1 },
|
|
@@ -641,13 +664,17 @@ class Engine {
|
|
|
641
664
|
});
|
|
642
665
|
}
|
|
643
666
|
const opsCount = packetAssistant.ops.length;
|
|
644
|
-
|
|
667
|
+
// Informational SEND[103] broadcasts (#free-text-capture) are log rows, not
|
|
668
|
+
// actions: excluded from the real-op count so a prose-only turn still strikes
|
|
669
|
+
// as no-ops, and the terminal scan ignores 1xx so they never set turnStatus.
|
|
670
|
+
const realOpsCount = packetAssistant.ops.filter((op) => !(op.op === "SEND" && op.signal === 103 && op.target === null)).length;
|
|
671
|
+
const sendOp = packetAssistant.ops.findLast((op) => op.op === "SEND" && typeof op.signal === "number" && op.signal >= 200);
|
|
645
672
|
// Rail #41 (revised): the per-turn requirement is "emit at least one
|
|
646
673
|
// op," not "emit a terminal SEND." SEND is purely a signal verb; many
|
|
647
674
|
// turns may pass without one. An empty op list is the only strike.
|
|
648
675
|
const turnStatus = sendOp !== undefined
|
|
649
676
|
? sendOp.signal
|
|
650
|
-
:
|
|
677
|
+
: realOpsCount === 0 ? TURN_STATUS_NO_OPS : TURN_STATUS_IMPLICIT_CONTINUE;
|
|
651
678
|
// Close the turn with the final packet, status, and usage stats.
|
|
652
679
|
const packet = this.#completePacket(requestPacket, packetAssistant, response.assistantRaw, provider);
|
|
653
680
|
const { usage, finishReason, model } = callMetadata;
|
|
@@ -717,24 +744,45 @@ class Engine {
|
|
|
717
744
|
const { assistant } = response;
|
|
718
745
|
const preParsedOps = assistant.ops;
|
|
719
746
|
const ops = [];
|
|
720
|
-
|
|
747
|
+
// #plan-reasoning: PLAN bodies are the model's in-band reasoning — hoisted
|
|
748
|
+
// out of the op stream into the reasoning field, never dispatched as a log op.
|
|
749
|
+
// Free text becomes informational SEND[103] log ops (#free-text-capture) below.
|
|
750
|
+
const planFragments = [];
|
|
751
|
+
const hoistPlan = (op) => {
|
|
752
|
+
if (op.op !== "PLAN")
|
|
753
|
+
return false;
|
|
754
|
+
const raw = (op.body ?? "").trim();
|
|
755
|
+
if (raw.length > 0)
|
|
756
|
+
planFragments.push(raw);
|
|
757
|
+
return true;
|
|
758
|
+
};
|
|
721
759
|
// Full PlurnkParseError context (line/column/source) is preserved
|
|
722
760
|
// here so runTurn can build TelemetryEvent envelopes per the
|
|
723
761
|
// grammar 0.17.0 protocol — model needs position info to locate
|
|
724
762
|
// its own offending content on the next turn.
|
|
725
763
|
const parseErrors = [];
|
|
726
764
|
if (preParsedOps !== undefined) {
|
|
727
|
-
|
|
765
|
+
for (const op of preParsedOps)
|
|
766
|
+
if (!hoistPlan(op))
|
|
767
|
+
ops.push(op);
|
|
728
768
|
}
|
|
729
769
|
else {
|
|
730
770
|
const parsed = PlurnkParser.parse(assistant.content);
|
|
731
771
|
for (const item of parsed.items) {
|
|
732
|
-
if (item.kind === "statement")
|
|
733
|
-
|
|
772
|
+
if (item.kind === "statement") {
|
|
773
|
+
if (!hoistPlan(item.statement))
|
|
774
|
+
ops.push(item.statement);
|
|
775
|
+
}
|
|
734
776
|
else if (item.kind === "text") {
|
|
777
|
+
// #free-text-capture: interstitial prose → an informational
|
|
778
|
+
// SEND[103] broadcast log op, interleaved in emission order.
|
|
735
779
|
const trimmed = item.text.trim();
|
|
736
780
|
if (trimmed.length > 0)
|
|
737
|
-
|
|
781
|
+
ops.push({
|
|
782
|
+
op: "SEND", suffix: "", signal: 103, target: null,
|
|
783
|
+
lineMarker: null, body: { raw: trimmed, json: null },
|
|
784
|
+
position: item.position ?? { line: 1, column: 1 },
|
|
785
|
+
});
|
|
738
786
|
}
|
|
739
787
|
else if (item.kind === "error") {
|
|
740
788
|
const err = item.error;
|
|
@@ -759,8 +807,8 @@ class Engine {
|
|
|
759
807
|
}
|
|
760
808
|
}
|
|
761
809
|
const wireReasoning = assistant.reasoning ?? "";
|
|
762
|
-
const
|
|
763
|
-
const reasoningParts = [wireReasoning,
|
|
810
|
+
const planReasoning = planFragments.join("\n\n");
|
|
811
|
+
const reasoningParts = [wireReasoning, planReasoning].filter((s) => s.length > 0);
|
|
764
812
|
const reasoning = reasoningParts.length > 0 ? reasoningParts.join("\n\n") : null;
|
|
765
813
|
return {
|
|
766
814
|
packetAssistant: { content: assistant.content, ops, reasoning },
|
|
@@ -776,7 +824,7 @@ class Engine {
|
|
|
776
824
|
const byRole = (role) => initialMessages.filter((m) => m.role === role).map((m) => m.content).join("\n\n");
|
|
777
825
|
const system_definition = byRole("system");
|
|
778
826
|
// user.prompt sources from the loop's most recent prompt entry first
|
|
779
|
-
// (plurnk
|
|
827
|
+
// (plurnk:///prompt/<loop_id>/<N> for the highest N written to date).
|
|
780
828
|
// This is what inject + the turn-1 foist write into. Falls back to
|
|
781
829
|
// the runLoop caller's messages.user for tests that bypass the
|
|
782
830
|
// foist mechanism entirely.
|
|
@@ -861,12 +909,26 @@ class Engine {
|
|
|
861
909
|
// The # Plurnk System Tools capability sheet (SPEC §tools). A hook: each
|
|
862
910
|
// enabled capability contributes one line, rendered above Requirements so
|
|
863
911
|
// the model sees what it can do before the rules. PLAN is the first
|
|
864
|
-
// contributor (gated by PLURNK_PLAN);
|
|
865
|
-
//
|
|
912
|
+
// contributor (gated by PLURNK_PLAN); each available executor tag then
|
|
913
|
+
// contributes its self-documenting example (plurnk-execs#7), retiring the
|
|
914
|
+
// blind EXEC.
|
|
866
915
|
#collectTools() {
|
|
867
916
|
const tools = [];
|
|
868
917
|
if (process.env.PLURNK_PLAN === "1") {
|
|
869
|
-
tools.push("
|
|
918
|
+
tools.push("* Begin every response with <<PLAN:{planning and reasoning}:PLAN");
|
|
919
|
+
}
|
|
920
|
+
// Each available runtime tag contributes its self-documenting example —
|
|
921
|
+
// the example carries syntax + purpose, so there's no prose line. Tags
|
|
922
|
+
// with no example (sh/node, covered by the core prompt) contribute
|
|
923
|
+
// nothing; available-only, so the model never sees an unusable tag. `* `
|
|
924
|
+
// bullets + bare op forms match the packet's list/op rendering (no `- `,
|
|
925
|
+
// no backticks — see packet-wire.ts).
|
|
926
|
+
if (this.#executors !== undefined) {
|
|
927
|
+
for (const tag of this.#executors.availableRuntimes()) {
|
|
928
|
+
const example = this.#executors.entry(tag)?.example;
|
|
929
|
+
if (example)
|
|
930
|
+
tools.push(`* ${example}`);
|
|
931
|
+
}
|
|
870
932
|
}
|
|
871
933
|
return tools;
|
|
872
934
|
}
|
|
@@ -965,7 +1027,7 @@ class Engine {
|
|
|
965
1027
|
// SPEC §packet packet.system.log — chronological action-entries for the loop.
|
|
966
1028
|
// Snapshot is taken at packet build (pre-dispatch this turn), so it
|
|
967
1029
|
// reflects "what has happened before this turn." Each row carries a
|
|
968
|
-
// log
|
|
1030
|
+
// log:///<loop_seq>/<turn_seq>/<sequence> coordinate the model can READ.
|
|
969
1031
|
async #buildLog(runId) {
|
|
970
1032
|
// SPEC §packet-terms: runs own log entries — log is the run's history,
|
|
971
1033
|
// not the loop's. Span all loops in the run so the model sees
|
|
@@ -1272,7 +1334,7 @@ class Engine {
|
|
|
1272
1334
|
return (row?.n ?? 0) > 0;
|
|
1273
1335
|
}
|
|
1274
1336
|
// Inject a prompt into the run's currently-executing loop. Writes a
|
|
1275
|
-
// plurnk
|
|
1337
|
+
// plurnk:///prompt/<loop_id>/<next-turn> entry whose body becomes
|
|
1276
1338
|
// packet.user.prompt at the next turn boundary. Last-wins: if two
|
|
1277
1339
|
// injects target the same next-turn slot, the second overwrites the
|
|
1278
1340
|
// first.
|
|
@@ -1401,7 +1463,8 @@ class Engine {
|
|
|
1401
1463
|
return this.#denyIfDisallowed("exec", origin);
|
|
1402
1464
|
}
|
|
1403
1465
|
if (statement.op === "COPY" || statement.op === "MOVE") {
|
|
1404
|
-
const
|
|
1466
|
+
const dst = statement.op === "COPY" ? (statement.body === null ? null : parsePath(statement.body)) : statement.body;
|
|
1467
|
+
const dstScheme = this.#schemeNameOf(dst);
|
|
1405
1468
|
const dstDenial = this.#denyIfDisallowed(dstScheme, origin);
|
|
1406
1469
|
if (dstDenial !== null)
|
|
1407
1470
|
return dstDenial;
|
|
@@ -1454,7 +1517,7 @@ class Engine {
|
|
|
1454
1517
|
return { status: 403, error: `scheme '${scheme}' is inactive under current loop flags` };
|
|
1455
1518
|
};
|
|
1456
1519
|
if (statement.op === "COPY" || statement.op === "MOVE") {
|
|
1457
|
-
return check(statement.target) ?? check(statement.body);
|
|
1520
|
+
return check(statement.target) ?? check(statement.op === "COPY" ? (statement.body === null ? null : parsePath(statement.body)) : statement.body);
|
|
1458
1521
|
}
|
|
1459
1522
|
return check(statement.target);
|
|
1460
1523
|
}
|
|
@@ -1462,11 +1525,14 @@ class Engine {
|
|
|
1462
1525
|
if (statement.op !== "COPY")
|
|
1463
1526
|
throw new Error("unreachable");
|
|
1464
1527
|
const srcPath = statement.target;
|
|
1465
|
-
|
|
1528
|
+
// COPY's body is an opaque raw string (grammar §COPY: a dest path OR a run-fork
|
|
1529
|
+
// prompt); parse it to the dest path. Non-path bodies (run:// fork prompts) are
|
|
1530
|
+
// not yet handled and surface as a 400.
|
|
1531
|
+
const dstPath = statement.body === null ? null : parsePath(statement.body);
|
|
1466
1532
|
if (srcPath === null)
|
|
1467
1533
|
return { status: 400, error: "COPY requires source path" };
|
|
1468
1534
|
if (dstPath === null)
|
|
1469
|
-
return { status: 400, error: "COPY
|
|
1535
|
+
return { status: 400, error: "COPY destination must be a parseable path in the body slot" };
|
|
1470
1536
|
return await this.#copyOrchestration({ statement, srcPath, dstPath, ctx });
|
|
1471
1537
|
}
|
|
1472
1538
|
async #handleMove(statement, ctx) {
|
|
@@ -1505,12 +1571,12 @@ class Engine {
|
|
|
1505
1571
|
}
|
|
1506
1572
|
// KILL — scheme-polymorphic destroy (plurnk-grammar#203 / 0.28.0). Entry-KILL
|
|
1507
1573
|
// permanently deletes the entry: the canonical delete now, MOVE→/dev/null
|
|
1508
|
-
// retired from the model's vocabulary. Process-KILL (exec
|
|
1574
|
+
// retired from the model's vocabulary. Process-KILL (exec:///) aborts the
|
|
1509
1575
|
// running spawn's controller (the same teardown loop.cancel rides), addressed
|
|
1510
1576
|
// by coordinate pathname (#203). The KILL body is an opaque
|
|
1511
1577
|
// annotation with no runtime meaning; it survives into the log row's tx for
|
|
1512
1578
|
// free via the statement serialization. Status: 200 killed · 404 unknown ·
|
|
1513
|
-
// 405 log
|
|
1579
|
+
// 405 log:/// (append-only) · 403 writableBy (the #checkWritable gate, KILL ∈
|
|
1514
1580
|
// MUTATING_OPS) · 200/410/304/404 exec (killed / killed-earlier / exited / unknown) · 501 no-kill/delete scheme.
|
|
1515
1581
|
async #handleKill(statement, ctx) {
|
|
1516
1582
|
if (statement.op !== "KILL")
|
|
@@ -1522,7 +1588,7 @@ class Engine {
|
|
|
1522
1588
|
if (schemeName === null)
|
|
1523
1589
|
return { status: 400, error: "KILL target must be a URL path with a scheme" };
|
|
1524
1590
|
if (schemeName === "log")
|
|
1525
|
-
return { status: 405, error: "log
|
|
1591
|
+
return { status: 405, error: "log:/// is append-only; KILL must bounce" };
|
|
1526
1592
|
if (schemeName === "exec") {
|
|
1527
1593
|
const execHandler = this.#schemes.get("exec");
|
|
1528
1594
|
if (execHandler === undefined || typeof execHandler.kill !== "function")
|
|
@@ -1535,14 +1601,12 @@ class Engine {
|
|
|
1535
1601
|
const delResult = await handler.deleteEntry(pathnameFromPath(path), ctx);
|
|
1536
1602
|
return { status: delResult.status };
|
|
1537
1603
|
}
|
|
1538
|
-
// PLAN —
|
|
1539
|
-
//
|
|
1540
|
-
//
|
|
1541
|
-
//
|
|
1542
|
-
//
|
|
1543
|
-
//
|
|
1544
|
-
// emits it; this handler exists only so the op resolves cleanly if we ever
|
|
1545
|
-
// deliberately reach for it (which would also require a plurnk.md override).
|
|
1604
|
+
// PLAN — the model's in-band reasoning op (plurnk-grammar 0.30.0, the 11th op).
|
|
1605
|
+
// Production HOISTS PLAN bodies into the turn's reasoning field in #parseAssistant
|
|
1606
|
+
// (#plan-reasoning) before dispatch runs, so PLAN normally never reaches here. This
|
|
1607
|
+
// handler is the op's direct-dispatch semantics (exercised by the dispatch unit
|
|
1608
|
+
// test): a pure no-op (PLAN ∉ MUTATING_OPS) whose body would serialize into the log
|
|
1609
|
+
// row's tx, no runtime effect.
|
|
1546
1610
|
#handlePlan(statement) {
|
|
1547
1611
|
if (statement.op !== "PLAN")
|
|
1548
1612
|
throw new Error("unreachable");
|
|
@@ -1657,7 +1721,7 @@ class Engine {
|
|
|
1657
1721
|
}
|
|
1658
1722
|
// Bare paths default to the file scheme per plurnk.md (grammar sysprompt):
|
|
1659
1723
|
// "Bare paths (no scheme) default to local relative project file paths."
|
|
1660
|
-
// file
|
|
1724
|
+
// file:/// remains an optional explicit form for absolute paths.
|
|
1661
1725
|
#schemeNameOf(path) {
|
|
1662
1726
|
if (path === null)
|
|
1663
1727
|
return null;
|
|
@@ -1682,11 +1746,11 @@ class Engine {
|
|
|
1682
1746
|
? { ...result.attrs }
|
|
1683
1747
|
: {};
|
|
1684
1748
|
// EXEC pathname is executor-domain + coordinate: the stream entry
|
|
1685
|
-
// lives at exec
|
|
1686
|
-
// exec
|
|
1749
|
+
// lives at exec:///<runtime>/<loop_seq>/<turn_seq>/<sequence> (e.g.
|
|
1750
|
+
// exec:///sh/1/1/2). The runtime leads — domain-aware, the executor
|
|
1687
1751
|
// as authority — and the coordinate that follows is already unique
|
|
1688
1752
|
// per statement, so no slug is injected. The log row's target points
|
|
1689
|
-
// at this same address; its log
|
|
1753
|
+
// at this same address; its log:/// coordinate shares the trailing
|
|
1690
1754
|
// <loop>/<turn>/<seq>, so the model correlates op to stream output.
|
|
1691
1755
|
// Runtime comes from statement.signal (EXEC's runtime slot) so it's
|
|
1692
1756
|
// resolvable for failed execs too; empty/absent = the default shell.
|
|
@@ -1697,7 +1761,7 @@ class Engine {
|
|
|
1697
1761
|
if (seqs === undefined)
|
|
1698
1762
|
throw new Error(`Engine.#writeLog: loop_turn_seqs returned no row for loop=${loopId} turn=${turnId}`);
|
|
1699
1763
|
const runtime = (typeof statement.signal === "string" && statement.signal.length > 0) ? statement.signal : "sh";
|
|
1700
|
-
const coordPathname =
|
|
1764
|
+
const coordPathname = `/${runtime}/${seqs.loop_seq}/${seqs.turn_seq}/${sequence}`;
|
|
1701
1765
|
target.scheme = "exec";
|
|
1702
1766
|
target.pathname = coordPathname;
|
|
1703
1767
|
attrsObj.pathname = coordPathname;
|
|
@@ -1748,7 +1812,7 @@ class Engine {
|
|
|
1748
1812
|
}
|
|
1749
1813
|
// Normalize a parsed path for storage. The `file` scheme is a routing
|
|
1750
1814
|
// internal — never stored, never rendered to the model. Both bare paths
|
|
1751
|
-
// and `file
|
|
1815
|
+
// and `file:///...` inputs collapse to scheme=null at this boundary, so
|
|
1752
1816
|
// entries.scheme / log_entries.scheme never carry the string "file".
|
|
1753
1817
|
#extractTarget(path) {
|
|
1754
1818
|
if (path === null)
|