@inetafrica/open-claudia 3.0.18 → 3.0.19

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,13 @@
1
1
  # Changelog
2
2
 
3
+ ## v3.0.19 — flattened to the top level (the folder-project model is gone)
4
+
5
+ - **The bot always runs at the top-level workspace now — no more "Pick a project first."** The per-folder session model (pick a subfolder of `WORKSPACE`, scope the conversation to it) has been removed. `currentSession` defaults to the workspace root (`{ name: "Workspace", dir: WORKSPACE }`) at state creation and on every migration/load, so it is never null. This directly fixes Spaces (and any non-Telegram channel that never picked a folder) replying with a dead "Pick a project first:" project-picker instead of engaging — the picker gate was the only thing blocking it.
6
+ - **Removed the folder-picker surface.** Gone: `/projects`, `/session`, the project-selection inline keyboard, and the `show:projects` / `s:<name>` button callbacks. Also removed the four media "Pick a project first." gates (voice/audio/photo/document) and the text-handler gate in the router — all dead once a session always exists. `core/projects.js` is deleted.
7
+ - **Conversation-level controls are unchanged.** `/sessions` (list past conversations), `/new` (fresh conversation), `/continue` (resume), `/end` (end current conversation — now resets to the root instead of clearing to null) all work as before, just always scoped to the one top-level workspace.
8
+ - **`/cron add` no longer takes a `<project>` argument** — it schedules at the workspace root: `/cron add "<schedule>" "<prompt>"`. Existing crons with a stored project still resolve via the scheduler's descriptor.
9
+ - **Agent-space / OpenClaw:** no new deps, no new env, no schema change; Docker image builds identically (FROM the pre-baked `:base`). Existing pods pick this up on their next approved upgrade.
10
+
3
11
  ## v3.0.18 — Spaces reconciles its task list (assigned tasks no longer silently missed)
4
12
 
5
13
  - **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.
package/README.md CHANGED
@@ -145,15 +145,16 @@ Each provider keeps its own persistent project session. Switching doesn't lose y
145
145
 
146
146
  ### Session management
147
147
 
148
+ The bot runs at the top-level workspace; there is no per-folder project selection.
149
+
148
150
  | Command | Description |
149
151
  |---------|-------------|
150
- | `/session [<project>]` | Pick a project to work on |
151
- | `/sessions` | List past conversations for current project |
152
- | `/projects` | Browse all workspace projects |
152
+ | `/new` | Start a fresh conversation |
153
+ | `/sessions` | List past conversations |
153
154
  | `/continue` | Resume last conversation explicitly |
154
155
  | `/compact` | Summarize conversation context now |
155
156
  | `/compactwindow [<tokens> \| off \| default]` | Set the auto-compact token threshold |
156
- | `/end` | End current session |
157
+ | `/end` | End the current conversation |
157
158
 
158
159
  When you select a project, the last conversation is automatically resumed. Tap "New conversation" to start fresh.
159
160
 
@@ -352,7 +353,7 @@ Foreground turns never silently fall back to a different provider. Utility work
352
353
 
353
354
  Conversation identity is the tuple of canonical user, project, provider, and native session ID. Claude Code and Codex histories and active pointers stay separate: switching providers restores only that provider's project session and never passes one provider's native session ID to another. Session-history entries are provider-tagged; ambiguous legacy records remain visible but non-selectable.
354
355
 
355
- Model, effort, budget, permission mode, and worktree settings are stored per provider. `/new` clears only the selected project/provider pointer, while `/end` closes the project selection without deleting history. Compaction and scheduled jobs capture an immutable provider/session tuple; an explicitly configured scheduled fallback starts fresh from a provider-neutral archived brief.
356
+ Model, effort, budget, permission mode, and worktree settings are stored per provider. `/new` clears only the active provider's conversation pointer, while `/end` ends the current conversation (resetting to the top-level workspace) without deleting history. Compaction and scheduled jobs capture an immutable provider/session tuple; an explicitly configured scheduled fallback starts fresh from a provider-neutral archived brief.
356
357
 
357
358
  ## Provider migration and rollback
358
359
 
@@ -517,9 +518,9 @@ For direct npm installs, `/upgrade` updates Open Claudia itself and does not ins
517
518
  Schedule recurring tasks:
