doer-agent 0.9.1 → 0.9.3

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.
@@ -0,0 +1,42 @@
1
+ import assert from "node:assert/strict";
2
+ import test from "node:test";
3
+ import { buildNatsConnectionOptions } from "./agent-jetstream.js";
4
+ import { heartbeatAgentSession } from "./agent-runtime-io.js";
5
+ test("NATS connection detects stale networks quickly and keeps reconnecting", () => {
6
+ const options = buildNatsConnectionOptions({
7
+ servers: ["tls://nats.example.com:4222"],
8
+ token: "secret",
9
+ });
10
+ assert.equal(options.pingInterval, 2_000);
11
+ assert.equal(options.maxPingOut, 2);
12
+ assert.equal(options.maxReconnectAttempts, -1);
13
+ assert.equal(options.reconnectTimeWait, 1_000);
14
+ assert.equal(options.token, "secret");
15
+ });
16
+ test("heartbeat stops waiting when a NATS flush stalls", async () => {
17
+ const startedAt = Date.now();
18
+ await assert.rejects(heartbeatAgentSession({
19
+ nc: { flush: () => new Promise(() => undefined) },
20
+ serverBaseUrl: "https://doer.example.com",
21
+ userId: "user-1",
22
+ agentToken: "token-1",
23
+ timeoutMs: 20,
24
+ postJson: async () => ({ ok: true }),
25
+ }), /nats flush timed out after 20ms/);
26
+ assert.ok(Date.now() - startedAt < 500);
27
+ });
28
+ test("heartbeat forwards its timeout to the HTTP probe", async () => {
29
+ let observedTimeout;
30
+ await heartbeatAgentSession({
31
+ nc: { flush: async () => undefined },
32
+ serverBaseUrl: "https://doer.example.com",
33
+ userId: "user-1",
34
+ agentToken: "token-1",
35
+ timeoutMs: 3_000,
36
+ postJson: async (_url, _body, options) => {
37
+ observedTimeout = options?.timeoutMs;
38
+ return { ok: true };
39
+ },
40
+ });
41
+ assert.equal(observedTimeout, 3_000);
42
+ });
@@ -1,4 +1,20 @@
1
1
  import { AckPolicy, connect, DeliverPolicy, JSONCodec, RetentionPolicy, StorageType, } from "nats";
