@letrquan/book 0.2.0 → 0.4.0-beta.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +3771 -40
- package/README.md +135 -1311
- package/dist/additional-roots-VAWRQ625.js +32 -0
- package/dist/{app-MOXXWEOY.js → app-J7BVPSUH.js} +9648 -5509
- package/dist/app-J7BVPSUH.js.map +1 -0
- package/dist/chunk-2AAOF5B5.js +28 -0
- package/dist/chunk-2AAOF5B5.js.map +1 -0
- package/dist/chunk-32WBCUAN.js +660 -0
- package/dist/chunk-32WBCUAN.js.map +1 -0
- package/dist/chunk-4DKHLZRI.js +120 -0
- package/dist/chunk-4DKHLZRI.js.map +1 -0
- package/dist/{chunk-CAGOB2N4.js → chunk-62EEAUID.js} +6 -4
- package/dist/chunk-62EEAUID.js.map +1 -0
- package/dist/chunk-AHC7OU75.js +1124 -0
- package/dist/chunk-AHC7OU75.js.map +1 -0
- package/dist/{chunk-E75IPST5.js → chunk-AJZWLKLQ.js} +73 -70
- package/dist/chunk-AJZWLKLQ.js.map +1 -0
- package/dist/chunk-B5FC3MYZ.js +395 -0
- package/dist/chunk-B5FC3MYZ.js.map +1 -0
- package/dist/{chunk-KWHQ2XXL.js → chunk-DNFQTSMJ.js} +55 -13
- package/dist/chunk-DNFQTSMJ.js.map +1 -0
- package/dist/{chunk-J7MIZ5CA.js → chunk-E5WLQSKA.js} +719 -89
- package/dist/chunk-E5WLQSKA.js.map +1 -0
- package/dist/chunk-ECLC4CIJ.js +1181 -0
- package/dist/chunk-ECLC4CIJ.js.map +1 -0
- package/dist/chunk-EEX6QNXV.js +127 -0
- package/dist/chunk-EEX6QNXV.js.map +1 -0
- package/dist/chunk-EFXVSQ5J.js +931 -0
- package/dist/chunk-EFXVSQ5J.js.map +1 -0
- package/dist/{chunk-V4BOYR52.js → chunk-ENLB7NYV.js} +1304 -326
- package/dist/chunk-ENLB7NYV.js.map +1 -0
- package/dist/chunk-FPR7SKIE.js +6926 -0
- package/dist/chunk-FPR7SKIE.js.map +1 -0
- package/dist/{chunk-DGMQEI6Y.js → chunk-HFB74CLM.js} +30 -9
- package/dist/chunk-HFB74CLM.js.map +1 -0
- package/dist/{chunk-5RDRAO4B.js → chunk-HOZXKOE3.js} +37 -1
- package/dist/chunk-HOZXKOE3.js.map +1 -0
- package/dist/chunk-IDYWN37T.js +36 -0
- package/dist/chunk-IDYWN37T.js.map +1 -0
- package/dist/chunk-J4F2QLM2.js +273 -0
- package/dist/chunk-J4F2QLM2.js.map +1 -0
- package/dist/chunk-JGF5HLSI.js +1065 -0
- package/dist/chunk-JGF5HLSI.js.map +1 -0
- package/dist/{chunk-XIFFTATU.js → chunk-KDNWMKXV.js} +7166 -4237
- package/dist/chunk-KDNWMKXV.js.map +1 -0
- package/dist/chunk-KXAYUIKP.js +90 -0
- package/dist/chunk-KXAYUIKP.js.map +1 -0
- package/dist/{chunk-3U5IBM24.js → chunk-MEOB2DZA.js} +12 -2
- package/dist/chunk-MEOB2DZA.js.map +1 -0
- package/dist/{chunk-WKBO5R4O.js → chunk-MV5PQNIA.js} +2 -2
- package/dist/chunk-MV5PQNIA.js.map +1 -0
- package/dist/chunk-N2G4Q7ME.js +2672 -0
- package/dist/chunk-N2G4Q7ME.js.map +1 -0
- package/dist/chunk-OJBMDM5H.js +299 -0
- package/dist/chunk-OJBMDM5H.js.map +1 -0
- package/dist/{chunk-URLQAXTR.js → chunk-PTDC7ZUP.js} +2 -2
- package/dist/chunk-PTDC7ZUP.js.map +1 -0
- package/dist/{chunk-574C6MQR.js → chunk-REASC2QG.js} +173 -64
- package/dist/chunk-REASC2QG.js.map +1 -0
- package/dist/{chunk-7N4J557C.js → chunk-RT5SATF7.js} +1 -1
- package/dist/chunk-RT5SATF7.js.map +1 -0
- package/dist/chunk-SBTU5BEC.js +114 -0
- package/dist/chunk-SBTU5BEC.js.map +1 -0
- package/dist/{chunk-5KLVH3PY.js → chunk-SWPDKLNM.js} +5 -3
- package/dist/chunk-SWPDKLNM.js.map +1 -0
- package/dist/chunk-UCFJ43OS.js +520 -0
- package/dist/chunk-UCFJ43OS.js.map +1 -0
- package/dist/{chunk-VZQXX3WW.js → chunk-W7SLYS3M.js} +80 -16
- package/dist/chunk-W7SLYS3M.js.map +1 -0
- package/dist/{chunk-ANSJPYGI.js → chunk-WV3O5PGX.js} +3 -3
- package/dist/chunk-WV3O5PGX.js.map +1 -0
- package/dist/chunk-XHAO3K7I.js +18 -0
- package/dist/chunk-XHAO3K7I.js.map +1 -0
- package/dist/chunk-XJRJVXJR.js +1050 -0
- package/dist/chunk-XJRJVXJR.js.map +1 -0
- package/dist/chunk-YIL6HTJX.js +663 -0
- package/dist/chunk-YIL6HTJX.js.map +1 -0
- package/dist/{chunk-24AXE6SP.js → chunk-ZPSZZUWF.js} +1 -1
- package/dist/chunk-ZPSZZUWF.js.map +1 -0
- package/dist/{command-approvals-6Y57VCWZ.js → command-approvals-ZRY66LCW.js} +7 -5
- package/dist/context-C3RAP6K5.js +41 -0
- package/dist/{hook-approvals-CTUFKEJT.js → hook-approvals-DPQD5H65.js} +6 -5
- package/dist/index.js +947 -203
- package/dist/index.js.map +1 -1
- package/dist/{interactive-assets-CCAAWVKH.js → interactive-assets-73AQO5WP.js} +18 -9
- package/dist/interactive-assets-73AQO5WP.js.map +1 -0
- package/dist/job-runner.js +166 -111
- package/dist/job-runner.js.map +1 -1
- package/dist/job-supervisor.d.ts +2 -0
- package/dist/job-supervisor.js +121 -0
- package/dist/job-supervisor.js.map +1 -0
- package/dist/loader-DQNPDKOX.js +27 -0
- package/dist/{mcp-approvals-WNPH65BK.js → mcp-approvals-27LJYADQ.js} +6 -5
- package/dist/{mcp-config-YIJIES2C.js → mcp-config-GXSODNCM.js} +4 -3
- package/dist/memory-store-I6Z54VUN.js +51 -0
- package/dist/{model-window-store-YFVNXCIP.js → model-window-store-6VKXXTPJ.js} +9 -4
- package/dist/openai-compatible-IPUNBJ53.js +16 -0
- package/dist/permission-approvals-Q5IMFEZI.js +15 -0
- package/dist/permissions-7Y35X6HZ.js +62 -0
- package/dist/provider-approvals-5AGH3RVT.js +76 -0
- package/dist/sandbox-N2RY72GN.js +42 -0
- package/dist/{scrollback-LUKLIBVI.js → scrollback-GD76GNM4.js} +44 -26
- package/dist/scrollback-GD76GNM4.js.map +1 -0
- package/dist/sdk.d.ts +2739 -2069
- package/dist/sdk.js +77 -42
- package/dist/sdk.js.map +1 -1
- package/dist/settings-loader-KDBH45AQ.js +43 -0
- package/dist/settings-loader-KDBH45AQ.js.map +1 -0
- package/dist/{settings-removed-JWWPT4WA.js → settings-removed-3ATWJEF3.js} +2 -2
- package/dist/settings-removed-3ATWJEF3.js.map +1 -0
- package/dist/shell-selection-Z5WORZU4.js +31 -0
- package/dist/shell-selection-Z5WORZU4.js.map +1 -0
- package/package.json +25 -24
- package/dist/app-MOXXWEOY.js.map +0 -1
- package/dist/chunk-24AXE6SP.js.map +0 -1
- package/dist/chunk-3U5IBM24.js.map +0 -1
- package/dist/chunk-574C6MQR.js.map +0 -1
- package/dist/chunk-5GDZ22YP.js +0 -449
- package/dist/chunk-5GDZ22YP.js.map +0 -1
- package/dist/chunk-5KLVH3PY.js.map +0 -1
- package/dist/chunk-5RDRAO4B.js.map +0 -1
- package/dist/chunk-7N4J557C.js.map +0 -1
- package/dist/chunk-A7JLBK2Y.js +0 -3506
- package/dist/chunk-A7JLBK2Y.js.map +0 -1
- package/dist/chunk-ANSJPYGI.js.map +0 -1
- package/dist/chunk-BTZZUCLI.js +0 -264
- package/dist/chunk-BTZZUCLI.js.map +0 -1
- package/dist/chunk-C7D4NJ2D.js +0 -217
- package/dist/chunk-C7D4NJ2D.js.map +0 -1
- package/dist/chunk-CAGOB2N4.js.map +0 -1
- package/dist/chunk-DGMQEI6Y.js.map +0 -1
- package/dist/chunk-E75IPST5.js.map +0 -1
- package/dist/chunk-EYGD6X2T.js +0 -180
- package/dist/chunk-EYGD6X2T.js.map +0 -1
- package/dist/chunk-GTVB4HEM.js +0 -765
- package/dist/chunk-GTVB4HEM.js.map +0 -1
- package/dist/chunk-J7MIZ5CA.js.map +0 -1
- package/dist/chunk-KWHQ2XXL.js.map +0 -1
- package/dist/chunk-L6LK2QIQ.js +0 -657
- package/dist/chunk-L6LK2QIQ.js.map +0 -1
- package/dist/chunk-MAQXCUR4.js +0 -1007
- package/dist/chunk-MAQXCUR4.js.map +0 -1
- package/dist/chunk-S4XL7HOM.js +0 -402
- package/dist/chunk-S4XL7HOM.js.map +0 -1
- package/dist/chunk-URLQAXTR.js.map +0 -1
- package/dist/chunk-V4BOYR52.js.map +0 -1
- package/dist/chunk-VZQXX3WW.js.map +0 -1
- package/dist/chunk-WKBO5R4O.js.map +0 -1
- package/dist/chunk-XIFFTATU.js.map +0 -1
- package/dist/context-ZC5IFEAX.js +0 -25
- package/dist/interactive-assets-CCAAWVKH.js.map +0 -1
- package/dist/loader-4IDS2RIE.js +0 -15
- package/dist/openai-compatible-RHZUBHV6.js +0 -10
- package/dist/permission-approvals-76PVREFD.js +0 -14
- package/dist/permissions-CHCKTFQQ.js +0 -28
- package/dist/sandbox-CNVAQDA7.js +0 -23
- package/dist/scrollback-LUKLIBVI.js.map +0 -1
- package/dist/settings-loader-M2QP5TX2.js +0 -21
- package/dist/shell-selection-V6QAYXPX.js +0 -17
- package/patches/ink+6.8.0.patch +0 -13
- package/scripts/apply-ink-patch.mjs +0 -29
- package/scripts/ink-patch.mjs +0 -59
- package/scripts/verify-ink-patch.mjs +0 -11
- /package/dist/{command-approvals-6Y57VCWZ.js.map → additional-roots-VAWRQ625.js.map} +0 -0
- /package/dist/{context-ZC5IFEAX.js.map → command-approvals-ZRY66LCW.js.map} +0 -0
- /package/dist/{hook-approvals-CTUFKEJT.js.map → context-C3RAP6K5.js.map} +0 -0
- /package/dist/{loader-4IDS2RIE.js.map → hook-approvals-DPQD5H65.js.map} +0 -0
- /package/dist/{mcp-approvals-WNPH65BK.js.map → loader-DQNPDKOX.js.map} +0 -0
- /package/dist/{mcp-config-YIJIES2C.js.map → mcp-approvals-27LJYADQ.js.map} +0 -0
- /package/dist/{model-window-store-YFVNXCIP.js.map → mcp-config-GXSODNCM.js.map} +0 -0
- /package/dist/{openai-compatible-RHZUBHV6.js.map → memory-store-I6Z54VUN.js.map} +0 -0
- /package/dist/{permission-approvals-76PVREFD.js.map → model-window-store-6VKXXTPJ.js.map} +0 -0
- /package/dist/{permissions-CHCKTFQQ.js.map → openai-compatible-IPUNBJ53.js.map} +0 -0
- /package/dist/{sandbox-CNVAQDA7.js.map → permission-approvals-Q5IMFEZI.js.map} +0 -0
- /package/dist/{settings-loader-M2QP5TX2.js.map → permissions-7Y35X6HZ.js.map} +0 -0
- /package/dist/{settings-removed-JWWPT4WA.js.map → provider-approvals-5AGH3RVT.js.map} +0 -0
- /package/dist/{shell-selection-V6QAYXPX.js.map → sandbox-N2RY72GN.js.map} +0 -0
|
@@ -5,46 +5,107 @@ import {
|
|
|
5
5
|
createAgentRunContext,
|
|
6
6
|
createRunAmbientSnapshot,
|
|
7
7
|
createTerminalOutcome,
|
|
8
|
+
isSpendlessUsage,
|
|
8
9
|
isTerminalStatus,
|
|
9
10
|
loadGitignore,
|
|
10
11
|
runAgentLoop,
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
runPostCompactHooks
|
|
14
|
-
} from "./chunk-XIFFTATU.js";
|
|
12
|
+
seedShellIds
|
|
13
|
+
} from "./chunk-KDNWMKXV.js";
|
|
15
14
|
import {
|
|
16
15
|
DEFAULT_CONFIDENCE_THRESHOLD,
|
|
17
16
|
REVIEW_LENSES,
|
|
17
|
+
addUsage,
|
|
18
|
+
applyCompactResult,
|
|
18
19
|
buildReviewerPrompt,
|
|
19
20
|
buildSingleReviewPrompt,
|
|
20
21
|
clampConfidence,
|
|
22
|
+
createGoal,
|
|
21
23
|
isReviewCategory,
|
|
22
24
|
isReviewSeverity,
|
|
25
|
+
judgeCompaction,
|
|
26
|
+
judgedResult,
|
|
23
27
|
loadReviewConfig,
|
|
24
28
|
renderReviewConfigInstruction,
|
|
25
29
|
renderReviewTarget,
|
|
26
|
-
|
|
27
|
-
|
|
30
|
+
resolveCompactBudgets,
|
|
31
|
+
resolveReviewTarget,
|
|
32
|
+
restoreGoal,
|
|
33
|
+
runCompact,
|
|
34
|
+
runHooks,
|
|
35
|
+
runPostCompactHooks
|
|
36
|
+
} from "./chunk-FPR7SKIE.js";
|
|
28
37
|
import {
|
|
38
|
+
observationKey,
|
|
39
|
+
observationPathFor,
|
|
29
40
|
onAbort,
|
|
30
41
|
throwIfAborted,
|
|
42
|
+
workspaceIdentity,
|
|
31
43
|
yieldToEventLoop
|
|
32
|
-
} from "./chunk-
|
|
44
|
+
} from "./chunk-N2G4Q7ME.js";
|
|
33
45
|
import {
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
46
|
+
globWalkIgnore
|
|
47
|
+
} from "./chunk-XJRJVXJR.js";
|
|
48
|
+
import {
|
|
49
|
+
defaultGitRunner,
|
|
50
|
+
describeMissingObjects,
|
|
51
|
+
hardenedGitArgs,
|
|
52
|
+
noFetchGitEnv,
|
|
53
|
+
readRepositoryGitConfig
|
|
54
|
+
} from "./chunk-32WBCUAN.js";
|
|
55
|
+
import {
|
|
56
|
+
buildChildEnv
|
|
57
|
+
} from "./chunk-XHAO3K7I.js";
|
|
58
|
+
import {
|
|
59
|
+
findRepositoryRoot,
|
|
60
|
+
tryRealPath
|
|
61
|
+
} from "./chunk-SBTU5BEC.js";
|
|
62
|
+
import {
|
|
63
|
+
resolveAdditionalRoots
|
|
64
|
+
} from "./chunk-4DKHLZRI.js";
|
|
38
65
|
import {
|
|
39
66
|
createDebugLogger,
|
|
40
67
|
createDebugLoggerWithCounter
|
|
41
|
-
} from "./chunk-
|
|
68
|
+
} from "./chunk-OJBMDM5H.js";
|
|
69
|
+
import {
|
|
70
|
+
isOutside,
|
|
71
|
+
normalizeToolResult,
|
|
72
|
+
resolveMentionReadablePath
|
|
73
|
+
} from "./chunk-YIL6HTJX.js";
|
|
74
|
+
import {
|
|
75
|
+
createUncGuard,
|
|
76
|
+
reachesUncPath,
|
|
77
|
+
uncGuardedWalkFs
|
|
78
|
+
} from "./chunk-EEX6QNXV.js";
|
|
79
|
+
import {
|
|
80
|
+
renderUntrustedText
|
|
81
|
+
} from "./chunk-WV3O5PGX.js";
|
|
82
|
+
|
|
83
|
+
// src/session-driver.ts
|
|
84
|
+
var SESSION_DRIVERS = ["human", "agent"];
|
|
85
|
+
function isSessionDriver(value) {
|
|
86
|
+
return SESSION_DRIVERS.includes(value);
|
|
87
|
+
}
|
|
88
|
+
function parseSessionDriver(value) {
|
|
89
|
+
const normalized = value?.trim().toLowerCase();
|
|
90
|
+
return isSessionDriver(normalized) ? normalized : void 0;
|
|
91
|
+
}
|
|
92
|
+
function resolveSessionDriver(input) {
|
|
93
|
+
const explicit = parseSessionDriver(input.explicit);
|
|
94
|
+
if (explicit) return { driver: explicit, signals: ["flag"] };
|
|
95
|
+
const fromEnv = parseSessionDriver(input.env.BOOK_SESSION_DRIVER);
|
|
96
|
+
if (fromEnv) return { driver: fromEnv, signals: ["BOOK_SESSION_DRIVER"] };
|
|
97
|
+
const signals = [];
|
|
98
|
+
const claudeCode = input.env.CLAUDECODE?.trim();
|
|
99
|
+
if (claudeCode && claudeCode !== "0") signals.push("CLAUDECODE");
|
|
100
|
+
if (input.env.AI_AGENT?.trim()) signals.push("AI_AGENT");
|
|
101
|
+
return { driver: signals.length > 0 ? "agent" : "human", signals };
|
|
102
|
+
}
|
|
42
103
|
|
|
43
104
|
// src/session/name.ts
|
|
44
105
|
var UNTITLED_SESSION_NAME = "Untitled session";
|
|
45
106
|
var MAX_SESSION_NAME_LENGTH = 56;
|
|
46
107
|
function deriveSessionName(prompt) {
|
|
47
|
-
const normalized = prompt.replace(/\r?\n/g, " ").replace(/\s+/g, " ").trim().replace(/^[/!#>*_
|
|
108
|
+
const normalized = prompt.replace(/^\s*`{3,}[^\n`]*\r?\n/, "").replace(/^[ \t]*`{3,}[\w+-]*[ \t\r]*$/gm, "").replace(/\r?\n/g, " ").replace(/\s+/g, " ").trim().replace(/^[/!#>*_~-]+\s*/, "").replace(/[.!?,;:]+$/, "").trim();
|
|
48
109
|
if (!normalized) return UNTITLED_SESSION_NAME;
|
|
49
110
|
const titled = normalized.replace(/^[a-z]/, (letter) => letter.toUpperCase());
|
|
50
111
|
if (Array.from(titled).length <= MAX_SESSION_NAME_LENGTH) return titled;
|
|
@@ -85,6 +146,7 @@ var READ_REF_LIMIT = 8;
|
|
|
85
146
|
var READ_TOTAL_CHARS = 16e3;
|
|
86
147
|
var TOOL_RESULT_PREVIEW_CHARS = 4e3;
|
|
87
148
|
var SESSION_INDEX_FILE = "session-index.json";
|
|
149
|
+
var HEADER_PROBE_BYTES = 16 * 1024;
|
|
88
150
|
var JSONL_READ_BUFFER_BYTES = 64 * 1024;
|
|
89
151
|
var IMAGE_STORAGE_KEY = /^[a-f0-9]{64}\.(?:png|jpg|gif|webp)$/;
|
|
90
152
|
function imageSessionDirectory(root, sessionId) {
|
|
@@ -173,30 +235,37 @@ var SessionStore = class {
|
|
|
173
235
|
}
|
|
174
236
|
persistIndex(deletedIds = [], changedIds) {
|
|
175
237
|
if (this.buildingIndex) return;
|
|
238
|
+
this.withIndexLock(() => this.writeIndexLocked(deletedIds, changedIds));
|
|
239
|
+
}
|
|
240
|
+
withIndexLock(action) {
|
|
176
241
|
const lock = this.acquireIndexLock();
|
|
177
242
|
try {
|
|
178
|
-
|
|
179
|
-
const ids = changedIds ?? Array.from(this.metadataIndex.keys());
|
|
180
|
-
for (const id of ids) {
|
|
181
|
-
const entry = this.metadataIndex.get(id);
|
|
182
|
-
if (entry) sessions[id] = entry;
|
|
183
|
-
}
|
|
184
|
-
for (const id of deletedIds) delete sessions[id];
|
|
185
|
-
const target = this.indexPath();
|
|
186
|
-
const temporary = `${target}.${process.pid}.${crypto.randomUUID()}.tmp`;
|
|
187
|
-
try {
|
|
188
|
-
writeFileSync(temporary, JSON.stringify({ version: 1, sessions }), "utf-8");
|
|
189
|
-
renameSync(temporary, target);
|
|
190
|
-
this.indexMtimeMs = statSync(target).mtimeMs;
|
|
191
|
-
this.metadataIndex.clear();
|
|
192
|
-
for (const [id, entry] of Object.entries(sessions)) this.metadataIndex.set(id, entry);
|
|
193
|
-
} finally {
|
|
194
|
-
if (existsSync(temporary)) unlinkSync(temporary);
|
|
195
|
-
}
|
|
243
|
+
return action();
|
|
196
244
|
} finally {
|
|
197
245
|
this.releaseIndexLock(lock);
|
|
198
246
|
}
|
|
199
247
|
}
|
|
248
|
+
/** The index write itself; the caller holds the index lock. */
|
|
249
|
+
writeIndexLocked(deletedIds, changedIds) {
|
|
250
|
+
const sessions = this.readPersistedIndexEntries();
|
|
251
|
+
const ids = changedIds ?? Array.from(this.metadataIndex.keys());
|
|
252
|
+
for (const id of ids) {
|
|
253
|
+
const entry = this.metadataIndex.get(id);
|
|
254
|
+
if (entry) sessions[id] = entry;
|
|
255
|
+
}
|
|
256
|
+
for (const id of deletedIds) delete sessions[id];
|
|
257
|
+
const target = this.indexPath();
|
|
258
|
+
const temporary = `${target}.${process.pid}.${crypto.randomUUID()}.tmp`;
|
|
259
|
+
try {
|
|
260
|
+
writeFileSync(temporary, JSON.stringify({ version: 1, sessions }), "utf-8");
|
|
261
|
+
renameSync(temporary, target);
|
|
262
|
+
this.indexMtimeMs = statSync(target).mtimeMs;
|
|
263
|
+
this.metadataIndex.clear();
|
|
264
|
+
for (const [id, entry] of Object.entries(sessions)) this.metadataIndex.set(id, entry);
|
|
265
|
+
} finally {
|
|
266
|
+
if (existsSync(temporary)) unlinkSync(temporary);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
200
269
|
acquireIndexLock() {
|
|
201
270
|
const path = `${this.indexPath()}.lock`;
|
|
202
271
|
for (let attempt = 0; attempt < 50; attempt++) {
|
|
@@ -274,7 +343,8 @@ var SessionStore = class {
|
|
|
274
343
|
name: meta.name,
|
|
275
344
|
createdAt: now,
|
|
276
345
|
updatedAt: now,
|
|
277
|
-
messageCount: 0
|
|
346
|
+
messageCount: 0,
|
|
347
|
+
...meta.driver ? { driver: meta.driver } : {}
|
|
278
348
|
}
|
|
279
349
|
};
|
|
280
350
|
appendFileSync(this.path(id), JSON.stringify(header) + "\n", "utf-8");
|
|
@@ -286,10 +356,10 @@ var SessionStore = class {
|
|
|
286
356
|
appendFileSync(this.path(id), serialized, "utf-8");
|
|
287
357
|
const cached = this.parsedSessions.get(id);
|
|
288
358
|
if (cached) {
|
|
289
|
-
const
|
|
359
|
+
const stat2 = statSync(this.path(id));
|
|
290
360
|
cached.records.push(record);
|
|
291
|
-
cached.size =
|
|
292
|
-
cached.mtimeMs =
|
|
361
|
+
cached.size = stat2.size;
|
|
362
|
+
cached.mtimeMs = stat2.mtimeMs;
|
|
293
363
|
cached.loaded = void 0;
|
|
294
364
|
cached.activeEventIds = void 0;
|
|
295
365
|
cached.activeRecords = void 0;
|
|
@@ -300,12 +370,12 @@ var SessionStore = class {
|
|
|
300
370
|
readRecords(id) {
|
|
301
371
|
const p = this.path(id);
|
|
302
372
|
if (!existsSync(p)) throw new Error(`Session not found: ${id}`);
|
|
303
|
-
const
|
|
373
|
+
const stat2 = statSync(p);
|
|
304
374
|
const cached = this.parsedSessions.get(id);
|
|
305
|
-
if (cached && cached.size ===
|
|
375
|
+
if (cached && cached.size === stat2.size && cached.mtimeMs === stat2.mtimeMs) {
|
|
306
376
|
return cached.records;
|
|
307
377
|
}
|
|
308
|
-
const
|
|
378
|
+
const records2 = [];
|
|
309
379
|
const descriptor = openSync(p, "r");
|
|
310
380
|
const buffer = Buffer.allocUnsafe(JSONL_READ_BUFFER_BYTES);
|
|
311
381
|
const decoder = new StringDecoder("utf8");
|
|
@@ -315,7 +385,7 @@ var SessionStore = class {
|
|
|
315
385
|
lineNumber++;
|
|
316
386
|
if (!line) return;
|
|
317
387
|
try {
|
|
318
|
-
|
|
388
|
+
records2.push(JSON.parse(line));
|
|
319
389
|
} catch {
|
|
320
390
|
log.warn("ignoring corrupt session record", { id, line: lineNumber });
|
|
321
391
|
}
|
|
@@ -337,11 +407,11 @@ var SessionStore = class {
|
|
|
337
407
|
closeSync(descriptor);
|
|
338
408
|
}
|
|
339
409
|
this.parsedSessions.set(id, {
|
|
340
|
-
size:
|
|
341
|
-
mtimeMs:
|
|
342
|
-
records
|
|
410
|
+
size: stat2.size,
|
|
411
|
+
mtimeMs: stat2.mtimeMs,
|
|
412
|
+
records: records2
|
|
343
413
|
});
|
|
344
|
-
return
|
|
414
|
+
return records2;
|
|
345
415
|
}
|
|
346
416
|
updateIndexAfterAppend(id, record) {
|
|
347
417
|
const entry = this.metadataIndex.get(id);
|
|
@@ -356,10 +426,15 @@ var SessionStore = class {
|
|
|
356
426
|
entry.meta.updatedAt = record.timestamp;
|
|
357
427
|
if (data.kind === "session_meta_patch") {
|
|
358
428
|
if (Object.prototype.hasOwnProperty.call(data, "name")) entry.meta.name = data.name;
|
|
429
|
+
if (isSessionDriver(data.driver)) {
|
|
430
|
+
entry.meta.driver = data.driver;
|
|
431
|
+
}
|
|
359
432
|
} else if (record.type === "user") {
|
|
360
433
|
entry.meta.messageCount++;
|
|
361
434
|
entry.lastMessageRole = "user";
|
|
362
|
-
if (!entry.meta.name && data.content
|
|
435
|
+
if (!entry.meta.name && data.content && data.kind !== "agent-notification") {
|
|
436
|
+
entry.meta.name = deriveSessionName(data.content);
|
|
437
|
+
}
|
|
363
438
|
} else if (record.type === "local") {
|
|
364
439
|
entry.meta.messageCount++;
|
|
365
440
|
entry.lastMessageRole = "assistant";
|
|
@@ -393,10 +468,10 @@ var SessionStore = class {
|
|
|
393
468
|
});
|
|
394
469
|
}
|
|
395
470
|
load(id) {
|
|
396
|
-
const
|
|
471
|
+
const records2 = this.readRecords(id);
|
|
397
472
|
const parsedCache = this.parsedSessions.get(id);
|
|
398
473
|
if (parsedCache?.loaded) return cloneLoadedSession(parsedCache.loaded);
|
|
399
|
-
const metaRec =
|
|
474
|
+
const metaRec = records2.find(
|
|
400
475
|
(record) => record.data?.kind === "session_meta"
|
|
401
476
|
);
|
|
402
477
|
const storedMeta = metaRec?.data;
|
|
@@ -406,7 +481,8 @@ var SessionStore = class {
|
|
|
406
481
|
createdAt: storedMeta?.createdAt ?? 0,
|
|
407
482
|
updatedAt: storedMeta?.updatedAt ?? 0,
|
|
408
483
|
messageCount: 0,
|
|
409
|
-
...storedMeta?.name === void 0 ? {} : { name: storedMeta.name }
|
|
484
|
+
...storedMeta?.name === void 0 ? {} : { name: storedMeta.name },
|
|
485
|
+
...isSessionDriver(storedMeta?.driver) ? { driver: storedMeta.driver } : {}
|
|
410
486
|
};
|
|
411
487
|
const transcript = [];
|
|
412
488
|
let contextHistory = [];
|
|
@@ -418,30 +494,28 @@ var SessionStore = class {
|
|
|
418
494
|
const checkpointByUserEventId = /* @__PURE__ */ new Map();
|
|
419
495
|
const snapshotIds = /* @__PURE__ */ new Set();
|
|
420
496
|
let latestPlan;
|
|
497
|
+
let latestGoal;
|
|
421
498
|
let carriedUsage;
|
|
422
499
|
const carriedModels = /* @__PURE__ */ new Set();
|
|
423
500
|
let count = 0;
|
|
424
|
-
for (let lineIndex = 0; lineIndex <
|
|
425
|
-
const record =
|
|
501
|
+
for (let lineIndex = 0; lineIndex < records2.length; lineIndex++) {
|
|
502
|
+
const record = records2[lineIndex];
|
|
426
503
|
const eventId = record.eventId ?? `legacy-line-${lineIndex + 1}`;
|
|
427
504
|
const data = record.data;
|
|
428
505
|
if (data.kind === "session_meta_patch") {
|
|
429
506
|
if (Object.prototype.hasOwnProperty.call(data, "name")) meta.name = data.name;
|
|
507
|
+
const driver = data.driver;
|
|
508
|
+
if (isSessionDriver(driver)) meta.driver = driver;
|
|
430
509
|
continue;
|
|
431
510
|
}
|
|
432
511
|
if (data.kind === "session_meta" || data.kind === "session_touch") continue;
|
|
433
512
|
if (record.type === "usage") {
|
|
434
513
|
const entry = record.data;
|
|
435
514
|
if (entry?.version === 1 && entry.usage) {
|
|
436
|
-
carriedUsage =
|
|
437
|
-
promptTokens: (carriedUsage?.promptTokens ?? 0) + (entry.usage.promptTokens ?? 0),
|
|
438
|
-
completionTokens: (carriedUsage?.completionTokens ?? 0) + (entry.usage.completionTokens ?? 0),
|
|
439
|
-
totalTokens: (carriedUsage?.totalTokens ?? 0) + (entry.usage.totalTokens ?? 0),
|
|
440
|
-
cacheCreationInputTokens: (carriedUsage?.cacheCreationInputTokens ?? 0) + (entry.usage.cacheCreationInputTokens ?? 0),
|
|
441
|
-
cacheReadInputTokens: (carriedUsage?.cacheReadInputTokens ?? 0) + (entry.usage.cacheReadInputTokens ?? 0)
|
|
442
|
-
};
|
|
515
|
+
carriedUsage = addUsage(carriedUsage ?? null, entry.usage);
|
|
443
516
|
const model = entry.responseModel ?? entry.requestedModel;
|
|
444
517
|
if (model) carriedModels.add(model);
|
|
518
|
+
for (const spent of entry.models ?? []) carriedModels.add(spent);
|
|
445
519
|
}
|
|
446
520
|
continue;
|
|
447
521
|
}
|
|
@@ -450,6 +524,11 @@ var SessionStore = class {
|
|
|
450
524
|
if (plan?.version === 1) latestPlan = plan;
|
|
451
525
|
continue;
|
|
452
526
|
}
|
|
527
|
+
if (record.type === "goal") {
|
|
528
|
+
const goal = record.data;
|
|
529
|
+
if (goal?.version === 1) latestGoal = goal;
|
|
530
|
+
continue;
|
|
531
|
+
}
|
|
453
532
|
if (record.type === "turn_checkpoint") {
|
|
454
533
|
const checkpoint = record.data;
|
|
455
534
|
if (checkpoint?.version !== 1 || !checkpoint.checkpointId || !checkpoint.userEventId || typeof checkpoint.prompt !== "string") {
|
|
@@ -460,6 +539,7 @@ var SessionStore = class {
|
|
|
460
539
|
id: checkpoint.checkpointId,
|
|
461
540
|
userEventId: checkpoint.userEventId,
|
|
462
541
|
prompt: checkpoint.prompt,
|
|
542
|
+
...checkpoint.promptTyped === true ? { promptTyped: true } : {},
|
|
463
543
|
attachments: checkpoint.attachments,
|
|
464
544
|
timestamp: record.timestamp,
|
|
465
545
|
...checkpoint.checkpoint,
|
|
@@ -601,6 +681,7 @@ var SessionStore = class {
|
|
|
601
681
|
toolCalls: data.toolCalls,
|
|
602
682
|
toolResults: normalizePersistedToolResults(data.toolResults),
|
|
603
683
|
fileObservations: data.fileObservations,
|
|
684
|
+
hostNotice: data.hostNotice,
|
|
604
685
|
timestamp: record.timestamp
|
|
605
686
|
};
|
|
606
687
|
transcript.push(message);
|
|
@@ -639,11 +720,11 @@ var SessionStore = class {
|
|
|
639
720
|
}
|
|
640
721
|
}
|
|
641
722
|
}
|
|
642
|
-
if (
|
|
723
|
+
if (records2.length) meta.updatedAt = records2[records2.length - 1].timestamp;
|
|
643
724
|
meta.messageCount = count;
|
|
644
725
|
if (!meta.name) {
|
|
645
726
|
const firstUserPrompt = transcript.find(
|
|
646
|
-
(message) => message.role === "user" && message.kind !== "local"
|
|
727
|
+
(message) => message.role === "user" && message.kind !== "local" && message.kind !== "agent-notification"
|
|
647
728
|
)?.content;
|
|
648
729
|
if (firstUserPrompt) meta.name = deriveSessionName(firstUserPrompt);
|
|
649
730
|
}
|
|
@@ -658,6 +739,7 @@ var SessionStore = class {
|
|
|
658
739
|
activeEventIds,
|
|
659
740
|
meta,
|
|
660
741
|
plan: latestPlan,
|
|
742
|
+
goal: latestGoal,
|
|
661
743
|
carriedUsage,
|
|
662
744
|
carriedModels: [...carriedModels],
|
|
663
745
|
history: contextHistory
|
|
@@ -780,9 +862,9 @@ var SessionStore = class {
|
|
|
780
862
|
activeRecordMap(id) {
|
|
781
863
|
const cache = this.parsedSessions.get(id);
|
|
782
864
|
if (cache?.recordsByActiveId) return cache.recordsByActiveId;
|
|
783
|
-
const
|
|
865
|
+
const records2 = this.activeRecordIndex(id);
|
|
784
866
|
const refreshed = this.parsedSessions.get(id);
|
|
785
|
-
const byId = new Map(
|
|
867
|
+
const byId = new Map(records2.map((record) => [record.eventId, record]));
|
|
786
868
|
if (refreshed) refreshed.recordsByActiveId = byId;
|
|
787
869
|
return byId;
|
|
788
870
|
}
|
|
@@ -805,9 +887,15 @@ var SessionStore = class {
|
|
|
805
887
|
}
|
|
806
888
|
return references;
|
|
807
889
|
}
|
|
808
|
-
|
|
890
|
+
/**
|
|
891
|
+
* The newest session in `cwd` that holds a conversation. A launch that ends
|
|
892
|
+
* before its first prompt leaves a session with no messages, and that is never
|
|
893
|
+
* the conversation `--continue` means (#428). `accept` replaces that default test
|
|
894
|
+
* for a caller with a wider idea of a live session, such as `book status`.
|
|
895
|
+
*/
|
|
896
|
+
mostRecentInCwd(cwd, accept = (meta) => meta.messageCount > 0) {
|
|
809
897
|
const normalized = normalizeWorkspace(cwd);
|
|
810
|
-
return this.list().find((meta) => meta.cwd === normalized);
|
|
898
|
+
return this.list().find((meta) => meta.cwd === normalized && accept(meta));
|
|
811
899
|
}
|
|
812
900
|
findByName(name) {
|
|
813
901
|
return this.list().find((meta) => meta.name === name);
|
|
@@ -824,19 +912,13 @@ var SessionStore = class {
|
|
|
824
912
|
if (preserveIds.has(id)) continue;
|
|
825
913
|
try {
|
|
826
914
|
if (entry.meta.updatedAt < cutoff) {
|
|
827
|
-
|
|
828
|
-
rmSync(imageSessionDirectory(this.root, id), { recursive: true, force: true });
|
|
829
|
-
this.metadataIndex.delete(id);
|
|
830
|
-
this.parsedSessions.delete(id);
|
|
915
|
+
this.discardSession(id);
|
|
831
916
|
removedIds.push(id);
|
|
832
917
|
removed++;
|
|
833
918
|
}
|
|
834
919
|
} catch {
|
|
835
920
|
if (existsSync(this.path(id)) && statSync(this.path(id)).mtimeMs < cutoff) {
|
|
836
|
-
|
|
837
|
-
rmSync(imageSessionDirectory(this.root, id), { recursive: true, force: true });
|
|
838
|
-
this.metadataIndex.delete(id);
|
|
839
|
-
this.parsedSessions.delete(id);
|
|
921
|
+
this.discardSession(id);
|
|
840
922
|
removedIds.push(id);
|
|
841
923
|
removed++;
|
|
842
924
|
}
|
|
@@ -845,7 +927,85 @@ var SessionStore = class {
|
|
|
845
927
|
if (removedIds.length > 0) this.persistIndex(removedIds, []);
|
|
846
928
|
return removed;
|
|
847
929
|
}
|
|
930
|
+
/**
|
|
931
|
+
* Remove one session this process created if nothing was ever recorded in it: its file
|
|
932
|
+
* holds nothing but the bare header `create` wrote (no turn, no run, no patch, no name).
|
|
933
|
+
* A name is something a user typed (`--name`), so a named session is kept. The decision
|
|
934
|
+
* is made from the stored file, under the index lock, and the file and its index entry go
|
|
935
|
+
* together. A file another process still holds (EBUSY, EPERM on Windows) is left in place.
|
|
936
|
+
* Once the file is gone the session is gone: an attachment directory that will not go, or an
|
|
937
|
+
* index file that cannot be rewritten, no longer undoes that (a later index rebuild drops the
|
|
938
|
+
* stale entry). Returns whether the session was removed.
|
|
939
|
+
*/
|
|
940
|
+
removeUntouched(id) {
|
|
941
|
+
if (!existsSync(this.path(id))) return false;
|
|
942
|
+
this.ensureIndex();
|
|
943
|
+
const entry = this.metadataIndex.get(id);
|
|
944
|
+
if (entry && (entry.meta.messageCount > 0 || entry.meta.name)) return false;
|
|
945
|
+
try {
|
|
946
|
+
return this.withIndexLock(() => {
|
|
947
|
+
if (!this.isUntouched(id)) return false;
|
|
948
|
+
try {
|
|
949
|
+
this.discardSession(id);
|
|
950
|
+
} catch (error) {
|
|
951
|
+
if (!isErrno(error, ["ENOENT"])) throw error;
|
|
952
|
+
this.dropSession(id);
|
|
953
|
+
}
|
|
954
|
+
try {
|
|
955
|
+
this.writeIndexLocked([id], []);
|
|
956
|
+
} catch (error) {
|
|
957
|
+
if (!isErrno(error, ["ENOENT", "EBUSY", "EPERM"])) throw error;
|
|
958
|
+
log.warn("could not rewrite the session index after removing a session", { id });
|
|
959
|
+
}
|
|
960
|
+
return true;
|
|
961
|
+
});
|
|
962
|
+
} catch (error) {
|
|
963
|
+
if (isErrno(error, ["ENOENT", "EBUSY", "EPERM"])) return false;
|
|
964
|
+
throw error;
|
|
965
|
+
}
|
|
966
|
+
}
|
|
967
|
+
isUntouched(id) {
|
|
968
|
+
if (statSync(this.path(id)).size > HEADER_PROBE_BYTES) return false;
|
|
969
|
+
const lines = readFileSync(this.path(id), "utf-8").split("\n").filter(Boolean);
|
|
970
|
+
if (lines.length === 0) return true;
|
|
971
|
+
if (lines.length > 1) return false;
|
|
972
|
+
try {
|
|
973
|
+
const header = JSON.parse(lines[0]);
|
|
974
|
+
const data = header.data ?? {};
|
|
975
|
+
return header.type === "session_meta" && data.kind === "session_meta" && !data.name;
|
|
976
|
+
} catch {
|
|
977
|
+
return false;
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
/** Remove one session's file, attachments and cached state. The index file is not rewritten. */
|
|
981
|
+
discardSession(id) {
|
|
982
|
+
this.unlinkSessionFile(id);
|
|
983
|
+
this.dropSession(id);
|
|
984
|
+
}
|
|
985
|
+
unlinkSessionFile(id) {
|
|
986
|
+
unlinkSync(this.path(id));
|
|
987
|
+
}
|
|
988
|
+
/** What remains once the session file is gone. Attachments are best-effort. */
|
|
989
|
+
dropSession(id) {
|
|
990
|
+
try {
|
|
991
|
+
this.removeAttachments(id);
|
|
992
|
+
} catch (error) {
|
|
993
|
+
log.warn("could not remove a session attachment directory", {
|
|
994
|
+
id,
|
|
995
|
+
error: error instanceof Error ? error.message : String(error)
|
|
996
|
+
});
|
|
997
|
+
}
|
|
998
|
+
this.metadataIndex.delete(id);
|
|
999
|
+
this.parsedSessions.delete(id);
|
|
1000
|
+
}
|
|
1001
|
+
removeAttachments(id) {
|
|
1002
|
+
rmSync(imageSessionDirectory(this.root, id), { recursive: true, force: true });
|
|
1003
|
+
}
|
|
848
1004
|
};
|
|
1005
|
+
function isErrno(error, codes) {
|
|
1006
|
+
const code = error?.code;
|
|
1007
|
+
return code !== void 0 && codes.includes(code);
|
|
1008
|
+
}
|
|
849
1009
|
function cloneLoadedSession(session) {
|
|
850
1010
|
const contextHistory = [...session.contextHistory];
|
|
851
1011
|
return {
|
|
@@ -860,6 +1020,7 @@ function cloneLoadedSession(session) {
|
|
|
860
1020
|
todos: session.plan.todos?.map((todo) => ({ ...todo })),
|
|
861
1021
|
tasks: session.plan.tasks?.map((task) => ({ ...task }))
|
|
862
1022
|
} : void 0,
|
|
1023
|
+
goal: session.goal ? { version: 1, goal: session.goal.goal ? { ...session.goal.goal } : null } : void 0,
|
|
863
1024
|
carriedUsage: session.carriedUsage ? { ...session.carriedUsage } : void 0,
|
|
864
1025
|
carriedModels: session.carriedModels ? [...session.carriedModels] : void 0,
|
|
865
1026
|
history: contextHistory
|
|
@@ -942,7 +1103,10 @@ function reviewRunnerFor(manager, attribution = {}) {
|
|
|
942
1103
|
rootRunId: attribution.rootRunId,
|
|
943
1104
|
parentRunId: attribution.parentRunId,
|
|
944
1105
|
parentSessionId: attribution.parentSessionId,
|
|
945
|
-
notifyParentOnCompletion: false
|
|
1106
|
+
notifyParentOnCompletion: false,
|
|
1107
|
+
// The report dies with the process, so a restart must not re-run a reviewer
|
|
1108
|
+
// whose result nobody would receive.
|
|
1109
|
+
resumeAfterRestart: false
|
|
946
1110
|
});
|
|
947
1111
|
return {
|
|
948
1112
|
id: record.id,
|
|
@@ -980,24 +1144,25 @@ function fixRunnerFor(manager, attribution = {}) {
|
|
|
980
1144
|
const evidence = (await manager.listEvidence({ ids: [evidenceId] }))[0];
|
|
981
1145
|
return evidence ? evidenceProjection(evidence) : void 0;
|
|
982
1146
|
},
|
|
983
|
-
async apply(agentId, evidenceId) {
|
|
984
|
-
const result = await manager.apply(agentId, evidenceId);
|
|
1147
|
+
async apply(agentId, evidenceId, options) {
|
|
1148
|
+
const result = await manager.apply(agentId, evidenceId, options);
|
|
985
1149
|
return { status: result.status, commit: result.commit, error: result.error };
|
|
986
1150
|
}
|
|
987
1151
|
};
|
|
988
1152
|
}
|
|
989
1153
|
|
|
990
1154
|
// src/review/json.ts
|
|
991
|
-
function
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
1155
|
+
function firstObjectIn(text) {
|
|
1156
|
+
return objectIn(text, 0)?.slice;
|
|
1157
|
+
}
|
|
1158
|
+
function objectIn(text, from) {
|
|
1159
|
+
const start = text.indexOf("{", from);
|
|
995
1160
|
if (start === -1) return void 0;
|
|
996
1161
|
let depth = 0;
|
|
997
1162
|
let inString = false;
|
|
998
1163
|
let escaped = false;
|
|
999
|
-
for (let index = start; index <
|
|
1000
|
-
const char =
|
|
1164
|
+
for (let index = start; index < text.length; index++) {
|
|
1165
|
+
const char = text[index];
|
|
1001
1166
|
if (inString) {
|
|
1002
1167
|
if (escaped) escaped = false;
|
|
1003
1168
|
else if (char === "\\") escaped = true;
|
|
@@ -1006,10 +1171,47 @@ function extractJsonObject(text) {
|
|
|
1006
1171
|
}
|
|
1007
1172
|
if (char === '"') inString = true;
|
|
1008
1173
|
else if (char === "{") depth++;
|
|
1009
|
-
else if (char === "}" && --depth === 0)
|
|
1174
|
+
else if (char === "}" && --depth === 0) {
|
|
1175
|
+
return { start, end: index + 1, slice: text.slice(start, index + 1) };
|
|
1176
|
+
}
|
|
1010
1177
|
}
|
|
1011
1178
|
return void 0;
|
|
1012
1179
|
}
|
|
1180
|
+
function fenceRuns(text) {
|
|
1181
|
+
const runs = [];
|
|
1182
|
+
for (let index = text.indexOf("```"); index !== -1; index = text.indexOf("```", index + 3)) {
|
|
1183
|
+
runs.push(index);
|
|
1184
|
+
}
|
|
1185
|
+
return runs;
|
|
1186
|
+
}
|
|
1187
|
+
function fenceContent(text, open, close) {
|
|
1188
|
+
return text.slice(open + 3, close).replace(/^json\s*/i, "");
|
|
1189
|
+
}
|
|
1190
|
+
function isJsonObject(candidate) {
|
|
1191
|
+
try {
|
|
1192
|
+
const parsed = JSON.parse(candidate);
|
|
1193
|
+
return parsed !== null && typeof parsed === "object" && !Array.isArray(parsed);
|
|
1194
|
+
} catch {
|
|
1195
|
+
return false;
|
|
1196
|
+
}
|
|
1197
|
+
}
|
|
1198
|
+
function extractJsonObject(text) {
|
|
1199
|
+
const runs = fenceRuns(text);
|
|
1200
|
+
const whole = objectIn(text, 0);
|
|
1201
|
+
if (whole !== void 0 && isJsonObject(whole.slice) && runs.every((run) => run >= whole.start && run < whole.end)) {
|
|
1202
|
+
return whole.slice;
|
|
1203
|
+
}
|
|
1204
|
+
let firstBalanced;
|
|
1205
|
+
for (let open = 0; open < runs.length; open++) {
|
|
1206
|
+
for (let close = open + 1; close < runs.length; close++) {
|
|
1207
|
+
const found = firstObjectIn(fenceContent(text, runs[open], runs[close]));
|
|
1208
|
+
if (found === void 0) continue;
|
|
1209
|
+
if (isJsonObject(found)) return found;
|
|
1210
|
+
firstBalanced ??= found;
|
|
1211
|
+
}
|
|
1212
|
+
}
|
|
1213
|
+
return firstBalanced ?? firstObjectIn(text);
|
|
1214
|
+
}
|
|
1013
1215
|
function parseJsonObject(text) {
|
|
1014
1216
|
const candidate = extractJsonObject(text);
|
|
1015
1217
|
if (!candidate) return void 0;
|
|
@@ -1207,6 +1409,29 @@ function rankFindings(findings) {
|
|
|
1207
1409
|
function filterLowConfidence(findings, threshold = DEFAULT_CONFIDENCE_THRESHOLD) {
|
|
1208
1410
|
return findings.filter((finding) => finding.confidence >= threshold);
|
|
1209
1411
|
}
|
|
1412
|
+
function isStanding(finding) {
|
|
1413
|
+
return finding.verification === void 0 || finding.verification === "confirmed";
|
|
1414
|
+
}
|
|
1415
|
+
function deriveReviewVerdict(input) {
|
|
1416
|
+
const entries = [...input.coverage.reviewers, input.coverage.verifier].filter(
|
|
1417
|
+
(entry) => entry !== void 0
|
|
1418
|
+
);
|
|
1419
|
+
if (entries.some((entry) => entry.status !== "completed")) {
|
|
1420
|
+
return { verdict: "inconclusive", reason: "incomplete-coverage" };
|
|
1421
|
+
}
|
|
1422
|
+
const standing = input.findings.filter(isStanding);
|
|
1423
|
+
if (standing.some((finding) => finding.severity === "critical")) {
|
|
1424
|
+
return { verdict: "blocking", reason: "standing-findings" };
|
|
1425
|
+
}
|
|
1426
|
+
if (standing.length > 0) return { verdict: "recommend", reason: "standing-findings" };
|
|
1427
|
+
if (input.findings.length > 0) {
|
|
1428
|
+
return { verdict: "inconclusive", reason: "unverified-findings" };
|
|
1429
|
+
}
|
|
1430
|
+
if (input.reviewerVerdicts?.some((verdict) => verdict === "inconclusive")) {
|
|
1431
|
+
return { verdict: "inconclusive", reason: "reviewer-inconclusive" };
|
|
1432
|
+
}
|
|
1433
|
+
return { verdict: "clean", reason: "clean" };
|
|
1434
|
+
}
|
|
1210
1435
|
|
|
1211
1436
|
// src/review/verify-findings.ts
|
|
1212
1437
|
function buildVerificationPrompt(findings, config = {}, target) {
|
|
@@ -1370,13 +1595,23 @@ async function runSingleReview(runner, scope, workspace, options = {}) {
|
|
|
1370
1595
|
rankFindings(filterLowConfidence(dedupeFindings(parsed.report.findings)))
|
|
1371
1596
|
);
|
|
1372
1597
|
const coverage = passCoverage("single", structured, parsed.droppedFindings, findings.length);
|
|
1598
|
+
const reviewerEntries = [coverage];
|
|
1599
|
+
const reportCoverage = { reviewers: reviewerEntries };
|
|
1600
|
+
const { verdict, reason } = deriveReviewVerdict({
|
|
1601
|
+
findings,
|
|
1602
|
+
coverage: reportCoverage,
|
|
1603
|
+
// A completed, structured pass whose own verdict is `inconclusive` is the
|
|
1604
|
+
// one reviewer signal that is not already a coverage entry.
|
|
1605
|
+
reviewerVerdicts: structured ? [parsed.report.verdict] : []
|
|
1606
|
+
});
|
|
1373
1607
|
const report = {
|
|
1374
|
-
verdict
|
|
1608
|
+
verdict,
|
|
1375
1609
|
findings,
|
|
1376
|
-
coverage:
|
|
1610
|
+
coverage: reportCoverage
|
|
1377
1611
|
};
|
|
1378
1612
|
const text = [
|
|
1379
|
-
renderCoverageWarnings(
|
|
1613
|
+
renderCoverageWarnings(reviewerEntries),
|
|
1614
|
+
renderVerdictReason("Review", reason),
|
|
1380
1615
|
renderReviewReport(report),
|
|
1381
1616
|
coverage.status === "completed" ? "" : renderRawOutput("reviewer", settled.result)
|
|
1382
1617
|
].filter(Boolean).join("\n\n");
|
|
@@ -1414,6 +1649,17 @@ function renderCoverageWarnings(entries) {
|
|
|
1414
1649
|
})
|
|
1415
1650
|
].join("\n");
|
|
1416
1651
|
}
|
|
1652
|
+
function renderVerdictReason(prefix, reason) {
|
|
1653
|
+
const explanation = {
|
|
1654
|
+
"incomplete-coverage": "",
|
|
1655
|
+
"standing-findings": "",
|
|
1656
|
+
"unverified-findings": "the verifier could neither confirm nor reject the remaining findings.",
|
|
1657
|
+
"reviewer-inconclusive": "a reviewer reported its review as inconclusive.",
|
|
1658
|
+
clean: ""
|
|
1659
|
+
};
|
|
1660
|
+
const text = explanation[reason];
|
|
1661
|
+
return text ? `${prefix}: ${text}` : "";
|
|
1662
|
+
}
|
|
1417
1663
|
async function runDeepReview(runner, scope, workspace, options = {}) {
|
|
1418
1664
|
if (scope.error) throw new Error(scope.error);
|
|
1419
1665
|
const target = await resolveReviewTarget(workspace, scope);
|
|
@@ -1514,22 +1760,22 @@ async function runDeepReview(runner, scope, workspace, options = {}) {
|
|
|
1514
1760
|
reviewerCoverage.sort(
|
|
1515
1761
|
(left, right) => REVIEW_LENSES.findIndex((lens) => lens.id === left.id) - REVIEW_LENSES.findIndex((lens) => lens.id === right.id)
|
|
1516
1762
|
);
|
|
1517
|
-
const completeDiscovery = reviewerCoverage.every((entry) => entry.status === "completed");
|
|
1518
1763
|
const candidates = reindex(rankFindings(filterLowConfidence(dedupeFindings(rawFindings))));
|
|
1519
1764
|
if (candidates.length === 0) {
|
|
1765
|
+
const reportCoverage2 = { reviewers: reviewerCoverage };
|
|
1766
|
+
const { verdict: verdict2, reason: reason2 } = deriveReviewVerdict({
|
|
1767
|
+
findings: [],
|
|
1768
|
+
coverage: reportCoverage2,
|
|
1769
|
+
reviewerVerdicts
|
|
1770
|
+
});
|
|
1520
1771
|
const report2 = {
|
|
1521
|
-
verdict:
|
|
1772
|
+
verdict: verdict2,
|
|
1522
1773
|
findings: [],
|
|
1523
|
-
coverage:
|
|
1774
|
+
coverage: reportCoverage2
|
|
1524
1775
|
};
|
|
1525
1776
|
const warning2 = renderCoverageWarnings(reviewerCoverage);
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
report: report2,
|
|
1529
|
-
text: [warning2 || "Deep review complete: no confirmed findings.", ...rawOutputs].join(
|
|
1530
|
-
"\n\n"
|
|
1531
|
-
)
|
|
1532
|
-
};
|
|
1777
|
+
const summary = warning2 || (reason2 === "clean" ? "Deep review complete: no confirmed findings." : renderVerdictReason("Deep review", reason2));
|
|
1778
|
+
return { target, report: report2, text: [summary, ...rawOutputs].join("\n\n") };
|
|
1533
1779
|
}
|
|
1534
1780
|
if (options.signal?.aborted) return cancelledDeepResult(target);
|
|
1535
1781
|
const verifier = await spawnTracked(
|
|
@@ -1556,20 +1802,30 @@ async function runDeepReview(runner, scope, workspace, options = {}) {
|
|
|
1556
1802
|
}
|
|
1557
1803
|
const verified = applyVerification(candidates, verdicts);
|
|
1558
1804
|
const ranked = rankFindings(verified);
|
|
1559
|
-
const
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1805
|
+
const reportCoverage = {
|
|
1806
|
+
reviewers: reviewerCoverage,
|
|
1807
|
+
verifier: verifierCoverage
|
|
1808
|
+
};
|
|
1809
|
+
const { verdict, reason } = deriveReviewVerdict({
|
|
1810
|
+
findings: ranked,
|
|
1811
|
+
coverage: reportCoverage,
|
|
1812
|
+
reviewerVerdicts
|
|
1813
|
+
});
|
|
1563
1814
|
const report = {
|
|
1564
1815
|
verdict,
|
|
1565
1816
|
findings: ranked,
|
|
1566
|
-
coverage:
|
|
1817
|
+
coverage: reportCoverage
|
|
1567
1818
|
};
|
|
1568
1819
|
const warning = renderCoverageWarnings([...reviewerCoverage, verifierCoverage]);
|
|
1569
1820
|
return {
|
|
1570
1821
|
target,
|
|
1571
1822
|
report,
|
|
1572
|
-
text: [
|
|
1823
|
+
text: [
|
|
1824
|
+
warning,
|
|
1825
|
+
renderVerdictReason("Deep review", reason),
|
|
1826
|
+
renderReviewReport(report),
|
|
1827
|
+
...rawOutputs
|
|
1828
|
+
].filter(Boolean).join("\n\n")
|
|
1573
1829
|
};
|
|
1574
1830
|
} catch (error) {
|
|
1575
1831
|
await stopNonTerminal(runner, spawned);
|
|
@@ -1684,7 +1940,15 @@ async function applyReviewFixes(runner, confirmed, options = {}) {
|
|
|
1684
1940
|
);
|
|
1685
1941
|
continue;
|
|
1686
1942
|
}
|
|
1687
|
-
|
|
1943
|
+
let applied;
|
|
1944
|
+
try {
|
|
1945
|
+
applied = await runner.apply(patcher.id, candidate.id, { signal: options.signal });
|
|
1946
|
+
} catch (error) {
|
|
1947
|
+
if (!options.signal?.aborted) throw error;
|
|
1948
|
+
result.cancelled = true;
|
|
1949
|
+
result.messages.push(`Stopped while applying ${finding.file}: ${REVIEW_CANCELLED}.`);
|
|
1950
|
+
break;
|
|
1951
|
+
}
|
|
1688
1952
|
if (applied.status === "applied") {
|
|
1689
1953
|
result.applied++;
|
|
1690
1954
|
result.messages.push(
|
|
@@ -1857,12 +2121,19 @@ function reduceAgentSessionSnapshot(snapshot, event) {
|
|
|
1857
2121
|
case "agent_permission":
|
|
1858
2122
|
case "agent_persistence":
|
|
1859
2123
|
case "skill_lifecycle":
|
|
2124
|
+
case "notice":
|
|
2125
|
+
case "goal_status":
|
|
1860
2126
|
case "background_job_start":
|
|
1861
2127
|
case "background_job_update":
|
|
1862
2128
|
case "background_job_output":
|
|
1863
2129
|
case "background_job_result":
|
|
1864
2130
|
case "background_job_dismiss":
|
|
1865
2131
|
return snapshot;
|
|
2132
|
+
// `agent_notice` is a child's own line for the operator (#305): it is not part of the
|
|
2133
|
+
// child's record and not part of the parent's transcript, so the snapshot drops it and
|
|
2134
|
+
// the TUI reads it from the manager subscription instead (see `useManagedAgents`).
|
|
2135
|
+
case "agent_notice":
|
|
2136
|
+
return snapshot;
|
|
1866
2137
|
case "evidence_update":
|
|
1867
2138
|
return { ...snapshot, evidence: upsertById(snapshot.evidence, event.evidence) };
|
|
1868
2139
|
case "attempt_discarded":
|
|
@@ -1900,6 +2171,10 @@ function reduceAgentSessionSnapshot(snapshot, event) {
|
|
|
1900
2171
|
}
|
|
1901
2172
|
|
|
1902
2173
|
// src/session/resolve.ts
|
|
2174
|
+
var log2 = createDebugLogger("session:resolve");
|
|
2175
|
+
function recordResumeDriver(store, sessionId, recorded, driver) {
|
|
2176
|
+
if (driver === "agent" && recorded !== "agent") store.patchMeta(sessionId, { driver: "agent" });
|
|
2177
|
+
}
|
|
1903
2178
|
function selectSession(store, selector, cwd) {
|
|
1904
2179
|
const exact = store.findByName(selector) ?? store.findById(selector);
|
|
1905
2180
|
if (exact) return exact;
|
|
@@ -1940,6 +2215,25 @@ function persistHistory(store, sessionId, history) {
|
|
|
1940
2215
|
});
|
|
1941
2216
|
}
|
|
1942
2217
|
}
|
|
2218
|
+
function discardUntouchedSession(store, launch) {
|
|
2219
|
+
if (!store || !launch.created) return false;
|
|
2220
|
+
try {
|
|
2221
|
+
return store.removeUntouched?.(launch.sessionId) ?? false;
|
|
2222
|
+
} catch (error) {
|
|
2223
|
+
log2.warn("could not remove the untouched session", {
|
|
2224
|
+
error: error instanceof Error ? error.message : String(error)
|
|
2225
|
+
});
|
|
2226
|
+
return false;
|
|
2227
|
+
}
|
|
2228
|
+
}
|
|
2229
|
+
function discardUntouchedSessions(store, launch, createdSessionIds) {
|
|
2230
|
+
let removed = 0;
|
|
2231
|
+
if (discardUntouchedSession(store, launch)) removed++;
|
|
2232
|
+
for (const sessionId of createdSessionIds) {
|
|
2233
|
+
if (discardUntouchedSession(store, { sessionId, created: true })) removed++;
|
|
2234
|
+
}
|
|
2235
|
+
return removed;
|
|
2236
|
+
}
|
|
1943
2237
|
function resolveSessionBootstrap(store, options) {
|
|
1944
2238
|
if (!store) {
|
|
1945
2239
|
if (options.resume || options.continue) {
|
|
@@ -1970,7 +2264,8 @@ function resolveSessionBootstrap(store, options) {
|
|
|
1970
2264
|
const sessionId2 = store.create({
|
|
1971
2265
|
id: options.sessionId,
|
|
1972
2266
|
cwd: options.cwd,
|
|
1973
|
-
name: options.sessionName
|
|
2267
|
+
name: options.sessionName,
|
|
2268
|
+
driver: options.driver
|
|
1974
2269
|
});
|
|
1975
2270
|
return {
|
|
1976
2271
|
sessionId: sessionId2,
|
|
@@ -1992,7 +2287,8 @@ function resolveSessionBootstrap(store, options) {
|
|
|
1992
2287
|
if (selected) {
|
|
1993
2288
|
const loaded = store.load(selected.id);
|
|
1994
2289
|
if (options.forkSession) {
|
|
1995
|
-
const
|
|
2290
|
+
const driver = loaded.meta.driver === "agent" ? "agent" : options.driver;
|
|
2291
|
+
const sessionId2 = store.fork ? store.fork(selected.id, { cwd: options.cwd, name: options.sessionName, driver }) : store.create({ cwd: options.cwd, name: options.sessionName, driver });
|
|
1996
2292
|
if (!store.fork) persistHistory(store, sessionId2, loaded.transcript);
|
|
1997
2293
|
return {
|
|
1998
2294
|
sessionId: sessionId2,
|
|
@@ -2004,12 +2300,14 @@ function resolveSessionBootstrap(store, options) {
|
|
|
2004
2300
|
rewindTargets: loaded.rewindTargets,
|
|
2005
2301
|
activeEventIds: loaded.activeEventIds,
|
|
2006
2302
|
plan: loaded.plan,
|
|
2303
|
+
goal: loaded.goal,
|
|
2007
2304
|
source: "resume",
|
|
2008
2305
|
persisted: true,
|
|
2009
2306
|
created: true
|
|
2010
2307
|
};
|
|
2011
2308
|
}
|
|
2012
2309
|
store.touch(selected.id);
|
|
2310
|
+
recordResumeDriver(store, selected.id, loaded.meta.driver, options.driver);
|
|
2013
2311
|
return {
|
|
2014
2312
|
sessionId: selected.id,
|
|
2015
2313
|
sessionName: loaded.meta.name,
|
|
@@ -2020,6 +2318,7 @@ function resolveSessionBootstrap(store, options) {
|
|
|
2020
2318
|
rewindTargets: loaded.rewindTargets,
|
|
2021
2319
|
activeEventIds: loaded.activeEventIds,
|
|
2022
2320
|
plan: loaded.plan,
|
|
2321
|
+
goal: loaded.goal,
|
|
2023
2322
|
carriedUsage: loaded.carriedUsage,
|
|
2024
2323
|
carriedModels: loaded.carriedModels,
|
|
2025
2324
|
source: "resume",
|
|
@@ -2027,7 +2326,11 @@ function resolveSessionBootstrap(store, options) {
|
|
|
2027
2326
|
created: false
|
|
2028
2327
|
};
|
|
2029
2328
|
}
|
|
2030
|
-
const sessionId = store.create({
|
|
2329
|
+
const sessionId = store.create({
|
|
2330
|
+
cwd: options.cwd,
|
|
2331
|
+
name: options.sessionName,
|
|
2332
|
+
driver: options.driver
|
|
2333
|
+
});
|
|
2031
2334
|
return {
|
|
2032
2335
|
sessionId,
|
|
2033
2336
|
sessionName: options.sessionName,
|
|
@@ -2044,8 +2347,9 @@ function resolveSessionBootstrap(store, options) {
|
|
|
2044
2347
|
}
|
|
2045
2348
|
|
|
2046
2349
|
// src/input/file-mentions.ts
|
|
2350
|
+
import { stat } from "fs/promises";
|
|
2047
2351
|
import { isAbsolute, relative, resolve as resolve2 } from "path";
|
|
2048
|
-
import
|
|
2352
|
+
import { glob } from "tinyglobby";
|
|
2049
2353
|
var DEFAULT_IGNORE = [
|
|
2050
2354
|
"**/.git/**",
|
|
2051
2355
|
"**/node_modules/**",
|
|
@@ -2066,9 +2370,8 @@ function resolveWorkspaceMentionPath(workspace, mentionPath) {
|
|
|
2066
2370
|
const normalized = mentionPath.replace(/\\/g, "/");
|
|
2067
2371
|
const candidate = isAbsolute(normalized) ? normalized : resolve2(root, normalized);
|
|
2068
2372
|
const filePath = resolve2(candidate);
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
return { filePath, relativePath: normalizeMentionPath(rel) };
|
|
2373
|
+
if (isOutside(root, filePath)) return null;
|
|
2374
|
+
return { filePath, relativePath: normalizeMentionPath(relative(root, filePath)) };
|
|
2072
2375
|
}
|
|
2073
2376
|
function findActiveFileMention(input) {
|
|
2074
2377
|
for (let i = input.length - 1; i >= 0; i--) {
|
|
@@ -2090,21 +2393,50 @@ function replaceActiveFileMention(input, mention, replacementPath) {
|
|
|
2090
2393
|
const mentionText = needsQuotes ? `@"${replacementPath}" ` : `@${replacementPath} `;
|
|
2091
2394
|
return input.slice(0, mention.start) + mentionText + input.slice(mention.end);
|
|
2092
2395
|
}
|
|
2396
|
+
async function fileSizeBytes(filePath) {
|
|
2397
|
+
try {
|
|
2398
|
+
return (await stat(filePath)).size;
|
|
2399
|
+
} catch {
|
|
2400
|
+
return 0;
|
|
2401
|
+
}
|
|
2402
|
+
}
|
|
2403
|
+
function withMissingAncestors(entries) {
|
|
2404
|
+
const reported = new Set(entries.filter((entry) => entry.endsWith("/")));
|
|
2405
|
+
const missing = [];
|
|
2406
|
+
for (const entry of entries) {
|
|
2407
|
+
const path = entry.endsWith("/") ? entry.slice(0, -1) : entry;
|
|
2408
|
+
for (let cut = path.lastIndexOf("/"); cut > 0; cut = path.lastIndexOf("/", cut - 1)) {
|
|
2409
|
+
const ancestor = path.slice(0, cut + 1);
|
|
2410
|
+
if (reported.has(ancestor)) break;
|
|
2411
|
+
reported.add(ancestor);
|
|
2412
|
+
missing.push(ancestor);
|
|
2413
|
+
}
|
|
2414
|
+
}
|
|
2415
|
+
return missing.length > 0 ? [...entries, ...missing] : entries;
|
|
2416
|
+
}
|
|
2093
2417
|
async function getFileMentionCandidates(workspace, query, limit = 50, signal) {
|
|
2418
|
+
const reachesUnc = createUncGuard();
|
|
2419
|
+
if (reachesUnc(workspace)) return [];
|
|
2094
2420
|
const normalizedQuery = normalizeMentionPath(query).toLowerCase();
|
|
2095
2421
|
const gitignore = loadGitignore(workspace).patterns;
|
|
2096
|
-
const ignore = [...DEFAULT_IGNORE, ...gitignore];
|
|
2422
|
+
const ignore = globWalkIgnore([...DEFAULT_IGNORE, ...gitignore]);
|
|
2097
2423
|
let entries;
|
|
2098
2424
|
try {
|
|
2099
|
-
entries =
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
|
|
2103
|
-
|
|
2104
|
-
|
|
2105
|
-
|
|
2106
|
-
|
|
2107
|
-
|
|
2425
|
+
entries = withMissingAncestors(
|
|
2426
|
+
await glob("**/*", {
|
|
2427
|
+
cwd: workspace,
|
|
2428
|
+
dot: true,
|
|
2429
|
+
onlyFiles: false,
|
|
2430
|
+
ignore,
|
|
2431
|
+
// A pattern naming a directory returns that directory, not everything inside it — which is
|
|
2432
|
+
// the whole point of walking for mentions.
|
|
2433
|
+
expandDirectories: false,
|
|
2434
|
+
// The walk follows links: on Windows each one is asked about first, so none to a share is
|
|
2435
|
+
// resolved.
|
|
2436
|
+
fs: uncGuardedWalkFs(workspace, reachesUnc),
|
|
2437
|
+
signal
|
|
2438
|
+
})
|
|
2439
|
+
);
|
|
2108
2440
|
} catch {
|
|
2109
2441
|
return [];
|
|
2110
2442
|
}
|
|
@@ -2112,7 +2444,8 @@ async function getFileMentionCandidates(workspace, query, limit = 50, signal) {
|
|
|
2112
2444
|
const scored = [];
|
|
2113
2445
|
for (let index = 0; index < entries.length; index++) {
|
|
2114
2446
|
const entry = entries[index];
|
|
2115
|
-
const
|
|
2447
|
+
const isDirectory = entry.endsWith("/");
|
|
2448
|
+
const display = normalizeMentionPath(isDirectory ? entry.slice(0, -1) : entry);
|
|
2116
2449
|
const lower = display.toLowerCase();
|
|
2117
2450
|
const base = lower.split("/").pop() ?? lower;
|
|
2118
2451
|
let score = null;
|
|
@@ -2122,11 +2455,10 @@ async function getFileMentionCandidates(workspace, query, limit = 50, signal) {
|
|
|
2122
2455
|
else if (base.startsWith(normalizedQuery)) score = 2;
|
|
2123
2456
|
else if (lower.includes(normalizedQuery)) score = 4;
|
|
2124
2457
|
if (score !== null) {
|
|
2125
|
-
const kind = entry.dirent.isDirectory() ? "directory" : "file";
|
|
2126
2458
|
scored.push({
|
|
2127
|
-
path:
|
|
2128
|
-
kind,
|
|
2129
|
-
desc:
|
|
2459
|
+
path: isDirectory ? `${display}/` : display,
|
|
2460
|
+
kind: isDirectory ? "directory" : "file",
|
|
2461
|
+
desc: "",
|
|
2130
2462
|
score
|
|
2131
2463
|
});
|
|
2132
2464
|
}
|
|
@@ -2138,7 +2470,18 @@ async function getFileMentionCandidates(workspace, query, limit = 50, signal) {
|
|
|
2138
2470
|
return a.path.localeCompare(b.path);
|
|
2139
2471
|
});
|
|
2140
2472
|
throwIfAborted(signal);
|
|
2141
|
-
|
|
2473
|
+
const listed = scored.slice(0, limit);
|
|
2474
|
+
const sizes = await Promise.all(
|
|
2475
|
+
listed.map(
|
|
2476
|
+
(candidate) => candidate.kind === "directory" ? 0 : fileSizeBytes(resolve2(workspace, candidate.path))
|
|
2477
|
+
)
|
|
2478
|
+
);
|
|
2479
|
+
throwIfAborted(signal);
|
|
2480
|
+
return listed.map(({ score: _score, path, kind }, index) => ({
|
|
2481
|
+
path,
|
|
2482
|
+
kind,
|
|
2483
|
+
desc: kind === "directory" ? "directory" : `${sizes[index]} bytes`
|
|
2484
|
+
}));
|
|
2142
2485
|
}
|
|
2143
2486
|
|
|
2144
2487
|
// src/session/lifecycle.ts
|
|
@@ -2154,15 +2497,96 @@ async function runSessionEnd(config, sessionId, reason, options) {
|
|
|
2154
2497
|
await runHooks(
|
|
2155
2498
|
config.settings.hooks.SessionEnd,
|
|
2156
2499
|
"SessionEnd",
|
|
2157
|
-
{
|
|
2500
|
+
{
|
|
2501
|
+
workspace: config.workspace,
|
|
2502
|
+
event: "SessionEnd",
|
|
2503
|
+
sessionId,
|
|
2504
|
+
reason,
|
|
2505
|
+
status: options?.endOutcome?.status,
|
|
2506
|
+
stopReason: options?.endOutcome?.reason
|
|
2507
|
+
},
|
|
2158
2508
|
options
|
|
2159
2509
|
);
|
|
2160
2510
|
}
|
|
2161
2511
|
|
|
2162
2512
|
// src/input/input-expansion.ts
|
|
2163
|
-
import {
|
|
2513
|
+
import { readFileSync as readFileSync2, statSync as statSync2 } from "fs";
|
|
2164
2514
|
import { exec } from "child_process";
|
|
2165
2515
|
import { createHash as createHash2 } from "crypto";
|
|
2516
|
+
import { isAbsolute as isAbsolute2, relative as relative2, resolve as resolve3 } from "path";
|
|
2517
|
+
|
|
2518
|
+
// src/input/committed-files.ts
|
|
2519
|
+
var REPOSITORY_SELECTION = {
|
|
2520
|
+
GIT_DIR: void 0,
|
|
2521
|
+
GIT_WORK_TREE: void 0,
|
|
2522
|
+
GIT_INDEX_FILE: void 0,
|
|
2523
|
+
GIT_OBJECT_DIRECTORY: void 0,
|
|
2524
|
+
GIT_ALTERNATE_OBJECT_DIRECTORIES: void 0,
|
|
2525
|
+
GIT_COMMON_DIR: void 0,
|
|
2526
|
+
GIT_NAMESPACE: void 0,
|
|
2527
|
+
GIT_PREFIX: void 0
|
|
2528
|
+
};
|
|
2529
|
+
var GIT_ENV = { ...REPOSITORY_SELECTION, ...noFetchGitEnv() };
|
|
2530
|
+
var GIT_TIMEOUT_MS = 1e4;
|
|
2531
|
+
function records(stdout) {
|
|
2532
|
+
return stdout.split("\0").filter(Boolean).flatMap((record) => {
|
|
2533
|
+
const tab = record.indexOf(" ");
|
|
2534
|
+
if (tab === -1) return [];
|
|
2535
|
+
return [{ fields: record.slice(0, tab).split(" "), path: record.slice(tab + 1) }];
|
|
2536
|
+
});
|
|
2537
|
+
}
|
|
2538
|
+
async function committedFileStates(repoRoot, paths, signal) {
|
|
2539
|
+
const states = /* @__PURE__ */ new Map();
|
|
2540
|
+
if (paths.length === 0) return states;
|
|
2541
|
+
const { pins, partialClone } = await readRepositoryGitConfig(repoRoot, void 0, {
|
|
2542
|
+
signal,
|
|
2543
|
+
timeoutMs: GIT_TIMEOUT_MS,
|
|
2544
|
+
env: GIT_ENV
|
|
2545
|
+
});
|
|
2546
|
+
const git = async (args) => {
|
|
2547
|
+
try {
|
|
2548
|
+
return (await defaultGitRunner(hardenedGitArgs([...pins, "--literal-pathspecs", ...args]), {
|
|
2549
|
+
cwd: repoRoot,
|
|
2550
|
+
signal,
|
|
2551
|
+
timeoutMs: GIT_TIMEOUT_MS,
|
|
2552
|
+
env: GIT_ENV
|
|
2553
|
+
})).stdout;
|
|
2554
|
+
} catch (error) {
|
|
2555
|
+
if (!(error instanceof Error)) throw error;
|
|
2556
|
+
const message = describeMissingObjects(error.message, partialClone);
|
|
2557
|
+
throw message === error.message ? error : new Error(message, { cause: error });
|
|
2558
|
+
}
|
|
2559
|
+
};
|
|
2560
|
+
const settled = await Promise.allSettled([
|
|
2561
|
+
git(["ls-tree", "-r", "-z", "--full-tree", "HEAD", "--", ...paths]),
|
|
2562
|
+
git(["ls-files", "-z", "--stage", "--full-name", "--", ...paths]),
|
|
2563
|
+
git(["hash-object", "--", ...paths])
|
|
2564
|
+
]);
|
|
2565
|
+
const [tree, index, hashes] = settled.map((result) => {
|
|
2566
|
+
if (result.status === "rejected") throw result.reason;
|
|
2567
|
+
return result.value;
|
|
2568
|
+
});
|
|
2569
|
+
const committed = /* @__PURE__ */ new Map();
|
|
2570
|
+
for (const { fields, path } of records(tree)) {
|
|
2571
|
+
if ((fields[0] === "100644" || fields[0] === "100755") && fields[1] === "blob") {
|
|
2572
|
+
committed.set(path, fields[2]);
|
|
2573
|
+
}
|
|
2574
|
+
}
|
|
2575
|
+
const staged = /* @__PURE__ */ new Map();
|
|
2576
|
+
for (const { fields, path } of records(index)) {
|
|
2577
|
+
if (fields[2] === "0") staged.set(path, fields[1]);
|
|
2578
|
+
}
|
|
2579
|
+
const worktree = hashes.split(/\r?\n/);
|
|
2580
|
+
paths.forEach((path, position) => {
|
|
2581
|
+
const blob = committed.get(path);
|
|
2582
|
+
if (blob === void 0) states.set(path, "untracked");
|
|
2583
|
+
else if (staged.get(path) !== blob || worktree[position] !== blob) states.set(path, "changed");
|
|
2584
|
+
else states.set(path, "unchanged");
|
|
2585
|
+
});
|
|
2586
|
+
return states;
|
|
2587
|
+
}
|
|
2588
|
+
|
|
2589
|
+
// src/input/input-expansion.ts
|
|
2166
2590
|
var AT_MENTION_CHAR_LIMIT = 2e4;
|
|
2167
2591
|
function isMentionBoundary2(input, index) {
|
|
2168
2592
|
if (index === 0) return true;
|
|
@@ -2173,11 +2597,76 @@ function splitTrailingPunctuation(token) {
|
|
|
2173
2597
|
while (end > 0 && /[.,;:!?)]/.test(token[end - 1])) end--;
|
|
2174
2598
|
return { path: token.slice(0, end), trailing: token.slice(end) };
|
|
2175
2599
|
}
|
|
2176
|
-
function
|
|
2600
|
+
function codeRanges(input) {
|
|
2601
|
+
const fenced = fencedCodeRanges(input);
|
|
2602
|
+
const inline = [];
|
|
2603
|
+
let cursor = 0;
|
|
2604
|
+
for (const [start, end] of fenced) {
|
|
2605
|
+
collectParagraphInlineCodeSpans(input.slice(cursor, start), cursor, inline);
|
|
2606
|
+
cursor = end;
|
|
2607
|
+
}
|
|
2608
|
+
collectParagraphInlineCodeSpans(input.slice(cursor), cursor, inline);
|
|
2609
|
+
return [...fenced, ...inline];
|
|
2610
|
+
}
|
|
2611
|
+
function collectParagraphInlineCodeSpans(segment, base, ranges) {
|
|
2612
|
+
const blankLine = /\n[ \t]*\r?\n/g;
|
|
2613
|
+
let start = 0;
|
|
2614
|
+
let separator;
|
|
2615
|
+
while ((separator = blankLine.exec(segment)) !== null) {
|
|
2616
|
+
collectInlineCodeSpans(segment.slice(start, separator.index), base + start, ranges);
|
|
2617
|
+
start = separator.index + separator[0].length;
|
|
2618
|
+
}
|
|
2619
|
+
collectInlineCodeSpans(segment.slice(start), base + start, ranges);
|
|
2620
|
+
}
|
|
2621
|
+
function fencedCodeRanges(input) {
|
|
2622
|
+
const ranges = [];
|
|
2623
|
+
let fence = null;
|
|
2624
|
+
let offset = 0;
|
|
2625
|
+
while (offset < input.length) {
|
|
2626
|
+
const newline = input.indexOf("\n", offset);
|
|
2627
|
+
const lineEnd = newline === -1 ? input.length : newline;
|
|
2628
|
+
const rangeEnd = newline === -1 ? input.length : newline + 1;
|
|
2629
|
+
const line = input.slice(offset, lineEnd).replace(/\r$/, "");
|
|
2630
|
+
if (fence) {
|
|
2631
|
+
const close = /^ {0,3}(`{3,}|~{3,})[ \t]*$/.exec(line);
|
|
2632
|
+
if (close && close[1][0] === fence.char && close[1].length >= fence.length) {
|
|
2633
|
+
ranges.push([fence.start, rangeEnd]);
|
|
2634
|
+
fence = null;
|
|
2635
|
+
}
|
|
2636
|
+
} else {
|
|
2637
|
+
const open = /^ {0,3}(?:(`{3,})(?!.*`)|(~{3,}))/.exec(line);
|
|
2638
|
+
if (open) {
|
|
2639
|
+
const run = open[1] ?? open[2];
|
|
2640
|
+
fence = { start: offset, char: run[0], length: run.length };
|
|
2641
|
+
}
|
|
2642
|
+
}
|
|
2643
|
+
offset = rangeEnd;
|
|
2644
|
+
}
|
|
2645
|
+
if (fence) ranges.push([fence.start, input.length]);
|
|
2646
|
+
return ranges;
|
|
2647
|
+
}
|
|
2648
|
+
function collectInlineCodeSpans(text, base, ranges) {
|
|
2649
|
+
const runs = [...text.matchAll(/`+/g)];
|
|
2650
|
+
for (let i = 0; i < runs.length; i++) {
|
|
2651
|
+
const length = runs[i][0].length;
|
|
2652
|
+
for (let j = i + 1; j < runs.length; j++) {
|
|
2653
|
+
if (runs[j][0].length !== length) continue;
|
|
2654
|
+
ranges.push([base + (runs[i].index ?? 0), base + (runs[j].index ?? 0) + length]);
|
|
2655
|
+
i = j;
|
|
2656
|
+
break;
|
|
2657
|
+
}
|
|
2658
|
+
}
|
|
2659
|
+
}
|
|
2660
|
+
function isInsideRanges(ranges, index) {
|
|
2661
|
+
return ranges.some(([start, end]) => index >= start && index < end);
|
|
2662
|
+
}
|
|
2663
|
+
function findMentionTokens(input, excluded = []) {
|
|
2664
|
+
if (!input.includes("@")) return [];
|
|
2177
2665
|
const tokens = [];
|
|
2666
|
+
const code = codeRanges(input);
|
|
2178
2667
|
let i = 0;
|
|
2179
2668
|
while (i < input.length) {
|
|
2180
|
-
if (input[i] !== "@" || !isMentionBoundary2(input, i)) {
|
|
2669
|
+
if (input[i] !== "@" || !isMentionBoundary2(input, i) || isInsideRanges(code, i) || isInsideRanges(excluded, i)) {
|
|
2181
2670
|
i++;
|
|
2182
2671
|
continue;
|
|
2183
2672
|
}
|
|
@@ -2233,94 +2722,198 @@ function formatMentionError(filePath, reason) {
|
|
|
2233
2722
|
function looksBinary(content) {
|
|
2234
2723
|
return content.includes("\0");
|
|
2235
2724
|
}
|
|
2236
|
-
|
|
2237
|
-
|
|
2238
|
-
|
|
2239
|
-
|
|
2725
|
+
var REFUSAL_TEXT = {
|
|
2726
|
+
outside: "it is outside the workspace and additionalDirectories",
|
|
2727
|
+
unc: "it is, or links to, a network (UNC) path",
|
|
2728
|
+
"no-repository": "a project command inlines only files committed to git, and there is no repository",
|
|
2729
|
+
untracked: "a project command inlines only files committed to git, and git does not track it",
|
|
2730
|
+
changed: "a project command inlines only committed files without local changes, and it has some",
|
|
2731
|
+
"git-failed": "a project command inlines only committed files, and git could not check it"
|
|
2732
|
+
};
|
|
2733
|
+
function formatRefusedMentionNotice(refused) {
|
|
2734
|
+
return `\u26A0 Left @${renderUntrustedText(refused.path, 400)} as written: ${REFUSAL_TEXT[refused.reason]}.`;
|
|
2735
|
+
}
|
|
2736
|
+
function mentionTarget(workspace, mentionPath, additionalRoots) {
|
|
2737
|
+
const inWorkspace = resolveWorkspaceMentionPath(workspace, mentionPath);
|
|
2738
|
+
if (inWorkspace) return inWorkspace;
|
|
2739
|
+
const normalized = mentionPath.replace(/\\/g, "/");
|
|
2740
|
+
if (!isAbsolute2(normalized)) return null;
|
|
2741
|
+
const filePath = resolve3(normalized);
|
|
2742
|
+
if (!additionalRoots.some((root) => !isOutside(root, filePath))) return null;
|
|
2743
|
+
return { filePath, relativePath: normalizeMentionPath(relative2(resolve3(workspace), filePath)) };
|
|
2744
|
+
}
|
|
2745
|
+
function decideMention(workspace, mentionPath, additionalRoots) {
|
|
2746
|
+
const target = mentionTarget(workspace, mentionPath, additionalRoots);
|
|
2747
|
+
if (!target) return null;
|
|
2748
|
+
if (reachesUncPath(target.filePath)) return { kind: "refused", reason: "unc" };
|
|
2749
|
+
const realPath = tryRealPath(target.filePath);
|
|
2750
|
+
if (realPath === void 0) return null;
|
|
2751
|
+
let regular;
|
|
2752
|
+
try {
|
|
2753
|
+
const stat2 = statSync2(realPath);
|
|
2754
|
+
if (stat2.isDirectory()) return null;
|
|
2755
|
+
regular = stat2.isFile();
|
|
2756
|
+
} catch {
|
|
2757
|
+
return null;
|
|
2758
|
+
}
|
|
2759
|
+
const readable = resolveMentionReadablePath(
|
|
2760
|
+
{ workspaceRoot: workspace, additionalRoots },
|
|
2761
|
+
realPath
|
|
2762
|
+
);
|
|
2763
|
+
if (!("path" in readable)) {
|
|
2764
|
+
return { kind: "refused", reason: readable.reason === "unc" ? "unc" : "outside" };
|
|
2765
|
+
}
|
|
2766
|
+
return { kind: "file", realPath, relativePath: target.relativePath, regular };
|
|
2767
|
+
}
|
|
2768
|
+
async function applyProjectCommandRule(workspace, decisions, signal) {
|
|
2769
|
+
const files = decisions.filter(
|
|
2770
|
+
(decision) => decision?.kind === "file"
|
|
2771
|
+
);
|
|
2772
|
+
if (files.length === 0) return decisions;
|
|
2773
|
+
const refuse = (reason) => decisions.map(
|
|
2774
|
+
(decision) => decision?.kind === "file" ? { kind: "refused", reason } : decision
|
|
2775
|
+
);
|
|
2776
|
+
const repoRoot = findRepositoryRoot(workspace);
|
|
2777
|
+
if (repoRoot === void 0) return refuse("no-repository");
|
|
2778
|
+
const inRepository = (realPath) => isOutside(repoRoot, realPath) ? void 0 : relative2(repoRoot, realPath).replace(/\\/g, "/");
|
|
2779
|
+
const paths = /* @__PURE__ */ new Set();
|
|
2780
|
+
for (const file of files) {
|
|
2781
|
+
const path = file.regular ? inRepository(file.realPath) : void 0;
|
|
2782
|
+
if (path !== void 0) paths.add(path);
|
|
2783
|
+
}
|
|
2784
|
+
let states;
|
|
2785
|
+
try {
|
|
2786
|
+
states = await committedFileStates(repoRoot, [...paths], signal);
|
|
2787
|
+
} catch {
|
|
2788
|
+
states = /* @__PURE__ */ new Map();
|
|
2789
|
+
for (const path of paths) {
|
|
2790
|
+
try {
|
|
2791
|
+
states.set(path, (await committedFileStates(repoRoot, [path], signal)).get(path));
|
|
2792
|
+
} catch {
|
|
2793
|
+
states.set(path, "git-failed");
|
|
2794
|
+
}
|
|
2795
|
+
}
|
|
2796
|
+
}
|
|
2797
|
+
return decisions.map((decision) => {
|
|
2798
|
+
if (decision?.kind !== "file") return decision;
|
|
2799
|
+
const path = inRepository(decision.realPath);
|
|
2800
|
+
const state = path === void 0 ? "untracked" : states.get(path) ?? "untracked";
|
|
2801
|
+
return state === "unchanged" ? decision : { kind: "refused", reason: state };
|
|
2802
|
+
});
|
|
2803
|
+
}
|
|
2804
|
+
function inlineMention(token, decision, scope) {
|
|
2805
|
+
if (decision?.kind !== "file") return { text: null };
|
|
2806
|
+
if (!decision.regular) {
|
|
2807
|
+
return { text: formatMentionError(decision.relativePath, "path is not a regular file") };
|
|
2808
|
+
}
|
|
2809
|
+
let bytes;
|
|
2240
2810
|
try {
|
|
2241
|
-
|
|
2242
|
-
|
|
2243
|
-
|
|
2244
|
-
|
|
2245
|
-
|
|
2246
|
-
|
|
2247
|
-
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
|
|
2811
|
+
bytes = readFileSync2(decision.realPath);
|
|
2812
|
+
} catch (error) {
|
|
2813
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2814
|
+
return { text: formatMentionError(token.path, message.slice(0, 200) || "unable to read file") };
|
|
2815
|
+
}
|
|
2816
|
+
const observation = {
|
|
2817
|
+
// Filed the way Read and Edit file and look a file up (`observationPathFor`): canonical, so a
|
|
2818
|
+
// mention through a link, or of a file in an `additionalDirectories` entry, counts as seen.
|
|
2819
|
+
path: observationPathFor(scope.workspace, decision.realPath),
|
|
2820
|
+
workspaceId: scope.workspaceId,
|
|
2821
|
+
sha256: createHash2("sha256").update(bytes).digest("hex"),
|
|
2822
|
+
byteSize: bytes.byteLength,
|
|
2823
|
+
operation: "mention",
|
|
2824
|
+
sourceRef: scope.sourceRef,
|
|
2825
|
+
timestamp: Date.now()
|
|
2826
|
+
};
|
|
2827
|
+
const content = bytes.toString("utf-8");
|
|
2828
|
+
if (looksBinary(content)) {
|
|
2829
|
+
return {
|
|
2830
|
+
text: formatMentionError(decision.relativePath, "file appears to be binary"),
|
|
2831
|
+
observation
|
|
2832
|
+
};
|
|
2833
|
+
}
|
|
2834
|
+
const truncated = content.length > AT_MENTION_CHAR_LIMIT;
|
|
2835
|
+
const body = truncated ? content.slice(0, AT_MENTION_CHAR_LIMIT) : content;
|
|
2836
|
+
const suffix = truncated ? `
|
|
2251
2837
|
|
|
2252
2838
|
[File truncated at ${AT_MENTION_CHAR_LIMIT} characters; use the Read tool for more.]` : "";
|
|
2253
|
-
|
|
2254
|
-
|
|
2839
|
+
return {
|
|
2840
|
+
text: `
|
|
2841
|
+
Contents of ${decision.relativePath}:
|
|
2255
2842
|
|
|
2256
2843
|
\`\`\`
|
|
2257
2844
|
${body}${suffix}
|
|
2258
2845
|
\`\`\`
|
|
2259
|
-
|
|
2260
|
-
|
|
2261
|
-
|
|
2262
|
-
return formatMentionError(filePath, message.slice(0, 200) || "unable to read file");
|
|
2263
|
-
}
|
|
2846
|
+
`,
|
|
2847
|
+
observation
|
|
2848
|
+
};
|
|
2264
2849
|
}
|
|
2265
|
-
function
|
|
2266
|
-
|
|
2267
|
-
if (tokens.length === 0) return input;
|
|
2268
|
-
let output = "";
|
|
2269
|
-
let cursor = 0;
|
|
2270
|
-
for (const token of tokens) {
|
|
2271
|
-
output += input.slice(cursor, token.start);
|
|
2272
|
-
output += expandMention(token.path, workspace);
|
|
2273
|
-
output += token.trailing;
|
|
2274
|
-
cursor = token.end + token.trailing.length;
|
|
2275
|
-
}
|
|
2276
|
-
output += input.slice(cursor);
|
|
2277
|
-
return output;
|
|
2850
|
+
function mentionTokenRanges(input) {
|
|
2851
|
+
return findMentionTokens(input).map((token) => [token.start, token.end]);
|
|
2278
2852
|
}
|
|
2279
|
-
function
|
|
2280
|
-
const
|
|
2281
|
-
|
|
2282
|
-
|
|
2283
|
-
|
|
2284
|
-
|
|
2285
|
-
try {
|
|
2286
|
-
const info = statSync2(resolved.filePath);
|
|
2287
|
-
if (!info.isFile()) continue;
|
|
2288
|
-
const bytes = readFileSync2(resolved.filePath);
|
|
2289
|
-
observations.push({
|
|
2290
|
-
path: resolved.relativePath,
|
|
2291
|
-
workspaceId,
|
|
2292
|
-
sha256: createHash2("sha256").update(bytes).digest("hex"),
|
|
2293
|
-
byteSize: bytes.byteLength,
|
|
2294
|
-
operation: "mention",
|
|
2295
|
-
sourceRef,
|
|
2296
|
-
timestamp: Date.now()
|
|
2297
|
-
});
|
|
2298
|
-
} catch {
|
|
2299
|
-
}
|
|
2300
|
-
}
|
|
2301
|
-
return observations;
|
|
2853
|
+
function findShellLines(input) {
|
|
2854
|
+
const fenced = fencedCodeRanges(input);
|
|
2855
|
+
return [...input.matchAll(/^!(\S.*)$/gm)].filter((match) => !isInsideRanges(fenced, match.index ?? 0)).map((match) => {
|
|
2856
|
+
const start = match.index ?? 0;
|
|
2857
|
+
return { start, end: start + match[0].length, command: match[1] };
|
|
2858
|
+
});
|
|
2302
2859
|
}
|
|
2303
|
-
|
|
2304
|
-
|
|
2305
|
-
|
|
2860
|
+
function shellLineRanges(lines) {
|
|
2861
|
+
return lines.map((line) => [line.start, line.end]);
|
|
2862
|
+
}
|
|
2863
|
+
async function expandUserInputDetailed(input, workspace, options = {}) {
|
|
2864
|
+
const shellLines = options.expandShell && !options.projectCommand ? findShellLines(input) : [];
|
|
2865
|
+
const tokens = findMentionTokens(input, shellLineRanges(shellLines));
|
|
2866
|
+
if (shellLines.length === 0 && tokens.length === 0) {
|
|
2867
|
+
return { text: input, observations: [], refused: [] };
|
|
2868
|
+
}
|
|
2869
|
+
const additionalRoots = options.additionalRoots ?? [];
|
|
2870
|
+
const decide = (token) => decideMention(workspace, token.path, additionalRoots);
|
|
2871
|
+
const decided = options.projectCommand ? await applyProjectCommandRule(workspace, tokens.map(decide), options.signal) : void 0;
|
|
2872
|
+
const scope = {
|
|
2873
|
+
workspace,
|
|
2874
|
+
workspaceId: workspaceIdentity(workspace),
|
|
2875
|
+
sourceRef: options.sourceRef ?? ""
|
|
2876
|
+
};
|
|
2877
|
+
const observations = [];
|
|
2878
|
+
const refused = [];
|
|
2879
|
+
const edits = [
|
|
2880
|
+
...shellLines.map((line) => ({
|
|
2881
|
+
start: line.start,
|
|
2882
|
+
end: line.end,
|
|
2883
|
+
apply: () => executeShellExpansion(line.command, workspace, options.signal)
|
|
2884
|
+
})),
|
|
2885
|
+
...tokens.map((token, index) => ({
|
|
2886
|
+
start: token.start,
|
|
2887
|
+
end: token.end,
|
|
2888
|
+
apply: () => {
|
|
2889
|
+
const decision = decided ? decided[index] : decide(token);
|
|
2890
|
+
if (decision?.kind === "refused")
|
|
2891
|
+
refused.push({ path: token.path, reason: decision.reason });
|
|
2892
|
+
const inlined = inlineMention(token, decision, scope);
|
|
2893
|
+
if (inlined.observation) observations.push(inlined.observation);
|
|
2894
|
+
return inlined.text ?? token.raw;
|
|
2895
|
+
}
|
|
2896
|
+
}))
|
|
2897
|
+
].sort((left, right) => left.start - right.start);
|
|
2306
2898
|
let output = "";
|
|
2307
2899
|
let cursor = 0;
|
|
2308
|
-
for (const
|
|
2309
|
-
|
|
2310
|
-
|
|
2311
|
-
|
|
2312
|
-
output += await executeShellExpansion(command, workspace, signal);
|
|
2313
|
-
cursor = index + match[0].length;
|
|
2900
|
+
for (const edit of edits) {
|
|
2901
|
+
output += input.slice(cursor, edit.start);
|
|
2902
|
+
output += await edit.apply();
|
|
2903
|
+
cursor = edit.end;
|
|
2314
2904
|
}
|
|
2315
|
-
return output + input.slice(cursor);
|
|
2905
|
+
return { text: output + input.slice(cursor), observations, refused };
|
|
2316
2906
|
}
|
|
2317
2907
|
function executeShellExpansion(command, workspace, signal) {
|
|
2318
|
-
return new Promise((
|
|
2908
|
+
return new Promise((resolve4) => {
|
|
2319
2909
|
exec(
|
|
2320
2910
|
command,
|
|
2321
2911
|
{
|
|
2322
2912
|
cwd: workspace,
|
|
2323
2913
|
encoding: "utf8",
|
|
2914
|
+
// The `!command` the user typed is theirs, so it must not inherit the NODE_ENV Book
|
|
2915
|
+
// defaulted for its own renderer.
|
|
2916
|
+
env: buildChildEnv(),
|
|
2324
2917
|
timeout: 1e4,
|
|
2325
2918
|
maxBuffer: 1024 * 1024,
|
|
2326
2919
|
signal,
|
|
@@ -2329,18 +2922,18 @@ function executeShellExpansion(command, workspace, signal) {
|
|
|
2329
2922
|
(error, stdout) => {
|
|
2330
2923
|
if (error) {
|
|
2331
2924
|
const message = error.message.slice(0, 200) || "unknown error";
|
|
2332
|
-
|
|
2925
|
+
resolve4(`(command '${command}' failed: ${message})`);
|
|
2333
2926
|
return;
|
|
2334
2927
|
}
|
|
2335
2928
|
const value = stdout.trim();
|
|
2336
|
-
|
|
2929
|
+
resolve4(value || `(command '${command}' produced no output)`);
|
|
2337
2930
|
}
|
|
2338
2931
|
);
|
|
2339
2932
|
});
|
|
2340
2933
|
}
|
|
2341
2934
|
|
|
2342
2935
|
// src/session/agent-interactions.ts
|
|
2343
|
-
var
|
|
2936
|
+
var log3 = createDebugLoggerWithCounter("session:interactions");
|
|
2344
2937
|
function snapshotOf(permission, planApproval, userQuestions, elicitations) {
|
|
2345
2938
|
return Object.freeze({
|
|
2346
2939
|
pendingPermission: permission ? Object.freeze({ toolCall: permission.toolCall }) : null,
|
|
@@ -2369,11 +2962,11 @@ var AgentInteractionController = class {
|
|
|
2369
2962
|
return () => this.listeners.delete(listener);
|
|
2370
2963
|
}
|
|
2371
2964
|
requestPermission(toolCall) {
|
|
2372
|
-
return new Promise((
|
|
2373
|
-
const request = { toolCall, resolve:
|
|
2965
|
+
return new Promise((resolve4) => {
|
|
2966
|
+
const request = { toolCall, resolve: resolve4 };
|
|
2374
2967
|
if (this.pendingPermission) {
|
|
2375
2968
|
this.pendingPermissionQueue.push(request);
|
|
2376
|
-
|
|
2969
|
+
log3.event("permission:queued", {
|
|
2377
2970
|
tool: toolCall.name,
|
|
2378
2971
|
id: toolCall.id,
|
|
2379
2972
|
queueLength: this.pendingPermissionQueue.length
|
|
@@ -2381,19 +2974,19 @@ var AgentInteractionController = class {
|
|
|
2381
2974
|
} else {
|
|
2382
2975
|
this.pendingPermission = request;
|
|
2383
2976
|
this.publish();
|
|
2384
|
-
|
|
2977
|
+
log3.event("permission:pending", { tool: toolCall.name, id: toolCall.id });
|
|
2385
2978
|
}
|
|
2386
2979
|
});
|
|
2387
2980
|
}
|
|
2388
2981
|
settlePermission(result, via) {
|
|
2389
2982
|
const pending = this.pendingPermission;
|
|
2390
2983
|
if (!pending) {
|
|
2391
|
-
|
|
2984
|
+
log3.event("permission:settled:noop", { reason: "no-pending", result, via });
|
|
2392
2985
|
return false;
|
|
2393
2986
|
}
|
|
2394
2987
|
this.pendingPermission = this.pendingPermissionQueue.shift() ?? null;
|
|
2395
2988
|
this.publish();
|
|
2396
|
-
|
|
2989
|
+
log3.event("permission:settled", {
|
|
2397
2990
|
tool: pending.toolCall.name,
|
|
2398
2991
|
id: pending.toolCall.id,
|
|
2399
2992
|
result,
|
|
@@ -2411,27 +3004,27 @@ var AgentInteractionController = class {
|
|
|
2411
3004
|
this.pendingPermission = null;
|
|
2412
3005
|
this.pendingPermissionQueue = [];
|
|
2413
3006
|
this.publish();
|
|
2414
|
-
for (const request of pending) request.resolve("deny");
|
|
2415
|
-
|
|
3007
|
+
for (const request of pending) request.resolve({ result: "deny", reason: "dismissed" });
|
|
3008
|
+
log3.event("permission:cancelled-all", { via, count: pending.length });
|
|
2416
3009
|
return true;
|
|
2417
3010
|
}
|
|
2418
3011
|
requestPlanApproval(plan) {
|
|
2419
3012
|
if (this.pendingPlanApproval) this.settlePlanApproval("reject", "superseded");
|
|
2420
|
-
return new Promise((
|
|
2421
|
-
this.pendingPlanApproval = { plan, resolve:
|
|
3013
|
+
return new Promise((resolve4) => {
|
|
3014
|
+
this.pendingPlanApproval = { plan, resolve: resolve4 };
|
|
2422
3015
|
this.publish();
|
|
2423
|
-
|
|
3016
|
+
log3.event("plan-approval:pending", { len: plan.length });
|
|
2424
3017
|
});
|
|
2425
3018
|
}
|
|
2426
3019
|
settlePlanApproval(result, via) {
|
|
2427
3020
|
const pending = this.pendingPlanApproval;
|
|
2428
3021
|
if (!pending) {
|
|
2429
|
-
|
|
3022
|
+
log3.event("plan-approval:settled:noop", { reason: "no-pending", result, via });
|
|
2430
3023
|
return false;
|
|
2431
3024
|
}
|
|
2432
3025
|
this.pendingPlanApproval = null;
|
|
2433
3026
|
this.publish();
|
|
2434
|
-
|
|
3027
|
+
log3.event("plan-approval:settled", { result, via, len: pending.plan.length });
|
|
2435
3028
|
pending.resolve(result);
|
|
2436
3029
|
return true;
|
|
2437
3030
|
}
|
|
@@ -2442,10 +3035,10 @@ var AgentInteractionController = class {
|
|
|
2442
3035
|
message: `Duplicate user question request: ${request.id}`
|
|
2443
3036
|
});
|
|
2444
3037
|
}
|
|
2445
|
-
return new Promise((
|
|
2446
|
-
this.pendingUserQuestions = [...this.pendingUserQuestions, { request, resolve:
|
|
3038
|
+
return new Promise((resolve4) => {
|
|
3039
|
+
this.pendingUserQuestions = [...this.pendingUserQuestions, { request, resolve: resolve4 }];
|
|
2447
3040
|
this.publish();
|
|
2448
|
-
|
|
3041
|
+
log3.event("user-question:pending", {
|
|
2449
3042
|
id: request.id,
|
|
2450
3043
|
count: request.questions.length,
|
|
2451
3044
|
queueLength: this.pendingUserQuestions.length,
|
|
@@ -2456,14 +3049,14 @@ var AgentInteractionController = class {
|
|
|
2456
3049
|
settleUserQuestion(result, via, requestId) {
|
|
2457
3050
|
const index = requestId ? this.pendingUserQuestions.findIndex((entry) => entry.request.id === requestId) : 0;
|
|
2458
3051
|
if (index < 0 || this.pendingUserQuestions.length === 0) {
|
|
2459
|
-
|
|
3052
|
+
log3.event("user-question:settled:noop", { reason: "no-pending", via, requestId });
|
|
2460
3053
|
return false;
|
|
2461
3054
|
}
|
|
2462
3055
|
const next = [...this.pendingUserQuestions];
|
|
2463
3056
|
const [pending] = next.splice(index, 1);
|
|
2464
3057
|
this.pendingUserQuestions = next;
|
|
2465
3058
|
this.publish();
|
|
2466
|
-
|
|
3059
|
+
log3.event("user-question:settled", {
|
|
2467
3060
|
id: pending.request.id,
|
|
2468
3061
|
action: result.action,
|
|
2469
3062
|
via,
|
|
@@ -2480,7 +3073,7 @@ var AgentInteractionController = class {
|
|
|
2480
3073
|
for (const entry of pending) {
|
|
2481
3074
|
entry.resolve({ action: "cancel", message: `Question cancelled via ${via}.` });
|
|
2482
3075
|
}
|
|
2483
|
-
|
|
3076
|
+
log3.event("user-question:cancelled-all", { via, count: pending.length });
|
|
2484
3077
|
return pending.length;
|
|
2485
3078
|
}
|
|
2486
3079
|
/**
|
|
@@ -2491,10 +3084,10 @@ var AgentInteractionController = class {
|
|
|
2491
3084
|
if (this.pendingElicitations.some((entry) => entry.request.id === request.id)) {
|
|
2492
3085
|
return Promise.resolve({ action: "cancel" });
|
|
2493
3086
|
}
|
|
2494
|
-
return new Promise((
|
|
2495
|
-
this.pendingElicitations = [...this.pendingElicitations, { request, resolve:
|
|
3087
|
+
return new Promise((resolve4) => {
|
|
3088
|
+
this.pendingElicitations = [...this.pendingElicitations, { request, resolve: resolve4 }];
|
|
2496
3089
|
this.publish();
|
|
2497
|
-
|
|
3090
|
+
log3.event("elicitation:pending", {
|
|
2498
3091
|
id: request.id,
|
|
2499
3092
|
server: request.server,
|
|
2500
3093
|
fields: request.fields.length,
|
|
@@ -2505,14 +3098,14 @@ var AgentInteractionController = class {
|
|
|
2505
3098
|
settleElicitation(result, via, requestId) {
|
|
2506
3099
|
const index = requestId ? this.pendingElicitations.findIndex((entry) => entry.request.id === requestId) : 0;
|
|
2507
3100
|
if (index < 0 || this.pendingElicitations.length === 0) {
|
|
2508
|
-
|
|
3101
|
+
log3.event("elicitation:settled:noop", { reason: "no-pending", via, requestId });
|
|
2509
3102
|
return false;
|
|
2510
3103
|
}
|
|
2511
3104
|
const next = [...this.pendingElicitations];
|
|
2512
3105
|
const [pending] = next.splice(index, 1);
|
|
2513
3106
|
this.pendingElicitations = next;
|
|
2514
3107
|
this.publish();
|
|
2515
|
-
|
|
3108
|
+
log3.event("elicitation:settled", {
|
|
2516
3109
|
id: pending.request.id,
|
|
2517
3110
|
action: result.action,
|
|
2518
3111
|
via,
|
|
@@ -2527,7 +3120,7 @@ var AgentInteractionController = class {
|
|
|
2527
3120
|
this.pendingElicitations = [];
|
|
2528
3121
|
this.publish();
|
|
2529
3122
|
for (const entry of pending) entry.resolve({ action: "cancel" });
|
|
2530
|
-
|
|
3123
|
+
log3.event("elicitation:cancelled-all", { via, count: pending.length });
|
|
2531
3124
|
return pending.length;
|
|
2532
3125
|
}
|
|
2533
3126
|
cancelAll(via) {
|
|
@@ -2594,21 +3187,22 @@ var AgentSessionOperations = class {
|
|
|
2594
3187
|
};
|
|
2595
3188
|
|
|
2596
3189
|
// src/session/agent-session.ts
|
|
2597
|
-
|
|
2598
|
-
|
|
2599
|
-
return
|
|
2600
|
-
|
|
2601
|
-
|
|
2602
|
-
|
|
2603
|
-
|
|
2604
|
-
|
|
2605
|
-
};
|
|
3190
|
+
var log4 = createDebugLogger("session:usage");
|
|
3191
|
+
function persistGoal(target, goal) {
|
|
3192
|
+
if (!target.timelineStore || !target.sessionId) return;
|
|
3193
|
+
const data = { version: 1, goal: goal ? { ...goal } : null };
|
|
3194
|
+
target.timelineStore.append(target.sessionId, {
|
|
3195
|
+
type: "goal",
|
|
3196
|
+
timestamp: Date.now(),
|
|
3197
|
+
data
|
|
3198
|
+
});
|
|
2606
3199
|
}
|
|
2607
3200
|
var AgentSession = class {
|
|
2608
3201
|
interactions = new AgentInteractionController();
|
|
2609
3202
|
operations = new AgentSessionOperations();
|
|
2610
3203
|
runLoop;
|
|
2611
3204
|
compactRunner;
|
|
3205
|
+
judgeRunner;
|
|
2612
3206
|
postCompactHooksRunner;
|
|
2613
3207
|
sessionStartRunner;
|
|
2614
3208
|
sessionEndRunner;
|
|
@@ -2616,12 +3210,27 @@ var AgentSession = class {
|
|
|
2616
3210
|
listeners = /* @__PURE__ */ new Set();
|
|
2617
3211
|
runGeneration = 0;
|
|
2618
3212
|
lifecycleStartedSessionId;
|
|
3213
|
+
lifecycleStart;
|
|
2619
3214
|
lifecycleEndedSessionId;
|
|
3215
|
+
lifecycleEnd;
|
|
2620
3216
|
runtime;
|
|
2621
3217
|
registryFactory;
|
|
3218
|
+
/**
|
|
3219
|
+
* Every root this session has run, and where its spend is written (#336).
|
|
3220
|
+
*
|
|
3221
|
+
* `onUsage` persists what a run's own responses report, which leaves two
|
|
3222
|
+
* shapes of spend with nowhere to go: whatever a managed child spent after the
|
|
3223
|
+
* root's last response, and a root whose model was never called at all — a
|
|
3224
|
+
* `/review` the host performed. Neither reaches a store on the way out, so the
|
|
3225
|
+
* process ends holding tokens no later process restores, and `--max-budget-usd`
|
|
3226
|
+
* re-authorises them. Registered by `run()` and by hosts that create a root
|
|
3227
|
+
* themselves (`trackRunUsage`), and drained by `dispose`.
|
|
3228
|
+
*/
|
|
3229
|
+
usageTargets = /* @__PURE__ */ new Map();
|
|
2622
3230
|
constructor(dependencies = {}) {
|
|
2623
3231
|
this.runLoop = dependencies.runLoop ?? runAgentLoop;
|
|
2624
3232
|
this.compactRunner = dependencies.compactRunner ?? runCompact;
|
|
3233
|
+
this.judgeRunner = dependencies.judgeRunner ?? judgeCompaction;
|
|
2625
3234
|
this.postCompactHooksRunner = dependencies.postCompactHooksRunner ?? runPostCompactHooks;
|
|
2626
3235
|
this.sessionStartRunner = dependencies.sessionStartRunner ?? runSessionStart;
|
|
2627
3236
|
this.sessionEndRunner = dependencies.sessionEndRunner ?? runSessionEnd;
|
|
@@ -2674,6 +3283,11 @@ var AgentSession = class {
|
|
|
2674
3283
|
contextMessage: request.contextMessage,
|
|
2675
3284
|
userMessage,
|
|
2676
3285
|
sessionName: request.sessionName,
|
|
3286
|
+
sessionNameSource: request.sessionNameSource,
|
|
3287
|
+
sessionNameExact: request.sessionNameExact,
|
|
3288
|
+
expandShellInput: request.expandShellInput,
|
|
3289
|
+
projectCommand: request.projectCommand,
|
|
3290
|
+
typedText: request.typedText,
|
|
2677
3291
|
snapshotStore: request.snapshotStore,
|
|
2678
3292
|
timelineStore: request.timelineStore,
|
|
2679
3293
|
signal: control.signal,
|
|
@@ -2719,6 +3333,7 @@ var AgentSession = class {
|
|
|
2719
3333
|
...request.options,
|
|
2720
3334
|
runtime,
|
|
2721
3335
|
displayMessage: request.displayMessage,
|
|
3336
|
+
typedText: request.typedText,
|
|
2722
3337
|
userMessageId: userMessage.id,
|
|
2723
3338
|
userMessageTimestamp: userMessage.timestamp,
|
|
2724
3339
|
userFileObservations: userMessage.fileObservations,
|
|
@@ -2766,9 +3381,61 @@ var AgentSession = class {
|
|
|
2766
3381
|
getRuntime() {
|
|
2767
3382
|
return this.runtime;
|
|
2768
3383
|
}
|
|
3384
|
+
/** The session's `/goal`, if one is set. */
|
|
3385
|
+
getGoal() {
|
|
3386
|
+
return this.runtime.activeGoal;
|
|
3387
|
+
}
|
|
3388
|
+
/**
|
|
3389
|
+
* Set the session's goal, replacing any earlier one. The next time the agent
|
|
3390
|
+
* stops without calling a tool, the loop checks the condition (`agent/goal.ts`).
|
|
3391
|
+
* Taking effect on the runtime means a goal set mid-run applies to that run.
|
|
3392
|
+
*/
|
|
3393
|
+
setGoal(condition, target = {}) {
|
|
3394
|
+
const goal = createGoal(condition);
|
|
3395
|
+
this.runtime.activeGoal = goal;
|
|
3396
|
+
persistGoal(target, goal);
|
|
3397
|
+
return goal;
|
|
3398
|
+
}
|
|
3399
|
+
/**
|
|
3400
|
+
* Carry a goal into a conversation that started fresh -- a plan handoff reseeds a new
|
|
3401
|
+
* session to implement the plan the goal was guiding -- keeping its progress.
|
|
3402
|
+
*/
|
|
3403
|
+
adoptGoal(goal, target = {}) {
|
|
3404
|
+
this.runtime.activeGoal = { ...goal };
|
|
3405
|
+
persistGoal(target, this.runtime.activeGoal);
|
|
3406
|
+
}
|
|
3407
|
+
/** Clear the session's goal. Returns the goal that was cleared, if there was one. */
|
|
3408
|
+
clearGoal(target = {}) {
|
|
3409
|
+
const goal = this.runtime.activeGoal;
|
|
3410
|
+
if (!goal) return void 0;
|
|
3411
|
+
this.runtime.activeGoal = void 0;
|
|
3412
|
+
persistGoal(target, null);
|
|
3413
|
+
return goal;
|
|
3414
|
+
}
|
|
3415
|
+
/** Put back an unmet goal a resumed session recorded. Call after the runtime it belongs on is installed. */
|
|
3416
|
+
restoreGoal(record) {
|
|
3417
|
+
this.runtime.activeGoal = restoreGoal(record);
|
|
3418
|
+
}
|
|
3419
|
+
/**
|
|
3420
|
+
* Stop the outgoing runtime, write what it spent, and install its replacement.
|
|
3421
|
+
*
|
|
3422
|
+
* The one place a runtime goes away, so it is where its spend is written: the
|
|
3423
|
+
* children are stopped first, so a managed agent charged during the teardown is
|
|
3424
|
+
* in the figures the flush reads, and its targets are released with it rather
|
|
3425
|
+
* than walked by every later sweep. A `/resume` or `/rewind` that reached the
|
|
3426
|
+
* replacement without this left the outgoing runtime's unpersisted spend behind
|
|
3427
|
+
* with it, and the resumed session restored a carry short of what it had cost.
|
|
3428
|
+
*/
|
|
2769
3429
|
replaceRuntime(options = {}, via = "session_transition") {
|
|
2770
|
-
this.runtime
|
|
2771
|
-
this.runtime
|
|
3430
|
+
const outgoing = this.runtime;
|
|
3431
|
+
outgoing.agentManager?.handOffLateApplications(() => this.runtime.agentManager);
|
|
3432
|
+
outgoing.dispose(via);
|
|
3433
|
+
this.releaseRunUsage(outgoing);
|
|
3434
|
+
this.runtime = new SessionRuntime({
|
|
3435
|
+
...options,
|
|
3436
|
+
// The managers this session disposed, the outgoing one included, own nothing (#568).
|
|
3437
|
+
disposedAgentInstanceIds: options.disposedAgentInstanceIds ?? outgoing.disposedAgentInstanceIds
|
|
3438
|
+
});
|
|
2772
3439
|
return this.runtime;
|
|
2773
3440
|
}
|
|
2774
3441
|
subscribe(listener) {
|
|
@@ -2793,16 +3460,165 @@ var AgentSession = class {
|
|
|
2793
3460
|
this.operations.reset({ bookTerminalReason: "session_disposed" });
|
|
2794
3461
|
this.runGeneration++;
|
|
2795
3462
|
this.runtime.dispose(via);
|
|
3463
|
+
this.releaseRunUsage(this.runtime);
|
|
3464
|
+
}
|
|
3465
|
+
/**
|
|
3466
|
+
* Record that `rootRunId`'s spend belongs in `target`, so the flush at the end
|
|
3467
|
+
* of the session can persist what no response of that root reported.
|
|
3468
|
+
*
|
|
3469
|
+
* `run()` registers every root it runs. A host that starts a root itself and
|
|
3470
|
+
* never sends a turn — a print-mode `/review`, whose reviewer agents spend
|
|
3471
|
+
* under a root the root model is never asked for — registers it here, or its
|
|
3472
|
+
* spend leaves the process with it. Re-registering a root replaces the target,
|
|
3473
|
+
* so a run under an existing root keeps writing to where that root writes.
|
|
3474
|
+
*/
|
|
3475
|
+
trackRunUsage(rootRunId, target) {
|
|
3476
|
+
this.usageTargets.set(rootRunId, { target, runtime: this.runtime });
|
|
3477
|
+
}
|
|
3478
|
+
/**
|
|
3479
|
+
* Register a compaction's root the way `run` registers a turn's.
|
|
3480
|
+
*
|
|
3481
|
+
* A compactor's model calls are charged to a run context, and no turn of that
|
|
3482
|
+
* root may ever run — a manual `/compact` mints a root of its own, and a send
|
|
3483
|
+
* cancelled between the host's auto-compact in `beforePrepare` and `run()`
|
|
3484
|
+
* never reaches `run`'s own registration. Without this, that spend is charged
|
|
3485
|
+
* to a root no flush knows about, and leaves the process unwritten.
|
|
3486
|
+
*/
|
|
3487
|
+
registerUsageTarget(request, runtime) {
|
|
3488
|
+
if (!request.runContext || !request.timelineStore || !request.sessionId) return;
|
|
3489
|
+
this.usageTargets.set(request.runContext.rootRunId, {
|
|
3490
|
+
target: { sessionId: request.sessionId, timelineStore: request.timelineStore },
|
|
3491
|
+
runtime
|
|
3492
|
+
});
|
|
3493
|
+
}
|
|
3494
|
+
/**
|
|
3495
|
+
* Write what a root has spent and no `usage` record covers, as one record
|
|
3496
|
+
* named after the response the host is being told about.
|
|
3497
|
+
*
|
|
3498
|
+
* The `onUsage` writer, and the reason it is one function rather than a
|
|
3499
|
+
* closure: every host reaches it the same way, so the watermark it moves and
|
|
3500
|
+
* the ones `flushRunUsage` moves are the same figure, written once. Cost is
|
|
3501
|
+
* not stored — pricing changes between processes, so it is re-derived from the
|
|
3502
|
+
* tokens at bootstrap, deliberately at the most expensive model involved.
|
|
3503
|
+
*
|
|
3504
|
+
* The watermark moves only where a record really lands. A turn that reaches no
|
|
3505
|
+
* store — a session that moved on, a run with nowhere to write — leaves its
|
|
3506
|
+
* spend unpersisted instead, for the next writer under this root to record;
|
|
3507
|
+
* counted as written, no record would ever hold it and no restart would
|
|
3508
|
+
* restore it.
|
|
3509
|
+
*/
|
|
3510
|
+
persistReportedUsage(rootRunId, reported, metadata) {
|
|
3511
|
+
const entry = this.usageTargets.get(rootRunId);
|
|
3512
|
+
if (!entry) return;
|
|
3513
|
+
const { target, runtime } = entry;
|
|
3514
|
+
const recordUsage = runtime.runAccounting.peekUnpersistedUsage(rootRunId) ?? reported;
|
|
3515
|
+
if (isSpendlessUsage(recordUsage)) return;
|
|
3516
|
+
const store = target.isCurrent?.() === false ? void 0 : target.timelineStore;
|
|
3517
|
+
if (!store) return;
|
|
3518
|
+
store.append(target.sessionId, {
|
|
3519
|
+
type: "usage",
|
|
3520
|
+
timestamp: Date.now(),
|
|
3521
|
+
data: {
|
|
3522
|
+
version: 1,
|
|
3523
|
+
usage: recordUsage,
|
|
3524
|
+
// The response that triggered this write, which is not the same thing as
|
|
3525
|
+
// the models the delta covers: a dearer child may have finished before it.
|
|
3526
|
+
requestedModel: metadata?.requestedModel,
|
|
3527
|
+
responseModel: metadata?.responseModel,
|
|
3528
|
+
models: runtime.runAccounting.modelsFor(rootRunId)
|
|
3529
|
+
}
|
|
3530
|
+
});
|
|
3531
|
+
runtime.runAccounting.commitPersistedUsage(rootRunId);
|
|
3532
|
+
}
|
|
3533
|
+
/**
|
|
3534
|
+
* Write what a root has spent and no `usage` record covers, if it can be
|
|
3535
|
+
* written at all.
|
|
3536
|
+
*
|
|
3537
|
+
* One record for the whole delta, so what this writes and what it then commits
|
|
3538
|
+
* are the same figure by construction. It is named after the dearest model the
|
|
3539
|
+
* root has spent on rather than after any one of them: `carriedCostUsd`
|
|
3540
|
+
* (`headless.ts`) prices a restored carry at the most expensive model in
|
|
3541
|
+
* `carriedModels` regardless of which record said what, and the records do not
|
|
3542
|
+
* attribute tokens to models, so one name is all the restored pool can be said
|
|
3543
|
+
* to have cost.
|
|
3544
|
+
*
|
|
3545
|
+
* `requireCurrent` is the end of a run, where the target's own currency still
|
|
3546
|
+
* decides whether it is the session to write to. The end-of-session sweep does
|
|
3547
|
+
* not pass it: by then every operation lease has been released, so consulting
|
|
3548
|
+
* one would skip every target the TUI ever registered — which is where a
|
|
3549
|
+
* background agent's late spend goes.
|
|
3550
|
+
*
|
|
3551
|
+
* A store that throws is contained here. This runs from a run's `finally` and
|
|
3552
|
+
* from the runtime swap, so an escaping filesystem error would replace a
|
|
3553
|
+
* completed run's outcome, abort the sweep part-way, and leave the session
|
|
3554
|
+
* installed on a disposed runtime. Nothing was written, so the watermark stays
|
|
3555
|
+
* where it was and the spend remains owed to the next writer that can.
|
|
3556
|
+
*/
|
|
3557
|
+
flushRunUsage(rootRunId, requireCurrent) {
|
|
3558
|
+
const entry = this.usageTargets.get(rootRunId);
|
|
3559
|
+
if (!entry) return;
|
|
3560
|
+
const { target, runtime } = entry;
|
|
3561
|
+
const recordUsage = runtime.runAccounting.peekUnpersistedUsage(rootRunId);
|
|
3562
|
+
if (!recordUsage || isSpendlessUsage(recordUsage)) return;
|
|
3563
|
+
const store = requireCurrent && target.isCurrent?.() === false ? void 0 : target.timelineStore;
|
|
3564
|
+
if (!store) return;
|
|
3565
|
+
try {
|
|
3566
|
+
store.append(target.sessionId, {
|
|
3567
|
+
type: "usage",
|
|
3568
|
+
timestamp: Date.now(),
|
|
3569
|
+
data: {
|
|
3570
|
+
version: 1,
|
|
3571
|
+
usage: recordUsage,
|
|
3572
|
+
requestedModel: runtime.runAccounting.dearestModel(rootRunId),
|
|
3573
|
+
models: runtime.runAccounting.modelsFor(rootRunId)
|
|
3574
|
+
}
|
|
3575
|
+
});
|
|
3576
|
+
runtime.runAccounting.commitPersistedUsage(rootRunId);
|
|
3577
|
+
} catch (error) {
|
|
3578
|
+
log4.warn("usage record could not be appended; spend left unpersisted", {
|
|
3579
|
+
rootRunId,
|
|
3580
|
+
sessionId: target.sessionId,
|
|
3581
|
+
error: error instanceof Error ? error.message : String(error)
|
|
3582
|
+
});
|
|
3583
|
+
}
|
|
3584
|
+
}
|
|
3585
|
+
/**
|
|
3586
|
+
* Write what the roots of a runtime that is going away spent, and drop their
|
|
3587
|
+
* targets: nothing can be charged to a root whose runtime is gone, so keeping
|
|
3588
|
+
* its target would only make every later sweep walk it.
|
|
3589
|
+
*
|
|
3590
|
+
* Whatever lease a target carries is ignored — a `send()` lease has been
|
|
3591
|
+
* released by the time any of these runs, so consulting one would skip every
|
|
3592
|
+
* target the TUI ever made.
|
|
3593
|
+
*/
|
|
3594
|
+
releaseRunUsage(runtime) {
|
|
3595
|
+
for (const [rootRunId, entry] of [...this.usageTargets.entries()]) {
|
|
3596
|
+
if (entry.runtime !== runtime) continue;
|
|
3597
|
+
this.flushRunUsage(rootRunId, false);
|
|
3598
|
+
this.usageTargets.delete(rootRunId);
|
|
3599
|
+
}
|
|
2796
3600
|
}
|
|
2797
3601
|
async startLifecycle(config, sessionId, source, options) {
|
|
2798
|
-
if (this.lifecycleStartedSessionId === sessionId)
|
|
3602
|
+
if (this.lifecycleStartedSessionId === sessionId) {
|
|
3603
|
+
await this.lifecycleStart?.catch(() => void 0);
|
|
3604
|
+
return;
|
|
3605
|
+
}
|
|
2799
3606
|
this.lifecycleStartedSessionId = sessionId;
|
|
2800
|
-
|
|
3607
|
+
this.lifecycleStart = this.sessionStartRunner(config, sessionId, source, options);
|
|
3608
|
+
await this.lifecycleStart;
|
|
2801
3609
|
}
|
|
3610
|
+
// A second call for a session that is already ending waits for the SessionEnd in flight
|
|
3611
|
+
// instead of returning at once, which let a second exit or a `/clear` racing an exit move on
|
|
3612
|
+
// while the hooks were still running. The first caller reports a failed SessionEnd; a caller that
|
|
3613
|
+
// only waited for it does not report the same failure a second time.
|
|
2802
3614
|
async endLifecycle(config, sessionId, reason, options) {
|
|
2803
|
-
if (this.lifecycleEndedSessionId === sessionId)
|
|
3615
|
+
if (this.lifecycleEndedSessionId === sessionId) {
|
|
3616
|
+
await this.lifecycleEnd?.catch(() => void 0);
|
|
3617
|
+
return;
|
|
3618
|
+
}
|
|
2804
3619
|
this.lifecycleEndedSessionId = sessionId;
|
|
2805
|
-
|
|
3620
|
+
this.lifecycleEnd = this.sessionEndRunner(config, sessionId, reason, options);
|
|
3621
|
+
await this.lifecycleEnd;
|
|
2806
3622
|
}
|
|
2807
3623
|
async clearSession(request) {
|
|
2808
3624
|
request.onTransitionStart?.();
|
|
@@ -2811,9 +3627,20 @@ var AgentSession = class {
|
|
|
2811
3627
|
if (request.previousName && request.store) {
|
|
2812
3628
|
request.store.patchMeta(request.currentSessionId, { name: request.previousName });
|
|
2813
3629
|
}
|
|
2814
|
-
const sessionId = request.store ? request.store.create({
|
|
3630
|
+
const sessionId = request.store ? request.store.create({
|
|
3631
|
+
cwd: request.config.workspace,
|
|
3632
|
+
driver: request.config.sessionDriver
|
|
3633
|
+
}) : request.timelineStore?.create({
|
|
3634
|
+
cwd: request.config.workspace,
|
|
3635
|
+
driver: request.config.sessionDriver
|
|
3636
|
+
}) ?? crypto.randomUUID();
|
|
3637
|
+
if (request.store) request.createdSessions?.add(sessionId);
|
|
2815
3638
|
if (request.store && request.timelineStore && request.timelineStore !== request.store) {
|
|
2816
|
-
request.timelineStore.create({
|
|
3639
|
+
request.timelineStore.create({
|
|
3640
|
+
id: sessionId,
|
|
3641
|
+
cwd: request.config.workspace,
|
|
3642
|
+
driver: request.config.sessionDriver
|
|
3643
|
+
});
|
|
2817
3644
|
}
|
|
2818
3645
|
const bootstrap = emptySessionBootstrap(
|
|
2819
3646
|
sessionId,
|
|
@@ -2839,6 +3666,12 @@ var AgentSession = class {
|
|
|
2839
3666
|
this.operations.cancel({ bookTerminalReason: "session_replaced" });
|
|
2840
3667
|
await this.endLifecycle(request.config, request.currentSessionId, "resume");
|
|
2841
3668
|
request.store.touch(selected.id);
|
|
3669
|
+
recordResumeDriver(
|
|
3670
|
+
request.store,
|
|
3671
|
+
selected.id,
|
|
3672
|
+
loaded.meta.driver,
|
|
3673
|
+
request.config.sessionDriver
|
|
3674
|
+
);
|
|
2842
3675
|
const bootstrap = {
|
|
2843
3676
|
sessionId: selected.id,
|
|
2844
3677
|
sessionName: loaded.meta.name,
|
|
@@ -2852,11 +3685,14 @@ var AgentSession = class {
|
|
|
2852
3685
|
// user who wrote a plan, switched away and came back resumed a half-finished
|
|
2853
3686
|
// objective with an empty task list and no notice that it had been dropped.
|
|
2854
3687
|
plan: loaded.plan,
|
|
3688
|
+
goal: loaded.goal,
|
|
2855
3689
|
source: "resume",
|
|
2856
3690
|
persisted: true,
|
|
2857
3691
|
created: false
|
|
2858
3692
|
};
|
|
2859
3693
|
this.reset("session-resume");
|
|
3694
|
+
const resumed = this.replaceRuntime({}, "session-resume");
|
|
3695
|
+
seedShellIds(resumed.backgroundShells, loaded.transcript, loaded.contextHistory);
|
|
2860
3696
|
request.onTransition?.(bootstrap);
|
|
2861
3697
|
await this.startLifecycle(request.config, selected.id, "resume");
|
|
2862
3698
|
return { status: "transitioned", bootstrap };
|
|
@@ -2877,10 +3713,13 @@ var AgentSession = class {
|
|
|
2877
3713
|
gitHead: capture.gitHead,
|
|
2878
3714
|
codeUnavailableReason: capture.reason
|
|
2879
3715
|
};
|
|
3716
|
+
const rewindPrompt = request.typedText ?? request.displayMessage;
|
|
3717
|
+
const promptTyped = request.typedText !== void 0;
|
|
2880
3718
|
const rewindTarget = {
|
|
2881
3719
|
id: checkpointId,
|
|
2882
3720
|
userEventId: request.userMessage.id,
|
|
2883
|
-
prompt:
|
|
3721
|
+
prompt: rewindPrompt,
|
|
3722
|
+
...promptTyped ? { promptTyped } : {},
|
|
2884
3723
|
attachments: request.userMessage.attachments,
|
|
2885
3724
|
timestamp: checkpointTimestamp,
|
|
2886
3725
|
...checkpoint,
|
|
@@ -2894,23 +3733,32 @@ var AgentSession = class {
|
|
|
2894
3733
|
version: 1,
|
|
2895
3734
|
checkpointId,
|
|
2896
3735
|
userEventId: request.userMessage.id,
|
|
2897
|
-
prompt:
|
|
3736
|
+
prompt: rewindPrompt,
|
|
3737
|
+
...promptTyped ? { promptTyped } : {},
|
|
2898
3738
|
attachments: request.userMessage.attachments,
|
|
2899
3739
|
checkpoint
|
|
2900
3740
|
}
|
|
2901
3741
|
});
|
|
2902
|
-
const { contextMessage, sessionName } = await this.recordUserMessage(request);
|
|
2903
|
-
return { status: "prepared", contextMessage, rewindTarget, sessionName };
|
|
3742
|
+
const { contextMessage, sessionName, mentionNotices } = await this.recordUserMessage(request);
|
|
3743
|
+
return { status: "prepared", contextMessage, rewindTarget, sessionName, mentionNotices };
|
|
2904
3744
|
}
|
|
2905
3745
|
async recordUserMessage(request) {
|
|
2906
|
-
const
|
|
2907
|
-
|
|
3746
|
+
const expansion = request.contextMessage !== void 0 ? void 0 : await expandUserInputDetailed(request.displayMessage, request.config.workspace, {
|
|
3747
|
+
expandShell: request.expandShellInput === true,
|
|
3748
|
+
signal: request.signal,
|
|
3749
|
+
sourceRef: request.userMessage.id,
|
|
3750
|
+
// The roots Read serves besides the workspace, resolved as the agent loop does, and
|
|
3751
|
+
// only for text that can hold a mention at all.
|
|
3752
|
+
additionalRoots: request.displayMessage.includes("@") ? resolveAdditionalRoots(
|
|
3753
|
+
request.config.workspace,
|
|
3754
|
+
request.config.settings.additionalDirectories
|
|
3755
|
+
) : [],
|
|
3756
|
+
projectCommand: request.projectCommand
|
|
3757
|
+
});
|
|
3758
|
+
const contextMessage = expansion?.text ?? request.contextMessage ?? request.displayMessage;
|
|
3759
|
+
const mentionNotices = (expansion?.refused ?? []).map(formatRefusedMentionNotice);
|
|
2908
3760
|
request.userMessage.contextContent = contextMessage === request.displayMessage ? void 0 : contextMessage;
|
|
2909
|
-
request.userMessage.fileObservations =
|
|
2910
|
-
request.displayMessage,
|
|
2911
|
-
request.config.workspace,
|
|
2912
|
-
request.userMessage.id
|
|
2913
|
-
) : [];
|
|
3761
|
+
request.userMessage.fileObservations = expansion?.observations ?? [];
|
|
2914
3762
|
const observationLedger = (request.runtime ?? this.runtime).fileObservationLedger;
|
|
2915
3763
|
for (const observation of request.userMessage.fileObservations) {
|
|
2916
3764
|
observationLedger.set(observationKey(observation.workspaceId, observation.path), observation);
|
|
@@ -2934,40 +3782,139 @@ var AgentSession = class {
|
|
|
2934
3782
|
}
|
|
2935
3783
|
});
|
|
2936
3784
|
if (request.timelineStore) request.onUserMessagePersisted?.();
|
|
2937
|
-
|
|
3785
|
+
if (request.userMessage.kind === "agent-notification") {
|
|
3786
|
+
return { contextMessage, sessionName: request.sessionName, mentionNotices };
|
|
3787
|
+
}
|
|
3788
|
+
const sessionName = request.sessionName?.trim() || request.sessionNameExact?.trim() || deriveSessionName(request.sessionNameSource ?? request.displayMessage);
|
|
2938
3789
|
if (!request.sessionName?.trim()) {
|
|
2939
3790
|
request.timelineStore?.patchMeta?.(request.sessionId, { name: sessionName });
|
|
2940
3791
|
}
|
|
2941
|
-
return { contextMessage, sessionName };
|
|
3792
|
+
return { contextMessage, sessionName, mentionNotices };
|
|
2942
3793
|
}
|
|
2943
3794
|
async compact(request) {
|
|
2944
3795
|
const runtime = request.runtime ?? this.runtime;
|
|
2945
3796
|
if (request.runContext) runtime.runAccounting.startRoot(request.runContext);
|
|
3797
|
+
this.registerUsageTarget(request, runtime);
|
|
2946
3798
|
const result = await this.compactRunner(request.config, request.history, {
|
|
2947
|
-
...request.options,
|
|
2948
|
-
sessionId: request.sessionId
|
|
2949
|
-
|
|
2950
|
-
|
|
2951
|
-
|
|
2952
|
-
|
|
2953
|
-
|
|
2954
|
-
|
|
2955
|
-
|
|
2956
|
-
|
|
2957
|
-
|
|
2958
|
-
|
|
2959
|
-
|
|
2960
|
-
|
|
2961
|
-
|
|
2962
|
-
|
|
2963
|
-
|
|
2964
|
-
|
|
2965
|
-
|
|
3799
|
+
...this.accountedOptions(request.options, request.runContext, runtime, request.onUsage),
|
|
3800
|
+
sessionId: request.sessionId
|
|
3801
|
+
});
|
|
3802
|
+
if (result.status !== "compacted") return { result };
|
|
3803
|
+
if (request.isCurrent?.() === false) return { result };
|
|
3804
|
+
return this.commitCompactResult(result, request);
|
|
3805
|
+
}
|
|
3806
|
+
/**
|
|
3807
|
+
* Deferred compaction (`plans/async-compaction-plan.md`): the reducer half of
|
|
3808
|
+
* `compact`, on a snapshot, without the record, the boundary or the
|
|
3809
|
+
* PostCompact hooks. PreCompact hooks still run -- they are the refusal
|
|
3810
|
+
* point, and the suspect-input scan they see is the snapshot's.
|
|
3811
|
+
*/
|
|
3812
|
+
async prepareCompact(request) {
|
|
3813
|
+
const runtime = request.runtime ?? this.runtime;
|
|
3814
|
+
if (request.runContext) runtime.runAccounting.startRoot(request.runContext);
|
|
3815
|
+
this.registerUsageTarget(request, runtime);
|
|
3816
|
+
const result = await this.compactRunner(request.config, request.history, {
|
|
3817
|
+
...this.accountedOptions(request.options, request.runContext, runtime, request.onUsage),
|
|
3818
|
+
sessionId: request.sessionId
|
|
3819
|
+
});
|
|
3820
|
+
if (result.status !== "compacted") return { status: result.status, result };
|
|
3821
|
+
return {
|
|
3822
|
+
status: "prepared",
|
|
3823
|
+
prepared: {
|
|
3824
|
+
snapshot: [...request.history],
|
|
3825
|
+
result,
|
|
3826
|
+
trigger: request.options.trigger,
|
|
3827
|
+
preContextTokens: request.options.preContextTokens
|
|
3828
|
+
}
|
|
3829
|
+
};
|
|
3830
|
+
}
|
|
3831
|
+
/**
|
|
3832
|
+
* The other half: apply a prepared compaction to the history as it stands,
|
|
3833
|
+
* ask the judge whether the checkpoint holds what the steps taken meanwhile
|
|
3834
|
+
* relied on, and only then write the record and run the hooks. A rejected
|
|
3835
|
+
* or inapplicable checkpoint is dropped, and the caller falls back to a
|
|
3836
|
+
* synchronous compaction.
|
|
3837
|
+
*/
|
|
3838
|
+
async commitCompact(request) {
|
|
3839
|
+
const runtime = request.runtime ?? this.runtime;
|
|
3840
|
+
const { prepared } = request;
|
|
3841
|
+
const applied = applyCompactResult(prepared.result, prepared.snapshot, request.history, {
|
|
3842
|
+
toolResultMaxTokens: resolveCompactBudgets(request.config).retainedToolResultMaxTokens
|
|
3843
|
+
});
|
|
3844
|
+
if (!applied) {
|
|
3845
|
+
return {
|
|
3846
|
+
result: {
|
|
3847
|
+
status: "skipped",
|
|
3848
|
+
reason: "not-applicable",
|
|
3849
|
+
message: "The history no longer extends the snapshot the reducer read."
|
|
3850
|
+
}
|
|
3851
|
+
};
|
|
3852
|
+
}
|
|
3853
|
+
const snapshotIds = new Set(prepared.snapshot.map((message) => message.id));
|
|
3854
|
+
const delta = request.history.filter((message) => !snapshotIds.has(message.id));
|
|
3855
|
+
if (request.runContext) runtime.runAccounting.startRoot(request.runContext);
|
|
3856
|
+
this.registerUsageTarget(request, runtime);
|
|
3857
|
+
const accounted = this.accountedOptions(
|
|
3858
|
+
{ ...request.options, trigger: prepared.trigger },
|
|
3859
|
+
request.runContext,
|
|
3860
|
+
runtime,
|
|
3861
|
+
request.onUsage
|
|
3862
|
+
);
|
|
3863
|
+
const judge = await this.judgeRunner(request.config, applied, delta, {
|
|
3864
|
+
signal: accounted.signal,
|
|
3865
|
+
provider: accounted.provider,
|
|
3866
|
+
beforeModelCall: accounted.beforeModelCall,
|
|
3867
|
+
onUsage: accounted.onUsage,
|
|
3868
|
+
onUsageMissing: accounted.onUsageMissing
|
|
2966
3869
|
});
|
|
3870
|
+
if (accounted.signal?.aborted || judge.note === "aborted") {
|
|
3871
|
+
return { result: { status: "failed", reason: "aborted", error: "Compaction aborted." } };
|
|
3872
|
+
}
|
|
3873
|
+
const result = judgedResult(applied, judge);
|
|
2967
3874
|
if (result.status !== "compacted") return { result };
|
|
2968
3875
|
if (request.isCurrent?.() === false) return { result };
|
|
3876
|
+
return this.commitCompactResult(result, {
|
|
3877
|
+
config: request.config,
|
|
3878
|
+
history: request.history,
|
|
3879
|
+
sessionId: request.sessionId,
|
|
3880
|
+
transcriptOrdinal: request.transcriptOrdinal,
|
|
3881
|
+
options: { ...request.options, trigger: prepared.trigger },
|
|
3882
|
+
runContext: request.runContext,
|
|
3883
|
+
runtime,
|
|
3884
|
+
timelineStore: request.timelineStore,
|
|
3885
|
+
isCurrent: request.isCurrent,
|
|
3886
|
+
onCommitted: request.onCommitted
|
|
3887
|
+
});
|
|
3888
|
+
}
|
|
3889
|
+
/**
|
|
3890
|
+
* The compactor's model calls charged to the run, the way `compact` has always charged
|
|
3891
|
+
* them, and reported to the host's own session bill when it keeps one (#370).
|
|
3892
|
+
*/
|
|
3893
|
+
accountedOptions(options, runContext, runtime, onUsage) {
|
|
3894
|
+
return {
|
|
3895
|
+
...options,
|
|
3896
|
+
beforeModelCall: runContext ? (model) => {
|
|
3897
|
+
const requestCheck = options.beforeModelCall?.(model);
|
|
3898
|
+
if (requestCheck && !requestCheck.allowed) return requestCheck;
|
|
3899
|
+
return runtime.runAccounting.checkBeforeModelCall(runContext.rootRunId, model);
|
|
3900
|
+
} : options.beforeModelCall,
|
|
3901
|
+
// Left exactly as it was when there is nothing to chain: a compaction with
|
|
3902
|
+
// no run context and no host callback hands the runner its own options back.
|
|
3903
|
+
onUsage: runContext || onUsage ? (usage, metadata) => {
|
|
3904
|
+
if (runContext) runtime.runAccounting.record(runContext, usage, metadata);
|
|
3905
|
+
options.onUsage?.(usage, metadata);
|
|
3906
|
+
onUsage?.(usage);
|
|
3907
|
+
} : options.onUsage,
|
|
3908
|
+
onUsageMissing: runContext ? (metadata) => {
|
|
3909
|
+
runtime.runAccounting.markUsageUnknown(runContext, metadata, "compaction_usage");
|
|
3910
|
+
options.onUsageMissing?.(metadata);
|
|
3911
|
+
} : options.onUsageMissing
|
|
3912
|
+
};
|
|
3913
|
+
}
|
|
3914
|
+
/** The record, the boundary, `onCommitted` and the PostCompact hooks for a compacted result. */
|
|
3915
|
+
async commitCompactResult(result, request) {
|
|
2969
3916
|
const timestamp = Date.now();
|
|
2970
|
-
const
|
|
3917
|
+
const recorded = {
|
|
2971
3918
|
id: result.compactId,
|
|
2972
3919
|
trigger: result.trigger,
|
|
2973
3920
|
transcriptOrdinal: request.transcriptOrdinal,
|
|
@@ -2977,7 +3924,13 @@ var AgentSession = class {
|
|
|
2977
3924
|
postContextTokens: result.postContextTokens,
|
|
2978
3925
|
generation: result.generation,
|
|
2979
3926
|
checkpointVersion: 2,
|
|
2980
|
-
timestamp
|
|
3927
|
+
timestamp,
|
|
3928
|
+
carriedCount: result.carriedCount
|
|
3929
|
+
};
|
|
3930
|
+
const boundary = {
|
|
3931
|
+
...recorded,
|
|
3932
|
+
...result.degraded ? { degraded: true } : {},
|
|
3933
|
+
...result.warning ? { warning: result.warning } : {}
|
|
2981
3934
|
};
|
|
2982
3935
|
const data = {
|
|
2983
3936
|
version: 2,
|
|
@@ -2990,14 +3943,16 @@ var AgentSession = class {
|
|
|
2990
3943
|
preContextTokens: result.preContextTokens,
|
|
2991
3944
|
postContextTokens: result.postContextTokens,
|
|
2992
3945
|
replacementHistory: result.replacementHistory,
|
|
2993
|
-
boundary,
|
|
3946
|
+
boundary: recorded,
|
|
2994
3947
|
throughEventRef: result.throughEventRef,
|
|
2995
3948
|
summarizedCount: result.summarizedCount,
|
|
2996
3949
|
retainedCount: result.retainedCount,
|
|
3950
|
+
carriedCount: result.carriedCount,
|
|
2997
3951
|
strategy: result.strategy,
|
|
2998
3952
|
modelCalls: result.modelCalls,
|
|
2999
3953
|
degraded: result.degraded,
|
|
3000
|
-
warning: result.warning
|
|
3954
|
+
warning: result.warning,
|
|
3955
|
+
...result.judge ? { judge: result.judge } : {}
|
|
3001
3956
|
};
|
|
3002
3957
|
if (request.timelineStore && request.sessionId) {
|
|
3003
3958
|
request.timelineStore.append(request.sessionId, {
|
|
@@ -3012,8 +3967,7 @@ var AgentSession = class {
|
|
|
3012
3967
|
trigger: result.trigger,
|
|
3013
3968
|
sessionId: request.sessionId,
|
|
3014
3969
|
focus: request.options.focus,
|
|
3015
|
-
onHookEvent: request.options.onHookEvent
|
|
3016
|
-
signal: request.options.signal
|
|
3970
|
+
onHookEvent: request.options.onHookEvent
|
|
3017
3971
|
});
|
|
3018
3972
|
return { result, boundary };
|
|
3019
3973
|
}
|
|
@@ -3049,6 +4003,14 @@ var AgentSession = class {
|
|
|
3049
4003
|
allowedTools: request.options?.allowedTools
|
|
3050
4004
|
})
|
|
3051
4005
|
);
|
|
4006
|
+
this.usageTargets.set(runContext.rootRunId, {
|
|
4007
|
+
target: {
|
|
4008
|
+
sessionId: request.sessionId,
|
|
4009
|
+
timelineStore: request.timelineStore,
|
|
4010
|
+
isCurrent: request.isCurrent
|
|
4011
|
+
},
|
|
4012
|
+
runtime
|
|
4013
|
+
});
|
|
3052
4014
|
const finalizeOutcome = (outcome) => {
|
|
3053
4015
|
if (!terminalOutcome) {
|
|
3054
4016
|
terminalOutcome = outcome;
|
|
@@ -3066,13 +4028,6 @@ var AgentSession = class {
|
|
|
3066
4028
|
emit({ type: "session", sessionId: request.sessionId });
|
|
3067
4029
|
const unsubscribeShellEvents = runtime.shellManager.subscribe((event) => emit(event));
|
|
3068
4030
|
try {
|
|
3069
|
-
let persistedUsage = {
|
|
3070
|
-
promptTokens: 0,
|
|
3071
|
-
completionTokens: 0,
|
|
3072
|
-
totalTokens: 0,
|
|
3073
|
-
cacheReadInputTokens: 0,
|
|
3074
|
-
cacheCreationInputTokens: 0
|
|
3075
|
-
};
|
|
3076
4031
|
let lastPlanSignature = "";
|
|
3077
4032
|
const persistPlan = () => {
|
|
3078
4033
|
if (!request.timelineStore) return;
|
|
@@ -3094,6 +4049,15 @@ var AgentSession = class {
|
|
|
3094
4049
|
data
|
|
3095
4050
|
});
|
|
3096
4051
|
};
|
|
4052
|
+
let lastGoalSignature = "";
|
|
4053
|
+
const persistRunGoal = () => {
|
|
4054
|
+
if (request.isCurrent?.() === false) return;
|
|
4055
|
+
const goal = runtime.activeGoal ?? null;
|
|
4056
|
+
const signature = JSON.stringify(goal);
|
|
4057
|
+
if (signature === lastGoalSignature) return;
|
|
4058
|
+
lastGoalSignature = signature;
|
|
4059
|
+
persistGoal({ sessionId: request.sessionId, timelineStore: request.timelineStore }, goal);
|
|
4060
|
+
};
|
|
3097
4061
|
const baseLoopCallbacks = {
|
|
3098
4062
|
onText: (content) => {
|
|
3099
4063
|
streamedAssistantText += content;
|
|
@@ -3106,6 +4070,12 @@ var AgentSession = class {
|
|
|
3106
4070
|
},
|
|
3107
4071
|
onToolCall: (toolCall) => emit({ type: "tool_use", toolCall }),
|
|
3108
4072
|
onToolResult: (toolResult) => emit({ type: "tool_result", toolResult }),
|
|
4073
|
+
onNotice: (message) => emit({ type: "notice", message }),
|
|
4074
|
+
onGoalStatus: (event) => {
|
|
4075
|
+
persistRunGoal();
|
|
4076
|
+
emit({ type: "goal_status", event });
|
|
4077
|
+
},
|
|
4078
|
+
onAuxiliaryUsage: callbacks.onAuxiliaryUsage,
|
|
3109
4079
|
onError: (error) => {
|
|
3110
4080
|
emittedError = error;
|
|
3111
4081
|
emit({ type: "error", error });
|
|
@@ -3117,7 +4087,9 @@ var AgentSession = class {
|
|
|
3117
4087
|
finalizeOutcome(outcome2);
|
|
3118
4088
|
},
|
|
3119
4089
|
onPermissionRequired: (toolCall) => {
|
|
3120
|
-
if (request.isCurrent?.() === false)
|
|
4090
|
+
if (request.isCurrent?.() === false) {
|
|
4091
|
+
return Promise.resolve({ result: "deny", reason: "dismissed" });
|
|
4092
|
+
}
|
|
3121
4093
|
return callbacks.onPermissionRequired ? callbacks.onPermissionRequired(toolCall) : this.interactions.requestPermission(toolCall);
|
|
3122
4094
|
},
|
|
3123
4095
|
onPlanApprovalRequired: (plan) => {
|
|
@@ -3139,32 +4111,21 @@ var AgentSession = class {
|
|
|
3139
4111
|
},
|
|
3140
4112
|
onUsage: (nextUsage, metadata) => {
|
|
3141
4113
|
usage = nextUsage;
|
|
3142
|
-
|
|
3143
|
-
const recordUsage = inclusive ? subtractUsage(inclusive, persistedUsage) : nextUsage;
|
|
3144
|
-
if (inclusive) persistedUsage = inclusive;
|
|
3145
|
-
if (recordUsage.totalTokens <= 0 && recordUsage.promptTokens <= 0) return;
|
|
3146
|
-
if (request.isCurrent?.() !== false) {
|
|
3147
|
-
request.timelineStore?.append(request.sessionId, {
|
|
3148
|
-
type: "usage",
|
|
3149
|
-
timestamp: Date.now(),
|
|
3150
|
-
data: {
|
|
3151
|
-
version: 1,
|
|
3152
|
-
usage: recordUsage,
|
|
3153
|
-
requestedModel: metadata?.requestedModel,
|
|
3154
|
-
responseModel: metadata?.responseModel
|
|
3155
|
-
}
|
|
3156
|
-
});
|
|
3157
|
-
}
|
|
4114
|
+
this.persistReportedUsage(runContext.rootRunId, nextUsage, metadata);
|
|
3158
4115
|
callbacks.onUsage?.(nextUsage, metadata);
|
|
3159
4116
|
},
|
|
3160
4117
|
getMode: callbacks.getMode,
|
|
3161
4118
|
onModeChange: callbacks.onModeChange,
|
|
4119
|
+
// The root run's only: a managed child and a Task subagent run the loop
|
|
4120
|
+
// with their own callbacks, and neither announces calls to a stream the
|
|
4121
|
+
// host is reading for them.
|
|
4122
|
+
beforeToolExecution: callbacks.beforeToolExecution,
|
|
3162
4123
|
onPlanHandoff: callbacks.onPlanHandoff,
|
|
3163
|
-
//
|
|
3164
|
-
//
|
|
3165
|
-
// context-overflow path at the bottom of the turn is deliberately not
|
|
3166
|
-
// gated by it. Nulling the callback disabled that recovery too.
|
|
4124
|
+
// Passed through as is: the loop gates every compaction site on
|
|
4125
|
+
// `autoCompactEnabled`, the context-overflow recovery included.
|
|
3167
4126
|
onCompact: callbacks.onCompact,
|
|
4127
|
+
prepareCompact: callbacks.prepareCompact,
|
|
4128
|
+
commitCompact: callbacks.commitCompact,
|
|
3168
4129
|
onAssistantMessageComplete: (message) => {
|
|
3169
4130
|
if (request.isCurrent?.() === false) return;
|
|
3170
4131
|
request.timelineStore?.append(request.sessionId, {
|
|
@@ -3180,7 +4141,10 @@ var AgentSession = class {
|
|
|
3180
4141
|
kind: message.kind ?? "conversation",
|
|
3181
4142
|
toolCalls: message.toolCalls,
|
|
3182
4143
|
toolResults: message.toolResults,
|
|
3183
|
-
fileObservations: message.fileObservations
|
|
4144
|
+
fileObservations: message.fileObservations,
|
|
4145
|
+
// Without this a resumed session reads a host notice as the model's
|
|
4146
|
+
// own reply: `finalAnswerText` and the answer walk both key on it.
|
|
4147
|
+
hostNotice: message.hostNotice
|
|
3184
4148
|
}
|
|
3185
4149
|
});
|
|
3186
4150
|
callbacks.onAssistantMessageComplete?.(message);
|
|
@@ -3213,7 +4177,10 @@ var AgentSession = class {
|
|
|
3213
4177
|
id: message.id,
|
|
3214
4178
|
content: message.content,
|
|
3215
4179
|
kind: message.kind ?? "conversation",
|
|
3216
|
-
includeInContext: message.includeInContext ?? true
|
|
4180
|
+
includeInContext: message.includeInContext ?? true,
|
|
4181
|
+
// A host-appended prompt stays host-written across a reload: the ledger,
|
|
4182
|
+
// Carried Turns key on it.
|
|
4183
|
+
derivedContent: message.derivedContent
|
|
3217
4184
|
}
|
|
3218
4185
|
});
|
|
3219
4186
|
},
|
|
@@ -3224,21 +4191,27 @@ var AgentSession = class {
|
|
|
3224
4191
|
onHookEvent: callbacks.onHookEvent,
|
|
3225
4192
|
onAgentEvent: (event) => emit(event)
|
|
3226
4193
|
};
|
|
3227
|
-
|
|
3228
|
-
|
|
3229
|
-
|
|
3230
|
-
|
|
3231
|
-
|
|
3232
|
-
|
|
3233
|
-
|
|
3234
|
-
|
|
3235
|
-
|
|
3236
|
-
|
|
3237
|
-
|
|
3238
|
-
|
|
3239
|
-
|
|
3240
|
-
|
|
3241
|
-
|
|
4194
|
+
let messages;
|
|
4195
|
+
try {
|
|
4196
|
+
messages = await this.runLoop(
|
|
4197
|
+
request.config,
|
|
4198
|
+
request.registry,
|
|
4199
|
+
request.prompt,
|
|
4200
|
+
effectiveHistory,
|
|
4201
|
+
baseLoopCallbacks,
|
|
4202
|
+
request.mode,
|
|
4203
|
+
{
|
|
4204
|
+
...request.options,
|
|
4205
|
+
runtime,
|
|
4206
|
+
runContext,
|
|
4207
|
+
resolveAttachment: request.options?.resolveAttachment ?? (request.timelineStore?.readImageAttachment ? (attachment) => request.timelineStore.readImageAttachment(request.sessionId, attachment) : void 0),
|
|
4208
|
+
signal: request.signal
|
|
4209
|
+
}
|
|
4210
|
+
);
|
|
4211
|
+
} finally {
|
|
4212
|
+
this.flushRunUsage(runContext.rootRunId, true);
|
|
4213
|
+
if (runtime.activeGoal) persistRunGoal();
|
|
4214
|
+
}
|
|
3242
4215
|
const partialOutput = streamedAssistantText.length > 0 || messages.slice(effectiveHistory.length).some((message) => message.role === "assistant");
|
|
3243
4216
|
const outcome = finalizeOutcome(
|
|
3244
4217
|
terminalOutcome ?? (request.signal?.aborted ? classifyAbortReason(request.signal.reason, partialOutput) : emittedError ? createTerminalOutcome("failed", "provider_error", {
|
|
@@ -3287,10 +4260,10 @@ async function captureSnapshot(snapshotStore) {
|
|
|
3287
4260
|
return { ok: false, reason: "Filesystem checkpoint storage is unavailable." };
|
|
3288
4261
|
}
|
|
3289
4262
|
if (snapshotStore.captureAsync) return snapshotStore.captureAsync();
|
|
3290
|
-
return new Promise((
|
|
4263
|
+
return new Promise((resolve4, reject) => {
|
|
3291
4264
|
setTimeout(() => {
|
|
3292
4265
|
try {
|
|
3293
|
-
|
|
4266
|
+
resolve4(snapshotStore.capture());
|
|
3294
4267
|
} catch (error) {
|
|
3295
4268
|
reject(error);
|
|
3296
4269
|
}
|
|
@@ -3481,6 +4454,8 @@ function validateElicitationContent(fields, content) {
|
|
|
3481
4454
|
}
|
|
3482
4455
|
|
|
3483
4456
|
export {
|
|
4457
|
+
parseSessionDriver,
|
|
4458
|
+
resolveSessionDriver,
|
|
3484
4459
|
displaySessionName,
|
|
3485
4460
|
normalizeWorkspace,
|
|
3486
4461
|
SessionStore,
|
|
@@ -3491,8 +4466,11 @@ export {
|
|
|
3491
4466
|
findActiveFileMention,
|
|
3492
4467
|
replaceActiveFileMention,
|
|
3493
4468
|
getFileMentionCandidates,
|
|
4469
|
+
mentionTokenRanges,
|
|
3494
4470
|
createAgentSessionSnapshot,
|
|
3495
4471
|
reduceAgentSessionSnapshot,
|
|
4472
|
+
discardUntouchedSession,
|
|
4473
|
+
discardUntouchedSessions,
|
|
3496
4474
|
resolveSessionBootstrap,
|
|
3497
4475
|
AgentSession,
|
|
3498
4476
|
toElicitationFields,
|
|
@@ -3501,4 +4479,4 @@ export {
|
|
|
3501
4479
|
coerceElicitationValue,
|
|
3502
4480
|
validateElicitationContent
|
|
3503
4481
|
};
|
|
3504
|
-
//# sourceMappingURL=chunk-
|
|
4482
|
+
//# sourceMappingURL=chunk-ENLB7NYV.js.map
|