@inetafrica/open-claudia 3.0.31 → 3.0.34

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 CHANGED
@@ -1,5 +1,18 @@
1
1
  # Changelog
2
2
 
3
+ ## v3.0.34 — /upgrade no longer triggers a rollout storm
4
+
5
+ - **One `/upgrade` used to mean up to five rollouts.** On AgentSpace pods the control plane answers `/upgrade` by rolling the deployment, which SIGTERMs the bot mid-turn. Telegram long-polling keeps no persisted offset, so the still-unconfirmed `/upgrade` update was redelivered to the freshly booted pod — which triggered *another* rollout, and so on. Observed on rahilcoder 2026-07-17: deployment revisions 47–51 (five rollouts in three minutes) from a single command; every earlier `/upgrade` had silently double-fired the same way (revs 42/43, 45/46).
6
+ - **Fix: an upgrade-in-flight marker (`upgrade-in-flight.json` in the config dir, 10-minute freshness) guards the handler.** A redelivered or impatiently re-run `/upgrade` inside the window is acknowledged but not re-triggered: if the pod has already restarted since the request it replies "that upgrade already went through", otherwise "already in flight". The marker is cleared on a definitive trigger failure so an immediate retry stays possible, and simply ages out otherwise.
7
+ - **The ack now goes out BEFORE the rollout is requested.** Previously the "Upgrade requested" confirmation was sent after the control-plane call, so the SIGTERM routinely killed it — Rahil saw reboot greetings *before* any acknowledgement. Ack-first means the user always sees the confirmation, then the reboot.
8
+ - **Agent-space / OpenClaw:** no new deps, no new env, no schema change; Docker image builds FROM the existing `:base`. Existing pods pick this up on their next upgrade — that upgrade itself will still multi-fire (old code is doing the triggering), all later ones are single-shot.
9
+
10
+ ## v3.0.33 — Full sudo inside the pod (re-cut of v3.0.32; the pin guard bit us again)
11
+
12
+ - **`claudia` now has `NOPASSWD:ALL` sudo in the pod image.** The rebuilt `:base` widens the baked sudoers from the old command allow-list to full passwordless sudo, so the agent can install packages and operate inside its own container like a normal machine. This pairs with agent-space's IaaS-style security-policy system: the blast radius is fenced at the pod's NetworkPolicy (egress/ingress presets, DB-rendered), not by crippling the user inside the container.
13
+ - **Re-cut of v3.0.32.** That tag bumped `package.json` but not the version pin in `test-provider-language.js`, so `pretest` asserted 3.0.31, the **build** job failed, and the **docker** job was skipped — nothing published, `:latest` stayed on 3.0.31. Identical payload here with the pin bumped. (Same failure mode as v3.0.30 → v3.0.31.)
14
+ - **Agent-space / OpenClaw:** no new deps, no new env, no schema change; Docker image builds FROM the rebuilt full-sudo `:base`. Existing pods pick this up on their next approved upgrade.
15
+
3
16
  ## v3.0.31 — Re-cut of the approval-card release (the version-pin guard bit us)
4
17
 
5
18
  - **Same feature payload as v3.0.30, this time it builds.** The v3.0.30 tag's build failed before publishing anything. `test-provider-language.js` pins the release version and asserts it "must remain unchanged without explicit approval" — a deliberate gate so every bump is consciously acknowledged in the same commit. `package.json` was bumped to 3.0.30 but that pin wasn't, so `pretest` asserted 3.0.29, the **build** job failed, and the **docker** job was skipped (no npm publish, no image — `:base` never entered it). The pin now tracks the release, so the build goes green.
package/core/handlers.js CHANGED
@@ -29,6 +29,7 @@ const keyring = require("./keyring");
29
29
  const { redactSensitive, registerSecrets } = require("./redact");
30
30
  const { runDoctorChecks, formatDoctorReport } = require("./doctor");
31
31
  const { isNewerVersion } = require("./version");
