naiad-cli 0.2.7 → 0.2.8

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.
@@ -0,0 +1,240 @@
1
+ import { readFileSync, readdirSync, unlinkSync, rmSync, statSync, openSync, readSync, closeSync, createReadStream } from "fs";
2
+ import { createHash } from "crypto";
3
+ const THROTTLE_MS = 30_000;
4
+ const JSONL_MAX_BYTES = 200 * 1024 * 1024;
5
+ const MARKER_LINE = '{"type":"system","message":"[Naiad] Session log truncated — earlier entries exceeded 200MB limit."}\n';
6
+ export class CheckpointUploader {
7
+ client;
8
+ sessionId;
9
+ sessionDir;
10
+ attempt;
11
+ lastUploadAt = 0;
12
+ pendingCheckpoint = null;
13
+ constructor(client, sessionId, sessionDir, attempt) {
14
+ this.client = client;
15
+ this.sessionId = sessionId;
16
+ this.sessionDir = sessionDir;
17
+ this.attempt = attempt;
18
+ }
19
+ async handleCheckpoint(msg, forceUpload = false) {
20
+ this.pendingCheckpoint = msg;
21
+ const now = Date.now();
22
+ if (!forceUpload && now - this.lastUploadAt < THROTTLE_MS) {
23
+ return;
24
+ }
25
+ await this.uploadCheckpoint();
26
+ }
27
+ async flushOnExit() {
28
+ if (this.pendingCheckpoint) {
29
+ await this.uploadCheckpoint();
30
+ }
31
+ }
32
+ async uploadCheckpoint() {
33
+ const msg = this.pendingCheckpoint;
34
+ if (!msg)
35
+ return;
36
+ this.pendingCheckpoint = null;
37
+ try {
38
+ const policies = await this.client.requestCheckpointPolicies(this.sessionId);
39
+ const skippedArtifacts = [...msg.skippedArtifacts];
40
+ const uploadedKeys = {};
41
+ // Incremental checksum: UTF8(base_sha) + 0x0A + UTF8(head_sha)
42
+ const hash = createHash("sha256");
43
+ hash.update(msg.base_sha, "utf8");
44
+ hash.update("\n");
45
+ hash.update(msg.head_sha, "utf8");
46
+ // Upload patch
47
+ if (msg.patchPath && !msg.skippedArtifacts.includes("patch")) {
48
+ const patchData = readFileSync(msg.patchPath);
49
+ hash.update("\n");
50
+ hash.update(patchData);
51
+ await this.uploadArtifact(policies.artifacts["patch.bin"], patchData);
52
+ uploadedKeys.patch_key = policies.artifacts["patch.bin"].fields["key"];
53
+ }
54
+ // Upload untracked
55
+ if (msg.untrackedPath && !msg.skippedArtifacts.includes("untracked")) {
56
+ hash.update("\n");
57
+ const untrackedData = await this.readAndHash(msg.untrackedPath, hash);
58
+ await this.uploadArtifact(policies.artifacts["untracked.tar.gz"], untrackedData);
59
+ uploadedKeys.untracked_key = policies.artifacts["untracked.tar.gz"].fields["key"];
60
+ }
61
+ // Upload session JSONL (possibly truncated)
62
+ const sessionJsonl = this.findSessionJsonl();
63
+ if (sessionJsonl) {
64
+ const sessionResult = this.prepareSessionJsonl(sessionJsonl);
65
+ if (sessionResult === null) {
66
+ skippedArtifacts.push("session");
67
+ }
68
+ else if (sessionResult === "stream") {
69
+ hash.update("\n");
70
+ const sessionData = await this.readAndHash(sessionJsonl, hash);
71
+ await this.uploadArtifact(policies.artifacts["session.jsonl"], sessionData);
72
+ uploadedKeys.session_key = policies.artifacts["session.jsonl"].fields["key"];
73
+ }
74
+ else {
75
+ hash.update("\n");
76
+ hash.update(sessionResult);
77
+ await this.uploadArtifact(policies.artifacts["session.jsonl"], sessionResult);
78
+ uploadedKeys.session_key = policies.artifacts["session.jsonl"].fields["key"];
79
+ }
80
+ }
81
+ else {
82
+ skippedArtifacts.push("session");
83
+ }
84
+ const checksum = "sha256:" + hash.digest("hex");
85
+ // Build manifest
86
+ const manifest = {
87
+ version: 1,
88
+ attempt: this.attempt,
89
+ base_sha: msg.base_sha,
90
+ head_sha: msg.head_sha,
91
+ checksum,
92
+ patch_key: uploadedKeys.patch_key ?? null,
93
+ patch_bytes: msg.patchBytes,
94
+ untracked_key: uploadedKeys.untracked_key ?? null,
95
+ untracked_bytes: msg.untrackedPath ? statSync(msg.untrackedPath).size : 0,
96
+ untracked_files: msg.untrackedFiles,
97
+ session_key: uploadedKeys.session_key ?? null,
98
+ session_bytes: sessionJsonl ? statSync(sessionJsonl).size : 0,
99
+ timestamp: new Date().toISOString(),
100
+ skipped_artifacts: skippedArtifacts,
101
+ };
102
+ // Upload manifest LAST
103
+ const manifestData = Buffer.from(JSON.stringify(manifest, null, 2));
104
+ await this.uploadArtifact(policies.artifacts["manifest.json"], manifestData);
105
+ // Update session metadata
106
+ const patchUpdate = {};
107
+ if (uploadedKeys.patch_key)
108
+ patchUpdate.git_patch_key = uploadedKeys.patch_key;
109
+ if (uploadedKeys.session_key)
110
+ patchUpdate.session_jsonl_key = uploadedKeys.session_key;
111
+ if (Object.keys(patchUpdate).length > 0) {
112
+ await this.client.patchSession(this.sessionId, patchUpdate);
113
+ }
114
+ this.lastUploadAt = Date.now();
115
+ // Clean up temp files
116
+ this.cleanupTempFiles(msg);
117
+ }
118
+ catch (err) {
119
+ console.error("[checkpoint] Upload failed:", err.message);
120
+ // Re-queue for next attempt
121
+ if (!this.pendingCheckpoint) {
122
+ this.pendingCheckpoint = msg;
123
+ }
124
+ }
125
+ }
126
+ async uploadArtifact(policy, data) {
127
+ const formData = new FormData();
128
+ for (const [key, value] of Object.entries(policy.fields)) {
129
+ formData.append(key, value);
130
+ }
131
+ formData.append("file", new Blob([new Uint8Array(data)]));
132
+ const res = await fetch(policy.url, {
133
+ method: "POST",
134
+ body: formData,
135
+ });
136
+ if (!res.ok && res.status !== 204) {
137
+ const text = await res.text().catch(() => "");
138
+ throw new Error(`Artifact upload failed (${res.status}): ${text}`);
139
+ }
140
+ }
141
+ findSessionJsonl() {
142
+ try {
143
+ const files = readdirSync(this.sessionDir).filter((f) => f.endsWith(".jsonl"));
144
+ if (files.length === 0)
145
+ return null;
146
+ // Use the most recently modified
147
+ let best = null;
148
+ let bestMtime = 0;
149
+ for (const f of files) {
150
+ const full = `${this.sessionDir}/${f}`;
151
+ const st = statSync(full);
152
+ if (st.mtimeMs > bestMtime) {
153
+ bestMtime = st.mtimeMs;
154
+ best = full;
155
+ }
156
+ }
157
+ return best;
158
+ }
159
+ catch {
160
+ return null;
161
+ }
162
+ }
163
+ prepareSessionJsonl(filePath) {
164
+ try {
165
+ const st = statSync(filePath);
166
+ if (st.size === 0)
167
+ return null;
168
+ if (st.size <= JSONL_MAX_BYTES) {
169
+ return "stream";
170
+ }
171
+ // Truncation: keep last ~200MB on newline boundary
172
+ const markerBytes = Buffer.byteLength(MARKER_LINE, "utf8");
173
+ const maxPayload = JSONL_MAX_BYTES - markerBytes;
174
+ const seekPos = st.size - maxPayload;
175
+ const fd = openSync(filePath, "r");
176
+ try {
177
+ // Read a small chunk to find the next newline
178
+ const scanBuf = Buffer.alloc(Math.min(1024 * 1024, maxPayload));
179
+ const bytesRead = readSync(fd, scanBuf, 0, scanBuf.length, seekPos);
180
+ let newlineOffset = -1;
181
+ for (let i = 0; i < bytesRead; i++) {
182
+ if (scanBuf[i] === 0x0a) {
183
+ newlineOffset = i;
184
+ break;
185
+ }
186
+ }
187
+ const startPos = newlineOffset >= 0 ? seekPos + newlineOffset + 1 : seekPos + scanBuf.length;
188
+ const tailSize = st.size - startPos;
189
+ const tailBuf = Buffer.alloc(tailSize);
190
+ readSync(fd, tailBuf, 0, tailSize, startPos);
191
+ const marker = Buffer.from(MARKER_LINE, "utf8");
192
+ return Buffer.concat([marker, tailBuf]);
193
+ }
194
+ finally {
195
+ closeSync(fd);
196
+ }
197
+ }
198
+ catch (err) {
199
+ console.error("[checkpoint] Failed to read session JSONL:", err.message);
200
+ return null;
201
+ }
202
+ }
203
+ readAndHash(filePath, hash) {
204
+ return new Promise((resolve, reject) => {
205
+ const chunks = [];
206
+ const stream = createReadStream(filePath);
207
+ stream.on("data", (chunk) => {
208
+ if (typeof chunk === "string")
209
+ chunk = Buffer.from(chunk);
210
+ hash.update(chunk);
211
+ chunks.push(chunk);
212
+ });
213
+ stream.on("end", () => resolve(Buffer.concat(chunks)));
214
+ stream.on("error", reject);
215
+ });
216
+ }
217
+ cleanupTempFiles(msg) {
218
+ try {
219
+ if (msg.patchPath)
220
+ unlinkSync(msg.patchPath);
221
+ }
222
+ catch { }
223
+ try {
224
+ if (msg.untrackedPath)
225
+ unlinkSync(msg.untrackedPath);
226
+ }
227
+ catch { }
228
+ // Try to remove the temp directory
229
+ if (msg.patchPath || msg.untrackedPath) {
230
+ const dir = msg.patchPath
231
+ ? msg.patchPath.replace(/\/[^/]+$/, "")
232
+ : msg.untrackedPath.replace(/\/[^/]+$/, "");
233
+ try {
234
+ rmSync(dir, { recursive: true, force: true });
235
+ }
236
+ catch { }
237
+ }
238
+ }
239
+ }
240
+ //# sourceMappingURL=checkpoint.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"checkpoint.js","sourceRoot":"","sources":["../../src/sync/checkpoint.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,EAAE,QAAQ,EAAE,QAAQ,EAAE,QAAQ,EAAE,SAAS,EAAE,gBAAgB,EAAE,MAAM,IAAI,CAAC;AAC9H,OAAO,EAAE,UAAU,EAAa,MAAM,QAAQ,CAAC;AAG/C,MAAM,WAAW,GAAG,MAAM,CAAC;AAC3B,MAAM,eAAe,GAAG,GAAG,GAAG,IAAI,GAAG,IAAI,CAAC;AAC1C,MAAM,WAAW,GAAG,uGAAuG,CAAC;AAc5H,MAAM,OAAO,kBAAkB;IACrB,MAAM,CAAY;IAClB,SAAS,CAAS;IAClB,UAAU,CAAS;IACnB,OAAO,CAAS;IAChB,YAAY,GAAG,CAAC,CAAC;IACjB,iBAAiB,GAA6B,IAAI,CAAC;IAE3D,YAAY,MAAiB,EAAE,SAAiB,EAAE,UAAkB,EAAE,OAAe;QACnF,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC;QACrB,IAAI,CAAC,SAAS,GAAG,SAAS,CAAC;QAC3B,IAAI,CAAC,UAAU,GAAG,UAAU,CAAC;QAC7B,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC;IACzB,CAAC;IAED,KAAK,CAAC,gBAAgB,CAAC,GAAsB,EAAE,WAAW,GAAG,KAAK;QAChE,IAAI,CAAC,iBAAiB,GAAG,GAAG,CAAC;QAE7B,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;QACvB,IAAI,CAAC,WAAW,IAAI,GAAG,GAAG,IAAI,CAAC,YAAY,GAAG,WAAW,EAAE,CAAC;YAC1D,OAAO;QACT,CAAC;QAED,MAAM,IAAI,CAAC,gBAAgB,EAAE,CAAC;IAChC,CAAC;IAED,KAAK,CAAC,WAAW;QACf,IAAI,IAAI,CAAC,iBAAiB,EAAE,CAAC;YAC3B,MAAM,IAAI,CAAC,gBAAgB,EAAE,CAAC;QAChC,CAAC;IACH,CAAC;IAEO,KAAK,CAAC,gBAAgB;QAC5B,MAAM,GAAG,GAAG,IAAI,CAAC,iBAAiB,CAAC;QACnC,IAAI,CAAC,GAAG;YAAE,OAAO;QACjB,IAAI,CAAC,iBAAiB,GAAG,IAAI,CAAC;QAE9B,IAAI,CAAC;YACH,MAAM,QAAQ,GAAG,MAAM,IAAI,CAAC,MAAM,CAAC,yBAAyB,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;YAE7E,MAAM,gBAAgB,GAAG,CAAC,GAAG,GAAG,CAAC,gBAAgB,CAAC,CAAC;YACnD,MAAM,YAAY,GAAyE,EAAE,CAAC;YAE9F,+DAA+D;YAC/D,MAAM,IAAI,GAAG,UAAU,CAAC,QAAQ,CAAC,CAAC;YAClC,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC;YAClC,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;YAClB,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC;YAElC,eAAe;YACf,IAAI,GAAG,CAAC,SAAS,IAAI,CAAC,GAAG,CAAC,gBAAgB,CAAC,QAAQ,CAAC,OAAO,CAAC,EAAE,CAAC;gBAC7D,MAAM,SAAS,GAAG,YAAY,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC;gBAC9C,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;gBAClB,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC;gBACvB,MAAM,IAAI,CAAC,cAAc,CAAC,QAAQ,CAAC,SAAS,CAAC,WAAW,CAAC,EAAE,SAAS,CAAC,CAAC;gBACtE,YAAY,CAAC,SAAS,GAAG,QAAQ,CAAC,SAAS,CAAC,WAAW,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;YACzE,CAAC;YAED,mBAAmB;YACnB,IAAI,GAAG,CAAC,aAAa,IAAI,CAAC,GAAG,CAAC,gBAAgB,CAAC,QAAQ,CAAC,WAAW,CAAC,EAAE,CAAC;gBACrE,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;gBAClB,MAAM,aAAa,GAAG,MAAM,IAAI,CAAC,WAAW,CAAC,GAAG,CAAC,aAAa,EAAE,IAAI,CAAC,CAAC;gBACtE,MAAM,IAAI,CAAC,cAAc,CAAC,QAAQ,CAAC,SAAS,CAAC,kBAAkB,CAAC,EAAE,aAAa,CAAC,CAAC;gBACjF,YAAY,CAAC,aAAa,GAAG,QAAQ,CAAC,SAAS,CAAC,kBAAkB,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;YACpF,CAAC;YAED,4CAA4C;YAC5C,MAAM,YAAY,GAAG,IAAI,CAAC,gBAAgB,EAAE,CAAC;YAC7C,IAAI,YAAY,EAAE,CAAC;gBACjB,MAAM,aAAa,GAAG,IAAI,CAAC,mBAAmB,CAAC,YAAY,CAAC,CAAC;gBAC7D,IAAI,aAAa,KAAK,IAAI,EAAE,CAAC;oBAC3B,gBAAgB,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;gBACnC,CAAC;qBAAM,IAAI,aAAa,KAAK,QAAQ,EAAE,CAAC;oBACtC,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;oBAClB,MAAM,WAAW,GAAG,MAAM,IAAI,CAAC,WAAW,CAAC,YAAY,EAAE,IAAI,CAAC,CAAC;oBAC/D,MAAM,IAAI,CAAC,cAAc,CAAC,QAAQ,CAAC,SAAS,CAAC,eAAe,CAAC,EAAE,WAAW,CAAC,CAAC;oBAC5E,YAAY,CAAC,WAAW,GAAG,QAAQ,CAAC,SAAS,CAAC,eAAe,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;gBAC/E,CAAC;qBAAM,CAAC;oBACN,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;oBAClB,IAAI,CAAC,MAAM,CAAC,aAAa,CAAC,CAAC;oBAC3B,MAAM,IAAI,CAAC,cAAc,CAAC,QAAQ,CAAC,SAAS,CAAC,eAAe,CAAC,EAAE,aAAa,CAAC,CAAC;oBAC9E,YAAY,CAAC,WAAW,GAAG,QAAQ,CAAC,SAAS,CAAC,eAAe,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;gBAC/E,CAAC;YACH,CAAC;iBAAM,CAAC;gBACN,gBAAgB,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;YACnC,CAAC;YAED,MAAM,QAAQ,GAAG,SAAS,GAAG,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;YAEhD,iBAAiB;YACjB,MAAM,QAAQ,GAAG;gBACf,OAAO,EAAE,CAAC;gBACV,OAAO,EAAE,IAAI,CAAC,OAAO;gBACrB,QAAQ,EAAE,GAAG,CAAC,QAAQ;gBACtB,QAAQ,EAAE,GAAG,CAAC,QAAQ;gBACtB,QAAQ;gBACR,SAAS,EAAE,YAAY,CAAC,SAAS,IAAI,IAAI;gBACzC,WAAW,EAAE,GAAG,CAAC,UAAU;gBAC3B,aAAa,EAAE,YAAY,CAAC,aAAa,IAAI,IAAI;gBACjD,eAAe,EAAE,GAAG,CAAC,aAAa,CAAC,CAAC,CAAC,QAAQ,CAAC,GAAG,CAAC,aAAa,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;gBACzE,eAAe,EAAE,GAAG,CAAC,cAAc;gBACnC,WAAW,EAAE,YAAY,CAAC,WAAW,IAAI,IAAI;gBAC7C,aAAa,EAAE,YAAY,CAAC,CAAC,CAAC,QAAQ,CAAC,YAAY,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;gBAC7D,SAAS,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE;gBACnC,iBAAiB,EAAE,gBAAgB;aACpC,CAAC;YAEF,uBAAuB;YACvB,MAAM,YAAY,GAAG,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,SAAS,CAAC,QAAQ,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC;YACpE,MAAM,IAAI,CAAC,cAAc,CAAC,QAAQ,CAAC,SAAS,CAAC,eAAe,CAAC,EAAE,YAAY,CAAC,CAAC;YAE7E,0BAA0B;YAC1B,MAAM,WAAW,GAA2B,EAAE,CAAC;YAC/C,IAAI,YAAY,CAAC,SAAS;gBAAE,WAAW,CAAC,aAAa,GAAG,YAAY,CAAC,SAAS,CAAC;YAC/E,IAAI,YAAY,CAAC,WAAW;gBAAE,WAAW,CAAC,iBAAiB,GAAG,YAAY,CAAC,WAAW,CAAC;YACvF,IAAI,MAAM,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;gBACxC,MAAM,IAAI,CAAC,MAAM,CAAC,YAAY,CAAC,IAAI,CAAC,SAAS,EAAE,WAAW,CAAC,CAAC;YAC9D,CAAC;YAED,IAAI,CAAC,YAAY,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;YAE/B,sBAAsB;YACtB,IAAI,CAAC,gBAAgB,CAAC,GAAG,CAAC,CAAC;QAC7B,CAAC;QAAC,OAAO,GAAQ,EAAE,CAAC;YAClB,OAAO,CAAC,KAAK,CAAC,6BAA6B,EAAE,GAAG,CAAC,OAAO,CAAC,CAAC;YAC1D,4BAA4B;YAC5B,IAAI,CAAC,IAAI,CAAC,iBAAiB,EAAE,CAAC;gBAC5B,IAAI,CAAC,iBAAiB,GAAG,GAAG,CAAC;YAC/B,CAAC;QACH,CAAC;IACH,CAAC;IAEO,KAAK,CAAC,cAAc,CAAC,MAAsB,EAAE,IAAY;QAC/D,MAAM,QAAQ,GAAG,IAAI,QAAQ,EAAE,CAAC;QAChC,KAAK,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,MAAM,CAAC,EAAE,CAAC;YACzD,QAAQ,CAAC,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC;QAC9B,CAAC;QACD,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,IAAI,IAAI,CAAC,CAAC,IAAI,UAAU,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC;QAE1D,MAAM,GAAG,GAAG,MAAM,KAAK,CAAC,MAAM,CAAC,GAAG,EAAE;YAClC,MAAM,EAAE,MAAM;YACd,IAAI,EAAE,QAAQ;SACf,CAAC,CAAC;QACH,IAAI,CAAC,GAAG,CAAC,EAAE,IAAI,GAAG,CAAC,MAAM,KAAK,GAAG,EAAE,CAAC;YAClC,MAAM,IAAI,GAAG,MAAM,GAAG,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,EAAE,CAAC,CAAC;YAC9C,MAAM,IAAI,KAAK,CAAC,2BAA2B,GAAG,CAAC,MAAM,MAAM,IAAI,EAAE,CAAC,CAAC;QACrE,CAAC;IACH,CAAC;IAEO,gBAAgB;QACtB,IAAI,CAAC;YACH,MAAM,KAAK,GAAG,WAAW,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC,CAAC;YAC/E,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC;gBAAE,OAAO,IAAI,CAAC;YACpC,iCAAiC;YACjC,IAAI,IAAI,GAAkB,IAAI,CAAC;YAC/B,IAAI,SAAS,GAAG,CAAC,CAAC;YAClB,KAAK,MAAM,CAAC,IAAI,KAAK,EAAE,CAAC;gBACtB,MAAM,IAAI,GAAG,GAAG,IAAI,CAAC,UAAU,IAAI,CAAC,EAAE,CAAC;gBACvC,MAAM,EAAE,GAAG,QAAQ,CAAC,IAAI,CAAC,CAAC;gBAC1B,IAAI,EAAE,CAAC,OAAO,GAAG,SAAS,EAAE,CAAC;oBAC3B,SAAS,GAAG,EAAE,CAAC,OAAO,CAAC;oBACvB,IAAI,GAAG,IAAI,CAAC;gBACd,CAAC;YACH,CAAC;YACD,OAAO,IAAI,CAAC;QACd,CAAC;QAAC,MAAM,CAAC;YACP,OAAO,IAAI,CAAC;QACd,CAAC;IACH,CAAC;IAEO,mBAAmB,CAAC,QAAgB;QAC1C,IAAI,CAAC;YACH,MAAM,EAAE,GAAG,QAAQ,CAAC,QAAQ,CAAC,CAAC;YAC9B,IAAI,EAAE,CAAC,IAAI,KAAK,CAAC;gBAAE,OAAO,IAAI,CAAC;YAE/B,IAAI,EAAE,CAAC,IAAI,IAAI,eAAe,EAAE,CAAC;gBAC/B,OAAO,QAAQ,CAAC;YAClB,CAAC;YAED,mDAAmD;YACnD,MAAM,WAAW,GAAG,MAAM,CAAC,UAAU,CAAC,WAAW,EAAE,MAAM,CAAC,CAAC;YAC3D,MAAM,UAAU,GAAG,eAAe,GAAG,WAAW,CAAC;YACjD,MAAM,OAAO,GAAG,EAAE,CAAC,IAAI,GAAG,UAAU,CAAC;YAErC,MAAM,EAAE,GAAG,QAAQ,CAAC,QAAQ,EAAE,GAAG,CAAC,CAAC;YACnC,IAAI,CAAC;gBACH,8CAA8C;gBAC9C,MAAM,OAAO,GAAG,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,GAAG,IAAI,EAAE,UAAU,CAAC,CAAC,CAAC;gBAChE,MAAM,SAAS,GAAG,QAAQ,CAAC,EAAE,EAAE,OAAO,EAAE,CAAC,EAAE,OAAO,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;gBAEpE,IAAI,aAAa,GAAG,CAAC,CAAC,CAAC;gBACvB,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,SAAS,EAAE,CAAC,EAAE,EAAE,CAAC;oBACnC,IAAI,OAAO,CAAC,CAAC,CAAC,KAAK,IAAI,EAAE,CAAC;wBACxB,aAAa,GAAG,CAAC,CAAC;wBAClB,MAAM;oBACR,CAAC;gBACH,CAAC;gBAED,MAAM,QAAQ,GAAG,aAAa,IAAI,CAAC,CAAC,CAAC,CAAC,OAAO,GAAG,aAAa,GAAG,CAAC,CAAC,CAAC,CAAC,OAAO,GAAG,OAAO,CAAC,MAAM,CAAC;gBAC7F,MAAM,QAAQ,GAAG,EAAE,CAAC,IAAI,GAAG,QAAQ,CAAC;gBACpC,MAAM,OAAO,GAAG,MAAM,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC;gBACvC,QAAQ,CAAC,EAAE,EAAE,OAAO,EAAE,CAAC,EAAE,QAAQ,EAAE,QAAQ,CAAC,CAAC;gBAE7C,MAAM,MAAM,GAAG,MAAM,CAAC,IAAI,CAAC,WAAW,EAAE,MAAM,CAAC,CAAC;gBAChD,OAAO,MAAM,CAAC,MAAM,CAAC,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;YAC1C,CAAC;oBAAS,CAAC;gBACT,SAAS,CAAC,EAAE,CAAC,CAAC;YAChB,CAAC;QACH,CAAC;QAAC,OAAO,GAAQ,EAAE,CAAC;YAClB,OAAO,CAAC,KAAK,CAAC,4CAA4C,EAAE,GAAG,CAAC,OAAO,CAAC,CAAC;YACzE,OAAO,IAAI,CAAC;QACd,CAAC;IACH,CAAC;IAEO,WAAW,CAAC,QAAgB,EAAE,IAAU;QAC9C,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;YACrC,MAAM,MAAM,GAAa,EAAE,CAAC;YAC5B,MAAM,MAAM,GAAG,gBAAgB,CAAC,QAAQ,CAAC,CAAC;YAC1C,MAAM,CAAC,EAAE,CAAC,MAAM,EAAE,CAAC,KAAsB,EAAE,EAAE;gBAC3C,IAAI,OAAO,KAAK,KAAK,QAAQ;oBAAE,KAAK,GAAG,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;gBAC1D,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;gBACnB,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;YACrB,CAAC,CAAC,CAAC;YACH,MAAM,CAAC,EAAE,CAAC,KAAK,EAAE,GAAG,EAAE,CAAC,OAAO,CAAC,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC;YACvD,MAAM,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC;QAC7B,CAAC,CAAC,CAAC;IACL,CAAC;IAEO,gBAAgB,CAAC,GAAsB;QAC7C,IAAI,CAAC;YACH,IAAI,GAAG,CAAC,SAAS;gBAAE,UAAU,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC;QAC/C,CAAC;QAAC,MAAM,CAAC,CAAA,CAAC;QACV,IAAI,CAAC;YACH,IAAI,GAAG,CAAC,aAAa;gBAAE,UAAU,CAAC,GAAG,CAAC,aAAa,CAAC,CAAC;QACvD,CAAC;QAAC,MAAM,CAAC,CAAA,CAAC;QACV,mCAAmC;QACnC,IAAI,GAAG,CAAC,SAAS,IAAI,GAAG,CAAC,aAAa,EAAE,CAAC;YACvC,MAAM,GAAG,GAAG,GAAG,CAAC,SAAS;gBACvB,CAAC,CAAC,GAAG,CAAC,SAAS,CAAC,OAAO,CAAC,UAAU,EAAE,EAAE,CAAC;gBACvC,CAAC,CAAC,GAAG,CAAC,aAAc,CAAC,OAAO,CAAC,UAAU,EAAE,EAAE,CAAC,CAAC;YAC/C,IAAI,CAAC;gBACH,MAAM,CAAC,GAAG,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;YAChD,CAAC;YAAC,MAAM,CAAC,CAAA,CAAC;QACZ,CAAC;IACH,CAAC;CACF"}
@@ -0,0 +1,25 @@
1
+ import type { APIClient } from "../api/client.js";
2
+ import type { ChildProcess } from "child_process";
3
+ export interface CommandListenerOptions {
4
+ client: APIClient;
5
+ sessionId: string;
6
+ sessionDir: string;
7
+ piProcess: ChildProcess;
8
+ onJsonlUpload?: () => void;
9
+ }
10
+ export declare class CommandListener {
11
+ private client;
12
+ private sessionId;
13
+ private sessionDir;
14
+ private piProcess;
15
+ private onJsonlUpload?;
16
+ private abortController;
17
+ private lastAckedSeq;
18
+ private running;
19
+ constructor(opts: CommandListenerOptions);
20
+ start(): void;
21
+ stop(): void;
22
+ private connectLoop;
23
+ private streamCommands;
24
+ private handleCommand;
25
+ }
@@ -0,0 +1,152 @@
1
+ import { writeFileSync, openSync, closeSync, fsyncSync } from "fs";
2
+ import { join } from "path";
3
+ export class CommandListener {
4
+ client;
5
+ sessionId;
6
+ sessionDir;
7
+ piProcess;
8
+ onJsonlUpload;
9
+ abortController = null;
10
+ lastAckedSeq = 0;
11
+ running = false;
12
+ constructor(opts) {
13
+ this.client = opts.client;
14
+ this.sessionId = opts.sessionId;
15
+ this.sessionDir = opts.sessionDir;
16
+ this.piProcess = opts.piProcess;
17
+ this.onJsonlUpload = opts.onJsonlUpload;
18
+ }
19
+ start() {
20
+ if (this.running)
21
+ return;
22
+ this.running = true;
23
+ this.connectLoop();
24
+ }
25
+ stop() {
26
+ this.running = false;
27
+ this.abortController?.abort();
28
+ this.abortController = null;
29
+ }
30
+ async connectLoop() {
31
+ while (this.running) {
32
+ try {
33
+ await this.streamCommands();
34
+ }
35
+ catch (err) {
36
+ if (!this.running)
37
+ return;
38
+ console.error("[command-listener] Connection lost, reconnecting in 3s:", err);
39
+ await sleep(3000);
40
+ }
41
+ }
42
+ }
43
+ async streamCommands() {
44
+ this.abortController = new AbortController();
45
+ const url = this.client.getCommandStreamUrl(this.sessionId, this.lastAckedSeq);
46
+ const res = await fetch(url, {
47
+ headers: {
48
+ ...this.client.getAuthHeaders(),
49
+ Accept: "text/event-stream",
50
+ },
51
+ signal: this.abortController.signal,
52
+ });
53
+ if (!res.ok) {
54
+ throw new Error(`Command stream failed: ${res.status}`);
55
+ }
56
+ const reader = res.body?.getReader();
57
+ if (!reader)
58
+ throw new Error("No response body");
59
+ const decoder = new TextDecoder();
60
+ let buffer = "";
61
+ while (this.running) {
62
+ const { done, value } = await reader.read();
63
+ if (done)
64
+ break;
65
+ buffer += decoder.decode(value, { stream: true });
66
+ const lines = buffer.split("\n");
67
+ buffer = lines.pop() ?? "";
68
+ let currentEvent = "";
69
+ let currentData = "";
70
+ let currentId = "";
71
+ for (const line of lines) {
72
+ if (line.startsWith("event: ")) {
73
+ currentEvent = line.slice(7).trim();
74
+ }
75
+ else if (line.startsWith("data: ")) {
76
+ currentData = line.slice(6);
77
+ }
78
+ else if (line.startsWith("id: ")) {
79
+ currentId = line.slice(4).trim();
80
+ }
81
+ else if (line === "" && currentEvent) {
82
+ // End of SSE message
83
+ if (currentEvent === "session_ended") {
84
+ console.error("[command-listener] Session ended, stopping");
85
+ this.stop();
86
+ return;
87
+ }
88
+ if (currentEvent === "user_message" && currentData) {
89
+ try {
90
+ const parsed = JSON.parse(currentData);
91
+ await this.handleCommand(parsed.id, parsed.text, parseInt(currentId, 10));
92
+ }
93
+ catch (err) {
94
+ console.error("[command-listener] Failed to handle command:", err);
95
+ }
96
+ }
97
+ currentEvent = "";
98
+ currentData = "";
99
+ currentId = "";
100
+ }
101
+ }
102
+ }
103
+ }
104
+ async handleCommand(commandId, text, seq) {
105
+ // Step a: Append to local JSONL (fsync)
106
+ const jsonlPath = join(this.sessionDir, "injected-messages.jsonl");
107
+ const entry = JSON.stringify({
108
+ type: "user_injected_message",
109
+ text,
110
+ command_id: commandId,
111
+ seq,
112
+ timestamp: new Date().toISOString(),
113
+ }) + "\n";
114
+ const fd = openSync(jsonlPath, "a");
115
+ try {
116
+ writeFileSync(fd, entry);
117
+ fsyncSync(fd);
118
+ }
119
+ finally {
120
+ closeSync(fd);
121
+ }
122
+ // Step b: Trigger JSONL upload (best-effort, non-blocking)
123
+ if (this.onJsonlUpload) {
124
+ try {
125
+ this.onJsonlUpload();
126
+ }
127
+ catch {
128
+ // best-effort
129
+ }
130
+ }
131
+ // Step c: Deliver to pi via follow_up on stdin
132
+ const followUp = JSON.stringify({ type: "follow_up", message: text });
133
+ try {
134
+ this.piProcess.stdin?.write(followUp + "\n");
135
+ }
136
+ catch (err) {
137
+ console.error("[command-listener] Failed to write to pi stdin:", err);
138
+ }
139
+ // Step d: Ack only after steps a-c
140
+ try {
141
+ await this.client.ackCommand(this.sessionId, commandId);
142
+ this.lastAckedSeq = seq;
143
+ }
144
+ catch (err) {
145
+ console.error("[command-listener] Failed to ack command:", err);
146
+ }
147
+ }
148
+ }
149
+ function sleep(ms) {
150
+ return new Promise((resolve) => setTimeout(resolve, ms));
151
+ }
152
+ //# sourceMappingURL=command-listener.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"command-listener.js","sourceRoot":"","sources":["../../src/sync/command-listener.ts"],"names":[],"mappings":"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"}
@@ -8,6 +8,10 @@ import {
8
8
  type ToolCall,
9
9
  createAssistantMessageEventStream,
10
10
  } from "@mariozechner/pi-ai";
