@inetafrica/open-claudia 3.0.16 → 3.0.18
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 +15 -0
- package/channels/spaces/adapter.js +94 -7
- package/channels/spaces/state.js +143 -1
- package/core/access.js +3 -3
- package/core/config.js +26 -9
- package/core/connected-apps.js +12 -0
- package/core/handlers.js +50 -0
- package/core/loopback.js +4 -5
- package/core/system-prompt.js +4 -1
- package/package.json +2 -2
- package/test-provider-language.js +9 -6
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,20 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## v3.0.18 — Spaces reconciles its task list (assigned tasks no longer silently missed)
|
|
4
|
+
|
|
5
|
+
- **Assigning a task to the bot now actually engages it.** An assignment creates the task but emits **no notification**, and the conversational plane only woke on a socket "notification" push — so a task assigned to the bot could sit there while the bot never noticed (confirmed live: bot could `listTasks` its assigned task, but had 0 notifications and never engaged). The adapter now runs a self-contained **reconciliation poll**: on its own timer it calls `listTasks()` (bot-scoped to its assigned tasks) and synthesises a `task_assigned` engage for any active task it hasn't engaged yet.
|
|
6
|
+
- **Robust to socket churn.** The Spaces socket transport reconnects constantly; a push emitted during a gap is lost with no replay. The timer also re-runs the existing notification catch-up `poll()` and, crucially, doesn't depend on the connected-apps poller (which can be disabled) — the Spaces plane drives itself. Cadence is `OC_SPACES_POLL_MS` (default 60s).
|
|
7
|
+
- **Per-task engaged-dedup.** Engagement is now tracked per task (`markTaskEngaged`/`taskEngaged`), not per notification-id, so a task the socket already handled is skipped by reconcile and vice-versa — no double replies. Terminal (completed/archived/cancelled) tasks are skipped.
|
|
8
|
+
- **A successful `listTasks()` is a connection signal** (`markRestConnected`) — the third leg of the hybrid auto-enable, independent of the flaky socket. `/spaces status` now shows a "Last poll" line. Still owner-gated: `/spaces pause` stops replies; reconcile records connection but engages nothing while paused/off.
|
|
9
|
+
- **Agent-space / OpenClaw:** no new deps, no new required env (one optional `OC_SPACES_POLL_MS`), no schema change; Docker image builds identically (FROM the pre-baked `:base`). Existing pods pick this up on their next approved upgrade.
|
|
10
|
+
|
|
11
|
+
## v3.0.17 — Spaces auto-enables on connection; control moves into chat
|
|
12
|
+
|
|
13
|
+
- **The Spaces conversational plane no longer hides behind an env flag.** `OC_SPACES_CONVERSATIONAL` is gone. Auto-replies now enable themselves the moment the bot is **connected** to Spaces, on a hybrid signal: central authoritatively lists Spaces among the bot's connected apps, **or** the socket has actually received a Spaces notification (which only happens for a task the bot is a member of). Either one flips it on — so it works today over the socket even before the central `/api/principal/applications` endpoint is deployed. It stays self-gating: no Spaces membership ⇒ no notifications ⇒ no replies.
|
|
14
|
+
- **All control is in chat now — nothing to set on the pod.** New owner command `/spaces`: `status` (connection + whether replies are live + task mode), `pause` / `resume` (the off-switch for auto-replies; mentions still hit the radar, I just don't answer), and `mode advisory|autonomous|off` (task-working posture, overriding the env default live). State persists in `spaces-state.json`; the owner never needs filesystem or env access.
|
|
15
|
+
- **The Spaces channel auto-includes whenever `KAZEE_BOT_TOKEN` is set**, even if `CHANNELS` doesn't list it — Spaces is fixed infra on the same token as Kazee chat, so "has token ⇒ Spaces available" is what makes auto-enable reachable without editing env.
|
|
16
|
+
- **Agent-space / OpenClaw:** no new deps, no new env required, no schema change; Docker image builds identically (FROM the pre-baked `:base`). Existing pods pick this up on their next approved upgrade.
|
|
17
|
+
|
|
3
18
|
## v3.0.16 — Spaces task loop + approval thread-mirror (dormant)
|
|
4
19
|
|
|
5
20
|
- **Kazee Spaces connected-apps gains its two remaining pieces, both shipped OFF.** The task-working loop runs in **advisory** mode by default: when the bot engages a Spaces task thread, its turns for that task are serialized (one at a time, while different tasks proceed in parallel), a persistent cursor + cross-restart dedup lives in `spaces-state.json`, and a new `OC_SPACES_TASK_LOOP` mode (`advisory` | `autonomous` | `off`) selects the framing. The **approval thread-mirror** routes any write/destructive action from a Spaces-origin turn to the owner's Telegram Approve/Deny button (fail-closed if no reachable owner channel — the run stays blocked rather than posting a dead button into the thread) and mirrors a non-binding status comment into the task thread on request and on outcome.
|
|
@@ -25,19 +25,25 @@ const spacesState = require("./state");
|
|
|
25
25
|
const ENGAGE_TYPES = new Set(["mention", "reply", "task_assigned"]);
|
|
26
26
|
|
|
27
27
|
class SpacesAdapter {
|
|
28
|
-
constructor({ id = "spaces", url = "https://api.spaces.kazee.africa", token, ownerUserId = "",
|
|
28
|
+
constructor({ id = "spaces", url = "https://api.spaces.kazee.africa", token, ownerUserId = "", taskLoopMode = "advisory", pollMs }) {
|
|
29
29
|
this.id = id;
|
|
30
30
|
this.type = "spaces";
|
|
31
31
|
this.url = url.replace(/\/$/, "");
|
|
32
32
|
this.token = token;
|
|
33
33
|
this.ownerUserId = ownerUserId || "";
|
|
34
|
-
|
|
35
|
-
this.
|
|
34
|
+
// Env default posture; the live value is chat-overridable via spacesState.
|
|
35
|
+
this.taskLoopModeDefault = taskLoopMode || "advisory";
|
|
36
|
+
// Self-driving reconciliation cadence. The connected-apps poller used to be
|
|
37
|
+
// the only thing calling poll(); it can be disabled, so the adapter drives
|
|
38
|
+
// its own timer and never depends on it.
|
|
39
|
+
this.pollMs = Number(pollMs) > 0 ? Number(pollMs) : 60000;
|
|
36
40
|
this.client = new SpacesClient({ baseUrl: this.url, token });
|
|
37
41
|
this._listeners = { message: new Set(), action: new Set() };
|
|
38
42
|
this._socket = null;
|
|
39
43
|
this._seen = new Set(); // notification-id dedup across socket + poll
|
|
40
44
|
this._taskChains = new Map(); // taskId -> tail promise, for per-task serialization
|
|
45
|
+
this._pollTimer = null;
|
|
46
|
+
this._initTimer = null;
|
|
41
47
|
}
|
|
42
48
|
|
|
43
49
|
on(event, fn) {
|
|
@@ -62,15 +68,32 @@ class SpacesAdapter {
|
|
|
62
68
|
onDisconnect: (reason) => console.log(`Spaces socket disconnected: ${reason}`),
|
|
63
69
|
onError: (err) => console.error("Spaces socket error:", err.message),
|
|
64
70
|
});
|
|
71
|
+
// Self-contained catch-up loop: reconcile assigned tasks (which emit no
|
|
72
|
+
// notification) and replay any unread notifications the socket missed during
|
|
73
|
+
// a reconnect gap. Runs regardless of the connected-apps poller. unref so it
|
|
74
|
+
// never holds the process open on its own.
|
|
75
|
+
const tick = () => { this._tick(); };
|
|
76
|
+
this._pollTimer = setInterval(tick, this.pollMs);
|
|
77
|
+
if (this._pollTimer.unref) this._pollTimer.unref();
|
|
78
|
+
// Give the socket a moment to settle, then do an immediate first pass.
|
|
79
|
+
this._initTimer = setTimeout(tick, 3000);
|
|
80
|
+
if (this._initTimer.unref) this._initTimer.unref();
|
|
65
81
|
}
|
|
66
82
|
|
|
67
83
|
async stop() {
|
|
68
84
|
try { this._socket?.disconnect?.(); } catch (e) {}
|
|
69
85
|
this._socket = null;
|
|
86
|
+
if (this._pollTimer) { clearInterval(this._pollTimer); this._pollTimer = null; }
|
|
87
|
+
if (this._initTimer) { clearTimeout(this._initTimer); this._initTimer = null; }
|
|
70
88
|
}
|
|
71
89
|
|
|
72
|
-
|
|
73
|
-
|
|
90
|
+
async _tick() {
|
|
91
|
+
await this.reconcile();
|
|
92
|
+
await this.poll();
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// REST catch-up for NOTIFICATION-bearing events (mentions, replies) the socket
|
|
96
|
+
// missed during a reconnect gap. Read-only.
|
|
74
97
|
async poll() {
|
|
75
98
|
try {
|
|
76
99
|
const notifications = await this.client.listNotifications({ limit: 40, unreadOnly: true });
|
|
@@ -81,10 +104,63 @@ class SpacesAdapter {
|
|
|
81
104
|
}
|
|
82
105
|
}
|
|
83
106
|
|
|
107
|
+
// Reconcile against the task list. Assigning a task to the bot creates the task
|
|
108
|
+
// but emits NO notification, so poll() (notification-based) can never see it —
|
|
109
|
+
// this is why an assigned task silently failed to engage. listTasks() on the
|
|
110
|
+
// bot's own token returns exactly the tasks assigned to it, so we synthesise a
|
|
111
|
+
// task_assigned event for any active task we haven't engaged yet. Per-task
|
|
112
|
+
// engaged-dedup (not notification-id) means a task the socket already handled
|
|
113
|
+
// is skipped here. Read-only; the only write is the eventual reply, gated as
|
|
114
|
+
// usual. Success is also membership proof independent of the flaky socket.
|
|
115
|
+
async reconcile() {
|
|
116
|
+
let tasks;
|
|
117
|
+
try {
|
|
118
|
+
tasks = await this.client.listTasks();
|
|
119
|
+
} catch (e) {
|
|
120
|
+
console.error("Spaces reconcile error:", e.message);
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
if (!Array.isArray(tasks)) return;
|
|
124
|
+
// Being able to list our own tasks proves we're connected — the REST leg of
|
|
125
|
+
// the hybrid signal, which survives socket churn.
|
|
126
|
+
try { spacesState.markRestConnected(); } catch (e) {}
|
|
127
|
+
|
|
128
|
+
let active = false;
|
|
129
|
+
try { active = spacesState.conversationalActive(); } catch (e) {}
|
|
130
|
+
if (!active) return; // connection recorded; engagement stays gated (paused/off)
|
|
131
|
+
|
|
132
|
+
const TERMINAL = new Set(["completed", "archived", "cancelled", "done"]);
|
|
133
|
+
for (const task of tasks) {
|
|
134
|
+
const taskId = String(task.id || task._id || "");
|
|
135
|
+
if (!taskId || task.isArchived) continue;
|
|
136
|
+
if (TERMINAL.has(String(task.status || "").toLowerCase())) continue;
|
|
137
|
+
try { if (spacesState.taskEngaged(taskId)) continue; } catch (e) {}
|
|
138
|
+
const spaceId = task.spaceId && typeof task.spaceId === "object"
|
|
139
|
+
? (task.spaceId.id || task.spaceId._id || null)
|
|
140
|
+
: (task.spaceId || null);
|
|
141
|
+
const synthetic = {
|
|
142
|
+
id: `recon:${taskId}`,
|
|
143
|
+
type: "task_assigned",
|
|
144
|
+
targetType: "task",
|
|
145
|
+
targetId: taskId,
|
|
146
|
+
title: task.title || "",
|
|
147
|
+
spaceId,
|
|
148
|
+
actorId: task.creatorId || "",
|
|
149
|
+
};
|
|
150
|
+
this._handleNotification(synthetic, "reconcile");
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
84
154
|
_notificationId(payload) {
|
|
85
155
|
return String(payload?.id || payload?._id || "");
|
|
86
156
|
}
|
|
87
157
|
|
|
158
|
+
// Live task-loop posture: chat override (spacesState) wins over the env default.
|
|
159
|
+
_taskLoopMode() {
|
|
160
|
+
try { return spacesState.taskLoopMode(this.taskLoopModeDefault); }
|
|
161
|
+
catch (e) { return this.taskLoopModeDefault; }
|
|
162
|
+
}
|
|
163
|
+
|
|
88
164
|
// Notifications arrive in two shapes: the socket payload
|
|
89
165
|
// (broadcastNotification: flat {type,targetType,targetId,actorId,actorName})
|
|
90
166
|
// and the REST row ({category, actorId:{...}, sourceEntityId, data.payload}).
|
|
@@ -137,13 +213,21 @@ class SpacesAdapter {
|
|
|
137
213
|
const n = this._normalize(payload);
|
|
138
214
|
if (!n.taskId) return;
|
|
139
215
|
|
|
216
|
+
// Receiving any Spaces notification proves membership → mark connected. This
|
|
217
|
+
// is the socket half of the hybrid "connected ⇒ conversational" signal; no
|
|
218
|
+
// env flag needed to turn auto-replies on.
|
|
219
|
+
try { spacesState.markSocketConnected(); } catch (e) {}
|
|
220
|
+
|
|
140
221
|
// Advance this task's cursor (awareness events included) so it always
|
|
141
222
|
// reflects the last activity we observed on the task.
|
|
142
223
|
try { spacesState.recordCursor(n.taskId, { notificationId: id, type: n.type }); } catch (e) {}
|
|
143
224
|
|
|
144
225
|
// Awareness-only types never auto-trigger a turn. They still fire an
|
|
145
226
|
// "awareness" listener (if any) so the poller can track/surface them.
|
|
146
|
-
|
|
227
|
+
// conversationalActive() = connected (central or socket) AND not owner-muted.
|
|
228
|
+
let active = false;
|
|
229
|
+
try { active = spacesState.conversationalActive(); } catch (e) {}
|
|
230
|
+
const engage = active && ENGAGE_TYPES.has(n.type);
|
|
147
231
|
this._emit("awareness", { adapter: this, notificationId: id, source, ...n });
|
|
148
232
|
if (!engage) return;
|
|
149
233
|
|
|
@@ -157,9 +241,12 @@ class SpacesAdapter {
|
|
|
157
241
|
text: n.content || `[${n.type} on task ${n.title || n.taskId}]`,
|
|
158
242
|
messageId: id,
|
|
159
243
|
from: { id: n.actorId, name: n.actorName },
|
|
160
|
-
spaces: { taskId: n.taskId, spaceId: n.spaceId, notificationType: n.type, deepLink: n.deepLink, title: n.title, mode: this.
|
|
244
|
+
spaces: { taskId: n.taskId, spaceId: n.spaceId, notificationType: n.type, deepLink: n.deepLink, title: n.title, mode: this._taskLoopMode() },
|
|
161
245
|
raw: payload,
|
|
162
246
|
};
|
|
247
|
+
// Record the engagement so the reconciliation poll won't re-synthesise an
|
|
248
|
+
// assignment for a task the socket/poll already handled (per-task dedup).
|
|
249
|
+
try { spacesState.markTaskEngaged(n.taskId); } catch (e) {}
|
|
163
250
|
// Per-task serialization: a task's turns run one at a time so two comments
|
|
164
251
|
// on the same task can't spawn concurrent, conflicting agent runs. Other
|
|
165
252
|
// tasks proceed in parallel.
|
package/channels/spaces/state.js
CHANGED
|
@@ -21,9 +21,33 @@ let store = null;
|
|
|
21
21
|
function load() {
|
|
22
22
|
if (store) return store;
|
|
23
23
|
const raw = readJsonWithFallback(STATE_FILE, null) || {};
|
|
24
|
+
const rawConn = raw.connection && typeof raw.connection === "object" ? raw.connection : {};
|
|
25
|
+
const rawCtrl = raw.control && typeof raw.control === "object" ? raw.control : {};
|
|
24
26
|
store = {
|
|
25
27
|
processed: Array.isArray(raw.processed) ? raw.processed.slice(-PROCESSED_CAP) : [],
|
|
26
28
|
tasks: raw.tasks && typeof raw.tasks === "object" ? raw.tasks : {},
|
|
29
|
+
// Connection signal (hybrid): central authoritatively lists Spaces among the
|
|
30
|
+
// bot's connected apps, OR the socket has actually received a Spaces
|
|
31
|
+
// notification (which only happens for a task the bot is a member of). Either
|
|
32
|
+
// one means "connected" — no env flag required.
|
|
33
|
+
connection: {
|
|
34
|
+
central: !!rawConn.central,
|
|
35
|
+
centralAt: rawConn.centralAt || 0,
|
|
36
|
+
socketFirstAt: rawConn.socketFirstAt || 0,
|
|
37
|
+
socketLastAt: rawConn.socketLastAt || 0,
|
|
38
|
+
// REST reconciliation proof: a successful listTasks() means the bot can
|
|
39
|
+
// read its own Spaces tasks, which is membership proof independent of the
|
|
40
|
+
// socket (whose transport churns). Third leg of the hybrid signal.
|
|
41
|
+
restFirstAt: rawConn.restFirstAt || 0,
|
|
42
|
+
restLastAt: rawConn.restLastAt || 0,
|
|
43
|
+
},
|
|
44
|
+
// Owner control, set from chat (/spaces …) — never from env/filesystem, which
|
|
45
|
+
// the owner can't reach on a pod. `muted` is the off-switch for auto-replies;
|
|
46
|
+
// `taskLoopMode` overrides the env default posture.
|
|
47
|
+
control: {
|
|
48
|
+
muted: !!rawCtrl.muted,
|
|
49
|
+
taskLoopMode: rawCtrl.taskLoopMode || null,
|
|
50
|
+
},
|
|
27
51
|
};
|
|
28
52
|
return store;
|
|
29
53
|
}
|
|
@@ -68,9 +92,127 @@ function getCursor(taskId) {
|
|
|
68
92
|
return s.tasks[String(taskId || "")] || null;
|
|
69
93
|
}
|
|
70
94
|
|
|
95
|
+
// Mark that a task has had an agent turn emitted for it (from any source: a
|
|
96
|
+
// socket/poll notification OR the reconciliation poll). The reconciliation poll
|
|
97
|
+
// reads this to avoid re-engaging a task the socket already handled — dedup is
|
|
98
|
+
// per-task, not per-notification-id, because a synthesised assignment has no
|
|
99
|
+
// real notification id to match against.
|
|
100
|
+
function markTaskEngaged(taskId) {
|
|
101
|
+
const id = String(taskId || "");
|
|
102
|
+
if (!id) return;
|
|
103
|
+
const s = load();
|
|
104
|
+
const cur = s.tasks[id] || { count: 0 };
|
|
105
|
+
cur.engaged = true;
|
|
106
|
+
cur.lastEngagedAt = new Date().toISOString();
|
|
107
|
+
s.tasks[id] = cur;
|
|
108
|
+
save();
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function taskEngaged(taskId) {
|
|
112
|
+
const s = load();
|
|
113
|
+
const t = s.tasks[String(taskId || "")];
|
|
114
|
+
return !!(t && t.engaged);
|
|
115
|
+
}
|
|
116
|
+
|
|
71
117
|
function snapshot() {
|
|
72
118
|
const s = load();
|
|
73
119
|
return { tasks: s.tasks, processedCount: s.processed.length };
|
|
74
120
|
}
|
|
75
121
|
|
|
76
|
-
|
|
122
|
+
// --- Connection signal -----------------------------------------------------
|
|
123
|
+
|
|
124
|
+
// Called on every inbound Spaces notification. Receiving one is proof the bot is
|
|
125
|
+
// a member of that task's space, so it flips the socket-connected signal on.
|
|
126
|
+
function markSocketConnected() {
|
|
127
|
+
const s = load();
|
|
128
|
+
const now = Date.now();
|
|
129
|
+
if (!s.connection.socketFirstAt) s.connection.socketFirstAt = now;
|
|
130
|
+
s.connection.socketLastAt = now;
|
|
131
|
+
save();
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Called by the adapter's reconciliation poll after a successful listTasks().
|
|
135
|
+
// Being able to list our own tasks is membership proof that survives socket
|
|
136
|
+
// churn — so it's a first-class connection signal, not just a socket fallback.
|
|
137
|
+
function markRestConnected() {
|
|
138
|
+
const s = load();
|
|
139
|
+
const now = Date.now();
|
|
140
|
+
if (!s.connection.restFirstAt) s.connection.restFirstAt = now;
|
|
141
|
+
s.connection.restLastAt = now;
|
|
142
|
+
save();
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Called by the connected-apps poller with central's authoritative answer to
|
|
146
|
+
// "is Spaces among my connected apps?". Persisted so a restart keeps the signal
|
|
147
|
+
// until the next poll refreshes it.
|
|
148
|
+
function setCentralConnected(connected) {
|
|
149
|
+
const s = load();
|
|
150
|
+
const val = !!connected;
|
|
151
|
+
if (s.connection.central === val && s.connection.centralAt) return;
|
|
152
|
+
s.connection.central = val;
|
|
153
|
+
s.connection.centralAt = Date.now();
|
|
154
|
+
save();
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Hybrid: connected if central says so, or the socket has ever delivered a
|
|
158
|
+
// Spaces notification. Membership is the real-world gate — you can't receive a
|
|
159
|
+
// notification for a space you're not in — so socket-ever-connected is a safe,
|
|
160
|
+
// sticky signal.
|
|
161
|
+
function isConnected() {
|
|
162
|
+
const s = load();
|
|
163
|
+
return !!(s.connection.central || s.connection.socketFirstAt || s.connection.restFirstAt);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// --- Owner control (from chat) ---------------------------------------------
|
|
167
|
+
|
|
168
|
+
function setMuted(muted) {
|
|
169
|
+
const s = load();
|
|
170
|
+
s.control.muted = !!muted;
|
|
171
|
+
save();
|
|
172
|
+
return s.control.muted;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function setTaskLoopMode(mode) {
|
|
176
|
+
const s = load();
|
|
177
|
+
const m = String(mode || "").toLowerCase();
|
|
178
|
+
s.control.taskLoopMode = ["advisory", "autonomous", "off"].includes(m) ? m : null;
|
|
179
|
+
save();
|
|
180
|
+
return s.control.taskLoopMode;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// The task-working posture for engaged Spaces turns: chat override wins, else the
|
|
184
|
+
// env default the caller passes ("advisory" unless overridden).
|
|
185
|
+
function taskLoopMode(envDefault = "advisory") {
|
|
186
|
+
const s = load();
|
|
187
|
+
return s.control.taskLoopMode || String(envDefault || "advisory").toLowerCase();
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// The single gate the adapter/loopback/access all read: auto-replies are live
|
|
191
|
+
// when the bot is connected to Spaces AND the owner hasn't paused them.
|
|
192
|
+
function conversationalActive() {
|
|
193
|
+
const s = load();
|
|
194
|
+
return isConnected() && !s.control.muted;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// For /spaces status.
|
|
198
|
+
function controlSnapshot() {
|
|
199
|
+
const s = load();
|
|
200
|
+
return {
|
|
201
|
+
connected: isConnected(),
|
|
202
|
+
central: s.connection.central,
|
|
203
|
+
socketFirstAt: s.connection.socketFirstAt,
|
|
204
|
+
socketLastAt: s.connection.socketLastAt,
|
|
205
|
+
restFirstAt: s.connection.restFirstAt,
|
|
206
|
+
restLastAt: s.connection.restLastAt,
|
|
207
|
+
muted: s.control.muted,
|
|
208
|
+
conversationalActive: conversationalActive(),
|
|
209
|
+
taskLoopMode: taskLoopMode(),
|
|
210
|
+
taskLoopOverride: s.control.taskLoopMode,
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
module.exports = {
|
|
215
|
+
markProcessed, recordCursor, getCursor, markTaskEngaged, taskEngaged, snapshot, STATE_FILE,
|
|
216
|
+
markSocketConnected, markRestConnected, setCentralConnected, isConnected,
|
|
217
|
+
setMuted, setTaskLoopMode, taskLoopMode, conversationalActive, controlSnapshot,
|
|
218
|
+
};
|
package/core/access.js
CHANGED
|
@@ -54,11 +54,11 @@ function isChatAuthorized(chatId) {
|
|
|
54
54
|
if (!id) return false;
|
|
55
55
|
// Spaces task threads authorize by participation: the bot only receives a
|
|
56
56
|
// Spaces notification for a task it follows/was mentioned on, so the inbound
|
|
57
|
-
// itself is the authorization signal.
|
|
58
|
-
//
|
|
57
|
+
// itself is the authorization signal. Effective only while the conversational
|
|
58
|
+
// plane is active (connected to Spaces AND the owner hasn't paused replies).
|
|
59
59
|
try {
|
|
60
60
|
if (currentTransport() === "spaces"
|
|
61
|
-
&& require("
|
|
61
|
+
&& require("../channels/spaces/state").conversationalActive()) {
|
|
62
62
|
return true;
|
|
63
63
|
}
|
|
64
64
|
} catch (e) {}
|
package/core/config.js
CHANGED
|
@@ -251,16 +251,15 @@ function loadChannels() {
|
|
|
251
251
|
url,
|
|
252
252
|
token,
|
|
253
253
|
ownerUserId: config.SPACES_OWNER_USER_ID || "",
|
|
254
|
-
//
|
|
255
|
-
//
|
|
256
|
-
//
|
|
257
|
-
//
|
|
258
|
-
|
|
259
|
-
//
|
|
260
|
-
//
|
|
261
|
-
// (work to completion; writes still hit the owner-approval gate), or
|
|
262
|
-
// "off". Only shapes the agent's framing; approval safety is separate.
|
|
254
|
+
// Conversational auto-replies are no longer env-gated: the adapter
|
|
255
|
+
// enables them itself once Spaces is CONNECTED (central lists it, or
|
|
256
|
+
// the socket receives a notification) and the owner hasn't paused via
|
|
257
|
+
// /spaces. The socket always starts here for awareness.
|
|
258
|
+
// taskLoopMode is only the DEFAULT posture; /spaces mode overrides it
|
|
259
|
+
// live. "advisory" = read/reason/recommend; "autonomous" = work to
|
|
260
|
+
// completion (writes still hit the owner-approval gate); "off".
|
|
263
261
|
taskLoopMode: String(config.OC_SPACES_TASK_LOOP || process.env.OC_SPACES_TASK_LOOP || "advisory").toLowerCase(),
|
|
262
|
+
pollMs: Number(config.OC_SPACES_POLL_MS || process.env.OC_SPACES_POLL_MS) || 60000,
|
|
264
263
|
},
|
|
265
264
|
});
|
|
266
265
|
} else if (type === "voice") {
|
|
@@ -283,6 +282,24 @@ function loadChannels() {
|
|
|
283
282
|
console.error(`Unknown channel type: ${type} — skipping.`);
|
|
284
283
|
}
|
|
285
284
|
}
|
|
285
|
+
// Auto-include the Spaces channel whenever the bot has its own kzb_ token, even
|
|
286
|
+
// if CHANNELS didn't list it. Spaces is fixed infra on the same token as Kazee
|
|
287
|
+
// chat, and the owner can't edit CHANNELS on a pod — so "have token ⇒ Spaces
|
|
288
|
+
// available" is what makes auto-enable reachable without touching env. It's
|
|
289
|
+
// self-gating: no Spaces membership ⇒ no notifications ⇒ no replies.
|
|
290
|
+
if ((config.KAZEE_BOT_TOKEN) && !channels.some((c) => c.type === "spaces")) {
|
|
291
|
+
channels.push({
|
|
292
|
+
id: "spaces",
|
|
293
|
+
type: "spaces",
|
|
294
|
+
opts: {
|
|
295
|
+
url: config.SPACES_URL || SPACES_DEFAULT_URL,
|
|
296
|
+
token: config.KAZEE_BOT_TOKEN,
|
|
297
|
+
ownerUserId: config.SPACES_OWNER_USER_ID || "",
|
|
298
|
+
taskLoopMode: String(config.OC_SPACES_TASK_LOOP || process.env.OC_SPACES_TASK_LOOP || "advisory").toLowerCase(),
|
|
299
|
+
pollMs: Number(config.OC_SPACES_POLL_MS || process.env.OC_SPACES_POLL_MS) || 60000,
|
|
300
|
+
},
|
|
301
|
+
});
|
|
302
|
+
}
|
|
286
303
|
return channels;
|
|
287
304
|
}
|
|
288
305
|
|
package/core/connected-apps.js
CHANGED
|
@@ -169,6 +169,18 @@ async function tick() {
|
|
|
169
169
|
console.error("connected-apps: apps refresh failed:", e.message);
|
|
170
170
|
}
|
|
171
171
|
}
|
|
172
|
+
// Central's authoritative half of the "connected ⇒ conversational" signal:
|
|
173
|
+
// tell the Spaces conversational plane whether central lists Spaces among this
|
|
174
|
+
// bot's apps. The socket half is set independently when a notification lands,
|
|
175
|
+
// so auto-replies work even before this endpoint is deployed.
|
|
176
|
+
try {
|
|
177
|
+
const spacesConnected = store.apps.some((a) => {
|
|
178
|
+
const c = connectorFor(a);
|
|
179
|
+
return c && c.appKey === "spaces";
|
|
180
|
+
});
|
|
181
|
+
require("../channels/spaces/state").setCentralConnected(spacesConnected);
|
|
182
|
+
} catch (e) {}
|
|
183
|
+
|
|
172
184
|
for (const app of store.apps) {
|
|
173
185
|
if (!connectorFor(app)) continue;
|
|
174
186
|
await pollApp(app);
|
package/core/handlers.js
CHANGED
|
@@ -793,6 +793,56 @@ register({
|
|
|
793
793
|
},
|
|
794
794
|
});
|
|
795
795
|
|
|
796
|
+
register({
|
|
797
|
+
name: "spaces", description: "Control Kazee Spaces auto-replies (status/pause/resume/mode)",
|
|
798
|
+
args: "<status | pause | resume | mode advisory|autonomous|off>", ownerOnly: true,
|
|
799
|
+
handler: async (env, { tail }) => {
|
|
800
|
+
if (!ownerEnv(env)) return;
|
|
801
|
+
let spacesState;
|
|
802
|
+
try { spacesState = require("../channels/spaces/state"); }
|
|
803
|
+
catch (e) { return send("Spaces state unavailable — the Spaces channel isn't loaded."); }
|
|
804
|
+
const parts = (tail || "").trim().split(/\s+/).filter(Boolean);
|
|
805
|
+
const op = (parts[0] || "status").toLowerCase();
|
|
806
|
+
|
|
807
|
+
const fmtTime = (ms) => (ms ? new Date(ms).toISOString().replace("T", " ").slice(0, 16) + "Z" : "never");
|
|
808
|
+
const statusText = () => {
|
|
809
|
+
const s = spacesState.controlSnapshot();
|
|
810
|
+
const src = s.central ? "central" : (s.socketFirstAt ? "socket" : (s.restFirstAt ? "poll" : "—"));
|
|
811
|
+
const replies = s.conversationalActive ? "ON" : (s.connected ? "paused" : "off (not connected)");
|
|
812
|
+
const modeLine = s.taskLoopOverride
|
|
813
|
+
? `${s.taskLoopMode} (override)`
|
|
814
|
+
: `${s.taskLoopMode} (default)`;
|
|
815
|
+
return [
|
|
816
|
+
"<b>Kazee Spaces</b>",
|
|
817
|
+
`Connected: ${s.connected ? "yes" : "no"}${s.connected ? ` (via ${src})` : ""}`,
|
|
818
|
+
`Auto-replies: ${replies}`,
|
|
819
|
+
`Task mode: ${modeLine}`,
|
|
820
|
+
`Last notification: ${fmtTime(s.socketLastAt)}`,
|
|
821
|
+
`Last poll: ${fmtTime(s.restLastAt)}`,
|
|
822
|
+
].join("\n");
|
|
823
|
+
};
|
|
824
|
+
|
|
825
|
+
if (op === "status") return send(statusText());
|
|
826
|
+
if (op === "pause") {
|
|
827
|
+
spacesState.setMuted(true);
|
|
828
|
+
return send("Spaces auto-replies <b>paused</b>. Mentions/replies still hit the radar; I just won't answer until you /spaces resume.\n\n" + statusText());
|
|
829
|
+
}
|
|
830
|
+
if (op === "resume") {
|
|
831
|
+
spacesState.setMuted(false);
|
|
832
|
+
return send("Spaces auto-replies <b>resumed</b>.\n\n" + statusText());
|
|
833
|
+
}
|
|
834
|
+
if (op === "mode") {
|
|
835
|
+
const m = (parts[1] || "").toLowerCase();
|
|
836
|
+
if (!["advisory", "autonomous", "off"].includes(m)) {
|
|
837
|
+
return send("Usage: /spaces mode <advisory|autonomous|off>\n\n• advisory — read, reason, recommend (no autonomous write chains)\n• autonomous — work to completion (writes still need your approval)\n• off — no task-mode framing");
|
|
838
|
+
}
|
|
839
|
+
spacesState.setTaskLoopMode(m);
|
|
840
|
+
return send(`Spaces task mode set to <b>${m}</b>.\n\n` + statusText());
|
|
841
|
+
}
|
|
842
|
+
return send("Usage: /spaces <status | pause | resume | mode advisory|autonomous|off>");
|
|
843
|
+
},
|
|
844
|
+
});
|
|
845
|
+
|
|
796
846
|
register({
|
|
797
847
|
name: "cluster", description: "Control this bot's own deployment (status/logs/restart/start/stop/scale/sync)",
|
|
798
848
|
args: "<status|logs|restart|start|stop|scale 0|1|sync>", ownerOnly: true,
|
package/core/loopback.js
CHANGED
|
@@ -524,15 +524,14 @@ async function handleJson(req, res, url, kind) {
|
|
|
524
524
|
// watched by non-owners, so the BINDING Approve/Deny goes to the owner's
|
|
525
525
|
// real button channel (Telegram) while the task thread gets a non-binding
|
|
526
526
|
// status mirror. Origin (channelId/adapter) still records the Spaces thread
|
|
527
|
-
// so a detached re-run wakes the agent back on the task.
|
|
528
|
-
//
|
|
529
|
-
|
|
527
|
+
// so a detached re-run wakes the agent back on the task. A Spaces run only
|
|
528
|
+
// reaches here when the conversational plane is active (connected + not
|
|
529
|
+
// muted), so any Spaces-origin approval routes to the owner.
|
|
530
530
|
let buttonAdapter = adapter;
|
|
531
531
|
let buttonChannel = channelId;
|
|
532
532
|
let approverAdapter = "";
|
|
533
533
|
let approverChannel = "";
|
|
534
|
-
const spacesOrigin = adapter.type === "spaces"
|
|
535
|
-
&& require("./project-transcripts").truthy(cfg.OC_SPACES_CONVERSATIONAL || process.env.OC_SPACES_CONVERSATIONAL, false);
|
|
534
|
+
const spacesOrigin = adapter.type === "spaces";
|
|
536
535
|
if (spacesOrigin) {
|
|
537
536
|
const owner = require("./relationship").ownerTarget();
|
|
538
537
|
const oa = owner
|
package/core/system-prompt.js
CHANGED
|
@@ -779,7 +779,10 @@ function buildSpacesTaskModeBlock() {
|
|
|
779
779
|
let transport;
|
|
780
780
|
try { transport = require("./context").currentTransport(); } catch (e) { transport = ""; }
|
|
781
781
|
if (transport !== "spaces") return "";
|
|
782
|
-
|
|
782
|
+
// Chat override (/spaces mode) wins over the env default posture.
|
|
783
|
+
const envDefault = String(config.OC_SPACES_TASK_LOOP ?? process.env.OC_SPACES_TASK_LOOP ?? "advisory").toLowerCase();
|
|
784
|
+
let mode = envDefault;
|
|
785
|
+
try { mode = require("../channels/spaces/state").taskLoopMode(envDefault); } catch (e) {}
|
|
783
786
|
if (mode === "off") return "";
|
|
784
787
|
if (mode === "autonomous") {
|
|
785
788
|
return [
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@inetafrica/open-claudia",
|
|
3
|
-
"version": "3.0.
|
|
3
|
+
"version": "3.0.18",
|
|
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 && node test-kazee-channel-health.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"
|
|
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 && 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-spaces-autoenable.js"
|
|
14
14
|
},
|
|
15
15
|
"files": [
|
|
16
16
|
"bot.js",
|
|
@@ -32,12 +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
|
-
// Spaces
|
|
37
|
-
//
|
|
38
|
-
//
|
|
39
|
-
//
|
|
40
|
-
|
|
35
|
+
// 3.0.18 approved by Sumeet 2026-07-14 ("Yeah, go for it build it push it out"):
|
|
36
|
+
// the Spaces conversational plane now also RECONCILES against its task list. An
|
|
37
|
+
// assignment creates a task but emits NO notification, and the socket-push-only
|
|
38
|
+
// path missed it — so the adapter drives its own timer that calls listTasks()
|
|
39
|
+
// (bot-scoped to assigned tasks) and synthesises a task_assigned engage for any
|
|
40
|
+
// active task it hasn't engaged, with per-task dedup so the socket path isn't
|
|
41
|
+
// doubled. A successful listTasks() is a third connection leg (markRestConnected)
|
|
42
|
+
// that survives socket churn. Builds on 3.0.17's connection-based auto-enable.
|
|
43
|
+
assert.strictEqual(pkg.version, "3.0.18", "release version must remain unchanged without explicit approval");
|
|
41
44
|
for (const keyword of ["claude", "codex", "coding-agent", "provider-agnostic"]) {
|
|
42
45
|
assert.ok(pkg.keywords.includes(keyword), `package keyword missing: ${keyword}`);
|
|
43
46
|
}
|