@plurnk/plurnk-service 0.29.0 → 0.35.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 +47 -23
- 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 +9 -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 +11 -6
- package/dist/core/Engine.d.ts.map +1 -1
- package/dist/core/Engine.js +199 -73
- package/dist/core/Engine.js.map +1 -1
- package/dist/core/ExecutorRegistry.d.ts +2 -0
- package/dist/core/ExecutorRegistry.d.ts.map +1 -1
- package/dist/core/ExecutorRegistry.js +1 -0
- package/dist/core/ExecutorRegistry.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 +5 -0
- package/dist/core/SchemeRegistry.d.ts.map +1 -1
- package/dist/core/SchemeRegistry.js +39 -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.d.ts.map +1 -1
- package/dist/core/git-membership.js +10 -7
- package/dist/core/git-membership.js.map +1 -1
- package/dist/core/git-state.d.ts.map +1 -1
- package/dist/core/git-state.js +3 -1
- package/dist/core/git-state.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/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/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/scheme-types.d.ts +2 -1
- package/dist/core/scheme-types.d.ts.map +1 -1
- package/dist/core/session-settings.d.ts +19 -0
- package/dist/core/session-settings.d.ts.map +1 -0
- package/dist/core/session-settings.js +44 -0
- package/dist/core/session-settings.js.map +1 -0
- 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 +6 -2
- 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 +76 -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 +26 -9
- package/dist/schemes/_entry-find.js.map +1 -1
- package/dist/schemes/_entry-graph.d.ts +6 -0
- package/dist/schemes/_entry-graph.d.ts.map +1 -1
- package/dist/schemes/_entry-graph.js +8 -0
- 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 +60 -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 +14 -12
- 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.d.ts.map +1 -1
- package/dist/schemes/_entry-send.js +5 -4
- 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 +31 -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 +2 -6
- package/dist/server/envelope.d.ts.map +1 -1
- package/dist/server/envelope.js +19 -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 +6 -2
- 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 +18 -20
- 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 +55 -10
- package/dist/server/methods/session_create.js.map +1 -1
- package/dist/server/methods/session_prompts.d.ts +5 -0
- package/dist/server/methods/session_prompts.d.ts.map +1 -0
- package/dist/server/methods/session_prompts.js +29 -0
- package/dist/server/methods/session_prompts.js.map +1 -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.map +1 -1
- package/dist/server/yolo.js +9 -0
- package/dist/server/yolo.js.map +1 -1
- package/migrations/0000-00-00.01_schema.sql +35 -9
- package/package.json +9 -10
- package/requirements.md +2 -2
- package/persona.md +0 -1
package/dist/core/Engine.js
CHANGED
|
@@ -5,6 +5,10 @@ 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";
|
|
9
|
+
import RunCap from "./run-cap.js";
|
|
10
|
+
import SessionSettings from "./session-settings.js";
|
|
11
|
+
import { decodePathParens } from "./path-decode.js";
|
|
8
12
|
import { DEFAULT_LOOP_FLAGS } from "./scheme-types.js";
|
|
9
13
|
import { LineMarkerOps, MimetypeBinary, editedSpan } from "../content/index.js";
|
|
10
14
|
import { readFile } from "node:fs/promises";
|
|
@@ -59,14 +63,12 @@ const readMaxCommands = () => {
|
|
|
59
63
|
};
|
|
60
64
|
// PLURNK_MANIFEST_ITEMS — the turn-0 manifest preview. null = off (no foist);
|
|
61
65
|
// -1 = the full manifest; positive N = the first N items. 0 / unset = off.
|
|
66
|
+
const normalizeManifestItems = (n) => (!Number.isFinite(n) || n === 0 ? null : n < 0 ? -1 : n);
|
|
62
67
|
const readManifestItems = () => {
|
|
63
68
|
const raw = process.env.PLURNK_MANIFEST_ITEMS;
|
|
64
69
|
if (raw === undefined || raw.length === 0)
|
|
65
70
|
return null;
|
|
66
|
-
|
|
67
|
-
if (!Number.isFinite(n) || n === 0)
|
|
68
|
-
return null;
|
|
69
|
-
return n < 0 ? -1 : n;
|
|
71
|
+
return normalizeManifestItems(Number.parseInt(raw, 10));
|
|
70
72
|
};
|
|
71
73
|
// Resolution timeout — proposed entries auto-cancel if nothing arrives
|
|
72
74
|
// within this window. SPEC.md §engine-rails (proposal lifecycle) + §methods (loop.resolve).
|
|
@@ -81,16 +83,16 @@ const readProposalTimeoutMs = () => {
|
|
|
81
83
|
return n;
|
|
82
84
|
};
|
|
83
85
|
const pathnameFromPath = (path) => {
|
|
84
|
-
if (path.kind === "
|
|
85
|
-
return path.
|
|
86
|
-
return path.raw;
|
|
86
|
+
if (path.kind === "regex")
|
|
87
|
+
return path.raw; // regex source — parens are syntax, never encoded
|
|
88
|
+
return decodePathParens(path.kind === "url" ? path.pathname : path.raw); // #239 item 4
|
|
87
89
|
};
|
|
88
90
|
// Default turn.status when ops were emitted but no SEND. Model is implicitly
|
|
89
91
|
// continuing; loop.status stays 102 either way (only SEND broadcast advances
|
|
90
92
|
// loop terminal). No strike, no telemetry.
|
|
91
93
|
const TURN_STATUS_IMPLICIT_CONTINUE = 102;
|
|
92
94
|
// Status assigned to a turn that emitted NO ops at all. Strike-worthy; the
|
|
93
|
-
// action routes through telemetry.errors[] (§telemetry).
|
|
95
|
+
// action routes through telemetry.errors[] (§telemetry, §telemetry-no-error-scheme — never an error:// scheme).
|
|
94
96
|
const TURN_STATUS_NO_OPS = 422;
|
|
95
97
|
// Rail #38: action-entry statuses that DON'T accumulate strikes. Model adapted
|
|
96
98
|
// to a finding (not_found, op_not_supported); no penalty. Rummy parallel:
|
|
@@ -127,7 +129,7 @@ const fingerprintOp = (stmt) => {
|
|
|
127
129
|
}
|
|
128
130
|
const lm = stmt.lineMarker;
|
|
129
131
|
if (lm !== null && lm !== undefined)
|
|
130
|
-
parts.push(`L:${lm.
|
|
132
|
+
parts.push(`L:${lm.marks.join(",")}`);
|
|
131
133
|
return parts.length > 0 ? `|${parts.join("|")}` : "";
|
|
132
134
|
};
|
|
133
135
|
if (path === null) {
|
|
@@ -239,6 +241,8 @@ class Engine {
|
|
|
239
241
|
#loopAborts = new Map();
|
|
240
242
|
#streamEventNotify;
|
|
241
243
|
#wakeRunNotify;
|
|
244
|
+
#injectRun;
|
|
245
|
+
#cancelRun;
|
|
242
246
|
// Telemetry event fan-out: every TelemetryEvent pushed to the loop's
|
|
243
247
|
// buffer is also broadcast live to the connected client(s) on the
|
|
244
248
|
// session. Without this, the client sees `loop/terminated` with a
|
|
@@ -247,11 +251,13 @@ class Engine {
|
|
|
247
251
|
#telemetryEventNotify;
|
|
248
252
|
// Cached plurnk GBNF — read once on the first constrained generate (#189).
|
|
249
253
|
#gbnfCache = null;
|
|
250
|
-
constructor({ db, schemes, mimetypes, streamEventNotify, wakeRunNotify, telemetryEventNotify, tokenize }) {
|
|
254
|
+
constructor({ db, schemes, mimetypes, streamEventNotify, wakeRunNotify, injectRun, cancelRun, telemetryEventNotify, tokenize }) {
|
|
251
255
|
this.#db = db;
|
|
252
256
|
this.#schemes = schemes;
|
|
253
257
|
this.#streamEventNotify = streamEventNotify;
|
|
254
258
|
this.#wakeRunNotify = wakeRunNotify;
|
|
259
|
+
this.#injectRun = injectRun;
|
|
260
|
+
this.#cancelRun = cancelRun;
|
|
255
261
|
this.#telemetryEventNotify = telemetryEventNotify;
|
|
256
262
|
// Default to empty discovery — standalone Engine construction (in
|
|
257
263
|
// tests) gets no handlers, and content flows through the framework's
|
|
@@ -315,6 +321,7 @@ class Engine {
|
|
|
315
321
|
// both sides per the grammar 0.17.0 TelemetryEvent protocol.
|
|
316
322
|
this.#telemetryEventNotify?.(sessionId, { loopId, event });
|
|
317
323
|
}
|
|
324
|
+
// Telemetry drains as it's read into the packet — each event surfaces once. §telemetry-drain-on-read
|
|
318
325
|
#drainTelemetry(loopId) {
|
|
319
326
|
const buf = this.#telemetryBuffer.get(loopId);
|
|
320
327
|
if (buf === undefined)
|
|
@@ -343,7 +350,7 @@ class Engine {
|
|
|
343
350
|
}
|
|
344
351
|
return slice.join("\n");
|
|
345
352
|
}
|
|
346
|
-
async runLoop({ provider, messages,
|
|
353
|
+
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
354
|
const turnIds = [];
|
|
348
355
|
const suddenDeathThreshold = maxTurns - maxStrikes;
|
|
349
356
|
// Per-loop AbortController for scheme-side cancellation propagation.
|
|
@@ -385,7 +392,7 @@ class Engine {
|
|
|
385
392
|
return { turnIds, finalStatus: row.status, hitMaxTurns: false, reason: "external" };
|
|
386
393
|
}
|
|
387
394
|
if (maxTurns >= 0 && turnIds.length >= maxTurns) {
|
|
388
|
-
await this.#db.engine_loop_cancel.run({ loop_id: loopId });
|
|
395
|
+
await this.#db.engine_loop_cancel.run({ loop_id: loopId, message: "max_turns" });
|
|
389
396
|
cleanup("forceful", "max_turns");
|
|
390
397
|
return { turnIds, finalStatus: 499, hitMaxTurns: true, reason: "max_turns" };
|
|
391
398
|
}
|
|
@@ -400,13 +407,13 @@ class Engine {
|
|
|
400
407
|
await delay(execWaitMs, undefined, { signal });
|
|
401
408
|
}
|
|
402
409
|
const turn = await this.runTurn({
|
|
403
|
-
provider, messages,
|
|
410
|
+
provider, messages, requirements, sessionId, runId, loopId, origin, signal, onDispatch,
|
|
404
411
|
turnNumber: turnIds.length + 1, maxTurns,
|
|
405
412
|
});
|
|
406
413
|
turnIds.push(turn.turnId);
|
|
407
414
|
// SPEC §grinder: budget hard-stop — packet won't fit even collapsed → abandon.
|
|
408
415
|
if (turn.budgetHardStop) {
|
|
409
|
-
await this.#db.engine_loop_cancel.run({ loop_id: loopId });
|
|
416
|
+
await this.#db.engine_loop_cancel.run({ loop_id: loopId, message: "budget_overflow" });
|
|
410
417
|
cleanup("forceful", "budget_overflow");
|
|
411
418
|
return { turnIds, finalStatus: 499, hitMaxTurns: false, reason: "budget_overflow" };
|
|
412
419
|
}
|
|
@@ -425,7 +432,7 @@ class Engine {
|
|
|
425
432
|
state.turnErrors++;
|
|
426
433
|
// SPEC §grinder: a non-soft grinder fire counts toward the strike streak.
|
|
427
434
|
if (turn.budgetStruck)
|
|
428
|
-
state.turnErrors++;
|
|
435
|
+
state.turnErrors++; // a grinder fire bumps the strike streak — §grinder-strike-coupling
|
|
429
436
|
this.#strikeState.set(loopId, state);
|
|
430
437
|
// Rail #38: strike accounting. Three sources strike a turn:
|
|
431
438
|
// 1. recordedFailed — any action-entry at hard failure status
|
|
@@ -447,7 +454,7 @@ class Engine {
|
|
|
447
454
|
if (struck) {
|
|
448
455
|
state.streak++;
|
|
449
456
|
if (state.streak >= maxStrikes) {
|
|
450
|
-
await this.#db.engine_loop_cancel.run({ loop_id: loopId });
|
|
457
|
+
await this.#db.engine_loop_cancel.run({ loop_id: loopId, message: "strike_threshold" });
|
|
451
458
|
cleanup("forceful", "strike_threshold");
|
|
452
459
|
return { turnIds, finalStatus: 499, hitMaxTurns: false, reason: "strike_threshold" };
|
|
453
460
|
}
|
|
@@ -465,7 +472,7 @@ class Engine {
|
|
|
465
472
|
}
|
|
466
473
|
}
|
|
467
474
|
}
|
|
468
|
-
async runTurn({ provider, messages,
|
|
475
|
+
async runTurn({ provider, messages, requirements = "", sessionId, runId, loopId, origin = "model", signal, onDispatch, turnNumber = 1, maxTurns = 50, }) {
|
|
469
476
|
// === Turn-as-container model ===
|
|
470
477
|
//
|
|
471
478
|
// Turn rows are created at runTurn OPEN (status=102, placeholder
|
|
@@ -497,12 +504,15 @@ class Engine {
|
|
|
497
504
|
// otherwise.
|
|
498
505
|
let nextActionIndex = 1;
|
|
499
506
|
if (seq === 1) {
|
|
500
|
-
// Operator doc READs (PLURNK_MD_<ALIAS
|
|
507
|
+
// Operator doc READs (PLURNK_MD_<ALIAS>, §actor-boundary-doc-injection). The docs were materialized
|
|
501
508
|
// as plurnk:///<entry> entries by the plurnk run (loop_run, via the
|
|
502
509
|
// §actor-boundary keystone); foist a READ of each into THIS turn-0 so the model
|
|
503
510
|
// reads them inline. It sees only the READ — the materializing EDIT
|
|
504
511
|
// lives in the plurnk run's log, never the model's.
|
|
505
|
-
|
|
512
|
+
// #231 — env docs (PLURNK_MD_*) UNION the session's client docs; foist a READ of
|
|
513
|
+
// each materialized plurnk:///<alias>.md (loop_run materialized the same set).
|
|
514
|
+
const { mdDocs } = await SessionSettings.read(this.#db, sessionId);
|
|
515
|
+
for (const doc of await SessionSettings.resolveDocs(mdDocs)) {
|
|
506
516
|
const docTarget = {
|
|
507
517
|
kind: "url", raw: `plurnk:///${doc.entryName}`, scheme: "plurnk",
|
|
508
518
|
username: null, password: null, hostname: null, port: null,
|
|
@@ -531,10 +541,17 @@ class Engine {
|
|
|
531
541
|
target: promptPath, lineMarker: null,
|
|
532
542
|
body: promptRow.prompt, position: { line: 1, column: 1 },
|
|
533
543
|
};
|
|
544
|
+
let promptLogId;
|
|
534
545
|
await this.dispatch({
|
|
535
546
|
statement: promptStmt, sessionId, runId, loopId, turnId,
|
|
536
|
-
sequence: nextActionIndex, origin: "plurnk",
|
|
547
|
+
sequence: nextActionIndex, origin: "plurnk",
|
|
548
|
+
onDispatch: (id) => { promptLogId = id; onDispatch?.(id); },
|
|
537
549
|
});
|
|
550
|
+
// §prompt-fold (User Note 6): the prompt EDIT duplicates
|
|
551
|
+
// packet.user.prompt (its own section), so fold it — logged for
|
|
552
|
+
// forensics, collapsed in the model's log, re-OPENable.
|
|
553
|
+
if (promptLogId !== undefined)
|
|
554
|
+
await this.#db.engine_fold_log_entry.run({ id: promptLogId });
|
|
538
555
|
nextActionIndex++;
|
|
539
556
|
}
|
|
540
557
|
}
|
|
@@ -573,7 +590,9 @@ class Engine {
|
|
|
573
590
|
// manifest is JSON); off by default. AFTER the manifest write so the READ hits
|
|
574
591
|
// it, not a 404; same plurnk-origin foist as the operator docs.
|
|
575
592
|
if (seq === 1) {
|
|
576
|
-
|
|
593
|
+
// #231 — a session's client-chosen manifestItems REPLACES the env default outright.
|
|
594
|
+
const { manifestItems: sessionMI } = await SessionSettings.read(this.#db, sessionId);
|
|
595
|
+
const manifestItems = sessionMI !== null ? normalizeManifestItems(sessionMI) : readManifestItems();
|
|
577
596
|
if (manifestItems !== null) {
|
|
578
597
|
const manifestRead = {
|
|
579
598
|
op: "READ", suffix: "", signal: null, lineMarker: null,
|
|
@@ -604,14 +623,14 @@ class Engine {
|
|
|
604
623
|
// queries log_entries scoped to the run — the prompt entry just
|
|
605
624
|
// written (if turn 1) is part of that query result.
|
|
606
625
|
let requestPacket = await this.#buildRequestPacket({
|
|
607
|
-
initialMessages: messages,
|
|
626
|
+
initialMessages: messages, requirements, runId, loopId,
|
|
608
627
|
currentTurnSeq: seq, provider, gitStatus,
|
|
609
628
|
});
|
|
610
629
|
// SPEC §grinder — budget grinder, pre-LLM: reclaim window on actual overflow.
|
|
611
630
|
const enforced = await this.#enforceBudget({
|
|
612
631
|
packet: requestPacket, provider, runId, loopId, turnId, sessionId, turnNumber,
|
|
613
632
|
rebuild: (telemetryErrors) => this.#buildRequestPacket({
|
|
614
|
-
initialMessages: messages,
|
|
633
|
+
initialMessages: messages, requirements, runId, loopId,
|
|
615
634
|
currentTurnSeq: seq, provider, telemetryErrors, gitStatus,
|
|
616
635
|
}),
|
|
617
636
|
});
|
|
@@ -632,14 +651,14 @@ class Engine {
|
|
|
632
651
|
// The 0.28.0 EOS-forcing root terminates the turn at the status SEND, but a
|
|
633
652
|
// grammar can't bound degeneration *inside* a statement body — this caps the
|
|
634
653
|
// decode at the free window so a runaway can't reach the context wall.
|
|
635
|
-
const genCeiling = _a.computeCeiling(provider.contextSize, this.#budgetCeiling);
|
|
654
|
+
const genCeiling = _a.computeCeiling(provider.contextSize, this.#budgetCeiling); // provider.contextSize, the immutable identity, read by the budget — §provider-surface-identity
|
|
636
655
|
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 });
|
|
656
|
+
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
657
|
// Engine splits wire-level response: emission (content, reasoning,
|
|
639
658
|
// parsed ops) → packet.assistant per Packet.json §assistant;
|
|
640
659
|
// call-metadata (usage, finishReason, model) → Turn columns per
|
|
641
660
|
// Turn.json. Mixing the two on packet.assistant was the wrong layer.
|
|
642
|
-
const { packetAssistant, callMetadata, parseErrors } = this.#splitResponse(response);
|
|
661
|
+
const { packetAssistant, callMetadata, parseErrors } = this.#splitResponse(response); // raw assistant content is opaque — split, never interpreted — §provider-guarantees-assistantraw-opaque
|
|
643
662
|
// Surface parse errors to the model's NEXT packet so it can self-
|
|
644
663
|
// correct. Without this, malformed emissions (e.g. a READ matcher
|
|
645
664
|
// body starting with `//` being interpreted as xpath) silently
|
|
@@ -685,7 +704,7 @@ class Engine {
|
|
|
685
704
|
usage_prompt: usage.prompt,
|
|
686
705
|
usage_completion: usage.completion,
|
|
687
706
|
usage_cached: usage.cached,
|
|
688
|
-
usage_cost_pico: provider.costFor(usage),
|
|
707
|
+
usage_cost_pico: provider.costFor(usage), // §provider-surface-costfor
|
|
689
708
|
finish_reason: finishReason,
|
|
690
709
|
model,
|
|
691
710
|
});
|
|
@@ -699,7 +718,8 @@ class Engine {
|
|
|
699
718
|
// entries (avoids bloating forensics with hundreds of identical refusals)
|
|
700
719
|
// and the model gets a single telemetry signal next packet so it knows
|
|
701
720
|
// its emission was truncated.
|
|
702
|
-
|
|
721
|
+
// #232 — a session's maxCommands is a tighten-only ceiling: min() the env ceiling.
|
|
722
|
+
const maxCommands = Math.min(readMaxCommands(), (await SessionSettings.read(this.#db, sessionId)).maxCommands ?? Number.POSITIVE_INFINITY);
|
|
703
723
|
const opsToDispatch = packetAssistant.ops.slice(0, maxCommands);
|
|
704
724
|
const droppedCount = opsCount - opsToDispatch.length;
|
|
705
725
|
const statuses = [];
|
|
@@ -820,9 +840,12 @@ class Engine {
|
|
|
820
840
|
// and §user) BEFORE the provider call. The same packet object is then
|
|
821
841
|
// completed with assistant + assistantRaw after the model responds, so
|
|
822
842
|
// the stored packet and the wire payload share one source of truth.
|
|
823
|
-
async #buildRequestPacket({ initialMessages,
|
|
843
|
+
async #buildRequestPacket({ initialMessages, requirements, runId, loopId, currentTurnSeq, provider, gitStatus, telemetryErrors: presetTelemetry, }) {
|
|
824
844
|
const byRole = (role) => initialMessages.filter((m) => m.role === role).map((m) => m.content).join("\n\n");
|
|
825
|
-
|
|
845
|
+
// plurnk.md (grammar/dialects) THEN the scheme catalogue: grammar 0.49+ is
|
|
846
|
+
// scheme-agnostic, so the service teaches what schemes exist + what they do
|
|
847
|
+
// at packet-time (grammar#239 item 7). SchemeRegistry.teach() assembles it.
|
|
848
|
+
const system_definition = `${byRole("system")}\n\n${this.#schemes.teach()}`;
|
|
826
849
|
// user.prompt sources from the loop's most recent prompt entry first
|
|
827
850
|
// (plurnk:///prompt/<loop_id>/<N> for the highest N written to date).
|
|
828
851
|
// This is what inject + the turn-1 foist write into. Falls back to
|
|
@@ -832,13 +855,8 @@ class Engine {
|
|
|
832
855
|
const prompt = (latestPromptRow !== undefined && typeof latestPromptRow.content === "string" && latestPromptRow.content.length > 0)
|
|
833
856
|
? latestPromptRow.content
|
|
834
857
|
: 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
858
|
// Requirements is engine-sourced, NOT threaded from callers — that threading is
|
|
841
|
-
// exactly how it went missing (
|
|
859
|
+
// exactly how it went missing (callers read the sysprompt but never the
|
|
842
860
|
// requirements). Read Paths.defaultRequirements (PLURNK_REQUIREMENTS env →
|
|
843
861
|
// requirements.md) fresh each build so edits take effect; a non-empty param wins.
|
|
844
862
|
const requirementsText = requirements.length > 0 ? requirements : await readFile(Paths.defaultRequirements, "utf8");
|
|
@@ -849,7 +867,7 @@ class Engine {
|
|
|
849
867
|
// form — wire-payload tokens may differ slightly because chat-
|
|
850
868
|
// template scaffolding adds bytes, but the subtotal tracks "what
|
|
851
869
|
// the model has to process" closely enough for budget diagnostics.
|
|
852
|
-
const countTokens = (t) => provider.countTokens(t);
|
|
870
|
+
const countTokens = (t) => provider.countTokens(t); // §provider-surface-counttokens
|
|
853
871
|
// Budget readout (SPEC.md §tokenomics). Two-pass: measure the wire-rendered
|
|
854
872
|
// index/log sections (budget-independent), install the readout with a
|
|
855
873
|
// tokensFree placeholder, measure the assembled total, resolve free,
|
|
@@ -859,21 +877,21 @@ class Engine {
|
|
|
859
877
|
// headline omitted, section lines still shown).
|
|
860
878
|
const ceiling = _a.computeCeiling(provider.contextSize, this.#budgetCeiling);
|
|
861
879
|
const scratch = {
|
|
862
|
-
system: { system_definition,
|
|
880
|
+
system: { system_definition, log },
|
|
863
881
|
user: { prompt, telemetry: { budget: "", errors: telemetryErrors, git: gitStatus }, tools: this.#collectTools(), system_requirements: requirementsText },
|
|
864
882
|
};
|
|
865
883
|
const sections = PacketWire.measureBudgetSections(scratch, countTokens);
|
|
866
884
|
scratch.user.telemetry.budget = this.#renderBudget(sections, ceiling);
|
|
867
885
|
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);
|
|
886
|
+
const tokensFree = ceiling === null ? null : Math.max(0, ceiling - total); // free floors at 0 on overshoot — §tokenomics-over-budget-floor
|
|
887
|
+
const percent = ceiling === null ? null : Math.round((total / ceiling) * 100); // usage as % of the ceiling — §tokenomics-context-percent
|
|
870
888
|
const budget = tokensFree === null
|
|
871
889
|
? scratch.user.telemetry.budget
|
|
872
890
|
: scratch.user.telemetry.budget
|
|
873
891
|
.replace(TOKEN_USAGE_PLACEHOLDER, String(total))
|
|
874
892
|
.replace(TOKEN_PERCENT_PLACEHOLDER, String(percent))
|
|
875
893
|
.replace(TOKENS_FREE_PLACEHOLDER, String(tokensFree));
|
|
876
|
-
const system = { tokens: 0, system_definition,
|
|
894
|
+
const system = { tokens: 0, system_definition, log };
|
|
877
895
|
const user = { tokens: 0, prompt, telemetry: { budget, errors: telemetryErrors, git: gitStatus }, tools: scratch.user.tools, system_requirements: requirementsText };
|
|
878
896
|
system.tokens = countTokens(PacketWire.renderSystemContent(system));
|
|
879
897
|
user.tokens = countTokens(PacketWire.renderUserContent(user));
|
|
@@ -896,10 +914,12 @@ class Engine {
|
|
|
896
914
|
for (const t of sections.log.byTurn)
|
|
897
915
|
lines.push(`| ${t.turn} | ${t.tokens} |`);
|
|
898
916
|
}
|
|
899
|
-
// The heaviest individual
|
|
900
|
-
//
|
|
917
|
+
// The heaviest individual log items — the FOLD targets behind the weight
|
|
918
|
+
// (§tokenomics {§tokenomics-largest-entries}). "items", not "entries": the readout
|
|
919
|
+
// lists log:/// rows (log items), distinct from catalog entries (plurnk.md: "EDIT
|
|
920
|
+
// is only for entries. Do not attempt to edit log items.").
|
|
901
921
|
if (sections.log.largest.length > 0) {
|
|
902
|
-
lines.push("Heaviest
|
|
922
|
+
lines.push("Heaviest items:", "| item | tokens |", "|---|--:|");
|
|
903
923
|
for (const e of sections.log.largest)
|
|
904
924
|
lines.push(`| ${e.path} | ${e.tokens} |`);
|
|
905
925
|
}
|
|
@@ -912,10 +932,11 @@ class Engine {
|
|
|
912
932
|
// contributor (gated by PLURNK_PLAN); each available executor tag then
|
|
913
933
|
// contributes its self-documenting example (plurnk-execs#7), retiring the
|
|
914
934
|
// blind EXEC.
|
|
935
|
+
// The capability sheet — the live tool surface (PLAN + wired executor tags). §tools-capability-sheet
|
|
915
936
|
#collectTools() {
|
|
916
937
|
const tools = [];
|
|
917
|
-
if (process.env.PLURNK_PLAN === "1") {
|
|
918
|
-
tools.push("* Begin every response with <<PLAN
|
|
938
|
+
if (process.env.PLURNK_PLAN === "1") { // <<PLAN advertised only when PLAN is enabled — §tools-plan-gated
|
|
939
|
+
tools.push("* Begin every response with <<PLAN:...:PLAN");
|
|
919
940
|
}
|
|
920
941
|
// Each available runtime tag contributes its self-documenting example —
|
|
921
942
|
// the example carries syntax + purpose, so there's no prose line. Tags
|
|
@@ -925,18 +946,37 @@ class Engine {
|
|
|
925
946
|
// no backticks — see packet-wire.ts).
|
|
926
947
|
if (this.#executors !== undefined) {
|
|
927
948
|
for (const tag of this.#executors.availableRuntimes()) {
|
|
928
|
-
const
|
|
929
|
-
if (example)
|
|
930
|
-
tools.push(`* ${example}`);
|
|
949
|
+
const entry = this.#executors.entry(tag);
|
|
950
|
+
if (entry?.example)
|
|
951
|
+
tools.push(`* ${entry.example}`);
|
|
952
|
+
// #note12 — link the executor's fuller doc (materialized at plurnk:///docs/<tag>);
|
|
953
|
+
// its token cost rides that manifest entry, so no inline recount here.
|
|
954
|
+
if (entry?.documentation)
|
|
955
|
+
tools.push(`* docs for ${tag}: plurnk:///docs/${tag}`);
|
|
931
956
|
}
|
|
932
957
|
}
|
|
933
958
|
return tools;
|
|
934
959
|
}
|
|
960
|
+
// #note12 — the daughter-provided reference docs (schemes' + execs' `documentation`),
|
|
961
|
+
// materialized at plurnk:///docs/<name> by loop_run (like operator docs) so the
|
|
962
|
+
// catalogue's doc-links READ and the manifest carries each doc's token cost.
|
|
963
|
+
docEntries() {
|
|
964
|
+
const out = this.#schemes.docs();
|
|
965
|
+
if (this.#executors !== undefined) {
|
|
966
|
+
for (const tag of this.#executors.availableRuntimes()) {
|
|
967
|
+
const doc = this.#executors.entry(tag)?.documentation;
|
|
968
|
+
if (doc !== undefined && doc.length > 0)
|
|
969
|
+
out.push({ name: tag, content: doc });
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
return out;
|
|
973
|
+
}
|
|
935
974
|
// SPEC §grinder — the budget grinder. Runs pre-LLM (in runTurn, after the packet
|
|
936
975
|
// is built, before provider.generate); fires only on actual overflow. Two
|
|
937
976
|
// passes, re-measuring between. Folds (never deletes) — the prior turn's logs,
|
|
938
977
|
// then the catalog except the manifest lifeline. The strike it raises and the
|
|
939
978
|
// hard-stop it can signal are returned to runLoop, which owns abandonment.
|
|
979
|
+
// §grinder-overflow-only — fires only on actual overflow, never speculatively
|
|
940
980
|
async #enforceBudget({ packet, provider, runId, loopId, turnId, sessionId, turnNumber, rebuild }) {
|
|
941
981
|
const ceiling = _a.computeCeiling(provider.contextSize, this.#budgetCeiling);
|
|
942
982
|
const measure = (p) => p.system.tokens + p.user.tokens;
|
|
@@ -946,7 +986,7 @@ class Engine {
|
|
|
946
986
|
const note = (scheme) => { folded.set(scheme, (folded.get(scheme) ?? 0) + 1); };
|
|
947
987
|
// Pass 1 — prior-turn rollback: fold the latest emissions (the ones that
|
|
948
988
|
// 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 });
|
|
989
|
+
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
990
|
for (const le of priorLogs)
|
|
951
991
|
note(le.scheme ?? "log");
|
|
952
992
|
if (priorLogs.length > 0)
|
|
@@ -955,17 +995,18 @@ class Engine {
|
|
|
955
995
|
let current = priorLogs.length > 0 ? await rebuild(errors) : packet;
|
|
956
996
|
if (measure(current) <= ceiling) {
|
|
957
997
|
this.#emitBudgetOverflow(sessionId, loopId, folded);
|
|
958
|
-
return { packet: current, fit: true, struck: turnNumber > 1 };
|
|
998
|
+
return { packet: current, fit: true, struck: turnNumber > 1 }; // turn 0/1 overflow is the environment, never a strike — §grinder-soft-turn-0-1
|
|
959
999
|
}
|
|
960
1000
|
// Prior-turn rollback is the only budget lever now: entries don't render
|
|
961
1001
|
// (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.
|
|
1002
|
+
// the packet is over and the caller hard-413s. §grinder-hard-413-abort
|
|
963
1003
|
this.#emitBudgetOverflow(sessionId, loopId, folded);
|
|
964
1004
|
return { packet: current, fit: measure(current) <= ceiling, struck: turnNumber > 1 };
|
|
965
1005
|
}
|
|
966
1006
|
// The model-facing budget event (SPEC §grinder, §telemetry): which entries left the
|
|
967
1007
|
// window, by scheme — the model's own terms, no mechanism vocabulary. The
|
|
968
1008
|
// strike this overflow triggers stays engine-internal (gamification policy).
|
|
1009
|
+
// §grinder-event-model-terms — model-facing terms only; the strike stays engine-internal
|
|
969
1010
|
#emitBudgetOverflow(sessionId, loopId, folded) {
|
|
970
1011
|
if (folded.size === 0)
|
|
971
1012
|
return;
|
|
@@ -1061,7 +1102,7 @@ class Engine {
|
|
|
1061
1102
|
source: r.source,
|
|
1062
1103
|
}));
|
|
1063
1104
|
}
|
|
1064
|
-
// §env-delta — at pre-turn build, surface what changed in the shared world since this
|
|
1105
|
+
// §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
1106
|
// run last looked. No per-run snapshot (§machine-processes "a run is its log"): every
|
|
1066
1107
|
// edit is already a span-carrying log row, so PULL other actors' EDITs on shared
|
|
1067
1108
|
// entries since this run's prior turn — real cross-run edits and the plurnk run's
|
|
@@ -1084,6 +1125,19 @@ class Engine {
|
|
|
1084
1125
|
});
|
|
1085
1126
|
written++;
|
|
1086
1127
|
}
|
|
1128
|
+
// §run-scheme — loop-terminations: a sibling's loop reaching terminal surfaces the
|
|
1129
|
+
// same way an entry-change does, carrying its deliverable (the SEND body) or the
|
|
1130
|
+
// abandonment reason. Folded, attributed to the terminated run.
|
|
1131
|
+
const terms = await this.#db.engine_pull_loop_terminations.all({ session_id: sessionId, run_id: runId, since });
|
|
1132
|
+
for (const t of terms) {
|
|
1133
|
+
await this.#db.engine_insert_loop_termination_delta.run({
|
|
1134
|
+
run_id: runId, loop_id: loopId, turn_id: turnId, sequence: fromSequence + written,
|
|
1135
|
+
source: String(t.run_id), pathname: `/${t.run_name}`,
|
|
1136
|
+
rx: t.terminal_message ?? `loop "${t.prompt}" ended (${t.status})`,
|
|
1137
|
+
status: t.status,
|
|
1138
|
+
});
|
|
1139
|
+
written++;
|
|
1140
|
+
}
|
|
1087
1141
|
return written;
|
|
1088
1142
|
}
|
|
1089
1143
|
// §env-delta — the filesystem as an actor. Ambient disk divergences detected at
|
|
@@ -1092,11 +1146,12 @@ class Engine {
|
|
|
1092
1146
|
// world changed," so the fiction keeps its perspective aligned with what its tooling
|
|
1093
1147
|
// would show. The fiction lives in the plurnk run's log; every other run pulls it
|
|
1094
1148
|
// through the one delta path, exactly like a sibling's real edit.
|
|
1149
|
+
// §membership-emi-divergence-signal — disk divergences logged as the plurnk run's source=file EDIT fictions
|
|
1095
1150
|
async #logFsFictions(sessionId, divergences) {
|
|
1096
1151
|
if (divergences.length === 0)
|
|
1097
1152
|
return;
|
|
1098
1153
|
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",
|
|
1154
|
+
?? await this.#db.envelope_insert_run.get({ session_id: sessionId, name: "plurnk", origin: "plurnk" });
|
|
1100
1155
|
if (run === undefined)
|
|
1101
1156
|
throw new Error("logFsFictions: plurnk run resolution returned no row");
|
|
1102
1157
|
const loop = await this.#db.envelope_insert_client_loop.get({ run_id: run.id });
|
|
@@ -1129,6 +1184,7 @@ class Engine {
|
|
|
1129
1184
|
signal: this.#loopAborts.get(loopId)?.signal,
|
|
1130
1185
|
streamEventNotify: this.#streamEventNotify,
|
|
1131
1186
|
wakeRunNotify: this.#wakeRunNotify,
|
|
1187
|
+
injectRun: this.#injectRun,
|
|
1132
1188
|
mimetypes: this.#mimetypes,
|
|
1133
1189
|
tokenize: this.#tokenize,
|
|
1134
1190
|
pushTelemetry: (event) => this.#pushTelemetry(sessionId, loopId, event),
|
|
@@ -1171,10 +1227,10 @@ class Engine {
|
|
|
1171
1227
|
result = await this.#run("exec", statement, schemeCtx);
|
|
1172
1228
|
}
|
|
1173
1229
|
else {
|
|
1174
|
-
result = await this.#run(this.#schemeNameOf(statement.target), statement, schemeCtx);
|
|
1230
|
+
result = await this.#run(this.#schemeNameOf(statement.target), statement, schemeCtx); // §op-methods-op-dispatch
|
|
1175
1231
|
}
|
|
1176
1232
|
}
|
|
1177
|
-
catch (err) {
|
|
1233
|
+
catch (err) { // a scheme exception becomes the op's 500 outcome — §scheme-surface-exception-500
|
|
1178
1234
|
result = {
|
|
1179
1235
|
status: 500,
|
|
1180
1236
|
error: err instanceof Error ? err.message : String(err),
|
|
@@ -1183,7 +1239,7 @@ class Engine {
|
|
|
1183
1239
|
}
|
|
1184
1240
|
const logEntryId = await this.#writeLog({ statement, result, runId, loopId, turnId, sequence, origin });
|
|
1185
1241
|
onDispatch?.(logEntryId);
|
|
1186
|
-
// Proposal lifecycle (SPEC.md §engine-rails + §methods loop.resolve). When a
|
|
1242
|
+
// Proposal lifecycle (SPEC.md §engine-rails + §methods loop.resolve; §proposal-202-pauses). When a
|
|
1187
1243
|
// scheme returns status 202, the entry is written as state='proposed';
|
|
1188
1244
|
// dispatch then PAUSES on a per-entry waiter until resolution
|
|
1189
1245
|
// arrives via Engine.resolveProposal (from the loop/resolve RPC,
|
|
@@ -1208,7 +1264,10 @@ class Engine {
|
|
|
1208
1264
|
// YOLO listener auto-resolves) BEFORE awaiting — they may
|
|
1209
1265
|
// resolve synchronously inside their handlers.
|
|
1210
1266
|
const target = this.#extractTarget(statement.target);
|
|
1211
|
-
const flags = await this.#loadLoopFlags(loopId);
|
|
1267
|
+
const flags = await this.#loadLoopFlags(loopId); // the loop/proposal notification carries flags (yolo) — §dual-yolo-proposal-carries-flags
|
|
1268
|
+
// #note10 — if the target diverged on disk this turn, the model's EDIT is based
|
|
1269
|
+
// on a stale read; flag it so a YOLO auto-accept rejects instead of clobbering.
|
|
1270
|
+
const diverged = await this.#db.engine_target_diverged_this_turn.get({ run_id: runId, turn_id: turnId, scheme: target.scheme, pathname: target.pathname });
|
|
1212
1271
|
const event = {
|
|
1213
1272
|
logEntryId, sessionId, runId, loopId, turnId,
|
|
1214
1273
|
op: statement.op,
|
|
@@ -1216,6 +1275,7 @@ class Engine {
|
|
|
1216
1275
|
body: typeof result.body === "string" ? result.body : "",
|
|
1217
1276
|
attrs: (result.attrs ?? {}),
|
|
1218
1277
|
flags,
|
|
1278
|
+
staleClobberRisk: diverged !== undefined,
|
|
1219
1279
|
};
|
|
1220
1280
|
for (const listener of this.#proposalPendingListeners) {
|
|
1221
1281
|
try {
|
|
@@ -1245,6 +1305,7 @@ class Engine {
|
|
|
1245
1305
|
}
|
|
1246
1306
|
return result;
|
|
1247
1307
|
}
|
|
1308
|
+
// On accept, run the scheme's applyResolution — File writes disk, Exec spawns. §proposal-accept-applies
|
|
1248
1309
|
async #runApplyResolution(statement, originalResult, resolution, ids) {
|
|
1249
1310
|
const { sessionId, runId, loopId, turnId } = ids;
|
|
1250
1311
|
if (resolution.decision !== "accept")
|
|
@@ -1406,7 +1467,7 @@ class Engine {
|
|
|
1406
1467
|
// transitions to cancelled with outcome='timeout'.
|
|
1407
1468
|
if (this.#pendingProposals.has(logEntryId)) {
|
|
1408
1469
|
this.#pendingProposals.delete(logEntryId);
|
|
1409
|
-
resolve({ decision: "cancel", outcome: "timeout" });
|
|
1470
|
+
resolve({ decision: "cancel", outcome: "timeout" }); // §proposal-timeout-cancels
|
|
1410
1471
|
}
|
|
1411
1472
|
}, timeoutMs);
|
|
1412
1473
|
this.#pendingProposals.set(logEntryId, { resolve, timeoutHandle });
|
|
@@ -1415,8 +1476,8 @@ class Engine {
|
|
|
1415
1476
|
async #applyResolution(logEntryId, resolution) {
|
|
1416
1477
|
// Map decision → terminal state + HTTP-aligned status:
|
|
1417
1478
|
// accept → state='resolved', status=200
|
|
1418
|
-
// reject → state='failed', status=400, outcome='rejected' (default)
|
|
1419
|
-
// cancel → state='cancelled',status=499, outcome='loop_aborted' (default)
|
|
1479
|
+
// reject → state='failed', status=400, outcome='rejected' (default) §proposal-reject-fails
|
|
1480
|
+
// cancel → state='cancelled',status=499, outcome='loop_aborted' (default) §proposal-cancel-aborts
|
|
1420
1481
|
// resolution.outcome wins over the default when supplied; this is how
|
|
1421
1482
|
// veto filters (Phase E.2 proposal.accepting) can specify a more
|
|
1422
1483
|
// precise outcome string like 'policy_veto' or 'timeout'.
|
|
@@ -1462,6 +1523,11 @@ class Engine {
|
|
|
1462
1523
|
if (statement.op === "EXEC") {
|
|
1463
1524
|
return this.#denyIfDisallowed("exec", origin);
|
|
1464
1525
|
}
|
|
1526
|
+
// A run-fork (COPY src=run://) is gated by run://'s writableBy — its body
|
|
1527
|
+
// is a fork prompt, not a dst path, so the entry-COPY dst-parse below
|
|
1528
|
+
// doesn't apply. §machine-processes
|
|
1529
|
+
if (this.#isRunFork(statement))
|
|
1530
|
+
return this.#denyIfDisallowed("run", origin);
|
|
1465
1531
|
if (statement.op === "COPY" || statement.op === "MOVE") {
|
|
1466
1532
|
const dst = statement.op === "COPY" ? (statement.body === null ? null : parsePath(statement.body)) : statement.body;
|
|
1467
1533
|
const dstScheme = this.#schemeNameOf(dst);
|
|
@@ -1492,7 +1558,7 @@ class Engine {
|
|
|
1492
1558
|
return null;
|
|
1493
1559
|
if (manifest.writableBy.includes(origin))
|
|
1494
1560
|
return null;
|
|
1495
|
-
return { status: 403, error: `writer '${origin}' is not in writableBy for scheme '${schemeName}'` };
|
|
1561
|
+
return { status: 403, error: `writer '${origin}' is not in writableBy for scheme '${schemeName}'` }; // §scheme-surface-writableby-403
|
|
1496
1562
|
}
|
|
1497
1563
|
// Per-loop flag gating. Schemes self-declare their flag affinity in
|
|
1498
1564
|
// their manifest (excludedInAsk / requiresWeb /
|
|
@@ -1516,14 +1582,50 @@ class Engine {
|
|
|
1516
1582
|
return null;
|
|
1517
1583
|
return { status: 403, error: `scheme '${scheme}' is inactive under current loop flags` };
|
|
1518
1584
|
};
|
|
1585
|
+
if (this.#isRunFork(statement))
|
|
1586
|
+
return check(statement.target); // body is a fork prompt, not a dst path
|
|
1519
1587
|
if (statement.op === "COPY" || statement.op === "MOVE") {
|
|
1520
1588
|
return check(statement.target) ?? check(statement.op === "COPY" ? (statement.body === null ? null : parsePath(statement.body)) : statement.body);
|
|
1521
1589
|
}
|
|
1522
1590
|
return check(statement.target);
|
|
1523
1591
|
}
|
|
1592
|
+
// A COPY whose SOURCE is run:// is a run-fork, not an entry-copy — its body
|
|
1593
|
+
// is the fork's seed prompt, not a destination path. The COPY gates and
|
|
1594
|
+
// #handleCopy branch on this so they never parse the prompt as a dst path.
|
|
1595
|
+
#isRunFork(statement) {
|
|
1596
|
+
return statement.op === "COPY" && this.#schemeNameOf(statement.target) === "run";
|
|
1597
|
+
}
|
|
1598
|
+
// COPY(run:///<src>):prompt — fork: deep-copy the source run's log into a new
|
|
1599
|
+
// run (Fork), then start it with the prompt (ctx.injectRun). Source "."/"" =
|
|
1600
|
+
// self (ctx.runId); a name resolves within the session (404 if absent).
|
|
1601
|
+
// §machine-processes-fork-copies-the-log
|
|
1602
|
+
async #handleRunFork(statement, ctx) {
|
|
1603
|
+
const target = statement.target;
|
|
1604
|
+
if (target === null)
|
|
1605
|
+
return { status: 400, error: "run:// fork requires a source run" };
|
|
1606
|
+
const name = pathnameFromPath(target).replace(/^\/+/, "");
|
|
1607
|
+
let srcRunId = ctx.runId;
|
|
1608
|
+
if (name !== "" && name !== ".") {
|
|
1609
|
+
const row = await this.#db.run_resolve_by_name.get({ session_id: ctx.sessionId, name });
|
|
1610
|
+
if (row === undefined)
|
|
1611
|
+
return { status: 404, error: `run:///${name} not found in this session` };
|
|
1612
|
+
srcRunId = row.id;
|
|
1613
|
+
}
|
|
1614
|
+
if (ctx.injectRun === undefined)
|
|
1615
|
+
throw new Error("run fork: injectRun capability absent");
|
|
1616
|
+
const denied = await RunCap.deny(this.#db, ctx.sessionId);
|
|
1617
|
+
if (denied !== null)
|
|
1618
|
+
return denied;
|
|
1619
|
+
const branchRunId = await Fork.fork(this.#db, srcRunId);
|
|
1620
|
+
const branch = await this.#db.fork_get_run.get({ id: branchRunId });
|
|
1621
|
+
await ctx.injectRun({ sessionId: ctx.sessionId, runId: branchRunId, prompt: typeof statement.body === "string" ? statement.body : "" });
|
|
1622
|
+
return { status: 200, body: branch?.name ?? "" };
|
|
1623
|
+
}
|
|
1524
1624
|
async #handleCopy(statement, ctx) {
|
|
1525
1625
|
if (statement.op !== "COPY")
|
|
1526
1626
|
throw new Error("unreachable");
|
|
1627
|
+
if (this.#isRunFork(statement))
|
|
1628
|
+
return await this.#handleRunFork(statement, ctx);
|
|
1527
1629
|
const srcPath = statement.target;
|
|
1528
1630
|
// COPY's body is an opaque raw string (grammar §COPY: a dest path OR a run-fork
|
|
1529
1631
|
// prompt); parse it to the dest path. Non-path bodies (run:// fork prompts) are
|
|
@@ -1542,17 +1644,17 @@ class Engine {
|
|
|
1542
1644
|
const dstPath = statement.body;
|
|
1543
1645
|
if (srcPath === null)
|
|
1544
1646
|
return { status: 400, error: "MOVE requires source path" };
|
|
1545
|
-
// MOVE is relocation only — deletion is KILL's job (§move). The /dev/null
|
|
1647
|
+
// MOVE is relocation only — deletion is KILL's job (§move, §move-dev-null-not-special). The /dev/null
|
|
1546
1648
|
// and null-body delete-by-MOVE back-compat is retired: no silent debt.
|
|
1547
1649
|
if (dstPath === null)
|
|
1548
|
-
return { status: 400, error: "MOVE requires a destination; use KILL to delete" };
|
|
1650
|
+
return { status: 400, error: "MOVE requires a destination; use KILL to delete" }; // §move-null-body-400
|
|
1549
1651
|
const srcSchemeName = this.#schemeNameOf(srcPath);
|
|
1550
1652
|
if (srcSchemeName === null)
|
|
1551
1653
|
return { status: 400, error: "MOVE source must be a URL path with a scheme" };
|
|
1552
1654
|
const srcHandler = this.#schemes.get(srcSchemeName);
|
|
1553
1655
|
if (srcHandler === undefined || typeof srcHandler.deleteEntry !== "function")
|
|
1554
1656
|
return { status: 501 };
|
|
1555
|
-
// Relocation: COPY then DELETE source.
|
|
1657
|
+
// Relocation: COPY then DELETE source (§move-relocation-deletes-source).
|
|
1556
1658
|
const copyResult = await this.#copyOrchestration({ statement, srcPath, dstPath, ctx });
|
|
1557
1659
|
if (copyResult.status >= 400)
|
|
1558
1660
|
return copyResult;
|
|
@@ -1595,6 +1697,23 @@ class Engine {
|
|
|
1595
1697
|
return { status: 501 };
|
|
1596
1698
|
return await execHandler.kill(pathnameFromPath(path), ctx);
|
|
1597
1699
|
}
|
|
1700
|
+
if (schemeName === "run") {
|
|
1701
|
+
// terminate — abort any run by address; whoever holds it may end it.
|
|
1702
|
+
// `.`/"" = self. cancelRun (→ Daemon.cancelDrain) aborts the run's signal
|
|
1703
|
+
// (its loop closes 499); an idle run is a no-op-200, a missing run 404.
|
|
1704
|
+
const name = pathnameFromPath(path).replace(/^\/+/, "");
|
|
1705
|
+
let runId = ctx.runId;
|
|
1706
|
+
if (name !== "" && name !== ".") {
|
|
1707
|
+
const row = await this.#db.run_resolve_by_name.get({ session_id: ctx.sessionId, name });
|
|
1708
|
+
if (row === undefined)
|
|
1709
|
+
return { status: 404, error: `run:///${name} not found in this session` };
|
|
1710
|
+
runId = row.id;
|
|
1711
|
+
}
|
|
1712
|
+
if (this.#cancelRun === undefined)
|
|
1713
|
+
throw new Error("run kill: cancelRun capability absent");
|
|
1714
|
+
this.#cancelRun(runId);
|
|
1715
|
+
return { status: 200 };
|
|
1716
|
+
}
|
|
1598
1717
|
const handler = this.#schemes.get(schemeName);
|
|
1599
1718
|
if (handler === undefined || typeof handler.deleteEntry !== "function")
|
|
1600
1719
|
return { status: 501 };
|
|
@@ -1612,6 +1731,7 @@ class Engine {
|
|
|
1612
1731
|
throw new Error("unreachable");
|
|
1613
1732
|
return { status: 200 };
|
|
1614
1733
|
}
|
|
1734
|
+
// Same- and cross-scheme COPY share one orchestrator — §copy-cross-scheme-copy §move-cross-scheme-move
|
|
1615
1735
|
async #copyOrchestration({ statement, srcPath, dstPath, ctx }) {
|
|
1616
1736
|
const srcSchemeName = this.#schemeNameOf(srcPath);
|
|
1617
1737
|
const dstSchemeName = this.#schemeNameOf(dstPath);
|
|
@@ -1627,7 +1747,7 @@ class Engine {
|
|
|
1627
1747
|
const dstPathname = pathnameFromPath(dstPath);
|
|
1628
1748
|
const srcResult = await srcHandler.readEntry(srcPathname, ctx);
|
|
1629
1749
|
if (srcResult.status !== 200 || srcResult.entry === null)
|
|
1630
|
-
return { status: 404, error: `COPY/MOVE source not found: ${srcSchemeName}://${srcPathname}` };
|
|
1750
|
+
return { status: 404, error: `COPY/MOVE source not found: ${srcSchemeName}://${srcPathname}` }; // §copy-missing-source-404 §move-missing-source-404
|
|
1631
1751
|
const entry = srcResult.entry;
|
|
1632
1752
|
// Destination read — the conflict/no-op verdict is deferred until the
|
|
1633
1753
|
// to-be-written content is known (after <L> slice + tag resolution below),
|
|
@@ -1641,7 +1761,7 @@ class Engine {
|
|
|
1641
1761
|
for (const [channelName, channelData] of Object.entries(entry.channels)) {
|
|
1642
1762
|
const expectedMimetype = dstChannels[channelName];
|
|
1643
1763
|
if (expectedMimetype !== undefined && expectedMimetype !== channelData.mimetype) {
|
|
1644
|
-
return { status: 415, error: `mimetype mismatch on channel '${channelName}': ${channelData.mimetype} vs ${expectedMimetype}` };
|
|
1764
|
+
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
1765
|
}
|
|
1646
1766
|
}
|
|
1647
1767
|
// `<L>` source range slicing per SPEC.md §op-invariants (symmetric with READ
|
|
@@ -1663,7 +1783,7 @@ class Engine {
|
|
|
1663
1783
|
}
|
|
1664
1784
|
channels = sliced;
|
|
1665
1785
|
}
|
|
1666
|
-
// Tag resolution: signal = replace; absent/empty = carry from source
|
|
1786
|
+
// Tag resolution: signal = replace (§copy-signal-replaces-source-tags); absent/empty = carry from source (§copy-no-signal-carries-source-tags)
|
|
1667
1787
|
const tags = (Array.isArray(statement.signal) && statement.signal.length > 0)
|
|
1668
1788
|
? statement.signal
|
|
1669
1789
|
: entry.tags;
|
|
@@ -1679,8 +1799,8 @@ class Engine {
|
|
|
1679
1799
|
&& writeNames.every((n, i) => n === dstNames[i] && (channels[n]?.content ?? "") === (dstChannels[n]?.content ?? ""));
|
|
1680
1800
|
const sameTags = [...tags].sort().join("") === [...dstExisting.entry.tags].sort().join("");
|
|
1681
1801
|
if (sameContent && sameTags)
|
|
1682
|
-
return { status: 304 };
|
|
1683
|
-
return { status: 409, error: `COPY/MOVE destination exists: ${dstSchemeName}://${dstPathname}` };
|
|
1802
|
+
return { status: 304 }; // identical → §copy-noop-304
|
|
1803
|
+
return { status: 409, error: `COPY/MOVE destination exists: ${dstSchemeName}://${dstPathname}` }; // §copy-conflict-409
|
|
1684
1804
|
}
|
|
1685
1805
|
const writeResult = await dstHandler.writeEntry(dstPathname, { channels, tags }, ctx);
|
|
1686
1806
|
// A file dest returns 202 (disk write → §membership review): propagate the
|
|
@@ -1696,7 +1816,10 @@ class Engine {
|
|
|
1696
1816
|
if (status === null)
|
|
1697
1817
|
return { status: 400 };
|
|
1698
1818
|
if (status === 200 || status === 499) {
|
|
1699
|
-
|
|
1819
|
+
// the loop's terminal message — its deliverable — rides the termination delta.
|
|
1820
|
+
const body = statement.body;
|
|
1821
|
+
const message = body === null ? null : typeof body === "string" ? body : body.raw;
|
|
1822
|
+
await this.#db.engine_loop_set_status.run({ status, loop_id: loopId, message });
|
|
1700
1823
|
}
|
|
1701
1824
|
return { status };
|
|
1702
1825
|
}
|
|
@@ -1817,12 +1940,15 @@ class Engine {
|
|
|
1817
1940
|
#extractTarget(path) {
|
|
1818
1941
|
if (path === null)
|
|
1819
1942
|
return { scheme: null, username: null, password: null, hostname: null, port: null, pathname: null, params: null, fragment: null };
|
|
1943
|
+
// `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.
|
|
1944
|
+
if (path.kind === "regex")
|
|
1945
|
+
return { scheme: null, username: null, password: null, hostname: null, port: null, pathname: path.raw, params: null, fragment: null }; // regex source — no decode
|
|
1820
1946
|
if (path.kind === "local")
|
|
1821
|
-
return { scheme: null, username: null, password: null, hostname: null, port: null, pathname: path.raw, params: null, fragment: null };
|
|
1947
|
+
return { scheme: null, username: null, password: null, hostname: null, port: null, pathname: decodePathParens(path.raw), params: null, fragment: null }; // #239 item 4
|
|
1822
1948
|
const scheme = path.scheme === "file" ? null : path.scheme;
|
|
1823
1949
|
return {
|
|
1824
1950
|
scheme, username: path.username, password: path.password,
|
|
1825
|
-
hostname: path.hostname, port: path.port, pathname: path.pathname,
|
|
1951
|
+
hostname: path.hostname, port: path.port, pathname: decodePathParens(path.pathname), // #239 item 4
|
|
1826
1952
|
params: JSON.stringify(path.params), fragment: path.fragment,
|
|
1827
1953
|
};
|
|
1828
1954
|
}
|