@ouro.bot/cli 0.1.0-alpha.241 → 0.1.0-alpha.243
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/changelog.json +10 -1
- package/dist/heart/core.js +39 -52
- package/dist/heart/daemon/agent-discovery.js +34 -0
- package/dist/heart/daemon/cli-render.js +46 -10
- package/dist/heart/daemon/daemon.js +3 -4
- package/dist/heart/providers/anthropic.js +5 -28
- package/dist/heart/providers/azure.js +2 -21
- package/dist/heart/providers/error-classification.js +63 -0
- package/dist/heart/providers/github-copilot.js +2 -23
- package/dist/heart/providers/minimax.js +2 -21
- package/dist/heart/providers/openai-codex.js +8 -32
- package/package.json +1 -1
package/changelog.json
CHANGED
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"_note": "This changelog is maintained as part of the PR/version-bump workflow. Agent-curated, not auto-generated. Agents read this file directly via read_file to understand what changed between versions.",
|
|
3
3
|
"versions": [
|
|
4
|
+
{
|
|
5
|
+
"version": "0.1.0-alpha.243",
|
|
6
|
+
"changes": [
|
|
7
|
+
"fix(daemon): per-agent Git Sync in `ouro status` \u2014 was always showing `disabled` regardless of any agent's `agent.json`, because the daemon process has no argv-derived agent identity so `getSyncConfig()` fell into its catch. Status payload now carries a per-agent `sync` array (one row per enabled bundle) rendered as its own section like Senses and Workers, instead of a single global field on the overview block."
|
|
8
|
+
]
|
|
9
|
+
},
|
|
4
10
|
{
|
|
5
11
|
"version": "0.1.0-alpha.241",
|
|
6
12
|
"changes": [
|
|
@@ -11,7 +17,10 @@
|
|
|
11
17
|
"version": "0.1.0-alpha.242",
|
|
12
18
|
"changes": [
|
|
13
19
|
"fix(friends): stable local CLI identity \u2014 dropped hostname from external ID (was `username@hostname`, now just `username`). macOS hostname instability (`Mac` vs `Aris-MacBook-Pro.local`) was creating duplicate friend records with separate sessions and trust levels.",
|
|
14
|
-
"feat(friends): migration fallback \u2014 FriendResolver now searches for old `username@*` format IDs when exact match fails, linking new stable ID to existing friend record."
|
|
20
|
+
"feat(friends): migration fallback \u2014 FriendResolver now searches for old `username@*` format IDs when exact match fails, linking new stable ID to existing friend record.",
|
|
21
|
+
"fix(heart): retry-everything-except-blocklist policy \u2014 the SDK 'Request timed out.' error from MiniMax (and other providers) was reaching slugger as a terminal failure because neither the generic isTransientError detector nor the per-provider classifier recognized it. Replaced the two-layer transient detection with a small blocklist (HTTP 400/401/403/404/422 + classifications auth-failure/usage-limit). Default policy now retries every other error.",
|
|
22
|
+
"fix(heart/providers): drop the 30s OpenAI/Anthropic SDK timeout from all five providers (anthropic, azure, github-copilot, minimax, openai-codex). The SDK timeout caps the entire stream lifetime, so 30s killed any reasoning model mid-generation. SDK defaults (\u224810min) are sane.",
|
|
23
|
+
"refactor(heart/providers): consolidate duplicated isNetworkError + classifyXxxError scaffolding into a shared `error-classification.ts` module. Each provider now delegates via `classifyHttpError(err, overrides)` and only carries its own provider-specific quirks (Anthropic 529, Codex usage-limit message detection)."
|
|
15
24
|
]
|
|
16
25
|
},
|
|
17
26
|
{
|
package/dist/heart/core.js
CHANGED
|
@@ -10,8 +10,7 @@ exports.isExternalStateQuery = isExternalStateQuery;
|
|
|
10
10
|
exports.getSettleRetryError = getSettleRetryError;
|
|
11
11
|
exports.stripLastToolCalls = stripLastToolCalls;
|
|
12
12
|
exports.repairOrphanedToolCalls = repairOrphanedToolCalls;
|
|
13
|
-
exports.
|
|
14
|
-
exports.classifyTransientError = classifyTransientError;
|
|
13
|
+
exports.isRetryBlocked = isRetryBlocked;
|
|
15
14
|
exports.runAgent = runAgent;
|
|
16
15
|
const config_1 = require("./config");
|
|
17
16
|
const identity_1 = require("./identity");
|
|
@@ -353,48 +352,39 @@ function isContextOverflow(err) {
|
|
|
353
352
|
return true;
|
|
354
353
|
return false;
|
|
355
354
|
}
|
|
356
|
-
//
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
//
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
355
|
+
// HTTP statuses that will never become retryable on their own — the request is
|
|
356
|
+
// semantically wrong (malformed, unauthorized, missing route, etc.) and the
|
|
357
|
+
// caller has to do something different before it can succeed.
|
|
358
|
+
const NON_RETRYABLE_HTTP_STATUSES = new Set([
|
|
359
|
+
400, // Bad Request — malformed payload
|
|
360
|
+
401, // Unauthorized — credentials invalid/expired
|
|
361
|
+
403, // Forbidden — credentials lack permission
|
|
362
|
+
404, // Not Found — model/route doesn't exist
|
|
363
|
+
422, // Unprocessable Entity — semantic validation failure
|
|
364
|
+
]);
|
|
365
|
+
// Provider-classified error categories that we never retry. usage-limit is
|
|
366
|
+
// distinct from rate-limit: rate limits clear in seconds (retryable), usage
|
|
367
|
+
// limits are billing quotas that take hours/days to reset.
|
|
368
|
+
const NON_RETRYABLE_CLASSIFICATIONS = new Set([
|
|
369
|
+
"auth-failure",
|
|
370
|
+
"usage-limit",
|
|
371
|
+
]);
|
|
372
|
+
// Default policy: retry every error from the provider, EXCEPT the small set
|
|
373
|
+
// above. The user explicitly requested this — past behavior was to retry only
|
|
374
|
+
// on a known-transient list, which silently dropped real harness/SDK timeouts
|
|
375
|
+
// (e.g. OpenAI SDK's "Request timed out." has no err.code and no status, so
|
|
376
|
+
// the substring matchers missed it).
|
|
377
|
+
function isRetryBlocked(error, classification) {
|
|
378
|
+
const status = error.status;
|
|
379
|
+
if (status !== undefined && NON_RETRYABLE_HTTP_STATUSES.has(status))
|
|
370
380
|
return true;
|
|
371
|
-
if (
|
|
372
|
-
return true;
|
|
373
|
-
if (msg.includes("socket hang up"))
|
|
374
|
-
return true;
|
|
375
|
-
if (msg.includes("getaddrinfo"))
|
|
376
|
-
return true;
|
|
377
|
-
// HTTP 429 / 500 / 502 / 503 / 504
|
|
378
|
-
const status = err.status;
|
|
379
|
-
if (status === 429 || status === 500 || status === 502 || status === 503 || status === 504)
|
|
381
|
+
if (NON_RETRYABLE_CLASSIFICATIONS.has(classification))
|
|
380
382
|
return true;
|
|
381
383
|
return false;
|
|
382
384
|
}
|
|
383
|
-
function classifyTransientError(err) {
|
|
384
|
-
if (!(err instanceof Error))
|
|
385
|
-
return "unknown error";
|
|
386
|
-
const status = err.status;
|
|
387
|
-
if (status === 429)
|
|
388
|
-
return "rate limited";
|
|
389
|
-
if (status === 401 || status === 403)
|
|
390
|
-
return "auth error";
|
|
391
|
-
if (status && status >= 500)
|
|
392
|
-
return "server error";
|
|
393
|
-
return "network error";
|
|
394
|
-
}
|
|
395
385
|
const MAX_RETRIES = 3;
|
|
396
386
|
const RETRY_BASE_MS = 2000;
|
|
397
|
-
const
|
|
387
|
+
const RETRY_LABELS = {
|
|
398
388
|
"auth-failure": "auth error",
|
|
399
389
|
"usage-limit": "usage limit",
|
|
400
390
|
"rate-limit": "rate limited",
|
|
@@ -942,10 +932,10 @@ async function runAgent(messages, callbacks, channel, signal, options) {
|
|
|
942
932
|
callbacks.onError(new Error("context trimmed, retrying..."), "transient");
|
|
943
933
|
continue;
|
|
944
934
|
}
|
|
945
|
-
//
|
|
946
|
-
//
|
|
947
|
-
//
|
|
948
|
-
//
|
|
935
|
+
// Retry policy: retry every error EXCEPT those on the blocklist
|
|
936
|
+
// (NON_RETRYABLE_HTTP_STATUSES / NON_RETRYABLE_CLASSIFICATIONS).
|
|
937
|
+
// The classification still drives the user-facing label and the
|
|
938
|
+
// auth-failure guidance message below — it just no longer gates retries.
|
|
949
939
|
const errorForClassification = e instanceof Error ? e : /* v8 ignore next -- defensive @preserve */ new Error(String(e));
|
|
950
940
|
let providerClassification;
|
|
951
941
|
try {
|
|
@@ -955,25 +945,23 @@ async function runAgent(messages, callbacks, channel, signal, options) {
|
|
|
955
945
|
/* v8 ignore next -- defensive: classifyError should not throw @preserve */
|
|
956
946
|
providerClassification = "unknown";
|
|
957
947
|
}
|
|
958
|
-
const
|
|
959
|
-
|
|
960
|
-
|| providerClassification === "network-error";
|
|
961
|
-
const retryableByGeneric = isTransientError(e);
|
|
962
|
-
const shouldRetry = (retryableByGeneric || retryableByClassification) && retryCount < MAX_RETRIES;
|
|
948
|
+
const blocked = isRetryBlocked(errorForClassification, providerClassification);
|
|
949
|
+
const shouldRetry = !blocked && retryCount < MAX_RETRIES;
|
|
963
950
|
(0, runtime_1.emitNervesEvent)({
|
|
964
951
|
level: shouldRetry ? "info" : "warn",
|
|
965
952
|
event: shouldRetry ? "engine.provider_retry" : "engine.provider_retry_skip",
|
|
966
953
|
component: "engine",
|
|
967
954
|
message: shouldRetry
|
|
968
955
|
? `provider error is retryable (attempt ${retryCount + 1}/${MAX_RETRIES})`
|
|
969
|
-
:
|
|
956
|
+
: blocked
|
|
957
|
+
? `provider error is on retry blocklist`
|
|
958
|
+
: `provider error retries exhausted`,
|
|
970
959
|
meta: {
|
|
971
960
|
provider: providerRuntime.id,
|
|
972
961
|
model: providerRuntime.model,
|
|
973
962
|
retryCount,
|
|
974
963
|
maxRetries: MAX_RETRIES,
|
|
975
|
-
|
|
976
|
-
retryableByClassification,
|
|
964
|
+
blocked,
|
|
977
965
|
providerClassification,
|
|
978
966
|
errorMessage: errorForClassification.message.slice(0, 200),
|
|
979
967
|
httpStatus: e.status ?? null,
|
|
@@ -982,8 +970,7 @@ async function runAgent(messages, callbacks, channel, signal, options) {
|
|
|
982
970
|
if (shouldRetry) {
|
|
983
971
|
retryCount++;
|
|
984
972
|
const delay = RETRY_BASE_MS * Math.pow(2, retryCount - 1);
|
|
985
|
-
|
|
986
|
-
const cause = TRANSIENT_RETRY_LABELS[providerClassification] ?? providerClassification;
|
|
973
|
+
const cause = RETRY_LABELS[providerClassification];
|
|
987
974
|
callbacks.onError(new Error(`${cause}, retrying in ${delay / 1000}s (${retryCount}/${MAX_RETRIES})...`), "transient");
|
|
988
975
|
// Wait with abort support
|
|
989
976
|
const aborted = await new Promise((resolve) => {
|
|
@@ -34,6 +34,7 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
36
|
exports.listEnabledBundleAgents = listEnabledBundleAgents;
|
|
37
|
+
exports.listBundleSyncRows = listBundleSyncRows;
|
|
37
38
|
const fs = __importStar(require("fs"));
|
|
38
39
|
const path = __importStar(require("path"));
|
|
39
40
|
const identity_1 = require("../identity");
|
|
@@ -79,3 +80,36 @@ function listEnabledBundleAgents(options = {}) {
|
|
|
79
80
|
}
|
|
80
81
|
return discovered.sort((left, right) => left.localeCompare(right));
|
|
81
82
|
}
|
|
83
|
+
/**
|
|
84
|
+
* Read the per-agent sync block from each enabled bundle's agent.json.
|
|
85
|
+
* Used by the daemon (and stopped-state status renderer) to build per-agent
|
|
86
|
+
* sync rows without depending on argv-derived global identity. Bundles that
|
|
87
|
+
* cannot be read are skipped silently.
|
|
88
|
+
*/
|
|
89
|
+
function listBundleSyncRows(options = {}) {
|
|
90
|
+
const bundlesRoot = options.bundlesRoot ?? (0, identity_1.getAgentBundlesRoot)();
|
|
91
|
+
const readFileSync = options.readFileSync ?? fs.readFileSync;
|
|
92
|
+
const agents = listEnabledBundleAgents(options);
|
|
93
|
+
const rows = [];
|
|
94
|
+
for (const agent of agents) {
|
|
95
|
+
const configPath = path.join(bundlesRoot, `${agent}.ouro`, "agent.json");
|
|
96
|
+
let enabled = false;
|
|
97
|
+
let remote = "origin";
|
|
98
|
+
try {
|
|
99
|
+
const raw = readFileSync(configPath, "utf-8");
|
|
100
|
+
const parsed = JSON.parse(raw);
|
|
101
|
+
if (parsed.sync && typeof parsed.sync === "object") {
|
|
102
|
+
if (typeof parsed.sync.enabled === "boolean")
|
|
103
|
+
enabled = parsed.sync.enabled;
|
|
104
|
+
if (typeof parsed.sync.remote === "string" && parsed.sync.remote.length > 0) {
|
|
105
|
+
remote = parsed.sync.remote;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
catch {
|
|
110
|
+
// Best-effort: bundle without readable config still gets a row with defaults
|
|
111
|
+
}
|
|
112
|
+
rows.push({ agent, enabled, remote });
|
|
113
|
+
}
|
|
114
|
+
return rows;
|
|
115
|
+
}
|
|
@@ -54,6 +54,7 @@ const runtime_mode_1 = require("./runtime-mode");
|
|
|
54
54
|
const identity_1 = require("../identity");
|
|
55
55
|
const daemon_tombstone_1 = require("./daemon-tombstone");
|
|
56
56
|
const daemon_health_1 = require("./daemon-health");
|
|
57
|
+
const agent_discovery_1 = require("./agent-discovery");
|
|
57
58
|
// ── Field extractors ──
|
|
58
59
|
function stringField(value) {
|
|
59
60
|
return typeof value === "string" ? value : null;
|
|
@@ -72,10 +73,14 @@ function parseStatusPayload(data) {
|
|
|
72
73
|
const overview = raw.overview;
|
|
73
74
|
const senses = raw.senses;
|
|
74
75
|
const workers = raw.workers;
|
|
76
|
+
const sync = raw.sync;
|
|
75
77
|
if (!overview || typeof overview !== "object" || Array.isArray(overview))
|
|
76
78
|
return null;
|
|
77
79
|
if (!Array.isArray(senses) || !Array.isArray(workers))
|
|
78
80
|
return null;
|
|
81
|
+
// sync is optional for backward compatibility — older daemons may omit it
|
|
82
|
+
if (sync !== undefined && !Array.isArray(sync))
|
|
83
|
+
return null;
|
|
79
84
|
const parsedOverview = {
|
|
80
85
|
daemon: stringField(overview.daemon) ?? "unknown",
|
|
81
86
|
health: stringField(overview.health) ?? "unknown",
|
|
@@ -89,8 +94,6 @@ function parseStatusPayload(data) {
|
|
|
89
94
|
senseCount: numberField(overview.senseCount) ?? 0,
|
|
90
95
|
entryPath: stringField(overview.entryPath) ?? "unknown",
|
|
91
96
|
mode: stringField(overview.mode) ?? "unknown",
|
|
92
|
-
syncEnabled: booleanField(overview.syncEnabled) ?? false,
|
|
93
|
-
syncRemote: stringField(overview.syncRemote) ?? "origin",
|
|
94
97
|
};
|
|
95
98
|
const parsedSenses = senses.map((entry) => {
|
|
96
99
|
if (!entry || typeof entry !== "object" || Array.isArray(entry))
|
|
@@ -135,12 +138,26 @@ function parseStatusPayload(data) {
|
|
|
135
138
|
lastSignal: stringField(row.lastSignal) ?? null,
|
|
136
139
|
};
|
|
137
140
|
});
|
|
138
|
-
|
|
141
|
+
const parsedSync = (sync ?? []).map((entry) => {
|
|
142
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry))
|
|
143
|
+
return null;
|
|
144
|
+
const row = entry;
|
|
145
|
+
const agent = stringField(row.agent);
|
|
146
|
+
const enabled = booleanField(row.enabled);
|
|
147
|
+
const remote = stringField(row.remote);
|
|
148
|
+
if (!agent || enabled === null || !remote)
|
|
149
|
+
return null;
|
|
150
|
+
return { agent, enabled, remote };
|
|
151
|
+
});
|
|
152
|
+
if (parsedSenses.some((row) => row === null) ||
|
|
153
|
+
parsedWorkers.some((row) => row === null) ||
|
|
154
|
+
parsedSync.some((row) => row === null))
|
|
139
155
|
return null;
|
|
140
156
|
return {
|
|
141
157
|
overview: parsedOverview,
|
|
142
158
|
senses: parsedSenses,
|
|
143
159
|
workers: parsedWorkers,
|
|
160
|
+
sync: parsedSync,
|
|
144
161
|
};
|
|
145
162
|
}
|
|
146
163
|
// ── ANSI color helpers (private) ──
|
|
@@ -232,9 +249,6 @@ function formatDaemonStatusOutput(response, fallback) {
|
|
|
232
249
|
lines.push(kvLine("Socket", ov.socketPath));
|
|
233
250
|
lines.push(kvLine("Outlook", ov.outlookUrl));
|
|
234
251
|
lines.push(kvLine("Health", `${statusDot(ov.health)} ${ov.health}`));
|
|
235
|
-
/* v8 ignore next -- cosmetic: sync-enabled branch requires agent with sync configured @preserve */
|
|
236
|
-
const syncLabel = ov.syncEnabled ? `${green("●")} enabled (remote: ${ov.syncRemote})` : `${dim("○")} disabled`;
|
|
237
|
-
lines.push(kvLine("Git Sync", syncLabel));
|
|
238
252
|
lines.push(kvLine("Updated", ov.lastUpdated));
|
|
239
253
|
lines.push("");
|
|
240
254
|
// ── Senses ──
|
|
@@ -295,6 +309,19 @@ function formatDaemonStatusOutput(response, fallback) {
|
|
|
295
309
|
}
|
|
296
310
|
lines.push("");
|
|
297
311
|
}
|
|
312
|
+
// ── Git Sync (per agent) ──
|
|
313
|
+
if (payload.sync.length > 0) {
|
|
314
|
+
lines.push(` ${teal("──")} ${bold("Git Sync")} ${teal("─".repeat(35))}`);
|
|
315
|
+
const agentNameWidth = Math.max(12, ...payload.sync.map((r) => r.agent.length));
|
|
316
|
+
for (const row of payload.sync) {
|
|
317
|
+
const name = row.agent.padEnd(agentNameWidth);
|
|
318
|
+
const dot = row.enabled ? green("●") : dim("○");
|
|
319
|
+
const stateText = (row.enabled ? "enabled " : "disabled").padEnd(10);
|
|
320
|
+
const detail = row.enabled ? `remote: ${row.remote}` : "";
|
|
321
|
+
lines.push(` ${name} ${dot} ${stateText} ${dim(detail)}`);
|
|
322
|
+
}
|
|
323
|
+
lines.push("");
|
|
324
|
+
}
|
|
298
325
|
return lines.join("\n");
|
|
299
326
|
}
|
|
300
327
|
function formatVersionOutput() {
|
|
@@ -304,7 +331,7 @@ function formatVersionOutput() {
|
|
|
304
331
|
return mode === "dev" ? `${version} (dev)` : version;
|
|
305
332
|
/* v8 ignore stop */
|
|
306
333
|
}
|
|
307
|
-
function buildStoppedStatusPayload(socketPath) {
|
|
334
|
+
function buildStoppedStatusPayload(socketPath, syncRows = []) {
|
|
308
335
|
const metadata = (0, runtime_metadata_1.getRuntimeMetadata)();
|
|
309
336
|
const repoRoot = (0, identity_1.getRepoRoot)();
|
|
310
337
|
return {
|
|
@@ -321,14 +348,23 @@ function buildStoppedStatusPayload(socketPath) {
|
|
|
321
348
|
senseCount: 0,
|
|
322
349
|
entryPath: path.join(repoRoot, "dist", "heart", "daemon", "daemon-entry.js"),
|
|
323
350
|
mode: (0, runtime_mode_1.detectRuntimeMode)(repoRoot),
|
|
324
|
-
syncEnabled: false,
|
|
325
|
-
syncRemote: "origin",
|
|
326
351
|
},
|
|
327
352
|
senses: [],
|
|
328
353
|
workers: [],
|
|
354
|
+
sync: syncRows,
|
|
329
355
|
};
|
|
330
356
|
}
|
|
331
357
|
function daemonUnavailableStatusOutput(socketPath, healthFilePath) {
|
|
358
|
+
// Read per-agent sync config from disk so the user still sees it when the daemon is down.
|
|
359
|
+
// Best-effort: any fs error returns [] and the section is omitted.
|
|
360
|
+
let syncRows = [];
|
|
361
|
+
try {
|
|
362
|
+
syncRows = (0, agent_discovery_1.listBundleSyncRows)();
|
|
363
|
+
}
|
|
364
|
+
catch {
|
|
365
|
+
// listBundleSyncRows already swallows fs errors internally; this catch is a defensive
|
|
366
|
+
// safety net for environments where the fs module itself is partially mocked.
|
|
367
|
+
}
|
|
332
368
|
/* v8 ignore start — tombstone read tested in daemon-status-tombstone.test; branch misreported @preserve */
|
|
333
369
|
const tombstone = (0, daemon_tombstone_1.readDaemonTombstone)();
|
|
334
370
|
const deathLine = tombstone
|
|
@@ -339,7 +375,7 @@ function daemonUnavailableStatusOutput(socketPath, healthFilePath) {
|
|
|
339
375
|
formatDaemonStatusOutput({
|
|
340
376
|
ok: true,
|
|
341
377
|
summary: "daemon not running",
|
|
342
|
-
data: buildStoppedStatusPayload(socketPath),
|
|
378
|
+
data: buildStoppedStatusPayload(socketPath, syncRows),
|
|
343
379
|
}, "daemon not running"),
|
|
344
380
|
"",
|
|
345
381
|
];
|
|
@@ -42,7 +42,7 @@ const net = __importStar(require("net"));
|
|
|
42
42
|
const os = __importStar(require("os"));
|
|
43
43
|
const path = __importStar(require("path"));
|
|
44
44
|
const identity_1 = require("../identity");
|
|
45
|
-
const
|
|
45
|
+
const agent_discovery_1 = require("./agent-discovery");
|
|
46
46
|
const runtime_1 = require("../../nerves/runtime");
|
|
47
47
|
const runtime_metadata_1 = require("./runtime-metadata");
|
|
48
48
|
const runtime_mode_1 = require("./runtime-mode");
|
|
@@ -234,7 +234,7 @@ class OuroDaemon {
|
|
|
234
234
|
const workers = buildWorkerRows(snapshots);
|
|
235
235
|
const senses = this.senseManager?.listSenseRows() ?? [];
|
|
236
236
|
const repoRoot = (0, identity_1.getRepoRoot)();
|
|
237
|
-
const
|
|
237
|
+
const sync = (0, agent_discovery_1.listBundleSyncRows)({ bundlesRoot: this.bundlesRoot });
|
|
238
238
|
return {
|
|
239
239
|
overview: {
|
|
240
240
|
daemon: "running",
|
|
@@ -246,11 +246,10 @@ class OuroDaemon {
|
|
|
246
246
|
senseCount: senses.length,
|
|
247
247
|
entryPath: path.join(repoRoot, "dist", "heart", "daemon", "daemon-entry.js"),
|
|
248
248
|
mode: (0, runtime_mode_1.detectRuntimeMode)(repoRoot),
|
|
249
|
-
syncEnabled: syncConfig.enabled,
|
|
250
|
-
syncRemote: syncConfig.remote,
|
|
251
249
|
},
|
|
252
250
|
workers,
|
|
253
251
|
senses,
|
|
252
|
+
sync,
|
|
254
253
|
};
|
|
255
254
|
}
|
|
256
255
|
async start() {
|
|
@@ -45,6 +45,7 @@ const identity_1 = require("../identity");
|
|
|
45
45
|
const runtime_1 = require("../../nerves/runtime");
|
|
46
46
|
const streaming_1 = require("../streaming");
|
|
47
47
|
const model_capabilities_1 = require("../model-capabilities");
|
|
48
|
+
const error_classification_1 = require("./error-classification");
|
|
48
49
|
const ANTHROPIC_SETUP_TOKEN_PREFIX = "sk-ant-oat01-";
|
|
49
50
|
const ANTHROPIC_SETUP_TOKEN_MIN_LENGTH = 80;
|
|
50
51
|
const ANTHROPIC_OAUTH_BETA_HEADER = "claude-code-20250219,oauth-2025-04-20,fine-grained-tool-streaming-2025-05-14,interleaved-thinking-2025-05-14";
|
|
@@ -221,42 +222,19 @@ function mergeAnthropicToolArguments(current, partial) {
|
|
|
221
222
|
}
|
|
222
223
|
return current + partial;
|
|
223
224
|
}
|
|
224
|
-
/* v8 ignore start -- shared network error utility, tested via classification tests @preserve */
|
|
225
|
-
function isNetworkError(error) {
|
|
226
|
-
const code = error.code || "";
|
|
227
|
-
if (["ECONNRESET", "ECONNREFUSED", "ENOTFOUND", "ETIMEDOUT", "EPIPE",
|
|
228
|
-
"EAI_AGAIN", "EHOSTUNREACH", "ENETUNREACH", "ECONNABORTED"].includes(code))
|
|
229
|
-
return true;
|
|
230
|
-
const msg = error.message || "";
|
|
231
|
-
return msg.includes("fetch failed") || msg.includes("socket hang up") || msg.includes("getaddrinfo");
|
|
232
|
-
}
|
|
233
|
-
/* v8 ignore stop */
|
|
234
225
|
function classifyAnthropicError(error) {
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
return "rate-limit";
|
|
240
|
-
if (status === 529 || (status && status >= 500))
|
|
241
|
-
return "server-error";
|
|
242
|
-
if (isNetworkError(error))
|
|
243
|
-
return "network-error";
|
|
244
|
-
return "unknown";
|
|
226
|
+
return (0, error_classification_1.classifyHttpError)(error, {
|
|
227
|
+
isAuthFailure: isAnthropicAuthFailure,
|
|
228
|
+
isServerError: (e) => e.status === 529,
|
|
229
|
+
});
|
|
245
230
|
}
|
|
246
|
-
/* v8 ignore start -- auth detection: only called from classifyAnthropicError which always passes Error @preserve */
|
|
247
231
|
function isAnthropicAuthFailure(error) {
|
|
248
|
-
if (!(error instanceof Error))
|
|
249
|
-
return false;
|
|
250
|
-
const status = error.status;
|
|
251
|
-
if (status === 401 || status === 403)
|
|
252
|
-
return true;
|
|
253
232
|
const lower = error.message.toLowerCase();
|
|
254
233
|
return (lower.includes("oauth authentication") ||
|
|
255
234
|
lower.includes("authentication failed") ||
|
|
256
235
|
lower.includes("unauthorized") ||
|
|
257
236
|
lower.includes("invalid api key"));
|
|
258
237
|
}
|
|
259
|
-
/* v8 ignore stop */
|
|
260
238
|
async function streamAnthropicMessages(client, model, request) {
|
|
261
239
|
const { system, messages } = toAnthropicMessages(request.messages);
|
|
262
240
|
const anthropicTools = toAnthropicTools(request.activeTools);
|
|
@@ -438,7 +416,6 @@ function createAnthropicProviderRuntime(model) {
|
|
|
438
416
|
function createClient(token) {
|
|
439
417
|
return new sdk_1.default({
|
|
440
418
|
authToken: token,
|
|
441
|
-
timeout: 30000,
|
|
442
419
|
maxRetries: 0,
|
|
443
420
|
defaultHeaders: {
|
|
444
421
|
"anthropic-beta": ANTHROPIC_OAUTH_BETA_HEADER,
|
|
@@ -41,28 +41,10 @@ const config_1 = require("../config");
|
|
|
41
41
|
const runtime_1 = require("../../nerves/runtime");
|
|
42
42
|
const streaming_1 = require("../streaming");
|
|
43
43
|
const model_capabilities_1 = require("../model-capabilities");
|
|
44
|
+
const error_classification_1 = require("./error-classification");
|
|
44
45
|
const COGNITIVE_SERVICES_SCOPE = "https://cognitiveservices.azure.com/.default";
|
|
45
|
-
/* v8 ignore start -- shared network error utility, tested via classification tests @preserve */
|
|
46
|
-
function isNetworkError(error) {
|
|
47
|
-
const code = error.code || "";
|
|
48
|
-
if (["ECONNRESET", "ECONNREFUSED", "ENOTFOUND", "ETIMEDOUT", "EPIPE",
|
|
49
|
-
"EAI_AGAIN", "EHOSTUNREACH", "ENETUNREACH", "ECONNABORTED"].includes(code))
|
|
50
|
-
return true;
|
|
51
|
-
const msg = error.message || "";
|
|
52
|
-
return msg.includes("fetch failed") || msg.includes("socket hang up") || msg.includes("getaddrinfo");
|
|
53
|
-
}
|
|
54
|
-
/* v8 ignore stop */
|
|
55
46
|
function classifyAzureError(error) {
|
|
56
|
-
|
|
57
|
-
if (status === 401 || status === 403)
|
|
58
|
-
return "auth-failure";
|
|
59
|
-
if (status === 429)
|
|
60
|
-
return "rate-limit";
|
|
61
|
-
if (status && status >= 500)
|
|
62
|
-
return "server-error";
|
|
63
|
-
if (isNetworkError(error))
|
|
64
|
-
return "network-error";
|
|
65
|
-
return "unknown";
|
|
47
|
+
return (0, error_classification_1.classifyHttpError)(error);
|
|
66
48
|
}
|
|
67
49
|
// @azure/identity is imported dynamically (below) rather than at the top level
|
|
68
50
|
// because it's a heavy package (~30+ transitive deps) and we only need it when
|
|
@@ -113,7 +95,6 @@ function createAzureProviderRuntime(model) {
|
|
|
113
95
|
endpoint: azureConfig.endpoint.replace(/\/openai.*$/, ""),
|
|
114
96
|
deployment: azureConfig.deployment,
|
|
115
97
|
apiVersion: azureConfig.apiVersion,
|
|
116
|
-
timeout: 30000,
|
|
117
98
|
maxRetries: 0,
|
|
118
99
|
};
|
|
119
100
|
if (useApiKey) {
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.isNetworkError = isNetworkError;
|
|
4
|
+
exports.classifyHttpError = classifyHttpError;
|
|
5
|
+
const runtime_1 = require("../../nerves/runtime");
|
|
6
|
+
// Node socket / DNS error codes that indicate a transient network failure.
|
|
7
|
+
const NETWORK_ERROR_CODES = new Set([
|
|
8
|
+
"ECONNRESET",
|
|
9
|
+
"ECONNREFUSED",
|
|
10
|
+
"ENOTFOUND",
|
|
11
|
+
"ETIMEDOUT",
|
|
12
|
+
"EPIPE",
|
|
13
|
+
"EAI_AGAIN",
|
|
14
|
+
"EHOSTUNREACH",
|
|
15
|
+
"ENETUNREACH",
|
|
16
|
+
"ECONNABORTED",
|
|
17
|
+
]);
|
|
18
|
+
// Substrings the OpenAI/Anthropic SDKs use when wrapping fetch/socket failures
|
|
19
|
+
// into Error.message instead of an err.code.
|
|
20
|
+
const NETWORK_ERROR_MESSAGE_PATTERNS = [
|
|
21
|
+
"fetch failed",
|
|
22
|
+
"socket hang up",
|
|
23
|
+
"getaddrinfo",
|
|
24
|
+
"request timed out", // OpenAI SDK timeout — see SDK source
|
|
25
|
+
"request timeout",
|
|
26
|
+
"connection error",
|
|
27
|
+
];
|
|
28
|
+
// True if the error looks like a transient network issue (no HTTP status, just
|
|
29
|
+
// a socket/DNS/timeout failure from the underlying transport).
|
|
30
|
+
function isNetworkError(error) {
|
|
31
|
+
const code = error.code || "";
|
|
32
|
+
if (NETWORK_ERROR_CODES.has(code))
|
|
33
|
+
return true;
|
|
34
|
+
const msg = (error.message || "").toLowerCase();
|
|
35
|
+
return NETWORK_ERROR_MESSAGE_PATTERNS.some((pat) => msg.includes(pat));
|
|
36
|
+
}
|
|
37
|
+
// Standard HTTP error → ProviderErrorClassification mapping. Providers wrap
|
|
38
|
+
// this with their own overrides.
|
|
39
|
+
function classifyHttpError(error, overrides) {
|
|
40
|
+
const status = error.status;
|
|
41
|
+
if (overrides?.isAuthFailure?.(error) || status === 401 || status === 403) {
|
|
42
|
+
return "auth-failure";
|
|
43
|
+
}
|
|
44
|
+
if (status === 429) {
|
|
45
|
+
if (overrides?.isUsageLimit?.(error))
|
|
46
|
+
return "usage-limit";
|
|
47
|
+
return "rate-limit";
|
|
48
|
+
}
|
|
49
|
+
if (overrides?.isServerError?.(error) || (status !== undefined && status >= 500)) {
|
|
50
|
+
return "server-error";
|
|
51
|
+
}
|
|
52
|
+
if (isNetworkError(error))
|
|
53
|
+
return "network-error";
|
|
54
|
+
return "unknown";
|
|
55
|
+
}
|
|
56
|
+
/* v8 ignore start — module-level observability event */
|
|
57
|
+
(0, runtime_1.emitNervesEvent)({
|
|
58
|
+
component: "engine",
|
|
59
|
+
event: "engine.error_classification_loaded",
|
|
60
|
+
message: "shared provider error classification loaded",
|
|
61
|
+
meta: {},
|
|
62
|
+
});
|
|
63
|
+
/* v8 ignore stop */
|
|
@@ -10,30 +10,10 @@ const config_1 = require("../config");
|
|
|
10
10
|
const runtime_1 = require("../../nerves/runtime");
|
|
11
11
|
const streaming_1 = require("../streaming");
|
|
12
12
|
const model_capabilities_1 = require("../model-capabilities");
|
|
13
|
-
|
|
14
|
-
function isNetworkError(error) {
|
|
15
|
-
const code = error.code || "";
|
|
16
|
-
if (["ECONNRESET", "ECONNREFUSED", "ENOTFOUND", "ETIMEDOUT", "EPIPE",
|
|
17
|
-
"EAI_AGAIN", "EHOSTUNREACH", "ENETUNREACH", "ECONNABORTED"].includes(code))
|
|
18
|
-
return true;
|
|
19
|
-
const msg = error.message || "";
|
|
20
|
-
return msg.includes("fetch failed") || msg.includes("socket hang up") || msg.includes("getaddrinfo");
|
|
21
|
-
}
|
|
22
|
-
/* v8 ignore stop */
|
|
23
|
-
/* v8 ignore start -- duplicated classification pattern, tested via provider unit tests @preserve */
|
|
13
|
+
const error_classification_1 = require("./error-classification");
|
|
24
14
|
function classifyGithubCopilotError(error) {
|
|
25
|
-
|
|
26
|
-
if (status === 401 || status === 403)
|
|
27
|
-
return "auth-failure";
|
|
28
|
-
if (status === 429)
|
|
29
|
-
return "rate-limit";
|
|
30
|
-
if (status && status >= 500)
|
|
31
|
-
return "server-error";
|
|
32
|
-
if (isNetworkError(error))
|
|
33
|
-
return "network-error";
|
|
34
|
-
return "unknown";
|
|
15
|
+
return (0, error_classification_1.classifyHttpError)(error);
|
|
35
16
|
}
|
|
36
|
-
/* v8 ignore stop */
|
|
37
17
|
function createGithubCopilotProviderRuntime(model) {
|
|
38
18
|
(0, runtime_1.emitNervesEvent)({
|
|
39
19
|
component: "engine",
|
|
@@ -57,7 +37,6 @@ function createGithubCopilotProviderRuntime(model) {
|
|
|
57
37
|
const client = new openai_1.default({
|
|
58
38
|
apiKey: config.githubToken,
|
|
59
39
|
baseURL: config.baseUrl,
|
|
60
|
-
timeout: 30000,
|
|
61
40
|
maxRetries: 0,
|
|
62
41
|
});
|
|
63
42
|
if (isCompletionsModel) {
|
|
@@ -8,27 +8,9 @@ exports.createMinimaxProviderRuntime = createMinimaxProviderRuntime;
|
|
|
8
8
|
const openai_1 = __importDefault(require("openai"));
|
|
9
9
|
const config_1 = require("../config");
|
|
10
10
|
const runtime_1 = require("../../nerves/runtime");
|
|
11
|
-
|
|
12
|
-
function isNetworkError(error) {
|
|
13
|
-
const code = error.code || "";
|
|
14
|
-
if (["ECONNRESET", "ECONNREFUSED", "ENOTFOUND", "ETIMEDOUT", "EPIPE",
|
|
15
|
-
"EAI_AGAIN", "EHOSTUNREACH", "ENETUNREACH", "ECONNABORTED"].includes(code))
|
|
16
|
-
return true;
|
|
17
|
-
const msg = error.message || "";
|
|
18
|
-
return msg.includes("fetch failed") || msg.includes("socket hang up") || msg.includes("getaddrinfo");
|
|
19
|
-
}
|
|
20
|
-
/* v8 ignore stop */
|
|
11
|
+
const error_classification_1 = require("./error-classification");
|
|
21
12
|
function classifyMinimaxError(error) {
|
|
22
|
-
|
|
23
|
-
if (status === 401 || status === 403)
|
|
24
|
-
return "auth-failure";
|
|
25
|
-
if (status === 429)
|
|
26
|
-
return "rate-limit";
|
|
27
|
-
if (status && status >= 500)
|
|
28
|
-
return "server-error";
|
|
29
|
-
if (isNetworkError(error))
|
|
30
|
-
return "network-error";
|
|
31
|
-
return "unknown";
|
|
13
|
+
return (0, error_classification_1.classifyHttpError)(error);
|
|
32
14
|
}
|
|
33
15
|
const streaming_1 = require("../streaming");
|
|
34
16
|
const model_capabilities_1 = require("../model-capabilities");
|
|
@@ -48,7 +30,6 @@ function createMinimaxProviderRuntime(model) {
|
|
|
48
30
|
const client = new openai_1.default({
|
|
49
31
|
apiKey: minimaxConfig.apiKey,
|
|
50
32
|
baseURL: "https://api.minimaxi.chat/v1",
|
|
51
|
-
timeout: 30000,
|
|
52
33
|
maxRetries: 0,
|
|
53
34
|
});
|
|
54
35
|
return {
|
|
@@ -11,6 +11,7 @@ const identity_1 = require("../identity");
|
|
|
11
11
|
const runtime_1 = require("../../nerves/runtime");
|
|
12
12
|
const streaming_1 = require("../streaming");
|
|
13
13
|
const model_capabilities_1 = require("../model-capabilities");
|
|
14
|
+
const error_classification_1 = require("./error-classification");
|
|
14
15
|
const OPENAI_CODEX_AUTH_FAILURE_MARKERS = [
|
|
15
16
|
"authentication failed",
|
|
16
17
|
"unauthorized",
|
|
@@ -43,43 +44,19 @@ function getOpenAICodexReauthGuidance(reason) {
|
|
|
43
44
|
getOpenAICodexOAuthInstructions(),
|
|
44
45
|
].join("\n");
|
|
45
46
|
}
|
|
46
|
-
/* v8 ignore start -- shared network error utility, tested via classification tests @preserve */
|
|
47
|
-
function isNetworkError(error) {
|
|
48
|
-
const code = error.code || "";
|
|
49
|
-
if (["ECONNRESET", "ECONNREFUSED", "ENOTFOUND", "ETIMEDOUT", "EPIPE",
|
|
50
|
-
"EAI_AGAIN", "EHOSTUNREACH", "ENETUNREACH", "ECONNABORTED"].includes(code))
|
|
51
|
-
return true;
|
|
52
|
-
const msg = error.message || "";
|
|
53
|
-
return msg.includes("fetch failed") || msg.includes("socket hang up") || msg.includes("getaddrinfo");
|
|
54
|
-
}
|
|
55
|
-
/* v8 ignore stop */
|
|
56
47
|
function classifyOpenAICodexError(error) {
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
return "rate-limit";
|
|
65
|
-
}
|
|
66
|
-
if (status && status >= 500)
|
|
67
|
-
return "server-error";
|
|
68
|
-
if (isNetworkError(error))
|
|
69
|
-
return "network-error";
|
|
70
|
-
return "unknown";
|
|
48
|
+
return (0, error_classification_1.classifyHttpError)(error, {
|
|
49
|
+
isAuthFailure: isOpenAICodexAuthFailure,
|
|
50
|
+
isUsageLimit: (e) => {
|
|
51
|
+
const lower = e.message.toLowerCase();
|
|
52
|
+
return lower.includes("usage") || lower.includes("quota") || lower.includes("exceeded your");
|
|
53
|
+
},
|
|
54
|
+
});
|
|
71
55
|
}
|
|
72
|
-
/* v8 ignore start -- auth detection: only called from classifyOpenAICodexError which always passes Error @preserve */
|
|
73
56
|
function isOpenAICodexAuthFailure(error) {
|
|
74
|
-
if (!(error instanceof Error))
|
|
75
|
-
return false;
|
|
76
|
-
const status = error.status;
|
|
77
|
-
if (status === 401 || status === 403)
|
|
78
|
-
return true;
|
|
79
57
|
const lower = error.message.toLowerCase();
|
|
80
58
|
return OPENAI_CODEX_AUTH_FAILURE_MARKERS.some((marker) => lower.includes(marker));
|
|
81
59
|
}
|
|
82
|
-
/* v8 ignore stop */
|
|
83
60
|
function decodeJwtPayload(token) {
|
|
84
61
|
const parts = token.split(".");
|
|
85
62
|
if (parts.length < 2)
|
|
@@ -143,7 +120,6 @@ function createOpenAICodexProviderRuntime(model) {
|
|
|
143
120
|
"OpenAI-Beta": "responses=experimental",
|
|
144
121
|
originator: "ouroboros",
|
|
145
122
|
},
|
|
146
|
-
timeout: 30000,
|
|
147
123
|
maxRetries: 0,
|
|
148
124
|
});
|
|
149
125
|
let nativeInput = null;
|