patchrelay 0.2.0 → 0.4.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.
@@ -0,0 +1,48 @@
1
+ import { spawn } from "node:child_process";
2
+ export function buildOpenCommand(config, worktreePath, resumeThreadId) {
3
+ const args = ["--dangerously-bypass-approvals-and-sandbox"];
4
+ if (resumeThreadId) {
5
+ args.push("resume", "-C", worktreePath, resumeThreadId);
6
+ }
7
+ else {
8
+ args.push("-C", worktreePath);
9
+ }
10
+ return {
11
+ command: config.runner.codex.bin,
12
+ args,
13
+ };
14
+ }
15
+ export async function runInteractiveCommand(command, args) {
16
+ return await new Promise((resolve, reject) => {
17
+ const child = spawn(command, args, {
18
+ stdio: "inherit",
19
+ });
20
+ child.on("error", reject);
21
+ child.on("exit", (code, signal) => {
22
+ if (signal) {
23
+ resolve(1);
24
+ return;
25
+ }
26
+ resolve(code ?? 0);
27
+ });
28
+ });
29
+ }
30
+ export async function openExternalUrl(url) {
31
+ const candidates = process.platform === "darwin"
32
+ ? [{ command: "open", args: [url] }]
33
+ : process.platform === "win32"
34
+ ? [{ command: "cmd", args: ["/c", "start", "", url] }]
35
+ : [{ command: "xdg-open", args: [url] }];
36
+ for (const candidate of candidates) {
37
+ try {
38
+ const exitCode = await runInteractiveCommand(candidate.command, candidate.args);
39
+ if (exitCode === 0) {
40
+ return true;
41
+ }
42
+ }
43
+ catch {
44
+ // Try the next opener.
45
+ }
46
+ }
47
+ return false;
48
+ }
@@ -0,0 +1,13 @@
1
+ export function writeOutput(stream, text) {
2
+ stream.write(text);
3
+ }
4
+ export function formatDoctor(report) {
5
+ const lines = ["PatchRelay doctor", ""];
6
+ for (const check of report.checks) {
7
+ const marker = check.status === "pass" ? "PASS" : check.status === "warn" ? "WARN" : "FAIL";
8
+ lines.push(`${marker} [${check.scope}] ${check.message}`);
9
+ }
10
+ lines.push("");
11
+ lines.push(report.ok ? "Doctor result: ready" : "Doctor result: not ready");
12
+ return `${lines.join("\n")}\n`;
13
+ }
@@ -0,0 +1,31 @@
1
+ export async function runServiceCommands(runner, commands) {
2
+ for (const entry of commands) {
3
+ const exitCode = await runner(entry.command, entry.args);
4
+ if (exitCode !== 0) {
5
+ throw new Error(`Command failed with exit code ${exitCode}: ${entry.command} ${entry.args.join(" ")}`);
6
+ }
7
+ }
8
+ }
9
+ export async function tryManageService(runner, commands) {
10
+ try {
11
+ await runServiceCommands(runner, commands);
12
+ return { ok: true };
13
+ }
14
+ catch (error) {
15
+ return { ok: false, error: error instanceof Error ? error.message : String(error) };
16
+ }
17
+ }
18
+ export function installServiceCommands() {
19
+ return [
20
+ { command: "systemctl", args: ["--user", "daemon-reload"] },
21
+ { command: "systemctl", args: ["--user", "enable", "--now", "patchrelay.path"] },
22
+ { command: "systemctl", args: ["--user", "enable", "patchrelay.service"] },
23
+ { command: "systemctl", args: ["--user", "reload-or-restart", "patchrelay.service"] },
24
+ ];
25
+ }
26
+ export function restartServiceCommands() {
27
+ return [
28
+ { command: "systemctl", args: ["--user", "daemon-reload"] },
29
+ { command: "systemctl", args: ["--user", "reload-or-restart", "patchrelay.service"] },
30
+ ];
31
+ }
@@ -141,6 +141,85 @@ export class AuthoritativeLedgerStore {
141
141
  .get(projectId, linearIssueId);
142
142
  return row ? mapWorkspaceOwnership(row) : undefined;
143
143
  }
