fraim-hub 2.0.247 → 2.0.249

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.
@@ -36,6 +36,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
36
36
  return (mod && mod.__esModule) ? mod : { "default": mod };
37
37
  };
38
38
  Object.defineProperty(exports, "__esModule", { value: true });
39
+ exports.waitForDesktopHubReady = waitForDesktopHubReady;
39
40
  exports.runHub = runHub;
40
41
  // Hub-owned launcher used by the fraim-hub package. Keep this outside
41
42
  // src/cli/commands so the core fraim package has no Hub command implementation.
@@ -79,7 +80,7 @@ function openDesktopWindow(projectPath, preferredPort, runtimeId) {
79
80
  const electronBinary = resolveElectronBinary();
80
81
  const desktopEntry = resolveDesktopEntry();
81
82
  if (!electronBinary || !desktopEntry) {
82
- return false;
83
+ return null;
83
84
  }
84
85
  const args = projectPath
85
86
  ? [desktopEntry, '--project-path', projectPath, '--port', String(preferredPort)]
@@ -91,8 +92,7 @@ function openDesktopWindow(projectPath, preferredPort, runtimeId) {
91
92
  detached: true,
92
93
  stdio: 'ignore',
93
94
  });
94
- child.unref();
95
- return true;
95
+ return child;
96
96
  }
97
97
  function openBrowser(url) {
98
98
  if (process.platform === 'win32') {
@@ -207,6 +207,41 @@ function fetchRunningHubVersion(port) {
207
207
  req.on('timeout', () => { req.destroy(); resolve(null); });
208
208
  });
209
209
  }
