@inetafrica/open-claudia 3.0.33 → 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 +7 -0
- package/core/handlers.js +25 -2
- package/core/upgrade-marker.js +42 -0
- package/package.json +1 -1
- package/test-provider-language.js +9 -10
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
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
|
+
|
|
3
10
|
## v3.0.33 — Full sudo inside the pod (re-cut of v3.0.32; the pin guard bit us again)
|
|
4
11
|
|
|
5
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.
|
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
|
@@ -32,16 +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.
|
|
36
|
-
//
|
|
37
|
-
//
|
|
38
|
-
//
|
|
39
|
-
//
|
|
40
|
-
//
|
|
41
|
-
//
|
|
42
|
-
//
|
|
43
|
-
|
|
44
|
-
assert.strictEqual(pkg.version, "3.0.33", "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");
|
|
45
44
|
for (const keyword of ["claude", "codex", "coding-agent", "provider-agnostic"]) {
|
|
46
45
|
assert.ok(pkg.keywords.includes(keyword), `package keyword missing: ${keyword}`);
|
|
47
46
|
}
|