@gajae-code/coding-agent 0.18.1 → 0.18.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.
- package/CHANGELOG.md +60 -0
- package/dist/types/cli/stats-cli.d.ts +0 -5
- package/dist/types/config/autorouting-tier-map.d.ts +29 -2
- package/dist/types/config/model-registry.d.ts +16 -1
- package/dist/types/exa/mcp-client.d.ts +1 -5
- package/dist/types/exec/bash-shell-guardian.d.ts +26 -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/lifecycle.d.ts +6 -1
- package/dist/types/sdk/broker/recovery-backoff.d.ts +62 -0
- package/dist/types/sdk/broker/runtime.d.ts +20 -0
- 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/host/session-runtime.d.ts +7 -0
- package/dist/types/sdk/prompt-watchdog.d.ts +1 -1
- package/dist/types/tools/bash.d.ts +1 -0
- package/package.json +7 -7
- package/src/cli/stats-cli.ts +4 -36
- package/src/cli-main.ts +4 -8
- package/src/cli.ts +4 -8
- package/src/commands/commit.ts +2 -2
- package/src/commands/sdk.ts +1 -2
- package/src/config/autorouting-tier-map.ts +20 -2
- package/src/config/model-preset-registry.ts +17 -2
- package/src/config/model-profile-activation.ts +3 -1
- package/src/config/model-profiles.ts +11 -3
- package/src/config/model-registry.ts +45 -17
- package/src/exa/mcp-client.ts +41 -8
- package/src/exec/bash-shell-guardian.ts +184 -34
- package/src/hashline/execute.ts +51 -3
- package/src/hashline/parser.ts +63 -16
- package/src/internal-urls/docs-index.generated.ts +22 -22
- package/src/modes/acp/acp-agent.ts +39 -4
- package/src/modes/controllers/ssh-command-controller.ts +3 -1
- package/src/sdk/broker/lifecycle.ts +408 -218
- package/src/sdk/broker/recovery-backoff.ts +135 -0
- package/src/sdk/broker/runtime.ts +83 -0
- package/src/sdk/broker/startup-budget.ts +6 -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/host/session-runtime.ts +99 -3
- package/src/sdk/prompt-watchdog.ts +17 -4
- package/src/session/agent-session.ts +14 -2
- package/src/session/internal/managed-session-storage.ts +23 -0
- package/src/tools/bash.ts +39 -0
- 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
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) {
|