@inetafrica/open-claudia 3.4.23 → 3.4.24
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 +10 -0
- package/bin/schedule.js +45 -11
- package/core/loopback.js +13 -2
- package/core/runner.js +2 -2
- package/core/scheduler.js +197 -35
- package/package.json +1 -1
- package/test-provider-compaction.js +93 -0
- package/test-provider-language.js +7 -1
- package/test-provider-pack-review.js +25 -2
- package/test-provider-parity-e2e.js +5 -3
- package/test-provider-scheduler.js +490 -3
- package/test-run-lock.js +2 -0
|
@@ -289,6 +289,98 @@ async function codexRestrictionProbe() {
|
|
|
289
289
|
assert.deepStrictEqual(claudeInvocation.args.slice(claudeInvocation.args.indexOf("--permission-mode"), claudeInvocation.args.indexOf("--permission-mode") + 2), ["--permission-mode", "plan"]);
|
|
290
290
|
}
|
|
291
291
|
|
|
292
|
+
// 21 Sep (task 76ecr7): a watch passed while a long turn ran, the turn ended in
|
|
293
|
+
// a compaction, and the alarm looked lost. The queued delivery waits out the
|
|
294
|
+
// turn and the compaction, then runs once at the next idle moment on the
|
|
295
|
+
// compacted session; after a later session switch it still runs, on its own
|
|
296
|
+
// branch, without taking over the live session.
|
|
297
|
+
async function deferredWatchAcrossCompactionProbe() {
|
|
298
|
+
const { createSchedulerService } = require("./core/scheduler");
|
|
299
|
+
for (const switchAfter of [false, true]) {
|
|
300
|
+
resetFixture("claude");
|
|
301
|
+
const records = new Map([["watch-long-turn", {
|
|
302
|
+
id: "watch-long-turn",
|
|
303
|
+
kind: "cron",
|
|
304
|
+
schedule: "* * * * *",
|
|
305
|
+
check: "fixture-check",
|
|
306
|
+
repeat: false,
|
|
307
|
+
expiresAt: Date.now() + 60 * 60 * 1000,
|
|
308
|
+
adapter: "telegram-fixture",
|
|
309
|
+
adapterType: "telegram",
|
|
310
|
+
channelId: "fixture",
|
|
311
|
+
canonicalUserId: state.userId,
|
|
312
|
+
provider: "claude",
|
|
313
|
+
providerSettings: {},
|
|
314
|
+
project: "alpha",
|
|
315
|
+
projectDir: alphaDir,
|
|
316
|
+
sessionId: "old-claude",
|
|
317
|
+
prompt: "the SDK tag pipeline finished",
|
|
318
|
+
label: "SDK pipeline",
|
|
319
|
+
occurrenceId: "occ-long-turn",
|
|
320
|
+
status: "pending",
|
|
321
|
+
attemptCount: 0,
|
|
322
|
+
maxAttempts: 3,
|
|
323
|
+
disabled: false,
|
|
324
|
+
}]]);
|
|
325
|
+
const jobsStore = {
|
|
326
|
+
get: (id) => (records.has(id) ? structuredClone(records.get(id)) : null),
|
|
327
|
+
update(id, patch) {
|
|
328
|
+
if (!records.has(id)) return null;
|
|
329
|
+
records.set(id, { ...records.get(id), ...structuredClone(patch) });
|
|
330
|
+
return structuredClone(records.get(id));
|
|
331
|
+
},
|
|
332
|
+
remove(id) { const value = records.get(id) || null; records.delete(id); return value; },
|
|
333
|
+
listAll: () => [...records.values()].map((job) => structuredClone(job)),
|
|
334
|
+
};
|
|
335
|
+
const timers = [];
|
|
336
|
+
const runs = [];
|
|
337
|
+
const scheduler = createSchedulerService({
|
|
338
|
+
jobsStore,
|
|
339
|
+
adapters: [{ id: "telegram-fixture", type: "telegram" }],
|
|
340
|
+
workspace,
|
|
341
|
+
getUserState: () => state,
|
|
342
|
+
runInChat: async (_chat, fn) => fn(),
|
|
343
|
+
send: async () => ({ ok: true }),
|
|
344
|
+
setTimer: (fn, delay) => { timers.push({ fn, delay }); return timers.length; },
|
|
345
|
+
clearTimer: () => {},
|
|
346
|
+
scheduleCron: () => ({ stop() {} }),
|
|
347
|
+
providerStatus: () => ({ availability: { state: "available" }, auth: { state: "authenticated" } }),
|
|
348
|
+
fallbackProviders: [],
|
|
349
|
+
runWatchCheck: async () => ({ code: 0 }),
|
|
350
|
+
admitRunContext: (_prompt, cwd, _replyTo, opts) => ({ cwd, ...opts }),
|
|
351
|
+
runAgent: async (_prompt, _cwd, _replyTo, opts) => {
|
|
352
|
+
runs.push(opts);
|
|
353
|
+
return { ok: true, status: "succeeded", persistence: { transcript: { ok: true } }, delivery: { ok: true } };
|
|
354
|
+
},
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
state.runningProcess = { pid: 4242 };
|
|
358
|
+
assert.strictEqual((await scheduler.fireJob("watch-long-turn")).deferred, true, "a watch that passes mid-turn is queued");
|
|
359
|
+
for (let i = 0; i < 3; i++) await timers.at(-1).fn();
|
|
360
|
+
state.runningProcess = null;
|
|
361
|
+
let calls = 0;
|
|
362
|
+
const compaction = serviceWith(async () => {
|
|
363
|
+
calls += 1;
|
|
364
|
+
if (calls === 1) {
|
|
365
|
+
assert.strictEqual(state.isCompacting, true);
|
|
366
|
+
assert.strictEqual((await timers.at(-1).fn()).deferred, true, "the delivery waits out the compaction");
|
|
367
|
+
return result({ sessionId: "old-claude" });
|
|
368
|
+
}
|
|
369
|
+
return result({ sessionId: "compacted-claude" });
|
|
370
|
+
});
|
|
371
|
+
assert.strictEqual((await compaction.compact(alphaDir, { operationId: `compact-watch-${switchAfter}` })).compacted, true);
|
|
372
|
+
assert.strictEqual(runs.length, 0);
|
|
373
|
+
assert.ok(records.get("watch-long-turn"), "the queued delivery survives the compaction");
|
|
374
|
+
if (switchAfter) stateApi.setProviderSession(state, "claude", "alpha", "fresh-claude-after-new");
|
|
375
|
+
|
|
376
|
+
assert.strictEqual((await timers.at(-1).fn()).ok, true, "the next idle moment delivers it");
|
|
377
|
+
assert.strictEqual(runs.length, 1, "exactly once");
|
|
378
|
+
assert.strictEqual(runs[0].resumeSessionId, "compacted-claude", "it resumes the compacted session, not the pre-compaction one");
|
|
379
|
+
assert.strictEqual(runs[0].attachSession, !switchAfter, "it never takes over a session the user switched to");
|
|
380
|
+
assert.strictEqual(records.has("watch-long-turn"), false, "a delivered one-shot watch is consumed, not left listed");
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
292
384
|
function duplicateCommitProbe() {
|
|
293
385
|
resetFixture("claude");
|
|
294
386
|
const transaction = {
|
|
@@ -364,6 +456,7 @@ function restartRecoveryProbe() {
|
|
|
364
456
|
await immutableSuccessProbe();
|
|
365
457
|
await failureProbe();
|
|
366
458
|
await codexRestrictionProbe();
|
|
459
|
+
await deferredWatchAcrossCompactionProbe();
|
|
367
460
|
duplicateCommitProbe();
|
|
368
461
|
restartRecoveryProbe();
|
|
369
462
|
console.log("provider compaction OK");
|
|
@@ -500,7 +500,13 @@ assert.match(pkg.description, /provider-agnostic coding-agent harness/i);
|
|
|
500
500
|
// stopped run unwinds and its flag names that run, so it no longer cancels the
|
|
501
501
|
// next messages; "Queued." names what it waits on; a watch alarm stuck 15 min
|
|
502
502
|
// behind a busy room posts one line.
|
|
503
|
-
|
|
503
|
+
// 3.4.24 approved by Sumeet 2026-09-24 20:30 EAT ("If there's fixes needed for open
|
|
504
|
+
// Claudia proceed."): a repeating watch keeps polling while its delivery waits or
|
|
505
|
+
// retries, and new hits fold into it; 3 check timeouts in a row alarm once; far
|
|
506
|
+
// wakeups sleep in chunks instead of firing at once; a failed jobs-store write
|
|
507
|
+
// keeps the retry armed; listings say where a fired job stands; loopback says
|
|
508
|
+
// which step failed; the pack-review release gate no longer flakes on 30 s.
|
|
509
|
+
assert.strictEqual(pkg.version, "3.4.24", "release version must remain unchanged without explicit approval");
|
|
504
510
|
for (const keyword of ["claude", "codex", "coding-agent", "provider-agnostic"]) {
|
|
505
511
|
assert.ok(pkg.keywords.includes(keyword), `package keyword missing: ${keyword}`);
|
|
506
512
|
}
|
|
@@ -404,21 +404,43 @@ async function lifecycleProbe() {
|
|
|
404
404
|
return { serialized: true, staleProtected: true, failureRecorded: true, shutdownDrained: true };
|
|
405
405
|
}
|
|
406
406
|
|
|
407
|
+
// A loaded CI runner has taken over 30 s for one probe child; the kill then
|
|
408
|
+
// read as "failed (null)" and failed the release gate (v3.4.19, job 43703).
|
|
409
|
+
const CHILD_TIMEOUT_MS = 120000;
|
|
410
|
+
|
|
411
|
+
// Why a child did not exit 0: killed by the timeout, killed by a signal, never
|
|
412
|
+
// started, or a real non-zero exit.
|
|
413
|
+
function childFailure(child, timeoutMs = CHILD_TIMEOUT_MS) {
|
|
414
|
+
if (child.error?.code === "ETIMEDOUT") return `timed out after ${timeoutMs / 1000} s (killed with ${child.signal || "SIGTERM"})`;
|
|
415
|
+
if (child.signal) return `was killed by ${child.signal}`;
|
|
416
|
+
if (child.error) return `could not start (${child.error.code || child.error.message})`;
|
|
417
|
+
return `failed (exit ${child.status})`;
|
|
418
|
+
}
|
|
419
|
+
|
|
407
420
|
function runChild(args) {
|
|
408
421
|
const child = spawnSync(process.execPath, [__filename, ...args], {
|
|
409
422
|
cwd: __dirname,
|
|
410
423
|
env: { ...process.env },
|
|
411
424
|
encoding: "utf8",
|
|
412
|
-
timeout:
|
|
425
|
+
timeout: CHILD_TIMEOUT_MS,
|
|
413
426
|
});
|
|
414
427
|
if (child.status !== 0) {
|
|
415
|
-
throw new Error(`child ${args.join(" ")}
|
|
428
|
+
throw new Error(`child ${args.join(" ")} ${childFailure(child)}\n${child.stdout}\n${child.stderr}`);
|
|
416
429
|
}
|
|
417
430
|
const line = child.stdout.split("\n").find((entry) => entry.startsWith(RESULT_PREFIX));
|
|
418
431
|
if (!line) throw new Error(`child ${args.join(" ")} returned no result\n${child.stdout}\n${child.stderr}`);
|
|
419
432
|
return JSON.parse(line.slice(RESULT_PREFIX.length));
|
|
420
433
|
}
|
|
421
434
|
|
|
435
|
+
function childFailureProbe() {
|
|
436
|
+
const hung = spawnSync(process.execPath, ["-e", "setTimeout(() => {}, 10000)"], { encoding: "utf8", timeout: 200 });
|
|
437
|
+
assert.strictEqual(childFailure(hung, 200), "timed out after 0.2 s (killed with SIGTERM)", "a timeout reads as a timeout");
|
|
438
|
+
const failing = spawnSync(process.execPath, ["-e", "process.exit(3)"], { encoding: "utf8", timeout: 10000 });
|
|
439
|
+
assert.strictEqual(childFailure(failing), "failed (exit 3)", "a failing probe keeps its exit code");
|
|
440
|
+
const killed = spawnSync(process.execPath, ["-e", "process.kill(process.pid, 'SIGKILL')"], { encoding: "utf8", timeout: 10000 });
|
|
441
|
+
assert.strictEqual(childFailure(killed), "was killed by SIGKILL");
|
|
442
|
+
}
|
|
443
|
+
|
|
422
444
|
async function main() {
|
|
423
445
|
if (process.argv[2] === "--provider-probe") {
|
|
424
446
|
const result = await providerProbe(process.argv[3]);
|
|
@@ -431,6 +453,7 @@ async function main() {
|
|
|
431
453
|
return;
|
|
432
454
|
}
|
|
433
455
|
|
|
456
|
+
childFailureProbe();
|
|
434
457
|
const claude = runChild(["--provider-probe", "claude"]);
|
|
435
458
|
const codex = runChild(["--provider-probe", "codex"]);
|
|
436
459
|
assert.deepStrictEqual(codex.mutations, claude.mutations, "identical fixture decisions produce identical memory mutations under Claude and Codex");
|
|
@@ -21,7 +21,9 @@ const SUPPORT_SUITES = Object.freeze({
|
|
|
21
21
|
scheduler: ["test-provider-scheduler.js", "provider scheduler OK"],
|
|
22
22
|
sideChat: ["test-provider-side-chat.js", "provider side chat OK"],
|
|
23
23
|
recall: ["test-provider-recall.js", "provider recall OK"],
|
|
24
|
-
|
|
24
|
+
// Three probe children, each allowed 120 s under a loaded runner; the suite
|
|
25
|
+
// cap must not undercut them (a 90 s cap would move the v3.4.19 flake here).
|
|
26
|
+
review: ["test-provider-pack-review.js", "provider pack review OK", 8 * 60 * 1000],
|
|
25
27
|
mutationQueue: ["test-memory-mutation-queue.js", "memory mutation queue OK"],
|
|
26
28
|
dream: ["test-provider-dream.js", "provider dream OK"],
|
|
27
29
|
subagent: ["test-provider-subagent.js", "provider subagent OK"],
|
|
@@ -457,7 +459,7 @@ function runProviderChild(kind) {
|
|
|
457
459
|
function runSupportSuites(provider) {
|
|
458
460
|
assert.ok(["claude", "codex"].includes(provider));
|
|
459
461
|
const passed = new Set();
|
|
460
|
-
for (const [key, [script, marker]] of Object.entries(SUPPORT_SUITES)) {
|
|
462
|
+
for (const [key, [script, marker, suiteTimeoutMs = 90000]] of Object.entries(SUPPORT_SUITES)) {
|
|
461
463
|
const root = fs.mkdtempSync(path.join(os.tmpdir(), `open-claudia-parity-suite-${key}-`));
|
|
462
464
|
try {
|
|
463
465
|
fs.mkdirSync(path.join(root, "home"));
|
|
@@ -466,7 +468,7 @@ function runSupportSuites(provider) {
|
|
|
466
468
|
cwd: repoRoot,
|
|
467
469
|
env: isolatedEnv(root, provider),
|
|
468
470
|
encoding: "utf8",
|
|
469
|
-
timeout:
|
|
471
|
+
timeout: suiteTimeoutMs,
|
|
470
472
|
maxBuffer: 8 * 1024 * 1024,
|
|
471
473
|
});
|
|
472
474
|
assert.ifError(result.error);
|