doer-agent 0.2.4 → 0.2.6

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