ocuclaw 1.2.4 → 1.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +21 -6
- package/dist/config/runtime-config.js +84 -3
- package/dist/domain/activity-status-adapter.js +138 -605
- package/dist/domain/activity-status-arbiter.js +109 -0
- package/dist/domain/activity-status-labels.js +906 -0
- package/dist/domain/code-span-regions.js +103 -0
- package/dist/domain/conversation-state.js +14 -1
- package/dist/domain/debug-store.js +56 -182
- package/dist/domain/glasses-ui-content-summary.js +62 -0
- package/dist/domain/glasses-ui-system-prompt.js +28 -0
- package/dist/domain/message-emoji-allowlist.js +16 -0
- package/dist/domain/message-emoji-filter.js +33 -55
- package/dist/domain/neural-emoji-reactor-system-prompt.js +43 -0
- package/dist/domain/neural-emoji-reactor-tag-config.js +56 -0
- package/dist/domain/neural-pace-modulator-system-prompt.js +32 -0
- package/dist/domain/neural-pace-modulator-tag-config.js +51 -0
- package/dist/domain/tagged-span-parser.js +121 -0
- package/dist/domain/tagged-span-strip.js +38 -0
- package/dist/even-ai/even-ai-endpoint.js +91 -0
- package/dist/even-ai/even-ai-run-waiter.js +14 -0
- package/dist/even-ai/even-ai-settings-store.js +14 -0
- package/dist/gateway/gateway-bridge.js +14 -2
- package/dist/gateway/gateway-timing-ledger.js +457 -0
- package/dist/gateway/openclaw-client.js +462 -38
- package/dist/index.js +28 -1
- package/dist/runtime/downstream-handler.js +754 -83
- package/dist/runtime/ocuclaw-settings-store.js +74 -31
- package/dist/runtime/plugin-version-service.js +23 -0
- package/dist/runtime/protocol-adapter.js +9 -0
- package/dist/runtime/provider-usage-select.js +168 -0
- package/dist/runtime/relay-client-nudge-controller.js +553 -0
- package/dist/runtime/relay-core.js +1293 -225
- package/dist/runtime/relay-health-monitor.js +172 -0
- package/dist/runtime/relay-operation-registry.js +263 -0
- package/dist/runtime/relay-service.js +201 -1
- package/dist/runtime/relay-worker-approval-replay-cache.js +68 -0
- package/dist/runtime/relay-worker-entry.js +32 -0
- package/dist/runtime/relay-worker-health.js +272 -0
- package/dist/runtime/relay-worker-protocol.js +281 -0
- package/dist/runtime/relay-worker-queue.js +202 -0
- package/dist/runtime/relay-worker-supervisor.js +1004 -0
- package/dist/runtime/relay-worker-transport.js +1051 -0
- package/dist/runtime/session-context-service.js +189 -0
- package/dist/runtime/session-service.js +638 -27
- package/dist/runtime/upstream-runtime.js +1167 -60
- package/dist/tools/device-info-tool.js +242 -0
- package/dist/tools/glasses-ui-cron.js +427 -0
- package/dist/tools/glasses-ui-descriptors.js +261 -0
- package/dist/tools/glasses-ui-limits.js +21 -0
- package/dist/tools/glasses-ui-paint-floor.js +99 -0
- package/dist/tools/glasses-ui-recipes.js +581 -0
- package/dist/tools/glasses-ui-surfaces.js +278 -0
- package/dist/tools/glasses-ui-template.js +182 -0
- package/dist/tools/glasses-ui-tool.js +1111 -0
- package/dist/tools/session-title-tool.js +209 -0
- package/dist/version.js +2 -0
- package/openclaw.plugin.json +163 -15
- package/package.json +14 -5
- package/skills/glasses-ui/SKILL.md +156 -0
- package/dist/runtime/downstream-server.js +0 -1891
|
@@ -3,6 +3,7 @@ import * as crypto from "node:crypto";
|
|
|
3
3
|
import * as fs from "node:fs";
|
|
4
4
|
import * as path from "node:path";
|
|
5
5
|
import WebSocket from "ws";
|
|
6
|
+
import { createGatewayTimingLedger } from "./gateway-timing-ledger.js";
|
|
6
7
|
|
|
7
8
|
// --- Constants ---
|
|
8
9
|
|
|
@@ -19,9 +20,14 @@ const SCOPES = [
|
|
|
19
20
|
"operator.approvals",
|
|
20
21
|
"operator.admin",
|
|
21
22
|
];
|
|
22
|
-
const
|
|
23
|
+
const MIN_PROTOCOL_VERSION = 3;
|
|
24
|
+
const MAX_PROTOCOL_VERSION = 4;
|
|
23
25
|
const HISTORY_ACTIVITY_POLL_INTERVAL_MS = 500;
|
|
24
26
|
const HISTORY_ACTIVITY_POLL_LIMIT = 40;
|
|
27
|
+
// Per-request ACK timeout: fires only while waiting for the *initial* ack, not
|
|
28
|
+
// during a mid-turn run. Comfortably above normal ack latency but below the
|
|
29
|
+
// ~60s coarse tick-watch run backstop. Disarmed when an accepted ack arrives.
|
|
30
|
+
const RPC_ACK_TIMEOUT_MS = 15000;
|
|
25
31
|
|
|
26
32
|
const THINKING_SUMMARY_KEYS = [
|
|
27
33
|
"summary",
|
|
@@ -411,6 +417,231 @@ function normalizeThinkingSummarySource(rawSource) {
|
|
|
411
417
|
return null;
|
|
412
418
|
}
|
|
413
419
|
|
|
420
|
+
const FAILURE_LABEL_MAX_CHARS = 120;
|
|
421
|
+
const FAILURE_DETAIL_MAX_CHARS = 240;
|
|
422
|
+
const FAILOVER_REASON_ACTIVITY_CODE_MAP = Object.freeze({
|
|
423
|
+
rate_limit: "provider_rate_limited",
|
|
424
|
+
billing: "provider_quota_exhausted",
|
|
425
|
+
auth: "provider_auth_invalid",
|
|
426
|
+
auth_permanent: "provider_auth_invalid",
|
|
427
|
+
overloaded: "provider_unavailable",
|
|
428
|
+
timeout: "provider_timeout",
|
|
429
|
+
format: "provider_request_invalid",
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
function shortText(text, maxChars) {
|
|
433
|
+
if (!text) return "";
|
|
434
|
+
if (text.length <= maxChars) return text;
|
|
435
|
+
if (maxChars <= 3) return ".".repeat(Math.max(maxChars, 0));
|
|
436
|
+
return `${text.slice(0, maxChars - 3)}...`;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
function sanitizeFailureText(rawText, maxChars) {
|
|
440
|
+
if (rawText === undefined || rawText === null) return null;
|
|
441
|
+
let text = String(rawText);
|
|
442
|
+
|
|
443
|
+
text = text.replace(
|
|
444
|
+
new RegExp(`([?&](?:token|access_token|api_key|key|password|secret)=)[^&#\\s]+`, "gi"),
|
|
445
|
+
"$1[redacted]",
|
|
446
|
+
);
|
|
447
|
+
text = text.replace(
|
|
448
|
+
/((?:api[_-]?key|token|password|secret)\s*[=:]\s*)([^,\s"'`]+)/gi,
|
|
449
|
+
"$1[redacted]",
|
|
450
|
+
);
|
|
451
|
+
text = text.replace(/(authorization\s*:\s*bearer\s+)[^\s"'`]+/gi, "$1[redacted]");
|
|
452
|
+
text = text.replace(/\bBearer\s+[A-Za-z0-9._-]{8,}\b/g, "Bearer [redacted]");
|
|
453
|
+
text = text.replace(
|
|
454
|
+
/\b(sk-[A-Za-z0-9]{16,}|ghp_[A-Za-z0-9]{20,}|xox[baprs]-[A-Za-z0-9-]{10,})\b/g,
|
|
455
|
+
"[redacted]",
|
|
456
|
+
);
|
|
457
|
+
text = text.replace(/\s+/g, " ").trim();
|
|
458
|
+
if (!text) return null;
|
|
459
|
+
return shortText(text, maxChars);
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
function normalizeFailureHint(rawHint) {
|
|
463
|
+
if (typeof rawHint !== "string") return "";
|
|
464
|
+
const trimmed = rawHint.trim().toLowerCase();
|
|
465
|
+
if (!trimmed) return "";
|
|
466
|
+
return trimmed.replace(/[\s-]+/g, "_");
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
function mapFailureHintToActivityCode(rawHint) {
|
|
470
|
+
const normalizedHint = normalizeFailureHint(rawHint);
|
|
471
|
+
if (!normalizedHint) return null;
|
|
472
|
+
if (Object.hasOwn(FAILOVER_REASON_ACTIVITY_CODE_MAP, normalizedHint)) {
|
|
473
|
+
return FAILOVER_REASON_ACTIVITY_CODE_MAP[normalizedHint];
|
|
474
|
+
}
|
|
475
|
+
if (
|
|
476
|
+
normalizedHint === "auth_scope" ||
|
|
477
|
+
normalizedHint === "auth_refresh" ||
|
|
478
|
+
normalizedHint === "auth_html_403"
|
|
479
|
+
) {
|
|
480
|
+
return "provider_auth_invalid";
|
|
481
|
+
}
|
|
482
|
+
if (normalizedHint === "proxy") {
|
|
483
|
+
return "provider_unavailable";
|
|
484
|
+
}
|
|
485
|
+
return null;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
function inferFailureHintFromText(rawText) {
|
|
489
|
+
if (typeof rawText !== "string") return null;
|
|
490
|
+
const text = rawText.trim().toLowerCase();
|
|
491
|
+
if (!text) return null;
|
|
492
|
+
|
|
493
|
+
if (
|
|
494
|
+
text.includes("rate_limit") ||
|
|
495
|
+
text.includes("rate limit") ||
|
|
496
|
+
text.includes("rate limited") ||
|
|
497
|
+
text.includes("too many requests") ||
|
|
498
|
+
text.includes("usage limit") ||
|
|
499
|
+
text.includes("organization usage limit")
|
|
500
|
+
) {
|
|
501
|
+
return "rate_limit";
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
if (
|
|
505
|
+
text.includes("out of credits") ||
|
|
506
|
+
text.includes("insufficient credits") ||
|
|
507
|
+
text.includes("insufficient quota") ||
|
|
508
|
+
text.includes("quota exhausted") ||
|
|
509
|
+
text.includes("quota balance") ||
|
|
510
|
+
text.includes("payment required") ||
|
|
511
|
+
text.includes("billing hard limit") ||
|
|
512
|
+
text.includes("credit balance")
|
|
513
|
+
) {
|
|
514
|
+
return "billing";
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
if (
|
|
518
|
+
text.includes("invalid api key") ||
|
|
519
|
+
text.includes("api key invalid") ||
|
|
520
|
+
text.includes("authentication failed") ||
|
|
521
|
+
text.includes("missing scopes") ||
|
|
522
|
+
text.includes("missing scope") ||
|
|
523
|
+
text.includes("invalid_api_key") ||
|
|
524
|
+
text.includes("permission_error") ||
|
|
525
|
+
text.includes("oauth token refresh failed")
|
|
526
|
+
) {
|
|
527
|
+
return "auth";
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
if (text.includes("timed out") || text.includes("timeout")) {
|
|
531
|
+
return "timeout";
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
if (
|
|
535
|
+
text.includes("invalid request") ||
|
|
536
|
+
text.includes("bad request") ||
|
|
537
|
+
text.includes("provider_request_invalid")
|
|
538
|
+
) {
|
|
539
|
+
return "format";
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
if (
|
|
543
|
+
text.includes("service unavailable") ||
|
|
544
|
+
text.includes("provider unavailable") ||
|
|
545
|
+
text.includes("temporarily unavailable") ||
|
|
546
|
+
text.includes("overloaded")
|
|
547
|
+
) {
|
|
548
|
+
return "overloaded";
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
return null;
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
function resolveTerminalErrorCode(source, fallbackCode) {
|
|
555
|
+
const explicitCode = pickTrimmedString(source.code, source.errorCode);
|
|
556
|
+
if (explicitCode) {
|
|
557
|
+
return explicitCode;
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
const structuredHintCode = mapFailureHintToActivityCode(
|
|
561
|
+
pickTrimmedString(source.errorKind, source.failoverReason, source.providerRuntimeFailureKind),
|
|
562
|
+
);
|
|
563
|
+
if (structuredHintCode) {
|
|
564
|
+
return structuredHintCode;
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
const inferredHintCode = mapFailureHintToActivityCode(
|
|
568
|
+
inferFailureHintFromText(
|
|
569
|
+
pickTrimmedString(
|
|
570
|
+
source.detail,
|
|
571
|
+
source.message,
|
|
572
|
+
source.error,
|
|
573
|
+
source.label,
|
|
574
|
+
source.reason,
|
|
575
|
+
),
|
|
576
|
+
),
|
|
577
|
+
);
|
|
578
|
+
return inferredHintCode || fallbackCode || "agent_error";
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
function buildTerminalErrorActivity(data, fallbackRunId, fallbackSessionKey, fallbackCode) {
|
|
582
|
+
const source = isObject(data) ? data : {};
|
|
583
|
+
const code = resolveTerminalErrorCode(source, fallbackCode);
|
|
584
|
+
const labelSource = pickTrimmedString(
|
|
585
|
+
source.label,
|
|
586
|
+
source.title,
|
|
587
|
+
source.summary,
|
|
588
|
+
source.message,
|
|
589
|
+
source.error,
|
|
590
|
+
code,
|
|
591
|
+
"Run failed",
|
|
592
|
+
);
|
|
593
|
+
const detailSource = pickTrimmedString(
|
|
594
|
+
source.detail,
|
|
595
|
+
source.message,
|
|
596
|
+
source.error,
|
|
597
|
+
source.reason,
|
|
598
|
+
source.label,
|
|
599
|
+
code,
|
|
600
|
+
);
|
|
601
|
+
const runId = normalizeRunId(
|
|
602
|
+
pickTrimmedString(source.runId, fallbackRunId),
|
|
603
|
+
);
|
|
604
|
+
const sessionKey = normalizeSessionKey(
|
|
605
|
+
pickTrimmedString(source.sessionKey, fallbackSessionKey),
|
|
606
|
+
);
|
|
607
|
+
const activity = {
|
|
608
|
+
state: "idle",
|
|
609
|
+
sessionKey,
|
|
610
|
+
runId,
|
|
611
|
+
origin: "lifecycle",
|
|
612
|
+
phase: "error",
|
|
613
|
+
isError: true,
|
|
614
|
+
code,
|
|
615
|
+
};
|
|
616
|
+
const label = sanitizeFailureText(labelSource, FAILURE_LABEL_MAX_CHARS);
|
|
617
|
+
const detail = sanitizeFailureText(detailSource, FAILURE_DETAIL_MAX_CHARS);
|
|
618
|
+
if (label) activity.label = label;
|
|
619
|
+
if (detail) activity.detail = detail;
|
|
620
|
+
else if (label) activity.detail = label;
|
|
621
|
+
// failoverReason is set by the runtime when this terminal error is about to
|
|
622
|
+
// trigger a profile rotation or fallback retry. Surface it as a hint so the
|
|
623
|
+
// WebUI can suppress sticky failure feedback for runs that will retry on
|
|
624
|
+
// another auth profile or model rather than ending the user-visible turn.
|
|
625
|
+
if (pickTrimmedString(source.failoverReason)) {
|
|
626
|
+
activity.failoverPending = true;
|
|
627
|
+
}
|
|
628
|
+
return activity;
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
function buildStructuredError(data, fallbackMessage, fallbackCode) {
|
|
632
|
+
const source = isObject(data) ? data : {};
|
|
633
|
+
const error = new Error(
|
|
634
|
+
pickTrimmedString(source.message, source.error, fallbackMessage) || fallbackMessage,
|
|
635
|
+
);
|
|
636
|
+
const code = pickTrimmedString(source.code, source.errorCode, fallbackCode) || fallbackCode;
|
|
637
|
+
const requestId = pickTrimmedString(source.requestId);
|
|
638
|
+
const op = pickTrimmedString(source.op);
|
|
639
|
+
if (code) error.code = code;
|
|
640
|
+
if (requestId) error.requestId = requestId;
|
|
641
|
+
if (op) error.op = op;
|
|
642
|
+
return error;
|
|
643
|
+
}
|
|
644
|
+
|
|
414
645
|
function selectThinkingDisplayLabel({
|
|
415
646
|
summaryText,
|
|
416
647
|
boldLabelCandidate,
|
|
@@ -477,6 +708,7 @@ function parseThinkingSignatureId(rawSignature) {
|
|
|
477
708
|
|
|
478
709
|
function extractThinkingPayload(raw) {
|
|
479
710
|
if (!isObject(raw)) return null;
|
|
711
|
+
if (raw.redacted === true || raw.type === "redacted_thinking") return null;
|
|
480
712
|
const summaryEntry = pickFirstStringEntry(raw, THINKING_SUMMARY_KEYS);
|
|
481
713
|
const detailEntry = pickFirstStringEntry(raw, THINKING_DETAIL_KEYS);
|
|
482
714
|
const summaryText = normalizeThinkingText(summaryEntry ? summaryEntry.value : null);
|
|
@@ -544,6 +776,11 @@ class OpenClawClient extends EventEmitter {
|
|
|
544
776
|
constructor(opts = {}) {
|
|
545
777
|
super();
|
|
546
778
|
this._logger = normalizeLogger(opts.logger);
|
|
779
|
+
this._timingLedger = createGatewayTimingLedger({
|
|
780
|
+
logger: this._logger,
|
|
781
|
+
now: () => Date.now(),
|
|
782
|
+
emitTiming: (event) => this.emit("timing", event),
|
|
783
|
+
});
|
|
547
784
|
this._gatewayUrl = pickTrimmedString(opts.gatewayUrl);
|
|
548
785
|
this._gatewayToken = pickTrimmedString(opts.gatewayToken);
|
|
549
786
|
this._persistencePaths = resolvePersistencePaths(opts.stateDir);
|
|
@@ -570,7 +807,7 @@ class OpenClawClient extends EventEmitter {
|
|
|
570
807
|
this._activeRunSessionKey = null;
|
|
571
808
|
this._activeRunStartedAtMs = null;
|
|
572
809
|
this._activeRunGeneration = 0;
|
|
573
|
-
this._runTextBuffer = ""; //
|
|
810
|
+
this._runTextBuffer = ""; // accumulated assistant text for current run
|
|
574
811
|
|
|
575
812
|
// --- Agent identity (step 6) ---
|
|
576
813
|
this._agentIdentity = null;
|
|
@@ -589,6 +826,7 @@ class OpenClawClient extends EventEmitter {
|
|
|
589
826
|
|
|
590
827
|
setLogger(logger) {
|
|
591
828
|
this._logger = normalizeLogger(logger);
|
|
829
|
+
this._timingLedger.setLogger(this._logger);
|
|
592
830
|
}
|
|
593
831
|
|
|
594
832
|
/**
|
|
@@ -630,6 +868,7 @@ class OpenClawClient extends EventEmitter {
|
|
|
630
868
|
clearTimeout(this._reconnectTimer);
|
|
631
869
|
this._reconnectTimer = null;
|
|
632
870
|
}
|
|
871
|
+
this._timingLedger.clear("stop");
|
|
633
872
|
this._invalidateActiveRun();
|
|
634
873
|
this._stopTickWatch();
|
|
635
874
|
if (this._ws) {
|
|
@@ -654,10 +893,37 @@ class OpenClawClient extends EventEmitter {
|
|
|
654
893
|
const id = crypto.randomUUID();
|
|
655
894
|
const frame = { type: "req", id, method, params };
|
|
656
895
|
const expectFinal = opts && opts.expectFinal === true;
|
|
896
|
+
const diagnostic = opts && opts.diagnostic;
|
|
657
897
|
const promise = new Promise((resolve, reject) => {
|
|
658
|
-
this._pending.set(id, { resolve, reject, expectFinal });
|
|
898
|
+
this._pending.set(id, { resolve, reject, expectFinal, method, diagnostic });
|
|
659
899
|
});
|
|
900
|
+
// Per-request ACK timeout: reject if the initial ack never arrives. Disarmed
|
|
901
|
+
// when an accepted ack arrives (keepPending branch) so a legitimately
|
|
902
|
+
// long-running mid-turn run is NOT killed by this timeout.
|
|
903
|
+
const timer = setTimeout(() => {
|
|
904
|
+
const pendingEntry = this._pending.get(id);
|
|
905
|
+
if (!pendingEntry) return;
|
|
906
|
+
this._pending.delete(id);
|
|
907
|
+
const err = new Error("rpc ack timeout");
|
|
908
|
+
err.code = "rpc_timeout";
|
|
909
|
+
err.retryable = true;
|
|
910
|
+
pendingEntry.reject(err);
|
|
911
|
+
}, RPC_ACK_TIMEOUT_MS);
|
|
912
|
+
if (timer.unref) {
|
|
913
|
+
timer.unref();
|
|
914
|
+
}
|
|
915
|
+
const pendingForTimer = this._pending.get(id);
|
|
916
|
+
if (pendingForTimer) {
|
|
917
|
+
pendingForTimer.timer = timer;
|
|
918
|
+
}
|
|
660
919
|
const raw = JSON.stringify(frame);
|
|
920
|
+
this._timingLedger.recordRequestSent({
|
|
921
|
+
requestId: id,
|
|
922
|
+
method,
|
|
923
|
+
params,
|
|
924
|
+
expectFinal,
|
|
925
|
+
diagnostic,
|
|
926
|
+
});
|
|
661
927
|
this.emit("protocol", { direction: "out", frame });
|
|
662
928
|
this._ws.send(raw);
|
|
663
929
|
return promise;
|
|
@@ -728,13 +994,17 @@ class OpenClawClient extends EventEmitter {
|
|
|
728
994
|
}
|
|
729
995
|
|
|
730
996
|
/**
|
|
731
|
-
* Resolve an
|
|
997
|
+
* Resolve an approval request.
|
|
732
998
|
* @param {string} id - Approval request ID
|
|
733
999
|
* @param {string} decision - "allow-once", "allow-always", or "deny"
|
|
734
1000
|
* @returns {Promise}
|
|
735
1001
|
*/
|
|
736
1002
|
resolveApproval(id, decision) {
|
|
737
|
-
|
|
1003
|
+
const method =
|
|
1004
|
+
typeof id === "string" && id.startsWith("plugin:")
|
|
1005
|
+
? "plugin.approval.resolve"
|
|
1006
|
+
: "exec.approval.resolve";
|
|
1007
|
+
return this.request(method, { id, decision });
|
|
738
1008
|
}
|
|
739
1009
|
|
|
740
1010
|
_beginActiveRun(runId, sessionKey) {
|
|
@@ -791,6 +1061,7 @@ class OpenClawClient extends EventEmitter {
|
|
|
791
1061
|
this._connectSent = false;
|
|
792
1062
|
|
|
793
1063
|
// Reset per-connection state
|
|
1064
|
+
this._timingLedger.clear("connect_reset");
|
|
794
1065
|
this._lastSeq = null;
|
|
795
1066
|
this._lastTick = null;
|
|
796
1067
|
this._historyResolved = false;
|
|
@@ -819,6 +1090,7 @@ class OpenClawClient extends EventEmitter {
|
|
|
819
1090
|
this._ws = null;
|
|
820
1091
|
this._stopTickWatch();
|
|
821
1092
|
this._stopHistoryActivityPolling();
|
|
1093
|
+
this._timingLedger.clear("disconnect");
|
|
822
1094
|
this._flushPendingErrors(new Error(`gateway closed (${code}): ${reasonText}`));
|
|
823
1095
|
this.emit("disconnected", { code, reason: reasonText });
|
|
824
1096
|
this.emit("status", "disconnected");
|
|
@@ -865,7 +1137,23 @@ class OpenClawClient extends EventEmitter {
|
|
|
865
1137
|
// If expectFinal, skip intermediate acks (status: "accepted") (step 4)
|
|
866
1138
|
const payload = parsed.payload;
|
|
867
1139
|
const status = payload && payload.status;
|
|
868
|
-
|
|
1140
|
+
const keepPending =
|
|
1141
|
+
pending.expectFinal && parsed.ok === true && status === "accepted";
|
|
1142
|
+
this._timingLedger.recordResponseReceived({
|
|
1143
|
+
requestId: parsed.id,
|
|
1144
|
+
ok: parsed.ok === true,
|
|
1145
|
+
payload: parsed.payload,
|
|
1146
|
+
response: parsed.payload,
|
|
1147
|
+
error: parsed.error,
|
|
1148
|
+
keepPending,
|
|
1149
|
+
});
|
|
1150
|
+
if (keepPending) {
|
|
1151
|
+
// The accepted ack arrived: the run is legitimately long-running, so
|
|
1152
|
+
// disarm the ACK timeout (the coarse tick-watch remains the run backstop).
|
|
1153
|
+
if (pending.timer) {
|
|
1154
|
+
clearTimeout(pending.timer);
|
|
1155
|
+
pending.timer = null;
|
|
1156
|
+
}
|
|
869
1157
|
// Track the runId from the ack
|
|
870
1158
|
if (payload.runId) {
|
|
871
1159
|
this._activeRunId = payload.runId;
|
|
@@ -874,6 +1162,11 @@ class OpenClawClient extends EventEmitter {
|
|
|
874
1162
|
return; // Keep the pending entry, wait for final response
|
|
875
1163
|
}
|
|
876
1164
|
|
|
1165
|
+
// Final settle: clear the ACK timeout before resolving/rejecting.
|
|
1166
|
+
if (pending.timer) {
|
|
1167
|
+
clearTimeout(pending.timer);
|
|
1168
|
+
pending.timer = null;
|
|
1169
|
+
}
|
|
877
1170
|
this._pending.delete(parsed.id);
|
|
878
1171
|
if (parsed.ok) {
|
|
879
1172
|
pending.resolve(parsed.payload);
|
|
@@ -951,7 +1244,7 @@ class OpenClawClient extends EventEmitter {
|
|
|
951
1244
|
return;
|
|
952
1245
|
}
|
|
953
1246
|
|
|
954
|
-
// ---
|
|
1247
|
+
// --- Approval events ---
|
|
955
1248
|
if (evt.event === "exec.approval.requested") {
|
|
956
1249
|
this.emit("approval", evt.payload);
|
|
957
1250
|
return;
|
|
@@ -962,11 +1255,57 @@ class OpenClawClient extends EventEmitter {
|
|
|
962
1255
|
return;
|
|
963
1256
|
}
|
|
964
1257
|
|
|
1258
|
+
if (evt.event === "plugin.approval.requested") {
|
|
1259
|
+
this.emit("approval", { ...(evt.payload || {}), approvalKind: "plugin" });
|
|
1260
|
+
return;
|
|
1261
|
+
}
|
|
1262
|
+
|
|
1263
|
+
if (evt.event === "plugin.approval.resolved") {
|
|
1264
|
+
this.emit("approvalResolved", { ...(evt.payload || {}), approvalKind: "plugin" });
|
|
1265
|
+
return;
|
|
1266
|
+
}
|
|
1267
|
+
|
|
965
1268
|
// --- Agent events (step 5) ---
|
|
966
1269
|
if (evt.event === "agent") {
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
1270
|
+
const payload = evt.payload || {};
|
|
1271
|
+
const data = payload.data || {};
|
|
1272
|
+
this._timingLedger.recordGatewayEventReceived({
|
|
1273
|
+
eventName: evt.event,
|
|
1274
|
+
payload: evt.payload,
|
|
1275
|
+
kind: evt.event,
|
|
1276
|
+
runId: payload.runId,
|
|
1277
|
+
stream: payload.stream,
|
|
1278
|
+
phase: data.phase,
|
|
1279
|
+
data,
|
|
1280
|
+
});
|
|
1281
|
+
// History-gate decouple (F18, history half): only the run-end COMMIT
|
|
1282
|
+
// mutates the persistent message list downstream (lifecycle:end ->
|
|
1283
|
+
// emit("message") -> conversationState.addMessage, a blind push), and the
|
|
1284
|
+
// history hydrate is a DESTRUCTIVE replace-all (conversation-state.ts
|
|
1285
|
+
// hydrate()). To avoid the commit being clobbered/duplicated by a later
|
|
1286
|
+
// hydrate, the commit must still run AFTER history resolves. Every other
|
|
1287
|
+
// agent event (lifecycle:start, assistant/streaming, tool, error) emits
|
|
1288
|
+
// only transient/live effects (activity/streaming/error) that hydrate
|
|
1289
|
+
// never touches, so processing them immediately is safe and removes the
|
|
1290
|
+
// reconnect responsiveness delay. The intact commit event is queued and
|
|
1291
|
+
// replayed via _drainEventQueue, preserving the de-dup contract exactly.
|
|
1292
|
+
const isCommitEvent = data.phase === "end" && payload.stream === "lifecycle";
|
|
1293
|
+
if (isCommitEvent && !this._historyResolved) {
|
|
1294
|
+
// Snapshot the commit-relevant LIVE instance state at enqueue time. The
|
|
1295
|
+
// lifecycle:end branch reads _runTextBuffer / _activeRunId / _gapDuringRun
|
|
1296
|
+
// (and _activeRunSessionKey as the session-key fallback) — all of which a
|
|
1297
|
+
// LATER run's lifecycle:start (_beginActiveRun) or an _invalidateActiveRun
|
|
1298
|
+
// mutates before the queue drains. Capturing now keeps the deferred commit
|
|
1299
|
+
// contemporaneous with its own run, restoring pre-F18 replay semantics.
|
|
1300
|
+
this._eventQueue.push({
|
|
1301
|
+
payload: evt.payload,
|
|
1302
|
+
capturedCommit: {
|
|
1303
|
+
fullText: this._runTextBuffer,
|
|
1304
|
+
activeRunId: this._activeRunId,
|
|
1305
|
+
activeRunSessionKey: this._activeRunSessionKey,
|
|
1306
|
+
gapDuringRun: this._gapDuringRun,
|
|
1307
|
+
},
|
|
1308
|
+
});
|
|
970
1309
|
return;
|
|
971
1310
|
}
|
|
972
1311
|
this._handleAgentEvent(evt.payload);
|
|
@@ -980,7 +1319,7 @@ class OpenClawClient extends EventEmitter {
|
|
|
980
1319
|
* Handle an agent event payload.
|
|
981
1320
|
* Buffers assistant text deltas, emits activity/message events.
|
|
982
1321
|
*/
|
|
983
|
-
_handleAgentEvent(payload) {
|
|
1322
|
+
_handleAgentEvent(payload, capturedCommit) {
|
|
984
1323
|
if (!payload) return;
|
|
985
1324
|
|
|
986
1325
|
const { runId, stream, data } = payload;
|
|
@@ -988,7 +1327,7 @@ class OpenClawClient extends EventEmitter {
|
|
|
988
1327
|
|
|
989
1328
|
switch (stream) {
|
|
990
1329
|
case "lifecycle":
|
|
991
|
-
this._handleLifecycleEvent(runId, data, payload.sessionKey);
|
|
1330
|
+
this._handleLifecycleEvent(runId, data, payload.sessionKey, capturedCommit);
|
|
992
1331
|
break;
|
|
993
1332
|
case "assistant":
|
|
994
1333
|
this._handleAssistantEvent(runId, data);
|
|
@@ -998,14 +1337,32 @@ class OpenClawClient extends EventEmitter {
|
|
|
998
1337
|
break;
|
|
999
1338
|
case "error":
|
|
1000
1339
|
this._logger.error(`[openclaw] Agent error: ${JSON.stringify(data)}`);
|
|
1001
|
-
|
|
1340
|
+
{
|
|
1341
|
+
const terminalActivity = buildTerminalErrorActivity(
|
|
1342
|
+
data,
|
|
1343
|
+
runId || this._activeRunId,
|
|
1344
|
+
payload.sessionKey || this._activeRunSessionKey,
|
|
1345
|
+
"agent_error",
|
|
1346
|
+
);
|
|
1347
|
+
if (terminalActivity.runId && terminalActivity.sessionKey) {
|
|
1348
|
+
this.emit("activity", terminalActivity);
|
|
1349
|
+
this._timingLedger.recordRunTerminal({
|
|
1350
|
+
runId: terminalActivity.runId,
|
|
1351
|
+
});
|
|
1352
|
+
this._invalidateActiveRun();
|
|
1353
|
+
}
|
|
1354
|
+
this.emit(
|
|
1355
|
+
"error",
|
|
1356
|
+
buildStructuredError(data, "agent error", terminalActivity.code || "agent_error"),
|
|
1357
|
+
);
|
|
1358
|
+
}
|
|
1002
1359
|
break;
|
|
1003
1360
|
default:
|
|
1004
1361
|
break;
|
|
1005
1362
|
}
|
|
1006
1363
|
}
|
|
1007
1364
|
|
|
1008
|
-
_handleLifecycleEvent(runId, data, sessionKey) {
|
|
1365
|
+
_handleLifecycleEvent(runId, data, sessionKey, capturedCommit) {
|
|
1009
1366
|
switch (data.phase) {
|
|
1010
1367
|
case "start":
|
|
1011
1368
|
this._beginActiveRun(runId, sessionKey);
|
|
@@ -1021,18 +1378,48 @@ class OpenClawClient extends EventEmitter {
|
|
|
1021
1378
|
break;
|
|
1022
1379
|
|
|
1023
1380
|
case "end": {
|
|
1381
|
+
// Prefer the snapshot captured at history-gate enqueue time (F18 commit
|
|
1382
|
+
// defer). When the end event is processed IMMEDIATELY (history already
|
|
1383
|
+
// resolved), capturedCommit is undefined and live instance state is read
|
|
1384
|
+
// exactly as before — byte-for-byte unchanged. When DEFERRED, a later
|
|
1385
|
+
// run's start (or an invalidate) may have already clobbered these shared
|
|
1386
|
+
// fields, so the snapshot keeps the commit contemporaneous with its run.
|
|
1387
|
+
const committedActiveRunId = capturedCommit
|
|
1388
|
+
? capturedCommit.activeRunId
|
|
1389
|
+
: this._activeRunId;
|
|
1390
|
+
const committedActiveRunSessionKey = capturedCommit
|
|
1391
|
+
? capturedCommit.activeRunSessionKey
|
|
1392
|
+
: this._activeRunSessionKey;
|
|
1024
1393
|
// Assemble full response from buffered text
|
|
1025
|
-
const fullText = this._runTextBuffer;
|
|
1026
|
-
const completedRunId = normalizeRunId(
|
|
1394
|
+
const fullText = capturedCommit ? capturedCommit.fullText : this._runTextBuffer;
|
|
1395
|
+
const completedRunId = normalizeRunId(committedActiveRunId) || normalizeRunId(runId);
|
|
1027
1396
|
const completedSessionKey =
|
|
1028
1397
|
normalizeSessionKey(sessionKey) ||
|
|
1029
|
-
normalizeSessionKey(
|
|
1398
|
+
normalizeSessionKey(committedActiveRunSessionKey) ||
|
|
1030
1399
|
null;
|
|
1031
|
-
const gapDuringRun = this._gapDuringRun;
|
|
1400
|
+
const gapDuringRun = capturedCommit ? capturedCommit.gapDuringRun : this._gapDuringRun;
|
|
1032
1401
|
|
|
1033
1402
|
// Invalidate run state before emitting terminal idle so late history polls
|
|
1034
1403
|
// cannot reopen thinking for a completed run.
|
|
1035
|
-
this.
|
|
1404
|
+
this._timingLedger.recordRunTerminal({
|
|
1405
|
+
runId: completedRunId,
|
|
1406
|
+
});
|
|
1407
|
+
// Reconnect-window guard (F18 successor-run protection): a DEFERRED
|
|
1408
|
+
// commit (capturedCommit present) drains AFTER its run ended, and a
|
|
1409
|
+
// LATER run's lifecycle:start may already have become the live active
|
|
1410
|
+
// run (set _activeRunId, armed history polling, started buffering its
|
|
1411
|
+
// own text) before drain. An unconditional invalidate here would tear
|
|
1412
|
+
// down that live successor — stop its thinking-summary polling, null
|
|
1413
|
+
// its _activeRunId, and clear its _runTextBuffer — wiping state that
|
|
1414
|
+
// belongs to a run that has NOT ended. Only invalidate when this commit
|
|
1415
|
+
// IS the live active run (its own run, or a non-deferred/live commit
|
|
1416
|
+
// where the snapshot is absent). When _activeRunId is already null there
|
|
1417
|
+
// is nothing live to protect, so skipping the invalidate is a no-op.
|
|
1418
|
+
const commitIsLiveActiveRun =
|
|
1419
|
+
normalizeRunId(this._activeRunId) === normalizeRunId(committedActiveRunId);
|
|
1420
|
+
if (!capturedCommit || commitIsLiveActiveRun) {
|
|
1421
|
+
this._invalidateActiveRun();
|
|
1422
|
+
}
|
|
1036
1423
|
|
|
1037
1424
|
this.emit("message", {
|
|
1038
1425
|
runId: completedRunId,
|
|
@@ -1066,20 +1453,27 @@ class OpenClawClient extends EventEmitter {
|
|
|
1066
1453
|
case "error":
|
|
1067
1454
|
this._logger.error(`[openclaw] Agent lifecycle error: ${JSON.stringify(data)}`);
|
|
1068
1455
|
{
|
|
1069
|
-
const
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
runId:
|
|
1080
|
-
origin: "lifecycle",
|
|
1081
|
-
phase: "error",
|
|
1456
|
+
const terminalActivity = buildTerminalErrorActivity(
|
|
1457
|
+
data,
|
|
1458
|
+
runId || this._activeRunId,
|
|
1459
|
+
sessionKey || this._activeRunSessionKey,
|
|
1460
|
+
"agent_lifecycle_error",
|
|
1461
|
+
);
|
|
1462
|
+
if (terminalActivity.runId && terminalActivity.sessionKey) {
|
|
1463
|
+
this.emit("activity", terminalActivity);
|
|
1464
|
+
}
|
|
1465
|
+
this._timingLedger.recordRunTerminal({
|
|
1466
|
+
runId: terminalActivity.runId,
|
|
1082
1467
|
});
|
|
1468
|
+
this._invalidateActiveRun();
|
|
1469
|
+
this.emit(
|
|
1470
|
+
"error",
|
|
1471
|
+
buildStructuredError(
|
|
1472
|
+
data,
|
|
1473
|
+
"agent lifecycle error",
|
|
1474
|
+
terminalActivity.code || "agent_lifecycle_error",
|
|
1475
|
+
),
|
|
1476
|
+
);
|
|
1083
1477
|
}
|
|
1084
1478
|
break;
|
|
1085
1479
|
|
|
@@ -1098,11 +1492,20 @@ class OpenClawClient extends EventEmitter {
|
|
|
1098
1492
|
|
|
1099
1493
|
// Gateway sends accumulated text (full text so far), not deltas
|
|
1100
1494
|
if (typeof data.text === "string") {
|
|
1495
|
+
const previousTextLength = this._runTextBuffer.length;
|
|
1496
|
+
const gatewayReceivedAtMs = Date.now();
|
|
1497
|
+
const rawAssistantChars = data.text.length;
|
|
1498
|
+
const assistantDeltaChars = Math.max(0, rawAssistantChars - previousTextLength);
|
|
1499
|
+
const firstGatewayChunk = previousTextLength <= 0;
|
|
1101
1500
|
this._runTextBuffer = data.text;
|
|
1102
1501
|
this.emit("streaming", {
|
|
1103
1502
|
text: data.text,
|
|
1104
1503
|
sessionKey: this._activeRunSessionKey,
|
|
1105
1504
|
runId: runId || this._activeRunId || null,
|
|
1505
|
+
gatewayReceivedAtMs,
|
|
1506
|
+
rawAssistantChars,
|
|
1507
|
+
assistantDeltaChars,
|
|
1508
|
+
firstGatewayChunk,
|
|
1106
1509
|
});
|
|
1107
1510
|
}
|
|
1108
1511
|
}
|
|
@@ -1130,7 +1533,9 @@ class OpenClawClient extends EventEmitter {
|
|
|
1130
1533
|
if (args) activity.args = args;
|
|
1131
1534
|
if (path) activity.path = path;
|
|
1132
1535
|
if (typeof data.toolCallId === "string" && data.toolCallId.trim()) {
|
|
1133
|
-
|
|
1536
|
+
const trimmedToolCallId = data.toolCallId.trim();
|
|
1537
|
+
activity.activityId = trimmedToolCallId;
|
|
1538
|
+
activity.toolCallId = trimmedToolCallId;
|
|
1134
1539
|
}
|
|
1135
1540
|
if (Number.isFinite(data.seq)) {
|
|
1136
1541
|
activity.seq = Math.floor(data.seq);
|
|
@@ -1155,7 +1560,8 @@ class OpenClawClient extends EventEmitter {
|
|
|
1155
1560
|
});
|
|
1156
1561
|
};
|
|
1157
1562
|
|
|
1158
|
-
poll
|
|
1563
|
+
// Let setInterval fire the first poll one interval out so the initial
|
|
1564
|
+
// chat.history fetch doesn't contend with the first streaming chunk landing.
|
|
1159
1565
|
this._historyActivityPollTimer = setInterval(
|
|
1160
1566
|
poll,
|
|
1161
1567
|
HISTORY_ACTIVITY_POLL_INTERVAL_MS,
|
|
@@ -1300,6 +1706,7 @@ class OpenClawClient extends EventEmitter {
|
|
|
1300
1706
|
summary: extracted.label,
|
|
1301
1707
|
thinking: extracted.detail,
|
|
1302
1708
|
thinkingSummarySource: extracted.thinkingSummarySource,
|
|
1709
|
+
thinkingSignatureId: extracted.signatureId || null,
|
|
1303
1710
|
});
|
|
1304
1711
|
}
|
|
1305
1712
|
|
|
@@ -1344,8 +1751,8 @@ class OpenClawClient extends EventEmitter {
|
|
|
1344
1751
|
|
|
1345
1752
|
// Build connect request params
|
|
1346
1753
|
const params = {
|
|
1347
|
-
minProtocol:
|
|
1348
|
-
maxProtocol:
|
|
1754
|
+
minProtocol: MIN_PROTOCOL_VERSION,
|
|
1755
|
+
maxProtocol: MAX_PROTOCOL_VERSION,
|
|
1349
1756
|
client: {
|
|
1350
1757
|
id: CLIENT_ID,
|
|
1351
1758
|
version: CLIENT_VERSION,
|
|
@@ -1480,7 +1887,7 @@ class OpenClawClient extends EventEmitter {
|
|
|
1480
1887
|
const queue = this._eventQueue;
|
|
1481
1888
|
this._eventQueue = [];
|
|
1482
1889
|
for (const evt of queue) {
|
|
1483
|
-
this._handleAgentEvent(evt.payload);
|
|
1890
|
+
this._handleAgentEvent(evt.payload, evt.capturedCommit);
|
|
1484
1891
|
}
|
|
1485
1892
|
}
|
|
1486
1893
|
|
|
@@ -1493,13 +1900,24 @@ class OpenClawClient extends EventEmitter {
|
|
|
1493
1900
|
_scheduleReconnect(delayOverride) {
|
|
1494
1901
|
if (this._stopped) return;
|
|
1495
1902
|
|
|
1496
|
-
|
|
1903
|
+
// Capture the current base before advancing so jitter is computed from the
|
|
1904
|
+
// same epoch value that would previously have been used as the raw delay.
|
|
1905
|
+
const base = this._backoffMs;
|
|
1497
1906
|
|
|
1498
1907
|
// Advance backoff for next time (unless overridden)
|
|
1499
1908
|
if (typeof delayOverride !== "number") {
|
|
1500
1909
|
this._backoffMs = Math.min(this._backoffMs * 2, 30000);
|
|
1501
1910
|
}
|
|
1502
1911
|
|
|
1912
|
+
// Apply equal jitter to the scheduled delay so concurrent reconnect storms
|
|
1913
|
+
// don't all fire at the same instant. The stored _backoffMs base is NOT
|
|
1914
|
+
// mutated — doubling/cap/reset invariants are preserved.
|
|
1915
|
+
// delayOverride bypasses jitter (used for e.g. shutdown-driven reconnects).
|
|
1916
|
+
const delay =
|
|
1917
|
+
typeof delayOverride === "number"
|
|
1918
|
+
? delayOverride
|
|
1919
|
+
: Math.floor(base / 2 + Math.random() * (base / 2));
|
|
1920
|
+
|
|
1503
1921
|
this._logger.info(
|
|
1504
1922
|
`[openclaw] Reconnecting in ${delay}ms (backoff: ${this._backoffMs}ms)`
|
|
1505
1923
|
);
|
|
@@ -1559,6 +1977,12 @@ class OpenClawClient extends EventEmitter {
|
|
|
1559
1977
|
|
|
1560
1978
|
_flushPendingErrors(err) {
|
|
1561
1979
|
for (const [, pending] of this._pending) {
|
|
1980
|
+
// Clear the per-request ACK timeout so a flushed entry cannot fire a
|
|
1981
|
+
// late reject after the map is cleared.
|
|
1982
|
+
if (pending.timer) {
|
|
1983
|
+
clearTimeout(pending.timer);
|
|
1984
|
+
pending.timer = null;
|
|
1985
|
+
}
|
|
1562
1986
|
pending.reject(err);
|
|
1563
1987
|
}
|
|
1564
1988
|
this._pending.clear();
|