@plurnk/plurnk-service 0.34.0 → 0.39.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.
Files changed (103) hide show
  1. package/SPEC.md +51 -15
  2. package/dist/content/matcher.d.ts +1 -0
  3. package/dist/content/matcher.d.ts.map +1 -1
  4. package/dist/content/matcher.js +1 -1
  5. package/dist/content/matcher.js.map +1 -1
  6. package/dist/core/ChannelWrite.d.ts +1 -0
  7. package/dist/core/ChannelWrite.d.ts.map +1 -1
  8. package/dist/core/ChannelWrite.js.map +1 -1
  9. package/dist/core/Engine.d.ts +8 -2
  10. package/dist/core/Engine.d.ts.map +1 -1
  11. package/dist/core/Engine.js +99 -28
  12. package/dist/core/Engine.js.map +1 -1
  13. package/dist/core/ExecutorRegistry.d.ts +2 -0
  14. package/dist/core/ExecutorRegistry.d.ts.map +1 -1
  15. package/dist/core/ExecutorRegistry.js +1 -0
  16. package/dist/core/ExecutorRegistry.js.map +1 -1
  17. package/dist/core/SchemeRegistry.d.ts +4 -0
  18. package/dist/core/SchemeRegistry.d.ts.map +1 -1
  19. package/dist/core/SchemeRegistry.js +18 -1
  20. package/dist/core/SchemeRegistry.js.map +1 -1
  21. package/dist/core/fork.js +1 -1
  22. package/dist/core/fork.js.map +1 -1
  23. package/dist/core/git-membership.d.ts.map +1 -1
  24. package/dist/core/git-membership.js +44 -22
  25. package/dist/core/git-membership.js.map +1 -1
  26. package/dist/core/git-state.d.ts.map +1 -1
  27. package/dist/core/git-state.js +3 -1
  28. package/dist/core/git-state.js.map +1 -1
  29. package/dist/core/packet-wire.js +1 -1
  30. package/dist/core/packet-wire.js.map +1 -1
  31. package/dist/core/path-decode.d.ts +2 -0
  32. package/dist/core/path-decode.d.ts.map +1 -0
  33. package/dist/core/path-decode.js +8 -0
  34. package/dist/core/path-decode.js.map +1 -0
  35. package/dist/core/run-cap.d.ts +8 -0
  36. package/dist/core/run-cap.d.ts.map +1 -0
  37. package/dist/core/run-cap.js +20 -0
  38. package/dist/core/run-cap.js.map +1 -0
  39. package/dist/core/session-settings.d.ts +19 -0
  40. package/dist/core/session-settings.d.ts.map +1 -0
  41. package/dist/core/session-settings.js +44 -0
  42. package/dist/core/session-settings.js.map +1 -0
  43. package/dist/schemes/File.d.ts +2 -0
  44. package/dist/schemes/File.d.ts.map +1 -1
  45. package/dist/schemes/File.js +72 -11
  46. package/dist/schemes/File.js.map +1 -1
  47. package/dist/schemes/Log.js +7 -7
  48. package/dist/schemes/Log.js.map +1 -1
  49. package/dist/schemes/Run.d.ts.map +1 -1
  50. package/dist/schemes/Run.js +5 -1
  51. package/dist/schemes/Run.js.map +1 -1
  52. package/dist/schemes/_entry-find.d.ts +14 -3
  53. package/dist/schemes/_entry-find.d.ts.map +1 -1
  54. package/dist/schemes/_entry-find.js +59 -27
  55. package/dist/schemes/_entry-find.js.map +1 -1
  56. package/dist/schemes/_entry-graph.d.ts +6 -0
  57. package/dist/schemes/_entry-graph.d.ts.map +1 -1
  58. package/dist/schemes/_entry-graph.js +8 -0
  59. package/dist/schemes/_entry-graph.js.map +1 -1
  60. package/dist/schemes/_entry-manifest.d.ts.map +1 -1
  61. package/dist/schemes/_entry-manifest.js +13 -0
  62. package/dist/schemes/_entry-manifest.js.map +1 -1
  63. package/dist/schemes/_entry-ops.d.ts.map +1 -1
  64. package/dist/schemes/_entry-ops.js +4 -3
  65. package/dist/schemes/_entry-ops.js.map +1 -1
  66. package/dist/schemes/_entry-semantic.d.ts +5 -1
  67. package/dist/schemes/_entry-semantic.d.ts.map +1 -1
  68. package/dist/schemes/_entry-semantic.js +10 -8
  69. package/dist/schemes/_entry-semantic.js.map +1 -1
  70. package/dist/schemes/_entry-send.d.ts.map +1 -1
  71. package/dist/schemes/_entry-send.js +4 -3
  72. package/dist/schemes/_entry-send.js.map +1 -1
  73. package/dist/server/Daemon.d.ts.map +1 -1
  74. package/dist/server/Daemon.js +26 -1
  75. package/dist/server/Daemon.js.map +1 -1
  76. package/dist/server/envelope.d.ts +2 -0
  77. package/dist/server/envelope.d.ts.map +1 -1
  78. package/dist/server/envelope.js +8 -1
  79. package/dist/server/envelope.js.map +1 -1
  80. package/dist/server/methods/discover.d.ts.map +1 -1
  81. package/dist/server/methods/discover.js +6 -3
  82. package/dist/server/methods/discover.js.map +1 -1
  83. package/dist/server/methods/loop_run.d.ts.map +1 -1
  84. package/dist/server/methods/loop_run.js +14 -10
  85. package/dist/server/methods/loop_run.js.map +1 -1
  86. package/dist/server/methods/session_constraints.js +6 -6
  87. package/dist/server/methods/session_constraints.js.map +1 -1
  88. package/dist/server/methods/session_create.d.ts.map +1 -1
  89. package/dist/server/methods/session_create.js +55 -5
  90. package/dist/server/methods/session_create.js.map +1 -1
  91. package/dist/server/methods/session_prompts.d.ts +5 -0
  92. package/dist/server/methods/session_prompts.d.ts.map +1 -0
  93. package/dist/server/methods/session_prompts.js +29 -0
  94. package/dist/server/methods/session_prompts.js.map +1 -0
  95. package/dist/server/version-info.d.ts +14 -0
  96. package/dist/server/version-info.d.ts.map +1 -0
  97. package/dist/server/version-info.js +69 -0
  98. package/dist/server/version-info.js.map +1 -0
  99. package/dist/server/yolo.d.ts.map +1 -1
  100. package/dist/server/yolo.js +8 -0
  101. package/dist/server/yolo.js.map +1 -1
  102. package/migrations/0000-00-00.01_schema.sql +29 -8
  103. package/package.json +22 -22
