@inetafrica/open-claudia 3.5.1 → 3.5.4

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,20 @@
1
1
  # Changelog
2
2
 
3
+ ## 3.5.4
4
+
5
+ Parallel conversations in Kazee Chat threads, with the fixes from an independent review. Canary first, on the owner's own bot. (3.5.2 and 3.5.3 were tagged but never published: 3.5.2 failed its release test, 3.5.3 was cancelled for this build.)
6
+
7
+ - **A Kazee thread is its own conversation with the bot.** "Reply in thread" on any message starts a conversation that runs alongside the main chat and the other threads: its own session, transcript, queue, typing indicator and live preview, and the bot answers inside the thread. Up to 4 conversations run at once per person (`OC_MAX_PARALLEL_TURNS`) and 8 across the bot (`OC_MAX_PARALLEL_TURNS_GLOBAL`). Past that a message waits, says so, and starts when a slot frees. Inside one conversation turns still run one at a time.
8
+ - **A new thread starts "hybrid":** the message it hangs off, the replies so far, and the last 10 main-chat messages before it, not the whole history. `OC_THREAD_SEED=fresh` leaves the main messages out; `OC_THREAD_SEED=fork` (Claude) resumes the main conversation into the thread instead.
9
+ - **`/stop` stops only the conversation it is sent in, and drops the messages queued in it before the /stop,** naming them (`Cancelled. Dropped 2 queued: "reboot pe2", "and check the…"`). A conversation that is only waiting for a free slot drops its waiting messages the same way. `/stop all` does both for every conversation. A message sent after the /stop still runs. `/status` lists the conversations running now.
10
+ - **Everything a thread turn starts comes back to that thread:** wake-ups, watches and crons, files, photos and voice notes, approvals. A button pressed on a message the bot posted in a thread runs in that thread.
11
+ - **Groups:** a thread reply engages the bot when it is @mentioned, when the bot posted the thread's first message, or once the bot has replied in that thread. The main-chat catch-up leaves thread-only replies out.
12
+ - **A refused thread never loses a reply:** if Kazee Chat refuses the thread (for example its first message was deleted), the reply goes to the main chat, quoting that message.
13
+ - **Fixes from the review:** no typing indicator for a message that waits for a slot (it could stay on for good); a /stop during a compaction no longer eats the next message; in groups, a thread the bot replied in keeps engaging it even when the reply's id came back empty; a button pressed after a restart still runs in its thread; a job made in a forked thread never resumes the main conversation (`OC_THREAD_SEED=fork`); a job also waits for the conversation it replies into; a stuck run's late exit never frees a conversation another run holds; a turn can only post into its own thread; a Kazee send is never repeated because of bookkeeping; recall keeps a quiet conversation's own recent turns while other threads are busy.
14
+ - **Fix:** compaction read the wrong conversation's recent turns (the legacy "Workspace" transcript) when it summarised.
15
+ - **Changed:** background runs that have no chat now run beside chat conversations instead of queueing behind them, and count toward the caps.
16
+ - **Note:** this release also carries 3.5.0 and 3.5.1 (permission phase 0). The fleet's containers still need the bubblewrap fix for staff turns before any roll; this canary is for one bot on macOS, where the environment scrub works.
17
+
3
18
  ## 3.5.1
4
19
 
5
20
  Hardening after an adversarial review of 3.5.0, before any of it reaches the fleet. Canary first.
