fraim-hub 2.0.301 → 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/README.md +1 -1
- package/dist/src/ai-hub/configured-agents.js +17 -17
- package/dist/src/ai-hub/conversation-store.js +7 -0
- package/dist/src/ai-hub/host-session-state.js +9 -0
- package/dist/src/ai-hub/hosts.js +144 -12
- package/dist/src/ai-hub/server.js +238 -42
- package/dist/src/cli/doctor/checks/agent-cli-health-checks.js +26 -2
- package/dist/src/cli/setup/ide-detector.js +74 -4
- package/dist/src/cli/utils/managed-agent-paths.js +183 -1
- package/dist/src/config/persona-capability-bundles.js +3 -3
- package/dist/src/core/quality-evidence.js +10 -0
- package/dist/src/first-run/session-service.js +37 -7
- package/package.json +2 -2
- package/public/ai-hub/script.js +143 -6
- package/public/ai-hub/styles.css +17 -0
- package/public/first-run/script.js +19 -51
package/README.md
CHANGED
|
@@ -312,7 +312,7 @@ Use these defaults:
|
|
|
312
312
|
- `feature-specification` when the request is still fuzzy or needs clarified requirements, UX, or acceptance criteria.
|
|
313
313
|
- `technical-design` after the spec is approved and you need the implementation plan, file touchpoints, and risk handling.
|
|
314
314
|
- `feature-implementation` for code changes, bug fixes, and documentation updates that should be executed and validated.
|
|
315
|
-
- `test-
|
|
315
|
+
- `test-authoring` when you need reproduction coverage, missing tests, or stronger regression protection before implementation.
|
|
316
316
|
- `browser-application-validation` or `ui-polish-validation` after user-facing UI changes or when the ask is explicitly browser validation.
|
|
317
317
|
- `implementation-feature-review` when you need to verify the delivered behavior matches the feature spec.
|
|
318
318
|
- `implementation-design-review` when you need to verify the code matches the approved technical design.
|
|
@@ -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,
|
|
@@ -689,6 +689,13 @@ class AiHubConversationStore {
|
|
|
689
689
|
const { headers } = this.loadIndex(this.bucketDir(key), key);
|
|
690
690
|
return headers.map((h) => ({ ...h }));
|
|
691
691
|
}
|
|
692
|
+
// Issue #1477: the active conversation id, from the same cheap index used by
|
|
693
|
+
// loadProjectHeaders, so a headers-only list response can still report it.
|
|
694
|
+
loadProjectActiveId(projectPath) {
|
|
695
|
+
this.ensureMigrated();
|
|
696
|
+
const key = normalizeConversationKey(projectPath);
|
|
697
|
+
return this.loadIndex(this.bucketDir(key), key).activeId;
|
|
698
|
+
}
|
|
692
699
|
// Lazy body read: one full conversation.
|
|
693
700
|
loadConversation(projectPath, conversationId) {
|
|
694
701
|
this.ensureMigrated();
|
|
@@ -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;
|
|
@@ -16,6 +18,7 @@ exports.__clearEmployeeDetectionMemoryCacheForTests = __clearEmployeeDetectionMe
|
|
|
16
18
|
exports.__setEmployeeDetectionTtlForTests = __setEmployeeDetectionTtlForTests;
|
|
17
19
|
exports.__getEmployeeProbeRoundsForTests = __getEmployeeProbeRoundsForTests;
|
|
18
20
|
exports.__resetEmployeeProbeRoundsForTests = __resetEmployeeProbeRoundsForTests;
|
|
21
|
+
exports.isEmployeeDetectionRefreshing = isEmployeeDetectionRefreshing;
|
|
19
22
|
exports.detectEmployeesAsync = detectEmployeesAsync;
|
|
20
23
|
exports.detectEmployees = detectEmployees;
|
|
21
24
|
exports.prepareCodexBrowserHome = prepareCodexBrowserHome;
|
|
@@ -123,6 +126,41 @@ function parseSeekMentoringSignal(line) {
|
|
|
123
126
|
}
|
|
124
127
|
return null;
|
|
125
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
|
+
}
|
|
126
164
|
// Issue #710: extract the job name from a get_fraim_job tool call. This
|
|
127
165
|
// is structured known-job evidence; the server still validates the job id
|
|
128
166
|
// against the catalog before promoting any run.
|
|
@@ -183,6 +221,43 @@ function parseFraimJobLoadSignal(line) {
|
|
|
183
221
|
}
|
|
184
222
|
return null;
|
|
185
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
|
+
}
|
|
186
261
|
// Issue #347 — extract per-turn usage from the host's JSON stream.
|
|
187
262
|
// Codex: `{"type":"turn.completed","usage":{input_tokens, cached_input_tokens, output_tokens, reasoning_output_tokens}}`.
|
|
188
263
|
// Claude Code: `{"type":"result", ..., "usage":{input_tokens, output_tokens, cache_creation_input_tokens, cache_read_input_tokens}, "total_cost_usd": ...}`.
|
|
@@ -476,6 +551,29 @@ function readFraimJobFromArgs(rawArgs) {
|
|
|
476
551
|
return null;
|
|
477
552
|
return { jobId, source: 'get_fraim_job' };
|
|
478
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
|
+
}
|
|
479
577
|
function normalizeToolArgs(rawArgs) {
|
|
480
578
|
if (rawArgs && typeof rawArgs === 'object' && !Array.isArray(rawArgs)) {
|
|
481
579
|
return rawArgs;
|
|
@@ -895,10 +993,11 @@ function resolveHostInvocation(plan) {
|
|
|
895
993
|
args: ['/d', '/s', '/c', [command, ...args].map(escapeWindowsArg).join(' ')],
|
|
896
994
|
};
|
|
897
995
|
}
|
|
996
|
+
// Issue #1256 (slice a, AC-A3): delegates to the shared recovery helper so this probe agrees
|
|
997
|
+
// with server.ts's hubCommandVersion() and fraim doctor's health check on the same PATH,
|
|
998
|
+
// instead of each independently deciding what "the agent's PATH" means.
|
|
898
999
|
function buildAgentVersionProbePath(basePath) {
|
|
899
|
-
|
|
900
|
-
const withoutProjectBins = (0, managed_agent_paths_1.stripProjectLocalNodeBinDirs)(withoutManaged);
|
|
901
|
-
return (0, managed_agent_paths_1.appendBinDirsToPath)(withoutProjectBins, (0, managed_agent_paths_1.getManagedAgentBinDirs)());
|
|
1000
|
+
return (0, managed_agent_paths_1.buildRecoveredAgentPath)(basePath);
|
|
902
1001
|
}
|
|
903
1002
|
// Single source for the probe environment, shared by the sync and async probes so the two
|
|
904
1003
|
// cannot drift on how agent bin directories are put on PATH. Project-local npm shims are
|
|
@@ -918,7 +1017,10 @@ const availableByVersionProbe = (command) => {
|
|
|
918
1017
|
});
|
|
919
1018
|
if (result.status !== 0 || result.error)
|
|
920
1019
|
return null;
|
|
921
|
-
|
|
1020
|
+
// Issue #1256 (AC-D1/D2): only stdout counts as a version — an agent that exits 0 while
|
|
1021
|
+
// writing only to stderr (e.g. GitHub Copilot CLI's current failure mode) is broken, not
|
|
1022
|
+
// "available with an odd version string".
|
|
1023
|
+
return (0, managed_agent_paths_1.versionFromProbeOutput)(result.stdout, result.stderr);
|
|
922
1024
|
};
|
|
923
1025
|
// Issue #1010: the async counterpart of availableByVersionProbe. Same semantics (exit 0
|
|
924
1026
|
// from `<agent> --version` means the CLI is installed AND actually runs), but non-blocking
|
|
@@ -937,7 +1039,11 @@ const availableByVersionProbeAsync = (command) => new Promise((resolve) => {
|
|
|
937
1039
|
let settled = false;
|
|
938
1040
|
let timer;
|
|
939
1041
|
let child;
|
|
940
|
-
|
|
1042
|
+
// Issue #1256 (AC-D1/D2): kept as separate stdout/stderr buffers rather than one combined
|
|
1043
|
+
// buffer, so a broken CLI that exits 0 while writing only to stderr cannot have its error
|
|
1044
|
+
// text mistaken for a version string.
|
|
1045
|
+
const stdoutChunks = [];
|
|
1046
|
+
const stderrChunks = [];
|
|
941
1047
|
const finish = (value) => {
|
|
942
1048
|
if (settled)
|
|
943
1049
|
return;
|
|
@@ -951,8 +1057,8 @@ const availableByVersionProbeAsync = (command) => new Promise((resolve) => {
|
|
|
951
1057
|
env: versionProbeEnv(),
|
|
952
1058
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
953
1059
|
});
|
|
954
|
-
child.stdout?.on('data', (chunk) =>
|
|
955
|
-
child.stderr?.on('data', (chunk) =>
|
|
1060
|
+
child.stdout?.on('data', (chunk) => stdoutChunks.push(chunk));
|
|
1061
|
+
child.stderr?.on('data', (chunk) => stderrChunks.push(chunk));
|
|
956
1062
|
timer = setTimeout(() => {
|
|
957
1063
|
console.warn(`[ai-hub] agent version probe timed out after ${VERSION_PROBE_TIMEOUT_MS}ms: ${command}`);
|
|
958
1064
|
try {
|
|
@@ -965,8 +1071,9 @@ const availableByVersionProbeAsync = (command) => new Promise((resolve) => {
|
|
|
965
1071
|
timer.unref?.();
|
|
966
1072
|
child.on('error', () => finish(null));
|
|
967
1073
|
child.on('close', (code) => {
|
|
968
|
-
const
|
|
969
|
-
|
|
1074
|
+
const stdout = Buffer.concat(stdoutChunks).toString('utf8');
|
|
1075
|
+
const stderr = Buffer.concat(stderrChunks).toString('utf8');
|
|
1076
|
+
finish(code === 0 ? (0, managed_agent_paths_1.versionFromProbeOutput)(stdout, stderr) : null);
|
|
970
1077
|
});
|
|
971
1078
|
}
|
|
972
1079
|
catch {
|
|
@@ -986,11 +1093,13 @@ const availableByVersionProbeAsync = (command) => new Promise((resolve) => {
|
|
|
986
1093
|
// cached. The TTL is a safety net for a CLI installed outside the Hub; installs performed
|
|
987
1094
|
// THROUGH the Hub invalidate explicitly (see the install-agent route) so they appear at once.
|
|
988
1095
|
const EMPLOYEE_DETECTION_TTL_MS = 5 * 60 * 1000;
|
|
1096
|
+
const EMPLOYEE_DETECTION_REFRESH_SIGNAL_MS = 500;
|
|
989
1097
|
let employeeDetectionTtlMs = EMPLOYEE_DETECTION_TTL_MS;
|
|
990
1098
|
let cachedEmployees = null;
|
|
991
1099
|
let cachedEmployeesAtMs = 0;
|
|
992
1100
|
let cachedEmployeesContext = null;
|
|
993
1101
|
let inFlightDetection = null;
|
|
1102
|
+
let employeeDetectionRefreshSignalUntilMs = 0;
|
|
994
1103
|
function employeeDetectionContext() {
|
|
995
1104
|
return JSON.stringify({
|
|
996
1105
|
PATH: process.env.PATH || '',
|
|
@@ -1080,6 +1189,7 @@ function adoptPersistedEmployees(persisted) {
|
|
|
1080
1189
|
cachedEmployeesAtMs = persisted.detectedAtMs;
|
|
1081
1190
|
cachedEmployeesContext = employeeDetectionContext();
|
|
1082
1191
|
if (Date.now() - persisted.detectedAtMs > employeeDetectionTtlMs) {
|
|
1192
|
+
employeeDetectionRefreshSignalUntilMs = Date.now() + EMPLOYEE_DETECTION_REFRESH_SIGNAL_MS;
|
|
1083
1193
|
void detectEmployeesAsync({ force: true }).catch(() => undefined);
|
|
1084
1194
|
}
|
|
1085
1195
|
return persisted.employees;
|
|
@@ -1116,6 +1226,7 @@ function invalidateEmployeeDetectionCache() {
|
|
|
1116
1226
|
cachedEmployeesAtMs = 0;
|
|
1117
1227
|
cachedEmployeesContext = null;
|
|
1118
1228
|
inFlightDetection = null;
|
|
1229
|
+
employeeDetectionRefreshSignalUntilMs = 0;
|
|
1119
1230
|
try {
|
|
1120
1231
|
fs_1.default.rmSync(agentAvailabilityFilePath(), { force: true });
|
|
1121
1232
|
}
|
|
@@ -1127,6 +1238,7 @@ function __clearEmployeeDetectionMemoryCacheForTests() {
|
|
|
1127
1238
|
cachedEmployeesAtMs = 0;
|
|
1128
1239
|
cachedEmployeesContext = null;
|
|
1129
1240
|
inFlightDetection = null;
|
|
1241
|
+
employeeDetectionRefreshSignalUntilMs = 0;
|
|
1130
1242
|
}
|
|
1131
1243
|
/** Test seam only. Mirrors __resetLatestVersionCache in hub-latest-version.ts. */
|
|
1132
1244
|
function __setEmployeeDetectionTtlForTests(ttlMs) {
|
|
@@ -1158,6 +1270,16 @@ function buildEmployeeStatus(id, version) {
|
|
|
1158
1270
|
supportsRaw: supportsDirectPath(id),
|
|
1159
1271
|
};
|
|
1160
1272
|
}
|
|
1273
|
+
/**
|
|
1274
|
+
* Issue #1256 (slice b, AC-B1): true when a background re-probe is currently in flight —
|
|
1275
|
+
* i.e. the most recent detectEmployees()/detectEmployeesAsync() answer may have been served
|
|
1276
|
+
* from a stale persisted snapshot rather than a fresh probe. bootstrapResponse reads this
|
|
1277
|
+
* right after detection so the client can be told to poll again shortly instead of latching
|
|
1278
|
+
* a one-shot answer.
|
|
1279
|
+
*/
|
|
1280
|
+
function isEmployeeDetectionRefreshing() {
|
|
1281
|
+
return inFlightDetection !== null || Date.now() < employeeDetectionRefreshSignalUntilMs;
|
|
1282
|
+
}
|
|
1161
1283
|
/**
|
|
1162
1284
|
* Non-blocking employee detection. Probes every agent CONCURRENTLY, so the cost is the
|
|
1163
1285
|
* slowest single probe rather than the sum of all of them, and the event loop stays free
|
|
@@ -1677,24 +1799,29 @@ function parseHostLine(hostId, line) {
|
|
|
1677
1799
|
return {};
|
|
1678
1800
|
// Scan every line for structured signals the Hub UI cares about:
|
|
1679
1801
|
// seekMentoring (tracker), get_fraim_job (job identity promotion),
|
|
1680
|
-
// turn-level usage (totals),
|
|
1681
|
-
// (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).
|
|
1682
1805
|
const seekMentoring = parseSeekMentoringSignal(trimmed);
|
|
1683
1806
|
const fraimJob = parseFraimJobLoadSignal(trimmed);
|
|
1807
|
+
const executionMode = parseExecutionModeSignal(trimmed);
|
|
1684
1808
|
const usage = parseUsageSignal(trimmed);
|
|
1685
1809
|
const agentIdentity = parseAgentIdentitySignal(trimmed);
|
|
1810
|
+
const recurringPark = parseRecurringParkSignal(trimmed);
|
|
1686
1811
|
// Issue #1264 (R3): only worth checking when seekMentoring itself did not
|
|
1687
1812
|
// already resolve — a recognized call has nothing to diagnose.
|
|
1688
1813
|
const unrecognizedReviewHandoffError = seekMentoring ? null : detectUnrecognizedReviewHandoffCall(trimmed);
|
|
1689
1814
|
const withSignal = (event) => {
|
|
1690
|
-
if (!seekMentoring && !fraimJob && !usage && !agentIdentity && !unrecognizedReviewHandoffError)
|
|
1815
|
+
if (!seekMentoring && !fraimJob && !executionMode && !usage && !agentIdentity && !recurringPark && !unrecognizedReviewHandoffError)
|
|
1691
1816
|
return event;
|
|
1692
1817
|
return {
|
|
1693
1818
|
...event,
|
|
1694
1819
|
...(seekMentoring ? { seekMentoring } : {}),
|
|
1695
1820
|
...(fraimJob ? { fraimJob } : {}),
|
|
1821
|
+
...(executionMode ? { executionMode } : {}),
|
|
1696
1822
|
...(usage ? { usage } : {}),
|
|
1697
1823
|
...(agentIdentity ? { agentIdentity } : {}),
|
|
1824
|
+
...(recurringPark ? { recurringPark: true } : {}),
|
|
1698
1825
|
...(unrecognizedReviewHandoffError && !event.hostError ? { hostError: unrecognizedReviewHandoffError } : {}),
|
|
1699
1826
|
};
|
|
1700
1827
|
};
|
|
@@ -2140,6 +2267,11 @@ class CliHostRuntime {
|
|
|
2140
2267
|
detectEmployeesAsync() {
|
|
2141
2268
|
return detectEmployeesAsync();
|
|
2142
2269
|
}
|
|
2270
|
+
// Issue #1256 (slice b): lets bootstrapResponse tell the client whether the roster it just
|
|
2271
|
+
// served may be stale while a background re-probe lands.
|
|
2272
|
+
isEmployeeDetectionRefreshing() {
|
|
2273
|
+
return isEmployeeDetectionRefreshing();
|
|
2274
|
+
}
|
|
2143
2275
|
startRun(hostId, projectPath, message, handlers, sessionId, launchContext) {
|
|
2144
2276
|
// R11: start/startDirect mint sessions rather than resuming one, so they
|
|
2145
2277
|
// stay outside the continuation queue entirely.
|