doer-agent 0.3.6 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/agent.js CHANGED
@@ -9,6 +9,8 @@ const DEFAULT_SERVER_BASE_URL = "https://doer.cranix.net";
9
9
  const AGENT_MODULE_DIR = path.dirname(fileURLToPath(import.meta.url));
10
10
  const AGENT_PROJECT_DIR = path.join(AGENT_MODULE_DIR, "..");
11
11
  const AGENT_PACKAGE_JSON_PATH = path.join(AGENT_PROJECT_DIR, "package.json");
12
+ const HEARTBEAT_INTERVAL_MS = 5_000;
13
+ const HEARTBEAT_FAILURE_THRESHOLD = 3;
12
14
  let activeTaskLogContext = null;
13
15
  let workspaceRootOverride = null;
14
16
  const fsRpcCodec = StringCodec();
@@ -605,10 +607,13 @@ function resolveAgentSettingsDir() {
605
607
  function resolveAgentSettingsFilePath() {
606
608
  return path.join(resolveAgentSettingsDir(), "config.json");
607
609
  }
610
+ function resolveAgentModelInstructionsFilePath() {
611
+ return path.join(resolveAgentSettingsDir(), "model-instructions.md");
612
+ }
608
613
  function createDefaultAgentSettingsConfig() {
609
614
  return {
610
615
  general: {
611
- firstTurnPrompt: null,
616
+ personality: "pragmatic",
612
617
  },
613
618
  codex: {
614
619
  model: "gpt-5.4",
@@ -671,6 +676,9 @@ function normalizeNullableString(value) {
671
676
  const trimmed = value.trim();
672
677
  return trimmed ? trimmed : null;
673
678
  }
679
+ function normalizeCodexPersonality(value, fallback) {
680
+ return value === "friendly" || value === "pragmatic" ? value : fallback;
681
+ }
674
682
  function normalizeAgentSettingsConfig(value, fallback) {
675
683
  const base = fallback ?? createDefaultAgentSettingsConfig();
676
684
  const raw = value && typeof value === "object" && !Array.isArray(value) ? value : {};
@@ -685,7 +693,7 @@ function normalizeAgentSettingsConfig(value, fallback) {
685
693
  const figma = raw.figma && typeof raw.figma === "object" ? raw.figma : {};
686
694
  return {
687
695
  general: {
688
- firstTurnPrompt: normalizeNullableString(general.firstTurnPrompt) ?? base.general.firstTurnPrompt,
696
+ personality: normalizeCodexPersonality(general.personality, base.general.personality),
689
697
  },
690
698
  codex: {
691
699
  model: typeof codex.model === "string" && codex.model.trim() ? codex.model.trim() : base.codex.model,
@@ -757,6 +765,20 @@ async function writeAgentSettingsConfig(config) {
757
765
  await mkdir(dir, { recursive: true });
758
766
  await writeFile(resolveAgentSettingsFilePath(), `${JSON.stringify(config, null, 2)}\n`, "utf8");
759
767
  }
768
+ async function readAgentModelInstructions() {
769
+ const raw = await readFile(resolveAgentModelInstructionsFilePath(), "utf8").catch(() => "");
770
+ return raw.trim() ? raw : null;
771
+ }
772
+ async function writeAgentModelInstructions(value) {
773
+ const filePath = resolveAgentModelInstructionsFilePath();
774
+ const nextValue = typeof value === "string" ? value.trim() : "";
775
+ if (!nextValue) {
776
+ await unlink(filePath).catch(() => undefined);
777
+ return;
778
+ }
779
+ await mkdir(resolveAgentSettingsDir(), { recursive: true });
780
+ await writeFile(filePath, value ?? "", "utf8");
781
+ }
760
782
  function maskSecretPreview(secret) {
761
783
  if (secret.length <= 6) {
762
784
  return `${secret.slice(0, 1)}***${secret.slice(-1)}`;
@@ -769,7 +791,7 @@ function toMaskedSecret(value) {
769
791
  }
770
792
  return { has: true, masked: maskSecretPreview(value), length: value.length };
771
793
  }
772
- function toAgentSettingsPublic(config) {
794
+ async function toAgentSettingsPublic(config) {
773
795
  const realtimeKey = toMaskedSecret(config.realtime.apiKey);
774
796
  const gitOauth = toMaskedSecret(config.git.oauthToken);
775
797
  const awsSecret = toMaskedSecret(config.aws.secretAccessKey);
@@ -778,9 +800,11 @@ function toAgentSettingsPublic(config) {
778
800
  const notionToken = toMaskedSecret(config.notion.apiToken);
779
801
  const slackToken = toMaskedSecret(config.slack.botToken);
780
802
  const figmaToken = toMaskedSecret(config.figma.apiToken);
803
+ const customInstructions = await readAgentModelInstructions();
781
804
  return {
782
805
  general: {
783
- firstTurnPrompt: config.general.firstTurnPrompt,
806
+ personality: config.general.personality,
807
+ customInstructions,
784
808
  },
785
809
  codex: {
786
810
  model: config.codex.model,
@@ -872,7 +896,7 @@ function normalizeAgentSettingsPatch(value) {
872
896
  assignNested(section, key, raw[flatKey]);
873
897
  delete patch[flatKey];
874
898
  };
875
- move("firstTurnPrompt", "general", "firstTurnPrompt");
899
+ move("personality", "general", "personality");
876
900
  move("codexModel", "codex", "model");
877
901
  move("codexAuthMode", "codex", "authMode");
878
902
  move("realtimeModel", "realtime", "model");
@@ -1341,17 +1365,27 @@ function normalizeCodexModel(value) {
1341
1365
  const normalized = typeof value === "string" ? value.trim() : "";
1342
1366
  return normalized || "gpt-5.4";
1343
1367
  }
1368
+ function toTomlStringLiteral(value) {
1369
+ return `"${value.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
1370
+ }
1344
1371
  function buildManagedCodexArgs(args) {
1345
1372
  const promptArgs = ["--", args.prompt];
1346
1373
  const fixedArgs = ["--dangerously-bypass-approvals-and-sandbox"];
1374
+ const configArgs = [
1375
+ ...(args.personality ? ["--config", `personality=${toTomlStringLiteral(args.personality)}`] : []),
1376
+ ...(args.modelInstructionsFile
1377
+ ? ["--config", `model_instructions_file=${toTomlStringLiteral(args.modelInstructionsFile)}`]
1378
+ : []),
1379
+ ];
1347
1380
  const imageArgs = args.imagePaths.flatMap((imagePath) => ["--image", imagePath]);
1348
1381
  return [
1349
1382
  ...fixedArgs,
1383
+ ...configArgs,
1350
1384
  "--model",
1351
1385
  args.model,
1352
1386
  ...(args.sessionId
1353
- ? ["exec", "resume", "--json", ...imageArgs, args.sessionId, ...promptArgs]
1354
- : ["exec", "--json", ...imageArgs, ...promptArgs]),
1387
+ ? ["exec", "resume", ...imageArgs, args.sessionId, ...promptArgs]
1388
+ : ["exec", ...imageArgs, ...promptArgs]),
1355
1389
  ];
1356
1390
  }
1357
1391
  function buildLocalCodexCliCommand(args) {
@@ -1717,6 +1751,14 @@ async function handleSettingsRpcMessage(args) {
1717
1751
  const next = request.action === "update" ? normalizeAgentSettingsConfig(request.patch, existing) : existing;
1718
1752
  if (request.action === "update") {
1719
1753
  await writeAgentSettingsConfig(next);
1754
+ const customInstructions = typeof request.patch.customInstructions === "string"
1755
+ ? request.patch.customInstructions
1756
+ : request.patch.customInstructions === null
1757
+ ? null
1758
+ : undefined;
1759
+ if (customInstructions !== undefined) {
1760
+ await writeAgentModelInstructions(customInstructions);
1761
+ }
1720
1762
  }
1721
1763
  else if (request.defaults) {
1722
1764
  const filePath = resolveAgentSettingsFilePath();
@@ -1731,7 +1773,7 @@ async function handleSettingsRpcMessage(args) {
1731
1773
  payload: {
1732
1774
  requestId,
1733
1775
  ok: true,
1734
- settings: toAgentSettingsPublic(next),
1776
+ settings: await toAgentSettingsPublic(next),
1735
1777
  },
1736
1778
  });
1737
1779
  }
@@ -2337,6 +2379,8 @@ async function handleRunRpcMessage(args) {
2337
2379
  const runId = request.runId ?? requestId;
2338
2380
  await claimRunStartSlot({ runId, sessionId: request.sessionId });
2339
2381
  try {
2382
+ const localAgentSettings = await readAgentSettingsConfig(null);
2383
+ const customInstructions = await readAgentModelInstructions();
2340
2384
  const task = await startManagedRun({
2341
2385
  requestId,
2342
2386
  runId,
@@ -2350,6 +2394,8 @@ async function handleRunRpcMessage(args) {
2350
2394
  imagePaths: request.imagePaths,
2351
2395
  sessionId: request.sessionId,
2352
2396
  model: request.model,
2397
+ personality: localAgentSettings.general.personality,
2398
+ modelInstructionsFile: customInstructions ? resolveAgentModelInstructionsFilePath() : null,
2353
2399
  }),
2354
2400
  cwd: request.cwd,
2355
2401
  runtimeEnvPatch: request.runtimeEnvPatch,
@@ -3890,7 +3936,8 @@ function persistEventOrFatal(args) {
3890
3936
  }
3891
3937
  })();
3892
3938
  }
3893
- async function heartbeatAgent(args) {
3939
+ async function heartbeatAgentSession(args) {
3940
+ await args.nc.flush();
3894
3941
  await postJson(`${args.serverBaseUrl}/api/agent/heartbeat`, {
3895
3942
  userId: args.userId,
3896
3943
  agentToken: args.agentToken,
@@ -4330,23 +4377,45 @@ async function main() {
4330
4377
  else {
4331
4378
  writeAgentInfraError(`nats session restored agentId=${initialAgentId} servers=${jetstream.servers.join(",")} at=${formatLocalTimestamp()}`);
4332
4379
  }
4333
- let heartbeatHealthy = null;
4380
+ let heartbeatFailures = 0;
4381
+ let heartbeatInFlight = false;
4382
+ let sessionInvalidated = false;
4383
+ const invalidateAgentSession = (reason) => {
4384
+ if (sessionInvalidated) {
4385
+ return;
4386
+ }
4387
+ sessionInvalidated = true;
4388
+ writeAgentInfraError(`closing nats session: ${reason}`);
4389
+ void jetstream.nc.close().catch((error) => {
4390
+ const message = error instanceof Error ? error.message : String(error);
4391
+ writeAgentInfraError(`failed to close nats session: ${message}`);
4392
+ });
4393
+ };
4334
4394
  const heartbeatTimer = setInterval(() => {
4335
- void heartbeatAgent({ serverBaseUrl, userId, agentToken })
4395
+ if (heartbeatInFlight || sessionInvalidated) {
4396
+ return;
4397
+ }
4398
+ heartbeatInFlight = true;
4399
+ void heartbeatAgentSession({ nc: jetstream.nc, serverBaseUrl, userId, agentToken })
4336
4400
  .then(() => {
4337
- if (heartbeatHealthy === false) {
4401
+ heartbeatInFlight = false;
4402
+ if (heartbeatFailures > 0) {
4338
4403
  writeAgentInfraError(`heartbeat reconnected at=${formatLocalTimestamp()}`);
4339
4404
  }
4340
- heartbeatHealthy = true;
4405
+ heartbeatFailures = 0;
4341
4406
  })
4342
4407
  .catch((error) => {
4408
+ heartbeatInFlight = false;
4343
4409
  const message = error instanceof Error ? error.message : String(error);
4344
- if (heartbeatHealthy !== false) {
4345
- writeAgentInfraError(`heartbeat failed: ${message}`);
4410
+ heartbeatFailures += 1;
4411
+ if (heartbeatFailures > 1) {
4412
+ writeAgentInfraError(`heartbeat failed: ${message} (count=${heartbeatFailures}/${HEARTBEAT_FAILURE_THRESHOLD})`);
4413
+ }
4414
+ if (heartbeatFailures >= HEARTBEAT_FAILURE_THRESHOLD) {
4415
+ invalidateAgentSession(`heartbeat failure threshold reached at=${formatLocalTimestamp()}`);
4346
4416
  }
4347
- heartbeatHealthy = false;
4348
4417
  });
4349
- }, 10_000);
4418
+ }, HEARTBEAT_INTERVAL_MS);
4350
4419
  subscribeToFsRpc({
4351
4420
  jetstream,
4352
4421
  serverBaseUrl,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "doer-agent",
3
- "version": "0.3.6",
3
+ "version": "0.4.1",
4
4
  "description": "Reverse-polling agent runtime for doer",
5
5
  "type": "module",
6
6
  "main": "dist/agent.js",
@@ -1,39 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- import net from "node:net";
4
-
5
- function debugLog(...parts) {
6
- const now = new Date().toISOString();
7
- process.stderr.write(`[doer-mcp-proxy][${now}] ${parts.join(" ")}\n`);
8
- }
9
-
10
- const socketPath = (process.env.DOER_MCP_SOCKET || "").trim();
11
-
12
- if (!socketPath) {
13
- process.stderr.write("doer-mcp-proxy error: DOER_MCP_SOCKET is not set\n");
14
- process.exit(1);
15
- }
16
-
17
- debugLog("connecting", `socket=${socketPath}`);
18
- const socket = net.createConnection(socketPath);
19
-
20
- socket.on("connect", () => {
21
- debugLog("connected");
22
- process.stdin.pipe(socket);
23
- socket.pipe(process.stdout);
24
- });
25
-
26
- socket.on("error", (error) => {
27
- const message = error instanceof Error ? error.message : String(error);
28
- process.stderr.write(`doer-mcp-proxy error: ${message}\n`);
29
- process.exit(1);
30
- });
31
-
32
- socket.on("close", () => {
33
- debugLog("closed");
34
- process.exit(0);
35
- });
36
-
37
- process.stdin.on("error", () => {
38
- socket.destroy();
39
- });