@inetafrica/open-claudia 3.0.20 → 3.0.22
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/actions.js +3 -2
- package/core/context.js +13 -2
- package/core/handlers.js +15 -14
- package/core/identity.js +9 -0
- package/core/router.js +2 -0
- package/core/run-context.js +20 -2
- package/core/scheduler.js +4 -0
- package/core/state.js +54 -2
- package/core/turn-observer.js +11 -1
- package/package.json +1 -1
- package/test-provider-gateway.js +1 -0
- package/test-provider-language.js +12 -8
package/core/actions.js
CHANGED
|
@@ -575,19 +575,20 @@ async function handleAction(envelope) {
|
|
|
575
575
|
};
|
|
576
576
|
const p = presets[d.slice(3)];
|
|
577
577
|
if (!p) return;
|
|
578
|
+
const cronProject = getProjectKey(state);
|
|
578
579
|
scheduler.addJob({
|
|
579
580
|
kind: "cron",
|
|
580
581
|
adapter: envelope.adapter.id,
|
|
581
582
|
adapterType: envelope.adapter.type,
|
|
582
583
|
channelId: String(envelope.channelId),
|
|
583
584
|
canonicalUserId: envelope.canonicalUserId,
|
|
584
|
-
project:
|
|
585
|
+
project: cronProject,
|
|
585
586
|
prompt: p.prompt,
|
|
586
587
|
label: p.label,
|
|
587
588
|
source: "user",
|
|
588
589
|
schedule: p.schedule,
|
|
589
590
|
});
|
|
590
|
-
await send(`Added: ${p.label} for ${
|
|
591
|
+
await send(`Added: ${p.label} for ${cronProject}`);
|
|
591
592
|
return;
|
|
592
593
|
}
|
|
593
594
|
if (d === "cp:clear") {
|
package/core/context.js
CHANGED
|
@@ -6,8 +6,8 @@ const { AsyncLocalStorage } = require("node:async_hooks");
|
|
|
6
6
|
|
|
7
7
|
const chatContext = new AsyncLocalStorage();
|
|
8
8
|
|
|
9
|
-
function runInChat({ adapter, channelId, canonicalUserId, userId, transport, raw }, fn) {
|
|
10
|
-
return chatContext.run({ adapter, channelId, canonicalUserId, userId, transport, raw }, fn);
|
|
9
|
+
function runInChat({ adapter, channelId, canonicalUserId, userId, transport, raw, conversationKey }, fn) {
|
|
10
|
+
return chatContext.run({ adapter, channelId, canonicalUserId, userId, transport, raw, conversationKey }, fn);
|
|
11
11
|
}
|
|
12
12
|
|
|
13
13
|
function currentStore() {
|
|
@@ -44,6 +44,16 @@ function currentTransport() {
|
|
|
44
44
|
return s ? s.transport : null;
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
+
// The per-thread conversation key for this turn (e.g. "telegram:6251055967",
|
|
48
|
+
// "spaces:task:42"). Distinct from the (possibly unified) canonical speaker
|
|
49
|
+
// identity: identity says WHO is talking, conversationKey says WHICH thread —
|
|
50
|
+
// so the owner's Telegram DM and the owner's Spaces task keep separate live
|
|
51
|
+
// conversations even though both resolve to the same canonical self.
|
|
52
|
+
function currentConversationKey() {
|
|
53
|
+
const s = currentStore();
|
|
54
|
+
return s && s.conversationKey ? s.conversationKey : null;
|
|
55
|
+
}
|
|
56
|
+
|
|
47
57
|
module.exports = {
|
|
48
58
|
chatContext,
|
|
49
59
|
runInChat,
|
|
@@ -52,5 +62,6 @@ module.exports = {
|
|
|
52
62
|
currentCanonicalUserId,
|
|
53
63
|
currentUserId,
|
|
54
64
|
currentTransport,
|
|
65
|
+
currentConversationKey,
|
|
55
66
|
currentRaw,
|
|
56
67
|
};
|
package/core/handlers.js
CHANGED
|
@@ -15,7 +15,7 @@ const { register } = require("./commands");
|
|
|
15
15
|
const { send, deleteMessage } = require("./io");
|
|
16
16
|
const {
|
|
17
17
|
currentState, saveState, rootSession,
|
|
18
|
-
freshUsage, getActiveProvider, getProviderSession, setProviderSession, clearProviderSession,
|
|
18
|
+
freshUsage, getActiveProvider, getProjectKey, getProviderSession, setProviderSession, clearProviderSession,
|
|
19
19
|
getProviderSettings,
|
|
20
20
|
getProjectSessions, getLastProjectSession, userOwnsProviderSession,
|
|
21
21
|
createSessionCallbackToken, linkIdentity,
|
|
@@ -207,7 +207,7 @@ function switchProviderState(providerId, options = {}) {
|
|
|
207
207
|
};
|
|
208
208
|
state.settings.backend = provider;
|
|
209
209
|
if (Object.prototype.hasOwnProperty.call(options, "model")) state.settings.model = options.model;
|
|
210
|
-
const project = state
|
|
210
|
+
const project = getProjectKey(state);
|
|
211
211
|
if (project && !getProviderSession(state, provider, project)) {
|
|
212
212
|
const last = getLastProjectSession(state.userId, project, provider);
|
|
213
213
|
if (last) setProviderSession(state, provider, project, last.id);
|
|
@@ -237,9 +237,10 @@ function selectProjectState(project, options = {}) {
|
|
|
237
237
|
clearTransientQueue(state, "Project changed before the queued run started");
|
|
238
238
|
state.currentSession = { name: project.name.trim(), dir: project.dir || null };
|
|
239
239
|
const provider = getActiveProvider(state);
|
|
240
|
-
const
|
|
241
|
-
|
|
242
|
-
|
|
240
|
+
const key = getProjectKey(state);
|
|
241
|
+
const last = provider ? getLastProjectSession(state.userId, key, provider) : null;
|
|
242
|
+
if (last) setProviderSession(state, provider, key, last.id);
|
|
243
|
+
const sessionId = getProviderSession(state, provider, key);
|
|
243
244
|
state.isFirstMessage = !sessionId;
|
|
244
245
|
const persisted = persistSessionControl(state, options, () => {
|
|
245
246
|
state.currentSession = previous.currentSession;
|
|
@@ -251,7 +252,7 @@ function selectProjectState(project, options = {}) {
|
|
|
251
252
|
|
|
252
253
|
function startNewConversation(options = {}) {
|
|
253
254
|
const state = options.state || currentState();
|
|
254
|
-
const project = state
|
|
255
|
+
const project = getProjectKey(state);
|
|
255
256
|
if (!project || (options.projectName && options.projectName !== project)) {
|
|
256
257
|
return { ok: false, error: Object.assign(new Error("The selected project changed"), { code: "STALE_PROJECT" }) };
|
|
257
258
|
}
|
|
@@ -277,7 +278,7 @@ function startNewConversation(options = {}) {
|
|
|
277
278
|
function endCurrentSession(options = {}) {
|
|
278
279
|
const state = options.state || currentState();
|
|
279
280
|
const previous = { currentSession: state.currentSession, isFirstMessage: state.isFirstMessage };
|
|
280
|
-
const project = state
|
|
281
|
+
const project = getProjectKey(state) || "Workspace";
|
|
281
282
|
clearTransientQueue(state, "The conversation ended before the queued run started");
|
|
282
283
|
state.currentSession = rootSession();
|
|
283
284
|
state.isFirstMessage = true;
|
|
@@ -292,7 +293,7 @@ function endCurrentSession(options = {}) {
|
|
|
292
293
|
function activeContinueOptions(state = currentState()) {
|
|
293
294
|
const provider = getActiveProvider(state);
|
|
294
295
|
if (!provider) return null;
|
|
295
|
-
const sessionId = getProviderSession(state, provider, state
|
|
296
|
+
const sessionId = getProviderSession(state, provider, getProjectKey(state));
|
|
296
297
|
return sessionId ? { provider, resumeSessionId: sessionId } : null;
|
|
297
298
|
}
|
|
298
299
|
|
|
@@ -301,7 +302,7 @@ function activeContinueOptions(state = currentState()) {
|
|
|
301
302
|
function startSession(name, resumeSessionId, options = {}) {
|
|
302
303
|
const state = currentState();
|
|
303
304
|
const provider = options.provider || getActiveProvider(state);
|
|
304
|
-
const projectName =
|
|
305
|
+
const projectName = getProjectKey(state);
|
|
305
306
|
|
|
306
307
|
if (!provider) return send("Choose Claude Code or OpenAI Codex with /backend before starting a conversation.");
|
|
307
308
|
|
|
@@ -891,10 +892,10 @@ register({
|
|
|
891
892
|
const state = currentState();
|
|
892
893
|
const provider = getActiveProvider(state);
|
|
893
894
|
if (!provider) return send("Choose Claude Code or OpenAI Codex with /backend first.");
|
|
894
|
-
const project = state
|
|
895
|
+
const project = getProjectKey(state);
|
|
895
896
|
const sessions = getProjectSessions(state.userId, project, provider)
|
|
896
897
|
.filter((session) => session.selectable !== false);
|
|
897
|
-
if (sessions.length === 0) return send("No past conversations
|
|
898
|
+
if (sessions.length === 0) return send("No past conversations in this thread.");
|
|
898
899
|
const rows = sessions.slice(0, 10).map((s) => {
|
|
899
900
|
const date = new Date(s.lastUsed).toLocaleDateString();
|
|
900
901
|
const active = getProviderSession(state, provider, project) === s.id ? " (active)" : "";
|
|
@@ -905,12 +906,12 @@ register({
|
|
|
905
906
|
sessionId: s.id,
|
|
906
907
|
});
|
|
907
908
|
return [{
|
|
908
|
-
text: `${s.title}${active} · ${s.provider}
|
|
909
|
+
text: `${s.title}${active} · ${s.provider} — ${date}`,
|
|
909
910
|
callback_data: `ss:${token}`,
|
|
910
911
|
}];
|
|
911
912
|
});
|
|
912
|
-
rows.push([{ text: "New conversation", callback_data: `new:${
|
|
913
|
-
send(`Conversations in
|
|
913
|
+
rows.push([{ text: "New conversation", callback_data: `new:${project}` }]);
|
|
914
|
+
send(`Conversations in this thread:`, { keyboard: { inline_keyboard: rows } });
|
|
914
915
|
},
|
|
915
916
|
});
|
|
916
917
|
|
package/core/identity.js
CHANGED
|
@@ -18,6 +18,14 @@ function defaultCanonicalForChannel(transport, channelId) {
|
|
|
18
18
|
return channelKey(transport, channelId);
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
+
// The conversation (thread) key for a channel. Deliberately identical to the
|
|
22
|
+
// pre-unification default canonical: a thread is keyed by its raw channel
|
|
23
|
+
// identity, never by the unified speaker canonical. This is what keeps each
|
|
24
|
+
// surface's live conversation separate even when the speaker is one being.
|
|
25
|
+
function conversationKeyFor(transport, channelId) {
|
|
26
|
+
return channelKey(transport, channelId);
|
|
27
|
+
}
|
|
28
|
+
|
|
21
29
|
function loadIdentities() {
|
|
22
30
|
const raw = readJsonWithFallback(IDENTITIES_FILE, null);
|
|
23
31
|
return {
|
|
@@ -187,6 +195,7 @@ module.exports = {
|
|
|
187
195
|
normalizeCanonicalUserId,
|
|
188
196
|
channelKey,
|
|
189
197
|
defaultCanonicalForChannel,
|
|
198
|
+
conversationKeyFor,
|
|
190
199
|
isConfiguredOwnerChannel,
|
|
191
200
|
ownerCanonical,
|
|
192
201
|
channelIsOwner,
|
package/core/router.js
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
const fs = require("fs");
|
|
6
6
|
const path = require("path");
|
|
7
7
|
const { runInChat } = require("./context");
|
|
8
|
+
const { conversationKeyFor } = require("./identity");
|
|
8
9
|
const { dispatch } = require("./commands");
|
|
9
10
|
const { handleAction } = require("./actions");
|
|
10
11
|
const { isChatAuthorized } = require("./access");
|
|
@@ -79,6 +80,7 @@ function scope(envelope, fn) {
|
|
|
79
80
|
userId: envelope.userId,
|
|
80
81
|
transport: envelope.adapter?.type,
|
|
81
82
|
raw: envelope.raw,
|
|
83
|
+
conversationKey: conversationKeyFor(envelope.adapter?.type, envelope.channelId),
|
|
82
84
|
}, fn);
|
|
83
85
|
}
|
|
84
86
|
|
package/core/run-context.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
const crypto = require("crypto");
|
|
4
4
|
const path = require("path");
|
|
5
|
+
const { currentConversationKey } = require("./context");
|
|
5
6
|
|
|
6
7
|
function cloneSerializable(value, label = "run context value") {
|
|
7
8
|
if (value === undefined) return undefined;
|
|
@@ -23,7 +24,18 @@ function deepFreeze(value, seen = new Set()) {
|
|
|
23
24
|
}
|
|
24
25
|
|
|
25
26
|
function projectSnapshot(state, cwd, requestedProject) {
|
|
26
|
-
|
|
27
|
+
let value = requestedProject;
|
|
28
|
+
if (value === undefined) {
|
|
29
|
+
// No explicit project ⇒ this is a live chat turn: key the conversation
|
|
30
|
+
// (provider session) by the per-thread conversation key so each surface
|
|
31
|
+
// keeps its own session. dir stays the workspace root. Outside a turn
|
|
32
|
+
// (background jobs, tests) there is no conversation key ⇒ fall back to the
|
|
33
|
+
// shared currentSession, preserving prior behaviour byte-for-byte.
|
|
34
|
+
const convo = currentConversationKey();
|
|
35
|
+
value = convo
|
|
36
|
+
? { name: convo, dir: state.currentSession?.dir || cwd || null }
|
|
37
|
+
: state.currentSession;
|
|
38
|
+
}
|
|
27
39
|
if (value && typeof value === "object") {
|
|
28
40
|
const dir = value.dir || value.path || cwd || null;
|
|
29
41
|
return {
|
|
@@ -146,7 +158,13 @@ function sameCapturedProject(state, runContext) {
|
|
|
146
158
|
const live = state?.currentSession;
|
|
147
159
|
const captured = runContext?.project;
|
|
148
160
|
if (!live || !captured) return !live && !captured;
|
|
149
|
-
|
|
161
|
+
// "Same project" now means "same live conversation thread". Compare the
|
|
162
|
+
// captured key against the live conversation key (falling back to the shared
|
|
163
|
+
// currentSession name outside a chat turn), so a run that resumes/compacts is
|
|
164
|
+
// matched to the thread it was admitted on — not to whichever thread most
|
|
165
|
+
// recently touched the shared currentSession.
|
|
166
|
+
const liveName = currentConversationKey() || live.name || "";
|
|
167
|
+
return String(liveName) === String(captured.name || "")
|
|
150
168
|
&& String(live.dir || live.path || "") === String(captured.dir || captured.path || "");
|
|
151
169
|
}
|
|
152
170
|
|
package/core/scheduler.js
CHANGED
|
@@ -328,6 +328,10 @@ function createSchedulerService(dependencies = {}) {
|
|
|
328
328
|
userId,
|
|
329
329
|
transport: adapter.type,
|
|
330
330
|
raw: null,
|
|
331
|
+
// Key the resumed conversation by the job's saved thread, not the live
|
|
332
|
+
// shared session. Matches captured project.name (projectDescriptor(job).name)
|
|
333
|
+
// so sameCapturedProject / getProjectKey resolve the right thread mid-run.
|
|
334
|
+
conversationKey: projectDescriptor(job).name,
|
|
331
335
|
};
|
|
332
336
|
|
|
333
337
|
return runInChat(chat, async () => {
|
package/core/state.js
CHANGED
|
@@ -20,7 +20,7 @@ const {
|
|
|
20
20
|
channelKey,
|
|
21
21
|
identities,
|
|
22
22
|
} = require("./identity");
|
|
23
|
-
const { currentChannelId, currentCanonicalUserId } = require("./context");
|
|
23
|
+
const { currentChannelId, currentCanonicalUserId, currentConversationKey } = require("./context");
|
|
24
24
|
const { atomicWriteFileSync, readJsonWithFallback } = require("./fsutil");
|
|
25
25
|
const {
|
|
26
26
|
defaultMigrationSources,
|
|
@@ -385,7 +385,16 @@ function getActiveProvider(state) {
|
|
|
385
385
|
}
|
|
386
386
|
|
|
387
387
|
function getProjectKey(state, projectName) {
|
|
388
|
-
|
|
388
|
+
if (projectName !== undefined) return projectNameFrom(projectName);
|
|
389
|
+
// Conversation-per-thread: with no explicit project the caller means "the
|
|
390
|
+
// current thread", whose key is the live conversation key — so each surface
|
|
391
|
+
// (Telegram DM, Spaces task, Kazee chat) owns a separate provider session and
|
|
392
|
+
// usage bucket, even when they resolve to one unified canonical speaker.
|
|
393
|
+
// Outside a chat turn (background jobs, CLI, tests) there is no conversation
|
|
394
|
+
// key, so we fall back to currentSession — byte-identical to prior behaviour.
|
|
395
|
+
const convo = currentConversationKey();
|
|
396
|
+
if (convo) return convo;
|
|
397
|
+
return projectNameFrom(state?.currentSession);
|
|
389
398
|
}
|
|
390
399
|
|
|
391
400
|
function getProviderSession(state, providerId, projectName) {
|
|
@@ -558,6 +567,48 @@ function createUserState(userId) {
|
|
|
558
567
|
return state;
|
|
559
568
|
}
|
|
560
569
|
|
|
570
|
+
// Carry the legacy shared "Workspace" conversation into its per-thread key on
|
|
571
|
+
// the channel that owns it, so moving to conversation-per-thread keying never
|
|
572
|
+
// resets a live conversation. In-memory + non-destructive: the "Workspace"
|
|
573
|
+
// bucket is copied (not moved) and the natural post-run save persists it, so a
|
|
574
|
+
// restart before that save simply re-seeds deterministically. Only the "home"
|
|
575
|
+
// channel — the one whose raw identity equals this state's canonical — inherits
|
|
576
|
+
// it; secondary unified channels (the owner's Spaces task) start clean, which
|
|
577
|
+
// is the whole point (they must not resume another thread's session).
|
|
578
|
+
function seedConversationContinuity(state) {
|
|
579
|
+
const convo = currentConversationKey();
|
|
580
|
+
if (!convo || convo === "Workspace") return;
|
|
581
|
+
if (normalizeCanonicalUserId(convo) !== normalizeCanonicalUserId(state.userId)) return;
|
|
582
|
+
if (!isRecord(state.activeSessions)) return;
|
|
583
|
+
const legacy = state.activeSessions.Workspace;
|
|
584
|
+
if (!isRecord(legacy) || isRecord(state.activeSessions[convo])) return;
|
|
585
|
+
state.activeSessions[convo] = { ...legacy };
|
|
586
|
+
if (isRecord(state.usageBySession)) {
|
|
587
|
+
const prefix = "Workspace";
|
|
588
|
+
for (const [key, usage] of Object.entries(state.usageBySession)) {
|
|
589
|
+
if (key.startsWith(prefix) && isRecord(usage)) {
|
|
590
|
+
const moved = `${convo}${key.slice(prefix.length)}`;
|
|
591
|
+
if (!state.usageBySession[moved]) state.usageBySession[moved] = { ...usage };
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
state.usageBySession = boundUsageBySession(state.usageBySession);
|
|
595
|
+
}
|
|
596
|
+
// Carry the shared "Workspace" session history (sessions.json) into the
|
|
597
|
+
// per-thread key too, so /sessions listing and resume-ownership checks — both
|
|
598
|
+
// keyed by conversationKey — keep the home channel's existing conversations.
|
|
599
|
+
// Best-effort and run-once: reached only on the same first call that seeds
|
|
600
|
+
// activeSessions above (later calls early-return at the activeSessions guard).
|
|
601
|
+
try {
|
|
602
|
+
const uid = normalizeCanonicalUserId(state.userId);
|
|
603
|
+
const all = loadSessions();
|
|
604
|
+
const userSessions = all[uid];
|
|
605
|
+
if (isRecord(userSessions) && Array.isArray(userSessions.Workspace) && !userSessions[convo]) {
|
|
606
|
+
userSessions[convo] = userSessions.Workspace.map((record) => ({ ...record, project: convo }));
|
|
607
|
+
saveSessions(all, { strict: false });
|
|
608
|
+
}
|
|
609
|
+
} catch (e) {}
|
|
610
|
+
}
|
|
611
|
+
|
|
561
612
|
function getUserState(userId) {
|
|
562
613
|
const id = normalizeCanonicalUserId(userId);
|
|
563
614
|
if (!userStates.has(id)) userStates.set(id, createUserState(id));
|
|
@@ -571,6 +622,7 @@ function getUserState(userId) {
|
|
|
571
622
|
}
|
|
572
623
|
}
|
|
573
624
|
state.channelId = currentChannelId() || state.channelId;
|
|
625
|
+
try { seedConversationContinuity(state); } catch (e) {}
|
|
574
626
|
return state;
|
|
575
627
|
}
|
|
576
628
|
|
package/core/turn-observer.js
CHANGED
|
@@ -32,6 +32,16 @@ function createTurnObserver({ state, store }) {
|
|
|
32
32
|
try { guarded = chatContext.run(store, () => require("./relationship").isCurrentSpeakerGuarded()); }
|
|
33
33
|
catch (e) { guarded = true; }
|
|
34
34
|
|
|
35
|
+
// Spaces is a customer-facing surface that renders neither Telegram HTML nor
|
|
36
|
+
// internal recall/tool banners meaningfully — a <b> banner shows the literal
|
|
37
|
+
// tags. Suppress all owner-only chat visibility there; memory + graph feeding
|
|
38
|
+
// still run, only the chat emissions are muted.
|
|
39
|
+
let suppressChatVisibility = false;
|
|
40
|
+
try {
|
|
41
|
+
const adapter = chatContext.run(store, () => currentAdapter());
|
|
42
|
+
suppressChatVisibility = adapter?.type === "spaces";
|
|
43
|
+
} catch (e) { suppressChatVisibility = false; }
|
|
44
|
+
|
|
35
45
|
const packsLib = require("./packs");
|
|
36
46
|
const entitiesLib = require("./entities");
|
|
37
47
|
const toolsLib = require("./tools");
|
|
@@ -40,7 +50,7 @@ function createTurnObserver({ state, store }) {
|
|
|
40
50
|
// Deduped per turn: a node touched twice announces once.
|
|
41
51
|
const notified = new Set();
|
|
42
52
|
const notify = (key, text, opts) => {
|
|
43
|
-
if (guarded) return;
|
|
53
|
+
if (guarded || suppressChatVisibility) return;
|
|
44
54
|
if (notified.has(key)) return;
|
|
45
55
|
notified.add(key);
|
|
46
56
|
try { chatContext.run(store, () => send(text, opts).catch(() => {})); }
|
package/package.json
CHANGED
package/test-provider-gateway.js
CHANGED
|
@@ -404,6 +404,7 @@ function queueProbe() {
|
|
|
404
404
|
currentChannelId: () => store.channelId,
|
|
405
405
|
currentAdapter: () => store.adapter,
|
|
406
406
|
currentUserId: () => store.userId,
|
|
407
|
+
currentConversationKey: () => store.conversationKey || null,
|
|
407
408
|
});
|
|
408
409
|
installStub("./core/io", {
|
|
409
410
|
send: async () => ({ ok: true, messageId: "queued", editable: true }),
|
|
@@ -32,14 +32,18 @@ assert.doesNotMatch(setup, /Your Claude Code bot is connected|Description=Claude
|
|
|
32
32
|
|
|
33
33
|
const pkg = JSON.parse(read("package.json"));
|
|
34
34
|
assert.match(pkg.description, /provider-agnostic coding-agent harness/i);
|
|
35
|
-
// 3.0.
|
|
36
|
-
//
|
|
37
|
-
//
|
|
38
|
-
//
|
|
39
|
-
//
|
|
40
|
-
//
|
|
41
|
-
//
|
|
42
|
-
|
|
35
|
+
// 3.0.22 approved by Sumeet 2026-07-14 ("Yeh push out"): per-thread conversations
|
|
36
|
+
// + Spaces output hygiene. The live conversation now keys by THREAD
|
|
37
|
+
// (transport:channelId via currentConversationKey off AsyncLocalStorage) instead
|
|
38
|
+
// of by canonical identity alone, so Telegram DM and each Spaces task get isolated
|
|
39
|
+
// sessions/transcripts while memory + guardrails + soul stay one global brain.
|
|
40
|
+
// seedConversationContinuity does a one-time non-destructive COPY of the legacy
|
|
41
|
+
// "Workspace" session/usage/history into the home conversationKey (anchored by
|
|
42
|
+
// canonical match; secondary channels start clean). turn-observer suppresses the
|
|
43
|
+
// Spaces <b> banner + tool-trace leak. 3.0.21 shipped the same work but its CI
|
|
44
|
+
// build failed on THIS gate (package.json bumped without the assertion) — nothing
|
|
45
|
+
// published under 3.0.21; 3.0.22 is the corrected re-ship.
|
|
46
|
+
assert.strictEqual(pkg.version, "3.0.22", "release version must remain unchanged without explicit approval");
|
|
43
47
|
for (const keyword of ["claude", "codex", "coding-agent", "provider-agnostic"]) {
|
|
44
48
|
assert.ok(pkg.keywords.includes(keyword), `package keyword missing: ${keyword}`);
|
|
45
49
|
}
|