@love-moon/conductor-cli 0.2.15 → 0.2.17

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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@love-moon/conductor-cli",
3
- "version": "0.2.15",
4
- "gitCommitId": "e500981",
3
+ "version": "0.2.17",
4
+ "gitCommitId": "c2654e1",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "conductor": "bin/conductor.js"
@@ -17,8 +17,8 @@
17
17
  "test": "node --test"
18
18
  },
19
19
  "dependencies": {
20
- "@love-moon/tui-driver": "0.2.15",
21
- "@love-moon/conductor-sdk": "0.2.15",
20
+ "@love-moon/ai-sdk": "0.2.17",
21
+ "@love-moon/conductor-sdk": "0.2.17",
22
22
  "dotenv": "^16.4.5",
23
23
  "enquirer": "^2.4.1",
24
24
  "js-yaml": "^4.1.1",
@@ -29,8 +29,8 @@
29
29
  },
30
30
  "pnpm": {
31
31
  "overrides": {
32
- "@love-moon/tui-driver": "file:../modules/tui-driver",
33
- "@love-moon/conductor-sdk": "file:../modules/sdk"
32
+ "@love-moon/ai-sdk": "file:../modules/ai-sdk",
33
+ "@love-moon/conductor-sdk": "file:../modules/conductor-sdk"
34
34
  }
35
35
  }
36
36
  }
package/src/daemon.js CHANGED
@@ -8,6 +8,7 @@ import dotenv from "dotenv";
8
8
  import yaml from "js-yaml";
9
9
 
10
10
  import { ConductorWebSocketClient, ConductorConfig, loadConfig, ConfigFileNotFound } from "@love-moon/conductor-sdk";
11
+ import { DaemonLogCollector } from "./log-collector.js";
11
12
 
12
13
  dotenv.config();
13
14
 
