osborn 0.9.235 → 0.9.237

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.
@@ -47,7 +47,11 @@ async function buildRecallInjection(sessionId, workingDir, prompt) {
47
47
  try {
48
48
  if (!RECALL_ENABLED() || !sessionId || !workingDir)
49
49
  return '';
50
- const q = String(prompt || '').trim();
50
+ // CAP the recall query length (defense-in-depth). A pasted or echoed giant prompt — e.g. a
51
+ // "liked" log step that serialized a 24KB command+output — would otherwise build a massive
52
+ // FTS MATCH and an oversized embed request on the hot UserPromptSubmit path and stall the
53
+ // turn. A real user utterance is short; 512 chars is ample to retrieve on.
54
+ const q = String(prompt || '').trim().slice(0, 512);
51
55
  if (q.length < 3)
52
56
  return '';
53
57
  if (!storeExists(sessionId, workingDir))
@@ -0,0 +1,16 @@
1
+ export interface HousekeepingResult {
2
+ subagentsRemoved: string[];
3
+ corruptSlugsRemoved: string[];
4
+ scannedSlugs: number;
5
+ }
6
+ /**
7
+ * Prune stale sub-agent transcripts (>ttl) and empty/corrupt slug dirs.
8
+ * SAFE BY DESIGN: never deletes a {sessionId}.jsonl, never touches the active session's
9
+ * subtree, and (unless dryRun=false) deletes nothing at all. Conservative — only removes
10
+ * sub-agent artifacts older than the TTL and slug dirs that contain NO conversation jsonl.
11
+ */
12
+ export declare function cleanupHousekeeping(opts: {
13
+ dryRun: boolean;
14
+ activeSessionId?: string | null;
15
+ subagentTtlMs?: number;
16
+ }): HousekeepingResult;
@@ -0,0 +1,151 @@
1
+ // Housekeeping: prune stale sub-agent transcripts and empty/corrupt slug dirs from the Claude
2
+ // projects tree. Runs on the agent (boot + periodic). Ships DRY-RUN by default so we can eyeball
3
+ // what it WOULD delete in fly logs before enabling real deletion (OSBORN_HOUSEKEEPING_DRYRUN=0).
4
+ //
5
+ // Layout (see session-access.ts for the canonical map):
6
+ // ~/.claude/projects/{slug}/
7
+ // ├── {sessionId}.jsonl ← main conversation (the source of truth — NEVER deleted here)
8
+ // ├── {sessionId}/subagents/agent-*.jsonl ← SDK sub-agent transcripts (TTL target)
9
+ // ├── agent-*.jsonl ← project-level native Task transcripts (TTL target)
10
+ // └── osb/{sessionId}/session.db ← recall store, SLUG-LEVEL sibling of the jsonl (NEVER deleted here)
11
+ import { existsSync, readdirSync, statSync, lstatSync, rmSync } from 'node:fs';
12
+ import { join } from 'node:path';
13
+ import { getClaudeProjectsDir } from './config.js';
14
+ const DAY_MS = 24 * 60 * 60 * 1000;
15
+ const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
16
+ /**
17
+ * Prune stale sub-agent transcripts (>ttl) and empty/corrupt slug dirs.
18
+ * SAFE BY DESIGN: never deletes a {sessionId}.jsonl, never touches the active session's
19
+ * subtree, and (unless dryRun=false) deletes nothing at all. Conservative — only removes
20
+ * sub-agent artifacts older than the TTL and slug dirs that contain NO conversation jsonl.
21
+ */
22
+ export function cleanupHousekeeping(opts) {
23
+ const { dryRun, activeSessionId } = opts;
24
+ const ttl = opts.subagentTtlMs ?? DAY_MS;
25
+ const now = Date.now();
26
+ const tag = dryRun ? '🧹[dry-run] WOULD remove' : '🧹 removed';
27
+ const result = { subagentsRemoved: [], corruptSlugsRemoved: [], scannedSlugs: 0 };
28
+ let projectsDir;
29
+ try {
30
+ projectsDir = getClaudeProjectsDir();
31
+ }
32
+ catch {
33
+ return result;
34
+ }
35
+ let slugs;
36
+ try {
37
+ slugs = readdirSync(projectsDir);
38
+ }
39
+ catch {
40
+ return result;
41
+ }
42
+ const rm = (p) => { if (!dryRun) {
43
+ try {
44
+ rmSync(p, { recursive: true, force: true });
45
+ }
46
+ catch { }
47
+ } };
48
+ for (const slug of slugs) {
49
+ if (slug.startsWith('.'))
50
+ continue;
51
+ const slugPath = join(projectsDir, slug);
52
+ let slugStat;
53
+ try {
54
+ slugStat = lstatSync(slugPath);
55
+ }
56
+ catch {
57
+ continue;
58
+ }
59
+ if (slugStat.isSymbolicLink())
60
+ continue; // never follow or delete symlinked entries
61
+ // Stray non-directory entry directly under projects/ — garbage. Only if old enough,
62
+ // to avoid racing a write-in-progress.
63
+ if (!slugStat.isDirectory()) {
64
+ if (now - slugStat.mtimeMs > ttl) {
65
+ console.log(`${tag} stray projects entry: ${slugPath}`);
66
+ result.corruptSlugsRemoved.push(slugPath);
67
+ rm(slugPath);
68
+ }
69
+ continue;
70
+ }
71
+ result.scannedSlugs++;
72
+ let entries;
73
+ try {
74
+ entries = readdirSync(slugPath);
75
+ }
76
+ catch {
77
+ continue;
78
+ }
79
+ // Which session ids have a live top-level {sessionId}.jsonl in this slug?
80
+ const liveSessionIds = new Set();
81
+ for (const e of entries) {
82
+ if (e.endsWith('.jsonl')) {
83
+ const base = e.slice(0, -6);
84
+ if (UUID_RE.test(base))
85
+ liveSessionIds.add(base);
86
+ }
87
+ }
88
+ const slugContainsActive = !!activeSessionId && liveSessionIds.has(activeSessionId);
89
+ // (a) Sub-agent TTL sweep.
90
+ for (const e of entries) {
91
+ const full = join(slugPath, e);
92
+ // Project-level native SDK Task transcripts. Skip the active project's — a long-running
93
+ // active session may have legitimately old task transcripts still relevant to it.
94
+ if (e.startsWith('agent-') && e.endsWith('.jsonl')) {
95
+ if (slugContainsActive)
96
+ continue;
97
+ try {
98
+ if (now - statSync(full).mtimeMs > ttl) {
99
+ console.log(`${tag} subagent transcript: ${full}`);
100
+ result.subagentsRemoved.push(full);
101
+ rm(full);
102
+ }
103
+ }
104
+ catch { /* ignore per-entry */ }
105
+ continue;
106
+ }
107
+ // Per-session subdir → its subagents/ transcripts. Never the active session's subtree.
108
+ if (UUID_RE.test(e)) {
109
+ if (activeSessionId && e === activeSessionId)
110
+ continue;
111
+ const subagentsDir = join(full, 'subagents');
112
+ if (!existsSync(subagentsDir))
113
+ continue;
114
+ let subFiles = [];
115
+ try {
116
+ subFiles = readdirSync(subagentsDir);
117
+ }
118
+ catch {
119
+ continue;
120
+ }
121
+ for (const sf of subFiles) {
122
+ const sfull = join(subagentsDir, sf);
123
+ try {
124
+ if (now - statSync(sfull).mtimeMs > ttl) {
125
+ console.log(`${tag} subagent transcript: ${sfull}`);
126
+ result.subagentsRemoved.push(sfull);
127
+ rm(sfull);
128
+ }
129
+ }
130
+ catch { /* ignore per-entry */ }
131
+ }
132
+ }
133
+ }
134
+ // (b) Corrupt/empty slug: ONLY a TRULY EMPTY directory (zero entries) that is stale.
135
+ // We deliberately do NOT delete a slug that merely lacks a top-level *.jsonl — such a dir
136
+ // can still hold a slug-level `osb/{sessionId}/session.db` recall store or orphan subdirs,
137
+ // and a directory's mtime does NOT reflect nested writes, so "no jsonl + stale dir-mtime"
138
+ // is not a safe delete signal. An empty dir has nothing nested to lose.
139
+ if (entries.length === 0 && now - slugStat.mtimeMs > ttl) {
140
+ console.log(`${tag} empty slug dir: ${slugPath}`);
141
+ result.corruptSlugsRemoved.push(slugPath);
142
+ rm(slugPath);
143
+ }
144
+ }
145
+ const verb = dryRun ? 'would remove' : 'removed';
146
+ console.log(`🧹 housekeeping: scanned ${result.scannedSlugs} slug(s); ${verb} ` +
147
+ `${result.subagentsRemoved.length} stale sub-agent artifact(s), ` +
148
+ `${result.corruptSlugsRemoved.length} empty/corrupt slug(s)` +
149
+ (dryRun ? ' (dry-run — set OSBORN_HOUSEKEEPING_DRYRUN=0 to enable deletion)' : ''));
150
+ return result;
151
+ }
package/dist/index.js CHANGED
@@ -34,6 +34,7 @@ const __filename = fileURLToPath(import.meta.url);
34
34
  const __dirname = dirname(__filename);
35
35
  import { createPatch } from 'diff';
36
36
  import { loadConfig, getMcpServers, getEnabledMcpServerNames, getVoiceMode, getDirectConfig, listSessions, listAllClaudeSessions, invalidateSessionListCache, getMostRecentSessionId, sessionExists, getSessionSummary, getConversationHistory, ensureSessionWorkspace, getMcpServerStatusList, buildMcpServersForKeys, listWorkspaceArtifacts } from './config.js';
37
+ import { cleanupHousekeeping } from './housekeeping.js';
37
38
  import { createSTT, createTTS } from './voice-io.js';
38
39
  import { createClaudeLLM, NAMED_AGENTS, applyTurbo } from './claude-llm.js';
39
40
  import { clearPipelineFastBrainSession, prewarmBM25Index } from './pipeline-fastbrain.js';
@@ -2785,6 +2786,28 @@ async function main() {
2785
2786
  // Track the active resume session ID across scopes (ParticipantConnected + DataReceived)
2786
2787
  // Updated by resume_session, session_selected, continue_session, switch_session handlers
2787
2788
  let currentResumeSessionId;
2789
+ // Housekeeping: prune stale sub-agent transcripts (>24h) + empty/corrupt slug dirs. Ships
2790
+ // DRY-RUN by default (OSBORN_HOUSEKEEPING_DRYRUN != '0') so it only LOGS what it would remove
2791
+ // until we flip the env. Reads the active session live so it never touches the in-use subtree.
2792
+ const runHousekeeping = () => {
2793
+ try {
2794
+ const envDryRun = process.env.OSBORN_HOUSEKEEPING_DRYRUN !== '0';
2795
+ const activeSessionId = currentLLM?.sessionId ?? currentResumeSessionId ?? null;
2796
+ // Force dry-run whenever NO session is active (e.g. the boot run): with activeSessionId
2797
+ // null the in-use-session guards are inert, so only LOG then — real deletion waits until
2798
+ // a session is established (the periodic 6h runs), when the guards are meaningful.
2799
+ const dryRun = envDryRun || !activeSessionId;
2800
+ const res = cleanupHousekeeping({ dryRun, activeSessionId });
2801
+ if (!dryRun && (res.subagentsRemoved.length > 0 || res.corruptSlugsRemoved.length > 0)) {
2802
+ invalidateSessionListCache();
2803
+ }
2804
+ }
2805
+ catch (e) {
2806
+ console.warn('🧹 housekeeping failed:', e);
2807
+ }
2808
+ };
2809
+ runHousekeeping();
2810
+ setInterval(runHousekeeping, 6 * 60 * 60 * 1000);
2788
2811
  // Claude auth code submission handler (set during OAuth flow, cleared after)
2789
2812
  let pendingAuthSubmitCode = null;
2790
2813
  // Task deduplication guard - prevents Gemini re-execution loops
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "osborn",
3
- "version": "0.9.235",
3
+ "version": "0.9.237",
4
4
  "description": "Voice AI coding assistant - local agent that connects to Osborn frontend",
5
5
  "type": "module",
6
6
  "bin": {