@inetafrica/open-claudia 3.1.17 → 3.1.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +8 -0
- package/channels/kazee/adapter.js +60 -4
- package/channels/kazee/call/janus-client.js +5 -2
- package/channels/kazee/call/manager.js +13 -3
- package/channels/kazee/call/session.js +108 -3
- package/core/access.js +8 -1
- package/core/handlers.js +4 -2
- package/core/people.js +10 -1
- package/core/recall/ask.js +48 -0
- package/core/recall/index.js +5 -1
- package/core/relationship.js +26 -1
- package/core/router.js +1 -1
- package/core/runner.js +13 -6
- package/core/system-prompt.js +25 -4
- package/core/turn-observer.js +3 -2
- package/package.json +2 -2
- package/test-kazee-call-manager.js +4 -0
- package/test-kazee-call-session.js +47 -0
- package/test-provider-language.js +37 -1
- package/test-recall-engine.js +29 -0
- package/test-runner-watchdog-static.js +8 -0
- package/test-utility-provider-policy.js +6 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## v3.1.21 — Kazee public participation: owner seeding, group tag-gating, guarded external turns
|
|
4
|
+
|
|
5
|
+
- **Owner seeding for kazee-only pods.** A pod whose only owner signal is `KAZEE_OWNER_USER_ID` (empty `auth.json`) never got a people-store owner record, so every unknown inbound — group or DM — was refused with a false "no owner configured" (observed on network-mon, 2026-07-23). `seedOwnerFromLegacy` now falls back to the kazee env owner; legacy `auth.json` owners keep precedence and seeding stays idempotent.
|
|
6
|
+
- **Group chats engage only when addressed.** The kazee adapter fetches chat meta (5-min cache) and, in group chats, engages only on an @-mention of the bot or a reply to the bot's own message — matching chat-central's own `assistantAction` default `assistant_tagged_reply`. Chats set to `assistant_free_reply` engage on everything. Untagged group chatter is dropped silently (no roster prompts, no refusals — owner included). 1:1 chats are unchanged; a meta-fetch failure fails open to pre-gate behavior and is not cached.
|
|
7
|
+
- **Tagged externals get the guarded turn.** An engaged group message authorizes the turn via adapter-attested `envelope.engaged` (never derived from message content), but the speaker still classifies external: replies are vetted by the enforcer and escalate to the owner's approve-once / deny / make-a-rule card — the same pattern as Spaces. Slash commands stay closed to externals.
|
|
8
|
+
- **Escalation cards reach the kazee owner.** `ownerTarget` used to return env-seeded kazee owner handles as the raw user id, but kazee sends need a chat-document id, so approval cards would 404. User-id handles now resolve through `openDirectChat`, falling back to the configured owner target.
|
|
9
|
+
- New release-chain tests: `test-people-owner-seed.js` (env seed, legacy precedence, no-signal refusal) and `test-kazee-group-gate.js` (mention/reply engagement, free-reply, 1:1, meta failure, meta cache, engaged access pass-through).
|
|
10
|
+
|
|
3
11
|
## v3.1.17 — Call bridge joins through the caller's room-setup race (fixes "Call declined")
|
|
4
12
|
|
|
5
13
|
- Answering a Kazee web call could fail deterministically fast: the caller creates the Janus videoroom during its own dial-time setup, and the bot answers quicker than any human — its `join` hit Janus before the room existed, got 426 "No such room", and the whole call was rejected as `connection_failed` (surfacing as "Call declined" in the web UI).
|
|
@@ -28,9 +28,34 @@ class KazeeAdapter {
|
|
|
28
28
|
this.botUserId = botUserId || null;
|
|
29
29
|
this._listeners = { message: new Set(), action: new Set() };
|
|
30
30
|
this._socket = null;
|
|
31
|
+
this._chatMetaCache = new Map();
|
|
31
32
|
this._callManager = new KazeeCallManager({ adapter: this });
|
|
32
33
|
}
|
|
33
34
|
|
|
35
|
+
// chat-central's chat doc carries the group flag and the per-chat assistant
|
|
36
|
+
// policy (assistant_tagged_reply is the platform default for groups). Cached
|
|
37
|
+
// briefly so the participation gate costs one REST fetch per chat, not per
|
|
38
|
+
// message. Failures are NOT cached: a transient error must not stick a chat
|
|
39
|
+
// open or closed.
|
|
40
|
+
async _chatMeta(chatId) {
|
|
41
|
+
const cached = this._chatMetaCache.get(chatId);
|
|
42
|
+
if (cached && Date.now() - cached.at < 5 * 60 * 1000) return cached.meta;
|
|
43
|
+
try {
|
|
44
|
+
const res = await this._request("GET", `/chats/${encodeURIComponent(chatId)}`);
|
|
45
|
+
const chat = res?.chat || res || {};
|
|
46
|
+
if (chat.error) throw new Error(String(chat.error));
|
|
47
|
+
const meta = {
|
|
48
|
+
isGroupChat: !!chat.isGroupChat,
|
|
49
|
+
assistantAction: String(chat.assistantAction || ""),
|
|
50
|
+
};
|
|
51
|
+
this._chatMetaCache.set(chatId, { meta, at: Date.now() });
|
|
52
|
+
return meta;
|
|
53
|
+
} catch (e) {
|
|
54
|
+
console.error(`Kazee chat meta fetch failed (${chatId}):`, e.message);
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
34
59
|
on(event, fn) {
|
|
35
60
|
if (!this._listeners[event]) return () => {};
|
|
36
61
|
this._listeners[event].add(fn);
|
|
@@ -190,12 +215,18 @@ class KazeeAdapter {
|
|
|
190
215
|
}
|
|
191
216
|
}
|
|
192
217
|
|
|
193
|
-
// Reply-to context:
|
|
194
|
-
//
|
|
218
|
+
// Reply-to context: the socket payload may carry the full replied-to doc
|
|
219
|
+
// (chat-central V2) or just its id — resolve either into the same shape
|
|
220
|
+
// Telegram provides. Best-effort — failure is silent.
|
|
195
221
|
if (msg.replyTo) {
|
|
196
222
|
try {
|
|
197
|
-
|
|
198
|
-
|
|
223
|
+
let m = null;
|
|
224
|
+
if (typeof msg.replyTo === "object") {
|
|
225
|
+
m = msg.replyTo;
|
|
226
|
+
} else {
|
|
227
|
+
const data = await this._request("GET", `/message/${encodeURIComponent(String(msg.replyTo))}`);
|
|
228
|
+
m = data?.message || data;
|
|
229
|
+
}
|
|
199
230
|
if (m && (m._id || m.id)) {
|
|
200
231
|
envelope.reply = {
|
|
201
232
|
text: m.content || "",
|
|
@@ -210,6 +241,31 @@ class KazeeAdapter {
|
|
|
210
241
|
} catch (e) { /* best-effort — envelope is still valid without reply context */ }
|
|
211
242
|
}
|
|
212
243
|
|
|
244
|
+
// Group participation gate: in a group chat the bot engages only when
|
|
245
|
+
// addressed — an @-mention of the bot user or a reply to one of its own
|
|
246
|
+
// messages — unless the chat's assistantAction opts into free replies.
|
|
247
|
+
// Everyone is gated, owner included: a bot answering every group message
|
|
248
|
+
// drowns the room. 1:1 chats are never gated. Engaged messages are marked
|
|
249
|
+
// so the router can authorize the turn for non-owner speakers, who then
|
|
250
|
+
// run guarded (mandate/guardrail vetting + owner escalation).
|
|
251
|
+
if (this.botUserId) {
|
|
252
|
+
const meta = await this._chatMeta(channelId);
|
|
253
|
+
if (meta && meta.isGroupChat) {
|
|
254
|
+
const botId = String(this.botUserId);
|
|
255
|
+
const mentions = (Array.isArray(msg.mentions) ? msg.mentions : [])
|
|
256
|
+
.map((m) => String(m?._id || m?.id || m));
|
|
257
|
+
const mentioned = mentions.includes(botId);
|
|
258
|
+
const replyToBot = String(envelope.reply?.from?.id || "") === botId;
|
|
259
|
+
const freeReply = meta.assistantAction === "assistant_free_reply";
|
|
260
|
+
if (!mentioned && !replyToBot && !freeReply) {
|
|
261
|
+
console.log(`Kazee group gate: silent (chat=${channelId} from=${userId})`);
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
envelope.engaged = true;
|
|
265
|
+
envelope.surface = "group";
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
213
269
|
this._emit("message", envelope);
|
|
214
270
|
}
|
|
215
271
|
|
|
@@ -80,7 +80,7 @@ class JanusClient {
|
|
|
80
80
|
this.pending.delete(transaction);
|
|
81
81
|
reject(new Error(`Janus ${payload.janus} timed out`));
|
|
82
82
|
}, this.requestTimeoutMs);
|
|
83
|
-
this.pending.set(transaction, { resolve, reject, timer });
|
|
83
|
+
this.pending.set(transaction, { resolve, reject, timer, verb: payload.janus });
|
|
84
84
|
this.socket.send(JSON.stringify(frame), (error) => {
|
|
85
85
|
if (!error) return;
|
|
86
86
|
clearTimeout(timer);
|
|
@@ -108,7 +108,10 @@ class JanusClient {
|
|
|
108
108
|
try { frame = JSON.parse(raw.toString()); }
|
|
109
109
|
catch (_) { return; }
|
|
110
110
|
const pending = frame.transaction && this.pending.get(frame.transaction);
|
|
111
|
-
|
|
111
|
+
// Plugin requests get an ack then a real event, so acks must not settle
|
|
112
|
+
// them — but keepalive is answered with ONLY an ack, so waiting past it
|
|
113
|
+
// guaranteed a bogus "keepalive timed out" every interval.
|
|
114
|
+
if (pending && (frame.janus !== "ack" || pending.verb === "keepalive")) {
|
|
112
115
|
clearTimeout(pending.timer);
|
|
113
116
|
this.pending.delete(frame.transaction);
|
|
114
117
|
if (frame.janus === "error") pending.reject(new Error(frame.error?.reason || "Janus error"));
|
|
@@ -31,9 +31,13 @@ class KazeeCallManager {
|
|
|
31
31
|
const roomId = Number(payload.roomId);
|
|
32
32
|
const callType = String(payload.callType || "");
|
|
33
33
|
const owner = String(this.adapter.ownerUserId || "");
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
34
|
+
console.log(`Kazee call incoming: room=${roomId} chat=${chatId} from=${callerId} type=${callType}`);
|
|
35
|
+
const reject = (reason) => {
|
|
36
|
+
console.log(`Kazee call rejected: room=${roomId} from=${callerId} reason=${reason}`);
|
|
37
|
+
this.adapter._socket?.emit("call:reject", {
|
|
38
|
+
toUserId: callerId, callType, chatId, roomId, reason,
|
|
39
|
+
});
|
|
40
|
+
};
|
|
37
41
|
|
|
38
42
|
if (!callerId || !chatId || !Number.isSafeInteger(roomId)) return reject("invalid_call");
|
|
39
43
|
if (callerId !== owner) return reject("not_allowed");
|
|
@@ -49,6 +53,7 @@ class KazeeCallManager {
|
|
|
49
53
|
this.session = session;
|
|
50
54
|
try {
|
|
51
55
|
await session.start();
|
|
56
|
+
console.log(`Kazee call accepted: room=${roomId} from=${callerId}`);
|
|
52
57
|
this.adapter._socket?.emit("call:accept", {
|
|
53
58
|
toUserId: callerId, callType: "audio", chatId, roomId,
|
|
54
59
|
});
|
|
@@ -70,6 +75,11 @@ class KazeeCallManager {
|
|
|
70
75
|
// Provider narration/previews are useful in chat but must never be spoken
|
|
71
76
|
// into a call. Only the settled, guarded final answer reaches send().
|
|
72
77
|
callAdapter.suppressStreamPreview = true;
|
|
78
|
+
// Recall/tooltrace banners route through the same send() as the answer —
|
|
79
|
+
// without this flag the TTS reads memory-debug lines to the caller.
|
|
80
|
+
callAdapter.suppressChatVisibility = true;
|
|
81
|
+
// Prompt builder switches to phone-call brevity when it sees this.
|
|
82
|
+
callAdapter.liveCall = true;
|
|
73
83
|
callAdapter.send = async (_channelId, response) => {
|
|
74
84
|
await session.speak(response);
|
|
75
85
|
return true;
|
|
@@ -7,6 +7,12 @@ const { SpeechStream, synthesizePcm } = require("./speech-client");
|
|
|
7
7
|
|
|
8
8
|
const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
9
9
|
|
|
10
|
+
// Per-stage call logging. The media leg (ICE/RTP) can fail with zero thrown
|
|
11
|
+
// errors — a caller then hears silence while the log shows a clean
|
|
12
|
+
// accept→close. Timestamped stage lines make that failure mode visible and
|
|
13
|
+
// double as the STT/TTS timing measurements chat-side estimates lacked.
|
|
14
|
+
const clog = (msg) => console.log(`${new Date().toISOString()} ${msg}`);
|
|
15
|
+
|
|
10
16
|
const BASE_THRESHOLD_DB = -38;
|
|
11
17
|
// While TTS plays, the caller's device can leak bot audio back through its mic.
|
|
12
18
|
// Require a much louder signal before treating incoming audio as human speech.
|
|
@@ -20,6 +26,33 @@ const DEFAULT_JOIN_RETRY_DELAY_MS = 500;
|
|
|
20
26
|
const MAX_DURATION_GOODBYE = "We've hit the call time limit, so I'll hang up now. Message me any time.";
|
|
21
27
|
const IDLE_GOODBYE = "I haven't heard anything for a while, so I'll hang up now. Message me any time.";
|
|
22
28
|
|
|
29
|
+
// Audible state cues: a short ack chime when the caller finishes speaking, then
|
|
30
|
+
// a soft periodic tick while the turn is being worked on — dead air otherwise
|
|
31
|
+
// reads as a dropped call.
|
|
32
|
+
const THINK_TICK_INTERVAL_MS = 1600;
|
|
33
|
+
const THINKING_MAX_MS = 5 * 60_000;
|
|
34
|
+
|
|
35
|
+
function tonePcm48(segments, gain = 0.08) {
|
|
36
|
+
const rate = 48000;
|
|
37
|
+
const fade = Math.floor(rate * 0.012);
|
|
38
|
+
const buffers = segments.map(({ freq, ms }) => {
|
|
39
|
+
const samples = Math.floor((rate * ms) / 1000);
|
|
40
|
+
const buf = Buffer.alloc(samples * 2);
|
|
41
|
+
for (let i = 0; i < samples; i += 1) {
|
|
42
|
+
let a = gain;
|
|
43
|
+
if (i < fade) a *= i / fade;
|
|
44
|
+
if (i > samples - fade) a *= Math.max(0, (samples - i) / fade);
|
|
45
|
+
buf.writeInt16LE(Math.round(Math.sin((2 * Math.PI * freq * i) / rate) * a * 32767), i * 2);
|
|
46
|
+
}
|
|
47
|
+
return buf;
|
|
48
|
+
});
|
|
49
|
+
return Buffer.concat(buffers);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Rising two-tone "heard you", and a quieter single "still here" pulse.
|
|
53
|
+
const ACK_CHIME_PCM48 = tonePcm48([{ freq: 660, ms: 90 }, { freq: 990, ms: 110 }], 0.09);
|
|
54
|
+
const THINK_TICK_PCM48 = tonePcm48([{ freq: 520, ms: 90 }], 0.045);
|
|
55
|
+
|
|
23
56
|
class KazeeCallSession {
|
|
24
57
|
constructor({ roomId, chatId, callerId, botUserId, janusUrl, iceServers = [], onTranscript, onEnded,
|
|
25
58
|
maxCallMs, idleMs, synthesize = synthesizePcm,
|
|
@@ -53,6 +86,8 @@ class KazeeCallSession {
|
|
|
53
86
|
this._lastSpokeAt = 0;
|
|
54
87
|
this._limitTimer = null;
|
|
55
88
|
this._ending = false;
|
|
89
|
+
this._thinkTimer = null;
|
|
90
|
+
this._thinkingSince = 0;
|
|
56
91
|
this.sequence = Math.floor(Math.random() * 65535);
|
|
57
92
|
this.timestamp = Math.floor(Math.random() * 0xffffffff);
|
|
58
93
|
this.ssrc = Math.floor(Math.random() * 0xffffffff);
|
|
@@ -102,7 +137,10 @@ class KazeeCallSession {
|
|
|
102
137
|
await this._subscribe(feedId, publisher.display);
|
|
103
138
|
}
|
|
104
139
|
}
|
|
105
|
-
if (data.videoroom === "joined"
|
|
140
|
+
if (data.videoroom === "joined") {
|
|
141
|
+
clog(`Kazee call room joined: room=${this.roomId} publishers=${(data.publishers || []).length}`);
|
|
142
|
+
if (!this.publisher) await this._publish();
|
|
143
|
+
}
|
|
106
144
|
const gone = Number(data.leaving || data.unpublished || 0);
|
|
107
145
|
if (gone && this.subscribers.has(gone)) await this._removeSubscriber(gone);
|
|
108
146
|
if (data.videoroom === "destroyed") await this.close("room-destroyed");
|
|
@@ -111,6 +149,7 @@ class KazeeCallSession {
|
|
|
111
149
|
async _publish() {
|
|
112
150
|
if (this.publisher) return;
|
|
113
151
|
const pc = new this.PeerConnectionImpl({ iceServers: this.iceServers });
|
|
152
|
+
pc.iceConnectionStateChange?.subscribe?.((state) => clog(`Kazee call publisher ICE: ${state} room=${this.roomId}`));
|
|
114
153
|
const track = new MediaStreamTrack({ kind: "audio" });
|
|
115
154
|
const sender = pc.addTrack(track);
|
|
116
155
|
this.publisher = { pc, track, sender };
|
|
@@ -120,11 +159,14 @@ class KazeeCallSession {
|
|
|
120
159
|
const answer = configured?.jsep;
|
|
121
160
|
if (!answer) throw new Error("Janus publisher configure did not return SDP");
|
|
122
161
|
await pc.setRemoteDescription(answer);
|
|
162
|
+
clog(`Kazee call publisher configured: room=${this.roomId}`);
|
|
123
163
|
}
|
|
124
164
|
|
|
125
165
|
async _subscribe(feedId, display) {
|
|
166
|
+
clog(`Kazee call subscribing: feed=${feedId} display=${String(display || "")}`);
|
|
126
167
|
const handle = await this.janus.attach();
|
|
127
168
|
const pc = new this.PeerConnectionImpl({ iceServers: this.iceServers });
|
|
169
|
+
pc.iceConnectionStateChange?.subscribe?.((state) => clog(`Kazee call subscriber ICE: ${state} feed=${feedId}`));
|
|
128
170
|
const sub = {
|
|
129
171
|
handle,
|
|
130
172
|
pc,
|
|
@@ -142,7 +184,10 @@ class KazeeCallSession {
|
|
|
142
184
|
this.janus.onHandle(handle, (frame) => this._onSubscriberEvent(feedId, frame));
|
|
143
185
|
const attached = await this.janus.message(handle, { request: "join", ptype: "subscriber", room: this.roomId, feed: feedId });
|
|
144
186
|
await this._consumeSubscriberFrame(feedId, attached);
|
|
145
|
-
await sub.stt.connect().
|
|
187
|
+
await sub.stt.connect().then(
|
|
188
|
+
() => clog(`Kazee call STT connected: feed=${feedId}`),
|
|
189
|
+
(error) => console.error("Kazee call STT connect:", error.message),
|
|
190
|
+
);
|
|
146
191
|
}
|
|
147
192
|
|
|
148
193
|
async _onSubscriberEvent(feedId, frame) {
|
|
@@ -165,11 +210,18 @@ class KazeeCallSession {
|
|
|
165
210
|
const answer = await sub.pc.createAnswer();
|
|
166
211
|
await sub.pc.setLocalDescription(answer);
|
|
167
212
|
await this.janus.message(sub.handle, { request: "start", room: this.roomId }, sub.pc.localDescription);
|
|
213
|
+
clog(`Kazee call subscriber started: feed=${feedId}`);
|
|
168
214
|
}
|
|
169
215
|
}
|
|
170
216
|
|
|
171
217
|
_onFeedTranscript(feedId, text) {
|
|
172
218
|
const sub = this.subscribers.get(feedId);
|
|
219
|
+
const len = String(text || "").trim().length;
|
|
220
|
+
clog(`Kazee call transcript: feed=${feedId} chars=${len}${len ? "" : " (noise commit)"}`);
|
|
221
|
+
if (String(text || "").trim()) this._startThinking();
|
|
222
|
+
// Empty finalization (noise commit): nothing dispatches, no reply is
|
|
223
|
+
// coming — don't tick into the void.
|
|
224
|
+
else if (!this._speakActive && !this._speakQueue.length) this._stopThinking();
|
|
173
225
|
this.onTranscript(text, {
|
|
174
226
|
feedId,
|
|
175
227
|
display: sub?.display || "",
|
|
@@ -177,10 +229,49 @@ class KazeeCallSession {
|
|
|
177
229
|
});
|
|
178
230
|
}
|
|
179
231
|
|
|
232
|
+
// Writes a short cue into the outbound track. Yields to real speech: the
|
|
233
|
+
// moment an utterance is active (or starting), remaining frames are dropped.
|
|
234
|
+
async _playCue(pcm48) {
|
|
235
|
+
if (this.closed || this.speaking || this._speakActive || !this.publisher) return;
|
|
236
|
+
for (let offset = 0; offset < pcm48.length; offset += WEBRTC_SAMPLES * 2) {
|
|
237
|
+
if (this.closed || this.speaking || this._speakActive) return;
|
|
238
|
+
let frame = pcm48.subarray(offset, offset + WEBRTC_SAMPLES * 2);
|
|
239
|
+
if (frame.length < WEBRTC_SAMPLES * 2) frame = Buffer.concat([frame, Buffer.alloc(WEBRTC_SAMPLES * 2 - frame.length)]);
|
|
240
|
+
const opus = this.codec.encode(frame);
|
|
241
|
+
const payloadType = this.publisher?.sender?.codec?.payloadType || 111;
|
|
242
|
+
this.publisher?.track?.writeRtp(new RtpPacket(new RtpHeader({
|
|
243
|
+
payloadType, sequenceNumber: this.sequence++ & 0xffff, timestamp: this.timestamp >>> 0, ssrc: this.ssrc,
|
|
244
|
+
}), opus));
|
|
245
|
+
this.timestamp = (this.timestamp + WEBRTC_SAMPLES) >>> 0;
|
|
246
|
+
await wait(20);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
_startThinking() {
|
|
251
|
+
if (this.closed || this._thinkTimer) return;
|
|
252
|
+
this._thinkingSince = Date.now();
|
|
253
|
+
this._thinkTimer = setInterval(() => {
|
|
254
|
+
if (this.closed || Date.now() - this._thinkingSince > THINKING_MAX_MS) return this._stopThinking();
|
|
255
|
+
if (this.speaking || this._speakActive) return;
|
|
256
|
+
this._playCue(THINK_TICK_PCM48).catch(() => {});
|
|
257
|
+
}, THINK_TICK_INTERVAL_MS);
|
|
258
|
+
if (typeof this._thinkTimer.unref === "function") this._thinkTimer.unref();
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
_stopThinking() {
|
|
262
|
+
if (!this._thinkTimer) return;
|
|
263
|
+
clearInterval(this._thinkTimer);
|
|
264
|
+
this._thinkTimer = null;
|
|
265
|
+
}
|
|
266
|
+
|
|
180
267
|
_onFeedRtp(feedId, packet) {
|
|
181
268
|
if (this.closed || !packet?.payload?.length) return;
|
|
182
269
|
const sub = this.subscribers.get(feedId);
|
|
183
270
|
if (!sub) return;
|
|
271
|
+
if (!sub.firstRtpAt) {
|
|
272
|
+
sub.firstRtpAt = Date.now();
|
|
273
|
+
clog(`Kazee call first RTP: feed=${feedId}`);
|
|
274
|
+
}
|
|
184
275
|
let pcm;
|
|
185
276
|
try { pcm = sub.codec.decode(packet.payload); } catch (_) { return; }
|
|
186
277
|
sub.vad.thresholdDb = this.speaking ? SPEAKING_THRESHOLD_DB : BASE_THRESHOLD_DB;
|
|
@@ -193,14 +284,21 @@ class KazeeCallSession {
|
|
|
193
284
|
}
|
|
194
285
|
if (event?.type === "start") {
|
|
195
286
|
this._lastHeard = Date.now();
|
|
287
|
+
clog(`Kazee call VAD start: feed=${feedId}`);
|
|
196
288
|
// Barge-in: the human wins — stop talking and listen.
|
|
197
289
|
if (this.speaking) this.cancelSpeech();
|
|
290
|
+
this._stopThinking();
|
|
198
291
|
for (const frame of sub.prebuffer.splice(0)) this._feedAudio(sub, frame);
|
|
199
292
|
}
|
|
200
293
|
this._feedAudio(sub, pcm16);
|
|
201
294
|
if (event?.type === "commit") {
|
|
202
295
|
this._lastHeard = Date.now();
|
|
296
|
+
clog(`Kazee call VAD commit: feed=${feedId}`);
|
|
203
297
|
sub.stt.commit().catch((error) => console.error("Kazee call STT commit:", error.message));
|
|
298
|
+
this._playCue(ACK_CHIME_PCM48).catch(() => {});
|
|
299
|
+
// STT finalization takes seconds; start ticking at commit so the
|
|
300
|
+
// chime→first-tick stretch doesn't read as a dead line.
|
|
301
|
+
this._startThinking();
|
|
204
302
|
}
|
|
205
303
|
}
|
|
206
304
|
|
|
@@ -241,9 +339,12 @@ class KazeeCallSession {
|
|
|
241
339
|
}
|
|
242
340
|
|
|
243
341
|
async _playUtterance(text, gen) {
|
|
342
|
+
const synthStart = Date.now();
|
|
244
343
|
const synthesized = await this.synthesize(text);
|
|
245
344
|
if (this.closed || gen !== this._speakGen) return false;
|
|
246
345
|
const pcm48 = upsampleTo48(synthesized.pcm, synthesized.sampleRate);
|
|
346
|
+
clog(`Kazee call speak: synth=${Date.now() - synthStart}ms chars=${text.length}`);
|
|
347
|
+
this._stopThinking();
|
|
247
348
|
this.speaking = true;
|
|
248
349
|
try {
|
|
249
350
|
for (let offset = 0; offset < pcm48.length; offset += WEBRTC_SAMPLES * 2) {
|
|
@@ -268,7 +369,9 @@ class KazeeCallSession {
|
|
|
268
369
|
if (this.closed || this._ending) return;
|
|
269
370
|
const now = Date.now();
|
|
270
371
|
const overMax = now - this._startedAt >= this.maxCallMs;
|
|
271
|
-
|
|
372
|
+
// An audible thinking loop means a turn is still being worked on — that is
|
|
373
|
+
// not idleness, or long provider turns would get hung up on mid-answer.
|
|
374
|
+
const speechIdle = !this.speaking && !this._speakActive && !this._speakQueue.length && !this._thinkTimer;
|
|
272
375
|
const idle = speechIdle && now - Math.max(this._lastHeard, this._lastSpokeAt) >= this.idleMs;
|
|
273
376
|
if (!overMax && !idle) return;
|
|
274
377
|
this._ending = true;
|
|
@@ -295,7 +398,9 @@ class KazeeCallSession {
|
|
|
295
398
|
async close(reason = "ended") {
|
|
296
399
|
if (this.closed) return;
|
|
297
400
|
this.closed = true;
|
|
401
|
+
console.log(`Kazee call session closed: room=${this.roomId} reason=${reason}`);
|
|
298
402
|
if (this._limitTimer) { clearInterval(this._limitTimer); this._limitTimer = null; }
|
|
403
|
+
this._stopThinking();
|
|
299
404
|
this.cancelSpeech();
|
|
300
405
|
for (const feed of [...this.subscribers.keys()]) await this._removeSubscriber(feed);
|
|
301
406
|
try { await this.publisher?.pc?.close(); } catch (_) {}
|
package/core/access.js
CHANGED
|
@@ -48,10 +48,17 @@ function saveAuth(auth) {
|
|
|
48
48
|
fs.writeFileSync(AUTH_FILE, JSON.stringify(auth, null, 2));
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
-
function isChatAuthorized(chatId) {
|
|
51
|
+
function isChatAuthorized(chatId, envelope) {
|
|
52
52
|
if (matchesTransportOwner()) return true;
|
|
53
53
|
const id = String(chatId || "");
|
|
54
54
|
if (!id) return false;
|
|
55
|
+
// Adapter-attested engagement (e.g. a kazee group @-mention of the bot):
|
|
56
|
+
// the platform semantics invited the bot into this exchange, so the turn
|
|
57
|
+
// proceeds — the speaker still classifies external and runs fully guarded
|
|
58
|
+
// (reply vetting, action enforcement, owner escalation), same as the Spaces
|
|
59
|
+
// participation rule below. The flag is set only by adapter code, never
|
|
60
|
+
// derived from message content.
|
|
61
|
+
if (envelope && envelope.engaged === true) return true;
|
|
55
62
|
// Spaces task threads authorize by participation: the bot only receives a
|
|
56
63
|
// Spaces notification for a task it follows/was mentioned on, so the inbound
|
|
57
64
|
// itself is the authorization signal. Effective only while the conversational
|
package/core/handlers.js
CHANGED
|
@@ -1166,7 +1166,7 @@ register({
|
|
|
1166
1166
|
});
|
|
1167
1167
|
|
|
1168
1168
|
register({
|
|
1169
|
-
name: "engine", description: "Switch memory recall engine", args: "[classic|discoverer]",
|
|
1169
|
+
name: "engine", description: "Switch memory recall engine", args: "[classic|discoverer|ask]",
|
|
1170
1170
|
handler: async (env, { tail }) => {
|
|
1171
1171
|
if (!authorized(env)) return;
|
|
1172
1172
|
const { settings } = currentState();
|
|
@@ -1183,9 +1183,11 @@ register({
|
|
|
1183
1183
|
send(
|
|
1184
1184
|
`Recall engine: ${active}${settings.recallEngine ? "" : " (default)"}\n\n` +
|
|
1185
1185
|
"• discoverer — typed-edge graph + spreading activation + why-bullets (default)\n" +
|
|
1186
|
-
"• classic — keyword FTS + relevance judge (opt-out fallback)"
|
|
1186
|
+
"• classic — keyword FTS + relevance judge (opt-out fallback)\n" +
|
|
1187
|
+
"• ask — nothing auto-injected; I pull my own notes on demand and say so",
|
|
1187
1188
|
{ keyboard: { inline_keyboard: [
|
|
1188
1189
|
[{ text: "Discoverer (default)", callback_data: "eng:discoverer" }, { text: "Classic", callback_data: "eng:classic" }],
|
|
1190
|
+
[{ text: "Ask (on-demand)", callback_data: "eng:ask" }],
|
|
1189
1191
|
] } },
|
|
1190
1192
|
);
|
|
1191
1193
|
},
|
package/core/people.js
CHANGED
|
@@ -255,7 +255,16 @@ function seedOwnerFromLegacy() {
|
|
|
255
255
|
const auth = JSON.parse(fs.readFileSync(AUTH_FILE, "utf-8"));
|
|
256
256
|
legacyOwner = (auth.authorized || []).find((a) => a.isOwner === true) || null;
|
|
257
257
|
} catch (e) {}
|
|
258
|
-
if (!legacyOwner)
|
|
258
|
+
if (!legacyOwner) {
|
|
259
|
+
// Pods configured with only KAZEE_OWNER_USER_ID (empty auth.json) DO have
|
|
260
|
+
// an owner — without a person record here, every unknown inbound was
|
|
261
|
+
// refused with a false "no owner configured" message.
|
|
262
|
+
const kazeeOnlyOwner = (config && config.KAZEE_OWNER_USER_ID) || "";
|
|
263
|
+
if (!kazeeOnlyOwner) return null;
|
|
264
|
+
const person = add({ name: "Owner", isOwner: true, bio: "Seeded from KAZEE_OWNER_USER_ID" });
|
|
265
|
+
try { linkHandle(person.id, { adapter: "kazee", channelId: String(kazeeOnlyOwner), approvedBy: "bootstrap" }); } catch (e) {}
|
|
266
|
+
return person;
|
|
267
|
+
}
|
|
259
268
|
|
|
260
269
|
const adapter = "telegram";
|
|
261
270
|
const channelId = String(legacyOwner.chatId);
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
// Ask recall engine: no automatic memory injection. Recall becomes something
|
|
2
|
+
// the working model ASKS for instead of something done to it every turn.
|
|
3
|
+
//
|
|
4
|
+
// Why: the push engines' own KPIs (recall-kpi.jsonl, 3 weeks to 2026-07-23)
|
|
5
|
+
// graded 51-89% of injected packs as noise, only ~7% of injections were ever
|
|
6
|
+
// opened, and the discoverer walk taxed full-tier turns 10-16s. Meanwhile the
|
|
7
|
+
// jobs that genuinely need always-on delivery (lessons, speaker notes) never
|
|
8
|
+
// lived in this pipeline. So this engine injects nothing and instead tells
|
|
9
|
+
// the model how to pull: `open-claudia recall query` (the same seed → graph →
|
|
10
|
+
// walker pipeline, run on demand with the model's OWN question) plus the
|
|
11
|
+
// direct pack/entity/transcript commands.
|
|
12
|
+
//
|
|
13
|
+
// The block below is the entire per-turn cost: identical every turn, a few
|
|
14
|
+
// hundred bytes, zero matching, zero walker spawn, zero latency.
|
|
15
|
+
|
|
16
|
+
const ASK_BLOCK = [
|
|
17
|
+
"## Memory (on-demand mode)",
|
|
18
|
+
"No long-term memory was auto-injected this turn. Your notes (context packs, entities, tools, past conversations) are available when YOU decide they're needed:",
|
|
19
|
+
"- `open-claudia recall query \"<question>\" [--context \"<working context>\"] [--json]` — graph walk + cheap-model judgement over the whole memory corpus; returns relevant notes with why + pointers.",
|
|
20
|
+
"- Direct: `open-claudia pack show <dir>` · `entity show <slug>` · `tool search <query>` · `transcript-search \"<query>\"`.",
|
|
21
|
+
"Ask whenever the topic plausibly has history — a person, project, preference, or prior decision you can't fully place. When you do, say so in one short line (e.g. \"taking a look at my notes on X\") so the user sees the lookup.",
|
|
22
|
+
"",
|
|
23
|
+
"",
|
|
24
|
+
].join("\n");
|
|
25
|
+
|
|
26
|
+
async function run(ctx) {
|
|
27
|
+
try {
|
|
28
|
+
require("./metrics").logTurn({
|
|
29
|
+
engine: "ask",
|
|
30
|
+
provider: ctx?.provider || "",
|
|
31
|
+
model: "",
|
|
32
|
+
query: String(ctx?.userText || ""),
|
|
33
|
+
tier: "ask",
|
|
34
|
+
seeds: [],
|
|
35
|
+
activated: [],
|
|
36
|
+
cand: [],
|
|
37
|
+
kept: [],
|
|
38
|
+
gated: false,
|
|
39
|
+
latencyMs: 0,
|
|
40
|
+
walkerMs: 0,
|
|
41
|
+
walker: "off",
|
|
42
|
+
costUsd: 0,
|
|
43
|
+
});
|
|
44
|
+
} catch (e) { /* metrics continuity is best-effort */ }
|
|
45
|
+
return { packBlock: ASK_BLOCK, entityBlock: "", packMatches: [], entityMatches: [] };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
module.exports = { name: "ask", run, ASK_BLOCK };
|
package/core/recall/index.js
CHANGED
|
@@ -7,14 +7,18 @@
|
|
|
7
7
|
// to the RECALL_ENGINE env var, then to DEFAULT_ENGINE. Unknown names fall
|
|
8
8
|
// back to the default so a bad value can never break recall. The discoverer is
|
|
9
9
|
// the default: it's fail-open (falls back to the classic keyword baseline on
|
|
10
|
-
// any error), so it never recalls worse than classic, only better.
|
|
10
|
+
// any error), so it never recalls worse than classic, only better. `ask`
|
|
11
|
+
// injects nothing and points the model at on-demand recall instead
|
|
12
|
+
// (`open-claudia recall query`) — pull, not push.
|
|
11
13
|
|
|
12
14
|
const classic = require("./classic");
|
|
13
15
|
const discoverer = require("./discoverer");
|
|
16
|
+
const ask = require("./ask");
|
|
14
17
|
|
|
15
18
|
const ENGINES = {
|
|
16
19
|
classic,
|
|
17
20
|
discoverer,
|
|
21
|
+
ask,
|
|
18
22
|
};
|
|
19
23
|
|
|
20
24
|
// The default recall engine for channels that haven't explicitly chosen one
|
package/core/relationship.js
CHANGED
|
@@ -161,10 +161,35 @@ async function ownerTarget() {
|
|
|
161
161
|
String(h.channelId) === String(owner.primaryChannel.channelId))
|
|
162
162
|
: null;
|
|
163
163
|
const handle = primary || handles[0];
|
|
164
|
-
if (handle)
|
|
164
|
+
if (handle) {
|
|
165
|
+
const resolved = await resolveHandleSendTarget(handle);
|
|
166
|
+
if (resolved) return { ...resolved, person: owner };
|
|
167
|
+
}
|
|
165
168
|
}
|
|
166
169
|
} catch (e) { /* fall through to the env fallback */ }
|
|
167
170
|
return configuredOwnerTarget();
|
|
168
171
|
}
|
|
169
172
|
|
|
173
|
+
// Env/legacy-seeded kazee owner handles store the owner's USER id, but kazee
|
|
174
|
+
// sends address a chat-document id — posting buttons to the raw user id 404s
|
|
175
|
+
// and escalation silently dies. Resolve those handles to the 1:1 DM the same
|
|
176
|
+
// way configuredOwnerTarget does; every other handle is already a send target.
|
|
177
|
+
async function resolveHandleSendTarget(handle) {
|
|
178
|
+
const adapter = String(handle.adapter || "");
|
|
179
|
+
const channelId = String(handle.channelId || "");
|
|
180
|
+
if (adapter !== "kazee") return { adapter, channelId };
|
|
181
|
+
let kzOwner = "";
|
|
182
|
+
try { const cfg = require("./config"); kzOwner = String((cfg.config && cfg.config.KAZEE_OWNER_USER_ID) || ""); } catch (e) {}
|
|
183
|
+
if (!kzOwner || channelId !== kzOwner) return { adapter, channelId };
|
|
184
|
+
try {
|
|
185
|
+
const registry = require("./adapter-registry");
|
|
186
|
+
const kazee = (registry.getAdapters() || []).find((a) => a.type === "kazee");
|
|
187
|
+
if (kazee && typeof kazee.openDirectChat === "function") {
|
|
188
|
+
const chatId = await kazee.openDirectChat(channelId);
|
|
189
|
+
if (chatId) return { adapter, channelId: String(chatId) };
|
|
190
|
+
}
|
|
191
|
+
} catch (e) { /* outreach unavailable → let the env fallback decide */ }
|
|
192
|
+
return null;
|
|
193
|
+
}
|
|
194
|
+
|
|
170
195
|
module.exports = { speakerFor, currentSpeaker, guardActive, isCurrentSpeakerGuarded, ownerTarget, configuredOwnerTarget };
|
package/core/router.js
CHANGED
|
@@ -140,7 +140,7 @@ async function onMessage(envelope) {
|
|
|
140
140
|
return;
|
|
141
141
|
}
|
|
142
142
|
|
|
143
|
-
if (!isChatAuthorized(envelope.channelId)) {
|
|
143
|
+
if (!isChatAuthorized(envelope.channelId, envelope)) {
|
|
144
144
|
if (envelope.type === "text") {
|
|
145
145
|
if (isDuplicate(envelope)) return;
|
|
146
146
|
await introFlow.handleInbound(envelope, (text) => send(text));
|
package/core/runner.js
CHANGED
|
@@ -224,6 +224,13 @@ function telegramHtmlOpts(extra = {}) {
|
|
|
224
224
|
return adapter?.type === "telegram" ? { ...extra, parseMode: "HTML" } : extra;
|
|
225
225
|
}
|
|
226
226
|
|
|
227
|
+
// Status chatter (queue acks, usage alerts, reviewer/guardrail notes) must not
|
|
228
|
+
// reach an adapter whose send() is spoken aloud (live call turns) — only the
|
|
229
|
+
// settled reply belongs on the line.
|
|
230
|
+
function statusChatterMuted() {
|
|
231
|
+
try { return currentAdapter()?.suppressChatVisibility === true; } catch (e) { return false; }
|
|
232
|
+
}
|
|
233
|
+
|
|
227
234
|
function stableRunError(message, code = "PROVIDER_RUN_FAILED", details = {}) {
|
|
228
235
|
const error = new Error(redactSensitive(String(message || "Provider run failed")));
|
|
229
236
|
error.name = "ProviderRunError";
|
|
@@ -1523,7 +1530,7 @@ async function persistForegroundUsage(result, runContext, state = currentState()
|
|
|
1523
1530
|
state,
|
|
1524
1531
|
usage,
|
|
1525
1532
|
result.costUsd || 0,
|
|
1526
|
-
(text) => chatContext.run(store, () => send(text)),
|
|
1533
|
+
(text) => chatContext.run(store, () => (statusChatterMuted() ? true : send(text))),
|
|
1527
1534
|
{
|
|
1528
1535
|
backend: runContext.provider,
|
|
1529
1536
|
model: runContext.providerSettings?.model || null,
|
|
@@ -1925,7 +1932,7 @@ async function handoffMemoryReview(result, runContext, store, state) {
|
|
|
1925
1932
|
return speaker ? { name: speaker.name, isOwner: !!speaker.isOwner } : null;
|
|
1926
1933
|
} catch (_) { return null; }
|
|
1927
1934
|
})(),
|
|
1928
|
-
announce: showRecall ? (text) => chatContext.run(store, () => send(text)) : null,
|
|
1935
|
+
announce: showRecall ? (text) => chatContext.run(store, () => (statusChatterMuted() ? true : send(text))) : null,
|
|
1929
1936
|
});
|
|
1930
1937
|
}
|
|
1931
1938
|
|
|
@@ -1941,7 +1948,7 @@ async function considerGuardrail(result, runContext, store) {
|
|
|
1941
1948
|
const relationship = require("./relationship");
|
|
1942
1949
|
const speaker = relationship.speakerFor(store?.adapter?.type, store?.channelId, store?.userId);
|
|
1943
1950
|
if (relationship.guardActive(speaker)) return; // external speakers never capture
|
|
1944
|
-
const announce = (text, opts) => chatContext.run(store, () => send(text, opts));
|
|
1951
|
+
const announce = (text, opts) => chatContext.run(store, () => (statusChatterMuted() ? true : send(text, opts)));
|
|
1945
1952
|
await guardrail.consider({ text: runContext.userPrompt, speaker, announce });
|
|
1946
1953
|
} catch (e) { /* best-effort — a guardrail miss must never break a turn */ }
|
|
1947
1954
|
}
|
|
@@ -2132,9 +2139,9 @@ function runAgent(prompt, cwd, replyToMsgId, opts = {}) {
|
|
|
2132
2139
|
state.messageQueue.push({ runContext, store, deferred, queuedAt: Date.now() });
|
|
2133
2140
|
// Transient queue/compaction acks are a chat-UX affordance (Telegram/Kazee).
|
|
2134
2141
|
// On a Spaces task thread they'd post a throwaway "Queued." comment on the
|
|
2135
|
-
// task — noise.
|
|
2136
|
-
//
|
|
2137
|
-
if (currentAdapter()?.type !== "spaces") {
|
|
2142
|
+
// task — noise. On a live call, send() is the TTS line: "Queued." would be
|
|
2143
|
+
// spoken to the caller. Both stay quiet and just deliver the real reply.
|
|
2144
|
+
if (currentAdapter()?.type !== "spaces" && !statusChatterMuted()) {
|
|
2138
2145
|
send(state.isCompacting ? "Compacting context, will pick this up next…" : "Queued.", { replyTo: replyToMsgId }).catch(() => {});
|
|
2139
2146
|
}
|
|
2140
2147
|
return deferred.promise;
|
package/core/system-prompt.js
CHANGED
|
@@ -361,7 +361,9 @@ function buildSpeakerContextBlock() {
|
|
|
361
361
|
: "unscoped chat";
|
|
362
362
|
const blocks = [
|
|
363
363
|
"## Channel context",
|
|
364
|
-
|
|
364
|
+
adapter?.liveCall === true
|
|
365
|
+
? "- Interface: LIVE VOICE CALL on Kazee — the user is speaking on an open line; replies are spoken aloud to them."
|
|
366
|
+
: `- Interface: ${channelLabel} chat through Open Claudia.`,
|
|
365
367
|
];
|
|
366
368
|
|
|
367
369
|
const speaker = adapter && channelId
|
|
@@ -984,6 +986,16 @@ function bumpFtsMissCounter(n) {
|
|
|
984
986
|
// prompt) ONLY on voice-input turns of the voice channel — i.e. exactly when the
|
|
985
987
|
// reply will be read aloud by TTS (same gate as the streaming/auto-speak path in
|
|
986
988
|
// the runner). Text channels (Telegram/Kazee) and typed turns never see it.
|
|
989
|
+
const LIVE_CALL_GUIDANCE = [
|
|
990
|
+
"",
|
|
991
|
+
"",
|
|
992
|
+
"## Live voice call",
|
|
993
|
+
"You are on a LIVE voice call with the user right now. Their speech arrives transcribed; your final reply is synthesized straight onto the open line. Write it to be HEARD:",
|
|
994
|
+
"- Phone-call style: one to three short conversational sentences, then stop. No markdown, bullets, headings, emoji, backticks, or asterisks — they are noise when spoken.",
|
|
995
|
+
"- Do NOT read out code, file paths, command syntax, IDs, or URLs. Describe them in plain words, or offer to send the details to the chat as text.",
|
|
996
|
+
"- Tool time is dead air the caller sits through. Prefer answering from what you already know; keep any lookup to a couple of seconds, and offer to do longer digging after the call.",
|
|
997
|
+
].join("\n");
|
|
998
|
+
|
|
987
999
|
const VOICE_REPLY_GUIDANCE = [
|
|
988
1000
|
"",
|
|
989
1001
|
"",
|
|
@@ -1012,7 +1024,10 @@ const VOICE_NOTE_GUIDANCE = [
|
|
|
1012
1024
|
|
|
1013
1025
|
function voiceReplyGuidance(opts = {}) {
|
|
1014
1026
|
try {
|
|
1015
|
-
const { currentTransport } = require("./context");
|
|
1027
|
+
const { currentTransport, currentAdapter } = require("./context");
|
|
1028
|
+
// A live Kazee call: the reply is spoken onto the open line, so call-style
|
|
1029
|
+
// brevity outranks every chat-formatting rule.
|
|
1030
|
+
if (currentAdapter()?.liveCall === true) return LIVE_CALL_GUIDANCE;
|
|
1016
1031
|
const state = promptState(opts);
|
|
1017
1032
|
if (currentTransport() === "voice") return state.lastInputWasVoice ? VOICE_REPLY_GUIDANCE : "";
|
|
1018
1033
|
const { VOICE_REPLY_MODE } = require("./config");
|
|
@@ -1054,7 +1069,13 @@ async function buildOptionalRecallContext(prompt, opts = {}) {
|
|
|
1054
1069
|
const budget = memoryRecallBudget();
|
|
1055
1070
|
const settings = promptState(opts).settings || {};
|
|
1056
1071
|
const recall = require("./recall");
|
|
1057
|
-
|
|
1072
|
+
// Live call: the discoverer's LLM walk is many seconds of dead air the
|
|
1073
|
+
// caller sits through, so calls downgrade it to classic (~50ms keyword
|
|
1074
|
+
// baseline). The ask engine is already zero-latency (injects nothing, model
|
|
1075
|
+
// pulls on demand and narrates the lookup), so it rides through unchanged.
|
|
1076
|
+
const activeName = recall.activeEngineName(settings);
|
|
1077
|
+
const engineName = currentAdapter()?.liveCall === true && activeName === "discoverer" ? "classic" : activeName;
|
|
1078
|
+
const engine = recall.getEngine(engineName);
|
|
1058
1079
|
const recallOpts = { ...opts, recallCollector: collector };
|
|
1059
1080
|
const recallProvider = promptRunIdentity({ ...opts, state: promptState(opts) }).provider;
|
|
1060
1081
|
const helpers = {
|
|
@@ -1078,7 +1099,7 @@ async function buildOptionalRecallContext(prompt, opts = {}) {
|
|
|
1078
1099
|
const why = result.why || {};
|
|
1079
1100
|
collector.packDirs = (result.packMatches || []).map((match) => match.dir).filter(Boolean);
|
|
1080
1101
|
collector.recall = {
|
|
1081
|
-
engine: engine.name ||
|
|
1102
|
+
engine: engine.name || engineName,
|
|
1082
1103
|
gated: !!result.gated,
|
|
1083
1104
|
tier: result.tier || "",
|
|
1084
1105
|
packs: (result.packMatches || []).map((match) => ({ name: match.name || match.dir, why: why[`pack:${match.dir}`] || "" })),
|
package/core/turn-observer.js
CHANGED
|
@@ -38,11 +38,12 @@ function createTurnObserver({ state, store }) {
|
|
|
38
38
|
// Spaces is a customer-facing surface that renders neither Telegram HTML nor
|
|
39
39
|
// internal recall/tool banners meaningfully — a <b> banner shows the literal
|
|
40
40
|
// tags. Suppress all owner-only chat visibility there; memory + graph feeding
|
|
41
|
-
// still run, only the chat emissions are muted.
|
|
41
|
+
// still run, only the chat emissions are muted. Adapters may also opt out
|
|
42
|
+
// explicitly (voice calls: send() feeds TTS, so a banner would be spoken).
|
|
42
43
|
let suppressChatVisibility = false;
|
|
43
44
|
try {
|
|
44
45
|
const adapter = chatContext.run(store, () => currentAdapter());
|
|
45
|
-
suppressChatVisibility = adapter?.type === "spaces";
|
|
46
|
+
suppressChatVisibility = adapter?.type === "spaces" || adapter?.suppressChatVisibility === true;
|
|
46
47
|
} catch (e) { suppressChatVisibility = false; }
|
|
47
48
|
|
|
48
49
|
const packsLib = require("./packs");
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@inetafrica/open-claudia",
|
|
3
|
-
"version": "3.1.
|
|
3
|
+
"version": "3.1.21",
|
|
4
4
|
"description": "An always-on, provider-agnostic coding-agent harness for Claude Code and OpenAI Codex via chat",
|
|
5
5
|
"main": "bot.js",
|
|
6
6
|
"bin": {
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
"setup": "node setup.js",
|
|
12
12
|
"start": "node bot.js",
|
|
13
13
|
"pretest": "node test-provider-fixture.js && node test-provider-core-prompt.js && node test-turn-observer.js && node test-provider-prompt-parity.js && node test-provider-env-isolation.js && node test-provider-resume-parity.js && node test-provider-bootstrap.js && node test-provider-registry.js && node test-provider-stream-decoder.js && node test-provider-events.js && node test-provider-claude.js && node test-provider-codex.js && node test-provider-runner.js && node test-provider-refusal-recovery.js && node test-provider-run-lifecycle.js && node test-provider-gateway.js && node test-provider-side-chat.js && node test-single-runtime.js && node test-provider-migration-backup.js && node test-provider-state-migration.js && node test-provider-session-history.js && node test-provider-session-commands.js && node test-provider-compaction.js && node test-provider-scheduler.js && node test-credential-capture.js && node test-cursor-removal.js && node test-utility-provider-policy.js && node test-provider-subagent.js && node test-provider-recall.js && node test-provider-pack-review.js && node test-memory-mutation-queue.js && node test-provider-dream.js && node test-provider-enforcer.js && node test-provider-tool-hooks.js && node test-provider-boot-matrix.js && node test-provider-capabilities.js && node test-provider-language.js && node test-provider-coupling-audit.js && node test-provider-parity-e2e.js && node test-kazee-channel-health.js && node test-kazee-media-caption.js && node test-prompt-budget.js && node test-agency-ledger.js && node test-agency-deliberate.js && node test-guardrail.js && node test-spaces-comment-content.js && node test-boot-health.js && node test-lexical-floor.js && node test-safe-mode.js && node test-logout.js && node test-kazee-call-audio.js && node test-kazee-call-janus.js && node test-kazee-call-webrtc.js && node test-kazee-call-speech.js && node test-kazee-call-manager.js && node test-kazee-call-session.js",
|
|
14
|
-
"test": "OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node -e \"require('./vault'); console.log('OK')\" && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-runner-watchdog-static.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-usage-accounting.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-recall-engine.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-recall-graph.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-recall-discoverer.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-recall-query.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-walker-score.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-score-gate.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-cokept-warming.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-project-transcripts-smoke.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-abilities.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-ability-extraction.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-ability-couse.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-ability-transfer.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-ability-tiers.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-ability-merge-guard.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-learning-e2e.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-pack-nesting.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-pack-search.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-extractive-injection.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-journal-dedupe.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-read-signal.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-tools.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-tool-graph.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-tool-guard.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-tooling-mode.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-tool-manifest.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-recall-evolution.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-approval-async.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-unified-identity.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-queue-routing.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-persona-packs.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-speaker-persona.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-persona-pipeline.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-recall-relationship-gate.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-relationship.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-enforcer.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-identity-prune.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-kazee-message-id.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-delivery-contract.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-run-lock.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-web-sessions.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-telegram-poll-recovery.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-telegram-409-grace.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-telegram-conflict-heal-guard.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-single-instance.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-streaming-split.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-spaces-autoenable.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-say-loopback.js"
|
|
14
|
+
"test": "OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node -e \"require('./vault'); console.log('OK')\" && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-runner-watchdog-static.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-usage-accounting.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-recall-engine.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-recall-graph.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-recall-discoverer.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-recall-query.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-walker-score.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-score-gate.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-cokept-warming.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-project-transcripts-smoke.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-abilities.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-ability-extraction.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-ability-couse.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-ability-transfer.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-ability-tiers.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-ability-merge-guard.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-learning-e2e.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-pack-nesting.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-pack-search.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-extractive-injection.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-journal-dedupe.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-read-signal.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-tools.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-tool-graph.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-tool-guard.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-tooling-mode.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-tool-manifest.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-recall-evolution.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-approval-async.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-unified-identity.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-queue-routing.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-persona-packs.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-speaker-persona.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-persona-pipeline.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-recall-relationship-gate.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-relationship.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-enforcer.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-identity-prune.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-kazee-message-id.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-kazee-group-gate.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-people-owner-seed.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-delivery-contract.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-run-lock.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-web-sessions.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-telegram-poll-recovery.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-telegram-409-grace.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-telegram-conflict-heal-guard.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-single-instance.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-streaming-split.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-spaces-autoenable.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-say-loopback.js"
|
|
15
15
|
},
|
|
16
16
|
"files": [
|
|
17
17
|
"bot.js",
|
|
@@ -28,6 +28,10 @@ function fixture() {
|
|
|
28
28
|
assert.strictEqual(f.events[0].name, "call:accept");
|
|
29
29
|
f.manager.session.options.onTranscript("What is on my agenda?");
|
|
30
30
|
assert.strictEqual(f.inbound[0].payload.raw.ephemeralAudio, true);
|
|
31
|
+
// Call turns must mute observer banners (spoken otherwise) and flag live-call
|
|
32
|
+
// mode so the prompt builder switches to phone-style brevity.
|
|
33
|
+
assert.strictEqual(f.inbound[0].payload.adapter.suppressChatVisibility, true);
|
|
34
|
+
assert.strictEqual(f.inbound[0].payload.adapter.liveCall, true);
|
|
31
35
|
await f.inbound[0].payload.adapter.send("chat", "Your agenda is clear.");
|
|
32
36
|
assert.strictEqual(f.manager.session.spoken, "Your agenda is clear.");
|
|
33
37
|
await f.manager.ended({ roomId: 12 });
|
|
@@ -228,6 +228,52 @@ async function testJoinGivesUpAfterRetries() {
|
|
|
228
228
|
await session.close();
|
|
229
229
|
}
|
|
230
230
|
|
|
231
|
+
async function testThinkingCues() {
|
|
232
|
+
const { session } = makeSession({
|
|
233
|
+
synthesize: async () => ({ pcm: Buffer.alloc(WEBRTC_SAMPLES * 2 * 3), sampleRate: 48000 }),
|
|
234
|
+
});
|
|
235
|
+
let frames = 0;
|
|
236
|
+
session.publisher = { track: { writeRtp: () => { frames += 1; } }, sender: { codec: { payloadType: 111 } } };
|
|
237
|
+
await session._subscribe(101, "caller");
|
|
238
|
+
const sub = session.subscribers.get(101);
|
|
239
|
+
const gen = new OpusAudioCodec();
|
|
240
|
+
|
|
241
|
+
// Caller finishes a sentence: speech then sustained silence commits the turn,
|
|
242
|
+
// plays an audible "heard you" chime, and starts ticking right away — STT
|
|
243
|
+
// finalization takes seconds and must not sound like a dead line.
|
|
244
|
+
for (let i = 0; i < 3; i += 1) session._onFeedRtp(101, { payload: toneOpus(gen, 12000) });
|
|
245
|
+
for (let i = 0; i < 36; i += 1) session._onFeedRtp(101, { payload: toneOpus(gen, 30) });
|
|
246
|
+
assert.strictEqual(sub.stt.commits, 1);
|
|
247
|
+
await wait(60);
|
|
248
|
+
assert(frames > 0, "commit plays the ack chime");
|
|
249
|
+
assert(session._thinkTimer, "thinking loop starts at commit, before the transcript");
|
|
250
|
+
|
|
251
|
+
// A noise commit finalizes to empty text: nothing dispatches, so the ticks
|
|
252
|
+
// stop rather than promising an answer that will never come.
|
|
253
|
+
sub.stt.onTranscript(" ");
|
|
254
|
+
assert.strictEqual(session._thinkTimer, null, "empty transcript stops the thinking loop");
|
|
255
|
+
|
|
256
|
+
// Real transcript dispatch (re)starts the loop; it counts as activity for
|
|
257
|
+
// the idle check, and the answer's playback stops it.
|
|
258
|
+
sub.stt.onTranscript("question");
|
|
259
|
+
assert(session._thinkTimer, "thinking loop starts on transcript");
|
|
260
|
+
session._startedAt = Date.now();
|
|
261
|
+
session._lastHeard = Date.now() - 10_000;
|
|
262
|
+
session._lastSpokeAt = Date.now() - 10_000;
|
|
263
|
+
session.idleMs = 50;
|
|
264
|
+
await session._enforceLimits();
|
|
265
|
+
assert.strictEqual(session.closed, false, "audible thinking is not idleness");
|
|
266
|
+
await session.speak("answer");
|
|
267
|
+
assert.strictEqual(session._thinkTimer, null, "answer playback stops the thinking loop");
|
|
268
|
+
|
|
269
|
+
// A fresh thinking loop stops the moment the caller starts talking again.
|
|
270
|
+
session._startThinking();
|
|
271
|
+
for (let i = 0; i < 3; i += 1) session._onFeedRtp(101, { payload: toneOpus(gen, 12000) });
|
|
272
|
+
assert.strictEqual(session._thinkTimer, null, "caller speech stops the thinking loop");
|
|
273
|
+
gen.close();
|
|
274
|
+
await session.close();
|
|
275
|
+
}
|
|
276
|
+
|
|
231
277
|
async function testCloseResolvesSpeech() {
|
|
232
278
|
const { session } = makeSession({
|
|
233
279
|
synthesize: async () => ({ pcm: Buffer.alloc(WEBRTC_SAMPLES * 2 * 100), sampleRate: 48000 }),
|
|
@@ -254,6 +300,7 @@ async function testCloseResolvesSpeech() {
|
|
|
254
300
|
await testMaxDuration();
|
|
255
301
|
await testJoinRetriesNoSuchRoom();
|
|
256
302
|
await testJoinGivesUpAfterRetries();
|
|
303
|
+
await testThinkingCues();
|
|
257
304
|
await testCloseResolvesSpeech();
|
|
258
305
|
console.log("kazee call session: ok");
|
|
259
306
|
})().catch((error) => { console.error(error); process.exitCode = 1; });
|
|
@@ -227,7 +227,43 @@ assert.match(pkg.description, /provider-agnostic coding-agent harness/i);
|
|
|
227
227
|
// the Janus publisher join through transient 426 "No such room" — the caller
|
|
228
228
|
// creates the room at dial time and the bot answers faster than any human, so
|
|
229
229
|
// the single naked join lost the race deterministically.
|
|
230
|
-
|
|
230
|
+
// 3.1.18 approved by Sumeet 2026-07-23 ("Yes those three make sense but I
|
|
231
|
+
// think we need a better UI for the calls with AI… an audio sound to show
|
|
232
|
+
// listening and thinking", then "still stuff like queued and token usage alert
|
|
233
|
+
// etc came through"): live-call hygiene + audible UX — recall/tooltrace
|
|
234
|
+
// banners, queue acks, usage alerts, and reviewer/guardrail notes are never
|
|
235
|
+
// spoken into a call; a phone-style live-call prompt mode; call lifecycle
|
|
236
|
+
// logging; ack chime + thinking ticks starting at VAD commit; and call turns
|
|
237
|
+
// pin classic recall so memory lookup stops costing ~17s of dead air.
|
|
238
|
+
// 3.1.19 approved by Sumeet 2026-07-23 ("can we make it a flag so the recall
|
|
239
|
+
// mode has this as an option", after auditing push recall's own KPIs: 51-89%
|
|
240
|
+
// noise, ~7% of injections ever opened, 10-16s full-tier latency tax): new
|
|
241
|
+
// `ask` recall engine — /engine ask injects nothing per turn and instead
|
|
242
|
+
// instructs the model to pull its own memory on demand via `open-claudia
|
|
243
|
+
// recall query` and narrate the lookup; live-call pin now downgrades only the
|
|
244
|
+
// discoverer to classic, so ask rides through calls at zero latency.
|
|
245
|
+
// 3.1.20 approved by Sumeet 2026-07-23 ("We will come back to the voice stuff
|
|
246
|
+
// just push out what we have as a new version"): call per-stage media
|
|
247
|
+
// diagnostics — ISO-timestamped stage logs (ICE states, room-join publisher
|
|
248
|
+
// count, subscribe/start, first RTP, VAD start/commit, STT connect, transcript
|
|
249
|
+
// length, TTS synth ms) so a silent media-leg failure (accepted → nothing
|
|
250
|
+
// heard → remote-ended with zero errors) becomes visible and STT/TTS timings
|
|
251
|
+
// stop being estimates — plus the Janus keepalive ack fix (Janus answers
|
|
252
|
+
// keepalive with ONLY an ack; acks now settle keepalive requests alone,
|
|
253
|
+
// ending the bogus per-interval "keepalive timed out" log noise).
|
|
254
|
+
// 3.1.21 approved by Sumeet 2026-07-23 ("Proceed as you see fit and fix in a
|
|
255
|
+
// production ready way work through till it's fully fixed you have my approval
|
|
256
|
+
// unless you need confirmation"): kazee public participation — pods whose only
|
|
257
|
+
// owner signal is KAZEE_OWNER_USER_ID now seed an owner person record (an
|
|
258
|
+
// empty auth.json used to turn every unknown inbound into a false "no owner
|
|
259
|
+
// configured" refusal — network-mon, 2026-07-23); kazee group chats honor
|
|
260
|
+
// chat-central's assistantAction (default tagged-reply: the bot engages only
|
|
261
|
+
// on @-mention or a reply to its own message; free-reply engages everything);
|
|
262
|
+
// engaged externals run the guarded external turn with the owner
|
|
263
|
+
// approve/deny/make-a-rule escalation, same pattern as Spaces; and
|
|
264
|
+
// ownerTarget resolves kazee user-id owner handles to a DM chat id via
|
|
265
|
+
// openDirectChat so escalation cards actually deliver.
|
|
266
|
+
assert.strictEqual(pkg.version, "3.1.21", "release version must remain unchanged without explicit approval");
|
|
231
267
|
for (const keyword of ["claude", "codex", "coding-agent", "provider-agnostic"]) {
|
|
232
268
|
assert.ok(pkg.keywords.includes(keyword), `package keyword missing: ${keyword}`);
|
|
233
269
|
}
|
package/test-recall-engine.js
CHANGED
|
@@ -10,9 +10,38 @@ assert.strictEqual(recall.activeEngineName({ recallEngine: "classic" }), "classi
|
|
|
10
10
|
assert.strictEqual(recall.activeEngineName({ recallEngine: "nope" }), "discoverer", "unknown → default");
|
|
11
11
|
assert.strictEqual(recall.activeEngineName({ recallEngine: "CLASSIC" }), "classic", "case-insensitive");
|
|
12
12
|
assert.ok(recall.listEngines().includes("classic"));
|
|
13
|
+
assert.ok(recall.listEngines().includes("ask"), "ask engine must be selectable");
|
|
14
|
+
assert.strictEqual(recall.activeEngineName({ recallEngine: "ask" }), "ask", "ask selectable per channel");
|
|
13
15
|
assert.strictEqual(typeof recall.getEngine("classic").run, "function");
|
|
14
16
|
assert.strictEqual(recall.getEngine("bogus").name, "classic", "getEngine hard-floor stays classic (crash-proof)");
|
|
15
17
|
|
|
18
|
+
// Live-call fast-path: the discoverer's LLM walk is dead air a caller sits
|
|
19
|
+
// through, so call turns downgrade discoverer→classic — but the ask engine
|
|
20
|
+
// (zero injection, zero latency) must ride through unchanged.
|
|
21
|
+
{
|
|
22
|
+
const fs = require("fs");
|
|
23
|
+
const src = fs.readFileSync(require.resolve("./core/system-prompt.js"), "utf8");
|
|
24
|
+
assert.match(
|
|
25
|
+
src,
|
|
26
|
+
/liveCall === true && activeName === "discoverer" \? "classic" : activeName/,
|
|
27
|
+
"live-call turns must downgrade only the discoverer, preserving ask",
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// --- ask engine: injects nothing but the on-demand instructions ---
|
|
32
|
+
(async () => {
|
|
33
|
+
const ask = recall.getEngine("ask");
|
|
34
|
+
const out = await ask.run({ userText: "hello", provider: "claude" });
|
|
35
|
+
assert.strictEqual(out.packMatches.length, 0, "ask injects no packs");
|
|
36
|
+
assert.strictEqual(out.entityMatches.length, 0, "ask injects no entities");
|
|
37
|
+
assert.strictEqual(out.entityBlock, "", "ask has no entity block");
|
|
38
|
+
assert.match(out.packBlock, /on-demand mode/, "ask block declares the mode");
|
|
39
|
+
assert.match(out.packBlock, /recall query/, "ask block must point at the pull command");
|
|
40
|
+
assert.match(out.packBlock, /taking a look at my notes/, "ask block must ask the model to narrate lookups");
|
|
41
|
+
const out2 = await ask.run({});
|
|
42
|
+
assert.strictEqual(out2.packBlock, out.packBlock, "ask block is constant — no per-turn matching");
|
|
43
|
+
})().catch((e) => { console.error(e); process.exit(1); });
|
|
44
|
+
|
|
16
45
|
// env override when no setting
|
|
17
46
|
const prev = process.env.RECALL_ENGINE;
|
|
18
47
|
process.env.RECALL_ENGINE = "classic";
|
|
@@ -22,6 +22,14 @@ assert.match(lifecycle, /killTree\(proc\?\.pid,\s*"SIGKILL"\)/, 'watchdog must f
|
|
|
22
22
|
assert.match(lifecycle, /await terminationPromise/, 'terminal settlement must await forced descendant cleanup');
|
|
23
23
|
assert.match(runner, /await drainQueuedMessages\(state\)/, 'watchdog path must not strand queued follow-up messages');
|
|
24
24
|
|
|
25
|
+
// Live-call hygiene: status chatter must never be spoken down the line. Each
|
|
26
|
+
// in-turn status emitter (queue ack, usage alert, memory-review + guardrail
|
|
27
|
+
// announces) must consult statusChatterMuted(), which honors the call
|
|
28
|
+
// adapter's suppressChatVisibility flag.
|
|
29
|
+
assert.match(runner, /function statusChatterMuted\(\)[\s\S]{0,200}suppressChatVisibility === true/, 'statusChatterMuted must key off adapter.suppressChatVisibility');
|
|
30
|
+
assert.match(runner, /!== "spaces" && !statusChatterMuted\(\)/, 'queue/compaction ack must stay silent on live-call turns');
|
|
31
|
+
assert.strictEqual((runner.match(/statusChatterMuted\(\) \? true : send\(/g) || []).length, 3, 'usage alert, memory-review announce, and guardrail announce must all mute on live-call turns');
|
|
32
|
+
|
|
25
33
|
assert.ok(monolith.split('\n').length <= 12, 'bot-agent.js must be only a compatibility shim');
|
|
26
34
|
assert.match(monolith, /require\(["']\.\/bot["']\)/, 'legacy entrypoint must delegate to bot.js');
|
|
27
35
|
assert.ok(!/(?:OC_TURN_IDLE_MS|CLAUDE_PATH|spawn\(|parseStream|writeFile|saveState|codex)/i.test(monolith), 'compatibility shim must contain no watchdog, provider, parser, or writer implementation');
|
|
@@ -474,7 +474,12 @@ async function main() {
|
|
|
474
474
|
},
|
|
475
475
|
})),
|
|
476
476
|
env: { UTILITY_PROVIDER: "claude" },
|
|
477
|
-
|
|
477
|
+
// Grace must outlive scheduler starvation on a loaded runner: at 400ms the
|
|
478
|
+
// SIGKILL escalation raced the child's TERM handler before it could write
|
|
479
|
+
// drain-signal.json (same class as the 3.1.16 CI deflakes). The child
|
|
480
|
+
// normally exits on TERM within milliseconds; 2000ms only bounds the
|
|
481
|
+
// emergency path and still fits inside the 5000ms cancelAll budget.
|
|
482
|
+
killGraceMs: 2000,
|
|
478
483
|
});
|
|
479
484
|
const drainingRun = drainService.spawnUtilityAgent("shutdown drain", {
|
|
480
485
|
purpose: "review", systemPrompt: "drain", cwd: root,
|