@cachly-dev/mcp-server 0.9.13 → 0.9.15
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/dist/index.js +266 -8
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/index.ts +249 -8
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cachly-dev/mcp-server",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.15",
|
|
4
4
|
"mcpName": "io.github.cachly-dev/cachly-mcp",
|
|
5
5
|
"description": "Your AI forgets everything between sessions. cachly fixes that permanently. A causal brain that learns WHY things break — built from every bug fix, git commit, and CI run, without any explicit calls. Arrives pre-briefed each session. Predicts failures before they happen. Never re-researches the same problem. 63 MCP tools · Claude Code · Cursor · GitHub Copilot · Windsurf · Free tier · EU servers.",
|
|
6
6
|
"type": "module",
|
package/src/index.ts
CHANGED
|
@@ -4032,6 +4032,27 @@ const TOOLS = [
|
|
|
4032
4032
|
required: ['instance_id'],
|
|
4033
4033
|
},
|
|
4034
4034
|
},
|
|
4035
|
+
{
|
|
4036
|
+
name: 'brain_federate',
|
|
4037
|
+
description:
|
|
4038
|
+
'FedBrain Layer 6 — Private org knowledge transfer: copy CKG edges + lessons from a source brain ' +
|
|
4039
|
+
'into your brain for a specific domain (e.g. "billing", "auth", "deploy"). ' +
|
|
4040
|
+
'The new hire use case: one command gives you the senior engineer\'s 5 years of typed, confidence-weighted ' +
|
|
4041
|
+
'knowledge in your domain. Unlike syndicate_search (global, anonymous), brain_federate is org-private — ' +
|
|
4042
|
+
'both brains must be in the same Cachly org, or the source instance_id must be explicitly shared. ' +
|
|
4043
|
+
'Example: brain_federate(source="prod-brain-id", domain="billing", min_confidence=0.6)',
|
|
4044
|
+
inputSchema: {
|
|
4045
|
+
type: 'object',
|
|
4046
|
+
properties: {
|
|
4047
|
+
instance_id: { type: 'string', description: 'Your brain instance ID (destination)' },
|
|
4048
|
+
source: { type: 'string', description: 'Source brain instance ID to federate from' },
|
|
4049
|
+
domain: { type: 'string', description: 'Domain to transfer, e.g. "billing", "auth", "deploy", "infra". Use "*" for all domains.' },
|
|
4050
|
+
min_confidence: { type: 'number', description: 'Minimum edge confidence to transfer (default: 0.6)' },
|
|
4051
|
+
dry_run: { type: 'boolean', description: 'Preview what would be transferred without writing (default: false)' },
|
|
4052
|
+
},
|
|
4053
|
+
required: ['instance_id', 'source', 'domain'],
|
|
4054
|
+
},
|
|
4055
|
+
},
|
|
4035
4056
|
{
|
|
4036
4057
|
name: 'crystal_view',
|
|
4037
4058
|
description:
|
|
@@ -5302,6 +5323,8 @@ async function handleTool(name: string, args: Record<string, unknown>): Promise<
|
|
|
5302
5323
|
// Store conflict marker
|
|
5303
5324
|
const conflictKey = `cachly:ckg:conflict:${conceptId}`;
|
|
5304
5325
|
await redis.set(conflictKey, JSON.stringify({ topic, detected_at: new Date().toISOString(), fix_confidence: existEdge.confidence, fix_trials: existEdge.trials, failure_outcome: outcome }), 'EX', 60 * 60 * 24 * 90);
|
|
5326
|
+
// Auto-trigger MADC deliberation in background — never blocks learn_from_attempts
|
|
5327
|
+
handleTool('madc_deliberate', { instance_id, topic }).catch(() => undefined);
|
|
5305
5328
|
}
|
|
5306
5329
|
}
|
|
5307
5330
|
}
|
|
@@ -5918,6 +5941,34 @@ async function handleTool(name: string, args: Record<string, unknown>): Promise<
|
|
|
5918
5941
|
}
|
|
5919
5942
|
} catch { /* non-critical */ }
|
|
5920
5943
|
|
|
5944
|
+
// ── Layer 7 MCM: Blind Spot Detection ────────────────────────────────────
|
|
5945
|
+
// If the focus mentions a domain that has no CKG node → surface blind spot
|
|
5946
|
+
if (focus && focus.length > 3) {
|
|
5947
|
+
try {
|
|
5948
|
+
const focusTokens = focus.toLowerCase().replace(/[^a-z0-9\s:_-]/g, ' ').split(/\s+/).filter(t => t.length > 3);
|
|
5949
|
+
const blindSpots: string[] = [];
|
|
5950
|
+
for (const token of focusTokens.slice(0, 6)) {
|
|
5951
|
+
const nodeExists = await redis.exists(`cachly:ckg:node:${token}`);
|
|
5952
|
+
if (!nodeExists) {
|
|
5953
|
+
// Check if any node starts with this token (prefix match)
|
|
5954
|
+
const prefixKeys: string[] = [];
|
|
5955
|
+
const psStream = redis.scanStream({ match: `cachly:ckg:node:${token}*`, count: 10 });
|
|
5956
|
+
await new Promise<void>((res, rej) => { psStream.on('data', (b: string[]) => prefixKeys.push(...b)); psStream.on('end', res); psStream.on('error', rej); });
|
|
5957
|
+
if (prefixKeys.length === 0) blindSpots.push(token);
|
|
5958
|
+
}
|
|
5959
|
+
}
|
|
5960
|
+
if (blindSpots.length > 0) {
|
|
5961
|
+
lines.push(`🔭 **Blind spots detected for this focus:**`);
|
|
5962
|
+
for (const bs of blindSpots.slice(0, 3)) {
|
|
5963
|
+
lines.push(` ⬜ \`${bs}\` — no CKG knowledge. Suggestions:`);
|
|
5964
|
+
lines.push(` • \`brain_from_git(instance_id="...", concept="${bs}")\` — bootstrap from commits`);
|
|
5965
|
+
lines.push(` • \`fedbrain_search(query="${bs}")\` — search global commons`);
|
|
5966
|
+
}
|
|
5967
|
+
lines.push('');
|
|
5968
|
+
}
|
|
5969
|
+
} catch { /* non-critical */ }
|
|
5970
|
+
}
|
|
5971
|
+
|
|
5921
5972
|
if (focusLessons.length > 0) {
|
|
5922
5973
|
lines.push(`🎯 **Relevant for "${focus}":**`);
|
|
5923
5974
|
for (const l of focusLessons.slice(0, 4)) {
|
|
@@ -8071,8 +8122,51 @@ async function handleBulkLockStream(name: string, args: Record<string, unknown>)
|
|
|
8071
8122
|
``,
|
|
8072
8123
|
`🔍 **Active** (recalled at least once): ${recalled.length}`,
|
|
8073
8124
|
``,
|
|
8074
|
-
`💡 Run \`memory_consolidate\` to merge duplicates · \`knowledge_decay\` to see confidence scores.`,
|
|
8075
8125
|
].filter(s => s !== '');
|
|
8126
|
+
|
|
8127
|
+
// ── 10X brain_diff extensions ──────────────────────────────────────────
|
|
8128
|
+
// 1. Contested → Established (resolution nodes created in window)
|
|
8129
|
+
const resolutionKeys: string[] = [];
|
|
8130
|
+
const rsStream = redis.scanStream({ match: 'cachly:ckg:node:resolution-*', count: 100 });
|
|
8131
|
+
await new Promise<void>((res, rej) => { rsStream.on('data', (b: string[]) => resolutionKeys.push(...b)); rsStream.on('end', res); rsStream.on('error', rej); });
|
|
8132
|
+
const recentResolutions: Array<{ topic: string; resolution: string; ts: string }> = [];
|
|
8133
|
+
for (const rk of resolutionKeys) {
|
|
8134
|
+
const rr = await redis.get(rk);
|
|
8135
|
+
if (!rr) continue;
|
|
8136
|
+
try {
|
|
8137
|
+
const rn = JSON.parse(rr) as { topic?: string; resolution?: string; ts?: string };
|
|
8138
|
+
if (rn.ts && new Date(rn.ts).getTime() >= sinceMs) recentResolutions.push({ topic: rn.topic ?? rk, resolution: rn.resolution ?? 'unknown', ts: rn.ts });
|
|
8139
|
+
} catch { /* skip */ }
|
|
8140
|
+
}
|
|
8141
|
+
if (recentResolutions.length > 0) {
|
|
8142
|
+
lines.push(`🗳️ **MADC Resolutions** (${recentResolutions.length} contested beliefs resolved):`);
|
|
8143
|
+
for (const r of recentResolutions.slice(0, 5)) {
|
|
8144
|
+
const rIcon = r.resolution === 'unanimous_success' ? '✅' : r.resolution === 'unanimous_failure' ? '❌' : '⚠️';
|
|
8145
|
+
lines.push(` ${rIcon} \`${r.topic}\` → ${r.resolution}`);
|
|
8146
|
+
}
|
|
8147
|
+
lines.push('');
|
|
8148
|
+
}
|
|
8149
|
+
|
|
8150
|
+
// 2. New domains bootstrapped (domains in added lessons that weren't in older lessons)
|
|
8151
|
+
const existingDomains = new Set(updated.concat(recalled).map(l => l.topic.split(':')[0]));
|
|
8152
|
+
const newDomains = [...new Set(added.map(l => l.topic.split(':')[0]))].filter(d => !existingDomains.has(d));
|
|
8153
|
+
if (newDomains.length > 0) {
|
|
8154
|
+
lines.push(`🌱 **New domains bootstrapped:** ${newDomains.map(d => `\`${d}\``).join(', ')}`, '');
|
|
8155
|
+
}
|
|
8156
|
+
|
|
8157
|
+
// 3. FedBrain transfers received in window
|
|
8158
|
+
const fedHistRaw = await redis.lrange('cachly:fedbrain:federations', -20, -1);
|
|
8159
|
+
const recentFeds = fedHistRaw.map(r => { try { return JSON.parse(r) as { source: string; domain: string; transferred_at: string; nodes: number; edges: number }; } catch { return null; } })
|
|
8160
|
+
.filter(f => f && new Date(f!.transferred_at).getTime() >= sinceMs) as Array<{ source: string; domain: string; transferred_at: string; nodes: number; edges: number }>;
|
|
8161
|
+
if (recentFeds.length > 0) {
|
|
8162
|
+
lines.push(`🧠 **FedBrain transfers received (${recentFeds.length}):**`);
|
|
8163
|
+
for (const f of recentFeds.slice(0, 3)) {
|
|
8164
|
+
lines.push(` 📥 domain \`${f.domain}\` from \`${f.source.slice(0, 16)}…\` — ${f.nodes} nodes, ${f.edges} edges`);
|
|
8165
|
+
}
|
|
8166
|
+
lines.push('');
|
|
8167
|
+
}
|
|
8168
|
+
|
|
8169
|
+
lines.push(`💡 Run \`memory_consolidate\` to merge duplicates · \`knowledge_decay\` to see confidence scores.`);
|
|
8076
8170
|
return lines.join('\n');
|
|
8077
8171
|
}
|
|
8078
8172
|
|
|
@@ -8975,9 +9069,34 @@ smart_recall(
|
|
|
8975
9069
|
};
|
|
8976
9070
|
await redis.set(`cachly:ckg:node:${resNodeId}`, JSON.stringify(resNode));
|
|
8977
9071
|
|
|
8978
|
-
// Write contradicts edge if contested
|
|
9072
|
+
// Write contradicts edge if contested; decay loser confidence to 0.1 if unanimous
|
|
8979
9073
|
if (resolution === 'contested') {
|
|
8980
9074
|
await ckgUpdateEdge(redis, ckgSlug(topic), 'contradicts', resNodeId, false);
|
|
9075
|
+
} else {
|
|
9076
|
+
// Unanimous — demote the losing side's edges to near-zero confidence
|
|
9077
|
+
const loserOutcome = resolution === 'unanimous_success' ? 'failure' : 'success';
|
|
9078
|
+
const loserLessons = (loserOutcome === 'failure' ? failureLessons : successLessons);
|
|
9079
|
+
if (loserLessons.length > 0) {
|
|
9080
|
+
// Decay all 'fixes' edges from this concept that contradict the resolution
|
|
9081
|
+
const fromKeys = await redis.smembers(`cachly:ckg:idx:from:${ckgSlug(topic)}`);
|
|
9082
|
+
for (const fk of fromKeys) {
|
|
9083
|
+
const er = await redis.get(fk);
|
|
9084
|
+
if (!er) continue;
|
|
9085
|
+
try {
|
|
9086
|
+
const edge: CKGEdge = JSON.parse(er);
|
|
9087
|
+
if (edge.edgeType === 'fixes' && edge.confidence > 0.15) {
|
|
9088
|
+
if (resolution === 'unanimous_failure') {
|
|
9089
|
+
// Fix was wrong — decay to 0.1
|
|
9090
|
+
edge.confidence = 0.1;
|
|
9091
|
+
edge.last_updated = new Date().toISOString();
|
|
9092
|
+
await redis.set(fk, JSON.stringify(edge));
|
|
9093
|
+
}
|
|
9094
|
+
}
|
|
9095
|
+
} catch { /* skip corrupt edges */ }
|
|
9096
|
+
}
|
|
9097
|
+
}
|
|
9098
|
+
// Remove the conflict marker — deliberation resolved it
|
|
9099
|
+
await redis.del(`cachly:ckg:conflict:${ckgSlug(topic)}`);
|
|
8981
9100
|
}
|
|
8982
9101
|
|
|
8983
9102
|
const lines = [
|
|
@@ -9436,6 +9555,118 @@ smart_recall(
|
|
|
9436
9555
|
return lines.join('\n');
|
|
9437
9556
|
}
|
|
9438
9557
|
|
|
9558
|
+
// ── Layer 6: FedBrain — brain_federate ───────────────────────────────────
|
|
9559
|
+
case 'brain_federate': {
|
|
9560
|
+
const { instance_id, source, domain, min_confidence = 0.6, dry_run = false } = args as {
|
|
9561
|
+
instance_id: string; source: string; domain: string;
|
|
9562
|
+
min_confidence?: number; dry_run?: boolean;
|
|
9563
|
+
};
|
|
9564
|
+
if (instance_id === source) return `❌ Source and destination cannot be the same brain.`;
|
|
9565
|
+
|
|
9566
|
+
const destRedis = await getConnection(instance_id);
|
|
9567
|
+
const srcRedis = await getConnection(source);
|
|
9568
|
+
|
|
9569
|
+
const domainPattern = domain === '*' ? 'cachly:ckg:node:*' : `cachly:ckg:node:${domain}*`;
|
|
9570
|
+
|
|
9571
|
+
// Scan source CKG nodes matching the domain
|
|
9572
|
+
const nodeKeys: string[] = [];
|
|
9573
|
+
const nStream = srcRedis.scanStream({ match: domainPattern, count: 100 });
|
|
9574
|
+
await new Promise<void>((res, rej) => { nStream.on('data', (b: string[]) => nodeKeys.push(...b)); nStream.on('end', res); nStream.on('error', rej); });
|
|
9575
|
+
|
|
9576
|
+
if (nodeKeys.length === 0) {
|
|
9577
|
+
return [
|
|
9578
|
+
`🧠 **brain_federate: "${domain}"**`, '',
|
|
9579
|
+
`No CKG nodes found in source brain for domain \`${domain}\`.`,
|
|
9580
|
+
`The source brain may not have knowledge in this area yet.`,
|
|
9581
|
+
`Try: \`fedbrain_search(query="${domain}")\` to find global commons knowledge instead.`,
|
|
9582
|
+
].join('\n');
|
|
9583
|
+
}
|
|
9584
|
+
|
|
9585
|
+
// Transfer nodes + their outgoing edges
|
|
9586
|
+
let nodesTransferred = 0;
|
|
9587
|
+
let edgesTransferred = 0;
|
|
9588
|
+
let lessonsTransferred = 0;
|
|
9589
|
+
let skippedLowConf = 0;
|
|
9590
|
+
const transferLog: string[] = [];
|
|
9591
|
+
|
|
9592
|
+
for (const nk of nodeKeys) {
|
|
9593
|
+
const nodeRaw = await srcRedis.get(nk);
|
|
9594
|
+
if (!nodeRaw) continue;
|
|
9595
|
+
let node: CKGNode;
|
|
9596
|
+
try { node = JSON.parse(nodeRaw); } catch { continue; }
|
|
9597
|
+
|
|
9598
|
+
if (!dry_run) await destRedis.set(nk, nodeRaw);
|
|
9599
|
+
nodesTransferred++;
|
|
9600
|
+
|
|
9601
|
+
// Transfer outgoing edges for this node
|
|
9602
|
+
const edgeKeys = await srcRedis.smembers(`cachly:ckg:idx:from:${node.id}`);
|
|
9603
|
+
for (const ek of edgeKeys) {
|
|
9604
|
+
const edgeRaw = await srcRedis.get(ek);
|
|
9605
|
+
if (!edgeRaw) continue;
|
|
9606
|
+
let edge: CKGEdge;
|
|
9607
|
+
try { edge = JSON.parse(edgeRaw); } catch { continue; }
|
|
9608
|
+
if (edge.confidence < (min_confidence as number)) { skippedLowConf++; continue; }
|
|
9609
|
+
if (!dry_run) {
|
|
9610
|
+
await destRedis.set(ek, edgeRaw);
|
|
9611
|
+
await destRedis.sadd(`cachly:ckg:idx:from:${edge.from}`, ek);
|
|
9612
|
+
await destRedis.sadd(`cachly:ckg:idx:to:${edge.to}`, ek);
|
|
9613
|
+
}
|
|
9614
|
+
edgesTransferred++;
|
|
9615
|
+
}
|
|
9616
|
+
|
|
9617
|
+
// Transfer best-lesson for the same topic slug
|
|
9618
|
+
const lessonKey = `cachly:lesson:best:${node.id}`;
|
|
9619
|
+
const lessonRaw = await srcRedis.get(lessonKey);
|
|
9620
|
+
if (lessonRaw) {
|
|
9621
|
+
if (!dry_run) await destRedis.set(lessonKey, lessonRaw);
|
|
9622
|
+
lessonsTransferred++;
|
|
9623
|
+
if (transferLog.length < 8) {
|
|
9624
|
+
try {
|
|
9625
|
+
const l = JSON.parse(lessonRaw) as { topic: string; outcome: string; what_worked?: string };
|
|
9626
|
+
const icon = l.outcome === 'success' ? '✅' : l.outcome === 'failure' ? '❌' : '⚠️';
|
|
9627
|
+
transferLog.push(` ${icon} \`${l.topic}\` — ${(l.what_worked ?? '').slice(0, 80)}`);
|
|
9628
|
+
} catch { /* skip */ }
|
|
9629
|
+
}
|
|
9630
|
+
}
|
|
9631
|
+
}
|
|
9632
|
+
|
|
9633
|
+
// Record federation provenance
|
|
9634
|
+
if (!dry_run) {
|
|
9635
|
+
const provEntry = JSON.stringify({
|
|
9636
|
+
source, domain, transferred_at: new Date().toISOString(),
|
|
9637
|
+
nodes: nodesTransferred, edges: edgesTransferred, lessons: lessonsTransferred,
|
|
9638
|
+
});
|
|
9639
|
+
await destRedis.rpush('cachly:fedbrain:federations', provEntry);
|
|
9640
|
+
await destRedis.ltrim('cachly:fedbrain:federations', -50, -1);
|
|
9641
|
+
}
|
|
9642
|
+
|
|
9643
|
+
const dryTag = dry_run ? ' (DRY RUN — no writes)' : '';
|
|
9644
|
+
const lines = [
|
|
9645
|
+
`🧠 **brain_federate: "${domain}"**${dryTag}`, '',
|
|
9646
|
+
`📤 Source: \`${source}\``,
|
|
9647
|
+
`📥 Destination: \`${instance_id}\``,
|
|
9648
|
+
`🔍 Domain filter: \`${domain}\` | min_confidence: ${min_confidence}`,
|
|
9649
|
+
'',
|
|
9650
|
+
`### Transfer Summary`,
|
|
9651
|
+
` 🕸️ CKG nodes: ${nodesTransferred}`,
|
|
9652
|
+
` 🔗 CKG edges: ${edgesTransferred} (${skippedLowConf} skipped, confidence < ${min_confidence})`,
|
|
9653
|
+
` 📚 Lessons: ${lessonsTransferred}`,
|
|
9654
|
+
'',
|
|
9655
|
+
];
|
|
9656
|
+
if (transferLog.length > 0) {
|
|
9657
|
+
lines.push(`### Sample lessons transferred:`, ...transferLog);
|
|
9658
|
+
if (lessonsTransferred > transferLog.length) lines.push(` _... and ${lessonsTransferred - transferLog.length} more_`);
|
|
9659
|
+
lines.push('');
|
|
9660
|
+
}
|
|
9661
|
+
if (dry_run) {
|
|
9662
|
+
lines.push(`💡 Run without \`dry_run: true\` to apply the transfer.`);
|
|
9663
|
+
} else {
|
|
9664
|
+
lines.push(`✅ Transfer complete. Your brain now has the \`${domain}\` knowledge from \`${source}\`.`);
|
|
9665
|
+
lines.push(`🔍 Explore: \`ckg_inspect(concept="${domain}")\` | \`recall_best_solution(topic="${domain}:...")\``);
|
|
9666
|
+
}
|
|
9667
|
+
return lines.join('\n');
|
|
9668
|
+
}
|
|
9669
|
+
|
|
9439
9670
|
// ── crystal_view ──────────────────────────────────────────────────────────
|
|
9440
9671
|
case 'crystal_view': {
|
|
9441
9672
|
const { instance_id, show_raw = false } = args as { instance_id: string; show_raw?: boolean };
|
|
@@ -9861,6 +10092,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: TOOLS }))
|
|
|
9861
10092
|
|
|
9862
10093
|
let _autoSessionInstanceId: string | null = null;
|
|
9863
10094
|
let _autoSessionStarted = false;
|
|
10095
|
+
let _autoSessionStarting: Promise<void> | null = null; // guard against double-start race
|
|
9864
10096
|
let _autoSessionToolCount = 0;
|
|
9865
10097
|
|
|
9866
10098
|
// ── brain_from_git concurrency limiter ────────────────────────────────────────
|
|
@@ -9880,11 +10112,17 @@ function _gitSemRelease(): void {
|
|
|
9880
10112
|
|
|
9881
10113
|
async function autoStartSession(instanceId: string): Promise<void> {
|
|
9882
10114
|
if (_autoSessionStarted) return;
|
|
9883
|
-
|
|
9884
|
-
|
|
9885
|
-
|
|
9886
|
-
|
|
9887
|
-
|
|
10115
|
+
// If another call already started the session, await it instead of double-starting.
|
|
10116
|
+
if (_autoSessionStarting) { await _autoSessionStarting; return; }
|
|
10117
|
+
_autoSessionStarting = (async () => {
|
|
10118
|
+
_autoSessionStarted = true;
|
|
10119
|
+
_autoSessionInstanceId = instanceId;
|
|
10120
|
+
try {
|
|
10121
|
+
await handleTool('session_start', { instance_id: instanceId, focus: 'auto (MCP session)' });
|
|
10122
|
+
} catch { /* non-fatal — session tracking is a best-effort feature */ }
|
|
10123
|
+
_autoSessionStarting = null;
|
|
10124
|
+
})();
|
|
10125
|
+
await _autoSessionStarting;
|
|
9888
10126
|
|
|
9889
10127
|
// Auto-index the project if it hasn't been indexed in the last 24h.
|
|
9890
10128
|
// This is the main lever for growing token savings: more indexed code →
|
|
@@ -9930,11 +10168,14 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
9930
10168
|
// Auto-start brain session on first tool call that has an instance_id.
|
|
9931
10169
|
// Skip session management tools to avoid recursion.
|
|
9932
10170
|
const sessionTools = new Set(['session_start', 'session_end', 'auto_learn_session']);
|
|
9933
|
-
if (!sessionTools.has(name) && !_autoSessionStarted) {
|
|
10171
|
+
if (!sessionTools.has(name) && !_autoSessionStarted && !_autoSessionStarting) {
|
|
9934
10172
|
const instanceId = (args as Record<string, unknown>)?.instance_id as string | undefined;
|
|
9935
10173
|
if (instanceId) {
|
|
9936
10174
|
await autoStartSession(instanceId).catch(() => undefined);
|
|
9937
10175
|
}
|
|
10176
|
+
} else if (_autoSessionStarting) {
|
|
10177
|
+
// Another concurrent call is mid-start — wait for it before proceeding.
|
|
10178
|
+
await _autoSessionStarting.catch(() => undefined);
|
|
9938
10179
|
}
|
|
9939
10180
|
if (!sessionTools.has(name)) _autoSessionToolCount++;
|
|
9940
10181
|
|