@inetafrica/open-claudia 3.4.14 → 3.4.16
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 +14 -0
- package/bin/cli.js +28 -0
- package/bin/loopback-client.js +1 -0
- package/channels/kazee/call/manager.js +66 -14
- package/channels/kazee/call/session.js +154 -80
- package/channels/kazee/call/speech-client.js +96 -19
- package/channels/kazee/call/turn-gate.js +62 -0
- package/core/call-speech.js +111 -0
- package/core/loopback.js +10 -1
- package/core/providers/env.js +1 -0
- package/core/run-context.js +13 -6
- package/core/runner.js +69 -7
- package/core/system-prompt.js +19 -7
- package/package.json +4 -3
- package/test-call-speech.js +237 -0
- package/test-kazee-call-manager.js +57 -2
- package/test-kazee-call-session.js +153 -7
- package/test-kazee-call-speech.js +38 -2
- package/test-provider-env-isolation.js +5 -0
- package/test-provider-gateway.js +15 -1
- package/test-provider-language.js +3 -1
- package/test-provider-prompt-parity.js +16 -1
- package/test-provider-recall.js +46 -0
- package/test-provider-runner.js +34 -0
- package/test-tool-manifest.js +25 -12
- package/test-tools.js +11 -7
package/core/system-prompt.js
CHANGED
|
@@ -997,6 +997,10 @@ async function filterMatches(userText, contextText, packMatches, entityMatches,
|
|
|
997
997
|
packMatches: packMatches.filter((m) => m.origin === "user"),
|
|
998
998
|
entityMatches: entityMatches.filter((m) => m.origin === "user"),
|
|
999
999
|
});
|
|
1000
|
+
// The classic engine still uses an auxiliary model. On a live call that
|
|
1001
|
+
// adds seconds before the answering model can even start. Keep the local
|
|
1002
|
+
// user-keyword baseline; context-only hits still need a relevance judge.
|
|
1003
|
+
if (opts.liveCall === true) return failOpen();
|
|
1000
1004
|
if (packMatches.length === 0 && entityMatches.length === 0) {
|
|
1001
1005
|
return { packMatches, entityMatches };
|
|
1002
1006
|
}
|
|
@@ -1093,6 +1097,9 @@ const LIVE_CALL_GUIDANCE = [
|
|
|
1093
1097
|
"- Phone-call style: one to three short conversational sentences, then stop. No markdown, bullets, headings, emoji, backticks, or asterisks — they are noise when spoken.",
|
|
1094
1098
|
"- 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.",
|
|
1095
1099
|
"- 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.",
|
|
1100
|
+
"- 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.",
|
|
1101
|
+
"- 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.",
|
|
1102
|
+
"- 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.",
|
|
1096
1103
|
].join("\n");
|
|
1097
1104
|
|
|
1098
1105
|
const VOICE_REPLY_GUIDANCE = [
|
|
@@ -1126,7 +1133,12 @@ function voiceReplyGuidance(opts = {}) {
|
|
|
1126
1133
|
const { currentTransport, currentAdapter } = require("./context");
|
|
1127
1134
|
// A live Kazee call: the reply is spoken onto the open line, so call-style
|
|
1128
1135
|
// brevity outranks every chat-formatting rule.
|
|
1129
|
-
if (currentAdapter()?.liveCall === true)
|
|
1136
|
+
if (currentAdapter()?.liveCall === true) {
|
|
1137
|
+
const history = opts.runContext?.callHistory || [];
|
|
1138
|
+
return LIVE_CALL_GUIDANCE + (history.length
|
|
1139
|
+
? `\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.`
|
|
1140
|
+
: "");
|
|
1141
|
+
}
|
|
1130
1142
|
const state = promptState(opts);
|
|
1131
1143
|
if (currentTransport() === "voice") return state.lastInputWasVoice ? VOICE_REPLY_GUIDANCE : "";
|
|
1132
1144
|
const { VOICE_REPLY_MODE } = require("./config");
|
|
@@ -1168,12 +1180,12 @@ async function buildOptionalRecallContext(prompt, opts = {}) {
|
|
|
1168
1180
|
const budget = memoryRecallBudget();
|
|
1169
1181
|
const settings = promptState(opts).settings || {};
|
|
1170
1182
|
const recall = require("./recall");
|
|
1171
|
-
//
|
|
1172
|
-
//
|
|
1173
|
-
//
|
|
1174
|
-
|
|
1183
|
+
// Calls use classic matching with the auxiliary relevance model disabled
|
|
1184
|
+
// below. Merely selecting classic still waits on that model. Explicit ask
|
|
1185
|
+
// mode continues to retrieve memory on demand.
|
|
1186
|
+
const liveCall = opts.runContext?.liveCall ?? (currentAdapter()?.liveCall === true);
|
|
1175
1187
|
const activeName = recall.activeEngineName(settings);
|
|
1176
|
-
const engineName =
|
|
1188
|
+
const engineName = liveCall === true && activeName === "discoverer" ? "classic" : activeName;
|
|
1177
1189
|
const engine = recall.getEngine(engineName);
|
|
1178
1190
|
const recallOpts = { ...opts, recallCollector: collector };
|
|
1179
1191
|
const recallProvider = promptRunIdentity({ ...opts, state: promptState(opts) }).provider;
|
|
@@ -1186,7 +1198,7 @@ async function buildOptionalRecallContext(prompt, opts = {}) {
|
|
|
1186
1198
|
context,
|
|
1187
1199
|
packMatches,
|
|
1188
1200
|
entityMatches,
|
|
1189
|
-
{ provider: recallProvider },
|
|
1201
|
+
{ provider: recallProvider, liveCall },
|
|
1190
1202
|
),
|
|
1191
1203
|
logRecall,
|
|
1192
1204
|
buildPackBlock: (matches, recallBudget, engineOpts) => buildPackBlock(matches, recallBudget, { ...recallOpts, ...(engineOpts || {}) }),
|
package/package.json
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@inetafrica/open-claudia",
|
|
3
|
-
"version": "3.4.
|
|
3
|
+
"version": "3.4.16",
|
|
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": {
|
|
7
7
|
"open-claudia": "./bin/cli.js"
|
|
8
8
|
},
|
|
9
9
|
"scripts": {
|
|
10
|
-
"test:kazee-call": "node test-kazee-call-audio.js && node test-kazee-call-janus.js && node test-kazee-call-webrtc.js && node test-kazee-call-speech.js && node test-kazee-call-manager.js && node test-kazee-call-session.js",
|
|
10
|
+
"test:kazee-call": "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",
|
|
11
11
|
"setup": "node setup.js",
|
|
12
12
|
"start": "node bot.js",
|
|
13
|
-
"pretest": "node test-provider-fixture.js && node test-provider-core-prompt.js && node test-turn-observer.js && node test-provider-prompt-parity.js && node test-provider-env-isolation.js && node test-provider-resume-parity.js && node test-provider-bootstrap.js && node test-provider-registry.js && node test-provider-stream-decoder.js && node test-provider-events.js && node test-provider-claude.js && node test-provider-codex.js && node test-provider-runner.js && node test-provider-refusal-recovery.js && node test-provider-run-lifecycle.js && node test-provider-gateway.js && node test-provider-side-chat.js && node test-single-runtime.js && node test-provider-migration-backup.js && node test-provider-state-migration.js && node test-provider-session-history.js && node test-provider-session-commands.js && node test-provider-compaction.js && node test-provider-scheduler.js && node test-credential-capture.js && node test-cursor-removal.js && node test-utility-provider-policy.js && node test-provider-subagent.js && node test-provider-recall.js && node test-provider-pack-review.js && node test-memory-mutation-queue.js && node test-provider-dream.js && node test-provider-enforcer.js && node test-provider-tool-hooks.js && node test-provider-boot-matrix.js && node test-provider-capabilities.js && node test-provider-language.js && node test-provider-coupling-audit.js && node test-provider-parity-e2e.js && node test-kazee-channel-health.js && node test-kazee-media-caption.js && node test-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-spaces-comment-content.js && node test-boot-health.js && node test-lexical-floor.js && node test-safe-mode.js && node test-logout.js && node test-kazee-call-audio.js && node test-kazee-call-janus.js && node test-kazee-call-webrtc.js && node test-kazee-call-speech.js && node test-kazee-call-manager.js && node test-kazee-call-session.js",
|
|
13
|
+
"pretest": "node test-provider-fixture.js && node test-provider-core-prompt.js && node test-turn-observer.js && node test-provider-prompt-parity.js && node test-provider-env-isolation.js && node test-provider-resume-parity.js && node test-provider-bootstrap.js && node test-provider-registry.js && node test-provider-stream-decoder.js && node test-provider-events.js && node test-provider-claude.js && node test-provider-codex.js && node test-provider-runner.js && node test-provider-refusal-recovery.js && node test-provider-run-lifecycle.js && node test-provider-gateway.js && node test-provider-side-chat.js && node test-single-runtime.js && node test-provider-migration-backup.js && node test-provider-state-migration.js && node test-provider-session-history.js && node test-provider-session-commands.js && node test-provider-compaction.js && node test-provider-scheduler.js && node test-credential-capture.js && node test-cursor-removal.js && node test-utility-provider-policy.js && node test-provider-subagent.js && node test-provider-recall.js && node test-provider-pack-review.js && node test-memory-mutation-queue.js && node test-provider-dream.js && node test-provider-enforcer.js && node test-provider-tool-hooks.js && node test-provider-boot-matrix.js && node test-provider-capabilities.js && node test-provider-language.js && node test-provider-coupling-audit.js && node test-provider-parity-e2e.js && node test-kazee-channel-health.js && node test-kazee-media-caption.js && node test-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-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
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-abilities.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-ability-extraction.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-ability-couse.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-ability-transfer.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-ability-tiers.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-ability-merge-guard.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-learning-e2e.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-pack-nesting.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-pack-search.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-extractive-injection.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-journal-dedupe.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-read-signal.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-tools.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-tool-graph.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-tool-guard.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-tooling-mode.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-tool-manifest.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-recall-evolution.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-approval-async.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-unified-identity.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-queue-routing.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-persona-packs.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-speaker-persona.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-persona-pipeline.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-recall-relationship-gate.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-relationship.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-enforcer.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-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-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-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-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-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-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-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"
|
|
15
15
|
},
|
|
16
16
|
"files": [
|
|
@@ -80,6 +80,7 @@
|
|
|
80
80
|
"test-kazee-call-webrtc.js",
|
|
81
81
|
"test-kazee-call-speech.js",
|
|
82
82
|
"test-kazee-call-manager.js",
|
|
83
|
+
"test-call-speech.js",
|
|
83
84
|
"test-kazee-call-session.js",
|
|
84
85
|
"test-prompt-budget.js",
|
|
85
86
|
"test-agency-ledger.js",
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
|
|
4
|
+
const assert = require("assert");
|
|
5
|
+
const fs = require("fs");
|
|
6
|
+
const os = require("os");
|
|
7
|
+
const path = require("path");
|
|
8
|
+
const http = require("http");
|
|
9
|
+
const { spawn } = require("child_process");
|
|
10
|
+
|
|
11
|
+
function cli(env, args, input = "") {
|
|
12
|
+
return new Promise((resolve, reject) => {
|
|
13
|
+
const child = spawn(process.execPath, [path.join(__dirname, "bin/cli.js"), "call-say", ...args],
|
|
14
|
+
{ env, stdio: ["pipe", "pipe", "pipe"] });
|
|
15
|
+
let stdout = "", stderr = "";
|
|
16
|
+
child.stdout.on("data", data => { stdout += data; });
|
|
17
|
+
child.stderr.on("data", data => { stderr += data; });
|
|
18
|
+
child.on("error", reject);
|
|
19
|
+
child.on("close", code => resolve({ code, stdout, stderr }));
|
|
20
|
+
child.stdin.end(input);
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Real provider child -> real CLI child -> HTTP -> call adapter, with no
|
|
25
|
+
// model, speech service, or real recipient. Emit a terminal result only AFTER
|
|
26
|
+
// the first sentence has been acknowledged as completely spoken.
|
|
27
|
+
if (process.env.OC_CALL_SPEECH_FIXTURE === "1") {
|
|
28
|
+
(async () => {
|
|
29
|
+
const hasPermission = !!process.env.OC_CALL_SPEECH_TOKEN;
|
|
30
|
+
if (hasPermission) {
|
|
31
|
+
const result = await cli(process.env, ["--id", "first", "The first result is ready."]);
|
|
32
|
+
assert.strictEqual(result.code, 0, result.stderr);
|
|
33
|
+
}
|
|
34
|
+
process.stdout.write(JSON.stringify({ type: "result", subtype: "success", is_error: false,
|
|
35
|
+
result: hasPermission ? "The first result is ready. The second result is ready." : "No call speech permission." }) + "\n");
|
|
36
|
+
})().catch(error => { console.error(error.message); process.exitCode = 1; });
|
|
37
|
+
} else {
|
|
38
|
+
process.env.OPEN_CLAUDIA_TEST = "1";
|
|
39
|
+
process.env.OPEN_CLAUDIA_CONFIG_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "oc-call-speech-"));
|
|
40
|
+
process.env.WORKSPACE = process.env.OPEN_CLAUDIA_CONFIG_DIR;
|
|
41
|
+
const loopback = require("./core/loopback");
|
|
42
|
+
const speech = require("./core/call-speech");
|
|
43
|
+
const relationship = require("./core/relationship");
|
|
44
|
+
const enforcer = require("./core/enforcer");
|
|
45
|
+
const context = require("./core/context");
|
|
46
|
+
const { registerSecrets } = require("./core/redact");
|
|
47
|
+
const leases = [];
|
|
48
|
+
const deferred = () => { let resolve; const promise = new Promise(r => { resolve = r; }); return { promise, resolve }; };
|
|
49
|
+
let sequence = 0;
|
|
50
|
+
function fixture() {
|
|
51
|
+
const controller = new AbortController();
|
|
52
|
+
const spoken = [];
|
|
53
|
+
let active = true, cancelled = 0;
|
|
54
|
+
const adapter = { id: "kazee-test", type: "kazee", liveCall: true,
|
|
55
|
+
callTurn: { signal: controller.signal },
|
|
56
|
+
isCallTurnActive: () => active,
|
|
57
|
+
speakSentence: async text => { spoken.push(text); return true; },
|
|
58
|
+
cancelCallSpeech: () => { cancelled++; },
|
|
59
|
+
};
|
|
60
|
+
const store = { adapter, channelId: "call-room", userId: "caller", canonicalUserId: "kazee:caller", transport: "kazee" };
|
|
61
|
+
const run = { liveCall: true, purpose: "foreground", runId: `run-${++sequence}`, timeoutMs: 5000 };
|
|
62
|
+
const lease = speech.prepare(store, run);
|
|
63
|
+
leases.push(lease);
|
|
64
|
+
const payload = { adapter: adapter.id, channelId: store.channelId, userId: store.userId,
|
|
65
|
+
canonicalUserId: store.canonicalUserId, originRunId: run.runId,
|
|
66
|
+
callSpeechToken: lease.token, requestId: "sentence", text: "The result is ready." };
|
|
67
|
+
return { adapter, store, run, lease, payload, spoken, controller,
|
|
68
|
+
mute() { active = false; }, get cancelled() { return cancelled; } };
|
|
69
|
+
}
|
|
70
|
+
function post(info, payload, token = info.token) {
|
|
71
|
+
return new Promise((resolve, reject) => {
|
|
72
|
+
const data = JSON.stringify(payload);
|
|
73
|
+
const req = http.request(`${info.url}/call-say`, { method: "POST", headers: {
|
|
74
|
+
Authorization: `Bearer ${token}`, "Content-Type": "application/json", "Content-Length": Buffer.byteLength(data),
|
|
75
|
+
} }, res => {
|
|
76
|
+
let body = "";
|
|
77
|
+
res.on("data", chunk => { body += chunk; });
|
|
78
|
+
res.on("end", () => resolve({ status: res.statusCode, body: JSON.parse(body) }));
|
|
79
|
+
});
|
|
80
|
+
req.on("error", reject); req.end(data);
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
const rejectsStatus = (promise, status) => assert.rejects(promise, error => error.status === status);
|
|
84
|
+
async function main() {
|
|
85
|
+
relationship.isCurrentSpeakerGuarded = () => false;
|
|
86
|
+
const info = await loopback.start({ findAdapter: id => id === "kazee-test" ? { type: "kazee" } : null });
|
|
87
|
+
let f = fixture();
|
|
88
|
+
assert.strictEqual(speech.prepare(f.store, { ...f.run, liveCall: false }), null);
|
|
89
|
+
assert.strictEqual(speech.prepare(f.store, { ...f.run, purpose: "silent" }), null);
|
|
90
|
+
await rejectsStatus(speech.speak(f.payload), 409); // prepared but no provider yet
|
|
91
|
+
f.lease.activate(); f.lease.activate();
|
|
92
|
+
assert.strictEqual((await post(info, f.payload, "wrong-token")).status, 401);
|
|
93
|
+
for (const field of ["adapter", "channelId", "userId", "canonicalUserId", "originRunId", "callSpeechToken"]) {
|
|
94
|
+
await rejectsStatus(speech.speak({ ...f.payload, [field]: "other" }), 409);
|
|
95
|
+
}
|
|
96
|
+
assert.strictEqual(f.spoken.length, 0);
|
|
97
|
+
const env = { PATH: process.env.PATH, HOME: process.env.OPEN_CLAUDIA_CONFIG_DIR,
|
|
98
|
+
OPEN_CLAUDIA_CONFIG_DIR: process.env.OPEN_CLAUDIA_CONFIG_DIR,
|
|
99
|
+
OC_SEND_URL: info.url, OC_SEND_TOKEN: info.token, OC_CHANNEL_ADAPTER: f.adapter.id,
|
|
100
|
+
OC_CHANNEL_ID: f.store.channelId, OC_CHANNEL_USER_ID: f.store.userId,
|
|
101
|
+
OC_CANONICAL_USER_ID: f.store.canonicalUserId, OC_RUN_ID: f.run.runId, OC_CALL_SPEECH_TOKEN: f.lease.token };
|
|
102
|
+
const text = "A quoted `name` and $(text) are spoken literally.";
|
|
103
|
+
assert.strictEqual((await cli(env, ["--id", "stdin", "--stdin"], text)).code, 0);
|
|
104
|
+
assert.deepStrictEqual(f.spoken, [text]);
|
|
105
|
+
const duplicate = await cli(env, ["--id", "stdin", "--stdin"], text);
|
|
106
|
+
assert.strictEqual(duplicate.code, 0);
|
|
107
|
+
assert.match(duplicate.stdout, /Already spoken/);
|
|
108
|
+
assert.strictEqual(f.spoken.length, 1);
|
|
109
|
+
assert.strictEqual((await cli(env, ["--id", "stdin", "Different text."])).code, 1);
|
|
110
|
+
assert.strictEqual((await cli({ ...env, OC_CALL_SPEECH_TOKEN: "" }, ["A sentence."])).code, 1);
|
|
111
|
+
assert.strictEqual((await post(info, { ...f.payload, text: "x".repeat(451) })).status, 400);
|
|
112
|
+
registerSecrets(["synthetic-speech-secret"]);
|
|
113
|
+
await speech.speak({ ...f.payload, requestId: "redacted", text: "Value synthetic-speech-secret." });
|
|
114
|
+
assert.strictEqual(f.spoken.at(-1), "Value [REDACTED_SECRET].");
|
|
115
|
+
f.lease.close();
|
|
116
|
+
await rejectsStatus(speech.speak(f.payload), 409);
|
|
117
|
+
|
|
118
|
+
// Recipient classification and the actual outbound guard run inside the
|
|
119
|
+
// captured caller context, not the HTTP client's supplied identity.
|
|
120
|
+
f = fixture(); f.lease.activate();
|
|
121
|
+
relationship.isCurrentSpeakerGuarded = () => {
|
|
122
|
+
assert.strictEqual(context.currentUserId(), "caller"); return true;
|
|
123
|
+
};
|
|
124
|
+
enforcer.guardOutboundReply = async () => ({ allow: false });
|
|
125
|
+
await rejectsStatus(speech.speak(f.payload), 403);
|
|
126
|
+
enforcer.guardOutboundReply = async () => { throw new Error("guard unavailable"); };
|
|
127
|
+
await rejectsStatus(speech.speak({ ...f.payload, requestId: "guard-error" }), 403);
|
|
128
|
+
relationship.isCurrentSpeakerGuarded = () => { throw new Error("classification unavailable"); };
|
|
129
|
+
await rejectsStatus(speech.speak({ ...f.payload, requestId: "class-error" }), 403);
|
|
130
|
+
assert.strictEqual(f.spoken.length, 0);
|
|
131
|
+
relationship.isCurrentSpeakerGuarded = () => true;
|
|
132
|
+
enforcer.guardOutboundReply = async text => {
|
|
133
|
+
assert.strictEqual(context.currentCanonicalUserId(), "kazee:caller");
|
|
134
|
+
assert.strictEqual(text, f.payload.text);
|
|
135
|
+
return { allow: true };
|
|
136
|
+
};
|
|
137
|
+
assert.strictEqual((await speech.speak({ ...f.payload, requestId: "approved" })).spoken, true);
|
|
138
|
+
enforcer.guardOutboundReply = async text => {
|
|
139
|
+
assert.strictEqual(text, `${f.payload.text} Follow-up result.`);
|
|
140
|
+
return { allow: false };
|
|
141
|
+
};
|
|
142
|
+
await rejectsStatus(speech.speak({ ...f.payload, requestId: "cumulative", text: "Follow-up result." }), 403);
|
|
143
|
+
assert.strictEqual(f.spoken.length, 1, "guard sees the combined spoken answer, not an isolated follow-up");
|
|
144
|
+
|
|
145
|
+
f = fixture(); f.lease.activate();
|
|
146
|
+
relationship.isCurrentSpeakerGuarded = () => true;
|
|
147
|
+
const checking = deferred(), releaseCheck = deferred();
|
|
148
|
+
enforcer.guardOutboundReply = async () => { checking.resolve(); await releaseCheck.promise; return { allow: true }; };
|
|
149
|
+
const guarded = rejectsStatus(speech.speak(f.payload), 409);
|
|
150
|
+
await checking.promise; f.mute(); releaseCheck.resolve(); await guarded;
|
|
151
|
+
assert.strictEqual(f.spoken.length, 0, "speech resumed while reply guard was checking");
|
|
152
|
+
|
|
153
|
+
relationship.isCurrentSpeakerGuarded = () => false;
|
|
154
|
+
f = fixture(); f.lease.activate();
|
|
155
|
+
const playback = deferred(), playing = deferred();
|
|
156
|
+
f.adapter.speakSentence = async () => { playing.resolve(); return playback.promise; };
|
|
157
|
+
const first = speech.speak(f.payload);
|
|
158
|
+
const firstRejected = rejectsStatus(first, 409);
|
|
159
|
+
await playing.promise;
|
|
160
|
+
const retryRejected = rejectsStatus(speech.speak(f.payload), 409);
|
|
161
|
+
await rejectsStatus(speech.speak({ ...f.payload, requestId: "overlap" }), 429);
|
|
162
|
+
f.controller.abort(); playback.resolve(false);
|
|
163
|
+
await Promise.all([firstRejected, retryRejected]);
|
|
164
|
+
assert.strictEqual(f.cancelled, 1, "abort cancels current playback once");
|
|
165
|
+
await rejectsStatus(speech.speak(f.payload), 409);
|
|
166
|
+
|
|
167
|
+
f = fixture(); f.lease.activate();
|
|
168
|
+
for (let n = 0; n < 8; n++) await speech.speak({ ...f.payload, requestId: `bounded-${n}` });
|
|
169
|
+
await rejectsStatus(speech.speak({ ...f.payload, requestId: "ninth" }), 429);
|
|
170
|
+
assert.strictEqual(speech.unspokenSuffix("One. Two. Three.", ["One.", "Two."]), "Three.");
|
|
171
|
+
assert.strictEqual(speech.unspokenSuffix("One. Two.", ["One.", "Two."]), "");
|
|
172
|
+
assert.strictEqual(speech.unspokenSuffix("Correction: One is wrong.", ["One."]), "Correction: One is wrong.");
|
|
173
|
+
f = fixture(); f.lease.activate();
|
|
174
|
+
const expired = deferred(); f.adapter.speakSentence = () => expired.promise;
|
|
175
|
+
const expiring = rejectsStatus(speech.speak(f.payload), 409);
|
|
176
|
+
f.lease.close(); expired.resolve(false); await expiring;
|
|
177
|
+
assert.strictEqual(f.cancelled, 1);
|
|
178
|
+
|
|
179
|
+
await runnerIntegration(info);
|
|
180
|
+
console.log("call speech lease, guard, CLI and runner integration OK");
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
async function runnerIntegration(info) {
|
|
184
|
+
// Replace only model/prompt/observer dependencies; use the production
|
|
185
|
+
// foreground factory, invocation decorator, subprocess lifecycle and HTTP.
|
|
186
|
+
const providers = require("./core/providers");
|
|
187
|
+
const { createClaudeProvider } = require("./core/providers/claude");
|
|
188
|
+
const provider = createClaudeProvider({ hookTransport: false,
|
|
189
|
+
resolveExecutable: () => process.execPath, defaultModel: "fixture-model",
|
|
190
|
+
getAuthStatus: () => ({ state: "authenticated", reason: null }),
|
|
191
|
+
buildEnv: () => ({ PATH: process.env.PATH, HOME: process.env.OPEN_CLAUDIA_CONFIG_DIR,
|
|
192
|
+
OPEN_CLAUDIA_CONFIG_DIR: process.env.OPEN_CLAUDIA_CONFIG_DIR, OC_CALL_SPEECH_FIXTURE: "1" }),
|
|
193
|
+
});
|
|
194
|
+
const build = provider.buildMainInvocation.bind(provider);
|
|
195
|
+
provider.buildMainInvocation = input => { const result = build(input); return { ...result, args: [__filename, ...result.args] }; };
|
|
196
|
+
providers.resolveProviderForRun = () => provider;
|
|
197
|
+
require("./core/system-prompt").buildTurnPrompt = async prompt => ({
|
|
198
|
+
coreInstructions: "Fixture", dynamicContext: "", userPrompt: prompt, transcriptPaths: {}, recallMetadata: {},
|
|
199
|
+
});
|
|
200
|
+
require("./core/turn-observer").createTurnObserver = () => ({ onRecallSurfaced() {}, onToolStart() {} });
|
|
201
|
+
delete require.cache[require.resolve("./core/runner")];
|
|
202
|
+
const { createForegroundService } = require("./core/runner");
|
|
203
|
+
let lastLease;
|
|
204
|
+
const prepare = speech.prepare;
|
|
205
|
+
speech.prepare = (...args) => { lastLease = prepare(...args); if (lastLease) leases.push(lastLease); return lastLease; };
|
|
206
|
+
const f = fixture();
|
|
207
|
+
const state = { userId: "kazee:caller", currentSession: { name: "call-fixture", dir: process.env.WORKSPACE },
|
|
208
|
+
settings: { backend: "claude", model: "fixture-model", permissionMode: "default" },
|
|
209
|
+
providerSettings: { claude: { model: "fixture-model" } }, activeSessions: {}, messageQueue: [] };
|
|
210
|
+
const spoken = [];
|
|
211
|
+
f.adapter.suppressStreamPreview = true; f.adapter.suppressChatVisibility = true;
|
|
212
|
+
f.adapter.speakSentence = async text => {
|
|
213
|
+
assert.ok(state.runningProcess, "sentence is spoken before provider completion");
|
|
214
|
+
spoken.push(text); return true;
|
|
215
|
+
};
|
|
216
|
+
f.adapter.send = async (_channel, text) => {
|
|
217
|
+
await rejectsStatus(speech.speak({ ...f.payload, callSpeechToken: lastLease?.token }), 409);
|
|
218
|
+
spoken.push(speech.unspokenSuffix(text, spoken)); return true;
|
|
219
|
+
};
|
|
220
|
+
f.adapter.typing = async () => {};
|
|
221
|
+
const service = createForegroundService({ state, store: f.store, stopTyping() {}, capture: false, ownsAdmissionLock: false });
|
|
222
|
+
const result = await context.runInChat(f.store, () => service.runAgent("Give two results.", process.env.WORKSPACE, null));
|
|
223
|
+
assert.strictEqual(result.ok, true, JSON.stringify(result.error));
|
|
224
|
+
assert.deepStrictEqual(spoken, ["The first result is ready.", "The second result is ready."]);
|
|
225
|
+
const token = lastLease.token;
|
|
226
|
+
assert.strictEqual((await post(info, { ...f.payload, callSpeechToken: token, originRunId: result.runId })).status, 409);
|
|
227
|
+
assert.strictEqual(state.runningProcess, null);
|
|
228
|
+
const capturedService = createForegroundService({ state, store: f.store, stopTyping() {}, capture: true, ownsAdmissionLock: false, persistCapture: false });
|
|
229
|
+
const captured = await context.runInChat(f.store, () => capturedService.runAgentCapture("Capture only.", process.env.WORKSPACE));
|
|
230
|
+
assert.strictEqual(captured.ok, true, JSON.stringify(captured.error));
|
|
231
|
+
assert.strictEqual(captured.text, "No call speech permission.");
|
|
232
|
+
speech.prepare = prepare;
|
|
233
|
+
}
|
|
234
|
+
main().then(() => { for (const lease of leases) lease.close(); loopback.stop(); process.exit(0); }, error => {
|
|
235
|
+
console.error(error); for (const lease of leases) lease.close(); loopback.stop(); process.exit(1);
|
|
236
|
+
});
|
|
237
|
+
}
|
|
@@ -6,7 +6,7 @@ class FakeSession {
|
|
|
6
6
|
static instances = [];
|
|
7
7
|
constructor(options) { this.options = options; this.roomId = options.roomId; FakeSession.instances.push(this); }
|
|
8
8
|
async start() { this.started = true; }
|
|
9
|
-
async speak(text) { this.spoken = text; }
|
|
9
|
+
async speak(text) { this.spoken = text; (this.spokenChunks ||= []).push(text); }
|
|
10
10
|
async close(reason) { this.closed = reason; this.options.onEnded(reason); }
|
|
11
11
|
}
|
|
12
12
|
function fixture() {
|
|
@@ -37,8 +37,63 @@ function fixture() {
|
|
|
37
37
|
assert.strictEqual(f.inbound[0].payload.adapter.liveCall, true);
|
|
38
38
|
await f.inbound[0].payload.adapter.send("chat", "Your agenda is clear.");
|
|
39
39
|
assert.strictEqual(f.manager.session.spoken, "Your agenda is clear.");
|
|
40
|
-
|
|
40
|
+
const firstCallSession = f.manager.session;
|
|
41
|
+
const firstReply = f.inbound[0].payload.adapter;
|
|
42
|
+
firstCallSession.options.onSpeechStart();
|
|
43
|
+
await firstReply.send("chat", "An obsolete reply.");
|
|
44
|
+
assert.strictEqual(firstCallSession.spoken, "Your agenda is clear.", "resuming speech suppresses the old reply before a new final exists");
|
|
45
|
+
firstCallSession.options.onTranscript("Actually, what is tomorrow's agenda?");
|
|
46
|
+
assert.strictEqual(firstReply.callTurn.signal.aborted, true, "new complete utterance cancels obsolete queued work");
|
|
47
|
+
const secondReply = f.inbound[1].payload.adapter;
|
|
48
|
+
assert.ok(secondReply.callHistory.some(entry => entry.text === "What is on my agenda?"), "coalescing retains the earlier question as context");
|
|
49
|
+
assert.strictEqual(secondReply.callTurn.signal.aborted, false);
|
|
50
|
+
await secondReply.send("chat", "Tomorrow is free.");
|
|
51
|
+
assert.strictEqual(firstCallSession.spoken, "Tomorrow is free.");
|
|
52
|
+
let releaseClose;
|
|
53
|
+
const closeGate = new Promise(resolve => { releaseClose = resolve; });
|
|
54
|
+
firstCallSession.close = async reason => { await closeGate; firstCallSession.closed = reason; firstCallSession.options.onEnded(reason); };
|
|
55
|
+
const ending = f.manager.ended({ roomId: 12 });
|
|
56
|
+
assert.strictEqual(secondReply.callTurn.signal.aborted, true, "hangup invalidates queued work before slow media cleanup finishes");
|
|
57
|
+
releaseClose();
|
|
58
|
+
await ending;
|
|
41
59
|
assert.strictEqual(f.manager.session, null);
|
|
60
|
+
assert.strictEqual(secondReply.callTurn.signal.aborted, true, "hangup cancels queued work even after manager clears its session pointer");
|
|
61
|
+
await secondReply.send("chat", "Reply after hangup.");
|
|
62
|
+
assert.strictEqual(firstCallSession.spoken, "Tomorrow is free.");
|
|
63
|
+
await f.manager.incoming({ fromUserId: "owner-1", chatId: "chat", roomId: 13, callType: "audio" });
|
|
64
|
+
firstCallSession.options.onTranscript("Late transcript from old call.");
|
|
65
|
+
assert.strictEqual(f.inbound.length, 2, "old-session transcript cannot enter a replacement call");
|
|
66
|
+
for (let i = 0; i < 15; i++) f.manager.session.options.onTranscript(`${i} ${"a".repeat(2500)}`);
|
|
67
|
+
const history = f.inbound.at(-1).payload.adapter.callHistory;
|
|
68
|
+
assert.ok(history.length <= 8 && history.reduce((n, entry) => n + entry.text.length, 0) <= 12000, "voice history has an explicit bound");
|
|
69
|
+
await f.manager.stop();
|
|
70
|
+
|
|
71
|
+
// Explicitly streamed sentences enter history only when completely spoken;
|
|
72
|
+
// the settled final removes their exact prefix and preserves corrections.
|
|
73
|
+
f = fixture();
|
|
74
|
+
await f.manager.incoming({ fromUserId: "owner-1", chatId: "speech", roomId: 14, callType: "audio" });
|
|
75
|
+
const streamedSession = f.manager.session;
|
|
76
|
+
streamedSession.options.onTranscript("Give me both results.");
|
|
77
|
+
const streamed = f.inbound[0].payload.adapter;
|
|
78
|
+
assert.strictEqual(await streamed.speakSentence("First result."), true);
|
|
79
|
+
assert.strictEqual(await streamed.speakSentence("Second result."), true);
|
|
80
|
+
await streamed.send("speech", "First result. Second result. Third result.");
|
|
81
|
+
assert.deepStrictEqual(streamedSession.spokenChunks, ["First result.", "Second result.", "Third result."]);
|
|
82
|
+
await streamed.send("speech", "Correction: the first result changed.");
|
|
83
|
+
assert.strictEqual(streamedSession.spokenChunks.at(-1), "Correction: the first result changed.");
|
|
84
|
+
const beforeMute = streamedSession.spokenChunks.length;
|
|
85
|
+
streamedSession.options.onSpeechStart();
|
|
86
|
+
assert.strictEqual(await streamed.speakSentence("Stale sentence."), false);
|
|
87
|
+
assert.strictEqual(streamedSession.spokenChunks.length, beforeMute);
|
|
88
|
+
streamedSession.options.onTranscript("Now another question.");
|
|
89
|
+
const nextSpeech = f.inbound.at(-1).payload.adapter;
|
|
90
|
+
assert.ok(nextSpeech.callHistory.some(entry => entry.text.startsWith("First result. Second result.")));
|
|
91
|
+
assert.ok(nextSpeech.callHistory.some(entry => entry.text === "Give me both results."), "speech chunks keep the question in bounded history");
|
|
92
|
+
streamedSession.speak = async () => false;
|
|
93
|
+
assert.strictEqual(await nextSpeech.speakSentence("Interrupted sentence."), false);
|
|
94
|
+
assert.ok(!streamedSession._voiceHistory.some(entry => entry.text === "Interrupted sentence."));
|
|
95
|
+
await f.manager.stop();
|
|
96
|
+
assert.strictEqual(await nextSpeech.speakSentence("After hangup."), false);
|
|
42
97
|
|
|
43
98
|
// Outbound: session starts in caller mode, invite goes out, answer clears the ring timer.
|
|
44
99
|
f = fixture();
|