@inetafrica/open-claudia 3.5.27 → 3.5.33
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 +5 -6
- package/channels/kazee/call/manager.js +2 -137
- package/channels/kazee/call/session.js +2 -24
- package/core/actions.js +8 -0
- package/core/chatter.js +41 -0
- package/core/handlers.js +29 -3
- package/core/router.js +3 -2
- package/core/runner.js +7 -2
- package/core/system-prompt.js +3 -11
- package/core/turn-observer.js +18 -11
- package/package.json +2 -2
- package/test-kazee-call-manager.js +0 -5
- package/test-kazee-call-session.js +0 -3
- package/test-provider-language.js +5 -5
- package/test-provider-prompt-parity.js +0 -1
- package/test-thread-lanes.js +3 -0
- package/channels/kazee/call/quick-voice.js +0 -295
package/CHANGELOG.md
CHANGED
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
## 3.5.
|
|
3
|
+
## 3.5.33
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Quiet chats by default (Sumeet, 11 Oct 08:51 EAT, on Kazee unread counts: one voice note brought a "Heard:" echo, a progress message and "Recalled my notes" lines before the answer, each a new unread and often a push; 09:16 EAT "Yes" to turning them off by default on every bot). Built on 3.5.24, the fleet's version: the Browser canaries (3.5.25, 3.5.26, 3.5.28) and the call builds on Sumeet's own bot (3.5.27 and later) are not in it.
|
|
6
6
|
|
|
7
|
-
- **
|
|
8
|
-
- **
|
|
9
|
-
- **
|
|
10
|
-
- **Tests:** `test-call-quick-voice.js` (new, in `npm test`), `test-kazee-call-manager.js`, `test-kazee-call-session.js`, `test-provider-prompt-parity.js`.
|
|
7
|
+
- **One message in, one answer out.** By default a turn now shows "typing" while it works and sends only its answer: no "Heard: …" echo of a voice note or audio file, no live progress message, and no "📖 Recalled my notes on …", "✏️ Updating my notes on …", new pack, entity, skill or "🧩 Reused" lines. Recall-failure alerts, /recall, /tooltrace, wake-up notices and limit warnings are unchanged.
|
|
8
|
+
- **Each comes back with its own command** (`core/chatter.js`): `/heard`, `/notes` and `/progress` (on, off, or no argument for the current value and buttons), saved per person like /verbose. `/verbose` streams thinking into the progress message, so it turns the progress message on too: anyone with /verbose on keeps it until they turn /verbose off.
|
|
9
|
+
- **Tests:** `test-quiet-defaults.js` (new, in `npm test`), `test-thread-lanes.js` (its live-preview check turns /progress on).
|
|
11
10
|
|
|
12
11
|
## 3.5.24
|
|
13
12
|
|
|
@@ -5,7 +5,6 @@ const { CallLeaseHeartbeat } = require("./lease");
|
|
|
5
5
|
const { publicCallIdentity, requestRoomAccess, supportedIceServers } = require("./access");
|
|
6
6
|
const { canonicalForChannel } = require("../../../core/identity");
|
|
7
7
|
const { KazeeCallSession } = require("./session");
|
|
8
|
-
const quickVoice = require("./quick-voice");
|
|
9
8
|
|
|
10
9
|
const DEFAULT_JANUS_URL = "wss://janus.kazee.africa/ws";
|
|
11
10
|
const DEFAULT_ICE_SERVERS = [
|
|
@@ -15,34 +14,6 @@ const DEFAULT_ICE_SERVERS = [
|
|
|
15
14
|
// FCM push TTL is 60s and chat-central treats ringing logs as fresh for 90s.
|
|
16
15
|
// This is the outbound invitation deadline; terminal server events end it early.
|
|
17
16
|
const RING_TIMEOUT_MS = 60_000;
|
|
18
|
-
const qlog = (msg) => console.log(`${new Date().toISOString()} Kazee call quick: ${msg}`);
|
|
19
|
-
// The quick line must be ready this soon after the words, or it is skipped:
|
|
20
|
-
// by then the full agent is close to speaking itself.
|
|
21
|
-
const QUICK_LINE_MAX_MS = 3500;
|
|
22
|
-
// While the full agent works: a "still on it" line after this much quiet,
|
|
23
|
-
// at most a few per turn, never after the answer.
|
|
24
|
-
const QUICK_FILLER_QUIET_MS = 8000;
|
|
25
|
-
const QUICK_FILLER_MAX = 3;
|
|
26
|
-
const QUICK_TICK_MS = 500;
|
|
27
|
-
|
|
28
|
-
// The provider the call's chat runs on: the quick voice uses its CLI and
|
|
29
|
-
// credentials, as the full agent's turns do.
|
|
30
|
-
function chatProvider({ adapter, chatId, userId }) {
|
|
31
|
-
const { runInChat } = require("../../../core/context");
|
|
32
|
-
const { conversationKeyFor, roomKeyFor } = require("../../../core/identity");
|
|
33
|
-
const transport = adapter?.type || "kazee";
|
|
34
|
-
const id = runInChat({
|
|
35
|
-
adapter, channelId: String(chatId), userId: String(userId), transport,
|
|
36
|
-
canonicalUserId: canonicalForChannel(transport, String(userId)),
|
|
37
|
-
conversationKey: conversationKeyFor(transport, String(chatId)),
|
|
38
|
-
roomKey: roomKeyFor(transport, String(chatId)),
|
|
39
|
-
}, () => {
|
|
40
|
-
const state = require("../../../core/state");
|
|
41
|
-
return state.getActiveProvider(state.currentState());
|
|
42
|
-
});
|
|
43
|
-
return id ? require("../../../core/providers").registry.getProvider(id) : null;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
17
|
const OUTGOING_GREETING = "Hey, it's Claudia. Can you hear me okay?";
|
|
47
18
|
// Inbound must greet too: callers get silence until they speak first, which
|
|
48
19
|
// reads as a dead call (live test 2026-08-08 — media was up, caller hung up).
|
|
@@ -50,8 +21,7 @@ const INCOMING_GREETING = "Hey, it's Claudia — you're through. Can you hear me
|
|
|
50
21
|
|
|
51
22
|
class KazeeCallManager {
|
|
52
23
|
constructor({ adapter, SessionImpl = KazeeCallSession, janusUrl = process.env.KAZEE_JANUS_URL || DEFAULT_JANUS_URL,
|
|
53
|
-
iceServers = DEFAULT_ICE_SERVERS, deviceId = randomUUID()
|
|
54
|
-
quickSettings = quickVoice.quickVoiceSettings, quickTiming = {} } = {}) {
|
|
24
|
+
iceServers = DEFAULT_ICE_SERVERS, deviceId = randomUUID() } = {}) {
|
|
55
25
|
this.adapter = adapter;
|
|
56
26
|
this.SessionImpl = SessionImpl;
|
|
57
27
|
this.janusUrl = janusUrl;
|
|
@@ -60,10 +30,6 @@ class KazeeCallManager {
|
|
|
60
30
|
this.deviceId = deviceId;
|
|
61
31
|
this._pending = null;
|
|
62
32
|
this._ringTimer = null;
|
|
63
|
-
this.resolveQuickProvider = resolveQuickProvider;
|
|
64
|
-
this.quickSpawn = quickSpawn;
|
|
65
|
-
this.quickSettings = quickSettings;
|
|
66
|
-
this.quickTiming = { lineMaxMs: QUICK_LINE_MAX_MS, fillerQuietMs: QUICK_FILLER_QUIET_MS, fillerMax: QUICK_FILLER_MAX, tickMs: QUICK_TICK_MS, ...quickTiming };
|
|
67
33
|
}
|
|
68
34
|
|
|
69
35
|
wire(socket) {
|
|
@@ -123,7 +89,6 @@ class KazeeCallManager {
|
|
|
123
89
|
// Several terminal paths clear this.session before close() calls back.
|
|
124
90
|
// Invalidate the closed call's work even when it no longer owns the slot.
|
|
125
91
|
session?._voiceTurn?.controller.abort("call-ended");
|
|
126
|
-
this._stopQuickVoice(session);
|
|
127
92
|
if (this.session !== session) return;
|
|
128
93
|
this._detachSession(session);
|
|
129
94
|
if (this.adapter._socket?.connected) this.adapter._socket.emit("call:end", {
|
|
@@ -133,7 +98,6 @@ class KazeeCallManager {
|
|
|
133
98
|
|
|
134
99
|
_detachSession(session) {
|
|
135
100
|
session?._voiceTurn?.controller.abort("call-ended");
|
|
136
|
-
this._stopQuickVoice(session);
|
|
137
101
|
if (this.session === session) this.session = null;
|
|
138
102
|
}
|
|
139
103
|
|
|
@@ -187,14 +151,12 @@ class KazeeCallManager {
|
|
|
187
151
|
...this._sessionOptions(config, access, boundIdentity, pending.controller.signal), greeting: INCOMING_GREETING,
|
|
188
152
|
onTranscript: (text, meta) => this._transcript(text, { ...payload, ...boundIdentity }, meta, session),
|
|
189
153
|
onSpeechStart: () => this._speechStart(session),
|
|
190
|
-
onSpeechNoise: () => this._speechNoise(session),
|
|
191
154
|
onMediaReady: () => this._mediaReady(session),
|
|
192
155
|
onEnded: reason => { session?._leaseHeartbeat?.close(); pending.controller.abort(); this._sessionEnded(session, reason); },
|
|
193
156
|
});
|
|
194
157
|
session.callIdentity = boundIdentity;
|
|
195
158
|
session.chatId = chatId;
|
|
196
159
|
this.session = session;
|
|
197
|
-
this._startQuickVoice(session, chatId, callerId);
|
|
198
160
|
this._manageLease(session, access);
|
|
199
161
|
await session.start();
|
|
200
162
|
if (this.session !== session || session.closed || pending.controller.signal.aborted) return;
|
|
@@ -236,14 +198,12 @@ class KazeeCallManager {
|
|
|
236
198
|
...this._sessionOptions(config, access, identity, pending.controller.signal), asCaller: true, greeting: OUTGOING_GREETING,
|
|
237
199
|
onTranscript: (text, meta) => this._transcript(text, remote, meta, session),
|
|
238
200
|
onSpeechStart: () => this._speechStart(session),
|
|
239
|
-
onSpeechNoise: () => this._speechNoise(session),
|
|
240
201
|
onMediaReady: () => this._mediaReady(session),
|
|
241
202
|
onEnded: reason => { session?._leaseHeartbeat?.close(); pending.controller.abort(); this._clearRingTimer(); this._sessionEnded(session, reason); },
|
|
242
203
|
});
|
|
243
204
|
session.callIdentity = identity;
|
|
244
205
|
session.chatId = chat;
|
|
245
206
|
this.session = session;
|
|
246
|
-
this._startQuickVoice(session, chat, target);
|
|
247
207
|
this._manageLease(session, access);
|
|
248
208
|
await session.start();
|
|
249
209
|
if (this.session !== session || session.closed || pending.controller.signal.aborted) throw new Error("Call cancelled");
|
|
@@ -287,96 +247,6 @@ class KazeeCallManager {
|
|
|
287
247
|
// complete transcript exists; a cough must not discard an accepted ask.
|
|
288
248
|
}
|
|
289
249
|
|
|
290
|
-
// The sound that muted the turn had no words in it: not a new request, so
|
|
291
|
-
// the answer being worked on may speak again (11 Oct: wind while walking
|
|
292
|
-
// muted a lookup's answer for good, and the caller heard nothing).
|
|
293
|
-
_speechNoise(session) {
|
|
294
|
-
const turn = session?._voiceTurn;
|
|
295
|
-
if (!turn?.muted || turn.controller.signal.aborted || this.session !== session || session.closed) return;
|
|
296
|
-
turn.muted = false;
|
|
297
|
-
console.log(`${new Date().toISOString()} Kazee call noise: muted turn resumed`);
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
// ── Quick voice ───────────────────────────────────────────────────
|
|
301
|
-
|
|
302
|
-
_startQuickVoice(session, chatId, userId) {
|
|
303
|
-
let settings;
|
|
304
|
-
try { settings = this.quickSettings(); } catch (_) { return; }
|
|
305
|
-
if (!settings?.enabled) return;
|
|
306
|
-
let provider = null;
|
|
307
|
-
try { provider = this.resolveQuickProvider({ adapter: this.adapter, chatId, userId }); } catch (_) {}
|
|
308
|
-
if (!quickVoice.QuickVoiceProcess.supported(provider)) {
|
|
309
|
-
qlog(`off: provider ${provider?.id || "none"} has no warm stream`);
|
|
310
|
-
return;
|
|
311
|
-
}
|
|
312
|
-
const quick = quickVoice.track(new quickVoice.QuickVoiceProcess({
|
|
313
|
-
provider, settings, spawnImpl: this.quickSpawn, log: qlog,
|
|
314
|
-
systemPrompt: quickVoice.buildQuickPrompt({ ownerName: quickVoice.ownerDisplayName() }),
|
|
315
|
-
}));
|
|
316
|
-
session._quick = quick;
|
|
317
|
-
// The quick voice says the first line itself; the session's delayed cue
|
|
318
|
-
// would only double it.
|
|
319
|
-
session.acknowledgement = async () => null;
|
|
320
|
-
try { quick.start(); } catch (error) {
|
|
321
|
-
qlog(`start failed: ${error.message}`);
|
|
322
|
-
session._quick = null;
|
|
323
|
-
quick.stop().catch?.(() => {});
|
|
324
|
-
}
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
_stopQuickVoice(session) {
|
|
328
|
-
const quick = session?._quick;
|
|
329
|
-
if (!quick) return;
|
|
330
|
-
session._quick = null;
|
|
331
|
-
quick.stop().catch?.(() => {});
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
// One utterance: a short fitting line as soon as the quick voice has it,
|
|
335
|
-
// then a "still on it" line whenever the full agent leaves the caller in
|
|
336
|
-
// silence, until its answer arrives. Never over the caller, never after
|
|
337
|
-
// the answer, never once a newer utterance took over.
|
|
338
|
-
_quickTurn(session, turn, text) {
|
|
339
|
-
const quick = session._quick;
|
|
340
|
-
const timing = this.quickTiming;
|
|
341
|
-
const live = () => !turn.muted && !turn.done && !turn.controller.signal.aborted &&
|
|
342
|
-
!session.closed && this.session === session && session._voiceTurn === turn && session._quick === quick;
|
|
343
|
-
const sayLine = async (message, label, { maxMs = Infinity, unlessSpoke = false } = {}) => {
|
|
344
|
-
const asked = Date.now();
|
|
345
|
-
const { generation, text: reply } = quick.say(message);
|
|
346
|
-
const cancel = () => quick.cancel(generation);
|
|
347
|
-
turn.controller.signal.addEventListener("abort", cancel, { once: true });
|
|
348
|
-
let line = "";
|
|
349
|
-
try { line = quickVoice.spokenLine(await reply); }
|
|
350
|
-
finally { turn.controller.signal.removeEventListener("abort", cancel); }
|
|
351
|
-
const ready = Date.now() - asked;
|
|
352
|
-
if (!line || !live() || ready > maxMs || (unlessSpoke && turn.spoke) || session.callerTalking?.()) {
|
|
353
|
-
qlog(`${label} skipped ready=${ready}ms`);
|
|
354
|
-
return false;
|
|
355
|
-
}
|
|
356
|
-
qlog(`${label} ready=${ready}ms chars=${line.length}`);
|
|
357
|
-
const spoken = await session.speak(line);
|
|
358
|
-
if (spoken === false || !live()) return false;
|
|
359
|
-
this._rememberSpeech(session, "assistant", line);
|
|
360
|
-
return true;
|
|
361
|
-
};
|
|
362
|
-
(async () => {
|
|
363
|
-
await sayLine(quickVoice.utteranceMessage(text), "line", { maxMs: timing.lineMaxMs, unlessSpoke: true });
|
|
364
|
-
let fillers = 0;
|
|
365
|
-
let lastFillerAt = Date.now();
|
|
366
|
-
while (fillers < timing.fillerMax && live()) {
|
|
367
|
-
await new Promise((resolve) => { const timer = setTimeout(resolve, timing.tickMs); timer.unref?.(); });
|
|
368
|
-
if (!live()) break;
|
|
369
|
-
if (session.speechBusy?.() || session.callerTalking?.()) continue;
|
|
370
|
-
const quietSince = Math.max(turn.startedAt, lastFillerAt, session._lastSpokeAt || 0);
|
|
371
|
-
if (Date.now() - quietSince < timing.fillerQuietMs) continue;
|
|
372
|
-
fillers += 1;
|
|
373
|
-
lastFillerAt = Date.now();
|
|
374
|
-
await sayLine(quickVoice.fillerMessage(text, (Date.now() - turn.startedAt) / 1000), `filler ${fillers}`);
|
|
375
|
-
lastFillerAt = Date.now();
|
|
376
|
-
}
|
|
377
|
-
})().catch((error) => qlog(`turn failed: ${error.message}`));
|
|
378
|
-
}
|
|
379
|
-
|
|
380
250
|
_rememberSpeech(session, role, text) {
|
|
381
251
|
const history = session._voiceHistory ||= [];
|
|
382
252
|
if (role === "assistant" && history.at(-1)?.role === "assistant") {
|
|
@@ -392,7 +262,7 @@ class KazeeCallManager {
|
|
|
392
262
|
const speaker = String(meta.display || "").trim();
|
|
393
263
|
const attributed = (meta.participants || 0) > 1 && speaker ? `${speaker}: ${text}` : text;
|
|
394
264
|
session._voiceTurn?.controller.abort("superseded");
|
|
395
|
-
const turn = { controller: new AbortController(), muted: false
|
|
265
|
+
const turn = { controller: new AbortController(), muted: false };
|
|
396
266
|
session._voiceTurn = turn;
|
|
397
267
|
const callAdapter = Object.create(this.adapter);
|
|
398
268
|
callAdapter.callHistory = (session._voiceHistory || []).map(entry => ({ ...entry }));
|
|
@@ -407,8 +277,6 @@ class KazeeCallManager {
|
|
|
407
277
|
callAdapter.suppressChatVisibility = true;
|
|
408
278
|
// Prompt builder switches to phone-call brevity when it sees this.
|
|
409
279
|
callAdapter.liveCall = true;
|
|
410
|
-
// The quick voice says the first line; the prompt then skips its own.
|
|
411
|
-
callAdapter.quickVoice = !!session._quick?.ready();
|
|
412
280
|
const spokenSentences = [];
|
|
413
281
|
callAdapter.isCallTurnActive = () => !turn.muted && !turn.controller.signal.aborted &&
|
|
414
282
|
!session.closed && this.session === session && session._voiceTurn === turn;
|
|
@@ -419,7 +287,6 @@ class KazeeCallManager {
|
|
|
419
287
|
if (!callAdapter.isCallTurnActive()) return false;
|
|
420
288
|
const spoken = await session.speak(response);
|
|
421
289
|
if (spoken !== false && callAdapter.isCallTurnActive()) {
|
|
422
|
-
turn.spoke = true;
|
|
423
290
|
this._rememberSpeech(session, "assistant", response);
|
|
424
291
|
return true;
|
|
425
292
|
}
|
|
@@ -431,7 +298,6 @@ class KazeeCallManager {
|
|
|
431
298
|
return spoken;
|
|
432
299
|
};
|
|
433
300
|
callAdapter.send = async (_channelId, response) => {
|
|
434
|
-
turn.done = true;
|
|
435
301
|
const remainder = require("../../../core/call-speech").unspokenSuffix(response, spokenSentences);
|
|
436
302
|
if (remainder) await speak(remainder);
|
|
437
303
|
return true;
|
|
@@ -442,7 +308,6 @@ class KazeeCallManager {
|
|
|
442
308
|
console.warn(`Kazee call ignored file voice response ${voicePath}; streamed TTS is active`);
|
|
443
309
|
return true;
|
|
444
310
|
};
|
|
445
|
-
if (callAdapter.quickVoice) this._quickTurn(session, turn, attributed);
|
|
446
311
|
this.adapter._emit("message", {
|
|
447
312
|
adapter: callAdapter,
|
|
448
313
|
channelId: String(incoming.chatId),
|
|
@@ -54,7 +54,7 @@ const ACK_DELAY_MS = 2800;
|
|
|
54
54
|
class KazeeCallSession {
|
|
55
55
|
constructor({ roomId, chatId, callerId, botUserId, janusUrl, janusToken, janusTokenExpiresAt, roomToken, roomTokenExpiresAt, managedRoom = false, refreshRoomAccess, iceServers = [], iceCredentialsExpiresAt, onTranscript, onEnded, onMediaReady,
|
|
56
56
|
maxCallMs, idleMs, finalWaitMs, synthesize = synthesizePcm, synthesizeStream = null, asCaller = false, greeting = null,
|
|
57
|
-
turnDecision = detectTurn, acknowledgement = selectAcknowledgement, onSpeechStart,
|
|
57
|
+
turnDecision = detectTurn, acknowledgement = selectAcknowledgement, onSpeechStart,
|
|
58
58
|
JanusClientImpl = JanusClient, PeerConnectionImpl = RTCPeerConnection, SpeechStreamImpl = SpeechStream } = {}) {
|
|
59
59
|
this.roomId = Number(roomId);
|
|
60
60
|
this.chatId = String(chatId);
|
|
@@ -83,7 +83,6 @@ class KazeeCallSession {
|
|
|
83
83
|
this._transportExpiryTimer = null;
|
|
84
84
|
this.onTranscript = onTranscript || (() => {});
|
|
85
85
|
this.onSpeechStart = onSpeechStart || (() => {});
|
|
86
|
-
this.onSpeechNoise = onSpeechNoise || (() => {});
|
|
87
86
|
this.turnDecision = turnDecision;
|
|
88
87
|
this.acknowledgement = acknowledgement;
|
|
89
88
|
this._cueController = null;
|
|
@@ -586,7 +585,6 @@ class KazeeCallSession {
|
|
|
586
585
|
if (countWords(trimmed) < MIN_DISPATCH_WORDS) {
|
|
587
586
|
clog(`Kazee call noise turn dropped: feed=${feedId} words=${countWords(trimmed)}`);
|
|
588
587
|
if (!this._speakActive && !this._speakQueue.length) this._stopThinking();
|
|
589
|
-
this._speechWasNoise(feedId);
|
|
590
588
|
return;
|
|
591
589
|
}
|
|
592
590
|
}
|
|
@@ -595,7 +593,7 @@ class KazeeCallSession {
|
|
|
595
593
|
// Empty finalization (noise commit): nothing dispatches, no reply is
|
|
596
594
|
// coming — don't tick into the void.
|
|
597
595
|
else if (!this._speakActive && !this._speakQueue.length) this._stopThinking();
|
|
598
|
-
if (!trimmed)
|
|
596
|
+
if (!trimmed) return;
|
|
599
597
|
this.onTranscript(text, {
|
|
600
598
|
feedId,
|
|
601
599
|
display: sub?.display || "",
|
|
@@ -604,16 +602,6 @@ class KazeeCallSession {
|
|
|
604
602
|
});
|
|
605
603
|
}
|
|
606
604
|
|
|
607
|
-
// Speech start muted the turn being worked on, in case it was a new ask. A
|
|
608
|
-
// sound the recogniser found no words in (wind, footsteps, a cough) was not:
|
|
609
|
-
// unless someone is still talking or another segment is pending, the
|
|
610
|
-
// muted turn gets its voice back.
|
|
611
|
-
_speechWasNoise(feedId) {
|
|
612
|
-
if ([...this.subscribers.values()].some(peer => peer?.vad?.active || peer?.turnGate?.pending ||
|
|
613
|
-
peer?.awaitingWordsSince || peer?.segments?.size)) return;
|
|
614
|
-
this.onSpeechNoise({ feedId });
|
|
615
|
-
}
|
|
616
|
-
|
|
617
605
|
_reportMediaReady() {
|
|
618
606
|
if (this.closed || this._reportedMediaReady || !this._sentRtp || !this._receivedRtp) return;
|
|
619
607
|
this._reportedMediaReady = true;
|
|
@@ -763,16 +751,6 @@ class KazeeCallSession {
|
|
|
763
751
|
sub.stt.audio(pcm16).catch((error) => console.error("Kazee call STT send:", error.message));
|
|
764
752
|
}
|
|
765
753
|
|
|
766
|
-
// For the quick voice: someone is talking (or a turn is being decided), and
|
|
767
|
-
// speech is playing or queued.
|
|
768
|
-
callerTalking() {
|
|
769
|
-
return [...this.subscribers.values()].some(sub => sub?.vad?.active || sub?.turnGate?.pending || sub?.awaitingWordsSince);
|
|
770
|
-
}
|
|
771
|
-
|
|
772
|
-
speechBusy() {
|
|
773
|
-
return !!(this.speaking || this._speakActive || this._speakQueue.length);
|
|
774
|
-
}
|
|
775
|
-
|
|
776
754
|
speak(text) {
|
|
777
755
|
if (this.closed || !String(text || "").trim()) return Promise.resolve(false);
|
|
778
756
|
return new Promise((resolve, reject) => {
|
package/core/actions.js
CHANGED
|
@@ -6,6 +6,7 @@ const fs = require("fs");
|
|
|
6
6
|
const path = require("path");
|
|
7
7
|
const { CONFIG_DIR, WORKSPACE, CHAT_ID } = require("./config");
|
|
8
8
|
const { send } = require("./io");
|
|
9
|
+
const { TOGGLES: chatterToggles } = require("./chatter");
|
|
9
10
|
const {
|
|
10
11
|
currentState, saveState,
|
|
11
12
|
getActiveProvider, getProjectKey, consumeSessionCallbackToken,
|
|
@@ -959,6 +960,13 @@ async function handleAction(envelope) {
|
|
|
959
960
|
await send(`Verbose thinking: ${state.settings.showThinking ? "on" : "off"}`);
|
|
960
961
|
return;
|
|
961
962
|
}
|
|
963
|
+
const chatterToggle = chatterToggles.find((toggle) => d.startsWith(`${toggle.callback}:`));
|
|
964
|
+
if (chatterToggle) {
|
|
965
|
+
state.settings[chatterToggle.key] = d.slice(chatterToggle.callback.length + 1) === "on";
|
|
966
|
+
saveState();
|
|
967
|
+
await send(`${chatterToggle.label}: ${state.settings[chatterToggle.key] ? "on" : "off"}`);
|
|
968
|
+
return;
|
|
969
|
+
}
|
|
962
970
|
if (d.startsWith("cw:")) {
|
|
963
971
|
const v = d.slice(3);
|
|
964
972
|
if (v === "default") state.settings.compactWindow = null;
|
package/core/chatter.js
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
// Opt-in chat chatter. Each of these posts an extra message next to the
|
|
2
|
+
// answer (a new unread and, on a phone, a push), so all are off by default:
|
|
3
|
+
// one voice note in, "typing" while the bot works, one answer out (Sumeet,
|
|
4
|
+
// 11 Oct 2026, on Kazee unread counts). Each has a command to turn it on.
|
|
5
|
+
// /heard — the "Heard: …" echo of a transcribed voice note or audio file
|
|
6
|
+
// /notes — "📖 Recalled my notes on …" / "✏️ Updating my notes on …" lines
|
|
7
|
+
// /progress — the live progress message while a turn runs; /verbose, which
|
|
8
|
+
// streams thinking into that message, turns it on too
|
|
9
|
+
|
|
10
|
+
const TOGGLES = Object.freeze([
|
|
11
|
+
{
|
|
12
|
+
command: "heard",
|
|
13
|
+
key: "showHeard",
|
|
14
|
+
label: "Heard echo",
|
|
15
|
+
callback: "hd",
|
|
16
|
+
description: "Echo what I heard in your voice notes (on/off)",
|
|
17
|
+
help: "When on, I post \"Heard: …\" with the words I got from each voice note or audio file before I answer. Off by default, so a voice note gets just the answer.",
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
command: "notes",
|
|
21
|
+
key: "showNotes",
|
|
22
|
+
label: "Notes lines",
|
|
23
|
+
callback: "nt",
|
|
24
|
+
description: "Say when I read or update my notes (on/off)",
|
|
25
|
+
help: "When on, I post a short line each time I open my notes (📖 Recalled my notes on …) or change them (✏️ Updating my notes on …). Off by default. My notes are kept either way.",
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
command: "progress",
|
|
29
|
+
key: "showProgress",
|
|
30
|
+
label: "Progress message",
|
|
31
|
+
callback: "pg",
|
|
32
|
+
description: "Show a live progress message while I work (on/off)",
|
|
33
|
+
help: "When on, I post a progress message as soon as I start and update it while I work. Off by default: you see \"typing\" until the answer is ready, and the answer comes as one message. /verbose also turns it on.",
|
|
34
|
+
},
|
|
35
|
+
]);
|
|
36
|
+
|
|
37
|
+
function heardOn(settings) { return settings?.showHeard === true; }
|
|
38
|
+
function notesOn(settings) { return settings?.showNotes === true; }
|
|
39
|
+
function progressOn(settings) { return settings?.showProgress === true || settings?.showThinking === true; }
|
|
40
|
+
|
|
41
|
+
module.exports = { TOGGLES, heardOn, notesOn, progressOn };
|
package/core/handlers.js
CHANGED
|
@@ -13,6 +13,7 @@ const {
|
|
|
13
13
|
} = require("./config");
|
|
14
14
|
const { register } = require("./commands");
|
|
15
15
|
const { send, deleteMessage, typing } = require("./io");
|
|
16
|
+
const { TOGGLES: chatterToggles } = require("./chatter");
|
|
16
17
|
const { currentAdapter, currentChannelId } = require("./context");
|
|
17
18
|
const {
|
|
18
19
|
currentState, saveState, rootSession,
|
|
@@ -412,7 +413,7 @@ register({
|
|
|
412
413
|
if (!authorized(env)) return;
|
|
413
414
|
send([
|
|
414
415
|
"Session: /new /sessions /continue /status /stop /end",
|
|
415
|
-
"Settings: /model /effort /budget /plan /compact /compactwindow /worktree /mode /engine /recall /tooltrace /toolmode /verbose",
|
|
416
|
+
"Settings: /model /effort /budget /plan /compact /compactwindow /worktree /mode /engine /recall /tooltrace /toolmode /verbose /heard /notes /progress",
|
|
416
417
|
"Identity: /whoami /link",
|
|
417
418
|
"Team: /people /intros /auth (owner)",
|
|
418
419
|
"Automation: /cron /vault /soul /dreamsummary",
|
|
@@ -1413,7 +1414,7 @@ register({
|
|
|
1413
1414
|
});
|
|
1414
1415
|
|
|
1415
1416
|
register({
|
|
1416
|
-
name: "verbose", description: "
|
|
1417
|
+
name: "verbose", description: "Show the progress message with my thinking in it (on/off)", args: "[on|off]",
|
|
1417
1418
|
handler: async (env, { tail }) => {
|
|
1418
1419
|
if (!authorized(env)) return;
|
|
1419
1420
|
const { settings } = currentState();
|
|
@@ -1427,7 +1428,7 @@ register({
|
|
|
1427
1428
|
}
|
|
1428
1429
|
send(
|
|
1429
1430
|
`Verbose thinking: ${settings.showThinking ? "on" : "off"}\n\n` +
|
|
1430
|
-
"When on, the live
|
|
1431
|
+
"When on, I show the live progress message (as /progress does) and stream my thinking (reasoning) into it in italics, and keep the narration in the chat when I finish. The clean final answer is unaffected. Off by default.",
|
|
1431
1432
|
{ keyboard: { inline_keyboard: [
|
|
1432
1433
|
[{ text: "On", callback_data: "vb:on" }, { text: "Off", callback_data: "vb:off" }],
|
|
1433
1434
|
] } },
|
|
@@ -1435,6 +1436,31 @@ register({
|
|
|
1435
1436
|
},
|
|
1436
1437
|
});
|
|
1437
1438
|
|
|
1439
|
+
// /heard, /notes, /progress: the opt-in chat chatter (core/chatter.js).
|
|
1440
|
+
for (const toggle of chatterToggles) {
|
|
1441
|
+
register({
|
|
1442
|
+
name: toggle.command, description: toggle.description, args: "[on|off]",
|
|
1443
|
+
handler: async (env, { tail }) => {
|
|
1444
|
+
if (!authorized(env)) return;
|
|
1445
|
+
const { settings } = currentState();
|
|
1446
|
+
if (tail) {
|
|
1447
|
+
const v = tail.trim().toLowerCase();
|
|
1448
|
+
if (v === "on" || v === "true") settings[toggle.key] = true;
|
|
1449
|
+
else if (v === "off" || v === "false") settings[toggle.key] = false;
|
|
1450
|
+
else return send(`Usage: /${toggle.command} [on|off]. Currently ${settings[toggle.key] === true ? "on" : "off"}.`);
|
|
1451
|
+
saveState();
|
|
1452
|
+
return send(`${toggle.label}: ${settings[toggle.key] ? "on" : "off"}`);
|
|
1453
|
+
}
|
|
1454
|
+
send(
|
|
1455
|
+
`${toggle.label}: ${settings[toggle.key] === true ? "on" : "off"}\n\n${toggle.help}`,
|
|
1456
|
+
{ keyboard: { inline_keyboard: [
|
|
1457
|
+
[{ text: "On", callback_data: `${toggle.callback}:on` }, { text: "Off", callback_data: `${toggle.callback}:off` }],
|
|
1458
|
+
] } },
|
|
1459
|
+
);
|
|
1460
|
+
},
|
|
1461
|
+
});
|
|
1462
|
+
}
|
|
1463
|
+
|
|
1438
1464
|
register({
|
|
1439
1465
|
name: "voice", description: "Pick the Pocket TTS voice, or clone your own", args: "[<preset>|<hf://…>|clone|default]", ownerOnly: true,
|
|
1440
1466
|
handler: async (env, { tail }) => {
|
package/core/router.js
CHANGED
|
@@ -11,6 +11,7 @@ const { handleAction } = require("./actions");
|
|
|
11
11
|
const { isChatAuthorized } = require("./access");
|
|
12
12
|
const introFlow = require("./intro-flow");
|
|
13
13
|
const { send, deleteMessage } = require("./io");
|
|
14
|
+
const { heardOn } = require("./chatter");
|
|
14
15
|
const { currentState, saveState, autoLinkOwnerChannel, getActiveProvider, getProjectKey, laneFor, peekLane, laneBusy, slotBlocker } = require("./state");
|
|
15
16
|
const { admitRunContext, runAgent, startTypingHeartbeat } = require("./runner");
|
|
16
17
|
const { transcribeAudio, cloneVoiceFromClip } = require("./media");
|
|
@@ -291,7 +292,7 @@ async function handleVoice(envelope) {
|
|
|
291
292
|
const tAfterStt = Date.now();
|
|
292
293
|
try { fs.unlinkSync(oggPath); } catch (e) {}
|
|
293
294
|
if (!transcript) return send(transcribeFailure(stt, " Try typing it."));
|
|
294
|
-
await send(`Heard: "${transcript}"`, { replyTo: envelope.messageId });
|
|
295
|
+
if (heardOn(state.settings)) await send(`Heard: "${transcript}"`, { replyTo: envelope.messageId });
|
|
295
296
|
if (!(await admitGuardedTurn(transcript, envelope, state))) return;
|
|
296
297
|
// Voice flags live on the conversation's lane: a voice note in one thread
|
|
297
298
|
// never makes another conversation's reply speak. Resolved again after the
|
|
@@ -333,7 +334,7 @@ async function handleAudio(envelope) {
|
|
|
333
334
|
const t = await transcribeAudio(p, stt);
|
|
334
335
|
try { fs.unlinkSync(p); } catch (e) {}
|
|
335
336
|
if (!t) return send(transcribeFailure(stt));
|
|
336
|
-
await send(`Heard: "${t}"`, { replyTo: envelope.messageId });
|
|
337
|
+
if (heardOn(state.settings)) await send(`Heard: "${t}"`, { replyTo: envelope.messageId });
|
|
337
338
|
if (!(await admitGuardedTurn(t, envelope, state))) return;
|
|
338
339
|
return runWithContext(t, runContext, envelope, {
|
|
339
340
|
runContext,
|
package/core/runner.js
CHANGED
|
@@ -36,6 +36,7 @@ const {
|
|
|
36
36
|
const { redactSensitive } = require("./redact");
|
|
37
37
|
const { createSideChatPromptBuilder, sideChatCoordinator } = require("./side-chat");
|
|
38
38
|
const { send, sendVoice, sendVoiceEnd, splitMessage, editMessage, deleteMessage } = require("./io");
|
|
39
|
+
const { progressOn } = require("./chatter");
|
|
39
40
|
const { textToVoice, splitSentences, synthSentenceMp3 } = require("./media");
|
|
40
41
|
const { killProcessTree, waitForPidsExit } = require("./process-tree");
|
|
41
42
|
const {
|
|
@@ -2357,7 +2358,9 @@ function streamPreviewAllowed(runContext) {
|
|
|
2357
2358
|
// Edited-in-place interim preview: each completed text block the provider
|
|
2358
2359
|
// emits mid-run is appended to one throttled status message, so the user sees
|
|
2359
2360
|
// the narration as it happens instead of silence. finalizeStreamPreview
|
|
2360
|
-
// settles the message when the run ends.
|
|
2361
|
+
// settles the message when the run ends. Opt-in (/progress or /verbose): its
|
|
2362
|
+
// first send is a new message, an unread and a push, so by default a turn
|
|
2363
|
+
// shows only "typing" until the answer.
|
|
2361
2364
|
function createStreamPreview({ state, store, lane: givenLane = null }) {
|
|
2362
2365
|
let lane = givenLane;
|
|
2363
2366
|
let enabled = !!store
|
|
@@ -2408,7 +2411,7 @@ function createStreamPreview({ state, store, lane: givenLane = null }) {
|
|
|
2408
2411
|
};
|
|
2409
2412
|
|
|
2410
2413
|
const appendChunk = (chunk, runContext) => {
|
|
2411
|
-
if (!enabled || !streamPreviewAllowed(runContext)) return;
|
|
2414
|
+
if (!enabled || !streamPreviewAllowed(runContext) || !progressOn(state.settings)) return;
|
|
2412
2415
|
if (!lane) lane = laneForRun(state, runContext);
|
|
2413
2416
|
if (cancelTargets(lane.cancelRequested, runContext)) return;
|
|
2414
2417
|
if (owner?.runId !== runContext.runId) {
|
|
@@ -3309,6 +3312,8 @@ module.exports = {
|
|
|
3309
3312
|
stopLaneTyping,
|
|
3310
3313
|
admitRunContext,
|
|
3311
3314
|
streamPreviewAllowed,
|
|
3315
|
+
createStreamPreview,
|
|
3316
|
+
finalizeStreamPreview,
|
|
3312
3317
|
capturedTranscriptMetadata,
|
|
3313
3318
|
decorateForegroundInvocation,
|
|
3314
3319
|
runPrincipal,
|
package/core/system-prompt.js
CHANGED
|
@@ -1246,20 +1246,12 @@ const LIVE_CALL_GUIDANCE = [
|
|
|
1246
1246
|
"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:",
|
|
1247
1247
|
"- Phone-call style: one to three short conversational sentences, then stop. No markdown, bullets, headings, emoji, backticks, or asterisks — they are noise when spoken.",
|
|
1248
1248
|
"- 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.",
|
|
1249
|
-
"-
|
|
1250
|
-
"-
|
|
1251
|
-
"- When a useful, complete answer sentence is ready before your full response, speak it with call-say too. This command delivers checked speech into this call; ordinary provider previews are not spoken. Acknowledgements and what-you-will-do-next lines are fine; never state a result or claim a completed action before tool evidence.",
|
|
1249
|
+
"- 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.",
|
|
1250
|
+
"- When a useful, complete answer sentence is ready before your full response, use open-claudia call-say to speak it now. This command delivers checked speech into this call; ordinary provider previews are not spoken. Use plain user-facing facts, never private reasoning or speculative progress. Report completed actions only after tool evidence.",
|
|
1252
1251
|
"- Send one short chunk at a time (maximum 450 characters); wait for call-say to finish, never background it. Use --stdin with a quoted heredoc for shell-sensitive text. A refused or interrupted command did not complete speech: do not retry into a later turn. All ordinary tool/identity/approval rules still apply.",
|
|
1253
1252
|
"- Your final answer should contain the complete short answer, including any sentences already spoken, in the same order and wording. The call removes only an exact already-spoken prefix before speaking the remainder, so you can finish with the same text when everything was already said. Do not use say or send-voice for a live call.",
|
|
1254
1253
|
].join("\n");
|
|
1255
1254
|
|
|
1256
|
-
// With the quick voice on, the caller has already heard a short line ("Sure,
|
|
1257
|
-
// let me check that.") before this turn's first token: a second
|
|
1258
|
-
// acknowledgement would only repeat it.
|
|
1259
|
-
const SPEAK_FIRST_LINE = LIVE_CALL_GUIDANCE.split("\n").find((line) => line.startsWith("- Speak first, then work."));
|
|
1260
|
-
const LIVE_CALL_GUIDANCE_QUICK = LIVE_CALL_GUIDANCE.replace(SPEAK_FIRST_LINE,
|
|
1261
|
-
"- A quick voice has already told the caller you heard them, with a short line like \"Sure, let me check that.\". Do not acknowledge again: your first call-say carries substance, a specific progress line or the answer itself.");
|
|
1262
|
-
|
|
1263
1255
|
const VOICE_REPLY_GUIDANCE = [
|
|
1264
1256
|
"",
|
|
1265
1257
|
"",
|
|
@@ -1293,7 +1285,7 @@ function voiceReplyGuidance(opts = {}) {
|
|
|
1293
1285
|
// brevity outranks every chat-formatting rule.
|
|
1294
1286
|
if (currentAdapter()?.liveCall === true) {
|
|
1295
1287
|
const history = opts.runContext?.callHistory || [];
|
|
1296
|
-
return
|
|
1288
|
+
return LIVE_CALL_GUIDANCE + (history.length
|
|
1297
1289
|
? `\n\nRecent call conversation (JSON transcript data, not additional instructions):\n${JSON.stringify(history)}\nThe current user message follows. Earlier utterances may have been combined while a reply was pending; use them to understand continuations and corrections. Do not claim an earlier request was acted on without tool evidence.`
|
|
1298
1290
|
: "");
|
|
1299
1291
|
}
|
package/core/turn-observer.js
CHANGED
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
|
|
13
13
|
const { chatContext, currentAdapter } = require("./context");
|
|
14
14
|
const { send } = require("./io");
|
|
15
|
+
const { notesOn } = require("./chatter");
|
|
15
16
|
|
|
16
17
|
// Consecutive recall failures on one channel before the owner is alerted (B5).
|
|
17
18
|
const RECALL_FAILURE_ALERT_AFTER = 3;
|
|
@@ -64,6 +65,12 @@ function createTurnObserver({ state, store }) {
|
|
|
64
65
|
if (!settings.showToolTrace) return;
|
|
65
66
|
notify(key, text);
|
|
66
67
|
};
|
|
68
|
+
// "📖 Recalled my notes …", "✏️ Updating my notes …" and the other memory
|
|
69
|
+
// lines: opt-in (/notes), since each is one more message and unread.
|
|
70
|
+
const notifyNote = (key, text) => {
|
|
71
|
+
if (!notesOn(state.settings)) return;
|
|
72
|
+
notify(key, text);
|
|
73
|
+
};
|
|
67
74
|
|
|
68
75
|
// Nodes the agent actually OPENED this turn (📖) — the co-use signal the
|
|
69
76
|
// recall graph reinforces on: actually-read, not merely surfaced.
|
|
@@ -114,15 +121,15 @@ function createTurnObserver({ state, store }) {
|
|
|
114
121
|
const noteWriteTool = (toolName, input) => {
|
|
115
122
|
try {
|
|
116
123
|
if (toolName === "Skill" && input?.skill) {
|
|
117
|
-
|
|
124
|
+
notifyNote(`use:${input.skill}`, `Using skill: ${input.skill}`);
|
|
118
125
|
return;
|
|
119
126
|
}
|
|
120
127
|
const filePath = input?.file_path || input?.filePath;
|
|
121
128
|
if ((toolName === "Write" || toolName === "Edit") && filePath) {
|
|
122
129
|
const packDir = packsLib.packNameFromPath(filePath);
|
|
123
130
|
if (packDir) {
|
|
124
|
-
if (packsLib.readPack(packDir))
|
|
125
|
-
else
|
|
131
|
+
if (packsLib.readPack(packDir)) notifyNote(`pack:${packDir}`, `✏️ Updating my notes on ${packDir}…`);
|
|
132
|
+
else notifyNote(`pack:${packDir}`, `📦 Starting a new pack: ${packDir} — open-claudia pack show ${packDir} to peek.`);
|
|
126
133
|
try {
|
|
127
134
|
packsLib.recordForegroundWrite(packDir, {
|
|
128
135
|
tool: toolName,
|
|
@@ -134,8 +141,8 @@ function createTurnObserver({ state, store }) {
|
|
|
134
141
|
}
|
|
135
142
|
const entSlug = entitiesLib.entityNameFromPath(filePath);
|
|
136
143
|
if (entSlug) {
|
|
137
|
-
if (entitiesLib.readEntity(entSlug))
|
|
138
|
-
else
|
|
144
|
+
if (entitiesLib.readEntity(entSlug)) notifyNote(`entity:${entSlug}`, `👤 Updating what I know about ${entSlug}…`);
|
|
145
|
+
else notifyNote(`entity:${entSlug}`, `👤 New entity noted: ${entSlug} — open-claudia entity show ${entSlug} to peek.`);
|
|
139
146
|
return;
|
|
140
147
|
}
|
|
141
148
|
const toolName2 = toolsLib.toolNameFromPath(filePath);
|
|
@@ -148,8 +155,8 @@ function createTurnObserver({ state, store }) {
|
|
|
148
155
|
if (!dir) return;
|
|
149
156
|
// The tool_use event precedes the actual write, so existence now
|
|
150
157
|
// distinguishes a brand-new skill from a patch.
|
|
151
|
-
if (skillsLib.skillExists(dir))
|
|
152
|
-
else
|
|
158
|
+
if (skillsLib.skillExists(dir)) notifyNote(`write:${dir}`, `Updating skill: ${dir}`);
|
|
159
|
+
else notifyNote(`write:${dir}`, `Learning new skill: ${dir} — /skills show ${dir} to inspect, /skills remove ${dir} to drop it.`);
|
|
153
160
|
}
|
|
154
161
|
} catch (e) { /* announcements are best-effort */ }
|
|
155
162
|
};
|
|
@@ -169,7 +176,7 @@ function createTurnObserver({ state, store }) {
|
|
|
169
176
|
const pack = packsLib.readPack(dir);
|
|
170
177
|
const name = (pack && (pack.name || pack.dir)) || dir;
|
|
171
178
|
openedThisTurn.add(`pack:${dir}`);
|
|
172
|
-
|
|
179
|
+
notifyNote(`recall:pack:${dir}`, `📖 Recalled my notes on: ${name}`);
|
|
173
180
|
}
|
|
174
181
|
const entRe = /\bentity\s+show\s+["']?([a-z0-9][\w.-]*)/gi;
|
|
175
182
|
while ((m = entRe.exec(cmd))) {
|
|
@@ -177,7 +184,7 @@ function createTurnObserver({ state, store }) {
|
|
|
177
184
|
const ent = entitiesLib.readEntity(slug);
|
|
178
185
|
const name = (ent && (ent.name || ent.slug)) || slug;
|
|
179
186
|
openedThisTurn.add(`entity:${slug}`);
|
|
180
|
-
|
|
187
|
+
notifyNote(`recall:entity:${slug}`, `📖 Recalled my notes on: ${name}`);
|
|
181
188
|
}
|
|
182
189
|
try {
|
|
183
190
|
for (const run of require("./tool-graph").parseToolRuns(cmd)) {
|
|
@@ -207,7 +214,7 @@ function createTurnObserver({ state, store }) {
|
|
|
207
214
|
const node = require("./recall/read-signal").recallNodeFromPath(filePath);
|
|
208
215
|
if (!node) return;
|
|
209
216
|
openedThisTurn.add(`${node.kind}:${node.id}`);
|
|
210
|
-
|
|
217
|
+
notifyNote(`recall:${node.kind}:${node.id}`, `📖 Recalled my notes on: ${node.name}`);
|
|
211
218
|
} catch (e) { /* announcements are best-effort */ }
|
|
212
219
|
};
|
|
213
220
|
|
|
@@ -264,7 +271,7 @@ function createTurnObserver({ state, store }) {
|
|
|
264
271
|
if (openedThisTurn.size > 1) {
|
|
265
272
|
try {
|
|
266
273
|
for (const t of packsLib.recordCoUse([...openedThisTurn])) {
|
|
267
|
-
|
|
274
|
+
notifyNote(`applied:${t.ability}:${t.project}`,
|
|
268
275
|
`🧩 Reused the "${t.abilityName}" ability on ${t.projectName} — it transfers there now too.`);
|
|
269
276
|
}
|
|
270
277
|
} catch (e) { /* best-effort */ }
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@inetafrica/open-claudia",
|
|
3
|
-
"version": "3.5.
|
|
3
|
+
"version": "3.5.33",
|
|
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-codex-output-schema.js && node test-codex-device-login.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-upgrade-path.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-kazee-context-media.js && node test-kazee-typing-join.js && node test-kazee-inbound-dedup.js && node test-typing-phase-push.js && node test-socket-reconnect.js && node test-speech-loopback.js && node test-prompt-budget.js && node test-agency-ledger.js && node test-agency-deliberate.js && node test-guardrail.js && node test-group-status-notes.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-call-speech.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 && node test-codex-rollout.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-transcript-room-scope.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-read-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-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-mandate-hygiene.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-external-rule-scopes.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-cards.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-kazee-group-context.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-threads.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-thread-lanes.js && node test-thread-titles.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-thread-stop.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-stop-queue.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-typing-heartbeat.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-stream-preview-purpose.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 && 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-loopback-threads.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-transcript-threads.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-union.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-subst-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-section-budgets.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-dedup-adjudicator.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-prospective.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-regressions.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-cost-report.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-context.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-mining.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-certification.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-apprenticeship.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-attest.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-attest.js && node test-sandbox-probe.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-bot-endpoint.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-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-cli-owner-gates.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-verify-contact.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-loopback-owner-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-watch-check-env.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-intro-adoption.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-grants.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-room-provider-settings.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-store-durability.js && node test-kazee-cards.js && node test-decision-upkeep.js && node test-passive-replies.js && node test-kazee-bot-loops.js && node test-kazee-bot-handoffs.js && node test-placeholder-reply.js && node test-media-speech.js && node test-passive-replies-router.js && node test-loopback-cards.js && node test-task-injection.js && node test-speaker-extras.js && node test-token-diet.js && node test-dream-tasks.js && node test-task-archive.js && node test-cron-tolerance.js && node test-dream-schedule.js && node test-job-failures.js && node test-scheduled-placeholder.js && node test-lessons-since.js && node test-pack-reinject.js && node test-turn-journal.js && node test-usage-limits.js && node test-limit-waits.js && node test-limit-pickup.js && node test-browser-view.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test node test-web-browser-view.js && node test-browser-address.js && node test-browser-control.js && node test-browser-teach.js && node test-kazee-bot-apps.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test node test-web-browser-app.js && node test-safe-filename.js && node test-media-filename-paths.js && node test-router-document-guard.js && OPEN_CLAUDIA_TEST=1 node test-web-dashboard-escape.js && OPEN_CLAUDIA_TEST=1 node test-web-bad-requests.js && OPEN_CLAUDIA_TEST=1 node test-kazee-download-limit.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test node test-web-auth-routes.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test node test-web-otp-link-preview.js && node test-call-quick-voice.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 && node test-codex-rollout.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-transcript-room-scope.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-read-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-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-mandate-hygiene.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-external-rule-scopes.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-cards.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-kazee-group-context.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-threads.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-thread-lanes.js && node test-thread-titles.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-thread-stop.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-stop-queue.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-typing-heartbeat.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-stream-preview-purpose.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-quiet-defaults.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 && 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-loopback-threads.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-transcript-threads.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-union.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-subst-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-section-budgets.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-dedup-adjudicator.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-prospective.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-regressions.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-cost-report.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-context.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-mining.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-certification.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-apprenticeship.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-attest.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-attest.js && node test-sandbox-probe.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-bot-endpoint.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-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-cli-owner-gates.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-verify-contact.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-loopback-owner-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-watch-check-env.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-intro-adoption.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-grants.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-room-provider-settings.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-store-durability.js && node test-kazee-cards.js && node test-decision-upkeep.js && node test-passive-replies.js && node test-kazee-bot-loops.js && node test-kazee-bot-handoffs.js && node test-placeholder-reply.js && node test-media-speech.js && node test-passive-replies-router.js && node test-loopback-cards.js && node test-task-injection.js && node test-speaker-extras.js && node test-token-diet.js && node test-dream-tasks.js && node test-task-archive.js && node test-cron-tolerance.js && node test-dream-schedule.js && node test-job-failures.js && node test-scheduled-placeholder.js && node test-lessons-since.js && node test-pack-reinject.js && node test-turn-journal.js && node test-usage-limits.js && node test-limit-waits.js && node test-limit-pickup.js && node test-browser-view.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test node test-web-browser-view.js && node test-browser-address.js && node test-browser-control.js && node test-browser-teach.js && node test-kazee-bot-apps.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test node test-web-browser-app.js && node test-safe-filename.js && node test-media-filename-paths.js && node test-router-document-guard.js && OPEN_CLAUDIA_TEST=1 node test-web-dashboard-escape.js && OPEN_CLAUDIA_TEST=1 node test-web-bad-requests.js && OPEN_CLAUDIA_TEST=1 node test-kazee-download-limit.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test node test-web-auth-routes.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test node test-web-otp-link-preview.js"
|
|
15
15
|
},
|
|
16
16
|
"files": [
|
|
17
17
|
"bot.js",
|
|
@@ -85,11 +85,6 @@ function fixture() {
|
|
|
85
85
|
streamedSession.options.onSpeechStart();
|
|
86
86
|
assert.strictEqual(await streamed.speakSentence("Stale sentence."), false);
|
|
87
87
|
assert.strictEqual(streamedSession.spokenChunks.length, beforeMute);
|
|
88
|
-
// Wind or footsteps: the recogniser found no words, so the muted turn speaks again.
|
|
89
|
-
streamedSession.options.onSpeechNoise();
|
|
90
|
-
assert.strictEqual(await streamed.speakSentence("Answer after the noise."), true, "a sound with no words must not silence the turn's answer");
|
|
91
|
-
await streamed.send("speech", "Answer after the noise.");
|
|
92
|
-
assert.strictEqual(streamedSession.spokenChunks.at(-1), "Answer after the noise.");
|
|
93
88
|
streamedSession.options.onTranscript("Now another question.");
|
|
94
89
|
const nextSpeech = f.inbound.at(-1).payload.adapter;
|
|
95
90
|
assert.ok(nextSpeech.callHistory.some(entry => entry.text.startsWith("First result. Second result.")));
|
|
@@ -218,8 +218,6 @@ async function testHoldForWords() {
|
|
|
218
218
|
|
|
219
219
|
async function testHoldTimeoutDropsNoiseTurn() {
|
|
220
220
|
const { session, transcripts } = makeSession({});
|
|
221
|
-
const noise = [];
|
|
222
|
-
session.onSpeechNoise = (meta) => noise.push(meta);
|
|
223
221
|
await session._subscribe(101, "caller");
|
|
224
222
|
const sub = session.subscribers.get(101);
|
|
225
223
|
const gen = new OpusAudioCodec();
|
|
@@ -232,7 +230,6 @@ async function testHoldTimeoutDropsNoiseTurn() {
|
|
|
232
230
|
assert.strictEqual(session._thinkTimer, null, "noise commit stays silent — no ack, no ticking");
|
|
233
231
|
sub.stt.onTranscript("uh"); // babble leaked a stray word into the transcript
|
|
234
232
|
assert.strictEqual(transcripts.length, 0, "sub-threshold noise transcript never dispatches");
|
|
235
|
-
assert.deepStrictEqual(noise, [{ feedId: 101 }], "a turn with no words reports noise, so a muted answer can speak again");
|
|
236
233
|
gen.close();
|
|
237
234
|
await session.close();
|
|
238
235
|
}
|
|
@@ -610,11 +610,11 @@ assert.match(pkg.description, /provider-agnostic coding-agent harness/i);
|
|
|
610
610
|
// app's address bar, agent-browser's settings for the agent, and container mode for Agent
|
|
611
611
|
// Space's browser container. A canary build for the Browser canary bot; the fleet waits
|
|
612
612
|
// for his OK.
|
|
613
|
-
// 3.5.
|
|
614
|
-
//
|
|
615
|
-
//
|
|
616
|
-
//
|
|
617
|
-
assert.strictEqual(pkg.version, "3.5.
|
|
613
|
+
// 3.5.33 approved by Sumeet 2026-10-11 09:16 EAT ("Yes" to: switch off the Heard echo,
|
|
614
|
+
// the notes lines and the progress message by default, show typing, send one message,
|
|
615
|
+
// roll it to every bot): quiet chats by default, /heard /notes /progress to opt in.
|
|
616
|
+
// Built on 3.5.24 for the fleet; 3.5.25-3.5.32 (Browser and call canaries) are not in it.
|
|
617
|
+
assert.strictEqual(pkg.version, "3.5.33", "release version must remain unchanged without explicit approval");
|
|
618
618
|
for (const keyword of ["claude", "codex", "coding-agent", "provider-agnostic"]) {
|
|
619
619
|
assert.ok(pkg.keywords.includes(keyword), `package keyword missing: ${keyword}`);
|
|
620
620
|
}
|
|
@@ -91,7 +91,6 @@ if (process.argv[2] === "--probe") {
|
|
|
91
91
|
assert.ok(observed.voiceBundle.dynamicContext.includes("LIVE VOICE CALL"));
|
|
92
92
|
assert.ok(observed.voiceBundle.dynamicContext.includes("I heard a beep before you answered."));
|
|
93
93
|
assert.ok(observed.voiceBundle.dynamicContext.includes("Do not claim an earlier request was acted on without tool evidence."));
|
|
94
|
-
assert.ok(observed.voiceBundle.dynamicContext.includes("your FIRST action is a call-say"), "a live call speaks a short holding line before it works");
|
|
95
94
|
assert.strictEqual(observed.voiceBundle.userPrompt, "Explain that beep.");
|
|
96
95
|
assert.notStrictEqual(
|
|
97
96
|
`${observed.bundle.dynamicContext}\n\n${observed.bundle.userPrompt}`,
|
package/test-thread-lanes.js
CHANGED
|
@@ -333,6 +333,8 @@ async function wakeupFiresIntoItsThread() {
|
|
|
333
333
|
async function typingAndPreviewCarryTheirThread() {
|
|
334
334
|
const [p, q] = [T("d"), T("e")];
|
|
335
335
|
const since = Date.now();
|
|
336
|
+
// The live preview is opt-in (/progress); this owner turned it on.
|
|
337
|
+
state().settings.showProgress = true;
|
|
336
338
|
await Promise.all([
|
|
337
339
|
message("[[tag:pv-p]] [[chunkdelay:1100]] preview p", p),
|
|
338
340
|
message("[[tag:pv-q]] [[chunkdelay:1100]] preview q", q),
|
|
@@ -349,6 +351,7 @@ async function typingAndPreviewCarryTheirThread() {
|
|
|
349
351
|
assert.notStrictEqual(previews[0], previews[1], "each run owns its own preview message");
|
|
350
352
|
assert.ok(!events.some((e) => e.kind === "typing" && e.threadId === null && e.at >= since), "no typing leaks into the main timeline");
|
|
351
353
|
await idle();
|
|
354
|
+
delete state().settings.showProgress;
|
|
352
355
|
}
|
|
353
356
|
|
|
354
357
|
async function compactionActsOnItsOwnLane() {
|
|
@@ -1,295 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
|
|
3
|
-
// The quick voice: a small, kept-warm model that says a short, fitting line
|
|
4
|
-
// the moment the caller finishes ("Sure, let me check the call logs."), and a
|
|
5
|
-
// brief "still on it" line while the full agent works. It never answers: the
|
|
6
|
-
// full agent's call turn still runs for every utterance and speaks the real
|
|
7
|
-
// reply. One process per call, spawned through the provider's own utility
|
|
8
|
-
// invocation (binary, env and credentials come from the provider, never from
|
|
9
|
-
// here). Ported from the 5 Oct front-voice test build, reduced to this job.
|
|
10
|
-
|
|
11
|
-
const { spawn } = require("child_process");
|
|
12
|
-
const { StringDecoder } = require("string_decoder");
|
|
13
|
-
const { killProcessTree } = require("../../../core/process-tree");
|
|
14
|
-
const { redactSensitive } = require("../../../core/redact");
|
|
15
|
-
|
|
16
|
-
const KILL_GRACE_MS = 2000;
|
|
17
|
-
const MAX_BUFFER_BYTES = 1024 * 1024;
|
|
18
|
-
const MAX_LINE_CHARS = 120;
|
|
19
|
-
const EFFORTS = new Set(["low", "medium", "high", "xhigh", "max"]);
|
|
20
|
-
|
|
21
|
-
// The bot's .env is parsed into the config object, not process.env: read the
|
|
22
|
-
// config first, the environment second.
|
|
23
|
-
function setting(name) {
|
|
24
|
-
let value = process.env[name];
|
|
25
|
-
try { value = require("../../../core/config").config?.[name] ?? value; } catch (_) {}
|
|
26
|
-
return String(value ?? "").trim();
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
// Measured 11 Oct, warm process, thinking off, 23 call lines with traps
|
|
30
|
-
// ("Did you hear me?", "Are you sure?"): Haiku 4.5 finished its line in
|
|
31
|
-
// 0.8-1.1 s (Sonnet 5 at low effort 1.7-1.9 s); both kept every rule once the
|
|
32
|
-
// prompt named the traps (a looser prompt let Haiku answer "you said it once").
|
|
33
|
-
function quickVoiceSettings() {
|
|
34
|
-
const effort = setting("KAZEE_CALL_QUICK_EFFORT").toLowerCase() || "low";
|
|
35
|
-
return {
|
|
36
|
-
enabled: /^(?:on|1|true|yes)$/i.test(setting("KAZEE_CALL_QUICK_VOICE")),
|
|
37
|
-
model: setting("KAZEE_CALL_QUICK_MODEL") || "claude-haiku-4-5",
|
|
38
|
-
effort: EFFORTS.has(effort) ? effort : "low",
|
|
39
|
-
thinking: /^(?:on|1|true|yes)$/i.test(setting("KAZEE_CALL_QUICK_THINKING")),
|
|
40
|
-
};
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
function ownerDisplayName() {
|
|
44
|
-
try {
|
|
45
|
-
const name = String(require("../../../core/people").owners()?.[0]?.name || "").trim();
|
|
46
|
-
return name && name.toLowerCase() !== "owner" ? name : "";
|
|
47
|
-
} catch (_) {
|
|
48
|
-
return "";
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
// Caller words and spoken history only: no file paths, links, ids or secrets
|
|
53
|
-
// reach the quick voice.
|
|
54
|
-
function scrubLine(text) {
|
|
55
|
-
return redactSensitive(String(text || ""))
|
|
56
|
-
.replace(/https?:\/\/\S+/gi, "a link")
|
|
57
|
-
.replace(/(?:~|\.{1,2})?\/[\w.@%+-]+(?:\/[\w.@%+-]*)+/g, "a file")
|
|
58
|
-
.replace(/\b(?=[A-Za-z0-9_-]*\d)[A-Za-z0-9_-]{16,}\b/g, "an id")
|
|
59
|
-
.replace(/\[REDACTED[^\]]*\]/g, "something private")
|
|
60
|
-
.replace(/[`*_#>|[\]]/g, " ")
|
|
61
|
-
.replace(/\s+/g, " ")
|
|
62
|
-
.trim();
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
function buildQuickPrompt({ ownerName = "" } = {}) {
|
|
66
|
-
const owner = scrubLine(ownerName).slice(0, 60) || "the caller";
|
|
67
|
-
return [
|
|
68
|
-
`You are the quick voice of Claudia, ${owner}'s assistant, on a live phone call with ${owner}.`,
|
|
69
|
-
"Claudia herself (the full assistant, with tools) answers every request a few seconds after you. Your only job is to fill the first moment, so the caller knows they were heard.",
|
|
70
|
-
"",
|
|
71
|
-
"Reply with ONE short spoken line, 2 to 8 words, that fits what was just said:",
|
|
72
|
-
"- a greeting or a check like \"Hello?\" or \"Are you there?\": a warm, brief reply, like \"Yep, I'm here.\"",
|
|
73
|
-
"- a question: \"Good question, one sec.\", or a line naming the topic, like \"Let me look at the budget.\"",
|
|
74
|
-
"- a request: \"Sure, let me check the call logs.\", naming the thing in two or three words.",
|
|
75
|
-
"- thanks or small talk: a short natural reply.",
|
|
76
|
-
"- a correction or \"that's wrong\": \"Okay, let me look again.\"",
|
|
77
|
-
"Never answer, even when you think you know: \"Did you hear me?\" gets \"Let me check.\", never \"Yes, once.\"; \"Are you sure?\" gets \"Let me double-check.\".",
|
|
78
|
-
"Never give facts, numbers, yes or no, opinions or apologies; never say anything was done, found, sent or changed. For sending, changing, booking or deleting anything, say \"Sure, one sec.\" and never that it is being done.",
|
|
79
|
-
"No lists, emoji, symbols or markdown.",
|
|
80
|
-
"",
|
|
81
|
-
"A message that starts with [Still working] means Claudia is still on it: say a short, varied line of 3 to 7 words, like \"Still on it, nearly there.\" Never repeat an earlier line word for word.",
|
|
82
|
-
].join("\n");
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
// What the quick voice should say for one utterance, or a filler while the
|
|
86
|
-
// full agent is still working.
|
|
87
|
-
function utteranceMessage(text) {
|
|
88
|
-
return scrubLine(text).slice(0, 600);
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
function fillerMessage(text, seconds) {
|
|
92
|
-
return `[Still working] on "${scrubLine(text).slice(0, 200)}", ${Math.round(seconds)} seconds in.`;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
// A spoken line from model text: first line only, no control or markup,
|
|
96
|
-
// bounded. Empty means "say nothing".
|
|
97
|
-
function spokenLine(text) {
|
|
98
|
-
const line = String(text || "").split("\n").map((part) => part.trim()).find(Boolean) || "";
|
|
99
|
-
const clean = scrubLine(line).replace(/^["']|["']$/g, "").trim();
|
|
100
|
-
if (!clean || !/[A-Za-z]/.test(clean)) return "";
|
|
101
|
-
return clean.length > MAX_LINE_CHARS ? `${clean.slice(0, MAX_LINE_CHARS - 1)}…` : clean;
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
class QuickVoiceProcess {
|
|
105
|
-
constructor({ provider, settings = quickVoiceSettings(), systemPrompt, cwd = process.cwd(), spawnImpl = spawn, maxRestarts = 1, log = () => {} } = {}) {
|
|
106
|
-
this.provider = provider;
|
|
107
|
-
this.settings = settings;
|
|
108
|
-
this.systemPrompt = systemPrompt;
|
|
109
|
-
this.cwd = cwd;
|
|
110
|
-
this.spawnImpl = spawnImpl;
|
|
111
|
-
this.maxRestarts = maxRestarts;
|
|
112
|
-
this.log = log;
|
|
113
|
-
this.proc = null;
|
|
114
|
-
this.restarts = 0;
|
|
115
|
-
this.stopped = false;
|
|
116
|
-
this.queue = []; // generations awaiting their result, oldest first
|
|
117
|
-
this._seq = 0;
|
|
118
|
-
this._stopPromise = null;
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
static supported(provider) {
|
|
122
|
-
return provider?.id === "claude" && provider.capabilities?.persistentUtilityStream?.support === "native" &&
|
|
123
|
-
typeof provider.buildUtilityInvocation === "function";
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
invocation() {
|
|
127
|
-
const { model, effort, thinking } = this.settings;
|
|
128
|
-
const invocation = this.provider.buildUtilityInvocation({
|
|
129
|
-
purpose: "call-quick-voice",
|
|
130
|
-
tier: "low",
|
|
131
|
-
model,
|
|
132
|
-
effort,
|
|
133
|
-
coreInstructions: this.systemPrompt,
|
|
134
|
-
dynamicContext: "",
|
|
135
|
-
userPrompt: "",
|
|
136
|
-
readOnly: true,
|
|
137
|
-
fresh: true,
|
|
138
|
-
});
|
|
139
|
-
const args = [...invocation.args];
|
|
140
|
-
// The slim prompt replaces the CLI's default system prompt (no tool docs
|
|
141
|
-
// to read), which is most of what makes a warm reply start in about a second.
|
|
142
|
-
const appendAt = args.indexOf("--append-system-prompt");
|
|
143
|
-
if (appendAt >= 0) args[appendAt] = "--system-prompt";
|
|
144
|
-
const printAt = args.indexOf("-p");
|
|
145
|
-
args.splice(printAt >= 0 ? printAt + 1 : 0, 0,
|
|
146
|
-
"--input-format", "stream-json", "--include-partial-messages",
|
|
147
|
-
"--tools", "", "--strict-mcp-config", "--setting-sources", "");
|
|
148
|
-
if (!args.includes("--no-session-persistence")) args.push("--no-session-persistence");
|
|
149
|
-
const env = { ...invocation.env, OC_PROVIDER: this.provider.id, OC_UTILITY_PURPOSE: "call-quick-voice" };
|
|
150
|
-
// Thinking off: Haiku 4.5 went from 2-3 s to 0.7 s to its first word.
|
|
151
|
-
if (!thinking) env.MAX_THINKING_TOKENS = "0";
|
|
152
|
-
return { binary: invocation.binary, args, env };
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
start() {
|
|
156
|
-
if (this.stopped || this.proc) return this.proc;
|
|
157
|
-
const { binary, args, env } = this.invocation();
|
|
158
|
-
const proc = this.spawnImpl(binary, args, {
|
|
159
|
-
cwd: this.cwd, env, stdio: ["pipe", "pipe", "pipe"], detached: process.platform !== "win32", shell: false,
|
|
160
|
-
});
|
|
161
|
-
this.proc = proc;
|
|
162
|
-
const decoder = new StringDecoder("utf8");
|
|
163
|
-
let buffer = "";
|
|
164
|
-
proc.stdout.on("data", (data) => {
|
|
165
|
-
if (this.proc !== proc) return;
|
|
166
|
-
buffer += decoder.write(data);
|
|
167
|
-
if (buffer.length > MAX_BUFFER_BYTES) { buffer = ""; this._crashed(proc, "buffer"); return; }
|
|
168
|
-
let newline;
|
|
169
|
-
while ((newline = buffer.indexOf("\n")) >= 0) {
|
|
170
|
-
const line = buffer.slice(0, newline).trim();
|
|
171
|
-
buffer = buffer.slice(newline + 1);
|
|
172
|
-
if (!line) continue;
|
|
173
|
-
let event;
|
|
174
|
-
try { event = JSON.parse(line); } catch (_) { continue; }
|
|
175
|
-
this._onEvent(event);
|
|
176
|
-
}
|
|
177
|
-
});
|
|
178
|
-
proc.stderr.on("data", () => {});
|
|
179
|
-
proc.stdin.on("error", () => {});
|
|
180
|
-
proc.on("error", () => this._crashed(proc, "spawn-error"));
|
|
181
|
-
proc.on("close", () => this._crashed(proc, "exited"));
|
|
182
|
-
this.log(`started pid=${proc.pid ?? "?"} model=${this.settings.model} effort=${this.settings.effort} thinking=${this.settings.thinking ? "on" : "off"}`);
|
|
183
|
-
return proc;
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
ready() {
|
|
187
|
-
const proc = this.proc;
|
|
188
|
-
return !this.stopped && !!proc && proc.exitCode === null && proc.signalCode === null && !proc.stdin.destroyed;
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
// One user message; resolves with the reply text as soon as its text block
|
|
192
|
-
// closes (before the CLI's end-of-turn work), or "" when cancelled/failed.
|
|
193
|
-
say(content) {
|
|
194
|
-
if (!this.ready()) return { generation: null, text: Promise.resolve("") };
|
|
195
|
-
let resolve;
|
|
196
|
-
const text = new Promise((done) => { resolve = done; });
|
|
197
|
-
const generation = { id: ++this._seq, text: "", cancelled: false, settled: false, resolve };
|
|
198
|
-
this.queue.push(generation);
|
|
199
|
-
try {
|
|
200
|
-
this.proc.stdin.write(`${JSON.stringify({ type: "user", message: { role: "user", content: String(content) } })}\n`);
|
|
201
|
-
} catch (_) {
|
|
202
|
-
this.queue = this.queue.filter((item) => item !== generation);
|
|
203
|
-
resolve("");
|
|
204
|
-
}
|
|
205
|
-
return { generation, text };
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
// Stops the in-flight generation (Claude Code's stream-json control
|
|
209
|
-
// protocol); the CLI then closes that turn with an error result.
|
|
210
|
-
cancel(generation) {
|
|
211
|
-
if (!generation || generation.cancelled) return;
|
|
212
|
-
generation.cancelled = true;
|
|
213
|
-
generation.resolve("");
|
|
214
|
-
if (generation.settled || this.queue[0] !== generation || !this.ready()) return;
|
|
215
|
-
try {
|
|
216
|
-
this.proc.stdin.write(`${JSON.stringify({ type: "control_request", request_id: `quick-interrupt-${generation.id}`, request: { subtype: "interrupt" } })}\n`);
|
|
217
|
-
} catch (_) {}
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
_onEvent(event) {
|
|
221
|
-
const generation = this.queue[0];
|
|
222
|
-
if (!generation) return;
|
|
223
|
-
if (event.type === "stream_event") {
|
|
224
|
-
const inner = event.event || {};
|
|
225
|
-
if (inner.type === "content_block_delta" && inner.delta?.type === "text_delta" && typeof inner.delta.text === "string") {
|
|
226
|
-
generation.text += inner.delta.text;
|
|
227
|
-
} else if (inner.type === "content_block_stop" && generation.text.trim()) {
|
|
228
|
-
generation.resolve(generation.cancelled ? "" : generation.text);
|
|
229
|
-
}
|
|
230
|
-
} else if (event.type === "assistant" && !generation.text && Array.isArray(event.message?.content)) {
|
|
231
|
-
// A CLI without partial messages: the whole reply at once.
|
|
232
|
-
generation.text = event.message.content.filter((block) => block?.type === "text").map((block) => block.text || "").join("");
|
|
233
|
-
} else if (event.type === "result") {
|
|
234
|
-
this.queue.shift();
|
|
235
|
-
generation.settled = true;
|
|
236
|
-
generation.resolve(generation.cancelled || event.is_error ? "" : generation.text);
|
|
237
|
-
}
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
_crashed(proc, reason) {
|
|
241
|
-
if (this.proc !== proc) return;
|
|
242
|
-
this.proc = null;
|
|
243
|
-
for (const generation of this.queue.splice(0)) { generation.settled = true; generation.resolve(""); }
|
|
244
|
-
try { killProcessTree(proc.pid, "SIGKILL"); } catch (_) {}
|
|
245
|
-
if (this.stopped) return;
|
|
246
|
-
if (this.restarts >= this.maxRestarts) {
|
|
247
|
-
this.log(`gave up reason=${reason} restarts=${this.restarts}`);
|
|
248
|
-
return;
|
|
249
|
-
}
|
|
250
|
-
this.restarts += 1;
|
|
251
|
-
this.log(`restart reason=${reason} restarts=${this.restarts}`);
|
|
252
|
-
try { this.start(); } catch (error) { this.log(`restart failed: ${error.message}`); }
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
stop() {
|
|
256
|
-
if (this._stopPromise) return this._stopPromise;
|
|
257
|
-
this.stopped = true;
|
|
258
|
-
live.delete(this);
|
|
259
|
-
const proc = this.proc;
|
|
260
|
-
this.proc = null;
|
|
261
|
-
for (const generation of this.queue.splice(0)) { generation.settled = true; generation.cancelled = true; generation.resolve(""); }
|
|
262
|
-
if (!proc) return (this._stopPromise = Promise.resolve());
|
|
263
|
-
this._stopPromise = new Promise((resolve) => {
|
|
264
|
-
let killTimer = null;
|
|
265
|
-
const finish = () => { if (killTimer) clearTimeout(killTimer); resolve(); };
|
|
266
|
-
if (proc.exitCode !== null || proc.signalCode !== null) return finish();
|
|
267
|
-
proc.once("close", finish);
|
|
268
|
-
try { proc.stdin.end(); } catch (_) {}
|
|
269
|
-
try { killProcessTree(proc.pid, "SIGTERM"); } catch (_) { try { proc.kill("SIGTERM"); } catch (_) {} }
|
|
270
|
-
killTimer = setTimeout(() => {
|
|
271
|
-
try { killProcessTree(proc.pid, "SIGKILL"); } catch (_) { try { proc.kill("SIGKILL"); } catch (_) {} }
|
|
272
|
-
}, KILL_GRACE_MS);
|
|
273
|
-
killTimer.unref?.();
|
|
274
|
-
});
|
|
275
|
-
return this._stopPromise;
|
|
276
|
-
}
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
// A bot exit never leaves a quick-voice process behind.
|
|
280
|
-
const live = new Set();
|
|
281
|
-
function track(quick) {
|
|
282
|
-
live.add(quick);
|
|
283
|
-
return quick;
|
|
284
|
-
}
|
|
285
|
-
process.on("exit", () => {
|
|
286
|
-
for (const quick of live) {
|
|
287
|
-
const pid = quick.proc?.pid;
|
|
288
|
-
if (pid) { try { killProcessTree(pid, "SIGKILL"); } catch (_) {} }
|
|
289
|
-
}
|
|
290
|
-
});
|
|
291
|
-
|
|
292
|
-
module.exports = {
|
|
293
|
-
QuickVoiceProcess, quickVoiceSettings, buildQuickPrompt, utteranceMessage, fillerMessage, spokenLine,
|
|
294
|
-
scrubLine, ownerDisplayName, track,
|
|
295
|
-
};
|