package/bin/cli.js CHANGED
@@ -56,18 +56,26 @@ function sendViaLoopback(kind, restArgs) {
56
56
  const stat = fs.statSync(filePath);
57
57
  const params = new URLSearchParams({ channelId, adapter: adapterId, fileName });
58
58
  if (process.env.OC_CHANNEL_USER_ID) params.set("userId", process.env.OC_CHANNEL_USER_ID);
59
+ // A thread turn's file goes back into its thread.
60
+ if (process.env.OC_THREAD_ID) params.set("threadId", process.env.OC_THREAD_ID);
59
61
  if (caption) params.set("caption", caption);
62
+ const headers = {
63
+ Authorization: `Bearer ${sendToken}`,
64
+ "Content-Type": "application/octet-stream",
65
+ "Content-Length": stat.size,
66
+ };
67
+ // The run's attest lets the bot check the thread against the live run.
68
+ if (process.env.OC_RUN_ID && process.env.OC_RUN_ATTEST) {
69
+ headers["X-OC-Run-Id"] = process.env.OC_RUN_ID;
70
+ headers["X-OC-Run-Attest"] = process.env.OC_RUN_ATTEST;
71
+ }
60
72
 
61
73
  const req = http.request({
62
74
  method: "POST",
63
75
  hostname: u.hostname,
64
76
  port: u.port,
65
77
  path: `/${kind}?${params.toString()}`,
66
- headers: {
67
- Authorization: `Bearer ${sendToken}`,
68
- "Content-Type": "application/octet-stream",
69
- "Content-Length": stat.size,
70
- },
78
+ headers,
71
79
  }, (res) => {
72
80
  let body = "";
73
81
  res.on("data", (c) => { body += c; });
@@ -41,6 +41,9 @@ function context() {
41
41
  sendUrl,
42
42
  sendToken,
43
43
  channelId: process.env.OC_CHANNEL_ID,
44
+ // The chat thread the turn is in (set by the runner for thread turns):
45
+ // files, voice and jobs from this turn go back into that thread.
46
+ threadId: process.env.OC_THREAD_ID || null,
44
47
  adapterId: process.env.OC_CHANNEL_ADAPTER,
45
48
  userId: process.env.OC_CHANNEL_USER_ID || null,
46
49
  canonicalUserId: process.env.OC_CANONICAL_USER_ID || null,
@@ -91,6 +94,7 @@ function postJson(kind, body, { verified = false } = {}) {
91
94
  ...body,
92
95
  ...(kind === "call-say" ? { callSpeechToken: process.env.OC_CALL_SPEECH_TOKEN || "" } : {}),
93
96
  channelId: ctx.channelId,
97
+ threadId: ctx.threadId,
94
98
  adapter: ctx.adapterId,
95
99
  userId: ctx.userId,
96
100
  canonicalUserId: ctx.canonicalUserId,
package/bin/schedule.js CHANGED
@@ -85,6 +85,11 @@ function jobProgress(j) {
85
85
  return state ? `${since}${state}` : "";
86
86
  }
87
87
 
88
+ // A job created in a chat thread fires back into that thread.
89
+ function threadMarker(j) {
90
+ return j.threadId ? ` (thread ${String(j.threadId).slice(-6)})` : "";
91
+ }
92
+
88
93
  function fmtJob(j) {
89
94
  const when = j.kind === "wakeup"
90
95
  ? `fires ${new Date(j.fireAt).toISOString()}`
@@ -92,7 +97,7 @@ function fmtJob(j) {
92
97
  const proj = j.project ? ` [${j.project}]` : "";
93
98
  const src = j.source ? ` (${j.source})` : "";
94
99
  const progress = jobProgress(j);
95
- return `${j.id} ${j.kind} ${j.label || "(no label)"}${proj}${src} ${when}${progress ? ` [${progress}]` : ""}`;
100
+ return `${j.id} ${j.kind} ${j.label || "(no label)"}${proj}${src}${threadMarker(j)} ${when}${progress ? ` [${progress}]` : ""}`;
96
101
  }
97
102
 
98
103
  function fmtWatch(w, now = Date.now()) {
@@ -104,7 +109,7 @@ function fmtWatch(w, now = Date.now()) {
104
109
  ? " EXPIRED (fires on next poll)"
105
110
  : ` expires ${new Date(Number(w.expiresAt)).toISOString()}`)
106
111
  : " no expiry (pre-timeout watch)";
107
- return `${w.id} ${mode} polls "${w.schedule}"${expiry} check: ${w.check} → ${w.label || "(no label)"}`;
112
+ return `${w.id} ${mode}${threadMarker(w)} polls "${w.schedule}"${expiry} check: ${w.check} → ${w.label || "(no label)"}`;
108
113
  }
109
114
 
110
115
  async function runScheduleWakeup(args) {
package/bot.js CHANGED
@@ -131,9 +131,13 @@ async function gracefulShutdown(signal) {
131
131
  console.error("memory shutdown drain failed:", error?.message);
132
132
  return { drained: false };
133
133
  });
134
+ // Every running conversation (main timeline and each thread) has its own
135
+ // process on its own lane.
134
136
  for (const state of userStates.values()) {
135
- if (state.runningProcess) {
136
- try { killProcessTree(state.runningProcess.pid, "SIGTERM"); } catch (e) {}
137
+ if (!(state.lanes instanceof Map)) continue;
138
+ for (const lane of state.lanes.values()) {
139
+ if (!lane.runningProcess) continue;
140
+ try { killProcessTree(lane.runningProcess.pid, "SIGTERM"); } catch (e) {}
137
141
  }
138
142
  }
139
143
  await Promise.all([sideCleanup, memoryCleanup]);