pi-ultracode 0.1.2 → 0.3.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.
@@ -16,10 +16,14 @@ export interface RunHandle {
16
16
  details?: WorkflowRunDetails;
17
17
  }
18
18
 
19
+ interface RegistryScope {
20
+ runs: Map<string, RunHandle>;
21
+ order: string[];
22
+ }
23
+
19
24
  export class WorkflowRegistry {
20
- private readonly runs = new Map<string, RunHandle>();
25
+ private readonly scopes = new Map<string, RegistryScope>();
21
26
  private readonly listeners = new Set<() => void>();
22
- private order: string[] = [];
23
27
  private scopeDir?: string;
24
28
 
25
29
  register(
@@ -28,12 +32,13 @@ export class WorkflowRegistry {
28
32
  abort: () => void,
29
33
  details?: WorkflowRunDetails,
30
34
  ): RunHandle {
35
+ const scope = this.currentScope();
31
36
  const handle: RunHandle = { snapshot, abort, startedAt: Date.now(), details };
32
- this.runs.set(runId, handle);
33
- this.order = this.order.filter((id) => id !== runId);
34
- this.order.push(runId);
35
- // Keep at most the 50 most recent runs in memory without evicting active work.
36
- this.trimRuns();
37
+ scope.runs.set(runId, handle);
38
+ scope.order = scope.order.filter((id) => id !== runId);
39
+ scope.order.push(runId);
40
+ // Keep at most the 50 most recent runs per session without evicting active work.
41
+ this.trimRuns(scope);
37
42
  this.notify();
38
43
  return handle;
39
44
  }
@@ -56,6 +61,7 @@ export class WorkflowRegistry {
56
61
  /** Restore the newest task-detail manifests for the current Pi session. */
57
62
  restoreRuns(runsDir: string): number {
58
63
  this.setScope(runsDir);
64
+ const scope = this.currentScope();
59
65
  let files: string[];
60
66
  try {
61
67
  files = fs.readdirSync(runsDir)
@@ -69,7 +75,7 @@ export class WorkflowRegistry {
69
75
  let restored = 0;
70
76
  for (const file of files) {
71
77
  const loaded = WorkflowRunDetails.restore(file);
72
- if (!loaded || this.runs.has(loaded.snapshot.runId ?? loaded.details.runId)) continue;
78
+ if (!loaded || scope.runs.has(loaded.snapshot.runId ?? loaded.details.runId)) continue;
73
79
  const runId = loaded.snapshot.runId ?? loaded.details.runId;
74
80
  if (loaded.snapshot.status === "running") {
75
81
  loaded.snapshot.status = "aborted";
@@ -94,31 +100,32 @@ export class WorkflowRegistry {
94
100
  startedAt,
95
101
  details: loaded.details,
96
102
  };
97
- this.runs.set(runId, handle);
98
- this.order.push(runId);
103
+ scope.runs.set(runId, handle);
104
+ scope.order.push(runId);
99
105
  restored++;
100
106
  }
101
- this.trimRuns();
107
+ this.trimRuns(scope);
102
108
  if (restored) this.notify();
103
109
  return restored;
104
110
  }
105
111
 
112
+ /** Select a session without discarding live handles from other sessions. */
106
113
  setScope(runsDir: string): void {
107
114
  const resolved = path.resolve(runsDir);
108
115
  if (this.scopeDir === resolved) return;
109
116
  this.scopeDir = resolved;
110
- this.runs.clear();
111
- this.order = [];
117
+ this.currentScope();
112
118
  this.notify();
113
119
  }
114
120
 
115
121
  get(runId: string): RunHandle | undefined {
116
- return this.runs.get(runId);
122
+ return this.currentScope().runs.get(runId);
117
123
  }
118
124
 
119
125
  list(): RunHandle[] {
120
- return this.order
121
- .map((id) => this.runs.get(id))
126
+ const scope = this.currentScope();
127
+ return scope.order
128
+ .map((id) => scope.runs.get(id))
122
129
  .filter((handle): handle is RunHandle => Boolean(handle))
123
130
  .sort((left, right) => right.startedAt - left.startedAt);
124
131
  }
@@ -128,7 +135,7 @@ export class WorkflowRegistry {
128
135
  }
129
136
 
130
137
  abortAll(): void {
131
- for (const handle of this.runs.values()) {
138
+ for (const handle of this.currentScope().runs.values()) {
132
139
  if (handle.snapshot.status === "running") {
133
140
  try {
134
141
  handle.abort();
@@ -139,16 +146,26 @@ export class WorkflowRegistry {
139
146
  }
140
147
  }
141
148
 
142
- private trimRuns(): void {
143
- while (this.runs.size > 50) {
144
- const candidate = [...this.runs.entries()].sort((left, right) => {
149
+ private currentScope(): RegistryScope {
150
+ const key = this.scopeDir ?? "<default>";
151
+ let scope = this.scopes.get(key);
152
+ if (!scope) {
153
+ scope = { runs: new Map(), order: [] };
154
+ this.scopes.set(key, scope);
155
+ }
156
+ return scope;
157
+ }
158
+
159
+ private trimRuns(scope: RegistryScope): void {
160
+ while (scope.runs.size > 50) {
161
+ const candidate = [...scope.runs.entries()].sort((left, right) => {
145
162
  const leftActive = left[1].snapshot.status === "running" ? 1 : 0;
146
163
  const rightActive = right[1].snapshot.status === "running" ? 1 : 0;
147
164
  return leftActive - rightActive || left[1].startedAt - right[1].startedAt;
148
165
  })[0];
149
166
  if (!candidate) break;
150
- this.runs.delete(candidate[0]);
151
- this.order = this.order.filter((id) => id !== candidate[0]);
167
+ scope.runs.delete(candidate[0]);
168
+ scope.order = scope.order.filter((id) => id !== candidate[0]);
152
169
  }
153
170
  }
154
171
  }
@@ -0,0 +1,231 @@
1
+ import * as crypto from "node:crypto";
2
+ import * as fs from "node:fs";
3
+ import * as os from "node:os";
4
+ import * as path from "node:path";
5
+
6
+ const NO_FOLLOW = fs.constants.O_NOFOLLOW ?? 0;
7
+
8
+ export function standaloneWorkflowRunsDir(cwd: string): string {
9
+ const cwdKey = crypto.createHash("sha256").update(path.resolve(cwd)).digest("hex").slice(0, 24);
10
+ return path.join(os.homedir(), ".pi", "ultracode-runs", cwdKey);
11
+ }
12
+
13
+ /** Ensure an artifact directory is real; with a root, reject every symlink below that root. */
14
+ export function ensurePrivateArtifactDirectory(dir: string, trustedRoot?: string): void {
15
+ if (trustedRoot !== undefined) {
16
+ ensureContainedDirectory(trustedRoot, dir);
17
+ return;
18
+ }
19
+ let created = false;
20
+ try {
21
+ fs.lstatSync(dir);
22
+ } catch (error: any) {
23
+ if (error?.code !== "ENOENT") throw error;
24
+ fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
25
+ created = true;
26
+ }
27
+ assertRealDirectory(dir);
28
+ if (process.platform !== "win32") fs.chmodSync(dir, 0o700);
29
+ if (created) {
30
+ fsyncArtifactDirectory(dir);
31
+ const parent = path.dirname(dir);
32
+ if (parent !== dir) fsyncArtifactDirectory(parent);
33
+ }
34
+ }
35
+
36
+ function ensureContainedDirectory(trustedRoot: string, dir: string): void {
37
+ const root = path.resolve(trustedRoot);
38
+ const target = path.resolve(dir);
39
+ assertRealDirectory(root);
40
+ const relative = path.relative(root, target);
41
+ if (relative.startsWith("..") || path.isAbsolute(relative)) {
42
+ throw new Error(`workflow artifact directory must stay inside ${root}`);
43
+ }
44
+ let current = root;
45
+ for (const part of relative ? relative.split(path.sep) : []) {
46
+ current = path.join(current, part);
47
+ let created = false;
48
+ try {
49
+ assertRealDirectory(current);
50
+ } catch (error: any) {
51
+ if (error?.code !== "ENOENT") throw error;
52
+ fs.mkdirSync(current, { mode: 0o700 });
53
+ created = true;
54
+ assertRealDirectory(current);
55
+ }
56
+ if (process.platform !== "win32") fs.chmodSync(current, 0o700);
57
+ if (created) {
58
+ fsyncArtifactDirectory(current);
59
+ fsyncArtifactDirectory(path.dirname(current));
60
+ }
61
+ }
62
+ }
63
+
64
+ function assertRealDirectory(dir: string): void {
65
+ const stat = fs.lstatSync(dir);
66
+ if (stat.isSymbolicLink() || !stat.isDirectory()) {
67
+ throw new Error(`workflow artifact directory must be a real directory: ${dir}`);
68
+ }
69
+ }
70
+
71
+ /** Persist directory-entry changes on platforms that support directory fsync. */
72
+ export function fsyncArtifactDirectory(dir: string): void {
73
+ if (process.platform === "win32") return;
74
+ const fd = fs.openSync(dir, fs.constants.O_RDONLY | (fs.constants.O_DIRECTORY ?? 0));
75
+ try {
76
+ const stat = fs.fstatSync(fd);
77
+ if (!stat.isDirectory()) throw new Error(`workflow artifact parent must be a directory: ${dir}`);
78
+ fs.fsyncSync(fd);
79
+ } finally {
80
+ fs.closeSync(fd);
81
+ }
82
+ }
83
+
84
+ export function artifactPathExists(filePath: string): boolean {
85
+ try {
86
+ fs.lstatSync(filePath);
87
+ return true;
88
+ } catch (error: any) {
89
+ if (error?.code === "ENOENT") return false;
90
+ throw error;
91
+ }
92
+ }
93
+
94
+ /** Reject symlinks and non-regular artifact files before reading or resuming them. */
95
+ export function assertRegularArtifactFile(filePath: string, label: string): fs.Stats {
96
+ let stat: fs.Stats;
97
+ try {
98
+ stat = fs.lstatSync(filePath);
99
+ } catch (error) {
100
+ throw new Error(`${label} was not found: ${filePath}`, { cause: error });
101
+ }
102
+ if (stat.isSymbolicLink() || !stat.isFile()) {
103
+ throw new Error(`${label} must be a regular file and may not be a symlink: ${filePath}`);
104
+ }
105
+ return stat;
106
+ }
107
+
108
+ export function readContainedArtifactFile(
109
+ trustedRoot: string,
110
+ filePath: string,
111
+ label: string,
112
+ maxBytes = 16 * 1024 * 1024,
113
+ ): string {
114
+ const root = path.resolve(trustedRoot);
115
+ const target = path.resolve(filePath);
116
+ const relative = path.relative(root, target);
117
+ if (!relative || relative.startsWith("..") || path.isAbsolute(relative)) {
118
+ throw new Error(`${label} must stay inside ${root}`);
119
+ }
120
+ let current = root;
121
+ const parts = relative.split(path.sep);
122
+ for (const [index, part] of parts.entries()) {
123
+ current = path.join(current, part);
124
+ const stat = fs.lstatSync(current);
125
+ if (stat.isSymbolicLink()) throw new Error(`${label} path may not contain symlinks: ${current}`);
126
+ if (index < parts.length - 1 && !stat.isDirectory()) {
127
+ throw new Error(`${label} parent must be a directory: ${current}`);
128
+ }
129
+ }
130
+ return readArtifactFile(target, label, maxBytes);
131
+ }
132
+
133
+ export function readArtifactFile(filePath: string, label: string, maxBytes = 16 * 1024 * 1024): string {
134
+ assertRegularArtifactFile(filePath, label);
135
+ const fd = fs.openSync(filePath, fs.constants.O_RDONLY | NO_FOLLOW);
136
+ try {
137
+ const stat = fs.fstatSync(fd);
138
+ if (!stat.isFile()) throw new Error(`${label} must be a regular file: ${filePath}`);
139
+ if (stat.size > maxBytes) throw new Error(`${label} exceeds ${maxBytes} bytes: ${filePath}`);
140
+ const chunks: Buffer[] = [];
141
+ let total = 0;
142
+ while (total <= maxBytes) {
143
+ const chunk = Buffer.allocUnsafe(Math.min(64 * 1024, maxBytes + 1 - total));
144
+ const bytesRead = fs.readSync(fd, chunk, 0, chunk.length, null);
145
+ if (bytesRead === 0) break;
146
+ chunks.push(chunk.subarray(0, bytesRead));
147
+ total += bytesRead;
148
+ }
149
+ if (total > maxBytes) throw new Error(`${label} exceeds ${maxBytes} bytes: ${filePath}`);
150
+ return Buffer.concat(chunks, total).toString("utf8");
151
+ } finally {
152
+ fs.closeSync(fd);
153
+ }
154
+ }
155
+
156
+ export function appendArtifactFile(
157
+ filePath: string,
158
+ data: string | Uint8Array,
159
+ options: { trustedRoot: string },
160
+ ): void {
161
+ ensurePrivateArtifactDirectory(path.dirname(filePath), options.trustedRoot);
162
+ let fd: number | undefined;
163
+ try {
164
+ fd = fs.openSync(
165
+ filePath,
166
+ fs.constants.O_CREAT | fs.constants.O_WRONLY | fs.constants.O_APPEND | NO_FOLLOW,
167
+ 0o600,
168
+ );
169
+ const stat = fs.fstatSync(fd);
170
+ if (!stat.isFile()) throw new Error(`workflow artifact must be a regular file: ${filePath}`);
171
+ fs.writeFileSync(fd, data);
172
+ } finally {
173
+ if (fd !== undefined) fs.closeSync(fd);
174
+ }
175
+ }
176
+
177
+ /**
178
+ * Write an artifact through an exclusive random temporary file and atomic rename.
179
+ * Existing symlinks are rejected rather than followed.
180
+ */
181
+ export function writeArtifactFile(
182
+ filePath: string,
183
+ data: string | Uint8Array,
184
+ options: { trustedRoot: string; overwrite?: boolean },
185
+ ): void {
186
+ ensurePrivateArtifactDirectory(path.dirname(filePath), options.trustedRoot);
187
+ let existing: fs.Stats | undefined;
188
+ try {
189
+ existing = fs.lstatSync(filePath);
190
+ } catch (error: any) {
191
+ if (error?.code !== "ENOENT") throw error;
192
+ }
193
+ if (existing?.isSymbolicLink() || (existing && !existing.isFile())) {
194
+ throw new Error(`workflow artifact must be a regular file and may not be a symlink: ${filePath}`);
195
+ }
196
+ if (existing && !options.overwrite) {
197
+ throw new Error(`workflow artifact already exists: ${filePath}`);
198
+ }
199
+
200
+ const tempPath = path.join(
201
+ path.dirname(filePath),
202
+ `.${path.basename(filePath)}.${process.pid}.${crypto.randomBytes(8).toString("hex")}.tmp`,
203
+ );
204
+ let fd: number | undefined;
205
+ try {
206
+ fd = fs.openSync(
207
+ tempPath,
208
+ fs.constants.O_CREAT | fs.constants.O_EXCL | fs.constants.O_WRONLY | NO_FOLLOW,
209
+ 0o600,
210
+ );
211
+ fs.writeFileSync(fd, data);
212
+ fs.fsyncSync(fd);
213
+ fs.closeSync(fd);
214
+ fd = undefined;
215
+ fs.renameSync(tempPath, filePath);
216
+ fsyncArtifactDirectory(path.dirname(filePath));
217
+ } finally {
218
+ if (fd !== undefined) {
219
+ try {
220
+ fs.closeSync(fd);
221
+ } catch {
222
+ // best-effort descriptor cleanup
223
+ }
224
+ }
225
+ try {
226
+ fs.rmSync(tempPath, { force: true });
227
+ } catch {
228
+ // best-effort temporary-file cleanup
229
+ }
230
+ }
231
+ }