@gajae-code/coding-agent 0.18.1 → 0.18.6
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/CHANGELOG.md +161 -0
- package/dist/types/cli/stats-cli.d.ts +0 -5
- package/dist/types/cli-commands.d.ts +10 -0
- package/dist/types/cli-ordinary.d.ts +13 -0
- package/dist/types/cli.d.ts +12 -3
- package/dist/types/commands/contribution-prep.d.ts +0 -3
- package/dist/types/commands/sdk.d.ts +3 -0
- package/dist/types/commands/update.d.ts +1 -0
- package/dist/types/config/autorouting-tier-map.d.ts +29 -2
- package/dist/types/config/model-profile-activation.d.ts +19 -0
- package/dist/types/config/model-registry.d.ts +16 -1
- package/dist/types/coordinator-mcp/session-reaper.d.ts +5 -0
- package/dist/types/edit/modes/replace.d.ts +3 -3
- package/dist/types/edit/streaming.d.ts +2 -2
- package/dist/types/exa/mcp-client.d.ts +1 -5
- package/dist/types/exec/bash-shell-guardian.d.ts +26 -0
- package/dist/types/exec/bash-shell-supervisor.d.ts +12 -0
- package/dist/types/hashline/parser.d.ts +28 -5
- package/dist/types/modes/acp/acp-agent.d.ts +6 -1
- package/dist/types/sdk/broker/discovery.d.ts +8 -0
- package/dist/types/sdk/broker/ensure.d.ts +1 -0
- package/dist/types/sdk/broker/lifecycle-ledger.d.ts +4 -0
- package/dist/types/sdk/broker/lifecycle.d.ts +6 -1
- package/dist/types/sdk/broker/recovery-backoff.d.ts +62 -0
- package/dist/types/sdk/broker/runtime.d.ts +21 -1
- package/dist/types/sdk/broker/startup-budget.d.ts +5 -1
- package/dist/types/sdk/bus/index.d.ts +1 -0
- package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +1 -1
- package/dist/types/sdk/bus/telegram-daemon.d.ts +1 -0
- package/dist/types/sdk/cli/diagnostics-cli.d.ts +18 -0
- package/dist/types/sdk/diagnostics/observe-broker.d.ts +185 -0
- package/dist/types/sdk/host/control/operations.d.ts +1 -1
- package/dist/types/sdk/host/session-runtime.d.ts +7 -0
- package/dist/types/sdk/index.d.ts +1 -0
- package/dist/types/sdk/prompt-watchdog.d.ts +1 -1
- package/dist/types/sdk/router/session-router.d.ts +2 -0
- package/dist/types/sdk/session.d.ts +13 -0
- package/dist/types/session/agent-session.d.ts +5 -1
- package/dist/types/tools/ask-contract.d.ts +75 -254
- package/dist/types/tools/bash.d.ts +1 -0
- package/package.json +7 -7
- package/scripts/build-sdk-package-smoke.ts +5 -0
- package/src/cli/public-command-entry.ts +5 -0
- package/src/cli/ssh-cli.ts +6 -3
- package/src/cli/stats-cli.ts +4 -36
- package/src/cli/web-search-cli.ts +9 -3
- package/src/cli-commands.ts +14 -0
- package/src/cli-main.ts +4 -8
- package/src/cli-ordinary.ts +197 -0
- package/src/cli.ts +25 -186
- package/src/commands/commit.ts +2 -2
- package/src/commands/contribution-prep.ts +1 -3
- package/src/commands/sdk.ts +60 -2
- package/src/commands/update.ts +15 -1
- package/src/config/autorouting-tier-map.ts +20 -2
- package/src/config/file-lock.ts +91 -2
- package/src/config/model-preset-registry.ts +17 -2
- package/src/config/model-profile-activation.ts +73 -5
- package/src/config/model-profile-contract.ts +4 -1
- package/src/config/model-profiles.ts +29 -29
- package/src/config/model-registry.ts +45 -17
- package/src/config/model-resolver.ts +9 -1
- package/src/coordinator-mcp/event-webhook.ts +19 -14
- package/src/coordinator-mcp/policy.ts +14 -4
- package/src/coordinator-mcp/server.ts +142 -7
- package/src/coordinator-mcp/session-reaper.ts +9 -1
- package/src/defaults/gjc/skills/deep-interview/SKILL.md +1 -1
- package/src/edit/modes/replace.ts +6 -6
- package/src/edit/streaming.ts +15 -15
- package/src/exa/mcp-client.ts +41 -8
- package/src/exec/bash-shell-guardian.ts +184 -34
- package/src/exec/bash-shell-supervisor.ts +60 -3
- package/src/hashline/execute.ts +51 -3
- package/src/hashline/parser.ts +63 -16
- package/src/internal-urls/docs-index.generated.ts +29 -28
- package/src/memories/index.ts +8 -2
- package/src/modes/acp/acp-agent.ts +688 -371
- package/src/modes/components/tool-execution.ts +1 -1
- package/src/modes/controllers/ssh-command-controller.ts +3 -1
- package/src/prompts/tools/ask.md +2 -0
- package/src/sdk/broker/broker.ts +46 -0
- package/src/sdk/broker/daemon-entry.ts +67 -6
- package/src/sdk/broker/discovery.ts +8 -0
- package/src/sdk/broker/ensure.ts +156 -20
- package/src/sdk/broker/lifecycle-ledger.ts +110 -12
- package/src/sdk/broker/lifecycle.ts +108 -34
- package/src/sdk/broker/recovery-backoff.ts +135 -0
- package/src/sdk/broker/runtime.ts +88 -1
- package/src/sdk/broker/startup-budget.ts +6 -1
- package/src/sdk/broker/transport.ts +5 -1
- package/src/sdk/bus/chat-daemon-control.ts +2 -2
- package/src/sdk/bus/index.ts +305 -19
- package/src/sdk/bus/telegram-daemon-contract.ts +1 -1
- package/src/sdk/bus/telegram-daemon-control.ts +15 -12
- package/src/sdk/bus/telegram-daemon.ts +34 -1
- package/src/sdk/cli/diagnostics-cli.ts +247 -0
- package/src/sdk/diagnostics/observe-broker.ts +654 -0
- package/src/sdk/host/control/dispatch.ts +52 -4
- package/src/sdk/host/control/operations.ts +1 -1
- package/src/sdk/host/session-runtime.test.ts +40 -2
- package/src/sdk/host/session-runtime.ts +112 -8
- package/src/sdk/index.ts +1 -0
- package/src/sdk/lifecycle-session.ts +1 -0
- package/src/sdk/prompt-watchdog.ts +17 -4
- package/src/sdk/protocol/operation-registry.ts +1 -1
- package/src/sdk/router/session-router.ts +7 -1
- package/src/sdk/session.ts +37 -8
- package/src/session/agent-session.ts +128 -41
- package/src/session/blob-store.ts +78 -33
- package/src/session/internal/managed-session-storage.ts +119 -3
- package/src/setup/hermes/templates/operator-instructions.v1.md +19 -1
- package/src/setup/hermes-setup.ts +4 -1
- package/src/slash-commands/builtin-registry.ts +8 -5
- package/src/tools/ask-contract.test.ts +76 -1
- package/src/tools/ask-contract.ts +57 -6
- package/src/tools/ask.ts +16 -7
- package/src/tools/bash.ts +77 -29
- package/src/tools/descriptors.test.ts +29 -0
- package/src/tools/tool-catalog.generated.ts +7 -4
- package/src/web/search/index.ts +2 -2
- package/dist/types/cli/grep-cli.d.ts +0 -17
- package/dist/types/cli/shell-cli.d.ts +0 -8
- package/dist/types/commands/grep.d.ts +0 -42
- package/dist/types/commands/shell.d.ts +0 -21
- package/src/cli/grep-cli.ts +0 -160
- package/src/cli/shell-cli.ts +0 -176
- package/src/commands/grep.ts +0 -48
- package/src/commands/shell.ts +0 -29
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { dlopen, FFIType, ptr } from "bun:ffi";
|
|
2
2
|
import * as childProcess from "node:child_process";
|
|
3
3
|
import { createHmac, randomUUID, timingSafeEqual } from "node:crypto";
|
|
4
|
+
import * as fsSync from "node:fs";
|
|
4
5
|
import * as fs from "node:fs/promises";
|
|
5
6
|
import * as os from "node:os";
|
|
6
7
|
import * as path from "node:path";
|
|
@@ -63,10 +64,10 @@ export function extendOwnedDarwinAncestry<T extends { uniqueId: bigint; parentUn
|
|
|
63
64
|
return addedPids;
|
|
64
65
|
}
|
|
65
66
|
|
|
66
|
-
function authenticateOwnershipRecord(
|
|
67
|
+
export function authenticateOwnershipRecord(
|
|
67
68
|
line: string,
|
|
68
69
|
ledgerToken: string,
|
|
69
|
-
): { processRef?: NativeProcess; darwinUniqueId?: bigint } | undefined {
|
|
70
|
+
): { processRef?: NativeProcess; darwinUniqueId?: bigint; pending?: true } | undefined {
|
|
70
71
|
const record = parseOwnershipRecord(line);
|
|
71
72
|
if (!record) return undefined;
|
|
72
73
|
const uniqueId = record.darwinUniqueId ?? "";
|
|
@@ -80,15 +81,20 @@ function authenticateOwnershipRecord(
|
|
|
80
81
|
return undefined;
|
|
81
82
|
}
|
|
82
83
|
if (received.byteLength !== expected.byteLength || !timingSafeEqual(received, expected)) return undefined;
|
|
83
|
-
if (process.platform === "darwin" && !record.darwinUniqueId) return undefined;
|
|
84
84
|
const { Process } = require("@gajae-code/natives") as NativeProcessBindings;
|
|
85
85
|
const owned = Process.fromPid(record.pid);
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
86
|
+
const darwinUniqueId = record.darwinUniqueId ? { darwinUniqueId: BigInt(record.darwinUniqueId) } : {};
|
|
87
|
+
if (owned?.incarnation === record.incarnation) return { processRef: owned, ...darwinUniqueId };
|
|
88
|
+
// fromPid() conflates death with an inconclusive lookup. Only a confirmed
|
|
89
|
+
// absence or a different incarnation settles the record; an unknown result
|
|
90
|
+
// keeps it pending so the caller retries the line on the next scan.
|
|
91
|
+
if (!owned && Process.observe(record.pid).status === "unknown") return { pending: true, ...darwinUniqueId };
|
|
92
|
+
return darwinUniqueId;
|
|
90
93
|
}
|
|
91
94
|
|
|
95
|
+
/** Scans an inconclusive ledger record may stay pending before tracking fails closed. */
|
|
96
|
+
const MAX_PENDING_LEDGER_ATTEMPTS = 50;
|
|
97
|
+
|
|
92
98
|
function enableLinuxChildSubreaper(): boolean {
|
|
93
99
|
if (process.platform !== "linux") return true;
|
|
94
100
|
try {
|
|
@@ -157,14 +163,27 @@ function findDarwinLedgerHolders(device: number, inode: bigint): NativeProcess[]
|
|
|
157
163
|
}
|
|
158
164
|
}
|
|
159
165
|
|
|
160
|
-
type DarwinAncestryTracker = {
|
|
166
|
+
export type DarwinAncestryTracker = {
|
|
161
167
|
seed(uniqueId: bigint): void;
|
|
162
168
|
track(processRef: NativeProcess, uniqueId?: bigint): boolean;
|
|
169
|
+
trackGuardian(processRef: NativeProcess, uniqueId?: bigint): boolean;
|
|
163
170
|
poll(): boolean;
|
|
164
171
|
close(): void;
|
|
165
172
|
};
|
|
166
173
|
|
|
167
|
-
|
|
174
|
+
export type DarwinIdentity = { uniqueId: bigint; parentUniqueId: bigint };
|
|
175
|
+
|
|
176
|
+
/** Test seams for the Darwin tracker; production uses libproc and the natives binding. */
|
|
177
|
+
export type DarwinAncestryTrackerDeps = {
|
|
178
|
+
uniqueIdentity?: (pid: number) => DarwinIdentity | undefined;
|
|
179
|
+
fromPid?: (pid: number) => NativeProcess | null | undefined;
|
|
180
|
+
observe?: (pid: number) => { status: "present" | "absent" | "unknown"; incarnation?: string };
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
export function createDarwinAncestryTracker(
|
|
184
|
+
owned: Map<string, NativeProcess>,
|
|
185
|
+
deps: DarwinAncestryTrackerDeps = {},
|
|
186
|
+
): DarwinAncestryTracker | undefined {
|
|
168
187
|
if (process.platform !== "darwin") return undefined;
|
|
169
188
|
try {
|
|
170
189
|
const proc = dlopen("/usr/lib/libproc.dylib", {
|
|
@@ -180,10 +199,12 @@ function createDarwinAncestryTracker(owned: Map<string, NativeProcess>): DarwinA
|
|
|
180
199
|
// own children still chain, but it is retried on every poll until retained.
|
|
181
200
|
const retainedUniqueIds = new Set<bigint>();
|
|
182
201
|
const { Process } = require("@gajae-code/natives") as NativeProcessBindings;
|
|
202
|
+
const fromPid = deps.fromPid ?? ((pid: number) => Process.fromPid(pid));
|
|
203
|
+
const observe = deps.observe ?? ((pid: number) => Process.observe(pid));
|
|
183
204
|
// Reuse buffer across poll iterations to avoid allocations inside the loop.
|
|
184
205
|
const identityBuffer = new Uint8Array(56);
|
|
185
206
|
const identityView = new DataView(identityBuffer.buffer);
|
|
186
|
-
const
|
|
207
|
+
const defaultUniqueIdentity = (pid: number): { uniqueId: bigint; parentUniqueId: bigint } | undefined => {
|
|
187
208
|
const bytes = proc.symbols.proc_pidinfo(pid, 17, 0, ptr(identityBuffer), identityBuffer.byteLength);
|
|
188
209
|
if (bytes !== identityBuffer.byteLength) return undefined;
|
|
189
210
|
return {
|
|
@@ -191,9 +212,92 @@ function createDarwinAncestryTracker(owned: Map<string, NativeProcess>): DarwinA
|
|
|
191
212
|
parentUniqueId: identityView.getBigUint64(24, true),
|
|
192
213
|
};
|
|
193
214
|
};
|
|
215
|
+
const uniqueIdentity = deps.uniqueIdentity ?? defaultUniqueIdentity;
|
|
216
|
+
const incarnationOnlyAnchors: NativeProcess[] = [];
|
|
217
|
+
const anchorKeys = new Set<string>();
|
|
218
|
+
const addIncarnationOnlyAnchor = (processRef: NativeProcess): void => {
|
|
219
|
+
const key = `${processRef.pid}:${processRef.incarnation}`;
|
|
220
|
+
if (anchorKeys.has(key)) return;
|
|
221
|
+
anchorKeys.add(key);
|
|
222
|
+
incarnationOnlyAnchors.push(processRef);
|
|
223
|
+
};
|
|
224
|
+
// children() validates its parent and then snapshots the process table; the
|
|
225
|
+
// parent can exit and its pid be reused in between, returning a stranger's
|
|
226
|
+
// children. Only trust a snapshot if the parent still holds its incarnation
|
|
227
|
+
// after it was taken.
|
|
228
|
+
const childrenOf = (parent: NativeProcess): NativeProcess[] => {
|
|
229
|
+
const children = parent.children();
|
|
230
|
+
return fromPid(parent.pid)?.incarnation === parent.incarnation ? children : [];
|
|
231
|
+
};
|
|
232
|
+
const seedAnchorDescendants = (anchor: NativeProcess): void => {
|
|
233
|
+
const pending = [...childrenOf(anchor)];
|
|
234
|
+
while (pending.length > 0) {
|
|
235
|
+
const child = pending.pop()!;
|
|
236
|
+
retainOwnedProcess(owned, child);
|
|
237
|
+
// The unique id is read from a bare pid, so bracket it with incarnation
|
|
238
|
+
// checks against this handle: a pid reused in between would otherwise
|
|
239
|
+
// seed an unrelated process (and its descendants) as owned.
|
|
240
|
+
if (fromPid(child.pid)?.incarnation === child.incarnation) {
|
|
241
|
+
const childId = uniqueIdentity(child.pid)?.uniqueId;
|
|
242
|
+
if (fromPid(child.pid)?.incarnation === child.incarnation) {
|
|
243
|
+
if (childId !== undefined) {
|
|
244
|
+
knownUniqueIds.add(childId);
|
|
245
|
+
retainedUniqueIds.add(childId);
|
|
246
|
+
} else {
|
|
247
|
+
// Its flavor-17 query is denied too: without a unique id the
|
|
248
|
+
// ancestry walk can never reach its subtree, so it must outlive
|
|
249
|
+
// this anchor's pruning as an incarnation-only anchor itself.
|
|
250
|
+
addIncarnationOnlyAnchor(child);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
pending.push(...childrenOf(child));
|
|
255
|
+
}
|
|
256
|
+
};
|
|
194
257
|
const track = (processRef: NativeProcess, signedUniqueId?: bigint): boolean => {
|
|
258
|
+
// The bare-pid unique-id query is only trusted if the same incarnation
|
|
259
|
+
// holds on both sides of it; otherwise a pid reused after the record was
|
|
260
|
+
// authenticated would seed the replacement's ancestry as owned.
|
|
261
|
+
let identity: { uniqueId: bigint; parentUniqueId: bigint } | undefined;
|
|
262
|
+
if (signedUniqueId === undefined) {
|
|
263
|
+
const queried = uniqueIdentity(processRef.pid);
|
|
264
|
+
const after = observe(processRef.pid);
|
|
265
|
+
if (
|
|
266
|
+
after.status === "absent" ||
|
|
267
|
+
(after.status === "present" && after.incarnation !== processRef.incarnation)
|
|
268
|
+
) {
|
|
269
|
+
// Confirmed exited or reused since authentication: retain nothing new.
|
|
270
|
+
return true;
|
|
271
|
+
}
|
|
272
|
+
// Present with the same incarnation keeps the queried id. An
|
|
273
|
+
// inconclusive lookup cannot vouch for it, so the record falls through
|
|
274
|
+
// as incarnation-only and is kept as an anchor rather than dropped.
|
|
275
|
+
identity = after.status === "present" ? queried : undefined;
|
|
276
|
+
}
|
|
277
|
+
const uniqueId = signedUniqueId ?? identity?.uniqueId;
|
|
278
|
+
if (uniqueId === undefined) {
|
|
279
|
+
// Incarnation-only record: the child has no unique id to anchor the
|
|
280
|
+
// ancestry graph. Keep it as an anchor whose live descendants poll()
|
|
281
|
+
// seeds by unique id, so their subtrees stay tracked after the child
|
|
282
|
+
// exits and they reparent (PPID discovery alone would lose them).
|
|
283
|
+
// Residual gap (accepted over #6085's exit 70): if the child forks a
|
|
284
|
+
// descendant that leaves the supervisor process group, then exits before
|
|
285
|
+
// this record is scanned, nothing links that descendant back to us. The
|
|
286
|
+
// ledger watcher scans on append to keep that window minimal.
|
|
287
|
+
retainOwnedProcess(owned, processRef);
|
|
288
|
+
addIncarnationOnlyAnchor(processRef);
|
|
289
|
+
seedAnchorDescendants(processRef);
|
|
290
|
+
return true;
|
|
291
|
+
}
|
|
292
|
+
knownUniqueIds.add(uniqueId);
|
|
293
|
+
retainedUniqueIds.add(uniqueId);
|
|
294
|
+
retainOwnedProcess(owned, processRef);
|
|
295
|
+
return true;
|
|
296
|
+
};
|
|
297
|
+
const trackGuardian = (processRef: NativeProcess, signedUniqueId?: bigint): boolean => {
|
|
195
298
|
const identity = signedUniqueId === undefined ? uniqueIdentity(processRef.pid) : undefined;
|
|
196
299
|
const uniqueId = signedUniqueId ?? identity?.uniqueId;
|
|
300
|
+
// Guardian registration is strict: must have a unique id
|
|
197
301
|
if (uniqueId === undefined) return false;
|
|
198
302
|
knownUniqueIds.add(uniqueId);
|
|
199
303
|
retainedUniqueIds.add(uniqueId);
|
|
@@ -205,7 +309,19 @@ function createDarwinAncestryTracker(owned: Map<string, NativeProcess>): DarwinA
|
|
|
205
309
|
knownUniqueIds.add(uniqueId);
|
|
206
310
|
},
|
|
207
311
|
track,
|
|
312
|
+
trackGuardian,
|
|
208
313
|
poll() {
|
|
314
|
+
// Re-seed live anchors, then drop exited ones: their descendants were
|
|
315
|
+
// seeded by unique id while the anchor was alive, and ancestry now
|
|
316
|
+
// carries them, so re-walking a dead anchor only costs time.
|
|
317
|
+
for (let index = incarnationOnlyAnchors.length - 1; index >= 0; index--) {
|
|
318
|
+
const anchor = incarnationOnlyAnchors[index]!;
|
|
319
|
+
seedAnchorDescendants(anchor);
|
|
320
|
+
if (fromPid(anchor.pid)?.incarnation !== anchor.incarnation) {
|
|
321
|
+
incarnationOnlyAnchors.splice(index, 1);
|
|
322
|
+
anchorKeys.delete(`${anchor.pid}:${anchor.incarnation}`);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
209
325
|
const capacity = proc.symbols.proc_listallpids(null, 0);
|
|
210
326
|
if (capacity <= 0) return false;
|
|
211
327
|
const pids = new Int32Array(capacity + 64);
|
|
@@ -225,10 +341,10 @@ function createDarwinAncestryTracker(owned: Map<string, NativeProcess>): DarwinA
|
|
|
225
341
|
extendOwnedDarwinAncestry(knownUniqueIds, identities);
|
|
226
342
|
for (const [pid, listed] of identities) {
|
|
227
343
|
if (!knownUniqueIds.has(listed.uniqueId) || retainedUniqueIds.has(listed.uniqueId)) continue;
|
|
228
|
-
const before =
|
|
344
|
+
const before = fromPid(pid);
|
|
229
345
|
if (!before) continue;
|
|
230
346
|
const identity = uniqueIdentity(pid);
|
|
231
|
-
const after =
|
|
347
|
+
const after = fromPid(pid);
|
|
232
348
|
if (identity?.uniqueId === listed.uniqueId && after?.incarnation === before.incarnation) {
|
|
233
349
|
retainOwnedProcess(owned, after);
|
|
234
350
|
retainedUniqueIds.add(listed.uniqueId);
|
|
@@ -275,7 +391,7 @@ export async function runBashShellGuardian(): Promise<void> {
|
|
|
275
391
|
if (darwinTracker) {
|
|
276
392
|
const { Process } = require("@gajae-code/natives") as NativeProcessBindings;
|
|
277
393
|
const guardian = Process.fromPid(process.pid);
|
|
278
|
-
if (!guardian || !darwinTracker.
|
|
394
|
+
if (!guardian || !darwinTracker.trackGuardian(guardian)) {
|
|
279
395
|
darwinTracker.close();
|
|
280
396
|
await ledger.close();
|
|
281
397
|
await fs.rm(ownershipFilePath, { force: true });
|
|
@@ -306,6 +422,8 @@ export async function runBashShellGuardian(): Promise<void> {
|
|
|
306
422
|
input.once("close", () => supervisor.stdin.end());
|
|
307
423
|
let cleaning: Promise<void> | undefined;
|
|
308
424
|
let ledgerBuffer = "";
|
|
425
|
+
let pendingLedgerLines: string[] = [];
|
|
426
|
+
const pendingAttempts = new Map<string, number>();
|
|
309
427
|
let ownershipScan = Promise.resolve(true);
|
|
310
428
|
const scanOwnership = (): Promise<boolean> => {
|
|
311
429
|
ownershipScan = ownershipScan.then(async previousOk => {
|
|
@@ -317,11 +435,23 @@ export async function runBashShellGuardian(): Promise<void> {
|
|
|
317
435
|
return false;
|
|
318
436
|
}
|
|
319
437
|
ledgerBuffer += content;
|
|
320
|
-
const
|
|
321
|
-
ledgerBuffer =
|
|
438
|
+
const fresh = ledgerBuffer.split("\n");
|
|
439
|
+
ledgerBuffer = fresh.pop() ?? "";
|
|
440
|
+
const lines = [...pendingLedgerLines, ...fresh];
|
|
441
|
+
pendingLedgerLines = [];
|
|
322
442
|
for (const line of lines) {
|
|
323
443
|
if (!line) continue;
|
|
324
444
|
const owned = authenticateOwnershipRecord(line, ledgerToken);
|
|
445
|
+
if (owned?.pending) {
|
|
446
|
+
// Inconclusive lookup: retry next scan, bounded so a pid stuck in an
|
|
447
|
+
// unknown state cannot grow the queue or starve cleanup forever.
|
|
448
|
+
const attempts = (pendingAttempts.get(line) ?? 0) + 1;
|
|
449
|
+
if (attempts > MAX_PENDING_LEDGER_ATTEMPTS) return false;
|
|
450
|
+
pendingAttempts.set(line, attempts);
|
|
451
|
+
pendingLedgerLines.push(line);
|
|
452
|
+
} else {
|
|
453
|
+
pendingAttempts.delete(line);
|
|
454
|
+
}
|
|
325
455
|
if (owned?.darwinUniqueId && darwinTracker) darwinTracker.seed(owned.darwinUniqueId);
|
|
326
456
|
if (owned?.processRef && darwinTracker && !darwinTracker.track(owned.processRef, owned.darwinUniqueId))
|
|
327
457
|
return false;
|
|
@@ -335,28 +465,48 @@ export async function runBashShellGuardian(): Promise<void> {
|
|
|
335
465
|
return ownershipScan;
|
|
336
466
|
};
|
|
337
467
|
let periodicScanActive = false;
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
468
|
+
let periodicScanRequested = false;
|
|
469
|
+
const runPeriodicScan = (): void => {
|
|
470
|
+
if (periodicScanActive) {
|
|
471
|
+
periodicScanRequested = true;
|
|
472
|
+
return;
|
|
473
|
+
}
|
|
474
|
+
periodicScanActive = true;
|
|
475
|
+
void scanOwnership()
|
|
476
|
+
.then(ok => {
|
|
477
|
+
if (ok || supervisor.exitCode !== null || supervisor.signalCode !== null) return;
|
|
478
|
+
if (supervisor.pid) {
|
|
479
|
+
try {
|
|
480
|
+
process.kill(-supervisor.pid, "SIGKILL");
|
|
481
|
+
} catch {}
|
|
482
|
+
}
|
|
483
|
+
supervisor.kill("SIGKILL");
|
|
484
|
+
})
|
|
485
|
+
.finally(() => {
|
|
486
|
+
periodicScanActive = false;
|
|
487
|
+
if (periodicScanRequested && !cleaning) {
|
|
488
|
+
periodicScanRequested = false;
|
|
489
|
+
runPeriodicScan();
|
|
490
|
+
}
|
|
491
|
+
});
|
|
492
|
+
};
|
|
493
|
+
const ownershipScanTimer = darwinTracker ? setInterval(runPeriodicScan, 100) : undefined;
|
|
494
|
+
// Scan as soon as the supervisor appends a ledger record instead of waiting
|
|
495
|
+
// for the next tick. An incarnation-only record's descendants are only
|
|
496
|
+
// reachable while that child is alive, so the anchor must be taken promptly.
|
|
497
|
+
let ledgerWatcher: fsSync.FSWatcher | undefined;
|
|
498
|
+
if (darwinTracker) {
|
|
499
|
+
try {
|
|
500
|
+
ledgerWatcher = fsSync.watch(ownershipFilePath, runPeriodicScan);
|
|
501
|
+
ledgerWatcher.on("error", () => {});
|
|
502
|
+
} catch {
|
|
503
|
+
ledgerWatcher = undefined;
|
|
504
|
+
}
|
|
505
|
+
}
|
|
357
506
|
const cleanup = (): Promise<void> => {
|
|
358
507
|
cleaning ??= (async () => {
|
|
359
508
|
if (ownershipScanTimer) clearInterval(ownershipScanTimer);
|
|
509
|
+
ledgerWatcher?.close();
|
|
360
510
|
let trackingOk = await scanOwnership();
|
|
361
511
|
if (supervisor.exitCode === null && supervisor.signalCode === null) {
|
|
362
512
|
if (process.platform !== "win32" && supervisor.pid) {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { dlopen, FFIType, ptr } from "bun:ffi";
|
|
2
2
|
import * as childProcess from "node:child_process";
|
|
3
3
|
import { createHmac } from "node:crypto";
|
|
4
|
+
import type { EventEmitter } from "node:events";
|
|
4
5
|
import * as fs from "node:fs/promises";
|
|
5
6
|
import * as os from "node:os";
|
|
6
7
|
import * as path from "node:path";
|
|
@@ -121,6 +122,63 @@ async function reapLinuxAdoptedZombies(excludedPid: number | undefined): Promise
|
|
|
121
122
|
}
|
|
122
123
|
}
|
|
123
124
|
|
|
125
|
+
/**
|
|
126
|
+
* Reap adopted zombies only when the kernel reports a child state change.
|
|
127
|
+
*
|
|
128
|
+
* As the Linux child subreaper, the supervisor inherits orphaned descendants
|
|
129
|
+
* and must reap them, but a full `/proc` sweep is expensive. Polling it on a
|
|
130
|
+
* short interval kept every idle supervisor busy (issue #5972), so sweeps are
|
|
131
|
+
* driven by SIGCHLD instead. At most one sweep runs at a time, and a SIGCHLD that
|
|
132
|
+
* arrives mid-sweep schedules exactly one follow-up so a zombie created after
|
|
133
|
+
* its `/proc` entry was passed is never missed. Returns the unsubscribe hook.
|
|
134
|
+
*/
|
|
135
|
+
export function startLinuxAdoptedZombieReaper(
|
|
136
|
+
excludedPid: number | undefined,
|
|
137
|
+
reap: (excludedPid: number | undefined) => Promise<void> = reapLinuxAdoptedZombies,
|
|
138
|
+
signals: Pick<EventEmitter, "on" | "off"> = process,
|
|
139
|
+
): () => void {
|
|
140
|
+
if (process.platform !== "linux") return () => undefined;
|
|
141
|
+
let running = false;
|
|
142
|
+
let rerun = false;
|
|
143
|
+
let stopped = false;
|
|
144
|
+
let sweepPending = false;
|
|
145
|
+
const sweep = (): void => {
|
|
146
|
+
if (stopped) return;
|
|
147
|
+
if (running) {
|
|
148
|
+
rerun = true;
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
if (sweepPending) return;
|
|
152
|
+
sweepPending = true;
|
|
153
|
+
running = true;
|
|
154
|
+
void reap(excludedPid)
|
|
155
|
+
.catch(() => undefined)
|
|
156
|
+
.finally(() => {
|
|
157
|
+
running = false;
|
|
158
|
+
sweepPending = false;
|
|
159
|
+
if (!rerun || stopped) return;
|
|
160
|
+
rerun = false;
|
|
161
|
+
// Defer recursive call to avoid stack overflow
|
|
162
|
+
setImmediate(() => {
|
|
163
|
+
if (!stopped && !running) sweep();
|
|
164
|
+
});
|
|
165
|
+
});
|
|
166
|
+
};
|
|
167
|
+
try {
|
|
168
|
+
signals.on("SIGCHLD", sweep);
|
|
169
|
+
} catch {
|
|
170
|
+
// Signal handler installation may fail in some environments
|
|
171
|
+
}
|
|
172
|
+
return () => {
|
|
173
|
+
stopped = true;
|
|
174
|
+
try {
|
|
175
|
+
signals.off("SIGCHLD", sweep);
|
|
176
|
+
} catch {
|
|
177
|
+
// Signal handler removal may fail in some environments
|
|
178
|
+
}
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
|
|
124
182
|
function signalFromExit(code: number | null, signal: NodeJS.Signals | null): NodeJS.Signals | null {
|
|
125
183
|
if (signal) return signal;
|
|
126
184
|
if (code === null || code <= 128) return null;
|
|
@@ -210,8 +268,7 @@ export async function runBashShellSupervisor(): Promise<void> {
|
|
|
210
268
|
stdio: ["pipe", "pipe", "pipe"],
|
|
211
269
|
windowsHide: true,
|
|
212
270
|
});
|
|
213
|
-
const
|
|
214
|
-
zombieReaper.unref();
|
|
271
|
+
const stopZombieReaper = startLinuxAdoptedZombieReaper(worker.pid);
|
|
215
272
|
const pendingRuns: number[] = [];
|
|
216
273
|
let terminating = false;
|
|
217
274
|
let protocolToken: string | undefined;
|
|
@@ -315,7 +372,7 @@ export async function runBashShellSupervisor(): Promise<void> {
|
|
|
315
372
|
);
|
|
316
373
|
});
|
|
317
374
|
worker.once("exit", async (code, signal) => {
|
|
318
|
-
|
|
375
|
+
stopZombieReaper();
|
|
319
376
|
const resultSignal = signalFromExit(code, signal);
|
|
320
377
|
if (code === 0 && resultSignal === null) {
|
|
321
378
|
await reapLinuxSubreaperChildren();
|
package/src/hashline/execute.ts
CHANGED
|
@@ -11,12 +11,12 @@ import { assertEditableFileContent } from "../tools/auto-generated-guard";
|
|
|
11
11
|
import { invalidateFsScanAfterWrite } from "../tools/fs-cache-invalidation";
|
|
12
12
|
import { outputMeta } from "../tools/output-meta";
|
|
13
13
|
import { enforcePlanModeWrite, resolvePlanPath } from "../tools/plan-mode-guard";
|
|
14
|
-
import { HashlineMismatchError } from "./anchors";
|
|
14
|
+
import { formatFullAnchorRequirement, HashlineMismatchError } from "./anchors";
|
|
15
15
|
import { applyHashlineEdits, type HashlineApplyResult } from "./apply";
|
|
16
16
|
import { buildCompactHashlineDiffPreview } from "./diff-preview";
|
|
17
17
|
import { computeLineHash, HL_BODY_SEP } from "./hash";
|
|
18
18
|
import { type HashlineInputSection, splitHashlineInputs } from "./input";
|
|
19
|
-
import { HashlineMissingHashError, parseHashlineWithWarnings } from "./parser";
|
|
19
|
+
import { HashlineMissingHashError, HashlineMissingLineError, parseHashlineWithWarnings } from "./parser";
|
|
20
20
|
import { tryRecoverHashlineWithCache } from "./recovery";
|
|
21
21
|
import type {
|
|
22
22
|
ExecuteHashlineSingleOptions,
|
|
@@ -88,6 +88,17 @@ function formatMissingHashAnchors(fileLines: string[], start: number, end: numbe
|
|
|
88
88
|
return [...head, "...", format(end)];
|
|
89
89
|
}
|
|
90
90
|
|
|
91
|
+
function formatMissingHashRetryHeader(
|
|
92
|
+
fileLines: string[],
|
|
93
|
+
start: number,
|
|
94
|
+
end: number,
|
|
95
|
+
opSigil: HashlineMissingHashError["opSigil"],
|
|
96
|
+
): string {
|
|
97
|
+
const format = (line: number): string => `${line}${computeLineHash(line, fileLines[line - 1] ?? "")}`;
|
|
98
|
+
const anchorRange = start === end ? format(start) : `${format(start)}..${format(end)}`;
|
|
99
|
+
return `${opSigil}${anchorRange}`;
|
|
100
|
+
}
|
|
101
|
+
|
|
91
102
|
/**
|
|
92
103
|
* Parse an edit section. When an op names lines by number only, answer with
|
|
93
104
|
* the current full anchors for those lines so the model can retry without
|
|
@@ -101,7 +112,43 @@ async function parseHashlineSection(
|
|
|
101
112
|
try {
|
|
102
113
|
return parseHashlineWithWarnings(diff);
|
|
103
114
|
} catch (err) {
|
|
115
|
+
if (err instanceof HashlineMissingLineError) {
|
|
116
|
+
// When part of a range has a missing hash, suppress the "Did you mean" suggestion
|
|
117
|
+
// and require the full range to be re-supplied.
|
|
118
|
+
if (err.rangeRaw) {
|
|
119
|
+
throw new Error(
|
|
120
|
+
`Anchor "${err.hash}" lacks its line number. The range ${err.rangeRaw.start}..${err.rangeRaw.end} is incomplete. ` +
|
|
121
|
+
`Expected ${formatFullAnchorRequirement(err.hash)} Re-read the target to get the full range. ` +
|
|
122
|
+
`The edit was NOT applied.`,
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
const source = await readHashlineFile(absolutePath, pathText);
|
|
126
|
+
const fileLines = source.exists ? normalizeToLF(stripBom(source.rawContent).text).split("\n") : [];
|
|
127
|
+
let matchCount = 0;
|
|
128
|
+
let matchLine: number | undefined;
|
|
129
|
+
for (let index = 0; index < fileLines.length; index++) {
|
|
130
|
+
if (computeLineHash(index + 1, fileLines[index] ?? "") !== err.hash) continue;
|
|
131
|
+
matchCount++;
|
|
132
|
+
matchLine = index + 1;
|
|
133
|
+
}
|
|
134
|
+
if (matchCount === 1 && matchLine !== undefined) {
|
|
135
|
+
const suggestion = new HashlineMissingLineError(err.hash, matchLine, err.opSigil);
|
|
136
|
+
throw new Error(`${suggestion.message} The edit was NOT applied.`);
|
|
137
|
+
}
|
|
138
|
+
throw new Error(
|
|
139
|
+
`${err.message} Expected ${formatFullAnchorRequirement(err.hash)} ` +
|
|
140
|
+
`The hash matches ${matchCount} lines; re-read the target. The edit was NOT applied.`,
|
|
141
|
+
);
|
|
142
|
+
}
|
|
104
143
|
if (!(err instanceof HashlineMissingHashError)) throw err;
|
|
144
|
+
// When part of a range has a missing hash, suppress the copy-ready suggestion
|
|
145
|
+
// and require re-reading to get the full range.
|
|
146
|
+
if (err.rangeRaw) {
|
|
147
|
+
throw new Error(
|
|
148
|
+
`${err.message} The anchors are missing from the range ${err.rangeRaw.start}..${err.rangeRaw.end}. ` +
|
|
149
|
+
`Re-read the target to get the full range with hashes. The edit was NOT applied.`,
|
|
150
|
+
);
|
|
151
|
+
}
|
|
105
152
|
const source = await readHashlineFile(absolutePath, pathText);
|
|
106
153
|
if (!source.exists) throw err;
|
|
107
154
|
const fileLines = normalizeToLF(stripBom(source.rawContent).text).split("\n");
|
|
@@ -112,8 +159,9 @@ async function parseHashlineSection(
|
|
|
112
159
|
);
|
|
113
160
|
}
|
|
114
161
|
const anchors = formatMissingHashAnchors(fileLines, start, end);
|
|
162
|
+
const retryHeader = formatMissingHashRetryHeader(fileLines, start, end, err.opSigil);
|
|
115
163
|
throw new Error(
|
|
116
|
-
`${err.message}\nThe edit was NOT applied. Current anchors for ${pathText}:\n${anchors.join("\n")}`,
|
|
164
|
+
`${err.message}\nUse ${retryHeader}\nThe edit was NOT applied. Current anchors for ${pathText}:\n${anchors.join("\n")}`,
|
|
117
165
|
);
|
|
118
166
|
}
|
|
119
167
|
}
|
package/src/hashline/parser.ts
CHANGED
|
@@ -20,35 +20,59 @@ import type { Anchor, HashlineCursor, HashlineEdit } from "./types";
|
|
|
20
20
|
const LID_CAPTURE_RE = new RegExp(`^\\s*[>+\\-*]*\\s*${HL_HASH_CAPTURE_RE_RAW}(?:\\|.*)?\\s*$`);
|
|
21
21
|
const regexEscape = (str: string): string => str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
22
22
|
|
|
23
|
-
|
|
23
|
+
// Dots in copied text are literal unless followed by a possible range endpoint.
|
|
24
|
+
// Ambiguous copied ranges still require a re-read instead of a one-line retry.
|
|
25
|
+
const BARE_LINE_REF_RE =
|
|
26
|
+
/^\s*[>+\-*]*\s*([1-9]\d*)(?:\s*(?:-|\.\.)\s*([1-9]\d*))?(?:\|(?:(?!\.\.\s*[>+\-*]*\s*(?:[1-9]\d*|[a-z]{2}(?:\||\s*$))).)*)?\s*$/;
|
|
27
|
+
const HASH_ONLY_REF_RE = /^\s*[>+\-*]*\s*([a-z]{2})(?:\|.*)?\s*$/;
|
|
28
|
+
type HashlineOpSigil = typeof HL_OP_INSERT_BEFORE | typeof HL_OP_INSERT_AFTER | typeof HL_OP_REPLACE;
|
|
24
29
|
|
|
25
30
|
/**
|
|
26
|
-
* An op referenced lines by number alone
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
* extra read.
|
|
31
|
+
* An op referenced lines by number alone. The edit is never applied on a line
|
|
32
|
+
* number alone; the executor answers with the current full anchors so the model
|
|
33
|
+
* can retry without another read.
|
|
30
34
|
*/
|
|
31
35
|
export class HashlineMissingHashError extends Error {
|
|
32
36
|
constructor(
|
|
33
37
|
message: string,
|
|
34
38
|
readonly lines: { start: number; end: number },
|
|
39
|
+
readonly opSigil: HashlineOpSigil,
|
|
40
|
+
readonly rangeRaw?: { start: string; end: string },
|
|
35
41
|
) {
|
|
36
42
|
super(message);
|
|
37
43
|
this.name = "HashlineMissingHashError";
|
|
38
44
|
}
|
|
39
45
|
}
|
|
40
46
|
|
|
41
|
-
|
|
47
|
+
export class HashlineMissingLineError extends Error {
|
|
48
|
+
constructor(
|
|
49
|
+
readonly hash: string,
|
|
50
|
+
readonly lineNum: number | undefined,
|
|
51
|
+
readonly opSigil: HashlineOpSigil,
|
|
52
|
+
readonly rangeRaw?: { start: string; end: string },
|
|
53
|
+
) {
|
|
54
|
+
super(
|
|
55
|
+
lineNum === undefined
|
|
56
|
+
? `Anchor "${hash}" lacks its line number.`
|
|
57
|
+
: `Anchor "${hash}" lacks its line number. Did you mean ${opSigil}${lineNum}${hash}?`,
|
|
58
|
+
);
|
|
59
|
+
this.name = "HashlineMissingLineError";
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function parseLid(raw: string, lineNum: number, opSigil: HashlineOpSigil): Anchor {
|
|
42
64
|
const match = LID_CAPTURE_RE.exec(raw);
|
|
43
65
|
if (!match) {
|
|
66
|
+
const hashOnly = HASH_ONLY_REF_RE.exec(raw);
|
|
67
|
+
if (hashOnly) throw new HashlineMissingLineError(hashOnly[1], undefined, opSigil);
|
|
44
68
|
const bare = BARE_LINE_REF_RE.exec(raw);
|
|
45
69
|
if (bare) {
|
|
46
70
|
const start = Number.parseInt(bare[1], 10);
|
|
47
71
|
const end = bare[2] === undefined ? start : Number.parseInt(bare[2], 10);
|
|
48
72
|
throw new HashlineMissingHashError(
|
|
49
|
-
`line ${lineNum}: anchor ${JSON.stringify(raw.trim())} is missing its hash
|
|
50
|
-
`use the full anchor such as ${describeAnchorExamples(String(start))}.`,
|
|
73
|
+
`line ${lineNum}: anchor ${JSON.stringify(raw.trim())} is missing its hash.`,
|
|
51
74
|
{ start: Math.min(start, end), end: Math.max(start, end) },
|
|
75
|
+
opSigil,
|
|
52
76
|
);
|
|
53
77
|
}
|
|
54
78
|
throw new Error(
|
|
@@ -64,12 +88,12 @@ interface ParsedRange {
|
|
|
64
88
|
end: Anchor;
|
|
65
89
|
}
|
|
66
90
|
|
|
67
|
-
function parseRange(raw: string, lineNum: number): ParsedRange {
|
|
91
|
+
function parseRange(raw: string, lineNum: number, opSigil: HashlineOpSigil): ParsedRange {
|
|
68
92
|
// Reject an all-numeric range (`16..18`, `16-18`) as a whole so the error
|
|
69
93
|
// carries the full span rather than just its first endpoint.
|
|
70
|
-
if (BARE_LINE_REF_RE.test(raw)) parseLid(raw, lineNum);
|
|
94
|
+
if (BARE_LINE_REF_RE.test(raw)) parseLid(raw, lineNum, opSigil);
|
|
71
95
|
if (!raw.includes("..")) {
|
|
72
|
-
const start = parseLid(raw, lineNum);
|
|
96
|
+
const start = parseLid(raw, lineNum, opSigil);
|
|
73
97
|
return { start, end: { ...start } };
|
|
74
98
|
}
|
|
75
99
|
const [startRaw, endRaw, extra] = raw.split("..");
|
|
@@ -79,8 +103,30 @@ function parseRange(raw: string, lineNum: number): ParsedRange {
|
|
|
79
103
|
`For a one-line edit, repeat the same anchor on both sides.`,
|
|
80
104
|
);
|
|
81
105
|
}
|
|
82
|
-
|
|
83
|
-
|
|
106
|
+
let start: Anchor;
|
|
107
|
+
try {
|
|
108
|
+
start = parseLid(startRaw, lineNum, opSigil);
|
|
109
|
+
} catch (err) {
|
|
110
|
+
if (err instanceof HashlineMissingHashError) {
|
|
111
|
+
throw new HashlineMissingHashError(err.message, err.lines, err.opSigil, { start: startRaw, end: endRaw });
|
|
112
|
+
}
|
|
113
|
+
if (err instanceof HashlineMissingLineError) {
|
|
114
|
+
throw new HashlineMissingLineError(err.hash, err.lineNum, err.opSigil, { start: startRaw, end: endRaw });
|
|
115
|
+
}
|
|
116
|
+
throw err;
|
|
117
|
+
}
|
|
118
|
+
let end: Anchor;
|
|
119
|
+
try {
|
|
120
|
+
end = parseLid(endRaw, lineNum, opSigil);
|
|
121
|
+
} catch (err) {
|
|
122
|
+
if (err instanceof HashlineMissingHashError) {
|
|
123
|
+
throw new HashlineMissingHashError(err.message, err.lines, err.opSigil, { start: startRaw, end: endRaw });
|
|
124
|
+
}
|
|
125
|
+
if (err instanceof HashlineMissingLineError) {
|
|
126
|
+
throw new HashlineMissingLineError(err.hash, err.lineNum, err.opSigil, { start: startRaw, end: endRaw });
|
|
127
|
+
}
|
|
128
|
+
throw err;
|
|
129
|
+
}
|
|
84
130
|
if (end.line < start.line) {
|
|
85
131
|
throw new Error(`line ${lineNum}: range ${startRaw}..${endRaw} ends before it starts.`);
|
|
86
132
|
}
|
|
@@ -104,7 +150,8 @@ function parseInsertTarget(raw: string, lineNum: number, kind: "before" | "after
|
|
|
104
150
|
if (raw === "BOF") return { kind: "bof" };
|
|
105
151
|
if (raw === "EOF") return { kind: "eof" };
|
|
106
152
|
const cursorKind = kind === "before" ? "before_anchor" : "after_anchor";
|
|
107
|
-
|
|
153
|
+
const opSigil = kind === "before" ? HL_OP_INSERT_BEFORE : HL_OP_INSERT_AFTER;
|
|
154
|
+
return { kind: cursorKind, anchor: parseLid(raw, lineNum, opSigil) };
|
|
108
155
|
}
|
|
109
156
|
|
|
110
157
|
/**
|
|
@@ -137,7 +184,7 @@ const INSERT_BEFORE_OP_RE = new RegExp(
|
|
|
137
184
|
const INSERT_AFTER_OP_RE = new RegExp(
|
|
138
185
|
`^${regexEscape(HL_OP_INSERT_AFTER)}\\s*([^|\\s]+)(?:${HL_BODY_SEP_RE_RAW}(.*))?\\s*$`,
|
|
139
186
|
);
|
|
140
|
-
const REPLACE_OP_RE = new RegExp(`^${regexEscape(HL_OP_REPLACE)}\\s*([^\\s+<\\-=]\\S*)\\s*$`);
|
|
187
|
+
const REPLACE_OP_RE = new RegExp(`^${regexEscape(HL_OP_REPLACE)}\\s*([^\\s+<\\-=]\\S*(?:\\|.*)?)\\s*$`);
|
|
141
188
|
|
|
142
189
|
function isEnvelopeOrAbortMarkerLine(line: string): boolean {
|
|
143
190
|
const trimmed = line.trimEnd();
|
|
@@ -240,7 +287,7 @@ export function parseHashlineWithWarnings(diff: string): { edits: HashlineEdit[]
|
|
|
240
287
|
|
|
241
288
|
const replaceMatch = REPLACE_OP_RE.exec(line);
|
|
242
289
|
if (replaceMatch) {
|
|
243
|
-
const range = parseRange(replaceMatch[1], lineNum);
|
|
290
|
+
const range = parseRange(replaceMatch[1], lineNum, HL_OP_REPLACE);
|
|
244
291
|
const { payload, nextIndex } = collectPayload(lines, i + 1, lineNum, false);
|
|
245
292
|
if (payload.length > 0) {
|
|
246
293
|
for (const text of payload) {
|