@inetafrica/open-claudia 3.1.3 → 3.1.7
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 +22 -0
- package/channels/kazee/adapter.js +5 -0
- package/core/config.js +4 -1
- package/core/doctor.js +15 -4
- package/core/dream.js +12 -3
- package/core/media.js +34 -5
- package/core/providers/claude.js +15 -0
- package/core/router.js +2 -2
- package/core/runner.js +7 -1
- package/core/utility-agent.js +3 -0
- package/core/voice-policy.js +7 -0
- package/package.json +2 -2
- package/test-provider-claude.js +13 -0
- package/test-provider-dream.js +16 -0
- package/test-provider-enforcer.js +4 -2
- package/test-provider-language.js +21 -2
- package/test-provider-run-lifecycle.js +4 -2
- package/test-provider-subagent.js +2 -1
- package/test-utility-provider-policy.js +29 -11
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,27 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## v3.1.7 — CI spawn-race hardening (first published build of the v3.1.5 changes)
|
|
4
|
+
|
|
5
|
+
- v3.1.6 also never published. The loaded shared CI runner (node cold-boot 100-370ms vs ~50ms locally) keeps picking off the next-tightest spawn-timing test: this time `test-utility-provider-policy`'s shutdown-drain case, which flat-slept 100ms then cancelled — killing the fake agent before it booted, so its SIGTERM handler never wrote the expected signal capture (pipeline 10383; the v3.1.6 run-lifecycle fix itself passed there).
|
|
6
|
+
- Instead of fixing one test per failed pipeline, audited all 13 fake-agent-spawning test files for the pattern and hardened the three racy ones: `test-utility-provider-policy` (abort and drain cases now wait for the child's boot capture before cancelling; timeout case widened to 2500ms deadline / 400ms kill grace; helper wait deadlines to 15s), `test-provider-enforcer` (timeout verdict widened to 2500ms so the hung child's boot capture and SIGTERM record exist before assertions; kill grace 300ms), `test-provider-subagent` (helper wait deadline to 15s). Semantics unchanged everywhere — the same hangs, kills, aborts, and drains are asserted, just on deterministic readiness signals instead of guessed sleeps.
|
|
7
|
+
- Ships everything listed under v3.1.5 below; no product code changes.
|
|
8
|
+
|
|
9
|
+
## v3.1.6 — CI test-timing fix (also never published — see v3.1.7)
|
|
10
|
+
|
|
11
|
+
- The v3.1.5 tag never published: its CI build failed twice on `test-provider-run-lifecycle`'s forced-termination case, whose 120ms timeout lost the node-boot race on the loaded shared runner (the fake agent was SIGTERMed before it could write the descendant capture). Widened to 2000ms timeout / 500ms kill grace — semantics unchanged (agent still hangs, is force-killed, and the detached TERM-resistant descendant must survive).
|
|
12
|
+
- Ships everything listed under v3.1.5 below; no other code changes.
|
|
13
|
+
|
|
14
|
+
## v3.1.5 — Pod-identity hosted speech + dream memory access
|
|
15
|
+
|
|
16
|
+
- **Hosted speech now authenticates with the pod's own identity.** The speech-gateway bearer becomes `AGENTSPACE_POD_TOKEN` (minted per pod at provisioning, injected via the `openclaw-secrets` secret) with `SPEECH_API_KEY` kept as fallback — so provisioned bot pods get Pocket TTS/STT without any shared API key. The gateway introspects the token against the Agent Space backend (`/pods/self/speech-authorization`), which scopes it to `speech.tts`/`speech.stt` for bot pods only. Wired through config, media, router, runner, voice-policy, and `/doctor` (which now reports which credential the speech path is using). New hermetic `test-media-speech.js` covers bearer selection and fallback order.
|
|
17
|
+
- **Dream introspection can read memory again.** The nightly dream pass's introspection step was denied read access to the memory substrate; it now runs with explicit read grants (provider allow-list + utility-agent pass-through), with regressions in the claude-provider and dream test suites.
|
|
18
|
+
|
|
19
|
+
## v3.1.4 — Preserve Kazee inbound image captions
|
|
20
|
+
|
|
21
|
+
- Maps Kazee media message content onto the transport-neutral inbound `caption` field, so image instructions reach the agent prompt instead of being replaced by the generic image-description fallback.
|
|
22
|
+
- Leaves plain Kazee text and Telegram normalization unchanged.
|
|
23
|
+
- Adds a hermetic adapter regression covering captioned images, captionless images, and ordinary text.
|
|
24
|
+
|
|
3
25
|
## v3.1.3 — Pocket TTS voice-note provider
|
|
4
26
|
|
|
5
27
|
- Adds Agent Space Pocket TTS as the preferred voice synthesizer for Telegram and streamed voice replies.
|
|
@@ -146,6 +146,11 @@ class KazeeAdapter {
|
|
|
146
146
|
? msg.medias
|
|
147
147
|
: (msg.media ? [msg.media] : (msg.attachments || []));
|
|
148
148
|
if (rawMedia.length) {
|
|
149
|
+
// Kazee carries media captions in the same `content` field used for
|
|
150
|
+
// plain text. The image router reads the transport-neutral `caption`
|
|
151
|
+
// field (matching Telegram), so preserve it explicitly on media
|
|
152
|
+
// envelopes instead of silently falling back to "Describe this image".
|
|
153
|
+
envelope.caption = text;
|
|
149
154
|
envelope.media = rawMedia.map((a) => {
|
|
150
155
|
let kind = a.type;
|
|
151
156
|
if (!kind) {
|
package/core/config.js
CHANGED
|
@@ -143,8 +143,11 @@ const ELEVENLABS_VOICE_ID = config.ELEVENLABS_VOICE_ID || process.env.ELEVENLABS
|
|
|
143
143
|
const ELEVENLABS_MODEL = config.ELEVENLABS_MODEL || process.env.ELEVENLABS_MODEL || "eleven_v3";
|
|
144
144
|
const SPEECH_SERVICE_URL = config.SPEECH_SERVICE_URL || process.env.SPEECH_SERVICE_URL || "https://speech.agents.kazee.africa";
|
|
145
145
|
const SPEECH_API_KEY = config.SPEECH_API_KEY || process.env.SPEECH_API_KEY || "";
|
|
146
|
+
const AGENTSPACE_POD_TOKEN = config.AGENTSPACE_POD_TOKEN || process.env.AGENTSPACE_POD_TOKEN || "";
|
|
146
147
|
const POCKET_TTS_VOICE = config.POCKET_TTS_VOICE || process.env.POCKET_TTS_VOICE || "alba";
|
|
147
148
|
const TTS_PROVIDER = config.TTS_PROVIDER || process.env.TTS_PROVIDER || "pocket";
|
|
149
|
+
const rawVoiceReplyMode = String(config.VOICE_REPLY_MODE || process.env.VOICE_REPLY_MODE || "on-voice-input").trim().toLowerCase();
|
|
150
|
+
const VOICE_REPLY_MODE = ["on-voice-input", "always", "never"].includes(rawVoiceReplyMode) ? rawVoiceReplyMode : "on-voice-input";
|
|
148
151
|
const SOUL_FILE = config.SOUL_FILE || path.join(CONFIG_DIR, "soul.md");
|
|
149
152
|
const CRONS_FILE = config.CRONS_FILE || path.join(CONFIG_DIR, "crons.json");
|
|
150
153
|
const JOBS_FILE = config.JOBS_FILE || path.join(CONFIG_DIR, "jobs.json");
|
|
@@ -374,7 +377,7 @@ module.exports = {
|
|
|
374
377
|
TRANSCRIPTS_DIR,
|
|
375
378
|
WHISPER_CLI, WHISPER_MODEL, FFMPEG,
|
|
376
379
|
ELEVENLABS_API_KEY, ELEVENLABS_VOICE_ID, ELEVENLABS_MODEL,
|
|
377
|
-
SPEECH_SERVICE_URL, SPEECH_API_KEY, POCKET_TTS_VOICE, TTS_PROVIDER,
|
|
380
|
+
SPEECH_SERVICE_URL, SPEECH_API_KEY, AGENTSPACE_POD_TOKEN, POCKET_TTS_VOICE, TTS_PROVIDER, VOICE_REPLY_MODE,
|
|
378
381
|
SOUL_FILE, CRONS_FILE, JOBS_FILE, TASKS_DIR, VAULT_FILE, AUTH_FILE, IDENTITIES_FILE,
|
|
379
382
|
PEOPLE_FILE, INTROS_FILE, AUDIT_FILE,
|
|
380
383
|
STATE_FILE, SESSIONS_FILE,
|
package/core/doctor.js
CHANGED
|
@@ -6,7 +6,7 @@ const os = require("os");
|
|
|
6
6
|
const path = require("path");
|
|
7
7
|
const { execFileSync } = require("child_process");
|
|
8
8
|
const {
|
|
9
|
-
WHISPER_CLI, WHISPER_MODEL, FFMPEG, WORKSPACE, CONFIG_DIR,
|
|
9
|
+
WHISPER_CLI, WHISPER_MODEL, FFMPEG, SPEECH_SERVICE_URL, SPEECH_API_KEY, AGENTSPACE_POD_TOKEN, WORKSPACE, CONFIG_DIR,
|
|
10
10
|
botSubprocessEnv,
|
|
11
11
|
} = require("./config");
|
|
12
12
|
const { redactSensitive } = require("./redact");
|
|
@@ -149,14 +149,25 @@ function runDoctorChecks(options = {}) {
|
|
|
149
149
|
: "none",
|
|
150
150
|
action: providerSummary.defaultProvider.providerId ? "" : providerSummary.defaultProvider.reason,
|
|
151
151
|
});
|
|
152
|
-
|
|
153
|
-
|
|
152
|
+
const hostedSpeech = !!SPEECH_SERVICE_URL && !!(AGENTSPACE_POD_TOKEN || SPEECH_API_KEY);
|
|
153
|
+
checks.push({
|
|
154
|
+
ok: hostedSpeech,
|
|
155
|
+
warn: !hostedSpeech,
|
|
156
|
+
label: "Hosted speech",
|
|
157
|
+
detail: hostedSpeech ? `${SPEECH_SERVICE_URL} (${AGENTSPACE_POD_TOKEN ? "pod identity" : "API key"})` : "not configured",
|
|
158
|
+
action: hostedSpeech ? "" : "Set AGENTSPACE_POD_TOKEN (managed pods) or SPEECH_API_KEY.",
|
|
159
|
+
});
|
|
160
|
+
if (hostedSpeech) {
|
|
161
|
+
const ff = binaryCheck(FFMPEG || "ffmpeg", "ffmpeg");
|
|
162
|
+
checks.push({ ...ff, label: "Voice reply transcoder", warn: !ff.ok, action: ff.ok ? "" : "Install ffmpeg or set FFMPEG for Ogg/Opus voice replies." });
|
|
163
|
+
}
|
|
164
|
+
if (!hostedSpeech && (FFMPEG || WHISPER_CLI || WHISPER_MODEL)) {
|
|
154
165
|
const ff = binaryCheck(FFMPEG || "ffmpeg", "ffmpeg");
|
|
155
166
|
const wh = binaryCheck(WHISPER_CLI, "Whisper CLI");
|
|
156
167
|
checks.push({ ...ff, action: ff.ok ? "" : "Install ffmpeg or set FFMPEG." });
|
|
157
168
|
checks.push({ ...wh, action: wh.ok ? "" : "Install whisper.cpp or set WHISPER_CLI." });
|
|
158
169
|
checks.push({ ok: !!WHISPER_MODEL && fs.existsSync(WHISPER_MODEL), label: "Whisper model", detail: WHISPER_MODEL || "not configured", action: WHISPER_MODEL ? "Check model path." : "Set WHISPER_MODEL for voice notes." });
|
|
159
|
-
} else checks.push({ ok: true, warn: true, label: "
|
|
170
|
+
} else if (!hostedSpeech) checks.push({ ok: true, warn: true, label: "Local voice fallback", detail: "not configured (optional)", action: "Set FFMPEG/WHISPER_CLI/WHISPER_MODEL for local voice-note fallback." });
|
|
160
171
|
|
|
161
172
|
checks.push(checkWritableDir(WORKSPACE, "Workspace writable"));
|
|
162
173
|
checks.push(checkWritableDir(CONFIG_DIR, "Config dir writable"));
|
package/core/dream.js
CHANGED
|
@@ -390,7 +390,7 @@ function parseDream(text) {
|
|
|
390
390
|
entity_notes: arr("entity_notes"),
|
|
391
391
|
archive: arr("archive"),
|
|
392
392
|
lessons: arr("lessons"),
|
|
393
|
-
persona: typeof obj.persona === "string" ? obj.persona : null,
|
|
393
|
+
persona: typeof obj.persona === "string" && obj.persona.trim().toLowerCase() !== "null" ? obj.persona : null,
|
|
394
394
|
tuning: obj.tuning && typeof obj.tuning === "object" && obj.tuning.knob ? obj.tuning : null,
|
|
395
395
|
report: typeof obj.report === "string" ? obj.report.trim() : "",
|
|
396
396
|
};
|
|
@@ -774,7 +774,7 @@ function parseIntrospection(text) {
|
|
|
774
774
|
doc_edits: arr("doc_edits"),
|
|
775
775
|
entity_edits: arr("entity_edits"),
|
|
776
776
|
ideas: arr("ideas"),
|
|
777
|
-
persona: typeof obj.persona === "string" ? obj.persona : null,
|
|
777
|
+
persona: typeof obj.persona === "string" && obj.persona.trim().toLowerCase() !== "null" ? obj.persona : null,
|
|
778
778
|
report: typeof obj.report === "string" ? obj.report.trim() : "",
|
|
779
779
|
};
|
|
780
780
|
} catch (e) { return null; }
|
|
@@ -1018,7 +1018,14 @@ function negativeHebbianPhase(ev, { cap = 12 } = {}) {
|
|
|
1018
1018
|
return { lines, nodes, weakened, pruned };
|
|
1019
1019
|
}
|
|
1020
1020
|
|
|
1021
|
-
function dreamUtilityOptions(purpose, {
|
|
1021
|
+
function dreamUtilityOptions(purpose, {
|
|
1022
|
+
provider = null,
|
|
1023
|
+
timeoutMs,
|
|
1024
|
+
outputSchema,
|
|
1025
|
+
systemPrompt,
|
|
1026
|
+
cwd = process.cwd(),
|
|
1027
|
+
additionalReadOnlyDirectories = null,
|
|
1028
|
+
} = {}) {
|
|
1022
1029
|
return {
|
|
1023
1030
|
purpose,
|
|
1024
1031
|
provider,
|
|
@@ -1030,6 +1037,7 @@ function dreamUtilityOptions(purpose, { provider = null, timeoutMs, outputSchema
|
|
|
1030
1037
|
effort: DREAM_EFFORT,
|
|
1031
1038
|
timeoutMs,
|
|
1032
1039
|
cwd,
|
|
1040
|
+
additionalReadOnlyDirectories,
|
|
1033
1041
|
systemPrompt,
|
|
1034
1042
|
};
|
|
1035
1043
|
}
|
|
@@ -1202,6 +1210,7 @@ async function runDreamMutation({ trigger = "manual", provider = null, runUtilit
|
|
|
1202
1210
|
const introRun = await runUtility(buildIntrospectionPrompt(), dreamUtilityOptions("introspection", {
|
|
1203
1211
|
provider,
|
|
1204
1212
|
cwd: CODE_ROOT,
|
|
1213
|
+
additionalReadOnlyDirectories: [CONFIG_DIR],
|
|
1205
1214
|
timeoutMs: 12 * 60 * 1000,
|
|
1206
1215
|
outputSchema: INTROSPECTION_OUTPUT_SCHEMA,
|
|
1207
1216
|
systemPrompt: "You are Open Claudia's nightly self-introspection. You may READ files (your code, memory, soul) to understand yourself, but read-only mode forbids product side effects. After investigating, reply with ONLY the requested JSON object — no prose, no code fences.",
|
package/core/media.js
CHANGED
|
@@ -4,11 +4,15 @@
|
|
|
4
4
|
const fs = require("fs");
|
|
5
5
|
const path = require("path");
|
|
6
6
|
const { execSync } = require("child_process");
|
|
7
|
-
const { WHISPER_CLI, WHISPER_MODEL, FFMPEG, TEMP_DIR, ELEVENLABS_API_KEY, ELEVENLABS_VOICE_ID, ELEVENLABS_MODEL, SPEECH_SERVICE_URL, SPEECH_API_KEY, POCKET_TTS_VOICE, TTS_PROVIDER } = require("./config");
|
|
7
|
+
const { WHISPER_CLI, WHISPER_MODEL, FFMPEG, TEMP_DIR, ELEVENLABS_API_KEY, ELEVENLABS_VOICE_ID, ELEVENLABS_MODEL, SPEECH_SERVICE_URL, SPEECH_API_KEY, AGENTSPACE_POD_TOKEN, POCKET_TTS_VOICE, TTS_PROVIDER } = require("./config");
|
|
8
8
|
|
|
9
9
|
const TTS_CMD = process.platform === "darwin" ? "say" : null;
|
|
10
10
|
|
|
11
|
-
function
|
|
11
|
+
function speechBearerToken() {
|
|
12
|
+
return AGENTSPACE_POD_TOKEN || SPEECH_API_KEY || "";
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function transcribeAudioLocal(oggPath) {
|
|
12
16
|
if (!WHISPER_CLI || !FFMPEG) return null;
|
|
13
17
|
const wavPath = oggPath.replace(/\.[^.]+$/, ".wav");
|
|
14
18
|
execSync(`"${FFMPEG}" -i "${oggPath}" -ar 16000 -ac 1 -y "${wavPath}" 2>/dev/null`);
|
|
@@ -19,6 +23,30 @@ function transcribeAudio(oggPath) {
|
|
|
19
23
|
.join(" ").trim();
|
|
20
24
|
}
|
|
21
25
|
|
|
26
|
+
async function transcribeAudio(oggPath) {
|
|
27
|
+
const token = speechBearerToken();
|
|
28
|
+
if (SPEECH_SERVICE_URL && token) {
|
|
29
|
+
try {
|
|
30
|
+
const body = new FormData();
|
|
31
|
+
body.append("file", new Blob([fs.readFileSync(oggPath)]), path.basename(oggPath));
|
|
32
|
+
const res = await fetch(`${SPEECH_SERVICE_URL.replace(/\/$/, "")}/v1/audio/transcriptions`, {
|
|
33
|
+
method: "POST",
|
|
34
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
35
|
+
body,
|
|
36
|
+
signal: AbortSignal.timeout(120000),
|
|
37
|
+
});
|
|
38
|
+
if (res.ok) {
|
|
39
|
+
const payload = await res.json();
|
|
40
|
+
return String(payload.text || "").trim();
|
|
41
|
+
}
|
|
42
|
+
console.error(`Hosted STT failed: HTTP ${res.status}`);
|
|
43
|
+
} catch (e) {
|
|
44
|
+
console.error("Hosted STT error:", e.message);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return transcribeAudioLocal(oggPath);
|
|
48
|
+
}
|
|
49
|
+
|
|
22
50
|
function cleanForTTS(text) {
|
|
23
51
|
return text.replace(/[*_`#>\[\]()]/g, "").replace(/\n{2,}/g, ". ").replace(/\n/g, " ").trim();
|
|
24
52
|
}
|
|
@@ -73,12 +101,13 @@ async function elevenLabsToVoice(clean) {
|
|
|
73
101
|
// Pocket TTS through the Agent Space speech gateway. The gateway returns WAV;
|
|
74
102
|
// Telegram and voice transports receive a compact Ogg/Opus file.
|
|
75
103
|
async function pocketToVoice(clean) {
|
|
76
|
-
|
|
104
|
+
const token = speechBearerToken();
|
|
105
|
+
if (TTS_PROVIDER === "elevenlabs" || !token || !SPEECH_SERVICE_URL || !FFMPEG) return null;
|
|
77
106
|
let wavPath = null;
|
|
78
107
|
try {
|
|
79
108
|
const res = await fetch(`${SPEECH_SERVICE_URL.replace(/\/$/, "")}/v1/audio/speech`, {
|
|
80
109
|
method: "POST",
|
|
81
|
-
headers: { Authorization: `Bearer ${
|
|
110
|
+
headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json" },
|
|
82
111
|
body: JSON.stringify({ input: clean, voice: POCKET_TTS_VOICE, response_format: "wav" }),
|
|
83
112
|
signal: AbortSignal.timeout(60000),
|
|
84
113
|
});
|
|
@@ -165,4 +194,4 @@ async function synthSentenceMp3(text) {
|
|
|
165
194
|
}
|
|
166
195
|
}
|
|
167
196
|
|
|
168
|
-
module.exports = { transcribeAudio, textToVoice, splitSentences, synthSentenceMp3, pocketToVoice, TTS_CMD };
|
|
197
|
+
module.exports = { transcribeAudio, transcribeAudioLocal, textToVoice, splitSentences, synthSentenceMp3, pocketToVoice, speechBearerToken, TTS_CMD };
|
package/core/providers/claude.js
CHANGED
|
@@ -135,6 +135,20 @@ function appendToolLists(args, context) {
|
|
|
135
135
|
}
|
|
136
136
|
}
|
|
137
137
|
|
|
138
|
+
function appendReadOnlyDirectories(args, context) {
|
|
139
|
+
if (context.additionalReadOnlyDirectories === undefined || context.additionalReadOnlyDirectories === null) return;
|
|
140
|
+
if (!context.readOnly) {
|
|
141
|
+
throw providerError("INVALID_PROVIDER_CONTEXT", "additionalReadOnlyDirectories requires readOnly mode");
|
|
142
|
+
}
|
|
143
|
+
if (!Array.isArray(context.additionalReadOnlyDirectories)
|
|
144
|
+
|| context.additionalReadOnlyDirectories.some((dir) => typeof dir !== "string" || !dir.trim())) {
|
|
145
|
+
throw providerError("INVALID_PROVIDER_CONTEXT", "additionalReadOnlyDirectories must be an array of non-empty strings");
|
|
146
|
+
}
|
|
147
|
+
for (const dir of new Set(context.additionalReadOnlyDirectories.map((entry) => entry.trim()))) {
|
|
148
|
+
args.push("--add-dir", dir);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
138
152
|
function createClaudeProvider(options = {}) {
|
|
139
153
|
const resolveExecutable = options.resolveExecutable || (() => config.discoverProviderExecutable("claude"));
|
|
140
154
|
const defaultModel = options.defaultModel || CLAUDE_DEFAULT_MODEL;
|
|
@@ -195,6 +209,7 @@ function createClaudeProvider(options = {}) {
|
|
|
195
209
|
|| context.transcriptPaths?.directory
|
|
196
210
|
|| null;
|
|
197
211
|
if (transcriptDirectory) args.push("--add-dir", String(transcriptDirectory));
|
|
212
|
+
if (utility) appendReadOnlyDirectories(args, context);
|
|
198
213
|
|
|
199
214
|
if (!utility) appendSessionArgs(args, context);
|
|
200
215
|
appendModelAndLimits(args, context, defaultModel, utility);
|
package/core/router.js
CHANGED
|
@@ -193,7 +193,7 @@ async function handleVoice(envelope) {
|
|
|
193
193
|
const tStart = Date.now();
|
|
194
194
|
const oggPath = await envelope.adapter.downloadMedia(media);
|
|
195
195
|
const tAfterDownload = Date.now();
|
|
196
|
-
const transcript = transcribeAudio(oggPath);
|
|
196
|
+
const transcript = await transcribeAudio(oggPath);
|
|
197
197
|
const tAfterStt = Date.now();
|
|
198
198
|
try { fs.unlinkSync(oggPath); } catch (e) {}
|
|
199
199
|
if (!transcript) return send("Couldn't transcribe. Try typing it.");
|
|
@@ -222,7 +222,7 @@ async function handleAudio(envelope) {
|
|
|
222
222
|
});
|
|
223
223
|
envelope.adapter.typing(envelope.channelId).catch(() => {});
|
|
224
224
|
const p = await envelope.adapter.downloadMedia(media);
|
|
225
|
-
const t = transcribeAudio(p);
|
|
225
|
+
const t = await transcribeAudio(p);
|
|
226
226
|
try { fs.unlinkSync(p); } catch (e) {}
|
|
227
227
|
if (!t) return send("Couldn't transcribe.");
|
|
228
228
|
await send(`Heard: "${t}"`, { replyTo: envelope.messageId });
|
package/core/runner.js
CHANGED
|
@@ -1654,7 +1654,10 @@ async function deliverForegroundResult(result, runContext, state = currentState(
|
|
|
1654
1654
|
if (!sent.ok) throw new Error(`failed to deliver response chunk ${index + 1}`);
|
|
1655
1655
|
}
|
|
1656
1656
|
|
|
1657
|
-
|
|
1657
|
+
const { VOICE_REPLY_MODE } = require("./config");
|
|
1658
|
+
const { shouldReplyWithVoice } = require("./voice-policy");
|
|
1659
|
+
const shouldSpeak = shouldReplyWithVoice(VOICE_REPLY_MODE, runContext.lastInputWasVoice);
|
|
1660
|
+
if (result.ok && shouldSpeak) {
|
|
1658
1661
|
const { currentTransport } = require("./context");
|
|
1659
1662
|
if (currentTransport() === "voice") {
|
|
1660
1663
|
const sentences = splitSentences(finalText);
|
|
@@ -1668,6 +1671,9 @@ async function deliverForegroundResult(result, runContext, state = currentState(
|
|
|
1668
1671
|
if (voicePath) await sendVoice(voicePath);
|
|
1669
1672
|
}
|
|
1670
1673
|
await sendVoiceEnd();
|
|
1674
|
+
} else {
|
|
1675
|
+
const voicePath = await textToVoice(finalText);
|
|
1676
|
+
if (voicePath) await sendVoice(voicePath);
|
|
1671
1677
|
}
|
|
1672
1678
|
}
|
|
1673
1679
|
return { ok: true, chunks: chunks.length };
|
package/core/utility-agent.js
CHANGED
|
@@ -536,6 +536,9 @@ function createUtilityAgentService(options = {}) {
|
|
|
536
536
|
fresh: true,
|
|
537
537
|
readOnly: request.readOnly !== false,
|
|
538
538
|
cwd: request.cwd || process.cwd(),
|
|
539
|
+
...(request.additionalReadOnlyDirectories !== undefined
|
|
540
|
+
? { additionalReadOnlyDirectories: request.additionalReadOnlyDirectories }
|
|
541
|
+
: {}),
|
|
539
542
|
...(request.allowedTools !== undefined ? { allowedTools: request.allowedTools } : {}),
|
|
540
543
|
...(request.disallowedTools !== undefined ? { disallowedTools: request.disallowedTools } : {}),
|
|
541
544
|
...(request.outputSchema ? { outputSchema: request.outputSchema, outputSchemaPath: schemaResource.file } : {}),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@inetafrica/open-claudia",
|
|
3
|
-
"version": "3.1.
|
|
3
|
+
"version": "3.1.7",
|
|
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": {
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
"scripts": {
|
|
10
10
|
"setup": "node setup.js",
|
|
11
11
|
"start": "node bot.js",
|
|
12
|
-
"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-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",
|
|
12
|
+
"pretest": "node test-provider-fixture.js && node test-provider-core-prompt.js && node test-turn-observer.js && node test-provider-prompt-parity.js && node test-provider-env-isolation.js && node test-provider-resume-parity.js && node test-provider-bootstrap.js && node test-provider-registry.js && node test-provider-stream-decoder.js && node test-provider-events.js && node test-provider-claude.js && node test-provider-codex.js && node test-provider-runner.js && node test-provider-refusal-recovery.js && node test-provider-run-lifecycle.js && node test-provider-gateway.js && node test-provider-side-chat.js && node test-single-runtime.js && node test-provider-migration-backup.js && node test-provider-state-migration.js && node test-provider-session-history.js && node test-provider-session-commands.js && node test-provider-compaction.js && node test-provider-scheduler.js && node test-credential-capture.js && node test-cursor-removal.js && node test-utility-provider-policy.js && node test-provider-subagent.js && node test-provider-recall.js && node test-provider-pack-review.js && node test-memory-mutation-queue.js && node test-provider-dream.js && node test-provider-enforcer.js && node test-provider-tool-hooks.js && node test-provider-boot-matrix.js && node test-provider-capabilities.js && node test-provider-language.js && node test-provider-coupling-audit.js && node test-provider-parity-e2e.js && node test-kazee-channel-health.js && node test-kazee-media-caption.js && node test-prompt-budget.js && node test-agency-ledger.js && node test-agency-deliberate.js && node test-guardrail.js && node test-spaces-comment-content.js && node test-boot-health.js && node test-lexical-floor.js && node test-safe-mode.js && node test-logout.js",
|
|
13
13
|
"test": "OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node -e \"require('./vault'); console.log('OK')\" && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-runner-watchdog-static.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-usage-accounting.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-recall-engine.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-recall-graph.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-recall-discoverer.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-recall-query.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-walker-score.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-score-gate.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-cokept-warming.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-project-transcripts-smoke.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-abilities.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-ability-extraction.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-ability-couse.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-ability-transfer.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-ability-tiers.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-ability-merge-guard.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-learning-e2e.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-pack-nesting.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-pack-search.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-extractive-injection.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-journal-dedupe.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-read-signal.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-tools.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-tool-graph.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-tool-guard.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-tooling-mode.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-tool-manifest.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-recall-evolution.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-approval-async.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-unified-identity.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-queue-routing.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-persona-packs.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-speaker-persona.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-persona-pipeline.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-recall-relationship-gate.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-relationship.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-enforcer.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-identity-prune.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-kazee-message-id.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-delivery-contract.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-run-lock.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-web-sessions.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-telegram-poll-recovery.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-telegram-409-grace.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-telegram-conflict-heal-guard.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-single-instance.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-streaming-split.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-spaces-autoenable.js"
|
|
14
14
|
},
|
|
15
15
|
"files": [
|
package/test-provider-claude.js
CHANGED
|
@@ -161,15 +161,28 @@ const utility = validateInvocation(provider.buildUtilityInvocation(deepFreeze({
|
|
|
161
161
|
tier: "low",
|
|
162
162
|
maxTurns: 2,
|
|
163
163
|
toolHookSettings: "/fixture/utility-hook.json",
|
|
164
|
+
additionalReadOnlyDirectories: ["/fixture/memory", "/fixture/memory"],
|
|
164
165
|
})));
|
|
165
166
|
assert.ok(utility.args.includes("--no-session-persistence"));
|
|
166
167
|
assert.deepStrictEqual(utility.args.slice(utility.args.indexOf("--allowedTools"), utility.args.indexOf("--allowedTools") + 2), ["--allowedTools", "Read,Glob"]);
|
|
167
168
|
assert.deepStrictEqual(utility.args.slice(utility.args.indexOf("--permission-mode"), utility.args.indexOf("--permission-mode") + 2), ["--permission-mode", "plan"]);
|
|
169
|
+
assert.strictEqual(utility.args.filter((arg) => arg === "/fixture/memory").length, 1, "read-only utility directories are deduplicated");
|
|
170
|
+
assert.deepStrictEqual(utility.args.slice(utility.args.indexOf("/fixture/memory") - 1, utility.args.indexOf("/fixture/memory") + 1), ["--add-dir", "/fixture/memory"]);
|
|
168
171
|
assert.strictEqual(utility.args[utility.args.indexOf("--model") + 1], "claude-haiku-4-5-20251001");
|
|
169
172
|
assert.strictEqual(utility.args[utility.args.indexOf("--json-schema") + 1], JSON.stringify({ type: "object", required: ["ok"] }));
|
|
170
173
|
assert.strictEqual(utility.stdin, "UTILITY PROMPT", "utility prompt uses protected stdin transport");
|
|
171
174
|
assert.ok(!utility.args.includes("UTILITY PROMPT"), "utility prompt is absent from argv");
|
|
172
175
|
|
|
176
|
+
assert.throws(
|
|
177
|
+
() => provider.buildUtilityInvocation(simpleContext({ additionalReadOnlyDirectories: ["/fixture/memory"] })),
|
|
178
|
+
(error) => error && error.code === "INVALID_PROVIDER_CONTEXT",
|
|
179
|
+
"extra readable directories cannot expand a writable utility invocation",
|
|
180
|
+
);
|
|
181
|
+
assert.throws(
|
|
182
|
+
() => provider.buildUtilityInvocation(simpleContext({ readOnly: true, additionalReadOnlyDirectories: [""] })),
|
|
183
|
+
(error) => error && error.code === "INVALID_PROVIDER_CONTEXT",
|
|
184
|
+
);
|
|
185
|
+
|
|
173
186
|
const decoder = provider.createEventDecoder();
|
|
174
187
|
const raw = decoder.push(Buffer.from('{"type":"system","subtype":"init","session_id":"bound-session"}\n'));
|
|
175
188
|
assert.deepStrictEqual(provider.normalizeEvent(raw[0], richInvocation.parserState), [{ type: "session", sessionId: "bound-session" }]);
|
package/test-provider-dream.js
CHANGED
|
@@ -148,6 +148,8 @@ async function providerProbe(kind) {
|
|
|
148
148
|
readOnly: options.readOnly,
|
|
149
149
|
hasSchema: !!options.outputSchema,
|
|
150
150
|
effort: options.effort,
|
|
151
|
+
cwd: options.cwd,
|
|
152
|
+
additionalReadOnlyDirectories: options.additionalReadOnlyDirectories,
|
|
151
153
|
promptBytes: Buffer.byteLength(prompt),
|
|
152
154
|
});
|
|
153
155
|
return utility.spawnUtilityAgent(prompt, options);
|
|
@@ -199,6 +201,11 @@ async function providerProbe(kind) {
|
|
|
199
201
|
assert.strictEqual(call.effort, "max");
|
|
200
202
|
assert.ok(call.promptBytes > 100);
|
|
201
203
|
}
|
|
204
|
+
assert.strictEqual(calls[0].additionalReadOnlyDirectories, null, "consolidation does not gain memory filesystem access");
|
|
205
|
+
assert.strictEqual(calls[1].additionalReadOnlyDirectories, null, "applied consolidation does not gain memory filesystem access");
|
|
206
|
+
assert.deepStrictEqual(calls[2].additionalReadOnlyDirectories, [process.env.OPEN_CLAUDIA_CONFIG_DIR],
|
|
207
|
+
"introspection explicitly receives the memory config directory");
|
|
208
|
+
assert.strictEqual(calls[2].cwd, path.resolve(__dirname), "introspection remains rooted in the source tree");
|
|
202
209
|
|
|
203
210
|
const project = packs.readPack("project");
|
|
204
211
|
const entity = entities.findEntity("Parity Harness");
|
|
@@ -241,6 +248,12 @@ async function providerProbe(kind) {
|
|
|
241
248
|
if (kind === "claude") {
|
|
242
249
|
assert.ok(record.argv.includes("--json-schema"));
|
|
243
250
|
assert.ok(record.argv.includes("max"), "Claude retains maximum dream effort");
|
|
251
|
+
if (purpose === "introspection") {
|
|
252
|
+
const memoryIndex = record.argv.indexOf(process.env.OPEN_CLAUDIA_CONFIG_DIR);
|
|
253
|
+
assert.ok(memoryIndex > 0, "Claude introspection can read memory outside its cwd");
|
|
254
|
+
assert.strictEqual(record.argv[memoryIndex - 1], "--add-dir");
|
|
255
|
+
assert.ok(record.argv.includes("plan"), "extra memory access does not expand the read-only boundary");
|
|
256
|
+
}
|
|
244
257
|
} else {
|
|
245
258
|
assert.ok(record.argv.includes("--output-schema"));
|
|
246
259
|
assert.ok(record.argv.some((arg) => /model_reasoning_effort=.*max/.test(arg)), "max effort passes through natively since codex 0.144 supports it");
|
|
@@ -269,6 +282,9 @@ async function providerProbe(kind) {
|
|
|
269
282
|
assert.match(failed.report, /skipped the AI-led merges/i);
|
|
270
283
|
assert.strictEqual(packs.readPack("family-child").parent, "family", "deterministic filing survives provider failure");
|
|
271
284
|
|
|
285
|
+
assert.strictEqual(dream.parseDream('{"persona":"null"}').persona, null, "stringified JSON null is a persona no-op");
|
|
286
|
+
assert.strictEqual(dream.parseIntrospection('{"persona":" null "}').persona, null, "introspection stringified null is a persona no-op");
|
|
287
|
+
|
|
272
288
|
return {
|
|
273
289
|
provider: kind,
|
|
274
290
|
mutations: {
|
|
@@ -87,7 +87,7 @@ async function providerProbe(kind) {
|
|
|
87
87
|
const utility = createUtilityAgentService({
|
|
88
88
|
registry: registry([entry]),
|
|
89
89
|
env: { ENFORCER_PROVIDER: kind, [`ENFORCER_MODEL_${kind.toUpperCase()}`]: model },
|
|
90
|
-
killGraceMs:
|
|
90
|
+
killGraceMs: 300,
|
|
91
91
|
});
|
|
92
92
|
const enforcer = require("./core/enforcer");
|
|
93
93
|
assert.strictEqual(typeof enforcer.createEnforcerService, "function", "enforcer exposes an injectable utility-provider service");
|
|
@@ -106,7 +106,9 @@ async function providerProbe(kind) {
|
|
|
106
106
|
const timeoutService = enforcer.createEnforcerService({
|
|
107
107
|
spawnUtilityAgent: utility.spawnUtilityAgent,
|
|
108
108
|
audit,
|
|
109
|
-
|
|
109
|
+
// 2500ms so the hung child boots on a loaded CI runner (writing capture #5
|
|
110
|
+
// and arming its SIGTERM recorder) before the deadline fires.
|
|
111
|
+
timeoutMs: 2500,
|
|
110
112
|
});
|
|
111
113
|
const timedOut = await timeoutService.vetReply(speaker, "timeout verdict fixture");
|
|
112
114
|
|
|
@@ -157,10 +157,29 @@ assert.match(pkg.description, /provider-agnostic coding-agent harness/i);
|
|
|
157
157
|
// never re-offers a sign-in); a 🔁 Switch account button on connected
|
|
158
158
|
// providers in /login behind an explicit confirm that names the signed-in
|
|
159
159
|
// account where the CLI exposes it, then chains logout → fresh login.
|
|
160
|
-
// 3.1.
|
|
160
|
+
// 3.1.4 approved by Sumeet 2026-07-21 for the scoped Kazee inbound image-caption fix,
|
|
161
|
+
// including commit, push, release and deployment.
|
|
161
162
|
// as the preferred voice-note provider, with ElevenLabs fallback and local
|
|
162
163
|
// Whisper transcription unchanged.
|
|
163
|
-
|
|
164
|
+
// 3.1.5 approved by Sumeet 2026-07-21 ("Yeh" to "Want me to cut 3.1.5 and get
|
|
165
|
+
// going?"): ships pod-identity hosted speech (bearer = AGENTSPACE_POD_TOKEN
|
|
166
|
+
// with SPEECH_API_KEY fallback) so provisioned bot pods can use the Agent
|
|
167
|
+
// Space speech gateway without a shared API key, plus the dream introspection
|
|
168
|
+
// memory-read fix. Release unblocks the fleet pod-voice rollout (canary
|
|
169
|
+
// smart-brenda first).
|
|
170
|
+
// 3.1.6 approved by Sumeet 2026-07-21 ("Go"): identical content to 3.1.5
|
|
171
|
+
// (which CI never published — its build died twice on a 120ms node-boot race
|
|
172
|
+
// in test-provider-run-lifecycle's forced-termination case on the loaded
|
|
173
|
+
// shared runner) plus the test-timing fix (2000ms/500ms). Never retag a
|
|
174
|
+
// failed version — re-cut as +1.
|
|
175
|
+
// 3.1.7 approved by Sumeet 2026-07-21: still the v3.1.5 content, third cut.
|
|
176
|
+
// The 3.1.6 build died on the NEXT tightest spawn race (pipeline 10383:
|
|
177
|
+
// test-utility-provider-policy's drain case flat-slept 100ms then cancelled,
|
|
178
|
+
// killing the fake agent pre-boot so drain-signal.json never appeared; the
|
|
179
|
+
// v3.1.6 run-lifecycle fix itself passed there). Audited all 13 fake-agent
|
|
180
|
+
// test files and hardened the three racy ones (utility-policy, enforcer,
|
|
181
|
+
// subagent): wait on boot captures / generous deadlines, never tight sleeps.
|
|
182
|
+
assert.strictEqual(pkg.version, "3.1.7", "release version must remain unchanged without explicit approval");
|
|
164
183
|
for (const keyword of ["claude", "codex", "coding-agent", "provider-agnostic"]) {
|
|
165
184
|
assert.ok(pkg.keywords.includes(keyword), `package keyword missing: ${keyword}`);
|
|
166
185
|
}
|
|
@@ -342,8 +342,10 @@ async function main() {
|
|
|
342
342
|
parserState: createClaudeParserState(),
|
|
343
343
|
},
|
|
344
344
|
cwd: processTreeRoot,
|
|
345
|
-
|
|
346
|
-
|
|
345
|
+
// 120ms lost the node-boot race on loaded CI runners: the parent was
|
|
346
|
+
// SIGTERMed before spawnDescendantFixture ever ran (pipeline 10381, 2x).
|
|
347
|
+
timeoutMs: 2000,
|
|
348
|
+
killGraceMs: 500,
|
|
347
349
|
cleanup: async () => {},
|
|
348
350
|
});
|
|
349
351
|
const treeResult = await realTreeRun;
|
|
@@ -25,7 +25,8 @@ const { HELP, parseAgentArgs } = require("./bin/agent");
|
|
|
25
25
|
|
|
26
26
|
function wait(ms) { return new Promise((resolve) => setTimeout(resolve, ms)); }
|
|
27
27
|
|
|
28
|
-
|
|
28
|
+
// Loaded CI runners stretch node cold-boot far past local timings (pipelines 10381/10383).
|
|
29
|
+
async function waitForFile(file, timeoutMs = 15000) {
|
|
29
30
|
const started = Date.now();
|
|
30
31
|
while (!fs.existsSync(file)) {
|
|
31
32
|
if (Date.now() - started > timeoutMs) throw new Error(`timed out waiting for ${path.basename(file)}`);
|
|
@@ -30,7 +30,9 @@ function wait(ms) {
|
|
|
30
30
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
-
|
|
33
|
+
// Loaded CI runners stretch node cold-boot far past local timings (pipelines
|
|
34
|
+
// 10381/10383 died on 100-370ms budgets), so capture waits get generous deadlines.
|
|
35
|
+
async function waitForFile(file, timeoutMs = 15000) {
|
|
34
36
|
const started = Date.now();
|
|
35
37
|
while (!fs.existsSync(file)) {
|
|
36
38
|
if (Date.now() - started > timeoutMs) throw new Error(`timed out waiting for ${path.basename(file)}`);
|
|
@@ -38,7 +40,7 @@ async function waitForFile(file, timeoutMs = 2000) {
|
|
|
38
40
|
}
|
|
39
41
|
}
|
|
40
42
|
|
|
41
|
-
async function waitForPidExit(pid, timeoutMs =
|
|
43
|
+
async function waitForPidExit(pid, timeoutMs = 15000) {
|
|
42
44
|
const started = Date.now();
|
|
43
45
|
while (Date.now() - started <= timeoutMs) {
|
|
44
46
|
try { process.kill(pid, 0); }
|
|
@@ -414,12 +416,14 @@ async function main() {
|
|
|
414
416
|
},
|
|
415
417
|
})),
|
|
416
418
|
env: { UTILITY_PROVIDER: "claude" },
|
|
417
|
-
killGraceMs:
|
|
419
|
+
killGraceMs: 400,
|
|
418
420
|
});
|
|
419
421
|
const timeoutStarted = Date.now();
|
|
420
422
|
await assert.rejects(
|
|
421
423
|
timeoutService.spawnUtilityAgent("timeout", {
|
|
422
|
-
|
|
424
|
+
// 2500ms so the child boots (arming its SIGTERM recorder and spawning
|
|
425
|
+
// its descendant) on a loaded runner before the deadline fires.
|
|
426
|
+
purpose: "subagent", systemPrompt: "timeout", timeoutMs: 2500, cwd: root,
|
|
423
427
|
}),
|
|
424
428
|
(error) => {
|
|
425
429
|
assert.ok(error instanceof UtilityAgentError);
|
|
@@ -429,7 +433,7 @@ async function main() {
|
|
|
429
433
|
return true;
|
|
430
434
|
},
|
|
431
435
|
);
|
|
432
|
-
assert.ok(Date.now() - timeoutStarted >=
|
|
436
|
+
assert.ok(Date.now() - timeoutStarted >= 2500, "timeout settles only after its deadline and process close");
|
|
433
437
|
await waitForFile(signalCapture);
|
|
434
438
|
assert.strictEqual(JSON.parse(fs.readFileSync(signalCapture, "utf8")).signal, "SIGTERM");
|
|
435
439
|
await waitForFile(descendantCapture);
|
|
@@ -437,9 +441,14 @@ async function main() {
|
|
|
437
441
|
assert.strictEqual(timeoutService.stats().activeGlobal, 0, "timeout cleanup releases concurrency only after close");
|
|
438
442
|
|
|
439
443
|
const abortCapture = path.join(root, "abort-signal.json");
|
|
444
|
+
const abortBootCapture = path.join(root, "abort-boot.json");
|
|
440
445
|
const abortService = createUtilityAgentService({
|
|
441
446
|
registry: registry(providers({
|
|
442
|
-
codex: {
|
|
447
|
+
codex: {
|
|
448
|
+
FAKE_AGENT_SCENARIO: "forced-termination",
|
|
449
|
+
FAKE_AGENT_SIGNAL_CAPTURE: abortCapture,
|
|
450
|
+
FAKE_AGENT_CAPTURE: abortBootCapture,
|
|
451
|
+
},
|
|
443
452
|
})),
|
|
444
453
|
env: { UTILITY_PROVIDER: "codex" },
|
|
445
454
|
});
|
|
@@ -447,25 +456,34 @@ async function main() {
|
|
|
447
456
|
const aborted = abortService.spawnUtilityAgent("abort", {
|
|
448
457
|
purpose: "recall", systemPrompt: "abort", signal: controller.signal, cwd: root,
|
|
449
458
|
});
|
|
450
|
-
|
|
459
|
+
// Abort only after the child demonstrably booted (boot capture exists) so
|
|
460
|
+
// the TERM lands on an armed signal handler, not a cold spawn.
|
|
461
|
+
await waitForFile(abortBootCapture);
|
|
451
462
|
controller.abort();
|
|
452
463
|
await assertRejectsCode(aborted, "UTILITY_CANCELLED");
|
|
453
464
|
await waitForFile(abortCapture);
|
|
454
465
|
|
|
455
466
|
const drainCapture = path.join(root, "drain-signal.json");
|
|
467
|
+
const drainBootCapture = path.join(root, "drain-boot.json");
|
|
456
468
|
const drainService = createUtilityAgentService({
|
|
457
469
|
registry: registry(providers({
|
|
458
|
-
claude: {
|
|
470
|
+
claude: {
|
|
471
|
+
FAKE_AGENT_SCENARIO: "forced-termination",
|
|
472
|
+
FAKE_AGENT_SIGNAL_CAPTURE: drainCapture,
|
|
473
|
+
FAKE_AGENT_CAPTURE: drainBootCapture,
|
|
474
|
+
},
|
|
459
475
|
})),
|
|
460
476
|
env: { UTILITY_PROVIDER: "claude" },
|
|
461
|
-
killGraceMs:
|
|
477
|
+
killGraceMs: 400,
|
|
462
478
|
});
|
|
463
479
|
const drainingRun = drainService.spawnUtilityAgent("shutdown drain", {
|
|
464
480
|
purpose: "review", systemPrompt: "drain", cwd: root,
|
|
465
481
|
});
|
|
466
482
|
const drainingAssertion = assertRejectsCode(drainingRun, "UTILITY_CANCELLED");
|
|
467
|
-
|
|
468
|
-
|
|
483
|
+
// Cancel only after the child booted: pipeline 10383 showed a flat 100ms
|
|
484
|
+
// sleep kills it pre-boot, so its TERM handler never writes drain-signal.json.
|
|
485
|
+
await waitForFile(drainBootCapture);
|
|
486
|
+
const drained = await drainService.cancelAll({ timeoutMs: 5000 });
|
|
469
487
|
await drainingAssertion;
|
|
470
488
|
assert.deepStrictEqual(drained, { drained: true, active: 0 }, "bounded shutdown cancellation waits for utility child cleanup");
|
|
471
489
|
await waitForFile(drainCapture);
|