Skip to content

Commit 2156e89

Browse files
committed
feat: DO based git, fs storage, commit every saved file
1 parent e380ee4 commit 2156e89

File tree

6 files changed

+360
-14
lines changed

6 files changed

+360
-14
lines changed

worker/agents/core/simpleGeneratorAgent.ts

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ import { ConversationMessage, ConversationState } from '../inferutils/common';
4848
import { DeepCodeDebugger } from '../assistants/codeDebugger';
4949
import { DeepDebugResult } from './types';
5050
import { StateMigration } from './stateMigration';
51+
import { GitVersionControl } from '../git';
5152

5253
interface Operations {
5354
codeReview: CodeReviewOperation;
@@ -80,6 +81,7 @@ export class SimpleCodeGeneratorAgent extends Agent<Env, CodeGenState> {
8081
protected codingAgent: CodingAgentInterface = new CodingAgentInterface(this);
8182

8283
protected deploymentManager!: DeploymentManager;
84+
protected git: GitVersionControl;
8385

8486
private previewUrlCache: string = '';
8587
private templateDetailsCache: TemplateDetails | null = null;
@@ -159,9 +161,12 @@ export class SimpleCodeGeneratorAgent extends Agent<Env, CodeGenState> {
159161
() => this.state,
160162
(s) => this.setState(s)
161163
);
162-
164+
165+
// Initialize GitVersionControl
166+
this.git = new GitVersionControl(this.sql);
167+
163168
// Initialize FileManager
164-
this.fileManager = new FileManager(this.stateManager, () => this.getTemplateDetails());
169+
this.fileManager = new FileManager(this.stateManager, () => this.getTemplateDetails(), this.git);
165170

166171
// Initialize DeploymentManager first (manages sandbox client caching)
167172
// DeploymentManager will use its own getClient() override for caching
@@ -260,6 +265,7 @@ export class SimpleCodeGeneratorAgent extends Agent<Env, CodeGenState> {
260265

261266
async onStart(_props?: Record<string, unknown> | undefined): Promise<void> {
262267
this.logger().info(`Agent ${this.getAgentId()} session: ${this.state.sessionId} onStart`);
268+
await this.git.init();
263269
// Ignore if agent not initialized
264270
if (!this.state.templateName?.trim()) {
265271
this.logger().info(`Agent ${this.getAgentId()} session: ${this.state.sessionId} not initialized, ignoring onStart`);
@@ -554,7 +560,7 @@ export class SimpleCodeGeneratorAgent extends Agent<Env, CodeGenState> {
554560

555561
const readme = await this.operations.implementPhase.generateReadme(this.getOperationOptions());
556562

557-
this.fileManager.saveGeneratedFile(readme);
563+
this.fileManager.saveGeneratedFile(readme, "feat: README.md");
558564

559565
this.broadcast(WebSocketMessageResponses.FILE_GENERATED, {
560566
message: 'README.md generated successfully',
@@ -1138,7 +1144,7 @@ export class SimpleCodeGeneratorAgent extends Agent<Env, CodeGenState> {
11381144
});
11391145

11401146
// Update state with completed phase
1141-
this.fileManager.saveGeneratedFiles(finalFiles);
1147+
this.fileManager.saveGeneratedFiles(finalFiles, `feat: ${phase.name}`);
11421148

11431149
this.logger().info("Files generated for phase:", phase.name, finalFiles.map(f => f.filePath));
11441150

@@ -1303,7 +1309,7 @@ export class SimpleCodeGeneratorAgent extends Agent<Env, CodeGenState> {
13031309
this.getOperationOptions()
13041310
);
13051311

1306-
const fileState = this.fileManager.saveGeneratedFile(result);
1312+
const fileState = this.fileManager.saveGeneratedFile(result, `fix: ${file.filePath}`);
13071313

13081314
this.broadcast(WebSocketMessageResponses.FILE_REGENERATED, {
13091315
message: `Regenerated file: ${file.filePath}`,
@@ -1414,7 +1420,7 @@ export class SimpleCodeGeneratorAgent extends Agent<Env, CodeGenState> {
14141420
}, this.getOperationOptions());
14151421

14161422
if (fastCodeFixer.length > 0) {
1417-
this.fileManager.saveGeneratedFiles(fastCodeFixer);
1423+
this.fileManager.saveGeneratedFiles(fastCodeFixer, "fix: Fast smart code fixes");
14181424
await this.deployToSandbox(fastCodeFixer);
14191425
this.logger().info("Fast smart code fixes applied successfully");
14201426
}
@@ -1486,7 +1492,7 @@ export class SimpleCodeGeneratorAgent extends Agent<Env, CodeGenState> {
14861492
filePurpose: allFiles.find(f => f.filePath === file.filePath)?.filePurpose || '',
14871493
fileContents: file.fileContents
14881494
}));
1489-
this.fileManager.saveGeneratedFiles(fixedFiles);
1495+
this.fileManager.saveGeneratedFiles(fixedFiles, "fix: applied deterministic fixes");
14901496

14911497
await this.deployToSandbox(fixedFiles, false, "fix: applied deterministic fixes");
14921498
this.logger().info("Deployed deterministic fixes to sandbox");
@@ -2017,7 +2023,7 @@ export class SimpleCodeGeneratorAgent extends Agent<Env, CodeGenState> {
20172023
'[cloudflarebutton]',
20182024
prepareCloudflareButton(options.repositoryHtmlUrl, 'markdown')
20192025
);
2020-
this.fileManager.saveGeneratedFile(readmeFile);
2026+
this.fileManager.saveGeneratedFile(readmeFile, "feat: README updated with Cloudflare deploy button");
20212027
this.logger().info('README prepared with Cloudflare deploy button');
20222028

20232029
// Deploy updated README to sandbox so it's visible in preview

worker/agents/git/fs-adapter.ts

Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
/**
2+
* SQLite filesystem adapter for isomorphic-git
3+
* One DO = one Git repo, stored directly in SQLite
4+
*
5+
* Limits:
6+
* - Cloudflare DO SQLite: 10GB total storage
7+
* - Max parameter size: ~1MB per SQL statement parameter
8+
* - Git objects are base64-encoded to safely store binary data
9+
*/
10+
11+
export interface SqlExecutor {
12+
<T = unknown>(query: TemplateStringsArray, ...values: (string | number | boolean | null)[]): T[];
13+
}
14+
15+
// 1MB limit for Cloudflare DO SQL parameters, leave some headroom
16+
const MAX_OBJECT_SIZE = 900 * 1024; // 900KB
17+
18+
export class SqliteFS {
19+
constructor(private sql: SqlExecutor) {}
20+
21+
/**
22+
* Get storage statistics for observability
23+
*/
24+
getStorageStats(): { totalObjects: number; totalBytes: number; largestObject: { path: string; size: number } | null } {
25+
const objects = this.sql<{ path: string; data: string }>`SELECT path, data FROM git_objects`;
26+
27+
if (!objects || objects.length === 0) {
28+
return { totalObjects: 0, totalBytes: 0, largestObject: null };
29+
}
30+
31+
let totalBytes = 0;
32+
let largestObject: { path: string; size: number } | null = null;
33+
34+
for (const obj of objects) {
35+
const size = obj.data.length; // Base64 encoded size
36+
totalBytes += size;
37+
38+
if (!largestObject || size > largestObject.size) {
39+
largestObject = { path: obj.path, size };
40+
}
41+
}
42+
43+
return {
44+
totalObjects: objects.length,
45+
totalBytes,
46+
largestObject
47+
};
48+
}
49+
50+
init(): void {
51+
this.sql`
52+
CREATE TABLE IF NOT EXISTS git_objects (
53+
path TEXT PRIMARY KEY,
54+
data TEXT NOT NULL,
55+
mtime INTEGER NOT NULL
56+
)
57+
`;
58+
59+
// Create index for efficient directory listings
60+
this.sql`CREATE INDEX IF NOT EXISTS idx_git_objects_path ON git_objects(path)`;
61+
}
62+
63+
readFile(path: string, options?: { encoding?: 'utf8' }): Uint8Array | string {
64+
// Normalize path (remove leading slashes)
65+
const normalized = path.replace(/^\/+/, '');
66+
const result = this.sql<{ data: string }>`SELECT data FROM git_objects WHERE path = ${normalized}`;
67+
if (!result[0]) throw new Error(`ENOENT: ${path}`);
68+
69+
const base64Data = result[0].data;
70+
71+
// Decode from base64
72+
const binaryString = atob(base64Data);
73+
const bytes = new Uint8Array(binaryString.length);
74+
for (let i = 0; i < binaryString.length; i++) {
75+
bytes[i] = binaryString.charCodeAt(i);
76+
}
77+
78+
return options?.encoding === 'utf8' ? new TextDecoder().decode(bytes) : bytes;
79+
}
80+
81+
writeFile(path: string, data: Uint8Array | string): void {
82+
// Normalize path (remove leading slashes)
83+
const normalized = path.replace(/^\/+/, '');
84+
85+
// Convert to Uint8Array if string
86+
const bytes = typeof data === 'string' ? new TextEncoder().encode(data) : data;
87+
88+
// Check size limit
89+
if (bytes.length > MAX_OBJECT_SIZE) {
90+
throw new Error(`File too large: ${path} (${bytes.length} bytes, max ${MAX_OBJECT_SIZE})`);
91+
}
92+
93+
// Encode to base64 for safe storage
94+
let binaryString = '';
95+
for (let i = 0; i < bytes.length; i++) {
96+
binaryString += String.fromCharCode(bytes[i]);
97+
}
98+
const base64Content = btoa(binaryString);
99+
100+
this.sql`INSERT OR REPLACE INTO git_objects (path, data, mtime) VALUES (${normalized}, ${base64Content}, ${Date.now()})`;
101+
102+
// Only log if approaching size limit (no overhead for normal files)
103+
if (bytes.length > MAX_OBJECT_SIZE * 0.8) {
104+
console.warn(`[Git Storage] Large file: ${normalized} is ${(bytes.length / 1024).toFixed(1)}KB (limit: ${(MAX_OBJECT_SIZE / 1024).toFixed(1)}KB)`);
105+
}
106+
}
107+
108+
unlink(path: string): void {
109+
// Normalize path (remove leading slashes)
110+
const normalized = path.replace(/^\/+/, '');
111+
this.sql`DELETE FROM git_objects WHERE path = ${normalized}`;
112+
}
113+
114+
readdir(path: string): string[] {
115+
// Normalize path (remove leading/trailing slashes)
116+
const normalized = path.replace(/^\/+|\/+$/g, '');
117+
118+
let result;
119+
if (normalized === '') {
120+
// Root directory - get all paths
121+
result = this.sql<{ path: string }>`SELECT path FROM git_objects`;
122+
} else {
123+
// Subdirectory - match prefix
124+
result = this.sql<{ path: string }>`SELECT path FROM git_objects WHERE path LIKE ${normalized + '/%'}`;
125+
}
126+
127+
if (!result || result.length === 0) return [];
128+
129+
const children = new Set<string>();
130+
const prefixLen = normalized ? normalized.length + 1 : 0;
131+
132+
for (const row of result) {
133+
const relativePath = normalized ? row.path.substring(prefixLen) : row.path;
134+
const first = relativePath.split('/')[0];
135+
if (first) children.add(first);
136+
}
137+
138+
return Array.from(children);
139+
}
140+
141+
mkdir(_path: string): void {
142+
// No-op: directories are implicit in Git
143+
}
144+
145+
rmdir(path: string): void {
146+
// Normalize path (remove leading/trailing slashes)
147+
const normalized = path.replace(/^\/+|\/+$/g, '');
148+
this.sql`DELETE FROM git_objects WHERE path LIKE ${normalized + '%'}`;
149+
}
150+
151+
stat(path: string): { type: 'file' | 'dir'; mode: number; size: number; mtimeMs: number } {
152+
// Normalize path (remove leading slashes)
153+
const normalized = path.replace(/^\/+/, '');
154+
const result = this.sql<{ data: string; mtime: number }>`SELECT data, mtime FROM git_objects WHERE path = ${normalized}`;
155+
if (!result[0]) throw new Error(`ENOENT: ${path}`);
156+
157+
const row = result[0];
158+
return { type: 'file', mode: 0o100644, size: row.data.length, mtimeMs: row.mtime };
159+
}
160+
161+
lstat(path: string) {
162+
return this.stat(path);
163+
}
164+
165+
symlink(target: string, path: string): void {
166+
this.writeFile(path, target);
167+
}
168+
169+
readlink(path: string): string {
170+
return this.readFile(path, { encoding: 'utf8' }) as string;
171+
}
172+
}

0 commit comments

Comments
 (0)