@@ -212,6 +213,7 @@ export function startDaemon(config = {}, deps = {}) {
212
213
  const createWebSocketClient =
213
214
  deps.createWebSocketClient ||
214
215
  ((clientConfig, options) => new ConductorWebSocketClient(clientConfig, options));
216
+ const createLogCollector = deps.createLogCollector || ((backendUrl) => new DaemonLogCollector(backendUrl));
215
217
  const PROJECT_PATH_LOOKUP_TIMEOUT_MS = parsePositiveInt(
216
218
  process.env.CONDUCTOR_PROJECT_PATH_LOOKUP_TIMEOUT_MS,
217
219
  1500,
@@ -364,6 +366,7 @@ export function startDaemon(config = {}, deps = {}) {
364
366
  const activeTaskProcesses = new Map();
365
367
  const suppressedExitStatusReports = new Set();
366
368
  const seenCommandRequestIds = new Set();
369
+ const logCollector = createLogCollector(BACKEND_HTTP);
367
370
  const client = createWebSocketClient(sdkConfig, {
368
371
  extraHeaders: {
369
372
  "x-conductor-host": AGENT_NAME,
@@ -574,6 +577,59 @@ export function startDaemon(config = {}, deps = {}) {
574
577
  }
575
578
  if (event.type === "stop_task") {
576
579
  handleStopTask(event.payload);
580
+ return;
581
+ }
582
+ if (event.type === "collect_logs") {
583
+ void handleCollectLogs(event.payload);
584
+ }
585
+ }
586
+
587
+ async function handleCollectLogs(payload) {
588
+ const requestId = payload?.request_id ? String(payload.request_id).trim() : "";
589
+ const taskId = payload?.task_id ? String(payload.task_id).trim() : "";
590
+ const collectedAt = new Date().toISOString();
591
+
592
+ if (!requestId || !taskId) {
593
+ logError(`Invalid collect_logs payload: ${JSON.stringify(payload)}`);
594
+ return;
595
+ }
596
+
597
+ let result;
598
+ try {
599
+ result = await Promise.resolve(
600
+ logCollector.collect(taskId, {
601
+ tailLines: payload?.options?.tail_lines,
602
+ since: payload?.options?.since,
603
+ }),
604
+ );
605
+ } catch (error) {
606
+ result = {
607
+ projectPath: null,
608
+ logPath: null,
609
+ entries: [],
610
+ truncated: false,
611
+ error: `Failed to read log file: ${error?.message || error}`,
612
+ collectedAt,
613
+ };
614
+ }
615
+
616
+ try {
617
+ await client.sendJson({
618
+ type: "agent_log_collected",
619
+ payload: {
620
+ request_id: requestId,
621
+ task_id: taskId,
622
+ daemon_host: AGENT_NAME,
623
+ project_path: result.projectPath,
624
+ log_path: result.logPath,
625
+ logs: result.entries,
626
+ truncated: Boolean(result.truncated),
627
+ error: result.error,
628
+ collected_at: result.collectedAt || collectedAt,
629
+ },
630
+ });
631
+ } catch (error) {
632
+ logError(`Failed to report agent_log_collected for ${taskId}: ${error?.message || error}`);
577
633
  }
578
634
  }
579
635
 
@@ -0,0 +1,420 @@
1
+ import fs from "node:fs";
2
+ import { promises as fsp } from "node:fs";
3
+ import os from "node:os";
4
+ import path from "node:path";
5
+ import readline from "node:readline";
6
+
7
+ import yaml from "js-yaml";
8
+
9
+ function normalizeBackend(backend) {
10
+ return String(backend || "").trim().toLowerCase();
11
+ }
12
+
13
+ function resolveHomeDir(options) {
14
+ if (options?.homeDir) {
15
+ return options.homeDir;
16
+ }
17
+ return os.homedir();
18
+ }
19
+
20
+ function normalizeSessionId(sessionId) {
21
+ return typeof sessionId === "string" ? sessionId.trim() : "";
22
+ }
23
+
24
+ export function buildResumeArgsForBackend(backend, sessionId) {
25
+ const resumeSessionId = normalizeSessionId(sessionId);
26
+ if (!resumeSessionId) {
27
+ return [];
28
+ }
29
+ const normalizedBackend = normalizeBackend(backend);
30
+ if (normalizedBackend === "codex" || normalizedBackend === "code") {
31
+ return ["resume", resumeSessionId];
32
+ }
33
+ if (normalizedBackend === "claude" || normalizedBackend === "claude-code") {
34
+ return ["--resume", resumeSessionId];
35
+ }
36
+ if (normalizedBackend === "copilot") {
37
+ return [`--resume=${resumeSessionId}`];
38
+ }
39
+ throw new Error(`--resume is not supported for backend "${backend}"`);
40
+ }
41
+
42
+ export function resumeProviderForBackend(backend) {
43
+ const normalizedBackend = normalizeBackend(backend);
44
+ if (normalizedBackend === "codex" || normalizedBackend === "code") {
45
+ return "codex";
46
+ }
47
+ if (normalizedBackend === "claude" || normalizedBackend === "claude-code") {
48
+ return "claude";
49
+ }
50
+ if (normalizedBackend === "copilot") {
51
+ return "copilot";
52
+ }
53
+ return null;
54
+ }
55
+
56
+ export async function findSessionPath(provider, sessionId, options = {}) {
57
+ const normalizedProvider = String(provider || "").trim().toLowerCase();
58
+ if (normalizedProvider === "codex") {
59
+ return findCodexSessionPath(sessionId, options);
60
+ }
61
+ if (normalizedProvider === "claude") {
62
+ return findClaudeSessionPath(sessionId, options);
63
+ }
64
+ if (normalizedProvider === "copilot") {
65
+ return findCopilotSessionPath(sessionId, options);
66
+ }
67
+ throw new Error(`Unsupported provider: ${provider}`);
68
+ }
69
+
70
+ export async function findCodexSessionPath(sessionId, options = {}) {
71
+ const normalizedSessionId = normalizeSessionId(sessionId);
72
+ if (!normalizedSessionId) {
73
+ return null;
74
+ }
75
+ const homeDir = resolveHomeDir(options);
76
+ const sessionsDir = options.codexSessionsDir || path.join(homeDir, ".codex", "sessions");
77
+ return findCodexSessionFile(sessionsDir, normalizedSessionId);
78
+ }
79
+
80
+ export async function findClaudeSessionPath(sessionId, options = {}) {
81
+ const normalizedSessionId = normalizeSessionId(sessionId);
82
+ if (!normalizedSessionId) {
83
+ return null;
84
+ }
85
+
86
+ const homeDir = resolveHomeDir(options);
87
+ const projectsDir = options.claudeProjectsDir || path.join(homeDir, ".claude", "projects");
88
+ const sessionEntries = await findClaudeSessionEntries(projectsDir, normalizedSessionId);
89
+ if (sessionEntries.length > 0) {
90
+ return sessionEntries[0]?.source || null;
91
+ }
92
+
93
+ const tasksDir = options.claudeTasksDir || path.join(homeDir, ".claude", "tasks");
94
+ const directTaskDir = path.join(tasksDir, normalizedSessionId);
95
+ if (await pathExists(directTaskDir, "directory")) {
96
+ return directTaskDir;
97
+ }
98
+
99
+ return null;
100
+ }
101
+
102
+ export async function findCopilotSessionPath(sessionId, options = {}) {
103
+ const normalizedSessionId = normalizeSessionId(sessionId);
104
+ if (!normalizedSessionId) {
105
+ return null;
106
+ }
107
+
108
+ const homeDir = resolveHomeDir(options);
109
+ const sessionStateDir = options.copilotSessionStateDir || path.join(homeDir, ".copilot", "session-state");
110
+ const directJsonlPath = path.join(sessionStateDir, `${normalizedSessionId}.jsonl`);
111
+ if (await pathExists(directJsonlPath, "file")) {
112
+ return directJsonlPath;
113
+ }
114
+
115
+ const directSessionDir = path.join(sessionStateDir, normalizedSessionId);
116
+ if (await pathExists(directSessionDir, "directory")) {
117
+ return directSessionDir;
118
+ }
119
+
120
+ return findPathByName(sessionStateDir, normalizedSessionId);
121
+ }
122
+
123
+ export async function resolveSessionRunDirectory(sessionPath) {
124
+ const normalizedPath = typeof sessionPath === "string" ? sessionPath.trim() : "";
125
+ if (!normalizedPath) {
126
+ throw new Error("Invalid session path");
127
+ }
128
+ let stats;
129
+ try {
130
+ stats = await fsp.stat(normalizedPath);
131
+ } catch {
132
+ throw new Error(`Session path does not exist: ${normalizedPath}`);
133
+ }
134
+ return stats.isDirectory() ? normalizedPath : path.dirname(normalizedPath);
135
+ }
136
+
137
+ export async function inspectResumeTarget(backend, sessionId, options = {}) {
138
+ return resolveResumeContext(backend, sessionId, options);
139
+ }
140
+
141
+ export async function resolveResumeContext(backend, sessionId, options = {}) {
142
+ const normalizedSessionId = normalizeSessionId(sessionId);
143
+ if (!normalizedSessionId) {
144
+ throw new Error("--resume requires a session id");
145
+ }
146
+ const provider = resumeProviderForBackend(backend);
147
+ if (!provider) {
148
+ throw new Error(`--resume is not supported for backend "${backend}"`);
149
+ }
150
+
151
+ const sessionPath = await findSessionPath(provider, normalizedSessionId, options);
152
+ if (!sessionPath) {
153
+ throw new Error(`Invalid --resume session id for ${provider}: ${normalizedSessionId}`);
154
+ }
155
+
156
+ const cwdFromSession = await extractResumeCwdFromSession(provider, sessionPath, normalizedSessionId);
157
+ const fallbackCwd = await resolveSessionRunDirectory(sessionPath);
158
+ const cwd = cwdFromSession || fallbackCwd;
159
+ if (!(await isExistingDirectory(cwd))) {
160
+ throw new Error(`Resume workspace path does not exist: ${cwd}`);
161
+ }
162
+
163
+ return {
164
+ provider,
165
+ sessionId: normalizedSessionId,
166
+ sessionPath,
167
+ cwd,
168
+ debugMetadata: {
169
+ cwdSource: cwdFromSession ? "session" : "session_path",
170
+ sessionPath,
171
+ },
172
+ };
173
+ }
174
+
175
+ async function isExistingDirectory(targetPath) {
176
+ const normalizedPath = typeof targetPath === "string" ? targetPath.trim() : "";
177
+ if (!normalizedPath) {
178
+ return false;
179
+ }
180
+ try {
181
+ const stats = await fsp.stat(normalizedPath);
182
+ return stats.isDirectory();
183
+ } catch {
184
+ return false;
185
+ }
186
+ }
187
+
188
+ async function extractCodexResumeCwd(sessionPath) {
189
+ if (!sessionPath.endsWith(".jsonl")) {
190
+ return null;
191
+ }
192
+ const rl = readline.createInterface({
193
+ input: fs.createReadStream(sessionPath),
194
+ crlfDelay: Infinity,
195
+ });
196
+ for await (const line of rl) {
197
+ const trimmed = line.trim();
198
+ if (!trimmed) {
199
+ continue;
200
+ }
201
+ let entry;
202
+ try {
203
+ entry = JSON.parse(trimmed);
204
+ } catch {
205
+ continue;
206
+ }
207
+ const maybeCwd = entry?.type === "session_meta" ? entry?.payload?.cwd : null;
208
+ if (typeof maybeCwd === "string" && maybeCwd.trim()) {
209
+ return maybeCwd.trim();
210
+ }
211
+ }
212
+ return null;
213
+ }
214
+
215
+ async function extractClaudeResumeCwd(sessionPath, sessionId) {
216
+ if (!sessionPath.endsWith(".jsonl")) {
217
+ return null;
218
+ }
219
+ const rl = readline.createInterface({
220
+ input: fs.createReadStream(sessionPath),
221
+ crlfDelay: Infinity,
222
+ });
223
+ for await (const line of rl) {
224
+ const trimmed = line.trim();
225
+ if (!trimmed) {
226
+ continue;
227
+ }
228
+ let entry;
229
+ try {
230
+ entry = JSON.parse(trimmed);
231
+ } catch {
232
+ continue;
233
+ }
234
+ const idMatches = String(entry?.sessionId || "").trim() === sessionId;
235
+ const maybeCwd = entry?.cwd;
236
+ if (idMatches && typeof maybeCwd === "string" && maybeCwd.trim()) {
237
+ return maybeCwd.trim();
238
+ }
239
+ }
240
+ return null;
241
+ }
242
+
243
+ async function extractCopilotResumeCwd(sessionPath) {
244
+ let stats;
245
+ try {
246
+ stats = await fsp.stat(sessionPath);
247
+ } catch {
248
+ return null;
249
+ }
250
+
251
+ if (stats.isDirectory()) {
252
+ const workspaceYamlPath = path.join(sessionPath, "workspace.yaml");
253
+ try {
254
+ const yamlContent = await fsp.readFile(workspaceYamlPath, "utf8");
255
+ const parsed = yaml.load(yamlContent);
256
+ const maybeCwd = parsed && typeof parsed === "object" ? parsed.cwd : null;
257
+ if (typeof maybeCwd === "string" && maybeCwd.trim()) {
258
+ return maybeCwd.trim();
259
+ }
260
+ } catch {
261
+ return null;
262
+ }
263
+ return null;
264
+ }
265
+
266
+ if (!sessionPath.endsWith(".jsonl")) {
267
+ return null;
268
+ }
269
+
270
+ const rl = readline.createInterface({
271
+ input: fs.createReadStream(sessionPath),
272
+ crlfDelay: Infinity,
273
+ });
274
+ for await (const line of rl) {
275
+ const trimmed = line.trim();
276
+ if (!trimmed) {
277
+ continue;
278
+ }
279
+ let entry;
280
+ try {
281
+ entry = JSON.parse(trimmed);
282
+ } catch {
283
+ continue;
284
+ }
285
+ const maybeCwd = entry?.data?.context?.cwd || entry?.data?.cwd;
286
+ if (typeof maybeCwd === "string" && maybeCwd.trim()) {
287
+ return maybeCwd.trim();
288
+ }
289
+ }
290
+ return null;
291
+ }
292
+
293
+ async function extractResumeCwdFromSession(provider, sessionPath, sessionId) {
294
+ if (provider === "codex") {
295
+ return extractCodexResumeCwd(sessionPath);
296
+ }
297
+ if (provider === "claude") {
298
+ return extractClaudeResumeCwd(sessionPath, sessionId);
299
+ }
300
+ if (provider === "copilot") {
301
+ return extractCopilotResumeCwd(sessionPath);
302
+ }
303
+ return null;
304
+ }
305
+
306
+ async function findCodexSessionFile(rootDir, sessionId) {
307
+ const queue = [rootDir];
308
+ while (queue.length) {
309
+ const current = queue.pop();
310
+ let entries = [];
311
+ try {
312
+ entries = await fsp.readdir(current, { withFileTypes: true });
313
+ } catch {
314
+ continue;
315
+ }
316
+ for (const entry of entries) {
317
+ const fullPath = path.join(current, entry.name);
318
+ if (entry.isDirectory()) {
319
+ queue.push(fullPath);
320
+ } else if (entry.isFile() && entry.name.includes(sessionId) && entry.name.endsWith(".jsonl")) {
321
+ return fullPath;
322
+ }
323
+ }
324
+ }
325
+ return null;
326
+ }
327
+
328
+ async function findClaudeSessionEntries(projectsDir, sessionId) {
329
+ const entries = [];
330
+ let projectDirs = [];
331
+ try {
332
+ projectDirs = await fsp.readdir(projectsDir, { withFileTypes: true });
333
+ } catch {
334
+ return entries;
335
+ }
336
+
337
+ for (const projectDir of projectDirs) {
338
+ if (!projectDir.isDirectory()) {
339
+ continue;
340
+ }
341
+ const projectPath = path.join(projectsDir, projectDir.name);
342
+ let files = [];
343
+ try {
344
+ files = await fsp.readdir(projectPath, { withFileTypes: true });
345
+ } catch {
346
+ continue;
347
+ }
348
+
349
+ for (const file of files) {
350
+ if (!file.isFile()) {
351
+ continue;
352
+ }
353
+ if (!file.name.endsWith(".jsonl") || file.name.startsWith("agent-")) {
354
+ continue;
355
+ }
356
+
357
+ const filePath = path.join(projectPath, file.name);
358
+ const rl = readline.createInterface({
359
+ input: fs.createReadStream(filePath),
360
+ crlfDelay: Infinity,
361
+ });
362
+
363
+ for await (const line of rl) {
364
+ const trimmed = line.trim();
365
+ if (!trimmed) {
366
+ continue;
367
+ }
368
+ let entry;
369
+ try {
370
+ entry = JSON.parse(trimmed);
371
+ } catch {
372
+ continue;
373
+ }
374
+ if (entry.sessionId === sessionId) {
375
+ entries.push({ ...entry, source: filePath });
376
+ }
377
+ }
378
+ }
379
+ }
380
+
381
+ return entries;
382
+ }
383
+
384
+ async function findPathByName(rootDir, sessionId) {
385
+ const queue = [rootDir];
386
+ while (queue.length) {
387
+ const current = queue.pop();
388
+ let entries = [];
389
+ try {
390
+ entries = await fsp.readdir(current, { withFileTypes: true });
391
+ } catch {
392
+ continue;
393
+ }
394
+ for (const entry of entries) {
395
+ const fullPath = path.join(current, entry.name);
396
+ if (entry.name.includes(sessionId)) {
397
+ return fullPath;
398
+ }
399
+ if (entry.isDirectory()) {
400
+ queue.push(fullPath);
401
+ }
402
+ }
403
+ }
404
+ return null;
405
+ }
406
+
407
+ async function pathExists(targetPath, expectedType) {
408
+ try {
409
+ const stats = await fsp.stat(targetPath);
410
+ if (expectedType === "file") {
411
+ return stats.isFile();
412
+ }
413
+ if (expectedType === "directory") {
414
+ return stats.isDirectory();
415
+ }
416
+ return true;
417
+ } catch {
418
+ return false;
419
+ }
420
+ }
@@ -0,0 +1,181 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+
4
+ import { SessionDiskStore } from "@love-moon/conductor-sdk";
5
+
6
+ const DEFAULT_TAIL_LINES = 200;
7
+ const MAX_TAIL_LINES = 500;
8
+ const DEFAULT_TAIL_BYTES = 256 * 1024;
9
+ const MAX_TAIL_BYTES = 1024 * 1024;
10
+
11
+ function clampPositiveInt(value, fallback, max) {
12
+ const parsed = Number.parseInt(String(value ?? ""), 10);
13
+ if (!Number.isFinite(parsed) || parsed <= 0) {
14
+ return fallback;
15
+ }
16
+ return Math.min(parsed, max);
17
+ }
18
+
19
+ function normalizeSince(value) {
20
+ if (typeof value !== "string" || !value.trim()) {
21
+ return null;
22
+ }
23
+ const timestamp = Date.parse(value);
24
+ return Number.isFinite(timestamp) ? timestamp : null;
25
+ }
26
+
27
+ function inferLevel(message) {
28
+ const normalized = String(message || "").toLowerCase();
29
+ if (normalized.includes("error") || normalized.includes("failed")) {
30
+ return "ERROR";
31
+ }
32
+ if (normalized.includes("warn")) {
33
+ return "WARN";
34
+ }
35
+ return "INFO";
36
+ }
37
+
38
+ function parseLogLine(line) {
39
+ const content = String(line || "").trim();
40
+ if (!content) {
41
+ return null;
42
+ }
43
+
44
+ const rfcMatch = content.match(/^\[([^\]]+)\]\s+\[([A-Z]+)\]\s+(.*)$/);
45
+ if (rfcMatch) {
46
+ const timestamp = Date.parse(rfcMatch[1]);
47
+ if (Number.isFinite(timestamp)) {
48
+ return {
49
+ timestamp: new Date(timestamp).toISOString(),
50
+ level: rfcMatch[2],
51
+ message: rfcMatch[3],
52
+ };
53
+ }
54
+ }
55
+
56
+ const conductorMatch = content.match(/^\[([^\]]+?)\s+(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2})\]\s+(.*)$/);
57
+ if (conductorMatch) {
58
+ const timestamp = Date.parse(conductorMatch[2]);
59
+ if (Number.isFinite(timestamp)) {
60
+ return {
61
+ timestamp: new Date(timestamp).toISOString(),
62
+ level: inferLevel(conductorMatch[3]),
63
+ message: `[${conductorMatch[1]}] ${conductorMatch[3]}`,
64
+ };
65
+ }
66
+ }
67
+
68
+ return {
69
+ timestamp: null,
70
+ level: inferLevel(content),
71
+ message: content,
72
+ };
73
+ }
74
+
75
+ function readTailText(filePath, maxBytes) {
76
+ const stats = fs.statSync(filePath);
77
+ const bytesToRead = Math.min(stats.size, maxBytes);
78
+ const start = Math.max(0, stats.size - bytesToRead);
79
+ const buffer = Buffer.alloc(bytesToRead);
80
+ const fd = fs.openSync(filePath, "r");
81
+ try {
82
+ fs.readSync(fd, buffer, 0, bytesToRead, start);
83
+ } finally {
84
+ fs.closeSync(fd);
85
+ }
86
+
87
+ let text = buffer.toString("utf8");
88
+ if (start > 0) {
89
+ const firstNewline = text.indexOf("\n");
90
+ text = firstNewline >= 0 ? text.slice(firstNewline + 1) : "";
91
+ }
92
+ return {
93
+ text,
94
+ truncatedByBytes: start > 0,
95
+ };
96
+ }
97
+
98
+ export class DaemonLogCollector {
99
+ constructor(backendUrl, options = {}) {
100
+ this.sessionStore = options.sessionStore || SessionDiskStore.forBackendUrl(backendUrl);
101
+ this.readTailText = options.readTailText || readTailText;
102
+ this.existsSync = options.existsSync || fs.existsSync;
103
+ }
104
+
105
+ collect(taskId, options = {}) {
106
+ const normalizedTaskId = String(taskId || "").trim();
107
+ const collectedAt = new Date().toISOString();
108
+ if (!normalizedTaskId) {
109
+ return {
110
+ projectPath: null,
111
+ logPath: null,
112
+ entries: [],
113
+ truncated: false,
114
+ error: "task_id is required",
115
+ collectedAt,
116
+ };
117
+ }
118
+
119
+ const record = this.sessionStore.findByTaskId(normalizedTaskId);
120
+ if (!record) {
121
+ return {
122
+ projectPath: null,
123
+ logPath: null,
124
+ entries: [],
125
+ truncated: false,
126
+ error: "Task not found in session store",
127
+ collectedAt,
128
+ };
129
+ }
130
+
131
+ const projectPath = path.resolve(record.projectPath);
132
+ const logPath = path.join(projectPath, "conductor.log");
133
+ if (!this.existsSync(logPath)) {
134
+ return {
135
+ projectPath,
136
+ logPath,
137
+ entries: [],
138
+ truncated: false,
139
+ error: "Log file not found",
140
+ collectedAt,
141
+ };
142
+ }
143
+
144
+ const tailLines = clampPositiveInt(options.tailLines, DEFAULT_TAIL_LINES, MAX_TAIL_LINES);
145
+ const maxBytes = clampPositiveInt(options.maxBytes, DEFAULT_TAIL_BYTES, MAX_TAIL_BYTES);
146
+ const sinceMs = normalizeSince(options.since);
147
+ const { text, truncatedByBytes } = this.readTailText(logPath, maxBytes);
148
+ let anchorTimestampMs = null;
149
+ const allEntries = [];
150
+ for (const line of text.split(/\r?\n/)) {
151
+ const entry = parseLogLine(line);
152
+ if (!entry) {
153
+ continue;
154
+ }
155
+ const entryTimestampMs =
156
+ typeof entry.timestamp === "string" ? Date.parse(entry.timestamp) : anchorTimestampMs;
157
+ if (typeof entry.timestamp === "string" && Number.isFinite(entryTimestampMs)) {
158
+ anchorTimestampMs = entryTimestampMs;
159
+ }
160
+ if (sinceMs !== null) {
161
+ if (!Number.isFinite(entryTimestampMs)) {
162
+ continue;
163
+ }
164
+ if (entryTimestampMs < sinceMs) {
165
+ continue;
166
+ }
167
+ }
168
+ allEntries.push(entry);
169
+ }
170
+ const entries = allEntries.slice(-tailLines);
171
+
172
+ return {
173
+ projectPath,
174
+ logPath,
175
+ entries,
176
+ truncated: truncatedByBytes || allEntries.length > entries.length,
177
+ error: null,
178
+ collectedAt,
179
+ };
180
+ }
181
+ }