@@ -6,6 +6,9 @@ import EntryManifest from "../schemes/_entry-manifest.js";
6
6
  import GitMembership from "./git-membership.js";
7
7
  import GitState from "./git-state.js";
8
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";
9
12
  import { DEFAULT_LOOP_FLAGS } from "./scheme-types.js";
10
13
  import { LineMarkerOps, MimetypeBinary, editedSpan } from "../content/index.js";
11
14
  import { readFile } from "node:fs/promises";
@@ -60,14 +63,12 @@ const readMaxCommands = () => {
60
63
  };
61
64
  // PLURNK_MANIFEST_ITEMS — the turn-0 manifest preview. null = off (no foist);
62
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);
63
67
  const readManifestItems = () => {
64
68
  const raw = process.env.PLURNK_MANIFEST_ITEMS;
65
69
  if (raw === undefined || raw.length === 0)
66
70
  return null;
67
- const n = Number.parseInt(raw, 10);
68
- if (!Number.isFinite(n) || n === 0)
69
- return null;
70
- return n < 0 ? -1 : n;
71
+ return normalizeManifestItems(Number.parseInt(raw, 10));
71
72
  };
72
73
  // Resolution timeout — proposed entries auto-cancel if nothing arrives
73
74
  // within this window. SPEC.md §engine-rails (proposal lifecycle) + §methods (loop.resolve).
