doer-agent 0.4.1 → 0.4.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.
- package/dist/agent-codex-auth-rpc.js +322 -0
- package/dist/agent-codex-cli.js +210 -0
- package/dist/agent-fs-rpc.js +402 -0
- package/dist/agent-git-rpc.js +299 -0
- package/dist/agent-jetstream.js +120 -0
- package/dist/agent-run-execution.js +39 -0
- package/dist/agent-run-lifecycle.js +67 -0
- package/dist/agent-run-rpc.js +93 -0
- package/dist/agent-run-state.js +229 -0
- package/dist/agent-runtime-env.js +147 -0
- package/dist/agent-runtime-io.js +112 -0
- package/dist/agent-runtime-utils.js +253 -0
- package/dist/agent-session-loop.js +53 -0
- package/dist/agent-session-rpc.js +867 -0
- package/dist/agent-settings-rpc.js +75 -0
- package/dist/agent-settings.js +397 -0
- package/dist/agent-skill-rpc.js +164 -0
- package/dist/agent-task-execution.js +275 -0
- package/dist/agent.js +376 -4289
- package/package.json +1 -1
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import { existsSync, statSync } from "node:fs";
|
|
3
|
+
import { chmod, mkdir, writeFile } from "node:fs/promises";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
function pickFirstNonEmpty(values) {
|
|
6
|
+
for (const value of values) {
|
|
7
|
+
if (typeof value !== "string") {
|
|
8
|
+
continue;
|
|
9
|
+
}
|
|
10
|
+
const normalized = value.trim();
|
|
11
|
+
if (normalized) {
|
|
12
|
+
return normalized;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
return "";
|
|
16
|
+
}
|
|
17
|
+
async function ensureGitAskpassScript(agentProjectDir) {
|
|
18
|
+
const binDir = path.join(agentProjectDir, "runtime/bin");
|
|
19
|
+
const scriptPath = path.join(binDir, "git-askpass.sh");
|
|
20
|
+
const scriptBody = `#!/bin/sh
|
|
21
|
+
case "$1" in
|
|
22
|
+
*Username*) printf "%s\\n" "x-access-token" ;;
|
|
23
|
+
*Password*) printf "%s\\n" "\${GITHUB_TOKEN:-\${GH_TOKEN:-}}" ;;
|
|
24
|
+
*) printf "\\n" ;;
|
|
25
|
+
esac
|
|
26
|
+
`;
|
|
27
|
+
await mkdir(binDir, { recursive: true });
|
|
28
|
+
await writeFile(scriptPath, scriptBody, "utf8");
|
|
29
|
+
await chmod(scriptPath, 0o700).catch(() => undefined);
|
|
30
|
+
return scriptPath;
|
|
31
|
+
}
|
|
32
|
+
function applyGitIdentityIfPossible(args) {
|
|
33
|
+
if (!args.cwd) {
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
const inRepo = spawnSync("git", ["rev-parse", "--is-inside-work-tree"], {
|
|
37
|
+
cwd: args.cwd,
|
|
38
|
+
stdio: "ignore",
|
|
39
|
+
});
|
|
40
|
+
if (inRepo.status !== 0) {
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
const setName = spawnSync("git", ["config", "--local", "user.name", args.userName], {
|
|
44
|
+
cwd: args.cwd,
|
|
45
|
+
stdio: "ignore",
|
|
46
|
+
});
|
|
47
|
+
if (setName.status !== 0) {
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
const setEmail = spawnSync("git", ["config", "--local", "user.email", args.userEmail], {
|
|
51
|
+
cwd: args.cwd,
|
|
52
|
+
stdio: "ignore",
|
|
53
|
+
});
|
|
54
|
+
return setEmail.status === 0;
|
|
55
|
+
}
|
|
56
|
+
export function createRuntimeEnvHelpers(args) {
|
|
57
|
+
function resolveCodexHomePath() {
|
|
58
|
+
return path.join(args.resolveWorkspaceRoot(), ".codex");
|
|
59
|
+
}
|
|
60
|
+
function resolveShellPath() {
|
|
61
|
+
if (process.platform === "win32") {
|
|
62
|
+
return process.env.ComSpec || "cmd.exe";
|
|
63
|
+
}
|
|
64
|
+
const candidates = [process.env.SHELL, "/bin/bash", "/usr/bin/bash", "/bin/sh", "/usr/bin/sh"].filter((value) => typeof value === "string" && value.trim().length > 0);
|
|
65
|
+
for (const candidate of candidates) {
|
|
66
|
+
if (existsSync(candidate)) {
|
|
67
|
+
return candidate;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
throw new Error("No shell executable found. Set SHELL env or install /bin/sh (or bash).");
|
|
71
|
+
}
|
|
72
|
+
function resolveTaskWorkspace(rawCwd) {
|
|
73
|
+
const workspaceRoot = args.resolveWorkspaceRoot();
|
|
74
|
+
const requestedCwd = rawCwd?.trim() || "";
|
|
75
|
+
const resolvedCwd = requestedCwd
|
|
76
|
+
? path.isAbsolute(requestedCwd)
|
|
77
|
+
? path.resolve(requestedCwd)
|
|
78
|
+
: path.resolve(workspaceRoot, requestedCwd)
|
|
79
|
+
: workspaceRoot;
|
|
80
|
+
if (!existsSync(resolvedCwd)) {
|
|
81
|
+
throw new Error(`Invalid cwd: ${requestedCwd || "(empty)"} resolved to ${resolvedCwd} (path does not exist)`);
|
|
82
|
+
}
|
|
83
|
+
let stats;
|
|
84
|
+
try {
|
|
85
|
+
stats = statSync(resolvedCwd);
|
|
86
|
+
}
|
|
87
|
+
catch (error) {
|
|
88
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
89
|
+
throw new Error(`Invalid cwd: ${requestedCwd || "(empty)"} resolved to ${resolvedCwd} (${message})`);
|
|
90
|
+
}
|
|
91
|
+
if (!stats.isDirectory()) {
|
|
92
|
+
throw new Error(`Invalid cwd: ${requestedCwd || "(empty)"} resolved to ${resolvedCwd} (not a directory)`);
|
|
93
|
+
}
|
|
94
|
+
return resolvedCwd;
|
|
95
|
+
}
|
|
96
|
+
async function prepareTaskGitEnv(prepareArgs) {
|
|
97
|
+
const envPatch = {
|
|
98
|
+
GIT_TERMINAL_PROMPT: "0",
|
|
99
|
+
GCM_INTERACTIVE: "Never",
|
|
100
|
+
};
|
|
101
|
+
const githubToken = pickFirstNonEmpty([
|
|
102
|
+
prepareArgs.baseEnvPatch.GITHUB_TOKEN,
|
|
103
|
+
prepareArgs.baseEnvPatch.GH_TOKEN,
|
|
104
|
+
process.env.GITHUB_TOKEN,
|
|
105
|
+
process.env.GH_TOKEN,
|
|
106
|
+
]);
|
|
107
|
+
if (githubToken) {
|
|
108
|
+
envPatch.GITHUB_TOKEN = githubToken;
|
|
109
|
+
envPatch.GH_TOKEN = githubToken;
|
|
110
|
+
envPatch.GIT_ASKPASS_REQUIRE = "force";
|
|
111
|
+
envPatch.GIT_ASKPASS = await ensureGitAskpassScript(args.agentProjectDir);
|
|
112
|
+
}
|
|
113
|
+
const userName = pickFirstNonEmpty([
|
|
114
|
+
prepareArgs.baseEnvPatch.DOER_GIT_USER_NAME,
|
|
115
|
+
prepareArgs.baseEnvPatch.GIT_USER_NAME,
|
|
116
|
+
prepareArgs.baseEnvPatch.GIT_AUTHOR_NAME,
|
|
117
|
+
prepareArgs.baseEnvPatch.GIT_COMMITTER_NAME,
|
|
118
|
+
]);
|
|
119
|
+
const userEmail = pickFirstNonEmpty([
|
|
120
|
+
prepareArgs.baseEnvPatch.DOER_GIT_USER_EMAIL,
|
|
121
|
+
prepareArgs.baseEnvPatch.GIT_USER_EMAIL,
|
|
122
|
+
prepareArgs.baseEnvPatch.GIT_AUTHOR_EMAIL,
|
|
123
|
+
prepareArgs.baseEnvPatch.GIT_COMMITTER_EMAIL,
|
|
124
|
+
]);
|
|
125
|
+
const gitIdentityApplied = userName && userEmail
|
|
126
|
+
? applyGitIdentityIfPossible({
|
|
127
|
+
cwd: prepareArgs.cwd,
|
|
128
|
+
userName,
|
|
129
|
+
userEmail,
|
|
130
|
+
})
|
|
131
|
+
: false;
|
|
132
|
+
return {
|
|
133
|
+
envPatch,
|
|
134
|
+
meta: {
|
|
135
|
+
gitAskpassEnabled: Boolean(envPatch.GIT_ASKPASS),
|
|
136
|
+
gitIdentityApplied,
|
|
137
|
+
gitIdentityProvided: Boolean(userName && userEmail),
|
|
138
|
+
},
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
return {
|
|
142
|
+
resolveCodexHomePath,
|
|
143
|
+
resolveShellPath,
|
|
144
|
+
resolveTaskWorkspace,
|
|
145
|
+
prepareTaskGitEnv,
|
|
146
|
+
};
|
|
147
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
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
|
+
});
|
|
7
|
+
const text = await res.text();
|
|
8
|
+
let data = {};
|
|
9
|
+
if (text) {
|
|
10
|
+
try {
|
|
11
|
+
data = JSON.parse(text);
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
data = {};
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
if (!res.ok) {
|
|
18
|
+
const errObj = (data && typeof data === "object" ? data : {});
|
|
19
|
+
const message = typeof errObj.error === "string" ? errObj.error : `HTTP ${res.status}`;
|
|
20
|
+
throw new Error(message);
|
|
21
|
+
}
|
|
22
|
+
return data;
|
|
23
|
+
}
|
|
24
|
+
export async function getJson(url) {
|
|
25
|
+
const res = await fetch(url);
|
|
26
|
+
const text = await res.text();
|
|
27
|
+
let data = {};
|
|
28
|
+
if (text) {
|
|
29
|
+
try {
|
|
30
|
+
data = JSON.parse(text);
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
data = {};
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
if (!res.ok) {
|
|
37
|
+
const errObj = (data && typeof data === "object" ? data : {});
|
|
38
|
+
const message = typeof errObj.error === "string" ? errObj.error : `HTTP ${res.status}`;
|
|
39
|
+
throw new Error(message);
|
|
40
|
+
}
|
|
41
|
+
return data;
|
|
42
|
+
}
|
|
43
|
+
export function createEventPersistenceHelpers(args) {
|
|
44
|
+
const nextEventSeqByTask = new Map();
|
|
45
|
+
async function recordAgentEvent(recordArgs) {
|
|
46
|
+
await args.publishEvent(recordArgs);
|
|
47
|
+
}
|
|
48
|
+
function reserveNextEventSeq(taskId) {
|
|
49
|
+
const current = nextEventSeqByTask.get(taskId) ?? 1;
|
|
50
|
+
nextEventSeqByTask.set(taskId, current + 1);
|
|
51
|
+
return current;
|
|
52
|
+
}
|
|
53
|
+
function emitAgentMetaLog(level, message) {
|
|
54
|
+
const ctx = args.getActiveTaskLogContext();
|
|
55
|
+
if (!ctx) {
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
const seq = reserveNextEventSeq(ctx.taskId);
|
|
59
|
+
void recordAgentEvent({
|
|
60
|
+
jetstream: ctx.jetstream,
|
|
61
|
+
serverBaseUrl: ctx.serverBaseUrl,
|
|
62
|
+
taskId: ctx.taskId,
|
|
63
|
+
userId: ctx.userId,
|
|
64
|
+
type: "meta",
|
|
65
|
+
seq,
|
|
66
|
+
payload: {
|
|
67
|
+
channel: "agent",
|
|
68
|
+
level,
|
|
69
|
+
message,
|
|
70
|
+
at: args.formatTimestamp(),
|
|
71
|
+
},
|
|
72
|
+
}).catch((error) => {
|
|
73
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
74
|
+
process.stderr.write(`[doer-agent] meta log persist failed task=${ctx.taskId}: ${detail}\n`);
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
function persistEventOrFatal(persistArgs) {
|
|
78
|
+
void (async () => {
|
|
79
|
+
let attempt = 0;
|
|
80
|
+
let delayMs = 150;
|
|
81
|
+
while (attempt < 3) {
|
|
82
|
+
attempt += 1;
|
|
83
|
+
try {
|
|
84
|
+
await recordAgentEvent(persistArgs);
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
catch (error) {
|
|
88
|
+
if (attempt >= 3) {
|
|
89
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
90
|
+
args.onError(`task=${persistArgs.taskId} ${persistArgs.context}: ${message} (dropped after ${attempt} attempts)`);
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
await args.sleep(delayMs);
|
|
94
|
+
delayMs *= 2;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
})();
|
|
98
|
+
}
|
|
99
|
+
return {
|
|
100
|
+
reserveNextEventSeq,
|
|
101
|
+
emitAgentMetaLog,
|
|
102
|
+
recordAgentEvent,
|
|
103
|
+
persistEventOrFatal,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
export async function heartbeatAgentSession(args) {
|
|
107
|
+
await args.nc.flush();
|
|
108
|
+
await args.postJson(`${args.serverBaseUrl}/api/agent/heartbeat`, {
|
|
109
|
+
userId: args.userId,
|
|
110
|
+
agentToken: args.agentToken,
|
|
111
|
+
});
|
|
112
|
+
}
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
export function sanitizeUserId(userId) {
|
|
3
|
+
const normalized = userId.trim().replace(/[^a-zA-Z0-9_-]/g, "_");
|
|
4
|
+
return normalized.length > 0 ? normalized : "anonymous";
|
|
5
|
+
}
|
|
6
|
+
export function buildAgentRunRpcSubject(userId, agentId) {
|
|
7
|
+
return `doer.agent.run.rpc.${sanitizeUserId(userId)}.${agentId.trim()}`;
|
|
8
|
+
}
|
|
9
|
+
export function buildAgentRunEventsSubject(userId, agentId) {
|
|
10
|
+
return `doer.agent.run.events.${sanitizeUserId(userId)}.${agentId.trim()}`;
|
|
11
|
+
}
|
|
12
|
+
export function buildAgentSessionRpcSubject(userId, agentId) {
|
|
13
|
+
return `doer.agent.session.rpc.${sanitizeUserId(userId)}.${agentId.trim()}`;
|
|
14
|
+
}
|
|
15
|
+
export function buildAgentCodexAuthRpcSubject(userId, agentId) {
|
|
16
|
+
return `doer.agent.codex.auth.rpc.${sanitizeUserId(userId)}.${agentId.trim()}`;
|
|
17
|
+
}
|
|
18
|
+
export function buildAgentSettingsRpcSubject(userId, agentId) {
|
|
19
|
+
return `doer.agent.settings.rpc.${sanitizeUserId(userId)}.${agentId.trim()}`;
|
|
20
|
+
}
|
|
21
|
+
export function buildAgentGitRpcSubject(userId, agentId) {
|
|
22
|
+
return `doer.agent.git.rpc.${sanitizeUserId(userId)}.${agentId.trim()}`;
|
|
23
|
+
}
|
|
24
|
+
export function buildAgentSkillRpcSubject(userId, agentId) {
|
|
25
|
+
return `doer.agent.skill.rpc.${sanitizeUserId(userId)}.${agentId.trim()}`;
|
|
26
|
+
}
|
|
27
|
+
export function buildAgentFsRpcSubject(userId, agentId) {
|
|
28
|
+
return `doer.agent.fs.rpc.${sanitizeUserId(userId)}.${agentId.trim()}`;
|
|
29
|
+
}
|
|
30
|
+
export function parseBootstrapTaskConfig(value) {
|
|
31
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
const task = value;
|
|
35
|
+
const stream = typeof task.stream === "string" ? task.stream.trim() : "";
|
|
36
|
+
const subject = typeof task.subject === "string" ? task.subject.trim() : "";
|
|
37
|
+
const durable = typeof task.durable === "string" ? task.durable.trim() : "";
|
|
38
|
+
if (!stream || !subject || !durable) {
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
return { stream, subject, durable };
|
|
42
|
+
}
|
|
43
|
+
export function normalizeTaskIds(value) {
|
|
44
|
+
if (!Array.isArray(value)) {
|
|
45
|
+
return [];
|
|
46
|
+
}
|
|
47
|
+
const out = [];
|
|
48
|
+
for (const item of value) {
|
|
49
|
+
if (typeof item !== "string") {
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
const id = item.trim();
|
|
53
|
+
if (!id) {
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
out.push(id);
|
|
57
|
+
}
|
|
58
|
+
return out;
|
|
59
|
+
}
|
|
60
|
+
export function normalizeEnvPatch(value) {
|
|
61
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
62
|
+
return {};
|
|
63
|
+
}
|
|
64
|
+
const out = {};
|
|
65
|
+
for (const [key, raw] of Object.entries(value)) {
|
|
66
|
+
if (typeof raw !== "string") {
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
const normalizedKey = key.trim();
|
|
70
|
+
if (!normalizedKey) {
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
out[normalizedKey] = raw;
|
|
74
|
+
}
|
|
75
|
+
return out;
|
|
76
|
+
}
|
|
77
|
+
export function normalizeRunImagePaths(value) {
|
|
78
|
+
if (!Array.isArray(value)) {
|
|
79
|
+
return [];
|
|
80
|
+
}
|
|
81
|
+
const seen = new Set();
|
|
82
|
+
const out = [];
|
|
83
|
+
for (const item of value) {
|
|
84
|
+
if (typeof item !== "string") {
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
const normalized = item.trim();
|
|
88
|
+
if (!normalized || seen.has(normalized)) {
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
seen.add(normalized);
|
|
92
|
+
out.push(normalized);
|
|
93
|
+
}
|
|
94
|
+
return out;
|
|
95
|
+
}
|
|
96
|
+
export function fatalExit(message, error, writeAgentError) {
|
|
97
|
+
const detail = error instanceof Error ? error.message : typeof error === "string" ? error : error ? String(error) : "";
|
|
98
|
+
const full = detail ? `${message}: ${detail}` : message;
|
|
99
|
+
writeAgentError(`fatal: ${full}`);
|
|
100
|
+
process.exit(1);
|
|
101
|
+
}
|
|
102
|
+
export function sleep(ms) {
|
|
103
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
104
|
+
}
|
|
105
|
+
export function writeTaskStream(taskId, stream, chunk) {
|
|
106
|
+
const target = stream === "stdout" ? process.stdout : process.stderr;
|
|
107
|
+
const lines = chunk.replace(/\r/g, "\n").split("\n");
|
|
108
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
109
|
+
const line = lines[i];
|
|
110
|
+
if (line.length === 0 && i === lines.length - 1) {
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
target.write(`[doer-agent][task=${taskId}][${stream}] ${line}\n`);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
export function writeTaskUpload(taskId, message) {
|
|
117
|
+
process.stdout.write(`[doer-agent][task=${taskId}][upload] ${message}\n`);
|
|
118
|
+
}
|
|
119
|
+
export function writeRpcStream(requestId, stream, chunk) {
|
|
120
|
+
const target = stream === "stdout" ? process.stdout : process.stderr;
|
|
121
|
+
const lines = chunk.replace(/\r/g, "\n").split("\n");
|
|
122
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
123
|
+
const line = lines[i];
|
|
124
|
+
if (line.length === 0 && i === lines.length - 1) {
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
target.write(`[doer-agent][rpc=${requestId}][${stream}] ${line}\n`);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
export function writeRpcStatus(requestId, message) {
|
|
131
|
+
process.stdout.write(`[doer-agent][rpc=${requestId}][status] ${message}\n`);
|
|
132
|
+
}
|
|
133
|
+
export function writeRunStatus(runId, message) {
|
|
134
|
+
process.stdout.write(`[doer-agent][run=${runId}][status] ${message}\n`);
|
|
135
|
+
}
|
|
136
|
+
export function writeRunStream(runId, stream, chunk) {
|
|
137
|
+
const target = stream === "stdout" ? process.stdout : process.stderr;
|
|
138
|
+
const lines = chunk.split(/\r?\n/);
|
|
139
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
140
|
+
const line = lines[index];
|
|
141
|
+
if (!line && index === lines.length - 1) {
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
target.write(`[doer-agent][run=${runId}][${stream}] ${line}\n`);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
function resolveLogTimeZone() {
|
|
148
|
+
const configured = process.env.DOER_AGENT_LOG_TIMEZONE?.trim() || process.env.TZ?.trim();
|
|
149
|
+
return configured && configured.length > 0 ? configured : "Asia/Seoul";
|
|
150
|
+
}
|
|
151
|
+
function resolveTimeZoneOffsetString(date, timeZone) {
|
|
152
|
+
try {
|
|
153
|
+
const parts = new Intl.DateTimeFormat("en-US", {
|
|
154
|
+
timeZone,
|
|
155
|
+
timeZoneName: "shortOffset",
|
|
156
|
+
hour: "2-digit",
|
|
157
|
+
minute: "2-digit",
|
|
158
|
+
hour12: false,
|
|
159
|
+
}).formatToParts(date);
|
|
160
|
+
const token = parts.find((part) => part.type === "timeZoneName")?.value || "GMT+0";
|
|
161
|
+
const matched = token.match(/GMT([+-]\d{1,2})(?::?(\d{2}))?/i);
|
|
162
|
+
if (!matched) {
|
|
163
|
+
return "+00:00";
|
|
164
|
+
}
|
|
165
|
+
const hourRaw = matched[1] || "+0";
|
|
166
|
+
const minuteRaw = matched[2] || "00";
|
|
167
|
+
const sign = hourRaw.startsWith("-") ? "-" : "+";
|
|
168
|
+
const absHour = String(Math.abs(Number.parseInt(hourRaw, 10))).padStart(2, "0");
|
|
169
|
+
const absMinute = String(Math.abs(Number.parseInt(minuteRaw, 10))).padStart(2, "0");
|
|
170
|
+
return `${sign}${absHour}:${absMinute}`;
|
|
171
|
+
}
|
|
172
|
+
catch {
|
|
173
|
+
return "+00:00";
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
export function formatLocalTimestamp(date = new Date()) {
|
|
177
|
+
const timeZone = resolveLogTimeZone();
|
|
178
|
+
try {
|
|
179
|
+
const parts = new Intl.DateTimeFormat("en-CA", {
|
|
180
|
+
timeZone,
|
|
181
|
+
year: "numeric",
|
|
182
|
+
month: "2-digit",
|
|
183
|
+
day: "2-digit",
|
|
184
|
+
hour: "2-digit",
|
|
185
|
+
minute: "2-digit",
|
|
186
|
+
second: "2-digit",
|
|
187
|
+
hour12: false,
|
|
188
|
+
}).formatToParts(date);
|
|
189
|
+
const pick = (type) => {
|
|
190
|
+
return parts.find((part) => part.type === type)?.value || "00";
|
|
191
|
+
};
|
|
192
|
+
const year = pick("year");
|
|
193
|
+
const month = pick("month");
|
|
194
|
+
const day = pick("day");
|
|
195
|
+
const hours = pick("hour");
|
|
196
|
+
const minutes = pick("minute");
|
|
197
|
+
const seconds = pick("second");
|
|
198
|
+
const ms = String(date.getMilliseconds()).padStart(3, "0");
|
|
199
|
+
const offset = resolveTimeZoneOffsetString(date, timeZone);
|
|
200
|
+
return `${year}-${month}-${day}T${hours}:${minutes}:${seconds}.${ms}${offset}`;
|
|
201
|
+
}
|
|
202
|
+
catch {
|
|
203
|
+
return date.toISOString();
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
export function parseArgs(argv) {
|
|
207
|
+
const out = {};
|
|
208
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
209
|
+
const key = argv[i];
|
|
210
|
+
if (!key.startsWith("--")) {
|
|
211
|
+
continue;
|
|
212
|
+
}
|
|
213
|
+
const value = argv[i + 1];
|
|
214
|
+
if (typeof value === "string" && !value.startsWith("--")) {
|
|
215
|
+
out[key.slice(2)] = value;
|
|
216
|
+
i += 1;
|
|
217
|
+
continue;
|
|
218
|
+
}
|
|
219
|
+
out[key.slice(2)] = "true";
|
|
220
|
+
}
|
|
221
|
+
return out;
|
|
222
|
+
}
|
|
223
|
+
export function resolveArgOrEnv(args, argKeys, envKeys, fallback = "") {
|
|
224
|
+
for (const key of argKeys) {
|
|
225
|
+
const value = args[key]?.trim();
|
|
226
|
+
if (value) {
|
|
227
|
+
return value;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
for (const key of envKeys) {
|
|
231
|
+
const value = process.env[key]?.trim();
|
|
232
|
+
if (value) {
|
|
233
|
+
return value;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
return fallback;
|
|
237
|
+
}
|
|
238
|
+
export function resolveContainerReachableServerBaseUrl(serverBaseUrl) {
|
|
239
|
+
return serverBaseUrl;
|
|
240
|
+
}
|
|
241
|
+
export async function resolveAgentVersion(packageJsonPath) {
|
|
242
|
+
const raw = await readFile(packageJsonPath, "utf8").catch(() => "");
|
|
243
|
+
if (!raw) {
|
|
244
|
+
return "unknown";
|
|
245
|
+
}
|
|
246
|
+
try {
|
|
247
|
+
const parsed = JSON.parse(raw);
|
|
248
|
+
return typeof parsed.version === "string" && parsed.version.trim() ? parsed.version.trim() : "unknown";
|
|
249
|
+
}
|
|
250
|
+
catch {
|
|
251
|
+
return "unknown";
|
|
252
|
+
}
|
|
253
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
export async function runConnectedAgentSession(args) {
|
|
2
|
+
let heartbeatFailures = 0;
|
|
3
|
+
let heartbeatInFlight = false;
|
|
4
|
+
let sessionInvalidated = false;
|
|
5
|
+
const invalidateAgentSession = (reason) => {
|
|
6
|
+
if (sessionInvalidated) {
|
|
7
|
+
return;
|
|
8
|
+
}
|
|
9
|
+
sessionInvalidated = true;
|
|
10
|
+
args.onInfraError(`closing nats session: ${reason}`);
|
|
11
|
+
void args.jetstream.nc.close().catch((error) => {
|
|
12
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
13
|
+
args.onInfraError(`failed to close nats session: ${message}`);
|
|
14
|
+
});
|
|
15
|
+
};
|
|
16
|
+
const heartbeatTimer = setInterval(() => {
|
|
17
|
+
if (heartbeatInFlight || sessionInvalidated) {
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
heartbeatInFlight = true;
|
|
21
|
+
void args.heartbeatAgentSession({
|
|
22
|
+
nc: args.jetstream.nc,
|
|
23
|
+
serverBaseUrl: args.serverBaseUrl,
|
|
24
|
+
userId: args.userId,
|
|
25
|
+
agentToken: args.agentToken,
|
|
26
|
+
})
|
|
27
|
+
.then(() => {
|
|
28
|
+
heartbeatInFlight = false;
|
|
29
|
+
if (heartbeatFailures > 0) {
|
|
30
|
+
args.onInfraError(`heartbeat reconnected at=${args.formatTimestamp()}`);
|
|
31
|
+
}
|
|
32
|
+
heartbeatFailures = 0;
|
|
33
|
+
})
|
|
34
|
+
.catch((error) => {
|
|
35
|
+
heartbeatInFlight = false;
|
|
36
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
37
|
+
heartbeatFailures += 1;
|
|
38
|
+
if (heartbeatFailures > 1) {
|
|
39
|
+
args.onInfraError(`heartbeat failed: ${message} (count=${heartbeatFailures}/${args.heartbeatFailureThreshold})`);
|
|
40
|
+
}
|
|
41
|
+
if (heartbeatFailures >= args.heartbeatFailureThreshold) {
|
|
42
|
+
invalidateAgentSession(`heartbeat failure threshold reached at=${args.formatTimestamp()}`);
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
}, args.heartbeatIntervalMs);
|
|
46
|
+
args.subscribeAll();
|
|
47
|
+
const closeError = await args.jetstream.nc.closed();
|
|
48
|
+
clearInterval(heartbeatTimer);
|
|
49
|
+
args.stopAllSessionWatchers();
|
|
50
|
+
const detail = closeError instanceof Error ? closeError.message : "clean close";
|
|
51
|
+
args.onInfraError(`nats session ended: ${detail}; reconnecting`);
|
|
52
|
+
await args.sleep(1000);
|
|
53
|
+
}
|