@alfe.ai/gateway 0.3.2 → 0.4.0
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/bin/gateway.js +1 -1
- package/dist/health.js +413 -13
- package/dist/src/index.d.ts +46 -1
- package/dist/src/index.js +2 -2
- package/package.json +2 -2
package/dist/bin/gateway.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { a as installService, c as uninstallService, i as checkExistingDaemon, n as queryDaemonHealth, r as startDaemon, s as stopExistingDaemon, t as formatHealthReport
|
|
2
|
+
import { S as SOCKET_PATH, a as installService, c as uninstallService, i as checkExistingDaemon, n as queryDaemonHealth, r as startDaemon, s as stopExistingDaemon, t as formatHealthReport } from "../health.js";
|
|
3
3
|
import { t as LOG_FILE } from "../logger.js";
|
|
4
4
|
import { spawn } from "node:child_process";
|
|
5
5
|
//#region bin/gateway.ts
|
package/dist/health.js
CHANGED
|
@@ -5366,14 +5366,30 @@ var NoopRuntimeGate = class {
|
|
|
5366
5366
|
* both places.
|
|
5367
5367
|
*/
|
|
5368
5368
|
const AGENT_DAEMON_SENTRY_DSN = "https://a47f010d8d39fb4350f913492189f283@o4511008239452160.ingest.us.sentry.io/4511679448547328";
|
|
5369
|
+
const CLI_SENTRY_DSN = "https://82dde4631336561f2fcc89d7531623de@o4511008239452160.ingest.us.sentry.io/4511679411191808";
|
|
5370
|
+
/**
|
|
5371
|
+
* Dedicated project for errors emitted BY the agent runtime child process
|
|
5372
|
+
* (OpenClaw/Hermes) — crashes, spawn failures, and error-looking output. Kept
|
|
5373
|
+
* separate from `agent-daemon` so runtime noise never drowns daemon issues.
|
|
5374
|
+
* Mirrors `SENTRY_DSNS.agentRuntime` in `config/config.ts`.
|
|
5375
|
+
*/
|
|
5376
|
+
const AGENT_RUNTIME_SENTRY_DSN = "https://feb7fb2e3b8e723aec518e74715bfa6d@o4511008239452160.ingest.us.sentry.io/4511683667558400";
|
|
5369
5377
|
/** Surface → Sentry project: `cli` and `agent-daemon` respectively. */
|
|
5370
5378
|
const SURFACE_DSNS = {
|
|
5371
|
-
cli:
|
|
5379
|
+
cli: CLI_SENTRY_DSN,
|
|
5372
5380
|
daemon: AGENT_DAEMON_SENTRY_DSN
|
|
5373
5381
|
};
|
|
5374
5382
|
/** Held after a successful init so the capture helpers can run synchronously. */
|
|
5375
5383
|
let sentry = null;
|
|
5376
5384
|
/**
|
|
5385
|
+
* Second client for the `agent-runtime` project (daemon surface only). The
|
|
5386
|
+
* default client keeps the daemon's own DSN; runtime child-process events are
|
|
5387
|
+
* routed here via an explicitly-bound Scope — the documented multi-client
|
|
5388
|
+
* pattern. `null` until `initAgentSentry({ surface: "daemon" })` succeeds.
|
|
5389
|
+
*/
|
|
5390
|
+
let runtimeClient = null;
|
|
5391
|
+
let runtimeScope = null;
|
|
5392
|
+
/**
|
|
5377
5393
|
* Map the configured API URL to a coarse environment tag. Best-effort — returns
|
|
5378
5394
|
* `"unknown"` when no config is present yet (e.g. `alfe login` pre-setup).
|
|
5379
5395
|
*/
|
|
@@ -5404,12 +5420,13 @@ function errorReportingDisabled() {
|
|
|
5404
5420
|
} catch {}
|
|
5405
5421
|
return false;
|
|
5406
5422
|
}
|
|
5407
|
-
const KV_SECRET_RE =
|
|
5423
|
+
const KV_SECRET_RE = /(?<![A-Za-z0-9])([A-Za-z0-9_-]*(?:authorization|api[_-]?key|apikey|token|secret|password|passwd|bearer|credential)s?)(\s*[:=]\s*|\s+)("?)([^\s"']+)\3/gi;
|
|
5424
|
+
const URL_QUERY_SECRET_RE = /([?&](?:[A-Za-z0-9_-]*(?:token|secret|key|signature|credential|password)|sig|code|x-amz-[a-z-]+)=)[^&\s"']+/gi;
|
|
5408
5425
|
const MIN_BARE_SECRET_LEN = 16;
|
|
5409
5426
|
const ALFE_TOKEN_RE = /alfe_(?:dev|test|demo|live)_[A-Za-z0-9._-]+/g;
|
|
5410
5427
|
const BEARER_RE = /\bbearer\s+[A-Za-z0-9._\-+/=]+/gi;
|
|
5411
5428
|
function scrubString(value) {
|
|
5412
|
-
return value.replace(BEARER_RE, "Bearer [REDACTED]").replace(KV_SECRET_RE, (match, label, sep, _q, secret) => {
|
|
5429
|
+
return value.replace(BEARER_RE, "Bearer [REDACTED]").replace(URL_QUERY_SECRET_RE, "$1[REDACTED]").replace(KV_SECRET_RE, (match, label, sep, _q, secret) => {
|
|
5413
5430
|
if (!/[:=]/.test(sep) && secret.length < MIN_BARE_SECRET_LEN) return match;
|
|
5414
5431
|
return `${label}${sep}[REDACTED]`;
|
|
5415
5432
|
}).replace(ALFE_TOKEN_RE, "alfe_[REDACTED]");
|
|
@@ -5453,6 +5470,33 @@ async function initAgentSentry(options) {
|
|
|
5453
5470
|
const dsn = SURFACE_DSNS[options.surface].trim();
|
|
5454
5471
|
if (!dsn) return;
|
|
5455
5472
|
const mod = await import("@sentry/node");
|
|
5473
|
+
if (options.surface === "daemon" && !runtimeScope) try {
|
|
5474
|
+
const dsn = AGENT_RUNTIME_SENTRY_DSN.trim();
|
|
5475
|
+
{
|
|
5476
|
+
const client = new mod.NodeClient({
|
|
5477
|
+
dsn,
|
|
5478
|
+
environment: deriveEnvironment(),
|
|
5479
|
+
...options.release ? { release: options.release } : {},
|
|
5480
|
+
sampleRate: 1,
|
|
5481
|
+
tracesSampleRate: 0,
|
|
5482
|
+
sendDefaultPii: false,
|
|
5483
|
+
transport: mod.makeNodeTransport,
|
|
5484
|
+
stackParser: mod.defaultStackParser,
|
|
5485
|
+
integrations: [],
|
|
5486
|
+
beforeSend: (event) => scrubEvent(event)
|
|
5487
|
+
});
|
|
5488
|
+
client.init();
|
|
5489
|
+
const scope = new mod.Scope();
|
|
5490
|
+
scope.setClient(client);
|
|
5491
|
+
const runtime = deriveRuntime();
|
|
5492
|
+
if (runtime) scope.setTag("runtime", runtime);
|
|
5493
|
+
runtimeClient = client;
|
|
5494
|
+
runtimeScope = scope;
|
|
5495
|
+
}
|
|
5496
|
+
} catch {
|
|
5497
|
+
runtimeClient = null;
|
|
5498
|
+
runtimeScope = null;
|
|
5499
|
+
}
|
|
5456
5500
|
if (mod.getClient()) {
|
|
5457
5501
|
sentry = mod;
|
|
5458
5502
|
return;
|
|
@@ -5464,6 +5508,7 @@ async function initAgentSentry(options) {
|
|
|
5464
5508
|
sampleRate: 1,
|
|
5465
5509
|
tracesSampleRate: 0,
|
|
5466
5510
|
sendDefaultPii: false,
|
|
5511
|
+
...options.surface === "daemon" ? { integrations: (defaults) => defaults.filter((i) => i.name !== "OnUncaughtException" && i.name !== "OnUnhandledRejection") } : {},
|
|
5467
5512
|
beforeSend: (event) => scrubEvent(event),
|
|
5468
5513
|
beforeBreadcrumb: (breadcrumb) => scrubBreadcrumb(breadcrumb)
|
|
5469
5514
|
});
|
|
@@ -5484,6 +5529,10 @@ function setAgentContext(context) {
|
|
|
5484
5529
|
try {
|
|
5485
5530
|
if (context.agentId) sentry.setTag("agentId", context.agentId);
|
|
5486
5531
|
if (context.runtime) sentry.setTag("runtime", context.runtime);
|
|
5532
|
+
if (runtimeScope) {
|
|
5533
|
+
if (context.agentId) runtimeScope.setTag("agentId", context.agentId);
|
|
5534
|
+
if (context.runtime) runtimeScope.setTag("runtime", context.runtime);
|
|
5535
|
+
}
|
|
5487
5536
|
} catch {}
|
|
5488
5537
|
}
|
|
5489
5538
|
/**
|
|
@@ -5528,8 +5577,75 @@ function captureCliFailure(context, cause) {
|
|
|
5528
5577
|
});
|
|
5529
5578
|
} catch {}
|
|
5530
5579
|
}
|
|
5580
|
+
/**
|
|
5581
|
+
* Capture a runtime child-process crash (non-graceful exit) or spawn failure
|
|
5582
|
+
* into the dedicated `agent-runtime` project. Recent output travels as
|
|
5583
|
+
* breadcrumbs — NEVER `extra`, which `scrubEvent` (the runtime client's
|
|
5584
|
+
* `beforeSend`) deletes. Throttling is the caller's job (`CaptureThrottle` in
|
|
5585
|
+
* `runtime-output-monitor.ts`). No-op when the runtime client is not
|
|
5586
|
+
* initialised. Never throws.
|
|
5587
|
+
*/
|
|
5588
|
+
function captureRuntimeCrash(opts) {
|
|
5589
|
+
if (!runtimeScope) return;
|
|
5590
|
+
try {
|
|
5591
|
+
const scope = runtimeScope.clone();
|
|
5592
|
+
for (const { stream, line } of opts.recentOutput) scope.addBreadcrumb({
|
|
5593
|
+
category: `runtime.${stream}`,
|
|
5594
|
+
level: stream === "stderr" ? "warning" : "info",
|
|
5595
|
+
message: line
|
|
5596
|
+
});
|
|
5597
|
+
const crashKey = opts.spawnError ? `spawn:${opts.spawnError.code ?? "error"}` : String(opts.code ?? opts.signal ?? "unknown");
|
|
5598
|
+
scope.setFingerprint([
|
|
5599
|
+
"runtime-crash",
|
|
5600
|
+
opts.runtime,
|
|
5601
|
+
crashKey
|
|
5602
|
+
]);
|
|
5603
|
+
scope.setTags({
|
|
5604
|
+
runtime: opts.runtime,
|
|
5605
|
+
exitCode: String(opts.code),
|
|
5606
|
+
signal: String(opts.signal),
|
|
5607
|
+
crashesSuppressed: String(opts.crashesSuppressed ?? 0)
|
|
5608
|
+
});
|
|
5609
|
+
if (opts.spawnError) scope.captureException(opts.spawnError);
|
|
5610
|
+
else scope.captureMessage(`Runtime ${opts.runtime} crashed (code=${String(opts.code)}, signal=${String(opts.signal)})`, "error");
|
|
5611
|
+
} catch {}
|
|
5612
|
+
}
|
|
5613
|
+
/**
|
|
5614
|
+
* Capture an error-looking block of runtime output (stack trace, Python
|
|
5615
|
+
* traceback, or ERROR-level log line) detected while the child is running.
|
|
5616
|
+
* The block lines travel as breadcrumbs; the head line is the message.
|
|
5617
|
+
* Throttling/dedupe is the caller's job. No-op when Sentry is not initialised.
|
|
5618
|
+
* Never throws.
|
|
5619
|
+
*/
|
|
5620
|
+
function captureRuntimeErrorOutput(opts) {
|
|
5621
|
+
if (!runtimeScope) return;
|
|
5622
|
+
try {
|
|
5623
|
+
const scope = runtimeScope.clone();
|
|
5624
|
+
for (const line of opts.lines) scope.addBreadcrumb({
|
|
5625
|
+
category: `runtime.${opts.stream}`,
|
|
5626
|
+
level: "error",
|
|
5627
|
+
message: line
|
|
5628
|
+
});
|
|
5629
|
+
scope.setFingerprint([
|
|
5630
|
+
"runtime-output",
|
|
5631
|
+
opts.runtime,
|
|
5632
|
+
opts.kind,
|
|
5633
|
+
opts.fingerprintKey
|
|
5634
|
+
]);
|
|
5635
|
+
scope.setTags({
|
|
5636
|
+
runtime: opts.runtime,
|
|
5637
|
+
stream: opts.stream,
|
|
5638
|
+
kind: opts.kind,
|
|
5639
|
+
suppressedCount: String(opts.suppressedCount ?? 0)
|
|
5640
|
+
});
|
|
5641
|
+
scope.captureMessage(opts.lines[0].slice(0, 300), "error");
|
|
5642
|
+
} catch {}
|
|
5643
|
+
}
|
|
5531
5644
|
/** Flush queued events (small timeout) so short-lived commands deliver them. */
|
|
5532
5645
|
async function flushSentry(timeoutMs = 2e3) {
|
|
5646
|
+
try {
|
|
5647
|
+
if (runtimeClient) await runtimeClient.flush(timeoutMs);
|
|
5648
|
+
} catch {}
|
|
5533
5649
|
if (!sentry) return;
|
|
5534
5650
|
try {
|
|
5535
5651
|
await sentry.flush(timeoutMs);
|
|
@@ -21980,6 +22096,177 @@ function createLogger(component, additionalData) {
|
|
|
21980
22096
|
component
|
|
21981
22097
|
});
|
|
21982
22098
|
}
|
|
22099
|
+
const ANSI_RE = /\x1b\[[0-9;]*[A-Za-z]/g;
|
|
22100
|
+
function stripAnsi(line) {
|
|
22101
|
+
return line.replace(ANSI_RE, "");
|
|
22102
|
+
}
|
|
22103
|
+
/** Rolling window of the child's most recent output lines (both streams). */
|
|
22104
|
+
var OutputRingBuffer = class {
|
|
22105
|
+
lines = [];
|
|
22106
|
+
push(line) {
|
|
22107
|
+
this.lines.push({
|
|
22108
|
+
stream: line.stream,
|
|
22109
|
+
line: line.line.length > 500 ? line.line.slice(0, 500) : line.line
|
|
22110
|
+
});
|
|
22111
|
+
if (this.lines.length > 50) this.lines.shift();
|
|
22112
|
+
}
|
|
22113
|
+
/** Oldest → newest copy. */
|
|
22114
|
+
snapshot() {
|
|
22115
|
+
return this.lines.map((l) => ({ ...l }));
|
|
22116
|
+
}
|
|
22117
|
+
};
|
|
22118
|
+
const PY_TRACEBACK_START = "Traceback (most recent call last):";
|
|
22119
|
+
const NODE_ERROR_HEAD = /^\s*(?:Uncaught\s+|Unhandled\w*\s+)?[A-Z][A-Za-z0-9_]*(?:Error|Exception)\b\s*[:(]/;
|
|
22120
|
+
const LOG_ERROR_LEVEL = /^.{0,40}(?:\[(?:ERROR|FATAL|CRITICAL)\]|\b(?:ERROR|FATAL|CRITICAL)\b\s*[:|])/;
|
|
22121
|
+
const JSON_ERROR_LEVEL = /"level"\s*:\s*(?:"(?:error|fatal)"|(?:50|60)\b)/;
|
|
22122
|
+
const NODE_FRAME = /^\s+at\s/;
|
|
22123
|
+
const NODE_CAUSE = /^\s*caused by:?/i;
|
|
22124
|
+
const NODE_CHAINED = /^\s*[A-Z][A-Za-z0-9_]*(?:Error|Exception)\b\s*:/;
|
|
22125
|
+
const PY_FILE = /^\s*File "/;
|
|
22126
|
+
const PY_INDENTED = /^\s{2,}\S/;
|
|
22127
|
+
const PY_TERMINAL = /^[A-Za-z_][\w.]*(?:Error|Exception|Interrupt)?:/;
|
|
22128
|
+
/** Digits/hex-stripped, lowercased head line — stable across timestamps/ids. */
|
|
22129
|
+
function toFingerprintKey(head) {
|
|
22130
|
+
return head.toLowerCase().replace(/\b0x[0-9a-f]+\b/g, "<hex>").replace(/\d+/g, "<n>").trim().slice(0, 120);
|
|
22131
|
+
}
|
|
22132
|
+
function compact(blocks) {
|
|
22133
|
+
return blocks.filter((b) => b !== null);
|
|
22134
|
+
}
|
|
22135
|
+
/**
|
|
22136
|
+
* Stateful multi-line error detector. Feed lines (per stream) as they arrive;
|
|
22137
|
+
* returns the blocks completed by that line (usually zero or one — two when a
|
|
22138
|
+
* line closes a pending block AND is itself a single-line error-log block).
|
|
22139
|
+
* Call `flush()` on quiet periods / process exit to complete a trailing block.
|
|
22140
|
+
*/
|
|
22141
|
+
var ErrorLineDetector = class {
|
|
22142
|
+
pending = null;
|
|
22143
|
+
get isCollecting() {
|
|
22144
|
+
return this.pending !== null;
|
|
22145
|
+
}
|
|
22146
|
+
feed(stream, rawLine) {
|
|
22147
|
+
const line = stripAnsi(rawLine);
|
|
22148
|
+
if (this.pending) {
|
|
22149
|
+
if (stream === this.pending.stream && this.isContinuation(line)) {
|
|
22150
|
+
this.pending.lines.push(line);
|
|
22151
|
+
if (NODE_FRAME.test(line)) this.pending.sawFrame = true;
|
|
22152
|
+
if (this.pending.kind === "python-traceback" && PY_TERMINAL.test(line) && !PY_FILE.test(line) || this.pending.lines.length >= 40) return compact([this.finish()]);
|
|
22153
|
+
return [];
|
|
22154
|
+
}
|
|
22155
|
+
return compact([this.finish(), this.startOrDetect(stream, line)]);
|
|
22156
|
+
}
|
|
22157
|
+
return compact([this.startOrDetect(stream, line)]);
|
|
22158
|
+
}
|
|
22159
|
+
/** Force-complete a pending block (debounce timeout / process exit). */
|
|
22160
|
+
flush() {
|
|
22161
|
+
return this.finish();
|
|
22162
|
+
}
|
|
22163
|
+
startOrDetect(stream, line) {
|
|
22164
|
+
if (line.trim() === "") return null;
|
|
22165
|
+
if (line.startsWith(PY_TRACEBACK_START)) {
|
|
22166
|
+
this.pending = {
|
|
22167
|
+
kind: "python-traceback",
|
|
22168
|
+
stream,
|
|
22169
|
+
lines: [line],
|
|
22170
|
+
sawFrame: true
|
|
22171
|
+
};
|
|
22172
|
+
return null;
|
|
22173
|
+
}
|
|
22174
|
+
if (LOG_ERROR_LEVEL.test(line) || JSON_ERROR_LEVEL.test(line)) return {
|
|
22175
|
+
kind: "error-log",
|
|
22176
|
+
stream,
|
|
22177
|
+
lines: [line.slice(0, 500)],
|
|
22178
|
+
fingerprintKey: toFingerprintKey(line)
|
|
22179
|
+
};
|
|
22180
|
+
if (NODE_ERROR_HEAD.test(line)) {
|
|
22181
|
+
this.pending = {
|
|
22182
|
+
kind: "node-stack",
|
|
22183
|
+
stream,
|
|
22184
|
+
lines: [line],
|
|
22185
|
+
sawFrame: false
|
|
22186
|
+
};
|
|
22187
|
+
return null;
|
|
22188
|
+
}
|
|
22189
|
+
return null;
|
|
22190
|
+
}
|
|
22191
|
+
isContinuation(line) {
|
|
22192
|
+
if (!this.pending) return false;
|
|
22193
|
+
if (this.pending.kind === "python-traceback") return PY_FILE.test(line) || PY_INDENTED.test(line) || PY_TERMINAL.test(line);
|
|
22194
|
+
return NODE_FRAME.test(line) || NODE_CAUSE.test(line) || NODE_CHAINED.test(line);
|
|
22195
|
+
}
|
|
22196
|
+
finish() {
|
|
22197
|
+
const pending = this.pending;
|
|
22198
|
+
this.pending = null;
|
|
22199
|
+
if (!pending) return null;
|
|
22200
|
+
if (pending.kind === "node-stack" && !pending.sawFrame) return null;
|
|
22201
|
+
return {
|
|
22202
|
+
kind: pending.kind,
|
|
22203
|
+
stream: pending.stream,
|
|
22204
|
+
lines: pending.lines.map((l) => l.slice(0, 500)),
|
|
22205
|
+
fingerprintKey: toFingerprintKey(pending.lines[0])
|
|
22206
|
+
};
|
|
22207
|
+
}
|
|
22208
|
+
};
|
|
22209
|
+
/**
|
|
22210
|
+
* Bounds Sentry quota per RuntimeProcess. Worst case per agent:
|
|
22211
|
+
* ≤ `ERROR_CAPTURES_MAX` output events + ≤ 1 crash event per 10-minute window.
|
|
22212
|
+
*/
|
|
22213
|
+
var CaptureThrottle = class {
|
|
22214
|
+
windowStart = 0;
|
|
22215
|
+
windowCount = 0;
|
|
22216
|
+
errorsSuppressed = 0;
|
|
22217
|
+
lastSentByKey = /* @__PURE__ */ new Map();
|
|
22218
|
+
lastCrashCaptureAt = 0;
|
|
22219
|
+
hasCapturedCrash = false;
|
|
22220
|
+
crashesSuppressed = 0;
|
|
22221
|
+
constructor(stableUptimeMs) {
|
|
22222
|
+
this.stableUptimeMs = stableUptimeMs;
|
|
22223
|
+
}
|
|
22224
|
+
allowErrorCapture(fingerprintKey, now = Date.now()) {
|
|
22225
|
+
if (now - this.windowStart >= 6e5) {
|
|
22226
|
+
this.windowStart = now;
|
|
22227
|
+
this.windowCount = 0;
|
|
22228
|
+
}
|
|
22229
|
+
const lastSent = this.lastSentByKey.get(fingerprintKey);
|
|
22230
|
+
if (lastSent !== void 0 && now - lastSent < 6e5 || this.windowCount >= 5) {
|
|
22231
|
+
this.errorsSuppressed++;
|
|
22232
|
+
return {
|
|
22233
|
+
allow: false,
|
|
22234
|
+
suppressedCount: this.errorsSuppressed
|
|
22235
|
+
};
|
|
22236
|
+
}
|
|
22237
|
+
this.windowCount++;
|
|
22238
|
+
this.lastSentByKey.set(fingerprintKey, now);
|
|
22239
|
+
if (this.lastSentByKey.size > 50) {
|
|
22240
|
+
const oldest = this.lastSentByKey.keys().next().value;
|
|
22241
|
+
if (oldest !== void 0) this.lastSentByKey.delete(oldest);
|
|
22242
|
+
}
|
|
22243
|
+
const suppressed = this.errorsSuppressed;
|
|
22244
|
+
this.errorsSuppressed = 0;
|
|
22245
|
+
return {
|
|
22246
|
+
allow: true,
|
|
22247
|
+
suppressedCount: suppressed
|
|
22248
|
+
};
|
|
22249
|
+
}
|
|
22250
|
+
allowCrashCapture(uptimeMs, now = Date.now()) {
|
|
22251
|
+
const stable = uptimeMs >= this.stableUptimeMs;
|
|
22252
|
+
const throttled = this.hasCapturedCrash && now - this.lastCrashCaptureAt < 6e5;
|
|
22253
|
+
if (!stable && throttled) {
|
|
22254
|
+
this.crashesSuppressed++;
|
|
22255
|
+
return {
|
|
22256
|
+
allow: false,
|
|
22257
|
+
suppressedCount: this.crashesSuppressed
|
|
22258
|
+
};
|
|
22259
|
+
}
|
|
22260
|
+
this.hasCapturedCrash = true;
|
|
22261
|
+
this.lastCrashCaptureAt = now;
|
|
22262
|
+
const suppressed = this.crashesSuppressed;
|
|
22263
|
+
this.crashesSuppressed = 0;
|
|
22264
|
+
return {
|
|
22265
|
+
allow: true,
|
|
22266
|
+
suppressedCount: suppressed
|
|
22267
|
+
};
|
|
22268
|
+
}
|
|
22269
|
+
};
|
|
21983
22270
|
//#endregion
|
|
21984
22271
|
//#region src/runtime-process.ts
|
|
21985
22272
|
/**
|
|
@@ -21989,14 +22276,22 @@ function createLogger(component, additionalData) {
|
|
|
21989
22276
|
* and restarts on crash with exponential backoff.
|
|
21990
22277
|
*/
|
|
21991
22278
|
const log$1 = createLogger("RuntimeProcess");
|
|
22279
|
+
/** Quiet-period flush for a pending multi-line error block (e.g. a traceback
|
|
22280
|
+
* followed by silence — no closing line ever arrives to complete it). */
|
|
22281
|
+
const DETECTOR_FLUSH_DEBOUNCE_MS = 1500;
|
|
21992
22282
|
const BACKOFF_INITIAL_MS = 1e3;
|
|
21993
22283
|
const BACKOFF_MAX_MS = 3e4;
|
|
22284
|
+
const STABLE_UPTIME_MS = 6e4;
|
|
21994
22285
|
var RuntimeProcess = class {
|
|
21995
22286
|
child = null;
|
|
21996
22287
|
stopped = false;
|
|
21997
22288
|
backoffMs = BACKOFF_INITIAL_MS;
|
|
21998
22289
|
lastStartTime = 0;
|
|
21999
22290
|
restartTimer = null;
|
|
22291
|
+
ringBuffer = new OutputRingBuffer();
|
|
22292
|
+
detector = new ErrorLineDetector();
|
|
22293
|
+
throttle = new CaptureThrottle(STABLE_UPTIME_MS);
|
|
22294
|
+
detectorFlushTimer = null;
|
|
22000
22295
|
constructor(options) {
|
|
22001
22296
|
this.options = options;
|
|
22002
22297
|
}
|
|
@@ -22047,17 +22342,23 @@ var RuntimeProcess = class {
|
|
|
22047
22342
|
});
|
|
22048
22343
|
this.child.stdout?.on("data", (data) => {
|
|
22049
22344
|
const lines = data.toString().trim().split("\n");
|
|
22050
|
-
for (const line of lines)
|
|
22051
|
-
|
|
22052
|
-
|
|
22053
|
-
|
|
22345
|
+
for (const line of lines) {
|
|
22346
|
+
log$1.info({
|
|
22347
|
+
runtime: this.options.runtime,
|
|
22348
|
+
stream: "stdout"
|
|
22349
|
+
}, line);
|
|
22350
|
+
this.observeLine("stdout", line);
|
|
22351
|
+
}
|
|
22054
22352
|
});
|
|
22055
22353
|
this.child.stderr?.on("data", (data) => {
|
|
22056
22354
|
const lines = data.toString().trim().split("\n");
|
|
22057
|
-
for (const line of lines)
|
|
22058
|
-
|
|
22059
|
-
|
|
22060
|
-
|
|
22355
|
+
for (const line of lines) {
|
|
22356
|
+
log$1.warn({
|
|
22357
|
+
runtime: this.options.runtime,
|
|
22358
|
+
stream: "stderr"
|
|
22359
|
+
}, line);
|
|
22360
|
+
this.observeLine("stderr", line);
|
|
22361
|
+
}
|
|
22061
22362
|
});
|
|
22062
22363
|
this.child.on("exit", (code, signal) => {
|
|
22063
22364
|
this.child = null;
|
|
@@ -22086,7 +22387,26 @@ var RuntimeProcess = class {
|
|
|
22086
22387
|
signal,
|
|
22087
22388
|
backoffMs: this.backoffMs
|
|
22088
22389
|
}, "Runtime crashed — scheduling restart with backoff");
|
|
22089
|
-
|
|
22390
|
+
const uptime = Date.now() - this.lastStartTime;
|
|
22391
|
+
if (this.detectorFlushTimer) {
|
|
22392
|
+
clearTimeout(this.detectorFlushTimer);
|
|
22393
|
+
this.detectorFlushTimer = null;
|
|
22394
|
+
}
|
|
22395
|
+
this.emitErrorBlock(this.detector.flush());
|
|
22396
|
+
const crash = this.throttle.allowCrashCapture(uptime);
|
|
22397
|
+
if (crash.allow) captureRuntimeCrash({
|
|
22398
|
+
runtime: this.options.runtime,
|
|
22399
|
+
code,
|
|
22400
|
+
signal,
|
|
22401
|
+
uptimeMs: uptime,
|
|
22402
|
+
recentOutput: this.ringBuffer.snapshot(),
|
|
22403
|
+
crashesSuppressed: crash.suppressedCount
|
|
22404
|
+
});
|
|
22405
|
+
else log$1.debug({
|
|
22406
|
+
runtime: this.options.runtime,
|
|
22407
|
+
suppressed: crash.suppressedCount
|
|
22408
|
+
}, "Crash capture suppressed by throttle");
|
|
22409
|
+
if (uptime >= 6e4) this.backoffMs = BACKOFF_INITIAL_MS;
|
|
22090
22410
|
this.restartTimer = setTimeout(() => {
|
|
22091
22411
|
this.restartTimer = null;
|
|
22092
22412
|
this.start();
|
|
@@ -22098,6 +22418,66 @@ var RuntimeProcess = class {
|
|
|
22098
22418
|
runtime: this.options.runtime,
|
|
22099
22419
|
err: err.message
|
|
22100
22420
|
}, "Runtime process error");
|
|
22421
|
+
if (this.stopped) return;
|
|
22422
|
+
const crash = this.throttle.allowCrashCapture(Date.now() - this.lastStartTime);
|
|
22423
|
+
if (crash.allow) captureRuntimeCrash({
|
|
22424
|
+
runtime: this.options.runtime,
|
|
22425
|
+
code: null,
|
|
22426
|
+
signal: null,
|
|
22427
|
+
uptimeMs: Date.now() - this.lastStartTime,
|
|
22428
|
+
recentOutput: this.ringBuffer.snapshot(),
|
|
22429
|
+
crashesSuppressed: crash.suppressedCount,
|
|
22430
|
+
spawnError: err
|
|
22431
|
+
});
|
|
22432
|
+
});
|
|
22433
|
+
}
|
|
22434
|
+
/**
|
|
22435
|
+
* Track a child output line for error capture: ring-buffer it (crash
|
|
22436
|
+
* context) and run it through the error-block detector. A pending
|
|
22437
|
+
* multi-line block is flushed after a quiet period so a trailing traceback
|
|
22438
|
+
* isn't held forever.
|
|
22439
|
+
*/
|
|
22440
|
+
observeLine(stream, line) {
|
|
22441
|
+
try {
|
|
22442
|
+
this.ringBuffer.push({
|
|
22443
|
+
stream,
|
|
22444
|
+
line
|
|
22445
|
+
});
|
|
22446
|
+
for (const block of this.detector.feed(stream, line)) this.emitErrorBlock(block);
|
|
22447
|
+
if (this.detectorFlushTimer) {
|
|
22448
|
+
clearTimeout(this.detectorFlushTimer);
|
|
22449
|
+
this.detectorFlushTimer = null;
|
|
22450
|
+
}
|
|
22451
|
+
if (this.detector.isCollecting) this.detectorFlushTimer = setTimeout(() => {
|
|
22452
|
+
this.detectorFlushTimer = null;
|
|
22453
|
+
this.emitErrorBlock(this.detector.flush());
|
|
22454
|
+
}, DETECTOR_FLUSH_DEBOUNCE_MS);
|
|
22455
|
+
} catch (err) {
|
|
22456
|
+
log$1.debug({
|
|
22457
|
+
runtime: this.options.runtime,
|
|
22458
|
+
err: err instanceof Error ? err.message : String(err)
|
|
22459
|
+
}, "Runtime output observation failed");
|
|
22460
|
+
}
|
|
22461
|
+
}
|
|
22462
|
+
/** Report a completed error block to Sentry, throttle permitting. */
|
|
22463
|
+
emitErrorBlock(block) {
|
|
22464
|
+
if (!block) return;
|
|
22465
|
+
const { allow, suppressedCount } = this.throttle.allowErrorCapture(block.fingerprintKey);
|
|
22466
|
+
if (!allow) {
|
|
22467
|
+
log$1.debug({
|
|
22468
|
+
runtime: this.options.runtime,
|
|
22469
|
+
kind: block.kind,
|
|
22470
|
+
suppressed: suppressedCount
|
|
22471
|
+
}, "Runtime error-output capture suppressed by throttle");
|
|
22472
|
+
return;
|
|
22473
|
+
}
|
|
22474
|
+
captureRuntimeErrorOutput({
|
|
22475
|
+
runtime: this.options.runtime,
|
|
22476
|
+
stream: block.stream,
|
|
22477
|
+
kind: block.kind,
|
|
22478
|
+
lines: block.lines,
|
|
22479
|
+
fingerprintKey: block.fingerprintKey,
|
|
22480
|
+
suppressedCount
|
|
22101
22481
|
});
|
|
22102
22482
|
}
|
|
22103
22483
|
/**
|
|
@@ -22109,6 +22489,11 @@ var RuntimeProcess = class {
|
|
|
22109
22489
|
clearTimeout(this.restartTimer);
|
|
22110
22490
|
this.restartTimer = null;
|
|
22111
22491
|
}
|
|
22492
|
+
if (this.detectorFlushTimer) {
|
|
22493
|
+
clearTimeout(this.detectorFlushTimer);
|
|
22494
|
+
this.detectorFlushTimer = null;
|
|
22495
|
+
}
|
|
22496
|
+
this.emitErrorBlock(this.detector.flush());
|
|
22112
22497
|
const child = this.child;
|
|
22113
22498
|
if (!child) return;
|
|
22114
22499
|
return new Promise((resolve) => {
|
|
@@ -22820,6 +23205,21 @@ async function startDaemon() {
|
|
|
22820
23205
|
surface: "daemon",
|
|
22821
23206
|
release: await getCliVersion()
|
|
22822
23207
|
});
|
|
23208
|
+
let fatalExiting = false;
|
|
23209
|
+
process.on("uncaughtException", (err) => {
|
|
23210
|
+
if (fatalExiting) process.exit(1);
|
|
23211
|
+
fatalExiting = true;
|
|
23212
|
+
logger$1.error({
|
|
23213
|
+
err: err.message,
|
|
23214
|
+
stack: err.stack
|
|
23215
|
+
}, "Uncaught exception — daemon exiting");
|
|
23216
|
+
captureFatal(err);
|
|
23217
|
+
flushAndExit(1);
|
|
23218
|
+
});
|
|
23219
|
+
process.on("unhandledRejection", (reason) => {
|
|
23220
|
+
logger$1.error({ reason: reason instanceof Error ? reason.message : String(reason) }, "Unhandled promise rejection");
|
|
23221
|
+
captureFatal(reason instanceof Error ? reason : /* @__PURE__ */ new Error(`Unhandled rejection: ${String(reason)}`));
|
|
23222
|
+
});
|
|
22823
23223
|
if (!managed) {
|
|
22824
23224
|
await mkdir(join(homedir(), ".alfe"), { recursive: true });
|
|
22825
23225
|
const existingPid = await checkExistingDaemon();
|
|
@@ -23807,4 +24207,4 @@ function formatDuration(ms) {
|
|
|
23807
24207
|
return `${String(Math.round(seconds / 3600))}h`;
|
|
23808
24208
|
}
|
|
23809
24209
|
//#endregion
|
|
23810
|
-
export {
|
|
24210
|
+
export { fetchAgentConfig as C, PINNED_OPENCLAW_VERSION as E, SOCKET_PATH as S, resolveAgentIdentity as T, initAgentSentry as _, installService as a, ALFE_DIR as b, uninstallService as c, captureCliFailure as d, captureFatal as f, flushSentry as g, captureRuntimeErrorOutput as h, checkExistingDaemon as i, AGENT_DAEMON_SENTRY_DSN as l, captureRuntimeCrash as m, queryDaemonHealth as n, startService as o, captureIntegrationFailure as p, startDaemon as r, stopExistingDaemon as s, formatHealthReport as t, AGENT_RUNTIME_SENTRY_DSN as u, setAgentContext as v, loadDaemonConfig as w, PID_PATH as x, PROTOCOL_VERSION as y };
|
package/dist/src/index.d.ts
CHANGED
|
@@ -74,6 +74,13 @@ declare const PINNED_OPENCLAW_VERSION = "2026.6.8";
|
|
|
74
74
|
* both places.
|
|
75
75
|
*/
|
|
76
76
|
declare const AGENT_DAEMON_SENTRY_DSN = "https://a47f010d8d39fb4350f913492189f283@o4511008239452160.ingest.us.sentry.io/4511679448547328";
|
|
77
|
+
/**
|
|
78
|
+
* Dedicated project for errors emitted BY the agent runtime child process
|
|
79
|
+
* (OpenClaw/Hermes) — crashes, spawn failures, and error-looking output. Kept
|
|
80
|
+
* separate from `agent-daemon` so runtime noise never drowns daemon issues.
|
|
81
|
+
* Mirrors `SENTRY_DSNS.agentRuntime` in `config/config.ts`.
|
|
82
|
+
*/
|
|
83
|
+
declare const AGENT_RUNTIME_SENTRY_DSN = "https://feb7fb2e3b8e723aec518e74715bfa6d@o4511008239452160.ingest.us.sentry.io/4511683667558400";
|
|
77
84
|
/** Which surface initialised Sentry — tagged on every event. */
|
|
78
85
|
type SentrySurface = "cli" | "daemon";
|
|
79
86
|
interface InitAgentSentryOptions {
|
|
@@ -112,6 +119,44 @@ declare function captureIntegrationFailure(integration: string, phase: string, c
|
|
|
112
119
|
* Sentry is not initialised. Never throws.
|
|
113
120
|
*/
|
|
114
121
|
declare function captureCliFailure(context: string, cause?: unknown): void;
|
|
122
|
+
/** A line of runtime child-process output attached to a crash event. */
|
|
123
|
+
interface RuntimeOutputLine {
|
|
124
|
+
stream: "stdout" | "stderr";
|
|
125
|
+
line: string;
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Capture a runtime child-process crash (non-graceful exit) or spawn failure
|
|
129
|
+
* into the dedicated `agent-runtime` project. Recent output travels as
|
|
130
|
+
* breadcrumbs — NEVER `extra`, which `scrubEvent` (the runtime client's
|
|
131
|
+
* `beforeSend`) deletes. Throttling is the caller's job (`CaptureThrottle` in
|
|
132
|
+
* `runtime-output-monitor.ts`). No-op when the runtime client is not
|
|
133
|
+
* initialised. Never throws.
|
|
134
|
+
*/
|
|
135
|
+
declare function captureRuntimeCrash(opts: {
|
|
136
|
+
runtime: string;
|
|
137
|
+
code: number | null;
|
|
138
|
+
signal: string | null;
|
|
139
|
+
uptimeMs: number;
|
|
140
|
+
recentOutput: readonly RuntimeOutputLine[];
|
|
141
|
+
crashesSuppressed?: number;
|
|
142
|
+
/** Set for the child `error` event path (e.g. ENOENT — binary missing). */
|
|
143
|
+
spawnError?: Error;
|
|
144
|
+
}): void;
|
|
145
|
+
/**
|
|
146
|
+
* Capture an error-looking block of runtime output (stack trace, Python
|
|
147
|
+
* traceback, or ERROR-level log line) detected while the child is running.
|
|
148
|
+
* The block lines travel as breadcrumbs; the head line is the message.
|
|
149
|
+
* Throttling/dedupe is the caller's job. No-op when Sentry is not initialised.
|
|
150
|
+
* Never throws.
|
|
151
|
+
*/
|
|
152
|
+
declare function captureRuntimeErrorOutput(opts: {
|
|
153
|
+
runtime: string;
|
|
154
|
+
stream: "stdout" | "stderr";
|
|
155
|
+
kind: "node-stack" | "python-traceback" | "error-log";
|
|
156
|
+
lines: readonly string[];
|
|
157
|
+
fingerprintKey: string;
|
|
158
|
+
suppressedCount?: number;
|
|
159
|
+
}): void;
|
|
115
160
|
/** Flush queued events (small timeout) so short-lived commands deliver them. */
|
|
116
161
|
declare function flushSentry(timeoutMs?: number): Promise<void>;
|
|
117
162
|
/**
|
|
@@ -293,4 +338,4 @@ declare function checkExistingDaemon(): Promise<number | null>;
|
|
|
293
338
|
*/
|
|
294
339
|
declare function stopExistingDaemon(): Promise<boolean>;
|
|
295
340
|
//#endregion
|
|
296
|
-
export { AGENT_DAEMON_SENTRY_DSN, ALFE_DIR, type AgentIdentity, type AgentWorkspaceConfig, type DaemonConfig, type DaemonHealth, type IPCEvent, type IPCRequest, type IPCResponse, type InitAgentSentryOptions, PID_PATH, PINNED_OPENCLAW_VERSION, PROTOCOL_VERSION, SOCKET_PATH, type SentrySurface, captureCliFailure, captureFatal, captureIntegrationFailure, checkExistingDaemon, fetchAgentConfig, flushSentry, formatHealthReport, initAgentSentry, installService, loadDaemonConfig, logger, queryDaemonHealth, resolveAgentIdentity, setAgentContext, startDaemon, startService, stopExistingDaemon, uninstallService };
|
|
341
|
+
export { AGENT_DAEMON_SENTRY_DSN, AGENT_RUNTIME_SENTRY_DSN, ALFE_DIR, type AgentIdentity, type AgentWorkspaceConfig, type DaemonConfig, type DaemonHealth, type IPCEvent, type IPCRequest, type IPCResponse, type InitAgentSentryOptions, PID_PATH, PINNED_OPENCLAW_VERSION, PROTOCOL_VERSION, type RuntimeOutputLine, SOCKET_PATH, type SentrySurface, captureCliFailure, captureFatal, captureIntegrationFailure, captureRuntimeCrash, captureRuntimeErrorOutput, checkExistingDaemon, fetchAgentConfig, flushSentry, formatHealthReport, initAgentSentry, installService, loadDaemonConfig, logger, queryDaemonHealth, resolveAgentIdentity, setAgentContext, startDaemon, startService, stopExistingDaemon, uninstallService };
|
package/dist/src/index.js
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import { C as PINNED_OPENCLAW_VERSION, S as resolveAgentIdentity, _ as
|
|
1
|
+
import { C as fetchAgentConfig, E as PINNED_OPENCLAW_VERSION, S as SOCKET_PATH, T as resolveAgentIdentity, _ as initAgentSentry, a as installService, b as ALFE_DIR, c as uninstallService, d as captureCliFailure, f as captureFatal, g as flushSentry, h as captureRuntimeErrorOutput, i as checkExistingDaemon, l as AGENT_DAEMON_SENTRY_DSN, m as captureRuntimeCrash, n as queryDaemonHealth, o as startService, p as captureIntegrationFailure, r as startDaemon, s as stopExistingDaemon, t as formatHealthReport, u as AGENT_RUNTIME_SENTRY_DSN, v as setAgentContext, w as loadDaemonConfig, x as PID_PATH, y as PROTOCOL_VERSION } from "../health.js";
|
|
2
2
|
import { n as logger } from "../logger.js";
|
|
3
|
-
export { AGENT_DAEMON_SENTRY_DSN, ALFE_DIR, PID_PATH, PINNED_OPENCLAW_VERSION, PROTOCOL_VERSION, SOCKET_PATH, captureCliFailure, captureFatal, captureIntegrationFailure, checkExistingDaemon, fetchAgentConfig, flushSentry, formatHealthReport, initAgentSentry, installService, loadDaemonConfig, logger, queryDaemonHealth, resolveAgentIdentity, setAgentContext, startDaemon, startService, stopExistingDaemon, uninstallService };
|
|
3
|
+
export { AGENT_DAEMON_SENTRY_DSN, AGENT_RUNTIME_SENTRY_DSN, ALFE_DIR, PID_PATH, PINNED_OPENCLAW_VERSION, PROTOCOL_VERSION, SOCKET_PATH, captureCliFailure, captureFatal, captureIntegrationFailure, captureRuntimeCrash, captureRuntimeErrorOutput, checkExistingDaemon, fetchAgentConfig, flushSentry, formatHealthReport, initAgentSentry, installService, loadDaemonConfig, logger, queryDaemonHealth, resolveAgentIdentity, setAgentContext, startDaemon, startService, stopExistingDaemon, uninstallService };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@alfe.ai/gateway",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Alfe local gateway daemon — persistent control plane for agent integrations",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
"pino-roll": "^1.2.0",
|
|
24
24
|
"smol-toml": ">=1.6.1",
|
|
25
25
|
"ws": "^8.18.0",
|
|
26
|
-
"@alfe.ai/agent-api-client": "^0.
|
|
26
|
+
"@alfe.ai/agent-api-client": "^0.7.0",
|
|
27
27
|
"@alfe.ai/ai-proxy-local": "^0.0.13",
|
|
28
28
|
"@alfe.ai/config": "^0.3.0",
|
|
29
29
|
"@alfe.ai/integration-manifest": "^0.3.1",
|