11
+ import { execSync, execFileSync } from "child_process";
12
+ import * as fs from "fs";
13
+ import * as path from "path";
14
+ import * as os from "os";
11
15
 
12
16
  interface NaiadModel {
13
17
  id: string;
@@ -27,6 +31,115 @@ export default function (pi: ExtensionAPI) {
27
31
  return;
28
32
  }
29
33
 
34
+ // --- Git checkpoint on turn_end ---
35
+ const callbackUrl = process.env.NAIAD_CALLBACK_URL;
36
+ const reporter = {
37
+ send(type: string, data: unknown) {
38
+ if (callbackUrl) {
39
+ fetch(callbackUrl, {
40
+ method: "POST",
41
+ headers: { "Content-Type": "application/json" },
42
+ body: JSON.stringify({ type, data }),
43
+ }).catch(() => {});
44
+ } else if (process.send) {
45
+ process.send({ type, data });
46
+ }
47
+ },
48
+ };
49
+
50
+ if (typeof (pi as any).on === "function") {
51
+ (pi as any).on("turn_end", () => {
52
+ try {
53
+ const baseSha = process.env.NAIAD_BASE_SHA;
54
+ if (!baseSha) return;
55
+
56
+ const headSha = execFileSync("git", ["rev-parse", "HEAD"]).toString().trim();
57
+
58
+ let patch: Buffer;
59
+ let patchExceededMaxBuffer = false;
60
+ try {
61
+ patch = execFileSync("git", ["diff", "--binary", baseSha], {
62
+ maxBuffer: 100 * 1024 * 1024,
63
+ encoding: "buffer",
64
+ });
65
+ } catch (err: any) {
66
+ if (err.code === "ERR_CHILD_PROCESS_STDIO_MAXBUFFER") {
67
+ patchExceededMaxBuffer = true;
68
+ patch = Buffer.alloc(0);
69
+ } else {
70
+ throw err;
71
+ }
72
+ }
73
+
74
+ const untrackedList = execFileSync("git", ["ls-files", "-oz", "--exclude-standard"])
75
+ .toString().split("\0").filter(Boolean)
76
+ .filter(f => !f.startsWith("/") && !f.includes("..") && !f.startsWith("~"));
77
+
78
+ const sensitivePatterns = [".env", ".npmrc", ".netrc", ".pgpass", "credentials",
79
+ "id_rsa", "id_ed25519", ".pem", ".key", ".p12", ".keystore"];
80
+ const safeUntrackedList = untrackedList.filter(f => {
81
+ const basename = f.split("/").pop()!.toLowerCase();
82
+ return !sensitivePatterns.some(p => basename === p || basename.startsWith(p + "."));
83
+ });
84
+
85
+ let untrackedTarball: Buffer | null = null;
86
+ let untrackedExceededMaxBuffer = false;
87
+ if (safeUntrackedList.length > 0) {
88
+ const fileListBuf = Buffer.from(safeUntrackedList.join("\0") + "\0");
89
+ try {
90
+ untrackedTarball = execFileSync("tar", ["czf", "-", "--null", "-T", "-"], {
91
+ input: fileListBuf,
92
+ maxBuffer: 250 * 1024 * 1024,
93
+ });
94
+ } catch (err: any) {
95
+ if (err.code === "ERR_CHILD_PROCESS_STDIO_MAXBUFFER") {
96
+ untrackedExceededMaxBuffer = true;
97
+ untrackedTarball = null;
98
+ } else {
99
+ console.error("[naiad] tar failed, skipping untracked archive:", err.message);
100
+ untrackedTarball = null;
101
+ }
102
+ }
103
+ }
104
+
105
+ const patchOverCap = patchExceededMaxBuffer || patch.length > 50 * 1024 * 1024;
106
+ const untrackedOverCap = untrackedExceededMaxBuffer || (untrackedTarball !== null && untrackedTarball.length > 200 * 1024 * 1024);
107
+ const skippedArtifacts: string[] = [];
108
+ if (patchOverCap) skippedArtifacts.push("patch");
109
+ if (untrackedOverCap) skippedArtifacts.push("untracked");
110
+
111
+ const tmpDir = path.join(os.tmpdir(), `naiad-checkpoint-${Date.now()}`);
112
+ fs.mkdirSync(tmpDir, { recursive: true });
113
+
114
+ let patchPath: string | null = null;
115
+ if (!patchOverCap && patch.length > 0) {
116
+ patchPath = path.join(tmpDir, "patch.bin");
117
+ fs.writeFileSync(patchPath, patch);
118
+ }
119
+
120
+ let untrackedPath: string | null = null;
121
+ if (!untrackedOverCap && untrackedTarball) {
122
+ untrackedPath = path.join(tmpDir, "untracked.tar.gz");
123
+ fs.writeFileSync(untrackedPath, untrackedTarball);
124
+ }
125
+
126
+ reporter.send("checkpoint", {
127
+ base_sha: baseSha,
128
+ head_sha: headSha,
129
+ patchPath,
130
+ untrackedPath,
131
+ untrackedFiles: safeUntrackedList,
132
+ patchBytes: patch.length,
133
+ patchExceededMaxBuffer,
134
+ skippedArtifacts,
135
+ timestamp: Date.now(),
136
+ });
137
+ } catch (err: any) {
138
+ console.error("[naiad] checkpoint generation failed:", err.message);
139
+ }
140
+ });
141
+ }
142
+
30
143
  // Parse available models from env; fall back to a single placeholder if unset
31
144
  let naiadModels: NaiadModel[] = [];
32
145
  try {
@@ -39,7 +152,6 @@ export default function (pi: ExtensionAPI) {
39
152
  }
40
153
 
41
154
  // Event forwarding via HTTP callback (for interactive mode)
42
- const callbackUrl = process.env.NAIAD_CALLBACK_URL;
43
155
  if (callbackUrl && typeof (pi as any).on === "function") {
44
156
  const postEvent = (type: string, data: unknown) => {
45
157
  fetch(callbackUrl, {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "naiad-cli",
3
- "version": "0.2.7",
3
+ "version": "0.2.8",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "naiad": "./dist/index.js"