2
+ const NATS_PING_INTERVAL_MS = 2_000;
3
+ const NATS_MAX_PING_OUT = 2;
4
+ const NATS_RECONNECT_TIME_WAIT_MS = 1_000;
5
+ const NATS_RECONNECT_JITTER_MS = 250;
6
+ export function buildNatsConnectionOptions(args) {
7
+ return {
8
+ servers: args.servers,
9
+ ...(args.token ? { token: args.token } : {}),
10
+ pingInterval: NATS_PING_INTERVAL_MS,
11
+ maxPingOut: NATS_MAX_PING_OUT,
12
+ maxReconnectAttempts: -1,
13
+ reconnectTimeWait: NATS_RECONNECT_TIME_WAIT_MS,
14
+ reconnectJitter: NATS_RECONNECT_JITTER_MS,
15
+ reconnectJitterTLS: NATS_RECONNECT_JITTER_MS,
16
+ };
17
+ }
2
18
  export function normalizeNatsServers(value) {
3
19
  if (!Array.isArray(value)) {
4
20
  return [];
@@ -55,7 +71,7 @@ async function initJetStreamContext(args) {
55
71
  const stream = `DOER_AGENT_EVENTS_${sanitized}`;
56
72
  const subject = `doer.agent.events.${sanitized}`;
57
73
  const durable = `doer-agent-uploader-${sanitized}`;
58
- const nc = await connect(args.token ? { servers: args.servers, token: args.token } : { servers: args.servers });
74
+ const nc = await connect(buildNatsConnectionOptions({ servers: args.servers, token: args.token }));
59
75
  const jsm = await nc.jetstreamManager();
60
76
  await ensureJetStreamInfra({ jsm, stream, subject, durable });
61
77
  void (async () => {
@@ -1,9 +1,19 @@
1
- export async function postJson(url, body) {
2
- const res = await fetch(url, {
3
- method: "POST",
4
- headers: { "Content-Type": "application/json" },
5
- body: JSON.stringify(body),
6
- });
1
+ export async function postJson(url, body, options = {}) {
2
+ let res;
3
+ try {
4
+ res = await fetch(url, {
5
+ method: "POST",
6
+ headers: { "Content-Type": "application/json" },
7
+ body: JSON.stringify(body),
8
+ ...(options.timeoutMs ? { signal: AbortSignal.timeout(options.timeoutMs) } : {}),
9
+ });
10
+ }
11
+ catch (error) {
12
+ if (options.timeoutMs && error instanceof Error && error.name === "TimeoutError") {
13
+ throw new Error(`request timed out after ${options.timeoutMs}ms`);
14
+ }
15
+ throw error;
16
+ }
7
17
  const text = await res.text();
8
18
  let data = {};
9
19
  if (text) {
@@ -104,9 +114,22 @@ export function createEventPersistenceHelpers(args) {
104
114
  };
105
115
  }
106
116
  export async function heartbeatAgentSession(args) {
107
- await args.nc.flush();
117
+ let timeout;
118
+ try {
119
+ await Promise.race([
120
+ args.nc.flush(),
121
+ new Promise((_, reject) => {
122
+ timeout = setTimeout(() => reject(new Error(`nats flush timed out after ${args.timeoutMs}ms`)), args.timeoutMs);
123
+ }),
124
+ ]);
125
+ }
126
+ finally {
127
+ if (timeout) {
128
+ clearTimeout(timeout);
129
+ }
130
+ }
108
131
  await args.postJson(`${args.serverBaseUrl}/api/agent/heartbeat`, {
109
132
  userId: args.userId,
110
133
  agentToken: args.agentToken,
111
- });
134
+ }, { timeoutMs: args.timeoutMs });
112
135
  }
package/dist/agent.js CHANGED
@@ -28,7 +28,8 @@ const AGENT_PROJECT_DIR = path.join(AGENT_MODULE_DIR, "..");
28
28
  const AGENT_PACKAGE_JSON_PATH = path.join(AGENT_PROJECT_DIR, "package.json");
29
29
  const BUNDLED_SKILLS_ROOT = path.join(AGENT_PROJECT_DIR, "runtime", "skills");
30
30
  const HEARTBEAT_INTERVAL_MS = 5_000;
31
- const HEARTBEAT_FAILURE_THRESHOLD = 3;
31
+ const HEARTBEAT_FAILURE_THRESHOLD = 2;
32
+ const HEARTBEAT_REQUEST_TIMEOUT_MS = 3_000;
32
33
  const codexAppEventCodec = StringCodec();
33
34
  let activeTaskLogContext = null;
34
35
  let workspaceRootOverride = null;
@@ -187,6 +188,7 @@ const eventPersistenceHelpers = createEventPersistenceHelpers({
187
188
  const heartbeatSession = async (args) => {
188
189
  await heartbeatAgentSession({
189
190
  ...args,
191
+ timeoutMs: HEARTBEAT_REQUEST_TIMEOUT_MS,
190
192
  postJson,
191
193
  });
192
194
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "doer-agent",
3
- "version": "0.9.1",
3
+ "version": "0.9.3",
4
4
  "description": "Reverse-polling agent runtime for doer",
5
5
  "type": "module",
6
6
  "main": "dist/agent.js",
@@ -26,8 +26,8 @@
26
26
  },
27
27
  "dependencies": {
28
28
  "@modelcontextprotocol/sdk": "^1.29.0",
29
- "@openai/codex": "^0.144.5",
30
- "@openai/codex-sdk": "^0.144.5",
29
+ "@openai/codex": "^0.145.0",
30
+ "@openai/codex-sdk": "^0.145.0",
31
31
  "diff": "^9.0.0",
32
32
  "nats": "^2.29.3",
33
33
  "tar": "^7.5.15"