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
|
@@ -1,60 +1,15 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
"π",
|
|
7
|
-
"π",
|
|
8
|
-
"π",
|
|
9
|
-
"π₯°",
|
|
10
|
-
"π",
|
|
11
|
-
"π",
|
|
12
|
-
"π",
|
|
13
|
-
"π",
|
|
14
|
-
"π",
|
|
15
|
-
"π₯Ί",
|
|
16
|
-
"π
",
|
|
17
|
-
"π₯",
|
|
18
|
-
"βΊοΈ",
|
|
19
|
-
"π€¦",
|
|
20
|
-
"π€·",
|
|
21
|
-
"π",
|
|
22
|
-
"π",
|
|
23
|
-
"π€",
|
|
24
|
-
"π",
|
|
25
|
-
"π",
|
|
26
|
-
"π€",
|
|
27
|
-
"π",
|
|
28
|
-
"π",
|
|
29
|
-
"π³",
|
|
30
|
-
"π₯³",
|
|
31
|
-
"π",
|
|
32
|
-
"π",
|
|
33
|
-
"π",
|
|
34
|
-
"πͺ",
|
|
35
|
-
"β¨",
|
|
36
|
-
"π",
|
|
37
|
-
"π",
|
|
38
|
-
"π",
|
|
39
|
-
"π",
|
|
40
|
-
"π",
|
|
41
|
-
"π’",
|
|
42
|
-
"π",
|
|
43
|
-
"π",
|
|
44
|
-
"π©",
|
|
45
|
-
"π―",
|
|
46
|
-
"πΉ",
|
|
47
|
-
"π",
|
|
48
|
-
"π",
|
|
49
|
-
"π",
|
|
50
|
-
"π",
|
|
51
|
-
"π",
|
|
52
|
-
];
|
|
53
|
-
|
|
54
|
-
const MESSAGE_EMOJI_ALLOWLIST_SET = new Set(MESSAGE_EMOJI_ALLOWLIST);
|
|
1
|
+
import {
|
|
2
|
+
MESSAGE_EMOJI_ALLOWLIST,
|
|
3
|
+
MESSAGE_EMOJI_ALLOWLIST_SET,
|
|
4
|
+
} from "./message-emoji-allowlist.js";
|
|
5
|
+
|
|
55
6
|
const EMOJI_CLUSTER_SEGMENTER = new Intl.Segmenter(undefined, {
|
|
56
7
|
granularity: "grapheme",
|
|
57
8
|
});
|
|
9
|
+
// All emoji codepoints live above U+007F, so pure-ASCII text cannot contain
|
|
10
|
+
// any emoji and the segmenter walk is pure overhead. This regex is the
|
|
11
|
+
// gatekeeper for the fast path below.
|
|
12
|
+
const NON_ASCII_RE = /[^\x00-\x7F]/;
|
|
58
13
|
const RGI_EMOJI_SEQUENCE_RE = createOptionalRegex("^\\p{RGI_Emoji}$", "v");
|
|
59
14
|
const EMOJI_CLUSTER_FALLBACK_RE =
|
|
60
15
|
/(?:\p{Extended_Pictographic}|[\u{1F1E6}-\u{1F1FF}\u{1F3FB}-\u{1F3FF}\u20E3\u{E0020}-\u{E007F}])/u;
|
|
@@ -147,10 +102,33 @@ function emitGapForTextEnd(mode, beforeRemoved, afterRemoved, removedEmojiInGap)
|
|
|
147
102
|
return beforeRemoved + afterRemoved;
|
|
148
103
|
}
|
|
149
104
|
|
|
105
|
+
function filterAsciiDisplayFast(text) {
|
|
106
|
+
// Mirrors the segmenter walk for pure-ASCII display mode:
|
|
107
|
+
// - whitespace immediately before a newline is dropped
|
|
108
|
+
// - other [ \t]+ runs collapse to a single space
|
|
109
|
+
// - trailing horizontal whitespace at end-of-text is stripped
|
|
110
|
+
return text
|
|
111
|
+
.replace(/[ \t]+(\r\n|\r|\n)/g, "$1")
|
|
112
|
+
.replace(/[ \t]+/g, " ")
|
|
113
|
+
.replace(/[ \t]+$/g, "");
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function filterAsciiRawFast(text) {
|
|
117
|
+
// Raw mode preserves inline whitespace; only the post-loop trailing-trim
|
|
118
|
+
// remains for pure-ASCII input.
|
|
119
|
+
return text.replace(/[ \t]+$/g, "");
|
|
120
|
+
}
|
|
121
|
+
|
|
150
122
|
function filterEmojiText(text, mode) {
|
|
151
123
|
if (!text) return "";
|
|
152
124
|
|
|
153
125
|
const selectedMode = mode === "display" ? "display" : "raw";
|
|
126
|
+
|
|
127
|
+
if (!NON_ASCII_RE.test(text)) {
|
|
128
|
+
return selectedMode === "display"
|
|
129
|
+
? filterAsciiDisplayFast(text)
|
|
130
|
+
: filterAsciiRawFast(text);
|
|
131
|
+
}
|
|
154
132
|
const out = [];
|
|
155
133
|
let pendingWhitespaceBeforeRemoved = "";
|
|
156
134
|
let pendingWhitespaceAfterRemoved = "";
|
|
@@ -246,4 +224,4 @@ export function filterRawEmojiText(text) {
|
|
|
246
224
|
return filterEmojiText(text, "raw");
|
|
247
225
|
}
|
|
248
226
|
|
|
249
|
-
export { MESSAGE_EMOJI_ALLOWLIST };
|
|
227
|
+
export { MESSAGE_EMOJI_ALLOWLIST, MESSAGE_EMOJI_ALLOWLIST_SET };
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { MESSAGE_EMOJI_ALLOWLIST } from "./message-emoji-allowlist.js";
|
|
2
|
+
|
|
3
|
+
const DISABLED_NOTICE = `<neural_emoji_reactor>
|
|
4
|
+
The Neural Emoji Reactor has been turned off for the remainder of
|
|
5
|
+
this session. Do not use <emoji:X>...</emoji> spans in your replies,
|
|
6
|
+
even if earlier messages in this conversation used them.
|
|
7
|
+
</neural_emoji_reactor>`;
|
|
8
|
+
|
|
9
|
+
const ALLOWLIST_LINES = (() => {
|
|
10
|
+
const rows = [];
|
|
11
|
+
for (let i = 0; i < MESSAGE_EMOJI_ALLOWLIST.length; i += 20) {
|
|
12
|
+
rows.push(MESSAGE_EMOJI_ALLOWLIST.slice(i, i + 20).join(" "));
|
|
13
|
+
}
|
|
14
|
+
return rows.map((r) => ` ${r}`).join("\n");
|
|
15
|
+
})();
|
|
16
|
+
|
|
17
|
+
const ACTIVE_BLOCK = `<neural_emoji_reactor>
|
|
18
|
+
You can briefly change a small status emoji shown above your message
|
|
19
|
+
on the user's display by wrapping a short phrase in your reply with:
|
|
20
|
+
|
|
21
|
+
<emoji:X>your phrase</emoji>
|
|
22
|
+
|
|
23
|
+
The tags themselves are invisible β only the wrapped words are shown
|
|
24
|
+
in the message body.
|
|
25
|
+
|
|
26
|
+
Use this sparingly. Most messages should have NO span. Use one only
|
|
27
|
+
at moments where it adds real warmth, surprise, care, or playfulness
|
|
28
|
+
for a single short phrase. Do not tag every sentence. Do not nest
|
|
29
|
+
spans. Always close a span you open.
|
|
30
|
+
|
|
31
|
+
Allowed emoji (use exactly one per span, copied verbatim from this
|
|
32
|
+
list):
|
|
33
|
+
${ALLOWLIST_LINES}
|
|
34
|
+
|
|
35
|
+
Do not use off-list emoji; they will be ignored.
|
|
36
|
+
</neural_emoji_reactor>`;
|
|
37
|
+
|
|
38
|
+
export function composeNeuralEmojiReactorSystemPrompt(opts) {
|
|
39
|
+
const state = opts && opts.state;
|
|
40
|
+
if (state === "active") return ACTIVE_BLOCK;
|
|
41
|
+
if (state === "recently-disabled") return DISABLED_NOTICE;
|
|
42
|
+
return "";
|
|
43
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { MESSAGE_EMOJI_ALLOWLIST_SET } from "./message-emoji-allowlist.js";
|
|
2
|
+
|
|
3
|
+
const OPEN_PREFIX = "<emoji:";
|
|
4
|
+
const CLOSE_TAG = "</emoji>";
|
|
5
|
+
const CLOSE_LITERALS = Object.freeze(["</emoji>"]);
|
|
6
|
+
|
|
7
|
+
export const EMOJI_TAG_FAMILY_CONFIG = {
|
|
8
|
+
name: "emoji",
|
|
9
|
+
closeLiterals: CLOSE_LITERALS,
|
|
10
|
+
|
|
11
|
+
matchOpen(input, at) {
|
|
12
|
+
if (!input.startsWith(OPEN_PREFIX, at)) return null;
|
|
13
|
+
const closeIdx = input.indexOf(">", at + OPEN_PREFIX.length);
|
|
14
|
+
if (closeIdx === -1) return null;
|
|
15
|
+
const rawEmoji = input.slice(at + OPEN_PREFIX.length, closeIdx);
|
|
16
|
+
if (/\s/.test(rawEmoji)) return null;
|
|
17
|
+
return {
|
|
18
|
+
consumed: closeIdx - at + 1,
|
|
19
|
+
spanInit: { emoji: rawEmoji },
|
|
20
|
+
};
|
|
21
|
+
},
|
|
22
|
+
|
|
23
|
+
matchClose(input, at) {
|
|
24
|
+
if (!input.startsWith(CLOSE_TAG, at)) return null;
|
|
25
|
+
return { consumed: CLOSE_TAG.length, closeKind: "emoji" };
|
|
26
|
+
},
|
|
27
|
+
|
|
28
|
+
closeMatches(_activeOpen, _closeKind) {
|
|
29
|
+
return true;
|
|
30
|
+
},
|
|
31
|
+
|
|
32
|
+
validateOpen(spanInit) {
|
|
33
|
+
return MESSAGE_EMOJI_ALLOWLIST_SET.has(spanInit.emoji);
|
|
34
|
+
},
|
|
35
|
+
|
|
36
|
+
matchTrailingPartial(input, _suffixStart) {
|
|
37
|
+
const n = input.length;
|
|
38
|
+
// 1) Trailing prefix of OPEN_PREFIX
|
|
39
|
+
for (let len = Math.min(n, OPEN_PREFIX.length - 1); len > 0; len--) {
|
|
40
|
+
const tail = input.slice(n - len);
|
|
41
|
+
if (OPEN_PREFIX.startsWith(tail)) return len;
|
|
42
|
+
}
|
|
43
|
+
// 2) Trailing OPEN_PREFIX + bytes not yet terminated by `>`
|
|
44
|
+
const lastOpen = input.lastIndexOf(OPEN_PREFIX);
|
|
45
|
+
if (lastOpen !== -1) {
|
|
46
|
+
const after = input.indexOf(">", lastOpen + OPEN_PREFIX.length);
|
|
47
|
+
if (after === -1) return n - lastOpen;
|
|
48
|
+
}
|
|
49
|
+
// 3) Trailing prefix of CLOSE_TAG
|
|
50
|
+
for (let len = Math.min(n, CLOSE_TAG.length - 1); len > 0; len--) {
|
|
51
|
+
const tail = input.slice(n - len);
|
|
52
|
+
if (CLOSE_TAG.startsWith(tail)) return len;
|
|
53
|
+
}
|
|
54
|
+
return 0;
|
|
55
|
+
},
|
|
56
|
+
};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
const ACTIVE_BLOCK = `<neural_pace_modulator>
|
|
2
|
+
You can shape how quickly a short phrase reveals on the user's display
|
|
3
|
+
by wrapping it with one of:
|
|
4
|
+
|
|
5
|
+
<dwell>your phrase</dwell> β reveals slower, lets the line land
|
|
6
|
+
<skim>your phrase</skim> β reveals faster, rushes past a recap
|
|
7
|
+
|
|
8
|
+
The tags themselves are invisible β only the wrapped words are shown
|
|
9
|
+
in the message body.
|
|
10
|
+
|
|
11
|
+
Use this sparingly. Most messages have NO pace tag. Use one only at a
|
|
12
|
+
moment that genuinely benefits from it: a beat where the user should
|
|
13
|
+
sit with what was just said, or a recap that doesn't need to breathe.
|
|
14
|
+
Keep spans short (a few words). Do not tag every sentence. Do not nest
|
|
15
|
+
the same tag inside itself. Always close a tag you open.
|
|
16
|
+
|
|
17
|
+
You may combine pace tags with <emoji:X>...</emoji> if both apply to
|
|
18
|
+
the same phrase. They don't interfere.
|
|
19
|
+
</neural_pace_modulator>`;
|
|
20
|
+
|
|
21
|
+
const DISABLED_NOTICE = `<neural_pace_modulator>
|
|
22
|
+
The Neural Pace Modulator has been turned off for the remainder of
|
|
23
|
+
this session. Do not use <dwell>...</dwell> or <skim>...</skim>
|
|
24
|
+
spans in your replies, even if earlier messages used them.
|
|
25
|
+
</neural_pace_modulator>`;
|
|
26
|
+
|
|
27
|
+
export function composeNeuralPaceModulatorSystemPrompt(opts) {
|
|
28
|
+
const state = opts && opts.state;
|
|
29
|
+
if (state === "active") return ACTIVE_BLOCK;
|
|
30
|
+
if (state === "recently-disabled") return DISABLED_NOTICE;
|
|
31
|
+
return "";
|
|
32
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
const OPEN_DWELL = "<dwell>";
|
|
2
|
+
const OPEN_SKIM = "<skim>";
|
|
3
|
+
const CLOSE_DWELL = "</dwell>";
|
|
4
|
+
const CLOSE_SKIM = "</skim>";
|
|
5
|
+
const OPEN_LITERALS = Object.freeze([OPEN_DWELL, OPEN_SKIM]);
|
|
6
|
+
const CLOSE_LITERALS = Object.freeze([CLOSE_DWELL, CLOSE_SKIM]);
|
|
7
|
+
const ALL_LITERALS = Object.freeze([...OPEN_LITERALS, ...CLOSE_LITERALS]);
|
|
8
|
+
|
|
9
|
+
export const PACE_TAG_FAMILY_CONFIG = {
|
|
10
|
+
name: "pace",
|
|
11
|
+
closeLiterals: CLOSE_LITERALS,
|
|
12
|
+
|
|
13
|
+
matchOpen(input, at) {
|
|
14
|
+
if (input.startsWith(OPEN_DWELL, at)) {
|
|
15
|
+
return { consumed: OPEN_DWELL.length, spanInit: { mode: "dwell" } };
|
|
16
|
+
}
|
|
17
|
+
if (input.startsWith(OPEN_SKIM, at)) {
|
|
18
|
+
return { consumed: OPEN_SKIM.length, spanInit: { mode: "skim" } };
|
|
19
|
+
}
|
|
20
|
+
return null;
|
|
21
|
+
},
|
|
22
|
+
|
|
23
|
+
matchClose(input, at) {
|
|
24
|
+
if (input.startsWith(CLOSE_DWELL, at)) {
|
|
25
|
+
return { consumed: CLOSE_DWELL.length, closeKind: "dwell" };
|
|
26
|
+
}
|
|
27
|
+
if (input.startsWith(CLOSE_SKIM, at)) {
|
|
28
|
+
return { consumed: CLOSE_SKIM.length, closeKind: "skim" };
|
|
29
|
+
}
|
|
30
|
+
return null;
|
|
31
|
+
},
|
|
32
|
+
|
|
33
|
+
closeMatches(activeOpen, closeKind) {
|
|
34
|
+
return activeOpen.mode === closeKind;
|
|
35
|
+
},
|
|
36
|
+
|
|
37
|
+
matchTrailingPartial(input, _suffixStart) {
|
|
38
|
+
const n = input.length;
|
|
39
|
+
let best = 0;
|
|
40
|
+
for (const lit of ALL_LITERALS) {
|
|
41
|
+
for (let len = Math.min(n, lit.length - 1); len > best; len--) {
|
|
42
|
+
const tail = input.slice(n - len);
|
|
43
|
+
if (lit.startsWith(tail)) {
|
|
44
|
+
best = len;
|
|
45
|
+
break;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return best;
|
|
50
|
+
},
|
|
51
|
+
};
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { computeCodeSpanRegions } from "./code-span-regions.js";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @typedef {Object} TagFamilyConfig
|
|
5
|
+
* @property {string} name
|
|
6
|
+
* @property {ReadonlyArray<string>} closeLiterals
|
|
7
|
+
* @property {(input: string, atOffset: number) => { consumed: number, spanInit: object } | null} matchOpen
|
|
8
|
+
* @property {(input: string, atOffset: number) => { consumed: number, closeKind: any } | null} matchClose
|
|
9
|
+
* @property {(activeOpen: object, closeKind: any) => boolean} closeMatches
|
|
10
|
+
* @property {(input: string, suffixStart: number) => number} matchTrailingPartial
|
|
11
|
+
* @property {(spanInit: object) => boolean} [validateOpen]
|
|
12
|
+
* @property {(spanInit: object) => void} [onRejected]
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* @param {string} accumulatedText
|
|
17
|
+
* @param {ReadonlyArray<TagFamilyConfig>} families
|
|
18
|
+
* @returns {{ cleanText: string, spansByFamily: Record<string, object[]>, trailingPartialTag: boolean }}
|
|
19
|
+
*/
|
|
20
|
+
export function parseTaggedSpans(accumulatedText, families) {
|
|
21
|
+
const spansByFamily = {};
|
|
22
|
+
const activeOpens = new Map();
|
|
23
|
+
for (const fam of families) spansByFamily[fam.name] = [];
|
|
24
|
+
|
|
25
|
+
// Compute holdback once: each family declares how many trailing bytes look
|
|
26
|
+
// like the start of one of its tags; max wins. Held-back bytes are kept
|
|
27
|
+
// out of the parse loop so they neither become spans nor leak verbatim.
|
|
28
|
+
let holdback = 0;
|
|
29
|
+
for (const fam of families) {
|
|
30
|
+
const vote = fam.matchTrailingPartial(accumulatedText, 0);
|
|
31
|
+
if (vote > holdback) holdback = vote;
|
|
32
|
+
}
|
|
33
|
+
const scanEnd = accumulatedText.length - holdback;
|
|
34
|
+
|
|
35
|
+
// Tags quoted inside markdown code (inline backticks / fenced blocks) are
|
|
36
|
+
// literal text, not live tags β copy those regions through verbatim.
|
|
37
|
+
const codeRegions = computeCodeSpanRegions(accumulatedText);
|
|
38
|
+
let codeRegionIdx = 0;
|
|
39
|
+
|
|
40
|
+
let cleanText = "";
|
|
41
|
+
let i = 0;
|
|
42
|
+
const n = scanEnd;
|
|
43
|
+
|
|
44
|
+
outer: while (i < n) {
|
|
45
|
+
while (
|
|
46
|
+
codeRegionIdx < codeRegions.length &&
|
|
47
|
+
i >= codeRegions[codeRegionIdx][1]
|
|
48
|
+
) {
|
|
49
|
+
codeRegionIdx += 1;
|
|
50
|
+
}
|
|
51
|
+
if (
|
|
52
|
+
codeRegionIdx < codeRegions.length &&
|
|
53
|
+
i >= codeRegions[codeRegionIdx][0] &&
|
|
54
|
+
i < codeRegions[codeRegionIdx][1]
|
|
55
|
+
) {
|
|
56
|
+
cleanText += accumulatedText[i];
|
|
57
|
+
i += 1;
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
if (accumulatedText[i] === "<") {
|
|
61
|
+
// Try each family's matchClose, then matchOpen, in registration order.
|
|
62
|
+
for (const fam of families) {
|
|
63
|
+
const close = fam.matchClose(accumulatedText, i);
|
|
64
|
+
if (close) {
|
|
65
|
+
const active = activeOpens.get(fam.name);
|
|
66
|
+
if (active && fam.closeMatches(active.init, close.closeKind)) {
|
|
67
|
+
spansByFamily[fam.name].push({
|
|
68
|
+
...active.init,
|
|
69
|
+
start: active.start,
|
|
70
|
+
end: cleanText.length,
|
|
71
|
+
});
|
|
72
|
+
activeOpens.delete(fam.name);
|
|
73
|
+
}
|
|
74
|
+
// mismatched or dangling close β drop silently
|
|
75
|
+
i += close.consumed;
|
|
76
|
+
continue outer;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
for (const fam of families) {
|
|
80
|
+
const open = fam.matchOpen(accumulatedText, i);
|
|
81
|
+
if (open) {
|
|
82
|
+
// Latest-wins same-family: close any active span at current offset.
|
|
83
|
+
const prior = activeOpens.get(fam.name);
|
|
84
|
+
if (prior) {
|
|
85
|
+
spansByFamily[fam.name].push({
|
|
86
|
+
...prior.init,
|
|
87
|
+
start: prior.start,
|
|
88
|
+
end: cleanText.length,
|
|
89
|
+
});
|
|
90
|
+
activeOpens.delete(fam.name);
|
|
91
|
+
}
|
|
92
|
+
const accepted = fam.validateOpen ? fam.validateOpen(open.spanInit) : true;
|
|
93
|
+
if (accepted) {
|
|
94
|
+
activeOpens.set(fam.name, { start: cleanText.length, init: open.spanInit });
|
|
95
|
+
} else if (fam.onRejected) {
|
|
96
|
+
fam.onRejected(open.spanInit);
|
|
97
|
+
}
|
|
98
|
+
i += open.consumed;
|
|
99
|
+
continue outer;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
cleanText += accumulatedText[i];
|
|
104
|
+
i += 1;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Unclosed spans β run to cleanText end.
|
|
108
|
+
for (const fam of families) {
|
|
109
|
+
const active = activeOpens.get(fam.name);
|
|
110
|
+
if (active) {
|
|
111
|
+
spansByFamily[fam.name].push({
|
|
112
|
+
...active.init,
|
|
113
|
+
start: active.start,
|
|
114
|
+
end: cleanText.length,
|
|
115
|
+
});
|
|
116
|
+
activeOpens.delete(fam.name);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return { cleanText, spansByFamily, trailingPartialTag: holdback > 0 };
|
|
121
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
// Syntactic strip for all tagged-span grammar families.
|
|
2
|
+
// Removes well-formed openers and closers; keeps wrapped content.
|
|
3
|
+
// Removes orphan halves of either. Idempotent. No allowlist enforcement.
|
|
4
|
+
// Tags quoted inside markdown code (inline backticks / fenced blocks) are
|
|
5
|
+
// preserved verbatim so agents can talk about the tag grammar literally.
|
|
6
|
+
import { computeCodeSpanRegions } from "./code-span-regions.js";
|
|
7
|
+
|
|
8
|
+
const EMOJI_OPEN_RE = /<emoji:[^<>\s]+?>/g;
|
|
9
|
+
const EMOJI_CLOSE_RE = /<\/emoji>/g;
|
|
10
|
+
const PACE_OPEN_RE = /<(?:dwell|skim)>/g;
|
|
11
|
+
const PACE_CLOSE_RE = /<\/(?:dwell|skim)>/g;
|
|
12
|
+
|
|
13
|
+
// Single alternation so one pass over the original string keeps match
|
|
14
|
+
// offsets valid for the code-region exemption check.
|
|
15
|
+
const ALL_TAGS_RE = new RegExp(
|
|
16
|
+
[
|
|
17
|
+
EMOJI_OPEN_RE.source,
|
|
18
|
+
EMOJI_CLOSE_RE.source,
|
|
19
|
+
PACE_OPEN_RE.source,
|
|
20
|
+
PACE_CLOSE_RE.source,
|
|
21
|
+
].join("|"),
|
|
22
|
+
"g",
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
export function stripAllTaggedSpans(text) {
|
|
26
|
+
if (typeof text !== "string") return "";
|
|
27
|
+
if (!text) return "";
|
|
28
|
+
const codeRegions = computeCodeSpanRegions(text);
|
|
29
|
+
if (codeRegions.length === 0) {
|
|
30
|
+
return text.replace(ALL_TAGS_RE, "");
|
|
31
|
+
}
|
|
32
|
+
return text.replace(ALL_TAGS_RE, (match, offset) => {
|
|
33
|
+
for (const [start, end] of codeRegions) {
|
|
34
|
+
if (offset >= start && offset < end) return match;
|
|
35
|
+
}
|
|
36
|
+
return "";
|
|
37
|
+
});
|
|
38
|
+
}
|
|
@@ -257,6 +257,10 @@ export function createEvenAiEndpoint(opts = {}) {
|
|
|
257
257
|
typeof opts.emitListenInterceptRecovery === "function"
|
|
258
258
|
? opts.emitListenInterceptRecovery
|
|
259
259
|
: null;
|
|
260
|
+
const emitListenInterceptBroadcast =
|
|
261
|
+
typeof opts.emitListenInterceptBroadcast === "function"
|
|
262
|
+
? opts.emitListenInterceptBroadcast
|
|
263
|
+
: null;
|
|
260
264
|
const isUpstreamConnected =
|
|
261
265
|
typeof opts.isUpstreamConnected === "function"
|
|
262
266
|
? opts.isUpstreamConnected
|
|
@@ -269,6 +273,10 @@ export function createEvenAiEndpoint(opts = {}) {
|
|
|
269
273
|
typeof opts.shouldSeedThinkingForRoute === "function"
|
|
270
274
|
? opts.shouldSeedThinkingForRoute
|
|
271
275
|
: async () => false;
|
|
276
|
+
const seedFastModeForRoute =
|
|
277
|
+
typeof opts.seedFastModeForRoute === "function"
|
|
278
|
+
? opts.seedFastModeForRoute
|
|
279
|
+
: null;
|
|
272
280
|
const now =
|
|
273
281
|
typeof opts.now === "function" ? opts.now : () => Date.now();
|
|
274
282
|
const requestTimeoutMs = normalizePositiveInt(
|
|
@@ -599,8 +607,18 @@ export function createEvenAiEndpoint(opts = {}) {
|
|
|
599
607
|
id: requestId,
|
|
600
608
|
text: userText,
|
|
601
609
|
sessionKey,
|
|
610
|
+
source: "hybrid_voice_endpoint",
|
|
602
611
|
}),
|
|
603
612
|
);
|
|
613
|
+
if (emitListenInterceptBroadcast) {
|
|
614
|
+
try {
|
|
615
|
+
emitListenInterceptBroadcast({ sessionKey });
|
|
616
|
+
} catch (broadcastErr) {
|
|
617
|
+
logger.warn(
|
|
618
|
+
`[evenai] listen intercept broadcast callback failed: ${broadcastErr && broadcastErr.message ? broadcastErr.message : broadcastErr}`,
|
|
619
|
+
);
|
|
620
|
+
}
|
|
621
|
+
}
|
|
604
622
|
emitDebug(
|
|
605
623
|
"evenai",
|
|
606
624
|
"listen_intercept_dispatch_succeeded",
|
|
@@ -771,6 +789,47 @@ export function createEvenAiEndpoint(opts = {}) {
|
|
|
771
789
|
startedAtMs,
|
|
772
790
|
};
|
|
773
791
|
|
|
792
|
+
// Track the upstream runId so the disconnect handler can cancel the
|
|
793
|
+
// pending wait without having to wait for the full request timeout.
|
|
794
|
+
// `clientDisconnected` covers the pre-ack window: if the socket closes
|
|
795
|
+
// before sendMessage() resolves, activeRunId is still null so cancelRun
|
|
796
|
+
// is unreachable; we record the flag and short-circuit waitForRun once
|
|
797
|
+
// the ack arrives.
|
|
798
|
+
let activeRunId = null;
|
|
799
|
+
let clientDisconnected = false;
|
|
800
|
+
const onClientDisconnect = () => {
|
|
801
|
+
if (res.writableEnded) return;
|
|
802
|
+
clientDisconnected = true;
|
|
803
|
+
if (inFlight && inFlight.requestId === requestId) {
|
|
804
|
+
inFlight = null;
|
|
805
|
+
emitDebug(
|
|
806
|
+
"evenai",
|
|
807
|
+
"request_client_disconnect",
|
|
808
|
+
"info",
|
|
809
|
+
{ sessionKey: sessionKey || undefined, runId: activeRunId },
|
|
810
|
+
() => ({
|
|
811
|
+
requestId,
|
|
812
|
+
elapsedMs: now() - startedAtMs,
|
|
813
|
+
preAck: activeRunId == null,
|
|
814
|
+
}),
|
|
815
|
+
);
|
|
816
|
+
}
|
|
817
|
+
if (activeRunId && typeof runWaiter.cancelRun === "function") {
|
|
818
|
+
try {
|
|
819
|
+
runWaiter.cancelRun(activeRunId, "client_disconnect");
|
|
820
|
+
} catch (_err) {
|
|
821
|
+
// Cancellation is best-effort: the upstream run still completes
|
|
822
|
+
// server-side because no gateway abort RPC exists.
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
};
|
|
826
|
+
// `res` emits 'close' both when the response finishes normally and when
|
|
827
|
+
// the client disconnects mid-flight. We discriminate via
|
|
828
|
+
// res.writableEnded inside the handler. (req's own 'close' is not
|
|
829
|
+
// reliable for premature client aborts in Node 22 β the socket-level
|
|
830
|
+
// close is what fires.)
|
|
831
|
+
res.once("close", onClientDisconnect);
|
|
832
|
+
|
|
774
833
|
emitDebug(
|
|
775
834
|
"evenai",
|
|
776
835
|
"request_accepted",
|
|
@@ -802,6 +861,20 @@ export function createEvenAiEndpoint(opts = {}) {
|
|
|
802
861
|
) {
|
|
803
862
|
sendOptions.thinking = configuredDefaultThinking;
|
|
804
863
|
}
|
|
864
|
+
if (seedFastModeForRoute) {
|
|
865
|
+
try {
|
|
866
|
+
await Promise.resolve(
|
|
867
|
+
seedFastModeForRoute({ route, sessionKey, routingMode }),
|
|
868
|
+
);
|
|
869
|
+
} catch (err) {
|
|
870
|
+
// Seed failure must never block the send β the turn just runs
|
|
871
|
+
// without fast mode.
|
|
872
|
+
emitDebug("evenai", "fast_mode_seed_failed", "warn", { sessionKey }, () => ({
|
|
873
|
+
requestId,
|
|
874
|
+
message: err && err.message ? err.message : String(err),
|
|
875
|
+
}));
|
|
876
|
+
}
|
|
877
|
+
}
|
|
805
878
|
const ack = await promiseWithTimeout(
|
|
806
879
|
gatewayBridge.sendMessage(
|
|
807
880
|
userText,
|
|
@@ -815,6 +888,7 @@ export function createEvenAiEndpoint(opts = {}) {
|
|
|
815
888
|
if (!runId) {
|
|
816
889
|
throw new Error("Even AI upstream ack was missing a runId.");
|
|
817
890
|
}
|
|
891
|
+
activeRunId = runId;
|
|
818
892
|
if (trimString(ack && ack.status) && trimString(ack.status) !== "accepted") {
|
|
819
893
|
throw new Error(
|
|
820
894
|
trimString(ack && ack.error) || `Even AI upstream returned ${ack.status}.`,
|
|
@@ -832,6 +906,20 @@ export function createEvenAiEndpoint(opts = {}) {
|
|
|
832
906
|
}),
|
|
833
907
|
);
|
|
834
908
|
|
|
909
|
+
if (clientDisconnected) {
|
|
910
|
+
emitDebug(
|
|
911
|
+
"evenai",
|
|
912
|
+
"request_wait_skipped_after_disconnect",
|
|
913
|
+
"info",
|
|
914
|
+
{ sessionKey, runId },
|
|
915
|
+
() => ({
|
|
916
|
+
requestId,
|
|
917
|
+
elapsedMs: now() - startedAtMs,
|
|
918
|
+
}),
|
|
919
|
+
);
|
|
920
|
+
return true;
|
|
921
|
+
}
|
|
922
|
+
|
|
835
923
|
const remainingTimeoutMs = Math.max(1, requestTimeoutMs - (now() - startedAtMs));
|
|
836
924
|
const assistantText = await runWaiter.waitForRun({
|
|
837
925
|
runId,
|
|
@@ -889,6 +977,9 @@ export function createEvenAiEndpoint(opts = {}) {
|
|
|
889
977
|
if (inFlight && inFlight.requestId === requestId) {
|
|
890
978
|
inFlight = null;
|
|
891
979
|
}
|
|
980
|
+
if (typeof res.removeListener === "function") {
|
|
981
|
+
res.removeListener("close", onClientDisconnect);
|
|
982
|
+
}
|
|
892
983
|
}
|
|
893
984
|
}
|
|
894
985
|
|
|
@@ -242,6 +242,20 @@ export function createEvenAiRunWaiter(opts = {}) {
|
|
|
242
242
|
});
|
|
243
243
|
},
|
|
244
244
|
|
|
245
|
+
cancelRun(runId, reason) {
|
|
246
|
+
const normalized = normalizeRunId(runId);
|
|
247
|
+
if (!normalized) return false;
|
|
248
|
+
const message =
|
|
249
|
+
typeof reason === "string" && reason.length > 0
|
|
250
|
+
? `Even AI run cancelled: ${reason}.`
|
|
251
|
+
: "Even AI run cancelled.";
|
|
252
|
+
return rejectRun(
|
|
253
|
+
normalized,
|
|
254
|
+
createRunWaiterError("evenai_cancelled", message, { reason: reason || null }),
|
|
255
|
+
"run_wait_cancelled",
|
|
256
|
+
);
|
|
257
|
+
},
|
|
258
|
+
|
|
245
259
|
close() {
|
|
246
260
|
if (typeof offMessage === "function") offMessage();
|
|
247
261
|
if (typeof offActivity === "function") offActivity();
|
|
@@ -134,12 +134,19 @@ function isStoredSnapshotCanonical(value, snapshot) {
|
|
|
134
134
|
if (normalizeEvenAiListenEnabled(value.listenEnabled) !== snapshot.listenEnabled) {
|
|
135
135
|
return false;
|
|
136
136
|
}
|
|
137
|
+
if (normalizeEvenAiDefaultFastMode(value.defaultFastMode) !== snapshot.defaultFastMode) {
|
|
138
|
+
return false;
|
|
139
|
+
}
|
|
137
140
|
if (!Array.isArray(value.trackedThrowawayKeys)) {
|
|
138
141
|
return snapshot.trackedThrowawayKeys.length === 0;
|
|
139
142
|
}
|
|
140
143
|
return arraysEqual(value.trackedThrowawayKeys, snapshot.trackedThrowawayKeys);
|
|
141
144
|
}
|
|
142
145
|
|
|
146
|
+
export function normalizeEvenAiDefaultFastMode(value) {
|
|
147
|
+
return value === true;
|
|
148
|
+
}
|
|
149
|
+
|
|
143
150
|
export function normalizeEvenAiSettingsSnapshot(value = {}) {
|
|
144
151
|
return {
|
|
145
152
|
routingMode: normalizeEvenAiRoutingMode(value.routingMode),
|
|
@@ -147,6 +154,7 @@ export function normalizeEvenAiSettingsSnapshot(value = {}) {
|
|
|
147
154
|
defaultModel: normalizeEvenAiDefaultModel(value.defaultModel),
|
|
148
155
|
defaultThinking: normalizeEvenAiDefaultThinking(value.defaultThinking),
|
|
149
156
|
listenEnabled: normalizeEvenAiListenEnabled(value.listenEnabled),
|
|
157
|
+
defaultFastMode: normalizeEvenAiDefaultFastMode(value.defaultFastMode),
|
|
150
158
|
trackedThrowawayKeys: normalizeTrackedThrowawayKeys(value.trackedThrowawayKeys),
|
|
151
159
|
};
|
|
152
160
|
}
|
|
@@ -180,6 +188,7 @@ export function createEvenAiSettingsStore(opts = {}) {
|
|
|
180
188
|
systemPromptChars: snapshot.systemPrompt.length,
|
|
181
189
|
defaultModel: snapshot.defaultModel,
|
|
182
190
|
defaultThinking: snapshot.defaultThinking,
|
|
191
|
+
defaultFastMode: snapshot.defaultFastMode,
|
|
183
192
|
listenEnabled: snapshot.listenEnabled,
|
|
184
193
|
trackedThrowawayKeyCount: snapshot.trackedThrowawayKeys.length,
|
|
185
194
|
}),
|
|
@@ -213,6 +222,7 @@ export function createEvenAiSettingsStore(opts = {}) {
|
|
|
213
222
|
systemPromptChars: snapshot.systemPrompt.length,
|
|
214
223
|
defaultModel: snapshot.defaultModel,
|
|
215
224
|
defaultThinking: snapshot.defaultThinking,
|
|
225
|
+
defaultFastMode: snapshot.defaultFastMode,
|
|
216
226
|
listenEnabled: snapshot.listenEnabled,
|
|
217
227
|
trackedThrowawayKeyCount: snapshot.trackedThrowawayKeys.length,
|
|
218
228
|
}),
|
|
@@ -266,6 +276,7 @@ export function createEvenAiSettingsStore(opts = {}) {
|
|
|
266
276
|
systemPromptChars: loaded.systemPrompt.length,
|
|
267
277
|
defaultModel: loaded.defaultModel,
|
|
268
278
|
defaultThinking: loaded.defaultThinking,
|
|
279
|
+
defaultFastMode: loaded.defaultFastMode,
|
|
269
280
|
listenEnabled: loaded.listenEnabled,
|
|
270
281
|
trackedThrowawayKeyCount: loaded.trackedThrowawayKeys.length,
|
|
271
282
|
}),
|
|
@@ -350,6 +361,9 @@ export function createEvenAiSettingsStore(opts = {}) {
|
|
|
350
361
|
listenEnabled: hasOwn(patch, "listenEnabled")
|
|
351
362
|
? normalizeEvenAiListenEnabled(patch.listenEnabled)
|
|
352
363
|
: snapshot.listenEnabled,
|
|
364
|
+
defaultFastMode: hasOwn(patch, "defaultFastMode")
|
|
365
|
+
? normalizeEvenAiDefaultFastMode(patch.defaultFastMode)
|
|
366
|
+
: snapshot.defaultFastMode,
|
|
353
367
|
trackedThrowawayKeys: [...snapshot.trackedThrowawayKeys],
|
|
354
368
|
};
|
|
355
369
|
snapshot = next;
|