@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.
@@ -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
- assert.strictEqual(pkg.version, "3.4.23", "release version must remain unchanged without explicit approval");
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: 30000,
425
+ timeout: CHILD_TIMEOUT_MS,
413
426
  });
414
427
  if (child.status !== 0) {
415
- throw new Error(`child ${args.join(" ")} failed (${child.status})\n${child.stdout}\n${child.stderr}`);
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
- review: ["test-provider-pack-review.js", "provider pack review OK"],
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: 90000,
471
+ timeout: suiteTimeoutMs,
470
472
  maxBuffer: 8 * 1024 * 1024,
471
473
  });
472
474
  assert.ifError(result.error);