@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/core/scheduler.js CHANGED
@@ -32,8 +32,10 @@ const {
32
32
  getProviderSession,
33
33
  getProviderSettings,
34
34
  getUserState,
35
+ peekLane,
36
+ laneBusy,
35
37
  } = require("./state");
36
- const { canonicalForChannel, conversationKeyFor } = require("./identity");
38
+ const { canonicalForChannel, conversationKeyFor, parseConversationKey } = require("./identity");
37
39
  const { send: defaultSend } = require("./io");
38
40
  const { providerStatus: defaultProviderStatus } = require("./providers");
39
41
  const { redactSensitive } = require("./redact");
@@ -109,6 +111,13 @@ function buildWatchCheckEnv(source, stamp = {}, { owner = false } = {}) {
109
111
  return env;
110
112
  }
111
113
 
114
+ // The chat thread a job answers in: stored on the job when it was created in a
115
+ // thread, else read off its conversation key (older jobs, CLI-made jobs).
116
+ function jobThreadId(job) {
117
+ if (!job) return null;
118
+ return job.threadId || parseConversationKey(job.project).threadId || null;
119
+ }
120
+
112
121
  // The principal a watch-check's short-lived run acts for: the job's creator,
113
122
  // resolved from its stored channel/user the same way runPrincipal resolves a
114
123
  // turn's speaker. Owner → the check attributes as the owner (unchanged); a
@@ -136,6 +145,7 @@ function watchCheckPrincipal(job) {
136
145
  adapterId: job && job.adapter ? String(job.adapter) : null,
137
146
  adapterType,
138
147
  channelId,
148
+ threadId: jobThreadId(job),
139
149
  userId,
140
150
  room,
141
151
  };
@@ -299,8 +309,26 @@ function createSchedulerService(dependencies = {}) {
299
309
  return { name, dir: dir || workspace || process.cwd() };
300
310
  }
301
311
 