518
519
 
519
520
  ```
520
- /cron add "0 9 * * 1-5" myproject "Morning standup: summarize git changes since yesterday"
521
- /cron add "0 18 * * *" myproject "Git digest: what changed today?"
522
- /cron add "*/30 * * * *" myproject "Health check: verify the API is responding"
521
+ /cron add "0 9 * * 1-5" "Morning standup: summarize git changes since yesterday"
522
+ /cron add "0 18 * * *" "Git digest: what changed today?"
523
+ /cron add "*/30 * * * *" "Health check: verify the API is responding"
523
524
  ```
524
525
 
525
526
  Presets available via `/cron` menu. The agent can also schedule its own jobs with `open-claudia cron-add` / `schedule-wakeup` (see Background Work).
package/core/actions.js CHANGED
@@ -12,7 +12,6 @@ const {
12
12
  userOwnsProviderSession,
13
13
  } = require("./state");
14
14
  const { admitRunContext, runAgent } = require("./runner");
15
- const { listProjects, projectKeyboard } = require("./projects");
16
15
  const { isChatOwner, approveAuthRequest, denyAuthRequest, authRequestLabel } = require("./access");
17
16
  const accessStore = require("./access");
18
17
  const peopleStore = require("./people");
@@ -282,8 +281,6 @@ async function handleAction(envelope) {
282
281
 
283
282
  if (d.startsWith("ob:")) { finishOnboarding(d.slice(3)); return; }
284
283
 
285
- if (d === "show:projects") { await send("Pick:", { keyboard: projectKeyboard() }); return; }
286
- if (d.startsWith("s:")) { startSession(d.slice(2)); return; }
287
284
  if (d.startsWith("ss:")) {
288
285
  const resolved = resolveSessionCallbackAction(d.slice(3), state);
289
286
  if (resolved.status !== "ok") {
@@ -299,7 +296,7 @@ async function handleAction(envelope) {
299
296
  const expected = proj === "__workspace__" ? "Workspace" : proj;
300
297
  const result = startNewConversation({ state, projectName: expected });
301
298
  if (!result.ok) return send("That new-conversation button is stale. Open /sessions again.");
302
- await send(`Session: ${state.currentSession.name}\nNew conversation\n\nSend text, voice, or images.\n\n/sessions — switch conversation\n/session — switch project`);
299
+ await send("New conversation\n\nSend text, voice, or images.\n\n/sessions — switch conversation");
303
300
  return;
304
301
  }
305
302
  if (d === "a:continue") {
@@ -310,11 +307,9 @@ async function handleAction(envelope) {
310
307
  });
311
308
  }
312
309
  if (d === "a:end") {
313
- if (state.currentSession) {
314
- const result = endCurrentSession({ state });
315
- if (!result.ok) return send(`Could not end session: ${result.error.message}`);
316
- await send(`Ended: ${result.project}`, { keyboard: { inline_keyboard: [[{ text: "New", callback_data: "show:projects" }]] } });
317
- }
310
+ const result = endCurrentSession({ state });
311
+ if (!result.ok) return send(`Could not end conversation: ${result.error.message}`);
312
+ await send("Ended. Send anything to start a fresh conversation.");
318
313
  return;
319
314
  }
320
315
  if (d === "noop") return;
package/core/handlers.js CHANGED
@@ -14,7 +14,7 @@ const {
14
14
  const { register } = require("./commands");
15
15
  const { send, deleteMessage } = require("./io");
16
16
  const {
17
- currentState, saveState,
17
+ currentState, saveState, rootSession,
18
18
  freshUsage, getActiveProvider, getProviderSession, setProviderSession, clearProviderSession,
19
19
  getProviderSettings,
20
20
  getProjectSessions, getLastProjectSession, userOwnsProviderSession,
@@ -23,7 +23,6 @@ const {
23
23
  const { canonicalForChannel, canonicalForTelegram, normalizeCanonicalUserId, channelKey, identities, isConfiguredOwnerChannel, removeIdentityMapping } = require("./identity");
24
24
  const { isChatAuthorized, isChatOwner, recordPendingAuthRequest, authRequestLabel, hasOwner, bootstrapOwner } = require("./access");
25
25
  const { isOnboarded, startOnboarding } = require("./onboarding");
26
- const { listProjects, findProject, projectKeyboard } = require("./projects");
27
26
  const { vault } = require("./vault-store");
28
27
  const keyring = require("./keyring");
29
28
  const { redactSensitive, registerSecrets } = require("./redact");
@@ -60,10 +59,13 @@ function ownerEnv(envelope) {
60
59
  return isChatOwner(envelope.channelId);
61
60
  }
62
61
 
62
+ // Everything runs at the top-level workspace now, so there is nothing to gate
63
+ // on: ensure a root session exists and always proceed.
63
64
  function requireSession() {
64
- if (!currentState().currentSession) {
65
- send("Pick a project first:", { keyboard: projectKeyboard() });
66
- return false;
65
+ const state = currentState();
66
+ if (!state.currentSession) {
67
+ state.currentSession = rootSession();
68
+ saveState();
67
69
  }
68
70
  return true;
69
71
  }
@@ -274,11 +276,10 @@ function startNewConversation(options = {}) {
274
276
 
275
277
  function endCurrentSession(options = {}) {
276
278
  const state = options.state || currentState();
277
- if (!state.currentSession) return { ok: false, error: Object.assign(new Error("No session"), { code: "NO_SESSION" }) };
278
279
  const previous = { currentSession: state.currentSession, isFirstMessage: state.isFirstMessage };
279
- const project = state.currentSession.name;
280
- clearTransientQueue(state, "The project session ended before the queued run started");
281
- state.currentSession = null;
280
+ const project = state.currentSession?.name || "Workspace";
281
+ clearTransientQueue(state, "The conversation ended before the queued run started");
282
+ state.currentSession = rootSession();
282
283
  state.isFirstMessage = true;
283
284
  state.cancelRequested = false;
284
285
  const persisted = persistSessionControl(state, options, () => {
@@ -295,29 +296,21 @@ function activeContinueOptions(state = currentState()) {
295
296
  return sessionId ? { provider, resumeSessionId: sessionId } : null;
296
297
  }
297
298
 
299
+ // Resume a past conversation at the top-level workspace. Folder selection is
300
+ // gone; `name` is retained for the recorded project key (always "Workspace").
298
301
  function startSession(name, resumeSessionId, options = {}) {
299
302
  const state = currentState();
300
303
  const provider = options.provider || getActiveProvider(state);
301
- let projectName, projectDir;
302
- if (name === "__workspace__") {
303
- projectName = "Workspace";
304
- projectDir = WORKSPACE;
305
- } else {
306
- const result = findProject(name);
307
- if (!result) return send(`No match for "${name}".`, { keyboard: projectKeyboard() });
308
- if (Array.isArray(result)) return send("Multiple matches:", { keyboard: { inline_keyboard: result.map((p) => [{ text: p, callback_data: `s:${p}` }]) } });
309
- projectName = result;
310
- projectDir = path.join(WORKSPACE, result);
311
- }
304
+ const projectName = "Workspace";
312
305
 
313
- if (!provider) return send("Choose Claude Code or OpenAI Codex with /backend before opening a project session.");
306
+ if (!provider) return send("Choose Claude Code or OpenAI Codex with /backend before starting a conversation.");
314
307
 
315
308
  if (resumeSessionId && !userOwnsProviderSession(state.userId, projectName, provider, resumeSessionId)) {
316
- return send("That conversation is unavailable for this project and provider. Open /sessions again.");
309
+ return send("That conversation is unavailable for this provider. Open /sessions again.");
317
310
  }
318
311
 
319
- const selected = selectProjectState({ name: projectName, dir: projectDir }, { state, persist: false });
320
- if (!selected.ok) return send(`Could not select project: ${selected.error.message}`);
312
+ const selected = selectProjectState(rootSession(), { state, persist: false });
313
+ if (!selected.ok) return send(`Could not start conversation: ${selected.error.message}`);
321
314
 
322
315
  if (resumeSessionId) {
323
316
  setProviderSession(state, provider, projectName, resumeSessionId);
@@ -326,21 +319,21 @@ function startSession(name, resumeSessionId, options = {}) {
326
319
  const title = s ? s.title : "";
327
320
  state.isFirstMessage = false;
328
321
  saveState();
329
- send(`Session: ${projectName}\nResumed: ${title || resumeSessionId.slice(0, 8)}\n\nSend text, voice, or images.\n\n/sessions — switch conversation\n/session — switch project`);
322
+ send(`Resumed: ${title || resumeSessionId.slice(0, 8)}\n\nSend text, voice, or images.\n\n/sessions — switch conversation`);
330
323
  } else {
331
324
  const last = getLastProjectSession(state.userId, projectName, provider);
332
325
  if (last) {
333
326
  setProviderSession(state, provider, projectName, last.id);
334
327
  state.isFirstMessage = false;
335
328
  saveState();
336
- send(`Session: ${projectName}\nResumed: ${last.title}\n\nSend text, voice, or images.\n\n/sessions — switch conversation\n/session — switch project`, {
329
+ send(`Resumed: ${last.title}\n\nSend text, voice, or images.\n\n/sessions — switch conversation`, {
337
330
  keyboard: { inline_keyboard: [[{ text: "New conversation", callback_data: `new:${projectName}` }]] },
338
331
  });
339
332
  } else {
340
333
  const existing = getProviderSession(state, provider, projectName);
341
334
  state.isFirstMessage = !existing;
342
335
  saveState();
343
- send(`Session: ${projectName}${existing ? `\nResumed: ${existing.slice(0, 8)}` : ""}\n\nSend text, voice, or images.\n\n/sessions — switch conversation\n/session — switch project`);
336
+ send(`Ready.${existing ? `\nResumed: ${existing.slice(0, 8)}` : ""}\n\nSend text, voice, or images.\n\n/sessions — switch conversation`);
344
337
  }
345
338
  }
346
339
  }
@@ -361,7 +354,8 @@ register({
361
354
  handler: async (env) => {
362
355
  if (!authorized(env)) return;
363
356
  if (!isOnboarded()) return startOnboarding();
364
- send("Pick a project to start:", { keyboard: { inline_keyboard: [[{ text: "Pick a project", callback_data: "show:projects" }]] } });
357
+ requireSession();
358
+ send("Ready. Send text, voice, or images.\n\n/sessions — switch conversation · /new — fresh conversation");
365
359
  },
366
360
  });
367
361
 
@@ -370,7 +364,7 @@ register({
370
364
  handler: async (env) => {
371
365
  if (!authorized(env)) return;
372
366
  send([
373
- "Session: /session /sessions /projects /continue /status /stop /end",
367
+ "Session: /new /sessions /continue /status /stop /end",
374
368
  "Settings: /model /effort /budget /plan /compact /compactwindow /worktree /mode /engine /recall /tooltrace /toolmode /verbose",
375
369
  "Identity: /whoami /link",
376
370
  "Team: /people /intros /auth (owner)",
@@ -890,23 +884,7 @@ register({
890
884
  });
891
885
 
892
886
  register({
893
- name: "projects", description: "Browse all workspace projects",
894
- handler: async (env) => { if (authorized(env)) send("Pick:", { keyboard: projectKeyboard() }); },
895
- });
896
-
897
- register({
898
- name: "session", description: "Pick a project to work on", args: "[<project>]",
899
- handler: async (env, { tail }) => {
900
- if (!authorized(env)) return;
901
- if (tail) return startSession(tail);
902
- const cs = currentState().currentSession;
903
- if (cs) send(`Active: ${cs.name}\n\nSwitch?`, { keyboard: projectKeyboard() });
904
- else send("Pick:", { keyboard: projectKeyboard() });
905
- },
906
- });
907
-
908
- register({
909
- name: "sessions", description: "List conversations for this project",
887
+ name: "sessions", description: "List past conversations",
910
888
  handler: async (env) => {
911
889
  if (!authorized(env)) return;
912
890
  if (!requireSession()) return;
@@ -1452,15 +1430,13 @@ register({
1452
1430
  });
1453
1431
 
1454
1432
  register({
1455
- name: "end", description: "End current session",
1433
+ name: "end", description: "End the current conversation",
1456
1434
  handler: async (env) => {
1457
1435
  if (!authorized(env)) return;
1458
1436
  const state = currentState();
1459
- if (state.currentSession) {
1460
- const result = endCurrentSession({ state });
1461
- if (!result.ok) return send(`Could not end session: ${result.error.message}`);
1462
- send(`Ended: ${result.project}`, { keyboard: { inline_keyboard: [[{ text: "New session", callback_data: "show:projects" }]] } });
1463
- } else send("No session.");
1437
+ const result = endCurrentSession({ state });
1438
+ if (!result.ok) return send(`Could not end conversation: ${result.error.message}`);
1439
+ send("Ended. Send anything to start a fresh conversation.");
1464
1440
  },
1465
1441
  });
1466
1442
 
@@ -1980,7 +1956,7 @@ register({
1980
1956
  if (!tail) {
1981
1957
  const list = channelJobs();
1982
1958
  if (list.length === 0) {
1983
- return send("No crons." + archivedNote() + "\n\nAdd: /cron add \"<schedule>\" <project> \"<prompt>\"\n\nOr pick a preset:", {
1959
+ return send("No crons." + archivedNote() + "\n\nAdd: /cron add \"<schedule>\" \"<prompt>\"\n\nOr pick a preset:", {
1984
1960
  keyboard: { inline_keyboard: [
1985
1961
  [{ text: "Standup 9am", callback_data: "cp:standup" }, { text: "Git digest 6pm", callback_data: "cp:git" }],
1986
1962
  [{ text: "Dep check Mon", callback_data: "cp:deps" }, { text: "Health 30min", callback_data: "cp:health" }],
@@ -1990,24 +1966,23 @@ register({
1990
1966
  return send("Crons:\n\n" + list.map((c, i) => `${i + 1}. ${c.label} (${c.schedule}) — ${c.project || ""}`).join("\n") + archivedNote() + "\n\nRemove: /cron remove <#>");
1991
1967
  }
1992
1968
 
1993
- const addMatch = tail.match(/^add\s+"(.+)"\s+(\S+)\s+"(.+)"$/);
1969
+ const addMatch = tail.match(/^add\s+"(.+)"\s+"(.+)"$/);
1994
1970
  if (addMatch) {
1995
1971
  if (!cronLib.validate(addMatch[1])) return send("Invalid cron schedule.");
1996
- const proj = findProject(addMatch[2]);
1997
- if (!proj || Array.isArray(proj)) return send("Project not found.");
1998
1972
  scheduler.addJob({
1999
1973
  kind: "cron",
2000
1974
  adapter: env.adapter.id,
2001
1975
  adapterType: env.adapter.type,
2002
1976
  channelId: String(env.channelId),
2003
1977
  canonicalUserId: env.canonicalUserId,
2004
- project: proj,
2005
- prompt: addMatch[3],
2006
- label: addMatch[3].slice(0, 50),
1978
+ project: "Workspace",
1979
+ projectDir: WORKSPACE,
1980
+ prompt: addMatch[2],
1981
+ label: addMatch[2].slice(0, 50),
2007
1982
  source: "user",
2008
1983
  schedule: addMatch[1],
2009
1984
  });
2010
- return send(`Added: ${addMatch[3].slice(0, 50)} (${addMatch[1]}) for ${proj}`);
1985
+ return send(`Added: ${addMatch[2].slice(0, 50)} (${addMatch[1]})`);
2011
1986
  }
2012
1987
 
2013
1988
  const removeMatch = tail.match(/^remove\s+(\d+)$/);
@@ -2019,7 +1994,7 @@ register({
2019
1994
  return send(`Removed: ${removed ? removed.label : "(unknown)"}`);
2020
1995
  }
2021
1996
 
2022
- send('Usage: /cron | /cron add "<schedule>" <project> "<prompt>" | /cron remove <#>');
1997
+ send('Usage: /cron | /cron add "<schedule>" "<prompt>" | /cron remove <#>');
2023
1998
  },
2024
1999
  });
2025
2000
 
@@ -89,9 +89,7 @@ You are ${data.name}'s personal AI coding assistant, running 24/7 across chat ch
89
89
  saveEnvKey("ONBOARDED", "true");
90
90
  config.ONBOARDED = "true";
91
91
 
92
- send(`All set, ${data.name}! I'm configured and ready.\n\nTap /session to pick a project and start working.`, {
93
- keyboard: { inline_keyboard: [[{ text: "Pick a project", callback_data: "show:projects" }]] },
94
- });
92
+ send(`All set, ${data.name}! I'm configured and ready.\n\nSend text, voice, or images to get started. /help lists everything.`);
95
93
  }
96
94
 
97
95
  module.exports = { isOnboarded, startOnboarding, handleOnboarding, finishOnboarding };
package/core/router.js CHANGED
@@ -155,7 +155,6 @@ async function onAction(envelope) {
155
155
  async function handleVoice(envelope) {
156
156
  if (isDuplicate(envelope)) return;
157
157
  const state = currentState();
158
- if (!state.currentSession) return send("Pick a project first.");
159
158
  try {
160
159
  const media = envelope.media?.[0];
161
160
  if (!media) return;
@@ -190,7 +189,6 @@ async function handleVoice(envelope) {
190
189
  async function handleAudio(envelope) {
191
190
  if (isDuplicate(envelope)) return;
192
191
  const state = currentState();
193
- if (!state.currentSession) return send("Pick a project first.");
194
192
  try {
195
193
  const media = envelope.media?.[0];
196
194
  if (!media) return;
@@ -214,7 +212,6 @@ async function handleAudio(envelope) {
214
212
  async function handlePhoto(envelope) {
215
213
  if (isDuplicate(envelope)) return;
216
214
  const state = currentState();
217
- if (!state.currentSession) return send("Pick a project first.");
218
215
  try {
219
216
  const items = envelope.media || [];
220
217
  if (!items.length) return;
@@ -244,7 +241,6 @@ async function handlePhoto(envelope) {
244
241
  async function handleDocument(envelope) {
245
242
  if (isDuplicate(envelope)) return;
246
243
  const state = currentState();
247
- if (!state.currentSession) return send("Pick a project first.");
248
244
  try {
249
245
  const media = envelope.media?.[0];
250
246
  if (!media) return;
@@ -399,11 +395,6 @@ async function handleText(envelope) {
399
395
  return;
400
396
  }
401
397
 
402
- if (!state.currentSession) {
403
- const { projectKeyboard } = require("./projects");
404
- return send("Pick a project first:", { keyboard: projectKeyboard() });
405
- }
406
-
407
398
  const text = envelope.text || "";
408
399
  const runContext = admitRunContext(text, state.currentSession.dir, envelope.messageId, { purpose: "foreground" });
409
400
  const attachments = [];
package/core/state.js CHANGED
@@ -7,7 +7,7 @@ const path = require("path");
7
7
  const crypto = require("crypto");
8
8
  const {
9
9
  STATE_FILE, SESSIONS_FILE, IDENTITIES_FILE, CONFIG_DIR, CHAT_ID, UNIFIED_IDENTITY,
10
- JOBS_FILE, CRONS_FILE,
10
+ JOBS_FILE, CRONS_FILE, WORKSPACE,
11
11
  } = require("./config");
12
12
  const {
13
13
  normalizeCanonicalUserId,
@@ -292,7 +292,7 @@ function migrateSavedUser(value) {
292
292
 
293
293
  const result = {
294
294
  ...source,
295
- currentSession: isRecord(source.currentSession) ? { ...source.currentSession } : null,
295
+ currentSession: isRecord(source.currentSession) ? { ...source.currentSession } : rootSession(),
296
296
  settings,
297
297
  activeSessions,
298
298
  providerSettings,
@@ -496,6 +496,14 @@ function attachRuntimeAccessors(state) {
496
496
  return state;
497
497
  }
498
498
 
499
+ // The bot runs at a single top-level working directory. There is no per-folder
500
+ // project picker anymore, so every state starts already "in" the workspace
501
+ // root and currentSession is never null — readers (cwd, transcripts, usage) can
502
+ // rely on it being present on every channel, including the Spaces poller.
503
+ function rootSession() {
504
+ return { name: "Workspace", dir: WORKSPACE || process.cwd() };
505
+ }
506
+
499
507
  function createUserState(userId) {
500
508
  const saved = (savedState.users && savedState.users[userId]) || {};
501
509
  const hasSavedUser = hadPersistedStateFile
@@ -510,7 +518,7 @@ function createUserState(userId) {
510
518
  const state = {
511
519
  userId: String(userId),
512
520
  channelId: currentChannelId(),
513
- currentSession: saved.currentSession || null,
521
+ currentSession: saved.currentSession || rootSession(),
514
522
  runningProcess: null,
515
523
  preparingRun: false,
516
524
  cancelRequested: false,
@@ -1296,6 +1304,7 @@ module.exports = {
1296
1304
  getActiveUsage,
1297
1305
  getUsageForSession,
1298
1306
  usageSessionKey,
1307
+ rootSession,
1299
1308
  createUserState,
1300
1309
  getUserState,
1301
1310
  currentState,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@inetafrica/open-claudia",
3
- "version": "3.0.18",
3
+ "version": "3.0.19",
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,15 +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.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 itso 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");
35
+ // 3.0.19 approved by Sumeet 2026-07-14 ("Rip it out"): the per-folder
36
+ // project/session model is removed. The bot always runs at the top-level
37
+ // workspace currentSession defaults to { name: "Workspace", dir: WORKSPACE }
38
+ // and is never null which fixes Spaces (and any channel that never picked a
39
+ // folder) replying with a dead "Pick a project first:" picker. Removed
40
+ // /projects, /session, the project keyboard, the show:projects/s: callbacks,
41
+ // the router's five session gates, and core/projects.js. Conversation-level
42
+ // controls (/sessions, /new, /continue, /end) are unchanged.
43
+ assert.strictEqual(pkg.version, "3.0.19", "release version must remain unchanged without explicit approval");
44
44
  for (const keyword of ["claude", "codex", "coding-agent", "provider-agnostic"]) {
45
45
  assert.ok(pkg.keywords.includes(keyword), `package keyword missing: ${keyword}`);
46
46
  }
@@ -96,7 +96,7 @@ function newEndAndContinueProbe() {
96
96
 
97
97
  const ended = handlers.endCurrentSession({ state, persist: false });
98
98
  assert.strictEqual(ended.ok, true);
99
- assert.strictEqual(state.currentSession, null);
99
+ assert.strictEqual(state.currentSession.name, "Workspace", "ending resets to the top-level workspace, never null");
100
100
  assert.strictEqual(stateApi.getProviderSession(state, "codex", "alpha"), "codex-alpha");
101
101
  assert.strictEqual(stateApi.getProviderSession(state, "claude", "alpha"), "claude-alpha");
102
102
  assert.strictEqual(stateApi.userOwnsProviderSession(state.userId, "alpha", "codex", "codex-alpha"), true);
package/core/projects.js DELETED
@@ -1,43 +0,0 @@
1
- // Workspace project utilities. Reads filesystem; no IO dependencies.
2
-
3
- const fs = require("fs");
4
- const path = require("path");
5
- const { WORKSPACE } = require("./config");
6
-
7
- function listProjects() {
8
- try {
9
- return fs.readdirSync(WORKSPACE, { withFileTypes: true })
10
- .filter((d) => d.isDirectory())
11
- .map((d) => d.name)
12
- .filter((n) => !n.startsWith("."));
13
- } catch (e) { return []; }
14
- }
15
-
16
- function findProject(query) {
17
- const projects = listProjects();
18
- const q = query.toLowerCase();
19
- const exact = projects.find((p) => p.toLowerCase() === q);
20
- if (exact) return exact;
21
- const startsWith = projects.filter((p) => p.toLowerCase().startsWith(q));
22
- if (startsWith.length === 1) return startsWith[0];
23
- const contains = projects.filter((p) => p.toLowerCase().includes(q));
24
- if (contains.length === 1) return contains[0];
25
- return contains.length > 0 ? contains : null;
26
- }
27
-
28
- function projectKeyboard() {
29
- const projects = listProjects();
30
- const rows = [[{ text: "\u{1F4C1} Workspace (root)", callback_data: "s:__workspace__" }]];
31
- for (let i = 0; i < projects.length; i += 2) {
32
- const row = [{ text: projects[i], callback_data: `s:${projects[i]}` }];
33
- if (projects[i + 1]) row.push({ text: projects[i + 1], callback_data: `s:${projects[i + 1]}` });
34
- rows.push(row);
35
- }
36
- return { inline_keyboard: rows };
37
- }
38
-
39
- function workspacePath(name) {
40
- return name === "__workspace__" ? WORKSPACE : path.join(WORKSPACE, name);
41
- }
42
-
43
- module.exports = { listProjects, findProject, projectKeyboard, workspacePath };