@@ -82,9 +83,9 @@ const readProposalTimeoutMs = () => {
82
83
  return n;
83
84
  };
84
85
  const pathnameFromPath = (path) => {
85
- if (path.kind === "url")
86
- return path.pathname;
87
- 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
88
89
  };
89
90
  // Default turn.status when ops were emitted but no SEND. Model is implicitly
90
91
  // continuing; loop.status stays 102 either way (only SEND broadcast advances
@@ -241,6 +242,7 @@ class Engine {
241
242
  #streamEventNotify;
242
243
  #wakeRunNotify;
243
244
  #injectRun;
245
+ #cancelRun;
244
246
  // Telemetry event fan-out: every TelemetryEvent pushed to the loop's
245
247
  // buffer is also broadcast live to the connected client(s) on the
246
248
  // session. Without this, the client sees `loop/terminated` with a
@@ -249,12 +251,13 @@ class Engine {
249
251
  #telemetryEventNotify;
250
252
  // Cached plurnk GBNF — read once on the first constrained generate (#189).
251
253
  #gbnfCache = null;
252
- constructor({ db, schemes, mimetypes, streamEventNotify, wakeRunNotify, injectRun, telemetryEventNotify, tokenize }) {
254
+ constructor({ db, schemes, mimetypes, streamEventNotify, wakeRunNotify, injectRun, cancelRun, telemetryEventNotify, tokenize }) {
253
255
  this.#db = db;
254
256
  this.#schemes = schemes;
255
257
  this.#streamEventNotify = streamEventNotify;
256
258
  this.#wakeRunNotify = wakeRunNotify;
257
259
  this.#injectRun = injectRun;
260
+ this.#cancelRun = cancelRun;
258
261
  this.#telemetryEventNotify = telemetryEventNotify;
259
262
  // Default to empty discovery — standalone Engine construction (in
260
263
  // tests) gets no handlers, and content flows through the framework's
@@ -389,7 +392,7 @@ class Engine {
389
392
  return { turnIds, finalStatus: row.status, hitMaxTurns: false, reason: "external" };
390
393
  }
391
394
  if (maxTurns >= 0 && turnIds.length >= maxTurns) {
392
- await this.#db.engine_loop_cancel.run({ loop_id: loopId });
395
+ await this.#db.engine_loop_cancel.run({ loop_id: loopId, message: "max_turns" });
393
396
  cleanup("forceful", "max_turns");
394
397
  return { turnIds, finalStatus: 499, hitMaxTurns: true, reason: "max_turns" };
395
398
  }
@@ -410,7 +413,7 @@ class Engine {
410
413
  turnIds.push(turn.turnId);
411
414
  // SPEC §grinder: budget hard-stop — packet won't fit even collapsed → abandon.
412
415
  if (turn.budgetHardStop) {
413
- await this.#db.engine_loop_cancel.run({ loop_id: loopId });
416
+ await this.#db.engine_loop_cancel.run({ loop_id: loopId, message: "budget_overflow" });
414
417
  cleanup("forceful", "budget_overflow");
415
418
  return { turnIds, finalStatus: 499, hitMaxTurns: false, reason: "budget_overflow" };
416
419
  }
@@ -451,7 +454,7 @@ class Engine {
451
454
  if (struck) {
452
455
  state.streak++;
453
456
  if (state.streak >= maxStrikes) {
454
- await this.#db.engine_loop_cancel.run({ loop_id: loopId });
457
+ await this.#db.engine_loop_cancel.run({ loop_id: loopId, message: "strike_threshold" });
455
458
  cleanup("forceful", "strike_threshold");
456
459
  return { turnIds, finalStatus: 499, hitMaxTurns: false, reason: "strike_threshold" };
457
460
  }
@@ -506,7 +509,10 @@ class Engine {
506
509
  // §actor-boundary keystone); foist a READ of each into THIS turn-0 so the model
507
510
  // reads them inline. It sees only the READ — the materializing EDIT
508
511
  // lives in the plurnk run's log, never the model's.
509
- for (const doc of Paths.docs()) {
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)) {
510
516
  const docTarget = {
511
517
  kind: "url", raw: `plurnk:///${doc.entryName}`, scheme: "plurnk",
512
518
  username: null, password: null, hostname: null, port: null,
@@ -550,8 +556,8 @@ class Engine {
550
556
  }
551
557
  }
552
558
  // plurnk:///manifest.json — rewritten EVERY turn (a live view of the
553
- // entry set, which changes each turn). A derived view like the index,
554
- // NOT an action — written directly (Engine.inject's path): no log entry,
559
+ // entry set, which changes each turn). A derived view (computed each
560
+ // turn), NOT an action — written directly (Engine.inject's path): no log entry,
555
561
  // no sequence slot, not dispatched. The catalog body is built in the
556
562
  // schemes layer (_entry-manifest); the engine only orchestrates the
557
563
  // per-turn write. Does not list itself.
@@ -584,7 +590,9 @@ class Engine {
584
590
  // manifest is JSON); off by default. AFTER the manifest write so the READ hits
585
591
  // it, not a 404; same plurnk-origin foist as the operator docs.
586
592
  if (seq === 1) {
587
- const manifestItems = readManifestItems();
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();
588
596
  if (manifestItems !== null) {
589
597
  const manifestRead = {
590
598
  op: "READ", suffix: "", signal: null, lineMarker: null,
@@ -710,7 +718,8 @@ class Engine {
710
718
  // entries (avoids bloating forensics with hundreds of identical refusals)
711
719
  // and the model gets a single telemetry signal next packet so it knows
712
720
  // its emission was truncated.
713
- const maxCommands = readMaxCommands();
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);
714
723
  const opsToDispatch = packetAssistant.ops.slice(0, maxCommands);
715
724
  const droppedCount = opsCount - opsToDispatch.length;
716
725
  const statuses = [];
@@ -905,10 +914,12 @@ class Engine {
905
914
  for (const t of sections.log.byTurn)
906
915
  lines.push(`| ${t.turn} | ${t.tokens} |`);
907
916
  }
908
- // The heaviest individual entries — the FOLD targets behind the
909
- // weight (§tokenomics {§tokenomics-largest-entries}).
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.").
910
921
  if (sections.log.largest.length > 0) {
911
- lines.push("Heaviest entries:", "| entry | tokens |", "|---|--:|");
922
+ lines.push("Heaviest items:", "| item | tokens |", "|---|--:|");
912
923
  for (const e of sections.log.largest)
913
924
  lines.push(`| ${e.path} | ${e.tokens} |`);
914
925
  }
@@ -935,13 +946,31 @@ class Engine {
935
946
  // no backticks — see packet-wire.ts).
936
947
  if (this.#executors !== undefined) {
937
948
  for (const tag of this.#executors.availableRuntimes()) {
938
- const example = this.#executors.entry(tag)?.example;
939
- if (example)
940
- 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}`);
941
956
  }
942
957
  }
943
958
  return tools;
944
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
+ }
945
974
  // SPEC §grinder — the budget grinder. Runs pre-LLM (in runTurn, after the packet
946
975
  // is built, before provider.generate); fires only on actual overflow. Two
947
976
  // passes, re-measuring between. Folds (never deletes) — the prior turn's logs,
@@ -1008,7 +1037,7 @@ class Engine {
1008
1037
  }
1009
1038
  // Render-time mimetype invocation (SPEC §mimetype {§mimetype-handlers-fire-render-time},
1010
1039
  // §per-entry-channels {§per-entry-channels-preview-is-handler-output}). For each (run, entry, channel)
1011
- // with indexed=1, pass the channel's current content through
1040
+ // with expanded=1, pass the channel's current content through
1012
1041
  // mimetype.preview(content, budget). State is included verbatim — engine
1013
1042
  // does NOT branch on it (§channel-state {§channel-state-engine-does-not-branch-on-state}).
1014
1043
  // SPEC §telemetry: model-facing alert surface.
@@ -1069,7 +1098,7 @@ class Engine {
1069
1098
  mimetype_rx: r.mimetype_rx,
1070
1099
  tx: r.mimetype_tx === "application/json" ? JSON.parse(r.tx) : r.tx,
1071
1100
  mimetype_tx: r.mimetype_tx,
1072
- folded: r.indexed === 0,
1101
+ folded: r.expanded === 0,
1073
1102
  source: r.source,
1074
1103
  }));
1075
1104
  }
@@ -1096,6 +1125,19 @@ class Engine {
1096
1125
  });
1097
1126
  written++;
1098
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
+ }
1099
1141
  return written;
1100
1142
  }
1101
1143
  // §env-delta — the filesystem as an actor. Ambient disk divergences detected at
@@ -1223,6 +1265,9 @@ class Engine {
1223
1265
  // resolve synchronously inside their handlers.
1224
1266
  const target = this.#extractTarget(statement.target);
1225
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 });
1226
1271
  const event = {
1227
1272
  logEntryId, sessionId, runId, loopId, turnId,
1228
1273
  op: statement.op,
@@ -1230,6 +1275,7 @@ class Engine {
1230
1275
  body: typeof result.body === "string" ? result.body : "",
1231
1276
  attrs: (result.attrs ?? {}),
1232
1277
  flags,
1278
+ staleClobberRisk: diverged !== undefined,
1233
1279
  };
1234
1280
  for (const listener of this.#proposalPendingListeners) {
1235
1281
  try {
@@ -1567,6 +1613,9 @@ class Engine {
1567
1613
  }
1568
1614
  if (ctx.injectRun === undefined)
1569
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;
1570
1619
  const branchRunId = await Fork.fork(this.#db, srcRunId);
1571
1620
  const branch = await this.#db.fork_get_run.get({ id: branchRunId });
1572
1621
  await ctx.injectRun({ sessionId: ctx.sessionId, runId: branchRunId, prompt: typeof statement.body === "string" ? statement.body : "" });
@@ -1648,6 +1697,23 @@ class Engine {
1648
1697
  return { status: 501 };
1649
1698
  return await execHandler.kill(pathnameFromPath(path), ctx);
1650
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
+ }
1651
1717
  const handler = this.#schemes.get(schemeName);
1652
1718
  if (handler === undefined || typeof handler.deleteEntry !== "function")
1653
1719
  return { status: 501 };
@@ -1750,7 +1816,10 @@ class Engine {
1750
1816
  if (status === null)
1751
1817
  return { status: 400 };
1752
1818
  if (status === 200 || status === 499) {
1753
- await this.#db.engine_loop_set_status.run({ status, loop_id: loopId });
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 });
1754
1823
  }
1755
1824
  return { status };
1756
1825
  }
@@ -1872,12 +1941,14 @@ class Engine {
1872
1941
  if (path === null)
1873
1942
  return { scheme: null, username: null, password: null, hostname: null, port: null, pathname: null, params: null, fragment: null };
1874
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.
1875
- if (path.kind === "local" || path.kind === "regex")
1876
- return { scheme: null, username: null, password: null, hostname: null, port: null, pathname: path.raw, params: null, fragment: 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
1946
+ if (path.kind === "local")
1947
+ return { scheme: null, username: null, password: null, hostname: null, port: null, pathname: decodePathParens(path.raw), params: null, fragment: null }; // #239 item 4
1877
1948
  const scheme = path.scheme === "file" ? null : path.scheme;
1878
1949
  return {
1879
1950
  scheme, username: path.username, password: path.password,
1880
- hostname: path.hostname, port: path.port, pathname: path.pathname,
1951
+ hostname: path.hostname, port: path.port, pathname: decodePathParens(path.pathname), // #239 item 4
1881
1952
  params: JSON.stringify(path.params), fragment: path.fragment,
1882
1953
  };
1883
1954
  }