fraim-hub 2.0.302 → 2.0.303
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/configured-agents.js +17 -17
- package/dist/src/ai-hub/host-session-state.js +9 -0
- package/dist/src/ai-hub/hosts.js +111 -4
- package/dist/src/ai-hub/server.js +95 -36
- package/dist/src/config/persona-capability-bundles.js +1 -1
- package/dist/src/core/quality-evidence.js +10 -0
- package/package.json +2 -2
- package/public/ai-hub/script.js +13 -1
|
@@ -99,15 +99,15 @@ function normalizeCommand(value) {
|
|
|
99
99
|
timeoutMs: typeof value.timeoutMs === 'number' && value.timeoutMs > 0 ? Math.min(value.timeoutMs, 120_000) : 30_000,
|
|
100
100
|
};
|
|
101
101
|
}
|
|
102
|
-
function synthesizeDefaultConfiguredAgents(
|
|
102
|
+
function synthesizeDefaultConfiguredAgents(hosts) {
|
|
103
103
|
const timestamp = '1970-01-01T00:00:00.000Z';
|
|
104
|
-
return
|
|
105
|
-
.filter((
|
|
106
|
-
.map((
|
|
107
|
-
id: `${
|
|
108
|
-
label:
|
|
104
|
+
return hosts
|
|
105
|
+
.filter((host) => host.available)
|
|
106
|
+
.map((host) => ({
|
|
107
|
+
id: `${host.id}-default`,
|
|
108
|
+
label: host.label,
|
|
109
109
|
description: 'Default local launch for this agent tool.',
|
|
110
|
-
baseHostId:
|
|
110
|
+
baseHostId: host.id,
|
|
111
111
|
enabled: true,
|
|
112
112
|
createdAt: timestamp,
|
|
113
113
|
updatedAt: timestamp,
|
|
@@ -133,9 +133,9 @@ class AiHubConfiguredAgentStore {
|
|
|
133
133
|
return [];
|
|
134
134
|
}
|
|
135
135
|
}
|
|
136
|
-
listWithDefaults(
|
|
136
|
+
listWithDefaults(hosts) {
|
|
137
137
|
const persisted = this.list();
|
|
138
|
-
const defaults = synthesizeDefaultConfiguredAgents(
|
|
138
|
+
const defaults = synthesizeDefaultConfiguredAgents(hosts);
|
|
139
139
|
const seen = new Set(persisted.map((agent) => agent.id));
|
|
140
140
|
return [...persisted, ...defaults.filter((agent) => !seen.has(agent.id))];
|
|
141
141
|
}
|
|
@@ -166,14 +166,14 @@ class AiHubConfiguredAgentStore {
|
|
|
166
166
|
}
|
|
167
167
|
}
|
|
168
168
|
exports.AiHubConfiguredAgentStore = AiHubConfiguredAgentStore;
|
|
169
|
-
function checkConfiguredAgentAvailability(agent,
|
|
169
|
+
function checkConfiguredAgentAvailability(agent, hosts, env = process.env) {
|
|
170
170
|
const reasons = [];
|
|
171
171
|
const warnings = [];
|
|
172
|
-
const
|
|
172
|
+
const host = hosts.find((entry) => entry.id === agent.baseHostId);
|
|
173
173
|
if (!agent.enabled)
|
|
174
174
|
reasons.push('Configured agent is disabled.');
|
|
175
|
-
if (!
|
|
176
|
-
reasons.push(`${
|
|
175
|
+
if (!host?.available)
|
|
176
|
+
reasons.push(`${host?.label || agent.baseHostId} is not available on this machine.`);
|
|
177
177
|
if (agent.command) {
|
|
178
178
|
if (!agent.command.command.trim()) {
|
|
179
179
|
reasons.push('Command is empty.');
|
|
@@ -202,8 +202,8 @@ function checkConfiguredAgentAvailability(agent, employees, env = process.env) {
|
|
|
202
202
|
}
|
|
203
203
|
return { id: agent.id, label: agent.label, baseHostId: agent.baseHostId, enabled: agent.enabled, available: reasons.length === 0, reasons, warnings };
|
|
204
204
|
}
|
|
205
|
-
function checkConfiguredAgentReadiness(agent,
|
|
206
|
-
const check = checkConfiguredAgentAvailability(agent,
|
|
205
|
+
function checkConfiguredAgentReadiness(agent, hosts, env = process.env) {
|
|
206
|
+
const check = checkConfiguredAgentAvailability(agent, hosts, env);
|
|
207
207
|
if (!check.available)
|
|
208
208
|
return check;
|
|
209
209
|
if (agent.command)
|
|
@@ -225,8 +225,8 @@ function checkConfiguredAgentReadiness(agent, employees, env = process.env) {
|
|
|
225
225
|
};
|
|
226
226
|
}
|
|
227
227
|
}
|
|
228
|
-
function projectConfiguredAgent(agent,
|
|
229
|
-
const check = checkConfiguredAgentAvailability(agent,
|
|
228
|
+
function projectConfiguredAgent(agent, hosts) {
|
|
229
|
+
const check = checkConfiguredAgentAvailability(agent, hosts);
|
|
230
230
|
return {
|
|
231
231
|
id: agent.id,
|
|
232
232
|
label: agent.label,
|
|
@@ -57,6 +57,15 @@ class HostSessionState {
|
|
|
57
57
|
const fallback = Object.values(sessions).find((session) => this.isResumableForOwner(session, owner));
|
|
58
58
|
if (fallback)
|
|
59
59
|
return fallback;
|
|
60
|
+
// Issue #1515: once this conversation has ANY owner-qualified session recorded, that
|
|
61
|
+
// ledger is authoritative. The legacy `sessionId` mirror always reflects whichever
|
|
62
|
+
// configured agent last ran (see `mergeFromRun` below), not the requested owner, so
|
|
63
|
+
// treating it as resumable here would resume a different agent's session under the
|
|
64
|
+
// wrong owner (e.g. Codex Azure Script silently reusing a plain Codex session because
|
|
65
|
+
// both share `baseHostId: 'codex'`). The legacy fallback below is reserved for
|
|
66
|
+
// conversations that predate the owner-qualified ledger entirely.
|
|
67
|
+
if (Object.keys(sessions).length > 0)
|
|
68
|
+
return null;
|
|
60
69
|
const legacySessionId = typeof conversation.sessionId === 'string' ? conversation.sessionId.trim() : '';
|
|
61
70
|
if (!legacySessionId)
|
|
62
71
|
return null;
|
package/dist/src/ai-hub/hosts.js
CHANGED
|
@@ -5,7 +5,9 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.createHubEvent = exports.createHubMessage = exports.ScriptedHostRuntime = exports.FakeHostRuntime = exports.CliHostRuntime = exports.COMPACT_TASK_FAILURE_PATTERN = void 0;
|
|
7
7
|
exports.parseSeekMentoringSignal = parseSeekMentoringSignal;
|
|
8
|
+
exports.parseRecurringParkSignal = parseRecurringParkSignal;
|
|
8
9
|
exports.parseFraimJobLoadSignal = parseFraimJobLoadSignal;
|
|
10
|
+
exports.parseExecutionModeSignal = parseExecutionModeSignal;
|
|
9
11
|
exports.parseUsageSignal = parseUsageSignal;
|
|
10
12
|
exports.parseAgentIdentitySignal = parseAgentIdentitySignal;
|
|
11
13
|
exports.escapeWindowsArg = escapeWindowsArg;
|
|
@@ -124,6 +126,41 @@ function parseSeekMentoringSignal(line) {
|
|
|
124
126
|
}
|
|
125
127
|
return null;
|
|
126
128
|
}
|
|
129
|
+
// Issue #1509: detect the recurring-park marker emitted by the mentor inside its
|
|
130
|
+
// tool_result when it resolves a phase transition to WAITING_FOR_NEXT_INVOCATION.
|
|
131
|
+
// Scoped to genuine tool-result-shaped sub-objects — not a bare substring match —
|
|
132
|
+
// so any job that prints ai-mentor.ts source in an assistant-text message does NOT
|
|
133
|
+
// accidentally set the flag. A plain assistant message carries no sub-object whose
|
|
134
|
+
// type matches the tool-result pattern, so it is naturally excluded.
|
|
135
|
+
//
|
|
136
|
+
// Detection property: the marker must appear inside a JSON sub-object whose own
|
|
137
|
+
// `type` field matches the tool-result-type pattern. This covers all known host
|
|
138
|
+
// shapes (Claude Code tool_result, Codex function_call_output/mcp_tool_result,
|
|
139
|
+
// Copilot tool.execution_complete) and future hosts without per-host branches.
|
|
140
|
+
const TOOL_RESULT_TYPE_RE = /tool_result|function_call_output|mcp_tool_result|tool\.execution_complete/i;
|
|
141
|
+
function containsMarkerInToolResult(value) {
|
|
142
|
+
if (Array.isArray(value))
|
|
143
|
+
return value.some(item => containsMarkerInToolResult(item));
|
|
144
|
+
if (typeof value !== 'object' || value === null)
|
|
145
|
+
return false;
|
|
146
|
+
const obj = value;
|
|
147
|
+
if (typeof obj.type === 'string' && TOOL_RESULT_TYPE_RE.test(obj.type)) {
|
|
148
|
+
return JSON.stringify(obj).includes('FRAIM_RECURRING_PARK');
|
|
149
|
+
}
|
|
150
|
+
return Object.values(obj).some(v => containsMarkerInToolResult(v));
|
|
151
|
+
}
|
|
152
|
+
function parseRecurringParkSignal(line) {
|
|
153
|
+
if (!line.includes('FRAIM_RECURRING_PARK'))
|
|
154
|
+
return false;
|
|
155
|
+
let parsed;
|
|
156
|
+
try {
|
|
157
|
+
parsed = JSON.parse(line);
|
|
158
|
+
}
|
|
159
|
+
catch {
|
|
160
|
+
return false;
|
|
161
|
+
}
|
|
162
|
+
return containsMarkerInToolResult(parsed);
|
|
163
|
+
}
|
|
127
164
|
// Issue #710: extract the job name from a get_fraim_job tool call. This
|
|
128
165
|
// is structured known-job evidence; the server still validates the job id
|
|
129
166
|
// against the catalog before promoting any run.
|
|
@@ -184,6 +221,43 @@ function parseFraimJobLoadSignal(line) {
|
|
|
184
221
|
}
|
|
185
222
|
return null;
|
|
186
223
|
}
|
|
224
|
+
function parseExecutionModeSignal(line) {
|
|
225
|
+
if (!line.includes('Execution Mode Context'))
|
|
226
|
+
return null;
|
|
227
|
+
let parsed;
|
|
228
|
+
try {
|
|
229
|
+
parsed = JSON.parse(line);
|
|
230
|
+
}
|
|
231
|
+
catch {
|
|
232
|
+
return null;
|
|
233
|
+
}
|
|
234
|
+
if (typeof parsed !== 'object' || parsed === null)
|
|
235
|
+
return null;
|
|
236
|
+
const obj = parsed;
|
|
237
|
+
// Codex/codex_apps shape: the completed get_fraim_job tool call carries its
|
|
238
|
+
// result in item.result.content[].text. The local proxy side-channel does not
|
|
239
|
+
// run for this connector path, so Hub must consume the same payload directly.
|
|
240
|
+
if (obj.type === 'item.completed' &&
|
|
241
|
+
typeof obj.item === 'object' &&
|
|
242
|
+
obj.item !== null) {
|
|
243
|
+
const item = obj.item;
|
|
244
|
+
if (item.type === 'mcp_tool_call' && isFraimTool(item.tool, 'get_fraim_job')) {
|
|
245
|
+
const result = item.result;
|
|
246
|
+
const content = result && Array.isArray(result.content) ? result.content : [];
|
|
247
|
+
for (const entry of content) {
|
|
248
|
+
if (typeof entry !== 'object' || entry === null)
|
|
249
|
+
continue;
|
|
250
|
+
const text = entry.text;
|
|
251
|
+
if (typeof text !== 'string')
|
|
252
|
+
continue;
|
|
253
|
+
const signal = readExecutionModeFromText(text);
|
|
254
|
+
if (signal)
|
|
255
|
+
return signal;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
return null;
|
|
260
|
+
}
|
|
187
261
|
// Issue #347 — extract per-turn usage from the host's JSON stream.
|
|
188
262
|
// Codex: `{"type":"turn.completed","usage":{input_tokens, cached_input_tokens, output_tokens, reasoning_output_tokens}}`.
|
|
189
263
|
// Claude Code: `{"type":"result", ..., "usage":{input_tokens, output_tokens, cache_creation_input_tokens, cache_read_input_tokens}, "total_cost_usd": ...}`.
|
|
@@ -477,6 +551,29 @@ function readFraimJobFromArgs(rawArgs) {
|
|
|
477
551
|
return null;
|
|
478
552
|
return { jobId, source: 'get_fraim_job' };
|
|
479
553
|
}
|
|
554
|
+
function readExecutionModeFromText(text) {
|
|
555
|
+
const marker = '**Execution Mode Context:**';
|
|
556
|
+
const idx = text.indexOf(marker);
|
|
557
|
+
if (idx < 0)
|
|
558
|
+
return null;
|
|
559
|
+
const jsonStart = text.indexOf('```json', idx);
|
|
560
|
+
const jsonEnd = text.indexOf('```', jsonStart + 7);
|
|
561
|
+
if (jsonStart < 0 || jsonEnd < 0)
|
|
562
|
+
return null;
|
|
563
|
+
try {
|
|
564
|
+
const parsed = JSON.parse(text.slice(jsonStart + 7, jsonEnd).trim());
|
|
565
|
+
const mode = parsed.executionMode?.mode;
|
|
566
|
+
if (typeof mode !== 'string')
|
|
567
|
+
return null;
|
|
568
|
+
return {
|
|
569
|
+
mode: mode === 'trusted' ? 'trusted' : 'coached',
|
|
570
|
+
completedRuns: typeof parsed.executionMode?.completedRuns === 'number' ? parsed.executionMode.completedRuns : 0,
|
|
571
|
+
};
|
|
572
|
+
}
|
|
573
|
+
catch {
|
|
574
|
+
return null;
|
|
575
|
+
}
|
|
576
|
+
}
|
|
480
577
|
function normalizeToolArgs(rawArgs) {
|
|
481
578
|
if (rawArgs && typeof rawArgs === 'object' && !Array.isArray(rawArgs)) {
|
|
482
579
|
return rawArgs;
|
|
@@ -996,11 +1093,13 @@ const availableByVersionProbeAsync = (command) => new Promise((resolve) => {
|
|
|
996
1093
|
// cached. The TTL is a safety net for a CLI installed outside the Hub; installs performed
|
|
997
1094
|
// THROUGH the Hub invalidate explicitly (see the install-agent route) so they appear at once.
|
|
998
1095
|
const EMPLOYEE_DETECTION_TTL_MS = 5 * 60 * 1000;
|
|
1096
|
+
const EMPLOYEE_DETECTION_REFRESH_SIGNAL_MS = 500;
|
|
999
1097
|
let employeeDetectionTtlMs = EMPLOYEE_DETECTION_TTL_MS;
|
|
1000
1098
|
let cachedEmployees = null;
|
|
1001
1099
|
let cachedEmployeesAtMs = 0;
|
|
1002
1100
|
let cachedEmployeesContext = null;
|
|
1003
1101
|
let inFlightDetection = null;
|
|
1102
|
+
let employeeDetectionRefreshSignalUntilMs = 0;
|
|
1004
1103
|
function employeeDetectionContext() {
|
|
1005
1104
|
return JSON.stringify({
|
|
1006
1105
|
PATH: process.env.PATH || '',
|
|
@@ -1090,6 +1189,7 @@ function adoptPersistedEmployees(persisted) {
|
|
|
1090
1189
|
cachedEmployeesAtMs = persisted.detectedAtMs;
|
|
1091
1190
|
cachedEmployeesContext = employeeDetectionContext();
|
|
1092
1191
|
if (Date.now() - persisted.detectedAtMs > employeeDetectionTtlMs) {
|
|
1192
|
+
employeeDetectionRefreshSignalUntilMs = Date.now() + EMPLOYEE_DETECTION_REFRESH_SIGNAL_MS;
|
|
1093
1193
|
void detectEmployeesAsync({ force: true }).catch(() => undefined);
|
|
1094
1194
|
}
|
|
1095
1195
|
return persisted.employees;
|
|
@@ -1126,6 +1226,7 @@ function invalidateEmployeeDetectionCache() {
|
|
|
1126
1226
|
cachedEmployeesAtMs = 0;
|
|
1127
1227
|
cachedEmployeesContext = null;
|
|
1128
1228
|
inFlightDetection = null;
|
|
1229
|
+
employeeDetectionRefreshSignalUntilMs = 0;
|
|
1129
1230
|
try {
|
|
1130
1231
|
fs_1.default.rmSync(agentAvailabilityFilePath(), { force: true });
|
|
1131
1232
|
}
|
|
@@ -1137,6 +1238,7 @@ function __clearEmployeeDetectionMemoryCacheForTests() {
|
|
|
1137
1238
|
cachedEmployeesAtMs = 0;
|
|
1138
1239
|
cachedEmployeesContext = null;
|
|
1139
1240
|
inFlightDetection = null;
|
|
1241
|
+
employeeDetectionRefreshSignalUntilMs = 0;
|
|
1140
1242
|
}
|
|
1141
1243
|
/** Test seam only. Mirrors __resetLatestVersionCache in hub-latest-version.ts. */
|
|
1142
1244
|
function __setEmployeeDetectionTtlForTests(ttlMs) {
|
|
@@ -1176,7 +1278,7 @@ function buildEmployeeStatus(id, version) {
|
|
|
1176
1278
|
* a one-shot answer.
|
|
1177
1279
|
*/
|
|
1178
1280
|
function isEmployeeDetectionRefreshing() {
|
|
1179
|
-
return inFlightDetection !== null;
|
|
1281
|
+
return inFlightDetection !== null || Date.now() < employeeDetectionRefreshSignalUntilMs;
|
|
1180
1282
|
}
|
|
1181
1283
|
/**
|
|
1182
1284
|
* Non-blocking employee detection. Probes every agent CONCURRENTLY, so the cost is the
|
|
@@ -1697,24 +1799,29 @@ function parseHostLine(hostId, line) {
|
|
|
1697
1799
|
return {};
|
|
1698
1800
|
// Scan every line for structured signals the Hub UI cares about:
|
|
1699
1801
|
// seekMentoring (tracker), get_fraim_job (job identity promotion),
|
|
1700
|
-
// turn-level usage (totals),
|
|
1701
|
-
// (cost lookup when the host doesn't emit it)
|
|
1802
|
+
// turn-level usage (totals), fraim_connect agent identity
|
|
1803
|
+
// (cost lookup when the host doesn't emit it), and recurringPark
|
|
1804
|
+
// (marker emitted by the mentor when transitioning to the recurring sentinel).
|
|
1702
1805
|
const seekMentoring = parseSeekMentoringSignal(trimmed);
|
|
1703
1806
|
const fraimJob = parseFraimJobLoadSignal(trimmed);
|
|
1807
|
+
const executionMode = parseExecutionModeSignal(trimmed);
|
|
1704
1808
|
const usage = parseUsageSignal(trimmed);
|
|
1705
1809
|
const agentIdentity = parseAgentIdentitySignal(trimmed);
|
|
1810
|
+
const recurringPark = parseRecurringParkSignal(trimmed);
|
|
1706
1811
|
// Issue #1264 (R3): only worth checking when seekMentoring itself did not
|
|
1707
1812
|
// already resolve — a recognized call has nothing to diagnose.
|
|
1708
1813
|
const unrecognizedReviewHandoffError = seekMentoring ? null : detectUnrecognizedReviewHandoffCall(trimmed);
|
|
1709
1814
|
const withSignal = (event) => {
|
|
1710
|
-
if (!seekMentoring && !fraimJob && !usage && !agentIdentity && !unrecognizedReviewHandoffError)
|
|
1815
|
+
if (!seekMentoring && !fraimJob && !executionMode && !usage && !agentIdentity && !recurringPark && !unrecognizedReviewHandoffError)
|
|
1711
1816
|
return event;
|
|
1712
1817
|
return {
|
|
1713
1818
|
...event,
|
|
1714
1819
|
...(seekMentoring ? { seekMentoring } : {}),
|
|
1715
1820
|
...(fraimJob ? { fraimJob } : {}),
|
|
1821
|
+
...(executionMode ? { executionMode } : {}),
|
|
1716
1822
|
...(usage ? { usage } : {}),
|
|
1717
1823
|
...(agentIdentity ? { agentIdentity } : {}),
|
|
1824
|
+
...(recurringPark ? { recurringPark: true } : {}),
|
|
1718
1825
|
...(unrecognizedReviewHandoffError && !event.hostError ? { hostError: unrecognizedReviewHandoffError } : {}),
|
|
1719
1826
|
};
|
|
1720
1827
|
};
|
|
@@ -58,9 +58,6 @@ const brand_store_1 = require("../core/brand-store");
|
|
|
58
58
|
const project_fraim_paths_1 = require("../core/utils/project-fraim-paths");
|
|
59
59
|
const catalog_1 = require("./catalog");
|
|
60
60
|
const job_visualization_1 = require("../core/job-visualization");
|
|
61
|
-
// Issue #1491: reserved sentinel phase a recurring job parks at between
|
|
62
|
-
// scheduled checks (registry/delivery/recurring-checkpoint.md).
|
|
63
|
-
const resolve_phase_edge_1 = require("../core/resolve-phase-edge");
|
|
64
61
|
const custom_employees_1 = require("./custom-employees");
|
|
65
62
|
const agent_token_prices_1 = require("../local-mcp-server/agent-token-prices");
|
|
66
63
|
const hosts_1 = require("./hosts");
|
|
@@ -442,7 +439,7 @@ class AiHubRunRegistry {
|
|
|
442
439
|
}
|
|
443
440
|
}
|
|
444
441
|
// ─── Issue #578: Deployment + Host stores ─────────────────────────────────────
|
|
445
|
-
const
|
|
442
|
+
const VALID_HOST_IDS = ['codex', 'claude', 'gemini', 'copilot', 'antigravity'];
|
|
446
443
|
const SCHEDULED_FIRE_LEASE_TTL_MS = 2 * 60 * 1000;
|
|
447
444
|
const SCHEDULED_FIRE_LEASE_PRUNE_MS = 7 * 24 * 60 * 60 * 1000;
|
|
448
445
|
function startSessionSeedForHost(hostId, runId) {
|
|
@@ -2614,6 +2611,19 @@ function classifyExit(run, exitCode) {
|
|
|
2614
2611
|
const declaredPath = (0, catalog_1.loadJobPhases)(run.jobId, run.projectPath, run.runDiscriminant || 'feature');
|
|
2615
2612
|
const lastDeclared = declaredPath.length > 0 ? declaredPath[declaredPath.length - 1] : null;
|
|
2616
2613
|
if (lastDeclared && lastDeclared.id === currentPhase) {
|
|
2614
|
+
// Issues #1491/#1509: the mentor emits <!-- FRAIM_RECURRING_PARK --> in its reply
|
|
2615
|
+
// when it resolves the completed phase to the recurring-checkpoint sentinel.
|
|
2616
|
+
// This takes priority over the 'done' classification because the recurring-checkpoint
|
|
2617
|
+
// phase IS by design the terminal phase of its job — the job is NOT done, it is
|
|
2618
|
+
// parked until its next scheduled invocation.
|
|
2619
|
+
if (run.recurringPark === true) {
|
|
2620
|
+
// Scheduled/webhook runs park at the recurring sentinel; manager-triggered
|
|
2621
|
+
// runs are handled conservatively so the manager sees a response.
|
|
2622
|
+
if (run.sourceTrigger === 'scheduled' || run.sourceTrigger === 'webhook') {
|
|
2623
|
+
return { action: 'park', pauseReason: 'parked_recurring' };
|
|
2624
|
+
}
|
|
2625
|
+
return { action: 'park', pauseReason: 'awaiting_user' };
|
|
2626
|
+
}
|
|
2617
2627
|
return { action: 'done', pauseReason: 'done' };
|
|
2618
2628
|
}
|
|
2619
2629
|
if (compactingActive) {
|
|
@@ -2637,13 +2647,10 @@ function classifyExit(run, exitCode) {
|
|
|
2637
2647
|
// real description, not the post-clear fallback.
|
|
2638
2648
|
return { action: 'resume', pauseReason: 'working', recoveryKind: 'background_task_lost', systemNote: describeKilledBackgroundTasks(run) };
|
|
2639
2649
|
}
|
|
2640
|
-
//
|
|
2641
|
-
//
|
|
2642
|
-
|
|
2643
|
-
|
|
2644
|
-
// (not expected in normal operation) keeps today's conservative awaiting_user default.
|
|
2645
|
-
if (currentPhase === resolve_phase_edge_1.WAITING_FOR_NEXT_INVOCATION
|
|
2646
|
-
&& (run.sourceTrigger === 'scheduled' || run.sourceTrigger === 'webhook')) {
|
|
2650
|
+
// Issues #1491/#1509: also check recurringPark for mid-job phases (not the last
|
|
2651
|
+
// declared phase) — covers jobs where the sentinel is reached before the final step.
|
|
2652
|
+
if ((run.sourceTrigger === 'scheduled' || run.sourceTrigger === 'webhook')
|
|
2653
|
+
&& run.recurringPark === true) {
|
|
2647
2654
|
return { action: 'park', pauseReason: 'parked_recurring' };
|
|
2648
2655
|
}
|
|
2649
2656
|
return { action: 'park', pauseReason: 'awaiting_user' };
|
|
@@ -3266,9 +3273,18 @@ class AiHubServer {
|
|
|
3266
3273
|
// cache miss the synchronous form would spawnSync per agent (~2.7s) and freeze the
|
|
3267
3274
|
// event loop, making every concurrent request wait. Falls back to the sync form for
|
|
3268
3275
|
// HostRuntime stubs that do not implement the async variant.
|
|
3276
|
+
let employeesRefreshing = false;
|
|
3269
3277
|
const employees = this.hostRuntime.detectEmployeesAsync
|
|
3270
|
-
? await
|
|
3278
|
+
? await (() => {
|
|
3279
|
+
const wasRefreshing = this.hostRuntime.isEmployeeDetectionRefreshing?.() ?? false;
|
|
3280
|
+
const detection = this.hostRuntime.detectEmployeesAsync();
|
|
3281
|
+
employeesRefreshing = !wasRefreshing && (this.hostRuntime.isEmployeeDetectionRefreshing?.() ?? false);
|
|
3282
|
+
return detection;
|
|
3283
|
+
})()
|
|
3271
3284
|
: this.hostRuntime.detectEmployees();
|
|
3285
|
+
if (!this.hostRuntime.detectEmployeesAsync) {
|
|
3286
|
+
employeesRefreshing = this.hostRuntime.isEmployeeDetectionRefreshing?.() ?? false;
|
|
3287
|
+
}
|
|
3272
3288
|
const configuredAgents = this.configuredAgentStore
|
|
3273
3289
|
.listWithDefaults(employees)
|
|
3274
3290
|
.map((agent) => (0, configured_agents_1.projectConfiguredAgent)(agent, employees));
|
|
@@ -3360,7 +3376,7 @@ class AiHubServer {
|
|
|
3360
3376
|
// Issue #1256 (slice b, AC-B1): true when `employees` above may be a stale persisted
|
|
3361
3377
|
// snapshot while a background re-probe is in flight. The client uses this to schedule
|
|
3362
3378
|
// a bounded follow-up refresh instead of trusting a one-shot bootstrap forever.
|
|
3363
|
-
employeesRefreshing
|
|
3379
|
+
employeesRefreshing,
|
|
3364
3380
|
configuredAgents,
|
|
3365
3381
|
personas,
|
|
3366
3382
|
// Issue #1005: lets the client tell a resolved projection from the first-paint
|
|
@@ -3558,7 +3574,23 @@ class AiHubServer {
|
|
|
3558
3574
|
run.resumeCommand = (0, hosts_1.buildInteractiveResumeCommand)(hostIdForCommand, sessionId);
|
|
3559
3575
|
}
|
|
3560
3576
|
recordHostEvent(run, hostId, event, channel) {
|
|
3577
|
+
// Issue #1509: reset when a new agent process session starts (system event with
|
|
3578
|
+
// sessionId). Without this, a marker set during a compaction or background-task-lost
|
|
3579
|
+
// auto-resume's first process would bleed into the continuation's unrelated exit and
|
|
3580
|
+
// force parked_recurring on work that has nothing to do with the recurring sentinel.
|
|
3581
|
+
if (channel === 'system' && event.sessionId)
|
|
3582
|
+
run.recurringPark = undefined;
|
|
3583
|
+
// Set once the marker is confirmed in the current process; cleared above at next start.
|
|
3584
|
+
if (event.recurringPark)
|
|
3585
|
+
run.recurringPark = true;
|
|
3561
3586
|
appendHostMessage(run, hostId, event, channel);
|
|
3587
|
+
if (event.executionMode) {
|
|
3588
|
+
const normalizedMode = event.executionMode.mode === 'trusted' ? 'trusted' : 'coached';
|
|
3589
|
+
const normalizedRuns = typeof event.executionMode.completedRuns === 'number'
|
|
3590
|
+
? event.executionMode.completedRuns
|
|
3591
|
+
: 0;
|
|
3592
|
+
run.executionMode = { mode: normalizedMode, completedRuns: normalizedRuns };
|
|
3593
|
+
}
|
|
3562
3594
|
// Issue #1221: a real assistant reply proves the host recovered, so a
|
|
3563
3595
|
// previously recorded error is stale and must not keep haunting the next
|
|
3564
3596
|
// exit/recovery message. Issue #1284: a classified recovery signature is
|
|
@@ -5458,11 +5490,20 @@ class AiHubServer {
|
|
|
5458
5490
|
}
|
|
5459
5491
|
const employees = this.hostRuntime.detectEmployees();
|
|
5460
5492
|
const priorBaseHostId = (conversation.baseHostId || conversation.agentName);
|
|
5461
|
-
const priorSessionId = typeof conversation.sessionId === 'string' && conversation.sessionId.trim() ? conversation.sessionId.trim() : '';
|
|
5462
5493
|
const resolved = this.resolveLaunchAgent(configuredAgentId, undefined, employees);
|
|
5494
|
+
// Issue #1515: a shared baseHostId (e.g. Codex vs. Codex Azure Script) does not mean
|
|
5495
|
+
// the two configured agents share a session/auth store. Require the base host to
|
|
5496
|
+
// still match, then resolve the owner-qualified host session from #1047's ledger
|
|
5497
|
+
// instead of trusting whichever session the conversation's legacy `sessionId` field
|
|
5498
|
+
// last mirrored, so a switch across an isolated CODEX_HOME always restarts with a
|
|
5499
|
+
// handoff brief rather than attempting a resume that cannot succeed.
|
|
5500
|
+
const resolvedHostSession = priorBaseHostId === resolved.hostId
|
|
5501
|
+
? host_session_state_1.hostSessionState.resolve(conversation, { configuredAgentId: resolved.agent.id, baseHostId: resolved.hostId })
|
|
5502
|
+
: null;
|
|
5503
|
+
const priorSessionId = resolvedHostSession?.sessionId || '';
|
|
5463
5504
|
const managerNote = (body.instructions || '').trim() || 'Continue this job from the preserved Hub conversation state.';
|
|
5464
5505
|
const handoffSummary = buildAgentSwitchHandoffSummary(conversation);
|
|
5465
|
-
const canResumeSameHost =
|
|
5506
|
+
const canResumeSameHost = !!priorSessionId;
|
|
5466
5507
|
const initialStrategy = canResumeSameHost ? 'resume_same_host' : 'restart_with_handoff';
|
|
5467
5508
|
const activePriorRun = typeof conversation.runId === 'string' ? this.runRegistry.get(conversation.runId) : undefined;
|
|
5468
5509
|
if (activePriorRun?.status === 'running') {
|
|
@@ -6580,6 +6621,7 @@ class AiHubServer {
|
|
|
6580
6621
|
// offering next-job recommendations immediately, not only once the
|
|
6581
6622
|
// resumed session's next seekMentoring signal arrives.
|
|
6582
6623
|
current.nextJobRecommendations = null;
|
|
6624
|
+
current.reviewHandoff = null;
|
|
6583
6625
|
});
|
|
6584
6626
|
const startedFresh = this.runRegistry.get(run.id);
|
|
6585
6627
|
if (startedFresh)
|
|
@@ -6639,6 +6681,7 @@ class AiHubServer {
|
|
|
6639
6681
|
// Issue #1373 (Defect 2): see the no-session branch above for why
|
|
6640
6682
|
// this can only be stale at coaching-send time.
|
|
6641
6683
|
current.nextJobRecommendations = null;
|
|
6684
|
+
current.reviewHandoff = null;
|
|
6642
6685
|
});
|
|
6643
6686
|
const started = this.runRegistry.get(run.id);
|
|
6644
6687
|
if (started)
|
|
@@ -7102,8 +7145,8 @@ class AiHubServer {
|
|
|
7102
7145
|
if (!cronLib.validate(cronExpr)) {
|
|
7103
7146
|
return res.status(400).json({ error: 'Invalid cron expression.' });
|
|
7104
7147
|
}
|
|
7105
|
-
const
|
|
7106
|
-
const resolvedHostId =
|
|
7148
|
+
const validHosts = VALID_HOST_IDS;
|
|
7149
|
+
const resolvedHostId = validHosts.includes(hostId) ? hostId : 'claude';
|
|
7107
7150
|
let resolvedProjectPath;
|
|
7108
7151
|
let normalizedExpiresAt;
|
|
7109
7152
|
try {
|
|
@@ -7212,7 +7255,7 @@ class AiHubServer {
|
|
|
7212
7255
|
catch (err) {
|
|
7213
7256
|
return res.status(400).json({ error: err instanceof Error ? err.message : 'Invalid project path.' });
|
|
7214
7257
|
}
|
|
7215
|
-
const
|
|
7258
|
+
const validHosts = VALID_HOST_IDS;
|
|
7216
7259
|
const existing = this.deploymentStore.load().find((d) => d.id === id && d.type === 'scheduled');
|
|
7217
7260
|
if (!existing)
|
|
7218
7261
|
return res.status(404).json({ error: 'Deployment not found.' });
|
|
@@ -7221,7 +7264,7 @@ class AiHubServer {
|
|
|
7221
7264
|
if (jobId !== undefined && !this.resolveHubJob(resolvedProjectPath !== undefined ? resolvedProjectPath : existing.projectPath, jobId)) {
|
|
7222
7265
|
return res.status(400).json(this.invalidScheduleJobIdError(jobId));
|
|
7223
7266
|
}
|
|
7224
|
-
const nextHostId = hostId !== undefined &&
|
|
7267
|
+
const nextHostId = hostId !== undefined && validHosts.includes(hostId) ? hostId : existing.hostId;
|
|
7225
7268
|
const nextConfiguredAgentId = configuredAgentId !== undefined
|
|
7226
7269
|
? (typeof configuredAgentId === 'string' && configuredAgentId.trim() ? configuredAgentId.trim() : undefined)
|
|
7227
7270
|
: existing.configuredAgentId;
|
|
@@ -7294,8 +7337,8 @@ class AiHubServer {
|
|
|
7294
7337
|
if (!label || !jobId) {
|
|
7295
7338
|
return res.status(400).json({ error: 'label and jobId are required.' });
|
|
7296
7339
|
}
|
|
7297
|
-
const
|
|
7298
|
-
const resolvedHostId =
|
|
7340
|
+
const validHosts = VALID_HOST_IDS;
|
|
7341
|
+
const resolvedHostId = validHosts.includes(hostId) ? hostId : 'claude';
|
|
7299
7342
|
const now = new Date().toISOString();
|
|
7300
7343
|
const deployment = {
|
|
7301
7344
|
id: (0, crypto_1.randomUUID)(),
|
|
@@ -7347,7 +7390,7 @@ class AiHubServer {
|
|
|
7347
7390
|
catch (err) {
|
|
7348
7391
|
return res.status(400).json({ error: err instanceof Error ? err.message : 'Invalid project path.' });
|
|
7349
7392
|
}
|
|
7350
|
-
const
|
|
7393
|
+
const validHosts = VALID_HOST_IDS;
|
|
7351
7394
|
let updated = null;
|
|
7352
7395
|
const ok = this.deploymentStore.update(id, (dep) => {
|
|
7353
7396
|
if (label !== undefined)
|
|
@@ -7356,7 +7399,7 @@ class AiHubServer {
|
|
|
7356
7399
|
dep.jobId = jobId;
|
|
7357
7400
|
if (resolvedProjectPath !== undefined)
|
|
7358
7401
|
dep.projectPath = resolvedProjectPath;
|
|
7359
|
-
if (hostId !== undefined &&
|
|
7402
|
+
if (hostId !== undefined && validHosts.includes(hostId))
|
|
7360
7403
|
dep.hostId = hostId;
|
|
7361
7404
|
if (configuredAgentId !== undefined)
|
|
7362
7405
|
dep.configuredAgentId = typeof configuredAgentId === 'string' && configuredAgentId.trim() ? configuredAgentId.trim() : undefined;
|
|
@@ -7640,23 +7683,33 @@ class AiHubServer {
|
|
|
7640
7683
|
// Stable API endpoint for extension surfaces (Office add-ins, browser
|
|
7641
7684
|
// extensions, VS Code extensions, Electron tray) to start FRAIM jobs.
|
|
7642
7685
|
//
|
|
7643
|
-
// Body: {
|
|
7644
|
-
// Response: {
|
|
7686
|
+
// Body: { configuredAgentId?, hostId?, personaKey?, jobName, context?, projectPath? }
|
|
7687
|
+
// Response: { hubRunId, status, hostId, configuredAgentId, personaKey, jobName }
|
|
7645
7688
|
// -------------------------------------------------------------------------
|
|
7646
7689
|
this.app.post('/api/trigger', (req, res) => {
|
|
7647
7690
|
try {
|
|
7648
|
-
const {
|
|
7649
|
-
|
|
7650
|
-
|
|
7651
|
-
|
|
7691
|
+
const { jobName, context, projectPath: reqProjectPath } = req.body;
|
|
7692
|
+
const configuredAgentId = typeof req.body.configuredAgentId === 'string' ? req.body.configuredAgentId.trim() : '';
|
|
7693
|
+
const hostIdInput = typeof req.body.hostId === 'string' ? req.body.hostId.trim() : '';
|
|
7694
|
+
const explicitPersonaKey = typeof req.body.personaKey === 'string' ? req.body.personaKey.trim() : '';
|
|
7652
7695
|
if (!jobName) {
|
|
7653
7696
|
return res.status(400).json({ error: 'jobName is required.' });
|
|
7654
7697
|
}
|
|
7698
|
+
if (hostIdInput && !VALID_HOST_IDS.includes(hostIdInput)) {
|
|
7699
|
+
return res.status(400).json({ error: 'hostId must be one of the supported execution hosts.' });
|
|
7700
|
+
}
|
|
7655
7701
|
const projectPath = ensureDirectoryPath(reqProjectPath || this.defaultProjectPath());
|
|
7656
|
-
// Use the requested
|
|
7657
|
-
const
|
|
7658
|
-
const requestedHostId =
|
|
7659
|
-
|
|
7702
|
+
// Use the requested configured profile or base host directly.
|
|
7703
|
+
const requestedConfiguredAgentId = configuredAgentId || undefined;
|
|
7704
|
+
const requestedHostId = hostIdInput ? hostIdInput : undefined;
|
|
7705
|
+
if (!requestedConfiguredAgentId && !requestedHostId) {
|
|
7706
|
+
return res.status(400).json({ error: 'configuredAgentId or hostId is required.' });
|
|
7707
|
+
}
|
|
7708
|
+
const { hostId, agent: configuredAgent, launchContext } = this.resolveLaunchAgent(requestedConfiguredAgentId, requestedHostId);
|
|
7709
|
+
if (requestedConfiguredAgentId && requestedHostId && hostId !== requestedHostId) {
|
|
7710
|
+
return res.status(400).json({ error: 'configuredAgentId does not match hostId.' });
|
|
7711
|
+
}
|
|
7712
|
+
const personaKey = explicitPersonaKey || getCustomPersonaForJob(projectPath, jobName) || getHubPersonaForJob(jobName);
|
|
7660
7713
|
const contextText = context?.text?.trim() || '';
|
|
7661
7714
|
const sourceInfo = [
|
|
7662
7715
|
context?.sourceApp ? `sourceApp: ${context.sourceApp}` : '',
|
|
@@ -7687,8 +7740,7 @@ class AiHubServer {
|
|
|
7687
7740
|
phaseVisits: [],
|
|
7688
7741
|
totals: emptyTotals(),
|
|
7689
7742
|
lastStatusChangeAt: startTimestamp,
|
|
7690
|
-
|
|
7691
|
-
personaKey: getCustomPersonaForJob(projectPath, jobName) ?? getHubPersonaForJob(jobName),
|
|
7743
|
+
personaKey,
|
|
7692
7744
|
};
|
|
7693
7745
|
// Register the run before spawning so onEvent/onExit callbacks can
|
|
7694
7746
|
// safely call update() even if they fire synchronously (FakeHostRuntime).
|
|
@@ -7726,7 +7778,14 @@ class AiHubServer {
|
|
|
7726
7778
|
}, startSessionSeedForHost(hostId, run.id), this.withHubRunIdEnv(launchContext, run.id));
|
|
7727
7779
|
// Update the registry entry with the real child process handle.
|
|
7728
7780
|
this.runRegistry.attachChildIfRunning(run.id, child);
|
|
7729
|
-
return res.json({
|
|
7781
|
+
return res.json({
|
|
7782
|
+
hubRunId: run.id,
|
|
7783
|
+
status: 'started',
|
|
7784
|
+
hostId,
|
|
7785
|
+
configuredAgentId: configuredAgent.id,
|
|
7786
|
+
personaKey,
|
|
7787
|
+
jobName,
|
|
7788
|
+
});
|
|
7730
7789
|
}
|
|
7731
7790
|
catch (error) {
|
|
7732
7791
|
return res.status(400).json({ error: error instanceof Error ? error.message : 'Could not start run.' });
|
|
@@ -81,7 +81,7 @@ exports.PERSONA_CAPABILITY_BUNDLES = {
|
|
|
81
81
|
personaKey: 'huxley',
|
|
82
82
|
bundleId: 'persona-huxley-core',
|
|
83
83
|
catalogMetadata: buildCatalogMetadata('huxley', ['design-system-creation', 'user-facing-prototyping', 'ux-research-synthesis']),
|
|
84
|
-
protectedJobs: ['design-system-creation', 'user-facing-prototyping', 'website-creation', 'brand-creation', 'branding-quality-audit', 'ux-research-synthesis', 'user-journey-mapping'],
|
|
84
|
+
protectedJobs: ['design-system-creation', 'user-facing-prototyping', 'website-creation', 'brand-creation', 'branding-quality-audit', 'ux-research-synthesis', 'user-journey-mapping', 'ui-quality-assessment'],
|
|
85
85
|
protectedAliases: ['ux-design', 'brand-design'],
|
|
86
86
|
defaultHireMode: 'job',
|
|
87
87
|
lockCopy: 'Hire hUXley to unlock design-system, UX research, and brand design work for this request.'
|
|
@@ -39,6 +39,8 @@ exports.QUALITY_REGISTRY = {
|
|
|
39
39
|
'branding-quality-audit': { stage: 'branding', enforced: true, telemetryKind: 'score' },
|
|
40
40
|
// Product Quality
|
|
41
41
|
'code-quality-assessment': { stage: 'product-quality', enforced: true, telemetryKind: 'score' },
|
|
42
|
+
// UI/UX Quality
|
|
43
|
+
'ui-quality-assessment': { stage: 'ui-ux-quality', enforced: true, telemetryKind: 'score' },
|
|
42
44
|
// Test Quality
|
|
43
45
|
'test-quality-assessment': { stage: 'test-quality', enforced: true, telemetryKind: 'score' },
|
|
44
46
|
// Security
|
|
@@ -76,6 +78,7 @@ exports.STAGE_DISPLAY_NAMES = {
|
|
|
76
78
|
'business-strategy': 'Business Strategy',
|
|
77
79
|
'branding': 'Branding',
|
|
78
80
|
'product-quality': 'Product Quality',
|
|
81
|
+
'ui-ux-quality': 'UI/UX Quality',
|
|
79
82
|
'test-quality': 'Test Quality',
|
|
80
83
|
'security': 'Security',
|
|
81
84
|
'production-readiness': 'Production Readiness',
|
|
@@ -90,6 +93,7 @@ exports.ALL_STAGE_CATEGORIES = [
|
|
|
90
93
|
'business-strategy',
|
|
91
94
|
'branding',
|
|
92
95
|
'product-quality',
|
|
96
|
+
'ui-ux-quality',
|
|
93
97
|
'test-quality',
|
|
94
98
|
'security',
|
|
95
99
|
'production-readiness',
|
|
@@ -153,6 +157,12 @@ const QUALITY_SCORE_DIMENSIONS = {
|
|
|
153
157
|
'architecture',
|
|
154
158
|
'maintainability'
|
|
155
159
|
],
|
|
160
|
+
'ui-quality-assessment': [
|
|
161
|
+
'lookAndFeel',
|
|
162
|
+
'usabilityHeuristics',
|
|
163
|
+
'interactionConsistency',
|
|
164
|
+
'accessibility'
|
|
165
|
+
],
|
|
156
166
|
'test-quality-assessment': [
|
|
157
167
|
'coverage',
|
|
158
168
|
'testIntegrity',
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fraim-hub",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.303",
|
|
4
4
|
"description": "FRAIM Hub local companion package.",
|
|
5
5
|
"author": "Sid Mathur <sid.mathur@gmail.com>",
|
|
6
6
|
"homepage": "https://github.com/mathursrus/FRAIM#readme",
|
|
@@ -211,7 +211,7 @@
|
|
|
211
211
|
"electron-updater": "^6.8.9",
|
|
212
212
|
"express": "^5.2.1",
|
|
213
213
|
"extract-zip": "^2.0.1",
|
|
214
|
-
"fraim": "2.0.
|
|
214
|
+
"fraim": "2.0.303",
|
|
215
215
|
"mongodb": "^7.0.0",
|
|
216
216
|
"node-cron": "4.2.1",
|
|
217
217
|
"node-edge-tts": "^1.2.10",
|
package/public/ai-hub/script.js
CHANGED
|
@@ -3187,6 +3187,15 @@ function truncateKickoffForTitle(text) {
|
|
|
3187
3187
|
return words.slice(0, 6).join(' ');
|
|
3188
3188
|
}
|
|
3189
3189
|
|
|
3190
|
+
function hasManagerKickoffMessage(conv) {
|
|
3191
|
+
return !!(conv && Array.isArray(conv.messages)
|
|
3192
|
+
&& conv.messages.some((message) => message && message.role === 'manager' && (message.text || '').trim()));
|
|
3193
|
+
}
|
|
3194
|
+
|
|
3195
|
+
function savedTitleMatchesKickoff(conv, kickoffText) {
|
|
3196
|
+
return !!(conv && conv.title && String(conv.title).replace(/\s+/g, ' ').trim() === String(kickoffText || '').replace(/\s+/g, ' ').trim());
|
|
3197
|
+
}
|
|
3198
|
+
|
|
3190
3199
|
function kickoffDescription(conv) {
|
|
3191
3200
|
const text = managerKickoffText(conv);
|
|
3192
3201
|
if (!text) return '';
|
|
@@ -3197,9 +3206,12 @@ function kickoffDescription(conv) {
|
|
|
3197
3206
|
function composedConversationTitle(conv) {
|
|
3198
3207
|
if (!conv) return '';
|
|
3199
3208
|
const jobName = resolvedConversationJobName(conv);
|
|
3200
|
-
const
|
|
3209
|
+
const kickoffText = managerKickoffText(conv);
|
|
3210
|
+
const kickoff = truncateKickoffForTitle(kickoffText);
|
|
3201
3211
|
if (jobName && kickoff) return `${jobName}: ${kickoff}`;
|
|
3202
3212
|
if (jobName) return jobName;
|
|
3213
|
+
if (kickoff && hasManagerKickoffMessage(conv) && savedTitleMatchesKickoff(conv, kickoffText)) return kickoff;
|
|
3214
|
+
if (conv && conv.title) return conv.title;
|
|
3203
3215
|
if (kickoff) return kickoff;
|
|
3204
3216
|
return 'New job';
|
|
3205
3217
|
}
|