@plurnk/plurnk-service 0.28.0 → 0.34.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 +19 -20
- package/dist/Paths.d.ts +0 -1
- package/dist/Paths.d.ts.map +1 -1
- package/dist/Paths.js +2 -18
- package/dist/Paths.js.map +1 -1
- package/dist/content/line-marker.d.ts +4 -0
- package/dist/content/line-marker.d.ts.map +1 -1
- package/dist/content/line-marker.js +7 -1
- package/dist/content/line-marker.js.map +1 -1
- package/dist/content/matcher.js +1 -1
- package/dist/content/matcher.js.map +1 -1
- package/dist/content/mimetype-binary.js +1 -1
- package/dist/content/mimetype-binary.js.map +1 -1
- package/dist/content/path-mimetype.js +1 -1
- package/dist/content/path-mimetype.js.map +1 -1
- package/dist/content/read-resolve.js +1 -1
- package/dist/content/read-resolve.js.map +1 -1
- package/dist/core/ChannelWrite.d.ts +8 -0
- package/dist/core/ChannelWrite.d.ts.map +1 -1
- package/dist/core/ChannelWrite.js +9 -1
- package/dist/core/ChannelWrite.js.map +1 -1
- package/dist/core/Engine.d.ts +5 -6
- package/dist/core/Engine.d.ts.map +1 -1
- package/dist/core/Engine.js +107 -52
- package/dist/core/Engine.js.map +1 -1
- package/dist/core/ProviderInstantiate.js +1 -1
- package/dist/core/ProviderInstantiate.js.map +1 -1
- package/dist/core/SchemeRegistry.d.ts +1 -0
- package/dist/core/SchemeRegistry.d.ts.map +1 -1
- package/dist/core/SchemeRegistry.js +22 -0
- package/dist/core/SchemeRegistry.js.map +1 -1
- package/dist/core/fork.js +2 -2
- package/dist/core/fork.js.map +1 -1
- package/dist/core/git-membership.js +6 -6
- package/dist/core/git-membership.js.map +1 -1
- package/dist/core/packet-wire.d.ts +0 -1
- package/dist/core/packet-wire.d.ts.map +1 -1
- package/dist/core/packet-wire.js +6 -9
- package/dist/core/packet-wire.js.map +1 -1
- package/dist/core/scheme-types.d.ts +2 -1
- package/dist/core/scheme-types.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/schemes/Exec.d.ts +1 -0
- package/dist/schemes/Exec.d.ts.map +1 -1
- package/dist/schemes/Exec.js +4 -3
- package/dist/schemes/Exec.js.map +1 -1
- package/dist/schemes/File.d.ts +1 -0
- package/dist/schemes/File.d.ts.map +1 -1
- package/dist/schemes/File.js +3 -1
- package/dist/schemes/File.js.map +1 -1
- package/dist/schemes/Known.d.ts +1 -0
- package/dist/schemes/Known.d.ts.map +1 -1
- package/dist/schemes/Known.js +2 -0
- package/dist/schemes/Known.js.map +1 -1
- package/dist/schemes/Log.d.ts +1 -0
- package/dist/schemes/Log.d.ts.map +1 -1
- package/dist/schemes/Log.js +4 -1
- package/dist/schemes/Log.js.map +1 -1
- package/dist/schemes/Plurnk.d.ts +1 -0
- package/dist/schemes/Plurnk.d.ts.map +1 -1
- package/dist/schemes/Plurnk.js +1 -0
- package/dist/schemes/Plurnk.js.map +1 -1
- package/dist/schemes/Run.d.ts +17 -0
- package/dist/schemes/Run.d.ts.map +1 -0
- package/dist/schemes/Run.js +72 -0
- package/dist/schemes/Run.js.map +1 -0
- package/dist/schemes/Unknown.d.ts +1 -0
- package/dist/schemes/Unknown.d.ts.map +1 -1
- package/dist/schemes/Unknown.js +1 -0
- package/dist/schemes/Unknown.js.map +1 -1
- package/dist/schemes/_entry-crud.d.ts.map +1 -1
- package/dist/schemes/_entry-crud.js +2 -2
- package/dist/schemes/_entry-crud.js.map +1 -1
- package/dist/schemes/_entry-find.d.ts.map +1 -1
- package/dist/schemes/_entry-find.js +22 -6
- package/dist/schemes/_entry-find.js.map +1 -1
- package/dist/schemes/_entry-manifest.d.ts.map +1 -1
- package/dist/schemes/_entry-manifest.js +47 -30
- package/dist/schemes/_entry-manifest.js.map +1 -1
- package/dist/schemes/_entry-ops.d.ts.map +1 -1
- package/dist/schemes/_entry-ops.js +10 -9
- package/dist/schemes/_entry-ops.js.map +1 -1
- package/dist/schemes/_entry-semantic.d.ts +1 -1
- package/dist/schemes/_entry-semantic.d.ts.map +1 -1
- package/dist/schemes/_entry-semantic.js +5 -2
- package/dist/schemes/_entry-semantic.js.map +1 -1
- package/dist/schemes/_entry-send.js +1 -1
- package/dist/schemes/_entry-send.js.map +1 -1
- package/dist/server/ClientConnection.d.ts.map +1 -1
- package/dist/server/ClientConnection.js +2 -3
- package/dist/server/ClientConnection.js.map +1 -1
- package/dist/server/Daemon.d.ts +1 -3
- package/dist/server/Daemon.d.ts.map +1 -1
- package/dist/server/Daemon.js +25 -14
- package/dist/server/Daemon.js.map +1 -1
- package/dist/server/MethodRegistry.d.ts +0 -2
- package/dist/server/MethodRegistry.d.ts.map +1 -1
- package/dist/server/clientTurn.js +1 -1
- package/dist/server/clientTurn.js.map +1 -1
- package/dist/server/dsl.d.ts.map +1 -1
- package/dist/server/dsl.js +6 -4
- package/dist/server/dsl.js.map +1 -1
- package/dist/server/envelope.d.ts +0 -6
- package/dist/server/envelope.d.ts.map +1 -1
- package/dist/server/envelope.js +12 -24
- package/dist/server/envelope.js.map +1 -1
- package/dist/server/logEntry.d.ts.map +1 -1
- package/dist/server/logEntry.js.map +1 -1
- package/dist/server/methods/_dispatchAsPlurnk.js +1 -1
- package/dist/server/methods/_dispatchAsPlurnk.js.map +1 -1
- package/dist/server/methods/discover.d.ts.map +1 -1
- package/dist/server/methods/discover.js +1 -0
- package/dist/server/methods/discover.js.map +1 -1
- package/dist/server/methods/log_read.js +1 -1
- package/dist/server/methods/log_read.js.map +1 -1
- package/dist/server/methods/loop_cancel.js +1 -1
- package/dist/server/methods/loop_cancel.js.map +1 -1
- package/dist/server/methods/loop_inject.d.ts.map +1 -1
- package/dist/server/methods/loop_inject.js +1 -2
- package/dist/server/methods/loop_inject.js.map +1 -1
- package/dist/server/methods/loop_run.d.ts.map +1 -1
- package/dist/server/methods/loop_run.js +4 -10
- package/dist/server/methods/loop_run.js.map +1 -1
- package/dist/server/methods/session_attach.d.ts.map +1 -1
- package/dist/server/methods/session_attach.js +3 -9
- package/dist/server/methods/session_attach.js.map +1 -1
- package/dist/server/methods/session_constraints.js +1 -1
- package/dist/server/methods/session_constraints.js.map +1 -1
- package/dist/server/methods/session_create.d.ts.map +1 -1
- package/dist/server/methods/session_create.js +5 -10
- package/dist/server/methods/session_create.js.map +1 -1
- package/dist/server/yolo.d.ts.map +1 -1
- package/dist/server/yolo.js +1 -0
- package/dist/server/yolo.js.map +1 -1
- package/migrations/0000-00-00.01_schema.sql +16 -10
- package/package.json +9 -10
- package/requirements.md +2 -2
- package/persona.md +0 -1
package/dist/core/Engine.js
CHANGED
|
@@ -5,6 +5,7 @@ import EntryCrud from "../schemes/_entry-crud.js";
|
|
|
5
5
|
import EntryManifest from "../schemes/_entry-manifest.js";
|
|
6
6
|
import GitMembership from "./git-membership.js";
|
|
7
7
|
import GitState from "./git-state.js";
|
|
8
|
+
import Fork from "./fork.js";
|
|
8
9
|
import { DEFAULT_LOOP_FLAGS } from "./scheme-types.js";
|
|
9
10
|
import { LineMarkerOps, MimetypeBinary, editedSpan } from "../content/index.js";
|
|
10
11
|
import { readFile } from "node:fs/promises";
|
|
@@ -90,7 +91,7 @@ const pathnameFromPath = (path) => {
|
|
|
90
91
|
// loop terminal). No strike, no telemetry.
|
|
91
92
|
const TURN_STATUS_IMPLICIT_CONTINUE = 102;
|
|
92
93
|
// Status assigned to a turn that emitted NO ops at all. Strike-worthy; the
|
|
93
|
-
// action routes through telemetry.errors[] (§telemetry).
|
|
94
|
+
// action routes through telemetry.errors[] (§telemetry, §telemetry-no-error-scheme — never an error:// scheme).
|
|
94
95
|
const TURN_STATUS_NO_OPS = 422;
|
|
95
96
|
// Rail #38: action-entry statuses that DON'T accumulate strikes. Model adapted
|
|
96
97
|
// to a finding (not_found, op_not_supported); no penalty. Rummy parallel:
|
|
@@ -127,7 +128,7 @@ const fingerprintOp = (stmt) => {
|
|
|
127
128
|
}
|
|
128
129
|
const lm = stmt.lineMarker;
|
|
129
130
|
if (lm !== null && lm !== undefined)
|
|
130
|
-
parts.push(`L:${lm.
|
|
131
|
+
parts.push(`L:${lm.marks.join(",")}`);
|
|
131
132
|
return parts.length > 0 ? `|${parts.join("|")}` : "";
|
|
132
133
|
};
|
|
133
134
|
if (path === null) {
|
|
@@ -239,6 +240,7 @@ class Engine {
|
|
|
239
240
|
#loopAborts = new Map();
|
|
240
241
|
#streamEventNotify;
|
|
241
242
|
#wakeRunNotify;
|
|
243
|
+
#injectRun;
|
|
242
244
|
// Telemetry event fan-out: every TelemetryEvent pushed to the loop's
|
|
243
245
|
// buffer is also broadcast live to the connected client(s) on the
|
|
244
246
|
// session. Without this, the client sees `loop/terminated` with a
|
|
@@ -247,11 +249,12 @@ class Engine {
|
|
|
247
249
|
#telemetryEventNotify;
|
|
248
250
|
// Cached plurnk GBNF — read once on the first constrained generate (#189).
|
|
249
251
|
#gbnfCache = null;
|
|
250
|
-
constructor({ db, schemes, mimetypes, streamEventNotify, wakeRunNotify, telemetryEventNotify, tokenize }) {
|
|
252
|
+
constructor({ db, schemes, mimetypes, streamEventNotify, wakeRunNotify, injectRun, telemetryEventNotify, tokenize }) {
|
|
251
253
|
this.#db = db;
|
|
252
254
|
this.#schemes = schemes;
|
|
253
255
|
this.#streamEventNotify = streamEventNotify;
|
|
254
256
|
this.#wakeRunNotify = wakeRunNotify;
|
|
257
|
+
this.#injectRun = injectRun;
|
|
255
258
|
this.#telemetryEventNotify = telemetryEventNotify;
|
|
256
259
|
// Default to empty discovery — standalone Engine construction (in
|
|
257
260
|
// tests) gets no handlers, and content flows through the framework's
|
|
@@ -315,6 +318,7 @@ class Engine {
|
|
|
315
318
|
// both sides per the grammar 0.17.0 TelemetryEvent protocol.
|
|
316
319
|
this.#telemetryEventNotify?.(sessionId, { loopId, event });
|
|
317
320
|
}
|
|
321
|
+
// Telemetry drains as it's read into the packet — each event surfaces once. §telemetry-drain-on-read
|
|
318
322
|
#drainTelemetry(loopId) {
|
|
319
323
|
const buf = this.#telemetryBuffer.get(loopId);
|
|
320
324
|
if (buf === undefined)
|
|
@@ -343,7 +347,7 @@ class Engine {
|
|
|
343
347
|
}
|
|
344
348
|
return slice.join("\n");
|
|
345
349
|
}
|
|
346
|
-
async runLoop({ provider, messages,
|
|
350
|
+
async runLoop({ provider, messages, requirements = "", sessionId, runId, loopId, maxTurns = 50, maxStrikes = readMaxStrikes(), minCycles = readPositiveInt("PLURNK_MIN_CYCLES", DEFAULT_MIN_CYCLES), maxCyclePeriod = readPositiveInt("PLURNK_MAX_CYCLE_PERIOD", DEFAULT_MAX_CYCLE_PERIOD), origin = "model", signal, onDispatch, }) {
|
|
347
351
|
const turnIds = [];
|
|
348
352
|
const suddenDeathThreshold = maxTurns - maxStrikes;
|
|
349
353
|
// Per-loop AbortController for scheme-side cancellation propagation.
|
|
@@ -400,7 +404,7 @@ class Engine {
|
|
|
400
404
|
await delay(execWaitMs, undefined, { signal });
|
|
401
405
|
}
|
|
402
406
|
const turn = await this.runTurn({
|
|
403
|
-
provider, messages,
|
|
407
|
+
provider, messages, requirements, sessionId, runId, loopId, origin, signal, onDispatch,
|
|
404
408
|
turnNumber: turnIds.length + 1, maxTurns,
|
|
405
409
|
});
|
|
406
410
|
turnIds.push(turn.turnId);
|
|
@@ -425,7 +429,7 @@ class Engine {
|
|
|
425
429
|
state.turnErrors++;
|
|
426
430
|
// SPEC §grinder: a non-soft grinder fire counts toward the strike streak.
|
|
427
431
|
if (turn.budgetStruck)
|
|
428
|
-
state.turnErrors++;
|
|
432
|
+
state.turnErrors++; // a grinder fire bumps the strike streak — §grinder-strike-coupling
|
|
429
433
|
this.#strikeState.set(loopId, state);
|
|
430
434
|
// Rail #38: strike accounting. Three sources strike a turn:
|
|
431
435
|
// 1. recordedFailed — any action-entry at hard failure status
|
|
@@ -465,7 +469,7 @@ class Engine {
|
|
|
465
469
|
}
|
|
466
470
|
}
|
|
467
471
|
}
|
|
468
|
-
async runTurn({ provider, messages,
|
|
472
|
+
async runTurn({ provider, messages, requirements = "", sessionId, runId, loopId, origin = "model", signal, onDispatch, turnNumber = 1, maxTurns = 50, }) {
|
|
469
473
|
// === Turn-as-container model ===
|
|
470
474
|
//
|
|
471
475
|
// Turn rows are created at runTurn OPEN (status=102, placeholder
|
|
@@ -497,7 +501,7 @@ class Engine {
|
|
|
497
501
|
// otherwise.
|
|
498
502
|
let nextActionIndex = 1;
|
|
499
503
|
if (seq === 1) {
|
|
500
|
-
// Operator doc READs (PLURNK_MD_<ALIAS
|
|
504
|
+
// Operator doc READs (PLURNK_MD_<ALIAS>, §actor-boundary-doc-injection). The docs were materialized
|
|
501
505
|
// as plurnk:///<entry> entries by the plurnk run (loop_run, via the
|
|
502
506
|
// §actor-boundary keystone); foist a READ of each into THIS turn-0 so the model
|
|
503
507
|
// reads them inline. It sees only the READ — the materializing EDIT
|
|
@@ -531,10 +535,17 @@ class Engine {
|
|
|
531
535
|
target: promptPath, lineMarker: null,
|
|
532
536
|
body: promptRow.prompt, position: { line: 1, column: 1 },
|
|
533
537
|
};
|
|
538
|
+
let promptLogId;
|
|
534
539
|
await this.dispatch({
|
|
535
540
|
statement: promptStmt, sessionId, runId, loopId, turnId,
|
|
536
|
-
sequence: nextActionIndex, origin: "plurnk",
|
|
541
|
+
sequence: nextActionIndex, origin: "plurnk",
|
|
542
|
+
onDispatch: (id) => { promptLogId = id; onDispatch?.(id); },
|
|
537
543
|
});
|
|
544
|
+
// §prompt-fold (User Note 6): the prompt EDIT duplicates
|
|
545
|
+
// packet.user.prompt (its own section), so fold it — logged for
|
|
546
|
+
// forensics, collapsed in the model's log, re-OPENable.
|
|
547
|
+
if (promptLogId !== undefined)
|
|
548
|
+
await this.#db.engine_fold_log_entry.run({ id: promptLogId });
|
|
538
549
|
nextActionIndex++;
|
|
539
550
|
}
|
|
540
551
|
}
|
|
@@ -604,14 +615,14 @@ class Engine {
|
|
|
604
615
|
// queries log_entries scoped to the run — the prompt entry just
|
|
605
616
|
// written (if turn 1) is part of that query result.
|
|
606
617
|
let requestPacket = await this.#buildRequestPacket({
|
|
607
|
-
initialMessages: messages,
|
|
618
|
+
initialMessages: messages, requirements, runId, loopId,
|
|
608
619
|
currentTurnSeq: seq, provider, gitStatus,
|
|
609
620
|
});
|
|
610
621
|
// SPEC §grinder — budget grinder, pre-LLM: reclaim window on actual overflow.
|
|
611
622
|
const enforced = await this.#enforceBudget({
|
|
612
623
|
packet: requestPacket, provider, runId, loopId, turnId, sessionId, turnNumber,
|
|
613
624
|
rebuild: (telemetryErrors) => this.#buildRequestPacket({
|
|
614
|
-
initialMessages: messages,
|
|
625
|
+
initialMessages: messages, requirements, runId, loopId,
|
|
615
626
|
currentTurnSeq: seq, provider, telemetryErrors, gitStatus,
|
|
616
627
|
}),
|
|
617
628
|
});
|
|
@@ -632,14 +643,14 @@ class Engine {
|
|
|
632
643
|
// The 0.28.0 EOS-forcing root terminates the turn at the status SEND, but a
|
|
633
644
|
// grammar can't bound degeneration *inside* a statement body — this caps the
|
|
634
645
|
// decode at the free window so a runaway can't reach the context wall.
|
|
635
|
-
const genCeiling = _a.computeCeiling(provider.contextSize, this.#budgetCeiling);
|
|
646
|
+
const genCeiling = _a.computeCeiling(provider.contextSize, this.#budgetCeiling); // provider.contextSize, the immutable identity, read by the budget — §provider-surface-identity
|
|
636
647
|
const maxTokens = genCeiling === null ? undefined : Math.max(1, genCeiling - requestPacket.system.tokens - requestPacket.user.tokens);
|
|
637
|
-
const response = await provider.generate({ messages: modelMessages, runId: String(runId), signal, grammar: await this.#grammarConstraint(), maxTokens });
|
|
648
|
+
const response = await provider.generate({ messages: modelMessages, runId: String(runId), signal, grammar: await this.#grammarConstraint(), maxTokens }); // §provider-surface-generate §provider-guarantees-single-call §provider-guarantees-signal-wired
|
|
638
649
|
// Engine splits wire-level response: emission (content, reasoning,
|
|
639
650
|
// parsed ops) → packet.assistant per Packet.json §assistant;
|
|
640
651
|
// call-metadata (usage, finishReason, model) → Turn columns per
|
|
641
652
|
// Turn.json. Mixing the two on packet.assistant was the wrong layer.
|
|
642
|
-
const { packetAssistant, callMetadata, parseErrors } = this.#splitResponse(response);
|
|
653
|
+
const { packetAssistant, callMetadata, parseErrors } = this.#splitResponse(response); // raw assistant content is opaque — split, never interpreted — §provider-guarantees-assistantraw-opaque
|
|
643
654
|
// Surface parse errors to the model's NEXT packet so it can self-
|
|
644
655
|
// correct. Without this, malformed emissions (e.g. a READ matcher
|
|
645
656
|
// body starting with `//` being interpreted as xpath) silently
|
|
@@ -685,7 +696,7 @@ class Engine {
|
|
|
685
696
|
usage_prompt: usage.prompt,
|
|
686
697
|
usage_completion: usage.completion,
|
|
687
698
|
usage_cached: usage.cached,
|
|
688
|
-
usage_cost_pico: provider.costFor(usage),
|
|
699
|
+
usage_cost_pico: provider.costFor(usage), // §provider-surface-costfor
|
|
689
700
|
finish_reason: finishReason,
|
|
690
701
|
model,
|
|
691
702
|
});
|
|
@@ -820,9 +831,12 @@ class Engine {
|
|
|
820
831
|
// and §user) BEFORE the provider call. The same packet object is then
|
|
821
832
|
// completed with assistant + assistantRaw after the model responds, so
|
|
822
833
|
// the stored packet and the wire payload share one source of truth.
|
|
823
|
-
async #buildRequestPacket({ initialMessages,
|
|
834
|
+
async #buildRequestPacket({ initialMessages, requirements, runId, loopId, currentTurnSeq, provider, gitStatus, telemetryErrors: presetTelemetry, }) {
|
|
824
835
|
const byRole = (role) => initialMessages.filter((m) => m.role === role).map((m) => m.content).join("\n\n");
|
|
825
|
-
|
|
836
|
+
// plurnk.md (grammar/dialects) THEN the scheme catalogue: grammar 0.49+ is
|
|
837
|
+
// scheme-agnostic, so the service teaches what schemes exist + what they do
|
|
838
|
+
// at packet-time (grammar#239 item 7). SchemeRegistry.teach() assembles it.
|
|
839
|
+
const system_definition = `${byRole("system")}\n\n${this.#schemes.teach()}`;
|
|
826
840
|
// user.prompt sources from the loop's most recent prompt entry first
|
|
827
841
|
// (plurnk:///prompt/<loop_id>/<N> for the highest N written to date).
|
|
828
842
|
// This is what inject + the turn-1 foist write into. Falls back to
|
|
@@ -832,13 +846,8 @@ class Engine {
|
|
|
832
846
|
const prompt = (latestPromptRow !== undefined && typeof latestPromptRow.content === "string" && latestPromptRow.content.length > 0)
|
|
833
847
|
? latestPromptRow.content
|
|
834
848
|
: byRole("user");
|
|
835
|
-
// Resolve persona cascade: loops.persona > runs.persona >
|
|
836
|
-
// sessions.persona > caller-supplied default. SQL coalesces in one
|
|
837
|
-
// query; null result means no DB override exists, use the default.
|
|
838
|
-
const row = await this.#db.engine_resolve_persona.get({ loop_id: loopId });
|
|
839
|
-
const persona = (row?.persona !== undefined && row?.persona !== null) ? row.persona : defaultPersona;
|
|
840
849
|
// Requirements is engine-sourced, NOT threaded from callers — that threading is
|
|
841
|
-
// exactly how it went missing (
|
|
850
|
+
// exactly how it went missing (callers read the sysprompt but never the
|
|
842
851
|
// requirements). Read Paths.defaultRequirements (PLURNK_REQUIREMENTS env →
|
|
843
852
|
// requirements.md) fresh each build so edits take effect; a non-empty param wins.
|
|
844
853
|
const requirementsText = requirements.length > 0 ? requirements : await readFile(Paths.defaultRequirements, "utf8");
|
|
@@ -849,7 +858,7 @@ class Engine {
|
|
|
849
858
|
// form — wire-payload tokens may differ slightly because chat-
|
|
850
859
|
// template scaffolding adds bytes, but the subtotal tracks "what
|
|
851
860
|
// the model has to process" closely enough for budget diagnostics.
|
|
852
|
-
const countTokens = (t) => provider.countTokens(t);
|
|
861
|
+
const countTokens = (t) => provider.countTokens(t); // §provider-surface-counttokens
|
|
853
862
|
// Budget readout (SPEC.md §tokenomics). Two-pass: measure the wire-rendered
|
|
854
863
|
// index/log sections (budget-independent), install the readout with a
|
|
855
864
|
// tokensFree placeholder, measure the assembled total, resolve free,
|
|
@@ -859,21 +868,21 @@ class Engine {
|
|
|
859
868
|
// headline omitted, section lines still shown).
|
|
860
869
|
const ceiling = _a.computeCeiling(provider.contextSize, this.#budgetCeiling);
|
|
861
870
|
const scratch = {
|
|
862
|
-
system: { system_definition,
|
|
871
|
+
system: { system_definition, log },
|
|
863
872
|
user: { prompt, telemetry: { budget: "", errors: telemetryErrors, git: gitStatus }, tools: this.#collectTools(), system_requirements: requirementsText },
|
|
864
873
|
};
|
|
865
874
|
const sections = PacketWire.measureBudgetSections(scratch, countTokens);
|
|
866
875
|
scratch.user.telemetry.budget = this.#renderBudget(sections, ceiling);
|
|
867
876
|
const total = countTokens(PacketWire.renderSystemContent(scratch.system)) + countTokens(PacketWire.renderUserContent(scratch.user));
|
|
868
|
-
const tokensFree = ceiling === null ? null : Math.max(0, ceiling - total);
|
|
869
|
-
const percent = ceiling === null ? null : Math.round((total / ceiling) * 100);
|
|
877
|
+
const tokensFree = ceiling === null ? null : Math.max(0, ceiling - total); // free floors at 0 on overshoot — §tokenomics-over-budget-floor
|
|
878
|
+
const percent = ceiling === null ? null : Math.round((total / ceiling) * 100); // usage as % of the ceiling — §tokenomics-context-percent
|
|
870
879
|
const budget = tokensFree === null
|
|
871
880
|
? scratch.user.telemetry.budget
|
|
872
881
|
: scratch.user.telemetry.budget
|
|
873
882
|
.replace(TOKEN_USAGE_PLACEHOLDER, String(total))
|
|
874
883
|
.replace(TOKEN_PERCENT_PLACEHOLDER, String(percent))
|
|
875
884
|
.replace(TOKENS_FREE_PLACEHOLDER, String(tokensFree));
|
|
876
|
-
const system = { tokens: 0, system_definition,
|
|
885
|
+
const system = { tokens: 0, system_definition, log };
|
|
877
886
|
const user = { tokens: 0, prompt, telemetry: { budget, errors: telemetryErrors, git: gitStatus }, tools: scratch.user.tools, system_requirements: requirementsText };
|
|
878
887
|
system.tokens = countTokens(PacketWire.renderSystemContent(system));
|
|
879
888
|
user.tokens = countTokens(PacketWire.renderUserContent(user));
|
|
@@ -912,10 +921,11 @@ class Engine {
|
|
|
912
921
|
// contributor (gated by PLURNK_PLAN); each available executor tag then
|
|
913
922
|
// contributes its self-documenting example (plurnk-execs#7), retiring the
|
|
914
923
|
// blind EXEC.
|
|
924
|
+
// The capability sheet — the live tool surface (PLAN + wired executor tags). §tools-capability-sheet
|
|
915
925
|
#collectTools() {
|
|
916
926
|
const tools = [];
|
|
917
|
-
if (process.env.PLURNK_PLAN === "1") {
|
|
918
|
-
tools.push("* Begin every response with <<PLAN
|
|
927
|
+
if (process.env.PLURNK_PLAN === "1") { // <<PLAN advertised only when PLAN is enabled — §tools-plan-gated
|
|
928
|
+
tools.push("* Begin every response with <<PLAN:...:PLAN");
|
|
919
929
|
}
|
|
920
930
|
// Each available runtime tag contributes its self-documenting example —
|
|
921
931
|
// the example carries syntax + purpose, so there's no prose line. Tags
|
|
@@ -937,6 +947,7 @@ class Engine {
|
|
|
937
947
|
// passes, re-measuring between. Folds (never deletes) — the prior turn's logs,
|
|
938
948
|
// then the catalog except the manifest lifeline. The strike it raises and the
|
|
939
949
|
// hard-stop it can signal are returned to runLoop, which owns abandonment.
|
|
950
|
+
// §grinder-overflow-only — fires only on actual overflow, never speculatively
|
|
940
951
|
async #enforceBudget({ packet, provider, runId, loopId, turnId, sessionId, turnNumber, rebuild }) {
|
|
941
952
|
const ceiling = _a.computeCeiling(provider.contextSize, this.#budgetCeiling);
|
|
942
953
|
const measure = (p) => p.system.tokens + p.user.tokens;
|
|
@@ -946,7 +957,7 @@ class Engine {
|
|
|
946
957
|
const note = (scheme) => { folded.set(scheme, (folded.get(scheme) ?? 0) + 1); };
|
|
947
958
|
// Pass 1 — prior-turn rollback: fold the latest emissions (the ones that
|
|
948
959
|
// pushed it over). No prior turn (turn 1, env overflow) → no-op → pass 2.
|
|
949
|
-
const priorLogs = await this.#db.engine_grinder_prior_turn_logs.all({ loop_id: loopId, turn_id: turnId });
|
|
960
|
+
const priorLogs = await this.#db.engine_grinder_prior_turn_logs.all({ loop_id: loopId, turn_id: turnId }); // prior-turn rollback folds the latest emissions — §grinder-layer1-rollback
|
|
950
961
|
for (const le of priorLogs)
|
|
951
962
|
note(le.scheme ?? "log");
|
|
952
963
|
if (priorLogs.length > 0)
|
|
@@ -955,17 +966,18 @@ class Engine {
|
|
|
955
966
|
let current = priorLogs.length > 0 ? await rebuild(errors) : packet;
|
|
956
967
|
if (measure(current) <= ceiling) {
|
|
957
968
|
this.#emitBudgetOverflow(sessionId, loopId, folded);
|
|
958
|
-
return { packet: current, fit: true, struck: turnNumber > 1 };
|
|
969
|
+
return { packet: current, fit: true, struck: turnNumber > 1 }; // turn 0/1 overflow is the environment, never a strike — §grinder-soft-turn-0-1
|
|
959
970
|
}
|
|
960
971
|
// Prior-turn rollback is the only budget lever now: entries don't render
|
|
961
972
|
// (no index), so there is no catalog to collapse. If pass 1 didn't fit,
|
|
962
|
-
// the packet is over and the caller hard-413s.
|
|
973
|
+
// the packet is over and the caller hard-413s. §grinder-hard-413-abort
|
|
963
974
|
this.#emitBudgetOverflow(sessionId, loopId, folded);
|
|
964
975
|
return { packet: current, fit: measure(current) <= ceiling, struck: turnNumber > 1 };
|
|
965
976
|
}
|
|
966
977
|
// The model-facing budget event (SPEC §grinder, §telemetry): which entries left the
|
|
967
978
|
// window, by scheme — the model's own terms, no mechanism vocabulary. The
|
|
968
979
|
// strike this overflow triggers stays engine-internal (gamification policy).
|
|
980
|
+
// §grinder-event-model-terms — model-facing terms only; the strike stays engine-internal
|
|
969
981
|
#emitBudgetOverflow(sessionId, loopId, folded) {
|
|
970
982
|
if (folded.size === 0)
|
|
971
983
|
return;
|
|
@@ -1061,7 +1073,7 @@ class Engine {
|
|
|
1061
1073
|
source: r.source,
|
|
1062
1074
|
}));
|
|
1063
1075
|
}
|
|
1064
|
-
// §env-delta — at pre-turn build, surface what changed in the shared world since this
|
|
1076
|
+
// §env-delta (§actor-boundary-no-mutex: runs share without locks; a conflict surfaces as a delta, never prevented) — at pre-turn build, surface what changed in the shared world since this
|
|
1065
1077
|
// run last looked. No per-run snapshot (§machine-processes "a run is its log"): every
|
|
1066
1078
|
// edit is already a span-carrying log row, so PULL other actors' EDITs on shared
|
|
1067
1079
|
// entries since this run's prior turn — real cross-run edits and the plurnk run's
|
|
@@ -1092,11 +1104,12 @@ class Engine {
|
|
|
1092
1104
|
// world changed," so the fiction keeps its perspective aligned with what its tooling
|
|
1093
1105
|
// would show. The fiction lives in the plurnk run's log; every other run pulls it
|
|
1094
1106
|
// through the one delta path, exactly like a sibling's real edit.
|
|
1107
|
+
// §membership-emi-divergence-signal — disk divergences logged as the plurnk run's source=file EDIT fictions
|
|
1095
1108
|
async #logFsFictions(sessionId, divergences) {
|
|
1096
1109
|
if (divergences.length === 0)
|
|
1097
1110
|
return;
|
|
1098
1111
|
const run = await this.#db.envelope_get_run_by_name.get({ session_id: sessionId, name: "plurnk" })
|
|
1099
|
-
?? await this.#db.envelope_insert_run.get({ session_id: sessionId, name: "plurnk",
|
|
1112
|
+
?? await this.#db.envelope_insert_run.get({ session_id: sessionId, name: "plurnk", origin: "plurnk" });
|
|
1100
1113
|
if (run === undefined)
|
|
1101
1114
|
throw new Error("logFsFictions: plurnk run resolution returned no row");
|
|
1102
1115
|
const loop = await this.#db.envelope_insert_client_loop.get({ run_id: run.id });
|
|
@@ -1129,6 +1142,7 @@ class Engine {
|
|
|
1129
1142
|
signal: this.#loopAborts.get(loopId)?.signal,
|
|
1130
1143
|
streamEventNotify: this.#streamEventNotify,
|
|
1131
1144
|
wakeRunNotify: this.#wakeRunNotify,
|
|
1145
|
+
injectRun: this.#injectRun,
|
|
1132
1146
|
mimetypes: this.#mimetypes,
|
|
1133
1147
|
tokenize: this.#tokenize,
|
|
1134
1148
|
pushTelemetry: (event) => this.#pushTelemetry(sessionId, loopId, event),
|
|
@@ -1171,10 +1185,10 @@ class Engine {
|
|
|
1171
1185
|
result = await this.#run("exec", statement, schemeCtx);
|
|
1172
1186
|
}
|
|
1173
1187
|
else {
|
|
1174
|
-
result = await this.#run(this.#schemeNameOf(statement.target), statement, schemeCtx);
|
|
1188
|
+
result = await this.#run(this.#schemeNameOf(statement.target), statement, schemeCtx); // §op-methods-op-dispatch
|
|
1175
1189
|
}
|
|
1176
1190
|
}
|
|
1177
|
-
catch (err) {
|
|
1191
|
+
catch (err) { // a scheme exception becomes the op's 500 outcome — §scheme-surface-exception-500
|
|
1178
1192
|
result = {
|
|
1179
1193
|
status: 500,
|
|
1180
1194
|
error: err instanceof Error ? err.message : String(err),
|
|
@@ -1183,7 +1197,7 @@ class Engine {
|
|
|
1183
1197
|
}
|
|
1184
1198
|
const logEntryId = await this.#writeLog({ statement, result, runId, loopId, turnId, sequence, origin });
|
|
1185
1199
|
onDispatch?.(logEntryId);
|
|
1186
|
-
// Proposal lifecycle (SPEC.md §engine-rails + §methods loop.resolve). When a
|
|
1200
|
+
// Proposal lifecycle (SPEC.md §engine-rails + §methods loop.resolve; §proposal-202-pauses). When a
|
|
1187
1201
|
// scheme returns status 202, the entry is written as state='proposed';
|
|
1188
1202
|
// dispatch then PAUSES on a per-entry waiter until resolution
|
|
1189
1203
|
// arrives via Engine.resolveProposal (from the loop/resolve RPC,
|
|
@@ -1208,7 +1222,7 @@ class Engine {
|
|
|
1208
1222
|
// YOLO listener auto-resolves) BEFORE awaiting — they may
|
|
1209
1223
|
// resolve synchronously inside their handlers.
|
|
1210
1224
|
const target = this.#extractTarget(statement.target);
|
|
1211
|
-
const flags = await this.#loadLoopFlags(loopId);
|
|
1225
|
+
const flags = await this.#loadLoopFlags(loopId); // the loop/proposal notification carries flags (yolo) — §dual-yolo-proposal-carries-flags
|
|
1212
1226
|
const event = {
|
|
1213
1227
|
logEntryId, sessionId, runId, loopId, turnId,
|
|
1214
1228
|
op: statement.op,
|
|
@@ -1245,6 +1259,7 @@ class Engine {
|
|
|
1245
1259
|
}
|
|
1246
1260
|
return result;
|
|
1247
1261
|
}
|
|
1262
|
+
// On accept, run the scheme's applyResolution — File writes disk, Exec spawns. §proposal-accept-applies
|
|
1248
1263
|
async #runApplyResolution(statement, originalResult, resolution, ids) {
|
|
1249
1264
|
const { sessionId, runId, loopId, turnId } = ids;
|
|
1250
1265
|
if (resolution.decision !== "accept")
|
|
@@ -1406,7 +1421,7 @@ class Engine {
|
|
|
1406
1421
|
// transitions to cancelled with outcome='timeout'.
|
|
1407
1422
|
if (this.#pendingProposals.has(logEntryId)) {
|
|
1408
1423
|
this.#pendingProposals.delete(logEntryId);
|
|
1409
|
-
resolve({ decision: "cancel", outcome: "timeout" });
|
|
1424
|
+
resolve({ decision: "cancel", outcome: "timeout" }); // §proposal-timeout-cancels
|
|
1410
1425
|
}
|
|
1411
1426
|
}, timeoutMs);
|
|
1412
1427
|
this.#pendingProposals.set(logEntryId, { resolve, timeoutHandle });
|
|
@@ -1415,8 +1430,8 @@ class Engine {
|
|
|
1415
1430
|
async #applyResolution(logEntryId, resolution) {
|
|
1416
1431
|
// Map decision → terminal state + HTTP-aligned status:
|
|
1417
1432
|
// accept → state='resolved', status=200
|
|
1418
|
-
// reject → state='failed', status=400, outcome='rejected' (default)
|
|
1419
|
-
// cancel → state='cancelled',status=499, outcome='loop_aborted' (default)
|
|
1433
|
+
// reject → state='failed', status=400, outcome='rejected' (default) §proposal-reject-fails
|
|
1434
|
+
// cancel → state='cancelled',status=499, outcome='loop_aborted' (default) §proposal-cancel-aborts
|
|
1420
1435
|
// resolution.outcome wins over the default when supplied; this is how
|
|
1421
1436
|
// veto filters (Phase E.2 proposal.accepting) can specify a more
|
|
1422
1437
|
// precise outcome string like 'policy_veto' or 'timeout'.
|
|
@@ -1462,6 +1477,11 @@ class Engine {
|
|
|
1462
1477
|
if (statement.op === "EXEC") {
|
|
1463
1478
|
return this.#denyIfDisallowed("exec", origin);
|
|
1464
1479
|
}
|
|
1480
|
+
// A run-fork (COPY src=run://) is gated by run://'s writableBy — its body
|
|
1481
|
+
// is a fork prompt, not a dst path, so the entry-COPY dst-parse below
|
|
1482
|
+
// doesn't apply. §machine-processes
|
|
1483
|
+
if (this.#isRunFork(statement))
|
|
1484
|
+
return this.#denyIfDisallowed("run", origin);
|
|
1465
1485
|
if (statement.op === "COPY" || statement.op === "MOVE") {
|
|
1466
1486
|
const dst = statement.op === "COPY" ? (statement.body === null ? null : parsePath(statement.body)) : statement.body;
|
|
1467
1487
|
const dstScheme = this.#schemeNameOf(dst);
|
|
@@ -1492,7 +1512,7 @@ class Engine {
|
|
|
1492
1512
|
return null;
|
|
1493
1513
|
if (manifest.writableBy.includes(origin))
|
|
1494
1514
|
return null;
|
|
1495
|
-
return { status: 403, error: `writer '${origin}' is not in writableBy for scheme '${schemeName}'` };
|
|
1515
|
+
return { status: 403, error: `writer '${origin}' is not in writableBy for scheme '${schemeName}'` }; // §scheme-surface-writableby-403
|
|
1496
1516
|
}
|
|
1497
1517
|
// Per-loop flag gating. Schemes self-declare their flag affinity in
|
|
1498
1518
|
// their manifest (excludedInAsk / requiresWeb /
|
|
@@ -1516,14 +1536,47 @@ class Engine {
|
|
|
1516
1536
|
return null;
|
|
1517
1537
|
return { status: 403, error: `scheme '${scheme}' is inactive under current loop flags` };
|
|
1518
1538
|
};
|
|
1539
|
+
if (this.#isRunFork(statement))
|
|
1540
|
+
return check(statement.target); // body is a fork prompt, not a dst path
|
|
1519
1541
|
if (statement.op === "COPY" || statement.op === "MOVE") {
|
|
1520
1542
|
return check(statement.target) ?? check(statement.op === "COPY" ? (statement.body === null ? null : parsePath(statement.body)) : statement.body);
|
|
1521
1543
|
}
|
|
1522
1544
|
return check(statement.target);
|
|
1523
1545
|
}
|
|
1546
|
+
// A COPY whose SOURCE is run:// is a run-fork, not an entry-copy — its body
|
|
1547
|
+
// is the fork's seed prompt, not a destination path. The COPY gates and
|
|
1548
|
+
// #handleCopy branch on this so they never parse the prompt as a dst path.
|
|
1549
|
+
#isRunFork(statement) {
|
|
1550
|
+
return statement.op === "COPY" && this.#schemeNameOf(statement.target) === "run";
|
|
1551
|
+
}
|
|
1552
|
+
// COPY(run:///<src>):prompt — fork: deep-copy the source run's log into a new
|
|
1553
|
+
// run (Fork), then start it with the prompt (ctx.injectRun). Source "."/"" =
|
|
1554
|
+
// self (ctx.runId); a name resolves within the session (404 if absent).
|
|
1555
|
+
// §machine-processes-fork-copies-the-log
|
|
1556
|
+
async #handleRunFork(statement, ctx) {
|
|
1557
|
+
const target = statement.target;
|
|
1558
|
+
if (target === null)
|
|
1559
|
+
return { status: 400, error: "run:// fork requires a source run" };
|
|
1560
|
+
const name = pathnameFromPath(target).replace(/^\/+/, "");
|
|
1561
|
+
let srcRunId = ctx.runId;
|
|
1562
|
+
if (name !== "" && name !== ".") {
|
|
1563
|
+
const row = await this.#db.run_resolve_by_name.get({ session_id: ctx.sessionId, name });
|
|
1564
|
+
if (row === undefined)
|
|
1565
|
+
return { status: 404, error: `run:///${name} not found in this session` };
|
|
1566
|
+
srcRunId = row.id;
|
|
1567
|
+
}
|
|
1568
|
+
if (ctx.injectRun === undefined)
|
|
1569
|
+
throw new Error("run fork: injectRun capability absent");
|
|
1570
|
+
const branchRunId = await Fork.fork(this.#db, srcRunId);
|
|
1571
|
+
const branch = await this.#db.fork_get_run.get({ id: branchRunId });
|
|
1572
|
+
await ctx.injectRun({ sessionId: ctx.sessionId, runId: branchRunId, prompt: typeof statement.body === "string" ? statement.body : "" });
|
|
1573
|
+
return { status: 200, body: branch?.name ?? "" };
|
|
1574
|
+
}
|
|
1524
1575
|
async #handleCopy(statement, ctx) {
|
|
1525
1576
|
if (statement.op !== "COPY")
|
|
1526
1577
|
throw new Error("unreachable");
|
|
1578
|
+
if (this.#isRunFork(statement))
|
|
1579
|
+
return await this.#handleRunFork(statement, ctx);
|
|
1527
1580
|
const srcPath = statement.target;
|
|
1528
1581
|
// COPY's body is an opaque raw string (grammar §COPY: a dest path OR a run-fork
|
|
1529
1582
|
// prompt); parse it to the dest path. Non-path bodies (run:// fork prompts) are
|
|
@@ -1542,17 +1595,17 @@ class Engine {
|
|
|
1542
1595
|
const dstPath = statement.body;
|
|
1543
1596
|
if (srcPath === null)
|
|
1544
1597
|
return { status: 400, error: "MOVE requires source path" };
|
|
1545
|
-
// MOVE is relocation only — deletion is KILL's job (§move). The /dev/null
|
|
1598
|
+
// MOVE is relocation only — deletion is KILL's job (§move, §move-dev-null-not-special). The /dev/null
|
|
1546
1599
|
// and null-body delete-by-MOVE back-compat is retired: no silent debt.
|
|
1547
1600
|
if (dstPath === null)
|
|
1548
|
-
return { status: 400, error: "MOVE requires a destination; use KILL to delete" };
|
|
1601
|
+
return { status: 400, error: "MOVE requires a destination; use KILL to delete" }; // §move-null-body-400
|
|
1549
1602
|
const srcSchemeName = this.#schemeNameOf(srcPath);
|
|
1550
1603
|
if (srcSchemeName === null)
|
|
1551
1604
|
return { status: 400, error: "MOVE source must be a URL path with a scheme" };
|
|
1552
1605
|
const srcHandler = this.#schemes.get(srcSchemeName);
|
|
1553
1606
|
if (srcHandler === undefined || typeof srcHandler.deleteEntry !== "function")
|
|
1554
1607
|
return { status: 501 };
|
|
1555
|
-
// Relocation: COPY then DELETE source.
|
|
1608
|
+
// Relocation: COPY then DELETE source (§move-relocation-deletes-source).
|
|
1556
1609
|
const copyResult = await this.#copyOrchestration({ statement, srcPath, dstPath, ctx });
|
|
1557
1610
|
if (copyResult.status >= 400)
|
|
1558
1611
|
return copyResult;
|
|
@@ -1612,6 +1665,7 @@ class Engine {
|
|
|
1612
1665
|
throw new Error("unreachable");
|
|
1613
1666
|
return { status: 200 };
|
|
1614
1667
|
}
|
|
1668
|
+
// Same- and cross-scheme COPY share one orchestrator — §copy-cross-scheme-copy §move-cross-scheme-move
|
|
1615
1669
|
async #copyOrchestration({ statement, srcPath, dstPath, ctx }) {
|
|
1616
1670
|
const srcSchemeName = this.#schemeNameOf(srcPath);
|
|
1617
1671
|
const dstSchemeName = this.#schemeNameOf(dstPath);
|
|
@@ -1627,7 +1681,7 @@ class Engine {
|
|
|
1627
1681
|
const dstPathname = pathnameFromPath(dstPath);
|
|
1628
1682
|
const srcResult = await srcHandler.readEntry(srcPathname, ctx);
|
|
1629
1683
|
if (srcResult.status !== 200 || srcResult.entry === null)
|
|
1630
|
-
return { status: 404, error: `COPY/MOVE source not found: ${srcSchemeName}://${srcPathname}` };
|
|
1684
|
+
return { status: 404, error: `COPY/MOVE source not found: ${srcSchemeName}://${srcPathname}` }; // §copy-missing-source-404 §move-missing-source-404
|
|
1631
1685
|
const entry = srcResult.entry;
|
|
1632
1686
|
// Destination read — the conflict/no-op verdict is deferred until the
|
|
1633
1687
|
// to-be-written content is known (after <L> slice + tag resolution below),
|
|
@@ -1641,7 +1695,7 @@ class Engine {
|
|
|
1641
1695
|
for (const [channelName, channelData] of Object.entries(entry.channels)) {
|
|
1642
1696
|
const expectedMimetype = dstChannels[channelName];
|
|
1643
1697
|
if (expectedMimetype !== undefined && expectedMimetype !== channelData.mimetype) {
|
|
1644
|
-
return { status: 415, error: `mimetype mismatch on channel '${channelName}': ${channelData.mimetype} vs ${expectedMimetype}` };
|
|
1698
|
+
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
|
|
1645
1699
|
}
|
|
1646
1700
|
}
|
|
1647
1701
|
// `<L>` source range slicing per SPEC.md §op-invariants (symmetric with READ
|
|
@@ -1663,7 +1717,7 @@ class Engine {
|
|
|
1663
1717
|
}
|
|
1664
1718
|
channels = sliced;
|
|
1665
1719
|
}
|
|
1666
|
-
// Tag resolution: signal = replace; absent/empty = carry from source
|
|
1720
|
+
// Tag resolution: signal = replace (§copy-signal-replaces-source-tags); absent/empty = carry from source (§copy-no-signal-carries-source-tags)
|
|
1667
1721
|
const tags = (Array.isArray(statement.signal) && statement.signal.length > 0)
|
|
1668
1722
|
? statement.signal
|
|
1669
1723
|
: entry.tags;
|
|
@@ -1679,8 +1733,8 @@ class Engine {
|
|
|
1679
1733
|
&& writeNames.every((n, i) => n === dstNames[i] && (channels[n]?.content ?? "") === (dstChannels[n]?.content ?? ""));
|
|
1680
1734
|
const sameTags = [...tags].sort().join("") === [...dstExisting.entry.tags].sort().join("");
|
|
1681
1735
|
if (sameContent && sameTags)
|
|
1682
|
-
return { status: 304 };
|
|
1683
|
-
return { status: 409, error: `COPY/MOVE destination exists: ${dstSchemeName}://${dstPathname}` };
|
|
1736
|
+
return { status: 304 }; // identical → §copy-noop-304
|
|
1737
|
+
return { status: 409, error: `COPY/MOVE destination exists: ${dstSchemeName}://${dstPathname}` }; // §copy-conflict-409
|
|
1684
1738
|
}
|
|
1685
1739
|
const writeResult = await dstHandler.writeEntry(dstPathname, { channels, tags }, ctx);
|
|
1686
1740
|
// A file dest returns 202 (disk write → §membership review): propagate the
|
|
@@ -1817,7 +1871,8 @@ class Engine {
|
|
|
1817
1871
|
#extractTarget(path) {
|
|
1818
1872
|
if (path === null)
|
|
1819
1873
|
return { scheme: null, username: null, password: null, hostname: null, port: null, pathname: null, params: null, fragment: null };
|
|
1820
|
-
|
|
1874
|
+
// `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.
|
|
1875
|
+
if (path.kind === "local" || path.kind === "regex")
|
|
1821
1876
|
return { scheme: null, username: null, password: null, hostname: null, port: null, pathname: path.raw, params: null, fragment: null };
|
|
1822
1877
|
const scheme = path.scheme === "file" ? null : path.scheme;
|
|
1823
1878
|
return {
|