doer-agent 0.2.3 → 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 +2472 -408
- 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,452 +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
1023
|
writeRunStatus(task.id, `completed status=${task.status} exitCode=${task.resultExitCode ?? "null"} signal=${task.resultSignal ?? "null"}`);
|
|
560
1024
|
});
|
|
561
|
-
activeRuns.set(task.id, { task, child,
|
|
1025
|
+
activeRuns.set(task.id, { task, child, requestCancel });
|
|
562
1026
|
persistRetainedRun(task);
|
|
1027
|
+
void persistRunTask(task).catch(() => undefined);
|
|
563
1028
|
writeRunStatus(task.id, `started requestId=${args.requestId} cwd=${prepared.taskWorkspace}`);
|
|
564
1029
|
return cloneRunTask(task);
|
|
565
1030
|
}
|
|
566
|
-
|
|
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");
|
|
1049
|
+
}
|
|
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
|
+
});
|
|
1141
|
+
});
|
|
1142
|
+
}
|
|
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) {
|
|
567
1202
|
let requestId = "unknown";
|
|
568
1203
|
let responseSubject = "";
|
|
569
1204
|
try {
|
|
570
|
-
const payload = JSON.parse(
|
|
571
|
-
const request =
|
|
1205
|
+
const payload = JSON.parse(settingsRpcCodec.decode(args.msg.data));
|
|
1206
|
+
const request = normalizeSettingsRpcRequest({ request: payload, agentId: args.agentId });
|
|
572
1207
|
requestId = request.requestId;
|
|
573
1208
|
responseSubject = request.responseSubject;
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
userId: args.userId,
|
|
579
|
-
agentId: args.agentId,
|
|
580
|
-
command: request.command ?? "",
|
|
581
|
-
cwd: request.cwd,
|
|
582
|
-
chatId: request.chatId,
|
|
583
|
-
runtimeEnvPatch: request.runtimeEnvPatch,
|
|
584
|
-
codexAuthBundle: request.codexAuthBundle,
|
|
585
|
-
agentToken: args.agentToken,
|
|
586
|
-
});
|
|
587
|
-
publishRunRpcResponse({ nc: args.jetstream.nc, responseSubject, payload: { requestId, ok: true, task } });
|
|
588
|
-
return;
|
|
589
|
-
}
|
|
590
|
-
if (request.action === "list") {
|
|
591
|
-
const tasks = [...activeRuns.values()].map((entry) => cloneRunTask(entry.task));
|
|
592
|
-
const retained = [...retainedRuns.values()].filter((task) => !activeRuns.has(task.id)).map((task) => cloneRunTask(task));
|
|
593
|
-
const merged = [...tasks, ...retained]
|
|
594
|
-
.sort((a, b) => Date.parse(b.updatedAt) - Date.parse(a.updatedAt))
|
|
595
|
-
.slice(0, request.limit);
|
|
596
|
-
publishRunRpcResponse({ nc: args.jetstream.nc, responseSubject, payload: { requestId, ok: true, tasks: merged } });
|
|
597
|
-
return;
|
|
598
|
-
}
|
|
599
|
-
const stored = request.runId ? getStoredRun(request.runId) : null;
|
|
600
|
-
if (!stored || stored.agentId !== args.agentId || stored.userId !== args.userId) {
|
|
601
|
-
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);
|
|
602
1213
|
}
|
|
603
|
-
if (request.
|
|
604
|
-
const
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
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
|
+
}
|
|
609
1220
|
}
|
|
610
|
-
|
|
611
|
-
|
|
1221
|
+
publishSettingsRpcResponse({
|
|
1222
|
+
nc: args.jetstream.nc,
|
|
1223
|
+
responseSubject,
|
|
1224
|
+
payload: {
|
|
1225
|
+
requestId,
|
|
1226
|
+
ok: true,
|
|
1227
|
+
settings: toAgentSettingsPublic(next),
|
|
1228
|
+
},
|
|
1229
|
+
});
|
|
612
1230
|
}
|
|
613
1231
|
catch (error) {
|
|
614
1232
|
const message = error instanceof Error ? error.message : String(error);
|
|
615
1233
|
if (responseSubject) {
|
|
616
|
-
|
|
1234
|
+
publishSettingsRpcResponse({
|
|
617
1235
|
nc: args.jetstream.nc,
|
|
618
1236
|
responseSubject,
|
|
619
1237
|
payload: { requestId, ok: false, error: message },
|
|
620
1238
|
});
|
|
621
1239
|
}
|
|
622
|
-
writeAgentError(`
|
|
1240
|
+
writeAgentError(`settings rpc failed requestId=${requestId} error=${message}`);
|
|
623
1241
|
}
|
|
624
1242
|
}
|
|
625
|
-
function
|
|
626
|
-
const subject =
|
|
1243
|
+
function subscribeToSettingsRpc(args) {
|
|
1244
|
+
const subject = buildAgentSettingsRpcSubject(args.userId, args.agentId);
|
|
627
1245
|
args.jetstream.nc.subscribe(subject, {
|
|
628
1246
|
callback: (error, msg) => {
|
|
629
1247
|
if (error) {
|
|
630
1248
|
const message = error instanceof Error ? error.message : String(error);
|
|
631
|
-
writeAgentError(`
|
|
1249
|
+
writeAgentError(`settings rpc subscription error: ${message}`);
|
|
632
1250
|
return;
|
|
633
1251
|
}
|
|
634
|
-
void
|
|
1252
|
+
void handleSettingsRpcMessage({
|
|
1253
|
+
msg,
|
|
1254
|
+
jetstream: args.jetstream,
|
|
1255
|
+
agentId: args.agentId,
|
|
1256
|
+
});
|
|
635
1257
|
},
|
|
636
1258
|
});
|
|
637
|
-
writeAgentInfo(`
|
|
638
|
-
}
|
|
639
|
-
function isLikelyNatsAuthError(error) {
|
|
640
|
-
const message = (error instanceof Error ? error.message : String(error)).toLowerCase();
|
|
641
|
-
return (message.includes("auth")
|
|
642
|
-
|| message.includes("authorization")
|
|
643
|
-
|| message.includes("authentication")
|
|
644
|
-
|| message.includes("permission")
|
|
645
|
-
|| message.includes("jwt")
|
|
646
|
-
|| message.includes("token"));
|
|
647
|
-
}
|
|
648
|
-
function isLikelyNatsReconnectError(error) {
|
|
649
|
-
const message = (error instanceof Error ? error.message : String(error)).toLowerCase();
|
|
650
|
-
return (message.includes("connection_closed")
|
|
651
|
-
|| message.includes("connection closed")
|
|
652
|
-
|| message.includes("closed connection")
|
|
653
|
-
|| message.includes("disconnected")
|
|
654
|
-
|| message.includes("timeout")
|
|
655
|
-
|| message.includes("no responders"));
|
|
1259
|
+
writeAgentInfo(`settings rpc subscribed subject=${subject}`);
|
|
656
1260
|
}
|
|
657
|
-
function
|
|
658
|
-
if (
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
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;
|
|
666
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");
|
|
667
1301
|
}
|
|
668
|
-
|
|
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 {
|
|
669
1975
|
child.kill(signal);
|
|
670
1976
|
}
|
|
671
|
-
catch {
|
|
672
|
-
// noop
|
|
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}`;
|
|
2051
|
+
}
|
|
2052
|
+
catch {
|
|
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");
|
|
2349
|
+
}
|
|
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
|
+
};
|
|
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");
|
|
2369
|
+
try {
|
|
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
|
+
}
|
|
2397
|
+
}
|
|
2398
|
+
finally {
|
|
2399
|
+
await fd.close();
|
|
673
2400
|
}
|
|
674
2401
|
}
|
|
675
|
-
function
|
|
676
|
-
const
|
|
677
|
-
if (!
|
|
678
|
-
|
|
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");
|
|
679
2406
|
}
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
return true;
|
|
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");
|
|
684
2410
|
}
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
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");
|
|
689
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
|
+
};
|
|
690
2445
|
}
|
|
691
|
-
function
|
|
692
|
-
const
|
|
693
|
-
return
|
|
2446
|
+
function getSessionsRootPath() {
|
|
2447
|
+
const workspaceRoot = workspaceRootOverride ?? (process.env.WORKSPACE?.trim() || process.cwd());
|
|
2448
|
+
return path.join(workspaceRoot, ".codex", "sessions");
|
|
694
2449
|
}
|
|
695
|
-
function
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
hour: "2-digit",
|
|
701
|
-
minute: "2-digit",
|
|
702
|
-
hour12: false,
|
|
703
|
-
}).formatToParts(date);
|
|
704
|
-
const token = parts.find((part) => part.type === "timeZoneName")?.value || "GMT+0";
|
|
705
|
-
const matched = token.match(/GMT([+-]\d{1,2})(?::?(\d{2}))?/i);
|
|
706
|
-
if (!matched) {
|
|
707
|
-
return "+00:00";
|
|
708
|
-
}
|
|
709
|
-
const hourRaw = matched[1] || "+0";
|
|
710
|
-
const minuteRaw = matched[2] || "00";
|
|
711
|
-
const sign = hourRaw.startsWith("-") ? "-" : "+";
|
|
712
|
-
const absHour = String(Math.abs(Number.parseInt(hourRaw, 10))).padStart(2, "0");
|
|
713
|
-
const absMinute = String(Math.abs(Number.parseInt(minuteRaw, 10))).padStart(2, "0");
|
|
714
|
-
return `${sign}${absHour}:${absMinute}`;
|
|
715
|
-
}
|
|
716
|
-
catch {
|
|
717
|
-
return "+00:00";
|
|
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");
|
|
718
2455
|
}
|
|
2456
|
+
return resolved;
|
|
719
2457
|
}
|
|
720
|
-
function
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
month: "2-digit",
|
|
727
|
-
day: "2-digit",
|
|
728
|
-
hour: "2-digit",
|
|
729
|
-
minute: "2-digit",
|
|
730
|
-
second: "2-digit",
|
|
731
|
-
hour12: false,
|
|
732
|
-
}).formatToParts(date);
|
|
733
|
-
const pick = (type) => {
|
|
734
|
-
return parts.find((part) => part.type === type)?.value || "00";
|
|
735
|
-
};
|
|
736
|
-
const year = pick("year");
|
|
737
|
-
const month = pick("month");
|
|
738
|
-
const day = pick("day");
|
|
739
|
-
const hours = pick("hour");
|
|
740
|
-
const minutes = pick("minute");
|
|
741
|
-
const seconds = pick("second");
|
|
742
|
-
const ms = String(date.getMilliseconds()).padStart(3, "0");
|
|
743
|
-
const offset = resolveTimeZoneOffsetString(date, timeZone);
|
|
744
|
-
return `${year}-${month}-${day}T${hours}:${minutes}:${seconds}.${ms}${offset}`;
|
|
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;
|
|
745
2464
|
}
|
|
746
|
-
|
|
747
|
-
|
|
2465
|
+
const trimmed = value.trim();
|
|
2466
|
+
return trimmed || null;
|
|
2467
|
+
}
|
|
2468
|
+
function pickSessionString(...values) {
|
|
2469
|
+
for (const value of values) {
|
|
2470
|
+
const picked = toTrimmedStringOrNull(value);
|
|
2471
|
+
if (picked) {
|
|
2472
|
+
return picked;
|
|
2473
|
+
}
|
|
748
2474
|
}
|
|
2475
|
+
return null;
|
|
749
2476
|
}
|
|
750
|
-
function
|
|
751
|
-
const out =
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
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) {
|
|
755
2483
|
continue;
|
|
756
2484
|
}
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
2485
|
+
let entries = [];
|
|
2486
|
+
try {
|
|
2487
|
+
entries = await readdir(current, { withFileTypes: true });
|
|
2488
|
+
}
|
|
2489
|
+
catch {
|
|
761
2490
|
continue;
|
|
762
2491
|
}
|
|
763
|
-
|
|
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
|
+
}
|
|
764
2509
|
}
|
|
765
2510
|
return out;
|
|
766
2511
|
}
|
|
767
|
-
function
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
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();
|
|
772
2531
|
}
|
|
773
2532
|
}
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
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
|
|
778
2565
|
}
|
|
779
2566
|
}
|
|
780
2567
|
return fallback;
|
|
781
2568
|
}
|
|
782
|
-
function
|
|
783
|
-
|
|
784
|
-
|
|
2569
|
+
async function readLastAgentMessage(fileHandle, fileSize) {
|
|
2570
|
+
const chunkBytes = 16_384;
|
|
2571
|
+
const maxScanBytes = 131_072;
|
|
2572
|
+
if (fileSize <= 0) {
|
|
2573
|
+
return null;
|
|
785
2574
|
}
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
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;
|
|
790
2593
|
}
|
|
791
2594
|
}
|
|
792
|
-
|
|
2595
|
+
return extractLastAgentMessage([carry]);
|
|
793
2596
|
}
|
|
794
|
-
function
|
|
795
|
-
const
|
|
796
|
-
const
|
|
797
|
-
const
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
:
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
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;
|
|
806
2613
|
try {
|
|
807
|
-
|
|
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
|
+
};
|
|
808
2644
|
}
|
|
809
|
-
catch
|
|
810
|
-
|
|
811
|
-
throw new Error(`Invalid cwd: ${requestedCwd || "(empty)"} resolved to ${resolvedCwd} (${message})`);
|
|
2645
|
+
catch {
|
|
2646
|
+
return normalizeSessionMeta({}, filePath, mtimeMs);
|
|
812
2647
|
}
|
|
813
|
-
|
|
814
|
-
|
|
2648
|
+
finally {
|
|
2649
|
+
await fileHandle?.close().catch(() => undefined);
|
|
815
2650
|
}
|
|
816
|
-
return resolvedCwd;
|
|
817
|
-
}
|
|
818
|
-
function buildAgentFsRpcSubject(userId, agentId) {
|
|
819
|
-
return `doer.agent.fs.rpc.${sanitizeUserId(userId)}.${agentId.trim()}`;
|
|
820
2651
|
}
|
|
821
|
-
function
|
|
822
|
-
|
|
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));
|
|
823
2668
|
}
|
|
824
|
-
function
|
|
825
|
-
const
|
|
826
|
-
const
|
|
827
|
-
const
|
|
828
|
-
const
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
if (!useAbsolute && abs !== root && !abs.startsWith(root + path.sep)) {
|
|
832
|
-
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;
|
|
833
2676
|
}
|
|
834
|
-
const
|
|
835
|
-
|
|
836
|
-
|
|
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;
|
|
837
2780
|
}
|
|
838
|
-
return path.relative(root, target).split(path.sep).join("/") || ".";
|
|
839
|
-
};
|
|
840
|
-
return { abs, formatPath };
|
|
841
|
-
}
|
|
842
|
-
function parseFsRpcAction(value) {
|
|
843
|
-
if (value === "list" || value === "stat" || value === "fetch_file" || value === "read_text") {
|
|
844
|
-
return value;
|
|
845
2781
|
}
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
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
|
+
}
|
|
852
2796
|
}
|
|
853
|
-
|
|
854
|
-
}
|
|
855
|
-
async function executeFsRpc(args) {
|
|
856
|
-
const action = parseFsRpcAction(args.request.action);
|
|
857
|
-
const { abs, formatPath } = normalizeFsRpcPath(args.request.path);
|
|
858
|
-
if (action === "stat") {
|
|
859
|
-
const entry = await stat(abs);
|
|
2797
|
+
if (startLineIndex >= endLineIndex) {
|
|
860
2798
|
return {
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
path: formatPath(abs),
|
|
864
|
-
kind: entry.isDirectory() ? "dir" : "file",
|
|
865
|
-
size: entry.size,
|
|
866
|
-
mtimeMs: entry.mtimeMs,
|
|
2799
|
+
rawRows: [],
|
|
2800
|
+
nextCursor: endLineIndex,
|
|
867
2801
|
};
|
|
868
2802
|
}
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
throw new Error("path is not a directory");
|
|
873
|
-
}
|
|
874
|
-
const limit = Math.max(1, Math.min(normalizeFsRpcNumber(args.request.limit, 200), 1000));
|
|
875
|
-
const rows = await readdir(abs, { withFileTypes: true });
|
|
876
|
-
const items = await Promise.all(rows.map(async (row) => {
|
|
877
|
-
const child = path.join(abs, row.name);
|
|
878
|
-
const childStat = await stat(child);
|
|
879
|
-
return {
|
|
880
|
-
name: row.name,
|
|
881
|
-
path: formatPath(child),
|
|
882
|
-
kind: row.isDirectory() ? "dir" : "file",
|
|
883
|
-
size: childStat.size,
|
|
884
|
-
mtimeMs: childStat.mtimeMs,
|
|
885
|
-
};
|
|
886
|
-
}));
|
|
887
|
-
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) {
|
|
888
2806
|
return {
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
path: formatPath(abs),
|
|
892
|
-
items: items.slice(0, limit),
|
|
893
|
-
truncated: items.length > limit,
|
|
894
|
-
total: items.length,
|
|
2807
|
+
rawRows: [],
|
|
2808
|
+
nextCursor: endLineIndex,
|
|
895
2809
|
};
|
|
896
2810
|
}
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
}
|
|
902
|
-
const
|
|
903
|
-
const
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
throw new Error("missing upload parameters");
|
|
907
|
-
}
|
|
908
|
-
const data = await readFile(abs);
|
|
909
|
-
const fileName = path.basename(abs) || "file";
|
|
910
|
-
const form = new FormData();
|
|
911
|
-
form.append("file", new File([data], fileName));
|
|
912
|
-
form.append("chatId", chatId);
|
|
913
|
-
form.append("agentId", agentId);
|
|
914
|
-
const response = await fetch(uploadUrl, {
|
|
915
|
-
method: "POST",
|
|
916
|
-
headers: { Authorization: `Bearer ${args.agentToken}` },
|
|
917
|
-
body: form,
|
|
918
|
-
});
|
|
919
|
-
const text = await response.text();
|
|
920
|
-
let upload = {};
|
|
921
|
-
try {
|
|
922
|
-
upload = JSON.parse(text || "{}");
|
|
923
|
-
}
|
|
924
|
-
catch {
|
|
925
|
-
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();
|
|
926
2820
|
}
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
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;
|
|
930
2831
|
}
|
|
931
2832
|
return {
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
path: formatPath(abs),
|
|
935
|
-
size: entry.size,
|
|
936
|
-
upload,
|
|
2833
|
+
rawRows,
|
|
2834
|
+
nextCursor: endLineIndex,
|
|
937
2835
|
};
|
|
938
2836
|
}
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
throw new Error("path is not a file");
|
|
2837
|
+
finally {
|
|
2838
|
+
await fileHandle.close().catch(() => undefined);
|
|
942
2839
|
}
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
const
|
|
946
|
-
const
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
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)) {
|
|
951
2856
|
try {
|
|
952
|
-
const
|
|
953
|
-
|
|
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,
|
|
954
2886
|
ok: true,
|
|
955
|
-
action,
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
};
|
|
2887
|
+
action: "watch",
|
|
2888
|
+
watchId,
|
|
2889
|
+
event,
|
|
2890
|
+
},
|
|
2891
|
+
});
|
|
2892
|
+
};
|
|
2893
|
+
const cleanup = () => {
|
|
2894
|
+
if (!active) {
|
|
2895
|
+
return;
|
|
965
2896
|
}
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
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;
|
|
974
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
|
+
});
|
|
975
2976
|
}
|
|
976
|
-
|
|
977
|
-
|
|
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}`);
|
|
978
2991
|
}
|
|
979
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
|
+
}
|
|
980
3011
|
async function handleFsRpcMessage(args) {
|
|
981
3012
|
let payload = {};
|
|
982
3013
|
try {
|
|
@@ -984,7 +3015,11 @@ async function handleFsRpcMessage(args) {
|
|
|
984
3015
|
if (typeof payload.agentId === "string" && payload.agentId.trim() && payload.agentId !== args.agentId) {
|
|
985
3016
|
throw new Error("agent id mismatch");
|
|
986
3017
|
}
|
|
987
|
-
const result = await executeFsRpc({
|
|
3018
|
+
const result = await executeFsRpc({
|
|
3019
|
+
request: payload,
|
|
3020
|
+
serverBaseUrl: args.serverBaseUrl,
|
|
3021
|
+
agentToken: args.agentToken,
|
|
3022
|
+
});
|
|
988
3023
|
args.msg.respond(fsRpcCodec.encode(JSON.stringify(result)));
|
|
989
3024
|
}
|
|
990
3025
|
catch (error) {
|
|
@@ -1094,7 +3129,8 @@ async function handleShellRpcMessage(args) {
|
|
|
1094
3129
|
const startedAtMs = Date.now();
|
|
1095
3130
|
const prepared = await prepareCommandExecution({
|
|
1096
3131
|
cwd: request.cwd,
|
|
1097
|
-
|
|
3132
|
+
userId: args.userId,
|
|
3133
|
+
taskId: request.requestId,
|
|
1098
3134
|
codexAuthBundle: request.codexAuthBundle,
|
|
1099
3135
|
});
|
|
1100
3136
|
const child = spawnPreparedCommand({
|
|
@@ -1181,6 +3217,7 @@ function subscribeToShellRpc(args) {
|
|
|
1181
3217
|
void handleShellRpcMessage({
|
|
1182
3218
|
msg,
|
|
1183
3219
|
jetstream: args.jetstream,
|
|
3220
|
+
userId: args.userId,
|
|
1184
3221
|
agentId: args.agentId,
|
|
1185
3222
|
agentToken: args.agentToken,
|
|
1186
3223
|
});
|
|
@@ -1351,49 +3388,39 @@ async function checkCancelRequested(args) {
|
|
|
1351
3388
|
return Boolean(response.task?.cancelRequested);
|
|
1352
3389
|
}
|
|
1353
3390
|
async function prepareTaskCodexAuth(args) {
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
3391
|
+
void args;
|
|
3392
|
+
return {
|
|
3393
|
+
envPatch: {},
|
|
3394
|
+
cleanup: async () => { },
|
|
3395
|
+
meta: {
|
|
3396
|
+
codexAuthSource: "agent_local",
|
|
3397
|
+
codexAuthSynced: false,
|
|
3398
|
+
},
|
|
3399
|
+
};
|
|
1363
3400
|
}
|
|
1364
3401
|
async function prepareCodexAuthBundle(bundle) {
|
|
1365
|
-
|
|
1366
|
-
return null;
|
|
1367
|
-
}
|
|
1368
|
-
const codexHome = resolveCodexHomePath();
|
|
1369
|
-
await mkdir(codexHome, { recursive: true });
|
|
1370
|
-
const authFile = path.join(codexHome, "auth.json");
|
|
1371
|
-
await writeFile(authFile, bundle.authJson, "utf8");
|
|
1372
|
-
await chmod(authFile, 0o600).catch(() => undefined);
|
|
1373
|
-
const envPatch = {
|
|
1374
|
-
CODEX_HOME: codexHome,
|
|
1375
|
-
};
|
|
1376
|
-
if (typeof bundle.apiKey === "string" && bundle.apiKey.trim()) {
|
|
1377
|
-
envPatch.OPENAI_API_KEY = bundle.apiKey.trim();
|
|
1378
|
-
}
|
|
1379
|
-
const cleanup = async () => { };
|
|
3402
|
+
void bundle;
|
|
1380
3403
|
return {
|
|
1381
|
-
envPatch,
|
|
1382
|
-
cleanup,
|
|
3404
|
+
envPatch: {},
|
|
3405
|
+
cleanup: async () => { },
|
|
1383
3406
|
meta: {
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
codexAuthExpiresAt: bundle.expiresAt ?? null,
|
|
1387
|
-
codexAuthSynced: true,
|
|
3407
|
+
codexAuthSource: "agent_local",
|
|
3408
|
+
codexAuthSynced: false,
|
|
1388
3409
|
},
|
|
1389
3410
|
};
|
|
1390
3411
|
}
|
|
1391
3412
|
async function prepareCommandExecution(args) {
|
|
1392
3413
|
const shellPath = resolveShellPath();
|
|
1393
3414
|
const taskWorkspace = resolveTaskWorkspace(args.cwd);
|
|
3415
|
+
const codexHome = resolveCodexHomePath();
|
|
3416
|
+
await mkdir(codexHome, { recursive: true });
|
|
1394
3417
|
const codexAuth = await prepareCodexAuthBundle(args.codexAuthBundle);
|
|
3418
|
+
const localAgentSettings = await readAgentSettingsConfig(null);
|
|
1395
3419
|
const baseTaskEnvPatch = {
|
|
1396
|
-
|
|
3420
|
+
CODEX_HOME: codexHome,
|
|
3421
|
+
DOER_USER_ID: args.userId,
|
|
3422
|
+
DOER_AGENT_TASK_ID: args.taskId,
|
|
3423
|
+
...buildAgentSettingsEnvPatch(localAgentSettings),
|
|
1397
3424
|
...(codexAuth?.envPatch ?? {}),
|
|
1398
3425
|
WORKSPACE: taskWorkspace,
|
|
1399
3426
|
};
|
|
@@ -1484,6 +3511,8 @@ async function runTask(args) {
|
|
|
1484
3511
|
};
|
|
1485
3512
|
const shellPath = resolveShellPath();
|
|
1486
3513
|
const taskWorkspace = resolveTaskWorkspace(args.cwd);
|
|
3514
|
+
const codexHome = resolveCodexHomePath();
|
|
3515
|
+
await mkdir(codexHome, { recursive: true });
|
|
1487
3516
|
const runtimeConfig = await prepareTaskRuntimeConfig({
|
|
1488
3517
|
serverBaseUrl: args.serverBaseUrl,
|
|
1489
3518
|
taskId: args.taskId,
|
|
@@ -1496,7 +3525,10 @@ async function runTask(args) {
|
|
|
1496
3525
|
userId: args.userId,
|
|
1497
3526
|
agentToken: args.agentToken,
|
|
1498
3527
|
});
|
|
3528
|
+
const localAgentSettings = await readAgentSettingsConfig(null);
|
|
1499
3529
|
const baseTaskEnvPatch = {
|
|
3530
|
+
CODEX_HOME: codexHome,
|
|
3531
|
+
...buildAgentSettingsEnvPatch(localAgentSettings),
|
|
1500
3532
|
...(runtimeConfig?.envPatch ?? {}),
|
|
1501
3533
|
...(codexAuth?.envPatch ?? {}),
|
|
1502
3534
|
WORKSPACE: taskWorkspace,
|
|
@@ -1699,6 +3731,10 @@ async function main() {
|
|
|
1699
3731
|
const startupWorkspaceRoot = path.resolve(workspaceDir || process.cwd());
|
|
1700
3732
|
workspaceRootOverride = startupWorkspaceRoot;
|
|
1701
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();
|
|
1702
3738
|
const serverBaseUrlRaw = resolveArgOrEnv(args, ["server", "url"], ["DOER_AGENT_SERVER"], DEFAULT_SERVER_BASE_URL);
|
|
1703
3739
|
const requestedServerBaseUrl = serverBaseUrlRaw.replace(/\/$/, "");
|
|
1704
3740
|
const serverBaseUrl = resolveContainerReachableServerBaseUrl(requestedServerBaseUrl);
|
|
@@ -1764,8 +3800,36 @@ async function main() {
|
|
|
1764
3800
|
agentId: initialAgentId,
|
|
1765
3801
|
agentToken,
|
|
1766
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
|
+
});
|
|
1767
3830
|
subscribeToRunRpc({
|
|
1768
3831
|
jetstream,
|
|
3832
|
+
serverBaseUrl,
|
|
1769
3833
|
userId,
|
|
1770
3834
|
agentId: initialAgentId,
|
|
1771
3835
|
agentToken,
|