@gajae-code/coding-agent 0.15.4 → 0.15.5

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 (42) hide show
  1. package/CHANGELOG.md +20 -1
  2. package/dist/types/cli/update-cli.d.ts +25 -5
  3. package/dist/types/config/autorouting-tier-map.d.ts +3 -0
  4. package/dist/types/extensibility/extensions/types.d.ts +3 -0
  5. package/dist/types/extensibility/shared-events.d.ts +2 -0
  6. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +8 -1
  7. package/dist/types/sdk/cli/session-cli.d.ts +42 -1
  8. package/dist/types/sdk/host/host.d.ts +2 -0
  9. package/dist/types/sdk/host/session-runtime.d.ts +4 -0
  10. package/dist/types/session/agent-session.d.ts +4 -2
  11. package/dist/types/session/internal/managed-session-storage.d.ts +4 -3
  12. package/dist/types/tools/path-utils.d.ts +5 -6
  13. package/dist/types/tools/tmux-self-injection-guard.d.ts +15 -0
  14. package/package.json +7 -7
  15. package/src/cli/update-cli.ts +102 -1
  16. package/src/commands/sdk.ts +5 -0
  17. package/src/config/autorouting-tier-map.ts +1 -0
  18. package/src/config/model-profiles.ts +5 -5
  19. package/src/config/model-registry.ts +10 -4
  20. package/src/coordinator-mcp/policy.ts +1 -1
  21. package/src/extensibility/extensions/types.ts +3 -0
  22. package/src/extensibility/shared-events.ts +2 -0
  23. package/src/gjc-runtime/launch-worktree.ts +44 -7
  24. package/src/gjc-runtime/ralplan-runtime.ts +14 -1
  25. package/src/gjc-runtime/tmux-sessions.ts +145 -48
  26. package/src/internal-urls/docs-index.generated.ts +7 -7
  27. package/src/modes/prompt-action-autocomplete.ts +26 -47
  28. package/src/sdk/bus/chat-daemon-control.ts +2 -2
  29. package/src/sdk/bus/kind-aware-reconciliation.ts +10 -2
  30. package/src/sdk/bus/prompt-reconciliation.ts +5 -2
  31. package/src/sdk/bus/telegram-daemon-contract.ts +8 -1
  32. package/src/sdk/cli/session-cli.ts +149 -4
  33. package/src/sdk/host/host.ts +7 -0
  34. package/src/sdk/host/session-runtime.test.ts +821 -22
  35. package/src/sdk/host/session-runtime.ts +669 -67
  36. package/src/sdk/router/session-router.ts +69 -15
  37. package/src/session/agent-session.ts +232 -15
  38. package/src/session/internal/managed-session-storage.ts +64 -26
  39. package/src/session/session-manager.ts +26 -8
  40. package/src/tools/bash.ts +14 -0
  41. package/src/tools/path-utils.ts +77 -29
  42. package/src/tools/tmux-self-injection-guard.ts +359 -0
@@ -242,9 +242,8 @@ export interface ManagedFileSnapshot {
242
242
  };
243
243
  }
244
244
  /**
245
- * Descriptor identity of one managed file object. Deliberately excludes the
246
- * content sha256: append receipts must bind the post-operation object without
247
- * re-reading the whole payload.
245
+ * Descriptor identity of one managed file object. Append receipts include a
246
+ * content digest when the append implementation has already computed it.
248
247
  */
