pi-harness-runtime 0.10.13 → 0.10.14

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,277 @@
1
+ /**
2
+ * Job State Machine — RFC-0015
3
+ *
4
+ * Manages job lifecycle states with:
5
+ * - Defined state transitions
6
+ * - Event emission on every transition
7
+ * - Automatic checkpointing
8
+ * - Transition guards (validity checks)
9
+ *
10
+ * States:
11
+ * created -> planning -> queued -> running -> testing -> e2e_testing ->
12
+ * reviewing -> repairing -> paused_quota -> waiting_human -> ready_for_client -> archived
13
+ */
14
+ // Valid transitions map
15
+ const VALID_TRANSITIONS = {
16
+ created: ["planning"],
17
+ planning: ["queued", "cancelled"],
18
+ queued: ["running", "cancelled", "waiting_human"],
19
+ running: [
20
+ "testing",
21
+ "reviewing",
22
+ "repairing",
23
+ "paused_quota",
24
+ "blocked",
25
+ "waiting_human",
26
+ "cancelled",
27
+ ],
28
+ testing: [
29
+ "reviewing",
30
+ "running",
31
+ "repairing",
32
+ "paused_quota",
33
+ "waiting_human",
34
+ "cancelled",
35
+ ],
36
+ e2e_testing: [
37
+ "reviewing",
38
+ "repairing",
39
+ "paused_quota",
40
+ "waiting_human",
41
+ "cancelled",
42
+ ],
43
+ reviewing: [
44
+ "repairing",
45
+ "running",
46
+ "ready_for_client",
47
+ "paused_quota",
48
+ "waiting_human",
49
+ "cancelled",
50
+ ],
51
+ repairing: [
52
+ "running",
53
+ "testing",
54
+ "reviewing",
55
+ "paused_quota",
56
+ "waiting_human",
57
+ "cancelled",
58
+ ],
59
+ paused_quota: ["running", "waiting_human", "cancelled"],
60
+ waiting_human: ["running", "planning", "cancelled"],
61
+ blocked: ["running", "waiting_human", "cancelled"],
62
+ ready_for_client: ["archived", "repairing"],
63
+ cancelled: [],
64
+ archived: [],
65
+ };
66
+ export class JobStateMachine {
67
+ options;
68
+ currentCheckpoint = null;
69
+ eventLog = [];
70
+ constructor(options) {
71
+ this.options = options;
72
+ }
73
+ /**
74
+ * Initialize a new job with a requirement
75
+ */
76
+ async createJob(jobId, requirement) {
77
+ const now = new Date().toISOString();
78
+ const checkpoint = {
79
+ version: 1,
80
+ jobId,
81
+ status: "created",
82
+ requirement,
83
+ createdAt: now,
84
+ updatedAt: now,
85
+ };
86
+ const event = this.createEvent(jobId, "JobCreated", `Job ${jobId} created with requirement`, { requirement });
87
+ try {
88
+ await this.options.checkpointManager.save(checkpoint);
89
+ await this.options.checkpointManager.appendEvent(jobId, event);
90
+ this.currentCheckpoint = checkpoint;
91
+ this.eventLog.push(event);
92
+ return { success: true, checkpoint };
93
+ }
94
+ catch (error) {
95
+ return { success: false, error: String(error) };
96
+ }
97
+ }
98
+ /**
99
+ * Resume a job from checkpoint
100
+ */
101
+ async resumeJob(jobId) {
102
+ const checkpoint = await this.options.checkpointManager.load(jobId);
103
+ if (!checkpoint) {
104
+ return { success: false, error: `No checkpoint found for job ${jobId}` };
105
+ }
106
+ this.currentCheckpoint = checkpoint;
107
+ return { success: true, checkpoint };
108
+ }
109
+ /**
110
+ * Transition to a new state
111
+ */
112
+ async transition(to, data) {
113
+ if (!this.currentCheckpoint) {
114
+ return {
115
+ success: false,
116
+ error: "No active job. Call createJob() or resumeJob() first.",
117
+ };
118
+ }
119
+ const from = this.currentCheckpoint.status;
120
+ // Validate transition
121
+ if (!this.isValidTransition(from, to)) {
122
+ return {
123
+ success: false,
124
+ error: `Invalid transition: ${from} -> ${to}. Valid transitions: ${VALID_TRANSITIONS[from].join(", ") || "none"}`,
125
+ };
126
+ }
127
+ // Create updated checkpoint
128
+ const updated = {
129
+ ...this.currentCheckpoint,
130
+ status: to,
131
+ updatedAt: new Date().toISOString(),
132
+ ...data,
133
+ };
134
+ // Create event
135
+ const event = this.createEvent(this.currentCheckpoint.jobId, `StateTransition:${from}->${to}`, `Transitioned from ${from} to ${to}`, { from, to, ...data });
136
+ try {
137
+ await this.options.checkpointManager.save(updated);
138
+ await this.options.checkpointManager.appendEvent(this.currentCheckpoint.jobId, event);
139
+ this.currentCheckpoint = updated;
140
+ this.eventLog.push(event);
141
+ return { success: true, checkpoint: updated };
142
+ }
143
+ catch (error) {
144
+ return { success: false, error: String(error) };
145
+ }
146
+ }
147
+ /**
148
+ * Check if a transition is valid
149
+ */
150
+ isValidTransition(from, to) {
151
+ return VALID_TRANSITIONS[from]?.includes(to) ?? false;
152
+ }
153
+ /**
154
+ * Get available next states from current state
155
+ */
156
+ getAvailableTransitions() {
157
+ if (!this.currentCheckpoint)
158
+ return [];
159
+ return VALID_TRANSITIONS[this.currentCheckpoint.status] ?? [];
160
+ }
161
+ /**
162
+ * Get current checkpoint
163
+ */
164
+ getCheckpoint() {
165
+ return this.currentCheckpoint;
166
+ }
167
+ /**
168
+ * Get event log
169
+ */
170
+ getEventLog() {
171
+ return [...this.eventLog];
172
+ }
173
+ /**
174
+ * Set current task
175
+ */
176
+ async setCurrentTask(taskId) {
177
+ return this.transition(this.currentCheckpoint.status, {
178
+ currentTaskId: taskId,
179
+ });
180
+ }
181
+ /**
182
+ * Set provider
183
+ */
184
+ async setProvider(provider) {
185
+ return this.transition(this.currentCheckpoint.status, { provider });
186
+ }
187
+ /**
188
+ * Set resume time (for quota pause)
189
+ */
190
+ async setResumeTime(resumeAt) {
191
+ if (!this.currentCheckpoint) {
192
+ return { success: false, error: "No active job" };
193
+ }
194
+ this.currentCheckpoint.resumeAt = resumeAt;
195
+ this.currentCheckpoint.updatedAt = new Date().toISOString();
196
+ try {
197
+ await this.options.checkpointManager.save(this.currentCheckpoint);
198
+ return { success: true, checkpoint: this.currentCheckpoint };
199
+ }
200
+ catch (error) {
201
+ return { success: false, error: String(error) };
202
+ }
203
+ }
204
+ /**
205
+ * Record an error
206
+ */
207
+ async recordError(error) {
208
+ if (!this.currentCheckpoint) {
209
+ return { success: false, error: "No active job" };
210
+ }
211
+ this.currentCheckpoint.lastError = error;
212
+ this.currentCheckpoint.updatedAt = new Date().toISOString();
213
+ try {
214
+ await this.options.checkpointManager.save(this.currentCheckpoint);
215
+ return { success: true, checkpoint: this.currentCheckpoint };
216
+ }
217
+ catch (error) {
218
+ return { success: false, error: String(error) };
219
+ }
220
+ }
221
+ /**
222
+ * Check if job is in a terminal state
223
+ */
224
+ isTerminal() {
225
+ if (!this.currentCheckpoint)
226
+ return false;
227
+ return ["ready_for_client", "cancelled", "archived"].includes(this.currentCheckpoint.status);
228
+ }
229
+ /**
230
+ * Check if job can be resumed
231
+ */
232
+ canResume() {
233
+ if (!this.currentCheckpoint)
234
+ return false;
235
+ return (this.currentCheckpoint.status === "paused_quota" ||
236
+ this.currentCheckpoint.status === "blocked");
237
+ }
238
+ /**
239
+ * Get job status summary
240
+ */
241
+ getStatusSummary() {
242
+ if (!this.currentCheckpoint)
243
+ return null;
244
+ return {
245
+ jobId: this.currentCheckpoint.jobId,
246
+ status: this.currentCheckpoint.status,
247
+ isTerminal: this.isTerminal(),
248
+ canResume: this.canResume(),
249
+ };
250
+ }
251
+ createEvent(jobId, type, message, data) {
252
+ return {
253
+ ts: new Date().toISOString(),
254
+ jobId,
255
+ type,
256
+ message,
257
+ data,
258
+ };
259
+ }
260
+ }
261
+ /**
262
+ * Factory to create a state machine with a JsonCheckpointManager
263
+ */
264
+ export async function createJobStateMachine(rootDir, jobId) {
265
+ const { JsonCheckpointManager } = await import("../packages/checkpoint/src/checkpoint-manager.ts");
266
+ const manager = new JsonCheckpointManager(rootDir);
267
+ if (jobId) {
268
+ const checkpoint = await manager.load(jobId);
269
+ const machine = new JobStateMachine({ checkpointManager: manager });
270
+ if (checkpoint) {
271
+ machine.resumeJob(jobId);
272
+ }
273
+ return { machine, checkpoint };
274
+ }
275
+ const machine = new JobStateMachine({ checkpointManager: manager });
276
+ return { machine, checkpoint: null };
277
+ }