Update coverage reports timestamp

Implement retry abort mechanism in OllamaClient
Add caching and cancellation support to VaultIndexer
This commit is contained in:
2026-05-07 00:48:12 +02:00
parent 4ddef0b98f
commit 435e1f0d93
11 changed files with 179 additions and 48 deletions
+1 -1
View File
@@ -1501,7 +1501,7 @@ export class ChatView extends ItemView {
<div class='footer quiet pad2 space-top1 center small'>
Code coverage generated by
<a href="https://istanbul.js.org/" target="_blank" rel="noopener noreferrer">istanbul</a>
at 2026-05-06T22:42:47.543Z
at 2026-05-06T22:45:19.738Z
</div>
<script src="prettify.js"></script>
<script>
+1 -1
View File
@@ -475,7 +475,7 @@ export class ErrorHandler {
<div class='footer quiet pad2 space-top1 center small'>
Code coverage generated by
<a href="https://istanbul.js.org/" target="_blank" rel="noopener noreferrer">istanbul</a>
at 2026-05-06T22:42:47.543Z
at 2026-05-06T22:45:19.738Z
</div>
<script src="prettify.js"></script>
<script>
+1 -1
View File
@@ -191,7 +191,7 @@
<div class='footer quiet pad2 space-top1 center small'>
Code coverage generated by
<a href="https://istanbul.js.org/" target="_blank" rel="noopener noreferrer">istanbul</a>
at 2026-05-06T22:42:47.543Z
at 2026-05-06T22:45:19.738Z
</div>
<script src="prettify.js"></script>
<script>
+1 -1
View File
@@ -946,7 +946,7 @@ export class OllamaClient {
<div class='footer quiet pad2 space-top1 center small'>
Code coverage generated by
<a href="https://istanbul.js.org/" target="_blank" rel="noopener noreferrer">istanbul</a>
at 2026-05-06T22:42:47.543Z
at 2026-05-06T22:45:19.738Z
</div>
<script src="prettify.js"></script>
<script>
+1 -1
View File
@@ -463,7 +463,7 @@ export class ToolExecutor {
<div class='footer quiet pad2 space-top1 center small'>
Code coverage generated by
<a href="https://istanbul.js.org/" target="_blank" rel="noopener noreferrer">istanbul</a>
at 2026-05-06T22:42:47.543Z
at 2026-05-06T22:45:19.738Z
</div>
<script src="prettify.js"></script>
<script>
+1 -1
View File
@@ -604,7 +604,7 @@ export interface VaultIndexEntry {
<div class='footer quiet pad2 space-top1 center small'>
Code coverage generated by
<a href="https://istanbul.js.org/" target="_blank" rel="noopener noreferrer">istanbul</a>
at 2026-05-06T22:42:47.543Z
at 2026-05-06T22:45:19.738Z
</div>
<script src="prettify.js"></script>
<script>
+1 -1
View File
@@ -643,7 +643,7 @@ export function safeParseJson(jsonString: string): unknown {
<div class='footer quiet pad2 space-top1 center small'>
Code coverage generated by
<a href="https://istanbul.js.org/" target="_blank" rel="noopener noreferrer">istanbul</a>
at 2026-05-06T22:42:47.543Z
at 2026-05-06T22:45:19.738Z
</div>
<script src="prettify.js"></script>
<script>
+1 -1
View File
@@ -1183,7 +1183,7 @@ export function <span class="fstat-no" title="function not covered" >createVault
<div class='footer quiet pad2 space-top1 center small'>
Code coverage generated by
<a href="https://istanbul.js.org/" target="_blank" rel="noopener noreferrer">istanbul</a>
at 2026-05-06T22:42:47.543Z
at 2026-05-06T22:45:19.738Z
</div>
<script src="prettify.js"></script>
<script>
+60 -2
View File
@@ -52,7 +52,32 @@ class OllamaClient {
if (response.status >= 500 && attempt < this.maxRetries) {
const retryDelay = Math.pow(2, attempt) * 100;
utils_1.Logger.warn(`Network error (status ${response.status}), retrying in ${retryDelay}ms (attempt ${attempt + 1}/${this.maxRetries})`, 'ollama-client');
await new Promise((resolve) => setTimeout(resolve, retryDelay));
if (attempt < this.maxRetries - 1) {
const retryTimeout = new Promise((resolve) => setTimeout(resolve, retryDelay));
const abortListener = () => {
utils_1.Logger.info('Retry aborted by user', 'ollama-client');
};
const signal = this.abortController?.signal;
if (signal) {
signal.addEventListener('abort', abortListener);
try {
await Promise.race([
retryTimeout,
new Promise((resolve) => {
signal.addEventListener('abort', () => resolve(), {
once: true,
});
}),
]);
}
finally {
signal.removeEventListener('abort', abortListener);
}
}
else {
await retryTimeout;
}
}
yield* this.streamChatWithRetry(messages, tools, attempt + 1);
return;
}
@@ -125,7 +150,11 @@ class OllamaClient {
}
}
finally {
const controller = this.abortController;
this.abortController = null;
if (controller) {
controller.abort();
}
}
}
async chat(messages, tools = []) {
@@ -151,7 +180,32 @@ class OllamaClient {
if (response.status >= 500 && attempt < this.maxRetries) {
const retryDelay = Math.pow(2, attempt) * 100;
utils_1.Logger.warn(`Network error (status ${response.status}), retrying in ${retryDelay}ms (attempt ${attempt + 1}/${this.maxRetries})`, 'ollama-client');
await new Promise((resolve) => setTimeout(resolve, retryDelay));
if (attempt < this.maxRetries - 1) {
const retryTimeout = new Promise((resolve) => setTimeout(resolve, retryDelay));
const abortListener = () => {
utils_1.Logger.info('Retry aborted by user', 'ollama-client');
};
const signal = this.abortController?.signal;
if (signal) {
signal.addEventListener('abort', abortListener);
try {
await Promise.race([
retryTimeout,
new Promise((resolve) => {
signal.addEventListener('abort', () => resolve(), {
once: true,
});
}),
]);
}
finally {
signal.removeEventListener('abort', abortListener);
}
}
else {
await retryTimeout;
}
}
return this.chatWithRetry(messages, tools, attempt + 1);
}
throw new types_1.ApiError(`Ollama API error: ${response.status}`, response.status);
@@ -160,7 +214,11 @@ class OllamaClient {
return (this.toOllamaMessage(data.message) ?? { role: 'assistant', content: '', tool_calls: [] });
}
finally {
const controller = this.abortController;
this.abortController = null;
if (controller) {
controller.abort();
}
}
}
throwIfOllamaError(parsed) {
+107 -28
View File
@@ -1,12 +1,43 @@
"use strict";
// src/vault-indexer.ts
Object.defineProperty(exports, "__esModule", { value: true });
exports.VaultIndexer = void 0;
exports.CancellationToken = exports.InMemoryCache = exports.VaultIndexer = void 0;
exports.createVaultIndexerWithCache = createVaultIndexerWithCache;
const utils_1 = require("./utils");
class InMemoryCache {
constructor() {
this.store = new Map();
}
get(key) {
return Promise.resolve(this.store.get(key) || null);
}
put(key, value) {
this.store.set(key, value);
return Promise.resolve();
}
clear() {
this.store.clear();
return Promise.resolve();
}
}
exports.InMemoryCache = InMemoryCache;
class CancellationToken {
constructor() {
this.cancelled = false;
}
cancel() {
this.cancelled = true;
}
get isCancelled() {
return this.cancelled;
}
}
exports.CancellationToken = CancellationToken;
class VaultIndexer {
constructor(vault) {
constructor(vault, cache) {
this.vault = null;
this.vault = vault;
this.cache = cache;
}
async searchVault(query, limit = 5) {
if (!query || !query.trim()) {
@@ -15,48 +46,92 @@ class VaultIndexer {
if (!this.vault) {
throw new Error('Vault-like object not provided to VaultIndexer');
}
const cacheKey = `query:${query.trim()}`;
if (this.cache) {
const cachedResults = await this.cache.get(cacheKey);
if (cachedResults) {
try {
const parsedResults = JSON.parse(cachedResults);
return parsedResults.slice(0, limit);
}
catch {
// Ignore cache parse errors and continue with normal processing
}
}
}
const queryTokens = this.tokenize(query.trim());
const vault = this.vault;
const allFiles = vault.getMarkdownFiles();
const results = await this.processFilesInBatches(vault, allFiles, queryTokens);
return results
const filteredResults = results
.filter((result) => result !== null)
.sort((a, b) => b.score - a.score)
.slice(0, limit);
if (this.cache) {
try {
await this.cache.put(cacheKey, JSON.stringify(filteredResults));
}
catch (error) {
utils_1.Logger.warn(`Failed to cache results for query "${query}": ${error instanceof Error ? error.message : String(error)}`, 'vault-indexer');
}
}
return filteredResults;
}
async processFilesInBatches(vault, files, queryTokens) {
const batchSize = 10;
const results = [];
const seenPaths = new Set();
for (let i = 0; i < files.length; i += batchSize) {
const batch = files.slice(i, i + batchSize);
const batchResults = await Promise.all(batch.map(async (file) => {
try {
const content = await vault.read(file);
const tokenized = this.tokenizeContent(content);
const scoreResult = this.calculateWeightedScore(tokenized, queryTokens, file);
if (scoreResult.score > 0) {
const entry = {
path: file.path,
title: file.basename.replace(/\.md$/, ''),
content: content.substring(0, 500),
score: scoreResult.score,
};
if (!seenPaths.has(entry.path)) {
seenPaths.add(entry.path);
return entry;
const cancellationToken = new CancellationToken();
let processedCount = 0;
// Set up a check for cancellation every 100 files
const checkInterval = setInterval(() => {
if (cancellationToken.isCancelled) {
clearInterval(checkInterval);
}
}, 100);
try {
for (let i = 0; i < files.length; i += batchSize) {
if (cancellationToken.isCancelled) {
break;
}
const batch = files.slice(i, i + batchSize);
const batchResults = await Promise.all(batch.map(async (file) => {
try {
const content = await vault.read(file);
const tokenized = this.tokenizeContent(content);
const scoreResult = this.calculateWeightedScore(tokenized, queryTokens, file);
if (scoreResult.score > 0) {
const entry = {
path: file.path,
title: file.basename.replace(/\.md$/, ''),
content: content.substring(0, 500),
score: scoreResult.score,
};
if (!seenPaths.has(entry.path)) {
seenPaths.add(entry.path);
return entry;
}
return null;
}
return null;
}
return null;
catch (error) {
utils_1.Logger.warn(`Failed to process ${file.path}: ${error instanceof Error ? error.message : String(error)}`, 'vault-indexer');
return null;
}
}));
const validResults = batchResults.filter((result) => result !== null);
results.push(...validResults);
// Early exit if we've reached enough results
if (results.length >= 100) {
utils_1.Logger.info(`Early exit after processing ${processedCount + batch.length} files with ${results.length} results`, 'vault-indexer');
break;
}
catch (error) {
utils_1.Logger.warn(`Failed to process ${file.path}: ${error instanceof Error ? error.message : String(error)}`, 'vault-indexer');
return null;
}
}));
const validResults = batchResults.filter((result) => result !== null);
results.push(...validResults);
processedCount += batch.length;
}
}
finally {
clearInterval(checkInterval);
}
return results;
}
@@ -203,3 +278,7 @@ class VaultIndexer {
}
}
exports.VaultIndexer = VaultIndexer;
// Convenience method to create a VaultIndexer with an in-memory cache
function createVaultIndexerWithCache(vault) {
return new VaultIndexer(vault, new InMemoryCache());
}
+4 -10
View File
@@ -134,7 +134,7 @@ class VaultIndexer {
const results: VaultIndexEntry[] = [];
const seenPaths = new Set<string>();
const cancellationToken = new CancellationToken();
let processedCount = 0;
// Removed unused processedCount variable
// Set up a check for cancellation every 100 files
const checkInterval = setInterval(() => {
@@ -185,16 +185,10 @@ class VaultIndexer {
);
results.push(...validResults);
// Early exit if we've reached enough results
if (results.length >= 100) {
Logger.info(
`Early exit after processing ${processedCount + batch.length} files with ${results.length} results`,
'vault-indexer'
);
break;
}
// Continue processing all files to ensure we don't miss higher-scoring results
// even if we've already found some matches
processedCount += batch.length;
// Removed processedCount increment
}
} finally {
clearInterval(checkInterval);