249
248
  export interface ManagedFileIdentity {
250
249
  dev: bigint;
@@ -253,6 +252,7 @@ export interface ManagedFileIdentity {
253
252
  size: number;
254
253
  mtimeNs: bigint;
255
254
  ctimeNs: bigint;
255
+ sha256?: string;
256
256
  }
257
257
 
258
258
  /**
@@ -281,6 +281,7 @@ function sameManagedIdentity(left: ManagedFileIdentity, right: ManagedFileIdenti
281
281
  export interface ManagedBoundedAppendExpectation {
282
282
  readonly dev: string;
283
283
  readonly ino: string;
284
+ readonly nlink: string;
284
285
  readonly size: string;
285
286
  readonly mtimeNs: string;
286
287
  readonly ctimeNs: string;
@@ -295,6 +296,7 @@ function managedFileIdentityFromNative(identity: RecoveryFsIdentity): ManagedFil
295
296
  size: Number(identity.size),
296
297
  mtimeNs: BigInt(identity.mtimeNs),
297
298
  ctimeNs: BigInt(identity.ctimeNs),
299
+ ...(identity.sha256 ? { sha256: identity.sha256 } : {}),
298
300
  };
299
301
  }
300
302
 
@@ -1704,6 +1706,7 @@ export class ManagedSessionDescendantStore {
1704
1706
  return {
1705
1707
  dev: identity.dev.toString(),
1706
1708
  ino: identity.ino.toString(),
1709
+ nlink: identity.nlink.toString(),
1707
1710
  size: identity.size.toString(),
1708
1711
  mtimeNs: identity.mtimeNs.toString(),
1709
1712
  ctimeNs: identity.ctimeNs.toString(),
@@ -1718,17 +1721,20 @@ export class ManagedSessionDescendantStore {
1718
1721
  ): ManagedAppendReceipt {
1719
1722
  this.#beforeMutation();
1720
1723
  this.#assertBound();
1724
+ let appendExpectation = expected;
1721
1725
  if (!this.#authority) {
1722
1726
  const current = captureManagedFileIdentityStreamingNoFollow(this.#resolve(relativePath));
1723
- if (
1724
- current.dev.toString() !== expected.dev ||
1725
- current.ino.toString() !== expected.ino ||
1726
- current.size.toString() !== expected.size ||
1727
- current.mtimeNs.toString() !== expected.mtimeNs ||
1728
- current.ctimeNs.toString() !== expected.ctimeNs ||
1729
- current.sha256 !== expected.sha256
1730
- )
1727
+ const identityMatches =
1728
+ current.dev.toString() === expected.dev &&
1729
+ current.ino.toString() === expected.ino &&
1730
+ current.nlink.toString() === expected.nlink &&
1731
+ current.size.toString() === expected.size &&
1732
+ current.sha256 === expected.sha256;
1733
+ if (!identityMatches) {
1731
1734
  throw new Error("managed_append_identity_mismatch");
1735
+ }
1736
+ // mtime/ctime drift on the same file object is harmless. The append
1737
+ // helper performs its own descriptor-bound mutation check.
1732
1738
  const appended = appendManagedFileStreamingSync(
1733
1739
  this.#resolve(relativePath),
1734
1740
  bytes,
@@ -1738,16 +1744,35 @@ export class ManagedSessionDescendantStore {
1738
1744
  this.#assertBound();
1739
1745
  return managedAppendReceiptFromIdentity(appended);
1740
1746
  }
1747
+ const current = this.captureBoundedAppendExpectation(relativePath);
1748
+ if (!current) throw new Error("managed_append_identity_mismatch");
1749
+ if (
1750
+ current.dev !== expected.dev ||
1751
+ current.ino !== expected.ino ||
1752
+ current.nlink !== expected.nlink ||
1753
+ current.size !== expected.size ||
1754
+ current.sha256 !== expected.sha256
1755
+ )
1756
+ throw new Error("managed_append_identity_mismatch");
1757
+ const descriptorMatches =
1758
+ current.dev === expected.dev &&
1759
+ current.ino === expected.ino &&
1760
+ current.nlink === expected.nlink &&
1761
+ current.size === expected.size &&
1762
+ current.mtimeNs === expected.mtimeNs &&
1763
+ current.ctimeNs === expected.ctimeNs &&
1764
+ current.sha256 === expected.sha256;
1765
+ if (!descriptorMatches) appendExpectation = current;
1741
1766
  const relative = this.#relative(this.#resolve(relativePath));
1742
1767
  const appended = this.#authority.appendManaged(
1743
1768
  relative,
1744
1769
  bytes,
1745
- expected.dev,
1746
- expected.ino,
1747
- expected.size,
1748
- expected.mtimeNs,
1749
- expected.ctimeNs,
1750
- expected.sha256,
1770
+ appendExpectation.dev,
1771
+ appendExpectation.ino,
1772
+ appendExpectation.size,
1773
+ appendExpectation.mtimeNs,
1774
+ appendExpectation.ctimeNs,
1775
+ appendExpectation.sha256,
1751
1776
  );
1752
1777
  if (!appended.ok) throw managedAppendFailure(appended.code);
1753
1778
  if (!appended.identity)
@@ -1762,15 +1787,28 @@ export class ManagedSessionDescendantStore {
1762
1787
  expected: ManagedFileIdentity,
1763
1788
  ): ManagedAppendReceipt {
1764
1789
  const bounded = this.captureBoundedAppendExpectation(relativePath);
1765
- if (
1766
- !bounded ||
1767
- bounded.dev !== expected.dev.toString() ||
1768
- bounded.ino !== expected.ino.toString() ||
1769
- bounded.size !== String(expected.size) ||
1770
- bounded.mtimeNs !== expected.mtimeNs.toString() ||
1771
- bounded.ctimeNs !== expected.ctimeNs.toString()
1772
- )
1773
- throw new Error("managed_append_identity_mismatch");
1790
+ if (!bounded) throw new Error("managed_append_identity_mismatch");
1791
+ const descriptorMatches =
1792
+ bounded.dev === expected.dev.toString() &&
1793
+ bounded.ino === expected.ino.toString() &&
1794
+ bounded.nlink === expected.nlink.toString() &&
1795
+ bounded.size === String(expected.size) &&
1796
+ bounded.mtimeNs === expected.mtimeNs.toString() &&
1797
+ bounded.ctimeNs === expected.ctimeNs.toString();
1798
+ if (!descriptorMatches) {
1799
+ // Metadata-only drift is safe only when the same unlinked file object and
1800
+ // byte-exact predecessor are independently proven. Never accept a changed
1801
+ // object, length, link count, or digest as a benign touch.
1802
+ if (
1803
+ !expected.sha256 ||
1804
+ bounded.dev !== expected.dev.toString() ||
1805
+ bounded.ino !== expected.ino.toString() ||
1806
+ bounded.nlink !== expected.nlink.toString() ||
1807
+ bounded.size !== String(expected.size) ||
1808
+ bounded.sha256 !== expected.sha256
1809
+ )
1810
+ throw new Error("managed_append_identity_mismatch");
1811
+ }
1774
1812
  return this.appendExpectedSync(relativePath, bytes, bounded);
1775
1813
  }
1776
1814
 
@@ -8385,7 +8385,7 @@ export class SessionManager {
8385
8385
  this.#usageStatistics = commit.usageStatistics;
8386
8386
  this.#flushed = true;
8387
8387
  this.#ensuredOnDisk = true;
8388
- this.#managedPersistExpectedIdentity = managedIdentityFromDescriptor(terminalDescriptor);
8388
+ this.#managedPersistExpectedIdentity = this.#captureManagedPersistIdentity(sessionFile);
8389
8389
  this.#lazyReopenSucceeded = true;
8390
8390
  this.#lazyReopenFallbackReason = undefined;
8391
8391
  initialized = true;
@@ -8572,7 +8572,7 @@ export class SessionManager {
8572
8572
  this.#lazyReopenFallbackReason = "bounded_first_open_descriptor_changed";
8573
8573
  return false;
8574
8574
  }
8575
- this.#managedPersistExpectedIdentity = managedIdentityFromDescriptor(finalDescriptor);
8575
+ this.#managedPersistExpectedIdentity = this.#captureManagedPersistIdentity(sessionFile);
8576
8576
  this.#sessionId = discovery.header.id;
8577
8577
  this.#sessionName = discovery.header.title;
8578
8578
  this.#titleSource = discovery.header.titleSource;
@@ -14945,9 +14945,27 @@ export class SessionManager {
14945
14945
  this.#managedPersistExpectedIdentity = undefined;
14946
14946
  return;
14947
14947
  }
14948
- const descriptor = this.#managedTranscriptStore(sessionFile).descriptorExpected(path.basename(sessionFile));
14949
- if (!descriptor) throw new Error("managed_persist_identity_unavailable");
14950
- this.#managedPersistExpectedIdentity = managedIdentityFromDescriptor(descriptor);
14948
+ this.#managedPersistExpectedIdentity = this.#captureManagedPersistIdentity(sessionFile);
14949
+ }
14950
+
14951
+ /** Capture one descriptor-bound digest for a future metadata-drift comparison. */
14952
+ #captureManagedPersistIdentity(sessionFile: string): ManagedFileIdentity {
14953
+ const store = this.#managedTranscriptStore(sessionFile);
14954
+ const relativePath = path.basename(sessionFile);
14955
+ const bounded = store.captureBoundedAppendExpectation(relativePath);
14956
+ const descriptor = store.descriptorExpected(relativePath);
14957
+ if (!bounded || !descriptor) throw new Error("managed_persist_identity_unavailable");
14958
+ const identity = managedIdentityFromDescriptor(descriptor);
14959
+ if (
14960
+ bounded.dev !== identity.dev.toString() ||
14961
+ bounded.ino !== identity.ino.toString() ||
14962
+ bounded.nlink !== identity.nlink.toString() ||
14963
+ bounded.size !== String(identity.size) ||
14964
+ bounded.mtimeNs !== identity.mtimeNs.toString() ||
14965
+ bounded.ctimeNs !== identity.ctimeNs.toString()
14966
+ )
14967
+ throw new Error("managed_persist_identity_unavailable");
14968
+ return { ...identity, sha256: bounded.sha256 };
14951
14969
  }
