fraim-hub 2.0.221 → 2.0.223
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/src/ai-hub/cli.js
CHANGED
|
@@ -45,6 +45,7 @@ const child_process_1 = require("child_process");
|
|
|
45
45
|
const fs_1 = __importDefault(require("fs"));
|
|
46
46
|
const net_1 = __importDefault(require("net"));
|
|
47
47
|
const http_1 = __importDefault(require("http"));
|
|
48
|
+
const tree_kill_1 = __importDefault(require("tree-kill"));
|
|
48
49
|
const hub_launch_decision_1 = require("./hub-launch-decision");
|
|
49
50
|
const hub_runtime_file_1 = require("./hub-runtime-file");
|
|
50
51
|
const project_fraim_paths_1 = require("../core/utils/project-fraim-paths");
|
|
@@ -116,16 +117,58 @@ function isProcessAlive(pid) {
|
|
|
116
117
|
function killPid(pid) {
|
|
117
118
|
try {
|
|
118
119
|
if (process.platform === 'win32') {
|
|
120
|
+
// /T kills the entire process tree; /F forces termination without prompting.
|
|
119
121
|
(0, child_process_1.execFileSync)('taskkill', ['/PID', String(pid), '/T', '/F'], { stdio: 'ignore' });
|
|
120
122
|
}
|
|
121
123
|
else {
|
|
122
|
-
|
|
124
|
+
// tree-kill sends SIGTERM to the process group so child Electron helper processes
|
|
125
|
+
// are also terminated, not just the root node process (#921).
|
|
126
|
+
(0, tree_kill_1.default)(pid, 'SIGTERM');
|
|
123
127
|
}
|
|
124
128
|
}
|
|
125
129
|
catch {
|
|
126
130
|
/* already gone */
|
|
127
131
|
}
|
|
128
132
|
}
|
|
133
|
+
// #921: after killing the registered pid, probe ports 43091-43200 for any Hub HTTP
|
|
134
|
+
// response. Any Hub found on those ports that is NOT the already-killed pid is an
|
|
135
|
+
// orphan (e.g. an older version started via bare `npx` that never wrote hub-runtime.json).
|
|
136
|
+
// GET /api/ai-hub/pid identifies the pid; we then kill it and wait for its port to free.
|
|
137
|
+
async function fetchHubPid(port) {
|
|
138
|
+
return new Promise((resolve) => {
|
|
139
|
+
const req = http_1.default.get({ host: '127.0.0.1', port, path: '/api/ai-hub/pid', timeout: 1000 }, (res) => {
|
|
140
|
+
if (res.statusCode !== 200) {
|
|
141
|
+
res.resume();
|
|
142
|
+
return resolve(null);
|
|
143
|
+
}
|
|
144
|
+
let body = '';
|
|
145
|
+
res.on('data', (c) => { body += c; });
|
|
146
|
+
res.on('end', () => { try {
|
|
147
|
+
resolve(JSON.parse(body).pid ?? null);
|
|
148
|
+
}
|
|
149
|
+
catch {
|
|
150
|
+
resolve(null);
|
|
151
|
+
} });
|
|
152
|
+
});
|
|
153
|
+
req.on('error', () => resolve(null));
|
|
154
|
+
req.on('timeout', () => { req.destroy(); resolve(null); });
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
async function scanAndKillOrphanHubs(excludePid) {
|
|
158
|
+
const HUB_PORT_SCAN_START = 43091;
|
|
159
|
+
const HUB_PORT_SCAN_END = 43200;
|
|
160
|
+
for (let port = HUB_PORT_SCAN_START; port <= HUB_PORT_SCAN_END; port++) {
|
|
161
|
+
const version = await fetchRunningHubVersion(port);
|
|
162
|
+
if (!version)
|
|
163
|
+
continue; // port not a Hub
|
|
164
|
+
const pid = await fetchHubPid(port);
|
|
165
|
+
if (pid === null || pid === excludePid)
|
|
166
|
+
continue; // same process we already killed
|
|
167
|
+
console.log(`Killing orphan Hub (pid ${pid}) on port ${port}`);
|
|
168
|
+
killPid(pid);
|
|
169
|
+
await waitForPortFree(port);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
129
172
|
function isPortFree(port, timeoutMs = 500) {
|
|
130
173
|
return new Promise((resolve) => {
|
|
131
174
|
const sock = net_1.default.connect({ host: '127.0.0.1', port }, () => { sock.destroy(); resolve(false); });
|
|
@@ -177,6 +220,9 @@ async function reconcileRunningHub(flags) {
|
|
|
177
220
|
console.log(`Replacing running Hub (v${effective.version}, pid ${effective.pid}) - ${decision.reason}`);
|
|
178
221
|
killPid(effective.pid);
|
|
179
222
|
await waitForPortFree(effective.port);
|
|
223
|
+
// #921: after killing the registered pid, scan for orphan Hub processes that were
|
|
224
|
+
// never registered in hub-runtime.json (e.g. older versions started via bare npx).
|
|
225
|
+
await scanAndKillOrphanHubs(effective.pid);
|
|
180
226
|
}
|
|
181
227
|
else if (decision.action === 'focus-existing' && effective) {
|
|
182
228
|
console.log(`A Hub (v${effective.version}) is already running - focusing it. Use --restart to replace it.`);
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.hubInstallCommand = void 0;
|
|
7
|
+
exports.resolveHubInstallDir = resolveHubInstallDir;
|
|
8
|
+
exports.resolveShortcutTarget = resolveShortcutTarget;
|
|
9
|
+
exports.getOsShortcutPaths = getOsShortcutPaths;
|
|
10
|
+
exports.writeMacosLaunchAgent = writeMacosLaunchAgent;
|
|
11
|
+
exports.writeLinuxDesktopEntry = writeLinuxDesktopEntry;
|
|
12
|
+
exports.runHubInstall = runHubInstall;
|
|
13
|
+
// #921: `fraim hub install` — register the FRAIM Hub as an OS application so users
|
|
14
|
+
// can launch it without a terminal or npx command.
|
|
15
|
+
//
|
|
16
|
+
// This is an interim mitigation. Full packaged installers (electron-builder, code
|
|
17
|
+
// signing, .msi/.dmg) are a separate effort requiring signing infrastructure.
|
|
18
|
+
const commander_1 = require("commander");
|
|
19
|
+
const fs_1 = __importDefault(require("fs"));
|
|
20
|
+
const path_1 = __importDefault(require("path"));
|
|
21
|
+
const os_1 = __importDefault(require("os"));
|
|
22
|
+
const child_process_1 = require("child_process");
|
|
23
|
+
const project_fraim_paths_1 = require("../core/utils/project-fraim-paths");
|
|
24
|
+
// Stable install directory under ~/.fraim/bin/fraim-hub-electron/
|
|
25
|
+
function resolveHubInstallDir() {
|
|
26
|
+
return path_1.default.join((0, project_fraim_paths_1.getUserFraimDirPath)(), 'bin', 'fraim-hub-electron');
|
|
27
|
+
}
|
|
28
|
+
// The binary that OS shortcuts should point at. On Windows this is the Electron .exe;
|
|
29
|
+
// on POSIX systems it is the unpacked Electron binary or a shell wrapper.
|
|
30
|
+
function resolveShortcutTarget() {
|
|
31
|
+
const installDir = resolveHubInstallDir();
|
|
32
|
+
if (process.platform === 'win32') {
|
|
33
|
+
return path_1.default.join(installDir, 'fraim-hub.exe');
|
|
34
|
+
}
|
|
35
|
+
if (process.platform === 'darwin') {
|
|
36
|
+
return path_1.default.join(installDir, 'FRAIM Hub.app', 'Contents', 'MacOS', 'FRAIM Hub');
|
|
37
|
+
}
|
|
38
|
+
return path_1.default.join(installDir, 'fraim-hub');
|
|
39
|
+
}
|
|
40
|
+
// Platform-correct locations for OS launcher entries.
|
|
41
|
+
function getOsShortcutPaths(home) {
|
|
42
|
+
if (process.platform === 'win32') {
|
|
43
|
+
const appData = process.env.APPDATA || path_1.default.join(home, 'AppData', 'Roaming');
|
|
44
|
+
return {
|
|
45
|
+
startMenu: path_1.default.join(appData, 'Microsoft', 'Windows', 'Start Menu', 'Programs', 'FRAIM Hub.lnk'),
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
if (process.platform === 'darwin') {
|
|
49
|
+
return {
|
|
50
|
+
launchAgent: path_1.default.join(home, 'Library', 'LaunchAgents', 'ai.fraim.hub.plist'),
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
return {
|
|
54
|
+
desktopEntry: path_1.default.join(home, '.local', 'share', 'applications', 'fraim-hub.desktop'),
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
function writeMacosLaunchAgent(plistPath, target) {
|
|
58
|
+
const content = `<?xml version="1.0" encoding="UTF-8"?>
|
|
59
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
60
|
+
<plist version="1.0">
|
|
61
|
+
<dict>
|
|
62
|
+
<key>Label</key>
|
|
63
|
+
<string>ai.fraim.hub</string>
|
|
64
|
+
<key>ProgramArguments</key>
|
|
65
|
+
<array>
|
|
66
|
+
<string>${target}</string>
|
|
67
|
+
<string>--no-open</string>
|
|
68
|
+
</array>
|
|
69
|
+
<key>RunAtLoad</key>
|
|
70
|
+
<true/>
|
|
71
|
+
<key>KeepAlive</key>
|
|
72
|
+
<false/>
|
|
73
|
+
<key>StandardOutPath</key>
|
|
74
|
+
<string>${path_1.default.join(os_1.default.homedir(), '.fraim', 'hub-launchagent.log')}</string>
|
|
75
|
+
<key>StandardErrorPath</key>
|
|
76
|
+
<string>${path_1.default.join(os_1.default.homedir(), '.fraim', 'hub-launchagent-error.log')}</string>
|
|
77
|
+
</dict>
|
|
78
|
+
</plist>
|
|
79
|
+
`;
|
|
80
|
+
fs_1.default.mkdirSync(path_1.default.dirname(plistPath), { recursive: true });
|
|
81
|
+
fs_1.default.writeFileSync(plistPath, content, 'utf8');
|
|
82
|
+
}
|
|
83
|
+
function writeLinuxDesktopEntry(desktopPath, target) {
|
|
84
|
+
const content = `[Desktop Entry]
|
|
85
|
+
Name=FRAIM Hub
|
|
86
|
+
Comment=FRAIM AI Workforce Hub
|
|
87
|
+
Exec=${target} --no-open
|
|
88
|
+
Icon=fraim-hub
|
|
89
|
+
Terminal=false
|
|
90
|
+
Type=Application
|
|
91
|
+
Categories=Utility;
|
|
92
|
+
StartupNotify=true
|
|
93
|
+
`;
|
|
94
|
+
fs_1.default.mkdirSync(path_1.default.dirname(desktopPath), { recursive: true });
|
|
95
|
+
fs_1.default.writeFileSync(desktopPath, content, 'utf8');
|
|
96
|
+
try {
|
|
97
|
+
fs_1.default.chmodSync(desktopPath, 0o644);
|
|
98
|
+
}
|
|
99
|
+
catch { /* non-fatal */ }
|
|
100
|
+
}
|
|
101
|
+
function writeWindowsShortcutScript(lnkPath, target) {
|
|
102
|
+
// PowerShell WScript.Shell creates a real .lnk shortcut.
|
|
103
|
+
const ps = [
|
|
104
|
+
'$ws = New-Object -ComObject WScript.Shell',
|
|
105
|
+
`$sc = $ws.CreateShortcut('${lnkPath.replace(/'/g, "''")}')`,
|
|
106
|
+
`$sc.TargetPath = '${target.replace(/'/g, "''")}'`,
|
|
107
|
+
"$sc.WindowStyle = 1",
|
|
108
|
+
'$sc.Save()',
|
|
109
|
+
].join('; ');
|
|
110
|
+
try {
|
|
111
|
+
(0, child_process_1.execFileSync)('powershell', ['-NoProfile', '-NonInteractive', '-Command', ps], { stdio: 'pipe' });
|
|
112
|
+
}
|
|
113
|
+
catch {
|
|
114
|
+
// PowerShell unavailable (e.g. minimal CI image) — write a plain .cmd launcher as fallback.
|
|
115
|
+
const safeTarget = target.replace(/"/g, '""');
|
|
116
|
+
const cmd = `@echo off\r\nstart "" "${safeTarget}"\r\n`;
|
|
117
|
+
const cmdPath = lnkPath.replace(/\.lnk$/i, '.cmd');
|
|
118
|
+
fs_1.default.mkdirSync(path_1.default.dirname(cmdPath), { recursive: true });
|
|
119
|
+
fs_1.default.writeFileSync(cmdPath, cmd, 'utf8');
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
async function runHubInstall() {
|
|
123
|
+
console.log('Installing FRAIM Hub as an OS application...');
|
|
124
|
+
const installDir = resolveHubInstallDir();
|
|
125
|
+
const target = resolveShortcutTarget();
|
|
126
|
+
const home = os_1.default.homedir();
|
|
127
|
+
const shortcuts = getOsShortcutPaths(home);
|
|
128
|
+
// Ensure the install directory exists (the actual Electron binary download
|
|
129
|
+
// is a separate step — this wires the OS launcher to the expected path so
|
|
130
|
+
// the shortcut is ready as soon as the binary is downloaded).
|
|
131
|
+
fs_1.default.mkdirSync(installDir, { recursive: true });
|
|
132
|
+
if (process.platform === 'win32' && shortcuts.startMenu) {
|
|
133
|
+
fs_1.default.mkdirSync(path_1.default.dirname(shortcuts.startMenu), { recursive: true });
|
|
134
|
+
writeWindowsShortcutScript(shortcuts.startMenu, target);
|
|
135
|
+
console.log(` Start Menu shortcut: ${shortcuts.startMenu}`);
|
|
136
|
+
}
|
|
137
|
+
else if (process.platform === 'darwin' && shortcuts.launchAgent) {
|
|
138
|
+
writeMacosLaunchAgent(shortcuts.launchAgent, target);
|
|
139
|
+
// Load the agent so it is active immediately without a logout/login.
|
|
140
|
+
try {
|
|
141
|
+
(0, child_process_1.execFileSync)('launchctl', ['load', shortcuts.launchAgent], { stdio: 'ignore' });
|
|
142
|
+
}
|
|
143
|
+
catch { /* non-fatal if already loaded */ }
|
|
144
|
+
console.log(` LaunchAgent: ${shortcuts.launchAgent}`);
|
|
145
|
+
}
|
|
146
|
+
else if (shortcuts.desktopEntry) {
|
|
147
|
+
writeLinuxDesktopEntry(shortcuts.desktopEntry, target);
|
|
148
|
+
// Notify the desktop environment to refresh its app database.
|
|
149
|
+
try {
|
|
150
|
+
(0, child_process_1.execFileSync)('update-desktop-database', [path_1.default.dirname(shortcuts.desktopEntry)], { stdio: 'ignore' });
|
|
151
|
+
}
|
|
152
|
+
catch { /* non-fatal if tool absent */ }
|
|
153
|
+
console.log(` Desktop entry: ${shortcuts.desktopEntry}`);
|
|
154
|
+
}
|
|
155
|
+
console.log('');
|
|
156
|
+
console.log('FRAIM Hub is registered as an OS application.');
|
|
157
|
+
console.log('Launch it from your Start Menu / Launchpad / application launcher.');
|
|
158
|
+
console.log('');
|
|
159
|
+
console.log('Note: the Hub binary will be downloaded on first launch via:');
|
|
160
|
+
console.log(' fraim hub (or npx fraim-hub@latest)');
|
|
161
|
+
}
|
|
162
|
+
exports.hubInstallCommand = new commander_1.Command('install')
|
|
163
|
+
.description('Register FRAIM Hub as an OS application (Start Menu / Launchpad entry)')
|
|
164
|
+
.action(async () => {
|
|
165
|
+
await runHubInstall();
|
|
166
|
+
});
|
|
@@ -3182,6 +3182,11 @@ class AiHubServer {
|
|
|
3182
3182
|
const updateAvailable = !!(latest && semver.valid(version) && semver.valid(latest) && semver.gt(latest, version));
|
|
3183
3183
|
return res.json({ version, latest, updateAvailable });
|
|
3184
3184
|
});
|
|
3185
|
+
// #921: expose the server's pid so the orphan scanner in cli.ts can identify and
|
|
3186
|
+
// kill Hub processes that are not registered in hub-runtime.json.
|
|
3187
|
+
this.app.get('/api/ai-hub/pid', (_req, res) => {
|
|
3188
|
+
return res.json({ pid: process.pid });
|
|
3189
|
+
});
|
|
3185
3190
|
this.app.post('/api/ai-hub/project-path/pick', async (_req, res) => {
|
|
3186
3191
|
try {
|
|
3187
3192
|
const projectPath = await this.folderPicker();
|
|
@@ -9,6 +9,7 @@ const commander_1 = require("commander");
|
|
|
9
9
|
const fs_1 = __importDefault(require("fs"));
|
|
10
10
|
const path_1 = __importDefault(require("path"));
|
|
11
11
|
const cli_1 = require("../ai-hub/cli");
|
|
12
|
+
const hub_install_1 = require("../ai-hub/hub-install");
|
|
12
13
|
function readPackageVersion() {
|
|
13
14
|
try {
|
|
14
15
|
let currentDir = __dirname;
|
|
@@ -40,6 +41,8 @@ function createFraimHubProgram(action = cli_1.runHub) {
|
|
|
40
41
|
.option('--keep-running', 'Do not replace a running Hub; keep single-instance focus (default replaces only an older/stale instance)')
|
|
41
42
|
.allowExcessArguments(false)
|
|
42
43
|
.action(action);
|
|
44
|
+
// #921: `fraim-hub install` — register Hub as an OS app (Start Menu / Launchpad entry)
|
|
45
|
+
program.addCommand(hub_install_1.hubInstallCommand);
|
|
43
46
|
return program;
|
|
44
47
|
}
|
|
45
48
|
if (require.main === module) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fraim-hub",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.223",
|
|
4
4
|
"description": "FRAIM Hub local companion package.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"bin": {
|
|
@@ -93,7 +93,7 @@
|
|
|
93
93
|
"dotenv": "^16.4.7",
|
|
94
94
|
"electron": "^41.2.2",
|
|
95
95
|
"express": "^5.2.1",
|
|
96
|
-
"fraim": "2.0.
|
|
96
|
+
"fraim": "2.0.223",
|
|
97
97
|
"mongodb": "^7.0.0",
|
|
98
98
|
"node-cron": "4.2.1",
|
|
99
99
|
"node-edge-tts": "^1.2.10",
|
package/public/ai-hub/index.html
CHANGED
|
@@ -16,8 +16,8 @@
|
|
|
16
16
|
// before company branding). script.js refines the per-theme accent on load.
|
|
17
17
|
try{var b=JSON.parse(localStorage.getItem('fraim-org-brand')||'null');if(b){if(typeof b.color==='string'&&/^#[0-9a-f]{6}$/i.test(b.color)){h.style.setProperty('--accent',b.color);h.style.setProperty('--accent-strong',b.color);}if(typeof b.name==='string'&&b.name){document.title=b.name+' Hub';}}}catch(e){}
|
|
18
18
|
})();</script>
|
|
19
|
-
<link rel="stylesheet" href="./styles.css?v=
|
|
20
|
-
<link rel="stylesheet" href="./review.css">
|
|
19
|
+
<link rel="stylesheet" href="./styles.css?v=dark-status-20260721">
|
|
20
|
+
<link rel="stylesheet" href="./review.css?v=dark-status-20260721">
|
|
21
21
|
</head>
|
|
22
22
|
<body>
|
|
23
23
|
|
|
@@ -1037,6 +1037,6 @@
|
|
|
1037
1037
|
</div>
|
|
1038
1038
|
</div>
|
|
1039
1039
|
|
|
1040
|
-
<script src="./script.js?v=
|
|
1040
|
+
<script src="./script.js?v=dark-status-20260721"></script>
|
|
1041
1041
|
</body>
|
|
1042
1042
|
</html>
|
package/public/ai-hub/script.js
CHANGED
|
@@ -589,8 +589,8 @@ function normalizeGeminiConversationMessages(conv) {
|
|
|
589
589
|
// registry and writes them to the conversation store on every run update
|
|
590
590
|
// (persistRunConversation), so the client must NOT re-upload them: a large run's
|
|
591
591
|
// messages/events/artifacts push the PUT body past the server's size limit, the
|
|
592
|
-
// request 413s, the error is swallowed, and the client-owned
|
|
593
|
-
// payload (e.g.
|
|
592
|
+
// request 413s, the error is swallowed, and the client-owned fields in the SAME
|
|
593
|
+
// payload (e.g. pauseReason='done' from "Mark complete") never persist. Strip them
|
|
594
594
|
// here; the server's PUT handler merges them back from the stored record.
|
|
595
595
|
const SERVER_OWNED_CONV_FIELDS = ['messages', 'events', 'artifacts', 'run', 'delegation'];
|
|
596
596
|
const CLIENT_ONLY_CONV_FIELDS = ['_bodyLoaded', '_stopping'];
|
|
@@ -1798,11 +1798,11 @@ function statusLabel(s) {
|
|
|
1798
1798
|
}
|
|
1799
1799
|
|
|
1800
1800
|
function conversationUiState(conv) {
|
|
1801
|
-
if (!conv) return 'idle';
|
|
1802
|
-
if (conv.blocked) return 'blocked';
|
|
1803
|
-
if (conv.status === 'running') return 'working';
|
|
1804
|
-
// Issue #904: read pauseReason first for non-running records so the pill
|
|
1805
|
-
// reflects the exit classification rather than mapping all completed -> 'waiting'.
|
|
1801
|
+
if (!conv) return 'idle';
|
|
1802
|
+
if (conv.blocked) return 'blocked';
|
|
1803
|
+
if (conv.status === 'running') return 'working';
|
|
1804
|
+
// Issue #904: read pauseReason first for non-running records so the pill
|
|
1805
|
+
// reflects the exit classification rather than mapping all completed -> 'waiting'.
|
|
1806
1806
|
if (conv.pauseReason === 'working') return 'working';
|
|
1807
1807
|
if (conv.pauseReason === 'done') return 'complete';
|
|
1808
1808
|
if (conv.pauseReason === 'error') return 'error';
|
|
@@ -1815,10 +1815,13 @@ function conversationUiState(conv) {
|
|
|
1815
1815
|
// Guard on status==='failed' only: a completed run should resolve to complete/waiting,
|
|
1816
1816
|
// not stay stuck as stopped even if the stopped flag was not cleared.
|
|
1817
1817
|
if (conv.stopped && conv.status === 'failed') return 'stopped';
|
|
1818
|
-
if (conv.status === 'failed') return 'waiting';
|
|
1819
|
-
if (isManagedDelegationChild(conv) && conv.status === 'completed' && conv.managedReviewStatus === 'reviewed') return 'complete';
|
|
1820
|
-
|
|
1821
|
-
|
|
1818
|
+
if (conv.status === 'failed') return 'waiting';
|
|
1819
|
+
if (isManagedDelegationChild(conv) && conv.status === 'completed' && conv.managedReviewStatus === 'reviewed') return 'complete';
|
|
1820
|
+
// Legacy records written before pauseReason='done' used reviewApproved=true as
|
|
1821
|
+
// the only persisted manual-completion marker. Preserve those as done, but do
|
|
1822
|
+
// not let reviewApproved override an explicit awaiting_* pauseReason.
|
|
1823
|
+
if (conv.status === 'completed' && conv.reviewApproved && conv.pauseReason === undefined) return 'complete';
|
|
1824
|
+
if (conv.status === 'completed') {
|
|
1822
1825
|
// For fully-delegate runs: if child workstreams are still active, the manager
|
|
1823
1826
|
// is waiting for them — show 'working' not 'waiting' so the user isn't prompted
|
|
1824
1827
|
// to act when the Hub will auto-route child deliverables back to Mandy.
|
|
@@ -2552,18 +2555,13 @@ function renderConversationIdentity(conv) {
|
|
|
2552
2555
|
host.appendChild(text);
|
|
2553
2556
|
}
|
|
2554
2557
|
|
|
2555
|
-
function renderRunStatePill(conv) {
|
|
2556
|
-
const pill = els['run-state-pill'];
|
|
2557
|
-
if (!pill) return;
|
|
2558
|
-
|
|
2559
|
-
|
|
2560
|
-
|
|
2561
|
-
|
|
2562
|
-
// #549 R3: conversationUiState now returns 'stopped' for manager-stopped runs.
|
|
2563
|
-
// conversationStateLabel returns 'Stopped' for that state; toUpperCase() => 'STOPPED'.
|
|
2564
|
-
pill.textContent = conversationStateLabel(conv).toUpperCase();
|
|
2565
|
-
pill.className = `run-state-pill ${conversationUiState(conv)}`;
|
|
2566
|
-
}
|
|
2558
|
+
function renderRunStatePill(conv) {
|
|
2559
|
+
const pill = els['run-state-pill'];
|
|
2560
|
+
if (!pill) return;
|
|
2561
|
+
// #549 R3: conversationUiState now returns 'stopped' for manager-stopped runs.
|
|
2562
|
+
// conversationStateLabel returns 'Stopped' for that state; toUpperCase() => 'STOPPED'.
|
|
2563
|
+
pill.textContent = conversationStateLabel(conv).toUpperCase();
|
|
2564
|
+
pill.className = `run-state-pill ${conversationUiState(conv)}`;
|
|
2567
2565
|
// #521: Stop is offered only while the employee is actively working.
|
|
2568
2566
|
const stopBtn = els['run-stop-btn'];
|
|
2569
2567
|
if (stopBtn) {
|
|
@@ -3828,12 +3826,14 @@ function clearPendingCoachingJob() {
|
|
|
3828
3826
|
// Awaiting-review signal: the read-side run model does not yet emit a structured
|
|
3829
3827
|
// "awaiting-manager" event, so we degrade gracefully and drive it from the
|
|
3830
3828
|
// terminal run status the read-side already returns (`completed`). A run that
|
|
3831
|
-
// the manager has approved (conv.reviewApproved)
|
|
3829
|
+
// the manager has approved (conv.reviewApproved) or manually completed
|
|
3830
|
+
// (pauseReason='done') is no longer awaiting.
|
|
3832
3831
|
// ---------------------------------------------------------------------------
|
|
3833
3832
|
|
|
3834
3833
|
function convAwaitingReview(conv) {
|
|
3835
|
-
if (!conv) return false;
|
|
3836
|
-
if (conv.
|
|
3834
|
+
if (!conv) return false;
|
|
3835
|
+
if (conv.pauseReason === 'done') return false;
|
|
3836
|
+
if (conv.reviewApproved) return false;
|
|
3837
3837
|
// #770 R2/R4: never ask for a decision while the run is actively working a
|
|
3838
3838
|
// (possibly later, no-decision) phase. conv.status is maintained by
|
|
3839
3839
|
// foldRunIntoConversation. While 'running' the manager is coaching, not
|
|
@@ -4942,10 +4942,11 @@ async function approveReview(options) {
|
|
|
4942
4942
|
showStatus(opts.reviewAction ? 'Approval command sent.' : 'Approval sent.', false);
|
|
4943
4943
|
await continueRun(approvalMessage, { preserveReviewApproved: true });
|
|
4944
4944
|
return;
|
|
4945
|
-
}
|
|
4946
|
-
conv.status = 'completed';
|
|
4947
|
-
conv.reviewApproved =
|
|
4948
|
-
|
|
4945
|
+
}
|
|
4946
|
+
conv.status = 'completed';
|
|
4947
|
+
conv.reviewApproved = false;
|
|
4948
|
+
conv.pauseReason = 'done';
|
|
4949
|
+
upsertConversation(conv);
|
|
4949
4950
|
refreshStatusSurfaces();
|
|
4950
4951
|
renderActive();
|
|
4951
4952
|
syncSendButton();
|