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