14952
14970
  /** Current transcript descriptor, or null when unavailable. */
14953
14971
  #managedDescriptorSnapshotOrNull(): DescriptorSnapshot | null {
@@ -14997,7 +15015,7 @@ export class SessionManager {
14997
15015
  } else store.replaceSync(relativePath, bytes);
14998
15016
  const descriptor = store.descriptorExpected(relativePath);
14999
15017
  if (!descriptor) throw new Error("managed_replace_identity_unavailable");
15000
- this.#managedPersistExpectedIdentity = managedIdentityFromDescriptor(descriptor);
15018
+ this.#managedPersistExpectedIdentity = this.#captureManagedPersistIdentity(sessionFile);
15001
15019
  this.#publishCommitMarkerFromCurrentTranscriptSync();
15002
15020
  return;
15003
15021
  }
@@ -19961,7 +19979,7 @@ export class SessionManager {
19961
19979
  const returnManagedDescriptor = managedInspectionStore?.descriptorExpected(path.basename(filePath));
19962
19980
  if (!returnManagedDescriptor || !sameDescriptor(managedBoundedDescriptor, returnManagedDescriptor))
19963
19981
  throw new Error("Could not open session: unstable");
19964
- manager.#managedPersistExpectedIdentity = managedIdentityFromDescriptor(returnManagedDescriptor);
19982
+ manager.#managedPersistExpectedIdentity = manager.#captureManagedPersistIdentity(filePath);
19965
19983
  const header = manager.#fileEntries.find(entry => entry.type === "session") as SessionHeader | undefined;
19966
19984
  if (header?.cwd) manager.cwd = header.cwd;
19967
19985
  manager.buildSessionContext();
@@ -20079,7 +20097,7 @@ export class SessionManager {
20079
20097
  const finalDescriptor = store.descriptorExpected(path.basename(resolved));
20080
20098
  if (!finalDescriptor || !sameDescriptor(capturedDescriptor, finalDescriptor))
20081
20099
  throw new Error("source_changed");
20082
- boundedManager.#managedPersistExpectedIdentity = managedIdentityFromDescriptor(finalDescriptor);
20100
+ boundedManager.#managedPersistExpectedIdentity = boundedManager.#captureManagedPersistIdentity(resolved);
20083
20101
  boundedManager.buildSessionContext();
20084
20102
  return boundedManager;
20085
20103
  }
package/src/tools/bash.ts CHANGED
@@ -59,6 +59,7 @@ import {
59
59
  } from "./output-meta";
60
60
  import { resolveToCwd } from "./path-utils";
61
61
  import { formatToolWorkingDirectory, replaceTabs } from "./render-utils";
62
+ import { checkTmuxSelfInjection } from "./tmux-self-injection-guard";
62
63
  import { ToolAbortError, ToolError } from "./tool-errors";
63
64
  import { toolResult } from "./tool-result";
64
65
  import { clampTimeout, TOOL_TIMEOUTS } from "./tool-timeouts";
@@ -1053,6 +1054,19 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
1053
1054
  ...internalUrlOptions,
1054
1055
  ensureLocalParentDirs: this.session.bashRestrictionProfile !== "read-only",
1055
1056
  });
