salmon-loop 0.3.1 → 0.3.2

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.
@@ -1,3 +1,4 @@
1
+ import crypto from 'node:crypto';
1
2
  import { normalizePermissionMode } from '../../core/config/index.js';
2
3
  import { buildA2AAgentCard, createAcpFormalAgent, createAgentServerRuntime, createInteractionFacade, createSalmonTaskExecutor, createTaskEventBus, createPluginRegistry, createPromptRegistry, getUserAcpSessionStorePath, GitSnapshotCheckpointService, getLogger, mergeResolvedExtensions, PACKAGE_VERSION, PlainReporter, PluginLoader, resolveExtensions, resolveExecutionProfile, runSalmonLoop, setPluginRegistry, setPromptRegistry, startAcpStdioServer, StderrReporter, } from '../../core/facades/cli-serve.js';
3
4
  import { readPlan } from '../../core/plan/index.js';
@@ -222,7 +223,19 @@ export async function handleServeCommand(_options, command) {
222
223
  return;
223
224
  }
224
225
  const [scheme, token] = authHeader.split(' ');
225
- if (scheme?.toLowerCase() !== 'bearer' || !authTokens.includes(token)) {
226
+ let isAuthenticated = false;
227
+ if (scheme?.toLowerCase() === 'bearer' && token) {
228
+ const tokenBuffer = Buffer.from(token);
229
+ for (const authToken of authTokens) {
230
+ const authTokenBuffer = Buffer.from(authToken);
231
+ if (tokenBuffer.length === authTokenBuffer.length &&
232
+ crypto.timingSafeEqual(tokenBuffer, authTokenBuffer)) {
233
+ isAuthenticated = true;
234
+ break;
235
+ }
236
+ }
237
+ }
238
+ if (!isAuthenticated) {
226
239
  res.status(401).json({ error: 'Unauthorized' });
227
240
  return;
228
241
  }
@@ -14,7 +14,7 @@ export const CommandSuggestionList = ({ suggestions, selectedIndex, parentComman
14
14
  return (_jsxs(Box, { flexDirection: "column", borderStyle: "single", borderColor: COLORS.border.subtle, marginTop: 0, marginBottom: 0, paddingX: 0, width: "100%", children: [_jsx(Box, { flexDirection: "column", paddingY: 0, children: suggestions.map((item, index) => {
15
15
  const isSelected = index === selectedIndex;
16
16
  const hasSubcommands = !!item.command?.subcommands?.length;
17
- return (_jsxs(Box, { flexDirection: "row", paddingX: 1, children: [_jsx(Box, { width: 2, children: _jsx(Text, { color: COLORS.semantic.salmon, children: isSelected ? ' ' : ' ' }) }), _jsx(Box, { width: maxNameLength + 4, children: _jsx(Text, { color: isSelected ? COLORS.semantic.cyan : COLORS.semantic.blue, bold: isSelected, children: item.name }) }), _jsx(Box, { width: 2, marginRight: 1, children: hasSubcommands ? _jsx(Text, { color: COLORS.text.muted, children: "\u203A" }) : _jsx(Text, { children: " " }) }), _jsx(Box, { flexGrow: 1, children: _jsx(Text, { color: isSelected ? COLORS.text.primary : COLORS.text.muted, wrap: "truncate", children: item.description }) })] }, `${item.name}-${index}`));
17
+ return (_jsxs(Box, { flexDirection: "row", paddingX: 1, children: [_jsx(Box, { width: 2, children: _jsx(Text, { color: COLORS.semantic.salmon, children: isSelected ? ' ' : ' ' }) }), _jsx(Box, { width: maxNameLength + 4, children: _jsx(Text, { color: isSelected ? COLORS.semantic.cyan : COLORS.semantic.blue, bold: isSelected, children: item.name }) }), _jsx(Box, { width: 2, marginRight: 1, children: hasSubcommands ? _jsx(Text, { color: COLORS.text.muted, children: "\u203A" }) : _jsx(Text, { children: " " }) }), _jsx(Box, { flexGrow: 1, children: _jsx(Text, { color: isSelected ? COLORS.text.primary : COLORS.text.muted, wrap: "truncate", children: item.description }) })] }, `${item.name}-${index}`));
18
18
  }) }), suggestions[selectedIndex]?.command?.usage && (_jsxs(Box, { flexDirection: "row", borderStyle: "single", borderTop: true, borderLeft: false, borderRight: false, borderBottom: false, borderColor: COLORS.border.subtle, paddingX: 1, paddingY: 0, children: [_jsx(Text, { color: COLORS.semantic.blue, children: "TIP: " }), _jsx(Text, { color: COLORS.text.muted, children: "Usage: " }), _jsx(Text, { color: COLORS.text.primary, children: suggestions[selectedIndex].command?.usage })] })), _jsxs(Box, { flexDirection: "row", borderStyle: "single", borderTop: true, borderLeft: false, borderRight: false, borderBottom: false, borderColor: COLORS.border.subtle, paddingX: 1, paddingY: 0, justifyContent: "space-between", children: [_jsxs(Box, { children: [_jsx(Text, { color: COLORS.semantic.salmon, children: "\u2502 " }), _jsx(Text, { color: COLORS.semantic.blue, bold: true, children: title })] }), _jsx(Box, { children: _jsx(Text, { color: COLORS.text.muted, dimColor: true, children: "\u2191\u2193 nav \u00B7 \u23CE select \u00B7 esc close" }) })] })] }));
19
19
  };
20
20
  //# sourceMappingURL=CommandSuggestionList.js.map
@@ -214,25 +214,48 @@ export class ContextService {
214
214
  return true;
215
215
  }
216
216
  async evictExpiredEntries() {
217
- for (const [key, entry] of await this.cacheStore.entries()) {
218
- await this.isExpired(key, entry);
217
+ const entries = Array.from(await this.cacheStore.entries());
218
+ const now = Date.now();
219
+ const expiredEntries = entries.filter(([, entry]) => {
220
+ const last = this.getEntryTimestamp(entry);
221
+ return last && now - last > this.cacheTtlMs;
222
+ });
223
+ for (let i = 0; i < expiredEntries.length; i += 10) {
224
+ const chunk = expiredEntries.slice(i, i + 10);
225
+ await Promise.all(chunk.map(([key, entry]) => this.isExpired(key, entry)));
219
226
  }
220
227
  }
221
228
  async evictLruIfNeeded() {
222
- while ((await this.cacheStore.size()) > this.cacheMaxEntries) {
229
+ const size = await this.cacheStore.size();
230
+ if (size <= this.cacheMaxEntries)
231
+ return;
232
+ const excess = size - this.cacheMaxEntries;
233
+ const entries = Array.from(await this.cacheStore.entries());
234
+ if (excess === 1) {
223
235
  let victimKey;
224
236
  let victimTs = Number.POSITIVE_INFINITY;
225
- for (const [key, entry] of await this.cacheStore.entries()) {
237
+ for (const [key, entry] of entries) {
226
238
  const ts = this.getEntryTimestamp(entry);
227
239
  if (ts < victimTs) {
228
240
  victimTs = ts;
229
241
  victimKey = key;
230
242
  }
231
243
  }
232
- if (!victimKey)
233
- break;
234
- await this.cacheStore.delete(victimKey);
235
- this.cacheMetrics.evictions += 1;
244
+ if (victimKey) {
245
+ await this.cacheStore.delete(victimKey);
246
+ this.cacheMetrics.evictions += 1;
247
+ }
248
+ }
249
+ else {
250
+ entries.sort((a, b) => (this.getEntryTimestamp(a[1]) || 0) - (this.getEntryTimestamp(b[1]) || 0));
251
+ const victims = entries.slice(0, excess);
252
+ for (let i = 0; i < victims.length; i += 10) {
253
+ const chunk = victims.slice(i, i + 10);
254
+ await Promise.all(chunk.map(async ([key]) => {
255
+ await this.cacheStore.delete(key);
256
+ this.cacheMetrics.evictions += 1;
257
+ }));
258
+ }
236
259
  }
237
260
  }
238
261
  async getCacheStats() {
@@ -306,19 +306,26 @@ export class ChatSessionManager {
306
306
  const analysis = this.pruningEngine.analyzeSessions(sessions);
307
307
  let deleted = 0;
308
308
  let archived = 0;
309
+ const CHUNK_SIZE = 10;
309
310
  // Delete low-priority sessions
310
- for (const sessionId of analysis.sessionsToDelete) {
311
- await this.deleteSession(sessionId);
312
- deleted++;
311
+ for (let i = 0; i < analysis.sessionsToDelete.length; i += CHUNK_SIZE) {
312
+ const chunk = analysis.sessionsToDelete.slice(i, i + CHUNK_SIZE);
313
+ await Promise.all(chunk.map(async (sessionId) => {
314
+ await this.deleteSession(sessionId);
315
+ deleted++;
316
+ }));
313
317
  }
314
318
  // Archive medium-priority sessions
315
- for (const sessionId of analysis.sessionsToArchive) {
316
- const session = sessions.find((s) => s.meta.id === sessionId);
317
- if (session) {
318
- await this.archiveSession(session);
319
- await this.deleteSession(sessionId);
320
- archived++;
321
- }
319
+ for (let i = 0; i < analysis.sessionsToArchive.length; i += CHUNK_SIZE) {
320
+ const chunk = analysis.sessionsToArchive.slice(i, i + CHUNK_SIZE);
321
+ await Promise.all(chunk.map(async (sessionId) => {
322
+ const session = sessions.find((s) => s.meta.id === sessionId);
323
+ if (session) {
324
+ await this.archiveSession(session);
325
+ await this.deleteSession(sessionId);
326
+ archived++;
327
+ }
328
+ }));
322
329
  }
323
330
  return {
324
331
  deleted,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "salmon-loop",
3
- "version": "0.3.1",
3
+ "version": "0.3.2",
4
4
  "description": "A chat-first coding agent CLI for safe, reviewable repository changes",
5
5
  "type": "module",
6
6
  "bin": {