@plurnk/plurnk-service 0.7.0 → 0.8.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 +104 -74
- package/bin/plurnk-service.ts +132 -0
- package/dist/Paths.d.ts +8 -0
- package/dist/Paths.d.ts.map +1 -0
- package/dist/Paths.js +47 -0
- package/dist/Paths.js.map +1 -0
- package/dist/content/index.d.ts +9 -0
- package/dist/content/index.d.ts.map +1 -0
- package/dist/content/index.js +10 -0
- package/dist/content/index.js.map +1 -0
- package/dist/content/line-marker.d.ts +26 -0
- package/dist/content/line-marker.d.ts.map +1 -0
- package/dist/content/line-marker.js +323 -0
- package/dist/content/line-marker.js.map +1 -0
- package/dist/content/matcher.d.ts +15 -0
- package/dist/content/matcher.d.ts.map +1 -0
- package/dist/content/matcher.js +112 -0
- package/dist/content/matcher.js.map +1 -0
- package/dist/content/mimetype-binary.d.ts +9 -0
- package/dist/content/mimetype-binary.d.ts.map +1 -0
- package/dist/content/mimetype-binary.js +86 -0
- package/dist/content/mimetype-binary.js.map +1 -0
- package/dist/content/path-mimetype.d.ts +6 -0
- package/dist/content/path-mimetype.d.ts.map +1 -0
- package/dist/content/path-mimetype.js +49 -0
- package/dist/content/path-mimetype.js.map +1 -0
- package/dist/content/read-resolve.d.ts +20 -0
- package/dist/content/read-resolve.d.ts.map +1 -0
- package/dist/content/read-resolve.js +60 -0
- package/dist/content/read-resolve.js.map +1 -0
- package/dist/core/ChannelWrite.d.ts +33 -30
- package/dist/core/ChannelWrite.d.ts.map +1 -1
- package/dist/core/ChannelWrite.js +43 -41
- package/dist/core/ChannelWrite.js.map +1 -1
- package/dist/core/Engine.d.ts +14 -10
- package/dist/core/Engine.d.ts.map +1 -1
- package/dist/core/Engine.js +269 -94
- package/dist/core/Engine.js.map +1 -1
- package/dist/core/EnvFlags.d.ts +6 -3
- package/dist/core/EnvFlags.d.ts.map +1 -1
- package/dist/core/EnvFlags.js +62 -60
- package/dist/core/EnvFlags.js.map +1 -1
- package/dist/core/PluginLoader.d.ts +6 -3
- package/dist/core/PluginLoader.d.ts.map +1 -1
- package/dist/core/PluginLoader.js +77 -73
- package/dist/core/PluginLoader.js.map +1 -1
- package/dist/core/ProviderInstantiate.d.ts +4 -2
- package/dist/core/ProviderInstantiate.d.ts.map +1 -1
- package/dist/core/ProviderInstantiate.js +23 -22
- package/dist/core/ProviderInstantiate.js.map +1 -1
- package/dist/core/SchemeRegistry.d.ts +1 -1
- package/dist/core/SchemeRegistry.d.ts.map +1 -1
- package/dist/core/SchemeRegistry.js +3 -3
- package/dist/core/SchemeRegistry.js.map +1 -1
- package/dist/core/git-membership.d.ts +8 -0
- package/dist/core/git-membership.d.ts.map +1 -0
- package/dist/core/git-membership.js +125 -0
- package/dist/core/git-membership.js.map +1 -0
- package/dist/core/packet-wire.d.ts +47 -6
- package/dist/core/packet-wire.d.ts.map +1 -1
- package/dist/core/packet-wire.js +376 -312
- package/dist/core/packet-wire.js.map +1 -1
- package/dist/core/resolveForLoop.d.ts +14 -0
- package/dist/core/resolveForLoop.d.ts.map +1 -0
- package/dist/core/resolveForLoop.js +34 -0
- package/dist/core/resolveForLoop.js.map +1 -0
- package/dist/core/results.d.ts +40 -0
- package/dist/core/results.d.ts.map +1 -0
- package/dist/core/results.js +52 -0
- package/dist/core/results.js.map +1 -0
- package/dist/core/scheme-types.d.ts +5 -4
- package/dist/core/scheme-types.d.ts.map +1 -1
- package/dist/core/scheme-types.js +4 -4
- package/dist/core/scheme-types.js.map +1 -1
- package/dist/core/types.d.ts +27 -0
- package/dist/core/types.d.ts.map +1 -0
- package/dist/core/types.js +17 -0
- package/dist/core/types.js.map +1 -0
- package/dist/index.d.ts +1 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -43
- 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 +37 -21
- package/dist/schemes/Exec.js.map +1 -1
- package/dist/schemes/File.d.ts +0 -1
- package/dist/schemes/File.d.ts.map +1 -1
- package/dist/schemes/File.js +32 -66
- package/dist/schemes/File.js.map +1 -1
- package/dist/schemes/Known.d.ts.map +1 -1
- package/dist/schemes/Known.js +13 -13
- package/dist/schemes/Known.js.map +1 -1
- package/dist/schemes/Log.d.ts.map +1 -1
- package/dist/schemes/Log.js +8 -52
- package/dist/schemes/Log.js.map +1 -1
- package/dist/schemes/Plurnk.d.ts.map +1 -1
- package/dist/schemes/Plurnk.js +13 -13
- package/dist/schemes/Plurnk.js.map +1 -1
- package/dist/schemes/Skill.d.ts.map +1 -1
- package/dist/schemes/Skill.js +13 -13
- package/dist/schemes/Skill.js.map +1 -1
- package/dist/schemes/Unknown.d.ts.map +1 -1
- package/dist/schemes/Unknown.js +13 -13
- package/dist/schemes/Unknown.js.map +1 -1
- package/dist/schemes/_entry-crud.d.ts +5 -3
- package/dist/schemes/_entry-crud.d.ts.map +1 -1
- package/dist/schemes/_entry-crud.js +55 -50
- package/dist/schemes/_entry-crud.js.map +1 -1
- package/dist/schemes/_entry-find.d.ts +10 -3
- package/dist/schemes/_entry-find.d.ts.map +1 -1
- package/dist/schemes/_entry-find.js +99 -77
- package/dist/schemes/_entry-find.js.map +1 -1
- package/dist/schemes/_entry-manifest.d.ts +6 -0
- package/dist/schemes/_entry-manifest.d.ts.map +1 -0
- package/dist/schemes/_entry-manifest.js +45 -0
- package/dist/schemes/_entry-manifest.js.map +1 -0
- package/dist/schemes/_entry-ops.d.ts +7 -4
- package/dist/schemes/_entry-ops.d.ts.map +1 -1
- package/dist/schemes/_entry-ops.js +198 -316
- package/dist/schemes/_entry-ops.js.map +1 -1
- package/dist/schemes/_entry-send.d.ts +4 -1
- package/dist/schemes/_entry-send.d.ts.map +1 -1
- package/dist/schemes/_entry-send.js +57 -55
- package/dist/schemes/_entry-send.js.map +1 -1
- package/dist/server/ClientConnection.js +3 -3
- package/dist/server/ClientConnection.js.map +1 -1
- package/dist/server/Daemon.d.ts +5 -5
- package/dist/server/Daemon.d.ts.map +1 -1
- package/dist/server/Daemon.js +221 -176
- package/dist/server/Daemon.js.map +1 -1
- package/dist/server/clientTurn.d.ts +4 -1
- package/dist/server/clientTurn.d.ts.map +1 -1
- package/dist/server/clientTurn.js +19 -17
- package/dist/server/clientTurn.js.map +1 -1
- package/dist/server/dsl.d.ts +19 -16
- package/dist/server/dsl.d.ts.map +1 -1
- package/dist/server/dsl.js +127 -105
- package/dist/server/dsl.js.map +1 -1
- package/dist/server/envelope.d.ts +22 -19
- package/dist/server/envelope.d.ts.map +1 -1
- package/dist/server/envelope.js +116 -102
- package/dist/server/envelope.js.map +1 -1
- package/dist/server/logEntry.d.ts +4 -1
- package/dist/server/logEntry.d.ts.map +1 -1
- package/dist/server/logEntry.js +41 -39
- package/dist/server/logEntry.js.map +1 -1
- package/dist/server/methods/_dispatchAsClient.d.ts +3 -1
- package/dist/server/methods/_dispatchAsClient.d.ts.map +1 -1
- package/dist/server/methods/_dispatchAsClient.js +31 -29
- package/dist/server/methods/_dispatchAsClient.js.map +1 -1
- package/dist/server/methods/discover.d.ts +3 -1
- package/dist/server/methods/discover.d.ts.map +1 -1
- package/dist/server/methods/discover.js +8 -6
- package/dist/server/methods/discover.js.map +1 -1
- package/dist/server/methods/entry_read.d.ts +4 -1
- package/dist/server/methods/entry_read.d.ts.map +1 -1
- package/dist/server/methods/entry_read.js +54 -52
- package/dist/server/methods/entry_read.js.map +1 -1
- package/dist/server/methods/log_read.d.ts +4 -1
- package/dist/server/methods/log_read.d.ts.map +1 -1
- package/dist/server/methods/log_read.js +38 -36
- package/dist/server/methods/log_read.js.map +1 -1
- package/dist/server/methods/loop_cancel.d.ts +3 -1
- package/dist/server/methods/loop_cancel.d.ts.map +1 -1
- package/dist/server/methods/loop_cancel.js +19 -17
- package/dist/server/methods/loop_cancel.js.map +1 -1
- package/dist/server/methods/loop_resolve.d.ts +3 -1
- package/dist/server/methods/loop_resolve.d.ts.map +1 -1
- package/dist/server/methods/loop_resolve.js +42 -40
- package/dist/server/methods/loop_resolve.js.map +1 -1
- package/dist/server/methods/loop_run.d.ts +3 -1
- package/dist/server/methods/loop_run.d.ts.map +1 -1
- package/dist/server/methods/loop_run.js +104 -102
- package/dist/server/methods/loop_run.js.map +1 -1
- package/dist/server/methods/op_copy.d.ts +3 -1
- package/dist/server/methods/op_copy.d.ts.map +1 -1
- package/dist/server/methods/op_copy.js +25 -23
- package/dist/server/methods/op_copy.js.map +1 -1
- package/dist/server/methods/op_dispatch.d.ts +3 -1
- package/dist/server/methods/op_dispatch.d.ts.map +1 -1
- package/dist/server/methods/op_dispatch.js +18 -16
- package/dist/server/methods/op_dispatch.js.map +1 -1
- package/dist/server/methods/op_edit.d.ts +3 -1
- package/dist/server/methods/op_edit.d.ts.map +1 -1
- package/dist/server/methods/op_edit.js +23 -21
- package/dist/server/methods/op_edit.js.map +1 -1
- package/dist/server/methods/op_exec.d.ts +3 -1
- package/dist/server/methods/op_exec.d.ts.map +1 -1
- package/dist/server/methods/op_exec.js +20 -18
- package/dist/server/methods/op_exec.js.map +1 -1
- package/dist/server/methods/op_find.d.ts +3 -1
- package/dist/server/methods/op_find.d.ts.map +1 -1
- package/dist/server/methods/op_find.js +23 -21
- package/dist/server/methods/op_find.js.map +1 -1
- package/dist/server/methods/op_hide.d.ts +3 -1
- package/dist/server/methods/op_hide.d.ts.map +1 -1
- package/dist/server/methods/op_hide.js +23 -21
- package/dist/server/methods/op_hide.js.map +1 -1
- package/dist/server/methods/op_move.d.ts +3 -1
- package/dist/server/methods/op_move.d.ts.map +1 -1
- package/dist/server/methods/op_move.js +23 -21
- package/dist/server/methods/op_move.js.map +1 -1
- package/dist/server/methods/op_parse.d.ts +3 -1
- package/dist/server/methods/op_parse.d.ts.map +1 -1
- package/dist/server/methods/op_parse.js +24 -22
- package/dist/server/methods/op_parse.js.map +1 -1
- package/dist/server/methods/op_read.d.ts +3 -1
- package/dist/server/methods/op_read.d.ts.map +1 -1
- package/dist/server/methods/op_read.js +23 -21
- package/dist/server/methods/op_read.js.map +1 -1
- package/dist/server/methods/op_send.d.ts +3 -1
- package/dist/server/methods/op_send.d.ts.map +1 -1
- package/dist/server/methods/op_send.js +22 -20
- package/dist/server/methods/op_send.js.map +1 -1
- package/dist/server/methods/op_show.d.ts +3 -1
- package/dist/server/methods/op_show.d.ts.map +1 -1
- package/dist/server/methods/op_show.js +23 -21
- package/dist/server/methods/op_show.js.map +1 -1
- package/dist/server/methods/ping.d.ts +3 -1
- package/dist/server/methods/ping.d.ts.map +1 -1
- package/dist/server/methods/ping.js +8 -6
- package/dist/server/methods/ping.js.map +1 -1
- package/dist/server/methods/providers_list.d.ts +3 -1
- package/dist/server/methods/providers_list.d.ts.map +1 -1
- package/dist/server/methods/providers_list.js +19 -17
- package/dist/server/methods/providers_list.js.map +1 -1
- package/dist/server/methods/session_attach.d.ts +3 -1
- package/dist/server/methods/session_attach.d.ts.map +1 -1
- package/dist/server/methods/session_attach.js +43 -41
- package/dist/server/methods/session_attach.js.map +1 -1
- package/dist/server/methods/session_create.d.ts +3 -1
- package/dist/server/methods/session_create.d.ts.map +1 -1
- package/dist/server/methods/session_create.js +51 -49
- package/dist/server/methods/session_create.js.map +1 -1
- package/dist/server/methods/session_list.d.ts +3 -1
- package/dist/server/methods/session_list.d.ts.map +1 -1
- package/dist/server/methods/session_list.js +9 -7
- package/dist/server/methods/session_list.js.map +1 -1
- package/dist/server/methods/session_runs.d.ts +3 -1
- package/dist/server/methods/session_runs.d.ts.map +1 -1
- package/dist/server/methods/session_runs.js +19 -17
- package/dist/server/methods/session_runs.js.map +1 -1
- package/dist/server/methods/session_set_persona.d.ts +3 -1
- package/dist/server/methods/session_set_persona.d.ts.map +1 -1
- package/dist/server/methods/session_set_persona.js +28 -26
- package/dist/server/methods/session_set_persona.js.map +1 -1
- package/dist/server/methods/session_set_root.d.ts +3 -1
- package/dist/server/methods/session_set_root.d.ts.map +1 -1
- package/dist/server/methods/session_set_root.js +31 -29
- package/dist/server/methods/session_set_root.js.map +1 -1
- package/dist/server/yolo.d.ts +3 -1
- package/dist/server/yolo.d.ts.map +1 -1
- package/dist/server/yolo.js +15 -13
- package/dist/server/yolo.js.map +1 -1
- package/package.json +74 -30
- package/bin/plurnk-service.js +0 -112
- /package/migrations/{001_schema.sql → 0000-00-00.01_schema.sql} +0 -0
package/dist/core/Engine.js
CHANGED
|
@@ -1,18 +1,27 @@
|
|
|
1
|
+
var _a;
|
|
1
2
|
import { PlurnkParser, PlurnkParseError } from "@plurnk/plurnk-grammar";
|
|
2
3
|
import { Mimetypes, emptyRegistry } from "@plurnk/plurnk-mimetypes";
|
|
3
|
-
import
|
|
4
|
+
import EntryCrud from "../schemes/_entry-crud.js";
|
|
5
|
+
import EntryManifest from "../schemes/_entry-manifest.js";
|
|
6
|
+
import GitMembership from "./git-membership.js";
|
|
4
7
|
import { DEFAULT_LOOP_FLAGS } from "./scheme-types.js";
|
|
5
|
-
import {
|
|
8
|
+
import { LineMarkerOps, MimetypeBinary } from "../content/index.js";
|
|
6
9
|
// Plain JS module shared with bin/digest.js so wire projection and
|
|
7
10
|
// digest projection are structurally one function. tsconfig.build.json
|
|
8
11
|
// has allowJs:true so this gets copied through to dist/.
|
|
9
|
-
import
|
|
12
|
+
import PacketWire from "./packet-wire.js";
|
|
10
13
|
// SPEC §3.6: writer must be in target scheme's manifest.writableBy.
|
|
11
14
|
// SHOW/HIDE/READ/FIND are not gated — they touch visibility metadata or read.
|
|
12
15
|
const MUTATING_OPS = new Set(["EDIT", "SEND", "COPY", "MOVE", "EXEC"]);
|
|
13
16
|
const DEFAULT_PREVIEW_BUDGET = 256;
|
|
14
17
|
const DEFAULT_MAX_STRIKES = 3;
|
|
15
18
|
const DEFAULT_MAX_COMMANDS = 99;
|
|
19
|
+
const DEFAULT_BUDGET_CEILING = 0.9;
|
|
20
|
+
// Substituted into the budget readout after the assembled packet is measured
|
|
21
|
+
// (the figure depends on the packet's own rendered size — chicken/egg).
|
|
22
|
+
const TOKENS_FREE_PLACEHOLDER = "{{tokensFree}}";
|
|
23
|
+
const TOKEN_USAGE_PLACEHOLDER = "{{tokenUsage}}";
|
|
24
|
+
const TOKEN_PERCENT_PLACEHOLDER = "{{tokenPercent}}";
|
|
16
25
|
const readBudget = () => {
|
|
17
26
|
const raw = process.env.PLURNK_ENTRY_SIZE_DEFAULT_TOKENS;
|
|
18
27
|
if (raw === undefined || raw.length === 0)
|
|
@@ -22,6 +31,18 @@ const readBudget = () => {
|
|
|
22
31
|
return DEFAULT_PREVIEW_BUDGET;
|
|
23
32
|
return n;
|
|
24
33
|
};
|
|
34
|
+
// PLURNK_BUDGET_CEILING is dual-mode: <=1 is a fraction of the provider's
|
|
35
|
+
// context window, >1 is an absolute token wall — lets a demo pin a tiny
|
|
36
|
+
// ceiling regardless of the model's real window to force the grinder.
|
|
37
|
+
const readCeiling = () => {
|
|
38
|
+
const raw = process.env.PLURNK_BUDGET_CEILING;
|
|
39
|
+
if (raw === undefined || raw.length === 0)
|
|
40
|
+
return DEFAULT_BUDGET_CEILING;
|
|
41
|
+
const n = Number.parseFloat(raw);
|
|
42
|
+
if (!Number.isFinite(n) || n <= 0)
|
|
43
|
+
return DEFAULT_BUDGET_CEILING;
|
|
44
|
+
return n;
|
|
45
|
+
};
|
|
25
46
|
const readMaxStrikes = () => {
|
|
26
47
|
const raw = process.env.PLURNK_MAX_STRIKES;
|
|
27
48
|
if (raw === undefined || raw.length === 0)
|
|
@@ -131,41 +152,56 @@ const fingerprintOp = (stmt) => {
|
|
|
131
152
|
}
|
|
132
153
|
return base;
|
|
133
154
|
};
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
155
|
+
class Engine {
|
|
156
|
+
static computeCeiling(contextSize, config) {
|
|
157
|
+
// Absolute wall (config > 1) is window-independent — the point of the >1
|
|
158
|
+
// mode is to pin a ceiling even when the provider reports no window; cap at
|
|
159
|
+
// the real window when one is known. Ratio mode needs a window to scale.
|
|
160
|
+
if (config > 1)
|
|
161
|
+
return contextSize === null ? Math.floor(config) : Math.min(Math.floor(config), contextSize);
|
|
162
|
+
return contextSize === null ? null : Math.floor(contextSize * config);
|
|
163
|
+
}
|
|
164
|
+
// Per-turn fingerprint: sorted set of per-op fingerprints, joined. Order
|
|
165
|
+
// within a turn doesn't matter — we want the SET of activities.
|
|
166
|
+
static fingerprintTurn(ops) {
|
|
167
|
+
return ops.map(fingerprintOp).toSorted().join(",");
|
|
168
|
+
}
|
|
169
|
+
// Rail #39 cycle detector. For each candidate period k in [1, maxCyclePeriod],
|
|
170
|
+
// check whether the last k*minCycles entries form minCycles repetitions of the
|
|
171
|
+
// same length-k pattern. O(maxCyclePeriod × minCycles × max k) ≈ tiny. Rummy
|
|
172
|
+
// parallel: src/plugins/error/error.js detectCycle.
|
|
173
|
+
static detectCycle(history, minCycles, maxCyclePeriod) {
|
|
174
|
+
for (let k = 1; k <= maxCyclePeriod; k++) {
|
|
175
|
+
const needed = k * minCycles;
|
|
176
|
+
if (history.length < needed)
|
|
177
|
+
continue;
|
|
178
|
+
const tail = history.slice(-needed);
|
|
179
|
+
const cycle = tail.slice(0, k);
|
|
180
|
+
let match = true;
|
|
181
|
+
outer: for (let rep = 0; rep < minCycles; rep++) {
|
|
182
|
+
for (let j = 0; j < k; j++) {
|
|
183
|
+
if (tail[rep * k + j] !== cycle[j]) {
|
|
184
|
+
match = false;
|
|
185
|
+
break outer;
|
|
186
|
+
}
|
|
156
187
|
}
|
|
157
188
|
}
|
|
189
|
+
if (match)
|
|
190
|
+
return { detected: true, period: k, cycles: minCycles };
|
|
158
191
|
}
|
|
159
|
-
|
|
160
|
-
return { detected: true, period: k, cycles: minCycles };
|
|
192
|
+
return { detected: false };
|
|
161
193
|
}
|
|
162
|
-
return { detected: false };
|
|
163
|
-
};
|
|
164
|
-
export default class Engine {
|
|
165
194
|
#db;
|
|
166
195
|
#schemes;
|
|
167
196
|
#mimetypes;
|
|
168
197
|
#previewBudget;
|
|
198
|
+
#budgetCeiling;
|
|
199
|
+
// Write-time tokenizer (SPEC §14.2). Synchronous per the provider
|
|
200
|
+
// contract (§2.1). Populated from the active provider's countTokens via
|
|
201
|
+
// the Daemon; a divisor tripwire stands in only for bare/standalone
|
|
202
|
+
// construction before a provider is wired (same boot affordance as
|
|
203
|
+
// Mimetypes, §4.5). Real counts come from provider.countTokens.
|
|
204
|
+
#tokenize;
|
|
169
205
|
// Per-loop transient buffer of actionless failures pending surface in the
|
|
170
206
|
// NEXT packet's user.telemetry.errors[]. Drained by #buildTelemetryErrors.
|
|
171
207
|
// Map<loopId, TelemetryError[]>. SPEC §15.1.
|
|
@@ -200,7 +236,7 @@ export default class Engine {
|
|
|
200
236
|
// status code but has no way to surface why the loop degraded.
|
|
201
237
|
// Per-grammar 0.17.0 protocol — see SPEC §15.1.
|
|
202
238
|
#telemetryEventNotify;
|
|
203
|
-
constructor({ db, schemes, mimetypes, streamEventNotify, wakeRunNotify, telemetryEventNotify }) {
|
|
239
|
+
constructor({ db, schemes, mimetypes, streamEventNotify, wakeRunNotify, telemetryEventNotify, tokenize }) {
|
|
204
240
|
this.#db = db;
|
|
205
241
|
this.#schemes = schemes;
|
|
206
242
|
this.#streamEventNotify = streamEventNotify;
|
|
@@ -214,6 +250,11 @@ export default class Engine {
|
|
|
214
250
|
discovery: { registry: emptyRegistry(), handlers: new Map() },
|
|
215
251
|
});
|
|
216
252
|
this.#previewBudget = readBudget();
|
|
253
|
+
this.#budgetCeiling = readCeiling();
|
|
254
|
+
// Tripwire default matches the Mimetypes boot affordance (SPEC §4.5):
|
|
255
|
+
// the divisor stands in only until the provider-backed tokenizer is
|
|
256
|
+
// wired by the Daemon. Real counts come from provider.countTokens.
|
|
257
|
+
this.#tokenize = tokenize ?? ((text) => Math.ceil(text.length / 4));
|
|
217
258
|
}
|
|
218
259
|
#pushTelemetry(sessionId, loopId, event) {
|
|
219
260
|
const existing = this.#telemetryBuffer.get(loopId);
|
|
@@ -305,6 +346,12 @@ export default class Engine {
|
|
|
305
346
|
turnNumber: turnIds.length + 1, maxTurns,
|
|
306
347
|
});
|
|
307
348
|
turnIds.push(turn.turnId);
|
|
349
|
+
// SPEC §14.4: budget hard-stop — packet won't fit even collapsed → abandon.
|
|
350
|
+
if (turn.budgetHardStop) {
|
|
351
|
+
await this.#db.engine_loop_cancel.run({ loop_id: loopId });
|
|
352
|
+
cleanup("forceful", "budget_overflow");
|
|
353
|
+
return { turnIds, finalStatus: 499, hitMaxTurns: false, reason: "budget_overflow" };
|
|
354
|
+
}
|
|
308
355
|
// Rail #39: cycle detection. Push this turn's fingerprint to
|
|
309
356
|
// history, scan for repetition patterns. Detection bumps
|
|
310
357
|
// turnErrors so the strike system handles abandonment
|
|
@@ -315,9 +362,12 @@ export default class Engine {
|
|
|
315
362
|
// reason for treating the turn as a failure, not its own alert.
|
|
316
363
|
const state = this.#strikeState.get(loopId) ?? { streak: 0, turnErrors: 0, history: [] };
|
|
317
364
|
state.history.push(turn.fingerprint);
|
|
318
|
-
const cycle = detectCycle(state.history, minCycles, maxCyclePeriod);
|
|
365
|
+
const cycle = _a.detectCycle(state.history, minCycles, maxCyclePeriod);
|
|
319
366
|
if (cycle.detected)
|
|
320
367
|
state.turnErrors++;
|
|
368
|
+
// SPEC §14.4: a non-soft grinder fire counts toward the strike streak.
|
|
369
|
+
if (turn.budgetStruck)
|
|
370
|
+
state.turnErrors++;
|
|
321
371
|
this.#strikeState.set(loopId, state);
|
|
322
372
|
// Rail #38: strike accounting. Three sources strike a turn:
|
|
323
373
|
// 1. recordedFailed — any action-entry at hard failure status
|
|
@@ -409,13 +459,61 @@ export default class Engine {
|
|
|
409
459
|
nextActionIndex++;
|
|
410
460
|
}
|
|
411
461
|
}
|
|
462
|
+
// plurnk://manifest.json — rewritten EVERY turn (a live view of the
|
|
463
|
+
// entry set, which changes each turn). A derived view like the index,
|
|
464
|
+
// NOT an action — written directly (Engine.inject's path): no log entry,
|
|
465
|
+
// no sequence slot, not dispatched. The catalog body is built in the
|
|
466
|
+
// schemes layer (_entry-manifest); the engine only orchestrates the
|
|
467
|
+
// per-turn write. Does not list itself.
|
|
468
|
+
const systemCtx = {
|
|
469
|
+
db: this.#db, sessionId, runId, loopId, turnId,
|
|
470
|
+
writer: "system",
|
|
471
|
+
signal: this.#loopAborts.get(loopId)?.signal,
|
|
472
|
+
streamEventNotify: this.#streamEventNotify,
|
|
473
|
+
wakeRunNotify: this.#wakeRunNotify,
|
|
474
|
+
tokenize: this.#tokenize,
|
|
475
|
+
mimetypes: this.#mimetypes,
|
|
476
|
+
pushTelemetry: (event) => this.#pushTelemetry(sessionId, loopId, event),
|
|
477
|
+
};
|
|
478
|
+
// SPEC §14.3 D4/D5 — git-ls-files workspace membership, resolved at
|
|
479
|
+
// prompt-composition (EMI is eager + relevance-bounded). When the
|
|
480
|
+
// session's project_root is a git working tree, tracked files are
|
|
481
|
+
// members without a client `add`; active members are materialized
|
|
482
|
+
// (disk → body channel + visibility) so they surface in the index
|
|
483
|
+
// below. No-ops on headless / non-git sessions. Runs BEFORE the
|
|
484
|
+
// manifest + index build so this turn's packet reflects them.
|
|
485
|
+
await GitMembership.indexGitMembership(systemCtx);
|
|
486
|
+
await EntryCrud.writeEntry("manifest.json", {
|
|
487
|
+
channels: { body: { content: await EntryManifest.buildManifestBody(systemCtx, this.#previewBudget), mimetype: "application/json" } },
|
|
488
|
+
tags: [],
|
|
489
|
+
}, systemCtx, "plurnk");
|
|
412
490
|
// Build the spec'd packet (Packet.json) request half. #buildLog
|
|
413
491
|
// queries log_entries scoped to the run — the prompt entry just
|
|
414
492
|
// written (if turn 1) is part of that query result.
|
|
415
|
-
|
|
493
|
+
let requestPacket = await this.#buildRequestPacket({
|
|
416
494
|
initialMessages: messages, persona, requirements, runId, loopId,
|
|
417
495
|
currentTurnSeq: seq, provider,
|
|
418
496
|
});
|
|
497
|
+
// SPEC §14.4 — budget grinder, pre-LLM: reclaim window on actual overflow.
|
|
498
|
+
const enforced = await this.#enforceBudget({
|
|
499
|
+
packet: requestPacket, provider, runId, loopId, turnId, sessionId, turnNumber,
|
|
500
|
+
rebuild: (telemetryErrors) => this.#buildRequestPacket({
|
|
501
|
+
initialMessages: messages, persona, requirements, runId, loopId,
|
|
502
|
+
currentTurnSeq: seq, provider, telemetryErrors,
|
|
503
|
+
}),
|
|
504
|
+
});
|
|
505
|
+
requestPacket = enforced.packet;
|
|
506
|
+
if (!enforced.fit) {
|
|
507
|
+
// Hard 413: won't fit even with only the manifest left. Skip the LLM,
|
|
508
|
+
// close the turn, and let runLoop abandon (499).
|
|
509
|
+
const hardPacket = this.#completePacket(requestPacket, { content: "", ops: [], reasoning: null }, null, provider);
|
|
510
|
+
await this.#db.engine_close_turn.run({
|
|
511
|
+
id: turnId, status: 413, packet: JSON.stringify(hardPacket),
|
|
512
|
+
usage_prompt: 0, usage_completion: 0, usage_cached: 0, usage_cost_pico: 0,
|
|
513
|
+
finish_reason: "budget_hard_stop", model: provider.model,
|
|
514
|
+
});
|
|
515
|
+
return { turnId, status: 413, statuses: [], fingerprint: "", budgetStruck: enforced.struck, budgetHardStop: true };
|
|
516
|
+
}
|
|
419
517
|
const modelMessages = this.#packetToWireMessages(requestPacket);
|
|
420
518
|
const response = await provider.generate({ messages: modelMessages, signal });
|
|
421
519
|
// Engine splits wire-level response: emission (content, reasoning,
|
|
@@ -506,7 +604,7 @@ export default class Engine {
|
|
|
506
604
|
// nothing. Strike accounting (engine-internal) treats it as a
|
|
507
605
|
// struck turn; the model just sees an empty packet next turn.
|
|
508
606
|
// Per SPEC §15.1 gamification policy.
|
|
509
|
-
return { turnId, status: turnStatus, statuses, fingerprint: fingerprintTurn(packetAssistant.ops) };
|
|
607
|
+
return { turnId, status: turnStatus, statuses, fingerprint: _a.fingerprintTurn(packetAssistant.ops), budgetStruck: enforced.struck, budgetHardStop: false };
|
|
510
608
|
}
|
|
511
609
|
// Split the wire-level ProviderResponse into the two destinations:
|
|
512
610
|
// packet.assistant gets the model's emission (content, ops, reasoning);
|
|
@@ -568,7 +666,7 @@ export default class Engine {
|
|
|
568
666
|
// and §user) BEFORE the provider call. The same packet object is then
|
|
569
667
|
// completed with assistant + assistantRaw after the model responds, so
|
|
570
668
|
// the stored packet and the wire payload share one source of truth.
|
|
571
|
-
async #buildRequestPacket({ initialMessages, persona: defaultPersona, requirements, runId, loopId, currentTurnSeq, provider, }) {
|
|
669
|
+
async #buildRequestPacket({ initialMessages, persona: defaultPersona, requirements, runId, loopId, currentTurnSeq, provider, telemetryErrors: presetTelemetry, }) {
|
|
572
670
|
const byRole = (role) => initialMessages.filter((m) => m.role === role).map((m) => m.content).join("\n\n");
|
|
573
671
|
const system_definition = byRole("system");
|
|
574
672
|
// user.prompt sources from the loop's most recent prompt entry first
|
|
@@ -587,65 +685,117 @@ export default class Engine {
|
|
|
587
685
|
const persona = (row?.persona !== undefined && row?.persona !== null) ? row.persona : defaultPersona;
|
|
588
686
|
const index = await this.#buildIndex(runId, loopId);
|
|
589
687
|
const log = await this.#buildLog(runId);
|
|
590
|
-
const telemetryErrors = await this.#buildTelemetryErrors(loopId, currentTurnSeq);
|
|
688
|
+
const telemetryErrors = presetTelemetry ?? await this.#buildTelemetryErrors(loopId, currentTurnSeq);
|
|
591
689
|
// Per-section render-cost subtotals via provider's tokenizer.
|
|
592
690
|
// Engine approximates each section by tokenizing its serialized
|
|
593
691
|
// form — wire-payload tokens may differ slightly because chat-
|
|
594
692
|
// template scaffolding adds bytes, but the subtotal tracks "what
|
|
595
693
|
// the model has to process" closely enough for budget diagnostics.
|
|
596
|
-
const
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
//
|
|
601
|
-
//
|
|
602
|
-
//
|
|
603
|
-
|
|
604
|
-
const
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
return {
|
|
609
|
-
system: {
|
|
610
|
-
tokens: systemTokens,
|
|
611
|
-
system_definition,
|
|
612
|
-
persona,
|
|
613
|
-
index,
|
|
614
|
-
log,
|
|
615
|
-
},
|
|
616
|
-
user: {
|
|
617
|
-
tokens: userTokens,
|
|
618
|
-
prompt,
|
|
619
|
-
telemetry: { budget, errors: telemetryErrors },
|
|
620
|
-
system_requirements: requirements,
|
|
621
|
-
},
|
|
694
|
+
const countTokens = (t) => provider.countTokens(t);
|
|
695
|
+
// Budget readout (SPEC.md §14.2). Two-pass: measure the wire-rendered
|
|
696
|
+
// index/log sections (budget-independent), install the readout with a
|
|
697
|
+
// tokensFree placeholder, measure the assembled total, resolve free,
|
|
698
|
+
// substitute. Subtotals come from the real render — meta and fences
|
|
699
|
+
// included — not a serialized approximation. ceiling is the provider's
|
|
700
|
+
// window × PLURNK_BUDGET_CEILING (null when no window is reported →
|
|
701
|
+
// headline omitted, section lines still shown).
|
|
702
|
+
const ceiling = _a.computeCeiling(provider.contextSize, this.#budgetCeiling);
|
|
703
|
+
const scratch = {
|
|
704
|
+
system: { system_definition, persona, index, log },
|
|
705
|
+
user: { prompt, telemetry: { budget: "", errors: telemetryErrors }, system_requirements: requirements },
|
|
622
706
|
};
|
|
707
|
+
const sections = PacketWire.measureBudgetSections(scratch, countTokens);
|
|
708
|
+
scratch.user.telemetry.budget = this.#renderBudget(sections, ceiling);
|
|
709
|
+
const total = countTokens(PacketWire.renderSystemContent(scratch.system)) + countTokens(PacketWire.renderUserContent(scratch.user));
|
|
710
|
+
const tokensFree = ceiling === null ? null : Math.max(0, ceiling - total);
|
|
711
|
+
const percent = ceiling === null ? null : Math.round((total / ceiling) * 100);
|
|
712
|
+
const budget = tokensFree === null
|
|
713
|
+
? scratch.user.telemetry.budget
|
|
714
|
+
: scratch.user.telemetry.budget
|
|
715
|
+
.replace(TOKEN_USAGE_PLACEHOLDER, String(total))
|
|
716
|
+
.replace(TOKEN_PERCENT_PLACEHOLDER, String(percent))
|
|
717
|
+
.replace(TOKENS_FREE_PLACEHOLDER, String(tokensFree));
|
|
718
|
+
const system = { tokens: 0, system_definition, persona, index, log };
|
|
719
|
+
const user = { tokens: 0, prompt, telemetry: { budget, errors: telemetryErrors }, system_requirements: requirements };
|
|
720
|
+
system.tokens = countTokens(PacketWire.renderSystemContent(system));
|
|
721
|
+
user.tokens = countTokens(PacketWire.renderUserContent(user));
|
|
722
|
+
return { system, user };
|
|
723
|
+
}
|
|
724
|
+
// Budget readout body, rendered into the `# Plurnk System Budget` section.
|
|
725
|
+
// Headline `ceiling/free` only when a ceiling exists; section lines for the
|
|
726
|
+
// curatable index/log weight the model can HIDE back. tokensFree is a
|
|
727
|
+
// placeholder here — buildSystem substitutes it after measuring the packet.
|
|
728
|
+
#renderBudget(sections, ceiling) {
|
|
729
|
+
const lines = [];
|
|
730
|
+
if (ceiling !== null)
|
|
731
|
+
lines.push(`ceiling ${ceiling} · usage ${TOKEN_USAGE_PLACEHOLDER} (${TOKEN_PERCENT_PLACEHOLDER}%) · free ${TOKENS_FREE_PLACEHOLDER}`);
|
|
732
|
+
if (sections.index.channels > 0)
|
|
733
|
+
lines.push(`Index previews: ${sections.index.channels} channels, ${sections.index.tokens} tokens`);
|
|
734
|
+
if (sections.log.entries > 0) {
|
|
735
|
+
lines.push(`Log entries: ${sections.log.entries} entries, ${sections.log.tokens} tokens`);
|
|
736
|
+
if (sections.log.byScheme.length > 0) {
|
|
737
|
+
lines.push("| scheme | entries | tokens |", "|---|--:|--:|");
|
|
738
|
+
for (const s of sections.log.byScheme)
|
|
739
|
+
lines.push(`| ${s.scheme} | ${s.entries} | ${s.tokens} |`);
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
return lines.join("\n");
|
|
623
743
|
}
|
|
624
|
-
//
|
|
625
|
-
//
|
|
626
|
-
//
|
|
627
|
-
//
|
|
628
|
-
//
|
|
629
|
-
#
|
|
630
|
-
const
|
|
631
|
-
const
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
const
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
744
|
+
// SPEC §14.4 — the budget grinder. Runs pre-LLM (in runTurn, after the packet
|
|
745
|
+
// is built, before provider.generate); fires only on actual overflow. Two
|
|
746
|
+
// passes, re-measuring between. Hides (never deletes) — the prior turn's logs,
|
|
747
|
+
// then the catalog except the manifest lifeline. The strike it raises and the
|
|
748
|
+
// hard-stop it can signal are returned to runLoop, which owns abandonment.
|
|
749
|
+
async #enforceBudget({ packet, provider, runId, loopId, turnId, sessionId, turnNumber, rebuild }) {
|
|
750
|
+
const ceiling = _a.computeCeiling(provider.contextSize, this.#budgetCeiling);
|
|
751
|
+
const measure = (p) => p.system.tokens + p.user.tokens;
|
|
752
|
+
if (ceiling === null || measure(packet) <= ceiling)
|
|
753
|
+
return { packet, fit: true, struck: false };
|
|
754
|
+
const hidden = new Map();
|
|
755
|
+
const note = (scheme) => { hidden.set(scheme, (hidden.get(scheme) ?? 0) + 1); };
|
|
756
|
+
// Pass 1 — prior-turn rollback: hide the latest emissions (the ones that
|
|
757
|
+
// pushed it over). No prior turn (turn 1, env overflow) → no-op → pass 2.
|
|
758
|
+
const priorLogs = await this.#db.engine_grinder_prior_turn_logs.all({ loop_id: loopId, turn_id: turnId });
|
|
759
|
+
for (const le of priorLogs)
|
|
760
|
+
note(le.scheme ?? "log");
|
|
761
|
+
if (priorLogs.length > 0)
|
|
762
|
+
await this.#db.engine_grinder_hide_prior_turn_logs.run({ loop_id: loopId, turn_id: turnId });
|
|
763
|
+
const errors = packet.user.telemetry.errors;
|
|
764
|
+
let current = priorLogs.length > 0 ? await rebuild(errors) : packet;
|
|
765
|
+
if (measure(current) <= ceiling) {
|
|
766
|
+
this.#emitBudgetOverflow(sessionId, loopId, hidden);
|
|
767
|
+
return { packet: current, fit: true, struck: turnNumber > 1 };
|
|
768
|
+
}
|
|
769
|
+
// Pass 2 — index collapse: hide every catalog entry except the manifest.
|
|
770
|
+
const catalog = await this.#db.engine_grinder_catalog.all({ run_id: runId, session_id: sessionId });
|
|
771
|
+
for (const c of catalog)
|
|
772
|
+
note(c.scheme);
|
|
773
|
+
if (catalog.length > 0) {
|
|
774
|
+
const pairs = JSON.stringify(catalog.map((c) => ({ entry_id: c.entry_id, channel: c.channel })));
|
|
775
|
+
await this.#db.engine_grinder_hide_catalog.run({ run_id: runId, pairs });
|
|
776
|
+
}
|
|
777
|
+
current = catalog.length > 0 ? await rebuild(errors) : current;
|
|
778
|
+
this.#emitBudgetOverflow(sessionId, loopId, hidden);
|
|
779
|
+
return { packet: current, fit: measure(current) <= ceiling, struck: turnNumber > 1 };
|
|
780
|
+
}
|
|
781
|
+
// The model-facing budget event (SPEC §14.4, §15.1): which entries left the
|
|
782
|
+
// window, by scheme — the model's own terms, no mechanism vocabulary. The
|
|
783
|
+
// strike this overflow triggers stays engine-internal (gamification policy).
|
|
784
|
+
#emitBudgetOverflow(sessionId, loopId, hidden) {
|
|
785
|
+
if (hidden.size === 0)
|
|
786
|
+
return;
|
|
787
|
+
this.#pushTelemetry(sessionId, loopId, {
|
|
788
|
+
source: "engine:rail",
|
|
789
|
+
kind: "budget_overflow",
|
|
790
|
+
hidden: [...hidden.entries()].map(([scheme, count]) => ({ scheme, count })),
|
|
791
|
+
});
|
|
642
792
|
}
|
|
643
793
|
// Wire projection lives in ./packet-wire.js (plain JS) so Engine and
|
|
644
794
|
// bin/digest.js import the exact same function — structurally one
|
|
645
795
|
// implementation, no drift between wire and digest possible.
|
|
646
796
|
// Format: markdown (user pick over rummy's XML alternative, 2026-05-22).
|
|
647
797
|
#packetToWireMessages(packet) {
|
|
648
|
-
return packetToWireMessages(packet);
|
|
798
|
+
return PacketWire.packetToWireMessages(packet);
|
|
649
799
|
}
|
|
650
800
|
// Complete the packet by adding the model's response. After this the
|
|
651
801
|
// packet matches Packet.json fully and is ready for storage.
|
|
@@ -779,6 +929,7 @@ export default class Engine {
|
|
|
779
929
|
content: result.preview,
|
|
780
930
|
mimetype: row.mimetype,
|
|
781
931
|
tokens: row.tokens,
|
|
932
|
+
lines: result.totalLines,
|
|
782
933
|
};
|
|
783
934
|
}
|
|
784
935
|
return [...entries.values()];
|
|
@@ -793,6 +944,7 @@ export default class Engine {
|
|
|
793
944
|
streamEventNotify: this.#streamEventNotify,
|
|
794
945
|
wakeRunNotify: this.#wakeRunNotify,
|
|
795
946
|
mimetypes: this.#mimetypes,
|
|
947
|
+
tokenize: this.#tokenize,
|
|
796
948
|
pushTelemetry: (event) => this.#pushTelemetry(sessionId, loopId, event),
|
|
797
949
|
};
|
|
798
950
|
let result;
|
|
@@ -906,6 +1058,7 @@ export default class Engine {
|
|
|
906
1058
|
writer: "model", signal: this.#loopAborts.get(loopId)?.signal,
|
|
907
1059
|
streamEventNotify: this.#streamEventNotify,
|
|
908
1060
|
wakeRunNotify: this.#wakeRunNotify,
|
|
1061
|
+
tokenize: this.#tokenize,
|
|
909
1062
|
pushTelemetry: (event) => this.#pushTelemetry(sessionId, loopId, event),
|
|
910
1063
|
};
|
|
911
1064
|
const applyResult = await handler.applyResolution({
|
|
@@ -995,13 +1148,14 @@ export default class Engine {
|
|
|
995
1148
|
signal: this.#loopAborts.get(loopId)?.signal,
|
|
996
1149
|
streamEventNotify: this.#streamEventNotify,
|
|
997
1150
|
wakeRunNotify: this.#wakeRunNotify,
|
|
1151
|
+
tokenize: this.#tokenize,
|
|
998
1152
|
pushTelemetry: (event) => this.#pushTelemetry(sessionRow.session_id, loopId, event),
|
|
999
1153
|
};
|
|
1000
1154
|
const entry = {
|
|
1001
1155
|
channels: { body: { content: prompt, mimetype: "text/markdown" } },
|
|
1002
1156
|
tags: [],
|
|
1003
1157
|
};
|
|
1004
|
-
await writeEntry(pathname, entry, ctx, "plurnk");
|
|
1158
|
+
await EntryCrud.writeEntry(pathname, entry, ctx, "plurnk");
|
|
1005
1159
|
return { loopId, turnSeq };
|
|
1006
1160
|
}
|
|
1007
1161
|
// Subscribe to proposal-pending events. Daemon registers a listener
|
|
@@ -1173,8 +1327,9 @@ export default class Engine {
|
|
|
1173
1327
|
const srcHandler = this.#schemes.get(srcSchemeName);
|
|
1174
1328
|
if (srcHandler === undefined || typeof srcHandler.deleteEntry !== "function")
|
|
1175
1329
|
return { status: 501 };
|
|
1176
|
-
//
|
|
1177
|
-
|
|
1330
|
+
// MOVE to /dev/null (the grammar's idiomatic delete) or a null-body
|
|
1331
|
+
// MOVE deletes the source entry. SPEC §6.5.
|
|
1332
|
+
if (dstPath === null || pathnameFromPath(dstPath) === "/dev/null") {
|
|
1178
1333
|
const srcPathname = pathnameFromPath(srcPath);
|
|
1179
1334
|
const delResult = await srcHandler.deleteEntry(srcPathname, ctx);
|
|
1180
1335
|
return { status: delResult.status };
|
|
@@ -1206,12 +1361,12 @@ export default class Engine {
|
|
|
1206
1361
|
if (srcResult.status !== 200 || srcResult.entry === null)
|
|
1207
1362
|
return { status: 404, error: `COPY/MOVE source not found: ${srcSchemeName}://${srcPathname}` };
|
|
1208
1363
|
const entry = srcResult.entry;
|
|
1209
|
-
//
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1364
|
+
// Destination read — the conflict/no-op verdict is deferred until the
|
|
1365
|
+
// to-be-written content is known (after <L> slice + tag resolution below),
|
|
1366
|
+
// so an identical re-copy resolves to 304 instead of a phantom 409.
|
|
1367
|
+
const dstExisting = typeof dstHandler.readEntry === "function"
|
|
1368
|
+
? await dstHandler.readEntry(dstPathname, ctx)
|
|
1369
|
+
: null;
|
|
1215
1370
|
// Mimetype compatibility check against the destination scheme's manifest
|
|
1216
1371
|
const dstManifest = dstHandler.constructor.manifest;
|
|
1217
1372
|
const dstChannels = dstManifest?.channels ?? {};
|
|
@@ -1230,10 +1385,10 @@ export default class Engine {
|
|
|
1230
1385
|
if (lineMarker !== null) {
|
|
1231
1386
|
const sliced = {};
|
|
1232
1387
|
for (const [channelName, channelData] of Object.entries(entry.channels)) {
|
|
1233
|
-
if (isBinaryMimetype(channelData.mimetype)) {
|
|
1388
|
+
if (MimetypeBinary.isBinaryMimetype(channelData.mimetype)) {
|
|
1234
1389
|
return { status: 415, error: `cannot slice <L> on binary channel '${channelName}' (${channelData.mimetype})` };
|
|
1235
1390
|
}
|
|
1236
|
-
const r = sliceLinesRaw(channelData.content ?? "", lineMarker);
|
|
1391
|
+
const r = LineMarkerOps.sliceLinesRaw(channelData.content ?? "", lineMarker);
|
|
1237
1392
|
if (r.status !== 200)
|
|
1238
1393
|
return { status: r.status, error: r.error };
|
|
1239
1394
|
sliced[channelName] = { ...channelData, content: r.text ?? "" };
|
|
@@ -1244,6 +1399,21 @@ export default class Engine {
|
|
|
1244
1399
|
const tags = (Array.isArray(statement.signal) && statement.signal.length > 0)
|
|
1245
1400
|
? statement.signal
|
|
1246
1401
|
: entry.tags;
|
|
1402
|
+
// 304/409 on an existing destination (SPEC §6.4): a re-copy that would write
|
|
1403
|
+
// exactly what's already there — same channel contents, same tags — is a no-op
|
|
1404
|
+
// (304), mirroring EDIT's 304-on-noop (§6.1). A divergent destination is a real
|
|
1405
|
+
// collision (409); COPY/MOVE never clobbers.
|
|
1406
|
+
if (dstExisting !== null && dstExisting.status === 200 && dstExisting.entry !== null) {
|
|
1407
|
+
const dstChannels = dstExisting.entry.channels;
|
|
1408
|
+
const writeNames = Object.keys(channels).sort();
|
|
1409
|
+
const dstNames = Object.keys(dstChannels).sort();
|
|
1410
|
+
const sameContent = writeNames.length === dstNames.length
|
|
1411
|
+
&& writeNames.every((n, i) => n === dstNames[i] && (channels[n]?.content ?? "") === (dstChannels[n]?.content ?? ""));
|
|
1412
|
+
const sameTags = [...tags].sort().join("") === [...dstExisting.entry.tags].sort().join("");
|
|
1413
|
+
if (sameContent && sameTags)
|
|
1414
|
+
return { status: 304 };
|
|
1415
|
+
return { status: 409, error: `COPY/MOVE destination exists: ${dstSchemeName}://${dstPathname}` };
|
|
1416
|
+
}
|
|
1247
1417
|
const writeResult = await dstHandler.writeEntry(dstPathname, { channels, tags }, ctx);
|
|
1248
1418
|
return { status: writeResult.status, entryId: writeResult.entryId, created: writeResult.created };
|
|
1249
1419
|
}
|
|
@@ -1322,6 +1492,8 @@ export default class Engine {
|
|
|
1322
1492
|
}
|
|
1323
1493
|
}
|
|
1324
1494
|
const attrs = JSON.stringify(attrsObj);
|
|
1495
|
+
const txJson = JSON.stringify(statement);
|
|
1496
|
+
const rxJson = JSON.stringify(result);
|
|
1325
1497
|
const row = await this.#db.engine_insert_log_entry.get({
|
|
1326
1498
|
run_id: runId,
|
|
1327
1499
|
loop_id: loopId,
|
|
@@ -1340,11 +1512,12 @@ export default class Engine {
|
|
|
1340
1512
|
params: target.params,
|
|
1341
1513
|
fragment: target.fragment,
|
|
1342
1514
|
lineMarker: lineMarkerJson,
|
|
1343
|
-
tx:
|
|
1515
|
+
tx: txJson,
|
|
1344
1516
|
mimetype_tx: "application/json",
|
|
1345
|
-
rx:
|
|
1517
|
+
rx: rxJson,
|
|
1346
1518
|
mimetype_rx: "application/json",
|
|
1347
1519
|
status_rx: result.status,
|
|
1520
|
+
tokens: this.#tokenize(txJson) + this.#tokenize(rxJson),
|
|
1348
1521
|
state: isProposed ? "proposed" : "resolved",
|
|
1349
1522
|
outcome: null,
|
|
1350
1523
|
attrs,
|
|
@@ -1375,4 +1548,6 @@ export default class Engine {
|
|
|
1375
1548
|
return JSON.stringify(signal);
|
|
1376
1549
|
}
|
|
1377
1550
|
}
|
|
1551
|
+
_a = Engine;
|
|
1552
|
+
export default Engine;
|
|
1378
1553
|
//# sourceMappingURL=Engine.js.map
|