@inetafrica/open-claudia 3.0.7 → 3.0.9
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 +6 -0
- package/Dockerfile +5 -26
- package/core/providers/codex.js +4 -2
- package/core/providers/events.js +9 -4
- package/core/runner.js +2 -0
- package/package.json +1 -1
- package/test-provider-codex.js +2 -2
- package/test-provider-dream.js +1 -1
- package/test-provider-events.js +4 -3
- package/test-provider-language.js +5 -6
- package/test-provider-run-lifecycle.js +23 -2
- package/test-provider-stream-decoder.js +7 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## v3.0.9 — one fat record no longer kills the turn
|
|
4
|
+
|
|
5
|
+
- **Decoder failures are warnings, not run failures.** The v3 JSONL stream decoder capped provider records at 1MB and surfaced any oversized or malformed line as a terminal `error` event — so a single base64-image tool result (routine in image workflows) killed an otherwise healthy live turn with `Claude Code run failed: Provider JSONL record exceeded 1048576 bytes` (rahil, v3.0.8, 2026-07-12). `LINE_TOO_LARGE` / `MALFORMED_JSON` now normalize to a new non-terminal `warning` event: the record is skipped, the stream continues, and the warning is logged. If the dropped line happened to be the terminal record, the existing `PROVIDER_MISSING_TERMINAL` net still fails the run cleanly.
|
|
6
|
+
- **Line cap raised 1MB → 16MB.** Image-bearing records are legitimate; the cap now exists to bound memory, not to police normal payloads.
|
|
7
|
+
- **Agent-space / OpenClaw:** no new deps, no new env, no schema change; Docker image builds identically (FROM the pre-baked `:base`). Coverage: warning-shape assertions in `test-provider-events.js`, default-cap pin in `test-provider-stream-decoder.js`.
|
|
8
|
+
|
|
3
9
|
## v3.0.6 — cache-bust burn is now a one-line query
|
|
4
10
|
|
|
5
11
|
- **Usage records carry a `coldStart` flag.** The restart→cache-bust burn (each bot restart re-bills the whole accumulated window as cache *writes* instead of ~13×-cheaper cache *reads*) was invisible to every token-count analysis — same token counts, different price class — and took a forensic reconstruction of the 22-day usage history to quantify (~8% of all spend, ~$12/day at its worst). Now the first usage record a process writes for each session is tagged `coldStart: true`: a cold start mid-conversation is the restart fingerprint, so the burn is one jq filter away instead of a modelling exercise. Records already carried `cacheReadTokens`/`cacheCreationTokens`; this adds the missing "was the cache necessarily cold?" dimension.
|
package/Dockerfile
CHANGED
|
@@ -1,21 +1,8 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
#
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
curl \
|
|
7
|
-
ffmpeg \
|
|
8
|
-
ca-certificates \
|
|
9
|
-
git \
|
|
10
|
-
jq \
|
|
11
|
-
python3 \
|
|
12
|
-
python3-pip \
|
|
13
|
-
build-essential \
|
|
14
|
-
sudo \
|
|
15
|
-
openssh-client \
|
|
16
|
-
rsync \
|
|
17
|
-
chromium \
|
|
18
|
-
&& rm -rf /var/lib/apt/lists/*
|
|
1
|
+
# Release image. The slow system layer (apt + chromium, ~250 packages) lives
|
|
2
|
+
# in Dockerfile.base, published as the `:base` tag — rebuilt only when that
|
|
3
|
+
# file changes. This build is minutes: fresh agent CLIs + app source.
|
|
4
|
+
ARG BASE_IMAGE=git.coders.africa:5050/kazee/agent-space/open-claudia:base
|
|
5
|
+
FROM ${BASE_IMAGE}
|
|
19
6
|
|
|
20
7
|
# Install Claude Code CLI
|
|
21
8
|
RUN curl -fsSL https://claude.ai/install.sh | sh || \
|
|
@@ -31,14 +18,6 @@ RUN npm install -g @openai/codex
|
|
|
31
18
|
RUN npm install -g agent-browser
|
|
32
19
|
ENV AGENT_BROWSER_EXECUTABLE_PATH=/usr/bin/chromium
|
|
33
20
|
|
|
34
|
-
# Create non-root user (Claude Code refuses --dangerously-skip-permissions as root)
|
|
35
|
-
# node:20-slim already has uid/gid 1000 (node user). Create claudia with different IDs.
|
|
36
|
-
RUN groupadd -g 1001 claudia && useradd -u 1001 -g 1001 -m -d /data claudia
|
|
37
|
-
|
|
38
|
-
# Allow claudia to install packages at runtime without a password
|
|
39
|
-
RUN echo "claudia ALL=(ALL) NOPASSWD: /usr/bin/apt-get, /usr/bin/apt" > /etc/sudoers.d/claudia-apt && \
|
|
40
|
-
chmod 0440 /etc/sudoers.d/claudia-apt
|
|
41
|
-
|
|
42
21
|
# Create app directory
|
|
43
22
|
WORKDIR /app
|
|
44
23
|
|
package/core/providers/codex.js
CHANGED
|
@@ -6,8 +6,10 @@ const { createJsonlDecoder } = require("./events");
|
|
|
6
6
|
const { createCodexParserState, normalizeCodexEvent } = require("./codex-events");
|
|
7
7
|
const { createCodexHookTransport } = require("./codex-hook");
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
const
|
|
9
|
+
// gpt-5.6-sol-pro is rejected on ChatGPT-plan auth (400 from the API); it needs API-key billing.
|
|
10
|
+
const CODEX_MODELS = Object.freeze(["gpt-5.6-sol", "gpt-5.6-sol-pro", "gpt-5.6-terra", "gpt-5.6-luna"]);
|
|
11
|
+
// max + ultra verified live against codex 0.144 (ultra = proactive multi-agent behavior).
|
|
12
|
+
const CODEX_EFFORT_VALUES = Object.freeze(["minimal", "low", "medium", "high", "xhigh", "max", "ultra"]);
|
|
11
13
|
const REQUIRED_EXEC_HELP_FLAGS = Object.freeze(["--config", "--json", "--sandbox", "--image", "--model", "--skip-git-repo-check"]);
|
|
12
14
|
const REQUIRED_RESUME_HELP_FLAGS = Object.freeze(["--config", "--json", "--image", "--model", "--skip-git-repo-check"]);
|
|
13
15
|
|
package/core/providers/events.js
CHANGED
|
@@ -10,9 +10,12 @@ const NORMALIZED_EVENT_TYPES = Object.freeze([
|
|
|
10
10
|
"usage",
|
|
11
11
|
"result",
|
|
12
12
|
"error",
|
|
13
|
+
"warning",
|
|
13
14
|
]);
|
|
14
15
|
|
|
15
|
-
|
|
16
|
+
// 16MB: tool results carrying base64 images legitimately exceed 1MB — the old
|
|
17
|
+
// cap killed rahil's image-workflow turn (v3.0.8) on a single oversized record.
|
|
18
|
+
const DEFAULT_MAX_LINE_BYTES = 16 * 1024 * 1024;
|
|
16
19
|
|
|
17
20
|
function decoderError(code, message, details = {}) {
|
|
18
21
|
return { type: "decoder.error", code, message, ...details };
|
|
@@ -133,11 +136,13 @@ function providerErrorFlags(message) {
|
|
|
133
136
|
|
|
134
137
|
function normalizeDecoderError(rawEvent) {
|
|
135
138
|
if (!rawEvent || rawEvent.type !== "decoder.error") return null;
|
|
139
|
+
// Warning, not error: a single oversized or malformed record is skippable.
|
|
140
|
+
// If the dropped line was the terminal record, PROVIDER_MISSING_TERMINAL
|
|
141
|
+
// still fails the run cleanly.
|
|
136
142
|
return {
|
|
137
|
-
type: "
|
|
143
|
+
type: "warning",
|
|
144
|
+
code: rawEvent.code || "DECODER_ERROR",
|
|
138
145
|
message: rawEvent.message || "Provider stream decoding failed",
|
|
139
|
-
authError: false,
|
|
140
|
-
usageLimit: false,
|
|
141
146
|
};
|
|
142
147
|
}
|
|
143
148
|
|
package/core/runner.js
CHANGED
|
@@ -361,6 +361,8 @@ async function executeProviderInvocation(options = {}) {
|
|
|
361
361
|
} else if (event.type === "error") {
|
|
362
362
|
terminalSeen = true;
|
|
363
363
|
rawErrors.push(event);
|
|
364
|
+
} else if (event.type === "warning") {
|
|
365
|
+
console.error("provider stream warning:", redactSensitive(String(event.message || event.code || "unknown")));
|
|
364
366
|
}
|
|
365
367
|
if (onEvent) {
|
|
366
368
|
eventChain = eventChain.then(() => onEvent(event)).catch((error) => {
|
package/package.json
CHANGED
package/test-provider-codex.js
CHANGED
|
@@ -46,7 +46,7 @@ assert.strictEqual(provider.label, "OpenAI Codex");
|
|
|
46
46
|
assert.strictEqual(provider.isAvailable(), true);
|
|
47
47
|
assert.strictEqual(provider.executable(), "/fixture/codex");
|
|
48
48
|
assert.strictEqual(provider.defaultModel(), "gpt-5-codex");
|
|
49
|
-
assert.deepStrictEqual(provider.modelChoices(), ["gpt-5.6-sol", "gpt-5.6-terra", "gpt-5.6-luna"]);
|
|
49
|
+
assert.deepStrictEqual(provider.modelChoices(), ["gpt-5.6-sol", "gpt-5.6-sol-pro", "gpt-5.6-terra", "gpt-5.6-luna"]);
|
|
50
50
|
assert.ok(!provider.modelChoices().includes("o4-mini"));
|
|
51
51
|
assert.deepStrictEqual(provider.compatibilityStatus(), {
|
|
52
52
|
state: "compatible",
|
|
@@ -164,7 +164,7 @@ assert.throws(
|
|
|
164
164
|
(error) => error && error.code === "UNSUPPORTED_PROVIDER_CAPABILITY",
|
|
165
165
|
);
|
|
166
166
|
assert.throws(
|
|
167
|
-
() => provider.buildMainInvocation(simpleContext({ providerSettings: { effort: "
|
|
167
|
+
() => provider.buildMainInvocation(simpleContext({ providerSettings: { effort: "bogus", budget: null, worktree: false } })),
|
|
168
168
|
(error) => error && error.code === "UNSUPPORTED_PROVIDER_SETTING",
|
|
169
169
|
);
|
|
170
170
|
assert.throws(
|
package/test-provider-dream.js
CHANGED
|
@@ -243,7 +243,7 @@ async function providerProbe(kind) {
|
|
|
243
243
|
assert.ok(record.argv.includes("max"), "Claude retains maximum dream effort");
|
|
244
244
|
} else {
|
|
245
245
|
assert.ok(record.argv.includes("--output-schema"));
|
|
246
|
-
assert.ok(record.argv.some((arg) => /model_reasoning_effort=.*
|
|
246
|
+
assert.ok(record.argv.some((arg) => /model_reasoning_effort=.*max/.test(arg)), "max effort passes through natively since codex 0.144 supports it");
|
|
247
247
|
}
|
|
248
248
|
}
|
|
249
249
|
|
package/test-provider-events.js
CHANGED
|
@@ -24,6 +24,7 @@ assert.deepStrictEqual([...NORMALIZED_EVENT_TYPES].sort(), [
|
|
|
24
24
|
"tool_end",
|
|
25
25
|
"tool_start",
|
|
26
26
|
"usage",
|
|
27
|
+
"warning",
|
|
27
28
|
].sort());
|
|
28
29
|
|
|
29
30
|
const claudeState = createClaudeParserState();
|
|
@@ -154,10 +155,10 @@ assert.deepStrictEqual(codexAuth, [{
|
|
|
154
155
|
|
|
155
156
|
const decoderError = { type: "decoder.error", code: "MALFORMED_JSON", message: "Malformed provider JSONL record" };
|
|
156
157
|
assert.deepStrictEqual(normalizeClaudeEvent(decoderError, createClaudeParserState()), [{
|
|
157
|
-
type: "
|
|
158
|
-
}]);
|
|
158
|
+
type: "warning", code: "MALFORMED_JSON", message: "Malformed provider JSONL record",
|
|
159
|
+
}], "decoder failures must be non-fatal warnings, not run-killing errors");
|
|
159
160
|
assert.deepStrictEqual(normalizeCodexEvent(decoderError, createCodexParserState()), [{
|
|
160
|
-
type: "
|
|
161
|
+
type: "warning", code: "MALFORMED_JSON", message: "Malformed provider JSONL record",
|
|
161
162
|
}]);
|
|
162
163
|
|
|
163
164
|
assert.deepStrictEqual(normalizeClaudeEvent({ type: "future.claude.event" }, createClaudeParserState()), []);
|
|
@@ -32,12 +32,11 @@ assert.doesNotMatch(setup, /Your Claude Code bot is connected|Description=Claude
|
|
|
32
32
|
|
|
33
33
|
const pkg = JSON.parse(read("package.json"));
|
|
34
34
|
assert.match(pkg.description, /provider-agnostic coding-agent harness/i);
|
|
35
|
-
// 3.0.
|
|
36
|
-
//
|
|
37
|
-
//
|
|
38
|
-
//
|
|
39
|
-
|
|
40
|
-
assert.strictEqual(pkg.version, "3.0.7", "release version must remain unchanged without explicit approval");
|
|
35
|
+
// 3.0.9 approved by Sumeet 2026-07-12 ("ok then please fix that"): decoder
|
|
36
|
+
// LINE_TOO_LARGE/MALFORMED_JSON downgraded to non-fatal warnings and the JSONL
|
|
37
|
+
// line cap raised 1MB→16MB after a base64-image tool result killed rahil's
|
|
38
|
+
// live turn on v3.0.8.
|
|
39
|
+
assert.strictEqual(pkg.version, "3.0.9", "release version must remain unchanged without explicit approval");
|
|
41
40
|
for (const keyword of ["claude", "codex", "coding-agent", "provider-agnostic"]) {
|
|
42
41
|
assert.ok(pkg.keywords.includes(keyword), `package keyword missing: ${keyword}`);
|
|
43
42
|
}
|
|
@@ -232,8 +232,29 @@ async function main() {
|
|
|
232
232
|
proc.stdout.end();
|
|
233
233
|
proc.emit("close", 0, null);
|
|
234
234
|
const malformed = await malformedRun;
|
|
235
|
-
|
|
236
|
-
|
|
235
|
+
// A single bad record is skipped (warning), not fatal: a >1MB base64-image
|
|
236
|
+
// tool result killed a healthy live turn on v3.0.8.
|
|
237
|
+
assert.strictEqual(malformed.ok, true, "one undecodable record must not fail a run that reached its terminal event");
|
|
238
|
+
assert.strictEqual(malformed.text, "fixture");
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
{
|
|
242
|
+
// But if the stream NEVER recovers a terminal event, the run still fails.
|
|
243
|
+
const proc = fakeChild();
|
|
244
|
+
const lostTerminalRun = executeProviderInvocation({
|
|
245
|
+
runContext: context(),
|
|
246
|
+
provider: provider(),
|
|
247
|
+
invocation: invocation(),
|
|
248
|
+
cwd: "/fixture/project",
|
|
249
|
+
spawnProcess: () => proc,
|
|
250
|
+
cleanup: async () => {},
|
|
251
|
+
});
|
|
252
|
+
proc.stdout.write("{not-json}\n");
|
|
253
|
+
proc.stdout.end();
|
|
254
|
+
proc.emit("close", 0, null);
|
|
255
|
+
const lostTerminal = await lostTerminalRun;
|
|
256
|
+
assert.strictEqual(lostTerminal.ok, false);
|
|
257
|
+
assert.match(lostTerminal.error.message, /terminal/i);
|
|
237
258
|
}
|
|
238
259
|
|
|
239
260
|
{
|
|
@@ -3,7 +3,13 @@
|
|
|
3
3
|
"use strict";
|
|
4
4
|
|
|
5
5
|
const assert = require("assert");
|
|
6
|
-
const { createJsonlDecoder } = require("./core/providers/events");
|
|
6
|
+
const { createJsonlDecoder, DEFAULT_MAX_LINE_BYTES } = require("./core/providers/events");
|
|
7
|
+
|
|
8
|
+
assert.strictEqual(
|
|
9
|
+
DEFAULT_MAX_LINE_BYTES,
|
|
10
|
+
16 * 1024 * 1024,
|
|
11
|
+
"default line cap must fit base64-image tool results (1MB cap killed live image turns in v3.0.8)",
|
|
12
|
+
);
|
|
7
13
|
|
|
8
14
|
const unicodeEvent = { type: "fixture", text: "café 👋" };
|
|
9
15
|
const unicodeLine = Buffer.from(`${JSON.stringify(unicodeEvent)}\n`, "utf8");
|