144
+ upsertIssueSession(params) {
145
+ const now = isoNow();
146
+ this.connection
147
+ .prepare(`
148
+ INSERT INTO issue_sessions (
149
+ project_id, linear_issue_id, workspace_ownership_id, run_lease_id, thread_id, parent_thread_id,
150
+ source, linked_agent_session_id, created_at, updated_at, last_opened_at
151
+ ) VALUES (
152
+ @projectId, @linearIssueId, @workspaceOwnershipId, @runLeaseId, @threadId, @parentThreadId,
153
+ @source, @linkedAgentSessionId, @createdAt, @updatedAt, NULL
154
+ )
155
+ ON CONFLICT(thread_id) DO UPDATE SET
156
+ project_id = @projectId,
157
+ linear_issue_id = @linearIssueId,
158
+ workspace_ownership_id = @workspaceOwnershipId,
159
+ run_lease_id = CASE WHEN @setRunLeaseId = 1 THEN @runLeaseId ELSE issue_sessions.run_lease_id END,
160
+ parent_thread_id = CASE WHEN @setParentThreadId = 1 THEN @parentThreadId ELSE issue_sessions.parent_thread_id END,
161
+ source = @source,
162
+ linked_agent_session_id = CASE
163
+ WHEN @setLinkedAgentSessionId = 1 THEN @linkedAgentSessionId
164
+ ELSE issue_sessions.linked_agent_session_id
165
+ END,
166
+ updated_at = @updatedAt
167
+ `)
168
+ .run({
169
+ projectId: params.projectId,
170
+ linearIssueId: params.linearIssueId,
171
+ workspaceOwnershipId: params.workspaceOwnershipId,
172
+ runLeaseId: params.runLeaseId ?? null,
173
+ threadId: params.threadId,
174
+ parentThreadId: params.parentThreadId ?? null,
175
+ source: params.source,
176
+ linkedAgentSessionId: params.linkedAgentSessionId ?? null,
177
+ createdAt: now,
178
+ updatedAt: now,
179
+ setRunLeaseId: Number("runLeaseId" in params),
180
+ setParentThreadId: Number("parentThreadId" in params),
181
+ setLinkedAgentSessionId: Number("linkedAgentSessionId" in params),
182
+ });
183
+ return this.getIssueSessionByThreadId(params.threadId);
184
+ }
185
+ getIssueSessionByThreadId(threadId) {
186
+ const row = this.connection
187
+ .prepare("SELECT * FROM issue_sessions WHERE thread_id = ?")
188
+ .get(threadId);
189
+ return row ? mapIssueSession(row) : undefined;
190
+ }
191
+ listIssueSessionsForIssue(projectId, linearIssueId) {
192
+ const rows = this.connection
193
+ .prepare(`
194
+ SELECT * FROM issue_sessions
195
+ WHERE project_id = ? AND linear_issue_id = ?
196
+ ORDER BY
197
+ CASE WHEN last_opened_at IS NULL THEN 1 ELSE 0 END,
198
+ last_opened_at DESC,
199
+ id DESC
200
+ `)
201
+ .all(projectId, linearIssueId);
202
+ return rows.map((row) => mapIssueSession(row));
203
+ }
204
+ touchIssueSession(threadId) {
205
+ const now = isoNow();
206
+ const result = this.connection
207
+ .prepare(`
208
+ UPDATE issue_sessions
209
+ SET updated_at = @updatedAt,
210
+ last_opened_at = @lastOpenedAt
211
+ WHERE thread_id = @threadId
212
+ `)
213
+ .run({
214
+ threadId,
215
+ updatedAt: now,
216
+ lastOpenedAt: now,
217
+ });
218
+ if (result.changes < 1) {
219
+ return undefined;
220
+ }
221
+ return this.getIssueSessionByThreadId(threadId);
222
+ }
144
223
  createRunLease(params) {
145
224
  const result = this.connection
146
225
  .prepare(`
@@ -416,6 +495,22 @@ function mapRunLease(row) {
416
495
  ...(row.failure_reason === null ? {} : { failureReason: String(row.failure_reason) }),
417
496
  };
418
497
  }
498
+ function mapIssueSession(row) {
499
+ return {
500
+ id: Number(row.id),
501
+ projectId: String(row.project_id),
502
+ linearIssueId: String(row.linear_issue_id),
503
+ workspaceOwnershipId: Number(row.workspace_ownership_id),
504
+ threadId: String(row.thread_id),
505
+ source: row.source,
506
+ ...(row.run_lease_id === null ? {} : { runLeaseId: Number(row.run_lease_id) }),
507
+ ...(row.parent_thread_id === null ? {} : { parentThreadId: String(row.parent_thread_id) }),
508
+ ...(row.linked_agent_session_id === null ? {} : { linkedAgentSessionId: String(row.linked_agent_session_id) }),
509
+ createdAt: String(row.created_at),
510
+ updatedAt: String(row.updated_at),
511
+ ...(row.last_opened_at === null ? {} : { lastOpenedAt: String(row.last_opened_at) }),
512
+ };
513
+ }
419
514
  function mapObligation(row) {
420
515
  return {
421
516
  id: Number(row.id),
@@ -0,0 +1,54 @@
1
+ import { isoNow } from "./shared.js";
2
+ export class IssueProjectionStore {
3
+ connection;
4
+ constructor(connection) {
5
+ this.connection = connection;
6
+ }
7
+ upsertIssueProjection(params) {
8
+ this.connection
9
+ .prepare(`
10
+ INSERT INTO issue_projection (
11
+ project_id, linear_issue_id, issue_key, title, issue_url, current_linear_state, last_webhook_at, updated_at
12
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
13
+ ON CONFLICT(project_id, linear_issue_id) DO UPDATE SET
14
+ issue_key = COALESCE(excluded.issue_key, issue_projection.issue_key),
15
+ title = COALESCE(excluded.title, issue_projection.title),
16
+ issue_url = COALESCE(excluded.issue_url, issue_projection.issue_url),
17
+ current_linear_state = COALESCE(excluded.current_linear_state, issue_projection.current_linear_state),
18
+ last_webhook_at = COALESCE(excluded.last_webhook_at, issue_projection.last_webhook_at),
19
+ updated_at = excluded.updated_at
20
+ `)
21
+ .run(params.projectId, params.linearIssueId, params.issueKey ?? null, params.title ?? null, params.issueUrl ?? null, params.currentLinearState ?? null, params.lastWebhookAt ?? null, isoNow());
22
+ }
23
+ getIssueProjection(projectId, linearIssueId) {
24
+ const row = this.connection
25
+ .prepare("SELECT * FROM issue_projection WHERE project_id = ? AND linear_issue_id = ?")
26
+ .get(projectId, linearIssueId);
27
+ return row ? mapIssueProjection(row) : undefined;
28
+ }
29
+ getIssueProjectionByKey(issueKey) {
30
+ const row = this.connection
31
+ .prepare("SELECT * FROM issue_projection WHERE issue_key = ? ORDER BY updated_at DESC LIMIT 1")
32
+ .get(issueKey);
33
+ return row ? mapIssueProjection(row) : undefined;
34
+ }
35
+ getIssueProjectionByLinearIssueId(linearIssueId) {
36
+ const row = this.connection
37
+ .prepare("SELECT * FROM issue_projection WHERE linear_issue_id = ? ORDER BY updated_at DESC LIMIT 1")
38
+ .get(linearIssueId);
39
+ return row ? mapIssueProjection(row) : undefined;
40
+ }
41
+ }
42
+ function mapIssueProjection(row) {
43
+ return {
44
+ id: Number(row.id),
45
+ projectId: String(row.project_id),
46
+ linearIssueId: String(row.linear_issue_id),
47
+ ...(row.issue_key === null ? {} : { issueKey: String(row.issue_key) }),
48
+ ...(row.title === null ? {} : { title: String(row.title) }),
49
+ ...(row.issue_url === null ? {} : { issueUrl: String(row.issue_url) }),
50
+ ...(row.current_linear_state === null ? {} : { currentLinearState: String(row.current_linear_state) }),
51
+ ...(row.last_webhook_at === null ? {} : { lastWebhookAt: String(row.last_webhook_at) }),
52
+ updatedAt: String(row.updated_at),
53
+ };
54
+ }
@@ -0,0 +1,309 @@
1
+ import { isoNow } from "./shared.js";
2
+ export class IssueWorkflowCoordinator {
3
+ connection;
4
+ authoritativeLedger;
5
+ issueProjections;
6
+ issueWorkflows;
7
+ runReports;
8
+ constructor(dependencies) {
9
+ this.connection = dependencies.connection;
10
+ this.authoritativeLedger = dependencies.authoritativeLedger;
11
+ this.issueProjections = dependencies.issueProjections;
12
+ this.issueWorkflows = dependencies.issueWorkflows;
13
+ this.runReports = dependencies.runReports;
14
+ }
15
+ upsertTrackedIssue(params) {
16
+ this.issueProjections.upsertIssueProjection({
17
+ projectId: params.projectId,
18
+ linearIssueId: params.linearIssueId,
19
+ ...(params.issueKey ? { issueKey: params.issueKey } : {}),
20
+ ...(params.title ? { title: params.title } : {}),
21
+ ...(params.issueUrl ? { issueUrl: params.issueUrl } : {}),
22
+ ...(params.currentLinearState ? { currentLinearState: params.currentLinearState } : {}),
23
+ ...(params.lastWebhookAt ? { lastWebhookAt: params.lastWebhookAt } : {}),
24
+ });
25
+ const desiredReceiptId = this.resolveDesiredReceiptId({
26
+ projectId: params.projectId,
27
+ linearIssueId: params.linearIssueId,
28
+ ...(params.desiredWebhookId !== undefined ? { desiredWebhookId: params.desiredWebhookId } : {}),
29
+ ...(params.desiredReceiptId !== undefined ? { desiredReceiptId: params.desiredReceiptId } : {}),
30
+ });
31
+ this.authoritativeLedger.upsertIssueControl({
32
+ projectId: params.projectId,
33
+ linearIssueId: params.linearIssueId,
34
+ ...(params.desiredStage !== undefined ? { desiredStage: params.desiredStage } : {}),
35
+ ...(desiredReceiptId !== undefined ? { desiredReceiptId } : {}),
36
+ ...(params.activeWorkspaceId !== undefined ? { activeWorkspaceOwnershipId: params.activeWorkspaceId } : {}),
37
+ ...(params.activeStageRunId !== undefined ? { activeRunLeaseId: params.activeStageRunId } : {}),
38
+ ...(params.statusCommentId !== undefined ? { serviceOwnedCommentId: params.statusCommentId } : {}),
39
+ ...(params.activeAgentSessionId !== undefined ? { activeAgentSessionId: params.activeAgentSessionId } : {}),
40
+ lifecycleStatus: params.lifecycleStatus,
41
+ });
42
+ return this.issueWorkflows.getTrackedIssue(params.projectId, params.linearIssueId);
43
+ }
44
+ recordDesiredStage(params) {
45
+ const existing = this.issueWorkflows.getTrackedIssue(params.projectId, params.linearIssueId);
46
+ this.issueProjections.upsertIssueProjection({
47
+ projectId: params.projectId,
48
+ linearIssueId: params.linearIssueId,
49
+ ...(params.issueKey ? { issueKey: params.issueKey } : existing?.issueKey ? { issueKey: existing.issueKey } : {}),
50
+ ...(params.title ? { title: params.title } : existing?.title ? { title: existing.title } : {}),
51
+ ...(params.issueUrl ? { issueUrl: params.issueUrl } : existing?.issueUrl ? { issueUrl: existing.issueUrl } : {}),
52
+ ...(params.currentLinearState
53
+ ? { currentLinearState: params.currentLinearState }
54
+ : existing?.currentLinearState
55
+ ? { currentLinearState: existing.currentLinearState }
56
+ : {}),
57
+ lastWebhookAt: params.lastWebhookAt,
58
+ });
59
+ const existingIssueControl = this.authoritativeLedger.getIssueControl(params.projectId, params.linearIssueId);
60
+ const lifecycleStatus = existingIssueControl?.activeRunLeaseId || params.desiredStage
61
+ ? existing?.lifecycleStatus ?? "queued"
62
+ : existing?.lifecycleStatus ?? "idle";
63
+ const desiredReceiptId = this.resolveDesiredReceiptId({
64
+ projectId: params.projectId,
65
+ linearIssueId: params.linearIssueId,
66
+ ...(params.desiredWebhookId !== undefined ? { desiredWebhookId: params.desiredWebhookId } : {}),
67
+ ...(params.desiredReceiptId !== undefined ? { desiredReceiptId: params.desiredReceiptId } : {}),
68
+ });
69
+ this.authoritativeLedger.upsertIssueControl({
70
+ projectId: params.projectId,
71
+ linearIssueId: params.linearIssueId,
72
+ ...(params.desiredStage !== undefined ? { desiredStage: params.desiredStage } : {}),
73
+ ...(desiredReceiptId !== undefined ? { desiredReceiptId } : {}),
74
+ lifecycleStatus,
75
+ ...(existing?.statusCommentId ? { serviceOwnedCommentId: existing.statusCommentId } : {}),
76
+ ...(params.activeAgentSessionId !== undefined
77
+ ? { activeAgentSessionId: params.activeAgentSessionId }
78
+ : existing?.activeAgentSessionId
79
+ ? { activeAgentSessionId: existing.activeAgentSessionId }
80
+ : {}),
81
+ ...(existing?.activeWorkspaceId !== undefined ? { activeWorkspaceOwnershipId: existing.activeWorkspaceId } : {}),
82
+ ...(existingIssueControl?.activeRunLeaseId !== undefined ? { activeRunLeaseId: existingIssueControl.activeRunLeaseId } : {}),
83
+ });
84
+ return this.issueWorkflows.getTrackedIssue(params.projectId, params.linearIssueId);
85
+ }
86
+ claimStageRun(params) {
87
+ const transaction = this.connection.transaction(() => {
88
+ const issue = this.issueWorkflows.getTrackedIssue(params.projectId, params.linearIssueId);
89
+ const issueControl = this.authoritativeLedger.getIssueControl(params.projectId, params.linearIssueId);
90
+ if (!issue ||
91
+ !issueControl ||
92
+ issueControl.activeRunLeaseId !== undefined ||
93
+ issue.desiredStage !== params.stage ||
94
+ issue.desiredWebhookId !== params.triggerWebhookId) {
95
+ return undefined;
96
+ }
97
+ const workspaceOwnership = this.authoritativeLedger.upsertWorkspaceOwnership({
98
+ projectId: params.projectId,
99
+ linearIssueId: params.linearIssueId,
100
+ branchName: params.branchName,
101
+ worktreePath: params.worktreePath,
102
+ status: "active",
103
+ });
104
+ const runLease = this.authoritativeLedger.createRunLease({
105
+ issueControlId: issueControl.id,
106
+ projectId: params.projectId,
107
+ linearIssueId: params.linearIssueId,
108
+ workspaceOwnershipId: workspaceOwnership.id,
109
+ stage: params.stage,
110
+ status: "running",
111
+ workflowFile: params.workflowFile,
112
+ promptText: params.promptText,
113
+ triggerReceiptId: issueControl.desiredReceiptId ?? null,
114
+ });
115
+ this.authoritativeLedger.upsertWorkspaceOwnership({
116
+ projectId: params.projectId,
117
+ linearIssueId: params.linearIssueId,
118
+ branchName: params.branchName,
119
+ worktreePath: params.worktreePath,
120
+ status: "active",
121
+ currentRunLeaseId: runLease.id,
122
+ });
123
+ this.authoritativeLedger.upsertIssueControl({
124
+ projectId: params.projectId,
125
+ linearIssueId: params.linearIssueId,
126
+ desiredStage: null,
127
+ desiredReceiptId: null,
128
+ activeWorkspaceOwnershipId: workspaceOwnership.id,
129
+ activeRunLeaseId: runLease.id,
130
+ lifecycleStatus: "running",
131
+ ...(issue.statusCommentId ? { serviceOwnedCommentId: issue.statusCommentId } : {}),
132
+ ...(issue.activeAgentSessionId ? { activeAgentSessionId: issue.activeAgentSessionId } : {}),
133
+ });
134
+ const refreshedIssue = this.issueWorkflows.getTrackedIssue(params.projectId, params.linearIssueId);
135
+ const workspace = this.issueWorkflows.getWorkspace(workspaceOwnership.id);
136
+ const stageRun = this.issueWorkflows.getStageRun(runLease.id);
137
+ const pipeline = this.issueWorkflows.getPipelineRun(runLease.id);
138
+ return { issue: refreshedIssue, workspace, pipeline, stageRun };
139
+ });
140
+ return transaction();
141
+ }
142
+ updateStageRunThread(params) {
143
+ this.authoritativeLedger.updateRunLeaseThread({
144
+ runLeaseId: params.stageRunId,
145
+ threadId: params.threadId,
146
+ ...(params.parentThreadId !== undefined ? { parentThreadId: params.parentThreadId } : {}),
147
+ ...(params.turnId !== undefined ? { turnId: params.turnId } : {}),
148
+ });
149
+ const stageRun = this.issueWorkflows.getStageRun(params.stageRunId);
150
+ if (!stageRun) {
151
+ return;
152
+ }
153
+ const issue = this.issueWorkflows.getTrackedIssue(stageRun.projectId, stageRun.linearIssueId);
154
+ this.authoritativeLedger.upsertIssueSession({
155
+ projectId: stageRun.projectId,
156
+ linearIssueId: stageRun.linearIssueId,
157
+ workspaceOwnershipId: stageRun.workspaceId,
158
+ runLeaseId: stageRun.id,
159
+ threadId: params.threadId,
160
+ ...(params.parentThreadId !== undefined ? { parentThreadId: params.parentThreadId } : {}),
161
+ ...(issue?.activeAgentSessionId ? { linkedAgentSessionId: issue.activeAgentSessionId } : {}),
162
+ source: "stage_run",
163
+ });
164
+ }
165
+ finishStageRun(params) {
166
+ const stageRun = this.issueWorkflows.getStageRun(params.stageRunId);
167
+ if (!stageRun) {
168
+ return;
169
+ }
170
+ this.runReports.saveRunReport({
171
+ runLeaseId: params.stageRunId,
172
+ ...(params.summaryJson !== undefined ? { summaryJson: params.summaryJson } : {}),
173
+ ...(params.reportJson !== undefined ? { reportJson: params.reportJson } : {}),
174
+ });
175
+ this.authoritativeLedger.finishRunLease({
176
+ runLeaseId: params.stageRunId,
177
+ status: params.status === "failed" ? "failed" : "completed",
178
+ threadId: params.threadId,
179
+ ...(params.turnId !== undefined ? { turnId: params.turnId } : {}),
180
+ });
181
+ const runLease = this.authoritativeLedger.getRunLease(params.stageRunId);
182
+ if (runLease?.workspaceOwnershipId !== undefined) {
183
+ const issue = this.issueWorkflows.getTrackedIssue(stageRun.projectId, stageRun.linearIssueId);
184
+ this.authoritativeLedger.upsertIssueSession({
185
+ projectId: stageRun.projectId,
186
+ linearIssueId: stageRun.linearIssueId,
187
+ workspaceOwnershipId: runLease.workspaceOwnershipId,
188
+ runLeaseId: params.stageRunId,
189
+ threadId: params.threadId,
190
+ ...(runLease.parentThreadId ? { parentThreadId: runLease.parentThreadId } : {}),
191
+ ...(issue?.activeAgentSessionId ? { linkedAgentSessionId: issue.activeAgentSessionId } : {}),
192
+ source: "stage_run",
193
+ });
194
+ }
195
+ const workspace = this.authoritativeLedger.getWorkspaceOwnership(stageRun.workspaceId);
196
+ if (workspace) {
197
+ this.authoritativeLedger.upsertWorkspaceOwnership({
198
+ projectId: stageRun.projectId,
199
+ linearIssueId: stageRun.linearIssueId,
200
+ branchName: workspace.branchName,
201
+ worktreePath: workspace.worktreePath,
202
+ status: params.status === "completed" ? "active" : "paused",
203
+ currentRunLeaseId: null,
204
+ });
205
+ }
206
+ }
207
+ setIssueDesiredStage(projectId, linearIssueId, desiredStage, options) {
208
+ const existing = this.issueWorkflows.getTrackedIssue(projectId, linearIssueId);
209
+ const existingIssueControl = this.authoritativeLedger.getIssueControl(projectId, linearIssueId);
210
+ const desiredReceiptId = this.resolveDesiredReceiptId({
211
+ projectId,
212
+ linearIssueId,
213
+ ...(options?.desiredWebhookId !== undefined ? { desiredWebhookId: options.desiredWebhookId } : {}),
214
+ ...(options?.desiredReceiptId !== undefined ? { desiredReceiptId: options.desiredReceiptId } : {}),
215
+ });
216
+ this.authoritativeLedger.upsertIssueControl({
217
+ projectId,
218
+ linearIssueId,
219
+ ...(desiredStage !== undefined ? { desiredStage } : { desiredStage: null }),
220
+ ...(desiredReceiptId !== undefined
221
+ ? { desiredReceiptId }
222
+ : desiredStage === undefined
223
+ ? { desiredReceiptId: null }
224
+ : {}),
225
+ lifecycleStatus: options?.lifecycleStatus ??
226
+ (desiredStage ? "queued" : existingIssueControl?.activeRunLeaseId ? (existing?.lifecycleStatus ?? "idle") : "idle"),
227
+ ...(existing?.statusCommentId ? { serviceOwnedCommentId: existing.statusCommentId } : {}),
228
+ ...(existing?.activeAgentSessionId ? { activeAgentSessionId: existing.activeAgentSessionId } : {}),
229
+ ...(existing?.activeWorkspaceId !== undefined ? { activeWorkspaceOwnershipId: existing.activeWorkspaceId } : {}),
230
+ ...(existingIssueControl?.activeRunLeaseId !== undefined ? { activeRunLeaseId: existingIssueControl.activeRunLeaseId } : {}),
231
+ });
232
+ }
233
+ setIssueLifecycleStatus(projectId, linearIssueId, lifecycleStatus) {
234
+ const existing = this.issueWorkflows.getTrackedIssue(projectId, linearIssueId);
235
+ const existingIssueControl = this.authoritativeLedger.getIssueControl(projectId, linearIssueId);
236
+ this.authoritativeLedger.upsertIssueControl({
237
+ projectId,
238
+ linearIssueId,
239
+ lifecycleStatus,
240
+ ...(existing?.desiredStage ? { desiredStage: existing.desiredStage } : {}),
241
+ ...(existingIssueControl?.desiredReceiptId !== undefined ? { desiredReceiptId: existingIssueControl.desiredReceiptId } : {}),
242
+ ...(existing?.activeWorkspaceId !== undefined ? { activeWorkspaceOwnershipId: existing.activeWorkspaceId } : {}),
243
+ ...(existingIssueControl?.activeRunLeaseId !== undefined ? { activeRunLeaseId: existingIssueControl.activeRunLeaseId } : {}),
244
+ ...(existing?.statusCommentId ? { serviceOwnedCommentId: existing.statusCommentId } : {}),
245
+ ...(existing?.activeAgentSessionId ? { activeAgentSessionId: existing.activeAgentSessionId } : {}),
246
+ });
247
+ }
248
+ setIssueStatusComment(projectId, linearIssueId, statusCommentId) {
249
+ const existing = this.issueWorkflows.getTrackedIssue(projectId, linearIssueId);
250
+ const existingIssueControl = this.authoritativeLedger.getIssueControl(projectId, linearIssueId);
251
+ this.authoritativeLedger.upsertIssueControl({
252
+ projectId,
253
+ linearIssueId,
254
+ lifecycleStatus: existing?.lifecycleStatus ?? "idle",
255
+ ...(existing?.desiredStage ? { desiredStage: existing.desiredStage } : {}),
256
+ ...(existingIssueControl?.desiredReceiptId !== undefined ? { desiredReceiptId: existingIssueControl.desiredReceiptId } : {}),
257
+ ...(existing?.activeWorkspaceId !== undefined ? { activeWorkspaceOwnershipId: existing.activeWorkspaceId } : {}),
258
+ ...(existingIssueControl?.activeRunLeaseId !== undefined ? { activeRunLeaseId: existingIssueControl.activeRunLeaseId } : {}),
259
+ serviceOwnedCommentId: statusCommentId ?? null,
260
+ ...(existing?.activeAgentSessionId ? { activeAgentSessionId: existing.activeAgentSessionId } : {}),
261
+ });
262
+ }
263
+ setIssueActiveAgentSession(projectId, linearIssueId, agentSessionId) {
264
+ const existing = this.issueWorkflows.getTrackedIssue(projectId, linearIssueId);
265
+ const existingIssueControl = this.authoritativeLedger.getIssueControl(projectId, linearIssueId);
266
+ this.authoritativeLedger.upsertIssueControl({
267
+ projectId,
268
+ linearIssueId,
269
+ lifecycleStatus: existing?.lifecycleStatus ?? "idle",
270
+ ...(existing?.desiredStage ? { desiredStage: existing.desiredStage } : {}),
271
+ ...(existingIssueControl?.desiredReceiptId !== undefined ? { desiredReceiptId: existingIssueControl.desiredReceiptId } : {}),
272
+ ...(existing?.activeWorkspaceId !== undefined ? { activeWorkspaceOwnershipId: existing.activeWorkspaceId } : {}),
273
+ ...(existingIssueControl?.activeRunLeaseId !== undefined ? { activeRunLeaseId: existingIssueControl.activeRunLeaseId } : {}),
274
+ ...(existing?.statusCommentId ? { serviceOwnedCommentId: existing.statusCommentId } : {}),
275
+ activeAgentSessionId: agentSessionId ?? null,
276
+ });
277
+ }
278
+ resolveDesiredReceiptId(params) {
279
+ if (params.desiredReceiptId !== undefined) {
280
+ return params.desiredReceiptId;
281
+ }
282
+ if (params.desiredWebhookId === undefined) {
283
+ return undefined;
284
+ }
285
+ if (params.desiredWebhookId === null) {
286
+ return null;
287
+ }
288
+ return this.ensureDesiredReceipt(params.projectId, params.linearIssueId, params.desiredWebhookId);
289
+ }
290
+ ensureDesiredReceipt(projectId, linearIssueId, webhookId) {
291
+ const existing = this.connection
292
+ .prepare("SELECT id FROM event_receipts WHERE external_id = ? ORDER BY id DESC LIMIT 1")
293
+ .get(webhookId);
294
+ if (existing) {
295
+ return Number(existing.id);
296
+ }
297
+ const receipt = this.authoritativeLedger.insertEventReceipt({
298
+ source: "patchrelay-desired-stage",
299
+ externalId: webhookId,
300
+ eventType: "desired_stage",
301
+ receivedAt: isoNow(),
302
+ acceptanceStatus: "accepted",
303
+ projectId,
304
+ linearIssueId,
305
+ });
306
+ this.authoritativeLedger.markEventReceiptProcessed(receipt.id, "processed");
307
+ return receipt.id;
308
+ }
309
+ }