@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/dist/index.js
CHANGED
|
@@ -3904,6 +3904,26 @@ const TOOLS = [
|
|
|
3904
3904
|
required: ['instance_id'],
|
|
3905
3905
|
},
|
|
3906
3906
|
},
|
|
3907
|
+
{
|
|
3908
|
+
name: 'brain_federate',
|
|
3909
|
+
description: 'FedBrain Layer 6 — Private org knowledge transfer: copy CKG edges + lessons from a source brain ' +
|
|
3910
|
+
'into your brain for a specific domain (e.g. "billing", "auth", "deploy"). ' +
|
|
3911
|
+
'The new hire use case: one command gives you the senior engineer\'s 5 years of typed, confidence-weighted ' +
|
|
3912
|
+
'knowledge in your domain. Unlike syndicate_search (global, anonymous), brain_federate is org-private — ' +
|
|
3913
|
+
'both brains must be in the same Cachly org, or the source instance_id must be explicitly shared. ' +
|
|
3914
|
+
'Example: brain_federate(source="prod-brain-id", domain="billing", min_confidence=0.6)',
|
|
3915
|
+
inputSchema: {
|
|
3916
|
+
type: 'object',
|
|
3917
|
+
properties: {
|
|
3918
|
+
instance_id: { type: 'string', description: 'Your brain instance ID (destination)' },
|
|
3919
|
+
source: { type: 'string', description: 'Source brain instance ID to federate from' },
|
|
3920
|
+
domain: { type: 'string', description: 'Domain to transfer, e.g. "billing", "auth", "deploy", "infra". Use "*" for all domains.' },
|
|
3921
|
+
min_confidence: { type: 'number', description: 'Minimum edge confidence to transfer (default: 0.6)' },
|
|
3922
|
+
dry_run: { type: 'boolean', description: 'Preview what would be transferred without writing (default: false)' },
|
|
3923
|
+
},
|
|
3924
|
+
required: ['instance_id', 'source', 'domain'],
|
|
3925
|
+
},
|
|
3926
|
+
},
|
|
3907
3927
|
{
|
|
3908
3928
|
name: 'crystal_view',
|
|
3909
3929
|
description: 'Inspect the current Memory Crystal — the compressed wisdom distilled from all past sessions. ' +
|
|
@@ -4979,6 +4999,8 @@ async function handleTool(name, args) {
|
|
|
4979
4999
|
// Store conflict marker
|
|
4980
5000
|
const conflictKey = `cachly:ckg:conflict:${conceptId}`;
|
|
4981
5001
|
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);
|
|
5002
|
+
// Auto-trigger MADC deliberation in background — never blocks learn_from_attempts
|
|
5003
|
+
handleTool('madc_deliberate', { instance_id, topic }).catch(() => undefined);
|
|
4982
5004
|
}
|
|
4983
5005
|
}
|
|
4984
5006
|
}
|
|
@@ -5556,6 +5578,35 @@ async function handleTool(name, args) {
|
|
|
5556
5578
|
}
|
|
5557
5579
|
}
|
|
5558
5580
|
catch { /* non-critical */ }
|
|
5581
|
+
// ── Layer 7 MCM: Blind Spot Detection ────────────────────────────────────
|
|
5582
|
+
// If the focus mentions a domain that has no CKG node → surface blind spot
|
|
5583
|
+
if (focus && focus.length > 3) {
|
|
5584
|
+
try {
|
|
5585
|
+
const focusTokens = focus.toLowerCase().replace(/[^a-z0-9\s:_-]/g, ' ').split(/\s+/).filter(t => t.length > 3);
|
|
5586
|
+
const blindSpots = [];
|
|
5587
|
+
for (const token of focusTokens.slice(0, 6)) {
|
|
5588
|
+
const nodeExists = await redis.exists(`cachly:ckg:node:${token}`);
|
|
5589
|
+
if (!nodeExists) {
|
|
5590
|
+
// Check if any node starts with this token (prefix match)
|
|
5591
|
+
const prefixKeys = [];
|
|
5592
|
+
const psStream = redis.scanStream({ match: `cachly:ckg:node:${token}*`, count: 10 });
|
|
5593
|
+
await new Promise((res, rej) => { psStream.on('data', (b) => prefixKeys.push(...b)); psStream.on('end', res); psStream.on('error', rej); });
|
|
5594
|
+
if (prefixKeys.length === 0)
|
|
5595
|
+
blindSpots.push(token);
|
|
5596
|
+
}
|
|
5597
|
+
}
|
|
5598
|
+
if (blindSpots.length > 0) {
|
|
5599
|
+
lines.push(`🔭 **Blind spots detected for this focus:**`);
|
|
5600
|
+
for (const bs of blindSpots.slice(0, 3)) {
|
|
5601
|
+
lines.push(` ⬜ \`${bs}\` — no CKG knowledge. Suggestions:`);
|
|
5602
|
+
lines.push(` • \`brain_from_git(instance_id="...", concept="${bs}")\` — bootstrap from commits`);
|
|
5603
|
+
lines.push(` • \`fedbrain_search(query="${bs}")\` — search global commons`);
|
|
5604
|
+
}
|
|
5605
|
+
lines.push('');
|
|
5606
|
+
}
|
|
5607
|
+
}
|
|
5608
|
+
catch { /* non-critical */ }
|
|
5609
|
+
}
|
|
5559
5610
|
if (focusLessons.length > 0) {
|
|
5560
5611
|
lines.push(`🎯 **Relevant for "${focus}":**`);
|
|
5561
5612
|
for (const l of focusLessons.slice(0, 4)) {
|
|
@@ -7506,8 +7557,55 @@ async function handleBulkLockStream(name, args) {
|
|
|
7506
7557
|
``,
|
|
7507
7558
|
`🔍 **Active** (recalled at least once): ${recalled.length}`,
|
|
7508
7559
|
``,
|
|
7509
|
-
`💡 Run \`memory_consolidate\` to merge duplicates · \`knowledge_decay\` to see confidence scores.`,
|
|
7510
7560
|
].filter(s => s !== '');
|
|
7561
|
+
// ── 10X brain_diff extensions ──────────────────────────────────────────
|
|
7562
|
+
// 1. Contested → Established (resolution nodes created in window)
|
|
7563
|
+
const resolutionKeys = [];
|
|
7564
|
+
const rsStream = redis.scanStream({ match: 'cachly:ckg:node:resolution-*', count: 100 });
|
|
7565
|
+
await new Promise((res, rej) => { rsStream.on('data', (b) => resolutionKeys.push(...b)); rsStream.on('end', res); rsStream.on('error', rej); });
|
|
7566
|
+
const recentResolutions = [];
|
|
7567
|
+
for (const rk of resolutionKeys) {
|
|
7568
|
+
const rr = await redis.get(rk);
|
|
7569
|
+
if (!rr)
|
|
7570
|
+
continue;
|
|
7571
|
+
try {
|
|
7572
|
+
const rn = JSON.parse(rr);
|
|
7573
|
+
if (rn.ts && new Date(rn.ts).getTime() >= sinceMs)
|
|
7574
|
+
recentResolutions.push({ topic: rn.topic ?? rk, resolution: rn.resolution ?? 'unknown', ts: rn.ts });
|
|
7575
|
+
}
|
|
7576
|
+
catch { /* skip */ }
|
|
7577
|
+
}
|
|
7578
|
+
if (recentResolutions.length > 0) {
|
|
7579
|
+
lines.push(`🗳️ **MADC Resolutions** (${recentResolutions.length} contested beliefs resolved):`);
|
|
7580
|
+
for (const r of recentResolutions.slice(0, 5)) {
|
|
7581
|
+
const rIcon = r.resolution === 'unanimous_success' ? '✅' : r.resolution === 'unanimous_failure' ? '❌' : '⚠️';
|
|
7582
|
+
lines.push(` ${rIcon} \`${r.topic}\` → ${r.resolution}`);
|
|
7583
|
+
}
|
|
7584
|
+
lines.push('');
|
|
7585
|
+
}
|
|
7586
|
+
// 2. New domains bootstrapped (domains in added lessons that weren't in older lessons)
|
|
7587
|
+
const existingDomains = new Set(updated.concat(recalled).map(l => l.topic.split(':')[0]));
|
|
7588
|
+
const newDomains = [...new Set(added.map(l => l.topic.split(':')[0]))].filter(d => !existingDomains.has(d));
|
|
7589
|
+
if (newDomains.length > 0) {
|
|
7590
|
+
lines.push(`🌱 **New domains bootstrapped:** ${newDomains.map(d => `\`${d}\``).join(', ')}`, '');
|
|
7591
|
+
}
|
|
7592
|
+
// 3. FedBrain transfers received in window
|
|
7593
|
+
const fedHistRaw = await redis.lrange('cachly:fedbrain:federations', -20, -1);
|
|
7594
|
+
const recentFeds = fedHistRaw.map(r => { try {
|
|
7595
|
+
return JSON.parse(r);
|
|
7596
|
+
}
|
|
7597
|
+
catch {
|
|
7598
|
+
return null;
|
|
7599
|
+
} })
|
|
7600
|
+
.filter(f => f && new Date(f.transferred_at).getTime() >= sinceMs);
|
|
7601
|
+
if (recentFeds.length > 0) {
|
|
7602
|
+
lines.push(`🧠 **FedBrain transfers received (${recentFeds.length}):**`);
|
|
7603
|
+
for (const f of recentFeds.slice(0, 3)) {
|
|
7604
|
+
lines.push(` 📥 domain \`${f.domain}\` from \`${f.source.slice(0, 16)}…\` — ${f.nodes} nodes, ${f.edges} edges`);
|
|
7605
|
+
}
|
|
7606
|
+
lines.push('');
|
|
7607
|
+
}
|
|
7608
|
+
lines.push(`💡 Run \`memory_consolidate\` to merge duplicates · \`knowledge_decay\` to see confidence scores.`);
|
|
7511
7609
|
return lines.join('\n');
|
|
7512
7610
|
}
|
|
7513
7611
|
// ── v0.6 Cognitive Cache: causal_trace ───────────────────────────────────
|
|
@@ -8279,10 +8377,38 @@ smart_recall(
|
|
|
8279
8377
|
votes: { success: successVotes, failure: failureVotes, abstain: abstainVotes },
|
|
8280
8378
|
};
|
|
8281
8379
|
await redis.set(`cachly:ckg:node:${resNodeId}`, JSON.stringify(resNode));
|
|
8282
|
-
// Write contradicts edge if contested
|
|
8380
|
+
// Write contradicts edge if contested; decay loser confidence to 0.1 if unanimous
|
|
8283
8381
|
if (resolution === 'contested') {
|
|
8284
8382
|
await ckgUpdateEdge(redis, ckgSlug(topic), 'contradicts', resNodeId, false);
|
|
8285
8383
|
}
|
|
8384
|
+
else {
|
|
8385
|
+
// Unanimous — demote the losing side's edges to near-zero confidence
|
|
8386
|
+
const loserOutcome = resolution === 'unanimous_success' ? 'failure' : 'success';
|
|
8387
|
+
const loserLessons = (loserOutcome === 'failure' ? failureLessons : successLessons);
|
|
8388
|
+
if (loserLessons.length > 0) {
|
|
8389
|
+
// Decay all 'fixes' edges from this concept that contradict the resolution
|
|
8390
|
+
const fromKeys = await redis.smembers(`cachly:ckg:idx:from:${ckgSlug(topic)}`);
|
|
8391
|
+
for (const fk of fromKeys) {
|
|
8392
|
+
const er = await redis.get(fk);
|
|
8393
|
+
if (!er)
|
|
8394
|
+
continue;
|
|
8395
|
+
try {
|
|
8396
|
+
const edge = JSON.parse(er);
|
|
8397
|
+
if (edge.edgeType === 'fixes' && edge.confidence > 0.15) {
|
|
8398
|
+
if (resolution === 'unanimous_failure') {
|
|
8399
|
+
// Fix was wrong — decay to 0.1
|
|
8400
|
+
edge.confidence = 0.1;
|
|
8401
|
+
edge.last_updated = new Date().toISOString();
|
|
8402
|
+
await redis.set(fk, JSON.stringify(edge));
|
|
8403
|
+
}
|
|
8404
|
+
}
|
|
8405
|
+
}
|
|
8406
|
+
catch { /* skip corrupt edges */ }
|
|
8407
|
+
}
|
|
8408
|
+
}
|
|
8409
|
+
// Remove the conflict marker — deliberation resolved it
|
|
8410
|
+
await redis.del(`cachly:ckg:conflict:${ckgSlug(topic)}`);
|
|
8411
|
+
}
|
|
8286
8412
|
const lines = [
|
|
8287
8413
|
`🗳️ **MADC Deliberation: "${topic}"**`, '',
|
|
8288
8414
|
`📊 Evidence: ${successLessons.length} success/partial vs ${failureLessons.length} failure entries (${history.length} total)`,
|
|
@@ -8690,6 +8816,124 @@ smart_recall(
|
|
|
8690
8816
|
lines.push('', '---', `**Contribute:** \`fedbrain_contribute(lesson_key="fix:...")\``, `**Search:** \`fedbrain_search(query="...")\``, `**Confirm:** \`fedbrain_confirm(topic="...", outcome="worked")\``);
|
|
8691
8817
|
return lines.join('\n');
|
|
8692
8818
|
}
|
|
8819
|
+
// ── Layer 6: FedBrain — brain_federate ───────────────────────────────────
|
|
8820
|
+
case 'brain_federate': {
|
|
8821
|
+
const { instance_id, source, domain, min_confidence = 0.6, dry_run = false } = args;
|
|
8822
|
+
if (instance_id === source)
|
|
8823
|
+
return `❌ Source and destination cannot be the same brain.`;
|
|
8824
|
+
const destRedis = await getConnection(instance_id);
|
|
8825
|
+
const srcRedis = await getConnection(source);
|
|
8826
|
+
const domainPattern = domain === '*' ? 'cachly:ckg:node:*' : `cachly:ckg:node:${domain}*`;
|
|
8827
|
+
// Scan source CKG nodes matching the domain
|
|
8828
|
+
const nodeKeys = [];
|
|
8829
|
+
const nStream = srcRedis.scanStream({ match: domainPattern, count: 100 });
|
|
8830
|
+
await new Promise((res, rej) => { nStream.on('data', (b) => nodeKeys.push(...b)); nStream.on('end', res); nStream.on('error', rej); });
|
|
8831
|
+
if (nodeKeys.length === 0) {
|
|
8832
|
+
return [
|
|
8833
|
+
`🧠 **brain_federate: "${domain}"**`, '',
|
|
8834
|
+
`No CKG nodes found in source brain for domain \`${domain}\`.`,
|
|
8835
|
+
`The source brain may not have knowledge in this area yet.`,
|
|
8836
|
+
`Try: \`fedbrain_search(query="${domain}")\` to find global commons knowledge instead.`,
|
|
8837
|
+
].join('\n');
|
|
8838
|
+
}
|
|
8839
|
+
// Transfer nodes + their outgoing edges
|
|
8840
|
+
let nodesTransferred = 0;
|
|
8841
|
+
let edgesTransferred = 0;
|
|
8842
|
+
let lessonsTransferred = 0;
|
|
8843
|
+
let skippedLowConf = 0;
|
|
8844
|
+
const transferLog = [];
|
|
8845
|
+
for (const nk of nodeKeys) {
|
|
8846
|
+
const nodeRaw = await srcRedis.get(nk);
|
|
8847
|
+
if (!nodeRaw)
|
|
8848
|
+
continue;
|
|
8849
|
+
let node;
|
|
8850
|
+
try {
|
|
8851
|
+
node = JSON.parse(nodeRaw);
|
|
8852
|
+
}
|
|
8853
|
+
catch {
|
|
8854
|
+
continue;
|
|
8855
|
+
}
|
|
8856
|
+
if (!dry_run)
|
|
8857
|
+
await destRedis.set(nk, nodeRaw);
|
|
8858
|
+
nodesTransferred++;
|
|
8859
|
+
// Transfer outgoing edges for this node
|
|
8860
|
+
const edgeKeys = await srcRedis.smembers(`cachly:ckg:idx:from:${node.id}`);
|
|
8861
|
+
for (const ek of edgeKeys) {
|
|
8862
|
+
const edgeRaw = await srcRedis.get(ek);
|
|
8863
|
+
if (!edgeRaw)
|
|
8864
|
+
continue;
|
|
8865
|
+
let edge;
|
|
8866
|
+
try {
|
|
8867
|
+
edge = JSON.parse(edgeRaw);
|
|
8868
|
+
}
|
|
8869
|
+
catch {
|
|
8870
|
+
continue;
|
|
8871
|
+
}
|
|
8872
|
+
if (edge.confidence < min_confidence) {
|
|
8873
|
+
skippedLowConf++;
|
|
8874
|
+
continue;
|
|
8875
|
+
}
|
|
8876
|
+
if (!dry_run) {
|
|
8877
|
+
await destRedis.set(ek, edgeRaw);
|
|
8878
|
+
await destRedis.sadd(`cachly:ckg:idx:from:${edge.from}`, ek);
|
|
8879
|
+
await destRedis.sadd(`cachly:ckg:idx:to:${edge.to}`, ek);
|
|
8880
|
+
}
|
|
8881
|
+
edgesTransferred++;
|
|
8882
|
+
}
|
|
8883
|
+
// Transfer best-lesson for the same topic slug
|
|
8884
|
+
const lessonKey = `cachly:lesson:best:${node.id}`;
|
|
8885
|
+
const lessonRaw = await srcRedis.get(lessonKey);
|
|
8886
|
+
if (lessonRaw) {
|
|
8887
|
+
if (!dry_run)
|
|
8888
|
+
await destRedis.set(lessonKey, lessonRaw);
|
|
8889
|
+
lessonsTransferred++;
|
|
8890
|
+
if (transferLog.length < 8) {
|
|
8891
|
+
try {
|
|
8892
|
+
const l = JSON.parse(lessonRaw);
|
|
8893
|
+
const icon = l.outcome === 'success' ? '✅' : l.outcome === 'failure' ? '❌' : '⚠️';
|
|
8894
|
+
transferLog.push(` ${icon} \`${l.topic}\` — ${(l.what_worked ?? '').slice(0, 80)}`);
|
|
8895
|
+
}
|
|
8896
|
+
catch { /* skip */ }
|
|
8897
|
+
}
|
|
8898
|
+
}
|
|
8899
|
+
}
|
|
8900
|
+
// Record federation provenance
|
|
8901
|
+
if (!dry_run) {
|
|
8902
|
+
const provEntry = JSON.stringify({
|
|
8903
|
+
source, domain, transferred_at: new Date().toISOString(),
|
|
8904
|
+
nodes: nodesTransferred, edges: edgesTransferred, lessons: lessonsTransferred,
|
|
8905
|
+
});
|
|
8906
|
+
await destRedis.rpush('cachly:fedbrain:federations', provEntry);
|
|
8907
|
+
await destRedis.ltrim('cachly:fedbrain:federations', -50, -1);
|
|
8908
|
+
}
|
|
8909
|
+
const dryTag = dry_run ? ' (DRY RUN — no writes)' : '';
|
|
8910
|
+
const lines = [
|
|
8911
|
+
`🧠 **brain_federate: "${domain}"**${dryTag}`, '',
|
|
8912
|
+
`📤 Source: \`${source}\``,
|
|
8913
|
+
`📥 Destination: \`${instance_id}\``,
|
|
8914
|
+
`🔍 Domain filter: \`${domain}\` | min_confidence: ${min_confidence}`,
|
|
8915
|
+
'',
|
|
8916
|
+
`### Transfer Summary`,
|
|
8917
|
+
` 🕸️ CKG nodes: ${nodesTransferred}`,
|
|
8918
|
+
` 🔗 CKG edges: ${edgesTransferred} (${skippedLowConf} skipped, confidence < ${min_confidence})`,
|
|
8919
|
+
` 📚 Lessons: ${lessonsTransferred}`,
|
|
8920
|
+
'',
|
|
8921
|
+
];
|
|
8922
|
+
if (transferLog.length > 0) {
|
|
8923
|
+
lines.push(`### Sample lessons transferred:`, ...transferLog);
|
|
8924
|
+
if (lessonsTransferred > transferLog.length)
|
|
8925
|
+
lines.push(` _... and ${lessonsTransferred - transferLog.length} more_`);
|
|
8926
|
+
lines.push('');
|
|
8927
|
+
}
|
|
8928
|
+
if (dry_run) {
|
|
8929
|
+
lines.push(`💡 Run without \`dry_run: true\` to apply the transfer.`);
|
|
8930
|
+
}
|
|
8931
|
+
else {
|
|
8932
|
+
lines.push(`✅ Transfer complete. Your brain now has the \`${domain}\` knowledge from \`${source}\`.`);
|
|
8933
|
+
lines.push(`🔍 Explore: \`ckg_inspect(concept="${domain}")\` | \`recall_best_solution(topic="${domain}:...")\``);
|
|
8934
|
+
}
|
|
8935
|
+
return lines.join('\n');
|
|
8936
|
+
}
|
|
8693
8937
|
// ── crystal_view ──────────────────────────────────────────────────────────
|
|
8694
8938
|
case 'crystal_view': {
|
|
8695
8939
|
const { instance_id, show_raw = false } = args;
|
|
@@ -9091,6 +9335,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: TOOLS }))
|
|
|
9091
9335
|
// session_start/session_end manually.
|
|
9092
9336
|
let _autoSessionInstanceId = null;
|
|
9093
9337
|
let _autoSessionStarted = false;
|
|
9338
|
+
let _autoSessionStarting = null; // guard against double-start race
|
|
9094
9339
|
let _autoSessionToolCount = 0;
|
|
9095
9340
|
// ── brain_from_git concurrency limiter ────────────────────────────────────────
|
|
9096
9341
|
// Spawning many `git log` subprocesses in parallel can fork-bomb the host.
|
|
@@ -9117,12 +9362,21 @@ function _gitSemRelease() {
|
|
|
9117
9362
|
async function autoStartSession(instanceId) {
|
|
9118
9363
|
if (_autoSessionStarted)
|
|
9119
9364
|
return;
|
|
9120
|
-
|
|
9121
|
-
|
|
9122
|
-
|
|
9123
|
-
|
|
9365
|
+
// If another call already started the session, await it instead of double-starting.
|
|
9366
|
+
if (_autoSessionStarting) {
|
|
9367
|
+
await _autoSessionStarting;
|
|
9368
|
+
return;
|
|
9124
9369
|
}
|
|
9125
|
-
|
|
9370
|
+
_autoSessionStarting = (async () => {
|
|
9371
|
+
_autoSessionStarted = true;
|
|
9372
|
+
_autoSessionInstanceId = instanceId;
|
|
9373
|
+
try {
|
|
9374
|
+
await handleTool('session_start', { instance_id: instanceId, focus: 'auto (MCP session)' });
|
|
9375
|
+
}
|
|
9376
|
+
catch { /* non-fatal — session tracking is a best-effort feature */ }
|
|
9377
|
+
_autoSessionStarting = null;
|
|
9378
|
+
})();
|
|
9379
|
+
await _autoSessionStarting;
|
|
9126
9380
|
// Auto-index the project if it hasn't been indexed in the last 24h.
|
|
9127
9381
|
// This is the main lever for growing token savings: more indexed code →
|
|
9128
9382
|
// more semantic cache hits → fewer LLM calls for repeated questions.
|
|
@@ -9167,12 +9421,16 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
9167
9421
|
// Auto-start brain session on first tool call that has an instance_id.
|
|
9168
9422
|
// Skip session management tools to avoid recursion.
|
|
9169
9423
|
const sessionTools = new Set(['session_start', 'session_end', 'auto_learn_session']);
|
|
9170
|
-
if (!sessionTools.has(name) && !_autoSessionStarted) {
|
|
9424
|
+
if (!sessionTools.has(name) && !_autoSessionStarted && !_autoSessionStarting) {
|
|
9171
9425
|
const instanceId = args?.instance_id;
|
|
9172
9426
|
if (instanceId) {
|
|
9173
9427
|
await autoStartSession(instanceId).catch(() => undefined);
|
|
9174
9428
|
}
|
|
9175
9429
|
}
|
|
9430
|
+
else if (_autoSessionStarting) {
|
|
9431
|
+
// Another concurrent call is mid-start — wait for it before proceeding.
|
|
9432
|
+
await _autoSessionStarting.catch(() => undefined);
|
|
9433
|
+
}
|
|
9176
9434
|
if (!sessionTools.has(name))
|
|
9177
9435
|
_autoSessionToolCount++;
|
|
9178
9436
|
try {
|