1057
+ // Issue #5039: refuse tmux write verbs whose target resolves to this
1058
+ // agent's own tmux pane — such bytes forge a human `user` turn in the
1059
+ // transcript. This is after command expansion but before any spawn, so
1060
+ // blocked attempts inject no bytes and create no transcript turn.
1061
+ if (this.session.bashRestrictionProfile !== "read-only") {
1062
+ const injection = await checkTmuxSelfInjection(command, {
1063
+ env: process.env,
1064
+ cwd: this.session.cwd,
1065
+ });
1066
+ if (injection.block) {
1067
+ throw new ToolError(injection.reason ?? "Blocked: tmux self-pane injection.");
1068
+ }
1069
+ }
1056
1070
  const expandedEnv = env
1057
1071
  ? Object.fromEntries(
1058
1072
  await Promise.all(
@@ -11,17 +11,20 @@ const UNICODE_SPACES = /[\u00A0\u2000-\u200A\u202F\u205F\u3000]/g;
11
11
  const FILE_LINE_RANGE_RE = /^(?:L?\d+(?:[-+]L?\d+|-)?(?:,L?\d+(?:[-+]L?\d+|-)?)*|raw|conflicts)$/i;
12
12
  const FILE_LINE_RANGE_ONLY_RE = /^L?\d+(?:[-+]L?\d+|-)?(?:,L?\d+(?:[-+]L?\d+|-)?)*$/i;
13
13
  const FILE_RAW_ONLY_RE = /^raw$/i;
14
- // Schemes whose authority grammar is identifier-shaped and may therefore carry
15
- // a read selector immediately after the authority. Colons in a path, query, or
16
- // fragment remain part of the URL and are never considered selectors.
17
- const INTERNAL_SCHEMES_WITH_SELECTORS: Record<string, true> = {
14
+ // Schemes whose authority grammar is identifier-shaped and cannot contain a
15
+ // literal colon. Colons in a path, query, or fragment remain part of the URL
16
+ // and are never considered selectors.
17
+ const INTERNAL_SCHEMES_WITH_UNAMBIGUOUS_AUTHORITIES: Record<string, true> = {
18
18
  agent: true,
19
19
  artifact: true,
20
- issue: true,
21
- local: true,
22
20
  memory: true,
23
- gjc: true,
21
+ issue: true,
24
22
  pr: true,
23
+ };
24
+ const INTERNAL_SCHEMES_WITH_SELECTORS: Record<string, true> = {
25
+ ...INTERNAL_SCHEMES_WITH_UNAMBIGUOUS_AUTHORITIES,
26
+ gjc: true,
27
+ local: true,
25
28
  rule: true,
26
29
  skill: true,
27
30
  };
@@ -33,6 +36,18 @@ function normalizeUnicodeSpaces(str: string): string {
33
36
  return str.replace(UNICODE_SPACES, " ");
34
37
  }
35
38
 
39
+ function rawSkillPrefixForName(authority: string, skillName: string): string | undefined {
40
+ for (let index = 1; index < authority.length; index++) {
41
+ if (authority[index] !== ":") continue;
42
+ try {
43
+ if (decodeURIComponent(authority.slice(0, index)) === skillName) return authority.slice(0, index);
44
+ } catch {
45
+ return undefined;
46
+ }
47
+ }
48
+ return undefined;
49
+ }
50
+
36
51
  function tryMacOSScreenshotPath(filePath: string): string {
37
52
  // macOS writes a narrow no-break space before AM/PM, but the model normalizes it
38
53
  // to a plain space. The original name is not always `…PM.png`: attachment paths
@@ -149,12 +164,11 @@ export function splitPathAndSel(rawPath: string): { path: string; sel?: string }
149
164
  /**
150
165
  * Variant of {@link splitPathAndSel} for internal URLs (`scheme://...`).
151
166
  *
152
- * Artifact authorities are generated identifiers, so their complete tail is
153
- * unambiguously a selector and malformed selectors can be rejected before
154
- * resolving the artifact. Other resources may legitimately use colons in
155
- * their identities, so they only lose strict, recognized selector suffixes.
156
- * Skill authorities additionally use the active registry to distinguish a
157
- * skill name from its selector without making path-utils depend on skills.
167
+ * Selector-capable internal authorities use identifier-shaped resource names,
168
+ * so an authority-only `:<tail>` is an explicit read selector even when the
169
+ * selector is malformed. Skill authorities are the exception: namespaced
170
+ * skill names already contain a colon, so the active registry gets first
171
+ * refusal and the fallback recognizes a later colon as the selector boundary.
158
172
  */
159
173
  export function splitInternalUrlSel(
160
174
  rawPath: string,
@@ -167,32 +181,66 @@ export function splitInternalUrlSel(
167
181
 
168
182
  const schemeEnd = schemeMatch[0].length;
169
183
  const authorityTerminator = rawPath.slice(schemeEnd).search(/[/?#]/);
170
- if (authorityTerminator !== -1) return { path: rawPath };
171
- const authority = rawPath.slice(schemeEnd);
184
+ const authorityEnd = authorityTerminator === -1 ? rawPath.length : schemeEnd + authorityTerminator;
185
+ const authority = rawPath.slice(schemeEnd, authorityEnd);
186
+ const authoritySuffix = rawPath.slice(authorityEnd);
172
187
  const firstColon = authority.indexOf(":");
173
188
  if (firstColon === -1) return { path: rawPath };
174
-
175
- if (scheme === "artifact") {
176
- return { path: rawPath.slice(0, schemeEnd + firstColon), sel: authority.slice(firstColon + 1) };
177
- }
189
+ if (firstColon === 0) return { path: rawPath };
178
190
 
179
191
  if (scheme === "skill") {
180
192
  const activeSkillNames = options.activeSkillNames ?? [];
181
- if (activeSkillNames.includes(authority)) return { path: rawPath };
182
- const skillName = activeSkillNames
183
- .filter(name => authority.startsWith(`${name}:`))
184
- .sort((a, b) => b.length - a.length)[0];
185
- if (skillName) {
193
+ let decodedAuthority: string | undefined;
194
+ try {
195
+ decodedAuthority = decodeURIComponent(authority);
196
+ } catch {
197
+ // Let the resolver report malformed URL encoding when no selector boundary
198
+ // can be established from the raw authority.
199
+ }
200
+ if (decodedAuthority !== undefined && activeSkillNames.includes(decodedAuthority)) return { path: rawPath };
201
+ const rawSkillPrefix = activeSkillNames
202
+ .map(name => ({ name, prefix: rawSkillPrefixForName(authority, name) }))
203
+ .filter(item => item.prefix !== undefined)
204
+ .sort((a, b) => b.name.length - a.name.length)[0]?.prefix;
205
+ if (rawSkillPrefix) {
186
206
  return {
187
- path: `${rawPath.slice(0, schemeEnd)}${skillName}`,
188
- sel: authority.slice(skillName.length + 1),
207
+ path: `${rawPath.slice(0, schemeEnd)}${rawSkillPrefix}${authoritySuffix}`,
208
+ sel: authority.slice(rawSkillPrefix.length + 1),
189
209
  };
190
210
  }
211
+ const strict = splitPathAndSel(authority);
212
+ if (strict.sel !== undefined) {
213
+ return { path: `${rawPath.slice(0, schemeEnd)}${strict.path}${authoritySuffix}`, sel: strict.sel };
214
+ }
215
+ const firstTail = authority.slice(firstColon + 1);
216
+ if (FILE_LINE_RANGE_RE.test(firstTail)) {
217
+ return { path: `${rawPath.slice(0, schemeEnd + firstColon)}${authoritySuffix}`, sel: firstTail };
218
+ }
219
+ const selectorColon = authority.indexOf(":", firstColon + 1);
220
+ if (selectorColon === -1) return { path: rawPath };
221
+ return {
222
+ path: `${rawPath.slice(0, schemeEnd + selectorColon)}${authoritySuffix}`,
223
+ sel: authority.slice(selectorColon + 1),
224
+ };
225
+ }
226
+
227
+ if (!INTERNAL_SCHEMES_WITH_UNAMBIGUOUS_AUTHORITIES[scheme]) {
228
+ // A path/query/fragment after a path-like authority makes a colon-bearing
229
+ // resource identity indistinguishable from an authority selector. Preserve
230
+ // the complete URL; path-like selectors are accepted only on authority-only
231
+ // URLs, where the existing strict grammar is the sole interpretation.
232
+ if (authoritySuffix !== "") return { path: rawPath };
233
+ const strict = splitPathAndSel(authority);
234
+ if (strict.sel !== undefined) {
235
+ return { path: `${rawPath.slice(0, schemeEnd)}${strict.path}`, sel: strict.sel };
236
+ }
237
+ return { path: rawPath };
191
238
  }
192
239
 
193
- const strict = splitPathAndSel(authority);
194
- if (strict.sel === undefined) return { path: rawPath };
195
- return { path: `${rawPath.slice(0, schemeEnd)}${strict.path}`, sel: strict.sel };
240
+ return {
241
+ path: `${rawPath.slice(0, schemeEnd + firstColon)}${authoritySuffix}`,
242
+ sel: authority.slice(firstColon + 1),
243
+ };
196
244
  }
197
245
 
198
246
  function assertNotInternalUrl(expanded: string, original: string): void {