ocuclaw 1.3.1 → 1.3.2
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/config/runtime-config-session-title-model.test.js +22 -0
- package/dist/config/runtime-config.js +7 -1
- package/dist/domain/glasses-display-system-prompt.js +52 -0
- package/dist/domain/glasses-display-system-prompt.test.js +44 -0
- package/dist/domain/glasses-ui-system-prompt.js +6 -22
- package/dist/domain/glasses-ui-system-prompt.test.js +13 -0
- package/dist/domain/prompt-channel-fragments.js +32 -0
- package/dist/domain/prompt-channel-fragments.test.js +70 -0
- package/dist/gateway/gateway-timing-ledger.js +15 -3
- package/dist/gateway/openclaw-client.js +80 -3
- package/dist/index.js +22 -0
- package/dist/runtime/channel-two-hook.js +36 -0
- package/dist/runtime/container-env.js +41 -0
- package/dist/runtime/display-toggle-states.js +98 -0
- package/dist/runtime/register-session-title-distiller.js +100 -0
- package/dist/runtime/relay-core.js +209 -33
- package/dist/runtime/relay-service.js +120 -13
- package/dist/runtime/relay-worker-entry.js +26 -0
- package/dist/runtime/relay-worker-supervisor.js +43 -2
- package/dist/runtime/relay-worker-transport.js +41 -0
- package/dist/runtime/session-service.js +136 -12
- package/dist/runtime/session-title-distiller-budget.js +36 -0
- package/dist/runtime/session-title-distiller-helpers.js +130 -0
- package/dist/runtime/session-title-distiller.js +354 -0
- package/dist/runtime/session-title-record.js +21 -0
- package/dist/runtime/stable-prompt-snapshot.js +119 -0
- package/dist/tools/glasses-ui-cron.js +9 -3
- package/dist/tools/glasses-ui-paint-floor.js +10 -3
- package/dist/tools/glasses-ui-surfaces.js +8 -1
- package/dist/tools/glasses-ui-tool-description.test.js +16 -0
- package/dist/tools/glasses-ui-tool.js +88 -14
- package/dist/tools/session-title-tool.js +14 -76
- package/dist/tools/session-title-tool.test.js +53 -0
- package/dist/version.js +2 -2
- package/openclaw.plugin.json +9 -0
- package/package.json +4 -3
- package/skills/glasses-ui/SKILL.md +7 -0
|
@@ -2,15 +2,22 @@
|
|
|
2
2
|
//
|
|
3
3
|
// Governs ALL plugin->glass sends (initial RebuildPageContainer render and
|
|
4
4
|
// every surface_update patch). Collapses bursts to last-write-wins per field
|
|
5
|
-
// and emits at most one frame per paintFloorMs
|
|
6
|
-
//
|
|
5
|
+
// and emits at most one frame per paintFloorMs, with a leading-edge send + a
|
|
6
|
+
// trailing send carrying the final merged patch.
|
|
7
7
|
//
|
|
8
8
|
// There is NO glass-side paint-ack: the only backpressure signal is
|
|
9
9
|
// relay/BLE transport-side (see the isUnderBackpressure shed in Task 13).
|
|
10
10
|
// Local fake-list textContainerUpgrade scroll-swaps are client-side and never
|
|
11
11
|
// reach this coalescer.
|
|
12
|
+
//
|
|
13
|
+
// 250 is the unconditionally hardware-proven floor. Spike D approved lowering
|
|
14
|
+
// to 150 ONLY once the backpressure shed has a live signal; the shed's
|
|
15
|
+
// isGlassesSendBufferOverHighWater query is implemented nowhere yet, so the
|
|
16
|
+
// shed is inert and 150 would run without its safety condition. Restore 150
|
|
17
|
+
// when the relay-service bridge lands and is validated on hardware
|
|
18
|
+
// (roadmap step 4, docs/superpowers/plans/2026-06-10-glasses-ui-state-reset-and-roadmap.md).
|
|
12
19
|
|
|
13
|
-
export const DEFAULT_PAINT_FLOOR_MS =
|
|
20
|
+
export const DEFAULT_PAINT_FLOOR_MS = 250;
|
|
14
21
|
|
|
15
22
|
export function createPaintFloorCoalescer(deps) {
|
|
16
23
|
const paintFloorMs = Number.isFinite(deps.paintFloorMs) ? deps.paintFloorMs : DEFAULT_PAINT_FLOOR_MS;
|
|
@@ -236,7 +236,14 @@ export function createSurfaceStore(deps = {}) {
|
|
|
236
236
|
// be move-independent (replace, the schema DEFAULT, must behave like
|
|
237
237
|
// patch here, not silently drop a latched exit).
|
|
238
238
|
const priorTop = bySurface.get(top);
|
|
239
|
-
|
|
239
|
+
// SILENT stop: this is slot recycling for the incoming replace render, not
|
|
240
|
+
// a real outcome. A non-silent stop fires the cron's onResolve with a
|
|
241
|
+
// synthesized `preempted`, which (with no pending call at this instant)
|
|
242
|
+
// latches a bogus exit onto the prior entry — carried into makeEntry below,
|
|
243
|
+
// it makes the very render we are applying discard-for-exit on re-attach
|
|
244
|
+
// ("fresh render instantly dismissed", B7 — the real B3 contamination
|
|
245
|
+
// mechanism, found 2026-06-11).
|
|
246
|
+
stopCron(top, { silent: true });
|
|
240
247
|
bySurface.set(top, makeEntry(sessionKey, params && params.kind, priorTop));
|
|
241
248
|
return { mode: "replace", surfaceId: top };
|
|
242
249
|
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import { GLASSES_UI_TOOL_DESCRIPTION } from "./glasses-ui-tool.ts";
|
|
4
|
+
|
|
5
|
+
test("description now carries the follow-up + back/selected usage rules", () => {
|
|
6
|
+
const d = GLASSES_UI_TOOL_DESCRIPTION;
|
|
7
|
+
assert.match(d, /text_surface/);
|
|
8
|
+
assert.match(d, /list_surface/);
|
|
9
|
+
assert.match(d, /list_with_details_surface/);
|
|
10
|
+
// Channel-3 additions (moved from the old nudge):
|
|
11
|
+
assert.match(d, /NEXT output|next output/);
|
|
12
|
+
assert.match(d, /back/i);
|
|
13
|
+
assert.match(d, /selected/i);
|
|
14
|
+
// Skill pointer retained:
|
|
15
|
+
assert.match(d, /glasses-ui/);
|
|
16
|
+
});
|
|
@@ -542,8 +542,8 @@ export function createGlassesUiToolHandler(deps) {
|
|
|
542
542
|
// parent/chat after a back/pop, and the per-surface coalescer state doesn't
|
|
543
543
|
// leak across a long push/replace session. (pauseCron on push does NOT
|
|
544
544
|
// dispose — the parent resumes.)
|
|
545
|
-
stopCron: (id) => {
|
|
546
|
-
cronEngine.stop(id, { result: "preempted" });
|
|
545
|
+
stopCron: (id, opts) => {
|
|
546
|
+
cronEngine.stop(id, { result: "preempted" }, opts);
|
|
547
547
|
paintFloor.dispose(id);
|
|
548
548
|
},
|
|
549
549
|
mintSurfaceId: newSurfaceId,
|
|
@@ -610,11 +610,33 @@ export function createGlassesUiToolHandler(deps) {
|
|
|
610
610
|
refreshValidated = v.refresh;
|
|
611
611
|
}
|
|
612
612
|
|
|
613
|
+
// params.depth is the execute-level RUN-CALL ordinal (resets at agent_end).
|
|
614
|
+
// It is used ONLY as the "first render of this run" signal for stale-stack
|
|
615
|
+
// reaping below — it must NEVER reach the wire. The wire depth is derived
|
|
616
|
+
// from the store's true stack depth after applyRender (B6: ordinals never
|
|
617
|
+
// decrement on Back, so they drift past entry counts and break both the
|
|
618
|
+
// plugin pop reconciliation and the client's clear-vs-append decision).
|
|
613
619
|
const depth = Number.isFinite(params.depth) ? Math.max(1, Math.floor(params.depth)) : 1;
|
|
614
620
|
const update =
|
|
615
621
|
params.spec && (params.spec.update === "patch" || params.spec.update === "push")
|
|
616
622
|
? params.spec.update
|
|
617
623
|
: "replace";
|
|
624
|
+
// Stale-stack reaping (B3 safety net): a depth-1 render means NEW ROOT — a
|
|
625
|
+
// session stack still holding PUSHED children at that moment is orphan
|
|
626
|
+
// residue from an earlier run (e.g. a client that bailed to chat without
|
|
627
|
+
// popping). Reap it before registering so a stale child can't swallow this
|
|
628
|
+
// render's events or forward a stale latched exit. A SINGLE root entry is
|
|
629
|
+
// NOT stale — that's the designed patch/replace re-attach path
|
|
630
|
+
// (visible_awaiting_agent), which must keep its latch/queue semantics.
|
|
631
|
+
if (depth <= 1 && surfaceStore.stackDepth(sessionKey) > 1) {
|
|
632
|
+
const stackDepthBefore = surfaceStore.stackDepth(sessionKey);
|
|
633
|
+
const reapedPending = reapSession(sessionKey, { result: "preempted" });
|
|
634
|
+
emitLifecycle("stale_stack_reaped", "warn", {
|
|
635
|
+
sessionKey,
|
|
636
|
+
stackDepthBefore,
|
|
637
|
+
reapedPending,
|
|
638
|
+
});
|
|
639
|
+
}
|
|
618
640
|
// The plugin owns surfaceIds (spec §Core model). applyRender derives the
|
|
619
641
|
// target from the session's current top: patch/replace reuse the top id
|
|
620
642
|
// (re-attach in place), push mints a child + pauses the parent cron, the
|
|
@@ -646,6 +668,12 @@ export function createGlassesUiToolHandler(deps) {
|
|
|
646
668
|
}
|
|
647
669
|
}
|
|
648
670
|
|
|
671
|
+
// The wire depth is the TRUE stack depth (entry count) after applyRender:
|
|
672
|
+
// root=1, push=parent+1, replace/patch=unchanged. The client keys its
|
|
673
|
+
// clear-vs-append-vs-swap decision and Back classification on this value,
|
|
674
|
+
// and handleNavEvent's pop loop compares it against the same entry counts.
|
|
675
|
+
const wireDepth = Math.max(1, surfaceStore.stackDepth(sessionKey));
|
|
676
|
+
|
|
649
677
|
// Initial render uses the agent's seed (instant). Routed through the
|
|
650
678
|
// paint-floor coalescer as a leading-edge render sentinel so it shares the
|
|
651
679
|
// single send chokepoint (a render supersedes any queued field patch for
|
|
@@ -653,7 +681,7 @@ export function createGlassesUiToolHandler(deps) {
|
|
|
653
681
|
paintFloor.enqueue({
|
|
654
682
|
surfaceId,
|
|
655
683
|
sessionKey,
|
|
656
|
-
patch: { __render: true, __depth:
|
|
684
|
+
patch: { __render: true, __depth: wireDepth, __spec: validation.spec },
|
|
657
685
|
});
|
|
658
686
|
|
|
659
687
|
// Live-refresh path: kick off the cron in parallel. A `patch` onto an
|
|
@@ -776,6 +804,21 @@ export function createGlassesUiToolHandler(deps) {
|
|
|
776
804
|
guard += 1;
|
|
777
805
|
}
|
|
778
806
|
}
|
|
807
|
+
if (
|
|
808
|
+
popCount === 0 &&
|
|
809
|
+
storeDepthBefore > 1 &&
|
|
810
|
+
surfaceStore.topSurfaceId(sessionKey) === ev.surfaceId
|
|
811
|
+
) {
|
|
812
|
+
// Surface-match fallback (B6): a Back event reports the surfaceId being
|
|
813
|
+
// backed OUT OF — the store top. If the depth comparison said no-op
|
|
814
|
+
// (drifted ordinals from an older client, or any depth desync) but the
|
|
815
|
+
// reported surface IS the top with a parent beneath, pop exactly one
|
|
816
|
+
// level. Push events carry the PARENT surfaceId — never the top after a
|
|
817
|
+
// push — so this cannot misfire on a push report; and a duplicate Back
|
|
818
|
+
// delivery is idempotent (after the pop the top no longer matches).
|
|
819
|
+
resumedParent = surfaceStore.popBack(sessionKey);
|
|
820
|
+
popCount += 1;
|
|
821
|
+
}
|
|
779
822
|
// Push (newDepth > lastDepth) is already reflected in the store by the
|
|
780
823
|
// agent's push render (applyRender), so it is intentionally a no-op here.
|
|
781
824
|
emitLifecycle("nav_reconcile", "debug", {
|
|
@@ -791,19 +834,25 @@ export function createGlassesUiToolHandler(deps) {
|
|
|
791
834
|
navDepthBySession.set(sessionKey, newDepth);
|
|
792
835
|
}
|
|
793
836
|
|
|
837
|
+
// Stop crons, resolve pending calls with `outcome`, clear the session stack.
|
|
838
|
+
// Resolve pending + delete entries FIRST (so pending calls settle with
|
|
839
|
+
// `outcome`), THEN clear the per-session stack. exit() deletes entries
|
|
840
|
+
// without resolving, so it must NOT run before the drain or the pending
|
|
841
|
+
// promises would hang. Shared by the public drainSession (agent_end /
|
|
842
|
+
// disconnect) and the stale-stack reap in runDynamicUi (B3).
|
|
843
|
+
function reapSession(sessionKey, outcome) {
|
|
844
|
+
cronEngine.stopAllForSession(sessionKey, outcome);
|
|
845
|
+
const reaped = surfaceStore.drainSession(sessionKey, outcome);
|
|
846
|
+
surfaceStore.exit(sessionKey); // clear the stack (crons already stopped)
|
|
847
|
+
navDepthBySession.delete(sessionKey); // drop stale nav depth (stack is now 0)
|
|
848
|
+
return reaped;
|
|
849
|
+
}
|
|
850
|
+
|
|
794
851
|
return {
|
|
795
852
|
runDynamicUi,
|
|
796
853
|
handleNavEvent,
|
|
797
854
|
drainSession(sessionKey, outcome) {
|
|
798
|
-
|
|
799
|
-
// Resolve pending + delete entries FIRST (so pending calls settle with
|
|
800
|
-
// `outcome`), THEN clear the per-session stack. exit() deletes entries
|
|
801
|
-
// without resolving, so it must NOT run before the drain or the pending
|
|
802
|
-
// promises would hang.
|
|
803
|
-
const reaped = surfaceStore.drainSession(sessionKey, outcome);
|
|
804
|
-
surfaceStore.exit(sessionKey); // clear the stack (crons already stopped)
|
|
805
|
-
navDepthBySession.delete(sessionKey); // drop stale nav depth (stack is now 0)
|
|
806
|
-
return reaped;
|
|
855
|
+
return reapSession(sessionKey, outcome);
|
|
807
856
|
},
|
|
808
857
|
drainAll(outcome) {
|
|
809
858
|
cronEngine.stopAll(outcome);
|
|
@@ -858,6 +907,13 @@ export const GLASSES_UI_TOOL_DESCRIPTION = [
|
|
|
858
907
|
"exit-to-chat policy, the {{path|filter}} template + per-item {label,body}",
|
|
859
908
|
"reference, and worked examples (including a live system-stats",
|
|
860
909
|
"list_with_details surface). Keep this description lean; depth lives in the skill.",
|
|
910
|
+
"",
|
|
911
|
+
"After the call resolves, your NEXT output decides the glasses: another",
|
|
912
|
+
"render_glasses_ui replaces the surface (drill-down / next step); a short text",
|
|
913
|
+
"reply hands the screen back to chat (the surface disappears); a silent run-end",
|
|
914
|
+
"leaves the surface up until the user dismisses it. A \"back\" result means the",
|
|
915
|
+
"user wants to revise their previous answer — re-render it or pivot; after a",
|
|
916
|
+
"\"selected\" result, follow up with another render or a brief one-line ack.",
|
|
861
917
|
].join("\n");
|
|
862
918
|
|
|
863
919
|
// Shared per-session depth counter. OpenClaw loads the plugin's register(api)
|
|
@@ -1031,10 +1087,28 @@ export function registerGlassesUiTool(api, service) {
|
|
|
1031
1087
|
// Reconcile client nav-events with the SINGLE store stack (Task 16). On pop
|
|
1032
1088
|
// the client reports the surfaceId now back on top + the post-pop depth; the
|
|
1033
1089
|
// store knows it. The relay frame carries no sessionKey, so resolve it from
|
|
1034
|
-
// the surface's store entry (sessionForSurface)
|
|
1090
|
+
// the surface's store entry (sessionForSurface). Every plugin-load context
|
|
1091
|
+
// registers one of these handlers on the SHARED relay, so each nav-event
|
|
1092
|
+
// fans out to N contexts but at most one context's store knows the surface —
|
|
1093
|
+
// a context that cannot resolve it must NO-OP. (The old "main" fallback made
|
|
1094
|
+
// the sibling contexts reconcile an empty store carrying stale cross-session
|
|
1095
|
+
// lastDepth: the 3-4x duplicate nav_reconcile on hardware, bug B2.)
|
|
1035
1096
|
if (typeof service.onGlassesUiNavEvent === "function") {
|
|
1036
1097
|
service.onGlassesUiNavEvent((ev) => {
|
|
1037
|
-
const sessionKey = handler.sessionForSurface(ev.surfaceId)
|
|
1098
|
+
const sessionKey = handler.sessionForSurface(ev.surfaceId);
|
|
1099
|
+
if (!sessionKey) {
|
|
1100
|
+
try {
|
|
1101
|
+
if (typeof service.emitGlassesUiLifecycle === "function") {
|
|
1102
|
+
service.emitGlassesUiLifecycle("nav_event_skipped_foreign_surface", "debug", {
|
|
1103
|
+
evSurfaceId: ev.surfaceId,
|
|
1104
|
+
evDepth: ev.depth,
|
|
1105
|
+
});
|
|
1106
|
+
}
|
|
1107
|
+
} catch (_) {
|
|
1108
|
+
// observability must never break the nav path
|
|
1109
|
+
}
|
|
1110
|
+
return;
|
|
1111
|
+
}
|
|
1038
1112
|
handler.handleNavEvent(sessionKey, ev);
|
|
1039
1113
|
});
|
|
1040
1114
|
}
|
|
@@ -51,21 +51,12 @@ function isEvenAiDedicatedKey(sessionKey) {
|
|
|
51
51
|
}
|
|
52
52
|
|
|
53
53
|
function gateReason(sessionKey, deps) {
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
) {
|
|
61
|
-
return "feature_disabled";
|
|
62
|
-
}
|
|
63
|
-
// Hard guard against the agent titling a session before the user has sent
|
|
64
|
-
// their first real message. The synthetic session-starter prompt the agent
|
|
65
|
-
// sees on /new + the proactive prompt-hook nudge can otherwise tempt the
|
|
66
|
-
// model into titling from a non-user input (observed: titles like "New
|
|
67
|
-
// session"). Real user sends are recorded via dispatchOcuClawUserSend ->
|
|
68
|
-
// sessionService.recordFirstSentUserMessage; the synthetic starter never is.
|
|
54
|
+
// Explicit-rename-only: the Neural Session Names toggle governs AUTOMATIC
|
|
55
|
+
// titling (the distiller), not user-requested renames, so feature_disabled is
|
|
56
|
+
// gone. session_user_locked is gone too — a user who already named a session
|
|
57
|
+
// must be able to rename it again (the lock only blocks the distiller).
|
|
58
|
+
// The structural no_active_session / EvenAI-renamable guards live in the
|
|
59
|
+
// handler body. Only the no-user-message guard remains here.
|
|
69
60
|
if (
|
|
70
61
|
typeof deps.hasRecordedUserMessage === "function" &&
|
|
71
62
|
!deps.hasRecordedUserMessage(sessionKey)
|
|
@@ -104,7 +95,9 @@ export function createSessionTitleToolHandler(deps) {
|
|
|
104
95
|
err.code = blockedReason;
|
|
105
96
|
throw err;
|
|
106
97
|
}
|
|
107
|
-
const result = await deps.setSessionTitle(sessionKey, validation.spec.title
|
|
98
|
+
const result = await deps.setSessionTitle(sessionKey, validation.spec.title, {
|
|
99
|
+
origin: "user_tool",
|
|
100
|
+
});
|
|
108
101
|
if (result && result.ok === false) {
|
|
109
102
|
const err = new Error(`${result.code}: ${result.message || "set rejected"}`);
|
|
110
103
|
err.code = result.code;
|
|
@@ -115,60 +108,15 @@ export function createSessionTitleToolHandler(deps) {
|
|
|
115
108
|
return { setSessionTitle };
|
|
116
109
|
}
|
|
117
110
|
|
|
118
|
-
const TOOL_DESCRIPTION = [
|
|
119
|
-
"
|
|
111
|
+
export const TOOL_DESCRIPTION = [
|
|
112
|
+
"Rename the current chat session (shown in the user's glasses session list).",
|
|
120
113
|
"",
|
|
121
|
-
"
|
|
114
|
+
"Call ONLY when the user explicitly asks to rename or retitle the session.",
|
|
115
|
+
"Automatic titling is handled elsewhere — do not call this proactively.",
|
|
122
116
|
"",
|
|
123
|
-
"Title: 2-5 word noun phrase, ≤55 chars, no trailing punctuation or
|
|
124
|
-
"",
|
|
125
|
-
"Do not announce the rename unless the user explicitly asked to retitle.",
|
|
117
|
+
"Title: 2-5 word noun phrase, ≤55 chars, no trailing punctuation or quotes.",
|
|
126
118
|
].join("\n");
|
|
127
119
|
|
|
128
|
-
export function createSessionTitlePromptHook(deps) {
|
|
129
|
-
return function sessionTitleBeforePromptBuild(_event, ctx) {
|
|
130
|
-
const sessionKey = ctx && typeof ctx.sessionKey === "string" ? ctx.sessionKey : null;
|
|
131
|
-
if (!sessionKey) return undefined;
|
|
132
|
-
const title = typeof deps.getSessionTitle === "function" ? deps.getSessionTitle(sessionKey) : null;
|
|
133
|
-
const userLocked =
|
|
134
|
-
typeof deps.isSessionUserLocked === "function" && deps.isSessionUserLocked(sessionKey);
|
|
135
|
-
const featureEnabled =
|
|
136
|
-
typeof deps.isNeuralSessionNamesEnabled === "function"
|
|
137
|
-
? deps.isNeuralSessionNamesEnabled(sessionKey)
|
|
138
|
-
: true;
|
|
139
|
-
|
|
140
|
-
const fragments = [];
|
|
141
|
-
if (title) {
|
|
142
|
-
fragments.push(`Current session title: "${title}".`);
|
|
143
|
-
}
|
|
144
|
-
if (userLocked) {
|
|
145
|
-
fragments.push(
|
|
146
|
-
"The user has set a custom title; do not call set_session_title.",
|
|
147
|
-
);
|
|
148
|
-
} else if (!featureEnabled) {
|
|
149
|
-
fragments.push(
|
|
150
|
-
"Neural Topic Distiller is disabled; do not call set_session_title.",
|
|
151
|
-
);
|
|
152
|
-
} else if (title) {
|
|
153
|
-
fragments.push(
|
|
154
|
-
"Call set_session_title only if the topic has clearly shifted.",
|
|
155
|
-
);
|
|
156
|
-
} else {
|
|
157
|
-
const hasUserMessage =
|
|
158
|
-
typeof deps.hasRecordedUserMessage !== "function" ||
|
|
159
|
-
deps.hasRecordedUserMessage(sessionKey);
|
|
160
|
-
if (hasUserMessage) {
|
|
161
|
-
fragments.push(
|
|
162
|
-
"No session title is set yet. Call set_session_title now if the user's latest message names any concrete topic.",
|
|
163
|
-
);
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
if (fragments.length === 0) return undefined;
|
|
168
|
-
return { appendSystemContext: fragments.join(" ") };
|
|
169
|
-
};
|
|
170
|
-
}
|
|
171
|
-
|
|
172
120
|
export function registerSessionTitleTool(api, service) {
|
|
173
121
|
if (!api || typeof api.registerTool !== "function") {
|
|
174
122
|
throw new Error("registerSessionTitleTool requires api.registerTool");
|
|
@@ -196,14 +144,4 @@ export function registerSessionTitleTool(api, service) {
|
|
|
196
144
|
};
|
|
197
145
|
},
|
|
198
146
|
});
|
|
199
|
-
|
|
200
|
-
if (typeof api.on === "function") {
|
|
201
|
-
const hook = createSessionTitlePromptHook({
|
|
202
|
-
getSessionTitle: (sessionKey) => service.getSessionTitle(sessionKey),
|
|
203
|
-
isSessionUserLocked: (sessionKey) => service.isSessionUserLocked(sessionKey),
|
|
204
|
-
isNeuralSessionNamesEnabled: (sessionKey) => service.isNeuralSessionNamesEnabled(sessionKey),
|
|
205
|
-
hasRecordedUserMessage: (sessionKey) => service.hasRecordedUserMessage(sessionKey),
|
|
206
|
-
});
|
|
207
|
-
api.on("before_prompt_build", hook);
|
|
208
|
-
}
|
|
209
147
|
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import { createSessionTitleToolHandler, TOOL_DESCRIPTION } from "./session-title-tool.ts";
|
|
4
|
+
|
|
5
|
+
function deps(over = {}) {
|
|
6
|
+
const calls = [];
|
|
7
|
+
return {
|
|
8
|
+
calls,
|
|
9
|
+
peekSessionKey: () => "ocuclaw:123",
|
|
10
|
+
setSessionTitle: (k, t, o) => { calls.push({ k, t, o }); return { ok: true }; },
|
|
11
|
+
...over,
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
test("description is explicit-rename-only", () => {
|
|
16
|
+
assert.match(TOOL_DESCRIPTION, /explicitly asks to rename|user explicitly/i);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
test("explicit rename passes origin user_tool", async () => {
|
|
20
|
+
const d = deps();
|
|
21
|
+
const h = createSessionTitleToolHandler(d);
|
|
22
|
+
await h.setSessionTitle({ title: "Trip Planning" });
|
|
23
|
+
assert.equal(d.calls[0].o.origin, "user_tool");
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
test("a user-locked session can STILL be renamed via the tool", async () => {
|
|
27
|
+
const d = deps({
|
|
28
|
+
setSessionTitle: (k, t, o) => {
|
|
29
|
+
// service-layer would allow user_tool over a lock; tool must not pre-block
|
|
30
|
+
return { ok: true };
|
|
31
|
+
},
|
|
32
|
+
});
|
|
33
|
+
const h = createSessionTitleToolHandler(d);
|
|
34
|
+
const r = await h.setSessionTitle({ title: "New Name" });
|
|
35
|
+
assert.deepEqual(r, { ok: true });
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test("feature-disabled does NOT block an explicit rename", async () => {
|
|
39
|
+
const d = deps({ isNeuralSessionNamesEnabled: () => false });
|
|
40
|
+
const h = createSessionTitleToolHandler(d);
|
|
41
|
+
const r = await h.setSessionTitle({ title: "Anything" });
|
|
42
|
+
assert.deepEqual(r, { ok: true });
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test("still rejects an empty title", async () => {
|
|
46
|
+
const h = createSessionTitleToolHandler(deps());
|
|
47
|
+
await assert.rejects(() => h.setSessionTitle({ title: " " }), /title_empty/);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test("still rejects when no active session", async () => {
|
|
51
|
+
const h = createSessionTitleToolHandler(deps({ peekSessionKey: () => "" }));
|
|
52
|
+
await assert.rejects(() => h.setSessionTitle({ title: "X" }), /no_active_session/);
|
|
53
|
+
});
|
package/dist/version.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export const PLUGIN_VERSION = "1.3.
|
|
2
|
-
export const REQUIRES_CLIENT_VERSION = "1.3.
|
|
1
|
+
export const PLUGIN_VERSION = "1.3.2";
|
|
2
|
+
export const REQUIRES_CLIENT_VERSION = "1.3.2";
|
package/openclaw.plugin.json
CHANGED
|
@@ -43,6 +43,11 @@
|
|
|
43
43
|
"help": "Optional extra system prompt appended to Even AI runs only.",
|
|
44
44
|
"advanced": true
|
|
45
45
|
},
|
|
46
|
+
"sessionTitleModel": {
|
|
47
|
+
"label": "Session-title model",
|
|
48
|
+
"help": "Optional model override (\"provider/model\") for the background session-title distiller. Leave blank to use your normal model.",
|
|
49
|
+
"advanced": true
|
|
50
|
+
},
|
|
46
51
|
"evenAiRoutingMode": {
|
|
47
52
|
"label": "Even AI routing mode",
|
|
48
53
|
"help": "active = current session; background = dedicated background session; background_new = fresh background session per request.",
|
|
@@ -226,6 +231,10 @@
|
|
|
226
231
|
"maximum": 8000,
|
|
227
232
|
"default": 5000,
|
|
228
233
|
"description": "How long (ms) a fresh agent summary stays preferred over a tool label in the glasses activity status. Clamped 3000-8000."
|
|
234
|
+
},
|
|
235
|
+
"sessionTitleModel": {
|
|
236
|
+
"type": "string",
|
|
237
|
+
"description": "Optional model override (\"provider/model\") for the background session-title distiller. When absent, the user's normal model is used."
|
|
229
238
|
}
|
|
230
239
|
},
|
|
231
240
|
"if": {
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ocuclaw",
|
|
3
|
-
"version": "1.3.
|
|
4
|
-
"requiresClientVersion": "1.3.
|
|
3
|
+
"version": "1.3.2",
|
|
4
|
+
"requiresClientVersion": "1.3.2",
|
|
5
5
|
"description": "OcuClaw for Even Realities G2 smart glasses.",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"main": "./dist/index.js",
|
|
@@ -40,6 +40,7 @@
|
|
|
40
40
|
},
|
|
41
41
|
"scripts": {
|
|
42
42
|
"build": "node ./scripts/build.mjs",
|
|
43
|
-
"prepare": "node ./scripts/build.mjs"
|
|
43
|
+
"prepare": "node ./scripts/build.mjs",
|
|
44
|
+
"test:unit": "node --import ./scripts/test-loader.mjs --test --test-force-exit \"src/**/*.test.ts\""
|
|
44
45
|
}
|
|
45
46
|
}
|
|
@@ -8,6 +8,13 @@ user-invocable: false
|
|
|
8
8
|
|
|
9
9
|
`render_glasses_ui` paints an interactive surface on the user's Even G2 HUD instead of a text reply. The call blocks until the user selects, dismisses, or backs out. This skill is the source of truth for **authoring** surfaces; the tool description is deliberately lean.
|
|
10
10
|
|
|
11
|
+
## Before you author: is the tool loaded?
|
|
12
|
+
|
|
13
|
+
`render_glasses_ui` is a plugin tool, and depending on the host runtime it may not sit in your initial tool list:
|
|
14
|
+
|
|
15
|
+
1. **Not listed but searchable** — some runtimes (e.g. the Codex harness) defer OpenClaw dynamic tools behind tool search. Search your available/deferred tools for `render_glasses_ui` (it surfaces under the `openclaw` namespace), load it, and proceed.
|
|
16
|
+
2. **Not findable at all** — the host's tool policy is filtering plugin tools. Newer OpenClaw versions (2026.6+) default `tools.profile` to `"coding"`, a base allowlist that strips plugin-owned tools; skills are not policy-filtered, which is why you can read this guide for a tool you cannot call. Don't improvise a workaround: tell the user to run `openclaw config set tools.alsoAllow '["ocuclaw"]' --strict-json` (merging `"ocuclaw"` into any existing `alsoAllow` list rather than overwriting) and restart the gateway, then try again.
|
|
17
|
+
|
|
11
18
|
## Surface kinds
|
|
12
19
|
|
|
13
20
|
| kind | use it for | caps |
|