210
+ async function waitForDesktopHubReady(child, preferredPort, options = {}) {
211
+ const timeoutMs = options.timeoutMs ?? 15000;
212
+ const pollMs = options.pollMs ?? 250;
213
+ const runtimeId = options.runtimeId || 'hub';
214
+ const fraimDir = options.fraimDir || (0, project_fraim_paths_1.getUserFraimDirPath)();
215
+ const start = Date.now();
216
+ const childState = {};
217
+ child.once('exit', (code, signal) => {
218
+ childState.exit = { code, signal };
219
+ });
220
+ child.once('error', (error) => {
221
+ childState.error = error;
222
+ });
223
+ while (Date.now() - start < timeoutMs) {
224
+ if (childState.error) {
225
+ throw new Error(`FRAIM Hub desktop shell failed to launch: ${childState.error.message}`);
226
+ }
227
+ if (childState.exit) {
228
+ const detail = childState.exit.signal ? `signal ${childState.exit.signal}` : `exit code ${childState.exit.code ?? 'unknown'}`;
229
+ throw new Error(`FRAIM Hub desktop shell exited before the Hub became ready (${detail}).`);
230
+ }
231
+ const runtime = (0, hub_runtime_file_1.readHubRuntimeFile)(fraimDir, runtimeId);
232
+ const ports = runtime?.port && runtime.port !== preferredPort
233
+ ? [runtime.port, preferredPort]
234
+ : [preferredPort];
235
+ for (const port of ports) {
236
+ const version = await fetchRunningHubVersion(port);
237
+ if (version) {
238
+ return { port, version };
239
+ }
240
+ }
241
+ await new Promise((r) => setTimeout(r, pollMs));
242
+ }
243
+ throw new Error(`Timed out waiting for FRAIM Hub desktop shell to become ready on port ${preferredPort}.`);
244
+ }
210
245
  async function reconcileRunningHub(flags, runtimeId = 'hub') {
211
246
  const running = (0, hub_runtime_file_1.readHubRuntimeFile)((0, project_fraim_paths_1.getUserFraimDirPath)(), runtimeId);
212
247
  const confirmedVersion = running ? await fetchRunningHubVersion(running.port) : null;
@@ -243,8 +278,8 @@ async function runHub(options) {
243
278
  if (wantDesktop) {
244
279
  await reconcileRunningHub({ restart: !!options.restart, keepRunning: !!options.keepRunning }, runtimeId);
245
280
  }
246
- const openedDesktop = wantDesktop && openDesktopWindow(projectPath, preferredPort, runtimeId);
247
- if (!openedDesktop) {
281
+ const desktopChild = wantDesktop ? openDesktopWindow(projectPath, preferredPort, runtimeId) : null;
282
+ if (!desktopChild) {
248
283
  const port = await findAvailablePort(preferredPort);
249
284
  const server = new AiHubServer(projectPath ? { projectPath } : {});
250
285
  await server.start(port);
@@ -254,7 +289,9 @@ async function runHub(options) {
254
289
  openBrowser(url);
255
290
  return;
256
291
  }
257
- console.log('AI Hub desktop shell launched.');
292
+ const ready = await waitForDesktopHubReady(desktopChild, preferredPort, { runtimeId });
293
+ desktopChild.unref();
294
+ console.log(`AI Hub desktop shell launched at http://127.0.0.1:${ready.port}/ai-hub/ (v${ready.version}).`);
258
295
  if (projectPath)
259
296
  console.log(`Project path: ${projectPath}`);
260
297
  else
@@ -17,21 +17,22 @@ exports.buildCustomEmployeePersona = buildCustomEmployeePersona;
17
17
  // evaluatePersonaAccess) must never call resolveEmployee.
18
18
  const fs_1 = __importDefault(require("fs"));
19
19
  const path_1 = __importDefault(require("path"));
20
- const project_fraim_paths_1 = require("../core/utils/project-fraim-paths");
20
+ const pack_home_1 = require("../cli/utils/pack-home");
21
21
  const persona_hiring_1 = require("../config/persona-hiring");
22
22
  const EMPLOYEES_DIR_REL = path_1.default.join('fraim', 'personalized-employee', 'employees');
23
23
  function employeesDir(projectDir) {
24
24
  return path_1.default.join(projectDir, EMPLOYEES_DIR_REL);
25
25
  }
26
- // Issue #1002: the manager level is the default home for a custom employee, so
27
- // an employee the manager created is available in every project on the machine.
28
- // Two roots, matching capability precedence: the writable home wins over the
29
- // synced cache, so a just-created employee appears before any publish or sync.
30
- function managerEmployeesDirs() {
31
- return {
32
- local: path_1.default.join((0, project_fraim_paths_1.getUserFraimDirPath)(), 'personalized-employee', 'employees'),
33
- cache: path_1.default.join((0, project_fraim_paths_1.getUserFraimDirPath)(), 'manager', 'employees'),
34
- };
26
+ // Issue #1043 (review round 2): every layer has exactly one home, named by
27
+ // config, and that home holds the whole layer. There are no caches, so an
28
+ // employee has exactly one authoritative location per level.
29
+ const EMPLOYEES_SUBDIR = 'employees';
30
+ function layerEmployeeReadDirs(layer) {
31
+ return (0, pack_home_1.packReadRoots)(layer).map((root) => path_1.default.join(root, EMPLOYEES_SUBDIR));
32
+ }
33
+ /** The single writable employees directory for a layer. */
34
+ function layerEmployeeWriteDir(layer) {
35
+ return path_1.default.join((0, pack_home_1.resolvePackHome)(layer).root, EMPLOYEES_SUBDIR);
35
36
  }
36
37
  function readEmployeesFromDir(dir, scope) {
37
38
  if (!fs_1.default.existsSync(dir))
@@ -82,15 +83,23 @@ function safeSlug(key) {
82
83
  * cache. Reading the manager level here is what makes the manager-level default
83
84
  * usable: without it an employee created at the manager level would exist on
84
85
  * disk and appear in no roster.
86
+ *
87
+ * Issue #1043 adds the org level at the bottom of that order, so an employee
88
+ * shared company-wide is visible to everyone, and a manager or project copy of
89
+ * the same key still takes precedence over it.
85
90
  */
86
91
  function readCustomEmployees(projectDir) {
87
- const dirs = managerEmployeesDirs();
88
92
  const byKey = new Map();
89
93
  // Lowest precedence first, so a later write overwrites on key collision.
90
- for (const emp of readEmployeesFromDir(dirs.cache, 'manager'))
91
- byKey.set(emp.key, emp);
92
- for (const emp of readEmployeesFromDir(dirs.local, 'manager'))
93
- byKey.set(emp.key, emp);
94
+ // packReadRoots is highest-precedence-first, so it is walked in reverse.
95
+ for (const dir of [...layerEmployeeReadDirs('org')].reverse()) {
96
+ for (const emp of readEmployeesFromDir(dir, 'org'))
97
+ byKey.set(emp.key, emp);
98
+ }
99
+ for (const dir of [...layerEmployeeReadDirs('manager')].reverse()) {
100
+ for (const emp of readEmployeesFromDir(dir, 'manager'))
101
+ byKey.set(emp.key, emp);
102
+ }
94
103
  for (const emp of readEmployeesFromDir(employeesDir(projectDir), 'project'))
95
104
  byKey.set(emp.key, emp);
96
105
  return [...byKey.values()];
@@ -108,7 +117,9 @@ function writeCustomEmployee(projectDir, employee) {
108
117
  const slug = safeSlug(record.key);
109
118
  if (!slug)
110
119
  throw new Error(`Invalid employee key: ${record.key}`);
111
- const dir = scope === 'project' ? employeesDir(projectDir) : managerEmployeesDirs().local;
120
+ // Issue #1043 (review round 1): the org level now has a real writable home,
121
+ // resolved from config, so an org-scope write no longer has to be refused.
122
+ const dir = scope === 'project' ? employeesDir(projectDir) : layerEmployeeWriteDir(scope);
112
123
  fs_1.default.mkdirSync(dir, { recursive: true });
113
124
  fs_1.default.writeFileSync(path_1.default.join(dir, `${slug}.json`), JSON.stringify(record, null, 2), 'utf8');
114
125
  }
@@ -121,11 +132,12 @@ function deleteCustomEmployee(projectDir, key) {
121
132
  const slug = safeSlug(key);
122
133
  if (!slug)
123
134
  return false;
124
- const dirs = managerEmployeesDirs();
135
+ // Every place a copy could live, at every level. A delete that misses one
136
+ // reports success while the record is still listed from that level.
125
137
  const candidates = [
126
138
  path_1.default.join(employeesDir(projectDir), `${slug}.json`),
127
- path_1.default.join(dirs.local, `${slug}.json`),
128
- path_1.default.join(dirs.cache, `${slug}.json`),
139
+ ...layerEmployeeReadDirs('manager').map((d) => path_1.default.join(d, `${slug}.json`)),
140
+ ...layerEmployeeReadDirs('org').map((d) => path_1.default.join(d, `${slug}.json`)),
129
141
  ];
130
142
  let removed = false;
131
143
  for (const fp of candidates) {
@@ -0,0 +1,138 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.hostSessionState = exports.HostSessionState = void 0;
4
+ class HostSessionState {
5
+ keyFor(owner) {
6
+ const configuredAgentId = typeof owner.configuredAgentId === 'string' ? owner.configuredAgentId.trim() : '';
7
+ return configuredAgentId || `${owner.baseHostId}-default`;
8
+ }
9
+ applySession(target, owner, sessionId, options = {}) {
10
+ const normalizedSessionId = sessionId.trim();
11
+ if (!normalizedSessionId)
12
+ return;
13
+ const at = options.at || new Date().toISOString();
14
+ const key = this.keyFor(owner);
15
+ target.hostSessions = {
16
+ ...(target.hostSessions || {}),
17
+ [key]: {
18
+ configuredAgentId: owner.configuredAgentId || null,
19
+ baseHostId: owner.baseHostId,
20
+ sessionId: normalizedSessionId,
21
+ status: options.status || 'valid',
22
+ sourceRunId: options.sourceRunId ?? target.id ?? null,
23
+ updatedAt: at,
24
+ },
25
+ };
26
+ target.sessionId = normalizedSessionId;
27
+ }
28
+ mergeFromRun(record, run) {
29
+ const owner = {
30
+ configuredAgentId: run.configuredAgentId || record.configuredAgentId || null,
31
+ baseHostId: (run.baseHostId || run.hostId || record.baseHostId || record.agentName),
32
+ };
33
+ record.hostSessions = { ...(record.hostSessions || {}) };
34
+ for (const [key, session] of Object.entries(run.hostSessions || {})) {
35
+ record.hostSessions[key] = { ...session };
36
+ }
37
+ if (run.sessionId) {
38
+ const projectedRun = {
39
+ id: run.id,
40
+ sessionId: record.sessionId || undefined,
41
+ hostSessions: record.hostSessions,
42
+ };
43
+ this.applySession(projectedRun, owner, run.sessionId, { sourceRunId: run.id });
44
+ record.hostSessions = projectedRun.hostSessions;
45
+ }
46
+ const active = this.resolve(record, owner);
47
+ record.sessionId = active?.sessionId || run.sessionId || record.sessionId || null;
48
+ }
49
+ resolve(conversation, owner) {
50
+ if (!conversation)
51
+ return null;
52
+ const sessions = conversation.hostSessions || {};
53
+ const key = this.keyFor(owner);
54
+ const exact = sessions[key];
55
+ if (this.isResumableForOwner(exact, owner))
56
+ return exact;
57
+ const fallback = Object.values(sessions).find((session) => this.isResumableForOwner(session, owner));
58
+ if (fallback)
59
+ return fallback;
60
+ const legacySessionId = typeof conversation.sessionId === 'string' ? conversation.sessionId.trim() : '';
61
+ if (!legacySessionId)
62
+ return null;
63
+ return {
64
+ configuredAgentId: owner.configuredAgentId || null,
65
+ baseHostId: owner.baseHostId,
66
+ sessionId: legacySessionId,
67
+ status: 'suspect',
68
+ sourceRunId: typeof conversation.runId === 'string' ? conversation.runId : null,
69
+ updatedAt: typeof conversation.lastUpdatedAt === 'string' ? conversation.lastUpdatedAt : new Date().toISOString(),
70
+ };
71
+ }
72
+ markInvalid(conversation, owner, sessionId, reason, at = new Date().toISOString()) {
73
+ const normalizedSessionId = sessionId.trim();
74
+ if (!normalizedSessionId)
75
+ return;
76
+ const key = this.keyFor(owner);
77
+ const existing = conversation.hostSessions?.[key];
78
+ conversation.hostSessions = { ...(conversation.hostSessions || {}) };
79
+ conversation.hostSessions[key] = {
80
+ configuredAgentId: owner.configuredAgentId || existing?.configuredAgentId || null,
81
+ baseHostId: owner.baseHostId,
82
+ sessionId: normalizedSessionId,
83
+ status: 'invalid',
84
+ sourceRunId: existing?.sourceRunId || (typeof conversation.runId === 'string' ? conversation.runId : null),
85
+ updatedAt: existing?.updatedAt || at,
86
+ invalidatedAt: at,
87
+ invalidationReason: reason,
88
+ };
89
+ if (conversation.sessionId === normalizedSessionId) {
90
+ const replacement = this.resolve({ ...conversation, sessionId: null }, owner);
91
+ conversation.sessionId = replacement?.sessionId || null;
92
+ }
93
+ }
94
+ markInvalidRun(run, owner, sessionId, reason, at = new Date().toISOString()) {
95
+ const normalizedSessionId = sessionId.trim();
96
+ if (!normalizedSessionId)
97
+ return;
98
+ const key = this.keyFor(owner);
99
+ const existing = run.hostSessions?.[key];
100
+ run.hostSessions = { ...(run.hostSessions || {}) };
101
+ run.hostSessions[key] = {
102
+ configuredAgentId: owner.configuredAgentId || existing?.configuredAgentId || null,
103
+ baseHostId: owner.baseHostId,
104
+ sessionId: normalizedSessionId,
105
+ status: 'invalid',
106
+ sourceRunId: existing?.sourceRunId || run.id || null,
107
+ updatedAt: existing?.updatedAt || at,
108
+ invalidatedAt: at,
109
+ invalidationReason: reason,
110
+ };
111
+ if (run.sessionId === normalizedSessionId) {
112
+ run.sessionId = undefined;
113
+ }
114
+ }
115
+ hasInvalidSession(conversation, owner, sessionId) {
116
+ const normalizedSessionId = sessionId.trim();
117
+ if (!conversation || !normalizedSessionId)
118
+ return false;
119
+ const key = this.keyFor(owner);
120
+ const exact = conversation.hostSessions?.[key];
121
+ if (exact?.status === 'invalid' && exact.sessionId === normalizedSessionId)
122
+ return true;
123
+ return Object.values(conversation.hostSessions || {}).some((session) => (session.status === 'invalid'
124
+ && session.sessionId === normalizedSessionId
125
+ && session.baseHostId === owner.baseHostId));
126
+ }
127
+ isResumableForOwner(session, owner) {
128
+ if (!session || !session.sessionId || session.status === 'invalid')
129
+ return false;
130
+ if (session.baseHostId !== owner.baseHostId)
131
+ return false;
132
+ const requestedConfiguredAgentId = typeof owner.configuredAgentId === 'string' ? owner.configuredAgentId.trim() : '';
133
+ const sessionConfiguredAgentId = typeof session.configuredAgentId === 'string' ? session.configuredAgentId.trim() : '';
134
+ return !requestedConfiguredAgentId || !sessionConfiguredAgentId || requestedConfiguredAgentId === sessionConfiguredAgentId;
135
+ }
136
+ }
137
+ exports.HostSessionState = HostSessionState;
138
+ exports.hostSessionState = new HostSessionState();
@@ -1578,7 +1578,10 @@ function parseHostLine(hostId, line) {
1578
1578
  if (parsed.type === 'result') {
1579
1579
  // Don't emit message — the 'assistant' event already captured the turn text.
1580
1580
  // result carries usage data (parsed by parseUsageSignal above via withSignal).
1581
- return withSignal({ sessionId: parsed.session_id, raw: trimmed });
1581
+ // Claude result events can echo the requested resume id even when resume failed
1582
+ // with "No conversation found with session ID". Treat system events as the
1583
+ // authoritative session source instead of reinforcing a stale pointer.
1584
+ return withSignal({ raw: trimmed });
1582
1585
  }
1583
1586
  return withSignal({ raw: trimmed });
1584
1587
  }
@@ -3,9 +3,9 @@
3
3
  * Sideloads Office add-in manifests (Word + PowerPoint) so they appear under
4
4
  * Insert > My Add-ins > Developer Add-ins without admin rights or AppSource.
5
5
  *
6
- * Windows: writes HKCU\SOFTWARE\Microsoft\Office\16.0\WEF\Developer\<guid> =
7
- * ABSOLUTE FILE PATH to manifest.xml. This is exactly what Microsoft's
8
- * `office-addin-dev-settings register` writes. A URL value is for
6
+ * Windows: writes HKCU\SOFTWARE\Microsoft\Office\16.0\WEF\Developer\{<guid>}
7
+ * (Default) = ABSOLUTE FILE PATH to manifest.xml. This is the registry
8
+ * shape Word resolves for Developer Add-ins. A URL value is for
9
9
  * SharePoint/network-share catalogs and yields "catalog access denied"
10
10
  * for a developer sideload — do NOT use a URL here.
11
11
  * macOS: copies the manifest into each app's
@@ -23,6 +23,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
23
23
  };
24
24
  Object.defineProperty(exports, "__esModule", { value: true });
25
25
  exports.manifestXmlForPort = manifestXmlForPort;
26
+ exports.winDeveloperManifestSubkey = winDeveloperManifestSubkey;
26
27
  exports.isSideloaded = isSideloaded;
27
28
  exports.sideloadManifest = sideloadManifest;
28
29
  exports.removeSideload = removeSideload;
@@ -61,7 +62,7 @@ function generatedManifestPath(entry, userDataDir) {
61
62
  return path_1.default.join(userDataDir, 'office-manifests', entry.guid, 'manifest.xml');
62
63
  }
63
64
  function manifestXmlForPort(xml, httpPort) {
64
- return xml.replace(/(https?:\/\/(?:localhost|127\.0\.0\.1)):43091/g, `$1:${httpPort}`);
65
+ return xml.replace(/https?:\/\/(?:localhost|127\.0\.0\.1):43091/g, `http://127.0.0.1:${httpPort}`);
65
66
  }
66
67
  function prepareManifestForSideload(entry, sourcePath, options) {
67
68
  if (!options.httpPort)
@@ -76,16 +77,46 @@ function prepareManifestForSideload(entry, sourcePath, options) {
76
77
  // ---------------------------------------------------------------------------
77
78
  // Windows registry helpers
78
79
  // ---------------------------------------------------------------------------
79
- function winRegisteredValue(guid) {
80
+ function winDeveloperManifestSubkey(guid) {
81
+ return `${WEF_DEVELOPER_KEY}\\{${guid}}`;
82
+ }
83
+ function parseRegSzValue(stdout) {
84
+ const line = stdout.split(/\r?\n/).find(l => l.includes('REG_SZ'));
85
+ if (!line)
86
+ return null;
87
+ const idx = line.indexOf('REG_SZ');
88
+ return line.slice(idx + 'REG_SZ'.length).trim() || null;
89
+ }
90
+ function winRegisteredRootValue(guid) {
80
91
  const r = (0, child_process_1.spawnSync)('reg', ['query', WEF_DEVELOPER_KEY, '/v', guid], { encoding: 'utf8' });
81
92
  if (r.status !== 0 || !r.stdout.includes('REG_SZ'))
82
93
  return null;
83
94
  // Output line looks like: " <guid> REG_SZ C:\path\to\manifest.xml"
84
- const line = r.stdout.split(/\r?\n/).find(l => l.includes('REG_SZ'));
85
- if (!line)
95
+ return parseRegSzValue(r.stdout);
96
+ }
97
+ function winRegisteredDefaultValue(guid) {
98
+ const r = (0, child_process_1.spawnSync)('reg', ['query', winDeveloperManifestSubkey(guid), '/ve'], { encoding: 'utf8' });
99
+ if (r.status !== 0 || !r.stdout.includes('REG_SZ'))
86
100
  return null;
87
- const idx = line.indexOf('REG_SZ');
88
- return line.slice(idx + 'REG_SZ'.length).trim() || null;
101
+ // Output line looks like: " (Default) REG_SZ C:\path\to\manifest.xml"
102
+ return parseRegSzValue(r.stdout);
103
+ }
104
+ function winRegisteredValue(guid) {
105
+ // Word resolves the braced GUID subkey. Prefer it even when a legacy root
106
+ // value exists, so a stale effective registration cannot be hidden.
107
+ return winRegisteredDefaultValue(guid) ?? winRegisteredRootValue(guid);
108
+ }
109
+ function winWriteRegisteredValue(guid, manifestPath) {
110
+ const r = (0, child_process_1.spawnSync)('reg', [
111
+ 'add', winDeveloperManifestSubkey(guid),
112
+ '/ve', '/t', 'REG_SZ', '/d', manifestPath, '/f',
113
+ ], { encoding: 'utf8' });
114
+ if (r.status !== 0)
115
+ return { ok: false, reason: r.stderr || `reg add failed for ${guid}` };
116
+ // Remove the older root-value registration if present. Office does not need
117
+ // it, and keeping two registration shapes makes stale-state diagnosis harder.
118
+ (0, child_process_1.spawnSync)('reg', ['delete', WEF_DEVELOPER_KEY, '/v', guid, '/f'], { encoding: 'utf8' });
119
+ return { ok: true };
89
120
  }
90
121
  // ---------------------------------------------------------------------------
91
122
  // Public API
@@ -117,13 +148,10 @@ function sideloadManifest(projectPath, options = {}) {
117
148
  }
118
149
  const sideloadPath = prepareManifestForSideload(entry, manifestPath, options);
119
150
  if (process.platform === 'win32') {
120
- // Developer-key value = absolute file path to manifest.xml (NOT a URL).
121
- const r = (0, child_process_1.spawnSync)('reg', [
122
- 'add', WEF_DEVELOPER_KEY,
123
- '/v', entry.guid, '/t', 'REG_SZ', '/d', sideloadPath, '/f',
124
- ], { encoding: 'utf8' });
125
- if (r.status !== 0)
126
- return { ok: false, reason: r.stderr || `reg add failed for ${entry.guid}` };
151
+ // Developer add-in subkey default value = absolute file path to manifest.xml (NOT a URL).
152
+ const r = winWriteRegisteredValue(entry.guid, sideloadPath);
153
+ if (!r.ok)
154
+ return r;
127
155
  continue;
128
156
  }
129
157
  if (process.platform === 'darwin') {
@@ -146,6 +174,7 @@ function removeSideload() {
146
174
  for (const entry of MANIFESTS) {
147
175
  if (process.platform === 'win32') {
148
176
  (0, child_process_1.spawnSync)('reg', ['delete', WEF_DEVELOPER_KEY, '/v', entry.guid, '/f'], { encoding: 'utf8' });
177
+ (0, child_process_1.spawnSync)('reg', ['delete', winDeveloperManifestSubkey(entry.guid), '/f'], { encoding: 'utf8' });
149
178
  }
150
179
  else if (process.platform === 'darwin') {
151
180
  const target = macWefPath(entry.macContainer, entry.guid);
@@ -112,16 +112,15 @@ function normalizeAiHubProjectList(projects, currentProjectPath, options = {}) {
112
112
  const byPath = new Map();
113
113
  const currentCanonical = currentProjectPath ? canonicalProjectPath(currentProjectPath) : null;
114
114
  const removedKeys = new Set(normalizeRemovedProjectPaths(options.removedProjectPaths));
115
- const add = (entry, isCurrent = false) => {
115
+ const add = (entry) => {
116
116
  if (!entry)
117
117
  return;
118
118
  const displayPath = normalizeProjectPath(entry.folderPath);
119
119
  const dedupKey = canonicalProjectPath(displayPath);
120
- // Issue #788: the current project is always shown in its own workspace, even when
121
- // tombstoned (#719 never-empty Hub). This is a display override ONLY the tombstone
122
- // is neither erased here nor in persistence, so switching away re-hides the project
123
- // and no subsequent add can resurrect it.
124
- if (!isCurrent && removedKeys.has(dedupKey))
120
+ // Issue #1044: tombstoned paths are never shown, even when the path is the active
121
+ // project. The caller must switch to a different project (or clear projectPath) before
122
+ // deleting the server now enforces this only when other projects remain (#719 reverted).
123
+ if (removedKeys.has(dedupKey))
125
124
  return;
126
125
  if (!options.includeMissing && dedupKey !== currentCanonical && !projectPathExists(displayPath))
127
126
  return;
@@ -129,7 +128,7 @@ function normalizeAiHubProjectList(projects, currentProjectPath, options = {}) {
129
128
  byPath.set(dedupKey, existing ? { ...existing, ...entry, folderPath: existing.folderPath } : { ...entry, folderPath: displayPath });
130
129
  };
131
130
  if (currentProjectPath)
132
- add(normalizeProjectEntry({ folderPath: currentProjectPath }, currentProjectPath), true);
131
+ add(normalizeProjectEntry({ folderPath: currentProjectPath }, currentProjectPath));
133
132
  for (const project of projects)
134
133
  add(normalizeProjectEntry(project));
135
134
  return withUniqueProjectIds(Array.from(byPath.values()));