coxpit 5.3.1 โ 5.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.
- package/README.md +2 -0
- package/package.json +1 -1
- package/src/board.ts +14 -6
- package/src/cockpit.ts +125 -0
- package/src/db/index.ts +2 -0
- package/src/db/schema.ts +2 -0
- package/src/orchestrator.ts +183 -35
- package/src/server.ts +3 -0
package/README.md
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
<p align="center"><img src="docs/brand/og-lockup.png" alt="coxpit โ the cockpit for your agent fleet" width="640"></p>
|
|
2
2
|
|
|
3
|
+
> ๐ **Build log** ยท how this was built, on the [AIP Lab blog](https://aiplab.kr/blog/coxpit-fleet.html).
|
|
4
|
+
|
|
3
5
|
# Coxpit
|
|
4
6
|
|
|
5
7
|
**Own your agent fleet. Run parallel AI coding agents across your own machines โ steer them from any browser.**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "coxpit",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.4.0",
|
|
4
4
|
"description": "Self-hosted cockpit for running a fleet of AI coding agents across your own machines โ parallel worktree runs, live board, compare & merge, web terminal, design capture.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
package/src/board.ts
CHANGED
|
@@ -57,6 +57,9 @@ export const BOARD_HTML = /* html */ `<!doctype html>
|
|
|
57
57
|
.daemon-badge{font-family:var(--mono);font-size:10.5px;color:var(--faint);padding:3px 9px;
|
|
58
58
|
border:1px solid var(--line);border-radius:999px;background:var(--surface);cursor:default}
|
|
59
59
|
.daemon-badge b{color:var(--muted);font-weight:500}
|
|
60
|
+
.cockpit-link{text-decoration:none;color:var(--muted);font-family:var(--mono);font-size:11.5px;
|
|
61
|
+
border:1px solid var(--line);border-radius:7px;padding:5px 10px;display:inline-flex;align-items:center;gap:4px}
|
|
62
|
+
.cockpit-link:hover{color:var(--brand);border-color:var(--line-hi)}
|
|
60
63
|
.ws{display:flex;align-items:center;gap:7px;font-family:var(--mono);font-size:11px;color:var(--muted);
|
|
61
64
|
padding:4px 10px;border:1px solid var(--line);border-radius:999px;background:var(--surface)}
|
|
62
65
|
.dot{width:7px;height:7px;border-radius:50%;background:var(--s-failed);transition:background .3s}
|
|
@@ -842,7 +845,7 @@ export const BOARD_HTML = /* html */ `<!doctype html>
|
|
|
842
845
|
.fab{display:inline-flex}
|
|
843
846
|
#newBtn,.newnote{display:none}
|
|
844
847
|
header{padding:0 12px;gap:9px}
|
|
845
|
-
.brand .sub,.daemon-badge,.machines{display:none}
|
|
848
|
+
.brand .sub,.daemon-badge,.machines,.cockpit-link{display:none}
|
|
846
849
|
main{padding:14px}
|
|
847
850
|
.grid{grid-template-columns:1fr}
|
|
848
851
|
/* ๋ชจ๋ฌ = ํ๋ธ๋ฆฌ๋ */
|
|
@@ -879,6 +882,7 @@ ${ICON_SPRITE}
|
|
|
879
882
|
<div class="ws"><span class="dot" id="wsdot"></span><span id="wstext">connecting</span></div>
|
|
880
883
|
<button class="btn-ghost sm" id="bell" title="notify when a run settles"><svg class="ic"><use href="#i-bell-off"/></svg></button>
|
|
881
884
|
<button class="btn-ghost sm" id="remoteBtn" title="reach this daemon from elsewhere (Tailscale ยท recipes)"><svg class="ic"><use href="#i-external-link"/></svg></button>
|
|
885
|
+
<a class="cockpit-link" href="/cockpit" title="ํฐ๋ฏธ๋ ์ฐ์ ์
ธ (preview)">โจ Cockpit</a>
|
|
882
886
|
<div class="machines" id="machines"></div>
|
|
883
887
|
</header>
|
|
884
888
|
<div class="scrim" id="scrim"></div>
|
|
@@ -1801,6 +1805,10 @@ async function renderSettings(){
|
|
|
1801
1805
|
+ '<label class="set-row"><span class="set-lbl">Public URL '+lockNote(L.publicUrl,'COXPIT_PUBLIC_URL')+'</span>'
|
|
1802
1806
|
+ '<input id="setPublic" value="'+escA(ef.publicUrl||'')+'" placeholder="https://โฆ (deep-link base in notifications)"'+dis(L.publicUrl)+'></label>'
|
|
1803
1807
|
+ '</div>'
|
|
1808
|
+
+ '<div class="set-sec"><div class="set-h">Remote access</div>'
|
|
1809
|
+
+ '<div class="set-state">Reach this daemon from your other devices โ coxpit detects your Tailscale and drives it, or hands a copy-paste recipe. It never hosts a relay.</div>'
|
|
1810
|
+
+ '<div id="rmtSettings" class="rmt">checkingโฆ</div>'
|
|
1811
|
+
+ '</div>'
|
|
1804
1812
|
+ '<div class="set-sec"><div class="set-h">About</div>'
|
|
1805
1813
|
+ '<div class="set-kv"><span>version</span><b>'+esc(s.version)+'</b></div>'
|
|
1806
1814
|
+ '<div class="set-kv"><span>data folder</span><b>'+esc(s.dataDir)+'</b></div>'
|
|
@@ -1808,6 +1816,7 @@ async function renderSettings(){
|
|
|
1808
1816
|
+ '<div class="set-save"><button type="button" class="btn" id="setSave">Save settings</button>'
|
|
1809
1817
|
+ '<span class="set-saved" id="setSaved"></span></div>';
|
|
1810
1818
|
wireSettings();
|
|
1819
|
+
loadRemote(); // Remote access ์น์
(#rmtSettings) ์ฑ์ฐ๊ธฐ + ํ ๊ธ ๋ฐฐ์
|
|
1811
1820
|
}
|
|
1812
1821
|
function wireSettings(){
|
|
1813
1822
|
const save = $('setSave'); if (!save) return;
|
|
@@ -1989,10 +1998,9 @@ function paintOnboarding(goalsOnly){
|
|
|
1989
1998
|
+ '<li><b>Write a task</b> โ title + a prompt that names the target files</li>'
|
|
1990
1999
|
+ '<li><b>Run fleet</b> โ try <b>Dry run</b> first (free rehearsal), then <b>Real agent</b></li>'
|
|
1991
2000
|
+ '</ol></div>'
|
|
1992
|
-
+ '<div class="setup-sec"><p class="setup-label">Remote access</p>'
|
|
1993
|
-
+ '<div id="rmtOnboard" class="rmt">checking Tailscaleโฆ</div></div>'
|
|
1994
2001
|
+ '</div>';
|
|
1995
|
-
|
|
2002
|
+
// Remote access(Serve/Funnel)๋ ๋๋ค์ ์ฌ์ฉ์์๊ฒ ๋ถํ์ โ ์จ๋ณด๋ฉ์์ ์ ๊ฑฐํ๊ณ Settings ์์ ์์ธํ ๋ค๋ฃฌ๋ค.
|
|
2003
|
+
// (ํค๋ โคข ๋ฒํผ์ผ๋ก๋ ์ฌ์ ํ ์ ๊ทผ ๊ฐ๋ฅ)
|
|
1996
2004
|
}
|
|
1997
2005
|
function cardHTML(r){
|
|
1998
2006
|
const task = tasks.get(r.taskId);
|
|
@@ -3764,9 +3772,9 @@ async function loadRemote(){
|
|
|
3764
3772
|
function paintRemote(){
|
|
3765
3773
|
const html = remoteCardHTML(remoteData);
|
|
3766
3774
|
const ov = $('remoteBody'); if (ov) ov.innerHTML = html;
|
|
3767
|
-
const
|
|
3775
|
+
const st = $('rmtSettings'); if (st) st.innerHTML = html; // Settings ํ์ด์ง ์ธ๋ผ์ธ
|
|
3768
3776
|
wireRemote($('remoteOverlay'));
|
|
3769
|
-
wireRemote(document.getElementById('
|
|
3777
|
+
wireRemote(document.getElementById('setbox'));
|
|
3770
3778
|
}
|
|
3771
3779
|
|
|
3772
3780
|
function wireRemote(scope){
|
package/src/cockpit.ts
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
// Cockpit โ ํฐ๋ฏธ๋ ์ฐ์ ์
ธ (๋ณํ ๊ฐ๋ฐ์ฉ, /cockpit). board.ts ์ฒ๋ผ ์๊ฐ์๊ฒฐ ๋จ์ผ HTML(๋น๋ 0).
|
|
2
|
+
// ๋ฐฑ์๋(server ๋ผ์ฐํธยทterm.tsยทorchestrator)๋ ๋ณด๋์ ์ ๋ถ ๊ณต์ . Phase 5์์ ๋ฐ์คํฌํฑ ๊ธฐ๋ณธ์ ์ฌ๊ธฐ๋ก ํ๋ฆฝ.
|
|
3
|
+
// Phase 0 = ์ค์บํด๋(๋ ์ด์์ ๊ณจ๊ฒฉ + Board ํ ๊ธ + ๋ชจ๋ฐ์ผ ๋ณด๋ ๋ฆฌ๋ค์ด๋ ํธ). ํธ๋ฆฌยทํ์ธ ๊ทธ๋ฆฌ๋ยท์์ฒญ๋ฐ๋ Phase 2~3.
|
|
4
|
+
export const COCKPIT_HTML = /* html */ `<!doctype html>
|
|
5
|
+
<html lang="en">
|
|
6
|
+
<head>
|
|
7
|
+
<meta charset="utf-8" />
|
|
8
|
+
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
|
|
9
|
+
<title>coxpit ยท cockpit</title>
|
|
10
|
+
<link rel="icon" href="/brand/favicon.ico" sizes="any" />
|
|
11
|
+
<link rel="icon" type="image/png" sizes="32x32" href="/brand/favicon-32.png" />
|
|
12
|
+
<link rel="apple-touch-icon" href="/brand/apple-touch-icon.png" />
|
|
13
|
+
<style>
|
|
14
|
+
@font-face{font-family:'Pixelify';src:url('/brand/pixelify.woff2') format('woff2');font-weight:400 700;font-display:swap}
|
|
15
|
+
:root{
|
|
16
|
+
--bg:#0b0d12; --surface:#12151c; --surface2:#171b24; --panel:#0e1118;
|
|
17
|
+
--line:#222835; --line-hi:#2f3648;
|
|
18
|
+
--ink:#dee4ec; --muted:#8792a2; --faint:#5c6675;
|
|
19
|
+
--brand:#4ec9b0; --brand-ink:#062822; --brand-dim:rgba(78,201,176,.13);
|
|
20
|
+
--running:#55a7e0; --done:#58b368; --blocked:#d6a249; --failed:#e25b67;
|
|
21
|
+
--mono:ui-monospace,'SF Mono',SFMono-Regular,Menlo,Consolas,monospace;
|
|
22
|
+
--sans:-apple-system,BlinkMacSystemFont,'Segoe UI',system-ui,Roboto,sans-serif;
|
|
23
|
+
}
|
|
24
|
+
*{box-sizing:border-box}
|
|
25
|
+
html,body{height:100%}
|
|
26
|
+
body{margin:0;background:var(--bg);color:var(--ink);font-family:var(--sans);font-size:14px;line-height:1.5;
|
|
27
|
+
-webkit-font-smoothing:antialiased;overflow:hidden}
|
|
28
|
+
|
|
29
|
+
/* โโ topbar โโ */
|
|
30
|
+
header{display:flex;align-items:center;gap:13px;height:46px;padding:0 14px;
|
|
31
|
+
border-bottom:1px solid var(--line);background:rgba(18,21,28,.92);font-family:var(--mono)}
|
|
32
|
+
.brand{display:inline-flex;align-items:center;gap:8px;text-decoration:none}
|
|
33
|
+
.brand img{height:22px;width:auto;display:block}
|
|
34
|
+
.brand .wm{font-family:'Pixelify';font-weight:600;font-size:19px;color:var(--ink);letter-spacing:.01em;margin-left:-2px}
|
|
35
|
+
.mach{display:inline-flex;align-items:center;gap:7px;font-size:12px;color:var(--muted);border:1px solid var(--line);border-radius:7px;padding:4px 10px}
|
|
36
|
+
.mach .dot{width:6px;height:6px;border-radius:50%;background:var(--done)}
|
|
37
|
+
.vtabs{display:flex;gap:4px;margin-left:4px}
|
|
38
|
+
.vtab{font-size:12px;color:var(--muted);padding:6px 11px;border-radius:7px;display:inline-flex;align-items:center;gap:7px;cursor:pointer;background:none;border:none;font-family:var(--mono)}
|
|
39
|
+
.vtab.on{background:var(--brand-dim);color:var(--ink);box-shadow:inset 0 0 0 1px rgba(78,201,176,.28)}
|
|
40
|
+
.vtab .g{color:var(--brand)}
|
|
41
|
+
.right{margin-left:auto;display:flex;align-items:center;gap:9px}
|
|
42
|
+
.wip{font-size:10.5px;color:var(--blocked);border:1px solid rgba(214,162,73,.4);border-radius:999px;padding:2px 9px}
|
|
43
|
+
.toggle{font-size:12px;color:var(--muted);text-decoration:none;border:1px solid var(--line);border-radius:7px;padding:5px 11px}
|
|
44
|
+
.toggle:hover{color:var(--ink);border-color:var(--line-hi)}
|
|
45
|
+
|
|
46
|
+
/* โโ body: tree | main โโ */
|
|
47
|
+
.layout{display:grid;grid-template-columns:264px 1fr;height:calc(100vh - 46px)}
|
|
48
|
+
.rail{border-right:1px solid var(--line);overflow:auto;padding:12px 10px}
|
|
49
|
+
.lbl{font-family:var(--mono);font-size:10px;letter-spacing:.14em;text-transform:uppercase;color:var(--faint);padding:5px 8px 10px}
|
|
50
|
+
.skel{height:11px;border-radius:5px;background:linear-gradient(90deg,var(--surface) 0%,var(--surface2) 50%,var(--surface) 100%);
|
|
51
|
+
background-size:200% 100%;animation:sh 1.4s linear infinite;margin:9px 8px}
|
|
52
|
+
@keyframes sh{to{background-position:-200% 0}}
|
|
53
|
+
@media (prefers-reduced-motion:reduce){.skel{animation:none}}
|
|
54
|
+
|
|
55
|
+
.main{display:flex;flex-direction:column;min-width:0;background:var(--bg)}
|
|
56
|
+
.stage{flex:1;display:flex;align-items:center;justify-content:center;text-align:center;padding:24px}
|
|
57
|
+
.stage .card{max-width:460px}
|
|
58
|
+
.stage .glyph{font-family:var(--mono);font-size:26px;color:#2c3444;letter-spacing:5px;margin-bottom:14px}
|
|
59
|
+
.stage h1{font-family:var(--mono);font-size:17px;margin:0 0 8px;color:var(--ink);font-weight:700}
|
|
60
|
+
.stage p{color:var(--muted);font-size:13.5px;margin:0 0 6px;line-height:1.6}
|
|
61
|
+
.stage .steps{font-family:var(--mono);font-size:12px;color:var(--faint);margin-top:16px;text-align:left;
|
|
62
|
+
border:1px solid var(--line);border-radius:10px;background:var(--surface);padding:14px 16px;line-height:1.9}
|
|
63
|
+
.stage .steps b{color:var(--brand)}
|
|
64
|
+
.reqbar{display:flex;align-items:center;gap:10px;border-top:1px solid var(--line);background:var(--surface);padding:11px 14px;font-family:var(--mono)}
|
|
65
|
+
.reqbar .ctx{font-size:11px;color:var(--brand-ink);background:var(--brand);border-radius:6px;padding:3px 9px;font-weight:600}
|
|
66
|
+
.reqbar .caret{color:var(--brand)}
|
|
67
|
+
.reqbar .ph{color:var(--faint);font-size:13px;flex:1}
|
|
68
|
+
.reqbar .kbd{border:1px solid var(--line-hi);border-radius:5px;padding:1px 6px;color:var(--faint);font-size:11px}
|
|
69
|
+
</style>
|
|
70
|
+
</head>
|
|
71
|
+
<body>
|
|
72
|
+
<header>
|
|
73
|
+
<a class="brand" href="/"><img src="/brand/mark.png" alt="" /><span class="wm">coxpit</span></a>
|
|
74
|
+
<span class="mach"><span class="dot"></span><span id="mach">local</span></span>
|
|
75
|
+
<div class="vtabs">
|
|
76
|
+
<button type="button" class="vtab on"><span class="g">โ</span>Terminal</button>
|
|
77
|
+
<button type="button" class="vtab"><span class="g">โง</span>Review</button>
|
|
78
|
+
<button type="button" class="vtab"><span class="g">โค</span>Docs</button>
|
|
79
|
+
</div>
|
|
80
|
+
<div class="right">
|
|
81
|
+
<span class="wip">preview ยท Phase 0</span>
|
|
82
|
+
<a class="toggle" href="/">โ Board</a>
|
|
83
|
+
</div>
|
|
84
|
+
</header>
|
|
85
|
+
|
|
86
|
+
<div class="layout">
|
|
87
|
+
<aside class="rail">
|
|
88
|
+
<div class="lbl">Workspace</div>
|
|
89
|
+
<div class="skel" style="width:70%"></div>
|
|
90
|
+
<div class="skel" style="width:52%"></div>
|
|
91
|
+
<div class="skel" style="width:60%"></div>
|
|
92
|
+
<div class="skel" style="width:44%"></div>
|
|
93
|
+
</aside>
|
|
94
|
+
|
|
95
|
+
<main class="main">
|
|
96
|
+
<div class="stage">
|
|
97
|
+
<div class="card">
|
|
98
|
+
<div class="glyph">โ โ โ</div>
|
|
99
|
+
<h1>Cockpit โ ํฐ๋ฏธ๋ ์ฐ์ ์
ธ</h1>
|
|
100
|
+
<p>์ด ํ๋ฉด์ ์ ์
ธ์ ๊ณจ๊ฒฉ์
๋๋ค. ๋ฐฑ์๋๋ ๋ณด๋์ ๊ณต์ ํ๊ณ , ์ฌ๊ธฐ์ ํธ๋ฆฌยทํ์ธ ๊ทธ๋ฆฌ๋ ํฐ๋ฏธ๋ยท์์ฒญ๋ฐ๋ฅผ ๋จ๊ณ๋ก ์น์ต๋๋ค.</p>
|
|
101
|
+
<div class="steps">
|
|
102
|
+
<div><b>Phase 1</b> ยท tmux ์ฌ-adopt (์ฌ์์ ๋ด์ฑ ํ ๋)</div>
|
|
103
|
+
<div><b>Phase 2</b> ยท ์ํฌ์คํ์ด์ค ํธ๋ฆฌ + ํ์ธ ๊ทธ๋ฆฌ๋ ํฐ๋ฏธ๋</div>
|
|
104
|
+
<div><b>Phase 3</b> ยท ์์ฒญ๋ฐ ยท ํฌ์์ ํ์ผ ยท Review ยท ๋ธ๋ก๋์บ์คํธ</div>
|
|
105
|
+
<div><b>Phase 4</b> ยท verify in-loop</div>
|
|
106
|
+
</div>
|
|
107
|
+
</div>
|
|
108
|
+
</div>
|
|
109
|
+
<div class="reqbar">
|
|
110
|
+
<span class="ctx">cockpit</span><span class="caret">โบ</span>
|
|
111
|
+
<span class="ph">์์ฒญ๋ฐ โ Phase 3</span>
|
|
112
|
+
<span class="kbd">โ</span>
|
|
113
|
+
</div>
|
|
114
|
+
</main>
|
|
115
|
+
</div>
|
|
116
|
+
|
|
117
|
+
<script>
|
|
118
|
+
// ๋ชจ๋ฐ์ผ์ ํฐ๋ฏธ๋ ์ฐ์ ๋์ ๋ณด๋(๋ชจ๋ํฐ)๋ก โ Cockpit ์ ๋ฐ์คํฌํฑ ์ฐ์ .
|
|
119
|
+
// (๋ทฐํฌํธ ํญ + ํฐ์น ํํธ๋ก ํ์ โ screen.width ๋ ํค๋๋ฆฌ์ค/๋ฉํฐ๋ชจ๋ํฐ์ ๋ถ์ ํ)
|
|
120
|
+
if (window.innerWidth <= 860 && matchMedia('(pointer:coarse)').matches) location.replace('/');
|
|
121
|
+
// health ๋ก ๋จธ์ ๋ช
์ ๋๋ง ์ฑ์ด๋ค(์ค์บํด๋).
|
|
122
|
+
fetch('/api/health').then(function(r){return r.json()}).then(function(){}).catch(function(){});
|
|
123
|
+
</script>
|
|
124
|
+
</body>
|
|
125
|
+
</html>`;
|
package/src/db/index.ts
CHANGED
|
@@ -101,4 +101,6 @@ export async function ensureSchema(): Promise<void> {
|
|
|
101
101
|
try { await client.execute('ALTER TABLE tasks ADD COLUMN closed_at INTEGER'); } catch { /* exists */ }
|
|
102
102
|
try { await client.execute("ALTER TABLE task_groups ADD COLUMN coord_session_id TEXT NOT NULL DEFAULT ''"); } catch { /* exists */ }
|
|
103
103
|
try { await client.execute("ALTER TABLE tasks ADD COLUMN outputs TEXT NOT NULL DEFAULT '[]'"); } catch { /* exists */ }
|
|
104
|
+
try { await client.execute('ALTER TABLE agent_runs ADD COLUMN agent_pid INTEGER NOT NULL DEFAULT 0'); } catch { /* exists */ }
|
|
105
|
+
try { await client.execute('ALTER TABLE agent_runs ADD COLUMN log_offset INTEGER NOT NULL DEFAULT 0'); } catch { /* exists */ }
|
|
104
106
|
}
|
package/src/db/schema.ts
CHANGED
|
@@ -88,6 +88,8 @@ export const agentRuns = sqliteTable('agent_runs', {
|
|
|
88
88
|
prUrl: text('pr_url').notNull().default(''), // PR ๋ชจ๋๋ก ์ฌ๋ฆฐ pull request URL
|
|
89
89
|
model: text('model').notNull().default(''), // ๋ฐ์น๋ณ ๋ชจ๋ธ ์ง์ (๋น๊ฐ = CLI ๊ธฐ๋ณธ)
|
|
90
90
|
filesChanged: integer('files_changed').notNull().default(0),
|
|
91
|
+
agentPid: integer('agent_pid').notNull().default(0), // detached sh pgid โ ์ฌ์์ ํ ์์กด ํ์ /stop
|
|
92
|
+
logOffset: integer('log_offset').notNull().default(0), // ๋ด๊ตฌ ๋ก๊ทธ์์ tail ์ด ์๋นํ ๋ฐ์ดํธ(์ฌ-adopt ์ ์ฌ๊ธฐ๋ถํฐ)
|
|
91
93
|
startedAt: integer('started_at', { mode: 'timestamp' }),
|
|
92
94
|
endedAt: integer('ended_at', { mode: 'timestamp' }),
|
|
93
95
|
exitSummary: text('exit_summary').notNull().default(''),
|
package/src/orchestrator.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { randomBytes } from 'node:crypto';
|
|
2
2
|
import { posix as ppath } from 'node:path';
|
|
3
3
|
import { createInterface } from 'node:readline';
|
|
4
|
-
import { existsSync } from 'node:fs';
|
|
5
|
-
import { mkdir, copyFile, readFile, writeFile, rm } from 'node:fs/promises';
|
|
4
|
+
import { existsSync, statSync, openSync, readSync, closeSync, mkdirSync } from 'node:fs';
|
|
5
|
+
import { mkdir, copyFile, readFile, writeFile, rm, unlink } from 'node:fs/promises';
|
|
6
6
|
import { homedir } from 'node:os';
|
|
7
7
|
import type { ChildProcess } from 'node:child_process';
|
|
8
8
|
import { eq, inArray } from 'drizzle-orm';
|
|
@@ -111,6 +111,80 @@ async function setRun(runId: number, patch: Partial<typeof agentRuns.$inferInser
|
|
|
111
111
|
// ์คํ ์ค run ์ ์์ ํ๋ก์ธ์ค(stop ์ฉ). stoppedRuns = ์ฌ์ฉ์๊ฐ ๋ฉ์ถ run ํ์.
|
|
112
112
|
const liveChildren = new Map<number, ChildProcess>();
|
|
113
113
|
const stoppedRuns = new Set<number>();
|
|
114
|
+
// ์ฌ์์ ํ ์ฌ-adopt ๋์ด ๋ก๊ทธ๋ฅผ tail ์ค์ธ run(์์ ๊ฐ์ฒด๋ ์์ง๋ง ์ด์์์).
|
|
115
|
+
const adoptedRuns = new Set<number>();
|
|
116
|
+
|
|
117
|
+
const sleep = (ms: number) => new Promise<void>((r) => setTimeout(r, ms));
|
|
118
|
+
function pidAlive(pid: number): boolean {
|
|
119
|
+
if (!pid) return false;
|
|
120
|
+
try { process.kill(pid, 0); return true; } catch { return false; }
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// โโ ๋ด๊ตฌ ์คํ ๋ก๊ทธ(์ฌ์์ ๋ด์ฑ ํ ๋) โโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
124
|
+
// ์์ด์ ํธ ์ถ๋ ฅ์ ๋ฐ๋ชฌ ํ์ดํ๊ฐ ์๋๋ผ ์ด ํ์ผ๋ก ๋ฆฌ๋ค์ด๋ ํธ๋๋ค โ ๋ฐ๋ชฌ์ด ์ฃฝ์ด๋
|
|
125
|
+
// detached ์์ด์ ํธ๊ฐ ๊ณ์ ํ์ผ์ ์ฐ๊ณ , ๋ฐ๋ชฌ์ ๋ถํ
ํ offset ๋ถํฐ ์ฌ-tail ํ๋ค.
|
|
126
|
+
function runsDir(): string {
|
|
127
|
+
const d = ppath.join(ppath.dirname(config.dbPath), 'runs');
|
|
128
|
+
try { mkdirSync(d, { recursive: true }); } catch { /* best effort */ }
|
|
129
|
+
return d;
|
|
130
|
+
}
|
|
131
|
+
function runLogPath(runId: number): string { return ppath.join(runsDir(), `r${runId}.log`); }
|
|
132
|
+
const DONE_RE = /^__COXPIT_DONE__:(-?\d+)\s*$/;
|
|
133
|
+
/** ๋ก๊ทธ ํ์ผ tail โ offset ๋ถํฐ ์ ๋ฐ์ดํธ๋ฅผ ์ฝ์ด ๋ผ์ธ ํ์ฑโ์ด๋ฒคํธ, ์ข
๋ฃ ์ผํฐ๋ฌ(__COXPIT_DONE__) ๊ฐ์ง.
|
|
134
|
+
* aliveCheck ๊ฐ false ๋ฉด(์ผํฐ๋ฌ ์์ด ํ๋ก์ธ์ค ์ฌ๋ง) code=-1 ๋ก ์ข
๋ฃ. offset ์ ์ฃผ๊ธฐ์ ์ผ๋ก ์์. */
|
|
135
|
+
async function tailRunLog(
|
|
136
|
+
runId: number, provider: Provider, fromOffset: number,
|
|
137
|
+
aliveCheck?: () => boolean,
|
|
138
|
+
): Promise<{ code: number; lastResult: string; offset: number }> {
|
|
139
|
+
const log = runLogPath(runId);
|
|
140
|
+
let offset = Math.max(0, fromOffset);
|
|
141
|
+
let buf = '';
|
|
142
|
+
let lastResult = '';
|
|
143
|
+
let code = 0;
|
|
144
|
+
let done = false;
|
|
145
|
+
let sincePersist = 0;
|
|
146
|
+
const consume = () => {
|
|
147
|
+
if (!existsSync(log)) return;
|
|
148
|
+
const size = statSync(log).size;
|
|
149
|
+
if (size <= offset) return;
|
|
150
|
+
const fd = openSync(log, 'r');
|
|
151
|
+
try {
|
|
152
|
+
const b = Buffer.alloc(size - offset);
|
|
153
|
+
readSync(fd, b, 0, b.length, offset);
|
|
154
|
+
offset = size; buf += b.toString('utf8');
|
|
155
|
+
} finally { closeSync(fd); }
|
|
156
|
+
let nl: number;
|
|
157
|
+
while ((nl = buf.indexOf('\n')) >= 0) {
|
|
158
|
+
const line = buf.slice(0, nl); buf = buf.slice(nl + 1);
|
|
159
|
+
const m = DONE_RE.exec(line);
|
|
160
|
+
if (m) { code = parseInt(m[1] ?? '0', 10) || 0; done = true; continue; }
|
|
161
|
+
const p = provider.parseLine(line);
|
|
162
|
+
if (p) {
|
|
163
|
+
if (p.sessionId) void setRun(runId, { sessionId: p.sessionId });
|
|
164
|
+
if (p.resultText != null) lastResult = p.resultText;
|
|
165
|
+
void recordEvent(runId, p.kind, p.stored.slice(0, 2000));
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
// stdout+stderr ๋ฅผ ํ ๋ก๊ทธ๋ก ํฉ์ณค์ผ๋ฏ๋ก(2>&1), stream-json ์ด ์๋ non-json ๋ผ์ธ = stderr ๋ก ์ด๋ฆฐ๋ค.
|
|
169
|
+
const t = line.trim();
|
|
170
|
+
if (t && t[0] !== '{') void recordEvent(runId, 'stderr', t.slice(0, 2000));
|
|
171
|
+
}
|
|
172
|
+
};
|
|
173
|
+
while (!done) {
|
|
174
|
+
consume();
|
|
175
|
+
if (done) break;
|
|
176
|
+
if (aliveCheck && !aliveCheck()) { consume(); if (!done) { code = -1; } break; }
|
|
177
|
+
if (++sincePersist >= 8) { sincePersist = 0; void persistLogOffset(runId, offset); }
|
|
178
|
+
await sleep(250);
|
|
179
|
+
}
|
|
180
|
+
consume();
|
|
181
|
+
void persistLogOffset(runId, offset);
|
|
182
|
+
return { code, lastResult, offset };
|
|
183
|
+
}
|
|
184
|
+
/** offset ๋ง ๊ฐ๋ณ๊ฒ ์์(broadcast ์์ด โ setRun ์ ๋งค๋ฒ broadcast ๋ผ ๋ถ์ ํฉ). */
|
|
185
|
+
async function persistLogOffset(runId: number, offset: number): Promise<void> {
|
|
186
|
+
try { await db.update(agentRuns).set({ logOffset: offset }).where(eq(agentRuns.id, runId)); } catch { /* best effort */ }
|
|
187
|
+
}
|
|
114
188
|
|
|
115
189
|
// โโ ์์ด์ ํธ ์
ํ ์ค์ผ์คํธ๋ ์ด์
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
116
190
|
// run ๋ง๋ค 1ํ์ฉ ํ ํฐ์ ๋ฐ๊ธํด ์์ด์ ํธ env ๋ก ์ค๋ค. ์์ด์ ํธ๋ ๊ทธ ํ ํฐ์ผ๋ก
|
|
@@ -130,9 +204,9 @@ export function resolveAgentToken(token: string): number | null {
|
|
|
130
204
|
return agentTokens.get(token) ?? null;
|
|
131
205
|
}
|
|
132
206
|
|
|
133
|
-
/** run ์ด ์ง๊ธ ์ด์ ์๋๊ฐ(์์ ํ๋ก์ธ์ค ๋ณด์ ). aggregate ๋ทฐ์ live/steerable ํ์ ์ฉ. */
|
|
207
|
+
/** run ์ด ์ง๊ธ ์ด์ ์๋๊ฐ(์์ ํ๋ก์ธ์ค ๋ณด์ or ์ฌ-adopt tail ์ค). aggregate ๋ทฐ์ live/steerable ํ์ ์ฉ. */
|
|
134
208
|
export function isRunLive(runId: number): boolean {
|
|
135
|
-
return liveChildren.has(runId);
|
|
209
|
+
return liveChildren.has(runId) || adoptedRuns.has(runId);
|
|
136
210
|
}
|
|
137
211
|
|
|
138
212
|
/** ์์ด์ ํธ ํ๋กฌํํธ์ ๋ถ๋ ๋ฅ๋ ฅ ๊ณ ์ง โ ๋
๋ฆฝ ํ์์์
์ ๋ณ๋ ฌ ์๋ธ๋ฐ์ผ๋ก ๋บ ์ ์๋ค.
|
|
@@ -408,34 +482,48 @@ export async function launchRun(runId: number, real?: boolean): Promise<void> {
|
|
|
408
482
|
* ์์ด์ ํธ ์์ ํ๋ก์ธ์ค ๋ฐฐ์ (๊ณต์ฉ) โ ํ๋ก๋ฐ์ด๋๊ฐ stdout ๋ผ์ธ์ ์ ๊ทํ ์ด๋ฒคํธ๋ก
|
|
409
483
|
* ํ์ฑ, session ์บก์ฒ, ์ข
๋ฃ ์ files_changed ์ง๊ณ + ์ํ ์ ์ด. launchRun/steerRun ๊ณต์ .
|
|
410
484
|
*/
|
|
411
|
-
async function runAgentChild(runId: number, machine: MachineTarget, wtPath: string, cmd: string, provider: Provider): Promise<void> {
|
|
412
|
-
const
|
|
413
|
-
|
|
414
|
-
|
|
485
|
+
async function runAgentChild(runId: number, machine: MachineTarget, wtPath: string, cmd: string, provider: Provider, opts?: { fresh?: boolean }): Promise<void> {
|
|
486
|
+
const isRemote = machine.kind !== 'local' && machine.address !== '';
|
|
487
|
+
let code: number;
|
|
415
488
|
let lastResult = '';
|
|
416
|
-
if (child.stdout) {
|
|
417
|
-
const rl = createInterface({ input: child.stdout });
|
|
418
|
-
rl.on('line', (line: string) => {
|
|
419
|
-
const p = provider.parseLine(line);
|
|
420
|
-
if (!p) return;
|
|
421
|
-
if (p.sessionId) void setRun(runId, { sessionId: p.sessionId }); // steer(resume)์ฉ ์ธ์
ํค
|
|
422
|
-
if (p.resultText != null) lastResult = p.resultText;
|
|
423
|
-
void recordEvent(runId, p.kind, p.stored.slice(0, 2000));
|
|
424
|
-
});
|
|
425
|
-
}
|
|
426
|
-
if (child.stderr) {
|
|
427
|
-
const rle = createInterface({ input: child.stderr });
|
|
428
|
-
rle.on('line', (line: string) => {
|
|
429
|
-
const s = line.trim();
|
|
430
|
-
if (s) void recordEvent(runId, 'stderr', s.slice(0, 2000));
|
|
431
|
-
});
|
|
432
|
-
}
|
|
433
489
|
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
child
|
|
437
|
-
|
|
438
|
-
|
|
490
|
+
if (isRemote) {
|
|
491
|
+
// ์๊ฒฉ: ssh stdout ํ์ดํ๋ฅผ ์ค์๊ฐ ํ์ฑ(๊ธฐ์กด ๋์). ๋ก๊ทธ ํ์ผ/์ฌ-adopt ๋ ๋ก์ปฌ ์ ์ฉ.
|
|
492
|
+
const child = spawnShellOn(machine, cmd);
|
|
493
|
+
liveChildren.set(runId, child);
|
|
494
|
+
if (child.stdout) {
|
|
495
|
+
const rl = createInterface({ input: child.stdout });
|
|
496
|
+
rl.on('line', (line: string) => {
|
|
497
|
+
const p = provider.parseLine(line);
|
|
498
|
+
if (!p) return;
|
|
499
|
+
if (p.sessionId) void setRun(runId, { sessionId: p.sessionId });
|
|
500
|
+
if (p.resultText != null) lastResult = p.resultText;
|
|
501
|
+
void recordEvent(runId, p.kind, p.stored.slice(0, 2000));
|
|
502
|
+
});
|
|
503
|
+
}
|
|
504
|
+
if (child.stderr) {
|
|
505
|
+
const rle = createInterface({ input: child.stderr });
|
|
506
|
+
rle.on('line', (line: string) => { const s = line.trim(); if (s) void recordEvent(runId, 'stderr', s.slice(0, 2000)); });
|
|
507
|
+
}
|
|
508
|
+
code = await new Promise((resolve) => { child.on('close', (c) => resolve(c ?? 0)); child.on('error', () => resolve(-1)); });
|
|
509
|
+
liveChildren.delete(runId);
|
|
510
|
+
} else {
|
|
511
|
+
// ๋ก์ปฌ: ์์ด์ ํธ ์ถ๋ ฅ์ ๋ด๊ตฌ ๋ก๊ทธ๋ก ๋ฆฌ๋ค์ด๋ ํธ + ํ์ผ tail(์ฌ์์ ๋ด์ฑ). ์ข
๋ฃ๋ __COXPIT_DONE__ ์ผํฐ๋ฌ.
|
|
512
|
+
const fresh = opts?.fresh !== false; // launch=์ ๋ก๊ทธ / steer=์ด์ด์ฐ๊ธฐ
|
|
513
|
+
const log = runLogPath(runId);
|
|
514
|
+
const wrapped = `{ ${cmd} ; } ${fresh ? '>' : '>>'} ${shq(log)} 2>&1; printf '\\n__COXPIT_DONE__:%s\\n' "$?" >> ${shq(log)}`;
|
|
515
|
+
let fromOffset = 0;
|
|
516
|
+
if (fresh) { await setRun(runId, { logOffset: 0, agentPid: 0 }); }
|
|
517
|
+
else { const r = (await db.select({ o: agentRuns.logOffset }).from(agentRuns).where(eq(agentRuns.id, runId)).limit(1))[0]; fromOffset = r?.o ?? 0; }
|
|
518
|
+
const child = spawnShellOn(machine, wrapped);
|
|
519
|
+
liveChildren.set(runId, child);
|
|
520
|
+
child.on('error', () => { /* ํ์ดํ ๋ฏธ์ฌ์ฉ โ ๋ก๊ทธ tail ์ด ์ง์ค */ });
|
|
521
|
+
const pid = child.pid ?? 0;
|
|
522
|
+
if (pid) await setRun(runId, { agentPid: pid });
|
|
523
|
+
const r = await tailRunLog(runId, provider, fromOffset, () => pidAlive(pid));
|
|
524
|
+
code = r.code; lastResult = r.lastResult;
|
|
525
|
+
liveChildren.delete(runId);
|
|
526
|
+
}
|
|
439
527
|
|
|
440
528
|
const stat = await runShellOn(machine, `git -C ${shq(wtPath)} status --porcelain | wc -l`, 10000);
|
|
441
529
|
const filesChanged = stat.ok ? parseInt(stat.stdout.trim(), 10) || 0 : 0;
|
|
@@ -443,7 +531,7 @@ async function runAgentChild(runId: number, machine: MachineTarget, wtPath: stri
|
|
|
443
531
|
const wasStopped = stoppedRuns.delete(runId);
|
|
444
532
|
const status = wasStopped ? 'stopped' : code === 0 ? 'done' : 'failed';
|
|
445
533
|
const exitSummary = wasStopped ? 'stopped by user' : lastResult ? lastResult.slice(0, 500) : `exit ${code}`;
|
|
446
|
-
await setRun(runId, { status, endedAt: new Date(), filesChanged, exitSummary });
|
|
534
|
+
await setRun(runId, { status, endedAt: new Date(), filesChanged, exitSummary, agentPid: 0 });
|
|
447
535
|
// ๋ฌธ์ ์ฐ์ถ๋ฌผ์ ์ ์ฐฉ ์์ ์ ์ค๋
์ท โ worktree ์๋ฉธ(๋จธ์งยทClose) ํ์๋ ๋ ๋ ๋ทฐ ์ ์ง. best-effort.
|
|
448
536
|
if (filesChanged > 0) void snapshotRunDocs(runId);
|
|
449
537
|
void notifySettle(runId, status, filesChanged, exitSummary);
|
|
@@ -528,9 +616,9 @@ export async function steerRun(runId: number, message: string, mode: 'work' | 'a
|
|
|
528
616
|
const cmd = `cd ${shq(wt)} && ${envPrefix}${pidPrefix}{ ${resume}; }`;
|
|
529
617
|
if (!isRemote && config.agentOrch) {
|
|
530
618
|
const orchTimer = startOrchWatch(runId, wt, true);
|
|
531
|
-
void runAgentChild(runId, ctx.machine, wt, cmd, provider).finally(() => clearInterval(orchTimer));
|
|
619
|
+
void runAgentChild(runId, ctx.machine, wt, cmd, provider, { fresh: false }).finally(() => clearInterval(orchTimer));
|
|
532
620
|
} else {
|
|
533
|
-
void runAgentChild(runId, ctx.machine, wt, cmd, provider);
|
|
621
|
+
void runAgentChild(runId, ctx.machine, wt, cmd, provider, { fresh: false });
|
|
534
622
|
}
|
|
535
623
|
return { ok: true, detail: 'steering' };
|
|
536
624
|
}
|
|
@@ -554,6 +642,12 @@ export async function stopRun(runId: number): Promise<{ ok: boolean; detail: str
|
|
|
554
642
|
// stop ์์ฒญ์ ์ ์ฐ์ผ๋ก ์ฒ๋ฆฌํด ์นด๋๊ฐ ์์ํ '์งํ ์ค'์ผ๋ก ๋จ์ง ์๊ฒ ํ๋ค.
|
|
555
643
|
const zr = (await db.select().from(agentRuns).where(eq(agentRuns.id, runId)).limit(1))[0];
|
|
556
644
|
if (zr && (zr.status === 'running' || zr.status === 'starting')) {
|
|
645
|
+
// ์ฌ-adopt ๋์ด tail ์ค์ธ ์ด์์๋ ๋ก์ปฌ ์์ด์ ํธ โ ํ๋ก์ธ์ค ๊ทธ๋ฃน ์ข
๋ฃ. tailer ๊ฐ stopped ๋ก ์ ์ฐ.
|
|
646
|
+
if (zr.agentPid && pidAlive(zr.agentPid)) {
|
|
647
|
+
stoppedRuns.add(runId);
|
|
648
|
+
try { process.kill(-zr.agentPid, 'SIGTERM'); } catch { try { process.kill(zr.agentPid, 'SIGTERM'); } catch { /* gone */ } }
|
|
649
|
+
return { ok: true, detail: 'stopping (re-adopted agent)' };
|
|
650
|
+
}
|
|
557
651
|
await setRun(runId, { status: 'stopped', endedAt: new Date(), exitSummary: 'orphaned (daemon restarted) โ settled by stop' });
|
|
558
652
|
await recordEvent(runId, 'meta', JSON.stringify({ orphanSettled: true }));
|
|
559
653
|
return { ok: true, detail: 'no live process โ settled as stopped' };
|
|
@@ -1446,15 +1540,67 @@ export async function listDocuments(): Promise<{ runs: Array<{
|
|
|
1446
1540
|
* ๋ถํ
์ ์ฐ โ ๋ฐ๋ชฌ ์ฌ์์ ํ ์ด์์๋ ์์์ด ์์ ์ ์๋๋ฐ DB ๊ฐ running/starting ์ธ
|
|
1447
1541
|
* run(๊ณ ์)์ failed ๋ก ์ ๋ฆฌํ๋ค. workbench('open')๋ ์์ด์ ํธ๊ฐ ์์ผ๋ฏ๋ก ๋์ ์๋.
|
|
1448
1542
|
*/
|
|
1543
|
+
/** ์ฌ-adopt: ์ด์์๋ ๋ก์ปฌ run ์ ๋ก๊ทธ๋ฅผ offset ๋ถํฐ ์ด์ด tail โ ์๋ฃ ์ ์ ์ฐ. status ๋ running ์ ์ง. */
|
|
1544
|
+
async function reAdoptRun(run: typeof agentRuns.$inferSelect): Promise<void> {
|
|
1545
|
+
adoptedRuns.add(run.id);
|
|
1546
|
+
await recordEvent(run.id, 'meta', JSON.stringify({ reAdopted: true }));
|
|
1547
|
+
const ctx = await loadContext(run.id);
|
|
1548
|
+
const provider = getProvider(run.agent || 'claude-code');
|
|
1549
|
+
const machine: MachineTarget = ctx?.machine ?? { slug: 'local', kind: 'local', address: '', sshUser: '' };
|
|
1550
|
+
const wasStopped0 = stoppedRuns.has(run.id);
|
|
1551
|
+
const { code, lastResult } = await tailRunLog(run.id, provider, run.logOffset ?? 0, () => pidAlive(run.agentPid));
|
|
1552
|
+
adoptedRuns.delete(run.id);
|
|
1553
|
+
const stat = await runShellOn(machine, `git -C ${shq(run.worktreePath)} status --porcelain | wc -l`, 10000);
|
|
1554
|
+
const filesChanged = stat.ok ? parseInt(stat.stdout.trim(), 10) || 0 : 0;
|
|
1555
|
+
const wasStopped = stoppedRuns.delete(run.id) || wasStopped0;
|
|
1556
|
+
const status = wasStopped ? 'stopped' : code === 0 ? 'done' : 'failed';
|
|
1557
|
+
const exitSummary = wasStopped ? 'stopped by user' : lastResult ? lastResult.slice(0, 500) : `exit ${code}`;
|
|
1558
|
+
await setRun(run.id, { status, endedAt: new Date(), filesChanged, exitSummary, agentPid: 0 });
|
|
1559
|
+
if (filesChanged > 0) void snapshotRunDocs(run.id);
|
|
1560
|
+
void notifySettle(run.id, status, filesChanged, exitSummary);
|
|
1561
|
+
}
|
|
1562
|
+
|
|
1563
|
+
/**
|
|
1564
|
+
* ๋ถํ
์ ์ด์ ์ธ์คํด์ค์ ๋ผ์ด๋ธ run ์ ์ฐ โ ์ด์ ๋ฌด์กฐ๊ฑด failed ๊ฐ ์๋๋ผ:
|
|
1565
|
+
* โ ์์ด์ ํธ ํ๋ก์ธ์ค(agentPid)๊ฐ ์ด์์์ผ๋ฉด ๋ก๊ทธ๋ฅผ ์ด์ด tail(์ฌ-adopt) โ running ์ ์ง
|
|
1566
|
+
* โก ์ด๋ฏธ ๋ก๊ทธ์ __COXPIT_DONE__ ์ด ์๊ฑฐ๋ ํ๋ก์ธ์ค ์ฃฝ์ โ ๊ทธ ๊ฒฐ๊ณผ๋ก ์ ์ฐ
|
|
1567
|
+
* โข ๊ทธ ์ธ(์๊ฒฉยทpid ์์) โ orphaned failed (๊ธฐ์กด ๋์, worktree/diff ๋ณด์กด)
|
|
1568
|
+
* ๋ฐํ = ์ฌ-adopt ๋ run ์(์ ์ฐ๋ ๊ณ ์๋ ๋ฏธํฌํจ).
|
|
1569
|
+
*/
|
|
1449
1570
|
export async function reconcileOrphanRuns(): Promise<number> {
|
|
1450
1571
|
const stale = (await db.select().from(agentRuns)).filter(
|
|
1451
1572
|
(r) => r.status === 'running' || r.status === 'starting',
|
|
1452
1573
|
);
|
|
1574
|
+
let adopted = 0;
|
|
1453
1575
|
for (const r of stale) {
|
|
1454
|
-
|
|
1576
|
+
if (r.agentPid && pidAlive(r.agentPid)) {
|
|
1577
|
+
// ์์ง ์ด์์๋ ๋ก์ปฌ ์์ด์ ํธ โ ์ด์ด๋ฐ๋๋ค(๋ฐฑ๊ทธ๋ผ์ด๋ tail).
|
|
1578
|
+
void reAdoptRun(r);
|
|
1579
|
+
adopted++;
|
|
1580
|
+
continue;
|
|
1581
|
+
}
|
|
1582
|
+
// ์ฃฝ์์ง๋ง ๋ก๊ทธ์ ์๋ฃ ์ผํฐ๋ฌ์ด ์์ผ๋ฉด ๊ทธ ๊ฒฐ๊ณผ๋ก ์ ์ฐ(๋ค์ดํ์ ์ค ๋๋ ๊ฒฝ์ฐ).
|
|
1583
|
+
const log = runLogPath(r.id);
|
|
1584
|
+
let doneCode: number | null = null;
|
|
1585
|
+
try {
|
|
1586
|
+
if (existsSync(log)) {
|
|
1587
|
+
const tail = (await readFile(log, 'utf8')).slice(-4000);
|
|
1588
|
+
const m = /__COXPIT_DONE__:(-?\d+)/.exec(tail);
|
|
1589
|
+
if (m) doneCode = parseInt(m[1] ?? '0', 10) || 0;
|
|
1590
|
+
}
|
|
1591
|
+
} catch { /* ignore */ }
|
|
1592
|
+
if (doneCode !== null) {
|
|
1593
|
+
const stat = await runShellOn({ slug: 'local', kind: 'local', address: '', sshUser: '' }, `git -C ${shq(r.worktreePath)} status --porcelain | wc -l`, 10000);
|
|
1594
|
+
const filesChanged = stat.ok ? parseInt(stat.stdout.trim(), 10) || 0 : 0;
|
|
1595
|
+
const status = doneCode === 0 ? 'done' : 'failed';
|
|
1596
|
+
await setRun(r.id, { status, endedAt: new Date(), filesChanged, exitSummary: `settled on restart (exit ${doneCode})`, agentPid: 0 });
|
|
1597
|
+
if (filesChanged > 0) void snapshotRunDocs(r.id);
|
|
1598
|
+
continue;
|
|
1599
|
+
}
|
|
1600
|
+
await setRun(r.id, { status: 'failed', endedAt: new Date(), exitSummary: 'orphaned by daemon restart', agentPid: 0 });
|
|
1455
1601
|
await recordEvent(r.id, 'error', 'daemon restarted while this run was live โ settled as failed (worktree/branch preserved; diff still reviewable)');
|
|
1456
1602
|
}
|
|
1457
|
-
return
|
|
1603
|
+
return adopted;
|
|
1458
1604
|
}
|
|
1459
1605
|
|
|
1460
1606
|
/**
|
|
@@ -1614,6 +1760,8 @@ export async function cleanupRun(runId: number): Promise<{ ok: boolean; detail:
|
|
|
1614
1760
|
if (!ctx || !run || !run.worktreePath) return { ok: false, detail: 'no worktree' };
|
|
1615
1761
|
// worktree ๋ฅผ ์ง์ฐ๊ธฐ ์ ์ ๋ฌธ์ ์ค๋
์ท(์ ์ฐฉ ์ ํ๋ ์ํฌ๋ฒค์นยท์์ ํธ์ง๋ ํฌ์ฐฉ). best-effort.
|
|
1616
1762
|
await snapshotRunDocs(runId).catch(() => { /* ์ค๋
์ท ์คํจ๋ ์ ๋ฆฌ๋ฅผ ๋ง์ง ์์ */ });
|
|
1763
|
+
// ๋ด๊ตฌ ์คํ ๋ก๊ทธ๋ ์ ๋ฆฌ(๋ฌดํ ์ฆ๊ฐ ๋ฐฉ์ง). best-effort. (์๋ ์ง์ญ๋ณ์ rm ๊ณผ ์ด๋ฆ ์ถฉ๋ ํํผ๋ก unlink ์ฌ์ฉ)
|
|
1764
|
+
try { await unlink(runLogPath(runId)); } catch { /* gone */ }
|
|
1617
1765
|
// ์๊ฒฉ์ ์์กด ์์ด์ ํธ๊ฐ ์์ผ๋ฉด worktree ์ ๊ฑฐ ์ ์ ์ฃฝ์ธ๋ค(ํ์ผ ์ ๊ธยท์ข๋น ๋ฐฉ์ง).
|
|
1618
1766
|
if (ctx.machine.kind !== 'local' && ctx.machine.address !== '') {
|
|
1619
1767
|
await runShellOn(ctx.machine, remoteKillScript(run.worktreePath), 15000);
|
package/src/server.ts
CHANGED
|
@@ -25,6 +25,7 @@ import { addSink, removeSink, broadcast } from './hub';
|
|
|
25
25
|
import { getProvider, listProviders } from './providers';
|
|
26
26
|
import { remoteState, setServe, setFunnel } from './remote';
|
|
27
27
|
import { BOARD_HTML } from './board';
|
|
28
|
+
import { COCKPIT_HTML } from './cockpit';
|
|
28
29
|
|
|
29
30
|
const require_ = createRequire(import.meta.url);
|
|
30
31
|
|
|
@@ -203,6 +204,8 @@ export async function buildServer(): Promise<FastifyInstance> {
|
|
|
203
204
|
|
|
204
205
|
// ํ๋ฆฟ ๋ณด๋(๋จ์ผ ํ์ด์ง). ์ธ์ฆ ๊ฒ์ดํธ ์ ์ฉ๋จ(๋ฌด์ธ์ฆ ์์ฒญ์ ๊ฒ์ดํธ๊ฐ login/setup ํ์ด์ง๋ก ์๋ต).
|
|
205
206
|
app.get('/', async (_req, reply) => reply.type('text/html').send(BOARD_HTML));
|
|
207
|
+
// ํฐ๋ฏธ๋ ์ฐ์ ์
ธ(๋ณํ ๊ฐ๋ฐ) โ ๋ฐฑ์๋๋ ๋ณด๋์ ๊ณต์ . Phase 5์์ ๋ฐ์คํฌํฑ ๊ธฐ๋ณธ์ ์ฌ๊ธฐ๋ก ํ๋ฆฝ ์์ .
|
|
208
|
+
app.get('/cockpit', async (_req, reply) => reply.type('text/html').send(COCKPIT_HTML));
|
|
206
209
|
|
|
207
210
|
// โโโ ์ ๊ทผํค ์ธ์ฆ(access-key) โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
208
211
|
// ์์ฒญ์ด tunnel/https ๋ฅผ ํ๋ โ Secure ์ฟ ํค ์ฌ๋ถ ๊ฒฐ์ ์ฉ.
|