@inetafrica/open-claudia 3.0.3 → 3.0.5
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/channels/telegram/adapter.js +9 -1
- package/core/jobs.js +78 -7
- package/core/migration-backup.js +12 -7
- package/package.json +2 -2
- package/test-provider-dream.js +3 -0
- package/test-provider-language.js +5 -4
- package/test-provider-migration-backup.js +49 -0
- package/test-provider-scheduler.js +85 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## v3.0.5 — the conflict handler owns the poll
|
|
4
|
+
|
|
5
|
+
- **Hiccup heals no longer fight the 409 backoff.** During a 409 conflict streak the conflict handler pauses polling and retries on its own schedule — but a network hiccup (`ECONNRESET`) arriving mid-streak would schedule its own heal, which called `startPolling` straight back into the conflict and reset the streak clock. This was the live v2.15 burn mechanism: its own ghost long-poll after an ECONNRESET heal raised a 409, the old code exited on any 409, launchd respawned it (~1/hr, 208 lifetime startups), and every restart busted the prompt cache so the next turn re-billed the whole accumulated window uncached. Hiccup handling is now a no-op while a conflict streak is active — the conflict handler owns the poll lifecycle.
|
|
6
|
+
- **The 10-minute wedge backstop stands down during a conflict.** A conflict streak looks exactly like a wedge (no updates flowing), so the backstop could `exit(1)` mid-streak — the one remaining path where a token fight still killed the lock-holding bot (2 wedge-exits observed in the field). The backstop now skips while a conflict is active; a genuine >10-min wedge with no conflict still exits for a clean restart.
|
|
7
|
+
- **Investigated and cleared:** the v3 "token doubling" report was fully root-caused. Per-turn recall injection measured at parity (~4k tokens, 3 packs) across v2.15-classic, v2.15-discoverer, and v3-discoverer; the 160k→347k warning was the v3.0.0 cumulative-vs-peak display bug already fixed in v3.0.2; real spend was flat across the upgrade. The discoverer walker is identical in v2 and v3 (same haiku model via tier "low", same ~$0.015/turn) and stays on by default.
|
|
8
|
+
- **Agent-space / OpenClaw:** no new deps, no new env, no schema change; Docker image builds identically. New hermetic test `test-telegram-conflict-heal-guard.js` (wired into `npm test`) proves all four paths: hiccup no-op during conflict, hiccup still heals without conflict, backstop skipped during conflict, genuine wedge still exits.
|
|
9
|
+
|
|
10
|
+
## v3.0.4 — surviving the second mount, keeping the crons
|
|
11
|
+
|
|
12
|
+
- **A Kubernetes PVC remount no longer crashloops the bot.** The migration snapshot validator demanded exact owner-only modes (`0700` dirs, `0600` files). But kubelet's fsGroup ownership pass ORs group `rwX` + setgid onto every file and directory each time the volume is mounted — so the first v3 boot passed (snapshot created after the mount), and the **second pod recreation** hit `INVALID_MIGRATION_SNAPSHOT: migration snapshot has unsafe permissions` at boot and went CrashLoopBackOff. Every v3 pod on an fsGroup-mounted PVC was one recreation away from this (euvy hit it live on 2026-07-12 and needed a manual chmod rescue). Validation now rejects only world-access bits — group access is the pod's own supplemental group, not an exposure — while snapshots are still *created* `0700`/`0600`.
|
|
13
|
+
- **Legacy crons adopt your saved defaults instead of dying in the archive.** Pre-provider-aware jobs carried no pinned provider or project — they fired with whatever the bot had active — so the v3 migration classified every one of them "no unambiguous project" and archive-disabled it, silently killing real production crons (euvy's 6-hourly Airbyte data-sync among them). The jobs migration now looks up the owning user's saved state (`settings.backend` and `currentSession` name/dir, in both the v3 `users` shape and the pre-v3 global shape), adopts those as the job's provider/project, and marks the record `legacyDefaultsApplied` — preserving the old fire-time behaviour instead of erasing it. Jobs pinned to a removed provider (Cursor) keep their pin, stay archived, and are never reassigned; a job whose owner has no saved defaults still archives safely.
|
|
14
|
+
- **Re-reading the jobs store no longer rewrites stored nulls.** `Number(null)` is `0`, so the re-migration pass that runs on every read flipped `nextAttemptAt`/`lastFireAt` from `null` to epoch-`0` — harmless at runtime but a silent mutation on every load. Migration output is now byte-stable across passes.
|
|
15
|
+
- **Agent-space / OpenClaw:** no new deps, no new env, no schema change; Docker image builds identically. New hermetic probes: fsGroup-mode tolerance (group bits pass, world bits still refused) in `test-provider-migration-backup.js`; legacy-defaults adoption, v2/v3 state shapes, Cursor guard, and migration idempotency in `test-provider-scheduler.js`.
|
|
16
|
+
|
|
3
17
|
## v3.0.3 — the lock-holder stands its ground
|
|
4
18
|
|
|
5
19
|
- **A persistent 409 no longer restarts the bot.** v3.0.1 taught the poller to ride out short 409 conflicts but still `exit(1)` once a conflict outlived 2 minutes, on the theory that the survivor had to be a real second instance. In practice the rogue poller is lock-blind — a debugger-run dev checkout, a copy on another config dir, or another host on the same token — so the exit killed the *legitimate*, lock-holding service and launchd respawned it straight back into the same fight (five boot→409→exit cycles on 2026-07-12). The poller now never exits over a 409: it pauses 15s per retry, slows to 60s once the streak passes 2 minutes, sends the owner a throttled Telegram alert (max one per hour) naming the likely culprits, and posts an all-clear once the rogue goes away.
|
|
@@ -99,6 +99,11 @@ class TelegramAdapter {
|
|
|
99
99
|
// old exit-for-relaunch *was* the restart the bot kept doing on every idle
|
|
100
100
|
// timeout (a sleep/wake burst could exhaust all 6 attempts in seconds).
|
|
101
101
|
_onPollingHiccup(msg) {
|
|
102
|
+
// During a 409 streak the conflict handler owns the poll lifecycle; a heal
|
|
103
|
+
// here would startPolling straight back into the conflict and its errors
|
|
104
|
+
// would keep _wedgedSince pinned until the 10-min backstop killed us (the
|
|
105
|
+
// two wedge-exits of 2026-07-12 during the v3 upgrade handoff).
|
|
106
|
+
if (this._conflictTimer || this._conflictSince) return;
|
|
102
107
|
const now = Date.now();
|
|
103
108
|
if (!this._wedgedSince) this._wedgedSince = now;
|
|
104
109
|
if (now - this._lastHiccupLog > 60000) {
|
|
@@ -117,7 +122,10 @@ class TelegramAdapter {
|
|
|
117
122
|
// restart-in-place won't fix it — exit for a clean launchd relaunch. On a
|
|
118
123
|
// healthy network this never fires (heals recover in seconds); it replaces
|
|
119
124
|
// the old hair-trigger ~90s exit that caused the restart storm.
|
|
120
|
-
if (this._wedgedSince && Date.now() - this._wedgedSince > 600000) {
|
|
125
|
+
if (this._wedgedSince && !this._conflictSince && Date.now() - this._wedgedSince > 600000) {
|
|
126
|
+
// Never fires during a 409 streak: that's a token fight, not a network
|
|
127
|
+
// wedge, and exiting busts the prompt cache (full uncached re-bill of the
|
|
128
|
+
// conversation window on the next turn).
|
|
121
129
|
console.error("Polling wedged >10min — exiting for a clean restart.");
|
|
122
130
|
process.exit(1);
|
|
123
131
|
}
|
package/core/jobs.js
CHANGED
|
@@ -46,6 +46,14 @@ function clone(value) {
|
|
|
46
46
|
return value === undefined ? undefined : structuredClone(value);
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
+
// Number(null) is 0, so a bare Number.isFinite gate rewrites stored nulls to
|
|
50
|
+
// epoch timestamps on every re-migration pass.
|
|
51
|
+
function finiteTimeOrNull(value) {
|
|
52
|
+
if (value === null || value === undefined) return null;
|
|
53
|
+
const number = Number(value);
|
|
54
|
+
return Number.isFinite(number) ? number : null;
|
|
55
|
+
}
|
|
56
|
+
|
|
49
57
|
function occurrenceId(job, options = {}) {
|
|
50
58
|
if (nonEmpty(job.occurrenceId)) return nonEmpty(job.occurrenceId);
|
|
51
59
|
const factory = options.occurrenceIdFactory || (() => (
|
|
@@ -155,9 +163,9 @@ function normalizeRunnableJob(record, options = {}) {
|
|
|
155
163
|
disabled,
|
|
156
164
|
attemptCount,
|
|
157
165
|
maxAttempts,
|
|
158
|
-
nextAttemptAt:
|
|
166
|
+
nextAttemptAt: finiteTimeOrNull(record.nextAttemptAt),
|
|
159
167
|
createdAt,
|
|
160
|
-
lastFireAt:
|
|
168
|
+
lastFireAt: finiteTimeOrNull(record.lastFireAt),
|
|
161
169
|
lastFireOk: typeof record.lastFireOk === "boolean" ? record.lastFireOk : null,
|
|
162
170
|
lastError: isRecord(record.lastError) ? clone(record.lastError) : null,
|
|
163
171
|
};
|
|
@@ -169,6 +177,66 @@ function normalizeRunnableJob(record, options = {}) {
|
|
|
169
177
|
return normalized;
|
|
170
178
|
}
|
|
171
179
|
|
|
180
|
+
function readLegacyStateUsers(options = {}) {
|
|
181
|
+
if (options._legacyStateUsers !== undefined) return options._legacyStateUsers;
|
|
182
|
+
let users = null;
|
|
183
|
+
try {
|
|
184
|
+
if (!options.stateFile) throw new Error("no state file wired");
|
|
185
|
+
const raw = JSON.parse(fs.readFileSync(options.stateFile, "utf8"));
|
|
186
|
+
if (isRecord(raw)) {
|
|
187
|
+
// v3 state is { schemaVersion, users: { "<canonicalUserId>": {...} } };
|
|
188
|
+
// the pre-v3 file was one global user state at the top level.
|
|
189
|
+
users = isRecord(raw.users) ? raw.users : { "": raw };
|
|
190
|
+
}
|
|
191
|
+
} catch (_) {}
|
|
192
|
+
options._legacyStateUsers = users;
|
|
193
|
+
return users;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function legacyStateDefaultsFor(record, options = {}) {
|
|
197
|
+
const users = readLegacyStateUsers(options);
|
|
198
|
+
if (!users) return null;
|
|
199
|
+
const canonicalUserId = nonEmpty(record.canonicalUserId) || nonEmpty(options.defaultCanonicalUserId);
|
|
200
|
+
const user = (canonicalUserId && isRecord(users[canonicalUserId]) ? users[canonicalUserId] : null)
|
|
201
|
+
|| (isRecord(users[""]) ? users[""] : null);
|
|
202
|
+
if (!user) return null;
|
|
203
|
+
const provider = nonEmpty(user.settings?.backend)?.toLowerCase() || null;
|
|
204
|
+
const session = isRecord(user.currentSession) ? user.currentSession : {};
|
|
205
|
+
return {
|
|
206
|
+
provider: provider && RUNNABLE_PROVIDERS.has(provider) ? provider : null,
|
|
207
|
+
project: nonEmpty(session.name),
|
|
208
|
+
projectDir: nonEmpty(session.dir),
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Pre-provider-aware jobs fired with whatever provider and project the bot had
|
|
213
|
+
// active at fire time. Adopting the owning user's saved defaults preserves
|
|
214
|
+
// that behaviour instead of archive-disabling every legacy cron. Jobs pinned
|
|
215
|
+
// to a removed provider (Cursor) keep their pin and still archive.
|
|
216
|
+
function applyLegacyDefaults(record, options = {}) {
|
|
217
|
+
if (!isRecord(record)) return record;
|
|
218
|
+
const provider = legacyProvider(record);
|
|
219
|
+
if (provider === "cursor") return record;
|
|
220
|
+
const hasProvider = !!provider;
|
|
221
|
+
const hasProject = !!nonEmpty(record.project);
|
|
222
|
+
if (hasProvider && hasProject) return record;
|
|
223
|
+
const defaults = legacyStateDefaultsFor(record, options);
|
|
224
|
+
if (!defaults) return record;
|
|
225
|
+
const next = clone(record);
|
|
226
|
+
let applied = false;
|
|
227
|
+
if (!hasProvider && defaults.provider) {
|
|
228
|
+
next.provider = defaults.provider;
|
|
229
|
+
applied = true;
|
|
230
|
+
}
|
|
231
|
+
if (!hasProject && defaults.project) {
|
|
232
|
+
next.project = defaults.project;
|
|
233
|
+
if (!nonEmpty(next.projectDir) && defaults.projectDir) next.projectDir = defaults.projectDir;
|
|
234
|
+
applied = true;
|
|
235
|
+
}
|
|
236
|
+
if (applied) next.legacyDefaultsApplied = true;
|
|
237
|
+
return next;
|
|
238
|
+
}
|
|
239
|
+
|
|
172
240
|
function migrationReason(record, options = {}) {
|
|
173
241
|
const provider = legacyProvider(record);
|
|
174
242
|
if (provider === "cursor") return "Pinned to removed provider Cursor Agent";
|
|
@@ -194,16 +262,18 @@ function normalizeArchived(record, options = {}) {
|
|
|
194
262
|
}
|
|
195
263
|
|
|
196
264
|
function migrateJobsDocument(raw, options = {}) {
|
|
197
|
-
const
|
|
198
|
-
|
|
199
|
-
|
|
265
|
+
const legacySource = !(isRecord(raw) && raw.schemaVersion === JOBS_SCHEMA_VERSION);
|
|
266
|
+
const source = legacySource
|
|
267
|
+
? { jobs: Array.isArray(raw) ? raw : [], archived: [] }
|
|
268
|
+
: raw;
|
|
200
269
|
const jobs = [];
|
|
201
270
|
const archived = [];
|
|
202
271
|
|
|
203
272
|
for (const record of Array.isArray(source.jobs) ? source.jobs : []) {
|
|
204
|
-
const
|
|
273
|
+
const candidate = legacySource ? applyLegacyDefaults(record, options) : record;
|
|
274
|
+
const normalized = normalizeRunnableJob(candidate, options);
|
|
205
275
|
if (normalized) jobs.push(normalized);
|
|
206
|
-
else archived.push(archiveLegacyJob(
|
|
276
|
+
else archived.push(archiveLegacyJob(candidate, migrationReason(candidate, options), options));
|
|
207
277
|
}
|
|
208
278
|
for (const record of Array.isArray(source.archived) ? source.archived : []) {
|
|
209
279
|
const normalized = normalizeArchived(record, options);
|
|
@@ -281,6 +351,7 @@ function createJobsStore(options = {}) {
|
|
|
281
351
|
defaultChannelId: defaults.channelId,
|
|
282
352
|
defaultAdapter: defaults.adapter,
|
|
283
353
|
defaultAdapterType: defaults.adapterType,
|
|
354
|
+
stateFile: options.stateFile || path.join(configDir, path.basename(STATE_FILE)),
|
|
284
355
|
now: options.now,
|
|
285
356
|
occurrenceIdFactory: options.occurrenceIdFactory,
|
|
286
357
|
});
|
package/core/migration-backup.js
CHANGED
|
@@ -107,8 +107,13 @@ function assertRegularFile(filePath, code, label) {
|
|
|
107
107
|
return stat;
|
|
108
108
|
}
|
|
109
109
|
|
|
110
|
-
function assertPrivateMode(stat,
|
|
111
|
-
|
|
110
|
+
function assertPrivateMode(stat, code, label) {
|
|
111
|
+
// Kubernetes fsGroup ownership management ORs group rw(x) + setgid onto
|
|
112
|
+
// every file and directory each time the volume is mounted, so an exact
|
|
113
|
+
// owner-only mode check crashloops the second boot on any fsGroup-mounted
|
|
114
|
+
// PVC. Group access is the pod's own supplemental group; only world access
|
|
115
|
+
// is a real exposure.
|
|
116
|
+
if (process.platform !== "win32" && (stat.mode & 0o007) !== 0) {
|
|
112
117
|
throw migrationError(code, `${label} has unsafe permissions`);
|
|
113
118
|
}
|
|
114
119
|
}
|
|
@@ -596,18 +601,18 @@ function validateMigrationSnapshot(snapshotDir, options = {}) {
|
|
|
596
601
|
if (!snapshotStat.isDirectory() || snapshotStat.isSymbolicLink()) {
|
|
597
602
|
throw migrationError("INVALID_MIGRATION_SNAPSHOT", "migration snapshot must be a real directory");
|
|
598
603
|
}
|
|
599
|
-
assertPrivateMode(snapshotStat,
|
|
604
|
+
assertPrivateMode(snapshotStat, "INVALID_MIGRATION_SNAPSHOT", "migration snapshot");
|
|
600
605
|
const originalsStat = fs.lstatSync(path.join(resolvedSnapshot, "originals"));
|
|
601
606
|
if (!originalsStat.isDirectory() || originalsStat.isSymbolicLink()) {
|
|
602
607
|
throw migrationError("INVALID_MIGRATION_SNAPSHOT", "migration originals directory is invalid");
|
|
603
608
|
}
|
|
604
|
-
assertPrivateMode(originalsStat,
|
|
609
|
+
assertPrivateMode(originalsStat, "INVALID_MIGRATION_SNAPSHOT", "migration originals directory");
|
|
605
610
|
const manifestPath = path.join(resolvedSnapshot, MANIFEST_FILE);
|
|
606
611
|
const hashPath = path.join(resolvedSnapshot, MANIFEST_HASH_FILE);
|
|
607
612
|
const manifestStat = assertRegularFile(manifestPath, "INVALID_MIGRATION_MANIFEST", "migration manifest");
|
|
608
613
|
const manifestHashStat = assertRegularFile(hashPath, "INVALID_MIGRATION_MANIFEST", "migration manifest checksum");
|
|
609
|
-
assertPrivateMode(manifestStat,
|
|
610
|
-
assertPrivateMode(manifestHashStat,
|
|
614
|
+
assertPrivateMode(manifestStat, "INVALID_MIGRATION_MANIFEST", "migration manifest");
|
|
615
|
+
assertPrivateMode(manifestHashStat, "INVALID_MIGRATION_MANIFEST", "migration manifest checksum");
|
|
611
616
|
const manifestBytes = fs.readFileSync(manifestPath);
|
|
612
617
|
const expectedManifestHash = fs.readFileSync(hashPath, "utf8").trim();
|
|
613
618
|
if (!/^[a-f0-9]{64}$/.test(expectedManifestHash) || sha256(manifestBytes) !== expectedManifestHash) {
|
|
@@ -625,7 +630,7 @@ function validateMigrationSnapshot(snapshotDir, options = {}) {
|
|
|
625
630
|
}
|
|
626
631
|
if (!fs.existsSync(filePath)) throw migrationError("BACKUP_CHECKSUM_MISMATCH", `backup file is missing: ${entry.path}`);
|
|
627
632
|
const stat = assertRegularFile(filePath, "BACKUP_CHECKSUM_MISMATCH", `backup file ${entry.path}`);
|
|
628
|
-
assertPrivateMode(stat,
|
|
633
|
+
assertPrivateMode(stat, "BACKUP_CHECKSUM_MISMATCH", `backup file ${entry.path}`);
|
|
629
634
|
const bytes = fs.readFileSync(filePath);
|
|
630
635
|
if (stat.size !== entry.size || bytes.length !== entry.size || sha256(bytes) !== entry.sha256) {
|
|
631
636
|
throw migrationError("BACKUP_CHECKSUM_MISMATCH", `backup checksum verification failed: ${entry.path}`);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@inetafrica/open-claudia",
|
|
3
|
-
"version": "3.0.
|
|
3
|
+
"version": "3.0.5",
|
|
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": {
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
"setup": "node setup.js",
|
|
11
11
|
"start": "node bot.js",
|
|
12
12
|
"pretest": "node test-provider-fixture.js && node test-provider-core-prompt.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-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-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",
|
|
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-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-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-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"
|
|
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-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-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"
|
|
14
14
|
},
|
|
15
15
|
"files": [
|
|
16
16
|
"bot.js",
|
package/test-provider-dream.js
CHANGED
|
@@ -124,6 +124,9 @@ async function providerProbe(kind) {
|
|
|
124
124
|
const { createUtilityAgentService } = require("./core/utility-agent");
|
|
125
125
|
const utility = createUtilityAgentService({
|
|
126
126
|
registry: registryFor(provider),
|
|
127
|
+
// This probe models a global/nightly dream with no active chat. Do not let
|
|
128
|
+
// the parent Open Claudia process's OC_PROVIDER leak into provider choice.
|
|
129
|
+
activeProvider: () => null,
|
|
127
130
|
env: {
|
|
128
131
|
UTILITY_PROVIDER: "active",
|
|
129
132
|
DEFAULT_PROVIDER: kind,
|
|
@@ -32,10 +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
|
-
|
|
35
|
+
// 3.0.5 approved by Sumeet 2026-07-12 ("no do it on the latest co0de and push
|
|
36
|
+
// a new version" — 409-conflict/heal/wedge guards so a token fight never
|
|
37
|
+
// restarts the bot and busts the prompt cache; walker stays on per his call
|
|
38
|
+
// that v2's walker was fine and v3's is identical).
|
|
39
|
+
assert.strictEqual(pkg.version, "3.0.5", "release version must remain unchanged without explicit approval");
|
|
39
40
|
for (const keyword of ["claude", "codex", "coding-agent", "provider-agnostic"]) {
|
|
40
41
|
assert.ok(pkg.keywords.includes(keyword), `package keyword missing: ${keyword}`);
|
|
41
42
|
}
|
|
@@ -378,6 +378,54 @@ function sourceChangeProbe() {
|
|
|
378
378
|
}
|
|
379
379
|
}
|
|
380
380
|
|
|
381
|
+
function fsGroupToleranceProbe() {
|
|
382
|
+
if (process.platform === "win32") return;
|
|
383
|
+
const root = tempRoot("fsgroup");
|
|
384
|
+
try {
|
|
385
|
+
const configDir = installFixture("claude-only", root);
|
|
386
|
+
const opts = options(configDir);
|
|
387
|
+
const snapshot = ensureMigrationSnapshot(opts);
|
|
388
|
+
|
|
389
|
+
// Simulate the kubelet fsGroup ownership pass: group rwX + setgid ORed
|
|
390
|
+
// onto every directory and group rw onto every file at each pod mount.
|
|
391
|
+
const applyFsGroup = (target) => {
|
|
392
|
+
const stat = fs.lstatSync(target);
|
|
393
|
+
if (stat.isDirectory()) {
|
|
394
|
+
fs.chmodSync(target, (stat.mode & 0o777) | 0o2070);
|
|
395
|
+
for (const entry of fs.readdirSync(target)) applyFsGroup(path.join(target, entry));
|
|
396
|
+
} else {
|
|
397
|
+
fs.chmodSync(target, (stat.mode & 0o777) | 0o060);
|
|
398
|
+
}
|
|
399
|
+
};
|
|
400
|
+
applyFsGroup(snapshot.snapshotDir);
|
|
401
|
+
// chmod may silently drop setgid for non-members of the dir group, so
|
|
402
|
+
// assert only the group bits — the part every fsGroup mount guarantees.
|
|
403
|
+
assert.strictEqual(fs.statSync(snapshot.snapshotDir).mode & 0o770, 0o770);
|
|
404
|
+
|
|
405
|
+
const validated = validateMigrationSnapshot(snapshot.snapshotDir, { sources: opts.sources, verifySources: true });
|
|
406
|
+
assert.strictEqual(validated.ok, true, "group bits from a k8s fsGroup mount must not invalidate the snapshot");
|
|
407
|
+
const reused = ensureMigrationSnapshot(opts);
|
|
408
|
+
assert.strictEqual(reused.reused, true, "fsGroup-touched snapshot is reused at boot, not rejected");
|
|
409
|
+
|
|
410
|
+
fs.chmodSync(manifestFile(snapshot.snapshotDir), 0o664);
|
|
411
|
+
assert.throws(
|
|
412
|
+
() => validateMigrationSnapshot(snapshot.snapshotDir),
|
|
413
|
+
(error) => error.code === "INVALID_MIGRATION_MANIFEST" && /unsafe permissions/.test(error.message),
|
|
414
|
+
"world-readable manifest must still be rejected",
|
|
415
|
+
);
|
|
416
|
+
fs.chmodSync(manifestFile(snapshot.snapshotDir), 0o660);
|
|
417
|
+
|
|
418
|
+
fs.chmodSync(snapshot.snapshotDir, 0o2775);
|
|
419
|
+
assert.throws(
|
|
420
|
+
() => validateMigrationSnapshot(snapshot.snapshotDir),
|
|
421
|
+
(error) => error.code === "INVALID_MIGRATION_SNAPSHOT" && /unsafe permissions/.test(error.message),
|
|
422
|
+
"world-accessible snapshot dir must still be rejected",
|
|
423
|
+
);
|
|
424
|
+
} finally {
|
|
425
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
|
|
381
429
|
function integrationAndDocsProbe() {
|
|
382
430
|
const bot = fs.readFileSync(path.join(__dirname, "bot.js"), "utf8");
|
|
383
431
|
const scheduler = fs.readFileSync(path.join(__dirname, "core", "scheduler.js"), "utf8");
|
|
@@ -420,5 +468,6 @@ activationProbe();
|
|
|
420
468
|
rollbackProbe();
|
|
421
469
|
sourceChangeProbe();
|
|
422
470
|
rawByteProbe();
|
|
471
|
+
fsGroupToleranceProbe();
|
|
423
472
|
integrationAndDocsProbe();
|
|
424
473
|
console.log("provider migration backup OK");
|
|
@@ -84,6 +84,90 @@ function migrationProbe(jobsApi) {
|
|
|
84
84
|
assert.deepStrictEqual(rerun, cursor, "jobs migration must be idempotent");
|
|
85
85
|
}
|
|
86
86
|
|
|
87
|
+
function legacyDefaultsProbe(jobsApi) {
|
|
88
|
+
const temp = fs.mkdtempSync(path.join(os.tmpdir(), "open-claudia-legacy-defaults-"));
|
|
89
|
+
try {
|
|
90
|
+
const baseDefaults = {
|
|
91
|
+
defaultCanonicalUserId: "telegram:fixture",
|
|
92
|
+
defaultChannelId: "fixture-channel",
|
|
93
|
+
defaultAdapter: "telegram-fixture",
|
|
94
|
+
defaultAdapterType: "telegram",
|
|
95
|
+
now: () => 1_720_000_000_000,
|
|
96
|
+
occurrenceIdFactory: (job) => `occ-${job.id}`,
|
|
97
|
+
};
|
|
98
|
+
const legacyCron = {
|
|
99
|
+
id: "legacy-orphan-cron",
|
|
100
|
+
kind: "cron",
|
|
101
|
+
schedule: "0 */6 * * *",
|
|
102
|
+
prompt: "run the production sync",
|
|
103
|
+
canonicalUserId: "telegram:100",
|
|
104
|
+
channelId: "chan-100",
|
|
105
|
+
adapter: "telegram-fixture",
|
|
106
|
+
adapterType: "telegram",
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
const v3State = path.join(temp, "state-v3.json");
|
|
110
|
+
fs.writeFileSync(v3State, JSON.stringify({
|
|
111
|
+
schemaVersion: 3,
|
|
112
|
+
users: {
|
|
113
|
+
"telegram:100": {
|
|
114
|
+
settings: { backend: "codex" },
|
|
115
|
+
currentSession: { name: "data-sync-runner", dir: "/workspace/data-sync-runner" },
|
|
116
|
+
},
|
|
117
|
+
},
|
|
118
|
+
}));
|
|
119
|
+
const adopted = jobsApi.migrateJobsDocument([{ ...legacyCron }], { ...baseDefaults, stateFile: v3State });
|
|
120
|
+
assert.deepStrictEqual(adopted.archived, [], "legacy cron with saved user defaults must stay runnable");
|
|
121
|
+
assert.strictEqual(adopted.jobs.length, 1);
|
|
122
|
+
assert.strictEqual(adopted.jobs[0].provider, "codex");
|
|
123
|
+
assert.strictEqual(adopted.jobs[0].project, "data-sync-runner");
|
|
124
|
+
assert.strictEqual(adopted.jobs[0].projectDir, "/workspace/data-sync-runner");
|
|
125
|
+
assert.strictEqual(adopted.jobs[0].legacyDefaultsApplied, true);
|
|
126
|
+
assert.strictEqual(adopted.jobs[0].disabled, false);
|
|
127
|
+
|
|
128
|
+
const rerun = jobsApi.migrateJobsDocument(adopted, { ...baseDefaults, stateFile: v3State });
|
|
129
|
+
assert.deepStrictEqual(rerun, adopted, "legacy defaults adoption must be idempotent");
|
|
130
|
+
|
|
131
|
+
const pinned = jobsApi.migrateJobsDocument(
|
|
132
|
+
[{ ...legacyCron, id: "legacy-provider-pinned", provider: "claude" }],
|
|
133
|
+
{ ...baseDefaults, stateFile: v3State },
|
|
134
|
+
);
|
|
135
|
+
assert.strictEqual(pinned.jobs[0].provider, "claude", "explicit legacy provider must win over the saved default");
|
|
136
|
+
assert.strictEqual(pinned.jobs[0].project, "data-sync-runner");
|
|
137
|
+
assert.strictEqual(pinned.jobs[0].legacyDefaultsApplied, true);
|
|
138
|
+
|
|
139
|
+
const v2State = path.join(temp, "state-v2.json");
|
|
140
|
+
fs.writeFileSync(v2State, JSON.stringify({
|
|
141
|
+
settings: { backend: "claude" },
|
|
142
|
+
currentSession: { name: "alpha", dir: "/workspace/alpha" },
|
|
143
|
+
}));
|
|
144
|
+
const globalShape = jobsApi.migrateJobsDocument([{ ...legacyCron }], { ...baseDefaults, stateFile: v2State });
|
|
145
|
+
assert.strictEqual(globalShape.jobs.length, 1, "pre-v3 global state shape must supply defaults");
|
|
146
|
+
assert.strictEqual(globalShape.jobs[0].provider, "claude");
|
|
147
|
+
assert.strictEqual(globalShape.jobs[0].project, "alpha");
|
|
148
|
+
assert.strictEqual(globalShape.jobs[0].legacyDefaultsApplied, true);
|
|
149
|
+
|
|
150
|
+
const cursor = jobsApi.migrateJobsDocument(
|
|
151
|
+
[{ ...legacyCron, id: "legacy-cursor-cron", sessionKey: "cursorSessionId" }],
|
|
152
|
+
{ ...baseDefaults, stateFile: v3State },
|
|
153
|
+
);
|
|
154
|
+
assert.deepStrictEqual(cursor.jobs, [], "Cursor-pinned jobs must not adopt defaults");
|
|
155
|
+
assert.strictEqual(cursor.archived[0].provider, "cursor");
|
|
156
|
+
assert.strictEqual(cursor.archived[0].project, null, "archived Cursor evidence must stay pristine");
|
|
157
|
+
assert.ok(!cursor.archived[0].legacyDefaultsApplied);
|
|
158
|
+
assert.match(cursor.archived[0].archiveReason, /removed provider/i);
|
|
159
|
+
|
|
160
|
+
const noState = jobsApi.migrateJobsDocument(
|
|
161
|
+
[{ ...legacyCron }],
|
|
162
|
+
{ ...baseDefaults, stateFile: path.join(temp, "missing.json") },
|
|
163
|
+
);
|
|
164
|
+
assert.deepStrictEqual(noState.jobs, [], "without saved defaults the legacy cron still archives");
|
|
165
|
+
assert.match(noState.archived[0].archiveReason, /provider/i);
|
|
166
|
+
} finally {
|
|
167
|
+
fs.rmSync(temp, { recursive: true, force: true });
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
87
171
|
function migrationIntegrationProbe(jobsApi, migrationApi) {
|
|
88
172
|
const temp = fs.mkdtempSync(path.join(os.tmpdir(), "open-claudia-jobs-migration-"));
|
|
89
173
|
try {
|
|
@@ -669,6 +753,7 @@ async function main() {
|
|
|
669
753
|
assert.strictEqual(typeof schedulerApi.createSchedulerService, "function");
|
|
670
754
|
|
|
671
755
|
migrationProbe(jobsApi);
|
|
756
|
+
legacyDefaultsProbe(jobsApi);
|
|
672
757
|
migrationIntegrationProbe(jobsApi, migrationApi);
|
|
673
758
|
await immutableTerminalProbe(schedulerApi.createSchedulerService);
|
|
674
759
|
await duplicateFireProbe(schedulerApi.createSchedulerService);
|