doer-agent 0.2.4 → 0.2.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agent.js +2592 -439
- package/package.json +1 -1
package/dist/agent.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { spawn, spawnSync } from "node:child_process";
|
|
2
|
-
import {
|
|
3
|
-
import { chmod, mkdir, open, readFile, readdir, stat, writeFile } from "node:fs/promises";
|
|
2
|
+
import { existsSync, statSync, watch } from "node:fs";
|
|
3
|
+
import { chmod, mkdir, open, readFile, readdir, rename, rm, rmdir, stat, unlink, writeFile } from "node:fs/promises";
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
6
|
import { AckPolicy, connect, DeliverPolicy, JSONCodec, RetentionPolicy, StorageType, StringCodec } from "nats";
|
|
@@ -14,8 +14,16 @@ let workspaceRootOverride = null;
|
|
|
14
14
|
const fsRpcCodec = StringCodec();
|
|
15
15
|
const shellRpcCodec = StringCodec();
|
|
16
16
|
const runRpcCodec = StringCodec();
|
|
17
|
+
const sessionRpcCodec = StringCodec();
|
|
18
|
+
const codexAuthRpcCodec = StringCodec();
|
|
19
|
+
const settingsRpcCodec = StringCodec();
|
|
20
|
+
const gitRpcCodec = StringCodec();
|
|
17
21
|
const activeRuns = new Map();
|
|
18
22
|
const retainedRuns = new Map();
|
|
23
|
+
const activeSessionWatchers = new Map();
|
|
24
|
+
const sessionLineIndexCache = new Map();
|
|
25
|
+
const ANSI_RE = /\u001b\[[0-9;]*m/g;
|
|
26
|
+
let pendingCodexDeviceAuth = null;
|
|
19
27
|
function sanitizeUserId(userId) {
|
|
20
28
|
const normalized = userId.trim().replace(/[^a-zA-Z0-9_-]/g, "_");
|
|
21
29
|
return normalized.length > 0 ? normalized : "anonymous";
|
|
@@ -23,6 +31,18 @@ function sanitizeUserId(userId) {
|
|
|
23
31
|
function buildAgentRunRpcSubject(userId, agentId) {
|
|
24
32
|
return `doer.agent.run.rpc.${sanitizeUserId(userId)}.${agentId.trim()}`;
|
|
25
33
|
}
|
|
34
|
+
function buildAgentSessionRpcSubject(userId, agentId) {
|
|
35
|
+
return `doer.agent.session.rpc.${sanitizeUserId(userId)}.${agentId.trim()}`;
|
|
36
|
+
}
|
|
37
|
+
function buildAgentCodexAuthRpcSubject(userId, agentId) {
|
|
38
|
+
return `doer.agent.codex.auth.rpc.${sanitizeUserId(userId)}.${agentId.trim()}`;
|
|
39
|
+
}
|
|
40
|
+
function buildAgentSettingsRpcSubject(userId, agentId) {
|
|
41
|
+
return `doer.agent.settings.rpc.${sanitizeUserId(userId)}.${agentId.trim()}`;
|
|
42
|
+
}
|
|
43
|
+
function buildAgentGitRpcSubject(userId, agentId) {
|
|
44
|
+
return `doer.agent.git.rpc.${sanitizeUserId(userId)}.${agentId.trim()}`;
|
|
45
|
+
}
|
|
26
46
|
function normalizeNatsServers(value) {
|
|
27
47
|
if (!Array.isArray(value)) {
|
|
28
48
|
return [];
|
|
@@ -397,15 +417,16 @@ function normalizeRunRpcRequest(args) {
|
|
|
397
417
|
throw new Error("missing responseSubject");
|
|
398
418
|
}
|
|
399
419
|
const runId = typeof args.request.runId === "string" && args.request.runId.trim() ? args.request.runId.trim() : null;
|
|
400
|
-
const
|
|
401
|
-
|
|
402
|
-
|
|
420
|
+
const prompt = typeof args.request.prompt === "string" && args.request.prompt.trim() ? args.request.prompt.trim() : null;
|
|
421
|
+
const sessionId = typeof args.request.sessionId === "string" && args.request.sessionId.trim() ? args.request.sessionId.trim() : null;
|
|
422
|
+
const model = normalizeCodexModel(args.request.model);
|
|
423
|
+
if (action === "start" && !prompt) {
|
|
424
|
+
throw new Error("missing prompt");
|
|
403
425
|
}
|
|
404
426
|
if ((action === "get" || action === "cancel") && !runId) {
|
|
405
427
|
throw new Error("missing runId");
|
|
406
428
|
}
|
|
407
429
|
const cwd = typeof args.request.cwd === "string" && args.request.cwd.trim() ? args.request.cwd.trim() : null;
|
|
408
|
-
const chatId = typeof args.request.chatId === "string" && args.request.chatId.trim() ? args.request.chatId.trim() : null;
|
|
409
430
|
const sinceSeqRaw = Number(args.request.sinceSeq);
|
|
410
431
|
const sinceSeq = Number.isInteger(sinceSeqRaw) && sinceSeqRaw >= 0 ? sinceSeqRaw : null;
|
|
411
432
|
const limitRaw = Number(args.request.limit);
|
|
@@ -414,9 +435,10 @@ function normalizeRunRpcRequest(args) {
|
|
|
414
435
|
requestId,
|
|
415
436
|
action,
|
|
416
437
|
runId,
|
|
417
|
-
|
|
438
|
+
prompt,
|
|
439
|
+
sessionId,
|
|
440
|
+
model,
|
|
418
441
|
cwd,
|
|
419
|
-
chatId,
|
|
420
442
|
responseSubject,
|
|
421
443
|
sinceSeq,
|
|
422
444
|
limit,
|
|
@@ -427,26 +449,573 @@ function normalizeRunRpcRequest(args) {
|
|
|
427
449
|
function publishRunRpcResponse(args) {
|
|
428
450
|
args.nc.publish(args.responseSubject, runRpcCodec.encode(JSON.stringify(args.payload)));
|
|
429
451
|
}
|
|
430
|
-
async function
|
|
452
|
+
async function resolveRunsDir() {
|
|
431
453
|
const workspaceRoot = workspaceRootOverride ?? (process.env.WORKSPACE?.trim() || process.cwd());
|
|
432
454
|
const dir = path.join(workspaceRoot, ".doer-agent", "runs");
|
|
433
455
|
await mkdir(dir, { recursive: true });
|
|
434
456
|
return dir;
|
|
435
457
|
}
|
|
436
|
-
function
|
|
458
|
+
async function resetRunsDir() {
|
|
459
|
+
const dir = await resolveRunsDir();
|
|
460
|
+
await rm(dir, { recursive: true, force: true }).catch(() => undefined);
|
|
461
|
+
await mkdir(dir, { recursive: true });
|
|
462
|
+
}
|
|
463
|
+
async function persistRunTask(task) {
|
|
464
|
+
const dir = await resolveRunsDir();
|
|
465
|
+
const payload = {
|
|
466
|
+
runId: task.id,
|
|
467
|
+
agentId: task.agentId,
|
|
468
|
+
userId: task.userId,
|
|
469
|
+
sessionId: task.sessionId,
|
|
470
|
+
sessionFilePath: task.sessionFilePath,
|
|
471
|
+
status: task.status,
|
|
472
|
+
cancelRequested: task.cancelRequested,
|
|
473
|
+
createdAt: task.createdAt,
|
|
474
|
+
updatedAt: task.updatedAt,
|
|
475
|
+
startedAt: task.startedAt,
|
|
476
|
+
finishedAt: task.finishedAt,
|
|
477
|
+
error: task.error,
|
|
478
|
+
};
|
|
479
|
+
await writeFile(path.join(dir, `${task.id}.json`), `${JSON.stringify(payload, null, 2)}\n`, "utf8");
|
|
480
|
+
}
|
|
481
|
+
async function removeRunTask(runId) {
|
|
482
|
+
const dir = await resolveRunsDir();
|
|
483
|
+
await unlink(path.join(dir, `${runId}.json`)).catch(() => undefined);
|
|
484
|
+
}
|
|
485
|
+
function sanitizeRunLockSegment(value) {
|
|
486
|
+
return value.trim().replace(/[^a-zA-Z0-9._-]/g, "_").slice(0, 160) || "lock";
|
|
487
|
+
}
|
|
488
|
+
async function resolveRunLocksDir() {
|
|
489
|
+
const dir = path.join(await resolveRunsDir(), "locks");
|
|
490
|
+
await mkdir(dir, { recursive: true });
|
|
491
|
+
return dir;
|
|
492
|
+
}
|
|
493
|
+
async function resolveRunStartLockPath(args) {
|
|
494
|
+
const dir = await resolveRunLocksDir();
|
|
495
|
+
if (typeof args.sessionId === "string" && args.sessionId.trim()) {
|
|
496
|
+
return path.join(dir, `session__${sanitizeRunLockSegment(args.sessionId)}.lock`);
|
|
497
|
+
}
|
|
498
|
+
return path.join(dir, `run__${sanitizeRunLockSegment(args.runId)}.lock`);
|
|
499
|
+
}
|
|
500
|
+
async function claimRunStartSlot(args) {
|
|
501
|
+
const lockPath = await resolveRunStartLockPath(args);
|
|
502
|
+
try {
|
|
503
|
+
const handle = await open(lockPath, "wx");
|
|
504
|
+
try {
|
|
505
|
+
const payload = {
|
|
506
|
+
runId: args.runId,
|
|
507
|
+
sessionId: typeof args.sessionId === "string" && args.sessionId.trim() ? args.sessionId.trim() : null,
|
|
508
|
+
pid: process.pid,
|
|
509
|
+
createdAt: formatLocalTimestamp(),
|
|
510
|
+
};
|
|
511
|
+
await handle.writeFile(`${JSON.stringify(payload, null, 2)}\n`, "utf8");
|
|
512
|
+
}
|
|
513
|
+
finally {
|
|
514
|
+
await handle.close().catch(() => undefined);
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
catch (error) {
|
|
518
|
+
if (error?.code === "EEXIST") {
|
|
519
|
+
const lockContents = await readFile(lockPath, "utf8").catch(() => "");
|
|
520
|
+
const existingRunId = (() => {
|
|
521
|
+
try {
|
|
522
|
+
const parsed = JSON.parse(lockContents);
|
|
523
|
+
return typeof parsed.runId === "string" && parsed.runId.trim() ? parsed.runId.trim() : null;
|
|
524
|
+
}
|
|
525
|
+
catch {
|
|
526
|
+
return null;
|
|
527
|
+
}
|
|
528
|
+
})();
|
|
529
|
+
throw new Error(existingRunId ? `Another run is already active: ${existingRunId}` : "Another run is already active");
|
|
530
|
+
}
|
|
531
|
+
throw error;
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
async function updateRunStartSlotSession(args) {
|
|
535
|
+
const nextSessionId = args.sessionId.trim();
|
|
536
|
+
if (!nextSessionId) {
|
|
537
|
+
return;
|
|
538
|
+
}
|
|
539
|
+
const previousSessionId = typeof args.previousSessionId === "string" && args.previousSessionId.trim() ? args.previousSessionId.trim() : null;
|
|
540
|
+
if (previousSessionId === nextSessionId) {
|
|
541
|
+
return;
|
|
542
|
+
}
|
|
543
|
+
const currentPath = await resolveRunStartLockPath({ runId: args.runId, sessionId: previousSessionId });
|
|
544
|
+
const nextPath = await resolveRunStartLockPath({ runId: args.runId, sessionId: nextSessionId });
|
|
545
|
+
if (currentPath === nextPath) {
|
|
546
|
+
return;
|
|
547
|
+
}
|
|
548
|
+
try {
|
|
549
|
+
await rename(currentPath, nextPath);
|
|
550
|
+
}
|
|
551
|
+
catch (error) {
|
|
552
|
+
const code = error?.code;
|
|
553
|
+
if (code === "ENOENT") {
|
|
554
|
+
// Lock may already be released; nothing to migrate.
|
|
555
|
+
return;
|
|
556
|
+
}
|
|
557
|
+
if (code === "EEXIST") {
|
|
558
|
+
throw new Error(`Another run is already active for session: ${nextSessionId}`);
|
|
559
|
+
}
|
|
560
|
+
throw error;
|
|
561
|
+
}
|
|
562
|
+
const payload = {
|
|
563
|
+
runId: args.runId,
|
|
564
|
+
sessionId: nextSessionId,
|
|
565
|
+
pid: process.pid,
|
|
566
|
+
createdAt: formatLocalTimestamp(),
|
|
567
|
+
};
|
|
568
|
+
await writeFile(nextPath, `${JSON.stringify(payload, null, 2)}\n`, "utf8");
|
|
569
|
+
}
|
|
570
|
+
async function releaseRunStartSlot(args) {
|
|
571
|
+
const paths = new Set();
|
|
572
|
+
paths.add(await resolveRunStartLockPath({ runId: args.runId, sessionId: args.sessionId ?? null }));
|
|
573
|
+
paths.add(await resolveRunStartLockPath({ runId: args.runId, sessionId: null }));
|
|
574
|
+
for (const lockPath of paths) {
|
|
575
|
+
await unlink(lockPath).catch(() => undefined);
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
function resolveAgentSettingsDir() {
|
|
579
|
+
const workspaceRoot = workspaceRootOverride ?? (process.env.WORKSPACE?.trim() || process.cwd());
|
|
580
|
+
return path.join(workspaceRoot, ".doer-agent");
|
|
581
|
+
}
|
|
582
|
+
function resolveAgentSettingsFilePath() {
|
|
583
|
+
return path.join(resolveAgentSettingsDir(), "config.json");
|
|
584
|
+
}
|
|
585
|
+
function createDefaultAgentSettingsConfig() {
|
|
586
|
+
return {
|
|
587
|
+
general: {
|
|
588
|
+
firstTurnPrompt: null,
|
|
589
|
+
},
|
|
590
|
+
codex: {
|
|
591
|
+
model: "gpt-5.4",
|
|
592
|
+
authMode: "api_key",
|
|
593
|
+
apiKey: null,
|
|
594
|
+
},
|
|
595
|
+
realtime: {
|
|
596
|
+
model: process.env.OPENAI_REALTIME_MODEL?.trim() || "gpt-realtime",
|
|
597
|
+
voice: process.env.OPENAI_REALTIME_VOICE?.trim() || "alloy",
|
|
598
|
+
wakeName: null,
|
|
599
|
+
requireWakeName: true,
|
|
600
|
+
apiKey: null,
|
|
601
|
+
},
|
|
602
|
+
git: {
|
|
603
|
+
enabled: true,
|
|
604
|
+
name: null,
|
|
605
|
+
email: null,
|
|
606
|
+
authMode: "none",
|
|
607
|
+
oauthToken: null,
|
|
608
|
+
oauthLogin: null,
|
|
609
|
+
oauthScope: null,
|
|
610
|
+
},
|
|
611
|
+
aws: {
|
|
612
|
+
enabled: true,
|
|
613
|
+
accessKeyId: null,
|
|
614
|
+
defaultRegion: null,
|
|
615
|
+
secretAccessKey: null,
|
|
616
|
+
sessionToken: null,
|
|
617
|
+
},
|
|
618
|
+
jira: {
|
|
619
|
+
baseUrl: null,
|
|
620
|
+
email: null,
|
|
621
|
+
enabled: false,
|
|
622
|
+
apiToken: null,
|
|
623
|
+
},
|
|
624
|
+
notion: {
|
|
625
|
+
baseUrl: "https://api.notion.com",
|
|
626
|
+
version: "2022-06-28",
|
|
627
|
+
enabled: false,
|
|
628
|
+
apiToken: null,
|
|
629
|
+
},
|
|
630
|
+
slack: {
|
|
631
|
+
baseUrl: "https://slack.com/api",
|
|
632
|
+
enabled: false,
|
|
633
|
+
botToken: null,
|
|
634
|
+
},
|
|
635
|
+
figma: {
|
|
636
|
+
baseUrl: "https://api.figma.com",
|
|
637
|
+
enabled: false,
|
|
638
|
+
apiToken: null,
|
|
639
|
+
},
|
|
640
|
+
};
|
|
641
|
+
}
|
|
642
|
+
function normalizeNullableString(value) {
|
|
643
|
+
if (value === null) {
|
|
644
|
+
return null;
|
|
645
|
+
}
|
|
646
|
+
if (typeof value !== "string") {
|
|
647
|
+
return null;
|
|
648
|
+
}
|
|
649
|
+
const trimmed = value.trim();
|
|
650
|
+
return trimmed ? trimmed : null;
|
|
651
|
+
}
|
|
652
|
+
function normalizeAgentSettingsConfig(value, fallback) {
|
|
653
|
+
const base = fallback ?? createDefaultAgentSettingsConfig();
|
|
654
|
+
const raw = value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
655
|
+
const general = raw.general && typeof raw.general === "object" ? raw.general : {};
|
|
656
|
+
const codex = raw.codex && typeof raw.codex === "object" ? raw.codex : {};
|
|
657
|
+
const realtime = raw.realtime && typeof raw.realtime === "object" ? raw.realtime : {};
|
|
658
|
+
const git = raw.git && typeof raw.git === "object" ? raw.git : {};
|
|
659
|
+
const aws = raw.aws && typeof raw.aws === "object" ? raw.aws : {};
|
|
660
|
+
const jira = raw.jira && typeof raw.jira === "object" ? raw.jira : {};
|
|
661
|
+
const notion = raw.notion && typeof raw.notion === "object" ? raw.notion : {};
|
|
662
|
+
const slack = raw.slack && typeof raw.slack === "object" ? raw.slack : {};
|
|
663
|
+
const figma = raw.figma && typeof raw.figma === "object" ? raw.figma : {};
|
|
664
|
+
return {
|
|
665
|
+
general: {
|
|
666
|
+
firstTurnPrompt: normalizeNullableString(general.firstTurnPrompt) ?? base.general.firstTurnPrompt,
|
|
667
|
+
},
|
|
668
|
+
codex: {
|
|
669
|
+
model: typeof codex.model === "string" && codex.model.trim() ? codex.model.trim() : base.codex.model,
|
|
670
|
+
authMode: codex.authMode === "oauth" ? "oauth" : codex.authMode === "api_key" ? "api_key" : base.codex.authMode,
|
|
671
|
+
apiKey: codex.apiKey === null ? null : normalizeNullableString(codex.apiKey) ?? base.codex.apiKey,
|
|
672
|
+
},
|
|
673
|
+
realtime: {
|
|
674
|
+
model: typeof realtime.model === "string" && realtime.model.trim() ? realtime.model.trim() : base.realtime.model,
|
|
675
|
+
voice: typeof realtime.voice === "string" && realtime.voice.trim() ? realtime.voice.trim() : base.realtime.voice,
|
|
676
|
+
wakeName: realtime.wakeName === null ? null : normalizeNullableString(realtime.wakeName) ?? base.realtime.wakeName,
|
|
677
|
+
requireWakeName: typeof realtime.requireWakeName === "boolean" ? realtime.requireWakeName : base.realtime.requireWakeName,
|
|
678
|
+
apiKey: realtime.apiKey === null ? null : normalizeNullableString(realtime.apiKey) ?? base.realtime.apiKey,
|
|
679
|
+
},
|
|
680
|
+
git: {
|
|
681
|
+
enabled: typeof git.enabled === "boolean" ? git.enabled : base.git.enabled,
|
|
682
|
+
name: git.name === null ? null : normalizeNullableString(git.name) ?? base.git.name,
|
|
683
|
+
email: git.email === null ? null : normalizeNullableString(git.email) ?? base.git.email,
|
|
684
|
+
authMode: git.authMode === "oauth_app" ? "oauth_app" : git.authMode === "none" ? "none" : base.git.authMode,
|
|
685
|
+
oauthToken: git.oauthToken === null ? null : normalizeNullableString(git.oauthToken) ?? base.git.oauthToken,
|
|
686
|
+
oauthLogin: git.oauthLogin === null ? null : normalizeNullableString(git.oauthLogin) ?? base.git.oauthLogin,
|
|
687
|
+
oauthScope: git.oauthScope === null ? null : normalizeNullableString(git.oauthScope) ?? base.git.oauthScope,
|
|
688
|
+
},
|
|
689
|
+
aws: {
|
|
690
|
+
enabled: typeof aws.enabled === "boolean" ? aws.enabled : base.aws.enabled,
|
|
691
|
+
accessKeyId: aws.accessKeyId === null ? null : normalizeNullableString(aws.accessKeyId) ?? base.aws.accessKeyId,
|
|
692
|
+
defaultRegion: aws.defaultRegion === null ? null : normalizeNullableString(aws.defaultRegion) ?? base.aws.defaultRegion,
|
|
693
|
+
secretAccessKey: aws.secretAccessKey === null ? null : normalizeNullableString(aws.secretAccessKey) ?? base.aws.secretAccessKey,
|
|
694
|
+
sessionToken: aws.sessionToken === null ? null : normalizeNullableString(aws.sessionToken) ?? base.aws.sessionToken,
|
|
695
|
+
},
|
|
696
|
+
jira: {
|
|
697
|
+
baseUrl: jira.baseUrl === null ? null : normalizeNullableString(jira.baseUrl) ?? base.jira.baseUrl,
|
|
698
|
+
email: jira.email === null ? null : normalizeNullableString(jira.email) ?? base.jira.email,
|
|
699
|
+
enabled: typeof jira.enabled === "boolean" ? jira.enabled : base.jira.enabled,
|
|
700
|
+
apiToken: jira.apiToken === null ? null : normalizeNullableString(jira.apiToken) ?? base.jira.apiToken,
|
|
701
|
+
},
|
|
702
|
+
notion: {
|
|
703
|
+
baseUrl: notion.baseUrl === null ? null : normalizeNullableString(notion.baseUrl) ?? base.notion.baseUrl,
|
|
704
|
+
version: notion.version === null ? null : normalizeNullableString(notion.version) ?? base.notion.version,
|
|
705
|
+
enabled: typeof notion.enabled === "boolean" ? notion.enabled : base.notion.enabled,
|
|
706
|
+
apiToken: notion.apiToken === null ? null : normalizeNullableString(notion.apiToken) ?? base.notion.apiToken,
|
|
707
|
+
},
|
|
708
|
+
slack: {
|
|
709
|
+
baseUrl: slack.baseUrl === null ? null : normalizeNullableString(slack.baseUrl) ?? base.slack.baseUrl,
|
|
710
|
+
enabled: typeof slack.enabled === "boolean" ? slack.enabled : base.slack.enabled,
|
|
711
|
+
botToken: slack.botToken === null ? null : normalizeNullableString(slack.botToken) ?? base.slack.botToken,
|
|
712
|
+
},
|
|
713
|
+
figma: {
|
|
714
|
+
baseUrl: figma.baseUrl === null ? null : normalizeNullableString(figma.baseUrl) ?? base.figma.baseUrl,
|
|
715
|
+
enabled: typeof figma.enabled === "boolean" ? figma.enabled : base.figma.enabled,
|
|
716
|
+
apiToken: figma.apiToken === null ? null : normalizeNullableString(figma.apiToken) ?? base.figma.apiToken,
|
|
717
|
+
},
|
|
718
|
+
};
|
|
719
|
+
}
|
|
720
|
+
async function readAgentSettingsConfig(defaults) {
|
|
721
|
+
const fallback = normalizeAgentSettingsConfig(defaults ?? null);
|
|
722
|
+
const filePath = resolveAgentSettingsFilePath();
|
|
723
|
+
const raw = await readFile(filePath, "utf8").catch(() => "");
|
|
724
|
+
if (!raw.trim()) {
|
|
725
|
+
return fallback;
|
|
726
|
+
}
|
|
727
|
+
try {
|
|
728
|
+
return normalizeAgentSettingsConfig(JSON.parse(raw), fallback);
|
|
729
|
+
}
|
|
730
|
+
catch {
|
|
731
|
+
return fallback;
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
async function writeAgentSettingsConfig(config) {
|
|
735
|
+
const dir = resolveAgentSettingsDir();
|
|
736
|
+
await mkdir(dir, { recursive: true });
|
|
737
|
+
await writeFile(resolveAgentSettingsFilePath(), `${JSON.stringify(config, null, 2)}\n`, "utf8");
|
|
738
|
+
}
|
|
739
|
+
function maskSecretPreview(secret) {
|
|
740
|
+
if (secret.length <= 6) {
|
|
741
|
+
return `${secret.slice(0, 1)}***${secret.slice(-1)}`;
|
|
742
|
+
}
|
|
743
|
+
return `${secret.slice(0, 4)}...${secret.slice(-4)}`;
|
|
744
|
+
}
|
|
745
|
+
function toMaskedSecret(value) {
|
|
746
|
+
if (!value) {
|
|
747
|
+
return { has: false, masked: null, length: null };
|
|
748
|
+
}
|
|
749
|
+
return { has: true, masked: maskSecretPreview(value), length: value.length };
|
|
750
|
+
}
|
|
751
|
+
function toAgentSettingsPublic(config) {
|
|
752
|
+
const codexKey = toMaskedSecret(config.codex.apiKey);
|
|
753
|
+
const realtimeKey = toMaskedSecret(config.realtime.apiKey);
|
|
754
|
+
const gitOauth = toMaskedSecret(config.git.oauthToken);
|
|
755
|
+
const awsSecret = toMaskedSecret(config.aws.secretAccessKey);
|
|
756
|
+
const awsSession = toMaskedSecret(config.aws.sessionToken);
|
|
757
|
+
const jiraToken = toMaskedSecret(config.jira.apiToken);
|
|
758
|
+
const notionToken = toMaskedSecret(config.notion.apiToken);
|
|
759
|
+
const slackToken = toMaskedSecret(config.slack.botToken);
|
|
760
|
+
const figmaToken = toMaskedSecret(config.figma.apiToken);
|
|
761
|
+
return {
|
|
762
|
+
general: {
|
|
763
|
+
firstTurnPrompt: config.general.firstTurnPrompt,
|
|
764
|
+
},
|
|
765
|
+
codex: {
|
|
766
|
+
model: config.codex.model,
|
|
767
|
+
authMode: config.codex.authMode,
|
|
768
|
+
hasApiKey: codexKey.has,
|
|
769
|
+
apiKeyMasked: codexKey.masked,
|
|
770
|
+
apiKeyLength: codexKey.length,
|
|
771
|
+
},
|
|
772
|
+
realtime: {
|
|
773
|
+
model: config.realtime.model,
|
|
774
|
+
voice: config.realtime.voice,
|
|
775
|
+
wakeName: config.realtime.wakeName,
|
|
776
|
+
requireWakeName: config.realtime.requireWakeName,
|
|
777
|
+
hasApiKey: realtimeKey.has,
|
|
778
|
+
apiKeyMasked: realtimeKey.masked,
|
|
779
|
+
apiKeyLength: realtimeKey.length,
|
|
780
|
+
},
|
|
781
|
+
git: {
|
|
782
|
+
enabled: config.git.enabled,
|
|
783
|
+
name: config.git.name,
|
|
784
|
+
email: config.git.email,
|
|
785
|
+
authMode: config.git.authMode,
|
|
786
|
+
hasOauthToken: gitOauth.has,
|
|
787
|
+
oauthTokenMasked: gitOauth.masked,
|
|
788
|
+
oauthTokenLength: gitOauth.length,
|
|
789
|
+
oauthLogin: config.git.oauthLogin,
|
|
790
|
+
oauthScope: config.git.oauthScope,
|
|
791
|
+
},
|
|
792
|
+
aws: {
|
|
793
|
+
enabled: config.aws.enabled,
|
|
794
|
+
accessKeyId: config.aws.accessKeyId,
|
|
795
|
+
defaultRegion: config.aws.defaultRegion,
|
|
796
|
+
hasSecretAccessKey: awsSecret.has,
|
|
797
|
+
secretAccessKeyMasked: awsSecret.masked,
|
|
798
|
+
secretAccessKeyLength: awsSecret.length,
|
|
799
|
+
hasSessionToken: awsSession.has,
|
|
800
|
+
sessionTokenMasked: awsSession.masked,
|
|
801
|
+
sessionTokenLength: awsSession.length,
|
|
802
|
+
},
|
|
803
|
+
jira: {
|
|
804
|
+
baseUrl: config.jira.baseUrl,
|
|
805
|
+
email: config.jira.email,
|
|
806
|
+
enabled: config.jira.enabled,
|
|
807
|
+
hasApiToken: jiraToken.has,
|
|
808
|
+
apiTokenMasked: jiraToken.masked,
|
|
809
|
+
apiTokenLength: jiraToken.length,
|
|
810
|
+
},
|
|
811
|
+
notion: {
|
|
812
|
+
baseUrl: config.notion.baseUrl,
|
|
813
|
+
version: config.notion.version,
|
|
814
|
+
enabled: config.notion.enabled,
|
|
815
|
+
hasApiToken: notionToken.has,
|
|
816
|
+
apiTokenMasked: notionToken.masked,
|
|
817
|
+
apiTokenLength: notionToken.length,
|
|
818
|
+
},
|
|
819
|
+
slack: {
|
|
820
|
+
baseUrl: config.slack.baseUrl,
|
|
821
|
+
enabled: config.slack.enabled,
|
|
822
|
+
hasBotToken: slackToken.has,
|
|
823
|
+
botTokenMasked: slackToken.masked,
|
|
824
|
+
botTokenLength: slackToken.length,
|
|
825
|
+
},
|
|
826
|
+
figma: {
|
|
827
|
+
baseUrl: config.figma.baseUrl,
|
|
828
|
+
enabled: config.figma.enabled,
|
|
829
|
+
hasApiToken: figmaToken.has,
|
|
830
|
+
apiTokenMasked: figmaToken.masked,
|
|
831
|
+
apiTokenLength: figmaToken.length,
|
|
832
|
+
},
|
|
833
|
+
};
|
|
834
|
+
}
|
|
835
|
+
function normalizeAgentSettingsPatch(value) {
|
|
836
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
837
|
+
return {};
|
|
838
|
+
}
|
|
839
|
+
const raw = value;
|
|
840
|
+
const patch = { ...raw };
|
|
841
|
+
const assignNested = (section, key, value) => {
|
|
842
|
+
const current = patch[section] && typeof patch[section] === "object" && !Array.isArray(patch[section])
|
|
843
|
+
? ({ ...patch[section] })
|
|
844
|
+
: {};
|
|
845
|
+
current[key] = value;
|
|
846
|
+
patch[section] = current;
|
|
847
|
+
};
|
|
848
|
+
const move = (flatKey, section, key) => {
|
|
849
|
+
if (!(flatKey in raw)) {
|
|
850
|
+
return;
|
|
851
|
+
}
|
|
852
|
+
assignNested(section, key, raw[flatKey]);
|
|
853
|
+
delete patch[flatKey];
|
|
854
|
+
};
|
|
855
|
+
move("firstTurnPrompt", "general", "firstTurnPrompt");
|
|
856
|
+
move("codexModel", "codex", "model");
|
|
857
|
+
move("codexAuthMode", "codex", "authMode");
|
|
858
|
+
move("codexApiKey", "codex", "apiKey");
|
|
859
|
+
move("realtimeModel", "realtime", "model");
|
|
860
|
+
move("realtimeVoice", "realtime", "voice");
|
|
861
|
+
move("realtimeWakeName", "realtime", "wakeName");
|
|
862
|
+
move("realtimeRequireWakeName", "realtime", "requireWakeName");
|
|
863
|
+
move("realtimeApiKey", "realtime", "apiKey");
|
|
864
|
+
move("gitEnabled", "git", "enabled");
|
|
865
|
+
move("gitName", "git", "name");
|
|
866
|
+
move("gitEmail", "git", "email");
|
|
867
|
+
move("gitAuthMode", "git", "authMode");
|
|
868
|
+
move("gitOauthToken", "git", "oauthToken");
|
|
869
|
+
move("gitOauthLogin", "git", "oauthLogin");
|
|
870
|
+
move("gitOauthScope", "git", "oauthScope");
|
|
871
|
+
move("awsEnabled", "aws", "enabled");
|
|
872
|
+
move("awsAccessKeyId", "aws", "accessKeyId");
|
|
873
|
+
move("awsDefaultRegion", "aws", "defaultRegion");
|
|
874
|
+
move("awsSecretAccessKey", "aws", "secretAccessKey");
|
|
875
|
+
move("awsSessionToken", "aws", "sessionToken");
|
|
876
|
+
move("jiraBaseUrl", "jira", "baseUrl");
|
|
877
|
+
move("jiraEmail", "jira", "email");
|
|
878
|
+
move("jiraEnabled", "jira", "enabled");
|
|
879
|
+
move("jiraApiToken", "jira", "apiToken");
|
|
880
|
+
move("notionBaseUrl", "notion", "baseUrl");
|
|
881
|
+
move("notionVersion", "notion", "version");
|
|
882
|
+
move("notionEnabled", "notion", "enabled");
|
|
883
|
+
move("notionApiToken", "notion", "apiToken");
|
|
884
|
+
move("slackBaseUrl", "slack", "baseUrl");
|
|
885
|
+
move("slackEnabled", "slack", "enabled");
|
|
886
|
+
move("slackBotToken", "slack", "botToken");
|
|
887
|
+
move("figmaBaseUrl", "figma", "baseUrl");
|
|
888
|
+
move("figmaEnabled", "figma", "enabled");
|
|
889
|
+
move("figmaApiToken", "figma", "apiToken");
|
|
890
|
+
return patch;
|
|
891
|
+
}
|
|
892
|
+
async function resolveAgentSettingsConfig(args) {
|
|
893
|
+
const existing = await readAgentSettingsConfig(args.defaults ?? null);
|
|
894
|
+
const next = normalizeAgentSettingsConfig(args.patch ?? null, existing);
|
|
895
|
+
return next;
|
|
896
|
+
}
|
|
897
|
+
function buildAgentSettingsEnvPatch(config) {
|
|
898
|
+
const envPatch = {};
|
|
899
|
+
if (config.codex.authMode === "api_key" && config.codex.apiKey) {
|
|
900
|
+
envPatch.OPENAI_API_KEY = config.codex.apiKey;
|
|
901
|
+
}
|
|
902
|
+
if (config.git.enabled) {
|
|
903
|
+
if (config.git.name)
|
|
904
|
+
envPatch.GIT_AUTHOR_NAME = config.git.name;
|
|
905
|
+
if (config.git.name)
|
|
906
|
+
envPatch.GIT_COMMITTER_NAME = config.git.name;
|
|
907
|
+
if (config.git.email)
|
|
908
|
+
envPatch.GIT_AUTHOR_EMAIL = config.git.email;
|
|
909
|
+
if (config.git.email)
|
|
910
|
+
envPatch.GIT_COMMITTER_EMAIL = config.git.email;
|
|
911
|
+
if (config.git.oauthToken)
|
|
912
|
+
envPatch.GITHUB_TOKEN = config.git.oauthToken;
|
|
913
|
+
if (config.git.oauthToken)
|
|
914
|
+
envPatch.GH_TOKEN = config.git.oauthToken;
|
|
915
|
+
if (config.git.oauthLogin)
|
|
916
|
+
envPatch.DOER_GIT_OAUTH_LOGIN = config.git.oauthLogin;
|
|
917
|
+
if (config.git.oauthScope)
|
|
918
|
+
envPatch.DOER_GIT_OAUTH_SCOPE = config.git.oauthScope;
|
|
919
|
+
}
|
|
920
|
+
if (config.aws.enabled) {
|
|
921
|
+
if (config.aws.accessKeyId)
|
|
922
|
+
envPatch.AWS_ACCESS_KEY_ID = config.aws.accessKeyId;
|
|
923
|
+
if (config.aws.defaultRegion)
|
|
924
|
+
envPatch.AWS_DEFAULT_REGION = config.aws.defaultRegion;
|
|
925
|
+
if (config.aws.defaultRegion)
|
|
926
|
+
envPatch.AWS_REGION = config.aws.defaultRegion;
|
|
927
|
+
if (config.aws.secretAccessKey)
|
|
928
|
+
envPatch.AWS_SECRET_ACCESS_KEY = config.aws.secretAccessKey;
|
|
929
|
+
if (config.aws.sessionToken)
|
|
930
|
+
envPatch.AWS_SESSION_TOKEN = config.aws.sessionToken;
|
|
931
|
+
}
|
|
932
|
+
if (config.jira.enabled) {
|
|
933
|
+
if (config.jira.baseUrl)
|
|
934
|
+
envPatch.JIRA_BASE_URL = config.jira.baseUrl;
|
|
935
|
+
if (config.jira.email)
|
|
936
|
+
envPatch.JIRA_EMAIL = config.jira.email;
|
|
937
|
+
if (config.jira.apiToken)
|
|
938
|
+
envPatch.JIRA_API_TOKEN = config.jira.apiToken;
|
|
939
|
+
}
|
|
940
|
+
if (config.notion.enabled) {
|
|
941
|
+
if (config.notion.baseUrl)
|
|
942
|
+
envPatch.NOTION_BASE_URL = config.notion.baseUrl;
|
|
943
|
+
if (config.notion.version)
|
|
944
|
+
envPatch.NOTION_VERSION = config.notion.version;
|
|
945
|
+
if (config.notion.apiToken)
|
|
946
|
+
envPatch.NOTION_API_TOKEN = config.notion.apiToken;
|
|
947
|
+
}
|
|
948
|
+
if (config.slack.enabled) {
|
|
949
|
+
if (config.slack.baseUrl)
|
|
950
|
+
envPatch.SLACK_BASE_URL = config.slack.baseUrl;
|
|
951
|
+
if (config.slack.botToken)
|
|
952
|
+
envPatch.SLACK_BOT_TOKEN = config.slack.botToken;
|
|
953
|
+
}
|
|
954
|
+
if (config.figma.enabled) {
|
|
955
|
+
if (config.figma.baseUrl)
|
|
956
|
+
envPatch.FIGMA_BASE_URL = config.figma.baseUrl;
|
|
957
|
+
if (config.figma.apiToken)
|
|
958
|
+
envPatch.FIGMA_API_TOKEN = config.figma.apiToken;
|
|
959
|
+
}
|
|
960
|
+
return envPatch;
|
|
961
|
+
}
|
|
962
|
+
function cloneRunTask(task, _sinceSeq) {
|
|
437
963
|
return {
|
|
438
964
|
...task,
|
|
439
|
-
events: task.events
|
|
440
|
-
.filter((event) => typeof sinceSeq === "number" ? event.seq > sinceSeq : true)
|
|
441
|
-
.map((event) => ({ ...event, payload: { ...event.payload } })),
|
|
442
965
|
};
|
|
443
966
|
}
|
|
444
|
-
function
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
967
|
+
function extractCodexSessionMetadata(value) {
|
|
968
|
+
try {
|
|
969
|
+
const parsed = JSON.parse(value);
|
|
970
|
+
const lineType = typeof parsed.type === "string" ? parsed.type : "";
|
|
971
|
+
if (!parsed.payload || typeof parsed.payload !== "object" || Array.isArray(parsed.payload)) {
|
|
972
|
+
return { sessionId: null, sessionFilePath: null };
|
|
973
|
+
}
|
|
974
|
+
const payload = parsed.payload;
|
|
975
|
+
const sessionIdCandidate = typeof payload.sessionId === "string" && payload.sessionId.trim()
|
|
976
|
+
? payload.sessionId.trim()
|
|
977
|
+
: typeof payload.session_id === "string" && payload.session_id.trim()
|
|
978
|
+
? payload.session_id.trim()
|
|
979
|
+
: typeof payload.id === "string" && payload.id.trim() && (lineType === "session_meta" || lineType === "session.started")
|
|
980
|
+
? payload.id.trim()
|
|
981
|
+
: null;
|
|
982
|
+
const filePathCandidate = typeof payload.rollout_path === "string" && payload.rollout_path.trim()
|
|
983
|
+
? payload.rollout_path.trim()
|
|
984
|
+
: typeof payload.filePath === "string" && payload.filePath.trim()
|
|
985
|
+
? payload.filePath.trim()
|
|
986
|
+
: null;
|
|
987
|
+
return {
|
|
988
|
+
sessionId: sessionIdCandidate,
|
|
989
|
+
sessionFilePath: filePathCandidate,
|
|
990
|
+
};
|
|
991
|
+
}
|
|
992
|
+
catch {
|
|
993
|
+
return { sessionId: null, sessionFilePath: null };
|
|
994
|
+
}
|
|
995
|
+
}
|
|
996
|
+
async function updateRunSessionMetadata(task, metadata) {
|
|
997
|
+
let changed = false;
|
|
998
|
+
const previousSessionId = task.sessionId;
|
|
999
|
+
if (!task.sessionId && typeof metadata.sessionId === "string" && metadata.sessionId.trim()) {
|
|
1000
|
+
task.sessionId = metadata.sessionId.trim();
|
|
1001
|
+
changed = true;
|
|
1002
|
+
}
|
|
1003
|
+
if (!task.sessionFilePath && typeof metadata.sessionFilePath === "string" && metadata.sessionFilePath.trim()) {
|
|
1004
|
+
task.sessionFilePath = metadata.sessionFilePath.trim();
|
|
1005
|
+
changed = true;
|
|
1006
|
+
}
|
|
1007
|
+
if (!changed) {
|
|
1008
|
+
return;
|
|
1009
|
+
}
|
|
1010
|
+
task.updatedAt = formatLocalTimestamp();
|
|
1011
|
+
await persistRunTask(task).catch(() => undefined);
|
|
1012
|
+
if (!previousSessionId && task.sessionId) {
|
|
1013
|
+
await updateRunStartSlotSession({
|
|
1014
|
+
runId: task.id,
|
|
1015
|
+
previousSessionId,
|
|
1016
|
+
sessionId: task.sessionId,
|
|
1017
|
+
}).catch(() => undefined);
|
|
1018
|
+
}
|
|
450
1019
|
}
|
|
451
1020
|
function persistRetainedRun(task) {
|
|
452
1021
|
retainedRuns.set(task.id, cloneRunTask(task));
|
|
@@ -461,29 +1030,23 @@ function getStoredRun(runId) {
|
|
|
461
1030
|
async function startManagedRun(args) {
|
|
462
1031
|
const prepared = await prepareCommandExecution({
|
|
463
1032
|
cwd: args.cwd,
|
|
464
|
-
|
|
1033
|
+
userId: args.userId,
|
|
1034
|
+
taskId: args.runId,
|
|
465
1035
|
codexAuthBundle: args.codexAuthBundle,
|
|
466
1036
|
});
|
|
467
|
-
const child =
|
|
468
|
-
|
|
469
|
-
command: args.command,
|
|
470
|
-
patch: null,
|
|
471
|
-
shellPath: prepared.shellPath,
|
|
1037
|
+
const child = spawnManagedCodexCommand({
|
|
1038
|
+
codexArgs: args.codexArgs,
|
|
472
1039
|
taskWorkspace: prepared.taskWorkspace,
|
|
473
1040
|
env: prepared.env,
|
|
474
1041
|
agentToken: args.agentToken,
|
|
475
1042
|
});
|
|
476
|
-
const logsDir = await resolveRunLogsDir();
|
|
477
|
-
const logPath = path.join(logsDir, `${args.runId}.log`);
|
|
478
|
-
const logStream = createWriteStream(logPath, { flags: "a", encoding: "utf8" });
|
|
479
1043
|
const now = formatLocalTimestamp();
|
|
480
1044
|
const task = {
|
|
481
1045
|
id: args.runId,
|
|
482
1046
|
userId: args.userId,
|
|
483
1047
|
agentId: args.agentId,
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
chatId: args.chatId,
|
|
1048
|
+
sessionId: typeof args.sessionId === "string" && args.sessionId.trim() ? args.sessionId.trim() : null,
|
|
1049
|
+
sessionFilePath: null,
|
|
487
1050
|
status: "running",
|
|
488
1051
|
cancelRequested: false,
|
|
489
1052
|
resultExitCode: null,
|
|
@@ -493,22 +1056,7 @@ async function startManagedRun(args) {
|
|
|
493
1056
|
updatedAt: now,
|
|
494
1057
|
startedAt: now,
|
|
495
1058
|
finishedAt: null,
|
|
496
|
-
agentEventAckSeq: 0,
|
|
497
|
-
events: [],
|
|
498
1059
|
};
|
|
499
|
-
appendRunEvent(task, "meta", {
|
|
500
|
-
host: process.platform,
|
|
501
|
-
pid: child.pid ?? null,
|
|
502
|
-
startedAt: now,
|
|
503
|
-
command: args.command,
|
|
504
|
-
cwd: prepared.taskWorkspace,
|
|
505
|
-
requestedCwd: args.cwd,
|
|
506
|
-
shell: prepared.shellPath,
|
|
507
|
-
logPath,
|
|
508
|
-
...prepared.taskGitMeta,
|
|
509
|
-
...prepared.codexAuthMeta,
|
|
510
|
-
});
|
|
511
|
-
appendRunEvent(task, "status", { status: "running" });
|
|
512
1060
|
const cancellation = createManagedCancellation(child);
|
|
513
1061
|
const requestCancel = () => {
|
|
514
1062
|
if (task.status === "completed" || task.status === "failed" || task.status === "canceled") {
|
|
@@ -516,13 +1064,29 @@ async function startManagedRun(args) {
|
|
|
516
1064
|
}
|
|
517
1065
|
task.cancelRequested = true;
|
|
518
1066
|
task.updatedAt = formatLocalTimestamp();
|
|
1067
|
+
void persistRunTask(task).catch(() => undefined);
|
|
519
1068
|
writeRunStatus(task.id, "cancel requested");
|
|
520
1069
|
cancellation.requestCancel();
|
|
521
1070
|
};
|
|
1071
|
+
let stdoutBuffer = "";
|
|
522
1072
|
const recordChunk = (stream, chunk) => {
|
|
523
|
-
appendRunEvent(task, stream, { chunk, at: formatLocalTimestamp() });
|
|
524
|
-
logStream.write(JSON.stringify({ at: formatLocalTimestamp(), stream, chunk }) + "\n");
|
|
525
1073
|
writeRunStream(task.id, stream, chunk);
|
|
1074
|
+
if (stream !== "stdout" || (task.sessionId && task.sessionFilePath)) {
|
|
1075
|
+
return;
|
|
1076
|
+
}
|
|
1077
|
+
stdoutBuffer += chunk;
|
|
1078
|
+
const lines = stdoutBuffer.split(/\r?\n/);
|
|
1079
|
+
stdoutBuffer = lines.pop() ?? "";
|
|
1080
|
+
for (const line of lines) {
|
|
1081
|
+
const trimmed = line.trim();
|
|
1082
|
+
if (!trimmed) {
|
|
1083
|
+
continue;
|
|
1084
|
+
}
|
|
1085
|
+
const metadata = extractCodexSessionMetadata(trimmed);
|
|
1086
|
+
if (metadata.sessionId || metadata.sessionFilePath) {
|
|
1087
|
+
void updateRunSessionMetadata(task, metadata);
|
|
1088
|
+
}
|
|
1089
|
+
}
|
|
526
1090
|
};
|
|
527
1091
|
child.stdout.on("data", (chunk) => recordChunk("stdout", chunk));
|
|
528
1092
|
child.stderr.on("data", (chunk) => recordChunk("stderr", chunk));
|
|
@@ -531,488 +1095,2052 @@ async function startManagedRun(args) {
|
|
|
531
1095
|
task.status = "failed";
|
|
532
1096
|
task.error = message;
|
|
533
1097
|
task.finishedAt = formatLocalTimestamp();
|
|
534
|
-
appendRunEvent(task, "status", { status: "failed", error: message, finishedAt: task.finishedAt });
|
|
535
1098
|
persistRetainedRun(task);
|
|
536
1099
|
activeRuns.delete(task.id);
|
|
537
|
-
|
|
1100
|
+
void removeRunTask(task.id).catch(() => undefined);
|
|
1101
|
+
void releaseRunStartSlot({ runId: task.id, sessionId: task.sessionId }).catch(() => undefined);
|
|
538
1102
|
void prepared.codexAuthCleanup().catch(() => undefined);
|
|
539
1103
|
writeRunStatus(task.id, `failed error=${message}`);
|
|
540
1104
|
});
|
|
541
1105
|
child.once("close", (code, signal) => {
|
|
542
1106
|
cancellation.clear();
|
|
1107
|
+
if (stdoutBuffer.trim() && (!task.sessionId || !task.sessionFilePath)) {
|
|
1108
|
+
const metadata = extractCodexSessionMetadata(stdoutBuffer.trim());
|
|
1109
|
+
if (metadata.sessionId || metadata.sessionFilePath) {
|
|
1110
|
+
void updateRunSessionMetadata(task, metadata);
|
|
1111
|
+
}
|
|
1112
|
+
}
|
|
543
1113
|
task.resultExitCode = typeof code === "number" ? code : null;
|
|
544
1114
|
task.resultSignal = signal;
|
|
545
1115
|
task.finishedAt = formatLocalTimestamp();
|
|
546
1116
|
task.status = task.cancelRequested ? "canceled" : (task.resultExitCode ?? 1) === 0 ? "completed" : "failed";
|
|
547
1117
|
task.error = task.status === "failed" ? `Command exited with code ${task.resultExitCode ?? "null"}` : null;
|
|
548
|
-
appendRunEvent(task, "status", {
|
|
549
|
-
status: task.status,
|
|
550
|
-
exitCode: task.resultExitCode,
|
|
551
|
-
signal: task.resultSignal,
|
|
552
|
-
error: task.error,
|
|
553
|
-
finishedAt: task.finishedAt,
|
|
554
|
-
});
|
|
555
1118
|
persistRetainedRun(task);
|
|
556
1119
|
activeRuns.delete(task.id);
|
|
557
|
-
|
|
1120
|
+
void removeRunTask(task.id).catch(() => undefined);
|
|
1121
|
+
void releaseRunStartSlot({ runId: task.id, sessionId: task.sessionId }).catch(() => undefined);
|
|
558
1122
|
void prepared.codexAuthCleanup().catch(() => undefined);
|
|
559
|
-
if ((task.status === "completed" || task.status === "failed") && task.chatId) {
|
|
560
|
-
void notifyServerRunFinished({
|
|
561
|
-
serverBaseUrl: args.serverBaseUrl,
|
|
562
|
-
userId: args.userId,
|
|
563
|
-
agentToken: args.agentToken,
|
|
564
|
-
task,
|
|
565
|
-
}).catch((error) => {
|
|
566
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
567
|
-
writeAgentInfraError(`run completion notify failed runId=${task.id}: ${message}`);
|
|
568
|
-
});
|
|
569
|
-
}
|
|
570
1123
|
writeRunStatus(task.id, `completed status=${task.status} exitCode=${task.resultExitCode ?? "null"} signal=${task.resultSignal ?? "null"}`);
|
|
571
1124
|
});
|
|
572
|
-
activeRuns.set(task.id, { task, child,
|
|
1125
|
+
activeRuns.set(task.id, { task, child, requestCancel });
|
|
573
1126
|
persistRetainedRun(task);
|
|
1127
|
+
void persistRunTask(task).catch(() => undefined);
|
|
574
1128
|
writeRunStatus(task.id, `started requestId=${args.requestId} cwd=${prepared.taskWorkspace}`);
|
|
575
1129
|
return cloneRunTask(task);
|
|
576
1130
|
}
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
1131
|
+
function shellSingleQuote(value) {
|
|
1132
|
+
return `'${value.replace(/'/g, `'"'"'`)}'`;
|
|
1133
|
+
}
|
|
1134
|
+
function stripAnsi(value) {
|
|
1135
|
+
return value.replace(ANSI_RE, "");
|
|
1136
|
+
}
|
|
1137
|
+
function normalizeCodexModel(value) {
|
|
1138
|
+
const normalized = typeof value === "string" ? value.trim() : "";
|
|
1139
|
+
return normalized || "gpt-5.4";
|
|
1140
|
+
}
|
|
1141
|
+
function buildManagedCodexArgs(args) {
|
|
1142
|
+
const promptArgs = ["--", args.prompt];
|
|
1143
|
+
const fixedArgs = ["--dangerously-bypass-approvals-and-sandbox"];
|
|
1144
|
+
return [
|
|
1145
|
+
...fixedArgs,
|
|
1146
|
+
"--model",
|
|
1147
|
+
args.model,
|
|
1148
|
+
...(args.sessionId
|
|
1149
|
+
? ["exec", "resume", "--json", args.sessionId, ...promptArgs]
|
|
1150
|
+
: ["exec", "--json", ...promptArgs]),
|
|
1151
|
+
];
|
|
1152
|
+
}
|
|
1153
|
+
function buildLocalCodexCliCommand(args) {
|
|
1154
|
+
const quotedArgs = args.map(shellSingleQuote).join(" ");
|
|
1155
|
+
const direct = `exec codex ${quotedArgs}`;
|
|
1156
|
+
const fallback = `exec npm exec --yes --package doer-agent -- codex ${quotedArgs}`;
|
|
1157
|
+
const script = [
|
|
1158
|
+
"if command -v codex >/dev/null 2>&1; then",
|
|
1159
|
+
` ${direct}`,
|
|
1160
|
+
"fi",
|
|
1161
|
+
fallback,
|
|
1162
|
+
].join("\n");
|
|
1163
|
+
return `bash -lc ${shellSingleQuote(script)}`;
|
|
1164
|
+
}
|
|
1165
|
+
function hasDirectCodexBinary() {
|
|
1166
|
+
const result = spawnSync("bash", ["-lc", "command -v codex >/dev/null 2>&1"], {
|
|
1167
|
+
stdio: "ignore",
|
|
592
1168
|
});
|
|
1169
|
+
return result.status === 0;
|
|
593
1170
|
}
|
|
594
|
-
|
|
1171
|
+
function spawnManagedCodexCommand(args) {
|
|
1172
|
+
const env = {
|
|
1173
|
+
...args.env,
|
|
1174
|
+
DOER_AGENT_TOKEN: args.agentToken,
|
|
1175
|
+
};
|
|
1176
|
+
const child = hasDirectCodexBinary()
|
|
1177
|
+
? spawn("codex", args.codexArgs, {
|
|
1178
|
+
cwd: args.taskWorkspace,
|
|
1179
|
+
detached: process.platform !== "win32",
|
|
1180
|
+
env,
|
|
1181
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
1182
|
+
})
|
|
1183
|
+
: spawn("npm", ["exec", "--yes", "--package", "doer-agent", "--", "codex", ...args.codexArgs], {
|
|
1184
|
+
cwd: args.taskWorkspace,
|
|
1185
|
+
detached: process.platform !== "win32",
|
|
1186
|
+
env,
|
|
1187
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
1188
|
+
});
|
|
1189
|
+
child.stdout?.setEncoding("utf8");
|
|
1190
|
+
child.stderr?.setEncoding("utf8");
|
|
1191
|
+
return child;
|
|
1192
|
+
}
|
|
1193
|
+
async function runLocalCodexCli(args, timeoutMs) {
|
|
1194
|
+
const command = buildLocalCodexCliCommand(args);
|
|
1195
|
+
const workspaceRoot = workspaceRootOverride ?? (process.env.WORKSPACE?.trim() || process.cwd());
|
|
1196
|
+
const env = {
|
|
1197
|
+
...process.env,
|
|
1198
|
+
WORKSPACE: workspaceRoot,
|
|
1199
|
+
CODEX_HOME: resolveCodexHomePath(),
|
|
1200
|
+
};
|
|
1201
|
+
return await new Promise((resolve, reject) => {
|
|
1202
|
+
const child = spawn(command, {
|
|
1203
|
+
cwd: workspaceRoot,
|
|
1204
|
+
shell: resolveShellPath(),
|
|
1205
|
+
env,
|
|
1206
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
1207
|
+
});
|
|
1208
|
+
let stdout = "";
|
|
1209
|
+
let stderr = "";
|
|
1210
|
+
let done = false;
|
|
1211
|
+
let timedOut = false;
|
|
1212
|
+
child.stdout.setEncoding("utf8");
|
|
1213
|
+
child.stderr.setEncoding("utf8");
|
|
1214
|
+
child.stdout.on("data", (chunk) => {
|
|
1215
|
+
stdout += chunk;
|
|
1216
|
+
});
|
|
1217
|
+
child.stderr.on("data", (chunk) => {
|
|
1218
|
+
stderr += chunk;
|
|
1219
|
+
});
|
|
1220
|
+
const timer = setTimeout(() => {
|
|
1221
|
+
timedOut = true;
|
|
1222
|
+
sendSignalToTaskProcess(child, "SIGTERM");
|
|
1223
|
+
setTimeout(() => sendSignalToTaskProcess(child, "SIGKILL"), 1000);
|
|
1224
|
+
}, Math.max(500, timeoutMs));
|
|
1225
|
+
child.once("error", (error) => {
|
|
1226
|
+
if (done) {
|
|
1227
|
+
return;
|
|
1228
|
+
}
|
|
1229
|
+
done = true;
|
|
1230
|
+
clearTimeout(timer);
|
|
1231
|
+
reject(error);
|
|
1232
|
+
});
|
|
1233
|
+
child.once("exit", (code) => {
|
|
1234
|
+
if (done) {
|
|
1235
|
+
return;
|
|
1236
|
+
}
|
|
1237
|
+
done = true;
|
|
1238
|
+
clearTimeout(timer);
|
|
1239
|
+
resolve({ code, stdout, stderr, timedOut });
|
|
1240
|
+
});
|
|
1241
|
+
});
|
|
1242
|
+
}
|
|
1243
|
+
function parseCodexDeviceAuthOutput(raw) {
|
|
1244
|
+
const text = stripAnsi(raw);
|
|
1245
|
+
const urlMatch = text.match(/https?:\/\/[^\s]+/i);
|
|
1246
|
+
const codeMatch = text.match(/\b[A-Z0-9]{4,}(?:-[A-Z0-9]{4,})+\b/);
|
|
1247
|
+
return {
|
|
1248
|
+
verificationUri: urlMatch?.[0] ?? null,
|
|
1249
|
+
userCode: codeMatch?.[0] ?? null,
|
|
1250
|
+
};
|
|
1251
|
+
}
|
|
1252
|
+
function pendingCodexDeviceAuthMessage(state) {
|
|
1253
|
+
const parsed = parseCodexDeviceAuthOutput(state.output);
|
|
1254
|
+
if (parsed.verificationUri && parsed.userCode) {
|
|
1255
|
+
return `Waiting for approval. Enter code ${parsed.userCode} at ${parsed.verificationUri}`;
|
|
1256
|
+
}
|
|
1257
|
+
return stripAnsi(state.output).trim() || "Waiting for approval";
|
|
1258
|
+
}
|
|
1259
|
+
async function getLocalCodexLoginStatus() {
|
|
1260
|
+
const result = await runLocalCodexCli(["login", "status"], 5000);
|
|
1261
|
+
const merged = stripAnsi([result.stdout, result.stderr].filter(Boolean).join("\n")).trim();
|
|
1262
|
+
return {
|
|
1263
|
+
loggedIn: (result.code ?? 1) === 0,
|
|
1264
|
+
output: merged || ((result.code ?? 1) === 0 ? "Logged in" : "Not logged in"),
|
|
1265
|
+
};
|
|
1266
|
+
}
|
|
1267
|
+
async function waitForCodexDeviceCode(state, timeoutMs) {
|
|
1268
|
+
const startedAt = Date.now();
|
|
1269
|
+
while (Date.now() - startedAt < timeoutMs) {
|
|
1270
|
+
const parsed = parseCodexDeviceAuthOutput(state.output);
|
|
1271
|
+
if (parsed.verificationUri && parsed.userCode) {
|
|
1272
|
+
return;
|
|
1273
|
+
}
|
|
1274
|
+
if (state.child.exitCode !== null) {
|
|
1275
|
+
return;
|
|
1276
|
+
}
|
|
1277
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
1278
|
+
}
|
|
1279
|
+
}
|
|
1280
|
+
function normalizeSettingsRpcRequest(args) {
|
|
1281
|
+
const requestId = typeof args.request.requestId === "string" ? args.request.requestId.trim() : "";
|
|
1282
|
+
const responseSubject = typeof args.request.responseSubject === "string" ? args.request.responseSubject.trim() : "";
|
|
1283
|
+
const requestAgentId = typeof args.request.agentId === "string" ? args.request.agentId.trim() : "";
|
|
1284
|
+
const action = args.request.action === "update" ? "update" : "get";
|
|
1285
|
+
if (!requestId || !responseSubject || !requestAgentId || requestAgentId !== args.agentId) {
|
|
1286
|
+
throw new Error("invalid settings rpc request");
|
|
1287
|
+
}
|
|
1288
|
+
return {
|
|
1289
|
+
requestId,
|
|
1290
|
+
responseSubject,
|
|
1291
|
+
action,
|
|
1292
|
+
patch: normalizeAgentSettingsPatch(args.request.patch),
|
|
1293
|
+
defaults: args.request.defaults && typeof args.request.defaults === "object" && !Array.isArray(args.request.defaults)
|
|
1294
|
+
? normalizeAgentSettingsConfig(args.request.defaults)
|
|
1295
|
+
: null,
|
|
1296
|
+
};
|
|
1297
|
+
}
|
|
1298
|
+
function publishSettingsRpcResponse(args) {
|
|
1299
|
+
args.nc.publish(args.responseSubject, settingsRpcCodec.encode(JSON.stringify(args.payload)));
|
|
1300
|
+
}
|
|
1301
|
+
async function handleSettingsRpcMessage(args) {
|
|
595
1302
|
let requestId = "unknown";
|
|
596
1303
|
let responseSubject = "";
|
|
597
1304
|
try {
|
|
598
|
-
const payload = JSON.parse(
|
|
599
|
-
const request =
|
|
1305
|
+
const payload = JSON.parse(settingsRpcCodec.decode(args.msg.data));
|
|
1306
|
+
const request = normalizeSettingsRpcRequest({ request: payload, agentId: args.agentId });
|
|
600
1307
|
requestId = request.requestId;
|
|
601
1308
|
responseSubject = request.responseSubject;
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
serverBaseUrl: args.serverBaseUrl,
|
|
607
|
-
userId: args.userId,
|
|
608
|
-
agentId: args.agentId,
|
|
609
|
-
command: request.command ?? "",
|
|
610
|
-
cwd: request.cwd,
|
|
611
|
-
chatId: request.chatId,
|
|
612
|
-
runtimeEnvPatch: request.runtimeEnvPatch,
|
|
613
|
-
codexAuthBundle: request.codexAuthBundle,
|
|
614
|
-
agentToken: args.agentToken,
|
|
615
|
-
});
|
|
616
|
-
publishRunRpcResponse({ nc: args.jetstream.nc, responseSubject, payload: { requestId, ok: true, task } });
|
|
617
|
-
return;
|
|
618
|
-
}
|
|
619
|
-
if (request.action === "list") {
|
|
620
|
-
const tasks = [...activeRuns.values()].map((entry) => cloneRunTask(entry.task));
|
|
621
|
-
const retained = [...retainedRuns.values()].filter((task) => !activeRuns.has(task.id)).map((task) => cloneRunTask(task));
|
|
622
|
-
const merged = [...tasks, ...retained]
|
|
623
|
-
.sort((a, b) => Date.parse(b.updatedAt) - Date.parse(a.updatedAt))
|
|
624
|
-
.slice(0, request.limit);
|
|
625
|
-
publishRunRpcResponse({ nc: args.jetstream.nc, responseSubject, payload: { requestId, ok: true, tasks: merged } });
|
|
626
|
-
return;
|
|
627
|
-
}
|
|
628
|
-
const stored = request.runId ? getStoredRun(request.runId) : null;
|
|
629
|
-
if (!stored || stored.agentId !== args.agentId || stored.userId !== args.userId) {
|
|
630
|
-
throw new Error("Run not found");
|
|
1309
|
+
const existing = await readAgentSettingsConfig(request.defaults);
|
|
1310
|
+
const next = request.action === "update" ? normalizeAgentSettingsConfig(request.patch, existing) : existing;
|
|
1311
|
+
if (request.action === "update") {
|
|
1312
|
+
await writeAgentSettingsConfig(next);
|
|
631
1313
|
}
|
|
632
|
-
if (request.
|
|
633
|
-
const
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
1314
|
+
else if (request.defaults) {
|
|
1315
|
+
const filePath = resolveAgentSettingsFilePath();
|
|
1316
|
+
const raw = await readFile(filePath, "utf8").catch(() => "");
|
|
1317
|
+
if (!raw.trim()) {
|
|
1318
|
+
await writeAgentSettingsConfig(next);
|
|
1319
|
+
}
|
|
638
1320
|
}
|
|
639
|
-
|
|
640
|
-
|
|
1321
|
+
publishSettingsRpcResponse({
|
|
1322
|
+
nc: args.jetstream.nc,
|
|
1323
|
+
responseSubject,
|
|
1324
|
+
payload: {
|
|
1325
|
+
requestId,
|
|
1326
|
+
ok: true,
|
|
1327
|
+
settings: toAgentSettingsPublic(next),
|
|
1328
|
+
},
|
|
1329
|
+
});
|
|
641
1330
|
}
|
|
642
1331
|
catch (error) {
|
|
643
1332
|
const message = error instanceof Error ? error.message : String(error);
|
|
644
1333
|
if (responseSubject) {
|
|
645
|
-
|
|
1334
|
+
publishSettingsRpcResponse({
|
|
646
1335
|
nc: args.jetstream.nc,
|
|
647
1336
|
responseSubject,
|
|
648
1337
|
payload: { requestId, ok: false, error: message },
|
|
649
1338
|
});
|
|
650
1339
|
}
|
|
651
|
-
writeAgentError(`
|
|
1340
|
+
writeAgentError(`settings rpc failed requestId=${requestId} error=${message}`);
|
|
652
1341
|
}
|
|
653
1342
|
}
|
|
654
|
-
function
|
|
655
|
-
const subject =
|
|
1343
|
+
function subscribeToSettingsRpc(args) {
|
|
1344
|
+
const subject = buildAgentSettingsRpcSubject(args.userId, args.agentId);
|
|
656
1345
|
args.jetstream.nc.subscribe(subject, {
|
|
657
1346
|
callback: (error, msg) => {
|
|
658
1347
|
if (error) {
|
|
659
1348
|
const message = error instanceof Error ? error.message : String(error);
|
|
660
|
-
writeAgentError(`
|
|
1349
|
+
writeAgentError(`settings rpc subscription error: ${message}`);
|
|
661
1350
|
return;
|
|
662
1351
|
}
|
|
663
|
-
void
|
|
1352
|
+
void handleSettingsRpcMessage({
|
|
664
1353
|
msg,
|
|
665
1354
|
jetstream: args.jetstream,
|
|
666
|
-
serverBaseUrl: args.serverBaseUrl,
|
|
667
|
-
userId: args.userId,
|
|
668
1355
|
agentId: args.agentId,
|
|
669
|
-
|
|
1356
|
+
});
|
|
1357
|
+
},
|
|
1358
|
+
});
|
|
1359
|
+
writeAgentInfo(`settings rpc subscribed subject=${subject}`);
|
|
1360
|
+
}
|
|
1361
|
+
async function startLocalCodexDeviceAuth() {
|
|
1362
|
+
if (pendingCodexDeviceAuth && pendingCodexDeviceAuth.child.exitCode === null) {
|
|
1363
|
+
const parsed = parseCodexDeviceAuthOutput(pendingCodexDeviceAuth.output);
|
|
1364
|
+
return {
|
|
1365
|
+
loggedIn: false,
|
|
1366
|
+
output: pendingCodexDeviceAuthMessage(pendingCodexDeviceAuth),
|
|
1367
|
+
verificationUri: parsed.verificationUri,
|
|
1368
|
+
userCode: parsed.userCode,
|
|
1369
|
+
};
|
|
1370
|
+
}
|
|
1371
|
+
const workspaceRoot = workspaceRootOverride ?? (process.env.WORKSPACE?.trim() || process.cwd());
|
|
1372
|
+
const child = spawn(buildLocalCodexCliCommand(["login", "--device-auth"]), {
|
|
1373
|
+
cwd: workspaceRoot,
|
|
1374
|
+
shell: resolveShellPath(),
|
|
1375
|
+
detached: process.platform !== "win32",
|
|
1376
|
+
env: {
|
|
1377
|
+
...process.env,
|
|
1378
|
+
WORKSPACE: workspaceRoot,
|
|
1379
|
+
CODEX_HOME: resolveCodexHomePath(),
|
|
1380
|
+
},
|
|
1381
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
1382
|
+
});
|
|
1383
|
+
child.stdout.setEncoding("utf8");
|
|
1384
|
+
child.stderr.setEncoding("utf8");
|
|
1385
|
+
const state = { child, output: "" };
|
|
1386
|
+
pendingCodexDeviceAuth = state;
|
|
1387
|
+
const appendOutput = (chunk) => {
|
|
1388
|
+
state.output += chunk;
|
|
1389
|
+
};
|
|
1390
|
+
child.stdout.on("data", appendOutput);
|
|
1391
|
+
child.stderr.on("data", appendOutput);
|
|
1392
|
+
child.once("exit", () => {
|
|
1393
|
+
if (pendingCodexDeviceAuth === state) {
|
|
1394
|
+
pendingCodexDeviceAuth = null;
|
|
1395
|
+
}
|
|
1396
|
+
});
|
|
1397
|
+
await waitForCodexDeviceCode(state, 8000);
|
|
1398
|
+
const parsed = parseCodexDeviceAuthOutput(state.output);
|
|
1399
|
+
if ((!parsed.verificationUri || !parsed.userCode) && state.child.exitCode !== null) {
|
|
1400
|
+
throw new Error("Failed to read device code from Codex CLI");
|
|
1401
|
+
}
|
|
1402
|
+
return {
|
|
1403
|
+
loggedIn: false,
|
|
1404
|
+
output: pendingCodexDeviceAuthMessage(state),
|
|
1405
|
+
verificationUri: parsed.verificationUri,
|
|
1406
|
+
userCode: parsed.userCode,
|
|
1407
|
+
};
|
|
1408
|
+
}
|
|
1409
|
+
async function startLocalCodexLogin() {
|
|
1410
|
+
const workspaceRoot = workspaceRootOverride ?? (process.env.WORKSPACE?.trim() || process.cwd());
|
|
1411
|
+
const child = spawn(buildLocalCodexCliCommand(["login"]), {
|
|
1412
|
+
cwd: workspaceRoot,
|
|
1413
|
+
shell: resolveShellPath(),
|
|
1414
|
+
detached: process.platform !== "win32",
|
|
1415
|
+
env: {
|
|
1416
|
+
...process.env,
|
|
1417
|
+
WORKSPACE: workspaceRoot,
|
|
1418
|
+
CODEX_HOME: resolveCodexHomePath(),
|
|
1419
|
+
},
|
|
1420
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
1421
|
+
});
|
|
1422
|
+
let output = "";
|
|
1423
|
+
child.stdout.setEncoding("utf8");
|
|
1424
|
+
child.stderr.setEncoding("utf8");
|
|
1425
|
+
child.stdout.on("data", (chunk) => {
|
|
1426
|
+
output += chunk;
|
|
1427
|
+
});
|
|
1428
|
+
child.stderr.on("data", (chunk) => {
|
|
1429
|
+
output += chunk;
|
|
1430
|
+
});
|
|
1431
|
+
const result = await new Promise((resolve, reject) => {
|
|
1432
|
+
child.once("error", reject);
|
|
1433
|
+
child.once("exit", (code) => resolve({ code, output }));
|
|
1434
|
+
});
|
|
1435
|
+
const normalized = stripAnsi(result.output).trim();
|
|
1436
|
+
const parsed = parseCodexDeviceAuthOutput(result.output);
|
|
1437
|
+
if ((result.code ?? 1) === 0) {
|
|
1438
|
+
const status = await getLocalCodexLoginStatus().catch(() => null);
|
|
1439
|
+
return {
|
|
1440
|
+
loggedIn: status?.loggedIn === true,
|
|
1441
|
+
output: status?.output || normalized || "Login started",
|
|
1442
|
+
verificationUri: parsed.verificationUri,
|
|
1443
|
+
userCode: parsed.userCode,
|
|
1444
|
+
};
|
|
1445
|
+
}
|
|
1446
|
+
throw new Error(normalized || `Codex login failed with code ${result.code ?? "null"}`);
|
|
1447
|
+
}
|
|
1448
|
+
async function logoutLocalCodexAuth() {
|
|
1449
|
+
if (pendingCodexDeviceAuth && pendingCodexDeviceAuth.child.exitCode === null) {
|
|
1450
|
+
sendSignalToTaskProcess(pendingCodexDeviceAuth.child, "SIGTERM");
|
|
1451
|
+
setTimeout(() => {
|
|
1452
|
+
if (pendingCodexDeviceAuth?.child.exitCode === null) {
|
|
1453
|
+
sendSignalToTaskProcess(pendingCodexDeviceAuth.child, "SIGKILL");
|
|
1454
|
+
}
|
|
1455
|
+
}, 1000);
|
|
1456
|
+
pendingCodexDeviceAuth = null;
|
|
1457
|
+
}
|
|
1458
|
+
const result = await runLocalCodexCli(["logout"], 5000);
|
|
1459
|
+
let merged = stripAnsi([result.stdout, result.stderr].filter(Boolean).join("\n")).trim();
|
|
1460
|
+
const statusAfterLogout = await getLocalCodexLoginStatus().catch(() => null);
|
|
1461
|
+
if (statusAfterLogout?.loggedIn) {
|
|
1462
|
+
const authFile = path.join(resolveCodexHomePath(), "auth.json");
|
|
1463
|
+
await unlink(authFile).catch(() => undefined);
|
|
1464
|
+
const statusAfterDelete = await getLocalCodexLoginStatus().catch(() => null);
|
|
1465
|
+
if (statusAfterDelete?.output) {
|
|
1466
|
+
merged = [merged, statusAfterDelete.output].filter(Boolean).join("\n");
|
|
1467
|
+
}
|
|
1468
|
+
}
|
|
1469
|
+
return {
|
|
1470
|
+
loggedIn: false,
|
|
1471
|
+
output: merged || "Logged out",
|
|
1472
|
+
};
|
|
1473
|
+
}
|
|
1474
|
+
function normalizeCodexAuthRpcRequest(args) {
|
|
1475
|
+
const requestId = typeof args.request.requestId === "string" ? args.request.requestId.trim() : "";
|
|
1476
|
+
const responseSubject = typeof args.request.responseSubject === "string" ? args.request.responseSubject.trim() : "";
|
|
1477
|
+
const requestAgentId = typeof args.request.agentId === "string" ? args.request.agentId.trim() : "";
|
|
1478
|
+
const actionRaw = typeof args.request.action === "string" ? args.request.action.trim() : "";
|
|
1479
|
+
const action = actionRaw === "start" || actionRaw === "logout" ? actionRaw : "status";
|
|
1480
|
+
if (!requestId || !responseSubject || !requestAgentId || requestAgentId !== args.agentId) {
|
|
1481
|
+
throw new Error("invalid codex auth rpc request");
|
|
1482
|
+
}
|
|
1483
|
+
return { requestId, responseSubject, action };
|
|
1484
|
+
}
|
|
1485
|
+
function publishCodexAuthRpcResponse(args) {
|
|
1486
|
+
args.nc.publish(args.responseSubject, codexAuthRpcCodec.encode(JSON.stringify(args.payload)));
|
|
1487
|
+
}
|
|
1488
|
+
async function handleCodexAuthRpcMessage(args) {
|
|
1489
|
+
let requestId = "unknown";
|
|
1490
|
+
let responseSubject = "";
|
|
1491
|
+
try {
|
|
1492
|
+
const payload = JSON.parse(codexAuthRpcCodec.decode(args.msg.data));
|
|
1493
|
+
const request = normalizeCodexAuthRpcRequest({ request: payload, agentId: args.agentId });
|
|
1494
|
+
requestId = request.requestId;
|
|
1495
|
+
responseSubject = request.responseSubject;
|
|
1496
|
+
let result = null;
|
|
1497
|
+
if (request.action === "start") {
|
|
1498
|
+
const status = await getLocalCodexLoginStatus();
|
|
1499
|
+
if (status.loggedIn) {
|
|
1500
|
+
result = { loggedIn: true, output: status.output };
|
|
1501
|
+
}
|
|
1502
|
+
else {
|
|
1503
|
+
try {
|
|
1504
|
+
result = await startLocalCodexDeviceAuth();
|
|
1505
|
+
}
|
|
1506
|
+
catch (error) {
|
|
1507
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1508
|
+
const normalized = message.toLowerCase();
|
|
1509
|
+
if (normalized.includes("operation not permitted") ||
|
|
1510
|
+
normalized.includes("failed to read device code") ||
|
|
1511
|
+
normalized.includes("panic") ||
|
|
1512
|
+
normalized.includes("null object")) {
|
|
1513
|
+
result = await startLocalCodexLogin();
|
|
1514
|
+
}
|
|
1515
|
+
else {
|
|
1516
|
+
throw error;
|
|
1517
|
+
}
|
|
1518
|
+
}
|
|
1519
|
+
}
|
|
1520
|
+
}
|
|
1521
|
+
else if (request.action === "logout") {
|
|
1522
|
+
result = await logoutLocalCodexAuth();
|
|
1523
|
+
}
|
|
1524
|
+
else {
|
|
1525
|
+
const status = await getLocalCodexLoginStatus();
|
|
1526
|
+
if (status.loggedIn) {
|
|
1527
|
+
result = { loggedIn: true, output: status.output };
|
|
1528
|
+
}
|
|
1529
|
+
else if (pendingCodexDeviceAuth && pendingCodexDeviceAuth.child.exitCode === null) {
|
|
1530
|
+
const parsed = parseCodexDeviceAuthOutput(pendingCodexDeviceAuth.output);
|
|
1531
|
+
result = {
|
|
1532
|
+
loggedIn: false,
|
|
1533
|
+
output: pendingCodexDeviceAuthMessage(pendingCodexDeviceAuth),
|
|
1534
|
+
verificationUri: parsed.verificationUri,
|
|
1535
|
+
userCode: parsed.userCode,
|
|
1536
|
+
};
|
|
1537
|
+
}
|
|
1538
|
+
else {
|
|
1539
|
+
result = { loggedIn: false, output: status.output || "Not logged in" };
|
|
1540
|
+
}
|
|
1541
|
+
}
|
|
1542
|
+
publishCodexAuthRpcResponse({
|
|
1543
|
+
nc: args.jetstream.nc,
|
|
1544
|
+
responseSubject,
|
|
1545
|
+
payload: {
|
|
1546
|
+
requestId,
|
|
1547
|
+
ok: true,
|
|
1548
|
+
loggedIn: result.loggedIn,
|
|
1549
|
+
output: result.output,
|
|
1550
|
+
verificationUri: result.verificationUri ?? null,
|
|
1551
|
+
userCode: result.userCode ?? null,
|
|
1552
|
+
},
|
|
1553
|
+
});
|
|
1554
|
+
}
|
|
1555
|
+
catch (error) {
|
|
1556
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1557
|
+
if (responseSubject) {
|
|
1558
|
+
publishCodexAuthRpcResponse({
|
|
1559
|
+
nc: args.jetstream.nc,
|
|
1560
|
+
responseSubject,
|
|
1561
|
+
payload: { requestId, ok: false, error: message },
|
|
1562
|
+
});
|
|
1563
|
+
}
|
|
1564
|
+
writeAgentError(`codex auth rpc failed requestId=${requestId} error=${message}`);
|
|
1565
|
+
}
|
|
1566
|
+
}
|
|
1567
|
+
function subscribeToCodexAuthRpc(args) {
|
|
1568
|
+
const subject = buildAgentCodexAuthRpcSubject(args.userId, args.agentId);
|
|
1569
|
+
args.jetstream.nc.subscribe(subject, {
|
|
1570
|
+
callback: (error, msg) => {
|
|
1571
|
+
if (error) {
|
|
1572
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1573
|
+
writeAgentError(`codex auth rpc subscription error: ${message}`);
|
|
1574
|
+
return;
|
|
1575
|
+
}
|
|
1576
|
+
void handleCodexAuthRpcMessage({
|
|
1577
|
+
msg,
|
|
1578
|
+
jetstream: args.jetstream,
|
|
1579
|
+
agentId: args.agentId,
|
|
1580
|
+
});
|
|
1581
|
+
},
|
|
1582
|
+
});
|
|
1583
|
+
writeAgentInfo(`codex auth rpc subscribed subject=${subject}`);
|
|
1584
|
+
}
|
|
1585
|
+
function runLocalCommand(command, args, cwd) {
|
|
1586
|
+
return new Promise((resolve, reject) => {
|
|
1587
|
+
const child = spawn(command, args, {
|
|
1588
|
+
cwd,
|
|
1589
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
1590
|
+
});
|
|
1591
|
+
let stdout = "";
|
|
1592
|
+
let stderr = "";
|
|
1593
|
+
child.stdout.setEncoding("utf8");
|
|
1594
|
+
child.stderr.setEncoding("utf8");
|
|
1595
|
+
child.stdout.on("data", (chunk) => {
|
|
1596
|
+
stdout += chunk;
|
|
1597
|
+
});
|
|
1598
|
+
child.stderr.on("data", (chunk) => {
|
|
1599
|
+
stderr += chunk;
|
|
1600
|
+
});
|
|
1601
|
+
child.once("error", reject);
|
|
1602
|
+
child.once("close", (code) => {
|
|
1603
|
+
resolve({ code: code ?? 1, stdout, stderr });
|
|
1604
|
+
});
|
|
1605
|
+
});
|
|
1606
|
+
}
|
|
1607
|
+
function sanitizeGitRef(value) {
|
|
1608
|
+
if (typeof value !== "string" || !value.trim()) {
|
|
1609
|
+
return null;
|
|
1610
|
+
}
|
|
1611
|
+
const trimmed = value.trim();
|
|
1612
|
+
if (trimmed.startsWith("-") || /\s/.test(trimmed) || trimmed.includes("..") || trimmed.includes(":")) {
|
|
1613
|
+
throw new Error(`Invalid git ref: ${trimmed}`);
|
|
1614
|
+
}
|
|
1615
|
+
return trimmed;
|
|
1616
|
+
}
|
|
1617
|
+
function sanitizeGitPathspec(value) {
|
|
1618
|
+
if (typeof value !== "string") {
|
|
1619
|
+
throw new Error("Invalid pathspec");
|
|
1620
|
+
}
|
|
1621
|
+
const trimmed = value.trim().replace(/\\/g, "/");
|
|
1622
|
+
if (!trimmed || trimmed.startsWith("-") || trimmed.includes("\0")) {
|
|
1623
|
+
throw new Error(`Invalid pathspec: ${trimmed}`);
|
|
1624
|
+
}
|
|
1625
|
+
return trimmed;
|
|
1626
|
+
}
|
|
1627
|
+
function normalizeGitRpcRequest(args) {
|
|
1628
|
+
const requestId = typeof args.request.requestId === "string" ? args.request.requestId.trim() : "";
|
|
1629
|
+
const responseSubject = typeof args.request.responseSubject === "string" ? args.request.responseSubject.trim() : "";
|
|
1630
|
+
const requestAgentId = typeof args.request.agentId === "string" ? args.request.agentId.trim() : "";
|
|
1631
|
+
const targetPath = typeof args.request.targetPath === "string" ? args.request.targetPath.trim() : "";
|
|
1632
|
+
if (!requestId || !responseSubject || !requestAgentId || requestAgentId !== args.agentId || !targetPath) {
|
|
1633
|
+
throw new Error("invalid git rpc request");
|
|
1634
|
+
}
|
|
1635
|
+
const format = args.request.format === "name-only" ||
|
|
1636
|
+
args.request.format === "name-status" ||
|
|
1637
|
+
args.request.format === "stat" ||
|
|
1638
|
+
args.request.format === "numstat" ||
|
|
1639
|
+
args.request.format === "raw"
|
|
1640
|
+
? args.request.format
|
|
1641
|
+
: "patch";
|
|
1642
|
+
const ignoreWhitespace = args.request.ignoreWhitespace === "at-eol" ||
|
|
1643
|
+
args.request.ignoreWhitespace === "change" ||
|
|
1644
|
+
args.request.ignoreWhitespace === "all"
|
|
1645
|
+
? args.request.ignoreWhitespace
|
|
1646
|
+
: "none";
|
|
1647
|
+
const diffAlgorithm = args.request.diffAlgorithm === "minimal" ||
|
|
1648
|
+
args.request.diffAlgorithm === "patience" ||
|
|
1649
|
+
args.request.diffAlgorithm === "histogram"
|
|
1650
|
+
? args.request.diffAlgorithm
|
|
1651
|
+
: "default";
|
|
1652
|
+
const contextRaw = Number(args.request.contextLines);
|
|
1653
|
+
const contextLines = Number.isFinite(contextRaw) ? Math.max(0, Math.min(200, Math.trunc(contextRaw))) : null;
|
|
1654
|
+
const pathspecs = Array.isArray(args.request.pathspecs) ? args.request.pathspecs.map((item) => sanitizeGitPathspec(item)) : [];
|
|
1655
|
+
return {
|
|
1656
|
+
requestId,
|
|
1657
|
+
responseSubject,
|
|
1658
|
+
targetPath,
|
|
1659
|
+
base: sanitizeGitRef(args.request.base),
|
|
1660
|
+
target: sanitizeGitRef(args.request.target),
|
|
1661
|
+
mergeBase: args.request.mergeBase === true,
|
|
1662
|
+
staged: args.request.staged === true,
|
|
1663
|
+
format,
|
|
1664
|
+
contextLines,
|
|
1665
|
+
ignoreWhitespace,
|
|
1666
|
+
diffAlgorithm,
|
|
1667
|
+
findRenames: args.request.findRenames === true,
|
|
1668
|
+
pathspecs,
|
|
1669
|
+
};
|
|
1670
|
+
}
|
|
1671
|
+
function buildAgentGitDiffArgs(repoRootAbs, request) {
|
|
1672
|
+
const args = ["-C", repoRootAbs, "diff", "--no-color"];
|
|
1673
|
+
const displayParts = ["git", "diff", "--no-color"];
|
|
1674
|
+
if (request.staged) {
|
|
1675
|
+
args.push("--cached");
|
|
1676
|
+
displayParts.push("--cached");
|
|
1677
|
+
}
|
|
1678
|
+
if (typeof request.contextLines === "number") {
|
|
1679
|
+
args.push(`-U${request.contextLines}`);
|
|
1680
|
+
displayParts.push(`-U${request.contextLines}`);
|
|
1681
|
+
}
|
|
1682
|
+
if (request.ignoreWhitespace === "at-eol") {
|
|
1683
|
+
args.push("--ignore-space-at-eol");
|
|
1684
|
+
displayParts.push("--ignore-space-at-eol");
|
|
1685
|
+
}
|
|
1686
|
+
else if (request.ignoreWhitespace === "change") {
|
|
1687
|
+
args.push("--ignore-space-change");
|
|
1688
|
+
displayParts.push("--ignore-space-change");
|
|
1689
|
+
}
|
|
1690
|
+
else if (request.ignoreWhitespace === "all") {
|
|
1691
|
+
args.push("--ignore-all-space");
|
|
1692
|
+
displayParts.push("--ignore-all-space");
|
|
1693
|
+
}
|
|
1694
|
+
if (request.diffAlgorithm !== "default") {
|
|
1695
|
+
args.push(`--diff-algorithm=${request.diffAlgorithm}`);
|
|
1696
|
+
displayParts.push(`--diff-algorithm=${request.diffAlgorithm}`);
|
|
1697
|
+
}
|
|
1698
|
+
if (request.findRenames) {
|
|
1699
|
+
args.push("--find-renames");
|
|
1700
|
+
displayParts.push("--find-renames");
|
|
1701
|
+
}
|
|
1702
|
+
if (request.format === "name-only") {
|
|
1703
|
+
args.push("--name-only");
|
|
1704
|
+
displayParts.push("--name-only");
|
|
1705
|
+
}
|
|
1706
|
+
else if (request.format === "name-status") {
|
|
1707
|
+
args.push("--name-status");
|
|
1708
|
+
displayParts.push("--name-status");
|
|
1709
|
+
}
|
|
1710
|
+
else if (request.format === "stat") {
|
|
1711
|
+
args.push("--stat");
|
|
1712
|
+
displayParts.push("--stat");
|
|
1713
|
+
}
|
|
1714
|
+
else if (request.format === "numstat") {
|
|
1715
|
+
args.push("--numstat");
|
|
1716
|
+
displayParts.push("--numstat");
|
|
1717
|
+
}
|
|
1718
|
+
else if (request.format === "raw") {
|
|
1719
|
+
args.push("--raw");
|
|
1720
|
+
displayParts.push("--raw");
|
|
1721
|
+
}
|
|
1722
|
+
if (request.mergeBase) {
|
|
1723
|
+
if (!request.base || !request.target) {
|
|
1724
|
+
throw new Error("mergeBase mode requires both base and target");
|
|
1725
|
+
}
|
|
1726
|
+
const merged = `${request.base}...${request.target}`;
|
|
1727
|
+
args.push(merged);
|
|
1728
|
+
displayParts.push(merged);
|
|
1729
|
+
}
|
|
1730
|
+
else {
|
|
1731
|
+
if (request.base) {
|
|
1732
|
+
args.push(request.base);
|
|
1733
|
+
displayParts.push(request.base);
|
|
1734
|
+
}
|
|
1735
|
+
if (request.target) {
|
|
1736
|
+
args.push(request.target);
|
|
1737
|
+
displayParts.push(request.target);
|
|
1738
|
+
}
|
|
1739
|
+
}
|
|
1740
|
+
if (request.pathspecs.length > 0) {
|
|
1741
|
+
args.push("--", ...request.pathspecs);
|
|
1742
|
+
displayParts.push("--", ...request.pathspecs);
|
|
1743
|
+
}
|
|
1744
|
+
return { args, display: displayParts.join(" ") };
|
|
1745
|
+
}
|
|
1746
|
+
function buildUntrackedText(format, untrackedPaths) {
|
|
1747
|
+
if (untrackedPaths.length === 0) {
|
|
1748
|
+
return "";
|
|
1749
|
+
}
|
|
1750
|
+
if (format === "name-status" || format === "raw") {
|
|
1751
|
+
return `${untrackedPaths.map((item) => `??\t${item}`).join("\n")}\n`;
|
|
1752
|
+
}
|
|
1753
|
+
if (format === "name-only") {
|
|
1754
|
+
return `${untrackedPaths.join("\n")}\n`;
|
|
1755
|
+
}
|
|
1756
|
+
return `\n# Untracked files\n${untrackedPaths.join("\n")}\n`;
|
|
1757
|
+
}
|
|
1758
|
+
async function appendAgentLocalUntrackedDiff(repoRootAbs, request, baseOutput) {
|
|
1759
|
+
const listArgs = ["-C", repoRootAbs, "ls-files", "--others", "--exclude-standard"];
|
|
1760
|
+
if (request.pathspecs.length > 0) {
|
|
1761
|
+
listArgs.push("--", ...request.pathspecs);
|
|
1762
|
+
}
|
|
1763
|
+
const listResult = await runLocalCommand("git", listArgs, repoRootAbs);
|
|
1764
|
+
if (listResult.code !== 0) {
|
|
1765
|
+
return { output: baseOutput, hasUntracked: false };
|
|
1766
|
+
}
|
|
1767
|
+
const untrackedPaths = listResult.stdout.split(/\r?\n/).map((item) => item.trim()).filter(Boolean);
|
|
1768
|
+
if (untrackedPaths.length === 0) {
|
|
1769
|
+
return { output: baseOutput, hasUntracked: false };
|
|
1770
|
+
}
|
|
1771
|
+
if (request.format !== "patch") {
|
|
1772
|
+
return { output: `${baseOutput}${buildUntrackedText(request.format, untrackedPaths)}`, hasUntracked: true };
|
|
1773
|
+
}
|
|
1774
|
+
let output = baseOutput;
|
|
1775
|
+
for (const relPath of untrackedPaths) {
|
|
1776
|
+
const diffResult = await runLocalCommand("git", ["-C", repoRootAbs, "diff", "--no-color", "--no-index", "--", "/dev/null", relPath], repoRootAbs);
|
|
1777
|
+
if (diffResult.code !== 0 && diffResult.code !== 1) {
|
|
1778
|
+
throw new Error(diffResult.stderr.trim() || `Failed to render agent untracked diff: ${relPath}`);
|
|
1779
|
+
}
|
|
1780
|
+
if (diffResult.stdout) {
|
|
1781
|
+
output += diffResult.stdout;
|
|
1782
|
+
if (!output.endsWith("\n")) {
|
|
1783
|
+
output += "\n";
|
|
1784
|
+
}
|
|
1785
|
+
}
|
|
1786
|
+
}
|
|
1787
|
+
return { output, hasUntracked: true };
|
|
1788
|
+
}
|
|
1789
|
+
function publishGitRpcResponse(args) {
|
|
1790
|
+
args.nc.publish(args.responseSubject, gitRpcCodec.encode(JSON.stringify(args.payload)));
|
|
1791
|
+
}
|
|
1792
|
+
async function handleGitRpcMessage(args) {
|
|
1793
|
+
let requestId = "unknown";
|
|
1794
|
+
let responseSubject = "";
|
|
1795
|
+
try {
|
|
1796
|
+
const payload = JSON.parse(gitRpcCodec.decode(args.msg.data));
|
|
1797
|
+
const request = normalizeGitRpcRequest({ request: payload, agentId: args.agentId });
|
|
1798
|
+
requestId = request.requestId;
|
|
1799
|
+
responseSubject = request.responseSubject;
|
|
1800
|
+
if (!request.targetPath.startsWith("/")) {
|
|
1801
|
+
throw new Error("agent source requires an absolute directory path");
|
|
1802
|
+
}
|
|
1803
|
+
const topLevelResult = await runLocalCommand("git", ["-C", request.targetPath, "rev-parse", "--show-toplevel"], request.targetPath);
|
|
1804
|
+
if (topLevelResult.code !== 0) {
|
|
1805
|
+
publishGitRpcResponse({
|
|
1806
|
+
nc: args.jetstream.nc,
|
|
1807
|
+
responseSubject,
|
|
1808
|
+
payload: {
|
|
1809
|
+
requestId,
|
|
1810
|
+
ok: true,
|
|
1811
|
+
payload: {
|
|
1812
|
+
isGitRepo: false,
|
|
1813
|
+
mode: "git_diff",
|
|
1814
|
+
source: "agent",
|
|
1815
|
+
agent: { id: args.agentId, name: null },
|
|
1816
|
+
currentPath: request.targetPath,
|
|
1817
|
+
repoRoot: null,
|
|
1818
|
+
repoRelativePath: null,
|
|
1819
|
+
branch: null,
|
|
1820
|
+
gitDiff: {
|
|
1821
|
+
command: "git diff --no-color",
|
|
1822
|
+
format: "patch",
|
|
1823
|
+
output: "",
|
|
1824
|
+
outputTruncated: false,
|
|
1825
|
+
},
|
|
1826
|
+
message: "현재 경로가 Git 저장소가 아닙니다.",
|
|
1827
|
+
},
|
|
1828
|
+
},
|
|
1829
|
+
});
|
|
1830
|
+
return;
|
|
1831
|
+
}
|
|
1832
|
+
const repoRootAbs = topLevelResult.stdout.trim();
|
|
1833
|
+
const prefixResult = await runLocalCommand("git", ["-C", request.targetPath, "rev-parse", "--show-prefix"], request.targetPath);
|
|
1834
|
+
const repoRelativePath = prefixResult.code === 0 ? (prefixResult.stdout.trim().replace(/\/$/, "") || ".") : ".";
|
|
1835
|
+
const branchResult = await runLocalCommand("git", ["-C", repoRootAbs, "symbolic-ref", "--quiet", "--short", "HEAD"], repoRootAbs);
|
|
1836
|
+
const detachedResult = branchResult.code === 0 ? null : await runLocalCommand("git", ["-C", repoRootAbs, "rev-parse", "--short", "HEAD"], repoRootAbs);
|
|
1837
|
+
const branch = branchResult.code === 0 ? branchResult.stdout.trim() || null : detachedResult && detachedResult.code === 0 ? detachedResult.stdout.trim() || null : null;
|
|
1838
|
+
const gitDiffArgs = buildAgentGitDiffArgs(repoRootAbs, request);
|
|
1839
|
+
const gitDiffResult = await runLocalCommand("git", gitDiffArgs.args, repoRootAbs);
|
|
1840
|
+
if (gitDiffResult.code !== 0) {
|
|
1841
|
+
throw new Error(gitDiffResult.stderr.trim() || "Failed to run agent git diff");
|
|
1842
|
+
}
|
|
1843
|
+
const withUntracked = await appendAgentLocalUntrackedDiff(repoRootAbs, request, gitDiffResult.stdout);
|
|
1844
|
+
publishGitRpcResponse({
|
|
1845
|
+
nc: args.jetstream.nc,
|
|
1846
|
+
responseSubject,
|
|
1847
|
+
payload: {
|
|
1848
|
+
requestId,
|
|
1849
|
+
ok: true,
|
|
1850
|
+
payload: {
|
|
1851
|
+
isGitRepo: true,
|
|
1852
|
+
mode: "git_diff",
|
|
1853
|
+
source: "agent",
|
|
1854
|
+
agent: { id: args.agentId, name: null },
|
|
1855
|
+
currentPath: request.targetPath,
|
|
1856
|
+
repoRoot: repoRootAbs,
|
|
1857
|
+
repoRelativePath,
|
|
1858
|
+
branch,
|
|
1859
|
+
gitDiff: {
|
|
1860
|
+
command: withUntracked.hasUntracked ? `${gitDiffArgs.display} (+ untracked)` : gitDiffArgs.display,
|
|
1861
|
+
format: request.format,
|
|
1862
|
+
output: withUntracked.output,
|
|
1863
|
+
outputTruncated: false,
|
|
1864
|
+
},
|
|
1865
|
+
},
|
|
1866
|
+
},
|
|
1867
|
+
});
|
|
1868
|
+
}
|
|
1869
|
+
catch (error) {
|
|
1870
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1871
|
+
if (responseSubject) {
|
|
1872
|
+
publishGitRpcResponse({
|
|
1873
|
+
nc: args.jetstream.nc,
|
|
1874
|
+
responseSubject,
|
|
1875
|
+
payload: { requestId, ok: false, error: message },
|
|
1876
|
+
});
|
|
1877
|
+
}
|
|
1878
|
+
writeAgentError(`git rpc failed requestId=${requestId} error=${message}`);
|
|
1879
|
+
}
|
|
1880
|
+
}
|
|
1881
|
+
function subscribeToGitRpc(args) {
|
|
1882
|
+
const subject = buildAgentGitRpcSubject(args.userId, args.agentId);
|
|
1883
|
+
args.jetstream.nc.subscribe(subject, {
|
|
1884
|
+
callback: (error, msg) => {
|
|
1885
|
+
if (error) {
|
|
1886
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1887
|
+
writeAgentError(`git rpc subscription error: ${message}`);
|
|
1888
|
+
return;
|
|
1889
|
+
}
|
|
1890
|
+
void handleGitRpcMessage({
|
|
1891
|
+
msg,
|
|
1892
|
+
jetstream: args.jetstream,
|
|
1893
|
+
userId: args.userId,
|
|
1894
|
+
agentId: args.agentId,
|
|
1895
|
+
});
|
|
1896
|
+
},
|
|
1897
|
+
});
|
|
1898
|
+
writeAgentInfo(`git rpc subscribed subject=${subject}`);
|
|
1899
|
+
}
|
|
1900
|
+
async function handleRunRpcMessage(args) {
|
|
1901
|
+
let requestId = "unknown";
|
|
1902
|
+
let responseSubject = "";
|
|
1903
|
+
try {
|
|
1904
|
+
const payload = JSON.parse(runRpcCodec.decode(args.msg.data));
|
|
1905
|
+
const request = normalizeRunRpcRequest({ request: payload, agentId: args.agentId });
|
|
1906
|
+
requestId = request.requestId;
|
|
1907
|
+
responseSubject = request.responseSubject;
|
|
1908
|
+
if (request.action === "start") {
|
|
1909
|
+
const runId = request.runId ?? requestId;
|
|
1910
|
+
await claimRunStartSlot({ runId, sessionId: request.sessionId });
|
|
1911
|
+
try {
|
|
1912
|
+
const task = await startManagedRun({
|
|
1913
|
+
requestId,
|
|
1914
|
+
runId,
|
|
1915
|
+
serverBaseUrl: args.serverBaseUrl,
|
|
1916
|
+
userId: args.userId,
|
|
1917
|
+
agentId: args.agentId,
|
|
1918
|
+
sessionId: request.sessionId,
|
|
1919
|
+
codexArgs: buildManagedCodexArgs({
|
|
1920
|
+
prompt: request.prompt ?? "",
|
|
1921
|
+
sessionId: request.sessionId,
|
|
1922
|
+
model: request.model,
|
|
1923
|
+
}),
|
|
1924
|
+
cwd: request.cwd,
|
|
1925
|
+
runtimeEnvPatch: request.runtimeEnvPatch,
|
|
1926
|
+
codexAuthBundle: request.codexAuthBundle,
|
|
1927
|
+
agentToken: args.agentToken,
|
|
1928
|
+
});
|
|
1929
|
+
publishRunRpcResponse({ nc: args.jetstream.nc, responseSubject, payload: { requestId, ok: true, task } });
|
|
1930
|
+
}
|
|
1931
|
+
catch (error) {
|
|
1932
|
+
await releaseRunStartSlot({ runId, sessionId: request.sessionId }).catch(() => undefined);
|
|
1933
|
+
throw error;
|
|
1934
|
+
}
|
|
1935
|
+
return;
|
|
1936
|
+
}
|
|
1937
|
+
if (request.action === "list") {
|
|
1938
|
+
const tasks = [...activeRuns.values()].map((entry) => cloneRunTask(entry.task));
|
|
1939
|
+
const retained = [...retainedRuns.values()].filter((task) => !activeRuns.has(task.id)).map((task) => cloneRunTask(task));
|
|
1940
|
+
const merged = [...tasks, ...retained]
|
|
1941
|
+
.sort((a, b) => Date.parse(b.updatedAt) - Date.parse(a.updatedAt))
|
|
1942
|
+
.slice(0, request.limit);
|
|
1943
|
+
publishRunRpcResponse({ nc: args.jetstream.nc, responseSubject, payload: { requestId, ok: true, tasks: merged } });
|
|
1944
|
+
return;
|
|
1945
|
+
}
|
|
1946
|
+
const stored = request.runId ? getStoredRun(request.runId) : null;
|
|
1947
|
+
if (!stored || stored.agentId !== args.agentId || stored.userId !== args.userId) {
|
|
1948
|
+
throw new Error("Run not found");
|
|
1949
|
+
}
|
|
1950
|
+
if (request.action === "cancel") {
|
|
1951
|
+
const active = activeRuns.get(stored.id);
|
|
1952
|
+
active?.requestCancel();
|
|
1953
|
+
const task = cloneRunTask(active?.task ?? stored);
|
|
1954
|
+
publishRunRpcResponse({ nc: args.jetstream.nc, responseSubject, payload: { requestId, ok: true, task } });
|
|
1955
|
+
return;
|
|
1956
|
+
}
|
|
1957
|
+
const task = cloneRunTask(stored, request.sinceSeq);
|
|
1958
|
+
publishRunRpcResponse({ nc: args.jetstream.nc, responseSubject, payload: { requestId, ok: true, task } });
|
|
1959
|
+
}
|
|
1960
|
+
catch (error) {
|
|
1961
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1962
|
+
if (responseSubject) {
|
|
1963
|
+
publishRunRpcResponse({
|
|
1964
|
+
nc: args.jetstream.nc,
|
|
1965
|
+
responseSubject,
|
|
1966
|
+
payload: { requestId, ok: false, error: message },
|
|
1967
|
+
});
|
|
1968
|
+
}
|
|
1969
|
+
writeAgentError(`run rpc failed requestId=${requestId} error=${message}`);
|
|
1970
|
+
}
|
|
1971
|
+
}
|
|
1972
|
+
function subscribeToRunRpc(args) {
|
|
1973
|
+
const subject = buildAgentRunRpcSubject(args.userId, args.agentId);
|
|
1974
|
+
args.jetstream.nc.subscribe(subject, {
|
|
1975
|
+
callback: (error, msg) => {
|
|
1976
|
+
if (error) {
|
|
1977
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1978
|
+
writeAgentError(`run rpc subscription error: ${message}`);
|
|
1979
|
+
return;
|
|
1980
|
+
}
|
|
1981
|
+
void handleRunRpcMessage({
|
|
1982
|
+
msg,
|
|
1983
|
+
jetstream: args.jetstream,
|
|
1984
|
+
serverBaseUrl: args.serverBaseUrl,
|
|
1985
|
+
userId: args.userId,
|
|
1986
|
+
agentId: args.agentId,
|
|
1987
|
+
agentToken: args.agentToken,
|
|
670
1988
|
});
|
|
671
1989
|
},
|
|
672
1990
|
});
|
|
673
1991
|
writeAgentInfo(`run rpc subscribed subject=${subject}`);
|
|
674
1992
|
}
|
|
675
|
-
function isLikelyNatsAuthError(error) {
|
|
676
|
-
const message = (error instanceof Error ? error.message : String(error)).toLowerCase();
|
|
677
|
-
return (message.includes("auth")
|
|
678
|
-
|| message.includes("authorization")
|
|
679
|
-
|| message.includes("authentication")
|
|
680
|
-
|| message.includes("permission")
|
|
681
|
-
|| message.includes("jwt")
|
|
682
|
-
|| message.includes("token"));
|
|
1993
|
+
function isLikelyNatsAuthError(error) {
|
|
1994
|
+
const message = (error instanceof Error ? error.message : String(error)).toLowerCase();
|
|
1995
|
+
return (message.includes("auth")
|
|
1996
|
+
|| message.includes("authorization")
|
|
1997
|
+
|| message.includes("authentication")
|
|
1998
|
+
|| message.includes("permission")
|
|
1999
|
+
|| message.includes("jwt")
|
|
2000
|
+
|| message.includes("token"));
|
|
2001
|
+
}
|
|
2002
|
+
function isLikelyNatsReconnectError(error) {
|
|
2003
|
+
const message = (error instanceof Error ? error.message : String(error)).toLowerCase();
|
|
2004
|
+
return (message.includes("connection_closed")
|
|
2005
|
+
|| message.includes("connection closed")
|
|
2006
|
+
|| message.includes("closed connection")
|
|
2007
|
+
|| message.includes("disconnected")
|
|
2008
|
+
|| message.includes("timeout")
|
|
2009
|
+
|| message.includes("no responders"));
|
|
2010
|
+
}
|
|
2011
|
+
function sendSignalToTaskProcess(child, signal) {
|
|
2012
|
+
if (process.platform !== "win32" && typeof child.pid === "number") {
|
|
2013
|
+
try {
|
|
2014
|
+
// Detached child owns a process group; signal the whole group first.
|
|
2015
|
+
process.kill(-child.pid, signal);
|
|
2016
|
+
return;
|
|
2017
|
+
}
|
|
2018
|
+
catch {
|
|
2019
|
+
// Fall back to direct child signaling.
|
|
2020
|
+
}
|
|
2021
|
+
}
|
|
2022
|
+
try {
|
|
2023
|
+
child.kill(signal);
|
|
2024
|
+
}
|
|
2025
|
+
catch {
|
|
2026
|
+
// noop
|
|
2027
|
+
}
|
|
2028
|
+
}
|
|
2029
|
+
function requestTaskCancellation(taskId, reason) {
|
|
2030
|
+
const requestCancel = activeTaskCancelRequests.get(taskId);
|
|
2031
|
+
if (!requestCancel) {
|
|
2032
|
+
return false;
|
|
2033
|
+
}
|
|
2034
|
+
try {
|
|
2035
|
+
requestCancel();
|
|
2036
|
+
writeAgentInfo(`task cancel requested taskId=${taskId} via=${reason}`);
|
|
2037
|
+
return true;
|
|
2038
|
+
}
|
|
2039
|
+
catch (error) {
|
|
2040
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2041
|
+
writeAgentError(`task cancel request failed taskId=${taskId} via=${reason}: ${message}`);
|
|
2042
|
+
return false;
|
|
2043
|
+
}
|
|
2044
|
+
}
|
|
2045
|
+
function resolveLogTimeZone() {
|
|
2046
|
+
const configured = process.env.DOER_AGENT_LOG_TIMEZONE?.trim() || process.env.TZ?.trim();
|
|
2047
|
+
return configured && configured.length > 0 ? configured : "Asia/Seoul";
|
|
2048
|
+
}
|
|
2049
|
+
function resolveTimeZoneOffsetString(date, timeZone) {
|
|
2050
|
+
try {
|
|
2051
|
+
const parts = new Intl.DateTimeFormat("en-US", {
|
|
2052
|
+
timeZone,
|
|
2053
|
+
timeZoneName: "shortOffset",
|
|
2054
|
+
hour: "2-digit",
|
|
2055
|
+
minute: "2-digit",
|
|
2056
|
+
hour12: false,
|
|
2057
|
+
}).formatToParts(date);
|
|
2058
|
+
const token = parts.find((part) => part.type === "timeZoneName")?.value || "GMT+0";
|
|
2059
|
+
const matched = token.match(/GMT([+-]\d{1,2})(?::?(\d{2}))?/i);
|
|
2060
|
+
if (!matched) {
|
|
2061
|
+
return "+00:00";
|
|
2062
|
+
}
|
|
2063
|
+
const hourRaw = matched[1] || "+0";
|
|
2064
|
+
const minuteRaw = matched[2] || "00";
|
|
2065
|
+
const sign = hourRaw.startsWith("-") ? "-" : "+";
|
|
2066
|
+
const absHour = String(Math.abs(Number.parseInt(hourRaw, 10))).padStart(2, "0");
|
|
2067
|
+
const absMinute = String(Math.abs(Number.parseInt(minuteRaw, 10))).padStart(2, "0");
|
|
2068
|
+
return `${sign}${absHour}:${absMinute}`;
|
|
2069
|
+
}
|
|
2070
|
+
catch {
|
|
2071
|
+
return "+00:00";
|
|
2072
|
+
}
|
|
2073
|
+
}
|
|
2074
|
+
function formatLocalTimestamp(date = new Date()) {
|
|
2075
|
+
const timeZone = resolveLogTimeZone();
|
|
2076
|
+
try {
|
|
2077
|
+
const parts = new Intl.DateTimeFormat("en-CA", {
|
|
2078
|
+
timeZone,
|
|
2079
|
+
year: "numeric",
|
|
2080
|
+
month: "2-digit",
|
|
2081
|
+
day: "2-digit",
|
|
2082
|
+
hour: "2-digit",
|
|
2083
|
+
minute: "2-digit",
|
|
2084
|
+
second: "2-digit",
|
|
2085
|
+
hour12: false,
|
|
2086
|
+
}).formatToParts(date);
|
|
2087
|
+
const pick = (type) => {
|
|
2088
|
+
return parts.find((part) => part.type === type)?.value || "00";
|
|
2089
|
+
};
|
|
2090
|
+
const year = pick("year");
|
|
2091
|
+
const month = pick("month");
|
|
2092
|
+
const day = pick("day");
|
|
2093
|
+
const hours = pick("hour");
|
|
2094
|
+
const minutes = pick("minute");
|
|
2095
|
+
const seconds = pick("second");
|
|
2096
|
+
const ms = String(date.getMilliseconds()).padStart(3, "0");
|
|
2097
|
+
const offset = resolveTimeZoneOffsetString(date, timeZone);
|
|
2098
|
+
return `${year}-${month}-${day}T${hours}:${minutes}:${seconds}.${ms}${offset}`;
|
|
2099
|
+
}
|
|
2100
|
+
catch {
|
|
2101
|
+
return date.toISOString();
|
|
2102
|
+
}
|
|
2103
|
+
}
|
|
2104
|
+
function parseArgs(argv) {
|
|
2105
|
+
const out = {};
|
|
2106
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
2107
|
+
const key = argv[i];
|
|
2108
|
+
if (!key.startsWith("--")) {
|
|
2109
|
+
continue;
|
|
2110
|
+
}
|
|
2111
|
+
const value = argv[i + 1];
|
|
2112
|
+
if (typeof value === "string" && !value.startsWith("--")) {
|
|
2113
|
+
out[key.slice(2)] = value;
|
|
2114
|
+
i += 1;
|
|
2115
|
+
continue;
|
|
2116
|
+
}
|
|
2117
|
+
out[key.slice(2)] = "true";
|
|
2118
|
+
}
|
|
2119
|
+
return out;
|
|
683
2120
|
}
|
|
684
|
-
function
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
2121
|
+
function resolveArgOrEnv(args, argKeys, envKeys, fallback = "") {
|
|
2122
|
+
for (const key of argKeys) {
|
|
2123
|
+
const value = args[key]?.trim();
|
|
2124
|
+
if (value) {
|
|
2125
|
+
return value;
|
|
2126
|
+
}
|
|
2127
|
+
}
|
|
2128
|
+
for (const key of envKeys) {
|
|
2129
|
+
const value = process.env[key]?.trim();
|
|
2130
|
+
if (value) {
|
|
2131
|
+
return value;
|
|
2132
|
+
}
|
|
2133
|
+
}
|
|
2134
|
+
return fallback;
|
|
692
2135
|
}
|
|
693
|
-
function
|
|
694
|
-
if (process.platform
|
|
2136
|
+
function resolveShellPath() {
|
|
2137
|
+
if (process.platform === "win32") {
|
|
2138
|
+
return process.env.ComSpec || "cmd.exe";
|
|
2139
|
+
}
|
|
2140
|
+
const candidates = [process.env.SHELL, "/bin/bash", "/usr/bin/bash", "/bin/sh", "/usr/bin/sh"].filter((value) => typeof value === "string" && value.trim().length > 0);
|
|
2141
|
+
for (const candidate of candidates) {
|
|
2142
|
+
if (existsSync(candidate)) {
|
|
2143
|
+
return candidate;
|
|
2144
|
+
}
|
|
2145
|
+
}
|
|
2146
|
+
throw new Error("No shell executable found. Set SHELL env or install /bin/sh (or bash).");
|
|
2147
|
+
}
|
|
2148
|
+
function resolveTaskWorkspace(rawCwd) {
|
|
2149
|
+
const workspaceRoot = workspaceRootOverride ?? (process.env.WORKSPACE?.trim() || process.cwd());
|
|
2150
|
+
const requestedCwd = rawCwd?.trim() || "";
|
|
2151
|
+
const resolvedCwd = requestedCwd
|
|
2152
|
+
? path.isAbsolute(requestedCwd)
|
|
2153
|
+
? path.resolve(requestedCwd)
|
|
2154
|
+
: path.resolve(workspaceRoot, requestedCwd)
|
|
2155
|
+
: workspaceRoot;
|
|
2156
|
+
if (!existsSync(resolvedCwd)) {
|
|
2157
|
+
throw new Error(`Invalid cwd: ${requestedCwd || "(empty)"} resolved to ${resolvedCwd} (path does not exist)`);
|
|
2158
|
+
}
|
|
2159
|
+
let stats;
|
|
2160
|
+
try {
|
|
2161
|
+
stats = statSync(resolvedCwd);
|
|
2162
|
+
}
|
|
2163
|
+
catch (error) {
|
|
2164
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2165
|
+
throw new Error(`Invalid cwd: ${requestedCwd || "(empty)"} resolved to ${resolvedCwd} (${message})`);
|
|
2166
|
+
}
|
|
2167
|
+
if (!stats.isDirectory()) {
|
|
2168
|
+
throw new Error(`Invalid cwd: ${requestedCwd || "(empty)"} resolved to ${resolvedCwd} (not a directory)`);
|
|
2169
|
+
}
|
|
2170
|
+
return resolvedCwd;
|
|
2171
|
+
}
|
|
2172
|
+
function buildAgentFsRpcSubject(userId, agentId) {
|
|
2173
|
+
return `doer.agent.fs.rpc.${sanitizeUserId(userId)}.${agentId.trim()}`;
|
|
2174
|
+
}
|
|
2175
|
+
function buildAgentShellRpcSubject(userId, agentId) {
|
|
2176
|
+
return `doer.agent.shell.rpc.${sanitizeUserId(userId)}.${agentId.trim()}`;
|
|
2177
|
+
}
|
|
2178
|
+
function normalizeFsRpcPath(rawPath) {
|
|
2179
|
+
const root = workspaceRootOverride ?? (process.env.WORKSPACE?.trim() || process.cwd());
|
|
2180
|
+
const raw = typeof rawPath === "string" && rawPath.trim() ? rawPath.trim() : ".";
|
|
2181
|
+
const normalizedRaw = raw.replace(/\\/g, "/");
|
|
2182
|
+
const useAbsolute = path.isAbsolute(normalizedRaw);
|
|
2183
|
+
const rel = normalizedRaw.replace(/^\/+/, "") || ".";
|
|
2184
|
+
const abs = useAbsolute ? path.resolve(normalizedRaw) : path.resolve(root, rel);
|
|
2185
|
+
if (!useAbsolute && abs !== root && !abs.startsWith(root + path.sep)) {
|
|
2186
|
+
throw new Error("path escapes workspace root");
|
|
2187
|
+
}
|
|
2188
|
+
const formatPath = (target) => {
|
|
2189
|
+
if (useAbsolute) {
|
|
2190
|
+
return target.split(path.sep).join("/") || "/";
|
|
2191
|
+
}
|
|
2192
|
+
return path.relative(root, target).split(path.sep).join("/") || ".";
|
|
2193
|
+
};
|
|
2194
|
+
return { abs, formatPath };
|
|
2195
|
+
}
|
|
2196
|
+
function parseFsRpcAction(value) {
|
|
2197
|
+
if (value === "list" ||
|
|
2198
|
+
value === "stat" ||
|
|
2199
|
+
value === "fetch_file" ||
|
|
2200
|
+
value === "read_text" ||
|
|
2201
|
+
value === "read_file" ||
|
|
2202
|
+
value === "write_file" ||
|
|
2203
|
+
value === "download_file") {
|
|
2204
|
+
return value;
|
|
2205
|
+
}
|
|
2206
|
+
throw new Error("unsupported action");
|
|
2207
|
+
}
|
|
2208
|
+
function normalizeFsRpcNumber(value, fallback) {
|
|
2209
|
+
const n = Number(value);
|
|
2210
|
+
if (!Number.isFinite(n)) {
|
|
2211
|
+
return fallback;
|
|
2212
|
+
}
|
|
2213
|
+
return Math.floor(n);
|
|
2214
|
+
}
|
|
2215
|
+
function inferMimeType(filePath) {
|
|
2216
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
2217
|
+
if (ext === ".txt" || ext === ".md" || ext === ".log") {
|
|
2218
|
+
return "text/plain";
|
|
2219
|
+
}
|
|
2220
|
+
if (ext === ".json") {
|
|
2221
|
+
return "application/json";
|
|
2222
|
+
}
|
|
2223
|
+
if (ext === ".js" || ext === ".mjs" || ext === ".cjs") {
|
|
2224
|
+
return "text/javascript";
|
|
2225
|
+
}
|
|
2226
|
+
if (ext === ".ts" || ext === ".tsx") {
|
|
2227
|
+
return "text/typescript";
|
|
2228
|
+
}
|
|
2229
|
+
if (ext === ".jsx") {
|
|
2230
|
+
return "text/jsx";
|
|
2231
|
+
}
|
|
2232
|
+
if (ext === ".css") {
|
|
2233
|
+
return "text/css";
|
|
2234
|
+
}
|
|
2235
|
+
if (ext === ".html" || ext === ".htm") {
|
|
2236
|
+
return "text/html";
|
|
2237
|
+
}
|
|
2238
|
+
if (ext === ".xml") {
|
|
2239
|
+
return "application/xml";
|
|
2240
|
+
}
|
|
2241
|
+
if (ext === ".svg") {
|
|
2242
|
+
return "image/svg+xml";
|
|
2243
|
+
}
|
|
2244
|
+
if (ext === ".png") {
|
|
2245
|
+
return "image/png";
|
|
2246
|
+
}
|
|
2247
|
+
if (ext === ".jpg" || ext === ".jpeg") {
|
|
2248
|
+
return "image/jpeg";
|
|
2249
|
+
}
|
|
2250
|
+
if (ext === ".gif") {
|
|
2251
|
+
return "image/gif";
|
|
2252
|
+
}
|
|
2253
|
+
if (ext === ".webp") {
|
|
2254
|
+
return "image/webp";
|
|
2255
|
+
}
|
|
2256
|
+
if (ext === ".pdf") {
|
|
2257
|
+
return "application/pdf";
|
|
2258
|
+
}
|
|
2259
|
+
return "application/octet-stream";
|
|
2260
|
+
}
|
|
2261
|
+
async function executeFsRpc(args) {
|
|
2262
|
+
const action = parseFsRpcAction(args.request.action);
|
|
2263
|
+
const { abs, formatPath } = normalizeFsRpcPath(args.request.path);
|
|
2264
|
+
if (action === "stat") {
|
|
2265
|
+
const entry = await stat(abs);
|
|
2266
|
+
return {
|
|
2267
|
+
ok: true,
|
|
2268
|
+
action,
|
|
2269
|
+
path: formatPath(abs),
|
|
2270
|
+
kind: entry.isDirectory() ? "dir" : "file",
|
|
2271
|
+
size: entry.size,
|
|
2272
|
+
mtimeMs: entry.mtimeMs,
|
|
2273
|
+
};
|
|
2274
|
+
}
|
|
2275
|
+
if (action === "list") {
|
|
2276
|
+
const entry = await stat(abs);
|
|
2277
|
+
if (!entry.isDirectory()) {
|
|
2278
|
+
throw new Error("path is not a directory");
|
|
2279
|
+
}
|
|
2280
|
+
const limit = Math.max(1, Math.min(normalizeFsRpcNumber(args.request.limit, 200), 1000));
|
|
2281
|
+
const rows = await readdir(abs, { withFileTypes: true });
|
|
2282
|
+
const items = await Promise.all(rows.map(async (row) => {
|
|
2283
|
+
const child = path.join(abs, row.name);
|
|
2284
|
+
const childStat = await stat(child);
|
|
2285
|
+
return {
|
|
2286
|
+
name: row.name,
|
|
2287
|
+
path: formatPath(child),
|
|
2288
|
+
kind: row.isDirectory() ? "dir" : "file",
|
|
2289
|
+
size: childStat.size,
|
|
2290
|
+
mtimeMs: childStat.mtimeMs,
|
|
2291
|
+
};
|
|
2292
|
+
}));
|
|
2293
|
+
items.sort((a, b) => (a.kind === b.kind ? a.name.localeCompare(b.name) : a.kind === "dir" ? -1 : 1));
|
|
2294
|
+
return {
|
|
2295
|
+
ok: true,
|
|
2296
|
+
action,
|
|
2297
|
+
path: formatPath(abs),
|
|
2298
|
+
items: items.slice(0, limit),
|
|
2299
|
+
truncated: items.length > limit,
|
|
2300
|
+
total: items.length,
|
|
2301
|
+
};
|
|
2302
|
+
}
|
|
2303
|
+
if (action === "fetch_file") {
|
|
2304
|
+
const entry = await stat(abs);
|
|
2305
|
+
if (!entry.isFile()) {
|
|
2306
|
+
throw new Error("path is not a file");
|
|
2307
|
+
}
|
|
2308
|
+
const uploadUrl = typeof args.request.uploadUrl === "string" ? args.request.uploadUrl : "";
|
|
2309
|
+
const agentId = typeof args.request.agentId === "string" ? args.request.agentId : "";
|
|
2310
|
+
if (!uploadUrl || !agentId) {
|
|
2311
|
+
throw new Error("missing upload parameters");
|
|
2312
|
+
}
|
|
2313
|
+
const data = await readFile(abs);
|
|
2314
|
+
const fileName = path.basename(abs) || "file";
|
|
2315
|
+
const form = new FormData();
|
|
2316
|
+
form.append("file", new File([data], fileName));
|
|
2317
|
+
form.append("agentId", agentId);
|
|
2318
|
+
const response = await fetch(uploadUrl, {
|
|
2319
|
+
method: "POST",
|
|
2320
|
+
headers: { Authorization: `Bearer ${args.agentToken}` },
|
|
2321
|
+
body: form,
|
|
2322
|
+
});
|
|
2323
|
+
const text = await response.text();
|
|
2324
|
+
let upload = {};
|
|
2325
|
+
try {
|
|
2326
|
+
upload = JSON.parse(text || "{}");
|
|
2327
|
+
}
|
|
2328
|
+
catch {
|
|
2329
|
+
upload = {};
|
|
2330
|
+
}
|
|
2331
|
+
if (!response.ok) {
|
|
2332
|
+
const message = typeof upload.error === "string" ? upload.error : `upload failed: ${response.status}`;
|
|
2333
|
+
throw new Error(message);
|
|
2334
|
+
}
|
|
2335
|
+
return {
|
|
2336
|
+
ok: true,
|
|
2337
|
+
action,
|
|
2338
|
+
path: formatPath(abs),
|
|
2339
|
+
size: entry.size,
|
|
2340
|
+
upload,
|
|
2341
|
+
};
|
|
2342
|
+
}
|
|
2343
|
+
if (action === "write_file") {
|
|
2344
|
+
const contentBase64 = typeof args.request.contentBase64 === "string" ? args.request.contentBase64 : "";
|
|
2345
|
+
if (!contentBase64) {
|
|
2346
|
+
throw new Error("contentBase64 is required");
|
|
2347
|
+
}
|
|
2348
|
+
const parentDir = path.dirname(abs);
|
|
2349
|
+
await mkdir(parentDir, { recursive: true });
|
|
2350
|
+
const bytes = Buffer.from(contentBase64, "base64");
|
|
2351
|
+
await writeFile(abs, bytes);
|
|
2352
|
+
const entry = await stat(abs);
|
|
2353
|
+
return {
|
|
2354
|
+
ok: true,
|
|
2355
|
+
action,
|
|
2356
|
+
path: formatPath(abs),
|
|
2357
|
+
absolutePath: abs.split(path.sep).join("/"),
|
|
2358
|
+
size: entry.size,
|
|
2359
|
+
mimeType: inferMimeType(abs),
|
|
2360
|
+
mtimeMs: entry.mtimeMs,
|
|
2361
|
+
};
|
|
2362
|
+
}
|
|
2363
|
+
if (action === "download_file") {
|
|
2364
|
+
const downloadPath = typeof args.request.downloadPath === "string" ? args.request.downloadPath.trim() : "";
|
|
2365
|
+
if (!downloadPath) {
|
|
2366
|
+
throw new Error("downloadPath is required");
|
|
2367
|
+
}
|
|
2368
|
+
const downloadUrl = new URL(downloadPath, `${args.serverBaseUrl}/`).toString();
|
|
2369
|
+
const response = await fetch(downloadUrl, {
|
|
2370
|
+
method: "GET",
|
|
2371
|
+
headers: {
|
|
2372
|
+
Authorization: `Bearer ${args.agentToken}`,
|
|
2373
|
+
},
|
|
2374
|
+
});
|
|
2375
|
+
if (!response.ok) {
|
|
2376
|
+
const text = await response.text().catch(() => "");
|
|
2377
|
+
throw new Error(text || `download failed: ${response.status}`);
|
|
2378
|
+
}
|
|
2379
|
+
const bytes = Buffer.from(await response.arrayBuffer());
|
|
2380
|
+
const parentDir = path.dirname(abs);
|
|
2381
|
+
await mkdir(parentDir, { recursive: true });
|
|
2382
|
+
await writeFile(abs, bytes);
|
|
2383
|
+
const entry = await stat(abs);
|
|
2384
|
+
return {
|
|
2385
|
+
ok: true,
|
|
2386
|
+
action,
|
|
2387
|
+
path: formatPath(abs),
|
|
2388
|
+
absolutePath: abs.split(path.sep).join("/"),
|
|
2389
|
+
size: entry.size,
|
|
2390
|
+
mimeType: inferMimeType(abs),
|
|
2391
|
+
mtimeMs: entry.mtimeMs,
|
|
2392
|
+
};
|
|
2393
|
+
}
|
|
2394
|
+
const entry = await stat(abs);
|
|
2395
|
+
if (!entry.isFile()) {
|
|
2396
|
+
throw new Error("path is not a file");
|
|
2397
|
+
}
|
|
2398
|
+
if (action === "read_file") {
|
|
2399
|
+
const maxBytes = Math.max(1, Math.min(normalizeFsRpcNumber(args.request.maxBytes, 2_000_000), 5_000_000));
|
|
2400
|
+
const data = await readFile(abs);
|
|
2401
|
+
const truncated = data.byteLength > maxBytes;
|
|
2402
|
+
const bytes = truncated ? data.subarray(0, maxBytes) : data;
|
|
2403
|
+
return {
|
|
2404
|
+
ok: true,
|
|
2405
|
+
action,
|
|
2406
|
+
path: formatPath(abs),
|
|
2407
|
+
mimeType: inferMimeType(abs),
|
|
2408
|
+
size: entry.size,
|
|
2409
|
+
truncated,
|
|
2410
|
+
contentBase64: bytes.toString("base64"),
|
|
2411
|
+
};
|
|
2412
|
+
}
|
|
2413
|
+
const offset = Math.max(0, normalizeFsRpcNumber(args.request.offset, 0));
|
|
2414
|
+
const length = Math.max(1, Math.min(normalizeFsRpcNumber(args.request.length, 65536), 262144));
|
|
2415
|
+
const encoding = typeof args.request.encoding === "string" && args.request.encoding ? args.request.encoding : "utf8";
|
|
2416
|
+
const fd = await open(abs, "r");
|
|
2417
|
+
try {
|
|
2418
|
+
const buffer = Buffer.alloc(length);
|
|
2419
|
+
const readResult = await fd.read(buffer, 0, length, offset);
|
|
2420
|
+
const slice = buffer.subarray(0, readResult.bytesRead);
|
|
695
2421
|
try {
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
2422
|
+
const text = slice.toString(encoding);
|
|
2423
|
+
return {
|
|
2424
|
+
ok: true,
|
|
2425
|
+
action,
|
|
2426
|
+
path: formatPath(abs),
|
|
2427
|
+
offset,
|
|
2428
|
+
length: readResult.bytesRead,
|
|
2429
|
+
totalSize: entry.size,
|
|
2430
|
+
eof: offset + readResult.bytesRead >= entry.size,
|
|
2431
|
+
encoding,
|
|
2432
|
+
text,
|
|
2433
|
+
bytesRead: readResult.bytesRead,
|
|
2434
|
+
};
|
|
699
2435
|
}
|
|
700
|
-
catch {
|
|
701
|
-
|
|
2436
|
+
catch (error) {
|
|
2437
|
+
const message = error instanceof Error ? error.message : "failed to decode text";
|
|
2438
|
+
return {
|
|
2439
|
+
ok: false,
|
|
2440
|
+
action,
|
|
2441
|
+
path: formatPath(abs),
|
|
2442
|
+
error: message,
|
|
2443
|
+
};
|
|
702
2444
|
}
|
|
703
2445
|
}
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
}
|
|
707
|
-
catch {
|
|
708
|
-
// noop
|
|
2446
|
+
finally {
|
|
2447
|
+
await fd.close();
|
|
709
2448
|
}
|
|
710
2449
|
}
|
|
711
|
-
function
|
|
712
|
-
const
|
|
713
|
-
if (!
|
|
714
|
-
|
|
2450
|
+
function normalizeSessionRpcRequest(args) {
|
|
2451
|
+
const requestId = typeof args.request.requestId === "string" ? args.request.requestId.trim() : "";
|
|
2452
|
+
if (!requestId) {
|
|
2453
|
+
throw new Error("missing requestId");
|
|
715
2454
|
}
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
return true;
|
|
2455
|
+
const requestAgentId = typeof args.request.agentId === "string" ? args.request.agentId.trim() : "";
|
|
2456
|
+
if (!requestAgentId || requestAgentId !== args.agentId) {
|
|
2457
|
+
throw new Error("agent id mismatch");
|
|
720
2458
|
}
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
2459
|
+
const actionRaw = typeof args.request.action === "string" ? args.request.action.trim() : "";
|
|
2460
|
+
const action = actionRaw === "messages" || actionRaw === "delete" || actionRaw === "watch" || actionRaw === "stop_watch"
|
|
2461
|
+
? actionRaw
|
|
2462
|
+
: "list";
|
|
2463
|
+
const responseSubject = typeof args.request.responseSubject === "string" ? args.request.responseSubject.trim() : "";
|
|
2464
|
+
if (!responseSubject) {
|
|
2465
|
+
throw new Error("missing responseSubject");
|
|
2466
|
+
}
|
|
2467
|
+
const filePath = typeof args.request.filePath === "string" && args.request.filePath.trim() ? args.request.filePath.trim() : null;
|
|
2468
|
+
if ((action === "messages" || action === "delete" || action === "watch") && !filePath) {
|
|
2469
|
+
throw new Error("missing filePath");
|
|
2470
|
+
}
|
|
2471
|
+
const sinceLineRaw = Number(args.request.sinceLine);
|
|
2472
|
+
const sinceLine = Number.isInteger(sinceLineRaw) && sinceLineRaw > 0 ? sinceLineRaw : 0;
|
|
2473
|
+
const beforeRowIdRaw = Number(args.request.beforeRowId);
|
|
2474
|
+
const beforeRowId = Number.isInteger(beforeRowIdRaw) && beforeRowIdRaw > 0 ? beforeRowIdRaw : null;
|
|
2475
|
+
const pageSizeRaw = Number(args.request.pageSize);
|
|
2476
|
+
const pageSize = Number.isFinite(pageSizeRaw) ? Math.max(1, Math.min(Math.floor(pageSizeRaw), 100)) : 100;
|
|
2477
|
+
const watchId = typeof args.request.watchId === "string" && args.request.watchId.trim() ? args.request.watchId.trim() : null;
|
|
2478
|
+
if (action === "stop_watch" && !watchId) {
|
|
2479
|
+
throw new Error("missing watchId");
|
|
2480
|
+
}
|
|
2481
|
+
return {
|
|
2482
|
+
requestId,
|
|
2483
|
+
action,
|
|
2484
|
+
agentId: requestAgentId,
|
|
2485
|
+
filePath,
|
|
2486
|
+
sessionId: typeof args.request.sessionId === "string" && args.request.sessionId.trim() ? args.request.sessionId.trim() : null,
|
|
2487
|
+
sinceLine,
|
|
2488
|
+
beforeRowId,
|
|
2489
|
+
pageSize,
|
|
2490
|
+
responseSubject,
|
|
2491
|
+
watchId,
|
|
2492
|
+
};
|
|
2493
|
+
}
|
|
2494
|
+
function getSessionsRootPath() {
|
|
2495
|
+
const workspaceRoot = workspaceRootOverride ?? (process.env.WORKSPACE?.trim() || process.cwd());
|
|
2496
|
+
return path.join(workspaceRoot, ".codex", "sessions");
|
|
2497
|
+
}
|
|
2498
|
+
function resolveSessionFilePath(filePath) {
|
|
2499
|
+
const root = path.resolve(getSessionsRootPath());
|
|
2500
|
+
const resolved = path.resolve(filePath);
|
|
2501
|
+
if (!(resolved === root || resolved.startsWith(root + path.sep))) {
|
|
2502
|
+
throw new Error("filePath is outside sessions root");
|
|
2503
|
+
}
|
|
2504
|
+
return resolved;
|
|
2505
|
+
}
|
|
2506
|
+
function isObjectRecord(value) {
|
|
2507
|
+
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
2508
|
+
}
|
|
2509
|
+
const SESSION_RPC_BLOB_KEYS = new Set([
|
|
2510
|
+
"image_url",
|
|
2511
|
+
"image_base64",
|
|
2512
|
+
"content_base64",
|
|
2513
|
+
"file_data",
|
|
2514
|
+
"bytes",
|
|
2515
|
+
"data",
|
|
2516
|
+
]);
|
|
2517
|
+
function isInlineBlobString(value) {
|
|
2518
|
+
const trimmed = value.trim();
|
|
2519
|
+
if (!trimmed) {
|
|
724
2520
|
return false;
|
|
725
2521
|
}
|
|
2522
|
+
return trimmed.startsWith("data:") || trimmed.includes(";base64,");
|
|
726
2523
|
}
|
|
727
|
-
function
|
|
728
|
-
const
|
|
729
|
-
|
|
2524
|
+
function buildInlineBlobMarker(value) {
|
|
2525
|
+
const trimmed = value.trim();
|
|
2526
|
+
if (trimmed.startsWith("data:")) {
|
|
2527
|
+
const mimeEnd = trimmed.indexOf(";");
|
|
2528
|
+
const mimeType = mimeEnd > 5 ? trimmed.slice(5, mimeEnd) : "";
|
|
2529
|
+
if (mimeType) {
|
|
2530
|
+
return `[inline blob omitted: ${mimeType}]`;
|
|
2531
|
+
}
|
|
2532
|
+
}
|
|
2533
|
+
return "[inline blob omitted]";
|
|
730
2534
|
}
|
|
731
|
-
function
|
|
2535
|
+
function sanitizeSessionRpcPayload(value) {
|
|
2536
|
+
if (typeof value === "string") {
|
|
2537
|
+
return value;
|
|
2538
|
+
}
|
|
2539
|
+
if (Array.isArray(value)) {
|
|
2540
|
+
return value.map((entry) => sanitizeSessionRpcPayload(entry));
|
|
2541
|
+
}
|
|
2542
|
+
if (!isObjectRecord(value)) {
|
|
2543
|
+
return value;
|
|
2544
|
+
}
|
|
2545
|
+
const sanitized = {};
|
|
2546
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
2547
|
+
if (SESSION_RPC_BLOB_KEYS.has(key) && typeof entry === "string" && isInlineBlobString(entry)) {
|
|
2548
|
+
sanitized[key] = buildInlineBlobMarker(entry);
|
|
2549
|
+
continue;
|
|
2550
|
+
}
|
|
2551
|
+
sanitized[key] = sanitizeSessionRpcPayload(entry);
|
|
2552
|
+
}
|
|
2553
|
+
return sanitized;
|
|
2554
|
+
}
|
|
2555
|
+
function sanitizeSessionRpcRawLine(line) {
|
|
2556
|
+
const trimmed = line.trim();
|
|
2557
|
+
if (!trimmed.startsWith("{")) {
|
|
2558
|
+
return line;
|
|
2559
|
+
}
|
|
732
2560
|
try {
|
|
733
|
-
const
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
hour: "2-digit",
|
|
737
|
-
minute: "2-digit",
|
|
738
|
-
hour12: false,
|
|
739
|
-
}).formatToParts(date);
|
|
740
|
-
const token = parts.find((part) => part.type === "timeZoneName")?.value || "GMT+0";
|
|
741
|
-
const matched = token.match(/GMT([+-]\d{1,2})(?::?(\d{2}))?/i);
|
|
742
|
-
if (!matched) {
|
|
743
|
-
return "+00:00";
|
|
2561
|
+
const parsed = JSON.parse(line);
|
|
2562
|
+
if (!isObjectRecord(parsed) || !isObjectRecord(parsed.payload) || parsed.type !== "response_item") {
|
|
2563
|
+
return line;
|
|
744
2564
|
}
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
const absMinute = String(Math.abs(Number.parseInt(minuteRaw, 10))).padStart(2, "0");
|
|
750
|
-
return `${sign}${absHour}:${absMinute}`;
|
|
2565
|
+
return JSON.stringify({
|
|
2566
|
+
...parsed,
|
|
2567
|
+
payload: sanitizeSessionRpcPayload(parsed.payload),
|
|
2568
|
+
});
|
|
751
2569
|
}
|
|
752
2570
|
catch {
|
|
753
|
-
return
|
|
2571
|
+
return line;
|
|
754
2572
|
}
|
|
755
2573
|
}
|
|
756
|
-
function
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
const parts = new Intl.DateTimeFormat("en-CA", {
|
|
760
|
-
timeZone,
|
|
761
|
-
year: "numeric",
|
|
762
|
-
month: "2-digit",
|
|
763
|
-
day: "2-digit",
|
|
764
|
-
hour: "2-digit",
|
|
765
|
-
minute: "2-digit",
|
|
766
|
-
second: "2-digit",
|
|
767
|
-
hour12: false,
|
|
768
|
-
}).formatToParts(date);
|
|
769
|
-
const pick = (type) => {
|
|
770
|
-
return parts.find((part) => part.type === type)?.value || "00";
|
|
771
|
-
};
|
|
772
|
-
const year = pick("year");
|
|
773
|
-
const month = pick("month");
|
|
774
|
-
const day = pick("day");
|
|
775
|
-
const hours = pick("hour");
|
|
776
|
-
const minutes = pick("minute");
|
|
777
|
-
const seconds = pick("second");
|
|
778
|
-
const ms = String(date.getMilliseconds()).padStart(3, "0");
|
|
779
|
-
const offset = resolveTimeZoneOffsetString(date, timeZone);
|
|
780
|
-
return `${year}-${month}-${day}T${hours}:${minutes}:${seconds}.${ms}${offset}`;
|
|
2574
|
+
function toTrimmedStringOrNull(value) {
|
|
2575
|
+
if (typeof value !== "string") {
|
|
2576
|
+
return null;
|
|
781
2577
|
}
|
|
782
|
-
|
|
783
|
-
|
|
2578
|
+
const trimmed = value.trim();
|
|
2579
|
+
return trimmed || null;
|
|
2580
|
+
}
|
|
2581
|
+
function pickSessionString(...values) {
|
|
2582
|
+
for (const value of values) {
|
|
2583
|
+
const picked = toTrimmedStringOrNull(value);
|
|
2584
|
+
if (picked) {
|
|
2585
|
+
return picked;
|
|
2586
|
+
}
|
|
784
2587
|
}
|
|
2588
|
+
return null;
|
|
785
2589
|
}
|
|
786
|
-
function
|
|
787
|
-
const out =
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
2590
|
+
async function collectSessionJsonlFiles(rootDir) {
|
|
2591
|
+
const out = [];
|
|
2592
|
+
const stack = [rootDir];
|
|
2593
|
+
while (stack.length > 0) {
|
|
2594
|
+
const current = stack.pop();
|
|
2595
|
+
if (!current) {
|
|
791
2596
|
continue;
|
|
792
2597
|
}
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
2598
|
+
let entries = [];
|
|
2599
|
+
try {
|
|
2600
|
+
entries = await readdir(current, { withFileTypes: true });
|
|
2601
|
+
}
|
|
2602
|
+
catch {
|
|
797
2603
|
continue;
|
|
798
2604
|
}
|
|
799
|
-
|
|
2605
|
+
for (const entry of entries) {
|
|
2606
|
+
const fullPath = path.join(current, entry.name);
|
|
2607
|
+
if (entry.isDirectory()) {
|
|
2608
|
+
stack.push(fullPath);
|
|
2609
|
+
continue;
|
|
2610
|
+
}
|
|
2611
|
+
if (!entry.isFile() || !entry.name.toLowerCase().endsWith(".jsonl")) {
|
|
2612
|
+
continue;
|
|
2613
|
+
}
|
|
2614
|
+
try {
|
|
2615
|
+
const entryStat = await stat(fullPath);
|
|
2616
|
+
out.push({ filePath: fullPath, mtimeMs: entryStat.mtimeMs });
|
|
2617
|
+
}
|
|
2618
|
+
catch {
|
|
2619
|
+
// ignore removed files
|
|
2620
|
+
}
|
|
2621
|
+
}
|
|
800
2622
|
}
|
|
801
2623
|
return out;
|
|
802
2624
|
}
|
|
803
|
-
function
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
2625
|
+
async function readFirstLine(fileHandle, fileSize) {
|
|
2626
|
+
const chunkBytes = 16_384;
|
|
2627
|
+
const maxScanBytes = 262_144;
|
|
2628
|
+
let position = 0;
|
|
2629
|
+
let scanned = 0;
|
|
2630
|
+
let raw = "";
|
|
2631
|
+
while (position < fileSize && scanned < maxScanBytes) {
|
|
2632
|
+
const readSize = Math.min(chunkBytes, fileSize - position, maxScanBytes - scanned);
|
|
2633
|
+
const buffer = Buffer.alloc(readSize);
|
|
2634
|
+
const { bytesRead } = await fileHandle.read(buffer, 0, readSize, position);
|
|
2635
|
+
if (bytesRead <= 0) {
|
|
2636
|
+
break;
|
|
2637
|
+
}
|
|
2638
|
+
raw += buffer.toString("utf8", 0, bytesRead);
|
|
2639
|
+
scanned += bytesRead;
|
|
2640
|
+
position += bytesRead;
|
|
2641
|
+
const newlineIndex = raw.search(/\r?\n/);
|
|
2642
|
+
if (newlineIndex >= 0) {
|
|
2643
|
+
return raw.slice(0, newlineIndex).trim();
|
|
808
2644
|
}
|
|
809
2645
|
}
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
2646
|
+
return raw.trim();
|
|
2647
|
+
}
|
|
2648
|
+
function extractLastAgentMessage(candidateLines) {
|
|
2649
|
+
let fallback = null;
|
|
2650
|
+
for (const line of candidateLines) {
|
|
2651
|
+
const trimmed = line.trim();
|
|
2652
|
+
if (!trimmed) {
|
|
2653
|
+
continue;
|
|
2654
|
+
}
|
|
2655
|
+
try {
|
|
2656
|
+
const parsed = JSON.parse(trimmed);
|
|
2657
|
+
if (parsed.type !== "event_msg" || !isObjectRecord(parsed.payload)) {
|
|
2658
|
+
continue;
|
|
2659
|
+
}
|
|
2660
|
+
if (parsed.payload.type === "agent_message" && typeof parsed.payload.message === "string" && parsed.payload.message.trim()) {
|
|
2661
|
+
return {
|
|
2662
|
+
message: parsed.payload.message.trim(),
|
|
2663
|
+
updatedAt: toTrimmedStringOrNull(parsed.timestamp),
|
|
2664
|
+
};
|
|
2665
|
+
}
|
|
2666
|
+
if (!fallback &&
|
|
2667
|
+
parsed.payload.type === "task_complete" &&
|
|
2668
|
+
typeof parsed.payload.last_agent_message === "string" &&
|
|
2669
|
+
parsed.payload.last_agent_message.trim()) {
|
|
2670
|
+
fallback = {
|
|
2671
|
+
message: parsed.payload.last_agent_message.trim(),
|
|
2672
|
+
updatedAt: toTrimmedStringOrNull(parsed.timestamp),
|
|
2673
|
+
};
|
|
2674
|
+
}
|
|
2675
|
+
}
|
|
2676
|
+
catch {
|
|
2677
|
+
// ignore malformed lines
|
|
814
2678
|
}
|
|
815
2679
|
}
|
|
816
2680
|
return fallback;
|
|
817
2681
|
}
|
|
818
|
-
function
|
|
819
|
-
|
|
820
|
-
|
|
2682
|
+
async function readLastAgentMessage(fileHandle, fileSize) {
|
|
2683
|
+
const chunkBytes = 16_384;
|
|
2684
|
+
const maxScanBytes = 131_072;
|
|
2685
|
+
if (fileSize <= 0) {
|
|
2686
|
+
return null;
|
|
821
2687
|
}
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
2688
|
+
let position = fileSize;
|
|
2689
|
+
let scanned = 0;
|
|
2690
|
+
let carry = "";
|
|
2691
|
+
while (position > 0 && scanned < maxScanBytes) {
|
|
2692
|
+
const readSize = Math.min(chunkBytes, position, maxScanBytes - scanned);
|
|
2693
|
+
position -= readSize;
|
|
2694
|
+
scanned += readSize;
|
|
2695
|
+
const buffer = Buffer.alloc(readSize);
|
|
2696
|
+
const { bytesRead } = await fileHandle.read(buffer, 0, readSize, position);
|
|
2697
|
+
if (bytesRead <= 0) {
|
|
2698
|
+
break;
|
|
2699
|
+
}
|
|
2700
|
+
const merged = buffer.toString("utf8", 0, bytesRead) + carry;
|
|
2701
|
+
const lines = merged.split(/\r?\n/);
|
|
2702
|
+
carry = lines.shift() || "";
|
|
2703
|
+
const found = extractLastAgentMessage(lines.reverse());
|
|
2704
|
+
if (found) {
|
|
2705
|
+
return found;
|
|
826
2706
|
}
|
|
827
2707
|
}
|
|
828
|
-
|
|
2708
|
+
return extractLastAgentMessage([carry]);
|
|
829
2709
|
}
|
|
830
|
-
function
|
|
831
|
-
const
|
|
832
|
-
const
|
|
833
|
-
const
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
:
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
2710
|
+
function normalizeSessionMeta(rawMeta, filePath, mtimeMs) {
|
|
2711
|
+
const baseName = path.basename(filePath, path.extname(filePath));
|
|
2712
|
+
const meta = isObjectRecord(rawMeta) ? rawMeta : {};
|
|
2713
|
+
const updatedAtCandidate = pickSessionString(meta.updatedAt, meta.updated_at, meta.timestamp);
|
|
2714
|
+
return {
|
|
2715
|
+
id: pickSessionString(meta.sessionId, meta.session_id, meta.id) || baseName,
|
|
2716
|
+
label: pickSessionString(meta.label, meta.title, meta.name, meta.sessionLabel, meta.session_label) || baseName,
|
|
2717
|
+
updatedAt: updatedAtCandidate || new Date(mtimeMs).toISOString(),
|
|
2718
|
+
cwd: pickSessionString(meta.cwd, meta.workingDirectory, meta.working_directory),
|
|
2719
|
+
source: pickSessionString(meta.source, meta.sessionSource, meta.session_source) || "codex",
|
|
2720
|
+
originator: pickSessionString(meta.originator, meta.author, meta.user, meta.username) || "unknown",
|
|
2721
|
+
filePath,
|
|
2722
|
+
};
|
|
2723
|
+
}
|
|
2724
|
+
async function readSessionSummary(filePath, mtimeMs) {
|
|
2725
|
+
let fileHandle = null;
|
|
842
2726
|
try {
|
|
843
|
-
|
|
2727
|
+
fileHandle = await open(filePath, "r");
|
|
2728
|
+
const entryStat = await fileHandle.stat();
|
|
2729
|
+
const firstLine = await readFirstLine(fileHandle, entryStat.size);
|
|
2730
|
+
const tailSummary = await readLastAgentMessage(fileHandle, entryStat.size);
|
|
2731
|
+
let normalized = normalizeSessionMeta({}, filePath, mtimeMs);
|
|
2732
|
+
if (firstLine) {
|
|
2733
|
+
try {
|
|
2734
|
+
const parsed = JSON.parse(firstLine);
|
|
2735
|
+
const candidateMeta = parsed && parsed.type === "session_meta" && isObjectRecord(parsed.payload)
|
|
2736
|
+
? parsed.payload
|
|
2737
|
+
: isObjectRecord(parsed.session_meta)
|
|
2738
|
+
? parsed.session_meta
|
|
2739
|
+
: isObjectRecord(parsed.sessionMeta)
|
|
2740
|
+
? parsed.sessionMeta
|
|
2741
|
+
: isObjectRecord(parsed.meta)
|
|
2742
|
+
? parsed.meta
|
|
2743
|
+
: isObjectRecord(parsed.payload)
|
|
2744
|
+
? parsed.payload
|
|
2745
|
+
: parsed;
|
|
2746
|
+
normalized = normalizeSessionMeta(candidateMeta, filePath, mtimeMs);
|
|
2747
|
+
}
|
|
2748
|
+
catch {
|
|
2749
|
+
normalized = normalizeSessionMeta({}, filePath, mtimeMs);
|
|
2750
|
+
}
|
|
2751
|
+
}
|
|
2752
|
+
return {
|
|
2753
|
+
...normalized,
|
|
2754
|
+
label: tailSummary?.message || "(no agent message)",
|
|
2755
|
+
updatedAt: tailSummary?.updatedAt || normalized.updatedAt,
|
|
2756
|
+
};
|
|
844
2757
|
}
|
|
845
|
-
catch
|
|
846
|
-
|
|
847
|
-
throw new Error(`Invalid cwd: ${requestedCwd || "(empty)"} resolved to ${resolvedCwd} (${message})`);
|
|
2758
|
+
catch {
|
|
2759
|
+
return normalizeSessionMeta({}, filePath, mtimeMs);
|
|
848
2760
|
}
|
|
849
|
-
|
|
850
|
-
|
|
2761
|
+
finally {
|
|
2762
|
+
await fileHandle?.close().catch(() => undefined);
|
|
851
2763
|
}
|
|
852
|
-
return resolvedCwd;
|
|
853
|
-
}
|
|
854
|
-
function buildAgentFsRpcSubject(userId, agentId) {
|
|
855
|
-
return `doer.agent.fs.rpc.${sanitizeUserId(userId)}.${agentId.trim()}`;
|
|
856
2764
|
}
|
|
857
|
-
function
|
|
858
|
-
|
|
2765
|
+
async function listAgentSessions() {
|
|
2766
|
+
const sessionsRoot = getSessionsRootPath();
|
|
2767
|
+
let sessionsRootStat;
|
|
2768
|
+
try {
|
|
2769
|
+
sessionsRootStat = await stat(sessionsRoot);
|
|
2770
|
+
}
|
|
2771
|
+
catch {
|
|
2772
|
+
return [];
|
|
2773
|
+
}
|
|
2774
|
+
if (!sessionsRootStat.isDirectory()) {
|
|
2775
|
+
return [];
|
|
2776
|
+
}
|
|
2777
|
+
const files = await collectSessionJsonlFiles(sessionsRoot);
|
|
2778
|
+
files.sort((a, b) => b.mtimeMs - a.mtimeMs || a.filePath.localeCompare(b.filePath));
|
|
2779
|
+
const sessions = await Promise.all(files.slice(0, 10).map((file) => readSessionSummary(file.filePath, file.mtimeMs)));
|
|
2780
|
+
return sessions.sort((a, b) => Date.parse(b.updatedAt) - Date.parse(a.updatedAt) || b.filePath.localeCompare(a.filePath));
|
|
859
2781
|
}
|
|
860
|
-
function
|
|
861
|
-
const
|
|
862
|
-
const
|
|
863
|
-
const
|
|
864
|
-
const
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
2782
|
+
async function readSessionLineIndex(filePath) {
|
|
2783
|
+
const resolvedFile = resolveSessionFilePath(filePath);
|
|
2784
|
+
const entryStat = await stat(resolvedFile);
|
|
2785
|
+
const nextSize = entryStat.size;
|
|
2786
|
+
const cached = sessionLineIndexCache.get(resolvedFile) ?? null;
|
|
2787
|
+
if (cached && cached.size === nextSize) {
|
|
2788
|
+
return cached;
|
|
2789
|
+
}
|
|
2790
|
+
const fileHandle = await open(resolvedFile, "r");
|
|
2791
|
+
try {
|
|
2792
|
+
let lineStartOffsets = cached?.lineStartOffsets.slice() ?? [];
|
|
2793
|
+
let scanStart = cached?.size ?? 0;
|
|
2794
|
+
let endsWithNewline = cached?.endsWithNewline ?? false;
|
|
2795
|
+
if (!cached || nextSize < cached.size) {
|
|
2796
|
+
lineStartOffsets = nextSize > 0 ? [0] : [];
|
|
2797
|
+
scanStart = 0;
|
|
2798
|
+
endsWithNewline = false;
|
|
2799
|
+
}
|
|
2800
|
+
else if (cached.endsWithNewline && nextSize > cached.size) {
|
|
2801
|
+
lineStartOffsets.push(cached.size);
|
|
2802
|
+
}
|
|
2803
|
+
let position = scanStart;
|
|
2804
|
+
const chunkBytes = 65_536;
|
|
2805
|
+
while (position < nextSize) {
|
|
2806
|
+
const readSize = Math.min(chunkBytes, nextSize - position);
|
|
2807
|
+
const buffer = Buffer.alloc(readSize);
|
|
2808
|
+
const { bytesRead } = await fileHandle.read(buffer, 0, readSize, position);
|
|
2809
|
+
if (bytesRead <= 0) {
|
|
2810
|
+
break;
|
|
2811
|
+
}
|
|
2812
|
+
for (let index = 0; index < bytesRead; index += 1) {
|
|
2813
|
+
if (buffer[index] !== 0x0a) {
|
|
2814
|
+
continue;
|
|
2815
|
+
}
|
|
2816
|
+
const nextLineStart = position + index + 1;
|
|
2817
|
+
if (nextLineStart < nextSize) {
|
|
2818
|
+
lineStartOffsets.push(nextLineStart);
|
|
2819
|
+
}
|
|
2820
|
+
}
|
|
2821
|
+
position += bytesRead;
|
|
2822
|
+
}
|
|
2823
|
+
if (nextSize > 0) {
|
|
2824
|
+
const tail = Buffer.alloc(1);
|
|
2825
|
+
const { bytesRead } = await fileHandle.read(tail, 0, 1, nextSize - 1);
|
|
2826
|
+
endsWithNewline = bytesRead > 0 && tail[0] === 0x0a;
|
|
2827
|
+
}
|
|
2828
|
+
else {
|
|
2829
|
+
endsWithNewline = false;
|
|
2830
|
+
}
|
|
2831
|
+
const nextEntry = {
|
|
2832
|
+
size: nextSize,
|
|
2833
|
+
lineStartOffsets,
|
|
2834
|
+
endsWithNewline,
|
|
2835
|
+
};
|
|
2836
|
+
sessionLineIndexCache.set(resolvedFile, nextEntry);
|
|
2837
|
+
return nextEntry;
|
|
2838
|
+
}
|
|
2839
|
+
finally {
|
|
2840
|
+
await fileHandle.close().catch(() => undefined);
|
|
2841
|
+
}
|
|
2842
|
+
}
|
|
2843
|
+
async function getAgentSessionRawRows(args) {
|
|
2844
|
+
const resolvedFile = resolveSessionFilePath(args.filePath);
|
|
2845
|
+
const index = await readSessionLineIndex(resolvedFile);
|
|
2846
|
+
const totalLines = index.lineStartOffsets.length;
|
|
2847
|
+
const sinceLine = Math.max(0, Math.floor(args.sinceLine));
|
|
2848
|
+
const beforeRowId = args.beforeRowId && args.beforeRowId > 0 ? Math.floor(args.beforeRowId) : null;
|
|
2849
|
+
const maxRawRows = 200;
|
|
2850
|
+
const maxSelectionBytes = 120_000;
|
|
2851
|
+
const maxLineSelectionBytes = 4_096;
|
|
2852
|
+
const maxReadBytes = 2_000_000;
|
|
2853
|
+
if (totalLines === 0) {
|
|
2854
|
+
return {
|
|
2855
|
+
rawRows: [],
|
|
2856
|
+
nextCursor: 0,
|
|
2857
|
+
};
|
|
2858
|
+
}
|
|
2859
|
+
let startLineIndex = 0;
|
|
2860
|
+
let endLineIndex = totalLines;
|
|
2861
|
+
const getLineSpanBytes = (lineIndex) => {
|
|
2862
|
+
const start = index.lineStartOffsets[lineIndex] ?? index.size;
|
|
2863
|
+
const end = lineIndex + 1 < totalLines ? (index.lineStartOffsets[lineIndex + 1] ?? index.size) : index.size;
|
|
2864
|
+
return Math.max(0, end - start);
|
|
2865
|
+
};
|
|
2866
|
+
if (beforeRowId !== null) {
|
|
2867
|
+
endLineIndex = Math.max(0, Math.min(totalLines, beforeRowId - 1));
|
|
2868
|
+
startLineIndex = endLineIndex;
|
|
2869
|
+
let collectedRows = 0;
|
|
2870
|
+
let collectedSelectionBytes = 0;
|
|
2871
|
+
let collectedReadBytes = 0;
|
|
2872
|
+
while (startLineIndex > 0 && collectedRows < maxRawRows) {
|
|
2873
|
+
const nextIndex = startLineIndex - 1;
|
|
2874
|
+
const nextReadBytes = getLineSpanBytes(nextIndex);
|
|
2875
|
+
const nextSelectionBytes = Math.min(nextReadBytes, maxLineSelectionBytes);
|
|
2876
|
+
if (collectedRows > 0 && collectedSelectionBytes + nextSelectionBytes > maxSelectionBytes) {
|
|
2877
|
+
break;
|
|
2878
|
+
}
|
|
2879
|
+
if (collectedRows > 0 && collectedReadBytes + nextReadBytes > maxReadBytes) {
|
|
2880
|
+
break;
|
|
2881
|
+
}
|
|
2882
|
+
startLineIndex = nextIndex;
|
|
2883
|
+
collectedRows += 1;
|
|
2884
|
+
collectedSelectionBytes += nextSelectionBytes;
|
|
2885
|
+
collectedReadBytes += nextReadBytes;
|
|
2886
|
+
}
|
|
869
2887
|
}
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
2888
|
+
else if (sinceLine > 0) {
|
|
2889
|
+
startLineIndex = Math.min(totalLines, sinceLine);
|
|
2890
|
+
endLineIndex = startLineIndex;
|
|
2891
|
+
let collectedRows = 0;
|
|
2892
|
+
let collectedSelectionBytes = 0;
|
|
2893
|
+
let collectedReadBytes = 0;
|
|
2894
|
+
while (endLineIndex < totalLines && collectedRows < maxRawRows) {
|
|
2895
|
+
const nextReadBytes = getLineSpanBytes(endLineIndex);
|
|
2896
|
+
const nextSelectionBytes = Math.min(nextReadBytes, maxLineSelectionBytes);
|
|
2897
|
+
if (collectedRows > 0 && collectedSelectionBytes + nextSelectionBytes > maxSelectionBytes) {
|
|
2898
|
+
break;
|
|
2899
|
+
}
|
|
2900
|
+
if (collectedRows > 0 && collectedReadBytes + nextReadBytes > maxReadBytes) {
|
|
2901
|
+
break;
|
|
2902
|
+
}
|
|
2903
|
+
endLineIndex += 1;
|
|
2904
|
+
collectedRows += 1;
|
|
2905
|
+
collectedSelectionBytes += nextSelectionBytes;
|
|
2906
|
+
collectedReadBytes += nextReadBytes;
|
|
873
2907
|
}
|
|
874
|
-
return path.relative(root, target).split(path.sep).join("/") || ".";
|
|
875
|
-
};
|
|
876
|
-
return { abs, formatPath };
|
|
877
|
-
}
|
|
878
|
-
function parseFsRpcAction(value) {
|
|
879
|
-
if (value === "list" || value === "stat" || value === "fetch_file" || value === "read_text") {
|
|
880
|
-
return value;
|
|
881
2908
|
}
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
2909
|
+
else {
|
|
2910
|
+
startLineIndex = totalLines;
|
|
2911
|
+
let collectedRows = 0;
|
|
2912
|
+
let collectedSelectionBytes = 0;
|
|
2913
|
+
let collectedReadBytes = 0;
|
|
2914
|
+
while (startLineIndex > 0 && collectedRows < maxRawRows) {
|
|
2915
|
+
const nextIndex = startLineIndex - 1;
|
|
2916
|
+
const nextReadBytes = getLineSpanBytes(nextIndex);
|
|
2917
|
+
const nextSelectionBytes = Math.min(nextReadBytes, maxLineSelectionBytes);
|
|
2918
|
+
if (collectedRows > 0 && collectedSelectionBytes + nextSelectionBytes > maxSelectionBytes) {
|
|
2919
|
+
break;
|
|
2920
|
+
}
|
|
2921
|
+
if (collectedRows > 0 && collectedReadBytes + nextReadBytes > maxReadBytes) {
|
|
2922
|
+
break;
|
|
2923
|
+
}
|
|
2924
|
+
startLineIndex = nextIndex;
|
|
2925
|
+
collectedRows += 1;
|
|
2926
|
+
collectedSelectionBytes += nextSelectionBytes;
|
|
2927
|
+
collectedReadBytes += nextReadBytes;
|
|
2928
|
+
}
|
|
888
2929
|
}
|
|
889
|
-
|
|
890
|
-
}
|
|
891
|
-
async function executeFsRpc(args) {
|
|
892
|
-
const action = parseFsRpcAction(args.request.action);
|
|
893
|
-
const { abs, formatPath } = normalizeFsRpcPath(args.request.path);
|
|
894
|
-
if (action === "stat") {
|
|
895
|
-
const entry = await stat(abs);
|
|
2930
|
+
if (startLineIndex >= endLineIndex) {
|
|
896
2931
|
return {
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
path: formatPath(abs),
|
|
900
|
-
kind: entry.isDirectory() ? "dir" : "file",
|
|
901
|
-
size: entry.size,
|
|
902
|
-
mtimeMs: entry.mtimeMs,
|
|
2932
|
+
rawRows: [],
|
|
2933
|
+
nextCursor: endLineIndex,
|
|
903
2934
|
};
|
|
904
2935
|
}
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
throw new Error("path is not a directory");
|
|
909
|
-
}
|
|
910
|
-
const limit = Math.max(1, Math.min(normalizeFsRpcNumber(args.request.limit, 200), 1000));
|
|
911
|
-
const rows = await readdir(abs, { withFileTypes: true });
|
|
912
|
-
const items = await Promise.all(rows.map(async (row) => {
|
|
913
|
-
const child = path.join(abs, row.name);
|
|
914
|
-
const childStat = await stat(child);
|
|
915
|
-
return {
|
|
916
|
-
name: row.name,
|
|
917
|
-
path: formatPath(child),
|
|
918
|
-
kind: row.isDirectory() ? "dir" : "file",
|
|
919
|
-
size: childStat.size,
|
|
920
|
-
mtimeMs: childStat.mtimeMs,
|
|
921
|
-
};
|
|
922
|
-
}));
|
|
923
|
-
items.sort((a, b) => (a.kind === b.kind ? a.name.localeCompare(b.name) : a.kind === "dir" ? -1 : 1));
|
|
2936
|
+
const startOffset = index.lineStartOffsets[startLineIndex] ?? index.size;
|
|
2937
|
+
const endOffset = endLineIndex < totalLines ? (index.lineStartOffsets[endLineIndex] ?? index.size) : index.size;
|
|
2938
|
+
if (startOffset >= endOffset) {
|
|
924
2939
|
return {
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
path: formatPath(abs),
|
|
928
|
-
items: items.slice(0, limit),
|
|
929
|
-
truncated: items.length > limit,
|
|
930
|
-
total: items.length,
|
|
2940
|
+
rawRows: [],
|
|
2941
|
+
nextCursor: endLineIndex,
|
|
931
2942
|
};
|
|
932
2943
|
}
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
}
|
|
938
|
-
const
|
|
939
|
-
const
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
throw new Error("missing upload parameters");
|
|
943
|
-
}
|
|
944
|
-
const data = await readFile(abs);
|
|
945
|
-
const fileName = path.basename(abs) || "file";
|
|
946
|
-
const form = new FormData();
|
|
947
|
-
form.append("file", new File([data], fileName));
|
|
948
|
-
form.append("chatId", chatId);
|
|
949
|
-
form.append("agentId", agentId);
|
|
950
|
-
const response = await fetch(uploadUrl, {
|
|
951
|
-
method: "POST",
|
|
952
|
-
headers: { Authorization: `Bearer ${args.agentToken}` },
|
|
953
|
-
body: form,
|
|
954
|
-
});
|
|
955
|
-
const text = await response.text();
|
|
956
|
-
let upload = {};
|
|
957
|
-
try {
|
|
958
|
-
upload = JSON.parse(text || "{}");
|
|
959
|
-
}
|
|
960
|
-
catch {
|
|
961
|
-
upload = {};
|
|
2944
|
+
const fileHandle = await open(resolvedFile, "r");
|
|
2945
|
+
try {
|
|
2946
|
+
const readSize = endOffset - startOffset;
|
|
2947
|
+
const buffer = Buffer.alloc(readSize);
|
|
2948
|
+
const { bytesRead } = await fileHandle.read(buffer, 0, readSize, startOffset);
|
|
2949
|
+
const raw = buffer.toString("utf8", 0, bytesRead);
|
|
2950
|
+
const lines = raw.split(/\r?\n/);
|
|
2951
|
+
if (lines.length > 0 && lines[lines.length - 1] === "") {
|
|
2952
|
+
lines.pop();
|
|
962
2953
|
}
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
2954
|
+
const rawRows = [];
|
|
2955
|
+
let lineNumber = startLineIndex + 1;
|
|
2956
|
+
for (const line of lines) {
|
|
2957
|
+
if (line.trim()) {
|
|
2958
|
+
rawRows.push({
|
|
2959
|
+
id: lineNumber,
|
|
2960
|
+
raw: sanitizeSessionRpcRawLine(line),
|
|
2961
|
+
});
|
|
2962
|
+
}
|
|
2963
|
+
lineNumber += 1;
|
|
966
2964
|
}
|
|
967
2965
|
return {
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
path: formatPath(abs),
|
|
971
|
-
size: entry.size,
|
|
972
|
-
upload,
|
|
2966
|
+
rawRows,
|
|
2967
|
+
nextCursor: endLineIndex,
|
|
973
2968
|
};
|
|
974
2969
|
}
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
throw new Error("path is not a file");
|
|
2970
|
+
finally {
|
|
2971
|
+
await fileHandle.close().catch(() => undefined);
|
|
978
2972
|
}
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
const
|
|
982
|
-
const
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
2973
|
+
}
|
|
2974
|
+
function resolveSessionUploadsDir(sessionId) {
|
|
2975
|
+
const workspaceRoot = workspaceRootOverride ?? (process.env.WORKSPACE?.trim() || process.cwd());
|
|
2976
|
+
const safeSessionId = sessionId.trim().replace(/[^a-zA-Z0-9._-]/g, "_").slice(0, 160) || "session";
|
|
2977
|
+
return path.join(workspaceRoot, ".doer-agent", "sessions", safeSessionId);
|
|
2978
|
+
}
|
|
2979
|
+
async function deleteAgentSession(filePath, sessionId) {
|
|
2980
|
+
const resolvedFile = resolveSessionFilePath(filePath);
|
|
2981
|
+
sessionLineIndexCache.delete(resolvedFile);
|
|
2982
|
+
await unlink(resolvedFile);
|
|
2983
|
+
if (sessionId) {
|
|
2984
|
+
await rm(resolveSessionUploadsDir(sessionId), { recursive: true, force: true }).catch(() => undefined);
|
|
2985
|
+
}
|
|
2986
|
+
const sessionsRoot = path.resolve(getSessionsRootPath());
|
|
2987
|
+
let currentDir = path.dirname(resolvedFile);
|
|
2988
|
+
while (currentDir.startsWith(sessionsRoot + path.sep)) {
|
|
987
2989
|
try {
|
|
988
|
-
const
|
|
989
|
-
|
|
2990
|
+
const entries = await readdir(currentDir);
|
|
2991
|
+
if (entries.length > 0) {
|
|
2992
|
+
break;
|
|
2993
|
+
}
|
|
2994
|
+
await rmdir(currentDir);
|
|
2995
|
+
}
|
|
2996
|
+
catch {
|
|
2997
|
+
break;
|
|
2998
|
+
}
|
|
2999
|
+
currentDir = path.dirname(currentDir);
|
|
3000
|
+
}
|
|
3001
|
+
}
|
|
3002
|
+
function publishSessionRpcResponse(args) {
|
|
3003
|
+
args.nc.publish(args.responseSubject, sessionRpcCodec.encode(JSON.stringify(args.payload)));
|
|
3004
|
+
}
|
|
3005
|
+
async function startSessionWatch(args) {
|
|
3006
|
+
const resolvedFile = resolveSessionFilePath(args.filePath);
|
|
3007
|
+
const watchId = `watch_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 10)}`;
|
|
3008
|
+
let watcher = null;
|
|
3009
|
+
let active = true;
|
|
3010
|
+
const emitEvent = (event) => {
|
|
3011
|
+
if (!active) {
|
|
3012
|
+
return;
|
|
3013
|
+
}
|
|
3014
|
+
publishSessionRpcResponse({
|
|
3015
|
+
nc: args.nc,
|
|
3016
|
+
responseSubject: args.responseSubject,
|
|
3017
|
+
payload: {
|
|
3018
|
+
requestId: args.requestId,
|
|
990
3019
|
ok: true,
|
|
991
|
-
action,
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
};
|
|
3020
|
+
action: "watch",
|
|
3021
|
+
watchId,
|
|
3022
|
+
event,
|
|
3023
|
+
},
|
|
3024
|
+
});
|
|
3025
|
+
};
|
|
3026
|
+
const cleanup = () => {
|
|
3027
|
+
if (!active) {
|
|
3028
|
+
return;
|
|
1001
3029
|
}
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
3030
|
+
active = false;
|
|
3031
|
+
watcher?.close();
|
|
3032
|
+
watcher = null;
|
|
3033
|
+
activeSessionWatchers.delete(watchId);
|
|
3034
|
+
};
|
|
3035
|
+
const notifyFromContent = () => {
|
|
3036
|
+
emitEvent({
|
|
3037
|
+
type: "messages.changed",
|
|
3038
|
+
at: formatLocalTimestamp(),
|
|
3039
|
+
});
|
|
3040
|
+
};
|
|
3041
|
+
watcher = watch(resolvedFile, () => {
|
|
3042
|
+
notifyFromContent();
|
|
3043
|
+
});
|
|
3044
|
+
activeSessionWatchers.set(watchId, cleanup);
|
|
3045
|
+
emitEvent({ type: "stream.started", watchId, at: formatLocalTimestamp() });
|
|
3046
|
+
return watchId;
|
|
3047
|
+
}
|
|
3048
|
+
async function handleSessionRpcMessage(args) {
|
|
3049
|
+
let requestId = "unknown";
|
|
3050
|
+
let responseSubject = "";
|
|
3051
|
+
try {
|
|
3052
|
+
const payload = JSON.parse(sessionRpcCodec.decode(args.msg.data));
|
|
3053
|
+
const request = normalizeSessionRpcRequest({ request: payload, agentId: args.agentId });
|
|
3054
|
+
requestId = request.requestId;
|
|
3055
|
+
responseSubject = request.responseSubject;
|
|
3056
|
+
if (request.action === "list") {
|
|
3057
|
+
const sessions = await listAgentSessions();
|
|
3058
|
+
publishSessionRpcResponse({
|
|
3059
|
+
nc: args.jetstream.nc,
|
|
3060
|
+
responseSubject,
|
|
3061
|
+
payload: { requestId, ok: true, action: "list", sessions },
|
|
3062
|
+
});
|
|
3063
|
+
return;
|
|
1010
3064
|
}
|
|
3065
|
+
if (request.action === "messages") {
|
|
3066
|
+
const result = await getAgentSessionRawRows({
|
|
3067
|
+
filePath: request.filePath ?? "",
|
|
3068
|
+
sinceLine: request.sinceLine,
|
|
3069
|
+
beforeRowId: request.beforeRowId,
|
|
3070
|
+
pageSize: request.pageSize,
|
|
3071
|
+
});
|
|
3072
|
+
publishSessionRpcResponse({
|
|
3073
|
+
nc: args.jetstream.nc,
|
|
3074
|
+
responseSubject,
|
|
3075
|
+
payload: { requestId, ok: true, action: "messages", rawRows: result.rawRows, nextCursor: result.nextCursor },
|
|
3076
|
+
});
|
|
3077
|
+
return;
|
|
3078
|
+
}
|
|
3079
|
+
if (request.action === "delete") {
|
|
3080
|
+
await deleteAgentSession(request.filePath ?? "", request.sessionId);
|
|
3081
|
+
publishSessionRpcResponse({
|
|
3082
|
+
nc: args.jetstream.nc,
|
|
3083
|
+
responseSubject,
|
|
3084
|
+
payload: { requestId, ok: true, action: "delete" },
|
|
3085
|
+
});
|
|
3086
|
+
return;
|
|
3087
|
+
}
|
|
3088
|
+
if (request.action === "watch") {
|
|
3089
|
+
const watchId = await startSessionWatch({
|
|
3090
|
+
nc: args.jetstream.nc,
|
|
3091
|
+
requestId,
|
|
3092
|
+
responseSubject,
|
|
3093
|
+
filePath: request.filePath ?? "",
|
|
3094
|
+
});
|
|
3095
|
+
publishSessionRpcResponse({
|
|
3096
|
+
nc: args.jetstream.nc,
|
|
3097
|
+
responseSubject,
|
|
3098
|
+
payload: { requestId, ok: true, action: "watch", watchId },
|
|
3099
|
+
});
|
|
3100
|
+
return;
|
|
3101
|
+
}
|
|
3102
|
+
const stop = request.watchId ? activeSessionWatchers.get(request.watchId) : null;
|
|
3103
|
+
stop?.();
|
|
3104
|
+
publishSessionRpcResponse({
|
|
3105
|
+
nc: args.jetstream.nc,
|
|
3106
|
+
responseSubject,
|
|
3107
|
+
payload: { requestId, ok: true, action: "stop_watch", watchId: request.watchId },
|
|
3108
|
+
});
|
|
1011
3109
|
}
|
|
1012
|
-
|
|
1013
|
-
|
|
3110
|
+
catch (error) {
|
|
3111
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
3112
|
+
if (responseSubject) {
|
|
3113
|
+
publishSessionRpcResponse({
|
|
3114
|
+
nc: args.jetstream.nc,
|
|
3115
|
+
responseSubject,
|
|
3116
|
+
payload: {
|
|
3117
|
+
requestId,
|
|
3118
|
+
ok: false,
|
|
3119
|
+
error: message,
|
|
3120
|
+
},
|
|
3121
|
+
});
|
|
3122
|
+
}
|
|
3123
|
+
writeAgentError(`session rpc failed requestId=${requestId} error=${message}`);
|
|
1014
3124
|
}
|
|
1015
3125
|
}
|
|
3126
|
+
function subscribeToSessionRpc(args) {
|
|
3127
|
+
const subject = buildAgentSessionRpcSubject(args.userId, args.agentId);
|
|
3128
|
+
args.jetstream.nc.subscribe(subject, {
|
|
3129
|
+
callback: (error, msg) => {
|
|
3130
|
+
if (error) {
|
|
3131
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
3132
|
+
writeAgentError(`session rpc subscription error: ${message}`);
|
|
3133
|
+
return;
|
|
3134
|
+
}
|
|
3135
|
+
void handleSessionRpcMessage({
|
|
3136
|
+
msg,
|
|
3137
|
+
jetstream: args.jetstream,
|
|
3138
|
+
agentId: args.agentId,
|
|
3139
|
+
});
|
|
3140
|
+
},
|
|
3141
|
+
});
|
|
3142
|
+
writeAgentInfo(`session rpc subscribed subject=${subject}`);
|
|
3143
|
+
}
|
|
1016
3144
|
async function handleFsRpcMessage(args) {
|
|
1017
3145
|
let payload = {};
|
|
1018
3146
|
try {
|
|
@@ -1020,7 +3148,11 @@ async function handleFsRpcMessage(args) {
|
|
|
1020
3148
|
if (typeof payload.agentId === "string" && payload.agentId.trim() && payload.agentId !== args.agentId) {
|
|
1021
3149
|
throw new Error("agent id mismatch");
|
|
1022
3150
|
}
|
|
1023
|
-
const result = await executeFsRpc({
|
|
3151
|
+
const result = await executeFsRpc({
|
|
3152
|
+
request: payload,
|
|
3153
|
+
serverBaseUrl: args.serverBaseUrl,
|
|
3154
|
+
agentToken: args.agentToken,
|
|
3155
|
+
});
|
|
1024
3156
|
args.msg.respond(fsRpcCodec.encode(JSON.stringify(result)));
|
|
1025
3157
|
}
|
|
1026
3158
|
catch (error) {
|
|
@@ -1130,7 +3262,8 @@ async function handleShellRpcMessage(args) {
|
|
|
1130
3262
|
const startedAtMs = Date.now();
|
|
1131
3263
|
const prepared = await prepareCommandExecution({
|
|
1132
3264
|
cwd: request.cwd,
|
|
1133
|
-
|
|
3265
|
+
userId: args.userId,
|
|
3266
|
+
taskId: request.requestId,
|
|
1134
3267
|
codexAuthBundle: request.codexAuthBundle,
|
|
1135
3268
|
});
|
|
1136
3269
|
const child = spawnPreparedCommand({
|
|
@@ -1217,6 +3350,7 @@ function subscribeToShellRpc(args) {
|
|
|
1217
3350
|
void handleShellRpcMessage({
|
|
1218
3351
|
msg,
|
|
1219
3352
|
jetstream: args.jetstream,
|
|
3353
|
+
userId: args.userId,
|
|
1220
3354
|
agentId: args.agentId,
|
|
1221
3355
|
agentToken: args.agentToken,
|
|
1222
3356
|
});
|
|
@@ -1387,49 +3521,39 @@ async function checkCancelRequested(args) {
|
|
|
1387
3521
|
return Boolean(response.task?.cancelRequested);
|
|
1388
3522
|
}
|
|
1389
3523
|
async function prepareTaskCodexAuth(args) {
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
3524
|
+
void args;
|
|
3525
|
+
return {
|
|
3526
|
+
envPatch: {},
|
|
3527
|
+
cleanup: async () => { },
|
|
3528
|
+
meta: {
|
|
3529
|
+
codexAuthSource: "agent_local",
|
|
3530
|
+
codexAuthSynced: false,
|
|
3531
|
+
},
|
|
3532
|
+
};
|
|
1399
3533
|
}
|
|
1400
3534
|
async function prepareCodexAuthBundle(bundle) {
|
|
1401
|
-
|
|
1402
|
-
return null;
|
|
1403
|
-
}
|
|
1404
|
-
const codexHome = resolveCodexHomePath();
|
|
1405
|
-
await mkdir(codexHome, { recursive: true });
|
|
1406
|
-
const authFile = path.join(codexHome, "auth.json");
|
|
1407
|
-
await writeFile(authFile, bundle.authJson, "utf8");
|
|
1408
|
-
await chmod(authFile, 0o600).catch(() => undefined);
|
|
1409
|
-
const envPatch = {
|
|
1410
|
-
CODEX_HOME: codexHome,
|
|
1411
|
-
};
|
|
1412
|
-
if (typeof bundle.apiKey === "string" && bundle.apiKey.trim()) {
|
|
1413
|
-
envPatch.OPENAI_API_KEY = bundle.apiKey.trim();
|
|
1414
|
-
}
|
|
1415
|
-
const cleanup = async () => { };
|
|
3535
|
+
void bundle;
|
|
1416
3536
|
return {
|
|
1417
|
-
envPatch,
|
|
1418
|
-
cleanup,
|
|
3537
|
+
envPatch: {},
|
|
3538
|
+
cleanup: async () => { },
|
|
1419
3539
|
meta: {
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
codexAuthExpiresAt: bundle.expiresAt ?? null,
|
|
1423
|
-
codexAuthSynced: true,
|
|
3540
|
+
codexAuthSource: "agent_local",
|
|
3541
|
+
codexAuthSynced: false,
|
|
1424
3542
|
},
|
|
1425
3543
|
};
|
|
1426
3544
|
}
|
|
1427
3545
|
async function prepareCommandExecution(args) {
|
|
1428
3546
|
const shellPath = resolveShellPath();
|
|
1429
3547
|
const taskWorkspace = resolveTaskWorkspace(args.cwd);
|
|
3548
|
+
const codexHome = resolveCodexHomePath();
|
|
3549
|
+
await mkdir(codexHome, { recursive: true });
|
|
1430
3550
|
const codexAuth = await prepareCodexAuthBundle(args.codexAuthBundle);
|
|
3551
|
+
const localAgentSettings = await readAgentSettingsConfig(null);
|
|
1431
3552
|
const baseTaskEnvPatch = {
|
|
1432
|
-
|
|
3553
|
+
CODEX_HOME: codexHome,
|
|
3554
|
+
DOER_USER_ID: args.userId,
|
|
3555
|
+
DOER_AGENT_TASK_ID: args.taskId,
|
|
3556
|
+
...buildAgentSettingsEnvPatch(localAgentSettings),
|
|
1433
3557
|
...(codexAuth?.envPatch ?? {}),
|
|
1434
3558
|
WORKSPACE: taskWorkspace,
|
|
1435
3559
|
};
|
|
@@ -1520,6 +3644,8 @@ async function runTask(args) {
|
|
|
1520
3644
|
};
|
|
1521
3645
|
const shellPath = resolveShellPath();
|
|
1522
3646
|
const taskWorkspace = resolveTaskWorkspace(args.cwd);
|
|
3647
|
+
const codexHome = resolveCodexHomePath();
|
|
3648
|
+
await mkdir(codexHome, { recursive: true });
|
|
1523
3649
|
const runtimeConfig = await prepareTaskRuntimeConfig({
|
|
1524
3650
|
serverBaseUrl: args.serverBaseUrl,
|
|
1525
3651
|
taskId: args.taskId,
|
|
@@ -1532,7 +3658,10 @@ async function runTask(args) {
|
|
|
1532
3658
|
userId: args.userId,
|
|
1533
3659
|
agentToken: args.agentToken,
|
|
1534
3660
|
});
|
|
3661
|
+
const localAgentSettings = await readAgentSettingsConfig(null);
|
|
1535
3662
|
const baseTaskEnvPatch = {
|
|
3663
|
+
CODEX_HOME: codexHome,
|
|
3664
|
+
...buildAgentSettingsEnvPatch(localAgentSettings),
|
|
1536
3665
|
...(runtimeConfig?.envPatch ?? {}),
|
|
1537
3666
|
...(codexAuth?.envPatch ?? {}),
|
|
1538
3667
|
WORKSPACE: taskWorkspace,
|
|
@@ -1735,6 +3864,10 @@ async function main() {
|
|
|
1735
3864
|
const startupWorkspaceRoot = path.resolve(workspaceDir || process.cwd());
|
|
1736
3865
|
workspaceRootOverride = startupWorkspaceRoot;
|
|
1737
3866
|
process.chdir(startupWorkspaceRoot);
|
|
3867
|
+
process.env.WORKSPACE = startupWorkspaceRoot;
|
|
3868
|
+
process.env.CODEX_HOME = path.join(startupWorkspaceRoot, ".codex");
|
|
3869
|
+
await mkdir(process.env.CODEX_HOME, { recursive: true }).catch(() => undefined);
|
|
3870
|
+
await resetRunsDir();
|
|
1738
3871
|
const serverBaseUrlRaw = resolveArgOrEnv(args, ["server", "url"], ["DOER_AGENT_SERVER"], DEFAULT_SERVER_BASE_URL);
|
|
1739
3872
|
const requestedServerBaseUrl = serverBaseUrlRaw.replace(/\/$/, "");
|
|
1740
3873
|
const serverBaseUrl = resolveContainerReachableServerBaseUrl(requestedServerBaseUrl);
|
|
@@ -1800,6 +3933,26 @@ async function main() {
|
|
|
1800
3933
|
agentId: initialAgentId,
|
|
1801
3934
|
agentToken,
|
|
1802
3935
|
});
|
|
3936
|
+
subscribeToSessionRpc({
|
|
3937
|
+
jetstream,
|
|
3938
|
+
userId,
|
|
3939
|
+
agentId: initialAgentId,
|
|
3940
|
+
});
|
|
3941
|
+
subscribeToCodexAuthRpc({
|
|
3942
|
+
jetstream,
|
|
3943
|
+
userId,
|
|
3944
|
+
agentId: initialAgentId,
|
|
3945
|
+
});
|
|
3946
|
+
subscribeToSettingsRpc({
|
|
3947
|
+
jetstream,
|
|
3948
|
+
userId,
|
|
3949
|
+
agentId: initialAgentId,
|
|
3950
|
+
});
|
|
3951
|
+
subscribeToGitRpc({
|
|
3952
|
+
jetstream,
|
|
3953
|
+
userId,
|
|
3954
|
+
agentId: initialAgentId,
|
|
3955
|
+
});
|
|
1803
3956
|
subscribeToRunRpc({
|
|
1804
3957
|
jetstream,
|
|
1805
3958
|
serverBaseUrl,
|