steroids-cli 0.10.34 → 0.10.36
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/commands/loop-phases.d.ts +3 -2
- package/dist/commands/loop-phases.d.ts.map +1 -1
- package/dist/commands/loop-phases.js +146 -50
- package/dist/commands/loop-phases.js.map +1 -1
- package/dist/commands/loop.d.ts.map +1 -1
- package/dist/commands/loop.js +21 -3
- package/dist/commands/loop.js.map +1 -1
- package/dist/commands/tasks-reset.d.ts.map +1 -1
- package/dist/commands/tasks-reset.js +6 -0
- package/dist/commands/tasks-reset.js.map +1 -1
- package/dist/commands/workspaces.d.ts.map +1 -1
- package/dist/commands/workspaces.js +71 -1
- package/dist/commands/workspaces.js.map +1 -1
- package/dist/database/queries.d.ts +15 -1
- package/dist/database/queries.d.ts.map +1 -1
- package/dist/database/queries.js +43 -0
- package/dist/database/queries.js.map +1 -1
- package/dist/database/schema.d.ts +2 -2
- package/dist/database/schema.d.ts.map +1 -1
- package/dist/database/schema.js +4 -0
- package/dist/database/schema.js.map +1 -1
- package/dist/providers/codex.d.ts +6 -0
- package/dist/providers/codex.d.ts.map +1 -1
- package/dist/providers/codex.js +40 -14
- package/dist/providers/codex.js.map +1 -1
- package/dist/runners/global-db-connection.d.ts.map +1 -1
- package/dist/runners/global-db-connection.js +6 -0
- package/dist/runners/global-db-connection.js.map +1 -1
- package/dist/runners/global-db-schema.d.ts +3 -1
- package/dist/runners/global-db-schema.d.ts.map +1 -1
- package/dist/runners/global-db-schema.js +39 -2
- package/dist/runners/global-db-schema.js.map +1 -1
- package/dist/runners/orchestrator-loop.d.ts.map +1 -1
- package/dist/runners/orchestrator-loop.js +114 -6
- package/dist/runners/orchestrator-loop.js.map +1 -1
- package/dist/runners/wakeup.d.ts.map +1 -1
- package/dist/runners/wakeup.js +14 -0
- package/dist/runners/wakeup.js.map +1 -1
- package/dist/workspace/git-helpers.d.ts +54 -0
- package/dist/workspace/git-helpers.d.ts.map +1 -0
- package/dist/workspace/git-helpers.js +136 -0
- package/dist/workspace/git-helpers.js.map +1 -0
- package/dist/workspace/git-lifecycle.d.ts +53 -0
- package/dist/workspace/git-lifecycle.d.ts.map +1 -0
- package/dist/workspace/git-lifecycle.js +287 -0
- package/dist/workspace/git-lifecycle.js.map +1 -0
- package/dist/workspace/merge-lock.d.ts +25 -0
- package/dist/workspace/merge-lock.d.ts.map +1 -0
- package/dist/workspace/merge-lock.js +93 -0
- package/dist/workspace/merge-lock.js.map +1 -0
- package/dist/workspace/merge-pipeline.d.ts +21 -0
- package/dist/workspace/merge-pipeline.d.ts.map +1 -0
- package/dist/workspace/merge-pipeline.js +55 -0
- package/dist/workspace/merge-pipeline.js.map +1 -0
- package/dist/workspace/pool.d.ts +59 -0
- package/dist/workspace/pool.d.ts.map +1 -0
- package/dist/workspace/pool.js +247 -0
- package/dist/workspace/pool.js.map +1 -0
- package/dist/workspace/reconcile.d.ts +20 -0
- package/dist/workspace/reconcile.d.ts.map +1 -0
- package/dist/workspace/reconcile.js +50 -0
- package/dist/workspace/reconcile.js.map +1 -0
- package/dist/workspace/types.d.ts +27 -0
- package/dist/workspace/types.d.ts.map +1 -0
- package/dist/workspace/types.js +6 -0
- package/dist/workspace/types.js.map +1 -0
- package/migrations/020_add_workspace_fields.sql +10 -0
- package/migrations/manifest.json +9 -1
- package/package.json +1 -1
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Workspace pool manager — DB-backed slot claiming, release, and clone management.
|
|
4
|
+
*
|
|
5
|
+
* Pool slots live in the global database (`workspace_pool_slots`).
|
|
6
|
+
* Each slot is a full git clone at `~/.steroids/workspaces/<projectHash>/pool-<index>/`.
|
|
7
|
+
*/
|
|
8
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
+
exports.resolveRemoteUrl = resolveRemoteUrl;
|
|
10
|
+
exports.claimSlot = claimSlot;
|
|
11
|
+
exports.finalizeSlotPath = finalizeSlotPath;
|
|
12
|
+
exports.releaseSlot = releaseSlot;
|
|
13
|
+
exports.partialReleaseSlot = partialReleaseSlot;
|
|
14
|
+
exports.updateSlotStatus = updateSlotStatus;
|
|
15
|
+
exports.refreshSlotHeartbeat = refreshSlotHeartbeat;
|
|
16
|
+
exports.getSlot = getSlot;
|
|
17
|
+
exports.listProjectSlots = listProjectSlots;
|
|
18
|
+
exports.ensureSlotClone = ensureSlotClone;
|
|
19
|
+
const node_child_process_1 = require("node:child_process");
|
|
20
|
+
const node_fs_1 = require("node:fs");
|
|
21
|
+
const node_path_1 = require("node:path");
|
|
22
|
+
const git_helpers_js_1 = require("./git-helpers.js");
|
|
23
|
+
const clone_js_1 = require("../parallel/clone.js");
|
|
24
|
+
/**
|
|
25
|
+
* Resolve the remote URL for a project.
|
|
26
|
+
* Returns null for local-only projects (no remote or local filesystem path).
|
|
27
|
+
*/
|
|
28
|
+
function resolveRemoteUrl(projectPath) {
|
|
29
|
+
try {
|
|
30
|
+
const url = (0, node_child_process_1.execFileSync)('git', ['remote', 'get-url', 'origin'], {
|
|
31
|
+
cwd: projectPath,
|
|
32
|
+
encoding: 'utf-8',
|
|
33
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
34
|
+
}).trim();
|
|
35
|
+
if (!url)
|
|
36
|
+
return null;
|
|
37
|
+
// If it's a local filesystem path (not https://, ssh://, git@), treat as local-only
|
|
38
|
+
if (url.startsWith('/') || url.startsWith('.') || url.startsWith('~')) {
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
return url;
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Build the slot path for a given project and index.
|
|
49
|
+
*/
|
|
50
|
+
function buildSlotPath(projectPath, slotIndex) {
|
|
51
|
+
const workspaceRoot = (0, clone_js_1.getDefaultWorkspaceRoot)();
|
|
52
|
+
const projectHash = (0, clone_js_1.getProjectHash)(projectPath);
|
|
53
|
+
return (0, node_path_1.join)(workspaceRoot, projectHash, `pool-${slotIndex}`);
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Claim a pool slot for a task. Creates a new slot if none are idle.
|
|
57
|
+
* Uses BEGIN IMMEDIATE for serialised access.
|
|
58
|
+
*/
|
|
59
|
+
function claimSlot(globalDb, projectId, runnerId, taskId) {
|
|
60
|
+
const now = Date.now();
|
|
61
|
+
const claim = globalDb.transaction(() => {
|
|
62
|
+
// Try to find an idle slot
|
|
63
|
+
let slot = globalDb
|
|
64
|
+
.prepare(`SELECT * FROM workspace_pool_slots
|
|
65
|
+
WHERE project_id = ? AND status = 'idle'
|
|
66
|
+
LIMIT 1`)
|
|
67
|
+
.get(projectId);
|
|
68
|
+
if (slot) {
|
|
69
|
+
globalDb
|
|
70
|
+
.prepare(`UPDATE workspace_pool_slots
|
|
71
|
+
SET runner_id = ?, task_id = ?, status = 'coder_active',
|
|
72
|
+
claimed_at = ?, heartbeat_at = ?
|
|
73
|
+
WHERE id = ?`)
|
|
74
|
+
.run(runnerId, taskId, now, now, slot.id);
|
|
75
|
+
return globalDb
|
|
76
|
+
.prepare('SELECT * FROM workspace_pool_slots WHERE id = ?')
|
|
77
|
+
.get(slot.id);
|
|
78
|
+
}
|
|
79
|
+
// No idle slot — create a new one
|
|
80
|
+
const maxIndex = globalDb
|
|
81
|
+
.prepare('SELECT MAX(slot_index) as max_idx FROM workspace_pool_slots WHERE project_id = ?')
|
|
82
|
+
.get(projectId);
|
|
83
|
+
const nextIndex = (maxIndex?.max_idx ?? -1) + 1;
|
|
84
|
+
try {
|
|
85
|
+
globalDb
|
|
86
|
+
.prepare(`INSERT INTO workspace_pool_slots
|
|
87
|
+
(project_id, slot_index, slot_path, runner_id, task_id, status, claimed_at, heartbeat_at)
|
|
88
|
+
VALUES (?, ?, ?, ?, ?, 'coder_active', ?, ?)`)
|
|
89
|
+
.run(projectId, nextIndex, '', // slot_path set after we know the project path
|
|
90
|
+
runnerId, taskId, now, now);
|
|
91
|
+
}
|
|
92
|
+
catch (error) {
|
|
93
|
+
if (error.message?.includes('UNIQUE constraint')) {
|
|
94
|
+
// Race condition: another runner created a slot — retry idle search
|
|
95
|
+
slot = globalDb
|
|
96
|
+
.prepare(`SELECT * FROM workspace_pool_slots
|
|
97
|
+
WHERE project_id = ? AND status = 'idle'
|
|
98
|
+
LIMIT 1`)
|
|
99
|
+
.get(projectId);
|
|
100
|
+
if (slot) {
|
|
101
|
+
globalDb
|
|
102
|
+
.prepare(`UPDATE workspace_pool_slots
|
|
103
|
+
SET runner_id = ?, task_id = ?, status = 'coder_active',
|
|
104
|
+
claimed_at = ?, heartbeat_at = ?
|
|
105
|
+
WHERE id = ?`)
|
|
106
|
+
.run(runnerId, taskId, now, now, slot.id);
|
|
107
|
+
return globalDb
|
|
108
|
+
.prepare('SELECT * FROM workspace_pool_slots WHERE id = ?')
|
|
109
|
+
.get(slot.id);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
throw error;
|
|
113
|
+
}
|
|
114
|
+
const newSlotId = globalDb
|
|
115
|
+
.prepare(`SELECT id FROM workspace_pool_slots
|
|
116
|
+
WHERE project_id = ? AND slot_index = ?`)
|
|
117
|
+
.get(projectId, nextIndex);
|
|
118
|
+
return globalDb
|
|
119
|
+
.prepare('SELECT * FROM workspace_pool_slots WHERE id = ?')
|
|
120
|
+
.get(newSlotId.id);
|
|
121
|
+
});
|
|
122
|
+
return claim.immediate();
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Finalize the slot path and remote_url after claiming. Call once after claimSlot()
|
|
126
|
+
* when you know the projectPath.
|
|
127
|
+
*/
|
|
128
|
+
function finalizeSlotPath(globalDb, slotId, projectPath, remoteUrl) {
|
|
129
|
+
const slotPath = buildSlotPath(projectPath, getSlot(globalDb, slotId).slot_index);
|
|
130
|
+
globalDb
|
|
131
|
+
.prepare(`UPDATE workspace_pool_slots
|
|
132
|
+
SET slot_path = ?, remote_url = ?
|
|
133
|
+
WHERE id = ?`)
|
|
134
|
+
.run(slotPath, remoteUrl, slotId);
|
|
135
|
+
return globalDb
|
|
136
|
+
.prepare('SELECT * FROM workspace_pool_slots WHERE id = ?')
|
|
137
|
+
.get(slotId);
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Release a slot back to idle, clearing task fields.
|
|
141
|
+
*/
|
|
142
|
+
function releaseSlot(globalDb, slotId) {
|
|
143
|
+
globalDb
|
|
144
|
+
.prepare(`UPDATE workspace_pool_slots
|
|
145
|
+
SET status = 'idle', runner_id = NULL, task_id = NULL,
|
|
146
|
+
task_branch = NULL, starting_sha = NULL,
|
|
147
|
+
claimed_at = NULL, heartbeat_at = NULL
|
|
148
|
+
WHERE id = ?`)
|
|
149
|
+
.run(slotId);
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Partially release a slot back to idle, preserving workspace fields
|
|
153
|
+
* (task_branch, base_branch, starting_sha) so the reviewer phase can pick
|
|
154
|
+
* them up in the next loop iteration without needing to re-run prepareForTask.
|
|
155
|
+
*
|
|
156
|
+
* Only clears runner-tracking fields (runner_id, claimed_at, heartbeat_at).
|
|
157
|
+
*/
|
|
158
|
+
function partialReleaseSlot(globalDb, slotId) {
|
|
159
|
+
globalDb
|
|
160
|
+
.prepare(`UPDATE workspace_pool_slots
|
|
161
|
+
SET status = 'idle', runner_id = NULL,
|
|
162
|
+
claimed_at = NULL, heartbeat_at = NULL
|
|
163
|
+
WHERE id = ?`)
|
|
164
|
+
.run(slotId);
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* Update slot status and optional fields.
|
|
168
|
+
*/
|
|
169
|
+
function updateSlotStatus(globalDb, slotId, status, fields) {
|
|
170
|
+
const sets = ['status = ?'];
|
|
171
|
+
const params = [status];
|
|
172
|
+
if (fields?.task_id !== undefined) {
|
|
173
|
+
sets.push('task_id = ?');
|
|
174
|
+
params.push(fields.task_id);
|
|
175
|
+
}
|
|
176
|
+
if (fields?.task_branch !== undefined) {
|
|
177
|
+
sets.push('task_branch = ?');
|
|
178
|
+
params.push(fields.task_branch);
|
|
179
|
+
}
|
|
180
|
+
if (fields?.base_branch !== undefined) {
|
|
181
|
+
sets.push('base_branch = ?');
|
|
182
|
+
params.push(fields.base_branch);
|
|
183
|
+
}
|
|
184
|
+
if (fields?.starting_sha !== undefined) {
|
|
185
|
+
sets.push('starting_sha = ?');
|
|
186
|
+
params.push(fields.starting_sha);
|
|
187
|
+
}
|
|
188
|
+
params.push(slotId);
|
|
189
|
+
globalDb
|
|
190
|
+
.prepare(`UPDATE workspace_pool_slots SET ${sets.join(', ')} WHERE id = ?`)
|
|
191
|
+
.run(...params);
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* Refresh the heartbeat timestamp for a slot.
|
|
195
|
+
*/
|
|
196
|
+
function refreshSlotHeartbeat(globalDb, slotId) {
|
|
197
|
+
globalDb
|
|
198
|
+
.prepare('UPDATE workspace_pool_slots SET heartbeat_at = ? WHERE id = ?')
|
|
199
|
+
.run(Date.now(), slotId);
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* Get a single slot by ID.
|
|
203
|
+
*/
|
|
204
|
+
function getSlot(globalDb, slotId) {
|
|
205
|
+
return (globalDb
|
|
206
|
+
.prepare('SELECT * FROM workspace_pool_slots WHERE id = ?')
|
|
207
|
+
.get(slotId) ?? null);
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* List all slots for a project.
|
|
211
|
+
*/
|
|
212
|
+
function listProjectSlots(globalDb, projectId) {
|
|
213
|
+
return globalDb
|
|
214
|
+
.prepare('SELECT * FROM workspace_pool_slots WHERE project_id = ? ORDER BY slot_index')
|
|
215
|
+
.all(projectId);
|
|
216
|
+
}
|
|
217
|
+
/**
|
|
218
|
+
* Ensure the slot directory exists as a full clone.
|
|
219
|
+
* - If missing: full clone from remoteUrl (or projectPath for local-only).
|
|
220
|
+
* - If shallow: `git fetch --unshallow`.
|
|
221
|
+
* - Always ensure .steroids symlink.
|
|
222
|
+
*/
|
|
223
|
+
function ensureSlotClone(slot, remoteUrl, projectPath) {
|
|
224
|
+
const slotPath = slot.slot_path;
|
|
225
|
+
if (!(0, node_fs_1.existsSync)(slotPath) || !(0, node_fs_1.existsSync)((0, node_path_1.join)(slotPath, '.git'))) {
|
|
226
|
+
// Remove any partial directory
|
|
227
|
+
if ((0, node_fs_1.existsSync)(slotPath)) {
|
|
228
|
+
(0, node_fs_1.rmSync)(slotPath, { recursive: true, force: true });
|
|
229
|
+
}
|
|
230
|
+
// Create parent directories
|
|
231
|
+
(0, node_fs_1.mkdirSync)((0, node_path_1.resolve)(slotPath, '..'), { recursive: true });
|
|
232
|
+
// Full clone — no --depth, no --single-branch
|
|
233
|
+
const cloneSource = remoteUrl ?? projectPath;
|
|
234
|
+
(0, node_child_process_1.execFileSync)('git', ['clone', '--no-tags', cloneSource, slotPath], {
|
|
235
|
+
cwd: process.cwd(),
|
|
236
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
237
|
+
timeout: 300_000, // 5 min for large repos
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
// If shallow, unshallow it
|
|
241
|
+
if ((0, git_helpers_js_1.isShallowRepository)(slotPath)) {
|
|
242
|
+
(0, git_helpers_js_1.execGit)(slotPath, ['fetch', '--unshallow'], { timeoutMs: 300_000 });
|
|
243
|
+
}
|
|
244
|
+
// Ensure .steroids symlink points to the source project
|
|
245
|
+
(0, clone_js_1.ensureWorkspaceSteroidsSymlink)(slotPath, projectPath);
|
|
246
|
+
}
|
|
247
|
+
//# sourceMappingURL=pool.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pool.js","sourceRoot":"","sources":["../../src/workspace/pool.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Workspace pool reconciliation — reclaim stale slots and locks.
|
|
3
|
+
*
|
|
4
|
+
* Called during runner wakeup to reset state after crashes.
|
|
5
|
+
* Policy: Stale = reset. No state-dependent recovery.
|
|
6
|
+
*/
|
|
7
|
+
import type Database from 'better-sqlite3';
|
|
8
|
+
export interface ReconcileResult {
|
|
9
|
+
resetSlots: number;
|
|
10
|
+
deletedLocks: number;
|
|
11
|
+
taskIds: string[];
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Find and reset stale workspace pool slots and merge locks.
|
|
15
|
+
*
|
|
16
|
+
* Step 1: Slots with heartbeat_at < now - 5min AND status != 'idle' → reset to idle
|
|
17
|
+
* Step 2: Merge locks with heartbeat_at < now - 3min → delete
|
|
18
|
+
*/
|
|
19
|
+
export declare function reconcileStaleWorkspaces(globalDb: Database.Database): ReconcileResult;
|
|
20
|
+
//# sourceMappingURL=reconcile.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"reconcile.d.ts","sourceRoot":"","sources":["../../src/workspace/reconcile.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,QAAQ,MAAM,gBAAgB,CAAC;AAM3C,MAAM,WAAW,eAAe;IAC9B,UAAU,EAAE,MAAM,CAAC;IACnB,YAAY,EAAE,MAAM,CAAC;IACrB,OAAO,EAAE,MAAM,EAAE,CAAC;CACnB;AAED;;;;;GAKG;AACH,wBAAgB,wBAAwB,CAAC,QAAQ,EAAE,QAAQ,CAAC,QAAQ,GAAG,eAAe,CAyCrF"}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Workspace pool reconciliation — reclaim stale slots and locks.
|
|
4
|
+
*
|
|
5
|
+
* Called during runner wakeup to reset state after crashes.
|
|
6
|
+
* Policy: Stale = reset. No state-dependent recovery.
|
|
7
|
+
*/
|
|
8
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
+
exports.reconcileStaleWorkspaces = reconcileStaleWorkspaces;
|
|
10
|
+
const STALE_SLOT_TTL_MS = 10 * 60 * 1000; // 10 minutes — must exceed clone timeout (5min) + event loop block
|
|
11
|
+
const STALE_LOCK_TTL_MS = 10 * 60 * 1000; // 10 minutes — must exceed worst-case push retry (3×120s = 360s)
|
|
12
|
+
/**
|
|
13
|
+
* Find and reset stale workspace pool slots and merge locks.
|
|
14
|
+
*
|
|
15
|
+
* Step 1: Slots with heartbeat_at < now - 5min AND status != 'idle' → reset to idle
|
|
16
|
+
* Step 2: Merge locks with heartbeat_at < now - 3min → delete
|
|
17
|
+
*/
|
|
18
|
+
function reconcileStaleWorkspaces(globalDb) {
|
|
19
|
+
const now = Date.now();
|
|
20
|
+
const slotCutoff = now - STALE_SLOT_TTL_MS;
|
|
21
|
+
const lockCutoff = now - STALE_LOCK_TTL_MS;
|
|
22
|
+
// Step 1: Find stale slots
|
|
23
|
+
const staleSlots = globalDb
|
|
24
|
+
.prepare(`SELECT id, task_id FROM workspace_pool_slots
|
|
25
|
+
WHERE heartbeat_at < ? AND status != 'idle'`)
|
|
26
|
+
.all(slotCutoff);
|
|
27
|
+
const taskIds = [];
|
|
28
|
+
for (const slot of staleSlots) {
|
|
29
|
+
globalDb
|
|
30
|
+
.prepare(`UPDATE workspace_pool_slots
|
|
31
|
+
SET status = 'idle', runner_id = NULL, task_id = NULL,
|
|
32
|
+
task_branch = NULL, starting_sha = NULL,
|
|
33
|
+
claimed_at = NULL, heartbeat_at = NULL
|
|
34
|
+
WHERE id = ?`)
|
|
35
|
+
.run(slot.id);
|
|
36
|
+
if (slot.task_id) {
|
|
37
|
+
taskIds.push(slot.task_id);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
// Step 2: Delete stale merge locks
|
|
41
|
+
const lockResult = globalDb
|
|
42
|
+
.prepare('DELETE FROM workspace_merge_locks WHERE heartbeat_at < ?')
|
|
43
|
+
.run(lockCutoff);
|
|
44
|
+
return {
|
|
45
|
+
resetSlots: staleSlots.length,
|
|
46
|
+
deletedLocks: lockResult.changes,
|
|
47
|
+
taskIds,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
//# sourceMappingURL=reconcile.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"reconcile.js","sourceRoot":"","sources":["../../src/workspace/reconcile.ts"],"names":[],"mappings":";AAAA;;;;;GAKG;;AAoBH,4DAyCC;AAxDD,MAAM,iBAAiB,GAAG,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC,CAAC,mEAAmE;AAC7G,MAAM,iBAAiB,GAAG,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC,CAAC,iEAAiE;AAQ3G;;;;;GAKG;AACH,SAAgB,wBAAwB,CAAC,QAA2B;IAClE,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;IACvB,MAAM,UAAU,GAAG,GAAG,GAAG,iBAAiB,CAAC;IAC3C,MAAM,UAAU,GAAG,GAAG,GAAG,iBAAiB,CAAC;IAE3C,2BAA2B;IAC3B,MAAM,UAAU,GAAG,QAAQ;SACxB,OAAO,CACN;mDAC6C,CAC9C;SACA,GAAG,CAAC,UAAU,CAA4C,CAAC;IAE9D,MAAM,OAAO,GAAa,EAAE,CAAC;IAE7B,KAAK,MAAM,IAAI,IAAI,UAAU,EAAE,CAAC;QAC9B,QAAQ;aACL,OAAO,CACN;;;;sBAIc,CACf;aACA,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;QAEhB,IAAI,IAAI,CAAC,OAAO,EAAE,CAAC;YACjB,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;QAC7B,CAAC;IACH,CAAC;IAED,mCAAmC;IACnC,MAAM,UAAU,GAAG,QAAQ;SACxB,OAAO,CAAC,0DAA0D,CAAC;SACnE,GAAG,CAAC,UAAU,CAAC,CAAC;IAEnB,OAAO;QACL,UAAU,EAAE,UAAU,CAAC,MAAM;QAC7B,YAAY,EAAE,UAAU,CAAC,OAAO;QAChC,OAAO;KACR,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Workspace pool types — mirrors the workspace_pool_slots DB table.
|
|
3
|
+
*/
|
|
4
|
+
import type Database from 'better-sqlite3';
|
|
5
|
+
export type SlotStatus = 'idle' | 'coder_active' | 'awaiting_review' | 'review_active' | 'merging';
|
|
6
|
+
export interface PoolSlot {
|
|
7
|
+
id: number;
|
|
8
|
+
project_id: string;
|
|
9
|
+
slot_index: number;
|
|
10
|
+
slot_path: string;
|
|
11
|
+
remote_url: string | null;
|
|
12
|
+
runner_id: string | null;
|
|
13
|
+
task_id: string | null;
|
|
14
|
+
base_branch: string | null;
|
|
15
|
+
task_branch: string | null;
|
|
16
|
+
starting_sha: string | null;
|
|
17
|
+
status: SlotStatus;
|
|
18
|
+
claimed_at: number | null;
|
|
19
|
+
heartbeat_at: number | null;
|
|
20
|
+
}
|
|
21
|
+
export interface PoolSlotContext {
|
|
22
|
+
globalDb: Database.Database;
|
|
23
|
+
slot: PoolSlot;
|
|
24
|
+
heartbeatTimer: ReturnType<typeof setInterval> | null;
|
|
25
|
+
localOnly: boolean;
|
|
26
|
+
}
|
|
27
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/workspace/types.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,QAAQ,MAAM,gBAAgB,CAAC;AAE3C,MAAM,MAAM,UAAU,GAClB,MAAM,GACN,cAAc,GACd,iBAAiB,GACjB,eAAe,GACf,SAAS,CAAC;AAEd,MAAM,WAAW,QAAQ;IACvB,EAAE,EAAE,MAAM,CAAC;IACX,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,MAAM,EAAE,UAAU,CAAC;IACnB,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;CAC7B;AAED,MAAM,WAAW,eAAe;IAC9B,QAAQ,EAAE,QAAQ,CAAC,QAAQ,CAAC;IAC5B,IAAI,EAAE,QAAQ,CAAC;IACf,cAAc,EAAE,UAAU,CAAC,OAAO,WAAW,CAAC,GAAG,IAAI,CAAC;IACtD,SAAS,EAAE,OAAO,CAAC;CACpB"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/workspace/types.ts"],"names":[],"mappings":";AAAA;;GAEG"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
-- UP
|
|
2
|
+
-- Add workspace pool fields to tasks table
|
|
3
|
+
ALTER TABLE tasks ADD COLUMN conflict_count INTEGER NOT NULL DEFAULT 0;
|
|
4
|
+
ALTER TABLE tasks ADD COLUMN blocked_reason TEXT;
|
|
5
|
+
CREATE INDEX IF NOT EXISTS idx_tasks_conflict_count ON tasks(conflict_count) WHERE conflict_count > 0;
|
|
6
|
+
|
|
7
|
+
-- DOWN
|
|
8
|
+
-- SQLite doesn't support DROP COLUMN in older versions
|
|
9
|
+
-- ALTER TABLE tasks DROP COLUMN conflict_count;
|
|
10
|
+
-- ALTER TABLE tasks DROP COLUMN blocked_reason;
|
package/migrations/manifest.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": "0.2.1",
|
|
3
|
-
"latestDbVersion":
|
|
3
|
+
"latestDbVersion": 20,
|
|
4
4
|
"migrations": [
|
|
5
5
|
{
|
|
6
6
|
"id": 1,
|
|
@@ -153,6 +153,14 @@
|
|
|
153
153
|
"description": "Add start_commit_sha to tasks table to support End-State Squashing",
|
|
154
154
|
"checksum": "",
|
|
155
155
|
"cliVersion": "0.9.43"
|
|
156
|
+
},
|
|
157
|
+
{
|
|
158
|
+
"id": 20,
|
|
159
|
+
"name": "020_add_workspace_fields",
|
|
160
|
+
"file": "020_add_workspace_fields.sql",
|
|
161
|
+
"description": "Add conflict_count and blocked_reason to tasks for workspace pool lifecycle",
|
|
162
|
+
"checksum": "",
|
|
163
|
+
"cliVersion": "0.10.36"
|
|
156
164
|
}
|
|
157
165
|
]
|
|
158
166
|
}
|