doer-agent 0.5.7 → 0.5.9
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/dist/agent-session-rpc.js +170 -33
- package/package.json +1 -1
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { watch } from "node:fs";
|
|
2
|
-
import { open, readdir, realpath, rm, rmdir, stat, unlink } from "node:fs/promises";
|
|
2
|
+
import { open, mkdir, readdir, realpath, rm, rmdir, stat, unlink, writeFile } from "node:fs/promises";
|
|
3
|
+
import crypto from "node:crypto";
|
|
3
4
|
import path from "node:path";
|
|
4
5
|
import { StringCodec } from "nats";
|
|
5
6
|
const sessionRpcCodec = StringCodec();
|
|
@@ -89,6 +90,84 @@ function buildInlineBlobMarker(value) {
|
|
|
89
90
|
}
|
|
90
91
|
return "[inline blob omitted]";
|
|
91
92
|
}
|
|
93
|
+
function extensionForImageMimeType(mimeType) {
|
|
94
|
+
const normalized = mimeType.trim().toLowerCase();
|
|
95
|
+
if (normalized === "image/jpeg" || normalized === "image/jpg")
|
|
96
|
+
return ".jpg";
|
|
97
|
+
if (normalized === "image/webp")
|
|
98
|
+
return ".webp";
|
|
99
|
+
if (normalized === "image/gif")
|
|
100
|
+
return ".gif";
|
|
101
|
+
if (normalized === "image/svg+xml")
|
|
102
|
+
return ".svg";
|
|
103
|
+
if (normalized === "image/bmp")
|
|
104
|
+
return ".bmp";
|
|
105
|
+
if (normalized === "image/avif")
|
|
106
|
+
return ".avif";
|
|
107
|
+
return ".png";
|
|
108
|
+
}
|
|
109
|
+
function decodeInlineImageDataUrl(value) {
|
|
110
|
+
const trimmed = value.trim();
|
|
111
|
+
const match = /^data:([^;,]+)(?:;[^,]*)?;base64,([\s\S]+)$/i.exec(trimmed);
|
|
112
|
+
if (!match) {
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
const mimeType = match[1]?.trim().toLowerCase() || "";
|
|
116
|
+
if (!mimeType.startsWith("image/")) {
|
|
117
|
+
return null;
|
|
118
|
+
}
|
|
119
|
+
const base64 = match[2]?.replace(/\s/g, "") || "";
|
|
120
|
+
if (!base64) {
|
|
121
|
+
return null;
|
|
122
|
+
}
|
|
123
|
+
try {
|
|
124
|
+
return {
|
|
125
|
+
bytes: Buffer.from(base64, "base64"),
|
|
126
|
+
mimeType,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
catch {
|
|
130
|
+
return null;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
function sanitizeSessionPathSegment(value, fallback) {
|
|
134
|
+
return value.trim().replace(/[^a-zA-Z0-9._-]/g, "_").slice(0, 160) || fallback;
|
|
135
|
+
}
|
|
136
|
+
function inferSessionIdFromSessionFilePath(filePath) {
|
|
137
|
+
const base = path.basename(filePath, path.extname(filePath));
|
|
138
|
+
const match = /^rollout-\d{4}-\d{2}-\d{2}T\d{2}-\d{2}-\d{2}-(.+)$/.exec(base);
|
|
139
|
+
const inferred = match?.[1]?.trim() || "";
|
|
140
|
+
return inferred || null;
|
|
141
|
+
}
|
|
142
|
+
async function materializeInlineSessionImage(args) {
|
|
143
|
+
const decoded = decodeInlineImageDataUrl(args.value);
|
|
144
|
+
if (!decoded || decoded.bytes.byteLength === 0) {
|
|
145
|
+
return null;
|
|
146
|
+
}
|
|
147
|
+
const inferredSessionId = inferSessionIdFromSessionFilePath(args.filePath);
|
|
148
|
+
const sessionBase = (args.sessionId ? sanitizeSessionPathSegment(args.sessionId, "") : "")
|
|
149
|
+
|| (inferredSessionId ? sanitizeSessionPathSegment(inferredSessionId, "") : "")
|
|
150
|
+
|| sanitizeSessionPathSegment(path.basename(args.filePath, path.extname(args.filePath)), "session");
|
|
151
|
+
const hash = crypto.createHash("sha256").update(decoded.bytes).digest("hex").slice(0, 16);
|
|
152
|
+
const ext = extensionForImageMimeType(decoded.mimeType);
|
|
153
|
+
const name = `line-${args.lineNumber}-${hash}${ext}`;
|
|
154
|
+
const relPath = path.posix.join(".doer-agent", "sessions", sessionBase, "outputs", name);
|
|
155
|
+
const absPath = path.resolve(args.workspaceRoot, relPath);
|
|
156
|
+
if (!(absPath === args.workspaceRoot || absPath.startsWith(args.workspaceRoot + path.sep))) {
|
|
157
|
+
return null;
|
|
158
|
+
}
|
|
159
|
+
await mkdir(path.dirname(absPath), { recursive: true });
|
|
160
|
+
const existing = await stat(absPath).catch(() => null);
|
|
161
|
+
if (!existing || !existing.isFile() || existing.size !== decoded.bytes.byteLength) {
|
|
162
|
+
await writeFile(absPath, decoded.bytes);
|
|
163
|
+
}
|
|
164
|
+
return {
|
|
165
|
+
relPath,
|
|
166
|
+
name,
|
|
167
|
+
size: decoded.bytes.byteLength,
|
|
168
|
+
mimeType: decoded.mimeType,
|
|
169
|
+
};
|
|
170
|
+
}
|
|
92
171
|
function getSessionRpcPayloadByteLength(value) {
|
|
93
172
|
try {
|
|
94
173
|
const serialized = typeof value === "string" ? value : JSON.stringify(value);
|
|
@@ -104,12 +183,12 @@ function buildSessionRpcTruncatedMarker(label, value) {
|
|
|
104
183
|
? `[${label} truncated for session RPC pagination]`
|
|
105
184
|
: `[${label} truncated for session RPC pagination: ${byteLength} bytes omitted]`;
|
|
106
185
|
}
|
|
107
|
-
function sanitizeSessionRpcPayload(value) {
|
|
186
|
+
async function sanitizeSessionRpcPayload(value, args) {
|
|
108
187
|
if (typeof value === "string") {
|
|
109
188
|
return value;
|
|
110
189
|
}
|
|
111
190
|
if (Array.isArray(value)) {
|
|
112
|
-
return value.map((entry) => sanitizeSessionRpcPayload(entry));
|
|
191
|
+
return Promise.all(value.map((entry) => sanitizeSessionRpcPayload(entry, args)));
|
|
113
192
|
}
|
|
114
193
|
if (!isObjectRecord(value)) {
|
|
115
194
|
return value;
|
|
@@ -117,14 +196,25 @@ function sanitizeSessionRpcPayload(value) {
|
|
|
117
196
|
const sanitized = {};
|
|
118
197
|
for (const [key, entry] of Object.entries(value)) {
|
|
119
198
|
if (SESSION_RPC_BLOB_KEYS.has(key) && typeof entry === "string" && isInlineBlobString(entry)) {
|
|
199
|
+
if (key === "image_url") {
|
|
200
|
+
const materialized = await materializeInlineSessionImage({ ...args, value: entry });
|
|
201
|
+
if (materialized) {
|
|
202
|
+
sanitized[key] = materialized.relPath;
|
|
203
|
+
sanitized.name = typeof value.name === "string" && value.name.trim() ? value.name : materialized.name;
|
|
204
|
+
sanitized.size = typeof value.size === "number" && Number.isFinite(value.size) ? value.size : materialized.size;
|
|
205
|
+
sanitized.mimeType =
|
|
206
|
+
typeof value.mimeType === "string" && value.mimeType.trim() ? value.mimeType : materialized.mimeType;
|
|
207
|
+
continue;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
120
210
|
sanitized[key] = buildInlineBlobMarker(entry);
|
|
121
211
|
continue;
|
|
122
212
|
}
|
|
123
|
-
sanitized[key] = sanitizeSessionRpcPayload(entry);
|
|
213
|
+
sanitized[key] = await sanitizeSessionRpcPayload(entry, args);
|
|
124
214
|
}
|
|
125
215
|
return sanitized;
|
|
126
216
|
}
|
|
127
|
-
function sanitizeSessionRpcRawLine(line) {
|
|
217
|
+
async function sanitizeSessionRpcRawLine(line, args) {
|
|
128
218
|
const trimmed = line.trim();
|
|
129
219
|
if (!trimmed.startsWith("{")) {
|
|
130
220
|
return line;
|
|
@@ -159,7 +249,7 @@ function sanitizeSessionRpcRawLine(line) {
|
|
|
159
249
|
}
|
|
160
250
|
return JSON.stringify({
|
|
161
251
|
...parsed,
|
|
162
|
-
payload: sanitizeSessionRpcPayload(parsed.payload),
|
|
252
|
+
payload: await sanitizeSessionRpcPayload(parsed.payload, args),
|
|
163
253
|
});
|
|
164
254
|
}
|
|
165
255
|
catch {
|
|
@@ -279,33 +369,57 @@ function extractLastSessionMessage(candidateLines) {
|
|
|
279
369
|
}
|
|
280
370
|
return null;
|
|
281
371
|
}
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
372
|
+
function linePrefixContainsSessionMessageCandidate(prefix) {
|
|
373
|
+
return (/"type"\s*:\s*"event_msg"/.test(prefix) &&
|
|
374
|
+
/"type"\s*:\s*"(agent_message|user_message)"/.test(prefix));
|
|
375
|
+
}
|
|
376
|
+
async function readLineSpan(fileHandle, start, end) {
|
|
377
|
+
const readSize = Math.max(0, end - start);
|
|
378
|
+
if (readSize <= 0) {
|
|
379
|
+
return "";
|
|
287
380
|
}
|
|
288
|
-
|
|
289
|
-
let
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
const readSize = Math.min(chunkBytes, position, maxScanBytes - scanned);
|
|
293
|
-
position -= readSize;
|
|
294
|
-
scanned += readSize;
|
|
295
|
-
const buffer = Buffer.alloc(readSize);
|
|
296
|
-
const { bytesRead } = await fileHandle.read(buffer, 0, readSize, position);
|
|
381
|
+
const buffer = Buffer.alloc(readSize);
|
|
382
|
+
let totalBytesRead = 0;
|
|
383
|
+
while (totalBytesRead < readSize) {
|
|
384
|
+
const { bytesRead } = await fileHandle.read(buffer, totalBytesRead, readSize - totalBytesRead, start + totalBytesRead);
|
|
297
385
|
if (bytesRead <= 0) {
|
|
298
386
|
break;
|
|
299
387
|
}
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
388
|
+
totalBytesRead += bytesRead;
|
|
389
|
+
}
|
|
390
|
+
return buffer.toString("utf8", 0, totalBytesRead).trim();
|
|
391
|
+
}
|
|
392
|
+
async function readLastSessionMessage(workspaceRoot, filePath) {
|
|
393
|
+
const index = await readSessionLineIndex(workspaceRoot, filePath);
|
|
394
|
+
const totalLines = index.lineStartOffsets.length;
|
|
395
|
+
if (totalLines <= 0 || index.size <= 0) {
|
|
396
|
+
return null;
|
|
397
|
+
}
|
|
398
|
+
const resolvedFile = resolveSessionFilePath(workspaceRoot, filePath);
|
|
399
|
+
const fileHandle = await open(resolvedFile, "r");
|
|
400
|
+
try {
|
|
401
|
+
for (let lineIndex = totalLines - 1; lineIndex >= 0; lineIndex -= 1) {
|
|
402
|
+
const start = index.lineStartOffsets[lineIndex] ?? index.size;
|
|
403
|
+
const end = lineIndex + 1 < totalLines ? (index.lineStartOffsets[lineIndex + 1] ?? index.size) : index.size;
|
|
404
|
+
const spanBytes = Math.max(0, end - start);
|
|
405
|
+
if (spanBytes <= 0) {
|
|
406
|
+
continue;
|
|
407
|
+
}
|
|
408
|
+
const prefixBytes = Math.min(spanBytes, 1024);
|
|
409
|
+
const prefix = await readLineSpan(fileHandle, start, start + prefixBytes);
|
|
410
|
+
if (!linePrefixContainsSessionMessageCandidate(prefix)) {
|
|
411
|
+
continue;
|
|
412
|
+
}
|
|
413
|
+
const found = extractLastSessionMessage([await readLineSpan(fileHandle, start, end)]);
|
|
414
|
+
if (found) {
|
|
415
|
+
return found;
|
|
416
|
+
}
|
|
306
417
|
}
|
|
418
|
+
return null;
|
|
419
|
+
}
|
|
420
|
+
finally {
|
|
421
|
+
await fileHandle.close().catch(() => undefined);
|
|
307
422
|
}
|
|
308
|
-
return extractLastSessionMessage([carry]);
|
|
309
423
|
}
|
|
310
424
|
function normalizeSessionMeta(rawMeta, filePath, mtimeMs) {
|
|
311
425
|
const baseName = path.basename(filePath, path.extname(filePath));
|
|
@@ -321,13 +435,13 @@ function normalizeSessionMeta(rawMeta, filePath, mtimeMs) {
|
|
|
321
435
|
filePath,
|
|
322
436
|
};
|
|
323
437
|
}
|
|
324
|
-
async function readSessionSummary(filePath, mtimeMs) {
|
|
438
|
+
async function readSessionSummary(workspaceRoot, filePath, mtimeMs) {
|
|
325
439
|
let fileHandle = null;
|
|
326
440
|
try {
|
|
327
441
|
fileHandle = await open(filePath, "r");
|
|
328
442
|
const entryStat = await fileHandle.stat();
|
|
329
443
|
const firstLine = await readFirstLine(fileHandle, entryStat.size);
|
|
330
|
-
const tailSummary = await readLastSessionMessage(
|
|
444
|
+
const tailSummary = await readLastSessionMessage(workspaceRoot, filePath);
|
|
331
445
|
let normalized = normalizeSessionMeta({}, filePath, mtimeMs);
|
|
332
446
|
if (firstLine) {
|
|
333
447
|
try {
|
|
@@ -363,6 +477,7 @@ async function readSessionSummary(filePath, mtimeMs) {
|
|
|
363
477
|
}
|
|
364
478
|
}
|
|
365
479
|
async function listAgentSessions(workspaceRoot) {
|
|
480
|
+
const maxSessionSummaries = 10;
|
|
366
481
|
const sessionsRoot = getSessionsRootPath(workspaceRoot);
|
|
367
482
|
let sessionsRootStat;
|
|
368
483
|
try {
|
|
@@ -376,8 +491,23 @@ async function listAgentSessions(workspaceRoot) {
|
|
|
376
491
|
}
|
|
377
492
|
const files = await collectSessionJsonlFiles(workspaceRoot);
|
|
378
493
|
files.sort((a, b) => b.mtimeMs - a.mtimeMs || a.filePath.localeCompare(b.filePath));
|
|
379
|
-
const sessions =
|
|
380
|
-
|
|
494
|
+
const sessions = [];
|
|
495
|
+
for (let index = 0; index < files.length; index += 1) {
|
|
496
|
+
const file = files[index];
|
|
497
|
+
sessions.push(await readSessionSummary(workspaceRoot, file.filePath, file.mtimeMs));
|
|
498
|
+
sessions.sort((a, b) => toSortableTimestampMs(b.updatedAt) - toSortableTimestampMs(a.updatedAt) || b.filePath.localeCompare(a.filePath));
|
|
499
|
+
if (sessions.length > maxSessionSummaries) {
|
|
500
|
+
sessions.length = maxSessionSummaries;
|
|
501
|
+
}
|
|
502
|
+
const nextFile = files[index + 1] ?? null;
|
|
503
|
+
const oldestSelectedSession = sessions[maxSessionSummaries - 1] ?? null;
|
|
504
|
+
if (nextFile &&
|
|
505
|
+
oldestSelectedSession &&
|
|
506
|
+
toSortableTimestampMs(oldestSelectedSession.updatedAt) >= nextFile.mtimeMs) {
|
|
507
|
+
break;
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
return sessions;
|
|
381
511
|
}
|
|
382
512
|
async function readSessionLineIndex(workspaceRoot, filePath) {
|
|
383
513
|
const resolvedFile = resolveSessionFilePath(workspaceRoot, filePath);
|
|
@@ -442,6 +572,7 @@ async function readSessionLineIndex(workspaceRoot, filePath) {
|
|
|
442
572
|
}
|
|
443
573
|
async function getAgentSessionRawRows(args) {
|
|
444
574
|
const resolvedFile = resolveSessionFilePath(args.workspaceRoot, args.filePath);
|
|
575
|
+
const effectiveSessionId = args.sessionId || await readSessionIdFromSessionFile(resolvedFile).catch(() => null);
|
|
445
576
|
const index = await readSessionLineIndex(args.workspaceRoot, resolvedFile);
|
|
446
577
|
const totalLines = index.lineStartOffsets.length;
|
|
447
578
|
const sinceLine = Math.max(0, Math.floor(args.sinceLine));
|
|
@@ -555,7 +686,12 @@ async function getAgentSessionRawRows(args) {
|
|
|
555
686
|
let lineNumber = startLineIndex + 1;
|
|
556
687
|
for (const line of lines) {
|
|
557
688
|
if (line.trim()) {
|
|
558
|
-
const sanitized = sanitizeSessionRpcRawLine(line
|
|
689
|
+
const sanitized = await sanitizeSessionRpcRawLine(line, {
|
|
690
|
+
workspaceRoot: args.workspaceRoot,
|
|
691
|
+
filePath: resolvedFile,
|
|
692
|
+
lineNumber,
|
|
693
|
+
sessionId: effectiveSessionId,
|
|
694
|
+
});
|
|
559
695
|
rawRows.push({
|
|
560
696
|
id: lineNumber,
|
|
561
697
|
raw: sanitized,
|
|
@@ -573,7 +709,7 @@ async function getAgentSessionRawRows(args) {
|
|
|
573
709
|
}
|
|
574
710
|
}
|
|
575
711
|
function resolveSessionUploadsDir(workspaceRoot, sessionId) {
|
|
576
|
-
const safeSessionId = sessionId
|
|
712
|
+
const safeSessionId = sanitizeSessionPathSegment(sessionId, "session");
|
|
577
713
|
return path.join(workspaceRoot, ".doer-agent", "sessions", safeSessionId);
|
|
578
714
|
}
|
|
579
715
|
async function deleteAgentSession(workspaceRoot, filePath, sessionId) {
|
|
@@ -695,6 +831,7 @@ async function handleSessionRpcMessage(args) {
|
|
|
695
831
|
const result = await getAgentSessionRawRows({
|
|
696
832
|
workspaceRoot: args.workspaceRoot,
|
|
697
833
|
filePath: request.filePath ?? "",
|
|
834
|
+
sessionId: request.sessionId,
|
|
698
835
|
sinceLine: request.sinceLine,
|
|
699
836
|
beforeRowId: request.beforeRowId,
|
|
700
837
|
pageSize: request.pageSize,
|