patchrelay 0.7.2 → 0.7.3
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/dist/build-info.json
CHANGED
package/dist/config.js
CHANGED
|
@@ -101,7 +101,7 @@ const configSchema = z.object({
|
|
|
101
101
|
});
|
|
102
102
|
function defaultTriggerEvents(actor) {
|
|
103
103
|
if (actor === "app") {
|
|
104
|
-
return ["agentSessionCreated", "agentPrompted", "statusChanged"];
|
|
104
|
+
return ["issueCreated", "agentSessionCreated", "agentPrompted", "statusChanged"];
|
|
105
105
|
}
|
|
106
106
|
return ["statusChanged"];
|
|
107
107
|
}
|
|
@@ -28,10 +28,11 @@ export async function buildReconciliationSnapshot(params) {
|
|
|
28
28
|
.catch(() => ({ status: "unknown" }))
|
|
29
29
|
: ({ status: "unknown" });
|
|
30
30
|
const liveCodex = runLease.threadId
|
|
31
|
-
? await
|
|
32
|
-
.
|
|
33
|
-
|
|
34
|
-
|
|
31
|
+
? await hydrateLiveCodexState({
|
|
32
|
+
codex: params.codex,
|
|
33
|
+
threadId: runLease.threadId,
|
|
34
|
+
...(workspaceOwnership?.worktreePath ? { cwd: workspaceOwnership.worktreePath } : {}),
|
|
35
|
+
})
|
|
35
36
|
: ({ status: "unknown" });
|
|
36
37
|
const obligations = params.stores.obligations
|
|
37
38
|
.listPendingObligations({ runLeaseId: runLease.id, includeInProgress: true })
|
|
@@ -83,6 +84,39 @@ export async function buildReconciliationSnapshot(params) {
|
|
|
83
84
|
},
|
|
84
85
|
};
|
|
85
86
|
}
|
|
87
|
+
async function hydrateLiveCodexState(params) {
|
|
88
|
+
try {
|
|
89
|
+
const thread = await params.codex.readThread(params.threadId, true);
|
|
90
|
+
if (latestThreadTurn(thread)?.status === "interrupted" && params.cwd) {
|
|
91
|
+
const resumedThread = await tryResumeThread(params.codex, params.threadId, params.cwd);
|
|
92
|
+
if (resumedThread) {
|
|
93
|
+
return { status: "found", thread: resumedThread };
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return { status: "found", thread };
|
|
97
|
+
}
|
|
98
|
+
catch (error) {
|
|
99
|
+
const mapped = mapCodexReadFailure(error);
|
|
100
|
+
if (mapped.status === "missing" && params.cwd) {
|
|
101
|
+
const resumedThread = await tryResumeThread(params.codex, params.threadId, params.cwd);
|
|
102
|
+
if (resumedThread) {
|
|
103
|
+
return { status: "found", thread: resumedThread };
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return mapped;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
async function tryResumeThread(codex, threadId, cwd) {
|
|
110
|
+
try {
|
|
111
|
+
return await codex.resumeThread(threadId, cwd);
|
|
112
|
+
}
|
|
113
|
+
catch {
|
|
114
|
+
return undefined;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
function latestThreadTurn(thread) {
|
|
118
|
+
return thread.turns.at(-1);
|
|
119
|
+
}
|
|
86
120
|
function mapCodexReadFailure(error) {
|
|
87
121
|
const message = error instanceof Error ? error.message : String(error);
|
|
88
122
|
const normalized = message.trim().toLowerCase();
|
|
@@ -11,22 +11,23 @@ export class ServiceStageFinalizer {
|
|
|
11
11
|
codex;
|
|
12
12
|
linearProvider;
|
|
13
13
|
enqueueIssue;
|
|
14
|
+
logger;
|
|
14
15
|
feed;
|
|
15
16
|
inputDispatcher;
|
|
16
17
|
lifecyclePublisher;
|
|
17
18
|
actionApplier;
|
|
18
19
|
runAtomically;
|
|
19
|
-
constructor(config, stores, codex, linearProvider, enqueueIssue, logger, feed, runAtomically = (fn) => fn()) {
|
|
20
|
+
constructor(config, stores, codex, linearProvider, enqueueIssue, logger = consoleLogger(), feed, runAtomically = (fn) => fn()) {
|
|
20
21
|
this.config = config;
|
|
21
22
|
this.stores = stores;
|
|
22
23
|
this.codex = codex;
|
|
23
24
|
this.linearProvider = linearProvider;
|
|
24
25
|
this.enqueueIssue = enqueueIssue;
|
|
26
|
+
this.logger = logger;
|
|
25
27
|
this.feed = feed;
|
|
26
28
|
this.runAtomically = runAtomically;
|
|
27
|
-
|
|
28
|
-
this.
|
|
29
|
-
this.lifecyclePublisher = new StageLifecyclePublisher(config, stores, linearProvider, lifecycleLogger, feed);
|
|
29
|
+
this.inputDispatcher = new StageTurnInputDispatcher(stores, codex, this.logger);
|
|
30
|
+
this.lifecyclePublisher = new StageLifecyclePublisher(config, stores, linearProvider, this.logger, feed);
|
|
30
31
|
this.actionApplier = new ReconciliationActionApplier({
|
|
31
32
|
enqueueIssue,
|
|
32
33
|
deliverPendingObligations: (projectId, linearIssueId, threadId, turnId) => this.deliverPendingObligations(projectId, linearIssueId, threadId, turnId),
|
|
@@ -296,6 +297,9 @@ export class ServiceStageFinalizer {
|
|
|
296
297
|
if (!snapshot) {
|
|
297
298
|
return;
|
|
298
299
|
}
|
|
300
|
+
if (await this.restartInterruptedRun(snapshot)) {
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
299
303
|
const decision = reconcileIssue(snapshot.input);
|
|
300
304
|
if (decision.outcome === "hydrate_live_state") {
|
|
301
305
|
throw new Error(`Startup reconciliation requires live state hydration for ${snapshot.runLease.projectId}:${snapshot.runLease.linearIssueId}: ${decision.reasons.join("; ")}`);
|
|
@@ -305,6 +309,63 @@ export class ServiceStageFinalizer {
|
|
|
305
309
|
decision,
|
|
306
310
|
});
|
|
307
311
|
}
|
|
312
|
+
async restartInterruptedRun(snapshot) {
|
|
313
|
+
const liveCodex = snapshot.input.live?.codex;
|
|
314
|
+
const latestTurn = liveCodex?.status === "found" ? liveCodex.thread?.turns.at(-1) : undefined;
|
|
315
|
+
if (latestTurn?.status !== "interrupted") {
|
|
316
|
+
return false;
|
|
317
|
+
}
|
|
318
|
+
if (snapshot.runLease.turnId && latestTurn.id !== snapshot.runLease.turnId) {
|
|
319
|
+
return true;
|
|
320
|
+
}
|
|
321
|
+
if (!snapshot.runLease.threadId || !snapshot.workspaceOwnership?.worktreePath) {
|
|
322
|
+
return false;
|
|
323
|
+
}
|
|
324
|
+
const stageRun = this.findStageRunForIssue(snapshot.runLease.projectId, snapshot.runLease.linearIssueId, snapshot.runLease.threadId);
|
|
325
|
+
if (!stageRun) {
|
|
326
|
+
return false;
|
|
327
|
+
}
|
|
328
|
+
const issue = this.stores.issueWorkflows.getTrackedIssue(stageRun.projectId, stageRun.linearIssueId);
|
|
329
|
+
const turn = await this.codex.startTurn({
|
|
330
|
+
threadId: snapshot.runLease.threadId,
|
|
331
|
+
cwd: snapshot.workspaceOwnership.worktreePath,
|
|
332
|
+
input: buildRestartRecoveryPrompt(stageRun.stage),
|
|
333
|
+
});
|
|
334
|
+
this.stores.workflowCoordinator.updateStageRunThread({
|
|
335
|
+
stageRunId: stageRun.id,
|
|
336
|
+
threadId: snapshot.runLease.threadId,
|
|
337
|
+
turnId: turn.turnId,
|
|
338
|
+
});
|
|
339
|
+
this.inputDispatcher.routePendingInputs(stageRun, snapshot.runLease.threadId, turn.turnId);
|
|
340
|
+
await this.inputDispatcher.flush({
|
|
341
|
+
id: stageRun.id,
|
|
342
|
+
projectId: stageRun.projectId,
|
|
343
|
+
linearIssueId: stageRun.linearIssueId,
|
|
344
|
+
threadId: snapshot.runLease.threadId,
|
|
345
|
+
turnId: turn.turnId,
|
|
346
|
+
}, {
|
|
347
|
+
logFailures: true,
|
|
348
|
+
failureMessage: "Failed to deliver queued Linear comment during interrupted-turn recovery",
|
|
349
|
+
...(issue?.issueKey ? { issueKey: issue.issueKey } : {}),
|
|
350
|
+
});
|
|
351
|
+
this.logger.info({
|
|
352
|
+
issueKey: issue?.issueKey,
|
|
353
|
+
stage: stageRun.stage,
|
|
354
|
+
threadId: snapshot.runLease.threadId,
|
|
355
|
+
turnId: turn.turnId,
|
|
356
|
+
}, "Restarted interrupted Codex stage run during startup reconciliation");
|
|
357
|
+
this.feed?.publish({
|
|
358
|
+
level: "info",
|
|
359
|
+
kind: "stage",
|
|
360
|
+
issueKey: issue?.issueKey,
|
|
361
|
+
projectId: stageRun.projectId,
|
|
362
|
+
stage: stageRun.stage,
|
|
363
|
+
status: "running",
|
|
364
|
+
summary: `Recovered ${stageRun.stage} workflow after restart`,
|
|
365
|
+
detail: `Turn ${turn.turnId} resumed on the existing thread.`,
|
|
366
|
+
});
|
|
367
|
+
return true;
|
|
368
|
+
}
|
|
308
369
|
completeReconciledRun(projectId, linearIssueId, thread, params) {
|
|
309
370
|
const stageRun = this.findStageRunForIssue(projectId, linearIssueId, params.threadId);
|
|
310
371
|
const issue = this.stores.issueWorkflows.getTrackedIssue(projectId, linearIssueId);
|
|
@@ -380,3 +441,12 @@ function consoleLogger() {
|
|
|
380
441
|
level: "silent",
|
|
381
442
|
};
|
|
382
443
|
}
|
|
444
|
+
function buildRestartRecoveryPrompt(stage) {
|
|
445
|
+
return [
|
|
446
|
+
`PatchRelay restarted while the ${stage} workflow was mid-turn.`,
|
|
447
|
+
"Resume the existing work from the current worktree state on this same thread.",
|
|
448
|
+
"Inspect any uncommitted changes you already made before continuing.",
|
|
449
|
+
"Continue from the interrupted point instead of restarting the task from scratch.",
|
|
450
|
+
"When the work is actually complete, finish the normal workflow handoff for this stage.",
|
|
451
|
+
].join("\n");
|
|
452
|
+
}
|