312
+ // The session of the main timeline of the room a thread job belongs to.
313
+ function roomMainSession(state, job) {
314
+ if (!jobThreadId(job)) return null;
315
+ const threadRoom = parseConversationKey(job.project).threadId ? parseConversationKey(job.project).roomKey : null;
316
+ const roomKey = threadRoom || (job.adapterType && job.channelId ? conversationKeyFor(job.adapterType, job.channelId) : null);
317
+ if (!roomKey || roomKey === job.project) return null;
318
+ return getProviderSession(state, job.provider, roomKey) || null;
319
+ }
320
+
302
321
  function resolveCompactionLineage(state, job) {
303
322
  let sessionId = job.sessionId || null;
323
+ // A thread job never resumes its room's MAIN session: a job made in a
324
+ // thread's first (forked) turn by an older build stored the main id, and
325
+ // resuming it would write the thread's wakeup into the main conversation,
326
+ // possibly beside a live main turn. Use the thread's own session instead.
327
+ const mainSession = sessionId ? roomMainSession(state, job) : null;
328
+ if (mainSession && sessionId === mainSession) {
329
+ const own = getProviderSession(state, job.provider, job.project);
330
+ sessionId = own && own !== mainSession ? own : null;
331
+ }
304
332
  if (!sessionId) return { sessionId: null, attachSession: true };
305
333
  const seen = new Set();
306
334
  while (sessionId && !seen.has(sessionId)) {
@@ -707,6 +735,9 @@ function createSchedulerService(dependencies = {}) {
707
735
  // shared session. Matches captured project.name (projectDescriptor(job).name)
708
736
  // so sameCapturedProject / getProjectKey resolve the right thread mid-run.
709
737
  conversationKey: projectDescriptor(job).name,
738
+ // A job made in a chat thread answers in that thread: its notice, its
739
+ // typing indicator and its reply all go back into the thread.
740
+ threadId: jobThreadId(job),
710
741
  // …but the thread is not the room. A job belongs to the room it was
711
742
  // created in and must answer under THAT room's provider; without this the
712
743
  // room overlay can't resolve and the run silently falls back to the
@@ -721,7 +752,15 @@ function createSchedulerService(dependencies = {}) {
721
752
  return runInChat(chat, async () => {
722
753
  noteLegacyCreatorOnce(job);
723
754
  const state = stateForUser(canonicalUserId);
724
- if (state.runningProcess || state.preparingRun || state.isCompacting) {
755
+ // Busy means THIS job's conversation is running, compacting or has
756
+ // messages waiting; other conversations of the speaker may run meanwhile.
757
+ // The conversation it replies INTO counts too: a job whose project is not
758
+ // its chat's key (--project myrepo, a legacy basename) must not answer in
759
+ // a thread or timeline beside a live turn there.
760
+ const lane = peekLane(state, projectDescriptor(job).name);
761
+ const replyLane = peekLane(state, conversationKeyFor(adapter.type, job.channelId, jobThreadId(job)));
762
+ const occupied = (l) => laneBusy(l) || !!l?.messageQueue?.length;
763
+ if (occupied(lane) || occupied(replyLane)) {
725
764
  try { return markBusy(job); }
726
765
  catch (error) { return keepRetrying(job, error, busyDelayMs); }
727
766
  }
@@ -945,7 +984,9 @@ function createSchedulerService(dependencies = {}) {
945
984
  const sessionId = Object.prototype.hasOwnProperty.call(spec, "sessionId")
946
985
  ? spec.sessionId
947
986
  : getProviderSession(state, provider, project);
948
- const originRunId = spec.originRunId || state.activeRunContext?.runId || null;
987
+ // Fall back to the run of the job's own conversation only: another
988
+ // thread's running turn is not where this job came from.
989
+ const originRunId = spec.originRunId || (project ? peekLane(state, project)?.activeRunContext?.runId : null) || null;
949
990
  const providerSettings = spec.providerSettings || getProviderSettings(state, provider);
950
991
  return {
951
992
  ...spec,
@@ -1072,6 +1113,7 @@ module.exports = {
1072
1113
  terminalRunSucceeded,
1073
1114
  buildWatchCheckEnv,
1074
1115
  watchCheckPrincipal,
1116
+ jobThreadId,
1075
1117
  setAdapters,
1076
1118
  initScheduler,
1077
1119
  scheduleJob: service.scheduleJob,
package/core/side-chat.js CHANGED
@@ -143,6 +143,23 @@ async function defaultDeliver(payload, store) {
143
143
  return execute();
144
144
  }
145
145
 
146
+ // Side chats are per (speaker, conversation): a speaker's threads each get
147
+ // their own side answer, and /stop in one conversation cancels only its own.
148
+ // A bare canonical id (no lane) addresses every conversation of the speaker.
149
+ const LANE_SEP = "\u0000";
150
+ function sideChatKey(canonicalUserId, laneKey = null) {
151
+ return laneKey ? `${canonicalUserId}${LANE_SEP}${laneKey}` : String(canonicalUserId);
152
+ }
153
+ function keyMatches(key, canonicalUserId, laneKey = null) {
154
+ if (laneKey) return key === sideChatKey(canonicalUserId, laneKey);
155
+ const user = String(canonicalUserId);
156
+ return key === user || key.startsWith(`${user}${LANE_SEP}`);
157
+ }
158
+ function requestLaneKey(request) {
159
+ const runContext = request?.originalRunContext || request?.runContext || null;
160
+ return runContext?.laneKey || runContext?.project?.name || null;
161
+ }
162
+
146
163
  class SideChatCoordinator {
147
164
  constructor(options = {}) {
148
165
  this.runSideChat = options.runSideChat || ((request) => require("./runner").runSideChatRequest(request));
@@ -163,28 +180,39 @@ class SideChatCoordinator {
163
180
  return this.modeReader() === "agent";
164
181
  }
165
182
 
166
- isActive(canonicalUserId) {
167
- return this.activeByUser.has(String(canonicalUserId));
183
+ isActive(canonicalUserId, laneKey = null) {
184
+ return [...this.activeByUser.keys()].some((key) => keyMatches(key, canonicalUserId, laneKey));
168
185
  }
169
186
 
170
- attachProcess(canonicalUserId, proc) {
187
+ attachProcess(canonicalUserId, proc, laneKey = null) {
171
188
  if (!proc) return;
172
189
  let resolveDone;
173
190
  const done = new Promise((resolve) => { resolveDone = resolve; });
174
191
  const settle = () => resolveDone();
175
192
  proc.once("close", settle);
176
193
  proc.once("error", settle);
177
- this.processByUser.set(String(canonicalUserId), { proc, done });
194
+ this.processByUser.set(sideChatKey(canonicalUserId, laneKey), { proc, done });
178
195
  }
179
196
 
180
- detachProcess(canonicalUserId, proc) {
181
- const key = String(canonicalUserId);
197
+ detachProcess(canonicalUserId, proc, laneKey = null) {
198
+ const key = sideChatKey(canonicalUserId, laneKey);
182
199
  const entry = this.processByUser.get(key);
183
200
  if (!proc || entry?.proc === proc) this.processByUser.delete(key);
184
201
  }
185
202
 
186
- async cancel(canonicalUserId) {
187
- const key = String(canonicalUserId);
203
+ // Cancel the side chat of one conversation (laneKey), or of every
204
+ // conversation of the speaker when no lane is named.
205
+ async cancel(canonicalUserId, laneKey = null) {
206
+ if (!laneKey) {
207
+ const keys = new Set([...this.activeByUser.keys(), ...this.processByUser.keys()]
208
+ .filter((key) => keyMatches(key, canonicalUserId)));
209
+ const outcomes = await Promise.all([...keys].map((key) => this.cancelKey(key)));
210
+ return outcomes.some(Boolean);
211
+ }
212
+ return this.cancelKey(sideChatKey(canonicalUserId, laneKey));
213
+ }
214
+
215
+ async cancelKey(key) {
188
216
  const active = this.activeByUser.get(key);
189
217
  if (active) this.cancelledRequests.add(active.token);
190
218
  const entry = this.processByUser.get(key);
@@ -208,8 +236,8 @@ class SideChatCoordinator {
208
236
  }
209
237
 
210
238
  async cancelAll() {
211
- const users = new Set([...this.activeByUser.keys(), ...this.processByUser.keys()]);
212
- const outcomes = await Promise.all([...users].map((canonicalUserId) => this.cancel(canonicalUserId)));
239
+ const keys = new Set([...this.activeByUser.keys(), ...this.processByUser.keys()]);
240
+ const outcomes = await Promise.all([...keys].map((key) => this.cancelKey(key)));
213
241
  return outcomes.filter(Boolean).length;
214
242
  }
215
243
 
@@ -258,7 +286,7 @@ class SideChatCoordinator {
258
286
 
259
287
  async respond(input = {}) {
260
288
  const request = this.makePending(input);
261
- const userKey = String(request.canonicalUserId);
289
+ const userKey = sideChatKey(request.canonicalUserId, requestLaneKey(request));
262
290
  if (this.activeByUser.has(userKey)) {
263
291
  try {
264
292
  const delivery = await this.deliver({
@@ -343,4 +371,5 @@ module.exports = {
343
371
  createSideChatRequest,
344
372
  readRuntimeMode,
345
373
  sideChatCoordinator,
374
+ sideChatKey,
346
375
  };
@@ -88,6 +88,8 @@ function principalOf(principal = {}) {
88
88
  adapterId: text(principal.adapterId),
89
89
  adapterType: text(principal.adapterType),
90
90
  channelId: text(principal.channelId),
91
+ // The chat thread the run answers in (null: the channel's main timeline).
92
+ threadId: text(principal.threadId),
91
93
  userId: text(principal.userId),
92
94
  room: text(principal.room),
93
95
  purpose: text(principal.purpose),