32
+ const { recordUpgradeRequested, upgradeInFlightSince, clearUpgradeMarker } = require("./upgrade-marker");
32
33
  const jobs = require("./jobs");
33
34
  const scheduler = require("./scheduler");
34
35
  const skillsLib = require("./skills");
@@ -672,23 +673,45 @@ register({
672
673
  // delegate to the control plane so the deployment can be rolled with a
673
674
  // fresh image pull. The local npm path can't do that.
674
675
  if (process.env.AGENTSPACE_POD_TOKEN && process.env.AGENTSPACE_API_URL) {
676
+ // Redelivery guard: the rollout SIGTERMs this pod mid-turn and Telegram
677
+ // redelivers the still-unconfirmed /upgrade update to the rebooted pod.
678
+ // Unguarded, each replay triggered another rollout (one /upgrade was
679
+ // observed to cause five). A fresh marker means the request was already
680
+ // honoured — by this boot or the previous one.
681
+ const since = upgradeInFlightSince();
682
+ if (since) {
683
+ const bootedAt = Date.now() - Math.round(process.uptime() * 1000);
684
+ if (since < bootedAt) {
685
+ await send(`That upgrade already went through — I restarted onto the freshly pulled image and I'm running v${CURRENT_VERSION}. Skipping the duplicate trigger.`);
686
+ } else {
687
+ const mins = Math.max(1, Math.round((Date.now() - since) / 60000));
688
+ await send(`An upgrade rollout is already in flight (requested ~${mins} min ago) — not triggering another. If I seem stuck, the guard clears 10 minutes after the request.`);
689
+ }
690
+ return;
691
+ }
692
+ // Ack BEFORE triggering: once the control plane rolls the deployment we
693
+ // can be SIGTERM'd at any moment, and an unsent ack dies with us.
694
+ await send("Upgrade requested — AgentSpace will pull latest image and restart, see you in a minute.");
695
+ recordUpgradeRequested();
675
696
  try {
676
697
  const result = await requestAgentSpaceUpgrade();
677
698
  if (result && (result.status === 202 || result.status === 200)) {
678
- await send("Upgrade requested — AgentSpace will pull latest image and restart, see you in a minute.");
679
699
  return;
680
700
  }
681
701
  // A client-side timeout is not a failure: the control plane triggers the
682
702
  // rollout fire-and-forget, and a pod being rolled is exactly when its own
683
703
  // in-flight request gets cut off. Framing it as "failed" invites a retry
684
- // storm of redundant rollouts, so surface it as pending instead.
704
+ // storm of redundant rollouts, so surface it as pending instead — and
705
+ // keep the marker, since the rollout is most likely underway.
685
706
  if (result && result.status === 0 && /timeout/i.test(result.body || "")) {
686
707
  await send("Upgrade request sent but the reply timed out — likely because I'm already restarting. Give it a minute before re-running; I should come back on the new image.");
687
708
  return;
688
709
  }
710
+ clearUpgradeMarker();
689
711
  await send(`Upgrade request failed (status ${result?.status || "?"}). ${(result?.body || "").slice(0, 300)}`);
690
712
  return;
691
713
  } catch (e) {
714
+ clearUpgradeMarker();
692
715
  await send(`Upgrade request error: ${e.message || e}`);
693
716
  return;
694
717
  }
@@ -0,0 +1,42 @@
1
+ // Guards /upgrade on AgentSpace-managed pods against the redelivery loop:
2
+ // the control-plane rollout SIGTERMs this pod mid-turn, Telegram long-polling
3
+ // keeps no persisted offset, so the still-unconfirmed /upgrade update is
4
+ // redelivered to the freshly booted pod — which would trigger ANOTHER rollout
5
+ // (observed: one /upgrade → five rollouts in three minutes). A fresh marker
6
+ // on disk tells the handler the request was already honoured.
7
+
8
+ const fs = require("fs");
9
+ const path = require("path");
10
+ const { CONFIG_DIR } = require("./config");
11
+
12
+ const FILE = path.join(CONFIG_DIR, "upgrade-in-flight.json");
13
+ // An image pull + pod recreate completes well inside this window; after it a
14
+ // re-run of /upgrade is treated as a genuine new request.
15
+ const FRESH_MS = 10 * 60 * 1000;
16
+
17
+ // Call right before triggering the rollout. Best-effort: never throws.
18
+ function recordUpgradeRequested() {
19
+ try {
20
+ fs.writeFileSync(FILE, JSON.stringify({ at: Date.now() }));
21
+ } catch (e) { /* the recorder must never itself throw */ }
22
+ }
23
+
24
+ // Epoch ms of the in-flight upgrade request, or null when none is fresh.
25
+ // Deliberately does NOT clear the file: Telegram may redeliver more than one
26
+ // unconfirmed copy of the command, so the guard must hold until it ages out.
27
+ function upgradeInFlightSince() {
28
+ try {
29
+ const rec = JSON.parse(fs.readFileSync(FILE, "utf8"));
30
+ if (rec && typeof rec.at === "number" && Date.now() - rec.at <= FRESH_MS) {
31
+ return rec.at;
32
+ }
33
+ } catch (e) { /* absent/unreadable ⇒ no upgrade in flight */ }
34
+ return null;
35
+ }
36
+
37
+ // Call when the trigger definitively failed, so an immediate retry is allowed.
38
+ function clearUpgradeMarker() {
39
+ try { fs.unlinkSync(FILE); } catch (e) {}
40
+ }
41
+
42
+ module.exports = { recordUpgradeRequested, upgradeInFlightSince, clearUpgradeMarker };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@inetafrica/open-claudia",
3
- "version": "3.0.31",
3
+ "version": "3.0.34",
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": {
@@ -32,20 +32,15 @@ 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.31 approved by Sumeet 2026-07-16 ("push out a new version"): re-cuts the
36
- // v3.0.30 approval-card UX after that tag's build tripped THIS guard — the pin
37
- // below wasn't bumped alongside package.json, so pretest asserted 3.0.29 and the
38
- // build failed (docker skipped, nothing published). The UX rework: the owner's
39
- // approval card now shows "From <name>: <ask>" plus the proposed reply/action
40
- // (inbound text + speaker name threaded through context -> escalate -> approval
41
- // record). Approve/Deny edits the card in place (fresh-message fallback when the
42
- // adapter can't edit). "Make a rule" approves + delivers, then asks "just <name>
43
- // or everyone?" -> saved to the per-person mandate or a new owner-editable global
44
- // guardrails store (external-guardrails.md, /guardrails command). The enforcer
45
- // now vets every external reply against three layers: hardcoded security floor /
46
- // global guardrails / per-person mandate. No autonomy change: default-deny
47
- // stands, the guard still fails closed, and only the owner can create rules.
48
- assert.strictEqual(pkg.version, "3.0.31", "release version must remain unchanged without explicit approval");
35
+ // 3.0.34 approved by Sumeet 2026-07-17 (Rahil's triple-reboot screenshot +
36
+ // "explain and fix"): /upgrade redelivery guard. On AgentSpace pods the
37
+ // rollout SIGTERMs the bot mid-turn, Telegram long-polling holds no persisted
38
+ // offset, and the unconfirmed /upgrade update is redelivered to the rebooted
39
+ // pod — each replay triggered ANOTHER rollout (one /upgrade → five rollouts,
40
+ // deployment revisions 47-51). Fix: upgrade-in-flight marker in CONFIG_DIR
41
+ // guards the handler, and the ack is sent BEFORE the trigger so it can't be
42
+ // lost to the SIGTERM.
43
+ assert.strictEqual(pkg.version, "3.0.34", "release version must remain unchanged without explicit approval");
49
44
  for (const keyword of ["claude", "codex", "coding-agent", "provider-agnostic"]) {
50
45
  assert.ok(pkg.keywords.includes(keyword), `package keyword missing: ${keyword}`);
51
46
  }