@kb-labs/agent-history 0.2.0
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/README.md +77 -0
- package/dist/index.d.ts +216 -0
- package/dist/index.js +593 -0
- package/dist/index.js.map +1 -0
- package/package.json +51 -0
package/README.md
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# @kb-labs/agent-history
|
|
2
|
+
|
|
3
|
+
File change tracking, snapshots, and conflict resolution for KB Labs agents. Enables rollback of agent modifications and safe retry of failed tasks.
|
|
4
|
+
|
|
5
|
+
## Components
|
|
6
|
+
|
|
7
|
+
### FileChangeTracker
|
|
8
|
+
|
|
9
|
+
Tracks all file operations (create, modify, delete) performed by an agent.
|
|
10
|
+
|
|
11
|
+
```typescript
|
|
12
|
+
import { FileChangeTracker } from '@kb-labs/agent-history';
|
|
13
|
+
|
|
14
|
+
const tracker = new FileChangeTracker();
|
|
15
|
+
|
|
16
|
+
tracker.recordChange({
|
|
17
|
+
path: 'src/auth.ts',
|
|
18
|
+
type: 'modify',
|
|
19
|
+
before: originalContent,
|
|
20
|
+
after: newContent,
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
const changes = tracker.getChanges();
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
### SnapshotStorage
|
|
27
|
+
|
|
28
|
+
Persists file snapshots for recovery. Stores original content before modifications.
|
|
29
|
+
|
|
30
|
+
```typescript
|
|
31
|
+
import { SnapshotStorage } from '@kb-labs/agent-history';
|
|
32
|
+
|
|
33
|
+
const storage = new SnapshotStorage({ baseDir: '.kb/snapshots' });
|
|
34
|
+
|
|
35
|
+
await storage.save(sessionId, changes);
|
|
36
|
+
const snapshot = await storage.load(sessionId);
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
### ConflictDetector
|
|
40
|
+
|
|
41
|
+
Detects conflicts when agent modifications overlap with external changes (concurrent edits by user or another agent).
|
|
42
|
+
|
|
43
|
+
```typescript
|
|
44
|
+
import { ConflictDetector } from '@kb-labs/agent-history';
|
|
45
|
+
|
|
46
|
+
const detector = new ConflictDetector();
|
|
47
|
+
const conflicts = detector.detect(agentChanges, currentFileState);
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
### ConflictResolver
|
|
51
|
+
|
|
52
|
+
Resolves detected conflicts with configurable strategies (keep-agent, keep-external, merge).
|
|
53
|
+
|
|
54
|
+
```typescript
|
|
55
|
+
import { ConflictResolver } from '@kb-labs/agent-history';
|
|
56
|
+
|
|
57
|
+
const resolver = new ConflictResolver();
|
|
58
|
+
const result = resolver.resolve(conflict, strategy);
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## Types
|
|
62
|
+
|
|
63
|
+
```typescript
|
|
64
|
+
import type {
|
|
65
|
+
FileChange,
|
|
66
|
+
RollbackResult,
|
|
67
|
+
ConflictInfo,
|
|
68
|
+
StorageConfig,
|
|
69
|
+
DetectedConflict,
|
|
70
|
+
ConflictType, // 'modified' | 'deleted' | 'created'
|
|
71
|
+
ResolutionResult,
|
|
72
|
+
} from '@kb-labs/agent-history';
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## Dependencies
|
|
76
|
+
|
|
77
|
+
- `@kb-labs/agent-contracts` — shared types
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
import { FileChangeSummary } from '@kb-labs/agent-contracts';
|
|
2
|
+
export { ConflictInfo, FileChangeSummary, RollbackResult } from '@kb-labs/agent-contracts';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* File change history types
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Single file change snapshot.
|
|
10
|
+
* Captured by ChangeTrackingMiddleware via afterToolExec.
|
|
11
|
+
* agentId / runId are middleware context — not tool concerns.
|
|
12
|
+
*/
|
|
13
|
+
interface FileChange {
|
|
14
|
+
/** Unique change ID */
|
|
15
|
+
id: string;
|
|
16
|
+
/** Session ID for grouping changes */
|
|
17
|
+
sessionId: string;
|
|
18
|
+
/** Agent ID that made the change (injected by middleware) */
|
|
19
|
+
agentId: string;
|
|
20
|
+
/** Run ID — enables per-run/per-turn filtering and rollback */
|
|
21
|
+
runId: string;
|
|
22
|
+
/** File path relative to working directory */
|
|
23
|
+
filePath: string;
|
|
24
|
+
/** Operation type */
|
|
25
|
+
operation: 'write' | 'patch' | 'delete';
|
|
26
|
+
/** ISO timestamp when change was captured */
|
|
27
|
+
timestamp: string;
|
|
28
|
+
/** Snapshot before the change (undefined = new file) */
|
|
29
|
+
before?: {
|
|
30
|
+
content: string;
|
|
31
|
+
hash: string;
|
|
32
|
+
size: number;
|
|
33
|
+
};
|
|
34
|
+
/** Snapshot after the change */
|
|
35
|
+
after: {
|
|
36
|
+
content: string;
|
|
37
|
+
hash: string;
|
|
38
|
+
size: number;
|
|
39
|
+
};
|
|
40
|
+
/** Whether this change has been approved by the user */
|
|
41
|
+
approved?: boolean;
|
|
42
|
+
/** ISO timestamp when change was approved */
|
|
43
|
+
approvedAt?: string;
|
|
44
|
+
/** Operation-specific metadata from tool output */
|
|
45
|
+
metadata?: {
|
|
46
|
+
startLine?: number;
|
|
47
|
+
endLine?: number;
|
|
48
|
+
linesAdded?: number;
|
|
49
|
+
linesRemoved?: number;
|
|
50
|
+
isOverwrite?: boolean;
|
|
51
|
+
wasDeleted?: boolean;
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
interface StorageConfig {
|
|
55
|
+
/** Base path relative to workingDir. Default: '.kb/agents/sessions' */
|
|
56
|
+
basePath: string;
|
|
57
|
+
/** Max sessions to keep. Default: 30 */
|
|
58
|
+
maxSessions?: number;
|
|
59
|
+
/** Max age in days. Default: 30 */
|
|
60
|
+
maxAgeDays?: number;
|
|
61
|
+
/** Max total storage in MB. Default: 500 */
|
|
62
|
+
maxTotalSizeMb?: number;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* ChangeStore — persists and queries file change snapshots.
|
|
67
|
+
*
|
|
68
|
+
* Design principles (vs old FileChangeTracker):
|
|
69
|
+
* - No EventEmitter: events belong in middleware (AgentEventBus)
|
|
70
|
+
* - No agentId/runId state: injected per-call by ChangeTrackingMiddleware
|
|
71
|
+
* - No rollback I/O: rollback logic lives in middleware (uses workingDir from RunContext)
|
|
72
|
+
* - Pure storage + query: save, load, list, cleanup
|
|
73
|
+
*/
|
|
74
|
+
|
|
75
|
+
declare function computeHash(content: string): string;
|
|
76
|
+
declare function toSummary(change: FileChange): FileChangeSummary;
|
|
77
|
+
interface SaveChangeInput {
|
|
78
|
+
sessionId: string;
|
|
79
|
+
agentId: string;
|
|
80
|
+
runId: string;
|
|
81
|
+
filePath: string;
|
|
82
|
+
operation: 'write' | 'patch' | 'delete';
|
|
83
|
+
beforeContent: string | undefined;
|
|
84
|
+
afterContent: string;
|
|
85
|
+
metadata?: FileChange['metadata'];
|
|
86
|
+
}
|
|
87
|
+
declare class ChangeStore {
|
|
88
|
+
private readonly storage;
|
|
89
|
+
/** In-memory cache keyed by sessionId → changeId → FileChange */
|
|
90
|
+
private readonly cache;
|
|
91
|
+
constructor(workingDir: string, config?: Partial<StorageConfig>);
|
|
92
|
+
save(input: SaveChangeInput): Promise<FileChange>;
|
|
93
|
+
get(sessionId: string, changeId: string): Promise<FileChange | null>;
|
|
94
|
+
listSession(sessionId: string): Promise<FileChange[]>;
|
|
95
|
+
listRun(sessionId: string, runId: string): Promise<FileChange[]>;
|
|
96
|
+
listFile(sessionId: string, filePath: string): Promise<FileChange[]>;
|
|
97
|
+
/** Returns unique file paths touched in a session */
|
|
98
|
+
changedFiles(sessionId: string): Promise<string[]>;
|
|
99
|
+
summariesForRun(sessionId: string, runId: string): Promise<FileChangeSummary[]>;
|
|
100
|
+
cleanup(): Promise<{
|
|
101
|
+
deleted: number;
|
|
102
|
+
keptLast: number;
|
|
103
|
+
}>;
|
|
104
|
+
deleteSession(sessionId: string): Promise<void>;
|
|
105
|
+
private _cache;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Snapshot storage - persist file change snapshots to disk
|
|
110
|
+
*/
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Storage for file change snapshots
|
|
114
|
+
*/
|
|
115
|
+
declare class SnapshotStorage {
|
|
116
|
+
private workingDir;
|
|
117
|
+
private config;
|
|
118
|
+
constructor(workingDir: string, config?: Partial<StorageConfig>);
|
|
119
|
+
/**
|
|
120
|
+
* Save snapshot to disk
|
|
121
|
+
*/
|
|
122
|
+
saveSnapshot(sessionId: string, change: FileChange): Promise<void>;
|
|
123
|
+
/**
|
|
124
|
+
* Load snapshot by ID
|
|
125
|
+
*/
|
|
126
|
+
loadSnapshot(sessionId: string, changeId: string): Promise<FileChange | null>;
|
|
127
|
+
/**
|
|
128
|
+
* List all snapshots for session
|
|
129
|
+
*/
|
|
130
|
+
listSnapshots(sessionId: string): Promise<FileChange[]>;
|
|
131
|
+
/**
|
|
132
|
+
* Delete all snapshots for session
|
|
133
|
+
*/
|
|
134
|
+
deleteSession(sessionId: string): Promise<void>;
|
|
135
|
+
/**
|
|
136
|
+
* Cleanup old sessions based on retention policy
|
|
137
|
+
*/
|
|
138
|
+
cleanupOldSessions(): Promise<{
|
|
139
|
+
deleted: number;
|
|
140
|
+
keptLast: number;
|
|
141
|
+
}>;
|
|
142
|
+
/**
|
|
143
|
+
* Get session directory path
|
|
144
|
+
*/
|
|
145
|
+
private getSessionDir;
|
|
146
|
+
/**
|
|
147
|
+
* Update session index
|
|
148
|
+
*/
|
|
149
|
+
private updateIndex;
|
|
150
|
+
/**
|
|
151
|
+
* List all sessions with metadata
|
|
152
|
+
*/
|
|
153
|
+
private listAllSessions;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* ConflictDetector — detects file conflicts before write operations.
|
|
158
|
+
* Works with ChangeStore instead of the old FileChangeTracker.
|
|
159
|
+
*/
|
|
160
|
+
|
|
161
|
+
type ConflictType = 'none' | 'disjoint' | 'overlapping' | 'conflicting-intent' | 'semantic';
|
|
162
|
+
interface DetectedConflict {
|
|
163
|
+
type: ConflictType;
|
|
164
|
+
filePath: string;
|
|
165
|
+
/** Agent attempting the write */
|
|
166
|
+
currentAgentId: string;
|
|
167
|
+
/** Other agents who modified this file */
|
|
168
|
+
conflictingAgents: string[];
|
|
169
|
+
description: string;
|
|
170
|
+
/** 0–1: higher = easier to auto-resolve */
|
|
171
|
+
resolutionConfidence: number;
|
|
172
|
+
metadata?: {
|
|
173
|
+
overlapLines?: number[];
|
|
174
|
+
intentA?: string;
|
|
175
|
+
intentB?: string;
|
|
176
|
+
semanticIssue?: string;
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
interface ConflictCheckInput {
|
|
180
|
+
sessionId: string;
|
|
181
|
+
filePath: string;
|
|
182
|
+
agentId: string;
|
|
183
|
+
operation: 'write' | 'patch' | 'delete';
|
|
184
|
+
metadata?: {
|
|
185
|
+
startLine?: number;
|
|
186
|
+
endLine?: number;
|
|
187
|
+
content?: string;
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
declare class ConflictDetector {
|
|
191
|
+
private readonly store;
|
|
192
|
+
constructor(store: ChangeStore);
|
|
193
|
+
detectConflict(input: ConflictCheckInput): Promise<DetectedConflict | null>;
|
|
194
|
+
private _checkLineOverlap;
|
|
195
|
+
private _overlapLines;
|
|
196
|
+
private _detectSemanticConflict;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Unified diff generator for file change snapshots.
|
|
201
|
+
* Pure implementation — no external dependencies.
|
|
202
|
+
*/
|
|
203
|
+
/**
|
|
204
|
+
* Generate a unified diff string from before/after content.
|
|
205
|
+
* Context lines: 3 lines before and after each change hunk.
|
|
206
|
+
*/
|
|
207
|
+
declare function generateUnifiedDiff(filePath: string, beforeContent: string | undefined, afterContent: string, operation: 'write' | 'patch' | 'delete'): string;
|
|
208
|
+
/**
|
|
209
|
+
* Count added and removed lines from a unified diff string.
|
|
210
|
+
*/
|
|
211
|
+
declare function countDiffLines(diff: string): {
|
|
212
|
+
added: number;
|
|
213
|
+
removed: number;
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
export { ChangeStore, type ConflictCheckInput, ConflictDetector, type ConflictType, type DetectedConflict, type FileChange, type SaveChangeInput, SnapshotStorage, type StorageConfig, computeHash, countDiffLines, generateUnifiedDiff, toSummary };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,593 @@
|
|
|
1
|
+
import * as crypto from 'crypto';
|
|
2
|
+
import * as fs from 'fs';
|
|
3
|
+
import * as path from 'path';
|
|
4
|
+
|
|
5
|
+
// src/change-store.ts
|
|
6
|
+
var DEFAULT_CONFIG = {
|
|
7
|
+
basePath: ".kb/agents/sessions",
|
|
8
|
+
maxSessions: 30,
|
|
9
|
+
maxAgeDays: 30,
|
|
10
|
+
maxTotalSizeMb: 500
|
|
11
|
+
};
|
|
12
|
+
var SnapshotStorage = class {
|
|
13
|
+
workingDir;
|
|
14
|
+
config;
|
|
15
|
+
constructor(workingDir, config) {
|
|
16
|
+
this.workingDir = workingDir;
|
|
17
|
+
this.config = { ...DEFAULT_CONFIG, ...config };
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Save snapshot to disk
|
|
21
|
+
*/
|
|
22
|
+
async saveSnapshot(sessionId, change) {
|
|
23
|
+
const sessionDir = this.getSessionDir(sessionId);
|
|
24
|
+
const snapshotsDir = path.join(sessionDir, "snapshots");
|
|
25
|
+
await fs.promises.mkdir(snapshotsDir, { recursive: true });
|
|
26
|
+
const snapshotPath = path.join(snapshotsDir, `${change.id}.json`);
|
|
27
|
+
await fs.promises.writeFile(
|
|
28
|
+
snapshotPath,
|
|
29
|
+
JSON.stringify(change, null, 2),
|
|
30
|
+
"utf-8"
|
|
31
|
+
);
|
|
32
|
+
await this.updateIndex(sessionId, change);
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Load snapshot by ID
|
|
36
|
+
*/
|
|
37
|
+
async loadSnapshot(sessionId, changeId) {
|
|
38
|
+
try {
|
|
39
|
+
const snapshotPath = path.join(
|
|
40
|
+
this.getSessionDir(sessionId),
|
|
41
|
+
"snapshots",
|
|
42
|
+
`${changeId}.json`
|
|
43
|
+
);
|
|
44
|
+
const content = await fs.promises.readFile(snapshotPath, "utf-8");
|
|
45
|
+
return JSON.parse(content);
|
|
46
|
+
} catch (error) {
|
|
47
|
+
if (error.code === "ENOENT") {
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
if (error instanceof SyntaxError) {
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
throw error;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* List all snapshots for session
|
|
58
|
+
*/
|
|
59
|
+
async listSnapshots(sessionId) {
|
|
60
|
+
try {
|
|
61
|
+
const indexPath = path.join(this.getSessionDir(sessionId), "index.json");
|
|
62
|
+
const content = await fs.promises.readFile(indexPath, "utf-8");
|
|
63
|
+
const index = JSON.parse(content);
|
|
64
|
+
const changes = [];
|
|
65
|
+
for (const changeId of index.changes) {
|
|
66
|
+
const change = await this.loadSnapshot(sessionId, changeId);
|
|
67
|
+
if (change) {
|
|
68
|
+
changes.push(change);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
changes.sort((a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime());
|
|
72
|
+
return changes;
|
|
73
|
+
} catch (error) {
|
|
74
|
+
if (error.code === "ENOENT") {
|
|
75
|
+
const snapshotsDir = path.join(this.getSessionDir(sessionId), "snapshots");
|
|
76
|
+
try {
|
|
77
|
+
const files = await fs.promises.readdir(snapshotsDir);
|
|
78
|
+
const changes = [];
|
|
79
|
+
for (const file of files) {
|
|
80
|
+
if (file.endsWith(".json")) {
|
|
81
|
+
const changeId = file.replace(".json", "");
|
|
82
|
+
const change = await this.loadSnapshot(sessionId, changeId);
|
|
83
|
+
if (change) {
|
|
84
|
+
changes.push(change);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
changes.sort((a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime());
|
|
89
|
+
return changes;
|
|
90
|
+
} catch {
|
|
91
|
+
return [];
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
throw error;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Delete all snapshots for session
|
|
99
|
+
*/
|
|
100
|
+
async deleteSession(sessionId) {
|
|
101
|
+
const sessionDir = this.getSessionDir(sessionId);
|
|
102
|
+
try {
|
|
103
|
+
await fs.promises.rm(sessionDir, { recursive: true, force: true });
|
|
104
|
+
} catch (error) {
|
|
105
|
+
if (error.code !== "ENOENT") {
|
|
106
|
+
throw error;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Cleanup old sessions based on retention policy
|
|
112
|
+
*/
|
|
113
|
+
async cleanupOldSessions() {
|
|
114
|
+
path.join(this.workingDir, this.config.basePath);
|
|
115
|
+
try {
|
|
116
|
+
const sessions = await this.listAllSessions();
|
|
117
|
+
sessions.sort((a, b) => b.createdAt - a.createdAt);
|
|
118
|
+
let deleted = 0;
|
|
119
|
+
if (sessions.length > this.config.maxSessions) {
|
|
120
|
+
const toDelete = sessions.slice(this.config.maxSessions);
|
|
121
|
+
for (const session of toDelete) {
|
|
122
|
+
await this.deleteSession(session.id);
|
|
123
|
+
deleted++;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
const now = Date.now();
|
|
127
|
+
const maxAge = this.config.maxAgeDays * 24 * 60 * 60 * 1e3;
|
|
128
|
+
for (const session of sessions) {
|
|
129
|
+
if (now - session.createdAt > maxAge) {
|
|
130
|
+
await this.deleteSession(session.id);
|
|
131
|
+
deleted++;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
return {
|
|
135
|
+
deleted,
|
|
136
|
+
keptLast: Math.min(sessions.length, this.config.maxSessions)
|
|
137
|
+
};
|
|
138
|
+
} catch (error) {
|
|
139
|
+
if (error.code === "ENOENT") {
|
|
140
|
+
return { deleted: 0, keptLast: 0 };
|
|
141
|
+
}
|
|
142
|
+
throw error;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
146
|
+
// Private Methods
|
|
147
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
148
|
+
/**
|
|
149
|
+
* Get session directory path
|
|
150
|
+
*/
|
|
151
|
+
getSessionDir(sessionId) {
|
|
152
|
+
return path.join(this.workingDir, this.config.basePath, sessionId);
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Update session index
|
|
156
|
+
*/
|
|
157
|
+
async updateIndex(sessionId, change) {
|
|
158
|
+
const sessionDir = this.getSessionDir(sessionId);
|
|
159
|
+
const indexPath = path.join(sessionDir, "index.json");
|
|
160
|
+
let index;
|
|
161
|
+
try {
|
|
162
|
+
const content = await fs.promises.readFile(indexPath, "utf-8");
|
|
163
|
+
index = JSON.parse(content);
|
|
164
|
+
} catch (error) {
|
|
165
|
+
if (error.code === "ENOENT") {
|
|
166
|
+
index = {
|
|
167
|
+
sessionId,
|
|
168
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
169
|
+
changes: []
|
|
170
|
+
};
|
|
171
|
+
} else {
|
|
172
|
+
throw error;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
if (!index.changes.includes(change.id)) {
|
|
176
|
+
index.changes.push(change.id);
|
|
177
|
+
}
|
|
178
|
+
await fs.promises.writeFile(indexPath, JSON.stringify(index, null, 2), "utf-8");
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* List all sessions with metadata
|
|
182
|
+
*/
|
|
183
|
+
async listAllSessions() {
|
|
184
|
+
const _sessionsBaseDir = path.join(this.workingDir, this.config.basePath);
|
|
185
|
+
try {
|
|
186
|
+
const sessionDirs = await fs.promises.readdir(_sessionsBaseDir);
|
|
187
|
+
const sessions = [];
|
|
188
|
+
for (const sessionId of sessionDirs) {
|
|
189
|
+
const sessionDir = path.join(_sessionsBaseDir, sessionId);
|
|
190
|
+
const stats = await fs.promises.stat(sessionDir);
|
|
191
|
+
if (stats.isDirectory()) {
|
|
192
|
+
sessions.push({
|
|
193
|
+
id: sessionId,
|
|
194
|
+
createdAt: stats.birthtimeMs
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
return sessions;
|
|
199
|
+
} catch (error) {
|
|
200
|
+
if (error.code === "ENOENT") {
|
|
201
|
+
return [];
|
|
202
|
+
}
|
|
203
|
+
throw error;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
// src/change-store.ts
|
|
209
|
+
function generateChangeId() {
|
|
210
|
+
return `change-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`;
|
|
211
|
+
}
|
|
212
|
+
function computeHash(content) {
|
|
213
|
+
return crypto.createHash("sha256").update(content, "utf-8").digest("hex");
|
|
214
|
+
}
|
|
215
|
+
function toSummary(change) {
|
|
216
|
+
return {
|
|
217
|
+
changeId: change.id,
|
|
218
|
+
filePath: change.filePath,
|
|
219
|
+
operation: change.operation,
|
|
220
|
+
timestamp: change.timestamp,
|
|
221
|
+
linesAdded: change.metadata?.linesAdded,
|
|
222
|
+
linesRemoved: change.metadata?.linesRemoved,
|
|
223
|
+
isNew: !change.before,
|
|
224
|
+
sizeAfter: change.after.size
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
var ChangeStore = class {
|
|
228
|
+
storage;
|
|
229
|
+
/** In-memory cache keyed by sessionId → changeId → FileChange */
|
|
230
|
+
cache = /* @__PURE__ */ new Map();
|
|
231
|
+
constructor(workingDir, config) {
|
|
232
|
+
this.storage = new SnapshotStorage(workingDir, config);
|
|
233
|
+
}
|
|
234
|
+
// ── Write ────────────────────────────────────────────────────────────────
|
|
235
|
+
async save(input) {
|
|
236
|
+
const change = {
|
|
237
|
+
id: generateChangeId(),
|
|
238
|
+
sessionId: input.sessionId,
|
|
239
|
+
agentId: input.agentId,
|
|
240
|
+
runId: input.runId,
|
|
241
|
+
filePath: input.filePath,
|
|
242
|
+
operation: input.operation,
|
|
243
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
244
|
+
before: input.beforeContent !== void 0 ? {
|
|
245
|
+
content: input.beforeContent,
|
|
246
|
+
hash: computeHash(input.beforeContent),
|
|
247
|
+
size: Buffer.byteLength(input.beforeContent, "utf-8")
|
|
248
|
+
} : void 0,
|
|
249
|
+
after: {
|
|
250
|
+
content: input.afterContent,
|
|
251
|
+
hash: computeHash(input.afterContent),
|
|
252
|
+
size: Buffer.byteLength(input.afterContent, "utf-8")
|
|
253
|
+
},
|
|
254
|
+
metadata: input.metadata
|
|
255
|
+
};
|
|
256
|
+
await this.storage.saveSnapshot(input.sessionId, change);
|
|
257
|
+
this._cache(input.sessionId).set(change.id, change);
|
|
258
|
+
return change;
|
|
259
|
+
}
|
|
260
|
+
// ── Read ─────────────────────────────────────────────────────────────────
|
|
261
|
+
async get(sessionId, changeId) {
|
|
262
|
+
const cached = this._cache(sessionId).get(changeId);
|
|
263
|
+
if (cached) {
|
|
264
|
+
return cached;
|
|
265
|
+
}
|
|
266
|
+
const loaded = await this.storage.loadSnapshot(sessionId, changeId);
|
|
267
|
+
if (loaded) {
|
|
268
|
+
this._cache(sessionId).set(changeId, loaded);
|
|
269
|
+
}
|
|
270
|
+
return loaded;
|
|
271
|
+
}
|
|
272
|
+
async listSession(sessionId) {
|
|
273
|
+
return this.storage.listSnapshots(sessionId);
|
|
274
|
+
}
|
|
275
|
+
async listRun(sessionId, runId) {
|
|
276
|
+
const all = await this.listSession(sessionId);
|
|
277
|
+
return all.filter((c) => c.runId === runId);
|
|
278
|
+
}
|
|
279
|
+
async listFile(sessionId, filePath) {
|
|
280
|
+
const all = await this.listSession(sessionId);
|
|
281
|
+
return all.filter((c) => c.filePath === filePath);
|
|
282
|
+
}
|
|
283
|
+
/** Returns unique file paths touched in a session */
|
|
284
|
+
async changedFiles(sessionId) {
|
|
285
|
+
const all = await this.listSession(sessionId);
|
|
286
|
+
return [...new Set(all.map((c) => c.filePath))];
|
|
287
|
+
}
|
|
288
|
+
// ── Summaries ─────────────────────────────────────────────────────────────
|
|
289
|
+
async summariesForRun(sessionId, runId) {
|
|
290
|
+
const changes = await this.listRun(sessionId, runId);
|
|
291
|
+
return changes.map(toSummary);
|
|
292
|
+
}
|
|
293
|
+
// ── Cleanup ───────────────────────────────────────────────────────────────
|
|
294
|
+
async cleanup() {
|
|
295
|
+
return this.storage.cleanupOldSessions();
|
|
296
|
+
}
|
|
297
|
+
async deleteSession(sessionId) {
|
|
298
|
+
await this.storage.deleteSession(sessionId);
|
|
299
|
+
this.cache.delete(sessionId);
|
|
300
|
+
}
|
|
301
|
+
// ── Private ───────────────────────────────────────────────────────────────
|
|
302
|
+
_cache(sessionId) {
|
|
303
|
+
let m = this.cache.get(sessionId);
|
|
304
|
+
if (!m) {
|
|
305
|
+
m = /* @__PURE__ */ new Map();
|
|
306
|
+
this.cache.set(sessionId, m);
|
|
307
|
+
}
|
|
308
|
+
return m;
|
|
309
|
+
}
|
|
310
|
+
};
|
|
311
|
+
|
|
312
|
+
// src/conflict-detector.ts
|
|
313
|
+
var ConflictDetector = class {
|
|
314
|
+
constructor(store) {
|
|
315
|
+
this.store = store;
|
|
316
|
+
}
|
|
317
|
+
async detectConflict(input) {
|
|
318
|
+
const { sessionId, filePath, agentId, operation, metadata } = input;
|
|
319
|
+
const fileHistory = await this.store.listFile(sessionId, filePath);
|
|
320
|
+
if (fileHistory.length === 0) {
|
|
321
|
+
return null;
|
|
322
|
+
}
|
|
323
|
+
const otherChanges = fileHistory.filter((c) => c.agentId !== agentId);
|
|
324
|
+
if (otherChanges.length === 0) {
|
|
325
|
+
return null;
|
|
326
|
+
}
|
|
327
|
+
const conflictingAgents = [...new Set(otherChanges.map((c) => c.agentId))];
|
|
328
|
+
const latest = fileHistory[fileHistory.length - 1];
|
|
329
|
+
if (operation === "patch" && latest.operation === "patch") {
|
|
330
|
+
const overlaps = this._checkLineOverlap(
|
|
331
|
+
metadata?.startLine,
|
|
332
|
+
metadata?.endLine,
|
|
333
|
+
latest.metadata?.startLine,
|
|
334
|
+
latest.metadata?.endLine
|
|
335
|
+
);
|
|
336
|
+
if (!overlaps) {
|
|
337
|
+
return {
|
|
338
|
+
type: "disjoint",
|
|
339
|
+
filePath,
|
|
340
|
+
currentAgentId: agentId,
|
|
341
|
+
conflictingAgents,
|
|
342
|
+
description: "Patches on different lines \u2014 safe to merge",
|
|
343
|
+
resolutionConfidence: 1
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
const overlapLines = this._overlapLines(
|
|
347
|
+
metadata?.startLine,
|
|
348
|
+
metadata?.endLine,
|
|
349
|
+
latest.metadata?.startLine,
|
|
350
|
+
latest.metadata?.endLine
|
|
351
|
+
);
|
|
352
|
+
return {
|
|
353
|
+
type: "overlapping",
|
|
354
|
+
filePath,
|
|
355
|
+
currentAgentId: agentId,
|
|
356
|
+
conflictingAgents,
|
|
357
|
+
description: `Patches overlap on lines ${overlapLines.join(", ")}`,
|
|
358
|
+
resolutionConfidence: 0.7,
|
|
359
|
+
metadata: { overlapLines }
|
|
360
|
+
};
|
|
361
|
+
}
|
|
362
|
+
if (operation === "delete" && latest.operation !== "delete" || operation !== "delete" && latest.operation === "delete") {
|
|
363
|
+
return {
|
|
364
|
+
type: "conflicting-intent",
|
|
365
|
+
filePath,
|
|
366
|
+
currentAgentId: agentId,
|
|
367
|
+
conflictingAgents,
|
|
368
|
+
description: "One agent deletes while another modifies",
|
|
369
|
+
resolutionConfidence: 0.5,
|
|
370
|
+
metadata: { intentA: operation, intentB: latest.operation }
|
|
371
|
+
};
|
|
372
|
+
}
|
|
373
|
+
if (operation === "write" && latest.operation === "write") {
|
|
374
|
+
const semanticIssue = this._detectSemanticConflict(
|
|
375
|
+
latest.after.content,
|
|
376
|
+
metadata?.content ?? ""
|
|
377
|
+
);
|
|
378
|
+
if (semanticIssue) {
|
|
379
|
+
return {
|
|
380
|
+
type: "semantic",
|
|
381
|
+
filePath,
|
|
382
|
+
currentAgentId: agentId,
|
|
383
|
+
conflictingAgents,
|
|
384
|
+
description: "Semantic conflict in file rewrites",
|
|
385
|
+
resolutionConfidence: 0.3,
|
|
386
|
+
metadata: { semanticIssue }
|
|
387
|
+
};
|
|
388
|
+
}
|
|
389
|
+
return {
|
|
390
|
+
type: "overlapping",
|
|
391
|
+
filePath,
|
|
392
|
+
currentAgentId: agentId,
|
|
393
|
+
conflictingAgents,
|
|
394
|
+
description: "Full file overwrite by multiple agents",
|
|
395
|
+
resolutionConfidence: 0.6
|
|
396
|
+
};
|
|
397
|
+
}
|
|
398
|
+
return {
|
|
399
|
+
type: "overlapping",
|
|
400
|
+
filePath,
|
|
401
|
+
currentAgentId: agentId,
|
|
402
|
+
conflictingAgents,
|
|
403
|
+
description: "Generic file modification conflict",
|
|
404
|
+
resolutionConfidence: 0.5
|
|
405
|
+
};
|
|
406
|
+
}
|
|
407
|
+
// ── Private ───────────────────────────────────────────────────────────────
|
|
408
|
+
_checkLineOverlap(startA, endA, startB, endB) {
|
|
409
|
+
if (startA === void 0 || endA === void 0 || startB === void 0 || endB === void 0) {
|
|
410
|
+
return true;
|
|
411
|
+
}
|
|
412
|
+
return !(endA < startB || endB < startA);
|
|
413
|
+
}
|
|
414
|
+
_overlapLines(startA, endA, startB, endB) {
|
|
415
|
+
if (startA === void 0 || endA === void 0 || startB === void 0 || endB === void 0) {
|
|
416
|
+
return [];
|
|
417
|
+
}
|
|
418
|
+
const from = Math.max(startA, startB);
|
|
419
|
+
const to = Math.min(endA, endB);
|
|
420
|
+
if (from > to) {
|
|
421
|
+
return [];
|
|
422
|
+
}
|
|
423
|
+
const lines = [];
|
|
424
|
+
for (let i = from; i <= to; i++) {
|
|
425
|
+
lines.push(i);
|
|
426
|
+
}
|
|
427
|
+
return lines;
|
|
428
|
+
}
|
|
429
|
+
_detectSemanticConflict(contentA, contentB) {
|
|
430
|
+
const linesA = contentA.split("\n").length;
|
|
431
|
+
const linesB = contentB.split("\n").length;
|
|
432
|
+
const diff = Math.abs(linesA - linesB);
|
|
433
|
+
if (diff > linesA * 0.3) {
|
|
434
|
+
return `Significant structural change (${diff} line difference)`;
|
|
435
|
+
}
|
|
436
|
+
return null;
|
|
437
|
+
}
|
|
438
|
+
};
|
|
439
|
+
|
|
440
|
+
// src/diff-generator.ts
|
|
441
|
+
function lcsTable(a, b) {
|
|
442
|
+
const m = a.length;
|
|
443
|
+
const n = b.length;
|
|
444
|
+
const dp = Array.from({ length: m + 1 }, () => new Array(n + 1).fill(0));
|
|
445
|
+
for (let i = 1; i <= m; i++) {
|
|
446
|
+
for (let j = 1; j <= n; j++) {
|
|
447
|
+
dp[i][j] = a[i - 1] === b[j - 1] ? dp[i - 1][j - 1] + 1 : Math.max(dp[i - 1][j], dp[i][j - 1]);
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
return dp;
|
|
451
|
+
}
|
|
452
|
+
function lineDiff(oldText, newText) {
|
|
453
|
+
const oldLines = oldText ? oldText.split("\n") : [];
|
|
454
|
+
const newLines = newText ? newText.split("\n") : [];
|
|
455
|
+
if (oldLines[oldLines.length - 1] === "") {
|
|
456
|
+
oldLines.pop();
|
|
457
|
+
}
|
|
458
|
+
if (newLines[newLines.length - 1] === "") {
|
|
459
|
+
newLines.pop();
|
|
460
|
+
}
|
|
461
|
+
const dp = lcsTable(oldLines, newLines);
|
|
462
|
+
const ops = [];
|
|
463
|
+
let i = oldLines.length;
|
|
464
|
+
let j = newLines.length;
|
|
465
|
+
while (i > 0 || j > 0) {
|
|
466
|
+
if (i > 0 && j > 0 && oldLines[i - 1] === newLines[j - 1]) {
|
|
467
|
+
ops.push({ op: " ", line: oldLines[i - 1] });
|
|
468
|
+
i--;
|
|
469
|
+
j--;
|
|
470
|
+
} else if (j > 0 && (i === 0 || dp[i][j - 1] >= dp[i - 1][j])) {
|
|
471
|
+
ops.push({ op: "+", line: newLines[j - 1] });
|
|
472
|
+
j--;
|
|
473
|
+
} else {
|
|
474
|
+
ops.push({ op: "-", line: oldLines[i - 1] });
|
|
475
|
+
i--;
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
ops.reverse();
|
|
479
|
+
return ops;
|
|
480
|
+
}
|
|
481
|
+
function generateUnifiedDiff(filePath, beforeContent, afterContent, operation) {
|
|
482
|
+
if (operation === "delete") {
|
|
483
|
+
const lines = (beforeContent ?? "").split("\n");
|
|
484
|
+
if (lines[lines.length - 1] === "") {
|
|
485
|
+
lines.pop();
|
|
486
|
+
}
|
|
487
|
+
const header2 = `--- ${filePath} (before)
|
|
488
|
+
+++ /dev/null (deleted)
|
|
489
|
+
`;
|
|
490
|
+
const hunk = lines.length > 0 ? `@@ -1,${lines.length} +0,0 @@
|
|
491
|
+
${lines.map((l) => `-${l}`).join("\n")}
|
|
492
|
+
` : `@@ -0,0 +0,0 @@
|
|
493
|
+
`;
|
|
494
|
+
return header2 + hunk;
|
|
495
|
+
}
|
|
496
|
+
const oldText = beforeContent ?? "";
|
|
497
|
+
const newText = afterContent;
|
|
498
|
+
const ops = lineDiff(oldText, newText);
|
|
499
|
+
const CONTEXT = 3;
|
|
500
|
+
const header = `--- ${filePath} (before)
|
|
501
|
+
+++ ${filePath} (after)
|
|
502
|
+
`;
|
|
503
|
+
const hunks = [];
|
|
504
|
+
const changeRanges = [];
|
|
505
|
+
for (let idx = 0; idx < ops.length; idx++) {
|
|
506
|
+
if (ops[idx].op !== " ") {
|
|
507
|
+
const rangeStart = Math.max(0, idx - CONTEXT);
|
|
508
|
+
const rangeEnd = Math.min(ops.length - 1, idx + CONTEXT);
|
|
509
|
+
if (changeRanges.length > 0 && rangeStart <= changeRanges[changeRanges.length - 1].end) {
|
|
510
|
+
changeRanges[changeRanges.length - 1].end = rangeEnd;
|
|
511
|
+
} else {
|
|
512
|
+
changeRanges.push({ start: rangeStart, end: rangeEnd });
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
const merged = [];
|
|
517
|
+
for (const r of changeRanges) {
|
|
518
|
+
if (merged.length > 0 && r.start <= merged[merged.length - 1].end + 1) {
|
|
519
|
+
merged[merged.length - 1].end = Math.max(merged[merged.length - 1].end, r.end);
|
|
520
|
+
} else {
|
|
521
|
+
merged.push({ ...r });
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
const opOldLine = [];
|
|
525
|
+
const opNewLine = [];
|
|
526
|
+
let ol = 1;
|
|
527
|
+
let nl = 1;
|
|
528
|
+
for (const op of ops) {
|
|
529
|
+
opOldLine.push(ol);
|
|
530
|
+
opNewLine.push(nl);
|
|
531
|
+
if (op.op === " " || op.op === "-") {
|
|
532
|
+
ol++;
|
|
533
|
+
}
|
|
534
|
+
if (op.op === " " || op.op === "+") {
|
|
535
|
+
nl++;
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
if (merged.length === 0) {
|
|
539
|
+
return header + `@@ -1,${ol - 1} +1,${nl - 1} @@
|
|
540
|
+
` + ops.map((o) => ` ${o.line}`).join("\n") + (ops.length > 0 ? "\n" : "");
|
|
541
|
+
}
|
|
542
|
+
for (const range of merged) {
|
|
543
|
+
const slicedOps = ops.slice(range.start, range.end + 1);
|
|
544
|
+
const firstOldLine = opOldLine[range.start] ?? 1;
|
|
545
|
+
const firstNewLine = opNewLine[range.start] ?? 1;
|
|
546
|
+
let oldCount = 0;
|
|
547
|
+
let newCount = 0;
|
|
548
|
+
const hunkLines = [];
|
|
549
|
+
for (const op of slicedOps) {
|
|
550
|
+
if (op.op === " ") {
|
|
551
|
+
oldCount++;
|
|
552
|
+
newCount++;
|
|
553
|
+
hunkLines.push(` ${op.line}`);
|
|
554
|
+
} else if (op.op === "-") {
|
|
555
|
+
oldCount++;
|
|
556
|
+
hunkLines.push(`-${op.line}`);
|
|
557
|
+
} else {
|
|
558
|
+
newCount++;
|
|
559
|
+
hunkLines.push(`+${op.line}`);
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
hunks.push({
|
|
563
|
+
oldStart: firstOldLine,
|
|
564
|
+
oldCount,
|
|
565
|
+
newStart: firstNewLine,
|
|
566
|
+
newCount,
|
|
567
|
+
lines: hunkLines
|
|
568
|
+
});
|
|
569
|
+
}
|
|
570
|
+
const hunkStr = hunks.map((h) => {
|
|
571
|
+
const header2 = `@@ -${h.oldStart},${h.oldCount} +${h.newStart},${h.newCount} @@`;
|
|
572
|
+
return header2 + "\n" + h.lines.join("\n") + "\n";
|
|
573
|
+
}).join("");
|
|
574
|
+
return header + hunkStr;
|
|
575
|
+
}
|
|
576
|
+
function countDiffLines(diff) {
|
|
577
|
+
const lines = diff.split("\n");
|
|
578
|
+
let added = 0;
|
|
579
|
+
let removed = 0;
|
|
580
|
+
for (const line of lines) {
|
|
581
|
+
if (line.startsWith("+") && !line.startsWith("+++")) {
|
|
582
|
+
added++;
|
|
583
|
+
}
|
|
584
|
+
if (line.startsWith("-") && !line.startsWith("---")) {
|
|
585
|
+
removed++;
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
return { added, removed };
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
export { ChangeStore, ConflictDetector, SnapshotStorage, computeHash, countDiffLines, generateUnifiedDiff, toSummary };
|
|
592
|
+
//# sourceMappingURL=index.js.map
|
|
593
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/snapshot-storage.ts","../src/change-store.ts","../src/conflict-detector.ts","../src/diff-generator.ts"],"names":["header"],"mappings":"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","file":"index.js","sourcesContent":["/**\n * Snapshot storage - persist file change snapshots to disk\n */\n\nimport * as fs from 'node:fs';\nimport * as path from 'node:path';\nimport type { FileChange, StorageConfig } from './types.js';\n\nconst DEFAULT_CONFIG: Required<StorageConfig> = {\n basePath: '.kb/agents/sessions',\n maxSessions: 30,\n maxAgeDays: 30,\n maxTotalSizeMb: 500,\n};\n\n/**\n * Storage for file change snapshots\n */\nexport class SnapshotStorage {\n private workingDir: string;\n private config: Required<StorageConfig>;\n\n constructor(workingDir: string, config?: Partial<StorageConfig>) {\n this.workingDir = workingDir;\n this.config = { ...DEFAULT_CONFIG, ...config };\n }\n\n /**\n * Save snapshot to disk\n */\n async saveSnapshot(sessionId: string, change: FileChange): Promise<void> {\n const sessionDir = this.getSessionDir(sessionId);\n const snapshotsDir = path.join(sessionDir, 'snapshots');\n\n // Ensure directories exist\n await fs.promises.mkdir(snapshotsDir, { recursive: true });\n\n // Write snapshot file\n const snapshotPath = path.join(snapshotsDir, `${change.id}.json`);\n await fs.promises.writeFile(\n snapshotPath,\n JSON.stringify(change, null, 2),\n 'utf-8'\n );\n\n // Update index\n await this.updateIndex(sessionId, change);\n }\n\n /**\n * Load snapshot by ID\n */\n async loadSnapshot(sessionId: string, changeId: string): Promise<FileChange | null> {\n try {\n const snapshotPath = path.join(\n this.getSessionDir(sessionId),\n 'snapshots',\n `${changeId}.json`\n );\n\n const content = await fs.promises.readFile(snapshotPath, 'utf-8');\n return JSON.parse(content) as FileChange;\n } catch (error) {\n if ((error as NodeJS.ErrnoException).code === 'ENOENT') {\n return null;\n }\n if (error instanceof SyntaxError) {\n // Corrupted JSON - return null instead of throwing\n return null;\n }\n throw error;\n }\n }\n\n /**\n * List all snapshots for session\n */\n async listSnapshots(sessionId: string): Promise<FileChange[]> {\n try {\n const indexPath = path.join(this.getSessionDir(sessionId), 'index.json');\n const content = await fs.promises.readFile(indexPath, 'utf-8');\n const index = JSON.parse(content) as SessionIndex;\n\n // Load full snapshots\n const changes: FileChange[] = [];\n for (const changeId of index.changes) {\n const change = await this.loadSnapshot(sessionId, changeId);\n if (change) {\n changes.push(change);\n }\n }\n\n // Sort by timestamp (chronological order)\n changes.sort((a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime());\n\n return changes;\n } catch (error) {\n if ((error as NodeJS.ErrnoException).code === 'ENOENT') {\n // Index missing - fall back to reading snapshots directory directly\n const snapshotsDir = path.join(this.getSessionDir(sessionId), 'snapshots');\n\n try {\n const files = await fs.promises.readdir(snapshotsDir);\n const changes: FileChange[] = [];\n\n for (const file of files) {\n if (file.endsWith('.json')) {\n const changeId = file.replace('.json', '');\n const change = await this.loadSnapshot(sessionId, changeId);\n if (change) {\n changes.push(change);\n }\n }\n }\n\n // Sort by timestamp\n changes.sort((a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime());\n\n return changes;\n } catch {\n return [];\n }\n }\n throw error;\n }\n }\n\n /**\n * Delete all snapshots for session\n */\n async deleteSession(sessionId: string): Promise<void> {\n const sessionDir = this.getSessionDir(sessionId);\n\n try {\n await fs.promises.rm(sessionDir, { recursive: true, force: true });\n } catch (error) {\n // Ignore if doesn't exist\n if ((error as NodeJS.ErrnoException).code !== 'ENOENT') {\n throw error;\n }\n }\n }\n\n /**\n * Cleanup old sessions based on retention policy\n */\n async cleanupOldSessions(): Promise<{ deleted: number; keptLast: number }> {\n const _sessionsBaseDir = path.join(this.workingDir, this.config.basePath);\n\n try {\n const sessions = await this.listAllSessions();\n\n // Sort by creation time (newest first)\n sessions.sort((a, b) => b.createdAt - a.createdAt);\n\n let deleted = 0;\n\n // Delete by count (keep last N)\n if (sessions.length > this.config.maxSessions) {\n const toDelete = sessions.slice(this.config.maxSessions);\n\n for (const session of toDelete) {\n await this.deleteSession(session.id);\n deleted++;\n }\n }\n\n // Delete by age\n const now = Date.now();\n const maxAge = this.config.maxAgeDays * 24 * 60 * 60 * 1000;\n\n for (const session of sessions) {\n if (now - session.createdAt > maxAge) {\n await this.deleteSession(session.id);\n deleted++;\n }\n }\n\n return {\n deleted,\n keptLast: Math.min(sessions.length, this.config.maxSessions),\n };\n } catch (error) {\n if ((error as NodeJS.ErrnoException).code === 'ENOENT') {\n return { deleted: 0, keptLast: 0 };\n }\n throw error;\n }\n }\n\n // ═══════════════════════════════════════════════════════════════════════\n // Private Methods\n // ═══════════════════════════════════════════════════════════════════════\n\n /**\n * Get session directory path\n */\n private getSessionDir(sessionId: string): string {\n return path.join(this.workingDir, this.config.basePath, sessionId);\n }\n\n /**\n * Update session index\n */\n private async updateIndex(sessionId: string, change: FileChange): Promise<void> {\n const sessionDir = this.getSessionDir(sessionId);\n const indexPath = path.join(sessionDir, 'index.json');\n\n let index: SessionIndex;\n\n try {\n const content = await fs.promises.readFile(indexPath, 'utf-8');\n index = JSON.parse(content);\n } catch (error) {\n if ((error as NodeJS.ErrnoException).code === 'ENOENT') {\n // Create new index\n index = {\n sessionId,\n createdAt: new Date().toISOString(),\n changes: [],\n };\n } else {\n throw error;\n }\n }\n\n // Add change to index\n if (!index.changes.includes(change.id)) {\n index.changes.push(change.id);\n }\n\n // Write index\n await fs.promises.writeFile(indexPath, JSON.stringify(index, null, 2), 'utf-8');\n }\n\n /**\n * List all sessions with metadata\n */\n private async listAllSessions(): Promise<SessionMetadata[]> {\n const _sessionsBaseDir = path.join(this.workingDir, this.config.basePath);\n\n try {\n const sessionDirs = await fs.promises.readdir(_sessionsBaseDir);\n\n const sessions: SessionMetadata[] = [];\n\n for (const sessionId of sessionDirs) {\n const sessionDir = path.join(_sessionsBaseDir, sessionId);\n const stats = await fs.promises.stat(sessionDir);\n\n if (stats.isDirectory()) {\n sessions.push({\n id: sessionId,\n createdAt: stats.birthtimeMs,\n });\n }\n }\n\n return sessions;\n } catch (error) {\n if ((error as NodeJS.ErrnoException).code === 'ENOENT') {\n return [];\n }\n throw error;\n }\n }\n}\n\n// ═══════════════════════════════════════════════════════════════════════\n// Internal Types\n// ═══════════════════════════════════════════════════════════════════════\n\ninterface SessionIndex {\n sessionId: string;\n createdAt: string;\n changes: string[]; // Array of change IDs\n}\n\ninterface SessionMetadata {\n id: string;\n createdAt: number; // Timestamp\n}\n","/**\n * ChangeStore — persists and queries file change snapshots.\n *\n * Design principles (vs old FileChangeTracker):\n * - No EventEmitter: events belong in middleware (AgentEventBus)\n * - No agentId/runId state: injected per-call by ChangeTrackingMiddleware\n * - No rollback I/O: rollback logic lives in middleware (uses workingDir from RunContext)\n * - Pure storage + query: save, load, list, cleanup\n */\n\nimport * as crypto from 'node:crypto';\nimport type { FileChange, FileChangeSummary, StorageConfig } from './types.js';\nimport { SnapshotStorage } from './snapshot-storage.js';\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Helpers\n// ─────────────────────────────────────────────────────────────────────────────\n\nfunction generateChangeId(): string {\n return `change-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`;\n}\n\nexport function computeHash(content: string): string {\n return crypto.createHash('sha256').update(content, 'utf-8').digest('hex');\n}\n\nexport function toSummary(change: FileChange): FileChangeSummary {\n return {\n changeId: change.id,\n filePath: change.filePath,\n operation: change.operation,\n timestamp: change.timestamp,\n linesAdded: change.metadata?.linesAdded,\n linesRemoved: change.metadata?.linesRemoved,\n isNew: !change.before,\n sizeAfter: change.after.size,\n };\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// ChangeStore\n// ─────────────────────────────────────────────────────────────────────────────\n\nexport interface SaveChangeInput {\n sessionId: string;\n agentId: string;\n runId: string;\n filePath: string;\n operation: 'write' | 'patch' | 'delete';\n beforeContent: string | undefined;\n afterContent: string;\n metadata?: FileChange['metadata'];\n}\n\nexport class ChangeStore {\n private readonly storage: SnapshotStorage;\n /** In-memory cache keyed by sessionId → changeId → FileChange */\n private readonly cache = new Map<string, Map<string, FileChange>>();\n\n constructor(workingDir: string, config?: Partial<StorageConfig>) {\n this.storage = new SnapshotStorage(workingDir, config);\n }\n\n // ── Write ────────────────────────────────────────────────────────────────\n\n async save(input: SaveChangeInput): Promise<FileChange> {\n const change: FileChange = {\n id: generateChangeId(),\n sessionId: input.sessionId,\n agentId: input.agentId,\n runId: input.runId,\n filePath: input.filePath,\n operation: input.operation,\n timestamp: new Date().toISOString(),\n before: input.beforeContent !== undefined\n ? {\n content: input.beforeContent,\n hash: computeHash(input.beforeContent),\n size: Buffer.byteLength(input.beforeContent, 'utf-8'),\n }\n : undefined,\n after: {\n content: input.afterContent,\n hash: computeHash(input.afterContent),\n size: Buffer.byteLength(input.afterContent, 'utf-8'),\n },\n metadata: input.metadata,\n };\n\n await this.storage.saveSnapshot(input.sessionId, change);\n this._cache(input.sessionId).set(change.id, change);\n return change;\n }\n\n // ── Read ─────────────────────────────────────────────────────────────────\n\n async get(sessionId: string, changeId: string): Promise<FileChange | null> {\n const cached = this._cache(sessionId).get(changeId);\n if (cached) {return cached;}\n const loaded = await this.storage.loadSnapshot(sessionId, changeId);\n if (loaded) {this._cache(sessionId).set(changeId, loaded);}\n return loaded;\n }\n\n async listSession(sessionId: string): Promise<FileChange[]> {\n return this.storage.listSnapshots(sessionId);\n }\n\n async listRun(sessionId: string, runId: string): Promise<FileChange[]> {\n const all = await this.listSession(sessionId);\n return all.filter((c) => c.runId === runId);\n }\n\n async listFile(sessionId: string, filePath: string): Promise<FileChange[]> {\n const all = await this.listSession(sessionId);\n return all.filter((c) => c.filePath === filePath);\n }\n\n /** Returns unique file paths touched in a session */\n async changedFiles(sessionId: string): Promise<string[]> {\n const all = await this.listSession(sessionId);\n return [...new Set(all.map((c) => c.filePath))];\n }\n\n // ── Summaries ─────────────────────────────────────────────────────────────\n\n async summariesForRun(sessionId: string, runId: string): Promise<FileChangeSummary[]> {\n const changes = await this.listRun(sessionId, runId);\n return changes.map(toSummary);\n }\n\n // ── Cleanup ───────────────────────────────────────────────────────────────\n\n async cleanup(): Promise<{ deleted: number; keptLast: number }> {\n return this.storage.cleanupOldSessions();\n }\n\n async deleteSession(sessionId: string): Promise<void> {\n await this.storage.deleteSession(sessionId);\n this.cache.delete(sessionId);\n }\n\n // ── Private ───────────────────────────────────────────────────────────────\n\n private _cache(sessionId: string): Map<string, FileChange> {\n let m = this.cache.get(sessionId);\n if (!m) {\n m = new Map();\n this.cache.set(sessionId, m);\n }\n return m;\n }\n}\n","/**\n * ConflictDetector — detects file conflicts before write operations.\n * Works with ChangeStore instead of the old FileChangeTracker.\n */\n\nimport type { ChangeStore } from './change-store.js';\n\nexport type ConflictType = 'none' | 'disjoint' | 'overlapping' | 'conflicting-intent' | 'semantic';\n\nexport interface DetectedConflict {\n type: ConflictType;\n filePath: string;\n /** Agent attempting the write */\n currentAgentId: string;\n /** Other agents who modified this file */\n conflictingAgents: string[];\n description: string;\n /** 0–1: higher = easier to auto-resolve */\n resolutionConfidence: number;\n metadata?: {\n overlapLines?: number[];\n intentA?: string;\n intentB?: string;\n semanticIssue?: string;\n };\n}\n\nexport interface ConflictCheckInput {\n sessionId: string;\n filePath: string;\n agentId: string;\n operation: 'write' | 'patch' | 'delete';\n metadata?: {\n startLine?: number;\n endLine?: number;\n content?: string;\n };\n}\n\nexport class ConflictDetector {\n constructor(private readonly store: ChangeStore) {}\n\n async detectConflict(input: ConflictCheckInput): Promise<DetectedConflict | null> {\n const { sessionId, filePath, agentId, operation, metadata } = input;\n\n const fileHistory = await this.store.listFile(sessionId, filePath);\n if (fileHistory.length === 0) {return null;}\n\n const otherChanges = fileHistory.filter((c) => c.agentId !== agentId);\n if (otherChanges.length === 0) {return null;}\n\n const conflictingAgents = [...new Set(otherChanges.map((c) => c.agentId))];\n const latest = fileHistory[fileHistory.length - 1]!;\n\n // Disjoint patches → safe to auto-merge\n if (operation === 'patch' && latest.operation === 'patch') {\n const overlaps = this._checkLineOverlap(\n metadata?.startLine, metadata?.endLine,\n latest.metadata?.startLine, latest.metadata?.endLine,\n );\n\n if (!overlaps) {\n return {\n type: 'disjoint',\n filePath,\n currentAgentId: agentId,\n conflictingAgents,\n description: 'Patches on different lines — safe to merge',\n resolutionConfidence: 1.0,\n };\n }\n\n const overlapLines = this._overlapLines(\n metadata?.startLine, metadata?.endLine,\n latest.metadata?.startLine, latest.metadata?.endLine,\n );\n return {\n type: 'overlapping',\n filePath,\n currentAgentId: agentId,\n conflictingAgents,\n description: `Patches overlap on lines ${overlapLines.join(', ')}`,\n resolutionConfidence: 0.7,\n metadata: { overlapLines },\n };\n }\n\n // Conflicting intent: one deletes, another modifies\n if (\n (operation === 'delete' && latest.operation !== 'delete') ||\n (operation !== 'delete' && latest.operation === 'delete')\n ) {\n return {\n type: 'conflicting-intent',\n filePath,\n currentAgentId: agentId,\n conflictingAgents,\n description: 'One agent deletes while another modifies',\n resolutionConfidence: 0.5,\n metadata: { intentA: operation, intentB: latest.operation },\n };\n }\n\n // Two full rewrites\n if (operation === 'write' && latest.operation === 'write') {\n const semanticIssue = this._detectSemanticConflict(\n latest.after.content,\n metadata?.content ?? '',\n );\n if (semanticIssue) {\n return {\n type: 'semantic',\n filePath,\n currentAgentId: agentId,\n conflictingAgents,\n description: 'Semantic conflict in file rewrites',\n resolutionConfidence: 0.3,\n metadata: { semanticIssue },\n };\n }\n return {\n type: 'overlapping',\n filePath,\n currentAgentId: agentId,\n conflictingAgents,\n description: 'Full file overwrite by multiple agents',\n resolutionConfidence: 0.6,\n };\n }\n\n return {\n type: 'overlapping',\n filePath,\n currentAgentId: agentId,\n conflictingAgents,\n description: 'Generic file modification conflict',\n resolutionConfidence: 0.5,\n };\n }\n\n // ── Private ───────────────────────────────────────────────────────────────\n\n private _checkLineOverlap(\n startA?: number, endA?: number,\n startB?: number, endB?: number,\n ): boolean {\n if (startA === undefined || endA === undefined || startB === undefined || endB === undefined) {\n return true; // conservative: assume overlap\n }\n return !(endA < startB || endB < startA);\n }\n\n private _overlapLines(\n startA?: number, endA?: number,\n startB?: number, endB?: number,\n ): number[] {\n if (startA === undefined || endA === undefined || startB === undefined || endB === undefined) {\n return [];\n }\n const from = Math.max(startA, startB);\n const to = Math.min(endA, endB);\n if (from > to) {return [];}\n const lines: number[] = [];\n for (let i = from; i <= to; i++) {lines.push(i);}\n return lines;\n }\n\n private _detectSemanticConflict(contentA: string, contentB: string): string | null {\n const linesA = contentA.split('\\n').length;\n const linesB = contentB.split('\\n').length;\n const diff = Math.abs(linesA - linesB);\n if (diff > linesA * 0.3) {\n return `Significant structural change (${diff} line difference)`;\n }\n return null;\n }\n}\n","/**\n * Unified diff generator for file change snapshots.\n * Pure implementation — no external dependencies.\n */\n\n/**\n * Compute longest common subsequence lengths for Myers diff algorithm.\n */\nfunction lcsTable(a: string[], b: string[]): number[][] {\n const m = a.length;\n const n = b.length;\n const dp: number[][] = Array.from({ length: m + 1 }, () => new Array(n + 1).fill(0));\n for (let i = 1; i <= m; i++) {\n for (let j = 1; j <= n; j++) {\n dp[i]![j] = a[i - 1] === b[j - 1] ? dp[i - 1]![j - 1]! + 1 : Math.max(dp[i - 1]![j]!, dp[i]![j - 1]!);\n }\n }\n return dp;\n}\n\ntype DiffOp = { op: '+' | '-' | ' '; line: string };\n\n/**\n * Compute line-level diff between two texts using LCS.\n */\nfunction lineDiff(oldText: string, newText: string): DiffOp[] {\n const oldLines = oldText ? oldText.split('\\n') : [];\n const newLines = newText ? newText.split('\\n') : [];\n\n // Remove trailing empty line from split if original had no trailing newline\n if (oldLines[oldLines.length - 1] === '') {oldLines.pop();}\n if (newLines[newLines.length - 1] === '') {newLines.pop();}\n\n const dp = lcsTable(oldLines, newLines);\n\n const ops: DiffOp[] = [];\n\n let i = oldLines.length;\n let j = newLines.length;\n\n while (i > 0 || j > 0) {\n if (i > 0 && j > 0 && oldLines[i - 1] === newLines[j - 1]) {\n ops.push({ op: ' ', line: oldLines[i - 1]! });\n i--;\n j--;\n } else if (j > 0 && (i === 0 || dp[i]![j - 1]! >= dp[i - 1]![j]!)) {\n ops.push({ op: '+', line: newLines[j - 1]! });\n j--;\n } else {\n ops.push({ op: '-', line: oldLines[i - 1]! });\n i--;\n }\n }\n\n ops.reverse();\n return ops;\n}\n\n/**\n * Generate a unified diff string from before/after content.\n * Context lines: 3 lines before and after each change hunk.\n */\nexport function generateUnifiedDiff(\n filePath: string,\n beforeContent: string | undefined,\n afterContent: string,\n operation: 'write' | 'patch' | 'delete'\n): string {\n if (operation === 'delete') {\n const lines = (beforeContent ?? '').split('\\n');\n if (lines[lines.length - 1] === '') {lines.pop();}\n const header = `--- ${filePath}\\t(before)\\n+++ /dev/null\\t(deleted)\\n`;\n const hunk = lines.length > 0\n ? `@@ -1,${lines.length} +0,0 @@\\n${lines.map((l) => `-${l}`).join('\\n')}\\n`\n : `@@ -0,0 +0,0 @@\\n`;\n return header + hunk;\n }\n\n const oldText = beforeContent ?? '';\n const newText = afterContent;\n const ops = lineDiff(oldText, newText);\n\n const CONTEXT = 3;\n const header = `--- ${filePath}\\t(before)\\n+++ ${filePath}\\t(after)\\n`;\n\n // Build hunks from ops\n type Hunk = { oldStart: number; oldCount: number; newStart: number; newCount: number; lines: string[] };\n const hunks: Hunk[] = [];\n\n const oldLine = 1;\n const newLine = 1;\n\n // Identify change ranges (with context)\n type ChangeRange = { start: number; end: number };\n const changeRanges: ChangeRange[] = [];\n\n for (let idx = 0; idx < ops.length; idx++) {\n if (ops[idx]!.op !== ' ') {\n const rangeStart = Math.max(0, idx - CONTEXT);\n const rangeEnd = Math.min(ops.length - 1, idx + CONTEXT);\n\n if (changeRanges.length > 0 && rangeStart <= changeRanges[changeRanges.length - 1]!.end) {\n changeRanges[changeRanges.length - 1]!.end = rangeEnd;\n } else {\n changeRanges.push({ start: rangeStart, end: rangeEnd });\n }\n }\n }\n\n // Merge overlapping ranges\n const merged: ChangeRange[] = [];\n for (const r of changeRanges) {\n if (merged.length > 0 && r.start <= merged[merged.length - 1]!.end + 1) {\n merged[merged.length - 1]!.end = Math.max(merged[merged.length - 1]!.end, r.end);\n } else {\n merged.push({ ...r });\n }\n }\n\n // Build actual line counters per op\n const opOldLine: number[] = [];\n const opNewLine: number[] = [];\n let ol = 1;\n let nl = 1;\n for (const op of ops) {\n opOldLine.push(ol);\n opNewLine.push(nl);\n if (op.op === ' ' || op.op === '-') {ol++;}\n if (op.op === ' ' || op.op === '+') {nl++;}\n }\n\n if (merged.length === 0) {\n // No changes\n return header + `@@ -1,${ol - 1} +1,${nl - 1} @@\\n` +\n ops.map((o) => ` ${o.line}`).join('\\n') + (ops.length > 0 ? '\\n' : '');\n }\n\n for (const range of merged) {\n const slicedOps = ops.slice(range.start, range.end + 1);\n const firstOldLine = opOldLine[range.start] ?? 1;\n const firstNewLine = opNewLine[range.start] ?? 1;\n\n let oldCount = 0;\n let newCount = 0;\n const hunkLines: string[] = [];\n for (const op of slicedOps) {\n if (op.op === ' ') { oldCount++; newCount++; hunkLines.push(` ${op.line}`); }\n else if (op.op === '-') { oldCount++; hunkLines.push(`-${op.line}`); }\n else { newCount++; hunkLines.push(`+${op.line}`); }\n }\n\n hunks.push({\n oldStart: firstOldLine,\n oldCount,\n newStart: firstNewLine,\n newCount,\n lines: hunkLines,\n });\n }\n\n // Silence unused variable warning\n void oldLine;\n void newLine;\n\n const hunkStr = hunks.map((h) => {\n const header2 = `@@ -${h.oldStart},${h.oldCount} +${h.newStart},${h.newCount} @@`;\n return header2 + '\\n' + h.lines.join('\\n') + '\\n';\n }).join('');\n\n return header + hunkStr;\n}\n\n/**\n * Count added and removed lines from a unified diff string.\n */\nexport function countDiffLines(diff: string): { added: number; removed: number } {\n const lines = diff.split('\\n');\n let added = 0;\n let removed = 0;\n for (const line of lines) {\n if (line.startsWith('+') && !line.startsWith('+++')) {added++;}\n if (line.startsWith('-') && !line.startsWith('---')) {removed++;}\n }\n return { added, removed };\n}\n"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@kb-labs/agent-history",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "File change history, snapshots and conflict resolution for KB Labs Agents",
|
|
5
|
+
"files": [
|
|
6
|
+
"dist"
|
|
7
|
+
],
|
|
8
|
+
"type": "module",
|
|
9
|
+
"main": "./dist/index.js",
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"exports": {
|
|
12
|
+
".": {
|
|
13
|
+
"types": "./dist/index.d.ts",
|
|
14
|
+
"import": "./dist/index.js"
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
"scripts": {
|
|
18
|
+
"build": "tsup",
|
|
19
|
+
"dev": "tsup --watch",
|
|
20
|
+
"clean": "rimraf dist",
|
|
21
|
+
"type-check": "tsc --noEmit",
|
|
22
|
+
"lint": "eslint src --ext .ts",
|
|
23
|
+
"lint:fix": "eslint . --fix",
|
|
24
|
+
"test": "vitest run --passWithNoTests",
|
|
25
|
+
"test:watch": "vitest"
|
|
26
|
+
},
|
|
27
|
+
"dependencies": {
|
|
28
|
+
"@kb-labs/agent-contracts": "workspace:*"
|
|
29
|
+
},
|
|
30
|
+
"devDependencies": {
|
|
31
|
+
"@types/node": "^24.3.3",
|
|
32
|
+
"rimraf": "^6.0.1",
|
|
33
|
+
"tsup": "^8.5.0",
|
|
34
|
+
"typescript": "^5.6.3",
|
|
35
|
+
"@kb-labs/devkit": "link:../../../../infra/kb-labs-devkit",
|
|
36
|
+
"vitest": "^3.2.4"
|
|
37
|
+
},
|
|
38
|
+
"keywords": [
|
|
39
|
+
"kb-labs",
|
|
40
|
+
"agents",
|
|
41
|
+
"history",
|
|
42
|
+
"snapshots",
|
|
43
|
+
"conflict-resolution"
|
|
44
|
+
],
|
|
45
|
+
"author": "KB Labs",
|
|
46
|
+
"license": "MIT",
|
|
47
|
+
"engines": {
|
|
48
|
+
"node": ">=20.0.0",
|
|
49
|
+
"pnpm": ">=9.0.0"
|
|
50
|
+
}
|
|
51
|
+
}
|